浪子編程走四方 做者:浪子編程走四方,勤記錄,懂分享,刻意練習,日精進! 公衆號:深夜有話聊php
本文內容是對併發業務場景出現超賣狀況而寫的一片解決方案。主要是利用到了 Redis 中的隊列技術。css
所謂的超賣,就是咱們的售賣量大於了物品的庫存量。該狀況通常出如今電商系統中促銷類的業務場景中。輕則只是部分商品超賣,較小的經濟損失,可是當大量的超賣狀況,例如淘寶雙十一這樣的業務場景下致使超賣,則損失是很是大的,同時給用戶體驗帶來的也是負面影響,頗有可能損失用戶量。記得以前遇到一個公司,作電商項目,就是由於超賣致使公司倒閉。html
首先,咱們見下圖. jquery
1.第一步是咱們用戶進入商品秒殺頁面,點擊秒殺按鈕,向服務端發送秒殺請求。 2.服務端在接受到用戶秒殺請求,根據請求的商品id參數,去查詢數據庫中該商品id的庫存量。 3.當查詢到該商品庫存量後,進行判斷。若是庫存量不足,則返回給用戶,商品庫存不足的信息。 4.當查詢到該商品的庫存足夠時,則生成訂單數據並減小商品庫存。接着將成功信息返回給用戶。 5.用戶接受到搶購成功消息後,纔可進入下單頁面。此時按照正常邏輯,進行下單支付。
這種模式爲何會出現超賣呢?ajax
按照咱們上面所講的,按理來講是一種正常的邏輯流程。可是當並打量大的時候,就會出現超賣狀況。在上圖第 2 步驟中,是作商品庫存的查詢。假如此時咱們查詢到的商品庫存爲 1,這時候就會走 4 中上面的部分(插入搶購信息並減小庫存),因爲併發量大的狀況下,下一個請求在上一個還未執行減庫操做就去查詢了商品庫存,這時候查詢出來的庫存量依然是 1。一樣的,會走到 4 上面的步驟中去。而後上一個請求執行了減庫操做,此時庫存爲 0,第二個請求再去減庫時,就會把庫存量設置爲-1,這樣就出現了超賣狀況。因爲併發,同時會發生不少請求,所以減小的數量不單單是 1 了,或許是成百上千甚至上萬等等。redis
網上有不少這樣的思路,幾乎是經過<kbd>隊列技術</kbd>來解決的。先將商品庫存信息緩存到咱們的緩存中去,例如 Redis。(文章中示例也是經過該方案實現)。數據庫
這裏單獨講一講示例代碼中秒殺的解決思路。編程
$redis->lpush('商品id',1);
當每個商品有多少個庫存則循環多少次,這樣就能夠保證每一個商品隊列中的長度就是商品庫存長度。<font color='red'>其實這裏我的是有一個疑問的,若是商品少,咱們加入到緩存的耗時是很小的,可是商品數量大,這樣就很耗時,而且 redis 是放在內存中的,也暫用大量的內存。</font>json
當秒殺開始時,用戶發送請求,每次去檢測一下商品的隊列是否爲空,當非空時,則使用 lpop 減小一個長度,也就是減小一個庫存量。這時候將秒殺的信息寫入到緩存中去,給緩存信息配一個惟一的鍵,將該鍵返回給用戶。(因爲 lpop 是原子性的,便是大量併發來了,也是要在 Redis 內部進行排隊執行的,假如在判斷是否爲空時,檢測到是非空,進行 lpop 操做,因爲隊列是空,這時候去執行出隊列也是返回錯誤的)。緩存
返回給用戶秒殺成功的信息,用戶根據返回的鍵進行下單操做。利用該鍵,將秒殺中的緩存信息寫入數據庫並生成對應的訂單。
接下來,咱們能夠結合上圖,得出下面的流程圖:
建立公共的 Redis 鏈接
<?php /** * Redis鏈接 */ $redis = new Redis(); $result = $redis->connect('127.0.0.1',6379,2); if(!$result){ die('redis connect fail'); }
秒殺前將商品庫存寫入緩存中
/** * 模擬商品庫存如隊列 */ require_once __DIR__.'/redis_connect.php'; // 模擬數據庫查詢的商品數據 $goodsList = [ ['id'=>1,'name'=>'夏季外套','price'=>12.32,'count'=>12], ['id'=>2,'name'=>'冬季外套','price'=>12.32,'count'=>1], ['id'=>3,'name'=>'秋季外套','price'=>12.32,'count'=>2], ['id'=>4,'name'=>'春季外套','price'=>12.32,'count'=>23], ['id'=>5,'name'=>'男士內衣','price'=>12.32,'count'=>8], ['id'=>6,'name'=>'男士馬甲','price'=>12.32,'count'=>180], ['id'=>7,'name'=>'男士長褲','price'=>12.32,'count'=>120], ]; // 將商品庫存添加到redis隊列中 $goodqueue = 'goods:queue:'; foreach($goodsList as $key => $val){ $count = $val['count']; for($i=0;$i<$count;$i++){ $result = $redis->lpush($goodqueue.$val['id'],1); echo $result.'<br/>'; } }
模擬客戶發送請求,這裏能夠開多個窗口,增長請求量。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Document</title> </head> <body> 模擬秒殺場景,用戶請求 <div class="content"></div> <script src="https://cdn.bootcss.com/jquery/2.2.0/jquery.min.js"></script> <script> // 簡單模擬1000個用戶發送請求 for (let index = 0; index < 1000; index++) { $.ajax({ type: "POST", url: "http://localhost/Test/redis_miaosha.php", data: { userId: index, goodsId: Math.floor(Math.random() * 10) }, dataType: "json", success: function(res) { console.log(res.result); if (res.result === "OK") { $(".content").append( "<a href='http://localhost/Test/redis_server.php?key=" + res.key + "' target='_blank'>用戶id爲" + index + "的搶購成功!</a><br/>" ); } else if (res.result === "FAIL") { $(".content").append( "<a href=''>用戶id爲" + index + "的搶購失敗!</a><br/>" ); } } }); } </script> </body> </html>
服務端接收秒殺請求並寫入緩存
<?php /** * 模擬用戶秒殺場景 */ require_once __DIR__.'/redis_connect.php'; /** * * 1.接受用戶請求 * 2.驗證用戶是否已經參與秒殺,商品是否存在 * 3.根據商品id減小商品隊列中的庫存數量 * 4.將用戶的秒殺數據寫入server層中,並返回秒殺數據對應的惟一key值 * 5.用戶點擊下單,根據serve層中的緩存數據,生成訂單數據並減小數據庫商品的庫存數據 */ $getParams = $_POST; $userId = $getParams['userId']; $goodsId = $getParams['goodsId']; $key = 'goods:miaosha:'; $userResult = $redis->get($key.$userId); if($userResult){ $userResult = json_decode($userResult,true); echo json_encode(['result'=>$userResult['result'],'key'=>$key.$userId]);// 已經參與過秒殺了 die(); }else{ $goodqueue = 'goods:queue:'.$goodsId; $result = $redis->lpop($goodqueue);// 刪除商品redis隊列緩存 if($result){ $data = json_encode(['result'=>'OK','userId'=>$userId,'goodsId'=>$goodsId]); $redis->set($key.$userId,$data);// 將秒殺信息寫入緩存中 echo json_encode(['result'=>'OK','userId'=>$userId,'goodsId'=>$goodsId,'key'=>$key.$userId]); die(); }else{ echo json_encode(['result'=>'FAIL','message'=>'商品不存在','goodsId'=>$goodsId]);// 商品庫存不存在 die(); } }
客戶端在接收到秒殺請求結果後,進行支付
<?php /** * 用戶下單界面 */ require_once __DIR__.'/redis_connect.php'; $key = $_GET['key']; $data = $redis->get($key); /** * 生成訂單,訂單入庫 * */