一、问题现场还原
那是一个月黑风高的夜晚,小王正准备下班,突然运营群里炸了:
【运营】重大bug!用户下单成功了,但没扣库存!
【运营】已有多名用户反馈...
【运维】涉及金额已达¥12,580...
小王赶紧打开代码:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryService inventoryService;
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO order) {
// 1. 创建订单
Order orderEntity = new Order();
orderEntity.setUserId(order.getUserId());
orderEntity.setAmount(order.getAmount());
orderMapper.insert(orderEntity); // 插入成功
// 2. 扣减库存
inventoryService.decreaseStock(order.getSkuId(), order.getQuantity());
// ❌ 如果这里抛异常,订单已经插入了,但库存没扣!
// 3. 发送消息
messageService.sendOrderCreatedMessage(orderEntity.getId());
}
}
@Service
public class InventoryService {
public void decreaseStock(String skuId, Integer quantity) {
// 扣减库存逻辑
// ...
if (库存不足) {
throw new RuntimeException("库存不足");
}
}
}
问题分析:虽然createOrder加了@Transactional,但inventoryService.decreaseStock()是this调用(自调用),不走Spring的代理,所以事务根本没有生效!
二、原因剖析:Spring事务的代理机制
2.1 Spring事务基于AOP代理
┌─────────────────────────────────────────────────────────┐
│ Spring IOC容器 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ OrderService │ │ InventoryService│ │
│ │ ┌────────────┐ │ │ │ │
│ │ │ createOrder │ │ │ │ │
│ │ │ @Transactional│ │ │ │
│ │ └─────┬──────┘ │ │ │ │
│ │ │ │ │ │ │
│ │ ▼ │ │ │ │
│ │ ┌────────────┐ │ │ │ │
│ │ │ 事务代理 │ │ │ │ │
│ │ │ (AOP) │ │ │ │ │
│ │ └────────────┘ │ │ │ │
│ └────────┬─────────┘ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ decreaseStock() │ ← this调用,不经过代理! │
│ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
2.2 自调用失效的原因
当我们在同一个类中调用另一个方法时:
public void methodA() {
this.methodB(); // this.methodB() 不会走代理!
}
Spring的事务是通过AOP代理实现的,只有外部调用才会经过代理,内部调用(自调用) 会直接跳过代理。
2.3 @Transactional失效的场景汇总
| 场景 | 示例 | 是否生效 |
|---|---|---|
| 自调用 | this.method() | ❌ 不生效 |
| private方法 | @Transactional private method() | ❌ 不生效 |
| 异常被catch | try { } catch { } | ❌ 不生效 |
| 非RuntimeException | throw new Exception() | ❌ 不生效(默认只回滚RuntimeException) |
| 多数据源未指定 | 两个DataSource | ⚠️ 需要指定transactionManager |
三、解决方案:让事务”生效”
方案一:注入自身(推荐)
@Service
public class OrderService {
@Autowired
private OrderService self; // 注入自身
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO order) {
// 1. 创建订单
orderMapper.insert(orderEntity);
// 2. 扣减库存 - 通过代理调用
self.decreaseStockInTransaction(order.getSkuId(), order.getQuantity());
// 3. 发送消息
messageService.sendOrderCreatedMessage(orderEntity.getId());
}
@Transactional(rollbackFor = Exception.class)
public void decreaseStockInTransaction(String skuId, Integer quantity) {
// 扣减库存逻辑
inventoryMapper.decreaseStock(skuId, quantity);
}
}
方案二:使用TransactionTemplate
@Service
public class OrderService {
@Autowired
private TransactionTemplate transactionTemplate;
public void createOrder(OrderDTO order) {
transactionTemplate.executeWithoutResult(status -> {
// 1. 创建订单
orderMapper.insert(orderEntity);
// 2. 扣减库存
inventoryService.decreaseStock(skuId, quantity);
// 3. 发送消息
messageService.sendOrderCreatedMessage(orderEntity.getId());
});
}
}
方案三:使用AopContext.currentProxy()
@SpringBootApplication(exposeProxy = true) // 需要开启暴露代理
@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Service
public class OrderService {
public void createOrder(OrderDTO order) {
((OrderService) AopContext.currentProxy())
.decreaseStockInTransaction(skuId, quantity);
}
@Transactional(rollbackFor = Exception.class)
public void decreaseStockInTransaction(String skuId, Integer quantity) {
// ...
}
}
方案四:确保异常能被正确感知
@Transactional(rollbackFor = Exception.class) // 明确指定回滚条件
public void createOrder(OrderDTO order) {
try {
inventoryService.decreaseStock(skuId, quantity);
} catch (Exception e) {
log.error("扣减库存失败", e);
// 不要吞掉异常,否则事务不会回滚
throw e; // 重新抛出,或者不catch
}
}
四、事务传播行为详解
4.1 七种传播行为
public enum Propagation {
REQUIRED, // 如果当前有事务,加入该事务(默认)
REQUIRES_NEW, // 开启新事务,挂起当前事务
SUPPORTS, // 如果有事务,加入事务;没有则以非事务执行
NOT_SUPPORTED, // 以非事务执行,挂起当前事务
MANDATORY, // 必须在事务中执行,否则抛异常
NEVER, // 必须在非事务中执行,否则抛异常
NESTED // 嵌套事务( Savepoint)
}
4.2 常见场景选择
@Service
public class UserService {
@Autowired
private AccountService accountService;
@Transactional
public void registerUser(User user) {
// 1. 创建用户 - 使用当前事务
userMapper.insert(user);
// 2. 初始化账户 - 单独事务,失败不影响用户创建
accountService.initAccountWithNewTransaction(user.getId());
// 3. 发送欢迎邮件 - 非事务,失败不影响主流程
emailService.sendWelcomeEmail(user.getEmail()); // NOT_SUPPORTED
}
}
@Service
public class AccountService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void initAccountWithNewTransaction(Long userId) {
// 这个方法会开启新事务
// 即使这里失败,UserService的事务也不会回滚
accountMapper.initAccount(userId);
}
}
@Service
public class EmailService {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendWelcomeEmail(String email) {
// 以非事务执行,失败不影响主流程
}
}
五、排查工具:事务Debug
5.1 开启事务日志
# application.yml
logging:
level:
org.springframework.orm.jpa: DEBUG
org.springframework.transaction: DEBUG
5.2 使用@Transactional注解剖析
@Configuration
public class TransactionAspectConfig {
@Bean
public BeanFactoryTransactionAnnotationParser transactionAnnotationParser() {
return new BeanFactoryTransactionAnnotationParser();
}
public boolean isTransactional(Method method) {
Transactional tx = method.getAnnotation(Transactional.class);
return tx != null;
}
}
5.3 事务超时配置
@Transactional(timeout = 30) // 30秒超时
public void createOrder(OrderDTO order) {
// 如果超过30秒,自动回滚
}
六、预防措施:最佳实践清单
6.1 事务使用检查表
✅ @Transactional使用检查
├── 1. 是否在public方法上?(private方法不生效)
├── 2. 是否是外部调用?(自调用需要通过代理)
├── 3. 异常是否被catch吞掉?
├── 4. 是否指定了rollbackFor?(默认只回滚RuntimeException)
├── 5. 是否有多个数据源?是否指定了transactionManager?
├── 6. 是否需要配置事务超时?
└── 7. 传播行为是否正确?
6.2 编码规范建议
// 建议1:不要在事务方法中进行远程调用
@Transactional
public void createOrder(OrderDTO order) {
// ❌ 不好:远程调用在事务中,事务时间过长
remoteService.call();
// ✅ 好:先完成本地事务,再异步调用远程
}
// 建议2:大事务拆分
@Transactional
public void createOrder(OrderDTO order) {
// 保持事务简短
orderMapper.insert(order);
}
// 异步执行其他操作
@Async
public void afterOrderCreated(OrderDTO order) {
messageService.sendMessage(order);
inventoryService.decreaseStock(order.getSkuId(), order.getQuantity());
}
七、总结
今天我们学到了:
| 要点 | 说明 |
|---|---|
| 问题本质 | Spring事务基于代理,自调用不走代理 |
| 失效场景 | private方法、自调用、异常被catch、非RuntimeException |
| 解决方案 | 注入自身、TransactionTemplate、AopContext.currentProxy() |
| 传播行为 | REQUIRED(默认)、REQUIRES_NEW(开启新事务) |
| 最佳实践 | 保持事务简短、避免远程调用在事务中 |
彩蛋:小王最后用了”注入自身”的方案修复了bug。他在周会上分享经验时说道: “Spring的事务就像高考监考——只有从外部(监考老师)看过去才是有效的。你自己看着自己考试,那不就作弊了吗?”