背景
现在的大型互联网站系统都是基于SOA或者微服务架构设计的,系统之间通过远程服务调用或者异步消息等方式进行交互。分布式系统的环境非常复杂,网络抖动或者服务端系统响应慢都有可能造成重复的服务调用或者消息的重发,当服务端对于请求的响应或者消息的处理涉及到数据的变更时,可能会造成极大的危害。重复的请求或消息的后果在支付或者账务等系统中尤为严重,我们在设计服务端系统地时候,需要进行幂等控制。
何为幂等
幂等概念来自数学,表示N次变换和1次变换的结果是相同的。在我们的讨论的场景下指,客户端在通过同步服务调用或者异步消息与服务端交互而没有达到预期结果时,可能会进行多次交互,服务端为避免多次重复地处理而对服务资源产生地不良影响,必须要对同一请求的多次调用进行幂等控制。
业务开发中,经常会遇到重复提交的情况,无论是由于网络问题无法收到请求结果而重新发起请求,或者是服务端系统响应慢地情况。在支付或者账务等系统这种重复提交造成的问题有尤其明显,比如:向支付宝发起支付请求,由于网络问题或系统BUG重发,支付宝应该只扣一次钱。很显然,声明幂等的服务认为,外部调用者会存在多次调用的情况,为了防止外部多次调用对系统数据状态的发生多次改变,将服务设计成幂等。
幂等控制的解决方案
服务端系统对服务调用或者异步消息地处理,总结为就是对数据资源的增、删、改、查,针对这四种情况,我们来看一下如何进行幂等控制。
查询
查询操作是天然幂等的,对数据库而言它不会涉及到数据的更改,以下语句:select balance from account where account_no = '666666'
,不管执行多少次都不会对数据表中的数据造成影响。
删除
删除操作虽然涉及到数据表的变动,但在某些情况下它也是幂等的,比如下面根据订单号(支付系统中同一个订单号不会生成两次,一般的做法都是时间+sequence方式生成)删除订单的操作:delete from order where order_id = '2017011312345678'
,不管执行一次还是多次,在数据表上体现地变化都是一样(虽然可能对客户端来讲返回结果不一样,当账户存在时,第一次删除返回1,后面的删除操作都返回0)。设计服务端数据模型时,要避免删除的数据再次生成(每个字段都一样),否则多次执行删除有可能造成不利影响(把别人刚生成的数据又删掉了)。
新增
新增地数据中一个或多个字段能够唯一识别这条数据,那么我们在设计数据表时将其设置为主键或者唯一索引或者唯一组合索引,例如账号就是账户表中的主键,在创建账户时,如果出现重复创建的情况,在账户表中插入数据地时候,会出现幂等冲突,数据库的存储机制帮我们控制了幂等;
修改
- 利用去重表控制幂等,例如,在SOA架构下支付系统发起分布式事务同步调用账务时,账务首先是插入事务表,事务号transactionId是事务表的主键,然后在进行账户表等其他数据表的修改操作。由于事务号是唯一的,所以重复地调用会通过被事务表幂等住;
- 同样地,系统之间通过异步消息进行交互时,当订阅方系统响应慢时,消息中心很有可能对同一条消息进行重发,这个时候订阅方在根据消息内容进行业务处理之前,可以先将消息插入自己的消息表(消息的messageId是唯一的),以此来防止同一个消息(同样的messageId)的重复处理。
- select+update的方式幂等,例如在进行同步调用或者消息处理地时候,我们可以根据调用参数或者消息内容与与从db中查询出来数据的时间字段进行比较,如果数据库中查询出来的数据进行比较如果数据一致,则进行幂等控制。
- 乐观锁的方式,update with condition,我们在更新数据表中的数据时候,可以利用版本号或者时间戳字段(时间精度足够高)做乐观锁:
update order set status = 'C', version=version+1 where order_id = '2017011312345678' and version = #version#
或者
update accout set balance = 1000, trans_dt = #trans_dt# where account_no = '666666' and trans_dt < #trans_dt#
需要注意的是,在使用乐观锁做更新操作时,最好用主键或者唯一索引来更新,这样是行锁,否则更新时会锁表,锁表在高并发的环境下是灾难性的。
总结
幂等性使我们在设计系统时需要着重考虑的问题,尤其是涉及到重要数据处理的时候,比如支付或者账务系统的订单、账户都能数据,既要满足高并发的需要,又要准确处理每次请求,不能出现重复订单或者记错账的情况。