[关闭]
@elibinary 2017-11-12T06:17:04.000000Z 字数 2536 阅读 860

基于 Redis 的 locking 实现

DB


基于 Redis 的 lock 正是基于其单进程单线程及其原子操作来实现的。对于 Redis 来说,同一时刻只可能有一个命令正在操作,也就是说在 Redis 的层面上,请求是串行进行的。

SETNX

SETNX 是 Redis 的一个命令,完整形式是这样的:

  1. SETNX key value

它是 ‘set if not exists’ 的简写,正如其描述一样 SETNX 的作用是给 key 赋值,并且仅当目标 key 不存在时才能成功并返回 1

locking 实现主要就是基于 Redis 的这个命令

其过程是这样的:

  1. 首先 ClientA 需要获取锁,然后 SETNX lock_name time_stamp 返回 1 ,加锁成功
  2. 此时 ClientB 想要获取该锁,尝试 SETNX 结果因为 key 已存在 返回 0,知道锁正被占用然后选择等待或返回
  3. ClientA 做完要做的事情后,使用 DEL 命令删除 lock_name 来完成释放锁的操作
  4. 此时该锁可被其他 Client 抢占

看起来很完美,但其实这中间存在着很多细节上的问题,大致分析一下:

下面来看下 redis-objects 这个 gem 是如何实现这个 locking 的

Locking

看源码实现很简单,一共也就几十行代码

  1. # Get the lock and execute the code block. Any other code that needs the lock
  2. # (on any server) will spin waiting for the lock up to the :timeout
  3. # that was specified when the lock was defined.
  4. def lock(&block)
  5. expiration = nil
  6. try_until_timeout do
  7. expiration = generate_expiration
  8. # Use the expiration as the value of the lock.
  9. break if redis.setnx(key, expiration)
  10. # Lock is being held. Now check to see if it's expired (if we're using
  11. # lock expiration).
  12. # See "Handling Deadlocks" section on http://redis.io/commands/setnx
  13. if !@options[:expiration].nil?
  14. old_expiration = redis.get(key).to_f
  15. if old_expiration < Time.now.to_f
  16. # If it's expired, use GETSET to update it.
  17. expiration = generate_expiration
  18. old_expiration = redis.getset(key, expiration).to_f
  19. # Since GETSET returns the old value of the lock, if the old expiration
  20. # is still in the past, we know no one else has expired the locked
  21. # and we now have it.
  22. break if old_expiration < Time.now.to_f
  23. end
  24. end
  25. end
  26. begin
  27. yield
  28. ensure
  29. # We need to be careful when cleaning up the lock key. If we took a really long
  30. # time for some reason, and the lock expired, someone else may have it, and
  31. # it's not safe for us to remove it. Check how much time has passed since we
  32. # wrote the lock key and only delete it if it hasn't expired (or we're not using
  33. # lock expiration)
  34. if @options[:expiration].nil? || expiration > Time.now.to_f
  35. redis.del(key)
  36. end
  37. end
  38. end

先来看它获取锁的过程,首先会在超时时间内不断地循环尝试获取锁

  1. def try_until_timeout
  2. if @options[:timeout] == 0
  3. yield
  4. else
  5. start = Time.now
  6. while Time.now - start < @options[:timeout]
  7. yield
  8. sleep 0.1
  9. end
  10. end
  11. raise LockTimeout, "Timeout on lock #{key} exceeded #{@options[:timeout]} sec"
  12. end

可以看到,在超时时间内每隔 0.1 秒尝试一次

再回到 lock 方法,尝试获取锁的过程如下:

  1. 使用 SETNX 命令尝试获取锁,如果成功跳出循环往下执行真正的逻辑
  2. 如果失败就去校验锁的过期时间,如果没有过期就等待进入下一轮的尝试
  3. 如果检查到锁已过期,就使用 GETSET 命令给 lock_key 赋值并返回原值,看其是否超过期,没有就等待下一轮尝试
  4. 如果过期就拿到锁,可以开始干自己的事了
  5. 做完了自己的事后,在释放锁的操作前会前检查下是否已经过了自己获取锁时定下的过期时间,如果已经超时就不进行释放锁的操作
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注