如今的系統大都是7 * 24不間斷運行的,對高可用的要求很高。除此以外,系統須要保證不一樣城市,甚至於不一樣國家的可用性。html
MySQL數據庫通常來講是主從結構,經過跨地域部署主從複製查詢能夠保證跨地域的可用性。可是寫操做一旦跨機房,網絡的延遲和不可靠可能會形成執行時間超長或者執行失敗。而這對於那些對高可用系統有跨地域的要求的系統來講,就是不能接受的了。mysql
mysql自己具有多主複製的能力,可是咱們不能在多個mysql上進行毫無顧忌的寫,一旦兩個mysql實例在同一個表的同一行記錄都進行了寫操做就會形成複製問題,或者是實例之間數據的不一致。sql
從MySQL 5.7.17開始,發佈了MySQL Group Replication,Group Replication具有了多主的能力,它是一種強一致的實現,多個MySQL實例可以實時保證數據的一致性,可是它對網絡延時要求高,並且比價適合於金融業務場景。一旦跨地域性能很是低,網絡的延遲和抖動甚至於會形成整個系統的不可用。數據庫
大多數配置了多主複製的mysql集羣,在業務上咱們也是寫一個點,或者嚴格按照主鍵把請求哈希路由到不一樣的實例上來寫。服務器
固然大多數業務都是選擇單點寫,這樣業務上好控制。當業務跨IDC部署時,跨IDC的寫操做延時就會很高,影響業務性能。目前多IDC的業務部署有如下幾種:網絡
這種模式能寫性能很是高,可是對業務程序的要求更高,不只增長了開發難度,並且部署成本會偏高,須要上層有對應的路由功能。並且一旦寫請求對應的數據不屬於本IDC,同樣須要跨機房路由到其餘機房去寫。
並且這種模式一旦擴展到3個IDC的時候,每每須要在MySQL上部署環形複製,這種模式極易形成數據的不一致。架構
這種模式下,只有一個真正的主庫,全部的寫操做必須落到這個主庫上,是最多見的模式。它不須要有路由來根據不一樣的key路由到不一樣的服務程序上,可是對於不在主庫所在的IDC的服務程序須要實現讀寫分離。
這種模式顧名思義非對稱,就是說不一樣IDC的服務程序部署有必定的差別性,一旦咱們須要切換主庫時不少狀況下就須要手工操做,而且容易形成切換時的數據不一致。
同時,因爲IDC2中的寫操做是跨機房的,因爲跨機房的網絡延時和丟包影響,會對應用程序的服務性能產生巨大的影響。寫操做過多,就不可取。運維
從圖中能夠看出,雖然採用代理層可以解決對稱部署,可是沒法解決跨機房寫延時的問題。代理層是一個支持多套MySQL集羣的中間件平臺,詳細瞭解請參考:(http://www.jianshu.com/p/bc50221972ca)。函數
目前MySQL的主從複製是一種強一致的數據庫部署方案,咱們公司內部有不少業務都是沒有強一致性需求,而更親賴於高可用性,須要容忍機房故障,在任何一個機房出現故障時,也不能影響服務的正常進行。因此這種場景,咱們通常採用最終一致性數據庫模型。性能
MyShard是一種最終一致性數據庫,它一個支持多主多寫模式的存儲,是一個跨機房的對等部署的典範,可是MyShard的問題在於系統太重,部署和運維都是一件很是頭痛的事情,目前咱們數據庫團隊已經再也不對外推廣MyShard了,只是對線上已有的MyShard仍是提供技術支持和維護。
可是目前咱們公司仍是有不少業務是對於這種多機房同時寫有強需求,如何解決這個問題呢?我在去年就提出要開發一套基於MySQL的多寫方案,最近花時間突擊研究了一下終於有了突破,基本上實現了想要的輕量級的多主MySQL集羣。
從MySQL5.7開始,有了通訊渠道的概念,每個通訊渠道都是一個從服務器從主服務器得到二進制日誌的連接。這意味着每一個通訊渠道都得有一個IO_THREAD .咱們須要運行不一樣的 「CHANGE MASTER」 命令, 對於每個主服務器。咱們須要用到 「FOR CHANNEL」這個參數來提供通訊連接的名字。
舉個例子:
MySQL > CHANGE MASTER to MASTER_HOST='127.0.0.1',MASTER_PORT=6301 , MASTER_USER='repl', MASTER_PASSWORD='repl',MASTER_AUTO_POSITION = 1 FOR CHANNEL="idc1"; MySQL > start slave for channel='idc1' ;
多主MySQL是基於MySQL 5.7.19改造,充分利用了多源複製功能,每一個MySQL實例都會成爲集羣中的其餘的實例的從庫。
改造後的MySQL是通用和多寫的混合版本,對於不一樣的表採用不一樣的策略,支持多寫的表有必定的限制:
一、存儲在__mm 數據庫下 二、必須添加一個 __version字段,而且是在表的第一個字段__version字段用來處理版本衝突,其中最後一個bit用來描述是否該記錄以刪除。 最後一位爲0:有效數據 最後一位爲1:刪除數據
在該模式下,各個IDC的應用程序,均可以往機房內的MySQL實例進行讀寫操做。
寫:HASET table set col1 = ? AND col2=? WHERE K1=? AND k2=? 若是對應主鍵數據不存在,就插入數據,存在就更新數據 刪:HADELETE FROM table where k1=? AND k2=? 若是對應主鍵數據不存在,插入一條刪除記錄,存在,就改成帶刪除標記的數據
mmversion(seed,delete_flag):函數代兩個參數,第一個參數是版本生成的種子,若是爲0,採用系統時間自動生成種子;第二個參數是刪除標記。
mysql> select mmversion(0,0) ; +--------------------------------+ | mmversion(0,0) | +--------------------------------+ | 3232157937589817394 | +--------------------------------+ 1 row in set (0.00 sec)
mmversion_is_local(version):該函數用來返回是不是在本實例產生
mysql> select mmversion_is_local(3232157937589817394) ; +-----------------------------------------+ | mmversion_is_local(3232157937589817394) | +-----------------------------------------+ | 1 | +-----------------------------------------+
注意:該版本沒有對MySQL基本的語法作任何邏輯上的變動,因此基本的查詢語法、DML語法都是可使用的,而且也能夠用在多寫的表上面。
CREATE TABLE `b` ( `__version` bigint(20) unsigned NOT NULL, `id` bigint(20) auto_increment NOT NULL, `name` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
表的第一個字段必須是__version字段,bigint類型。
下面咱們同過DML語法在這種多主表中的寫法:
mysql> insert into b ( __version, id, name) values ( mmversion(0, 0 ), mmuuid(), '123') ; Query OK, 1 row affected (0.04 sec) mysql> select last_insert_id() ; +---------------------+ | last_insert_id() | +---------------------+ | 1617051614604954626 | +---------------------+ 1 row in set (0.00 sec) mysql> select * from b where id= 1617051614604954626 ; +---------------------+---------------------+------+ | __version | id | name | +---------------------+---------------------+------+ | 3234103229209909252 | 1617051614604954626 | 123 | +---------------------+---------------------+------+ 1 row in set (0.00 sec)
按照最終一致性的原理,版本號大的會勝出,更新語法中必定要判斷版本號,並更新版本號:
mysql> update b set name='234',__version=mmversion(0,0) where id=1 and __version < mmversion(0,0) ; Query OK, 1 row affected (0.03 sec) Rows matched: 1 Changed: 1 Warnings: 0
也能夠支持複雜的update
mysql> update b set name='4444', __version=mmversion(0,0) where name like '1%' and __version < mmversion(0,0) ; Query OK, 1 row affected (0.03 sec) Rows matched: 1 Changed: 1 Warnings: 0
mysql> update b set __version=mmversion(0,1) where name like '44%' and __version < mmversion(0,1) ; Query OK, 1 row affected (0.07 sec) Rows matched: 1 Changed: 1 Warnings: 0
mysql> haset b set name='123' where id=1 ; Query OK, 1 row affected (0.03 sec) mysql> haset b set name='234' where id=2 ; Query OK, 1 row affected (0.03 sec) mysql> select * from b ; +--------------------------------+----+--------+ | __version | id | name | +--------------------------------+----+--------+ | 3232159101525960754 | 1 | 123 | | 3232159110115897394 | 2 | 234 | +--------------------------------+----+--------+ 2 rows in set (0.00 sec)
mysql> hadelete from b where id=1 ; Query OK, 2 rows affected (0.23 sec) mysql> select * from b ; +--------------------------------+----+--------+ | __version | id | name | +--------------------------------+----+--------+ | 3232159183130343475 | 1 | 123 | | 3232159110115897394 | 2 | 234 | +--------------------------------+----+--------+ 2 rows in set (0.01 sec)
hadelete不會實際的刪除一條記錄,而是把__version字段的刪除標記爲標位1。
mysql> select * from b where id=1 and __version % 2 = 0 ; Empty set (0.00 sec) mysql> select * from b where __version %2 = 0 ; +--------------------------+----+--------+ | __version | id | name | +--------------------------+----+--------+ | 3232159110115897394 | 2 | 234 | +--------------------------+----+--------+ 1 row in set (0.00 sec)
這種語法不建議開發人員使用,可能形成數據不一致,通常由dba清除數據用
delete from b where id=1 ;
咱們仍然可使用自增id,這要求在集羣中的不一樣MySQL實例要設置不一樣的偏移,例如:
實例1:
auto-increment-increment = 2 auto-increment-offset = 1
實例2:
auto-increment-increment = 2 auto-increment-offset = 2
插入數據:
mysql> insert into c ( __version, name ) values ( mmversion(-1,0), '444') ; Query OK, 1 row affected (0.03 sec) mysql> select last_insert_id() ; +------------------+ | last_insert_id() | +------------------+ | 1 | +------------------+ 1 row in set (0.00 sec) mysql> select * from c where id=1 ; +---------------------+----+------+ | __version | id | name | +---------------------+----+------+ | 3234105709553524740 | 1 | 444 | +---------------------+----+------+ 1 row in set (0.00 sec)
MySQL原生的語法和功能都沒有改變,因此咱們能夠利用事物來處理一些復瑣事情。
start transaction; select * from a where id=1 and __version < new_version for update ; if ( has key ) update a set xxxx = ffff, __version = new_version where id=1; else insert into a ( id, xxxx , __version) values ( 1, 'ffff' ,new_version); end commit transaction ;
小禮物走一走,來簡書關注我