世間萬物,都有自己唯一的標識,比如人,每個人都有自己的指紋(白夜追凶給我科普的,同卵雙胞胎DNA一樣,但指紋不一樣)。又如中國人,每個中國人有自己的身份證。對於計算機,很多時候,也需要為每一份數據生成唯一的標識。在這里,數據的概念是非常寬泛的,比如數據量記錄、文件、消息,而唯一的標識我們稱之為id。
本文地址:http://www.cnblogs.com/xybaby/p/7616272.html
自增ID
使用過mysql的同學應該都知道,經常用自增id(auto increment)作為主鍵,這是一個為long的整數類型,每插入一條記錄,該值就會增加1,這樣每條記錄都有了唯一的id。自增id應該是使用最廣泛的id生成方式,其優點在於非常簡單、對數據庫索引友好、而且也能透露出一些信息,比如當前有多少條記錄(當然,用戶也可能通過id猜出總共有多少用戶,這就不太好)。但自增ID也有一些缺點:第一,id攜帶的信息太少,只能起到一個標識作用;第二,現在啥都是分布式的,如果多個mysql組成一個邏輯上的‘mysql’(比如水平分庫這種情況),每個物理mysql都使用自增id,局部來說是唯一的,但總體來說就不唯一了。
於是乎,我們需要為分布式系統生成全局唯一的id。最簡單的辦法,部署一個單點,比如單獨的服務(mysql)專門負責生成id,所有需要id的應用都通過這個單點獲取一個唯一的id,這樣就能保證系統中id的全局唯一性。但是分布式系統中最怕的就是單點故障(single point of failure),單點故障是可靠性、可用性的頭號天敵,因此即使是中心化服務(centralized service)也會搞成一個集群,比如zookeeper。按照這個思路,就有了Flicker的解決方案。
Flicker的解決辦法叫《Ticket Servers: Distributed Unique Primary Keys on the Cheap》,文章篇幅不長,而且通俗易懂,這里也有中文翻譯。簡單來說,Flicker是用兩組(多組)mysql來提供全局id的生成,多組mysql避免了單點,那么怎么保證多組mysql生成的id全局唯一呢,這就利用了mysql的自增id以及replace into語法。
TicketServer1:
auto-increment-increment = 2
auto-increment-offset = 1TicketServer2:
auto-increment-increment = 2
auto-increment-offset = 2
那么怎么獲取這個id呢,不可能每需要一個id的時候都插入一條記錄,這個時候就用到了replace into語法。 replace是insert、update的結合體,對於一條待插入的記錄,如果其主鍵或者唯一索引的值已經存在表中的話,那么會刪除舊的那條記錄,然后插入新的記錄;如果不存在,那么直接插入記錄。這個非常類似mongodb中的findandmodify語法。在Flicker中,是這么使用的,首先schema如下:
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
REPLACE INTO Tickets64 (stub) VALUES ('a');
SELECT LAST_INSERT_ID();
Flicker的解決辦法通俗易懂,但還是沒有解決id信息過少的問題,而且還是依賴單獨的一組服務(mysql)來生成全局id。如果全局id的生成不依賴額外的服務,而且包含豐富的信息那就最好了。
攜帶時間與空間信息的ID
UUID

