背景
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に任せるべき。アプリケーションレイヤで頑張るとちょっと大変!