1. Spring 中的 @Transactional 注解深度解析
从事Java开发这么多年,我见过太多因为事务使用不当导致的生产事故。记得有一次凌晨两点被叫起来处理问题,就是因为某个转账操作没有正确配置事务,导致金额扣了但对方账户没收到钱。今天我就结合15年踩坑经验,把@Transactional这个看似简单实则暗藏玄机的注解给大家讲透。
1.1 事务的本质与价值
事务(Transaction)本质上是一组不可分割的数据库操作序列。它的核心特性可以用ACID来概括:
- 原子性(Atomicity):事务内的操作要么全部成功,要么全部失败
- 一致性(Consistency):事务执行前后数据库状态保持一致
- 隔离性(Isolation):并发事务之间互不干扰
- 持久性(Durability):事务提交后修改永久生效
在实际业务中,最典型的例子就是银行转账:
java复制public void transfer(Long fromId, Long toId, BigDecimal amount) {
accountMapper.debit(fromId, amount); // 扣款
accountMapper.credit(toId, amount); // 存款
}
如果没有事务保护,当扣款成功但存款失败时,系统就会出现数据不一致。而加上@Transactional后,Spring会帮我们自动处理这种异常情况。
1.2 声明式 vs 编程式事务
Spring提供了两种事务管理方式:
| 方式 | 实现 | 优点 | 缺点 |
|---|---|---|---|
| 编程式事务 | 手动编写TransactionTemplate | 控制粒度细 | 代码侵入性强 |
| 声明式事务 | 使用@Transactional注解 |
代码简洁,非侵入式 | 控制粒度较粗 |
现在99%的场景都会选择声明式事务,只有在需要精细控制事务边界时才会考虑编程式。
2. @Transactional 工作原理剖析
2.1 AOP代理机制
Spring事务的实现核心是AOP(面向切面编程)。当我们给方法加上@Transactional后,Spring会通过动态代理生成一个代理对象。这个代理对象的工作流程如下:
- 方法调用前开启事务(设置autoCommit=false)
- 执行目标方法
- 方法正常结束时提交事务
- 方法抛出异常时回滚事务
可以通过以下代码验证代理机制:
java复制@Service
public class UserService {
@Transactional
public void createUser(User user) {
// 实际会打印出代理类名
System.out.println(this.getClass().getName());
}
}
输出结果会是类似UserService$$EnhancerBySpringCGLIB$$12345678这样的代理类名。
2.2 事务传播行为详解
传播行为(Propagation)定义了多个事务方法相互调用时的事务边界。这是最容易出问题的地方,我们来看具体场景:
java复制@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRED)
public void placeOrder(Order order) {
// 主订单逻辑
paymentService.processPayment(order);
}
}
@Service
public class PaymentService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processPayment(Order order) {
// 支付逻辑
}
}
Spring提供了7种传播行为,最常用的有:
- REQUIRED(默认):如果当前存在事务,则加入该事务;否则新建事务
- REQUIRES_NEW:新建事务,如果当前存在事务则挂起
- NESTED:如果当前存在事务,则在嵌套事务内执行
- SUPPORTS:如果当前存在事务,则加入;否则以非事务方式运行
实际项目中,我曾遇到一个坑:在REQUIRED传播行为下,内层方法抛出异常会导致外层事务也回滚。后来改用REQUIRES_NEW才解决了问题。
2.3 事务隔离级别对比
隔离级别控制事务之间的可见性,Spring支持的标准隔离级别有:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| READ_UNCOMMITTED | ✓ | ✓ | ✓ | 最高 |
| READ_COMMITTED(默认) | × | ✓ | ✓ | 高 |
| REPEATABLE_READ | × | × | ✓ | 中 |
| SERIALIZABLE | × | × | × | 最低 |
MySQL默认是REPEATABLE_READ,而Oracle默认是READ_COMMITTED。设置方法:
java复制@Transactional(isolation = Isolation.REPEATABLE_READ)
public void updateProduct(Product product) {
// ...
}
3. 注解使用最佳实践
3.1 正确配置注解属性
完整的@Transactional注解支持以下关键属性:
java复制@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT,
timeout = 30,
readOnly = false,
rollbackFor = {SQLException.class, IOException.class},
noRollbackFor = {BusinessException.class}
)
public void businessMethod() {
// ...
}
特别提醒几个容易忽略的点:
- timeout:事务超时时间(秒),超过会自动回滚。默认-1表示不超时
- readOnly:设为true可以优化查询性能,但写操作会抛出异常
- rollbackFor:默认只对RuntimeException回滚,建议显式指定Exception.class
3.2 方法可见性与自调用问题
事务生效有两个必要条件:
- 方法必须是public的(因为Spring代理基于接口或CGLIB)
- 必须通过代理对象调用(同类方法直接调用会失效)
典型错误示例:
java复制@Service
public class WrongService {
public void outer() {
this.inner(); // 自调用导致事务失效
}
@Transactional
public void inner() {
// ...
}
}
正确做法是通过ApplicationContext获取代理对象:
java复制@Service
public class CorrectService implements ApplicationContextAware {
private ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext context) {
this.context = context;
}
public void outer() {
context.getBean(CorrectService.class).inner();
}
@Transactional
public void inner() {
// ...
}
}
3.3 事务与锁的配合使用
在高并发场景下,事务需要与锁机制配合使用:
java复制@Transactional
public void deductStock(Long productId, int quantity) {
// 悲观锁示例
Product product = productMapper.selectForUpdate(productId);
if (product.getStock() >= quantity) {
product.setStock(product.getStock() - quantity);
productMapper.update(product);
} else {
throw new BusinessException("库存不足");
}
// 乐观锁示例
int version = product.getVersion();
int rows = productMapper.updateWithVersion(
productId, quantity, version);
if (rows == 0) {
throw new OptimisticLockException("版本冲突");
}
}
4. 常见问题排查手册
4.1 事务不生效的7种情况
根据多年经验,我总结了事务失效的常见原因:
- 方法非public:Spring无法代理private方法
- 自调用问题:同类方法直接调用绕过代理
- 异常被捕获:catch块没有重新抛出异常
- 异常类型不匹配:默认只回滚RuntimeException
- 数据库引擎不支持:如MyISAM不支持事务
- 多数据源未指定:需要明确指定事务管理器
- 传播行为配置不当:如SUPPORTS在没有事务时继续执行
4.2 性能优化建议
- 合理设置超时:避免长事务占用连接
java复制@Transactional(timeout = 5) // 5秒超时
public void batchProcess() {
// ...
}
- 只读事务优化:对查询方法启用readOnly
java复制@Transactional(readOnly = true)
public List<User> queryUsers() {
// ...
}
- 减小事务粒度:避免大事务
java复制// 反例:整个批量处理在一个事务中
@Transactional
public void batchInsert(List<Item> items) {
items.forEach(this::insert);
}
// 正例:每100条一个事务
public void batchInsert(List<Item> items) {
Lists.partition(items, 100).forEach(batch -> {
transactionTemplate.execute(status -> {
batch.forEach(this::insert);
return null;
});
});
}
4.3 多数据源事务处理
在Spring Boot多数据源项目中,需要明确指定事务管理器:
java复制@Configuration
public class TransactionConfig {
@Bean
@Primary
public PlatformTransactionManager primaryTM(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public PlatformTransactionManager secondaryTM(
@Qualifier("secondaryDS") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
@Service
public class CrossService {
@Transactional("primaryTM")
public void primaryOperation() {
// 操作主数据源
}
@Transactional("secondaryTM")
public void secondaryOperation() {
// 操作次数据源
}
// 需要分布式事务的场景建议使用Seata等框架
}
5. 高级应用场景
5.1 事务事件监听
Spring允许在事务不同阶段触发事件:
java复制@Component
public class TransactionListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleAfterCommit(ApplicationEvent event) {
// 事务提交后发送消息
kafkaTemplate.send("topic", event);
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void handleAfterRollback(ApplicationEvent event) {
// 事务回滚后记录日志
log.error("Transaction rolled back", event);
}
}
5.2 嵌套事务实践
NESTED传播行为创建保存点实现部分回滚:
java复制@Service
public class NestedService {
@Transactional
public void parentMethod() {
try {
childMethod(); // 创建保存点
} catch (Exception e) {
// 只回滚childMethod的操作
log.warn("Child method failed", e);
}
// parentMethod的其他操作
}
@Transactional(propagation = Propagation.NESTED)
public void childMethod() {
// ...
}
}
5.3 分布式事务方案
对于跨服务的事务,常见的解决方案有:
- Seata:阿里开源的分布式事务框架
- 消息队列:最终一致性方案
- Saga模式:长事务解决方案
以消息队列为例的可靠消息模式:
java复制@Transactional
public void createOrder(Order order) {
// 1. 本地事务
orderMapper.insert(order);
// 2. 发送预备消息
String msgId = rocketMQTemplate.sendMessageInTransaction(
"order-topic",
MessageBuilder.withPayload(order).build(),
null
);
// 3. 记录消息关联
transactionMapper.insert(msgId, order.getId());
}
// 事务监听器实现本地消息表
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void confirmMessage(OrderEvent event) {
rocketMQTemplate.send("order-topic", event);
}
6. 生产环境经验总结
经过多个大型项目的实践验证,我总结了以下黄金法则:
- 事务注解应该放在Service层:Controller层不应该处理事务
- 默认添加rollbackFor=Exception.class:避免异常漏网
- 同类方法调用要警惕:最容易导致事务失效的陷阱
- 长事务是性能杀手:单个事务不要超过3秒
- 读写分离要明确:写操作必须走主库
- 测试要覆盖异常流程:特别是边界条件和异常情况
最后分享一个真实案例:某电商系统在促销期间出现库存超卖,排查发现是因为事务方法中先查询库存再更新,中间没有加锁。解决方案是在事务中配合使用SELECT FOR UPDATE,同时将事务超时设为500ms避免长时间锁竞争。这个优化使系统抗住了10倍于平时的流量冲击。