Redis実践的な応用テクニック

1. スロークエリ管理

Redisは5つの主要データ型をサポートしますが、O(n)のコマンド処理時間が長くなると、シングルスレッドアーキテクチャの特性上、他のコマンドがブロックされる可能性があります。Redisのシングルスレッドモデルは、同時実行時にロックを必要としない利点がありますが、コマンドの実行時間に注意が必要です。

Redisはスロークエリを記録する機能を提供しており、設定された閾値を超えるコマンドを後で確認・分析することができます。

コマンドのライフサイクル

  • クライアントからサーバーへのネットワーク転送時間
  • コマンド実行時間(スロークエリ監視の対象)
  • サーバーからクライアントへのレスポンス時間

設定項目

# 現在の設定を確認
config get slowlog-max-len=128   # スロークエリキューの最大長
config get slowlog-log-slower-than=10000 # マイクロ秒単位の閾値

# 動的設定
config set slowlog-log-slower-than 0   # すべてのコマンドを記録
config set slowlog-max-len 128
config rewrite  # ローカル設定ファイルに永続化

スロークエリの管理コマンド

slowlog get [n]  # スロークエリキューを取得
'''
各ログエントリは以下を含む:
1. ログID
2. 発生タイムスタンプ
3. コマンド実行時間
4. 実行されたコマンドとその引数
'''
slowlog len     # キューの長さを取得
slowlog reset   # キューをクリア

活用方法:Redisの応答速度が低下した場合、スロークエリログを有効にしてしばらく観察し、記録された低速コマンドを分析します。不要なコマンドは削除し、必要な低速コマンドは別の効率的な方法で実装し直すことで、パフォーマンスを改善できます。

2. パイプラインとトランザクション

Redisはネイティブのトランザクションをサポートしていませんが、パイプライン機能を使って部分的なトランザクション動作を実現できます。

パイプラインは、複数のコマンドをバッチでパッケージ化し、1回のネットワークラウンドトリップでRedisサーバーに送信して一括実行し、結果もまとめて返します。

パフォーマンス効果:1回のパイプライン操作(nコマンド) = 1回のネットワーク時間 + n回のコマンド実行時間

Pythonでの実装例

import redis

pool = redis.ConnectionPool(host='127.0.0.1', port=6379)
r = redis.Redis(connection_pool=pool)

# パイプラインを作成
pipe = r.pipeline(transaction=True)
# トランザクションを開始
pipe.multi()
pipe.set('name', 'lqz')
# ここで例外が発生する可能性のあるコード
pipe.set('role', 'nb')

pipe.execute()  # 一括実行。これより前に例外が発生した場合、すべてのコマンドは実行されず、一貫性が保証される

Redisネイティブのトランザクション操作

# トランザクションの開始と実行
multi        # トランザクション開始
set name lqz
set age 18
exec         # トランザクション実行

楽観的ロックを用いたトランザクション

# WATCHを使って競合を検出
watch age     # ageキーを監視
multi
decr age      # ageをデクリメント
exec          # 監視中にageが変更されていなければ成功、されていれば失敗

# 別のクライアントで同時実行
multi
decr age
exec          # 先に実行されると、上記のWATCH内トランザクションは失敗する(楽観的ロック)

3. パブリッシュ/サブスクライブ

パブリッシュ/サブスクライブ(Pub/Sub)はオブザーバーパターンに基づいています。パブリッシャーがメッセージを発行すると、そのチャンネルを購読しているすべてのサブスクライバーがメッセージを受信します。なお、購読後に発行されたメッセージのみ受信でき、過去のメッセージは受信できません(プロデューサー・コンシューマーモデルとは異なります)。

使用例

  • 特定のブロガーの新規投稿通知
  • 商品のセール開始通知

Redisでの実装

# メッセージを発行
publish souhu:tv "hello world"

# メッセージを購読
subscribe souhu:tv

パブリッシュ/サブスクライブ vs メッセージキュー

  • Pub/Sub:購読しているすべてのサブスクライバーがメッセージを受信する
  • メッセージキュー:1つのメッセージは1つのコンシューマーのみが取得する

4. ビットマップ操作

ビットマップは本質的に文字列ですが、個々のビットを操作できます。

基本的な操作

# 文字列 "big" を設定(ビット表現:01100010 01101001 01100111)
set hello big

# ビットの取得
getbit hello 0  # 0番目のビットを取得 → 0
getbit hello 1  # 1番目のビットを取得 → 1

# ビットの設定
setbit hello 7 1  # 7番目のビットを1に設定

# 文字列として取得
get hello

# 指定範囲内の1の数をカウント(単位:バイト、1バイト=8ビット)
bitcount key start end

活用例:日次アクティブユーザー数

ユーザーIDが数値(1, 2, 3, ...)の場合、ビットマップを使うと効率的に独立ユーザー数をカウントできます。

# 各ユーザーのログイン状態をビットマップに記録
setbit users 9999 1  # ユーザーID 9999がログイン
setbit users 1 1     # ユーザーID 1がログイン

# アクティブユーザー数をカウント
bitcount users

セット vs ビットマップ の比較

データ型ユーザーIDあたりの容量必要なストレージ量メモリ使用量
Set(セット)32ビット(整数ID想定)5千万ユーザー32ビット × 5千万 = 200MB
ビットマップ1ビット1億ユーザー1ビット × 1億 = 12.5MB

※ 独立ユーザー数が10万の場合、ビットマップは引き続き12.5MB使用するのに対し、セットは32ビット × 1万 = 4MBで済む場合もあります。ユーザー数と日々のアクティブ率に応じて最適な方法を選択してください。

5. HyperLogLog

HyperLogLogは、非常に少ないメモリで大量データの独立した要素の数を推定するアルゴリズムです。Redisでは文字列型として実装されています。

基本的な操作

# 要素を追加
pfadd key element  # 複数要素を同時に追加可能

# 独立した要素の総数を取得
pfcount key

特徴

  • セットと同様に重複除去が可能だが、個別の要素を取得することはできない
  • 数百万件の独立したユーザー統計でも約15KBのメモリしか消費しない
  • 誤差率は約0.81%(ブルームフィルターと同様の性質)
  • UUIDのように規則性のないユーザーIDの統計に有効

6. GEO(地理空間情報)

GEO機能は緯度・経度を保存し、距離計算や範囲検索を可能にします。例えば、「近くのレストラン」や「近くの人」などの機能に利用されます。

データの保存と取得

# 位置情報を保存
geoadd cities:locations 116.28 39.55 beijing
geoadd cities:locations 117.12 39.08 tianjin
geoadd cities:locations 114.29 38.02 shijiazhuang
geoadd cities:locations 118.01 39.38 tangshan
geoadd cities:locations 115.29 38.51 baoding

# 位置情報を取得
geopos cities:locations beijing  # 北京の緯度経度を取得

# 2地点間の距離を計算
geodist cities:locations beijing tianjin km

# 指定した地点から半径100km以内の都市を検索
georadiusbymember cities:locations beijing 150 km

内部実装:GEOは内部的にSorted Set(zset)として保存されています。

7. 永続化

Redisのデータはメモリに保存されていますが、永続化により非同期でディスクに保存されます。

主要な永続化方式

方式説明類似例
RDB(スナップショット)ある時点のデータ全体のバックアップMySQLのDump
AOF(ログ)すべての書き込み操作をログに記録MySQLのBinlog
ハイブリッド永続化RDB + AOFの組み合わせ(高速なリカバリとデータ安全性を両立)-

7.1 RDB永続化

3つのトリガー方式

  1. 手動:save - クライアントで実行すると、その時点のデータをRDBファイルに保存(他のコマンドをブロック)
  2. 手動:bgsave - 非同期でバックアップ(他のコマンドをブロックしない)
  3. 設定ファイルによる自動トリガー

