YASD-TECH
YASD TECH
# Rails

SidekiqがRedisをどう使っているか(List・Sorted Set・Hash)

投稿日:2026/8/9

更新日:2026/8/9

ttitleImage

SidekiqがRedisをどう使っているか(List・Sorted Set・Hash)

perform_async を呼んだあと、ジョブは Redis の中でどんな形で持たれているのか。
Sidekiq は「全部まとめて1個のキュー」ではなく、用途ごとに Redis のデータ構造を使い分けている。
その対応関係を整理する。

結論

  • ジョブ本体はただの JSON 文字列。Ruby のオブジェクトは持てない。
  • 即実行キューは List。投入が LPUSH(左端)、取り出しが BRPOP(右端)で FIFO になる。
  • 予約・リトライは Sorted Set。スコアに「実行すべき時刻の UNIX タイムスタンプ」を入れ、時刻が来たら List へ移す。
  • プロセスの状態は Hash、プロセス一覧は Set。ダッシュボードの表示はこれを読んでいる。

全体像

flowchart LR
    App["Rails アプリ"]

    subgraph Redis["Redis"]
        Q["queue:default<br/>(List)"]
        S["schedule<br/>(Sorted Set)"]
        R["retry<br/>(Sorted Set)"]
        D["dead<br/>(Sorted Set)"]
        P["processes<br/>(Set) / プロセス情報(Hash)"]
    end

    Worker["Sidekiq プロセス"]

    App -->|"perform_async → LPUSH"| Q
    App -->|"perform_in / perform_at → ZADD"| S
    S -->|"時刻到来したら Poller が移動"| Q
    Q -->|BRPOP| Worker
    Worker -->|"失敗 → ZADD"| R
    R -->|"再実行時刻が来たら移動"| Q
    R -->|"リトライ上限"| D
    Worker -->|"稼働状況を書き込み"| P

1. ジョブ本体はただの JSON 文字列

WelcomeEmailJob.perform_async(123, "hoge") を呼ぶと、Redis に積まれるのは次のような1本の JSON 文字列。

json
{
  "class": "WelcomeEmailJob",
  "args": [123, "hoge"],
  "retry": true,
  "queue": "default",
  "jid": "a1b2c3d4e5f6",
  "created_at": 1691234567.123,
  "enqueued_at": 1691234567.456
}
キー 意味
class 実行するジョブクラス名。ワーカー側はこの文字列から const_get してインスタンス化する
args perform に渡される引数。JSON で表現できる値だけ
retry 失敗時のリトライ設定(true / false / 回数)
queue 投入先のキュー名
jid ジョブ一意 ID。ログ追跡やダッシュボードでの特定に使う
created_at / enqueued_at 作成時刻 / キュー投入時刻。予約ジョブだとこの2つがずれる

ここが Sidekiq の一番の制約。ActiveRecord のモデルをそのまま渡せない。
args は JSON にシリアライズされるので、渡せるのは String / Integer / Float / Boolean / nil / Array / Hash だけ。
ID を渡してワーカー側で find する、が基本形になる理由がこれ。
(ActiveJob 経由の場合は GlobalID でモデルを渡せるが、これも「gid://app/User/123 という文字列に変換している」だけで、
結局 Redis に入るのは文字列。実行時にはその ID で引き直されるので、状態が変わっている前提は変わらない)


2. 即実行キューは List

queue:default のような通常のキューは Redis の List で管理される。List は挿入順を保持する配列のような構造。

コマンド 位置
投入(Rails) LPUSH queue:default '{"class":...}' 左端(先頭)に積む
取得(Sidekiq) BRPOP queue:default 2 右端(末尾)から取る

左から入れて右から出すので、先に入れたジョブが先に出る(FIFO)。

BRPOPB は Blocking。「ジョブが入るまで待つ」コマンドなので、Sidekiq は
「ジョブある?ジョブある?」とポーリングし続ける必要がない。ジョブが LPUSH された瞬間に
ブロックが解除され、即座に拾い上げる。

