悲观锁

悲观锁认为并发冲突很可能发生,在操作数据前先加锁,确保同一时间只有一个事务能操作数据。

实现方式

数据库层面

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)
阻塞其他写操作
阻塞其他读锁否(可共享)
阻塞其他写锁
适用场景需要修改数据仅需读取数据,需保证一致性

特点

  • 先加锁,再操作
  • 适合并发冲突较多的场景
  • 会降低并发性能

注意事项

锁的粒度

  • 行锁:只锁定一行,性能较好
  • 表锁:锁定整个表,性能差,慎用

死锁

  • 多个事务互相等待对方释放锁
  • 可能导致事务永远无法完成
  • 需要合理设计加锁顺序

锁超时

  • 设置锁等待超时时间,避免长时间阻塞
  • 不同数据库超时设置方式不同

适用场景

  • 并发冲突较多的场景
  • 对数据一致性要求较高的场景
  • 例如:库存扣减、账户余额变动

参考链接