乐观锁和悲观锁,到底该怎么选?

为什么你的秒杀系统总超卖?转账偶尔对不上账?很可能------你选错了锁。

  • 悲观锁:适合冲突多、不能出错的场景(转账、抢票)
  • 乐观锁:适合冲突少、允许失败的场景(点赞、浏览量)
  • 没有最好,只有最合适

先说结论:两种完全不同的思路

悲观锁:先占着,你们等着

就像占座位,我先坐上去,你们想坐?等我起来再说。

sql 复制代码
START TRANSACTION;

-- 锁住这条记录
SELECT balance FROM accounts WHERE user_id = 123 FOR UPDATE;

-- 扣钱
UPDATE accounts SET balance = balance - 100 WHERE user_id = 123;

COMMIT; -- 释放锁

优点:不会出错

缺点:慢,大家排队等

sequenceDiagram participant A as 用户A participant DB as 数据库 participant B as 用户B A->>DB: 加锁并读数据 B->>DB: 想读数据 Note over B: 只能等着 A->>DB: 修改完,提交 DB->>B: 现在可以了

乐观锁:先干活,冲突了再说

就像写文档,大家都可以改,但提交的时候检查一下有没有人抢先改过。

sql 复制代码
-- 看一眼当前版本号
SELECT stock, version FROM products WHERE id = 456;
-- 结果:库存100,版本号5

-- 减库存,但要求版本号还是5
UPDATE products 
SET stock = 99, version = 6 
WHERE id = 456 AND version = 5;

-- 如果version变了,这条SQL不生效,说明有人抢先了

优点:快,不用等

缺点:冲突多了失败率高

sequenceDiagram participant A as 用户A participant DB as 数据库 participant B as 用户B A->>DB: 读数据(版本5) B->>DB: 读数据(版本5) A->>DB: 更新(要求版本5) Note over DB: 成功,版本改成6 B->>DB: 更新(要求版本5) Note over DB: 失败,版本已经是6了

四个真实场景

场景1:淘宝秒杀

10万人抢1000台iPhone,冲突超级大。

  • 纯悲观锁?排队排死
  • 纯乐观锁?99%失败,疯狂重试

真实做法:混合

  1. Redis先筛出1000人
  2. 这1000人用悲观锁扣库存
flowchart LR A[10万请求] --> B[Redis预扣减] B --> C[1000人通过] B --> D[99000人直接返回售罄] C --> E[悲观锁扣DB库存] E --> F[生成订单]

场景2:银行转账

你给朋友转500块,绝对不能错。

必须用悲观锁

sql 复制代码
START TRANSACTION;

-- 同时锁住两个账户(按ID顺序,防止死锁)
SELECT balance FROM accounts 
WHERE user_id IN (123, 456) 
ORDER BY user_id 
FOR UPDATE;

-- 扣钱、加钱、记账
UPDATE accounts SET balance = balance - 500 WHERE user_id = 123;
UPDATE accounts SET balance = balance + 500 WHERE user_id = 456;
INSERT INTO transactions (...) VALUES (...);

COMMIT;

关键点:

  • 一定要按顺序加锁
  • WHERE条件要走索引,不然会锁整张表

场景3:微博点赞

一条热门微博,每秒几千人点赞。

用乐观锁+Redis

python 复制代码
# Redis直接加1,毫秒级
redis.incr("post:999:likes")

# 后台慢慢同步到MySQL

10001个赞和10005个赞,用户根本看不出来,但"点了半天没反应"用户能感受到。

场景4:演唱会抢票

一个座位只能卖一次,不能超卖。

用悲观锁+预锁定

sql 复制代码
START TRANSACTION;

-- 锁住座位
SELECT status FROM seats 
WHERE concert_id = 100 AND seat_no = 'A-12' 
FOR UPDATE;

-- 锁定15分钟,等你付款
UPDATE seats 
SET status = '锁定', user_id = 你的ID, lock_time = NOW()
WHERE concert_id = 100 AND seat_no = 'A-12';

COMMIT;

定时任务释放超时的:

