抢单必须用tryLock而非lock,因其可控超时、避免线程阻塞雪崩;推荐配置tryLock(500, 30, TimeUnit.MILLISECONDS),并配合指数退避重试或延时队列,finally中需校验锁归属再释放。

抢单场景下,Redis分布式锁不能等、不能卡、不能误放——延迟高直接导致超卖,阻塞久直接拖垮接口。用 tryLock 配合精准参数,是唯一可控的快速失败路径。
为什么抢单必须用 tryLock 而不是 lock
抢购流量尖峰时,lock() 会无限等待锁释放,一个慢请求卡住线程池,后续所有请求排队雪崩。而 tryLock() 可控超时,失败立刻走降级逻辑。
-
lock()是阻塞式:没拿到锁就挂起线程,直到成功或中断 -
tryLock()是非阻塞式:指定等待上限,超时即返回false,业务可立即返回“手慢了”或进延时队列 - Redisson 的
tryLock(long waitTime, long leaseTime, TimeUnit unit)两个时间必须同时设,否则可能触发看门狗或永久等待
waitTime 设成多少才不卡又不丢单
waitTime 不是越小越好,也不是越大越稳——它得贴合 Redis RT 和业务容忍度。线上实测建议值:300ms~800ms。
- 低于 200ms:网络抖动(如 Redis RT 突然升到 150ms)就容易误判失败,丢掉本可抢到的单
- 高于 1s:单个请求占线太久,线程池积压,QPS 断崖下跌
- 推荐配置:
tryLock(500, 30, TimeUnit.MILLISECONDS)—— 最多等 500ms,拿到锁后最多持有 30ms(配合短业务逻辑) - 注意:leaseTime 必须大于业务平均耗时,否则锁提前释放;但也不能设成 30s,那等于放弃快速失败能力
如何避免 tryLock 失败后反复重试压垮 Redis
直接 while 循环重试 tryLock 是典型反模式——失败时高频打 Redis,可能把集群打满。
- 禁用无间隔重试:
while(!lock.tryLock(...)) Thread.sleep(10)会放大连接池压力 - 改用指数退避 + 有限次数:
maxRetries=3,间隔依次为 50ms、150ms、300ms - 更优解:失败后直接跳转到延时队列(如 Redis ZSet 实现),不重试,不占连接
- 连接池要同步调紧:
max-active=16、max-wait=200ms,防止tryLock等连接时自身先阻塞
最容易被忽略的 finally 安全释放点
哪怕用了 tryLock,如果 unlock() 写在 finally 里却没判断锁归属,照样误删他人锁。
- 错误写法:
finally { lock.unlock(); }—— 锁已过期被别人续上,此时 unlock 会删掉别人的锁 - 正确写法:
finally { if (lock.isHeldByCurrentThread()) lock.unlock(); } - Redisson 默认开启看门狗,但只对
tryLock(waitTime, leaseTime, ...)生效;若只传tryLock(time, unit),leaseTime 用默认 30s,看门狗才启动 - 极端情况:Redis 哨兵切换期间,
isHeldByCurrentThread()可能返回 false(连接断开),此时 unlock 会静默失败,不抛异常——得靠监控告警兜底


















