可以从三个方面来考虑
1.减少接口的响应时间
2.部署多个服务,负载均衡请求
3.sentinel服务熔断,请求控制
在我们实现的业务中,我们发现在提交学习记录的请求需要发好多次,如果并发量上来的话,会搞垮我们的服务。
大家看业务流程图

其实访问最多的业务就是更新学习记录中的不是第一次学完,更新学习小节时间的业务,其他都只是第一次
如何优化呢?
两种方案
1.利用mq异步处理
这是以前的流程

利用mq之后
接口的响应立即返回,减少了接口的响应时间
利用了mq的削峰填谷,大量的请求放到mq中,就像一个大坝一样,然后消费者只需要在自己能承受范围之内取消息,并不需要承受自己不能承受的,降低了写的频率

总结:优点:1.减少了响应时间
2.利用mq缓存消息,削峰填谷
3.降低写频率,减少数据库压力
缺点:1.需要依靠mq的可靠性
2.虽然对数据库的压力变小了,只是降低了写频率,并没有减少写的次数
2.利用redis
第二种方案就是利用redis来缓存插入的数据,然后再取最后一次操作数据库

对于我们的业务要不断更新数据库中的字段来说,我们只需要最后一次写入数据库就行
总结:优点:1.写缓存速度快,减少了数据库压力
2.降低了数据库写频率和写次数
缺点:1.实现复杂
2.依赖缓存redis可靠性
3.不支持事务和复杂业务
对于复杂的业务,对数据要保证一致性,redis没有回滚机制,所以这就是缺点
对于我们的业务大家再来看一下

只需要改造不是第一次学完的,更新学习记录的业务即可
思考这个业务应该采用那种方案改造呢
如果使用第一种,可以保证数据一致性,那么我们的业务仅仅是简单的更新学习记录的时间不涉及到复杂的业务,所以我们选择第二种方式
如何改造呢
思路
在进行学习记录的查询的时候,我们应该将学习记录的时刻信息写入redis,先查询redis,如果没有,然后查询数据库,也没有的话就是需要新增学习记录。
我们应该选redis数据结构呢
最先想到的是hash,用小节id作为key,value中存储键值对
实际上是不行的,多个用户有可能小节id相同,而且小节太多导致存储的太多
所以选用lesson_id作为key,value中的key为小节id,只需要增加key就行
选好了数据结构
思考业务的流程

根据finished字段判断是否是第一次学完
如果在第一次学完的时候把数据库的finished字段改为true,但是redis中的字段还是false,会导致,下一次重复小节数量加1,所以也要更新缓存,但是如果是更新缓存,在有错误的时候,触发了回滚,finished本来改为了true,回滚为false,但是redis中回滚不了,又会导致数据不一致,所以要清理redis缓存,即使回滚redis数据也被删除了,不会导致数据不一致性
更改业务
我们要更改的业务就有这四个方面
1.首先要更改的是业务1,不能频繁的修改数据库,所以改为修改缓存,然后落库
2.如果弄成定时任务,时间如果太长,会导致用户提交的时候没能保存到数据库,时间太短
会很频繁,我们可以用延迟任务,比如在提交了一个时间后,先保存在本地,然后再延迟20
s后,查看缓存中最新的数据是否和本次保存的一样,因为这个请求是每隔15s提交一次,所以
考虑到网落延迟,我们20s之后再判断是否一致,就能知道用户最后到底是不看了,还是继续观看
3.查询也是高频,会给数据库带来巨大压力,所以我们优先查询缓存,如果没有再查数据库,
数据库没有就是新增。
4.如果在第一次学完的时候把数据库的finished字段改为true,但是redis中的字段还是false,会导致,下一次重复小节数量加1,所以也要更新缓存,但是如果是更新缓存,在有错误的时候,触发了回滚,finished本来改为了true,回滚为false,但是redis中回滚不了,又会导致数据不一致,所以要清理redis缓存,即使回滚redis数据也被删除了,不会导致数据不一致性

