在回答“一个交换机还是多个交换机”的问题之前,我实际上想问另一个问题:我们是否真的需要为这种情况创建自定义交换机?
不同类型的对象事件自然而然地匹配不同类型的要发布的消息,但有时并非真正必要。如果我们将所有3种类型的事件抽象为“写入”事件,其子类型为“已创建”,“已更新”和“已删除”,会怎样?
| object | event | sub-type |
|
| user | write | created |
| user | write | updated |
| user | write | deleted |
解决方案1
支持这一点最简单的解决方案是只设计一个“user.write”队列,并通过全局默认交换机将所有用户写入事件消息直接发布到该队列。直接发布到队列的最大限制是它假定只有一个应用程序订阅了这种类型的消息。同一应用程序的多个实例订阅此队列也可以。
| queue | app |
|-------------------|
| user.write | app1 |
解决方案2
当有第二个应用程序(具有不同处理逻辑)想要订阅发布到队列的任何消息时,最简单的解决方案可能无法工作。当有多个应用程序订阅时,我们至少需要一个“fanout”类型的交换机,其绑定到多个队列。这样,消息将被发布到交换机,并且交换机将消息复制到每个队列。每个队列代表每个不同应用程序的处理作业。
| queue | subscriber |
|-------------------------------|
| user.write.app1 | app1 |
| user.write.app2 | app2 |
| exchange | type | binding_queue |
|---------------------------------------|
| user.write | fanout | user.write.app1 |
| user.write | fanout | user.write.app2 |
如果每个订阅者都关心并希望处理“user.write”的所有子类型事件,或者至少将所有这些子类型事件公开给每个订阅者不是问题,则第二种解决方案可以正常工作。例如,如果订阅应用程序仅用于保留交易日志;或者尽管订阅者仅处理“user.created”,但让它知道何时发生了“user.updated”或“user.deleted”也可以。当一些订阅者来自您组织的外部,并且您只想通知他们某些特定的子类型事件时,它变得不太优雅。例如,如果app2只想处理“user.created”,那么它根本不应该知道“user.updated”或“user.deleted”的信息。
解决方案3
为了解决上述问题,我们必须从“user.write”中提取“user.created”概念。使用“主题”类型的交换机可以帮助解决问题。在发布消息时,让我们使用"user.created/user.updated/user.deleted"作为路由键,这样我们就可以将“user.write.app1”队列的绑定键设置为"user.*",将“user.created.app2”队列的绑定键设置为"user.created"。
| queue | subscriber |
|---------------------------------|
| user.write.app1 | app1 |
| user.created.app2 | app2 |
| exchange | type | binding_queue | binding_key |
|-------------------------------------------------------|
| user.write | topic | user.write.app1 | user.* |
| user.write | topic | user.created.app2 | user.created |
解决方案4
如果可能会有更多的事件子类型,那么“topic”交换机类型更加灵活。但是,如果您清楚地知道事件的确切数量,则可以改用“direct”交换机类型以获得更好的性能。
| queue | subscriber |
|---------------------------------|
| user.write.app1 | app1 |
| user.created.app2 | app2 |
| exchange | type | binding_queue | binding_key |
|--------------------------------------------------------|
| user.write | direct | user.write.app1 | user.created |
| user.write | direct | user.write.app1 | user.updated |
| user.write | direct | user.write.app1 | user.deleted |
| user.write | direct | user.created.app2 | user.created |
回到“一个交换机还是多个?”的问题。目前为止,所有的解决方案都只使用了一个交换机。这很好,没有任何问题。那么,什么时候我们需要多个交换机呢?如果一个“主题”交换机有太多绑定,就会出现轻微的性能下降。如果“主题”交换机上的太多绑定导致性能差异真正成为问题,当然你可以使用更多的“直接”交换机来减少“主题”交换机绑定数量以获得更好的性能。但是,在这里我更想关注“一个交换机”解决方案的功能限制。
解决方案5
我们可能自然而然地考虑使用多个交换机来处理不同的事件组或维度。例如,除了上面提到的创建、更新和删除事件之外,如果我们有另一组事件:登录和注销 - 一组描述“用户行为”而不是“数据写入”的事件组。由于不同的事件组可能需要完全不同的路由策略和路由键和队列命名约定,因此拥有一个单独的user.behavior交换机是很自然的。
| queue | subscriber |
|----------------------------------|
| user.write.app1 | app1 |
| user.created.app2 | app2 |
| user.behavior.app3 | app3 |
| exchange | type | binding_queue | binding_key |
|--------------------------------------------------------------|
| user.write | topic | user.write.app1 | user.* |
| user.write | topic | user.created.app2 | user.created |
| user.behavior | topic | user.behavior.app3 | user.* |
其他解决方案
在某些情况下,我们可能需要为一个对象类型进行多个交换。例如,如果您想在不同的交换上设置不同的权限(例如,只允许选定的一个对象类型事件从外部应用程序发布到一个交换中,而另一个交换接受任何来自内部应用程序的事件)。或者,如果您想使用带有版本号后缀的不同交换来支持相同事件组的不同版本的路由策略。或者,您可能想要为交换到交换绑定定义一些“内部交换”,这些内部交换可以以分层方式管理路由规则。
总之,“最终解决方案取决于您的系统需求”,但是通过上述所有解决方案示例和背景考虑,我希望至少能引导您朝正确的方向思考。
我还创建了一篇博客文章,结合了本问题的背景、解决方案和其他相关考虑因素。