1.並發問題
並發大家都知道是什麼情況,這裡說的是並發多個請求搶佔同一個資源,直接上實例吧
請求:index.php?mod=a&action=b&taskid=6處理:
$key = "a_b::".$uid.'_'.$taskid; $v = $redis->get($key); if($v == 1){ $redis->setex($key,10,1); //处理逻辑省略 }
2.分析
邏輯看來還可以,結果發現資料庫中寫入了兩個同樣的請求結果,我看了記錄的時間戳,天!居然是同一秒. 我用microtime(true) log一下兩個請求的時間差居然相差了0.0001s,就是說$redis->setex($key,10,1);還沒執行成功第二個請求已經get到跟第一個請求一樣的結果。這不就是傳說中的並發搶佔資源。這中情況 聽過很多,在開發過程中也沒刻意去模擬實驗過。
3.解決
方案1:第一個反應就是要給處理過程加事務(資料庫是mysql innoDB),加事務的結果就是第一個請求成功了第二個請求會執行到後面撿查發現重了會回滾。其實mysql事務在保證資料一致性上是很ok的,但是透過回滾來保證唯一資源獨佔代價太大,做過mysql事務測試測同學都知道,事務中的insert是已經插進去了,回滾之後才刪掉的。
方案2:還有一個選擇就是php中的檔案獨佔鎖,那就是說這情況下我要新建用戶數* 任務數的檔案來實現每個請求資源的獨佔,如果獨佔資源較少的話可選的解決方法:
/** * 加锁 */ public function file_lock($filename){ $fp_key = sha1($filename); $this->fps[$fp_key] = fopen($filename, 'w+'); if($this->fps[$fp_key]){ return flock($this->fps[$fp_key], LOCK_EX|LOCK_NB); } return false; } /** * 解锁 */ public function file_unlock($filename){ $fp_key = sha1($filename); if($this->fps[$fp_key] ){ flock($this->fps[$fp_key] , LOCK_UN); fclose($this->fps[$fp_key] ); } }
方案3:發現$redis->setnx()可以提供原子操作的狀態:相同的key執行setnx之後沒過期或沒del,再執行會回傳false。這就讓兩個以上的並發請求得到控制必須成功取得鎖才能繼續。
/** * 加锁 */ public function task_lock($taskid){ $expire = 2; $lock_key ='task_get_reward_'.$this->uid.'_'.$taskid; $lock = $this->redis->setNX($lock_key , time());//设当前时间 if($lock){ $this->redis->expire($lock_key, $expire); //如果没执行完 2s锁失效 } if(!$lock){//如果获取锁失败 检查时间 $time = $this->redis->get($lock_key); if(time() - $time >= $expire){//添加时间戳判断为了避免expire执行失败导致死锁 当然可以用redis自带的事务来保证 $this->redis->rm($lock_key); } $lock = $this->redis->setNX($lock_key , time()); if($lock){ $this->redis->expire($lock_key, $expire); //如果没执行完 2s锁失效 } } return $lock; } /** * 解锁 */ public function task_unlock($taskid){ $this->set_redis(); $lock_key = 'task_get_reward_'.$this->uid.'_'.$taskid; $this->redis->rm($lock_key); }
說明下setNX 和expire 這兩個操作其實可以用redis事務來確保一致性