kafka和rocket消费机制差异

15 阅读6分钟

既然之前的总结“过于简略”,那我们这次来一次深度解剖。我们将从数据结构、底层交互流程、Offset 管理机制这几个维度,把 Kafka 和 RocketMQ 在“消息处理失败”这一场景下的表现差异彻底讲透。


核心场景设定

  • Topic: OrderTopic
  • Partition: 0
  • 消息流:
    • Offset 100: 订单 A(处理失败,数据库死锁)
    • Offset 101: 订单 B(处理成功

一、Kafka:流式日志(The Log)

Kafka 的设计哲学是 “哑代理,智客户端” (Dumb Broker, Smart Client)。Broker 只是一个负责追加写文件的系统,它根本不关心消息内容,也不关心单条消息的状态。

1. 只有“进度”,没有“状态”

Kafka 维护消费进度的方式非常粗犷。它不在每条消息上打勾,它只维护一个 High Watermark(水位线)Committed Offset

  • Kafka 的逻辑:Consumer 告诉我它提交了 101,那我就认为 0 ~ 101 之间的所有数据全完了。

2. 详细执行流程(Offset 100 失败,101 成功)

  1. 拉取 (Fetch):

    • Consumer 向 Broker 拉取一批消息(Batch),比如一次拉了 100, 101, 102 三条。
    • 这三条消息现在都在 Consumer 的内存里。
  2. 处理 Offset 100 (失败):

    • Consumer 执行业务逻辑,报错。
    • 关键点:Broker 对此一无所知。Broker 也没有“把 100 标记为失败”这种接口。
    • 代码选择:如果你的代码 catch 了错误并 continue,Consumer 内存里的游标指向了下一条。
  3. 处理 Offset 101 (成功):

    • Consumer 执行业务逻辑,成功。
    • 代码动作session.MarkMessage(101)。此时 Sarama 客户端在本地记录:current_offset = 101
  4. 提交 (Commit):

    • Sarama 后台协程周期性启动(默认 1秒)。
    • 它发现本地 current_offset = 101
    • 它向 Broker 发送请求:GroupCommit(Topic, Partition0, Offset=102) (意为下次从 102 开始)。
  5. 结果 (后果):

    • Broker 更新元数据:该组消费进度 = 102。
    • Offset 100 彻底丢失。下次重启、Rebalance,都会从 102 开始。

3. Kafka 如何救赎?(只能靠开发者硬写)

如果你不能丢消息,在 Kafka 里你必须在代码层面阻塞:

  • 死循环重试:在处理 100 失败时,while(true) 一直重试。这会导致 101 即使已经到了内存,也无法被处理(Head-of-line blocking,队头阻塞)。
  • 手动死信队列:自己写代码把 100 发送到 Topic_DLQ,然后标记 100 成功,继续处理 101。

二、RocketMQ:消息队列(The Queue)

RocketMQ(普通/并发模式)的设计哲学是 “业务消息引擎”。Broker 非常“聪明”,它知道每一条消息的状态,并且内置了定时器和重试队列。

1. 既有“进度”,又有“单条 Ack”

RocketMQ 在 Broker 端维护了一个逻辑上的消费进度,但它允许“空洞”。更重要的是,它引入了 “服务端重试” 机制。

2. 详细执行流程(Offset 100 失败,101 成功)

  1. 拉取 (Pull):

    • Consumer 拉取消息 100, 101
  2. 处理 Offset 100 (失败):

    • Consumer 执行业务逻辑,报错。
    • 代码动作:Consumer 返回 ConsumeConcurrentlyStatus.RECONSUME_LATER
    • 幕后黑科技 (ACK 阶段)
      • Consumer 客户端自动把这条消息 100 发回给 Broker
      • Broker 收到后,把消息 100 存入一个特殊的 Topic:%RETRY%ConsumerGroup
      • 原队列(OrderTopic)的进度,此时实际上已经向前推进了。
  3. 处理 Offset 101 (成功):

    • Consumer 执行业务逻辑,成功。
    • 代码动作:Consumer 返回 ConsumeConcurrentlyStatus.CONSUME_SUCCESS
    • Broker 收到 ACK,确认 101 完成。
  4. 重试调度 (The Magic):

    • 那条被扔进 %RETRY% 的 100 号消息,Broker 会根据重试次数设置一个定时器(例如 level 1 = 10秒)。
    • 10秒后:Broker 把这条消息再次投递给 Consumer。
    • Consumer 再次收到消息(MessageID 不变),此时可以再次尝试处理。
  5. 结果:

    • 101 先处理完
    • 100 稍后处理
    • 没有丢消息,但是消息乱序了

三、深度对比总结表

这个表格展示了两者最本质的区别:

特性KafkaRocketMQ (并发模式)
数据模型流 (Stream)
像录像带,连续读取。
队列 (Queue)
像任务列表,单据独立处理。
Offset 提交粒度累计位点 (Cumulative)
提交 N,代表 N 之前全完了。
单条 ACK (Individual)
虽然也维护 Offset,但配合重试队列实现了单条管理的错觉。
中间消息失败 (100失败)阻塞或跳过
要么死循环卡住全部分区,要么标记已消费直接丢弃。
服务端暂存 (Re-queue)
失败的消息被“踢”出原队列,放入重试队列,后续消息继续跑。
对 Offset 101 的影响如果 100 卡住,101 永远轮不到处理(队头阻塞)。101 可以先处理成功,完全不受 100 影响。
顺序性严格有序
只要分区不变,处理顺序永远是 100 -> 101。
乱序
最终执行顺序可能是 101 -> 100(重试)。
重试成本
需客户端自研(死循环、DB记录、Redis记录)。

框架自带,改个返回值即可。
适用场景日志、埋点、流计算、CDC (Change Data Capture)。交易、订单、扣款、通知等核心业务。

四、直观图解

Kafka 场景 (Offset 100 失败)

[Broker Log]  :  ... [99] [100] [101] [102] ...
                         ^     ^
[Consumer]    :    处理失败   处理成功
                         |     |
[Action]      :   (Skip?)  Mark(101)
                         |     |
[Commit]      :          V     V
[Broker Offset]:  Updated to 102 (100 is GONE)

RocketMQ 场景 (Offset 100 失败)

[Broker Queue]:  ... [99] [100] [101] [102] ...
                         |     |
[Consumer]    :   Ret_Later  Ret_Success
                         |     |
[Internal Op] :   Send back    |
                  to RetryQ    |
                         |     |
[Broker State]:  Offset moves past 101
                 [RetryQueue]: [100 (Delay 10s)]
                         .
                         . (10s later)
                         .
[Consumer]    :  Recieve [100] again -> Process

五、结论与建议

  • 结论:你用 Sarama (Kafka) 写代码时,千万不能带着 RocketMQ 的思维。在 Kafka 里,“提交 Offset”是一个不可逆的“批量确认”操作
  • 针对你当前代码的建议: 由于 Kafka 原生不支持“单条失败重试”,为了数据不丢,你通常只有两个选择:
    1. 强一致性(不能丢,不能乱):在 ConsumeClaim 里遇到错误使用 retry 库进行指数退避重试(Backoff Retry),一直重试直到成功。这会阻塞该分区后续消息的消费。
    2. 最终一致性(不能丢,允许乱):遇到错误,把消息手动生产到另一个 Topic(如 topic_retry),然后对当前消息 MarkMessage。你自己还需要写一个 Consumer 去消费 topic_retry。这其实就是人工实现了 RocketMQ 的功能。