在 Spring Boot 中,悲观锁乐观锁的实现通常结合 数据库操作(如 JPA/Hibernate、MyBatis) 和 并发控制(如 @Transactional、CAS) 来完成。


1. 悲观锁(Pessimistic Locking)

定义

悲观锁通过 数据库行级锁(SELECT ... FOR UPDATE 或 Java 同步机制(synchronized/ReentrantLock 来保证线程安全,适用于 写操作频繁、冲突概率高 的场景(如库存扣减、金融交易)。


实现方式

1.1 数据库行级锁(JPA/Hibernate)

Spring Data JPA 提供了 @Lock 注解,可以配合 @Query 或 @Modifying 使用。

示例:库存扣减(悲观锁)

import org.springframework.data.jpa.repository.*;
import javax.persistence.*;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // 悲观锁:查询时加行级锁(FOR UPDATE)
    @Lock(LockModeType.PESSIMISTIC_WRITE) // 写锁
    @Query("SELECT p FROM Product p WHERE p.id = :id")
    Product findByIdForUpdate(@Param("id") Long id);
}

服务层调用(事务管理)

@Service
public class OrderService {

    @Autowired
    private ProductRepository productRepository;

    @Transactional // 确保操作在事务中执行
    public void deductStock(Long productId, int quantity) {
        // 1. 查询时加锁(阻塞其他线程)
        Product product = productRepository.findByIdForUpdate(productId);

        // 2. 检查库存
        if (product.getStock() < quantity) {
            throw new RuntimeException("库存不足!");
        }

        // 3. 扣减库存
        product.setStock(product.getStock() - quantity);
        productRepository.save(product);
    }
}

关键点

  • @Lock(LockModeType.PESSIMISTIC_WRITE) 会生成 SELECT ... FOR UPDATE SQL,锁定行。
  • 必须在 事务 (@Transactional) 中使用,否则锁会在查询后立即释放。
  • 适用于 MySQL、PostgreSQL 等支持行级锁的数据库。

1.2 使用 synchronized 或 ReentrantLock(Java 层面)

如果不想依赖数据库锁,可以在 Java 代码中手动加锁。

示例:使用 ReentrantLock

@Service
public class OrderService {

    private final Map<Long, ReentrantLock> locks = new ConcurrentHashMap<>();

    public void deductStock(Long productId, int quantity) {
        // 为每个商品创建一个锁(避免全局锁竞争)
        locks.computeIfAbsent(productId, k -> new ReentrantLock()).lock();
        try {
            // 1. 查询库存
            Product product = productRepository.findById(productId).orElseThrow();
            // 2. 检查并扣减
            if (product.getStock() < quantity) {
                throw new RuntimeException("库存不足!");
            }
            product.setStock(product.getStock() - quantity);
            productRepository.save(product);
        } finally {
            locks.get(productId).unlock(); // 释放锁
        }
    }
}

缺点

  • 需要手动管理锁,容易出现死锁或性能瓶颈。
  • 不适用于分布式环境(需要分布式锁,如 Redis 的 RedLock)。

2. 乐观锁(Optimistic Locking)

定义

乐观锁假设 冲突概率低,不加锁,而是通过 版本号(version 或 CAS(Compare-And-Swap) 来检测冲突。适用于 读多写少 的场景(如社交点赞、浏览量统计)。


实现方式

2.1 版本号机制(JPA/Hibernate)

JPA 提供了 @Version 注解,自动实现乐观锁。

步骤 1:实体类添加版本号

@Entity
public class Product {
    @Id
    private Long id;
    private int stock;

    @Version // 乐观锁版本号
    private int version;
}

步骤 2:更新时自动检查版本

@Service
public class OrderService {

    @Autowired
    private ProductRepository productRepository;

    @Transactional
    public void deductStock(Long productId, int quantity) {
        Product product = productRepository.findById(productId).orElseThrow();

        if (product.getStock() < quantity) {
            throw new RuntimeException("库存不足!");
        }

        // 更新时,Hibernate 自动检查 version 是否匹配
        product.setStock(product.getStock() - quantity);
        productRepository.save(product); // 如果 version 不匹配,抛出 OptimisticLockingFailureException
    }
}

原理

  • save() 时,Hibernate 生成类似以下 SQL:
    UPDATE product SET stock = ?, version = version + 1
    WHERE id = ? AND version = ?;
  • 如果 version 不匹配,抛出 OptimisticLockingFailureException(需要捕获并重试)。

2.2 手动版本号(MyBatis)

如果使用 MyBatis,可以手动实现版本号检查。

步骤 1:数据库表添加 version 字段

ALTER TABLE product ADD COLUMN version INT DEFAULT 0;

步骤 2:Mapper XML 配置

<update id="deductStock">
    UPDATE product
    SET stock = stock - #{quantity}, version = version + 1
    WHERE id = #{productId} AND version = #{version}
</update>

步骤 3:服务层调用

@Service
public class OrderService {

    @Autowired
    private ProductMapper productMapper;

    @Transactional
    public void deductStock(Long productId, int quantity) {
        Product product = productMapper.selectById(productId);

        if (product.getStock() < quantity) {
            throw new RuntimeException("库存不足!");
        }

        int updated = productMapper.deductStock(productId, quantity, product.getVersion());
        if (updated == 0) {
            throw new OptimisticLockingFailureException("并发冲突,请重试!");
        }
    }
}

2.3 CAS(Compare-And-Swap)

Java 的 AtomicInteger 等原子类使用 CAS 实现乐观锁。

示例:使用 AtomicInteger 管理库存

@Service
public class OrderService {

    private final Map<Long, AtomicInteger> stockMap = new ConcurrentHashMap<>();

    public void deductStock(Long productId, int quantity) {
        stockMap.computeIfAbsent(productId, k -> new AtomicInteger(100)); // 初始库存 100

        AtomicInteger stock = stockMap.get(productId);
        while (true) {
            int current = stock.get();
            if (current < quantity) {
                throw new RuntimeException("库存不足!");
            }
            // CAS 尝试更新
            if (stock.compareAndSet(current, current - quantity)) {
                break;
            }
            // 如果失败,重试
        }
    }
}

适用场景

  • 单机内存操作(如缓存)。
  • 不适用于分布式环境(需要分布式 CAS,如 Redis 的 INCR + Lua 脚本)。

3. 悲观锁 vs 乐观锁在 Spring Boot 中的选择

特性 悲观锁 乐观锁
适用场景 写多读少(库存、金融) 读多写少(点赞、浏览量)
实现方式 @Lock(PESSIMISTIC_WRITE)synchronized @Version、CAS、手动版本号
并发性能 低(阻塞) 高(非阻塞)
冲突处理 自动阻塞 需手动重试(OptimisticLockingFailureException
分布式支持 数据库锁(局限于单库) 版本号(需分布式事务)

4. 实际应用建议

  1. 库存扣减(高并发)

    • 如果冲突多,用 悲观锁FOR UPDATE)。
    • 如果冲突少,用 乐观锁@Version)。
    • 分布式环境考虑 Redis 分布式锁 或 ZooKeeper
  2. 读多写少(如点赞)

    • 优先 乐观锁@Version 或 CAS)。
    • 避免悲观锁导致的性能瓶颈。
  3. 分布式场景

    • 悲观锁依赖数据库,不适合跨库。
    • 乐观锁需结合 分布式事务(Seata) 或 Redis Lua 脚本

