首頁  >  文章  >  後端開發  >  PHP和redis實作悲觀鎖機制的解析

PHP和redis實作悲觀鎖機制的解析

不言
不言原創
2018-06-12 16:32:542436瀏覽

這篇文章主要介紹了PHP redis實現的悲觀鎖機制,簡單介紹了redis鎖機制與樂觀鎖、悲觀鎖等概念,並結合實例形式分析了php redis實現悲觀鎖相關操作技巧,需要的朋友可以參考下方

本文實例講述了PHP redis實現的悲觀鎖定。分享給大家供大家參考,具體如下:

鎖定機制

通常使用的鎖分為樂觀鎖,悲觀鎖這兩種,簡單介紹下這兩種鎖,作為本文的背景知識,對這類知識已經有足夠了解的同學可以跳過這部分。

樂觀鎖

先來看下百度百科上的解釋:大多是基於資料版本( Version )記錄機制實現。何謂數據版本?即為資料增加一個版本標識,在基於資料庫表的版本解決方案中,一般是透過為資料庫表增加一個 “version” 字段來實現。讀取出資料時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提交資料的版本資料與資料庫表對應記錄的目前版本資訊進行比對,如果提交的資料版本號大於資料庫表目前版本號,則予以更新,否則認為是過期資料。

其實說穿了,就是好比一個健身房裡只有一台跑步機,在健身房門口有個排號機,每個進健身房的人都得先領一個號碼才能進入,如果跑步機上有人,則在一邊做熱身、喝喝水,如果跑步機上沒人,則確認跑步機上目前顯示的號碼(上一個用過跑步機的人的號碼)是否比自己手持的小,如果小,則可以使用;否則,就意味著過號,而過號在現實中我們的都知道要么走,要么重排,就是不能插隊,在系統中也是一樣的,通常是返回錯誤。

悲觀鎖定

同樣,來看下百度百科的解釋:具有強烈的獨佔和排他特性。它指的是對資料被外界(包括本系統目前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個資料處理過程中,將資料處於鎖定狀態。悲觀鎖的實現,往往依賴資料庫提供的鎖機制(也只有資料庫層提供的鎖機制才能真正保證資料存取的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改數據)。

然後,也同樣通俗的解釋下,還是那個健身房。這次在門口不需要排號機了,而是掛著把鑰匙(只有一把),想進去的人必須拿到這把鑰匙才行,拿到鑰匙的人可以進入,不管是熱身、喝水還是跑步都可以,直到他出來把鑰匙掛回牆上,下一個才能去爭取,拿到的才可以再進去。聽著好像有點不人性化,所以悲觀鎖定比較適合強一致性的場景,但效率比較低,特別是讀的並發低。樂觀鎖則適用於讀多寫少,並發衝突少的場景。

背景

先說下,本文的開發背景,方便大家了解為什麼要使用悲觀鎖以及文中鎖的詳細設計。

任務分發系統:任務池(mysql)中存在大量任務(文章),現在需要使用者協助編輯,系統基本需求如下(簡化版):

1、推送使用者感興趣的分類下的任務到使用者編輯器中;
2、使用者編輯提交一個任務後,自動推送下一個任務;
3、每次只分配一個任務給使用者;
4、如果一個使用者佔有某任務超過一定時間,則自動釋放任務,任務進入任務池,重新循環;
5、…

目標

#目標有兩個:

1、一個任務在同一時間段內只能被一個用戶所持有;

2、避免出現死任務,即避免任務被用戶長時間佔有,無法釋放。

想法

由於系統並發量較大,且有頻繁的寫入操作,所以選擇悲觀鎖定來控制每個任務只能同時由一位使用者領取。主要想法如下:

1、從任務池中找出一部分可指派的任務;
2、依照一定順序,選擇一個任務,作為候選推送任務;
3、嘗試對候選推送任務加鎖;
4、如果加鎖成功,則推送任務給用戶,並修改對應的任務狀態和用戶狀態;
5、如果加鎖失敗,則任務已被領取,重複2- 5,直到推送成功。

實作

這裡只介紹下鎖的實作機制,其餘業務邏輯略過。由於加鎖過程應該是不可拆解的,也就是常說的原子型操作,因此這裡選擇redis中的setnx操作作為加鎖的方法。

簡化版的程式碼如下:

function lock($strMutex, $intTimeout) {
  $objRedis = new Redis();
  //使用setnx原子型操作加锁
  $intRet  = $objRedis->setnx($strMutex, 1);
  if ($intRet) {
    //设置过期时间,防止死任务的出现
    $objRedis->expire($strMutex, $intTimeout);
    return true;
  }
  return false;
}

#這段程式碼有個問題,就是setnx成功,但expire失敗,這就可能存在死任務的情況。解決這個問題的一個通用方法是透過使用incr方法來取代setnx,具體如下:

function lock($strMutex, $intTimeout, $intMaxTimes = 0) {
  $objRedis = new Redis();
  //使用incr原子型操作加锁
  $intRet  = $objRedis->incr($strMutex);
  if ($intRet === 1) {
    //设置过期时间,防止死任务的出现
    $objRedis->expire($strMutex, $intTimeout);
    return true;
  }
  if ($intMaxTimes > 0 && $intRet >= $intMaxTimes && $objRedis->ttl($strMutex) === -1) {
    //当设置了最大加锁次数时,如果尝试加锁次数大于最大加锁次数并且无过期时间则强制解锁
    $objRedis->del($strMutex);
  }
  return false;
}

這段程式碼透過$intMaxTimes來保證即使在expire未成功的時候也能強制解鎖,保證系統不會出現死任務。

還有沒有更好的方法呢?

其實redis中的set作業已經相容了setnx,並且支援設定過期時間。

function lock($strMutex, $intTimeout) {
  $objRedis = new Redis();
  //使用setnx操作加锁,同时设置过期时间
  $strRet  = $objRedis->set($strMutex, 1, 'ex', $intTimeout, 'nx');
  if ($strRet === 'OK') {
    return true;
  }
  return false;
}

這個方法是我認為目前最好的,但為什麼沒有直接介紹這個方法,而是先介紹incr那個方法呢?其實細心的同學可以看到上面那一面有兩個加粗的字」通用「。之所以這麼說是因為set方法是從redis2.6.12版本才開始支援多參數的。

程度有限,歡迎指正~

以上就是本文的全部內容,希望對大家的學習有所幫助,更多相關內容請關注PHP中文網!

相關推薦:

php中可變函數的使用總結

#非遞歸實作PHP樹的方法

利用PHP取得使用者客戶端真實IP的方法

#

以上是PHP和redis實作悲觀鎖機制的解析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn