多数据源热切换(请求级别)spring+druid+mybatis-plus

JDBC、DriverManager、DataSource、Druid、c3p0、Hikri、Mybatis、spring-jdbc、mysql-driver

4bc60dbb8ac77cf5cb25c9c8fb4a1e71.svg

  • jdbc 是一种规范,定义接口api
  • DriverManager:驱动管理器类,用于管理所有注册的驱动程序。
  • DataSource: jdbc定义的接口,DriverManager 工具的替代项
  • Druid:数据库连接池
  • c3p0:数据库连接池
  • Hikri:数据库连接池
  • Mybatis:
  • spring-jdbc、
  • mysql-driver:JDBC的实现

多数据源的加载过程

17970a0f73b1ce99a04ce3c66cd3ceb7.svg

好处就是轻量级。

有问题,就是不支持自己热加载数据源配置。

单库多schema实现方式优化

租户表中记录对应的数据库。

所有的数据库记录在单独的表中,维护在内存里面,项目启动之后或者数据库的数据更新之后记得更新缓存。

7f1572c293225701b6ae9257cec302ff.svg

  • 建立主数据源连接:项目启动的时候,首先获取主数据源配置,之后先删除之前的主数据源配置(因为有可能修改),再添加新数据源的配置,最后去主数据库中获取所有数据源的信息,放到map里面
  • 建立每一个数据源的连接:之后 for 循环获取每一个数据源连接,同样放到map中。
  • 建立所有数据源的连接
  • 请求处理:根据用户的租户信息获取对应的数据库连接,处理对应的请求之后,再删除threadLocal里面的连接即可。

这样做的好处就是不需要每一张表都维护一个租户ID。

public class MultiDatasourceInitListener implements 
    ApplicationListener<ApplicationContextInitializedEvent>, Ordered {

    private static final Log log = Log.get();

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

    @Override
    public void onApplicationEvent(ApplicationContextInitializedEvent applicationContextInitializedEvent) {
        ConfigurableEnvironment environment = applicationContextInitializedEvent.getApplicationContext().getEnvironment();

        // 获取数据库连接配置
        String dataSourceDriver = environment.getProperty("spring.datasource.driver-class-name");
        String dataSourceUrl = environment.getProperty("spring.datasource.url");
        String dataSourceUsername = environment.getProperty("spring.datasource.username");
        String dataSourcePassword = environment.getProperty("spring.datasource.password");

        // 如果有为空的配置,终止执行
        if (ObjectUtil.hasEmpty(dataSourceUrl, dataSourceUsername, dataSourcePassword)) {
            throw new DataSourceException(DataSourceExceptionEnum.DATA_SOURCE_READ_ERROR);
        }

        // 创建主数据源的properties
        DruidProperties druidProperties = new DruidProperties();
        druidProperties.setDriverClassName(dataSourceDriver);
        druidProperties.setUrl(dataSourceUrl);
        druidProperties.setUsername(dataSourceUsername);
        druidProperties.setPassword(dataSourcePassword);

        // 创建主数据源
        DruidDataSource druidDataSource = DruidFactory.createDruidDataSource(druidProperties);

        // 初始化数据源容器
        try {
            DataSourceContext.initDataSource(druidProperties, druidDataSource);
        } catch (Exception e) {
            log.error(">>> 初始化数据源容器错误!", e);
            throw new DataSourceException(DataSourceExceptionEnum.INIT_DATA_SOURCE_ERROR);
        }

    }
}
Map<String, DruidProperties> allDataBaseInfo = 
    dataBaseInfoDao.getAllDataBaseInfo();

