MySQL uuid及其相關的一些簡單性能測試

運維同事導入一批大約500萬左右的數據,耗時較久。他使用的是純SQL導入,主鍵使用的是UUID,因爲業務原因沒有使用自增ID。
因爲是內網,不能遠程訪問。
通過溝通,大致覺得有兩個原因,一是因爲UUID作爲主鍵,二是表字段繁多,單行加起來接近10000的長度引起行溢出。

因爲是臨時一次性任務,同事沒有做深糾,我在這裏簡單做一個驗證。
版本:5.7.19
引擎:innodb


uuid主鍵的影響


mysql自帶的UUID()函數簡單方便,不重複。但是它缺點也是衆所周知的。

  • UUID的返回值通常是隨機的,而InnoDB的表實質是以主鍵組織存儲的索引,插入新的記錄不是順序追加,而會往前插入,造成頁分裂,表的再平衡。在數據量越大的情況,性能影響越嚴重。

  • 主鍵包含在每個二級索引中,過長的主鍵會浪費磁盤和內存的空間。


頁分裂


爲什麼主鍵ID的順序這麼重要?

mysql innodb引擎數據結構爲B+tree,查找的時候二分查找,這就要求數據是按順序存儲的。
而UUID是無序的,它作爲主鍵在寫入的時候,就可能會頻繁的進行中間插入和頁分裂。頁分裂會造成已有的數據移位,類似於arraylist進行插入數據。
因此表數據越多,插入的效率就會越低。也會使得磁盤空間利用率降低。


除了UUID,其它還有兩種常見的ID生成法。
一是自增id,最簡單效率也最高,但不適合分佈式唯一主鍵,和在一些敏感業務如訂單註冊用戶時,可能通過ID值會暴露一些商業祕密。


雪花算法


再一個是雪花算法,最常見的分佈式ID生成算法。

可以看到它跟時間戳(毫秒級)和服務器相關,通過12bit序列號區分同毫秒內產生的不同id。
因此能保證在單臺服務器上的大致趨勢遞增。 在併發高的情況下不能保證完全遞增,因此需考慮到這一點。

它有時鐘回撥的問題,這裏不展開。


定量驗證

以上只是定性的判斷。


下面做一下定量的測試。

本文驗證自增id,雪花算法ID,以及uuid作爲主鍵3種情況,往3張空表插入500萬條數據,每個批次10萬,共50個批次。

        StopWatch stopWatch = new StopWatch();
        stopWatch.start("準備數據階段");
        int count = 500 * 10000 ;
        List<TUser> list = new ArrayList<>(count);
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        List<TUser> finalList = list;
        Semaphore semaphore = new Semaphore(100);
        for (int i = 0; i < count; i++) {
            executorService.submit(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    semaphore.acquire();
                    TUser user = new TUser();
                    // 雪花算法ID
                    user.setId(SnowflakeIdUtil.snowflakeId());
                    user.setName(testService.getRandomName());
                    // UUID
                    user.setId(UUID.randomUUID().toString());
                    finalList.add(user);
                    semaphore.release();
                }
            }).get();
        }

        stopWatch.stop();

        System.out.println("開始寫入");

        List<Time> times = new ArrayList<>();
        stopWatch.start("寫入數據階段");
        List<TUser> subList = new ArrayList<>(100000);
        for (int i = 1; i <= count; i++) {
            TUser user = finalList.get(i-1);
            subList.add(user);
            if (i % 100000 == 0) {
                // finalList.remove(user);
                long start = System.currentTimeMillis();
                tUserDao.insertBatch(subList);
                long time = System.currentTimeMillis() - start;
                times.add(new Time(time));
                subList.clear();
                log.info("寫入一次" + i);
            }
        }

        stopWatch.stop();
        System.out.println(stopWatch.prettyPrint());
        // 將每個批次耗時打印出來,放入excel生成圖表
        times.stream().map(e -> e.getTime()).forEach(System.out::println);

總耗時情況

UU ID:414321ms
雪花ID 167748ms
自增ID 75829ms


將最後每個批次耗時的集合打印出來,放到Excel中,生成圖表。

縱座標表示耗時,單位ms
橫座標表示寫入批次



可以明顯的觀察到,使用UUID作爲主鍵,在表數據量在350萬以後,耗時上升極快,性能呈指數級下降。
同時自增ID效率高於雪花算法ID,都很穩定。

這是在行數據量極小(<50)的情況下。

在行大小> 8000,在頁16KB至少兩行數據的情況下,也必然會發生行溢出。這裏再看看測試情況。


行溢出情況

MySQL單行數據最大能存儲65535字節的數據,InnoDB的頁爲16KB,即16384字節,當行的實際存儲長度超過16384/2的長度時,就會行溢出。
爲什麼是/2,因爲頁至少需要存儲兩行數據。
爲什麼單頁至少存儲兩行數據,如果爲1,B+tree就退化爲了鏈表。

當行溢出的時候,會將大字段數據存放在頁類型爲Uncompress BLOB頁中,在當前頁就會對大字段建立一個引用,類假於二級索引。

所以設置測試表name長度大於大約16384/2的時候,就會發生行溢出現象。


行溢出時,3種不同ID的表現


測試代碼跟上面類似。
再準備3張空表。
字段類型varchar,長度10000,user.setName()設置實際長度大於8000。

這次只寫入50萬條數據,每批次1萬條數據,共50個批次。

這次uuid在寫入30萬條記錄後,耗時開始明顯上升。


以上是行溢出的情況下,UUID,雪花算法,自增ID等 3種ID的寫入性能情況。


uuid在溢出和不溢出下的表現


下面來測試一下,使用UUID在行溢出和不溢出的兩種情況下寫入性能情況。

再新建兩張表,t_user_uuid_long,此表name字段長度100000,最終寫入字符長度9995
t_user_uuid_short,此表name字段長度8000,最終寫入字符長度7995

寫入耗時曲線大致相當,看不出明顯差距。


但是讀的差距非常明顯!

因爲t_user_uuid_long 中的name字段發生了行溢出,。。。。。select的時候需要跨頁提取數據,類似於做了1次回表,而且回表還是跨頁的。

select id,name from t_user_uuid_short order by id asc limit 1000
耗時0.145s

select id,name from t_user_uuid_long order by id asc limit 1000
耗時4.153s,性能相差接近30倍。

小結

以上測試結果只是在測試機上的粗糙測試結果,不是基準測試,只做參考。

測試結果表明,運維同事的慢操作可能受UUID影響 ,但大字段對寫入的影響並不明顯。
不過大字段對讀取的性能影響明顯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章