Ruby on Railsでコネクションプールを導入したredis-rbの導入方法

# 始めに

Ruby on Rails では、Redis を利用して一時的に値を保持するケースが多々あると思います。(たとえば、カート情報の管理やカウント処理など)
もし Redis の String 型のバリューのみで要件を満たせるのであれば、[Railsのキャッシュ機構(低レベルキャッシュ)](https://railsguides.jp/caching_with_rails.html#%E4%BD%8E%E3%83%AC%E3%83%99%E3%83%AB%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5)を利用するだけで、Redis を簡単に扱うことができます。

Rails の `cache_store` に `:redis_cache_store` が設定されている前提で、以下のように Redis を利用できます。
```ruby
# 取得
Rails.cache.read(key)

# 保存
Rails.cache.write(key, value)

# 削除
Rails.cache.delete(key)
```
しかし、もしより複雑なデータ型（たとえば、List 型、Set 型、Sorted Set 型など）を Redis で扱いたい場合、利用したいRedisコマンドの種類が増えるため、Rails のキャッシュ機構だけでは対応が難しくなります。

そんなときに役立つのが、[`redis-rb`gem](https://github.com/redis/redis-rb)です。
>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](https://github.com/redis/redis-rb)にもあるように、このgemコネクションプールを提供していません。
そのため、スレッドセーフな実装のために[`connection_pool`Gem](https://github.com/mperham/connection_pool)の使用が強く推奨されています。

この記事では、コネクションプール導入した`redis-rb`gemの実装方法について解説します!

# ゴール
この記事の最終目標は、以下のように、`RedisConnectionPool.client` というクラスを作成し、簡単に Redis コマンドを実行できるようにすることです。
```ruby
# Sorted Set型に追加
RedisConnectionPool.client.zadd(key, score, member)

# 有効期限を設定
RedisConnectionPool.client.expire(key, ttl)
```
上記の使用例になります。
`RedisConnectionPool.client.{Redisコマンドのメソッド}`というように、Redisコマンドのメソッドを呼ぶだけで、コネクションプールを意識せずにRedisコマンドを自由に使用できます。

# 実装方法
まず、`redis-rb` gem をインストールします。
```shell
$ gem install redis
```
ちなみに、`redis-rb` gem は `connection_pool` gem に依存しているため、`redis-rb` のインストール時に `connection_pool` も自動的にインストールされる想定です。

次に、`RedisConnectionPool` クラスを作成します（今回はlib/redis_connection_pool.rb を作成）。
```ruby
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](https://github.com/mperham/connection_pool?tab=readme-ov-file#migrating-to-a-connection-pool)に記載があるように、`ConnectionPool::Wrapper` は高パフォーマンスを求める場面では最適ではありません。
よりパフォーマンスを求める場合は、`with` メソッドを利用して、ConnectionPool を直接使用する方法が推奨されています。
```ruby
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` クラスも示しておきます。

```ruby
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 をより効率的に活用してください！