开源项目:银发服务-功能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中
}