天机学堂 第四天 高并发优化总结

avatar
作者
猴君
阅读量:0

前端每隔15秒就发起一次请求,将播放记录写入数据库。

但问题是,提交播放记录的业务太复杂了,其中涉及到大量的数据库操作:

如何进行优化

单机并发能力 变同步为异步 合并写请求

提高单机并发:优化SQL,尽量走索引,避免双重for循环,添加缓存

 提高单机并发能力

变同步为异步

合并写请求

合并写请求方案其实是参考高并发读的优化思路:当读数据库并发较高时,我们可以把数据缓存到Redis,这样就无需访问数据库,大大减少数据库压力,减少响应时间。

既然读数据可以建立缓存,那么写数据可以不可以也缓存到Redis呢?

答案是肯定的,合并写请求就是指当写数据库并发较高时,不再直接写到数据库。而是先将数据缓存到Redis,然后定期将缓存中的数据批量写入数据库。

提交学习记录业务优化

记录是否已经存在放入缓存中(因为每次提交都会去数据库查询),更新学习记录时间放入缓存中

对提交学习记录进行改造,每隔15秒进行一次提交对数据库压力太大,考虑到只有最后一次提交才有效,所以我们对存在数据库操作的地方进行优化

而播放进度信息,不管更新多少次,下一次续播肯定是从最后的一次播放进度开始续播。也就是说我们只需要记住最后一次即可。因此可以采用合并写方案来降低数据库写的次数和频率,而异步写做不到。

 如何设计缓存字段?

用户学习视频的过程中,可能会在多个视频之间来回跳转,这就会导致频繁的创建缓存、缓存过期,影响到最终的业务性能。该如何解决呢?

使用hash key解决

实际操作中可以直接把实体类转化为JSON 当做value存入

 但是存在一定问题

但问题来了,我们怎么知道哪一次提交是最后一次提交呢?

只要用户一直在提交记录,Redis中的播放进度就会一直变化。如果Redis中的播放进度不变,肯定是停止了播放,是最后一次提交。

因此,我们只要能判断Redis中的播放进度是否变化即可。怎么判断呢?

核心思想

每当前端提交播放记录时,我们可以设置一个延迟任务并保存这次提交的进度。等待20秒后(因为前端每15秒提交一次,20秒就是等待下一次提交),检查Redis中的缓存的进度与任务中的进度是否一致。(把数据缓存到redis中,同时设置一个20秒的延迟任务,20秒后执行这个任务,执行这个任务的时候再一次跟redis中的时间比对,如果一样则更新数据库,否则跳过

  • 不一致:说明持续在提交,无需处理

  • 一致:说明是最后一次提交(暂停了或者离开播放了),(提交延迟任务)更新学习记录、更新课表最近学习小节和时间到数据库中

 延迟任务方案

DelayQueue的用法 

package com.tianji.learning.utils;  import lombok.Data;  import java.time.Duration; import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit;  @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;         }     } }

接下来就可以创建延迟任务,交给延迟队列保存:

