大型網站爲了軟解大量的併發訪問,除了在網站實現分佈式負載均衡,遠遠不夠。到了數據業務層、數據訪問層,若是仍是傳統的數據結構,或者只是單單靠一臺服務器扛,如此多的數據庫鏈接操做,數據庫必然會崩潰,數據丟失的話,後果更是 不堪設想。這時候,咱們會考慮如何減小數據庫的聯接,一方面採用優秀的代碼框架,進行代碼的優化,採用優秀的數據緩存技術如:redis,若是資金豐厚的話,必然會想到假設服務器羣,來分擔主數據庫的壓力。Ok切入今天微博主題,利用MySQL主從配置,實現讀寫分離,減輕數據庫壓力。這種方式,在現在不少網站裏都有使用,也不是什麼新鮮事情,今天總結一下,方便你們學習參考一下。java
原理:主服務器(master)負責寫操做(包括增刪改),有且僅有一臺;從服務器(slave)負責讀操做,能夠配置n臺,寫入數據的時候,首先master會把數據寫在本地 Binary Log 文件中,而後經過I/O 寫入 slave 的 relayLog 日誌文件中,以後才同步數據庫中,實現主從同步mysql
在這裏我使用的是兩臺CenterOS 6.5 的虛擬機進行配置,master IP :192.168.1.111 , Slave IP :192.168.1.112,開始了:redis
一: Mysql 配置主從分離spring
# vi /etc/my.cnf -- Mysql 的配置通常都是在這,直接運行命令進行修改sql
在【mysqld】添加如下:數據庫
server-id=1
log-bin=master-bin
log-bin-index=master-bin.index
隨後開啓數據庫apache
# service mysqld start;緩存
登陸進去數據庫:服務器
# mysql -uroot -p;session
進去以後建立一個用戶用來主從數據庫的通訊
# create user manager;
授予 REPLICATION SLAVE 權限就夠了
# grant replication slave on *.* to 'manager'@'192.168.1.112' identified by '123456';
# flush privileges;
以後查看一下master日誌
# show master status;
+-------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+-------------------+----------+--------------+------------------+
| master-bin.000001 | 1285 | | |
+-------------------+----------+--------------+------------------+
1 row in set (0.00 sec)
好了 ,master 配置完成了。
和 master 同樣,首先修改配置文件
# vi /etc/my.cnf
在【mysqld】添加如下:
server-id=2
relay-log-index=slave-relay-bin.index
relay-log=slave-relay-bin
而後開啓 mysql 服務,登陸進去以後,鏈接master
# change master to master_host='192.168.1.111', //Master 服務器Ip
master_port=3306,
master_user='manager',
master_password='123456',
master_log_file='master-bin.000001',//Master服務器產生的日誌
master_log_pos=0;
啓動 slave
# start slave
查看一下slave運行有沒有錯誤,若是沒有就說明已經配置好了,主和從已經正常工做了
# show slave status \G;
二: 配置Spring 和 Mybatis
首先修改一下 jdbc.properties ,配置兩條鏈接數據庫URL
jdbc.driver=com.mysql.jdbc.Driver jdbc.slave.url=jdbc:mysql://192.168.237.111/test?useUnicode=true&characterEncoding=utf8 jdbc.master.url=jdbc:mysql://192.168.237.112/test?useUnicode=true&characterEncoding=utf8 jdbc.username=xxxxx jdbc.password=xxxxx
定義一個攔截器,實現 import org.apache.ibatis.plugin.Interceptor 接口
package com.smy.dao.split; import java.util.Properties; 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.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Plugin; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.transaction.support.TransactionSynchronizationManager; @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 DynamicDataSourceInterceptor implements Interceptor { // 數據庫操做字符串的匹配,insert,update,delete
private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*"; private static Logger log = LoggerFactory.getLogger(DynamicDataSourceInterceptor.class); @Override public Object intercept(Invocation invocation) throws Throwable { // 判斷是否被事務管理
boolean synchronization = TransactionSynchronizationManager.isActualTransactionActive(); Object[] objects = invocation.getArgs(); MappedStatement ms = (MappedStatement) objects[0]; String lookupKey = DynamicDataSourceHolder.DB_MASTER;; // 判斷是否被事務管理
if (!synchronization) { // 讀操做
if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) { // selectKey 爲自增id查詢主鍵(SELECT LAST_INSERT_ID())方法,使用主庫
if (ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) { lookupKey = DynamicDataSourceHolder.DB_MASTER; } else { // 若是執行到了這裏說明就是沒有被事務管理也沒有指定主鍵Key,只能對sql // 語句進行匹配規則
BoundSql boundSql = ms.getBoundSql(objects[1]); String sql = boundSql.getSql().toLowerCase().replaceAll("\\t\\n\\r", " "); if (sql.matches(REGEX)) { lookupKey = DynamicDataSourceHolder.DB_MASTER; } else { lookupKey = DynamicDataSourceHolder.DB_SLAVE; } } } } else { // 若是被事務管理說明 就是增刪改,須要在 master 中操做
lookupKey = DynamicDataSourceHolder.DB_MASTER; } log.debug("設置方法[{}] use [{}] Strategy, SqlCommanType [{}]..", ms.getId(), lookupKey, ms.getSqlCommandType().name()); //設置訪問數據庫類型 master 或者 slave
DynamicDataSourceHolder.setDbType(lookupKey); return invocation.proceed(); } @Override public Object plugin(Object target) { // 若是執行的是增刪改的操做就使用本攔截,若是不是就直接返回
if (target instanceof Executor) { return Plugin.wrap(target, this); } return target; } @Override public void setProperties(Properties properties) { } }
定義一個類 DynamicDataSourceHolder 來管理 咱們的master 和 slave 常量,也就是管理咱們的數據源
package com.smy.dao.split; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DynamicDataSourceHolder { private static Logger log = LoggerFactory.getLogger(DynamicDataSourceHolder.class); 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 getDbType() { String db = contextHolder.get(); if(db==null) { db = DB_MASTER; } return db; } /** * 設置數據源 * @param dbType */
public static void setDbType(String dbType) { log.debug("所使用的數據源"+dbType); contextHolder.set(dbType); } /** * 清理數據源 */
public static void clearDbType() { contextHolder.remove(); } }
接下來 定義一個類 繼承org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 類,由於這個類可以動態路由到數據源
package com.smy.dao.split; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceHolder.getDbType(); } }
實現這個類的一個抽象方法,查看AbstractRoutingDataSource 類源碼有這麼一個方法:
/** * Retrieve the current target DataSource. Determines the * {@link #determineCurrentLookupKey() current lookup key}, performs * a lookup in the {@link #setTargetDataSources targetDataSources} map, * falls back to the specified * {@link #setDefaultTargetDataSource default target DataSource} if necessary. * @see #determineCurrentLookupKey() */
protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = determineCurrentLookupKey(); // 這個方法就肯定了要使用哪一個數據源,然而AbstractRoutingDataSource 類中,這個方法是抽象的,因此咱們要實現這個類並實現該方法 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; }
OK ,開始咱們在Spring 和 Mybatis 配置文件中配置咱們的攔截器 和 動態數據源Bean
Mybatis.xml 中配置添加plugin:
<plugins>
<plugin interceptor="com.smy.dao.split.DynamicDataSourceInterceptor" />
</plugins>
Spring-dao.xml 中:
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 2.數據庫鏈接池 -->
<!-- 定義抽象數據源,使其它數據源 Bean 繼承該數據源 -->
<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" />
<property name="maxStatements" value="0" />
</bean>
<!-- 主數據源 master -->
<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>
<!-- 從數據源 slave -->
<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>
<!-- 配置動態數據源,這兒targetDataSources就是路由數據源所對應的名稱 -->
<bean id="dynamicDataSource" class="com.smy.dao.split.DynamicDataSource">
<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="dynamicDataSource" />
</property>
</bean>
至於爲何這麼配置, 相信你們看過源碼以後就會很清楚了。。
The End 。。。。。。。。。。。。。。。。。。