So what do we do change ID’s to UUID as well. Well no, that’s not a good idea because we will simply increase work for our database server. It will now have to index a random string of 128 bit. The data will be more fragmented and bigger to fit in memory. This will definitely bring down the performance of our system.
第一例是當前db中有多少條記錄,第二列是使用uuid作為key時插入1 million條記錄耗費的時間,第三列是使用64位的整形作為key時插入1 million條記錄耗費的時間。從結果可以看出,隨着數據規模增大,使用uuid時的插入速度遠小於使用整形的情況。
MongoDB ObjectId
- a 4-byte value representing the seconds since the Unix epoch,
- a 3-byte machine identifier,
- a 2-byte process id, and
- a 3-byte counter, starting with a random value.
objectid有12個字節,包含時間信息(4字節、秒為單位)、機器標識(3字節)、進程id(2字節)、計數器(3字節,初始值隨機)。其中,時間位精度(秒或者毫秒)與序列位數,二者決定了單位時間內,對於同一個進程最多可產生多少唯一的ObjectId,在MongoDB中,那每秒就是2^24(16777216)。但是機器標識與進程id一定要保證是不重復的,否則極大概率上會產生重復的ObjectId。
mongos> x = ObjectId()ObjectId("59cf6033858d9d5a85caac02")mongos> x.getTimestamp()ISODate("2017-09-30T09:13:23Z")
1 def _machine_bytes(): 2 """Get the machine portion of an ObjectId. 3 """ 4 machine_hash = _md5func() 5 if PY3: 6 # gethostname() returns a unicode string in python 3.x 7 # while update() requires a byte string. 8 machine_hash.update(socket.gethostname().encode()) 9 else: 10 # Calling encode() here will fail with non-ascii hostnames 11 machine_hash.update(socket.gethostname()) 12 return machine_hash.digest()[0:3] 13 14 def __generate(self): 15 """Generate a new value for this ObjectId. 16 """ 17 oid = EMPTY 18 19 # 4 bytes current time 20 oid += struct.pack(">i", int(time.time())) 21 22 # 3 bytes machine 23 oid += ObjectId._machine_bytes 24 25 # 2 bytes pid 26 oid += struct.pack(">H", os.getpid() % 0xFFFF) 27 28 # 3 bytes inc 29 ObjectId._inc_lock.acquire() 30 oid += struct.pack(">i", ObjectId._inc)[1:4] 31 ObjectId._inc = (ObjectId._inc + 1) % 0xFFFFFF 32 ObjectId._inc_lock.release() 33 34 self.__id = oid
_machine_bytes函數首先獲取主機名,hash之后取前3個字節作為機器標識。核心在__generate函數,代碼有清晰的注釋。可以看到,oid的生成每次都獲取當前時間,int取整到秒,然后加上機器標識、進程號,而計數器(_inc)通過加鎖保證線程安全。
從代碼可以看出兩個問題:第一,即使在同一個機器同一個進程,也是可能產生相同的ObjectID的,因為_inc簡單自增,且每次都直接通過time.time獲取時間。第二,如果生成的機器標識相同,那么大大增加了產生相同ObjectId的概率。
與之對比,SnowFlake有對象的解決辦法:
第一:生成ID的時候,獲取並記錄當前的時間戳。如果當前時間戳與上一次記錄的時間戳相同,那么將計數器加一,如果計數器已滿,那么會等到下一毫秒才會生成ID。如果當前時間戳大於上一次記錄的時間戳,那么隨機初始化計數器,並生成ID。
第二:使用zookeeper來生成唯一的workerid,workerid類似mongodb的機器標識+進程號,保證了workerid的唯一性。
SnowFlake的方案,雖然沖突概率更小,但是需要額外的服務zookeeper,而且指出的workerid受限。ObjectiD的生成是由驅動負責的,不是MongoDB負責,這樣減輕了MongoDB負擔,也達到了去中心化服務的目的。
結構化ID思考
這里的結構化ID,就是指按一定規則,用時間、空間(機器)信息生成的ID,上面介紹的UUID以及各種變種都屬於結構化id。
結構化ID的優點在於充足的信息,最有用的肯定是時間信息,通過ID就能直接拿到數據的創建時間了;另外,天然起到了冷熱數據的分離。當然,有利必有弊,比如在ID作為分片鍵的分片環境中,如果ID包含時間信息,那么很可能在短時間內生成的數據會落在同一個分片。在《帶着問題學習分布式系統之數據分片》一文中,介紹了MongoDB分片的兩種方式:“hash partition”與“range partition“,如果使用ObjectId作為sharding key,且sharding方式為range partition,那么批量導入數據的時候就會導致數據落在同一個shard,結果就是大量chunk的split和migration,這是不太好的。
TFS文件名
如果結構化ID中包含分片信息,那就更好了,這樣就不會再維護數據與分片的信息,而是直接通過id找出對應的分片。我們來看看TFS的例子
TFS是淘寶研發的分布式文件存儲系,其的結構一定程度上參考了GFS(HDFS),元數據服務器稱之為Nameserver,實際的數據存儲服務器稱之為Dataserver。TFS將多個小文件合並成一個大文件,稱之為block,block是真實的物理存儲單元。因此,DataServer負責存儲Block,而NameServer維護block與DataServer的映射。那么小文件與block的映射關系在哪里維護呢?要知道小文件的量是很大的
TFS的文件名由塊號和文件號通過某種對應關系組成,最大長度為18字節。文件名固定以T開始,第二字節為該集群的編號(可以在配置項中指定,取值范圍 1~9)。余下的字節由Block ID和File ID通過一定的編碼方式得到。文件名由客戶端程序進行編碼和解碼
如圖所示:
從上圖可以看到,最終的文件名是包含了block id信息的的,那么如何利用這個blockid信息呢,如下圖所示:
當需要根據文件名獲取文件內容的時候,TFS的客戶端,首先通過文件名解析出Block id與File id,然后從NameServer上根據Block id查詢block所在的DataServer。然后從DataServer上根據Block id拿到對應的block,在根據file id從block中找到對應的文件。
TFS用於存儲淘寶大量的小文件,比如商品的各種尺寸的小圖片,這個數量是非常大的,如果用單獨的元數據服務器維護文件名與文件信息的映射,這個量是非常大的。而使用攜帶block id信息的文件名,很好規避了這個問題。但使用這種攜帶分區信息的ID時,需要考慮數據在分區之間的遷移情況,ID一般來說使不能變的,因此ID映射的應該是一個邏輯分區,而不是真正的物理分區。
總結
references
Ticket Servers: Distributed Unique Primary Keys on the Cheap
UUID(Universally unique identifier)
Are you designing Primary Keys and ID’s???Well think twice..