本章是一个完整的 Spring Boot 动态数据源切换示例,例如主数据库使用 lionsea 从数据库 lionsea_slave1、lionsea_slave2。只需要在对应的代码上使用 DataSource("slave1") 注解来实现数据库切换。

想要实现数据源动态切换,需要用到以下知识

  1. spring boot 中自定义注解
  2. spring boot 中的 aop 拦截
  3. mybatis 的增删改查操作

本项目源码 github 下载

1 新建 Spring Boot Maven 示例工程项目

注意:是用来 IDEA 开发工具

  1. File > New > Project,如下图选择 Spring Initializr 然后点击 【Next】下一步
  2. 填写 GroupId(包名)、Artifact(项目名) 即可。点击 下一步

    groupId=com.fishpro

    artifactId=dynamicdb
  3. 选择依赖 Spring Web Starter 前面打钩。
  4. 项目名设置为 spring-boot-study-dynamicdb.

2 依赖引入 Pom

3 动态数据源切换

3.1 新建多数据源注解 DataSource

文件路径(spring-boot-study/spring-boot-study-dynamicdb/src/main/java/com/fishpro/dynamicdb/datasource/annotation/DataSource.java)


/**
* 多数据源注解
* 在方法名上加入 DataSource('名称')
*
* @author fishpro
* */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {
String value() default "";
}

3.2 新建一个多数据源上下文切换 DynamicContextHolder

/**
* 多数据源上下文
*
*/
public class DynamicContextHolder {
@SuppressWarnings("unchecked")
private static final ThreadLocal<Deque<String>> CONTEXT_HOLDER = new ThreadLocal() {
@Override
protected Object initialValue() {
return new ArrayDeque();
}
}; /**
* 获得当前线程数据源
*
* @return 数据源名称
*/
public static String getDataSource() {
return CONTEXT_HOLDER.get().peek();
} /**
* 设置当前线程数据源
*
* @param dataSource 数据源名称
*/
public static void setDataSource(String dataSource) {
CONTEXT_HOLDER.get().push(dataSource);
} /**
* 清空当前线程数据源
*/
public static void clearDataSource() {
Deque<String> deque = CONTEXT_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
CONTEXT_HOLDER.remove();
}
} }

3.3 新建一个多数据源切面处理类

新建一个多数据源切面处理类指定Aop处理的注解点,和处理的事件(切换数据源),至此多数据源切换的主要工作就完成了。


/**
* 多数据源,切面处理类
*
*/
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DataSourceAspect {
protected Logger logger = LoggerFactory.getLogger(getClass()); /**
* 切面点 指定注解
* */
@Pointcut("@annotation(com.fishpro.dynamicdb.datasource.annotation.DataSource) " +
"|| @within(com.fishpro.dynamicdb.datasource.annotation.DataSource)")
public void dataSourcePointCut() { } /**
* 拦截方法指定为 dataSourcePointCut
* */
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Class targetClass = point.getTarget().getClass();
Method method = signature.getMethod(); DataSource targetDataSource = (DataSource)targetClass.getAnnotation(DataSource.class);
DataSource methodDataSource = method.getAnnotation(DataSource.class);
if(targetDataSource != null || methodDataSource != null){
String value;
if(methodDataSource != null){
value = methodDataSource.value();
}else {
value = targetDataSource.value();
} DynamicContextHolder.setDataSource(value);
logger.debug("set datasource is {}", value);
} try {
return point.proceed();
} finally {
DynamicContextHolder.clearDataSource();
logger.debug("clean datasource");
}
}
}

3.4 切换数据源

当Aop方法拦截到了带有注解 @DataSource 的方法的是,需要去执行指定的数据源,那么如何执行呢,这里我们使用阿里的 druid 链接池作为数据源连接池。这就要求我们需要对连接池进行一个可定制化的开发。程序安装Aop拦截到的信息去重新设定数据库路由,实现动态切换数据源的目标。

3.4.1 定义链接池的属性

在application.yml中的配置节点

spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123
initial-size: 10
max-active: 100
min-idle: 10
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
#Oracle需要打开注释
#validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
stat-view-servlet:
enabled: true
url-pattern: /druid/*
#login-username: admin
#login-password: admin
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: false
wall:
config:
multi-statement-allow: true

/**
* 多数据源主数据源属性
*
*/
public class DataSourceProperties {
private String driverClassName;
private String url;
private String username;
private String password; /**
* Druid默认参数
*/
private int initialSize = 2;
private int maxActive = 10;
private int minIdle = -1;
private long maxWait = 60 * 1000L;
private long timeBetweenEvictionRunsMillis = 60 * 1000L;
private long minEvictableIdleTimeMillis = 1000L * 60L * 30L;
private long maxEvictableIdleTimeMillis = 1000L * 60L * 60L * 7;
private String validationQuery = "select 1";
private int validationQueryTimeout = -1;
private boolean testOnBorrow = false;
private boolean testOnReturn = false;
private boolean testWhileIdle = true;
private boolean poolPreparedStatements = false;
private int maxOpenPreparedStatements = -1;
private boolean sharePreparedStatements = false;
private String filters = "stat,wall"; /* 省略自动化生成部分 */
}

3.4.2 多数据源从数据源属性类

在application.xml表示为,支持多数据库

dynamic:
datasource:
#slave1 slave2 数据源已测试
slave1:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/lionsea_slave1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
slave2:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/lionsea_slave2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
slave3:
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
url: jdbc:sqlserver://localhost:1433;DatabaseName=renren_security
username: sa
password: 123456
slave4:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/renren_security
username: renren
password: 123456

多数据源从数据源属性类,在application中表示为以 dynamic 为节点的配置

/**
* 多数据源属性 在application中表示为以 dynamic 为节点的配置
*
*/
@ConfigurationProperties(prefix = "dynamic")
public class DynamicDataSourceProperties {
private Map<String, DataSourceProperties> datasource = new LinkedHashMap<>(); public Map<String, DataSourceProperties> getDatasource() {
return datasource;
} public void setDatasource(Map<String, DataSourceProperties> datasource) {
this.datasource = datasource;
}
}

3.4.3 建立动态数据源工厂类

建立动态数据源工厂类用于创建动态数据源连接池 Druid

/**
* DruidDataSource
*
*/
public class DynamicDataSourceFactory { /**
* 通过自定义建立 Druid的数据源
* */
public static DruidDataSource buildDruidDataSource(DataSourceProperties properties) {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(properties.getDriverClassName());
druidDataSource.setUrl(properties.getUrl());
druidDataSource.setUsername(properties.getUsername());
druidDataSource.setPassword(properties.getPassword()); druidDataSource.setInitialSize(properties.getInitialSize());
druidDataSource.setMaxActive(properties.getMaxActive());
druidDataSource.setMinIdle(properties.getMinIdle());
druidDataSource.setMaxWait(properties.getMaxWait());
druidDataSource.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis());
druidDataSource.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis());
druidDataSource.setMaxEvictableIdleTimeMillis(properties.getMaxEvictableIdleTimeMillis());
druidDataSource.setValidationQuery(properties.getValidationQuery());
druidDataSource.setValidationQueryTimeout(properties.getValidationQueryTimeout());
druidDataSource.setTestOnBorrow(properties.isTestOnBorrow());
druidDataSource.setTestOnReturn(properties.isTestOnReturn());
druidDataSource.setPoolPreparedStatements(properties.isPoolPreparedStatements());
druidDataSource.setMaxOpenPreparedStatements(properties.getMaxOpenPreparedStatements());
druidDataSource.setSharePreparedStatements(properties.isSharePreparedStatements()); try {
druidDataSource.setFilters(properties.getFilters());
druidDataSource.init();
} catch (SQLException e) {
e.printStackTrace();
}
return druidDataSource;
}
}

3.4.3 配置多数据源配置类

/**
* 配置多数据源
*
*/
@Configuration
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceConfig {
@Autowired
private DynamicDataSourceProperties properties; @Bean
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
} @Bean
public DynamicDataSource dynamicDataSource(DataSourceProperties dataSourceProperties) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(getDynamicDataSource()); //默认数据源
DruidDataSource defaultDataSource = DynamicDataSourceFactory.buildDruidDataSource(dataSourceProperties);
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource); return dynamicDataSource;
} private Map<Object, Object> getDynamicDataSource(){
Map<String, DataSourceProperties> dataSourcePropertiesMap = properties.getDatasource();
Map<Object, Object> targetDataSources = new HashMap<>(dataSourcePropertiesMap.size());
dataSourcePropertiesMap.forEach((k, v) -> {
DruidDataSource druidDataSource = DynamicDataSourceFactory.buildDruidDataSource(v);
targetDataSources.put(k, druidDataSource);
}); return targetDataSources;
} }

