
01 引言
上一节我们使用qoder完成了动态数据源的demo,测试结果也没有让人失望。但是生成的代码会给我们带来什么样的思考,如果是我们自己实现,会不会考虑比agent更加全面呢?
我们带着阅读、学习的态度了解多数据源的动态切换的核心思路。
02 为什么需要动态多数据源
业务场景驱动技术选型:
- 读写分离:主库写入,从库读取,提升并发能力
- 数据隔离:不同业务模块使用独立数据库
- 分库分表:业务增长后的必然选择
- 灰度发布:新旧数据库并行,逐步迁移流量
核心诉求:在代码无感知的情况下,根据业务需求自动切换数据源。
主要的解决思路分大概两种:
- 继承
abstractroutingdatasource - 使用数据库的
schema
继承abstractroutingdatasource 今天主要学习的技术,简单说一下数据库的schema。
数据库的schema的前提是一个账号具有多个数据库的读写权限,通过任意一个数据库连接,来操作所有的数据库。每一个sql都必须通过tablename.表名的形式。
select * from test.user where id=1; update test.user set name='ai' where id=1
03 实现思路
通过注解的方式,类似mybaits-plus的动态数据源dynamic-datasource-spring-boot-starter的注解@ds
3.1 定义数据源注解
我们这里直接使用datasource
/**
* 多数据源切换注解
* 用于在方法或类级别指定数据源
*/
@target({elementtype.method, elementtype.type})
@retention(retentionpolicy.runtime)
@documented
public @interface datasource {
/**
* 数据源名称
* @return 数据源 key
*/
string value() default "master";
}关键点:
- 支持方法级和类级,方法级优先级更高
- 默认值避免遗忘配置
- 运行时保留供
aop识别
3.2 threadlocal 上下文管理
threadlocal是保持同一线程同一数据源的关键。
public class datasourcecontextholder {
private static final threadlocal<string> context_holder = new threadlocal<>();
public static void setdatasourcekey(string key) {
context_holder.set(key);
}
public static string getdatasourcekey() {
return context_holder.get();
}
public static void cleardatasourcekey() {
context_holder.remove(); // ⚠️ 必须清理!
}
}关键点:
threadlocal保证线程隔离,无需加锁- 每个请求线程独立维护数据源上下文
- 必须清理:防止线程池复用导致的数据污染
3.3 动态路由数据源
public class dynamicroutingdatasource extends abstractroutingdatasource {
/**
* 决定当前使用哪个数据源
* @return 数据源 key
*/
@override
protected object determinecurrentlookupkey() {
return datasourcecontextholder.getdatasourcekey();
}
}abstractroutingdatasource是spring-jdbc留给我们的扩展点。
spring 的巧妙设计:
- 每次获取数据库连接前,自动调用
determinecurrentlookupkey() - 从
threadlocal获取当前应使用的数据源 key - 自动从目标数据源 map 中查找并返回对应数据源
3.4aop自动切换
使用注解自然少不了aop的切面编程,我们需要通过@datasource注解获取运行时数据源的信息。
@aspect
@component
@order(1) // ⚠️ 关键:必须小于事务切面的 order
public class datasourceaspect {
@pointcut("@annotation(datasource) || @within(datasource)")
public void datasourcepointcut() {}
@before("datasourcepointcut()")
public void before(joinpoint joinpoint) {
method method = ((methodsignature) joinpoint.getsignature()).getmethod();
// 优先方法注解,其次类注解
datasource datasource = method.getannotation(datasource.class);
if (datasource == null) {
datasource = joinpoint.gettarget().getclass().getannotation(datasource.class);
}
if (datasource != null) {
datasourcecontextholder.setdatasourcekey(datasource.value());
}
}
@after("datasourcepointcut()")
public void after(joinpoint joinpoint) {
datasourcecontextholder.cleardatasourcekey();
}
}执行流程:
- 方法执行前 → 解析注解 → 设置数据源 key
- 执行业务逻辑 → spring 路由到对应数据源
- 方法执行后 → 清理
threadlocal→ 防止内存泄漏
关键点:
- 优先方法注解,其次类注解
- 切换数据源的
order必须小于事务切面的order,确保切换数据源在事务之前执行。
04 配置绑定
上面是整个数据源动态切换的整体思路,但是数据源怎么配置呢?
4.1 yml 配置
spring:
application:
name: boot-qoder
datasource:
# master 数据源配置
master:
driver-class-name: com.mysql.cj.jdbc.driver
jdbc-url: jdbc:mysql://127.0.0.1:3306/db_master?useunicode=true&characterencoding=utf8&usessl=false&servertimezone=asia/shanghai
username: root
password: root
# slave1 数据源配置
slave1:
driver-class-name: com.mysql.cj.jdbc.driver
jdbc-url: jdbc:mysql://127.0.0.1:3306/db_slave1?useunicode=true&characterencoding=utf8&usessl=false&servertimezone=asia/shanghai
username: root
password: root
# slave2 数据源配置
slave2:
driver-class-name: com.mysql.cj.jdbc.driver
jdbc-url: jdbc:mysql://127.0.0.1:3306/db_slave2?useunicode=true&characterencoding=utf8&usessl=false&servertimezone=asia/shanghai
username: root
password: root4.2 配置类
@configuration
@mapperscan(basepackages = "com.example.bootqoder.mapper", sqlsessiontemplateref = "sqlsessiontemplate")
public class datasourceconfig {
/**
* master 数据源
* @return 数据源实例
*/
@bean(name = "masterdatasource")
@configurationproperties(prefix = "spring.datasource.master")
public datasource masterdatasource() {
return datasourcebuilder.create()
.type(com.zaxxer.hikari.hikaridatasource.class)
.build();
}
/**
* slave1 数据源
* @return 数据源实例
*/
@bean(name = "slave1datasource")
@configurationproperties(prefix = "spring.datasource.slave1")
public datasource slave1datasource() {
return datasourcebuilder.create()
.type(com.zaxxer.hikari.hikaridatasource.class)
.build();
}
/**
* slave2 数据源
* @return 数据源实例
*/
@bean(name = "slave2datasource")
@configurationproperties(prefix = "spring.datasource.slave2")
public datasource slave2datasource() {
return datasourcebuilder.create()
.type(com.zaxxer.hikari.hikaridatasource.class)
.build();
}
/**
* 创建动态路由数据源
* @return 动态数据源
*/
@bean(name = "dynamicdatasource")
@primary
public datasource dynamicdatasource(
@qualifier("masterdatasource") datasource masterdatasource,
@qualifier("slave1datasource") datasource slave1datasource,
@qualifier("slave2datasource") datasource slave2datasource) {
dynamicroutingdatasource routingdatasource = new dynamicroutingdatasource();
// 配置多个数据源
map<object, object> targetdatasources = new hashmap<>();
targetdatasources.put("master", masterdatasource);
targetdatasources.put("slave1", slave1datasource);
targetdatasources.put("slave2", slave2datasource);
// 设置目标数据源
routingdatasource.settargetdatasources(targetdatasources);
// 设置默认数据源
routingdatasource.setdefaulttargetdatasource(masterdatasource);
return routingdatasource;
}
/**
* 创建 sqlsessionfactory
* @param datasource 数据源
* @return sqlsessionfactory
* @throws exception 异常
*/
@bean(name = "sqlsessionfactory")
@primary
public sqlsessionfactory sqlsessionfactory(@qualifier("dynamicdatasource") datasource datasource) throws exception {
mybatissqlsessionfactorybean sessionfactory = new mybatissqlsessionfactorybean();
sessionfactory.setdatasource(datasource);
// 设置 mapper xml 文件位置
pathmatchingresourcepatternresolver resolver = new pathmatchingresourcepatternresolver();
sessionfactory.setmapperlocations(resolver.getresources("classpath:mapper/*.xml"));
// 设置 mybatis plus 配置
mybatisconfiguration configuration = new mybatisconfiguration();
configuration.setmapunderscoretocamelcase(true);
sessionfactory.setconfiguration(configuration);
return sessionfactory.getobject();
}
/**
* 创建 sqlsessiontemplate
* @param sqlsessionfactory sqlsessionfactory
* @return sqlsessiontemplate
*/
@bean(name = "sqlsessiontemplate")
@primary
public sqlsessiontemplate sqlsessiontemplate(@qualifier("sqlsessionfactory") sqlsessionfactory sqlsessionfactory) {
return new sqlsessiontemplate(sqlsessionfactory);
}
}我们需要单独每一个数据源都要交给spring管理:
masterdatasourceslave1datasourceslave2datasource
然后才能创建动态数据源,统一由dynamicdatasource管理,返回的数据源类型为com.example.bootqoder.datasource.dynamicroutingdatasource,就是3.3创建的动态路由数据源。
最后需要设置sqlsessionfactory,由于使用了mybaits plus。就需要将动态数据源设置到com.baomidou.mybatisplus.extension.spring.mybatissqlsessionfactorybean里面去。
05 小结
个人感觉ai生成的代码,比我预想的要好多了。动态切换数据源,我事先没有考虑过在事务之后切换数据源的异常,但是ai考虑到了。
随着ai编程的深入,后面可能我们都不太关注代码该怎么写了,从一个执行者转变成管理者,手搓代码可能真的要变成大家调侃的古法编程了。
到此这篇关于spring boot 动态多数据源核心思路与关键介绍的文章就介绍到这了,更多相关spring boot 动态多数据源内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论