Java 技术探秘

人如果没有了梦想,跟闲鱼有什么区别?


  • 首页

  • 源码分析

  • 日常记录

  • 学习笔记

  • 解决方案

  • 关于

  • Search
close

springboot 多数据源下,开启事务后,数据源切换失败的处理

Published at: 2021-05-01   |   Categories: 解决方案     |   Reading: 998 words ~5min

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 新建两个类

  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;
    }

}
  1. 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模块.

相关文章:

  • springboot + druid 实现数据源下动态切换
  • springboot 多数据源下,开启事务后,数据源切换失败的处理
#springboot# #多数据源# #事务#
01.Apache thrift 之使用示例
java抽奖工具类:按概率抽奖
  • Table of Content
  • Site Information

Funcy

9 Blogs
4 Categories
24 Tags
Gitee 掘金
    • 1. 问题的产生
    • 2. 验证
    • 3. 分析
    • 4. 解决
    • 5. 事务是否能回滚
    • 5.1 仅操作main数据源的回滚情况
    • 5.2 仅操作dbx数据源的回滚情况
    • 5.3 同时操作main数据源与dbx数据源的回滚情况
    • 5.4 结论
    • 6. 是否有必要使用分布式事务
© 2009 - 2024 Java 技术探秘
Powered by - Hugo v0.127.0
粤ICP备2024268097号-1
0%