用同一uuid作为两个字段的值_分库设计中的主键选择
在先前的文章《又拍網架構中的分庫設計》中,我有提到過MySQL分庫設計中的主鍵選擇問題。在這篇文章里我想對這個問題進行展開討論,以此作為對上一篇文章的一個補充。
前面提到又拍網采用了全局唯一的字段作為主鍵。比如拿照片表為例,雖然不同用戶的照片數據存放在不同的Shard(或者說MySQL節點/實例, 請參考《又拍網架構中的分庫設計》)上,但是每一張照片擁有整個站點唯一的ID作為標示。
為什么要全局唯一?
我們在對數據庫集群作擴容時,為了保證負載的平衡,需要在不同的Shard之間進行數據的移動,如果主鍵不唯一,我們就沒辦法這樣隨意的移動數據。起初,我們考慮采用組合主鍵來解決這個問題。一般會以user_id和一個自增的photo_id來作為主鍵,這的確能解決移動數據可能帶來的主鍵沖突問題,但是就像在“又拍網架構中的分庫設計”中描述的那樣當Shard之間的數據發生關系后,我們需要用更多的字段來組成主鍵以保證唯一性,因此主鍵的索引會變的很大,從而影響查詢性能,同時也會影響寫入性能。
其次,每個Shard由兩臺MySQL服務器組成,而這兩臺服務器采用master-master的復制方式,以保證每個Shard一直可寫。master-master復制方式必須保證在兩臺服務器上各自插入的數據有不同的主鍵,不然當復制到另外一臺時就會出現主鍵重復錯誤。如果我們保證主鍵全局唯一,就自然的解決了這個問題。在沒有采用數據拆分的設計當中,如果要用自增字段,可以參考這篇文章里的解決辦法。
可能的解決方案
UUID
或許可以采用UUID作為主鍵,但是UUID好長的一串,放在URL里好難看啊,有木有?當然這個不是關鍵所在,更重要的原因還是性能。UUID的生成沒有順序性,所以在寫入時,需要隨機更改索引的不同位置,這就需要更多的IO操作,如果索引太大而不能存放在內存中的話就更是如此。而UUID索引時,一個key需要32個字節(當然如果采用二進制形式存儲的話可以壓縮到16個字節),因此整個索引也會相對比較大。
MySQL自增字段
在單個MySQL數據庫的應用中一般設置一個自增的字段就可以了,而在水平分庫的設計當中,這種方法顯然不能保證全局唯一。那么我們可以單獨建立一個庫用來生成ID,在Shard中的每張表在這個ID庫中都有一個對應的表,而這個對應的表只有一個字段,這個字段是自增的。當我們需要插入新的數據,我們首先在ID庫中的相應表中插入一條記錄,以此得到一個新的ID,然后將這個ID作為插入到Shard中的數據的主鍵。這個方法的缺點就是需要額外的插入操作,如果ID庫變的很大,性能也會隨之降低。所以一定要保證ID庫的數據集不要太大,一個辦法是定期清理前面的記錄。
引入其它工具
Redis、Memcached等都支持原子性的increment操作,而且因為它們的優秀性能可以減少寫入時的額外開銷,也許我們可以拿它們當作序列生成器。Memcached的問題在于不持久性,所以我們不會考慮。而Redis也不是實時持久的,當然也可以配置成實時的,但那樣怪怪的。當然也有一些持久的工具,比如Kyoto Cabinet、Tokyo Cabinet、MongoDB等等,傳說中性能都不錯,但是引入其它工具會增加架構的復雜程度,也會增加維護成本。我們的團隊很小,精力有限,我們奉行夠用就好的原則,也就是沒有特別的原因,在可以接受的情況下,盡量用我們熟悉的工具解決問題。所以,我們還是來考慮一下怎么樣用MySQL來解決這個問題吧。
更好的方案
我們一開始就是采用了上面所描述的MySQL自增字段的方法,后來看到《Ticket Servers: Distributed Unique Primary Keys on the Cheap》這篇文章里所描述的方法,豁然開朗。我經常這樣想:如果沒有那些開源產品、沒有那些無私分享經驗的人,光憑我們自己的能力能做到什么程度。很感謝那些人,所以我也盡量多的分享一些自己的經驗。
我先描述一下Flickr那篇文章里所描述的方法,他們使用了REPLACE INTO這個MySQL的擴展功能。REPLACE INTO和INSERT的功能一樣,但是當使用REPLACE INTO插入新數據行時,如果新插入的行的主鍵或唯一鍵(UNIQUE Key)已有的行重復時,已有的行會先被刪除,然后再將新數據行插入。你可以放心,這是原子操作。
建立類似下面的表:
CREATE TABLE `tickets64` ( `id` bigint(20) unsigned NOT NULL auto_increment, `stub` char(1) NOT NULL default '', PRIMARY KEY (`id`), UNIQUE KEY `stub` (`stub`) ) ENGINE=MyISAM;
當需要獲得全局唯一ID時,執行下面的SQL語句:
REPLACE INTO `tickets64` (`stub`) VALUES ('a'); SELECT LAST_INSERT_ID();
第一次執行這個語句后,ticket64表將包含以下數據:
+--------+------+ | id | stub | +--------+------+ | 1 | a | +--------+------+
以后再次執行前面的語句,stub字段值為’a’的行已經存在,所以MySQL會先刪除這一行,再插入。因此,第二次執行后,ticket64表還是只有一行數據,只是id字段的值為2。這個表將一直只有一行數據。
Flickr為Photo, Group, Account, Task各自建立了一張ticket表以保持各自的ID的連續性。其它業務表的ID都使用同一個ticket表產生。
不錯吧,其實還可以更棒。比如,只需要一張ticket表就可以為所有的業務表提供各自連續的ID。下面,來看一下我們的方法。首先來看一下表結構:
CREATE TABLE `sequence` ( `name` varchar(50) NOT NULL, `id` bigint(20) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`name`) ) ENGINE=InnoDB;
注意區別,id字段不是自增的,也不是主鍵。在使用前,我們需要先插入一些初始化數據:
INSERT INTO `sequence` (`name`) VALUES ('users'), ('photos'), ('albums'), ('comments');
接下來,我們可以通過執行下面的SQL語句來獲得新的照片ID:
UPDATE `sequence` SET `id` = LAST_INSERT_ID(`id` + 1) WHERE `name` = 'photos'; SELECT LAST_INSERT_ID();
我們執行了一個更新操作,將id字段增加1,并將增加后的值傳遞到LAST_INSERT_ID函數,從而指定了LAST_INSERT_ID的返回值。
實際上,我們不一定需要預先指定序列的名字。如果我們現在需要一種新的序列,我們可以直接執行下面的SQL語句:
INSERT INTO `sequence` (`name`) VALUES('new_business') ON DUPLICATE KEY UPDATE `id` = LAST_INSERT_ID(`id` + 1); SELECT LAST_INSERT_ID();
這里,我們采用了INSERT … ON DUPLICATE KEY UPDATE這個MySQL擴展,這個擴展的功能也和INSERT一樣插入一行新的記錄,但是當新插入的行的主鍵或唯一鍵(UNIQUE Key)和已有的行重復時,會對已有行進行UPDATE操作。
需要注意的是,當我們第一次執行上面的語句時,因為還沒有name為’new_business’的字段,所以正常的執行了插入操作,沒有執行UPDATE,所以也沒有為LAST_INSERT_ID傳遞值。所以之后執行SELECT LAST_INSERT_ID()返回的值不可確定,要看當前連接在此之前執行過什么操作,如果沒有執行過會影響LAST_INSERT_ID值的操作,那么返回值將是0,不然就是該操作產生的值。所以,我們應該盡量避免使用這種方式。
UPDATE: 這個方法更容易解決單點問題,也不局限于兩個服務器,只要對不同的服務器設置不同的初始值(但必須是連續的),然后將增量變為服務器數就行了。
總結一下
我還是那句話,夠用就好。當然,也不是說就不要去了解其它產品、方案了。又拍網也在使用一些新興的產品,比如Redis(在10年3月就開始在正式環境下使用了,算是比較早的使用者),因為它的引入的確能夠更好、更方便、更高效的解決我們的某些問題。關鍵還是需要在使用前對其進行足夠的了解。我會在后面的文章中介紹一下Redis的使用情況。
總結
以上是生活随笔為你收集整理的用同一uuid作为两个字段的值_分库设计中的主键选择的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 文件 numpy_通过 Kaggle 入
- 下一篇: mysql知识测试_MySQL基础知识测