spring kafka 可能会有的消息丢失的问题 以及解决方案
1.Kafka发送消息的两种方式
Kafka发送消息分为同步(sync)、异步(async)在与spring集成中通过配置文件修改
配置文件放在文末
-
<entry key="acks" value="1" />
-
acks = 0;表示producer不需要等待broker确认收到消息
-
acks = 1;表示producer至少需要等待leader已经成功写入本地log,但是follower如果没有成功备份同时leader挂掉,就造成消息丢失
-
acks = all 或者-1;表示需要等待 min.insync.replicas(默认为1,推荐配置大于2,如果配置为2,此时就需要leader和一个follower同步完后,才会返回ack)
Kafka生产者发送消息防止丢失
java提供了一个注解:@PostConstruct用来修饰非静态的void()方法,作用就是:当我们的项目启动时候就会进行预热,首先执行这个方法
使用kafkaTemplate在这个预热方法中使用setProducerListener();方法来为消息生产者创建一个监听器监听消息发送是否成功
@PostConstruct
public void initKafka(){
kafkaTemplate.setProducerListener(new ProducerListener<String,String>(){
public void onSuccess(String topic, Integer partition, String key, String value, RecordMetadata recordMetadata) {
System.out.println("kafka发送消息成功:topic:" + topic + "key:" + key + "value:" + value);
}
public void onError(String topic, Integer partition, String key, String value, Exception exception) {
// 消息发送失败 可以执行一系列操作....
System.err.println("kafka发送消息失败:topic:" + topic + "key:" + key + "value:" + value);
}
});
}
2.Kafka consumer接收消息
2.1自动提交
<entry key="enable.auto.commit" value="true"/>
假设我们生产者向服务器成功发送7条消息,消费者批量拿取,假设第一次拿取1,2两条消息,当消费者拿到消息之后会周期性的提交offset,也就是说,我拿到消息之后我就反馈给broker:我下次要从3开始,如果宕机,重启后还是从3开始就会造成消息丢失
解决方案: 在kafka producer发送之前,存入redis缓存中,当消费者正常消费时候在删除,同时,在编写一个定时任务,定时获取redis中消费失败的消息重新发送
// 防止消息丢失 (存入redis中)
redisTemplate.opsForHash().put("order",out_trade_no,order);
// kafka 发送消息
kafkaTemplate.send("test","order", JSON.toJSONString(order));
定时任务 cron表达式在线生成
/**
* 消息补偿
* 重新发送一遍
*/
@Scheduled(cron = "0 * * * * ?")
public void reSendMsg(){
List<Order> orderList = redisTemplate.opsForHash().values("order");
if(orderList == null){
return;
}
// 重新发送给消费者完成消费
for (Order order : orderList) {
kafkaTemplate.send("hgShop","order", JSON.toJSONString(order));
}
}
2.2手动提交
<entry key="enable.auto.commit" value="false"/>
当我们使用的手动提交时候,我们拿走1,2数据,但是在没有消费成功的情况下不反馈给 broker,比如:我消息1 消费完成,这时候消费者挂掉,重启之后,消费者重新获取消息,还是从2开始,因为消息1消费完成之后反馈给broker,但是消息2丢失了,没有反馈,broker就认为没有消费,这样也就能较好的避免大部分消息丢失。
要注意的是
在消费者端一定是实现 AcknowledgingMessageListener 接口 实现 重写两个参数的方法 然后 手动提交!!!
配置文件
自动提交–消费者
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
<bean id="consumerProperties" class="java.util.HashMap">
<constructor-arg>
<map>
<!--Kafka服务地址 -->
<entry key="bootstrap.servers" value="ip:port" />
<!--Consumer的组ID,相同group.id的consumer属于同一个组。 -->
<entry key="group.id" value="test-consumer-group" />
<!--如果此值设置为true,consumer会周期性的把当前消费的offset值保存到zookeeper。当consumer失败重启之后将会使用此值作为新开始消费的值。 -->
<entry key="enable.auto.commit" value="true" />
<!--网络请求的socket超时时间。实际超时时间由max.fetch.wait + socket.timeout.ms 确定 -->
<entry key="session.timeout.ms" value="15000 " />
<entry key="key.deserializer" value="org.apache.kafka.common.serialization.StringDeserializer" />
<entry key="value.deserializer" value="org.apache.kafka.common.serialization.StringDeserializer" />
</map>
</constructor-arg>
</bean>
<!-- 创建consumerFactory bean -->
<bean id="consumerFactory" class="org.springframework.kafka.core.DefaultKafkaConsumerFactory">
<constructor-arg ref="consumerProperties" />
</bean>
<!-- MessageListener -->
<bean id="kafkaListenerContainerFactory" class="org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory">
<property name="consumerFactory" ref="consumerFactory"></property>
</bean>
</beans>
自动提交–生产者
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
<!--参数配置 -->
<bean id="producerProperties" class="java.util.HashMap">
<constructor-arg>
<map>
<!-- kafka服务地址,可能是集群 value="localhost:9092,localhost:9093,localhost:9094"-->
<entry key="bootstrap.servers" value="ip:port" />
<!-- <entry key="group.id" value="0"/>
<entry key="retries" value="10"/>
<entry key="batch.size" value="16384"/>
<entry key="linger.ms" value="1"/>
<entry key="buffer.memory" value="33554432"/>-->
<entry key="key.serializer"
value="org.apache.kafka.common.serialization.StringSerializer" />
<entry key="value.serializer"
value="org.apache.kafka.common.serialization.StringSerializer" />
</map>
</constructor-arg>
</bean>
<!-- 创建kafkatemplate需要使用的producerfactory bean -->
<bean id="producerFactory" class="org.springframework.kafka.core.DefaultKafkaProducerFactory">
<constructor-arg>
<ref bean="producerProperties" />
</constructor-arg>
</bean>
<!-- 创建kafkatemplate bean,使用的时候,只需要注入这个bean,即可使用template的send消息方法 -->
<bean id="kafkaTemplate"
class="org.springframework.kafka.core.KafkaTemplate">
<constructor-arg ref="producerFactory" />
<!--设置对应topic test -是后台已经创建好的主题 也可以通过@KafkaListener设置-->
<property name="defaultTopic" value="test" />
</bean>
</beans>
手动提交–消费者
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
<!--消息监听器-->
<bean id="messageListenerContainer"
class="org.springframework.kafka.listener.KafkaMessageListenerContainer"
init-method="doStart">
<constructor-arg ref="consumerFactory"/>
<constructor-arg ref="containerProperties"/>
</bean>
<!-- 记得修改主题 -->
<bean id="containerProperties"
class="org.springframework.kafka.listener.ContainerProperties">
<!-- 构造函数 就是 主题的参数值 -->
<constructor-arg value="test"/><!--改自己的主题名字-->
<!-- 自定义个消息监听器 -->
<property name="messageListener" ref="myListnener"/>
<!--手工确认-->
<property name="ackMode" value="MANUAL"></property>
</bean>
<!-- -消息监听器 --> <!--↓↓↓↓↓↓↓↓改自己监视器的路径-->
<bean id="myListnener" class=""></bean>
<!-- 创建consumerFactory bean -->
<bean id="consumerFactory"
class="org.springframework.kafka.core.DefaultKafkaConsumerFactory">
<constructor-arg>
<ref bean="consumerProperties"/>
</constructor-arg>
</bean>
<bean id="consumerProperties" class="java.util.HashMap">
<constructor-arg>
<map>
<!--Kafka服务地址 --> <!--↓↓↓↓↓↓改自己的ip-->
<entry key="bootstrap.servers" value="ip:port"/>
<!--Consumer的组ID,相同group.id的consumer属于同一个组。 -->
<entry key="group.id" value="test-consumer-group"/>
<!--如果此值设置为true,consumer会周期性的把当前消费的offset值保存到zookeeper。当consumer失败重启之后将会使用此值作为新开始消费的值。 -->
<entry key="enable.auto.commit" value="false"/><!--手动提交 这里改成false-->
<!--<entry key="auto-offset-reset" value="earliest"></entry>-->
<!--网络请求的socket超时时间。实际超时时间由max.fetch.wait + socket.timeout.ms 确定 -->
<entry key="session.timeout.ms" value="15000 "/>
<entry key="key.deserializer"
value="org.apache.kafka.common.serialization.StringDeserializer"/>
<entry key="value.deserializer"
value="org.apache.kafka.common.serialization.StringDeserializer"/>
</map>
</constructor-arg>
</bean>
</beans>
手动提交–生产者
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
<!--参数配置 -->
<bean id="producerProperties" class="java.util.HashMap">
<constructor-arg>
<map>
<!-- kafka服务地址,可能是集群 value="localhost:9092,localhost:9093,localhost:9094"-->
<entry key="bootstrap.servers" value="ip:port" />
<!-- 有可能导致broker接收到重复的消息-->
<entry key="retries" value="0" />
<!-- 每次批量发送消息的数量 -->
<entry key="batch.size" value="5120" />
<!-- 默认0ms,在异步IO线程被触发后(任何一个topic,partition满都可以触发) -->
<entry key="linger.ms" value="1" />
<!-- 消息确认接收的模式 0:只管发 1:服务器leader 能收到 all:kafka每台机器都收到 -->
<entry key="acks" value="1" />
<!--producer可以用来缓存数据的内存大小。如果数据产生速度大于向broker发送的速度,producer会阻塞或者抛出异常 -->
<entry key="buffer.memory" value="33554432 " />
<entry key="key.serializer"
value="org.apache.kafka.common.serialization.StringSerializer" />
<entry key="value.serializer"
value="org.apache.kafka.common.serialization.StringSerializer" />
</map>
</constructor-arg>
</bean>
<!-- 创建kafkatemplate需要使用的producerfactory bean -->
<bean id="producerFactory"
class="org.springframework.kafka.core.DefaultKafkaProducerFactory">
<constructor-arg>
<ref bean="producerProperties" />
</constructor-arg>
</bean>
<!-- 创建kafkatemplate bean,使用的时候,只需要注入这个bean,即可使用template的send消息方法 -->
<bean id="KafkaTemplate"
class="org.springframework.kafka.core.KafkaTemplate">
<constructor-arg ref="producerFactory" />
<!--设置对应topic -->
<property name="defaultTopic" value="test" />
</bean>
</beans>