Slide 1

Slide 1 text

ソートできる UUID v7 をJavaで使うときの話

Slide 2

Slide 2 text

@yoshiori 2

Slide 3

Slide 3 text

3 写真もSNSもOK

Slide 4

Slide 4 text

今回のお話 4

Slide 5

Slide 5 text

今回のお話 5

Slide 6

Slide 6 text

JJUG CCC 2024 Spring CFP close 2024 Mar. 6

Slide 7

Slide 7 text

RFC 9562が正式に標準化 2024 May. 7 https://www.rfc-editor.org/rfc/rfc9562.html 雑にいうと既存のUUIDの仕様(RFC4122)にv6,7,8 の仕様を追加 したもの

Slide 8

Slide 8 text

今回(2024 Jun.)のお話 8 採択された

Slide 9

Slide 9 text

UUIDの説明 vol.1 しばらくJava関係ない話が続きますが この辺の知識、伏線回収で使います!!! 9

Slide 10

Slide 10 text

RFC4122が公開される UUID v1,2,3,4,5 の仕様 2005 10 https://www.rfc-editor.org/rfc/rfc4122

Slide 11

Slide 11 text

UUIDって何 Universally Unique Identifier (UUID) ◦ f81d4fae-7dec-11d0-a765-00a0c91e6bf6 ▫ こういうやつ ▫ 固定サイズ (128 ビット) ◦ 中央ID管理(ID生成機構)が必要無い ▫ DBのauto incrementとか ▫ 古くはTwitterのsnowflakeとか ◦ 生成方法は複数定義されている 11

Slide 12

Slide 12 text

RFC4122 version 3: 何かしらを元にMD5ハッシュ値で生成 12 version 2: DCEセキュリティUUID 使わない version 1: タイムスタンプ+クロックシーケンス+ノードID version 4: 完全乱数 version 5: 何かしらを元にSHA-1ハッシュ値で生成

Slide 13

Slide 13 text

13 UnsplashのMd Mahdiが撮影した写真 !?

Slide 14

Slide 14 text

RFC4122 version 3: 何かしらを元にMD5ハッシュ値で生成 14 version 2: DCEセキュリティUUID 使わない version 1: タイムスタンプ+クロックシーケンス+ノードID version 4: 完全乱数 version 5: 何かしらを元にSHA-1ハッシュ値で生成 あるじゃん

Slide 15

Slide 15 text

UUID version 1: タイムスタンプ+クロックシーケンス+ノードID UUID version 1 生成方法 ◦ 現在時刻(100ナノ秒単位) ▫ 1582年10月15日(グレゴリオ暦開始日)からの経過時間 ▫ それを3つに分割するtime_hi,time_mid,time_low ▫ そしてそれをtime_low,time_mid,time_hi(&version)の順で結合 ◦ クロックシーケンス ◦ ノードID(通常はMACアドレスを使用) 15

Slide 16

Slide 16 text

UUID version 1: タイムスタンプ+クロックシーケンス+ノードID UUID version 1 生成方法 ◦ 現在時刻(100ナノ秒単位) ▫ 1582年10月15日(グレゴリオ暦開始日)からの経過時間 ▫ それを3つに分割するtime_hi,time_mid,time_low ▫ そしてそれをtime_low,time_mid,time_hi(&version)の順で結合 ◦ クロックシーケンス ◦ ノードID(通常はMACアドレスを使用) 16 つまり雑にいうと上下逆にして る

Slide 17

Slide 17 text

UUID version 1: タイムスタンプ+クロックシーケンス+ノードID time_low,time_mid,time_hi(&version)の順で結合 ◦ コードで書くと... ▫ ((timestamp & 0x0FFFL) << 48) | ((timestamp & 0x1FFFL) << 16) | ((timestamp >> 32) & 0xFFFFL) ◦ time_hi を先頭に持ってくると先頭数バイトはしば らく同じものになってしまう ◦ 分散DBとかのキーにするとHotSpotを作りやすく なってしまう ◦ なのでtime_lowを先頭にしている 17

Slide 18

Slide 18 text

