商城秒殺:原理與實戰

Jmeter壓力測試

  我們的服務器,頁面能承擔多少數據來訪問,就需要用到壓力測試,就是模擬實際用戶使用情況,讓軟件在長時間超大負荷的情況下進行測試,來檢測系統的性能、可靠性。用Jmeter來實現。
  Jmeter安裝好後,進入Jmeter的可視化界面。所有的測試計劃都是在TestPlan中制定。點擊一個線程組來模擬用戶。然後在裏面設定用戶量,每個用戶發出多少次Http請求。然後配置HTTP請求的基本信息。
在這裏插入圖片描述
  點擊運行後想要看到結果,還需要在Jmeter中創建一個listener表,即summary Report,在這個聚合報表中就會出現我們發送的統計信息。其中有一個參數就是吞吐量,也就是你服務器每秒能處理多少個請求。
在這裏插入圖片描述
  最小14毫秒可以看到結果,最長6315毫秒看到結果,平均等待418毫秒看到結果,之所以是這樣是由底層設計造成的。205的吞吐量,也就是400個人,每個人發1個請求,需要2秒才能解決。可以看到,這裏最長等待時間太長和吞吐量太低。

穩定數據優化—處理平時不怎麼變更的數據進行優化

  在高併發環境下,大部分都是讀取數據的,所以數據庫最有可能成爲高併發的瓶頸,因此提高數據庫效率或者較低數據庫交互式首要考慮的問題。在電商中,很大一部分數據是在一段時間內穩定不變的,例如商品信息、會員信息等,對於穩定數據,常有兩種方式進行高併發處理:1.利用緩存(redis、memcached) 2.利用靜態化技術,轉化成html(因爲動態頁面會有一個服務器進行頁面渲染過程,會消耗資源,我們把一次訪問的結果轉成HTML靜態頁面進行存儲,下次訪問都訪問靜態頁面,從而降低服務器壓力)。
  在代碼中,我們讓商品信息的數據進行緩存處理,因爲商品是相對固定的。配置文件配置好後,用cacheable註解。該註解的含義是將第一次訪問的時候將方法的返回結果放入緩存,第二次訪問時不再執行方法內部的代碼,而是從緩存中直接提取數據。Cacheable需要兩個參數,一個key,一個value。
在這裏插入圖片描述
  Cacheable又叫做聲明式緩存。
  Redis也有可視化管理控制器,他有16個數據庫。我們要把對象放入redis緩存,必須要把這個對象序列化,也就是這個對象必須實現serializable。這樣,當我們第一次訪問網頁的時候比較i慢,第二次訪問的時候就會快很多。(另外幾個是因爲上面代碼我沒截圖完)
在這裏插入圖片描述
  此時跑了一次後我們再用Jmeter進行壓力測試,會發現吞吐量好了很多。

靜態化技術

  動態頁面便於管理,但是訪問網頁需要服務器進行頁面渲染導致消耗資源,而靜態頁面訪問速度快,但不易於管理,因此靜態化就是把兩者結合在了一起。也就是一次訪問把動態頁面渲染後變成靜態頁面存儲下來,之後的訪問就訪問靜態頁面了。也就是讓nginx去訪問靜態頁面。
在這裏插入圖片描述
  要實現靜態化技術,首先就要編寫靜態化處理代碼(輸出靜態處理的頁面地址):把返回的結果對象存放在Hashmap中,然後通過process進行生成靜態頁面。
在這裏插入圖片描述
  通過模板對象的process方法,該方法裏面有兩個參數,第一個參數是數據,第二個參數是哪裏輸出。FreemarkerConfig就是我們的容器。

  現在我們文件中有這麼一個HTML靜態頁面了,但是現在我們生成的html沒有樣式,把樣式複製進文件夾就可以了。
  現在靜態頁面有了,接下來要做的就是用Nginx與之進行綁定。Nginx是一款輕量級的web服務器和反向代理服務器,內存佔用少併發能力強。把Nginx安裝好後,打開Nginx,Nginx的端口號是80,Nginx打開只需要直接運行exe文件就可以了,關閉用命令行nginx –s stop就可以了。Nginx的核心配置文件是Nginx.conf,我們在覈心配置文件中增加訪問地址:
在這裏插入圖片描述
  這樣我們在網頁輸入localhost/goods/1333.html 就可以訪問了。Nginxx的地址也是Localhost。

自動靜態化處理—針對量級很大的情況

  因爲我們頁面不可能一直處於靜態的情況,可能會出現商品更新,此時就需要重新靜態化,對面量級很大的靜態化不可能重新把所有數據進行重新靜態化,就需要用到自動靜態化處理。
  自動靜態化處理依賴於任務調度:啓動任務調度的註釋@EnableScheduling。現在正在編寫任務調度代碼:
