0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RubyでRedisベースで同時実行を抑制する(悲観ロックからRedlockへの移行)

Posted at

分散ロックにおける 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 を利用する。という使い方をしています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?