sql 复制代码
-- 15分钟没付款?释放座位
UPDATE seats 
SET status = '可售' 
WHERE status = '锁定' 
AND lock_time < 15分钟前;

四个经典的坑

坑1:死锁

两个人互相等对方,谁也动不了。

错误:

  • 张三转李四:先锁123,再锁456
  • 李四转张三:先锁456,再锁123
  • 结果:互相等,死锁

正确:

sql 复制代码
-- 不管谁转谁,都按ID从小到大锁
SELECT * FROM accounts 
WHERE user_id IN (123, 456) 
ORDER BY user_id 
FOR UPDATE;

坑2:ABA问题

库存100 → 99 → 100,你以为没变,其实变过。

解决办法:用版本号

sql 复制代码
UPDATE products 
SET stock = 99, version = version + 1
WHERE id = 1 AND version = 旧版本号;

坑3:锁错了,锁了整张表

sql 复制代码
-- name没索引,结果把整张表锁了
SELECT * FROM users WHERE name = '张三' FOR UPDATE;

必须保证WHERE条件走索引。

坑4:疯狂重试

失败了立马重试,CPU直接100%。

正确做法:等一会儿再重试

java 复制代码
int retry = 0;
while (retry < 5) {
    if (更新成功) return true;
    Thread.sleep(10 * (1 << retry)); // 10ms, 20ms, 40ms...
    retry++;
}

性能测试数据

100万数据,1000线程同时改余额:

方案 每秒处理 平均耗时 失败率
悲观锁 1200次 350ms 0%
乐观锁(不重试) 8500次 50ms 95%
乐观锁(重试3次) 3200次 180ms 12%
混合策略 4500次 120ms 3%

混合策略最均衡。


怎么选?三个问题

flowchart TD A[你的场景] --> B{冲突多吗?} B -->|很多| C[悲观锁] B -->|不多| D[乐观锁] C --> E{能等吗?} E -->|能| F[直接用悲观锁] E -->|不能| G[混合策略] D --> H{失败能重试吗?} H -->|能| I[乐观锁+重试] H -->|不能| J[改用悲观锁] style F fill:#FFD700 style I fill:#90EE90 style G fill:#87CEEB

问自己:

  1. 冲突多不多? 多就悲观,少就乐观
  2. 能接受失败吗? 不能就悲观,能就乐观
  3. 速度重要吗? 重要就乐观+Redis

总结

悲观锁适合:

  • 转账、支付
  • 抢票、抢购
  • 库存扣减
  • 不能出错的操作

乐观锁适合:

  • 点赞、浏览量
  • 统计数据
  • 读多写少
  • 对性能要求高

真实项目往往混着用:

  • 看商品详情 → 乐观锁(快)
  • 真下单 → 悲观锁(准)

别纠结哪个"更好",懂业务,选对场景就行。

相关推荐
Cache技术分享2 小时前
264. Java 集合 - 插入元素性能对比:LinkedList vs ArrayList
前端·后端
青梅主码2 小时前
全球顶级大模型最新排名出炉:中国大模型表现优秀,DeepSeek V3.2 与 Kimi K2 Thinking 均挤进前 10
后端
linzeyang2 小时前
Advent of Code 2025 挑战全手写代码 Day 8 - 游乐场
后端·python
刘 大 望2 小时前
JVM(Java虚拟机)
java·开发语言·jvm·数据结构·后端·java-ee
SadSunset2 小时前
(3)第一个spring程序
java·后端·spring
北京中邦兴业2 小时前
GMP洁净环境监测法规深度解读:构建以风险为核心的动态防御体系
数据库·人工智能·面试·职场和发展
别动哪条鱼2 小时前
FFmpeg模块化架构
架构·ffmpeg
milanyangbo2 小时前
像Git一样管理数据:深入解析数据库并发控制MVCC的实现
服务器·数据库·git·后端·mysql·架构·系统架构
xhxxx2 小时前
一个空函数,如何成就 JS 继承的“完美方案”?
javascript·面试·ecmascript 6