【Java多数据源实现教程】实现动态数据源、多数据源切换方式(上)

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
云数据库 RDS MySQL Serverless,价值2615元额度,1个月
简介: 【Java多数据源实现教程】实现动态数据源、多数据源切换方式(上)

一、多数据源的典型使用场景


在实际开发中,经常可能遇到在一个应用中可能需要访问多个数据库的情况。以下是两种典型场景:


1️⃣业务复杂(数据量大)


数据分布在不同的数据库中,数据库拆了, 应用没拆。 一个公司多个子项目,各用各的数据库,涉及数据共享…


2c7cf15f69434cc0b5f0dc038eed23cf.png

2️⃣读写分离


为了解决数据库的读性能瓶颈(读比写性能更高, 写锁会影响读阻塞,从而影响读的性能)。

很多数据库拥主从架构。也就是,一台主数据库服务器,是对外提供增删改业务的生产服务器;另一(多)台从数据库服务器,主要进行读的操作。ꞏ

可以通过中间件(ShardingSphere、mycat、mysql-proxy 、TDDL …),但是有一些规模较小的公司,没有专门的中间件团队搭建读写分离基础设施,因此需要业务开发人员自行实现读写分离。


d00d17b116f94998abc9c3ef814c1916.png


这里的架构与上图类似。不同的是,在读写分离中,主库和从库的数据库是一致的(不考虑主从延迟)。数据更新操作(insert、update、delete)都是在主库上进行,主库将数据变更信息同步给从库。在查询时,可以在从库上进行,从而分担主库的压力。


二、多数据源实现原理及实现方法


原理:


对于大多数的java应用,都使用了spring框架,spring-jdbc模块提供AbstractRoutingDataSource,其内部可以包含了多个DataSource,然后在运行时来动态的访问哪个数据库。这种方式访问数据库的架构图如下所示:


3de22e7515f241dfa7c49bdb062dd0f8.png

应用直接操作的是AbstractRoutingDataSource的实现类,告诉AbstractRoutingDataSource访问哪个数据库,然后由AbstractRoutingDataSource从事先配置好的数据源(ds1、ds2)选择一个,来访问对应的数据库。

a9a3db5c62b941668f9def44a31a6ca2.png


(1)当执行数据库持久化操作,只要集成了Spring就一定会通过DataSourceUtils获取Connection

(2)通过Spring注入的DataSource获取Connection即可执行数据库操作。所以思路就是:只需配置一个实现了DataSource的Bean, 然后根据业务动态提供Connection即可

(3)其实Spring已经提供一个DataSource实现类用于动态切换数据源——AbstractRoutingDataSource

(4)分析AbstractRoutingDataSource即可实现动态数据源切换。


1️⃣通过AbstractRoutingDataSource实现动态数据源


通过这个类可以实现动态数据源切换。如下是这个类的成员变量:

private Map<Object, Object> targetDataSources;
private Object defaultTargetDataSource;
private Map<Object, DataSource> resolvedDataSources;


targetDataSources保存了key和数据库连接的映射关系

defaultTargetDataSource标识默认的连接

resolvedDataSources这个数据结构是通过targetDataSources构建而来,存储结构也是数据库标识和数据源的映射关系

而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件大事:


(1)定义AbstractRoutingDataSource实现类DynamicDataSource

(2)初始化时为targetDataSources设置不同数据源的DataSource和标识、及defaultTargetDataSource

(3)在determineTargetDataSource中提供对应的数据源标识即可

(4)切换数据源标识即可

什么到这还不会? 附上代码:


🍀(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();
    }
}

2️⃣多数据源切换方式


多数据源切换方式需要根据我们的具体需求进行选择:


🍀(1)AOP+自定义注解


用于不同业务的数据源: 一般利用AOP,结合自定义注解动态切换数据源


  • (1)自定义注解
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface WR {
    String value() default "W";
}

(2)切面类

@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());
    }
}

(3)使用注解

@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) {
    }
}

3️⃣Spring集成多个MyBatis框架实现多数据源


11da9df9f1ca4028b60f8481da47f9ce.png


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


