找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3936

积分

0

好友

540

主题
发表于 3 天前 | 查看: 15| 回复: 0

在Spring中进行事务管理非常简单,只需要在方法上加上注解@Transactional,Spring就可以自动帮我们进行事务的开启、提交、回滚操作。这导致许多人将Spring事务与@Transactional划上了等号,只要有数据库相关操作就直接给方法加上@Transactional注解。

不瞒你说,我之前也一直是这样,直到一次@Transactional的错误使用导致了生产事故,那次事故还直接影响了我的绩效。

@Transactional导致的生产事故回顾

2019年,我负责公司的一个内部报销项目。其中一个核心业务逻辑是:

  1. 员工加班可通过滴滴出行企业版打车,次日费用会自动同步到报销平台。
  2. 员工在平台勾选费用并创建报销单,系统会同步创建一条审批流(调用统一的流程平台)。

当时,创建报销单的核心代码如下:

/**
 * 保存报销单并创建工作流
 */
@Transactional(rollbackFor = Exception.class)
public void save(RequestBillDTO requestBillDTO){
     //调用流程HTTP接口创建工作流
    workflowUtil.createFlow(“BILL”,requestBillDTO);

    //转换DTO对象
    RequestBill requestBill = JkMappingUtils.convert(requestBillDTO, RequestBill.class);
    requestBillDao.save(requestBill);
    //保存明细表
    requestDetailDao.save(requestBill.getDetail())
}

代码看起来简洁“优雅”,先通过HTTP接口调用工作流引擎创建审批流,然后保存报销单。为了保证操作的一致性,在整个方法上加上了@Transactional注解。但你想过吗,这样真的能保证事务吗?

项目属于内部系统,并发不高,一直稳定运行。直到年末的一天下午(恰逢大雪后打车需求激增),公司发邮件通知年度报销窗口即将关闭。与此同时,工作流引擎正在进行安全加固。

收到邮件后,报销申请量在接近下班时达到顶峰,系统随即出现故障:数据库监控平台频繁告警,提示连接不足与大量死锁;日志显示调用流程引擎接口大量超时;同时持续抛出CannotGetJdbcConnectionException,数据库连接池被占满。

我们尝试过清理死锁进程,也进行过重启,但不到10分钟故障复现,投诉电话不断。最终,只能向全员发送停机维护通知。事后复盘,这次事故的根源正是save()方法,而罪魁祸首就是那个看似无害的@Transactional注解。

事故原因深度分析

@Transactional 注解基于AOP实现,本质是在目标方法执行前后进行拦截:在执行前开启或加入一个事务,在执行后根据情况提交或回滚。

当Spring处理该注解时,会从数据库连接池中获取一个connection,并开启事务绑定到当前线程的ThreadLocal上。这意味着,被@Transactional包裹的整个方法执行期间,都使用同一个数据库连接。如果方法中包含了耗时操作,比如调用外部HTTP接口、处理复杂业务逻辑或大批量数据,就会导致这个连接被长时间占用而不释放。一旦此类操作密集发生,数据库连接池很快就会被耗尽。

在这个案例中,事务内进行RPC调用(HTTP调用工作流引擎)导致数据库连接池被撑爆,是一个典型的长事务问题。类似的情况还包括在事务中进行大量数据查询、复杂的业务规则处理等。

什么是长事务?
顾名思义,就是运行时间长、长时间未提交的事务,也可以称之为大事务

长事务会引发哪些问题?
常见危害包括:

  1. 数据库连接池被占满,应用无法获取新的连接资源。
  2. 容易引发数据库死锁。
  3. 数据库回滚时间长。
  4. 在主从架构中会导致主从延迟变大。

如何有效避免长事务?

解决长事务的核心宗旨是:对事务方法进行拆分,尽量让事务变小、变快,减小事务的粒度。

首先,我们需要回顾Spring的两种事务管理方式。

