Mysql主從分離介紹及實現


參考:

  http://www.cnblogs.com/panxuejun/p/5887118.html

  https://www.cnblogs.com/alvin_xp/p/4162249.html

  https://blog.51cto.com/13555423/2068071

一.什么是Mysql主從分離

  將讀操作和寫操作分離到不同的數據庫上,避免主服務器出現性能瓶頸;主服務器進行寫操作時,不影響查詢應用服務器的查詢性能,降低阻塞,提高並發; 數據擁有多個容災副本,提高數據安全性,同時當主服務器故障時,可立即切換到其他服務器,提高系統可用性;

二.為什么要實現Mysql主從分離

  大型網站為了軟解大量的並發訪問,除了在網站實現分布式負載均衡,遠遠不夠。到了數據業務層、數據訪問層,如果還是傳統的數據結構,或者只是單單靠一台服務器扛,如此多的數據庫連接操作,數據庫必然會崩潰,數據丟失的話,后果更是 不堪設想。這時候,我們會考慮如何減少數據庫的聯接,一方面采用優秀的代碼框架,進行代碼的優化,采用優秀的數據緩存技術如:memcached,如果資金豐厚的話,必然會想到假設服務器群,來分擔主數據庫的壓力。

三.主從分離原理

 

  1.第一步:Master(主服務器)將操作記錄到binary log(二進制日志文件當中)【即每個事務更新數據完成之前先把操作記錄在日志文件中,Mysql將事務串行的寫入二進制日志文件中】,寫入日志文件完成之后,Master通知存儲引擎提交事務(注:對數據的操作成為一次二進制的日志事件【binary log event】);

  2.第二步:slave(從服務器)把binary log拷貝到relay log(中介日志)【相當於緩存作用,存儲在從服務器的緩存中】,首先slave會開始一個工作線程(I/O線程),I/O線程會在Master上打開一個普通的連接,然后讀取binary log事件,如果已經跟上master,就會睡眠,並等待Master產生新的事件,I/O線程將讀取的這些事件寫入到relay log;

  3.第三步:slave從做中介日志事件(relay log),sql線程讀取relay log事件並執行更新從服務器上的數據,使其與Master上的數據一致。

  總結:主服務器把操作記錄到binary log——>從服務器將binary log中的數據同步到relay log(中介日志中)——>從服務器讀取中介日志執行同步數據

四.數據庫主從配置

  注:可以是一主一從,一主多從,主從從等。

  我這里配置一主一從,自己有一台服務器,又借了室友一台服務器准備試試。

  1.兩個服務器:

    1.1配置主服務器(Master):

      打開binary log,配置mysql配置文件:

        (1)vim /etc/my.cnf(編輯服務器上的mysql配置文件,一般都存儲在這兒,如果沒有可以根據自己的配置文件):

        (2)配置打開binary log:

[mysqld]
#紅色的為新配置的打開binary log的配置 #配置binary log #配置server-id server-id=1 #打開二進制日志文件 log-bin=master-bin #打開二進制日志文件索引 log-bin-index=master-bin.index character_set_server=utf8 init_connect='SET NAMES utf8' datadir=/var/lib/mysql socket=/var/lib/mysql/mysql.sock # Disabling symbolic-links is recommended to prevent assorted security risks symbolic-links=0 # Settings user and group are ignored when systemd is used. # If you need to run mysqld under a different user or group, # customize your systemd unit file for mariadb according to the # instructions in http://fedoraproject.org/wiki/Systemd [mysqld_safe] log-error=/var/log/mariadb/mariadb.log pid-file=/var/run/mariadb/mariadb.pid

         

      (3)重啟mysql加載配置文件:service mysqld restart或者(etc/init.d/mysql stop 然后etc/init.d/mysql start)

        注:如果用的是MariaDB,會發生如下錯誤(Failed to start mysql.server.service: Unit not found.)則需要用systemctl restart mariadb.service啟動(參照:https://www.cnblogs.com/yuanchaoyong/p/9749060.html

        再注:systemctl和service是Linux服務管理的兩種方式,service命令其實是去/etc/init.d目錄下,去執行相關程序,systemd是Linux系統最新的初始化系統(init),作用是提高系統的啟動速度,盡可能啟動較少的進程,盡可能更多進程並發啟動。systemd對應的進程管理命令是systemctl

           systemctl命令兼容了service(即systemctl也會去/etc/init.d目錄下,查看,執行相關程序),並且systemctl命令管理systemd的資源Unit(systemd的Unit放在目錄/usr/lib/systemd/system(Centos)或/etc/systemd/system(Ubuntu))

      (4)SHOW MASTERT STATUS;

         會查看到第一個二進制日志文件:

      

    1.2從服務器配置:

      (1)同理進入mysql配置文件:vim /etc/my.cnf:

      (2)配置relay log:

 [mysqld]
  #配置relay log #配置server id server-id=2 #打開從服務器中介日志文件 relay-log=slave-relay-bin #打開從服務器中介日志文件索引 relay-log-index=slave-relay-bin.index
  datadir=/var/lib/mysql
  socket=/var/lib/mysql/mysql.sock
 
 # Disabling symbolic-links is recommended to prevent assorted security risks
  symbolic-links=0
 
  # Recommended in standard MySQL setup
  sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
  
  [mysqld_safe]
  log-error=/var/log/mysqld.log
  pid-file=/var/run/mysqld/mysqld.pid

        

      (3)同理,保存配置重啟mysql加載配置文件生效

 

     1.3相關連接配置:

      (1)在主服務器上(Master)創建一個用於用於從服務器連接並賦予其所有數據庫所有表的權限:

          創建用戶:create user 用戶名;

          賦予連接權限:GRANT REPLICATION SLAVE ON *.* TO 'repl'@'從服務器IP' IDENTIFIED BY '密碼'

          刷新:flush privileges;

 

       (2)從服務器建立連接(使用主服務器創建的repl用戶及密碼): 

          建立連接:change master to master_host='主服務器IP',master_port=主服務器MYSQL端口,master_user='用戶名',master_password='密碼',master_log_file='master-bin.000001',master_log_pos=0;

          讀取master-bin.000001文件,即主服務器的第一個日志文件,master_log_pos的作用是如果從服務器掛掉后,只要記得這個master_log_pos的大小,然后賦值就能恢復同步在某個時刻。

          開啟主從跟蹤:start slave;

          相應得關閉主從跟蹤:stop slave;

           查看從服務器show slave status \G:

            這里注意狀態是否正確,有可能連接不正確(如主服務器配置文件種有blind-address只能指定ip訪問數據庫,權限未賦予正確,防火牆等等),我最后遇到的原因是阿里雲服務器端口沒有打開3306端口,打開即可。

 

 

  2.測試:

    主服務器創建一個數據庫:

      

    從服務器也創建成功:

 

     

      

五.Spring代碼讀寫分離

  1.Spring中的AbstractRoutingDataSource(Eclipce中Ctrl+Shift+T搜索jar包)

    

     EclipceCtrl+O查看類中所有方法及屬性:查看AbstractRoutingDataSource中的determineTargetDataSource()方法:

      

  2.相關實現:

    DynamicDataSourceHolder:

 1 package com.swpu.o2o.dao.split;
 2 
 3 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
 4 /**
 5  * 繼承AbstractRoutingDataSource實現抽象方法
 6  * @author ASUS
 7  *
 8  */
 9 public class DynamicDataSource extends AbstractRoutingDataSource{
10 
11     @Override
12     protected Object determineCurrentLookupKey() {
13         return DynamicDataSourceHolder.getDbType();
14     }
15     
16 
17 }

 

    DynamicDataSource:

 1 package com.swpu.o2o.dao.split;
 2 
 3 import org.slf4j.Logger;
 4 import org.slf4j.LoggerFactory;
 5 
 6 
 7 
 8 public class DynamicDataSourceHolder {
 9     //這是日志模塊
10     private static Logger logger =  LoggerFactory.getLogger(DynamicDataSourceHolder.class);
11     //線程安全的ThreadLocal模式
12     private static ThreadLocal<String> contextHolder=new ThreadLocal<String>();
13     //兩個key,主服務器,從服務器
14     private static final String DB_MASTER="master";
15     public static final String DB_SLAVE="slave";
16     /**
17      * 獲取線程的DbType
18      * @return db
19      */
20     public static String getDbType(){
21         
22         String db=contextHolder.get();
23         //如果為空,默認為master,即支持寫,也支持讀
24         if(db==null){
25             db=DB_MASTER;
26         }
27         return db;
28     }
29     /**
30      * 設置線程的dbType
31      * @param str
32      */
33     public static void setDbType(String str){
34         logger.debug("所使用的數據源是:"+str);
35         contextHolder.set(str);
36         
37     }
38     /**
39      * 清理連接類型
40      */
41     public static void DbType(){
42         contextHolder.remove();
43     }
44 }

    mybatis攔截器:攔截mybatis傳遞進來的sql信息,根據sql信息選擇數據源,如寫的數據源【master】(update,insert等),讀的數據源【slave】(select)

 1 package com.swpu.o2o.dao.split;
 2 
 3 import java.util.Locale;
 4 import java.util.Properties;
 5 
 6 import org.apache.ibatis.executor.Executor;
 7 import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
 8 import org.apache.ibatis.mapping.BoundSql;
 9 import org.apache.ibatis.mapping.MappedStatement;
10 import org.apache.ibatis.mapping.SqlCommandType;
11 import org.apache.ibatis.plugin.Interceptor;
12 import org.apache.ibatis.plugin.Intercepts;
13 import org.apache.ibatis.plugin.Invocation;
14 import org.apache.ibatis.plugin.Plugin;
15 import org.apache.ibatis.plugin.Signature;
16 import org.apache.ibatis.session.ResultHandler;
17 import org.apache.ibatis.session.RowBounds;
18 import org.slf4j.Logger;
19 import org.slf4j.LoggerFactory;
20 import org.springframework.transaction.support.TransactionSynchronizationManager;
21 
22 /**
23  * 攔截器,實現mybatis中的Interceptor接口
24  * 
25  * @author ASUS
26  *
27  */
28 //指定攔截類型,mybatis會把增刪改封裝到update中
29 @Intercepts({@Signature(type=Executor.class,method="update",args={MappedStatement.class,Object.class}),
30     @Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class})})
31 public class DynamicDataSourceInterceptor implements Interceptor {
32     //日志
33     private static Logger logger =  LoggerFactory.getLogger(DynamicDataSourceInterceptor.class);
34 
35     // 匹配的正則表達(增刪改\u0020表示空格)
36     private static final String REGEX = ".*insert\\u0020.*|.*delete \\u0020.*|.*update\\u0020.*";
37 
38     // 主要攔截方法
39     @Override
40     public Object intercept(Invocation invocation) throws Throwable {
41         // 判斷當前是不是事務的
42         boolean synchronizationActive = TransactionSynchronizationManager.isActualTransactionActive();
43         Object[] objects = invocation.getArgs();
44         MappedStatement ms = (MappedStatement) objects[0];
45         String lookupKey = DynamicDataSourceHolder.DB_MASTER;
46         if (synchronizationActive != true) {
47             // 是否為讀方法
48             if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
49                 // selectKey為自增id查詢主鍵(SELECT_KEY_SUFFIX())方法,使用主庫(更新)
50                 if (ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
51                     lookupKey = DynamicDataSourceHolder.DB_MASTER;
52                 } else {
53                     // 格式化sql語句
54                     BoundSql boundsql = ms.getSqlSource().getBoundSql(objects[1]);
55                     String sql = boundsql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " ");
56                     // 使用正則匹配是否是增刪改
57                     if (sql.matches(REGEX)) {
58                         lookupKey = DynamicDataSourceHolder.DB_MASTER;
59 
60                     } else {
61                         lookupKey = DynamicDataSourceHolder.DB_SLAVE;
62                     }
63                 }
64             }
65 
66         } else {
67             lookupKey = DynamicDataSourceHolder.DB_SLAVE;
68         }
69         logger.debug("設置方法[{}]use [{}] Strategy,SqlCommanType[{}]",ms.getId(),lookupKey,ms.getSqlCommandType());
70         return invocation.proceed();
71     }
72 
73     // 返回封裝好的對象或代理對象
74     @Override
75     public Object plugin(Object target) {
76         // 如果是mybatis中的Executor對象(增刪改查),就通過intercept()封裝返回,否則直接防回
77         if (target instanceof Executor) {
78             return Plugin.wrap(target, this);
79         } else {
80             return target;
81         }
82     }
83 
84     // 設置相關代理,不是必備的
85     @Override
86     public void setProperties(Properties arg0) {
87         // TODO Auto-generated method stub
88 
89     }
90 
91 }
View Code  

    mybatis配置文件中配置攔截器:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <!DOCTYPE configuration 
 3  PUBLIC "-//mybatis.org//DTD Config 3.0//EN" 
 4  "http://mybatis.org/dtd/mybatis-3-config.dtd">
 5 <configuration>
 6     <!-- 配置全局屬性 -->
 7     <settings>
 8         <!-- 使用jdbc的getGeneratedKeys獲取數據庫自增主鍵值 -->
 9         <setting value="true" name="useGeneratedKeys" />
10         <!-- 使用列別名替換列名 默認:true -->
11         <setting value="true" name="useColumnLabel" />
12         <!-- 開啟駝峰命名轉換:Table{create_time} -> Entity{createTime} -->
13         <setting value="true" name="mapUnderscoreToCamelCase" />
14         <!-- 打印查詢語句 -->
15     </settings>
16     <!-- 配置mybatis攔截器 -->
17     <plugins>
18         <plugin interceptor="com.swpu.o2o.dao.split.DynamicDataSourceInterceptor"></plugin>
19     </plugins>
20 </configuration>

 

    配置datasource:

       mysql的相關信息(主從數據庫):

       datasource及動態數據源相關配置(選擇不同數據源):

 1 <bean class="com.mchange.v2.c3p0.ComboPooledDataSource" abstract="true"
 2     destroy-method="close" id="abstractDataSource">
 3     <!-- 配置連接池屬性 -->
 4     <property name="driverClass" value="${jdbc.driver}" />
 5     <property name="jdbcUrl" value="${jdbc.url}" />
 6     <property name="user" value="${jdbc.username}" />
 7     <property name="password" value="${jdbc.password}" />
 8     <!-- c3p0連接池的私有屬性 -->
 9     <property name="maxPoolSize" value="30" />
10     <property name="minPoolSize" value="10" />
11     <!-- 關閉連接后不自動commit -->
12     <property name="autoCommitOnClose" value="false" />
13     <!-- 獲取連接超時時間 -->
14     <property name="checkoutTimeout" value="10000" />
15     <!-- 當獲取連接失敗重試次數 -->
16     <property name="acquireRetryAttempts" value="2" />
17 </bean>
18 <!-- 主服務器數據源,繼承abstractDataSource,這里主庫從庫密碼一致 -->
19 <bean id="master" parent="abstractDataSource">
20     <property name="driverClass" value="${jdbc.driver}" />
21     <property name="jdbcUrl" value="${jdbc.masterurl}" />
22     <property name="user" value="${jdbc.username}" />
23     <property name="password" value="${jdbc.password}" />
24 </bean>
25 <!-- 從庫先關信息 -->
26 <bean id="slave" parent="abstractDataSource">
27     <property name="driverClass" value="${jdbc.driver}" />
28     <property name="jdbcUrl" value="${jdbc.slaveurl}" />
29     <property name="user" value="${jdbc.username}" />
30     <property name="password" value="${jdbc.password}" />
31 </bean>
32 <!-- 配置動態數據源,targetDataSouece就是lu'you數據源鎖對應的名稱 -->
33 <bean id="dynamicDataSource" class="com.swpu.o2o.dao.split.DynamicDataSource">
34     <property name="targetDataSource">
35         <map>
36         <!-- value-ref和datasource名字保持一致,key和DynamicDataSourceHolder中的kookupKey保持一致 -->
37             <entry value-ref="master" key="master"></entry>
38             <entry value-ref="slave" key="slave"></entry>
39 
40         </map>
41     </property>
42 </bean>
43 <!-- 放入連接池,做懶加載 -->
44 <bean id="dataSource" class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy">
45 <property name="targetDataSource">
46 <ref bean="dynamicDataSource"/></property>
47 </bean>
View Code

 

 

  3.測試成功

六.Django實現主從分離

  配置很簡單:https://blog.csdn.net/linzi1994/article/details/82934612

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM