redis-rb
GitHub概要
トピックス
スター履歴
キャッシュライブラリ
redis-rb
概要
redis-rbは、RedisとRubyアプリケーション間のインターフェースを提供する公式Rubyクライアントライブラリで、RedisのAPIと1対1の対応関係を保ちながら、慣用的なRubyインターフェースを提供し、パフォーマンスに重点を置いた設計が特徴です。
詳細
redis-rb(レディス・アールビー)は、RedisとRubyアプリケーション間のインターフェースを提供する公式Rubyクライアントライブラリです。RedisのAPIと1対1の対応関係を保ちながら、慣用的なRubyインターフェースを提供することを目指しています。スレッドセーフ性、クライアント側のシャーディング、パイプライニング、そしてパフォーマンスへの執着を特徴としています。すべてのRedisコマンドの包括的なサポートを提供し、Pub/Sub機能、トランザクション(MULTI/EXEC)、Sentinelサポートによる自動フェイルオーバー、分散操作、SSL/TLS接続をサポートしています。hiredisドライバーとの統合により、大きな応答や大規模なパイプラインでの速度最適化が可能です。デフォルトでは、各Redisインスタンスはサーバーに対して1つの接続のみを持ち、この接続の使用はミューテックスによって保護されています。接続プールの機能は提供されておらず、connection_poolgemの使用が強く推奨されています。Redis 2.8以上でSentinel機能を使用する場合は自動フェイルオーバーが可能で、クラスターサポートはcompanion gem「redis-clustering」で提供されています。SSL証明書認証、設定可能なタイムアウト、再接続試行の設定、豊富なエラーハンドリングなど、プロダクション環境での使用に必要な機能を包括的にサポートしています。
メリット・デメリット
メリット
- 公式サポート: Redisの公式Rubyクライアントライブラリ
- 1対1 API対応: RedisのAPIと1対1の対応関係で学習が容易
- 高性能: hiredisドライバーサポートによる速度最適化
- スレッドセーフ: ミューテックスによる安全な並行アクセス
- パイプライニング: 複数コマンドの効率的な一括実行
- Sentinel対応: 高可用性のための自動フェイルオーバー
- 豊富な機能: Pub/Sub、トランザション、分散操作の完全サポート
デメリット
- 接続プール非提供: 別途connection_poolgemが必要
- クラスター分離: クラスター機能は別gemでの提供
- 単一接続: インスタンス毎に1接続の制限
- Ruby依存: Ruby固有の機能に強く依存した設計
- 学習コスト: 高度な機能(Sentinel、分散等)の習得が必要
主要リンク
書き方の例
インストールと基本接続
# Gemfile
gem 'redis'
# または hiredis サポート付き(高速化)
gem 'redis'
gem 'hiredis-client'
# インストール
# bundle install
# 基本接続
require 'redis'
redis = Redis.new
# 詳細設定での接続
redis = Redis.new(host: "10.0.1.1", port: 6380, db: 15)
# URL接続
redis = Redis.new(url: "redis://:[email protected]:6380/15")
# SSL接続
redis = Redis.new(url: "rediss://10.0.1.1:6380/15")
# 接続テスト
puts redis.ping # => PONG
基本的なキー・バリュー操作
require 'redis'
redis = Redis.new
# 文字列の設定と取得
redis.set('best_car_ever', 'Tesla Model S')
car = redis.get('best_car_ever')
puts car # => "Tesla Model S"
# 期限付きキー設定
redis.setex('session:123', 3600, 'session_data') # 1時間後に期限切れ
# 数値の操作
redis.set('counter', 0)
count = redis.incr('counter')
puts count # => 1
# 複数キーの一括操作
redis.mset('key1', 'value1', 'key2', 'value2', 'key3', 'value3')
values = redis.mget('key1', 'key2', 'key3')
puts values # => ["value1", "value2", "value3"]
# キーの存在確認
if redis.exists('best_car_ever')
puts 'Key exists'
end
# キーの削除
redis.del('best_car_ever')
リスト・セット・ハッシュ操作
# リスト操作
redis.lpush('tasks', 'task1', 'task2', 'task3')
redis.rpush('tasks', 'task4')
# リストの内容を取得
tasks = redis.lrange('tasks', 0, -1)
puts tasks # => ["task3", "task2", "task1", "task4"]
# リストから要素を取得
task = redis.lpop('tasks')
puts task # => "task3"
# セット操作
redis.sadd('tags', 'ruby', 'redis', 'cache')
members = redis.smembers('tags')
puts members # => ["ruby", "redis", "cache"]
# セットの演算
redis.sadd('set1', 'a', 'b', 'c')
redis.sadd('set2', 'b', 'c', 'd')
intersection = redis.sinter('set1', 'set2')
puts intersection # => ["b", "c"]
# ハッシュ操作
redis.hset('user:1', 'name', 'John')
redis.hset('user:1', 'email', '[email protected]')
redis.hset('user:1', 'age', 30)
# ハッシュの一括設定
redis.hmset('user:2', 'name', 'Alice', 'email', '[email protected]', 'age', 25)
user_data = redis.hgetall('user:1')
puts user_data # => {"name"=>"John", "email"=>"john@example.com", "age"=>"30"}
name = redis.hget('user:1', 'name')
puts name # => "John"
パイプライニング
# パイプラインで複数コマンドを効率的に実行
redis.pipelined do |pipe|
pipe.set('key1', 'value1')
pipe.set('key2', 'value2')
pipe.get('key1')
pipe.get('key2')
pipe.incr('counter')
end
# Futureオブジェクトの利用
redis.pipelined do |pipe|
future_value1 = pipe.get('key1')
future_value2 = pipe.get('key2')
# パイプライン実行後に値を取得
puts future_value1.value # => "value1"
puts future_value2.value # => "value2"
end
# トランザクション付きパイプライン
redis.multi do |multi|
multi.set('account:1:balance', 100)
multi.set('account:2:balance', 200)
multi.decrby('account:1:balance', 50)
multi.incrby('account:2:balance', 50)
end
Pub/Sub機能
require 'redis'
require 'thread'
# Publisher側
def publisher(redis)
sleep(1) # Subscriberの準備を待つ
5.times do |i|
redis.publish('notifications', "Message #{i}")
sleep(1)
end
end
# Subscriber側
def subscriber(redis)
redis.subscribe('notifications') do |on|
on.subscribe do |channel, subscriptions|
puts "Subscribed to ##{channel} (#{subscriptions} subscriptions)"
end
on.message do |channel, message|
puts "Received message: #{message}"
end
on.unsubscribe do |channel, subscriptions|
puts "Unsubscribed from ##{channel} (#{subscriptions} subscriptions)"
end
end
end
# 別スレッドでPublisher実行
redis_pub = Redis.new
redis_sub = Redis.new
pub_thread = Thread.new { publisher(redis_pub) }
# Subscriber実行
subscriber(redis_sub)
pub_thread.join
接続プールの使用
require 'redis'
require 'connection_pool'
# 接続プールの設定
pool = ConnectionPool.new(size: 5, timeout: 5) do
Redis.new(host: 'localhost', port: 6379)
end
# 接続プールの使用
pool.with do |redis|
redis.set('pooled_key', 'pooled_value')
value = redis.get('pooled_key')
puts value # => "pooled_value"
end
# マルチスレッド環境での使用
threads = []
10.times do |i|
threads << Thread.new do
pool.with do |redis|
redis.incr('thread_counter')
count = redis.get('thread_counter')
puts "Thread #{i}: counter = #{count}"
end
end
end
threads.each(&:join)
エラーハンドリング
require 'redis'
redis = Redis.new
begin
redis.set('test_key', 'test_value')
value = redis.get('test_key')
puts value
rescue Redis::CannotConnectError
puts "Redis connection failed"
rescue Redis::TimeoutError
puts "Redis operation timed out"
rescue Redis::CommandError => e
puts "Redis command error: #{e.message}"
rescue Redis::BaseError => e
puts "Redis error: #{e.message}"
end
# 再接続設定
redis = Redis.new(
reconnect_attempts: 3,
reconnect_delay: 0.5,
reconnect_delay_max: 5.0
)
# 特定のブロックで再接続を無効化
redis.without_reconnect do
# このブロック内では再接続しない
redis.ping
end
Sentinel設定
require 'redis'
# Sentinel設定
redis = Redis.new(
url: "redis://mymaster",
sentinels: [
{ host: "127.0.0.1", port: 26379 },
{ host: "127.0.0.1", port: 26380 },
{ host: "127.0.0.1", port: 26381 }
],
role: :master
)
# スレーブ接続
redis_slave = Redis.new(
url: "redis://mymaster",
sentinels: [
{ host: "127.0.0.1", port: 26379 },
{ host: "127.0.0.1", port: 26380 },
{ host: "127.0.0.1", port: 26381 }
],
role: :slave
)
# 操作
redis.set('sentinel_key', 'sentinel_value')
value = redis_slave.get('sentinel_key')
puts value # => "sentinel_value"
分散Redis操作
require 'redis'
require 'redis/distributed'
# 分散Redis設定
distributed = Redis::Distributed.new([
"redis://127.0.0.1:6379/0",
"redis://127.0.0.1:6380/0",
"redis://127.0.0.1:6381/0"
])
# 分散操作(一貫したハッシュ化でノード選択)
distributed.set('user:1', 'Alice') # ノード1に配置
distributed.set('user:2', 'Bob') # ノード2に配置
distributed.set('user:3', 'Charlie') # ノード3に配置
# データ取得
user1 = distributed.get('user:1')
user2 = distributed.get('user:2')
puts "#{user1}, #{user2}" # => "Alice, Bob"
# すべてのノードの情報取得
distributed.nodes.each_with_index do |node, index|
puts "Node #{index}: #{node.info['redis_version']}"
end
SSL/TLS接続
require 'redis'
# SSL設定
redis = Redis.new(
host: 'localhost',
port: 6380,
ssl: true,
ssl_params: {
cert_file: '/path/to/client.crt',
key_file: '/path/to/client.key',
ca_file: '/path/to/ca.crt',
verify_mode: OpenSSL::SSL::VERIFY_PEER
}
)
# または
redis = Redis.new(url: "rediss://localhost:6380")
# 接続テスト
puts redis.ping # => PONG
高度なキャッシングパターン
require 'redis'
require 'json'
class CacheManager
def initialize(redis_client)
@redis = redis_client
end
def cached_method(key, expiration = 3600)
# キャッシュから取得を試行
cached_value = @redis.get(key)
return JSON.parse(cached_value) if cached_value
# キャッシュにない場合はブロックを実行
result = yield
# 結果をキャッシュに保存
@redis.setex(key, expiration, JSON.dump(result))
result
end
def invalidate_pattern(pattern)
keys = @redis.keys(pattern)
@redis.del(keys) if keys.any?
end
end
# 使用例
redis = Redis.new
cache = CacheManager.new(redis)
# 重い処理をキャッシュ
result = cache.cached_method('expensive_calc', 1800) do
# 重い計算処理
sleep(2)
{ result: (1..100000).sum, timestamp: Time.now }
end
puts result # 初回は2秒かかる
result = cache.cached_method('expensive_calc') { nil } # 2回目は即座に返る
# パターンマッチでキャッシュ無効化
cache.invalidate_pattern('user:*')