生产者消息可靠性
1. 生产者可靠性机制
1.1 消息确认机制-ACK
producer提供了三种消息确认的模式,通过配置 acks
来实现不同级别的可靠性保证。
ACK配置选项
flowchart TD
A[Producer发送消息] --> B{acks配置}
B -->|acks=0| C[不等待确认]
B -->|acks=1| D[等待Leader确认]
B -->|acks=-1/all| E[等待Leader+ISR确认]
C --> F[效率最高<br/>可靠性最低<br/>可能丢数据]
D --> G[中等效率<br/>中等可靠性<br/>Leader宕机可能丢数据]
E --> H[效率最低<br/>可靠性最高<br/>数据不会丢失]
subgraph "副本同步状态"
I[AR = ISR + OSR]
J[ISR: 同步副本集合]
K[OSR: 非同步副本集合]
end
详细说明:
-
acks=0
:表示生产者将数据发送出去就不管了,不等待任何返回。这种情况下数据传输效率最高,但是数据可靠性最低,当server挂掉的时候就会丢数据。 -
acks=1
(默认):表示数据发送到Kafka后,经过leader成功接收消息的确认,才算发送成功。如果leader宕机了,就会丢失数据。 -
acks=-1/all
:表示生产者需要等待ISR中的所有follower都确认接收到数据后才算发送完成,这样数据不会丢失,因此可靠性最高,性能最低。
数据完全可靠条件
flowchart LR
subgraph "副本集合关系"
E[AR全部副本]
F[ISR同步副本]
G[OSR非同步副本]
E --> H[AR = ISR + OSR]
F --> I[正常情况: AR = ISR, OSR = null]
end
flowchart LR
A[数据完全可靠] --> B[ACK级别 = -1]
A --> C[分区副本 >= 2]
A --> D[ISR最小副本数 >= 2]
数据完全可靠条件 = ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2
副本集合说明:
- AR (All Replicas):所有副本集合
- ISR (In-Sync Replicas):在指定时间内和leader保存数据同步的集合
- OSR (Out-Sync Replicas):不能在指定的时间内和leader保持数据同步集合
配置示例
// ACK 设置
properties.put(ProducerConfig.ACKS_CONFIG, "1");
// 重试次数,默认的重试次数是 Integer.MAX_VALUE
properties.put(ProducerConfig.RETRIES_CONFIG, 3);
1.2 数据去重-幂等性
1.2.1 幂等性原理
在一般的MQ模型中,常有以下的消息通信概念:
flowchart TD
A[消息传递语义] --> B["At Least Once<br/>至少一次"]
A --> C["At Most Once<br/>最多一次"]
A --> D["Exactly Once<br/>精确一次"]
B --> E["ACK=-1 + 副本>=2 + ISR>=2<br/>保证不丢失,可能重复"]
C --> F["ACK=0<br/>保证不重复,可能丢失"]
D --> G["至少一次 + 幂等性<br/>既不丢失也不重复"]
subgraph K11["Kafka 0.11版本特性"]
H[幂等性]
I[事务]
end
D --> H
D --> I
幂等性定义:对接口的多次调用所产生的结果和调用一次是一致的。生产者在进行重试的时候有可能会重复写入消息,而使用Kafka的幂等性功能之后就可以避免这种情况。
1.2.2 幂等性实现机制
sequenceDiagram
participant P as Producer
participant B as Broker
Note over P,B: 幂等性判断机制
P->>B: 消息1 <PID=1, Partition=0, SeqNum=1>
B->>B: 内存序列号=0, 收到序列号=1 (0+1=1) ✓
B-->>P: 存储消息,更新内存序列号=1
P->>B: 消息2 <PID=1, Partition=0, SeqNum=1> (重复)
B->>B: 内存序列号=1, 收到序列号=1 (<1) ✗
B-->>P: 丢弃重复消息,返回成功
P->>B: 消息3 <PID=1, Partition=0, SeqNum=5> (跳跃)
B->>B: 内存序列号=1, 收到序列号=5 (>>1) ✗
B-->>P: 抛出异常-消息丢失
重复数据的判断标准:具有 <PID, Partition, SeqNumber>
相同主键的消息提交时,Broker只会持久化一条。
- ProducerId(PID):Kafka每次重启都会分配一个新的
- Partition:分区号
- Sequence Number:序列化号,单调自增
幂等性判断逻辑:
- 收到序列号 = 内存序列号 + 1:存储消息
- 收到序列号 < 内存序列号:丢弃重复消息
- 收到序列号 >> 内存序列号:抛出异常,消息丢失
重要限制:幂等性只能保证在单分区单会话内不重复。
1.2.3 如何使用幂等性
开启幂等性功能的方式很简单,只需要显式地将生产者客户端参数 enable.idempotence
设置为true即可(这个参数的默认值为true),并且还需要确保生产者客户端的retries、acks、max.in.flight.requests.per.connection参数不被配置错,默认值就是对的。
// 开启幂等性(默认已开启)
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
1.3 消息事务
1.3.1 事务机制原理
由于幂等性不能跨分区运作,为了保证同时发的多条消息,要么全成功,要么全失败,kafka引入了事务的概念。
sequenceDiagram
participant P as Producer
participant TC as Transaction Coordinator
participant B1 as Broker1 (Partition0)
participant B2 as Broker2 (Partition1)
Note over P,B2: 事务处理流程
P->>TC: 1. initTransactions()
TC-->>P: 分配PID和Epoch
P->>TC: 2. beginTransaction()
TC-->>P: 开始事务
P->>B1: 3. 发送消息到Partition0
P->>B2: 3. 发送消息到Partition1
B1-->>P: 消息写入成功
B2-->>P: 消息写入成功
alt 正常提交
P->>TC: 4. commitTransaction()
TC->>B1: 写入事务提交标记
TC->>B2: 写入事务提交标记
TC-->>P: 事务提交成功
else 异常回滚
P->>TC: 4. abortTransaction()
TC->>B1: 写入事务回滚标记
TC->>B2: 写入事务回滚标记
TC-->>P: 事务回滚成功
end
开启事务的条件:producer 设置 transactional.id
的值并同时开启幂等性。
1.3.2 事务API
// 1 初始化事务
void initTransactions();
// 2 开启事务
void beginTransaction() throws ProducerFencedException;
// 3 在事务内提交已经消费的偏移量(主要用于消费者)
void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets, String consumerGroupId) throws ProducerFencedException;
// 4 提交事务
void commitTransaction() throws ProducerFencedException;
// 5 放弃事务(类似于回滚事务的操作)
void abortTransaction() throws ProducerFencedException;
1.3.3 事务使用示例
package com.luojia.kafka.product;
import com.luojia.kafka.partitioner.MyPartitioner;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
public class CustomProducerTransactions {
public static void main(String[] args) {
// 配置属性类
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartitioner.class.getName());
// Kafka事务消息,必须要指定事务ID,ID可以任意填写但必须全局唯一
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "transaction_id_01");
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
// 1.初始化事务
producer.initTransactions();
// 2.开始事务
producer.beginTransaction();
// 3.发送数据
try {
for (int i = 0; i < 5; i++) {
producer.send(new ProducerRecord<>("first", "kafka first transaction msg : " + i));
}
// 故意整一个异常,模拟事务回滚
// int i = 10/0;
// 4.提交事务
producer.commitTransaction();
} catch (Exception e) {
// 5.回滚事务
producer.abortTransaction();
} finally {
producer.close();
}
}
}
2. 消息的有序性
2.1 有序性基本概念
消息在单分区内有序,多分区内无序(如果对多分区进行排序,造成分区无法工作需要等待排序,浪费性能)。
flowchart LR
A[Producer] --> B[Topic]
subgraph SP["单分区有序保证"]
C[Partition 0]
C --> D["Message 1 - 时间戳: T1"]
D --> E["Message 2 - 时间戳: T2"]
E --> F["Message 3 - 时间戳: T3"]
G["Consumer读取顺序: 1→2→3 ✓"]
end
subgraph MP["多分区无序情况"]
H[Partition 1]
I[Partition 2]
H --> J["Msg A(T1), Msg C(T3)"]
I --> K["Msg B(T2), Msg D(T4)"]
L["Consumer读取可能顺序: A→B→C→D ✗"]
end
B --> C
B --> H
B --> I
kafka只能保证单分区下的消息顺序性,为了保证消息的顺序性,需要做到如下几点:
2.2 有序性配置要求
flowchart TD
A[消息有序性配置] --> B{是否开启幂等性?}
B -->|未开启幂等性| C[max.in.flight.requests.per.connection = 1]
B -->|开启幂等性| D[max.in.flight.requests.per.connection <= 5]
C --> E[缓冲队列最多放置1个请求<br/>严格保证顺序]
D --> F[Broker端缓存最多5个request<br/>利用序列号保证顺序]
subgraph "幂等性优势"
G[允许更高并发]
H[利用序列号排序]
I[性能更好]
end
D --> G
D --> H
D --> I
配置说明:
- 如果未开启幂等性:需要
max.in.flight.requests.per.connection
设置为1(缓冲队列最多放置1个请求) - 如果开启幂等性:需要
max.in.flight.requests.per.connection
设置为小于等于5
2.3 幂等性下的有序性保证机制
这是因为broker端会缓存producer主题分区下的五个request,保证最近5个request是有序的。
sequenceDiagram
participant P as Producer
participant B as Broker
Note over P,B: 幂等性下的有序性保证
P->>B: Request 1 (SeqNum=1)
P->>B: Request 2 (SeqNum=2)
P->>B: Request 3 (SeqNum=3) - 网络丢失
P->>B: Request 4 (SeqNum=4)
P->>B: Request 5 (SeqNum=5)
Note over B: Broker收到Request 1,2,4,5<br/>缓存Request 4,5等待Request 3
B-->>P: 确认 Request 1,2
P->>B: Request 3 (SeqNum=3) - 重发
Note over B: 收到Request 3,按序处理3,4,5
B-->>P: 确认 Request 3,4,5
rect rgb(200, 255, 200)
Note over P,B: 最终消息顺序: 1→2→3→4→5 ✓
end
2.4 有序性实现示例
示例场景:现在有5个请求,Request1、Request2、Request3、Request4、Request5,在向Kafka集群中发送的时候:
- Request1、Request2成功发送并确认
- Request3由于网络问题丢失,但Request4、Request5先到达
- Broker将Request4、Request5缓存起来,等待Request3
- Request3重发到达后,Broker按序处理Request3、Request4、Request5
- 因为是幂等的,所以每条消息都有自己的单调递增的序列号
- Broker最多缓存5条数据
flowchart LR
A[Request 1] --> B["✓ 成功发送"]
C[Request 2] --> D["✓ 成功发送"]
E[Request 3] --> F["✗ 网络丢失"]
G[Request 4] --> H["→ Broker缓存"]
I[Request 5] --> J["→ Broker缓存"]
F --> K[Request 3 重发]
K --> L["✓ 成功发送"]
subgraph BP["Broker处理顺序"]
M["1. 处理Request 1,2"]
N["2. 缓存Request 4,5"]
O["3. 等待Request 3"]
P["4. 收到Request 3后按序处理3,4,5"]
end
subgraph FR["最终结果"]
Q["消息顺序: 1→2→3→4→5"]
R["保证了单分区内的有序性"]
end
2.5 有序性配置建议
// 开启幂等性(推荐)
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
// 设置合适的并发请求数
properties.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 5);
// 如果对顺序要求极其严格,可以设置为1(性能较低)
// properties.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 1);
// 开启重试保证消息不丢失
properties.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
总结
Kafka生产者的消息可靠性机制通过多层保障确保数据的安全传输:
核心机制总结
flowchart LR
A["消息可靠性机制"] --> B["ACK确认机制"]
A --> C["幂等性机制"]
A --> D["事务机制"]
A --> E["有序性保证"]
B --> B1["acks=0 高效率低可靠"]
B --> B2["acks=1 平衡模式"]
B --> B3["acks=-1 高可靠低效率"]
B --> B4["完全可靠条件"]
C --> C1["PID+Partition+SeqNum"]
C --> C2["单分区单会话"]
C --> C3["防止重复消息"]
C --> C4["序列号判断"]
D --> D1["跨分区保证"]
D --> D2["原子性操作"]
D --> D3["事务协调器"]
D --> D4["全成功或全失败"]
E --> E1["单分区有序"]
E --> E2["多分区无序"]
E --> E3["幂等性配合"]
E --> E4["请求缓存机制"]
最佳实践建议
-
可靠性配置:
- 生产环境建议使用
acks=-1
- 开启幂等性
enable.idempotence=true
- 设置合理的重试次数
- 生产环境建议使用
-
有序性保证:
- 关键业务使用单分区发送
- 开启幂等性时设置
max.in.flight.requests.per.connection<=5
- 极端场景可设置为1但会影响性能
-
事务使用:
- 跨分区操作需要事务保证
- 设置全局唯一的
transactional.id
- 正确处理事务异常和回滚
-
性能平衡:
- 根据业务需求选择合适的可靠性级别
- 在可靠性和性能之间找到平衡点
- 监控和调优相关参数
通过合理配置这些机制,Kafka生产者能够在保证高可靠性的同时,提供良好的性能表现,满足各种业务场景的需求。