声明式事务
通过在方法上使用@Transactional注解进行事务管理的方式称为声明式事务。

  • 优点:使用极其简单,开发者无需关心事务的开启、提交、回滚等底层细节,只需关注业务逻辑。
  • 缺点:事务的粒度是整个方法,无法进行精细化控制。

编程式事务
开发者基于底层API,在代码中手动管理事务的开启、提交、回滚等操作。在Spring项目中,可以使用TransactionTemplate

@Autowired 
private TransactionTemplate transactionTemplate; 

... 

public void save(RequestBill requestBill) { 
    transactionTemplate.execute(transactionStatus -> {
        requestBillDao.save(requestBill);
        //保存明细表
        requestDetailDao.save(requestBill.getDetail());
        return Boolean.TRUE; 
    });
} 

使用编程式事务最大的好处就是可以精细化控制事务范围。因此,避免长事务最直接有效的方法之一,就是放弃使用声明式事务@Transactional,转而使用编程式事务手动控制事务范围

那么,如果既想享受@Transactional的简便,又想避免长事务,有没有办法呢?

搞事表情包:你过来,我保证不打死你

答案是:对方法进行拆分,将非事务操作与事务操作分离开。例如:

@Service
public class OrderService{

    public void createOrder(OrderCreateDTO createDTO){
        query();
        validate();
        saveData(createDTO);
    }

  //事务操作
    @Transactional(rollbackFor = Throwable.class)
    public void saveData(OrderCreateDTO createDTO){
        orderDao.insert(createDTO);
    }
}

这里将query()validate()这两个不需要事务的操作,与事务方法saveData()拆分开。然而,这种简单的拆分会掉入@Transactional失效的经典陷阱——同一个类内的方法调用。因为@Transactional通过Spring AOP生成的代理对象生效,而类内部方法调用使用的是原始对象,导致事务失效。其他常见的事务失效场景还包括:

  • @Transactional 应用在非 public 修饰的方法上。
  • @Transactional 注解属性 propagation 设置错误。
  • @Transactional 注解属性 rollbackFor 设置错误。
  • 异常被catch捕获导致@Transactional失效。

正确的拆分方式有以下两种:

1. 抽取到独立的Service/Manager层
将事务方法移动到另一个Bean中,通过Spring注入调用,符合代理对象调用的条件。

@Service
public class OrderService{
    @Autowired
   private OrderManager orderManager;

    public void createOrder(OrderCreateDTO createDTO){
        query();
        validate();
        orderManager.saveData(createDTO);
    }
}

@Service
public class OrderManager{

    @Autowired
   private OrderDao orderDao;

  @Transactional(rollbackFor = Throwable.class)
    public void saveData(OrderCreateDTO createDTO){
        orderDao.saveData(createDTO);
    }
}

2. 使用 AopContext.currentProxy()
在启动类开启代理暴露,在方法内通过工具类获取当前代理对象来调用事务方法。

// SpringBootApplication.java

@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class SpringBootApplication {}
// OrderService.java

public void createOrder(OrderCreateDTO createDTO){
    OrderService orderService = (OrderService)AopContext.currentProxy();
    orderService.saveData(createDTO);
}

总结

@Transactional注解在开发中提供了极大的便利,但缺乏精细控制,稍有不慎就可能引发长事务,进而导致数据库连接池耗尽、死锁等严重生产问题。对于业务逻辑复杂,特别是包含远程调用、耗时操作的方法,更推荐使用编程式事务(TransactionTemplate)进行手动控制。如果坚持使用@Transactional,则务必按照上述方案对方法进行合理拆分,确保事务粒度尽可能小。

云栈社区Java和架构板块中,有更多关于事务管理、性能调优及复杂系统设计的深度讨论与实战案例,欢迎一起交流学习,共同规避这些“优雅”的陷阱。




上一篇:Spring事务管理进阶:解决@Transactional局限性的6种实战方案
下一篇:Spring @Transactional失效的3种场景排查与底层原理剖析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-10 10:01 , Processed in 0.557451 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表