高並發解決方案之 redis原子操作(適用於秒殺場景)


秒殺活動:

秒殺場景一般會在電商網站或(APP/小程序)舉行一些活動或者節假日在12306網站上搶票時遇到。對於一些稀缺或者特價商品,一般會在約定時間點對其進行限量銷售,因為這些商品的特殊性,會吸引大量用戶前來搶購,並且會在約定的時間點同時在秒殺頁面進行搶購。

秒殺場景特點:

秒殺時大量用戶會在同一時間同時進行搶購,網站瞬時訪問流量激增。
秒殺一般是訪問請求數量遠遠大於庫存數量,只有少部分用戶能夠秒殺成功。
秒殺業務流程比較簡單,一般就是下訂單減庫存。

秒殺架構設計理念:

限流: 鑒於只有少部分用戶能夠秒殺成功,所以要限制大部分流量,只允許少部分流量進入服務后端。
削峰:對於秒殺系統瞬時會有大量用戶涌入,所以在搶購一開始會有很高的瞬間峰值。高峰值流量是壓垮系統很重要的原因,所以如何把瞬間的高流量變成一段時間平穩的流量也是設計秒殺系統很重要的思路。實現削峰的常用的方法有利用緩存和消息中間件等技術。
異步處理:秒殺系統是一個高並發系統,采用異步處理模式可以極大地提高系統並發量,其實異步處理就是削峰的一種實現方式。
內存緩存:秒殺系統最大的瓶頸一般都是數據庫讀寫,由於數據庫讀寫屬於磁盤IO,性能很低,如果能夠把部分數據或業務邏輯轉移到內存緩存,效率會有極大地提升。
可拓展:當然如果我們想支持更多用戶,更大的並發,最好就將系統設計成彈性可拓展的,如果流量來了,拓展機器就好了。像淘寶、京東等雙十一活動時會增加大量機器應對交易高峰。

設計思路:

將請求攔截在系統上游,降低下游壓力:秒殺系統特點是並發量極大,但實際秒殺成功的請求數量卻很少,所以如果不在前端攔截很可能造成數據庫讀寫鎖沖突,甚至導致死鎖,最終請求超時。
充分利用緩存:利用緩存可極大提高系統讀寫速度。
消息隊列:消息隊列可以削峰,將攔截大量並發請求,這也是一個異步處理過程,后台業務根據自己的處理能力,從消息隊列中主動的拉取請求消息進行業務處理。

前端方案 :瀏覽器端(js)

頁面靜態化:將活動頁面上的所有可以靜態的元素全部靜態化,並盡量減少動態元素。通過CDN來抗峰值。
禁止重復提交:用戶提交之后按鈕置灰,禁止重復提交
用戶限流:在某一時間段內只允許用戶提交一次請求,比如可以采取IP限流

后端方案

利用redis實現簡單的秒殺系統
Redis是一個分布式緩存系統,支持多種數據結構,我們可以利用Redis輕松實現一個強大的秒殺系統。
利用redis記錄讀取實時庫存。一旦庫存不足,立即拋出異常。反饋給前端。如果庫存足夠,通過rpc調用通知訂單微服務系統生成訂單。得到訂單系統的生成成功的反饋以后,在秒殺微服務系統中生成一個本地訂單記錄,在數據庫增銷量減庫存。
Redis 提供了 INCR/DECR/SETNX 命令,把RMW三個操作轉變為一個原子操作
Redis 是使用單線程串行處理客戶端的請求來操作命令,所以當 Redis 執行某個命令操作時,其他命令是無法執行的,這相當於命令操作是互斥執行的

生成秒殺訂單: 點擊查看代碼
//生成秒殺訂單
public function createLimitOrder($storeId, $userId, $unionId, $subStoreId, $activityId, $skuId, $limitPrice, $selectedNum, $consigneeId, $reservationTime, $message)
    {
        //...
        //通過redis 檢查是否還有充足庫存,如果不足,則拋出異常
        $this->controlWithRedisStock($storeId, $activityInfo, $skuId, $selectedNum);
        //...
        //調用rpc 使order微服務系統生成訂單,並返回orderId。
        $orderId = $this->orderRpc->createOrder($storeId, $userId, $subStoreId, $activityInfo, $skuId, $activityPrice, $selectedNum, $consigneeId, $reservationTime, $message);
        try {
            Db::beginTransaction();
            if ($orderId > 0) {
                //記錄本地秒殺訂單,和訂單系統的orderId綁定聯系起來。
                $id = $this->addLocalOrder($storeId, $userId, $orderId, $skuId, $selectedNum, $consigneeId, $activityInfo);
                $this->addSales($storeId, $activityId, $skuId, $selectedNum); //做已經出售增加 (庫存減少) //下單進行此操作
            }
        } catch (\Throwable $e) {
            throw new ErrException(ExceptionParseData::parseData($e));
            Db::rollback();
        }
        Db::commit();

        return $orderId;
    }
redis控制庫存:點擊查看代碼
private function controlWithRedisStock($storeId, $activityInfo, $skuId, $selectedNum)
    {
        //redis 計數 (原子操作)---start---------------解決高並發問題------------------ 
        if($activityInfo['inf'] == LimitCFG::STOCK_INF){  //無限庫存
            return true;
        }

        $key = $this->getRedisStockKey($storeId, $activityInfo["id"], $skuId);
        $redis_stock = $this->getOrSetRedisStockValByKey($key, $activityInfo['stock']);
        if ($redis_stock <= 0) { //初步抵擋一下:這邊的redis_stock 有可能不是最新的了。因為有一些請求已經同時通過了這一步,還沒有來及更新它。
            throw new ErrException(ExceptionCode::E12141); //  ["12141", "庫存不足了,無法下單"]
        }
        //這句是  原子性的 --- 程序到這里開始變成 串行()。
        $redis_stock = $this->decRedisStockByKey($key, $selectedNum);

        if ($redis_stock < 0) {
           //如果減多了再加回來
            $this->addRedisStockByKey($key, $selectedNum);
            throw new ErrException(ExceptionCode::E12141); //  ["12141", "庫存不足了,無法下單"]
        }
        return true;
        //redis 計數  (原子操作)---end---------------
    }

 public function getOrSetRedisStockValByKey($key, $dbStock)
    {

        $this->commonRedis->setNx($key, $dbStock);

        $redis_stock = $this->commonRedis->get($key);
        // var_dump($redis_stock);
        return $redis_stock;
    }

      public function decRedisStockByKey($key, $selectedNum)
    {
       return  $this->commonRedis->decrBy($key, $selectedNum);  //原子操作,返回的是 減過之后的值。有用,后續還要判斷
        
    }

參考:[https://www.cnblogs.com/wangzhongqiu/p/6557596.html] [https://blog.csdn.net/HiJamesChen/article/details/109382666]


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM