基于 Redis 的 locking 實(shí)現(xiàn)

基于 Redis 的 lock 正是基于其單進(jìn)程單線程及其原子操作來實(shí)現(xiàn)的。對于 Redis 來說,同一時(shí)刻只可能有一個(gè)命令正在操作,也就是說在 Redis 的層面上,請求是串行進(jìn)行的。

SETNX

SETNX 是 Redis 的一個(gè)命令,完整形式是這樣的:

SETNX key value

它是 ‘set if not exists’ 的簡寫,正如其描述一樣 SETNX 的作用是給 key 賦值,并且僅當(dāng)目標(biāo) key 不存在時(shí)才能成功并返回 1

locking 實(shí)現(xiàn)主要就是基于 Redis 的這個(gè)命令

其過程是這樣的:

  1. 首先 ClientA 需要獲取鎖,然后 SETNX lock_name time_stamp 返回 1 ,加鎖成功
  2. 此時(shí) ClientB 想要獲取該鎖,嘗試 SETNX 結(jié)果因?yàn)?key 已存在 返回 0,知道鎖正被占用然后選擇等待或返回
  3. ClientA 做完要做的事情后,使用 DEL 命令刪除 lock_name 來完成釋放鎖的操作
  4. 此時(shí)該鎖可被其他 Client 搶占

看起來很完美,但其實(shí)這中間存在著很多細(xì)節(jié)上的問題,大致分析一下:

  • 因?yàn)?ClientA 的 SETNX 和 DEL 是分開的操作,那么其中一個(gè)問題就是假如 ClientA 在加鎖之后因?yàn)槟承┰驔]有釋放掉這個(gè)鎖,就會(huì)導(dǎo)致很嚴(yán)重的后果
  • 上面的問題其實(shí)在設(shè)計(jì)時(shí)已經(jīng)被想到,解決辦法就是給這個(gè)鎖加上過期時(shí)間,鎖超過過期時(shí)間之后,其他的 Client 就可以釋放掉它,然后再通過 SETNX 搶占
  • 但是在上面的解決方法中其實(shí)有另外一個(gè)問題存在,那就是假如存在這樣的一種情況:
    • ClientA 在加鎖后沒有釋放鎖,此時(shí)有 ClientB 和 ClientC 在等待
    • 當(dāng)鎖超時(shí)時(shí),B 和 C 同時(shí)檢測到了超時(shí),然后執(zhí)行 DEL 操作,再 SETNX 搶鎖
    • 其單個(gè)操作都是原子性的,那么當(dāng) C 先 DEL 后又 SETNX 搶到了鎖這時(shí) B 執(zhí)行了 DEL 又把鎖釋放了,最后兩個(gè)都獲得了鎖
  • 還有另一種情況,假如 ClientA 并沒有死,而是在執(zhí)行一個(gè)很耗時(shí)的操作,鎖過期了也沒執(zhí)行完,然后在別人搶占了鎖之后,它完成了然后執(zhí)行了 DEL 釋放鎖的操作。。。GG

下面來看下 redis-objects 這個(gè) gem 是如何實(shí)現(xiàn)這個(gè) locking 的

Locking

看源碼實(shí)現(xiàn)很簡單,一共也就幾十行代碼

 # Get the lock and execute the code block. Any other code that needs the lock
# (on any server) will spin waiting for the lock up to the :timeout
# that was specified when the lock was defined.
def lock(&block)
  expiration = nil
  try_until_timeout do
    expiration = generate_expiration
    # Use the expiration as the value of the lock.
    break if redis.setnx(key, expiration)

    # Lock is being held.  Now check to see if it's expired (if we're using
    # lock expiration).
    # See "Handling Deadlocks" section on http://redis.io/commands/setnx
    if !@options[:expiration].nil?
      old_expiration = redis.get(key).to_f

      if old_expiration < Time.now.to_f
        # If it's expired, use GETSET to update it.
        expiration = generate_expiration
        old_expiration = redis.getset(key, expiration).to_f

        # Since GETSET returns the old value of the lock, if the old expiration
        # is still in the past, we know no one else has expired the locked
        # and we now have it.
        break if old_expiration < Time.now.to_f
      end
    end
  end
  begin
    yield
  ensure
    # We need to be careful when cleaning up the lock key.  If we took a really long
    # time for some reason, and the lock expired, someone else may have it, and
    # it's not safe for us to remove it.  Check how much time has passed since we
    # wrote the lock key and only delete it if it hasn't expired (or we're not using
    # lock expiration)
    if @options[:expiration].nil? || expiration > Time.now.to_f
      redis.del(key)
    end
  end
end

先來看它獲取鎖的過程,首先會(huì)在超時(shí)時(shí)間內(nèi)不斷地循環(huán)嘗試獲取鎖

def try_until_timeout
  if @options[:timeout] == 0
    yield
  else
    start = Time.now
    while Time.now - start < @options[:timeout]
      yield
      sleep 0.1
    end
  end
  raise LockTimeout, "Timeout on lock #{key} exceeded #{@options[:timeout]} sec"
end

可以看到,在超時(shí)時(shí)間內(nèi)每隔 0.1 秒嘗試一次

再回到 lock 方法,嘗試獲取鎖的過程如下:

  1. 使用 SETNX 命令嘗試獲取鎖,如果成功跳出循環(huán)往下執(zhí)行真正的邏輯
  2. 如果失敗就去校驗(yàn)鎖的過期時(shí)間,如果沒有過期就等待進(jìn)入下一輪的嘗試
  3. 如果檢查到鎖已過期,就使用 GETSET 命令給 lock_key 賦值并返回原值,看其是否超過期,沒有就等待下一輪嘗試
  4. 如果過期就拿到鎖,可以開始干自己的事了
  5. 做完了自己的事后,在釋放鎖的操作前會(huì)前檢查下是否已經(jīng)過了自己獲取鎖時(shí)定下的過期時(shí)間,如果已經(jīng)超時(shí)就不進(jìn)行釋放鎖的操作
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 目前實(shí)現(xiàn)分布式鎖的方式主要有數(shù)據(jù)庫、Redis和Zookeeper三種,本文主要闡述利用Redis的相關(guān)命令來實(shí)現(xiàn)...
    Aldeo閱讀 2,179評論 0 6
  • 一、分布式鎖的作用: redis寫入時(shí)不帶鎖定功能,為防止多個(gè)進(jìn)程同時(shí)進(jìn)行一個(gè)操作,出現(xiàn)意想不到的結(jié)果,so......
    魔法師_閱讀 2,125評論 0 6
  • SETNX命令簡介 命令格式 SETNX key value 將 key 的值設(shè)為 value,當(dāng)且僅當(dāng) key ...
    tangstream閱讀 2,013評論 0 2
  • 電話那頭,一把再熟悉不過的男聲接起。 “澤笙,救我”短短幾個(gè)字幾乎傾注了我所有力氣,但我像找到救命稻草一樣,死命地...
    胡寂生閱讀 403評論 0 1
  • 喜從天降霹靂響,歡天喜地梅飄香。 馬到功成威名就,良緣喜結(jié)歸故鄉(xiāng)。 不減當(dāng)年英雄色,心中執(zhí)念幸福長! 后記:細(xì)品慢...
    原味人生VS喜結(jié)良緣閱讀 216評論 0 0

友情鏈接更多精彩內(nèi)容