一、背景
在实际系统操作中,因为操作人员的误操作,或者网络抖动等一系列原因导致一个接口请求多次,或者一个业务执行多次情况。但是不管操作多少次,产生的操作结果应该都一致。
例如1:用户发起一笔支付,当遇到网络重发,系统bug重试,只允许扣除用户一次钱。
2 :创建业务数据,一次请求只能创建一个,创建多个就会出大问题。
3:发送消息给用户,不能因为业务操作问题发送重发信息给用户,否则用户会奔溃的。
二、幂等性概念
幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。
我的理解:幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的。
三、幂等常用思路
token机制
当客户端请求页面时,服务器会生成一个随机数token,并且将token放置到session当中,然后将token发给客户端(一般通过构造hidden表单)。下次客户端提交请求时,token会随着表单一起提交到服务器端。服务器端第一次验证相同过后,会将session中的token值更新下,若用户重复提交,第二次的验证判断将失败,因为用户提交的表单中的token没变,但服务器端session中token已经改变了。
乐观锁(通过版本号实现)
update table_xxx set name=#name#,version=version+1 where version=#version#;通过条件限制 update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0要求:quality-#subQuality# >= ,这个情景适合不用版本号,只更新是做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高;
去重表
利用数据库表单的特性来实现幂等,常用的一个思路是在表上构建唯一性索引。需求是博客点赞问题,要想防止一个人重复点赞,可以设计一张表,将博客id与用户id绑定建立唯一索引,每当用户点赞时就往表中写入一条数据,这样重复点赞的数据就无法写入。
我们可以借鉴数据库的乐观锁机制来举个例子
1、首先为表添加一个版本字段version
2、在执行更新操作前呢,会先去数据库查询这个version
3、然后执行更新语句,以version作为条件,例如:
UPDATE T_REPS SET COUNT = COUNT -1,VERSION = VERSION + 1 WHERE VERSION = 1
4、如果执行更新时有其他人先更新了这张表的数据,那么这个条件就不生效了,也就不会执行操作了,通过这种乐观锁的机制来保障幂等性。
消费端-幂等性保障
什么情况下会出现重复消费?
当消费者消费完消息时,在给生产端返回ack时由于网络中断,导致生产端未收到确认信息,该条消息会重新发送并被消费者消费,但实际上该消费者已成功消费了该条消息,这就是重复消费问题。
如何避免消息的重复消费问题?
消费端实现幂等性,就意味着,我们的消息永远不会消费多次,即使我们收到了多条一样的消息
业界主流的幂等性操作:
· 唯一ID + 指纹码机制,利用数据库主键去重
· 利用Redis的原子性去实现
唯一ID+指纹码机制
· 唯一ID + 指纹码机制,利用数据库主键去重
· SELECT COUNT(1) FROM T_ORDER WHERE ID = 唯一ID +指纹码
· 好处:实现简单
· 坏处:高并发下有数据库写入的性能瓶颈
· 解决方案:跟进ID进行分库分表进行算法路由
整个思路就是首先我们需要根据消息生成一个全局唯一的ID,然后还需要加上一个指纹码。这个指纹码它并不一定是系统去生成的,而是一些外部的规则或者内部的业务规则去拼接,它的目的就是为了保障这次操作是绝对唯一的。
将ID + 指纹码拼接好的值作为数据库主键,就可以进行去重了。即在消费消息前呢,先去数据库查询这条消息的指纹码标识是否存在,没有就执行insert操作,如果有就代表已经被消费了,就不需要管了。
对于高并发下的数据库性能瓶颈,可以跟进ID进行分库分表策略,采用一些路由算法去进行分压分流。应该保证ID通过这种算法,消息即使投递多次都落到同一个数据库分片上,这样就由单台数据库幂等变成多库的幂等。
利用Redis的原子性去实现
我们都知道redis是单线程的,并且性能也非常好,提供了很多原子性的命令。比如可以使用 setnx 命令。
在接收到消息后将消息ID作为key执行 setnx 命令,如果执行成功就表示没有处理过这条消息,可以进行消费了,执行失败表示消息已经被消费了。
使用 redis 的原子性去实现主要需要考虑两个点
· 第一:我们是否要进行数据落库,如果落库的话,关键解决的问题是数据库和缓存如何做到原子性?
· 第二:如果不进行落库,那么都存储到缓存中,如何设置定时同步的策略(同步到关系型数据库)?缓存又如何做到数据可靠性保障呢
关于不落库,定时同步的策略,目前主流方案有两种,第一种为双缓存模式,异步写入到缓存中,也可以异步写到数据库,但是最终会有一个回调函数检查,这样能保障最终一致性,不能保证100%的实时性。第二种是定时同步,比如databus同步。
1.使用redis的setnx命令的情况下,如果消费者端setnx成功后,进行消息消费,但是此时突然宕机。那么对于接下来一段时间内(指代锁的有效时长),就无法保证消息的及时消费?
答:首先宕机问题要尽量避免,通过一些高可用的方案降低宕机的风险,如果确实宕机了,对于已发送但未被消费的消息,可以自己去做补偿或者投递到延迟队列里处理,宕机会造成生产端消息堆积,如果对消息实时处理要求比较高,需要提前预备一些应急方案另起服务去处理这些消息。
2.redis的setnx怎么做幂等性的? 锁的有效时长设为多少呢
redis实现幂等很简单,我以redis实现接口的幂等性为例说明。你可以自定义一个幂等注解,然后配合AOP进行方法拦截,对拦截的请求信息(包括方法名+参数名+参数值)根据固定的规则去生成一个key,然后调用redis的setnx方法,如果返回ok,则正常调用方法,否则就是重复调用了。这样可以保证重复请求接口在一定时间内只会被成功处理一次。至于锁的有效时长要根据业务情况而定的。