基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

高并发场景下,多个线程同时争抢同一把分布式锁导致Redis CPU飙升,如何进行优化

知识点图片

在高并发场景下,多个线程争抢同一把分布式锁导致 Redis CPU 飙升,这是典型的“热点 Key” + “自旋重试(Spin)风暴”问题。

大量线程不断发起 SETNX 或 Lua 脚本请求,导致 Redis 的单线程事件循环被打满,CPU 飙升,甚至拖垮整个 Redis 节点。

要优化这个问题,我们需要从客户端控制、锁机制设计、业务架构三个维度来进行改造。以下是循序渐进的优化方案:


一、 客户端与锁机制优化(见效最快,首选)

1. 将“自旋轮询”改为“发布订阅(Pub/Sub)”机制

如果是你自己手写的 while(true) { if(setnx) break; Thread.sleep(); } 这种自旋锁,请立即放弃。
优化方案: 使用 Redisson 客户端。

  • 原理: Redisson 的分布式锁在获取锁失败时,不会疯狂轮询,而是会订阅该锁的 Redis Channel,然后线程进入阻塞状态。当持有锁的线程释放锁时,会往该 Channel 发布一条消息,唤醒等待的线程再去抢锁。
  • 效果: 极大地减少了对 Redis 的无效请求,直接干掉 CPU 飙升的根源。

2. 引入“本地锁(JVM 锁)”进行两级拦截(极度推荐)

虽然用了 Redisson,但如果有 10000 个请求同时打到同一台应用服务器上,让这 10000 个线程都去和 Redis 交互依然是浪费。
优化方案: 在去 Redis 抢锁之前,先在应用进程内抢 JVM 本地锁(如 ReentrantLocksynchronized

  • 流程:
    1. 线程先尝试获取 JVM 级别的本地锁。
    2. 获取到本地锁的 1个线程,再去 Redis 抢分布式锁。
    3. 其他 9999 个线程在本地 JVM 排队等待。
  • 效果: 如果你有 10 台应用服务器,原本 10万个并发请求会全部打到 Redis;加上本地锁后,最多只有 10 个请求会打到 Redis 进行分布式锁争抢。Redis 的压力骤降 99.9%。

二、 业务与数据结构设计优化(治本之策)

同一时间都在抢“同一把锁”,说明业务发生了严重的冲突。我们可以通过降低冲突概率来优化。

1. 细化锁的粒度

不要锁大对象,只锁真正需要互斥的小对象。

  • 错误做法: lock("update_order") (所有订单更新都抢一把锁)
  • 正确做法: lock("update_order:" + orderId) (只锁当前正在操作的订单)
  • 效果: 将一把全局锁分散成无数把细粒度锁,利用 Redis 多节点/集群的处理能力(不同的 Key 会 Hash 到不同的 Redis 节点),彻底解决单点热点问题。

2. 分段锁(Segmented Lock)机制

如果是类似“秒杀扣减库存”的场景,锁粒度已经细到了 lock("sku:1001"),但这个 SKU 就是有百万并发怎么处理?
优化方案: 借鉴 ConcurrentHashMap 的分段锁思想,把数据拆分。

  • 原理: 假设商品 A 有 1000 个库存,不要只放在一个 Key 里。将其拆分为 10 个分段:sku:1001:bucket:1sku:1001:bucket:10,每个分段 100 个库存。
  • 流程: 请求到来时,随机(或轮询)选择一个 bucket 进行加锁和扣减。如果该 bucket 扣减失败,再去重试其他 bucket。
  • 效果: 原本 100 万并发抢 1 把锁,变成了 10 万并发抢 10 把锁,极大地分散了单 Key 的 CPU 压力。

三、 架构层面的降维打击(逃课方案)

如果上述方案依然无法满足性能要求,说明“强同步+悲观锁”的模型已经不适合当前业务了

1. 改为“乐观锁(Optimistic Lock)”

抛弃 Redis 分布式锁,直接利用数据库的原子性或版本号(CAS)机制。

  • UPDATE inventory SET stock = stock - 1, version = version + 1 WHERE sku_id = 1001 AND stock > 0 AND version = old_version;
  • 适用场景: 读多写少,或者允许失败重试的场景。通过 DB 行锁代替 Redis 分布式锁。

2. 改为“异步排队”(MQ 削峰填谷)

真正的高并发写操作,通常都会放弃同步抢锁。

  • 原理: 将用户的并发请求直接丢进消息队列(Kafka / RabbitMQ / RocketMQ),然后给用户返回“请求处理中”。后台由消费者单线程(或根据 Key 分区并行)按顺序从队列中拉取消息进行处理。
  • 效果:并发争抢变成了串行排队,彻底消灭了分布式锁的需求,且系统吞吐量达到最大。

3. 利用 Redis Lua 脚本代替分布式锁

如果你抢锁的目的仅仅是为了对 Redis 里的某个数据做计算(比如扣库存)。

  • 优化方案: 直接写一段 Lua 脚本(比如判断库存足够就扣减并返回成功),把这段脚本发给 Redis 执行。
  • 原理: Redis 单线程执行 Lua 脚本本身就是原子的,不需要先加锁、再操作、再释放锁,3 次网络交互变成 1 次,大大降低了 CPU 开销。

总结与排查建议

遇到 Redis CPU 飙升,你的行动指南应该是:

  1. 立刻止血: 检查代码,是不是手写的 while(true) 死循环自旋锁?如果是,立刻换成 Redisson 并开启合理的 waitTime(获取锁超时时间)。
  2. 短期优化: 引入 本地锁 + 分布式锁 的双重锁机制,把绝大部分等待线程挡在应用层。
  3. 长期重构: 审视业务逻辑,是否可以细化锁粒度?是否可以改用分段锁?或者直接上 MQ 异步化

注意: 很多开发者遇到这个问题第一反应是“加 Redis 集群”,这在“同一把锁(热点 Key)”面前是无效的,因为同一个 Key 无论怎么 Hash 都只会落在一个确定的 Redis 节点上,单台节点的 CPU 依然会被打满。核心必须是减少请求量分散 Key

00:00
00:00