libilibi项目优化(1)使用Redis实现缓存

devtools/2025/3/6 9:29:54/

第一版

获取视频信息使用旁路缓存

  • 当视频信息存在缓存中时(命中),直接从缓存中获取。
  • 不存在缓存中时,先从数据库中查出对应的信息,写入缓存后再放回数据。
java">//获取视频详细信息@RequestMapping("/getVideoInfo")public ResponseVO getVideoInfo(@NotEmpty String videoId) {//旁路缓存模式,先从缓存中拿VideoInfo videoInfo = redisComponent.getVideoInfoDetail(videoId);if(videoInfo == null){//缓存中不存在就从数据库中取videoInfo = videoInfoService.getVideoInfoByVideoId(videoId);}if(videoInfo==null){throw new BusinessException(ResponseCodeEnum.CODE_404);}//将视频信息保存到缓存redisComponent.saveVideoInfoDeTail(videoInfo);//获取当前用户对应的点赞和投币信息TokenUserInfoDto userInfoDto = getTokenUserInfoDto();List<UserAction> userActionList = new ArrayList<>();if(userInfoDto!=null){UserActionQuery actionQuery = new UserActionQuery();actionQuery.setVideoId(videoId);actionQuery.setUserId(userInfoDto.getUserId());//查询视频对应用户的点赞投币收藏信息actionQuery.setActionTypeArray(new Integer[]{UserActionTypeEnum.VIDEO_LIKE.getType(),UserActionTypeEnum.VIDEO_COLLECT.getType(),UserActionTypeEnum.VIDEO_COIN.getType(),});userActionList = userActionService.findListByParam(actionQuery);}VideoInfoResultVo resultVo = new VideoInfoResultVo();//设置用户的点赞投币收藏信息resultVo.setUserActionList(userActionList);resultVo.setVideoInfo(CopyTools.copy(videoInfo, VideoInfoVo.class));return getSuccessResponseVO(resultVo);}

用户点赞、收藏使用异步缓存写入

在进行更新视频点赞、收藏数量等信息时,并非直接修改数据库,而是先修改缓存中的数据,再利用消息队列,或定时任务等方式,将缓存中的数据更新到数据库

java">@Override@Transactional(rollbackFor = Exception.class)public void saveAction(UserAction bean) {//旁路缓存模式,想从缓存中拿VideoInfo videoInfo = redisComponent.getVideoInfoDetail(bean.getVideoId());if(videoInfo == null){//缓存中不存在就从数据库中取videoInfo = videoInfoMapper.selectByVideoId(bean.getVideoId());}if(videoInfo==null){throw new BusinessException(ResponseCodeEnum.CODE_404);}//设置视频对应的用户idbean.setVideoUserId(videoInfo.getUserId());//获得对应的用户行为(点赞,收藏,投币,评论点赞)UserActionTypeEnum actionTypeEnum = UserActionTypeEnum.getByType(bean.getActionType());if(actionTypeEnum==null){throw new BusinessException(ResponseCodeEnum.CODE_600);}//从数据库中根据视频id,评论id(若为评论点赞的话),行为类型,和用户行为来查询对应的行为记录UserAction dbAction = userActionMapper.selectByVideoIdAndCommentIdAndActionTypeAndUserId(bean.getVideoId(),bean.getCommentId(), bean.getActionType(), bean.getUserId());bean.setActionTime(new Date());switch (actionTypeEnum){//点赞和收藏case VIDEO_LIKE:case VIDEO_COLLECT://若存在点赞和收藏的记录,则取消点赞或收藏if(dbAction!=null){userActionMapper.deleteByActionId(dbAction.getActionId());}else{//添加对应的行为记录userActionMapper.insert(bean);}//若之前点过赞或收藏过则改变数量为-1,否则为1Integer changeCount = dbAction == null? Constants.ONE:-Constants.ONE;//更新视频对应的点赞或收藏信息if (actionTypeEnum.getType() == 2){Integer likeCount = videoInfo.getLikeCount();likeCount += changeCount;videoInfo.setLikeCount(likeCount);}else{Integer collectCount = videoInfo.getCollectCount();collectCount += changeCount;videoInfo.setCollectCount(collectCount);}//videoInfoMapper.updateCountInfo(bean.getVideoId(),actionTypeEnum.getField(),changeCount);if(actionTypeEnum == UserActionTypeEnum.VIDEO_COLLECT){//更新es的收藏数量esSearchComponent.updateDocCount(videoInfo.getVideoId(), SearchOrderTypeEnum.VIDEO_COLLECT.getField(), changeCount);}break;//投币case VIDEO_COIN:if (videoInfo.getUserId().equals(bean.getUserId())) {throw new BusinessException("UP主不能给自己投币");}if (dbAction != null) {throw new BusinessException("对本稿件的投币枚数已用完");}//更新当前用户的硬币数量, 返回的整数即为成功更新的行数Integer updateCoinCountInfo = userInfoMapper.updateCoinCountInfo(bean.getUserId(), -bean.getActionCount());//未成功更新说明硬币数量不够if(updateCoinCountInfo == 0){throw new BusinessException("硬币数量不足");}updateCoinCountInfo = userInfoMapper.updateCoinCountInfo(bean.getVideoUserId(), bean.getActionCount());//并发操作,数据库行上锁if(updateCoinCountInfo == 0){throw new BusinessException("投币失败");}//添加对应的行为记录userActionMapper.insert(bean);//更新视频对应的点赞或收藏信息//videoInfoMapper.updateCountInfo(bean.getVideoId(),actionTypeEnum.getField(), bean.getActionCount());break;}//将视频信息保存到缓存redisComponent.saveVideoInfoDeTail(videoInfo);}
使用定时任务同步缓存数据库
java">@Scheduled(cron = "0 0/5 * * * ?") // 每5分钟执行一次public void updateCachePeriodically() {try {// 从Redis中批量获取视频信息Map<String, VideoInfo> videoInfoBatch = redisComponent.getVideoInfoBatch();if (videoInfoBatch == null || videoInfoBatch.isEmpty()) {log.info("No video info found in Redis cache.");return;}// 提取视频信息列表List<VideoInfo> videoInfoList = new ArrayList<>(videoInfoBatch.values());// 批量更新数据库videoInfoService.addOrUpdateBatch(videoInfoList);log.info("Successfully updated {} video infos in the database.", videoInfoList.size());} catch (Exception e) {log.error("Error occurred during periodic cache update", e);}}

