1.主要做到以下兩點:
盡量將請求過濾在上游。
盡可能的利用緩存(大多數(shù)場景下都是查多于寫)。
如果流量巨大,導(dǎo)致各個層的壓力都很大可以適當?shù)募訖C器橫向擴容。如果加不了機器那就只有放棄流量直接返回失敗抒巢。快速失敗非常重要秉犹,至少可以保證系統(tǒng)的可用性蛉谜。
業(yè)務(wù)分批執(zhí)行:對于下單稚晚、付款等操作可以異步執(zhí)行提高吞吐率。
主要目的就是盡量少的請求直接訪問到 DB型诚。
2. 架構(gòu)圖
前端請求進入 web 層客燕,對應(yīng)的代碼就是 controller。
之后將真正的庫存校驗狰贯、下單等請求發(fā)往 Service 層(其中 RPC 調(diào)用依然采用的 dubbo也搓,只是更新為最新版本,本次不會過多討論 dubbo 相關(guān)的細節(jié)涵紊,有興趣的可以查看 基于dubbo的分布式架構(gòu))傍妒。
Service 層再對數(shù)據(jù)進行落地,下單完成
其實拋開秒殺這個場景來說正常的一個下單流程可以簡單分為以下幾步:
校驗庫存
扣庫存
創(chuàng)建訂單
支付
3.常見問題
3.1 超賣現(xiàn)象(使用樂觀鎖更新)
3.2 提高吞吐量
為了進一步提高秒殺時的吞吐量以及響應(yīng)效率摸柄,這里的 web 和 Service 都進行了橫向擴展颤练。
web 利用 Nginx 進行負載。
Service 也是多臺應(yīng)用
3.2當并發(fā)量達到幾百萬時(分布式限流)
我們將并發(fā)控制在一個可控的范圍之內(nèi)塘幅,然后快速失敗這樣就能最大程度的保護系統(tǒng)昔案。
3.3 sql查詢太多(redis緩存)
這種數(shù)據(jù)我們完全可以放在內(nèi)存中,效率比在數(shù)據(jù)庫要高很多电媳。
由于我們的應(yīng)用是分布式的,所以堆內(nèi)緩存顯然不合適庆亡,Redis 就非常適合匾乓。
這次主要改造的是 Service 層:
每次查詢庫存時走 Redis。
扣庫存時更新 Redis又谋。
需要提前將庫存信息寫入 Redis(手動或者程序自動都可以)拼缝。
3.4請求同步轉(zhuǎn)異步(kafka)
這里我們將寫訂單以及更新庫存的操作進行異步化,利用 Kafka 來進行解耦和隊列的作用彰亥。
每當一個請求通過了限流到達了 Service 層通過了庫存校驗之后就將訂單信息發(fā)給 Kafka 咧七,這樣一個請求就可以直接返回了。
消費程序再對數(shù)據(jù)進行入庫落地任斋。
因為異步了继阻,所以最終需要采取回調(diào)或者是其他提醒的方式提醒用戶購買完成。
4. 總結(jié)
其實經(jīng)過上面的一頓優(yōu)化總結(jié)起來無非就是以下幾點:
盡量將請求攔截在上游废酷。
還可以根據(jù) UID 進行限流瘟檩。
最大程度的減少請求落到 DB。
多利用緩存澈蟆。
同步操作異步化墨辛。
fail fast,盡早失敗趴俘,保護應(yīng)用睹簇。
5擎椰、悲觀鎖
簡單理解下悲觀鎖:當一個事務(wù)鎖定了一些數(shù)據(jù)之后,只有當當前鎖提交了事務(wù)敬飒,釋放了鎖刑枝,其他事務(wù)才能獲得鎖并執(zhí)行操作。
這里使用select for update的方式利用數(shù)據(jù)庫開啟了悲觀鎖垛叨,鎖定了id=1的這條數(shù)據(jù)(注意:這里除非是使用了索引會啟用行級鎖伦糯,不然是會使用表鎖,將整張表都鎖住嗽元。)敛纲。之后使用commit提交事務(wù)并釋放鎖,這樣下一個線程過來拿到的就是正確的數(shù)據(jù)剂癌。
悲觀鎖一般是用于并發(fā)不是很高淤翔,并且不允許臟讀等情況。但是對數(shù)據(jù)庫資源消耗較大佩谷。
6.樂觀鎖
那么有沒有性能好旁壮,支持的并發(fā)也更多的方式呢?
那就是樂觀鎖谐檀。
樂觀鎖是首先假設(shè)數(shù)據(jù)沖突很少抡谐,只有在數(shù)據(jù)提交修改的時候才進行校驗,如果沖突了則不會進行更新桐猬。
通常的實現(xiàn)方式增加一個version字段麦撵,為每一條數(shù)據(jù)加上版本。每次更新的時候version+1溃肪,并且更新時候帶上版本號
實踐:基于分布式微服務(wù)的秒殺搶購功能的實現(xiàn)
借下圖
樂優(yōu).png
秒殺設(shè)計到的微服務(wù)
注冊中心(Eurake) : @EnableEurekaServer開啟注冊中心免胃,實現(xiàn)對各種微服務(wù)的集中管理
網(wǎng)關(guān)徽服務(wù)(zuul) : @EnableDiscoveryClient將服 務(wù)注冊到到注冊中心,@EnablezuulProxy開啟 網(wǎng)關(guān)服務(wù)惫撰,對微服務(wù)路口做統(tǒng)一管理羔沙, 實現(xiàn)路由,降級(容錯回退)厨钻,限流的功能扼雏。如果多臺服務(wù)器,可以通過路徑和服務(wù)的綁定path: /user-service/* ; serviceld: user-service2,實現(xiàn)負載均衡(默認是Ribbon輪詢莉撇,還有隨機)
用戶中心微服務(wù)(user-service) :@EnableDiscoveryClient將 用戶中心微服務(wù)注冊到到注冊中心呢蛤,實現(xiàn)注冊和登錄功能
授權(quán)中心微服務(wù)(auth-service) : @EnableDiscoveryClient將用戶中心微服務(wù)注冊到到注冊中心實現(xiàn)對登錄的鑒權(quán)。
商品微服務(wù)(item-service) : @EnableDiscoveryClient將商品微服務(wù)注冊到到注冊中心棍郎,做商品的添加和查詢其障。
具體秒殺流程邏輯
網(wǎng)關(guān)對部分不需要登錄認證的接口放行(要優(yōu)化)1.注冊用戶,網(wǎng)關(guān)對注冊放行
登錄接口到網(wǎng)關(guān)涂佃,被路由到授權(quán)中心励翼,授權(quán)中心微服務(wù)調(diào)用用戶中心的登錄接口進行校驗蜈敢,校驗成功,利用JWT生成token,然后利用RSA非對稱加密token,生成公鑰和私鑰保存汽抚,然后將token返回到客戶端
秒殺業(yè)務(wù)
- 在商品微服務(wù)中設(shè)置秒殺參數(shù)抓狭,根據(jù)參數(shù)的商品Id查詢商品,構(gòu)建商品秒殺表造烁,添加否过,然后更新redis緩存
BoundHashOperations<String, Object, Object> hashOperations = this.stringRedisTemplate.boundHashOps(KEY PREFIX);
1/判斷是否存在此K值
if (hashOperations.hasKey(KEY PREFI){
hashOperations.delete(KEY_ PREFIX);
seckiloods.forEach(goods > hashOperatiosput(goos.getkud(.totring(), goods.getstock).totrin));))
使用秒殺功能需要登錄驗證,創(chuàng)建登錄攔截(LoginInterceptor extends HandlerinterceptorAdapter)對token進行驗證惭蟋,認證通過將用戶信息存放到線程域中苗桂,并且走一個限流攔截AccessInterceptor extends HandlerinterceptorAdapter)實現(xiàn)限流功能
構(gòu)建秒殺路徑(限流),加密告组,保存到redis緩存,隱藏秒殺路徑煤伟,防止刷單。
4. 秒殺
4.1. 驗證秒殺路徑
4.2. 讀取庫存木缝, 減1后更新緩存
4.3. 庫存不足直接返回“排隊中”
4.4. 庫存充足便锨, 將商品信息封裝入隊MQ,然后直接返回“排隊中”
- 然后訂單微服務(wù)監(jiān)聽隊列,消費隊列我碟,
5.1判斷庫存不足放案,將該商品設(shè)置成不可秒殺狀態(tài),
5.2查看是否秒殺到怎囚,秒殺到直接返回卿叽,
5.3沒有秒殺到,創(chuàng)建訂單