悲观锁
悲观锁认为并发冲突很可能发生,在操作数据前先加锁,确保同一时间只有一个事务能操作数据。
实现方式
数据库层面
SELECT … FOR UPDATE(写锁)
-- 一锁二查三更新
BEGIN TRANSACTION;
-- 1. 加锁(行锁)
SELECT * FROM stock WHERE id = 1 FOR UPDATE;
-- 2. 检查库存
-- ...
-- 3. 更新库存 在锁保护下进行业务操作
UPDATE stock SET count = count - 1 WHERE id = 1;
COMMIT;SELECT … LOCK IN SHARE MODE(读锁)
BEGIN TRANSACTION;
-- 加共享锁,允许其他事务读但不允许写
SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE;
-- 检查余额等操作
-- ...
COMMIT;Java 代码示例
// deduct stock 扣除库存(写锁示例)
@Transactional
public boolean deductStock(Long stockId, int quantity) {
// 1. 查询并加锁(写锁)
Stock stock = stockRepository.findByIdForUpdate(stockId);
if (stock == null || stock.getCount() < quantity) {
return false;
}
// 2. 扣减库存
stock.setCount(stock.getCount() - quantity);
stockRepository.save(stock);
return true;
}
// 读锁示例 - 查询账户余额(不修改数据)
@Transactional
public BigDecimal getAccountBalance(Long accountId) {
// 使用读锁查询,允许其他事务读但阻止写
Account account = accountRepository.findByIdForShare(accountId);
return account != null ? account.getBalance() : BigDecimal.ZERO;
}Repository 层定义
@Repository
public interface StockRepository extends JpaRepository<Stock, Long> {
// 写锁 - SELECT ... FOR UPDATE
@Query(value = "SELECT * FROM stock WHERE id = ?1 FOR UPDATE", nativeQuery = true)
Stock findByIdForUpdate(Long id);
// 读锁 - SELECT ... LOCK IN SHARE MODE
@Query(value = "SELECT * FROM stock WHERE id = ?1 LOCK IN SHARE MODE", nativeQuery = true)
Stock findByIdForShare(Long id);
}读写锁对比
| 特性 | 写锁 (FOR UPDATE) | 读锁 (LOCK IN SHARE MODE) |
|---|---|---|
| 阻塞其他写操作 | 是 | 是 |
| 阻塞其他读锁 | 否 | 否(可共享) |
| 阻塞其他写锁 | 是 | 是 |
| 适用场景 | 需要修改数据 | 仅需读取数据,需保证一致性 |
特点
- 先加锁,再操作
- 适合并发冲突较多的场景
- 会降低并发性能
注意事项
锁的粒度
- 行锁:只锁定一行,性能较好
- 表锁:锁定整个表,性能差,慎用
死锁
- 多个事务互相等待对方释放锁
- 可能导致事务永远无法完成
- 需要合理设计加锁顺序
锁超时
- 设置锁等待超时时间,避免长时间阻塞
- 不同数据库超时设置方式不同
适用场景
- 并发冲突较多的场景
- 对数据一致性要求较高的场景
- 例如:库存扣减、账户余额变动