生产环境经常出现重复的数据,而这个绝大部分原因是发生了重复的请求。
重复请求是指同一个请求因为某些原因被多次提交。
导致这个情况会有几种场景:
微服务场景:
- 在微服务架构下,接口超时,微服务框架会进行重试。
用户交互的时候多次点击,如:快速点击按钮多次。
MQ消息中间件:消息重复消费。
第三方平台接口:因为异常也会导致多次异步回调。
幂等性定义
如上,针对重复请求,我们在设计某些接口时,要考虑如何保证接口幂等。
那什么是接口幂等呢?
定义:多次调用对系统的产生的影响是一样的,即对资源的作用是一样的,但是返回值允许不同。
- 并且不会因为多次点击而产生了副作用。
比如支付场景:
用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了。
用户再次点击按钮,此时会进行第二次扣款,返回结果成功。
- 如果用户查询余额返发现多扣钱了,流水记录也变成了两条,这就没有保证接口的幂等性。
幂等场景
查询
- 不会对数据产生任何变化,具备幂等性。
新增
- 数据都会新增多条,不具备幂等性。
修改
- 直接赋值更新,具备幂等性。
- 计算赋值更新,不具备幂等性。
删除
- 多次操作,结果一样,具备幂等性。
总结:新增没有唯一主键约束的数据,和计算赋值更新操作都不具备幂等性。
直接赋值:
update user set num = 20 where userid=123;
计算赋值:
update user set num = num + 20 where userid=123;
幂等性方案
悲观锁
id字段一定是主键或者唯一索引,不然可能造成锁表的结果。
- 数据锁定时间可能会很长,需要根据实际情况选用。
select * from user where id = 1 for update;
乐观锁
加上了版本号后,计算赋值更新场景,具备了幂等性。
缺点:
- 在操作业务前,需要先查询出当前的version版本。
update user set num= num + 20, version = version + 1 where userid=123 and version=1;
唯一约束
利用数据库的主键唯一约束的特性,解决在insert场景时幂等问题。
去重表
把唯一主键插入去重表,再进行业务操作,且他们在同一个事务中。
- 这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。
去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。
- 保证了数据一致性。
去重表是跟业务无关的,很多业务可以共用同一个去重表。
- 需要规划好唯一主键。
图片
分布式锁
如果多个机器可能在同一时间同时处理相同的数据:
- 比如多台机器定时任务都拿到了相同数据处理。
就可以加分布式锁,锁定此数据,处理完成后释放锁。
- 获取到锁的必须先判断这个数据是否被处理过。
Token机制
图片
❝
服务端提供了发送token的接口。
在执行业务前,先去获取token,服务器会把token保存到redis中。
- 然后调用业务接口请求时,把token携带过去。
服务器判断token是否存在redis中,存在表示第一次请求。
- 这时把redis中的token删除,继续执行业务。
如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给client。
- 这样就保证了业务代码,不被重复执行。
缺点
业务请求每次请求,都会有额外的请求:
- 一次获取token请求、判断token是否存在。
真实的生产环境中,1万请求也许只会存在10个左右的请求会发生重试。
- 为了这10个请求,需要让9990个请求都发生了额外的请求。
逻辑实现:
可以通过自定义注解将进行改造。
- 在需要保证幂等的方法上,添加自定义注解即。
图片
总结
幂等性是系统服务对外一种承诺,特别业务中涉及的钱的部分,一定要慎重再慎重。
虽然前端做限制会更容易点,但前后端都需要做努力。