ざっくり分かるRedis入門　～データ構造と主な機能について～


勤務先のプロダクトでRedisと呼ばれるミドルウェアがキャッシュサーバとして使われているが
雰囲気でやってたので体系的に学んでみる事にした。

（記事内にテーブルを多用してしまったのでPCでの閲覧を推奨。スマホだとテーブルは見にくいですね・・・）

# Redisとは？

インメモリ Key-Value ストア
インメモリなので、Redisを再起動すると格納したデータが全て消える。（揮発性）
（後述の永続化を行えばその限りではない）

# Redisのアーキテクチャ

**Redis Server**と**Redis Client**がある
Redis Serverは（デフォルトでは）`6379`ポートで待ち受ける
この6379ポートにtelnetで接続してコマンドを実行する事でredis serverを操作できる。
**シングルスレッド**の**イベントドリブン**で動作する。
CPUは**1コア**のみ使用する。

# Redisで扱えるデータ構造

RedisではデータをKeyとValueのペアで保存する。
Valueとして扱えるデータ構造がいくつかある。

| 名称 | 説明 | 主な操作コマンド |
|---|---|---|
| strings | 文字列。バイナリセーフなので画像なども保存できる。ただし最大1GB | `SET` `GET` |
| hashes | フィールドと値のマップ。| `HSET` `HGET` |
| lists | 文字列型のリスト。| `LPUSH` `RPUSH` `LRANGE` |
| sets | 順不同の集合。メンバの重複を許可しない。| `SADD` `SINTER` `SUNION` |
| sorted sets | 文字列型の集合。　| `ZADD` `ZREM` |
| bit arrays | bitの配列。ビット単位で操作できる。 | `BITFIELD` |
| hyperloglogs | ユニークなデータを数えるためのデータ構造。HLLアルゴリズムを使用し高速かつ省メモリで推定できる | `PFADD` `PFCOUNT` |
| geospatial indexes | 位置情報（緯度、経度、名前） 。中心位置と半径を指定して検索などが出来る。| `GEOADD` `GEORADIUS` |
| streams | 時系列データ。発生時刻＋フィールドと値のマップ。 | `XADD` `XREVRANGE` |

コマンドの接頭辞を見れば扱うデータ構造が大体わかる。Hならハッシュ、Xなら時系列データ、など。

これらのデータ構造を上手く使うことでRedisを単なるキャッシュサーバとしてではなく
排他制御のロック管理やソート処理に活用する事ができる

# 設定ファイルについて

設定ファイルは、`/etc/redis.conf`にある。
dockerコンテナの場合は、`/usr/local/etc/redis/redis.conf`にある。

以下の形式で設定を記述する（**ディレクティブ**と呼ばれる）

```
keyword argument1 argument2 ... argumentN
```

ファイルが存在しない場合は、デフォルトの設定が使用される。

