Springboot处理事务

1、@Transactional前言

最近在操作springboot的时候,需要来进行操作数据库,需要来使用事务来进行管理。通过一个注解即可来进行搞定这里的问题。

@Transactional

通过一个注解就可以搞定数据库操作的事务问题。

在springboot继承mybatis和mybatis-plus的过程中,已经帮我们自动配置好了事务管理器,我们只需要在使用的时候加上@Transactional注解即可。但是需要注意的是在使用@Transactional注解的时候,小心事务失效的问题。

事务失效

在介绍下面的之前,还需要了解一下MySQL数据库中的隔离级别和事务。

一般来说有两种方式来开启事务:1、begin/commit/rollback;2、start transaction/commit/rollback;

在springboot中是否需要开启事务是通过注解@Transactional注解来决定的,开启了之后需要将自动提交来设置成为手动提交的方式。

这里是需要来进行提前说明的。

2、案例演示

那么这里首先通过一个案例来进行演示一下,参考链接

    @Transactional
    public void sellProduct() throws ClassNotFoundException {
        log.info("----------------->>>>>>>开启日志<<<<<------------------------");
        LOCK.lock();
        try {
            System.out.println(Thread.currentThread().getName() + ":抢到锁了,进入到方法中来");
            // 首先查询库存
            Product product = productMapper.selectById(1L);
            Integer productcount = product.getProductcount();
            System.out.println(Thread.currentThread().getName() + ":当前库存是:" + productcount);
            if (productcount > 0) {
                product.setProductcount(productcount - 1);
                // 更新操作
                productMapper.updateById(product);
                Buy buy = new Buy();
                buy.setProductname(product.getProductname());
                buy.setUsername(Thread.currentThread().getName());
                // 保存操作
                buyMapper.insert(buy);
                System.out.println(Thread.currentThread().getName() + ":减库存,创建订单完毕!");
            } else {
                System.out.println(Thread.currentThread().getName() + ":没有库存了");
            }
        } finally {
            System.out.println(Thread.currentThread().getName() + ":释放锁");
            // 释放锁
            LOCK.unlock();
        }
    }

2.1、问题

首先先准备两个问题:

1、事务的开始在哪里进行的?

2、事务的提交和回滚操作是在哪里操作的?

那么带着这两个问题来找答案。

2.2、 查询哪个事务正在执行SQL

首先准备一个SQL语句,如下所示:

select * from information_schema.innodb_trx;

这个语句可以显示当前数据库中有哪些事务正在执行SQL语句。

那么在

 Product product = productMapper.selectById(1L);

语句上打上断点,然后利用上面的SQL来进行查询,发现是没有的;当SQL语句执行的时候,再次来执行SQL语句,发现可以看到当前事务中正在执行SQL语句。

那么也就说明了事务的开始时间并非是在第一行就开始了。

这里说明一下:在innodb执行引擎下,事务的开启执行是在执行SQL的时机时才会触发,那么之前的都是准备工作。

根据上面说的,事务已经有了,那么需要将自动提交改为手动提交,这两个步骤是在哪里来进行操作的呢?

2.3、手动设置事务

那么这里的测试我使用的是单元测试

    @Test
    void contextLoads() throws ClassNotFoundException {
        productService.sellProduct();
    }

那么断点打在这里:

![](

)

首先可以看到的是这里显示的是一个动态代理对象,使用的是CGLIB来进行创建的。

那么继续跟进去,慢慢看这里的操作:

从这里可以看到将会走动态代理来调用方法获取得到结果

然后来调用目标方法:

从调用的注解上翻译:使用 getTransaction 和 commit/rollback 调用进行标准事务划分。

那么这里已经来到了操作的重点了。那么看看对应的操作

首先来对事务来进行检查,判断事务上的注解的合理性

返回返回一个事务操作,下面看看里面做了什么操作:

看一下方法上的注解,开启新的事务。继续向下进行:

那么看一下这里的操作,首先获取得到autocommit,来进行判断是否为true,也就是自动提交,如果是,那么在下面将connection中的自动提交设置成fasle。那么在哪里将connection的autocommit设置成true的?

其实看一下265行,获取得到连接的操作。

首先hireka数据库连接池先来进行初始化操作,那么初始化完成之后,肯定是要创建连接,然后获取得到对应的连接。

看一下这里的调用链路,一般人还真找不到。在初始化的时候来进行设置的,但是可以可以看到对autocommit设置的是true。

看一下方法调用栈的效果:

将autocommit设置的时候是在获取得到连接的地方。然后将其设置成false。

记录一下设置成true的接口位置:java.sql.Connection#setAutoCommit

2.4、提交事务和回滚事务

那么这里设置完成了之后,接下来就应该执行我们的目标方法了。执行完成之后决定是否是提交事务还是回滚事务。

还是之前的那个方法:将Standard transaction demarcation with getTransaction and commit/rollback calls.进行分离开来的方法

当这行代码执行完成之后,就来到了下面的使用重点方法了。

现在一旦看到这个retVal就觉得是返回值。那么看try代码中的注释:环绕通知,调用目标对象。那么也就是在这里来调用我们在service层中写的代码。看这里的

try{
    
}catch(){
    
}finnaly{
    
}

感觉自己又挣到了。哈哈哈

看一下这里的执行逻辑。如果目标方法执行正常,那么执行finnaly代码块中的代码。如果catch中的异常没有得到处理,那么在finnaly结束之后,依然会向外抛出异常。那么将会由外层的异常来进行处理。那么最终会抛到controller中去,然后抛出到exceptionhandler中去进行处理,然后将值进行返回。那么不管是做了什么。首先现将这里的事务的回滚和提交先继续看下去。

注意:finnal中的代码不是提交,而是将我们在connection上有着自定义化的操作的标记给擦除掉。也就是说先把连接上的东西给去掉,因为提交或者是回滚事务后还需要将数据库连接还回去。我也不知道为什么这里要提前到这里来进行操作,可能是为了防止后期需要做的事情有干扰,所以提前了。

然后再catch代码块中可以看到注释:目标方法出现异常!那么需要来进行执行的操作。

这里回想一下事务失效中的一条规则:当自己手动的进行try...catch,而又没有将异常抛出去的时候,其实被抓住的异常相对于调用者来说,是没有异常产生的。所以也会走finnaly中的代码,然后进行提交事务。所以源码中很清楚,但是源码中的这种思想要学会。

2.4.1、正常提交事务

那么首先看一下正常提交时候的吧,然后再看一下异常提交的时候。这里还是比较有意思的。

这里看一下commitTransactionAfterReturning方法,在正常处理完成之后提交事务。不要被这个方法的意思给误解了!

我以为的不是coding代码的人以为的提交,但是不是真的提交。

还需要经过什么?还需要经过两个判断,判断是否有rollbackonly!

这个异常就比较常见了!!出现的原因是两个@Transacitonal方法来进行调用,类似下面这种:

A(){
    B();
}

由于B方法在执行中出现了异常,那么导致A也将事务进行了回滚。所以这里也比较有意思了。那么可以将事务传播行为修改一番,将其变成即使B方法执行失败了,A方法如果执行成功,也是可以正常提交事务的。当然,这个会在处理完事务中的异常之后来举例子提及到。

那么正常提交的处理完成之后,看一下处理异常的案例。那么如果说异常的话,springboot中默认能够处理的异常是什么?

2.4.3、默认异常处理

那么先手动的去制造一个异常来看看:

