背景
前段時間有反饋過來說線上有用戶一天會多次簽到吞琐,簽到時間都是同一秒。經(jīng)排查推測是并發(fā)請求一起過來,每個請求都判斷今天未簽到所以都給了放行條件。
所以很有必要加上并發(fā)獨占鎖兴枯。
常規(guī)流程
- redis 緩存值,key存在則返回報錯
- 文件獨占鎖
flock($file_handle, LOCK_EX)
矩欠, 用完釋放
方法有多個結(jié)束出口财剖,需要多處寫釋放代碼。
lock類
把各地方的加鎖癌淮、解鎖抽離出來躺坟,新建lock類。
原理
調(diào)用傳入用戶id乳蓄、鎖名稱咪橙,加鎖方法組裝 redis 緩存 key,
判斷緩存是否存在,不存在則寫入緩存美侦;
存在則進行計數(shù)器增加 incr
产舞,并且返回false。
調(diào)用加獨占鎖
//activity.receiveDailyTaskGift
Lock($mid, 'receiveDailyTask')->addUserLock();
釋放獨占鎖
//領(lǐng)取簽到任務(wù)禮金
public function receiveDailyTaskGift($param)
{
$data = $this->receiveDailyTaskGift($param);
if ($data['code'] != -1) {
Lock($param['mid'], 'receiveDailyTask')->clearUserLock();
}
return $data;
}
防刷用戶標記(可拓展)
incrLockTimes()
進行計數(shù)器累加音榜,超過了配置中的最大請求數(shù)記錄用戶庞瘸,可列為刷子用戶進行拉黑封號等操作。
實現(xiàn)源碼
/**
* 用戶獨占鎖
* created by HongXunPan
*/
class Lock
{
//一定時間內(nèi)最大請求次數(shù) 超過則列為刷子
private $maxTimes = 5;
private $mid;
private $lockName;
private $redisKey;
public function __construct($mid, $lockName = 'lock')
{
$this->mid = $mid;
$this->lockName = $lockName;
$this->redisKey = $this->lockName .'_'. $this->mid;
}
/**
* 添加用戶獨占鎖
* @param int $time
* @return bool|false|int
*/
public function addUserLock($time = 10)
{
if (empty($this->mid)) {
return false;
}
$result = $this->incrLockTimes();
if ($time != 0 && $result == 1) {
ocache::redisMember()->expire($this->redisKey, $time);
}
return $result;
}
/**
* 清除用戶獨占鎖
* @return bool|int
*/
public function clearUserLock()
{
if (empty($this->mid)) {
return false;
}
return ocache::redisMember()->delete($this->redisKey);
}
/**
* 一定時間內(nèi)頻繁請求的用戶
* 超過設(shè)定次數(shù)則特別標記重點觀察
*/
private function incrLockTimes()
{
$times = ocache::redisMember()->incr($this->redisKey);
if ($times >= $this->maxTimes) {
//一定時間內(nèi)發(fā)多個請求 超過最大限制
oo::logs()->dayLog('user lock max time, userId=' . $this->mid . ', key = ' .$this->redisKey . ',times=' . $times, 'lock' . $this->lockName.'txt');
//todo 封號xxx
}
return $times;
}
//文件鎖的效率耗時比redis的少,如果不考慮計數(shù)器防刷的情況下優(yōu)先考慮文件鎖
//-------文件鎖-------
//文件鎖的弊端是不能設(shè)置過期時間赠叼,不適合做一定時間內(nèi)限制多少次請求以及超出部分怎么處理
public function addFileLock()
{
$lock_path = PATH_DAT . 'lock/';
$fileName = $lock_path . $this->redisKey;
$file_handle = fopen($fileName, 'wb+');
if (flock($file_handle, LOCK_EX)) {
return true;
} else {
return false;
}
}
//todo 需要考慮文件鎖 用完的文件是否需要刪除
public function clearFileLock()
{
$lock_path = PATH_DAT . 'lock/';
$fileName = $lock_path . $this->redisKey;
$file_handle = fopen($fileName, 'wb+');
flock($file_handle, LOCK_UN);
unlink($fileName);
}
//-------文件鎖--------
}