文章目錄
- 1.可靠消息最終一致性
- 1.1 本地消息表
- 1.1.1 本地消息表的優缺點
- 1.消息堆積,掃表慢
- 2.集中式掃表,會影響正常業務
- 3.定時掃表的延遲問題
- 1.1.2 本地消息表的代碼實踐
- 1.表結構設計
- 2.具體業務實現
- 1.2 事務消息
- 1.2.1 事務消息的三個階段
- 階段1:事務準備與消息預提交
- 階段2:執行本地事務與最終提交
- 階段3:消息投遞與事務完成
- 1.2.2 總結
- 2.最大努力通知
- 2.1 最大努力通知流程
- 2.2 最大努力通知和本地消息表區別?
- 3.面試挑戰
- 3.1 系統設計題
- 3.2 技術深度題
- 3.3 C. 性能優化題
- 3.4 面試回答技巧
- 3.5 技術深度展示
- 3.6 面試中需要注意的點
- 3.7 面試準備建議
- 3.8 面試話術模板
在分布式事務的實現中,有很多種方案,其中比較常用的就是基于MQ來實現,在MQ的具體實現中,又有很多種具體的方案,從大的方面來說,于MQ的分布式事務基可以分為兩種:
- 可靠消息最終一致性
- 最大努力通知
1.可靠消息最終一致性
可靠消息最終一致性,顧名思義就是依賴可靠的消息,來實現一種最終一致性的模型,
他的大致流程就是:
- 1、事務的發起方執行本地事務
- 2、事務的發起方向事務的參與方發送MQ消息
- 3、事務的參與方接收到MQ消息后執行自己的本地事務
這里面事務的發起方和參與方都需要各自執行本地事務,他們之間,通過可靠消息來保障最終一致。
那么,怎么樣的消息算可靠呢。
直接依賴kafka、rocketMQ發送一個消息就可靠了么?
顯然是不行的,因為我們知道,在出現網絡分區、網絡延遲等情況時,是沒辦法保證消息一定可以發出去的,也沒辦法保證消息發出去就一定能被成功消費。
那么想要做到讓這個消息可靠,一般有兩種做法:
- 本地消息表
- 事務消息
下面我們一個個來了解一下
1.1 本地消息表
這個方案的主要思想是將分布式事務拆分為本地事務和消息事務兩個部分,本地事務在本地數據庫中進行提交或回滾,而消息事務則將消息寫入消息中間件中,以實現消息的可靠投遞和順序性
一般來說的做法是,在發送消息之前,先創建一條本地消息,并且保證寫本地業務數據的操作和寫本地消息記錄的操作在同一個事務中。這樣就能確保只要業務操作成功,本地消息一定可以寫成功。
整體流程如上圖所示,消息發出去之后,等待消費者消費,在消費者端,接收到消息之后,做業務處理,處理成功后再修改本地消息表的狀態。
這個過程中,可能有幾個步驟都可能發生失敗,那么如果失敗了怎么辦呢?
- 1、2如果失敗,因為在同一個事務中,所以事務會回滾,3及以后的步驟都不會執行。數據是一致的。
- 3如果失敗,如果是同步,可以直接根據發送狀態,如果發送失敗,直接去重試,如果是異步,可以監聽異常回調和發送結果,如果失敗,也是立刻同步重試(注意最大重試次數),同時還需要有一個定時任務,不斷的掃描本地消息數據,對干未成功的消息進行重新投遞,大致代碼如下:
Mq.sendAsync(topic.getTopic(), message,new CallBack() {@Overridepublic void onException(Throwable exception) {retry(topic, message, 2);}@Overridepublic void onResult(SendResponse res) {if (!res.isSucceed()) {retry(topic, message, 2);}}});private static boolean retry(MqTopicEnum topic, SimpleMessage message, int retry) {int times = 0;boolean rst = false;for (; retry > 0; retry--) {times++;rst = SendOnce(topic, message);if (rst) {break;}}if (!rst) {//重試3次仍失敗,要發出告警AlarmUtil.metric(AlarmCode.MQ_SEND_ERROR, topic.getTopic(), message.getKey(), null);}return rst;}
- 4、5如果失敗,則依靠消息的重投機制,不斷地重試
- 6、7如果失敗,那么就相當于兩個分布式系統中的業務數據已經一致了,但是本地消息表的狀態還是錯的。這種情況也可以借助定時任務繼續重投消息,讓下游冪等消費再重新更改消息狀態,或者本系統也可以通過定時任務去查詢下游系統的狀態,如果已經成功了,則直接推進消息狀態即可。
1.1.1 本地消息表的優缺點
優點:
- 1.可靠性高:基于本地消息表實現分布式事務,可以將本地消息的持久化和本地業務邏輯操作,放到一個事務中執行進行原子性的提交,從而保證了消息的可靠性。
- 2.可擴展性好:基于本地消息表實現分布式事務,可以將消息的發送和本地事務的執行分開處理,從而提高了系 自統的可擴展性。
- 3.適用范圍廣:基于本地消息表實現分布式事務,可以適用于多種不同的業務場景,可以滿足不同業務場景下的需求。
但是其缺點也非常明顯,因為我們是通過定時任務去掃表,所以會遇到以下幾個問題:
- 1.消息堆積后,通過掃表去消息是否發送以及下游是否消費成功的效率極低
- 2.集中式掃表,會影響正常業務
- 3.定時掃表會存在延遲問題
知道了問題,我們逐一去解決
1.消息堆積,掃表慢
隨著本地消息表中的數據量越來越大,通過定時任務掃表的方式會越來越慢,那么想要解決這個問題,首先可以考慮加索引。
我們可以在state字段上增加一個素引,雖然這個字段的區分度不高,但是一般來說,這張表中,success的數據量占90%,而init的數據量只占10%,而我們掃表的時候只關心init即可,所以增加索引后,掃表的效率是可以大大提升的。
里面涉及到一個知識點:就是針對區分度不高的字段加索引的問題
- 如果是加聚合索引,盡可能讓區分布不高的字段排后
- 如果是針對單一字段加索引,如果字段區分度不高,索引大概率不會生效,還是會去全表查詢,但是有一種場景例外,如男女比例是95:5,那么,這時候,如果我用"女’作為性別的查詢條件的話,還是可以走索引,并且有很大的性能提升的,原因就是因為他可以過濾掉大部分數據。走索引可以大大提升效率。這種一般在任務表中比較多,比如任務表中有狀態,兩種情況: INIT和SUCCESS,大多數情況下,任務都是SUCCESS的,只有一少部分是INIT,這時候就可以給這個字段加索引。這樣當我們掃描任務表執行任務的時候,還是可以大大提升查詢效率的。
只加索引肯定是不能夠滿足大數據量的情況下遇到的高并發問題,此時我們還需要引入多線程并發掃表,在掃表的時候通過分段的思想進行數據隔離即可,整體流程如下:
假設有10個線程,那么第一個線程就掃描D處于0-1000的數據,第二個線程掃描1001-2000的數據,第三個線程掃描2001-3000的數據。這樣以此類推,線程之間通過分段的方式就做好了隔離,可以避免同一個數據被多個線程掃描到。
這個做法,有個小問題,那就是INIT的數據的ID可能不是連續的,那么就需要考慮其他的分段方式,比如在時間表中增加一個業務ID,然后根據這個biz id做分片也可以。
public void fn() {Long minId = messageService.getMinTinitId();for (int i = 1; i <= threadPool.size(); i++) {Long maxId = minId + segmentSize() * i;List<Message> messages = messageService.scanInitMessages(minId, maxId);proccee(messages);minId = maxId + 1;}}
SELECT * FROM RETRY_MESSAGE
WHERE STATE ="INIT"
AND
BIZ_ID between min and max
雖然解決了掃表慢的問題,但是又帶來另外一個問題:如果業務量比較大的話,集中式的掃描數據庫勢必給數據庫帶來一定的壓力,那么就會影響到正常的業務。
2.集中式掃表,會影響正常業務
因為數據量大的話會一直掃表做查詢,數據量大的時候查詢就會很慢,那么數據庫連接數就會被占滿。導致應用的正常請求拿不到連接.
那么想要解決這個問題,首先可以考慮,不掃主庫,而是掃描備庫。
當然很多人說主從庫之間的數據同步都是依賴binlog,由于Binlog的刷盤策略,可能會出現數據丟失或者同步延遲的問題,但是我們這個場景之所以能這么做,是因為這個業務場景一般都是可以接受一定的數據延遲的,那么備庫帶來延遲就可以忽略,但是備庫是沒有業務操作的,所以對備庫的掃描是不會對業務造成影響的。
當然,這里還要考慮一個問題,那就是備庫掃描數據之后的執行,執行完該如何同步到主庫,這里可以直接修改主庫,主備庫數據ID一致的,直接去修改主庫的就行了。不建議直接在備庫上修改。不光此類業務,其他的主從讀寫邏輯也是建議:所有的更新、刪除以及新增操作一定要在主庫執行,從庫只做讀或只做數據副本邏輯。
但是不管怎么樣,備庫還是可以分擔掃表的這個大量高峰請求的
除了掃備庫,還有一個方案,那就是做分庫了。把原來集中在同一個數據庫的數據分散到不同的數據庫中。這樣用 自集群代替單庫來整體對外提供服務,可以大大的提升吞吐量。
因為多個數據庫的話,每個庫提供的連接數就會多,并且多個實例的話,CPU、IO、LOAD這些指標也可以互相分擔
3.定時掃表的延遲問題
定時任務都是集中式的定時執行的,那么就會存在延遲的問題。隨著數據庫越來越大,延時會越來越長。
想要降低延遲,那就要拋棄定時任務的方案,可以考慮延遲消息,基于延遲消息來做定時執行。
基于RocketMq類似的消息隊列做延遲消息之后,可以做到流量削峰,解耦,還可以緩解數據庫的壓力。這種做法比定時掃表的性能要好,實時性也更高。
1.1.2 本地消息表的代碼實踐
1.表結構設計
索引設計:
- state作為普通索引,用于高效掃表。這個也可以和 retry_count 、next_retry_at 等字段一起建個聯合索引。
- message key + message type 作為唯一索引,防止重復插入
lock _version,主要用于樂觀鎖處理,避免出現并發問題導致更新錯誤
next_retry_at 和 last_retry_time 這兩個字段可以有一個也行,都沒有也不是不行,看你的實際業務情況,有的任務是可以主動設定下次執行時間,比如特殊的消息就是要3小時執行一次,那么就可以在每次執行后,如果失敗了,則把當前時間加上3小時,設置到 next_retry_at 上面去。
last_retry_time 這個是方便我們掃表的時候可以設置特殊的過濾條件,比如只針對沒重試過的消息( last_retry_time is null )進行掃描,或者針對10分鐘之前的消息處理( last_retry_time <now -10 min)等等。
2.具體業務實現
@Transactionalpublic void order(rderDTO orderDTO) {orderServive.createOrder(orderDTO);messageService.createMessage(orderDTO);}
但是這里只是記錄了本地消息,還需要把本地消息通過MQ發出去。這里就可以有很多辦法了
- 一種是異步掃表還可以直接同步發消息
- 也可以借助Spring Event來異步處理,都是可以的。
但是如果是同步發的話時效性肯定更好,但是同步發消息需要注意,要把調MQ發消息的地方放到事務外,要不然自會因為MQ網絡延遲等問題導致回滾。
所以就可以用編程式事務,當然你也可以在發送MQ發消息的方法上,事務傳播機制設置為Propagation.NOT_SUPPORT,這樣就不會影響事務
@AutowiredTransactionTemplate transactionTemplate;public void order(OrderDTO orderDTO) {boolean transactionSuccess = transactionTemplate.execute(new TransactionCallback<Boolean>() {@Overridepublic Boolean doInTransaction(TransactionStatus status) {try {orderServive.createOrder(orderDTO);messageService.createMessage(orderDTO);return true; // 表示事務執行成功} catch (Exception e) {status.setRollbackOnly();return false; // 表示事務執行失敗}}});if (transactionSuccess) {// 事務執行成功,可以執行 mgService.send(orderDTO);messageService.updateSuccess(orderDTO);} else {// 事務執行失敗的處理邏輯// 可以拋出異常,添加告警、記錄日志等throw new RuntimeException("事務執行失敗");}}
在事務中寫入本地業務數據+本地消息,然后在事務外發MQ消息,如果發送失敗了,也不影響事務的commit,如果發送成功了,把本地消息表的狀態推進一下。
如果失敗,下一次再通過定時任務掃表把需要處理的事件查出來重發就行了
所以本地消息表中還需要有一個定時任務,還需要提供一個接口給下游回調,但是上面說到,定時任務會導致掃表任務
讀到這兒,同學們可能就會發現,我們目前所有的流程都沒有提到下游服務失敗了,但是我們上游執行成功了,怎么保證業務一致性的問題,如:在整個交易鏈路里,乘客發單命中風控,然后走預付邏輯,乘客預付成功之后,就會走發單邏輯,此處就會有個問題,如果發單失敗,是走重新發單,還是走退款邏輯呢,當然里面涉及的業務鏈路也比較復雜,我們只談創建交易單和創建乘客單這兩個邏輯,兩個的順序一定是先A在B的邏輯,對于本地消息表的方案,首先,我們需要知道,本地消息表的方案并不適合用在這種需要回滾的場景,而是適合用在哪種不需要回景。什么場景不需要回滾呢?
在舉個例子,我們都知道,用戶下單之后,會給用戶創建一個運費險,那么這個場景,一般運費險的投保過程前置條件就是下單成功,而且,下單的時候給用戶表達了有運費險,那么就意味著,一旦下單成功了,就必須要功。不能因為投保未成功而導致訂單回滾。包括我上面這個例子,為了整個業務的完單量和GMV,一旦成功支付成功,我們就必須訂單創建成功也是一樣的道理
所以,這就是典型的本地消息表的,或者是事務消息適合的場景!!!即不回滾,必須成功。
但是,如果面試官問了這個問題,你回答了上面的內容,他還是問你,我就要回滾,該咋做呢?
那么我們只能參考類似Mysql innodb引擎下的undoLog的回滾機制,即補償機制,當然這個也是在分布式事務的范疇,這個就是咱們常說的:SAGA 事務
SAGA 的核心思想一句話就可以說明白,就是把業務分成一個個步驟,當某一個步驟失敗的時候,就反向補償前面的步驟,SAGA的核心:反向補償
以A:交易 B:行程兩個服務為例
流程為:A創建交易單,乘客支付成功之后,B服務創建建訂單為例
首先本地消息表保證的是消息一定能發送成功且一定能夠被下游服務消費到,這個控制的邏輯總結起來就一句話:先一直嘗試重試,重試達到上限,通過延遲消息或者定時任務去讀本地消息表,將未投遞成功的消息重新投遞,或未消費的消息以及消費失敗的消息重新發送
如果B服務遇到問題,如創建訂單失敗,先對B服務進行回滾,然后在業務層面,執行補償,如直接執行退款流程等
但是上面還是有個我們之前提到的問題,就是定時任務當遇到消息量過大,消息表數量太多的時候,會遇到因掃表時間過長無效占用數據庫連接等問題,所以我們可以通過以下方案去解決:
首先,下游服務處理消息時發生業務失敗,如網絡異常、數據校驗不通過、依賴服務不可用等等,需要先明確返回個失敗的響應,這樣MQ就會繼續投遞這個消息。所以,失敗的時候,我們優先考慮的是重試,而不是回滾。
之后如果多次重試都不成功,那么就可以借助死信隊列,把這條失敗的消息放進死信隊列中,然后上游可以再監聽這個死信隊列的消息,做本地事務的回滾。
當然,為了避免出現錯誤。回滾前建議反查一下下游的接口,避免實際成功了的情況。
其實,這個方案,看上去挺好的,但是實際上就很麻煩,因為這么做了之后,兩個系統之間的耦合就很嚴重,互相依賴消息,互相依賴接口查詢。而且一旦有多個消息監聽者都要去操作,比如下單后要依次運費險投保、給用戶發消息、給用戶加積分等等,就很麻煩如果部分成功,部分失敗,又怎么處理呢?實際上系統根本沒法處理。。。。
但是如果面試官一定要,那有沒有更好的方案了。當然,對于很多面試官來說,你不說他也不知道有這些問題,那就給他這個他想聽的答案好了。
實際大部分公司,包括我們公司采用的是人工介入,當一個消息多次投遞都不成功的話,記錄到DB,并且當這種問題環比或者同比增長超過閾值時,人工介入。
同時也可以依靠對帳機制做準實時對賬,發現不一致的情況人工介入。
這種場景發生的概率很低,一旦發生了可能就是有一些特殊的原因,可能需要人工介入才能解決。
1.2 事務消息
很多人讀到這兒會一臉問號,上面不是一直在提到用MQ去解耦流程嗎,怎么又提到一個事務消息,如果不用事務消息,就用本地消息的話,那么一次操作一般是這樣的流程:
- 1、執行本地事務
- 2、發送MQ消息
- 3、消費MQ消息
如果一切順利,那么沒啥說的,雙方都能處理成功,最終是一致的,但是實際情況是,因為網絡延遲、網絡抖動服務器本身的穩定性、MQ自身的穩定性等原因,這個過程會出現各種各樣的問題。
一旦在第一個參與者本地事務操作之后,如果出現了MQ發送失敗、或者發送成功了,但是MQ自己存儲失敗了等原因,可能就會導致不一致了。
有人會問了,那這里如果MQ發失敗了,本地事務回滾不就行了么?
有問題,因為會出現一種極端情況,那就是當出現網絡抖動的時候,發送MQ因為網絡超時返回了失敗,本地事務回滾之后,但是網絡超時不一定是MQ沒有接收到,有可能處理成功了,但是返回的時候超時了。這時候就會出現:本地事務回滾了、但是MQ發送成功了的問題。這時候下游正常消費MQ之后,就又出現不一致了。
而且,MQ自身也不一定可靠,不管是哪種MQ,在極端情況下,都是有可能丟消息的,也就說,可能會出現本地事務成功之后,發送MQ成功了,但是因為MQ自身原因,導致消息丟了,還是會出現不一致。
所以,總之就是引入MQ之后,會因為各種原因導致不一致,那怎么解決這個問題呢?
解決方案就是能有一個機制保證MQ一定可以發送成功,或者是如果失敗了,也有機制能夠重試讓他成功。
秉持這個思路,上面的本地消息表其實就是在做這個事,但是執行起來又會遇到其它問題,消息量增大,分布分表改造會較大,不分庫分表,會出現性能瓶頸的問題
所以引入了事務消息 (RocketMQ中的那種,非Kafka中的那種),即把一個發送消息的過程拆成2步,先發一個半消息,確保成功之后,在執行本地事務,本地事務成功后,再發第二個半消息。
1.2.1 事務消息的三個階段
MQ的事務主要分為三個階段:
階段1:事務準備與消息預提交
1.發送Half消息
- 事務主動方(系統A)先發送half消息(半消息) 到MQ
- MQ持久化但不立即投遞(對下游不可見)
- MQ返回持久化成功ACK(確保消息不丟失)
半消息
Half消息是指已經發送到MQ服務器并持久化,但暫時不能投遞給消費者的消息。
半消息是發消息到指定的topic:RMQ_SYS_TRANS_HALF_TOPIC
半消息里存儲的數據結構大致如下:
RMQ_SYS_TRANS_HALF_TOPIC
├── 半消息1(原始Topic: order_topic, 原始消息內容)
├── 半消息2(原始Topic: inventory_topic, 原始消息內容)
├── 半消息3(原始Topic: payment_topic, 原始消息內容)
└── ...
- 正常消息:發送 -> 持久化 -> 立即投遞
- Half消息:發送 -> 持久化 -> 等待確認 -> 決定是否投遞
階段2:執行本地事務與最終提交
1.本地事務處理
- 系統A收到ACK后執行本地業務邏輯(如扣減庫存/創建訂單)
2.提交二次確認
- 根據事務結果向MQ發送指令:
- ? Commit → 半消息標記為可投遞狀態
- ? Rollback → MQ刪除半消息
- ?? 超時未響應 → 觸發消息回查
?? 設計重點:Commit/Rollback可能因網絡、宕機丟失,需超時回查兜底
3.事務回查API
生產者需實現 TransactionListener:
- executeLocalTransaction():執行業務邏輯
- checkLocalTransaction():響應回查請求
public class OrderTransactionListener implements TransactionListener {// 執行本地事務(如創建訂單)@Overridepublic LocalTransactionState executeLocalTransaction(Message msg, Object arg) {try {createOrder(msg.getOrderId()); // DB操作return LocalTransactionState.COMMIT_MESSAGE;} catch (Exception e) {return LocalTransactionState.ROLLBACK_MESSAGE;}}// Broker 回查時觸發(如超時未收到Commit)@Overridepublic LocalTransactionState checkLocalTransaction(MessageExt msg) {OrderStatus status = orderDao.getStatus(msg.getOrderId());return status == PAID ? COMMIT_MESSAGE : ROLLBACK_MESSAGE;}
}
階段3:消息投遞與事務完成
MQ投遞消息
- 僅處理Commit狀態的半消息 → 投遞給消費者(系統B)
消費者處理
- 系統B執行本地事務(如更新賬戶余額)
- 返回處理結果ACK給MQ:
- ? 成功 → MQ刪除消息(事務完結)
- ? 失敗 → 觸發消息重投機制
🔁 重試保障:MQ按配置間隔(如2s/5s/10s)重試,上限N次后進死信隊列人工介入
2.異常場景與容錯機制
場景1:事務主動方提交超時
- 問題:MQ未收到Commit/Rollback指令
- 解決方案:消息回查
在RocketMQ的事務消息中,如果半消息發送成功后,RocketMQ Broker在規定時間內沒有收到COMMIT或者ROLLBACK消息。
RocketMQ會向應用程序發送一條檢查請求,應用程序可以通過回調方法返回是否要提交或回滾該事務消息。如果應用程序在規定時間內未能返回響應,RocketMQ會將該消息標記為“UNKNOW”狀態
在標記為“UNKNOW”狀態的事務消息中,如果應用程序有了明確的結果,還可以向MQ發送COMMIT或者ROLLBACK.
但是MQ不會一直等下去,如果過期時間己到,RocketMQ會自動回滾該事務消息,將其從事務消息日志中刪除.
場景2:消費者處理失敗
- 問題:系統B消費異常(網絡中斷/業務異常)
- 解決方案:
- MQ自動重試投遞(至少一次語義)
- 消費者冪等設計:通過唯一事務ID避免重復消費
場景3:MQ服務端故障
- 預防措施:
- 半消息持久化磁盤(Broker高可用部署)
- 同步刷盤+主從復制(防單點故障)
場景4:第一次半消息發送失敗怎么辦
在事務消息的一致性方案中,我們是先發半消息,再做業務操作的
所以,如果半消息發失敗了,那么業務操作也不會進行,不會有不一致的問題。
遇到這種情況重試就行了。(可以自己重試,也可以依賴上游重試)
1.2.2 總結
RocketMQ的事務消息是通過ransactionListener接口T和半消息機制來來實現的。
在發送事務消息時,首先向RocketMQ Broker發送一條“half消息”(即半消息),半消息將被存儲在Broker端的事務消息日志中,但是這個消息還不能被消費者消費。
接下來,在半消息發送成功后,應用程序通過執行本地事務來確定是否要提交該事務消息。
如果本地事務執行成功,就會通知RocketMQ Broker提交該事務消息,使得該消息可以被消費者消費;否則,就會通知RocketMQ Broker回滾該事務消息,該消息將被刪除,從而保證消息不會被消費者消費。
2.最大努力通知
2.1 最大努力通知流程
所謂最大努力通知,換句話說就是并不保證100%通知到。這種分布式事務的方案,通常也是借助異步消息進行通知的。
采用這種方式一般是下游業務失敗了對整體邏輯也不會影響,如乘客支付成功給乘客發短信或者郵件的邏輯,即使發送消息失敗也不會影響后續流程。
具體邏輯如下:
發送者將消息發送給消息隊列,接收者從消息隊列中消費消息。在這個過程中,如果出現了網絡通信故障或者消息隊列發生了故障,就有可能導致消息傳遞失敗,即消息被丟失。因此,最大努力通知無法保證每個接收者都能成功接收到消息,但是可以盡最大努力去通知。
下面是一個簡單的例子來說明最大努力通知的過程。假設有一個在線商城系統,顧客可以下訂單購買商品。當顧客成功下單后,通知顧客訂單已經確認。這個通知就可以采用最大努力通知的方式。
- 顧客下單后,商城訂單系統會生成訂單并記錄訂單信息。
- 商城訂單系統通過最大努力通知機制,將訂單確認通知發送給用戶通知服務.
- 用戶通知服務把下單消息通過電子郵件發送給用戶。
- 商城系統不會等待顧客的確認,而是將通知放入消息隊列中,并盡力發送通知。
- 如果通知發送成功,那就很好,顧客會盡快收到訂單確認郵件。但如果由于網絡問題、電子郵件服務器問題或其他原因導致通知發送失敗,商城系統可能會做一些嘗試,盡可能的通知,重試多次后還是不成功,則不再發送
需要注意的是,在最大努力通知的過程中,可能會出現消息重復發送的情況,也可能會出現消息丟失的情況。因此,在設計最大努力通知系統時,需要根據實際業務需求和風險承受能力來確定最大努力通知的策略和重試次數以及對消息進行冪等等處理。
最大努力通知這種事務實現方案,一般用在消息通知這種場景中,因為這種場景中如果存在一些不一致影響也不大。
2.2 最大努力通知和本地消息表區別?
方案 | 可靠性 | 性能 | 復雜度 | 適用場景 |
---|---|---|---|---|
本地消息表 | 高 | 中等 | 中等 | 強一致性要求 |
事務消息 | 高 | 高 | 低 | 簡單場景 |
最大努力通知 | 低 | 高 | 低 | 通知類場景 |
本地消息表相對于最大努力通知而言,引入了本地消息表,通過本地事務來保證消息可以發送成功。相對來說,具有更強的可靠性,可以在一定程度上保證消息的傳遞不丟失。但是,本地消息表也會帶來額外的存儲開銷和網絡通信成本。
而最大努力通知這種方案比較簡單,但是可能存在丟消息的情況。其實,一般業務中,也會通過對賬來解決的,并不會完全放任消息丟失,只不過對賬的機制會有一定的延時,并且可能需要人工介入。
3.面試挑戰
3.1 系統設計題
面試官:設計一個訂單系統,如何保證訂單創建和庫存扣減的一致性?
您的回答框架:
- 問題分析:說明分布式事務的挑戰
- 方案對比:本地消息表 vs 事務消息 vs 最大努力通知
- 技術選型:根據業務場景選擇合適方案
- 實現細節:提供具體的代碼實現
- 優化策略:多線程掃描、索引優化、主從分離
3.2 技術深度題
面試官:RocketMQ事務消息是如何保證一致性的?
您的回答要點:
- 三個階段:半消息 → 本地事務 → 最終確認
- 異常處理:回查機制、重試策略
- 容錯設計:超時處理、死信隊列
3.3 C. 性能優化題
面試官:消息表數據量大時如何優化掃描性能?
您的回答策略:
- 索引優化:狀態字段索引、復合索引
- 并發控制:多線程分段掃描
- 架構優化:主從分離、分庫分表
3.4 面試回答技巧
STAR法則應用:
- Situation: “我們系統遇到了消息堆積問題…”
- Task: “需要設計一個可靠的分布式事務方案…”
- Action: “采用了本地消息表 + 多線程掃描…”
- Result: “性能提升了80%,消息可靠性達到99.9%…”
3.5 技術深度展示
// 展示您的技術深度
@Transactional
public void createOrder(OrderRequest request) {// 1. 業務邏輯Order order = createOrderLogic(request);// 2. 消息記錄(同一事務)LocalMessage message = createMessageRecord(order);// 3. 事務提交后異步發送asyncSendMessage(message);
}
3.6 面試中需要注意的點
? 要強調的亮點:
- 問題分析能力:從業務場景到技術方案的推導
- 技術選型思維:不同方案的優缺點對比
- 性能優化經驗:從索引到架構的全面優化
- 異常處理能力:各種失敗場景的應對策略
?? 要避免的問題:
- 不要死記硬背:理解原理比記憶代碼更重要
- 不要過度復雜:根據面試官水平調整技術深度
- 不要忽略業務:技術方案要結合業務場景
3.7 面試準備建議
1.準備核心知識點:
- 分布式事務的CAP理論
- 本地消息表的實現原理
- RocketMQ事務消息的機制
- 性能優化的具體策略
2.準備實際案例:
- 項目中遇到的具體問題
- 解決方案的選擇過程
- 優化效果的量化數據
3.準備代碼示例:
- 關鍵代碼片段要能現場手寫
- 理解每行代碼的作用
- 能解釋設計思路
建議在面試前:
- 重點準備幾個核心概念的解釋
- 準備1-2個具體的項目案例
- 練習現場畫圖或寫代碼
- 準備一些性能優化的量化數據
這樣就能在面試中很好地展示您的技術能力和項目經驗!
3.8 面試話術模板
"在分布式系統中,我們經常遇到數據一致性問題。比如訂單創建和庫存扣減,如果不在一個事務中,
可能會出現訂單創建成功但庫存扣減失敗的情況。我們采用了本地消息表的方案:
1. 在同一個事務中創建訂單和消息記錄
2. 事務提交后異步發送消息
3. 通過定時任務保證消息最終發送成功這個方案的優勢是..."