高并发场景下如何保证领券接口的幂等性

708 阅读4分钟

在高并发场景下,由于存在大量的并发操作,如果接口没有进行幂等性设计,可能会导致重复提交、多次扣减等问题,进而影响业务稳定性和数据准确性。 其实市面上目前有非常多的幂等措施,但是在使用这些幂等方案前,我们都必须要保证业务的幂等一致性。什么意思,假设我们有一个业务场景是领取奖券的,但是每个用户都只能领取一张券。例如以下代码块A:


CouponRecord cp = couponRecordDao.findByUserId(userId,couponId);
if(cp!=null){
    //已经领取奖券,需要限制
    return;
}
//记录奖券信息....
couponRecordDao.insert(userId,couponId);
...

但是遇到高并发场景下,上述的这种思路可能就不那么可靠了。因为在并发场景下,可能同时存在多个线程执行findByUserId返回为null,然后同时插入了多条记录。所以结合这类业务场景,我们再来探讨接口的幂等性方案,会更靠谱一些。

使用验证码

每次请求时生成一个验证码,如果该验证码已经被使用过了,后续请求将被拒绝。这种思路其实一般可以用于类似登录弹窗的业务场景中。保证一次验证码只能用一回,同时还要确保,每次生成新的验证码缓存到redis的时候,需要将之前的缓存记录给清掉。对于领券业务来说,流程图如下所示:

image.png

其实这种思路在很多平台中都会有使用,例如各大手机运营商中。利用输入手机验证码,然后发送验证码校验接口给到后台,在后台进行校验和删除操作,这里可以使用lua脚本,先get,如果存在则delete,只有delete成功的请求才能处罚奖券的插入。

不过这种方案比较耗费短信费用,成本较高。

使用 Token

通过在请求头或者请求参数中传递 Token,来验证该请求是否已经被处理过。例如在一些活动页面中,用户可以在APP的抢购展示页中疯狂点击抢券按钮。如果后台没有做幂等处理的话,很可能会导致用户手速过快,一秒内点击了多次抢券按钮,从而给后台计算逻辑带来影响。

这里我们可以基于用户正常访问的Token功能去做幂等控制,例如每次请求的时候,前端都会携带Cookie,这个Cookie中会记录用户的token信息,而这些token信息其实就是一个字符串,每次请求后台服务集群的时候,token都需要先进行校验才可访问正常的业务服务。

那么我们可以利用这些token,给它做个缓存,例如缓存在Redis当中,缓存个3秒,这样每次请求抵达接口的时候,就判断下,如果Redis中有缓存,就表明以及有请求抵达过了,因此进行限制。这种思路能限制住3秒内,一个token只接收一个请求。

image.png

在限制住的3秒内,只要完成了上边的代码块A,那么后续的请求就可以利用数据库查询去防止插入重复了。这种思路也是大家在工作中最长使用的策略了。

MQ处理

将原先的并发请求,全部转移到队列中,然后交给单个线程进行消费。例如使用RocketMQ接收请求,然后这个MQ只给一个消费者去进行同步消费处理。等处理完之后,再通过异步请求去更新领券的状态。这种思路较为复杂一些,如果数据量过多,很容易发生消息堆积问题。

image.png

这种设计还会引发一个新的问题,就是如何异步去通知用户,以及用户点击了领券按钮,是否还得设计一张表去记录领券状态告知用户,整体下来架构比较复杂。

数据库唯一约束

通过数据库上的唯一约束(unique key)来保证数据的唯一性。当插入或更新冲突时,数据库会自动拒绝该操作。这种方案也是目前大家用的比较多的一种思路,实用了唯一索引之后,我们的代码可以直接变成insert插入,如果插入成功,则表示领券成功,如果插入失败,则表示已经领取奖券了。

image.png

基于分布式锁的实现

通过设计一种公共分布式锁,保证接口幂等性。每次进行抢券的时候,接口里面就加入一把分布式锁的抢夺代码,只有抢到锁的线程才可以继续执行。这种思路需要项目组的技术组件有提供成熟的分布式锁功能才建议使用。否则如果分布式锁不成熟,一些异常情况的发生可能导致锁无法正常释放,业务执行中断等异常。