目录
1 SpringBoot分库配置
1.1 准备数据
1.2 springboot+mybatis使用分包方式整合
1.2.1 pom.xml
1.2.2 application.yml 配置文件
1.2.3 连接数据源配置文件
1.2.3.1 连接源配置一
1.2.3.2 连接源配置二
1.2.4 项目结构
1.3 springboot+druid+mybatisplus使用注解整合
1.3.1 pom.xml
1.3.2 application.yml 配置文件
1.3.3 使用@DS区分数据源
1.4 读写分离库
1.4.1 AbstractRoutingDataSource原理
1.4.2 ThreadLocal工具类
1.4.3 继承AbstractRoutingDataSource
1.4.4 配置数据源
1.4.5 测试
1.4.6 使用自定义注解
1.4.6.1 定义注解
1.4.6.2 实现aop
1.4.7 动态添加数据源
1.4.7.1 数据源实体
1.4.7.2 修改DynamicDataSource
1.4.7.3 动态添加数据源
1.4.7.4 启动添加配置
最近项目用到了 spring
多数据源配置,恰好看到这篇好文章,特地分享下
主要介绍两种整合方式,分别是 springboot+mybatis
使用分包方式整合,和 springboot+druid+mybatisplus
使用注解方式整合
在本地新建两个数据库,名称分别为db1
和db2
,新建一张user
表,表结构如下
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(25) NOT NULL COMMENT '姓名',
`age` int(2) DEFAULT NULL COMMENT '年龄',
`sex` tinyint(1) NOT NULL DEFAULT '0' COMMENT '性别:0-男,1-女',
`addr` varchar(100) DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='测试用户表'
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.9.RELEASE
com.example
multipledatasource
0.0.1-SNAPSHOT
multipledatasource
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-web
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.0
mysql
mysql-connector-java
runtime
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
server:
port: 8080 # 启动端口
spring:
datasource:
db1: # 数据源1
jdbc-url: jdbc:mysql://localhost:3306/db1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
db2: # 数据源2
jdbc-url: jdbc:mysql://localhost:3306/db2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
注意事项:
各个版本的 springboot
配置 datasource
时参数有所变化,例如低版本配置数据库 url
时使用 url
属性,高版本使用 jdbc-url
属性,请注意区分
@Configuration
@MapperScan(basePackages = "com.example.multipledatasource.mapper.db1", sqlSessionFactoryRef = "db1SqlSessionFactory")
public class DataSourceConfig1 {
@Primary // 表示这个数据源是默认数据源, 这个注解必须要加,因为不加的话spring将分不清楚那个为主数据源(默认数据源)
@Bean("db1DataSource")
@ConfigurationProperties(prefix = "spring.datasource.db1") //读取application.yml中的配置参数映射成为一个对象
public DataSource getDb1DataSource(){
return DataSourceBuilder.create().build();
}
@Primary
@Bean("db1SqlSessionFactory")
public SqlSessionFactory db1SqlSessionFactory(@Qualifier("db1DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
// mapper的xml形式文件位置必须要配置,不然将报错:no statement (这种错误也可能是mapper的xml中,namespace与项目的路径不一致导致)
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/db1/*.xml"));
return bean.getObject();
}
@Primary
@Bean("db1SqlSessionTemplate")
public SqlSessionTemplate db1SqlSessionTemplate(@Qualifier("db1SqlSessionFactory") SqlSessionFactory sqlSessionFactory){
return new SqlSessionTemplate(sqlSessionFactory);
}
}
@Configuration
@MapperScan(basePackages = "com.example.multipledatasource.mapper.db2", sqlSessionFactoryRef = "db2SqlSessionFactory")
public class DataSourceConfig2 {
@Bean("db2DataSource")
@ConfigurationProperties(prefix = "spring.datasource.db2")
public DataSource getDb1DataSource(){
return DataSourceBuilder.create().build();
}
@Bean("db2SqlSessionFactory")
public SqlSessionFactory db1SqlSessionFactory(@Qualifier("db2DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/db2/*.xml"));
return bean.getObject();
}
@Bean("db2SqlSessionTemplate")
public SqlSessionTemplate db1SqlSessionTemplate(@Qualifier("db2SqlSessionFactory") SqlSessionFactory sqlSessionFactory){
return new SqlSessionTemplate(sqlSessionFactory);
}
}
注意事项:
在 service
层中根据不同的业务注入不同的 dao
层
如果是主从复制- -读写分离:比如 db1
中负责增删改,db2
中负责查询。但是需要注意的是负责增删改的数据库必须是主库(master)
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.9.RELEASE
com.example
mutipledatasource2
0.0.1-SNAPSHOT
mutipledatasource2
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-web
com.baomidou
mybatis-plus-boot-starter
3.2.0
com.baomidou
dynamic-datasource-spring-boot-starter
2.5.6
mysql
mysql-connector-java
runtime
com.alibaba
druid-spring-boot-starter
1.1.20
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
local1
local1
true
local2
local2
server:
port: 8080
spring:
datasource:
dynamic:
primary: db1 # 配置默认数据库
datasource:
db1: # 数据源1配置
url: jdbc:mysql://localhost:3306/db1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
db2: # 数据源2配置
url: jdbc:mysql://localhost:3306/db2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
initial-size: 1
max-active: 20
min-idle: 1
max-wait: 60000
autoconfigure:
exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 去除druid配置
DruidDataSourceAutoConfigure
会注入一个DataSourceWrapper
,其会在原生的spring.datasource
下找 url, username, password
等。动态数据源 URL
等配置是在 dynamic
下,因此需要排除,否则会报错。排除方式有两种,一种是上述配置文件排除
,还有一种可以在项目启动类排除
@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
给使用非默认数据源
添加注解@DS
@DS
可以注解在 方法
上和 类
上,同时存在方法注解优先于类上注解。
注解在 service
实现或 mapper
接口方法上,不要同时在 service
和 mapper
注解
mapper上使用
@DS("db2")
public interface UserMapper extends BaseMapper {
}
service上使用
@Service
@DS("db2")
public class ModelServiceImpl extends ServiceImpl implements IModelService {}
方法上使用
@Select("SELECT * FROM user")
@DS("db2")
List selectAll();
读写分离库使用AbstractRoutingDataSource
AbstractRoutingDataSource
类和aop
结合,还可以用来作为读写分离库
AbstractRoutingDataSource
原理:
Map
:保存了所有可能的数据源,key
为数据库的key
,value
为DataSource
对象或字符串形式的连接信息Object defaultTargetDataSource
:保存了默认的数据源,用于找不到具体的数据源时使用afterPropertiesSet()
方法:targetDataSources
数据源信息成
的形式,保存在Map
中defaultTargetDataSource
中的默认数据源信息解析成 DataSource
对象保存在 DataSource resolvedDefaultDataSource
中determineCurrentLookupKey()
:提供给子类重写,指定当前线程使用的具体的数据源的key
,此方法为抽象方法,通过扩展这个方法来实现数据源的切换。目标数据源的结构为:Map
其key为lookup key
,lookup key
通常是绑定在线程上下文中,根据这个key
去resolvedDataSources
中取出DataSource
。determineTargetDataSource()
:根据 determineCurrentLookupKey()
方法返回的key
返回数据源DataSouce
对象,若没有,则使用默认数据源对象getConnection()
:根据determineTargetDataSource()
返回的数据源,与其建立连接根据上面的分析,我们可以按照下面的步骤去实现:
DynamicDataSource
类继承AbstractRoutingDataSource
,重写determineCurrentLookupKey()
方法。targetDataSources
和defaultTargetDataSource
,通过afterPropertiesSet()
方法将数据源写入resolvedDataSources
和resolvedDefaultDataSource
。AbstractRoutingDataSource
的getConnection()
方法时,determineTargetDataSource()
方法返回DataSource
执行底层的getConnection()
创建一个类用于操作 ThreadLocal
,主要是通过get,set,remove
方法来获取、设置、删除当前线程对应的数据源。
public class DataSourceContextHolder {
//此类提供线程局部变量。这些变量不同于它们的正常对应关系是每个线程访问一个线程(通过get、set方法),有自己的独立初始化变量的副本。
private static final ThreadLocal DATASOURCE_HOLDER = new ThreadLocal<>();
/**
* 设置数据源
*/
public static void setDataSource(String dataSourceName){
DATASOURCE_HOLDER.set(dataSourceName);
}
/**
* 获取当前线程的数据源
*/
public static String getDataSource(){
return DATASOURCE_HOLDER.get();
}
/**
* 删除当前数据源
*/
public static void removeDataSource(){
DATASOURCE_HOLDER.remove();
}
}
定义一个动态数据源继承 AbstractRoutingDataSource
,通过determineCurrentLookupKey
方法与上述实现的ThreadLocal
类中的get
方法进行关联,实现动态切换数据源。
/**
* @description: 实现动态数据源,根据AbstractRoutingDataSource路由到不同数据源中
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultDataSource,Map
上述代码中,还实现了一个动态数据源类的构造方法,主要是为了设置默认数据源,以及以Map
保存的各种目标数据源。其中Map
的key
是设置的数据源名称,value
则是对应的数据源(DataSource)
配置文件
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
master:
url: jdbc:mysql://xxxxxx:3306/test1?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: xxxx
password: xxxx
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://xxxxx:3306/test2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: xxxx
password: xxxx
driver-class-name: com.mysql.cj.jdbc.Driver
配置类
/**
* @description: 设置数据源
**/
@Configuration
public class DateSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
public DataSource slaveDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource createDynamicDataSource(){
Map
通过配置类,将配置文件中的配置的数据库信息转换成datasource
,并添加到DynamicDataSource
中,同时通过@Bean
将DynamicDataSource
注入Spring
中进行管理,后期在进行动态数据源添加时,会用到。
我们创建一个getData的方法,参数就是需要查询数据的数据源名称。
@GetMapping("/getData.do/{datasourceName}")
public String getMasterData(@PathVariable("datasourceName") String datasourceName){
DataSourceContextHolder.setDataSource(datasourceName);
TestUser testUser = testUserMapper.selectOne(null);
DataSourceContextHolder.removeDataSource();
return testUser.getUserName();
}
在上述代码中,我们看到DataSourceContextHolder.setDataSource(datasourceName);
来设置了当前线程需要查询的数据库,通过DataSourceContextHolder.removeDataSource();
来移除当前线程已设置的数据源。
使用过Mybatis-plus
动态数据源的小伙伴,应该还记得我们在使用切换数据源时会使用到DynamicDataSourceContextHolder.push(String ds);
和DynamicDataSourceContextHolder.poll();
这两个方法,翻看源码我们会发现其实就是在使用ThreadLocal
时使用了栈,这样的好处就是能使用多数据源嵌套
注:启动程序时,不要忘记将SpringBoot自动添加数据源进行排除哦,否则会报循环依赖问题。
在上述中,虽然已经实现了动态切换数据源,但是我们会发现如果涉及到多个业务进行切换数据源的话,我们就需要在每一个实现类中添加这一段代码。假如使用注解来进行优化呢,如下
我们就用mybatis
动态数据源切换的注解:DS
,代码如下:
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DS {
String value() default "master";
}
@Aspect
@Component
@Slf4j
public class DSAspect {
@Pointcut("@annotation(com.test.dynamic_datasource.dynamic.aop.DS)")
public void dynamicDataSource(){}
@Around("dynamicDataSource()")
public Object datasourceAround(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature)point.getSignature();
Method method = signature.getMethod();
DS ds = method.getAnnotation(DS.class);
if (Objects.nonNull(ds)){
DataSourceContextHolder.setDataSource(ds.value());
}
try {
return point.proceed();
} finally {
DataSourceContextHolder.removeDataSource();
}
}
}
代码使用了@Around
,通过ProceedingJoinPoint
获取注解信息,拿到注解传递值,然后设置当前线程的数据源。
业务场景 :有时候我们的业务会要求我们从保存有其他数据源的数据库表中添加这些数据源,然后再根据不同的情况切换这些数据源。因此我们需要改造下 DynamicDataSource
来实现动态加载数据源。
@Data
@Accessors(chain = true)
public class DataSourceEntity {
/**
* 数据库地址
*/
private String url;
/**
* 数据库用户名
*/
private String userName;
/**
* 密码
*/
private String passWord;
/**
* 数据库驱动
*/
private String driverClassName;
/**
* 数据库key,即保存Map中的key
*/
private String key;
}
实体中定义数据源的一般信息,同时定义一个key
用于作为DynamicDataSource
中Map
中的key
/**
* @description: 实现动态数据源,根据AbstractRoutingDataSource路由到不同数据源中
*/
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
private final Map
在改造后的DynamicDataSource
中,我们添加可以一个 private final Map
,这个map
会在添加数据源的配置文件时将创建的Map
数据源信息通过DynamicDataSource
构造方法进行初始赋值,即:DateSourceConfig
类中的createDynamicDataSource()
方法中。
同时我们在该类中添加了一个createDataSource
方法,进行数据源的创建,并添加到map
中,再通过super.setTargetDataSources(this.targetDataSourceMap);
进行目标数据源的重新赋值
上述代码已经实现了添加数据源的方法,那么我们来模拟通过从数据库表中添加数据源,然后我们通过调用加载数据源的方法将数据源添加进数据源Map
中。
在主数据库中定义一个数据库表,用于保存数据库信息。
create table test_db_info(
id int auto_increment primary key not null comment '主键Id',
url varchar(255) not null comment '数据库URL',
username varchar(255) not null comment '用户名',
password varchar(255) not null comment '密码',
driver_class_name varchar(255) not null comment '数据库驱动'
name varchar(255) not null comment '数据库名称'
)
为了方便,我们将之前的从库录入到数据库中,修改数据库名称。
insert into test_db_info(url, username, password,driver_class_name, name)
value ('jdbc:mysql://xxxxx:3306/test2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false',
'root','123456','com.mysql.cj.jdbc.Driver','add_slave')
启动SpringBoot
时添加数据源:
@Component
public class LoadDataSourceRunner implements CommandLineRunner {
@Resource
private DynamicDataSource dynamicDataSource;
@Resource
private TestDbInfoMapper testDbInfoMapper;
@Override
public void run(String... args) throws Exception {
List testDbInfos = testDbInfoMapper.selectList(null);
if (CollectionUtils.isNotEmpty(testDbInfos)) {
List ds = new ArrayList<>();
for (TestDbInfo testDbInfo : testDbInfos) {
DataSourceEntity sourceEntity = new DataSourceEntity();
BeanUtils.copyProperties(testDbInfo,sourceEntity);
sourceEntity.setKey(testDbInfo.getName());
ds.add(sourceEntity);
}
dynamicDataSource.createDataSource(ds);
}
}
}