Kafka 进阶学习(十五)—— 事务

260 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第14天,点击查看活动详情

前言

今天是我 Kafka 学习的第 15 天,今天我学习的内容是 Kafka 的事务。

下面开始我们今天的学习之旅!

消息保障传输

事务简单的来说就是保证所有原子操作要么全部成功,要么全部失败。在 Kafka 中体现的就是消息的传输。我们先来看一下消息传输保证层级。

一般而言,消息中间件的消息传输保障有 3 个层级,分别如下:

  1. at most once:至多一次。消息可能会丢失,但绝对不会重复传输;
  2. at least once:最少一次。消息绝不会丢失,但可能会重复传输;
  3. exactly once:恰好一次。每条消息肯定会被传输一次且仅传输一次。

Kafka 的消息传输保障机制非常直观。当生产者向 Kafka 发送消息时,一旦消息被成功提交到日志文件,由于多副本机制的存在,这条消息就不会丢失。如果生产者发送消息到 Kafka 之后,遇到了网络问题而造成通信中断,那么生产者就无法判断该消息是否已经提交。虽然 Kafka 无法确定网络故障期间发生了什么,但生产者可以进行多次重试来确保消息已经写入 Kafka,这个重试的过程中有可能会造成消息的重复写入,所以这里 Kafka 提供的消息传输保障为·at least once

对消费者而言,消费者处理消息和提交消费位移的顺序在很大程度上决定了消费者提供哪一种消息传输保障:

  • 如果消费者在拉取完消息之后,应用逻辑 先处理消息后提交消费位移,那么在消息处理之后且在位移提交之前消费者宕机了,待它重新上线之后,会从上一次位移提交的位置拉取,这样就出现了重复消费,因为有部分消息已经处理过了只是还没来得及提交消费位移,此时就对应 at least once。
  • 如果消费者在拉完消息之后,应用逻辑 先提交消费位移后进行消息处理,那么在位移提交之后且在消息处理完成之前消费者宕机了,待它重新上线之后,会从已经提交的位移处开始重新消费,但之前尚有部分消息未进行消费,如此就会发生消息丢失,此时就对应 at most once。

如果要想要需要实现 exactly once,则需要幂等和事务两个特性。

幂等

所谓的幂等,简单地说就是对接口的多次调用所产生的结果和调用一次是一致的。生产者在进行重试的时候有可能会重复写入消息,而使用 Kafka 的幂等性功能之后就可以避免这种情况。

开启幂等性功能的方式很简单,只需要显式地将生产者客户端参数 enable.idempotence 设置为 true 即可(这个参数的默认值为 false)。

为了实现生产者的幂等性,Kafka 为此引入了 producer id(以下简称 PID)和序列号(sequence number)这两个概念,每个新的生产者实例在初始化的时候都会被分配一个 PID,这个 PID 对用户而言是完全透明的。对于每个 PID,消息发送到的每一个分区都有对应的序列号,这些序列号从 0 开始单调递增。生产者每发送一条消息就会将 <PID,分区> 对应的序列号的值加 1。

broker 端会在内存中为每一对 <PID,分区> 维护一个序列号。对于收到的每一条消息,只有当它的序列号的值(SN_new)比 broker 端中维护的对应的序列号的值(SN_old)大 1(即SN_new=SN_old+1)时,broker 才会接收它。如果 SN_new<SN_old+1,那么说明消息被重复写入,broker 可以直接将其丢弃。如果 SN_new>SN_old+1,那么说明中间有数据尚未写入,出现了乱序,暗示可能有消息丢失,对应的生产者会抛出OutOfOrderSequenceException,这个异常是一个非常严重的异常。

注意,引入序列号来实现幂等也只是针对每一对<PID,分区>而言的,也就是说,Kafka 的幂等只能保证单个生产者会话(session)中单分区的幂等。

事务

幂等性并不能跨多个分区运作,而事务可以弥补这个缺陷。事务可以保证对多个分区写入操作的原子性。操作的原子性是指多个操作要么全部成功,要么全部失败,不存在部分成功、部分失败的可能。

对流式应用(Stream Processing Applications)而言,一个典型的应用模式为“consume-transform-produce”。在这种模式下消费和生产并存:应用程序从某个主题中消费消息,然后经过一系列转换后写入另一个主题,消费者可能在提交消费位移的过程中出现问题而导致重复消费,也有可能生产者重复生产消息。Kafka 中的事务可以使应用程序将消费消息、生产消息、提交消费位移当作原子操作来处理,同时成功或失败,即使该生产或消费会跨多个分区。

为了实现事务,应用程序必须提供唯一的 transactionalId,这个 transactionalId 通过客户端参数 transactional.id 来显式设置。事务要求生产者开启幂等特性,因此通过将 transactional.id 参数设置为非空从而开启事务特性的同时需要将 enable.idempotence 设置为 true。

参考文档

  • 《深入理解 Kafka:核心设计与实践原理》—— 朱忠华

往期文章