libilibi项目总结(13)异步视频转码和播放数量统计

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 中拉取队列任务,确保了任务的高效异步处理。
  • 任务处理:针对每个队列任务,分别调用了不同的服务方法(如视频转码、更新播放数等)。
  • 容错处理:每个队列任务在发生异常时会捕获并记录日志,但不会阻止后续任务的执行。

可能的改进点

  1. 死循环和线程阻塞:当前的实现使用了死循环和 Thread.sleep(1500) 来控制队列消费间隔。对于高并发的场景,可以考虑优化为使用异步事件或消息队列来避免长时间的线程阻塞。
  2. 队列监控和告警:在生产环境中,如果队列为空且线程一直阻塞,可能会导致不必要的资源消耗。可以加上监控和告警机制,提醒管理员队列出现异常或任务滞留。
  3. 资源管理:线程池的大小是固定的,随着业务增长,可能需要动态调整线程池大小或采用更复杂的调度策略。

这种设计适用于需要高效处理多个队列任务的场景,但随着业务量增长,可能需要优化队列消费方式以及线程池的管理。

使用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.propertiesapplication.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 会是一个更好的选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值