// 根据数据源信息初始化所有的DataSource
for (Map.Entry<String, DruidProperties> entry : allDataBaseInfo.entrySet()) {
    String dbName = entry.getKey();
    DruidProperties druidProperties = entry.getValue();
    
    // 如果是主数据源,不用初始化第二遍,如果是其他数据源就通过property初始化
    if (dbName.equalsIgnoreCase(DatabaseConstant.MASTER_DATASOURCE_NAME)) {
        DATA_SOURCES_CONF.put(dbName, druidProperties);
        DATA_SOURCES.put(dbName, dataSourcePrimary);
    } else {
        DataSource dataSource = createDataSource(dbName, druidProperties);
        DATA_SOURCES.put(dbName, dataSource);
    }
}
-- 获取所有数据源配置
select db_name,jdbc_driver,jdbc_url,user_name,password from sys_database_info
-- 增加数据源配置
INSERT INTO `sys_database_info`(`id`, `db_name`, `jdbc_driver`, `user_name`, 
                                `password`, `jdbc_url`, `remarks`, `create_time`)
                                VALUES (?, ?, ?, ?, ?, ?, ?, ?)
-- 删除数据源配置
DELETE from sys_database_info where db_name = ?

-- 创建新租户的数据库
CREATE DATABASE IF NOT EXISTS ? DEFAULT CHARSET utf8 COLLATE utf8_general_ci;

-- 删除旧租户的数据库
DROP DATABASE ?;

租户的多数据源切换的aop

/**
 * 租户的多数据源切换的aop
 */
@Aspect
public class TenantSourceExAop implements Ordered {

    private final Logger log = LoggerFactory.getLogger(this.getClass());

    /**
     * 拦截控制器层
     */
    @Pointcut("execution(* *..controller.*.*(..))")
    public void cutService() {
    }

    @Around("cutService()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        try {
            // 根据系统总开关来进行aop
            if (ConstantContextHolder.getTenantOpenFlag()) {

                // 当前用户已经登陆并且租户信息不为空
                if (LoginContextHolder.me().hasLogin()) {
                    Dict tenantInfo = LoginContextHolder.me().getSysLoginUser().getTenants();
                    if (tenantInfo != null) {

                        // 获取当前用户登录的租户标识,切换数据源
                        String tenantDbName = tenantInfo.getStr(TenantConstants.TENANT_DB_NAME);
                        if (StrUtil.isNotBlank(tenantDbName)) {
                            CurrentDataSourceContext.setDataSourceType(tenantDbName);
                            log.debug(">>> 多租户AOP--TenantSourceExAop--设置数据源为:" + tenantDbName);
                        }
                    }
                }
            }
            return point.proceed();
        } finally {
            log.debug(">>> 多租户AOP--TenantSourceExAop--清空数据源信息!");
            CurrentDataSourceContext.clearDataSourceType();
            TenantCodeHolder.remove();
            TenantDbNameHolder.remove();
        }
    }

    /**
     * aop的顺序要早于多数据源切换的
     */
    @Override
    public int getOrder() {
        return AopSortConstant.TENANT_EXCHANGE_AOP;
    }

}

详细参考:

https://www.cnblogs.com/zdd-java/p/zdd_datasource_aop.html

public abstract class AbstractRoutingDataSource extends AbstractDataSource {

    /**
     * 子类实现决定最终数据源
     *
     * @return 数据源
     */
    protected abstract DataSource determineDataSource();

    @Override
    public Connection getConnection() throws SQLException {
        return determineDataSource().getConnection();
    }

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

    @Override
    @SuppressWarnings("unchecked")
    public <T> T unwrap(Class<T> iface) throws SQLException {
        if (iface.isInstance(this)) {
            return (T) this;
        }
        return determineDataSource().unwrap(iface);
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return (iface.isInstance(this) || determineDataSource().isWrapperFor(iface));
    }
}
class DynamicDataSource implements AbstractDataSource { 
    
    @Override
    protected DataSource determineDataSource() {

        // 获取当前Context存储的数据源名称
        String dataSourceType = CurrentDataSourceContext.getDataSourceType();

        // 如果当前Context没有值,就用主数据源
        if (StrUtil.isEmpty(dataSourceType)) {
            dataSourceType = DatabaseConstant.MASTER_DATASOURCE_NAME;
        }

        // 从数据源容器中获取对应的数据源
        Map<String, DataSource> dataSources = DataSourceContext.getDataSources();
        return dataSources.get(dataSourceType);
    }
}
@Configuration
@Import(DataSourcePropertiesConfig.class)
public class MultiDataSourceConfig {

