接上文多数据源配置:
一、功能演示
代码生成菜单页面, 展示数据源切换
查询主库
查询从库
二、前端传参切换数据源
页面路径: 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 字符串
请求时候带上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") {
@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根据以下三种切换:
- 根据request请求判断
- 获取请求头参数切换
- 根据登录用户切换
@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 {
}
}