3.5 测试多数据源

使用 Spring Boot 的测试类进行测试,建立一个基于 Mybatis 的 CRUD。

3.5.1 新建三个数据一个主库,2个从库

新建三个数据一个主库,2个从库,每个数据库都新建下面的表

DROP TABLE IF EXISTS `demo_test`;
CREATE TABLE `demo_test` (
`id` bigint(20) NOT NULL,
`name` varchar(255) NOT NULL,
`status` tinyint(4) DEFAULT NULL,
`is_deleted` tinyint(4) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`create_user_id` bigint(20) DEFAULT NULL,
`age` bigint(20) DEFAULT NULL,
`content` text,
`body` longtext,
`title` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`) USING BTREE,
KEY `idx_title` (`title`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

3.5.2 建立一个基于 Mybatis 的 CRUD

这里主要使用代码生成器生产一个增删改查(也可以手动)主要包括了 dao/domain/service/impl

/domain/DemoTestDO.java

public class DemoTestDO implements Serializable {
private static final long serialVersionUID = 1L; //
private Long id;
//
private String name;
//
private Integer status;
//
private Integer isDeleted;
//
private Date createTime;
//
private Long createUserId;
//
private Long age;
//
private String content;
//
private String body;
//
private String title;
//省略自动生成的部分 }

/dao/DemoTestDao.java

@Mapper
public interface DemoTestDao { DemoTestDO get(Long id); List<DemoTestDO> list(Map<String, Object> map); int count(Map<String, Object> map); int save(DemoTestDO demoTest); int update(DemoTestDO demoTest); int remove(Long id); int batchRemove(Long[] ids);
}

具体见 本项目源码 github 下载

3.5.3 编写测试代码

动态代码测试方法类

@Service
//@DataSource("slave1")
public class DynamicDataSourceTestService {
@Autowired
private DemoTestDao demoTestDao; @Transactional
public void updateDemoTest(Long id){
DemoTestDO user = new DemoTestDO();
user.setId(id);
user.setTitle("13500000000");
demoTestDao.update(user);
} @Transactional
@DataSource("slave1")
public void updateDemoTestBySlave1(Long id){
DemoTestDO user = new DemoTestDO();
user.setId(id);
user.setTitle("13500000001");
demoTestDao.update(user);
} @DataSource("slave2")
@Transactional
public void updateDemoTestBySlave2(Long id){
DemoTestDO user = new DemoTestDO();
user.setId(id);
user.setTitle("13500000002");
demoTestDao.update(user); //测试事物
// int i = 1/0;
}
}

动态测试代码

@RunWith(SpringRunner.class)
@SpringBootTest
public class DynamicdbApplicationTests { @Autowired
private DynamicDataSourceTestService dynamicDataSourceTestService; /**
* 观察三个数据源中的数据是否正确
* */
@Test
public void testDaynamicDataSource(){
Long id = 1L; dynamicDataSourceTestService.updateDemoTest(id);
dynamicDataSourceTestService.updateDemoTestBySlave1(id);
dynamicDataSourceTestService.updateDemoTestBySlave2(id);
} }

3.6 测试

右键 DynamicDataSourceTest 执行 Run DynamicDataSourceTest

默认数据库操作成功
slave1数据库操作成功
slave2数据库操作成功 Process finished with exit code 0

Spring Boot 如何动态切换数据源的更多相关文章

  1. SSM动态切换数据源

    有需求就要想办法解决,最近参与的项目其涉及的三个数据表分别在三台不同的服务器上,这就有点突兀了,第一次遇到这种情况,可这难不倒笔者,资料一查,代码一打,回头看看源码,万事大吉 1. 预备知识 这里默认 ...

  2. 在使用 Spring Boot 和 MyBatis 动态切换数据源时遇到的问题以及解决方法

    相关项目地址:https://github.com/helloworlde/SpringBoot-DynamicDataSource 1. org.apache.ibatis.binding.Bind ...

  3. Spring AOP动态切换数据源

    现在稍微复杂一点的项目,一个数据库也可能搞不定,可能还涉及分布式事务什么的,不过由于现在我只是做一个接口集成的项目,所以分布式就先不用了,用Spring AOP来达到切换数据源,查询不同的数据库就可以 ...

  4. Spring + Mybatis 项目实现动态切换数据源

    项目背景:项目开发中数据库使用了读写分离,所有查询语句走从库,除此之外走主库. 最简单的办法其实就是建两个包,把之前数据源那一套配置copy一份,指向另外的包,但是这样扩展很有限,所有采用下面的办法. ...

  5. Spring+Mybatis动态切换数据源

    功能需求是公司要做一个大的运营平台: 1.运营平台有自身的数据库,维护用户.角色.菜单.部分以及权限等基本功能. 2.运营平台还需要提供其他不同服务(服务A,服务B)的后台运营,服务A.服务B的数据库 ...

  6. Spring动态切换数据源及事务

    前段时间花了几天来解决公司框架ssm上事务问题.如果不动态切换数据源话,直接使用spring的事务配置,是完全没有问题的.由于框架用于各个项目的快速搭建,少去配置各个数据源配置xml文件等.采用了动态 ...

  7. Spring学习总结(16)——Spring AOP实现执行数据库操作前根据业务来动态切换数据源

    深刻讨论为什么要读写分离? 为了服务器承载更多的用户?提升了网站的响应速度?分摊数据库服务器的压力?就是为了双机热备又不想浪费备份服务器?上面这些回答,我认为都不是错误的,但也都不是完全正确的.「读写 ...

  8. Spring Boot集成Mybatis双数据源

    这里用到了Spring Boot + Mybatis + DynamicDataSource配置动态双数据源,可以动态切换数据源实现数据库的读写分离. 添加依赖 加入Mybatis启动器,这里添加了D ...

  9. spring、spring boot中配置多数据源

    在项目开发的过程中,有时我们有这样的需求,需要去调用别的系统中的数据,那么这个时候系统中就存在多个数据源了,那么我们如何来解决程序在运行的过程中到底是使用的那个数据源呢? 假设我们系统中存在2个数据源 ...

随机推荐

  1. 函数的应用_python

    一.各类函数应用 1.高阶函数:函数里面调用函数 例子: def add(a): return (a*a) def print_info(fun,c,d): #传入函数与参数 return fun(c ...

  2. Suggestions On Setting LED Holiday Light

    We all like the cheerful glow of holiday lights, so the process goes seamless from start to finish. ...

  3. JDK的安装与环境配置(Windows10)

    1.安装JDK首先需要到官网下载jdk,下载地址为:https://www.oracle.com/technetwork/java/javase/downloads/index.html 目前官方已经 ...

  4. docker命令总结(一)

    个人简单总结: 参数 用途 语法 示例 search 在docker hub中搜索镜像 docker search 镜像名称 docker search nginx pull 在docker hub中 ...

  5. MySQL加号+ 的作用

    案例:查询员工名和员工姓,连接成一个字段,并显示为: 姓名 SELECT last_name+first_name AS 姓名 FROM employees;没有报错但姓名一下全是0 Java中的 + ...

  6. Git的基本使用 -- 分支管理

    查看分支 git branch 前面带 * 的为当前所在分支 创建分支 git branch 分支名 切换分支 git checkout 分支名 创建并切换到此分支 git checkout -b 分 ...

  7. php/js将 CST时间转成格式化时间

    PHP :比较简单 $str = 'Wed Jul 24 11:24:33 CST 2019'; echo date('Y-m-d H:i:s', strtotime($str)); echo dat ...

  8. Apache Kafka(六)- High Throughput Producer

    High Throughput Producer 在有大量消息需要发送的情况下,默认的Kafka Producer配置可能无法达到一个可观的的吞吐.在这种情况下,我们可以考虑调整两个方面,以提高Pro ...

  9. Iris_MVC

    2. MVC包使用 在Iris框架中,封装了mvc包作为对mvc架构的支持,方便开发者遵循mvc的开发原则进行开发. iris框架支持请求数据.模型.持久数据分层处理,并支持各层级模块代码绑定执行. ...

  10. 7_6 带宽(UVa140)<回溯法:最优性剪枝>

    给定一个图(V,E),其中V为顶点的集合,E为边的集合,属于VxV.给定V中元素的一种排序,那么顶点v的带宽定义如下:在当前给定的排序中,与v距离最远的且与v有边相连的顶点与v的距离.给定排序的带宽定 ...