互联网项目数据量大,很多业务模块使用统一数据库,会导致DB 压力过大,不足以支撑服务要求,于是现如今分布式项目下大都使用多个数据库,项目中的多数据源切换成为了必要的技术支持.
https://github.com/MinJW/dynamic-db-demo:
本文中使用的所有代码 都在项目中有 包括压力测试和sql提供
JDK1.8
Spring-boot 版本:1.5.9.RELEASE
MYSQL57
全部依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
一共四个库 分四个模块
每个数据库都是一张表 一条数据 id都是1 单name为模块名 用来测试数据源切换是否成功
配置文件
# 用户模块
com.mjw.datasource.user.url=jdbc:mysql://localhost:3306/mjw_user
com.mjw.datasource.user.driver-class-name=com.mysql.jdbc.Driver
com.mjw.datasource.user.username=root
com.mjw.datasource.user.password=root
# 钱包模块
com.mjw.datasource.wallet.url=jdbc:mysql://localhost:3306/mjw_wallet
com.mjw.datasource.wallet.driver-class-name=com.mysql.jdbc.Driver
com.mjw.datasource.wallet.username=root
com.mjw.datasource.wallet.password=root
# 设备模块
com.mjw.datasource.device.url=jdbc:mysql://localhost:3306/mjw_device
com.mjw.datasource.device.driver-class-name=com.mysql.jdbc.Driver
com.mjw.datasource.device.username=root
com.mjw.datasource.device.password=root
# 订单模块
com.mjw.datasource.order.url=jdbc:mysql://localhost:3306/mjw_order
com.mjw.datasource.order.driver-class-name=com.mysql.jdbc.Driver
com.mjw.datasource.order.username=root
com.mjw.datasource.order.password=root
我使用的方式大致讲 是在service层加注解标识使用哪个数据源,然后service层会使用aop 获取注解上的数据源标识,从而切换到响应数据源上.
@Configuration
public class DataSourceConfigurer {
private Logger logger = LoggerFactory.getLogger(DataSourceConfigurer.class);
@Bean(name = "user")
@Primary
@ConfigurationProperties(prefix = "com.mjw.datasource.user")
public DataSource userDataSource(){
logger.debug("userDataSource init ...");
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "wallet")
@ConfigurationProperties(prefix = "com.mjw.datasource.wallet")
public DataSource walletDataSource(){
logger.debug("walletDataSource init ...");
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "device")
@ConfigurationProperties(prefix = "com.mjw.datasource.device")
public DataSource deviceDataSource(){
logger.debug("deviceDataSource init ...");
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "order")
@ConfigurationProperties(prefix = "com.mjw.datasource.order")
public DataSource orderDataSource(){
logger.debug("orderDataSource init ...");
return DruidDataSourceBuilder.create().build();
}
/**
* @Title
* @Description 动态数据初始化
* @param
* @return javax.sql.DataSource
* @throw
* @author MinJunWen
* @date 2018/11/17 16:25
*/
@Bean
public DataSource dynamicDataSource(){
MyDynamicDataSource myDynamicDataSource = new MyDynamicDataSource();
Map<Object,Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("user",userDataSource());
dataSourceMap.put("wallet",walletDataSource());
dataSourceMap.put("device",deviceDataSource());
dataSourceMap.put("order",orderDataSource());
//设置默认的dataSource
myDynamicDataSource.setDefaultTargetDataSource(userDataSource());
//设置数据源map
myDynamicDataSource.setTargetDataSources(dataSourceMap);
return myDynamicDataSource;
}
@Bean
@ConfigurationProperties(prefix = "mybatis")
public SqlSessionFactoryBean sqlSessionFactoryBean() {
logger.debug("sqlSessionFactoryBean init ...");
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
return sqlSessionFactoryBean;
}
@Bean
public PlatformTransactionManager transactionManager() {
logger.debug("transactionManager init ...");
return new DataSourceTransactionManager(dynamicDataSource());
}
}
注意: 一定要有一个默认数据源 如果未设置数据源或者设置的数据源找不到会使用默认数据源,我这里默认是userDataSource
public class MyDynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbLookupKeyContextHolder.getDataSourceKey();
}
}
切换数据源核心就是 继承 AbstractRoutingDataSource 类重写determineCurrentLookupKey 方法,返回dataSource Bean标识.
public class DbLookupKeyContextHolder {
/**
* @Description 存放当前线程线程 选择数据链接 key
* @author MinJunWen
* @date 2018/11/23 14:42
*/
private static final ThreadLocal<String> CURRENT_LOOKUP_KEY = new ThreadLocal<>();
/**
* @Description 设置当前选择数据库的key
* @author MinJunWen
* @date 2018/11/23 14:42
*/
public static void setDataSourceKey(String lookupKey){
CURRENT_LOOKUP_KEY.set(lookupKey);
}
/**
* @Description 获取当前选择数据库的key
* @author MinJunWen
* @date 2018/11/23 15:34
*/
public static String getDataSourceKey(){
return CURRENT_LOOKUP_KEY.get();
}
}
每个线程中存储使用的数据源标识,会在aop且面中设置数据源
@Aspect
@Component
public class ChangeDataSourceAop {
private Logger logger = LoggerFactory.getLogger(ChangeDataSourceAop.class);
/**
* @Description 切面 针对所有service
* @author MinJunWen
* @date 2018/11/23 15:23
*/
@Pointcut("execution( * com.mjw.business.*.service.*.*(..))")
public void serviceCut(){};
@Before("serviceCut()")
public void changeDataSource(JoinPoint point) {
String value = point.getTarget().getClass().getAnnotation(MjwDb.class).value().getValue();
if(!StringUtils.isNullOrEmpty(value)){
logger.info("switch db lookupkey ==>" + value);
DbLookupKeyContextHolder.setDataSourceKey(value);
}
}
}
我这里切的是所有service调用
从service实例上获取我自定义的注解标识 来获取对应的数据源 设置到当前线程中即可
注意: 我是从service层切面来设置数据源 所以在一个service 要获取另一个库的数据 不能直接注入dao来调用 而是应该注入service来使用.否则将无法正确切换数据源,开发时要注意代码规范.
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface MjwDb {
MjwDbEnum value();
}
注解的value 强制使用枚举
public enum MjwDbEnum {
DEVICE("device"),ORDER("order"),USER("user"),WALLET("wallet");
private String value;
MjwDbEnum(String value){
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
具体的controller service dao 就不方这里了 有源码提供自己可以测试下
每个模块提供一个根据id查询的接口
/user 使用的userService 是有user数据源标识的 根据我们的数据 只有user数据库才会有返沪user 切换时正确得
根据我aop中打印的日志 也是没问题的
2019-11-09 14:58:41.934 INFO 500356 --- [nio-8888-exec-2] com.mjw.common.aop.ChangeDataSourceAop : switch db lookupkey ==>user
其实它是存储在当前请求的线程变量中的,不用担心会多个请求中使用的数据源混乱问题
这里是一个简单的js请求 模仿下来测试
<script type="text/javascript">
var log = console.log.bind(console)
var fields = ['device','order','user','wallet']
var intv = setInterval("func()","1");
var func = function(){
for(var i = 0; i < 1000; i++){
parseQuest(fields[i % 4])
//log(fields[i % 4])
}
}
$(function(){
$(document).keydown(function(event){
if(event.keyCode == 32){
clearInterval(intv)
}
});
})
var parseQuest = function(field){
$.ajax({
url:'http://localhost:8888/'+field,
data:{'id':1},
type:"post",
dataType:"json",
success:function(data){
if(field == data.name){
//log('success==>' + data.name)
}else{
log(field+'=>success**********'+data.name)
}
},
fail:function(data){
log(field+'=>error***************'+data)
}
})
}
</script>
每秒跑1000个请求 每个数据源有250个请求 ,无任何压力 运行正常
源码地址:https://github.com/MinJW/dynamic-db-demo
测试的js代码在 resources/static/job中 直接在文件夹中打开就行 不用跑起项目访问