Redis Setとは?基本と使い方を徹底解説


Redis Setとは?基本と使い方を徹底解説

はじめに

現代のアプリケーション開発において、データストアの選択は非常に重要です。特に、高速な読み書き性能が求められるキャッシュ層や、特定のデータ構造に特化した処理を行いたい場合には、リレーショナルデータベースだけでなく、NoSQLデータベースやインメモリデータストアが活用されます。その中でもRedisは、その圧倒的なパフォーマンスと豊富なデータ型で広く利用されています。

Redisは単なるキーバリューストアではなく、String、List、Hash、Set、Sorted Set、Streamなど、様々なデータ型をネイティブにサポートしています。これらのデータ型を適切に使い分けることで、アプリケーションの設計がシンプルになり、複雑な処理も効率的に実装できます。

本記事では、Redisが提供する主要なデータ型の一つである「Set」に焦点を当て、その基本概念から詳しい使い方、応用例、内部構造、パフォーマンス特性、さらには利用上の注意点やベストプラクティスまでを、約5000語にわたって徹底的に解説します。

Setは、集合論における「集合」の概念をコンピュータ上で実現したものです。数学の集合と同様に、「要素の重複を許さない非順序コレクション」という特性を持ちます。この特性が、特定のアプリケーションシナリオで非常に強力な威力を発揮します。

例えば、

  • サイトのユニーク訪問者数をカウントしたい。
  • ある商品に付けられたタグの一覧を管理したい。
  • ユーザーの友達リストを効率的に扱いたい。
  • 複数のリストに含まれる共通の要素を見つけたい。

といった要件に対し、Setは非常にシンプルかつ高性能な解を提供してくれます。

この記事を通じて、Redis Setの強力な機能とその使い方を理解し、あなたのアプリケーション開発に活かせるようになることを目指します。

Redisのデータ型としてのSet

Redisは様々なデータ型を提供しており、それぞれが特定のデータ構造やアクセスパターンに最適化されています。Setを理解するために、まずはRedisの他の主要なデータ型と比較してみましょう。

  • String: 最も基本的なデータ型。文字列、整数、浮動小数点数、バイナリデータなどを格納できます。単一の値とキーが紐づきます。
  • List: 順序付けられた要素の集まり。両端から要素を追加・削除するキューやスタック、または単純なリストとして使えます。要素は重複してもよく、順序が保証されます。
  • Hash: フィールドと値のペアの集まり。オブジェクトやレコードを表現するのに適しています。一つのキーに対して複数のフィールドを持つことができます。
  • Sorted Set: 要素にスコアが付与されたSet。Setの特性である「重複しない」「非順序」に加えて、「スコアによって順序付けられる」という特性を持ちます。ランキング機能の実装などに利用されます。
  • Set: 重複しない要素の集まりで、要素に順序はありません。 これがSetの最も重要な特徴です。

Setの核心は、以下の2つの特性にあります。

  1. 非順序 (Unordered): Setに要素を追加しても、格納された順序は保持されません。要素を取り出す際も、特定の順序は保証されません。
  2. 重複なし (No Duplicates): Setは同じ要素を複数格納することができません。既に追加されている要素を再度追加しようとしても、操作は成功しますが、Setの内容は変化しません。

これらの特性により、Setは「ある要素がコレクションに含まれているか?」という確認(存在確認)や、「複数のコレクション間で共通、または異なる要素を見つける」(集合演算)といった操作を、非常に効率的に行うことができます。

Setは内部的にはハッシュテーブル(または要素が全て整数の場合は特定の最適化された構造)を使用して実装されています。これにより、要素の追加、削除、存在確認といった操作が平均的にO(1)の計算量で実行されます。これは、Setの要素数が増加しても、これらの基本的な操作にかかる時間がほぼ一定であるということを意味し、高いスケーラビリティの基盤となります。

Setの基本操作

Redis Setは、コマンドを使って操作します。ここでは、Setを操作するための基本的なコマンドとその使い方を詳しく見ていきます。

1. 要素の追加 (SADD)

SADDコマンドは、指定したSetに1つ以上の要素を追加します。既に追加されている要素は無視されます。

SADD key member [member ...]

  • key: 要素を追加するSetのキー名。
  • member: 追加する要素。複数指定可能。

戻り値: 実際に追加された新しい要素の数。既存の要素を再度追加しようとした場合はカウントされません。

例:

“`bash

‘myset’ というSetに要素 ‘apple’ と ‘banana’ を追加

SADD myset apple banana
(integer) 2

‘myset’ に ‘banana’ (既存) と ‘cherry’ を追加

SADD myset banana cherry
(integer) 1 # ‘cherry’ だけが新しく追加された

‘myset’ の内容を確認 (順序は保証されない)

SMEMBERS myset
1) “apple”
2) “banana”
3) “cherry”
“`

SADDはSetが存在しない場合は新しく作成してから要素を追加します。Setは文字列を要素として保持できます。バイナリデータも格納可能ですが、通常はUTF-8エンコードされた文字列が使われます。

時間計算量: O(N) where N is the number of members to add. (Adding a single member is O(1) on average.)
単一要素の追加は平均O(1)です。複数要素を一度に追加する場合、追加する要素数Nに対してO(N)となります。内部的には各要素のハッシュ値を計算し、ハッシュテーブルに挿入する操作が行われます。ハッシュ衝突がない理想的なケースではO(1)ですが、実際にはハッシュ衝突の解決により変動する可能性があるため、平均O(1)と表現されます。

2. 要素の取得 (SMEMBERS)

