• Spring Boot中的事务是如何实现的


    本文首发于微信公众号【猿灯塔】,转载引用请说明出处

    今天呢!灯塔君跟大家讲:

    Spring Boot中的事务是如何实现的

    1. 概述

    一直在用SpringBoot中的@Transactional来做事务管理,但是很少没想过SpringBoot是如何实现事务管理的,今天从源码入手,看看@Transactional是如何实现事务的,最后我们结合源码的理解,自己动手写一个类似的注解来实现事务管理,帮助我们加深理解

    2. 事务的相关知识

    开始看源码之前,我们先回顾下事务的相关知识。

    开始看源码之前,我们先回顾下事务的相关知识。

    2.1 事务的隔离级别

    事务为什么需要隔离级别呢?这是因为在并发事务情况下,如果没有隔离级别会导致如下问题:

    脏读(Dirty Read) :当A事务对数据进行修改,但是这种修改还没有提交到数据库中,B事务同时在访问这个数据,由于没有隔离,B获取的数据有可能被A事务回滚,这就导致了数据不一致的问题。

    丢失修改(Lost To Modify): 当A事务访问数据100,并且修改为100-1=99,同时B事务读取数据也是100,修改数据100-1=99,最终两个事务的修改结果为99,但是实际是98。事务A修改的数据被丢失了。

    不可重复读(Unrepeatable Read):指A事务在读取数据X=100的时候,B事务把数据X=100修改为X=200,这个时候A事务第二次读取数据X的时候,发现X=200了,导致了在整个A事务期间,两次读取数据X不一致了,这就是不可重复读。

    幻读(Phantom Read):幻读和不可重复读类似。幻读表现在,当A事务读取表数据时候,只有3条数据,这个时候B事务插入了2条数据,当A事务再次读取的时候,发现有5条记录了,平白无故多了2条记录,就像幻觉一样。

    不可重复读 VS 幻读不可重复读的重点是修改 : 同样的条件 , 你读取过的数据 , 再次读取出来发现值不一样了,重点在更新操作。 幻读的重点在于新增或者删除:同样的条件 , 第 1 次和第 2 次读出来的记录数不一样,重点在增删操作。

    所以,为了避免上述的问题,事务中就有了隔离级别的概念

    在Spring中定义了五种表示隔离级别的常量:

    常量

    说明

    TransactionDefinition.ISOLATION_DEFAULT

    数据库默认的隔离级别,MySQL默认采用的 REPEATABLE_READ隔离级别

    TransactionDefinition.ISOLATION_READ_UNCOMMITTED

    最低的隔离级别,允许读取未提交的数据变更,可能会导致脏读、幻读或不可重复读

    TransactionDefinition.ISOLATION_READ_COMMITTED

    允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生

    TransactionDefinition.ISOLATION_REPEATABLE_READ

    对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,**可以阻止脏读和不可重复读,但幻读仍有可能发生。**MySQL中通过MVCC解决了该隔离级别下出现幻读的可能。

    TransactionDefinition.ISOLATION_SERIALIZABLE

    串行化隔离级别,该级别可以防止脏读、不可重复读以及幻读,但是串行化会影响性能。

    2.2 Spring中事务的传播机制

    为什么Spring中要搞一套事务的传播机制呢?这是Spring给我们提供的事务增强工具,主要是解决方法之间调用,事务如何处理的问题。比如有方法A、方法B和方法C,在A中调用了方法B和方法C。伪代码如下:

    MethodA{

    MethodB;

    MethodC;

    }

    MethodB{

    }

    MethodC{

    }

    假设三个方法中都开启了自己的事务,那么他们之间是什么关系呢?MethodA的回滚会影响MethodB和MethodC吗?Spring中的事务传播机制就是解决这个问题的。

    Spring中定义了七种事务传播行为:

    类型

    说明

    PROPAGATION_REQUIRED

    如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择

    PROPAGATION_SUPPORTS

    支持当前事务,如果当前没有事务,就以非事务方式执行。

    PROPAGATION_MANDATORY

    使用当前的事务,如果当前没有事务,就抛出异常。

    PROPAGATION_REQUIRES_NEW

    新建事务,如果当前存在事务,把当前事务挂起。

    PROPAGATION_NOT_SUPPORTED

    以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

    PROPAGATION_NEVER

    以非事务方式执行,如果当前存在事务,则抛出异常。

    PROPAGATION_NESTED

    如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

    这七种传播机制是如何影响事务的,感兴趣的同学可以阅读这篇文章

    3. 如何实现异常回滚的

    回顾完了事务的相关知识,接下来我们正式来研究下Spring Boot中如何通过@Transactional来管理事务的,我们重点看看它是如何实现回滚的。

    在Spring中TransactionInterceptor和PlatformTransactionManager这两个类是整个事务模块的核心,TransactionInterceptor负责拦截方法执行,进行判断是否需要提交或者回滚事务。PlatformTransactionManager是Spring 中的事务管理接口,真正定义了事务如何回滚和提交。我们重点研究下这两个类的源码。

    TransactionInterceptor类中的代码有很多,我简化一下逻辑,方便说明:

    //以下代码省略部分内容

    public Object invoke(MethodInvocation invocation) throws Throwable {

    //获取事务调用的目标方法

    Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

    //执行带事务调用

    return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);

    }

    invokeWithinTransaction 简化逻辑如下:

    //TransactionAspectSupport.class

    //省略了部分代码

    protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,

    final InvocationCallback invocation) throws Throwable {

    Object retVal;

    try {

    //调用真正的方法体

    retVal = invocation.proceedWithInvocation();

    }

    catch (Throwable ex) {

    // 如果出现异常,执行事务异常处理

    completeTransactionAfterThrowing(txInfo, ex);

    throw ex;

    }

    finally {

    //最后做一下清理工作,主要是缓存和状态等

    cleanupTransactionInfo(txInfo);

    }

    //如果没有异常,直接提交事务。

    commitTransactionAfterReturning(txInfo);

    return retVal;

    }

    事务出现异常回滚的逻辑completeTransactionAfterThrowing如下:

    //省略部分代码

    protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {

    //判断是否需要回滚,判断的逻辑就是看有没有声明事务属性,同时判断是不是在目前的这个异常中执行回滚。

    if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {

    //执行回滚

    txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());

    }

    else {

    //否则不需要回滚,直接提交即可。

    txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());

    }

    }

    }

    上面的代码已经把Spring的事务的基本原理说清楚了,如何进行判断执行事务,如何回滚。下面到了真正执行回滚逻辑的代码中PlatformTransactionManager接口的子类,我们以JDBC的事务为例,DataSourceTransactionManager就是jdbc的事务管理类。跟踪上面的代码rollback(txInfo.getTransactionStatus())可以发现最终执行的代码如下:

    @Override

    protected void doRollback(DefaultTransactionStatus status) {

    DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();

    Connection con = txObject.getConnectionHolder().getConnection();

    if (status.isDebug()) {

    logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");

    }

    try {

    //调用jdbc的 rollback进行回滚事务。

    con.rollback();

    }

    catch (SQLException ex) {

    throw new TransactionSystemException("Could not roll back JDBC transaction", ex);

    }

    }

    3.1 小结

    这里小结下Spring 中事务的实现思路,Spring 主要依靠 TransactionInterceptor 来拦截执行方法体,判断是否开启事务,然后执行事务方法体,方法体中catch住异常,接着判断是否需要回滚,如果需要回滚就委托真正的TransactionManager 比如JDBC中的DataSourceTransactionManager来执行回滚逻辑。提交事务也是同样的道理。

    4. 手写一个注解实现事务回滚

    我们弄清楚了Spring的事务执行流程,那我们可以模仿着自己写一个注解,实现遇到指定异常就回滚的功能。这里持久层就以最简单的JDBC为例。我们先梳理下需求,首先注解我们可以基于Spring 的AOP来实现,接着既然是JDBC,那么我们需要一个类来帮我们管理连接,用来判断异常是否回滚或者提交。梳理完就开干吧。

    4.1 首先加入依赖

     <dependency>

                <groupId>org.springframework.boot</groupId>

                <artifactId>spring-boot-starter-jdbc</artifactId>

            </dependency>

            <dependency>

                <groupId>org.springframework.boot</groupId>

                <artifactId>spring-boot-starter-aop</artifactId>

            </dependency>

    4.2 新增一个注解

    /**

     * @description:

     * @author: luozhou

     * @create: 2020-03-29 17:05

     **/@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Inherited@Documentedpublic @interface MyTransaction {

        //指定异常回滚

        Class<? extends Throwable>[] rollbackFor() default {};

    }

    4.3 新增连接管理器

    该类帮助我们管理连接,该类的核心功能是把取出的连接对象绑定到线程上,方便在AOP处理中取出,进行提交或者回滚操作。

    /**

     * @description:

     * @author: luozhou

     * @create: 2020-03-29 21:14

     **/@Componentpublic class DataSourceConnectHolder {

        @Autowired

        DataSource dataSource;

        /**

         * 线程绑定对象

         */

        ThreadLocal<Connection> resources = new NamedThreadLocal<>("Transactional resources");

        public Connection getConnection() {

            Connection con = resources.get();

            if (con != null) {

                return con;

            }

            try {

                con = dataSource.getConnection();

                //为了体现事务,全部设置为手动提交事务

                con.setAutoCommit(false);

            } catch (SQLException e) {

                e.printStackTrace();

            }

            resources.set(con);

            return con;

        }

        public void cleanHolder() {

            Connection con = resources.get();

            if (con != null) {

                try {

                    con.close();

                } catch (SQLException e) {

                    e.printStackTrace();

                }

            }

            resources.remove();

        }

    }

    4.4 新增一个切面

    这部分是事务处理的核心,先获取注解上的异常类,然后捕获住执行的异常,判断异常是不是注解上的异常或者其子类,如果是就回滚,否则就提交。

    /**

     * @description:

     * @author: luozhou

     * @create: 2020-03-29 17:08

     **/@Aspect@Componentpublic class MyTransactionAopHandler {

        @Autowired

        DataSourceConnectHolder connectHolder;

        Class<? extends Throwable>[] es;

        //拦截所有MyTransaction注解的方法

        @org.aspectj.lang.annotation.Pointcut("@annotation(luozhou.top.annotion.MyTransaction)")

        public void Transaction() {

        }

        @Around("Transaction()")

        public Object TransactionProceed(ProceedingJoinPoint proceed) throws Throwable {

            Object result = null;

            Signature signature = proceed.getSignature();

            MethodSignature methodSignature = (MethodSignature) signature;

            Method method = methodSignature.getMethod();

            if (method == null) {

                return result;

            }

            MyTransaction transaction = method.getAnnotation(MyTransaction.class);

            if (transaction != null) {

                es = transaction.rollbackFor();

            }

            try {

                result = proceed.proceed();

            } catch (Throwable throwable) {

                //异常处理

                completeTransactionAfterThrowing(throwable);

                throw throwable;

            }

            //直接提交

            doCommit();

            return result;

        }

    /**

    * 执行回滚,最后关闭连接和清理线程绑定

    */

        private void doRollBack() {

            try {

                connectHolder.getConnection().rollback();

            } catch (SQLException e) {

                e.printStackTrace();

            } finally {

                connectHolder.cleanHolder();

            }

        }

    /**

    *执行提交,最后关闭连接和清理线程绑定

    */

        private void doCommit() {

            try {

                connectHolder.getConnection().commit();

            } catch (SQLException e) {

                e.printStackTrace();

            } finally {

                connectHolder.cleanHolder();

            }

        }

    /**

    *异常处理,捕获的异常是目标异常或者其子类,就进行回滚,否则就提交事务。

    */

        private void completeTransactionAfterThrowing(Throwable throwable) {

            if (es != null && es.length > 0) {

                for (Class<? extends Throwable> e : es) {

                    if (e.isAssignableFrom(throwable.getClass())) {

                        doRollBack();

                    }

                }

            }

            doCommit();

        }

    }

    4.5 测试验证

    创建一个tb_test表,表结构如下:

    SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;

    -- ------------------------------ Table structure for tb_test-- ----------------------------DROP TABLE IF EXISTS `tb_test`;CREATE TABLE `tb_test` (

      `id` int(11) NOT NULL,

      `email` varchar(255) DEFAULT NULL,

      PRIMARY KEY (`id`)

    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;

    SET FOREIGN_KEY_CHECKS = 1;

    4.5.1 编写一个Service

    saveTest方法调用了2个插入语句,同时声明了@MyTransaction事务注解,遇到NullPointerException就进行回滚,最后我们执行了除以0操作,会抛出ArithmeticException。我们用单元测试看看数据是否会回滚。

    /**

     * @description:

     * @author: luozhou kinglaw1204@gmail.com

     * @create: 2020-03-29 22:05

     **/@Servicepublic class MyTransactionTest implements TestService {

        @Autowired

        DataSourceConnectHolder holder;

    //一个事务中执行两个sql插入

       @MyTransaction(rollbackFor = NullPointerException.class)

        @Override

        public void saveTest(int id) {

            saveWitharamters(id, "luozhou@gmail.com");

            saveWitharamters(id + 10, "luozhou@gmail.com");

            int aa = id / 0;

        }

    //执行sql

       private void saveWitharamters(int id, String email) {

            String sql = "insert into tb_test values(?,?)";

            Connection connection = holder.getConnection();

            PreparedStatement stmt = null;

            try {

                stmt = connection.prepareStatement(sql);

                stmt.setInt(1, id);

                stmt.setString(2, email);

                stmt.executeUpdate();

            } catch (SQLException e) {

                e.printStackTrace();

            }

        }

        

    }

    4.5.2 单元测试

    @SpringBootTest@RunWith(SpringRunner.class)class SpringTransactionApplicationTests {

        @Autowired

        private TestService service;

        @Test

        void contextLoads() throws SQLException {

            service.saveTest(1);

        }

    }

    上图代码声明了事务对NullPointerException异常进行回滚,运行中遇到了ArithmeticException异常,所以是不会回滚的,我们在右边的数据库中刷新发现数据正常插入成功了,说明并没有回滚。

    我们把回滚的异常类改为ArithmeticException,把原数据清空再执行一次,出现了ArithmeticException异常,这个时候查看数据库是没有记录新增成功了,这说明事物进行回滚了,表明我们的注解起作用了。

    5. 总结

    本文最开始回顾了事务的相关知识,并发事务会导致脏读丢失修改不可重复读幻读,为了解决这些问题,数据库中就引入了事务的隔离级别,隔离级别包括:读未提交读提交可重复读串行化

    Spring中增强了事务的概念,为了解决方法A、方法B和方法C之间的事务关系,引入了事务传播机制的概念。

    Spring中的@Transactional注解的事务实现主要通过TransactionInterceptor拦截器来进行实现的,拦截目标方法,然后判断异常是不是目标异常,如果是目标异常就行进行回滚,否则就进行事务提交。

    最后我们自己通过JDBC结合Spring的AOP自己写了个@MyTransactional的注解,实现了遇到指定异常回滚的功能。

    365天干货不断,可以微信搜索「 猿灯塔」第一时间阅读,回复【资料】【面试】【简历】有我准备的一线大厂面试资料和简历模板

  • 相关阅读:
    算法-经典趣题-寻找假银币
    一天一个 Linux 命令(3):cat 命令
    算法-经典趣题-青蛙过河
    常用数据库有哪些?
    SpringBoot2.0入门教程(一) 快速入门,项目构建HelloWorld示例
    一天一个 Linux 命令(2):ls 命令
    算法-经典趣题-爱因斯坦阶梯问题
    一天一个 Linux 命令(1):vim 命令
    什么是开发环境、测试环境、UAT环境、仿真环境、生产环境?
    算法-经典趣题-渔夫捕鱼
  • 原文地址:https://www.cnblogs.com/yuandengta/p/12742567.html
Copyright © 2020-2023  润新知