Spring Boot 整合——kafka消息转换、使用异步获取消息以及使用事务消息

Spring Boot 整合之前的内容

项目名称 描述 地址
base-data-mybatis 整合mybatis-plus(实际上官方教程已经很多,只做了自定义插件) 未完成
base-jpa JPA基础使用 JPA 数据模型定义
base-jpa-query JPA多表关联使用 JPA 数据模型关联操作
base-log 日志配置 SpringBoot日志配置
base-rabbit rabbitMQ简单使用 RabbitMQ基础使用
base-rabbit3 rabbitMQ一些自定义配置 消息确认回调、消息转换以及消息异常处理
base-rabbit-delay rabbitMQ延时队列 延时队列和消息重试
base-redis redis简单使用 RedisTemplate基础使用;Redis实现简单的发布订阅以及配置序列化方式
base-redis-lock redis分布式锁 Redis分布式锁的简单实现
base-redis-delay 基于有赞的延时消息方案的简单实现 延时队列的简单实现
base-swagger swagger使用 wagger2使用
base-mongodb mongodb简单使用 MongoDB安装以及Spring Boot整合,MongoDB实体创建以及简单CRUD,MongoDB聚合操作,MongoDB分组去重以及MongoDB联表查询

关于版本

依赖 版本
springboot 2.0.8.RELEASE
mongodb 4.0.14

项目地址

因为涉及的代码较多,所以并不会贴出所有代码。本篇文章涉及的源码下载地址: https://gitee.com/daifyutils/springboot-samples

消息处理

针对消息的处理主要有两种方式:

  1. 对消息序列化的设置
  2. 实现RecordMessageConverter接口对消息进行处理

消息序列化

消息序列化主要是在初始化factory的时候对key和value进行操作的序列化设置。

    @Bean
    @ConditionalOnMissingBean(ConsumerFactory.class)
    public ConsumerFactory<?, ?> kafkaConsumerFactory() {
        DefaultKafkaConsumerFactory<String, Object> consumerFactory = new DefaultKafkaConsumerFactory<>(
                this.properties.buildConsumerProperties());
        // 对数据进行反序列化
        consumerFactory.setKeyDeserializer(new StringDeserializer());
        // 需要注意在对值进行解密的时候,kafka需要指定安全包名,或者使用*表示所有
        JsonDeserializer jsonDeserializer = new JsonDeserializer();
        jsonDeserializer.addTrustedPackages("*");
        consumerFactory.setValueDeserializer(jsonDeserializer);

        return consumerFactory;
    }

    @Bean
    @ConditionalOnMissingBean(ProducerFactory.class)
    public ProducerFactory<?, ?> kafkaProducerFactory() {
        DefaultKafkaProducerFactory<String, Object> factory = new DefaultKafkaProducerFactory<>(
            this.properties.buildProducerProperties());
        String transactionIdPrefix = this.properties.getProducer()
            .getTransactionIdPrefix();
        // 对数据进行序列化
        factory.setKeySerializer(new StringSerializer());
        factory.setValueSerializer(new JsonSerializer());

        if (transactionIdPrefix != null) {
            factory.setTransactionIdPrefix(transactionIdPrefix);
        }
        return factory;
    }

当然这种操作只是对数据的序列化操作,需要对数据内容进行修改需要实现RecordMessageConverter接口

  • 首先需要创建一个实现RecordMessageConverter接口的类
/**
 * 自定义消息转换器
 * @author daify
 */
@Log4j2
public class CustomRecordMessageConverter implements RecordMessageConverter {

    /**
     * 负责处理消费端传递的内容
     * @param consumerRecord
     * @param acknowledgment
     * @param consumer
     * @param type
     * @return
     */
    @Override
    public Message<?> toMessage(ConsumerRecord<?, ?> consumerRecord,
                                Acknowledgment acknowledgment,
                                Consumer<?, ?> consumer,
                                Type type) {

        return null;
    }