package com.tianji.learning.utils;  import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test;  import java.time.Duration; import java.util.concurrent.DelayQueue;  @Slf4j class DelayTaskTest {     @Test     void testDelayQueue() throws InterruptedException {         // 1.初始化延迟队列         DelayQueue<DelayTask<String>> queue = new DelayQueue<>();         // 2.向队列中添加延迟执行的任务         log.info("开始初始化延迟任务。。。。");         queue.add(new DelayTask<>("延迟任务3", Duration.ofSeconds(3)));         queue.add(new DelayTask<>("延迟任务1", Duration.ofSeconds(1)));         queue.add(new DelayTask<>("延迟任务2", Duration.ofSeconds(2)));         // TODO 3.尝试执行任务              } }

执行任务

package com.tianji.learning.utils;  import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test;  import java.time.Duration; import java.util.concurrent.DelayQueue;  @Slf4j class DelayTaskTest {     @Test     void testDelayQueue() throws InterruptedException {         // 1.初始化延迟队列         DelayQueue<DelayTask<String>> queue = new DelayQueue<>();         // 2.向队列中添加延迟执行的任务         log.info("开始初始化延迟任务。。。。");         queue.add(new DelayTask<>("延迟任务3", Duration.ofSeconds(3)));         queue.add(new DelayTask<>("延迟任务1", Duration.ofSeconds(1)));         queue.add(new DelayTask<>("延迟任务2", Duration.ofSeconds(2)));         // 3.尝试执行任务         while (true) {             DelayTask<String> task = queue.take();             log.info("开始执行延迟任务:{}", task.getData());         }     } }

 

 开始改造

封装的工具类

延迟队列里面放的就是这一个个DelayTask<T>

@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 @RequiredArgsConstructor @Component public class LearningRecordDelayTaskHandler {      private final StringRedisTemplate redisTemplate;     private final DelayQueue<DelayTask<RecordTaskData>> queue = new DelayQueue<>();     private final static String RECORD_KEY_TEMPLATE = "learning:record:{}";     private final LearningRecordMapper recordMapper;     private final ILearningLessonService lessonService;     private static volatile boolean begin = true;     // 项目启动后 当前类实例化 属性输入之后 方法就会运行 一般用来做初始化工作     @PostConstruct     public void init(){         CompletableFuture.runAsync(this::handleDelayTask);         log.debug("开启 新线程执行handleDelayTask方法");// 开启 新线程执行handleDelayTask方法     }     @PreDestroy  // 当前类是实例 销毁之前该方法执行     public void destroy(){         log.debug("关闭学习记录处理的延迟任务");         begin = false;     }     private void handleDelayTask(){         while (begin){             try {                 // 1.尝试获取任务   take是阻塞方法                 DelayTask<RecordTaskData> task = queue.take();                 log.debug("获取到要处理的播放记录任务");                 RecordTaskData data = task.getData();                 // 2.读取Redis缓存                 LearningRecord record = readRecordCache(data.getLessonId(), data.getSectionId());                 log.debug("获取到要处理的播放记录任务,任务数据{}  缓存中的数据{}",data,record);                 if (record == null) {                     continue;                 }                 // 3.比较数据                 if(!Objects.equals(data.getMoment(), record.getMoment())){                     // 4.如果不一致,播放进度在变化,无需持久化                     continue;                 }                 // 5.如果一致,证明用户离开了视频,需要持久化                 // 5.1.更新学习记录                 record.setFinished(null);                 recordMapper.updateById(record);                 // 5.2.更新课表                 LearningLesson lesson = new LearningLesson();                 lesson.setId(data.getLessonId());                 lesson.setLatestSectionId(data.getSectionId());                 lesson.setLatestLearnTime(LocalDateTime.now());                 lessonService.updateById(lesson);                  log.debug("准备持久化学习记录信息");             } catch (Exception e) {                 log.error("处理播放记录任务发生异常", e);             }         }     }      public void addLearningRecordTask(LearningRecord record){         // 1.添加数据到Redis缓存         writeRecordCache(record);         // 2.提交延迟任务到延迟队列 DelayQueue         queue.add(new DelayTask<>(new RecordTaskData(record), Duration.ofSeconds(20)));     }      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);         }     }      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;         }     }      public void cleanRecordCache(Long lessonId, Long sectionId){         // 删除数据         String key = StringUtils.format(RECORD_KEY_TEMPLATE, lessonId);         redisTemplate.opsForHash().delete(key, sectionId.toString());     }     // 缓存实体类     @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();         }     }     // 任务实体类     @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();         }     } } 

使用线程池来处理任务

目前我们的延迟任务执行还是单线程模式,大家将其改造为线程池模式,

 拉取方法 还是使用哪个开辟的线程去拉取,但是拉取完之后的执行让线程池里面的来执行

 面试题

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!