Javaで実現するUUID v7：時刻順序を考慮したユニーク識別子

この記事は[グロースエクスパートナーズ アドベントカレンダー2023](https://qiita.com/advent-calendar/2023/gxp)の13日目の記事です。
今年は2023年新卒入社の方などの投稿で盛り上がり、シリーズ２もつくられました。シリーズ２にふさわしい、簡単に読めて役立つ記事をお届けしたい所存です。

# はじめに

UUID v7を試してみよう

この記事では時間順序を持ち、ランダム性も備えたUUID v7を紹介するとともに、JavaでのUUID v7の生成とUUID v7からタイムスタンプを取得する実装も紹介します。

## 要約
* UUID v4をRDBのプリマリーキーとして用いると、B-Treeインデックスの断片化や挿入処理遅延の懸念がある
* 解決策としてUUID v7が提案されている
* JavaにもUUID v7を生成するライブラリがあり利用可能になっている
* 生成したUUID v7の値からタイムスタンプが取得できる

# UUID v4の課題
ユニークな値が欲しいとき、UUIDが真っ先に浮かぶという方は多いと思います。
下記のような文字列を見たことがあるかもしれませんが、実際は128bitの数値です。
```
4e390b5a-c22f-4702-a16e-2921f5c75c44
```

UUIDとして広く使われているのはUUID v4というものです。
v4がどんなものかというと、[RFCの仕様](https://www.ietf.org/rfc/rfc4122.txt)をみれば、ほぼランダムな値から構成されていることがわかります。

> ```
>4.4.  Algorithms for Creating a UUID from Truly Random or
>      Pseudo-Random Numbers
>
>   The version 4 UUID is meant for generating UUIDs from truly-random or
>   pseudo-random numbers.
>
>   The algorithm is as follows:
>
>   o  Set the two most significant bits (bits 6 and 7) of the
>      clock_seq_hi_and_reserved to zero and one, respectively.
>
>   o  Set the four most significant bits (bits 12 through 15) of the
>      time_hi_and_version field to the 4-bit version number from
>      Section 4.1.3.
>
>   o  Set all the other bits to randomly (or pseudo-randomly) chosen
>      values.
> ```
引用元：https://www.ietf.org/rfc/rfc4122.txt から
      

UUID v4は予測困難な一意の値にできるという意味では非常に有効ですが、 **UUID v4をテーブルのキーにするとデータの断片化がおきやすく、性能問題につながる可能性がある** というデメリットがあります。

# 対策としてのUUID v7
そんな課題を解決するためUUID v7がIETFに提案されています。現在はドラフトで正式に発行される前の状態のようです。

[UUID 新フォーマットに関するドラフト](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7)

資料によると、先頭48bitにUUID生成時エポックミリ秒を持つことがわかります。これによって、順序とランダム性も持ち合わせるということが可能になっています。
```
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           unix_ts_ms                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          unix_ts_ms           |  ver  |       rand_a          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var|                        rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
引用元：https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuidv7-field-and-bit-layout


# JavaでUUID v7を扱う
Javaの標準クラスライブラリに存在する`java.util.UUID`クラスはJDK21の時点でまだv7の生成は実装されていません。Javaでのuuid v7を利用可能にするライブラリとしては以下が存在します。

[cowtowncoder/java-uuid-generator](https://github.com/cowtowncoder/java-uuid-generator) 

このライブラリを使うサンプルコードを掲載します。
まずはgradle等のビルドツールで依存関係を追加します。

```groovy: build.gradle
dependencies {
    implementation 'com.fasterxml.uuid:java-uuid-generator:4.3.0'
}
```
## UUID v7を生成する
`com.fasterxml.uuid.impl.TimeBasedEpochGenerator`を用いることでUUID v7を生成することができます。

```java
package jp.co.gxp.adcal_2023;

import java.util.UUID;

import com.fasterxml.uuid.Generators;
import com.fasterxml.uuid.impl.TimeBasedEpochGenerator;

/**
 * UUID v7形式のIDを生成します.
 */
public class IdGenerator {

    // 生成の度にロックを取得する方式なので、
    // 生成時間が気になる場合には、
    // 例えばキーの種類ごとにgeneratorインスタンスを持った方がよさそう[※1]
    private static final TimeBasedEpochGenerator GENERATOR;
    static {
        GENERATOR = Generators.timeBasedEpochGenerator();
    }

    public static UUID generateId() {
        return GENERATOR.generate();
    }
}
```
※1: [com.fasterxml.uuid.impl.TimeBasedEpochGenerator#construct](https://github.com/cowtowncoder/java-uuid-generator/blob/master/src/main/java/com/fasterxml/uuid/impl/TimeBasedEpochGenerator.java#L121C20-L121C20) 参照

## UUID v7からタイムスタンプを取得する
先頭48bitがUnix timestamp milli secondsです。
uuidは128bitなので、先頭64bitをlongとして取得し、16bitシフトライトすることでタイムスタンプ値を得られます。
```java
package jp.co.gxp.adcal_2023;

import java.time.Instant;
import java.util.UUID;

public final class UUIDTimeUtil {

    /**
     * UUID v7からタイムスタンプを取得します.
     * nullチェックとかuuidバージョンチェックは割愛.
     */
    public static Instant timestampFrom(UUID v7) {
        long epochMilli = v7.getMostSignificantBits() >> 16; 
        return Instant.ofEpochMilli(epochMilli);
    }

}
```
`java.time.Instant`のインスタンスが得られるので、例えば以下のコードでOffsetDateTimeに変換できます。
```java
// 2023-12-12T09:16:13.039+09:00 などが得られる
OffsetDateTime timestamp = timestampFrom(IdGenerator.generateId())
  .atZone(ZoneId.systemDefault())
  .toOffsetDateTime()
```

# まとめ
JavaでもUUID v7を扱う準備ができているよ、という紹介でした。

UUIDクラスはJava 5.0で2004年に追加されたものです。それが20年たってもそのまま新しい仕様にも対応できているという点に驚きました。JavaのUUIDクラスのAPI設計も、UUIDの仕様も、よく考えられた仕組みであることに感動しました。
このような発展性、後方互換性を持った仕様やAPIを作れるというのは素晴らしいですね。

# おまけ
[UUIDの新フォーマットに関するRFCドラフト](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-dbms-and-database-considera) にはDBのキーとして使う場合は文字列ではなく128bitの数値として保持せよ、とあります。

> For many applications, such as databases, storing UUIDs as text is unnecessarily verbose, requiring 288 bits to represent 128 bit UUID values. Thus, where feasible, UUIDs SHOULD be stored within database applications as the underlying 128 bit binary value.

postgresにはUUID型がありますが、MySQLではBINARY型で`BINARY(16)`を用いるとよさそうです。
こういうヒントがあるのもRFCのよいところですね。