概念
重复请求是指一个请求因为某些原因被多次提交, 场景简述如下:
1)用户快速多次点击按钮
2)Nginx 失败重试机制
3)服务框架失败重试机制
4)MQ 消息重复消费
5)第三方支付支付成功后, 因为异常原因导致的多次异步回调;
幂等性是指同样的请求参数, 多次请求返回的结果相同. 一般是因为重复请求导致的重复操作等, 但重复请求不只包含并发时的重复请求还包括并并发情况下的业务重试.
基本原理
实现幂等需要两个条件 1, 同一请求参数(并发请求或非并发请求);2, 多次请求返回的结果一致. 一般大家讲的都是并发情况下的, 使用并发控制解决, 但还有一点是要满足返回的结果一致, 这个一般根据场景来定, 是返回相同结果还是返回失败.
发生原因
1)分布式系统中网络的三态性: 成功, 失败, 未知, 未知时一般三方系统会定期重试.
2)用户重复提交或系统重试机制导致的多次请求;
常见解决方案
解决思路: 并发控制 + 返回相同结果
分类: 按是否更改数据可以把接口分为查询类接口和更新类接口, 查询类接口天然支持幂等, 因此幂等性主要是解决更新类接口幂等.
1)唯一索引
描述: 比如订单号做唯一索引, 同一订单号只能插入一条记录;
应用场景: 适用于单库单表的新增场景.
2)Select+[Insert/Update]
描述: 先进行查询, 根据查询结果判断是否符合更新条件, 符合则更新;
应用场景: 因为两条 Sql 非原子操作, 适合并发量不高的新增或修改场景.
3)数据库乐观锁
描述: 根据某一字段做为更新条件, 如何不满足, 则更新失败, 比如状态字段或增加自增版本号字段.[版本号字段可以解决 ABA 问题]
应用场景: 适合非高并发的更新, 且有版本控制字段的场景. 如果高并发更新, 评估利弊后可使用悲观锁.
4)防重 Token
描述: 页面加载时, 先请求服务端返回防重 Token, 用户提交时将 token 一起提交到服务端, 服务端判断 token 是否存在, 存在则执行, 不存在则异常处理.[可根据业务规则是更新 token 的状态值还是直接删除 token 来标识已处理过]
应用场景: 适用于没有唯一性字段的添加或修改类场景.
5)防重表
描述: 基于数据库的方式进行并发控制, 此表通过唯一字段 + 唯一索引来保障不重复处理数据.
应用场景: 简单分布式情况下对添加或修改类场景, 进行并发或防重控制(也适用于老系统不想新增并发控制字段, 统一进行并发字段存储的场景).[复杂分布式因为请求量或数据量太大, 超过了单表的限制, 此时防重表可能存在出错的情况]
6)分布式锁
描述: 以唯一字段作为 key 进行加锁, 请求处理时先判断是否有锁, 无锁则先加锁再处理逻辑, 重复请求因为已经加锁, 则说明重复, 则不处理.
场景: 适合分布式高并发场景或不适用其它方式的场景, 比如发验证短信 60 秒控制, 因为控制信息是记录在缓存中的, 无法使用乐观锁等方式, 因此只能使用分布式锁.
小结: 解决方案的核心是根据资源 (数据) 的唯一性或唯一条件进行并发控制.
应用场景举例
以订单流程为例, 介绍下幂等实现的常规解决方案.
订单流程:
1, 用户提交订单 ->待支付
2, 用户付款成功 ->待出库
3, 商品出库 ->等待收货
4, 买家收货 ->完成
其中: 提交订单为添加类接口, 付款成功, 商品出库, 等待收货, 完成为修改类接口.
1)订单提交 ->待支付
单机环境: 订单号唯一索引或 Select+Insert
分布式环境: Redis 分布式锁或防重 token 或防重表
2)用户付款成功 ->待出库, 商品出库 - 等待收货, 买家收货 ->完成
单机环境: 乐观锁
分布式环境: Redis 锁或防重 token 或防重表或 Select+Update 或乐观锁
降级方案
分布式锁 + 唯一索引或乐观锁
分布式环境下, 1)如果只加分布式锁可能会存在锁失效的情况, 2)业务层锁控制后, 数据操作服务可能会超时重试; 因此, 依旧需要有唯一索引或数据库乐观锁来进行并发控制, 保障最后一道防线.
文章小结
本文对重复请求和需要幂等控制的场景进行了介绍, 并讲解了常见的解决方案, 最后以订单流程为例介绍了方案的具体应用. 希望对大家在防重和幂等控制设计上有帮助, 不足之处, 请批评指正, 欢迎一起交流讨论.
来源: https://www.cnblogs.com/itfly8/p/10648084.html