隨着網站業務的不斷髮展,用戶量的不斷增長,數據量成倍地增加,數據庫的訪問量也呈線性地增加。特別是在用戶訪問高峯期間,併發訪問量忽然增大,數據庫的負載壓力也會增大,若是架構方案不夠健壯,那麼數據庫服務器頗有可能在高併發訪問負載壓力下宕機,形成數據訪問服務的失效,從而致使網站的業務中斷,給公司和用戶形成雙重損失。那麼,有木有一種方案可以解決此問題,使得數據庫再也不由於負載壓力太高而成爲網站的瓶頸呢?答案確定是有的。html
目前,大部分的主流關係型數據庫都提供了主從熱備功能,經過配置兩臺(或多臺)數據庫的主從關係,能夠將一臺數據庫服務器的數據更新同步到另外一臺服務器上。網站能夠利用數據庫的這一功能,實現數據庫的讀寫分離,從而改善數據庫的負載壓力。mysql
利用數據庫的讀寫分離,Web服務器在寫數據的時候,訪問主數據庫(Master),主數據庫經過主從複製機制將數據更新同步到從數據庫(Slave),這樣當Web服務器讀數據的時候,就能夠經過從數據庫得到數據。這一方案使得在大量讀操做的Web應用能夠輕鬆地讀取數據,而主數據庫也只會承受少許的寫入操做,還能夠實現數據熱備份,可謂是一箭雙鵰的方案。算法
剛剛咱們瞭解了關係型數據庫的讀寫分離可以實現數據庫的主從架構,那麼主從架構中最重要的數據複製又是怎麼一回事呢?MySQL做爲最流行的關係型數據庫之一,經過了解MySQL的數據複製流程,會使得咱們對主從複製的認知會有必定的幫助。sql
從上圖來看,總體上有以下三個步湊:數據庫
(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的緩存中,因此中繼日誌的開銷很小。ide
(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)中。
(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測試新建用戶是否能夠登陸;
(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)還要建立一個用戶,這個用戶具備對全部數據庫的增刪查改的權限,以便用來進行測試;
(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