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);
- }
新聞熱點
疑難解答