java中用select for update实现悲观锁,需在事务中执行、数据库支持行级锁、禁用自动提交;spring推荐@transactional,jdbc需手动设autocommit=false并显式提交;sql要明确锁定范围,避免全表扫描;注意锁超时与异常处理。

在 Java 中使用 SELECT FOR UPDATE 实现悲观锁,核心是通过 JDBC 在事务中执行带 FOR UPDATE 的查询语句,让数据库对选中的行加写锁,防止其他事务并发修改。关键点在于:必须在事务中执行、数据库需支持行级锁(如 MySQL InnoDB)、连接不能自动提交。
确保事务环境和手动提交
Spring 或原生 JDBC 都必须显式控制事务边界。若使用 Spring,推荐用 @Transactional 注解,并确认传播行为为 REQUIRED(默认),且事务管理器已正确配置(如 DataSourceTransactionManager)。
- Spring Boot 默认开启事务自动代理,但需确保方法是 public 且被 Spring 容器管理(非 this. 调用)
- JDBC 原生方式需调用
connection.setAutoCommit(false),并在业务完成后手动commit()或rollback() - 避免在锁住记录后做耗时操作(如远程调用、复杂计算),否则会延长锁持有时间,影响并发性能
编写带 FOR UPDATE 的 SQL 查询
SQL 语句需明确指定锁定范围。常见写法:
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
-
SELECT * FROM order WHERE id = ? FOR UPDATE;—— 锁单行(主键精确匹配) -
SELECT * FROM inventory WHERE product_code = ? FOR UPDATE;—— 若product_code有唯一索引,也锁单行;无索引则可能升级为表锁或锁多行 - MySQL 中,
FOR UPDATE在可重复读(RR)隔离级别下使用 next-key lock(间隙锁 + 行锁),能防止幻读 - 避免
SELECT ... FOR UPDATE LIMIT 1不带 WHERE 条件,易引发全表扫描和锁表风险
配合 MyBatis 或 JdbcTemplate 使用
MyBatis 中直接在 XML 或注解 SQL 中写 FOR UPDATE 即可(注意空格和分号):
JdbcTemplate 示例:
String sql = "SELECT * FROM account WHERE user_id = ? FOR UPDATE";Account account = jdbcTemplate.queryForObject(sql, new Object[]{userId}, accountRowMapper);
- MyBatis-Plus 从 3.4.0+ 支持
lambdaQuery().forUpdate()方法,底层仍生成相同 SQL - 注意:MyBatis 默认不缓存
FOR UPDATE查询结果,无需额外配置 - 若查询返回 null(记录不存在),则不加锁,后续 insert 时需自行处理并发插入问题(如先 select 再 insert 可能发生重复,建议用 insert on duplicate key/update or ignore)
异常与超时控制
数据库锁等待可能阻塞甚至死锁,应主动设限:
- MySQL 可通过
innodb_lock_wait_timeout设置全局锁等待超时(默认 50 秒),也可在会话级执行SET innodb_lock_wait_timeout = 5; - 应用层可用
tryLock思路:SQL 加FOR UPDATE NOWAIT(Oracle/PostgreSQL 支持),MySQL 8.0.1 以上支持FOR UPDATE SKIP LOCKED和FOR UPDATE NOWAIT,失败时抛LockWaitTimeoutException,可捕获后重试或降级 - Spring 中可通过
@Transactional(timeout = 10)控制事务总时长,避免长时间挂起
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










