Redis分布式锁实现抢单秒杀:从SETNX到Redisson的选型与压测避坑

发布时间:2026/10/6 19:19:38

Redis分布式锁实现抢单秒杀:从SETNX到Redisson的选型与压测避坑 简介这份资源面向Java后端开发者与高并发场景学习者聚焦电商秒杀抢单中的库存超卖与并发控制难题提供一套基于Redis分布式锁的完整实现方案。压缩包共13个文件约59KB以5个Java源码文件为核心配合properties配置、pom.xml依赖描述、mvnw与cmd构建脚本及README说明文档结构精简便于直接导入SpringBoot工程运行调试。内容围绕分布式锁加锁与自动释放、Redis原子操作扣减库存、消息队列削峰、抢单结果状态返回等关键环节展开并涉及幂等性与事务一致性等进阶考量。目前已有4053人学习下载适合希望理解秒杀系统设计思路、对照代码梳理锁与队列协作流程的开发者参考也可作为课程设计或面试准备的实践素材。1. 抢单秒杀场景下Redis 分布式锁到底锁住了什么618 或整点秒杀开始的那一秒同一件商品可能被几千个请求同时命中。数据库行锁扛不住这个量级本地synchronized只在单机 JVM 内有效多实例部署下形同虚设。这时候大家都会想到用 Redis 分布式锁来兜底让所有实例去抢同一把锁抢到的那个才有资格扣库存。听起来简单但真正上线后你会发现锁没锁住、锁提前释放、锁被别人删掉这些玄学问题一个接一个。这篇笔记就围绕「redis 分布式锁实现抢单秒杀」这条主线把锁的选型、加锁解锁的原子性、锁续期、库存扣减的落库时机以及压测时最容易翻车的几个点讲透。适合已经会用 Redis 做缓存、但还没在秒杀链路里真正压过分布式锁的后端同学也适合正在准备分布式锁面试题、想把答案落到代码上的人。读完你应该能自己搭一套可压测的抢单 Demo并知道每个参数为什么这么设。2. 从 SETNX 到 Redisson抢单锁的三种实现与选型理由2.1 为什么抢单场景不能只用 SETNX最早大家用SETNX key value加锁配合EXPIRE设过期时间。问题在于这两条命令不是原子的如果SETNX成功之后、EXPIRE执行之前服务挂了这把锁就永远不会过期后续所有抢单请求全部阻塞。后来 Redis 2.6.12 之后SET命令支持NX和EX参数一条命令搞定加锁和过期SET lock:order:1001 uuid-value NX EX 10这条命令的含义是只有当lock:order:1001不存在时才设置成功同时 10 秒后自动过期。返回值是OK表示抢到锁nil表示没抢到。uuid-value必须是每个请求唯一的后面解锁时要用它来判断「这把锁是不是我加的」否则可能误删别人的锁。但只用SET NX EX还不够。抢单业务里扣库存、生成订单、写流水可能超过 10 秒锁提前过期后另一个请求进来就会出现两个请求同时扣同一件商品。这就是锁续期要解决的问题。2.2 Redisson 的看门狗机制与抢单锁参数生产环境我一般直接用 Redisson它把加锁、续期、可重入、解锁的 Lua 脚本都封装好了。核心是看门狗watchdog默认锁过期时间 30 秒后台线程每 10 秒检查一次如果业务还没执行完就自动续到 30 秒。注意看门狗只在「不指定 leaseTime」时才生效一旦你手动传了leaseTimeRedisson 就不会自动续期。// 获取锁对象key 按商品维度隔离避免不同商品互相阻塞 RLock lock redissonClient.getLock(lock:seckill:sku: skuId); try { // 尝试加锁最多等 3 秒不指定 leaseTime 以启用看门狗 boolean locked lock.tryLock(3, TimeUnit.SECONDS); if (!locked) { // 没抢到锁直接返回不要在这里自旋重试会把 Redis 打满 return Result.fail(抢购太火爆请重试); } // 临界区查库存、扣减、写订单 return seckillService.doSeckill(userId, skuId); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return Result.fail(系统繁忙); } finally { // 只有当前线程持有锁时才释放避免误删 if (lock.isHeldByCurrentThread()) { lock.unlock(); } }参数上tryLock的等待时间设 3 秒比较稳太短会导致大量请求直接失败太长会让线程堆积。锁 key 一定要带skuId如果所有商品共用一把锁秒杀时不同商品之间会互相排队吞吐直接掉一个数量级。isHeldByCurrentThread()这个判断不能省它对应 Redisson 内部用 Hash 结构记录持有线程 ID 的机制是解锁安全的关键。2.3 三种方案对比与选型建议方案原子加锁自动续期可重入适用场景SETNX EXPIRE否否否仅学习不推荐生产SET NX EX Lua 解锁是否否逻辑简单、耗时可控的短任务Redisson RLock是是是抢单秒杀、长事务临界区选型逻辑很直接抢单链路里临界区包含数据库操作耗时不确定必须要有续期能力所以 Redisson 是默认选择。如果你不想引入 Redisson至少也要用SET NX EX加 Lua 解锁脚本并且把过期时间设得足够覆盖 P99 耗时。3. 把锁落到抢单链路库存扣减与 Lua 原子脚本3.1 库存预热与扣减的原子性秒杀开始前先把库存从数据库加载到 Redis用 String 类型存seckill:stock:skuId。扣减时不能先GET再DECR这两步之间会有并发窗口。正确做法是用 Lua 脚本把「判断库存是否大于 0」和「扣减」合成一个原子操作-- KEYS[1] 库存 keyARGV[1] 扣减数量 local stock redis.call(GET, KEYS[1]) if not stock then return -1 -- 库存未预热 end if tonumber(stock) tonumber(ARGV[1]) then return 0 -- 库存不足 end redis.call(DECRBY, KEYS[1], ARGV[1]) return 1 -- 扣减成功返回值约定-1表示 key 不存在0表示库存不够1表示扣减成功。Java 侧用DefaultRedisScript加载这段脚本通过redisTemplate.execute(script, keys, args)调用。Lua 脚本在 Redis 单线程模型里执行天然不会被其他命令打断这是它比多条命令组合更可靠的根本原因。3.2 分布式锁与 Lua 扣减的配合顺序有了原子扣减脚本是不是就不需要分布式锁了不是。Lua 保证的是「扣库存」这一步的原子性但抢单链路还有「同一用户不能重复下单」「扣完库存要写订单」这些跨 key、跨系统的约束。我一般的顺序是先用分布式锁按userId skuId加锁防止同一用户并发重复提交锁内执行 Lua 脚本扣 Redis 库存扣减成功后发消息到 MQ异步落库生成订单释放锁。这里锁的粒度是「用户 商品」比按商品加锁更细能减少锁竞争。如果按商品加锁同一商品的所有用户都要排队QPS 上不去。按用户维度加锁后只有同一用户的并发请求会互斥不同用户之间靠 Lua 脚本的原子性保证库存不超卖。String lockKey lock:seckill: userId : skuId; RLock lock redissonClient.getLock(lockKey); if (!lock.tryLock(2, TimeUnit.SECONDS)) { return Result.fail(请勿重复提交); } try { Long result redisTemplate.execute(stockScript, Collections.singletonList(seckill:stock: skuId), 1); if (result null || result 0) { return Result.fail(库存不足); } // 发送 MQ 消息异步创建订单 mqProducer.send(new SeckillMessage(userId, skuId)); return Result.success(抢购成功订单生成中); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }3.3 锁超时与业务耗时的匹配锁的过期时间必须大于临界区 P99 耗时。我压测时会把临界区耗时打点如果 P99 是 200ms锁过期设 3 到 5 秒足够。但如果你用了 Redisson 看门狗默认 30 秒其实偏长一旦服务假死锁要 30 秒才释放期间该用户的请求全部失败。我的习惯是显式设置leaseTime为 5 秒同时确保临界区不会超过 3 秒用业务超时来兜底而不是完全依赖看门狗。提示leaseTime一旦设置看门狗就不再续期。设置前先确认临界区最大耗时留出至少 2 倍余量。4. 压测时最容易翻车的四个坑4.1 锁被误删现象是库存扣了两次现象压测时偶尔出现同一用户扣了两次库存日志里两次请求都显示加锁成功。原因解锁时没有校验锁的持有者A 请求的锁过期后 B 请求拿到锁A 执行完直接DEL把 B 的锁删了。解决解锁必须用 Lua 脚本比较 value 再删除或者直接用 Redisson 的unlock()它内部已经做了持有者校验。-- 安全解锁只有 value 匹配才删除 if redis.call(GET, KEYS[1]) ARGV[1] then return redis.call(DEL, KEYS[1]) else return 0 end4.2 锁等待时间过长导致线程池打满现象秒杀开始后 Tomcat 线程数飙升大量请求超时。原因tryLock等待时间设了 10 秒没抢到锁的线程一直挂着线程池被占满。解决等待时间控制在 1 到 3 秒没抢到直接快速失败返回「抢购火爆」让前端引导用户重试。秒杀场景下快速失败比排队等待体验更好也能保护后端。4.3 Redis 主从切换导致锁丢失现象Redis 主节点宕机从节点升主原本加锁的 key 还没同步过去新请求又能加锁成功。原因Redis 主从复制是异步的锁数据可能丢失。解决如果业务绝对不能超卖用 RedLock 或者把库存扣减的最终一致性交给数据库唯一索引兜底。我的做法是 Redis 扣减成功后数据库订单表对userId skuId建唯一索引即使锁失效重复扣了 Redis 库存落库时也会被唯一约束拦住。4.4 看门狗线程与业务线程池互相拖累现象服务运行一段时间后Redisson 续期线程报RedisCommandTimeoutException锁提前过期。原因业务线程池和看门狗共用同一个 Redis 连接池业务高峰时连接被占满续期命令发不出去。解决给 Redisson 单独配置连接池或者把lockWatchdogTimeout调大同时监控redis command timed out日志。连接池参数上最小空闲连接建议不低于 10最大连接数按 QPS 估算。注意redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeout这类报错在压测时很常见先查连接池再查网络延迟最后才怀疑锁逻辑。5. 用 JMeter 验证锁是否真的锁住了5.1 压测脚本与断言设计验证分布式锁有没有生效不能只看「没报错」要设计能暴露并发问题的断言。我用 JMeter 建一个 500 线程、循环 10 次的压测计划请求体里带同一个userId和skuId然后加两个断言响应中「抢购成功」的出现次数不能超过库存数数据库订单表里userId skuId不能有重复记录。压测前先把 Redis 库存设为 100数据库订单表清空。跑完后用 SQL 核对-- 检查是否超卖 SELECT sku_id, COUNT(*) AS order_count FROM seckill_order WHERE sku_id 1001 GROUP BY sku_id HAVING COUNT(*) 100; -- 检查同一用户是否重复下单 SELECT user_id, sku_id, COUNT(*) AS cnt FROM seckill_order WHERE sku_id 1001 GROUP BY user_id, sku_id HAVING COUNT(*) 1;两条 SQL 都返回空才说明锁和库存扣减配合正确。如果第一条有结果说明超卖回去查 Lua 脚本和锁粒度如果第二条有结果说明用户维度锁没生效检查 lockKey 拼接。5.2 监控指标与日志埋点光靠压测不够线上要能实时看到锁的健康度。我会在加锁前后打点记录三个指标加锁成功率、加锁平均等待时间、锁持有时间。加锁成功率突然下降通常是 Redis 连接池或网络问题锁持有时间变长说明临界区里有慢 SQL 或 MQ 发送阻塞。日志里把lockKey、userId、skuId、traceId一起打出来出问题时能直接定位到具体请求。long start System.currentTimeMillis(); boolean locked lock.tryLock(2, TimeUnit.SECONDS); long waitMs System.currentTimeMillis() - start; metrics.recordLockWait(waitMs); if (!locked) { log.warn(lock_fail lockKey{} userId{} skuId{} waitMs{}, lockKey, userId, skuId, waitMs); return Result.fail(抢购火爆); }这套埋点跑一周你就能知道当前锁参数是否合理。如果加锁等待 P99 超过 500ms说明锁竞争太激烈要么缩小锁粒度要么在网关层做限流把无效请求挡在锁外面。5.3 一个我踩过的参数坑最后说个血泪经验tryLock的等待时间单位别写错。我有次把TimeUnit.SECONDS写成了TimeUnit.MILLISECONDS结果等待时间从 3 秒变成 3 毫秒压测时加锁成功率直接掉到 30%排查了半天才发现是单位问题。现在我的习惯是所有和时间相关的参数都在常量类里定义并且带上单位后缀比如LOCK_WAIT_SECONDS 3调用时只传常量不写裸数字。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/6 19:19:38

