CLOVER🍀

That was when it all began.

JavaでUUIDを扱えるライブラリーを調べる

これは、なにをしたくて書いたもの?

JavaでUUIDを使おうと思ったら、java.util.UUID#randomUUID#toStringでバージョン4のUUIDを使っていると思います。

UUID (Java SE 17 & JDK 17)

でも、UUIDってバージョン4以外にも種類がありましたよね、ということで、Javaでバージョン4以外のUUIDを扱えるライブラリーを
調べてみました。

UUIDのバージョン

そもそも、UUIDについて少し見ておきましょう。UUIDのRFCはこちらです。

RFC 4122: A UUID URN Namespace

UUIDは128ビットの数字で、一意の識別子となるものです。以下のような感じですね。

jshell> java.util.UUID.randomUUID().toString()
$1 ==> "a3240b70-d32d-4551-86d2-3bf5fc07649f"

UUIDの形式は以下となっています。

      The formal definition of the UUID string representation is
      provided by the following ABNF [7]:

      UUID                   = time-low "-" time-mid "-"
                               time-high-and-version "-"
                               clock-seq-and-reserved
                               clock-seq-low "-" node
      time-low               = 4hexOctet
      time-mid               = 2hexOctet
      time-high-and-version  = 2hexOctet
      clock-seq-and-reserved = hexOctet
      clock-seq-low          = hexOctet
      node                   = 6hexOctet
      hexOctet               = hexDigit hexDigit
      hexDigit =
            "0" / "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9" /
            "a" / "b" / "c" / "d" / "e" / "f" /
            "A" / "B" / "C" / "D" / "E" / "F"

time-high-and-versionフィールドにはバージョンが含まれていて、-区切りの3つ目の最初の文字はバージョン番号になります。

The version number is in the most significant 4 bits of the time
stamp (bits 4 through 7 of the time_hi_and_version field).

たとえば、先ほどの例だと4551の部分の4がタイプ4であることを表しています。

jshell> java.util.UUID.randomUUID().toString()
$1 ==> "a3240b70-d32d-4551-86d2-3bf5fc07649f"

RFC 4122では、5つのバージョンのUUIDが定められています。

  • バージョン1
  • バージョン2
    • DCEセキュリティUUIDと呼ばれ、バージョン1の一部をPOSIXのユーザーIDやグループIDで差し替えたもの
  • バージョン3
  • バージョン4
    • 乱数から生成したもの
  • バージョン5

そしてさらに、ドラフト段階のものが提案されていて、この中にはバージョン6、7、8が含まれています。

A Universally Unique IDentifier (UUID) URN Namespace

GitHub - uuid6/uuid6-ietf-draft: Next Generation UUID Formats

この中では、UUIDは一意である特性を活かしてデータベースのキーとしてよく使われるものの、オートインクリメントなものとしては
使えないということが言われています。

One area UUIDs have gained popularity is as database keys. This stems from the increasingly distributed nature of modern applications. In such cases, "auto increment" schemes often used by databases do not work well, as the effort required to coordinate unique numeric identifiers across a network can easily become a burden. The fact that UUIDs can be used to create unique, reasonably short values in distributed systems without requiring synchronization makes them a good alternative, but UUID versions 1-5 lack certain other desirable characteristics

そして、ソート可能な識別子を定めることが動機になっています。

Due to the aforementioned issue, many widely distributed database applications and large application vendors have sought to solve the problem of creating a better time-based, sortable unique identifier for use as a database key. This has lead to numerous implementations over the past 10+ years solving the same problem in slightly different ways.

それぞれのバージョンの特性は、以下です。

  • バージョン6
    • バージョン1を改善し、タイムスタンプの情報をソート可能な形に見直したもの
  • バージョン7
    • Unixエポックタイムスタンプから派生したフィールド持ち、バージョン1〜6よりエントロピーを改善したもの
  • バージョン8

バージョン8は、ちょっと扱いが違いますね。

まだドラフト段階ですが、バージョン7がよく使われることになりそうですね。

なお、ソート可能な一意なIDなら、ULIDというものもあります。

JavaでULIDを使いたい(Sulky ULIDを使う) - CLOVER🍀

JavaでのUUIDの選択肢

ここでJavaでのUUIDの選択肢を探してみると、以下のような感じでした。

更新状態などを見て、実際に使おうと思うと以下あたりから絞り込むのかなと思います。

今回は、Java Uuid Generator(JUG)を試してみます。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.8 2023-07-18
OpenJDK Runtime Environment (build 17.0.8+7-Ubuntu-122.04)
OpenJDK 64-Bit Server VM (build 17.0.8+7-Ubuntu-122.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.4 (dfbb324ad4a7c8fb0bf182e6d91b0ae20e3d2dd9)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.8, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-78-generic", arch: "amd64", family: "unix"

Java Uuid Generator(JUG)を使う

Java Uuid Generator(JUG)のGitHubリポジトリーは、こちら。

GitHub - cowtowncoder/java-uuid-generator: Java Uuid Generator (JUG) is a library for generating all (3) types of UUIDs on Java. See (http://github.com/tlaukkan/mono-uuid-generator) for C#-based sister project!

ドキュメントはREADME.mdのみで、WikiからはJavadocをたどることができます。

Home · cowtowncoder/java-uuid-generator Wiki · GitHub

Java UUID Generator 4.2.0 API

使い方は、こちら。

Java Uuid Generator (JUG) / Usage

依存関係を追加して

Java Uuid Generator (JUG) / Usage / Maven Dependency

こちらに従ってUUIDを生成するライブラリーとして使います。

Java Uuid Generator (JUG) / Usage / Using JUG as Library

また、単体でCLIとしても使えるようです。

Java Uuid Generator (JUG) / Usage / Using JUG as CLI

今回は、ライブラリーとして使ってみます。

準備

Maven依存関係等はこちら。

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.fasterxml.uuid</groupId>
            <artifactId>java-uuid-generator</artifactId>
            <version>4.2.0</version>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.24.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.1.2</version>
            </plugin>
        </plugins>
    </build>

Java Uuid Generator(JUG)に必要な依存関係は、こちらですね。

        <dependency>
            <groupId>com.fasterxml.uuid</groupId>
            <artifactId>java-uuid-generator</artifactId>
            <version>4.2.0</version>
        </dependency>

確認はテストコードでやるので、JUnit 5とAssertJを入れています。

使ってみる

では、Java Uuid Generator(JUG)を使ってみます。

テストコードの雛形はこちら。

src/test/java/org/littlewings/uuid/JugGettingStartedTest.java

package org.littlewings.uuid;

import com.fasterxml.uuid.Generators;
import com.fasterxml.uuid.impl.*;
import org.junit.jupiter.api.Test;

import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;

class JugGettingStartedTest {
    // ここに、テストを書く!
}

以下のREADME.mdにも書かれているように、起点はcom.fasterxml.uuid.Generatorsになります。

Java Uuid Generator (JUG) / Usage / Using JUG as LibraryGenerating UUIDs

こんな感じですね。

    @Test
    void simple() {
        UUID uuidV1 = Generators.timeBasedGenerator().generate();
        System.out.printf("UUID v1 = %s%n", uuidV1.toString());
        assertThat(uuidV1.toString()).containsPattern("^[^-]+-[^-]+-1[^-]+-[^-]+-[^-]+$");

        UUID uuidV4 = Generators.randomBasedGenerator().generate();
        System.out.printf("UUID v4 = %s%n", uuidV4.toString());
        assertThat(uuidV4.toString()).containsPattern("^[^-]+-[^-]+-4[^-]+-[^-]+-[^-]+$");

        UUID uuidV5 = Generators.nameBasedGenerator().generate("source name");
        System.out.printf("UUID v5 = %s%n", uuidV5.toString());
        assertThat(uuidV5.toString()).containsPattern("^[^-]+-[^-]+-5[^-]+-[^-]+-[^-]+$");

        UUID uuidV6 = Generators.timeBasedReorderedGenerator().generate();
        System.out.printf("UUID v6 = %s%n", uuidV6.toString());
        assertThat(uuidV6.toString()).containsPattern("^[^-]+-[^-]+-6[^-]+-[^-]+-[^-]+$");

        UUID uuidV7 = Generators.timeBasedEpochGenerator().generate();
        System.out.printf("UUID v7 = %s%n", uuidV7.toString());
        assertThat(uuidV7.toString()).containsPattern("^[^-]+-[^-]+-7[^-]+-[^-]+-[^-]+$");
    }

Generatorsから目的にあったcom.fasterxml.uuid.UUIDGeneratorクラスのサブクラスのインスタンスを生成し、generateメソッドを
使うことでUUIDが得られます。

System.out.printfしている結果はこちらです。

UUID v1 = acaffd7b-3465-11ee-a8c8-d57e5908feb4
UUID v4 = da265cbd-2c53-46fa-a65c-bd2c53a6fa22
UUID v5 = 6c7475b2-9251-5490-82ca-c2d1a2af1b98
UUID v6 = 1ee3465a-cb8f-6e2c-a8c8-1d53d7fe1737
UUID v7 = 0189cb40-f3ea-74f8-b863-7f05ac9cd0c8

正規表現で確認していますが、-区切りの3つ目の先頭はバージョン番号になっていますね。

ちょっと驚いたのは、生成されるUUIDはjava.util.UUIDインスタンスだということです。独自のクラスになっているのかな?と思って
いたのですが、そうではありませんでした。

com.fasterxml.uuid.UUIDGeneratorクラスのインスタンスは、こんな感じに保持して使いまわすことができます。

    @Test
    void generator() {
        TimeBasedGenerator timeBasedGenerator = Generators.timeBasedGenerator();
        assertThat(timeBasedGenerator.generate().toString()).containsPattern("^[^-]+-[^-]+-1[^-]+-[^-]+-[^-]+$");
        assertThat(timeBasedGenerator.generate().toString()).containsPattern("^[^-]+-[^-]+-1[^-]+-[^-]+-[^-]+$");

        RandomBasedGenerator randomBasedGenerator = Generators.randomBasedGenerator();
        assertThat(randomBasedGenerator.generate().toString()).containsPattern("^[^-]+-[^-]+-4[^-]+-[^-]+-[^-]+$");
        assertThat(randomBasedGenerator.generate().toString()).containsPattern("^[^-]+-[^-]+-4[^-]+-[^-]+-[^-]+$");

        NameBasedGenerator nameBasedGenerator = Generators.nameBasedGenerator();
        assertThat(nameBasedGenerator.generate("source name1").toString()).containsPattern("^[^-]+-[^-]+-5[^-]+-[^-]+-[^-]+$");
        assertThat(nameBasedGenerator.generate("source name2").toString()).containsPattern("^[^-]+-[^-]+-5[^-]+-[^-]+-[^-]+$");

        TimeBasedReorderedGenerator timeBasedReorderedGenerator = Generators.timeBasedReorderedGenerator();
        assertThat(timeBasedReorderedGenerator.generate().toString()).containsPattern("^[^-]+-[^-]+-6[^-]+-[^-]+-[^-]+$");
        assertThat(timeBasedReorderedGenerator.generate().toString()).containsPattern("^[^-]+-[^-]+-6[^-]+-[^-]+-[^-]+$");

        TimeBasedEpochGenerator timeBasedEpochGenerator = Generators.timeBasedEpochGenerator();
        assertThat(timeBasedEpochGenerator.generate().toString().toString()).containsPattern("^[^-]+-[^-]+-7[^-]+-[^-]+-[^-]+$");
        assertThat(timeBasedEpochGenerator.generate().toString().toString()).containsPattern("^[^-]+-[^-]+-7[^-]+-[^-]+-[^-]+$");
    }

README.mdによると、各Generatorはスレッドセーフだそうです。

Generators are fully thread-safe, so a single instance may be shared among multiple threads.

com.fasterxml.uuid.GeneratorsJavadocも見ておくとよいでしょう。

Generators (Java UUID Generator 4.2.0 API)

一意なIDが生成されることを確認する

最後に、大量のUUID生成+マルチスレッドで一意なIDが作成できているか、簡単に確認するテストを書いてみます。

src/test/java/org/littlewings/uuid/CreatedUUIDsTest.java

package org.littlewings.uuid;

import com.fasterxml.uuid.Generators;
import com.fasterxml.uuid.impl.*;
import org.junit.jupiter.api.Test;

import java.util.Set;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.function.IntFunction;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static org.assertj.core.api.Assertions.assertThat;

class CreatedUUIDsTest {
    void uniqueness(IntFunction<UUID> generator, int times) {
        ExecutorService es = Executors.newFixedThreadPool(10);

        try {
            Set<Future<String>> futures =
                    IntStream
                            .rangeClosed(1, times)
                            .mapToObj(i -> es.submit(() -> generator.apply(i).toString()))
                            .collect(Collectors.toSet());

            Set<String> ids =
                    futures
                            .stream()
                            .map(f -> {
                                try {
                                    return f.get();
                                } catch (ExecutionException | InterruptedException e) {
                                    throw new RuntimeException(e);
                                }
                            })
                            .collect(Collectors.toSet());

            assertThat(ids).hasSize(times);
        } finally {
            es.shutdown();

            try {
                es.awaitTermination(10L, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                // ignore
            }
        }
    }

    @Test
    void v1() {
        TimeBasedGenerator timeBasedGenerator = Generators.timeBasedGenerator();
        uniqueness(count -> timeBasedGenerator.generate(), 10_000_000);
    }

    @Test
    void v4() {
        RandomBasedGenerator randomBasedGenerator = Generators.randomBasedGenerator();
        uniqueness(count -> randomBasedGenerator.generate(), 10_000_000);
    }

    @Test
    void v5() {
        NameBasedGenerator nameBasedGenerator = Generators.nameBasedGenerator();
        uniqueness(count -> nameBasedGenerator.generate("source name" + count), 10_000_000);
    }

    @Test
    void v6() {
        TimeBasedReorderedGenerator timeBasedReorderedGenerator = Generators.timeBasedReorderedGenerator();
        uniqueness(count -> timeBasedReorderedGenerator.generate(), 10_000_000);
    }

    @Test
    void v7() {
        TimeBasedEpochGenerator timeBasedEpochGenerator = Generators.timeBasedEpochGenerator();
        uniqueness(count -> timeBasedEpochGenerator.generate(), 10_000_000);
    }
}

確かに、マルチスレッド環境下でも問題にはならなさそうですね。

ところでシングルスレッドなどいろんなパターンで確認してみたのですが、バージョン4のUUID生成が1番速度が出ない傾向にあるように
見えますね。

こんなところで。

まとめ

Javaの標準ライブラリーで使うことができる、バージョン4以外のUUIDを扱えるライブラリーを調べつつ、そもそもUUIDについても
少し調べてまとめてみました。

バージョン6以降のUUIDがドラフトで存在することは知っていたのですが、今回ちゃんと見る機会を作っておいてよかったかなと。

正式に決まった時に、またキャッチアップしたいところですね。