技术派自定义多数据源方案(👍强烈推荐)
一个项目使用多数据源这种场景可以说是很常见的了,当然这里说的多数据源并不是专指一个项目,连接几个不同的数据库(对于信奉微服务治理美学的小伙伴而言,一个微服务是不建议出现连接多个不同数据库的), 相比于此,更有可能出现的则是主从数据库的配置
主库负责读写,从库专指读查询;那么我们一般针对这种多数据源,可以怎么显现呢?
接下来我们以技术派的实际实现来进行说明,手把手教你实现一个自定义的多数据源动态切换方案
本文主要涉及以下知识点:
- 动态数据源切换
- AOP
- mybatis拦截器
- 上下文
- 函数式方法
- 自定义配置加载解析
环境准备
考虑到直接再本机搭建一个主从的数据库比较麻烦,所以我们根据账号权限的方式来模拟主从数据源的配置
- 主库:拥有完整的读写权限
- 从库:只拥有读权限
因此我们只需要再自己的服务器上,新建一个只读的账号即可,操作方式如下
- 创建账号
创建一个只读账号,userName = 'readonly', password = ''
CREATE USER 'readonly'@'%' IDENTIFIED BY '';
- 授予技术派db的只读权限
grant select on pai_coding.* to 'readonly'@'%';
- 刷新权限
FLUSH PRIVILEGES;
方案设计
基础知识点
主要借助AbstractRoutingDataSource来实现动态数据源,简单介绍下它的几个核心要点:
defaultTargetDataSource:表示默认使用的数据源Map: 动态数据源可以根据key来切换使用的数据源
protected Object determineCurrentLookupKey() {
// 返回上面 targetDataSources 对应的key
// 根据这个key,来抉择最终实际选择的数据源时哪个
}
重点关注上面的这个方法,当我们想使用哪个数据源时,就发回这个数据源对应的key,在实际代码执行时,就根据这个key,到 targetDataSources这个map中找对应的数据源
设计方案
基于上面的动态数据源的几个核心知识点,所以当我们需要实现动态数据源切换时,自然而然可以想到的一个方案就是在每次db执行之前,塞入这个希望使用的数据源key

如上的设计思路,主要借助aop的思想来实现,通过上下文来存储当前方法的执行,具体希望使用的数据源,然后再执行的sql的时候,AbstractRoutingDataSource直接从上下文中获取key,以此来抉择具体的数据源
基于上面的方案,接下来我们补充一下细节
1. 数据源选择方式
通过AOP的方式进行拦截,写入数据源选择,这种方式适用于方法级别的粒度,基于此常见的实现方式就是通过自定义注解,来标注需要使用的数据源
如自定义注解,通过设置ds值来指定这个方法执行时选择的数据源
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DsAno {
/**
* 启用的数据源
*
* @return
*/
String ds() default "";
}
但是,请注意上面这种方式不适用于代码块维度的数据源选择策略
2. 上下文
通过上下文来保存当前选中的数据源,因此可以借助 ThreadLocal 来实现
一个简单的数据源选择上下文
public class DsContextHolder{
private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>();
}
3. 动态数据源选择
自定义的动态数据源路由方案
protected Object determineCurrentLookupKey() {
return DsContextHolder.get();
}
4. aop拦截
借助AOP,拦截方法、类上拥有DS注解的方法执行,然后从这个注解中读取选中的数据源,然后写入上下文;再方法执行完毕之后,清空上下文
基本的实现策略
/**
* 切入点, 拦截类上、方法上有注解的方法,用于切换数据源
*/
@Pointcut("@annotation(com.github.paicoding.forum.core.dal.DsAno) || @within(com.github.paicoding.forum.core.dal.DsAno)")
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
DsAno ds = getDsAno(proceedingJoinPoint);
try {
if (ds != null && (StringUtils.isNotBlank(ds.ds()) || ds.value() ...未能定位到原文的划线
企业级Agent工作流编排项目PaiFlow
Vibe Coding版本的PaiAgent
派聪明RAG AI知识库Java版本+Go版本
微服务 PmHub、技术派、MYDB
求职派JobClaw(OpenClaw/Hermes架构
PaiCLI(类似Claude Code的Agent
派简历(代码已完成)
等实战项目。
1. 微信扫右侧的优惠券加入知识星球
2. 解锁星球的实战项目教程和源码: 项目源码+教程获取
真诚点赞 诚不我欺
回复