关于消息服务的思考

346 阅读5分钟

介绍

几乎所有管理型的系统,都需要站内信(也称作站内消息)的功能,同时可能还会有发送短信、钉钉、微信、邮件等需求。本文试图提供一些构建消息功能的建议。

消息功能的基本要求

  • 支持多租户
  • 消息的种类可扩展,可以在不影响其他消息种类运行的前提下,新增消息种类
  • 支持重试,可以在发送失败后,按照策略自动重试,在超过最大重试次数后计入错误记录表,可人工干预来选择继续重试还是放弃
  • 可以应对和处理突发的大量消息
  • 不会随着时间推移,数据量变大而造成性能问题

消息的分类

消息从发送方式上可以分为:

  • 定向消息,比如待办、待阅消息等等
    • 把消息发送给1个或者有限的几个人
  • 广播消息,也叫通知消息
    • 把消息发送给全体人员

消息从接收方式上可以分为:

  • 站内消息
  • 站外消息
    • 钉钉消息
    • 短信消息
    • 其他,比如邮件、微信等等

对于广播消息,如果不关心用户是否查看(已读)过消息,那么只需要记录消息内容、发送时间即可。如果需要关心哪些用户已经查看(已读)过,那么还需要有一种机制来高效地存储这些通知消息

通用(General Purpose)消息高阶架构

image.png

这是一个典型的Pub-Sub模型,这里涉及到三个主要的角色:

  • Publisher
    • 负责按照一定的编码规则发送消息到Channel
  • Channel
    • 负责接收、缓存和转发消息
    • Channel从是否支持跨进程分发消息,可以分为:
      • 进程内分发,可用的技术包括:Spring Event、RxJava、EventBus(deprecated)
      • 进程间分发,可用的技术包括:Kafka、RabbitMQ、ZeroMQ等MQ产品
  • Subscriber
    • 负责从Channel监听和消费消息,并按照发送时的编码规则进行解码
    • 根据消息的属性,可能把消息进一步转发,比如发送到钉钉平台、短信平台、站内通知等等

实现方案

我们可以打一套支持多组,支持站内消息、站外消息、定向消息以及广播消息的消息服务套件。该套件包括:

  • 一组可扩展的消息发送服务、接收服务
  • 一个可配置的Channel,即可以根据需要在Kafka、RabbitMQ、Spring Event之间切换
  • 一套简单易用的客户端API(隐藏内部通信细节)

消息流转示意图

消息通用模型.png

  • 消息发送服务和消息接收服务都可以从TenantContext中获取当前的租户
  • 对于每个新的租户,都需要新建一套租户私有的Channel(比如:Kafka的topic),来缓存和中转消息
  • Channel的配置动态化
  • 消息接收者连接的钉钉、短信等的配置动态化

对消息类型的支持

  • 对站内消息的支持,一方面把消息连同接收人一起存入数据库(比如MongoDB)、一方面通过SSE向用户发送即时消息
  • 对广播消息的支持,一方面把消息存入数据库(比如MongoDB),一方面通过SSE向用户发送即时消息;在用户阅读过消息后,在数据库中记录已读消息

对扩展性的支持

消息服务应该具备良好的扩展性,体现在:

  • 通过SPI机制增加Channel类型(比如:默认支持Kafka,可以通过扩展来支持RabbitMQ、RocketMQ、或者自定义的消息通道)
  • 通过SPI机制增加消息类型的支持,比如增加发送到微信的能力

以上扩展机制都需要按照一定的规约(SPI)来开发代码,并且需要重新部署服务。

对可配置性的支持

可以让租户从既定列表里面选择Channel、选择消息类型,来满足租户使用消息服务的需求。

因此,消息服务的配置是租户的一个资源属性,就像计算资源、存储资源一样。

租户idchannel_typemessage_type
tenant_1KafkaSMS,DingTalk
tenant_2KafkaSMS,Notice

技术选型参考

进程内消息的发送与订阅

同一个JVM进程内,消息的发布与订阅机制,可以有多种选型。很多年前,普遍使用Guava的EventBus来实现。但是随着技术的不断迭代,Guava官方开始不建议使用EventBus,而是推荐使用DI框架。

这里推荐使用Spring的事件机制(同样基于Pub-Sub模式)来实现进程内的消息通信。

Spring的事件机制,包含三大组件:

  • ApplicationEventPublisher接口
    • 可以直接注入
    • 也可以通过ApplicationContext使用(因为ApplicationContext实现了ApplicationEventPublisher接口)
  • ApplicationEvent
    • 所有的事件对象,都应该继承自该类
  • ApplicationListener接口 或者 EventListener/TransactionalEventListener注解
    • 根据实际情况,使用任何一个都可以

进程间消息的发送与订阅

对于跨进程的消息发布与订阅,分场合使用Kafka或者RabbitMQ。

  • 如果只限于微服务之间的消息传递,则建议使用Spring Cloud Stream Rabbit
  • 如果涉及到与其他系统之间的消息传递,则使用Spring Kafka

结论

本文主要为实现消息功能提供一些建议和参考,主要涉及到了站内消息、站外消息,以及定向消息和广播消息等类型。在实际开发中,需要根据具体的业务场景来灵活选择合适的方案。