@Service publicclassProductService{ @Autowired private ProductMapper productMapper; /** * 扣减库存(乐观锁实现) * @return true-扣减成功,false-库存不足或版本冲突需重试 */ publicbooleandeductStockWithOptimisticLock(Long productId, int quantity){ // 1. 查询商品信息(包含版本号) Product product = productMapper.selectById(productId); if (product.getStock() < quantity) { returnfalse; // 库存不足 } // 2. 执行更新,携带版本号条件 // UPDATE product SET stock = stock - #{quantity}, version = version + 1 // WHERE id = #{id} AND version = #{version} int affectedRows = productMapper.deductStockOptimistic( productId, quantity, product.getVersion() // 携带查询时的版本号 ); // 3. 判断是否更新成功 return affectedRows > 0; } /** * 带重试机制的乐观锁扣减 */ @Retryable(value = OptimisticLockException.class, maxAttempts= 3) publicbooleandeductStockWithRetry(Long productId, int quantity){ boolean success = deductStockWithOptimisticLock(productId, quantity); if (!success) { thrownew OptimisticLockException("版本冲突,请重试"); } returntrue; } }
Mapper XML:
<selectid="selectById"resultType="com.example.Product"> SELECT id, name, stock, version FROM product WHERE id = #{id} select> <updateid="deductStockOptimistic"> UPDATE product SET stock = stock - #{quantity}, version = version + 1 WHERE id = #{id} AND version = #{version} update>
悲观锁在 MySQL 中通过 SELECT ... FOR UPDATE 实现,利用数据库的排他锁(X 锁)机制。
1. FOR UPDATE 语法
img
上图展示了悲观锁的执行流程:
事务 A 首先执行 SELECT ... FOR UPDATE,数据库会对查询到的记录加排他锁(X 锁)
事务 B 同时也想对同一行执行 FOR UPDATE,由于锁被事务 A 持有,事务 B 会阻塞等待
事务 A 完成更新并 COMMIT 后,锁被释放
事务 B 此时才能获取到锁,继续执行
Java 代码示例:
@Service publicclassProductService{ @Autowired private ProductMapper productMapper; /** * 扣减库存(悲观锁实现) * 注意:必须在事务中执行 */ @Transactional publicbooleandeductStockWithPessimisticLock(Long productId, int quantity){ // 1. 加锁查询(FOR UPDATE) // SELECT id, name, stock FROM product WHERE id = #{id} FOR UPDATE Product product = productMapper.selectByIdForUpdate(productId); // 此时其他事务如果想操作这条记录,必须等待当前事务提交 if (product.getStock() < quantity) { returnfalse; // 库存不足 } // 2. 执行更新 productMapper.updateStock(productId, quantity); // 3. 事务提交时自动释放锁 returntrue; } }
Mapper XML:
<selectid="selectByIdForUpdate"resultType="com.example.Product"> SELECT id, name, stock FROM product WHERE id = #{id} FOR UPDATE select> <updateid="updateStock"> UPDATE product SET stock = stock - #{quantity} WHERE id = #{id} update>
2. 锁的范围与索引
⚠️ 重要:FOR UPDATE 的锁范围与索引密切相关:
场景
锁范围
风险
通过主键/唯一索引查询
只锁匹配的行
✅ 推荐
通过普通索引查询
锁索引匹配的所有行 + 间隙
⚠️ 可能扩大锁范围
无索引查询
锁整张表
❌ 严重性能问题
-- ✅ 推荐:通过主键查询,只锁一行 SELECT * FROM product WHEREid = 1FORUPDATE; -- ⚠️ 注意:无索引会锁表 SELECT * FROM product WHEREname = 'iPhone'FORUPDATE; -- name 无索引
3. 死锁预防
悲观锁可能导致死锁,需要遵循以下原则:
/** * 死锁预防原则: * 1. 按固定顺序加锁(如按 ID 升序) * 2. 避免长事务 * 3. 设置合理的锁等待超时 */ @Transactional publicvoidtransfer(Long fromId, Long toId, BigDecimal amount){ // 按ID升序加锁,避免循环等待 Long first = Math.min(fromId, toId); Long second = Math.max(fromId, toId); Account acc1 = accountMapper.selectByIdForUpdate(first); Account acc2 = accountMapper.selectByIdForUpdate(second); // 执行转账逻辑... }