Scadb是一個(gè)分布式MySQL中間件,目前它支持的SQL語句是MySQL的一個(gè)子集,而且也沒打算創(chuàng)造獨(dú)特的語法出來,這樣方便業(yè)務(wù)方可以在MySQL和Scadb之間無縫的切換。那么Scadb是否能夠支持自增id呢?
一、Mongodb的ObjectID的啟示
我們首先在Mongodb中產(chǎn)生一個(gè)ObjectID,
》 db.test.insert({foo:'bar'})
WriteResult({ "nInserted" : 1 })
》db.test.findOne()
{ "_id" : ObjectId("59197e923879ad7925b2a3f0"), "foo" : "bar" }
Mongodb沒有支持自增id,缺省的文檔都會(huì)產(chǎn)生一個(gè)ObjectID,是唯一ID,Mongodb的ObjectID格式如下所示,一個(gè)ObjectID主要由4個(gè)部分組成:
95----------63---------39--------23-----0
—————————————^
時(shí)間戳 --------機(jī)器 -----進(jìn)程 --自增計(jì)數(shù)器
時(shí)間戳:將剛才生成的objectid的前4位進(jìn)行提取“59197e92”,十進(jìn)制為1494843026,這是我們產(chǎn)生數(shù)據(jù)時(shí)候的時(shí)間戳;
- 機(jī)器:接下來的三個(gè)字節(jié)就是“3879ad”,這三個(gè)字節(jié)是主機(jī)信息,一般的實(shí)現(xiàn)為組合所有的網(wǎng)卡信息成一個(gè)字符串,然后進(jìn)行計(jì)算散列值;
- PID:上面的機(jī)器信息是為了確保在不同機(jī)器產(chǎn)生的objectId不沖突,而pid就是為了在同一臺(tái)機(jī)器不同的mongodb進(jìn)程產(chǎn)生了objectId不沖突,接下來的“7925”兩位就是產(chǎn)生objectId的進(jìn)程標(biāo)識(shí)符,我們知道linux操作系統(tǒng)進(jìn)程id是int型,所以兩個(gè)字節(jié)是存儲(chǔ)不下的,mongodb這里也是采用了進(jìn)程信息的散列值然后取2個(gè)byte。
- 自增計(jì)數(shù)器:前面的九個(gè)字節(jié)是保證了一秒內(nèi)不同機(jī)器不同進(jìn)程生成objectId不沖突,這后面的三個(gè)字節(jié)“b2a3f0”是一個(gè)自動(dòng)增加的計(jì)數(shù)器,用來確保在同一秒內(nèi)產(chǎn)生的objectId也不會(huì)發(fā)現(xiàn)沖突,只要一個(gè)進(jìn)程一秒內(nèi)產(chǎn)生的數(shù)據(jù)少于16777216,就能保證記錄的唯一性。
ObjectId的這個(gè)主鍵生成策略,很好地解決了在分布式環(huán)境下高并發(fā)情況主鍵唯一性問題,值得學(xué)習(xí)借鑒。Scadb也是一種分布式數(shù)據(jù)庫,無法實(shí)現(xiàn)自增id。那么在設(shè)計(jì)之初,本來也考慮參考Mongodb的唯一ID方案,但是ObjectID是一個(gè)12個(gè)字節(jié)的數(shù)值,而Scadb是MySQL的中間件,MySQL沒有一個(gè)一種合適的類型的來支持12個(gè)字節(jié),當(dāng)然,我們可以考慮用字符串,但是也從性能方面考慮,于是決定要采用整形BIGINT來存儲(chǔ)唯一ID。
二、Scadb如何支持唯一ID
從上面的分析我們知道,Mongodb采用了3個(gè)byte存儲(chǔ)機(jī)器ID,2個(gè)字節(jié)存儲(chǔ)進(jìn)程ID,就是為了保證每一個(gè)啟動(dòng)的Mongodb的進(jìn)程能夠產(chǎn)生一個(gè)唯一的進(jìn)程信息(其實(shí)根據(jù)前面講解的原理,還是無法100%保證進(jìn)程標(biāo)識(shí)的唯一性);
與此同時(shí)Scadb還要把整個(gè)ID號(hào)存儲(chǔ)在一個(gè)BIGINT也就是8個(gè)字節(jié)的整數(shù)中,所以這種方案行不通。但是思路可以借鑒,我們只需要保證每個(gè)啟動(dòng)的進(jìn)程有一個(gè)唯一的ID號(hào)。
進(jìn)程號(hào)生成
由于Scadb的server進(jìn)程不是多租戶模式,也即是說所以我們只需要保證服務(wù)于一個(gè)業(yè)務(wù)的Scadb Server進(jìn)程能夠有一個(gè)唯一的進(jìn)程號(hào)即可。Scadb的配置是基于Zookeeper的,這樣我們很容易基于Zookeeper來生成一個(gè)唯一的進(jìn)程號(hào)。
假設(shè)現(xiàn)在我們服務(wù)的業(yè)務(wù)名是test,那么它在zookeeper上的主路徑是:/scadb/businesses/test,創(chuàng)建一個(gè)子目錄:/scadb/businesses/test/runners。
在該子目錄下,我們讓每個(gè)Scadb server在啟動(dòng)的時(shí)候,在/scadb/businesses/test/runners目錄下創(chuàng)建一個(gè)臨時(shí)節(jié)點(diǎn),節(jié)點(diǎn)創(chuàng)建成功,那么這個(gè)節(jié)點(diǎn)就作為Scadb server的進(jìn)程號(hào)。由于是臨時(shí)節(jié)點(diǎn),一旦進(jìn)程被殺死或者因?yàn)槟撤N原因掛了,那么這個(gè)節(jié)點(diǎn)也會(huì)自動(dòng)消失。
具體的邏輯為:
1、啟動(dòng)時(shí),隨機(jī)一個(gè)整數(shù)runnerId;
2、創(chuàng)建臨時(shí)節(jié)點(diǎn):/scadb/businesses/test/runners/${runnerId}
3、如果創(chuàng)建成功,結(jié)束;否則runnerId = (runnerId + 1) % 512,重復(fù)第2步。
唯一ID生成
有了進(jìn)程號(hào)以后,那么唯一ID的生成就很簡單了,Scadb的唯一ID的格式為:
63-62---------30----28------19--------- 0
—————————————
預(yù)留 時(shí)間戳 預(yù)留 進(jìn)程號(hào) 自增計(jì)數(shù)器
最高位(63):我預(yù)留了,主要是因?yàn)镴ava等語言對(duì)無符號(hào)64位整數(shù)支持不友好,所以這一位沒有使用;
時(shí)間戳(31-62):占用4個(gè)字節(jié);
預(yù)留2位(30,29):主要考慮到以后其他的可擴(kuò)展的功能;
進(jìn)程號(hào)(20-28):占用了9位,每個(gè)業(yè)務(wù)可以支持512個(gè)進(jìn)程,我壓測過一個(gè)scadb可以支持5萬次的普通數(shù)據(jù)庫訪問操作,所以512個(gè)進(jìn)程對(duì)于一個(gè)業(yè)務(wù)來說夠用了;
自增計(jì)數(shù)器(0-19):占用了20位,可以支持每秒1048576次不同值的生成,對(duì)于一個(gè)進(jìn)程來說,這個(gè)數(shù)已經(jīng)是非常高了。
具體代碼不在這里列舉了,大家可以參考源碼中的ScadbUUID類。
三、Scadb唯一ID實(shí)戰(zhàn)
首選我們創(chuàng)建一個(gè)有自增字段的表,如下所示:
mysql> /!scadb:partitionkey=id rule=rule10/CREATE TABLE
b(
->idbigint(20) NOT NULL AUTO_INCREMENT,
->namevarchar(50) COLLATE utf8_bin DEFAULT NULL,
->tdatetime DEFAULT NULL,
-> PRIMARY KEY (id)
-> ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin
-> ;
Query OK, 0 rows affected (0.92 sec)
注意:
- 1.雖然Scadb不支持唯一ID,但是為了保持與MySQL語法的統(tǒng)一性,這里仍然采用了AUTO_INCREMENT,這樣b表就是一個(gè)支持唯一ID的分區(qū)表了;
- 2.這里自增字段只能是bigint型,不支持int型;
我們可以通過下面的語法來查看表的結(jié)構(gòu):
mysql> show create table b ;
+-------+------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+-------+------------------------------------------------------------------------------------------------------------------+
| b | /!scadb:partitionkey=id rule=rule10/CREATE TABLEb(
idbigint(20) NOT NULL AUTO_INCREMENT,
name varchar(50) COLLATE utf8_bin DEFAULT NULL,
t datetime DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin |
+-------+----------------------------------------------------------------------------------------------------------------+
1 row in set (0.01 sec)
現(xiàn)在我們插入一條記錄:
mysql> insert into b ( name ) values ( '123') ;
Query OK, 1 row affected (0.12 sec)
由于插入時(shí)沒有指定ID,那么Scadb會(huì)自動(dòng)為我們產(chǎn)生一個(gè)ID號(hào),我們查詢一下上次產(chǎn)生的自增ID:
mysql> select last_insert_id() ;
+---------------------+
| last_insert_id() |
+---------------------+
| 3208787689176782285 |
+---------------------+
1 row in set (0.03 sec)
現(xiàn)在我們根據(jù)該ID號(hào)查詢一下,Scadb產(chǎn)生的整條記錄:
mysql> select * from b where id = 3208787689176782285 ;
+---------------------+------+------+
| id | name | t |
+---------------------+------+------+
| 3208787689176782285 | 123 | NULL |
+---------------------+------+------+
1 row in set (0.07 sec)
四、最后
唯一ID不不僅僅數(shù)據(jù)庫中需要用到,在業(yè)務(wù)開發(fā)中,我們也經(jīng)常會(huì)碰到需要產(chǎn)生唯一ID的場景,希望本文的原理能夠幫助到大家。
另外如果想了解scadb的架構(gòu),請(qǐng)閱讀:scadb架構(gòu)介紹。