Redigoを使う(3) トランザクションを行う


はじめに

RedisのGo言語向けクライアントライブラリRedigoの使い方を見ます。
本記事ではトランザクションの行い方を見ていきます。

環境

基本の流れ

Redisにおけるトランザクションについては、Redis Documentation (Japanese Translation) をまずは一読しておきましょう。

以下は、Redigoでトランザクションを行う典型的なコードです。SETとSADDという2つのコマンドを実行します。Conn.Send関数を使うのが定石です。

main.go
package main

import (
    "fmt"

    "github.com/gomodule/redigo/redis"
)

func main() {
    // 接続
    conn, err := redis.Dial("tcp", "localhost:6379")
    if err != nil {
        panic(err)
    }
    defer conn.Close()

    // トランザクションの開始
    err = conn.Send("MULTI")
    if err != nil {
        panic(err)
    }

    // 文字列型の追加
    err = conn.Send("SET", "money", 1000)
    if err != nil {
        panic(err)
    }

    // セット型の追加
    err = conn.Send("SADD", "fruits", "apple")
    if err != nil {
        panic(err)
    }

    // コマンドの実行
    r, err := redis.Values(conn.Do("EXEC"))
    if err != nil {
        panic(err)
    }
    fmt.Println(r) // [OK 1]
}

Conn.Sendは、クライアントの送信バッファにコマンドをため込むための関数です。この関数を呼んだ時点では、サーバに対する通信は発生しません。ここでは、MULTI、SET、SADDの3つがため込まれます。そしてConn.Do("EXEC")が呼ばれたタイミングで、送信バッファの内容がまとめてサーバに送信されます。戻り値で実行結果が得られます。戻り値をredis.Values関数に通すことで、interface{}型の配列に結果を変換できます。

このように、Send("MULTI")Send(任意のコマンド)×(複数回) ⇒ Do("EXEC") というのが、トランザクションの基本の流れです。もちろん、Sendを使わずにDoだけで同じトランザクションを実現することも(おそらく)可能ですが、定石どおりが無難と思います。

なお、Send関数はサーバとの通信を伴わないため、戻り値がエラーとなる可能性は小さいです。しかし、メモリ不足などの事態を想定し、エラーチェックは行っておいた方がよいと思います。

CAS(Check-And-Set)操作

さて、実際の利用場面では、あるキーで読み出した値から何か処理を行い、その結果でキーの値を更新する、というケースがあると思います。このとき、複数クライアントからの並行処理による競合の可能性を考慮しなければなりません。

Redisでは、WATCHというコマンドで競合を検出できます。以下のコードは、moneyというキーで数値を読み出し、200を加えた値で更新します。

main.go
package main

import (
    "fmt"
    "time"

    "github.com/gomodule/redigo/redis"
)

func main() {
    // 接続
    conn, err := redis.Dial("tcp", "localhost:6379")
    if err != nil {
        panic(err)
    }
    defer conn.Close()

    // キーの監視を開始
    w, err := redis.String(conn.Do("WATCH", "money"))
    if err != nil {
        panic(err)
    }
    fmt.Println(w) // OK

    // 値の取得
    m, err := redis.Int(conn.Do("GET", "money"))
    if err != nil {
        panic(err) // 値が存在しなければ終了
    }
    fmt.Println(m) // 更新前の値を出力

    // トランザクションの開始
    err = conn.Send("MULTI")
    if err != nil {
        panic(err)
    }

    // 値の更新
    err = conn.Send("SET", "money", m+200)
    if err != nil {
        panic(err)
    }

    // コマンドの実行
    r, err := redis.Values(conn.Do("EXEC"))
    if err != nil {
        panic(err) // トランザクション失敗時はpanic
    }
    fmt.Println(r) // トランザクション成功時は[OK]を出力
}

始めにWATCHで、キーmoneyの監視を開始しています。
次にGETでmoneyの値を取得します。そしてMULTIリクエストでSETを行っています。

他のクライアントからの値の書き換えが発生しなければ、トランザクションは成功し、moneyに200足された値が書き込まれてコードは[OK]を出力して終了します。

反対に、WATCHを行ってからEXECを行うまでの間に、他のクライアントから値が別の値に書き換えられたとします。このときトランザクションは失敗し、EXECの行はエラーとなります。(実用上のコードでは、エラーの検出時には、リトライするなり適切にエラーハンドリングを行う必要があります。)

ちなみに、WATCHの後で、何らかのエラーにより、EXECの前に処理を中断する場合は、通常はUNWATCHコマンドをサーバに送って監視の終了を教える必要があります。RedigoではConn.Closeの実行時に自動でUNWATCHを送る実装となっているため、利用者側が明示的にUNWATCHする必要はなさそうです。

ところで本記事では、説明のために足し算の処理を示しましたが、RedisではINCRBYというコマンドで一発で足し算が行えます。同じことをしたいときはこちらのコマンドを使いましょう。

おわりに

Redigoでトランザクションを行う手順を見ました。

今後、できたらRedigoの以下の機能についても書きたいです。

  • コネクションプール
  • 各種ユーティリティ関数
  • パブリッシュ/サブスクライブ

参考