java动态多数据源客户端分片

本文介绍了如何在Spring中动态构建和管理多个数据源,以解决数据分片和高可用性问题。作者首先指出了mycat和sharding-jdbc的不足,然后展示了如何通过代码动态创建数据源bean,利用配置文件简化数据源定义,并实现了基于线程局部变量的数据源切换。测试表明该方案有效,但也提出了可优化的方向,如通过参数控制分片和结合配置中心实现动态更新。
摘要由CSDN通过智能技术生成

需求描述

之前在项目中使用的mycat中间件做数据分片,但是因为mycat中心化的问题,一旦挂掉服务就会宕机(虽然基本没挂过),当然可以考虑做双机热备,但毕竟官方没有高可用方案。
后来又有项目使用sharding-jdbc,做客户端分片,一方面sharding的功能很多,大部分分片方式我们实际用不上,另一方面一些复杂sql无法支持。
正好最近的项目又需要多数据源分片方案,就基于以往的不足手动做一个简单的(不考虑跨片数据操作),又能符合项目需求的解决方式。

动态构建数据源对象

以前数据源少的时候,做的比较简单

Slf4j
@Configuration
public class MulDataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "datasource-config.ds1001")
    public DruidDataSource ds1001() {
        DruidDataSource ds = new DruidDataSourceInit();
        return ds;
    }

    @Bean
    @ConfigurationProperties(prefix = "datasource-config.ds1002")
    public DruidDataSource ds1002() {
        return new DruidDataSourceInit();
    }

    @Bean
    @ConfigurationProperties(prefix = "datasource-config.ds1005")
    public DruidDataSource ds1005() {
        return new DruidDataSourceInit();
    }
	@Primary
    @Bean(name = "multiDataSource")
    public MultiRouteDataSource simpleRouteDataSource() {
        MultiRouteDataSource multiDataSource = new MultiRouteDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        fillTargetDataSources(targetDataSources,"1001",ds1001());
        fillTargetDataSources(targetDataSources,"1002",ds1002());
        fillTargetDataSources(targetDataSources,"1005",ds1005());
         multiDataSource.setTargetDataSources(targetDataSources);
//        默认数据源
//        multiDataSource.setDefaultTargetDataSource(ds133Master());
        return multiDataSource;
    }
        /**
     * 填充目标数据源
     * @param targetDataSources
     * @param dsName
     * @param druidDataSource
     */
    private void fillTargetDataSources(Map<Object, Object> targetDataSources,String dsName,DruidDataSource druidDataSource){
        targetDataSources.put(dsName, druidDataSource);
    }
}

现在数据源多了,这么搞不方便,所以使用代码构建bean


/**
 * @ClassName MulDataSourceInit
 * @Description
 * @Author zhangx
 * @Date 2021/4/16 9:56
 **/
@Slf4j
public class MulDataSourceInit {
	// dataSourceProperty是配置文件,applicationContext 是spring上下文
    public MulDataSourceInit(DataSourceProperty dataSourceProperty, ApplicationContext applicationContext){
        String username = dataSourceProperty.getUsername();
        String password = dataSourceProperty.getPassword();
        Integer maxActive = dataSourceProperty.getMaxActive();
        Integer initialSize = dataSourceProperty.getInitialSize();
        Integer minIdle = dataSourceProperty.getMinIdle();
        Map<String,String> urlMap = dataSourceProperty.getConfig();

        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
        urlMap.forEach((index,address)->{
            BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(DataSourceBean.class);
            String beanName = DataSourceConstant.BEAN_NAME_PREFIX + index;
            String url = "jdbc:mysql://" + address + "?characterEncoding=utf8&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull";
            beanDefinitionBuilder.addConstructorArgValue(url);
            beanDefinitionBuilder.addConstructorArgValue(username);
            beanDefinitionBuilder.addConstructorArgValue(password);
            beanDefinitionBuilder.addConstructorArgValue(initialSize);
            beanDefinitionBuilder.addConstructorArgValue(maxActive);
            beanDefinitionBuilder.addConstructorArgValue(minIdle);
            beanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getBeanDefinition());
            log.info("注册数据源{}",beanName);
        });
    }

}

配置文件,config中key作为数据源bean的名字后缀,前缀是ds-,value就是实际数据库地址,比手动一个一个写bean方便多了。。。

mul-datasource:
  username: admin
  password: Admin@2020
  maxActive: 20
  initialSize: 5
  minIdle: 5
  config: {
    1: 10.10.10.190:3306/device_data_1,
    2: 10.10.10.190:3306/device_data_2
  }

对应的配置文件解析类

/**
 * @ClassName DataSourceDefine
 * @Description 配置文件
 * @Author zhangx
 * @Date 2021/4/16 8:57
 **/
@Data
@Component
@ConfigurationProperties(prefix = "mul-datasource")
public class DataSourceProperty {

    private String username;
    private String password;

    private Integer maxActive;
    private Integer initialSize;
    private Integer minIdle;

    private Map<String,String> config;
}

为multiDataSource注入多数据源对象

这个不多说了,spring本身留了接口让我们扩展;

首先在在spring中注入MulDataSourceInit

/**
 * @ClassName DsBeanConfig
 * @Description 生成MulDataSourceInit bean
 * @Author zhangx
 * @Date 2021/4/16 8:40
 **/
@Slf4j
@Configuration
public class DsBeanConfig {

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private DataSourceProperty dataSourceProperty;

    @Bean
    public MulDataSourceInit mulDataSourceInit(){
        return new MulDataSourceInit(dataSourceProperty,applicationContext);
    }
}

