日々のあれこれφ(..)

もっぱら壁打ち

redis入門+トランザクション周りの覚書

サーバサイドエンジニア3年目にして先日初めてredisとやりとりするような処理を書きました。

せっかくなので調べたことを残しておこうと思います。

基本的な操作(cli

接続

$ redis-cli 
127.0.0.1:6379> 

-h {HOST}-n {db} など

redisのdbについて

  • 選択可能な名前空間の形式。0~15などのidを指定する形で使う
  • 未指定の場合は0になる
  • redis clusterでは、0のみがサポートされる
127.0.0.1:6379> SELECT 1
OK
127.0.0.1:6379[1]>

127.0.0.1:6379[1]> SELECT 15
OK
127.0.0.1:6379[15]> SELECT 16
(error) ERR DB index is out of range
127.0.0.1:6379[1]> SELECT 0
OK
127.0.0.1:6379>
  • database_idの数は databasesで確認でき、この設定を変更すれば増減できる
127.0.0.1:6379[15]> CONFIG GET databases
1) "databases"
2) "16"

set/get

127.0.0.1:6379> SET count 1
OK
127.0.0.1:6379> GET count
"1"

key一覧を見る

  • KEYS {pattern} でpatternに一致する全てのキーを返す。
127.0.0.1:6379> KEYS *
1) "key1"

大規模なデータベースで実行するとパフォーマンスに影響が出る可能性があるため注意。

キーに格納されている値を増やす

127.0.0.1:6379> INCR count
(integer) 2
127.0.0.1:6379> GET count
"2"

キーに有効期限を設ける

  • EXPIRE {key} {秒}
    • 有効期限を設定
  • TTL {key}
    • 残り時間を確認
# keyを設定
127.0.0.1:6379> SET count 3
OK

# timeoutを20sに設定
127.0.0.1:6379> EXPIRE count 20
(integer) 1

# 参照できる
127.0.0.1:6379> GET count
"3"

# 残り時間を確認
127.0.0.1:6379> TTL count
(integer) 13
127.0.0.1:6379> TTL count
(integer) 12
...
127.0.0.1:6379> TTL count
(integer) 1
127.0.0.1:6379> TTL count
(integer) 0
127.0.0.1:6379> TTL count
(integer) -2

# keyが存在しなくなっている
127.0.0.1:6379> GET count
(nil)

TTLの返り値に関して、-1タイムアウトが設定されていない値、 -2 はkeyが存在しないもの。

トランザクションを貼る

  • MULTI
    • トランザクションを貼るときのコマンド。multiの後に入力したコマンドはキューに詰められ、EXECコマンドが入力されたタイミングで一括で反映させる事ができる
  • WATCH
    • 楽観ロック。キーをwatchすると、watchされたキーに変更が加えらた場合トランザクション全体を中止することができる(EXECを実行したタイミングでトランザクション処理の各コマンドが実行されなくなり、代わりにnullが返る)

トランザクション成功例

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> 
127.0.0.1:6379(TX)> SET count 1
QUEUED
127.0.0.1:6379(TX)> INCR count
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) (integer) 2
127.0.0.1:6379> GET count
"2"

トランザクション失敗例

127.0.0.1:6379> SET count 1
OK
127.0.0.1:6379> WATCH count
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR COUNT
QUEUED
# このタイミングで別ターミナルで INCR countを実行し、count keyに変更を加える
127.0.0.1:6379(TX)> INCR COUNT
QUEUED
127.0.0.1:6379(TX)> INCR COUNT
QUEUED
127.0.0.1:6379(TX)> EXEC
(nil)
127.0.0.1:6379> GET count
"2"  # TX内のincr結果が反映されていない

基本的な操作(redis-py)

import redis

def main():
    client = redis.Redis(
        host="localhost",
        port=6379,
        db=1,
    )

    client.set('count', 1)
    client.incr('count', 3)
    print(client.get('count')) # b'4'


if __name__ == "__main__":
    main()

pipelineメソッド

単一のリクエストでサーバーに複数のコマンドを実行させる事ができる。

import redis

def main():
    client = redis.Redis(
        host="localhost",
        port=6379,
        db=1,
    )

    pipe = client.pipeline()
    pipe.set('count', 1)
    pipe.incr('count', 3)
    pipe.get('count')
    print(pipe.execute())  # [True, 4, b'4']


if __name__ == "__main__":
    main()

pipeline()はtransactionオプションがあり、デフォルトでonになっているが明示的にoffにして使うこともできる。

transactionメソッド

pipelineとは別に、transactionメソッドというものもあり、それを使うこともできる。 transactionメソッドを使うと、watchや監視対象keyに変更が加えられたときのWatchErrorハンドリングを書かなくても済むが、少しわかりにくかった。

import redis

def main():
    redis_client = redis.Redis(
        host="localhost",
        port=6379,
        db=1,
    )

    def incr_req_cnt(p):
        p.multi()
        p.incr(key, amount=2)
        p.incr(key, amount=2)

    key = "count"
    tx_result = redis_client.transaction(incr_req_cnt, *key, value_from_callable=True)
    print(tx_result)


if __name__ == "__main__":
    main()

transaction関数の中身は次のようになっている。

https://github.com/redis/redis-py/blob/883fca7199a7ab7adc583b9ee324a9e44d0909eb/redis/client.py#L1074

    def transaction(self, func, *watches, **kwargs):
        """
        Convenience method for executing the callable `func` as a transaction
        while watching all keys specified in `watches`. The 'func' callable
        should expect a single argument which is a Pipeline object.
        """
        shard_hint = kwargs.pop("shard_hint", None)
        value_from_callable = kwargs.pop("value_from_callable", False)
        watch_delay = kwargs.pop("watch_delay", None)
        with self.pipeline(True, shard_hint) as pipe:
            while True:
                try:
                    if watches:
                        pipe.watch(*watches)
                    func_value = func(pipe)
                    exec_value = pipe.execute()
                    return func_value if value_from_callable else exec_value
                except WatchError:
                    if watch_delay is not None and watch_delay > 0:
                        time.sleep(watch_delay)
                    continue

WatchErrorが発生した場合はリトライされる(watch_delayを指定することで再実行までの時間を指定できる。リトライ回数は指定できない模様)

value_from_callable 変数を指定することで、transactionの返り値を設定できる。 例えば上記の incr_req_cnt()return 'success'を入れたとして、 value_from_callable=Trueにするとprintの中身は'success'になり、Falseにすると、 pipe.execute() の返り値(上記コードではcountキーが0だった場合 [2, 4])が出力される。

参考