Mysql主從複製及讀寫分離
主從複製
什麼是主從複製
主從複製,是用來建立一個和主數據庫完全一樣的數據庫環境,稱爲從數據庫;主數據庫一般是準實時的業務數據庫。
主從複製的作用
1、做數據的熱備,作爲後備數據庫,主數據庫服務器故障後,可切換到從數據庫繼續工作,避免數據丟失。
2、架構的擴展。業務量越來越大,I/O訪問頻率過高,單機無法滿足,此時做多庫的存儲,降低磁盤I/O訪問的頻率,提高單個機器的I/O性能。
3、讀寫分離,使數據庫能支撐更大的併發。在報表中尤其重要。由於部分報表sql語句非常的慢,導致鎖表,影響前臺服務。如果前臺使用master,報表使用slave,那麼報表sql將不會造成前臺鎖,保證了前臺速度。
主從複製原理
1.數據庫有個bin-log二進制文件,記錄了所有sql語句。
2.我們的目標就是把主數據庫的bin-log文件的sql語句複製過來。
3.讓其在從數據的relay-log重做日誌文件中再執行一次這些sql語句即可。
4.下面的主從配置就是圍繞這個原理配置
5.具體需要三個線程來操作:
- 1.binlog輸出線程:每當有從庫連接到主庫的時候,主庫都會創建一個線程然後發送binlog內容到從庫。對於每一個即將發送給從庫的sql事件,binlog輸出線程會將其鎖住。一旦該事件被線程讀取完之後,該鎖會被釋放,即使在該事件完全發送到從庫的時候,該鎖也會被釋放。在從庫裏,當複製開始的時候,從庫就會創建兩個線程進行處理:
- 2.從庫I/O線程:當START SLAVE語句在從庫開始執行之後,從庫創建一個I/O線程,該線程連接到主庫並請求主庫發送binlog裏面的更新記錄到從庫上。從庫I/O線程讀取主庫的binlog輸出線程發送的更新並拷貝這些更新到本地文件,其中包括relay log文件。
- 3.從庫的SQL線程:從庫創建一個SQL線程,這個線程讀取從庫I/O線程寫到relay log的更新事件並執行。
可以知道,對於每一個主從複製的連接,都有三個線程。擁有多個從庫的主庫爲每一個連接到主庫的從庫創建一個binlog輸出線程,每一個從庫都有它自己的I/O線程和SQL線程。
從庫通過創建兩個獨立的線程,使得在進行復制時,從庫的讀和寫進行了分離。因此,即使負責執行的線程運行較慢,負責讀取更新語句的線程並不會因此變得緩慢。比如說,如果從庫有一段時間沒運行了,當它在此啓動的時候,儘管它的SQL線程執行比較慢,它的I/O線程可以快速地從主庫裏讀取所有的binlog內容。這樣一來,即使從庫在SQL線程執行完所有讀取到的語句前停止運行了,I/O線程也至少完全讀取了所有的內容,並將其安全地備份在從庫本地的relay log,隨時準備在從庫下一次啓動的時候執行語句。【從庫生成兩個線程,一個I/O線程,一個SQL線程;i/o線程去請求主庫 的binlog,並將得到的binlog日誌寫到relay log(中繼日誌) 文件中;主庫會生成一個 log dump 線程,用來給從庫 i/o線程傳binlog;SQL 線程,會讀取relay log文件中的日誌,並解析成具體操作,來實現主從的操作一致,而最終數據一致;】
可以知道,對於每一個主從複製的連接,都有三個線程。擁有多個從庫的主庫爲每一個連接到主庫的從庫創建一個binlog輸出線程,每一個從庫都有它自己的I/O線程和SQL線程。
主從複製如圖:
原理圖2
步驟一:主庫db的更新事件(update、insert、delete)被寫到binlog
步驟二:從庫發起連接,連接到主庫
步驟三:此時主庫創建一個binlog dump thread線程,把binlog的內容發送到從庫
步驟四:從庫啓動之後,創建一個I/O線程,讀取主庫傳過來的binlog內容並寫入到relay log.
步驟五:還會創建一個SQL線程,從relay log裏面讀取內容,從Exec_Master_Log_Pos位置開始執行讀取到的更新事件,將更新內容寫入到slave的db.
主從複製搭建步驟 :
1.linux安裝mysql,使用yum指令安裝:yum install mysql-server 準備兩臺機器分別安裝mysql數據庫
2.安裝完成啓動mysql服務:service mysqld start
3.指定mysql數據庫密碼:/usr/bin/mysqladmin -u root password 'root'
4.測試:mysql -uroot -proot
5.默認裝完mysql不允許遠程訪問[使用root開啓遠程訪問]步驟如下:
-
select user,host,password from user;
-
刪除沒有密碼的用戶:delete from user where password=’’;
-
修改端口:允許所有端口進行訪問:update user set host='%';
-
提交commit;
-
刷新mysql權限:flush privileges;
-
navicat連接成功:
6.兩臺搭建成功之後測試兩臺機器能否相互連接:[標識連接成功]
[一臺機器連接另一臺mysql] mysql -u root -p -h 192.168.87.130 -P 3306
[另一臺機器連接本機的mysql] mysql -u root -p -h 192.168.87.129 -P 3306
7.開始搭建[開始位置]修改mysql的配置文件:vim /etc/my.cnf [下面是添加配置文件的內容]
server-id=1 [多臺機器的id名字不能相同]
log-bin=mysql-bin
log-slave-updates
slave-skip-errors=all
8.修改完成重新啓動mysql服務指令:service mysqld restart
9.登陸mysql:mysql -uroot -proot 檢測配置是否生效:SHOW VARIABLES like 'server_id';[結束位置]
10.以上的步驟在另一臺機器[mysql從庫]做相同的配置.[開始位置]--[結束位置]
11.查看主節點[主庫]的狀態信息,文件名稱:show master status;
12.在從機上執行指令stop slave; 將如下配置修改後在從機的mysql客戶端執行
將如下配置修改後在從機的mysql客戶端執行
change master to
master_host='192.168.64.132',
master_user='root',
master_password='root',
master_log_file='mysql-bin.000001',
master_log_pos=106;
13.在從節點[從庫]開啓複製功能指令:start slave;
14.在從機上查看從節點[從庫]狀態:show slave status\G; 表示搭建成功
15.測試連接兩臺服務器: [搭建成功]
-
在主庫中創建數據庫ems,刷新從庫出現ems數據庫
-
在主庫中的表添加數據,從庫刷新會同步主庫的數據
搭建主從複製問題解決方案參考:
https://blog.csdn.net/anljf/article/details/6822980
讀寫分離
使用場景
類似淘寶網這樣的網站,海量數據的存儲和訪問成爲了系統設計的瓶頸問題,日益增長的業務數據,無疑對數據庫造成了相當大的負載,同時對於系統的穩定性和擴展性提出很高的要求。隨着時間和業務的發展,數據庫中的表會越來越多,表中的數據量也會越來越大,相應地,數據操作的開銷也會越來越大;另外,無論怎樣升級硬件資源,單臺服務器的資源(CPU、磁盤、內存、網絡IO、事務數、連接數)總是有限的,最終數據庫所能承載的數據量、數據處理能力都將遭遇瓶頸。分表、分庫和讀寫分離可以有效地減小單臺數據庫的壓力。
讀寫分離架構圖
讀寫分離就是:一主多從,讀寫分離,主動同步,是一種常見的數據庫架構,一般來說:
- 主庫:提供數據庫寫服務;
- 從庫:提供數據庫讀服務;
- 主從之間:通過某種機制同步數據,比如MySOL的binlog
一個主從同步的集羣通常稱爲一個“分組”,這也是分組這個概念的含義。
爲什麼要讀寫分離
- 因爲數據庫的“寫”(寫10000條數據到oracle可能要3分鐘)操作是比較耗時的。
- 但是數據庫的“讀”(從oracle讀10000條數據可能只要5秒鐘)。
- 所以讀寫分離,解決的是,數據庫的寫入,影響了查詢的效率。
什麼時候要讀寫分離
數據庫不一定要讀寫分離,如果程序使用數據庫較多時,而更新少,查詢多的情況下會考慮使用,利用數據庫 主從同步 。可以減少數據庫壓力,提高性能。當然,數據庫也有其它優化方案。memcache 或是 表折分,或是搜索引擎。都是解決方法。
讀寫分離實現
第一種:
代碼已上傳github : [email protected]:13849141963/mysql-maste-slave.git
1.首先我們需要配置多個數據源,我是用xml進行配置的其他方法大同小異,就是多建立了幾個數據源組件對象。[一主一從]
jdbc.properties
mysql.driverClassName.write = com.mysql.jdbc.Driver
mysql.url.write = jdbc:mysql://192.168.64.132:3306/slave?characterEncoding=utf8
mysql.username.write = root
mysql.password.write = root
mysql.driverClassName.read = com.mysql.jdbc.Driver
mysql.url.read = jdbc:mysql://192.168.64.135:3306/slave?characterEncoding=utf8
mysql.username.read = root
mysql.password.read = root
applicationContext.xml
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--創建寫數據源-->
<bean id="writeDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${mysql.driverClassName.write}"/>
<property name="url" value="${mysql.url.write}"/>
<property name="username" value="${mysql.username.write}"/>
<property name="password" value="${mysql.password.write}"/>
<!--初始化連接大小-->
<property name="initialSize" value="0"/>
<!--連接池的最大使用連接數量-->
<property name="maxActive" value="20"/>
<!--連接池的最小空閒-->
<property name="minIdle" value="0"/>
<!--獲取連接最大等待時間-->
<property name="maxWait" value="60000"/>
<property name="validationQuery"><value>SELECT 1</value></property>
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="testWhileIdle" value="true" />
<!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="25200000" />
<!-- 打開removeAbandoned功能 -->
<property name="removeAbandoned" value="true" />
<!-- 1800秒,也就是30分鐘 -->
<property name="removeAbandonedTimeout" value="1800" />
<!-- 關閉abanded連接時輸出錯誤日誌 -->
<property name="logAbandoned" value="true" />
<!-- 監控數據庫 -->
<!-- <property name="filters" value="stat" /> -->
<property name="filters" value="mergeStat" />
</bean>
<!--創建讀數據源-->
<bean id="readDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${mysql.driverClassName.read}"/>
<property name="url" value="${mysql.url.read}"/>
<property name="username" value="${mysql.username.read}"/>
<property name="password" value="${mysql.password.read}"/>
<!--初始化連接大小-->
<property name="initialSize" value="0"/>
<!--連接池的最大使用連接數量-->
<property name="maxActive" value="20"/>
<!--連接池的最小空閒-->
<property name="minIdle" value="0"/>
<!--獲取連接最大等待時間-->
<property name="maxWait" value="60000"/>
<property name="validationQuery"><value>SELECT 1</value></property>
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="testWhileIdle" value="true" />
<!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="25200000" />
<!-- 打開removeAbandoned功能 -->
<property name="removeAbandoned" value="true" />
<!-- 1800秒,也就是30分鐘 -->
<property name="removeAbandonedTimeout" value="1800" />
<!-- 關閉abanded連接時輸出錯誤日誌 -->
<property name="logAbandoned" value="true" />
<!-- 監控數據庫 -->
<!-- <property name="filters" value="stat" /> -->
<property name="filters" value="mergeStat" />
</bean>
2.使用AbstractRoutingDataSource 的實現類,進行靈活的切換,可以通過AOP或者手動編程設置當前的DataSource,不用修改我們編寫的對於繼承AbstractRoutingDataSource 的實現類的修改,這樣的編寫方式比較好,至於其中的實現原理,讓我細細到來。我們想看看如何去應用,實現原理慢慢的說!
private Map<Object, Object> targetDataSources;
private Object defaultTargetDataSource;
private Map<Object, DataSource> resolvedDataSources;
targetDataSources中保存了key和數據庫連接的映射關係,defaultTargetDataSource表示默認的鏈接,resolvedDataSources這個數據結構是通過targetDataSources構建而來,存儲的結構也是數據庫標識和數據源的映射關係。
下面需要繼承AbstractRoutingDataSource類,實現我們自己的數據庫選擇邏輯DynamicDataSource 類,先上代碼:
/****
* 產生動態數據源 AbstractRoutingDataSource是spring提供對數據源進行選擇的類
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/***
* 根據返回的key決定使用哪個數據源
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return ContextDBHolder.getDataSource();
}
}
3.創建ContextDBHolder類去設置數據源,使用ThreadLocal進行對數據源進行綁定。
public class ContextDBHolder {
//使用ThreadLocal進行綁定數據源key,同一個線程使用同一個數據源key
private static final ThreadLocal<String> t = new ThreadLocal<String>();
/****
* 設置數據源
* @param key
*/
public static void setDataSource(String key){
t.set(key);
}
/*****
* 返回數據源
*/
public static String getDataSource(){
return t.get();
}
/*****
* 釋放數據源
*/
public static void closeDataSource(){
t.remove();
}
}
4.創建註解 作用在方法上判斷是讀還是寫操作
/****
* 自定義註解類型:
* @Retention :元註解-->修飾註解的註解
* RetentionPolicy:SOURCE 源碼有效 編譯之後不會再.class文件中
* CLASS 編譯有效 編譯之後會留在.class文件中 運行時不生效
* RUNTIME 運行時有效
* @Target :作用修飾自定義註解類可以加在什麼位置
* ElementType:METHOD 使用在方法上
* TYPE 使用在類上
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Read {
}
5.通過aop切面進行動態選擇數據源
/****
* 作用:根據目標類中調用的方法往DynamicDataSource中的determineCurrentLookupKey方法動態設置返回值
* 目標類: save update delete 相關方法使用write
* query select 相關方法使用read
*/
@Component("chooseDBAdvice")
public class ChooseDBAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
//獲取方法對象
Method method = invocation.getMethod();
//判斷當前方上是否存在read註解
boolean annotationPresent = method.isAnnotationPresent(Read.class);
if(annotationPresent){
//設置讀數據源
ContextDBHolder.setDataSource("read");
System.out.println("設置讀數據源~~~~~~");
}else{
//設置寫數據源
ContextDBHolder.setDataSource("write");
System.out.println("設置寫數據源~~~~~~");
}
//執行目標方法
Object proceed = invocation.proceed();
//釋放數據源
ContextDBHolder.closeDataSource();
return proceed;
}
}
<context:component-scan base-package="com.zy.cn"/>
<!--具體實現該接口的 bean-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--創建寫數據源-->
<bean id="writeDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${mysql.driverClassName.write}"/>
<property name="url" value="${mysql.url.write}"/>
<property name="username" value="${mysql.username.write}"/>
<property name="password" value="${mysql.password.write}"/>
<!--初始化連接大小-->
<property name="initialSize" value="0"/>
<!--連接池的最大使用連接數量-->
<property name="maxActive" value="20"/>
<!--連接池的最小空閒-->
<property name="minIdle" value="0"/>
<!--獲取連接最大等待時間-->
<property name="maxWait" value="60000"/>
<property name="validationQuery"><value>SELECT 1</value></property>
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="testWhileIdle" value="true" />
<!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="25200000" />
<!-- 打開removeAbandoned功能 -->
<property name="removeAbandoned" value="true" />
<!-- 1800秒,也就是30分鐘 -->
<property name="removeAbandonedTimeout" value="1800" />
<!-- 關閉abanded連接時輸出錯誤日誌 -->
<property name="logAbandoned" value="true" />
<!-- 監控數據庫 -->
<!-- <property name="filters" value="stat" /> -->
<property name="filters" value="mergeStat" />
</bean>
<!--創建讀數據源-->
<bean id="readDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${mysql.driverClassName.read}"/>
<property name="url" value="${mysql.url.read}"/>
<property name="username" value="${mysql.username.read}"/>
<property name="password" value="${mysql.password.read}"/>
<!--初始化連接大小-->
<property name="initialSize" value="0"/>
<!--連接池的最大使用連接數量-->
<property name="maxActive" value="20"/>
<!--連接池的最小空閒-->
<property name="minIdle" value="0"/>
<!--獲取連接最大等待時間-->
<property name="maxWait" value="60000"/>
<property name="validationQuery"><value>SELECT 1</value></property>
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="testWhileIdle" value="true" />
<!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="25200000" />
<!-- 打開removeAbandoned功能 -->
<property name="removeAbandoned" value="true" />
<!-- 1800秒,也就是30分鐘 -->
<property name="removeAbandonedTimeout" value="1800" />
<!-- 關閉abanded連接時輸出錯誤日誌 -->
<property name="logAbandoned" value="true" />
<!-- 監控數據庫 -->
<!-- <property name="filters" value="stat" /> -->
<property name="filters" value="mergeStat" />
</bean>
<!--管理動態數據源-->
<bean id="dynamicDataSource" class="com.zy.cn.config.DynamicDataSource">
<property name="targetDataSources">
<map>
<entry key="write" value-ref="writeDataSource"/>
<entry key="read" value-ref="readDataSource"/>
</map>
</property>
<!--<property name="defaultTargetDataSource" value="writeDataSource"/>-->
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dynamicDataSource"/>
<property name="mapperLocations" value="com/zy/cn/mapper/*.xml"/>
<property name="typeAliasesPackage" value="com.zy.cn.entity"/>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.zy.cn.dao"></property>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dynamicDataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager" order="2"/>
<!-- 配置druid監控spring jdbc -->
<bean id="druid-stat-interceptor"
class="com.alibaba.druid.support.spring.stat.DruidStatInterceptor">
</bean>
<bean id="druid-stat-pointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut"
scope="prototype">
<property name="patterns">
<list>
<value>com.zy.cn.dao</value>
</list>
</property>
</bean>
<aop:config>
<aop:advisor advice-ref="druid-stat-interceptor"
pointcut-ref="druid-stat-pointcut" order="3"/>
</aop:config>
<!--配置數據源切面-->
<aop:config proxy-target-class="true">
<aop:pointcut id="pc" expression="execution(* com.zy.cn.service.*.*(..))"></aop:pointcut>
<aop:advisor advice-ref="chooseDBAdvice" pointcut-ref="pc" order="1"></aop:advisor>
</aop:config>
6.測試增加,修改,查詢可以看出動態切換數據源,說明讀寫分離搭建成功。
第二種:
Mycat 是一個強大的數據庫中間件,不僅僅可以用作讀寫分離、以及分表分庫、容災備份,而且可以用於多
租戶應用開發、雲平臺基礎設施、讓你的架構具備很強的適應性和靈活性,藉助於即將發佈的 Mycat 智能優化模
塊,系統的數據訪問瓶頸和熱點一目瞭然,根據這些統計分析數據,你可以自動或手工調整後端存儲,將不同的
表映射到不同存儲引擎上,而整個應用的代碼一行也不用改變.
架構圖如下:
搭建步驟 【在這裏我們只做讀寫分離】:和上面兩臺機器一樣
1.linux環境下安裝mycat數據庫中間件 下載地址:http://dl.mycat.io/ 加壓:tar -zxvf Mycat-server-1.6-RELEASE-20161028204710-linux.tar.gz
2.安裝jdk,版本需在1.7以及以上配置環境變量
3.修改mycat 中的conf文件夾下的server.xml,schema.xml文件 【標籤屬性請參看mycat官方文檔】
server.xml
mycat登錄的用戶名
<user name="root">mycat登錄的密碼
<property name="password">123456</property>
別名[隨便]
<property name="schemas">TESTDB</property>
<!-- 表級 DML 權限設置 -->
<!--
<privileges check="false">
<schema name="TESTDB" dml="0110" >
<table name="tb01" dml="0000"></table>
<table name="tb02" dml="1111"></table>
</schema>
</privileges>
-->
</user>
<user name="user">
<property name="password">user</property>
<property name="schemas">TESTDB</property>
該用戶只能進行讀操作
<property name="readOnly">true</property>
</user>
schema.xml
<mycat:schema xmlns:mycat="http://io.mycat/">
對應server.xml中schema標籤的值
<schema name="TESTDB" checkSQLschema="false" sqlMaxLimit="100" dataNode="dn1">
<table name="user" dataNode="dn1"/>
</schema>
<!-- 定義MyCat的數據節點 db1爲需要操作的數據庫-->
<dataNode name="dn1" dataHost="localhost" database="db1" />
<dataHost name="localhost" maxCon="1000" minCon="10" balance="3"
writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<!--配置後臺數據庫的IP地址和端口號,還有賬號密碼 -->
<!-- master負責寫 -->
<writeHost host="hostM1" url="192.168.64.137:3306" user="root"
password="root">
<!--slave負責讀-->
<readHost host="hostS2" url="192.168.64.136:3306" user="root" password="root" />
</writeHost>
</dataHost>
</mycat:schema>
4.啓動mycat中間件 bin目錄下啓動
./mycat start
5.查看日誌文件 log目錄下
tail -f -n 100 wrapper.log
6.連接mycat 用戶名密碼均爲mycat中server.xml中進行配置的
mysql -uroot -p123456 -h192.168.64.138 -P8066 -DTESTDB
7.測試: 在user表中name字段用主機IP,通過日誌查詢發現只會出現從機上的數據IP端口爲從機的端口,添加也是一樣的,
select * from user;