前言

spring事务还是要单独拿起来说,大部分的开发使用都是用@Transactional去控制事物,有经验的会加上参数rollbackFor = Exception.class,正常情况下不会出问题,但是结合某种特殊的业务场景下就会出现不可预知的bug。

怎么说呢?有这样一个业务场景:我们需要向某个公司去同步数据,同步完成以后,修改表里的字段push_status为1,代表这条数据已经同步过了。正常的思路,就是查出数据、组装符合接口的数据、请求同步接口、修改字段,然后在方法上加一个事物注解,就结束了。

但是,这里会有个坑!具体什么坑看下面模拟

模拟业务

这里为了更好的说明这个坑,所以模拟业务,然后需要准备一些东西

准备

新建个测试表,这里随便创建一个接

1
2
3
CREATE TABLE `test` (
`id` varchar(100) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

新建模拟接口

image-20220107210542605

新建service层,进行dao层插入操作,这里的100次循环,我们模拟需要同步的数据,通过syncDataRequest模拟同步过去的数据

image-20220107211505964

测试

这里正常访问接口一口,看数据量,模拟正常业务请求

image-20220107211539014

全部处理成功,在查看数据库

image-20220107211615112

没有问题,这是正常的情况,并不能说明什么。

我们模拟异常的情况,模拟之前先清空test表的数据,我们写个逻辑,如果i是80就抛出异常,模拟异常情况,再跑一遍

image-20220107211835258

请求

image-20220107211936861

在处理完79id,80id失败了,抛出异常,然后去查看数据库数据,理论数据库应该会有80条数据,因为是从0开始的

image-20220107212043631

哎,数据并没有插入进去,这。。。不慌,跑一手路。。。

其实道理很简单,抛出异常,被@Transactional注解捕捉到了,于是他直接全部回滚,正常业务是没问题,但是我们的业务是同步一次数据并修改数据库,代表这条数据已经同步了,绝对不能回滚,毕竟我们已经同步过去的数据,不能说还要告诉人家公司,我们业务异常了,你给我回滚一下,公司分布式事务吗?

这个时候,其实有很多办法去解决,详说一下

解决

定义成功失败状态

定义请求的状态,如果成功就修改数据库,如果失败就不处理,并且try catch一下

image-20220107213116272

模拟请求一下

image-20220107213236948

发现80的异常并没有被处理,在查看数据库

image-20220107213313282

没有问题,皆大欢喜,打完收工!这就完了?并不是,相信大部分人是这样做的。这里会存在一个问题。

  • 我们如何保证同步完数据,修改数据库一定成功

这玩意没法保证啊,万一,在你请求完数据库,数据库抽风,就是插入失败,或者数据库IO突然变高,插入数据库超时抛出异常,那完犊子了,前面的数据修改全部回滚了,这种问题是致命的,导致数据不同步。

当然有大聪明可能会想,我一次查一个数据,同步一次不就好了?这里想说,单走一个6

那么,如何解解决的?

提前插入法和手动提交事务

我们提前执行修改数据库,然后在同步完数据的时候,在手动提交事务不就好了,然后每次循环都是这样操作,每次循环,开始事物,在结尾决定是否提交事务或者回滚事务。

这里需要用到事务api说下:

注入事务管理对象

image-20220107214126452

定义一个事物信息,并交给事物管理器

image-20220107214427524

try catch业务代码,处理事物的提交和回滚

image-20220107214540035

这样我们就可以手动的处理事物,并且我们是先去修改数据库,并未提交事务,如果请求成功,那就提交事务,如果请求失败,那就回滚失败,这样保证了业务处理的原子性。

具体测试,我就不测试, 然后因为api麻烦的原因,于是,写了一个工具类

注意:手动的事物,需要提交几次,就要开启几次,就是如果遇到循环手动提交事务的逻辑,就把开启事物写在循环里,否则异常

image-20220107215310439

工具类

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* 当单纯的事物注解已经无法满足我们的需求的时候,可以使用此工具类
* 建议,在多次事物提交的情况下使用
* @ClassName TransactionTool.java
* @author zhouyulin
* @version 1.0.0
* @Description 事务管理工具类
* @createTime 2022/1/7 20:46
*/
@Component
public class TransactionTool {


@Autowired
private DataSourceTransactionManager txManager;

/**
* @description: 开启事务默认级别是PROPAGATION_REQUIRES_NEW
* @return: org.springframework.transaction.TransactionStatus
* @author zhouyulin
* @date: 2022/1/7 17:45
*/
public TransactionStatus start() {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
return txManager.getTransaction(def);
}

/**
* @description: 开启事务,可以设置级别
* @param: propagationBehavior
* @return: org.springframework.transaction.TransactionStatus
* @author zhouyulin
* @date: 2022/1/7 17:49
*/
public TransactionStatus start(int propagationBehavior) {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(propagationBehavior);
return txManager.getTransaction(def);
}

/**
* @description: 开启事务,设置事务信息
* @param: def
* @return: org.springframework.transaction.TransactionStatus
* @author zhouyulin
* @date: 2022/1/7 17:50
*/
public TransactionStatus start(DefaultTransactionDefinition def) {
return txManager.getTransaction(def);
}


/**
* @description: 提交事务
* @param: status
* @return: void
* @author zhouyulin
* @date: 2022/1/7 17:48
*/
public void commit(TransactionStatus status) {
txManager.commit(status);
}

/**
* @description: 回滚事务
* @param: status
* @return: void
* @author zhouyulin
* @date: 2022/1/7 17:48
*/
public void rollback(TransactionStatus status) {
txManager.rollback(status);
}

}