設定例

save 900 1      # 900秒以内に1回以上の変更
save 300 10     # 300秒以内に10回以上の変更
save 60 10000   # 60秒以内に10000回以上の変更
dbfilename dump.rdb

# 実際の設定例
save 900 20
save 300 10
save 60 5
dbfilename dump.rdb

注意:RDB方式ではデータ損失の可能性があります。Redisをキャッシュとして使用する場合に適しています。データの正確性が重要な場合はAOFを使用してください。

7.2 AOF永続化

AOFはクライアントからの書き込みコマンドをログファイルに記録します。クラッシュが発生しても、ログを再生することで完全にデータを復元できます。

3つの同期戦略

戦略動作説明
always各コマンド後すぐにfsyncでディスクに書き込む最も安全だが、ディスク性能がボトルネックになる
everysec(デフォルト)1秒ごとにfsyncでバッファをディスクに書き込むバランスの取れた設定
noOSが適切と判断したタイミングで書き込む最大性能だが、データ損失リスク大

AOFのリライト戦略

書き込みが増えるとAOFファイルが肥大化するため、リライト機能で最適化します。

# 設定例
auto-aof-rewrite-min-size 64mb          # AOFファイルがこのサイズを超えたらリライト開始
auto-aof-rewrite-percentage 100         # 前回のファイルサイズからの増加率が100%以上でリライト

推奨設定

appendonly yes                       # AOFを有効化
appendfilename appendonly.aof        # ログファイル名
appendfsync everysec                 # 同期戦略(everysec推奨)
no-appendfsync-on-rewrite yes        # リライト中のAOF書き込みを許可/禁止(yesで多少のデータ損失を許容)

8. マスター/スレーブレプリケーション

Redisのマスター/スレーブレプリケーションは、データの冗長性と読み取り性能のスケーリングを実現します。1つのマスターは複数のスレーブを持つことができ、スレーブは1つのマスターのみを持ちます。

レプリケーションの仕組み

  1. スレーブがslaveofコマンドでマスターに接続し、SYNCを送信
  2. マスターはBGSAVEを実行してRDBスナップショットを作成し、スレーブに送信
  3. スレーブはRDBを適用してデータを復元
  4. マスターはそれ以降の書き込み操作をコマンドとしてスレーブに伝播

セットアップ手順

マスター設定(6379ポート)

daemonize yes
port 6379
dir "/root/redis/data"
logfile "6379.log"
save 900 20
save 300 10
save 60 5
dbfilename dump.rdb
appendonly yes
appendfilename appendonly.aof
appendfsync everysec
no-appendfsync-on-rewrite yes

スレーブ設定(6380ポート)

daemonize yes
port 6380
dir "/root/redis/data2"
logfile "6380.log"
save 900 20
save 300 10
save 60 5
dbfilename dump.rdb
appendonly yes
appendfilename appendonly.aof
appendfsync everysec
no-appendfsync-on-rewrite yes
# スレーブとして設定
slaveof 127.0.0.1 6379
slave-read-only yes

スレーブでのデータ書き込みは制限されます。infoコマンドでレプリケーションの状態を確認できます。

9. センチネル(Sentinel)

センチネルは高可用性を実現するためのシステムで、マスター障害時の自動フェイルオーバーを提供します。

一主二従のセットアップ

# マスター(6379ポート)設定
daemonize yes
pidfile /var/run/redis.pid
port 6379
dir "/root/redis/data"
logfile 6379.log

# スレーブ1(6378ポート)設定
daemonize yes
pidfile /var/run/redis2.pid
port 6378
dir "/root/redis/data1"
logfile 6378.log
slaveof 127.0.0.1 6379
slave-read-only yes

# スレーブ2(6377ポート)設定
daemonize yes
pidfile /var/run/redis3.pid
port 6377
dir "/root/redis/data2"
logfile 6377.log
slaveof 127.0.0.1 6379
slave-read-only yes

