JDBC中用SELECT ... FOR UPDATE行级锁保证库存一致性,需在显式事务中执行、命中索引、精确WHERE条件;替代方案是带条件的UPDATE语句一次完成扣减。

在 JDBC 中用行级锁保证库存扣减一致性,核心是借助数据库的 SELECT ... FOR UPDATE 语句,在事务中对目标库存记录加写锁,阻止其他并发事务读取并修改同一行,从而避免超卖。
使用 SELECT ... FOR UPDATE 显式加锁
这是最常用且直接的方式。需确保:
- 操作在 显式事务内执行(关闭自动提交,手动 commit/rollback);
- 查询语句必须命中索引(如主键或唯一索引),否则可能升级为表锁,严重降低并发性能;
- WHERE 条件要精确,避免锁住不相关行(例如用
id = ?而非name = ?且 name 无索引)。
示例代码片段:
conn.setAutoCommit(false);
String sql = "SELECT stock FROM products WHERE id = ? FOR UPDATE";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, productId);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
int currentStock = rs.getInt("stock");
if (currentStock >= needCount) {
// 执行更新
String updateSql = "UPDATE products SET stock = stock - ? WHERE id = ?";
try (PreparedStatement ups = conn.prepareStatement(updateSql)) {
ups.setInt(1, needCount);
ups.setLong(2, productId);
ups.executeUpdate();
}
conn.commit();
} else {
conn.rollback();
throw new RuntimeException("库存不足");
}
}
}
注意隔离级别与锁行为
MySQL InnoDB 默认可重复读(REPEATABLE READ),SELECT ... FOR UPDATE 会加临键锁(Next-Key Lock),既锁记录又锁间隙,防止幻读。但要注意:
- 若查询条件不满足任何记录,InnoDB 仍可能锁住插入意向间隙,影响并发插入;
- 读已提交(READ COMMITTED)下,
FOR UPDATE只锁匹配到的行,不锁间隙,但需配合更严格的业务校验; - 不要依赖“先查后更”(非原子)逻辑,必须把查和更放在同一事务+锁机制下。
替代方案:用 UPDATE 带条件一次完成
如果只需扣减且不依赖中间值做复杂判断,可跳过显式 SELECT,直接用带条件的 UPDATE:
String sql = "UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?";
int affected = stmt.executeUpdate(sql);
if (affected == 0) {
throw new RuntimeException("库存不足或已被扣完");
}
这种方式原子性强、无需额外锁语句、减少网络往返,且天然具备行级锁(UPDATE 本身会加 X 锁)。适合简单扣减场景。
补充建议
- 锁粒度尽量小:按商品 ID 扣减,而不是按品类或全表;
- 事务尽量短:加锁后快速完成更新并提交,避免长时间持有锁;
- 合理设置数据库连接池和超时(如
queryTimeout),防止死锁或长事务拖垮系统; - 生产环境建议配合 Redis 预减库存 + 数据库最终校验,兼顾性能与强一致。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