SMEMBERSコマンドは、指定したSetに含まれるすべての要素を返します。Setは非順序コレクションなので、返される要素の順序は保証されません。

SMEMBERS key

  • key: 要素を取得するSetのキー名。

戻り値: 指定したSetに含まれるすべての要素のリスト。キーが存在しない場合は空のリストを返します。

例:

“`bash

‘myset’ のすべての要素を取得

SMEMBERS myset
1) “apple”
2) “banana”
3) “cherry”

存在しないSetの要素を取得

SMEMBERS non_existent_set
(empty array)
“`

SMEMBERSはSetの要素数が多い場合に注意が必要です。Set内のすべての要素を取得してクライアントに送信するため、Setのサイズが大きくなると、ネットワーク帯域やRedisサーバーのCPUに負荷をかける可能性があります。数万、数百万といった要素を持つSetに対して頻繁にSMEMBERSを実行するのは避けるべきです。

より効率的に多数の要素をイテレーション(走査)したい場合は、Redis 2.8で導入されたカーソルベースのイテレーションコマンドであるSSCANを使用します。SSCANは一度に返す要素数を制限し、カーソルを使って続きを取得していくため、サーバーへの負荷を抑えながら大規模なSetを走査できます。

時間計算量: O(N) where N is the size of the set.
Setに含まれる要素数Nに比例します。これは、Set内のすべての要素を走査する必要があるためです。

3. 要素の存在確認 (SISMEMBER)

SISMEMBERコマンドは、指定した要素がSetに含まれているかを確認します。これはSetの最も典型的な使い方の1つです。

SISMEMBER key member

  • key: 確認対象のSetのキー名。
  • member: 存在を確認したい要素。

戻り値:
* 1: 要素がSetに含まれている場合。
* 0: 要素がSetに含まれていない、またはSetが存在しない場合。

例:

“`bash

‘myset’ に ‘apple’ が含まれているか確認

SISMEMBER myset apple
(integer) 1

‘myset’ に ‘grape’ が含まれているか確認

SISMEMBER myset grape
(integer) 0

存在しないSetに対して確認

SISMEMBER non_existent_set some_member
(integer) 0
“`

SISMEMBERは、その高速性(平均O(1))から、ユーザーのログイン状態確認(セッションIDをSetに保持)、特定のコンテンツへのアクセス権限確認、または既に処理済みかどうかのフラグとして広く利用されます。

時間計算量: O(1) on average.
内部のハッシュテーブルにより、要素のハッシュ値を計算して参照するだけで存在確認ができるため、非常に高速です。

4. 要素の削除 (SREM)

SREMコマンドは、指定したSetから1つ以上の要素を削除します。Setに含まれていない要素を指定してもエラーにはならず、単に無視されます。

SREM key member [member ...]

  • key: 要素を削除するSetのキー名。
  • member: 削除する要素。複数指定可能。

戻り値: 実際にSetから削除された要素の数。

例:

“`bash

‘myset’ から ‘banana’ を削除

SREM myset banana
(integer) 1

‘myset’ から ‘apple’ (既存) と ‘grape’ (存在しない) を削除

SREM myset apple grape
(integer) 1 # ‘apple’ だけが削除された

削除後の ‘myset’ の内容を確認

SMEMBERS myset
1) “cherry”

存在しないSetから削除しようとする

SREM non_existent_set some_member
(integer) 0
“`

SREMもまたSetのサイズに依存せず、平均O(1)で要素を削除できます(複数指定の場合はO(N))。ユーザーが特定のタグを外したり、友達リストからユーザーを削除したりする際に利用できます。

時間計算量: O(N) where N is the number of members to remove. (Removing a single member is O(1) on average.)
単一要素の削除は平均O(1)です。複数要素を一度に削除する場合、削除する要素数Nに対してO(N)となります。内部的には要素のハッシュ値を計算し、ハッシュテーブルから削除する操作が行われます。

5. Setの要素数取得 (SCARD)

SCARDコマンドは、指定したSetに含まれる要素の数を取得します。

SCARD key

  • key: 要素数を取得するSetのキー名。

戻り値: Setに含まれる要素の数。キーが存在しない場合は 0 を返します。

例:

“`bash

‘myset’ の要素数を取得

SCARD myset
(integer) 1 # ‘cherry’ のみ

新しい要素を追加後、再度要素数を取得

SADD myset apple banana
(integer) 2
SCARD myset
(integer) 3 # ‘cherry’, ‘apple’, ‘banana’

存在しないSetの要素数を取得

SCARD non_existent_set
(integer) 0
“`

SCARDは、Setのサイズを知りたい場合に非常に高速(O(1))で実行できます。例えば、オンラインユーザー数をカウントしたり、特定の条件を満たすアイテムの数を表示したりする場合に便利です。

時間計算量: O(1).
Redisは各データ型の要素数を内部で管理しており、Setのサイズを即座に取得できます。

6. Setからのランダムな要素取得 (SRANDMEMBER)

SRANDMEMBERコマンドは、Setの中から1つ以上のランダムな要素を選択して返します。このコマンドはSetの内容を変更しません。

SRANDMEMBER key [count]

  • key: 要素を取得するSetのキー名。
  • count (オプション): 取得したいランダムな要素の数。

戻り値:
* count を指定しない場合: ランダムな要素1つ。Setが空またはキーが存在しない場合は nil を返します。
* count を指定する場合: ランダムな要素のリスト。Setが空またはキーが存在しない場合は空のリストを返します。
* count が正の場合: 重複しない count 個の要素のリスト。Setの要素数より大きい count を指定した場合、Set内のすべての要素が返されます。
* count が負の場合: 重複を許して abs(count) 個の要素のリスト。Setの要素数に関わらず、指定した数の要素が返されます。

