面試官:給我一個(gè)避免消息重復(fù)消費(fèi)的解決方案?
消息中間件是分布式系統(tǒng)常用的組件,無(wú)論是異步化、解耦、削峰等都有廣泛的應(yīng)用價(jià)值。

簡(jiǎn)單的消息去重解決方案
insert into t_order values .....
update t_inv set count = count-1 where good_id = 'good123';
select * from t_order where order_no = 'order123'
if(order != null) {
return ;//消息重復(fù),直接返回
}
并發(fā)重復(fù)消息
select * from t_order where order_no = 'order123'
if(order != null) {
return ;//消息重復(fù),直接返回
}
select * from t_order where order_no = 'THIS_ORDER_NO' for update //開啟事務(wù)
if(order.status != null) {
return ;//消息重復(fù),直接返回
}
Exactly Once
Exactly-Once 是指發(fā)送到消息系統(tǒng)的消息只能被消費(fèi)端處理且僅處理一次,即使生產(chǎn)端重試消息發(fā)送導(dǎo)致某消息重復(fù)投遞,該消息在消費(fèi)端也只被消費(fèi)一次。
基于關(guān)系數(shù)據(jù)庫(kù)事務(wù)插入消息表
update t_order
set status =
'SUCCESS' where order_no=
'order123';
1.開啟事務(wù)
2.插入消息表(處理好主鍵沖突的問題)
3.更新訂單表(原消費(fèi)邏輯)
4.提交事務(wù)
https://help.aliyun.com/document_detail/102777.html

更復(fù)雜的業(yè)務(wù)場(chǎng)景
檢查庫(kù)存(RPC) 鎖庫(kù)存(RPC) 開啟事務(wù),插入訂單表(MySQL) 調(diào)用某些其他下游服務(wù)(RPC) 更新訂單狀態(tài) commit 事務(wù)(MySQL)
庫(kù)存系統(tǒng)消費(fèi)A:檢查庫(kù)存并做鎖庫(kù)存,發(fā)送消息B給訂單服務(wù) 訂單系統(tǒng)消費(fèi)消息B:插入訂單表(MySQL),發(fā)送消息C給自己(下游系統(tǒng))消費(fèi) 下游系統(tǒng)消費(fèi)消息C:處理部分邏輯,發(fā)送消息D給訂單系統(tǒng) 訂單系統(tǒng)消費(fèi)消息D:更新訂單狀態(tài)

更通用的解決方案

問題一:消息已經(jīng)消費(fèi)成功了,第二條消息將被直接冪等處理掉(消費(fèi)成功)。 問題二:并發(fā)場(chǎng)景下的消息,依舊能滿足不會(huì)出現(xiàn)消息重復(fù),即穿透冪等擋板的問題。 問題三:支持上游業(yè)務(wù)生產(chǎn)者重發(fā)的業(yè)務(wù)重復(fù)的消息冪等問題。


1.性能上損耗更低 2.上面我們講到的超時(shí)時(shí)間可以直接利用Redis本身的ttl實(shí)現(xiàn)
show me code
https://github.com/Jaskey/RocketMQDedupListener ,
//利用Redis做冪等表
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("TEST-APP1");
consumer.subscribe("TEST-TOPIC", "*");
String appName = consumer.getConsumerGroup();// 大部分情況下可直接使用consumer group名
StringRedisTemplate stringRedisTemplate = null;// 這里省略獲取StringRedisTemplate的過程
DedupConfig dedupConfig = DedupConfig.enableDedupConsumeConfig(appName, stringRedisTemplate);
DedupConcurrentListener messageListener = new SampleListener(dedupConfig);
consumer.registerMessageListener(messageListener);
consumer.start();
這種實(shí)現(xiàn)是否一勞永逸?
步驟1:檢查庫(kù)存(RPC) 步驟2:鎖庫(kù)存(RPC) 步驟3:開啟事務(wù),插入訂單表(MySQL) 步驟4:調(diào)用某些其他下游服務(wù)(RPC) 步驟5:更新訂單狀態(tài) 步驟6:commit 事務(wù)(MySQL)
本實(shí)現(xiàn)方式的價(jià)值?

1.各種由于Broker、負(fù)載均衡等原因?qū)е碌南⒅赝哆f的重復(fù)問題 2.各種上游生產(chǎn)者導(dǎo)致的業(yè)務(wù)級(jí)別消息重復(fù)問題 3.重復(fù)消息并發(fā)消費(fèi)的控制窗口問題,就算重復(fù),重復(fù)也不可能同一時(shí)間進(jìn)入消費(fèi)邏輯
一些其他的消息去重的建議
#1.消息消費(fèi)失敗做好回滾處理。如果消息消費(fèi)失敗本身是帶回滾機(jī)制的,那么消息重試自然就沒有副作用了。 #2.消費(fèi)者做好優(yōu)雅退出處理。這是為了盡可能避免消息消費(fèi)到一半程序退出導(dǎo)致的消息重試。 #3.一些無(wú)法做到冪等的操作,至少要做到終止消費(fèi)并告警。例如鎖庫(kù)存的操作,如果統(tǒng)一的業(yè)務(wù)流水鎖成功了一次庫(kù)存,再觸發(fā)鎖庫(kù)存,如果做不到冪等的處理,至少要做到消息消費(fèi)觸發(fā)異常(例如主鍵沖突導(dǎo)致消費(fèi)異常等)
程序汪資料鏈接
程序汪接的7個(gè)私活都在這里,經(jīng)驗(yàn)整理
Java項(xiàng)目分享 最新整理全集,找項(xiàng)目不累啦 04版
堪稱神級(jí)的Spring Boot手冊(cè),從基礎(chǔ)入門到實(shí)戰(zhàn)進(jìn)階
臥槽!字節(jié)跳動(dòng)《算法中文手冊(cè)》火了,完整版 PDF 開放下載!
臥槽!阿里大佬總結(jié)的《圖解Java》火了,完整版PDF開放下載!
字節(jié)跳動(dòng)總結(jié)的設(shè)計(jì)模式 PDF 火了,完整版開放下載!
歡迎添加程序汪個(gè)人微信 itwang007 進(jìn)粉絲群或圍觀朋友圈
評(píng)論
圖片
表情
