この記事は Go3 Advent Calendar 2017 の6日目の記事です。

はじめに

DBに保存するデータのIDやセッションIDなどの一意なIDを、分散したWebアプリ上で発行することで、発行処理をスケールさせたいといったケースがあります。
そういったケースでは、UUIDやSnowflakeなどの使用例が良く紹介されています。
この記事では、Go製のライブラリで、Goアプリから簡単に使用できるID Generatorである「xid」について紹介します。
このxidは自分が仕事で開発しているシステムでも採用しています。

GitHubリポジトリ: https://github.com/rs/xid

xidについて

詳しくはGitHubのREADMEの書かれていますが、その中から一部抜粋して紹介します。

binaryのformat

全体で12bytesで、先頭から以下のように構成されています。

  • 4bytes: Unix timestamp (秒単位)
  • 3bytes: ホストの識別子
  • 2bytes: プロセスID
  • 3bytes: ランダムな値からスタートしたカウンタの値

生成される文字列

20文字のlower caseの英数字。([0-9a-v]{20})

例: b8hpcg8hv3amvi9dol0g

特徴

  • サイズがUUIDより小さく、Snowflakeより大きい
  • 設定が不要
  • 生成されるバイナリや文字列がsortable
  • ホスト/プロセスごとに秒間16,777,216 (24 bits)のユニークなIDを発行可能
  • Lockを使用しない

など

使い方

コード:

package main

import (
    "fmt"

    "github.com/rs/xid"
)

func main() {
    // idを生成
    guid := xid.New()
    fmt.Println(guid.String())

    // binaryの各partの情報
    machine := guid.Machine()
    pid := guid.Pid()
    time := guid.Time()
    counter := guid.Counter()
    fmt.Printf("machine: %v, pid: %v, time: %v, counter: %v\n", machine, pid, time, counter)
}

実行結果:

b8hpcg8hv3amvi9dol0g
machine: [17 248 213], pid: 28617, time: 2017-12-03 15:14:25 +0900 JST, counter: 2999617

xidの実装を見てみる

まず、ID型が12bytesの配列として定義されています。

// ID represents a unique request id
type ID [rawLen]byte

const rawLen = 12 // binary raw len

新しいIDを生成する部分は以下の New() のようになっており、先頭から各part (unix timestamp, machineID, pid, counter) の値を埋めていることがわかります。

// objectIDCounter is atomically incremented when generating a new ObjectId
// using NewObjectId() function. It's used as a counter part of an id.
// This id is initialized with a random value.
var objectIDCounter = randInt()

// machineId stores machine id generated once and used in subsequent calls
// to NewObjectId function.
var machineID = readMachineID()

// pid stores the current process id
var pid = os.Getpid()

// New generates a globaly unique ID
func New() ID {
    var id ID
    // Timestamp, 4 bytes, big endian
    binary.BigEndian.PutUint32(id[:], uint32(time.Now().Unix()))
    // Machine, first 3 bytes of md5(hostname)
    id[4] = machineID[0]
    id[5] = machineID[1]
    id[6] = machineID[2]
    // Pid, 2 bytes, specs don't specify endianness, but we use big endian.
    id[7] = byte(pid >> 8)
    id[8] = byte(pid)
    // Increment, 3 bytes, big endian
    i := atomic.AddUint32(&objectIDCounter, 1)
    id[9] = byte(i >> 16)
    id[10] = byte(i >> 8)
    id[11] = byte(i)
    return id
}

以下の String() が、文字列表現を取得する部分です。
custom versionのbase32 encodingを使用して、12bytesの配列 (ID) から20文字の文字列表現を生成しています。

const (
    encodedLen = 20 // string encoded len

    // encoding stores a custom version of the base32 encoding with lower case letters.
    encoding = "0123456789abcdefghijklmnopqrstuv"
)

// String returns a base32 hex lowercased with no padding representation of the id (char set is 0-9, a-v).
func (id ID) String() string {
    text := make([]byte, encodedLen)
    encode(text, id[:])
    return string(text)
}

// encode by unrolling the stdlib base32 algorithm + removing all safe checks
func encode(dst, id []byte) {
    dst[0] = encoding[id[0]>>3]
    dst[1] = encoding[(id[1]>>6)&0x1F|(id[0]<<2)&0x1F]
    dst[2] = encoding[(id[1]>>1)&0x1F]
    // 以下省略
    // ...
}

IDから各part (unix timestamp, machineID, pid, counter) を取得する部分の実装です。

// Time returns the timestamp part of the id.
// It's a runtime error to call this method with an invalid id.
func (id ID) Time() time.Time {
    // First 4 bytes of ObjectId is 32-bit big-endian seconds from epoch.
    secs := int64(binary.BigEndian.Uint32(id[0:4]))
    return time.Unix(secs, 0)
}

// Machine returns the 3-byte machine id part of the id.
// It's a runtime error to call this method with an invalid id.
func (id ID) Machine() []byte {
    return id[4:7]
}

// Pid returns the process id part of the id.
// It's a runtime error to call this method with an invalid id.
func (id ID) Pid() uint16 {
    return binary.BigEndian.Uint16(id[7:9])
}

// Counter returns the incrementing value part of the id.
// It's a runtime error to call this method with an invalid id.
func (id ID) Counter() int32 {
    b := id[9:12]
    // Counter is stored as big-endian 3-byte value
    return int32(uint32(b[0])<<16 | uint32(b[1])<<8 | uint32(b[2]))
}

文字列表現からIDに戻すことも可能で、以下の実装になっています。

// FromString reads an ID from its string representation
func FromString(id string) (ID, error) {
    i := &ID{}
    err := i.UnmarshalText([]byte(id))
    return *i, err
}

まとめ

Go製のUnique ID Generator「xid」について、特徴、使い方、実装などを紹介しました。
他のID生成方法との比較についてはあまり取り上げなかったので、以下の記事などを参照していただくと良いかと思います。

ユースケースに合う場合はぜひxidを使用してみてください。

1494740783