在分布式消息系统中,消息的传输担保有两个层面的含义,一是对消息发送与接收的担保,二是对消息成功处理的担保。我们常说的at most once, at least onceexactly once在这两层中意义是不同的。

  • at most once
    如果只论消息的收发,那么消息只会被投递一次而不管能否被consumer收到。如果考虑消息的处理,那么这条消息可能会因为没有收到而未处理,也可能因为收到以后崩溃而未处理。
  • at least once
    只论消息收发的情况下,消息可能会被重复投递。考虑消息处理时,则消息可能会被成功处理一次后,因为其它 consumer 再次收到消息而被重复处理。
  • exactly once
    只论消息接收时,消息只会被成功收发一次。考虑消息处理时,则保证消息只会被成功处理一次。

对于kafka来说,它默认保证的是消息收发层面 at least once,即如果consumerA没有收到消息,则kafka会重发消息。但是对于开发者而言,只收到消息是毫无意义的,必须对消息进行处理。因此考虑消息处理的成功与否才是开发者最关心的问题。 下面只讨论这种情况下的消息传输担保。

由于kafka在客户端保存offset的机制,开发者可以自己实现对at most onceexactly once的保证,这取决于 consumer 提交 offset 的时机

consumer对消息的处理可以分成三步:读取消息、处理消息和提交offset。如果consumer读取消息后,先提交offset,再处理消息,那么如果提交offset后崩溃没有来得及处理消息,这时因为offset已经提交所以其它consumer不会再次收到消息,这就是at most once的实现。如果读取消息后,先处理消息,再提交 offset , 此时若处理完消息后崩溃,那么由于offset还没提交,其它consumer会再次收到此消息进行处理,这就是at least once的实现。实际上开发者最想要的是exactly once, 我们有两种方式来实现只有一次的保证。

第一种是使用两阶段提交。即消息处理与提交offset看作一个事务,通过引入协调者实现两阶段提交协议。

第二种比较简单,是将consumer处理的结果与offset值输出到同一存储结构中。举个例子,consumer A收到扣钱的消息,那么它对消息的处理结果就是 update 数据库将余额减少。在 update 余额的同时再在当前表中写入offset值。这种方式最关键的一点是,要让Kafka在你指定的数据库中保存offset值(Kafka提供的 low level API可以做到这一点,而 Hight level API只能将offset存放到zookeepr中)。这样一来,当kafka检查offset时会读你的库,而你在处理消息时已经更新了offset, 这样消息处理跟提交offset就相当于是一件事了。