UUID version 1: タイムスタンプ+クロックシーケンス+ノードID ノードID(通常はMACアドレスを使用) ◦ ここが結構懸念点になった ◦ MACアドレスの露見はいろいろな情報を明らかにし ちゃう ◦ さらに仮想マシンとコンテナーの出現により、MAC アドレスの一意性は保証されなくなった 18

Slide 19

Slide 19 text

UUID v1,2,3,4,5 (RFC4122)をとりまく個人的な感想 ◦ 表現形式と生成方式を分けて考える ▫ 表示形式 ■ 固定サイズ (128 ビット)を16進数でハイフン区切り ▫ 生成方式 ■ v1,2,3,4,5 が定義される ■ すべてなるべく分散することを目的に生成 ■ v1 ● 時間データを持っているが辞書順ではソートできないパースして頑張 ればソートは出来る ● しかしノードIDにセキュリティリスクがある ◦ なるべくランダムでしかもノードIDとかを持たないほうが良い 19

Slide 20

Slide 20 text

RFC4122 version 3: 何かしらを元にMD5ハッシュ値で生成 20 version 2: DCEセキュリティUUID 使わない version 1: タイムスタンプ+クロックシーケンス+ノードID version 4: 完全乱数 version 5: 何かしらを元にSHA-1ハッシュ値で生成 なのでコレが一般化

Slide 21

Slide 21 text

JavaのUUID RFC4122を元に作られてる 21

Slide 22

Slide 22 text

Java のUUID ◦ UUID(long mostSigBits, long leastSigBits) ▫ コンストラクタ ▫ 上位ビットと下位ビットの2つのlong ▫ UUID v1 とかは自分で時間から計算して作る ◦ static UUID nameUUIDFromBytes(byte[] name) ▫ v3 が作れるよ ◦ static UUID randomUUID() ▫ v4 が作れるよ ◦ static UUID fromString(String name) ▫ 文字列表現(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx )から生成 22

Slide 23

Slide 23 text

UUIDの説明 vol.2 ここから伏線回収のための説明 23

Slide 24

Slide 24 text

“ 生成時間順にソート出来ないと使いにくいなぁ 24

Slide 25

Slide 25 text

“ 生成時間順にソート出来ないと使いにくいなぁ 25 HotSpotは確かに防げるが逆にデータベースキーインデックスの局所性が低い、つまりランダムな場 所に入るので一般的なB-treeとかその変種ではパフォーマンスの影響の懸念がある等々 インデックスに関してはそこまで影響ないという意見もあったりする。あとは一般的に新しく作ら れたオブジェクトのほうがアクセスされる頻度が多いみたいなのもあるので時系列順に並んでいる 方がよいという意見もある 僕はこの領域にそこまで詳しくない。けど純粋に order by id desc limit 100 して最新のデータ をみたいとかあるのでやはり生成時間順に並んでないと不便だなぁとか (created_atカラム作れとかそういう話でもない)

Slide 26

Slide 26 text

新しいフォーマットが提案される 最初はv6 2020 26 https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-00.txt https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-00.html

Slide 27

Slide 27 text

UUID v6 v1 からの変更点 ◦ ノードIDはセキュリティ的にも無いほうが良いね ◦ クロックシーケンスも無いほうが良いね ▫ 乱数値に(一応元の値を使うことも可能) ◦ タイムスタンプを入れ替えるのをやめる ▫ 引き続きグレゴリオ暦ベース ▫ time_low,time_mid,time_high_and_version -> time_high,time_mid,time_low 27

Slide 28

Slide 28 text

“ グレゴリオ暦とかあんま使わなくね? 28

Slide 29

Slide 29 text

UUID v7,v8 29 v7 一般的によく使われているUNIXタ イムスタンプを使うようになっ た。精度はミリ秒。 あとはランダム値。 (細かくいうとカウンタとかもあ るがそれも生成方法の1つにラン ダムが許されているので本当に必 要なとき以外気にしなくて良い) v8 実験的またはベンダー固有の使用 のためのフォーマット。 あんまり気にしなくて良い。

Slide 30

Slide 30 text

RFC9562が公開される UUID v6,7,8 の追加 2024 May. 30 https://datatracker.ietf.org/doc/rfc9562/ https://www.rfc-editor.org/rfc/rfc9562

Slide 31

Slide 31 text

