事务隔离级别
事务隔离级别控制并发事务之间的可见性,解决数据一致性问题。
并发问题
| 问题 | 说明 | 示例 |
|---|---|---|
| 脏读 | 读取到未提交的数据 | A修改后未提交,B读到修改值,A回滚,B读到脏数据 |
| 不可重复读 | 同事务内两次读取结果不同 | A读取数据,B修改并提交,A再次读取结果不同 |
| 幻读 | 同事务内两次查询记录数不同 | A查询age>20,B插入age=25的记录,A再次查询多出记录 |
五种隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| DEFAULT | - | - | - | 使用数据库默认隔离级别 |
| READ_UNCOMMITTED | ✓ | ✓ | ✓ | 读未提交,最低隔离 |
| READ_COMMITTED | ✗ | ✓ | ✓ | 读已提交 |
| REPEATABLE_READ | ✗ | ✗ | ✓ | 可重复读 |
| SERIALIZABLE | ✗ | ✗ | ✗ | 串行化,最高隔离 |
DEFAULT - 使用数据库默认
Java
@Transactional(isolation = Isolation.DEFAULT)
public void saveOrder(Order order) {
// MySQL默认: REPEATABLE_READ
// PostgreSQL默认: READ_COMMITTED
// Oracle默认: READ_COMMITTED
}
READ_UNCOMMITTED - 读未提交
Java
// 允许读取未提交数据,存在脏读风险
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public List<Account> findAll() {
return accountDao.findAll();
}
实际开发几乎不使用此隔离级别。
READ_COMMITTED - 读已提交
Java
// 只读取已提交数据,防止脏读
@Transactional(isolation = Isolation.READ_COMMITTED)
public Account getAccount(Long id) {
Account account = accountDao.findById(id);
// 此时其他事务已提交的修改可见
return account;
}
PostgreSQL、Oracle、SQL Server默认级别。
REPEATABLE_READ - 可重复读
Java
// 保证同事务内读取一致,防止脏读和不可重复读
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void processAccount(Long id) {
Account a1 = accountDao.findById(id); // 余额100
// 其他事务修改并提交,余额变为80
Account a2 = accountDao.findById(id); // 仍然读取100
// MySQL通过MVCC实现
}
MySQL默认隔离级别,通过MVCC防止不可重复读,通过间隙锁防止幻读。
SERIALIZABLE - 串行化
Java
// 最高隔离级别,完全串行执行
@Transactional(isolation = Isolation.SERIALIZABLE)
public void transfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountDao.findById(fromId);
Account to = accountDao.findById(toId);
// 执行期间完全阻塞其他事务
accountDao.transfer(from, to, amount);
}
性能最差,仅在极端一致性要求场景使用。
Spring配置方式
注解配置
Java
@Service
public class OrderService {
@Transactional(isolation = Isolation.READ_COMMITTED)
public void createOrder(Order order) { }
@Transactional(isolation = Isolation.REPEATABLE_READ)
public Order getOrder(Long id) { return null; }
}
XML配置
XML
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="get*" isolation="READ_COMMITTED" read-only="true"/>
<tx:method name="save*" isolation="REPEATABLE_READ"/>
<tx:method name="*" isolation="DEFAULT"/>
</tx:attributes>
</tx:advice>
全局配置
YAML
spring:
datasource:
hikari:
transaction-isolation: TRANSACTION_READ_COMMITTED
脏读问题演示
Java
// 隔离级别: READ_UNCOMMITTED
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void demo() {
// 线程A: 修改数据未提交
account.setBalance(200);
accountDao.update(account);
// 线程B: 可能读到未提交的200
Account a = accountDao.findById(1L); // 余额200(脏读风险)
// 线程A: 回滚
// 线程B读取的200变成无效数据
}
不可重复读问题演示
Java
// 隔离级别: READ_COMMITTED
@Transactional(isolation = Isolation.READ_COMMITTED)
public void demo() {
// 第一次读取
Account a1 = accountDao.findById(1L); // 余额100
// 其他事务修改并提交,余额变为80
// 第二次读取(结果不同)
Account a2 = accountDao.findById(1L); // 余额80(不可重复读)
}
幻读问题演示
Java
// 隔离级别: REPEATABLE_READ
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void demo() {
// 第一次查询
List<Account> list1 = accountDao.findByAgeGreaterThan(20); // 5条记录
// 其他事务插入age=25的记录并提交
// 第二次查询(MySQL防止幻读)
List<Account> list2 = accountDao.findByAgeGreaterThan(20); // 仍然5条
// 但如果是INSERT操作可能受影响
accountDao.insert(new Account("test", 30)); // 可能主键冲突
}
隔离级别选择
| 业务场景 | 推荐隔离级别 | 原因 |
|---|---|---|
| 报表统计 | READ_COMMITTED | 平衡性能和一致性 |
| 金融交易 | REPEATABLE_READ | 防止余额读取不一致 |
| 高并发读取 | READ_COMMITTED | 减少锁等待 |
| 极端一致性 | SERIALIZABLE | 完全隔离 |
要点总结
- DEFAULT使用数据库默认设置
- READ_UNCOMMITTED存在脏读风险,几乎不用
- READ_COMMITTED防止脏读,主流数据库默认
- REPEATABLE_READ防止脏读和不可重复读,MySQL默认
- SERIALIZABLE完全隔离,性能最差
- 隔离级别越高,并发性能越低
📝 发现内容有误?点击此处直接编辑