複数キューを優先度つきで見る場合も、BRPOP queue:critical queue:default 2 のように
引数を並べるだけで「critical を先に見る」が実現できる。

なお、存在するキュー名の一覧は別途 queues という SetSADD されている。
ダッシュボードの「キュー一覧」はこれを読んでいる。

ハマりどころ:BRPOP した瞬間、そのジョブは Redis から消える

BRPOP は「取り出す」コマンドなので、ワーカーがジョブを受け取った時点で Redis 上には残っていない。
つまり 実行中に kill -9 などでプロセスが即死すると、そのジョブは失われる

  • 通常の停止(TERM)なら、Sidekiq は処理中のジョブを待ってから終了するので消えない
  • OOM Killer や kill -9、コンテナの強制停止では落ちる
  • OSS 版でこれを厳密に防ぐ手段はない(Enterprise 版の super_fetchRPOPLPUSH
    作業用リストに退避することで回収可能にしている)

「絶対に落としたくないジョブ」は、ジョブ側を冪等にしたうえで、
DB 側にも実行予定レコードを持たせて突き合わせる、といった設計で担保することになる。


3. 予約・リトライは Sorted Set

perform_in(1.hour, ...) のような予約実行や、失敗して再実行待ちのジョブは List ではなく
Sorted Set で持たれる。キー名はそれぞれ scheduleretry

Sorted Set は、各要素に score(数値) を付けて自動でソートしてくれる構造。
Sidekiq はこの score に 「実行すべき時刻の UNIX タイムスタンプ」 を入れる。

ZADD schedule 1691238167.0 '{"class":"WelcomeEmailJob","args":[123],...}'
                ^^^^^^^^^^^^
                実行したい時刻

こうしておくと「今すぐ実行すべきジョブ」を取り出す操作が、
「score が現在時刻以下の要素を取る」 という Redis の1コマンドで済む。

Sidekiq 内部のポーリングスレッド(Scheduled::Poller)が数秒おきにこれを実行している。

sequenceDiagram
    participant App as Rails アプリ
    participant SS as schedule / retry(Sorted Set)
    participant Poller as Sidekiq Poller
    participant Q as queue:default(List)
    participant W as ワーカースレッド

    App->>SS: ZADD schedule 実行時刻 ジョブJSON
    Note over SS: 実行時刻順に並んだ状態で待機

    loop 数秒おき
        Poller->>SS: ZRANGE schedule -inf 現在時刻 BYSCORE
        SS-->>Poller: 時刻が来たジョブ
        Poller->>SS: ZREM(取れたものだけ)
        Poller->>Q: LPUSH(通常キューへ移動)
    end

    Q->>W: BRPOP
    W->>W: perform 実行

ZREM の戻り値で「自分が取った」ことを確認してから LPUSH するので、
Sidekiq プロセスが複数台いても同じジョブが二重に enqueue されない。

リトライ時に増えるフィールド

retry に積まれるジョブ JSON には、失敗の情報が追記される。

json
{
  "class": "WelcomeEmailJob",
  "args": [123],
  "jid": "a1b2c3d4e5f6",
  "retry_count": 2,
  "failed_at": 1691234600.0,
  "retried_at": 1691234900.0,
  "error_class": "Net::OpenTimeout",
  "error_message": "execution expired"
}

次のリトライ時刻は retry_count から指数的に伸びる形で決まり、その値がそのまま score になる。
リトライ上限に達したジョブは dead(これも Sorted Set)へ移され、
ダッシュボードの Dead 一覧に出る。

ジョブの状態遷移

stateDiagram-v2
    [*] --> scheduled : perform_in / perform_at
    [*] --> enqueued : perform_async
    scheduled --> enqueued : 実行時刻が来た
    enqueued --> processing : BRPOP
    processing --> [*] : 成功
    processing --> retry : 例外を raise
    retry --> enqueued : リトライ時刻が来た
    retry --> dead : リトライ上限に到達
    dead --> enqueued : 手動で Retry

