始めに
Ruby on Rails では、Redis を利用して一時的に値を保持するケースが多々あると思います。(たとえば、カート情報の管理やカウント処理など)
もし Redis の String 型のバリューのみで要件を満たせるのであれば、Railsのキャッシュ機構(低レベルキャッシュ)を利用するだけで、Redis を簡単に扱うことができます。
Rails の cache_store
に :redis_cache_store
が設定されている前提で、以下のように Redis を利用できます。
# 取得
Rails.cache.read(key)
# 保存
Rails.cache.write(key, value)
# 削除
Rails.cache.delete(key)
しかし、もしより複雑なデータ型(たとえば、List 型、Set 型、Sorted Set 型など)を Redis で扱いたい場合、利用したいRedisコマンドの種類が増えるため、Rails のキャッシュ機構だけでは対応が難しくなります。
そんなときに役立つのが、redis-rb
gemです。
The client does not provide connection pooling. Each Redis instance has one and only one connection to the server, and use of this connection is protected by a mutex.
As such it is heavilly recommended to use the connection_pool gem, e.g.
ただ、上記のGithubのreadmeにもあるように、このgemコネクションプールを提供していません。
そのため、スレッドセーフな実装のためにconnection_pool
Gemの使用が強く推奨されています。
この記事では、コネクションプール導入したredis-rb
gemの実装方法について解説します!
ゴール
この記事の最終目標は、以下のように、RedisConnectionPool.client
というクラスを作成し、簡単に Redis コマンドを実行できるようにすることです。
# Sorted Set型に追加
RedisConnectionPool.client.zadd(key, score, member)
# 有効期限を設定
RedisConnectionPool.client.expire(key, ttl)
上記の使用例になります。
RedisConnectionPool.client.{Redisコマンドのメソッド}
というように、Redisコマンドのメソッドを呼ぶだけで、コネクションプールを意識せずにRedisコマンドを自由に使用できます。
実装方法
まず、redis-rb
gem をインストールします。
$ gem install redis
ちなみに、redis-rb
gem は connection_pool
gem に依存しているため、redis-rb
のインストール時に connection_pool
も自動的にインストールされる想定です。
次に、RedisConnectionPool
クラスを作成します(今回はlib/redis_connection_pool.rb を作成)。
class RedisConnectionPool
class << self
def client
@client ||= ConnectionPool::Wrapper.new(size:) { Redis.new(url:, password:, timeout: 5) }
end
private
def size
ENV.fetch('RAILS_MAX_THREADS', 5)
end
def url
ENV.fetch('REDIS_URL', 'redis://localhost:6379/1')
end
def password
ENV.fetch('REDIS_PASSWORD', nil)
end
end
end
このクラスは Redis への接続を管理し、接続プールを使用して複数のスレッド間で接続を共有します。
client
メソッドは、Redis クライアントをラップした ConnectionPool::Wrapper
オブジェクトを返し、スレッドセーフに Redis を操作できるようにします。
実装は以上です。
(補足)パフォーマンスの改善
It's not high-performance so you'll want to port your performance sensitive code to use with as soon as possible.
connection_pool
Gemのreadmeに記載があるように、ConnectionPool::Wrapper
は高パフォーマンスを求める場面では最適ではありません。
よりパフォーマンスを求める場合は、with
メソッドを利用して、ConnectionPool を直接使用する方法が推奨されています。
RedisConnectionPool.with do |client|
# Sorted Set型に追加
client.zadd(key, score, member)
# 有効期限を設定
client.expire(key, ttl)
end
with
メソッドを使用するようにRedisConnectionPool.client
を改良した場合の使用イメージは上記のとおりです。
複数の Redis コマンドをまとめて実行する場合、with
メソッドを使うことで、1つの接続で効率的に処理できます。
なぜ ConnectionPool::Wrapper
を使用するのか
この記事では、あえて ConnectionPool::Wrapper
を使用しました。その理由は以下の通りです。
- 既存の Redis 利用方法からの変更が少なく、運用が容易
-
with
メソッドを使うと、毎回ブロックを使用する必要があり、コードの可読性が低下する可能性がある - 複数の Redis コマンドを一度に使用するケースが少なく今のところ発生しなさそう
with
メソッド使用のためのRedisConnectionPool
クラス改良方法
とはいえ、チームによっては with
メソッドの使用が適している場合もあるため、以下に with
メソッドを使用する場合の RedisConnectionPool
クラスも示しておきます。
class RedisConnectionPool
class << self
delegate :with, to: :pool
private
def pool
@pool ||= ConnectionPool.new(size:) { Redis.new(url:, password:, timeout: 5) }
end
def size
ENV.fetch('RAILS_MAX_THREADS', 5)
end
def url
ENV.fetch('REDIS_URL', 'redis://localhost:6379/1')
end
def password
ENV.fetch('REDIS_PASSWORD', nil)
end
end
end
まとめ
Redis の利便性を最大限に活用するためには、redis-rb
と connection_pool
を適切に組み合わせることが重要です。
特に、複雑なデータ型やスレッドセーフな実装が必要な場合、接続プールを導入することでパフォーマンスと安定性を確保できます。
その上、ConnectionPool::Wrapper
を利用すれば、既存のコードベースに対する影響を最小限に抑えながら、手軽に接続プールを導入できます。
パフォーマンスを重視する場合は、with
メソッドを使用して、より効率的な処理が可能です。
この記事を参考に、ぜひあなたのプロジェクトで Redis をより効率的に活用してください!