1 幂等场景
- 用户重复操作:用户在使用产品时,可能会无意的触发多笔交易,甚至没有响应而有意触发多笔交易
- 网络波动:因网络波动,可能会引起重复请求
- 分布式消息消费:任务发布后,使用分布式消息服务来进行消费
- 未关闭的重试机制:因开发人员、测试人员或运维人员没有检查出来,而开启的重试机制(如Nginx重试、RPC通信重试或业务层重试等)
2 幂等性分析
- 新增类请求
数据库自增主键,不具备幂等性 - 查询类动作
重复查询不会产生或变更新的数据,因此查询是天然具备幂等性 - 更新类请求
基于条件查询的Update,不一定具有幂等性(需要根据实际情况进行分析判断) - 删除类请求
基于主建的Delete具备幂等性
一般业务层面都是逻辑删除(即Update操作),而基于主键的逻辑删除操作也是具有幂等性的
3 幂等重要性
- 电商超卖现象
- 重复转账、扣款或付款
- 重复增加金币、积分或优惠券
超卖现象
比如某商品的库存为1,此时用户1和用户2并发购买该商品,用户1提交订单后该商品的库存被修改为0,而此时用户2并不知道的情况下提交订单,该商品的库存再次被修改为-1这就是超卖现象。
究其深层原因,是因为数据库底层的写操作和读操作可以同时进行,虽然写操作默认带有隐式锁(即对同一数据不能同时进行写操作)但是读操作默认是不带锁的,所以当用户1去修改库存的时候,用户2依然可以都到库存为1,所以出现了超卖现象。
解决方案A:可以对读操作加上显式锁(即在select …语句最后加上for
update)这样一来用户1在进行读操作时用户2就需要排队等待了。但问题来了,如果该商品很热门并发量很高那么效率就会大大的下降,如何解决呢?(解决方案B)
解决方案B:我们可以有条件有选择的在读操作上加锁,比如可以对库存做一个判断,当库存小于一个量时开始加锁,让购买者排队,这样一来就解决了超卖现象。
4 幂等性设计不能脱离业务来讨论
- 状态机控制
这种方法适合在有状态机流转的情况下,比如就会订单的创建和付款,订单的付款肯定是在之前,这时我们可以通过在设计状态字段时,使用int类型,并且通过值类型的大小来做幂等,比如订单的创建为0,付款成功为100,付款失败为99。 - 去重表
这种方法适用于在业务中有唯一标的插入场景中,比如在以上的支付场景中,如果一个订单只会支付一次,所以订单ID可以作为唯一标识。这时,我们就可以建一张去重表,并且把唯一标识作为唯一索引,在我们实现时,把创建支付单据和写入去去重表,放在一个事务中,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。 - 插入或更新
这种方法插入并且有唯一索引的情况,比如我们要关联商品品类,其中商品的ID和品类的ID可以构成唯一索引,并且在数据表中也增加了唯一索引。这时就可以使用InsertOrUpdate操作。 - 全局唯一ID
如果使用全局唯一ID,就是根据业务的操作和内容生成一个全局ID,在执行操作前先根据这个全局唯一ID是否存在,来判断这个操作是否已经执行。如果不存在则把全局ID,存储到存储系统中,比如数据库、Redis等。如果存在则表示该方法已经执行。
使用全局唯一ID是一个通用方案,可以支持插入、更新、删除业务操作。但是这个方案看起来很美但是实现起来比较麻烦,下面的方案适用于特定的场景,但是实现起来比较简单。
- 多版本控制
这种方法适合在更新的场景中,比如我们要更新商品的名字,这时我们就可以在更新的接口中增加一个版本号,来做幂等:
实战解决方案
- 验证颗粒度小、框架层、业务层零侵入:filter、拦截器不ok,业务层注解AOP
- 过滤重复请求:AOP环绕通知,前置通知检查key存在性、后置通知释放key,key已存在过滤请求
- 并发请求:多线程查询key、创建key不ok,利用redis单线程+保证key操作原子性,引入分布式锁
- key释放的原子操作:释放只能释放自己线程的key,发生异常要在finaly中释放,引入redis事务,watch监听key
- 极端情况:正常业务耗时,而key过期了;redis主从或者集群,master节点崩溃,slave节点未升级,数据同步未成功造成数据丢失。引入redisson分布式java解决方案,定时key续约,集群数据分布式内存网格存储