設定ディレクティブの一覧、およびその意味と使用目的は以下にある（Redis Version 5.0.3の場合）
[redis/redis.conf at 5.0.3 · antirez/redis · GitHub](https://github.com/antirez/redis/blob/5.0.3/redis.conf)

# Redisの主な機能

大まかに７つある

| 名称 | 説明 |
|---|---|---|
| replication | マスタースレーブ間で**データを同期**する仕組み |
| Lua scripting | `EVAL`コマンドと`EVALSHA`コマンドでLuaスクリプトを実行できる |
| LRU eviction | メモリ使用量が上限に達した際に、使われていないデータを追い出して**メモリを確保**する仕組み。 |
| transactions  | コマンドを**まとめて発行**する仕組み（別名パイプライン） |
| persistence | Redisサーバを再起動しても**データを維持**するための仕組み。|
| Redis Sentinel |Redisサーバの**死活監視/通知**および**自動フェイルオーバー機能**を提供する仕組み|
| Redis Cluster | 複数のRedisサーバにデータを**分散**させる仕組み|

#### replication

マスタースレーブ間でデータを同期する仕組み
マスタースレーブ間のリンクが切れた場合、スレーブは自動的にマスターに再接続する。
可用性を向上させるために使用する。

#### Lua scripting

`EVAL`コマンドと`EVALSHA`コマンドでLuaスクリプトを実行できる
`EVAL`コマンドはLuaスクリプトを直接実行できる
`EVALSHA`コマンドはあらかじめ登録しておいたLuaスクリプトを呼び出す事ができる。
`SCRIPT LOAD`コマンドを使用してLuaスクリプトを登録する。
登録時にSHA1ハッシュが発行されるので、これを使って、`EVALSHA`コマンドで呼び出す事ができる。
アプリケーションからSHA1ハッシュでLuaスクリプトを呼び出す事で
「Luaでデータを加工してから保存」といった事ができる。
実行中は他のリクエストをブロックする。

#### LRU eviction

メモリ使用量が上限に達した際に、使われていないデータを追い出してメモリを確保する仕組み。
LRUとはLeast Recently Usedの略で「最近最も使われなかったもの」を表す

`maxmemory`ディレクティブでメモリ使用量の上限を設定できる。
メモリ使用量の上限に達した際のふるまいは`maxmemory-policy`ディレクティブで設定できる。
デフォルトでは、`maxmemory-policy`が`noeviction`になっており当該機能は使われない。

#### transactions 

コマンドをまとめて発行する仕組み（別名パイプライン）
RDBMSとは異なり、ロールバックやロックは出来ない。
１連の処理の原子性は保証される（全て実行 or 全て未実行）が、データの整合性は担保されない。
コマンドをまとめて発行するのでラウンドトリップが少なくなりパフォーマンスが向上する

`MULTI`コマンドでトランザクションを開始する
`EXEC`コマンドでトランザクション内のコマンドをまとめて実行する。

#### persistence

Redisを再起動してもデータを維持するための仕組み。
`RDB`と`AOF`と呼ばれる２つの方式がある

| 方式 | 説明 |
|---|---|---|
| RDB | 指定した間隔でスナップショットを作成する |
| AOF | すべての書き込みコマンドを記録する。起動時にログをリプレイし、元のデータを再構成する。 |

##### `RDB`に関連する設定（ディレクティブ）

| ディレクティブ | 説明 | デフォルト値 |
|---|---|---|
| save | 保存間隔 | save 900 1<br>save 300 10<br>save 60 10000|
| dbfilename | ファイル名 |dump.rdb|
| dir  | 保存先ディレクトリ(AOFと共通)|./|
| rdbcompression  | 圧縮を行うか| yes |

##### `AOF`に関連する設定（ディレクティブ）

| ディレクティブ | 説明 | デフォルト値 |
|---|---|---|
| appendonly  | AOFを有効にするかどうか | no |
| appendfilename  | ファイル名 | no |
| dir  | 保存先ディレクトリ(RDBと共通)|./|
| appendfsync   | ディスクにフラッシュするタイミング | everysec(毎秒)|
| auto-aof-rewrite-percentage  | AOFファイルのサイズが何%増えたらAOFファイルの再構築を行うか | 100 |
| auto-aof-rewrite-min-size  | AOFファイルが指定サイズ以下の場合は再構築を行わない。 | 64mb |


#### Redis Sentinel 

Redisサーバの死活監視/通知および自動フェイルオーバー機能を提供する仕組み

#### Redis Cluster

複数のRedisサーバ(ノードと呼ぶ)にデータを分散させる仕組み（シャーディング）
データのキー値に応じて格納先のノードを振り分ける。
同じデータが複数のノードに格納される事はない。

# とりあえず触ってみる

ひとまずdockerでredisサーバを立てる。
[Docker Hub](https://hub.docker.com/_/redis)

```
docker run --name some-redis -d redis
```

サーバを立てたものの、接続するためのクライアントが無い事に気づいた

調べると[redis-cli](https://redis.io/topics/rediscli)というツールがあるらしい
他にもGUIのツールなどもあるらしいが、まずは、スタンダードっぽいredis-cliを使ってみる。

Docker Hubを眺めていると、上記で立てたコンテナに対して
redis-cliで接続できる起動方法を見つけたのでこれを試す。

```
docker run -it --link some-redis:redis --rm redis redis-cli -h redis -p 6379
```

こんな感じでプロンプトが表示された
ここからredisのコマンドを叩くことでデータを追加したり削除したりできそう。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/71575/4fa893fb-057a-23e4-4df1-7f8b2c497d55.png)
コマンドリファレンスに従っていくつか叩いてみる
[コマンドリファレンス — redis 2.0.3 documentation](http://redis.shibu.jp/commandreference/)

まずは`INFO`
サーバの情報が表示される。
redisのバージョンは5.0.3らしい。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/71575/e36834e1-22ab-0d28-4865-f15daaa040c7.png)

追加、更新、削除をやってみる。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/71575/6f835d3a-622d-da05-2ef2-d6d091af0c20.png)

値に`有効期限`を持たせて、有効期限を超えた値は自動的に削除させたりできるらしい。

# PHPからRedisサーバに接続してみる

phpからredisを扱うためのライブラリは多数ある
[PHP Bindings](https://redis.io/clients#php)

Laravelでは[predis/predis](https://packagist.org/packages/predis/predis)パッケージと[Redis PHP拡張](https://github.com/phpredis/phpredis)をサポートしている。
PHP拡張の方が処理が高速だが、インストールが複雑。
今回は前者のpredisパッケージからredisを触ってみる。

以下が、試しに書いてみたPHPスクリプトです。
いくつかのデータ構造を登録して取得してvar_dumpで出力しています。
出力はvar_dumpの下の行にコメント記載しています。

```php
<?php
require_once 'vendor/autoload.php';

// Redisサーバに接続
$redis = new Predis\Client([
    'host' => 'some-redis',
    'port' => 6379,
]);

// redisの全データ削除
$redis->flushdb();

/*
 * データ構造：文字列
 */
$redis->set('my-key', 'my-value');
var_dump($redis->get('my-key'));
// 出力： 'my-value'

/*
 * データ構造：リスト
 */
$redis->rpush('my-list', 'first');
$redis->rpush('my-list', 'second');
$redis->rpush('my-list', 'third');
var_dump($redis->llen('my-list'));
// 出力： 3
var_dump($redis->lrange('my-list', 0, 2));
// 出力： 'first', 'second', 'third'

/*
 * データ構造：セット
 */
$redis->sadd('my-set', 'alpha');
$redis->sadd('my-set', 'bravo');
$redis->sadd('my-set', 'charlie');
var_dump($redis->smembers('my-set'));
// 出力： 'alpha', 'charlie', 'bravo'

/*
 * データ構造：ハッシュ
 */
$redis->hmset('my-hash', [
    'title'        => 'redisを分かった気になる本',
    'author'       => 'me',
    'publisher'    => 'sya-syu-syo',
    'publish-date' => '2018-01-01 01:01:01',
]);
var_dump($redis->hmget('my-hash', [
    'title',
    'author',
    'publisher'
]));
// 出力： 'redisを分かった気になる本', 'me', 'sya-syu-syo'

/**
 * Pipelineを使用して、複数のコマンドをまとめて発行する
 */
$replies = $redis->pipeline(function($pipe) {
    $pipe->incr('my-counter');
    $pipe->incr('my-counter');
    $pipe->incr('my-counter');
    $pipe->incr('my-counter');
    $pipe->incr('my-counter');
});
var_dump($redis->get('my-counter'));
// 出力： 5
```

# まとめ

Redisってキャッシュサーバ以外にも使えるんだぜ！！！