PHP
Redis

今更、Redisを触りながら学ぶ

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

Redisとは?

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

Redisのアーキテクチャ

Redis ServerRedis 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

Redisの主な機能

大まかに7つある

名称 説明
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-policynoevictionになっており当該機能は使われない。

transactions

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

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

persistence

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

方式 説明
RDB 指定した間隔でスナップショットを作成する
AOF すべての書き込みコマンドを記録する。起動時にログをリプレイし、元のデータを再構成する。
RDBに関連する設定(ディレクティブ)
ディレクティブ 説明 デフォルト値
save 保存間隔 save 900 1
save 300 10
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

docker run --name some-redis -d redis

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

調べるとredis-cliというツールがあるらしい
他にも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
コマンドリファレンスに従っていくつか叩いてみる
コマンドリファレンス — redis 2.0.3 documentation

まずはINFO
サーバの情報が表示される。
redisのバージョンは5.0.3らしい。
image.png

追加、更新、削除をやってみる。
image.png

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

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

phpからredisを扱うためのライブラリは多数ある
PHP Bindings

Laravelではpredis/predisパッケージとRedis PHP拡張をサポートしている。
PHP拡張の方が処理が高速だが、インストールが複雑。
今回は前者のpredisパッケージからredisを触ってみる。

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

<?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ってキャッシュサーバ以外にも使えるんだぜ!!!