# MySQL 的乐观锁和悲观锁是什么?
6 min read
Table of Contents
MySQL 的乐观锁和悲观锁是什么?
MySQL 乐观锁与悲观锁详解
【核心定义】
乐观锁是一种无锁化的并发控制思想,它假设事务间冲突概率低,只在提交时检测冲突;悲观锁则是一种先加锁后操作的并发控制思想,它假设事务间冲突概率高,在操作前就锁定资源。
【关键要点】
- 悲观锁实现方式:通过数据库的排他锁(
SELECT ... FOR UPDATE)或共享锁(SELECT ... LOCK IN SHARE MODE)实现,锁定期间其他事务无法修改。 - 乐观锁实现方式:通过版本号(version)或时间戳字段实现,更新时检查数据是否被修改,通常结合CAS(Compare And Swap)操作。
- 适用场景差异:悲观锁适合写操作频繁、冲突概率高的场景;乐观锁适合读多写少、冲突概率低的场景。
【深度推导/细节】
悲观锁的加锁过程(以InnoDB为例)
-- Step 1: 开启事务并加排他锁START TRANSACTION;SELECT * FROM products WHERE id = 1 FOR UPDATE;
-- Step 2: 执行业务逻辑(其他事务的FOR UPDATE会被阻塞)UPDATE products SET stock = stock - 1 WHERE id = 1;
-- Step 3: 提交事务释放锁COMMIT;锁的粒度:InnoDB的行锁是通过对索引项加锁实现的,如果WHERE条件没有用到索引,会升级为表锁。
乐观锁的冲突检测逻辑
-- Step 1: 查询时获取当前版本号SELECT stock, version FROM products WHERE id = 1;
-- Step 2: 更新时带上版本条件(业务层计算新值)UPDATE productsSET stock = new_stock, version = version + 1WHERE id = 1 AND version = old_version;
-- Step 3: 检查影响行数-- 如果affected_rows = 0,说明版本已变更,需要重试或报错ABA问题:单纯版本号可能遇到值回滚问题,可通过时间戳或递增版本号解决。
【关联/对比】
| 维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 并发性能 | 低(串行执行) | 高(并行执行) |
| 死锁风险 | 高(可能形成等待环) | 无(无锁机制) |
| 实现复杂度 | 低(数据库原生支持) | 高(需业务层处理冲突) |
| 数据一致性 | 强(锁定期间绝对一致) | 最终一致(可能需重试) |
| 适用场景 | 库存扣减、金额修改 | 点赞计数、状态更新 |
与MVCC的关系:InnoDB的MVCC(多版本并发控制)本质是乐观锁思想在数据库引擎层的实现,通过Read View和undo log实现非锁定读。
【线程安全考量】
- 悲观锁的线程安全:数据库层面保证,但需要正确管理事务边界,避免长事务导致锁等待超时。
- 乐观锁的线程安全:依赖业务层重试机制,常见方案:
- 有限次重试(3-5次)
- 指数退避策略
- 进入消息队列异步处理
【版本差异与实践优化】
- MySQL 5.7+:支持SKIP LOCKED和NOWAIT语法,优化悲观锁的等待机制
- MySQL 8.0:原子DDL和更好的死锁检测
- 优化建议:
- 混合使用:关键业务用悲观锁,非关键业务用乐观锁
- 监控指标:锁等待时间、死锁频率、重试次数
- 索引优化:确保WHERE条件使用索引,避免锁升级
『面试官追问』
-
乐观锁在超高并发下大量失败重试怎么办?
- 引入令牌桶或漏桶限流
- 将冲突操作序列化到消息队列
- 考虑改用悲观锁或分布式锁
-
SELECT FOR UPDATE会锁表还是锁行?
- 取决于WHERE条件是否使用索引
- 使用主键/唯一索引:锁行
- 无索引或索引失效:锁表
- 间隙锁:防止幻读,锁住范围间隙
-
乐观锁的版本号字段用什么类型?
- 推荐BIGINT自增,避免溢出
- 时间戳精度不够(毫秒级可能重复)
- 可结合业务流水号作为版本标识
-
如何选择两种锁机制?
- 冲突频率 > 20%:悲观锁
- 响应时间要求高:乐观锁
- 数据热点严重:考虑分库分表+乐观锁
-
乐观锁与CAS的关系?
- 乐观锁是思想,CAS是实现方式之一
- 数据库乐观锁通过WHERE条件实现CAS语义
- Java中的Atomic类使用CPU指令级CAS
总结要点:悲观锁“先防后做”,乐观锁“先做后验”;选择依据是冲突概率和性能要求;实际生产常根据业务场景混合使用,并配合监控告警体系。