springboot 多数据源下,开启事务后,数据源切换失败的处理
1. 问题的产生
在之前的文章中,整合了spring多数据源后,开始进行了愉快地编码中。不过这种"愉快"并没有持续多久,就碰到了一个诡异的问题:数据源切换突然不起作用了。经多次代码比对,发现无法切换的方法上加了个注解:@Transactional
.
2. 验证
首先,对无@Transactional
注解的代码进行访问:
@Override
public List<User> selectByQuery(UserQuery query) {
//省略其他代码
...
}
访问http://localhost:8084/api/user/get?appId=3
,结果如下:
[
{
"uid": 1,
"nick": "db3_张三"
},
{
"uid": 2,
"nick": "db3_李四"
},
{
"uid": 3,
"nick": "db3_王五"
}
]
可以看到,结果正常。
接着,添加@Transactional
注解,再次访问:
@Override
@Transactional(rollbackFor = Exception.class)
public List<User> selectByQuery(UserQuery query) {
//省略其他代码
...
}
访问http://localhost:8084/api/user/get?appId=3
,结果如下:
[
{
"uid":1,
"nick":"main_张三"
},
{
"uid":2,
"nick":"main_李四"
},
{
"uid":3,
"nick":"main_王五"
}
]
可以看到,获取到的是main数据源中的数据。
3. 分析
这里引进csdn上一位博主《springboot+mybatis解决多数据源切换事务控制不生效的问题》的分析:
当我们配置了事物管理器和拦截Service中的方法后,每次执行Service中方法前会开启一个事务,并且同时会缓存一些东西:DataSource、SqlSessionFactory、Connection等,所以,我们在外面再怎么设置要求切换数据源也没用,因为Conneciton都是从缓存中拿的,所以我们要想能够顺利的切换数据源,实际就是能够动态的根据DatabaseType获取不同的Connection,并且要求不能影响整个事物的特性。
4. 解决
这里采用文章《springboot+mybatis解决多数据源切换事务控制不生效的问题》中的解决方法,主要添加两个类:
4.1 新建两个类
- MultiDataSourceTransaction.java类:
@Slf4j
public class MultiDataSourceTransaction implements Transaction {
private final DataSource dataSource;
private Connection mainConnection;
private String mainDatabaseIdentification;
private ConcurrentMap<String, Connection> otherConnectionMap;
private boolean isConnectionTransactional;
private boolean autoCommit;
public MultiDataSourceTransaction(DataSource dataSource) {
notNull(dataSource, "No DataSource specified");
this.dataSource = dataSource;
otherConnectionMap = new ConcurrentHashMap<>();
mainDatabaseIdentification = DynamicDataSourceContextHolder.getMainDateSource();
}
/**
* {@inheritDoc}
*/
@Override
public Connection getConnection() throws SQLException {
String databaseIdentification = DynamicDataSourceContextHolder.getDataSourceType();
if (null == databaseIdentification || databaseIdentification.equals(mainDatabaseIdentification)) {
if (mainConnection != null) {
return mainConnection;
} else {
openMainConnection();
mainDatabaseIdentification = databaseIdentification;
return mainConnection;
}
} else {
if (!otherConnectionMap.containsKey(databaseIdentification)) {
try {
Connection conn = dataSource.getConnection();
otherConnectionMap.put(databaseIdentification, conn);
} catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
}
}
return otherConnectionMap.get(databaseIdentification);
}
}
private void openMainConnection() throws SQLException {
this.mainConnection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.mainConnection.getAutoCommit();
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.mainConnection, this.dataSource);
if (log.isDebugEnabled()) {
log.debug(
"JDBC Connection ["
+ this.mainConnection
+ "] will"
+ (this.isConnectionTransactional ? " " : " not ")
+ "be managed by Spring");
}
}
/**
* {@inheritDoc}
*/
@Override
public void commit() throws SQLException {
if (this.mainConnection != null && !this.isConnectionTransactional && !this.autoCommit) {
if (log.isDebugEnabled()) {
log.debug("Committing JDBC Connection [" + this.mainConnection + "]");
}
this.mainConnection.commit();
for (Connection connection : otherConnectionMap.values()) {
connection.commit();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void rollback() throws SQLException {
if (this.mainConnection != null && !this.isConnectionTransactional && !this.autoCommit) {
if (log.isDebugEnabled()) {
log.debug("Rolling back JDBC Connection [" + this.mainConnection + "]");
}
this.mainConnection.rollback();
for (Connection connection : otherConnectionMap.values()) {
connection.rollback();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void close() throws SQLException {
DataSourceUtils.releaseConnection(this.mainConnection, this.dataSource);
for (Connection connection : otherConnectionMap.values()) {
DataSourceUtils.releaseConnection(connection, this.dataSource);
}
}
@Override
public Integer getTimeout() throws SQLException {
return null;
}
}
- MultiDataSourceTransactionFactory类
public class MultiDataSourceTransactionFactory extends SpringManagedTransactionFactory {
@Override
public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level,
boolean autoCommit) {
return new MultiDataSourceTransaction(dataSource);
}
}
4.2 在mybatis-plus
配置中添加配置:
@Configuration
public class MybatisPlusConfig {
private String mapperLocations = "classpath*:mapper/**/*.xml";
@Bean("sqlSessionFactory")
@DependsOn({"dynamicDataSource"})
@Primary
public MybatisSqlSessionFactoryBean sqlSessionFactory(DataSource dynamicDataSource) {
MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dynamicDataSource);
sessionFactoryBean.setConfigLocation(new ClassPathResource("/mybatis/mybatis-config.xml"));
sessionFactoryBean.setMapperLocations(ResourceUtil.getPathMatchingResource(mapperLocations));
//事务配置
sessionFactoryBean.setTransactionFactory(new MultiDataSourceTransactionFactory());
return sessionFactoryBean;
}
}
4.3 验证
依然是@Transactional
注解:
@Override
@Transactional(rollbackFor = Exception.class)
public List<User> selectByQuery(UserQuery query) {
//省略其他代码
...
}
访问http://localhost:8084/api/user/get?appId=3
,结果如下:
[
{
"uid": 1,
"nick": "db3_张三"
},
{
"uid": 2,
"nick": "db3_李四"
},
{
"uid": 3,
"nick": "db3_王五"
}
]
这次结果终于是正确了。
5. 事务是否能回滚
经过以上处理后,数据源可以正常切换了,但事务能正常回滚吗?接下来验证如下情况下事务的回滚情况。
5.1 仅操作main数据源
的回滚情况
在 AppController.java
中开放一个接口:
@ResponseBody
@RequestMapping("save")
public String save(String appName, String dataSource) {
appService.save(appName, dataSource);
return "success";
}
接着,在 AppService.java
中添加一个方法,在模拟回滚情况:
@Override
//这里先注释掉事务
//@Transactional(rollbackFor = Exception.class)
public int save(String appName, String dataSource) {
App entity = new App();
entity.setAppName(appName);
entity.setDataSource(dataSource);
int result = appMapper.insert(entity);
//这里人为抛出一个异常,正常情况下是会回滚的
throw new RuntimeException("这里抛出一个异常");
}
main.app
表的当前记录如下:
id | app_name | data_source |
---|---|---|
1 | 应用1 | db1 |
2 | 应用2 | db2 |
3 | 应用3 | db3 |
接着,在浏览器中访问http://localhost:8084/api/app/save?appName=test01&dataSource=testDb1
,
页面报了500,表明执行到了异常抛出的那一步:
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Sun Nov 10 09:55:09 CST 2019
There was an unexpected error (type=Internal Server Error, status=500).
????????
再看看main.app
表,结果如下:
id | app_name | data_source |
---|---|---|
1 | 应用1 | db1 |
2 | 应用2 | db2 |
3 | 应用3 | db3 |
4 | test01 | testDb1 |
可以看到,记录插入成功。
此时再开启事务:
@Override
@Transactional(rollbackFor = Exception.class)
public int save(String appName, String dataSource) {
App entity = new App();
entity.setAppName(appName);
entity.setDataSource(dataSource);
int result = appMapper.insert(entity);
//这里人为抛出一个异常,正常情况下是会回滚的
throw new RuntimeException("这里抛出一个异常");
}
再访问链接http://localhost:8084/api/app/save?appName=test02&dataSource=testDb2
,此时要保存的appName为test02
, dataSource为testDb2
,结果如下:
再看看main.app
表中的记录,没有变化:
id | app_name | data_source |
---|---|---|
1 | 应用1 | db1 |
2 | 应用2 | db2 |
3 | 应用3 | db3 |
4 | test01 | testDb1 |
这表明事务成功回滚了。
5.2 仅操作dbx数据源
的回滚情况
操作db1
、db2
、db3
中的任意一个数据库都可以,这里我们选取db2
。
我们在UserController.java
中开放一个接口,用于操作保存:
@ResponseBody
@RequestMapping("save")
public String save(String nick) {
userService.save(nick);
return "success";
}
接着,在UserServiceImpl.java
中,添加事务具体的插入操作:
@Override
//这里先注释事务
// @Transactional(rollbackFor = Exception.class)
public int save(String nick) {
User user = new User();
user.setNick(nick);
int result = userMapper.insert(user);
throw new RuntimeException("这里抛出一个异常");
}
先来看下db2.users
中的数据:
|uid| nick |
|---| --- |
| 1 | db2_张三 |
| 2 | db2_李四 |
| 3 | db2_王五 |
接着,我们来请求下save
接口:http://localhost:8084/api/user/save?appId=2&nick=test01
(链接中的appId=2
,表示使用db2数据源),完成后,db2中的数据如下:
|uid| nick |
|---| --- |
| 1 | db2_张三 |
| 2 | db2_李四 |
| 3 | db2_王五 |
| 4 | test01 |
数据能正常添加,这与我们的期望一致。
下面再开启事务:
@Override
@Transactional(rollbackFor = Exception.class)
public int save(String nick) {
User user = new User();
user.setNick(nick);
int result = userMapper.insert(user);
throw new RuntimeException("这里抛出一个异常");
}
然后再访问http://localhost:8084/api/user/save?appId=2&nick=test02
(为了直观看到结果,这里的nick
改成test01
),结果如下:
|uid| nick |
|---| --- |
| 1 | db2_张三 |
| 2 | db2_李四 |
| 3 | db2_王五 |
| 4 | test01 |
| 5 | test02 |
神奇的一幕来了:事务并没有回滚。
5.3 同时操作main数据源
与dbx数据源
的回滚情况
从以上两种情况来看,我们可以分析出,这种情况下,main数据源
中的事务会回滚,而 dbx数据源
中的事务不会回滚。不过为了正确性,我们还是得验证下。
同样地,在AppController.java
开放一个接口:
@ResponseBody
@RequestMapping("saveMix")
public String saveMix(String appName, String dataSource, String nick) {
appService.saveMix(appName, dataSource, nick);
return "success";
}
接着,在AppService.java
中添加一个具体的事务操作方法:
@Override
//这里先注释事务
//@Transactional(rollbackFor = Exception.class)
public int saveMix(String appName, String dataSource, String nick) {
App app = new App();
app.setAppName(appName);
app.setDataSource(dataSource);
int result1 = appMapper.insert(app);
User user = new User();
user.setNick(nick);
int result2 = userMapper.insert(user);
throw new RuntimeException("这里抛出一个异常");
}
看看此时main.app
与db2.users
中的数据:
-
main.app
id app_name data_source 1 应用1 db1 2 应用2 db2 3 应用3 db3 4 test01 testDb1 -
db2.users
uid nick 1 db2_张三 2 db2_李四 3 db2_王五 4 test01 5 test02
再请求下接口:http://localhost:8084/api/app/saveMix?appId=2&nick=test03&appName=test02&dataSource=testDb2
(注意参数的变化),发现数据有成功添加:
-
main.app
id app_name data_source 1 应用1 db1 2 应用2 db2 3 应用3 db3 4 test01 testDb1 6 test02 testDb2 -
db2.users
uid nick 1 db2_张三 2 db2_李四 3 db2_王五 4 test01 5 test02 6 test03
接着,开启事务:
@Override
@Transactional(rollbackFor = Exception.class)
public int saveMix(String appName, String dataSource, String nick) {
App app = new App();
app.setAppName(appName);
app.setDataSource(dataSource);
int result1 = appMapper.insert(app);
User user = new User();
user.setNick(nick);
int result2 = userMapper.insert(user);
throw new RuntimeException("这里抛出一个异常");
}
再访问接口http://localhost:8084/api/app/saveMix?appId=2&nick=test04&appName=test03&dataSource=testDb3
(注意参数的变化),结果如下:
-
main.app
id app_name data_source 1 应用1 db1 2 应用2 db2 3 应用3 db3 4 test01 testDb1 6 test02 testDb2 -
db2.users
uid nick 1 db2_张三 2 db2_李四 3 db2_王五 4 test01 5 test02 6 test03 7 test04
可以看到,main.app
回滚成功,而 db2.users
未回滚。
5.4 结论
启用事务后,main数据源
可以正常回滚,而 dbx数据源
无法回滚。原因是,项目的默认数据源是main
,在开启事务时,使用的是默认数据源,无论后面切换到哪个数据源,事务只有在main数据源上是开启的,事务回滚与提交也只操作main数据源。
画图示意如下:
6. 是否有必要使用分布式事务
一般情况下而言,发现项目中要使用多数据源事务后,反思下是否真的有必要,是否可以通过调用服务接口的形式处理,或者补偿机制来进行。
另外,关于多数据源事务、分布式事务,又是另一个广阔的话题了,待研究。
代码源码见gitee仓库https://gitee.com/funcy/springboot-demomultiple-datasource-transaction
模块.
相关文章: