什么是幂等性?
多次请求某一个资源对于资源本身应该具有同样等结果,也就是说,其任意多次执行对资源本身所产生等影响的结果均与第一次执行的影响的结果相同。(多次请求的资源都是相同的导致数据库存储脏数据)
产生幂等性场景
- 网络波动, 可能会引起重复请求
- 用户重复操作,用户在操作时候可能会无意触发多次下单交易,甚至没有响应而有意触发多次交易
- 应用使用了失效或超时重试机制(Nginx重试、RPC重试或业务层重试等)
- 页面重复刷新
- 使用浏览器后退按钮重复之前的操作,导致重复提交表单
- 使用浏览器历史记录重复提交表单
- 浏览器重复的HTTP请求
- 定时任务重复执行
- 用户双击提交按钮
幂等在哪一层实现?
1 | 数据访问层 |
幂等性解决方案
1. 按钮只可操作一次
一般是提交后把按钮置灰或loding状态,消除用户因为重复点击而产生的重复记录,比如添加操作,由于点击两次而产生两条记录
2. token机制
功能上允许重复提交,但要保证重复提交不产生副作用,比如点击n次只产生一条记录,具体实现就是进入页面时申请一个token,然后后面所有的请求都带上这个token,后端根据token来避免重复请求。
3. 使用Post/Redirect/Get模式
在提交后执行页面重定向,这就是所谓的Post-Redirect—Get(PRG)模式,简单来说就是当用户提交连表单后,跳转到一个重定向的信息页面,这样就避免用户按F5刷新导致的重复提交,而且也不会出现浏览器表单重复提交的警告,也能消除按浏览器前进和后退导致同样重复提交的问题。
4. 在session存放特殊标志
在服务端,生成一个唯一的标识符,将它存入session,同时前端获取这个标识符的值将它写入表单的隐藏中,用于用户输入信息后点击一起提交,在服务器端,获取表单中隐藏字段的值,与session中的唯一标识符比较,相等说明是首次提交,就处理本次请求,然后将session中的唯一标识符移除,不相等则表示是重复提交,不再做处理。
5. 使用唯一索引防止新增脏数据
利用数据库唯一索引机制,当数据重复时,插入数据库会抛出异常,保证不会出现脏数据。
6. Token + Redis
以订单为例:
第一阶段:在进入到提交订单页面之前,需要订单系统根据用户信息向后端发起一个申请Token的请求,后端将Token保存到Redis缓存中,为第二阶段操作使用。
第二阶段: 订单系统拿着申请到的token发起提交订单请求,后端会检查Redis中是否存在该Token, 如果存在, 表示第一次发起订单提交请求,开始逻辑处理,处理完逻辑后删除Redis中的Token 当有重复请求的时候,检查缓存中Token是否存在。不存在表示非法请求。
7. 状态机
针对更新操作,比如业务上需要修改订单状态,例如订单状态有待支付、支付中、支付成功、支付失败、订单超时关闭等,在设计的时候最好只支持状态的单向改变(不可逆),也就是在更新的时候where条件里可以加上status = {状态},多次调用的话实际上也只会执行一次。
8. 乐观锁
如果更新已有数据,可以进行加锁更新,也可以设计表结构时使用乐观锁,通过version来做乐观锁,这样既能保证执行效率,又能保证幂等, 乐观锁的version版本在更新业务数据要自增update table set version = version + 1 where id = #{id} and version = #{version}
示例: 当有重复请求的时候,第一个请求会获取当前商品的version版本号,得到的version为1,紧接着由于第一个请求还没更新商品的version,第二个请求获取的version依然也是1, 这时候第一个请求操作更新的时候带上version并作为条件并且自增更新,这时候商品的version就会变成2,当第二个请求去操作更新的时候明显version不一致导致更新失败。
9. 防重表
以支付为例: 使用唯一主键去做防重表的唯一索引,比如使用订单号作为防重表的唯一索引,每一次请求都根据订单号向防重表中插入一条数据,插入成功说明可以处理后面的业务,当处理完业务逻辑之后删除防重表中的订单号数据,后续如果有重复请求,则会因为防重表唯一索引原因导致插入失败,直接返回操作失败,直到第一次请求返回结果,可以看出防重表作用就是加锁的功能。注: 最好结合状态机幂等先判断一下
10. select + insert or update or delete
该方案就是操作之前先查询一下,符合要求再插入,该方案在没有并发的系统中可以解决幂等问题,在单JVM有并发的时候可以用JVM加锁来保证幂等性,在分布式环境它是无法保证幂等性,可以使用分布式来保证。
11. 分布式锁
在进入方法时,先去获取锁,假如获取到锁,就继续后面的流程,假如没有获取到锁就等待锁释放后直到获取锁,当执行完方法时则进行释放锁。
该解决方案可以用来解决分布式系统幂等性。
常用的分布式锁实现采用的方案是 Redis 和 Zookeeper等工具,使用分布式锁类似于防重表,将防重并发放到缓存中,较为高效,思路相同,同一时间只能完成一次支付请求。
注: 获取锁最好设置个超时时间,防止意外没有释放到锁
12. 缓冲队列
将请求都快速地接收下来后放入缓冲队列中,后续使用异步任务处理队列中的数据,过滤掉重复的请求,该解决方案优点是同步处理改成异步处理、高吞吐量,缺点则是不能及时地返回请求结果,需要后续轮询得处理结果。
13. 全局唯一号
比如通过source来源 + 唯一序列号传入给后端,后端来判断请求是否重复,在并发时只能处理一个请求,其他相同并发请求要么返回请求重复,要么等待 前面请求执行完成后再执行。
转载自:陈死狗