1. Concurrency problem
Everyone knows what concurrency is. What we are talking about here is multiple concurrent requests to seize the same resource. Just go to the instance
Request: index.php?mod=a&action=b&taskid=6 Processing:
$key = "a_b::".$uid.'_'.$taskid; $v = $redis->get($key); if($v == 1){ $redis->setex($key,10,1); //处理逻辑省略 }
2. Analysis
The logic seems to be OK, but it turns out that two identical request results were written in the database. I looked at the recorded timestamps, and my God! They were actually the same second. I used microtime(true) to log the two The time difference between the two requests is actually 0.0001s, that is to say, $redis->setex($key,10,1); has not yet been executed successfully. The second request has already obtained the same result as the first request. Isn't this the legendary concurrent resource preemption? I have heard of this situation many times, and I did not deliberately simulate it during the development process.
3. Solution
Option 1: The first reaction is to add a transaction to the processing process (the database is mysql innoDB). The result of adding the transaction is that the first request succeeds and the second request will be executed until later and the duplicate is found. It will be rolled back. In fact, mysql transactions are very good at ensuring data consistency, but it is too expensive to ensure the exclusive use of unique resources through rollback. Students who have done mysql transaction testing know that the insert in the transaction has already been inserted, and rollback It was deleted later.
Option 2: Another option is the file exclusive lock in php, that is to say, in this case, I need to create a new file with the number of users * the number of tasks to achieve the exclusivity of each requested resource. If there are fewer exclusive resources, it is optional. Solution:
/** * 加锁 */ 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] ); } }
Option 3: Found that $redis->setnx() can provide the status of atomic operations: the same key has not expired or deled after executing setnx, and then executing it will return false. This allows more than two concurrent requests to be controlled and must successfully acquire the lock before they can continue.
/** * 加锁 */ 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); }
Explain that the two operations setNX and expire can actually use redis transactions to ensure consistency