問題場景
web系統(tǒng)在線上運行時,偶爾會遇到前端抖動(同一時刻發(fā)送多次同一請求)的情況。 由于我們的服務是分布式部署,當出現(xiàn)兩個請求落到了不同服務器,則無法通過數(shù)據(jù)庫的事務和隔離級別解決。
當請求是創(chuàng)建新用戶時,由于前端將同一請求發(fā)送了兩次,并且請求落在了不同的后端機上,此時兩臺服務器同時去數(shù)據(jù)庫中查詢是否存在該用戶,返回的結果都是不存在,于是都執(zhí)行了創(chuàng)建該用戶,導致數(shù)據(jù)庫中存在了兩個用戶。
方案
1、前端解決
由于前端代碼錯誤,或者交互設計影響(例如點擊提交后按鈕未置灰,可以重復點擊),導致了重復請求的情況。
即使前端修復了該問題,當接口被人攻擊時,仍然會出現(xiàn)該上述問題。
因此該方案不夠可靠。數(shù)據(jù)的安全性和完整性不能依賴于前端,必須后端解決
2、多機到單機
在負載均衡上配置請求按源地址或者url進行hash,保證同一來源的請求落到同一個后端機上,然后在代碼中使用隊列存儲最近一段時間的請求,通過這種方式過濾重復的請求。如果同一臺機器上php-fpm啟動了多個子進程,那么需要通過共享內存和并發(fā)鎖的方式,保證同一臺機器同一時刻的請求不會重復
該方案也有一定局限性,并且如果一臺機器上啟動2個服務實例,就無法解決該問題。 并且最終也將使用并發(fā)鎖。
3、悲觀鎖
在每次處理請求時,先查找數(shù)據(jù)庫是否存在該數(shù)據(jù),并加上悲觀鎖select for update。 在高并發(fā)的情況下,這樣操作會影響性能,并且select for update中where條件必須是主鍵,否則將不是行級鎖,而是表級鎖。
因此該方案并不是一種很好的方案
4、并發(fā)控制鎖
使用redis單實例,正確地實現(xiàn)了并發(fā)控制鎖。參考文章
class LockHelper
{
static public function lock($key,$ttl)
{
$lockID = rand(0,100000)."_".uniqid();
$redis = KVStore::getInstance(KVStore::PLATOV2);
$isLock = $redis->set($key, $lockID, array('nx', 'ex'=>$ttl));
return array($isLock,$lockID);
}
static public function unlock($key,$lockID)
{
$redis = KVStore::getInstance(KVStore::PLATOV2);
$script = <<<LUA
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
LUA;
return $redis->eval($script, array($key,$lockID),1);
}
}
這里lock和unlock都是原子操作。lock時使用隨機數(shù)是為了防止出現(xiàn)誤刪除其它請求創(chuàng)建的鎖的情況。unlock時為什么使用lua腳本呢?
Lua與Multi/EXEC的功能比較像,在執(zhí)行過程中,redis不會執(zhí)行其它命令,這就不會有并發(fā)訪問的問題,這是非常好的。但Multi/EXEC要求所有命令都是獨立的,后面的命令無法知道前面的命令是否執(zhí)行成功,因為redis中并沒有if等語句,使用Lua可以解決這個問題。
測試腳本如下
function test()
{
$key = "hello";
for($i=0;$i<=100;$i++){
$pid = pcntl_fork();
if($pid == -1){
die("could not fork");
}else if($pid == 0){
$cpid = posix_getpid();
while(1){
list($isLock,$lockID) = LockHelper::lock($key,10);
echo "進程{$cpid}競爭鎖\n";
$sleepTime = rand(0,3000000);
if($isLock){
$duration = $sleepTime/1000000.0;
echo "進程{$cpid}獲取鎖,sleep{$duration}\n";
usleep($sleepTime);
$ret = LockHelper::unlock($key,$lockID);
if($ret){
echo "進程{$cpid}釋放鎖\n";
}
}
}
}
}
while(1){
sleep(10);
}
}
測試結果如下,使用鎖后性能仍可以達到1w左右的qps,可見對性能的影響不大。
