LoginSignup
0
0

More than 5 years have passed since last update.

redlockでレースコンディションを解消する

Posted at

背景

Rails version             5.0.3
Ruby version              2.3.3-p222 (x86_64-darwin18)
RubyGems version          2.5.2
Rack version              2.0.3
JavaScript Runtime        therubyracer (V8)
Database adapter          mysql2spatial

Sidekiqのワーカーが並列実行する時に、レコードが重複に生成されて、そのレースコンディションを解消するために、Redlockを使ってみました。

前提

  • 必要な概念
    • Segment
    • Location
    • Place
    • User
segemnt : location => m : n
place : location => 1 : n
user : location => 1 : n
segment : user => m : n
  • segment作成
    • segment workerの中でplace_idとuser_idを使って、locationを生成する 

起因

同じplaceを使って、同時に複数のsegmentを作成しようとすると、複数のlocationを作られてしまう。同じplace_idとuser_idに対して、唯一のlocationを作られるのは望ましいですが、そうではなかった。

検討

原因はDatabaseのlocationテーブルにそういう制御を入れなかった。

add_index :locations, %[place_id user_id]

もし↑の制御があれば、レコードを作成する時に、テーブルをロックされるので、重複作成はないはず。

対応

案A: Databae layer

  • 重複レコードを削除し、インデックスを貼る

既に重複のレコードを作られてしまって、運用も始めているため、削除するなら、全てのリレーションを直さないといけないので、ほぼ不可能です!

削除しないとインデックスを貼れないので、この案は見送り!

案B: Application layer

  • pessimistic lockを使って、レースコンディションを解消

ちょうどredisを使っているので、redisをそのまま使って制御したいので、PatrickTulskie/redis-lock: Pessimistic locking for ruby redis を使ってみた

実装的には意外に簡単でした。


def lock_manager
    Redlock::Client.new(
      [
        ENV['REDIS_ENDPOINT']
      ],
      retry_count: 3,
      retry_delay: 1000, # milliseconds
      retry_jitter: 50, # milliseconds
      redis_timeout: 5 # seconds
    )
end

lock_manager.lock!(resource, 4000) do
  find_or_create_location(user_id, place_id, feature)
end
  • 4000
    • 4秒以内lockを取れなかったら、タイムアウトする
  • retry_count: 3
    • lockを取るまで、三回リトライする
  • redis_timeout: 5 # seconds
    • redisとの接続は5秒タイムアウトする

感想

初期にDatabaseにインデックスをちゃんと追加すればこんなことが起きないはずです。

DBの仕事をDBに任せるべき。アプリケーションレイヤで頑張るとちょっと大変!

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