在很多具体应用场景的时候,我们需要用到动态数据源的情况,比如多租户的场景,系统登录时需要根据用户信息切换到用户对应的数据库。又比如业务A要访问A数据库,业务B要访问B数据库等,都可以使用动态数据源方案进行解决。接下来,我们就来讲解如何实现动态数据源,以及在过程中剖析动态数据源背后的实现原理。
实现案例本教程案例基于 Spring Boot + Mybatis + MySQL 实现。
数据库设计首先需要安装好MySQL数据库,新建数据库 master,slave,分别创建用户表,用来测试数据源,SQL脚本如下。
-- ---------------------------------------------------- -- 用户-- ---------------------------------------------------- -- Table structure for `sys_user`-- ----------------------------------------------------DROp TABLE IF EXISTS `sys_user`; CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', `name` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) COMMENT '密码', `salt` varchar(40) COMMENT '盐', `email` varchar(100) COMMENT '邮箱', `mobile` varchar(100) COMMENT '手机号', `status` tinyint COMMENT '状态 0:禁用 1:正常', `dept_id` bigint(20) COMMENT '机构ID', `create_by` varchar(50) COMMENT '创建人', `create_time` datetime COMMENT '创建时间', `last_update_by` varchar(50) COMMENT '更新人', `last_update_time` datetime COMMENT '更新时间', `del_flag` tinyint DEFAULT 0 COMMENT '是否删除 -1:已删除 0:正常', PRIMARY KEY (`id`), UNIQUE INDEX (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户';新建工程
新建一个Spring Boot工程,最终代码结构如下。
添加依赖
添加Spring Boot,Spring Aop,Mybatis,MySQL,Swagger相关依赖。Swagger方便用来测试接口。
pom.xml
配置文件4.0.0 top.ivan.demo springboot-dynamic-datasource0.0.1-SNAPSHOT jar springboot-dynamic-datasource Demo project for Spring Boot org.springframework.boot spring-boot-starter-parent2.0.4.RELEASE UTF-8 UTF-8 1.8 1.3.2 2.8.0 org.springframework.boot spring-boot-starter-weborg.springframework.boot spring-boot-starter-testtest org.springframework.boot spring-boot-starter-aoporg.mybatis.spring.boot mybatis-spring-boot-starter${mybatis.spring.version} mysql mysql-connector-javaio.springfox springfox-swagger2${swagger.version} io.springfox springfox-swagger-ui${swagger.version} org.springframework.boot spring-boot-maven-plugin
修改配置文件,添加两个数据源,可以是同一个主机地址的两个数据库master,slave,也可是两个不同主机的地址,根据实际情况配置。
application.yml
spring: datasource: master: driver-class-name: com.mysql.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource jdbcUrl: jdbc:mysql://127.0.0.1:3306/master?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8 username: root password: 123 slave: driver-class-name: com.mysql.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource jdbcUrl: jdbc:mysql://127.0.0.1:3306/slave?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8 username: root password: 123启动类
启动类添加 exclude = {DataSourceAutoConfiguration.class}, 以禁用数据源默认自动配置。
数据源默认自动配置会读取 spring.datasource.* 的属性创建数据源,所以要禁用以进行定制。
@ComponentScan(basePackages = "com.louis.springboot") 是扫描范围,都知道不用多说。
DynamicDatasourceApplication.java
package com.louis.springboot.dynamic.datasource;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;import org.springframework.context.annotation.ComponentScan;@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) // 禁用数据源自动配置@ComponentScan(basePackages = "com.louis.springboot")public class DynamicDatasourceApplication { public static void main(String[] args) {
SpringApplication.run(DynamicDatasourceApplication.class, args);
}
}数据源配置类创建一个数据源配置类,主要做以下几件事情:
1. 配置 dao,model,xml mapper文件的扫描路径。
2. 注入数据源配置属性,创建master、slave数据源。
3. 创建一个动态数据源,并装入master、slave数据源。
4. 将动态数据源设置到SQL会话工厂和事务管理器。
如此,当进行数据库操作时,就会通过我们创建的动态数据源去获取要操作的数据源了。
package com.louis.springboot.dynamic.datasource.config;import java.util.HashMap;import java.util.Map;import javax.sql.DataSource;import org.mybatis.spring.SqlSessionFactoryBean;import org.mybatis.spring.annotation.MapperScan;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.boot.jdbc.DataSourceBuilder;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import org.springframework.core.io.support.PathMatchingResourcePatternResolver;import org.springframework.jdbc.datasource.DataSourceTransactionManager;import org.springframework.transaction.PlatformTransactionManager;import com.louis.springboot.dynamic.datasource.dds.DynamicDataSource;@Configuration
@MapperScan(basePackages = {"com.louis.**.dao"}) // 扫描DAOpublic class MybatisConfig {
@Bean("master")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.master") public DataSource master() { return DataSourceBuilder.create().build();
}
@Bean("slave")
@ConfigurationProperties(prefix = "spring.datasource.slave") public DataSource slave() { return DataSourceBuilder.create().build();
}
@Bean("dynamicDataSource") public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map数据源上下文动态数据源的切换主要是通过调用这个类的方法来完成的。在任何想要进行切换数据源的时候都可以通过调用这个类的方法实现切换。比如系统登录时,根据用户信息调用这个类的数据源切换方法切换到用户对应的数据库。
主要方法介绍:
1. 切换数据源
在任何想要进行切换数据源的时候都可以通过调用这个类的方法实现切换。
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}2. 重置数据源
将数据源重置回默认的数据源。默认数据源通过 DynamicDataSource.setDefaultDataSource(ds) 进行设置。
public static void clearDataSourceKey() {
contextHolder.remove();
}3. 获取当前数据源key
public static String getDataSourceKey() { return contextHolder.get();
}完整代码如下
DynamicDataSourceContextHolder.java
package com.louis.springboot.dynamic.datasource.dds;import java.util.ArrayList;import java.util.Collection;import java.util.List;public class DynamicDataSourceContextHolder { private static final ThreadLocal contextHolder = new ThreadLocal() {
@Override protected String initialValue() { return "master";
}
};
public static List dataSourceKeys = new ArrayList<>();
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}
public static String getDataSourceKey() { return contextHolder.get();
}
public static void clearDataSourceKey() {
contextHolder.remove();
}
public static boolean containDataSourceKey(String key) { return dataSourceKeys.contains(key);
}
public static boolean addDataSourceKeys(Collection extends Object> keys) { return dataSourceKeys.addAll(keys);
}
} 注解式数据源到这里,在任何想要动态切换数据源的时候,只要调用 DynamicDataSourceContextHolder.setDataSourceKey(key) 就可以完成了。
接下来我们实现通过注解的方式来进行数据源的切换,原理就是添加注解(如@DataSource(value="master")),然后实现注解切面进行数据源切换。
创建一个动态数据源注解,拥有一个value值,用于标识要切换的数据源的key。
DataSource.java
package com.louis.springboot.dynamic.datasource.dds;import java.lang.annotation.documented;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@documentedpublic @interface DataSource {
String value();
}创建一个AOP切面,拦截带 @DataSource 注解的方法,在方法执行前切换至目标数据源,执行完成后恢复到默认数据源。
DynamicDataSourceAspect.java
package com.louis.springboot.dynamic.datasource.dds;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.After;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.springframework.core.annotation.Order;import org.springframework.stereotype.Component;@Aspect
@Order(-1) // 该切面应当先于 @Transactional 执行@Componentpublic class DynamicDataSourceAspect {
@Before("@annotation(dataSource))") public void switchDataSource(JoinPoint point, DataSource dataSource) { if (!DynamicDataSourceContextHolder.containDataSourceKey(dataSource.value())) {
System.out.println("DataSource [{}] doesn't exist, use default DataSource [{}] " + dataSource.value());
} else { // 切换数据源 DynamicDataSourceContextHolder.setDataSourceKey(dataSource.value());
System.out.println("Switch DataSource to [" + DynamicDataSourceContextHolder.getDataSourceKey() + "] in Method [" + point.getSignature() + "]");
}
}
@After("@annotation(dataSource))") public void restoreDataSource(JoinPoint point, DataSource dataSource) { // 将数据源置为默认数据源 DynamicDataSourceContextHolder.clearDataSourceKey();
System.out.println("Restore DataSource to [" + DynamicDataSourceContextHolder.getDataSourceKey()
+ "] in Method [" + point.getSignature() + "]");
}
}到这里,动态数据源相关的处理代码就完成了。
编写用户业务代码接下来编写用户查询业务代码,用来进行测试,只需添加一个查询接口即可。
编写一个控制器,包含两个查询方法,分别注解 master 和 slave 数据源。
SysUserController.java
package com.louis.springboot.dynamic.datasource.controller;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import com.louis.springboot.dynamic.datasource.dds.DataSource;import com.louis.springboot.dynamic.datasource.service.SysUserService;@RestController
@RequestMapping("user")public class SysUserController {
@Autowired private SysUserService sysUserService;
@DataSource(value="master")
@PostMapping(value="/findAll") public Object findAll() { return sysUserService.findAll();
}
@DataSource(value="slave")
@PostMapping(value="/findAll2") public Object findAll2() { return sysUserService.findAll();
}
}
下面是正常的业务代码,没有什么好说明的,直接贴代码了。
SysUser.java
public class SysUser { private Long id;
private String name; private String password; private String salt; private String email; private String mobile; private Byte status; private Long deptId;
private String deptName;
private Byte delFlag;
private String createBy; private Date createTime; private String lastUpdateBy; private Date lastUpdateTime;
...setter and getter
}
SysUserMapper.java
package com.louis.springboot.dynamic.datasource.dao;import java.util.List;import com.louis.springboot.dynamic.datasource.model.SysUser;public interface SysUserMapper { int deleteByPrimaryKey(Long id); int insert(SysUser record); int insertSelective(SysUser record);
SysUser selectByPrimaryKey(Long id); int updateByPrimaryKeySelective(SysUser record); int updateByPrimaryKey(SysUser record);
List findAll();
}
SysUserMapper.xml
id, name, password, salt, email, mobile, status, dept_id, create_by, create_time, last_update_by, last_update_time, del_flag
SysUserService.java
package com.louis.springboot.dynamic.datasource.service;import java.util.List;import com.louis.springboot.dynamic.datasource.model.SysUser;public interface SysUserService {
List findAll();
}
SysUserServiceImpl.java
package com.louis.springboot.dynamic.datasource.service.impl;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import com.louis.springboot.dynamic.datasource.dao.SysUserMapper;import com.louis.springboot.dynamic.datasource.model.SysUser;import com.louis.springboot.dynamic.datasource.service.SysUserService;
@Servicepublic class SysUserServiceImpl implements SysUserService {
@Autowired private SysUserMapper sysUserMapper;
public List findAll() { return sysUserMapper.findAll();
}
} 测试效果启动系统,访问 http://localhost:8080/swagger-ui.html,分别测试两个接口,成功返回数据。
user/findAll (master数据源)
user/findAll2 (slave数据源)
源码下载
码云:https://gitee.com/liuge1988/spring-boot-demo.git
作者:朝雨忆轻尘
出处:https://www.cnblogs.com/xifengxiaoma/



