悲观锁适合写多、强一致、不能错场景,需事务控制、索引命中、短锁持有;乐观锁适合读多、写少、可重试场景,依赖version字段比对,失败需重试或提示;选型看冲突频率、写代价、是否分布式。

Java 与 MySQL 协同处理高并发时,乐观锁和悲观锁不是“选一个就行”,而是要根据业务特征、数据冲突概率、重试成本、一致性要求来决定用哪一种,甚至混合使用。核心不在于技术多炫,而在于锁的时机、粒度和释放是否精准匹配真实场景。
悲观锁:适合“写多、强一致、不能错”的场景
在 Java 中调用 MySQL 悲观锁,本质是让数据库在事务内对特定行加排他锁(X 锁),其他事务必须等待。关键不在 Java 写法,而在 SQL 和事务控制是否到位:
- 必须显式开启事务:用 @Transactional 注解或手动执行
BEGIN,且不能依赖默认自动提交(autocommit=1); - 加锁语句必须命中索引:例如
SELECT * FROM order WHERE id = ? FOR UPDATE—— 若id是主键,锁行为是行级;若用name且无索引,会升级为表锁,直接拖垮并发能力; - 锁持有时间要短:查到数据后尽快完成业务逻辑(如校验库存、生成订单),避免在事务里做 HTTP 调用、文件读写等耗时操作;
- 注意死锁风险:多个事务按不同顺序访问多行(如事务 A 先锁 id=1 再锁 id=2,事务 B 反过来),MySQL 可能抛出
Deadlock found,需捕获并重试。
乐观锁:适合“读多、写少、可重试”的场景
Java 层实现乐观锁,通常不依赖数据库锁机制,而是靠字段比对完成冲突检测。最常用的是 version 版本号 方式:
- 表结构中增加
version INT DEFAULT 0字段; - 查询时一并查出 version 值(如 MyBatis-Plus 的
selectById自动映射); - 更新时用带条件的 SQL:
UPDATE product SET stock = stock - 1, version = version + 1 WHERE id = ? AND version = ?; - Java 判断
update返回影响行数:为 0 表示 version 不匹配(已被别人更新),此时可选择立即重试、返回失败提示,或进队列异步处理。
注意:乐观锁无法防止“ABA 问题”或中间状态被跳过(比如两次更新之间有非法中间值),但对库存扣减、积分变动这类幂等性较强的业务已足够可靠。
怎么选?看三个关键指标
不用凭感觉,对照业务实际回答以下问题:
- 冲突频率高吗? —— 秒杀商品、抢红包、账户余额实时变动,冲突概率 >10%,优先悲观锁;用户资料修改、文章点赞、配置更新,冲突概率极低,选乐观锁;
- 写操作代价大吗? —— 如果一次更新要调三次外部接口、写五张表、发消息,重试成本太高,不适合乐观锁反复失败重试;
- 系统是否分布式? —— 单体服务可用数据库锁兜底;微服务+多实例时,悲观锁仍有效(因锁在 MySQL),但乐观锁更轻量、无跨节点协调开销,也更容易对接 Redis 分布式锁做兜底。
实战中常被忽略的细节
很多并发问题不是锁没用,而是用错了地方:
- FOR UPDATE 后没 COMMIT 或 ROLLBACK:锁会一直挂着,导致后续请求长时间阻塞,甚至连接池耗尽;
- 查询为空时仍执行 FOR UPDATE:InnoDB 会对查询范围加间隙锁(gap lock),可能意外锁住一大片不存在的记录,引发死锁;
- MyBatis-Plus 的 @Version 注解没配生效:需确认全局配置开启乐观锁插件,且实体类字段类型与数据库一致(如 Integer vs int);
-
时间戳代替 version 时精度不足:MySQL 的
datetime(3)在毫秒级并发下仍可能撞车,不如自增 version 稳定。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











