救命!我的 Spring 事务正在裸奔,罪魁祸首竟是这个配置!

101 阅读5分钟

在 Spring 里,事务的隔离级别就像是给数据访问设定的“防护罩”,一共有 五种,它们决定了不同事务之间的数据互相影响的程度:

1️⃣ DEFAULT(默认)
  • 概念:Spring 使用数据库默认的隔离级别。通常情况下,数据库的默认隔离级别是 READ_COMMITTED,但这取决于所用的数据库。例如,Oracle 默认隔离级别是 READ_COMMITTED,MySQL 默认隔离级别是REPEATABLE READ
  • 应用:当你不显式设置事务隔离级别时,Spring 会使用数据库默认的设置。这意味着,隔离级别会根据不同的数据库而有所不同。
2️⃣ READ_UNCOMMITTED(读未提交)
  • 概念:最底层的隔离级别,允许读取未提交的数据。

  • 问题

    • 脏读:一个事务可以读取另一个事务尚未提交的数据。如果那个事务回滚,那么读取到的数据就会是错误的。
    • 不可重复读:事务内多次读取同一数据,结果不同,因为其他事务已修改数据。
    • 幻读:事务内多次查询时,查询结果集不同,因为其他事务可能插入或删除了数据。
  • 应用场景:适用于数据一致性要求不高且对性能要求较高的场景,如实时分析,日志系统等。

3️⃣ READ_COMMITTED(读已提交)
  • 概念:事务只能读取已提交的数据,这避免了脏读。

  • 问题

    • 不可重复读:如果事务内多次读取同一数据,结果可能不同,因为其他事务可以修改已提交的数据。
    • 幻读:如果一个事务进行多次查询时,其他事务可能插入、删除数据,导致查询结果集不同。
  • 应用场景:大多数应用场景中是默认选择的隔离级别,性能较好且避免了脏读,适用于数据一致性要求适中的应用。

4️⃣ REPEATABLE_READ(可重复读)
  • 概念:确保在同一事务内的多次读取结果一致,避免脏读和不可重复读。

  • 问题

    • 幻读:虽然避免了脏读和不可重复读,但其他事务仍然可以插入新数据,导致查询结果集发生变化。
  • MySQL 实现:通过 MVCC(多版本并发控制)技术,MySQL 能有效避免脏读和不可重复读,减少幻读的发生,但不完全消除幻读。

  • 应用场景:适用于需要强一致性的场景,如银行业务、金融系统等。

5️⃣ SERIALIZABLE(可串行化)
  • 概念:强制所有事务按顺序执行,这样就避免了脏读、不可重复读和幻读。
  • 代价:性能显著下降,因为所有事务都必须按顺序执行,这会导致很多事务处于等待状态。
  • 应用场景:适用于对数据一致性要求极高的场景,如资金转账、订单系统等,但需要权衡性能影响。

🔧 Spring 中事务隔离级别的设置

Spring 提供了 @Transactional 注解来设置事务的隔离级别:

import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Isolation;

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void processTransaction() {
    // 核心业务逻辑
}

isolation 属性可以设置为:

  • Isolation.DEFAULT:数据库默认隔离级别
  • Isolation.READ_UNCOMMITTED:读未提交
  • Isolation.READ_COMMITTED:读已提交
  • Isolation.REPEATABLE_READ:可重复读
  • Isolation.SERIALIZABLE:可串行化

🌍 隔离级别与脏读、不可重复读、幻读的关系

脏读是指一个事务读到了另一个事务未提交的“脏数据”。
不可重复读是指在一个事务内多次读取同一数据,由于其他事务的修改导致数据不一致。
幻读是指一个事务读取到了其他事务插入的“幻行”。

隔离级别脏读不可重复读幻读
READ_UNCOMMITTED(读未提交)✅ 可能发生✅ 可能发生✅ 可能发生
READ_COMMITTED(读已提交)❌ 避免✅ 可能发生✅ 可能发生
REPEATABLE_READ(可重复读)❌ 避免❌ 避免✅ 可能发生
SERIALIZABLE(可串行化)❌ 避免❌ 避免❌ 避免

🛠️ 扩展知识

🚀 隔离级别与性能的权衡

  • 低隔离级别(READ_UNCOMMITTED 和 READ_COMMITTED) :虽然能够提升并发性能,但在数据一致性方面有所牺牲,可能导致脏读、不可重复读或幻读。
  • 高隔离级别(REPEATABLE_READ 和 SERIALIZABLE) :确保数据一致性,但性能开销较大,因为需要更多的资源来管理并发事务。

🏦 数据库对隔离级别的实现差异

  • MySQL:默认使用 REPEATABLE_READ,通过 MVCC 机制来避免脏读和不可重复读,但幻读依然可能存在。
  • Oracle:默认使用 READ_COMMITTED,并通过内存中的 undo 日志来保证事务数据的一致性。

🛑 SERIALIZABLE 的本质目标

SERIALIZABLE 是 SQL 标准中最高级别的事务隔离,其核心目标是:

保证并发执行的多个事务,其最终结果等价于这些事务按某种顺序一个接一个地执行(即“串行”执行)

但这并不要求事务物理上真的一个一个运行。现代数据库通过更智能的技术,在逻辑上达到串行化效果的同时,尽可能提升并发性能

SERIALIZABLE实现方式原理性能代表数据库
基于锁的串行化(Locking-Based)使用严格的共享锁、排他锁、范围锁(如间隙锁)来阻止并发访问低,并发差,容易死锁MySQL(InnoDB)
可串行化快照隔离(SSI, Serializable Snapshot Isolation)基于 MVCC 快照 + 冲突检测机制,在提交时判断是否存在“危险结构”高,支持高并发读写PostgreSQL、Google Spanner
时间戳排序 / 多版本控制增强版Oracle 使用增强的多版本一致性模型,在快照基础上进行写冲突检测中高,优于传统锁机制Oracle

📝 实践建议

  1. 事务设计时,需根据业务需求选择合适的隔离级别。如果业务对一致性要求较高,可以选择 REPEATABLE_READ 或 SERIALIZABLE,但要注意它们带来的性能代价。
  2. 性能优先的场景:如日志系统、数据分析等,可以选择较低的隔离级别(READ_COMMITTED 或 READ_UNCOMMITTED)。
  3. 使用 @Transactional 注解时,根据具体的业务场景调整隔离级别,确保既能保证数据一致性,又能在性能上达成平衡。