緩存庫存—用緩存解決交易問題
概述
本篇博客介紹了下單交易的性能優化技術,通過交易驗證緩存的優化,庫存緩存模型優化解決了交易流程中繁瑣耗性能的驗證緩存,並解決數據庫庫存行鎖的問題,同時也引入了緩存與數據庫分佈式提交過程中不一致的風險。
本章的學習目標是:
- 掌握高效交易驗證方式
- 掌握緩存庫存模型
一、高效交易驗證
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原理
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的安裝
修改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;
}
}