Skip to content
Published on

Redis 8.8 の Array 型 — ストリーム以来初めて増えたコア型、そして Valkey にはないもの

シェア
Authors

はじめに — 6日違いで出た二つのリリース

2026年5月、同じ根から分かれた二つのプロジェクトがほぼ同時にリリースを出しました。Valkey 9.1.0が5月19日Redis 8.8.0が5月25日。6日の差です。

二つのリリースノートを並べて読むと温度差がはっきりします。Valkey 9.1.0は自ら「Upgrade urgency LOW」と書いた最初の9.1安定版で、新機能はクラスターバスのトラフィックメトリクス追加とリハッシュ中のレイテンシスパイク緩和の2件、残りはCVE3件とバグ修正です。Redis 8.8.0のリストは性格が違います — その一番上にこう書かれています。「New data structure: Array (@antirez)」。

フォーク劇の話をしたいわけではありません。ソースツリーで確認できる具体的な違いを見ようとしています。そしてこの違いは実際に src/server.h の中に一行として存在します。

ストリーム以来初めて増えたコア型

Redisのコアオブジェクト型は server.h に定数として埋め込まれています。Redis 8.8.0タグの該当部分はこうです。

#define OBJ_STRING 0    /* String object. */
#define OBJ_LIST 1      /* List object. */
#define OBJ_SET 2       /* Set object. */
#define OBJ_ZSET 3      /* Sorted set object. */
#define OBJ_HASH 4      /* Hash object. */
#define OBJ_MODULE 5    /* Module object. */
#define OBJ_STREAM 6    /* Stream object. */
#define OBJ_ARRAY 7     /* Array object. */

OBJ_ARRAY 7 が新しく加わった行です。その前の OBJ_STREAM 6 はストリームで、XADD のコマンド定義を見ると since5.0.0 です。つまりコアオブジェクト型が増えたのは、2018年のRedis 5.0のストリーム以来今回が初めてです。

ここでよくある誤解を一つ正しておきます。「ベクターセットが8.0に入ったのでは?」 — その通りですが、ベクターセットはコア型ではありません。VADD の定義は modules/vector-sets/commands.json にあり、グループは vector_set、実装は modules/vector-sets/hnsw.c です。つまりRedisツリーに同梱されるモジュールであって、t_*.c のコア型ではありません。一方Arrayは src/t_array.c(約73KB)と src/sparsearray.c(約80KB)としてコアに直接入りました。

Arrayが実際に何なのか

公式ドキュメントの定義はこうです — Arrayは0から2⁶⁴−1の範囲の整数インデックスを文字列値に対応づける疎(sparse)インデックスアドレス型データ構造です。リストのように連番の位置でアクセスするのではなくインデックスで直接アクセスし、間の空きスロットを確保しなくても任意のインデックスをセットできます。

なぜ必要なのかは、antirezがPR #15162の本文に自ら書いています。彼の表現をそのまま移せば、既存の型はどれも「位置自体がビジネス上の意味を持つ」場面ではどこかしっくりきません — ハッシュはランダムなルックアップをくれますがインデックスを別途キーとして保存しなければならず区間の可視性がなく、リストは追加とトリミングをくれますが途中にあるものへのアクセスが難しく、ストリームは追記専用イベントというまた別のものです。スロット37番、ステップ4番、行18552番、ファイルの11行目 — これらが彼の挙げた例です。

まとめると、Arrayが狙う場所はこうです。

  • インデックスそのものがドメインキーであるデータ(時間スロット、座席番号、行番号、ステップ番号)
  • 値がまばらにしか存在しない疎データ(センサーの欠測、一部の行にだけ付く注釈)
  • 直近N件だけを保持するリングバッファ

内部表現 — スライス、ポインタタギング、スーパーディレクトリ

antirezがPR本文に書いたエンコーディングの説明は4段階です。

  1. 密(dense)なとき、Arrayは実質的に少し豪華なC配列です。インデックスを保存するコストを別途払いません。
  2. ただし完全にフラットにする代わりに4096要素単位のスライスに分割し、各スライスは要素が少ないとき特殊な疎エンコーディングを使います。スライスが空になるとディレクトリには NULL だけが残ります。
  3. 小さな整数、浮動小数点数、短い文字列はポインタタギングされ、ポインタスロット自体を超える追加メモリを使いません。
  4. 非常に疎になると、ウィンドウ化されたディレクトリ群のスーパーディレクトリを使います。この表現は要素数が800万を超えるか、非常に高いインデックスがセットされたときにのみ発動します。

