因为项目中用到了多数据源 不可避免的会有各种各样的问题
列一下最主要的问题:
1 如何配置多数据源???
2 spring项目中多数据源无法切换???
3 操作了2个或者2个以上数据库的数据无法保证事务的一致性(也就是多数据源事务管理)
上面的3个问题 其实我只遇到了其中的一个 (spring项目中多数据源无法切换)就是加了@Transactional注解后 数据源无法切换的问题 这个问题当时就解决了 知其然还要知其所以然, 所以现在没有那么忙了 就把问题终结一下 知道其中的原理
1 项目的整合
springboot + mybatis puls + dynamic
boot的版本为 2.2.6
pom :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--mybatis puls-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>2.3</version>
</dependency>
<!--多数据源配置-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<!--druid 连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--oracle驱动-->
<dependency>
<groupId>com.ymtc.platform</groupId>
<artifactId>ojdbc6</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--去除内嵌tomcat-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<!--websocket依赖包-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>8.5.23</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-spring-boot-starter</artifactId>
<version>4.0.0</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
</dependency>
yml:
server:
port: 8088
# rocketmq 4.9.3
#rocketmq:
# # 多个用;隔开
# name-server: 10.22.15.61:9876;10.22.15.60:9876
mybatis-plus:
global-config:
#刷新mapper 调试神器
refresh-mapper: true
#实体扫描,多个package用逗号或者分号分隔
#typeAliasesPackage: com.bs.platform.bizform.entity.*
#主键类型 0:"数据库ID自增", 1:"用户输入ID",2:"全局唯一ID (数字类型唯一ID)", 3:"全局唯一ID UUID";
id-type: 3
#字段策略 0:"忽略判断",1:"非 NULL 判断"),2:"非空判断"
field-strategy: 2
#驼峰下划线转换
db-column-underline: true
configuration:
#配置返回数据库(column下划线命名&&返回java实体是驼峰命名),自动匹配无需as(没开启这个,SQL需要写as: select user_id as userId)
map-underscore-to-camel-case: true
cache-enabled: false
#配置JdbcTypeForNull, oracle数据库必须配置
jdbc-type-for-null: 'null'
spring:
main:
#重名会覆盖 默认会报错无法启动
allow-bean-definition-overriding: true
autoconfigure:
exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
datasource:
type: com.alibaba.druid.pool.DruidDataSource
dynamic:
druid: #以下是全局默认值,可以全局更改
#监控统计拦截的filters
filters: stat,wall
#配置初始化大小/最小/最大
initial-size: 1
min-idle: 1
max-active: 20
datasource:
master:
url: jdbc:oracle:thin:@10.22.32.17:1521/midtdb
username: esi_comm
password: esi_test_2022
driver-class-name: oracle.jdbc.driver.OracleDriver
iadquery:
url: jdbc:oracle:thin:@10.22.13.71:1521/yaetdb
username: iadsmpm_query
password: iadsmpm_query_test2022
driver-class-name: oracle.jdbc.driver.OracleDriver
(一些更加细致的配置和作用这里就不细说了)
到此 整合工作就可以完成了 就能够启动项目 访问数据库了第一个问题就解决了
补充一下:
dynamic 多数据源的配置 使用 @DS 注解来切换数据源 value就是yml z中配置的数据源的名称 这个@DS注解可以放到类或者方法上, baomido的官网建议的是这个注解应该用到service层的实现类上或者方法上,具体的影响可以上官网看看
2 数据源切换的问题
@Service
public class aaaServiceImpl implements aaaService {
@Autowired // 访问master数据库
private SmartPmTblCurrentMapper smartPmTblCurrentDao;
@Autowired // 访问iadquery数据库
private SmartPMAutoCallService service;
@Override
public String asd() {
List<SmartPmTblCurrent> smartPmTblCurrents = smartPmTblCurrentDao.selectList(null);
List<SmartPMChartMoveTarget> chartData = service.getChartData();
return "12123123";
}
}
这个时候没有如何问题
在aaaServiceImpl 类上加上 @Transactional 开启事务
java.sql.SQLSyntaxErrorException: ORA-00942: 表或视图不存在
通过排查 发现数据源没有切换过来导致当前的库中找不到查询的表
问题是为什么数据源这个时候就无法切换呢?
这个方法的确定目标数据源方法(determineTargetDataSource)代码为:
/**
* Retrieve the current target DataSource. Determines the
* {@link #determineCurrentLookupKey() current lookup key}, performs
* a lookup in the {@link #setTargetDataSources targetDataSources} map,
* falls back to the specified
* {@link #setDefaultTargetDataSource default target DataSource} if necessary.
* @see #determineCurrentLookupKey()
*/
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
看到这里的determineCurrentLookupKey方法的时候一切都能明了了。
那么再回到本文的题目,spring项目中多数据源无法切换,为什么呢?
因为我在代码中对于动态数据源的切换代码写在了service方法内了,而那个service的方法头上又加上了@Transactional注解,那么在spring框架执行的过程中,它先执行TransactionInterceptor的方法,而后再去执行我所写的service的具体代码,而在TransactionInterceptor执行的过程中它会先获取到数据源的连接,正因为如此,那么切换无论我在service里怎么写切换的数据源的代码它都不会再去执行了。
说的直白一点 总结一下: 就是加上事务的时候 开启事务会缓存 DataSource和SqlSessionTemplate信息 导致数据源没有办法切换
怎么去解决:
手动的去切数据源(这里就不展开说)
需要切换数据源的service代码不要开启事务
将切换数据源的代码写在service层之前,即写在controller层里
3 重点来了 前面的解决了问题 那么当我们同时要修改2个数据库 这个时候肯定是需要事务的支持的 那怎么办???
JTA基本概念
JTA即Java-Transaction-API,JTA允许应用程序执行分布式事务处理,即在两个或多个网络计算机资源上访问并且更新数据。JDBC驱动程序对JTA的支持极大地增强了数据访问能力。
XA协议是数据库层面的一套分布式事务管理的规范,JTA是XA协议在Java中的实现,多个数据库或是消息厂商实现JTA接口,开发人员只需要调用SpringJTA接口即可实现JTA事务管理功能。
JTA事务比JDBC事务更强大。一个JTA事务可以有多个参与者,而一个JDBC事务则被限定在一个单一的数据库连接。下列任一个Java平台的组件都可以参与到一个JTA事务中
分布式事务
分布式事务(DistributedTransaction)包括事务管理器(TransactionManager)和一个或多个支持 XA 协议的资源管理器 ( Resource Manager )。
资源管理器是任意类型的持久化数据存储容器,例如在开发中常用的关系型数据库:MySQL,Oracle等,消息中间件RocketMQ、RabbitMQ等。
事务管理器提供事务声明,事务资源管理,同步,事务上下文传播等功能,并且负责着所有事务参与单元者的相互通讯的责任。JTA规范定义了事务管理器与其他事务参与者交互的接口,其他的事务参与者与事务管理器进行交互。
pom加上
<!--JTA组件核心依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
yml加上
spring:
jta:
transaction-manager-id: jtaManager
我们就以2个数据源为例
配置类:
DruidIadqueryConfig
@Configuration
@MapperScan(basePackages = {"com.bs.dao.iadquery"},sqlSessionTemplateRef = "iadquerySqlSessionTemplate")
public class DruidIadqueryConfig {
@Autowired
private IadqueryConfig iadqueryConfig;
@Bean("dataSourceTwo")
public DataSource dataSourceOne () throws SQLException {
// 设置数据库连接
// MysqlXADataSource mysqlXADataSource = new MysqlXADataSource();
OracleXADataSource oracleXADataSource = new OracleXADataSource();
oracleXADataSource.setURL(iadqueryConfig.getUrl());
oracleXADataSource.setUser(iadqueryConfig.getUsername());
oracleXADataSource.setPassword(iadqueryConfig.getPassword());
oracleXADataSource.setDriverType(iadqueryConfig.getDriverClassName());
// 事务管理器
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
atomikosDataSourceBean.setXaDataSource(oracleXADataSource);
atomikosDataSourceBean.setUniqueResourceName("dataSourceTwo");
return atomikosDataSourceBean;
}
@Bean(name = "sqlSessionFactoryTwo")
public SqlSessionFactory sqlSessionFactoryOne(
@Qualifier("dataSourceTwo") DataSource dataSourceOne) throws Exception{
// 配置Session工厂
// SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
sessionFactory.setDataSource(dataSourceOne);
return sessionFactory.getObject();
}
@Bean(name = "iadquerySqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(
@Qualifier("sqlSessionFactoryTwo") SqlSessionFactory sqlSessionFactory) {
// 配置Session模板
return new SqlSessionTemplate(sqlSessionFactory);
}
}
@Configuration
@MapperScan(basePackages = {"com.bs.dao.master"},sqlSessionTemplateRef = "masterSqlSessionTemplate")
public class DruidMasterConfig {
@Autowired
private MasterConfig masterConfig ;
@Primary //
@Bean("dataSourceOne")
public DataSource dataSourceOne () throws SQLException {
// 设置数据库连接
// MysqlXADataSource mysqlXADataSource = new MysqlXADataSource();
OracleXADataSource oracleXADataSource = new OracleXADataSource();
oracleXADataSource.setURL(masterConfig.getUrl());
oracleXADataSource.setUser(masterConfig.getUsername());
oracleXADataSource.setPassword(masterConfig.getPassword());
oracleXADataSource.setDriverType(masterConfig.getDriverClassName());
// 事务管理器
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
atomikosDataSourceBean.setXaDataSource(oracleXADataSource);
atomikosDataSourceBean.setUniqueResourceName("dataSourceOne");
return atomikosDataSourceBean;
}
@Primary
@Bean(name = "sqlSessionFactoryOne")
public SqlSessionFactory sqlSessionFactoryOne(
@Qualifier("dataSourceOne") DataSource dataSourceOne) throws Exception{
// 配置Session工厂
// SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
sessionFactory.setDataSource(dataSourceOne);
return sessionFactory.getObject();
}
@Primary
@Bean(name = "masterSqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(
@Qualifier("sqlSessionFactoryOne") SqlSessionFactory sqlSessionFactory) {
// 配置Session模板
return new SqlSessionTemplate(sqlSessionFactory);
}
}
@Configuration
@Data
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.iadquery")
public class IadqueryConfig {
private String url;
private String username;
private String password;
private String driverClassName;
}
@Configuration
@Data
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.master")
public class MasterConfig {
private String url;
private String username;
private String password;
private String driverClassName;
}
加上事务启动项目:
2022-10-09 17:35:28 DEBUG org.springframework.web.servlet.DispatcherServlet:1131 - Completed 200 OK
打完 欢迎交流