浅聊事务

聊事务,先明白什么是事务,举个烂大街的例子,银行转账,转出方减钱,转入方加钱。这种情况下是不允许其中一个环节单独失败的,但是这种情况又必须执行两次以上的数据库操作语句,保证语句执行结果的一致性功能就是事务功能。

现在的关系型数据库大多都支持事务控制,事务的四大特性,我还真记不太清,原子性,隔离性,一致性,还有一个特性是什么来着,我查查,查到了,还有一个持久性。持久性没啥好说的,我就记不住。原子性,一致性,这个都好理解,就是都成功或者都失败。隔离性,这个就需要注意一点。具体情况具体处理。理论知识就不赘述了。

接下来进入下一话题,《母猪的产后护理》,对不起,拿错书了,

介绍一下spring提供的事务控制,分为编程式和声明式,先说我们用的多的声明式事务控制,且不建议使用编程式的事务控制,springboot2.5.x系列默认就开启了注解式事务控制,只需要在类上或方法上加上@Transactional注解就可以了,加在类上,表示此类中的所有方法都添加了默认的事务支持,如果 类上和方法上同时加了注解,以方法上的注解为准。只要方法抛出了RuntimeException,事务就进行回滚的操作,但如果你把异常try catch了,且没有在catch块里抛出RuntimeException,这种情况下出现异常事务是不会进行回滚的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ServiceA{
@Transactional
public void setTest(){
// 这种情况下出现异常是不会进行事务回滚的。
try {
updateById(null);
}catch (Exception e){
e.printStackTrace();
}

// 会进行事务回滚的写法
try {
updateById(null);
}catch (Exception e){
e.printStackTrace();
throw new RuntimeException("测试");
}
}
}

详解@Transactional注解

rollbackFor参数

这个参数是表明捕获什么异常进行回滚数据的。默认只有捕获到RuntimeException和Error才会进行回滚,(基于springboot2.5.x,不同版本可能有差异,请自己查看代码。)不会回滚的是什么异常呢,就是那种需要声明的异常,springboot事务控制是不会捕获到,并回滚数据的,至于为啥,很简单啊,因为声明的异常,大多都是自己手动处理的,继续往上抛或者Try Catch一下,如果不手动处理,程序无法编译。比如IO异常,这些是不会被事务控制捕捉到的。

propagation参数

propagation 代表事务的传播行为,默认值为Propagation.REQUIRED,其他的属性信息如下:

  • Propagation.REQUIRED:如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。( 也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务 )

  • Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。

  • Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。

  • Propagation.REQUIRES_NEW:重新创建一个新的事务,如果当前存在事务,暂停当前的事务。( 当类A中的 a 方法用默认Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停 a方法的事务 )

-Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,暂停当前的事务。

  • Propagation.NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常。

  • Propagation.NESTED :和 Propagation.REQUIRED 效果一样。

更多参数可以百度了解。

实际的问题

本人处理一些银行账务动账信息时,比如一个记账操作时,我需要把需要记账的数据的状态先改成在处理的状态,且需要提交事务,且需要确定下一个事务开启时需要记账的数据状态已经是修改过后的状态,防止重复操作同一笔数据,那么问题就来了,在使用spring声明事务控制时,如何进行这样的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ServiceA{
@Transactional
public void setTest(){
// 检查数据状态是否处于正常状态,如果在处理中就不进行操作
if (status == "2"){
log.info("此数据正在修改中,无法操作");
return;
}

// 先更新一下状态
setStatus("2");
// 此时需要提交

// 下面进行一些耗时的操作,

//操作结束后,还需进行一些需要事务控制的操作

}
}

这种情况就默认的事务控制一个方法内只能有一个事务,所以中间那步的提交操作以我的解决方式,既然一个方法只能有一个事务,那我把逻辑单独提一个方法不就行了,说干就干,这是修改后的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

public class ServiceA{
@Transactional
public void setTest2(){
// 先更新一下状态
setStatus("2");

}

@Transactional
public void setTest(){
// 检查数据状态是否处于正常状态,如果在处理中就不进行操作
if (status == "2"){
log.info("此数据正在修改中,无法操作");
return;
}

// 先更新一下状态
// setStatus("2");
// 调用方法
this.setTest2();
// 此时需要提交

// 下面进行一些耗时的操作,

//操作结束后,还需进行一些需要事务控制的操作

}
}

这个时候信心满满的操作发现setTest2方法调用过后,发现还是只有一个事务,且setTest2的更新操作还是没有分开提交,原因是,JDK的动态代理。在SpringIoC容器中返回的调用的对象是代理对象而不是真实的对象,只有被动态代理直接调用的才会产生事务。这里的this是自身是真实对象而不是代理对象,问题到这里已经很明朗了,这样啊,既然this调用的话事务不能起作用,spring最擅长的不就是注入,我注入一个,然后代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ServiceA{
// 这是我注入的,实际就是从容器里面取出自己的代理类
@Resource
ServiceA serviceA;

@Transactional
public void setTest2(){
// 先更新一下状态
setStatus("2");

}

@Transactional
public void setTest(){
// 检查数据状态是否处于正常状态,如果在处理中就不进行操作
if (status == "2"){
log.info("此数据正在修改中,无法操作");
return;
}

// 先更新一下状态
// setStatus("2");
// 调用方法
// this.setTest2();
// 使用注入的对象调用方法,就能触发事务了,
serviceA.setTest2();
// 此时需要提交

// 下面进行一些耗时的操作,

//操作结束后,还需进行一些需要事务控制的操作

}

}

到这步,心里想着应该就可以了,但实际上还是不行,这个时候我们就要看这个了注解,默认的规则是

如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。

很显然这个不是我想要的,因为我就是需要两个事务分别提交,你还给我加入同一个事务里,闹呢,一个一个看,配置事务的隔离性配置为Propagation.REQUIRES_NEW

REQUIRES_NEW,重新创建一个新的事务,如果当前存在事务,暂停当前的事务。且新增的事务处理完成后在处理当前的事务

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class ServiceA{
@Resource
ServiceA serviceA;

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void setTest2(){
// 先更新一下状态
setStatus("2");

}

@Transactional
public void setTest(){
// 检查数据状态是否处于正常状态,如果在处理中就不进行操作
if (status == "2"){
log.info("此数据正在修改中,无法操作");
return;
}

// 先更新一下状态
// setStatus("2");
// 调用方法
// this.setTest2();
// 使用注入的对象调用方法,就能触发事务了,
serviceA.setTest2();
// 此时需要提交

// 下面进行一些耗时的操作,

//操作结束后,还需进行一些需要事务控制的操作

}

}

注意看控制台,应该会出现两个事务,这个时候我的需求就算是完成了。给你带来方便的事务总会多多少少给你带来些许的不便,这些不便的地方叫做规则,因为你用了别人开发的技术,你就需要研究和遵守别人的定下的规则。一开始可能会让你束手束脚的,但是一旦你掌握了规则,你的编码能力会得到大大的提升。

手动控制事务

不建议使用,在一些特殊的场景里可以使用,如多线程的情况下,直接看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 需要注入的对象
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;

// 开启事务

TransactionStatus transactionStatus=dataSourceTransactionManager.getTransaction(transactionDefinition);

// 提交事务
dataSourceTransactionManager.commit(transactionStatus);

// 回滚事务
dataSourceTransactionManager.rollback(transactionStatus);

具体使用方法根据自己的业务来编写代码就可以了

女孩子 风景 外廊 冬景色 夕阳 晚霞