分散ロックにおける Redlock の実装と活用
概要
本記事では、分散ロックの実装に Redlock を採用したシステムでの実装パターンと使用例について詳しく解説します。Redlock は、Redis ベースの分散ロックライブラリで、複数のプロセスやサーバー間でのリソースの排他制御を実現します。
データベースの悲観ロックから分散ロックへの移行背景
従来のデータベースの悲観ロック(SELECT ... FOR UPDATE)では、以下の問題が発生していました:
1. スレッド占有によるリソース効率の悪化
- ロックが解放されるまでスレッドを占有してしまう
- 長時間の処理でスレッドプールが枯渇する可能性
- アプリケーションサーバーのスケーラビリティが制限される
2. DDL 実行時のメタデータロック競合
- レコード数が多いテーブルに DDL を流す際にメタデータロックと競合
- 本番環境でのスキーマ変更時に障害を発生させやすい
- 運用時のリスクが高い
3. 外部キー制約による複数レコードロック競合
- 外部キーを貼ることによって複数のレコードの悲観ロック同士が競合
- デッドロックの発生リスクが高い
- ロック順序の管理が複雑になる
これらの問題を解決するため、Redis ベースの分散ロック(Redlock)への移行を決定しました。
Redlock の設定
初期設定
# config/initializers/redlock.rb
Rails.application.config.redlock = Redlock::Client.new(
[ ENV.fetch("REDIS_URL") { "redis://127.0.0.1:6379#{ENV["PARALLEL_TEST_PROCESS"] ? "/#{ENV['TEST_ENV_NUMBER'] || 1}" : ""}" } ],
namespace: "redlock"
)
設定の特徴
-
Redis URL: 環境変数
REDIS_URLから取得、デフォルトはredis://127.0.0.1:6379 - テスト環境対応: 並列テスト実行時にデータベース番号を分離
-
ネームスペース:
"redlock"でキーを分離 -
Gemfile:
gem "redlock"でライブラリを追加
実装例:リソース更新処理での分散ロック
基本的な実装パターン
# app/services/resource_update_service.rb
class ResourceUpdateService
class ProcessingError < StandardError; end
def call
Rails.application.config.redlock.lock("ResourceUpdateService/#{resource.id}", 10000) do |locked|
if locked
ActiveRecord::Base.transaction do
# リソース更新処理
res = external_api.update!(...)
update_by_response!(res.dig("update", "result"))
end
else
raise ProcessingError
end
end
end
end
ロックキーの設計
-
ロックキー:
"ResourceUpdateService/#{resource.id}" - タイムアウト: 10 秒(10000ms)
-
エラーハンドリング: ロック取得失敗時に
ProcessingErrorを発生
ロック競合時のハンドリング戦略
1. Sidekiq ワーカーでの自動リトライ
# app/workers/resource_update_worker.rb
class ResourceUpdateWorker < ApplicationWorker
def perform(resource_id)
resource = Resource.find(resource_id)
ResourceUpdateService.new(resource).call
rescue ResourceUpdateService::ProcessingError => e
# ロック競合時はSidekiqの自動リトライに委ねる
raise
end
private
def lock_args(args)
resource_id = args.first
resource = Resource.find(resource_id)
[resource.user_id] # ユーザー単位でのロック
end
end
注意事項
Redis ベースのロックなので何らかの理由で同時実行される可能性があることに注意。
- データの揮発性
- ttl による同時実行
そのため 必ず 同時実行が NG の場合には RDS による悲観ロックなど他の方法を検討する必要がありそうです。
=> 同時実行が必要な場合は unique index に基づいてレコード作成できたかによって実行可否を判断。 前段で作成されたレコードに対して冪等に処理を実行する際に Redlock を利用する。という使い方をしています。