高并发场景中,乐观锁和悲观锁哪个更适合?

#中间件#面试八股

高并发场景中,乐观锁和悲观锁哪个更适合?

结论:没有绝对更优,取决于冲突概率。冲突低(读多写少、行级争用少)用乐观锁(Optimistic Lock),避免长时间持锁;冲突高(秒杀扣库存、同一行被高频更新)用悲观锁(Pessimistic Lock)或「串行化 + 排队」,因为乐观锁在高冲突下会退化成大量失败重试,把 CPU 全烧在自旋上。

一、两种锁的代价模型

维度乐观锁悲观锁
实现版本号、CAS、WHERE version = ?SELECT ... FOR UPDATE、分布式锁
持锁时间短,仅提交瞬间长,覆盖整个事务
冲突低时吞吐高,无阻塞无谓排队
冲突高时重试风暴、失败率飙升排队但可控,易死锁
跨行或跨库天然支持需分布式锁,代价高
用户体验冲突时重试或提示稍后等待,超时失败

二、冲突率决定成败

单次尝试成功概率 P ≈ 1 - c   (c 为冲突概率)
重试 k 次后的失败概率 ≈ c^k

 c = 0.1,k = 3  → 失败率 0.1%
 c = 0.5,k = 3  → 失败率 12.5%
 c = 0.9,k = 3  → 失败率 72.9%   ← 乐观锁在高冲突下基本失效

冲突率可用「同一秒内同一行的更新次数 ÷ 该行 QPS」估算。超过 30% 就该考虑换方案。

三、乐观锁实现

UPDATE t_stock
   SET quantity = quantity - #{n},
       version  = version + 1
 WHERE sku_id  = #{skuId}
   AND version = #{version}
   AND quantity >= #{n};
-- 影响行数为 0:被其他请求抢先,应用层退避重试或直接失败
public boolean deduct(long skuId, int n, int maxRetry) {
    for (int i = 0; i < maxRetry; i++) {
        Stock s = stockMapper.selectById(skuId);
        if (s.getQuantity() < n) {
            return false;
        }
        int rows = stockMapper.deductWithVersion(skuId, n, s.getVersion());
        if (rows == 1) {
            return true;
        }
        backoff(i);      // 指数退避 + 抖动,避免重试同步化
    }
    return false;
}

四、高冲突改用分段加原子操作

秒杀更常见的做法是「库存分段 + Redis 原子扣减」,把单行争用拆成 N 行:

-- KEYS[1] = stock:sku:1001:seg   (Hash:段号 -> 剩余库存)
-- ARGV[1] = 购买数量
local segs = redis.call('HGETALL', KEYS[1])
for i = 1, #segs, 2 do
  local seg, qty = segs[i], tonumber(segs[i + 1])
  if qty >= tonumber(ARGV[1]) then
    redis.call('HINCRBY', KEYS[1], seg, -tonumber(ARGV[1]))
    return 1
  end
end
return 0

数据库侧再异步落库,用消息队列把同一 SKU 的写操作串行化,把「锁竞争」转换为「排队」。

五、常见追问

Q1:乐观锁会有 ABA 问题吗? 版本号自增不会 ABA;用 CAS(旧值, 新值) 直接比较数值才会。加版本号或时间戳即可规避。

Q2:悲观锁会不会把连接池占满? 会。FOR UPDATE 的事务越长,连接占用越久,高并发下极易打满连接池。必须把事务缩到「查、改、提交」三步,禁止在锁内调用 RPC。

Q3:分布式场景怎么选? 同一行数据在单库时用数据库行锁;跨库或跨服务时用 Redis、etcd 分布式锁,但要接受锁服务本身的延迟与故障风险。更优的是无锁设计:幂等、唯一约束、状态机。

Q4:乐观锁失败后要不要重试? 要,但要有上限(通常 3 次)和退避抖动。无上限重试在冲突高时会把线程池和数据库一起拖垮。

小结

  • 低冲突用乐观锁省锁开销,高冲突用悲观锁或排队避免重试风暴。
  • 冲突率超过 30% 就该换方案,别靠加大重试次数硬扛。
  • 真正的解法常常是拆热点(分段库存),而不是换锁。
笔记加载中…