01.03 Spring核心|事务管理实战
导读
- 目标:深入理解Spring事务管理的核心机制,包括事务传播行为、隔离级别、事务失效场景等,掌握事务管理的实战应用。
- 适用场景:Spring框架学习、源码阅读、面试准备、架构设计。
一、事务核心概念
1.1 事务特性(ACID)
事务特性(ACID):
- 原子性(Atomicity):事务要么全部成功,要么全部失败
- 一致性(Consistency):事务前后数据保持一致
- 隔离性(Isolation):事务之间相互隔离
- 持久性(Durability):事务提交后数据持久化
1.2 Spring事务管理
Spring事务管理:
java
// 1. 编程式事务
@Autowired
private TransactionTemplate transactionTemplate;
public void transfer() {
transactionTemplate.execute(status -> {
// 业务逻辑
accountService.debit(fromAccount, amount);
accountService.credit(toAccount, amount);
return null;
});
}
// 2. 声明式事务(推荐)
@Transactional
public void transfer() {
accountService.debit(fromAccount, amount);
accountService.credit(toAccount, amount);
}
二、@Transactional注解
2.1 @Transactional属性
@Transactional属性:
java
@Transactional(
propagation = Propagation.REQUIRED, // 传播行为
isolation = Isolation.DEFAULT, // 隔离级别
timeout = 30, // 超时时间(秒)
readOnly = false, // 只读
rollbackFor = Exception.class, // 回滚异常
noRollbackFor = RuntimeException.class // 不回滚异常
)
public void transfer() {
// 业务逻辑
}
2.2 传播行为详解
什么是传播行为?
传播行为(Propagation)定义了当一个事务方法被另一个事务方法调用时,事务应该如何传播。Spring定义了7种传播行为。
传播行为对比表:
| 传播行为 | 说明 | 当前有事务 | 当前无事务 | 使用场景 |
|---|---|---|---|---|
| REQUIRED(默认) | 如果存在事务则加入,否则创建新事务 | 加入当前事务 | 创建新事务 | 大多数业务方法(90%场景) |
| REQUIRES_NEW | 总是创建新事务,挂起当前事务 | 挂起当前事务,创建新事务 | 创建新事务 | 日志记录、审计操作 |
| SUPPORTS | 如果存在事务则加入,否则非事务执行 | 加入当前事务 | 非事务执行 | 查询方法,可事务可非事务 |
| NOT_SUPPORTED | 以非事务方式执行,挂起当前事务 | 挂起当前事务,非事务执行 | 非事务执行 | 调用不支持事务的第三方服务 |
| MANDATORY | 必须在事务中执行,否则抛出异常 | 加入当前事务 | 抛出异常 | 必须保证在事务中执行的方法 |
| NEVER | 不能在事务中执行,否则抛出异常 | 抛出异常 | 非事务执行 | 禁止在事务中执行的方法 |
| NESTED | 嵌套事务,支持部分回滚 | 创建嵌套事务 | 创建新事务 | 复杂业务,需要部分回滚 |
2.2.1 REQUIRED(默认,最常用)
行为:如果当前存在事务,则加入该事务;如果当前不存在事务,则创建一个新的事务。
使用场景 :90%的业务方法都应该使用REQUIRED,这是最常用的传播行为。
java
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private PaymentService paymentService;
// 主事务方法
@Transactional(propagation = Propagation.REQUIRED) // 可以省略,默认就是REQUIRED
public void createOrder(Order order) {
// 1. 保存订单
orderRepository.save(order);
// 2. 调用支付服务(会加入当前事务)
paymentService.processPayment(order.getId(), order.getAmount());
// 如果paymentService.processPayment()抛出异常,整个事务回滚
}
}
@Service
public class PaymentService {
@Transactional(propagation = Propagation.REQUIRED) // 加入OrderService的事务
public void processPayment(Long orderId, BigDecimal amount) {
// 支付逻辑
// 如果这里抛出异常,OrderService的事务也会回滚
}
}
执行流程:
OrderService.createOrder() [创建事务T1]
└─> PaymentService.processPayment() [加入事务T1]
└─> 如果异常,T1回滚,订单和支付都回滚
2.2.2 REQUIRES_NEW(独立事务)
行为:总是创建一个新的事务,如果当前存在事务,则把当前事务挂起。
使用场景:
- 日志记录:日志必须记录,即使主业务失败
- 审计操作:审计信息必须保存
- 独立业务:需要独立提交的业务逻辑
java
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private AuditService auditService;
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(Order order) {
try {
// 1. 保存订单(事务T1)
orderRepository.save(order);
// 2. 记录审计日志(新事务T2,独立提交)
auditService.recordAudit("订单创建", order.getId());
// 3. 如果这里抛出异常,T1回滚,但T2已提交
if (order.getAmount().compareTo(new BigDecimal("10000")) > 0) {
throw new RuntimeException("订单金额过大");
}
} catch (Exception e) {
// T1回滚,订单未保存
// 但T2已提交,审计日志已记录
throw e;
}
}
}
@Service
public class AuditService {
// 使用REQUIRES_NEW,确保审计日志独立提交
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordAudit(String action, Long orderId) {
AuditLog log = new AuditLog(action, orderId, new Date());
auditRepository.save(log);
// 这个事务会立即提交,不受主事务影响
}
}
执行流程:
OrderService.createOrder() [创建事务T1]
└─> AuditService.recordAudit() [挂起T1,创建新事务T2]
└─> T2提交(审计日志已保存)
└─> 如果后续异常,T1回滚(订单未保存)
└─> 结果:审计日志已保存,订单未保存
⚠️ 注意事项:
- REQUIRES_NEW会创建新事务,性能开销较大
- 新事务提交后,主事务回滚不会影响新事务
- 谨慎使用,只在确实需要独立提交的场景使用
2.2.3 SUPPORTS(支持事务)
行为:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务方式执行。
使用场景:
- 查询方法:可以事务执行,也可以非事务执行
- 可选的业务逻辑:不强制要求事务
java
@Service
public class UserService {
@Transactional(propagation = Propagation.SUPPORTS)
public User findUser(Long id) {
// 如果调用方有事务,则加入事务(保证一致性)
// 如果调用方无事务,则非事务执行(提高性能)
return userRepository.findById(id);
}
@Transactional(propagation = Propagation.REQUIRED)
public void updateUser(User user) {
// 先查询(如果有事务则加入,无事务则非事务)
User existing = findUser(user.getId());
// 更新(在事务中)
userRepository.save(user);
}
}
执行流程:
场景1:有事务
UserService.updateUser() [创建事务T1]
└─> UserService.findUser() [加入事务T1]
场景2:无事务
UserService.findUser() [非事务执行]
2.2.4 NOT_SUPPORTED(不支持事务)
行为:以非事务方式执行操作,如果当前存在事务,则把当前事务挂起。
使用场景:
- 调用不支持事务的第三方服务(如某些NoSQL数据库)
- 性能优化:某些只读操作不需要事务
java
@Service
public class OrderService {
@Autowired
private RedisService redisService; // Redis不支持事务
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(Order order) {
// 1. 保存订单(事务T1)
orderRepository.save(order);
// 2. 更新缓存(挂起T1,非事务执行)
redisService.updateCache(order.getId(), order);
// 3. 继续事务T1
// 如果这里异常,T1回滚,但缓存已更新(数据可能不一致)
}
}
@Service
public class RedisService {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void updateCache(Long orderId, Order order) {
// Redis操作,不支持事务
redisTemplate.opsForValue().set("order:" + orderId, order);
}
}
⚠️ 注意事项:
- 可能导致数据不一致(主事务回滚,但非事务操作已执行)
- 谨慎使用,确保理解业务影响
2.2.5 MANDATORY(强制事务)
行为:必须在事务中执行,如果当前不存在事务,则抛出异常。
使用场景:
- 必须保证数据一致性的方法
- 强制要求调用方开启事务
java
@Service
public class AccountService {
// 必须在事务中执行
@Transactional(propagation = Propagation.MANDATORY)
public void transfer(Account from, Account to, BigDecimal amount) {
// 转账操作必须在事务中,否则数据不一致
from.debit(amount);
to.credit(amount);
}
}
@Service
public class TransferService {
@Transactional(propagation = Propagation.REQUIRED) // 必须开启事务
public void processTransfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId);
Account to = accountRepository.findById(toId);
// 调用transfer,必须在事务中
accountService.transfer(from, to, amount);
}
// 错误示例:无事务调用
public void processTransferWithoutTransaction(Long fromId, Long toId, BigDecimal amount) {
// 这里调用transfer会抛出异常:No existing transaction found
accountService.transfer(from, to, amount);
}
}
2.2.6 NEVER(禁止事务)
行为:不能在事务中执行,如果当前存在事务,则抛出异常。
使用场景:
- 禁止在事务中执行的方法(如某些只读操作)
- 性能优化:确保方法非事务执行
java
@Service
public class StatisticsService {
// 统计方法,禁止在事务中执行(提高性能)
@Transactional(propagation = Propagation.NEVER)
public Statistics getStatistics() {
// 复杂的统计查询,不需要事务
return statisticsRepository.calculate();
}
}
@Service
public class ReportService {
public void generateReport() {
// 正确:非事务调用
Statistics stats = statisticsService.getStatistics();
}
@Transactional
public void generateReportInTransaction() {
// 错误:在事务中调用,会抛出异常
Statistics stats = statisticsService.getStatistics(); // 抛出异常
}
}
2.2.7 NESTED(嵌套事务)
行为:如果当前存在事务,则创建一个嵌套事务;如果当前不存在事务,则创建一个新事务。嵌套事务可以部分回滚。
使用场景:
- 复杂业务逻辑,需要部分回滚
- **保存点(Savepoint)**机制
java
@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(Order order) {
// 1. 保存订单(主事务)
orderRepository.save(order);
try {
// 2. 处理优惠券(嵌套事务)
couponService.applyCoupon(order.getId(), order.getCouponId());
} catch (CouponException e) {
// 嵌套事务回滚,但主事务继续
log.error("优惠券处理失败,继续创建订单", e);
}
// 3. 处理支付(主事务继续)
paymentService.processPayment(order.getId(), order.getAmount());
// 如果这里异常,整个事务回滚(包括订单和支付)
}
}
@Service
public class CouponService {
// 嵌套事务:如果失败,只回滚优惠券相关操作
@Transactional(propagation = Propagation.NESTED)
public void applyCoupon(Long orderId, String couponId) {
// 优惠券逻辑
// 如果这里异常,只回滚这个嵌套事务,不影响主事务
}
}
执行流程:
OrderService.createOrder() [创建主事务T1]
├─> 保存订单 [T1]
├─> CouponService.applyCoupon() [创建嵌套事务T2]
│ └─> 如果异常,T2回滚,T1继续
└─> 处理支付 [T1]
└─> 如果异常,T1回滚(包括订单和支付)
⚠️ 注意事项:
- NESTED需要数据库支持保存点(Savepoint)
- MySQL的InnoDB支持,但需要JDBC驱动支持
- 不是所有数据库都支持嵌套事务
2.2.8 传播行为选择指南
快速决策树:
需要事务吗?
├─ 是 → 必须保证在事务中?
│ ├─ 是 → MANDATORY
│ └─ 否 → 需要独立提交?
│ ├─ 是 → REQUIRES_NEW(日志、审计)
│ └─ 否 → 需要部分回滚?
│ ├─ 是 → NESTED(复杂业务)
│ └─ 否 → REQUIRED(90%场景)
└─ 否 → 禁止在事务中?
├─ 是 → NEVER
└─ 否 → 可以非事务执行?
├─ 是 → SUPPORTS(查询方法)
└─ 否 → NOT_SUPPORTED(第三方服务)
实战建议:
- 默认使用REQUIRED:90%的业务方法
- 日志/审计用REQUIRES_NEW:确保独立提交
- 查询方法用SUPPORTS:灵活,可事务可非事务
- 复杂业务用NESTED:需要部分回滚的场景
- 谨慎使用其他:确保理解业务影响
2.3 隔离级别详解
什么是隔离级别?
隔离级别(Isolation)定义了事务之间的隔离程度,用于解决并发事务导致的数据一致性问题。
并发事务问题:
| 问题 | 说明 | 示例 |
|---|---|---|
| 脏读(Dirty Read) | 读取到未提交的数据 | 事务A修改数据未提交,事务B读取到修改后的数据,A回滚,B读取到脏数据 |
| 不可重复读(Non-Repeatable Read) | 同一事务中多次读取结果不一致 | 事务A读取数据,事务B修改并提交,A再次读取结果不同 |
| 幻读(Phantom Read) | 同一事务中多次查询结果集不一致 | 事务A查询10条记录,事务B插入1条并提交,A再次查询得到11条 |
隔离级别对比表:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 | 使用场景 |
|---|---|---|---|---|---|
| READ_UNCOMMITTED | ❌ 可能 | ❌ 可能 | ❌ 可能 | 最高 | 几乎不使用 |
| READ_COMMITTED | ✅ 避免 | ❌ 可能 | ❌ 可能 | 较高 | Oracle默认,大多数场景 |
| REPEATABLE_READ | ✅ 避免 | ✅ 避免 | ❌ 可能 | 中等 | MySQL默认,需要可重复读 |
| SERIALIZABLE | ✅ 避免 | ✅ 避免 | ✅ 避免 | 最低 | 严格要求一致性 |
2.3.1 READ_UNCOMMITTED(读未提交)
行为:允许读取未提交的数据,最低隔离级别。
问题:可能出现脏读、不可重复读、幻读。
使用场景 :几乎不使用,除非对数据一致性要求极低。
java
// 示例:脏读问题
// 事务A
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void transfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId);
from.setBalance(from.getBalance().subtract(amount)); // 未提交
accountRepository.save(from);
// 如果这里异常回滚,但事务B可能已读取到修改后的余额
}
// 事务B(并发执行)
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public BigDecimal getBalance(Long accountId) {
// 可能读取到事务A未提交的数据(脏读)
Account account = accountRepository.findById(accountId);
return account.getBalance(); // 可能是脏数据
}
⚠️ 不推荐使用:数据一致性无法保证。
2.3.2 READ_COMMITTED(读已提交)
行为:只能读取已提交的数据,避免脏读,但可能出现不可重复读和幻读。
使用场景 :大多数业务场景的默认选择,Oracle数据库默认隔离级别。
java
// 示例:不可重复读问题
// 事务A
@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateOrderStatus(Long orderId, String status) {
Order order = orderRepository.findById(orderId);
order.setStatus(status);
orderRepository.save(order);
// 提交后,事务B再次读取会看到新状态
}
// 事务B
@Transactional(isolation = Isolation.READ_COMMITTED)
public void checkOrder(Long orderId) {
Order order1 = orderRepository.findById(orderId);
// 假设此时事务A提交了更新
Order order2 = orderRepository.findById(orderId);
// order1和order2的状态可能不同(不可重复读)
}
实战案例:账户余额查询
java
@Service
public class AccountService {
// 使用READ_COMMITTED,保证读取到已提交的数据
@Transactional(isolation = Isolation.READ_COMMITTED)
public BigDecimal getBalance(Long accountId) {
// 只能读取到已提交的余额,不会读取到未提交的转账
Account account = accountRepository.findById(accountId);
return account.getBalance();
}
@Transactional(isolation = Isolation.READ_COMMITTED)
public void transfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId);
Account to = accountRepository.findById(toId);
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
accountRepository.save(from);
accountRepository.save(to);
// 提交后,其他事务才能看到新的余额
}
}
适用场景:
- ✅ 大多数业务场景(90%)
- ✅ 对性能要求较高的场景
- ✅ 可以接受不可重复读的业务
2.3.3 REPEATABLE_READ(可重复读)
行为:同一事务中多次读取同一数据,结果一致。避免脏读和不可重复读,但可能出现幻读。
使用场景:MySQL默认隔离级别,需要保证同一事务中数据一致性的场景。
java
// 示例:避免不可重复读
// 事务A
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void calculateTotal(Long userId) {
// 第一次查询
List<Order> orders1 = orderRepository.findByUserId(userId);
BigDecimal total1 = orders1.stream()
.map(Order::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 假设此时事务B插入了新订单并提交
// 第二次查询(REPEATABLE_READ保证结果一致)
List<Order> orders2 = orderRepository.findByUserId(userId);
BigDecimal total2 = orders2.stream()
.map(Order::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// total1 == total2(可重复读)
// 但可能遗漏事务B插入的新订单(幻读)
}
实战案例:对账业务
java
@Service
public class ReconciliationService {
// 对账需要保证同一事务中数据一致
@Transactional(isolation = Isolation.REPEATABLE_READ)
public ReconciliationResult reconcile(Long accountId, Date date) {
// 1. 查询账户余额
Account account = accountRepository.findById(accountId);
BigDecimal balance = account.getBalance();
// 2. 计算交易总额(需要多次查询)
List<Transaction> transactions = transactionRepository
.findByAccountIdAndDate(accountId, date);
BigDecimal total = transactions.stream()
.map(Transaction::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 3. 再次查询余额(REPEATABLE_READ保证一致)
Account account2 = accountRepository.findById(accountId);
// account.getBalance() == account2.getBalance()
return new ReconciliationResult(balance, total);
}
}
适用场景:
- ✅ 需要多次读取同一数据的业务
- ✅ 对账、统计等需要数据一致性的场景
- ✅ MySQL默认,大多数场景可用
⚠️ 注意:可能出现幻读,需要根据业务判断是否可接受。
2.3.4 SERIALIZABLE(串行化)
行为:最高隔离级别,事务串行执行,完全避免脏读、不可重复读、幻读。
问题:性能最低,并发性差。
使用场景 :极少使用,只在严格要求数据一致性的场景。
java
// 示例:严格的库存扣减
@Service
public class InventoryService {
// 使用SERIALIZABLE,确保库存扣减的严格一致性
@Transactional(isolation = Isolation.SERIALIZABLE)
public void deductInventory(Long productId, Integer quantity) {
Product product = productRepository.findById(productId);
if (product.getStock() < quantity) {
throw new InsufficientStockException("库存不足");
}
product.setStock(product.getStock() - quantity);
productRepository.save(product);
// SERIALIZABLE确保不会有并发问题
// 但性能较差,并发请求会串行执行
}
}
适用场景:
- ✅ 金融交易(金额计算)
- ✅ 库存扣减(严格一致性)
- ✅ 票务系统(座位分配)
⚠️ 注意事项:
- 性能开销大,会严重影响并发性能
- 可能导致死锁
- 只在确实需要严格一致性的场景使用
2.3.5 隔离级别选择指南
快速决策树:
需要严格一致性?
├─ 是 → SERIALIZABLE(金融、库存)
└─ 否 → 需要可重复读?
├─ 是 → REPEATABLE_READ(对账、统计)
└─ 否 → READ_COMMITTED(大多数场景,推荐)
实战建议:
- 默认使用READ_COMMITTED:大多数业务场景(90%)
- 需要可重复读用REPEATABLE_READ:对账、统计等场景
- 严格要求一致性用SERIALIZABLE:金融、库存等场景
- 避免使用READ_UNCOMMITTED:几乎不使用
数据库默认隔离级别:
| 数据库 | 默认隔离级别 | 说明 |
|---|---|---|
| MySQL | REPEATABLE_READ | InnoDB引擎 |
| Oracle | READ_COMMITTED | |
| PostgreSQL | READ_COMMITTED | |
| SQL Server | READ_COMMITTED |
性能对比(大致):
性能:READ_UNCOMMITTED > READ_COMMITTED > REPEATABLE_READ > SERIALIZABLE
一致性:READ_UNCOMMITTED < READ_COMMITTED < REPEATABLE_READ < SERIALIZABLE
实战案例:电商订单系统
java
@Service
public class OrderService {
// 1. 订单查询:使用READ_COMMITTED(性能优先)
@Transactional(isolation = Isolation.READ_COMMITTED, readOnly = true)
public Order getOrder(Long orderId) {
return orderRepository.findById(orderId);
}
// 2. 订单创建:使用REPEATABLE_READ(需要一致性)
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void createOrder(Order order) {
// 需要多次查询商品信息,保证一致性
Product product = productRepository.findById(order.getProductId());
// 再次查询,保证价格一致
Product product2 = productRepository.findById(order.getProductId());
// product.getPrice() == product2.getPrice()
order.setPrice(product.getPrice());
orderRepository.save(order);
}
// 3. 库存扣减:使用SERIALIZABLE(严格一致性)
@Transactional(isolation = Isolation.SERIALIZABLE)
public void deductStock(Long productId, Integer quantity) {
Product product = productRepository.findById(productId);
if (product.getStock() < quantity) {
throw new InsufficientStockException();
}
product.setStock(product.getStock() - quantity);
productRepository.save(product);
}
}
三、事务实现原理
3.1 事务代理机制
事务代理机制:
java
// Spring事务通过AOP实现
// 1. 创建代理对象
@Transactional
public class UserService {
public void saveUser(User user) {
// 业务逻辑
}
}
// 2. 代理对象执行流程
public class TransactionInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 开启事务
TransactionStatus status = transactionManager.getTransaction(definition);
try {
// 执行目标方法
Object result = invocation.proceed();
// 提交事务
transactionManager.commit(status);
return result;
} catch (Exception e) {
// 回滚事务
transactionManager.rollback(status);
throw e;
}
}
}
三、事务失效场景与解决方案
3.1 事务失效的常见场景
事务失效场景总结:
| 场景 | 原因 | 解决方案 |
|---|---|---|
| 方法非public | Spring AOP只能代理public方法 | 改为public方法 |
| 同类调用 | 不会经过代理 | 提取到另一个Service或使用AopContext |
| 异常被捕获 | 异常未抛出,不会触发回滚 | 让异常抛出或手动回滚 |
| 数据库不支持 | MyISAM引擎不支持事务 | 使用InnoDB引擎 |
| 未启用事务管理 | 未配置@EnableTransactionManagement | 启用事务管理 |
| 方法被final修饰 | 无法被CGLIB代理 | 移除final修饰符 |
| 异常类型不匹配 | 默认只回滚RuntimeException | 指定rollbackFor |
3.2 场景1:方法非public
问题:@Transactional只能用于public方法。
java
@Service
public class UserService {
// ❌ 错误:private方法,事务失效
@Transactional
private void saveUser(User user) {
userRepository.save(user);
}
// ✅ 正确:public方法
@Transactional
public void saveUser(User user) {
userRepository.save(user);
}
}
原因:Spring AOP使用代理机制,只能代理public方法。
3.3 场景2:同类调用(最常见)
问题:同一个类中方法调用,不会经过代理。
java
@Service
public class UserService {
@Transactional
public void methodA() {
// 业务逻辑
methodB(); // ❌ 同类调用,不会经过代理,事务失效
}
@Transactional
public void methodB() {
userRepository.save(new User());
// 如果这里抛出异常,methodB的事务不会回滚
}
}
解决方案1:提取到另一个Service(推荐)
java
@Service
public class UserService {
@Autowired
private UserTransactionService userTransactionService;
public void methodA() {
// 业务逻辑
userTransactionService.methodB(); // ✅ 通过另一个Service调用
}
}
@Service
public class UserTransactionService {
@Transactional
public void methodB() {
userRepository.save(new User());
// 事务生效
}
}
解决方案2:注入自己(不推荐,但可用)
java
@Service
public class UserService {
@Autowired
private UserService self; // 注入自己
public void methodA() {
// 业务逻辑
self.methodB(); // ✅ 通过代理调用
}
@Transactional
public void methodB() {
userRepository.save(new User());
// 事务生效
}
}
解决方案3:使用AopContext.currentProxy()(需要配置)
java
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true) // 暴露代理
public class AppConfig {
}
@Service
public class UserService {
public void methodA() {
// 业务逻辑
((UserService) AopContext.currentProxy()).methodB(); // ✅ 获取当前代理
}
@Transactional
public void methodB() {
userRepository.save(new User());
// 事务生效
}
}
3.4 场景3:异常被捕获
问题:异常被catch,不会触发回滚。
java
@Service
public class UserService {
@Transactional
public void saveUser(User user) {
try {
userRepository.save(user);
// 模拟异常
if (user.getName() == null) {
throw new RuntimeException("用户名不能为空");
}
} catch (Exception e) {
// ❌ 异常被捕获,不会触发回滚
log.error("保存用户失败", e);
// 事务不会回滚,数据已保存
}
}
}
解决方案1:让异常抛出(推荐)
java
@Service
public class UserService {
@Transactional(rollbackFor = Exception.class)
public void saveUser(User user) throws Exception {
userRepository.save(user);
if (user.getName() == null) {
throw new Exception("用户名不能为空"); // ✅ 抛出异常,触发回滚
}
}
}
解决方案2:手动回滚
java
@Service
public class UserService {
@Autowired
private TransactionTemplate transactionTemplate;
@Transactional
public void saveUser(User user) {
try {
userRepository.save(user);
if (user.getName() == null) {
throw new RuntimeException("用户名不能为空");
}
} catch (Exception e) {
// ✅ 手动回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
log.error("保存用户失败", e);
}
}
}
解决方案3:在catch中重新抛出异常
java
@Service
public class UserService {
@Transactional(rollbackFor = Exception.class)
public void saveUser(User user) {
try {
userRepository.save(user);
if (user.getName() == null) {
throw new Exception("用户名不能为空");
}
} catch (Exception e) {
log.error("保存用户失败", e);
throw e; // ✅ 重新抛出异常,触发回滚
}
}
}
3.5 场景4:异常类型不匹配
问题:@Transactional默认只回滚RuntimeException和Error。
java
@Service
public class UserService {
@Transactional // 默认只回滚RuntimeException
public void saveUser(User user) throws Exception {
userRepository.save(user);
throw new Exception("业务异常"); // ❌ Exception不会触发回滚
}
}
解决方案:指定rollbackFor
java
@Service
public class UserService {
@Transactional(rollbackFor = Exception.class) // ✅ 指定回滚所有异常
public void saveUser(User user) throws Exception {
userRepository.save(user);
throw new Exception("业务异常"); // ✅ 会触发回滚
}
}
3.6 场景5:数据库引擎不支持
问题:MySQL的MyISAM引擎不支持事务。
sql
-- ❌ MyISAM引擎不支持事务
CREATE TABLE user (
id BIGINT PRIMARY KEY,
name VARCHAR(100)
) ENGINE=MyISAM;
解决方案:使用InnoDB引擎
sql
-- ✅ InnoDB引擎支持事务
CREATE TABLE user (
id BIGINT PRIMARY KEY,
name VARCHAR(100)
) ENGINE=InnoDB;
3.7 场景6:未启用事务管理
问题:未配置@EnableTransactionManagement。
java
// ❌ 未启用事务管理
@Configuration
public class AppConfig {
// 缺少@EnableTransactionManagement
}
解决方案:启用事务管理
java
// ✅ 启用事务管理
@Configuration
@EnableTransactionManagement // Spring Boot自动配置,通常不需要手动添加
public class AppConfig {
}
3.8 事务生效检查清单
✅ 确保事务生效的检查清单:
- ✅ 方法必须是public
- ✅ 不能是同类调用(提取到另一个Service)
- ✅ 异常必须抛出(不要catch或重新抛出)
- ✅ 指定正确的rollbackFor
- ✅ 数据库引擎支持事务(InnoDB)
- ✅ 已启用事务管理
- ✅ 方法不能被final修饰
- ✅ 确保@Transactional注解被扫描到
四、实战案例:完整的事务管理方案
4.1 电商订单系统
java
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private PaymentService paymentService;
@Autowired
private InventoryService inventoryService;
@Autowired
private AuditService auditService;
/**
* 创建订单(主事务)
* 使用REQUIRED,确保所有操作在同一事务中
*/
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED,
rollbackFor = Exception.class,
timeout = 30
)
public Order createOrder(OrderDTO orderDTO) {
// 1. 扣减库存(同一事务)
inventoryService.deductStock(orderDTO.getProductId(), orderDTO.getQuantity());
// 2. 创建订单
Order order = new Order();
order.setProductId(orderDTO.getProductId());
order.setQuantity(orderDTO.getQuantity());
order.setAmount(orderDTO.getAmount());
order.setStatus("PENDING");
order = orderRepository.save(order);
try {
// 3. 处理支付(同一事务)
paymentService.processPayment(order.getId(), order.getAmount());
order.setStatus("PAID");
} catch (PaymentException e) {
// 支付失败,整个事务回滚(订单和库存都回滚)
order.setStatus("FAILED");
throw e;
}
// 4. 记录审计日志(独立事务,即使主事务回滚也保存)
try {
auditService.recordAudit("订单创建", order.getId());
} catch (Exception e) {
// 审计日志失败不影响主事务
log.error("审计日志记录失败", e);
}
return order;
}
}
@Service
public class InventoryService {
/**
* 扣减库存(严格一致性)
* 使用SERIALIZABLE,确保库存扣减的严格一致性
*/
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.SERIALIZABLE,
rollbackFor = Exception.class
)
public void deductStock(Long productId, Integer quantity) {
Product product = productRepository.findById(productId);
if (product.getStock() < quantity) {
throw new InsufficientStockException("库存不足");
}
product.setStock(product.getStock() - quantity);
productRepository.save(product);
}
}
@Service
public class PaymentService {
/**
* 处理支付(主事务)
*/
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED,
rollbackFor = Exception.class
)
public void processPayment(Long orderId, BigDecimal amount) {
// 支付逻辑
Payment payment = new Payment();
payment.setOrderId(orderId);
payment.setAmount(amount);
paymentRepository.save(payment);
// 调用第三方支付接口
// 如果失败,抛出PaymentException,触发回滚
}
}
@Service
public class AuditService {
/**
* 记录审计日志(独立事务)
* 使用REQUIRES_NEW,确保审计日志独立提交
*/
@Transactional(
propagation = Propagation.REQUIRES_NEW,
isolation = Isolation.READ_COMMITTED
)
public void recordAudit(String action, Long orderId) {
AuditLog log = new AuditLog();
log.setAction(action);
log.setOrderId(orderId);
log.setCreateTime(new Date());
auditRepository.save(log);
// 这个事务会立即提交,不受主事务影响
}
}
高频面试问答(深度解析)
1. Spring事务失效的场景?
标准答案:
- 方法非public:@Transactional只能用于public方法
- 同类调用:同一个类中方法调用,不会经过代理
- 异常被捕获:异常被catch,不会触发回滚
- 数据库不支持:如MySQL的MyISAM引擎不支持事务
- 异常类型不匹配:默认只回滚RuntimeException
- 未启用事务管理:未配置@EnableTransactionManagement
深入追问与回答思路:
Q: 同类调用为什么失效?
java
@Service
public class UserService {
@Transactional
public void methodA() {
methodB(); // 同类调用,不会经过代理,事务失效
}
@Transactional
public void methodB() {
// 业务逻辑
}
}
原因:Spring AOP使用代理机制,同类调用是直接调用this.methodB(),不会经过代理对象。
解决方案:
- 提取到另一个Service(推荐)
- 注入自己(不推荐)
- 使用AopContext.currentProxy()(需要配置)
Q: 如何保证事务生效?
java
// 1. 方法必须是public
@Transactional
public void method() { // public方法
// 业务逻辑
}
// 2. 异常必须抛出
@Transactional(rollbackFor = Exception.class)
public void method() throws Exception {
try {
// 业务逻辑
} catch (Exception e) {
// 不要捕获,让异常抛出
throw e;
}
}
// 3. 使用代理调用
// 通过其他Service调用,或使用AopContext.currentProxy()
延伸阅读
- 《Spring源码深度解析》--- 郝佳
- Spring官方文档:https://spring.io/docs
- Spring源码:https://github.com/spring-projects/spring-framework