使用uuid作为数据库主键,被技术总监怼了!
一、前言
在日常開發(fā)中,數(shù)據(jù)庫中主鍵id的生成方案,主要有三種
數(shù)據(jù)庫自增ID
采用隨機數(shù)生成不重復(fù)的ID
采用jdk提供的uuid
對于這三種方案,我發(fā)現(xiàn)在數(shù)據(jù)量少的情況下,沒有特別的差異,但是當單表的數(shù)據(jù)量達到百萬級以上時候,他們的性能有著顯著的區(qū)別,光說理論不行,還得看實際程序測試,今天就帶著大家一探究竟!
二、程序?qū)嵗?/h3>
首先,我們在本地數(shù)據(jù)庫中創(chuàng)建三張單表tb_uuid_1、tb_uuid_2、tb_uuid_3,同時設(shè)置tb_uuid_1表的主鍵為自增長模式,腳本如下:
CREATE?TABLE?`tb_uuid_1`?(`id`?bigint(20)?unsigned?NOT?NULL?AUTO_INCREMENT,`name`?varchar(20)?DEFAULT?NULL,PRIMARY?KEY?(`id`) )?ENGINE=InnoDB?DEFAULT?CHARSET=utf8mb4?COLLATE=utf8mb4_unicode_ci?COMMENT='主鍵ID自增長'; CREATE?TABLE?`tb_uuid_2`?(`id`?bigint(20)?unsigned?NOT?NULL,`name`?varchar(20)?DEFAULT?NULL,PRIMARY?KEY?(`id`) )?ENGINE=InnoDB?DEFAULT?CHARSET=utf8mb4?COLLATE=utf8mb4_unicode_ci?COMMENT='主鍵ID隨機數(shù)生成'; CREATE?TABLE?`tb_uuid_3`?(`id`?varchar(50)??NOT?NULL,`name`?varchar(20)?DEFAULT?NULL,PRIMARY?KEY?(`id`) )?ENGINE=InnoDB?DEFAULT?CHARSET=utf8mb4?COLLATE=utf8mb4_unicode_ci?COMMENT='主鍵采用uuid生成';下面,我們采用Springboot + mybatis來實現(xiàn)插入測試。
2.1、數(shù)據(jù)庫自增
以數(shù)據(jù)庫自增為例,首先編寫好各種實體、數(shù)據(jù)持久層操作,方便后續(xù)進行測試
/***?表實體*/ public?class?UUID1?implements?Serializable?{private?Long?id;private?String?name;//省略set、get } /***?數(shù)據(jù)持久層操作*/ public?interface?UUID1Mapper?{/***?自增長插入*?@param?uuid1*/@Insert("INSERT?INTO?tb_uuid_1(name)?VALUES(#{name})")void?insert(UUID1?uuid1); } /***?自增ID,單元測試*/ @Test public?void?testInsert1(){long?start?=?System.currentTimeMillis();for?(int?i?=?0;?i?<?1000000;?i++)?{uuid1Mapper.insert(new?UUID1().setName("張三"));}long?end?=?System.currentTimeMillis();System.out.println("花費時間:"?+??(end?-?start)); }2.2、采用隨機數(shù)生成ID
這里,我們采用twitter的雪花算法來實現(xiàn)隨機數(shù)ID的生成,工具類如下:
public?class?SnowflakeIdWorker?{private?static?SnowflakeIdWorker?instance?=?new?SnowflakeIdWorker(0,0);/***?開始時間截?(2015-01-01)*/private?final?long?twepoch?=?1420041600000L;/***?機器id所占的位數(shù)*/private?final?long?workerIdBits?=?5L;/***?數(shù)據(jù)標識id所占的位數(shù)*/private?final?long?datacenterIdBits?=?5L;/***?支持的最大機器id,結(jié)果是31?(這個移位算法可以很快的計算出幾位二進制數(shù)所能表示的最大十進制數(shù))*/private?final?long?maxWorkerId?=?-1L?^?(-1L?<<?workerIdBits);/***?支持的最大數(shù)據(jù)標識id,結(jié)果是31*/private?final?long?maxDatacenterId?=?-1L?^?(-1L?<<?datacenterIdBits);/***?序列在id中占的位數(shù)*/private?final?long?sequenceBits?=?12L;/***?機器ID向左移12位*/private?final?long?workerIdShift?=?sequenceBits;/***?數(shù)據(jù)標識id向左移17位(12+5)*/private?final?long?datacenterIdShift?=?sequenceBits?+?workerIdBits;/***?時間截向左移22位(5+5+12)*/private?final?long?timestampLeftShift?=?sequenceBits?+?workerIdBits?+?datacenterIdBits;/***?生成序列的掩碼,這里為4095?(0b111111111111=0xfff=4095)*/private?final?long?sequenceMask?=?-1L?^?(-1L?<<?sequenceBits);/***?工作機器ID(0~31)*/private?long?workerId;/***?數(shù)據(jù)中心ID(0~31)*/private?long?datacenterId;/***?毫秒內(nèi)序列(0~4095)*/private?long?sequence?=?0L;/***?上次生成ID的時間截*/private?long?lastTimestamp?=?-1L;/***?構(gòu)造函數(shù)*?@param?workerId?????工作ID?(0~31)*?@param?datacenterId?數(shù)據(jù)中心ID?(0~31)*/public?SnowflakeIdWorker(long?workerId,?long?datacenterId)?{if?(workerId?>?maxWorkerId?||?workerId?<?0)?{throw?new?IllegalArgumentException(String.format("worker?Id?can't?be?greater?than?%d?or?less?than?0",?maxWorkerId));}if?(datacenterId?>?maxDatacenterId?||?datacenterId?<?0)?{throw?new?IllegalArgumentException(String.format("datacenter?Id?can't?be?greater?than?%d?or?less?than?0",?maxDatacenterId));}this.workerId?=?workerId;this.datacenterId?=?datacenterId;}/***?獲得下一個ID?(該方法是線程安全的)*?@return?SnowflakeId*/public?synchronized?long?nextId()?{long?timestamp?=?timeGen();//?如果當前時間小于上一次ID生成的時間戳,說明系統(tǒng)時鐘回退過這個時候應(yīng)當拋出異常if?(timestamp?<?lastTimestamp)?{throw?new?RuntimeException(String.format("Clock?moved?backwards.??Refusing?to?generate?id?for?%d?milliseconds",?lastTimestamp?-?timestamp));}//?如果是同一時間生成的,則進行毫秒內(nèi)序列if?(lastTimestamp?==?timestamp)?{sequence?=?(sequence?+?1)?&?sequenceMask;//?毫秒內(nèi)序列溢出if?(sequence?==?0)?{//阻塞到下一個毫秒,獲得新的時間戳timestamp?=?tilNextMillis(lastTimestamp);}}//?時間戳改變,毫秒內(nèi)序列重置else?{sequence?=?0L;}//?上次生成ID的時間截lastTimestamp?=?timestamp;//?移位并通過或運算拼到一起組成64位的IDreturn?((timestamp?-?twepoch)?<<?timestampLeftShift)?//|?(datacenterId?<<?datacenterIdShift)?//|?(workerId?<<?workerIdShift)?//|?sequence;}/***?阻塞到下一個毫秒,直到獲得新的時間戳*?@param?lastTimestamp?上次生成ID的時間截*?@return?當前時間戳*/protected?long?tilNextMillis(long?lastTimestamp)?{long?timestamp?=?timeGen();while?(timestamp?<=?lastTimestamp)?{timestamp?=?timeGen();}return?timestamp;}/***?返回以毫秒為單位的當前時間*?@return?當前時間(毫秒)*/protected?long?timeGen()?{return?System.currentTimeMillis();}public?static?SnowflakeIdWorker?getInstance(){return?instance;}public?static?void?main(String[]?args)?throws?InterruptedException?{SnowflakeIdWorker?idWorker?=?SnowflakeIdWorker.getInstance();for?(int?i?=?0;?i?<?10;?i++)?{long?id?=?idWorker.nextId();Thread.sleep(1);System.out.println(id);}} }其他的操作,與上面類似。
2.3、uuid
同樣的,uuid的生成,我們事先也可以將工具類編寫好:
public?class?UUIDGenerator?{/***?獲取uuid*?@return*/public?static?String?getUUID(){return?UUID.randomUUID().toString();} }最后的單元測試,代碼如下:
@RunWith(SpringRunner.class) @SpringBootTest() public?class?UUID1Test?{private?static?final?Integer?MAX_COUNT?=?1000000;@Autowiredprivate?UUID1Mapper?uuid1Mapper;@Autowiredprivate?UUID2Mapper?uuid2Mapper;@Autowiredprivate?UUID3Mapper?uuid3Mapper;/***?測試自增ID耗時*/@Testpublic?void?testInsert1(){long?start?=?System.currentTimeMillis();for?(int?i?=?0;?i?<?MAX_COUNT;?i++)?{uuid1Mapper.insert(new?UUID1().setName("張三"));}long?end?=?System.currentTimeMillis();System.out.println("自增ID,花費時間:"?+??(end?-?start));}/***?測試采用雪花算法生產(chǎn)的隨機數(shù)ID耗時*/@Testpublic?void?testInsert2(){long?start?=?System.currentTimeMillis();for?(int?i?=?0;?i?<?MAX_COUNT;?i++)?{long?id?=?SnowflakeIdWorker.getInstance().nextId();uuid2Mapper.insert(new?UUID2().setId(id).setName("張三"));}long?end?=?System.currentTimeMillis();System.out.println("花費時間:"?+??(end?-?start));}/***?測試采用UUID生成的ID耗時*/@Testpublic?void?testInsert3(){long?start?=?System.currentTimeMillis();for?(int?i?=?0;?i?<?MAX_COUNT;?i++)?{String?id?=?UUIDGenerator.getUUID();uuid3Mapper.insert(new?UUID3().setId(id).setName("張三"));}long?end?=?System.currentTimeMillis();System.out.println("花費時間:"?+??(end?-?start));} }三、性能測試
程序環(huán)境搭建完成之后,啥也不說了,直接擼起袖子,將單元測試跑起來!
首先測試一下,插入100萬數(shù)據(jù)的情況下,三者直接的耗時結(jié)果如下:
在原有的數(shù)據(jù)量上,我們繼續(xù)插入30萬條數(shù)據(jù),三者耗時結(jié)果如下:
可以看出在數(shù)據(jù)量 100W 左右的時候,uuid的插入效率墊底,隨著插入的數(shù)據(jù)量增長,uuid 生成的ID插入呈直線下降!
時間占用量總體效率排名為:自增ID > 雪花算法生成的ID >> uuid生成的ID。
在數(shù)據(jù)量較大的情況下,為什么uuid生成的ID遠不如自增ID呢?
關(guān)于這點,我們可以從 mysql 主鍵存儲的內(nèi)部結(jié)構(gòu)來進行分析。
3.1、自增ID內(nèi)部結(jié)構(gòu)
自增的主鍵的值是順序的,所以 Innodb 把每一條記錄都存儲在一條記錄的后面。
當達到頁面的最大填充因子時候(innodb默認的最大填充因子是頁大小的15/16,會留出1/16的空間留作以后的修改),會進行如下操作:
下一條記錄就會寫入新的頁中,一旦數(shù)據(jù)按照這種順序的方式加載,主鍵頁就會近乎于順序的記錄填滿,提升了頁面的最大填充率,不會有頁的浪費
新插入的行一定會在原有的最大數(shù)據(jù)行下一行,mysql定位和尋址很快,不會為計算新行的位置而做出額外的消耗
3.2、使用uuid的索引內(nèi)部結(jié)構(gòu)
uuid相對順序的自增id來說是毫無規(guī)律可言的,新行的值不一定要比之前的主鍵的值要大,所以innodb無法做到總是把新行插入到索引的最后,而是需要為新行尋找新的合適的位置從而來分配新的空間。
這個過程需要做很多額外的操作,數(shù)據(jù)的毫無順序會導(dǎo)致數(shù)據(jù)分布散亂,將會導(dǎo)致以下的問題:
寫入的目標頁很可能已經(jīng)刷新到磁盤上并且從緩存上移除,或者還沒有被加載到緩存中,innodb在插入之前不得不先找到并從磁盤讀取目標頁到內(nèi)存中,這將導(dǎo)致大量的隨機IO
因為寫入是亂序的,innodb不得不頻繁的做頁分裂操作,以便為新的行分配空間,頁分裂導(dǎo)致移動大量的數(shù)據(jù),一次插入最少需要修改三個頁以上
由于頻繁的頁分裂,頁會變得稀疏并被不規(guī)則的填充,最終會導(dǎo)致數(shù)據(jù)會有碎片
在把值載入到聚簇索引(innodb默認的索引類型)以后,有時候會需要做一次OPTIMEIZE TABLE來重建表并優(yōu)化頁的填充,這將又需要一定的時間消耗。
因此,在選擇主鍵ID生成方案的時候,盡可能別采用uuid的方式來生成主鍵ID,隨著數(shù)據(jù)量越大,插入性能會越低!
四、總結(jié)
在實際使用過程中,推薦使用主鍵自增ID和雪花算法生成的隨機ID。
但是使用自增ID也有缺點:
別人一旦爬取你的數(shù)據(jù)庫,就可以根據(jù)數(shù)據(jù)庫的自增id獲取到你的業(yè)務(wù)增長信息,很容易進行數(shù)據(jù)竊取。
其次,對于高并發(fā)的負載,innodb在按主鍵進行插入的時候會造成明顯的鎖爭用,主鍵的上界會成為爭搶的熱點,因為所有的插入都發(fā)生在這里,并發(fā)插入會導(dǎo)致間隙鎖競爭。
總結(jié)起來,如果業(yè)務(wù)量小,推薦采用自增ID,如果業(yè)務(wù)量大,推薦采用雪花算法生成的隨機ID。
本篇文章主要從實際程序?qū)嵗霭l(fā),討論了三種主鍵ID生成方案的性能差異, 鑒于筆者才疏學(xué)淺,可能也有理解不到位的地方,歡迎網(wǎng)友們批評指出!
五、參考
1、方志明 - 使用雪花id或uuid作為Mysql主鍵,被老板懟了一頓!
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎
總結(jié)
以上是生活随笔為你收集整理的使用uuid作为数据库主键,被技术总监怼了!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 绝了,66道并发多线程面试题汇总
- 下一篇: kotlin字符串数组_Kotlin程序