消息中间件
当服务之间需要异步化和解耦,这时需要消息中间件来完成。
当系统A自己的逻辑完成后需要通知其他系统,但是这个通知和系统A本身的逻辑并没有直接依赖关系,如果通过同步调用来告诉其他系统的话会使系统依赖太多而变得太重,而且如果有多个系统需要通知的话会使链路变得十分复杂,并且性能影响会很大。这时可以使用消息中间件,系统A只需将消息发送给消息中间件即可,由消息中间件来负责消息的下发,如果有系统有场景需要依赖这个消息,直接订阅即可,消息中间件会保证消息下发的可靠性。这样整个架构会变得十分清晰,也就是消息中间件给整个系统架构带来的异步化和解耦的功效。
Java有自己的消息规范,即JMS,如Hornetq、ActiveMQ等都是JMS的实现,但是到了大型互联网的场景中就不太适用了。前面说过我们最需要消息中间件的异步化和解耦这两个特点,在这个基础之上我们需要着重考虑的是顺序保证、扩展性、可靠性、一致性、多集群订阅等问题。
一致性
消息发送的一致性就是业务处理成功和消息发送出去需要保持一致,不能出现业务处理成功但消息没发送出去,或者业务处理失败但消息却发送出去的情况。那么消息中间件需要如何来保证消息的一致性呢。
在单机的情况下我们知道使用事务可以保证程序不同动作的一致性,只要有一个失败全部回滚,消息中间件的一致性也可以参考这种方式。
上图描述了保证一致性的消息中间件的发送消息流程:
- 发送消息给消息中间件:发送的消息这时只是待处理状态
- 消息中间件入库消息
- 消息中间件返回结果:消息入库的结果
- 业务操作:上面消息入库成功才继续做业务操作
- 发送业务操作结果给消息中间件:靠补偿机制保证成功
- 更改存储中消息状态:业务成功将消息状态改为待发送,否则直接删除。靠补偿机制保证成功
上面的6个流程基本能保证业务操作和消息发送的一致性,除了两种情况:在第5步或第6步时失败,可能会导致业务处理成功了,但是消息状态没更新从而没发送出去。针对这种情况的我们可以做个补偿机制,做同步重试机制来保证一致性。
消息模型
在JMS中,有Queue(点对点)和Topic(发布/订阅)两种模型。Queue模型是将消息都放到一个统一队列中,最终只有一个应用会去消费这个消息,所以也成为PTP(点对点)方式。而Topic模型与Queue模型最大的不同在于消息的接收部分,只要订阅了这个消息的应用,都能接收到这个消息。
在上面的两种消息模型中,是否接收都是针对每台机器与消息中间件的连接来判断的。在大型互联网场景中,我们的应用都是分布式部署的,每台机器都会有连接,而我们的需求是消息发布者发出消息后,订阅了这个消息的应用,每个应用集群只会接收到一条消息。这里我们可以对模型增加clusterId,每个应用有自己的clusterId,一个clusterId只会接收到一条消息。
可靠性
我们在消息发送时已经让消息入库,保证了发送的成功,但是接收端也是需要保证可靠。对于消息订阅分为持久订阅和非持久订阅,非持久订阅就是订阅者正常运行时接收消息正常,但是当订阅者是非运行状态时,如重启等场景,这时如果有消息下发是会失败的,消息会直接丢弃。但如果是持久订阅方式,消息则会保留,等待订阅者下次启动后再投递给接收者。因此要保证可靠的话,需要选择持久订阅方式。
消息从发送端到接收端共分为三个阶段:发送者将消息发送到消息中间件;消息中间件将消息存储;消息中间件把消息投递给消费者。要保证这三个阶段都是可靠的,才能保证整个消息是可靠的。
消息发送端的可靠性
消息发送端的可靠性保证比较容易,应用将消息发送给消息中间件后,应用应接收到消息中间件返回的成功结果才算可靠。
消息存储的可靠性
消息存储的可靠性基本通过落地来保证,直接采用现有的存储系统,如关系型数据库、分布式文件系统、NoSQL数据库等,这个可以根据自己的场景需求来选择。
存储的消息一般会包括下面一些信息:
消息体存储:
- 消息id:消息的唯一标识
- 创建时间
- 自定义属性
- 发送者clusterId:上面介绍过,发送应用的集群标识
消息投递:
- 唯一id
- 消息id
- 接受者clusterId:接收应用的集群标识
- 投递次数
- 下次投递时间
消息投递的可靠性
这一步骤和消息发送类似,通过请求响应来判断,接受者接收成功后响应给消息中间件后,消息才能删除。
投递的时候需要使用多线程,接收消息的工作放在一个线程池,处理消息接收结果放在另一个线程池,防止业务处理结果太慢把投递线程池堵死。
扩展性
上面已经介绍了消息中间件的核心完整流程,除了核心流程外,我们还需要考虑一些扩展功能。
消息优先级
可以针对消息进行设置优先级,投递的时候根据优先级来确定投递顺序。
自定义属性
自定义属性由业务系统来使用,业务可以往自定义属性里面放一些业务的东西,订阅者可以从自定义属性中拿出来使用。
另外消息的投递还分为PUSH和PULL两种方式,这两个各有优缺点,可以根据不同的场景去决定最终使用哪种方式投递消息。