悲观锁和乐观锁
摘要:SpringBoot中实现并发控制主要通过悲观锁和乐观锁。悲观锁适用于写频繁场景,使用数据库行级锁(@Lock)或Java同步机制(synchronized/ReentrantLock)保证线程安全;乐观锁适合读多写少场景,通过版本号(@Version)或CAS实现,需处理冲突重试。数据库悲观锁需配合@Transactional,乐观锁会抛出OptimisticLockingFailureE
在 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 UPDATESQL,锁定行。- 必须在 事务 (
@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. 实际应用建议
-
库存扣减(高并发):
- 如果冲突多,用 悲观锁(
FOR UPDATE)。 - 如果冲突少,用 乐观锁(
@Version)。 - 分布式环境考虑 Redis 分布式锁 或 ZooKeeper。
- 如果冲突多,用 悲观锁(
-
读多写少(如点赞):
- 优先 乐观锁(
@Version或 CAS)。 - 避免悲观锁导致的性能瓶颈。
- 优先 乐观锁(
-
分布式场景:
- 悲观锁依赖数据库,不适合跨库。
- 乐观锁需结合 分布式事务(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 中,悲观锁适合强一致性要求高的场景,乐观锁适合高并发读多写少的场景。根据业务选择合适的方案,并注意分布式环境下的扩展性。
更多推荐


所有评论(0)