需求描述
之前在项目中使用的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);
}
}
总结
- 这只是将数据库分片,到底数据源和业务数据具体如何划分,要看实际
- 没有解决跨片数据操作问题,注意spring的事务在跨片操作中无效
- 启动程序时候可以通过传入参数控制本java进程到底处理哪些分片,结合脚本效果更佳
可优化的点
结合脚本启动命令传入参数控制程序分片(需要重启)
结合配置中心比如zk做到程序自动更换分片信息(不需要重启)
跨片操作与事务