对同一条消息构造三种真实的消费方式,让committed offset的推进时机和业务处理完成的时机真实呈现不同的先后关系,看三种投递语义具体差在哪一步。
命令 · python c1 = Consumer({..., "enable.auto.commit": True, "auto.commit.interval.ms": 200}) m = c1.poll() # 读到 evt-0 time.sleep(0.5) # 真实等后台线程把offset自动提交上去 # 模拟:业务处理在这里崩溃,从未真正执行
consumer读到: evt-0 等待500ms(auto.commit.interval.ms=200,真实offset这时候已经被自动提交了) 模拟:这条消息真实的业务处理在这时候崩溃,从未真正执行 重启后查committed offset: 1 ——已经推过去了,这条消息的业务效果真实永远丢失 重启后从committed offset继续读,读到的是下一条: evt-1 (第一条evt-0真实被跳过了)
自动提交是一个跟"业务处理有没有完成"完全脱钩的定时器,它只看"这条消息有没有被poll过",200毫秒到了就真实把offset推上去,不管你的代码是不是真的处理完了。一旦处理逻辑在提交之后、真正生效之前崩溃,重启后消费者会真实认为evt-0已经处理过,直接跳到evt-1——业务效果最多发生一次(可能是0次),这就是at-most-once名字的由来。
命令 · python c2 = Consumer({..., "enable.auto.commit": False}) m = c2.poll() effects.append(m.value()) # 业务处理真实已经生效 # 模拟:commit()调用之前进程真实崩溃,没有commit
真实完成业务处理: ['evt-0'] (处理已经真实生效,但commit还没调用) 模拟:进程在调用commit()之前真实崩溃 重启后重新消费(offset真实没推进过): evt-0 真实业务效果被执行了两次: ['evt-0', 'evt-0'] ——这就是at-least-once:不丢,但可能重复
手动提交把顺序反过来:先真实完成业务处理,处理完再commit。这样即使中途崩溃,最坏结果是offset没推进,下次重启会真实重新拿到同一条消息再处理一次——不会丢,但真实可能重复。这也是"at-least-once"里"至少"两个字真实的含义:业务效果真实发生了1次或者更多次,从不会是0次。
命令 · python p3.begin_transaction() m3 = c3.poll() effects.append(m3.value()) # 第一次真实处理,仅内存中 p3.abort_transaction() # 模拟:commit之前崩溃,直接中止 p3b.begin_transaction() m3b = c3b.poll() # offset没推进,读到同一条 effects.append(m3b.value()) # 第二次真实处理 p3b.send_offsets_to_transaction(...) p3b.commit_transaction() # 这次真实提交成功
真实完成业务处理(仅内存中,未提交): ['evt-0'] 模拟:进程在commit_transaction()之前真实崩溃(直接不提交,让事务真实中止) 重启后重新消费(offset真实没推进过,因为事务真实回滚了): evt-0 这次真实提交成功。业务处理真实只被最终确认了一次,即便中途真实重试过一轮: 真实处理过程中出现过的原始值: ['evt-0', 'evt-0'] (有2次尝试) 但只有最后一次真实被事务提交确认,等价于exactly-once的最终效果
每一步都是本文上面三个场景真实执行得到的原始结果。
三种语义的真实差别全部落在"offset提交"和"业务处理"这两件事真实的先后顺序与绑定关系上:提交早于处理完成,可能丢(at-most-once);处理完成早于提交,可能重复(at-least-once);把两者用事务绑成同一个原子单元,重试造成的物理重复会被回滚掉,下游最终只看到一份被确认的结果(exactly-once)。没有任何一种语义真实改变了"网络和进程都可能在任意时刻失败"这个前提,它们只是决定了失败发生时,你会承受丢失还是重复。