封装的代码如下
@RequiredArgsConstructor
@Slf4j
@Component
public class LearningRecordDelayTaskHandler {
private final StringRedisTemplate redisTemplate;
private final static String RECORD_KEY_TEMPLATE="learning:record:{}";
private final DelayQueue<DelayTask<RecordTaskData>> queue=new DelayQueue<>();
private final LearningRecordMapper learningRecordMapper;
private final LearningLessonService learningLessonService;
public static volatile boolean begin = true;
@PostConstruct
public void init(){
CompletableFuture.runAsync(this::handleDelayTask);
}
@PreDestroy
public void destroy(){
begin = false;
log.debug("延迟任务停止!");
}
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(record.getMoment(),data.getMoment())){
//不一致,说明用户还在持续提交播放进度,放弃旧数据
continue;
}
//4.一致,持久化数据到数据库
//4.1更新学习记录的moment
record.setFinished(null);
learningRecordMapper.updateById(record);
//4.2更新课表最近学习的信息
LearningLesson learningLesson = new LearningLesson();
learningLesson.setId(data.getLessonId());
learningLesson.setLatestSectionId(data.getSectionId());
learningLesson.setLatestLearnTime(LocalDateTime.now());
learningLessonService.updateById(learningLesson);
} catch (InterruptedException e) {
log.error("处理延迟任务发送异常",e);
}
}
}
/*
* 缓存到redis
* 提交延迟任务
* */
public void addLearningRecordTask(LearningRecord learningRecord){
//1.添加数据到Redis缓存
writeRecordCache(learningRecord);
//2.添加延迟任务到队列
queue.add(new DelayTask<>(Duration.ofSeconds(20),new RecordTaskData(learningRecord)));
}
/*
* 查询缓存,没有查数据库
* */
public LearningRecord readRecordCache(Long lessonId,Long sectionId){
try {
//1.读取redis数据
String key= StringUtils.format(RECORD_KEY_TEMPLATE,lessonId);
Object cache = redisTemplate.opsForHash().get(key, sectionId.toString());
if (cache==null){
return null;
}
//2.数据检查与转换
LearningRecord record = JsonUtils.toBean(cache.toString(), LearningRecord.class);
return record;
} 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());
}
public void writeRecordCache(LearningRecord learningRecord) {
log.info("更新学习记录缓存数据");
try {
//1.数据转换
RecordCacheData recordCacheData = new RecordCacheData(learningRecord);
String jsonStr = JsonUtils.toJsonStr(recordCacheData);
//2.写入redis
String key= StringUtils.format(RECORD_KEY_TEMPLATE,learningRecord.getLessonId());
redisTemplate.opsForHash().
put(key,learningRecord.getSectionId().toString(),jsonStr);
//3.添加缓存过期时间
redisTemplate.expire(key, Duration.ofMinutes(1));
} catch (Exception e) {
log.error("更新学习记录缓存异常",e);
}
}
/*
* 这个类是用来存储学习记录的缓存数据
* */
@Data
@NoArgsConstructor
private static class RecordCacheData{
private Long id;
private Integer moment;
private Boolean finished;
public RecordCacheData(LearningRecord learningRecord) {
this.id = learningRecord.getId();
this.moment = learningRecord.getMoment();
this.finished = learningRecord.getFinished();
}
}
/*
* 这个类是用来在延迟队列中存值,并且查询redis比较数据是否一致
* */
@Data
@NoArgsConstructor
private static class RecordTaskData{
private Long lessonId;
private Long sectionId;
private Integer moment;
public RecordTaskData(LearningRecord learningRecord) {
this.lessonId = learningRecord.getLessonId();
this.moment = learningRecord.getMoment();
this.sectionId = learningRecord.getSectionId();
}
}
}
业务中改造的代码如下
private boolean handleVideoRecord(Long userId, LearningRecordFormDTO recordDto) {
//1.查询旧的学习记录
/*LearningRecord oldRecord = lambdaQuery().select().eq(LearningRecord::getUserId, userId)
.eq(LearningRecord::getLessonId, recordDto.getLessonId())
.eq(LearningRecord::getSectionId, recordDto.getSectionId())
.one();*/
/*改为查redis*/
LearningRecord oldRecord=queryOldRecord(recordDto.getLessonId(),recordDto.getSectionId());
//2.判断是否存在
//3.不存在,则新增
if (oldRecord==null){
//3.不存在,则新增
//3.1.转换dto为po
LearningRecord record = BeanUtils.copyBean(recordDto, LearningRecord.class);
record.setUserId(userId);
record.setFinished(false);
record.setCreateTime(recordDto.getCommitTime());
record.setUpdateTime(recordDto.getCommitTime());
//3.2.写入数据库
boolean save = save(record);
if (!save){
throw new DbException("添加视频学习记录失败");
}
return false;
}
//4.存在,则更新
//4.1判断是否是第一次完成
boolean finished = !oldRecord.getFinished()&&recordDto.getMoment() * 2 >= recordDto.getDuration();
if (!finished){
LearningRecord record = new LearningRecord();
record.setLessonId(recordDto.getLessonId());
record.setSectionId(recordDto.getSectionId());
record.setMoment(recordDto.getMoment());
record.setId(oldRecord.getId());
record.setFinished(oldRecord.getFinished());
taskHandler.addLearningRecordTask(record);
return false;
}
//4.2更新
boolean success = lambdaUpdate().set(LearningRecord::getMoment, recordDto.getMoment())
.set(finished, LearningRecord::getFinished, true)
.set(finished, LearningRecord::getFinishTime, recordDto.getCommitTime())
.eq(LearningRecord::getId, oldRecord.getId())
.update();
if (!success){
throw new DbException("更新视频学习记录失败");
}
//4.3清理缓存
taskHandler.cleanRecordCache(recordDto.getLessonId(),recordDto.getSectionId());
return true;
}
/*优先缓存*/
private LearningRecord queryOldRecord(Long lessonId, Long sectionId) {
//1.查询缓存
LearningRecord record = taskHandler.readRecordCache(lessonId, sectionId);
//2.如果命中,直接返回
if (record!=null){
return record;
}
//3.未命中,查询数据库
record = lambdaQuery().select()
.eq(LearningRecord::getLessonId, lessonId)
.eq(LearningRecord::getSectionId, sectionId)
.one();
//3.如果未命中,返回null
if (record==null){
return null;
}
//4.写入缓存
taskHandler.writeRecordCache(record);
return record;
}
来自黑马程序员的天机学堂
1217

被折叠的 条评论
为什么被折叠?



