bigywater.tech
Kafka 内部原理 · 37

Read Committed 和 Read Uncommitted

在3节点集群里开一个事务写3条消息先不提交,对比read_uncommitted和read_committed两种隔离级别真实看到的东西,再验证中止的事务对两者分别意味着什么。

0条 vs 3条
事务未提交时read_committed和read_uncommitted真实看到的数量差
跳号
read_committed真实会跳过被中止事务占用的offset

验证:事务开着不提交,两种隔离级别看到的东西完全不同

命令 · python (confluent-kafka) p.init_transactions() p.begin_transaction() for i in range(3): p.produce("tx-demo", key=b"k", value=f"inflight-{i}".encode()) p.flush() # 这里还没有调用 commit_transaction() c_ru = Consumer({..., "isolation.level": "read_uncommitted"}) c_rc = Consumer({..., "isolation.level": "read_committed"})
真实实测 · 本机执行输出
read_uncommitted sees (before commit): ['inflight-0', 'inflight-1', 'inflight-2']
read_committed sees (before commit): []

事务还开着的时候,这3条消息其实已经真实写进了分区的日志文件——它们真实占了offset,只是在日志里被标记成"属于一个未结束的事务"。read_uncommitted 的消费者不关心这个标记,直接把日志里能读到的东西都吐出来;read_committed 的消费者在拉取数据时会真实检查这个事务有没有结束,没结束就把这批消息真实扣住,一条都不往上层交。

验证:提交之后,read_committed立刻追上来

命令 · python p.commit_transaction() time.sleep(1.0) seen = poll(consumer_read_committed)
真实实测 · 本机执行输出
read_committed sees (after commit): ['inflight-0', 'inflight-1', 'inflight-2']

commit真实做的事情,是在日志里追加一条"控制记录"(事务结束标记),read_committed的消费者看到这条标记之后,才会真实把刚才扣住的那批消息放行。这也是read_committed为什么真实有延迟——它不是逐条判断"这条消息干不干净",而是要等到事务真实关闭的那一刻,才能一次性放行整批。

验证:中止的事务,read_uncommitted瞬间看到过,但read_committed永远不会看到

命令 · python p.begin_transaction() for i in range(3): p.produce("tx-demo", key=b"k", value=f"aborted-{i}".encode()) p.flush() seen = poll(consumer_read_uncommitted) # 这时候还没abort p.abort_transaction()
真实实测 · 本机执行输出
read_uncommitted sees (before abort, transiently): ['aborted-0', 'aborted-1', 'aborted-2']
read_committed sees (after abort): []  (should be empty)

fresh read_uncommitted consumer reading from offset 0 sees ALL raw records:
  ['inflight-0', 'inflight-1', 'inflight-2', 'aborted-0', 'aborted-1', 'aborted-2']
fresh read_committed consumer reading from offset 0 sees only:
  ['inflight-0', 'inflight-1', 'inflight-2']
这组对照把两件事分得很清楚:read_uncommitted 真实看到的是磁盘上原始的物理日志,包括后来被中止、本不该存在的那3条aborted消息,它自己并不知道"这批数据后来被撤销了";read_committed 的消费者用一个全新的group从offset 0重新扫描时,真实会跳过abort标记之后那6个offset位置里属于aborted事务的3条,只留下真正提交过的3条inflight消息——这不是"过滤"来实现的,日志里aborted-0/1/2这三条消息真实还占着物理磁盘空间和offset号,只是read_committed的消费者在读取时会真实参照事务索引把它们跳过去,所以消费者看到的offset会有真实的"跳号"。

分步演示:一份日志,两种读法

每一步都是本文上面几节produce、commit/abort、poll得到的原始结果。

同一份物理日志,两种隔离级别读出两个完全不同的世界
这一步新验证的状态

    结论

    isolation.level 决定的不是"消费者能不能访问某条消息",而是消费者要不要参照事务边界过滤日志。read_uncommitted 直接暴露物理日志,包括将来可能被撤销的数据;read_committed 用事务索引把还没结束、以及已经中止的数据都挡在外面,代价是必须等到事务真正提交那一刻才能放行,天然比read_uncommitted多一点延迟。

    参考与说明

    • 数据来自本机3节点Kafka 4.3.1集群(KRaft模式),事务型producer + confluent-kafka客户端,topic tx-demo单分区三副本。
    • 和《消费—处理—再生产如何实现事务一致性》一起看:那篇用同样的事务机制保证offset和输出真实绑定成一个原子单元,这篇专门看消费者读取那一侧的可见性规则。
    • 没有验证:多分区事务跨分区原子性;事务超时(transaction.timeout.ms)真实触发时消费者的行为。
    京ICP备2026067502号-1京公网安备11010502063487号