センチネルの設定(3つのセンチネルプロセス)

# sentinel_26379.conf
port 26379
daemonize yes
dir /root/redis/data
protected-mode no
bind 0.0.0.0
logfile "redis_sentinel.log"
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000

設定の説明

  • sentinel monitor:監視対象のマスター名、ホスト、ポート、フェイルオーバーに必要なセンチネルの同意数
  • down-after-milliseconds:マスターがダウンと判断されるまでの時間(ミリ秒)
  • parallel-syncs:フェイルオーバー後、同時に新しいマスターと同期できるスレーブの最大数
  • failover-timeout:フェイルオーバーに関する各種タイムアウト

Pythonでセンチネルを操作

import redis
from redis.sentinel import Sentinel

# センチネルサーバーに接続
sentinel = Sentinel([
    ('127.0.0.1', 26379),
    ('127.0.0.1', 26378),
    ('127.0.0.1', 26377)
], socket_timeout=5)

# マスターのアドレスを取得
master_addr = sentinel.discover_master('mymaster')
print(master_addr)

# スレーブのアドレスを取得
slave_addrs = sentinel.discover_slaves('mymaster')
print(slave_addrs)

# 読み書き分離の例
# master = sentinel.master_for('mymaster', socket_timeout=0.5)
# master.set('foo', 'bar')
# slave = sentinel.slave_for('mymaster', socket_timeout=0.5)
# result = slave.get('foo')

10. クラスター

Redis Clusterは、複数のRedisノードにデータを自動的に分散させるソリューションで、大規模なデータ量や高スループットを必要とする環境で使用されます。Redis 3.0で導入されました。

データ分散方式

Redis Clusterは仮想スロット方式を採用しており、16384個のスロットを使用します。

  • キーはCRC16でハッシュ化され、16383で割った余りで所属スロットが決まる
  • 各ノードはどのスロットを担当するかの情報を共有
  • クライアントは任意のノードに接続し、必要に応じて適切なノードにリダイレクトされる

クラスター構築の基本手順

# 6台のノード設定例(3マスター + 3スレーブ)
# redis-7000.conf ~ redis-7005.conf

port 7000
daemonize yes
dir "/root/redis/data/"
logfile "7000.log"
dbfilename "dump-7000.rdb"
cluster-enabled yes
cluster-config-file nodes-7000.conf
cluster-require-full-coverage yes

# 一括で設定ファイルを生成
sed 's/7000/7001/g' redis-7000.conf > redis-7001.conf
# ... 同様に7002~7005

# ノードを起動
redis-server ./redis-7000.conf
# ... 同様に7001~7005

# クラスターを作成(--cluster-replicas 1 で各マスターに1つのスレーブ)
redis-cli --cluster create --cluster-replicas 1 \
    127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \
    127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005

クラスターの管理

# クラスター情報の確認
cluster nodes   # ノード情報
cluster slots   # スロット割り当て情報
cluster info    # 全体情報

# クラスターモードで接続
redis-cli -c  # 自動リダイレクト可能

ノード追加(スケールアウト)

# 1. 新しいノードを設定して起動(例:7006、7007)
redis-server ./redis-7006.conf
redis-server ./redis-7007.conf

# 2. クラスターに追加
redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000
redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7000

# 3. スレーブの設定
redis-cli -p 7007 cluster replicate [7006のノードID]

# 4. スロットの再配分
redis-cli --cluster reshard 127.0.0.1:7000

Pythonでクラスターを操作

# pip install redis-py-cluster
from rediscluster import RedisCluster

startup_nodes = [
    {"host":"127.0.0.1", "port": "7000"},
    {"host":"127.0.0.1", "port": "7001"},
    {"host":"127.0.0.1", "port": "7002"}
]

rc = RedisCluster(startup_nodes=startup_nodes)
rc.set("foo", "bar")
print(rc.get("foo"))

タグ: redis slowlog pipeline transactions pubsub

7月22日 02:49 投稿