数据库操作三种主要问题
-
脏读:一个事务读到了另一个未提交事务修改的数据。
-
不可重复读:一个事务内,多次读取同一数据,但由于另一个已提交事务的修改或删除,导致前后读取的结果不一致。
-
幻读:一个事务内,多次按相同条件查询,但由于另一个已提交事务的新增操作,导致前后查询到的结果集数量不一致。
事务隔离级别是为了解决数据库并发操作时可能出现的三类主要问题
事务隔离级别
四种隔离级别区别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 使用场景 |
|---|---|---|---|---|
| READ_UNCOMMITTED | ✅ 可能 | ✅ 可能 | ✅ 可能 | 实时性要求极高,可接受脏数据 |
| READ_COMMITTED | ❌ 不会 | ✅ 可能 | ✅ 可能 | 默认推荐,平衡性能与一致性 |
| REPEATABLE_READ | ❌ 不会 | ❌ 不会 | ✅ 可能 | 需要数据一致性保证 |
| SERIALIZABLE | ❌ 不会 | ❌ 不会 | ❌ 不会 | 最高一致性要求,性能最低 |
读未提交
介绍: 最低隔离级别,该级别允许事务读取其他事务未提交的数据变更,可能导致脏读(Dirty Read)。脏读是指一个事务读取了另一个事务尚未提交的数据,如果后者回滚,前者读取的数据就是无效的。
使用:
/**
* 示例1: READ UNCOMMITTED - 演示脏读
* 事务A修改数据但未提交,事务B可以读到未提交的数据
*/
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void readUncommittedTransactionA() {
System.out.println("\n=== READ UNCOMMITTED 示例 ===");
System.out.println("事务A开始 - 准备修改张三的余额为1500但暂不提交");
// 查询原始数据
Account account = accountRepository.findByAccountName("张三")
.orElseThrow(() -> new RuntimeException("账户不存在"));
System.out.println("事务A查询原始余额: " + account.getBalance());
// 修改数据但不立即提交
account.setBalance(new BigDecimal("1500.00"));
accountRepository.save(account);
System.out.println("事务A修改余额为1500,但尚未提交");
// 等待事务B读取数据(此时数据未提交)
try {
Thread.sleep(3000); // 等待3秒让事务B读取
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 模拟事务A回滚
System.out.println("事务A发生异常,准备回滚");
throw new RuntimeException("事务A模拟异常,进行回滚");
}
/**
* 事务B:读取未提交的数据
*/
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void readUncommittedTransactionB() {
try {
Thread.sleep(1000); // 等待事务A先开始
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("事务B开始 - 读取张三的余额");
// 此时事务A尚未提交,但事务B可以读到未提交的数据
Account account = accountRepository.findByAccountName("张三")
.orElseThrow(() -> new RuntimeException("账户不存在"));
System.out.println("事务B读取到余额: " + account.getBalance() + " ← 这是脏读!");
System.out.println("事务B提交");
}
特点:性能好, 无锁竞争;会脏读、不可重复读、幻读;
读已提交
介绍: 该级别保证事务只能读取其他事务已提交的数据,避免脏读(Dirty Read),但允许不可重复读(Non-repeatable Read)和幻读(Phantom Read)。
使用:
**
* 示例2: READ COMMITTED - 演示不可重复读
* 事务A读取数据后,事务B修改并提交,事务A再次读取得到不同结果
*/
@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommittedTransactionA(CountDownLatch latch) {
System.out.println("\n=== READ COMMITTED 示例 ===");
System.out.println("事务A开始 - 将多次读取李四的余额");
// 第一次读取
Account account1 = accountRepository.findByAccountName("李四")
.orElseThrow(() -> new RuntimeException("账户不存在"));
System.out.println("事务A第一次读取余额: " + account1.getBalance());
// 通知事务B可以开始修改
latch.countDown();
// 等待事务B修改并提交
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 第二次读取(此时事务B已提交修改)
Account account2 = accountRepository.findByAccountName("李四")
.orElseThrow(() -> new RuntimeException("账户不存在"));
System.out.println("事务A第二次读取余额: " + account2.getBalance() + " ← 不可重复读!");
System.out.println("事务A提交");
}
@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommittedTransactionB(CountDownLatch latch) {
try {
latch.await(); // 等待事务A的第一次读取完成
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("事务B开始 - 修改李四的余额");
Account account = accountRepository.findByAccountName("李四")
.orElseThrow(() -> new RuntimeException("账户不存在"));
// 修改余额
account.setBalance(account.getBalance().add(new BigDecimal("500.00")));
accountRepository.save(account);
System.out.println("事务B将余额从2000修改为2500并提交");
System.out.println("事务B提交");
}
特点:不会脏读,性能一般;会不可重复读、幻读;
可重复读
介绍:该级别保证在同一事务中多次读取同一数据时,结果始终保持一致,即使其他事务在此期间修改了该数据。
使用:
public void repeatableReadTransactionA(CountDownLatch latch) {
System.out.println("\n=== REPEATABLE READ 示例 ===");
System.out.println("事务A开始 - 将多次查询余额>1000的账户");
// 第一次查询
List<Account> accounts1 = accountRepository.findByBalanceGreaterThan(new BigDecimal("1000.00"));
System.out.println("事务A第一次查询(余额>1000): " +
accounts1.stream().map(Account::getAccountName).toList());
// 通知事务B可以开始操作
latch.countDown();
// 等待事务B操作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 第二次查询 - 在REPEATABLE READ级别下,结果应该一致
List<Account> accounts2 = accountRepository.findByBalanceGreaterThan(new BigDecimal("1000.00"));
System.out.println("事务A第二次查询(余额>1000): " +
accounts2.stream().map(Account::getAccountName).toList() +
" ← 可重复读,结果一致");
System.out.println("事务A提交");
}
@Transactional(isolation = Isolation.READ_COMMITTED)
public void repeatableReadTransactionB(CountDownLatch latch) {
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("事务B开始 - 修改王五的余额为1500");
Account account = accountRepository.findByAccountName("王五")
.orElseThrow(() -> new RuntimeException("账户不存在"));
// 修改余额,使其满足事务A的查询条件
account.setBalance(new BigDecimal("1500.00"));
accountRepository.save(account);
System.out.println("事务B将王五余额从500修改为1500并提交");
System.out.println("事务B提交");
}
序列化
介绍:序列化是事务隔离级别中最严格的一种。它通过完全锁定事务涉及的数据范围,确保所有事务串行执行,从而避免脏读、不可重复读和幻读问题。
使用:
@Transactional(isolation = Isolation.SERIALIZABLE, timeout = 10)
public void serializableTransactionA(CountDownLatch startLatch, CountDownLatch endLatch) {
System.out.println("\n=== SERIALIZABLE 示例 ===");
try {
startLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("事务A开始 - 查询所有账户并计算总余额");
// 查询所有账户
List<Account> accounts = accountRepository.findAll();
BigDecimal totalBalance = accounts.stream()
.map(Account::getBalance)
.reduce(BigDecimal.ZERO, BigDecimal::add);
System.out.println("事务A查询到总余额: " + totalBalance);
// 模拟长时间操作
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 再次查询验证一致性
List<Account> accounts2 = accountRepository.findAll();
BigDecimal totalBalance2 = accounts2.stream()
.map(Account::getBalance)
.reduce(BigDecimal.ZERO, BigDecimal::add);
System.out.println("事务A再次查询总余额: " + totalBalance2 + " ← 完全一致");
endLatch.countDown();
System.out.println("事务A提交");
}
@Transactional(isolation = Isolation.SERIALIZABLE, timeout = 10)
public void serializableTransactionB(CountDownLatch startLatch, CountDownLatch endLatch) {
System.out.println("事务B开始 - 等待执行插入操作");
// 通知事务A开始
startLatch.countDown();
try {
// 等待事务A先开始查询
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("事务B尝试插入新账户...");
try {
// 在SERIALIZABLE级别下,这个插入会被阻塞,直到事务A提交
Account newAccount = new Account("赵六", new BigDecimal("3000.00"));
accountRepository.save(newAccount);
System.out.println("事务B插入新账户成功");
} catch (Exception e) {
System.out.println("事务B操作被阻塞或超时: " + e.getMessage());
}
try {
endLatch.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("事务B提交");
}
spring 中的事务
一般依靠@Transactional注解进行使用;
常用的使用方式
@Transactional(rollbackFor = {Exception.class})
public Test {
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})
public void save(Map<String, Object> paramMap) {
//this.baseMapper.insert()
}
}
默认的隔离级别
Propagation propagation() default Propagation.REQUIRED;
/**
* Use the default isolation level of the underlying datastore.
* All other levels correspond to the JDBC isolation levels.
* @see java.sql.Connection
*/
DEFAULT(TransactionDefinition.ISOLATION_DEFAULT);
/**
* Use the default isolation level of the underlying datastore.
* All other levels correspond to the JDBC isolation levels.
* @see java.sql.Connection
*/
int ISOLATION_DEFAULT = -1;
如果不指定事务隔离级别,那么spring的事务隔离级别将使用数据库的默认隔离级别;
- MySQL默认为REPEATABLE_READ
- Oracle默认为READ_COMMITTED
- SQL Server默认为READ_COMMITTED
七种传播行为
| 传播行为 | 中文名称 | 当前存在事务 | 行为特点 | 子事务失败影响 | 使用场景 |
|---|---|---|---|---|---|
| REQUIRED | 必需事务 | ✅ | 加入当前事务 | ✅ 导致父事务回滚 | 默认选择,大多数业务场景 |
| REQUIRES_NEW | 新建事务 | ✅ | 挂起当前,创建新事务 | ❌ 不影响父事务 | 日志、审计、消息发送 |
| NESTED | 嵌套事务 | ✅ | 创建保存点,嵌套事务 | ✅ 导致父事务回滚 | 复杂业务的部分回滚 |
| SUPPORTS | 支持事务 | ✅ | 加入当前事务 | ✅ 导致父事务回滚 | 可选事务支持的方法 |
| NOT_SUPPORTED | 非事务支持 | ✅ | 挂起当前,非事务运行 | ❌ 不影响父事务 | 不需要事务的只读操作 |
| NEVER | 禁止事务 | ✅ | 抛出异常 | - | 强制要求无事务环境 |
| MANDATORY | 强制事务 | ❌ | 抛出异常 | - | 强制要求有事务环境 |
1012

被折叠的 条评论
为什么被折叠?



