开源项目:银发服务-功能1-视频的秒级续播

开源项目:银发服务-功能1-视频的秒级续播

前言:先介绍一下项目的背景,作者目前大二,已经大体学完市面的后端技术栈,为了对自己的就业有帮助,我缝合了多个项目的特点,构思出一个老年人服务的项目,因为作者目前还不会前端,所以只能提供后端代码,但每个功能都是各大项目的两点,以后如果写项目可以大胆缝合!等作者学会前端之后会一一把功能全部实现,只不过工作量貌似有点大,哈哈。

目前想到的技术点:

1.视频的秒级续播,后续的面对面服务,保险服务都会有视频介绍功能,所以优化视频播放舒适度

2.保险服务,设计很多比较复杂的业务,表就有很多张

3.支付服务,我会尽量写的牛逼一点,综合很多开源方案

4.实时沟通服务,这个比较简单,WebSocket,但可能会牵扯到计网八股的引入,所以也会发布

5.统一的鉴权微服务模块,我也会结合很多方案,尽量做好

6.数据采集功能,对热门服务数据进行采集

7.一个物联网检测功能,检测老人的生活

8.住宿服务,也相当于保险的一个业务

其他:后续的项目我都会提供实现的功能演示,只不过时间很长,作者能力有限,可能在后续的过程中有很多代码或理解的错误,希望各位大神不吝赐教!

视频播放的优化

DelayQueue延迟队列的方案(单线程)

对延迟队列认识

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>

可见只有继承了Delayed类的方案才能使用DelayQueue,所以我们需要自己定义一个延迟任务类

对延迟任务类的定义

@Data
public class DelayTask<D> implements Delayed {
    //数据
    private D data;
    //超时时间
    private long deadlineNanos;

    public DelayTask(D data, Duration delayTime) {
        this.data = data;
        this.deadlineNanos = System.nanoTime() + delayTime.toNanos();
    }

    //该方法用于返回任务剩余的延迟时间
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(Math.max(0, deadlineNanos - System.nanoTime()), TimeUnit.NANOSECONDS);
    }

    //未使用,但实现接口必须实现
    @Override
    public int compareTo(Delayed o) {
        long l = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
        if(l > 0){
            return 1;
        }else if(l < 0){
            return -1;
        }else {
            return 0;
        }
    }
}

对延迟任务类的编写

@Slf4j
@Component
@RequiredArgsConstructor
public class LearningRecordDelayTaskHandler {

    private final StringRedisTemplate redisTemplate;
    private final LearningRecordMapper recordMapper;
    private final ILearningLessonService lessonService;
    //创建了一个唯一的实例
    private final DelayQueue<DelayTask<RecordTaskData>> queue = new DelayQueue<>();
    private final static String RECORD_KEY_TEMPLATE = "learning:record:{}";
    private static volatile boolean begin = true;

    //Bean成功注册,即异步开始handleDelayTask的调用 
    @PostConstruct
    public void init() {
        CompletableFuture.runAsync(this::handleDelayTask);
    }

