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