最近有项目需要支持多租户(多租户之后会单独开一篇文章说),多租户架构中需要用到多数据源,即物理隔离,需要不同租户对应不同的RMDB数据库实例,故本篇文章先行对多数据源的进行探讨。
通常我们的工程仅存在唯一数据源以及对应的一套数据库连接池,如SpringBoot应用中如下配置:
# 基础配置
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/my_db?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
# Hikari 连接池配置
hikari:
# 最小空闲连接数量
minimum-idle: 5
# 空闲连接存活最大时间,默认600000(10分钟)
idle-timeout: 180000
# 连接池最大连接数,默认是10
maximum-pool-size: 10
# 此属性控制从池返回的连接的默认自动提交行为,默认值:true
auto-commit: true
# 连接池名称
pool-name: MyHikariCP
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
max-lifetime: 1800000
# 数据库连接超时时间,默认30秒,即30000
connection-timeout: 30000
connection-test-query: SELECT 1
以Mybatis生态为例,支持多数据源的方式有如下2种。
此种方式需按照数据源对Mapper接口及mapper.xml进行分包,
如下图存在2个数据源,则需要分成2个包,如ds1和ds2:

同时比较重要的是需要对Mybatis中不同包下的Mapper注入不同的DataSource,因此每个数据源都需要单独进行配置,如截图中存在2个数据源分别对应DataSourceConfig1和DataSourceConfig2两个配置类,同时需要将一个数据源设置为主数据源,避免Spring启动无法注入数据源报错。
多数据源配置application.yml规划如下:
spring:
# DataSource Config
datasource:
ds1: # 数据源1
# Hikari 连接池配置,具体配置属性同spring.datasource.hikari.*
jdbc-url: jdbc:mysql://localhost:3306/multi-ds-1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
# 最小空闲连接数量
minimum-idle: 5
# 空闲连接存活最大时间,默认600000(10分钟)
idle-timeout: 180000
# 连接池最大连接数,默认是10
maximum-pool-size: 10
# 此属性控制从池返回的连接的默认自动提交行为,默认值:true
auto-commit: true
# 连接池名称
pool-name: DS1-POOL
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
max-lifetime: 1800000
# 数据库连接超时时间,默认30秒,即30000
connection-timeout: 30000
connection-test-query: SELECT 1
ds2: # 数据源2
# Hikari 连接池配置,具体配置属性同spring.datasource.hikari.*
jdbc-url: jdbc:mysql://localhost:3306/multi-ds-2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
# 最小空闲连接数量
minimum-idle: 5
# 空闲连接存活最大时间,默认600000(10分钟)
idle-timeout: 180000
# 连接池最大连接数,默认是10
maximum-pool-size: 10
# 此属性控制从池返回的连接的默认自动提交行为,默认值:true
auto-commit: true
# 连接池名称
pool-name: DS2-POOL
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
max-lifetime: 1800000
# 数据库连接超时时间,默认30秒,即30000
connection-timeout: 30000
connection-test-query: SELECT 1
多数据源配置类定义如下:
/**
* 数据源1 - 配置
*
* 注:默认仅@Primary主数据源支持事务@Transactional
*
* @author luohq
* @date 2022-08-06
*/
@Configuration
//注意此处需扫描对应数据源包下的mapper接口,且sqlSessionFactory为当前类中定义的SqlSessionFactory
@MapperScan(basePackageClasses = {MyDataMapper1.class}, sqlSessionFactoryRef = "ds1SqlSessionFactory")
public class DataSourceConfig1 {
@Primary // 表示这个数据源是默认数据源, 这个注解必须要加,因为不加的话spring将分不清楚那个为主数据源(默认数据源)
@Bean("ds1DataSource")
@ConfigurationProperties(prefix = "spring.datasource.ds1") //读取application.yml中的配置参数映射成为一个对象
public DataSource ds1DataSource1() {
return DataSourceBuilder.create().build();
}
@Primary
@Bean("ds1SqlSessionFactory")
public SqlSessionFactory ds1SqlSessionFactory(@Qualifier("ds1DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
// mapper的xml形式文件位置必须要配置,不然将报错:no statement (这种错误也可能是mapper的xml中,namespace与项目的路径不一致导致)
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/ds1/*.xml"));
return bean.getObject();
}
@Primary
@Bean("ds1SqlSessionTemplate")
public SqlSessionTemplate ds1SqlSessionTemplate(@Qualifier("ds1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
----------------------------------------------------------------------------
/**
* 数据源2 - 配置
* 注:默认仅@Primary主数据源支持事务@Transactional,当前非主数据源不支持事务
* @author luohq
* @date 2022-08-06
*/
@Configuration
//注意此处需扫描对应数据源包下的mapper接口,且sqlSessionFactory为当前类中定义的SqlSessionFactory
@MapperScan(basePackageClasses = {MyDataMapper2.class}, sqlSessionFactoryRef = "ds2SqlSessionFactory")
public class DataSourceConfig2 {
@Bean("ds2DataSource")
@ConfigurationProperties(prefix = "spring.datasource.ds2")
public DataSource ds2DataSource(){
return DataSourceBuilder.create().build();
}
@Bean("ds2SqlSessionFactory")
public SqlSessionFactory ds2SqlSessionFactory(@Qualifier("ds2DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/ds2/*.xml"));
return bean.getObject();
}
@Bean("ds2SqlSessionTemplate")
public SqlSessionTemplate ds2SqlSessionTemplate(@Qualifier("ds2SqlSessionFactory") SqlSessionFactory sqlSessionFactory){
return new SqlSessionTemplate(sqlSessionFactory);
}
}
在使用多数据源时,仅需将不同数据源包下的Mapper接口注入使用即可,如:
/**
*
* 我的数据 服务实现类
*
*
* @author luohq
* @since 2022-08-06
*/
@Service
public class MyDataServiceImpl implements IMyDataService {
@Resource
private MyDataMapper1 myDataMapper1;
@Resource
private MyDataMapper2 myDataMapper2;
@Override
public MyData findByIdFromDs1(Long id) {
return this.myDataMapper1.selectById(id);
}
@Override
public MyData findByIdFromDs2(Long id) {
return this.myDataMapper2.selectById(id);
}
/**
* 仅@Primary主数据源ds1支持事务,非主数据源ds2不支持事务
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Integer addBothData(MyData myData1, MyData myData2) {
Integer retCount1 = this.myDataMapper1.insert(myData1);
Integer retCount2 = this.myDataMapper2.insert(myData2);
if (true) {
throw new RuntimeException("业务异常 - 制造数据库回滚!");
}
return retCount1 + retCount2;
}
/**
* 仅@Primary主数据源支持事务
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Integer addData1(MyData myData) {
Integer retCount = this.myDataMapper1.insert(myData);
return retCount;
}
/**
* 非主数据源不支持事务
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Integer addData2(MyData myData) {
Integer retCount = this.myDataMapper2.insert(myData);
return retCount;
}
}
以上方式确实可以实现多数据源,但是此种方式存在如下问题:
@Primary的主数据源,不支持其他 非@Primary数据源
@Primary数据源 支持事务,参见上例代码中的MyDataServiceImpl.addData1非@Primary数据源 不支持事务,参见上例代码中的MyDataServiceImpl.addData2@Primary数据源 支持事务,如上例代码中addBothData同时调用myDataMapper1和myDataMapper2,实际测试myDataMapper1的操作支持事务,而myDataMapper2完全脱离了当前事务的管理。@Primary主数据源仅作写操作,如MyDataWriteMapper.java,而其他非@Primary数据源仅作读操作,如MyDataReadMapper.java。以上示例源码参见:
https://gitee.com/luoex/multi-datasource-demo/tree/master/mb-package-multi-ds
在实际开发时,我这边多数都是直接使用Mybatis-Plus作为DAO层,Mybatis-Plus作为Mybatis的增强,提供了很多开箱即用的方便特性,比如内建的CRUD操作、强大的基于Wrapper的条件构造器、分页、ID生成等等。在Mybatis-Plus生态中作者也提供了多数据源方案,即基于dynamic-datasource-spring-boot-starter的实现:
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.2version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>dynamic-datasource-spring-boot-starterartifactId>
<version>3.5.1version>
dependency>
该dynamic-datasource扩展代码是开源的:
https://github.com/baomidou/dynamic-datasource-spring-boot-starter
https://gitee.com/baomidou/dynamic-datasource-spring-boot-starter
但是文档是付费的:
https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611
我看的文档是公司小伙伴付费买的,公司内是可以传播的,但不可以在网络上传播。
dynamic-datasource集成还是比较方便的,同时支持Druid、HikariCP等诸多连接池。
以集成HikariCP连接池为例,application.yml配置如下:
# dynamic-datasource多数据源配置
spring:
datasource:
dynamic:
primary: ds1 #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
hikari: # 全局hikariCP参数,所有值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置)
connection-timeout: 30000
max-pool-size: 10
min-idle: 5
idle-timeout: 180000
max-lifetime: 1800000
connection-test-query: SELECT 1
datasource:
ds1: # 数据源名称即对应连接池名称
url: jdbc:mysql://localhost:3306/multi-ds-1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
hikari: # 当前数据源HikariCP参数(继承全局、部分覆盖全局)
max-pool-size: 20
ds2:
url: jdbc:mysql://localhost:3306/multi-ds-2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
max-pool-size: 15
# Mybatis-Plus相关配置
mybatis-plus:
global-config:
db-config:
id-type: assign_id
代码结构如下图,对比之前提到的基于原生Mybatis分包的方式,此种方式不需要对Mapper接口、Mapper.xml进行分包:

切换数据源时,可通过在Service实现类中对应方法通过@DS("具体配置中的数据源名称")指定对应的数据源:
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.luo.demo.multi.ds.dynamic.dto.MyDataQueryDto;
import com.luo.demo.multi.ds.dynamic.entity.MyData;
import com.luo.demo.multi.ds.dynamic.mapper.MyDataMapper;
import com.luo.demo.multi.ds.dynamic.service.IMyDataService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.Objects;
/**
*
* 我的数据 服务实现类
*
*
* @author luohq
* @since 2022-08-07
*/
@Service
public class MyDataServiceImpl implements IMyDataService {
@Resource
private MyDataMapper myDataMapper;
@Override
@DS("ds1")
public MyData findByIdFromDs1(Long id) {
//selectById - 支持自动拼接租户Id参数
return this.myDataMapper.selectById(id);
}
@Override
@DS("ds1")
public MyData findByQueryFromDs1(MyDataQueryDto myDataQueryDto) {
//QueryWrapper - 支持自动拼接租户Id参数
return this.myDataMapper.selectOne(Wrappers.<MyData>lambdaQuery()
.eq(Objects.nonNull(myDataQueryDto.getId()), MyData::getId, myDataQueryDto.getId())
.like(StringUtils.hasText(myDataQueryDto.getMyName()), MyData::getMyName, myDataQueryDto.getMyName()));
}
@Override
public MyData findByName(String myName) {
//mapper.xml自定义查询 - 支持自动拼接租户Id参数
return this.myDataMapper.selectByName(myName);
}
@Override
@DS("ds2")
public MyData findByIdFromDs2(Long id) {
return this.myDataMapper.selectById(id);
}
/**
* 单@Transactional内不支持切换数据源,
* 即先使用ds1,则后续一直使用同一ds1连接,
* 当前事务生效,但都会插入ds1中
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Integer addBothData(MyData myData1, MyData myData2) {
Integer retCount1 = this.addData1(myData1);
Integer retCount2 = this.addData2(myData2);
//if (true) {
// throw new RuntimeException("业务异常 - 制造数据库回滚!");
//}
return retCount1 + retCount2;
}
/**
* 支持事务
*/
@Override
@Transactional(rollbackFor = Exception.class)
@DS("ds1")
public Integer addData1(MyData myData) {
//支持自动设置tenantId
Integer retCount = this.myDataMapper.insert(myData);
return retCount;
}
/**
* 支持事务
*/
@Override
@Transactional(rollbackFor = Exception.class)
@DS("ds2")
public Integer addData2(MyData myData) {
//支持自动设置tenantId
Integer retCount = this.myDataMapper.insert(myData);
return retCount;
}
}
关于@DS注解需要注意:
以上示例源码参见:
https://gitee.com/luoex/multi-datasource-demo/tree/master/mp-dynamic-ds
dynamic-datasource提供自定义的 @DSTransactional 注解,
关于使用@DSTransactional支持多数据源本地事务的示例代码如下:
/**
* 本地多数据源事务 - 测试服务实现类
*
* @author luohq
* @date 2022-08-09 13:42
*/
@Service
public class MyDataMultiDsLocalTxServiceImpl implements IMyDataMultiDsLocalTxService {
@Resource
private IMyDataService myDataService;
/**
* 此处需使用@DSTransactional,需注意不是Spring @Transactional,
* 使用@DSTransactional支持切换数据源,而@Transactional方法中无法切换数据源
* 注:需跨服务调用切换DS,否则仅使用第一个数据源,即2条记录都插入到ds1中
*/
@Override
@DSTransactional
//@DS("ds1") //如果ds1是默认数据源则不需要DS注解。
public Integer addBothData(MyData myData1, MyData myData2) {
Integer retCount1 = this.myDataService.addData1(myData1);
Integer retCount2 = this.myDataService.addData2(myData2);
//if (true) {
// throw new RuntimeException("测试多数据源异常回滚!");
//}
return retCount1 + retCount2;
}
}
------------------------------------------------------------------------------------
/**
*
* 我的数据 服务实现类
*
*
* @author luohq
* @since 2022-08-07
*/
@Service
public class MyDataServiceImpl extends ServiceImpl<MyDataMapper, MyData> implements IMyDataService {
@Resource
private MyDataMapper myDataMapper;
/**
* 支持事务 - @DSTransactional区别于Spring @Transactional
*/
@Override
@DSTransactional
@DS("ds1")
public Integer addData1(MyData myData) {
//支持自动设置tenantId
Integer retCount = this.myDataMapper.insert(myData);
return retCount;
}
/**
* 支持事务 - @DSTransactional区别于Spring @Transactional
*/
@DS("ds2")
@Override
@DSTransactional
public Integer addData2(MyData myData) {
//支持自动设置tenantId
Integer retCount = this.myDataMapper.insert(myData);
return retCount;
}
}
示例代码中MyDataMultiDsLocalTxServiceImpl主服务,该主服务中addBothData方法跨服务调用MyDataServiceImpl中的使用@DS(“ds1)的addData1和使用@DS(“ds2”)的addData2,即主服务使用默认数据源(主服务方法亦可通过@DS(”…")指定数据源,未指定则使用默认),调用不同数据源的服务。
关于各服务方法上事务注解及最终效果总结为下表:
| 主服务 MyDataMultiDsLocalTxServiceImpl.addBothData | ds1服务 @DS(“ds1”) MyDataServiceImpl.addData1 | ds2服务 @DS(“ds1”) MyDataServiceImpl.addData1 | 效果 |
|---|---|---|---|
| @DSTransactional | @DSTransactional | @DSTransactional | 调用主服务支持全局事务提交、回滚, 单独调用ds服务各自支持事务 |
| @DSTransactional | @Transactional | @Transactional | 调用主服务支持全局事务提交、回滚, 单独调用ds服务各自支持事务 |
| @DSTransactional | 无 | 无 | 调用主服务支持全局事务提交、回滚, 单独调用ds服务不支持事务 |
| 无 | @DSTransactional | @DSTransactional | 不支持全局事务, 调用ds服务各自管理自身事务 |
| @Transactional | Spring @Transactional不支持切换数据源 |
上面提到的 @DSTransactional 支持多数据源本地事务,如何定义多数据源本地事务?
参考如下服务分布图:

其中橙黄色的ServiceA|B|C为服务实例,而蓝色Resource即为各自服务实例对应数据库存储实例,
整个服务调用链组成了一个分布式事务,而各自Service实例自身的事务管理即为本地事务,
如下图中的绿框即标记出了各自的本地事务,其中ServiceA和ServiceC仅包含唯一的数据源,而ServiceB同时使用2个数据源,即ServiceB的需要管理的事务即为多数据源本地事务。

之前提到Dynamic-Datasource中的 @DSTransactional 注解,
而如上图中的完整的 分布式事务(且 存在单个服务包含多个数据源的情况) 场景,可以结合Seata:
注:
在引入多数据源时需谨慎,尤其是微服务场景下,可以尽早考虑将多个数据源各自拆分到不同的单个服务中,
- 拆分后单个服务中仅包含唯一数据源,降低开发难度,便于针对单数据源进行优化,
- 此时可直接使用Spring原生@Transactional管理单数据源本地事务(即无需引入dynamic-datasource扩展),
- 若有分布式事务管理场景,可再引入Seata。
使用Mybatis-Plus及对应的Dynamic-Datasource扩展,
DynamicDataSourceContextHolder.push("ds1")
通过上述特性描述,不难发现其和多租户架构相当契合:
以上基于Mybatis-Plus及Dynamic-Datasource扩展的方案即可实现一套多租户(物理隔离)架构,多租户实现方案后续会单开一遍文章介绍,本文不做重点描述。
以上仅介绍Mybatis-Plus及Dynamic-Datasource扩展的部分功能,感兴趣的小伙伴可以继续深入研究:
参考:
Mybatis分包:
springboot-整合多数据源配置(MapperScan分包、mybatis plus - dynamic-datasource-spring-boot-starter)
Mybatis-Plus Dynamic Datasource:
https://baomidou.com/pages/a61e1b/
https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611