Java電商秒殺系統性能優化(六)——交易性能優化技術之緩存庫存

概述

本篇博客介紹了下單交易的性能優化技術,通過交易驗證緩存的優化,庫存緩存模型優化解決了交易流程中繁瑣耗性能的驗證緩存,並解決數據庫庫存行鎖的問題,同時也引入了緩存與數據庫分佈式提交過程中不一致的風險。

本章的學習目標是:

  • 掌握高效交易驗證方式
  • 掌握緩存庫存模型

一、高效交易驗證


1.1 交易性能瓶頸

  • 採用jmeter壓測進行性能壓測,請求改爲POST請求,加入消息體中,average 500ms tps200/s cpu% 75%;
    交易驗證完全依賴數據庫,通過SQL語句的方式發送給數據庫,完成讀操作;
  • 庫存行鎖
  • 後置處理邏輯
    在這裏插入圖片描述

1.2 交易驗證優化

  • 獲取用戶信息,用戶風控策略優化:策略緩存模型化,將對應的風控內容做到redis緩存裏面,例如是否異地登錄賬號異常,將風控的策略通過異步的方式寫入對應緩存中,在實時查詢過程中做一個風控策略的實時攔截;
  • 活動校驗策略優化:引入活動發佈流程,模型緩存化,緊急下線能力;
    運營發現活動有異常,在後臺將對應的活動進行修改,比如將活動提前結束。若線上在redis的緩存沒有正常過期,即便修改了活動時間,但是用戶還是可以以活動秒殺價格交易,因此需要一個緊急下線能力。所以運營人員至少要在活動開始前半個小時將活動發佈上去,半個小時內足夠進行緩存的預熱。在後設計一個緊急下線的接口,用代碼實現可以清除redis內的緩存。當redis內無法查詢狀態,就會去數據庫內查詢活動狀態,從而達到緊急下架的能力;

驗收效果
jmeter驗收驗證優化效果
Average time = 600ms tps=1200/s

二、緩存庫存模型

2.1 庫存行鎖優化

itemId需要創建唯一索引

alter table item_stock add unique index
item_id_index(item_id)

2.1.1 扣減庫存緩存化

方案

  • 活動發佈同步庫存進緩存;
  • 下單交易減緩存庫存;

問題:
數據庫記錄不一致,緩存中修改了但是數據庫中的數據沒有進行修改;

2.1.2 異步同步數據庫

方案

  • 活動發佈同步庫存進緩存;
  • 下單交易減緩存庫存;
  • 異步消息扣減數據庫內庫存;

可以讓C端用戶完成購買商品的高效體驗,又能保證數據庫最終的一致性;

2.2 異步消息隊列rocketmq

rocketmq

  • 高性能,高併發,分佈式消息中間件;
  • 典型應用場景:分佈式事務,異步解耦;

RocketMQ原理
RocketMq
Producer解決消息生產的問題,Consumer消息的消費端
Broker
相當於一箇中間人,由topic和MessageQueue組成,任何一條rocketmq的消息都是隸屬於某一個topic,一個topic可以被一個messagebroker管理,也可以被多個messagebroker管理;

2.2.1 部署模型

Broker向Nameserver發送註冊請求,broker的ip和負責的topic,queue;
每一個broker至少有一個queue,producer從Nameserver上發現broker1;

採用負載輪詢的方式第一次請求到queue1中,生成一個message1,第二次請求到queue2,生成一個message2,同時consumer會向這兩個queue分別建立長連接
當producer做對應投遞時,consumer會被喚醒,拉取對應的message,這種方式被稱爲長輪詢

Consumer group的作用:以queue爲單位作爲一個消息的管理,當consumer消費完一個message的時候會回覆一個消息給對應的queue,並且將對應的message2變爲已經消費成功要被幹掉的狀態;

若對應的一個queue被多個consumer消費的情況下,勢必會造成一個同步的問題,存在一個鎖競爭的機制,rocketmq採用的是以queue爲單位平均的分配給consumer,所以設計一個好的中間件就是爲了保證queue和consumer的數量相等;

當有多個consumer group,一個訂單系統就屬於一個group,另外一個consumer group就屬於商品系統;生產者端並不知道生成的消息對應的消費端是哪個系統,只會無腦的投消息,關注這個消息的人指出來即可。topicA還是以consumer_group爲管理的基礎單元,一個queue1可以被一個consumer group中的一個consumer所消費,也可以被另一個consumer group中的consumer所消費,以consumer_group去做對應消息的消費和管理

