人人都会AI编程

10.3 事务隔离级别与锁机制

更新时间:2026-07-11

事务保证了操作的原子性,但当多个事务并发执行时,相互之间可能产生干扰。数据库通过隔离级别锁机制来解决并发问题,Spring 则提供了声明式的配置方式,让开发者在业务层就能精细控制这些底层行为,而无需编写与数据库相关的样板代码。

10.3.1 并发事务带来的问题

理解隔离级别之前,先要清楚并发场景下到底会出现哪些异常现象。SQL 标准定义了四种典型问题:

  • 脏读(Dirty Read):一个事务读到了另一个事务尚未提交的修改。如果后一个事务回滚,读到的数据就是无效的“脏数据”。
  • 不可重复读(Non-Repeatable Read):同一个事务内,两次读取同一行数据,结果却不同。因为中间有其他事务修改了该行并提交。
  • 幻读(Phantom Read):同一个事务内,两次执行同样的条件查询,结果集的行数不同。因为中间有其他事务插入了满足条件的新行。
  • 丢失更新(Lost Update):两个事务同时读取同一行,基于读取的值进行修改后提交,后提交的事务覆盖了前者的修改,导致前者的更新丢失。

不同数据库对这些问题的解决程度不同,而隔离级别就是一套标准的防护等级。

10.3.2 四种事务隔离级别

SQL 标准定义了四种隔离级别,从宽松到严格依次为:

  1. READ UNCOMMITTED(读未提交)
  • 允许事务读取其他事务未提交的数据。
  • 存在脏读、不可重复读、幻读风险。
  • 性能最高,但一致性最差,极少在生产中使用。
  1. READ COMMITTED(读已提交)
  • 事务只能读取其他事务已提交的数据,解决了脏读。
  • 不可重复读和幻读仍然可能发生。
  • 是 Oracle、PostgreSQL 的默认级别,许多场景下的合理选择。
  1. REPEATABLE READ(可重复读)
  • 保证同一个事务内多次读取同一行数据的结果一致,解决了不可重复读。
  • 幻读仍可能出现(部分数据库如 MySQL InnoDB 通过 MVCC 和间隙锁对幻读也有一定抑制,但不完全符合 SQL 标准的幻读定义)。
  • MySQL 的默认级别,适合对读一致性要求较高的场景。
  1. SERIALIZABLE(串行化)
  • 强制事务串行执行,所有读写都加锁,彻底避免脏读、不可重复读和幻读。
  • 并发性能急剧下降,只在对数据一致性要求极高且并发量较低的场合使用。

实际项目中,绝大多数场景使用 READ COMMITTEDREPEATABLE READ。前者并发度更好,后者能避免同一事务内读数据的前后不一致。

10.3.3 Spring 中声明隔离级别

Spring 事务抽象将上述隔离级别对应为 @Transactional 注解的 isolation 属性:

@Service
public class OrderService {

    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void processOrder(Long orderId) {
        // 该事务使用 READ COMMITTED 隔离级别
        Order order = orderRepository.findById(orderId).orElseThrow();
        // ...
    }
}

Isolation 枚举值包括:

  • DEFAULT:使用底层数据库的默认隔离级别(最常用,保持与数据库一致)。
  • READ_UNCOMMITTED
  • READ_COMMITTED
  • REPEATABLE_READ
  • SERIALIZABLE

重要提示:并非所有数据库都完整支持所有级别,具体行为取决于数据库实现。修改隔离级别会增加数据库开销,应仅在明确需要时覆盖默认值。

10.3.4 锁机制:悲观锁与乐观锁

隔离级别本质上是数据库通过锁或多版本并发控制(MVCC)实现的。Spring 除了让开发者声明隔离级别,还允许在应用层控制锁策略,形成互补。

悲观锁(Pessimistic Lock)