例:

“`bash

‘myset’ の現在の要素: ‘cherry’, ‘apple’, ‘banana’

ランダムな要素を1つ取得

SRANDMEMBER myset
“banana” # 実行するたびに異なる要素が返される可能性がある

重複なしでランダムな要素を2つ取得

SRANDMEMBER myset 2
1) “apple”
2) “cherry” # 実行するたびに異なる要素と順序が返される可能性がある

重複を許してランダムな要素を5つ取得 (Setの要素数は3つ)

SRANDMEMBER myset -5
1) “cherry”
2) “banana”
3) “cherry”
4) “apple”
5) “banana” # 同じ要素が複数回出現する可能性がある

存在しないSetに対して実行

SRANDMEMBER non_existent_set
(nil)
SRANDMEMBER non_existent_set 2
(empty array)
“`

SRANDMEMBERは、お題のランダム表示、おすすめアイテムの提示など、Setから無作為に要素を選びたい場合に便利です。

時間計算量:
* count を指定しない場合: O(1) on average.
* count を指定する場合:
* count が正の場合: O(N) where N is the number of elements to return. If N is much smaller than the set size, it’s O(N). If N is close to the set size, it’s closer to O(S) where S is the set size, as it might need to retry picking elements to avoid duplicates.
* count が負の場合: O(N) where N is the absolute value of count.
単一要素の取得はO(1)です。複数要素を取得する場合、重複を許さない場合はSetのサイズや取得数によってO(N)またはO(S)になります(小さなSetから多くの要素を取る場合は全要素をスキャンするのに近くなるため)。重複を許す場合は、単純に指定回数だけランダムピックを繰り返すためO(|count|)となります。

7. Setからのランダムな要素削除と取得 (SPOP)

SPOPコマンドは、Setの中からランダムな要素を1つ(または複数)選択し、その要素をSetから削除してから返します。Setをキューやスタックのように扱いたい場合に利用できますが、Setは順序がないため、厳密にはキューやスタックとは異なります。

SPOP key [count]

  • key: 要素を取得・削除するSetのキー名。
  • count (オプション): 取得・削除したいランダムな要素の数。Redis 3.2から追加されました。

戻り値:
* count を指定しない場合: ランダムな要素1つ。Setが空またはキーが存在しない場合は nil を返します。
* count を指定する場合: ランダムな要素のリスト。Setが空またはキーが存在しない場合は空のリストを返します。

例:

“`bash

‘myset’ の現在の要素: ‘cherry’, ‘apple’, ‘banana’

ランダムな要素を1つ取得し、Setから削除

SPOP myset
“apple” # 実行するたびに異なる要素が返される可能性がある

削除後の ‘myset’ の内容を確認

SMEMBERS myset
1) “cherry”
2) “banana”

重複なしでランダムな要素を2つ取得し、Setから削除

SPOP myset 2
1) “banana”
2) “cherry” # 残りの要素がすべて返される (Setの要素数より大きいcountを指定した場合)

削除後の ‘myset’ の内容を確認

SMEMBERS myset
(empty array)

存在しないSetに対して実行

SPOP non_existent_set
(nil)
SPOP non_existent_set 2
(empty array)
“`

SPOPは、処理キューからランダムにタスクを取り出して処理する、といったシナリオに利用できます。SRANDMEMBERと異なり要素がSetから消えるため、一度処理した要素を再度処理しないようにする用途に適しています。

時間計算量:
* count を指定しない場合: O(1) on average.
* count を指定する場合: O(N) where N is the number of elements to return.
単一要素の取得・削除は平均O(1)です。複数要素を取得・削除する場合、指定した要素数Nに対してO(N)となります。

Setの集合演算

Setの強力な機能の一つは、複数のSet間で集合演算を実行できることです。和集合、差集合、積集合といった操作がサポートされており、これらを応用することで複雑な要件もシンプルに実装できます。

1. 和集合 (SUNION, SUNIONSTORE)

和集合は、2つ以上のSetに含まれるすべての要素を合わせた新しい集合を生成する操作です。元のSetには重複して含まれていた要素も、結果のSetでは1つの要素として扱われます。

  • SUNION: 複数のSetの和集合を計算し、その結果の要素を返します。
    SUNION key [key ...]

    • key: 和集合の対象となるSetのキー名。複数指定可能。
    • 戻り値: 和集合の結果の要素のリスト。
  • SUNIONSTORE: 複数のSetの和集合を計算し、その結果を新しいSetとして別のキーに格納します。
    SUNIONSTORE destination key [key ...]

    • destination: 和集合の結果を格納する新しいSetのキー名。既に存在する場合は上書きされます。
    • key: 和集合の対象となるSetのキー名。複数指定可能。
    • 戻り値: 結果のSetに格納された要素の数。

例:

“`bash

SADD setA 1 2 3 4
(integer) 4
SADD setB 3 4 5 6
(integer) 4

setA と setB の和集合を計算 (要素を取得)

SUNION setA setB
1) “1”
2) “2”
3) “3”
4) “4”
5) “5”
6) “6” # 順序は保証されない

setA と setB の和集合を計算し、’setC’ に格納

SUNIONSTORE setC setA setB
(integer) 6 # setCに格納された要素数

‘setC’ の内容を確認

SMEMBERS setC
1) “1”
2) “2”
3) “3”
4) “4”
5) “5”
6) “6”
“`

応用例:
* 複数のユーザーが興味を示したタグの全体リスト。
* 特定のキャンペーンに参加したすべてのユーザーのリスト。
* 複数のカテゴリに属するすべての製品リスト。

