介绍
几乎所有管理型的系统,都需要站内信(也称作站内消息)的功能,同时可能还会有发送短信、钉钉、微信、邮件等需求。本文试图提供一些构建消息功能的建议。
消息功能的基本要求
- 支持多租户
- 消息的种类可扩展,可以在不影响其他消息种类运行的前提下,新增消息种类
- 支持重试,可以在发送失败后,按照策略自动重试,在超过最大重试次数后计入错误记录表,可人工干预来选择继续重试还是放弃
- 可以应对和处理突发的大量消息
- 不会随着时间推移,数据量变大而造成性能问题
消息的分类
消息从发送方式上可以分为:
- 定向消息,比如待办、待阅消息等等
- 把消息发送给1个或者有限的几个人
- 广播消息,也叫通知消息
- 把消息发送给全体人员
消息从接收方式上可以分为:
- 站内消息
- 站外消息
- 钉钉消息
- 短信消息
- 其他,比如邮件、微信等等
对于广播消息,如果不关心用户是否查看(已读)过消息,那么只需要记录消息内容、发送时间即可。如果需要关心哪些用户已经查看(已读)过,那么还需要有一种机制来高效地存储这些通知消息
通用(General Purpose)消息高阶架构
这是一个典型的Pub-Sub模型,这里涉及到三个主要的角色:
- Publisher
- 负责按照一定的编码规则发送消息到Channel
- Channel
- 负责接收、缓存和转发消息
- Channel从是否支持跨进程分发消息,可以分为:
- 进程内分发,可用的技术包括:Spring Event、RxJava、EventBus(deprecated)
- 进程间分发,可用的技术包括:Kafka、RabbitMQ、ZeroMQ等MQ产品
- Subscriber
- 负责从Channel监听和消费消息,并按照发送时的编码规则进行解码
- 根据消息的属性,可能把消息进一步转发,比如发送到钉钉平台、短信平台、站内通知等等
实现方案
我们可以打一套支持多组,支持站内消息、站外消息、定向消息以及广播消息的消息服务套件。该套件包括:
- 一组可扩展的消息发送服务、接收服务
- 一个可配置的Channel,即可以根据需要在Kafka、RabbitMQ、Spring Event之间切换
- 一套简单易用的客户端API(隐藏内部通信细节)
消息流转示意图
- 消息发送服务和消息接收服务都可以从TenantContext中获取当前的租户
- 对于每个新的租户,都需要新建一套租户私有的Channel(比如:Kafka的topic),来缓存和中转消息
- Channel的配置动态化
- 消息接收者连接的钉钉、短信等的配置动态化
对消息类型的支持
- 对站内消息的支持,一方面把消息连同接收人一起存入数据库(比如MongoDB)、一方面通过SSE向用户发送即时消息
- 对广播消息的支持,一方面把消息存入数据库(比如MongoDB),一方面通过SSE向用户发送即时消息;在用户阅读过消息后,在数据库中记录已读消息
对扩展性的支持
消息服务应该具备良好的扩展性,体现在:
- 通过SPI机制增加Channel类型(比如:默认支持Kafka,可以通过扩展来支持RabbitMQ、RocketMQ、或者自定义的消息通道)
- 通过SPI机制增加消息类型的支持,比如增加发送到微信的能力
以上扩展机制都需要按照一定的规约(SPI)来开发代码,并且需要重新部署服务。
对可配置性的支持
可以让租户从既定列表里面选择Channel、选择消息类型,来满足租户使用消息服务的需求。
因此,消息服务的配置是租户的一个资源属性,就像计算资源、存储资源一样。
| 租户id | channel_type | message_type |
|---|---|---|
| tenant_1 | Kafka | SMS,DingTalk |
| tenant_2 | Kafka | SMS,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
结论
本文主要为实现消息功能提供一些建议和参考,主要涉及到了站内消息、站外消息,以及定向消息和广播消息等类型。在实际开发中,需要根据具体的业务场景来灵活选择合适的方案。