@Transactional
public void sellProduct() throws ClassNotFoundException {
    log.info("----------------->>>>>>>开启日志<<<<<------------------------");
    LOCK.lock();
    try {
        System.out.println(Thread.currentThread().getName() + ":抢到锁了,进入到方法中来");
        // 首先查询库存
        Product product = productMapper.selectById(1L);
        int i = 1/0;
        Integer productcount = product.getProductcount();
        System.out.println(Thread.currentThread().getName() + ":当前库存是:" + productcount);
        if (productcount > 0) {
            product.setProductcount(productcount - 1);
            // 更新操作
            productMapper.updateById(product);
            Buy buy = new Buy();
            buy.setProductname(product.getProductname());
            buy.setUsername(Thread.currentThread().getName());
            // 保存操作
            buyMapper.insert(buy);
            System.out.println(Thread.currentThread().getName() + ":减库存,创建订单完毕!");
        } else {
            System.out.println(Thread.currentThread().getName() + ":没有库存了");
        }
    } finally {
        System.out.println(Thread.currentThread().getName() + ":释放锁");
        // 释放锁
        LOCK.unlock();
    }
}

那么肯定会出现异常!

那么接着看一下如何来处理异常的:

看一下方法注释:处理一个throwable,完成事务。 我们可能会提交或回滚,具体取决于配置。

那么这里的配置指的是什么?指的是我们在@Transactional中配置的一些注解指定的值。

这里来看一下java的异常体系:

那么接着看springboot对事务处理默认的异常是什么?再看一下代码,关键在于这个rollback方法的判断:

有方法注释一定要看方法注释,真的是很有帮助:

1、方法注释:获胜规则是最浅的规则(即继承层次结构中最接近异常的规则)。 如果没有规则适用 (-1),则返回 false。
    
2、if判断中的注释:如果没有规则匹配,则用户超类行为(未选中的回滚)。    

这里的规则就是我们在@Transactional注解中配置的值,所以这里面的值不是能够随便来进行配置的。一会儿会给一个规范配置。

那么我们从这里也可以知道,如果知道一个异常是回滚还是提交呢?就一直在继承体系中来进行查找,那么这里也就说明了spring中肯定是存在着默认的异常处理类的。这里的变量命名的十分有意思。

那么对于我手动制造的异常来说,是没有找到对应的匹配规则的。那么则会走if中的判断,那么看一下if中的判断:

默认行为与 EJB 一样:在未经检查的异常 (RuntimeException) 上回滚,假设任何业务规则之外的意外结果。 此外,我们还尝试回滚错误,这显然也是一个意想不到的结果。 相比之下,检查异常被认为是业务异常,因此是事务性业务方法的常规预期结果,即一种仍然允许资源操作的常规完成的替代返回值。
这在很大程度上与 TransactionTemplate 的默认行为一致,除了 TransactionTemplate 还会回滚未声明的已检查异常(极端情况)。 对于声明性事务,我们希望将检查异常有意声明为业务异常,从而导致默认提交。
--------------------------------------------------------------------------------------------------------------   
--------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------    
    @Override
    public boolean rollbackOn(Throwable ex) {
    return (ex instanceof RuntimeException || ex instanceof Error);
}

看一下这里的注释:

1、默认是RuntimeException和Error;

2、将检查时异常需要注意!可能会导致提交(所以阿里体系规范,最好写Exception);

针对上面的2中的一会儿可以来进行实验,但是感觉没有必要来进行操作。

那么代码执行到了这里,返回为TRUE之后,

将会走到这里的代码中来:

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;
}

应用程序异常被回滚异常覆盖

执行回滚操作。还会将异常给抛出来,交给调用者来进行处理。这里是为了记录一下日志而已。因为我们一路看下来,都是在进行try...catch...,但是又一路向外抛,没有对异常来进行处理,只是做了一下日志的记录而已。

但是对于上面的winner==null来说,我们是否可以手动的来更改这个值呢?答案也是可以的:

@Transactional(noRollbackFor = ArithmeticException.class)

在@Transactional注解上加上属性的值,那么再来进行判断:

在这里可以看到因为我们在注解上加了ArithmeticException,不让其进行回滚操作,那么这里匹配到了之后,winner就会被赋值,然后if判断就不会生效。因为最终返回的是fasle,那么就会执行到了commit操作里面来:

如果这个时候你理解要提交事务的话,那么你又理解错了。

还是刚刚那个逻辑!因为这里考虑的是事物方法调用事务方法的问题,如果被调用的出现了异常,那么调用者的事务如何来进行处理呢?

2.4.4、使用注解注意细节

那么这里我分别举例来说明一下几个问题:

1、事务的开启和准备工作是有区别的,在innodb数据库引擎中,只有真正开始执行SQL的时候,这个时候才算是开启了事务。

2、在上面的基础之上,如果还没有来的及执行SQL语句,就已经抛出了异常。那么事务如何来进行处理。

最常见是FileNotFoundException异常等!但是这种会走提交事务的逻辑!因为其不是默认异常的子类,那么返回false,会走commit操作

那么这里就有点问题了。因为对于抛出的是编译时异常来说,应该都会来进行处理。但是也会有对应的问题出现:

编译时异常在前;  
sql操作在后;

那么这种操作,会导致事务提交,然后抛出异常。

那么这里手动制造一个:

    public void test222() throws ClassNotFoundException, IOException {
        FileInputStream inputStream = new FileInputStream("dsafdsaf:fdksajflkds");
        inputStream.close();
        System.out.println("hello");
        int i = 1/0;
        System.out.println("hello");
    }

当编译时异常发生之后,那么下面的将不会继续执行了,向上层抛出异常。

然后事务提交。那么这种没有关系。但是如下所示:

sql1操作在前;
编译时异常在后;
sql2操作在前;    

那么这个地方如果出现了编译时异常,将会导致上面SQL执行成功并提交,下面的并不会执行。那么将会导致问题的发生。

所以这里建议无论是运行时异常还是编译时异常,都将其设置成Exception或者是Throwable的类型。

以防万一!还是这样来进行设置比较保守。

2.4.5、Rollback

那么再继续刚刚的话题:如果出现了rollback的场景,该如何来进行解决呢?这种场景下应该是事务方法调用事务方法:

这里需要首先说明一下spring中的事务传播行为是REQUIRED行为,也就是说,如果上下文存在着事务,那么使用上下文的事务;如果上下文没有事务,那么创建新的事务。所以下面的两个方法如果存在着事务方法调用,那么使用的应该是同一个事务。

    @Transactional
    public void sellProduct() throws ClassNotFoundException {
        log.info("----------------->>>>>>>开启日志<<<<<------------------------");
        LOCK.lock();
        try {
            ProductService productService = SpringApplicationContext.getBean(ProductService.class);
            log.info("对应的productService是----------->>>{}", productService);
            // 两个事务方法来进行调用!那么看看对应的效果数据!
            productService.sellProductZ();
        } catch (Exception e) {
            log.error("程序异常,详细信息是:{}", e.getLocalizedMessage(), e);
        }
        try {
            System.out.println(Thread.currentThread().getName() + ":抢到锁了,进入到方法中来");
            // 首先查询库存
            Product product = productMapper.selectById(1L);
            Integer productcount = product.getProductcount();
            System.out.println(Thread.currentThread().getName() + ":当前库存是:" + productcount);
            if (productcount > 0) {
                product.setProductcount(productcount - 1);
                // 更新操作
                productMapper.updateById(product);
                Buy buy = new Buy();
                buy.setProductname(product.getProductname());
                buy.setUsername(Thread.currentThread().getName());
                // 保存操作
                buyMapper.insert(buy);
                System.out.println(Thread.currentThread().getName() + ":减库存,创建订单完毕!");
            } else {
                System.out.println(Thread.currentThread().getName() + ":没有库存了");
            }
        } finally {
            System.out.println(Thread.currentThread().getName() + ":释放锁");
            // 释放锁
            LOCK.unlock();
        }
    }

    @Transactional
    public void sellProductZ() {
        // 随便来制造一个异常
        int a = 1 / 0;
    }

