跳到主要内容

10.6 隔离级别

隔离级别决定多个事务并发读写同一批数据时,能看到什么结果。dbVisitor 的 Isolation 最终会映射到 JDBC Connection#setTransactionIsolation(int)。

隔离级别可以用于三种事务入口:

注解式事务
@Transactional(isolation = Isolation.READ_COMMITTED)
public void createOrder(long orderId) {
...
}
模板事务
txTemplate.execute(tranStatus -> {
...
return null;
}, Propagation.REQUIRED, Isolation.REPEATABLE_READ);
编程式事务
TransactionStatus tran = txManager.begin(
Propagation.REQUIRED,
Isolation.SERIALIZABLE
);

如何选择​

想达到的效果常用隔离级别
使用数据库默认配置DEFAULT
避免读到未提交数据,兼顾并发性能READ_COMMITTED
同一事务内多次读取同一行结果保持一致REPEATABLE_READ
要求最高一致性,接受明显并发性能损耗SERIALIZABLE
明确允许脏读,只追求很低一致性成本READ_UNCOMMITTED
信息

不同数据库对默认隔离级别和具体语义的实现可能不同。例如 MySQL、PostgreSQL、Oracle 对 REPEATABLE_READ、幻读、锁的处理并不完全一样。dbVisitor 负责设置 JDBC 隔离级别,但不能改变数据库自身的事务模型。

不同的隔离级别均以下面这张表为例子:

mysql> select * from students;
+----+-------+
| id | name |
+----+-------+
| 1 | Alice |
+----+-------+

DEFAULT​

默认事务隔离级别,具体使用的隔离级别由数据库驱动决定。

  • 常量 Isolation.DEFAULT
使用数据库默认隔离级别
@Transactional
public void createOrder(long orderId) {
...
}

脏读 (Read Uncommitted)​

最低的隔离级别。事务 A 可以读到事务 B 更新后但未提交的数据。如果事务 B 回滚,事务 A 读到的就是脏数据。

  • 常量 Isolation.READ_UNCOMMITTED
显式允许 READ_UNCOMMITTED
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public List<OrderInfo> queryFastButWeakConsistent() {
return orderMapper.queryRecentOrders();
}
时序事务 A事务 B效果
T1set isolation level read uncommitted
T2beginbegin
T3update students set name='bob' where id=1
T4select * from students where id=1读到 'bob'(脏读)
T5rollback
T6select * from students where id=1读回 'Alice'

不可重复读 (Read Committed)​

事务 A 只能读取到其他事务已提交的数据。但在同一事务中,两次相同查询可能返回不同结果(因为期间有其他事务提交了修改)。

  • 常量 Isolation.READ_COMMITTED
常见业务写入使用 READ_COMMITTED
@Transactional(isolation = Isolation.READ_COMMITTED)
public void payOrder(long orderId) {
orderMapper.markPaid(orderId);
orderMapper.insertPayLog(orderId);
}
时序事务 A事务 B效果
T1set isolation level read committedset isolation level read committed
T2beginbegin
T3select * from students where id=1读到 'Alice'
T4update students set name='bob' where id=1
T5select * from students where id=1仍读到 'Alice'(未提交不可见)
T6commit
T7select * from students where id=1读到 'bob'(不可重复读)
T8commit

可重复读 (Repeatable Read)​

防止同一事务重复读取已有行时受到其他事务已提交修改的影响。自身写入、范围内新增行和锁定读的行为还取决于数据库实现。

  • 常量 Isolation.REPEATABLE_READ
同一事务内需要稳定读取
@Transactional(isolation = Isolation.REPEATABLE_READ)
public OrderSummary buildOrderSummary(long orderId) {
Order order = orderMapper.queryOrder(orderId);
List<OrderItem> items = orderMapper.queryItems(orderId);
return new OrderSummary(order, items);
}
幻读

在 Repeatable Read 下,一个事务可能遇到幻读(Phantom Read): 幻读通常指同一范围查询因其他事务提交而出现或消失记录。下面展示 MySQL InnoDB 中一致性读与更新操作可能观察到不同数据的情形,不是所有数据库在该级别的共同表现。

时序事务 A事务 B效果
T1set isolation level repeatable readset isolation level repeatable read
T2beginbegin
T3select * from students where id=99结果为空
T4insert into students (id, name) values (99, 'bob')
T5commit
T6select * from students where id=99仍为空(可重复读)
T7update students set name='alice' where id=99更新成功
T8select * from students where id=99出现数据(幻读)

可串行化 (Serializable)​

要求已提交事务的效果等价于按某种顺序串行执行,不意味着事务必须逐个运行。它排除脏读、不可重复读、幻读及不可串行化结果。

  • 常量 Isolation.SERIALIZABLE
需要最高一致性时使用
@Transactional(isolation = Isolation.SERIALIZABLE)
public void allocateUniqueNumber(String bizType) {
Long nextNumber = numberMapper.queryNextNumber(bizType);
numberMapper.updateNextNumber(bizType, nextNumber + 1);
}
性能影响

数据库可能通过锁或并发冲突检测实现此级别,并不等同于全库排他锁。需要考虑等待、死锁或序列化失败;发生可重试冲突时,应按数据库要求重试整个事务。详见 PostgreSQL 事务隔离说明。

各级别对比​

下表描述标准允许的现象;数据库可以提供更强保证,例如 PostgreSQL 的 Repeatable Read 不允许幻读。

隔离级别脏读不可重复读幻读
READ_UNCOMMITTED可能可能可能
READ_COMMITTED-可能可能
REPEATABLE_READ--可能
SERIALIZABLE---

深入阅读​

  • 注解式事务:在 @Transactional 上指定隔离级别。
  • 模板事务:在 execute 的第三个参数中指定隔离级别。
  • 跨 API 事务:了解不同 API 如何共享事务连接及其隔离级别。