5. 常见问题与解决方案

5.1 悲观锁导致死锁

问题:多个事务相互等待锁,形成死锁。
解决

  • 设置超时:@Transactional(timeout = 5)
  • 按固定顺序获取锁(如 productId 升序)。

5.2 乐观锁冲突重试

问题OptimisticLockingFailureException 需要手动重试。
解决

@Retryable(value = OptimisticLockingFailureException.class, maxAttempts = 3)
@Transactional
public void deductStock(Long productId, int quantity) {
    // ...
}

5.3 分布式环境下的锁

问题:单机锁(synchronized/ReentrantLock)在分布式环境失效。
解决

  • 使用 Redis 分布式锁Redisson)。
  • 使用 ZooKeeper 分布式锁

6. 总结

场景 推荐方案 Spring Boot 实现
高并发写(库存) 悲观锁(数据库行锁) @Lock(PESSIMISTIC_WRITE) + @Transactional
读多写少(点赞) 乐观锁(版本号) @Version + 重试机制
单机内存操作 CAS(AtomicInteger compareAndSet()
分布式环境 Redis 分布式锁 / ZooKeeper Redisson 或 Curator

在 Spring Boot 中,悲观锁适合强一致性要求高的场景,乐观锁适合高并发读多写少的场景。根据业务选择合适的方案,并注意分布式环境下的扩展性。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