一、背景介绍
随着用户需求的不断变化,在很多的业务环境下,我们需要用到动态数据源,比如多租户的场景,简单的说就是,多个租户除了数据源不一样,服务代码和数据表结构完全一致。这个时候采用动态数据源方案,可以极大的简化服务工程量。
图片
在介绍动态数据源之前,我们先一起来看看多数据源在 Spring Boot 中的实现方式。
二、多数据源实现介绍
服务框架采用 Spring Boot + Mybatis + Druid 来实现数据库的访问和操作,数据库采用 Mysql 来存储和查询,程序环境如下:
- Mysql:5.7
- JDK:1.8
- Spring Boot:2.1.0
- Mybatis:3.5.0
- Druid:1.1.10
2.1、数据库准备
为了便于演示,在 Mysql 数据库中创建两个库,分别是db_test_1和db_test_2。
在db_test_1数据库中创建一张用户表,脚本如下:
CREATE TABLE`tb_user_info` (
`id`int(11) unsignedNOTNULL,
`user_name`varchar(50) DEFAULTNULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;
在db_test_2数据库中创建另一张账户表,脚本如下:
CREATE TABLE`tb_account_info` (
`id`int(11) unsignedNOTNULL,
`account_name`varchar(50) DEFAULTNULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;
创建完成之后,确保数据库的 IP、端口、用户和密码,能远程正常联通。
2.2、工程环境准备
以Spring Boot框架为基础,创建一个服务工程,并在pom.xml中添加相关的依赖包,示例如下:
<!--spring boot核心-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--spring boot 测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--mysql 驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<!--aspectj 注解代理-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
2.3、编写多数据源配置服务
Spring Boot 支持根据一定的规则来动态选择数据源,用户可以通过继承AbstractRoutingDataSource抽象类并重写determineCurrentLookupKey()方法来完成数据源的切换。
在每次执行数据库操作之前,它会先调用determineCurrentLookupKey()抽象方法,根据初始化时设置的数据源集合,通过其中的key来决定使用哪个数据源。
具体实现方式如下!
2.3.1、创建动态数据源服务类
首先,创建一个DynamicDataSource类,并继承AbstractRoutingDataSource抽象类,同时重写determineCurrentLookupKey()方法,代码示例如下:
package com.example.dynamic.datasource.config;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}
2.3.2、创建动态数据源缓存类
然后,创建一个DataSourceContextHolder类,用于缓存数据源,同时需要确保线程环境下安全,这里采用ThreadLocal线程本地变量来实现数据源key的缓存,代码示例如下:
package com.example.dynamic.datasource.config;
publicclass DataSourceContextHolder {
/**
* 设置线程独立变量,用于存储数据源唯一标记
*/
privatestaticfinal ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();
/**
* 设置数据源
* @param dataSourceName 数据源名称
*/
public static void set(String dataSourceName){
DATASOURCE_HOLDER.set(dataSourceName);
}
/**
* 获取当前线程的数据源
* @return 数据源名称
*/
public static String get(){
return DATASOURCE_HOLDER.get();
}
/**
* 删除当前数据源
*/
public static void remove(){
DATASOURCE_HOLDER.remove();
}
}
2.3.3、创建动态数据源配置类
接着,创建一个DataSourceConfig配置类,设置动态数据源相关的参数,并注入到 Bean 工厂,代码示例如下:
package com.example.dynamic.datasource.config;
@Configuration
publicclass DataSourceConfig {
@Bean(name = "db1")
@ConfigurationProperties(prefix = "spring.datasource.db1.druid")
public DataSource db1(){
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "db2")
@ConfigurationProperties(prefix = "spring.datasource.db2.druid")
public DataSource db2(){
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary
public DynamicDataSource createDynamicDataSource(){
// 配置数据源集合,其中key代表数据源名称,DataSourceContextHolder中缓存的就是这个key
Map<Object,Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("db1",db1());
dataSourceMap.put("db2",db2());
// 注入动态数据源到bean工厂
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 设置默认数据源
dynamicDataSource.setDefaultTargetDataSource(db1());
// 设置动态数据源集
dynamicDataSource.setTargetDataSources(dataSourceMap);
return dynamicDataSource;
}
}
2.3.4、编写相关配置变量
根据上面的配置变量,我们还需要在application.properties文件中添加相关的数据源变量,内容如下:
# 数据库源1
spring.datasource.db1.druid.url=jdbc:mysql://localhost:3306/db_test_1
spring.datasource.db1.druid.username=root
spring.datasource.db1.druid.password=root
spring.datasource.db1.druid.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.db1.druid.initial-size=1
spring.datasource.db1.druid.min-idle=1
spring.datasource.db1.druid.max-active=5
spring.datasource.db1.druid.max-wait=60000
# 数据库源2
spring.datasource.db2.druid.url=jdbc:mysql://localhost:3306/db_test_2
spring.datasource.db2.druid.username=root
spring.datasource.db2.druid.password=root
spring.datasource.db2.druid.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.db2.druid.initial-size=1
spring.datasource.db2.druid.min-idle=1
spring.datasource.db2.druid.max-active=5
spring.datasource.db2.druid.max-wait=60000
2.3.5、排除自动装配数据源
默认情况下,Spring Boot 启动的时候会自动加载数据源配置,因为我们没有按照约定配置指定的数据源参数,当启动服务的时候会报错。
因此,需要在注解@SpringBootApplication类上排除自动装配数据源配置,内容如下:
package com.example.dynamic.datasource;
@MapperScan("com.example.dynamic.datasource.mapper")
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2.3.6、业务相关服务类
最后基于上文数据库中创建的表,编写相关的entity、service和dao层代码,以便于后续验证数据源的切换操作。
以用户表的新增操作为例,代码如下:
package com.example.dynamic.datasource.service;
@Service
public class UserInfoService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional
public void insert(UserInfo entity){
userInfoMapper.insert(entity);
}
}
账户表的新增操作,代码如下:
package com.example.dynamic.datasource.service;
@Service
public class AccountInfoService {
@Autowired
private AccountInfoMapper accountInfoMapper;
@Transactional
public void insert(AccountInfo entity){
accountInfoMapper.insert(entity);
}
}
entity和dao层代码就不再贴进来了,比较简单。
2.4、编写单元测试
最后我们编写一个单元测试,验证一下代码的正确性,示例如下:
package com.example.dynamic.datasource.junit;
@RunWith(SpringRunner.class)
@SpringBootTest
public class DynamicDataSourceJunit {
@Autowired
private AccountInfoService accountInfoService;
@Autowired
private UserInfoService userInfoService;
@Test
public void testUserInfo(){
try {
// 1.设置需要切换的数据源
DataSourceContextHolder.set("db1");
// 2.执行插入操作
userInfoService.insert(new UserInfo(1, "张三"));
} finally {
// 3.操作完成之后,移除本地线程缓存的数据源
DataSourceContextHolder.remove();
}
}
@Test
public void testAccountInfo(){
try {
// 1.设置需要切换的数据源
DataSourceContextHolder.set("db2");
// 2.执行插入操作
accountInfoService.insert(new AccountInfo(1, "中国银行"));
} finally {
// 3.操作完成之后,移除本地线程缓存的数据源
DataSourceContextHolder.remove();
}
}
}
运行单元测试,输出结果如下:
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6ff6efdc]
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@78226c36] will be managed by Spring
==> Preparing: insert into tb_user_info(id, user_name) values(?, ?)
==> Parameters: 1(Integer), 张三(String)
<== Updates: 1
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6ff6efdc]
...
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1c9e07c6]
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@2b10ace9] will be managed by Spring
==> Preparing: insert into tb_account_info(id, account_name) values(?, ?)
==> Parameters: 1(Integer), 中国银行(String)
<== Updates: 1
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1c9e07c6]
从日志打印上可以清晰的看到,数据被成功的插入到对应的数据库中。
2.5、利用切面代理类设置数据源
在上文中,我们采用的是手动方式来设置数据源,在实际的业务开发中,我们通常会采用切面代理类来设置数据源,以便简化代码复杂度。
实现过程如下。
2.5.1、创建数据源注解
首先,定义一个数据源注解来实现数据源的切换,同时配置一个默认的数据源名称,代码示例如下:
package com.example.dynamic.datasource.config.aop;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DbSource {
/**
* 数据源key值
* @return
*/
String value() default "db1";
}
2.5.2、编写数据源代理类
接着,基于@DbSource注解,创建一个 AOP 代理类,所有配置该注解的方法都会被前后拦截,代码示例如下:
package com.example.dynamic.datasource.config.aop;
@Order(1)
@Aspect
@Component
publicclass DbSourceAspect {
privatestaticfinal Logger LOGGER = LoggerFactory.getLogger(DbSourceAspect.class);
@Pointcut("@annotation(com.example.dynamic.datasource.config.aop.DbSource)")
public void dynamicDataSource(){}
@Around("dynamicDataSource()")
public Object datasourceAround(ProceedingJoinPoint point) throws Throwable {
// 获取要切换的数据源名称
MethodSignature methodSignature = (MethodSignature)point.getSignature();
Method method = methodSignature.getMethod();
DbSource dbSource = method.getAnnotation(DbSource.class);
LOGGER.info("select dataSource:" + dbSource.value());
DataSourceContextHolder.set(dbSource.value());
try {
return point.proceed();
} finally {
DataSourceContextHolder.remove();
}
}
}
这里有一个很重要的配置就是@Order(1),表示代理类会优先被执行,值越低优先级越高。
当@DbSource和@Transactional注解同时应用在一个方法上时,如果不指定代理类的顺序,当调用方法的时候,会出现事务异常的问题。
原因在于:@Transactional注解所在的代理类,默认优先级是Integer.MAX,也就是最后被执行;当自定义的代理类没有配置优先级时,同样默认值也是Integer.MAX,但是自定义的会排在后面执行,此时当调用标注事务注解的方法时,事务代理类会先执行,但是数据源并没有被设置,会导致事务控制异常,这一点需要特别注意。
2.5.3、使用注解切换数据源
最后,在需要的方法上配置相关的数据源注解即可。
以用户服务类为例,代码示例如下:
@Service
public class UserInfoService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional
@DbSource(value = "db1")
public void add(UserInfo entity){
userInfoMapper.insert(entity);
}
}
账户服务类,代码示例如下:
@Service
public class AccountInfoService {
@Autowired
private AccountInfoMapper accountInfoMapper;
@Transactional
@DbSource(value = "db2")
public void add(AccountInfo entity){
accountInfoMapper.insert(entity);
}
}
2.5.4、编写单元测试
最后,编写一个单元测试方法,验证代码的正确性,代码示例如下:
package com.example.dynamic.datasource.junit;
@RunWith(SpringRunner.class)
@SpringBootTest
public class DynamicDataSourceJunit {
@Autowired
private AccountInfoService accountInfoService;
@Autowired
private UserInfoService userInfoService;
@Test
public void test(){
// 新增用户
userInfoService.add(new UserInfo(1, "张三"));
// 新增账户数据
accountInfoService.add(new AccountInfo(1, "中国银行"));
}
}
日志输出结果如下:
图片
可以看到,数据被正确的插入到对应的数据库中,与预期一致。
可以发现,采用 aop 代理的方式来切换数据源,业务实现上会更加的灵活。
三、动态数据源配置介绍
在上文中,我们介绍了多数据源的配置实现方式,这种配置方式有一个不好的地方在于:配置文件都是写死的。
能不能改成动态的加载数据源呢,答案是可以的!
下面我们一起来看看相关的具体实现方式。
3.1、数据库准备
首先,我们需要准备一张数据源配置表。新建一个test_db数据库,然后在数据库中创建一张数据源配置表,脚本如下:
CREATE TABLE`tb_db_info` (
`id`int(11) unsignedNOTNULL AUTO_INCREMENT,
`db_name`varchar(50) DEFAULTNULL,
`db_url`varchar(200) DEFAULTNULL,
`driver_class_name`varchar(100) DEFAULTNULL,
`username`varchar(80) DEFAULTNULL,
`password`varchar(80) DEFAULTNULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3DEFAULTCHARSET=utf8mb4;
最后,初始化两条数据,方便后续数据源的查询。
INSERT INTO `tb_db_info` (`id`, `db_name`, `db_url`, `driver_class_name`, `username`, `password`)
VALUES
(1, 'db1', 'jdbc:mysql://localhost:3306/db_test_1', 'com.mysql.jdbc.Driver', 'root', 'root'),
(2, 'db2', 'jdbc:mysql://localhost:3306/db_test_2', 'com.mysql.jdbc.Driver', 'root', 'root');
3.2、修改全局配置文件
我们还是以上文介绍的工程为例,把之前自定义的配置参数删除掉,重新基于 Spring Boot 约定的配置方式,添加相关的数据源参数,内容如下:
# 配置默认数据源
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
3.3、编写相关的服务类
基于数据库中tb_db_info表,编写相关的查询逻辑,代码示例如下:
package com.example.dynamic.datasource.entity;
publicclass DbInfo {
/**
* 主键ID
*/
private Integer id;
/**
* 数据库key,即保存Map中的key
*/
private String dbName;
/**
* 数据库地址
*/
private String dbUrl;
/**
* 数据库驱动
*/
private String driverClassName;
/**
* 数据库用户名
*/
private String username;
/**
* 数据库密码
*/
private String password;
// set、get方法等...
}
public interface DbInfoMapper {
List<DbInfo> findAll();
}
<?xml versinotallow="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.dynamic.datasource.mapper.DbInfoMapper">
<select id="findAll" resultType="com.example.dynamic.datasource.entity.DbInfo">
select
id
,db_name as dbName
,db_url as dbUrl
,driver_class_name as driverClassName
,username
,password
from tb_db_info
order by id
</select>
</mapper>
3.4、修改动态数据源服务类
对DynamicDataSource类进行一些调整,代码如下:
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
/**
* 重新加载数据源集合
* @param dbList
*/
public void loadDataSources(List<DbInfo> dbList){
try {
Map<Object, Object> targetDataSourceMap = new HashMap<>();
for (DbInfo source : dbList) {
// 初始化数据源
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(source.getDriverClassName());
dataSource.setUrl(source.getDbUrl());
dataSource.setUsername(source.getUsername());
dataSource.setPassword(source.getPassword());
dataSource.setInitialSize(1);
dataSource.setMinIdle(1);
dataSource.setMaxActive(5);
dataSource.setTestWhileIdle(true);
dataSource.setTestOnBorrow(true);
dataSource.setValidationQuery("select 1 ");
dataSource.init();
targetDataSourceMap.put(source.getDbName(), dataSource);
}
super.setTargetDataSources(targetDataSourceMap);
// 重新初始化resolvedDataSources对象
super.afterPropertiesSet();
} catch (Exception e){
e.printStackTrace();
}
}
}
3.5、修改动态数据源配置类
对DataSourceConfig类也需要进行一些调整,通过 Spring Boot 默认的数据源配置类初始化一个数据源实例对象,代码如下:
@Configuration
publicclass DataSourceConfig {
@Autowired
private DataSourceProperties basicProperties;
/**
* 注入动态数据源
* @param dataSource
* @return
*/
@Bean
@Primary
public DynamicDataSource dynamicDataSource(){
// 获取初始数据源
DataSource defaultDataSource = basicProperties.initializeDataSourceBuilder().build();
Map<Object,Object> targetDataSources = new HashMap<>();
targetDataSources.put("defaultDataSource", defaultDataSource);
// 注入动态数据源
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 设置默认数据源
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
// 设置动态数据源集
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
}
3.6、配置启动时加载数据源服务类
以上的配置调整完成之后,我们还需要配置一个服务启动监听类,将从数据库中查询到的数据配置信息加载到DynamicDataSource对象中,代码示例如下:
@Component
publicclass LoadDataSourceRunner implements CommandLineRunner {
@Autowired
private DbInfoMapper dbInfoMapper;
@Autowired
private DynamicDataSource dynamicDataSource;
@Override
public void run(String... args) {
List<DbInfo> dbList = dbInfoMapper.findAll();
if(!CollectionUtils.isEmpty(dbList)){
dynamicDataSource.loadDataSources(dbList);
}
}
}
3.7、调整 SpringBootApplication 注解配置
以上的实现方式,因为启动的时候,采用的是 Spring Boot 默认的数据源配置实现,因此无需排除DataSourceAutoConfiguration类,可以将相关参数移除掉。
package com.example.dynamic.datasource;
@MapperScan("com.example.dynamic.datasource.mapper")
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3.8、代码测试
最后,重新运行单元测试方法,输出结果如下:
图片
从日志上可以清晰的看到,运行结果与预期一致!
四、小结
本文主要围绕利用 Spring Boot 来实现动态数据源的加载,进行一次知识的整合和总结,如果描述不对的地方,欢迎大家留言指出!
五、参考
1.https://www.baeldung.com/spring-boot-configure-multiple-datasources
2.https://cloud.tencent.com/developer/article/2370197