在這裏插入圖片描述
  用@Scheduled註解表示我要按照指定的時間來執行我們的代碼,cron=”* * * * * ?”即按照每秒鐘執行一次,cron表達式來規定執行時間,代表秒 分 時 日 月 星期。有了任務調度後,我們可以規定把規定時間內例如5分鐘修改的數據進行靜態化,就減少服務器壓力很多。要這樣設置,我們就需要在goods表中添加一個最近更新時間的行,然後在程序中獲取5分鐘內修改過的數據對象,把這些對象進行重新靜態化就可以了。
(獲取最近五分鐘修改過的數據)

動靜數據分離

  在一個網頁中,除了有靜態數據,還應該有動態的經常變更的數據,例如評論,所以我們需要對這些靜態和動態數據進行區別對待。對於這種動態數據就需要在靜態頁面中使用AJAX動態加載後端產生的數據。當我們HTML用AJAX發出請求的時候,注意是向我們的NGINX發出的請求,而不是向後臺Tomcat發出請求,Nginx作爲代理服務器轉交給Tomcat去Mysql中取數據。這裏注意的是,所有的請求和交互都是通過Nginx進行處理的。
在這裏插入圖片描述
  在數據庫中生成一張評論表。對於前端代碼參考網站layui。因爲要用到Nginx所以需要在Nginx的配置文件nginx.conf中進行配置代理的是誰。

秒殺活動-超賣現象

  秒殺是最常見的高併發,且是瞬時的高併發。在秒殺中,最常見的挑戰是爲了避免超賣,即如何避免購買商品的人數不超過商品數量上限。現在我們模擬這個過程。
Dao層:
在這裏插入圖片描述
Service層:
在這裏插入圖片描述
Web層:
在這裏插入圖片描述

  編寫好後,我們網頁輸入多次刷新是沒有問題的,但是一旦我們用Jmeter模擬高併發的場景,就會報錯了,因爲會出現線程安全,此時你會發現購買的商品就超賣了,因爲當多個人同時訪問代碼的時候,可能同時都拿到的是10,然後同時減1,變量是9,但是卻賣了多個。這個時候就要用到redis緩存來進行互存預減的方式解決。

用redis解決超發問題

  之所以用redis解決超發問題,是因爲1.redis是單線程模型,redis操作的所有指令都會以隊列的形式在單線程中處理,早來的早處理,不會出現同時被處理的情況。2.redi是內存存儲,效率高。3.天生分佈式支持。解決思路如下:

①活動開始
  從活動商品列表中找到正在進行秒殺活動的商品,可以用任務調度定時掃描秒殺活動表,然後找到要進行的活動後用Redis中提供的List列表類型,我們把十個商品放入List當中作爲商品庫存,因爲redis是單線程的,所以不用擔心之前出現的多個用戶訪問的線程安全的問題。

②搶購過程
  Redis中還提供了一個set類型,當一個用戶買了商品後,該商品從List當中彈出,然後把用戶加入到set類型裏。之所以用Set是因爲set元素唯一,避免了同一個用戶同時秒殺多個。
在這裏插入圖片描述
③編寫任務調度刪除在活動商品表中已經結束了的活動商品
  現在編寫代碼:
在這裏插入圖片描述
  先定義了一個要秒殺的商品對象Promotionseckill,也把該對象在數據庫中新建了一個表。在springboot中提供了操作redis的模板類,叫做RedisTemplate,我們要用redis操作就要先注入RedisTemplate。Redis的使用之前也需要配置配置文件哈。
  在進行商品list的注入的時候,是必須在這個秒殺商品是在活動時間內爲前提的,所以會先一個findUnstartSeckill()方法返回在規定時間內的秒殺商品,任務調度是在0秒的時候開始接下來的0秒每一秒執行一次。插入之前還需要刪除以前重複的活動任務。RedisTemplate對象他會默認使用jdk的序列化方式幫我們把對象序列化。
  現在編寫搶購過程的代碼。先講講爲什麼之前不用redis時的超發出現原因,因爲之前的程序中都是獲取當前的庫存,因爲獲取庫存和更新庫存有時間差,在高併發的環境下就可能出現超發。而在redis裏面,通過預減過程,如果商品從List彈出成功,則把獲取商品的用戶加入set,當彈出完再次彈出則返回null,遇到null就不會再把用戶加入set了。
在這裏插入圖片描述
  OpsForList()是針對這個List集合的,leftPop()是左側彈出。通過responsseBody註解來給客戶端把map結構轉成json格式的字符串返回return。
在這裏插入圖片描述

  一個用戶不能搶購多次,所以還需要有一個去重處理,即在service層判斷同一個用戶是否多次搶購到。否則我們的set裏面就不是10個了,這裏就體現了爲什麼用set,因爲set不允許重複元素的出現,可以根據這個特性來進行去重判斷,判斷set中是否已經擁有了這個用戶。
在這裏插入圖片描述

利用MQ進行訂單流量的削峯限流

  之所以需要MQ進行削峯限流,是因爲前臺業務和後臺業務處理能力不對稱造成的。首先,秒殺用戶前面已經講過用redis緩存的方式進行確權得到哪些用戶可以購買,可以購買的用戶都存放在redis的set集合裏,對於redis而言處理能力很快,每秒可以處理1000個訂單,但是後臺支付訂單處理能力就很慢了,因爲可能會和多個系統進行對接,那麼每秒可能只能處理200個訂單,這樣當1000個用戶同時給後臺服務器處理的時候,就會出現問題。所以傳統同步處理方式不可取,我們不可能讓用戶提交訂單後一直等待後臺服務器把其他業務處理完了然後再來處理你的訂單,此時就需要異步處理。
在這裏插入圖片描述

  此時,一秒鐘1000個用戶都給MQ,消費者每次只從MQ中提取200個,從而預防宕機。
在這裏插入圖片描述

  現在模擬這個過程,先創建一張用戶訂單表,然後把RabbitMQ配置進spring boot中。然後把我們前臺頁面推送的訂單編號給rabbitmq,處理訂單的業務邏輯就根據自己的處理能力去MQ中拿取訂單進行處理返回,但這裏返回的並不是處理結果,而是訂單編號OrderNo,這個訂單編號是用來進行信息檢索判斷訂單是否創建成功的,當返回訂單編號後可以隔幾秒鐘再次發送一個checkorder去檢查這個訂單是否已經創建好了,如果沒創建成功則繼續等待,直到創建成功。
在這裏插入圖片描述
  現在模擬代碼:要使用RabbitMQ,需要把rabbitTemplate注入進去,這個模板對象封裝了rabbitm的一系列的操作方法。
①Service層
  把用戶id和隨機生成的訂單號放入map,然後把map放入MQ中。
ConvertAndSend有三個參數,第一個是交換機名字,第二個是路由件爲Null,第三個是數據data。返回訂單信息oorderNo給web層打印輸出。

②現在給一個web層代碼
  凡是能進入sendOrderToQueue代碼的用戶,都是在redis的set中的擁有購買權限的用戶,因爲不是得話就進入catch了。
在這裏插入圖片描述

③在rabbitmq的可視化管理中localhost:15672,創建好我們的mq隊列以及交換機。
④訂單處理業務,從MQ中抽取訂單來進行處理。在操作之前,需要配置定義消費者最多同時處理多少個消息,這裏是10個。
在這裏插入圖片描述
  然後用RabbitListener註解來獲取到我們配置的MQ,包括隊列和交換機信息。

@Component
public class OrderConsumer {
    @Resource
    private OrderDAO orderDAO;

    @RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue(value = "queue-order") ,
                    exchange = @Exchange(value = "exchange-order" , type = "fanout")
            )
    )
    @RabbitHandler
    public void handleMessage(@Payload Map data , Channel channel ,
                              @Headers Map<String,Object> headers){
        System.out.println("=======獲取到訂單數據:" + data + "===========);");
        try {
            //對接支付寶、對接物流系統、日誌登記。。。。
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Order order = new Order();
            order.setOrderNo(data.get("orderNo").toString());
            order.setOrderStatus(0);
            order.setUserid(data.get("userid").toString());
            order.setRecvName("xxx");
            order.setRecvMobile("1393310xxxx");
            order.setRecvAddress("xxxxxxxxxx");
            order.setAmout(19.8f);
            order.setPostage(0f);
            order.setCreateTime(new Date());
            orderDAO.insert(order);
            Long tag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
            channel.basicAck(tag , false);//消息確認 
            System.out.println(data.get("orderNo") + "訂單已創建");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

  basicAck有兩個參數,第一個參數是channel的id號,第二個參數是false表示不進行批量接收。

⑤檢查訂單是否創建好
  因爲創建訂單有一個過程,當我們把訂單發送給MQ時因爲MQ是異步處理業務所以很快就會返回一個訂單編號(service層把訂單給MQ後直接返回了訂單編號,不用管MQ過程),前端拿到訂單編號後不代表數據庫中就有訂單了(訂單就創建好了),因爲訂單處理業務可能還需要連接其他端口等很慢,所以需要在ajax中調用web層方法查詢訂單是否創建好(ajax中可以設定每幾秒鐘調用一次方法),否則就一直處於等待頁面。

Nginx負載均衡

  之前的所有秒殺操作,都是在單臺電腦上完成的,單臺電腦的處理能力是有極限的,往常是需要多臺服務器來處理,也就要用到Nginx負載均衡。負載均衡就是把任務分配到多個服務器上分別執行進而達到提高執行效率縮短執行時間的目的,負載均衡有兩種:硬負載均衡、軟負載均衡。
  硬件負載均衡常見的f5,是封裝好的硬件專門負責負載均衡的。軟負載均衡就是我們用一臺電腦下載好負載均衡的軟件例如Njinx來實現負載均衡,常用於web領域。
  負載均衡的算法有:輪詢策略(默認)、權重策略、最少連接數、IP綁定策列(源地址散列算法)、隨機策列(random)、按響應時間(第三方,響應時間最短的優先)
  例如,我們在代碼中模擬這麼一個過程:
  我用四個不同的端口啓動四份不同的服務器,那麼我在訪問的時候就很麻煩,因爲我就需要選擇是訪問Localhost:8001/goods?gid=1335還是Localhost:8002還是8003還是8004,此時就需要用Nginx來幫忙分配。

首先安裝好Nginx,然後再Nginx.conf核心配置文件中進行配置。

①Upstream 用來配置後端服務器池。
在這裏插入圖片描述
②接下來配置端口的轉發和映射(轉發策略),用server。Location指的是對哪些進行映射,斜槓代表全部。
在這裏插入圖片描述

③然後在命令行輸入:start nginx.exe啓動nginx。
  現在我們用80端口進行請求轉發結果如下(默認的80端口,可以不用寫就默認):
在這裏插入圖片描述
  默認的負載均衡策略是輪詢,但如果你像修改策列,就需要在Nginx.conf配置文件中進行配置,例如如果你要用最少連接策略,則配置加一個東西:
在這裏插入圖片描述
  如果是用權重:
在這裏插入圖片描述

  Nginx也有故障排查的功能,也就是發送心跳包的形式,需要設置兩個參數,max_fails和fail_timeout,第一個參數是最大失敗次數,第二個參數代表單次連接超時時間,如果我Nginx發送給某一個服務器連續3次超時,則有問題,剔除該服務器。
在這裏插入圖片描述

設置Nginx集羣session共享

  Nginx環境下存在session不同步的問題,例如客戶端發送登陸請求後給nginx,nginx再分配給不同的服務器處理這個登陸,登陸成功後,登陸的賬號密碼存儲在該服務器的session中,例如下面的session-user:laoqi,此時用戶按F5刷新一下,他又把請求給了第二臺服務器,這樣就會回到登陸頁。
在這裏插入圖片描述
  要解決這個問題,就需要用到redis,之前問題出現的原因是session信息放到自己的主機上,現在我們把這些登陸信息放到redis服務器裏面,多臺後端服務器共享一個redis資源,這樣拿登陸的數據就去redis服務器裏面拿就不會出現這種情況了。
在這裏插入圖片描述

  要使用redis服務器,spring提供了一個子工程叫做spring session,spring session提供了管理用戶session的API實現,把spring session注入容器後,進行redis的配置,也就是redis配置在哪臺主機上,端口號是多少,密碼是多少等等。
  配置好後,他會自動的對session的操作進行監聽。Session放在redis裏有一個過期時間,當session放入redis後就需要設置redis裏對session的過期時間。
在這裏插入圖片描述

Nginx靜態資源緩存

  Tomcat中的靜態資源包括css、js,這些是不怎麼變的靜態資源,每次訪問都會對他們進行解析,當訪問量大的時候每次都要解析就很消耗資源。所以需要用到Nginx的靜態資源緩存來降低Tomcat的壓力。
在這裏插入圖片描述
  要使用Nginx靜態資源緩存,先要在Nginx.conf中配置兩個文件夾,一個是臨時文件夾,一個是設置緩存目錄。然後配置哪些靜態資源需要進行緩存,一般包括gif、jpg、css、png、js等。配置好後,就可以和之前講的把對象進行緩存結合起來,process方法後存儲的對象放在靜態資源緩存的地方,就可以了。

Nginx資源壓縮

  數據傳輸需要用到帶寬,而帶寬的租賃特別貴,所以帶寬大小就有限制,因此我們需要把我們的數據進行壓縮。
在這裏插入圖片描述
  當秒殺開始的時候,如上在100mbps的帶寬情況下,一下只能讓128位用戶下載我們的js。所以,我們用Nginx把緩存的js這些壓縮傳送給瀏覽器自己解壓。也需要在nginx.conf中配置即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章