最简单的kafka接入方式(kafka配置),kafka整合Spring

一.前言.

本文主要介绍了Springboot项目整合kafka的最简单的方式.

二.主要流程.

1.引入Maven
2.增加消费者和生产者配置
3.初始化读取配置
4.进行消费和生产消息

三.各个细节,步骤

1. 引入Maven

   <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId>
            <version>0.10.1.0</version>
        </dependency>

2. 增加消费者和生产者配置
这里需要三个配置文件,application.yml,消费者配置文件,生产者配置文件.
在application.yml中指定消费者和生产者配置文件的名称.
application.yml:

kafka:
  consumerTopic: topic1 #消费的topic名称
  consumerConfig: kafka_consumer.properties  #配置文件名称
  consumerCommit: true    #消费者是否提交偏移量
  producerTopic: topic2 #发送的topic名称
  producerConfig: kafka_producer.properties  #配置文件名称
  producerEnable: true  #生产者是否生产开关

kafka_consumer.properties:

#消费者组ID
group.id=test
#broker地址 中间以逗号分隔
bootstrap.servers=10.100.100.100:9092
#zookeeper地址
zookeeper.connect=10.100.100.100:2181
#是否自动提交偏移量 建议设置成false  自己控制偏移量提交
enable.auto.commit=false
#提交offset到zookeeper的时间间隔
auto.commit.interval.ms=1000
#重要 消费的起始位置  earliest表示从最早记录开始消费 latest表示从最新记录消费
auto.offset.reset=earliest
#每次拉取的最大条数
max.poll.records=100
#处理的超时时间
session.timeout.ms=100000
#单条消息的最大大小
max.request.size=104857600

kafka_producer.properties

#broker地址
bootstrap.servers=10.100.100.100:9092
#重要 副本确认模式 producer的消息发送确认机制
acks=1
#单条消息的最大大小
max.request.size=104857600
#生产缓冲区大小
buffer.memory=104857600
#重试次数
retries=3

3. 初始化读取配置.
      (1)创建配置类,利用Springboot自动加载application.yml中配置文件中kafka的相关参数.

@Component
@ConfigurationProperties(prefix = "kafka")
public class KafkaConfig {
    //消费TOPIC
    private String consumerTopic;
    //消费者相关配置文件路径
    private String consumerConfig;
    //消费者是否提交偏移量
    private boolean consumerCommit;
    //生产者生产TOPIC
    private String producerTopic;
    //生产者相关配置文件路径
    private String producerConfig;
    //生产者是否生产开关
    private boolean producerEnable;

    public String getConsumerTopic() {
        return consumerTopic;
    }

    public void setConsumerTopic(String consumerTopic) {
        this.consumerTopic = consumerTopic;
    }

    public String getConsumerConfig() {
        return consumerConfig;
    }

    public void setConsumerConfig(String consumerConfig) {
        this.consumerConfig = consumerConfig;
    }

    public boolean isConsumerCommit() {
        return consumerCommit;
    }

    public void setConsumerCommit(boolean consumerCommit) {
        this.consumerCommit = consumerCommit;
    }

    public String getProducerTopic() {
        return producerTopic;
    }

    public void setProducerTopic(String producerTopic) {
        this.producerTopic = producerTopic;
    }

    public String getProducerConfig() {
        return producerConfig;
    }

    public void setProducerConfig(String producerConfig) {
        this.producerConfig = producerConfig;
    }

    public boolean isProducerEnable() {
        return producerEnable;
    }

    public void setProducerEnable(boolean producerEnable) {
        this.producerEnable = producerEnable;
    }

    /**
    *@Description 将给定topic列表字符串拆解成列表
    *@Param [topics] topic列表字符串,中间逗号分隔
    *@Return java.util.List<java.lang.String> topic列表
    */
    public static List<String> getTopicLists(String topics) {
        String[] items = topics.split(",");
        List<String> result = new LinkedList<>();
        for (String item : items) {
            if (!StringUtils.isBlank(item)) {
                result.add(StringUtils.trim(item));
            }
        }
        return result;
    }
}

     (2)定义一个service类,加载生产者和消费者真正配置文件,完成生产对象和消费对象的初始化.

    @Autowired
    private KafkaConfig config;
    //消费对象
    private Consumer<String, String> messageConsumer;
    //生产对象
    private Producer<String, String> messageProducer;

    /**
    *@Description Kafka集群的初始化  包含加载配置文件,消费者、生产者的初始化
    */
    public void init() {
        //初始化生产者
        if (messageProducer == null && config.isProducerEnable()) {
            messageProducer = new KafkaProducer<>(PropertyFileUtil.load(config.getProducerConfig()),
                    new StringSerializer(), new StringSerializer());
        }

        //初始化消费者
        if (messageConsumer == null) {
            //已经在配置中配置了消费者组等等信息
            messageConsumer = new KafkaConsumer<>(PropertyFileUtil.load(config.getConsumerConfig()),
                    new StringDeserializer(), new StringDeserializer());
            //订阅主题
            messageConsumer.subscribe(config.getTopicLists(config.getConsumerTopic()));
        }
    }