在你的第一版实现中,已经很好地结合了旁路缓存(视频基本信息)和异步写入缓存(点赞、收藏、播放量等高频修改信息)。以下是对你现有实现的分析和进一步优化的建议,以确保数据一致性和系统的高效性。


现有实现的优点

  1. 旁路缓存模式

    • 视频基本信息通过旁路缓存模式实现,减少了数据库的读取压力。
    • 缓存未命中时,从数据库加载数据并更新缓存,保证了数据的最终一致性。
  2. 异步写入缓存

    • 点赞、收藏等高频修改信息直接更新缓存,然后通过定时任务批量同步到数据库,减少了数据库的写入压力。
    • 使用消息队列或定时任务确保数据最终一致性。
  3. 定时任务同步

    • 定时任务定期从缓存中获取数据并同步到数据库,避免了实时同步带来的性能开销。

潜在问题和优化建议

1. 缓存穿透问题

在旁路缓存模式下,如果视频信息不存在,每次请求都会直接查询数据库,可能导致数据库压力过大。

优化建议

  • 缓存中存储一个空对象或特殊标记(如 nullempty),表示该 videoId 对应的视频信息不存在。
  • 示例:
    java">if (videoInfo == null) {videoInfo = videoInfoService.getVideoInfoByVideoId(videoId);if (videoInfo == null) {redisComponent.saveVideoInfoDetail(new VideoInfo(videoId, "empty"), 5, TimeUnit.MINUTES); // 存储空对象,设置较短的过期时间throw new BusinessException(ResponseCodeEnum.CODE_404);}
    }
    
2. 缓存更新冲突

在多线程环境下,多个请求可能同时从数据库加载数据并更新缓存,导致缓存写入冲突。

优化建议

  • 使用分布式锁(如 Redisson)控制缓存的写入操作。
  • 示例:
    java">if (videoInfo == null) {redisComponent.lockVideoInfo(videoId); // 加锁videoInfo = videoInfoService.getVideoInfoByVideoId(videoId);if (videoInfo == null) {throw new BusinessException(ResponseCodeEnum.CODE_404);}redisComponent.saveVideoInfoDetail(videoInfo);redisComponent.unlockVideoInfo(videoId); // 解锁
    }
    
3. 数据一致性问题

在异步写入缓存模式下,缓存数据可能因为服务故障而丢失,导致数据不一致。

优化建议

  • 消息队列:将每次用户行为(如点赞、收藏)记录到消息队列中,后台消费者定期从队列中读取行为记录并更新缓存数据库
  • 持久化更新日志:在更新缓存时,将更新操作记录到持久化存储(如日志文件或数据库),以便在缓存服务故障时恢复数据。
  • 示例:
    java">// 发送消息到队列
    messageQueue.send(new VideoActionMessage(bean.getVideoId(), actionTypeEnum, changeCount));
    
4. 定时任务的优化

定时任务每次同步所有缓存数据,可能导致性能问题,尤其是在缓存数据量较大时。