注意在routeDataSource方法的入参mulDataSourceInit实际上在这个方法内并没有使用,之所以要带上是因为后面targetDataSources.put时,需要已经构建好的数据源bean,这样通过spring的依赖注入自动先把MulDataSourceInit 中的各个bean构建完成,否则会报错。

/**
 * 数据源配置类
 */
@Slf4j
@Configuration
public class MulDataSourceConfig {

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private DataSourceProperty dataSourceProperty;

    @Primary
    @Bean(name = "multiDataSource")
    public MultiRouteDataSource routeDataSource(MulDataSourceInit mulDataSourceInit) {

        MultiRouteDataSource multiDataSource = new MultiRouteDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();

        Map<String,String> urlMap = dataSourceProperty.getConfig();
        Set<String> dbIndex = urlMap.keySet();
        dbIndex.forEach(index->{
            DataSourceBean dataSourceBean = (DataSourceBean)applicationContext.getBean(DataSourceConstant.BEAN_NAME_PREFIX + index);
            targetDataSources.put(DataSourceConstant.BEAN_NAME_PREFIX + index,dataSourceBean);
        });
        multiDataSource.setTargetDataSources(targetDataSources);
//        默认数据源
//        multiDataSource.setDefaultTargetDataSource(ds133Master());
        return multiDataSource;
    }


    @Bean
    public DataSourceContext dataSourceContext(){
        return new DataSourceContext();
    }

}```

## 其他
使用到的其他一些对象

```java
@Slf4j
public class MultiRouteDataSource extends AbstractRoutingDataSource {

    @Autowired
    DataSourceContext dataSourceContext;
    @Override
    protected Object determineCurrentLookupKey() {
        //通过绑定线程的数据源上下文实现多数据源的动态切换,有兴趣的可以去查阅资料或源码
        String ds = dataSourceContext.getDataSource();
        log.info(" 获取数据源:" + ds);
        return ds;
    }

}```

```java
public class DataSourceConstant {

    public static final String BEAN_NAME_PREFIX = "ds-";
}```

```java
/**
 * 数据源上下文
 */
@Slf4j
public class DataSourceContext {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    /**
     * beanId
     * @param dsBeanName
     */
    public void setDataSource(String dsBeanName) {
//        放入数据源bean
        log.info("切换数据源:"+dsBeanName);
        contextHolder.set(dsBeanName);
    }



    public String getDataSource() {
        return contextHolder.get();
    }

    public void clearDataSource() {
        log.info("释放数据源:");
        contextHolder.remove();
    }
}```

```java
/**
 * @ClassName DataSourceConstant
 * @Description 常量
 * @Author zhangx
 * @Date 2021/4/16 9:34
 **/
public class DataSourceConstant {

    public static final String BEAN_NAME_PREFIX = "ds-";
}

测试

试试构建bean和切换数据源是否好使

/**
 * @ClassName TestController
 * @Description TODO
 * @Author zhangx
 * @Date 2021/4/16 9:09
 **/
@RestController
@RequestMapping("test")
public class TestController {
    @Autowired
    private ApplicationContext context;

    @Autowired
    private IDeviceDataService deviceDataService;

    @Autowired
    private DataSourceContext dataSourceContext;


    @GetMapping("bean/{index}")
    public String getBean(@PathVariable("index") String index){
    	//这里实验bean是够构建成功
        DataSourceBean dataSourceBean = (DataSourceBean)context.getBean(DataSourceConstant.BEAN_NAME_PREFIX + index);

        return dataSourceBean.getUrl();
    }

    @GetMapping("data/{index}")
    public BaseUser getData(@PathVariable("index") String index){
    	//手动切换数据源做个简单查询
        dataSourceContext.setDataSource(DataSourceConstant.BEAN_NAME_PREFIX + index);

        return deviceDataService.getUser();
    }
}

在service方法中切换测试

/**
 1. @ClassName DeviceDataServiceImpl
 2. @Description 测试
 3. @Author zhangx
 4. @Date 2021/4/15 17:46
 **/
@Slf4j
@Service
public class DeviceDataServiceImpl implements IDeviceDataService {


    @Autowired
    private BaseUserDao userDao;

    @Autowired
    private DataSourceContext dataSourceContext;

    @Override
    public void storage(String data) {
        System.out.println(Thread.currentThread().getId());
        dataSourceContext.setDataSource(DataSourceConstant.BEAN_NAME_PREFIX + "1");
        BaseUser user = userDao.selectById(1);
        log.info(JSON.toJSONString(user));
        dataSourceContext.setDataSource(DataSourceConstant.BEAN_NAME_PREFIX + "2");
        user = userDao.selectById(1);
        log.info(JSON.toJSONString(user));
    }

    @Override
    public BaseUser getUser() {
        BaseUser user = userDao.selectById(1);
        log.info(JSON.toJSONString(user));
        return user;
    }
}
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class ReceiverApp {

    public static void main(String[] args) {
        SpringApplication.run(ReceiverApp.class,args);
    }
}

总结

  1. 这只是将数据库分片,到底数据源和业务数据具体如何划分,要看实际
  2. 没有解决跨片数据操作问题,注意spring的事务在跨片操作中无效
  3. 启动程序时候可以通过传入参数控制本java进程到底处理哪些分片,结合脚本效果更佳

可优化的点

结合脚本启动命令传入参数控制程序分片(需要重启)
结合配置中心比如zk做到程序自动更换分片信息(不需要重启)
跨片操作与事务

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值