[Rails]Redisの導入と使い方

# Redisとは
## 特徴
非RDB(Relational Database)。マシンのメモリ上へデータを保存する。
※対比として、RDBであるMySQLではファイルへデータを保存している。


**メリット**

- メモリ上でデータを扱うため処理が早い(HDDとメモリでは数万倍の差がある)
- すべての処理が完了しないとロールバックされる機能がデフォである
- 直列処理であるため、データの不整合は発生しない
- 複数のDBを持てる


**デメリット**

- マシン上のメモリの消費が大きく、データの保存量上限は少ない
    - メモリ上限に達すると指定したルールに従ってデータが削除される
- マシンが落ちたときデータが揮発してしまう
- シングルスレッドでの動作であり、並列処理ができない & 1コアしか使えないためCPUのコア数を上げても無駄
- マスタ-スレーブ構成が可能だが、非同期であるため不整合が発生する可能性あり

## データの揮発対策
2パターンある

**スナップショットからの復旧**

「Redis Database」というファイルへRedisのスナップショットを作成できる、これを定期的に行うことである程度の防止が可能である。
直前のデータは復旧不可。

**AOF**

DB操作のログからデータをバックアップする方法。
ログを取る分、DB操作時の実行速度は落ちるが、直前のデータは復旧できる。

## 用途
- セッション管理: RDSでセッション情報を扱うより高速に動作させられる
- Webページのキャッシュ
- リアルタイム性を求められる機能(ランキングとか): リスト化した状態で保存して随時ソートを行う

## 類似のソフトウェアMemcached
うーん。目立ったメリットもないので、Redisにしとけばまぁ間違いなさそうである。

https://dev.classmethod.jp/articles/which-choice-redis-memcached/

## 参考

https://www.fenet.jp/infla/column/%E6%9C%AA%E5%88%86%E9%A1%9E/redis%E3%81%A8%E3%81%AF%E3%81%A9%E3%81%AE%E3%82%88%E3%81%86%E3%81%AA%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%EF%BC%9F3%E3%81%A4%E3%81%AE%E7%89%B9%E5%BE%B4%E3%82%84%E4%BD%BF%E3%81%84/

https://www.slideshare.net/yujiotani16/redis-76504393


# 利用手順
当然だが、Redisを利用するには、そのサーバーとクライアントソフトウェアが必要。
docker-composeでローカル開発環境を構築する際の手順




## Redisサーバー
### docker-compose.yml

```
  # redisコンテナはデフォルトで0〜15のインデックスのDBが作成され、リッスンポート6379で起動し、 
  # /dataに60秒に1回スナップショット(Redis Databaseファイル)を作成するようになっている
  redis:
    container_name: redis
    # dockerホスト起動時にコンテナが起動するよう設定
    restart: always
    image: "redis:latest"
    ports:
      - "6379:6379"
    volumes:
      - "./data/redis:/data"
```


## Redisクライアント
### gem(redis)のセットアップ

**①Gemfileに記載**

```
gem 'redis'
```

**②インストール**

```
bundle install
```

**③Redisクライアントの設定**

③-1. 各RAILS_ENVごとに切り替えるため、config/settings/<環境名>.yml に以下を記載する

```config/settings/<環境名>.yml
# 例. development.yml で ローカルのredisサーバーへ接続する
redis:
  host: redis
  port: 6379
  db: 0 # DBのindex. Redisはデフォで16のDBを持ち、0〜15のindexがある。
```

③-2. config/initializers/redis.rbを作成し、以下のように設定しておく

```
# RAILS_ENV別のredisクライアント設定読み込み
redis = Settings.redis

# redisクライアントのインスタンスを生成してRedis.currentでアクセスできるようにする
Redis.current = Redis.new(
  host: redis.host,
  port: redis.port,
  db: redis.db
)
```

④動作確認

```
# 前提: redisコンテナが起動している

# 1. railsサーバーが起動しているコンテナへログイン
docker exec -it <railsコンテナID> bash

# 2. rails console起動
bundle exec rails c

# 3. 適当なキー、値をセットしてみる
Redis.current.set("a", "b")
Redis.current.get("a")
=>
"b" と表示されれば、Redisサーバー & Redisクライアントともに正常に動作している

```

**参考**

https://github.com/redis-store/redis-rails


### 利用方法
redisクライアントのインスタンスに対してキー・バリューのset系メソッド(データの書き込み), get系メソッド(データの読み込み)を実行する、というだけ。
ただし、バリューは以下5つのデータ型が取れるが、それぞれで使うべきメソッドが違う。

**String型**

参考: リファレンス
- [set](https://www.rubydoc.info/gems/redis/Redis%2FCommands%2FStrings:set)
- [setex](https://www.rubydoc.info/gems/redis/Redis%2FCommands%2FStrings:setex)
- [get](https://www.rubydoc.info/gems/redis/Redis%2FCommands%2FStrings:get)

```
# 保存されるデータ型のイメージ
{'key': "value"}

# set
## 期限なし
Redis.current.set('en', 'Hello')
## 期限あり(秒で設定)
Redis.current.setex('en', 10, 'Hello')


# get
Redis.current.get('en')
=> "Hello" ※setexでセットした場合は、指定した秒数後はnilで返ってくる。


```
**List型**
先頭 or 末尾にデータの追加が可能. 

```
# 保存されるデータ型のイメージ
{"key": ["value1", "value2",...]}

# set
## 末尾追加
Redis.current.rpush("en", "Hello")

## 先頭追加
Redis.current.lpush("en", "Hello2")

# get
## i番目の値を取得
Redis.current.lindex("en", 0)
=>"Hello2"

## リストのi番目からj番目までの値を配列で取得(先頭インデックスは0)
Redis.current.lrange("en", 0, 1)
=>["Hello2", "Hello"]
```

**Set型**
valueの配列に同じ値の要素は登録不可能同。
順不同で保存。
集合演算メソッドが利用できる、というより集合演算が目的っぽい

```
# 保存されるデータ型のイメージ
{"key": ["value1", "value2",...]}

# set
## 追加
Redis.current.sadd("en", "Hello")
Redis.current.sadd("en", "Hello2")

Redis.current.sadd("en2", "Hello2")
Redis.current.sadd("en2", "Hello3")

# get
## 全要素を取得
Redis.current.smembers("en")
=>["Hello2", "Hello"]

## 和を出す
Redis.current.sunion("en", "en2")
=>["Hello2", "Hello3", "Hello",]
```

**SortedSet型**
ソート済みセット型。同じ値は登録不可能。
配列要素は「スコア」でソートされ、「順位」を持つ。スコア/順位で範囲検索が可能。

```
# 保存されるデータ型のイメージ
{"key": [["value1", score1], ["value2", score2]]}

# set
Redis.current.zadd("score_ranking", 10, "user_id_1")
Redis.current.zadd("score_ranking", 500, "user_id_2")
Redis.current.zadd("score_ranking", 600, "user_id_3")
Redis.current.zadd("score_ranking", 1, "user_id_4")

# get
## 特定要素の順位を取得
Redis.current.zrank("score_ranking", "user_id_3")
=>3

## スコア上位iからjまでの値を取得。iは0始まり
Redis.current.zrevrange("score_ranking", 0, 2)
=>["user_id_3", "user_id_2", "user_id_1"]

## スコア上位iからjまでの値とスコアを取得。iは0始まり
Redis.current.zrevrange("score_ranking", 0, 2, with_scores: true)
=> [["user_id_3", 600.0], ["user_id_2", 500.0], ["user_id_1", 10.0]]"user_id_3"], [500, "user_id_2"], [10, "user_id_1"]]

```

**Hash型**

文字列型のキー & バリューのハッシュ。キーでの検索は可能だが、値での検索は不可能。
RDBっぽい管理方法。

参考: リファレンス
- [hset](https://www.rubydoc.info/gems/redis/Redis%2FCommands%2FHashes:hset)
- [hsetnx](https://www.rubydoc.info/gems/redis/Redis%2FCommands%2FHashes:hsetnx)
- [hget](https://www.rubydoc.info/gems/redis/Redis%2FCommands%2FHashes:hget)
- [hmset](https://www.rubydoc.info/gems/redis/Redis%2FCommands%2FHashes:hmset)
- [hmget](https://www.rubydoc.info/gems/redis/Redis%2FCommands%2FHashes:mapped_hmget)

```
# 保存されるデータ型のイメージ
{"key": {"value_key": value_value}}
```

```
# 1つのカラム・値で構成されるレコードをset,getする例
## set
### すでに「"record"」が存在する場合は上書きされる
Redis.current.hset("record", "column_1_key", "column_1_value")
### すでに「"record"」が存在する場合は上書きされない 
Redis.current.hsetnx("record", "column_1_key", "column_1_value")
## get
Redis.current.hget("record", "column_1_key")
=>
"column1_value"

# 複数のカラム・値で構成されるレコードをset,getする例
## set
Redis.current.hmset("record",
  [
    "column1_key", "column1_value",
    "column2_key", "column2_value",
  ]
)

## get
Redis.current.hmget("record", ["column_1_key", "column_2_key"])
=> ["column_1_value", "column2_value"]
```

**参考**

https://www.rubydoc.info/gems/redis

https://mebee.info/2021/02/09/post-28037/

http://redis.shibu.jp/commandreference/lists.html

### 応用編

ActiveRecordなどのオブジェクトをString型にしてRedisへ保存する場合には、
- Marshal.dump(<オブジェクト>)
- Marshal.load(<オブジェクト>)

を使うといい感じにいける。

**キー: "key",　値: ActiveRecord型 でset, getする例**
```
# set
user = User.first
Redis.current.set("key", Marshal.dump(user))

# get
user_str = Redis.current.get("key")
Marshal.load(user_str)
```


### メモ
Redisクライアントのインスタンスを保存するようのクラスメソッド「Redis.current」がgem 「redis」に存在するので、各RailsソースにまたがってRedisを利用する際にはこれを使うべき

**参考**

https://webuilder240.com/blogs/cf23345d-ab52-4f42-8aa4-679cf482306b

## 参考