如何避免并发情况下的重复提交
背景
在业务开发中,我们常会面对防止重复请求的问题。当服务端对于请求的响应涉及数据的修改,或状态的变更时,可能会造成极大的危害。重复请求的后果在交易系统、售后维权,以及支付系统中尤其严重。
重复请求的一致性问题又称幂等性问题。
先弄清楚啥叫幂等性。
比如
1. 一个用户把他的性别设置为男,无论他设置多少次,他的性别都是男。
2. 比如我们查询余额(假设并没有任何使余额发生变化的行为),那么我们点击多少次查询余额得到的结果都应该是一样的。
以上都是幂等的。
但是,如果我们进行一笔交易,这笔交易实际已经正常的插入了数据库,但由于前台操作的抖动,快速操作,网络通信或者后端响应慢等原因,又来了一次。导致用户平白无故支付了两次。 这种情况我们该如何避免呢??
首先前端优化是必不可少的,这里暂且不谈。。。
我们谈谈后端
唯一键法
并发并不意味着每个request都处理的很快,也不意味着机器之间就不共享数据了。可以把所有带有副作用的task都给一个GUID,最后写进数据库之前查询一下这个GUID是否已经被执行过了。
订单状态法
用户调用支付,扣款成功后,更新对应订单状态,然后再保存流水。
(支付状态:未支付,已支付)
步骤:
1、查询订单支付状态
2、如果已经支付,直接返回结果
3、如果未支付,则支付扣款并且保存流水
4、返回支付结果
理论上,只要在数据状态更新前完成了查询操作,则业务逻辑的重复处理就依旧会发生。
基于缓存的数据验证
Redis存储查询轻量快速。在request进来的时候,可以先记录在缓存中。后续进来的request每次进行验证。整个流程处理完成,清除缓存。
I. 每次交易发起申请,读取缓存中是否有以orderId为key的值
II. 没有,则往缓存中写入以orderId为key的value
III.有,则说明有该订单正在进行。
IV. 操作完清缓存,或者缓存存值的时候设置生命周期
- 1
- 2
- 3
- 4
- 5
利用数据库的主键唯一
同一笔订单进来的话,数据库会报唯一索引的错误。这个时候后台对这个异常进行处理并返回给前端提示。
(那么如何保证误操作的交易的订单号都是一个订单号呢,如何和正常的多次交易情况区分开?)
缓存计数器
由于数据库的操作比较消耗性能,了解到redis的计数器也是原子性操作。果断采用计数器。既可以提高性能,还不用存储,而且能提升qps的峰值。
还是以支付为例子:
每次request进来则新建一个以orderId为key的计数器,然后+1。
如果>1(不能获得锁): 说明有操作在进行,删除。
如果=1(获得锁): 可以操作。
操作结束(删除锁):删除这个计数器。