胖胖的枫叶
主页
博客
知识图谱
产品设计
数据分析
企业架构
项目管理
效率工具
全栈开发
后端
前端
测试
运维
数据
面试
  • openJdk-docs
  • spring-projects-docs
  • mysql-docs
  • redis-commands
  • redis-projects
  • apache-rocketmq
  • docker-docs
  • mybatis-docs
  • netty-docs
  • journaldev
  • geeksforgeeks
  • 浮生若梦
  • 后端进阶
  • 并发编程网
  • 英语肌肉记忆锻炼软件
  • 墨菲安全
  • Redisson-docs
  • jmh-Visual
  • 美团技术
  • MavenSearch
主页
博客
知识图谱
产品设计
数据分析
企业架构
项目管理
效率工具
全栈开发
后端
前端
测试
运维
数据
面试
  • openJdk-docs
  • spring-projects-docs
  • mysql-docs
  • redis-commands
  • redis-projects
  • apache-rocketmq
  • docker-docs
  • mybatis-docs
  • netty-docs
  • journaldev
  • geeksforgeeks
  • 浮生若梦
  • 后端进阶
  • 并发编程网
  • 英语肌肉记忆锻炼软件
  • 墨菲安全
  • Redisson-docs
  • jmh-Visual
  • 美团技术
  • MavenSearch
  • 博客

    • 博客迁移说明
    • 2024年
    • 2023年
    • 2022年
    • 2021年
    • 2020年
    • 2019年
    • 2018年

在对老系统进行saas改造的时候,在项目初期使用了动态数据源的方式处理业务。

  • 本文内容仅针对数据存储方案。

动态数据源

**SaaS是Software-as-a-service(软件即服务)**它是一种通过Internet提供软件的模式,厂商将应用软件统一部署在自己的服务器

①独立性:每个租户的系统相互独立。

②平台性:所有租户归平台统一管理。

③隔离性:每个租户的数据相互隔离。

需求

  • 老项目系统进行saas改造工作。

方案

独立数据库、共享数据库、共享架构、OLTP、OLAP

  • 根据客户需求可以选择隔离数据库。
  • 也可以选择公用数据库。
  • 采用OLTP方案进行数据同步。
  • 采用OLAP方案进行冷数据同步。

技术方案

  • spring boot 2.4.5
  • mybatis plus
AbstractRoutingDataSource

Spring 官网提供的切换数据源的抽象方法,基于查找键将getConnection()调用路由到各种目标 DataSource 之一的抽象DataSource实现。后者通常(但不一定)通过一些线程绑定的事务上下文来确定。


  /**
   * determineTargetDataSource() 方法进行切换。
   **/
  @Override
	public Connection getConnection() throws SQLException {
		return determineTargetDataSource().getConnection();
	}

	@Override
	public Connection getConnection(String username, String password) throws SQLException {
		return determineTargetDataSource().getConnection(username, password);
	} 

  /**
   * 确定当前查找键。这通常会被实现来检查线程绑定的事务上下文。允许任意键。返回的键需要匹配存储的查找键类型,由resolveSpecifiedLookupKey方法解析。
   **/
  @Nullable
	protected abstract Object determineCurrentLookupKey();
  /**
   * 检索当前目标数据源。确定current lookup key ,在targetDataSources映射中执行查找,必要时回退到指定  的default target DataSource 
   **/
  protected DataSource determineTargetDataSource() {
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
		Object lookupKey = determineCurrentLookupKey();
		DataSource dataSource = this.resolvedDataSources.get(lookupKey);
		if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
			dataSource = this.resolvedDefaultDataSource;
		}
		if (dataSource == null) {
			throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
		}
		return dataSource;
	}

演示代码

启动效果

