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,我们可以高效地处理消息队列,并确保数据不会因为服务重启而丢失。这种方法不仅提高了系统的可靠性,还简化了消息处理的流程。