发件箱模式:打造微服务可靠消息传输

开发微服务以及其他分布式系统都不容易,任何问题都有可能发生,甚至还有关于这方面的研究论文

作为工程师,减少出错的可能性也应该是你的目标之一,本文将尝试使用发件箱模式(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); }1.2.3.4.5.6.7.8.9.10.

所有操作都在常规路径中按序完成,没有任何问题,一切都很好。

但如果其中任何一个操作失败了怎么办?

数据库不可用,保存 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); }1.2.3.4.5.6.7.8.

发件箱与工作单元在同一个事务中,因此可以将 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); } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.
发件箱模式架构图

下面是引入发件箱后的系统架构图,可以在数据库中看到 Outbox 表,因此可以将消息与相关实体一起通过同一事物存储到 Outbox 表中。

延伸阅读

通过本文,你应该对发件箱模式以及它解决的问题有了很好的理解。如果需要在分布式系统中实现可靠消息传递,那么发件箱模式是一个很好的解决方案。

如果需要了解发件箱模式的更多实现细节,可以观看以下油管视频:

How to use the Domain Events patternHow to implement the Outbox patternHow to add retries to the Outbox with Polly

THE END