聊聊事务处理-Spring声明式事务的使用

 2022-09-13
原文地址:https://blog.51cto.com/u_14295608/5455811

导语

在互联网数据库的使用中,对于那些电商和金融网站,最关注的内容毫无疑问就是数据库事务,因为对于热点商品的交易和库存以及金融产品的金额,是不允许发生错误的。但是他们面临的问题是,热门商品或者金融产品在上线销售的瞬间可能面临高并发的场景。例如,一款低门槛且高利率的金融产品实现宣布,第二天9带你进入抢购的阶段,那么该网站成千上万的会员会在第二天9点之前打开手机,平板和电脑准备疯狂抢购,在产品发布的瞬间会有大量的请求到达服务器,这时候因为存在高并发所以数据库的数据将在一个多事务的场景下运行,在没有采用一定手段的情况下就会造成数据不一致。于此同时网站也面临巨大的性能压力。面对这样的高并发的场景,掌握数据库事务机制是至关重要的,它能够帮助我们在一定程度上保证数据的一致性,并且有效提升系统性能,避免系统产生宕机,这对互联网企业应用的成败至关重要。

在Spring 中,数据库的事务是通过AOP技术来提供服务的。在JDBC中存在着大量的try…catch…finish…语句,也同时存在着大量的冗余代码,如那些打开和关闭数据库链接的代码以及事务回滚的代码。使用Spring AOP之后,Spring将他们擦除了,你可以看到更加干净的代码,没有那些try…catch…finish语句,也没有大量的冗余代码。不过在讨论高级话题之前,我们呢需要从简单的知识入手,并且讲述数据库隔离级别的内容,否则有些读者会很难理解后面的内容。
对于一些业务网站来说,产品库存的扣减,交易记录以及账户都必须要么同时成功,要么同时失败,这便是一种事务机制,对于这样的机制数据库给予了支持。而在一些特殊的场景之下,如一个批处理,他将处理多个交易,但是在一些交易中发生了异常,者额个时候则不能将所有的交易都回滚。如果所有的交易都回滚,那么那些本来能够正常处理的业务也无端的被回滚了,这显然不是我们所期待的结果。通过配置Spring的数据库事务传播行为,可以很方便的处理这样的场景。

不过无论如何都应该先配置数据库的信息,所以我们现在application.yml中进行代码配置

    spring:
      datasource: #数据源
        type: com.alibaba.druid.pool.DruidDataSource
        druid:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://172.17.0.1:3306/hc_official_website_1?useUnicode=true&characterEncoding=UTF8&serverTime=Asia/Shanghai
          filters: stat, wall, config
          #初始连接数
          initial-size: 1
          #最大活动数
          max-active: 100
          #最大等待毫秒数,10分钟
          max-wait: 60000
          #最小等待链接数量
          min-idle: 1
          time-between-eviction-runs-millis: 60000
          min-evictable-idle-time-millis: 300000
          validation-query: select 'x'
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          pool-prepared-statements: true

通过这样配置就可以在项目中定义好数据库连接池,这样就可以使用数据库了,本章后面的内容就可以使用他了。在Spring数据库事务中可以使用编程式事务,也可以使用声明式事务。大部分情况下,会使用声明式事务。编程式事务这种比较底层的方式已经基本被淘汰了,Spring Boot也不推荐我们使用,因此不讨论编程式事务。这里吧日志级别降低到DEBUG级别,这样就可以看到很详细的日志了,这样有助于观察Spring数据库事务机制的运行。由于目前MyBatis被广泛使用在持久层中,因此将以MyBatis框架作为持久层进行论述。

1.简单的数据库事务流程

通过AOP,允许我们把公共的代码抽取出来,单独实现,为了更好的论述,下面给出执行SQL的流程图

202209132232417921.png

这个流程与我们AOP约定流程十分接近,而在图中,有业务逻辑的部分也只是执行SQL的部分,其他步骤都是比较固定的,按照AOP的设计思想,就可以把除了执行SQL这步之外的步骤抽取出来,这便是Spring数据库事务编程的思想。

