一般使用的鎖分爲樂觀鎖,悲觀鎖這兩種,簡單介紹下這兩種鎖,做爲本文的背景知識,對這類知識已經有足夠了解的同窗能夠跳過這部分。php
先來看下百度百科上的解釋:大可能是基於數據版本( Version )記錄機制實現。何謂數據版本?即爲數據增長一個版本標識,在基於數據庫表的版本解決方案中,通常是經過爲數據庫表增長一個 「version」 字段來實現。讀取出數據時,將此版本號一同讀出,以後更新時,對此版本號加一。此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,若是提交的數據版本號大於數據庫表當前版本號,則予以更新,不然認爲是過時數據。html
其實說白了,就是比如一個健身房裏只有一臺跑步機,在健身房門口有個排號機,每一個進健身房的人都得先領一個號碼才能進入,若是跑步機上有人,則在一邊作作熱身、喝喝水,若是跑步機上沒人,則確認跑步機上當前顯示的號碼(上一個用過跑步機的人的號碼)是否比本身手持的小,若是小,則可使用;不然,就意味着過號,而過號在現實中咱們的都知道要麼走,要麼重排,就是不能插隊,在系統中也是同樣的,一般是返回錯誤。mysql
一樣,來看下百度百科的解釋:具備強烈的獨佔和排他特性。它指的是對數據被外界(包括本系統當前的其餘事務,以及來自外部系統的事務處理)修改持保守態度,所以,在整個數據處理過程當中,將數據處於鎖定狀態。悲觀鎖的實現,每每依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,不然,即便在本系統中實現了加鎖機制,也沒法保證外部系統不會修改數據)。redis
而後,也一樣通俗的解釋下,仍是那個健身房。此次在門口不須要排號機了,而是掛着把鑰匙(只有一把),想進去的人必須拿到這把鑰匙才行,拿到鑰匙的人能夠進入,不論是熱身、喝水仍是跑步均可以,直到他出來把鑰匙掛回牆上,下一個才能去爭取,拿到的才能夠再進去。聽着好像有點不人性化,因此悲觀鎖比較適合強一致性的場景,但效率比較低,特別是讀的併發低。樂觀鎖則適用於讀多寫少,併發衝突少的場景。sql
先說下,本文的開發背景,方便你們瞭解爲何要使用悲觀鎖以及文中鎖的詳細設計。
任務分發系統:任務池(mysql)中存在大量任務(文章),如今須要用戶協助編輯,系統基本需求以下(簡化版):
一、推送用戶感興趣的分類下的任務到用戶編輯器中;
二、用戶編輯提交一個任務後,自動推送下一個任務;
三、每次只分配一個任務給用戶;
四、若是一個用戶佔有某任務超過必定時間,則自動釋聽任務,任務進任務池,從新循環;
五、……數據庫
目標有兩個:
一、一個任務在同一時間段內只能被一個用戶所持有;
二、避免出現死任務,即避免任務被用戶長時間佔有,沒法釋放。markdown
因爲系統併發量較大,而且有頻繁的寫操做,因此選擇悲觀鎖來控制每一個任務只能同時被一個用戶領取。主要思路以下:
一、從任務池中找出一部分可分配的任務;
二、根據必定順序,選擇一個任務,做爲候選推送任務;
三、嘗試對候選推送任務加鎖;
四、若是加鎖成功,則推送任務給用戶,並修改對應的任務狀態和用戶狀態;
五、若是加鎖失敗,則任務已被領取,重複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,具體以下:post
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版本纔開始支持多參數的。
水平有限,歡迎指正~
如需轉發,請註明出處,thx~