作为一名在Java领域摸爬滚打多年的老码农,我见过太多因为滥用@Transactional注解导致的线上事故。Spring事务用起来确实方便,但就像一把双刃剑,用得不好反而会伤到自己。最近在给团队做代码评审时,又发现了几处典型的事务使用问题,这促使我决定系统梳理一下@Transactional的"罪与罚"。
Spring事务的本质是通过AOP代理实现的,这种设计带来了便利性的同时,也埋下了不少隐患。大厂之所以不推荐过度使用声明式事务,核心原因在于它的"黑盒效应"——开发者往往只关注了事务的表面功能,却忽视了底层实现细节,等到出现问题时为时已晚。下面我们就从技术实现角度,深入剖析那些容易踩坑的场景。
上周团队里有个小伙子遇到了一个诡异的问题:他写的下单方法明明加了@Transactional注解,但部分失败时数据却没有回滚。我一看代码就发现了问题:
java复制@Service
public class OrderService {
@Transactional
protected void createOrder(OrderDTO dto) {
// 订单创建逻辑
}
}
这里的关键点在于:Spring事务代理要求目标方法必须是public的。原理在于AbstractFallbackTransactionAttributeSource类中的这段逻辑:
java复制protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null; // 非public方法直接返回null表示不支持事务
}
// 后续处理逻辑...
}
实际开发中,我建议用IDE的代码检查工具(如SonarLint)配置规则,自动检测非public的事务方法,避免这类低级错误。
另一个常见的坑是在事务方法上使用final修饰符:
java复制@Service
public class PaymentService {
@Transactional
public final void processPayment(PaymentInfo info) {
// 支付处理逻辑
}
}
这会导致事务失效,因为Spring默认使用CGLIB创建代理类,而final方法无法被重写。同理,static方法也存在这个问题。我曾经在重构一个老系统时,就遇到过因为历史代码中大量final方法导致事务失效的案例。
这是最常被忽视的问题之一,几乎每个Java开发者都至少踩过一次这个坑:
java复制@Service
public class UserService {
public void register(User user) {
insertUser(user);
initUserProfile(user); // 事务失效!
}
@Transactional
public void initUserProfile(User user) {
// 初始化用户资料
}
}
当通过this引用直接调用时,实际上绕过了Spring代理,导致事务注解失效。解决方案有三种:
java复制@Service
public class ProfileService {
@Transactional
public void initUserProfile(User user) {
// 初始化逻辑
}
}
java复制@Service
public class UserService {
@Autowired
private UserService self;
public void register(User user) {
self.initUserProfile(user);
}
}
java复制((UserService)AopContext.currentProxy()).initUserProfile(user);
在电商系统中,我们经常需要异步处理一些非核心流程。但下面这种写法是有问题的:
java复制@Transactional
public void placeOrder(Order order) {
orderDao.save(order);
// 异步记录操作日志
new Thread(() -> {
logService.recordLog(order); // 新线程的事务与主线程无关
}).start();
}
事务绑定到线程上下文的本质是因为Spring使用ThreadLocal保存数据库连接。不同线程获取的是不同的连接,自然无法实现事务一致性。对于异步场景,正确的做法是:
在一次系统迁移中,我们发现有张表的事务始终不生效。最终发现是该表使用了MyISAM引擎:
sql复制CREATE TABLE `operation_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
...
) ENGINE=MyISAM;
MyISAM不支持事务这个特性经常被遗忘。建议在项目启动时,就通过以下SQL检查表引擎:
sql复制SELECT TABLE_NAME, ENGINE
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'your_db';
这是最隐蔽的事务陷阱之一:
java复制@Transactional
public void updateProduct(Product product) {
try {
productDao.update(product);
inventoryService.deduct(product); // 可能抛出RuntimeException
} catch (Exception e) {
log.error("更新失败", e); // 吞掉异常导致事务不回滚
}
}
Spring默认只对RuntimeException和Error进行回滚。如果捕获异常后不重新抛出,或者抛出的是受检异常,事务都不会回滚。正确的处理方式:
java复制@Transactional(rollbackFor = Exception.class) // 明确指定回滚异常类型
public void updateProduct(Product product) throws Exception {
try {
// 业务逻辑
} catch (BizException e) { // 业务异常特殊处理
throw new Exception("业务处理失败", e);
}
}
在非Spring Boot项目中,如果忘记配置事务管理器,所有@Transactional都会失效:
xml复制<!-- 必须显式配置 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
我曾经接手过一个老项目,就是因为缺少这行配置,导致整个系统的事务功能形同虚设。建议在项目启动时,通过以下方式验证事务配置是否生效:
java复制@SpringBootTest
class TransactionTest {
@Autowired
private PlatformTransactionManager transactionManager;
@Test
void testTxConfig() {
assertNotNull(transactionManager); // 验证事务管理器注入成功
}
}
Spring提供了7种事务传播行为,但90%的场景我们只需要关注这三种:
| 传播行为 | 说明 | 适用场景 |
|---|---|---|
| REQUIRED(默认) | 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务 | 大多数业务方法 |
| REQUIRES_NEW | 创建一个新的事务,如果当前存在事务,则把当前事务挂起 | 日志记录、审计等独立操作 |
| NESTED | 如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则表现与REQUIRED一样 | 可部分回滚的子操作 |
看这个例子:
java复制@Transactional
public void mainMethod() {
// 操作1
subMethod(); // 抛出异常
// 操作2
}
@Transactional(propagation = Propagation.NESTED)
public void subMethod() {
// 子操作
}
很多人以为NESTED传播下,subMethod回滚不会影响mainMethod,这是错误的!NESTED只是创建保存点,外层方法捕获异常后可以选择部分回滚。如果异常传播到外层,整个事务仍然会全部回滚。
这两个属性经常被忽视:
java复制@Transactional(timeout = 5, readOnly = true)
public List<Product> queryProducts(Condition cond) {
// 查询逻辑
}
大事务的典型特征:
可以通过Spring的TransactionSynchronizationManager监听事务生命周期:
java复制@Aspect
@Component
@Slf4j
public class TransactionMonitor {
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object monitorTx(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed();
long cost = System.currentTimeMillis() - start;
if (cost > 500) {
log.warn("Long transaction detected: {} ms, method: {}",
cost, pjp.getSignature());
}
return result;
}
}
方案1:编程式事务精确控制
java复制public void processOrder(Order order) {
// 非事务操作
validate(order);
// 事务操作1
transactionTemplate.execute(status -> {
orderDao.save(order);
return null;
});
// 非事务操作
sendEvent(order);
// 事务操作2
transactionTemplate.execute(status -> {
inventoryService.update(order);
return null;
});
}
方案2:拆分业务逻辑
将大事务拆分为多个小事务,通过消息队列或事件驱动实现最终一致性。
优先使用编程式事务
java复制@Autowired
private TransactionTemplate transactionTemplate;
public void doBusiness() {
// 非事务操作
transactionTemplate.execute(status -> {
// 事务操作
return null;
});
}
事务注解使用规范
java复制@Transactional(
rollbackFor = Exception.class,
timeout = 3,
readOnly = false
)
监控与告警
代码审查要点
在分布式架构流行的今天,传统的本地事务已经不能满足所有场景。对于复杂的业务系统,建议结合使用:
记住:事务不是越大约好,而是越精确越好。合理控制事务边界,是保证系统稳定性的重要手段。