# 延迟加载从库解决方案 ## 需求概述 ### 业务场景 ``` 主库(固定)+ 从库(库名可变) ├─ 主库:存储配置表(包含从库库名) ├─ 从库:启动时不加载,运行时根据配置表动态加载 └─ 操作:更新配置表 → 动态加载从库 → 切换写入数据 ``` ### 核心需求 1. **延迟加载**:应用启动时只加载主库,从库按需加载 2. **动态切换**:运行时可以随时切换从库库名 3. **配置管理**:从库配置存储在主库,便于管理 4. **无侵入性**:使用现有 `@DataSource` 注解,无需修改业务代码 --- ## 架构设计 ### 核心组件 | 组件 | 职责 | 文件位置 | |------|------|---------| | **sys_datasource_config** | 存储从库配置信息 | 主库表 | | **SysDatasourceConfig** | 配置实体类 | `datai-system/domain/` | | **SysDatasourceConfigMapper** | 配置数据访问层 | `datai-system/mapper/` | | **DataSourceManager** | 数据源管理器(扩展) | `datai-framework/manager/` | | **DynamicDataSourceService** | 动态数据源业务服务 | `datai-system/service/` | | **DynamicDataSource** | 动态数据源路由 | `datai-framework/datasource/` | | **DynamicDataSourceContextHolder** | 数据源上下文持有者 | `datai-framework/datasource/` | ### 工作流程 ``` 1. 应用启动 └─ 只加载 MASTER 数据源 └─ SLAVE 数据源不加载 2. 运行时加载从库 └─ 读取 sys_datasource_config 表 └─ 创建 SLAVE 数据源 └─ 注册到 DynamicDataSource 3. 切换从库库名 └─ 更新 sys_datasource_config 表 └─ 移除旧 SLAVE 数据源 └─ 创建新 SLAVE 数据源 └─ 注册到 DynamicDataSource 4. 使用从库 └─ @DataSource(DataSourceType.SLAVE) └─ AOP 切面切换数据源 └─ DynamicDataSourceContextHolder 设置上下文 └─ DynamicDataSource 路由到 SLAVE ``` --- ## 实现步骤 ### 步骤 1:创建数据源配置表 **文件位置:** `datai-system/src/main/resources/sql/sys_datasource_config.sql` ```sql -- 数据源配置表 -- 用于存储从库的配置信息,支持动态切换从库库名 CREATE TABLE `sys_datasource_config` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `ds_name` VARCHAR(50) NOT NULL COMMENT '数据源名称(如:SLAVE)', `db_name` VARCHAR(100) NOT NULL COMMENT '数据库名称', `db_host` VARCHAR(200) NOT NULL COMMENT '数据库主机', `db_port` INT(11) DEFAULT '3306' COMMENT '数据库端口', `username` VARCHAR(100) NOT NULL COMMENT '用户名', `password` VARCHAR(200) NOT NULL COMMENT '密码', `db_type` VARCHAR(20) DEFAULT 'mysql' COMMENT '数据库类型(mysql/postgresql/oracle等)', `status` CHAR(1) DEFAULT '0' COMMENT '状态(0正常 1停用)', `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', `create_by` VARCHAR(64) DEFAULT '' COMMENT '创建者', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_by` VARCHAR(64) DEFAULT '' COMMENT '更新者', `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_ds_name` (`ds_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据源配置表'; -- 初始化从库配置 INSERT INTO `sys_datasource_config` (`ds_name`, `db_name`, `db_host`, `db_port`, `username`, `password`, `db_type`, `status`, `remark`) VALUES ('SLAVE', 'datai_slave', '117.72.193.251', 3306, 'root', '2026.DataI', 'mysql', '0', '从库配置'); ``` --- ### 步骤 2:修改配置文件 **文件位置:** `datai-admin/src/main/resources/application-druid.yml` ```yaml spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver dynamic: primary: MASTER xa: false # 暂不启用分布式事务 datasource: MASTER: # 只配置主库 url: jdbc:mysql://117.72.193.251/datai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true username: root password: 2026.DataI # SLAVE: # 移除从库配置,运行时动态加载 druid: initialSize: 5 minIdle: 10 maxActive: 20 maxWait: 60000 connectTimeout: 30000 socketTimeout: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 maxEvictableIdleTimeMillis: 900000 validationQuery: SELECT 1 testWhileIdle: true testOnBorrow: false testOnReturn: false filters: stat,wall,slf4j,log4j2 connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 ``` --- ### 步骤 3:创建实体类 **文件位置:** `datai-system/src/main/java/com/datai/system/domain/SysDatasourceConfig.java` ```java package com.datai.system.domain; import com.datai.common.core.domain.BaseEntity; public class SysDatasourceConfig extends BaseEntity { private static final long serialVersionUID = 1L; private Long id; private String dsName; private String dbName; private String dbHost; private Integer dbPort; private String username; private String password; private String dbType; private String status; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getDsName() { return dsName; } public void setDsName(String dsName) { this.dsName = dsName; } public String getDbName() { return dbName; } public void setDbName(String dbName) { this.dbName = dbName; } public String getDbHost() { return dbHost; } public void setDbHost(String dbHost) { this.dbHost = dbHost; } public Integer getDbPort() { return dbPort; } public void setDbPort(Integer dbPort) { this.dbPort = dbPort; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getDbType() { return dbType; } public void setDbType(String dbType) { this.dbType = dbType; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } } ``` --- ### 步骤 4:创建 Mapper **文件位置:** `datai-system/src/main/java/com/datai/system/mapper/SysDatasourceConfigMapper.java` ```java package com.datai.system.mapper; import com.datai.system.domain.SysDatasourceConfig; import org.apache.ibatis.annotations.Mapper; @Mapper public interface SysDatasourceConfigMapper { SysDatasourceConfig selectByDsName(String dsName); int updateByDsName(SysDatasourceConfig config); int insert(SysDatasourceConfig config); } ``` **文件位置:** `datai-system/src/main/resources/mapper/system/SysDatasourceConfigMapper.xml` ```xml UPDATE sys_datasource_config db_name = #{dbName}, db_host = #{dbHost}, db_port = #{dbPort}, username = #{username}, password = #{password}, db_type = #{dbType}, status = #{status}, remark = #{remark}, update_by = #{updateBy}, update_time = sysdate() WHERE ds_name = #{dsName} INSERT INTO sys_datasource_config ( ds_name, db_name, db_host, db_port, username, password, db_type, status, remark, create_by, create_time ) VALUES ( #{dsName}, #{dbName}, #{dbHost}, #{dbPort}, #{username}, #{password}, #{dbType}, #{status}, #{remark}, #{createBy}, sysdate() ) ``` --- ### 步骤 5:扩展 DataSourceManager **文件位置:** `datai-framework/src/main/java/com/datai/framework/manager/DataSourceManager.java` 在现有代码中添加以下方法: ```java public class DataSourceManager implements InitializingBean { // ... 现有代码 ... public synchronized void addDynamicDataSource(String name, String url, String username, String password) { if (targetDataSources.containsKey(name)) { logger.warn("数据源 {} 已存在,先移除旧数据源", name); removeDataSource(name); } Properties prop = new Properties(); prop.setProperty("druid.url", url); prop.setProperty("druid.username", username); prop.setProperty("druid.password", password); DruidProperties druidProperties = SpringUtils.getBean(DruidProperties.class); prop.setProperty("druid.initialSize", String.valueOf(druidProperties.getInitialSize())); prop.setProperty("druid.minIdle", String.valueOf(druidProperties.getMinIdle())); prop.setProperty("druid.maxActive", String.valueOf(druidProperties.getMaxActive())); prop.setProperty("druid.maxWait", String.valueOf(druidProperties.getMaxWait())); prop.setProperty("druid.validationQuery", druidProperties.getValidationQuery()); prop.setProperty("druid.testWhileIdle", String.valueOf(druidProperties.isTestWhileIdle())); prop.setProperty("druid.testOnBorrow", String.valueOf(druidProperties.isTestOnBorrow())); prop.setProperty("druid.testOnReturn", String.valueOf(druidProperties.isTestOnReturn())); prop.setProperty("druid.filters", druidProperties.getFilters()); prop.setProperty("druid.connectionProperties", druidProperties.getConnectionProperties()); prop.setProperty("druid.timeBetweenEvictionRunsMillis", String.valueOf(druidProperties.getTimeBetweenEvictionRunsMillis())); prop.setProperty("druid.minEvictableIdleTimeMillis", String.valueOf(druidProperties.getMinEvictableIdleTimeMillis())); prop.setProperty("druid.maxEvictableIdleTimeMillis", String.valueOf(druidProperties.getMaxEvictableIdleTimeMillis())); DataSource dataSource = createDataSource(name, prop); putDataSource(name, dataSource); logger.info("动态数据源 {} 添加成功", name); } public synchronized void removeDataSource(String name) { DataSource dataSource = targetDataSources.remove(name); if (dataSource != null) { dataSourceKeyIndex.remove(dataSource); dsDatabaseId.remove(name); if (dataSource instanceof DruidDataSource) { try { ((DruidDataSource) dataSource).close(); } catch (Exception e) { logger.error("关闭数据源 {} 失败", name, e); } } logger.info("数据源 {} 已移除", name); } } public synchronized void updateDataSource(String name, String url, String username, String password) { removeDataSource(name); addDynamicDataSource(name, url, username, password); } } ``` --- ### 步骤 6:创建动态数据源服务 **文件位置:** `datai-system/src/main/java/com/datai/system/service/DynamicDataSourceService.java` ```java package com.datai.system.service; import com.datai.common.annotation.DataSource; import com.datai.common.enums.DataSourceType; import com.datai.framework.manager.DataSourceManager; import com.datai.system.domain.SysDatasourceConfig; import com.datai.system.mapper.SysDatasourceConfigMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class DynamicDataSourceService { @Autowired private DataSourceManager dataSourceManager; @Autowired private SysDatasourceConfigMapper configMapper; @DataSource(DataSourceType.MASTER) public void loadSlaveDataSource() { SysDatasourceConfig config = configMapper.selectByDsName("SLAVE"); if (config == null) { throw new RuntimeException("从库配置不存在"); } if (!"0".equals(config.getStatus())) { throw new RuntimeException("从库配置已停用"); } String url = buildJdbcUrl(config); dataSourceManager.addDynamicDataSource( config.getDsName(), url, config.getUsername(), config.getPassword() ); } @DataSource(DataSourceType.MASTER) public void updateSlaveConfig(String newDbName) { SysDatasourceConfig config = configMapper.selectByDsName("SLAVE"); if (config == null) { throw new RuntimeException("从库配置不存在"); } config.setDbName(newDbName); configMapper.updateByDsName(config); String url = buildJdbcUrl(config); dataSourceManager.updateDataSource( config.getDsName(), url, config.getUsername(), config.getPassword() ); } @DataSource(DataSourceType.MASTER) public SysDatasourceConfig getSlaveConfig() { return configMapper.selectByDsName("SLAVE"); } private String buildJdbcUrl(SysDatasourceConfig config) { String urlPattern; switch (config.getDbType().toLowerCase()) { case "mysql": urlPattern = "jdbc:mysql://%s:%d/%s?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%%2B8"; break; case "postgresql": urlPattern = "jdbc:postgresql://%s:%d/%s"; break; case "oracle": urlPattern = "jdbc:oracle:thin:@%s:%d:%s"; break; default: throw new RuntimeException("不支持的数据库类型:" + config.getDbType()); } return String.format(urlPattern, config.getDbHost(), config.getDbPort(), config.getDbName()); } } ``` --- ### 步骤 7:创建控制器(可选) **文件位置:** `datai-admin/src/main/java/com/datai/web/controller/system/DatasourceController.java` ```java package com.datai.web.controller.system; import com.datai.common.annotation.DataSource; import com.datai.common.core.controller.BaseController; import com.datai.common.core.domain.AjaxResult; import com.datai.common.enums.DataSourceType; import com.datai.system.domain.SysDatasourceConfig; import com.datai.system.service.DynamicDataSourceService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/system/datasource") public class DatasourceController extends BaseController { @Autowired private DynamicDataSourceService dynamicDataSourceService; @PostMapping("/loadSlave") public AjaxResult loadSlave() { dynamicDataSourceService.loadSlaveDataSource(); return AjaxResult.success("从库加载成功"); } @PostMapping("/switchSlave/{dbName}") public AjaxResult switchSlave(@PathVariable String dbName) { dynamicDataSourceService.updateSlaveConfig(dbName); return AjaxResult.success("从库切换成功"); } @GetMapping("/getSlaveConfig") @DataSource(DataSourceType.MASTER) public AjaxResult getSlaveConfig() { SysDatasourceConfig config = dynamicDataSourceService.getSlaveConfig(); return AjaxResult.success(config); } } ``` --- ## 使用示例 ### 示例 1:启动时加载从库 ```java @Component public class DataSourceInitializer implements ApplicationRunner { @Autowired private DynamicDataSourceService dynamicDataSourceService; @Override public void run(ApplicationArguments args) throws Exception { try { dynamicDataSourceService.loadSlaveDataSource(); logger.info("从库数据源加载成功"); } catch (Exception e) { logger.warn("从库数据源加载失败,将延迟加载", e); } } } ``` ### 示例 2:切换从库库名 ```java @Service public class OrderService { @Autowired private DynamicDataSourceService dynamicDataSourceService; @Autowired private OrderMapper orderMapper; @DataSource(DataSourceType.MASTER) public void processOrderToSlave(Long orderId, String slaveDbName) { Order order = orderMapper.selectById(orderId); dynamicDataSourceService.updateSlaveConfig(slaveDbName); writeToSlave(order); } @DataSource(DataSourceType.SLAVE) private void writeToSlave(Order order) { orderMapper.insert(order); } } ``` ### 示例 3:使用从库写入数据 ```java @Service public class DataService { @Autowired private DataMapper dataMapper; @DataSource(DataSourceType.SLAVE) public void writeToSlave(Data data) { dataMapper.insert(data); } @DataSource(DataSourceType.MASTER) public void writeToMaster(Data data) { dataMapper.insert(data); } } ``` --- ## 注意事项 ### ⚠️ 事务问题 **错误做法:** ```java @Transactional // ❌ 不要在事务中切换数据源 public void switchAndWrite() { dynamicDataSourceService.updateSlaveConfig("new_db"); writeToSlave(data); } ``` **正确做法:** ```java public void switchAndWrite() { dynamicDataSourceService.updateSlaveConfig("new_db"); writeToSlave(data); // 另一个方法,独立事务 } ``` ### ⚠️ 连接池管理 - 切换数据源时会关闭旧连接池 - 可能有短暂的服务不可用(毫秒级) - 建议在低峰期切换 ### ⚠️ 并发问题 已通过 `synchronized` 关键字保证线程安全: ```java public synchronized void updateDataSource(String name, String url, String username, String password) { removeDataSource(name); addDynamicDataSource(name, url, username, password); } ``` ### ⚠️ 数据库类型支持 当前支持: - MySQL - PostgreSQL - Oracle 如需支持其他数据库,在 `buildJdbcUrl()` 方法中添加对应的 URL 模板。 --- ## 方案优势 | 特性 | 说明 | |------|------| | ✅ 延迟加载 | 启动时只加载主库,从库按需加载 | | ✅ 动态切换 | 运行时可以随时切换从库库名 | | ✅ 配置管理 | 从库配置存储在主库,便于管理 | | ✅ 线程安全 | 使用 synchronized 和 ThreadLocal 保证线程隔离 | | ✅ 无侵入 | 使用现有 `@DataSource` 注解,无需修改业务代码 | | ✅ 易扩展 | 支持多种数据库类型 | --- ## 测试验证 ### 测试步骤 1. **执行 SQL 脚本**:创建 `sys_datasource_config` 表 2. **修改配置文件**:移除 SLAVE 配置 3. **启动应用**:验证只加载 MASTER 数据源 4. **调用加载接口**:`POST /system/datasource/loadSlave` 5. **验证从库可用**:使用 `@DataSource(DataSourceType.SLAVE)` 写入数据 6. **切换从库库名**:`POST /system/datasource/switchSlave/new_db_name` 7. **验证新从库**:确认数据写入到新数据库 ### 验证命令 ```bash # 1. 加载从库 curl -X POST http://localhost:8080/system/datasource/loadSlave # 2. 查看从库配置 curl http://localhost:8080/system/datasource/getSlaveConfig # 3. 切换从库 curl -X POST http://localhost:8080/system/datasource/switchSlave/datai_slave_new ``` --- ## 相关文件 | 文件 | 路径 | |------|------| | 配置文件 | [application-druid.yml](file:///d:/idea_demo/datai/datai-admin/src/main/resources/application-druid.yml) | | 数据源管理器 | [DataSourceManager.java](file:///d:/idea_demo/datai/datai-framework/src/main/java/com/datai/framework/manager/DataSourceManager.java) | | 动态数据源 | [DynamicDataSource.java](file:///d:/idea_demo/datai/datai-framework/src/main/java/com/datai/framework/datasource/DynamicDataSource.java) | | 上下文持有者 | [DynamicDataSourceContextHolder.java](file:///d:/idea_demo/datai/datai-framework/src/main/java/com/datai/framework/datasource/DynamicDataSourceContextHolder.java) | | 数据源注解 | [DataSource.java](file:///d:/idea_demo/datai/datai-common/src/main/java/com/datai/common/annotation/DataSource.java) | | 数据源类型 | [DataSourceType.java](file:///d:/idea_demo/datai/datai-common/src/main/java/com/datai/common/enums/DataSourceType.java) | | AOP 切面 | [DataSourceAspect.java](file:///d:/idea_demo/datai/datai-framework/src/main/java/com/datai/framework/aspectj/DataSourceAspect.java) | --- ## 总结 本方案实现了延迟加载从库的功能,通过在主库存储从库配置,运行时动态创建和切换数据源,满足了业务需求。方案具有以下特点: 1. **灵活性高**:支持运行时动态切换从库库名 2. **侵入性低**:使用现有注解机制,无需修改业务代码 3. **可维护性强**:配置集中管理,便于运维 4. **线程安全**:通过同步机制保证并发安全 该方案适用于需要动态切换数据源的业务场景,如多租户数据隔离、数据归档、报表库切换等。