【问题标题】:Ruby - Redis based mutex with expiration implementationRuby - 基于 Redis 的互斥锁,具有过期实现
【发布时间】:2013-07-24 22:57:40
【问题描述】:

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

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

类似的东西:

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

一个un-expiring mutex可以使用redis的setnx mutex 1实现:

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

但是如果我需要一个具有超时机制的互斥锁怎么办(例如,为了避免 ruby​​ 代码在redis.delete 命令之前失败,从而导致互斥锁永远被锁定,但不仅仅是因为这个原因) .

这样做显然是行不通的:

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

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

当然,我希望有类似setnxex 的东西,它会自动设置键的值和过期时间,但前提是该键不存在。不幸的是,据我所知,Redis 不支持这一点。

但我找到了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 来解决这个问题......

【问题讨论】:

    标签: ruby multithreading locking redis mutex


    【解决方案1】:

    如果您使用的是 Redis 2.6+,则可以使用 Lua 脚本引擎更简单地执行此操作。 Redis documentation 说:

    根据定义,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 }
    

    【讨论】:

      【解决方案2】:

      redis 2.6.12 开始,您可以:redis.set(key, 1, nx: true, ex: 3600) 实际上是SET key 1 NX EX 3600

      我受到 Chris 和 Mickey 解决方案的简单性的启发,并使用此代码(以及一些功能和 rspec)创建了 gem - simple_redis_lock

      def lock(key, timeout)
        if @redis.set(key, Time.now, nx: true, px: timeout)
          begin
            yield
          ensure
            release key
          end
        end
      end
      

      我探索了一些其他很棒的选择:

      1. mlanett/redis-lock
      2. PatrickTulskie/redis-lock
      3. leandromoreira/redlock-rb
      4. dv/redis-semaphore

      但是他们有太多的阻塞特性来获取锁,并且没有使用这个单一的SET KEY 1 NX EX 3600 atomic redis 语句。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-08-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-22
        • 1970-01-01
        • 2010-12-01
        相关资源
        最近更新 更多