參考:
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 }
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>
3.測試成功
六.Django實現主從分離
配置很簡單:https://blog.csdn.net/linzi1994/article/details/82934612