ruoyi-vue-plus-多数据源
集成SpringBoot多数据源应用 , 基于AOP切面实现多数据源切换应用
参考文档
- 官方文档 : https://www.kancloud.cn (opens new window)
- ruoyi-vue-plus应用文档 : https://plus-doc.dromara.org (opens new window)
# 快速应用
大致步骤
- 引入依赖
- 配置数据源基础信息
- 切换数据源
- 应用...
# 依赖
<!-- dynamic-datasource 多数据源-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
# 多数据源基础配置
基础应用示例
点击展开
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
# 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
dynamic:
# 性能分析插件(有性能损耗 不建议生产环境使用)
p6spy: true
# 设置默认的数据源或者数据源组,默认值即为 master
primary: master
# 严格模式 匹配不到数据源则报错
strict: true
datasource:
# 主库数据源
master:
type: ${spring.datasource.type}
driverClassName: com.mysql.cj.jdbc.Driver
# jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
url: jdbc:mysql://localhost:3306/xxxx?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
username: root
password: root
# 从库数据源 (开启懒加载)
slave:
lazy: true
type: ${spring.datasource.type}
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/xxxx?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
username:
password:
# 其他数据源配置 (替换掉从数据库即可)
# oracle:
# type: ${spring.datasource.type}
# driverClassName: oracle.jdbc.OracleDriver
# url: jdbc:oracle:thin:@//localhost:1521/XE
# username: ROOT
# password: root
# hikari:
# connectionTestQuery: SELECT 1 FROM DUAL
# postgres:
# type: ${spring.datasource.type}
# driverClassName: org.postgresql.Driver
# url: jdbc:postgresql://localhost:5432/postgres?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
# username: root
# password: root
# sqlserver:
# type: ${spring.datasource.type}
# driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
# url: jdbc:sqlserver://localhost:1433;DatabaseName=tempdb;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true
# username: SA
# password: root
注意
- 不支持 Service接口中的方法使用
@DS
注解 - 不支持 Mapper层中的默认方法不能使用
@DS
注解 , 但支持自定义方法使用@DS
注解
# 切换数据源
在 类/方法 中配置 @DS
注解 , 其注解的值是对应上面从数据源的名称
@GetMapping("/test1")
@DS("slave")
public R<String> test() {
log.info("多数据源应用测试");
TestDemoVo vo = testDemoService.queryById(3L);
log.info("vo => {}", vo);
return R.ok("多数据源应用测试");
}
// 测试结果观察不同库中的字段即可
# 数据源配置
配置数据源一般会在 datasource
节点 下配置多个不同dsName数据源
配置形式
# 多主多从 纯粹多库(记得设置primary) 混合配置
spring: spring: spring:
datasource: datasource: datasource:
dynamic: dynamic: dynamic:
datasource: datasource: datasource:
master_1: mysql: master:
master_2: oracle: slave_1:
slave_1: sqlserver: slave_2:
slave_2: postgresql: oracle_1:
slave_3: h2: oracle_2:
# 动态切换数据源
切换数据源有两种方式
- 声明式 , 采用
@DS
注解 实现 (示例跳转) - 编程式 , 采用
DynamicDataSourceContextHolder
数据源切换工具类
DynamicDataSourceContextHolder
类 , 有如下功能 :
返回 | 方法 | 说明 |
---|---|---|
String | peek() | 获得 当前线程数据源名 |
String | push(String ds) | 设置 当前线程数据源 (切换数据源) |
void | poll() | 清空 当前线程数据源 |
void | clear() | 清空 本地线程 (强制) |
编程式示例
@GetMapping("/test2")
public R<String> test2() {
TestDemoVo vo = testDemoService.queryById(3L);
String peek = DynamicDataSourceContextHolder.peek();
log.info("peek1 => {}\n vo1 => {}", peek, vo);
log.info("============切换至 slave_1");
// 切换数据源
DynamicDataSourceContextHolder.push("slave_1");
TestDemoVo vo2 = testDemoService.queryById(3L);
String peek2 = DynamicDataSourceContextHolder.peek();
DynamicDataSourceContextHolder.poll();
log.info("peek2 => {}\n vo2 => {}", peek2, vo2);
log.info("============清空当前数据源 , 回到主数据源");
// 使用主数据源
TestDemoVo vo3 = testDemoService.queryById(3L);
String peek3 = DynamicDataSourceContextHolder.peek();
log.info("peek3 => {}\n vo3 => {}", peek3, vo3);
DynamicDataSourceContextHolder.clear();
return R.ok("多数据源应用测试");
}
// 测试结果观察不同库中的字段即可
提示
编程式可以在代理模式实现 . 拦截器路由控制数据源切换 点击跳转
注意
异步线程需要重新切换数据源
# 动态解析数据源
通过 @DS
注解 实现对dsName数据源解析访问
支持方式应用 :
- 指定dsName数据源名称
- 请求头参数 (header)
- 会话参数 (session)
- 方法参数
指定dsName数据源名称
匹配配置类中的 datasource
节点 下的不同dsName数据源
点击展开
@GetMapping("/test1")
@DS("slave")
public R<String> test() {
log.info("多数据源应用测试");
TestDemoVo vo = testDemoService.queryById(3L);
log.info("vo => {}", vo);
return R.ok("多数据源应用测试");
}
请求头参数
通过 #header
访问请求头对象
点击展开
@DS("#header.datasource")
@GetMapping("/test3")
public R<String> test3() {
TestDemoVo vo = testDemoService.queryById(3L);
log.info("vo => {}", vo);
return R.ok("多数据源应用测试");
}
方法参数
点击展开
// 单参数
@DS("#datasource")
@GetMapping("/test4/{datasource}")
public R<String> test4(@PathVariable String datasource) {
TestDemoVo vo = testDemoService.queryById2(3L);
log.info("vo => {}", vo);
return R.ok("多数据源应用测试");
}
// 对象参数
// 通过 testKey指定 dsName数据源名
@DS("#bo.testKey")
@GetMapping("/test5")
public R<String> temp5(TestDemoBo bo) {
log.info("bo => {} ", bo);
TestDemoVo vo = testDemoService.queryById2(3L);
log.info("vo => {}", vo);
return R.ok("多数据源应用测试");
}
提示
@DS
注解 切换数据源也支持在类中应用 , 一般情况不建议
如果类和方法同时使用 @DS
注解 , 那么优先应用方法中的注解 (就近原则)
# 负载均衡配置
本框架默认采用轮询策略实现负载均衡 . 将多个从数据源编排为组 , 以组的形式进行轮询 , 前缀作为组名 , 中间以 _
分割 , 后缀作为序号
语法 : <dsName组名>_<序号>
策略类型
- 轮询策略 (默认)
- 随机策略
示例
# 多主多从
spring:
datasource:
dynamic:
# strategy: com.baomidou.dynamic.datasource.strategy.LoadBalanceDynamicDataSourceStrategy # 轮询策略配置(默认)
strategy: com.baomidou.dynamic.datasource.strategy.RandomDynamicDataSourceStrategy # 随机策略配置
datasource:
master:
slave_1:
slave_2:
slave_3:
实现策略
根据 dsName组名
匹配后缀的序号 (slave_1、slave_2、slave_3)
@GetMapping("/test1")
@DS("slave")
public R<String> test() {
log.info("多数据源应用测试");
TestDemoVo vo = testDemoService.queryById(3L);
log.info("vo => {}", vo);
return R.ok("多数据源应用测试");
}
# 动态管理数据源
在运行期间动态的管理数据源正删改查 , 通过 DynamicRoutingDataSource
类 核心组件实现管理
DynamicRoutingDataSource
类 是管理数据源集合的对象 , 当中有多种方法进行对数据源操作管理
DynamicRoutingDataSource
类 常用方法
返回 | 方法 | 说明 |
---|---|---|
Map<String, DataSource> | getDataSources() | 获取 所有数据源 |
Map<String, GroupDataSource> | getGroupDataSources() | 获取 所有数据源组 |
DataSource | getDataSource(String ds) | 获取 指定dsName数据源 |
void | addDataSource(String ds, DataSource dataSource) | 添加 数据源 |
void | addGroupDataSource(String ds, DataSource dataSource) | 添加 数据源组 |
void | removeDataSource(String ds) | 删除 指定dsName数据源 |
代码示例
/**
* 获取所有数据源
*/
// Bean自动注入应用即可 (以下注入采用构造方法隐式注入)
private final DynamicRoutingDataSource dynamicRoutingDataSource;
@GetMapping("/test10")
public R<List<String>> test10() {
// 查询所有数据源
Map<String, DataSource> dataSources = dynamicRoutingDataSource.getDataSources();
dataSources.forEach((k, v) -> log.info("k => {}\n- v => {}\n===============", k, v));
}
/**
* 添加数据源
*/
private final DefaultDataSourceCreator dataSourceCreator;
@GetMapping("/test11")
public R<String> test11() {
// 从数据源配置
DataSourceProperty property = new DataSourceProperty();
property.setDriverClassName("com.mysql.cj.jdbc.Driver");
property.setType(HikariDataSource.class);
property.setUrl("jdbc:mysql://localhost:3306/****?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true");
property.setUsername("root");
property.setPassword("root");
property.setPoolName("bzPool");
// 创建数据源
DataSource creatorDataSource = dataSourceCreator.createDataSource(property);
dynamicRoutingDataSource.addDataSource(property.getPoolName(), creatorDataSource);
return R.ok("添加成功");
}
/**
* 移除数据源
*/
@GetMapping("/test12")
public R<String> test12() {
// 从数据源查询
dynamicRoutingDataSource.removeDataSource("bzPool");
return R.ok("移除成功");
}
更多应用等功能自行测试
# 事务管理
在数据源通信中一般是一个连接一个事务管理 , 一旦涉及多数据源那么常规的事务管理将会失去效果
该框架有解决方案 , 在最外层的方法添加 @DSTransactional
注解 , 实现多数据源事务管理 (基于AOP实现)
示例
点击展开
Controller层
@GetMapping("/test7")
@DSTransactional
public R<String> test7() {
boolean b1 = testDemoService.delById1(2L);
log.info("b1 => {}", b1);
boolean b2 = testDemoService.delById2(3L);
log.info("b2 => {}", b2);
return R.ok("多数据源应用测试");
}
Service层
@DS("slave_1")
@Override
public boolean delById1(Long id) {
String peek = DynamicDataSourceContextHolder.peek();
System.out.println("peek1 = " + peek);
return baseMapper.deleteById(id) > 0;
}
@DS("slave_2")
@Override
public boolean delById2(Long id) {
// 异常回滚测试
//if (true) throw new ServiceException("异常");
String peek = DynamicDataSourceContextHolder.peek();
System.out.println("peek2 = " + peek);
return baseMapper.deleteById(id) > 0;
}
# 拦截器动态切换数据源
拦截器切换可以根据请求信息任意控制数据源的切换 (SpringBoot拦截器如何配置? (opens new window))
示例
/**
* 拦截所有路由即可 (特殊业务自行增加拦截器配置等信息)
*/
public class DynamicInterceptor implements HandlerInterceptor {
/**
* 请求前 切换数据源
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 拦截携带有 slave1 , slave1 的url控制切换
String rui = request.getRequestURI();
// 主数据源
String ds = "master";
if (rui.contains("slave1")) {
ds = "slave1";
}
if (rui.contains("slave2")) {
ds = "slave2";
}
// DIY 路由控制切换数据源
DynamicDataSourceContextHolder.push(ds);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/**
* 请求结束后清空当前线程的数据源
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
DynamicDataSourceContextHolder.clear();
}
}
# 数据源工具
DataBaseHelper
工具类
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class DataBaseHelper {
private static final DynamicRoutingDataSource DS = SpringUtils.getBean(DynamicRoutingDataSource.class);
/**
* 获取当前数据库类型
*/
public static DataBaseType getDataBaseType() {
DataSource dataSource = DS.determineDataSource();
try (Connection conn = dataSource.getConnection()) {
DatabaseMetaData metaData = conn.getMetaData();
String databaseProductName = metaData.getDatabaseProductName();
return DataBaseType.find(databaseProductName);
} catch (SQLException e) {
throw new ServiceException(e.getMessage());
}
}
public static boolean isMySql() {
return DataBaseType.MY_SQL == getDataBaseType();
}
public static boolean isOracle() {
return DataBaseType.ORACLE == getDataBaseType();
}
public static boolean isPostgerSql() {
return DataBaseType.POSTGRE_SQL == getDataBaseType();
}
public static boolean isSqlServer() {
return DataBaseType.SQL_SERVER == getDataBaseType();
}
// 通用兼容数据源拼接模板
public static String findInSet(Object var1, String var2) {
DataBaseType dataBasyType = getDataBaseType();
String var = Convert.toStr(var1);
if (dataBasyType == DataBaseType.SQL_SERVER) {
// charindex(',100,' , ',0,100,101,') <> 0
return "charindex('," + var + ",' , ','+" + var2 + "+',') <> 0";
} else if (dataBasyType == DataBaseType.POSTGRE_SQL) {
// (select position(',100,' in ',0,100,101,')) <> 0
return "(select position('," + var + ",' in ','||" + var2 + "||',')) <> 0";
} else if (dataBasyType == DataBaseType.ORACLE) {
// instr(',0,100,101,' , ',100,') <> 0
return "instr(','||" + var2 + "||',' , '," + var + ",') <> 0";
}
// find_in_set('100' , '0,100,101')
return "find_in_set('" + var + "' , " + var2 + ") <> 0";
}
}