轉載:
https://mp.weixin.qq.com/s?__biz=Mzg5MzY5NDM3MQ==&mid=2247490866&idx=1&sn=0081517454680c85e0ed23eda4e82df5&chksm=c02ba5fef75c2ce8b0c7f54182f3bda539230c75d2d75ed2b514b93decc0ff0c5de548a35dc3&cur_album_id=3548464749150224391&scene=190#rd
1.秒殺系統的方案設計要點
2.秒殺系統的數據 + 頁面 + 接口的處理方案
3.秒殺系統的負載均衡方案底層相關
4.秒殺系統的限流機制和超賣問題處理
5.秒殺系統的異步下單和高可用方案
1.秒殺系統的方案設計要點
(1)秒殺促銷活動的數據處理
(2)秒殺促銷活動的頁面處理
(3)秒殺場景下的負載均衡架構
(4)秒殺場景下的高并發搶購處理
(5)秒殺場景下的異步下單處理
(6)秒殺成功后的業務邏輯處理
(7)秒殺系統的高可用設計
(8)秒殺系統的壓測 + 故障演練以及實時大盤
(1)秒殺促銷活動的數據處理
一.秒殺促銷活動的業務流程
二.秒殺促銷活動的Redis數據結構
(2)秒殺促銷活動的頁面處理
一.秒殺活動頁面靜態化處理
二.頁面靜態化以及CDN緩存處理
三.靜態頁面的緩存以及文件服務器存儲
四.秒殺接口url的隱藏處理
五.后端與前端秒殺倒計時的時鐘同步
六.秒殺開始時的驗證碼處理
七.秒殺活動頁面的限流處理
(3)秒殺場景下的負載均衡架構
一.防黑客DDoS攻擊的高防IP處理
二.秒殺場景下的SLB負載均衡架構
三.基于SLB的秒殺場景和普通場景的分流隔離
四.基于Nginx的秒殺請求分發
五.基于Nginx的秒殺請求限流
六.Nginx的內核參數調優
(4)秒殺場景下的高并發搶購處理
一.秒殺搶購請求的處理鏈路
二.Tomcat的內核參數調優
三.秒殺搶購接口的限流處理
四.秒殺搶購請求的去重處理
五.秒殺接口的防刷防作弊處理
六.商品庫存的Redis分布式緩存
七.商品庫存的Lua腳本扣減邏輯
八.商品庫存的超賣問題處理
九.秒殺接口的多線程并發優化
十.秒殺接口的Disruptor內存隊列異步化
(5)秒殺場景下的異步下單處理
一.秒殺場景下的RocketMQ集群架構
二.秒殺場景下的消息隊列
三.秒殺場景下的Redis + RocketMQ一致性回滾
四.秒殺場景下的數據庫架構
五.秒殺場景下的分庫分表技術
六.高并發場景下的數據庫壓測
七.高并發場景下的數據庫連接池參數調優
八.高并發場景下的數據庫內核參數調優
九.秒殺下單服務的核心業務邏輯
(6)秒殺成功后的業務邏輯處理
一.秒殺成功后的異步通知
二.秒殺成功后的訂單查詢
三.秒殺成功后的訂單支付及后續邏輯
四.秒殺成功后長期不支付的處理
(7)秒殺系統的高可用設計
一.秒殺系統全鏈路的中間件高可用
二.秒殺系統全鏈路的高可用降級
三.Redis緩存崩潰后的秒殺系統自動恢復
四.RocketMQ集群崩潰后的臨時本地存儲降級
五.數據庫集群崩潰后的臨時本地存儲降級
六.秒殺服務崩潰后的防服務雪崩降級
七.秒殺系統的全鏈路漏斗式流量限制
八.秒殺系統的雙機房多活部署
九.秒殺系統部分機房故障時的降級
(8)秒殺系統的壓測 + 故障演練以及實時大盤
一.秒殺系統的全鏈路壓測及針對性優化
二.秒殺系統的全鏈路故障演練及高可用驗證
三.秒殺系統基于大數據技術的實時數據大盤
2.秒殺系統的數據 + 頁面 + 接口的處理方案
(1)秒殺活動的描述
(2)秒殺活動管理系統的功能
(3)秒殺活動數據進行數據庫 + 緩存雙寫
(4)秒殺活動數據存儲在Redis的List和Hash中
(5)秒殺活動頁面數據的動靜分離
(6)基于CDN來緩存秒殺活動靜態頁面
(7)CDN靜態數據緩存的失效與命中
(8)基于定時授時的前后端時鐘同步處理
(9)秒殺搶購接口地址的動態隱藏處理
(1)秒殺活動的描述
每天會有多個場次的秒殺活動,不同場次的秒殺活動有不同的秒殺商品。比如10:00正在搶購、12:00即將開始、14:00即將開始、16:00即將開始等。
(2)秒殺活動管理系統的功能
新建一個秒殺場次,秒殺場次會有開始時間和結束時間。比如一個秒殺場次持續3天,每天都是12點時開始搶購。可以給一個秒殺場次加入一些秒殺商品,并設置對應的折扣、價格和數量等。
(3)秒殺活動數據進行數據庫 + 緩存雙寫
首先,秒殺活動的數據會存放在數據庫里。然后,秒殺活動系統會對外提供一個接口,通過這個接口可以查詢獲取配置的秒殺場次和秒殺商品。一旦配置好秒殺活動的數據,之后一般不會隨便變動。
所以當用戶在APP大量查詢查秒殺活動數據時,就沒必要頻繁查數據庫了。因此可采取數據庫 + 緩存雙寫的模式,秒殺活動的數據也存儲一份到緩存中,并在查詢時直接查緩存中的數據。
(4)秒殺活動數據存儲在Redis的List和Hash中
可以通過Redis的List和Hash數據結構來存儲秒殺活動數據。比如可以通過一個key為"seckill::#{日期}::rounds"的List數據結構,存儲當日秒殺場次,List里可以存放秒殺場次的ID主鍵值。這樣根據"seckill::#{日期}::rounds",就能獲取當日秒殺場次的主鍵ID。
比如可以通過一個key為"seckill::round::#{秒殺場次ID}::info"的Hash數據結構,存儲秒殺場次的基本信息,Hash里可存放秒殺場次開始和結束時間等。接著根據秒殺場次的主鍵ID,就可以獲取每個秒殺場次的具體數據。
比如可以通過key為"seckill::round::#{秒殺場次ID}::products"的List數據結構,存儲秒殺場次對應的所有秒殺商品信息,這樣根據秒殺場次ID就能獲取其對應的秒殺商品的ID集合。
比如可以通過key為"seckill::#{秒殺場次ID}::#{商品ID}::info"的Hash數據結構,存儲秒殺商品的具體數據,比如秒殺價格和秒殺數量等。這樣根據秒殺商品的ID,就能獲取秒殺商品的具體標題和描述等。
(5)秒殺活動頁面數據的動靜分離
一.在對普通商品的詳情頁進行架構設計時
一般采用偽靜態化設計,也就是動態渲染,實際上并沒有靜態化。比如處理頁面請求時,先在Nginx層基于Lua從多級緩存里加載頁面數據,然后基于模板技術動態渲染成一個靜態頁面,最后再返回靜態頁面。如果采用純靜態化設計,即把每個詳情頁渲染成靜態頁面。那么每天都需要對全量幾億甚至幾十億商品的詳情頁進行一次靜態渲染,其中涉及的性能損耗、時間開銷、存儲空間,在成本上都是無法接受的。
二.在對秒殺商品的活動頁進行架構設計時
秒殺商品活動頁面是不太適合進行動態渲染的。因為每天需要參與秒殺活動的商品數會很少,對應的商品頁面數也很少,而這種秒殺商品頁面的訪問頻率卻非常高,很多用戶都會集中某個時間進行訪問。所以秒殺商品活動頁面最好采用純靜態化設計,進行靜態渲染,秒殺活動頁面的數據需要實現動靜分離。
圖片
三.秒殺活動商品詳情頁的靜態化處理
所謂動靜分離,就是秒殺商品活動頁已經通過HTML頁面渲染系統把內容填充好了,如商品標題、價格、描述等,大部分頁面信息都是靜態的,而頁面的個人信息、登錄狀態、個性化推薦等內容則是動態的。
圖片
(6)基于CDN來緩存秒殺活動靜態頁面
CDN是多臺服務器 + 智能DNS的結合體。CDN服務就是把靜態頁面緩存到不同地區的多臺緩存服務器上,然后根據用戶線路所在的地區,通過CDN服務商的智能DNS,自動選擇一個最近的緩存服務器讓用戶訪問,以此提高訪問速度。這種方案對靜態頁面的效果非常好。
由于秒殺商品活動頁面(靜態頁面)一般分為兩個部分:一部分是把數據都嵌入HTML以后的靜態頁面,一部分是HTML頁面引用的js、css和圖片,所以可以將這些靜態資源都推送到CDN里去,從而提高秒殺商品活動頁面的訪問速度。
(7)CDN靜態數據緩存的失效與命中
如果不小心修改了秒殺商品的頁面,那么就需要讓CDN緩存快速失效。因為修改頁面后直接推送頁面到CDN服務器上,可能需要時間比較長。所以最好還是修改完頁面后,直接通知CDN服務器讓緩存失效。之后再重新推送修改后的秒殺商品頁面過去,這個過程可通過RocketMQ進行異步處理。
不同的CDN分散在不同的地區,必然存在不同的用戶訪問量。所以有些CDN節點的命中率高,有些CDN節點的命中率低。讓CDN緩存快速失效時,可以優先選擇命中率高的節點。
注意:CDN服務器的數量不要太多,因為太多了也會影響通知各節點快速失效的效率,而且CDN節點盡量都選擇在距離系統大部分用戶比較近的地方。這樣萬一CDN節點要失效緩存,也可以讓失效的速度快一些,成本低一些。
(8)基于定時授時的前后端時鐘同步處理
用戶進入秒殺商品活動頁面后,就要等待秒殺活動開始。此時一般需要前端定時訪問后端的一個授時服務接口,進行時鐘同步。
圖片
(9)秒殺搶購接口地址的動態隱藏處理
不應該直接在前端頁面里提前暴露秒殺搶購的接口,以防惡意訪問。而應該把秒殺搶購的接口做成動態URL,直到秒殺開始前1分鐘,才讓秒殺前端頁面發送請求到后臺,去獲取動態的秒殺搶購接口URL,而且訪問該URL時要帶上一個隨機字符串的md5加密值才能允許訪問。
3.秒殺系統的負載均衡方案底層相關
(1)LVS(Linux Virtual Server)介紹
(2)異地多機房多活LVS集群部署介紹
(3)基于NAT技術實現的LVS請求轉發原理
(4)基于IP隧道模式的LVS請求與響應分離原理
(5)LVS的多種負載均衡算法
(6)LVS的Linux內核級實現原理
(7)基于七層網絡協議的負載均衡技術如何運作
(8)結合四層協議的LVS + 七層協議的Nginx來使用
(9)KeepAlived + LVS高可用及Nginx集群高可用
(10)同步異步以及阻塞非阻塞的區別
(11)抗下高并發的服務器架構模式
(12)Nginx的三大核心架構
(13)為什么Nginx之后還要接入一個網關
(14)使用獨立二級域名隔離秒殺系統與電商系統
(1)LVS(Linux Virtual Server)介紹
LVS集群的負載均衡器會接收服務的所有客戶端請求,然后根據調度算法決定哪個集群節點應該處理和響應請求,負載均衡器(簡稱LB)有時也被稱為LVS Director。
一.LVS虛擬服務器的體系結構
一組服務器通過高速的局域網或地理分布的廣域網相互連接,在它們的前端有一個負載均衡調度器(Load Balancer)。
二.LVS工作原理
負載均衡調度器能無縫地將網絡請求調度到真實服務器上,從而使得服務器集群的結構對客戶是透明的。用戶訪問LVS集群提供的服務就像訪問一臺高性能、高可用的服務器,服務程序不受LVS集群的影響,無需作任何修改。系統的伸縮性通過在服務集群中透明地加入和刪除一個節點來達到,通過檢測節點或服務進程故障和正確地重置系統達到高可用性。注意:LVS的負載均衡調度技術是在Linux內核中實現的。
圖片
(2)異地多機房多活LVS集群部署介紹
異地多機房多活的LVS集群部署:假設有一個國際化站點在不同的國家都部署了一個機房,這些機房都存儲了同樣的數據,互相間會進行數據交換和同步。然后該國際化站點會通過LVS集群部署來共享一個虛擬IP地址,這樣用戶在訪問站點時,就會解析這個共享的虛擬IP地址,把請求路由到用戶國內的機房。
(3)基于NAT模式實現的LVS請求轉發原理
一.NAT模式的原理簡介
二.NAT模式的工作流程
三.NAT模式的補充說明
一.NAT模式的原理簡介
Virtual Server via Network Address Translation (VS/NAT)。通過NAT網絡地址轉換,負載均衡調度器LB會重寫請求報文的目標地址。然后根據預設的調度算法,將請求分派給后端的真實服務器,真實服務器處理請求返回響應報文時必須要通過負載均衡調度器LB。經過負載均衡調度器LB時,響應報文的源地址也會被重寫。響應報文重寫完源地址后再返回給客戶端,從而完成整個負載調度過程。
NAT模式類似公路上的收費站,來去都要經過LB負載均衡器。通過修改目標地址端口或源地址端口,來完成請求和響應的轉發。
二.NAT模式的工作流程
客戶端通過虛擬IP地址訪問服務時,請求報文先到達負載均衡調度器LB。此時負載均衡調度器會根據調度算法從一組真實服務器中選出一臺服務器,然后將請求報文的目標地址(VIP)改寫成選定服務器的地址(RIP)。請求報文的目標端口改寫成選定服務器的相應端口(RS提供的服務端口),最后將修改后的報文發送給選出的服務器RS(Real Server)。
同時,負載均衡調度器LB會在一個Hash表中記錄這個連接。這樣當這個連接的下一個報文到達時,就可以從連接的Hash表中,獲取原來選定的服務器地址端口,并改寫地址將報文傳給該服務器(RS)。
當來自真實服務器RS的響應報文返回負載均衡調度器LB時,LB會將響應報文的源地址端口改為VIP和相應端口,然后再返回給客戶端。
三.NAT模式的補充說明
一般LVS服務器會對外提供一個VIP,就是虛擬服務器的IP地址。所有客戶端都會訪問這一個虛擬IP地址,也就是LVS服務器的地址。接著LVS服務器收到請求后,會基于NAT技術進行地址改寫。
比如瀏覽器要對某服務發送請求,此時需要將該請求發送給LVS服務器。首先會基于TCP/IP協議,和LVS服務器在進行TCP三次握手建立TCP連接。然后在TCP連接基礎上,將HTTP請求報文發送到LVS上。接著LVS再把這個HTTP請求報文轉發給某服務的Tomcat服務器。最后Tomcat獲取到一個完整的HTTP請求報文后,就可以處理請求了。
注意:LVS是工作在四層網絡協議上的負載均衡技術,轉發的是報文。Nginx是工作在七層網絡協議上的負載均衡技術,轉發的是請求。第四層的網絡協議是TCP/IP協議,第七層的網絡協議是HTTP協議。
圖片
(4)基于IP隧道模式的LVS請求與響應分離原理
一.IP隧道模式的原理簡介
二.IP隧道模式的工作流程
一.IP隧道模式的原理簡介
采用NAT技術時,由于請求和響應的報文都經過調度器LB進行地址重寫。當客戶請求越來越多時,負載均衡調度器LB的處理能力將成為瓶頸。為解決這個問題,LB會把請求報文通過IP隧道轉發至真實服務器,而真實服務器將響應處理后直接返回給客戶端用戶,這樣調度器就只需要處理請求的入站報文。
由于一般而言,服務的響應數據比請求報文大很多,所以采用IP隧道模式后,集群系統的最大吞吐量可以提高10倍。
二.IP隧道模式的工作流程
IP隧道模式的連接調度和管理與NAT模式一樣,只是報文轉發方法不同。LB會先根據各個服務器的負載情況、連接數多少,動態選擇一臺服務器。然后將原請求的報文封裝在另一個IP報文中,接著再將封裝后的IP報文轉發給選出的真實服務器。真實服務器收到報文后,先將其解封獲得原來目標地址為VIP地址的報文。真實服務器發現VIP地址被配置在本地的IP隧道設備上(此處要人為配置),所以就處理這個請求,然后根據路由表將響應報文直接返回給客戶端。
注意,根據TCP/IP協議棧處理:由于請求報文的目標地址為VIP,響應報文的源地址肯定也為VIP,所以響應報文不需要做任何修改,可以直接返回給客戶。客戶認為得到正常的服務,而不會知道究竟是哪一臺服務器處理的。
圖片
(5)LVS的多種負載均衡算法
一.輪詢調度算法(Round-Robin)
該算法會將請求依次分配不同的RS節點,也就是在RS節點中均攤請求。
這種算法簡單,但是只適合于RS節點處理性能相差不大的情況。
二.加權輪詢調度算法(Weighted Round-Robin)
該算法會依據不同RS節點的權值來分配請求。
權值較高的RS將優先獲得請求,相同權值的RS得到相同數目的請求數。
三.目的地址哈希調度算法(Destination Hashing)
以目的地址為關鍵字查找一個靜態Hash表來獲得需要的RS。
四.源地址哈希調度算法(Source Hashing)
以源地址為關鍵字查找一個靜態Hash表來獲得需要的RS。
五.加權最小連接數調度算法(Weighted Least-Connection)
假設各臺RS的權值依次為Wi(i=1…n),當前的TCP連接數依次為Ti(i=1…n),
依次選取Ti/Wi為最小的RS作為下一個分配的RS。
六.最小連接數調度算法(Least-Connection)
IPVS表存儲了所有的活躍的TCP連接,
把新的連接請求發送到當前連接數最小的RS。
七.基于地址的最小連接數調度算法
將來自同一目的地址的請求分配給同一臺RS節點。
八.基于地址帶重復最小連接數調度算法
對于某一目的地址,對應有一個RS子集。
對此地址請求,為它分配子集中連接數最小RS。
(6)LVS的Linux內核級實現原理
一.LOCAL_IN鏈和IP_FORWARD鏈的IPVS模塊處理報文改寫和轉發
LVS實際上是在Linux內核里修改了TCP/IP協議棧,這樣可以對收到的請求直接在Linux內核層面進行地址改寫和轉發。由于LVS運行在內核層面,這讓它的性能和吞吐量都極高。LVS有個IPVS模塊掛載在了內核的LOCAL_IN鏈和IP_FORWARD鏈。
當一個請求報文到達時,如果目標地址是VIP,就會轉交給LOCAL_IN鏈,然后請求報文就會被掛載在LOCAL_IN鏈上的IPVS模塊處理。IPVS模塊會根據負載均衡算法選擇一個RS,對報文進行改寫和轉發,接著會在Hash表中記錄這個連接和轉發的后端服務器地址。這樣報文再到達時,就可根據Hash表里的連接對應的服務器地址來轉發。
當一個響應報文返回時(NAT模式下),就會交給IP_FORWARD鏈,然后響應報文就會被掛載在IP_FORWARD鏈上的IPVS模塊處理。也就是改寫響應報文的地址,返回給客戶端。
由于在LVS中Hash表的一個連接數據只要128字節,所以一臺LVS服務器可以輕松調度幾百萬個連接。
二.Hash表的超時連接通過時間輪和分段鎖來移除
早期LVS會對Hash表里的連接設置一個定時器,連接超時就會回收該連接。但如果有幾百萬個連接,那么可能會導致一個很嚴重的問題。那就是有幾百萬個定時器,這么多定時器一起運行會導致很大的CPU負載。后來采用了Kafka的時間輪機制來改進。
所以如果要在內存里對數萬甚至數十萬的任務進行超時監控,最好不要每個任務都設置一個定時器,因為會對CPU和內存消耗極大。
Kafka的時間輪機制大概就是:不同的時間輪會有不同的時間周期,可以把不同的超時時間的連接放在不同的時間輪格子里,讓一個或者多個指針不停的旋轉,每隔一秒讓指針轉一下,這樣就可以讓不同的時間格里的連接超時失效。
其次,更新Hash表時使用分段鎖。也就是把Hash表拆成很多個小分段,不同的分段一把鎖。這樣可以降低鎖的粒度,減少高并發過程中的鎖的頻繁沖突,跟ConcurrentHashMap是一個原理。
(7)基于七層網絡協議的負載均衡技術如何運作
LVS是運行在四層網絡協議上的負載均衡技術,即對TCP報文進行轉發。對LVS來說,沒有HTTP這樣的概念,它只關注最底層的網絡報文。
如果要實現運行在七層網絡協議上的負載均衡技術,即對HTTP請求轉發,那么最大的問題在于:存在大量的內核空間和用戶空間的切換。
首先,需要經過多次報文交互后建立好一個TCP連接。然后,獲取通過TCP連接發送過來的完整HTTP協議請求。接著,從內核空間切換到用戶空間,將HTTP協議請求交給用戶空間運行的一個負載均衡技術去處理,也就是根據請求里的一些內容來將請求轉發給真實的后端服務器。最后,從用戶空間切換到內核空間,跟真實的后端服務器建立TCP連接,把HTTP協議請求發送過去。
之后,真實后端服務器返回響應時,又會從內核空間切換到用戶空間,把響應轉交給用戶空間的負載均衡技術來處理。處理完后,從用戶空間切換到內核空間,將響應通過內核發送給客戶端。
一般涉及到用戶空間的系統,單機每秒可抗幾千甚至幾萬請求,但是并發和吞吐遠遠低于不涉及用戶空間的系統。比如LVS單機每秒可抗幾萬到幾十萬甚至百萬的請求,因為LVS直接由Linux內核進行處理,無須切換內核空間到用戶空間。但運行在七層網絡協議上的負載均衡,可以根據HTTP請求進行路由轉發。
(8)結合四層協議的LVS + 七層協議的Nginx來使用
四層協議的LVS和七層協議的Nginx通常會結合在一起來使用。因為在七層協議上進行負載均衡的性能遠不如LVS,而僅僅在四層協議上進行負載均衡的LVS又不能進行一些高階的轉發,也就是沒有辦法根據HTTP請求的內容去進行一些高階的功能和轉發。而基于七層協議的Nginx則可根據HTTP請求的內容進行很多高階處理,比如可以在Nginx里嵌入lua腳本、在Nginx本地處理請求、讀取緩存等。
所以通常的做法是:在負載均衡最外側,部署一個基于四層協議的LVS作為核心的負載均衡的設備。通過對LVS進行大量的調優和優化,可以輕松做到單機百萬級的并發量。然后LVS會將請求轉發到基于七層協議的Nginx,讓Nginx根據HTTP請求的內容進行進一步轉發。
圖片
(9)KeepAlived + LVS高可用及Nginx集群高可用
Keepalived一開始就是專為LVS設計的,專門用來監控LVS集群系統中各個服務節點的狀態,但是后來又加入了VRRP的功能。因此Keepalived除了配合LVS服務外,也可作為其他服務的高可用軟件,比如Nginx、Haproxy、httpd、MySql。
Keepalive服務在LVS中的兩大用途:
一.healthcheck(健康檢查)
二.fallover(失敗接管)
(10)同步異步以及阻塞非阻塞的區別
同步和異步是指通信模式,常用來描述RPC網絡通信,比如同步RPC或異步RPC。同步就是調用方一直等待響應,異步就是調用方發出請求后直接返回,有結果返回時再回調調用方。
阻塞和非阻塞是指IO調用模式,常用來描述基于Socket的IO操作。比如阻塞IO和非阻塞IO,阻塞IO就是線程掛起來等待IO結果,非阻塞IO就是線程不會掛起來等待一個IO操作的結果。
一.同步阻塞
同步指的是客戶端發出請求后,一直同步等待響應。服務端收到請求后,要執行IO操作,如磁盤IO、網絡IO、數據庫IO等。此時服務端針對所有的IO操作,都會阻塞等待IO結果。無論是網絡IO調用其他服務,還是磁盤IO讀寫本地文件。
二.同步非阻塞
同步指的是客戶端發出請求后,一直同步等待響應。服務端收到請求后,發現IO操作沒法直接完成,直接去處理其他IO。但此時不返回響應給客戶端,等非阻塞IO完成了,再把結果返回給客戶端。
三.異步阻塞
客戶端發出請求后不等待響應,服務端收到請求后阻塞式IO,IO完成后再通知客戶端。一般很少使用異步阻塞模式。
四.異步非阻塞
客戶端發出請求后不等待響應,服務端收到請求后非阻塞IO,IO完成后再通知客戶端。IO不能馬上完成就去處理其他IO,等IO完成后再通知客戶端,Nginx就是使用了異步非阻塞模型。
(11)抗下高并發的服務器架構模式
一.多進程模式
有一個主進程,每當收到一個請求就交給一個子進程來進行處理。首會預生成一些子進程,然后處理完請求后不回收子進程,而是通過池化進行管理。Apache服務器就是這種多進程模式,但現在不太流行了,Nginx也是多進程模式。如果采用高配置的物理機,那么就可以開辟很多進程,大量的進程就可以高性能高并發地處理大量高并發請求。
二.單進程多線程模式
Tomcat是輕量級的Servlet容器服務器,采用了單進程多線程模式。Tomcat會基于NIO,使用有限的線程資源,抗下大量的高并發。
(12)Nginx的三大核心架構
異步非阻塞架構 + 多進程架構 + 模塊化架構
Nginx啟動后會有一個主進程和多個子進程,采用的是異步非阻塞模式。主進程負責建立、綁定和關閉Socket網絡連接,子進程負責處理客戶端請求。
一個子進程收到請求后,客戶端可以直接去處理其他事情(異步模式)。如果子進程發現IO操作不能馬上處理,那么就會去處理別的請求。這些IO操作比如是讀取本地磁盤的html或請求后端的Tomcat服務器,等IO操作執行完畢后,Linux內核會通知子進程,子進程再通知客戶端。
此外,Nginx還會把自己內部的大量功能做成了很多模塊,這種模塊化架構設計非常容易開發者進行自定義擴展。
(13)為什么Nginx之后還要接入一個網關
為了避免頻繁上下線微服務系統時,出現頻繁修改Nginx配置的情況。
(14)使用獨立二級域名隔離秒殺系統與電商系統
假設域名mall.demo.com指向了一個電商網站,用戶都是通過訪問該域名來進行瀏覽商品、生成訂單、支付訂單等操作。
如果用戶也通過該域名來訪問秒殺系統、進行秒殺搶購,甚至將秒殺系統和電商系統部署在一起,都在一批機器上,那么系統或機器的瞬時流量將可能超高,影響網站正常請求。
所以最好將電商系統和秒殺系統的二級域名部署到不同的LVS+Nginx。電商系統:mall.demo.com --> 秒殺系統:seckill.demo.com。
此外,秒殺商品的詳情頁所需的靜態資源文件最好也部署到CDN上去。尤其是商品詳情頁里可能包含大量的高清圖片、甚至視頻,盡量避免秒殺時的大流量撐滿LVS + Nginx服務器的網絡帶寬,影響并發。
4.秒殺系統的限流機制和超賣問題處理
(1)使用云廠商的DDoS高防產品防止黑客攻擊
(2)基于云廠商的驗證碼產品攔截黃牛和黑客請求
(3)開發一套秒殺系統的反作弊機制
(4)基于限流算法對秒殺系統進行整體限流
(5)如何基于Nginx + Lua實現一套業務限流機制
(6)秒殺搶購是否可以基于數據庫來實現
(7)秒殺商品庫存寫入Redis的Hash數據結構
(8)基于Redis緩存實現的秒殺搶購細節
(9)秒殺過程中庫存超賣類型問題的解決方案
方案一:使用分布式鎖(悲觀鎖)
方案二:請求放入FIFO內存隊列
方案三:使用樂觀鎖
方案四:封裝Lua腳本
方案五:庫存放入Redis隊列
(10)秒殺成功后的異步下單與支付
(11)秒殺系統是否還需要處理限流、防刷和防重
(1)如何使用云廠商的DDoS高防產品防止黑客攻擊
如果秒殺活動剛開始,黑客就對秒殺系統發起DDoS攻擊,那么就很容易將LVS -> Nginx -> Tomcat服務器資源消耗完,從而影響正常用戶的秒殺請求。
為了避免DDoS攻擊,可以使用云廠商的DDoS高防商業產品。首先將獨立的二級域名解析到DDoS高防產品去,然后再將DDoS高防產品的源站地址解析到負載均衡服務器上。這樣所有請求都會先經過DDoS高防產品,其中DDoS攻擊請求會被過濾掉,合法請求才會到負載均衡服務器上。
(2)基于云廠商的驗證碼產品攔截黃牛和黑客請求
驗證碼產品會基于大數據和AI驗證請求是否合法。如果請求合法,則判定驗證碼滑動通過,將請求發送到秒殺系統后端。
(3)開發一套秒殺系統的反作弊機制
比如一個用戶在一個搶購場次內,不允許對同一個商品重復搶購,只允許一個用戶對同一個商品發起指定次數的搶購。比如分析歷史請求日志,看看哪些IP和用戶喜歡每個秒殺場次都參加。反作弊機制,往往需要對用戶日常行為進行分析,來判斷請求是否合理。
(4)基于限流算法對秒殺系統進行整體限流
在Nginx環節,可以基于Lua腳本實現秒殺系統的整體性限流。即Nginx會允許多少請求可以訪問秒殺系統,而限流算法可以選擇令牌桶算法和漏桶算法。
(5)如何基于Nginx + Lua實現一套業務限流機制
一.整體限流機制
首先使用DDoS高防產品防DDoS攻擊,然后通過驗證碼攔截非法請求,接著開發作弊系統防止作弊刷單,最后先通過Nginx進行整體限流、再通過Redis進行業務限流。
二.業務限流機制
首先在Nginx層進行整體限流,然后再通過Redis進行業務限流。所謂業務限流,對于秒殺系統而言,就是限制每個商品的購買數量。所以一般來說,業務限流會基于Redis + Lua來實現。
三.限流機制應用舉例
假設當前有幾十萬的瞬時并發請求,其中的10萬是來自真實用戶的秒殺搶購請求、幾十萬是DDoS攻擊請求、幾千是黃牛的非法請求、幾百是某些用戶的作弊請求。
那么經過整體限流機制后:幾十萬的DDoS攻擊請求會被過濾掉、幾千的非法請求會被驗證碼攔截掉、幾百的用戶作弊請求會被作弊系統禁止掉,最后進入Nginx的請求有10萬。
那么經過Nginx層的業務限流機制后,這10萬請求又可能會過濾掉比如5萬,最終有5萬的請求會進行業務限流。如果商品限購數量為2萬,那么最終又會過濾掉3萬請求,而放行其中的2萬請求。這放行的2萬請求可搶購到商品,8萬請求被整體限流和業務限流過濾了。對于基于Tomcat部署的秒殺系統而言,最后收到的最多就是大約2萬請求。
(6)秒殺搶購是否可以基于數據庫來實現
利用MySQL數據庫的行鎖來實現搶購效果的隱患:比如update stock set 庫存 = 庫存 - 1。雖然數據庫的行鎖可保證多線程并發更新一行數據時,是串行執行。但如果秒殺的庫存數量有1萬,然后并發涌入1萬請求,則容易擊垮數據庫。
一般都會使用Redis或其他NoSQL來存放秒殺商品的庫存數量。在秒殺開始前,把秒殺商品的庫存提前加載到Redis緩存里。進行秒殺時,就扣減Redis里的庫存,直到Redis里的庫存扣減完畢。并在監控平臺實時展示秒殺商品的:可銷售庫存、鎖定庫存、已銷售庫存。
(7)秒殺商品庫存寫入Redis的Hash數據結構
可以使用Redis的Hash結構來存儲秒殺商品的庫存信息,在Hash結構中存儲可銷售庫存、鎖定庫存、已銷售庫存,其中會使用Redis的hincrby命令和hdecrby命令來增加庫存和扣減庫存。
seckill::product::123 = {
sale_stock_amount: 100,//可銷售庫存
locked_stock_amount: 0,//鎖定庫存
saled_stock_amount: 0//已銷售庫存
}
(8)基于Redis緩存實現的秒殺搶購細節
秒殺開始前,需要提前將商品庫存寫入到Hash中的可銷售庫存里。秒殺開始時,會讀取Hash中的可銷售庫存,判斷是否可以購買。如果可以購買就扣減可銷售庫存,并增加鎖定庫存。之后執行異步下單流程:即等用戶支付完訂單后,扣減鎖定庫存,并增加已銷售庫存。
(9)秒殺過程中庫存超賣問題的解決方案
秒殺搶購過程中可能出現的庫存超賣問題的解決方案如下:(其實這些方案也可以用來解決分布式環境下更新數據的順序性問題)
方案一:使用分布式鎖(悲觀鎖)
方案二:請求放入FIFO內存隊列
方案三:使用樂觀鎖
方案四:封裝Lua腳本
方案五:庫存放入Redis隊列
方案一:使用分布式鎖(悲觀鎖)
比如首先通過Redisson框架去Redis中獲取一個名為"seckill::product::123::lock"的鎖,在某個線程獲得鎖的期間不會有其他線程去查詢和更新庫存數據。此時該線程可以放心地查詢和扣減庫存,之后再釋放鎖。
但這種加分布式鎖的方案并不適合高并發場景。Redisson分布式鎖的實現是通過Lua腳本 + Watch Dog來實現鎖等待的,每個獲取不到鎖的線程都會不停輪詢然后嘗試加鎖,中間都有一個等待的過程。在等待的過程中,這些線程就可能會產生很多網絡通信開銷,影響性能。
獲取鎖的線程釋放鎖之后,其他線程還都處于鎖等待的過程中。此時過了幾十ms才會有下一個線程獲取鎖,所以分布式鎖的整體并發能力不是太好。
方案二:請求放入FIFO內存隊列
比如秒殺搶購系統將所有進來的搶購請求全部放入同一個內存隊列中排隊,按照先進先出的順序從內存隊列中出隊,然后再去Redis里執行扣減邏輯。
這種方案需要注意:把對某個商品的搶購請求都路由到一臺機器上,進入同一個內存隊列,從而保證對這個商品的搶購都是有序的。
內存隊列的方案,可以嚴格保證搶購請求的處理順序。但是萬一系統重啟或宕機,就會導致內存隊列里的數據丟失。以及如果搶購請求特別多,可能會導致內存隊列占用大量內存頻繁GC。
方案三:使用樂觀鎖
樂觀鎖方案要求每個庫存數據都綁定一個版本號,每次更新庫存時先用樂觀鎖判斷,庫存是否發生過改變,如果沒發生過改變才可以更新。
可以使用Redis的Watch機制 + pipeline來實現這種樂觀鎖。首先Watch一些數據是否有變化,然后通過pipeline一次性提交多個操作。如果數據有變化,那么pipeline就會提交失敗。如果數據沒變化,那么pipeline就會提交成功。提交的pipeline的最后一個操作,需要獲取庫存的最新值。如果發現庫存是負數,那么就表示秒殺失敗。注意:pipeline雖然可以保證多個操作命令順序執行,但卻不是原子性的,因為不能保證事務(同時成功和同時失敗)。
樂觀鎖方案可能會很耗費CPU。比如某線程更新庫存時,因為庫存版本已被其他線程頻繁修改而一直失敗。此時該線程需要重新查詢最新的版本,重新發起更新。所以樂觀鎖方案可能會導致很多秒殺搶購請求都處于輪詢狀態。
方案四:封裝Lua腳本(建議的方案)
一個秒殺搶購請求對應一段Lua腳本,直接將Lua腳本提交到Redis中執行。Redis可以保證Lua腳本按順序執行,且每個Lua腳本的執行都是原子的。從而避免了超賣問題,也大大降低了對性能影響和受系統風險的影響。
具體來說就是:把查詢庫存數量是否可以搶購 + 更新庫存字段 + 判斷是否搶購成功等,這些秒殺搶購過程中涉及到的核心邏輯都封裝在一個Lua腳本里,然后提交這個Lua腳本到Redis中去執行。這樣即便每秒上萬請求,也就是提交上萬個Lua腳本到Redis內存里執行。
此外,這種Lua腳本方案還可以支持進行庫存分片優化。因為畢竟Redis是單線程的,通過實現Redis庫存分片可以提升并發性能。假設Redis集群部署了三個Master,那么每個商品庫存可分成三個分片,從而實現對一臺Redis機器的壓力均勻分散到三臺Redis機器上。
方案五:庫存放入Redis隊列
提前把商品庫存寫入到Redis的一個隊列中,搶購時再從隊列中彈出庫存。比如秒殺商品庫存為10,那么就把10個庫存數據寫入Redis的一個隊列。用戶發起秒殺搶購請求時,再不停地從隊列里出隊。如果從隊列中獲取不到數據了,則說明搶購結束了。
但是這種方案,不適合庫存數量特別多的情況,否則可能會出現占用大量內存的問題,比如庫存數量有10000等。
(10)秒殺成功后的異步下單與支付
秒殺成功后,就會立刻返回響應給客戶端,并進行異步下單和支付。此時客戶端的秒殺頁面會顯示:“秒殺搶購進行中,請耐心等待”。之后客戶端會每隔1s發送一個請求到后臺查詢,秒殺訂單是否已生成。
(11)秒殺系統是否還需要處理限流、防刷和防重
其實不需要了,秒殺系統只需要關注秒殺搶購的邏輯即可,因為限流、防刷、防重已在LVS + Nginx + Lua中處理了。
5.秒殺系統的異步下單和高可用方案
(1)使用MQ來進行秒殺下單削峰
(2)秒殺異步下單的流量控制處理
(3)下單成功后的訂單支付邏輯與未支付邏輯
(4)MQ消息零丟失的處理
(5)MQ消息重復的處理
(6)MQ消息積壓的處理
(7)秒殺系統的全鏈路高可用架構設計
(8)Redis集群崩潰時秒殺系統的自動恢復處理
(9)Redis主節點崩潰時沒及時同步導致超賣問題
(10)MQ集群崩潰時寫內存刷磁盤的處理
(11)訂單系統異常時的MQ重試隊列與死信隊列
(12)秒殺搶購成功時的異構存儲
(1)使用MQ來進行秒殺下單削峰
MQ的三大作用是:削峰、異步提升性能、解耦。由于秒殺時的搶購請求可能會遠遠超出訂單系統的日常下單請求,比如日常的下單請求數高峰是每秒幾百QPS,而秒殺時的下單請求數則是每秒幾千。所以面對這樣的請求高峰,為了保護訂單系統,最好使用MQ進行下單削峰。
(2)秒殺異步下單的流量控制處理
秒殺的異步下單可以使用線程池進行流控去請求訂單系統。比如部署2臺服務器,每臺機器開啟幾十個線程消費MQ的搶購成功消息。
(3)下單成功后的訂單支付邏輯與未支付邏輯
用戶搶購秒殺商品成功后,會在客戶端界面里進行等待。此時客戶端可能會不斷輪詢訂單列表,或者用戶也可能會刷新訂單列表。當客戶端發現訂單已創建好了,那么用戶就可以對訂單進行支付。
當訂單系統成功創建一個訂單時,會發送一條延遲消費的消息到MQ里,比如發送一條延遲30分鐘才消費的消息到MQ中。這樣當訂單系統在30分鐘后消費這個消息時,會檢查訂單支付狀態。如果發現訂單還沒支付就會直接關閉訂單,并且釋放鎖定的庫存。
使用MQ這類消息中間件時,需要注意三個問題:消息丟失問題、消息重復問題、消息積壓問題。
(4)MQ消息零丟失的處理
一.發送消息到MQ的零丟失
方案一:同步發送消息 + 反復多次重試
方案二:事務消息機制
兩者都有保證消息發送零丟失的效果,但是經過分析,事務消息方案整體會更好一些。
二.MQ收到消息之后的零丟失
開啟同步刷盤策略 + 主從架構同步機制
只要讓一個Broker收到消息后同步寫入磁盤,同時同步復制給其他Broker,然后再返回響應給生產者表示寫入成功,此時就可保證MQ不會丟消息。
三.消費消息的零丟失
RocketMQ的消費者天然就可以保證:處理完消息后,才會提交消息的offset到Broker去,所以只要注意避免采用多線程異步處理消息的方式即可。
(5)MQ消息重復的處理
一般來說,往MQ里重復發送同樣的消息是可以接受的。因為即使MQ里有多條重復消息,也不會直接影響系統的核心數據。所以關鍵要保證:消費者從MQ里獲取消息進行處理時,消息不能重復。要保證消息消費的冪等性,優先推薦的是業務判斷法,而非狀態判斷法。
一.業務判斷法
直接根據數據庫存儲中的記錄判斷消息是否處理過,如果已經處理過了,那么就別再次處理了。
二.狀態判斷法
基于Redis的消息發送狀態來判斷,在極端情況下沒法完全保證冪等性。
(6)MQ消息積壓的處理
方案一:讓訂單直接在Redis中創建
為了避免MQ消息積壓,導致出現訂單遲遲沒創建成功的問題,可能會想到先讓訂單在Redis中創建這么一種解決方案。雖然該方案可以讓用戶馬上基于Redis來查詢訂單,并進行支付。但是整個訂單的管理鏈路需要與Redis耦合在一起了,改造起來并不優雅。比如可能需要考慮:訂單列表的分頁展示要基于MySQL + Redis來實現。
方案二:釋放鎖定庫存的降級處理
消費MQ消息時,先檢查消息的消費時間和寫入時間之差是否已超某閾值。比如消費消息的時間減去消息寫入MQ的時間已超過2分鐘,而一般情況下消息從寫入MQ開始到完成消費只需20s左右,那么此時就可認為MQ出現了比較嚴重的消息積壓問題,就可以啟動針對消息積壓而釋放鎖定庫存的降級方案。
具體而言就是:對于消費消息的服務端,調用Redis的接口釋放鎖定的庫存。對于用戶前端,可設定超2分鐘還沒獲取訂單創建成功的通知則跳轉提示。提示用戶:秒殺搶購失敗,需要重新進行秒殺下單。
由于釋放秒殺庫存是非常快的,所以積壓的MQ消息可以非常快處理完。當然釋放庫存的過程中,可以消費積壓的MQ消息繼續正常創建訂單。其實這個方案采取的思路就是丟棄MQ消息的思路。
(7)秒殺系統的全鏈路高可用架構設計
LVS高可用:LVS雙機器部署+ Keepalived
Nginx高可用:多機器冗余部署 +LVS會做負載均衡
秒殺搶購服務:多機器冗余部署+ Nginx會做負載均衡
Redis Cluster:Master-Slave主從架構+ 自動故障切換
RocketMQ:集群部署+ 多副本冗余
秒殺下單服務:多機器冗余部署
秒殺庫存服務:多機器冗余部署
(8)Redis集群崩潰時秒殺系統的自動恢復處理
按照上述的高可用架構方案,其實任何一個環節里的任何一臺機器宕機,都不會影響系統正常的運行,但就是怕有的環節直接全面崩潰。
比如整個Redis集群都崩潰了,那么就沒法進行秒殺搶購了。如果此時向所有請求都返回搶購失敗,那么用戶體驗也不好,因為這樣可能會導致這次秒殺活動徹底失敗。
此時比較好的方案是:首先將用戶秒殺搶購的日志順序寫入本地磁盤,進入OS Cache。然后返回秒殺狀態通知用戶,目前商品正在搶購,讓用戶耐心等待。用戶收到通知后,可能會停留在頁面等待,可能會進入秒殺商品詳情頁。其中秒殺狀態可以設計為如下:搶購失敗、搶購進行中、搶購成功、完成創建訂單、完成支付訂單等。
由于大部分流量在LVS + Nginx層攔截了,只有少部分流量進入秒殺系統,所以寫入到本地磁盤的用戶秒殺搶購日志,也不會太多。
當緊急修復或重啟Redis集群后,就可以讓秒殺系統的后臺線程自動探查Redis是否恢復。如果恢復了,那么就順序讀取本地磁盤的秒殺流水日志,通過流量重放重新去Redis執行搶購流程即可。
(9)Redis主節點崩潰時沒及時同步導致超賣問題
如果基于Redis來做秒殺搶購,那么一般會使用Redis Cluster。而Redis Cluster又是主從同步架構 + 異步復制的,如果Master節點宕機,沒來得及把庫存扣減操作同步到Slave節點。當Slave節點切換成Master節點后,就可能會導致庫存超賣問題。
這個Redis異步復制導致的庫存超賣問題比較難解決。根據CAP理論,保證了AP,就必然不能保證C,所以會出現超賣。為了保證不出現超賣,就要舍棄A,保證CP。除非改造Redis源碼,把異步復制做成同步復制。任何一個秒殺搶購請求在Master執行完畢后,庫存數據必須從Master節點復制到Slave節點后,才能認為數據操作成功。但如果改造成同步復制,同步復制又會大大降低Redis的高性能。
方案一:取消Redis主從同步
可以用Codis對幾臺機器上的Redis做分布式數據存儲和路由,取消Slave。如果秒殺系統的服務器發現其連接的Redis宕機,就將該秒殺系統負責的用戶搶購操作轉化為流水日志寫入本地磁盤文件。等到連接的Redis機器恢復后,再重放流水日志去執行搶購流程。這種方案不僅可以解決庫存超賣問題,還能實現Redis崩潰時的高可用。
注意:雖然Codis取消了Slave,但為了讓Redis的數據不丟失,也可以在Codis機器上緩存寫命令。
方案二:訂單系統在創建秒殺訂單時進行檢查
當訂單系統在消費搶購成功的MQ消息,準備創建秒殺訂單時,檢查可銷售庫存、鎖定庫存、已銷售庫存,是否出現異常。如果出現異常則不允許創建訂單,然后通知用戶創建秒殺訂單失敗。
方案三:自研中間件
借鑒Redis的高性能機制、RocketMQ的高可用機制,開發一個類似的中間件,專門用來存儲和扣減秒殺商品的庫存。
秒殺過程中可能出現庫存超賣問題的解決方案:
一.使用分布式鎖(悲觀鎖)
二.請求放入FIFO內存隊列
三.使用樂觀鎖
四.封裝Lua腳本
五.庫存放入Redis隊列
(10)MQ集群崩潰時寫內存刷磁盤的處理
MQ集群崩潰,只不過是無法創建秒殺訂單而已,搶購還是能正常執行的。所以當MQ集群崩潰時,完全沒必要讓秒殺系統等待MQ恢復。此時依然可以直接返回是否搶購成功給用戶,并將搶購成功消息寫入內存。然后再將內存中的搶購成功消息刷入本地磁盤文件。接著后臺開啟一個線程,慢慢讀取磁盤數據并直接調用創建訂單接口。從而繞過了從MQ消費搶購成功的消息,以較低的速度創建搶購訂單。
(11)訂單系統異常時的MQ重試隊列與死信隊列
如果訂單系統出現異常,對MQ里的搶購成功消息沒能消費成功,此時可以基于MQ的重試隊列進行重試。如果重試多次都不行,則需要進入MQ的死信隊列。MQ會有專門處理死信的線程,比如每隔1小時再進行反復重試。
如果訂單系統出現異常,其實也就意味著MQ消息會出現積壓,此時的解決方案還可以是:若發現MQ消息積壓超過某個時間,則發送另一條快速失敗的消息到MQ,讓非訂單系統消費該快速失敗的消息,直接修改Redis去釋放庫存。
(12)秒殺搶購成功時的異構存儲
用戶搶購秒殺商品成功后,要修改Redis的庫存,也要發送消息到MQ,這時就需要解決異構存儲下的一致性問題了。否則可能出現Redis的庫存修改成功,但是消息沒有發送到MQ。
由于Redis是支持事務機制的,所以用戶秒殺搶購成功時,可以開啟一個Redis事務。在Redis事務里,先更新Redis的數據,然后發送消息到MQ。如果發送消息到MQ成功了,那么才提交Redis事務,最好返回搶購成功的信息給用戶。
如果發送消息給MQ失敗了,此時可以執行降級方案,也就是寫MQ消息到內存并刷入本地磁盤。只要降級方案執行成功,那么這個Redis事務也算成功。
如果開啟Redis事務成功,發送消息到MQ成功,但提交Redis事務失敗。那么此時可使用MQ的事務機制,通過Redis事務 + MQ事務保證一致性。