Spring Boot 结合 Kafka、Redis 高效处理消息队列

764 阅读3分钟

Spring Boot 结合 Kafka、Redis 高效处理消息队列

在本文章中,我们将展示如何使用 Spring Boot 监听 Kafka 消息,将消息缓存到 Redis,并实现一个队列消费线程的管理和监控机制。我们将详细介绍每一步的实现,并列出所需的依赖。

步骤 1:创建 Spring Boot 项目

首先,创建一个新的 Spring Boot 项目。

步骤 2:添加依赖

在 pom.xml 文件中添加以下依赖:

<dependencies>

    <!-- Spring Boot Starter Kafka -->
    <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka</artifactId>
    </dependency>

    <!-- Redisson (Redis Client) -->
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson-spring-boot-starter</artifactId>
        <version>3.16.3</version>
    </dependency>

    <!-- FastJSON (JSON Parser) -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.75</version>
    </dependency>

    <!-- Lombok (Simplifies Java Code) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.20</version>
        <scope>provided</scope>
    </dependency>

    <!-- Spring Boot Starter Logging -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>
</dependencies>

步骤 3:配置 Kafka 和 Redis

在 application.yml 文件中添加 Kafka 和 Redis 的配置,具体配置(redisson address等等)需要根据实际项目需求为主

spring:
  kafka:
    bootstrap-servers: localhost:9092
    consumer:
      group-id: your_group_id
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer

redisson:
  address: redis://127.0.0.1:6379

步骤 4:创建 EventProcessorManager 类

创建一个名为 EventProcessorManager 的类,并实现 Kafka 消息监听、消息缓存到 Redis 以及队列消费线程的管理和监控。

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.redisson.api.RListMultimapCache;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
public class EventProcessorManager {

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private TaskScheduler taskScheduler;

    private Thread consumeThread;
    private final LinkedBlockingQueue<EventDto<?>> scheduleQueue = new LinkedBlockingQueue<>();

    /**
     * 初始化方法,在构造 bean 后运行。
     * 初始化调度队列和线程监控。
     */
    @PostConstruct
    public void init() {
        this.initScheduleQueue();
        this.initConsumeThreadMonitor();
    }

    /**
     * 初始化一个定时任务来监控消费线程。
     * 如果消费线程不存活,则重新初始化调度队列。
     */
    private void initConsumeThreadMonitor() {
        taskScheduler.scheduleWithFixedDelay(
                () -> {
                    Boolean isAlive = Optional.ofNullable(consumeThread).map(Thread::isAlive).orElse(true);
                    log.debug("检查队列消费线程是否可用, isAlive:{}", isAlive);
                    if (isAlive) {
                        return;
                    }
                    log.warn("本地消费队列线程已被销毁,正在创建新的消费队列");
                    this.initScheduleQueue();
                },
                DateUtil.offsetMinute(new Date(), 5),
                5 * 60 * 1000);
    }

    /**
     * Kafka 监听方法,处理来自指定主题的消息。
     * 将事件保存到缓存,并添加到调度队列。
     *
     * @param consumer Kafka 消费记录
     */
    @KafkaListener(topics = {"your_topic_name"}, groupId = "your_group_id")
    public void eventListener(ConsumerRecord<String, String> consumer) {
        log.info("接收到主题:{}, 键:{}, 值:{}", consumer.topic(), consumer.key(), consumer.value());
        String value = consumer.value();
        if (StrUtil.isBlank(value)) {
            return;
        }

        EventDto<?> eventDto = JSON.parseObject(value, new TypeReference<EventDto<EventData>>() {});
        this.saveToCache(eventDto);
        scheduleQueue.add(eventDto);
    }

    /**
     * 添加事件到缓存和调度队列。
     *
     * @param eventDto 要添加的事件
     * @param <T>      事件数据的类型
     */
    public <T extends EventData> void addEvent(EventDto<T> eventDto) {
        this.saveToCache(eventDto);
        scheduleQueue.add(eventDto);
    }