gem5预取器定制与aarch64 SPEC2006评估实战拆解

深入拆解gem5的预取器定制与aarch64下的SPEC2006评估我这次把gem5缓存预取器研究这件事从头到尾走了一遍,从读源码、改预取逻辑,到在aarch64架构下把SPEC2006跑起来,中间踩了不少坑。这篇文章就把完整的过程、关键参数、源码结构、运行命令、…

2026/10/6 19:19:38

10Gbps QPSK光纤通信系统仿真:从链路建模到性能分析

做光纤通信系统仿真,一上来就对着“10Gbps 正交相位移键控QPSK光纤通信系统”这种标题,很容易被吓到。其实拆开看,核心就三件事:搭一条能跑的QPSK光纤链路,把真实光纤里的衰减、色散、非线性和ASE噪声建模进去&#xf…

2026/10/6 19:19:38

Redis分布式锁在秒杀场景下的实现与避坑指南

简介:这份资源围绕高并发场景下的抢单秒杀需求,给出基于Redis分布式锁的完整实现方案,面向具备SpringBoot与Redis基础的Java后端开发者,以及需要应对超卖、重复抢单等并发问题的电商系统学习者。压缩包共13个文件,约59…

2026/10/6 20:24:41

2024年Win11+Ubuntu 22.04.1 LTS双系统安装与避坑指南

