
01 引言
上一节我们使用Qoder完成了动态数据源的demo,测试结果也没有让人失望。但是生成的代码会给我们带来什么样的思考,如果是我们自己实现,会不会考虑比Agent更加全面呢?
我们带着阅读、学习的态度了解多数据源的动态切换的核心思路。
02 为什么需要动态多数据源
业务场景驱动技术选型:
- 读写分离:主库写入,从库读取,提升并发能力
- 数据隔离:不同业务模块使用独立数据库
- 分库分表:业务增长后的必然选择
- 灰度发布:新旧数据库并行,逐步迁移流量
核心诉求:在代码无感知的情况下,根据业务需求自动切换数据源。
主要的解决思路分大概两种:
- 继承
AbstractRoutingDataSource - 使用数据库的
schema
继承AbstractRoutingDataSource 今天主要学习的技术,简单说一下数据库的schema。
数据库的schema的前提是一个账号具有多个数据库的读写权限,通过任意一个数据库连接,来操作所有的数据库。每一个SQL都必须通过tableName.表名的形式。
sql
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
java
/**
* 多数据源切换注解
* 用于在方法或类级别指定数据源
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
/**
* 数据源名称
* @return 数据源 key
*/
String value() default "master";
}
关键点:
- 支持方法级和类级,方法级优先级更高
- 默认值避免遗忘配置
- 运行时保留供
AOP识别
3.2 ThreadLocal 上下文管理
ThreadLocal是保持同一线程同一数据源的关键。
java
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 动态路由数据源
java
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
/**
* 决定当前使用哪个数据源
* @return 数据源 key
*/
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceKey();
}
}
AbstractRoutingDataSource是spring-jdbc留给我们的扩展点。
Spring 的巧妙设计:
- 每次获取数据库连接前,自动调用
determineCurrentLookupKey() - 从
ThreadLocal获取当前应使用的数据源 key - 自动从目标数据源 Map 中查找并返回对应数据源
3.4 AOP自动切换
使用注解自然少不了AOP的切面编程,我们需要通过@DataSource注解获取运行时数据源的信息。
java
@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 配置
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: root
4.2 配置类
java
@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编程的深入,后面可能我们都不太关注代码该怎么写了,从一个执行者转变成管理者,手搓代码可能真的要变成大家调侃的古法编程了。