初心で挑むredis入門 ~Redis lists編~
今回はredisで使えるlistsについてみていきます。先日公開したHashesについてもぜひご覧ください。
早速検証
redisの環境構築については先日公開した以下の記事を参考にしてください。
Redis listsのドキュメントは以下になります。
スタックとキューについて
listsを使うとスタックとキューを両方実装できます。改めてスタックとキューについておさらいしておきましょう。
- スタック
- LIFO (Last In First Out)方式のリストであり、最後に入れたデータから順番に利用される
- push:データをリストの末尾に追加する操作
- pop:リストの末尾のデータを削除する操作
- キュー
- FIFO (First In First Out)方式のリストであり、最初に入れたデータから順番に利用される
- push:データをリストの末尾に追加する操作(本来のキューの操作ではこの処理をenqueueと呼びます)
- pop:リストの先頭のデータを削除する操作(本来のキューの操作ではこの処理をdequeueと呼びます)
ようは、入れた順番に沿ってデータを利用するのがキュー、逆順に利用するのがスタックです。
まずは利用可能機能を列挙します!
今回は先に利用できる機能をまとめて最初に列挙しちゃいます!
- データ挿入系
- LPUSH: リストの先頭にデータを挿入
- RPUSH: リストの末尾にデータを挿入
- データ取り出し系
- LPOP: リストの先頭のデータを削除して取り出し
- RPOP: リストの末尾のデータを削除して取り出し
- BLPOP: リストの先頭のデータを削除して取り出しするが、リストがからの場合はデータが挿入されるまでブロッキングする
- そのほか
- LLEN: リストの長さを取得
- LMOVE: 要素をリストAからリストBに移動させる
- BLMOVE: 要素をリストAからリストBに移動させるが、リストAがからの場合はデータが挿入されるまでブロッキングする
- LRANGE: リストの範囲を指定して要素を抽出
- LTRIM: リストの範囲を指定して削除
スタックの実装
まずはスタックを実装してみます。スタックはLIFOのため、最後に挿入されたデータから順番に取得できます。
import redis
r = redis.Redis(host="localhost", port=6379, db=0)
r.rpush("lists:stack", "value1")
print(f"{r.llen('lists:stack')=}")
for i in range(2, 6):
r.rpush("lists:stack", f"value{i}")
print(f"{r.llen('lists:stack')=}")
for _ in range(r.llen("lists:stack")):
fetched_value = r.rpop("lists:stack")
remain_items = r.llen("lists:stack")
print(f"{fetched_value=}, {remain_items=}")
上記コードではまずlists:stackというリストを作成してvalue1をリスト末尾に追加しています。その後value2からvalue5をリスト末尾に追加しています。その後、リストの長さを取得して最後に保存されたものから取得できることを試しています。
こちらのコードを実行すると以下のような結果になります。
uv run data_lists_stack.py
# 結果
r.llen('lists:stack')=1
r.llen('lists:stack')=5
fetched_value=b'value5', remain_items=4
fetched_value=b'value4', remain_items=3
fetched_value=b'value3', remain_items=2
fetched_value=b'value2', remain_items=1
fetched_value=b'value1', remain_items=0
結果を見ても、rpushでデータ末尾にデータを挿入し、rpopで逆順でデータを取得できることが確認できました。
キューの実装
次にFIFOであるキューを実装してみましょう。キューはFIFOのため、最初に挿入されたデータから順番に取得できます。
import redis
r = redis.Redis(host="localhost", port=6379, db=0)
r.rpush("lists:queue", "value1")
print(f"{r.llen('lists:queue')=}")
for i in range(2, 6):
r.rpush("lists:queue", f"value{i}")
print(f"{r.llen('lists:queue')=}")
for _ in range(r.llen("lists:queue")):
fetched_value = r.lpop("lists:queue")
remain_items = r.llen("lists:queue")
print(f"{fetched_value=}, {remain_items=}")
上記コードではまずlists:queueというリストを作成してvalue1をリスト末尾に追加しています。その後value2からvalue5をリスト末尾に追加しています。その後、リストの長さを取得して最初に保存されたものから取得できることを試しています。先ほどと変わっているところとして変わっているところとしては、デートを取得する関数がrpopではなくlpopになっていることです。lpopにすることで、リストの先頭からデータを取得できます。
それではこのコードを実行してみましょう。
uv run data_lists_queue.py
# 結果
r.llen('lists:queue')=1
r.llen('lists:queue')=5
fetched_value=b'value1', remain_items=4
fetched_value=b'value2', remain_items=3
fetched_value=b'value3', remain_items=2
fetched_value=b'value4', remain_items=1
fetched_value=b'value5', remain_items=0
結果を見ると、最初に設定された値から順番に取得できていることが確認でき、キューの実装になっていることが確認できました。
リストからリストへの値の移動
lmoveを利用すると、リストAからリストBに値を移動させることができます。
以下の例では、リストAの先頭のデータをリストBに移動するコードです。
import redis
r = redis.Redis(host="localhost", port=6379, db=0)
def show_length():
print(f"{r.llen('lists:listA')=}")
print(f"{r.llen('lists:listB')=}")
def initialize():
for i in range(6):
r.rpush("lists:listA", f"listA_value{i}")
r.rpush("lists:listB", f"listB_value{i}")
def move(_from, to):
r.lmove("lists:listA", "lists:listB", _from, to)
def show_result():
print("\n===== Lists:listA's values =====")
for _ in range(r.llen("lists:listA")):
fetched_value = r.lpop("lists:listA")
print(f"{fetched_value}")
print("\n===== Lists:listB's values =====")
for _ in range(r.llen("lists:listB")):
fetched_value = r.lpop("lists:listB")
print(f"{fetched_value}")
print("LEFT -> LEFT")
initialize()
show_length()
move("LEFT", "LEFT")
show_length()
show_result()
print("\nLEFT -> RIGHT")
initialize()
show_length()
move("LEFT", "RIGHT")
show_length()
show_result()
print("\nRIGHT -> LEFT")
initialize()
show_length()
move("RIGHT", "LEFT")
show_length()
show_result()
print("\nRIGHT -> RIGHT")
initialize()
show_length()
move("RIGHT", "RIGHT")
show_length()
show_result()
実装されている関数はそれぞ隷下の用途になっています。
-
show_length: リストの長さを表示する -
initialize: リストを初期化する -
move: リストAからリストBに値を移動する。_fromにはリストAのLFETかRIGHTか、toにはリストBのLEFTかRIGHTかを指定する -
show_result: リストの値を表示する
これを実行した結果以下になります(結果が多くて折りたたんでいます)。_fromにLEFTを指定するとリストAの先頭から、_fromにRIGHTを指定するとリストAの末尾から値が取られていることが確認できました。また、toにLEFTを指定するとリストBの先頭に値が追加され、toにRIGHTを指定するとリストBの末尾に値が追加されることを確認できました。
実行結果
uv run data_lists_lmove.py
# 結果
LEFT -> LEFT
r.llen('lists:listA')=6
r.llen('lists:listB')=6
r.llen('lists:listA')=5
r.llen('lists:listB')=7
===== Lists:listA's values =====
b'listA_value1'
b'listA_value2'
b'listA_value3'
b'listA_value4'
b'listA_value5'
===== Lists:listB's values =====
b'listA_value0'
b'listB_value0'
b'listB_value1'
b'listB_value2'
b'listB_value3'
b'listB_value4'
b'listB_value5'
LEFT -> RIGHT
r.llen('lists:listA')=6
r.llen('lists:listB')=6
r.llen('lists:listA')=5
r.llen('lists:listB')=7
===== Lists:listA's values =====
b'listA_value1'
b'listA_value2'
b'listA_value3'
b'listA_value4'
b'listA_value5'
===== Lists:listB's values =====
b'listB_value0'
b'listB_value1'
b'listB_value2'
b'listB_value3'
b'listB_value4'
b'listB_value5'
b'listA_value0'
RIGHT -> LEFT
r.llen('lists:listA')=6
r.llen('lists:listB')=6
r.llen('lists:listA')=5
r.llen('lists:listB')=7
===== Lists:listA's values =====
b'listA_value0'
b'listA_value1'
b'listA_value2'
b'listA_value3'
b'listA_value4'
===== Lists:listB's values =====
b'listA_value5'
b'listB_value0'
b'listB_value1'
b'listB_value2'
b'listB_value3'
b'listB_value4'
b'listB_value5'
RIGHT -> RIGHT
r.llen('lists:listA')=6
r.llen('lists:listB')=6
r.llen('lists:listA')=5
r.llen('lists:listB')=7
===== Lists:listA's values =====
b'listA_value0'
b'listA_value1'
b'listA_value2'
b'listA_value3'
b'listA_value4'
===== Lists:listB's values =====
b'listB_value0'
b'listB_value1'
b'listB_value2'
b'listB_value3'
b'listB_value4'
b'listB_value5'
b'listA_value5'
指定した範囲の値の参照
lrangeを利用すると、インデックスを指定して値を参照することができます。
import redis
r = redis.Redis(host="localhost", port=6379, db=0)
for i in range(6):
r.rpush("lists:range", f"value{i}")
print(f"{r.lrange('lists:range', 0, 3)}")
print(f"{r.lrange('lists:range', 0, -1)}")
上記コードでは、6つのデータをlists:rangeに登録し、lrangeでインデックスを指定してその範囲の値を取得します。なお、最初と最後のインデックスを指定しますが、両端のインデックスの値は含まれます。また、最後のインデックスに-1を指定するとリストの末尾まで参照することを指定できます。
それでは早速実行してみます。
uv run data_lists_lrange.py
# 結果
[b'value0', b'value1', b'value2', b'value3']
[b'value0', b'value1', b'value2', b'value3', b'value4', b'value5']
結果を見ると、0~3で指定した時はvalue0からvalue3まで取得できており、0~-1で指定した時はリストの先頭から末尾まで取得できていることが確認できました。
注意点
ドキュメントの解説でいくつか注意点が述べられています!
lists以外のデータに対してオペレーションできない
例えばsetでデータを設定してからlpushでリストの先頭にデータを追加しようとすると、エラーになるとのことです。
>>> res37 = r.set("new_bikes", "bike:1")
>>> print(res37)
True
>>> r.type("new_bikes")
'string'
>>> try:
... r.lpush("new_bikes", "bike:2", "bike:3")
... except redis.exceptions.ResponseError as e:
... print(e)
redis.exceptions.ResponseError:
WRONGTYPE Operation against a key holding the wrong kind of value
全てのデータを取り出すとリストは存在しなくなる
リストに登録された全ての値をpopした場合、そのリストは存在しない扱いになるようです。
>>> r.lpush("bikes:repairs", "bike:1", "bike:2", "bike:3")
>>> r.llen("bikes:repairs")
3
>>> r.exists("bikes:repairs")
True
>>> r.lpop("bikes:repairs")
b'bike:1'
>>> r.lpop("bikes:repairs")
b'bike:2'
>>> r.lpop("bikes:repairs")
b'bike:3'
>>> r.exists("bikes:repairs")
False
格納可能な要素数
listsの最大の長さは2^32 - 1(4,294,967,295)とのことです。
まとめ
今回はRedis listsの機能を紹介しました。スタックやキューなどの実装もでき、データシリーズの保存もできるので、様々な用途で活用できます。使い方自体もとても直感できに利用できますので、ぜひご利用ください。次回はsetについて紹介します!
Discussion