在MySQL的并发控制场景中,锁机制是保障数据一致性的核心手段,其中悲观锁和乐观锁是两种最常用的方案。不少开发者在实际业务中发现,使用悲观锁后系统的吞吐量会出现明显下降,而切换到乐观锁版本号机制后性能往往能得到改善,这背后的原因值得深入探究。

悲观锁导致系统吞吐量下降的核心原因
悲观锁的核心思想是“先加锁再操作”,它默认认为数据在并发场景下一定会出现冲突,因此在操作数据前就会先获取锁,阻塞其他事务对该数据的操作,直到当前事务提交或回滚释放锁。
这种机制在并发量较低、冲突概率高的场景下能保证数据一致性,但在高并发场景下会带来几个明显的性能问题:
- 锁竞争开销大:大量事务同时请求同一行数据的锁时,未获取到锁的事务会进入等待队列,频繁的上下文切换和锁等待会消耗大量CPU和内存资源。
- 锁持有时间长:如果事务中包含复杂的业务逻辑,锁的持有时间会被拉长,进一步加剧锁竞争,导致更多请求阻塞。
- 可能出现死锁:多个事务互相持有对方需要的锁时,会触发死锁检测机制,数据库需要回滚部分事务来解除死锁,额外的回滚操作也会降低整体吞吐量。
悲观锁的典型使用示例
MySQL中悲观锁通常通过SELECT ... FOR UPDATE语句实现,以下是常见的使用代码:
-- 开启事务 START TRANSACTION; -- 查询id为1的商品库存,并加悲观锁 SELECT stock FROM product WHERE id = 1 FOR UPDATE; -- 业务逻辑:扣减库存 UPDATE product SET stock = stock - 1 WHERE id = 1; -- 提交事务,释放锁 COMMIT;
上述代码中,事务执行SELECT ... FOR UPDATE后会锁定id为1的商品行,其他事务执行同样的查询或更新操作时会阻塞,直到当前事务提交。
乐观锁版本号机制的实现原理
乐观锁的核心思想是“先操作再校验”,它默认认为数据冲突的概率很低,因此操作过程中不会加锁,仅在提交更新时通过版本号判断数据是否被其他事务修改过。
版本号机制是乐观锁最常用的实现方式,具体逻辑如下:
- 在数据表中新增一个
version字段,每次更新数据时版本号加1。 - 查询数据时同时获取当前的
version值。 - 更新数据时,在WHERE条件中带上查询到的
version值,同时把version加1。 - 如果更新语句返回的影响行数为1,说明更新成功;如果影响行数为0,说明数据已经被其他事务修改过,当前更新失败,需要重试或提示用户。
乐观锁版本号机制的实现示例
首先需要在商品表中新增version字段:
ALTER TABLE product ADD COLUMN version INT DEFAULT 0 NOT NULL;
乐观锁的更新逻辑代码如下:
-- 开启事务 START TRANSACTION; -- 查询商品库存和当前版本号 SELECT stock, version FROM product WHERE id = 1; -- 假设查询到的stock为10,version为5 -- 更新时校验版本号,同时版本号加1 UPDATE product SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = 5; -- 判断更新是否成功 -- 如果影响行数为1,说明更新成功,提交事务 COMMIT; -- 如果影响行数为0,说明版本号不匹配,更新失败,回滚事务并重试 ROLLBACK;
在Java业务代码中,重试逻辑可以这样实现:
public boolean deductStockWithOptimisticLock(int productId) {
int retryCount = 3;
while (retryCount > 0) {
// 查询商品库存和版本号
Product product = productDao.selectById(productId);
if (product.getStock() <= 0) {
return false;
}
// 执行更新,返回影响行数
int rows = productDao.updateStockWithVersion(productId, product.getStock() - 1, product.getVersion());
if (rows > 0) {
return true;
}
// 更新失败,重试次数减1
retryCount--;
}
return false;
}
两种锁方案的适用场景对比
悲观锁和乐观锁没有绝对的好坏,需要根据业务场景的特性选择:
| 对比维度 | 悲观锁 | 乐观锁版本号机制 |
|---|---|---|
| 冲突概率 | 适合冲突概率高的场景 | 适合冲突概率低的场景 |
| 并发性能 | 高并发下吞吐量低,锁竞争开销大 | 高并发下吞吐量高,无锁等待 |
| 实现复杂度 | 数据库原生支持,实现简单 | 需要额外维护版本号,更新失败需要重试,实现稍复杂 |
| 数据一致性 | 强一致性,操作期间数据不会被修改 | 最终一致性,更新失败需要重试保证最终正确 |
如果业务是秒杀、库存扣减这类冲突概率极高的场景,悲观锁能避免大量重试带来的额外开销;如果是用户资料修改、订单状态更新这类冲突概率低的场景,乐观锁版本号机制能大幅提升系统吞吐量,减少请求阻塞。
需要注意的是,乐观锁的重试次数不能设置过高,否则重试本身也会消耗资源,通常设置3-5次重试即可,超过次数可以提示用户操作失败。