多数据源热切换(请求级别)spring+druid+mybatis-plus
JDBC、DriverManager、DataSource、Druid、c3p0、Hikri、Mybatis、spring-jdbc、mysql-driver
- jdbc 是一种规范,定义接口api
- DriverManager:驱动管理器类,用于管理所有注册的驱动程序。
- DataSource: jdbc定义的接口,DriverManager 工具的替代项
- Druid:数据库连接池
- c3p0:数据库连接池
- Hikri:数据库连接池
- Mybatis:
- spring-jdbc、
- mysql-driver:JDBC的实现
多数据源的加载过程
好处就是轻量级。
有问题,就是不支持自己热加载数据源配置。
单库多schema实现方式优化
租户表中记录对应的数据库。
所有的数据库记录在单独的表中,维护在内存里面,项目启动之后或者数据库的数据更新之后记得更新缓存。
- 建立主数据源连接:项目启动的时候,首先获取主数据源配置,之后先删除之前的主数据源配置(因为有可能修改),再添加新数据源的配置,最后去主数据库中获取所有数据源的信息,放到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();
}
}
动态添加租户
@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是不是可以解决这个问题?

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) 收藏 举报
浙公网安备 33010602011771号