本文为 【Java多数据源实现教程】 相关知识,由于自己最近在做导师的项目的时候需要使用这种技术,于是自学了相关技术原理与实现,并将其整理如下,具体包含:多数据源的典型使用场景
(包含业务复杂场景
、读写分离场景
),多数据源实现原理及实现方法
(包含通过AbstractRoutingDataSource实现动态数据源
、多数据源切换方式
、Spring集成多个MyBatis框架实现多数据源
),多数据源事务控制
(包含只使用主库TransactionManger
、一个方法开启2个事务
),dynamic-datasource多数源组件
等~
📌博主主页:小新要变强 的主页
👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~
👉Java微服务开源项目可参考:企业级Java微服务开源项目(开源框架,用于学习、毕设、公司项目、私活等,减少开发工作,让您只关注业务!)
在实际开发中,经常可能遇到在一个应用中可能需要访问多个数据库的情况。以下是两种典型场景:
数据分布在不同的数据库中,数据库拆了, 应用没拆。 一个公司多个子项目,各用各的数据库,涉及数据共享…
这里的架构与上图类似。不同的是,在读写分离中,主库和从库的数据库是一致的(不考虑主从延迟)。数据更新操作(insert、update、delete)都是在主库上进行,主库将数据变更信息同步给从库。在查询时,可以在从库上进行,从而分担主库的压力。
原理:
对于大多数的java应用,都使用了spring框架,spring-jdbc模块提供AbstractRoutingDataSource,其内部可以包含了多个DataSource,然后在运行时来动态的访问哪个数据库。这种方式访问数据库的架构图如下所示:
应用直接操作的是AbstractRoutingDataSource的实现类,告诉AbstractRoutingDataSource访问哪个数据库,然后由AbstractRoutingDataSource从事先配置好的数据源(ds1、ds2)选择一个,来访问对应的数据库。
通过这个类可以实现动态数据源切换。如下是这个类的成员变量:
private Map<Object, Object> targetDataSources;
private Object defaultTargetDataSource;
private Map<Object, DataSource> resolvedDataSources;
而AbstractRoutingDataSource实现了InitializingBean接口,并实现了afterPropertiesSet方法。afterPropertiesSet方法是初始化bean的时候执行,通常用作数据初始化。(resolvedDataSources就是在这里赋值)
@Override
public void afterPropertiesSet() {
...
this.resolvedDataSources = new HashMap<Object, DataSource(this.targetDataSources.size());//初始化resolvedDataSources
//循环targetDataSources,并添加到resolvedDataSources中
for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) {
Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
this.resolvedDataSources.put(lookupKey, dataSource);
}
...
}
AbstractRoutingDataSource
实现类DynamicDataSource
然后初始化targetDataSources
和key为数据源标识(可以是字符串、枚举、都行,因为标识是Object)、defaultTargetDataSource
即可AbstractRoutingDataSource.getConnection
会接着调用提供的模板方法:determineTargetDataSource
determineTargetDataSource
该方法返回的数据库标识从resolvedDataSources
中拿到对应的数据源DynamicDataSource
中实现determineTargetDataSource
为其提供一个数据库标识总结,在整个代码中我们只需做4件大事:
AbstractRoutingDataSource
实现类DynamicDataSource
targetDataSources
设置不同数据源的DataSource
和标识、及defaultTargetDataSource
determineTargetDataSource
中提供对应的数据源标识即可什么到这还不会? 附上代码:
🍀(1)配置多数据源和 AbstractRoutingDataSource
的自定义实现类:DynamicDataSource
配置多数据:
application.yml:
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
datasource1:
url: jdbc:mysql://127.0.0.1:3306/datasource1?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
username: root
password: 123456
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver
datasource2:
url: jdbc:mysql://127.0.0.1:3306/datasource2?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
username: root
password: 123456
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver
DynamicDataSourceConfig.java:
@Configuration
public class DynamicDataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.datasource1")
public DataSource firstDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.datasource2")
public DataSource secondDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary
public DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>(5);
targetDataSources.put(DataSourceNames.FIRST, firstDataSource);
targetDataSources.put(DataSourceNames.SECOND, secondDataSource);
return new DynamicDataSource(firstDataSource, targetDataSources);
}
}
DynamicDataSource.java:
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。
* 也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】,相当于线程的 private static 类型变量。
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 决定使用哪个数据源之前需要把多个数据源的信息以及默认数据源信息配置好
*
* @param defaultTargetDataSource 默认数据源
* @param targetDataSources 目标数据源
*/
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object>
targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return getDataSource();
}
public static void setDataSource(String dataSource) {
CONTEXT_HOLDER.set(dataSource);
}
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
多数据源切换方式需要根据我们的具体需求进行选择:
🍀(1)AOP+自定义注解
用于不同业务的数据源: 一般利用AOP,结合自定义注解动态切换数据源
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface WR {
String value() default "W";
}
@Component
@Aspect
public class DynamicDataSourceAspect {
// 前置通知
@Before("within(com.tuling.dynamic.datasource.service.impl.*) && @annotation(wr)")
public void before(JoinPoint joinPoint, WR wr){
System.out.println(wr.value());
}
}
@Service
public class FrendImplService implements FrendService {
@Autowired
FrendMapper frendMapper;
@Override
@WR("R") // 库2
public List<Frend> list() {
return frendMapper.list();
}
@Override
@WR("W") // 库1
public void save(Frend frend) {
frendMapper.save(frend);
}
}
🍀(2)MyBatis插件
用于读写分离的数据源:如果是MyBatis可以结合插件实现读写分离动态切换数据源
@Intercepts(
{@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class,ResultHandler.class})})
public class DynamicDataSourcePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] objects = invocation.getArgs();
MappedStatement ms = (MappedStatement) objects[0];
// 读方法
if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
DynamicDataSource.name.set("R");
} else {
// 写方法
DynamicDataSource.name.set("W");
}
// 修改当前线程要选择的数据源的key
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
}
}
WDataSourceConfig.java:
@MapperScan(basePackages = "com.tuling.dynamic.datasource.mapper.w", sqlSessionFactoryRef = "wSqlSessionFactory")
public class WDataSourceConfig {
@Bean
@Primary
public SqlSessionFactory wSqlSessionFactory(@Qualifier("dataSource1") DataSource dataSource1) throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource1);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/w/*.xml"));
/*主库设置sql控制台打印*/
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setLogImpl(StdOutImpl.class);
sessionFactory.setConfiguration(configuration);
return sessionFactory.getObject();
}
}
RDataSourceConfig.java:
@MapperScan(basePackages = "com.tuling.dynamic.datasource.mapper.r", sqlSessionFactoryRef = "rSqlSessionFactory")
public class RMyBatisConfig {
@Bean
public SqlSessionFactory rSqlSessionFactory(@Qualifier("dataSource2") DataSource dataSource2) throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource2);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/r/*.xml"));
/*从库设置sql控制台打印*/
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setLogImpl(StdOutImpl.class);
sessionFactory.setConfiguration(configuration);
return sessionFactory.getObject();
}
}
👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~