    //Bean销毁的时候将begin重新恢复false
    @PreDestroy
    public void destroy() {
        begin = false;
        log.debug("延迟任务停止执行!");
    }
    
//异步执行的任务,一直比较Redis中的数据,当前端停止发送数据的时候,即会查询到延迟队列中的数据和Redis中的一致,此时这个数据就是
//我们需要保存的数据
    public void handleDelayTask() {
        while (begin) {
            try {
                // 1.获取到期的延迟任务
                DelayTask<RecordTaskData> task = queue.take();
                RecordTaskData data = task.getData();
                // 2.查询Redis缓存
                LearningRecord record = readRecordCache(data.getLessonId(), data.getSectionId());
                if (record == null) {
                    continue;
                }
                // 3.比较数据,moment值
                if (!Objects.equals(data.getMoment(), record.getMoment())) {
                    // 不一致,说明用户还在持续提交播放进度,放弃旧数据
                    continue;
                }
                // 4.一致,持久化播放进度数据到数据库
                // 4.1.更新学习记录的moment
                record.setFinished(null);
                recordMapper.updateById(record);
                // 4.2.更新课表最近学习信息
                LearningLesson lesson = new LearningLesson();
                lesson.setId(data.getLessonId());
                lesson.setLatestSectionId(data.getSectionId());
                lesson.setLatestLearnTime(LocalDateTime.now());
                lessonService.updateById(lesson);
            } catch (Exception e) {
                log.error("处理延迟任务发生异常", e);
            }
        }
    }
//每次前端发来的数据,我们要同时保存在Redis和延迟队列中,但延迟队列中的数据20s之后才会被拿出来比较,具有延时比较性
    public void addLearningRecordTask(LearningRecord record) {
        // 1.添加数据到Redis缓存
        writeRecordCache(record);
        // 2.提交延迟任务到延迟队列 DelayQueue
        queue.add(new DelayTask<>(new RecordTaskData(record), Duration.ofSeconds(20)));
    }
//具体保存在Redis中的方法,设置了过期时间,防止数据太多
    public void writeRecordCache(LearningRecord record) {
        log.debug("更新学习记录的缓存数据");
        try {
            // 1.数据转换
            String json = JsonUtils.toJsonStr(new RecordCacheData(record));
            // 2.写入Redis
            String key = StringUtils.format(RECORD_KEY_TEMPLATE, record.getLessonId());
            redisTemplate.opsForHash().put(key, record.getSectionId().toString(), json);
            // 3.添加缓存过期时间
            redisTemplate.expire(key, Duration.ofMinutes(1));
        } catch (Exception e) {
            log.error("更新学习记录缓存异常", e);
        }
    }
//循环中需要一直比较Redis中的数据,此方法就是Redis数据的读取方法
    public LearningRecord readRecordCache(Long lessonId, Long sectionId) {
        try {
            // 1.读取Redis数据
            String key = StringUtils.format(RECORD_KEY_TEMPLATE, lessonId);
            Object cacheData = redisTemplate.opsForHash().get(key, sectionId.toString());
            if (cacheData == null) {
                return null;
            }
            // 2.数据检查和转换
            return JsonUtils.toBean(cacheData.toString(), LearningRecord.class);
        } catch (Exception e) {
            log.error("缓存读取异常", e);
            return null;
        }
    }
//在成功保存数据的时候,我们需要删除Redis中的缓存    
    public void cleanRecordCache(Long lessonId, Long sectionId) {
        // 删除数据
        String key = StringUtils.format(RECORD_KEY_TEMPLATE, lessonId);
        redisTemplate.opsForHash().delete(key, sectionId.toString());
    }
//Redis数据的转化操作,方便我们保存数据到Redis中
    @Data
    @NoArgsConstructor
    private static class RecordCacheData {
        private Long id;
        private Integer moment;
        private Boolean finished;

        public RecordCacheData(LearningRecord record) {
            this.id = record.getId();
            this.moment = record.getMoment();
            this.finished = record.getFinished();
        }
    }
//延迟队列中具体的数据,与Redis中的数据保持一致
    @Data
    @NoArgsConstructor
    private static class RecordTaskData {
        private Long lessonId;
        private Long sectionId;
        private Integer moment;

        public RecordTaskData(LearningRecord record) {
            this.lessonId = record.getLessonId();
            this.sectionId = record.getSectionId();
            this.moment = record.getMoment();
        }
    }

调用部分

//注入
@RequiredArgsConstructor
private final LearningRecordDelayTaskHandler taskHandler; 
//调用
taskHandler.writeRecordCache(record);

实例展示

@SpringBootTest
class LearningRecordDelayTaskHandlerTest {

    @Resource
    private LearningRecordDelayTaskHandler learningRecordDelayTaskHandler;

