事务是一组操作的集合,是一个不可分割的操作。事务会把所有的操作作为一个整体,一起向数据库提交或者是撤销操作请求。所以这组操作要么同时成功,要么同时失败。我们在进行程序开发时,也会有事务的需求。

参考文章:

深入解析 @Transactional——Spring 事务管理的核心

Springboot @Transactional大事务处理的几点建议

Spring事务基本信息

Spring事务在解决什么问题

  1. ‌数据一致性‌:在分布式系统中,多个服务或组件可能需要同时操作同一数据,事务确保这些操作要么全部成功,要么全部失败,从而保持数据的一致性。
  2. 并发控制‌:在多用户并发访问的情况下,事务可以控制对共享资源的访问,避免数据冲突和损坏。
  3. 故障恢复‌:当操作过程中发生错误时,事务可以回滚到操作前的状态,确保系统不会处于不一致的状态。

Spring事务的实现原理

Spring事务的实现基于 AOP (面向切面编程)和 动态代理 。Spring通过代理机制对目标对象进行包装,插入事务管理的逻辑,包括开启事务、提交事务和回滚事务等。具体来说:

  • 动态代理‌:Spring使用 JDK动态代理CGLIB动态代理 来创建代理对象。
  • AOP‌:通过切面编程,在方法调用前后插入事务管理的逻辑。

Spring事务的配置和使用

在Spring中配置事务主要通过注解@Transactional来实现。该注解可以应用于类或方法上,指示该方法需要事务管理。


@Transactional

基础信息

@Transactional是Spring框架中用于声明式事务管理的关键注解,其核心功能是确保标注的方法或类在数据库操作中遵循ACID原则(原子性、一致性、隔离性、持久性),实现要么全部成功提交,要么全部回滚的机制。通过AOP(面向切面编程)实现,在方法执行前开启事务,执行后根据结果提交或回滚。‌‌

Spring 事务默认的回滚规则

只有未捕获的 RuntimeException(运行时异常)或 Error 才会触发回滚,而普通的 Exception(检查异常)不会触发回滚。

若你希望所有异常都能回滚,可加上 rollbackFor = Exception.class,避免出现“事务看起来生效了,但并没有真正回滚”的情况。

常见属性配置

属性 作用 示例
propagation 控制事务传播行为(如REQUIRED、REQUIRES_NEW) @Transactional(propagation = Propagation.REQUIRED)
isolation 设置事务隔离级别(如READ_COMMITTED) @Transactional(isolation = Isolation.READ_COMMITTED)
rollbackFor 指定触发回滚的异常类型 @Transactional(rollbackFor = Exception.class)
timeout 定义事务超时时间(秒) @Transactional(timeout = 30)

Spring事务为什么会失效

  1. try-catch导致的事务失效

    Spring 只有在方法抛出异常时,才会触发回滚。如果你在 catch 里吞掉了异常,那事务也就不会回滚了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @Transactional
    public void deletePersonById(Long id){
    try {
    // 业务处理
    int x = 1 / 0; // 触发异常
    } catch (Exception e) { // catch 里吞掉了异常,Spring 感知不到异常的发生
    System.out.println("发生异常,但事务未回滚");
    }
    }
    // 手动抛出异常
    @Transactional
    public void deletePersonById(Long id){
    try {
    // 业务处理
    int x = 1 / 0; // 触发异常
    } catch (Exception e) {
    throw new RuntimeException("手动抛出异常,确保事务回滚", e);
    }
    }
    // 异常自然传播
    @Transactional
    public void deletePersonById(Long id) throws Exception {
    // 业务处理
    int x = 1 / 0; // 事务会回滚
    }
  2. 方法不是public

    @Transactional 只会作用于 public 方法,如果你加在 privateprotected 方法上,事务不会生效。因为Spring 事务是通过 代理机制 实现的,而 JDK 动态代理只能代理 public 方法,所以其他访问级别的方法都不行。

    1
    2
    3
    4
    @Transactional
    private void deleteDept(Long id) { } // 事务不会生效
    @Transactional
    protected void deleteDept(Long id) { } // 事务不会生效
  3. 同一类里,方法互相调用

    这里 deleteDept 方法调用了 deleteEmp,但 deleteEmp 上的 @Transactional 不会生效!原因是:Spring 的事务是基于代理的,this.deleteEmp(id) 直接调用了本类的方法,没有经过 Spring 代理,所以事务不会生效。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Service
    public class DeptServiceImpl {
    @Autowired
    private DeptMapper deptMapper;

    @Transactional
    public void deleteDept(Long id) {
    this.deleteEmp(id); // 事务不会生效!
    }

    @Transactional
    public void deleteEmp(Long id) {
    empMapper.delByDeptId(id);
    }
    }

    正确的处理方法应是通过 Spring 管理的 Bean 调用(如下),或者使用 ApplicationContext 获取代理对象,再调用方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Service
    public class DeptServiceImpl {
    @Autowired
    private DeptServiceImpl self;

    @Transactional
    public void deleteDept(Long id) {
    self.deleteEmp(id); // 事务生效!
    }

    @Transactional
    public void deleteEmp(Long id) {
    empMapper.delByDeptId(id);
    }
    }
  4. 数据库引擎不支持事务

    如果你用的 MySQL 表引擎是 MyISAM,事务是不可能生效的,因为 MyISAM 根本不支持事务!要确保你的表是 InnoDB:

    1
    2
    SHOW TABLE STATUS WHERE Name = 'dept';
    ALTER TABLE dept ENGINE = InnoDB;

@Transactional的传播机制

@Transactional注解支持多种事务传播机制,这些机制定义了事务的行为方式。通过Propagation配置,主要的传播机制及含义如下:

传播类型 含义
Propagation.REQUIRED 如果当前已有事务则加入当前事务,否则开启新的事务
Propagation.REQUIRED_NEW 无论当前是否有事务都开启新的事务;如果当前存在事务,则把当前事务挂起。
Propagation.SUPPORTED 如果当前事务存在就加入事务,否则以非事务运行
Propagation.NOT_SUPPORTED 始终以非事务方式执行;如果当前存在事务,则挂起当前事务
Propagation.NEVER 不使用事务,如果当前事务存在,则抛出异常
Propagation.MANDATORY 当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。
Propagation.NESTED 父子(嵌套)事务,父提交子提交,父回滚全回滚,子回滚不影响父事务

这些传播机制提供了灵活的事务控制选项,可以根据不同的业务需求选择合适的事务行为,避免事务的方法过于长,一个事务里面调用的库表越多,就越有可能造成死锁,所以我们要根据具体的需要拆分使用。例如:

  • 在需要确保操作完全独立于其他事务时,可以使用REQUIRES_NEW
  • 不需要事务时,可以使用NOT_SUPPORTED或NEVER
  • 需要嵌套事务时,可以使用NESTED

避免事务的方法过于长,一个事务里面调用的库表越多,就越有可能造成死锁,所以我们要根据具体的需要拆分使用

高吞吐量下使用@Transactional注解导致性能降低

  1. 批量处理

    尽可能将多个数据库操作合并到单个事务中。例如,可以使用JDBC的batch updates或者在ORM框架(如Spring Data JPA, Hibernate)中利用其批量操作功能。 示例(Spring Data JPA):

    1
    2
    3
    4
    @Transactional
    public void batchSave(List<Entity> entities) {
    entityRepository.saveAll(entities);
    }
  2. 减少事务范围

    尽量减小@Transactional注解覆盖的方法范围。只在确实需要事务控制的代码块上使用@Transactional。例如,只在服务层而非控制器层使用事务。

  3. 异步事务处理

对于非阻塞操作,可以考虑将事务逻辑移至异步处理中。例如,使用@Async注解来异步执行事务操作,但这通常需要额外的配置来确保事务的正确管理。示例(Spring @Async):

1
2
3
4
5
6
7
8
9
10
@EnableAsync
@Service
public class AsyncService {

@Async
@Transactional
public void asyncTransactionalMethod() {
// 执行事务操作
}
}
  1. 优化数据库配置

    • 索引优化:确保数据库表上有适当的索引,以加快查询和插入速度。

    • 连接池优化:使用高效的数据库连接池(如HikariCP),并合理配置其参数(如最大连接数、连接超时时间等)。

    • 读写分离:对于读多写少的应用,可以考虑实现数据库的读写分离。

  2. 避免大事务

    • 避免在单个事务中处理大量数据。如果可能,将大事务拆分成多个小事务。
  3. 使用乐观锁或悲观锁

    • 在高并发场景下,使用乐观锁或悲观锁可以减少锁的竞争,提高并发性能。乐观锁通常用于写操作较少的情况,而悲观锁则适用于写操作较多的情况。

@Transactional大事务处理

什么是大事务

总体任务对应的事务运行时间比较长,长时间未提交的事务

大事务的危害

  • 并发情况下,数据库连接池资源占满。大事务提交不及时,导致连接资源释放缓慢。
  • 数据库死锁和锁等待。innodb引擎背景下,事务如果占用了排他锁,会容易导致并发情况下数据死锁或者锁等待。
  • 大事务Rt时间长,容易导致接口超时。
  • 大事务回滚时间长。
  • 数据库主从架构下,数据同步延迟

解决方案

  • 将声明式事务的@Transactional方式 合理的替换为 编程式事务TransactionTemplate 的方式
    声明式事务的粒度最小是整个方法,可能会导致业务里不必要的逻辑都加了事务。编程式事务细化需要加事务的逻辑上,形成实际有用的事务块。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Autowired
    private TransactionTemplate transactionTemplate;

    public void testTransaction() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
    @Override
    protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
    try {
    // .... 业务代码
    } catch (Exception e){
    //回滚
    transactionStatus.setRollbackOnly();
    }
    }
    });
    }
  • 将查询放在事务方法外
    使用@Transactional 又想避免产生大事务,需对方法进行拆分,将不需要事务管理的逻辑与事务操作分开

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Service
    public class TransactionTestService{
    // 避免同一个类内部方法相互调用,实例方法调用代理方法而导致事务失效
    @Resource
    private TransactionTestService service;

    public void create(ParamDto dto){
    queryData1();
    queryData2();
    service.save(dto);
    }

    //事务操作
    @Transactional(rollbackFor = Exception.class)
    public void save(ParamDto dto){
    paramDao.insert(dto);
    }
    }
  • 避免跨服务间的远程调用
    服务间的通讯及服务之间的调用时间 受网络环境和远端接口Rt时间的影响,可能会比较耗时。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // 反例:
    // 事务操作
    @Transactional(rollbackFor = Exception.class)
    public void save(ParamDto dto){
    // 调用了其他服务
    otherRemoteApi();
    paramDao.insert(dto);
    }

    // 修改为:
    @Autowired
    private TransactionTemplate transactionTemplate;
    public void save(ParamDto dto){
    // 调用了其他服务
    otherRemoteApi();
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
    @Override
    protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
    try {
    paramDao.insert(dto);
    } catch (Exception e){
    //回滚
    transactionStatus.setRollbackOnly();
    }
    }
    });
    }
  • 事务中不应该一次性处理太多的数据,可以使用分批执行

  • 事务中的方法可以根据业务使用异步执行