# Database: Redis 編
このハンズオンの狙い
- Docker に慣れる
- Redis を実際に使ってみる
- よく使いたくなる処理(部品)を作ってみる
# 事前準備
- Docker を使用できる
- install は主題ではないので、Docker image を使用します
- 何らかの LL を使える
- 例は Python で書きますが、Ruby, Perl, JavaScript, Go なども使えます。適当に読み替えてください
# 事前準備手順
Redis と Python のイメージを使えるようにする
docker pull redis:6.2.3-alpine3.13 docker pull python:3.9.5-slim-buster docker images REPOSITORY TAG IMAGE ID CREATED SIZE python 3.9.5-slim-buster afaa64e7c7fe 15 hours ago 115MB redis 6.2.3-alpine3.13 efb4fa30f1cf 9 days ago 32.3MB
1
2
3
4
5
6
7サーバを起動する
docker run --rm --name test-server redis:6.2.3-alpine3.13
1test-server
という名前で redis サーバーを 起動しますReady to accept connections
と出ればOK このターミナルは開いたままにします- もし、ターミナルが閉じたり、Ctrl-C で終了してしまったら、再度 起動してください
別のターミナルを開いて、redis-cli でサーバに接続してみる
- redis-cli を起動します。 これで対話的に コマンドが打てます。
- 下の二通りの使い方があります。どちらも
test-server
として先程起動したredis の中を見れます
# ネットワーク越し 別コンテナ docker run -it --link test-server:redis --rm redis:6.2.3-alpine3.13 sh -c 'exec redis-cli -h "$REDIS_PORT_6379_TCP_ADDR" -p "$REDIS_PORT_6379_TCP_PORT"' # 直接 乗り込んで起動 docker exec -it test-server redis-cli
1
2
3
4- redis のping コマンドで PONG と帰ってくれば 正常に接続できています。
172.17.0.2:6379> ping PONG 172.17.0.2:6379> exit
1
2
3- Ctrl-C もしくは exit コマンドで抜けます。
- Windows 環境は Git Bash の MINGW64 環境で動作確認しました
- winpty docker -it 〜 としたらいけました
終了
- ここまで完了できたら 事前準備完了です。 無事 これらを行うことができました。
- docker image の 取得
- redis server の起動
- redis-cli による Redis の操作
- ここまで完了できたら 事前準備完了です。 無事 これらを行うことができました。
# あらすじ
- Redis ざっくり説明
- Redis を直接触ってみよう
- 応用課題: プログラムで DB 登録しよう
# 資料
# Redis とは何か
KVS(Key-Value-Store) の一種
- データを Key と Value で 管理するもの
- 構造が単純故に、気軽に使えたり、分散しやすいなど RDBMS とは違った特性を持つデータストア
RDBMS vs KVS
- よく言われるWebサービスの最小構成として LAMP (Linux, Apache, MySQL, PHP/Perl/Python) と言う言葉がある
- データを扱うのに MySQL, PostgreSQL のような データを 表で管理する RDBMS を使うのはよくあること。
- 帳簿のように 決められた形式で決められた属性のデータをきれいに整理するのには向いていた
- でも、 RDBMS では 困る場合もある。
- 多様: 例えばセンサーの情報 センサーが100種類あれば100種類のテーブルを作るのか? 1万なら? 現実的ではない
- 大量: 巨大な表同士を連携しながら1つのデータにするRDBMS では 1台のマシンで扱える規模に限界がある。
- 高頻度: 毎秒凄まじい数のリクエストを高速に処理しないといけない時、RDBMS の負荷分散では仕組み上の限界がある
- そこで RDBMS とは別の仕組みでデータを処理できるデータベースが 最近 利用されてきた
- KVSやドキュメントデータベース、JSONのママ登録できるRDBMSや、グラフデータベースなど
Redis の特徴
- オンメモリで動作し、速い
- 設定で永続化やクラスタリングもできる
- 便利な機能やデータ型がある
- 単純な KVS に加えて Redis では いくつものデータ型がサポートされている
- String: 512MB までの テキストやバイナリ
- Hashes: フィールドと値のリスト
- Lists: 順序を保持してくれる文字列の集合 (4番目に入れたやつ などが取れる)
- Sets: 順序なしの文字列の集合で、他のSets 同士で 足したり引いたり 共通する要素を抜き出したりなどの 演算ができる
- Sorted sets: ソート済みのSets. 値を入れるときに並べ替えながら入れてくれる、 また、 決められた範囲で何件あるか、 3~5位は誰か などの演算ができる (bitmap, HLL, pubsub, streaming)
- expire(データの有効期限): 決められた時間が立ったあとデータを消すという処理をする必要がない
- トランザクション: Luaスクリプトなどを用意することで 複数の操作を一連の操作として行えることができる
- Pub/Sub のメッセージング: 多対多の疎結合なやりとりをサポートしている。 これにより チャットや 配信の仕組みなどを 簡単に作れる
- 地理データ用のコマンド: 二点間の距離とか 10km四方にある ラーメン屋の検索 とかができる
- 単純な KVS に加えて Redis では いくつものデータ型がサポートされている
- シンプルに出来が良い
- 機能がコンパクトにまとまっており、信頼性が高く動かしていて気持ちがいい(主観)
- オンメモリで動作し、速い
世間でのRedis の 使用例
- Web server のセッション保持
- 高速にアクセスしたいメタデータの保持
- コンテンツ/データのキャッシュ
- データの一時キュー
- sorted set 型によるランキング
- set 型によるタグ付けと集合演算
# Redis を直接触ってみよう
Redis コマンド一覧 (opens new window) も見ながら色々触ってみましょう。
# 一番単純なkey-value
set (opens new window)/get (opens new window)
set
で保存してget
で取り出します。
172.17.0.2:6379> set key1 yamagarashi
OK
172.17.0.2:6379> set key2 kaizoku
OK
172.17.0.2:6379> get key1
"yamagarashi"
172.17.0.2:6379> get key2
"kaizoku"
172.17.0.2:6379> get key3
(nil)
2
3
4
5
6
7
8
9
10
これがredisで最も基本的なkeyとvalueの扱いです。set
やget
を「コマンド」と呼び、その後ろは引数です。
keyを色々操作してみましょう。
keys (opens new window) はkeyの検索ができます。keys *
とすると存在するすべてのキーが出力されます。
ちなみにredisはシングルスレッドで動作するソフトウェアで、keys *
で大量のkeyを表示すると一定時間処理が止まってしまい、本番サーバに重大な影響を与えてしまうので注意しましょう。
172.17.0.2:6379> keys key*
1) "key1"
2) "key2"
2
3
他にも色々
- exists (opens new window): もうセットされてるのか とかがわかる
- del (opens new window): key を指定してそれを削除する
- rename (opens new window): key の 名前を変えられる
172.17.0.2:6379> exists key2
(integer) 1 # 存在しているので1(true)
172.17.0.2:6379> exists key3
(integer) 0 # 存在しないので0(false)
172.17.0.2:6379> del key2
(integer) 1
172.17.0.2:6379> keys *
1) "key1"
172.17.0.2:6379> rename key1 key5
OK
172.17.0.2:6379> get key5
"yamagarashi"
2
3
4
5
6
7
8
9
10
11
12
キーには有効期限を設定し、一定時間後に自動削除することができます。 キャッシュなどの用途において、アプリケーション側でキャッシュ削除をしなくても勝手に削除されるのが非常に便利です。
172.17.0.2:6379> set key7 kiemasu
OK
172.17.0.2:6379> expire key7 20 # 20秒後に消す
(integer) 1
172.17.0.2:6379> ttl key7 # ttlは残り秒数を表示する
(integer) 17
172.17.0.2:6379> ttl key7
(integer) 14
172.17.0.2:6379> get key7 # まだ見えてる
"kiemasu"
172.17.0.2:6379> ttl key7
(integer) 8
172.17.0.2:6379> ttl key7
(integer) 3
172.17.0.2:6379> ttl key7
(integer) 1
172.17.0.2:6379> ttl key7
(integer) -2
172.17.0.2:6379> get key7 # 消えた
(nil)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 数値の扱い
redisはよくランキング情報を一時的にキャッシュしたり数値を扱うことが多いため、そのためのコマンドが用意されています。
- incr (opens new window): 1足す
- decr (opens new window): 1引く
- incrby (opens new window): n足す
- decrby (opens new window): n引く
172.17.0.2:6379> set boss_damage 0
OK
172.17.0.2:6379> incr boss_damage
(integer) 1
172.17.0.2:6379> incr boss_damage
(integer) 2
172.17.0.2:6379> get boss_damage
"2"
172.17.0.2:6379> incr boss_damage
(integer) 3
172.17.0.2:6379> get boss_damage
"3"
172.17.0.2:6379> decrby boss_damage 2
(integer) 1
172.17.0.2:6379> get boss_damage
"1"
172.17.0.2:6379> incrby boss_damage 10
(integer) 11
172.17.0.2:6379> get boss_damage
"11"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 色々なデータ型(Lists)
上で例に出したのは全てstring(文字列)型の単純なデータです。redisにはそれ以外にも面白いデータ型を用意しています。
最初に紹介するのは Lists (opens new window) 型です。
172.17.0.2:6379> rpush enemy_list slime
(integer) 1
172.17.0.2:6379> rpush enemy_list drakee
(integer) 2
172.17.0.2:6379> lrange enemy_list 0 -1
1) "slime"
2) "drakee"
172.17.0.2:6379> lpush enemy_list magician
(integer) 3
172.17.0.2:6379> lrange enemy_list 0 -1
1) "magician"
2) "slime"
3) "drakee"
2
3
4
5
6
7
8
9
10
11
12
13
rpush
はリストの末尾に要素を追加するコマンド、lpush
は逆に先頭に追加するコマンドです。
lrange
の引数がどういう意味なのか、色々試してみてください。
# 色々なデータ型(Sets)
Sets (opens new window) はunique性が保証されたリストです。 Setsは順序を考慮しませんが、勝手にソートしてくれる Sorted sets (opens new window) も存在します。
172.17.0.2:6379> sadd favorites site1
(integer) 1
172.17.0.2:6379> sadd favorites siteABC
(integer) 1
172.17.0.2:6379> sadd favorites site3
(integer) 1
172.17.0.2:6379> sadd favorites siteABC
(integer) 0
172.17.0.2:6379> smembers favorites
1) "site1"
2) "siteABC"
3) "site3"
2
3
4
5
6
7
8
9
10
11
12
# 色々なデータ型(Hashes)
Hashes (opens new window) はプログラミング言語で言うところのMap的な、あるいはオブジェクトを表現できるデータです。
172.17.0.2:6379> hset slime attack 5 deffence 3 hp 3 exp 1
(integer) 4
172.17.0.2:6379> hget slime hp
"3"
172.17.0.2:6379> hmget slime deffence hp
1) "3"
2) "3"
172.17.0.2:6379> hmget slime attack deffence
1) "5"
2) "3"
172.17.0.2:6379> hgetall slime
1) "attack"
2) "5"
3) "deffence"
4) "3"
5) "hp"
6) "3"
7) "exp"
8) "1"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
例のようにオブジェクトを表現してもいいですし、key-value的な使い方もできます。
# Pub/sub
redisはkey-valueストアとしてだけではなく、シンプルなpub/subとしても利用可能です。 pub/subとは特定のチャンネルをsubscribe(購読)しているプログラムに対して、そのチャンネルにpublish(出版)することで多数のsubscriber(読者)に情報を届けることができるモデルです。 publisherとsubscriberそれぞれのプログラムがお互いを知らなくても通信ができるため素結合に維持ができ、動的に通信相手が変わるようなユースケースに利用されます。
pub/subモデルを利用したアーキテクチャは「イベント駆動型アーキテクチャ」とも呼ばれ、Apache kafka (opens new window) などが有名です。kafkaの場合はsubscriberが誰もいなかった時にデータを貯めておいて、確実に届けてくれたりもするのですが、redisのpub/subはリアルタイムにsubscribeしているプログラムにのみデータが送信され、届かなかったデータは破棄されるシンプルなものです。 しかしシンプルであるがゆえ信頼性が高く、動作も軽いという特徴があります。
ターミナルを2つ開いてください。
ターミナル1でsubscribe
を実行すると、データ待ちの状態になります。
172.17.0.2:6379> subscribe channelA
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channelA"
3) (integer) 1
2
3
4
5
6
ターミナル2でデータをpublish
してみましょう。
172.17.0.2:6379> publish channelA hello
(integer) 1
172.17.0.2:6379> publish channelA I
(integer) 1
172.17.0.2:6379> publish channelA am
(integer) 1
172.17.0.2:6379>
2
3
4
5
6
7
subscribeしている側にどのように見えたでしょうか。
# データの永続性
redisはオンメモリで動くため、再起動するとデータは消えます。 基本的には消えてもいいデータを扱うことが多いですが、再起動のたびに消えてしまうのも面倒なので永続化の仕組みが存在します。
persistence (opens new window)
永続化には主に2つのやり方があります。
- RDB(Redis Database): 一定時間ごとに全データをスナップショットしてファイルに残しておく。リレーショナルデータベース(RDB)とは関係ない
- AOF(Append Only File): 更新をログとして記録していく。スナップショット作成が高コストなのに対して、小さいデータを追記していくので低コスト。一方でディスクサイズは大きくなる。
どちらか、あるいは両方を使うかどうかはユースケース次第です。 しかしいずれもデータの更新からファイルへの記録にはラグがあり、更新内容が必ず永続化される保証はないことに注意が必要です。
# 応用課題: プログラムから Redis を使おう
- それでは、実際にPythonでRedis を利用するコードを書いていきます。
- 手元の環境で好きなエディタを使って Python のコードを書いた上で docker container の中から そのコードを読み出せるようにします。
- 事前準備のおさらいも兼ねています
# コンテナの中から実行する
どこか作業用のディレクトリを作成してそこに移動してください
mkdir iij_bootcamp_redis cd iij_bootcamp_redis
1
2プログラムを置く場所として iij_bootcamp_redisの中に app ディレクトリを作成し その中にhello_world.py を作成してください エディタは自由です。
mkdir app vim app/hello_world.py
1
2print("Hello World!")
1今いる場所から見ると app/hello_world.py が作られたはずです。
$ cat app/hello_world.py print("Hello World!")
1
2- そうでない場合は cd コマンドで 階層を移動して 必ず app ディレクトリのひとつ上の階層に移動してください (例だとiij_bootcamp_redisにいてほしい)
コンテナ の中から起動
- -v で 今いるところの app ディレクトリを コンテナの中では /app に マウントします。
docker run -it --rm -v `pwd`/app:/app python:3.9.5-slim-buster bash
1 - ls -l で ファイルが有るか確認します。
コンテナ内部 $ ls -l /app total 4 -rw-rw-r-- 1 1000 1000 22 May 13 08:37 hello_world.py
1
2
3 - python で起動します
コンテナ内部: $ python /app/hello_world.py Hello World!
1
2 - うまく Hello World! と表示されたら このコンテナは一旦閉じてしまいましょう
コンテナ内部: $ exit
1
- -v で 今いるところの app ディレクトリを コンテナの中では /app に マウントします。
これで、 手元で書いたファイルを コンテナ内のPython から起動することができました。
# Redis Server 起動
- 事前準備の通り Redis Server を起動してください。
docker run --rm --name test-server redis:6.2.3-alpine3.13
Ready to accept connections
と出ればOK このターミナルは開いたままにします。- もし、ターミナルが閉じたり、Ctrl-C で終了してしまったら、再度 起動してください
# Redis Server 接続
Redis には redis-cli という ツールが有ります。 事前準備では ping などを試しました。
- 他にも どんな動きがRedis に対して実行されているか知ることができる機能があるので紹介します
以下の二通りの接続方法のうち好きな方を選んでください
# ネットワーク越し 別コンテナ
docker run -it --link test-server:redis --rm redis:6.2.3-alpine3.13 sh -c 'exec redis-cli -h "$REDIS_PORT_6379_TCP_ADDR" -p "$REDIS_PORT_6379_TCP_PORT"'
# 直接 乗り込んで起動
docker exec -it test-server redis-cli
2
3
4
5
- エラーメッセージ以外のすべてを通知するように設定
127.0.0.1:6379> config set 'notify-keyspace-events' AKE
- すべてのキーに対して通知を購読
127.0.0.1:6379> psubscribe '__key*__:*'
- 最初は真っ黒のままです。 何かしら redis に対して操作を行うと どんなことを行ったのか画面に表示されるようになります。
- 今は開きっぱなしにしましょう
# コンテナの中から Redis Server へ接続
次に Redis へ接続するコードを書いてみます。
- 再度、Pythonのコンテナを起動します。 先ほどと同様 iij_bootcamp_redis の中から実行してください。
docker run -it --link test-server:redis --rm --name test-python -v `pwd`/app:/app python:3.9.5-slim-buster bash
1- 先程と同様に /app には hello_world.py があるはずです
コンテナ内部: $ cd /app コンテナ内部: $ ls hello_world.py
1
2
3- さて、今度は redis と接続をするため 事前準備で行ったようにライブラリのインストールをしましょう。
- なお、現状 コンテナを起動するたびに 必要ですが 自分でDockerfile を書くなど image を build することで割愛することも可能です
コンテナ内部: $ pip install redis Collecting redis Downloading https://files.pythonhosted.org/packages/ac/a7/cff10cc5f1180834a3ed564d148fb4329c989cbb1f2e196fc9a10fa07072/redis-3.2.1-py2.py3-none-any.whl (65kB) |████████████████████████████████| 71kB 3.2MB/s Installing collected packages: redis Successfully installed redis-3.2.1
1
2
3
4
5
6- 先程起動したRedis Server の情報が連携できているか確認します。
- 細かい値が違くても問題ないです。
コンテナ内部: $ env | grep REDIS REDIS_PORT_6379_TCP_PROTO=tcp REDIS_PORT=tcp://172.17.0.2:6379 REDIS_NAME=/test-python/redis REDIS_PORT_6379_TCP_ADDR=172.17.0.2 REDIS_PORT_6379_TCP=tcp://172.17.0.2:6379 REDIS_ENV_REDIS_DOWNLOAD_URL=http://download.redis.io/releases/redis-5.0.5.tar.gz REDIS_PORT_6379_TCP_PORT=6379 REDIS_ENV_REDIS_DOWNLOAD_SHA=2139009799d21d8ff94fc40b7f36ac46699b9e1254086299f8d3b223ca54a375 REDIS_ENV_GOSU_VERSION=1.10 REDIS_ENV_REDIS_VERSION=5.0.5
1
2
3
4
5
6
7
8
9
10
11
Python には 事前準備にも使った対話式のUIがあります。
- 起動
コンテナ内部: $ python
1- ping してみる。
>>> import os, redis >>> conn = redis.Redis(host=os.environ['REDIS_PORT_6379_TCP_ADDR'], port=os.environ['REDIS_PORT_6379_TCP_PORT'], db=0) >>> conn.ping() True
1
2
3
4- 事前準備ではここまででした。 それでは実際にこの対話式UIを利用して redis へ書き込みをしてみましょう
// 複数データセット: mset コマンド >>> conn.mset({'key1': 'value1', 'key2': 'value2'}) True // 複数データゲット: mget コマンド >>> conn.mget(['key1', 'key2']) [b'value1', b'value2']
1
2
3
4
5
6
7// 複数データセット,ゲットを別手法でやってみる : mset,mget,scan コマンド >>> keys = conn.scan(match='key*') >>> values_1 = conn.mget(keys[1]) >>> print(values_1) [b'value1', b'value2'] // データを追加して >>> conn.mset({'key1': 'value1', 'key2': 'value2', 'id1': 'ichiro', 'id2': 'jiro'}) // key から始まるキーに紐づく値 >>> keys = conn.scan(match='key*') >>> values_1 = conn.mget(keys[1]) >>> print(values_1) [b'value1', b'value2'] // id から始まるキーに紐づく値 >>> keys = conn.scan(match='id*') >>> all_id = conn.mget(keys[1]) >>> print(all_id) [b'ichiro', b'jiro']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 複数データ削除: delete コマンド >>> _, keys = conn.scan(match='key*') >>> print(conn.mget(keys)) [b'value1', b'value2'] >>> ret = conn.delete(*keys) >>> print(conn.mget(keys)) [None, None] // もちろん消してないやつは消えていない >>> _, keys = conn.scan(match='id*') >>> print(conn.mget(keys)) [b'ichiro', b'jiro'] >>>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 要素を入れてみる
普通に List や Hash として使って、適当に要素を入れてみよう
- /etc/services の ポート -> プロトコル名称 対応表を作る
{ '1/tcp': [ 'tcpmux' ], '7/tcp': [ 'echo' ], '7/udp': [ 'echo' ], '9/tcp': [ 'discard', 'sink', 'null' ], '9/udp': [ 'discard', 'sink', 'null' ], '11/tcp': [ 'systat', 'users' ], # ... }
1
2
3
4
5
6
7
8
9サンプル: https://github.com/iij/bootcamp/blob/master/src/database/redis/ex01.py (opens new window)
- /etc/passwd の内容をユーザごとのハッシュに格納する
{ 'root': { 'passwd': 'x', 'uid': '0', 'gid': '0', 'name': 'root', 'home': '/root', 'shell': '/bin/bash' }, 'daemon': { 'passwd': 'x', 'uid': '1', 'gid': '1', 'name': 'daemon', 'home': '/usr/sbin', 'shell': '/usr/sbin/nologin' } # ... }
1
2
3
4
5サンプル: https://github.com/iij/bootcamp/blob/master/src/database/redis/ex02.py (opens new window)
# Expire を試してみる
- expire 付きで値を格納し、実際にその時間が過ぎると値が消えていくことを確認する サンプル: https://github.com/iij/bootcamp/blob/master/src/database/redis/ex03.py (opens new window)
# メッセージキューを実装する
メッセージキュー的なものを実装してみよう
- 依頼側: ランダム秒 sleep して、メッセージをキューに入れる
- 取り出し側: ランダム秒 sleep して、メッセージをキューから取り出す
使用例: web クローラー
- 依頼側: URL をどんどんキューに入れる
- 取り出し側: キューを見て URL が入っていたら、取得してファイルに保存する
- 結果(ファイルの場所)を別のキューで戻してもいいですね
# Advanced: Redis Pub/Sub, Streams を使ってみよう
# Advanced: GIS で遊んでみよう
- 今どき オープンデータとして 様々な施設の座標を公開している自治体があります。
- jupyter nodebook では csv の 処理などはとても簡便にすることができます
- redis.geoadd で登録: https://redis-py.readthedocs.io/en/stable/#redis.Redis.geoadd (opens new window)
- redis.georadius で検索が可能です。:https://redis-py.readthedocs.io/en/stable/#redis.Redis.georadius (opens new window)
# 参考資料
CC BY-SA Licensed | Copyright (c) 2023, Internet Initiative Japan Inc.