在3节点集群里开一个事务写3条消息先不提交,对比read_uncommitted和read_committed两种隔离级别真实看到的东西,再验证中止的事务对两者分别意味着什么。
命令 · 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 的消费者在拉取数据时会真实检查这个事务有没有结束,没结束就把这批消息真实扣住,一条都不往上层交。
命令 · 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为什么真实有延迟——它不是逐条判断"这条消息干不干净",而是要等到事务真实关闭的那一刻,才能一次性放行整批。
命令 · 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']
每一步都是本文上面几节produce、commit/abort、poll得到的原始结果。
isolation.level 决定的不是"消费者能不能访问某条消息",而是消费者要不要参照事务边界过滤日志。read_uncommitted 直接暴露物理日志,包括将来可能被撤销的数据;read_committed 用事务索引把还没结束、以及已经中止的数据都挡在外面,代价是必须等到事务真正提交那一刻才能放行,天然比read_uncommitted多一点延迟。