RFC9562 version 8: カスタム定義用 31 version 7: ミリ秒精度のUNIXタイムスタンプベース version 6: v1ベース。タイムスタンプの上位ビットを前にした

Slide 32

Slide 32 text

RFC9562 version 8: カスタム定義用 32 version 7: ミリ秒精度のUNIXタイムスタンプベース version 6: v1ベース。タイムスタンプの上位ビットを前にした 新規で使うならコレ使っておけば良い

Slide 33

Slide 33 text

JavaのUUID RFC9562を使うには(伏線回収) 33

Slide 34

Slide 34 text

Javaで使うRFC9562 34 表現形式 これはそもそも仕様自体も 何も変更無いのでそのまま使え る。 生成方式 まだ標準APIには入ってないので 外部ライブラリを使う必要があ る。 - cowtowncoder/java-uuid-generator - f4b6a3/uuid-creator など

Slide 35

Slide 35 text

Javaで使うRFC9562 35 表現形式 これはそもそも仕様自体も 何も変更無いのでそのまま使え る。 生成方式 まだ標準APIには入ってないので 外部ライブラリを使う必要があ る。 - cowtowncoder/java-uuid-generator - f4b6a3/uuid-creator AUTION CAUTION CAUTION CAUTION CAUTIO CAUTION CAUTION CAUT AUTION CAUTION CAUTION CAUTION CAUTI ION CAUTION CAUTION CAUTION CAUTION TIO N C A U TIO N C A U TIO N C A U TIO N

Slide 36

Slide 36 text

Java のUUID ◦ UUID(long mostSigBits, long leastSigBits) ▫ コンストラクタ ▫ 上位ビットと下位ビットの2つのlong ▫ UUID v1 とかは自分で時間から計算して作る ◦ static UUID nameUUIDFromBytes(byte[] name) ▫ v3 が作れるよ ◦ static UUID randomUUID() ▫ v4 が作れるよ ◦ static UUID fromString(String name) ▫ 文字列から生成 36

Slide 37

Slide 37 text

UUID(long mostSigBits, long leastSigBits) 内部的にもこれで持っている ◦ 128 bitのデータを持つのにlong(64 bit)x2は理に かなっている ◦ なので内部的にはいろいろなところで使われている ◦ 例えばUUIDクラスは順番を持っている ▫ implements Comparable ▫ public int compareTo(UUID val) 内でも使用 ■ ここが問題になる 37

Slide 38

Slide 38 text