2. Spring 声明式事务的使用

从第四章中可以看到Spring AOP的约定,他会把我们的代码织入约定的流程中。同样的,使用AOP的思维之后,执行SQL的代码就可以织入Spring约定的数据库事务的流程中,所以需要首先掌握这个约定。

2.1 Spring声明式事务的使用

在讲解Spring AOP的时候,只要我们遵循约定,就可以把自己开发的代码织入到约定的流程中。为了擦除令人厌烦的try catch finish语句,减少那些数据库连接开闭和事务回滚提交的代码,SPring利用AOP为我们提供了一个数据库事务的约定流程。通过这个流程就可以减少大量的冗余代码和一些没有必要的try…catch…finish语句,让开发着能够更加集中于业务的开发,而不是数据库链接资源和事务的功能开发,这样开发的代码可读性就更高,也更好维护。
对于事务,需要通过标注告诉Spring在什么地方启用数据库事务功能。对于声明式事务,是使用@Transactional进行标注的。这个注解可以标注在类上或者方法上,当他标注在类上时,代表这个类所有公共(public)非静态的方法都将启用事务功能。在@Transactional中,还允许配置许多的属性,如事务的隔离级别和传播行为,这是本章的核心内容。又如异常类型,从而确定放生什么异常下回滚事务和什么异常下不回滚事务。这些配置内容,是在Spring IOC容器在加载时就会将这些配置信息解析出来,然后将这些信息存到事务定义器(TransactionDefinition接口的实现类)里,并且记录那些类,那些方法需要启动事务,采用什么策略去启动事务。这个过程中,我们所要做的只是给需要事务的类或者方法标注@Transactional和配置属性而已,并不是十分复杂。
有了@TRansactional的配置,Spring就会知道在哪里启动事务机制,其约定流程如下

202209132232431062.png

因为这个约定非常重要所以在这里做进一步讨论

当Spring的上下文开始调用被@Transactional标注的类或者方法时,Spring就会产生AOP的功能。注意事务的底层需要AOP的功能,这是Spring事务的底层实现,后面我们呢会看到一些陷阱。那么当开启事务时,就会根据事务定义器内的配置去设置事务,首先是根据传播行为去确定事务的策略,有传播行为我们后面再讲,这里暂时放下。然后是隔离级别,超时时间,只读等内容的设置,只是这一步设置事务并不需要开发者完成,而是Spring事务拦截器根据@Transactional配置的内容来完成。

在上述场景中,Spring通过对注解@Transactional属性配置去设置数据库事务,跟着Sprign就会开始调用开发者编写的业务代码。执行业代码可能发生异常也可能不发生异常。在Spring数据库事务的流程中,他会根据是否发生异常采用不同的策略。
如果没有发生异常,Spring数据库拦截器就会帮我们提交事务,这点也并不需要我们干预。如果发生异常就要判断一次事务定义器内的配置,如果定义器已经约定该类型的异常不回滚事务就去提交事务,如果没有任何配置或者不是配置不回滚事务的异常,则会回滚事务,并将异常抛出,这一步也是有事务拦截器完成的。

无论发生异常与否,Spring都会释放事务资源,这样就可以保证数据库连接池的正常使用,这也是有Spring事务拦截器完成的内容。

