<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          使用uuid作為數(shù)據(jù)庫(kù)主鍵,被技術(shù)總監(jiān)懟了!

          共 17456字,需瀏覽 35分鐘

           ·

          2021-05-12 15:11

          一、前言

          在日常開發(fā)中,數(shù)據(jù)庫(kù)中主鍵id的生成方案,主要有三種

          • 數(shù)據(jù)庫(kù)自增ID
          • 采用隨機(jī)數(shù)生成不重復(fù)的ID
          • 采用jdk提供的uuid

          對(duì)于這三種方案,我發(fā)現(xiàn)在數(shù)據(jù)量少的情況下,沒(méi)有特別的差異,但是當(dāng)單表的數(shù)據(jù)量達(dá)到百萬(wàn)級(jí)以上時(shí)候,他們的性能有著顯著的區(qū)別,光說(shuō)理論不行,還得看實(shí)際程序測(cè)試,今天就帶著大家一探究竟!

          二、程序?qū)嵗?/span>

          首先,我們?cè)诒镜財(cái)?shù)據(jù)庫(kù)中創(chuàng)建三張單表tb_uuid_1tb_uuid_2、tb_uuid_3,同時(shí)設(shè)置tb_uuid_1表的主鍵為自增長(zhǎng)模式,腳本如下:

          CREATE TABLE `tb_uuid_1` (
            `id` bigint(20unsigned NOT NULL AUTO_INCREMENT,
            `name` varchar(20DEFAULT NULL,
            PRIMARY KEY (`id`)
          ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='主鍵ID自增長(zhǎng)';
          CREATE TABLE `tb_uuid_2` (
            `id` bigint(20unsigned NOT NULL,
            `name` varchar(20DEFAULT NULL,
            PRIMARY KEY (`id`)
          ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='主鍵ID隨機(jī)數(shù)生成';
          CREATE TABLE `tb_uuid_3` (
            `id` varchar(50)  NOT NULL,
            `name` varchar(20DEFAULT NULL,
            PRIMARY KEY (`id`)
          ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='主鍵采用uuid生成';

          下面,我們采用Springboot + mybatis來(lái)實(shí)現(xiàn)插入測(cè)試。

          2.1、數(shù)據(jù)庫(kù)自增

          以數(shù)據(jù)庫(kù)自增為例,首先編寫好各種實(shí)體、數(shù)據(jù)持久層操作,方便后續(xù)進(jìn)行測(cè)試

          /**
           * 表實(shí)體
           */

          public class UUID1 implements Serializable {

              private Long id;

              private String name;
            
            //省略set、get
          }
          /**
           * 數(shù)據(jù)持久層操作
           */

          public interface UUID1Mapper {

              /**
               * 自增長(zhǎng)插入
               * @param uuid1
               */

              @Insert("INSERT INTO tb_uuid_1(name) VALUES(#{name})")
              void insert(UUID1 uuid1);
          }
          /**
           * 自增ID,單元測(cè)試
           */

          @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("花費(fèi)時(shí)間:" +  (end - start));
          }

          2.2、采用隨機(jī)數(shù)生成ID

          這里,我們采用twitter的雪花算法來(lái)實(shí)現(xiàn)隨機(jī)數(shù)ID的生成,工具類如下:

          public class SnowflakeIdWorker {

              private static SnowflakeIdWorker instance = new SnowflakeIdWorker(0,0);

              /**
               * 開始時(shí)間截 (2015-01-01)
               */

              private final long twepoch = 1420041600000L;
              /**
               * 機(jī)器id所占的位數(shù)
               */

              private final long workerIdBits = 5L;
              /**
               * 數(shù)據(jù)標(biāo)識(shí)id所占的位數(shù)
               */

              private final long datacenterIdBits = 5L;
              /**
               * 支持的最大機(jī)器id,結(jié)果是31 (這個(gè)移位算法可以很快的計(jì)算出幾位二進(jìn)制數(shù)所能表示的最大十進(jìn)制數(shù))
               */

              private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
              /**
               * 支持的最大數(shù)據(jù)標(biāo)識(shí)id,結(jié)果是31
               */

              private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
              /**
               * 序列在id中占的位數(shù)
               */

              private final long sequenceBits = 12L;
              /**
               * 機(jī)器ID向左移12位
               */

              private final long workerIdShift = sequenceBits;
              /**
               * 數(shù)據(jù)標(biāo)識(shí)id向左移17位(12+5)
               */

              private final long datacenterIdShift = sequenceBits + workerIdBits;
              /**
               * 時(shí)間截向左移22位(5+5+12)
               */

              private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
              /**
               * 生成序列的掩碼,這里為4095 (0b111111111111=0xfff=4095)
               */

              private final long sequenceMask = -1L ^ (-1L << sequenceBits);
              /**
               * 工作機(jī)器ID(0~31)
               */

              private long workerId;
              /**
               * 數(shù)據(jù)中心ID(0~31)
               */

              private long datacenterId;
              /**
               * 毫秒內(nèi)序列(0~4095)
               */

              private long sequence = 0L;
              /**
               * 上次生成ID的時(shí)間截
               */

              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;
              }
              /**
               * 獲得下一個(gè)ID (該方法是線程安全的)
               * @return SnowflakeId
               */

              public synchronized long nextId() {
                  long timestamp = timeGen();
                  // 如果當(dāng)前時(shí)間小于上一次ID生成的時(shí)間戳,說(shuō)明系統(tǒng)時(shí)鐘回退過(guò)這個(gè)時(shí)候應(yīng)當(dāng)拋出異常
                  if (timestamp < lastTimestamp) {
                      throw new RuntimeException(
                              String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
                  }
                  // 如果是同一時(shí)間生成的,則進(jìn)行毫秒內(nèi)序列
                  if (lastTimestamp == timestamp) {
                      sequence = (sequence + 1) & sequenceMask;
                      // 毫秒內(nèi)序列溢出
                      if (sequence == 0) {
                          //阻塞到下一個(gè)毫秒,獲得新的時(shí)間戳
                          timestamp = tilNextMillis(lastTimestamp);
                      }
                  }
                  // 時(shí)間戳改變,毫秒內(nèi)序列重置
                  else {
                      sequence = 0L;
                  }
                  // 上次生成ID的時(shí)間截
                  lastTimestamp = timestamp;
                  // 移位并通過(guò)或運(yùn)算拼到一起組成64位的ID
                  return ((timestamp - twepoch) << timestampLeftShift) //
                          | (datacenterId << datacenterIdShift) //
                          | (workerId << workerIdShift) //
                          | sequence;
              }
              /**
               * 阻塞到下一個(gè)毫秒,直到獲得新的時(shí)間戳
               * @param lastTimestamp 上次生成ID的時(shí)間截
               * @return 當(dāng)前時(shí)間戳
               */

              protected long tilNextMillis(long lastTimestamp) {
                  long timestamp = timeGen();
                  while (timestamp <= lastTimestamp) {
                      timestamp = timeGen();
                  }
                  return timestamp;
              }
              /**
               * 返回以毫秒為單位的當(dāng)前時(shí)間
               * @return 當(dāng)前時(shí)間(毫秒)
               */

              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();
              }
          }

          最后的單元測(cè)試,代碼如下:

          @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;

              /**
               * 測(cè)試自增ID耗時(shí)
               */

              @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,花費(fèi)時(shí)間:" +  (end - start));
              }

              /**
               * 測(cè)試采用雪花算法生產(chǎn)的隨機(jī)數(shù)ID耗時(shí)
               */

              @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("花費(fèi)時(shí)間:" +  (end - start));
              }

              /**
               * 測(cè)試采用UUID生成的ID耗時(shí)
               */

              @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("花費(fèi)時(shí)間:" +  (end - start));
              }
          }

          三、性能測(cè)試

          程序環(huán)境搭建完成之后,啥也不說(shuō)了,直接擼起袖子,將單元測(cè)試跑起來(lái)!

          首先測(cè)試一下,插入100萬(wàn)數(shù)據(jù)的情況下,三者直接的耗時(shí)結(jié)果如下:

          在原有的數(shù)據(jù)量上,我們繼續(xù)插入30萬(wàn)條數(shù)據(jù),三者耗時(shí)結(jié)果如下:

          可以看出在數(shù)據(jù)量 100W 左右的時(shí)候,uuid的插入效率墊底,隨著插入的數(shù)據(jù)量增長(zhǎng),uuid 生成的ID插入呈直線下降!

          時(shí)間占用量總體效率排名為:自增ID > 雪花算法生成的ID >> uuid生成的ID。

          在數(shù)據(jù)量較大的情況下,為什么uuid生成的ID遠(yuǎn)不如自增ID呢

          關(guān)于這點(diǎn),我們可以從 mysql 主鍵存儲(chǔ)的內(nèi)部結(jié)構(gòu)來(lái)進(jìn)行分析。

          3.1、自增ID內(nèi)部結(jié)構(gòu)

          自增的主鍵的值是順序的,所以 Innodb 把每一條記錄都存儲(chǔ)在一條記錄的后面。

          當(dāng)達(dá)到頁(yè)面的最大填充因子時(shí)候(innodb默認(rèn)的最大填充因子是頁(yè)大小的15/16,會(huì)留出1/16的空間留作以后的修改),會(huì)進(jìn)行如下操作:

          • 下一條記錄就會(huì)寫入新的頁(yè)中,一旦數(shù)據(jù)按照這種順序的方式加載,主鍵頁(yè)就會(huì)近乎于順序的記錄填滿,提升了頁(yè)面的最大填充率,不會(huì)有頁(yè)的浪費(fèi)
          • 新插入的行一定會(huì)在原有的最大數(shù)據(jù)行下一行,mysql定位和尋址很快,不會(huì)為計(jì)算新行的位置而做出額外的消耗

          3.2、使用uuid的索引內(nèi)部結(jié)構(gòu)

          uuid相對(duì)順序的自增id來(lái)說(shuō)是毫無(wú)規(guī)律可言的,新行的值不一定要比之前的主鍵的值要大,所以innodb無(wú)法做到總是把新行插入到索引的最后,而是需要為新行尋找新的合適的位置從而來(lái)分配新的空間。

          這個(gè)過(guò)程需要做很多額外的操作,數(shù)據(jù)的毫無(wú)順序會(huì)導(dǎo)致數(shù)據(jù)分布散亂,將會(huì)導(dǎo)致以下的問(wèn)題:

          • 寫入的目標(biāo)頁(yè)很可能已經(jīng)刷新到磁盤上并且從緩存上移除,或者還沒(méi)有被加載到緩存中,innodb在插入之前不得不先找到并從磁盤讀取目標(biāo)頁(yè)到內(nèi)存中,這將導(dǎo)致大量的隨機(jī)IO
          • 因?yàn)閷懭胧莵y序的,innodb不得不頻繁的做頁(yè)分裂操作,以便為新的行分配空間,頁(yè)分裂導(dǎo)致移動(dòng)大量的數(shù)據(jù),一次插入最少需要修改三個(gè)頁(yè)以上
          • 由于頻繁的頁(yè)分裂,頁(yè)會(huì)變得稀疏并被不規(guī)則的填充,最終會(huì)導(dǎo)致數(shù)據(jù)會(huì)有碎片

          在把值載入到聚簇索引(innodb默認(rèn)的索引類型)以后,有時(shí)候會(huì)需要做一次OPTIMEIZE TABLE來(lái)重建表并優(yōu)化頁(yè)的填充,這將又需要一定的時(shí)間消耗。

          因此,在選擇主鍵ID生成方案的時(shí)候,盡可能別采用uuid的方式來(lái)生成主鍵ID,隨著數(shù)據(jù)量越大,插入性能會(huì)越低!

          四、總結(jié)

          在實(shí)際使用過(guò)程中,推薦使用主鍵自增ID和雪花算法生成的隨機(jī)ID。

          但是使用自增ID也有缺點(diǎn):

          1. 別人一旦爬取你的數(shù)據(jù)庫(kù),就可以根據(jù)數(shù)據(jù)庫(kù)的自增id獲取到你的業(yè)務(wù)增長(zhǎng)信息,很容易進(jìn)行數(shù)據(jù)竊取。

          2. 其次,對(duì)于高并發(fā)的負(fù)載,innodb在按主鍵進(jìn)行插入的時(shí)候會(huì)造成明顯的鎖爭(zhēng)用,主鍵的上界會(huì)成為爭(zhēng)搶的熱點(diǎn),因?yàn)樗械牟迦攵及l(fā)生在這里,并發(fā)插入會(huì)導(dǎo)致間隙鎖競(jìng)爭(zhēng)。

          總結(jié)起來(lái),如果業(yè)務(wù)量小,推薦采用自增ID,如果業(yè)務(wù)量大,推薦采用雪花算法生成的隨機(jī)ID。

          本篇文章主要從實(shí)際程序?qū)嵗霭l(fā),討論了三種主鍵ID生成方案的性能差異, 鑒于筆者才疏學(xué)淺,可能也有理解不到位的地方,歡迎網(wǎng)友們批評(píng)指出!

          五、參考

          1、方志明 - 使用雪花id或uuid作為Mysql主鍵,被老板懟了一頓!



          瀏覽 36
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  涩小说校园春色图片区视频区小说区 | 炮友五月天 | 国产三级大全 | 操穴网 | 久久成人片 |