项目中配置多数据源应用场景

项目背景

公司新开的项目,需要连接业务库,业务库是集群部署的,整体结构,一个集群,对应多个分区,每个分区保存多个店铺数据,也有独立部署的店铺,分区就是数据库连接实例

大概思路

1.一集群都有一个公库,先连接公库地址,查询当前集群下面的所有分区数据库连接地址
2.拿到分区的数据连接地址之后,创建分区数据库连接
3.使用SpringBoot多数据源配置,动态切换数据源

上代码

直说大概思路,代码不方面贴出来,A集群,先看原来数据库设计,公库下主要有个cm_db(数据库表)表维护这当前集群下的所有分区连接信息,主要字段有db_id(数据库id),db_host(服务器地址,主从用","隔开),db_user(用户名),db_pwd(密码)和cm_seller(商家表)主要字段seller_id(卖家id),shop_id(店铺id),db_id(数据库id)

配置公库连接信息
spring:
  application:
    name: order-server # 应用名称
  datasource:
    druid:
      a: #测试库
        url: jdbc:mysql://xxx:3306/yyy?allowMultiQueries=true&useUnicode=true&useSSL=true&characterEncoding=utf8&tinyInt1isBit=false
        username: root
        password: root
        driver-class-name: com.mysql.jdbc.Driver
        # 初始化时建立物理连接的个数。初始化发生在显示调用 init 方法,或者第一次 getConnection 时
        initialSize: 10
        # 最小连接池数量
        minIdle: 10
        # 最大连接池数量
        maxActive: 100
        # 获取连接时最大等待时间,单位毫秒。配置了 maxWait 之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置 useUnfairLock 属性为 true 使用非公平锁。
        maxWait: 60000
        # Destroy 线程会检测连接的间隔时间,如果连接空闲时间大于等于 minEvictableIdleTimeMillis 则关闭物理连接。
        timeBetweenEvictionRunsMillis: 60000
        # 连接保持空闲而不被驱逐的最小时间
        minEvictableIdleTimeMillis: 300000
        # 用来检测连接是否有效的 sql 因数据库方言而异, 例如 oracle 应该写成 SELECT 1 FROM DUAL
        validationQuery: SSELECT 1
        # 建议配置为 true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于 timeBetweenEvictionRunsMillis,执行 validationQuery 检测连接是否有效。
        testWhileIdle: true
        # 申请连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。
        testOnBorrow: false
        # 归还连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。
        testOnReturn: false
        # 是否自动回收超时连接
        removeAbandoned: true
        # 超时时间 (以秒数为单位)
        remove-abandoned-timeout: 1800
        logAbandoned: true
        pinGlobalTxToPhysicalConnection: true
      b: #测试库
        url: jdbc:mysql://xxx:3306/yyy?allowMultiQueries=true&useUnicode=true&useSSL=true&characterEncoding=utf8&tinyInt1isBit=false
        username: root
        password: root
        driver-class-name: com.mysql.jdbc.Driver
        # 初始化时建立物理连接的个数。初始化发生在显示调用 init 方法,或者第一次 getConnection 时
        initialSize: 10
        # 最小连接池数量
        minIdle: 10
        # 最大连接池数量
        maxActive: 100
        # 获取连接时最大等待时间,单位毫秒。配置了 maxWait 之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置 useUnfairLock 属性为 true 使用非公平锁。
        maxWait: 60000
        # Destroy 线程会检测连接的间隔时间,如果连接空闲时间大于等于 minEvictableIdleTimeMillis 则关闭物理连接。
        timeBetweenEvictionRunsMillis: 60000
        # 连接保持空闲而不被驱逐的最小时间
        minEvictableIdleTimeMillis: 300000
        # 用来检测连接是否有效的 sql 因数据库方言而异, 例如 oracle 应该写成 SELECT 1 FROM DUAL
        validationQuery: SSELECT 1
        # 建议配置为 true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于 timeBetweenEvictionRunsMillis,执行 validationQuery 检测连接是否有效。
        testWhileIdle: true
        # 申请连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。
        testOnBorrow: false
        # 归还连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。
        testOnReturn: false
        # 是否自动回收超时连接
        removeAbandoned: true
        # 超时时间 (以秒数为单位)
        remove-abandoned-timeout: 1800
        logAbandoned: true
        pinGlobalTxToPhysicalConnection: true

