1.背景 對於資料量在1千萬,單個mysql資料庫就可以支援,但是如果資料量大於這個數的時候,例如1億,那麼查詢的效能就會很低。此時需要對資料庫做水平切分,常見的做法是按照使用者的帳號進行hash,然後選擇對應的資料庫。 水平切分圖,資料落入不同的庫中
2.實現
2.1示意圖
先來看下大致示意圖:
- 圖1是比較常見的情況,單個資料庫
- 圖2展示了web應用和資料庫之間的一個中介層,這個中介層去選擇使用哪個資料庫。 2.2資料庫配置 首先我們需要配置多個資料來源,我是用xml進行配置的其他方法大同小異,就是多建立了幾個bean。
<bean id="parentDataSource" abstract="true" class="org.apache.tomcat.jdbc.pool.DataSource" destroy-method="close" p:maxWait="10000" p:removeAbandoned="true" p:removeAbandonedTimeout="180" p:connectionProperties="clientEncoding=UTF-8" p:validationQuery="SELECT 1" p:validationInterval="30000" p:testOnBorrow="false" p:testOnReturn="false" p:testWhileIdle="true" p:timeBetweenEvictionRunsMillis="10000" p:minEvictableIdleTimeMillis="60000" p:logAbandoned="false" p:defaultAutoCommit="true" /> <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;
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="dataSourceSwitcher" class="com.netease.mail.activity.service.switcher.DataSourceSwitcher"> <property name="targetDataSources"> <map> <entry key="ds1" value-ref="childDataSource1"/> <entry key="ds2" value-ref="childDataSource2"/> </map> </property> <property name="defaultTargetDataSource" ref="childDataSource1"/> </bean> <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為枚舉類型,如下
@Getterpublic 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() { }
流程圖如下所示
切點處理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"); }}