1.背景
对于数据量在1千万,单个mysql数据库就可以支持,但是如果数据量大于这个数的时候,例如1亿,那么查询的性能就会很低。此时需要对数据库做水平切分,常见的做法是按照用户的账号进行hash,然后选择对应的数据库。
水平切分图,数据落入不同的库中
2.实现
2.1示意图
先来看下大致示意图:
图1是比较常见的情况,单个数据库
图2展示了web应用和数据库之间的一个中间层,这个中间层去选择使用哪个数据库。
2.2数据库配置
首先我们需要配置多个数据源,我是用xml进行配置的其他方法大同小异,就是多建立了几个bean。
<bean id="dataSource" parent="parentDataSource"
p:driverClassName="com.mysql.jdbc.Driver"
p:username="${jdbc.user}"
p:password="${jdbc.password}"
p:initialSize="20"
p:maxActive="200"
p:maxIdle="200"
p:minIdle="5"/>
<bean id="childDataSource1" parent="dataSource">
<property name="url" value="${jdbc.url1}" />
</bean>
<bean id="childDataSource2" parent="dataSource">
<property name="url" value="${jdbc.url2}" />
</bean>
这里我建了两个数据源bean的id分别为childDataSource1,childDataSource2,为了方便起见,这里只有数据库的url不同,故都继承了dataSource。
2.3 java实现
先把定义的多个数据库bean放一放,先来看下spring中对动态选择数据源的支持。
在spring中有一个抽象类AbstractRoutingDataSource类,通过这个类可以实现动态选择数据源。来看下这个类的成员变量
private Map<Object, Object> targetDataSources;
private Object defaultTargetDataSource;
private Map<Object, DataSource> resolvedDataSources;
1
2
3
targetDataSources中保存了key和数据库连接的映射关系,defaultTargetDataSource表示默认的链接,resolvedDataSources这个数据结构是通过targetDataSources构建而来,存储的结构也是数据库标识和数据源的映射关系。
下面需要继承AbstractRoutingDataSource类,实现我们自己的数据库选择逻辑DataSourceSwitcher类,先上代码:
public class DataSourceSwitcher extends AbstractRoutingDataSource{
private static final Logger LOGGER = LoggerFactory.getLogger("INTERACTIVE_LOGGER");
private static final ThreadLocal<String> dataSourceKey = new ThreadLocal<String>();
public static void clearDataSourceType() {
LOGGER.debug("thread:{},remove,dataSource:{}",Thread.currentThread().getName());
dataSourceKey.remove();
}
@Override
protected Object determineCurrentLookupKey() {
String s = dataSourceKey.get();
LOGGER.debug("thread:{},determine,dataSource:{}",Thread.currentThread().getName(),s);
return s;
}
public static void setDataSourceKey(String dataSource) {
LOGGER.debug("thread:{},set,dataSource:{}",Thread.currentThread().getName(),dataSource);
dataSourceKey.set(dataSource);
}
}
第5行,threadLocal的成员变量dataSource(由于不同的请求所需要的数据源可能不一样),用于存储数据源标识。
第8行,清除数据源操作.
第14行,该方法决定了需要使用哪个数据库,该方法是抽象方法,必须由我们实现,那么现在来看下这个方法是如何使用的
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, “DataSource router not initialized”);
Object lookupKey = determineCurrentLookupKey();//这里获取数据库标识
DataSource dataSource = this.resolvedDataSources.get(lookupKey);//获得具体的数据源
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException(“Cannot determine target DataSource for lookup key [” + lookupKey + “]”);
}
return dataSource;
}
所以我们需要在determineCurrentLookupKey方法中返回数据库标识即可
第20行,设置数据源方法。
2.4 数据库配置和DataSourceSwitcher类结合
现在把我们之前的数据库配置和DataSourceSwitcher进行合并,我在数据库的xml配置上添加如下配置:
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSourceSwitcher" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="configLocation" value="classpath:mybatis/sql-map-config.xml" />
<property name="mapperLocations" value="classpath:mybatis/mapper/*.xml"/>
<property name="dataSource" ref="dataSourceSwitcher" />
</bean>
可以看到,我对targetDataSources进行了初始化,ds1对应了数据源childDataSource1;ds2对应了数据源childDataSource2。
使用的话只要调用DataSourceSwitcher.setDataSourceKey(“ds1”),即将数据源切换到了childDataSource1。
2.5 增加切面处理
如果每次执行方法都要设置一下数据源实在是件很麻烦的事情,另外我们需要对某个key进行hash后选择数据库,这块也没有实现。现在借助spring切面的功能,可以解决这两个问题。
大致思路如下:
2.5.1 自定义注解
定义UseDataSource注解
/**
-
数据源注解
-
Created by hzlaojiaqi on 2017/12/26.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseDataSource {/**
- 数据源
- @return
*/
DataSourceType value() default DataSourceType.SOURCE_1;
/**
- 是否使用hashkey,若为true,则使用对应字段的哈希值进行计算,选择数据源,
- 且指定的{@link DataSourceType}不起作用
- @return
*/
boolean useHashKey() default false;
}
DataSourceType为枚举类型,如下
@Getter
public enum DataSourceType {
SOURCE_1(“ds1”, “数据源1-默认数据源”),
SOURCE_2(“ds2”, “数据源2”);
DataSourceType(String source, String desc) {
this.source = source;
this.desc = desc;
}
String source;
String desc;
/*
* @param hashKey
* @return
*/
public static String getByKey(String hashKey){
//根据hashkey来获取所需要的数据源
int i = Math.abs(hashKey.hashCode()) % DataSourceType.values().length;
return DataSourceType.values()[i].getSource();
}
}
useHashKey是否使用hashkey。
定义DSKey注解,该注解用于标注在对应的方法变量上,表示对该变量的值进行hash。
/**
*
-
数据源选择 注解
-
用在参数上,表示使用对应字段的hashcode来选择数据库
-
Created by hzlaojiaqi on 2017/12/26.
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface DSKey {String value() default “”;
}
2.5.2 aop进行拦截
由于只需要对UseDataSource这个注解进行拦截,因此切点可以设置如下
@Pointcut("@annotation(com.netease.mail.activity.aop.annotation.UseDataSource)")
public void useDataSource() {
}
1
2
3
流程图如下所示
切点处理Around方法如下:
/**
* @param joinPoint
* @return
* @throws Throwable
*/
@Around(“useDataSource() && @annotation(anno)”)
public Object dataSourceSwitcher(ProceedingJoinPoint joinPoint, UseDataSource anno) throws Throwable {
String ds="";
//若使用hashkey,则根据hashkey进行选择数据源
if(anno.useHashKey()){
ds=DataSourceType.getByKey(getHashKeyFromMethod(joinPoint));
}else{
//直接获取数据源
DataSourceType value = anno.value();
ds=value.getSource();
}
//设置数据源
DataSourceSwitcher.setDataSourceKey(ds);
try {
//执行方法
Object result = joinPoint.proceed();
return result;
}catch (Exception e){
throw e;
}finally {
//切换回原来的数据源(重要)
DataSourceSwitcher.setDataSourceKey(DataSourceType.SOURCE_1.getSource());
}
}
getHashKeyFromMethod方法获取了用@DSKey标注的变量的值,实现如下:
/**
* @param joinPoint
* @return
*/
public String getHashKeyFromMethod(ProceedingJoinPoint joinPoint){
MethodSignature signature=MethodSignature.class.cast(joinPoint.getSignature());
Method method = signature.getMethod();
Object[] args = joinPoint.getArgs();
Parameter[] declaredFields = method.getParameters();
int index=0;
for(Parameter temp:declaredFields){
Annotation[] annotations = temp.getAnnotations();
for(Annotation anTemp:annotations){
if(anTemp instanceof DSKey){
return String.valueOf(args[index]);
}
}
index++;
}
throw new RuntimeException("can not get field with @DsKey annotation");
}
3.使用
我们看下实际的效果,定义两个controller, insert用的是固定的Dasrouce.SOURCE_1,insert2用的是uid的hash值进行数据源的选择。
@RequestMapping(value = “/ajax/insert.do”,method = RequestMethod.GET)
@ResponseBody
@UseDataSource(DataSourceType.SOURCE_1)
public AjaxResult insert(@RequestParam String uid, HttpServletRequest httpServletRequest){
WebCouponWinner webCouponWinner=new WebCouponWinner();
webCouponWinner.setUid(uid);
webCouponWinner.setInsertTime(TimeUtil.now());
webCouponWinnerDao.insert(webCouponWinner);
return new AjaxResult(RetCode.SUCCESS);
}
@RequestMapping(value = "/ajax/insert2.do",method = RequestMethod.GET)
@ResponseBody
@UseDataSource(useHashKey = true)
public AjaxResult insert2(@RequestParam @DSKey String uid, HttpServletRequest httpServletRequest){
WebCouponWinner webCouponWinner=new WebCouponWinner();
webCouponWinner.setUid(uid);
webCouponWinner.setInsertTime(TimeUtil.now());
webCouponWinnerDao.insert(webCouponWinner);
return new AjaxResult(RetCode.SUCCESS);
}
执行/ajax/insert.do,uid分别传1和2,得到如下结果
两条记录都在同一个数据库中
再执行/ajax/insert2.do,uid分别传1和2,得到如下结果
以及
两条记录在不同的库中,符合预期。
完整的DataSourceAsp,需要注意下该切面必须要在事务注解@Transactional之前,由于在开始事务之前就需要确定数据源,所以设置DataSourceAsp的**@Order(Ordered.LOWEST_PRECEDENCE-1),@Transactional的order是最小值**
package com.netease.mail.activity.aop;
import com.netease.mail.activity.aop.annotation.DSKey;
import com.netease.mail.activity.aop.annotation.UseDataSource;
import com.netease.mail.activity.aop.type.DataSourceType;
import com.netease.mail.activity.exception.custom.BizException;
import com.netease.mail.activity.service.complex.MonitorService;
import com.netease.mail.activity.service.switcher.DataSourceSwitcher;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
/**
*
-
数据源切换
-
Created by hzlaojiaqi on 2017/12/26.
*/
@Component
@Aspect
@Slf4j(topic = “THIRDPARTY_LOGGER”)
@Order(Ordered.LOWEST_PRECEDENCE-1)
public class DataSourceAsp {@Autowired
MonitorService mMonitor;/**
- 针对所有的Mapped
*/
@Pointcut("@annotation(com.netease.mail.activity.aop.annotation.UseDataSource)")
public void useDataSource() {
}
/**
- @param joinPoint
- @return
- @throws Throwable
*/
@Around(“useDataSource() && @annotation(anno)”)
public Object dataSourceSwitcher(ProceedingJoinPoint joinPoint, UseDataSource anno) throws Throwable {
String ds="";
if(anno.useHashKey()){
ds=DataSourceType.getByKey(getHashKeyFromMethod(joinPoint));
}else{
DataSourceType value = anno.value();
ds=value.getSource();
}
DataSourceSwitcher.setDataSourceKey(ds);
try {
Object result = joinPoint.proceed();
return result;
}catch (Exception e){
throw e;
}finally {
DataSourceSwitcher.setDataSourceKey(DataSourceType.SOURCE_1.getSource());
}
}
/**
- @param joinPoint
- @return
*/
public String getHashKeyFromMethod(ProceedingJoinPoint joinPoint){
MethodSignature signature=MethodSignature.class.cast(joinPoint.getSignature());
Method method = signature.getMethod();
Object[] args = joinPoint.getArgs();
Parameter[] declaredFields = method.getParameters();
int index=0;
for(Parameter temp:declaredFields){
Annotation[] annotations = temp.getAnnotations();
for(Annotation anTemp:annotations){
if(anTemp instanceof DSKey){
return String.valueOf(args[index]);
}
}
index++;
}
throw new BizException(“can not get field with @DsKey annotation”);
}
- 针对所有的Mapped
}