SQL乐观锁与悲观锁是数据库并发控制的核心机制,两者的设计思路和应用场景差异明显,合理运用并优化能大幅提升系统的并发处理能力和数据安全性。乐观锁假设并发冲突发生概率较低,操作时不主动加锁,提交时校验数据版本;悲观锁则假设冲突概率高,操作前先加锁,阻止其他事务修改数据。

核心原理回顾
悲观锁
悲观锁依赖数据库本身的锁机制实现,常见的是SELECT ... FOR UPDATE语句,执行时会锁定查询到的行,其他事务无法对这些行执行修改、删除操作,直到当前事务提交或回滚释放锁。不同数据库的悲观锁实现略有差异,MySQL的InnoDB引擎在RR隔离级别下,FOR UPDATE会使用行锁,若查询未命中索引则会升级为表锁。
-- MySQL悲观锁示例,锁定id为1的用户记录 START TRANSACTION; SELECT balance FROM user WHERE id = 1 FOR UPDATE; -- 执行业务逻辑,比如扣减余额 UPDATE user SET balance = balance - 100 WHERE id = 1; COMMIT;
乐观锁
乐观锁不依赖数据库锁机制,通常通过版本号或者时间戳实现。表中新增version字段,每次更新时校验版本号是否匹配,匹配则更新并递增版本号,不匹配则说明数据已被其他事务修改,当前操作失败。
-- 乐观锁更新示例,假设当前查询到的version为5 UPDATE product SET stock = stock - 1, version = version + 1 WHERE id = 10 AND version = 5; -- 若受影响行数为0,说明版本不匹配,更新失败
高级应用场景
悲观锁的适用场景
- 并发冲突概率高的场景,比如秒杀活动的库存扣减,大量请求同时操作同一行数据,乐观锁会导致大量更新失败,重试成本过高。
- 业务操作需要多步完成,且中间状态不能被其他事务干扰,比如转账操作,需要先锁定转出和转入账户,再执行金额变更。
- 数据一致性要求极高,不允许任何脏写、幻读的场景,比如金融核心交易数据修改。
乐观锁的适用场景
- 并发冲突概率低的场景,比如用户修改个人资料,同一时间很少有两个用户同时修改同一个账号的信息。
- 读多写少的场景,大部分操作是查询,只有少量更新操作,乐观锁不会阻塞读请求,能提升整体吞吐量。
- 分布式场景下跨服务的并发控制,不需要依赖数据库的长事务,版本号校验可以在应用层完成,适配微服务架构。
混合使用场景
在一些复杂业务中可以将两者结合,比如电商订单的支付回调场景:先使用悲观锁锁定订单记录,防止回调重复处理,再使用乐观锁校验订单状态版本,避免状态被其他业务线程修改。这种方式既保障了操作的互斥性,又避免了长事务持有锁的时间过长。
优化技巧
锁粒度优化
使用悲观锁时务必保证查询条件命中索引,避免行锁升级为表锁。可以通过EXPLAIN语句分析查询计划,确认是否使用了索引。如果业务需要锁定多行数据,尽量按照固定顺序加锁,避免死锁发生。
-- 错误示例:未命中索引,会升级为表锁 SELECT * FROM user WHERE name = '张三' FOR UPDATE; -- 正确示例:id是主键索引,使用行锁 SELECT * FROM user WHERE id = 1 FOR UPDATE;
乐观锁冲突优化
当乐观锁更新失败时,不要无限制重试,可以设置最大重试次数,超过次数后返回友好提示。同时可以优化版本号校验逻辑,比如允许版本号在一定范围内匹配,或者结合业务规则判断冲突是否可接受,减少不必要的重试。
// Java乐观锁重试示例
int maxRetry = 3;
int retryCount = 0;
boolean success = false;
while (retryCount < maxRetry) {
// 查询当前版本和数据
Product product = productDao.selectById(10);
if (product.getStock() <= 0) {
break;
}
// 执行乐观锁更新
int rows = productDao.updateStockWithVersion(10, product.getVersion());
if (rows > 0) {
success = true;
break;
}
retryCount++;
}
事务时长优化
无论是乐观锁还是悲观锁,都要尽量缩短事务持有锁的时间。将非数据库操作(比如调用外部接口、日志处理)放到事务之外,只把必要的数据库操作放在事务内部。对于悲观锁,事务提交或回滚后锁会立即释放,因此要避免在事务中执行耗时操作。
隔离级别适配
根据业务需求选择合适的数据库隔离级别,避免过高的隔离级别导致锁范围扩大。比如如果只是需要避免脏读,可以选择RC隔离级别,减少RR隔离级别下的间隙锁带来的性能影响。对于乐观锁来说,隔离级别主要影响查询到的数据版本,一般RC级别就能满足需求。
注意事项
使用悲观锁时要注意死锁问题,数据库一般会自动检测死锁并回滚代价较小的事务,但最好在业务层也做死锁重试处理。乐观锁的版本号字段要选择合适的数据类型,避免频繁更新导致溢出,一般使用INT或者BIGINT类型即可。另外,乐观锁不适合更新条件复杂的场景,比如更新条件涉及多个字段的校验,版本号校验可能会覆盖部分业务规则,需要额外处理。
实际开发中要根据业务并发量、冲突概率、一致性要求综合选择锁方案,没有绝对最优的锁,只有最适合当前场景的锁。