使用场景:在分布式系统之间,通过发送和接受消息来解耦服务,分摊单个应用同时调用多个服务的压力。

如何确保消息发送成功

先来看下传统的做法

1
2
3
4
5
6
7
8
9
void foo1() {
// 业务操作:写数据库,调用服务等
// 发送消息给中间件
}
// or
void foo2() {
// 发送消息给中间件
// 业务操作:写数据库,调用服务等
}

正常情况下,这两种方式出现不一致的概率可能并不大,但一旦出现比如 foo1() 消息服务器挂了等情况,那么消息就发送不出去了;或者 foo2() 消息成功,业务挂了,那么数据就不一致了。

有其他办法吗?

使用 JMS 的 XA 系列接口,通过 XA 接口对分布式事务的支持,来保证业务操作和发送消息的一致性。有两个缺点:

  1. 分布式事务性能开销大。
  2. 业务操作必须支持 XA 协议才能与发送消息一起来做分布式事务。

能不能综合一下?

1
2
3
4
5
6
7
void foo3() {
// 发送消息给中间件,带个标记为待处理,并返回消息存储结果(成功/失败)
// if(消息中间件返回 false) { 放弃业务处理,结束 }
// 业务操作:写数据库,调用服务等,并返回业务接口(成功/失败)
// 发送业务结果给消息中间件
// (消息中间件收到业务结果:if(业务成功){ 更新消息状态为可发送,并进行调度 } else { 删除消息,结束 })
}

foo3() 比 foo2() 多增加了一次消息网络请求和状态更新操作,整体上额外的开销并不大。不过消息中间件与业务之间的依赖比较严重。

如何解决消息中间件与使用者的强依赖关系

可以考虑将本地磁盘作为一个消息存储的容器,通过应用内的消息中间件客户端,将应用内的消息写入本地磁盘,应为都在应用内,可以做成一个本地事务,保证应用内的业务和消息发送一定是成功的。

本地磁盘消息写入成功后,消息中间件客户端就可以通过(轮询本地消息等方式)控制将消息向服务端发送。

有没有问题?

当消息中间件挂了,并且本地磁盘数据也坏了的情况下,消息就丢失了。这种情况,只有在业务上进行消息的补发才是最彻底的容灾手段。

消息存储的几种方式

  • 实现基于文件的消息存储
  • 采用数据库进行消息存储
  • 基于双击内存的消息存储

如何确保消息投递成功

投递是指:消息中间件服务端将消息发送给消息消费者。

当消息消费者显式的告诉中间件消费消费成功时,中间件才能确保消息投递成功了,从而删除消息。

重复消息的产生和方案

  1. 应用端消息的重复发送

如果中间件消息存储失败,应用重复发送,则是正常行为。

当中间件消息存储成功,但此时中间件服务器挂了,或者返回结果时网络出现故障,或者负载高导致响应超时,都会导致应用端收不到成功的通知,进而产生重复发送。

这种情况下一个解决办法是重试发消息时,使用同一个消息 id,而不用在消息中间件端产生消息 id。

  1. 消息到达了消息存储,由中间件向外投递时,产生重复。

消息消费者处理完毕后,自身的应用出问题了,或者网络出问题了,或者处理时间较长,导致返回超时,都会导致消息中间件收不到消费结果,进而重复投递。

也有可能消息中间件收到结果了,但是自身挂了,或者消息状态更新时出故障了,也会导致重复投递。

这种情况,可以让消息消费者来处理,即要求消费者对消息的处理的幂等的。