一文吃透 MySQL 乐观锁与悲观锁:原理、实战与流程图解
一文吃透 MySQL 乐观锁与悲观锁:原理、实战与流程图解
|
🌺The Begin🌺点点关注,收藏不迷路🌺
|
前言
在 MySQL 数据库高并发场景下,数据一致性问题往往是系统设计的重中之重。当多个事务同时操作同一条数据时,如何避免“丢失更新”、“脏读”等问题?锁机制就是解决这类问题的核心手段。本文将通过图文并茂的方式,详细剖析 MySQL 中的乐观锁与悲观锁,包括它们的原理、适用场景以及具体的代码实现。
1. 什么是悲观锁(Pessimistic Lock)
悲观锁是一种“先获取锁,再操作数据”的机制。它认为当前事务操作数据时,一定会有其他事务来并发修改,所以它在操作数据之前会主动加锁,直到当前事务提交或回滚后,锁才会释放。在此期间,其他任何想操作这条数据的事务都会被阻塞。
1.1 悲观锁的实现原理
在 MySQL 的 InnoDB 引擎中,使用 SELECT ... FOR UPDATE 语句实现悲观锁。该语句会对查询出的数据行加上排他锁(X锁)。需要注意的是,使用悲观锁时要关闭自动提交(SET autocommit=0)。
1.2 悲观锁的适用场景
- 写操作频繁:数据冲突概率高,悲观锁可以避免大量无效的重试。
- 强一致性要求:例如银行转账、扣减库存等对数据准确性要求极高的场景。
1.3 悲观锁代码示例
假设我们有一个商品库存表 product_stock:
CREATE TABLE `product_stock` (
`id` int NOT NULL AUTO_INCREMENT,
`product_name` varchar(100) DEFAULT NULL,
`stock` int DEFAULT '0',
`version` int DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
使用悲观锁扣减库存:
-- 事务1
SET autocommit = 0;
BEGIN;
-- 加排他锁查询当前库存
SELECT stock FROM product_stock WHERE id = 1 FOR UPDATE;
-- 假设业务逻辑判断 stock >= 1
UPDATE product_stock SET stock = stock - 1 WHERE id = 1;
COMMIT; -- 释放锁
如果另一个事务同时在执行相同的 FOR UPDATE,会被阻塞,直到第一个事务提交。
2. 什么是乐观锁(Optimistic Lock)
乐观锁是一种“先操作数据,提交时检查冲突”的机制。它认为数据一般情况下不会发生冲突,所以不加锁,只在更新时通过版本号或时间戳判断数据是否被修改过。如果发现数据已被其他事务修改,则放弃当前操作或重试。
2.1 乐观锁的实现原理
乐观锁通常通过版本号(version)或时间戳来实现。核心 SQL 如下:
UPDATE table_name
SET stock = new_stock, version = version + 1
WHERE id = ? AND version = old_version;
如果 affected rows = 1 表示更新成功;如果 = 0 表示数据已被其他事务修改,需要重试。
2.2 乐观锁的适用场景
- 读操作远多于写操作:如文章阅读量统计、用户点赞等。
- 冲突概率低:系统整体并发量可控,偶尔发生的冲突通过重试解决。
2.3 乐观锁代码示例(基于版本号)
业务层伪代码(Java 示例):
public boolean deductStock(int productId, int quantity) {
int retryCount = 3;
while (retryCount-- > 0) {
// 1. 查询当前商品信息(包含版本号)
ProductStock stock = dao.selectById(productId);
int oldVersion = stock.getVersion();
int oldStock = stock.getStock();
if (oldStock < quantity) {
return false; // 库存不足
}
int newStock = oldStock - quantity;
int newVersion = oldVersion + 1;
// 2. 使用版本号条件更新
int rows = dao.updateStockWithVersion(productId, newStock, newVersion, oldVersion);
if (rows == 1) {
return true; // 更新成功
}
// 3. 更新失败,说明版本号已变化,进入重试
}
return false; // 重试耗尽,更新失败
}
对应的 SQL:
UPDATE product_stock
SET stock = newStock, version = newVersion
WHERE id = productId AND version = oldVersion;
3. 乐观锁 vs 悲观锁:全面对比
| 对比维度 | 乐观锁 | 悲观锁 |
|---|---|---|
| 核心思想 | 假设无冲突,提交时检查 | 假设有冲突,提前加锁 |
| 实现方式 | 版本号 / 时间戳 + CAS | SELECT … FOR UPDATE |
| 锁机制 | 无实际数据库锁,靠业务逻辑 | 数据库行锁 / 表锁 |
| 并发性能 | 高,适合读多写少 | 低,存在锁等待和死锁风险 |
| 冲突处理 | 应用层重试或回滚 | 事务排队等待 |
| 数据库依赖 | 低(任何数据库都支持) | 依赖 InnoDB 等支持行锁的引擎 |
| 适用场景 | 读多写少、冲突概率低 | 写多读少、强一致性要求 |
4. 注意事项与常见误区
4.1 悲观锁的注意事项
- 一定要带索引:
FOR UPDATE如果没走索引,可能会锁表,严重降低并发。 - 避免长事务:持有锁时间越长,阻塞越严重,建议尽快提交。
- 警惕死锁:多个事务以不同顺序获取锁时可能发生死锁,数据库会自动检测并回滚其中一个事务。
4.2 乐观锁的注意事项
- ABA 问题:版本号机制天然解决了 ABA 问题(因为版本号严格递增)。
- 重试次数:高冲突场景下大量重试会降低性能,应设置合理的重试上限。
- 批量更新:乐观锁不适合批量操作,因为无法保证原子性。
5. 如何选择?一张决策图帮你做决定
简单总结:
- 金融、库存扣减 → 悲观锁(或使用
select ... for update后再更新) - 点赞、计数、配置表 → 乐观锁
6. 混合使用:乐观锁 + 悲观锁
在实际复杂的业务场景中,也可以两种锁混合使用。例如:
- 外层使用乐观锁进行快速检查
- 在真正更新时使用悲观锁确保一致性
但这种方案会增加代码复杂度,一般不建议初学者尝试。
结语
乐观锁和悲观锁没有绝对的优劣,核心在于根据业务场景选择最合适的方案。对于互联网项目,通常优先考虑乐观锁以提升吞吐量;对于金融类项目,悲观锁能带来更强的数据安全感。
希望通过本文的流程图解和代码示例,你能彻底掌握这两种 MySQL 并发控制利器。如果你在实际项目中遇到过锁相关的坑,欢迎评论区交流讨论!

|
🌺The End🌺点点关注,收藏不迷路🌺
|
更多推荐



所有评论(0)