[main] [SpringApplication.java : 679] The following profiles are active: dev
[main] [Bootstrap.java : 68] UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used
[main] [ServletContextImpl.java : 371] Initializing Spring embedded WebApplicationContext
[main] [ServletWebServerApplicationContext.java : 289] Root WebApplicationContext: initialization completed in 1022 ms
[main] [HikariDataSource.java : 110] HikariPool-1 - Starting...
[main] [HikariDataSource.java : 123] HikariPool-1 - Start completed.
[main] [DynamicRoutingDataSource.java : 33] setDynamicRoutingDataSource ... 
[main] [DynamicRoutingDataSourceConfig.java : 82] initDynamicDataSource ...
[main] [BaseJdbcLogger.java : 137] ==>  Preparing: SELECT id,tenant_id,tenant_name,datasource_url,datasource_username,datasource_password,datasource_driver,system_account,system_password,system_project,is_enable,create_time,update_time FROM tenant_info
[main] [BaseJdbcLogger.java : 137] ==> Parameters: 
[main] [BaseJdbcLogger.java : 137] <==      Total: 3
[main] [DynamicRoutingDataSourceConfig.java : 98]  init  dataSource OTQyNTkyMjA3ODI5ODYwMzUy
[main] [HikariDataSource.java : 80] HikariPool-2 - Starting...
[main] [HikariDataSource.java : 82] HikariPool-2 - Start completed.
[main] [DynamicRoutingDataSourceConfig.java : 98]  init  dataSource OTQyNTkyMjA3OTA5NTUyMTI4
[main] [HikariDataSource.java : 80] HikariPool-3 - Starting...
[main] [HikariDataSource.java : 82] HikariPool-3 - Start completed.
[main] [DynamicRoutingDataSourceConfig.java : 98]  init  dataSource OTQyNTkyMjA3OTU5ODgzNzc2
[main] [HikariDataSource.java : 80] HikariPool-4 - Starting...
[main] [HikariDataSource.java : 82] HikariPool-4 - Start completed.
[main] [DynamicRoutingDataSource.java : 33] setDynamicRoutingDataSource ... 
[main] [ExecutorConfigurationSupport.java : 181] Initializing ExecutorService 'applicationTaskExecutor'
[main] [Undertow.java : 120] starting server: Undertow - 2.2.7.Final
[main] [Xnio.java : 95] XNIO version 3.8.0.Final
[main] [NioXnio.java : 59] XNIO NIO Implementation Version 3.8.0.Final
[main] [Version.java : 52] JBoss Threads version 3.1.0.Final
[main] [UndertowWebServer.java : 133] Undertow started on port(s) 9033 (http)
[main] [StartupInfoLogger.java : 61] Started AppApplication in 2.921 seconds (JVM running for 3.593)

  • 首先初始化默认的数据源HikariPool-1。
  • 通过查询租户表,再次初始化3个数据源DataSourceHikariPool-2\HikariPool-3\HikariPool-4。

测试用例

  • 数据源信息
    • 获取全部数据源
    • 获取当前数据源key
  • 切换数据源
    • 当前数据源维护的数据库。
    • 当前数据源的key
# 数据源信息 all-db 维护数据源key , db 当前生效的数据源key
➜  Downloads curl http://127.0.0.1:9033
{"code":"200","all-db":["942592207909552128","942592207829860352","942592207959883776"],"db":"master"}%
# 切换数据源 key 当前数据源key ,db 当前数据源连接的数据库。
➜  Downloads curl http://127.0.0.1:9033/942592207909552128
{"code":"200","key":"942592207909552128","db":["docker_dynamic_data_1"]}% 
# 数据源信息 all-db 维护数据源key , db 当前生效的数据源key。这里发现数据已经切换。
➜  Downloads curl http://127.0.0.1:9033                   
{"code":"200","all-db":["942592207909552128","942592207829860352","942592207959883776"],"db":"942592207909552128"}%
# 数据源信息 all-db 维护数据源key , db 当前生效的数据源key。如果数据源不存在会直接切换到默认数据库。
➜  Downloads curl http://127.0.0.1:9033/1
{"code":"200","key":"master","db":["docker_dynamic_data"]}% 

DynamicDataSourceContextHolder

数据源上下文管理工具,用于维护当前线程数据源key。

/**
 * @author z201.coding@gmail.com
 **/
public class DynamicDataSourceContextHolder {

    private static class SingletonHolder {
        private static final DynamicDataSourceContextHolder INSTANCE = new DynamicDataSourceContextHolder();
    }

    private DynamicDataSourceContextHolder() {
    }

    public static final DynamicDataSourceContextHolder getInstance() {
        return SingletonHolder.INSTANCE;
    }


    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
        /**
         * 将 master 数据源的 key作为默认数据源的 key
         */
        @Override
        protected String initialValue() {
            return DynamicDataSourceConstant.MASTER;
        }
    };
    /**
     * 数据源的 key集合,用于切换时判断数据源是否存在
     */
    private static Set<Object> dataSourceKeys = Collections.synchronizedSet(new HashSet<>());

    /**
     * 切换数据源
     *
     * @param key 数据源
     */
    public void setDataSourceKey(String key) {
        if (containDataSourceKey(key)) {
            if (!ObjectUtils.isEmpty(key)) {
                contextHolder.set(key);
            }
        }
    }

    /**
     * 获取数据源
     *
     * @return
     */
    public String getDataSourceKey() {
        return contextHolder.get();
    }

    /**
     * 重置数据源
     */
    public void clearDataSourceKey() {
        contextHolder.remove();
    }

    /**
     * 判断是否包含数据源
     *
     * @param key 数据源
     * @return
     */
    public boolean containDataSourceKey(String key) {
        return dataSourceKeys.contains(key);
    }

    /**
     * 添加数据源Keys
     *
     * @param keys
     * @return
     */
    public boolean addDataSourceKeys(Collection<? extends Object> keys) {
        return dataSourceKeys.addAll(keys);
    }

    /**
     * 获取全部数据源列表
     * @return
     */
    public Set<Object> all(){
        return dataSourceKeys;
    }
}

DynamicRoutingDataSource

AbstractRoutingDataSource实现类,用于初始化数据源相关信息。

/**
 * @author z201.coding@gmail.com
 **/
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    private static final Logger logger = LoggerFactory.getLogger(DynamicRoutingDataSource.class);

    @Override
    public DataSource determineTargetDataSource() {
        return super.determineTargetDataSource();
    }

    /**
     /**
     * 必须执行此操作,才会重新初始化AbstractRoutingDataSource 中的 resolvedDataSources,也只有这样,动态切换才会起效
     * @param defaultTargetDataSource 默认的数据源
     * @param targetDataSources       多数据源每个key对应一个数据源
     */
    public void setDynamicRoutingDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
        DynamicDataSourceContextHolder.getInstance().addDataSourceKeys(targetDataSources.keySet());
        logger.info("setDynamicRoutingDataSource ... ");
    }

    /**
     * 切换数据源
     * @param key
     * @return
     */
    public boolean toggleDataSource(String key){
        if (DynamicDataSourceContextHolder.getInstance().containDataSourceKey(key)) {
            String concurrentDataBase = DynamicDataSourceContextHolder.getInstance().getDataSourceKey();
            DynamicDataSourceContextHolder.getInstance().setDataSourceKey(key);
            determineTargetDataSource();
            logger.info("toggleDataSource {} -> {}",concurrentDataBase ,key);
            return true;
        }
        return false;
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getInstance().getDataSourceKey();
    }

}

DynamicRoutingDataSourceConfig

初始化相关bean。这里需要将DynamicRoutingDataSource放到MybatisSqlSessionFactoryBean、PlatformTransactionManager中。

/**
 * @author z201.coding@gmail.com
 **/
@Configuration
public class DynamicRoutingDataSourceConfig {

    private static final Logger logger = LoggerFactory.getLogger(DynamicRoutingDataSourceConfig.class);

    @Autowired
    ApplicationContext applicationContext;

    @Resource
    TenantInfoDao tenantInfoDao;

    @Autowired
    HikariDataSource hikariDataSource;

    @Bean(DynamicDataSourceConstant.MASTER)
    @ConfigurationProperties(prefix = "spring.datasource.hikari")
    public DataSource master() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConditionalOnBean(name = {DynamicDataSourceConstant.MASTER})
    public DynamicRoutingDataSource dynamicRoutingDataSource(@Qualifier(DynamicDataSourceConstant.MASTER) DataSource dataSource) {
        List<String> dataBasesList = new JdbcTemplate(dataSource).queryForList("SELECT DATABASE()", String.class);
        if (CollectionUtils.isEmpty(dataBasesList)) {
            ClassPathResource classPathResource = new ClassPathResource("create.sql");
            try {
                ScriptUtils.executeSqlScript(dataSource.getConnection(), classPathResource);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
        dynamicRoutingDataSource.setDynamicRoutingDataSource(dataSource, new HashMap<>());

        return dynamicRoutingDataSource;
    }

    @PostConstruct
    @ConditionalOnBean(DynamicRoutingDataSource.class)
    public void initDynamicDataSource() throws Exception {
        logger.info("initDynamicDataSource ...");
        DynamicRoutingDataSource dynamicRoutingDataSource = applicationContext.getBean(DynamicRoutingDataSource.class);
        HikariDataSource master = (HikariDataSource) applicationContext.getBean(DynamicDataSourceConstant.MASTER);
        Map<Object, Object> dataSourceMap = new HashMap<>();
        List<TenantInfo> tenantInfoList = tenantInfoDao.selectList(null);
        tenantInfoList.stream().forEach(i ->
                {
                    /**
                     *       maximumPoolSize: 20 # 连接池最大连接数,默认是10
                     *       minimumIdle: 5  # 最小空闲连接数量
                     *       idleTimeout: 600000 # 空闲连接存活最大时间,默认600000(10分钟)
                     *       connectionTimeout: 30000 # 数据库连接超时时间,默认30秒,即30000
                     *       maxLifetime: 1800000 # 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
                     */
                    logger.info(" init  dataSource {}", i.getTenantName());
                    HikariConfig hikariConfig = new HikariConfig();
                    hikariConfig.setDriverClassName(i.getDatasourceDriver());
                    hikariConfig.setJdbcUrl(i.getDatasourceUrl());
                    hikariConfig.setUsername(i.getDatasourceUsername());
                    hikariConfig.setPassword(i.getDatasourcePassword());
                    hikariConfig.setMaximumPoolSize(20);
                    hikariConfig.setMinimumIdle(2);
                    hikariConfig.setIdleTimeout(600000);
                    hikariConfig.setConnectionTimeout(30000);
                    hikariConfig.setMaxLifetime(1800000);
                    // 这里其实是需要对连接信息进行测试。演示环境暂时不处理。
                    // new HikariDataSource(hikariConfig) 用于初始化连接信息。
                    DataSource dataSource = new HikariDataSource(hikariConfig);
                    dataSourceMap.put(i.getTenantId(), dataSource);
                }
        );
        // 设置数据源
        dynamicRoutingDataSource.setDynamicRoutingDataSource(master, dataSourceMap);
    }

    @Bean
    public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("dynamicRoutingDataSource") DynamicRoutingDataSource dynamicRoutingDataSource
    ) throws Exception {
        MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
        sessionFactory.setDataSource(dynamicRoutingDataSource);
        // 可以从配置文件中获取,这里演示暂时写死。
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
        sessionFactory.setTypeAliasesPackage("cn.z201.dynamic.persistence.entity");
        MybatisConfiguration config = new MybatisConfiguration();
        config.setMapUnderscoreToCamelCase(true);  //开启下划线转驼峰
        config.setLogImpl(Slf4jImpl.class); // 日志输出实现
        sessionFactory.setConfiguration(config);
        return sessionFactory;
    }

    /**
     * 配置事务管理, 使用事务时在方法头部添加@Transactional注解即可
     *
     * @param dynamicRoutingDataSource
     * @return
     */
    @Bean
    public PlatformTransactionManager transactionManager(@Qualifier("dynamicRoutingDataSource") DynamicRoutingDataSource dynamicRoutingDataSource) {
        return new DataSourceTransactionManager(dynamicRoutingDataSource);
    }

    /**
     * jdbc 扩展
     *
     * @param dynamicRoutingDataSource
     * @return
     */
    @Bean
    public DynamicJdbcTemplateManager dynamicJdbcTemplateManager(@Qualifier("dynamicRoutingDataSource") DynamicRoutingDataSource dynamicRoutingDataSource) {
        return new DynamicJdbcTemplateManager(dynamicRoutingDataSource);
    }


}

单元测试

测试前初始化一个默认数据库、租户表信息。

测试流程

