發(fā)件箱模式:打造微服務(wù)可靠消息傳輸
開發(fā)微服務(wù)以及其他分布式系統(tǒng)都不容易,任何問題都有可能發(fā)生,甚至還有關(guān)于這方面的研究論文。
作為工程師,減少出錯(cuò)的可能性也應(yīng)該是你的目標(biāo)之一,本文將嘗試使用發(fā)件箱模式(Outbox pattern) 來實(shí)現(xiàn)這一點(diǎn)。
如何在分布式系統(tǒng)中實(shí)現(xiàn)組件之間的可靠通信?
發(fā)件箱模式是此類問題的一種優(yōu)雅解決方案,該方案讓我們能夠?qū)崿F(xiàn)事務(wù)性保證,并至少向外部系統(tǒng)傳遞一次消息。
讓我們看看發(fā)件箱模式如何解決這個(gè)問題,以及如何實(shí)現(xiàn)。
發(fā)件箱模式解決了什么問題?
當(dāng)然,要理解發(fā)件箱模式解決了什么問題,我們先給出一個(gè)問題。
下面是一個(gè)用戶注冊流程的示例,有幾件事正在發(fā)生:
- 將 User 保存到數(shù)據(jù)庫
- 向 User 發(fā)生歡迎郵件
- 向消息總線發(fā)布 UserRegisteredEvent
public async Task RegisterUserAsync(User user, CancellationToken token)
{
_userRepository.Insert(user);
await _unitOfWork.SaveChangesAsync(token);
await _emailService.SendWelcomeEmailAsync(user, token);
await _eventBus.PublishAsync(new UserRegisteredEvent(user.Id), token);
}
所有操作都在常規(guī)路徑中按序完成,沒有任何問題,一切都很好。
但如果其中任何一個(gè)操作失敗了怎么辦?
- 數(shù)據(jù)庫不可用,保存 User 失敗
- 郵件服務(wù)中斷,無法發(fā)送郵件
- 向服務(wù)總線發(fā)布事件沒有成功
另外,想象一下這種情況:你已經(jīng)將 User 保存到數(shù)據(jù)庫中,并向他發(fā)送了歡迎郵件,但未能成功發(fā)布 UserRegisteredEvent 來通知其他服務(wù)。怎么才能從這種情況中恢復(fù)過來?
發(fā)件箱模式可以幫你自動更新數(shù)據(jù)庫并將消息發(fā)送到消息總線。
實(shí)現(xiàn)發(fā)件箱模式
首先在數(shù)據(jù)庫中引入一個(gè)表示發(fā)件箱(Outbox) 的新表,可以將這個(gè)表稱為 OutboxMessages,用于存儲需要傳遞的所有消息?,F(xiàn)在,我們不再直接向外部服務(wù)發(fā)出請求,而是簡單的將消息作為新行存儲在發(fā)件箱表中,消息通常以 JSON 格式存儲。
然后引入后臺進(jìn)程,定期輪詢 OutboxMessages 表。如果發(fā)現(xiàn)有未處理的消息,就發(fā)布該消息并標(biāo)記為已發(fā)送。如果由于某種原因造成消息發(fā)布失敗,就在下一次執(zhí)行時(shí)重試。
注意,通過重試,現(xiàn)在實(shí)現(xiàn)了至少一次消息傳遞(at-least-once message delivery)。對于常規(guī)路徑,消息只發(fā)布一次,而在重試的情況下,則會發(fā)布多次。
我們現(xiàn)在可以基于發(fā)件箱模式重寫上面的 RegisterUserAsync 方法:
public async Task RegisterUserAsync(User user, CancellationToken token)
{
_userRepository.Insert(user);
_outbox.Insert(new UserRegisteredEvent(user.Id));
await _unitOfWork.SaveChangesAsync(token);
}
發(fā)件箱與工作單元在同一個(gè)事務(wù)中,因此可以將 User 自動保存到數(shù)據(jù)庫中,并持久化 OutboxMessage。如果保存到數(shù)據(jù)庫失敗,則回滾整個(gè)事務(wù),并且不會向消息總線發(fā)送任何消息。
由于現(xiàn)在將 UserRegisteredEvent 的發(fā)布轉(zhuǎn)移到了工作進(jìn)程,因此需要添加一個(gè)處理程序,以便向用戶發(fā)送歡迎郵件。下面是 SendWelcomeEmailHandler 類的一個(gè)例子:
public classSendWelcomeEmailHandler : IHandle<UserRegisteredEvent>
{
privatereadonly IUserRepository _userRepository;
privatereadonly IEmailService _emailService;
public SendWelcomeEmailHandler(
IUserRepository userRepository,
IEmailService emailService)
{
_userRepository = userRepository;
_emailService = emailService;
}
public async Task Handle(UserRegisteredEvent message)
{
var user = await _userRepository.GetByIdAsync(message.UserId);
await _emailService.SendWelcomeEmailAsync(user);
}
}
發(fā)件箱模式架構(gòu)圖
下面是引入發(fā)件箱后的系統(tǒng)架構(gòu)圖,可以在數(shù)據(jù)庫中看到 Outbox 表,因此可以將消息與相關(guān)實(shí)體一起通過同一事物存儲到 Outbox 表中。
延伸閱讀
通過本文,你應(yīng)該對發(fā)件箱模式以及它解決的問題有了很好的理解。如果需要在分布式系統(tǒng)中實(shí)現(xiàn)可靠消息傳遞,那么發(fā)件箱模式是一個(gè)很好的解決方案。
如果需要了解發(fā)件箱模式的更多實(shí)現(xiàn)細(xì)節(jié),可以觀看以下油管視頻: