隨著業(yè)務的不斷擴張,應用壓力逐漸增大,特別是數(shù)據(jù)庫。不論從讀寫分離還是分庫的方法來提高應用的性能,都需要涉及到多數(shù)據(jù)源問題。本文主要介紹在Spring MVC+Mybatis下的多數(shù)據(jù)源配置。主要通過Spring提供的AbstractRoutingDataSource來實現(xiàn)多數(shù)據(jù)源。
1. 繼承AbstractRoutingDataSource
AbstractRoutingDataSource 是spring提供的一個多數(shù)據(jù)源抽象類。spring會在使用事務的地方來調(diào)用此類的determineCurrentLookupKey()
方法來獲取數(shù)據(jù)源的key值。我們繼承此抽象類并實現(xiàn)此方法:
package com.ctitc.collect.manage.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
*
* @author zongbo
* 實現(xiàn)spring多路由配置,由spring調(diào)用
*/
public class DataSourceRouter extends AbstractRoutingDataSource {
// 獲取數(shù)據(jù)源名稱
protected Object determineCurrentLookupKey() {
return HandleDataSource.getDataSource();
}
}
2. 線程內(nèi)部數(shù)據(jù)源處理類
DataSourceRouter
類中通過HandleDataSource.getDataSource()
獲取數(shù)據(jù)源的key值。此方法應該和線程綁定。
package com.ctitc.collect.manage.datasource;
/**
* 線程相關的數(shù)據(jù)源處理類
* @author zongbo
*
*/
public class HandleDataSource {
// 數(shù)據(jù)源名稱線程池
private static final ThreadLocal<String> holder = new ThreadLocal<String>();
/**
* 設置數(shù)據(jù)源
* @param datasource 數(shù)據(jù)源名稱
*/
public static void setDataSource(String datasource) {
holder.set(datasource);
}
/**
* 獲取數(shù)據(jù)源
* @return 數(shù)據(jù)源名稱
*/
public static String getDataSource() {
return holder.get();
}
/**
* 清空數(shù)據(jù)源
*/
public static void clearDataSource() {
holder.remove();
}
}
3. 自定義數(shù)據(jù)源注解類
對于spring來說,注解即簡單方便且可讀性也高。所以,我們也通過注解在service的方法前指定所用的數(shù)據(jù)源。我們先定義自己的注解類,其中value為數(shù)據(jù)源的key值。
package com.ctitc.collect.manage.datasource;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 數(shù)據(jù)源注解類
* @author zongbo
*
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
String value();
}
4. AOP 攔截service并切換數(shù)據(jù)源
指定注解以后,我們可以通過AOP攔截所有service方法,在方法執(zhí)行之前獲取方法上的注解:即數(shù)據(jù)源的key值。
package com.ctitc.collect.manage.datasource;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* 切換數(shù)據(jù)源(不同方法調(diào)用不同數(shù)據(jù)源)
*/
@Aspect
@Component
@Order(1) //請注意:這里order一定要小于tx:annotation-driven的order,即先執(zhí)行DataSourceAspect切面,再執(zhí)行事務切面,才能獲取到最終的數(shù)據(jù)源
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DataSourceAspect {
static Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);
/**
* 切入點 service包及子孫包下的所有類
*/
@Pointcut("execution(* com.ctitc.collect.service..*.*(..))")
public void aspect() {
}
/**
* 配置前置通知,使用在方法aspect()上注冊的切入點
*/
@Before("aspect()")
public void before(JoinPoint point) {
Class<?> target = point.getTarget().getClass();
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod() ;
DataSource dataSource = null ;
//從類初始化
dataSource = this.getDataSource(target, method) ;
//從接口初始化
if(dataSource == null){
for (Class<?> clazz : target.getInterfaces()) {
dataSource = getDataSource(clazz, method);
if(dataSource != null){
break ;//從某個接口中一旦發(fā)現(xiàn)注解,不再循環(huán)
}
}
}
if(dataSource != null && !StringUtils.isEmpty(dataSource.value()) ){
HandleDataSource.setDataSource(dataSource.value());
}
}
@After("aspect()")
public void after(JoinPoint point) {
//使用完記得清空
HandleDataSource.setDataSource(null);
}
/**
* 獲取方法或類的注解對象DataSource
* @param target 類class
* @param method 方法
* @return DataSource
*/
public DataSource getDataSource(Class<?> target, Method method){
try {
//1.優(yōu)先方法注解
Class<?>[] types = method.getParameterTypes();
Method m = target.getMethod(method.getName(), types);
if (m != null && m.isAnnotationPresent(DataSource.class)) {
return m.getAnnotation(DataSource.class);
}
//2.其次類注解
if (target.isAnnotationPresent(DataSource.class)) {
return target.getAnnotation(DataSource.class);
}
} catch (Exception e) {
e.printStackTrace();
logger.error(MessageFormat.format("通過注解切換數(shù)據(jù)源時發(fā)生異常[class={0},method={1}]:"
, target.getName(), method.getName()),e) ;
}
return null ;
}
}
5. 數(shù)據(jù)源配置
假設我有兩個庫:業(yè)務庫和訂單庫。先要配置這兩個數(shù)據(jù)源
- 業(yè)務數(shù)據(jù)源
<bean id="busiDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<description>業(yè)務數(shù)據(jù)源</description>
<!-- 數(shù)據(jù)庫基本信息配置 -->
<property name="driverClassName" value="${busi.driverClassName}" />
<property name="url" value="${busi.url}" />
<property name="username" value="${busi.username}" />
<property name="password" value="${busi.password}" />
<!-- 初始化連接數(shù)量 -->
<property name="initialSize" value="${druid.initialSize}" />
<!-- 最大并發(fā)連接數(shù) -->
<property name="maxActive" value="${druid.maxActive}" />
<!-- 最小空閑連接數(shù) -->
<property name="minIdle" value="${druid.minIdle}" />
<!-- 配置獲取連接等待超時的時間 -->
<property name="maxWait" value="${druid.maxWait}" />
<!-- 超過時間限制是否回收 -->
<property name="removeAbandoned" value="${druid.removeAbandoned}" />
<!-- 超過時間限制多長; -->
<property name="removeAbandonedTimeout" value="${druid.removeAbandonedTimeout}" />
<!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="${druid.timeBetweenEvictionRunsMillis}" />
<!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="${druid.minEvictableIdleTimeMillis}" />
<!-- 用來檢測連接是否有效的sql,要求是一個查詢語句-->
<property name="validationQuery" value="${druid.validationQuery}" />
<!-- 申請連接的時候檢測 -->
<property name="testWhileIdle" value="${druid.testWhileIdle}" />
<!-- 申請連接時執(zhí)行validationQuery檢測連接是否有效,配置為true會降低性能 -->
<property name="testOnBorrow" value="${druid.testOnBorrow}" />
<!-- 歸還連接時執(zhí)行validationQuery檢測連接是否有效,配置為true會降低性能 -->
<property name="testOnReturn" value="${druid.testOnReturn}" />
<!-- 打開PSCache,并且指定每個連接上PSCache的大小 -->
<property name="poolPreparedStatements" value="${druid.poolPreparedStatements}" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="${druid.maxPoolPreparedStatementPerConnectionSize}" />
<!--屬性類型是字符串,通過別名的方式配置擴展插件,常用的插件有:
監(jiān)控統(tǒng)計用的filter:stat
日志用的filter:log4j
防御SQL注入的filter:wall -->
<property name="filters" value="${druid.filters}" />
</bean>
- 訂單數(shù)據(jù)源
<bean id="orderDataSource" class="com.alibaba.druid.pool.DruidDataSource" parent="busiDataSource">
<description>訂單數(shù)據(jù)源</description>
<property name="driverClassName" value="${order.driverClassName}" />
<property name="url" value="${order.url}" />
<property name="username" value="${order.username}" />
<property name="password" value="${order.password}" />
</bean>
- dataSource 則是剛剛實現(xiàn)的
DataSourceRouter
,且需要指定此類的targetDataSources
屬性和defaultTargetDataSource
屬性。
targetDataSources
:數(shù)據(jù)源列表,key-value形式,即上面配置的兩個數(shù)據(jù)源
defaultTargetDataSource
:默認數(shù)據(jù)源,如果未指定數(shù)據(jù)源 或者指定的數(shù)據(jù)源不存在的話 默認使用這個數(shù)據(jù)源
<bean id="dataSource" class="com.ctitc.collect.manage.datasource.DataSourceRouter" lazy-init="true">
<description>多數(shù)據(jù)源路由</description>
<property name="targetDataSources">
<map key-type="java.lang.String" value-type="javax.sql.DataSource">
<!-- write -->
<entry key="busi" value-ref="busiDataSource" />
<entry key="order" value-ref="orderDataSource" />
</map>
</property>
<!-- 默認數(shù)據(jù)源,如果未指定數(shù)據(jù)源 或者指定的數(shù)據(jù)源不存在的話 默認使用這個數(shù)據(jù)源 -->
<property name="defaultTargetDataSource" ref="busiDataSource" />
</bean>
6. AOP的順序問題
由于我使用的注解式事務,和我們的AOP數(shù)據(jù)源切面有一個順序的關系。數(shù)據(jù)源切換必須先執(zhí)行,數(shù)據(jù)庫事務才能獲取到正確的數(shù)據(jù)源。所以要明確指定 注解式事務和 我們AOP數(shù)據(jù)源切面的先后順序。
- 我們數(shù)據(jù)源切換的AOP是通過注解來實現(xiàn)的,只需要在AOP類上加上一個
order(1)
注解即可,其中1代表順序號。 - 注解式事務的是通過xml配置啟動
<tx:annotation-driven transaction-manager="transactionManager"
proxy-target-class="true" order="2" />
7. 示例Demo
在每個service方法前使用@DataSource("數(shù)據(jù)源key")
注解即可。
@Override
@DataSource("busi")
@Transactional(readOnly = true, propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public List<BasVersion> test1() {
// TODO Auto-generated method stub
return coreMapper.getVersion();
}
@Override
@DataSource("order")
@Transactional(readOnly = true, propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public List<BasVersion> test2() {
// TODO Auto-generated method stub
return coreMapper.getVersion();
}