# Database: Redis 編

このハンズオンの狙い

  • Docker に慣れる
  • Redis を実際に使ってみる
  • よく使いたくなる処理(部品)を作ってみる

# 事前準備

  • Docker を使用できる
    • install は主題ではないので、Docker image を使用します
  • 何らかの LL を使える
    • 例は Python で書きますが、Ruby, Perl, JavaScript, Go なども使えます。適当に読み替えてください

# 事前準備手順

  1. 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
  2. サーバを起動する

    docker run --rm --name test-server redis:6.2.3-alpine3.13
    
    1
    • test-server という名前で redis サーバーを 起動します
    • Ready to accept connections と出ればOK このターミナルは開いたままにします
    • もし、ターミナルが閉じたり、Ctrl-C で終了してしまったら、再度 起動してください
  3. 別のターミナルを開いて、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 〜 としたらいけました
  4. 終了

    • ここまで完了できたら 事前準備完了です。 無事 これらを行うことができました。
      1. docker image の 取得
      2. redis server の起動
      3. 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四方にある ラーメン屋の検索 とかができる
    • シンプルに出来が良い
      • 機能がコンパクトにまとまっており、信頼性が高く動かしていて気持ちがいい(主観)
  • 世間での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)
1
2
3
4
5
6
7
8
9
10

これがredisで最も基本的なkeyとvalueの扱いです。setgetを「コマンド」と呼び、その後ろは引数です。 keyを色々操作してみましょう。

keys (opens new window) はkeyの検索ができます。keys *とすると存在するすべてのキーが出力されます。 ちなみにredisはシングルスレッドで動作するソフトウェアで、keys *で大量のkeyを表示すると一定時間処理が止まってしまい、本番サーバに重大な影響を与えてしまうので注意しましょう。

172.17.0.2:6379> keys key*
1) "key1"
2) "key2"
1
2
3

他にも色々

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"
1
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 数値の扱い

redisはよくランキング情報を一時的にキャッシュしたり数値を扱うことが多いため、そのためのコマンドが用意されています。

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"
1
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"
1
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"
1
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"
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

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>
1
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
    2
    print("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
  • これで、 手元で書いたファイルを コンテナ内のPython から起動することができました。

# Redis Server 起動

  • 事前準備の通り Redis Server を起動してください。
docker run --rm --name test-server redis:6.2.3-alpine3.13
1
  • Ready to accept connections と出ればOK このターミナルは開いたままにします。
  • もし、ターミナルが閉じたり、Ctrl-C で終了してしまったら、再度 起動してください

# Redis Server 接続

  • Redis には redis-cli という ツールが有ります。 事前準備では ping などを試しました。

  • 以下の二通りの接続方法のうち好きな方を選んでください

# ネットワーク越し 別コンテナ
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
5
  • エラーメッセージ以外のすべてを通知するように設定
127.0.0.1:6379> config set 'notify-keyspace-events' AKE
1
  • すべてのキーに対して通知を購読
127.0.0.1:6379> psubscribe '__key*__:*'
1
  • 最初は真っ黒のままです。 何かしら 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

# 要素を入れてみる

# Expire を試してみる

# メッセージキューを実装する

  • メッセージキュー的なものを実装してみよう

    • 依頼側: ランダム秒 sleep して、メッセージをキューに入れる
    • 取り出し側: ランダム秒 sleep して、メッセージをキューから取り出す
  • 使用例: web クローラー

    • 依頼側: URL をどんどんキューに入れる
    • 取り出し側: キューを見て URL が入っていたら、取得してファイルに保存する
      • 結果(ファイルの場所)を別のキューで戻してもいいですね

# Advanced: Redis Pub/Sub, Streams を使ってみよう

# Advanced: GIS で遊んでみよう

# 参考資料


CC BY-SA Licensed | Copyright (c) 2023, Internet Initiative Japan Inc.