在上述场景中,我们还有一个重要事务配置属性没有讨论,那就是传播行为。它属于事务方法之间调用的行为,后面我们会对其做更加详细的讨论。但是无论如何,从流程中我们可以看出开发者在整个流程中只需要完成业务逻辑就可以,其他的使用Spring事务机制和其配置就可以,这样就可以把try…catch–finish,数据库链接管理和事务提交回滚的代码交由Spring拦截器完成,而只是需要完成业务代码即可,所以你可以看到如下的代码

    package cn.hctech2006.boot.bootmybatis.service.impl;
    
    import cn.hctech2006.boot.bootmybatis.bean.SysRole;
    import cn.hctech2006.boot.bootmybatis.mapper.SysRoleMapper;
    import cn.hctech2006.boot.bootmybatis.service.SysRoleService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.util.List;
    
    /**
     * 服务实现类
     */
    @Service
    public class SysRoleServiceImpl implements SysRoleService {
        @Autowired
        private SysRoleMapper sysRoleMapper=null;
        @Override
        @Transactional
        public SysRole findById(Long id) {
            return sysRoleMapper.selectByPrimaryKey(id);
        }

这里仅仅是使用一个@Transactional来配置,大幅减少代码,同时代码具陪更高的可读性和可维护性。

2.2 @Transactional的配置项

数据库事务属性都可以由@Teansactional来配置,先来探讨他的源码

    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by Fernflower decompiler)
    //
    
    package org.springframework.transaction.annotation;
    
    
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface Transactional {
    //通过bean Name指定事务管理器
        @AliasFor("transactionManager")
        String value() default "";
    //同value属性
        @AliasFor("value")
        String transactionManager() default "";
    //指定传播行为
        Propagation propagation() default Propagation.REQUIRED;
    //指定隔离级别
        Isolation isolation() default Isolation.DEFAULT;
    //指定超时时间(单位秒)
        int timeout() default -1;
    //是否只读事务
        boolean readOnly() default false;
    //方法在发生指定异常时回滚,默认是所有异常都回滚
        Class<? extends Throwable>[] rollbackFor() default {};
    //方法在发生指定异常名称时回滚,默认是所有异常回滚
        String[] rollbackForClassName() default {};
    //方法在发生指定异常时不会滚,默认是所有异常不回滚
        Class<? extends Throwable>[] noRollbackFor() default {};
    //方法在发生指定异常时不回滚,默认是所有异常不回滚
        String[] noRollbackForClassName() default {};
    }

value和transactionManager属性是配置一个Spring的事务管理器,关于他后面会进行详细讨论;timeout是事务允许存在的时间戳,单位是秒;readOnly属性定义的是事务是否是只读事务;rollbackFor, rollBackForClassName, noRollbackFor, noRollbackForClassName都是指定异常,我们在流程中可以看到在使用带有事务的方法时,可能发生异常,通过这些属性的设置可以指定在什么异常的情况下仍然执行事务,在什么异常的情况下回滚事务,这些可以根据自己的需要进行指定。

以上都比较好理解,真正麻烦的是propagation和isolation这两个属性。propagation指的是传播行为,isolation则是隔离级别,他需要了解数据库的特性才能够使用,而这两个麻烦的东西就是本章的核心内容,也是互联网企业关心的内容之一,因此才值得我们后面话费较大篇幅去讲解他们的内容和使用方法。由于这里使用到了事务管理器,所以我们接下来讨论一下Spring的事务管理器。
关于注解@TRansactional值得注意的是它可以放在接口上也可以放在实现类上。但是Spring团队推荐放在实现类上,因为放在接口上将使得你的类基于接口的代理时他才生效。通过AOP学习,我们知道在SPring可以使用JDK动态代理,也可以使用CGLIG动态代理。如果使用接口,那么你将不能切换为CGLIG动态代理,而只是允许你使用JDK动态代理,并且使用对应的接口去代理你的类,这样才能驱动这个注解,这会大大限制你的使用。因此实现类上使用@TRansactional注解才是最佳的范式,我们也采用这种方式

2.3 Spring事务管理器

上面的事务流程中,事务的打开,回滚,提交都是有事务管理器来完成的。在Spring中,事务管理器的顶层接口为PlatformTransactionManager,Spring还为此定义了一些列的接口和类,如图

202209132232442983.png

