一種簡(jiǎn)單的ID生成策略: Mysql表生成全局唯一ID的實(shí)現(xiàn)
生成全局ID的方法很多, 這里記錄下一種簡(jiǎn)單的方案: 利用mysql的自增id生成全局唯一ID.
1. 創(chuàng)建一張只需要兩個(gè)字段的表:
CREATE TABLE `guid` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `stub` char(1) NOT NULL DEFAULT '' COMMENT '樁字段,占坑的', PRIMARY KEY (`id`), UNIQUE KEY `uk_stub` (`stub`) -- 將 stub 設(shè)為唯一索引 ) ENGINE=MyISAM AUTO_INCREMENT=1000000000 DEFAULT CHARSET=utf8;
指定自增起始: alter table guid auto_increment=1000000000, 這樣可以保證ID為10位(漲到11位幾乎不可能吧).
2. 定義 mybatis mapper:
@Mapper public interface GuidMapper { /**獲取全局唯一ID * @return */ // replace into afs_guid(stub) values('a'); // select last_insert_id(); @Insert("REPLACE INTO guid (stub) VALUES('a')") @SelectKey(statement = {"SELECT LAST_INSERT_ID()"}, keyProperty = "guidHolder.id", before = false, resultType = long.class) int getGuid( @Param("guidHolder") GuidHolder guidHolder); @Data public static class GuidHolder{ private long id; private String stub; }
3. 測(cè)試
GuidMapper.GuidHolder guidHolder = new GuidMapper.GuidHolder(); int i = guidMapper.getGuid(guidHolder); long guid = guidHolder.getId(); // guid 就是返回的ID
尾巴
并發(fā)安全問(wèn)題
REPLACE INTO 類(lèi)似于 INSERT 是安全的. 不只是它會(huì)先判斷主鍵或唯一鍵是否重復(fù), 重復(fù), 則刪除原有的, 新增一條, 替換原來(lái)的.
SELECT LAST_INSERT_ID() 是和mysql連接綁定的, 當(dāng)前連接上, 操作觸發(fā)了auto_increment值改變, 得到新的數(shù)值, 這個(gè)數(shù)值, 只會(huì)被當(dāng)前連接可見(jiàn). 其他連接也只會(huì)拿到它改變auto_increment后的值.
以上兩點(diǎn)保證了 并發(fā)安全 .
另外, 即使手動(dòng)將id的值改小了, 下次 replace into 后依然會(huì)從上次自增的基礎(chǔ)上繼續(xù)自增. 因?yàn)槭謩?dòng)修改id的值, 不會(huì)改變auto_increment的值.
補(bǔ)充知識(shí):集群高并發(fā)情況下如何保證分布式唯一全局Id生成
前言
系統(tǒng)唯一ID是我們?cè)谠O(shè)計(jì)一個(gè)系統(tǒng)的時(shí)候常常會(huì)遇見(jiàn)的問(wèn)題,也常常為這個(gè)問(wèn)題而糾結(jié)。
這篇文章就是給各位看官提供一個(gè)生成分布式唯一全局id生成方案的思路,希望能幫助到大家。
不足之處,請(qǐng)多多指教??!
問(wèn)題
為什么需要分布式全局唯一ID以及分布式ID的業(yè)務(wù)需求
在復(fù)雜分布式系統(tǒng)中,往往需要對(duì)大量的數(shù)據(jù)和消息進(jìn)行唯一標(biāo)識(shí),如在美團(tuán)點(diǎn)評(píng)的金融、支付、餐飲、酒店
貓眼電影等產(chǎn)品的系統(tǒng)中數(shù)據(jù)逐漸增長(zhǎng),對(duì)數(shù)據(jù)庫(kù)分庫(kù)分表后需要有一個(gè)唯一ID來(lái)標(biāo)識(shí)一條數(shù)據(jù)或信息;
特別Ian的訂單、騎手、優(yōu)惠券都需要有唯一ID做標(biāo)識(shí)
此時(shí)一個(gè)能夠生成全局唯一ID的系統(tǒng)是非常必要的
ID生成規(guī)則部分硬性要求
全局唯一
趨勢(shì)遞增
在MySQL的InnoDB引擎中使用的是聚集索引,由于多數(shù)RDBMS使用Btree的數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ)索引,在主鍵的選擇上面我們應(yīng)該盡量使用有序的主鍵保證寫(xiě)入性能
單調(diào)遞增
保證下一個(gè)ID一定大于上一個(gè)ID,例如事務(wù)版本號(hào)、IM增量消息、排序等特殊需求
信息安全
如果ID是連續(xù),惡意用戶的爬取工作就非常容易做了,直接按照順序下載指定URL即可,如果是訂單號(hào)就危險(xiǎn)了,競(jìng)爭(zhēng)對(duì)手可以直接知道我們一天的單量,所以在一些應(yīng)用場(chǎng)景下,需要ID無(wú)規(guī)則不規(guī)則,讓競(jìng)爭(zhēng)對(duì)手不好猜
含時(shí)間戳
一樣能夠快速在開(kāi)發(fā)中了解這個(gè)分布式ID什么時(shí)候生成的
ID號(hào)生成系統(tǒng)的可用性要求
高可用
發(fā)布一個(gè)獲取分布式ID請(qǐng)求,服務(wù)器就要保證99.999%的情況下給我創(chuàng)建一個(gè)唯一分布式ID
低延遲
發(fā)一個(gè)獲取分布式ID的請(qǐng)求,服務(wù)器就要快,極速
高QPS
例如并發(fā)一口氣10萬(wàn)個(gè)創(chuàng)建分布式ID請(qǐng)求同時(shí)殺過(guò)來(lái),服務(wù)器要頂?shù)米∏乙幌伦映晒?chuàng)建10萬(wàn)個(gè)分布式ID
一般通用解決方案
UUID
UUID.randomUUID() , UUID的標(biāo)準(zhǔn)型包含32個(gè)16進(jìn)制數(shù)字,以連字號(hào)分為五段,形式為 8-4-4-4-12的36個(gè)字符,性能非常高,本地生成,沒(méi)有網(wǎng)絡(luò)消耗。
存在問(wèn)題
入數(shù)據(jù)庫(kù)性能差,因?yàn)閁UID是無(wú)序的
無(wú)序,無(wú)法預(yù)測(cè)他的生成順序,不能生成遞增有序的數(shù)字
首先分布式id一般都會(huì)作為逐漸,但是按照mysql官方推薦主鍵盡量越短越好,UUID每一個(gè)都很長(zhǎng),所以不是很推薦。
主鍵,ID作為主鍵時(shí),在特定的環(huán)境下會(huì)存在一些問(wèn)題
比如做DB主鍵的場(chǎng)景下,UUID就非常不適用MySQL官方有明確的說(shuō)明
索引,B+樹(shù)索引的分裂
既然分布式ID是主鍵,然后主鍵是包含索引的,而mysql的索引是通過(guò)B+樹(shù)來(lái)實(shí)現(xiàn)的,每一次新的UUID數(shù)據(jù)的插入,為了查詢的優(yōu)化,都會(huì)對(duì)索引底層的B+樹(shù)進(jìn)行修改,因?yàn)閁UID數(shù)據(jù)是無(wú)序的,所以每一次UUID數(shù)據(jù)的插入都會(huì)對(duì)主鍵的B+樹(shù)進(jìn)行很大的修改,這一點(diǎn)很不好,插入完全無(wú)序,不但會(huì)導(dǎo)致一些中間節(jié)點(diǎn)產(chǎn)生分裂,也會(huì)白白創(chuàng)造出很多不飽和的節(jié)點(diǎn),這樣大大降低了數(shù)據(jù)庫(kù)插入的性能。
UUID只能保證全局唯一性,不滿足后面的趨勢(shì)遞增,單調(diào)遞增
數(shù)據(jù)庫(kù)自增主鍵
單機(jī)
在分布式里面,數(shù)據(jù)庫(kù)的自增ID機(jī)制的主要原理是:數(shù)據(jù)庫(kù)自增ID和mysql數(shù)據(jù)庫(kù)的replace into實(shí)現(xiàn)的,這里的replace into跟insert功能 類(lèi)似,不同點(diǎn)在于:replace into首先嘗試插入數(shù)據(jù)列表中,如果發(fā)現(xiàn)表中已經(jīng)有此行數(shù)據(jù)(根據(jù)主鍵或唯一索引判斷)則先刪除,在插入,否則直接插入新數(shù)據(jù)。
REPLACE INTO的含義是插入一條記錄,如果表中唯一索引的值遇到?jīng)_突,則替換老數(shù)據(jù)
REPLACE into t_test(stub) values('b');
select LAST_INSERT_ID();
我們每次插入的時(shí)候,發(fā)現(xiàn)都會(huì)把原來(lái)的數(shù)據(jù)給替換,并且ID也會(huì)增加
這就滿足了
遞增性
單調(diào)性
唯一性
在分布式情況下,并且并發(fā)量不多的情況,可以使用這種方案來(lái)解決,獲得一個(gè)全局的唯一ID
集群分布式集群
那數(shù)據(jù)庫(kù)自增ID機(jī)制適合做分布式ID嗎?答案是不太適合
系統(tǒng)水平擴(kuò)展比較困難,比如定義好步長(zhǎng)和機(jī)器臺(tái)數(shù)之后,如果要添加機(jī)器該怎么辦,假設(shè)現(xiàn)在有一臺(tái)機(jī)器發(fā)號(hào)是:1,2,3,4,5,(步長(zhǎng)是1),這個(gè)時(shí)候需要擴(kuò)容機(jī)器一臺(tái),可以這樣做:把第二胎機(jī)器的初始值設(shè)置得比第一臺(tái)超過(guò)很多,貌似還好,但是假設(shè)線上如果有100臺(tái)機(jī)器,這個(gè)時(shí)候擴(kuò)容要怎么做,簡(jiǎn)直是噩夢(mèng),所以系統(tǒng)水平擴(kuò)展方案復(fù)雜難以實(shí)現(xiàn)。
數(shù)據(jù)庫(kù)壓力還是很大,每次獲取ID都得讀寫(xiě)一次數(shù)據(jù)庫(kù),非常影響性能,不符合分布式ID里面的延遲低和高QPS的規(guī)則(在高并發(fā)下,如果都去數(shù)據(jù)庫(kù)里面獲取ID,那是非常影響性能的)
基于Redis生成全局ID策略
單機(jī)版
因?yàn)镽edis是單線程,天生保證原子性,可以使用原子操作INCR和INCRBY來(lái)實(shí)現(xiàn)
INCRBY:設(shè)置增長(zhǎng)步長(zhǎng)
集群分布式
注意:在Redis集群情況下,同樣和MySQL一樣需要設(shè)置不同的增長(zhǎng)步長(zhǎng),同時(shí)key一定要設(shè)置有效期,可以使用Redis集群來(lái)獲取更高的吞吐量。
假設(shè)一個(gè)集群中有5臺(tái)Redis,可以初始化每臺(tái)Redis的值分別是 1,2,3,4,5 , 然后設(shè)置步長(zhǎng)都是5
各個(gè)Redis生成的ID為:
A:1 6 11 16 21
B:2 7 12 17 22
C:3 8 13 18 23
D:4 9 14 19 24
E:5 10 15 20 25
但是存在的問(wèn)題是,就是Redis集群的維護(hù)和保養(yǎng)比較麻煩,配置麻煩。因?yàn)橐O(shè)置單點(diǎn)故障,哨兵值守
但是主要是的問(wèn)題就是,為了一個(gè)ID,卻需要引入整個(gè)Redis集群,有種殺雞焉用牛刀的感覺(jué)
雪花算法
是什么
Twitter的分布式自增ID算法,Snowflake
最初Twitter把存儲(chǔ)系統(tǒng)從MySQL遷移到Cassandra(由Facebook開(kāi)發(fā)一套開(kāi)源分布式NoSQL數(shù)據(jù)庫(kù)系統(tǒng))因?yàn)镃assandra沒(méi)有順序ID生成機(jī)制,所有開(kāi)發(fā)了這樣一套全局唯一ID生成服務(wù)。
Twitter的分布式雪花算法SnowFlake,經(jīng)測(cè)試SnowFlake每秒可以產(chǎn)生26萬(wàn)個(gè)自增可排序的ID
twitter的SnowFlake生成ID能夠按照時(shí)間有序生成
SnowFlake算法生成ID的結(jié)果是一個(gè)64Bit大小的整數(shù),為一個(gè)Long型(轉(zhuǎn)換成字符串后長(zhǎng)度最多19)
分布式系統(tǒng)內(nèi)不會(huì)產(chǎn)生ID碰撞(由datacenter 和 workerID做區(qū)分)并且效率較高
分布式系統(tǒng)中,有一些需要全局唯一ID的場(chǎng)景,生成ID的基本要求
在分布式環(huán)境下,必須全局唯一性
一般都需要單調(diào)遞增,因?yàn)橐话阄ㄒ籌D都會(huì)存在數(shù)據(jù)庫(kù),而InnoDB的特性就是將內(nèi)容存儲(chǔ)在主鍵索引上的葉子節(jié)點(diǎn),而且是從左往右遞增的,所有考慮到數(shù)據(jù)庫(kù)性能,一般生成ID也最好是單調(diào)遞增的。為了防止ID沖突可以使用36位UUID,但是UUID有一些缺點(diǎn),首先是它相對(duì)比較長(zhǎng),并且另外UUID一般是無(wú)序的
可能還會(huì)需要無(wú)規(guī)則,因?yàn)槿绻褂梦ㄒ籌D作為訂單號(hào)這種,為了不讓別人知道一天的訂單量多少,就需要這種規(guī)則
結(jié)構(gòu)
雪花算法的幾個(gè)核心組成部分
在Java中64bit的證書(shū)是long類(lèi)型,所以在SnowFlake算法生成的ID就是long類(lèi)存儲(chǔ)的
第一部分
二進(jìn)制中最高位是符號(hào)位,1表示負(fù)數(shù),0表示正數(shù)。生成的ID一般都是用整數(shù),所以最高位固定為0。
第二部分
第二部分是41bit時(shí)間戳位,用來(lái)記錄時(shí)間戳,毫秒級(jí)
41位可以表示 2^41 -1 個(gè)數(shù)字
如果只用來(lái)表示正整數(shù),可以表示的范圍是: 0 - 2^41 -1,減1是因?yàn)榭梢员硎镜臄?shù)值范圍是從0開(kāi)始計(jì)算的,而不是從1。
也就是說(shuō)41位可以表示 2^41 - 1 毫秒的值,轉(zhuǎn)換成單位年則是 69.73年
第三部分
第三部分為工作機(jī)器ID,10Bit用來(lái)記錄工作機(jī)器ID
可以部署在2^10 = 1024個(gè)節(jié)點(diǎn),包括5位 datacenterId(數(shù)據(jù)中心,機(jī)房) 和 5位 workerID(機(jī)器碼)
5位可以表示的最大正整數(shù)是 2 ^ 5 = 31個(gè)數(shù)字,來(lái)表示不同的數(shù)據(jù)中心 和 機(jī)器碼
第四部分
12位bit可以用來(lái)表示的正整數(shù)是 2^12 = 4095,即可以用0 1 2 … 4094 來(lái)表示同一個(gè)機(jī)器同一個(gè)時(shí)間戳內(nèi)產(chǎn)生的4095個(gè)ID序號(hào)。
SnowFlake可以保證
所有生成的ID按時(shí)間趨勢(shì)遞增
整個(gè)分布式系統(tǒng)內(nèi)不會(huì)產(chǎn)生重復(fù)ID,因?yàn)橛衐atacenterId 和 workerId來(lái)做區(qū)分
實(shí)現(xiàn)
雪花算法是由scala算法編寫(xiě)的,有人使用java實(shí)現(xiàn),github地址
/** * twitter的snowflake算法 -- java實(shí)現(xiàn) * * @author beyond * @date 2016/11/26 */ public class SnowFlake { /** * 起始的時(shí)間戳 */ private final static long START_STMP = 1480166465631L; /** * 每一部分占用的位數(shù) */ private final static long SEQUENCE_BIT = 12; //序列號(hào)占用的位數(shù) private final static long MACHINE_BIT = 5; //機(jī)器標(biāo)識(shí)占用的位數(shù) private final static long DATACENTER_BIT = 5;//數(shù)據(jù)中心占用的位數(shù) /** * 每一部分的最大值 */ private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT); private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT); private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT); /** * 每一部分向左的位移 */ private final static long MACHINE_LEFT = SEQUENCE_BIT; private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT; private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT; private long datacenterId; //數(shù)據(jù)中心 private long machineId; //機(jī)器標(biāo)識(shí) private long sequence = 0L; //序列號(hào) private long lastStmp = -1L;//上一次時(shí)間戳 public SnowFlake(long datacenterId, long machineId) { if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) { throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0"); } if (machineId > MAX_MACHINE_NUM || machineId < 0) { throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0"); } this.datacenterId = datacenterId; this.machineId = machineId; } /** * 產(chǎn)生下一個(gè)ID * * @return */ public synchronized long nextId() { long currStmp = getNewstmp(); if (currStmp < lastStmp) { throw new RuntimeException("Clock moved backwards. Refusing to generate id"); } if (currStmp == lastStmp) { //相同毫秒內(nèi),序列號(hào)自增 sequence = (sequence + 1) & MAX_SEQUENCE; //同一毫秒的序列數(shù)已經(jīng)達(dá)到最大 if (sequence == 0L) { currStmp = getNextMill(); } } else { //不同毫秒內(nèi),序列號(hào)置為0 sequence = 0L; } lastStmp = currStmp; return (currStmp - START_STMP) << TIMESTMP_LEFT //時(shí)間戳部分 | datacenterId << DATACENTER_LEFT //數(shù)據(jù)中心部分 | machineId << MACHINE_LEFT //機(jī)器標(biāo)識(shí)部分 | sequence; //序列號(hào)部分 } private long getNextMill() { long mill = getNewstmp(); while (mill <= lastStmp) { mill = getNewstmp(); } return mill; } private long getNewstmp() { return System.currentTimeMillis(); } public static void main(String[] args) { SnowFlake snowFlake = new SnowFlake(2, 3); for (int i = 0; i < (1 << 12); i++) { System.out.println(snowFlake.nextId()); } } }
工程落地經(jīng)驗(yàn)
hutools工具包
地址:https://github.com/looly/hutool
SpringBoot整合雪花算法
引入hutool工具類(lèi)
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.3.1</version> </dependency>
整合
/** * 雪花算法 * * @author: 陌溪 * @create: 2020-04-18-11:08 */ public class SnowFlakeDemo { private long workerId = 0; private long datacenterId = 1; private Snowflake snowFlake = IdUtil.createSnowflake(workerId, datacenterId); @PostConstruct public void init() { try { // 將網(wǎng)絡(luò)ip轉(zhuǎn)換成long workerId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr()); } catch (Exception e) { e.printStackTrace(); } } /** * 獲取雪花ID * @return */ public synchronized long snowflakeId() { return this.snowFlake.nextId(); } public synchronized long snowflakeId(long workerId, long datacenterId) { Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId); return snowflake.nextId(); } public static void main(String[] args) { SnowFlakeDemo snowFlakeDemo = new SnowFlakeDemo(); for (int i = 0; i < 20; i++) { new Thread(() -> { System.out.println(snowFlakeDemo.snowflakeId()); }, String.valueOf(i)).start(); } } }
得到結(jié)果
1251350711346790400 1251350711346790402 1251350711346790401 1251350711346790403 1251350711346790405 1251350711346790404 1251350711346790406 1251350711346790407 1251350711350984704 1251350711350984706 1251350711350984705 1251350711350984707 1251350711350984708 1251350711350984709 1251350711350984710 1251350711350984711 1251350711350984712 1251350711355179008 1251350711355179009 1251350711355179010
優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
毫秒數(shù)在高維,自增序列在低位,整個(gè)ID都是趨勢(shì)遞增的
不依賴數(shù)據(jù)庫(kù)等第三方系統(tǒng),以服務(wù)的方式部署,穩(wěn)定性更高,生成ID的性能也是非常高的
可以根據(jù)自身業(yè)務(wù)特性分配bit位,非常靈活
缺點(diǎn)
依賴機(jī)器時(shí)鐘,如果機(jī)器時(shí)鐘回?fù)?,?huì)導(dǎo)致重復(fù)ID生成
在單機(jī)上是遞增的,但由于涉及到分布式環(huán)境,每臺(tái)機(jī)器上的時(shí)鐘不可能完全同步,有時(shí)候會(huì)出現(xiàn)不是全局遞增的情況,此缺點(diǎn)可以認(rèn)為無(wú)所謂,一般分布式ID只要求趨勢(shì)遞增,并不會(huì)嚴(yán)格要求遞增,90%的需求只要求趨勢(shì)遞增。
其它補(bǔ)充
為了解決時(shí)鐘回?fù)軉?wèn)題,導(dǎo)致ID重復(fù),后面有人專(zhuān)門(mén)提出了解決的方案
百度開(kāi)源的分布式唯一ID生成器 UidGenerator
Leaf - 美團(tuán)點(diǎn)評(píng)分布式ID生成系統(tǒng)
以上這篇一種簡(jiǎn)單的ID生成策略: Mysql表生成全局唯一ID的實(shí)現(xiàn)就是小編分享給大家的全部?jī)?nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
MySQL索引的缺點(diǎn)以及MySQL索引在實(shí)際操作中有哪些事項(xiàng)
以下的文章主要介紹的是MySQL索引的缺點(diǎn)以及MySQL索引在實(shí)際操作中有哪些事項(xiàng)是值得我們大家注意的,我們大家可能不知道過(guò)多的對(duì)索引進(jìn)行使用將會(huì)造成濫用,需要的朋友可以了解下2012-12-12MySQL所支持的數(shù)據(jù)類(lèi)型與表字段約束類(lèi)型的學(xué)習(xí)教程
這篇文章主要介紹了MySQL所支持的數(shù)據(jù)類(lèi)型與表字段約束類(lèi)型的學(xué)習(xí)教程,是MySQL入門(mén)學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-12-12mysql innodb 異常修復(fù)經(jīng)驗(yàn)分享
這篇文章主要介紹了mysql innodb 異常修復(fù)經(jīng)驗(yàn)分享,需要的朋友可以參考下2017-04-04Mysql| 使用通配符進(jìn)行模糊查詢?cè)斀?like,%,_)
這篇文章主要介紹了Mysql| 使用通配符進(jìn)行模糊查詢?cè)斀?like,%,_),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08MySQL數(shù)據(jù)庫(kù)定時(shí)備份的幾種實(shí)現(xiàn)方法
本文主要介紹了MySQL數(shù)據(jù)庫(kù)定時(shí)備份的幾種實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-07-07