enable-auto-commit: false
如果生产者发完消息后,因为网络抖动,没有收到ack,但实际上broker已经收到了,此时生产会进行重试,于是broker就会收到多条相同的消息,而造成消费者重复消费
Kafka的幂等性就是为了避免出现生产者重试的时候出现重复写入消息的情况。
开启幂等性功能配置(该配置默认为false
)如下
prop.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true);
幂等性:多次访问的结果时一样的。对于rest的请求(get(幂等)、post(非幂等)、put(幂等),delete(幂等))
解决方法:可以针对消息生成md5等保存在mysql(唯一索引)或者redis里面,在处理之前去mysql或者redis里面判断是否已经消费过。这个也是幂等性的思想
默认情况下,消费完消息会提交offset给kafka,避免消费,
场景1:当consumer消费完消息,但是没有返回offset时,Consumser挂了,触发rebalance,则会出现重复消费
场景2:当一个消费组在消耗一个poll到的消息时,超过了设定的poll的间隔时间,则kafka会剔除此消费者,触发rebalance,导致Offset提交失败。Rebalance以后还是会从消费者之前的Offset处消费消息
解决方法:提高消费端的性男女,调整消息处理的超时时间,或者较少一次poll的条数。
保证消息按顺序发送给kafka,且消息不丢失
//保证幂等性、消息顺序性
props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, kafkaProducerProp.getMaxInFlightRequestsPerConnection());
必须是相同的主题,同一个主题,必须发送到同一个分区,并且并需保证Kafka中的消息是顺序的,因为同一个主题,同一个分区,一个消费组,只会有一个消费者消费消息,就可以实现顺序消费
kafka的顺序消费使用场景不多,因为牺牲了性能,但是rocketmq有专门的功能保证顺序性
消息的消费速度远赶不上生产者的生成消息的速度,随着没有被消费的数据堆积越来越多,消费的寻址性能会越来越差,最后导致整个kafka的对外服务性能降低,从而印象其他服务访问速度,造成雪崩。
kafka本身是没有延迟队列的功能的,RabbitMQ、RocketMQ有延迟队列的功能。
可以有一下的解决方案来实现延迟加载
1.在发送延迟消息时不直接发送到目标topic,而是发送到一个处理延迟消息的topic,例如delay-minutes-1
2.写一段代码拉取delay-minutes-1中的消息,将满足条件的的消息发送给真正的主题里
因为kafka配置“max.poll.interval.ms”则,如果不在设定的时间内处理完消息则视为消费者挂掉,会进行rebalance,KafaConsumer提供了暂停和恢复的API,调用暂停就无法拉取新的消息,同时长时间不消费也不会认为消费者挂掉
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.ExecutionException;
@SpringBootTest
public class DelayQueueTest {
private KafkaConsumer consumer;
private KafkaProducer producer;
private volatile Boolean exit = false;
private final Object lock = new Object();
private final String servers = "";
@BeforeEach
void initConsumer() {
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
props.put(ConsumerConfig.GROUP_ID_CONFIG, "d");
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, "5000");
consumer = new KafkaConsumer<>(props, new StringDeserializer(), new StringDeserializer());
}
@BeforeEach
void initProducer() {
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
producer = new KafkaProducer<>(props);
}
@Test
void testDelayQueue() throws JsonProcessingException, InterruptedException {
String topic = "delay-minutes-1";
List topics = Collections.singletonList(topic);
consumer.subscribe(topics);
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (lock) {
consumer.resume(consumer.paused());
lock.notify();
}
}
}, 0, 1000);
do {
synchronized (lock) {
ConsumerRecords consumerRecords = consumer.poll(Duration.ofMillis(200));
if (consumerRecords.isEmpty()) {
lock.wait();
continue;
}
boolean timed = false;
for (ConsumerRecord consumerRecord : consumerRecords) {
long timestamp = consumerRecord.timestamp();
TopicPartition topicPartition = new TopicPartition(consumerRecord.topic(), consumerRecord.partition());
if (timestamp + 60 * 1000 < System.currentTimeMillis()) {
String value = consumerRecord.value();
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(value);
JsonNode jsonNodeTopic = jsonNode.get("topic");
String appTopic = null, appKey = null, appValue = null;
if (jsonNodeTopic != null) {
appTopic = jsonNodeTopic.asText();
}
if (appTopic == null) {
continue;
}
JsonNode jsonNodeKey = jsonNode.get("key");
if (jsonNodeKey != null) {
appKey = jsonNode.asText();
}
JsonNode jsonNodeValue = jsonNode.get("value");
if (jsonNodeValue != null) {
appValue = jsonNodeValue.asText();
}
// send to application topic
ProducerRecord producerRecord = new ProducerRecord<>(appTopic, appKey, appValue);
try {
producer.send(producerRecord).get();
// success. commit message
OffsetAndMetadata offsetAndMetadata = new OffsetAndMetadata(consumerRecord.offset() + 1);
HashMap metadataHashMap = new HashMap<>();
metadataHashMap.put(topicPartition, offsetAndMetadata);
consumer.commitSync(metadataHashMap);
} catch (ExecutionException e) {
consumer.pause(Collections.singletonList(topicPartition));
consumer.seek(topicPartition, consumerRecord.offset());
timed = true;
break;
}
} else {
consumer.pause(Collections.singletonList(topicPartition));
consumer.seek(topicPartition, consumerRecord.offset());
timed = true;
break;
}
}
if (timed) {
lock.wait();
}
}
} while (!exit);
}
}