這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
周末花了一晚上的時間,用Go寫了一個ID產生服務,Github地址:go-id-alloc。
分布式ID產生,就我來看主要是2個流派,各有利弊,沒有完美的實現。
1,snowflake流派。
它用於twitter的微博ID,因為是timeline按發布時間排序,所以這個演算法是用毫秒時間戳記作為ID的左半部,從而可以實現按時間有序。
像新浪微博也是在使用類似的ID產生演算法,snowflake的好處是去中心化,但是依賴時鐘的準確性,最差的情況是時鐘發生了回退,那麼ID就會重複;而如果不開啟NTP同步時鐘,那麼不同節點分配的時間不同,也會影響feed流的排序,所以在我看來只能說基本可用,一旦時鐘回退比較大的區間,服務是完全停用。美團在這方面做了一些工作,主要還是在發現回退以及警示方面的事情,可以參考:Leaf — 美團點評分布式ID產生系統。
2,mysql流派。
該流派使用廣泛,基本原理就是mysql的自增主鍵。最初為了擴充性能,會通過部署多台mysql,為每個mysql設定不同的起始id,從而實現橫向擴充性。
mysql支援自訂表格的auto_increment屬性,可以用於控制起始ID:
Transact-SQLCREATE TABLE `partition_1` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `meanless` tinyint(4) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `meanless` (`meanless`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;ALTER TABLE `partition_1` auto_increment=1;
12345678 |
CREATE TABLE `partition_1` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `meanless` tinyint(4) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `meanless` (`meanless`)) ENGINE=InnoDB DEFAULT CHARSET=utf8; ALTER TABLE `partition_1` auto_increment=1; |
ALTER修改了parition_1表的起始ID=1,不同的表可以設定不同的起始ID,例如給partition_2設定auto_increment=2。
僅僅這樣是不夠的,因為預設partition_1的下一個自增ID=2,會和partition_2表分配的ID重複。
mysql提供了另外一個配置,叫做auto_increment_increment,可以在mysql session級設定(set auto_increment_increment=xxx;),也可以設定給整個mysql執行個體。該配置用於控制步長,在上述例子中我會設定auto_increment_increment=2,那麼2個partition表的ID分配情況如下:
你會發現,步長設定為分區的個數,就可以避免ID衝突,整體向更大的ID共同增長了。
那麼如何分配下一個ID呢?一般insert新紀錄會產生新的ID,而這樣會導致資料規模增長,更好的方法是使用replace命令:
Shellreplace into partition_0(`meanless`) values(0)
1 |
replace into partition_0(`meanless`) values(0) |
因為meanless唯一鍵的原因,id欄位會自增,同時最多隻會產生一條記錄。
在我的go-id-alloc項目中,就是採用了這樣的方式實現ID自增,但是僅僅這樣還是不夠的。資料庫更新畢竟存在一個效能的瓶頸,在請求壓力更大的業務情境下終將成為一個瓶頸。
新的方案基於mysql自增原理實現,通過”號段”批量擷取的方式,將資料庫的寫入壓力降低為忽略不計,下面說明其原理。
仍舊以上面的分配布局為例,兩個mysql分別產生如下的ID序列:
現在我假設一個號段長度10000,那麼當replace產生了ID=1的時候,表示分配得到了[0, 10000)這個號段。同樣的,當再次replace時產生ID=3,那麼表示分配得到了[20000, 30000)這個號段。
將基於號段的序列重新整理,就會像下面這樣:
- [0, 10000),[20000, 30000),[40000,50000),[60000,70000),[80000,90000)
- [10000,20000),[30000,40000),[50000,60000),[70000,80000),[90000,100000)
觀察出規律了嗎?若分配得到的ID是N,那麼號段就是[(N – 1) * SIZE, N * SIZE)。
有了號段,我們只需要寫一個服務,每次向mysql分配一個ID,也就得到了一個獨佔的號段,接下來的ID分配請求可以直接從記憶體中的號段擷取。另外,應當在記憶體裡的號段消耗殆盡之前,向mysql擷取新的號段。
通過提升號段的SIZE,我們可以減少訪問資料庫的頻率,從而提升整個ID分配的服務能力。
mysql故障
因為ID序列儲存在mysql,因此Mysql遺失資料就變得不可容忍。一般我們有mysql的master-slave模式來即時備份資料,但是畢竟主從存在延遲,主庫宕機可能導致最新的更新沒有同步給從庫,那麼就會導致再次分配ID產生重複。
沒錯,這是mysql方案的劣勢,就像snowflake有其自身的劣勢一樣。但是,通常這個問題可以避免,通過設定更大的號段SIZE,我們可以確保在有限的主從延遲時間內(比如1分鐘的延遲),根據業務的請求量,最多隻會丟失N次replace產生的ID自增。在這種假設下,我們可以令從庫的起始ID跳過一定數量的步長,確保它不會重複。
使用情境
snowflake方案適合時間有序的情境,並且外界無法猜測一天的ID分配總量,從而無法猜測某個公司的業務量;缺點是時間回退服務就會不可用。
mysql方案適合內部業務,對ID有更多的控制能力(比如定義起始ID),擴充性很強,能滿足任何體量的業務規模;缺點是依賴mysql,另外ID存在規律,容易暴露公司業務量。