本文目录导读:
热点锁”的优化拆分细化,通常指的是在高并发场景下,针对单一线程池、单一数据库行、单一缓存key等“热点资源”产生的竞争锁进行的优化。
核心思路是:避免所有线程都去抢同一把锁,而是把锁的粒度变细,或者将竞争压力分散。
以下是几种主流的优化和拆分细化策略:
物理/数据维度拆分(最常用)
将“一个大锁”变成“多个小锁”,线程只关注自己需要的数据。
-
分段锁(Segmentation):
- 场景: 内存中的共享资源(如一个大型的
HashMap、ConcurrentHashMap的实现思路)、订单号段。 - 做法: 将共享数据分成 N 个段(Segment),每个段拥有一把独立的锁。
- 效果: 线程A锁住段1,线程B可以同时修改段2,彻底消除对段1的竞争。
- 注意: 如果业务逻辑需要跨段操作(如全表统计),则需要额外处理(如无锁读取或全局快照)。
- 场景: 内存中的共享资源(如一个大型的
-
哈希分片(Hash Sharding):
- 场景: 热点数据库行、热点缓存key、热点消息队列。
- 做法: 在原锁的key后面加上一个随机数或Hash值(如
lock_item_${itemId}变成lock_item_${itemId}_${Random.nextInt(1000)})。 - 示例(Redis分布式锁):
// 原始的、竞争激烈的锁 // String lockKey = "lock:order:123"; // 优化后:拆分成1000个逻辑片 int shard = userId % 1000; // 或者使用随机数 String lockKey = "lock:order:123_shard_" + shard;
- 注意: 在释放锁或业务回调时,需要知道当前是哪个分片。
时间维度拆分(削峰填谷)
将“集中地、瞬间地抢锁”变成“允许分散地、排队地占用”。
-
令牌桶/漏桶(Token/Lock Bucket):
- 场景: 秒杀系统的扣减库存、限流。
- 做法: 不直接抢锁,而是从桶中取令牌,如果桶中没令牌,线程立即失败或重试。
- 效果: 将瞬时的高并发请求平滑成均匀的请求流。
-
分级锁(Hierarchical Locking):
- 场景: 秒杀场景。
- 做法:
- 第一层(内存级):本地缓存或局部计数器快速过滤(比如同时只有1000人能进入下一步)。
- 第二层(Redis级):分布式令牌桶或原子减库存(CAS)。
- 第三层(数据库级):最终一致性写入。
- 效果: 绝大多数请求在第一层就被拦截,根本不会触碰到数据库的“行锁”。
逻辑/粒度拆分(缩小临界区)
只锁住必须串行的最小代码块。
-
读写锁拆分(ReadWriteLock):
- 场景: 配置热更新、读多写少的库存查询。
- 做法: 读操作使用共享锁(Read Lock),写操作使用排他锁(Write Lock)。
- 效果: 读读不互斥,只有写操作才阻塞整个锁,这能极大提升读并发,但写锁依然需要优化。
-
乐观锁(CAS/版本号)代替悲观锁:
- 场景: 高频读取、低频写入的库存表行。
- 做法: 不
SELECT ... FOR UPDATE,而是UPDATE table SET version=version+1 WHERE id=xxx AND version=xxx。 - 效果: 读操作无锁,写操作通过重试机制解决冲突,这是拆分“锁的持有时间”的常用做法。
-
事务中锁的剥离(降级锁范围):
- 场景: 需要处理一张表的更新,同时要查另一张表。
- 做法: 将耗时的非关键操作(如日志记录、无关接口调用)移出被锁资源的事务或同步块。
- 效果: 缩短锁的持有时间。
分散热点 + 异步化(釜底抽薪)
如果锁的冲突无法通过拆分解决,尝试不要让所有线程都去抢它。
-
本地缓存 + 异步回写:
- 场景: 热词统计、广告点击量。
- 做法: 在应用层先用 ConcurrentHashMap 或 Striped Lock 聚合本地数据,每隔几秒(或满阈值)批量异步写入Redis或DB。
- 效果: 百万级的并发请求只在应用内存中本地排队,只在写入时抢锁。
-
消息队列解耦(MQ削峰):
- 场景: 热点下单、库存扣减。
- 做法: 请求方不直接抢锁,而是将请求发送到消息队列,后端消费者单线程或小批量地处理。
- 效果: 彻底消除了同步抢锁,将冲突转化为顺序处理的流水线。
实战案例:优化一个“热点订单ID的分布式锁”
原始问题: 大量用户同时下单同一商品(item_id=123),使用 RedisLock("lock:order:123") 进行库存扣减,导致锁争抢极严重。
优化方案(组合拳):
- 分片: 将库存分到10个库存桶(
stock_0...stock_9),锁key变为lock:stock:123_shard_{userId%10}。 - 降级: 优先级队列先做本地Token校验(在应用层JVM内过滤掉70%的无效请求)。
- 缩短持有时间: 只在Redis原子减库存时加锁,后面的数据库写入改为异步MQ。
- 兜底: 如果某个分片锁依然热点(如用户群集中),对该分片做二次分片(如10个桶变100个)。
| 策略 | 核心原理 | 适用场景 | 风险/成本 |
|---|---|---|---|
| 分段锁 (Segments) | 物理拆分数据段 | 内存大集合、DB表分片 | 跨段操作需特殊处理 |
| 哈希分片 (Sharding) | 负载均衡到不同锁key | 分布式锁的热点key | 释放锁时需要知道分片号 |
| 乐观锁 (Version/CAS) | 不阻塞,失败重试 | 读多写少,冲突概率低 | 写冲突高时性能反而下降 |
| 读写锁 (RWLock) | 读共享,写互斥 | 配置热更新、读多写少 | 写锁时可能饿死读锁 |
| 异步批处理 (MQ/Local) | 将同步抢锁改为异步流 | 秒杀、日志、统计 | 牺牲强一致性(最终一致) |
最重要的原则是: 不要试图优化一个根本不存在的热点,先通过监控(Redis慢查询、数据库行锁等待、应用线程Dump)确认确实有一个锁在造成阻塞,再针对它进行上述的拆分细化。