1.主要做到以下兩點:
盡量將請求過濾在上游。
盡可能的利用緩存(大多數場景下都是查多於寫)。
如果流量巨大,導致各個層的壓力都很大可以適當的加機器橫向擴容。如果加不了機器那就只有放棄流量直接返回失敗。快速失敗非常重要,至少可以保證系統的可用性。
業務分批執行:對於下單、付款等操作可以異步執行提高吞吐率。
主要目的就是盡量少的請求直接訪問到 DB。
2. 架構圖
前端請求進入 web 層,對應的代碼就是 controller。
之后將真正的庫存校驗、下單等請求發往 Service 層(其中 RPC 調用依然采用的 dubbo,只是更新為最新版本,本次不會過多討論 dubbo 相關的細節,有興趣的可以查看 基於dubbo的分布式架構)。
Service 層再對數據進行落地,下單完成
其實拋開秒殺這個場景來說正常的一個下單流程可以簡單分為以下幾步:
校驗庫存
扣庫存
創建訂單
支付
3.常見問題
3.1 超賣現象(使用樂觀鎖更新)
3.2 提高吞吐量
為了進一步提高秒殺時的吞吐量以及響應效率,這里的 web 和 Service 都進行了橫向擴展。
web 利用 Nginx 進行負載。
Service 也是多台應用
當並發量達到幾百萬時(分布式限流)
我們將並發控制在一個可控的范圍之內,然后快速失敗這樣就能最大程度的保護系統。
3.3 sql查詢太多(redis緩存)
這種數據我們完全可以放在內存中,效率比在數據庫要高很多。
由於我們的應用是分布式的,所以堆內緩存顯然不合適,Redis 就非常適合。
這次主要改造的是 Service 層:
每次查詢庫存時走 Redis。
扣庫存時更新 Redis。
需要提前將庫存信息寫入 Redis(手動或者程序自動都可以)。
3.4請求同步轉異步(kafka)
這里我們將寫訂單以及更新庫存的操作進行異步化,利用 Kafka 來進行解耦和隊列的作用。
每當一個請求通過了限流到達了 Service 層通過了庫存校驗之后就將訂單信息發給 Kafka ,這樣一個請求就可以直接返回了。
消費程序再對數據進行入庫落地。
因為異步了,所以最終需要采取回調或者是其他提醒的方式提醒用戶購買完成。
4. 總結
其實經過上面的一頓優化總結起來無非就是以下幾點:
盡量將請求攔截在上游。
還可以根據 UID 進行限流。
最大程度的減少請求落到 DB。
多利用緩存。
同步操作異步化。
fail fast,盡早失敗,保護應用。
5、悲觀鎖
簡單理解下悲觀鎖:當一個事務鎖定了一些數據之后,只有當當前鎖提交了事務,釋放了鎖,其他事務才能獲得鎖並執行操作。
這里使用select for update的方式利用數據庫開啟了悲觀鎖,鎖定了id=1的這條數據(注意:這里除非是使用了索引會啟用行級鎖,不然是會使用表鎖,將整張表都鎖住。)。之后使用commit提交事務並釋放鎖,這樣下一個線程過來拿到的就是正確的數據。
悲觀鎖一般是用於並發不是很高,並且不允許臟讀等情況。但是對數據庫資源消耗較大。
6.樂觀鎖
那么有沒有性能好,支持的並發也更多的方式呢?
那就是樂觀鎖。
樂觀鎖是首先假設數據沖突很少,只有在數據提交修改的時候才進行校驗,如果沖突了則不會進行更新。
通常的實現方式增加一個version字段,為每一條數據加上版本。每次更新的時候version+1,並且更新時候帶上版本號
實踐:基於分布式微服務的秒殺搶購功能的實現
借下圖
秒殺設計到的微服務
注冊中心(Eurake) : @EnableEurekaServer開啟注冊中心,實現對各種微服務的集中管理
網關徽服務(zuul) : @EnableDiscoveryClient將服 務注冊到到注冊中心,@EnablezuulProxy開啟 網關服務,對微服務路口做統一管理, 實現路由,降級(容錯回退),限流的功能。如果多台服務器,可以通過路徑和服務的綁定path: /user-service/* ; serviceld: user-service2,實現負載均衡(默認是Ribbon輪詢,還有隨機)
用戶中心微服務(user-service) :@EnableDiscoveryClient將 用戶中心微服務注冊到到注冊中心,實現注冊和登錄功能
授權中心微服務(auth-service) : @EnableDiscoveryClient將用戶中心微服務注冊到到注冊中心實現對登錄的鑒權。
商品微服務(item-service) : @EnableDiscoveryClient將商品微服務注冊到到注冊中心,做商品的添加和查詢。
具體秒殺流程邏輯
網關對部分不需要登錄認證的接口放行(要優化)1.注冊用戶,網關對注冊放行
登錄接口到網關,被路由到授權中心,授權中心微服務調用用戶中心的登錄接口進行校驗,校驗成功,利用JWT生成token,然后利用RSA非對稱加密token,生成公鑰和私鑰保存,然后將token返回到客戶端
秒殺業務
- 在商品微服務中設置秒殺參數,根據參數的商品Id查詢商品,構建商品秒殺表,添加,然后更新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));))
使用秒殺功能需要登錄驗證,創建登錄攔截(LoginInterceptor extends HandlerinterceptorAdapter)對token進行驗證,認證通過將用戶信息存放到線程域中,並且走一個限流攔截AccessInterceptor extends HandlerinterceptorAdapter)實現限流功能
構建秒殺路徑(限流),加密,保存到redis緩存,隱藏秒殺路徑,防止刷單。
4. 秒殺
4.1. 驗證秒殺路徑
4.2. 讀取庫存, 減1后更新緩存
4.3. 庫存不足直接返回“排隊中”
4.4. 庫存充足, 將商品信息封裝入隊MQ,然后直接返回“排隊中”
- 然后訂單微服務監聽隊列,消費隊列,
5.1判斷庫存不足,將該商品設置成不可秒殺狀態,
5.2查看是否秒殺到,秒殺到直接返回,
5.3沒有秒殺到,創建訂單