public class PropertyFileUtil {

	/**
	*@Description 加载配置文件
	*/
	public static Properties load(String path) {
		InputStream stream = findInUserDir(path);
		if (stream == null) {
			stream = findInResource(path);
		}
		if (stream == null) {
			return null;
		}
		Properties properties = new Properties();
		try {
			properties.load(stream);
		} catch (IOException ex) {
			throw new RuntimeException(ex);
		}
		return properties;
	}

	private static InputStream findInUserDir(String file) {
		String userDir = System.getProperty("user.dir");
		if (userDir != null) {
			File f = new File(userDir, file);
			if (f.exists() && f.isFile()) {
				try {
					FileInputStream in = new FileInputStream(f);
					logger.info("加载配置:" + f.getAbsolutePath());
					return in;
				} catch (FileNotFoundException e) {
					return null;
				}
			}
		}
		return null;
	}

	private static InputStream findInResource(String file) {
		logger.info("加载配置:classpath:/" + file);
		return PropertyFileUtil.class.getResourceAsStream("/" + file);
	}
}

4. 进行真正的生产和消费.
生产:

 /**
    *@Description Kafka消息发送方式,这里是异步发送
    */
    public void produce(String key, String value) {
        //首先判断如果value为空则跳过
        if(StringUtils.isBlank(value)) {
            return;
        }
        if (messageProducer != null) {
            String topic = config.getProducerTopic();
            messageProducer.send(
                    new ProducerRecord<>(topic, key, value),
                    new Callback() {
                        @Override
                        public void onCompletion(RecordMetadata metadata, Exception ex) {
                            if (ex != null) {
                                // 发生错误
                                LOGGER.error("kafka生产数据异常,异常数据为-->key:{},value:{}",key,value,ex);
                                throw new RuntimeException("kafka生产数据异常...");
                            } else {
                                // 发送成功
                                LOGGER.info("{}异步生产完毕",key);
                            }
                        }
                    });
        }
    }

消费:

     //默认为false
    private volatile boolean reqClose;
  /**
    *@Description 消费者消费消息方法
    */
    public void consume() {
        //计数器
        long retry = 0;
        //因为要不断获取消息,所以需要循环
        while (!reqClose) {
            try {
                ConsumerRecords<String, String> records = messageConsumer.poll(1000);
                //会一次返回多个值,所以需要遍历
                for (ConsumerRecord<String, String> record : records) {
                    if (reqClose) {
                        LOGGER.info("跳出消費..");
                        break;
                    }
                    String key =  record.key();
                    String value = record.value();
                    //这里写真正的业务逻辑,处理完再提offset,避免丢失数据
                   
                }
                /**
                 * 手动提交offset,第一次消费数据的时候会从初始值,也就是你当前消费者保存的offset开始消费,并且会持续消费
                 *    下一条数据,因为虽然我们没有提交offset,但是内存中还维护了一个offset,但是一旦我们关闭了消费者,
                 *     再下一次启动时,又会从当前消费者保存的offset开始消费,那也就会造成重复消费.
                 */
                if (config.isConsumerCommit()) {
                    //同步提交,线程会阻塞到当前offset提交成功.
                    messageConsumer.commitSync();
                }
            } catch (Exception ex) {
                if (retry % 60 == 0) {
                    // 防止Kafka失联时,错误过多,控制1分钟出一次日志
                    LOGGER.error("Kafka Operation Failed", ex);
                    retry++;
                }
            }
        }
        // 释放资源
        this.close();
    }

完整的service处理类,这里定义了一个接口,然后使用一个实现类去实现.

public interface KafkaService {
    /**
    *@Description 初始化Kafka生产者及消费者
    */
    public void init();

    /**
    *@Description 消费kafka消息
    */
    public void consume();

    /**
    *@Description 生产kafka消息
    *@Param [key, value] 消息的key和value
    */
    public void produce(String key, String value);
    /**
    *@Description 关闭释放资源
    */
    void reqClose();

    /**
     * 重置kafka消费的offset
     */
    void resetOffset();


    /**
     *@Description 处理消息的业务方法
     */
    void consumeRecord(String key,String msg);



}



@Service
public class KafkaServiceImpl implements KafkaService {

    private static Logger LOGGER = LoggerFactory.getLogger(KafkaServiceImpl.class);

    @Autowired
    private KafkaConfig config;

    private volatile boolean reqClose;
    private volatile boolean closed;

    private Consumer<String, String> messageConsumer;
    private Producer<String, String> messageProducer;

