SpringBoot事务基础知识及开发中遇到的问题分享

一、什么是事务

数据库事务是指作为单个逻辑工作单元执行的一系列操作,这些操作要么一起成功,要么一起失败,是一个不可分割的工作单元。

二、Spring Boot中的事务管理方式

在 Spring Boot 中事务管理有两种方式:编程式事务和声明式事务。

1、编程式事务

把事务代码嵌入到业务逻辑中。代码耦合度高。

在 Spring Boot 中实现编程式事务有两种方法:对于编程式事务管理,spring推荐使用TransactionTemplate。

1、使用 TransactionTemplate 对象实现编程式事务:

@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                try {
                    // ....  业务代码
                } catch (Exception e){
                    //回滚
                    transactionStatus.setRollbackOnly();
                }
            }
        });
}

2、使用更加底层的TransactionManager对象实现编程式事务:

@Autowired
private PlatformTransactionManager transactionManager;
​
public void testTransaction() {
  // 获取TransactionStatus
  TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
          try {
               // ....  业务代码
              
               // 提交事务
              transactionManager.commit(status);
          } catch (Exception e) {
               // 回滚事务
              transactionManager.rollback(status);
          }
}

2、声明式事务

原理:基于AOP的思想,将事务和业务代码解耦。

一般通过在方法上或类上添加 @Transactional 注解实现声明式事务。当标注在类上时,表示该类的所有public方法都将支持事务。当标注在方法上时,表示该方法将在一个事务内执行。

在方法执行前,自动开启事务;在方法成功执行完,自动提交事务;如果方法在执行期间,出现了异常,那么它会自动回滚事务。

@Transactional注解实现原理:

该注解是通过JDBC的事务 + Spring的AOP动态代理来完成的。事务开始时,通过AOP机制,生成一个代理connection对象,

并将其放入 DataSource 实例的某个与 DataSourceTransactionManager 相关的某处容器中。

在接下来的整个事务中,客户代码都应该使用该 connection 连接数据库,

执行所有数据库命令。

事务结束时,回滚在第1步骤中得到的代理 connection 对象上执行的数据库命令,

然后关闭该代理 connection 对象。

三、Spring事务管理接口介绍

三大基础组件:PlatformTransactionManager事务管理器接口、TransactionDefinition、TransactionStatus

  • PlatformTransactionManager : 事务管理器。通过这个接口,Spring 为各个平台如:JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager)等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。

  • TransactionDefinition : 事务的一些属性信息,如超时时间、隔离级别、传播属性、回滚规则、是否只读。

  • TransactionStatus: 接口用来记录事务的一些状态信息,如是否是一个新的事务、是否已被标记为回滚。

public interface TransactionStatus{
    boolean isNewTransaction(); // 是否是新的事务
    boolean hasSavepoint(); // 是否有恢复点
    void setRollbackOnly();  // 设置为只回滚
    boolean isRollbackOnly(); // 是否为只回滚
    boolean isCompleted; // 是否已完成
}

四、spring中的事务属性详解:

1、事务传播性:事务传播行为是为了解决业务层方法之间互相调用的事务问题。

当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。 例如: 方法可能继续在现有事务中运行, 也可能开启一个新事务,并在自己的事务中运行。 事务的传播行为可以由传播属性指定, 在TransactionDefinition中定义了7个表示传播行为的常量,Spring 中对应定义了 7 种类传播行为:默认传播属性是REQUIRED。

事务的传播性:在不同service层调事务。

@Service
Class A {
    @Autowired
    B b;
    @Transactional(propagation = Propagation.xxx)
    public void aMethod {
        //do something
        b.bMethod();
    }
}
​
@Service
Class B {
    @Transactional(propagation = Propagation.xxx)
    public void bMethod {
       //do something
    }
}

举个例子:我们在 A 类的aMethod()方法中调用了 B 类的 bMethod() 方法。这个时候就涉及到业务层方法之间互相调用的事务问题。如果我们的 bMethod()如果发生异常需要回滚,如何配置事务传播行为才能让 aMethod()也跟着回滚呢?

这里写图片描述

2、事务隔离性:

数据库允许多个并发事务同时对其数据进⾏读写和修改,隔离性可以防⽌多个事务并发执⾏时由于交叉执⾏⽽导致数据的不⼀致。在实际开发与应用中存在多个事务同时运行的情况,多个事务同时执行期间对数据的读取可能会出现异常,数据的不一致性。为了解决这些异常因此需要对事务的隔离性进行设置。事务隔离级别的设置是为了防止其它事务影响当前事务的一种策略。

2.1异常的种类:多个事务运行可能导致以下几种异常,

脏读:是指在一个事务中读取了另一个事务的未提交的更改,然后在这个事务中对这些更改进行了修改,最后另一个事务提交了这些更改,导致数据的一致性被破坏。

不可重复读:指在同一个事务中,多次读取同一行数据时,得到的结果不一致,这是因为另一个事务在当前事务执行期间修改了这个数据。假设有一个订单表,其中包含一个customer_id字段,用于存储订单的客户ID。在一个事务中,你读取了一个订单,并记录下了这个订单的customer_id。然后,在另一个事务中,你修改了这个订单的customer_id。最后,在第一个事务中,你再次读取这个订单,那么你可能会读取到修改后的customer_id,这就是不可重复读。

幻读:指在同一个事务中,多次查询同一个范围的数据时,读取到的数据的数目不同,这是因为另一个事务在当前事务执行期间插入或删除了数据。例如,如果在一个事务中读取了一个订单表中的所有订单,然后在另一个事务中插入了一个新的订单,最后当前事务再次读取这个订单表中的所有订单,那么就可能会出现幻读。这就是因为在当前事务执行期间,另一个事务插入了一个新的订单,导致当前事务读取到的数据的数目不同。

2.2 每个隔离级别对这些问题的解决情况如下:隔离级别由低到高。

读未提交:一个事务还没提交时,它做的变更就能被别的事务看到。

读已提交:一个事务提交之后,它做的变更才会被其他事务看到。

可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。

串行化:顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

其中读提交解决了脏读,可重复读解决了“不可重复读”,串行化解决了幻读。当然串行化也是效率最低的。

2.3 SpringBoot中的隔离级别

TransactionDefinition接口中定义了五个表示隔离级别的常量,Spring 也相应地定义了一个枚举类:Isolation。

DEFAULT(默认):使用底层数据库的默认隔离级别。对于大多数数据库来说,通常是READ_COMMITTED。

READ_UNCOMMITTED:读未提交。允许脏读、不可重复读和幻读。这是最低的隔离级别,一个事务可以读取另一个事务未提交的数据。

READ_COMMITTED:读已提交。避免脏读。一个事务只能读取已提交的数据。但是,可能发生不可重复读和幻读,因为在同一个事务中,另一个事务可能会修改数据。

REPEATABLE_READ:可重复读。避免脏读和不可重复读。在同一个事务中,多次读取同一行数据时,得到的结果是一致的。但是,可能发生幻读,因为在同一个事务中,另一个事务可能会插入或删除数据。

SERIALIZABLE:串行化。避免脏读、不可重复读和幻读。事务串行执行,保证了数据的一致性和完整性,但是性能太低。

3、事务回滚规则

这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)时才会回滚,Error 也会导致事务回滚,但是,在遇到检查型(Checked)异常时不会回滚。

如果你想要回滚你定义的特定的异常类型的话,可以这样:

@Transactional(rollbackFor= MyException.class)

4、事务超时时间

所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为-1,这表示事务的超时时间取决于底层事务系统或者没有超时时间。

5、事务是否只读

对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。

MySQL 默认对每一个新建立的连接都启用了autocommit模式。在该模式下,每一个发送到 MySQL 服务器的sql语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务。但是,如果你给方法加上了Transactional注解的话,这个方法执行的所有sql会被放在一个事务中。如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的什么收益。如果不加Transactional,每条sql会开启一个单独的事务,中间被其它事务改了数据,都会实时读取到最新值。

如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持 SQL 执行期间的读一致性;

如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询 SQL 必须保证整体的读一致性,否则,在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。

五、@Transactional注解

1、属性参数说明:

参数 意义 isolation 事务隔离级别,默认为DEFAULT propagation 事务传播机制,默认为REQUIRED readOnly 事务读写性,默认为false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。 noRollbackFor 一组异常类,遇到时不回滚,默认为{} noRollbackForClassName 一组异常类名,遇到时不回滚,默认为{} rollbackFor 一组异常类,遇到时回滚,默认为{} rollbackForClassName 一组异常类名,遇到时回滚,默认为{} timeout 超时时间,以秒为单位,默认值为 -1。如果超过该时间限制但事务还没有完成,则自动回滚事务。 value 可选的限定描述符,指定使用的事务管理器,默认为“”

这里写图片描述

2、作用范围:

方法:推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。

类:如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。

接口:不推荐在接口上使用。

3、@Transactional失效场景:

1、同一个类中方法调用,导致失效

由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。