简介:这份文档面向已装Windows 11、希望再装Ubuntu 22.04.1 LTS组成双系统的用户,覆盖从环境检查到安装完成的全流程。内容包含查看BIOS与UEFI模式、下载ISO镜像、用Rufus制作启动U盘、压缩磁盘分区、BIOS启动项设置,以及安装时选择“与Windo…

2026/10/6 20:24:41

Windows 11 装 Ubuntu 22.04.1 LTS 双系统:UEFI 引导避坑指南

简介:这份文档面向已装好 Windows 11、想再装 Ubuntu 22.04.1 LTS 组建双系统的用户,尤其适合初次接触 UEFI 引导与磁盘分区的新手。内容围绕双系统安装全流程展开,涵盖基础环境检查、Rufus 启动盘制作、与 Windows Boot Manager 共存的安装方…

2026/10/6 20:24:41

轻量AI中台实战:用私有化大模型实现智能录入与对账

去年年底结账,财务主管把一摞打印出来的银行流水拍在桌上,说这个月有三十二笔款项对不上业务订单,业务那边又说,光是补齐客户名称和合同编号就加班了两晚。我当时听完只有一个想法:这不是人不够勤快的问题,…

2026/10/6 20:24:41

国产RFSOC+FPGA双芯宽带高速信号处理板设计实战

这阵子集中做了一块国产RFSOCFPGA双芯架构的宽带高速信号处理板,从方案评估、器件选型到回板调试、跑通数据链路,整套流程走下来,踩了不少坑,也积累了一些值得记录的经验。做这类板卡的人应该都有同感:单看RFSOC的集成…

2026/10/6 20:24:41

大模型评测平台落地方案:集成Coze-Loop与AllData的自动化评估体系

跑了一年多大模型的落地项目,我越来越认同一个判断:真正卡住AI应用落地的,往往不是模型本身的智商,而是“怎么看它到底行不行”这套评测机制。我们团队在AllData平台上集成了开源项目Coze-Loop,搭了一套大模型评测平台…

2026/10/6 20:19:40

System-1 判断模型接入 agent loop:MCP 工具链下的循环控制实践

1. 为什么我会想到把 System-1 判断模型塞进 agent loop 先说清楚这篇要聊的东西是什么。System-1 判断模型,指的是那种"看一眼就给结论"的快速决策模型——它不追求长链条推理,而是在极短时间内对当前状态做一个高置信度的判断,输…

2026/10/5 6:32:56

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/6 4:01:51

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/6 17:46:51

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 0:03:23

MR25H40CDF+STM32F031C6工业级高可靠数据存储方案

1. 项目概述:为什么在工业现场非得用 MR25H40CDF 配 STM32F031C6 做数据存储?在工厂产线的 PLC 控制柜里、在风电变流器的散热片背面、在矿井监测终端的金属外壳下,你经常能看到一块指甲盖大小的黑色芯片——它既不是 Flash,也不是…

2026/10/6 0:03:23

MRAM+STM32工业断电数据保全实战指南

1. 项目概述:为什么在工业现场非得用 MR25H40CDF 配 STM32F031C6 做数据存储?在工厂产线的PLC柜里、在野外无人值守的环境监测终端里、在高速运转的包装机控制板上,你经常能看到一块指甲盖大小的黑色芯片,旁边贴着“MR25H40CDF”丝…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