Java 篇-项目实战-天机学堂(从0到1)-day4
java 篇 1.基础地基 2.设计原理 3.项目实战学习建议先看视频跟着学再看文档自己敲不会地方到处问问完总结记笔记。高并发方案分析这章涉及高并发优化建议先看下配套的文档资料。这里我自己总结了一下首先原来的提交学习记录流程涉及多处数据库新增和更新操作且是同步。那这里呢就有两种方案第一种用 MQ 变同步为异步减频率不减次数。第二种合并写请求那具体选哪种呢取决于具体的业务因为合并写代码写起来复杂一些如果业务简单可以采用这种如果业务复杂还是采用 MQ。那看我们这里的业务对于考试是不是低频的那第一次学完是不是也是低频的那什么是高频的查记录是否存在更新学习记录更新课表最近学习小节这些就是高频的。那在想想为啥要更新更新啥是不是就是时间和状态呀因为视频播放会每隔时间去记录当前的记录时间那这里是不是就想到用合并写请求了因为只有最后一次提交是有效的并不需要每次都写数据库。所以优化的就是下面绿色的线路那查记录肯定得用 redis,在内存当中查那用哪个结构呢需要存哪些字段呢是不是很容易想到哈希呀。那这里一个小节作为一个 key,一个课有多个小节一个用户有多个课。全宇宙不止一个用户那 key 的数量是不是爆炸了so改进了下这里的 key 用 lessonId锁定的是用户和课不是小节了。有了 redis 之后流程得变下原来更新都是在数据库当中的如果不是第一次学完就直接更新 redis 然后异步刷就好了当第一次学完才需要到数据库改 finished 相关字段。在针对异步持久化处理关键就是刷盘的时机这里可以再去看下文档资料理解下。这里采用延迟任务就是拿本地和 redis 比较那什么时候比较肯定不是一开始就去比较呀redis 缓存都还没刷新呢所以得至少等一个刷新 Redis 的周期这个就叫延迟任务考虑到网络延时之类的最好再晚几秒。比如刷新 redis 周期为 15s那检测就 20s那延迟任务的实现方案常见有以下四种这里先采用 DelayQueue 实现新建个工具类DelayQueue 需要 Delayed 参数先看下 Delayed 接口注意这是接口只能去 implements 实现有个 getDelay()返回的是 long 类型getDelay() 返回距离目标执行时间还有多久。时间间隔剩余时间。那它又继承了 Comparable 接口Comparable 接口让对象能够与自身类型比较大小实现排序功能。返回值含义负数 (0)this o (当前对象小于参数)零 (0)this o (相等)正数 (0)this o (当前对象大于参数)所以我们的 DelayTaskj 继承 Delayed 还得实现这两个接口按 Altins,自动构建。那 getDelay()返回的是剩余时间所以就是两个时刻相减。所以得定义个死亡时刻然后拿这个减去 System.nanoTime(),这里为纳秒级别你可以把 System.nanoTime()理解成时间戳但它主要用于计算间隔。那这里可能为负数所以得加个 Math.max(0, )。但是如果只是注释那样写那它传的 unit 就没用了那 unit 的作用就是当你调用这个方法时可以指定单位。unit.convert(值, 值的单位)内部的值以及其对应的单位调用的时候指定你要的单位然后是 compareTo()方法实现返回的是 int 类型根据正负号进行排序。如果当前得到剩余时间比传入目标的大就输出 1剩余时间小的排前面。这里为啥不直接输出 l 呢因为一个是 long 类型的一个是 int 类型的如果直接强转那除了传剩余时间那它是不是还得传一些数据呀这里呢就用到了泛型那这里含参构造方法你想想看deadlineNanos 是时刻不能每次都去计算再传进去吧so 这里只传入时间间隔就行了注意单位.toNanos()转换一下完整代码如下之后进行单元测试在 src 下新建目录你就能看到 test 了然后按 Altins,构造测试方法看下 DelayQueue 的源码里面传的元素继承了 Delayed 实现了 BlockingQueue这里就是 DelayTask.不是你刚才不是说 Delayed 是接口怎么这里用 extends 呀因为呀这是泛型上下界的语法规则然后加 Slf4j 打日志看时间因为你不知道里面的任务什么时候发出来所以得 while 循环。queue.take()去拿这里可能阻塞需要抛异常。只要方法让线程进入等待状态睡眠、等待、阻塞、超时基本都会抛出 InterruptedException。代码改造添加播放记录到缓存并添加延迟任务业务比较复杂所以这里定义一个工具类一个方法对应一个业务操作。先干 ①可以分为两个部分第一个添加数据到 Redis 缓存第二个提交延迟任务到队列因为这是写入操作所以是 void那这个工具里面的方法得暴露出来给别人用所以用 public。因为添加数据库操作很普遍你更新数据库删了缓存后面写 Redis 又得用到这个了所以也把它搞成一个方法。我们对着 Redis 当中数据结构看这里用到键值都是 String 类型遇到 Object 变 json那最简单的就是 hashKey,直接通过 record.getLessonId()获得记得转为 String 类型那针对于 KEY它前缀都有固定写法,模块:业务:{id} 前面都是 private final static 写死的后面可能会把这些归到一个类里面那格式有了就用 StringUtils.format(前缀,id)那之后就是 HashValue,如果一个个转成 map不优雅所以这里得定义个里面属性放需要的字段。这个就单纯属于这个工具类用的所以用 private static,这里含餐构造方法不用传三个就给 record 就 o 了ok然后进行 Json 序列话JsonUtils._toJsonStr_(new RecordCacheData(record))然后就是将这三部分写入redisTemplate.opsForHash().put(key, record.getLessonId().toString(), json);注意写法.opsForHash().方法最后别忘了加 ttl,redisTemplate.expire(key, Duration._ofMinutes_(1));这里传入的是 Duration之后就是异常处理打日志try-catch 捕获 ctlaltt第一部分干掉开始干第二部分用到队列先 new 一个队列前面 RedisTemplete 自动注入的。DelayQueue 需要 DelayTask那 DelayTask 当中的泛型又需要一个类。那这个类长啥样呢这个队列的作用就是去看 Redis 当中的数据有木有变化那根据这张表是不是得有 lessonId ,sectionId 去定哪条数据呀那怎么判断这条有木有变呢是不是就是看 moment 呀跟前面一样传入需要的参数搞定代码改造播放记录缓存的读取和清除方法开始干 ②记录是否存在就是去 redis 当中查数据不存在返回 null首先明白读读的是什么数据读的是缓存当中的 HashValue 数据对应的 RecordCacheData,就三个字段id、moment、finished,够用吗够用查记录是否存在看有木有 id,是第一次学完更 finished,不是第一次学完更 moment。那这里为啥用 LearningRecord 呢因为如果缓存没有去查数据库返回类型保持一致那传入的参数是什么呢那不就是 redis 当中的 KEY 和 HashKey 嘛记得 redisTemplate 里面所需要的参数都是 String 类型cacheData 是 Json 对象这里用 JsonUtils 把它转为 LearningRecord 类型并返回注意里面的参数也是 String 类型。最后加上健壮性处理和日志try-catch 异常处理。然后是删除缓存 ③这个没啥好说的代码改造异步执行延迟任务开始干 ④先拆分流程步骤1.拿到期的延迟任务 2.查 redis 3.比较 moment 4.一致怎么处理不一致又怎么处理先干 1首先延迟任务直接到队列里面取queue.take(),这里可能阻塞所以得套 try-catch那这里呢是把整个流程都包住了记录的异常就是全部异常Exception e。那为啥这里不用判断 task 是否为 null 呢前面已经说了它会阻塞。2.查 redis之前已经写好的方法直接用。如果为 nullcontinueredis 存的是最新的数据当中没有也就不需要执行操作了这里不能 returnreturn 就跳出 while 循环了其他的任务还没处理呢。这里还有个小细节因为 RecordTaskData 是内部静态类嘛那传的参数可以直接 data.lessonId、data.sectionId 传进去但不推荐。3.比较数据比较 moment因为是 Integer 类型比较所以得用.equals(), 当然你写 时候它也会设黄提醒。如果不一致说明用户还在继续播放不需要处理过。如果一致就需要更新两张表这里先注入进来为啥一个用 Mapper一个用 Service 呢因为 recordService 肯定得调用这个工具类把那这就循环依赖了。而操作 lesson 是异步的所以 lessonService 不会主动调用它所以用 Service.这里设置最近学习信息当中最近学习时间设置为当前时间理论时间为当前时间-20s但没啥关系因为进度看的是 moment。然后两者都用内置的.updateById()方法为啥这里设置 finished 为 null 呢因为单一职责只负责更新记录 moment,finished 由另外的业务逻辑单独处理。lesson 表没啥好说的把该变的值赋给他就行。现在这个方法实现了那它什么时候开始执行呢是不是这个当前类被加载队列创建好之后就开始执行直到整个项目销毁才结束啊。那怎么去实现呢1.使用 PostConstruct2.实现 InitializingBean 接口这里采用第一种方式注意这里不能直接调用 handleDelayTask 方法因为它也属于 Spring 周期的一环这里死循环阻塞了就卡住了。所以得采用异步的方式CompletableFuture._runAsync_(this::handleDelayTask);里面接收的参数是 Runnable 接口Runnable 的特点特点说明没有返回值void run()不能抛出 checked 异常只能在内部处理函数式接口可以用 Lambda 或方法引用然后这里加了静态变量 begin 更优雅地关闭线程private static volatile boolean _begin _ true; 这里不能加 final 因为和 volatile 互斥一个不可变一个可变保证线程可见性。ok,延迟任务工具完成最后把它注入到 Spring 容器当中创建 Bean 初始化供其他类用到自动注入。代码改造改造提交学习记录接口先放个原来的流程和现在的流程作为参照再看原来的代码因为考试为低频所以不用动。那我们先去干处理视频的方法原先呢是直接从数据库当中查现在得先查缓存如果缓存命中直接返回如果未命中查数据库写缓存返回。这里有个小问题一个是 record 可能为 null,空指针异常。这里先不管了缓存穿透缓存空值就好了先过。然后写入缓存应该还可以通过 CachePut 来实现查到 old 以后进行判断如果 old 为 null都是新增学习记录不用改不为 null,再看是否第一次完成如果是就执行第一次操作然后返回 false. 这里还有个疑问为啥不直接用 old然后改个 moment这个就不知道了。如果不是第一次后面都执行更新学习记录操作最后别忘了清理缓存最后我们进行测试redis 的配置看 nacos因为这里不支持试看所以理论上应该是当打开一个还没看的小节里面的 finished 为 false当进度超过一半要更新数据库后台有记录然后删缓存之后在打开点击继续学习这条记录才会出现就如上图所示。后续改进这里可以采用线程池并行处理这里可以改为 MQ,或 Redisson如果对你有帮助的话请点赞关注收藏。热爱可抵一切 ❤️