EmployeeServiceImpl类:    
    @Transactional
    public void calledFunc() {
        LambdaUpdateWrapper<Employee> wrapper = new LambdaUpdateWrapper<>();
        wrapper.set(Employee::getName, "修改后的姓名").eq(Employee::getId, "1");
        employeeMapper.update(null, wrapper);
        throw new RuntimeException("异常");
    }
 
    @Override
    public void test() {
        calledFunc();
    }
Test类:
    @Test
    void testTransaction() {
        employeeService.test();
    }

此时虽然方法抛出了异常,但是事务控制失效,异常事务并没有回滚,数据更新成功了。

必须通过代理过的类从外部调用目标方法才能生效。

2、如果一个方法添加了@Transactional注解声明事务,而方法内又使用了try catch 捕捉异常,则方法内的异常捕捉会覆盖事务对异常的判断,从而异致事务失效而不回滚。

EmployeeServiceImpl类:
    @Transactional
    public void test2() {
        try {
            LambdaUpdateWrapper<Employee> wrapper = new LambdaUpdateWrapper<>();
            wrapper.set(Employee::getName, "修改后的姓名").eq(Employee::getId, "1");
            employeeMapper.update(null, wrapper);
            throw new RuntimeException("异常");
        } catch (RuntimeException e) {
            System.out.println("捕获到了异常");
        }
    }

如何解决:

第一个方法:给@Transactional注解增加:rollbackFor属性并手动抛出指定的异常。

第二个方法:在捕捉到异常后手动rollback。

3、@Transactional 属性 rollbackFor 设置错误,导致异常不满足回滚条件

EmployeeServiceImpl类:   
   @Transactional
    public void calledFunc() throws Exception{
        LambdaUpdateWrapper<Employee> wrapper = new LambdaUpdateWrapper<>();
        wrapper.set(Employee::getName, "修改后的姓名").eq(Employee::getId, "1");
        employeeMapper.update(null, wrapper);
        throw new Exception("受检异常");
    }
 
    @Override
    @Transactional
    public void test() throws Exception {
        calledFunc();
    }
    @Test
    void testTransaction() {
        try {
            employeeService.test();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

解决方法:设置rollbackFor:@Transactional(rollbackFor = Exception.class)

4、@Transactional 注解传播属性 propagation 设置错误,父事务回滚子事务不会回滚

EmployeeServiceImpl类:
    @Override
    @Transactional
    public void test() {
        LambdaUpdateWrapper<Employee> wrapper = new LambdaUpdateWrapper<>();
        wrapper.set(Employee::getName, "修改后的姓名").eq(Employee::getId, "2");
        employeeMapper.update(null, wrapper);
        detectApplicationDealer.calledFunc();
        throw new RuntimeException("受检异常");
    }
NameDetectApplicationDealer类:
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public void calledFunc() {
        LambdaUpdateWrapper<Employee> wrapper = new LambdaUpdateWrapper<>();
        wrapper.set(Employee::getName, "修改后的姓名").eq(Employee::getId, "1");
        employeeMapper.update(null, wrapper);
    }

调用方法抛出异常,被调用方法使用Propagation.REQUIRES_NEW属性,会开启一个新的事务,外层事务不影响内部事务的提交/回滚;内部事务出现异常,会影响外部事务的提交/回滚。

5、@Transactional 应用在非 public 修饰的方法上

6、数据库不支持事务,事务也不会自动回滚

当我们在程序中添加了 @Transactional,相当于给调用的数据库发送了:开始事务、提交事务、回滚事务的指令,但是如果数据库本身不支持事务,比如 MySQL 中设置了使用 MyISAM 引擎,因为它本身是不支持事务的,这种情况下,即使在程序中添加了 @Transactional 注解,那么依然不会有事务的行为,也就不会执行事务的自动回滚了。

在这种情况下,我们只需要设置 MySQL 的引擎为 InnoDB 就可以解决问题了,因为 InnoDB 是支持事务的,当然 MySQL 5.1 之后的默认引擎就是 InnoDB。

六、开发中遇到的问题分享

1、未捕获非运行时异常。

2、事务未提交导致查询数据库查不到。

七、参考文档:

Spring 事务详解 | JavaGuide(Java面试 + 学习指南)

Spring Boot中的事务是如何实现的 - 知乎 (zhihu.com)

详解Spring的事务管理PlatformTransactionManager-腾讯云开发者社区-腾讯云 (tencent.com)

面试突击86:SpringBoot 事务不回滚?怎么解决?Java王磊_InfoQ写作社区

【Spring Boot】事务的隔离级别与事务的传播特性详解:如何在 Spring 中使用事务?不同隔离级别的区别?_spring事务隔离级别-CSDN博客

  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值