一、前言
最近遇到很多业务在前期告诉迭代过程中,忽略很多幂等性处理,这都是技术债务,趁此机会,梳理一下业界常用的幂等性方案。
二、场景
什么是幂等性呢?
这里其实说的是:系统中有很多操作,是不管做多少次,都应该产生一样的效果或返回一样的结果。
例如 :
-
前端重复提交选中的数据,应该后台只产生对应这个数据的一个反应结果;
这种情况开发过程中,经常遇到,用户点击按钮没反应,再次点击。
秒杀类似的场景,用户疯狂在一个时刻多次点击。ps 当然前端可以加锁。 -
我们发起一笔付款请求,应该只扣用户账户一次钱,当遇到网络重发或系统bug重发,也应该只扣一次钱;
-
同步消息,同样的消息只处理一次。
这种情况大家不要小瞧,实际中经常遇到:
本身消息系统保证高可靠,针对发送、接收超时都会要求重发。
消息系统服务器故障时,大量消息重发。
业务某些原因修复数据,需要回溯之前的消息。 -
创建业务订单,一次业务请求只能创建一个,创建多个就会出问题。
很多很多重要的情况都需要幂等的特性来支持。
三、幂等性概念
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。 在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。
四、常用方案
唯一索引 :
即数据库唯一索引或者唯一组合索引,用来防止重复数据,这种幂等方式是非常严格的,一般做法是:
操作数据时,先从数据库查询是否存在该记录,不存在则插入,插入成功,继续进行接下来操作。
当然即使当时并发度极高,或者数据库主从延迟,导致有些没有查到,继续插入,数据库层直接抛 dumpli key 异常。存在则直接返回。
比如:支付宝的资金账户,支付宝也有用户账户,每个用户只能有一个资金账户,怎么防止给用户创建资金账户多个,那么给资金账户表中的用户ID加唯一索引,所以一个用户新增成功一个资金账户记录。
token机制 :
集群环境采用token加redis(redis单线程的,处理需要排队);
单JVM环境:采用token加redis或token加jvm内存。
一般流程:1. 数据提交前要向服务的申请token,token放到redis或jvm内存,token有效时间;
2. 提交后后台校验token,同时删除token,生成新的token返回。token特点:要申请,一次有效性,可以限流。
其实就是类似于redis的分布式锁,这里也很多坑,大家可以看下这个:redis深水区经验梳理 redis的分布式锁的坑都在里面。
悲观锁 :
获取数据的时候加锁获取。select * from table_xxx where id=‘xxx’ for update;
注意:id字段一定是主键或者唯一索引,不然是锁表。
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用;
其实高并发业务下,极少使用,除非流量比较低
乐观锁 :
乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高。乐观锁的实现方式多种多样可以通过version或者其他状态条件:
- 通过版本号 :
update bx_tablex set name=#name#,version=version+1 where version=#version#
- 通过条件限制 :
update bx_tabley set bx_amount= bx_amount - #subAmount# where bx_amount-#subAmount# >= 0
要求:quality-#subQuality# >= ,这个情景适合不用版本号,只更新是做数据安全校验,比如用在库存模型上,扣份额和回滚份额,性能更高;
注意:乐观锁的更新操作,最好用主键或者唯一索引来更新,这样是行锁,否则更新时会锁表,修改sql如下
update bx_tablex set name=#name#,version=version+1 where id=#id# and version=#version#;
update bx_tabley set bx_amount= bx_amount - #subAmount# where id=#id# and bx_amount-#subAmount# >= 0
分布式锁:
分布式系统构建全局唯一索引比较困难,例如唯一性的字段没法确定。
分布式锁其实通过第三方的系统(redis或zookeeper或etcd),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。
状态机幂等 :
在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。注意:订单等单据类业务,存在很长的状态流转,一定要深刻理解状态机,对业务系统设计能力提高有很大帮助
五、幂等时长
这里,我专门提一下这个事情,很多时候,我们防重,其实就是调用接口的那一刻,或者接消息的那一刻,但是超时重试,用户多点击,这种情况,我们可以存储幂等键key到redis 5分钟自动过期,能解决大部分场景。
如果你的业务时刻都要防重,那么你的幂等就需要永久存储起来,成本很高。笔者所在公司曾经遇到过消息系统故障,重发了一天的消息。。。。
总之,跟随你的业务而定,幂等也是分很多种情况,大家择优选择。
六、后续
这是业界常见的幂等方案,但不是全部,如果你遇到过更多的幂等性问题,或者更高性能的方案,欢迎跟我留言。