######配置默认的数据源,配置Druid数据库连接池,配置sql工厂加载mybatis_plus的文件,扫描实体类等

@Slf4j
@Configuration
@EnableTransactionManagement
public class DataSourceConfig {

    /**
     * a集群
     * @return
     */
    @Bean(name = "a")
    @ConfigurationProperties(prefix = "spring.datasource.druid.a")
    public DataSource a() {
        return new DruidDataSource();
    }

    /**
     * b集群
     * @return
     */
    @Bean(name = "b")
    @ConfigurationProperties(prefix = "spring.datasource.druid.b")
    public DataSource b() {
        return new DruidDataSource();
    }


    /**
     * 默认数据源配置和多数据源配置
     *
     * @return 数据源
     */
    @Bean(name = "dynamicDataSource")
    @Qualifier("dynamicDataSource")
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 默认数据源
        dynamicDataSource.setDefaultTargetDataSource(masterDateSource());
        // 配置多数据源
        Map<Object, Object> dataBaseMap = new HashMap<>(16);
        //默认数据源
        dataBaseMap.put(Constant.DB_MASTER, a());

        DruidDataSource fxcs = (DruidDataSource) a();
        if (!StringUtils.isEmpty(a.getRawJdbcUrl())) {
            log.info("初始化分区标识:{},url:{},用户名:{},密码:{}"
                    ,"fxcs"
                    , a.getUrl()
                    , a.getUsername()
                    , a.getPassword());
            dataBaseMap.put("a", a());
        }
        DruidDataSource fxcp = (DruidDataSource) b();
        if (!StringUtils.isEmpty(b.getRawJdbcUrl())) {
            log.info("初始化分区标识:{},url:{},用户名:{},密码:{}"
                    ,"fxcp"
                    , b.getUrl()
                    , b.getUsername()
                    , b.getPassword());
            dataBaseMap.put("b", b());
        }
       
    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
        // 导入mybatissqlsession配置
        MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
        // 指明数据源
        sessionFactory.setDataSource(dataSource);
        // 指明mapper.xml位置(配置文件中指明的xml位置会失效用此方式代替,具体原因未知)
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/**/*.xml"));
        // 指明实体扫描(多个package用逗号或者分号分隔)
        sessionFactory.setTypeAliasesPackage("com.xxx.entity");
        // 导入mybatis配置
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setCacheEnabled(false);
        sessionFactory.setConfiguration(configuration);
        // 添加分页功能 乐观锁
        sessionFactory.setPlugins(new Interceptor[]{
                paginationInterceptor(),optimisticLockerInterceptor()
        });
        // 导入全局配置
        sessionFactory.setGlobalConfig(globalConfiguration());
        return sessionFactory.getObject();
    }


    /**
     * 在代码中配置MybatisPlus替换掉application.yml中的配置
     *
     * @return
     */
    @Bean
    public GlobalConfiguration globalConfiguration() {
        GlobalConfiguration configuration = new GlobalConfiguration();
        configuration.setLogicDeleteValue("1");
        configuration.setLogicNotDeleteValue("0");
        // 主键类型 0:数据库ID自增, 1:用户输入ID,2:全局唯一ID (数字类型唯一ID), 3:全局唯一ID UUID
        configuration.setIdType(0);
        // 驼峰下划线转换
        configuration.setDbColumnUnderline(true);
        configuration.setMetaObjectHandler(new MyMetaObjectHandler());
        configuration.setSqlInjector(new LogicSqlInjector());
        // 是否动态刷新mapper
        configuration.setRefresh(true);
        return configuration;
    }
    /**
     * mybatis-plus分页插件<br>
     * 文档:http://mp.baomidou.com<br>
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        paginationInterceptor.setDialectType("mysql");
        return paginationInterceptor;
    }


    /**
     * 乐观锁插件
     *
     * @return
     */
    @Bean
    public OptimisticLockerInterceptor optimisticLockerInterceptor() {
        return new OptimisticLockerInterceptor();
    }

    /**
     * 事务管理
     *
     * @param dataSource 数据源
     * @return 事务管理
     */
    @Bean(name = "sqlTransactionManager")
    public PlatformTransactionManager platformTransactionManager(@Qualifier("dynamicDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "sqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

}
数据源包装对象
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Accessors(chain = true)
public class TpDataSource {
    private String url;
    private String username;
    private String password;
    private String driveClass;
    private String datasourceId;
}
手动切换数据源
@Slf4j
public class DataSourceContextHolder {
    /**
     * 线程独立
     */
    private static  final ThreadLocal<String> contextHolder = new ThreadLocal<String>();


    /**
     * 获取数据源名
     *
     * @return 数据库名
     */
    public static String getDataBaseType() {
        return contextHolder.get();
    }

    /**
     * 设置数据源名(切换数据源)
     *在在事务之前设置 否则事务不生效  最好在controller层设置好
     * @param dataBase 数据库类型
     */
    public static void setDataBaseType(String dataBase) {
        log.debug("设置数据源:{}",dataBase);
        contextHolder.set(dataBase);
    }

    /**
     * 清除数据源名
     */
    public static void clearDataBaseType() {
        contextHolder.remove();
    }

}
创建数据源
@Data
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {

    //数据源列表
    private Map<Object, Object> dynamicTargetDataSources;

    //默认数据源
    private Object dynamicDefaultTargetDataSource;

    /**
     * 获取当前数据源并打印日志记录
     *
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        String datasource = DataSourceContextHolder.getDataBaseType();
        if (!StringUtils.isEmpty(datasource)) {
            Map<Object, Object> dynamicTargetDataSources2 = this.dynamicTargetDataSources;
            if (!dynamicTargetDataSources2.containsKey(datasource)) {
                log.error("不存在的数据源:{}", datasource);
            }
        }
        log.debug("当前数据源:{}",StringUtils.isEmpty(datasource)?"默认数据源":datasource);
        return datasource;
    }

    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);
        this.dynamicTargetDataSources = targetDataSources;
    }

    @Override
    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        this.dynamicDefaultTargetDataSource = defaultTargetDataSource;
    }

    public boolean createDataSource(TpDataSource tpDataSource) {
        if (!testDatasource(tpDataSource)) {
            log.error("测试数据源连接失效==数据源key:{},数据源url:{},账户:{},密码:{}"
                    ,tpDataSource.getDatasourceId()
                    ,tpDataSource.getUrl()
                    ,tpDataSource.getUsername()
                    ,tpDataSource.getPassword());
            return false;
        }
        try {
            DruidDataSource druidDataSource = new DruidDataSource();
            druidDataSource.setName(tpDataSource.getDatasourceId());
            druidDataSource.setDriverClassName(tpDataSource.getDriveClass());
            druidDataSource.setUrl(tpDataSource.getUrl());
            druidDataSource.setUsername(tpDataSource.getUsername());
            druidDataSource.setPassword(tpDataSource.getPassword());
            druidDataSource.setValidationQuery("SELECT 1");
            druidDataSource.init();
            this.dynamicTargetDataSources.put(tpDataSource.getDatasourceId(), druidDataSource);
            // 将map赋值给父类的TargetDataSources
            setTargetDataSources(this.dynamicTargetDataSources);
            // 将TargetDataSources中的连接信息放入resolvedDataSources管理
            super.afterPropertiesSet();
            log.info("数据源初始化成功:{},{}", tpDataSource.getDatasourceId(), tpDataSource.getUrl());
        } catch (SQLException throwables) {
            log.error("数据源初始化失败==数据源key:{},数据源url:{},账户:{},密码:{}"
                    ,tpDataSource.getDatasourceId()
                    ,tpDataSource.getUrl()
                    ,tpDataSource.getUsername()
                    ,tpDataSource.getPassword());
            log.error(throwables.getMessage(),throwables);
            return false;
        }
        return true;
    }


    /**
     * 测试数据源连接是否有效
     *
     * @return
     */
    public boolean testDatasource(TpDataSource tpDataSource) {
        try {
            Class.forName(tpDataSource.getDriveClass());
            DriverManager.getConnection(tpDataSource.getUrl(), tpDataSource.getUsername(), tpDataSource.getPassword());
            return true;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return false;
        }
    }
}
初始化分区数据源
@Component
@Slf4j
public class CommonConfig {

    @Resource(name = "dynamicDataSource")
    private  DynamicDataSource dynamicDataSource;
    @Autowired
    private DbService dbService;

    @PostConstruct
    public void init(){
        Map<Object, Object> map = dynamicDataSource.getDynamicTargetDataSources();
        HashMap<Object, Object> objectObjectHashMap = new HashMap<>(16);
        for (Map.Entry<Object, Object> objectObjectEntry : map.entrySet()) {
            objectObjectHashMap.put(objectObjectEntry.getKey(),objectObjectEntry.getValue());
        }
        for (Map.Entry<Object, Object> objectObjectEntry : objectObjectHashMap.entrySet()) {
            log.info("数据源:{}",objectObjectEntry.getKey().toString());
            try {
                createDataSource(objectObjectEntry.getKey());
            } catch (Exception e) {
                log.error(e.getMessage(),e);
            }
        }
    }

    private void createDataSource(Object key){
        DataSourceContextHolder.setDataBaseType(key.toString());
        List<DbEntity> dbEntities = dbService.selectList(new EntityWrapper<DbEntity>());
        for (DbEntity dbEntity : dbEntities) {
            log.info("当前集群下:{},分区:{}",key.toString(), JSONObject.toJSONString(dbEntity));
            String[] split = dbEntity.getDbHost().split(",");
            String[] split1 = dbEntity.getDbUser().split(",");
            String[] split2 = dbEntity.getDbPwd().split(",");
            for(int a=0;a<split.length;a++){
                TpDataSource build = TpDataSource.builder()
                        .datasourceId(split[a]+"_"+dbEntity.getDbName())
                        .url("jdbc:mysql://" + split[a] + ":" + dbEntity.getDbPort()
                                + "/" + dbEntity.getDbName()
                                + "?allowMultiQueries=true&useUnicode=true&useSSL=true&characterEncoding=utf8&tinyInt1isBit=false&rewriteBatchedStatements=true")
                        .username(split1[a])
                        .driveClass("com.mysql.jdbc.Driver")
                        .password(split2[a])
                        .build();
                log.info("集群:{},准备创建数据源key:{},db_id:{},数据源url:{},用户名:{},密码:{}"
                        ,key.toString()
                        , split[a]+"_"+dbEntity.getDbName()
                        , dbEntity.getDbId()
                        , build.getUrl()
                        , split1[a]
                        , split2[a]);
                dynamicDataSource.createDataSource(build);
            }
        }
    }

}
注解切换数据源
@Target(ElementType.METHOD) // 作用到方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时有效
@Documented
public @interface CutDataSource {
    /**
     * 默认主库
     */
    String source() default Constant.DB_MASTER;
}
AOP切面
@Aspect // 申明是个spring管理的bean
@Component
@Slf4j
@Order(-1) //确保该切面在transaction之前执行
@RequiredArgsConstructor
public class CutDataSourceAspect {

	//当前service也是公库的表,维护这店铺分配那个分区
    private final SellerService sellerService;
    //分区连接信息
    private final DbService dbService;
    private final RedisUtil redisUtil;

    /**
     * 申明一个切点 里面是 execution表达式
     */
    @Pointcut("@annotation(com.xxx.CutDataSource)")
    private void controllerAspect() {
    }

    /**
     * 请求method前打印内容
     *
     * @param joinPoint
     */
    @Before(value = "controllerAspect()")
    public void methodBefore(JoinPoint joinPoint) {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        //分区标识 从网关层传递过来的 可以通过店铺的域名 确定是那个集群下的
        String db_tag = request.getHeader("DB_TAG");
        String sid = request.getHeader("SID");
        if (StringUtils.isEmpty(db_tag) || StringUtils.isEmpty(sid)) {
            throw new RRException(ErrorUtils.getErrorCode(ErrorEnum.SYSTEM_RESOURCE_NOT_FIND.value(), 00)
                    , ErrorEnum.SYSTEM_RESOURCE_NOT_FIND.getMessage());
        }
        //查询业务DB  可以适当的缓存
        DataSourceContextHolder.setDataBaseType(db_tag);
        DbEntity dbEntity;
        String s = redisUtil.get(Constant.DB_TAG_SID + db_tag + "_" + sid);
        if (StringUtils.isEmpty(s)) {
            SellerEntity sellerEntity = sellerService.selectOne(new EntityWrapper<SellerEntity>()
                    .eq("shop_id", sid));
            if (sellerEntity == null) {
                log.error("当前公库找不到商铺信息=====IP:{},DB_TAG:{},SID:{},UID:{},请求路径:{},请求方式:{},请求类方法:{},请求类方法参数:{}"
                        , IpUtils.getIpAddress(request)
                        , db_tag
                        , sid
                        , request.getHeader("UID")
                        , request.getRequestURL().toString()
                        , request.getMethod()
                        , joinPoint.getSignature()
                        , Arrays.toString(joinPoint.getArgs()));
                DataSourceContextHolder.clearDataBaseType();
                throw new RRException(ErrorUtils.getErrorCode(ErrorEnum.SYSTEM_RESOURCE_NOT_FIND.value(), 00)
                        , ErrorEnum.SYSTEM_RESOURCE_NOT_FIND.getMessage());
            }
            dbEntity = dbService.selectById(sellerEntity.getDbId());
            if (dbEntity == null) {
                log.error("当前公库找不到商铺DB信息=====IP:{},DB_TAG:{},SID:{},UID:{},请求路径:{},请求方式:{},请求类方法:{},请求类方法参数:{}"
                        , IpUtils.getIpAddress(request)
                        , db_tag
                        , sid
                        , request.getHeader("UID")
                        , request.getRequestURL().toString()
                        , request.getMethod()
                        , joinPoint.getSignature()
                        , Arrays.toString(joinPoint.getArgs()));
                DataSourceContextHolder.clearDataBaseType();
                throw new RRException(ErrorUtils.getErrorCode(ErrorEnum.SYSTEM_RESOURCE_NOT_FIND.value(), 00)
                        , ErrorEnum.SYSTEM_RESOURCE_NOT_FIND.getMessage());
            }
            redisUtil.set(Constant.DB_TAG_SID + db_tag + "_" + sid, JSONObject.toJSONString(dbEntity), 60 * 5);
        } else {
            dbEntity = JSONObject.parseObject(s, DbEntity.class);
        }
        // 获取注解中的参数值
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        // 获取注解Action
        CutDataSource annotation = method.getAnnotation(CutDataSource.class);
        String value = annotation.source();

        String[] split = dbEntity.getDbHost().split(",");
        String datasourceId;
        if (Constant.DB_MASTER.equals(value)) {
            datasourceId = split[0];
        } else if (Constant.DB_SLAVE.equals(value)) {
           datasourceId= split.length > 1 ? split[1] : split[0];
        } else {
            throw new RRException(ErrorUtils.getErrorCode(ErrorEnum.SYSTEM_RESOURCE_NOT_FIND.value(), 00)
                    , ErrorEnum.SYSTEM_RESOURCE_NOT_FIND.getMessage());
        }
        DataSourceContextHolder.setDataBaseType(datasourceId + "_" + dbEntity.getDbName());
        log.info("IP:{},DB_TAG:{},数据源key:{},SID:{},UID:{},主从:{},请求路径:{},请求方式:{},请求类方法:{},请求类方法参数:{}"
                , IpUtils.getIpAddress(request)
                , db_tag
                , datasourceId + "_" + dbEntity.getDbName()
                , sid
                , request.getHeader("UID")
                , value
                , request.getRequestURL().toString()
                , request.getMethod()
                , joinPoint.getSignature()
                , Arrays.toString(joinPoint.getArgs()));
    }

    /**
     * 清空contextHolder 防止内存溢出
     *
     * @param joinPoint
     */
    @After(value = "controllerAspect()")
    public void afterSwitchDataSource(JoinPoint joinPoint) {
        DataSourceContextHolder.clearDataBaseType();
    }
}

业务使用

*注意,从库是不能插入数据,可以根据自己的业务场景,配置是否主库

@RestController
public class TestController {

    @RequestMapping(value = "/test", method = RequestMethod.GET)
    @CutDataSource(source = Constant.DB_SLAVE)
    public R test() {
        return R.ok();
    }
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值