相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
4天前
|
Web App开发 JavaScript 前端开发
《手把手教你》系列技巧篇(三十九)-java+ selenium自动化测试-JavaScript的调用执行-上篇(详解教程)
【5月更文挑战第3天】本文介绍了如何在Web自动化测试中使用JavaScript执行器(JavascriptExecutor)来完成Selenium API无法处理的任务。首先,需要将WebDriver转换为JavascriptExecutor对象,然后通过executeScript方法执行JavaScript代码。示例用法包括设置JS代码字符串并调用executeScript。文章提供了两个实战场景:一是当时间插件限制输入时,用JS去除元素的readonly属性;二是处理需滚动才能显示的元素,利用JS滚动页面。还给出了一个滚动到底部的代码示例,并提供了详细步骤和解释。
28 10
|
11天前
|
Java 测试技术 Python
《手把手教你》系列技巧篇(三十六)-java+ selenium自动化测试-单选和多选按钮操作-番外篇(详解教程)
【4月更文挑战第28天】本文简要介绍了自动化测试的实战应用,通过一个在线问卷调查(&lt;https://www.sojump.com/m/2792226.aspx/&gt;)为例,展示了如何遍历并点击问卷中的选项。测试思路包括找到单选和多选按钮的共性以定位元素,然后使用for循环进行点击操作。代码设计方面,提供了Java+Selenium的示例代码,通过WebDriver实现自动答题。运行代码后,可以看到控制台输出和浏览器的相应动作。文章最后做了简单的小结,强调了本次实践是对之前单选多选操作的巩固。
24 0
|
1天前
|
前端开发 Java 测试技术
《手把手教你》系列技巧篇(四十二)-java+ selenium自动化测试 - 处理iframe -下篇(详解教程)
【5月更文挑战第6天】本文介绍了如何使用Selenium处理含有iframe的网页。作者首先解释了iframe是什么,即HTML中的一个框架,用于在一个页面中嵌入另一个页面。接着,通过一个实战例子展示了在QQ邮箱登录页面中,由于输入框存在于iframe内,导致直接定位元素失败。作者提供了三种方法来处理这种情况:1)通过id或name属性切换到iframe;2)使用webElement对象切换;3)通过索引切换。最后,给出了相应的Java代码示例,并提醒读者根据iframe的实际情况选择合适的方法进行切换和元素定位。
7 0
|
1天前
|
Oracle 关系型数据库 Java
java操作多数据源将oracle数据同步达梦数据库
java操作多数据源将oracle数据同步达梦数据库
|
2天前
|
前端开发 测试技术 Python
《手把手教你》系列技巧篇(四十一)-java+ selenium自动化测试 - 处理iframe -上篇(详解教程)
【5月更文挑战第5天】本文介绍了HTML中的`iframe`标签,它用于在网页中嵌套其他网页。`iframe`常用于加载外部内容或网站的某个部分,以实现页面美观。文章还讲述了使用Selenium自动化测试时如何处理`iframe`,通过`switchTo().frame()`方法进入`iframe`,完成相应操作,然后使用`switchTo().defaultContent()`返回主窗口。此外,文章提供了一个包含`iframe`的HTML代码示例,并给出了一个简单的自动化测试代码实战,演示了如何在`iframe`中输入文本。
14 3
|
3天前
|
JavaScript 前端开发 Java
《手把手教你》系列技巧篇(四十)-java+ selenium自动化测试-JavaScript的调用执行-下篇(详解教程)
【5月更文挑战第4天】本文介绍了如何使用JavaScriptExecutor在自动化测试中实现元素高亮显示。通过创建并执行JS代码,可以改变元素的样式,例如设置背景色和边框,以突出显示被操作的元素。文中提供了一个Java示例,展示了如何在Selenium中使用此方法,并附有代码截图和运行效果展示。该技术有助于跟踪和理解测试过程中的元素交互。
8 0
|
5天前
|
JavaScript 前端开发 测试技术
《手把手教你》系列技巧篇(三十八)-java+ selenium自动化测试-日历时间控件-下篇(详解教程)
【5月更文挑战第2天】在自动化测试过程中,经常会遇到处理日期控件的点击问题。宏哥之前分享过一种方法,但如果输入框是`readonly`属性,这种方法就无法奏效了。不过,通过修改元素属性,依然可以实现自动化填写日期。首先,定位到日期输入框并移除`readonly`属性,然后使用`sendKeys`方法输入日期。这样,即使输入框设置了`readonly`,也能成功处理日期控件。
24 1
|
6天前
|
Java 测试技术 Python
《手把手教你》系列技巧篇(三十七)-java+ selenium自动化测试-日历时间控件-上篇(详解教程)
【5月更文挑战第1天】该文介绍了使用Selenium自动化测试网页日历控件的方法。首先,文章提到在某些Web应用中,日历控件常用于选择日期并筛选数据。接着,它提供了两个实现思路:一是将日历视为文本输入框,直接输入日期;二是模拟用户交互,逐步选择日期。文中给出了JQueryUI网站的一个示例,并展示了对应的Java代码实现,包括点击日历、选择日期等操作。
26 0
|
12天前
|
Java 测试技术 项目管理
Java基础教程(22)-构建工具Maven的基本使用
【4月更文挑战第22天】Maven是Java项目管理及构建工具,简化构建、测试、打包和部署等任务。遵循约定优于配置原则,核心是`pom.xml`配置文件,用于管理依赖和项目信息。安装涉及下载、解压、配置环境变量。在IDEA中使用Maven创建项目,通过`pom.xml`添加依赖和管理版本。常用命令包括`clean`、`compile`、`test`、`package`、`install`和`deploy`。IDEA支持直接执行这些命令。
|
12天前
|
NoSQL Java 关系型数据库
Java基础教程(21)-Java连接MongoDB
【4月更文挑战第21天】MongoDB是开源的NoSQL数据库,强调高性能和灵活性。Java应用通过MongoDB Java驱动与之交互,涉及MongoClient、MongoDatabase、MongoCollection和Document等组件。连接MongoDB的步骤包括:配置连接字符串、创建MongoClient、选择数据库和集合。伪代码示例展示了如何建立连接、插入和查询数据。