Hands-Onで学ぶ、はじめてのRedis

# Redis Hands-On

## 1. 事前準備

### 1.1. Redisコンテナーの起動 (初回起動の場合)

```bash:shell
docker run -d --name redis-test -p 6379:6379 redis:latest
```

### 1.2. Redisコンテナーの起動 (前回起動した同じコンテナーを利用する場合)

```bash:shell
! docker start redis-test
```

### 1.3. Hands-On前にRedisの中身をクリーニングする

以下をnotebookのセルで実行

```python:notebook
r.flushdb()
```

```terminal:output
True
```

## 2. Redisの基本動作の確認

### 2.1. 文字列操作: キーバリューの設定/取得/上書きの動作の確認

Redisの基本的なキーバリューの「設定 (Set)」「取得 (Get)」「上書き」の動作を確認する

- `set`:
    - `language` という名前のキーに、最初に `Python` という値を設定
    - `language` キーの値を 'Rust' に上書きも可能
- `get`:
    - 設定した値の読み出し

```python:notebook
import redis
r = redis.Redis(host='localhost', port=6379, db=0)

# SetとGet
r.set('language', 'Python')
print(r.get('language').decode())  # → Python

# 文字列の上書き
r.set('language', 'Rust')
print(r.get('language').decode())  # → Rust
```

```terminal:output
Python
Rust
```

### 2.2. List: リストにデータを順番に格納/取り出しの実施

- `lpush`:
    - `fruits` という名前の「順番付きのデータ入れ（リスト）」を用意する
    - そのリストの先頭に `apple`、次に `banana`、最後に `orange` を順番に押し込んでいく
	- 結果、リストの中は先頭から `['orange', 'banana', 'apple']` という並びになる
- `lrange`:
    - `fruits`リストの中身を最初から最後まで全部取り出して画面に表示する
    - `0`は最初の要素
    - `-1`は最後の要素という意味

```python:notebook
# LPUSHでリストに値を追加
r.lpush('fruits', 'apple', 'banana', 'orange')

# LRANGEで全要素を取得（0から-1まで）
print(r.lrange('fruits', 0, -1))  # → [b'orange', b'banana', b'apple']
```

```terminal:output
[b'orange', b'banana', b'apple']
```

### 2.3. Set: セット型データを使って重複させたくないデータを管理

- `sadd`:
    - `colors` という名前の「順番がなく、同じものは入らない特別な袋（セット）」を用意
    - その袋に `red`, `blue`, `green` という色と、もう一度 `red` を入れようとする
	- でも、同じものは入らないルールなので、袋の中には `red`, `blue`, `green` がそれぞれ1つずつだけ入るはず
- `smembers`:
    - `colors` 袋の中身を全部（順番は気にせず）取り出して、画面に表示する
    - このとき重複がなく、各要素はユニークとなっている

```python:notebook
# SADDでセットに追加（重複は無視される）
r.sadd('colors', 'red', 'blue', 'green', 'red')

# SMEMBERSですべて取得（順不同）
print(r.smembers('colors'))  # → {b'red', b'green', b'blue'}
```

```terminal:output
{b'blue', b'red', b'green'}
```

### 2.4. Hash: 1つのハッシュに複数の情報を項目ごとに整理して保存できる

- `hset`:
    - `user:1` という名前の「プロフィールカード（ハッシュ）」を用意する
    - そのカードに、`name`という項目には`Alice`、`age`という項目には`30`と書き込む
- `hget`:
    - 次に、そのカードから`name`の項目だけを取り出して表示する（→ `Alice`と表示される）
- `hgetall`:
    - 最後に、そのカードに書かれている「すべての項目とその内容」をまとめて取り出して表示する

```python:notebook
# HSETでフィールドをセット
r.hset('user:1', mapping={'name': 'Alice', 'age': 30})

# HGETで1つのフィールドを取得
print(r.hget('user:1', 'name').decode())  # → Alice

# HGETALLで全フィールド取得
print(r.hgetall('user:1'))  # → {b'name': b'Alice', b'age': b'30'}
```

```terminal:output
Alice
{b'name': b'Alice', b'age': b'30'}
```

### 2.5. Sorted Set: 値が低い順もしくは高い順にデータを取り出し

- `zadd`: 
    - `ranking` という名前の「得点付きランキングボード（ソート済みセット）」を作る
    - ランキングボードに、「`Alice`さんは`100`点」「`Bob`さんは`150`点」「`Carol`さんは`120`点」と登録する
	- Redisが内部で自動的にスコアの低い順に並べ替える準備をしてくれる
- `zrange`: 
    - ランキングボードのメンバー全員を、スコアが低い順に、もしくはスコアが高い順にスコアといっしょに取り出して画面に表示する
    - スコアが高い順にする場合は引数に `desc=True` を用いる

```python:notebook
# ZADDでスコア付きデータ追加
r.zadd('ranking', {'Alice': 100, 'Bob': 150, 'Carol': 120})

# ZRANGEでソートされた順に取得（withscores=Trueでスコア付き）
print(r.zrange('ranking', 0, -1, withscores=True))
# → [(b'Alice', 100.0), (b'Carol', 120.0), (b'Bob', 150.0)]

# ZRANGEでソートされた逆順に取得（withscores=Trueでスコア付き, desc=Trueで逆順）
print(r.zrange('ranking', 0, -1, withscores=True, desc=True))
# → [(b'Alice', 100.0), (b'Carol', 120.0), (b'Bob', 150.0)]
```

