一個秒殺系統的結構設計

阿里一位大牛曾經說過,應用性能拓展的三要素是:緩存,異步,批處理

秒殺業務在如今的電商平臺中十分常見,如淘寶雙十一秒殺,Nike官網的秒殺等等。同時,也是考驗程序員架構能力和綜合知識掌握的一個重要部分,本次博客根據筆者多年代碼經驗就秒殺業務討論一下它的架構設計

秒殺面對的問題

1. 高併發下單請求

秒殺系統的一個重要特徵就是在一瞬間的高併發下單請求,此時,一個Tomcat是扛不住上萬QPS的。此時我們可以設置分佈式集羣,Nginx負載均衡,Redis緩存,流量削峯等等手段方法來迎接這麼一個十萬百萬級別QPS的高併發請求

2. 盜刷接口

譬如去年中秋節在網上瘋傳的阿里中秋月餅事件

秒殺活動最怕的是刷單問題,腳本刷單會造成大量頻繁一致的請求,同時還會造成秒殺活動的不公平性。防止這種不斷髮起的重複請求,我們可以在前端設置請求間隔,譬如1s內不能多次點擊,後臺對同一身份進行令牌請求限制。或者設置動態變換的接口

3. 訂單超買

其實超賣或者少賣是和業務邏輯有關的,有些促銷活動可以接受超買,有些促銷活動不接受超買。在本次秒殺系統設計中,我們設置的業務邏輯是不可超買

在高併發的情況下,可以通過在數據庫中設置隔離級別,樂觀鎖等等方法實現

秒殺業務的設計方案

1. 秒殺業務分佈式水平擴展

業務性能達到瓶頸,此時最簡單的就是再增加一臺一模一樣的後臺服務器,進行分佈式處理。然後使用nginx作爲靜態服務器,進行負載均衡(常用算法如輪詢)和反向代理

  • 通過tomcat-log來記載nginx的訪問日誌

  • 保證nginx和後臺服務tomcat的長連接

  • Nginx主要是通過epoll多路複用模型,master worker進程模型和協程機制達到高性能的要求

2. 查詢緩存之Redis緩存

衆所周知,當緩存離用戶越近,效果越好,但同時,代價也越大。首先我們可以將商品數據存儲在Redis緩存當中

  • redis緩存共分爲單機版,sentinel哨兵模式和cluster模式
  • 在單機版中,我們可以把商品詳情存入redis中,當修改商品或者下單減庫存以後對該商品詳情進行更新
  • 同時,還要設置每一個key值的過期時間
3. 查詢緩存之nginx緩存
  • 有兩種緩存方式,一種是nginx proxy cache,另一種是nginx lua cache
  • 其中,第一種是將後臺的數據通過nginx存入文件中,這樣其實性能比較差
  • 第二種通過nginx lua dic可以將數據放入內存中,增加效率,也可以通過nginx lua redis直接到redis中把數據讀取出來
4. 查詢緩存之頁面靜態化
  • nginx對靜態文件的優化不是特別好,我們可以將靜態頁面放到CDN(Content Distributed Network)中
  • 當用戶發出DNS請求時,發送到DNS服務器,DNS服務器查詢到是CNAME地址,然後把DNS請求解析發送給CNAME服務器進行DNS解析(CNAME地址就指向阿里雲的服務器),然後CNAME服務器會找到用戶IP 的歸屬地,然後交給用戶歸屬地的服務器查看是否有請求的靜態文件,有的話就進分發,沒有就交給我們本來的nginx進行解析,把靜態資源發送給用戶
  • 我們可以將請求後的json數據也作爲和js,css,html,image一樣的靜態文件保存下來
  • 也可以通過phantomjs將頁面請求後渲染的HTML保存下來,在服務端完成html,css甚至js的load渲染成純HTML文件以後直接以靜態資源部署在CDN上
5. 下單預減庫存異步處理
  • 在處理了靜態查詢頁面之後,我們還需要對下單操作進行優化
  • 最常見的方式是使用redis預先減庫存,然後把庫存消息通過消息中間件如RocketMQ把消息異步持久化到數據庫中
  • 同時,引入RocketMQ的Transaction機制,其中的sendTransaction用到了兩階段提交
  • 爲了保證checkLocalTransaction()可以check到響應的狀態,我們需要LogData(指訂單操作流水)因爲訂單流水的單獨行鎖和商品ID的行鎖熱點不同,所以訂單流水的行鎖更新可以接受
  • 同時,當庫存售罄以後,我們可以在redis中打上標記,直接對下單請求拋出“庫存不足”異常,減輕數據庫的壓力
6. 下單操作之SQL優化
  • 如果使用數據庫update做隱式加排他鎖時:

    update item_stock set stock = stock - {amount} where item_id = {itemId} and stock >= {amount}
    

    我們需要注意在Innodb中,把item_id設置爲索引,這樣會把表鎖變爲行鎖

  • 同時,我們也可以在讀未提交的隔離級別中使用樂觀鎖:

    update item_stock set stock = stock - {amount} and version = {version} + 1 where item_id = {item_id} and version = {version}
    
7. 流量削峯之令牌桶
  • 所謂令牌桶算法的基本思路是:每個請求嘗試獲取一個令牌,後端只處理持有令牌的請求,生產令牌的速度和效率我們都可以自己限定,guava提供了RateLimter的api供我們使用
  • 我們還可以在發放令牌時加上驗證機制,如身份,商品、活動是否存在,以此來防止盜刷
8. 流量削峯之隊伍泄洪
  • 這裏我們使用隊列的方式,其實和令牌桶的處理有相似之處

  • 開一個線程池,根據配置自動讀取線程池核心線程數,一次只能併發執行核心線程數個請求。其餘請求阻塞在隊列中

    threadPoolExecutor = new ThreadPoolExecutor(20, 40, 1L, TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(100),
                    new ThreadPoolExecutor.CallerRunsPolicy());
    threadPoolExecutor.submit(() -> {
        String orderLogId = orderService.initOrderLog(orderDTO.getItemId(),orderDTO.getAmount());
        if (!mqProducer.transactionAsyncReduceStock(orderDTO,orderLogId)) {
            throw new ReturnException(EmReturnError.UNKNOWN_ERROR, "下單失敗");
        }
        return null;
    });
    
9. 服務器的配置
  • 首先,我們可以配置Tomcat的連接

    server.tomcat.accept-count:100 #等待隊列長度
    server.tomcat.max-connections:10000 #最大可被連接數 1000
    server.tomcat.max-threads:200 #最大工作線程數  800
    server.tomcat.min-spare-threads:10 #最小工作線程數  100
    
  • 其次,我們可以配置cache-control對頁面靜態化進行緩存回源設置

  • 同時,也可以配置Mysql的一些設置

    max_connection = 100
    innodb_file_per_table = 1
    innodb_buffer_pool_size = 1G
    innodb_log_file_size = 256M 
    innodb_log_buffer_size = 16M # 日誌發生切換時,仍然有緩衝可以寫日誌
    innodb_flush_log_at_trx_commit =2 #只調用write方法寫入linux的cache中,每隔1s後調用flush方法
    							   =1 #只要事物提交,立刻把log刷盤
    							   =0 #每隔1s,flush到磁盤中
    innodb_data_file_path=ibdata1:1G;ibdata2:1G;ibdata3:1G:autoextend #mysql文件分區
    

結構圖

此處的架構沒有考慮分佈式和CDN
在這裏插入圖片描述

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