4番がなぜあるのかはantirezの開発記に出てきます。彼は、ユーザーが ARSET myarray 293842948324 foo をしても巨大な確保なしに動くことを望んでいて、最初に組んだ2段(ディレクトリ+スライス)の構造では足りないことを実装中に気づいたと書いています。そこで特定の条件下で構造が内部的に形を変え、スライスされた密なディレクトリ群のスーパーディレクトリになるよう設計し直し、その狙いは ARSCAN が区間幅ではなく実在する要素数に比例する時間で動くようにすることでした。

ここで正直に触れておくべきことがあります。その目標は「典型的なケース」に対するものであって保証ではありません。ARSCANAROP の公式な計算量表記はこうです — "O(P) where P is visited positions in touched slices (dense scanned slots + sparse entries), with worst-case O(|end-start|+1) and typical case close to O(N)"。最悪ケースは区間幅に比例します。ドキュメント自身がこの留保を書いているという事実こそ、むしろ信頼できる兆候です。

コマンド18個

Redis 8.8.0タグの src/commands/ にある AR* コマンドはちょうど18個です。

ARSET  ARGET  ARMSET  ARMGET  ARGETRANGE  ARSCAN
ARDEL  ARDELRANGE  ARLEN  ARCOUNT  ARINFO
ARINSERT  ARNEXT  ARSEEK  ARRING  ARLASTITEMS
AROP  ARGREP

(antirezの開発記には ARPOP への言及がありますが、8.8.0の最終的なコマンド一覧にはありません。ブログ記事に出てきた名前を鵜呑みにせず、タグ付けされたソースを見るほうが安全です。)

性格ごとにまとめるとこうです。

基本の読み書き。 ARSET は指定インデックスから連続する複数の値を書き込みます。ARGET は一つのインデックスを読み、セットされていないインデックスにはnilを返します。ARMSETARMGET は複数のインデックス・値ペアを一度に扱います。ここまでは全て @fast です。

カーソル系。 Arrayは基本的にステートレスですが、最後に追加された項目のインデックスだけを一つ覚えています。ARINSERT はそのカーソル位置に書き込みカーソルを進め、ARNEXT は次に使われるインデックスを教え、ARSEEK はカーソルを望む位置に移動します。

リングバッファ。 ARRING はサイズを指定すると、あふれたときに自動で巻き戻して切り詰めます。ARLASTITEMS は直近に入ったN件を返し、REV で順序を反転します。

サーバーサイド集計。 AROP は区間に対して SUMMINMAXANDORXOR、そして MATCHUSED を実行します。

検索。 ARGREP は区間の要素をテキスト述語で走査します。述語は EXACTMATCHGLOBRE の4種類で、複数の述語を AND または OR で組み合わせられ、LIMITWITHVALUESNOCASE オプションが付きます。

ドキュメントにある実際の動作例を移すとこうです。

ARSET  events:1 0 "login" "click" "purchase"   -> 3
ARGET  events:1 0                              -> "login"
ARGET  events:1 999                            -> nil

ARRING readings 3 "v0"  -> 0
ARRING readings 3 "v1"  -> 1
ARRING readings 3 "v2"  -> 2
ARRING readings 3 "v3"  -> 0      # サイズ3を超えて0番に巻き戻る
ARGET  readings 0       -> "v3"   # 最も古いv0を上書きする

ARLASTITEMS readings 3        -> ["v1", "v2", "v3"]
ARLASTITEMS readings 3 REV    -> ["v3", "v2", "v1"]

ARLENとARCOUNTは違うものを数える

実務で最初に踏む地雷はこれです。ドキュメントの例をそのまま移します。

ARSET   sparse 0 "a"        -> 1
ARSET   sparse 1000000 "b"  -> 1
ARLEN   sparse              -> 1000001
ARCOUNT sparse              -> 2

ARLEN は「最大インデックス+1」で ARCOUNT は「空でない要素の数」です。どちらもO(1)ですが意味はまるで違います。リストの LLEN の感覚で ARLEN を使うと、要素2個のArrayから百万が出てきます。疎なデータ構造としては当然の定義ですが、当然の定義こそ夜中の3時に人を捕まえるものです。

ARGETRANGEARSCAN の違いも同じ構図です — ARGETRANGE seq 0 3 は空きをnilで埋めて ['a', 'b', None, 'd'] を返し、ARSCAN seq 0 3 は存在するものだけをインデックスと一緒に返します。

ベンダーベンチマークが語ること、語らないこと

Redisが8.8発表記事に出した数値です。全てベンダー自己測定で、条件はRedis 8.8単一インスタンス、Intel Sapphire Rapids m7i.metal-24xl、要素10万個です。

操作(10万要素、1KB値)ArrayListHash
ランダム要素読み取り675K ops/sec133K ops/sec626K ops/sec
ランダム要素書き込み757K ops/sec137K ops/sec689K ops/sec
ランダム要素削除841K ops/sec730K ops/sec

Redisの要約は「ランダム要素操作においてArrayはHashより8〜15%良いスループットを出し、Listより最低5倍速い」です。リングバッファの比較はこうです。

