一尘不染

Ruby-具有到期实现的基于Redis的互斥锁

redis

我正在尝试使用Redis实现基于内存的多进程共享互斥体,该互斥体支持超时。

我需要互斥锁是非阻塞的,这意味着我只需要能够知道是否能够获取互斥锁,如果不能,则只需继续执行后备代码即可。

遵循以下原则:

if lock('my_lock_key', timeout: 1.minute)
  # Do some job
else
  # exit
end

一个 未到期的互斥 可以使用Redis的的实现setnx mutex 1

if redis.setnx('#{mutex}', '1')
  # Do some job
  redis.delete('#{mutex}')
else
  # exit
end

但是,如果我需要具有超时机制的互斥锁(为了避免在redis.delete命令之前红宝石代码失败,导致互斥锁被永久锁定的情况,例如,但并非仅出于此原因)。

做这样的事情显然是行不通的:

redis.multi do  
  redis.setnx('#{mutex}', '1')
  redis.expire('#{mutex}', key_timeout)
end

因为即使我无法设置互斥锁,我也会重新设置该互斥锁的过期时间(setnx返回0)。

自然,我本来希望拥有类似的功能setnxex,即自动使用到期时间设置键的值,但前提是该键尚不存在。不幸的是,据我所知,Redis不支持此功能。

但是,我确实找到了find renamenx key otherkey,这使您可以将一个键重命名为另一个键,前提是另一个键不存在。

我想到了这样的内容(出于演示目的,我将其完整地写下来,并且没有将其分解为方法):

result = redis.multi do
  dummy_key = "mutex:dummy:#{Time.now.to_f}#{key}"
  redis.setex dummy_key, key_timeout, 0
  redis.renamenx dummy_key, key
end
if result.length > 1 && result.second == 1
  # do some job
  redis.delete key
else
  # exit
end

在这里,我为虚拟密钥设置了到期时间,并尝试将其重命名为真实密钥(在一次交易中)。

如果renamenx操作失败,那么我们将无法获取互斥体,但不会造成任何危害:虚拟密钥将过期(可以选择添加一行代码立即将其删除),并且真实密钥的到期时间将保持不变。

如果renamenx操作成功,则我们可以获得互斥量,并且互斥量将获得所需的到期时间。

任何人都可以看到上述解决方案的任何缺陷吗?是否有针对此问题的更标准解决方案?我真的很讨厌使用外部gem来解决这个问题。


阅读 376

收藏
2020-06-20

共1个答案

一尘不染

如果您使用的是Redis
2.6+,则可以使用Lua脚本引擎更轻松地完成此操作。在Redis的文件说:

Redis脚本在定义上是事务性的,因此您可以使用Redis事务进行任何操作,还可以使用脚本进行操作,通常该脚本会更简单,更快速。

实现它很简单:

LUA_ACQUIRE = "return redis.call('setnx', KEYS[1], 1) == 1 and redis.call('expire', KEYS[1], KEYS[2]) and 1 or 0"
def lock(key, timeout = 3600)
  if redis.eval(LUA_ACQUIRE, key, timeout) == 1
    begin
      yield
    ensure
      r.del key
    end
  end
end

用法:

lock("somejob") { do_exclusive_job }
2020-06-20