优化建议

  • 增量更新:只同步缓存中发生变化的数据,而不是全量同步。
  • 设置缓存过期时间:为缓存中的高频修改信息设置较短的过期时间(如 5 分钟),确保数据不会长时间不一致。
  • 示例:
    java">@Scheduled(cron = "0 0/5 * * * ?")
    public void updateCachePeriodically() {List<VideoMetrics> changedMetrics = redisComponent.getChangedMetrics();if (changedMetrics == null || changedMetrics.isEmpty()) {log.info("No changed video metrics found in Redis cache.");return;}videoInfoService.updateMetricsBatch(changedMetrics);log.info("Successfully updated {} video metrics in the database.", changedMetrics.size());
    }
    
5. 数据库更新的原子性

在更新数据库时,可能存在并发问题,导致数据不一致。

优化建议

  • 使用数据库事务(@Transactional)确保更新操作的原子性。
  • 在更新数据库时,使用乐观锁或悲观锁,避免并发冲突。

优化后的代码示例

获取视频信息
java">@RequestMapping("/getVideoInfo")
public ResponseVO getVideoInfo(@NotEmpty String videoId) {VideoInfo videoInfo = redisComponent.getVideoInfoDetail(videoId);if (videoInfo == null) {redisComponent.lockVideoInfo(videoId); // 加锁videoInfo = videoInfoService.getVideoInfoByVideoId(videoId);if (videoInfo == null) {redisComponent.saveVideoInfoDetail(new VideoInfo(videoId, "empty"), 5, TimeUnit.MINUTES); // 存储空对象redisComponent.unlockVideoInfo(videoId); // 解锁throw new BusinessException(ResponseCodeEnum.CODE_404);}redisComponent.saveVideoInfoDetail(videoInfo);redisComponent.unlockVideoInfo(videoId); // 解锁}// 获取用户行为信息TokenUserInfoDto userInfoDto = getTokenUserInfoDto();List<UserAction> userActionList = new ArrayList<>();if (userInfoDto != null) {UserActionQuery actionQuery = new UserActionQuery();actionQuery.setVideoId(videoId);actionQuery.setUserId(userInfoDto.getUserId());actionQuery.setActionTypeArray(new Integer[]{UserActionTypeEnum.VIDEO_LIKE.getType(),UserActionTypeEnum.VIDEO_COLLECT.getType(),UserActionTypeEnum.VIDEO_COIN.getType()});userActionList = userActionService.findListByParam(actionQuery);}VideoInfoResultVo resultVo = new VideoInfoResultVo();resultVo.setUserActionList(userActionList);resultVo.setVideoInfo(CopyTools.copy(videoInfo, VideoInfoVo.class));return getSuccessResponseVO(resultVo);
}
用户行为处理
java">@Override
@Transactional(rollbackFor = Exception.class)
public void saveAction(UserAction bean) {VideoInfo videoInfo = redisComponent.getVideoInfoDetail(bean.getVideoId());if (videoInfo == null) {videoInfo = videoInfoMapper.selectByVideoId(bean.getVideoId());if (videoInfo == null) {throw new BusinessException(ResponseCodeEnum.CODE_404);}}bean.setVideoUserId(videoInfo.getUserId());UserActionTypeEnum actionTypeEnum = UserActionTypeEnum.getByType(bean.getActionType());if (actionTypeEnum == null) {throw new BusinessException(ResponseCodeEnum.CODE_600);}UserAction dbAction = userActionMapper.selectByVideoIdAndCommentIdAndActionTypeAndUserId(bean.getVideoId(), bean.getCommentId(), bean.getActionType(), bean.getUserId());bean.setActionTime(new Date());switch (actionTypeEnum) {case VIDEO_LIKE:case VIDEO_COLLECT:if (dbAction != null) {userActionMapper.deleteByActionId(dbAction.getActionId());} else {userActionMapper.insert(bean);}Integer changeCount = dbAction == null ? Constants.ONE : -Constants.ONE;updateVideoMetrics(bean.getVideoId(), actionTypeEnum, changeCount);break;case VIDEO_COIN:// 投币逻辑break;}redisComponent.saveVideoInfoDetail(videoInfo); // 更新缓存messageQueue.send(new VideoActionMessage(bean.getVideoId(), actionTypeEnum, changeCount)); // 发送消息到队列
}
更新视频指标
java">private void updateVideoMetrics(String videoId, UserActionTypeEnum actionType, Integer changeCount) {VideoMetrics metrics = redisComponent.getVideoMetrics(videoId);if (metrics == null) {metrics = new VideoMetrics(videoId);}switch (actionType) {case VIDEO_LIKE:metrics.setLikeCount(metrics.getLikeCount() + changeCount);break;case VIDEO_COLLECT:metrics.setCollectCount(metrics.getCollectCount() + changeCount);break;}redisComponent.saveVideoMetrics(metrics);
}
定时任务同步
java">@Scheduled(cron = "0 0/5 * * * ?")
public void updateCachePeriodically() {List<VideoMetrics> changedMetrics = redisComponent.getChangedMetrics();if (changedMetrics == null || changedMetrics.isEmpty()) {log.info("No changed video metrics found in Redis cache.");return;}videoInfoService.updateMetricsBatch(changedMetrics);log.info("Successfully updated {} video metrics in the database.", changedMetrics.size());
}

总结

通过上述优化,你的实现能够更好地解决以下问题:

  1. 缓存穿透:通过存储空对象避免无效的数据库查询。
  2. 缓存更新冲突:使用分布式锁确保缓存更新的线程安全。
  3. 数据一致性:结合消息队列和定时任务确保缓存数据库的最终一致性。
  4. 性能优化:通过增量更新和缓存过期时间减少定时任务的压力。

这种设计既保证了系统的高性能,又确保了数据的一致性。

第二版(规划中)

  • 将视频的播放量,点赞,收藏信息独立出来,使用Hash进行缓存
  • 定时任务只更新有变动的视频数据
  • 实现用户点赞高亮信息从SET中获取

http://www.ppmy.cn/devtools/164962.html

相关文章

Java常用正则表达式(身份证号、邮箱、手机号)格式校验

目录 身份证号的正则表达式 代码解释 正则表达式 方法 isValidIDCard 注意事项 校验邮箱的正则表达式 代码解释 正则表达式 方法 isValidEmail 注意事项 手机号的正则表达式 中国大陆手机号校验&#xff08;支持空字符串&#xff09; 代码解释 通用手机号校验&am…

STM32之ADC

逐次逼近式ADC&#xff1a; 左边是8路输入通道&#xff0c;左下是地址锁存和译码&#xff0c;可将通道的地址锁存进ADDA&#xff0c;ADDB&#xff0c;ADDC类似38译码器的结构&#xff0c;ALE为锁存控制键&#xff0c;通道选择开关可控制选择单路或者多路通道&#xff0c;DAC为…

Linux 基本开发工具的使用(yum、vim、gcc、g++、gdb、make/makefile)

文章目录 Linux 软件包管理器 - yum理解什么是软件包和yum如何查看/查找软件包如何安装软件如何实现本地机器和云服务器之间的文件互传如何卸载软件 Linux 编辑器 - vim 的使用vim 的基本概念vim 的基本操作vim 命令模式各命令汇总vim 底行模式各命令汇总vim 的简单配置 Linux …

【算法方法总结·四】字符串操作的一些技巧和注意事项

【算法方法总结四】字符串操作的一些技巧和注意事项 【算法方法总结一】二分法的一些技巧和注意事项【算法方法总结二】双指针的一些技巧和注意事项【算法方法总结三】滑动窗口的一些技巧和注意事项【算法方法总结四】字符串操作的一些技巧和注意事项 【字符串操作】 此章节涉…

Python从PowerBI Server上取得报表数据的方法

下载PowerBI报表文件&#xff0c;提取数据文件读取数据并存为CSV文件 使用Python和Restful API下载PowerBI Server上报表为.pbix格式的报表文件&#xff0c;再把它当做zip文件解压出其中的数据文件&#xff0c;然后用Python读取该文件的内容并存储为CSV文件。最后详细论述Powe…

Python 面向对象高级编程-定制类

目录 __str__ __iter__ __getitem__ __getattr__ __call__ 小结 看到类似__slots__这种形如__xxx__的变量或者函数名就要注意&#xff0c;这些在Python中是有特殊用途的。 __slots__我们已经知道怎么用了&#xff0c;__len__()方法我们也知道是为了能让class作用于len()…

确定信号分析:从傅里叶级数到信号带宽的Matlab实践

关键词&#xff1a;傅里叶变换 信号能量 功率谱密度 自相关函数 信号带宽 Matlab仿真 内容摘要&#xff1a; 本文系统讲解确定信号分析的核心理论与Matlab实践&#xff0c;涵盖周期信号的傅里叶级数展开、非周期信号的傅里叶变换及性质、信号能量与功率的计算、自相关函数与频…

能简述一下动态 SQL 的执行原理吗

MyBatis 的动态 SQL 是一种强大的功能&#xff0c;允许开发者根据条件动态生成 SQL 语句。它的执行原理主要涉及以下几个步骤&#xff1a; ### **1. 解析映射文件** 当 MyBatis 启动时&#xff0c;会加载并解析映射文件&#xff08;Mapper.xml&#xff09;&#xff0c;提取其中…