リングサイズ / 要素サイズArray (ARRING)List (RPUSH+LTRIM)
1K / 100B1.11M inserts/sec512K inserts/sec
100K / 100B1.12M inserts/sec528K inserts/sec
1K / 1KB840K inserts/sec424K inserts/sec
100K / 1KB837K inserts/sec413K inserts/sec

この表の正直な読み方はこうです。

Hash比の利得は大きくありません。 8〜15%はベンダーが自社の新機能を宣伝しながら自社ハードウェアで測った数値です。すでにHashをうまく使えているなら、この数字だけで移行する理由にはなりません。

List比5倍は比較対象が不利なものです。 Listの「ランダム要素読み取り」はそもそもO(N)の連結リスト走査です。Listが本来やるべきでないことをやらせて5倍が出ただけで、Listが遅いわけではありません。ARRING の2倍のほうが公平な比較です — RPUSH+LTRIM の2往復を一つのアトミックなコマンドに縮めたものなので、実際に使われるパターンに合っています。

メモリはArrayのほうが多く使います。 同じ記事の表によれば100バイト要素で要素あたりArray 122バイト、List 104バイト、Hash 151バイトで、1KB要素ではそれぞれ1290 / 1035 / 1337バイトです。Redis自身の表現でArrayはListより要素あたり約18%多くのメモリを使います。リングバッファを LPUSH+LTRIM から ARRING に変えるとスループット2倍を得る代わりにその分メモリを余計に払うことになります。

そしてこの表にないものがあります — 疎なケースのメモリ数値、クラスター環境、レプリケーション/AOF負荷、ARMでの数値。発表記事はx86単一インスタンスしか見せていません。

ドキュメントはまだpreviewと書いている

これが最も重要な留保で、発表記事にはなくドキュメントだけにあります。公式Arrayドキュメントの最初の一行はこうです。

Array is a new data type that is currently in preview and may be subject to change.

GAリリースに載って出てきたのに、ドキュメントは依然として「プレビューであり変更される可能性がある」と明記しています。コマンド定義の since は全て 8.8.0 ですが、それはAPI安定性の約束ではないということです。プロダクションのデータモデルをここに載せる前に、この一行を必ず計算に入れる必要があります。

留保がもう一つあります。ARGREP の正規表現のためにTREライブラリが deps/tre としてベンダリングされました(8.8.0の deps/ 一覧で確認できます)。antirezがTREを選んだ理由は、病理的パターンで時間・空間が爆発しないという保証のためで、彼は foo|bar|zap 形式の選言(alternation)が非効率だった部分を自ら最適化し、潜在的なセキュリティ課題をいくつか修正したと書いています。それでも純粋な事実は残ります — サーバーに新しいネイティブC正規表現エンジンが入り、ユーザー入力パターンがその上で動くということです。PRに付いた自動レビューボットもこの点を「新しいネイティブコードの露出面」として指摘しました。

Valkeyにはない

本稿の出発点に戻ります。Valkey 9.1.0タグの src/server.h を見ると、オブジェクト型は OBJ_STREAM 6 で終わっています。OBJ_ARRAY はありません。src/ には t_hash.ct_list.ct_set.ct_stream.ct_string.ct_zset.c の6個しかなく t_array.c はありません。src/commands/AR で始まるコマンドもありません。

つまり2026年5月時点で、Redisはコア型7個、Valkeyは6個です。同じコードベースから分かれた二つのサーバーが、今や異なるデータ構造の集合を持ちます。これはパフォーマンスチューニングや設定の違いではなくデータモデルの違いであり、方向は一方向です — Arrayでモデル化したデータはValkeyへは移せません。

ここにライセンスが重なります。Redis 8.8の LICENSE.txt はRSALv2 / SSPLv1 / AGPLv3の三重ライセンスで、Valkeyの COPYINGSPDX-License-Identifier: BSD-3-Clause です。この背景はオープンソースライセンス大転換編で別途扱いました。要点だけ言えば、BSDにとどまるためにValkeyを選んだ組織にとってArrayはそもそも選択肢になく、Arrayが魅力的ならそのライセンスを受け入れる決断も同時に下すことになります。機能の選択ではなくバンドルの選択です。

8.8の残り — INCREXの方が実用的かもしれない

Arrayが見出しですが、多くのチームにとって8.8ですぐ役立つのは INCREX かもしれません。レートリミッターはこれまで大半がLuaスクリプトで実装されてきましたが、8.8がこれをコマンドにしました(実装者はraffertyyuとRedisチーム)。

INCREX key
       [<BYFLOAT|BYINT> increment]
       [LBOUND lowerbound] [UBOUND upperbound] [SATURATE]
       [EX sec | PX msec | EXAT unix-time-sec | PXAT unix-time-msec | PERSIST]
       [ENX]

