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、总结

那么最后一个关于锁,很明显的使用错误。锁已经释放了,但是事务还没有提交。所以无论是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  写的代码很烂  阅读(5903)  评论(0编辑  收藏  举报