想在Rails中使用Redis进行互斥控制
给定背景
在用户登录时,已经实现了一项更新大量数据的处理。这个更新处理可以保持最新状态,只需要大约每隔几个小时执行一次。然而,除了登录时,还可能同时从多个地方访问到更新处理,为了防止并行执行,已经实现了排他控制。然而,由于更新处理的错误日志等因素,有人怀疑排他控制是否运作正常。因此,决定进行调查和改进。
环境
-
- Ruby 3.1
- Rails 7.0.2
传统的互斥控制
在过去,我们使用了Rails.cache来进行排他控制。Rails.cache已经配置为使用Redis。
# 更新処理処理クラス
# 更新処理を実行する際に排他制御する
# valueの値がtrueならロック中、falseなら解除中とする
# ロックする
key = 'locking_id:1'
Rails.cache.write(key, true)
# 〜〜更新処理〜〜
# ロックを解除する
Rails.cache.write(key, false)
# 更新処理を呼び出す前にロックが掛かってないか確認する
# ロックがかかっていればtrue, 解除中ならfalse, 取得できないときはnilなのでfalseを返すようにする
key = 'locking_id:1'
is_lock = Rails.cache.read(key) || false
unless is_lock
# 〜〜更新処理クラスを呼び出す〜〜
end
起初看起来并没有问题,但在确认互斥状态并进行加锁之前,在代码中有几十行的间隔,几乎同时执行的两次访问等,在实际情况下,重复的更新处理正在通过这个间隔执行多次。
优化的互斥控制
为了同时进行确认和独占状态的更新,我们决定使用Redis的SETNX方法。
使用SETNX方法
-
- Set not exists の略で、keyが存在してなければその値をセットするメソッド。
-
- セットすることができたら1、セットできなかったら0を返してくれる
-
- 現在はSetメソッドにあるnxオプションを使っても同じことができる
- 参考:https://redis.io/commands/setnx/
实施后的结果
# 更新処理を実行する際に排他制御する
# Redisと接続する
redis = Redis.new(...)
# ロックする
key = 'locking_id:1'
is_set = redis.set(key, true, nx:true)
# セットできなかったらロックがかかっているので更新処理せずに処理を抜ける
return unless is_set
# 〜〜更新処理〜〜
# ロックを解除する
# 解除する際はデータを削除しておく
redis.del(key)
通过这样的实现,确认和锁定同时进行,因此即使在几乎同时被访问时也能实现排他控制。
在Rails的缓存中不可用吗?
我最初考虑是否可以通过Rails.cache的方法来实现与传统方式相同的功能,因此尝试使用Rails.cache.write并传递选项。但试验下来发现,即使这样写并尝试使用nx选项,功能也无法正常运行。
# 常にセットされておりnxオプションが機能してない
is_lock = Rails.cache.write(key, value, nx: true)
结束
在使用Redis进行排他控制时,可以使用nx选项!