CentOS6中MySql5.6資料庫主從複製/讀寫分離(二)
程式碼層面實現讀寫分離
在文章(一)中我們已經有了兩個資料庫而且已經實現了主從資料庫同步,接下來的問題就是在我們的業務程式碼裡面實現讀寫分離,假設我們使用的是主流的ssm的框架開發的web專案,這裡面我們需要多個數據源。
在此之前,我們在專案中一般會使用一個數據庫使用者遠端操作資料庫(避免直接使用root使用者),因此我們需要在主從資料庫裡面都建立一個使用者mysqluser,賦予其增刪改查的許可權
mysql> GRANT select,insert,update,delete ON . TO ‘mysqluser’@’%’ IDENTIFIED BY ‘mysqlpassword’ WITH GRANT OPTION;
然後我們的程式裡就用mysqluser這個使用者操作資料庫
編寫jdbc.propreties
#mysql驅動 jdbc.driver=com.mysql.jdbc.Driver #主資料庫地址 jdbc.master.url=jdbc:mysql://xxx.xxx.xxx.xx:3306/testsplit?useUnicode=true&characterEncoding=utf8 #從資料庫地址 jdbc.slave.url=jdbc:mysql://xxx.xxx.xxx.xx:3306/testsplit?useUnicode=true&characterEncoding=utf8 #資料庫賬號 jdbc.username=mysqluser jdbc.password=mysqlpassword
這裡我們指定了兩個資料庫地址,其中的xxx分別是我們的主從資料庫的ip地址,埠都是使用預設的3306
2.配置資料來源
在spring-dao.xml中配置資料來源(這裡就不累贅介紹spring的配置了,假設大家都已經配置好執行環境),配置如下:
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置整合mybatis過程 -->
<!-- 1.配置資料庫相關引數properties的屬性:${url} -->
<context:property-placeholder location="classpath:jdbc.properties" />
<!-- 掃描dao包下所有使用註解的型別 -->
<context:component-scan base-package="c n.xzchain.testsplit.dao" />
<!-- 2.資料庫連線池 -->
<bean id="abstractDataSource" abstract="true" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<!-- c3p0連線池的私有屬性 -->
<property name="maxPoolSize" value="30" />
<property name="minPoolSize" value="10" />
<!-- 關閉連線後不自動commit -->
<property name="autoCommitOnClose" value="false" />
<!-- 獲取連線超時時間 -->
<property name="checkoutTimeout" value="10000" />
<!-- 當獲取連線失敗重試次數 -->
<property name="acquireRetryAttempts" value="2" />
</bean>
<!--主庫配置-->
<bean id="master" parent="abstractDataSource">
<!-- 配置連線池屬性 -->
<property name="driverClass" value="${jdbc.driver}" />
<property name="jdbcUrl" value="${jdbc.master.url}" />
<property name="user" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
<!--從庫配置-->
<bean id="slave" parent="abstractDataSource">
<!-- 配置連線池屬性 -->
<property name="driverClass" value="${jdbc.driver}" />
<property name="jdbcUrl" value="${jdbc.slave.url}" />
<property name="user" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
<!--配置動態資料來源,這裡的targetDataSource就是路由資料來源所對應的名稱-->
<bean id="dataSourceSelector" class="cn.xzchain.testsplit.dao.split.DataSourceSelector">
<property name="targetDataSources">
<map>
<entry value-ref="master" key="master"></entry>
<entry value-ref="slave" key="slave"></entry>
</map>
</property>
</bean>
<!--配置資料來源懶載入-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy">
<property name="targetDataSource">
<ref bean="dataSourceSelector"></ref>
</property>
</bean>
<!-- 3.配置SqlSessionFactory物件 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 注入資料庫連線池 -->
<property name="dataSource" ref="dataSource" />
<!-- 配置MyBaties全域性配置檔案:mybatis-config.xml -->
<property name="configLocation" value="classpath:mybatis-config.xml" />
<!-- 掃描entity包 使用別名 -->
<property name="typeAliasesPackage" value="cn.xzchain.testsplit.entity" />
<!-- 掃描sql配置檔案:mapper需要的xml檔案 -->
<property name="mapperLocations" value="classpath:mapper/*.xml" />
</bean>
<!-- 4.配置掃描Dao介面包,動態實現Dao介面,注入到spring容器中 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 注入sqlSessionFactory -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
<!-- 給出需要掃描Dao介面包 -->
<property name="basePackage" value="cn.xzchain.testsplit.dao" />
</bean>
</beans>
說明:
首先讀取配置檔案jdbc.properties,然後在我們定義了一個基於c3p0連線池的父類“抽象”資料來源,然後配置了兩個具體的資料來源master、slave,繼承了abstractDataSource,這裡面就配置了資料庫連線的具體屬性,然後我們配置了動態資料來源,他將決定使用哪個具體的資料來源,這裡面的關鍵就是DataSourceSelector,接下來我們會實現這個bean。下一步設定了資料來源的懶載入,保證在資料來源載入的時候其他依賴的bean已經載入好了。接著就是常規的配置了,我們的mybatis全域性配置檔案如下
- 踩坑記錄
關於 配置MyBaties全域性配置檔案:mybatis-config.xml
<!-- 此處value後引用的配置檔案,如果該配置檔案在src下可以不加路徑直接寫檔名稱引用,如果是放在自己特定的資料夾下需要加上級目錄,例如我的:classpath:cfg/mybatis-config.xml , 千萬注意classpath後不要加*,配置找不到 -->
<property name="configLocation" value="classpath:mybatis-config.xml" />
3.mybatis全域性配置檔案
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置全域性屬性 -->
<settings>
<!-- 使用jdbc的getGeneratedKeys獲取資料庫自增主鍵值 -->
<setting name="useGeneratedKeys" value="true" />
<!-- 使用列別名替換列名 預設:true -->
<setting name="useColumnLabel" value="true" />
<!-- 開啟駝峰命名轉換:Table{create_time} -> Entity{createTime} -->
<setting name="mapUnderscoreToCamelCase" value="true" />
<!-- 列印查詢語句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
</settings>
<plugins>
<plugin interceptor="cn.xzchain.testsplit.dao.split.DateSourceSelectInterceptor"></plugin>
</plugins>
</configuration>
這裡面的關鍵就是DateSourceSelectInterceptor這個攔截器,它會攔截所有的資料庫操作,然後分析sql語句判斷是“讀”操作還是“寫”操作,我們接下來就來實現上述的DataSourceSelector和DateSourceSelectInterceptor
4.編寫DataSourceSelector
DataSourceSelector就是我們在spring-dao.xml配置的,用於動態配置資料來源。程式碼如下:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @author lihang
* @date 2017/12/6.
* @description 繼承了AbstractRoutingDataSource,動態選擇資料來源
*/
public class DataSourceSelector extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.getDataSourceType();
}
}
我們只要繼承AbstractRoutingDataSource並且重寫determineCurrentLookupKey()方法就可以動態配置我們的資料來源。
編寫DynamicDataSourceHolder,程式碼如下:
/**
* @author lihang
* @date 2017/12/6.
* @description
*/
public class DynamicDataSourceHolder {
/**用來存取key,ThreadLocal保證了執行緒安全*/
private static ThreadLocal<String> contextHolder = new ThreadLocal<String>();
/**主庫*/
public static final String DB_MASTER = "master";
/**從庫*/
public static final String DB_SLAVE = "slave";
/**
* 獲取執行緒的資料來源
* @return
*/
public static String getDataSourceType() {
String db = contextHolder.get();
if (db == null){
//如果db為空則預設使用主庫(因為主庫支援讀和寫)
db = DB_MASTER;
}
return db;
}
/**
* 設定執行緒的資料來源
* @param s
*/
public static void setDataSourceType(String s) {
contextHolder.set(s);
}
/**
* 清理連線型別
*/
public static void clearDataSource(){
contextHolder.remove();
}
}
這個類決定返回的資料來源是master還是slave,這個類的初始化我們就需要藉助DateSourceSelectInterceptor了,我們攔截所有的資料庫操作請求,通過分析sql語句來判斷是讀還是寫操作,讀操作就給DynamicDataSourceHolder設定slave源,寫操作就給其設定master源,程式碼如下:
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.Locale;
import java.util.Properties;
/**
* @author lihang
* @date 2017/12/6.
* @description 攔截資料庫操作,根據sql判斷是讀還是寫,選擇不同的資料來源
*/
@Intercepts({@Signature(type = Executor.class,method = "update",args = {MappedStatement.class,Object.class}),
@Signature(type = Executor.class,method = "query",args = {MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class})})
public class DateSourceSelectInterceptor implements Interceptor{
/**正則匹配 insert、delete、update操作*/
private static final String REGEX = ".*insert\\\\u0020.*|.*delete\\\\u0020.*|.*update\\\\u0020.*";
@Override
public Object intercept(Invocation invocation) throws Throwable {
//判斷當前操作是否有事務
boolean synchonizationActive = TransactionSynchronizationManager.isSynchronizationActive();
//獲取執行引數
Object[] objects = invocation.getArgs();
MappedStatement ms = (MappedStatement) objects[0];
//預設設定使用主庫
String lookupKey = DynamicDataSourceHolder.DB_MASTER;;
if (!synchonizationActive){
//讀方法
if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)){
//selectKey為自增主鍵(SELECT LAST_INSERT_ID())方法,使用主庫
if (ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)){
lookupKey = DynamicDataSourceHolder.DB_MASTER;
}else {
BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replace("[\\t\\n\\r]"," ");
//如果是insert、delete、update操作 使用主庫
if (sql.matches(REGEX)){
lookupKey = DynamicDataSourceHolder.DB_MASTER;
}else {
//使用從庫
lookupKey = DynamicDataSourceHolder.DB_SLAVE;
}
}
}
}else {
//一般使用事務的都是寫操作,直接使用主庫
lookupKey = DynamicDataSourceHolder.DB_MASTER;
}
//設定資料來源
DynamicDataSourceHolder.setDataSourceType(lookupKey);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor){
//如果是Executor(執行增刪改查操作),則攔截下來
return Plugin.wrap(target,this);
}else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
}
}
通過這個攔截器,所有的insert、delete、update操作設定使用master源,select會使用slave源。
接下來就是測試了,我這是生產環境的程式碼,直接列印日誌,小夥伴可以加上日誌後測試使用的是哪個資料來源,結果和預期一樣,這樣我們就實現了讀寫分離~
ps:我們可以配置多個slave用於負載均衡,只需要在spring-dao.xml中新增slave1、slave2、slave3……然後修改dataSourceSelector這個bean,
<bean id="dataSourceSelector" class="cn.xzchain.o2o.dao.split.DataSourceSelector">
<property name="targetDataSources">
<map>
<entry value-ref="master" key="master"></entry>
<entry value-ref="slave1" key="slave1"></entry>
<entry value-ref="slave2" key="slave2"></entry>
<entry value-ref="slave3" key="slave3"></entry>
</map>
</property>
在map標籤中新增slave1、slave2、slave3……即可,具體的負載均衡策略我們在DynamicDataSourceHolder、DateSourceSelectInterceptor中實現即可。
最後整理一下整個流程:
1.專案啟動後,在依賴的bean載入完成後,我們的資料來源通過LazyConnectionDataSourceProxy開始載入,他會引用dataSourceSelector載入資料來源。
2.DataSourceSelector會選擇一個數據源,我們在程式碼裡設定了預設資料來源為master,在初始化的時候我們就預設使用master源。
3.在資料庫操作執行時,DateSourceSelectInterceptor攔截器攔截了請求,通過分析sql決定使用哪個資料來源,“讀操作”使用slave源,“寫操作”使用master源。
寫在後面
現在很多讀寫分離中介軟體已經大大簡化了我們的工作,但是自己實現一個小體量的讀寫分離有助於我們進一步理解資料庫讀寫分離在業務上的實現,呼~