springboot+mybatis-plus多租户动态切换数据源

数据库连接池druid:


            com.alibaba
            druid-spring-boot-starter
            1.1.21

yml中druid配置:

druid:
      initial-size: 5                                       # 初始化大小
      min-idle: 10                                          # 最小连接数
      max-active: 20                                        # 最大连接数
      max-wait: 60000                                       # 获取连接时的最大等待时间

配置文件中配置默认的数据源datasource,默认数据源中需要建立一张各租户的数据源表:springboot+mybatis-plus多租户动态切换数据源_第1张图片

 上图表中有redis-database字段为切换redis库时使用的字段,如不需要可自行修改。

创建DataSourceContextHolder用于切换数据源,使用threadlocal保留当前线程数据源信息:

@Slf4j
public class DataSourceContextHolder {
    
    private static final ThreadLocal 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 custom;

    @Override
    protected Object determineCurrentLookupKey() {
        String datasourceId = DataSourceContextHolder.getDataSource();
        if(!StringUtils.isEmpty(datasourceId)){
            Map 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 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 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 map = this.custom;
        Set 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 targetDataSources = new HashMap();
        //额外数据源配置 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 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   database          = new ThreadLocal();

    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;
        }

    }
}

你可能感兴趣的:(java)