時間計算量: O(N) where N is the total number of elements in all the input sets. (Plus O(M) for SUNIONSTORE where M is the size of the result set to store).
すべての入力Setの要素数の合計に比例します。SUNIONSTOREの場合は、結果のSetのサイズMに対する書き込みコストO(M)が加わります。入力Setが大きい場合、実行に時間がかかる可能性があります。

2. 差集合 (SDIFF, SDIFFSTORE)

差集合は、最初のSetに含まれていて、かつそれ以外のSetには含まれていない要素の集合を生成する操作です。

  • SDIFF: 最初のSetに含まれていて、それ以外のSetには含まれていない要素を返します。
    SDIFF key [key ...]

    • key: 最初のSetが基準となり、それ以降のSetから差分を計算します。
    • 戻り値: 差集合の結果の要素のリスト。
  • SDIFFSTORE: 最初のSetに含まれていて、それ以外のSetには含まれていない要素を計算し、その結果を新しいSetとして別のキーに格納します。
    SDIFFSTORE destination key [key ...]

    • destination: 差集合の結果を格納する新しいSetのキー名。
    • key: 最初のSetが基準となり、それ以降のSetから差分を計算します。
    • 戻り値: 結果のSetに格納された要素の数。

例:

“`bash

SADD setA 1 2 3 4
(integer) 4
SADD setB 3 4 5 6
(integer) 4
SADD setC 4 6 7
(integer) 3

setA – (setB U setC) を計算 (setAに含まれ、かつ setB, setC のどちらにも含まれない要素)

SDIFF setA setB setC
1) “1”
2) “2” # setAに含まれる1, 2, 3, 4 のうち、setB(3,4,5,6), setC(4,6,7) にも含まれる3, 4 を除外

setA – setB を計算し、’setD’ に格納

SDIFFSTORE setD setA setB
(integer) 2 # 結果: 1, 2

‘setD’ の内容を確認

SMEMBERS setD
1) “1”
2) “2”
“`

応用例:
* サイトにアクセスしたユーザーのうち、特定のページをまだ見ていないユーザーのリスト。
* ある製品を購入した顧客のうち、まだレビューを投稿していない顧客のリスト。
* 複数のタグが付いているアイテムから、特定のタグだけが付いているアイテムを除外したい場合。

時間計算量: O(N) where N is the total number of elements in all the input sets. (Plus O(M) for SDIFFSTORE where M is the size of the result set to store).
すべての入力Setの要素数の合計に比例します。SDIFFSTOREの場合は、結果のSetのサイズMに対する書き込みコストO(M)が加わります。

3. 積集合 (SINTER, SINTERSTORE)

積集合は、2つ以上のSetすべてに共通して含まれる要素の集合を生成する操作です。

  • SINTER: 複数のSetの積集合を計算し、その結果の要素を返します。
    SINTER key [key ...]

    • key: 積集合の対象となるSetのキー名。複数指定可能。
    • 戻り値: 積集合の結果の要素のリスト。
  • SINTERSTORE: 複数のSetの積集合を計算し、その結果を新しいSetとして別のキーに格納します。
    SINTERSTORE destination key [key ...]

    • destination: 積集合の結果を格納する新しいSetのキー名。
    • key: 積集合の対象となるSetのキー名。複数指定可能。
    • 戻り値: 結果のSetに格納された要素の数。

例:

“`bash

SADD setA 1 2 3 4
(integer) 4
SADD setB 3 4 5 6
(integer) 4
SADD setC 4 6 7
(integer) 3

setA と setB の積集合を計算

SINTER setA setB
1) “3”
2) “4” # setAとsetBの両方に含まれる要素

setA, setB, setC の積集合を計算

SINTER setA setB setC
1) “4” # setA, setB, setCすべてに含まれる要素

setA と setB の積集合を計算し、’setE’ に格納

SINTERSTORE setE setA setB
(integer) 2 # 結果: 3, 4

‘setE’ の内容を確認

SMEMBERS setE
1) “3”
2) “4”
“`

応用例:
* 複数のタグがすべて付けられているアイテムのリスト。
* 特定の複数のカテゴリに属する製品リスト。
* 共通の友達を探す機能。
* 複数のユーザーが同時に「いいね」したアイテムのリスト。

時間計算量: O(N*K) worst case, O(N) typical where N is the number of elements in the smallest input set and K is the number of input sets. (Plus O(M) for SINTERSTORE where M is the size of the result set to store).
入力Setの数Kと、最も小さい入力Setの要素数Nに依存します。Redisは通常、最も小さいSetを基準に走査することで効率を最適化します。そのため、要素数N * Set数Kが最悪計算量となりますが、実際にはより効率的なアルゴリズムが使われるため、典型的なケースではほぼO(N)に近いパフォーマンスとなります。SINTERSTOREの場合は、結果のSetのサイズMに対する書き込みコストO(M)が加わります。

集合演算コマンド (*STORE) の使い分け

SUNIONSTORE, SDIFFSTORE, SINTERSTORE コマンドは、計算結果をクライアントに返す代わりに、新しいSetとしてサーバー上に保存します。これは特に以下のケースで有用です。

  • 計算結果を頻繁に再利用する場合: 結果を毎回計算するよりも、一度計算して保存し、それを参照する方が効率的です。
  • 計算結果が大きい場合: 大量の要素をクライアントに送信するのはネットワーク負荷が高くなります。サーバー側で保存することで、ネットワーク帯域を節約できます。
  • 複数の集合演算を組み合わせる場合: 中間結果をサーバー上に保持しておけば、一連の操作を効率的に実行できます。

