去重和幂等
什么是幂等
服务调用必然会碰到网络延迟等问题导致客户端进行失败重试,多次提交,幂等就是在该类情况下,保证重试和正常访问一次成功对系统资源的影响是一致的
幂等性不仅仅只是多次请求结果一致,还包括第一次操作产生影响后,以后多次操作不会再产生影响。并且幂等关注的是是否对资源产生影响,而不关注结果
参考:https://www.turingtopia.com/article/details/20afdce72901445f9a66563a8441e78d
不论去重还是幂等,都需要有一个"唯一key’,并且有地方对“唯一key”进行存储
⽐如说「5分钟相同内容消息去重」,我直接MD5请求参数作为唯⼀Key。「1⼩时模板去重」则是「模板
ID+userId」作为唯⼀Key,「⼀天内渠道去重」则是「渠道ID+userId」作为唯⼀Key...
--java3y《对线面试官》
产生原理
请求可能有网络问题-》需要重试
接口没有做请求去重-》已经完成请求相应的动作-》重试-》幂等问题 俗称F(F(x)) = F(x)
更新数据是依赖读取的数据为基础调教,当遇到高并发的时候会出现幂等问题
常见产生原因
由于重复点击或者网络重发 eg:1)点击提交按钮两次;2)点击刷新按钮;3)使用浏览器后退按钮重复之前的操作,导致重复提交表单;4)使用浏览器历史记录重复提交表单;5)浏览器重复的HTTP请;6)nginx重发等情况;7)分布式RPC的try重发等;作者:锦成同学
链接:https://juejin.cn/post/6844903894384902158
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
HTTP请求方式已经隐含了幂等约定:GET、PUT、DELETE原则要保证幂等,而POST不需要幂等。
解决方案
参考:https://zhuanlan.zhihu.com/p/432631103
前端解决方案
前端防重
通过前端防重保证幂等是最简单的实现方式,前端相关属性和 JS 代码即可完成设置。可靠性并不好,有经验的人员可以通过工具跳过页面
仍能重复提交。主要适用于表单重复提交或按钮重复点击。 不知道前端设置啥东西。。。
PRG模式
POST-REDIRECT-GET 重定向,提交表单后重定向到完成后的页面而不是停留在表单页面,同时防止了通过浏览器按钮前进/后退导致表单
重复提交(怎么防止的?不清楚)。 是一种比较常见的前端防重策略。
Token模式
主要是防重
- 客户端会先发送一个请求去获取 token,服务端会生成一个全局唯一的 ID 作为 token 保存在 redis 中,同时把这个 ID 返回给客户端
- 客户端第二次调用业务请求的时候必须携带这个 token
- 服务端会校验这个 token,如果校验成功,则执行业务,并删除 redis 中的 token
- 如果校验失败,说明 redis 中已经没有对应的 token,则表示重复操作,直接返回指定的结果给客户端
注意:redis的查询与删除需要保证原子性 分布式锁与lua表达式 可以使用Redisson
后端解决方案
去重表(唯一索引)
创建一张表作为去重表,建立一个或者多个字段的唯一索引作为防重字段用于保证并发下数据只有一条。
在向业务表插入数据之前,先向去重表插入,如果插入失败表示重复
比如:同一个用户同一件商品不能在同一分钟下两次单,那么就需要 user_id, product_id, created_at 这三个字段做为去重字段。
唯一主键 insert、delete场景
通过数据库的唯一主键约束或者唯一索引约束,重复的key就不会插入成功了,可以防止新增脏数据,具有防重效果
乐观锁update场景(状态机)
数据库乐观锁方案只适用于更新操作的过程,可以提前在对应数据库表中添加一个标识充当版本字段,用于判断数据是否被修改过
对代码具有一定入侵性,需要增加字段
基本思路:版本号+条件
以订单扣减库存举例
将查询的version号在修改时传递进去,判断版本号是否一致,也就是数据有没有被修改,没有就update成功,已经被修改过就update失败,提示数据过期。
update tb_stock set amount=amount-#{num},version=version+1 where goods_id=#{goodsId} and version=#{version}
那么同时下单的用户就只有一个能扣减库存成功,其他的都失败,可以再加条件防止超卖amount-#{num}>=0
订单只支付一次的场景:状态机
update table item set item.status=:newstatus where item.id = :id and item.status = oldstatus
为什么不用悲观锁?
首先悲观锁有可能会锁表,有性能问题。
由于 InnoDB 预设是 Row-Level Lock,所以只有「明确」的指定主键,MySQL 才会执行 Row lock (行锁) ,否则 MySQL 将会执行 Table Locck(锁表)Lock
其次,悲观锁可能产生死锁
悲观锁:假定肯定会发生冲突,屏蔽一切可能违反数据完整性的操作
数据库的悲观锁通过for update实现,一般伴随事务一起使用
select * from t_order where orderId=#{orderId} fro update
redis分布式锁实现
分布式系统使用同步锁肯定是不行的,对于分布式系统可以用个乐观锁或者分布式锁来解决幂等问题
- 客户端先请求服务端,会拿到一个能代表这次请求业务的唯一字段
- 将该字段以 SETNX 的方式存入 redis 中,并根据业务设置相应的超时时间
- 如果设置成功,证明这是第一次请求,则执行后续的业务逻辑
- 如果设置失败,则代表已经执行过当前请求,直接返回
与上文的 token 方案中使用 redis 类似,基本还是 分布式 ID+分布式锁。
这里涉及分布式锁问题
单机系统——java同步锁
把查询状态的代码和更新的代码放到一个同步锁内,这样同一时刻只能有一个线程进入执行,等执行完其他线程才能进入,这样能解决幂等性问题,但是加入同步块里面的业务代码执行时间较长,这样会严重影响用户体验和系统的吞吐量,所以不是最佳方案。
可以参考:https://blog.csdn.net/a745233700/article/details/88084219
作者: deity-night
出处: https://www.cnblogs.com/deity-night/
关于作者:码农
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接 如有问题, 可邮件(***@163.com)咨询.