Redis: Redis Lists(パート2)
このレッスンでは、要素のクエリ、変更、挿入を含む高度なリスト操作について解説します。
1. LINDEX:インデックスによる要素の取得
LINDEXはインデックスで要素を取得します。削除は行いません。
(1) 基本的な使用方法
REDIS
LPUSH mylist "one" "two" "three" "four" "five"
# 最初の要素を取得(インデックス0)
LINDEX mylist 0
"five"
# 2番目の要素を取得
LINDEX mylist 1
"four"
# 最後の要素を取得
LINDEX mylist -1
"one"
# 最後から2番目の要素を取得
LINDEX mylist -2
"two"
(2) インデックス範囲外
REDIS
# インデックス範囲外の場合はnilを返す
LINDEX mylist 10
(nil)
LINDEX mylist -10
(nil)
💡 パフォーマンス: LINDEXの時間複雑度はO(N)で、Nは絶対インデックス値です。先頭と末尾へのアクセスは高速ですが、中央へのアクセスは低速です。
2. LSET:インデックスによる要素の設定
LSETは指定されたインデックスの要素を変更します。
(1) 基本的な使用方法
REDIS
LRANGE mylist 0 -1
1) "five"
2) "four"
3) "three"
4) "two"
5) "one"
# 最初の要素を変更
LSET mylist 0 "FIRST"
OK
# 最後の要素を変更
LSET mylist -1 "LAST"
OK
LRANGE mylist 0 -1
1) "FIRST"
2) "four"
3) "three"
4) "two"
5) "LAST"
(2) インデックス範囲外
REDIS
LSET mylist 10 "value"
(error) ERR index out of range
⚠️ 補足: LSETは既存のインデックスのみ変更できます。新しい要素を挿入することはできません。
3. LINSERT:ピボットの前または後に挿入
LINSERTは指定されたピボット要素の前または後に新しい要素を挿入します。
(1) LINSERT BEFORE:前に挿入
REDIS
LRANGE mylist 0 -1
1) "FIRST"
2) "four"
3) "three"
4) "two"
5) "LAST"
# "three"の前に"BEFORE_THREE"を挿入
LINSERT mylist BEFORE "three" "BEFORE_THREE"
(integer) 6 # リストの長さを返す
LRANGE mylist 0 -1
1) "FIRST"
2) "four"
3) "BEFORE_THREE"
4) "three"
5) "two"
6) "LAST"
(2) LINSERT AFTER:後に挿入
REDIS
# "three"の後に"AFTER_THREE"を挿入
LINSERT mylist AFTER "three" "AFTER_THREE"
(integer) 7
LRANGE mylist 0 -1
1) "FIRST"
2) "four"
3) "BEFORE_THREE"
4) "three"
5) "AFTER_THREE"
6) "two"
7) "LAST"
(3) ピボットが見つからない場合
REDIS
# 指定されたピボット要素が存在しない
LINSERT mylist BEFORE "notexist" "value"
(integer) -1 # ピボットが見つからない場合は-1を返す
⚠️ パフォーマンス警告: LINSERTの時間複雑度はO(N)で、ピボットを見つけるためにリストを走査する必要があります。非常に長いリストでは低速です。
4. LREM:値による要素の削除
LREMはリストから指定された値に一致する要素を削除します。
(1) 基本的な使用方法
REDIS
LPUSH mylist "a" "b" "a" "c" "a" "d"
LRANGE mylist 0 -1
1) "d"
2) "a"
3) "c"
4) "a"
5) "b"
6) "a"
# 先頭から"a"を2つ削除
LREM mylist 2 "a"
(integer) 2 # 2つ削除
LRANGE mylist 0 -1
1) "d"
2) "c"
3) "b"
4) "a"
(2) パラメータリファレンス
| パラメータ | 説明 |
|---|---|
| count > 0 | 先頭からcount個削除 |
| count < 0 | 末尾からabs(count)個削除 |
| count = 0 | 一致するすべての要素を削除 |
(3) 末尾から削除
REDIS
LPUSH mylist "a" "b" "a" "c" "a"
# 末尾から"a"を1つ削除
LREM mylist -1 "a"
(integer) 1
(4) すべての一致する要素を削除
REDIS
LPUSH mylist "a" "b" "a" "c" "a"
# すべての"a"を削除
LREM mylist 0 "a"
(integer) 3
LRANGE mylist 0 -1
1) "c"
2) "b"
5. RPOPLPUSH:要素の移動
RPOPLPUSHは、あるリストの末尾から要素をポップし、別のリストの先頭にプッシュします。
(1) 基本的な使用方法
REDIS
# ソースリスト
LPUSH source "one" "two" "three"
# 宛先リスト
LPUSH destination "a" "b"
# ソースの末尾を宛先の先頭に移動
RPOPLPUSH source destination
"one" # 移動された要素を返す
LRANGE source 0 -1
1) "three"
2) "two"
LRANGE destination 0 -1
1) "one"
2) "a"
3) "b"
(2) 使用例:循環キュー
REDIS
# タスクを末尾から先頭に移動してラウンドロビン処理
RPOPLPUSH queue:task queue:task
(3) 使用例:バックアップキュー
REDIS
# 処理中にタスクをバックアップキューに移動
RPOPLPUSH queue:task queue:backup
# 処理が失敗した場合、タスクはバックアップキューから復元可能
6. BRPOPLPUSH:ブロッキング移動
BRPOPLPUSHはRPOPLPUSHのブロッキングバージョンです。
REDIS
# ソースリストが空の場合、最大10秒ブロック待機
BRPOPLPUSH source destination 10
💡 使用例: BRPOPLPUSHは信頼性の高いメッセージキューによく使用され、メッセージの損失を防ぎます。
7. LMOVE:要素の移動(Redis 6.2+)
LMOVEはRPOPLPUSHの一般化バージョンで、ソースと宛先の両方の位置を指定できます。
REDIS
# ソースの右からポップし、宛先の左にプッシュ
LMOVE source destination RIGHT LEFT
# ソースの左からポップし、宛先の右にプッシュ
LMOVE source destination LEFT RIGHT
8. 高度なListアプリケーション
(1) ユースケース1:ページネーション
REDIS
# 記事リスト
LPUSH articles "article:1" "article:2" ... "article:100"
# 1ページ目(1ページ10件)
LRANGE articles 0 9
# 2ページ目
LRANGE articles 10 19
# 3ページ目
LRANGE articles 20 29
(2) ユースケース2:最近の連絡先
REDIS
# 連絡先を最近のリストに追加
LPUSH contacts:user:1 "user:2"
# 既に存在する場合、一度削除してから追加(先頭に移動)
LREM contacts:user:1 0 "user:2"
LPUSH contacts:user:1 "user:2"
# 最近の10件の連絡先を取得
LRANGE contacts:user:1 0 9
(3) ユースケース3:ラウンドロビンスケジューリング
REDIS
# タスクリスト
LPUSH tasks "task1" "task2" "task3"
# ラウンドロビン:タスクをポップ、処理、末尾に戻す
RPOPLPUSH tasks tasks
"task1" # task1を処理
# 次はtask2が取得される
(4) ユースケース4:上限付きログ
REDIS
# ログエントリを追加
LPUSH log:app "log message"
# 最新の1000件のログエントリを保持
LTRIM log:app 0 999
# 最近のログを表示
LRANGE log:app 0 99
9. Listのパフォーマンス最適化
(1) 中央操作を避ける
REDIS
# ❌ パフォーマンスが悪い:中央での挿入や削除
LINSERT mylist BEFORE "middle" "new"
LREM mylist 1 "middle"
# ✅ パフォーマンスが良い:先頭と末尾のみで操作
LPUSH mylist "new"
LPOP mylist
(2) 大きなリストを避ける
REDIS
# ❌ リストが大きすぎる
LLEN mylist
(integer) 1000000
# ✅ LTRIMで長さを制限
LTRIM mylist 0 9999
(3) LRANGEでバッチ取得
REDIS
# ❌ 一度に広い範囲を取得
LRANGE mylist 0 -1
# ✅ バッチで取得
LRANGE mylist 0 99
LRANGE mylist 100 199
(4) 適切なデータ構造の選択
| 要件 | 推奨データ構造 |
|---|---|
| キュー/スタック | List |
| 最新N件 | List + LTRIM |
| ページネーション | List + LRANGE |
| 重複排除リスト | Set または Sorted Set |
| 重み付きソート | Sorted Set |
10. Listの制限
(1) 要素の検索が遅い
REDIS
# 特定の値を見つけるにはリスト全体を走査する必要がある
LINDEX mylist 500000 # O(N)操作、非常に遅い
(2) 中央操作が遅い
REDIS
# 中央での挿入や削除には要素のシフトが必要
LINSERT mylist BEFORE "middle" "new" # O(N)
(3) 値からインデックスを見つけられない
Redisは、値の内容からそのインデックスを見つけるコマンドを提供していません。これはアプリケーションレベルで実装する必要があります。
▶ サンプル
リストの高度な操作:
BASH
127.0.0.1:6379> RPUSH queue "job1" "job2" "job3"
(integer) 3
127.0.0.1:6379> LLEN queue
(integer) 3
127.0.0.1:6379> LINSERT queue BEFORE "job2" "job1.5"
(integer) 4
127.0.0.1:6379> LRANGE queue 0 -1
1) "job1"
2) "job1.5"
3) "job2"
4) "job3"
❓ よくある質問
Q LINDEXとLRANGEの違いは何ですか?
A LINDEXは単一の要素を返し、LRANGEは要素のリストを返します。
LINDEX mylist 0 は LRANGE mylist 0 0 と同等です。Q リストの途中に要素を挿入するにはどうすればよいですか?
A LINSERTを使用しますが、パフォーマンスは良くありません。頻繁に中央挿入が必要な場合は、別のデータ構造を検討してください。
Q RPOPLPUSHはアトミックですか?
A はい。RPOPLPUSHはアトミックです。完全に成功するか、完全に失敗します。
Q 優先度キューを実装するにはどうすればよいですか?
A 複数のリスト(例:queue:high、queue:low)を使用します。最初に高優先度キューをチェックし、次に低優先度キューをチェックします。
Q ListとSetの違いは何ですか?
A Listは順序付けられ、重複を許可します。Setは順序なしで要素は一意です。順序が必要な場合はList、重複排除が必要な場合はSetを使用します。
📖 まとめ
- LINDEXでインデックスによる要素の取得、LSETでインデックスによる要素の設定
- LINSERTでピボットの前後に挿入(O(N)のパフォーマンス)
- LREMで値による要素の削除、カウントと方向を制御
- RPOPLPUSHで要素の移動、循環キューやバックアップキューに使用
- BRPOPLPUSHでブロッキング移動、信頼性の高いメッセージキューに使用
- 高度なListアプリケーション:ページネーション、最近の連絡先、ラウンドロビンスケジューリング
- パフォーマンス最適化:中央操作を避け、リストの長さを制限し、バッチ取得
📝 練習問題
- インデックス操作: リストを作成し、LINDEXで異なる位置の要素を取得し、LSETで要素を変更しましょう
- 挿入と削除: LINSERTで中央に挿入し、LREMで値による要素の削除を練習しましょう
- 要素の移動: RPOPLPUSHを使用して循環キューを実装し、タスクのラウンドロビンを実現しましょう
- ページネーション: 20個の要素を持つリストを作成し、5件ずつのページネーションを実装しましょう
11. 次のレッスン
次のレッスンでは、Redis Sets(パート1)について学びます。基本的なセット操作を解説します。