ただし、結果を格納するための追加のメモリが必要になる点に注意が必要です。また、これらの操作は複数のSetを対象とするため、対象のSetが全て同じRedisインスタンス(またはRedis Clusterのスロット)にある必要があります。異なるスロットに分散しているSet間では、直接的な集合演算はできません。

Setの応用例

Setのユニークで高速な操作、特に存在確認と集合演算は、様々なアプリケーションシナリオで強力なツールとなります。

1. タグの実装

ブログ記事や商品、ユーザーなどにタグを付ける機能はよくあります。Setを使うと、これを効率的に実装できます。

  • タグの管理:

    • あるアイテム(例: 記事ID 101)に付けられたタグを、Setとして保持します。キーは article:101:tags のようになります。
    • タグを追加するには SADD article:101:tags redis database nosql
    • タグを削除するには SREM article:101:tags nosql
    • タグの一覧を取得するには SMEMBERS article:101:tags
    • 特定のタグ(例: redis)がアイテムに付けられているか確認するには SISMEMBER article:101:tags redis
  • タグからのアイテム検索:

    • 特定のタグ(例: redis)が付けられたすべてのアイテムのリストを、別のSetとして保持します。キーは tag:redis:articles のようになります。
    • 記事ID 101にタグ redis が付けられたら、SADD tag:redis:articles 101 を実行します。
    • 記事ID 101からタグ redis が削除されたら、SREM tag:redis:articles 101 を実行します。
    • タグ redis が付けられたすべての記事IDを取得するには SMEMBERS tag:redis:articles
  • 複数のタグによるアイテム検索 (AND検索):

    • タグ redisdatabase の両方が付けられた記事を検索したい場合、tag:redis:articlestag:database:articles の積集合を取ります。
    • SINTER tag:redis:articles tag:database:articles
  • 複数のタグによるアイテム検索 (OR検索):

    • タグ redis または database のいずれかが付けられた記事を検索したい場合、tag:redis:articlestag:database:articles の和集合を取ります。
    • SUNION tag:redis:articles tag:database:articles

この設計により、タグ付けやタグによる検索を非常に高速に行うことができます。

2. ユニークユーザーのカウント (訪問者数など)

Webサイトやアプリケーションで、ある期間内のユニーク訪問者数をカウントしたい場合によくSetが使われます。

  • 日ごとのユニーク訪問者数:

    • 毎日、または時間帯ごとに新しいSetを作成します。キーは unique:visitors:20231027 のようになります。
    • ユーザーがアクセスするたびに、そのユーザーのID(セッションIDやユーザーIDなど)を対応する日のSetに SADD します。
    • SADD unique:visitors:20231027 user_id_abc
    • その日のユニーク訪問者数を取得するには SCARD unique:visitors:20231027
  • 週ごと/月ごとのユニーク訪問者数:

    • 週や月のSetキーに対して、毎日(または毎週)の日ごとのSetを SUNIONSTORE でマージしていきます。
    • SUNIONSTORE unique:visitors:weekly:2023W43 unique:visitors:weekly:2023W43 unique:visitors:20231023 (最初の週のSetがない場合は、最初の日のSetをコピーするか、最初の日のSetと空のSetでUNIONSTOREを実行)
    • 週のユニーク訪問者数を取得するには SCARD unique:visitors:weekly:2023W43

Setを使うことで、ユーザーIDを重複なく効率的に管理し、ユニーク数をO(1)で取得できます。

3. 友達リスト / フォロワーリストの実装

SNSなどにおける友達リストやフォロワーリストもSetで実装できます。

  • 友達リスト:

    • ユーザーID 123 の友達リストを user:123:friends というSetで管理します。
    • 友達を追加するには SADD user:123:friends user_id_456
    • 友達を削除するには SREM user:123:friends user_id_456
    • 友達リストを取得するには SMEMBERS user:123:friends
    • ユーザーID 123 と 789 が友達関係にあるか確認するには、SISMEMBER user:123:friends user_id_789SISMEMBER user:789:friends user_id_123 の両方が真であるかを確認します(双方向の関係の場合)。
  • 共通の友達:

    • ユーザーID 123 と 456 の共通の友達を探すには、user:123:friendsuser:456:friends の積集合を取ります。
    • SINTER user:123:friends user:456:friends

Setの高速な存在確認と集合演算が活かされます。

4. 共同購入 / 「この商品を買った人はこんな商品も買っています」機能

  • 共同購入者:

    • 商品ID 501 を購入したユーザーのSetを product:501:buyers で管理します。
    • ユーザーID 123 が商品 501 を購入したら SADD product:501:buyers user_id_123
    • 商品 501 と 602 の両方を購入したユーザーを探すには SINTER product:501:buyers product:602:buyers
  • 協調フィルタリングの基礎 (簡単なレコメンデーション):

    • ある商品(例: 商品ID 501)を買った人が、他のどの商品を買っているかを調べたい場合。
    • まず、商品 501 を買ったユーザーのSet (product:501:buyers) を取得します。
    • 次に、これらのユーザーそれぞれが買った商品のSet(例: user:123:purchased_products)を全て集めます。
    • product:501:buyers に含まれる各ユーザーの user:*:purchased_products Setを全て対象とした和集合を計算します。
    • SUNION user:user_id_1:purchased_products user:user_id_2:purchased_products ...
    • この和集合から、元の商品 501 を除外したものが、「商品 501 を買った人が他に買っている商品」のリストになります。