悲观锁假定并发冲突极可能发生,因此在访问数据时立即加锁,阻止其他事务同时修改。

  • 共享锁(读锁):允许其他事务读,但不允许写。
  • 排他锁(写锁):不允许其他事务读取或写入。

在 Spring Data JPA 中,可以通过 @Lock 注解使用悲观锁:

public interface OrderRepository extends JpaRepository<Order, Long> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT o FROM Order o WHERE o.id = :id")
    Optional<Order> findByIdForUpdate(@Param("id") Long id);
}

对应的 SQL 会发出 SELECT ... FOR UPDATE,锁住查询到的行,直到事务结束才释放。

使用场景:争抢激烈的资源(如扣减库存、账户扣款),冲突概率高的操作。缺点是会降低并发度,容易引发死锁,需要严格控制事务范围。

乐观锁(Optimistic Lock)

乐观锁假设冲突很少发生,不在数据库层面加锁,而是通过版本号或时间戳来检测冲突。

JPA 提供了简单的实现方式:在实体中增加一个用 @Version 注解的字段:

@Entity
public class Product {
    @Id
    private Long id;
    private String name;
    private Integer stock;

    @Version
    private Long version;  // 版本号
}

每次更新数据时,JPA 会自动检查 version 字段。更新语句会形如:

UPDATE product SET stock = ?, version = version + 1
WHERE id = ? AND version = ?

如果 WHERE 条件影响的行数为 0,说明在读取之后数据已被其他事务修改,此时 JPA 会抛出 OptimisticLockException,开发者可以捕获后重试或提示用户。

使用场景:读多写少、冲突概率低的业务,如用户资料更新、文章编辑等。乐观锁不会阻塞读操作,并发性能高,但需要处理重试逻辑。

10.3.5 真实场景下的组合运用

不同业务对一致性和并发性的需求差异很大,开发中经常混合使用隔离级别和锁机制:

  • 账户转账:使用 REPEATABLE READ + 悲观锁 SELECT ... FOR UPDATE,既保证事务内多次读取余额一致,又防止并发扣款超额。
  • 秒杀扣库存:通常采用乐观锁,在扣减时检查版本号,失败后快速重试;也可以使用数据库行锁,但要控制事务粒度,避免长事务锁竞争。
  • 报表统计:多数情况下 READ COMMITTED 足够,如果需要快照式一致性,可以提升到 REPEATABLE READ 或使用只读事务,避免幻读影响统计结果。
  • 订单状态流转:单纯的状态更新并发冲突不高,乐观锁最为合适,无需提升隔离级别。

10.3.6 常见陷阱与最佳实践

  1. 不要滥用 SERIALIZABLE:它会导致大量锁等待和超时,绝大多数业务可通过其他手段(如乐观锁、合理的事务边界)达到数据一致性要求。
  2. 注意数据库默认隔离级别的差异:MySQL 默认为 REPEATABLE READ,而 PostgreSQL、Oracle 默认为 READ COMMITTED。Spring 的 DEFAULT 会跟随数据库设置,迁移数据库时需要验证行为差异。
  3. 悲观锁要与事务边界配合:加锁操作必须包裹在事务内,并且事务应尽快提交,否则锁持有时间过长,严重影响并发。避免在锁内调用外部服务或执行耗时计算。
  4. 乐观锁的重试与幂等:自动重试时要确保操作幂等性,防止重复扣款。可使用本地重试结合唯一索引或分布式锁兜底。
  5. 隔离级别只影响读:即使设置了最高隔离级别,更新操作依然可能产生丢失更新。对写操作的保护更多依赖于锁机制(悲观或乐观)。
  6. 结合日志与监控:生产环境中应监控数据库锁等待、死锁日志,适时调整事务边界和索引,保证吞吐量。

掌握了事务隔离级别和锁机制的用法,就可以在 Spring 中灵活地为不同业务场景配置合适的数据保护策略,既保数据一致性,又兼顾系统并发能力。