    /**
    *@Description Kafka集群的初始化  包含加载配置文件,消费者、生产者的初始化
    */
    @Override
    public void init() {
        LOGGER.info("初始化生产者..");
        if (messageProducer == null && config.isProducerEnable()) {
            messageProducer = new KafkaProducer<>(PropertyFileUtil.load(config.getProducerConfig()),
                    new StringSerializer(), new StringSerializer());
        }

        LOGGER.info("初始化消费者..");
        if (messageConsumer == null) {
            //已经在配置中配置了消费者组等等信息
            messageConsumer = new KafkaConsumer<>(PropertyFileUtil.load(config.getConsumerConfig()),
                    new StringDeserializer(), new StringDeserializer());
            //订阅主题
            messageConsumer.subscribe(config.getTopicLists(config.getConsumerTopic()));
        }
    }

    /**
    *@Description Kafka消息发送方式
    */
    @Override
    public void produce(String key, String value) {
        //首先判断如果value为空则跳过
        if(StringUtils.isBlank(value)) {
            return;
        }
        if (messageProducer != null) {
            String topic = config.getProducerTopic();
            messageProducer.send(
                    new ProducerRecord<>(topic, key, value),
                    new Callback() {
                        @Override
                        public void onCompletion(RecordMetadata metadata, Exception ex) {
                            if (ex != null) {
                                // 发生错误
                                LOGGER.error("kafka生产数据异常,异常数据为-->key:{},value:{}",key,value,ex);
                                throw new RuntimeException("kafka生产数据异常...");
                            } else {
                                // 发送成功
                                LOGGER.info("{}异步生产完毕",key);
                            }
                        }
                    });
        }
    }

    /**
    *@Description 消费者消费消息方法
    */
    @Override
    public void consume() {
        //计数器
        long retry = 0;
        //因为要不断获取消息,所以需要循环
        while (!reqClose) {
            try {
                ConsumerRecords<String, String> records = messageConsumer.poll(1000);
                //会一次返回多个值,所以需要遍历
                for (ConsumerRecord<String, String> record : records) {
                    if (reqClose) {
                        LOGGER.info("跳出消費..");
                        break;
                    }
                    String key =  record.key();
                    String value = record.value();
                    //处理具体的逻辑业务,一条一条处理,处理完再提交offset,避免丢失数据
                    consumeRecord(key,value);
                }
                /**
                 * 手动提交offset,第一次消费数据的时候会从初始值,也就是你当前消费者保存的offset开始消费,并且会持续消费
                 *    下一条数据,因为虽然我们没有提交offset,但是内存中还维护了一个offset,但是一旦我们关闭了消费者,
                 *     再下一次启动时,又会从当前消费者保存的offset开始消费,那也就会造成重复消费.
                 */
                if (config.isConsumerCommit()) {
                    //同步提交,线程会阻塞到当前offset提交成功.
                    messageConsumer.commitSync();
                }
            } catch (Exception ex) {
                if (retry % 60 == 0) {
                    // 防止Kafka失联时,错误过多,控制1分钟出一次日志
                    LOGGER.error("Kafka Operation Failed", ex);
                    retry++;
                }
            }
        }
        // 释放资源
        this.close();
    }

    /**
    *@Description 处理消息的业务方法
    */
    @Override
    public void consumeRecord(String key,String msg) {
        LOGGER.info("接收到kafka消息,key:{}",key);
       
    }

    /**
    *@Description 重置消费者偏移量
    */
    @Override
    public void resetOffset() {
        Consumer<String, String> consumer = new KafkaConsumer<>(PropertyFileUtil.load(config.getConsumerConfig()),
                new StringDeserializer(), new StringDeserializer());
        List<String> topics = config.getTopicLists(config.getConsumerTopic());
        for (String topic : topics) {
            consumer.subscribe(Arrays.asList(topic));
            ConsumerRecords<String, String> records = consumer.poll(2000);
            Set<TopicPartition> topicList = consumer.assignment();
            Map<TopicPartition, Long> endMap = consumer.endOffsets(topicList);
            Map<TopicPartition, Long> beginmap = consumer.beginningOffsets(topicList);
            long singleTpLagSize = 1000000;
            for (TopicPartition tp : topicList) {
                long endOffset = endMap.get(tp);
                long beginOffset = beginmap.get(tp);
                long aimOffset = endOffset - singleTpLagSize;
                if (aimOffset > 0 && aimOffset >= beginOffset) {
                    consumer.seek(tp, endOffset-singleTpLagSize);
                } else {
                    consumer.seek(tp, beginOffset);
                }
            }
            consumer.commitSync();
        }
    }

    /**
     * 是否关闭kafka得长轮询
     * @return
     */
    public boolean isClosed() {
        return closed;
    }

    /**
     * 关闭资源
     */
    private void close() {
        try {
            if (messageConsumer != null) {
                messageConsumer.close();
            }
            if (messageProducer != null) {
                messageProducer.close();
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        closed = true;
        LOGGER.info("Kafka資源釋放完畢!");
    }

    /**
     * 关闭kafka得循环消费逻辑
     */
    public void reqClose() {
        this.reqClose = true;
    }
}

江湖路远,相逢即为缘分,如略有帮助,一键三联可否,谢谢啦

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员bling

义父,感谢支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值