2.2.2 主從複製機制

如果對應的broker1產生了任何異常,producer知道broker掉線了,沒辦法投遞對應的消息;

broker2作爲broker1的slave,平時不對外進行服務,只做消息的從庫,一旦對應的message1被消費;一旦broker1發生異常,nameserver感知到會將broker2變爲主庫,並且通知producer和consumer端,讓其通過broker2去接管對應的消費,slave和master之間的數據可以是同步,也可以是異步。

同步的話,producer生成在broker1中生產message1成功,也要broker2中備份message1也能夠成功,性能偏低;

主broker1作爲生產成功\消費成功即可,roker2做異步複製即可。只要網絡的延遲小,對應的cpu處理速度快,是不會發生消息丟失的情況。但是在分佈式的環境下,沒有辦法同時保證強一致性和可用性。如果選擇強可用性肯定會降低強一致性,當發生主備切換的時候可能會發生消息的丟失。

2.2.3 分佈式事務

分佈式事務
之前所有的操作都是在單庫上面去執行的,依賴於spring的transactional標籤,藉助於MySQL數據庫的ACID****(原子性、一致性、隔離性、持久性)對應的剛性事務強一致的方式保證了數據庫事務的一致性問題。

分佈式設計CAP三方面,一致性、可用性、分區容忍性

分區容忍性是必要的,要麼選擇強一致性等待所有的數據都一致的時候纔可用;要麼就是犧牲強一致性變得可用。所以犧牲強一致性來實現CAP中的A和P(可用性和分區容忍性)。強一致性是重要的,但是不追求瞬時狀態的強一致性,追求的是最終的一致性,達到基礎可用、最終一致性、軟狀態;

軟狀態:在應用當中會瞬時的存在有數據不一致性的情況,比如一部分數據已經成功,另外一部分數據還在處理當中。那我們的業務認爲這些是可以容忍的;

在我們的緩存庫存中,redis中存儲的狀態都是正確的,但是由於異步消息隊列的consumer沒有被觸發,在那一瞬時數據庫的狀態是錯誤的。但只要分佈式事務的消息投遞成功,數據庫的狀態就會被正確更新,這個設計就是用來處理庫存最終一致性的方案。只要消息中間件有99%以上的高可用的方式,就有99%以上的概率是可以保證數據庫的狀態可以跟redis中的狀態是一致的。

2.2.4 rocketmq的安裝

rocketmq安裝地址

修改JAVA_OPT
JAVA_OPT="${JAVA_OPT} -server -Xms512m -Xmx512m -Xmn512m"

./mqadmin updateTopic -n localhost:9876 -t stock -c DefaultCluster
//完成topic的創建就可以使用該創建的topic

2.3 緩存庫存接入異步化

新建一個mq的package;
新建一個MqProducer.java和MqConsumer.java;

在application.properties
mq.nameserver.addr=115.28.67.199:9876 //nameserver地址和端口
mq.topicname=TopicTest //topicname

//進入poxm.xml接入rocketmq的jar包
<dependency>
  <groupId>org.apache.rocketmq</groupId>
  <artifactId>rocketmq-client</artifactId>
  <version>4.3.0</version>
</dependency>

MqProducer.java
MqConsumer.java

ItemServiceImpl.java
一旦更新庫存成功,發送一條消息出去讓異步消息隊列感知到用來減數據庫的庫存
    @Autowired
    private MqProducer mqProducer;
    @Override

@Transactional
public boolean decreaseStock(Integer itemId, Integer amount) throws BusinessException {
    //int affectedRow =  itemStockDOMapper.decreaseStock(itemId,amount);
    long result = redisTemplate.opsForValue().increment("promo_item_stock_"+itemId,amount.intValue() * -1);
    if(result >0){
        //更新庫存成功
        return true;
    }else if(result == 0){
        //打上庫存已售罄的標識
        redisTemplate.opsForValue().set("promo_item_stock_invalid_"+itemId,"true");

        //更新庫存成功
        return true;
    }else{
        //更新庫存失敗
        increaseStock(itemId,amount);
        return false;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章