    /**
     * 多数据源的连接池
     */
    @Bean
    public DynamicDataSource dataSource() {
        return new DynamicDataSource();
    }
}
public class RemoveTenantListener implements ApplicationListener<ServletRequestHandledEvent> {

    @Override
    public void onApplicationEvent(ServletRequestHandledEvent event) {
        TenantCodeHolder.remove();
        TenantDbNameHolder.remove();
    }

}

动态添加租户

a0d451ee1a49eb60f431c3956f8577c3.svg

@Override
@Transactional(rollbackFor = Exception.class)
public void add(TenantInfoParam param) {

    // 创建租户数据库
    DruidProperties druidProperties = SpringUtil.getBean(DruidProperties.class);
    String databaseName = TENANT_DB_PREFIX + param.getCode();
    DatabaseUtil.createDatabase(druidProperties, databaseName);

    // 创建租户的数据源记录
    DatabaseInfoService databaseInfoService;
    try {
        databaseInfoService = SpringUtil.getBean(DatabaseInfoService.class);
    } catch (Exception e) {
        throw new TenantException(TenantExceptionEnum.DBS_MODULAR_NOT_ENABLE_ERROR);
    }
    DatabaseInfoParam dataBaseInfo = DataBaseInfoFactory.createDataBaseInfo(druidProperties, databaseName);
    databaseInfoService.add(dataBaseInfo);

    // 初始化租户的数据库
    SqlRunUtil.runClassPathSql(INIT_SQL_FILE_NAME, databaseName);

    // 插入租户记录
    TenantInfo tenantInfo = new TenantInfo();
    BeanUtil.copyProperties(param, tenantInfo);
    tenantInfo.setDbName(databaseName);
    this.save(tenantInfo);

    // 切换数据源到新的租户,初始化新租户的用户名和密码
    String hashPw = BCrypt.hashpw(param.getAdminPassword(), BCrypt.gensalt());
    DataSource dataSource = DataSourceContext.getDataSources().get(databaseName);
    Connection connection = null;
    try {
        connection = dataSource.getConnection();
        SqlExecutor.execute(connection, UPDATE_SQL, hashPw);
    } catch (SQLException e) {
        log.error(">>> 更新多租户的用户密码错误!", e);
        throw new TenantException(TenantExceptionEnum.UPDATE_TENANT_PASSWORD_ERROR);
    } finally {
        DbUtil.close(connection);
    }
}

一个事务相关的问题

每次调用的时候会走chooseDataSource再走AOP,经查询发现是事务问题导致,因为它先走了事务切面,然后事务还没结束的时候如果再去切换数据源的话是不成立的。方法解决很简单,设置一下切换数据源的AOP的优先级,确保在事务执行之前就已经切换数据源

有事务时不管注解是什么都会取master,spring事务切面优先执行,创建事务时dataSource已经被绑定了,去执行方法时通过切面虽然切换了当前数据源但是,执行sql时是从事务的threadLocal resources 中取,取的是创建事务时的数据库连接。

声明式事务与上文切换数据源的aop都是基于动态代理,或者说后置处理器实现的,标记一个@Order是不是可以解决这个问题?

1647262928453-07f051a8-5dc9-4557-802d-411414566f85.png

https://blog.csdn.net/qq_31156277/article/details/85227415

有那么多dataSource,但都不是Bean,只有一个是Bean,

和Bean的名字么有关系,使用的时候使用的是接口,调用的时候看的是子类,所以这里有多个子类是Bean的时候就会使用@Primary来决定,都没有的话就会抛出异常。

但是本项目里面因为只有一个DataSource,所以不存在这个问题

posted on 2025-10-14 23:51  chuchengzhi  阅读(13)  评论(0)    收藏  举报

导航

杭州技术博主,专注分享云计算领域实战经验、技术教程与行业洞察, 打造聚焦云计算技术的垂直博客,助力开发者快速掌握云服务核心能力。

褚成志 云计算 技术博客