Bootstrap

spring中kafka消息丢失问题(提供者、消费者)附解决方案

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>
;