    /**
     * 负责将生产者的消息进行处理
     * @param message
     * @param s
     * @return
     */
    @Override
    public ProducerRecord<?, ?> fromMessage(Message<?> message, String s) {
        log.info("执行了………………fromMessage");
        log.info("acknowledgment 内容:{}", JSON.toJSONString(message));

        log.info("consumer 内容:{}", s);
        String valueStr = JSON.toJSONString(message.getPayload());
        // 此处在对消息处理的时候可以尝试修改消息目标
        ProducerRecord record = new ProducerRecord(KafkaConfig.TOPIC2,valueStr);

        return record;
    }
}
  • 在初始化KafkaTemplate的时候进行消息转换器设置
    @Bean
    @ConditionalOnMissingBean(KafkaTemplate.class)
    public KafkaTemplate<?, ?> kafkaTemplate(
        ProducerFactory<Object, Object> kafkaProducerFactory,
        ProducerListener<Object, Object> kafkaProducerListener) {

        KafkaTemplate<Object, Object> kafkaTemplate = new KafkaTemplate<>(
            kafkaProducerFactory);
        // 设置消息转换
        kafkaTemplate.setMessageConverter(new CustomRecordMessageConverter());
        kafkaTemplate.setProducerListener(kafkaProducerListener);
        kafkaTemplate.setDefaultTopic(this.properties.getTemplate().getDefaultTopic());
        return kafkaTemplate;
    }

  • 测试

在这里测试向kafka-topic1中发送消息,但是进过消息处理的时候目标会被修改到了kafka-topic2这个时候控制台输出的结果

## 经过了消息处理器
d.s.k.c.c.CustomRecordMessageConverter   : 执行了………………fromMessage
d.s.k.c.c.CustomRecordMessageConverter   : acknowledgment 内容:{"headers":{"id":"ff32f269-3e7d-984e-0f4f-936167b91903","timestamp":1588412460297},"payload":{"id":1,"type":0},"topic":"kafka-topic1"}
d.s.k.c.c.CustomRecordMessageConverter   : consumer 内容:null
o.a.k.clients.producer.ProducerConfig    : ProducerConfig values: 

## kafka-topic2消费端监听的结果
o.a.kafka.common.utils.AppInfoParser     : Kafka version : 1.0.2
o.a.kafka.common.utils.AppInfoParser     : Kafka commitId : 2a121f7b1d402825
d.s.k.c.consumer.KafkaConsumerListener   : kafka-topic2接收结果:"{\"id\":1,\"type\":0}"

异步发送消息

发送消息的时候,我们可能需要获取消息结果,但是等待时间过长会导致线程阻塞起来,所以我们可以通过异步方式获取消息发送的结果,这样可以大大提高了生产者的吞吐量。

获取消息结果的代码

同步