从这里看到,事务方法sellProduct调用事务方法sellProductZ。

上面是在模拟一种情况,事务方法sellProductZ出现了异常,那么调用者的事务方法,该如何来进行处理?

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

出现了这个异常

那么这里是怎么造成的?肯定是因为事务方法sellProductZ抛出了异常。然后接下来因为在事务方法sellProduct进行了try....catch...操作

但是看看sellProductZ的处理:

追踪一下:

那么出现了异常肯定会走这里:

因为是算术异常,属于运行时异常。那么是RuntimeException的子类,所以提交将事务进行回滚:

查看方法注释:处理实际回滚。 已完成标志已被检查。那么我们关注的点是:

也就是说sellProductZ方法异常,这里会将一个标记置为true。看上来的日志说明:加载事务失败,将事务需要进行回滚标记下来。

那么接着看这里进行了标记,看一下sellProduct的处理方式。因为在里面来进行了try...catch...操作,所以内部没有异常,那么整个方法执行完成之后需要进行提交。

但是之前说过,提交是真的提交吗?并不是,还需要检查roll-back,那么一切说起来就很合理了。

做一些清除操作,然后开始提交事务。

那么这里就需要来做检查,判断是否是rollback。那么看一下对应的检查:

	protected boolean shouldCommitOnGlobalRollbackOnly() {
		return false;
	}

这里返回的是false,那么只需要关注的是后面的为什么是true即可。

最终会来到:

		@Override
		public boolean isRollbackOnly() {
			return getConnectionHolder().isRollbackOnly();
		}

那么应该是一个公用一个事务,那么在一个连接上的操作。上面sellProductZ在这里将其置为了TRUE,那么表示需要进行回滚。

那么再看上面的图,就表示的是已经可以回滚事务了。

所以要是sellProductZ方法出现了异常,而sellProduct执行正常可以进行提交的正确使用方式是使用正确的传播行为。

应该是如果上下文中存在着事务,那么不去使用这个事务,而是挂起,自己新创建一个,那么毫无疑问的是

做一个修改的地方:

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sellProductZ() {
        // 随便来制造一个异常
        int a = 1 / 0;
    }

那么再次来进行调用,查看一下结果。发现结果就是正确的了!这块的原理还是比较好分析的。

3、事务回调

Spring 事务的钩子函数

接下来,我将以一个实际的案例来描述Spring事务钩子函数的正确使用姿势。

3.1、案例背景

拿支付系统相关的业务来举例。在支付系统中,我们需要记录每个账户的资金流水(记录用户A因为哪个操作扣了钱,因为哪个操作加了钱),这样我们才能对每个账户的账做到心中有数,对于支付系统而言,资金流水的数据可谓是最重要的。

因此,为了防止支付系统的老大徇私舞弊,CTO提了一个流水存档的需求:要求支付系统对每个账户的资金流水做一份存档,要求支付系统在写流水的时候,把流水相关的信息以消息的形式推送到kafka,由存档系统消费这个消息并落地到库里(这个库只有存档系统拥有写权限)。

整个需求的流程如下所示:

整个需求的流程还是比较简单的,考虑到后续会有其他事业部也要进行数据存档操作,CTO建议支付系统团队内部开发一个二方库,这个二方库的主要功能就是发送消息到kafka中去。

3.2、确定方案

既然要求开发一个二方库,因此,我们需要考虑如下几件事情:

1、技术栈使用的springboot,因此,这里最好以starter的方式提供

2、二方库需要发送消息给kafka,最好是二方库内部基于kafka生产者的api创建生产者,不要使用Spring自带的kafkaTemplate,因为集成方有可能已经使用了kafkaTemplate。不能与集成方造成冲突。

3、减少对接方的集成难度、学习成本,最好是提供一个简单实用的api,业务侧能简单上手。

4、发送消息这个操作需要支持事务,尽量不影响主业务

在上述的几件事情中,最需要注意的应该就是第4点:发送消息这个操作需要支持事务,尽量不影响主业务。 这是什么意思呢?首先,尽量不影响主业务,这个最简单的方式就是使用异步机制。其次,需要支持事务是指:假设我们的api是在事务方法内部调用的,那么我们需要保证事务提交后再执行这个api。 那么,我们的流水落地api应该要有这样的功能:

内部可以判断当前是否存在事务,如果存在事务,则需要等事务提交后再异步发送消息给kafka。如果不存在事务则直接异步发送消息给kafka。而且这样的判断逻辑得放在二方库内部才行。那现在摆在我们面前的问题就是:我要如何判断当前是否存在事务,以及如何在事务提交后再触发我们自定义的逻辑呢?

3.3、TransactionSynchronizationManager显神威

这个类内部所有的变量、方法都是static修饰的,也就是说它其实是一个工具类。是一个事务同步器。下述是流水落地API的伪代码,这段代码就解决了我们上述提到的疑问:

private final ExecutorService executor = Executors.newSingleThreadExecutor();

public void sendLog() {
    // 判断当前是否存在事务
    if (!TransactionSynchronizationManager.isSynchronizationActive()) {
        // 无事务,异步发送消息给kafka        
        executor.submit(() -> {
            // 发送消息给kafka
            try {
                // 发送消息给kafka
            } catch (Exception e) {
                // 记录异常信息,发邮件或者进入待处理列表,让开发人员感知异常
            }
        });
        return;
    }

    // 有事务,则添加一个事务同步器,并重写afterCompletion方法(此方法在事务提交|回滚时后会做回调)
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {

        @Override
        public void afterCompletion(int status) {
            if (status == TransactionSynchronization.STATUS_COMMITTED) {
                // 事务提交后,再异步发送消息给kafka
                executor.submit(() -> {
                    try {
                     // 发送消息给kafka
                    } catch (Exception e) {
                     // 记录异常信息,发邮件或者进入待处理列表,让开发人员感知异常
                    }
                });
            }
        }

    });

}

代码比较简单,其主要是TransactionSynchronizationManager的使用。

3.3.1、判断是否存在事务?

TransactionSynchronizationManager.isSynchronizationActive() 方法显神威

我们先看下这个方法的源码:

// TransactionSynchronizationManager.java类内部的部分代码

private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
   new NamedThreadLocal<>("Transaction synchronizations");

public static boolean isSynchronizationActive() {
    return (synchronizations.get() != null);
}

很明显,synchronizations是一个线程变量(ThreadLocal)。那它是在什么时候set进去的呢?

这里的话,可以参考下这个方法:org.springframework.transaction.support.TransactionSynchronizationManager#initSynchronization,其源码如下所示:

/**
  * Activate transaction synchronization for the current thread.
  * Called by a transaction manager on transaction begin.
  * @throws IllegalStateException if synchronization is already active
  */
public static void initSynchronization() throws IllegalStateException {
    if (isSynchronizationActive()) {
        throw new IllegalStateException("Cannot activate transaction synchronization - already active");
    }
    logger.trace("Initializing transaction synchronization");
    synchronizations.set(new LinkedHashSet<>());
}

由源码中的注释也可以知道,它是在事务管理器开启事务时调用的。换句话说,只要我们的程序执行到带有事务特性的方法时,就会在线程变量中放入一个LinkedHashSet,用来标识当前存在事务。只要isSynchronizationActive返回true,则代表当前有事务。

因此,结合这两个方法我们是指能解决我们最开始提出的疑问:要如何判断当前是否存在事务

3.3.2、如何在事务提交后触发自定义逻辑?

TransactionSynchronizationManager.registerSynchronization()方法显神威

我们来看下这个方法的源代码:

/**
  * Register a new transaction synchronization for the current thread.
  * Typically called by resource management code.
  * <p>Note that synchronizations can implement the
  * {@link org.springframework.core.Ordered} interface.
  * They will be executed in an order according to their order value (if any).
  * @param synchronization the synchronization object to register
  * @throws IllegalStateException if transaction synchronization is not active
  * @see org.springframework.core.Ordered
  */
public static void registerSynchronization(TransactionSynchronization synchronization)
    throws IllegalStateException {

    Assert.notNull(synchronization, "TransactionSynchronization must not be null");
    if (!isSynchronizationActive()) {
        throw new IllegalStateException("Transaction synchronization is not active");
    }
    synchronizations.get().add(synchronization);
}

这里又使用到了synchronizations线程变量,我们在判断是否存在事务时,就是判断这个线程变量内部是否有值。那我们现在想在事务提交后触发自定义逻辑和这个有什么关系呢?

我们在上面构建流水落地api的伪代码中有向synchronizations内部添加了一个TransactionSynchronizationAdapter,内部并重写了afterCompletion方法,其代码如下所示:

TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {

    @Override
    public void afterCompletion(int status) {
        if (status == TransactionSynchronization.STATUS_COMMITTED) {
            // 事务提交后,再异步发送消息给kafka
            executor.submit(() -> {
                    try {
                     // 发送消息给kafka
                    } catch (Exception e) {
                     // 记录异常信息,发邮件或者进入待处理列表,让开发人员感知异常
                    }
            });
        }
    }

});

我们结合registerSynchronization的源码来看,其实这段代码主要就是向线程变量内部的LinkedHashSet添加了一个对象而已,但就是这么一个操作,让Spring在事务执行的过程中变得“有事情可做”。这是什么意思呢?

是因为Spring在执行事务方法时,对于操作事务的每一个阶段都有一个回调操作,比如:

trigger系列的回调

invoke系列的回调

而我们现在的需求就是在事务提交后触发自定义的函数,那就是在invokeAfterCommitinvokeAfterCompletion这两个方法来选了。首先,这两个方法都会拿到所有TransactionSynchronization的集合(其中会包括我们上述添加的TransactionSynchronizationAdapter)。

但是要注意一点:invokeAfterCommit只能拿到集合,invokeAfterCompletion除了集合还有一个int类型的参数,而这个int类型的参数其实是当前事务的一种状态。也就是说,如果我们重写了invokeAfterCompletion方法,我们除了能拿到集合外,还能拿到当前事务的状态。

因此,此时我们可以根据这个状态来做不同的事情,比如:可以在事务提交时做自定义处理,也可以在事务回滚时做自定义处理等等。

备注一下:手动回滚标记

TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

4、总结

上面有说到,我们判断当前是否存在事务、添加钩子函数都是依赖线程变量的。因此,我们在使用过程中,一定要避免切换线程。否则会出现不生效的情况。

参考:https://mp.weixin.qq.com/s/F5EJlqwQX663dL8cBMur5w

4、总结

那么最后一个关于锁,很明显的使用错误。锁已经释放了,但是事务还没有提交。所以无论是syncronized还是lock锁都无法避免。

建议加在controller层中,或者是另外新起一个事务方法来进行调用,亦或者是加入动态对象到当前对象中来。

或者另外一种方式来使用编程式事务,这种方式可以规避很多问题的发生。比如说:

长事务连接

链接:https://mp.weixin.qq.com/s/Q1VnZd5rt5OFaRGaXtABMg

编程式事务的使用:https://blog.csdn.net/whhahyy/article/details/48370879?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_title~default-0.fixedcolumn&spm=1001.2101.3001.4242.1

3.1、使用编程式事务

使用TransactionTemplate 不需要显式地开始事务,甚至不需要显式地提交事务。这些步骤都由模板完成。但出现异常时,应通过TransactionStatus 的setRollbackOnly 显式回滚事务。
TransactionTemplate 的execute 方法接收一个TransactionCallback 实例。Callback 也是Spring 的经典设计,用于简化用户操作, TransactionCallback 包含如下方法。
• Object dolnTransaction(TransactionStatus status) 。
该方法的方法体就是事务的执行体。
如果事务的执行体没有返回值,则可以使用TransactionCallbackWithoutResultl类的实例。这是个抽象类,不能直接实例化,只能用于创建匿名内部类。它也是TransactionCallback 接口的子接口,该抽象类包含一个抽象方法:
• void dolnTransactionWithoutResult(TransactionStatus status)该方法与dolnTransaction 的效果非常相似,区别在于该方法没有返回值,即事务执行体无须返回值。

那么看一下对应的配置信息:

@Configuration
@AllArgsConstructor
public class BianChengShiConfig {

    private final DataSource dataSource;


    @Bean
    public DataSourceTransactionManager dataSourceTransactionManager(){
        return new DataSourceTransactionManager(dataSource);
    }


    @Bean
    public TransactionTemplate transactionTemplate(){
        TransactionTemplate transactionTemplate = new TransactionTemplate(dataSourceTransactionManager());
        // 设置事务传播行为
        transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        // 设置隔离级别
        transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        // 默认为false,这里可以不需要来进行设置
        transactionTemplate.setReadOnly(false);
        // 设置超时时间  这里不需要来进行设置
        transactionTemplate.setTimeout(4000);
        return transactionTemplate;
    }
}

然后看一下对应的使用:

@Service
@AllArgsConstructor
public class UserServiceImpl implements UserService {

    private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);

    private final UserMapper userMapper;
    private final TransactionTemplate transactionTemplate;

    @Override
    public boolean transfer(String from, String to, Double money) {
        // 模拟出现了异常,如何来进行处理?
        transactionTemplate.execute((transactionStatus -> {
            userMapper.transfer(from, -money);
            // 模拟出现了异常,如何来进行处理?
            int i = 1 / 0;
            userMapper.transfer(to, money);
            return transactionStatus;
        }));
        return true;
    }

}

注意:在业务层不需要手动的来对异常代码进行try...catch....

如果这样子做了,那么就意味着我们需要手动的来进行手动回滚事务。

看一下源码:

	public <T> T execute(TransactionCallback<T> action) throws TransactionException {
		Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");

		if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
			return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
		}
		else {
			TransactionStatus status = this.transactionManager.getTransaction(this);
			T result;
			try {
				result = action.doInTransaction(status);
			}
			catch (RuntimeException | Error ex) {
				// Transactional code threw application exception -> rollback
				rollbackOnException(status, ex);
				throw ex;
			}
			catch (Throwable ex) {
				// Transactional code threw unexpected exception -> rollback
				rollbackOnException(status, ex);
				throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
			}
			this.transactionManager.commit(status);
			return result;
		}
	}

看到:

result = action.doInTransaction(status);

这行代码就是我们在代码中进行的逻辑实现:

transactionTemplate.execute((transactionStatus -> {
            userMapper.transfer(from, -money);
            // 模拟出现了异常,如何来进行处理?
            int i = 1 / 0;
            userMapper.transfer(to, money);
            return transactionStatus;
        }));

当我们的代码中出现了异常,会进入到catch代码块中去,然后去尝试进行回滚操作;说明了这里已经帮我们来实现了这样的一个过程。

如果代码执行正常,那么最终会执行到commit代码块中来进行执行。

这种使用方式也是比较优雅的。

感觉这种使用方式要比@Transactional注解使用起来好用一些。而且还能够避免一些出现的问题。

没有必要将对其他的一些操作,如磁盘IO操作放置在事务中来进行执行。如果需要的话,加上syncronized或者是lock锁都可以。

参考:

1、https://blog.csdn.net/xrt95050/article/details/18076167
2、https://blog.csdn.net/qq_33404395/article/details/83377382?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-1.pc_relevant_paycolumn_v2&spm=1001.2101.3001.4242.2&utm_relevant_index=4

3、https://www.cnblogs.com/aliger/p/3898869.html
posted @ 2021-12-22 16:10  写的代码很烂  阅读(6197)  评论(0编辑  收藏  举报