阿里一位大牛曾經說過,應用性能拓展的三要素是:緩存,異步,批處理
秒殺業務在如今的電商平臺中十分常見,如淘寶雙十一秒殺,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