zoukankan      html  css  js  c++  java
  • Spring事务解决什么问题?如何解决?存在什么问题?

    声明:本文参考自微信公众号:草捏子

    1.基本的jdbc事务管理代码:

    // 开启数据库连接
    Connection con = openConnection();
    try {
        // 关闭自动提交
        con.setAutoCommit(false);
        // 业务处理
        // ...  
        // 提交事务
        con.commit();
    } catch (SQLException | MyException e) {
        // 捕获异常,回滚事务
        try {
            con.rollback();
        } catch (SQLException ex) {
            ex.printStackTrace();
        }
    } finally {
        // 关闭连接
        try {
            con.setAutoCommit(true);
            con.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

      直接使用JDBC进行事务管理的代码直观上来看,存在两个问题:

        1.业务处理代码与事务管理代码混杂;

        2.大量的异常处理代码(在catch中还要try-catch)。

        3.不同的访问技术,需要使用不同的API,所以导致繁杂的事务管理API。

    2.繁杂事务管理的API:

      针对该问题,我们很容易可以想到,在众多事务管理的API上抽象一层。通过定义接口屏蔽具体实现,再使用策略模式来决定具体的API。下面我们看下Spring事务中定义的抽象接口。

      在Spring事务中,核心接口是PlatformTransactionManager,也叫事务管理器,其定义如下:

    public interface PlatformTransactionManager extends TransactionManager {
        // 获取事务(新的事务或者已经存在的事务)
        TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
       throws TransactionException;   
        // 提交事务
        void commit(TransactionStatus status) throws TransactionException;
        // 回滚事务
        void rollback(TransactionStatus status) throws TransactionException;
    }

      getTransaction通过入参TransactionDefinition来获得TransactionStatus,即通过定义的事务元信息来创建相应的事务对象。在TransactionDefinition中会包含事务的元信息

      PropagationBehavior:传播行为;

      IsolationLevel:隔离级别;

      Timeout:超时时间;

      ReadOnly:是否只读。

      根据TransactionDefinition获得的TransactionStatus中会封装事务对象,并提供了操作事务查看事务状态的方法,例如:

        setRollbackOnly:标记事务为Rollback-only,以使其回滚;

        isRollbackOnly:查看是否被标记为Rollback-only;

        isCompleted:查看事务是否已完成(提交或回滚完成)。

      还支持嵌套事务的相关方法:

        createSavepoint:创建savepoint;

        rollbackToSavepoint:回滚到指定savepoint;

        releaseSavePoint:释放savepoint。

      TransactionStatus事务对象可被传入到commit方法或rollback方法中,完成事务的提交或回滚。

      下面我们通过一个具体实现来理解TransactionStatus的作用。以commit方法为例,如何通过TransactionStatus完成事务的提交。AbstractPlatformTransactionManagerPlatformTransactionManager接口的的实现,作为模板类,其commit实现如下:

    public final void commit(TransactionStatus status) throws TransactionException {
        // 1.检查事务是否已完成
        if (status.isCompleted()) {
            throw new IllegalTransactionStateException(
                "Transaction is already completed - do not call commit or rollback more than once per transaction");
        }
    
        // 2.检查事务是否需要回滚(局部事务回滚)
        DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
        if (defStatus.isLocalRollbackOnly()) {
            if (defStatus.isDebug()) {
                logger.debug("Transactional code has requested rollback");
            }
            processRollback(defStatus, false);
            return;
        }
    
        // 3.检查事务是否需要回滚(全局事务回滚)
        if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
            if (defStatus.isDebug()) {
                logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
            }
            processRollback(defStatus, true);
            return;
        }
        
        // 4.提交事务
        processCommit(defStatus);
    }

      commit模板方法中定义了事务提交的基本逻辑,通过查看status的事务状态来决定抛出异常还是回滚,或是提交。其中的processRollbackprocessCommit方法也是模板方法,进一步定义了回滚、提交的逻辑。以processCommit方法为例,具体的提交操作将由抽象方法doCommit完成。

    protected abstract void doCommit(DefaultTransactionStatus status) throws TransactionException;

      doCommit的实现取决于具体的数据访问技术。我们看下JDBC相应的具体实现类DataSourceTransactionManager中的doCommit实现。

    protected void doCommit(DefaultTransactionStatus status) {
        // 获取status中的事务对象    
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
        // 通过事务对象获得数据库连接对象
        Connection con = txObject.getConnectionHolder().getConnection();
        if (status.isDebug()) {
            logger.debug("Committing JDBC transaction on Connection [" + con + "]");
        }
        try {
            // 执行commit
            con.commit();
        }
        catch (SQLException ex) {
            throw new TransactionSystemException("Could not commit JDBC transaction", ex);
        }
    }

      commitprocessCommit方法中我们根据入参的TransactionStatus提供的事务状态来决定事务行为,而在doCommit中需要执行事务提交时将会通过TransactionStatus中的事务对象来获得数据库连接对象,再执行最后的commit操作。通过这个示例我们可以理解TransactionStatus所提供的事务状态和事务对象的作用。

      下面是用Spring事务API改写后的事务管理代码:  

    // 获得事务管理器
    PlatformTransactionManager txManager = getPlatformTransactionManager();
    DefaultTransactionDefinition def = new DefaultTransactionDefinition();
    // 指定事务元信息
    def.setName("SomeTxName");
    def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    // 获得事务
    TransactionStatus status = txManager.getTransaction(def);
    try {
        // 业务处理
    }
    catch (MyException ex) {
        // 捕获异常,回滚事务
        txManager.rollback(status);
        throw ex;
    }
    // 提交事务
    txManager.commit(status);

      无论是使用JDBC、Hibernate还是MyBatis,我们只需要传给txManager相应的具体实现就可以在多种数据访问技术中切换。

      小结:Spring事务通过PlatformTransactionManagerTransactionDefinitionTransactionStatus接口统一事务管理API,并结合策略模式和模板方法决定具体实现。

    3.Spring事务是如何解决大量的异常处理代码?

      为什么使用JDBC的代码中会需要写这么多的异常处理代码。这是因为Connection的每个方法都会抛出SQLException,而SQLException又是检查异常,这就强制我们在使用其方法时必须进行异常处理。那Spring事务是如何解决该问题的。我们看下doCommit方法:

    protected void doCommit(DefaultTransactionStatus status) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
        Connection con = txObject.getConnectionHolder().getConnection();
        if (status.isDebug()) {
            logger.debug("Committing JDBC transaction on Connection [" + con + "]");
        }
        try {
            con.commit();
        }
        catch (SQLException ex) {
            // 异常转换
            throw new TransactionSystemException("Could not commit JDBC transaction", ex);
        }
    }

      Connectioncommit方法会抛出检查异常SQLException,在catch代码块中SQLException将被转换成TransactionSystemException抛出,而TransactionSystemException是一个非检查异常。通过将检查异常转换成非检查异常,让我们能够自行决定是否捕获异常,不强制进行异常处理。

      Spring事务中几乎为数据库的所有错误都定义了相应的异常,统一了JDBC、Hibernate、MyBatis等不同异常API。这有助于我们在处理异常时使用统一的异常API接口,无需关心具体的数据访问技术。

      小结:Spring事务通过异常转换避免强制异常处理。

    4.业务处理代码与事务管理代码混杂

      在2.1节中给出了使用Spring事务API的写法,即编程式事务管理,但仍未解决“业务处理代码与事务管理代码混杂”的问题。这时候就可以利用Spring AOP将事务管理代码这一横切关注点从代码中剥离出来,即声明式事务管理。以注解方式为例,通过为方法标注@Transaction注解,将为该方法提供事务管理。其原理如下图所示:

      

       Spring事务会为@Transaction标注的方法的类生成AOP增强的动态代理类对象,并且在调用目标方法的拦截链中加入TransactionInterceptor进行环绕增加,实现事务管理。

      下面我们看下TransactionInterceptor中的具体实现,其invoke方法中将调用invokeWithinTransaction方法进行事务管理,如下所示:

    protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
            throws Throwable {
    
        // 查询目标方法事务属性、确定事务管理器、构造连接点标识(用于确认事务名称)
        final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
        final PlatformTransactionManager tm = determineTransactionManager(txAttr);
        final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
    
        if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
            // 创建事务
            TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
            Object retVal = null;
            try {
                // 通过回调执行目标方法
                retVal = invocation.proceedWithInvocation();
            }
            catch (Throwable ex) {
                // 目标方法执行抛出异常,根据异常类型执行事务提交或者回滚操作
                completeTransactionAfterThrowing(txInfo, ex);
                throw ex;
            }
            finally {
                // 清理当前线程事务信息
                cleanupTransactionInfo(txInfo);
            }
            // 目标方法执行成功,提交事务
            commitTransactionAfterReturning(txInfo);
            return retVal;
        } else {
            // 带回调的事务执行处理,一般用于编程式事务
            // ...
        }
    }

      在调用目标方法前后加入了创建事务、处理异常、提交事务等操作。这让我们不必编写事务管理代码,只需通过@Transaction的属性指定事务相关元信息。

      小结:Spring事务通过AOP提供声明式事务将业务处理代码和事务管理代码分离。

    5.Spring事务存在的问题:

      Spring事务为了我们解决了第一节中列出的三个问题,但同时也会带来些新的问题。

      5.1:非public方法失效

        @Transactional只有标注在public级别的方法上才能生效,对于非public方法将不会生效。这是由于Spring AOP不支持对private、protect方法进行拦截。从原理上来说,动态代理是通过接口实现,所以自然不能支持private和protect方法的。而CGLIB是通过继承实现,其实是可以支持protect方法的拦截的,但Spring AOP中并不支持这样使用,笔者猜测做此限制是出于代理方法应是public的考虑,以及为了保持CGLIB和动态代理的一致。如果需要对protect或private方法拦截则建议使用AspectJ。

      5.2:自调用失效

        当通过在Bean的内部方法直接调用带有@Transactional的方法时,@Transactional将失效,例如:

    public void saveAB(A a, B b)
    {
        saveA(a);
        saveB(b);
    }
    
    @Transactional
    public void saveA(A a)
    {
        dao.saveA(a);
    }
    
    @Transactional
    public void saveB(B b)
    {
        dao.saveB(b);
    }

        在saveAB中调用saveA和saveB方法,两者的@Transactional都将失效。这是因为Spring事务的实现基于代理类,当在内部直接调用方法时,将不会经过代理对象,而是直接调用目标对象的方法,无法被TransactionInterceptor拦截处理。解决办法:

        1.通过ApplicationContextAware注入的上下文获得代理对象。

    public void saveAB(A a, B b)
    {
        Test self = (Test) applicationContext.getBean("Test");
        self.saveA(a);
        self.saveB(b);
    }

        2.通过AopContext获得代理对象。

    public void saveAB(A a, B b)
    {
        Test self = (Test)AopContext.currentProxy();
        self.saveA(a);
        self.saveB(b);
    }

        3.通过@Autowired注解注入代理对象。

    @Component
    public class Test {
    
        @Autowired
        Test self;
    
        public void saveAB(A a, B b)
        {
            self.saveA(a);
            self.saveB(b);  
        }
        // ...
    }

        4.将saveA、saveB方法拆分到另一个类中。

    public void saveAB(A a, B b)
    {
        txOperate.saveA(a);
        txOperate.saveB(b);
    }

      5.3:检查异常默认不回滚

      在默认情况下,抛出非检查异常会触发回滚,而检查异常不会。

      根据invokeWithinTransaction方法,我们可以知道异常处理逻辑在completeTransactionAfterThrowing方法中,其实现如下:

    protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
        if (txInfo != null && txInfo.getTransactionStatus() != null) {
            if (logger.isTraceEnabled()) {
                logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
                             "] after exception: " + ex);
            }
            if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
                try {
                    // 异常类型为回滚异常,执行事务回滚
                    txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
                }
                catch (TransactionSystemException ex2) {
                    logger.error("Application exception overridden by rollback exception", ex);
                    ex2.initApplicationException(ex);
                    throw ex2;
                }
                catch (RuntimeException | Error ex2) {
                    logger.error("Application exception overridden by rollback exception", ex);
                    throw ex2;
                }
            }
            else {
                try {
                    // 异常类型为非回滚异常,仍然执行事务提交
                    txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
                }
                catch (TransactionSystemException ex2) {
                    logger.error("Application exception overridden by commit exception", ex);
                    ex2.initApplicationException(ex);
                    throw ex2;
                }
                catch (RuntimeException | Error ex2) {
                    logger.error("Application exception overridden by commit exception", ex);
                    throw ex2;
                }
            }
        }
    }

      根据rollbackOn判断异常是否为回滚异常。只有RuntimeExceptionError的实例,即非检查异常,或者在@Transaction通过rollbackFor属性指定的回滚异常类型,才会回滚事务。否则将继续提交事务。所以如果需要对非检查异常进行回滚,需要记得指定rollbackFor属性,不然将回滚失效。

      5.4:catch异常无法回滚

      在3.3节中我们说到只有抛出非检查异常或是rollbackFor中指定的异常才能触发回滚。如果我们把异常catch住,而且没抛出,则会导致无法触发回滚,这也是开发中常犯的错误。例如:

    @Transactional
    public void insert(List<User> users) {
        try {
            JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
            for (User user : users) {
                String insertUserSql = "insert into User (id, name) values (?,?)";
                jdbcTemplate.update(insertUserSql, new Object[] { user.getId(),
                                                                 user.getName() });
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

      这里由于catch住了所有Exception,并且没抛出。当插入发生异常时,将不会触发回滚。

      但同时我们也可以利用这种机制,用try-catch包裹不用参与事务的数据操作,例如对于写入一些不重要的日志,我们可将其用try-catch包裹,避免抛出异常,则能避免写日志失败而影响事务的提交。

  • 相关阅读:
    问题描述:判断一个整数 n 是否为 2 的幂次方
    C#的关键字Explicit 和 Implicit
    .NET写入文件操作
    C# Main函数详解
    SpringBoot增加过滤XSS脚本攻击
    Hutool工具包导出Excel文件异常 You need to add dependency of poi-ooxml to your project
    微信H5表单点击输入框提示防欺诈盗号,请勿支付或输入qq密码
    RedisTemplate执行lua脚本在Redis集群模式下报错EvalSha is not supported in cluster environment.
    SpringBoot使用RedisTemplate+Lua脚本实现Redis分布式锁
    SpringBoot使用Thymeleaf打成jar包部署找不到页面
  • 原文地址:https://www.cnblogs.com/wk-missQ1/p/13502265.html
Copyright © 2011-2022 走看看