当我们引入其他框架时,还会有其他的事务管理的类.因为本书会议MyBatis框架去讨论Spring数据库事务方面的问题,最常用到的事务管理器是DataSourceTransactionManager.从途中可以看出,他也是一个实现了接口PlatformTransactionManager的类,为此可以看到PlatformTransactionManager接口的源码

    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by Fernflower decompiler)
    //
    
    package org.springframework.transaction;
    
    import org.springframework.lang.Nullable;
    
    public interface PlatformTransactionManager extends TransactionManager {
    //获取事务,它还会设置数据属性
        TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
    //提交事务
        void commit(TransactionStatus var1) throws TransactionException;
    //回滚事务
        void rollback(TransactionStatus var1) throws TransactionException;
    }

显然这些方法并不难理解,只需要简单的介绍一下就可以了。Spring事务管理,就是将这些方法按照约定织入对应的流程中的,其中getTransaction方法的参数是一个事务定义器(TransactionDefinition),他是依赖于我们配置的@Transactional的配置项生成的,于是通过它就能够设置事务的属性了,而提交和回滚事务也是通过commit和rollback方法来执行的。
在Spring Boot中,当你依赖与mybatis-spring-boot-starter之后,他会自动创建一个DataSourceTransactionManager对象,作为事务管理器,所以我们一般不需要自己创建事务管理器。

2.4 测试数据库事务

依旧采用之前的SysRole,SysRoleMapper

