您現在的位置是:首頁 > 網路遊戲首頁網路遊戲
使用uuid作為資料庫主鍵,被技術總監懟了一頓
- 2021-06-08
怎麼給主鍵新增自增
看完本文,你一定會有所收穫
一、摘要
在日常開發中,資料庫中主鍵id的生成方案,主要有三種
資料庫自增ID
採用隨機數生成不重複的ID
採用jdk提供的uuid
對於這三種方案,我發現在資料量少的情況下,沒有特別的差異,但是當單表的資料量達到百萬級以上時候,他們的效能有著顯著的區別,光說理論不行,還得看實際程式測試,今天小編就帶著大家一探究竟!
二、程式例項
首先,我們在本地資料庫中建立三張單表
tb_uuid_1
、
tb_uuid_2
、
tb_uuid_3
,同時設定
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隨機數生成’;
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
來實現插入測試。
2。1、資料庫自增
以資料庫自增為例,首先編寫好各種實體、資料持久層操作,方便後續進行測試
/** * 表實體 */public class UUID1 implements Serializable { private Long id; private String name; //省略set、get}
/** * 資料持久層操作 */public interface UUID1Mapper { /** * 自增長插入 * @param uuid1 */ @Insert(“INSERT INTO tb_uuid_1(name) VALUES(#{name})”) void insert(UUID1 uuid1);}
/** * 自增ID,單元測試 */@Testpublic 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、採用隨機數生成ID
這裡,我們
採用twitter的雪花演算法來實現隨機數ID的生成
,工具類如下:
public class SnowflakeIdWorker { private static SnowflakeIdWorker instance = new SnowflakeIdWorker(0,0); /** * 開始時間截 (2015-01-01) */ private final long twepoch = 1420041600000L; /** * 機器id所佔的位數 */ private final long workerIdBits = 5L; /** * 資料標識id所佔的位數 */ private final long datacenterIdBits = 5L; /** * 支援的最大機器id,結果是31 (這個移位演算法可以很快的計算出幾位二進位制數所能表示的最大十進位制數) */ private final long maxWorkerId = -1L ^ (-1L << workerIdBits); /** * 支援的最大資料標識id,結果是31 */ private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); /** * 序列在id中佔的位數 */ private final long sequenceBits = 12L; /** * 機器ID向左移12位 */ private final long workerIdShift = sequenceBits; /** * 資料標識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; /** * 資料中心ID(0~31) */ private long datacenterId; /** * 毫秒內序列(0~4095) */ private long sequence = 0L; /** * 上次生成ID的時間截 */ private long lastTimestamp = -1L; /** * 建構函式 * @param workerId 工作ID (0~31) * @param datacenterId 資料中心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生成的時間戳,說明系統時鐘回退過這個時候應當丟擲異常 if (timestamp < lastTimestamp) { throw new RuntimeException( String。format(“Clock moved backwards。 Refusing to generate id for %d milliseconds”, lastTimestamp - timestamp)); } // 如果是同一時間生成的,則進行毫秒內序列 if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; // 毫秒內序列溢位 if (sequence == 0) { //阻塞到下一個毫秒,獲得新的時間戳 timestamp = tilNextMillis(lastTimestamp); } } // 時間戳改變,毫秒內序列重置 else { sequence = 0L; } // 上次生成ID的時間截 lastTimestamp = timestamp; // 移位並透過或運算拼到一起組成64位的ID return ((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; @Autowired private UUID1Mapper uuid1Mapper; @Autowired private UUID2Mapper uuid2Mapper; @Autowired private UUID3Mapper uuid3Mapper; /** * 測試自增ID耗時 */ @Test public 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)); } /** * 測試採用雪花演算法生產的隨機數ID耗時 */ @Test public 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耗時 */ @Test public 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)); }}
三、效能測試
程式環境搭建完成之後,啥也不說了,直接擼起袖子,將單元測試跑起來!
首先測試一下,插入100萬資料的情況下,三者直接的耗時結果如下:
在原有的資料量上,我們繼續插入30萬條資料,三者耗時結果如下:
可以看出在資料量 100W 左右的時候,uuid的插入效率墊底,隨著插入的資料量增長,uuid 生成的ID插入呈直線下降!
時間佔用量總體效率排名為:
自增ID > 雪花演算法生成的ID >> uuid生成的ID
。
在資料量較大的情況下,為什麼uuid生成的ID遠不如自增ID呢
?
關於這點,我們可以從 mysql 主鍵儲存的內部結構來進行分析。
3。1、自增ID內部結構
自增的主鍵的值是順序的,所以 Innodb 把每一條記錄都儲存在一條記錄的後面。
當達到頁面的最大填充因子時候(innodb預設的最大填充因子是頁大小的15/16,會留出1/16的空間留作以後的修改),會進行如下操作:
下一條記錄就會寫入新的頁中,一旦資料按照這種順序的方式載入,主鍵頁就會近乎於順序的記錄填滿,提升了頁面的最大填充率,不會有頁的浪費
新插入的行一定會在原有的最大資料行下一行,mysql定位和定址很快,不會為計算新行的位置而做出額外的消耗
3。2、使用uuid的索引內部結構
uuid相對順序的自增id來說是毫無規律可言的,新行的值不一定要比之前的主鍵的值要大,所以innodb無法做到總是把新行插入到索引的最後,而是需要為新行尋找新的合適的位置從而來分配新的空間。
這個過程需要做很多額外的操作,資料的毫無順序會導致資料分佈散亂,將會導致以下的問題:
寫入的目標頁很可能已經重新整理到磁碟上並且從快取上移除,或者還沒有被載入到快取中,innodb在插入之前不得不先找到並從磁碟讀取目標頁到記憶體中,這將導致大量的隨機IO
因為寫入是亂序的,innodb不得不頻繁的做頁分裂操作,以便為新的行分配空間,頁分裂導致移動大量的資料,一次插入最少需要修改三個頁以上
由於頻繁的頁分裂,頁會變得稀疏並被不規則的填充,最終會導致資料會有碎片
在把值載入到聚簇索引(innodb預設的索引型別)以後,有時候會需要做一次
OPTIMEIZE TABLE
來重建表並最佳化頁的填充,這將又需要一定的時間消耗。
因此,在選擇主鍵ID生成方案的時候,儘可能別採用uuid的方式來生成主鍵ID,隨著資料量越大,插入效能會越低!
四、總結
在實際使用過程中,推薦使用主鍵自增ID和雪花演算法生成的隨機ID。
但是使用自增ID也有缺點:
1、別人一旦爬取你的資料庫,就可以根據資料庫的自增id獲取到你的業務增長資訊,很容易進行資料竊取。2、其次,對於高併發的負載,innodb在按主鍵進行插入的時候會造成明顯的鎖爭用,主鍵的上界會成為爭搶的熱點,因為所有的插入都發生在這裡,併發插入會導致間隙鎖競爭。
總結起來,如果業務量小,推薦採用自增ID,如果業務量大,推薦採用雪花演算法生成的隨機ID。
本篇文章主要從實際程式例項出發,討論了三種主鍵ID生成方案的效能差異, 鑑於筆者才疏學淺,可能也有理解不到位的地方,歡迎網友們批評指出!