    /**
     * 将事件保存到 Redis 缓存中,并设置过期时间。
     *
     * @param eventDto 要保存的事件
     * @param <T>      事件数据的类型
     */
    private <T extends EventData> void saveToCache(EventDto<T> eventDto) {
        String topic = Optional.ofNullable(eventDto.getTopic()).orElse("default_topic");
        eventDto.setTopic(topic);

        RListMultimapCache<String, Object> cacheMultimap = redissonClient.getListMultimapCache(topic);
        cacheMultimap.put(eventDto.getMessagesId(), JSON.toJSONString(eventDto));
        cacheMultimap.expireKey(eventDto.getMessagesId(), 24, TimeUnit.HOURS);
    }

    /**
     * 初始化调度队列,清空队列并从缓存中加载事件。
     * 启动一个新线程来处理队列中的事件。
     */
    private void initScheduleQueue() {
        // 清空队列,避免重复添加任务
        scheduleQueue.clear();

        // 从 Redis 缓存中加载消息到 scheduleQueue
        loadEventsFromCache();

        Runnable runnable = () -> {
            for (; ; ) {
                Optional<EventDto<?>> scheduleIdOpt = Optional.ofNullable(scheduleQueue.poll());
                if (!scheduleIdOpt.isPresent()) {
                    try {
                        Thread.sleep(1000L);
                    } catch (InterruptedException ignored) {
                    }
                    continue;
                }
                EventDto<?> eventDto = scheduleIdOpt.get();
                try {
                    this.operateEvent(eventDto);
                } catch (Exception e) {
                    log.error("事件消息消费失败, dto:{}", eventDto, e);
                }
            }
        };
        consumeThread = new Thread(runnable);
        consumeThread.start();
    }

    /**
     * 从 Redis 缓存中加载事件并添加到调度队列。
     */
    private void loadEventsFromCache() {
        // 获取所有缓存的主题
        Set<String> topics = redissonClient.getKeys().getKeysByPattern("your_topic_pattern:*");
        for (String topic : topics) {
            RListMultimapCache<String, String> cacheMultimap = redissonClient.getListMultimapCache(topic);
            // 获取该主题下的所有消息
            Map<String, List<String>> allMessages = cacheMultimap.readAllMap();
            for (Map.Entry<String, List<String>> entry : allMessages.entrySet()) {
                for (String message : entry.getValue()) {
                    EventDto<?> eventDto = JSON.parseObject(message, new TypeReference<EventDto<EventData>>() {});
                    scheduleQueue.add(eventDto);
                }
            }
        }
    }

    /**
     * 处理一个事件,通过从缓存中删除该事件并记录操作日志。
     *
     * @param eventDto 要处理的事件
     */
    private void operateEvent(EventDto<?> eventDto) {
        String topic = Optional.ofNullable(eventDto.getTopic()).orElse("default_topic");

        RListMultimapCache<String, String> listMultimapCache = redissonClient.getListMultimapCache(topic);
        long row = listMultimapCache.fastRemove(eventDto.getMessagesId());

        if (row == 0) {
            log.warn("事件消息ID不存在,跳过当前操作, eventDto:{}", JSON.toJSONString(eventDto));
            return;
        }

        log.info("执行调度事件队列成功, eventDto:{}", JSON.toJSONString(eventDto));
    }
}

步骤 5:创建 EventDto 类

创建一个 EventDto 类,用于封装事件数据。

public class EventDto<T extends EventData> {、
    private String eventKey;
    private String topic;
    private String messagesId;
    private T data;

    // Getters and Setters
}

步骤 6:创建 EventData 类

创建一个 EventData 类,作为事件数据的基类。

public class EventData {
    // Define your event data fields and methods here
}

步骤 7:发送kafka事件推送

@Autowired private KafkaTemplate<String, String> kafkaTemplate;

EventDto eventDto = new EventDto<>().setKey("your event key").setTopic("your event topic")
.setData("your custom EventDto");

kafkaTemplate.send(eventDto.getTopic(), eventDto.getKey(), JSON.toJSONString(eventDto));

步骤 8:kafka事件接收

EventProcessorManager @KafkaListener 录入 EventDto 传递的event topic

步骤 9:消息队列的事件处理

EventProcessorManager operateEvent(EventDto<?> eventDto)中处理消息队列的业务逻辑

总结

通过使用Spring Kafka和Redisson,我们可以高效地处理消息队列,并确保数据不会因为服务重启而丢失。这种方法不仅提高了系统的可靠性,还简化了消息处理的流程。