代碼中建議在service(業務)層進行主從分離。同一個service方法內部不建議再進行主從分離。這裏是事務切面層,我們知道,在同一個事務中會使用同一條鏈接進行處理,在業務層方法內部邏輯不再建議進行主從分離,避免數據不一致問題的出現。
以下方案通過 繼承 AbstractRoutingDataSource類+註解+aop+ThreadLocal 實現註解方式的數據源的動態切換
1、讀數據庫切換註解類,在service註解表示開啓讀庫操作,自動方法完成後自動清空數據源數據
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
@Inherited
public @interface SlaveDataSource {
}
2、spring aop切面類
@Aspect
public class DataSourceAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(DataSourceAspect.class);
/**
* 寫庫對應的數據源key 默認此庫
*/
public static final String MASTER = "master";
/**
* 讀庫對應的數據源key
*/
public static final String SLAVE = "slave";
/**
* 在service層方法獲取datasource對象之前,在切面中指定當前線程數據源slave
*/
@Before(value = "execution(* *(..)) && @annotation(SlaveDataSource)")
public void before(JoinPoint point) {
String clazz = point.getTarget().getClass().getName();
String name = point.getSignature().getName();
LOGGER.debug(clazz+"."+name+" --- datasource---> :"+SLAVE);
DataSourceHolder.putDataSource(SLAVE);
}
@After(value = "execution(* *(..)) && @annotation(SlaveDataSource)")
public void after(JoinPoint joinPoint) {
DataSourceHolder.clearDataSourceType();
}
注意,需要開啓springAop配置,同時將對象反向注入
<!-- aop註解支持 使用aspectj代理 不使用java動態代理-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
<!--動態數據源-->
<bean id="dataSourceAspect" class="com.konka.utils.database.DataSourceAspect"/>
3、動態數據源類,繼承 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 獲取與數據源相關的key
* 此key是Map<String,DataSource> resolvedDataSources 中與數據源綁定的key值
* 在通過determineTargetDataSource獲取目標數據源時使用
*/
@Override
protected Object determineCurrentLookupKey() {
return DataSourceHolder.getDataSource();
}
}
4、數據源處理類
public class DataSourceHolder {
public static final ThreadLocal<String> HOLDER = new ThreadLocal<String>();
/**
* 綁定當前線程數據源
*/
public static void putDataSource(String datasource) {
HOLDER.set(datasource);
}
/**
* 獲取當前線程的數據源
*
* @return
*/
public static String getDataSource() {
return HOLDER.get();
}
/**
* 移除數據源
*/
public static void clearDataSourceType() {
HOLDER.remove();
}
}
springMybatis.xml的配置,注:使用的爲阿里的druid數據庫連接池,如果使用dbcp請自行切換
<!--阿里監控配置-->
<bean id="stat-filter" class="com.alibaba.druid.filter.stat.StatFilter">
<property name="slowSqlMillis" value="10000"/>
<property name="logSlowSql" value="true"/>
<property name="mergeSql" value="true"/>
</bean>
<!-- 配置主數據源 -->
<bean name="masterDataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="driverClassName" value="${master_jdbc_driverClassName}"/>
<property name="url" value="${master_jdbc_url}"/>
<property name="username" value="${master_jdbc_username}"/>
<property name="password" value="${master_jdbc_password}"/>
<property name="validationQuery" value="${master_validationQuery}"/>
<!--連接池啓動時創建的初始化連接數量(默認值爲0)-->
<property name="initialSize" value="${master_sql_initial_size}"/>
<!--連接池中最小的空閒的連接數,低於這個數量會被創建新的連接
(默認爲0,調整爲5,該參數越接近maxIdle,性能越好,
因爲連接的創建和銷燬,都是需要消耗資源的;但是不能太大,
因爲在機器很空閒的時候,也會創建低於minidle個數的連接,
類似於jvm參數中的Xmn設置)-->
<property name="minIdle" value="${master_sql_min_idle}"/>
<!--連接池中可同時連接的最大的連接數
(默認值爲8,調整爲20,高峯單機器在20併發左右,自己根據應用場景定)-->
<property name="maxActive" value="${master_sql_max_active}"/>
<!--最大等待時間,當沒有可用連接時,連接池等待連接釋放的最大時間,
超過該時間限制會拋出異常,如果設置-1表示無限等待
(默認爲無限,調整爲60000ms,避免因線程池不夠用,而導致請求被無限制掛起)-->
<property name="maxWait" value="30000"/>
<!--起了一個Evict的TimerTask定時線程進行控制
(可通過設置參數timeBetweenEvictionRunsMillis>0),
定時對線程池中的鏈接進行validateObject校驗,
對無效的鏈接進行關閉後,會調用ensureMinIdle,
適當建立鏈接保證最小的minIdle連接數。-->
<property name="testWhileIdle" value="true"/>
<!--借出連接時不要測試,否則很影響性能-->
<property name="testOnBorrow" value="false"/>
<!--返回連接時不要測試,否則很影響性能-->
<property name="testOnReturn" value="false"/>
<!--每30秒運行一次空閒連接回收器-->
<property name="timeBetweenEvictionRunsMillis" value="30000"/>
<!--池中的連接空閒30分鐘後被回收 -->
<property name="minEvictableIdleTimeMillis" value="210000"/>
<!--超過removeAbandonedTimeout時間後,是否進行沒用連接(廢棄)的回收
(默認爲false,調整爲true) 當可用連接數少於3個時才執行-->
<property name="removeAbandoned" value="true"/>
<!--連接泄漏回收參數,180秒,泄露的連接可以被刪除的超時值-->
<property name="removeAbandonedTimeout" value="10"/>
<!--標記當連接被回收時是否打印程序的stack traces日誌(默認爲false,未調整)-->
<property name="logAbandoned" value="true"/>
<!--監控-->
<property name="proxyFilters">
<list>
<ref bean="stat-filter"/>
</list>
</property>
<property name="useGlobalDataSourceStat" value="true"/>
</bean>
<!-- 配置從數據源 -->
<bean name="slaveDataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="driverClassName" value="${slave_jdbc_driverClassName}"/>
<property name="url" value="${slave_jdbc_url}"/>
<property name="username" value="${slave_jdbc_username}"/>
<property name="password" value="${slave_jdbc_password}"/>
<property name="validationQuery" value="${slave_validationQuery}"/>
<!--連接池啓動時創建的初始化連接數量(默認值爲0)-->
<property name="initialSize" value="${slave_sql_initial_size}"/>
<!--連接池中最小的空閒的連接數,低於這個數量會被創建新的連接
(默認爲0,調整爲5,該參數越接近maxIdle,性能越好,
因爲連接的創建和銷燬,都是需要消耗資源的;但是不能太大,
因爲在機器很空閒的時候,也會創建低於minidle個數的連接,
類似於jvm參數中的Xmn設置)-->
<property name="minIdle" value="${slave_sql_min_idle}"/>
<!--連接池中可同時連接的最大的連接數
(默認值爲8,調整爲20,高峯單機器在20併發左右,自己根據應用場景定)-->
<property name="maxActive" value="${slave_sql_max_active}"/>
<!--最大等待時間,當沒有可用連接時,連接池等待連接釋放的最大時間,
超過該時間限制會拋出異常,如果設置-1表示無限等待
(默認爲無限,調整爲60000ms,避免因線程池不夠用,而導致請求被無限制掛起)-->
<property name="maxWait" value="30000"/>
<!--起了一個Evict的TimerTask定時線程進行控制
(可通過設置參數timeBetweenEvictionRunsMillis>0),
定時對線程池中的鏈接進行validateObject校驗,
對無效的鏈接進行關閉後,會調用ensureMinIdle,
適當建立鏈接保證最小的minIdle連接數。-->
<property name="testWhileIdle" value="true"/>
<!--借出連接時不要測試,否則很影響性能-->
<property name="testOnBorrow" value="false"/>
<!--返回連接時不要測試,否則很影響性能-->
<property name="testOnReturn" value="false"/>
<!--每30秒運行一次空閒連接回收器-->
<property name="timeBetweenEvictionRunsMillis" value="30000"/>
<!--池中的連接空閒30分鐘後被回收 -->
<property name="minEvictableIdleTimeMillis" value="210000"/>
<!--超過removeAbandonedTimeout時間後,是否進行沒用連接(廢棄)的回收
(默認爲false,調整爲true) 當可用連接數少於3個時才執行-->
<property name="removeAbandoned" value="true"/>
<!--連接泄漏回收參數,180秒,泄露的連接可以被刪除的超時值-->
<property name="removeAbandonedTimeout" value="10"/>
<!--標記當連接被回收時是否打印程序的stack traces日誌(默認爲false,未調整)-->
<property name="logAbandoned" value="true"/>
<!--監控-->
<property name="proxyFilters">
<list>
<ref bean="stat-filter"/>
</list>
</property>
<property name="useGlobalDataSourceStat" value="true"/>
</bean>
<!-- 動態數據源,根據service接口上的註解來決定取哪個數據源 -->
<bean id="dataSource" class="com.konka.utils.database.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<!-- read or slave -->
<entry key="slave" value-ref="slaveDataSource"/>
<!-- write or master -->
<entry key="master" value-ref="masterDataSource"/>
</map>
</property>
<property name="defaultTargetDataSource" ref="masterDataSource"/>
</bean>
使用方式:對service方法上添加註解切換數據源,默認爲主庫
@Service
public class PingServiceImpl implements PingService {
private final BasicConfigDao basicConfigDao;
@Autowired
public PingServiceImpl(BasicConfigDao basicConfigDao) {
this.basicConfigDao = basicConfigDao;
}
@Override
public void pingMaster() {
basicConfigDao.ping();
}
@SlaveDataSource
@Override
public void pingSalve() {
basicConfigDao.ping();
}
}