ExecuteQueue
@Component
@Slf4j
public class ExecuteQueueTask {
private static final int THREAD_PRIORITY = Thread.MIN_PRIORITY + 1; // 设置为较低的优先级
private static final int POOL_SIZE = Constants.LENGTH_10; // 线程池大小
private ExecutorService executorService;
@Resource
private VideoInfoPostService videoInfoPostService;
@Resource
private RedisUtils redisUtils;
@Resource
private VideoInfoService videoInfoService;
@Resource
private RedisComponent redisComponent;
@Resource
private EsSearchComponent esSearchComponent;
@Resource
private VideoPlayHistoryService videoPlayHistoryService;
@PostConstruct
public void init() {
executorService = Executors.newFixedThreadPool(POOL_SIZE, new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setPriority(THREAD_PRIORITY); // 设置线程优先级
t.setName("VideoTransferThread-" + threadNumber.getAndIncrement());
return t;
}
});
consumeTransferFileQueue();
}
//转码文件队列
public void consumeTransferFileQueue() {
executorService.execute(() -> {
while (true) {
try {
VideoInfoFilePost videoInfoFile = (VideoInfoFilePost) redisUtils.rpop(Constants.REDIS_KEY_QUEUE_TRANSFER);
if (videoInfoFile == null) {
Thread.sleep(1500);
continue;
}
videoInfoPostService.transferVideoFile(videoInfoFile);
} catch (Exception e) {
log.error("获取转码文件队列信息失败", e);
}
}
});
}
@PostConstruct
public void consumeVideoPlayQueue() {
executorService.execute(() -> {
while (true) {
try {
VideoPlayInfoDto videoPlayInfoDto = (VideoPlayInfoDto) redisUtils.rpop(Constants.REDIS_KEY_QUEUE_VIDEO_PLAY);
if (videoPlayInfoDto == null) {
Thread.sleep(1500);
continue;
}
//更新播放数
videoInfoService.addReadCount(videoPlayInfoDto.getVideoId());
if (!StringTools.isEmpty(videoPlayInfoDto.getUserId())) {
//记录历史
videoPlayHistoryService.saveHistory(videoPlayInfoDto.getUserId(), videoPlayInfoDto.getVideoId(), videoPlayInfoDto.getFileIndex());
}
//按天记录播放数
redisComponent.recordVideoPlayCount(videoPlayInfoDto.getVideoId());
//更新es播放数量
esSearchComponent.updateDocCount(videoPlayInfoDto.getVideoId(), SearchOrderTypeEnum.VIDEO_PLAY.getField(), 1);
} catch (Exception e) {
log.error("获取视频播放文件队列信息失败", e);
}
}
});
}
}
这段代码展示了一个名为 ExecuteQueueTask
的类,其功能是通过线程池从 Redis 中拉取任务,并异步执行相关的任务处理。这里的两个主要任务是处理 转码文件队列 和 视频播放队列,它们分别执行相关的业务逻辑。具体分析如下:
1. 类级别注解和属性
@Component
: 使ExecuteQueueTask
类成为一个 Spring Bean,可以被 Spring 容器管理。类会在应用启动时被自动扫描并实例化。@Slf4j
: 自动生成一个log
对象,用于日志记录。THREAD_PRIORITY
: 设置线程池线程的优先级,这里是设置为低优先级 (Thread.MIN_PRIORITY + 1
)。POOL_SIZE
: 线程池的大小,这里为 10,定义了线程池最多能够同时执行 10 个线程。executorService
: 用于管理线程池的ExecutorService
实例。@Resource
: 用于注入 Spring 管理的 Bean。
2. init()
方法
@PostConstruct
: 表示这个方法在类初始化后会被自动调用。executorService
: 初始化为一个固定大小(10)的线程池,使用Executors.newFixedThreadPool
创建。每个线程的优先级被设置为较低(THREAD_PRIORITY
),线程的名称会以"VideoTransferThread-"
开头,并根据递增的数字命名。consumeTransferFileQueue()
和consumeVideoPlayQueue()
方法分别用于消费转码文件队列和视频播放队列。线程池在启动时就会开始执行这两个方法。
3. consumeTransferFileQueue()
方法
这个方法通过一个死循环不断从 Redis 队列中获取待转码的文件,并将其传递给 videoInfoPostService.transferVideoFile(videoInfoFile)
方法进行处理。
redisUtils.rpop(Constants.REDIS_KEY_QUEUE_TRANSFER)
: 从 Redis 中拉取一个待处理的视频转码文件。如果队列为空,则等待 1.5 秒(Thread.sleep(1500)
)后再进行下一次拉取。videoInfoPostService.transferVideoFile(videoInfoFile)
: 处理视频转码文件的逻辑。
如果在获取队列信息或处理文件时发生异常,则记录错误日志。
4. consumeVideoPlayQueue()
方法
这个方法通过一个死循环不断从 Redis 队列中获取视频播放信息,并执行以下任务:
- 增加视频播放数量:
videoInfoService.addReadCount(videoPlayInfoDto.getVideoId())
- 如果用户 ID 不为空,则记录视频播放历史:
videoPlayHistoryService.saveHistory(...)
- 记录视频的播放数(按天计数):
redisComponent.recordVideoPlayCount(videoPlayInfoDto.getVideoId())
- 更新 Elasticsearch 中的视频播放数量:
esSearchComponent.updateDocCount(...)
同样,如果在获取队列信息或执行任务时发生异常,则记录错误日志。
5. 总结
- 线程池管理:通过
ExecutorService
管理多个并发执行的任务,这使得系统能够高效地异步处理视频转码和播放队列任务。 - Redis 队列消费:通过
redisUtils.rpop
从 Redis 中拉取队列任务,确保了任务的高效异步处理。 - 任务处理:针对每个队列任务,分别调用了不同的服务方法(如视频转码、更新播放数等)。
- 容错处理:每个队列任务在发生异常时会捕获并记录日志,但不会阻止后续任务的执行。
可能的改进点
- 死循环和线程阻塞:当前的实现使用了死循环和
Thread.sleep(1500)
来控制队列消费间隔。对于高并发的场景,可以考虑优化为使用异步事件或消息队列来避免长时间的线程阻塞。 - 队列监控和告警:在生产环境中,如果队列为空且线程一直阻塞,可能会导致不必要的资源消耗。可以加上监控和告警机制,提醒管理员队列出现异常或任务滞留。
- 资源管理:线程池的大小是固定的,随着业务增长,可能需要动态调整线程池大小或采用更复杂的调度策略。
这种设计适用于需要高效处理多个队列任务的场景,但随着业务量增长,可能需要优化队列消费方式以及线程池的管理。
使用RabbitMQ进行优化
使用 RabbitMQ 替换 Redis 的消息队列可以更好地管理消息的传递、持久化和消费,并且支持更复杂的消息处理模式(例如:确认机制、消息路由等)。下面是如何将原来使用 Redis 的消息队列替换为 RabbitMQ,并结合你现有的代码做一些修改和优化。
步骤 1:添加 RabbitMQ 相关依赖
首先需要在项目中引入 RabbitMQ 的相关依赖,如果是 Spring Boot 项目,通常使用 spring-boot-starter-amqp
来集成 RabbitMQ。
Maven 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
步骤 2:配置 RabbitMQ
在 application.properties
或 application.yml
中配置 RabbitMQ 的连接信息。
application.properties
示例:
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
application.yml
示例:
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
步骤 3:定义 RabbitMQ 的消息队列、交换机和路由
接下来,我们定义 RabbitMQ 的队列、交换机和路由。通常使用 @Configuration
注解的配置类来定义这些组件。
@Configuration
public class RabbitConfig {
@Bean
public MessageConverter messageConverter() {
// 使用自定义的消息转换器来更严格地处理反序列化
return new Jackson2JsonMessageConverter();
}
// 队列定义
@Bean
public Queue transferFileQueue() {
return new Queue(Constants.RABBITMQ_QUEUE_TRANSFER, true); // durable 确保队列持久化
}
@Bean
public Queue videoPlayQueue() {
return new Queue(Constants.RABBITMQ_QUEUE_VIDEO_PLAY, true);
}
// Direct 类型交换机
@Bean
public DirectExchange directExchange() {
return new DirectExchange("directExchange", true, false); // durable,是否持久化
}
// 队列和交换机的绑定
@Bean
public Binding transferFileBinding(Queue transferFileQueue, DirectExchange directExchange) {
return BindingBuilder.bind(transferFileQueue).to(directExchange).with("transferFileRoutingKey");
}
@Bean
public Binding videoPlayBinding(Queue videoPlayQueue, DirectExchange directExchange) {
return BindingBuilder.bind(videoPlayQueue).to(directExchange).with("videoPlayRoutingKey");
}
}
步骤 4:发送消息到 RabbitMQ(生产者)
现在,你需要将消息发送到 RabbitMQ 队列。与 Redis 中的 rpush
操作类似,RabbitMQ 发送消息需要使用 AmqpTemplate
。
发送转码文件队列消息:
@Service
public class VideoInfoPostService {
@Autowired
private AmqpTemplate amqpTemplate;
public void transferVideoFile( VideoInfoFilePost videoInfoFile) {
// 发送消息到 RabbitMQ
amqpTemplate.convertAndSend("directExchange", "transferFileRoutingKey", videoInfoFile);
}
}
发送视频播放队列消息:
@Service
public class VideoInfoService {
@Autowired
private AmqpTemplate amqpTemplate;
public void recordVideoPlay(VideoPlayInfoDto videoPlayInfoDto) {
// 发送消息到 RabbitMQ
amqpTemplate.convertAndSend("directExchange", "videoPlayRoutingKey", videoPlayInfoDto);
}
}
步骤 5:消费消息(消费者)
消费者会监听 RabbitMQ 中的队列,并处理相关任务。与 Redis 不同,RabbitMQ 提供了更加可靠的消息确认机制。
转码文件队列消费者:
@Service
public class TransferFileQueueConsumer {
@RabbitListener(queues = Constants.RABBITMQ_QUEUE_TRANSFER)
public void consumeTransferFileQueue(@Payload VideoInfoFilePost videoInfoFile) {
try {
videoInfoPostService.transferVideoFile(videoInfoFile);
} catch (Exception e) {
log.error("处理转码文件队列消息失败", e);
}
}
}
视频播放队列消费者:
@Service
public class VideoPlayQueueConsumer {
@RabbitListener(queues = Constants.RABBITMQ_QUEUE_VIDEO_PLAY)
public void consumeVideoPlayQueue(@Payload VideoPlayInfoDto videoPlayInfoDto) {
try {
// 更新播放数、记录历史、更新ES等操作
videoInfoService.addReadCount(videoPlayInfoDto.getVideoId());
videoPlayHistoryService.saveHistory(videoPlayInfoDto.getUserId(), videoPlayInfoDto.getVideoId(), videoPlayInfoDto.getFileIndex());
redisComponent.recordVideoPlayCount(videoPlayInfoDto.getVideoId());
esSearchComponent.updateDocCount(videoPlayInfoDto.getVideoId(), SearchOrderTypeEnum.VIDEO_PLAY.getField(), 1);
} catch (Exception e) {
log.error("处理视频播放队列消息失败", e);
}
}
}
步骤 6:处理消息确认和失败重试(可选)
RabbitMQ 提供了消息确认机制,确保消息被成功消费。如果消费者未能成功消费消息,可以设置重试机制或者将失败的消息转到死信队列。
示例:
@RabbitListener(queues = Constants.RABBITMQ_QUEUE_TRANSFER)
public void consumeTransferFileQueue(VideoInfoFilePost videoInfoFile) {
try {
videoInfoPostService.transferVideoFile(videoInfoFile);
} catch (Exception e) {
log.error("处理转码文件队列消息失败", e);
// 将失败的消息转发到死信队列
// or requeue
}
}
优点:
- 持久化和可靠性:RabbitMQ 提供了更强的消息持久化机制,确保即使服务重启,消息也不会丢失。
- 复杂的路由和交换机模式:RabbitMQ 支持多种交换机类型(如 Direct、Fanout、Topic、Headers),可以灵活地实现不同的消息路由策略。
- 消息确认机制:RabbitMQ 提供了消费端消息确认(acknowledgment)机制,可以确保消息的可靠消费,避免消息丢失。
总结:
通过上述步骤,你可以使用 RabbitMQ 替换 Redis 实现消息队列功能。RabbitMQ 提供了更多的特性(如消息确认、消息持久化、交换机路由等),非常适合处理需要高可靠性和高复杂性的消息传递场景。如果对高并发和实时性有较高要求,RabbitMQ 会是一个更好的选择。