数据库连接池druid:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
yml中druid配置:
druid:
initial-size: 5 # 初始化大小
min-idle: 10 # 最小连接数
max-active: 20 # 最大连接数
max-wait: 60000 # 获取连接时的最大等待时间
配置文件中配置默认的数据源datasource,默认数据源中需要建立一张各租户的数据源表:
上图表中有redis-database字段为切换redis库时使用的字段,如不需要可自行修改。
创建DataSourceContextHolder用于切换数据源,使用threadlocal保留当前线程数据源信息:
@Slf4j
public class DataSourceContextHolder {
private static final ThreadLocal<String> DATA_SOURCE = new ThreadLocal<>();
/**
* 切换数据源
*/
public static void setDataSource(String datasourceId) {
DATA_SOURCE.set(datasourceId);
log.info("已切换到数据源:{}",datasourceId);
}
public static String getDataSource() {
return DATA_SOURCE.get();
}
/**
* 删除数据源
*/
public static void removeDataSource() {
DATA_SOURCE.remove();
log.info("已切换到默认主数据源");
}
}
继承AbstractRoutingDataSource实现根据不同请求切换数据源:
@Slf4j
@Data
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
private boolean debug = true;
/**
* 存储注册的数据源
*/
private volatile Map<Object, Object> custom;
@Override
protected Object determineCurrentLookupKey() {
String datasourceId = DataSourceContextHolder.getDataSource();
if(!StringUtils.isEmpty(datasourceId)){
Map<Object, Object> map = this.custom;
if(map.containsKey(datasourceId)){
log.info("当前数据源是:{}",datasourceId);
}else{
log.info("不存在数据源:{}",datasourceId);
return null;
}
}else{
log.info("当前是默认数据源");
}
return datasourceId;
}
@Override
public void setTargetDataSources(Map<Object, Object> param) {
super.setTargetDataSources(param);
this.custom = param;
}
/**
* @Description: 检查数据源是否已经创建
* @author:fff
* @time:2022/4/3
* @param dataSource
*/
public void checkCreateDataSource(DatabaseList dataSource){
String datasourceId = dataSource.getFactoryCode();
Map<Object, Object> map = this.custom;
if(map.containsKey(datasourceId)){
//这里检查一下之前创建的数据源,现在是否连接正常
DruidDataSource druidDataSource = (DruidDataSource) map.get(datasourceId);
boolean flag = true;
DruidPooledConnection connection = null;
try {
connection = druidDataSource.getConnection();
} catch (SQLException throwAbles) {
//抛异常了说明连接失效吗,则删除现有连接
log.error(throwAbles.getMessage());
flag = false;
delDataSources(datasourceId);
//
}finally {
//如果连接正常记得关闭
if(null != connection){
try {
connection.close();
} catch (SQLException e) {
log.error(e.getMessage());
}
}
}
if(!flag){
createDataSource(dataSource);
}
}else {
createDataSource(dataSource);
}
}
/**
* @Description: 创建数据源
* @author:fff
* @time:2022/4/3
* @param dataSource
*/
private void createDataSource(DatabaseList dataSource) {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection(dataSource.getUrl(), dataSource.getUser(), dataSource.getPassword());
if(connection==null){
log.error("数据源配置有错误,DataSource:{}",dataSource);
}else{
connection.close();
}
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setName(dataSource.getFactoryCode());
druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
druidDataSource.setUrl(dataSource.getUrl());
druidDataSource.setUsername(dataSource.getUser());
druidDataSource.setPassword(dataSource.getPassword());
druidDataSource.setMaxActive(20);
druidDataSource.setMinIdle(5);
//获取连接最大等待时间,单位毫秒
druidDataSource.setMaxWait(6000);
String validationQuery = "select 1 from dual";
//申请连接时执行validationQuery检测连接是否有效,防止取到的连接不可用
druidDataSource.setTestOnBorrow(true);
druidDataSource.setValidationQuery(validationQuery);
druidDataSource.init();
this.custom.put(dataSource.getFactoryCode(),druidDataSource);
// 将map赋值给父类的TargetDataSources
setTargetDataSources(this.custom);
// 将TargetDataSources中的连接信息放入resolvedDataSources管理
super.afterPropertiesSet();
} catch (Exception e) {
log.error("数据源创建失败",e);
}
}
/**
* @Description: 删除数据源
* @author:fff
* @time:2022/4/3
* @param datasourceId
*/
private void delDataSources(String datasourceId) {
Map<Object, Object> map = this.custom;
Set<DruidDataSource> druidDataSourceInstances = DruidDataSourceStatManager.getDruidDataSourceInstances();
for (DruidDataSource dataSource : druidDataSourceInstances) {
if (datasourceId.equals(dataSource.getName())) {
map.remove(datasourceId);
//从实例中移除当前dataSource
DruidDataSourceStatManager.removeDataSource(dataSource);
// 将map赋值给父类的TargetDataSources
setTargetDataSources(map);
// 将TargetDataSources中的连接信息放入resolvedDataSources管理
super.afterPropertiesSet();
}
}
}
}
接下来配置默认数据源,还有数据库连接池的信息,以及将数据源配置到sql工厂当中:
@Configuration
@Slf4j
public class DruidDBConfig {
@Value("${spring.datasource.url}")
private String dbUrl;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Value("${spring.datasource.driver-class-name}")
private String driverClassName;
@Value("${spring.datasource.druid.initial-size}")
private int initialSize;
@Value("${spring.datasource.druid.min-idle}")
private int minIdle;
@Value("${spring.datasource.druid.max-active}")
private int maxActive;
@Value("${spring.datasource.druid.max-wait}")
private int maxWait;
@Bean
@Primary
@Qualifier("mainDataSource")
public DataSource dataSource() throws SQLException {
DruidDataSource datasource = new DruidDataSource();
// 基础连接信息
datasource.setUrl(this.dbUrl);
datasource.setUsername(username);
datasource.setPassword(password);
datasource.setDriverClassName(driverClassName);
// 连接池连接信息
datasource.setInitialSize(initialSize);
datasource.setMinIdle(minIdle);
datasource.setMaxActive(maxActive);
datasource.setMaxWait(maxWait);
//是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
datasource.setPoolPreparedStatements(false);
datasource.setMaxPoolPreparedStatementPerConnectionSize(20);
//申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用
datasource.setTestOnBorrow(true);
//建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
datasource.setTestWhileIdle(true);
//用来检测连接是否有效的sql
datasource.setValidationQuery("select 1 from dual");
//配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
datasource.setTimeBetweenEvictionRunsMillis(60000);
//配置一个连接在池中最小生存的时间,单位是毫秒,这里配置为3分钟180000
datasource.setMinEvictableIdleTimeMillis(180000);
datasource.setKeepAlive(true);
return datasource;
}
@Bean(name = "dynamicDataSource")
@Qualifier("dynamicDataSource")
public DynamicRoutingDataSource dynamicDataSource() throws SQLException {
DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
dynamicDataSource.setDebug(false);
//配置缺省的数据源
dynamicDataSource.setDefaultTargetDataSource(dataSource());
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
//额外数据源配置 TargetDataSources
targetDataSources.put("mainDataSource", dataSource());
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
//对新的SqlSessionFactory配置 修改mybatis-plus Page自动分页失效问题 以及 找不到xml问题
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.addInterceptor(new MybatisPlusConfig().paginationInterceptor());
sqlSessionFactoryBean.setConfiguration(configuration);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*.xml"));
return sqlSessionFactoryBean.getObject();
}
/**
* @Description: 将动态数据加载类添加到事务管理器
* @param dataSource
* @return org.springframework.jdbc.datasource.DataSourceTransactionManager
*/
@Bean
public DataSourceTransactionManager transactionManager(DynamicRoutingDataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
现在就可以根据数据源表中唯一值切换数据源了,调用changeDB(数据源唯一值):
@Resource
DatabaseListService databaseListService;
@Resource
private DynamicRoutingDataSource dynamicRoutingDataSource;
@Override
public boolean changeDB(String datasourceId) {
//切到默认数据源
DataSourceContextHolder.removeDataSource();
//找到所有的配置
List<DatabaseList> databaseListList = databaseListService.list();
if(!CollectionUtils.isEmpty(databaseListList)){
for (DatabaseList d : databaseListList) {
if(d.getFactoryCode().equals(datasourceId)){
//判断连接是否存在,不存在就创建
dynamicRoutingDataSource.checkCreateDataSource(d);
//切换数据源
DataSourceContextHolder.setDataSource(d.getFactoryCode());
return true;
}
}
}
return false;
}
以上就完成了所有步骤,可以切换数据源了。
在我的项目里,我在登录的拦截器中校验完权限后判断属于哪个数据源来切换。
下面说一下redis库的动态切换:
同样使用threadlocal保留当前线程的redis库,在get或set时设置redis数据库索引:
@Slf4j
@Component
public class RedisUtil {
private static RedisTemplate redisTemplate;
@Autowired
public void setRedisTemplate(RedisTemplate redisTemplate) {
RedisUtil.redisTemplate = redisTemplate;
}
private static ThreadLocal<Integer> database = new ThreadLocal<Integer>();
public static Integer getDatabase() {
return (Integer) database.get();
}
public static void setDatabase(Integer db) {
database.set(db);
}
/**
* 设置数据库索引
*
*/
public static void setDbIndex() {
//默认使用1
Integer dbIndex = 1;
if(database.get()!=null){
dbIndex = database.get();
}
LettuceConnectionFactory redisConnectionFactory = (LettuceConnectionFactory) redisTemplate
.getConnectionFactory();
if (redisConnectionFactory == null) {
return;
}
redisConnectionFactory.setDatabase(dbIndex);
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 属性设置后
redisConnectionFactory.afterPropertiesSet();
// 重置连接
redisConnectionFactory.resetConnection();
}
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public static Object get(String key) {
setDbIndex();
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public static boolean set(String key, Object value) {
setDbIndex();
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}