発表記事が説明する要点は三つです。第一に、新しいカウンター値と実際に適用された増分を同時に返すので、呼び出し側が即座に許可/拒否を判断できます。第二に、ENX を与えるとキーに満了がないときだけ満了を設定するので、ウィンドウのTTLはウィンドウ生成時にのみ固定され、その後のリクエストに押されて延長されません — Luaなしでやろうとすると誰もが一度は間違えるあの部分です。第三に、境界を超えると拒否し、SATURATE を与えると境界に貼り付けて部分的に受け入れます。レートリミティングのアルゴリズム自体はトークンバケットとスライディングウィンドウ編に整理してあります。

このほか8.8には、ストリームコンシューマがメッセージを明示的に返却する XNACK(SILENT/FAIL/FATAL モード)、ハッシュフィールド単位の通知、ZUNION/ZINTERCOUNT 集計子、JSON.SETFPHA(浮動小数点配列をBF16/FP16/FP32/FP64のどれで保存するか指定)が入りました。

AIが作ったデータ構造だという事実

antirezは開発記で、この型をどう作ったかを隠さずに書いています。1月の第一週に始め、最初の月は仕様書だけを手で書いたそうです。最初はOpusとペアを組み、GPT-5.3が出た後は設計と開発をCodexに移したといいます。2ヶ月目からは自動コーディングで実装しながらレビューし続け、先述のインダイレクション再設計はこの時期に出てきました。3ヶ月目はストレステストでした。

彼の結論をそのまま移すと、高品質なシステムプログラミングでは依然として本人が完全に関与する必要があるが、AIのおかげで普段なら飛ばしていたであろう複雑さのレベルまで到達できたということです。そして ARGREP は計画になかった機能でした — データ構造をあれこれ使ってみるうちにマークダウンファイルをArrayに入れることになり、エージェント用スキル知識ベースが必要だった自身の状況とかみ合って作ったといいます。

PR自体の規模は確認可能な数字として残っています — コミット18個、86ファイル、+22,258 / −39行。5月4日に開かれ5月13日にマージされ、12日後の8.8.0 GAに載りました。これをどう評価するかは各自次第ですが、少なくとも「AIが書いたコードがプロダクションデータベースのコアデータ構造として入った」という具体的で記録された事例であることは記しておく価値があります。ドキュメントのpreview表示を見るとき、この文脈を合わせて見るのも合理的です。

で、あなたは使うべきか

使ってコストに見合う場合

  • インデックスが本当にドメインキーである — 時間スロット、座席/枠番号、行番号、ワークフローステップ。今ハッシュにインデックスを文字列フィールドとして詰め込んでいるなら、まさにこの場所です。
  • リングバッファを RPUSH+LTRIM で回していて、その往復と非アトミック性が実際に痛い。ARRING が一コマンドに縮めてくれます。
  • 区間集計をすべてクライアント側に引き寄せてやっている。AROP がサーバー側で完結させます。

過剰または危険な場合

  • push/popが必要、または要素の間に挿入する必要がある → Redis自身がリストを使えと言っています。
  • 数値インデックスではなくフィールド名でアクセスする → ハッシュを使えと言っています。
  • メモリが逼迫している → リストより要素あたり約18%多く払います。
  • Valkeyを使っている、または使う可能性がある → 存在しない型です。
  • API安定性が必要 → ドキュメントがpreviewと書いています。
  • すでにうまく動いているハッシュベースの設計がある → ベンダーベンチマークの8〜15%という数字は書き直しのコストを正当化しません。

おわりに

まとめるとこうです。Redis 8.8はストリーム以来初めてコアオブジェクト型を一つ増やし、その型はインデックスが意味を持つデータのための疎コンテナです。4096要素のスライスとポインタタギングで密・疎の両方をこなし、コマンド18個で集計とgrepまでサーバー側で完結させます。同じ月に出たValkey 9.1のソースにはこの型がなく、今後もデータモデルのレベルで両サーバーは分かれたまま進みます。

同時にこれはGAに載ったのにドキュメントがpreviewと書いている型であり、ベンダーベンチマークの利得の大半はリストにリストが不得意なことをやらせて出たもので、Hash比の実質的な利得は一桁台〜十数パーセントにメモリを追加で払う取引です。

だから結論は「Redisに良いものができたから使おう」ではありません。今インデックスをハッシュキーに文字列として詰め込んでいる、あるいは LPUSH+LTRIM の往復を我慢しているなら、Arrayはその特定の不便さを正確に狙ったものです。そういう不便さがなければ、8.8であなたにとって重要なのはArrayではなく INCREXかもしれませんし、何もないかもしれません。新しいデータ構造は新しいハンマーであり、釘があるかどうかをまず確認するのが順序です。

参考資料