一、業務發展驅動數據發展
隨着網站業務的不斷發展,用戶量的不斷增加,數據量成倍地增長,數據庫的訪問量也呈線性地增長。特別是在用戶訪問高峰期間,並發訪問量突然增大,數據庫的負載壓力也會增大,如果架構方案不夠健壯,那么數據庫服務器很有可能在高並發訪問負載壓力下宕機,造成數據訪問服務的失效,從而導致網站的業務中斷,給公司和用戶造成雙重損失。那么,有木有一種方案能夠解決此問題,使得數據庫不再因為負載壓力過高而成為網站的瓶頸呢?答案肯定是有的。
目前,大部分的主流關系型數據庫都提供了主從熱備功能,通過配置兩台(或多台)數據庫的主從關系,可以將一台數據庫服務器的數據更新同步到另一台服務器上。網站可以利用數據庫的這一功能,實現數據庫的讀寫分離,從而改善數據庫的負載壓力。
利用數據庫的讀寫分離,Web服務器在寫數據的時候,訪問主數據庫(Master),主數據庫通過主從復制機制將數據更新同步到從數據庫(Slave),這樣當Web服務器讀數據的時候,就可以通過從數據庫獲得數據。這一方案使得在大量讀操作的Web應用可以輕松地讀取數據,而主數據庫也只會承受少量的寫入操作,還可以實現數據熱備份,可謂是一舉兩得的方案。
二、MySQL數據復制原理
剛剛我們了解了關系型數據庫的讀寫分離能夠實現數據庫的主從架構,那么主從架構中最重要的數據復制又是怎么一回事呢?MySQL作為最流行的關系型數據庫之一,通過了解MySQL的數據復制流程,會使得我們對主從復制的認知會有一定的幫助。
從上圖來看,整體上有如下三個步湊:
(1)Master將改變記錄到二進制日志(binary log)中(這些記錄叫做二進制日志事件,binary log events);
(2)Slave將Master的二進制日志事件(binary log events)拷貝到它的中繼日志(relay log);
PS:從圖中可以看出,Slave服務器中有一個I/O線程(I/O Thread)在不停地監聽Master的二進制日志(Binary Log)是否有更新:如果沒有它會睡眠等待Master產生新的日志事件;如果有新的日志事件(Log Events),則會將其拷貝至Slave服務器中的中繼日志(Relay Log)。
(3)Slave重做中繼日志(Relay Log)中的事件,將Master上的改變反映到它自己的數據庫中。
PS:從圖中可以看出,Slave服務器中有一個SQL線程(SQL Thread)從中繼日志讀取事件,並重做其中的事件從而更新Slave的數據,使其與Master中的數據一致。只要該線程與I/O線程保持一致,中繼日志通常會位於OS的緩存中,所以中繼日志的開銷很小。
三、MySQL主從復制實戰
3.1 實驗環境總覽與准備工作
(1)實驗環境
①服務器環境:本次我們主要借助VMware Workstation搭建一個三台Windows Server 2003組成的MySQL服務器集群,其中一台作為Master服務器(IP:192.168.80.10),其余兩台均作為Slave服務器(IP:192.168.80.11,192.168.80.12)。
②客戶機環境:本次我們在Windows 7宿主機(IP:192.168.80.1)編寫一個C#控制台程序,對MySQL服務器進行基本的CRUD訪問測試。
(2)准備工作
下載MySQL文件:http://dev.mysql.com/downloads/mysql/5.5.html#downloads
這里我們選擇5.5版本,為了節省時間,直接選擇了Archive免安裝版本。又由於虛擬機中的Windows Server 2003是32位,所以選擇了32-bit的Archive版本進行使用。
下載完成后,將三個壓縮包分別拷貝至Master(IP:192.168.80.10)、Slave1(IP:192.168.80.11)及Slave2(IP:192.168.80.12)中。
3.2 配置MySQL主服務器
(1)將MySQL文件拷貝到Master服務器,並解壓到一個指定文件夾。這里我放在了:C:\MySQLServer\mysql-5.5.40-win32
(2)新建一個配置文件,取名為:my-master.ini,添加以下內容:
1 [client] 2 port=3306 3 default-character-set=utf8 4 5 [mysqld] 6 port=3306 7 8 #character_set_server=utf8 一定要這樣寫; 9 character_set_server=utf8 10 11 #解壓目錄 12 basedir=C:\MySQLServer\mysql-5.5.40-win32 13 14 #解壓目錄下data目錄,必須為data目錄 15 datadir=C:\MySQLServer\mysql-5.5.40-win32\data 16 17 #sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES 這個有問題,在創建完新用戶登錄時報錯 18 sql_mode=NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION 19 20 #主服務器的配置 21 #01.開啟二進制日志 22 log-bin=master-bin 23 #02.使用二進制日志的索引文件 24 log-bin-index=master.bin.index 25 #03.為服務器添加唯一的編號 26 server-id=1
(3)將my-master.ini傳送到Master服務器中mysql所在的文件夾中,並在命令行中將其注冊為Windows服務:(這里要轉到mysql的bin文件夾中進行操作,因為沒有設置環境變量)
(4)啟動mysql服務,並設為自啟動類型;
(5)使用root賬號登陸mysql,創建一個具有復制權限的用戶;(此時root是沒有密碼的,直接回車即可)
(6)在Slave1或Slave2上通過遠程登錄Master上的mysql測試新建用戶是否可以登錄;
3.3 配置MySQL從服務器
(1)同Master服務器,將MySQL文件拷貝解壓到指定文件夾下;
(2)新建一個配置文件,取名為:my-slave.ini,添加以下內容:
[client] port=3306 default-character-set=utf8 [mysqld] port=3306 #character_set_server=utf8 一定要這樣寫; character_set_server=utf8 #解壓目錄 basedir=C:\MySQLServer\mysql-5.5.40-win32 #解壓目錄下data目錄,必須為data目錄 datadir=C:\MySQLServer\mysql-5.5.40-win32\data #sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES 這個有問題,在創建完新用戶登錄時報錯 sql_mode=NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION #從服務器的配置 #01.為服務器添加唯一的編號 server-id=2 #02.開啟中繼日志 relay-log=slave-relay-log-bin #03.使用中繼日志的索引文件 relay-log-index=slave-relay-log-bin.index
PS:這里server-id要確保唯一,我們這里Master(192.168.80.10)的server-id=1,那么Slave1(192.168.80.11)就設置其server-id=2,Slave2(192.168.80.12)則設置其server-id=3。
(3)將my-slave.ini傳送到Slave1和Slave2服務器中mysql所在的文件夾中,並在命令行中將其注冊為Windows服務:(這里要轉到mysql的bin文件夾中進行操作,因為沒有設置環境變量)
(4)分別啟動兩台Slave的mysql服務,步湊同master所述;當然,也可以在cmd中輸入命令:net start MySQL
(5)分別使用兩台Slave的root賬號登陸mysql,通過指定的語句配置主從關系設置;
(6) 為了方便后面的測試,這里我們在Master上通過root進入mysql,創建一個測試用的數據庫和數據表;
(7)還要創建一個用戶,這個用戶具有對所有數據庫的增刪查改的權限,以便用來進行測試;
3.4 編寫C#程序測試主從復制結構
(1)下載mysql for .net開發包,添加對mysql.data.dll的引用
(2)在控制台程序中寫代碼訪問Master服務器,並查看程序運行結果;
①數據庫連接部分:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <connectionStrings> <add name="mysqlmaster" connectionString="server=192.168.80.10;database=dbtest;uid=sa;password=123456"/> </connectionStrings> </configuration>
②程序代碼部分:在程序中首先顯示user表內容(這時表是空的),然后會添加5條user信息,其中會修改第3條user信息的name為Edison Chou,最后會刪除第5條user信息;

static void Main(string[] args) { string connStr = ConfigurationManager.ConnectionStrings["mysqlmaster"] .ConnectionString; // 01.Query ShowUserData(connStr); // 02.Add a user to table for (int i = 0; i < 5; i++) { AddUserData(connStr, "TestUser" + (i + 1).ToString()); } ShowUserData(connStr); // 03.Update a user on table UpdateUserData(connStr, 3, "EdisonChou"); ShowUserData(connStr); // 04.Delete a user from table DeleteUserData(connStr, 5); ShowUserData(connStr); Console.ReadKey(); } #region 01.Func:ShowUserData private static void ShowUserData(string connStr) { using (MySqlConnection con = new MySqlConnection(connStr)) { con.Open(); using (MySqlCommand cmd = con.CreateCommand()) { cmd.CommandText = "select * from user"; using (MySqlDataReader reader = cmd.ExecuteReader()) { if (reader.HasRows) { Console.WriteLine("------------table:user------------"); while (reader.Read()) { Console.WriteLine(reader[0] + "-" + reader[1]); } Console.WriteLine("------------table:user------------"); } } } } } #endregion #region 02.Func:AddUserData private static void AddUserData(string connStr, string userName) { using (MySqlConnection con = new MySqlConnection(connStr)) { con.Open(); using (MySqlCommand cmd = con.CreateCommand()) { cmd.CommandText = "insert into user(name) values('" + userName + "')"; int result = cmd.ExecuteNonQuery(); if (result > 0) { Console.WriteLine("Add User Successfully."); } } } } #endregion #region 03.Func:UpdateUserData private static void UpdateUserData(string connStr, int userId, string userName) { using (MySqlConnection con = new MySqlConnection(connStr)) { con.Open(); using (MySqlCommand cmd = con.CreateCommand()) { cmd.CommandText = "update user set name='" + userName + "' where id=" + userId; int result = cmd.ExecuteNonQuery(); if (result > 0) { Console.WriteLine("Update User Successfully."); } } } } #endregion #region 04.Func:DeleteUserData private static void DeleteUserData(string connStr, int userId) { using (MySqlConnection con = new MySqlConnection(connStr)) { con.Open(); using (MySqlCommand cmd = con.CreateCommand()) { cmd.CommandText = "delete from user where id=" + userId; int result = cmd.ExecuteNonQuery(); if (result > 0) { Console.WriteLine("Delete User Successfully."); } } } } #endregion
③程序運行結果:
(3)在Slave1(192.168.80.11)和Slave2(192.168.80.12)上查看user表是否自動進行了數據同步;
①首先在Master上查看user表還剩哪些信息?
②其次在Slave1上查看user表是否進行了同步:
③最后在Slave2上查看user表是否進行了同步:
(4)初步嘗試讀寫分離:一主一從模式的一個最簡單的實現方式
①在Slave1上新建一個只具有讀(select)權限的用戶,這里取名為reader:
create user reader;
grant select on *.* to reader identified by '123456';
②新增一個mysqlslave的數據庫連接字符串:
<connectionStrings> <add name="mysqlmaster" connectionString="server=192.168.80.10;database=dbtest;uid=sa;password=123456"/> <add name="mysqlslave" connectionString="server=192.168.80.11;database=dbtest;uid=reader;password=123456"/> </connectionStrings>
③新增一個枚舉DbCommandType來記錄讀操作和寫操作:
public enum DbCommandType { Read, Write }
④修改讀取數據表的代碼判斷是讀操作還是寫操作:

private static void ShowUserData(DbCommandType commandType) { string connStr = null; if (commandType == DbCommandType.Write) { connStr = ConfigurationManager.ConnectionStrings["mysqlmaster"] .ConnectionString; } else { connStr = ConfigurationManager.ConnectionStrings["mysqlslave"] .ConnectionString; } using (MySqlConnection con = new MySqlConnection(connStr)) { con.Open(); using (MySqlCommand cmd = con.CreateCommand()) { cmd.CommandText = "select * from user"; using (MySqlDataReader reader = cmd.ExecuteReader()) { if (reader.HasRows) { Console.WriteLine("------------table:user------------"); while (reader.Read()) { Console.WriteLine(reader[0] + "-" + reader[1]); } Console.WriteLine("------------table:user------------"); } } } } }
PS:關於MySQL的讀寫分離實現,主要有以下幾種方式:
一種是基於MySQL-Proxy做調度服務器模式,另一種是借助阿里巴巴開源項目Amoeba(變形蟲)項目實現(這種方式貌似用的比較多),另外呢就是自己寫一個類似於哈希算法的程序庫來選擇目標數據庫;
學習小結
此次我們主要簡單地學習了主從復制的一些相關概念,了解了MySQL在Windows下搭建主從復制架構的過程,最后通過改變程序方式使得一主一從模式下實現讀寫分離(雖然是很簡單很粗陋的實現)。后續有空時,我會嘗試在Linux下借助阿里巴巴開源項目Amoeba搭建真正的MySQL讀寫分離模式,到時也會將搭建的過程分享出來。雖然,我沒有相關的真實實踐經驗,也有很多人跟我說“你這是在紙上談兵”,我也知道“紙上得來終覺淺,絕知此事要躬行”,但在沒畢業之前,我還是會做一些相關的初步了解性質的實踐學習,也許以后到了公司,就會有真正的戰場在等着我了。當然,如果你覺得我寫這篇博客花了點心思,那就麻煩點個贊,謝謝啦!
參考資料
(1)李智慧,《大型網站技術架構-核心原理與案例分析》:http://item.jd.com/11322972.html
(2)guisu,《高性能Mysql主從架構的復制原理及配置詳解》:http://blog.csdn.net/hguisu/article/details/7325124
(3)Ghost,《高性能的MySQL主從復制架構》:http://www.uml.org.cn/sjjm/201211061.asp
(4)飛鴻無痕,《Amoeba搞定MySQL讀寫分離》:http://blog.chinaunix.net/uid-20639775-id-154600.html (此文講解了如何借助Amoeba構建MySQL主從復制讀寫分離,值得閱讀)
附件下載
(1)mysql-5.5.40(Archive版本):http://pan.baidu.com/s/1c0u6X80
(2)相關配置文件(master與slave):http://pan.baidu.com/s/1dDENI73
(3)C#測試程序DEMO:http://pan.baidu.com/s/1kT42gAz