前言:
在最近的实际工作当中,遇到一个业务场景,需要保证新老数据库的同步,在动态切换数据库当中,遇到了@Transactional和@DS的冲突问题,这里我写了一份个人的总结,从业务还原、原因剖析和如何解决等等一步步阐明。
目录
(二)@DSTransactional注解代替@Transactional
(五)、临时添加一个temp数据库,进行插入操作,并抛出异常
(七)、使用@DSTransactional注解,在slave和temp之间抛出异常
(八)、使用@DSTransactional注解,在最后抛出异常
一、场景模拟
1、先导入pom.xml依赖
1.
<dependencies>
2.
<dependency>
3.
<groupId>org.springframework.boot
</groupId>
4.
<artifactId>spring-boot-starter-jdbc
</artifactId>
5.
</dependency>
6.
<dependency>
7.
<groupId>org.springframework.boot
</groupId>
8.
<artifactId>spring-boot-starter-web
</artifactId>
9.
</dependency>
10.
11.
<dependency>
12.
<groupId>org.springframework.boot
</groupId>
13.
<artifactId>spring-boot-starter-test
</artifactId>
14.
<scope>test
</scope>
15.
</dependency>
16.
17.
<!--数据源切换依赖-->
18.
<dependency>
19.
<groupId>com.baomidou
</groupId>
20.
<artifactId>dynamic-datasource-spring-boot-starter
</artifactId>
21.
<version>3.5.2
</version>
22.
</dependency>
23.
24.
<!--MySQL驱动依赖-->
25.
<dependency>
26.
<groupId>mysql
</groupId>
27.
<artifactId>mysql-connector-java
</artifactId>
28.
<scope>runtime
</scope>
29.
</dependency>
30.
31.
<!--Mybatis依赖-->
32.
<dependency>
33.
<groupId>org.mybatis.spring.boot
</groupId>
34.
<artifactId>mybatis-spring-boot-starter
</artifactId>
35.
<version>2.2.2
</version>
36.
</dependency>
37.
</dependencies>
2、yml文件配置了3个数据源,主数据源是master,从数据源是slave,后续临时加了个数据源temp,为了用于事务的测试,数据库均为MySQL。
1.
2.
server:
3.
port: 8080
4.
5.
spring:
6.
datasource:
7.
dynamic:
8.
primary: master
9.
datasource:
10.
master:
11.
username: root
12.
password: root
13.
url: jdbc:mysql://localhost:3306/master?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
14.
driver-class-name: com.mysql.cj.jdbc.Driver
15.
16.
slave:
17.
username: root
18.
password: root
19.
url: jdbc:mysql://localhost:3306/slave?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
20.
driver-class-name: com.mysql.cj.jdbc.Driver
21.
temp:
22.
username: root
23.
password: root
24.
url: jdbc:mysql://localhost:3306/temp?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
25.
driver-class-name: com.mysql.cj.jdbc.Driver
26.
27.
mybatis:
28.
mapper-locations: classpath:mapping/*.xml

3、分别编写主数据源和从数据源的Mapper层接口
1.
@Mapper
2.
@DS("master")
3.
public
interface
MasterMapper {
4.
int
insertUser
(User user);
5.
}
1.
@Mapper
2.
@DS("slave")
3.
public
interface
SlaveMapper {
4.
int
insertRole
(Role role);
5.
}
4、分别编写对应的XML文件
1.
<?xml version="1.0" encoding="UTF-8" ?>
2.
<!DOCTYPE mapper
3.
PUBLIC
"-//mybatis.org//DTD Mapper 3.0//EN"
4.
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
5.
<mapper namespace="com.example.demo.mapper.MasterMapper">
6.
<insert id="insertUser" parameterType="com.example.demo.bean.User">
7.
INSERT INTO user (username, password)
8.
VALUES(#{username}, #{password})
9.
</insert>
10.
</mapper>
1.
<?xml version="1.0" encoding="UTF-8" ?>
2.
<!DOCTYPE mapper
3.
PUBLIC
"-//mybatis.org//DTD Mapper 3.0//EN"
4.
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
5.
<mapper namespace="com.example.demo.mapper.SlaveMapper">
6.
<insert id="insertRole" parameterType="com.example.demo.bean.Role">
7.
INSERT INTO role (role)
8.
VALUES(#{role})
9.
</insert>
10.
</mapper>
5、编写Service方法
1.
@Service
2.
public
class
UserService {
3.
4.
@Resource
5.
private MasterMapper masterMapper;
6.
7.
@Resource
8.
private SlaveMapper slaveMapper;
9.
10.
@Transactional
11.
public
void
Add
(){
12.
User
user
=
new
User();
13.
user.setUsername(
"张三");
14.
user.setPassword(
"123456");
15.
masterMapper.insertUser(user);
16.
Role
role
=
new
Role();
17.
role.setRole(
"管理员");
18.
slaveMapper.insertRole(role);
19.
}
20.
}

6、测试运行,报错如下
Error updating database. Cause: java.sql.SQLSyntaxErrorException: Table
'master.role' doesn't exist
The error may exist in file
[D:\JavaProjects\Java\demo\target\classes\mapping\SlaveMapper.xml]
The error may involve com.example.demo.mapper.SlaveMapper.insertRole-
Inline
The error occurred while setting parameters
SQL: INSERT INTO role (role) VALUES(?)
Cause: java.sql.SQLSyntaxErrorException: Table 'master.role' doesn't
exist
; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: Table 'master.role' doesn't exist
从报错的表面上来看,是因为在主数据库当中不存在role这个表,可是我们已经切换了数据源呀,可为什么还是报错呢,具体的详细原因,我们往下细说。
二、原因分析
@Transactional开启事务的时候,会先从数据库连接池获取是数据库的连接(基于Spring的AOP切面),我们UserService方法上面没有打上@DS注解,所以Spring默认采用的是主数据源,而且在这之后,这个事务会通过ThreadLocal跟当前线程绑定并也报错了connection连接,通俗的来讲,在进入UserService方法的时候,当前事务以及绑定了数据源Master,在运行到SlaveMapper接口时,因为当前事务的connection连接已经存在,所以拿到的数据源还是默认的Master,于是想找到Slave当中的role表,当然是不可能的,所以只能报错了。
三、解决方案
(一)更改事务的传播机制
其实我们只要更改一下事务的传播机制,将它设置为:Propagation.REQUIRES_NEW即可,意思就是将原有的Spring事务挂起,并创建一个新的事务并分配的一个新的connection,两者不影响,具体操作如下:
1、修改原有的UserService代码
不用通过@DS指定数据源,因为默认是Master;将slave业务操作分离出来,封装到一个Service服务类当中,再通过@Resource注解注入进来,最后还是指定一下回滚策略,遇到异常就回滚。
1.
@Service
2.
public
class
UserService {
3.
4.
@Resource
5.
private MasterMapper masterMapper;
6.
7.
@Resource
8.
private SlaveService slaveService;
9.
10.
@Transactional(rollbackFor = Exception.class)
11.
public
void
Add
(){
12.
User
user
=
new
User();
13.
user.setUsername(
"张三");
14.
user.setPassword(
"123456");
15.
masterMapper.insertUser(user);
16.
slaveService.slave();
17.
}
18.
19.
}
2、 编写SlaveService服务代码
必须通过@DS指定一下数据源为slave,在slave方法上面重新修改一下事务的传播机制即可
1.
@Service
2.
@DS("slave")
3.
public
class
SlaveService {
4.
@Resource
5.
private SlaveMapper slaveMapper;
6.
7.
@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
8.
public
void
slave
(){
9.
Role
role
=
new
Role();
10.
role.setRole(
"管理员");
11.
slaveMapper.insertRole(role);
12.
}
13.
}
3、其他的保持不变,最后我们再测试一下,看一下输出结果,成功了!
(二)@DSTransactional注解代替@Transactional
我们可以使用@DSTransactional注解代替@Transactional即可,其他什么都不用动,也是最简单的方法。
1、导入pom.xml依赖
1.
<dependency>
2.
<groupId>com.baomidou
</groupId>
3.
<artifactId>dynamic-datasource-spring-boot-starter
</artifactId>
4.
<version>3.5.0
</version>
5.
</dependency>
2、修改UserService代码
1.
@Service
2.
public
class
UserService {
3.
4.
@Resource
5.
private MasterMapper masterMapper;
6.
7.
@Resource
8.
private SlaveMapper slaveMapper;
9.
10.
@DSTransactional
11.
public
void
Add
(){
12.
User
user
=
new
User();
13.
user.setUsername(
"张三");
14.
user.setPassword(
"123456");
15.
masterMapper.insertUser(user);
16.
Role
role
=
new
Role();
17.
role.setRole(
"管理员");
18.
slaveMapper.insertRole(role);
19.
}
20.
}
3、运行测试,查看输出结果,成功!
四、事务回滚机制
(一)、在Master和Slave事务执行前抛出异常
UserService类:
1.
@Transactional(rollbackFor = Exception.class)
2.
public
void
Add
(){
3.
User
user
=
new
User();
4.
user.setUsername(
"张三");
5.
user.setPassword(
"123456");
6.
int
a
=
1/
0;
7.
masterMapper.insertUser(user);
8.
slaveService.slave();
9.
}
结果:数据保持一致
(二)、当master事务和slave事务中间抛出异常
UserService类:
1.
@Transactional(rollbackFor = Exception.class)
2.
public
void
Add
(){
3.
User
user
=
new
User();
4.
user.setUsername(
"张三");
5.
user.setPassword(
"123456");
6.
masterMapper.insertUser(user);
7.
int
a
=
1/
0;
8.
slaveService.slave();
9.
}
结果:回滚master事务,slave事务无影响
(三)、在slave方法中抛出异常
SlaveService类:
1.
@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
2.
public
void
slave
(){
3.
Role
role
=
new
Role();
4.
role.setRole(
"管理员");
5.
slaveMapper.insertRole(role);
6.
int
a
=
1/
0;
7.
}
结果:master和slave事务都会进行回滚
(四)、在master和slave事务之后
UserService类:
1.
@Transactional(rollbackFor = Exception.class)
2.
public
void
Add
(){
3.
User
user
=
new
User();
4.
user.setUsername(
"张三");
5.
user.setPassword(
"123456");
6.
masterMapper.insertUser(user);
7.
slaveService.slave();
8.
int
a
=
1/
0;
9.
}
结果:master事务回滚,slave已经提交事务,入库
(五)、临时添加一个temp数据库,进行插入操作,并抛出异常
UserService类:
1.
@Transactional(rollbackFor = Exception.class)
2.
public
void
Add
(){
3.
User
user
=
new
User();
4.
user.setUsername(
"张三");
5.
user.setPassword(
"123456");
6.
masterMapper.insertUser(user);
7.
slaveService.slave();
8.
tempService.temp();
9.
}
TempService类:
1.
@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
2.
public
void
temp
(){
3.
Car
car
=
new
Car();
4.
car.setCar(
"AE86");
5.
tempMapper.insertCar(car);
6.
int
a
=
1/
0;
7.
}
结果:master回滚,slave事务提交,temp回滚
(六)、嵌套
UserService类:
1.
@Transactional(rollbackFor = Exception.class)
2.
public
void
Add
(){
3.
User
user
=
new
User();
4.
user.setUsername(
"张三");
5.
user.setPassword(
"123456");
6.
masterMapper.insertUser(user);
7.
slaveService.slave();
8.
}
SlaveService类:
1.
@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
2.
public
void
slave
(){
3.
Role
role
=
new
Role();
4.
role.setRole(
"管理员");
5.
slaveMapper.insertRole(role);
6.
tempService.temp();
7.
}
TempService类:
1.
@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
2.
public
void
temp
(){
3.
Car
car
=
new
Car();
4.
car.setCar(
"AE86");
5.
tempMapper.insertCar(car);
6.
int
a
=
1/
0;
7.
}
结果:master回滚,slave回滚,temp回滚
(七)、使用@DSTransactional注解,在slave和temp之间抛出异常
UserService类
1.
@DSTransactional
2.
public
void
Add
(){
3.
User
user
=
new
User();
4.
user.setUsername(
"张三");
5.
user.setPassword(
"123456");
6.
masterMapper.insertUser(user);
7.
Role
role
=
new
Role();
8.
role.setRole(
"管理员");
9.
slaveMapper.insertRole(role);
10.
int a=
1/
0;
11.
Car
car
=
new
Car();
12.
car.setCar(
"AE86");
13.
tempMapper.insertCar(car);
14.
}
结果:master回滚,slave回滚、temp回滚
(八)、使用@DSTransactional注解,在最后抛出异常
1.
@DSTransactional
2.
public
void
Add
(){
3.
User
user
=
new
User();
4.
user.setUsername(
"张三");
5.
user.setPassword(
"123456");
6.
masterMapper.insertUser(user);
7.
Role
role
=
new
Role();
8.
role.setRole(
"管理员");
9.
slaveMapper.insertRole(role);
10.
Car
car
=
new
Car();
11.
car.setCar(
"AE86");
12.
tempMapper.insertCar(car);
13.
int a=
1/
0;
14.
}
结果:同上
五、gitee源码地址
六、总结
以上就是我目前对于这种问题的分析与解决方案,参考了网上各种各样的解决方案,写了一份总结文档。
版权声明:本文为CSDN博主「黄团团」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接
通过此篇文章了解到,普通的数据源切换中,事务失效的原因;
多数据源中,可在事务中开启新事物,但是由于事务的传播性,导致外部事物无法影响内部事务,但是内部事务会直接影响外部事务;
可以依然怒多数据源的事务来解决类似问题,既: @DSTransactional,这样就达成了既定的目标,事务一致性;与单数据源事务,基本没有区别;
本文转自 jimolvxing.blog.csdn.net/article/det…,如有侵权,请联系删除。