Kafka实践----shock wave 2

378 阅读6分钟

环境

  • springcloudAlibaba
  • docker
  • zookeeper
  • kafka

参考

blog.csdn.net/yuanlong122…

安装

镜像拉取

docker pull wurstmeister/zookeeper
docker pull wurstmeister/kafka

运行zookeeper

docker run -itd --name zookeeper -p 2181:2181 wurstmeister/zookeeper

注: 网上很多使用run -i运行,这种后来连接的时候可能会报错

运行kafka

docker run -itd --name kafka --publish 9092:9092 --link zookeeper --env KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 --env KAFKA_ADVERTISED_HOST_NAME=127.0.0.1 --env KAFKA_ADVERTISED_PORT=9092 wurstmeister/kafka:latest
参数说明
KAFKA_ZOOKEEPER_CONNECT= 配置zookeeper管理kafka的路径10.9.44.11:2181/kafka
KAFKA_ADVERTISED_HOST_NAME= 把kafka的地址端口注册给zookeeper
KAFKA_ADVERTISED_PORT= zookeeper端口号

注: 这里同样使用-itd运行,不然可能会出现kafka容器运行失败的情况

依次运行两个容器即可成功启动kafka,之后使用docker exec 进入kafka容器

进入kafka文件夹使用下面的命令创建主题

kafka-topics.sh --create --zookeeper zookeeper:2181 --replication-factor 1 --partitions 2 --topic topic1

注: 如果kafka只有一个服务器,那么--replication-factor配置必须小于2

可以不手动创建topic,在执行代码kafkaTemplate.send("topic1", normalMessage)发送消息时,kafka会帮我们自动完成topic的创建工作,但这种情况下创建的topic默认只有一个分区,分区也没有副本。

引入依赖

<dependency>
   <groupId>org.springframework.kafka</groupId>
   <artifactId>spring-kafka</artifactId>
</dependency>

配置说明

kafka:
  bootstrap-servers: 127.0.0.1:9092
  producer:
    # 发生错误后,消息重发的次数。
    retries: 0
    #当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算。
    batch-size: 16384
    # 设置生产者内存缓冲区的大小。
    buffer-memory: 33554432
    # 键的序列化方式
    key-serializer: org.apache.kafka.common.serialization.StringSerializer
    # 值的序列化方式
    value-serializer: org.apache.kafka.common.serialization.StringSerializer
    # acks=0 : 生产者在成功写入消息之前不会等待任何来自服务器的响应。
    # acks=1 : 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应。
    # acks=all :只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。
    acks: 1
      #properties:
    # 使用自定义的分区选择器
    #{partitioner.class:  org.example.config.CustomizePartitioner}
  consumer:
    # 自动提交的时间间隔 在spring boot 2.X 版本中这里采用的是值的类型为Duration 需要符合特定的格式,如1S,1M,2H,5D
    auto-commit-interval: 1S
    # 该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理:
    # latest(默认值)在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录)
    # earliest :在偏移量无效的情况下,消费者将从起始位置读取分区的记录
    auto-offset-reset: earliest
    # 是否自动提交偏移量,默认值是true,为了避免出现重复数据和数据丢失,可以把它设置为false,然后手动提交偏移量
    # 如果先设置为false,然后设置为true还是或重复消费,必须重新推送消息,自动修改偏移量才行
    # 如果先设置为true,然后设置为false,偏移量前面的数据是不会重复消费的
    enable-auto-commit: false
    # 键的反序列化方式
    key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
    # 值的反序列化方式
    value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
    group-id: defaultConsumerGroup
    # 最多批量消费50条
    # max-poll-records: 50
  listener:
    # 在侦听器容器中运行的线程数。
    concurrency: 5
    #listner负责ack,每调用一次,就立即commit
    ack-mode: manual_immediate
    missing-topics-fatal: false

实践

普通消息推送

生产者
@RestController
@RequestMapping("kafka")
public class KafkaController {
    @Autowired
    private KafkaTemplate<String, Object> kafkaTemplate;
    // 发送消息
    @GetMapping("pushMessage")
    public void pushMessage(@RequestParam(required = false) String message) {
        kafkaTemplate.send("topic1", message);
    }
}
消费者
@Component
public class KafkaConsumer {
    /**
     * @description: TODO 消费监听
     * errorHandler可以指定消费错误调用的方法
     * containerFactory 可以使用自定义的消息过滤器
     * @SendTo("topic2")消息转发
     * @author shock wave 2
     * @date
     * @version 1.0
     */
    @KafkaListener(topics = {"topic1"},containerFactory = "filterContainerFactory")
    public void onMessage(ConsumerRecord<?, ?> record, Acknowledgment ack){
        //第二个参数为手动·ack参数
        // 消费的哪个topic、partition的消息,打印出消息内容
        System.out.println("简单消费:"+record.topic()+"-"+record.partition()+"-"+record.value());
        //手动消息确认
        ack.acknowledge();
    }
    }