4. プロセスの状態は Hash と Set

ダッシュボードの「稼働中プロセス」「実行中ジョブ」の表示も、すべて Redis から読んでいる。

用途 キー(おおよそ)
稼働中の Sidekiq プロセス一覧 Set processes
各プロセスの状態(起動時刻・並列数・busy 数など) Hash <ホスト名>:<PID>:<識別子>
そのプロセスが今処理しているジョブ Hash 上記キー + :work
累計の処理数・失敗数 String(カウンタ) stat:processed / stat:failed

各 Sidekiq プロセスは数秒おきに ハートビート を打っていて、そのたびに
自分の Hash を更新し、キーに TTL(60秒程度)を張り直している。

この TTL が「プロセスが死んだら一覧から自動で消える」を実現している。
ハートビートが止まればキーが期限切れで消滅し、ダッシュボードからも消える。
死活監視のための別テーブルもハートビート受信サーバも要らない、という設計。

「今どのスレッドが何のジョブを処理しているか」は :work の Hash に
スレッドID → ジョブ情報の JSON という形で入っている。
Hash なので「特定スレッドの情報だけ更新」「まとめて全部取得」の両方が安く済む。


データ構造の使い分けまとめ

用途 なぜその型か
即実行キュー List 順序を保持でき、BRPOP でブロッキング取得できる
予約実行 / リトライ待ち Sorted Set score に実行時刻を入れれば「時刻が来たものだけ」を範囲取得できる
キュー名一覧 / プロセス一覧 Set 重複を持ちたくない集合。存在確認が O(1)
プロセス状態 / 実行中ジョブ Hash フィールド単位で読み書きしたい構造化データ
処理数・失敗数 String INCR でアトミックに加算できるカウンタ

Sidekiq が速いのは「Redis が速いから」だけではなく、
やりたい操作がそのまま Redis の1コマンドになるデータ構造を選んでいるから、という部分が大きい。
「時刻の来たジョブを探す」を SQL でやろうとすれば
WHERE run_at <= NOW() FOR UPDATE SKIP LOCKED 相当の仕組みが要るところを、
Sorted Set の範囲取得1発に落としている。


ハマりどころ

  • args を太らせない。 ジョブ JSON は Redis(=メモリ)に載る。
    CSV の中身を丸ごと渡すのではなく、S3 のキーやレコード ID を渡して、ワーカー側で取得する。
  • 予約ジョブの精度は Poller の間隔まで。 perform_in(5.seconds, ...)
    「5秒後ぴったり」ではなく「5秒後以降、次のポーリングで」実行される。秒単位の精度は期待しない。
  • retry / dead が溜まるとメモリを食う。 Sorted Set に JSON が積み上がるので、
    リトライ上限や dead の保持期間を放置しない。
  • enqueued_atcreated_at の差がキューの詰まり具合ではない。
    予約ジョブでは最初からずれる。滞留を見たいなら enqueued_at と現在時刻の差を見る。
  • デプロイ中に消えるジョブに注意。 前述のとおり BRPOP 済みのジョブは Redis 上にない。
    強制終了を伴うデプロイをするなら、ジョブは冪等に書いておく。

参考

Index

  • SidekiqがRedisをどう使っているか(List・Sorted Set・Hash)
  • 結論
  • 全体像
  • 1. ジョブ本体はただの JSON 文字列
  • 2. 即実行キューは List
  • ハマりどころ:BRPOP した瞬間、そのジョブは Redis から消える
  • 3. 予約・リトライは Sorted Set
  • リトライ時に増えるフィールド
  • ジョブの状態遷移
  • 4. プロセスの状態は Hash と Set
  • データ構造の使い分けまとめ
  • ハマりどころ
  • 参考