1、什么是幂等?
一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。这里的意思就是用户对于同一种操作发起的一次或者多次请求,其结果状态是一致的,不会因为多次请求而产生了其他影响。
2、为什么需要幂等
引用一个经典的例子,那就是支付。用户购买商品支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了。用户再次点击按钮,假设这里的请求没有做幂等处理,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条。而实际期望的是,一笔订单只进行一次扣款。
3、常见的幂等控制实现方式有哪些?
>>>>使用数据库操作做幂等:
- 查询操作(select是天然的幂等操作,操作前置select状态,查询一次和查询多次,在数据不变的情况下,查询结果是一样的。这种处理方式只适合没有并发的情况,操作前,先查询是否被处理过,防止过多的写操作及异常。并发容易出现ABA问题,需要结合版本号。也可以使用select+insert方案,但仅适合在一些小型或者并发不高的项目。)
- 删除操作(删除操作也是幂等的,删除一次和多次删除都是把数据删除。注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个)
- 唯一索引unique index(通过指定数据库创建唯一索引或者组合唯一索引,都可以避免脏数据,保证幂等,但是注意处理异常。处理前,先前置插入数据到db,捕获Duplicate Exception判断是否重复处理。插入:比如在以上的支付场景中,如果一个订单只会支付一次,所以订单ID可以作为唯一标识。这时,我们就可以建一张去重表,并且把唯一标识作为唯一索引,在我们实现时,把创建支付单据和写入去去重表,放在一个事务中,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。更新或插入:
insert into goods_category (goods_id,category_id,create_time,update_time) values(#{goodsId},#{categoryId}, now(), now()) on DUPLICATE KEY UPDATE update_time= now()
update goods set name=#{newName}, version=version+1 where id=#{ id} and version<${ version}
)
- 悲观锁(获取数据,用的不多,容易锁表。
select * from table_xxx where id='xxx' for update;
>>>>使用三方做幂等:设计全局唯一ID
- 分布式锁(数据处理在分布式环境下一向是很很麻烦的,因为Synchronize和ReentrantLock在分布式环境下是达不到并发锁的效果的。所以分布式锁,一般都采用第三方,例如,redis,zookeeper,就好比,去第三方认证获取一个permit,处理完了就去注销这个permit,确保同一时刻,只有一个能执行成功
分布式环境下,经常还需要考虑一个全局唯一索引的问题,常用解决方案如下:
- 雪花算法
- 令牌桶算法
- 漏桶算法
- 自定义唯一索引生成器)
- token机制(围绕着三方面问题:
- 如何保证请求接口响应结果的一致性;
- 如何防止恶意请求或攻击;
- 分布式环境下如何保证请求接口的响应结果;
可以选择token+redis方案来实现:
- 请求之前,先向服务器申请一个token,服务器缓存token到redis,加上超时时间;
- 请求的时候带上token;
- 服务器从redis获取token校验,通过后处理、响应、删除token;未通过校验,则提示token无效;
注意:
- token在redis上设置的超时时间控制合理,防止恶意请求;
- 服务器校验token时,采用redis.delete操作,直接使用get+delete规避并发带来的问题;
- token一定程度上可以限流;
- 状态机(如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。在幂等的处理机制中,状态机是个很有用的东东,已经确定的状态不会回滚处理,比如订单业务里面,订单是已支付状态,再来这个订单的支付请求,肯定是不行的嘛,怼回去。很多场景或者系统中都用到状态机,所以设计合理的状态机制,是很有用且很必要的。这种方法适合在有状态机流转的情况下,比如就会订单的创建和付款,订单的付款肯定是在之前,这时我们可以通过在设计状态字段时,使用int类型,并且通过值类型的大小来做幂等,比如订单的创建为0,付款成功为100。付款失败为99。在做状态机更新时,我们就这可以这样控制
update `order` set status=#{ status} where id=#{ id} and status<#{ status}
)
4、什么时候该使用幂等?
一般需要幂等的场景是写场景,并非所有场景下我们都需要幂等设计,只有一些数据敏感、需要保证强一致性的场景下才会需要。
当某个业务流程存在被重复处理的场景,且要求该业务流程无论被执行多少次,最终结果都要和只处理一次时的状态保持一致(请求的次数不影响最终处理状态),这时就需要幂等。
5、使用场景
- 前端相同请求表单被重复提交。例如:①提交按钮被用户多次点击;②请求超时但实际处理成功,前端重试;
- 上层业务重复调用。例如:由于上层业务逻辑的不合理,重复调用底层服务处理。
- 请求超时重试。例如:rpc调用超时重试、代理层超时重试、中间件客户端超时重试等。
- 消息重复消费。例如:mq不保证消息唯一,可能重复推送;消息处理ack超时重复推送等。
- 任务调度中心重复调度。例如:定时任务、延时任务回调时,单个任务被多次重复调用。
- 单个请求业务处理时,部分流程异常。例如:处理单个请求时,步骤1成功、步骤2异常,步骤1未回滚,第二次请求时,步骤1仍会重复处理。
- 网络报文重发。一般极少出现,server端正常收到报文,但client端未收到ack,于是client端进行重发。