public int compareTo(UUID val) 順番比較 ◦ 上位のlongを比較して上位が完全に一緒だったとき に下位のlongを比較している 38 @Override public int compareTo(UUID val) { // The ordering is intentionally set up so that the UUIDs // can simply be numerically compared as two numbers int mostSigBits = Long.compare(this.mostSigBits, val.mostSigBits); return mostSigBits != 0 ? mostSigBits : Long.compare(this.leastSigBits, val.leastSigBits); } https://github.com/openjdk/jdk/blob/d60331a21c30271340f7d6d58f3122f0e6431a04/src/java.base/share/classes/java/util/UUID.java#L557-L562

Slide 39

Slide 39 text

おわかりいただけただろう か 39 UnsplashのEdo Nugrohoが撮影した写真

Slide 40

Slide 40 text

UUID(long mostSigBits, long leastSigBits) 内部的にもこれで持っている ◦ 128 bitのデータを持つのにlong(64 bit)x2は理に かなっている ◦ なので内部的にはいろいろなところで使われている ◦ 順番を持っている implements Comparable ◦ public int compareTo(UUID val) 内でも使用 ▫ ここが問題になる 40

Slide 41

Slide 41 text

128 bitのデータを持つのにlong(64 bit)x2は理にかなっている 符号の問題 ◦ UUID は符号なし、long(64 bit)は符号あり ◦ つまりある程度大きい値を変換すると内部的には負 のlongを保持することになる 41 jshell> var max = UUID.fromString("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF") max ==> ffffffff-ffff-ffff-ffff-ffffffffffff jshell> max.getMostSignificantBits() $2 ==> -1 jshell> max.getLeastSignificantBits() $3 ==> -1 ちなみにUUID MAXもRFC9562 で追加された定数

Slide 42

Slide 42 text

public int compareTo(UUID val) 順番比較 ◦ 上位のlongを比較して上位が完全に一緒だったとき に下位のlongを比較している 42 @Override public int compareTo(UUID val) { // The ordering is intentionally set up so that the UUIDs // can simply be numerically compared as two numbers int mostSigBits = Long.compare(this.mostSigBits, val.mostSigBits); return mostSigBits != 0 ? mostSigBits : Long.compare(this.leastSigBits, val.leastSigBits); } https://github.com/openjdk/jdk/blob/d60331a21c30271340f7d6d58f3122f0e6431a04/src/java.base/share/classes/java/util/UUID.java#L557-L562

Slide 43

Slide 43 text

符号の問題 compareTo で問題になる(Sort等々) ◦ 辞書的順番とは異なる順序付けになる 43 jshell> var max = UUID.fromString("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF") max ==> ffffffff-ffff-ffff-ffff-ffffffffffff jshell> var nilUUID = UUID.fromString("00000000-0000-0000-0000-000000000000") nilUUID ==> 00000000-0000-0000-0000-000000000000 jshell> Collections.min(List.of(max, nilUUID)) $3 ==> ffffffff-ffff-ffff-ffff-ffffffffffff

Slide 44

Slide 44 text

生成時間順に辞書的にソートするために 作られたRFC9562なのに うまくソート出来ない! 44 UnsplashのGiammarco Boscaroが撮影した写真

Slide 45

Slide 45 text

JavaのUUIDのソート じゃぁ、どうすればよいか ◦ ソートなどは外部からComparator渡してあげる ▫ 雑にtoStringして比較するとか ▫ 各種ライブラリにはComparatorも付属してるのでそれ使う ◦ 知識として知っていないとハマる ▫ 実際僕はUUIDベースのpaginationのテストを書いているときに ハマって調べた結果が今回の発表に繋がってる 45

Slide 46

Slide 46 text

JavaのUUID 背景補足 ◦ JavaのUUIDはまだRFC4122だった時代に作られた ▫ UUIDは辞書的順番に生成されない ▫ むしろ分散しやすいように意識してた ◦ なので compareTo の実装はしょうが無い ▫ Long.compareUnsigned 使えばよかったのでは? ■ Long.compareUnsigned はJava8から(UUIDクラスはJava5) 46

Slide 47

Slide 47 text

まとめ JavaでRFC9562を使う 47

Slide 48

Slide 48 text

JavaでRFC9562を使う 48 UUID v7 の生成 まだ標準にはなっていないので外 部ライブラリ使う - cowtowncoder/java-uuid-generator - f4b6a3/uuid-creator など ソート時はComparator指定する 各種ライブラリに付属しているの でそれを使うと良い。 何も指定しないと辞書順にはソー トできない。

Slide 49

Slide 49 text

おまけ 時間が余ったら喋る 49

Slide 50

Slide 50 text

Max UUID ffffffff-ffff-ffff-ffff-ffffffffffff ◦ もともとNil UUIDはRFC4122で定義されてた ▫ 全部0 ◦ 時間順にソートできるよになるから上も欲しいよね ◦ 名前の候補が色々あった ▫ OmniUUIDというのもあってこれが面白かった ▫ ラテン語で「全て」という意味らしい ▫ nilがもともとラテン語で「無」を表すのでその反対として候補 に出てた ▫ まぁ、わかりやすいMAXで決まった 50

Slide 51

Slide 51 text

UUID のバージョン判定方法 2個目のハイフンの後ろがバージョン番号 ◦ 9fa7d6d6-725a-4f92-9c9c-f53b8be7d5bb ▫ この場合は v4 だとわかる ◦ なので今後UUID.compareToもそれ見て下位互換保つ んじゃないかなぁ ▫ Nil UUID,Max UUIDの処理して ▫ バージョン見て6,7,8だったらLong.compareUnsigned使う ▫ それ以外は今までどおりLong.compare使う 51

Slide 52

Slide 52 text

f4b6a3/uuid-creator 作者から連絡きた この辺の話を雑にBlogに書いたら翻訳して読んでくれ てたっぽい(のでREADMEにリンクあります) 52

Slide 53

Slide 53 text

おしまい! 53