大型網站為了軟解大量的並發訪問,除了在網站實現分布式負載均衡,遠遠不夠。到了數據業務層、數據訪問層,如果還是傳統的數據結構,或者只是單單靠一台服務器扛,如此多的數據庫連接操作,數據庫必然會崩潰,數據丟失的話,后果更是 不堪設想。這時候,我們會考慮如何減少數據庫的聯接,一方面采用優秀的代碼框架,進行代碼的優化,采用優秀的數據緩存技術如:redis,如果資金豐厚的話,必然會想到假設服務器群,來分擔主數據庫的壓力。Ok切入今天微博主題,利用MySQL主從配置,實現讀寫分離,減輕數據庫壓力。這種方式,在如今很多網站里都有使用,也不是什么新鮮事情,今天總結一下,方便大家學習參考一下。
原理:主服務器(master)負責寫操作(包括增刪改),有且僅有一台;從服務器(slave)負責讀操作,可以配置n台,寫入數據的時候,首先master會把數據寫在本地 Binary Log 文件中,然后通過I/O 寫入 slave 的 relayLog 日志文件中,之后才同步數據庫中,實現主從同步
在這里我使用的是兩台CenterOS 6.5 的虛擬機進行配置,master IP :192.168.1.111 , Slave IP :192.168.1.112,開始了:
一: Mysql 配置主從分離
1.下載安裝 Mysql 數據庫
1.1 # yum list | grep mysql --我們通過命令可以查看yum上提供下載的mysql的版本信息
1.2 # yum install -y mysql-server mysql mysql-deve --運行命令開始安裝直到安裝完成
2. 配置Mysql數據庫的 master 和 slave
2.1 首先配置 master:
# vi /etc/my.cnf -- Mysql 的配置一般都是在這,直接運行命令進行修改
在【mysqld】添加以下:
server-id=1
log-bin=master-bin
log-bin-index=master-bin.index
隨后開啟數據庫
# service mysqld start;
登錄進去數據庫:
# mysql -uroot -p;
進去之后創建一個用戶用來主從數據庫的通信
# 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 配置完成了。
2,2 配置slave
和 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 。。。。。。。。。。。。。。。。。。