SSM整合系列之 配置多資料來源並實現手動切換資料來源可實現讀寫分離
摘要:在之前的開發中有很多場景用到了多資料來源的情況,如在做資料冷熱遷移的時候,將冷資料遷移到另一個庫,查詢冷資料時需要切換資料庫;在和天貓京東等電商對接時,因為有多套系統在同時使用,在客戶授權時,需要根據客戶使用的版本,儲存到對應的資料庫中。基於此,在這裡呢簡單實現一個SSM系統的多資料來源配置,手動切換資料來源。在此基礎上延伸一下,如果兩個資料來源是主從複製的,那麼就可以實現讀寫分離。
1.專案搭建
可以參考本系列文章,部落格地址:https://blog.csdn.net/caiqing116/article/details/84573166
或者直接下載專案,git地址:https://github.com/gitcaiqing/SSM_DEMO.git
2.搭建多個數據庫服務
在這裡呢我特地搭建了2個MySQL服務,埠號分別是3306,3308(方便後續讀寫分離直接使用 微笑),用於資料來源切換演示。當然看官你也可以直接1個MySQL服務建立2個數據庫實現。
搭建多個MySQL服務可參考:https://blog.csdn.net/caiqing116/article/details/84899680
建立資料庫db_ssmdemo
CREATE DATABASE db_ssmdemo CHARACTER SET UTF8;
建立表tb_basic_user
CREATE TABLE `tb_basic_user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵自增', `userId` varchar(32) DEFAULT NULL COMMENT '使用者ID', `utype` int(1) DEFAULT '0' COMMENT '使用者型別 0管理員1普通使用者', `username` varchar(20) DEFAULT NULL COMMENT '使用者名稱', `password` varchar(100) DEFAULT NULL COMMENT 'MD5加密密碼', `headimg` varchar(200) DEFAULT NULL COMMENT '頭像', `realname` varchar(20) DEFAULT NULL COMMENT '真實姓名', `sex` int(1) DEFAULT NULL COMMENT '性別', `age` int(2) DEFAULT NULL COMMENT '年齡', `mobile` varchar(20) DEFAULT NULL COMMENT '手機號', `email` varchar(50) DEFAULT NULL COMMENT '郵件地址', `credate` datetime DEFAULT NULL COMMENT '建立時間', `upddate` datetime DEFAULT NULL COMMENT '更新時間', PRIMARY KEY (`id`) ) DEFAULT CHARSET=utf8;
3.專案配置
(1)config/jdbc.config配置
#連線驅動 jdbc.driverClassName=com.mysql.jdbc.Driver #埠號3306的資料來源 jdbc.url = jdbc\:mysql\://localhost\:3306/db_ssmdemo?useUnicode\=true&characterEncoding\=UTF-8&allowMultiQueries\=true jdbc.username = root jdbc.password = 123456 #埠號3308的資料來源 jdbc.3308.url = jdbc\:mysql\://localhost\:3308/db_ssmdemo?useUnicode\=true&characterEncoding\=UTF-8&allowMultiQueries\=true jdbc.3308.username = root jdbc.3308.password = 123456 #定義初始連線數 jdbc.initialSize=2 #定義最大連線數 jdbc.maxActive=20 #定義最大空閒 jdbc.maxIdle=20 #定義最小空閒 jdbc.minIdle=1 #定義最長等待時間 jdbc.maxWait=60000 #驗證資料庫連線的有效性 jdbc.validationQuery=select 1
(2)spring/mybatis.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd">
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations">
<list>
<value>classpath:sql/*.xml</value>
</list>
</property>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.ssm.mapper" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate" scope="prototype">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
<!-- 使用annotation定義事務,使用cglib代理,解決同一service中事務方法相互呼叫的 巢狀事務失效問題 -->
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
<!--事務配置 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
(3)spring/dataAccessContext.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
<description>資料庫、事務配置</description>
<!-- 埠號3306的資料來源-->
<bean id="dataSource3306" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="initialSize" value="${jdbc.initialSize}" />
<property name="maxActive" value="${jdbc.maxActive}" />
<property name="maxIdle" value="${jdbc.maxIdle}" />
<property name="minIdle" value="${jdbc.minIdle}" />
<property name="maxWait" value="${jdbc.maxWait}"></property>
<property name="validationQuery" value="${jdbc.validationQuery}" />
<!-- 監控資料庫 -->
<!--<property name="filters" value="mergeStat" />-->
<property name="filters" value="stat" />
<property name="connectionProperties" value="druid.stat.mergeSql=true" />
</bean>
<!-- 埠號3308的資料來源 -->
<bean id="dataSource3308" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.3308.url}" />
<property name="username" value="${jdbc.3308.username}" />
<property name="password" value="${jdbc.3308.password}" />
<property name="initialSize" value="${jdbc.initialSize}" />
<property name="maxActive" value="${jdbc.maxActive}" />
<property name="maxIdle" value="${jdbc.maxIdle}" />
<property name="minIdle" value="${jdbc.minIdle}" />
<property name="maxWait" value="${jdbc.maxWait}"></property>
<property name="validationQuery" value="${jdbc.validationQuery}" />
<!-- 監控資料庫 -->
<!--<property name="filters" value="mergeStat" />-->
<property name="filters" value="stat" />
<property name="connectionProperties" value="druid.stat.mergeSql=true" />
</bean>
<!-- 資料來源,需要自定義類繼承AbstractRoutingDataSource,實現determineCurrentLookupKey -->
<bean id="dataSource" class="com.ssm.datasource.DynamicDataSource">
<!-- 設定預設資料來源 -->
<property name="defaultTargetDataSource" ref="dataSource3306"></property>
<!-- 設定多個數據源,後臺切換資料來源key與這裡key配置需要一致 -->
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="dataSource3306" value-ref="dataSource3306"/>
<entry key="dataSource3308" value-ref="dataSource3308"/>
</map>
</property>
</bean>
4.自定義類繼承AbstractRoutingDataSource,實現determineCurrentLookupKey
首先我們來看下AbstractRoutingDataSource關鍵原始碼
package org.springframework.jdbc.datasource.lookup;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.util.Assert;
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
private Map<Object, Object> targetDataSources;
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
private Map<Object, DataSource> resolvedDataSources;
private DataSource resolvedDefaultDataSource;
//此方法是配置 <property name="targetDataSources">賦值map,後續切換資料來源便是在此map中取值
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
}
//設定預設資料來源
public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
this.defaultTargetDataSource = defaultTargetDataSource;
}
public void setLenientFallback(boolean lenientFallback) {
this.lenientFallback = lenientFallback;
}
public void setDataSourceLookup(DataSourceLookup dataSourceLookup) {
this.dataSourceLookup = (dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());
}
//屬性賦值之後呼叫此方法將目標資料來源存於resolvedDataSources
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
//配置的目標資料來源
this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size());
for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) {
Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
this.resolvedDataSources.put(lookupKey, dataSource);
}
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
//顯然是根據determineTargetDataSource()返回的最終資料來源進行連線
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
//返回最終資料來源
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
//實現determineCurrentLookupKey()返回顯然需要和配置的
//<entry key="dataSource3306" value-ref="dataSource3306"/> <entry key="dataSource3308" value-ref="dataSource3308"/>
//保持一致,否則將始終是預設配置的資料來源
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;
}
//此方法就是我們需要自己實現的
protected abstract Object determineCurrentLookupKey();
}
然後我們按此實現determineCurrentLookupKey方法,返回值為dataSource3306或dataSource3308
package com.ssm.datasource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 定義動態資料來源,整合spring提供的AbstractRoutingDataSource,實現determineCurrentLookupKey
* @author https://blog.csdn.net/caiqing116
*/
public class DynamicDataSource extends AbstractRoutingDataSource{
private static final Logger log = LoggerFactory.getLogger(DynamicDataSource.class);
@Override
protected Object determineCurrentLookupKey() {
//使用DataSourceContextHolder保證執行緒安全,並且得到當前執行緒中的資料來源
Object dataType = DataSourceContextHolder.getDataType();
log.info("當前資料來源:{}",dataType);
return dataType;
}
}
5.藉助ThreadLocal類,通過ThreadLocal類傳遞引數設定資料來源
package com.ssm.datasource;
/**
* 切換資料來源,清除資料來源資訊等
* @author https://blog.csdn.net/caiqing116
*/
public class DataSourceContextHolder {
//定義資料來源標識和配置檔案dataAccessContext.xml配置的bean id一致
public static final String DATASOURCE = "dataSource3306";
public static final String DATASOURCE3308 = "dataSource3308";
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
/**
* 設定當前資料來源
* @param dataType 資料元型別 DATASOURCE | DATASOURCE3308
*/
public static void setDatatype(String dataType) {
contextHolder.set(dataType);
}
/**
* 獲取當前資料來源
* @return
*/
public static String getDataType(){
return contextHolder.get();
}
/**
* 清除
*/
public static void clear() {
contextHolder.remove();
}
/**
* 切到3306埠資料來源
*/
public static void mark3306() {
setDatatype(DATASOURCE);
}
/**
* 切到3308埠資料來源
*/
public static void mark3308() {
setDatatype(DATASOURCE3308);
}
}
6.測試
編寫增刪查改Service和實現類,這些在之前的文章中有介紹,就不重複介紹了,mapper及對映檔案參考文章:https://blog.csdn.net/caiqing116/article/details/84581171
com/ssm/service/BasicUserService.java
package com.ssm.service;
import java.util.List;
import com.ssm.entity.BasicUser;
import com.ssm.entity.Page;
/**
* 使用者Service
* @author https://blog.csdn.net/caiqing116
*
*/
public interface BasicUserService {
Integer insert(BasicUser basicUser);
Integer deleteById(Integer id);
BasicUser selectById(Integer id);
Integer updateById(BasicUser basicUser);
BasicUser selectByUsername(String username);
}
com/ssm/service/impl/BasicUserServiceImpl.java
package com.ssm.service.impl;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ssm.entity.BasicUser;
import com.ssm.entity.Page;
import com.ssm.mapper.BasicUserMapper;
import com.ssm.service.BasicUserService;
/**
* 使用者Service實現類
* @author https://blog.csdn.net/caiqing116
*
*/
@Service
public class BasicUserServiceImpl implements BasicUserService{
@Autowired
private BasicUserMapper basicUserMapper;
/**
* 插入使用者
*/
public Integer insert(BasicUser basicUser) {
return basicUserMapper.insertSelective(basicUser);
}
/**
* 根據id刪除
*/
public Integer deleteById(Integer id) {
return basicUserMapper.deleteByPrimaryKey(id);
}
/**
* 根據id查詢
*/
public BasicUser selectById(Integer id) {
return basicUserMapper.selectByPrimaryKey(id);
}
/**
* 根據id更新
*/
public Integer updateById(BasicUser basicUser) {
return basicUserMapper.updateByPrimaryKeySelective(basicUser);
}
/**
* 根據使用者名稱查詢
*/
public BasicUser selectByUsername(String username) {
return basicUserMapper.selectByUsername(username);
}
}
Service測試類
package com.ssm.service;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.ssm.datasource.DataSourceContextHolder;
import com.ssm.entity.BasicUser;
import com.ssm.util.EncryptKit;
import com.ssm.util.UuidUtil;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:spring/*.xml","classpath:servlet/*.xml" })
public class BasicUserServiceTest {
private static final Logger log = LoggerFactory.getLogger(BasicUserServiceTest.class);
@Autowired
private BasicUserService basicUserService;
@Test
public void testInsert() {
BasicUser basicUser = new BasicUser();
basicUser.setUtype(1);
basicUser.setUserid(UuidUtil.getUuid());
basicUser.setUsername("劍非道");
basicUser.setRealname("劍非道");
basicUser.setPassword(EncryptKit.MD5("123456"));
basicUser.setAge(18);
//切換到3308插入資料
DataSourceContextHolder.clear();
DataSourceContextHolder.mark3308();
int result = basicUserService.insert(basicUser);
DataSourceContextHolder.clear();
log.info("basicUser:"+basicUser);
log.info("插入行數:"+result);
}
}
經過上文測試我們查詢資料庫結果顯示在3306埠服務資料庫db_ssmdemo中無記錄,在3308埠服務資料庫db_ssmdemo有記錄,說明由預設資料來源3306切換到3308。
同理我們可以簡單修改切換資料來源程式碼:
DataSourceContextHolder.mark3308(); 修改為 DataSourceContextHolder.mark3306();
執行測試方法,那麼再次查詢3306查詢到記錄則證明切換成功。