NaaN日記

やったこと、覚えたことを発信する場

RedisのexpireにNXをつけた方が良かったとき

特定の時間にリセットするようなキャッシュをHashを使って管理したい、というときはNXをつけた方が良かったです。

次の使い方を想定する。

  • 1時間に一度、0分のタイミングでキャッシュをリセットする
  • HGETでfieldを探して、すでに存在していればvalueを使う
  • 存在しなければ計算してHSETでfield valueを追加し、次の0分までの秒数を有効期限としてexpireで設定する

12:00ちょうどのとき、残り3600秒間利用したいので、expireで3600を指定した。

redis> HSET myhash field1 value1
(integer) 1
redis> EXPIRE myhash 3600
(integer) 1

次に、12:30にfield2へのアクセスがあったので、計算し、追加した。

HSET myhash field2 value2
(integer) 1
EXPIRE myhash 1800
(integer) 1

このやり方だと、EXPIREに設定するときに計算した秒の値がズレていると毎時0分を超えてキャッシュしてしまう。
さきほどfield2を追加したとき、厳密には12:30:00.100だったので、13:00:00.100まで延長された。

このあと、13:00:00.000〜13:00:00.099の0分を超えたキャッシュが存在している間に同じように新しいfield valueをセットすると、0分のときに次の0分までの時間は3600秒なので、この1時間の間に作成したキャッシュをexpireせずに、1時間延長できてしまう

NXをつける

EXPIREにはOptionsとして、NXやLTが設定できるので、これを設定することで、キャッシュを確実に期限切れ状態にできる。

EXPIRE | Docs

  • NX: 有効期限の設定がない場合のみ、有効期限を設定できる
  • XX: 有効期限が設定済みの場合のみ、有効期限を設定できる
  • GT: 有効期限が設定済みで現在より大きい場合のみ、有効期限を設定できる
  • LT: 有効期限が設定済みで現在より短い場合のみ、有効期限を設定できる

今回の使い方だと、keyに対しての有効期限は固定なので、expireを設定するのは最初の一回だけで良い。

HSET myhash field3 value3
(integer) 1
EXPIRE myhash 600 NX
(integer) 0 -- 操作がスキップされた。

こうすることで、初回以降は有効期限が上書きされなくなるので、12:00:00.300に最初のキャッシュの有効期限を3600で作ったとしても、13:00.00.300にはEXPIREされる。
(13:00.00.200にキャッシュを更新しようとしても、操作がスキップされる)

このように、ひとつのkeyに要素を何度も追加するようなときには、毎回expire設定を上書きしない方が良い。


しかし、13:00:00.000ぴったりにEXPIREしたい、という場合には、この方法は使えない。
PEXPIREでミリ秒単位の有効期限を設定すればより0秒に近づくが、ミリ秒だとしてもズレる。
したがって、ミリ秒単位の遅れも許されずにぴったりにEXPIREしたい場合には、他の方法を検討する必要がある。

他の方法

  • EXPIREAT | Docsというのもあって、これだと有効期限を厳密なunix timestampで設定できるので、ぴったりのunix timestampを計算すれば、その時間ぴったりにキャッシュを切らすことができる
    • 今回の例のような、ある時間までを有効期限に決めたい場合、unix timestampを計算してセットするのが素直な実装だろう
  • 切り替えタイミングの時間をkeyに含めてしまう
    • 時間経過でkeyが変わるので、前の時間のキャッシュは取得できない
  • key単位ではなく、fieldごとに有効期限を決めたいなら、HEXPIRE | Docsもある