# spring-boot-starter-eds **Repository Path**: idler41/spring-boot-starter-eds ## Basic Information - **Project Name**: spring-boot-starter-eds - **Description**: 运行时动态切换、创建、销毁的数据源 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-09-09 - **Last Updated**: 2025-10-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # eds eds 是一个基于 Spring Boot AbstractRoutingDataSource 与 Hikari 实现的增强型动态数据源管理组件,支持运行时按需加载与初始化数据源,有效解决多版本数据库驱动兼容性及动态管理问题。 ## 功能列表 - 支持指定目录动态加载驱动,且兼容多版本驱动共存 - 借助`EdsPropertiesLoader`接口和`@Eds`注解,应用在运行期间能够根据实际需求动态加载并初始化数据源,实现按需配置与加载 - 数据源可手动新增、替换、销毁,长时间未访问数据源也会自动销毁,不占用资源 ## 核心原理 ### 多版本驱动兼容 DriverManager 有严格的类加载器安全检查机制,通过 `isDriverAllowed` 方法检查驱动和调用者是否在同一个类加载器层次结构中。当使用自定义的 `URLClassLoader` 加载驱动时,DriverManager 会拒绝返回这些驱动,因为调用者(如 HikariCP 的 DriverDataSource)的类加载器与驱动的类加载器不匹配。 具体来说: 1. DriverManager 使用 `driver.getClass() == aClass` 的方式比较驱动类,不同类加载器加载的同一个类不相等 2. 即使驱动成功注册到 DriverManager,当 HikariCP 尝试通过 DriverManager 获取驱动时,由于类加载器不匹配,isDriverAllowed 返回 false 3. 这导致 DriverManager.getDrivers() 无法返回自定义ClassLoader加载的驱动 为了解决这个问题,eds 采用以下机制实现多版本驱动共存: 1. 扩展双亲委派模型,自定义`URLClassLoader`并重写`loadClass`方法,优先从当前ClassLoader加载驱动类,避免父类加载器有驱动类干扰 2. 每次初始化可以加载1个驱动jar包,也可以加载指定目录下所有jar包,即实现了驱动版本隔离,也适配了加载额外依赖的需求 3. 利用线程上下文类加载器,确保HikariCP能够直接通过类名实例化对应版本的驱动,绕过DriverManager的类加载器限制 4. 切换前保存了旧类加载器,加载完后恢复旧类加载器,且自定义`URLClassLoader`的父类构造器就是旧类加载器,保证驱动类与其他额外加载的依赖可互相访问(比如hive除了驱动,还有其他依赖需要动态引入) ### 并发处理 1. **数据源获取的无锁设计** - 使用 `ConcurrentHashMap` 存储数据源,利用其get操作无锁特性获取数据源 - 即使存在并发问题导致获取到旧数据源,也被认为是可以接受的,因为这等同于加锁前获取到的数据源 2. **数据源创建的并发安全处理** - 原子性创建操作:使用 ConcurrentHashMap 的 computeIfAbsent 方法保证相同key的数据源只会被创建一次,避免重复创建 - 死锁预防机制:computeIfAbsent的堵塞锁不可重入,因此通过 LAST_DS_KEY_WHEN_REGISTRY ThreadLocal 变量检测在创建数据源过程中是否再次访问相同数据源,防止死锁 3. **数据源销毁的并发安全处理** - 标记阶段:`ScanExpiredEdsThread` 线程扫描过期数据源,将其标记为 removed 并从 `targetDataSources` 中移除 - 异步销毁阶段:`DestroyEdsThread` 线程从 `evictQueue` 中取出待销毁的数据源,只有在确认数据源空闲时才真正关闭 - 线程安全队列:使用 `LinkedBlockingQueue` 作为 `evictQueue`,保证生产者(扫描线程)和消费者(销毁线程)的安全通信 - 安全关闭检查:通过 `isCanClose()` 方法检查数据源是否正在使用,避免关闭正在处理业务的数据源 这种无锁访问设计通过牺牲一定的数据一致性来换取更高的并发性能,对于数据源管理这种场景来说是可以接受的,因为短时间的数据延迟不会影响数据源整体功能。 ### 需要注意点 1. druid 初始化较繁琐(特别是与数据源功能关联不大的 filter),所以暂不支持 druid 2. 访问动态数据源时如使用spring的事务注解,要注意保证数据源不被切换,否则可能导致事务失效 ## 与传统JDBC连接池的区别 传统JDBC连接管理中,应用程序直接管理Connection的生命周期,调用`connection.close()`方法可以明确归还连接到连接池或关闭物理连接,整个生命周期完全可控。 而DataSource模式下,应用程序切换具体数据源后,无法主动感知数据源是否被使用,连接的使用状态和归还时机,存在以下挑战: 1. **连接生命周期不可控**:数据源借出连接后无法主动回收,依赖应用程序正确调用close() 2. **资源泄露风险**:应用程序忘记关闭连接会导致连接泄露,最终耗尽连接池资源 3. **动态管理困难**:无法准确判断数据源是否正在使用,难以安全地销毁或替换数据源 eds通过以下方式解决这些问题: 1. **访问时间追踪**:记录每个数据源的最后访问时间,实现基于时间的自动销毁机制 2. **安全销毁机制**:通过异步扫描和延迟销毁策略,确保只在数据源空闲时才进行销毁 3. **状态监控**:提供数据源状态检查功能,避免在使用过程中销毁数据源 4. **生命周期管理**:支持手动新增、替换、销毁数据源,提供灵活的动态管理能力 ## 快速使用 1. 添加依赖 ```xml com.one.blocks spring-boot-starter-eds 1.0.0-RELEASE ``` 2. 配置数据源 ```yaml spring: datasource: # @Eds注解未配置值时切换到默认数据源 edsDefaultKey: master # 数据源过期时间(单位:毫秒) edsExpireTime: 7200000 # 数据源扫描时间(单位:毫秒) edsScanTime: 1800000 eds: # 数据源key,参数名参照HikariCP文档(注意,这里暂不支持springboot的kebab-case风格,要求严格与HikariCP文档参数一致,使用驼峰命名) master: jdbcUrl: jdbc:mysql://dev.mesh-db.com:33006/data_mesh?useSSL=false&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true username: admin password: 123456 driverClassName: com.mysql.cj.jdbc.Driver connectionTestQuery: SELECT 1 FROM DUAL minimumIdle: 1 maximumPoolSize: 20 # registerMbeans: true connectionTimeout: 5000 idleTimeout: 600000 maxLifetime: 1200000 ``` 3. 实现动态数据源配置加载接口 ```java import org.springframework.stereotype.Component; @Component public class DataSourceConfig implements EdsPropertiesLoader { /** * 根据数据源配置存储位置修改@Eds注解值,不存储在数据库可不加@Eds注解 * @param dsKey 数据源唯一标识 * @return eds数据源配置 */ @Eds @Override public EdsRemoteProperties load(String dsKey) { // 从远程加载数据源配置 ConnectionPool data = loadFromRemote(dsKey); // 转换为hikariProperties Properties hikariProperties = new Properties(); hikariProperties.put("jdbcUrl", data.getJdbcUrl()); hikariProperties.put("driverClassName", data.getDriverName()); hikariProperties.put("username", data.getUsername()); hikariProperties.put("password", data.getPassword()); if (StringUtils.isNotBlank(data.getExtraJson())) { hikariProperties.putAll(JSON.parseObject(data.getExtraJson(), HashMap.class)); } return EdsRemoteProperties.builder() // jar包路径 .jarPath(data.getDriverUrl()) .hikariProperties(hikariProperties) .build(); } } ``` 4. 数据源动态切换 @Eds注解值可使用SringEL表达式,同时避免java文件因编译后丢失参数名导致访问失败问题,统一用#p0、#p1、#p2...表示第几个参数 ```java import org.springframework.stereotype.Component; @Component public class EdsHelper { @Eds("#p0") public List scanTable(String dsKey, String catalog) { // 具体业务 } @Eds("#p1") public List scanTable(String catalog, String dsKey) { // 具体业务 } @Eds("#p0.tableContext.jobMeta.writerDsKey") @Transactional(rollbackFor = Exception.class) public void saveInTransaction(TaskContext taskContext, List plugins, List> insertList, List> updateList) { // 具体业务 } } ```