背景
在大型互聯(lián)網(wǎng)應(yīng)用中蹋半,隨著業(yè)務(wù)量的增大拯啦,數(shù)據(jù)庫中單表的數(shù)據(jù)量會達(dá)到千萬、上億的量級符相,為緩解數(shù)據(jù)庫壓力拆融,往往采取分庫分表的策略。分庫分表后需要有一個(gè)唯一ID來標(biāo)識一條數(shù)據(jù)或消息啊终,數(shù)據(jù)庫的自增ID顯然不能滿足需求镜豹,此時(shí)就需要有一個(gè)能夠生成全局唯一ID的系統(tǒng)。全局唯一ID有幾個(gè)特性:
1蓝牲、全局唯一性:不能出現(xiàn)重復(fù)的ID號趟脂,這是最基本的要求。
2例衍、趨勢遞增:以MySQL為例 Mysql InnoDB引擎中使用的是聚集索引昔期,由于多數(shù)RDBMS使用B-tree的數(shù)據(jù)結(jié)構(gòu)來存儲索引數(shù)據(jù)已卸,在主鍵的選擇上面我們應(yīng)該盡量使用有序的主鍵保證寫入性能。
3镇眷、高性能:ID生成響應(yīng)要塊咬最,否則反倒會成為業(yè)務(wù)瓶頸
4、高可用:復(fù)雜的分布式系統(tǒng)中欠动,業(yè)務(wù)對分布式ID生成系統(tǒng)可用性要求極高永乌,比如:訂單系統(tǒng)、優(yōu)惠券具伍、倉庫系統(tǒng)等因?yàn)榉植际絀D生成系統(tǒng)癱瘓從而導(dǎo)致一些核心業(yè)務(wù)無法進(jìn)行翅雏,會引發(fā)一場災(zāi)難
我們要樹立一個(gè)理念,沒有完美的解決方案人芽,每種方案都有優(yōu)缺點(diǎn)望几,在具體的選擇上要根據(jù)具體的業(yè)務(wù)選擇合適的方案。
一:UUID (不推薦)
在用到全局唯一id時(shí)萤厅,我們很容易想到UUID橄抹,畢竟它有著全球唯一的特性。
UUID(Universally Unique Identifier)的標(biāo)準(zhǔn)型式包含32個(gè)16進(jìn)制數(shù)字惕味,以連字號分為五段楼誓,形式為8-4-4-4-12的36個(gè)字符,示例:550e8400-e29b-41d4-a716-446655440000
像用作訂單號UUID這樣的字符串沒有絲毫的意義名挥,看不出和訂單相關(guān)的有用信息疟羹;而對于數(shù)據(jù)庫來說用作業(yè)務(wù)主鍵ID,它不僅是太長還是字符串禀倔,存儲性能差查詢也很耗時(shí)榄融,所以不推薦用作分布式ID。
優(yōu)點(diǎn):
性能非常高:本地生成救湖,沒有網(wǎng)絡(luò)消耗愧杯。
缺點(diǎn):
1、不易于存儲:UUID太長鞋既,16字節(jié)128位民效,通常以36長度的字符串表示,很多場景不適用涛救。MySQL官方明確建議主鍵要盡量越短越好
2畏邢、無序的字符串,不具備趨勢自增特性检吆。作為數(shù)據(jù)庫主鍵 UUID 的無序性會導(dǎo)致數(shù)據(jù)位置頻繁變動(dòng)舒萎,嚴(yán)重影響性能。
3蹭沛、沒有具體的業(yè)務(wù)含義臂寝。比如:用于訂單號章鲤,這樣的字符串顯然沒有意義。
二:利用數(shù)據(jù)庫自增特性
具體實(shí)現(xiàn)是咆贬,單獨(dú)創(chuàng)建一個(gè)Mysql實(shí)例败徊,設(shè)置主鍵屬性為auto_increment,當(dāng)我們需要一個(gè)id時(shí)掏缎,往數(shù)據(jù)庫中插入一條數(shù)據(jù)拿到該記錄的主鍵id皱蹦,如:利用SELECT LAST_INSERT();
優(yōu)點(diǎn):
實(shí)現(xiàn)簡單,利用數(shù)據(jù)庫系統(tǒng)特性實(shí)現(xiàn)
缺點(diǎn):
強(qiáng)依賴DB眷蜈,如果數(shù)據(jù)庫宕機(jī)沪哺,就是引發(fā)致命問題,也可以利用集群部署保證高可用酌儒,但要考慮主從復(fù)制模式下數(shù)據(jù)一致性問題辜妓;多臺機(jī)器不能生成重復(fù)id(可以設(shè)置不同的起始值和自增步長)
三:利用數(shù)據(jù)庫號段模式
可以理解為從數(shù)據(jù)庫批量的獲取自增ID,每次從數(shù)據(jù)庫取出一個(gè)號段范圍忌怎,比如1~1000籍滴,可以想下如果每次獲取ID都得讀寫一次數(shù)據(jù)庫,勢必會對數(shù)據(jù)庫造成較大壓力榴啸。順便說下號段模式是目前很多分布式ID生成器的主流實(shí)現(xiàn)方式之一
CREATE TABLE id_sequence (
id int(10) NOT NULL,
max_id bigint(20) NOT NULL COMMENT '當(dāng)前最大id',
step int(10) NOT NULL COMMENT '號段的步長',
biz_tag varchar(128) NOT NULL COMMENT '業(yè)務(wù)類型',
version int(20) NOT NULL COMMENT '版本號',
desc varchar(256) COMMIT '描述'
PRIMARY KEY (`id`)
)
biz_tag :代表不同業(yè)務(wù)類型
max_id :當(dāng)前最大的可用id
step :代表號段的長度
version :是一個(gè)樂觀鎖孽惰,每次都更新version,保證并發(fā)時(shí)數(shù)據(jù)的正確性
四:基于Redis實(shí)現(xiàn)
Redis的所有命令操作都是單線程的插掂,本身提供像 incr 和 increby 這樣的自增原子命令灰瞻,所以能保證生成的 ID 肯定是唯一有序的
需要考慮用集群方式保證可用性和高性能(高吞吐量)腥例,同時(shí)需要主要考慮redis持久化
五:雪花算法(Snowflake)
這種方案把64-bit分別劃分成多段辅甥,分開來標(biāo)示機(jī)器、時(shí)間等燎竖,比如在snowflake中的64-bit分別表示如下圖所示:
Snowflake ID組成結(jié)構(gòu):正數(shù)位(占1比特)+ 時(shí)間戳(占41比特)+ 機(jī)器ID(占5比特)+ 數(shù)據(jù)中心(占5比特)+ 自增值(占12比特)璃弄,總共64比特組成的一個(gè)Long類型。
第一個(gè)bit位(1bit):Java中l(wèi)ong的最高位是符號位代表正負(fù)构回,正數(shù)是0夏块,負(fù)數(shù)是1,一般生成ID都為正數(shù)纤掸,所以默認(rèn)為0脐供。
時(shí)間戳部分(41bit):毫秒級的時(shí)間,不建議存當(dāng)前時(shí)間戳借跪,而是用(當(dāng)前時(shí)間戳 - 固定開始時(shí)間戳)的差值政己,可以使產(chǎn)生的ID從更小的值開始;41位的時(shí)間戳可以使用69年掏愁,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年
工作機(jī)器id(10bit):也被叫做workId歇由,這個(gè)可以靈活配置卵牍,機(jī)房或者機(jī)器號組合都可以。
序列號部分(12bit)沦泌,自增值支持同一毫秒內(nèi)同一個(gè)節(jié)點(diǎn)可以生成4096個(gè)ID
根據(jù)這個(gè)算法的邏輯糊昙,只需要將這個(gè)算法用Java語言實(shí)現(xiàn)出來,封裝為一個(gè)工具方法谢谦,那么各個(gè)業(yè)務(wù)應(yīng)用可以直接使用該工具方法來獲取分布式ID释牺,只需保證每個(gè)業(yè)務(wù)應(yīng)用有自己的工作機(jī)器id即可,而不需要單獨(dú)去搭建一個(gè)獲取分布式ID的應(yīng)用他宛。
各個(gè)廠商實(shí)現(xiàn)的分布式生成器
美團(tuán)(Leaf) 支持Leaf-segment數(shù)據(jù)庫方案和Leaf-snowflake方案
百度(uid-generator) uid-generator是基于Snowflake算法實(shí)現(xiàn)的船侧,與原始的snowflake算法不同在于,uid-generator支持自定義時(shí)間戳厅各、工作機(jī)器ID和 序列號 等各部分的位數(shù)镜撩,而且uid-generator中采用用戶自定義workId的生成策略。
滴滴(Tinyid) 基于號段模式
阿里(Sequence)類似號段模式