角色服务类接口以及实现类

    package cn.hctech2006.boot.bootmybatis.service.impl;
    
    import cn.hctech2006.boot.bootmybatis.bean.SysRole;
    import cn.hctech2006.boot.bootmybatis.mapper.SysRoleMapper;
    import cn.hctech2006.boot.bootmybatis.service.SysRoleService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.PlatformTransactionManager;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.util.List;
    
    /**
     * 服务实现类
     */
    @Service
    public class SysRoleServiceImpl implements SysRoleService {
        @Autowired
        private SysRoleMapper sysRoleMapper=null;
    
        @Override
        @Transactional(isolation = Isolation.READ_COMMITTED, timeout = 1)
        public int save(SysRole record) {
            return sysRoleMapper.insert(record);
        }
    
        @Override
        @Transactional(isolation = Isolation.READ_COMMITTED, timeout = 1)
        public SysRole findById(Long id) {
            return sysRoleMapper.selectByPrimaryKey(id);
        }

代码中的方法标注了注解@Transactional,意味着这两个方法将启用Spring数据库事务机制。在事务配置中,采用了读写提交的隔离级别,后面我们将讨论隔离级别的含义,这里的代码还会限制超时时间为1s。然后可以写一个控制器,用来测试事务的启动情况。

    package cn.hctech2006.boot.bootmybatis.controller;
    
    import cn.hctech2006.boot.bootmybatis.bean.SysRole;
    import cn.hctech2006.boot.bootmybatis.service.SysRoleService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 控制器测试MyBatis接口
     */
    @RestController
    @RequestMapping("/mybatis")
    public class RoleController {
        @Autowired
        private SysRoleService sysRoleService;
        @RequestMapping("/getRole")
        @ResponseBody
        public SysRole getRole(Long id){
            return sysRoleService.findById(id);
        }
    
        /**
         * 测试插入用户
         * @return
         */
        @RequestMapping("insertRole")
        public Map<String, Object> insertRole(){
            SysRole sysRole = new SysRole();
            sysRole.setCreateBy("lidengyin");
            sysRole.setCreateTime(new Date());
            sysRole.setDelFlag((byte)0);
            sysRole.setName("asa33");
            sysRole.setRemark("ssss");
            sysRole.setLastUpdateBy("lidengyin");
            sysRole.setLastUpdateTime(new Date());
            int update = sysRoleService.save(sysRole);
            Map<String, Object> result = new HashMap<>();
            result.put("success", update==1);
            result.put("role", sysRole);
            return result;
        }
    }

有了控制器,我们还需要给Spring Boot配置MyBatis框架内容

    #MyBatis常用的配置
    mybatis:
      #MyBatis映射文件通配

这样MyBatis框架就配置完成了。依赖于mybatis-spring-boot-starter之后,Spring Boot就会自动创建事务管理器,MyBatis的sqlSessionFactory和SqlSessionTemplate等内容。下面我们需要配置SpringBoot的运行文件以达到配置的目的,并且查看SpringBoot为我们创建的事务管理器,SqlSessionFactory和SqlSessionTemplate信息。启动文件如代码所示

    package cn.hctech2006.boot.bootmybatis;
    
    @SpringBootApplication
    //定义MyBatis扫描路径
    @MapperScan(
            //指定扫描包
            basePackages = "cn.hctech2006.boot.bootmybatis.mapper",
            //指定SqlSessionFactory
            sqlSessionFactoryRef = "sqlSessionFactory",
            //指定sqlSessionTemplte,将会忽略sqlSessionFactory的配置
            sqlSessionTemplateRef = "sqlSessionTemplate",
            //markerInterface=Class.class限制扫描接口不常用
            //markerInterface =
            //注解限制
            annotationClass = Repository.class
    )
    public class BootMybatisApplication {
        //注入事务管理器,他又Spring Boot自动生成
        @Autowired
        PlatformTransactionManager platformTransactionManager = null;
        //使用后初始化方法,观察自动生成的事务管理器
        @PostConstruct
        public void viewTransactionManager(){
            //启动前加入断点检测
            System.out.println(platformTransactionManager.getClass().getName());
        }
        public static void main(String[] args) {
            SpringApplication.run(BootMybatisApplication.class, args);
        }
    
    }

首先这里使用了@MapperScan扫描对应的包,并且限定了只有被注解@Repository标注的接口,这样就可以把MyBatis对应的接口文件扫描到Spring IoC容器中。这样通过注解@Autowired直接注入事务管理器,他是通过Spring Boot的机制自动生成的,并不需要我们去关心。而在iewMyBatis中,因为先前已经把IoC容器注解进来了,所以通过IoC容器获取对应的Bean以监控他们,并且在加粗的地方加上断点,这样我们就可以以DEBUG的范式启动他并且进入断点了。如下是我们启动时监控得到的内容。

202209132232461954.png

从途中可以看出SpringBoot已经生成了事务管理器,这便是Spring Boot的魅力,允许我们以最小的配置代价运行Spring 的项目。那么按照之前的约定使用注解@Transactionassl标注类和方法之后,Spring的事务拦截器就会同时使用事务管理器的方法开启事务,然后将代码织入Spring数据库事务的流程中,如果发成异常,就回滚事务,如果不发生异常,那就会提交事务,这样我们从大量的冗余代码中解放了出来,所以我们可以看到在服务类中的代码是比较简介的。下面我们打开浏览器,在地址栏输入就能看到日志打印出来

202209132232474895.png

从日志中我们可以看出Spring获取了数据库连接

    JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@9c2f3c0]

并且修改了隔离级别(有两次修改,但是Spring接管之后的修改是sql执行之后的一次,但是现在是执行之前的):

    Changing isolation level of JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@9c2f3c0]

然后执行SQL

    ==>  Preparing: select id, name, remark, create_by, create_time, last_update_time, last_update_by, del_flag from sys_role where id = ? 
    2020-04-14 15:33:40.405 DEBUG 17503 --- [nio-8242-exec-1] c.h.b.b.m.S.selectByPrimaryKey           : ==> Parameters: 8(Long)
    2020-04-14 15:33:40.429 DEBUG 17503 --- [nio-8242-exec-1] c.h.b.b.m.S.selectByPrimaryKey           : <==

在最后会自动关闭和提交数据库事务

202209132232486946.png

可以看出是先释放sqlSession再释放JDBC。

因为我们对方法标注了@Transactional,所以Spring会把对应的代码织入约定的事务流程。

这个最基本的知识就算更新完了,接下来换一篇更新隔离级别和传播行为的基础知识