秒杀系统的幂等,只做一层Redis判重远远不够

0 阅读3分钟

面试经常被问到幂等设计,很多人的回答是:用Redis的SETNX做个判重就行了。

这个回答不能说错,但只对了三分之一。

秒杀专栏第15篇写完了,专门讲幂等设计。在实际的秒杀系统中,幂等做一层是扛不住的,我们的方案是三层防护,从外到内:Redis SETNX抢占、数据库唯一索引、业务状态机,三层各管各的范围,缺一层就有漏洞。

先说Redis层。消费端收到消息后,用SETNX原子操作抢占处理权,同一个traceId只有第一个到达的消息能通过,后续重复消息直接跳过。这一层能挡住99%以上的重复请求。但问题是Redis不是万无一失的,宕机恢复后可能丢数据(取决于AOF的fsync策略),SETNX的key可能消失。这时候如果没有第二层兜底,重复请求就穿透到业务逻辑了。

第二层是数据库的唯一索引。在订单表的traceId字段上建唯一索引,即使Redis层漏过了重复请求,数据库的DuplicateKeyException也会阻止重复插入。消费端捕获这个异常后,不当错误处理,而是查询已有订单返回给前端。

第三层是业务状态机。前两层解决的是「不重复创建」,第三层解决的是「重复操作不改变结果」。支付回调来了两次,UPDATE语句的WHERE条件里带了order_status = 0,第一次成功更新,第二次影响0行直接忽略。一条带条件的UPDATE语句就够了,数据库行锁保证原子性。

这篇文章还讲了一个容易被忽视的细节:PROCESSING状态的过期时间设计。设了60秒过期,消费线程崩溃后key会自动过期,允许RocketMQ下一次重试。不设过期时间的话,处理中途崩溃后这条消息就永远无法被重试了。另外还区分了「确定性失败」和「临时异常」的不同处理方式,库存不足用markFail直接标记失败,网络超时用release删除key允许重试。

做了这么多年开发,见过不少线上事故是幂等没做到位引起的。重复扣库存、重复建单、重复发放权益,每一种都是资损。很多人知道要做幂等,但只做了一层就觉得够了。三层各有各的弱点,组合在一起才没有死角。

这个秒杀专栏从需求分析、架构设计到每一行核心代码,覆盖了秒杀系统从0到1的完整链路。不是泛泛地讲概念,而是每个方案背后为什么这么选、不这么做会出什么问题,都讲清楚了。如果你正在做高并发系统、准备面试、或者想系统地学一套生产级的秒杀方案,这个专栏能帮你省掉大量踩坑的时间。订阅专栏,少走弯路。

最近在知乎出了秒杀专栏,感兴趣的可以订阅一下。至于知识星球的,可以搜:

  • 老码头的技术浮生录

它是一个能实际帮你解决难题的星球。有问题的,找知心的Sam哥,支持无限次语音一对一解决你遇到的难题。另外后续我新写的所有对外的付费专栏,在星球内都是免费的,且可以拿到所有源代码。

我的知乎账号:

  • SamDeepThinking