根本原因是客户端在waitTime内反复轮询Redis加锁,导致大量无意义请求;应改用无参tryLock、前置队列排队、显式leaseTime、本地锁+分布式锁两级过滤,并细化锁粒度、禁用看门狗、升级Redisson版本。

Redis分布式锁为什么会在客户端自旋耗CPU?
根本原因不是Redis本身在自旋,而是客户端调用 tryLock(long waitTime, long leaseTime, TimeUnit unit) 时,在 waitTime 内反复轮询 Redis 是否能加锁成功。每次轮询都是一次独立的 SET key value NX EX 请求;若锁竞争激烈(比如1000个线程同时抢同一把锁),且 waitTime 设为30秒、重试间隔默认50ms,单个线程就可能发起600+次无意义请求——这些请求全走网络+Redis执行,最终在客户端线程池和Redis服务端共同放大成CPU尖峰。
如何避免客户端无意义轮询?
关键不是“减少重试次数”,而是让等待行为脱离主动轮询模式:
- 用
tryLock()无参版本(即只设leaseTime,不设waitTime):获取失败直接返回,由上层业务决定是降级、排队或抛异常,彻底规避自旋 - 若必须等待,改用带阻塞语义的队列(如
BlockingQueue或 Redis List +BLPOP)做前置排队,再统一发起一次锁请求,把“N选1”的竞争收敛为“1次请求” - 禁用 Redisson 默认的
lockWatchdogTimeout续期机制(它本身也会触发后台定时任务轮询),改用显式leaseTime,例如lock.tryLock(3, 10, TimeUnit.SECONDS)—— 这样锁最多存10秒,业务必须在10秒内完成,不依赖看门狗续命
本地锁 + 分布式锁两级过滤能砍掉多少自旋?
实测中,90%以上的同进程内并发请求根本不需要走到Redis。例如扣减商品库存,先用 ConcurrentHashMap.computeIfAbsent("stock_" + skuId, k -> new ReentrantLock()) 获取本地锁,加锁失败立即返回,成功后再调 redisson.getLock("redis_stock_" + skuId).tryLock()。这样做的效果:
- 本地锁超时必须设短(如
tryLock(50, TimeUnit.MILLISECONDS)),否则会卡住线程池 - 本地锁只对同JVM内有效,跨实例仍靠Redis锁兜底,二者语义不能混用
- 注意本地锁容器本身不能无限增长,需配合LRU或定时清理,否则
ConcurrentHashMap内存泄漏
Redis端还能怎么配合降低自旋压力?
客户端优化之外,Redis侧几个硬性配置能显著缓解:
- 确保锁 key 是细粒度的,比如
"inventory_" + skuId而非"inventory_lock",避免所有请求挤在一个key上触发串行化瓶颈 - 集群部署时,用哈希标签
{inventory_123}强制分片,防止某一个slot成为热点 - 禁用
subscriptionMode=SLAVE(尤其主从切换频繁时),该模式下Redisson会高频重建订阅连接,间接加剧CPU波动 - 升级 Redisson 到 3.23.0+,老版本(如3.16.x)中
CommandsQueue在高积压场景下存在死循环风险,直接拉满CPU
真正难处理的从来不是“怎么加锁”,而是“谁该等、等多久、等不到怎么办”。把等待逻辑从Redis客户端移出去,交给业务层或中间件控制,才能跳出自旋陷阱。


















