- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
之前有文章提供了springboot多数据源动态注册切换的整合方案,在后续使用过程中,发现在事务控制中有多种bug发生,决定对此问题进行分析与解决
多数据源切换流程结构图如下所示,包含几个组成元素
在Controller加入@Transitional注解后,数据源切换会失效,只会操作主库,查询资料后解决方案是将切面的Order设置为-1使之执行顺序在事务控制拦截之前,修改后证实有效,但是后续再次切换别的库或者进行主库操作无效,拿到的connection始终是第一次切换后的库对应的连接
分析代码后发现AbstractRoutingDataSource只负责提供getConnection这一层级,但是后续对connection的操作无法跟踪,项目框架mybatis和jdbcTemplate混合使用,后续操作在spring层面对于事务/数据源/连接这三者的逻辑层面操作是相同的,jdbcTemplate代码较为简单,所以以此为切入点进一步分析
通过断点调试会发现sql语句的执行最终会落到execute方法,方法中开始就是通过DataSourceUtils.getConnection获取连接,这里就是我们需要追踪的地方,点进去发现跳转到doGetConnection方法,这里面就是我们需要分析的具体逻辑
第一行获取的ConnectionHolder就是当前事务对应的线程持有对象,因为我们知道,事务的本质就是方法内部的sql执行时对应的是同一个数据库connection,对于不同的嵌套业务方法,唯一相同的是当前线程ID一致,所以我们将connection与线程绑定就可以实现事务控制
点进getResource方法,发现dataSource是作为一个key去一个Map集合里取出对应的contextHolder
到这里我们好像发现点什么,之前对jdbcTemplatechu实例化设定数据源直接赋值自定义的DynamicDataSource,所以在事物中每次我们获取connection依据就是DynamicDataSource这个对象作为key,所以每次都会一样了!!
@Bean
public JdbcTemplate jdbcTemplate(){
JdbcTemplate jdbcTemplate = null;
try{
jdbcTemplate = new JdbcTemplate(dynamicDataSource());
}catch (Exception e){
e.printStackTrace();
}
return jdbcTemplate;
}
后续针对mybatis查找了相关资料,事务控制默认实现是SpringManagedTransaction,源码查看后发现了熟悉的DataSourceUtils.getConnection,证明我们的分析方向是正确的
自定义操作类继承jdbcTemplate重写getDataSource,将我们获取的DataSource这个对应的key指定到实际切换库的数据源对象上即可
public class DynamicJdbcTemplate extends JdbcTemplate {
@Override
public DataSource getDataSource() {
DynamicDataSource router = (DynamicDataSource) super.getDataSource();
DataSource acuallyDataSource = router.getAcuallyDataSource();
return acuallyDataSource;
}
public DynamicJdbcTemplate(DataSource dataSource) {
super(dataSource);
}
}
public DataSource getAcuallyDataSource() {
Object lookupKey = determineCurrentLookupKey();
if (null == lookupKey) {
return this;
}
DataSource determineTargetDataSource = this.determineTargetDataSource();
return determineTargetDataSource == null ? this : determineTargetDataSource;
}
自定义事务操作类,实现Transaction接口,替换TransitionFactory,这里的实现与网上的解决方案略有不同,网上是定义三个变量,datasource(动态数据源对象)/connection(主连接)/connections(从库连接),但是框架需要mybatis和jdbctemplate进行统一,mybatis是从connection层面控制,jdbctemplate是从datasource层面控制,所以全部使用键值对存储
public class DynamicTransaction implements Transaction {
private final DynamicDataSource dynamicDataSource;
private ConcurrentHashMap<String, DataSource> dataSources;
private ConcurrentHashMap<String, Connection> connections;
private ConcurrentHashMap<String, Boolean> autoCommits;
private ConcurrentHashMap<String, Boolean> isConnectionTransactionals;
public DynamicTransaction(DataSource dataSource) {
this.dynamicDataSource = (DynamicDataSource) dataSource;
dataSources = new ConcurrentHashMap<>();
connections = new ConcurrentHashMap<>();
autoCommits = new ConcurrentHashMap<>();
isConnectionTransactionals = new ConcurrentHashMap<>();
}
public Connection getConnection() throws SQLException {
String dataBaseID = DBContextHolder.getDataSource();
if (!dataSources.containsKey(dataBaseID)) {
DataSource dataSource = dynamicDataSource.getAcuallyDataSource();
dataSources.put(dataBaseID, dataSource);
}
if (!connections.containsKey(dataBaseID)) {
Connection connection = DataSourceUtils.getConnection(dataSources.get(dataBaseID));
connections.put(dataBaseID, connection);
}
if (!autoCommits.containsKey(dataBaseID)) {
boolean autoCommit = connections.get(dataBaseID).getAutoCommit();
autoCommits.put(dataBaseID, autoCommit);
}
if (!isConnectionTransactionals.containsKey(dataBaseID)) {
boolean isConnectionTransactional = DataSourceUtils.isConnectionTransactional(connections.get(dataBaseID), dataSources.get(dataBaseID));
isConnectionTransactionals.put(dataBaseID, isConnectionTransactional);
}
return connections.get(dataBaseID);
}
public void commit() throws SQLException {
for (String dataBaseID : connections.keySet()) {
Connection connection = connections.get(dataBaseID);
boolean isConnectionTransactional = isConnectionTransactionals.get(dataBaseID);
boolean autoCommit = autoCommits.get(dataBaseID);
if (connection != null && !isConnectionTransactional && !autoCommit) {
connection.commit();
}
}
}
public void rollback() throws SQLException {
for (String dataBaseID : connections.keySet()) {
Connection connection = connections.get(dataBaseID);
boolean isConnectionTransactional = isConnectionTransactionals.get(dataBaseID);
boolean autoCommit = autoCommits.get(dataBaseID);
if (connection != null && !isConnectionTransactional && !autoCommit) {
connection.rollback();
}
}
}
public void close() {
for (String dataBaseID : connections.keySet()) {
Connection connection = connections.get(dataBaseID);
DataSource dataSource = dataSources.get(dataBaseID);
DataSourceUtils.releaseConnection(connection, dataSource);
}
}
public Integer getTimeout() {
return null;
}
}
public class DynamicTransactionFactory extends SpringManagedTransactionFactory {
@Override
public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
return new DynamicTransaction(dataSource);
}
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
//SpringBootExecutableJarVFS.addImplClass(SpringBootVFS.class);
final PackagesSqlSessionFactoryBean sessionFactory = new PackagesSqlSessionFactoryBean();
sessionFactory.setDataSource(dynamicDataSource());
sessionFactory.setTransactionFactory(new DynamicTransactionFactory());
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mybatis/**/*Mapper.xml"));
//关闭驼峰转换,防止带下划线的字段无法映射
sessionFactory.getObject().getConfiguration().setMapUnderscoreToCamelCase(false);
return sessionFactory.getObject();
}
事务中库动态切换的问题解决了,但是只针对了主库事务,如果从库操作也需要事务的特性该如何操作呢,这里就需要在注册数据源时针对每个数据源手动注册一个事务管理器
主库是固定的,可以直接在配置Bean中声明masterTransitionManage并设置为默认
@Bean("masterTransactionManager")
@Primary
public DataSourceTransactionManager MasterTransactionManager() {
return new DataSourceTransactionManager(masterDataSource());
}
从库的事务管理器我们可以拿到dataSource初始化对象,然后向Spring容器注册单例对象
public static void registerSingletonBean(String beanName, Object singletonObject) {
//将applicationContext转换为ConfigurableApplicationContext
ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) context;
//获取BeanFactory
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getAutowireCapableBeanFactory();
if(configurableApplicationContext.containsBean(beanName)) {
defaultListableBeanFactory.destroySingleton(beanName);
}
//动态注册bean.
defaultListableBeanFactory.registerSingleton(beanName, singletonObject);
}
SpringBootBeanUtil.registerSingletonBean(key + "TransactionManager", new DataSourceTransactionManager(druidDataSource));
在使用时只要对@Transitional注解指定transitionFactory名字即可
解决这个问题花费了三天的时间,查了很多资料和解决方案,很多都是只有参考性或者特异性的,所以还是需把握问题的核心加上部分源码的追踪,比如本文中需要清晰的认识到Transition-Connection-LocalThread三者的关联关系,才能找对排查的方向
后续实现了集成基于JMS(atomikos)的XA两段式提交的全局事务,使用DruidXADataSrouce出现了druid和atomikos两者线程池交互出现泄露的情况放弃了,给小伙伴们避个坑
我试图通过预准备语句使用同一连接执行多个查询,但无法完全实现! 代码片段: public class PostPrReqDaoImpl implements PostPrReqDaoInterface
我目前有一个 2 列宽的 DataGridView,第一列是 DataGridViewTextBoxColumn,第二列是 DataGridViewComboBoxColumn。我还有一个预生成的通用
当我在一台机器上运行以下代码时,我得到了 org.apache.tomcat.dbcp.dbcp.BasicDataSource 的 tomcat 实现,当我在另一台机器上运行它时,我得到了 org.
不确定这是否可行,但这是我的设置。 我有一台带有双启动功能的笔记本电脑。 一个一个分区我有 WinXP 和 MSAccess 2000在另一个分区上,Ubuntu 10.04,带有 apache we
我试过: czmlDataSource.load(czmlurl).then(function(){ viewer.dataSource
我有一个 TableView 和一个数组源。当我在 viewDidLoad 方法中初始化数组时,tableview 显示数组中的数据。当我从 Internet 上的 XML 数据的 URL 填充数组时
我对 DataSource 和 SessionFactory 之间的区别感到困惑。 我认为SessionFactory是一个用于检索 session 的管理器(我猜这实际上是与数据库的连接)。 Dat
我想存储大量(~数千)个字符串并能够使用通配符执行匹配。 例如,这里是一个示例内容: Folder1 文件夹 1/Folder2 Folder1/* Folder1/Folder2/Folder3 文
我有一个 DataGridView 和一个从 SQL 表填充的一些对象的列表。我曾使用两种方法将列表绑定(bind)到网格。 1.直接使用列表到数据源 grdSomeList.DataSource =
我正在尝试在 DataGridView 中设置一些内容。看起来这应该很简单,但我遇到了麻烦。我想显示三列: 代码ID 代号 带有 TypeName 的 DisplayMember 和 TypeID 的
在我的 Config.groovy我把线: grails.config.locations = [ "classpath:app-config.properties"] 我在哪里设置数据源的定义。文件
为了这个问题,假设我有一个包含各种酒类的 Excel 数据源电子表格。 (Cell A) | (Cell B) Bacardi | Rum Smirnoff | Vodka Another Vodka
由于我经常使用第三方 API,我认为创建一些 Magento 模块以实现轻松连接和查询它们会很有帮助。理想情况下,您可以像这样查询 API... $data = Mage::getModel( 'to
将后台线程频繁更新的数据源与 GUI 主线程同步的最佳方法是什么? 我应该在每个方法调用周围放置一个 pthread 互斥体吗?这对我来说似乎也很重。 编辑:我正在寻找 10.5 解决方案 最佳答案
经过几个小时的点击和试用,在查看各种帖子寻求帮助后,这段代码终于起作用了。但我希望有人帮助我理解函数(i,dat),这意味着什么?下面是我的完整代码 - function get_assignedta
我使用的是 Wildfly 10.1 版本,有两个数据源,如下所示, jdbc:mysql://${dbhostn
我正在学习数据源,我想我开始理解它,但我不明白这一段。 据我所知,MySQL 和 PostgreSQL 等数据库供应商编写了自己的不同 DataSource 接口(interface)的实现。现在,这
我有一个关于 TomEE 和使用 tomee.xml 中指定的数据源的奇怪问题。值得注意的是,我使用的是 Netbeans、TomEE 和 MySQL。在 Ubuntu 13.04(Xubuntu 最
WWDC 2019 确实充满了 iOS 的新内容以及 TableViews 和 CollectionView 的新数据源,即 UITableViewDiffableDataSource . 我已成功将
我在独立模式下运行 jboss 并将 standalone.xml 中的数据源设置为以下内容: jdbc:sqlserver://myip:1433;databaseNam
我是一名优秀的程序员,十分优秀!