```terminal:output
[(b'Alice', 100.0), (b'Carol', 120.0), (b'Bob', 150.0)]
[(b'Bob', 150.0), (b'Carol', 120.0), (b'Alice', 100.0)]
```

## 3. Listを使ったTodoリストの応用例

念のため環境のクリーン

```python:notebook
r.flushdb()
```

### 3.1. 新しいタスクを追加（`LPUSH`）

```python:notebook
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
# 新しいタスクを追加
r.lpush('todo', '買い物に行く', 'メール返信', 'Redisを学ぶ')
```

### 3.2. タスク一覧を表示（`LRANGE`）

```python:notebook
# 現在のタスク一覧
print('Todoリスト:')
for task in r.lrange('todo', 0, -1):
    print('-', task.decode())
```

```terminal:output
Todoリスト:
- Redisを学ぶ
- メール返信
- 買い物に行く
```

### 3.3. 完了したタスクを削除（`RPOP`）

- `LPUSH`でタスクを追加した場合、`RPOP` は「一番古い情報（最初に追加されたタスク）」を取り出すことになる
- これは、お店の行列と同じで、「最初に来た人 (First-In) が最初にサービスを受ける (First-Out)」なので、FIFO（ファイフォ）キューとなる

```python:notebook
# タスクを完了（末尾から削除）
done = r.rpop('todo')
print('完了タスク:', done.decode())
```

```terminal:output
完了タスク: 買い物に行く
```

### 3.4. 削除確認

```python:notebook
# 更新後のタスク一覧
print('更新後のTodoリスト:')
for task in r.lrange('todo', 0, -1):
    print('-', task.decode())
```

`買い物に行く`が削除されていることを確認

```terminal:output
更新後のTodoリスト:
- Redisを学ぶ
- メール返信
```

## 4. ユーザー情報を管理（Hash）

念のためRedisの初期化を実施

```python:notebook
r.flushdb()
```

### 4.1. ユーザー情報（名前、年齢、メール）を登録（`HSET`）

```python:notebook
import redis

r = redis.Redis(host='localhost', port=6379, db=0)

# ユーザー情報の登録
user_id = 'user:1001'
r.hset(user_id, mapping={
    'name': 'Taro',
    'age': '25',
    'email': 'taro@example.com'
})
```


### 4.2. 個別の情報取得（`HGET`）

```python:notebook
# 個別に取得
print('名前:', r.hget(user_id, 'name').decode())
```

```terminal:output
名前: Taro
```

### 4.3. 全情報取得（`HGETALL`）

```python:notebook
# すべて取得
print('ユーザー情報:')
for key, value in r.hgetall(user_id).items():
    print(f"{key.decode()}: {value.decode()}")
```

```terminal:output
ユーザー情報:
name: Taro
age: 25
email: taro@example.com
```

### 4.3. RedisInsightでの内容確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73207/8dbd88ec-beb8-4489-832b-bb2f280af111.png)

## 5. 人気ランキング（Sorted Set）

### 5.1. ユーザーのスコアを追加（`ZADD`）

```python:notebook
import redis

r = redis.Redis(host='localhost', port=6379, db=0)

# スコアを追加（スコアは数値）
r.zadd('ranking', {
    'Taro': 150,
    'Hanako': 180,
    'Ken': 120
})
```

### 5.2. ランキング表示（`ZRANGE`）

```python:notebook
# ランキング表示（スコア付き）
print('人気ランキング:')
for name, score in r.zrange('ranking', 0, -1, withscores=True):
    print(f"{name.decode()}: {int(score)}")
```

```terminal:output
人気ランキング:
Ken: 120
Taro: 150
Hanako: 180
```

## 6. Redis Pub/Sub とは？

Pub/Subの意味

- `Publisher`: メッセージを「チャンネル」に送る人（例：通知を送る側）
- `Subscriber`: チャンネルを購読してメッセージを受け取る人（例：通知を受け取る側）

応用例

- Flask + WebSocket + Redis でチャットアプリのバックエンドに
- ログ監視やイベント通知システムに活用
- IoT デバイスのイベント通知にもよく使われます

### 6.1. Subscriber（受け取る側）のコードを保存する

以下の内容を `subscriber.py` という名前で保存する

```python:subscriber.py
import redis

r = redis.Redis(host='localhost', port=6379, db=0)
pubsub = r.pubsub()

# 'news' チャンネルを購読
pubsub.subscribe('news')
print("チャンネル 'news' を購読中...")

for message in pubsub.listen():
    if message['type'] == 'message':
        print("受信:", message['data'].decode())
```


### 6.2. Publisher（送る側）

以下の内容を `publisher.py` という名前で保存する

```python:publisher.py
import redis
import time

r = redis.Redis(host='localhost', port=6379, db=0)

messages = [
    "Redisの練習を始めよう！",
    "新しいタスクが追加されました",
    "通知：サーバーが起動しました",
]

for msg in messages:
    r.publish('news', msg)
    print("送信:", msg)
    time.sleep(1)

```

### 6.3. terminal1を開いて、以下のコマンドを実行する

```bash
$ python subscriber.py
チャンネル 'news' を購読中...
```

### 6.4. terminal2を開いて、以下のコマンドを実行する

```bash
$ python publisher.py
送信: Redisの練習を始めよう！
送信: 新しいタスクが追加されました
送信: 通知：サーバーが起動しました
```

### 6.5. terminal1の表示を観察する

```terminal
受信: Redisの練習を始めよう！
受信: 新しいタスクが追加されました
受信: 通知：サーバーが起動しました
```

## 7. Clean Up

### 6.1. Redisコンテナーの停止

```bash:terminal
docker stop redis-test
```