通过同步方式获取消息发送结果可以通过下面的代码

    @Async
    public String sendAsync(MyMessage myMessage, String topic) {
        CustomMessage message = new CustomMessage();
        message.setPayload(myMessage,topic);
        try {
            SendResult<String, Object> stringObjectSendResult = kafkaTemplate.send(message).get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        return JSON.toJSONString(message);
    }

异步

而如果希望获取结果使用异步方式可以使用下面的代码。

    @Async
    public String sendAsync(MyMessage myMessage, String topic) {
        CustomMessage message = new CustomMessage();
        message.setPayload(myMessage,topic);
        ListenableFuture<SendResult<String, Object>> send = kafkaTemplate.send(message);
        send.addCallback(new CustomListenableFutureCallback(message));
        return JSON.toJSONString(message);
    }

send.addCallback(new CustomListenableFutureCallback(message)); 是通过回调的方式获得最终结果,传入的方法需要实现ListenableFutureCallback接口。下面是一个实现此接口的方法

/**
 * 获取消费的异步结果
 * @author daify
 */
@Log4j2
public class CustomListenableFutureCallback implements ListenableFutureCallback<SendResult<String, Object>> {

    public CustomListenableFutureCallback(CustomMessage customMessage) {
        this.customMessage = customMessage;
    }

    private CustomMessage customMessage;

    /**
     * 失败的时候
     * @param throwable
     */
    @Override
    public void onFailure(Throwable throwable) {
        log.error("执行了onFailure");
        log.error("topic:{},message:{}" ,
            customMessage.getPayload(),
            JSON.toJSONString(customMessage.getPayload()));
    }

    /**
     * 成功的办法
     * @param stringMyMessageSendResult
     */
    @Override
    public void onSuccess(SendResult<String, Object> stringMyMessageSendResult) {
        log.info("执行了onSuccess");
        log.info("topic:{},message:{}" ,
            customMessage.getPayload(),
            JSON.toJSONString(customMessage.getPayload()));
    }
}

现在通过异步方式发送消息后控制台会打印下面内容

d.s.k.c.c.CustomListenableFutureCallback : 执行了onSuccess
d.s.k.c.c.CustomListenableFutureCallback : topic:MyMessage(id=2, name=sendAsync, type=2),message:{"id":2,"name":"sendAsync","type":2}
d.s.k.c.consumer.KafkaConsumerListener   : kafka-topic2接收结果:"{\"id\":2,\"name\":\"sendAsync\",\"type\":2}"

使用事务消息

首先要使用事务消息需要对配置进行修改。主要是针对生产者的修改

  application:
    name: base.kafka
  kafka:
    bootstrap-servers: kafka服务地址1:端口,kafka服务地址2:端口,kafka服务地址3:端口
    producer:
      # 写入失败时,重试次数。当leader节点失效,一个repli节点会替代成为leader节点,此时可能出现写入失败,
      # 当retris为0时,produce不会重复。retirs重发,此时repli节点完全成为leader节点,不会产生消息丢失。
      retries: 3
      #procedure要求leader在考虑完成请求之前收到的确认数,用于控制发送记录在服务端的持久化,其值可以为如下:
      #acks = 0 如果设置为零,则生产者将不会等待来自服务器的任何确认,该记录将立即添加到套接字缓冲区并视为已发送。在这种情况下,无法保证服务器已收到记录,并且重试配置将不会生效(因为客户端通常不会知道任何故障),为每条记录返回的偏移量始终设置为-1。
      #acks = 1 这意味着leader会将记录写入其本地日志,但无需等待所有副本服务器的完全确认即可做出回应,在这种情况下,如果leader在确认记录后立即失败,但在将数据复制到所有的副本服务器之前,则记录将会丢失。
      #acks = all 这意味着leader将等待完整的同步副本集以确认记录,这保证了只要至少一个同步副本服务器仍然存活,记录就不会丢失,这是最强有力的保证,这相当于acks = -1的设置。
      #可以设置的值为:all, -1, 0, 1
      acks: all
      transaction-id-prefix: transaction'
### 下面消费端内容不变
......

我们在使用kafka的时候,希望在某些业务出现问题的时候,终止对kafka传递的消息,所以KafkaTemplate提供了一个executeInTransaction方法让我们实现在事务的消息发送。

executeInTransaction的使用相当简单,只需要使用kafkaTemplate.executeInTransaction便实现了事务的消息

    /**
     * 事务性的消息发送
     * @param myMessage
     * @return
     */
    public String sendInTransaction(MyMessage myMessage){

        for (int i = 0; i < 5; i++) {
            int index = i;
            myMessage.setId(i);
            kafkaTemplate.executeInTransaction(new KafkaOperations.OperationsCallback<String, Object, Object>() {
                @Override
                public Object doInOperations(KafkaOperations<String, Object> operations) {
                    try {
                        SendResult<String, Object> result = null;
                        if (index == 3) {
                            throw new RuntimeException();
                        }
                        try {
                            result = kafkaTemplate.send("transaction-test", "测试数据:" + index).get();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } catch (ExecutionException e) {
                            e.printStackTrace();
                        }
                        log.info("kafka 事务消息: {}" , "测试数据:" + index);
                        return JSON.toJSONString(myMessage);
                    } catch (Exception e) {
                        e.printStackTrace();
                        return "发送失败";
                    }
                }
            });
        }
        return JSON.toJSONString(myMessage);
    }

尝试调用上面的方法向"transaction-test"的topic输入数据最后控制台中会发现出现错误的第四条数据因为抛出异常,其他发送的消息并未出现在消费端的日志中

d.s.k.t.producer.TransactionKafkaSender  : kafka 事务消息: 测试数据:0
d.s.k.t.consumer.KafkaConsumerListener   : transaction-test接收结果:"测试数据:0"
d.s.k.t.producer.TransactionKafkaSender  : kafka 事务消息: 测试数据:1
d.s.k.t.consumer.KafkaConsumerListener   : transaction-test接收结果:"测试数据:1"
d.s.k.t.producer.TransactionKafkaSender  : kafka 事务消息: 测试数据:2
d.s.k.t.consumer.KafkaConsumerListener   : transaction-test接收结果:"测试数据:2"
java.lang.RuntimeException
	at dai.samples.kafka.transaction.producer.TransactionKafkaSender$1.doInOperations(TransactionKafkaSende..... 异常数据
d.s.k.t.producer.TransactionKafkaSender  : kafka 事务消息: 测试数据:4
d.s.k.t.consumer.KafkaConsumerListener   : transaction-test接收结果:"测试数据:4"

到目前,springboot关于kafka的消息发送的简单操作就截止了。后面我会介绍消费端的内容。


个人水平有限,上面的内容可能存在没有描述清楚或者错误的地方,假如开发同学发现了,请及时告知,我会第一时间修改相关内容。假如我的这篇内容对你有任何帮助的话,麻烦给我点一个赞。你的点赞就是我前进的动力。

你可能感兴趣的:(#,Spring,Boot常用组件,JAVA)