これはあくまでSetを使った簡単な例であり、より高度なレコメンデーションにはSorted Setや他のアルゴリズムが使われることが多いですが、Setは基礎的な集合分析に役立ちます。

5. メッセージングシステムにおける購読者管理

Pub/Subモデルにおいて、特定のトピックの購読者リストをSetで管理できます。

  • トピック購読者:
    • トピック news:sports の購読者リストを topic:news:sports:subscribers Setで管理します。
    • ユーザーID 123 がこのトピックを購読したら SADD topic:news:sports:subscribers user_id_123
    • 購読を解除したら SREM topic:news:sports:subscribers user_id_123
    • 特定のトピックの購読者全員にメッセージを送信する場合、SMEMBERS topic:news:sports:subscribers で購読者リストを取得し、それぞれにメッセージを送ります。

この方式は、RedisのPub/Sub機能自体とは別のアプローチですが、Setによる購読者リスト管理は柔軟なユースケース(例: 特定ユーザーグループへの一括送信など)に対応できます。

Setの高度なトピック

Setの基本的な使い方に加えて、内部構造やパフォーマンス、分散環境での挙動などを理解することは、より効率的で堅牢なシステムを構築するために重要です。

1. 内部エンコーディング (intset, hashtable)

Redis Setは、格納する要素の性質や数に応じて、内部的に異なるデータ構造を使用します。これは、メモリ効率とパフォーマンスを最適化するためのRedisの工夫です。Setの内部エンコーディングには主に以下の2種類があります。

  • intset: Setの要素がすべて64ビット符号付き整数で、かつ要素数が特定の閾値(デフォルトではset-max-intset-entries、Redis 7.0以前は512、7.0以降は4096)以下の場合は、intsetと呼ばれる特殊な構造が使われます。intsetは整数のソート済み配列として実装されており、非常にコンパクトでメモリ効率が良いです。整数の検索は二分探索で行われます(O(log N))。要素の追加や削除は要素の移動が必要なためO(N)かかることがありますが、intsetのサイズが小さい間はこのオーバーヘッドも許容範囲内です。
  • hashtable: intsetの条件(要素が全て整数でない、または要素数が閾値を超えた)を満たさない場合は、通常のハッシュテーブルが使われます。これはDict(Redisのハッシュテーブル実装)のキーだけを使用する形になります。ハッシュテーブルは要素の追加、削除、存在確認が平均O(1)で実行でき、文字列を含む任意のバイナリデータを要素として格納できます。

RedisはSetに対する操作を実行する際に、必要に応じて自動的にintsetからhashtableへのエンコーディング変換を行います。一度hashtableに変換されると、intsetに戻ることはありません。

この内部エンコーディングの違いは、Setの要素数が少なく、かつ全て整数の場合にSADDSREMが一時的にO(N)になる可能性があるという点で影響しますが、通常の使用では透過的に扱われます。要素数が多くなったり、文字列を含む場合はhashtableになり、期待通りのO(1)パフォーマンスが得られます。

設定パラメータ set-max-intset-entries を調整することで、intsetを使う最大要素数を変更できます。値を大きくすればより大きなSetでintsetのメモリ効率を利用できますが、要素の追加・削除のコストが増加する可能性があります。

2. パフォーマンス特性

各Setコマンドの時間計算量は、前述の通りです。 Setの操作は、要素の追加/削除/存在確認といった単一要素操作が平均O(1)、全要素取得(SMEMBERS)や集合演算がO(N)またはそれ以上となります。

重要なポイントは以下の通りです。

  • O(1)操作の活用: SADD, SREM, SISMEMBER, SCARD, SPOP, SRANDMEMBER (count=1) は、Setのサイズに関わらず非常に高速です。Setの強みはこれらの操作にあります。ユニーク性の確保や高速な存在確認が必要なユースケースで最大限に活かせます。
  • O(N)操作の注意点: SMEMBERS, SSCAN, SRANDMEMBER (count > 1), SPOP (count > 1), SUNION, SDIFF, SINTER といった操作は、SetのサイズNに比例して時間がかかります。Setが非常に大きい場合(数百万以上の要素など)、これらの操作はレイテンシを増加させ、サーバーに負荷をかける可能性があります。
    • SMEMBERS の代わりに SSCAN を利用して、サーバーへの負荷を抑えながら要素を少しずつ取得することを検討しましょう。
    • 大規模なSetに対する集合演算(SUNION, SDIFF, SINTER)は、計算時間とネットワーク転送の両方でコストがかかります。結果をクライアントに返すのではなく、*STORE コマンドを使ってサーバー側に格納する方が効率的な場合があります。ただし、*STORE コマンドも計算自体はO(N)またはO(N*K)かかることに変わりはありません。
    • 非常に大規模なSetに対する集合演算を頻繁に行う必要がある場合は、RedisのSetが最適なデータ構造ではないかもしれません。設計を見直したり、他の技術(例: 外部のバッチ処理、分散処理フレームワークなど)との組み合わせを検討する必要があります。

3. Set操作の注意点(大きなSetに対する操作のコスト)

大規模なSetに対するO(N)操作(特に SMEMBERS や集合演算)は、Redisサーバーのイベントループをブロックし、他のクライアントからの要求に対する応答を遅延させる可能性があります。これはRedisがシングルスレッドで動く(データ型操作自体はシングルスレッドのイベントループで処理される)ためです。

対策としては、以下の点が考えられます。

  • SSCAN の利用: 大規模なSetの全要素を取得したい場合は、SMEMBERS ではなく SSCAN を使いましょう。一度に処理する要素数を制限することで、サーバーの負担を分散できます。
  • *STORE コマンドの利用: 集合演算の結果をクライアントに返すのではなく、サーバーに保存することでネットワーク転送のコストを削減できます。
  • 集合演算の実行タイミング: 大規模なSetに対する集合演算は、ピーク時を避けて実行する(例えば、夜間にバッチ処理として実行するなど)ことを検討しましょう。
  • Setの分割: 非常に大きなSetを扱うのではなく、複数の小さなSetに分割できないか設計を見直します。例えば、tag:redis:articles のSetが大きくなりすぎる場合は、日付やカテゴリなどでさらに分割するなどが考えられます。
  • 非同期/バックグラウンド処理: 複雑な集合演算が必要な場合は、アプリケーション側で非同期処理として実行したり、ワーカースレッドや別のプロセスにオフロードしたりすることを検討します。
  • Redis Modulesの利用: RedisGearsなどのRedis Modulesを使うことで、サーバーサイドで並列にデータ処理を実行し、メインスレッドをブロックする時間を減らすことができる場合があります。

4. 分散Redis環境でのSet(クラスタモードなど)

Redis Clusterでは、データは複数のノード(シャード)に分散されます。キーはハッシュスロットに基づいて各ノードに割り当てられます。

Setの操作に関して、Redis Clusterでは以下の点に注意が必要です。

  • 単一キー操作: SADD, SREM, SISMEMBER, SCARD, SPOP, SRANDMEMBER, SSCAN といった単一のキーを対象とする操作は、そのキーが配置されているノードで直接実行されるため、問題ありません。
  • 複数キー操作 (集合演算): SUNION, SDIFF, SINTER, SUNIONSTORE, SDIFFSTORE, SINTERSTORE といった複数のキーを対象とする集合演算は、対象となるすべてのキーが同じハッシュスロットに割り当てられている必要があります。異なるスロットに割り当てられているキーを含むコマンドを実行しようとすると、CROSSSLOT エラーが発生します。
  • CROSSSLOT回避のためのキー設計: 集合演算を行いたいSetのキーが常に同じスロットに配置されるようにするには、ハッシュタグを利用します。キー名の中に {...} という形式で特定の文字列を含めると、Redis Clusterはその文字列だけをハッシュスロット計算に使用します。
    例: SINTER {user123}:friends {user123}:followers
    この場合、{user123} の部分だけがハッシュスロット計算に使われるため、user123:friendsuser123:followers は同じスロットに配置されます。集合演算を行いたいSetのキーは、共通のハッシュタグを持つように設計することで、CROSSSLOTエラーを回避できます。

Redis Cluster環境でSetの集合演算を効率的に行うためには、キー設計が非常に重要になります。

Setと他のデータ型の組み合わせ

Setは単独でも有用ですが、Redisの他のデータ型と組み合わせることで、より複雑なデータ構造や機能を効率的に実装できます。

  • SetとHash:

    • ユーザーの属性情報をHash (user:user_id:profile) に格納し、ユーザーが付けたタグをSet (user:user_id:tags) に格納する。
    • 特定のタグを持つユーザーを探す(タグからユーザーIDのSetを取得)し、そのユーザーのプロフィール(Hash)を取得するといった連携が可能。
    • 商品情報をHash (product:product_id:info) に格納し、商品のタグをSet (product:product_id:tags) に格納する。特定のタグを持つ商品(SetからIDを取得)の詳細情報(Hash)を取得する。
  • SetとSorted Set:

    • ユーザーがフォローしている人のリストをSet (user:user_id:following) に格納し、そのユーザーのタイムラインフィードを、投稿時間のスコアを持つSorted Set (user:user_id:feed) で管理する。フォローしている人の新しい投稿(Sorted Set)を取得し、それを自分のフィードSorted Setにマージする(Sorted SetのZUNIONSTOREなど)。
    • ある商品に付けられたタグをSet (product:product_id:tags) に格納し、商品の人気度や販売数をSorted Set (product:sales_rank) で管理する。特定のタグが付いた商品の中から、人気ランキング上位のものを取得する、といった組み合わせが可能。

このように、Setの「重複なし・非順序」という特性と、他のデータ型の特性(Stringのシンプルさ、Listの順序性、Hashの構造化、Sorted Setの順序とユニーク性)を組み合わせることで、多様なアプリケーション要件に対応できます。

Setの設計パターンとベストプラクティス

