tags:redis read&write redis加鎖和解鎖 phpphp
習慣性說一下寫這篇文章要說明什麼,咱們常常用redis進行加鎖操做,目的是爲了解決併發可能帶來的問題。可是使用redis加鎖的方式有多種,本文對常見的幾種方式進行解析,並提供一種相對完美的方案。redis
這是一個經典問題,請看代碼:併發
//redis中的某個鍵自增 $val = $this->redis->get($key); $val ++; $this->redis->set($val);
這段代碼邏輯沒有問題,就是先讀取數據,再修改數據,在寫回修改,這裏是但願每次訪問都遞增變量$val的值,但在併發狀況下,存在狀況是兩個進程都讀取到了同樣的初始值,而後都加1,最後寫回Redis,這種狀況就會統計數據比實際的少。這個問題應該有許多人遇到過,思考過怎麼解決這類問題。這裏給出一個統一的解決方案,就是儘可能保證操做的原子性,好比能夠用redis的incr命令來實現自增(能夠認爲redis的命令是原子的)。this
由上面的問題再進一步,來探討一個你們經常使用的,爲一個操做進行加鎖。lua
問題場景以下:有一個商品,每一個用戶均可以去修改商品信息。假設用戶id分別爲6和8的用戶對id爲123的商品進行操做。操作系統
$key = '123'; $val = $this->redis->get($key); if(!$val){ $this->redis->set($key,'123'); $this->redis->expire($key,'4'); /**此處修改商品信息操做 ****** **/ $this->redis->del($key); }else{ echo '錯誤提示'; }
上面這個錯誤示例,
錯誤點1:set和expire是分開寫的,若是說程序執行中再執行了set()後出現崩潰,則這個就變成了永久鎖(雖然這是個小几率事件)。code
錯誤點2:這個商品中設置的key是商品id,val也是商品id,不少人認爲只有一個key就能夠了,val是什麼無所謂。這就缺乏了鎖的標識,沒法判斷這個鎖的擁有者是誰,從而會帶來一系列影響以下。隊列
針對錯誤1和錯誤2的第1點,咱們只須要去除read & write模式就能夠解決,解決方案爲進程
//同時設置val和過時時間,並使用setnx $status = $this->redis->setnx($key,$val,$expireTime); if($status){ /**此處修改商品信息操做 ****** **/ $this->redis->del($key); }else{ echo '錯誤提示'; }
setnx,能夠在設置時檢查是否存在鎖不存在則設置並返回1,若是存在不覆蓋並返回0。事件
針對錯誤2第2點,咱們須要爲每一個進程設置一個獨立的本身能夠識別的val,若是一個用戶只能開一個進程,這個val能夠爲用戶id,若是一個用戶能夠設置多個進程,那麼必須按照實際車狀況採用其餘方式來區分,這裏咱們以用戶id爲例,而且在刪除的時候只能刪除本身的鎖。那麼這裏問題又出現了,若是咱們寫成這樣:
//同時設置val和過時時間,並使用setnx $userId = 2; $status = $this->redis->setnx($key,$userId,$expireTime); if($status){ /**此處修改商品信息操做 ****** **/ if($this->redis->get($key) == $userId){ $this->redis->del($key); } }else{ echo '錯誤提示'; }
這種狀況看似沒有什麼問題,其實否則,你們注意我再設置所得時候,設置了一個過時時間,假如這個時間設置的是4秒,那麼若是進程A執行到刪除前一刻一不當心超過了4秒,那麼這個鎖就自動消失了。而另外一個進程B查到沒有鎖,就加了一把本身的鎖,此時進程A執行刪除,就把B的鎖給刪除了(極小機率事件)。
這裏解決方案有兩種
//同時設置val和過時時間,並使用setnx $userId = 2; $status = $this->redis->setnx($key,$userId,$expireTime); if($status){ /**此處修改商品信息操做 ****** **/ //由於寫這個博客的機器沒有裝redis,因此沒有驗證這個語法對不對。請你們見諒 $script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; $result = $this->redis->eval(script,array($key,$val),1); if ($result) { return true; } }else{ echo '錯誤提示'; }
這裏就把兩個操做變成了一個原子操做。解決的加鎖和解鎖可能出現的問題。
咱們來講一些題外話拓展:在進程有可能出現衝突的地方,通常咱們叫作臨界區(操做系統中也有這個概念,是經過另外一種叫作PV信號量的方式來解決的,其實能夠理解爲組織等待進程隊列,P操做不能獲取到資源使用權的則進入等待隊列,等待V操做釋放資源後,檢查是否有等待隊列,進行進程釋放。固然PV操做也是原子性的。因此說解決類似問題的辦法也有必定的類似性)。
歡迎你們評論補充 --- vinter_he