接上文多数据源配置:
RuoYi-Vue-Plus (多数据源配置)-CSDN博客
一、功能演示
代码生成菜单页面, 展示数据源切换
查询主库
查询从库
二、前端传参切换数据源
页面路径: src/views/tool/gen/index.vue
搜索框如下:下面4发送请求时候,在header带上 要切换数据库
headers: { 'datasource': localStorage.getItem("dataName") },
前端输入框,到发送请求代码如下
1--页面输入框
<el-form-item label="数据源" prop="dataName"><el-inputv-model="queryParams.dataName"placeholder="请输入数据源名称"clearable@keyup.enter.native="handleQuery"/></el-form-item>2--搜索操作/** 搜索按钮操作 */handleQuery() {localStorage.setItem("dataName", this.queryParams.dataName);this.queryParams.pageNum = 1;this.getList();},3-- 查询表集合 getList() {this.loading = true;listTable(this.addDateRange(this.queryParams, this.dateRange)).then(response => {this.tableList = response.rows;this.total = response.total;this.loading = false;});},4-查询生成表数据
export function listTable(query) {return request({headers: { 'datasource': localStorage.getItem("dataName") },url: '/tool/gen/list',method: 'get',params: query})
}
后台实现类标记 @DS("#header.datasource")注解,就完成通过head切换
@DS("#header.datasource")
@Slf4j
@RequiredArgsConstructor
@Service
public class GenTableServiceImpl implements IGenTableService {
。。。。。省略代码
三、字符串、实体类接受参数切换数据源
3.1 字符串
请求时候带上name入参:GET http://localhost:8080/testDynamic2?name=slave- @DS("#name") 实现类标注
//controller@GetMapping("testDynamic2")public void testDynamic2(String name) {TestDemoVo testDemoVo = iTestDemoService.queryById(name,2L);Console.log("打印数据:{}", testDemoVo);} //实现类
@Override@DS("#name")public TestDemoVo queryById(String name, Long id) {return baseMapper.selectVoById(id);}
结果:访问从库成功
打印数据:TestDemoVo(id=2, deptId=102, userId=3, orderNum=2, testKey=从库节点, value=22222, createTime=Tue Jul 23 10:18:35 GMT+08:00 2024, createBy=null, updateTime=null, updateBy=null)
2024-07-24 17:15:12 [XNIO-1 task-1] INFO c.r.f.i.PlusWebInvokeTimeInterceptor
- [PLUS]结束请求 => URL[GET /testDynamic2],耗时:[33]毫秒
3.2 实体类接受
- controller 设置对象的testkey属性值
- @DS("#testDemo.testKey")获取数据源
@GetMapping("testDynamic4")public void testDynamic4() {TestDemo testDemo = new TestDemo();testDemo.setTestKey("slave");TestDemoVo testDemoVo = iTestDemoService.queryById(testDemo,2L);Console.log("打印数据:{}", testDemoVo);}//实现类@Override@DS("#testDemo.testKey")public TestDemoVo queryById(TestDemo testDemo, Long id) {return baseMapper.selectVoById(id);}
结果:访问从库成功
打印数据:TestDemoVo(id=2, deptId=102, userId=3, orderNum=2, testKey=从库节点, value=22222, createTime=Tue Jul 23 10:18:35 GMT+08:00 2024, createBy=null, updateTime=null, updateBy=null)
2024-07-24 17:20:12 [XNIO-1 task-1] INFO c.r.f.i.PlusWebInvokeTimeInterceptor
- [PLUS]结束请求 => URL[GET /testDynamic2],耗时:[31]毫秒
四、手动切换数据源
4.1 DynamicDataSourceContextHolder 工具类
DynamicDataSourceContextHolder :核心基于ThreadLocal的切换数据源工具
- DynamicDataSourceContextHolder 使用new ArrayDeque<>链表存储(准确的是栈)原因:
- 为了支持嵌套切换,如ABC三个service都是不同的数据源
- 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。
- 传统的只设置当前线程的方式不能满足此业务需求,必须使用栈,后进先出。
链表申明代码如下;
private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {@Overrideprotected Deque<String> initialValue() {return new ArrayDeque<>();}};
该工具类提供了 CRUD,如下:
private DynamicDataSourceContextHolder() {}/*** 获得当前线程数据源** @return 数据源名称*/public static String peek() {return LOOKUP_KEY_HOLDER.get().peek();}/*** 设置当前线程数据源* <p>* 如非必要不要手动调用,调用后确保最终清除* </p>** @param ds 数据源名称*/public static String push(String ds) {String dataSourceStr = StringUtils.isEmpty(ds) ? "" : ds;LOOKUP_KEY_HOLDER.get().push(dataSourceStr);return dataSourceStr;}/*** 清空当前线程数据源* <p>* 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称* </p>*/public static void poll() {Deque<String> deque = LOOKUP_KEY_HOLDER.get();deque.poll();if (deque.isEmpty()) {LOOKUP_KEY_HOLDER.remove();}}/*** 强制清空本地线程* <p>* 防止内存泄漏,如手动调用了push可调用此方法确保清除* </p>*/public static void clear() {LOOKUP_KEY_HOLDER.remove();}
4.2测试手动切换数据源
演示一:手动切换到 slave 从库,并打印结果
@GetMapping("testDynamic5")public void testDynamic5() {TestDemoVo testDemoVo = iTestDemoService.queryById(2L);Console.log("打印数据:{}", testDemoVo);// 打印当前数据源String peek = DynamicDataSourceContextHolder.peek();Console.log("未设置数据源,打印当前数据源:{}", peek);// 切换数据源 slaveConsole.log("切换数据源:slave----------------------");String slave = DynamicDataSourceContextHolder.push("slave");Console.log("已经设置数据源,打印当前数据源:{}", slave);//调用完成:清空当前线程数据源DynamicDataSourceContextHolder.poll();//最后:强制清空本地线程DynamicDataSourceContextHolder.clear();}
运行结果:
打印数据:TestDemoVo(id=2, deptId=102, userId=3, orderNum=2, testKey=主库节点, value=22222, createTime=Tue Jul 23 10:18:35 GMT+08:00 2024, createBy=null, updateTime=null, updateBy=null)
未设置数据源,打印当前数据源:null
切换数据源:slave----------------------
已经设置数据源,打印当前数据源:slave
演示二:切换从库后,再次访问主库
代码:
@GetMapping("testDynamic5")public void testDynamic5() {TestDemoVo testDemoVo = iTestDemoService.queryById(2L);Console.log("打印数据:{}", testDemoVo);// 打印当前数据源String peek = DynamicDataSourceContextHolder.peek();Console.log("未设置数据源,打印当前数据源:{}", peek);// 切换数据源 slaveConsole.log("切换数据源:slave----------------------");String slave = DynamicDataSourceContextHolder.push("slave");Console.log("已经设置数据源,打印当前数据源:{}", slave);//调用完成:清空当前线程数据源DynamicDataSourceContextHolder.poll();// 切换数据源 masterConsole.log("切换数据源:master----------------------");String master = DynamicDataSourceContextHolder.push("master");Console.log("已经设置数据源,打印当前数据源:{}", master);//调用完成:清空当前线程数据源DynamicDataSourceContextHolder.poll();//最后:强制清空本地线程DynamicDataSourceContextHolder.clear();}
运行结果:访问主库
打印数据:TestDemoVo(id=2, deptId=102, userId=3, orderNum=2, testKey=主库节点, value=22222, createTime=Tue Jul 23 10:18:35 GMT+08:00 2024, createBy=null, updateTime=null, updateBy=null)
未设置数据源,打印当前数据源:null
切换数据源:slave----------------------
已经设置数据源,打印当前数据源:slave
切换数据源:master----------------------
已经设置数据源,打印当前数据源:master
演示三: 切换线程时候访问数据源
private final ThreadPoolTaskExecutor threadPoolTaskExecutor;/*** <简述>new 线程切换数据源* <详细描述>* @author syf* @date 2024/7/24 17:19*/@GetMapping("testDynamic6")public void testDynamic6() {TestDemoVo testDemoVo = iTestDemoService.queryById(2L);Console.log("打印数据:{}", testDemoVo);threadPoolTaskExecutor.submit(() -> {Console.log("切换数据源:slave----------------------");String slave = DynamicDataSourceContextHolder.push("slave");Console.log("已经设置数据源,打印当前数据源:{}", slave);TestDemoVo testDemoVo2 = iTestDemoService.queryById(2L);Console.log("新线程打印数据:{}", testDemoVo2);//调用完成:清空当前线程数据源DynamicDataSourceContextHolder.poll();//最后:强制清空本地线程DynamicDataSourceContextHolder.clear();});}
五、多数据源事务处理
5.1 数据源失效场景
场景:
实现类一个调用主库,另外一个 @DS("slave")标注调用从库。结果却是更新主库数据,如下截图:
- @Transactional 原生注解标注,会保证整个线程拿到的都是同一个连接,所以上面都更下主库
- 我们刚进入线程时候用的是主数据源,又因为有@Transactional 所以切换数据源也不生效
@GetMapping("testDynamic7")@Transactionalpublic void testDynamic7() {iTestDemoService.deleteIdMaster(2L);iTestDemoService.deleteIdSlave(2L);}//实现类的调用@Overridepublic void deleteIdMaster(Long id) {baseMapper.deleteById(id);}@Override@DS("slave")public void deleteIdSlave(Long id) {baseMapper.deleteById(id);}
执行结果: 标注更新从库,但是删除的是主库
5.2 解决办法
基于上面:
@Transactional 是基于数据库实现的事务
解决:
@DSTransactional 是基于AOP实现的事务
@GetMapping("testDynamic7")@DSTransactionalpublic void testDynamic7() {iTestDemoService.deleteIdMaster(2L);iTestDemoService.deleteIdSlave(2L);}
结果:删除从库
总结:
在需要切换数据源时候使用 @DSTransactional
不需要时候还是使用原生注解:@Transactional
六、拦截器切换数据源
拦截器切换数据源demo演示:
配置类:
@Configuration
public class DynamicDSConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new DynamicInterceptor()).addPathPatterns("/**");}
}
实现类,展示DEMO根据以下三种切换:
- 根据request请求判断
- 获取请求头参数切换
- 根据登录用户切换
@Slf4j
public class DynamicInterceptor implements HandlerInterceptor {//请求处理之前调用@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1-根据请求判断String requestURI = request.getRequestURI();log.info("requestURI:{}", requestURI);String ds = "";if (requestURI.contains("/testDynamic7")){ds = "slave";}//2-根据请求头动态切换String datasource = request.getHeader("datasource");if (StringUtils.isNotBlank(datasource)){ds = datasource;}//3- 更具登录用户动态切换LoginUser loginUser = null;try {loginUser = LoginHelper.getLoginUser();log.info("loginUser:{}", loginUser);if("admin".equals(loginUser.getUsername())){ds = "master";}}catch (Exception e){}DynamicDataSourceContextHolder.push(ds);return true;}//请求处理但是页面未渲染调用@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {}//请求处理完毕调用@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {}
}