php在大并發下redis鎖實現
在現如今電商盛行的時期,會出現很多促銷活動,最為常見的就是秒殺。在秒殺系統中最為常見的問題就是會出現超賣的情況,那么如何來杜絕超賣的情形了,在業務邏輯層面可以使用緩存以及加鎖的手法來避免超賣的情形。
現如今nosql已經非常流行和穩定了,在此我將通過redis和php來說明如何實現鎖機制。當然我使用redis加鎖并不是我的秒殺系統,而是最近做的一個項目有個用戶提現,初期沒有考慮到會有人惡意刷新接口,而導致用戶無限制提現。經過查看nginx日志,發現用戶在同一時間段,通過刷接口的方法超額提現,導致虧損
起初的提現代碼如下:
$uid = $this->user_id; if (empty($uid)) { return $this->responseJson(300, '請先登錄'); } $money = $this->request->get('money', 'trim'); $formId = $this->request->get('formId', 'trim', ''); $user_model = new User(); $user_info = $user_model->getUserInfoById($uid); $balance = $user_info['balance'] / 100; $phone = $this->request->get('phone', 'trim'); if ($money < 1) { return $this->responseJson(300, '最小提現金額為1元'); } if ($balance < $money) { return $this->responseJson(300, '賬戶余額不足'); } $openid = $user_info['openId']; if (in_array($openid, ['oMl_x0DiSCYqQuqJOKV9bAqR1Ugk', 'oMl_x0B1zoY70dbiwxAt4lg2fmL4', 'oMl_x0E0jYOK6NpbzwmTVJowpfpk', 'oMl_x0GHeAdKCZ8Iv1KD0CmdZLQ0', 'oMl_x0FBU1eWya1fG5xtVxryUYG4', 'oMl_x0CIMr5tItEy1QPtpI9eFJak', 'oMl_x0JWFdGOnf80W5oZOX-XfGcw'])) { return $this->responseJson(300, '正在處理中'); } if (!empty($phone)) { if (!isset($user_info['phone']) || (isset($user_info['phone']) && $user_info['phone'] != $phone)) { $user_model->update(['_id' => $this->user_id], ['$set' => ['phone' => $phone]]); } } $order_id = \Common::getOrder(); $res = $user_model->updateBalanceById($uid, $money * 100); if ($res) { $trans_res = \GlobalFunc::transfer($uid, $order_id, $openid, 'NO_CHECK', $money); if ($trans_res === false) { $user_model->updateBalanceById($uid, $money * 100, 2); $redis->del('with:draw:' . $uid); if (!empty($formId)) { $TemplateMsg = new \TemplateMsg(); $to_user = $user_info['openId']; $tem_id = 'ZSpYvjqdawADxr7j_8DJFuaoAdWbHhXdnAFlp5QF9L0'; $data = array( 'keyword1' => array('value' => '提現', 'color' => "#173177"), 'keyword2' => array('value' => date('Y-m-d H:i:s'), 'color' => "#173177"), 'keyword3' => array('value' => $trans_res['err_code_des'] . "。申請提現金額已自動退回賬戶余額中。", 'color' => '#173177'), ); $page = 'pages/balance/balance'; $TemplateMsg->doSend($to_user, $tem_id, $formId, $data, $page); } return $this->responseJson(300, $trans_res['err_code_des']); } $redis->del('with:draw:' . $uid); } else { $redis->del('with:draw:' . $uid); return $this->responseJson(300, '提現失敗,請稍后重試'); } ....
看上面代碼邏輯感覺似乎沒有什么問題,確實,在正常的情況下是不會出現問題,如果有人惡意的去刷接口的話,上述問題就出現了。為了防止用戶惡意刷接口,所以對現有代碼做了如下修改
$uid = $this->user_id; if (empty($uid)) { return $this->responseJson(300, '請先登錄'); } $money = $this->request->get('money', 'trim'); $formId = $this->request->get('formId', 'trim', ''); $user_model = new User(); $user_info = $user_model->getUserInfoById($uid); $balance = $user_info['balance'] / 100; $phone = $this->request->get('phone', 'trim'); if ($money < 1) { return $this->responseJson(300, '最小提現金額為1元'); } if ($balance < $money) { return $this->responseJson(300, '賬戶余額不足'); } $redis = $this->cache('redis'); $redis->incr('with:draw:' . $uid); if (intval($redis->get('with:draw:' . $uid)) > 1) { return $this->responseJson(300, '正在處理中'); } $openid = $user_info['openId']; if (in_array($openid, ['oMl_x0DiSCYqQuqJOKV9bAqR1Ugk', 'oMl_x0B1zoY70dbiwxAt4lg2fmL4', 'oMl_x0E0jYOK6NpbzwmTVJowpfpk', 'oMl_x0GHeAdKCZ8Iv1KD0CmdZLQ0', 'oMl_x0FBU1eWya1fG5xtVxryUYG4', 'oMl_x0CIMr5tItEy1QPtpI9eFJak', 'oMl_x0JWFdGOnf80W5oZOX-XfGcw'])) { return $this->responseJson(300, '正在處理中'); } if (!empty($phone)) { if (!isset($user_info['phone']) || (isset($user_info['phone']) && $user_info['phone'] != $phone)) { $user_model->update(['_id' => $this->user_id], ['$set' => ['phone' => $phone]]); } } $order_id = \Common::getOrder(); $res = $user_model->updateBalanceById($uid, $money * 100); if ($res) { $trans_res = \GlobalFunc::transfer($uid, $order_id, $openid, 'NO_CHECK', $money); if ($trans_res === false) { $user_model->updateBalanceById($uid, $money * 100, 2); $redis->del('with:draw:' . $uid); if (!empty($formId)) { $TemplateMsg = new \TemplateMsg(); $to_user = $user_info['openId']; $tem_id = 'ZSpYvjqdawADxr7j_8DJFuaoAdWbHhXdnAFlp5QF9L0'; $data = array( 'keyword1' => array('value' => '提現', 'color' => "#173177"), 'keyword2' => array('value' => date('Y-m-d H:i:s'), 'color' => "#173177"), 'keyword3' => array('value' => $trans_res['err_code_des'] . "。申請提現金額已自動退回賬戶余額中。", 'color' => '#173177'), ); $page = 'pages/balance/balance'; $TemplateMsg->doSend($to_user, $tem_id, $formId, $data, $page); } return $this->responseJson(300, $trans_res['err_code_des']); } $redis->del('with:draw:' . $uid); } else { $redis->del('with:draw:' . $uid); return $this->responseJson(300, '提現失敗,請稍后重試'); }
...
代碼調整后似乎防止了用戶刷接口的行為,但是后期有用戶反映,自己提不了現了。經過一番查看,原來redis的值一直存在,雖然用戶操作完成后會刪除key,但是也會存在在用戶沒有完全操作完成而導致流程中斷,所以會導致key刪除失敗,為了解決鎖不釋放的問題,又對上述代碼進行修改,在設置鎖的時候,設置一個過期時間,修復如下
if (intval($redis->get('with:draw:' . $uid)) > 1) { if ($redis->ttl('with:draw:' . $uid) == -1) { $redis->expire('with:draw:' . $uid, 30); } return $this->responseJson(300, '正在處理中'); }
這樣就可以實現鎖不釋放的問題,但是上述代碼除了使用incr操作外,還可以使用redis的setnx來代替,其實是一樣的效果,但是無論你用那種還是有點問題就是,當你寫入成功之后,突然斷網或服務器宕機的情況,這時還會出現上述問題,那應該如何來解決呢。其實完全可以通過redis的 Multi/Exec結合來解決上述問題,其代碼如下
$redis->multi(); $redis->setNX($key, $value); $redis->expire($key, $ttl); $redis->exec();
這樣就可以解決突然情況帶來的妖怪問題了
總結:通過redis可以實現大并發的數據請求操作,通過事務的操作來加鎖和釋放鎖,達到數據完整性
雖然上述問題解決了,但是代碼還是有待優化,從 2.6.12 起,SET 涵蓋了 SETEX 的功能,并且 SET 本身已經包含了設置過期時間的功能,也就是說,我們前面需要的功能只用 SET 就可以實現。所以上述代碼可以簡化為
$redis->set($key, $random, array('nx', 'ex' => $ttl));
以上就是如何解決并發和惡意刷接口的解決方法,以作記錄
浙公網安備 33010602011771號