目录
什么是幂等
数学中:在一次元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同;在二次元运算为幂等时,自己重复运算的结果等于它自己的元素。
计算机学中:幂等指多次操作产生的影响只会跟一次执行的结果相同,通俗的说:某个行为重复的执行,最终获取的结果是相同的,不会因为重复执行对系统造成变化。
为什么需要幂等性
幂等性一般发生在BS架构中,CS架构可以通过按钮的disable控制不重复提交。BS架构中由于网络延迟,页面交互,MQ重复消费,页面回退重复提交,微服务接口重试机制,功能上一个人只能点赞一次等问题导致架构上必须有一定机制来保证功能上的幂等性。
常用幂等性场景:
- 下订单:网络传输数据和处理逻辑导致的下单到跳转订单页面有一定时延,如果交互上不特意控制,很容易重复下单;BS架构前后端是以http交互,如果模拟请求重复提交很容易导致生成多个订单,从而影响后续。
- 提交表单:类似下订单,只不过判重的依据有区别。提交表单指的是添加数据到服务器,下订单技术原理上跟提交表单一直,但功能上是后端根据“下订单”操作生成订单数据。
- 页面回退重复提交:提交成功后,跳转到success页面,但你点击“回退”,页面跳转到提交页面并重复提交数据,这在交互或者技术上也需要保证。要不不能回退,要不接口需要幂等性不能点击一个回退即生成一个订单。
- 抽奖点赞:一个用户可以抽奖2次,一个用户可以对一个动态点赞一次。这种功能各异,但需要在技术上保证,不能因为网络延迟或者用户重复点击导致一个用户可以抽奖>2次,判重逻辑需要根据场景确定。
- 消息重复消费:BS后端分布式架构中会经常使用MQ用来解耦和削峰填谷,但没有万无一失的MQ,所以就需要在消费者逻辑中保证及时重复消息也需要可以被正确处理,比如:重复加载数据的命令,已加载过就不应该重复加载,重复命令消息即应该丢弃。
- 微服务接口重试机制:分布式微服务RPC框架都有重试机制,即:客户端认为调用失败后重复调用的尝试。如果调用成功,服务端已在运行逻辑,网络抖动,客户端判定失败并重试。
幂等性分类
接口幂等性: Http,Rpc接口等
消息幂等性:对MQ消息消费
功能幂等性:控制操作次数
幂等性技术保证手段
前端交互控制
在点击“提交”后,首先将按钮Disable掉,或者弹出“加载中…”框,并设置为模态modal对话框乐观锁控制。
在请求返回,或者异常后解除disable和modal。
无论什么功能,交互上需要控制,将一切操作建立在用户可能瞎操作的基础上。
分布式锁控制
分布式锁只能重置手抖重复提交场景,原因是分布式锁只能控制“同一时间段只能有一个客户来操作某个订单”场景,并且设置重复请求等待策略为:“直接返回”。
类似,比如给一个帖子添加评论,回退重复提交场景,因为两次操作是有长时间的空隙的,并没有并行,所以这类手段就不行了。
数据库唯一约束保证幂等性
类似给帖子点赞场景,要求:一个用户只能点击一次赞。
实现:场景一个表like(帖子ID,点赞用户ID),并设置为联合主键
这样就保住重复插入会报错,如果insert前查询一下是否已点赞就更好了。
点评:用点赞场景只是来说明“数据库唯一约束”保住幂等性的逻辑,实际点赞场景用数据库是扛不住的,更为实际的方案是使用redis来保证。
使用数据库锁保证
数据库锁有悲观锁和乐观锁区别:
悲观锁:select * from t from update;
乐观锁:update t set name= 1 where id=11 and version = 12;
业务上的悲观锁和乐观锁区别:
悲观锁:我准备提交的时候,保证所有涉及业务数据属于锁定状态。
乐观锁:只有已提交未支付的订单才可以支付,状态status字段就类似DB中表的version
方案设计:
- 实际架构中,DB性能并不高,所以一般不用数据库支持数据存取之外的功能。
- DB乐观锁没有实际价值,添加version条件是能保证数据没有更新,但没有提示,在产品设计(交互)上不合格
- 悲观锁基本不能用
- 可行方案:对同一个订单的操作,我们首先可以“分布式锁”保证对同一个订单操作的串行,然后使用乐观锁,判断订单状态和数据是否符合操作提交。
乐观锁在MQ消息去重的判断
前提:
- 顺序消息
- 消息中添加递增version
消费者:
- 拿到消息获取version
- 判断该topic的version处理到那个数字了
- 如果当前消息的version号 <= redis中的version值,那么就说明已经处理过,或者正在处理
特别说明:
- 如果不是顺序消息,如使用多个queue就保证不了顺序,就没法用这个方法来做了
- 非顺序消息就需要redis set值来判断了。
redis set值判重
很多数据需要处理,只能被处理一次,比如我们可以计算数据的MD5,或者给数据添加version。将其放入redis的set数据结构,每次处理数据,先看这个值是否已经存在,如果已经存在就不处理。
适用场景:
- 表达重复提交:set的值 最好有一个时间限制
- 非顺序MQ消费判重
token保证幂等性
每次提交需要提前请求一个token,类似你要去景山,就需要拿身份证去旁边的售票厅领取一张门票,进门的时候门票只能用一次。
流程:
- 准备提交数据
- 领取门票token
- 后端系统记录这个token,并设置过期时间
- 填写数据,并点击提交,携带token
- 接口先验证token,成功则del 该token 记录
- 验证失败,则直接返回,让领取门票后提交
接口验证逻辑并发控制:
- 根据token从redis中get值value
- 如果value不为空,说明token有效,则删除
- value为空,说明token无效或者过期,则返回
- 场景:加入两个请求,前后完成get操作。value都不为空,则可能重复提交。
- 解决:获取,比较,删除 应该原子,可以使用redis 执行lua脚本实现
删除token和执行业务 先后问题:
- 一般是先删除token,原因是后删除token可能会导致重复提交,既然是幂等性就需要在接到请求的第一时间删除token
- 先删除token也可能会因为操作失败而没法重复提交,但可以重复填写数据提交。最起码能保证数据没有重复
技术选性
幂等性方案需要根据实际需求来综合几种思路来实现。
开发人员需要在开发的时候,有幂等性这个意识。一般产品不会给你提出这个要求,但这是一个开发的最基本职业素养。
参考
END