幂等性方案
这种问题的本质在于,提交了一样的数据,形成的结果也一致,造成了许多冗余数据。那么,假如在用户抢购商品时,如何防止用户重复提交呢?
简介
幂等本身是一个数学概念,即f(n) = 1^n^,无论 n为多少,f(n)的值永远为1。在程序中,对于幂等的定义为:无论对某一个资源操作了多少次,其影响都应是相同的。换句话说,在接口重复调用的情况下,对系统产生的影响是一样的
例如:
1
select * from order_info where order_id = 1
这个语句无论执行多少次,虽然结果有可能出现不同,都不会对数据产生改变,具备幂等性。
实现幂等性是为了防止用户重复提交表单、防止恶意刷单、防止消息重复消费等问题。
幂等设计主要考虑两个方面:空间、时间
- 空间:定义幂等的范围,如订单不允许重复下单
- 时间:定义幂等的有效期。有些业务需要永久保证幂等,如下单、支付。而部分业务只需要保证一段时间幂等即可。
同时对于幂等的使用一般都会伴随着出现锁的概念,用于解决并发安全问题。
前端防重
通过前端防重保证幂等是最简单的实现方式,前端相关属性和JS代码即可完成设置。
常用解决方案
点击按钮,发出请求,此时按钮变为不可点的状态,只有拿到返回结果后,才可以继续点击。
缺点:页面刷新或重新进入,又可以继续点击了
采用 PRG(POST-REDIRECT-GET) 模式,即用户提交表单后,重定向到另外一个页面,而不是停留在原先的表单页面。
基于token的防重
利用 token 的机制来保证幂等是一种常见解决方案,其流程如下:
那么是先执行业务,还是先删除token呢?这个问题就类似于缓存更新问题(先删除缓存,然后再更新数据库的逻辑是错误的)
先执行业务,再删除token
如果采用的是判断token存不存在的模式,先执行业务,再删token,会导致执行业务成功 ,但还没有删除token这个空隙,进来的请求也被认为是正常的请求(携带token的并发请求),这样就出错了。
- 业务执行成功,但token删除失败
- 并发请求
当然,可以利用Redis单线程和原子自增性质,默认存入的Redis的token自增为1,后续请求只有value为2的时候,才执行业务(其它值均认为是重复请求),这样就可以先执行业务,后等待token自动过期
先删token,再执行业务
先删除token,那么后面的重复请求都不会通过校验,保证了幂等性,但是业务处理可能会失败,那么就需要通知客户端重新获取token
token方案也是有一定弊端的:每次业务的请求都需要一个额外的请求去获取token,但是在实际的业务场景中出现重复提交或者业务处理失败的情况不多,虽然redis性能好,但是这也是一种资源的浪费。
基于pageId+redis的方案
token方案虽然解决了幂等性问题,但是需要进行两次请求,那是否可以做成一次请求呢?以订单业务为例:
- 下单时,前端生成一个pageId,规则为:productId+当前时间戳
- 携带该pageId发送业务请求,利用redis的单线程和原型性的特点,对该pageId进行自增,拿到返回结果。
- 只有返回值为1,表示是正常的请求,准许执行业务;如果是 >1的值,则表示是重复请求,不许通过,从而保证了幂等性。
- 可以对该pageId的合法性进行校验,比如对productId校验,这就需要事先在Redis中存放或者利用布隆过滤器
防重表
定义一张表作为防重表,同时在该表中建立一个或多个字段的唯一索引作为防重字段,保证只有一条数据,在向业务表中插入数据之前先向防重表插入,如果插入失败则表示是重复数据。
乐观锁
乐观锁是基于数据库完成分布式锁的一种实现,实现的方式有两种
- 基于版本号
- 基于条件
可以提前在对应的数据表中多添加一个字段,充当当前数据的版本标识,这样每次对该数据库该表的这条数据执行更新时,都会将该版本标识作为一个条件,值为上次待更新数据中的版本标识的值。
缺点:需要每次事先知道这条数据对应的版本号,当并发请求时,只有一个人能成功,在某些业务中不合理。
