PHP高并發(fā)高負(fù)載下的3種實(shí)戰(zhàn)場(chǎng)景解決方法示例
前言:
在實(shí)際開發(fā)項(xiàng)目中,產(chǎn)品一旦推廣開來,總能遇到一些小問題。比如某個(gè)接口突然就請(qǐng)求崩掉了,某個(gè)提交接口明明做了限制為什么就多出了好多重復(fù)的記錄。還有是某個(gè)記錄超過限制進(jìn)行修改了,以下就以這幾個(gè)小問題總結(jié)一下平時(shí)采取的解決方法。
1.緩存失效場(chǎng)景,就比如某個(gè)接口做了數(shù)據(jù)緩存,緩存過期導(dǎo)致突然某個(gè)時(shí)刻大量請(qǐng)求直接讀數(shù)據(jù)庫(kù)。解決方法設(shè)置redis緩存回調(diào)事件,訂閱失效頻道。所以這個(gè)也可以用來處理某些業(yè)務(wù)場(chǎng)景到期處理方式。
2.接口冪等性場(chǎng)景,就比如注冊(cè)接口,通過手機(jī)號(hào)查詢是否存在記錄。但有時(shí)出現(xiàn)網(wǎng)絡(luò)延遲用戶連點(diǎn)等情況,會(huì)出現(xiàn)數(shù)據(jù)庫(kù)出現(xiàn)幾條一樣的用戶數(shù)據(jù)記錄。
3.商品庫(kù)存超賣場(chǎng)景,比如某個(gè)活動(dòng)商品下單,多個(gè)用戶同時(shí)下一個(gè)商品的訂單,從而導(dǎo)致庫(kù)存超賣的現(xiàn)象。解決方法可以使用樂觀鎖或者悲觀鎖解決此問題。
場(chǎng)景一,緩存失效回調(diào)。
1. 設(shè)置Redis回調(diào)事件方法。
(1). 打開Redis客戶終端,輸入命令非持久性的回調(diào)事件設(shè)置
config set notify-keyspace-events Ex
(2). windows平臺(tái)打開Redis安裝目錄中找到"redis.windows-service.conf",然后打開編輯找到notify-keyspace-events那一行,去掉"#",改為notify-keyspace-events “Ex"。
(3). 其中Redis還可以設(shè)置訂閱鍵名的回調(diào),比如訂閱某個(gè)鍵名的del操作等,可以在conf中設(shè)置不同的,方法網(wǎng)上也有的。
2. 訂閱redis某個(gè)庫(kù)的鍵失效的頻道名。
可以在命令測(cè)試,也可以通過PHP代碼訂閱然后cli環(huán)境下運(yùn)行腳本。
命令: subscribe __keyevent@0__:expired
3. 設(shè)置有限期
重新打開一個(gè)新的redis客戶終端輸入一個(gè)帶有效期的鍵值對(duì),如下(鍵名test_key_name, 時(shí)間30s, 值ceshi)
命令: setex test_key_name 30 ceshi
4. 查看鍵失效回調(diào)訂閱的命令窗口是否出現(xiàn)失效的鍵名。
5. 代碼實(shí)現(xiàn)鍵名的失效事件訂閱。
<?php //設(shè)置php腳本執(zhí)行時(shí)間 set_time_limit(0); //設(shè)置socket連接超時(shí)時(shí)間 ini_set('default_socket_timeout', -1); class redisSubscribe { protected $config = [ "host" => "127.0.0.1", "password" => "6379" ]; protected $redis; public function __construct() { try { $this->redis = new \Redis(); $this->redis->pconnect($this->config['host'],$this->config['password']); } catch(\Exception $e) { echo "redis錯(cuò)誤:".$e->getMessage().PHP_EOL; } } // 普通消息訂閱 public function normal() { //聲明頻道名稱 $channelName = "test"; try { $this->redis->subscribe([$channelName], function ($redis, $channel, $msg) { echo 'channel:' . $channel . ',message:' . $msg . PHP_EOL; file_put_contents('subscribe.log',"\n-".$msg."-\n",FILE_APPEND); }); } catch (\Exception $e) { echo $e->getMessage(); } } // 訂閱Key失效事件的頻道 public function keyNotify() { echo "wathc keyNotify start~~".PHP_EOL; // Key事件回調(diào) //$channel = "__keyevent@0__:expired"; // 0號(hào)庫(kù)的Key過期事件頻道名 $channel = "__keyevent@*__:expired"; // 所有庫(kù)的Key過期事件頻道名 try { $this->redis->subscribe([$channel], function ($redis, $channel, $msg) { echo 'channel:' . $channel . '===========' . ',message:' . $msg . PHP_EOL; file_put_contents('subscribe.log',"\n-".$msg."-\n",FILE_APPEND); }); } catch (\Exception $e) { echo $e->getMessage(); } } } (new redisSubscribe())->keyNotify(); ?>
6. 通過PHP-cli運(yùn)行該腳本
然后也可以setex一個(gè)短時(shí)間的鍵,然后查看命令是否輸出該失效的鍵名。
7. 緩存失效應(yīng)用展開。
(1). 代碼中設(shè)置的所有鍵名都配置到項(xiàng)目的全局配置文件中。
(2). 服務(wù)器中開一個(gè)守護(hù)進(jìn)程(持續(xù)運(yùn)行訂閱某個(gè)庫(kù)或者所有庫(kù)的鍵失效回調(diào)事件腳本)。
(3). 當(dāng)該腳本有回調(diào)時(shí),取出鍵名去全局緩存鍵名數(shù)組中匹配。
(4). 規(guī)則業(yè)務(wù)可以自行設(shè)計(jì)。
(5). 比如取出一個(gè)"cate5"的鍵名,則可以取資訊表中查詢分類ID為5的所有數(shù)據(jù)然后再進(jìn)行緩存。
(6). 緩存失效事件還一個(gè)高端玩法,就是取代某些定時(shí)任務(wù)。比如可以將某個(gè)訂單作為鍵名緩存,當(dāng)該鍵名失效就可以取出鍵名拿到ID去數(shù)據(jù)庫(kù)中將訂單狀態(tài)修改為失效。
場(chǎng)景二,接口冪等性。
接口重復(fù)數(shù)據(jù)也就是在高并發(fā)下的數(shù)據(jù)添加場(chǎng)景。最典型的是注冊(cè)接口,用戶在網(wǎng)絡(luò)延遲大或者信號(hào)不穩(wěn)定的情況下。并且同時(shí)大量用戶在進(jìn)行注冊(cè)操作,用戶點(diǎn)擊了一次沒反應(yīng)然后再次點(diǎn)擊多個(gè)。
在沒有做冪等性處理只是拿到手機(jī)號(hào)查詢數(shù)據(jù)庫(kù)是否存在,用戶表又沒分庫(kù)分表,查詢緩慢,查詢出來后,多條并發(fā)的請(qǐng)求都繞過了手機(jī)號(hào)已經(jīng)存在的條件判斷,所以就出現(xiàn)了ID不同,但是其他字段一樣的記錄。
- 對(duì)于高并發(fā)數(shù)據(jù)添加,可以使用Redis的setnx。
- setnx是設(shè)置鍵并且在有效期內(nèi)有值時(shí),再次對(duì)該鍵名進(jìn)行重復(fù)賦值無法進(jìn)行,會(huì)返回0。
- 可以代碼在對(duì)某些條件查詢是否存在時(shí),可以將條件組成鍵名賦值。添加記錄時(shí)再次對(duì)鍵名重新賦值,返回null則表示已經(jīng)存在。
- 以下代碼是項(xiàng)目中的一個(gè)測(cè)試方法,使用的redis是封裝的,借鑒需要修改。
/** * @Notes: 高并發(fā)防止重復(fù)提交(插入數(shù)據(jù)) 【保證接口的冪等性】 * @Interface preventRepeatSubmit * @return mixed * @author: bqs * @Time: 2020/6/19 14:56 */ public function preventRepeatSubmit() { /* 比如查詢某條(什么條件)記錄是否存在,分布式鎖機(jī)制[redis的原子性setnx] * 1. 通過條件拼接為唯一的鍵名,將鍵名setnx設(shè)置一個(gè)30s有效期的值 * 2. setnx設(shè)置鍵名不成功(返回0)表示已經(jīng)存在,接口則直接返回記錄已經(jīng)存在 * 3. 根據(jù)該條件查詢數(shù)據(jù)庫(kù)記錄,如果存在,接口再返回記錄已經(jīng)存在 * 【只要添加記錄前需要查詢什么是否存在則都需要考慮高并發(fā)情況,則通過此方案】 */ $redis = Redis::db(0); $no = date('YmdHis',time()).mt_rand(1000,9999); //$no = 202006191537447811; // 是否添加鎖表 $addLock = false; if ($redis->setnx($no,1)) { $redis->expire($no,30); //設(shè)置30s過期時(shí)間 } else { $addLock = true; // 訂單已經(jīng)存在則鎖住 } // 數(shù)據(jù)庫(kù)查詢是否存在 $isExist = Db::name('ztest')->where(['no'=>$no])->find(); if ($isExist) { $addLock = true; } if (!$addLock) { $data = [ "no" => $no, "tab_num" => 2, "stock" => 20, "create_time" => time() ]; $res = Db::name('ztest')->insertGetId($data); } return "添加數(shù)據(jù)成功"; }
場(chǎng)景三,庫(kù)存超賣。
庫(kù)存超賣是一個(gè)很常見的秒殺或者其他高并發(fā)場(chǎng)景下的數(shù)據(jù)更新問題。網(wǎng)絡(luò)上的解決方法也是多種多樣,對(duì)該問題延伸的數(shù)據(jù)庫(kù)樂觀鎖,悲觀鎖的知識(shí)點(diǎn)也是數(shù)不勝數(shù)。
所以,這里我也不再介紹數(shù)據(jù)庫(kù)的存儲(chǔ)引擎機(jī)制,事務(wù),表鎖等概念。直接以代碼展現(xiàn),以下是以樂觀鎖實(shí)現(xiàn)的數(shù)據(jù)庫(kù)更新問題。
- 高并發(fā)下,對(duì)單條記錄的修改。一般修改前會(huì)對(duì)某字段進(jìn)行判斷,但是并發(fā)情況下,拿查詢的結(jié)果進(jìn)行攔截是極其的不靠譜。不過也可以對(duì)查詢進(jìn)行加鎖,但是需要在同一事務(wù)中。
- 庫(kù)存字段添加無符號(hào)的字段約束,所以再大的并發(fā)在修改為0之后也不會(huì)出現(xiàn)負(fù)數(shù)了,在修改的操作時(shí)捕捉修改為負(fù)數(shù)時(shí)的數(shù)據(jù)庫(kù)異常。
- 表中添加version字段,這個(gè)也是網(wǎng)上盛傳的樂觀鎖經(jīng)典實(shí)例了,后面的原理和流程我就不介紹了,代碼也是這樣寫的,所以直接貼代碼了。
/** * @Notes: 高并發(fā)樂觀鎖 - (更新數(shù)據(jù)) * @Interface testConcurrence * @return mixed * @author: bqs * @Time: 2020/6/19 14:25 */ public function testConcurrence() { // 開啟事務(wù) Db::startTrans(); // 查詢ID25當(dāng)前的庫(kù)存和版本號(hào) $curr = Db::name('ztest')->field('stock,version')->where(['id'=>25])->find(); // 判斷庫(kù)存是否小于0 if ($curr && $curr['stock'] <= 0) { throw new \Exception('物品已售罄',302); } try { // 修改庫(kù)存 - 獲取ID25的行瑣 $updateRes = Db::name('ztest')->where(['id'=>25,'version'=>$curr['version']])->update(['stock'=>$curr['stock']-1,'version'=>$curr['version']+1]); // 標(biāo)識(shí)并發(fā)過來修改的,拿到的version太舊,事務(wù)回滾重新回到查詢?cè)僮咭槐? if (!$updateRes) { Db::rollback(); } } catch(\Exception $e) { Db::rollback(); // 記錄日志,或者返回 } // 事務(wù)提交 Db::commit(); return '購(gòu)買成功了'; }
以上就是PHP高并發(fā)高負(fù)載下的3種實(shí)戰(zhàn)場(chǎng)景解決方法示例的詳細(xì)內(nèi)容,更多關(guān)于PHP高并發(fā)高負(fù)載場(chǎng)景的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
實(shí)例探索PHP只讀屬性改變游戲規(guī)則的特性
這篇文章主要為大家介紹了PHP只讀屬性改變游戲規(guī)則的特性實(shí)例探索,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01PHP進(jìn)行批量任務(wù)處理不超時(shí)的解決方法
這篇文章主要介紹了PHP進(jìn)行批量任務(wù)處理不超時(shí)的解決方法,結(jié)合實(shí)例形式簡(jiǎn)單分析了php結(jié)合ajax進(jìn)行異步處理實(shí)現(xiàn)批量任務(wù)不超時(shí)的相關(guān)技巧,需要的朋友可以參考下2016-07-07兩種php去除二維數(shù)組的重復(fù)項(xiàng)方法
這篇文章主要介紹了兩種php去除二維數(shù)組的重復(fù)項(xiàng)方法,大家可以進(jìn)行比較看哪一種更適合自己,需要的朋友可以參考下2015-11-11PHP實(shí)現(xiàn)時(shí)間日期友好顯示實(shí)現(xiàn)代碼
之前腳本之家小編也為大家分享過類似的時(shí)間日期顯示代碼,這里為大家分享的更加友好,大家根據(jù)說明調(diào)用即可2019-09-09PHP解析目錄路徑的3個(gè)函數(shù)總結(jié)
這篇文章主要介紹了PHP解析目錄路徑的3個(gè)函數(shù)總結(jié),本文總結(jié)了basename、dirname、pathinfo3個(gè)函數(shù),它們分別處理路徑的不同部分,需要的朋友可以參考下2014-11-11php中照片旋轉(zhuǎn) (orientation) 問題的正確處理
這篇文章主要介紹了php中照片旋轉(zhuǎn) (orientation) 問題的正確處理,文中給出了詳細(xì)的介紹和示例代碼,相信對(duì)大家具有一定的參考價(jià)值,有需要的朋友們下面來一起看看吧。2017-02-02smarty靜態(tài)實(shí)驗(yàn)表明,網(wǎng)絡(luò)上是錯(cuò)的~呵呵
smarty靜態(tài)實(shí)驗(yàn)表明,網(wǎng)絡(luò)上是錯(cuò)的~呵呵...2006-11-11