分布式环境下的幂等控制
简介
接口的幂等性:相同的请求参数,多次请求结果一致。相同请求无论发起多少次,服务器只会处理一次,或者处理多次结果一样
需要幂等的场景
查询接口
下单时防止重复请求
内部服务异常重试等等
如何保证幂等
幂等首先是需要分辨出是否为重复请求。假如是简单的select查询,是天然的幂等,不需要额外处理。
其他情况比如下单业务,可以使用唯一业务编号(订单号)来保证幂等性,或者使用额外字段来专门控制,比如nonce值,timestamp字段。支付宝的支付相关接口就是利用这种方法
以支付为例,在不考虑并发的情况下的简单实现:
1、先用订单号查询订单是否支付过;2、支付过则直接返回,没有支付则走正常支付流程
但是在高并发的情况下这种实现不能保证原子性,第2步会先于第一步执行
分布式下的幂等实现
乐观锁
以mysql的乐观锁来举例
mysql乐观锁
表格增加version版本字段,更新时增加版本控制,要控制版本号一次递增。在加乐观锁时必须要加上主键索引,防止表锁
Update table set version = version +1 where version < 新版本号
或者增加一个字段,使用时间戳来控制版本
或者使用业务状态字段status来控制。比如订单状态1下单预处理->2下单成功待发货->3已发货待签收->4签收完成,状态之间是有线性关系的,需要修改订单状态必须要是在特定的状态下才能修改
使用何种方式需要依据具体业务分析
不过,乐观锁存在失效的情况,就是常说的ABA问题,不过如果version版本一直是自增的就不会出现ABA的情况。
防重表
使用订单号orderNo做为去重表的唯一索引,每次请求都根据订单号向去重表中插入一条数据。第一次请求查询订单支付状态,当然订单没有支付,进行支付操作,无论成功与否,执行完后更新订单状态为成功或失败,删除去重表中的数据。后续的订单因为表中唯一索引而插入失败,则返回操作失败,直到第一次的请求完成(成功或失败)。可以看出防重表作用是加锁的功能。
数据库唯一索引
可以在唯一业务编号字段上增加唯一索引来控制重复数据插入
以上是通过数据库建立乐观锁来实现幂等性。一般实际业务中需要配合多种方案来保证幂等,比如前置加分布式锁、token令牌,避免无效请求到达数据库
分布式锁
这里使用的防重表可以使用分布式锁代替,比如Redis。订单发起支付请求,支付系统会去Redis缓存中查询是否存在该订单号的Key,如果不存在,则向Redis增加Key为订单号。查询订单支付已经支付,如果没有则进行支付,支付完成后删除该订单号的Key。通过Redis做到了分布式锁,只有这次订单订单支付请求完成,下次请求才能进来。相比去重表,将放并发做到了缓存中,较为高效。思路相同,同一时间只能完成一次支付请求。
token令牌
这种方式分成两个阶段:申请token阶段和支付阶段。
第一阶段,在进入到提交订单页面之前,需要订单系统根据用户信息向支付系统发起一次申请token的请求,支付系统将token保存到Redis缓存中,为第二阶段支付使用。
第二阶段,订单系统拿着申请到的token发起支付请求,支付系统会检查Redis中是否存在该token,如果存在,表示第一次发起支付请求,删除缓存中token后开始支付逻辑处理;如果缓存中不存在,表示非法请求。
实际上这里的token是一个信物,支付系统根据token确认,你是你妈的孩子。不足是需要系统间交互两次,流程较上述方法复杂。
token令牌的方式可以防止同一个请求被发起两次,但是不能保证相同业务参数请求被发起两次,也就是防止不了恶意请求,此时需要配合分布式锁来控制相同参数的请求频率,例如对用户id进行加分布式锁,必须要该订单处理完成才可以发起下一次请求。
对于重复请求前端也可以做一些处理,比如按钮按下后增加限制,必须等待接口返回才恢复
支付缓冲区
把订单的支付请求都快速地接下来,一个快速接单的缓冲管道。后续使用异步任务处理管道中的数据,过滤掉重复的待支付订单。
优点是同步转异步,高吞吐。不足是不能及时地返回支付结果,需要后续监听支付结果的异步返回。
幂等性接口的不足
1. 增加了额外控制幂等的业务逻辑,复杂化了业务功能;
2. 把并行执行的功能改为串行执行,降低了执行效率。
因此除了业务上的特殊要求外,尽量不提供幂等的接口。
参考