说明
演示demo采用springboot+mybatis-plus,代码结构来自springboot集成mybatis-plus
0. 概述
在执行多条增删改语句时,要保证这些操作是完整的、原子的,简单说就是这些操作要么全部成功,要么全部失败。事务起到了至关重要的作用。
1. 场景
操作一张表,张三向李四转账。
1.0 账户表初始数据
1.1 正常转账
1.1.1 代码片段
/**
*
* @author wangsm
* @Description 张三李四余额均为1000,张三 向 李四转500,
* **/
@Override
public void test01() {
// 张三扣钱 500
boolean zsResult = this.update(new LambdaUpdateWrapper<Account>()
.eq(Account::getId, 1743837874049695745L)
.setSql("balance = balance - " + 500.00));
// 李四增加 500
boolean lsResult = this.update(new LambdaUpdateWrapper<Account>()
.eq(Account::getId, 1743849364563660802L)
.setSql("balance = balance + " + 500.00));
log.info("转账结果->【{}】", zsResult && lsResult ? "成功" : "失败");
}
1.1.2 执行结果
正常情况之下没出现任何的问题,下面演示下出现异常的情况。
1.2 异常转账
1.2.0 恢复数据
先将余额都重新设置为1000,在张三扣钱后,人为制造异常,然后李四再加钱。
1.2.1 代码片段
/**
*
* @author wangsm
* @Description 张三李四余额均为1000,张三 向 李四转500,
* **/
@Override
public void test01() {
// 张三扣钱 500
boolean zsResult = this.update(new LambdaUpdateWrapper<Account>()
.eq(Account::getId, 1743837874049695745L)
.setSql("balance = balance - " + 500.00));
// 人为制造异常
int i= 1 / 0;
// 李四增加 500
boolean lsResult = this.update(new LambdaUpdateWrapper<Account>()
.eq(Account::getId, 1743849364563660802L)
.setSql("balance = balance + " + 500.00));
log.info("转账结果->【{}】", zsResult && lsResult ? "成功" : "失败");
}
1.2.2 执行结果
显然,张三的钱成功扣除了,但李四的钱并没有加上,问题很大。
1.2.3 问题分析
张三扣钱的sql执行后,事务直接提交了。
1.2.4 解决方法
在张三扣钱的sql执行前开启事务,将提交和回滚事务的权利交给spring。
2. 声明式事务
注解形式添加事务,对代码无侵染。
2.1 事务注解@Transactional
先将余额都重新设置为1000,在上述方法签名上加@Transactional
2.1.1 执行结果
2.2.2 数据库
显然,张三的钱“回来了”,余额都没变。事务注解起到了作用。
3. 事务失效的场景
事务注解用起来确实很爽,解决了问题,但一些场景会使事务注解失效,常见的场景如下。
3.1 方法修饰符为private或final
3.1.0 恢复数据
先将余额都重新设置为1000,在张三扣钱后,人为制造异常,然后李四再加钱。
3.1.1 方法修改符改为private
将2.1中方法的修饰符改为private,但该方法实现的是接口中的方法,可定不行,那就先改改方法名并且去掉重写的注解,这次没问题了。
但是但是,idea直接报错了
大概意思是,注解@Transactional修饰的方法不能被覆盖。
再或者不改动原来的方法,直接写个测试方法加上private和事务注解。结果是一样的。
总结:@Transactional和private 不能同时修饰一个方法。换成final修饰也是一样,问题的本质是,被private和final修饰的方法不能被重写。
3.2 事务方法被当前类实例调用
3.2.0 恢复代码
将代码恢复为3.1之祈安的样子,先暂时去掉事务注解
3.2.0 抽成方法
方法代码太多的时候,为了不影响主干上的业务逻辑,我们习惯于将某个实现的细节抽象成一个方法,方便阅读。但在被抽出来的方法上面加事务注解是无效的。下面将转账的代码抽成一个方法,并在方法上添加事务注解。
3.2.1 代码片段
/**
*
* @author wangsm
* @Description 张三李四余额均为1000,张三 向 李四转500,
* **/
@Override
@Transactional
public void test01() {
// 开始转账
startTransferAccount();
}
/**
*
* @author wangsm
* @Description 转账
*
**/
@Transactional
public void startTransferAccount(){
// 张三扣钱 500
boolean zsResult = this.update(new LambdaUpdateWrapper<Account>()
.eq(Account::getId, 1743837874049695745L)
.setSql("balance = balance - " + 500.00));
// 人为制造异常
int i= 1 / 0;
// 李四增加 500
boolean lsResult = this.update(new LambdaUpdateWrapper<Account>()
.eq(Account::getId, 1743849364563660802L)
.setSql("balance = balance + " + 500.00));
log.info("转账结果->【{}】", zsResult && lsResult ? "成功" : "失败");
}
3.2.2 执行结果
没错,还是它(手动狗头)
那让我们看看数据吧
???什么情况,事务失效了。
3.2.3 分析一波
spring对事物的控制是通过代理对象来实现的,在事务起作用的情况2.1中,test01方法的调用者实际上是该service对象的代理对象,spring通过AOP切面的形式为该方法添加事务开启和事务提交,在方法正常结束后,事务被提交,否则被回滚。
而startTransferAccount方法是被当前的service对象this调用的,并不是spring的代理对象,因此就没有添加AOP切面,没有控制事务开启和提交、回滚的操作。
3.2.4 思考
如果将事务注解转移到test01 上呢,即去掉startTransferAccount方法上的注解,在test01上加事务注解,事务起作用吗?
别慌,先把余额都恢复成1000,测试一下。
事务注解起作用了,数据归滚了。
那是咋回事?很简单,转账的行为虽然被抽成了一个方法,但它还是test01方法的一部分,test01添加了事务注解,又是被spring代理对象调用的,事务当然起作用了。
3.2.5 解决3.2.4中的长事务问题
你以为3.2.4很好了吗,答案是:并没有。3.2.4 的事务虽然生效了,但是造成了长事务问题,影响数据库的效率,好的方式还是在抽出来的方法添加事务。既然spring为当前对象生成了代理的对象,那我们拿到该对象,用该对象调用该方法不就好了。上代码
3.2.5.1 添加springboot的AOP依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.2.5.2 添加注解暴露代理对象
在项目的启动类上添加如下注解
@EnableAspectJAutoProxy(exposeProxy = true)
3.2.5.2 修改代码
获取代理对象proxy,让proxy来调用startTransferAccount方法
AccountServiceImpl proxy = (AccountServiceImpl)AopContext.currentProxy();
proxy.startTransferAccount();
3.2.5.3 测试一波
事务起作用了!!!
3.3 “吞了”异常
3.3.0 恢复数据库数据
将余额全部还原成1000
3.3.1 代码片段
// 张三扣钱 500
try {
boolean zsResult = this.update(new LambdaUpdateWrapper<Account>()
.eq(Account::getId, 1743837874049695745L)
.setSql("balance = balance - " + 500.00));
// 人为制造异常
int i= 1 / 0;
// 李四增加 500
boolean lsResult = this.update(new LambdaUpdateWrapper<Account>()
.eq(Account::getId, 1743849364563660802L)
.setSql("balance = balance + " + 500.00));
log.info("转账结果->【{}】", zsResult && lsResult ? "成功" : "失败");
} catch (Exception e) {
e.printStackTrace();
}
3.3.2 执行代码
控制台
数据库
钱少了,事务并未生效,sql没有回滚。
3.3.3 总结
spring事务回滚的策略是检测到代理的方法出现异常后才开始触发事务回滚,因此不能人为处理掉抛出的异常,否则事务失效。
3.4 异常类型不匹配
spring事务注解默认回滚的异常类型为error或者RuntimeException及其子类,其他的异常类型不回滚。
3.4.1 恢复数据库数据
将余额全部还原成1000
3.4.2 测试
/**
*
* @author wangsm
* @Description 张三李四余额均为1000,张三 向 李四转500,
* **/
@Override
@Transactional
public void test01() throws FileNotFoundException {
// 开始转账
// startTransferAccount();
// 张三扣钱 500
boolean zsResult = this.update(new LambdaUpdateWrapper<Account>()
.eq(Account::getId, 1743837874049695745L)
.setSql("balance = balance - " + 500.00));
// 受检异常
FileInputStream inputStream = new FileInputStream("filename.txt");
// 李四增加 500
boolean lsResult = this.update(new LambdaUpdateWrapper<Account>()
.eq(Account::getId, 1743849364563660802L)
.setSql("balance = balance + " + 500.00));
log.info("转账结果->【{}】", zsResult && lsResult ? "成功" : "失败");
}
3.4.3 执行结果
抛出FileNotFoundException的异常是IOException 的子类,为受检异常
数据库结果
另外,如果在事务注解上声明的回滚的异常类型和实际抛出的类型不匹配,或者声明的异常类型并不是抛出异常的父类,则也不会回滚。
3.5 事务方法中开启多线程执行业务方法
3.5.1 恢复数据库数据
将余额全部还原成1000
3.5.2 代码片段
开启另外的线程执行转账的方法
/**
*
* @author wangsm
* @Description 张三李四余额均为1000,张三 向 李四转500,
* **/
@Override
@Transactional
public void test01() {
// 开始转账
new Thread(() -> startTransferAccount()).start();
}
/**
*
* @author wangsm
* @Description 转账
*
**/
@Transactional
public void startTransferAccount(){
// 张三扣钱 500
boolean zsResult = this.update(new LambdaUpdateWrapper<Account>()
.eq(Account::getId, 1743837874049695745L)
.setSql("balance = balance - " + 500.00));
// 人为制造异常
int i = 1 / 0;
// 李四增加 500
boolean lsResult = this.update(new LambdaUpdateWrapper<Account>()
.eq(Account::getId, 1743849364563660802L)
.setSql("balance = balance + " + 500.00));
log.info("转账结果->【{}】", zsResult && lsResult ? "成功" : "失败");
}
3.5.3 执行结果
4.编程式事务
既然声明式事务那么多事儿,那我们自己动手控制事务就好了。
4.1 代码片段
// 注入事务管理器对象
@Autowired
private DataSourceTransactionManager transactionManager;
/**
*
* @author wangsm
* @Description 张三李四余额均为1000,张三 向 李四转500,
* **/
@Override
public void test01() {
// 开始转账
startTransferAccount();
}
/**
*
* @author wangsm
* @Description 转账
*
**/
@Transactional
public void startTransferAccount(){
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// 事物传播行为,开启新事务,这样会比较安全些。
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
// 获得事务状态
TransactionStatus status = transactionManager.getTransaction(def);
try {
//逻辑代码,可以写上你的逻辑处理代码
// 张三扣钱 500
boolean zsResult = this.update(new LambdaUpdateWrapper<Account>()
.eq(Account::getId, 1743837874049695745L)
.setSql("balance = balance - " + 500.00));
// 人为制造异常
int i = 1 / 0;
// 李四增加 500
boolean lsResult = this.update(new LambdaUpdateWrapper<Account>()
.eq(Account::getId, 1743849364563660802L)
.setSql("balance = balance + " + 500.00));
boolean transferResult = zsResult && lsResult;
log.info("转账结果->【{}】", transferResult ? "成功" : "失败");
if (!transferResult) {
throw new RuntimeException("转账失败");
}
// 提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 开始回滚
transactionManager.rollback(status);
}
}
4.2 测试一波
事务回滚了!!!
那,如果没有异常能正常提交吗?测一下不就知道了
先注释掉抛异常的代码 int i = 1 / 0;
然后执行方法,查看控制台和数据库。
没问题,事务提交了。
结束
若有错误,欢迎指正,十分感谢!原创不易,感谢点赞关注!