带回调消息推送

/**
 * @description: TODO 带回调的生产者 方式一
 * @author shock wave 2
 * @date
 * @version 1.0
 */
@GetMapping("pushCallBackMessageV1")
public void pushCallBackMessage(@RequestParam(required = false) String message) {
    kafkaTemplate.send("topic1", message).addCallback(success -> {
        // 消息发送到的topic
        String topic = success.getRecordMetadata().topic();
        // 消息发送到的分区
        int partition = success.getRecordMetadata().partition();
        // 消息在分区内的offset
        long offset = success.getRecordMetadata().offset();
        System.out.println("发送消息成功:" + topic + "-" + partition + "-" + offset);
    }, failure -> {
        System.out.println("发送消息失败:" + failure.getMessage());
    });
}

事务消息

/**
     * @description: TODO 推送事务消息
     * @author shock wave 2
     * @date
     * @version 1.0
     */
    @GetMapping("pushTransactionMessage")
    public void pushTransactionMessage(@RequestParam(required = false) String message){
        // 声明事务:后面报错消息不会发出去
        kafkaTemplate.executeInTransaction(operations -> {
            operations.send("topic1",message);
            throw new RuntimeException("fail");
        });
        // 不声明事务:后面报错但前面消息已经发送成功了
//        kafkaTemplate.send("topic1",message);
//        throw new RuntimeException("fail");
    }

自定义消息过滤器

/**
 * @author shock wave 2
 * @version 1.0
 * @description: TODO kafka消息过滤器
 * @date 2021/8/3 16:19
 */
@Component
public class KafkaConfigConsumer {
    @Autowired
    private ConsumerFactory consumerFactory;
    // 消息过滤器
    @Bean
    public ConcurrentKafkaListenerContainerFactory filterContainerFactory() {
        ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory();
        factory.setConsumerFactory(consumerFactory);
        // 被过滤的消息将被丢弃
        factory.setAckDiscarded(true);
        // 消息过滤策略
        factory.setRecordFilterStrategy(consumerRecord -> {
            System.out.println(consumerRecord);
            if (consumerRecord.value().toString().contains("22")) {
                return false;
            }
            //返回true消息则被过滤
            return true;
        });
        return factory;
    }
}

消费者使用下面注释,指定过滤器

@KafkaListener(topics = {"topic1"},containerFactory = "filterContainerFactory")

自定义分区器

/**
 * @author shock wave 2
 * @version 1.0
 * @description: TODO 自定义分区器
 * ① 若发送消息时指定了分区(即自定义分区策略),则直接将消息append到指定分区;
 *
 * ② 若发送消息时未指定 patition,但指定了 key(kafka允许为每条消息设置一个key),则对key值进行hash计算,根据计算结果路由到指定分区,这种情况下可以保证同一个 Key 的所有消息都进入到相同的分区;
 *
 * ③  patition 和 key 都未指定,则使用kafka默认的分区策略,轮询选出一个 patition;
 * @date 2021/8/3 16:07
 */
public class CustomizePartitioner implements Partitioner {
    @Override
    public int partition(String s, Object o, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster) {
        // 自定义分区规则
        return 0;
    }

    @Override
    public void close() {

    }

    @Override
    public void configure(Map<String, ?> map) {

    }
}

自定义异常处理

@Bean
public ConsumerAwareListenerErrorHandler consumerAwareErrorHandler() {
    return (message, exception, consumer) -> {
        System.out.println("消费异常:"+message.getPayload());
        return null;
    };
}

消费者使用以下注释,自定义消费失败的异常处理

@KafkaListener(topics = {"topic1"},containerFactory = "filterContainerFactory",errorHandler="consumerAwareErrorHandler")

批量消费

配置文件

#开启批量处理
listener:
   type: batch
#最多批量处理50条数据
consumer:
   max-poll-records: 50
具体可以查看上面的配置文件

消费者

/**
 * @description: TODO 使用list接收批量消息
 * @author shock wave 2
 * @date
 * @version 1.0
 */
@KafkaListener(id = "consumer2",groupId = "felix-group", topics = "topic1")
public void onMessage3(List<ConsumerRecord<?, ?>> records) {
    System.out.println(">>>批量消费一次,records.size()="+records.size());
    for (ConsumerRecord<?, ?> record : records) {
        System.out.println(record.value());
    }
}

其他

@KafkaListener(id = "consumer1",groupId = "felix-group",topicPartitions = {@TopicPartition(topic = "topic1", partitions = { "0" }),@TopicPartition(topic = "topic2", partitions = "0", partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "8"))})
* ① id:消费者ID;
* ② groupId:消费组ID;
* ③ topics:监听的topic,可监听多个;
* ④ topicPartitions:可配置更加详细的监听信息,可指定topic、parition、offset监听。