  1. 插入租户表。插入测试的租户信息。
  2. 循环切换租户数据源。并输出对应数据源连接的db信息。

SQL

CREATE
DATABASE IF NOT EXISTS `docker_dynamic_data` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

DROP TABLE IF EXISTS docker_dynamic_data.`tenant_info`;
CREATE TABLE docker_dynamic_data.`tenant_info`
(
    `id`                  BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
    `is_enable`           bit(1) NOT NULL DEFAULT b'1' COMMENT '数据是否有效 1 有效 0 无效',
    `create_time`         bigint(20) unsigned NOT NULL COMMENT '创建时间',
    `update_time`         bigint(20) unsigned NOT NULL COMMENT '更新时间',
    `tenant_id`           VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '租户id',
    `tenant_name`         VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '租户名称',
    `datasource_url`      VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '数据源url',
    `datasource_username` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '数据源用户名',
    `datasource_password` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '数据源密码',
    `datasource_driver`   VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '数据源驱动',
    `system_account`      VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '系统账号',
    `system_password`     VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '账号密码',
    `system_project`      VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '系统PROJECT',
    PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;

CREATE UNIQUE INDEX tenant_id_unique on docker_dynamic_data.`tenant_info` (tenant_id);
CREATE UNIQUE INDEX tenant_name_unique on docker_dynamic_data.`tenant_info` (tenant_name);

AppApplicationTest

@Slf4j
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = AppApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
@AutoConfigureMockMvc
// 指定单元测试方法顺序
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class AppApplicationTest {

    @Autowired
    private ApplicationContext applicationContext;

    @Resource
    private TenantInfoDao tenantInfoDao;

    @Autowired
    private DynamicJdbcTemplateManager jdbcTemplate;

    @BeforeEach
    public void before() {
        log.info("before");
        List<TenantInfo> tenantInfoList = tenantInfoDao.selectList(null);
        if (CollectionUtils.isEmpty(tenantInfoList)) {
            TenantInfo tenantInfo = new TenantInfo();
            int i = 3;
            for (int j = 0; j < i; j++) {
                String sql = "CREATE DATABASE IF NOT EXISTS `docker_dynamic_data_${db}` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;".replace("${db}", String.valueOf(j));
                log.info("sql {} \n", sql);
                tenantInfo.setTenantId(String.valueOf(SnowflakeTool.getInstance().nextId()));
                tenantInfo.setTenantName(Base64.getEncoder().encodeToString(tenantInfo.getTenantId().getBytes()));
                tenantInfo.setDatasourceUsername("root");
                tenantInfo.setDatasourcePassword("123456");
                tenantInfo.setDatasourceDriver("com.mysql.cj.jdbc.Driver");
                tenantInfo.setDatasourceUrl(DynamicRoutingDataSourceTool.buildDataBase("docker_dynamic_data_" + j));
                tenantInfoDao.insert(tenantInfo);
            }
        }
    }


    @AfterEach
    public void after() {
        log.info("after");
    }

    @Test
    public void setUp() {
        log.info("bean Count {}", applicationContext.getBeanDefinitionCount());
        lookupCollectionType(applicationContext);
        DynamicRoutingDataSource dynamicRoutingDataSource = applicationContext.getBean(DynamicRoutingDataSource.class);
        Set<Object> dataSourceKeys = DynamicDataSourceContextHolder.getInstance().all();
        log.info("dataSourceKey All {}", dataSourceKeys);
        for (Object dataSourceKey : dataSourceKeys) {
            // 切换数据库
            dynamicRoutingDataSource.toggleDataSource(dataSourceKey.toString());
            // jdbcTemplate 在运行时需要重新指定 DataSource
            concurrentDataBase();
        }
    }

    private void concurrentDataBase() {
        List<String> dataBasesList = jdbcTemplate.dynamicJdbcTemplate().queryForList("SELECT DATABASE();", String.class);
        log.info("concurrentDataBase {}", dataBasesList);
    }

    /**
     * 单一类型集合查找
     *
     * @param beanFactory
     */
    private void lookupCollectionType(BeanFactory beanFactory) {
        if (beanFactory instanceof ListableBeanFactory) {
            ListableBeanFactory listableBeanFactory = (ListableBeanFactory) beanFactory;
            Map<String, DataSource> beansMap = listableBeanFactory.getBeansOfType(DataSource.class);
            beansMap.forEach((key, value) -> {
                System.out.println("单一类型集合查找  " + key + " " + value);
            });
        }
    }


}
  • Console
[main] [AppApplicationTest.java : 53] before
[main] [BaseJdbcLogger.java : 137] ==>  Preparing: SELECT id,tenant_id,tenant_name,datasource_url,datasource_username,datasource_password,datasource_driver,system_account,system_password,system_project,is_enable,create_time,update_time FROM tenant_info
[main] [BaseJdbcLogger.java : 137] ==> Parameters: 
[main] [BaseJdbcLogger.java : 137] <==      Total: 3
[main] [AppApplicationTest.java : 80] bean Count 96
单一类型集合查找  master HikariDataSource (HikariPool-1)
单一类型集合查找  dynamicRoutingDataSource cn.z201.dynamic.dynamic.DynamicRoutingDataSource@63cd2cd2
[main] [AppApplicationTest.java : 84] dataSourceKey All [942592207909552128, 942592207829860352, 942592207959883776]
[main] [DynamicRoutingDataSource.java : 46] toggleDataSource master -> 942592207909552128
[main] [AppApplicationTest.java : 95] concurrentDataBase [docker_dynamic_data_1]
[main] [DynamicRoutingDataSource.java : 46] toggleDataSource 942592207909552128 -> 942592207829860352
[main] [AppApplicationTest.java : 95] concurrentDataBase [docker_dynamic_data_0]
[main] [DynamicRoutingDataSource.java : 46] toggleDataSource 942592207829860352 -> 942592207959883776
[main] [AppApplicationTest.java : 95] concurrentDataBase [docker_dynamic_data_2]
[main] [AppApplicationTest.java : 75] after

END

Last Updated:
Contributors: 庆峰