前言
咱们星球中的商城系统中使用了动态数据源的功能,实现了分库分表的订单库的读库和写库的自动切换。
有球友反馈说,对动态数据源不太熟悉。
今天这篇文章就专门跟大家一起聊聊动态数据源,希望对你会有所帮助。
一、为什么需要动态数据源?
有些小伙伴在开发中可能会遇到这样的场景:一个系统需要同时访问多个数据库,或者需要根据业务参数动态选择数据源。这
时候,传统的单数据源配置就显得力不从心了。
1.1 传统多数据源的问题
传统方式的多个数据源配置,硬编码,不灵活。
例如下面这样:
java
@Configuration
public class TraditionalDataSourceConfig {
@Bean
@Primary
public DataSource primaryDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/db1");
dataSource.setUsername("user1");
dataSource.setPassword("pass1");
return dataSource;
}
@Bean
public DataSource secondaryDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/db2");
dataSource.setUsername("user2");
dataSource.setPassword("pass2");
return dataSource;
}
}
使用时需要手动管理数据源。
java
@Repository
public class TraditionalUserDao {
@Autowired
@Qualifier("primaryDataSource")
private DataSource primaryDataSource;
@Autowired
@Qualifier("secondaryDataSource")
private DataSource secondaryDataSource;
public User findUserFromPrimary(Long id) {
// 需要手动获取连接、处理异常、关闭连接
try (Connection conn = primaryDataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
stmt.setLong(1, id);
ResultSet rs = stmt.executeQuery();
// 处理结果集...
} catch (SQLException e) {
throw new RuntimeException("查询失败", e);
}
}
}
每个方法都要重复这样的模板代码,需要手动指定数据源,很麻烦。
那么,如何做优化呢?
1.2 动态数据源的优势
接下来,我们一起看看使用动态数据源后的优雅代码。
java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 根据租户ID自动选择数据源
public User findUserByTenant(Long userId, String tenantId) {
// 设置数据源上下文
DataSourceContextHolder.setDataSource(tenantId);
try {
return userMapper.selectById(userId);
} finally {
// 清理上下文
DataSourceContextHolder.clear();
}
}
// 多租户数据聚合查询
public UserAggregateInfo getUserAggregateInfo(Long userId) {
UserAggregateInfo result = new UserAggregateInfo();
// 查询主库
DataSourceContextHolder.setDataSource("master");
result.setBaseInfo(userMapper.selectById(userId));
// 查询归档库
DataSourceContextHolder.setDataSource("archive");
result.setHistory(userMapper.selectHistory(userId));
// 查询统计库
DataSourceContextHolder.setDataSource("stats");
result.setStatistics(userMapper.selectStats(userId));
return result;
}
}
代码中能根据租户ID自动选择数据源。
代码一下子变得更优雅了。
二、动态数据源的原理
有些小伙伴在使用动态数据源时,可能只是简单配置使用,并不清楚其底层工作原理。
理解核心原理对于排查问题和性能优化至关重要。
下面跟大家一起聊聊动态数据源的核心原理,希望对你会有所帮助。
数据源路由的核心机制
动态数据源的核心在于AbstractRoutingDataSource,它是Spring框架提供的抽象类:
java
// Spring AbstractRoutingDataSource 源码分析
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
// 目标数据源映射表
private Map<Object, Object> targetDataSources;
// 默认数据源
private Object defaultTargetDataSource;
// 解析后的数据源映射
private Map<Object, DataSource> resolvedDataSources;
// 解析后的默认数据源
private DataSource resolvedDefaultDataSource;
// 关键方法:确定当前查找键
protected abstract Object determineCurrentLookupKey();
// 获取连接时选择数据源
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
// 确定目标数据源
protected DataSource determineTargetDataSource() {
// 获取查找键
Object lookupKey = determineCurrentLookupKey();
// 根据查找键获取数据源
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.resolvedDefaultDataSource != null || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
}
线程安全的数据源上下文管理
java
/**
* 数据源上下文管理器 - 核心组件
* 使用ThreadLocal保证线程安全
*/
public class DataSourceContextHolder {
// 使用ThreadLocal保证线程隔离
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
// 支持嵌套数据源切换的栈
private static final ThreadLocal<Deque<String>> DATASOURCE_STACK = ThreadLocal.withInitial(ArrayDeque::new);
// 设置数据源
public static void setDataSource(String dataSource) {
if (dataSource == null) {
throw new IllegalArgumentException("数据源不能为null");
}
CONTEXT_HOLDER.set(dataSource);
// 同时压入栈,支持嵌套调用
DATASOURCE_STACK.get().push(dataSource);
}
// 获取当前数据源
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
// 清除数据源
public static void clear() {
CONTEXT_HOLDER.remove();
Deque<String> stack = DATASOURCE_STACK.get();
if (!stack.isEmpty()) {
stack.pop();
// 如果栈中还有元素,恢复到上一个数据源
if (!stack.isEmpty()) {
CONTEXT_HOLDER.set(stack.peek());
}
}
}
// 强制清除所有上下文(用于线程池场景)
public static void clearCompletely() {
CONTEXT_HOLDER.remove();
DATASOURCE_STACK.get().clear();
}
// 判断是否已设置数据源
public static boolean hasDataSource() {
return CONTEXT_HOLDER.get() != null;
}
}
/**
* 自定义路由数据源
*/
@Component
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
// 所有可用的数据源
private final Map<Object, Object> targetDataSources = new ConcurrentHashMap<>();
@Override
protected Object determineCurrentLookupKey() {
String dataSourceKey = DataSourceContextHolder.getDataSource();
if (dataSourceKey == null) {
// 返回默认数据源
return "default";
}
// 验证数据源是否存在
if (!targetDataSources.containsKey(dataSourceKey)) {
throw new IllegalArgumentException("数据源 " + dataSourceKey + " 不存在");
}
logger.debug("当前使用数据源: {}", dataSourceKey);
return dataSourceKey;
}
// 添加数据源
public void addDataSource(String key, DataSource dataSource) {
this.targetDataSources.put(key, dataSource);
// 更新目标数据源映射
setTargetDataSources(new HashMap<>(this.targetDataSources));
// 重新初始化
afterPropertiesSet();
}
// 移除数据源
public void removeDataSource(String key) {
if (this.targetDataSources.containsKey(key)) {
DataSource dataSource = (DataSource) this.targetDataSources.remove(key);
// 关闭数据源连接池
closeDataSource(dataSource);
// 更新目标数据源映射
setTargetDataSources(new HashMap<>(this.targetDataSources));
afterPropertiesSet();
}
}
// 获取所有数据源
public Map<Object, Object> getTargetDataSources() {
return Collections.unmodifiableMap(targetDataSources);
}
private void closeDataSource(DataSource dataSource) {
if (dataSource instanceof HikariDataSource) {
((HikariDataSource) dataSource).close();
} else if (dataSource instanceof org.apache.tomcat.jdbc.pool.DataSource) {
((org.apache.tomcat.jdbc.pool.DataSource) dataSource).close();
}
// 其他类型的数据源关闭逻辑...
}
}
动态数据源执行流程
三、基于Spring Boot的完整实现
有些小伙伴在配置动态数据源时可能会遇到各种问题,下面我提供一个生产级别的完整实现。
完整配置实现
java
/**
* 动态数据源配置类
*/
@Configuration
@EnableTransactionManagement
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceConfig {
@Autowired
private DynamicDataSourceProperties properties;
/**
* 主数据源(默认数据源)
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
/**
* 从数据源1
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave1")
public DataSource slave1DataSource() {
return DataSourceBuilder.create().build();
}
/**
* 从数据源2
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave2")
public DataSource slave2DataSource() {
return DataSourceBuilder.create().build();
}
/**
* 动态数据源
*/
@Bean
@Primary
public DataSource dynamicDataSource(DataSource masterDataSource,
DataSource slave1DataSource,
DataSource slave2DataSource) {
Map<Object, Object> targetDataSources = new HashMap<>(8);
targetDataSources.put("master", masterDataSource);
targetDataSources.put("slave1", slave1DataSource);
targetDataSources.put("slave2", slave2DataSource);
DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
// 设置默认数据源
dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
// 设置目标数据源
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
/**
* 事务管理器
*/
@Bean
public PlatformTransactionManager transactionManager(DataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
/**
* MyBatis配置
*/
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dynamicDataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dynamicDataSource);
// 配置MyBatis
org.apache.ibatis.session.Configuration configuration =
new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(true);
configuration.setLazyLoadingEnabled(false);
configuration.setAggressiveLazyLoading(false);
sessionFactory.setConfiguration(configuration);
return sessionFactory.getObject();
}
}
/**
* 数据源配置属性类
*/
@ConfigurationProperties(prefix = "spring.datasource")
@Data
public class DynamicDataSourceProperties {
/**
* 主数据源配置
*/
private HikariConfig master = new HikariConfig();
/**
* 从数据源1配置
*/
private HikariConfig slave1 = new HikariConfig();
/**
* 从数据源2配置
*/
private HikariConfig slave2 = new HikariConfig();
/**
* 动态数据源配置
*/
private DynamicConfig dynamic = new DynamicConfig();
@Data
public static class DynamicConfig {
/**
* 默认数据源
*/
private String primary = "master";
/**
* 是否开启严格模式
*/
private boolean strict = false;
/**
* 数据源健康检查间隔(秒)
*/
private long healthCheckInterval = 30;
}
}
应用配置文件
yaml
# application.yml
spring:
datasource:
# 动态数据源配置
dynamic:
primary: master
strict: true
health-check-interval: 30
# 主数据源
master:
jdbc-url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true
username: root
password: master_password
driver-class-name: com.mysql.cj.jdbc.Driver
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
pool-name: MasterHikariPool
# 从数据源1
slave1:
jdbc-url: jdbc:mysql://slave1:3306/slave_db?useUnicode=true&characterEncoding=utf8
username: root
password: slave1_password
driver-class-name: com.mysql.cj.jdbc.Driver
maximum-pool-size: 15
minimum-idle: 3
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
pool-name: Slave1HikariPool
# 从数据源2
slave2:
jdbc-url: jdbc:mysql://slave2:3306/slave_db?useUnicode=true&characterEncoding=utf8
username: root
password: slave2_password
driver-class-name: com.mysql.cj.jdbc.Driver
maximum-pool-size: 15
minimum-idle: 3
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
pool-name: Slave2HikariPool
# MyBatis配置
mybatis:
configuration:
map-underscore-to-camel-case: true
cache-enabled: true
lazy-loading-enabled: false
aggressive-lazy-loading: false
注解式数据源切换
java
/**
* 数据源注解
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
/**
* 数据源名称
*/
String value() default "master";
/**
* 是否在方法执行后清除数据源(默认清除)
*/
boolean clear() default true;
}
/**
* 数据源切面
*/
@Aspect
@Component
@Slf4j
public class DataSourceAspect {
/**
* 定义切点:所有标注@DataSource注解的方法
*/
@Pointcut("@annotation(com.example.annotation.DataSource)")
public void dataSourcePointCut() {}
/**
* 环绕通知:在方法执行前后切换数据源
*/
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DataSource dataSourceAnnotation = method.getAnnotation(DataSource.class);
if (dataSourceAnnotation == null) {
// 类级别注解
dataSourceAnnotation = point.getTarget().getClass().getAnnotation(DataSource.class);
}
if (dataSourceAnnotation != null) {
String dataSourceKey = dataSourceAnnotation.value();
boolean clearAfter = dataSourceAnnotation.clear();
try {
log.debug("切换数据源到: {}", dataSourceKey);
DataSourceContextHolder.setDataSource(dataSourceKey);
// 执行原方法
return point.proceed();
} finally {
if (clearAfter) {
DataSourceContextHolder.clear();
log.debug("清除数据源上下文");
}
}
}
// 没有注解,使用默认数据源
return point.proceed();
}
}
四、高级特性
有些小伙伴在基础功能实现后,可能会遇到一些高级场景的需求。
下面介绍几个生产环境中常用的高级特性。
读写分离自动路由
java
/**
* 读写分离数据源路由器
*/
@Component
@Slf4j
public class ReadWriteDataSourceRouter {
// 读数据源列表
private final List<String> readDataSources = Arrays.asList("slave1", "slave2");
// 轮询计数器
private final AtomicInteger counter = new AtomicInteger(0);
/**
* 根据SQL自动选择数据源
*/
public String determineDataSource(boolean isReadOperation) {
if (isReadOperation && !readDataSources.isEmpty()) {
// 读操作:轮询选择从库
int index = counter.getAndIncrement() % readDataSources.size();
if (counter.get() > 9999) {
counter.set(0); // 防止溢出
}
String readDataSource = readDataSources.get(index);
log.debug("读操作选择数据源: {}", readDataSource);
return readDataSource;
} else {
// 写操作:选择主库
log.debug("写操作选择数据源: master");
return "master";
}
}
/**
* 根据SQL语句判断是否为读操作
*/
public boolean isReadOperation(String sql) {
if (sql == null) {
return true; // 默认为读操作
}
String trimmedSql = sql.trim().toLowerCase();
return trimmedSql.startsWith("select") ||
trimmedSql.startsWith("show") ||
trimmedSql.startsWith("explain");
}
}
/**
* MyBatis拦截器 - 自动读写分离
*/
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
@Component
@Slf4j
public class ReadWriteInterceptor implements Interceptor {
@Autowired
private ReadWriteDataSourceRouter dataSourceRouter;
@Override
public Object intercept(Invocation invocation) throws Throwable {
String methodName = invocation.getMethod().getName();
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
boolean isReadOperation = "query".equals(methodName);
String sql = getSql(ms, invocation.getArgs()[1]);
// 如果当前没有手动设置数据源,则自动选择
if (!DataSourceContextHolder.hasDataSource()) {
String dataSource = dataSourceRouter.determineDataSource(isReadOperation);
DataSourceContextHolder.setDataSource(dataSource);
try {
return invocation.proceed();
} finally {
DataSourceContextHolder.clear();
}
}
return invocation.proceed();
}
private String getSql(MappedStatement ms, Object parameter) {
BoundSql boundSql = ms.getBoundSql(parameter);
return boundSql.getSql();
}
}
多租户数据源管理
java
/**
* 多租户数据源管理器
*/
@Component
@Slf4j
public class TenantDataSourceManager {
@Autowired
private DynamicRoutingDataSource dynamicRoutingDataSource;
@Autowired
private DataSourceProperties dataSourceProperties;
// 租户数据源配置缓存
private final Map<String, TenantDataSourceConfig> tenantConfigCache = new ConcurrentHashMap<>();
/**
* 根据租户ID获取数据源
*/
public DataSource getDataSourceForTenant(String tenantId) {
String dataSourceKey = "tenant_" + tenantId;
// 检查是否已存在数据源
if (dynamicRoutingDataSource.getTargetDataSources().containsKey(dataSourceKey)) {
return (DataSource) dynamicRoutingDataSource.getTargetDataSources().get(dataSourceKey);
}
// 动态创建数据源
synchronized (this) {
if (!dynamicRoutingDataSource.getTargetDataSources().containsKey(dataSourceKey)) {
DataSource dataSource = createTenantDataSource(tenantId);
dynamicRoutingDataSource.addDataSource(dataSourceKey, dataSource);
log.info("为租户 {} 创建数据源: {}", tenantId, dataSourceKey);
}
}
return (DataSource) dynamicRoutingDataSource.getTargetDataSources().get(dataSourceKey);
}
/**
* 动态创建租户数据源
*/
private DataSource createTenantDataSource(String tenantId) {
TenantDataSourceConfig config = getTenantConfig(tenantId);
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(buildJdbcUrl(config));
dataSource.setUsername(config.getUsername());
dataSource.setPassword(config.getPassword());
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setMaximumPoolSize(10);
dataSource.setMinimumIdle(2);
dataSource.setConnectionTimeout(30000);
dataSource.setIdleTimeout(600000);
dataSource.setMaxLifetime(1800000);
dataSource.setPoolName("TenantPool_" + tenantId);
return dataSource;
}
/**
* 获取租户数据源配置(可从配置中心或数据库获取)
*/
private TenantDataSourceConfig getTenantConfig(String tenantId) {
return tenantConfigCache.computeIfAbsent(tenantId, key -> {
// 这里可以从配置中心、数据库或缓存中获取租户配置
// 简化实现,实际项目中需要完善
TenantDataSourceConfig config = new TenantDataSourceConfig();
config.setHost("tenant-" + tenantId + ".db.example.com");
config.setPort(3306);
config.setDatabase("tenant_" + tenantId);
config.setUsername("tenant_" + tenantId);
config.setPassword("password_" + tenantId);
return config;
});
}
private String buildJdbcUrl(TenantDataSourceConfig config) {
return String.format("jdbc:mysql://%s:%d/%s?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true",
config.getHost(), config.getPort(), config.getDatabase());
}
@Data
public static class TenantDataSourceConfig {
private String host;
private int port;
private String database;
private String username;
private String password;
}
}
数据源健康监控
java
/**
* 数据源健康监控器
*/
@Component
@Slf4j
public class DataSourceHealthMonitor {
@Autowired
private DynamicRoutingDataSource dynamicRoutingDataSource;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
// 健康状态缓存
private final Map<String, Boolean> healthStatus = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
// 启动健康检查任务
scheduler.scheduleAtFixedRate(this::checkAllDataSources, 0, 30, TimeUnit.SECONDS);
}
/**
* 检查所有数据源的健康状态
*/
public void checkAllDataSources() {
Map<Object, Object> dataSources = dynamicRoutingDataSource.getTargetDataSources();
for (Map.Entry<Object, Object> entry : dataSources.entrySet()) {
String dataSourceKey = (String) entry.getKey();
DataSource dataSource = (DataSource) entry.getValue();
boolean isHealthy = checkDataSourceHealth(dataSource);
healthStatus.put(dataSourceKey, isHealthy);
if (!isHealthy) {
log.warn("数据源 {} 健康检查失败", dataSourceKey);
// 可以发送告警通知
}
}
}
/**
* 检查单个数据源健康状态
*/
private boolean checkDataSourceHealth(DataSource dataSource) {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement()) {
ResultSet rs = stmt.executeQuery("SELECT 1");
return rs.next() && rs.getInt(1) == 1;
} catch (SQLException e) {
log.error("数据源健康检查异常", e);
return false;
}
}
/**
* 获取数据源健康状态
*/
public boolean isDataSourceHealthy(String dataSourceKey) {
return healthStatus.getOrDefault(dataSourceKey, true);
}
/**
* 获取健康的数据源列表
*/
public List<String> getHealthyDataSources() {
return healthStatus.entrySet().stream()
.filter(Map.Entry::getValue)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
@PreDestroy
public void destroy() {
scheduler.shutdown();
}
}
五、动态数据源的应用场景
让我们通过架构图来理解动态数据源的典型应用场景:
六、优缺点
优点
- 灵活性高:支持运行时动态添加、移除数据源
- 解耦性好:业务代码与具体数据源解耦
- 扩展性强:易于实现读写分离、多租户等复杂场景
- 维护方便:数据源配置集中管理,便于维护
缺点
- 复杂度增加:系统架构变得更加复杂
- 事务管理复杂:跨数据源事务需要特殊处理
- 连接池开销:每个数据源都需要独立的连接池
- 调试困难:数据源切换增加了调试复杂度
七、生产环境注意事项
事务管理策略
java
/**
* 多数据源事务管理器
*/
@Component
@Slf4j
public class MultiDataSourceTransactionManager {
/**
* 在多个数据源上执行事务性操作
*/
@Transactional(rollbackFor = Exception.class)
public void executeInTransaction(Runnable task, String... dataSources) {
if (dataSources.length == 1) {
// 单数据源事务
DataSourceContextHolder.setDataSource(dataSources[0]);
try {
task.run();
} finally {
DataSourceContextHolder.clear();
}
} else {
// 多数据源伪事务(最终一致性)
executeWithCompensation(task, dataSources);
}
}
/**
* 使用补偿机制实现多数据源"事务"
*/
private void executeWithCompensation(Runnable task, String[] dataSources) {
List<Runnable> compensationTasks = new ArrayList<>();
try {
// 按顺序执行各个数据源的操作
for (String dataSource : dataSources) {
DataSourceContextHolder.setDataSource(dataSource);
try {
// 执行实际业务操作
task.run();
// 记录补偿操作
compensationTasks.add(0, createCompensationTask(dataSource));
} finally {
DataSourceContextHolder.clear();
}
}
} catch (Exception e) {
// 执行补偿操作
log.error("多数据源操作失败,执行补偿操作", e);
executeCompensation(compensationTasks);
throw e;
}
}
private void executeCompensation(List<Runnable> compensationTasks) {
for (Runnable compensation : compensationTasks) {
try {
compensation.run();
} catch (Exception ex) {
log.error("补偿操作执行失败", ex);
// 记录补偿失败,需要人工介入
}
}
}
}
性能优化建议
- 连接池优化:根据业务特点调整各数据源连接池参数
- 数据源预热:应用启动时预热常用数据源
- 缓存策略:缓存数据源配置和路由信息
- 监控告警:建立完善的数据源监控体系
总结
动态数据源是一个强大的技术方案,能够很好地解决多数据源管理的复杂性。
通过本文的详细解析,我们可以看到:
- 核心原理 :基于
AbstractRoutingDataSource和ThreadLocal的上下文管理 - 实现方式:注解+AOP的声明式数据源切换
- 高级特性:读写分离、多租户、健康监控等生产级功能
- 适用场景:多租户、读写分离、分库分表等复杂数据架构
在实际项目中,建议根据具体业务需求选择合适的实现方案,不要过度设计。
同时,要建立完善的监控和运维体系,确保动态数据源的稳定运行。
最后说一句(求关注,别白嫖我)
如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
更多项目实战在我的技术网站:http://www.susan.net.cn/project