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