    @Test
    void tset() throws InterruptedException {
        // 创建 LearningRecord 对象并设置各个字段
        LearningRecord record = new LearningRecord();
        record.setLessonId(1L);
        record.setSectionId(3L);
        record.setMoment(18);
        record.setUserId(2L);
        record.setId(1874982265395179525l);
        record.setFinished(true);
        //将record 添加到任务处理程序中
        learningRecordDelayTaskHandler.addLearningRecordTask(record);

        //将线程阻塞30s,保证延迟队列能成功运行
        Thread.sleep(30000); // 确保超过延迟队列的 20 秒设置
    }
}

DelayQueue延迟队列的方案(线程池)

线程池的配置

@Configuration
public class ThreadPoolConfig {
    @Bean("delayTaskExecutor")
    public Executor delayTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数(根据机器 CPU 核数调整)
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() * 2);
        // 最大线程数(建议设置上限避免资源耗尽)
        executor.setMaxPoolSize(50);
        // 队列容量(缓冲突发流量)
        executor.setQueueCapacity(1000);
        // 线程名前缀(方便日志排查)
        executor.setThreadNamePrefix("delay-task-");
        // 拒绝策略(记录日志并丢弃任务)
        executor.setRejectedExecutionHandler((r, e) -> 
            log.error("任务被拒绝,队列已满. Task: {}", r.toString())
        );
        // 允许核心线程超时销毁(避免空闲资源浪费)
        executor.setAllowCoreThreadTimeOut(true);
        // 线程空闲时间(秒)
        executor.setKeepAliveSeconds(60);
        executor.initialize();
        return executor;
    }
}

完整任务实现

@Slf4j
@Component
@RequiredArgsConstructor
public class LearningRecordDelayTaskHandler {

    // 原有依赖注入
    private final StringRedisTemplate redisTemplate;
    private final LearningRecordMapper recordMapper;
    private final ILearningLessonService lessonService;
    private final DelayQueue<DelayTask<RecordTaskData>> queue = new DelayQueue<>();

    // 线程池配置
    @Autowired
    @Qualifier("delayTaskExecutor")
    private ThreadPoolTaskExecutor delayTaskExecutor;

    // 使用 AtomicBoolean 替代 volatile boolean
    private final AtomicBoolean running = new AtomicBoolean(true);

    @PostConstruct
    public void init() {
        // 启动多个消费者线程(提升队列消费速度)
        int consumerThreads = 4; // 根据需求调整
        for (int i = 0; i < consumerThreads; i++) {
            CompletableFuture.runAsync(this::handleDelayTask, delayTaskExecutor);
        }
    }

    @PreDestroy
    public void destroy() {
        // 1. 停止任务拉取
        running.set(false);
        // 2. 优雅关闭线程池(等待正在执行的任务完成)
        delayTaskExecutor.shutdown();
        try {
            if (!delayTaskExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
                log.warn("线程池未在30秒内关闭,强制终止");
                delayTaskExecutor.shutdownNow();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            delayTaskExecutor.shutdownNow();
        }
        log.debug("延迟任务处理已停止");
    }

    public void handleDelayTask() {
        while (running.get()) {
            try {
                DelayTask<RecordTaskData> task = queue.take();
                // 提交到线程池处理(带异常处理)
                CompletableFuture.runAsync(() -> processTask(task), delayTaskExecutor)
                        .exceptionally(ex -> {
                            log.error("线程池任务执行异常", ex);
                            return null;
                        });
            } catch (InterruptedException e) {
                log.warn("延迟任务处理线程被中断");
                Thread.currentThread().interrupt();
                break; // 主动退出循环
            }
        }
    }

    private void processTask(DelayTask<RecordTaskData> task) {
        try {
            RecordTaskData data = task.getData();
            // 1. 查询Redis缓存
            LearningRecord record = readRecordCache(data.getLessonId(), data.getSectionId());
            if (record == null) return;

            // 2. 比较数据版本
            if (!Objects.equals(data.getMoment(), record.getMoment())) {
                log.debug("数据已过期,放弃处理. TaskID: {}", data.getSectionId());
                return;
            }

            // 3. 更新数据库
            record.setFinished(null);
            recordMapper.updateById(record);

            // 4. 更新课表信息(带重试机制)
            executeWithRetry(() -> {
                LearningLesson lesson = new LearningLesson();
                lesson.setId(data.getLessonId());
                lesson.setLatestSectionId(data.getSectionId());
                lesson.setLatestLearnTime(LocalDateTime.now());
                lessonService.updateById(lesson);
            }, 3); // 最大重试3次

        } catch (Exception e) {
            log.error("处理任务发生异常. TaskID: {}", task.getData().getSectionId(), e);
        } finally {
            // 清理缓存(可选)
            cleanRecordCache(task.getData().getLessonId(), task.getData().getSectionId());
        }
    }

    // 带重试机制的通用执行方法
    private void executeWithRetry(Runnable action, int maxRetries) {
        int retries = 0;
        while (retries < maxRetries) {
            try {
                action.run();
                return;
            } catch (Exception e) {
                retries++;
                log.warn("操作失败,开始第 {} 次重试. 异常: {}", retries, e.getMessage());
                if (retries >= maxRetries) {
                    throw e;
                }
                try {
                    Thread.sleep(1000 * retries); // 指数退避
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
    // 其他方法保持不变(writeRecordCache/readRecordCache等)
}

死信队列实现

在这里插入图片描述

死信队列的配置

@Configuration
public class RabbitMQConfig {

    //普通交换机的名称
    public final static String X_EXCHANGE = "X";
    //死信交换机的名称
    public final static String Y_DEAD_LETTER_EXCHANGE = "Y";
    //普通队列的名称
    public final static String QUEUE_A = "QA";
    //死信队列的名称
    public final static String DEAD_LETTER_QUEUE = "QD";

    //声明普通交换机 xExchange 别名
    @Bean("xExchange")
    public DirectExchange xExchange() {
        return new DirectExchange(X_EXCHANGE);
    }

    //声明死信交换机 yExchange 别名
    @Bean("yExchange")
    public DirectExchange yExchange() {
        return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
    }

    //声明普通队列 TTL 为10s
    @Bean("queueA")
    public Queue queueA() {
        //正常队列绑定死信交换机信息,因为正常队列消息会变为死信
        Map<String, Object> arguments = new HashMap<>();
        //正常队列设置死信交换机 参数 key 是固定值
        arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
        //正常队列设置死信 routing-key 参数 key 是固定值
        arguments.put("x-dead-letter-routing-key", "YD");
        //设置过期时间 10s,单位是ms,可以在消费者正常队列处设置,也可以在生产者发送处设置
        arguments.put("x-message-ttl", 20000);
        return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build();
    }

    //声明死信队列
    @Bean("queueD")
    public Queue queueD() {
        //没有参数
        return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();
    }

    //将 X 正常交换机和 QA 队列 绑定
    @Bean
    public Binding queueABindingX(@Qualifier("queueA") Queue queueA,
                                  @Qualifier("xExchange") DirectExchange xExchange) {
        return BindingBuilder.bind(queueA).to(xExchange).with("XA");
    }

    //将 Y 死信交换机和 QD 死信队列 绑定
    @Bean
    public Binding queueABindingY(@Qualifier("queueD") Queue queueD,
                                  @Qualifier("yExchange") DirectExchange yExchange) {
        return BindingBuilder.bind(queueD).to(yExchange).with("YD");
    }  
}

生产者

 public void RabbitMqTest() {
        //1.实验数据
        LearningRecord record = new LearningRecord();
        record.setLessonId(1L);
        record.setSectionId(3L);
        record.setMoment(18);
        record.setUserId(2L);
        record.setId(1874982265395179525l);
        record.setFinished(true);
        //2.发送到Redis中
        // 数据转换
        String json = JsonUtils.toJsonStr(new RecordTaskData(record));
        // 写入Redis
        String key = StringUtils.format("learning:record:{}", record.getLessonId());
        redisTemplate.opsForHash().put(key, record.getSectionId().toString(), json);
        // 添加缓存过期时间
        redisTemplate.expire(key, Duration.ofMinutes(1));
        //3.发送到延迟队列中
        rabbitTemplate.convertAndSend("X", "XA", json);
    }

消费者

不提供完整代码

 @RabbitListener(queues = "QD")
    public void received(Message message) throws Exception{
        //1.检查Redis中的值
        //2.若和死信队列中接收的值不一致则结束,因为队列的消费时间始终大于前端发送的时间,只要前端在发送,死信队列拿到的信息永远是滞后的。
        //3.若一致则保存在Mysql中
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值