RuoYi-Vue-Plus (多数据源注解使用、【手动、拦截器】切换数据源)

接上文多数据源配置:

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-input
          v-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 字符串
  1. 请求时候带上name入参:GET http://localhost:8080/testDynamic2?name=slave
  2. @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 实体类接受
  1. controller 设置对象的testkey属性值
  2. @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 工具类
  1. DynamicDataSourceContextHolder :核心基于ThreadLocal的切换数据源工具
  2.      DynamicDataSourceContextHolder 使用new ArrayDeque<>链表存储(准确的是栈)原因:
  •       为了支持嵌套切换,如ABC三个service都是不同的数据源
  •      其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。
  •      传统的只设置当前线程的方式不能满足此业务需求,必须使用栈,后进先出

   链表申明代码如下;

    
    private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {
        @Override
        protected 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);

        // 切换数据源 slave
        Console.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);

        // 切换数据源 slave
        Console.log("切换数据源:slave----------------------");
        String slave = DynamicDataSourceContextHolder.push("slave");
        Console.log("已经设置数据源,打印当前数据源:{}", slave);
        //调用完成:清空当前线程数据源
        DynamicDataSourceContextHolder.poll();

        // 切换数据源 master
         Console.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")
    @Transactional
    public void testDynamic7() {
        iTestDemoService.deleteIdMaster(2L);
        iTestDemoService.deleteIdSlave(2L);
    }



//实现类的调用
 @Override
    public 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")
    @DSTransactional
    public void testDynamic7() {
        iTestDemoService.deleteIdMaster(2L);
        iTestDemoService.deleteIdSlave(2L);
    }

结果:删除从库

总结:

在需要切换数据源时候使用 @DSTransactional

不需要时候还是使用原生注解:@Transactional

六、拦截器切换数据源

拦截器切换数据源demo演示:

配置类:

@Configuration
public class DynamicDSConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(new DynamicInterceptor())
           .addPathPatterns("/**");
    }
}

实现类,展示DEMO根据以下三种切换:

  1. 根据request请求判断
  2. 获取请求头参数切换
  3. 根据登录用户切换
@Slf4j
public class DynamicInterceptor implements HandlerInterceptor {

    //请求处理之前调用
    @Override
    public 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;
    }

    //请求处理但是页面未渲染调用
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }

    //请求处理完毕调用
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
    }
}

  • 13
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

syfjava

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值