Redis Setを効果的に活用し、安定したパフォーマンスを維持するための設計パターンとベストプラクティスをいくつか紹介します。

  • キー設計の考え方:

    • キー名にはSetが表現するエンティティ(ユーザー、記事、商品など)と、そのSetが保持する要素の性質(タグ、友達、購入者など)を明確に含めましょう。例: user:{user_id}:friends, article:{article_id}:tags, product:{product_id}:buyers
    • Redis Clusterを使用している場合は、集合演算を行いたいSetのキーに共通のハッシュタグ {...} を含めることを忘れないでください。例: {user_id}:friends, {user_id}:followers
  • Setサイズの管理:

    • Setは非常に多数の要素を格納できますが、あまりにも巨大なSet(数百万、数千万要素)は管理や特定の操作で問題を引き起こす可能性があります(メモリ使用量、O(N)操作の遅延)。
    • Setが肥大化する場合は、要素の性質や時間などでSetを分割できないか検討します。例: 日ごとのユニーク訪問者数を月ごとのSetにマージしたら元の日のSetは削除する、古いログデータを保持するSetをアーカイブするなど。
    • Setのサイズを定期的に監視し、異常に増加していないか確認します。
  • パフォーマンスモニタリング:

    • Redisサーバー全体のCPU使用率、メモリ使用量、ネットワーク帯域、コマンド実行レイテンシなどを監視します。
    • 特にSMEMBERSや集合演算といったO(N)操作の実行頻度や実行時間を監視し、ボトルネックになっていないか確認します。Redis Slowlog を活用すると、実行に時間のかかったコマンドを特定できます。
  • トランザクションとパイプライン:

    • Setに対する複数の操作をアトミックに実行したい場合は、Redis Transaction (MULTI/EXEC) を使用します。例えば、あるSetから要素を削除し、別のSetに追加する、といった操作をまとめて実行できます。
    • 複数のSet操作をまとめて実行することで、ネットワークラウンドトリップのオーバーヘッドを削減したい場合は、Redis Pipeline を使用します。例えば、複数のSetに同じ要素をSADDする場合など。
  • 要素のデータ型:

    • Setの要素はバイナリセーフな文字列です。通常はID(数値やUUID)や短い文字列を使用することが多いです。あまりに長い文字列や大きなバイナリデータを多数格納すると、メモリを圧迫する可能性があります。
    • 可能な場合は、よりコンパクトな表現形式を検討します。例えば、長大なURLではなく、URLのハッシュ値や短縮IDを格納するなど。
  • メモリ使用量の見積もり:

    • Setのメモリ使用量は、要素数と要素の平均サイズ、そして内部エンコーディングに依存します。hashtableエンコーディングの場合、各要素にはある程度のオーバーヘッド(ポインタなど)がかかります。intsetはよりコンパクトです。
    • Setの要素数を事前に見積もり、Redisインスタンスのメモリ容量を計画します。MEMORY USAGE key コマンドで特定のキーのメモリ使用量を確認できます。

トラブルシューティング

Redis Setを使用中に遭遇する可能性のある一般的な問題とその解決策について説明します。

  • Set操作が遅い:

    • 原因: Setが非常に大きい場合や、多数のキーに対する集合演算を実行している場合、O(N)やO(N*K)のコマンド(SMEMBERS, SUNION, SINTERなど)が遅延の原因となっている可能性があります。
    • 解決策:
      • Redis Slowlog を確認して、どのコマンドが遅いか特定します。
      • SMEMBERS の代わりに SSCAN を使用できないか検討します。
      • 集合演算の結果が必要な頻度を確認し、*STORE コマンドで結果をキャッシュする、または定期的にバックグラウンドで計算して保存することを検討します。
      • 対象のSetが大きくなりすぎていないか確認し、可能であればSetを分割する設計に変更します。
      • Redisサーバーのリソース(CPU、メモリ、ネットワーク)が不足していないか確認します。
  • メモリ使用量の増加:

    • 原因: Setに多数の要素を追加し続けている、または*STOREコマンドで大量の集合演算結果を保存している可能性があります。
    • 解決策:
      • MEMORY USAGE key コマンドや INFO MEMORY コマンドでSetのメモリ使用量を確認します。
      • 不要になったSetキーを削除します(DEL key)。特に*STOREコマンドで一時的に生成した結果Setを使い終わったら削除することを忘れないでください。
      • Setの要素数が無制限に増加しないよう、アプリケーション側で要素の有効期限を管理したり、古い要素を削除したりする仕組みを導入します。
      • Setの要素が冗長でないか、よりコンパクトな表現形式に変更できないか検討します。
  • Redis ClusterでのCROSSSLOTエラー:

    • 原因: 集合演算コマンド (SUNION, SINTER, SDIFF など) の対象となる複数のキーが、異なるハッシュスロットに配置されているためです。
    • 解決策: 集合演算を行いたいキーには、同じハッシュタグ {...} を含むようにキー名を設計します。これにより、Redis Clusterはそれらのキーを同じスロットに配置しようとします。キー設計の変更が必要な場合があります。

まとめ

本記事では、RedisのSetデータ型について、その基本的な概念から、SADD, SMEMBERS, SISMEMBER, SREM, SCARD, SRANDMEMBER, SPOPといった基本操作、さらにはSUNION, SDIFF, SINTERといった強力な集合演算までを詳しく解説しました。

Setは「重複しない要素の非順序コレクション」というシンプルな特性を持ちながら、要素の存在確認が平均O(1)と非常に高速であること、そして集合演算によって複数のSet間の関係性を効率的に分析できることから、多くのアプリケーションシナリオで役立ちます。

  • ユニークユーザーのカウント
  • タグ機能やカテゴリ機能
  • 友達リストやフォロー/フォロワーリスト
  • レコメンデーションの基礎分析
  • リアルタイムなデータフィルタリングや集計

といった機能は、Setを適切に活用することでシンプルかつ高性能に実装できます。

また、Setの内部構造(intset, hashtable)、パフォーマンス特性、大規模Setに対する操作の注意点、Redis Cluster環境での集合演算におけるCROSSSLوتエラーとその回避策(ハッシュタグ)、そしてSetと他のデータ型との組み合わせについても掘り下げて説明しました。

Redis Setを最大限に活かすためには、利用するSetのサイズ、操作の頻度、必要な操作(単一要素操作か集合演算か)を考慮し、適切なキー設計と運用を行うことが重要です。特に大規模なSetを扱う場合は、O(N)系の操作がパフォーマンスボトルネックにならないよう注意し、SSCAN*STORE コマンドの活用、Setの分割、オフラインでのバッチ処理なども視野に入れる必要があります。

この記事が、あなたのRedis Setに対する理解を深め、アプリケーション開発におけるデータ構造の選択肢を広げる一助となれば幸いです。Setの強力な機能をぜひあなたのプロジェクトで活用してみてください。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール