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系列的回调
而我们现在的需求就是在事务提交后触发自定义的函数,那就是在invokeAfterCommit
和invokeAfterCompletion
这两个方法来选了。首先,这两个方法都会拿到所有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