libilibi项目优化(1)使用Redis实现缓存
第一版
获取视频信息使用旁路缓存
- 当视频信息存在缓存中时(命中),直接从缓存中获取。
- 不存在缓存中时,先从数据库中查出对应的信息,写入缓存后再放回数据。
//获取视频详细信息
@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);
}
用户点赞、收藏使用异步缓存写入
在进行更新视频点赞、收藏数量等信息时,并非直接修改数据库,而是先修改缓存中的数据,再利用消息队列,或定时任务等方式,将缓存中的数据更新到数据库
@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);
}
//设置视频对应的用户id
bean.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,否则为1
Integer 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);
}
使用定时任务同步缓存与数据库
@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. 缓存穿透问题
在旁路缓存模式下,如果视频信息不存在,每次请求都会直接查询数据库,可能导致数据库压力过大。
优化建议:
- 在缓存中存储一个空对象或特殊标记(如
null
或empty
),表示该videoId
对应的视频信息不存在。 - 示例:
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)控制缓存的写入操作。
- 示例:
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. 数据一致性问题
在异步写入缓存模式下,缓存数据可能因为服务故障而丢失,导致数据不一致。
优化建议:
- 消息队列:将每次用户行为(如点赞、收藏)记录到消息队列中,后台消费者定期从队列中读取行为记录并更新缓存和数据库。
- 持久化更新日志:在更新缓存时,将更新操作记录到持久化存储(如日志文件或数据库),以便在缓存服务故障时恢复数据。
- 示例:
// 发送消息到队列 messageQueue.send(new VideoActionMessage(bean.getVideoId(), actionTypeEnum, changeCount));
4. 定时任务的优化
定时任务每次同步所有缓存数据,可能导致性能问题,尤其是在缓存数据量较大时。
优化建议:
- 增量更新:只同步缓存中发生变化的数据,而不是全量同步。
- 设置缓存过期时间:为缓存中的高频修改信息设置较短的过期时间(如 5 分钟),确保数据不会长时间不一致。
- 示例:
@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
)确保更新操作的原子性。 - 在更新数据库时,使用乐观锁或悲观锁,避免并发冲突。
优化后的代码示例
获取视频信息
@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);
}
用户行为处理
@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)); // 发送消息到队列
}
更新视频指标
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);
}
定时任务同步
@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());
}
总结
通过上述优化,你的实现能够更好地解决以下问题:
- 缓存穿透:通过存储空对象避免无效的数据库查询。
- 缓存更新冲突:使用分布式锁确保缓存更新的线程安全。
- 数据一致性:结合消息队列和定时任务确保缓存与数据库的最终一致性。
- 性能优化:通过增量更新和缓存过期时间减少定时任务的压力。
这种设计既保证了系统的高性能,又确保了数据的一致性。
第二版:使用 Redis Hash 和 Set 实现视频缓存和用户行为管理
- 将视频的播放量,点赞,收藏信息独立出来,使用Hash进行缓存。
- 定时任务只更新有变动的视频数据
- 实现用户点赞高亮信息从SET中获取
1. 视频信息的缓存和数据库同步
获取视频信息
@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);
}
Set 的使用:
- 用户点赞信息:使用 Redis 的 Set 数据结构存储每个视频的点赞用户 ID。Set 的特点是无序且唯一,非常适合存储点赞用户信息,因为每个用户只能点赞一次。
- 点赞操作:通过
redisComponent.likeVideo(videoId, userId)
将用户 ID 添加到 Set 中。 - 取消点赞操作:通过
redisComponent.unlikeVideo(videoId, userId)
从 Set 中移除用户 ID。 - 检查是否点赞:通过
redisComponent.hasUserLiked(videoId, userId)
检查用户 ID 是否在 Set 中。
Hash 的使用:
- 视频指标信息:使用 Redis 的 Hash 数据结构存储视频的播放量、点赞数、收藏数等信息。Hash 的特点是键值对存储,非常适合存储多个字段。
- 更新视频指标:通过
redisComponent.updateVideoMetrics(videoId, type, num)
更新视频的某个指标。
2. 用户行为处理
保存用户行为
@Override
@Transactional(rollbackFor = Exception.class)
public void saveAction(UserAction bean) {
// 获取视频基本信息
VideoInfo videoInfo = videoInfoMapper.selectByVideoId(bean.getVideoId());
if (videoInfo == null) {
throw new BusinessException(ResponseCodeEnum.CODE_404);
}
// 设置视频对应的用户ID
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:
handleLikeAction(videoInfo, bean, dbAction);
break;
case VIDEO_COLLECT:
handleCollectAction(videoInfo, bean, dbAction);
break;
case VIDEO_COIN:
handleCoinAction(videoInfo, bean, dbAction);
break;
}
}
处理点赞行为
private void handleLikeAction(VideoInfo videoInfo, UserAction bean, UserAction dbAction) {
String videoId = bean.getVideoId();
String userId = bean.getUserId();
boolean hasLiked = redisComponent.hasUserLiked(videoId, userId);
if (hasLiked) {
// 取消点赞
redisComponent.unlikeVideo(videoId, userId);
updateVideoMetrics(videoId, Constants.VIDEO_LIKE_NUM, -1);
} else {
// 点赞
redisComponent.likeVideo(videoId, userId);
updateVideoMetrics(videoId, Constants.VIDEO_LIKE_NUM, 1);
}
}
处理收藏行为
private void handleCollectAction(VideoInfo videoInfo, UserAction bean, UserAction dbAction) {
String videoId = bean.getVideoId();
if (dbAction != null) {
userActionMapper.deleteByActionId(dbAction.getActionId());
updateVideoMetrics(videoId, Constants.VIDEO_COLLECT_NUM, -1);
} else {
userActionMapper.insert(bean);
updateVideoMetrics(videoId, Constants.VIDEO_COLLECT_NUM, 1);
}
}
处理投币行为
private void handleCoinAction(VideoInfo videoInfo, UserAction bean, UserAction dbAction) {
String videoId = bean.getVideoId();
String userId = bean.getUserId();
if (videoInfo.getUserId().equals(userId)) {
throw new BusinessException("UP主不能给自己投币");
}
if (dbAction != null) {
throw new BusinessException("对本稿件的投币枚数已用完");
}
Integer updateCoinCountInfo = userInfoMapper.updateCoinCountInfo(userId, -bean.getActionCount());
if (updateCoinCountInfo == 0) {
throw new BusinessException("硬币数量不足");
}
updateCoinCountInfo = userInfoMapper.updateCoinCountInfo(videoInfo.getUserId(), bean.getActionCount());
if (updateCoinCountInfo == 0) {
throw new BusinessException("投币失败");
}
userActionMapper.insert(bean);
updateVideoMetrics(videoId, Constants.VIDEO_COIN_NUM, bean.getActionCount());
}
3. 定时任务同步缓存到数据库
@Scheduled(cron = "0 0/5 * * * ?") // 每5分钟执行一次
public void updateCachePeriodically() {
Set<String> keySet = redisUtils.getByKeyPrefix(Constants.VIDEO_METRICS_KEY);
List<String> keyList = new ArrayList<>(keySet);
if (keyList.isEmpty()) {
return;
}
for (String key : keyList) {
// 获取缓存中的视频指标信息
Map<String, Integer> videoMetrics = redisComponent.getAllVideoMetrics(key);
String videoId = key.substring(key.lastIndexOf(":") + 1);
// 更新数据库
VideoInfo videoInfo = new VideoInfo();
videoInfo.setVideoId(videoId);
videoInfo.setLikeCount(videoMetrics.getOrDefault(Constants.VIDEO_LIKE_NUM, 0));
videoInfo.setCollectCount(videoMetrics.getOrDefault(Constants.VIDEO_COLLECT_NUM, 0));
videoInfo.setCoinCount(videoMetrics.getOrDefault(Constants.VIDEO_COIN_NUM, 0));
videoInfoService.updateVideoMetrics(videoInfo);
// 删除旧的缓存
redisComponent.deleteAllVideoMetrics(videoId);
redisComponent.deleteVideoInfoDeTail(videoId);
log.info("数据更新成功");
}
}
Set 的使用:
- 用户点赞信息:通过
redisComponent.hasUserLiked(videoId, userId)
检查用户是否点赞,通过redisComponent.likeVideo(videoId, userId)
添加点赞信息,通过redisComponent.unlikeVideo(videoId, userId)
删除点赞信息。
Hash 的使用:
- 视频指标信息:通过
redisComponent.updateVideoMetrics(videoId, type, num)
更新视频的某个指标,通过redisComponent.getAllVideoMetrics(key)
获取视频的所有指标信息。
4. RedisComponent 实现
@Component
public class RedisComponent {
@Autowired
private RedisUtils redisUtils;
// 获取视频详细信息
public VideoInfo getVideoInfoDetail(String videoId) {
return (VideoInfo) redisUtils.get(Constants.VIDEO_DETAIL_KEY + videoId);
}
// 保存视频详细信息
public void saveVideoInfoDeTail(VideoInfo videoInfo) {
redisUtils.setex(Constants.VIDEO_DETAIL_KEY + videoInfo.getVideoId(), videoInfo, Constants.TIME_SECONDS_DAY);
}
// 删除视频详细信息
public void deleteVideoInfoDeTail(String videoId) {
redisUtils.delete(Constants.VIDEO_DETAIL_KEY + videoId);
}
// 批量获取视频信息
public Map<String, VideoInfo> getVideoInfoBatch() {
Map<String, VideoInfo> batch = redisUtils.getBatch(Constants.VIDEO_DETAIL_KEY);
return batch;
}
// 用户点赞操作
public void likeVideo(String videoId, String userId) {
String key = Constants.VIDEO_LIKEUSERS_KEY + videoId;
redisUtils.addSet(key, userId);
}
// 用户取消点赞操作
public void unlikeVideo(String videoId, String userId) {
String key = Constants.VIDEO_LIKEUSERS_KEY + videoId;
redisUtils.removeSet(key, userId);
}
// 检查用户是否点赞
public boolean hasUserLiked(String videoId, String userId) {
String key = Constants.VIDEO_LIKEUSERS_KEY + videoId;
return redisUtils.hasMemberSet(key, userId);
}
// 更新视频相关数量信息
public void updateVideoMetrics(String videoId, String type, Integer num) {
String key = Constants.VIDEO_METRICS_KEY + videoId;
redisUtils.putHash(key, type, num);
}
// 获取缓存中的视频的所有信息
public Map<String, Integer> getAllVideoMetrics(String key) {
return redisUtils.getAllHash(key);
}
// 删除缓存中的视频的所有信息
public void deleteAllVideoMetrics(String videoId) {
redisUtils.delete(Constants.VIDEO_METRICS_KEY + videoId);
}
}
Set 的使用:
- 用户点赞信息:使用 Redis 的 Set 数据结构存储每个视频的点赞用户 ID。Set 的特点是无序且唯一,非常适合存储点赞用户信息,因为每个用户只能点赞一次。
- 点赞操作:通过
redisUtils.addSet(key, userId)
将用户 ID 添加到 Set 中。 - 取消点赞操作:通过
redisUtils.removeSet(key, userId)
从 Set 中移除用户 ID。 - 检查是否点赞:通过
redisUtils.hasMemberSet(key, userId)
检查用户 ID 是否在 Set 中。
Hash 的使用:
- 视频指标信息:使用 Redis 的 Hash 数据结构存储视频的播放量、点赞数、收藏数等信息。Hash 的特点是键值对存储,非常适合存储多个字段。
- 更新视频指标:通过
redisUtils.putHash(key, type, num)
更新视频的某个指标。 - 获取视频指标:通过
redisUtils.getAllHash(key)
获取视频的所有指标信息。
5. Constants 类
public class Constants {
// Redis Key 前缀
public static final String VIDEO_DETAIL_KEY = "video:info:";
public static final String VIDEO_METRICS_KEY = "video:metrics:";
public static final String VIDEO_LIKEUSERS_KEY = "video:likes:";
// 视频相关字段
public static final String VIDEO_LIKE_NUM = "likeNum";
public static final String VIDEO_COLLECT_NUM = "collectNum";
public static final String VIDEO_COIN_NUM = "coinNum";
// 时间常量
public static final long TIME_SECONDS_DAY = 86400; // 一天的秒数
}
6. 优点
- 封装性强:所有 Redis 操作封装在
RedisComponent
和RedisUtils
中,代码更简洁,易于维护。 - 通用性:支持任意类型的键和值,适用于多种场景。
- 扩展性:可以轻松扩展到其他 Redis 数据结构的操作。
- 性能优化:通过定时任务只同步有变动的视频数据,减少数据库压力。
过程具体描述
1. 视频信息的缓存和数据库同步
流程描述:
-
获取视频信息:
- 用户请求获取视频详细信息。
- 系统首先从 Redis 缓存中查找视频信息。
- 如果缓存中存在视频信息,直接返回给用户。
- 如果缓存中不存在视频信息,系统从数据库中获取视频信息。
- 将获取到的视频信息保存到 Redis 缓存中,以便后续请求可以直接从缓存中获取。
- 返回视频信息给用户。
-
用户行为处理:
- 用户进行点赞、收藏或投币操作。
- 系统根据用户操作类型,调用相应的处理方法。
- 对于点赞操作,系统检查用户是否已经点赞。如果已经点赞,则取消点赞;如果没有点赞,则添加点赞。
- 对于收藏操作,系统检查用户是否已经收藏。如果已经收藏,则取消收藏;如果没有收藏,则添加收藏。
- 对于投币操作,系统检查用户是否有足够的硬币。如果有足够的硬币,则进行投币操作;如果没有足够的硬币,则返回错误信息。
- 更新视频的相应指标(点赞数、收藏数、投币数)。
-
定时任务同步缓存到数据库:
- 定时任务每5分钟执行一次。
- 系统从 Redis 缓存中获取所有视频的指标信息。
- 对于每个视频,系统更新数据库中的相应指标。
- 删除旧的缓存信息,确保缓存的及时性和准确性。
2. 流程图
获取视频信息流程图:
+----------------+ +----------------+ +----------------+
| 用户请求视频 | --> | 从Redis缓存中 | --> | 返回视频信息 |
| 详细信息 | | 查找视频信息 | | 给用户 |
+----------------+ +----------------+ +----------------+
^
|
| 缓存中不存在
v
+----------------+
| 从数据库中 |
| 获取视频信息 |
+----------------+
^
|
| 保存到Redis
v
+----------------+
| 将视频信息 |
| 保存到缓存 |
+----------------+
用户行为处理流程图:
+----------------+ +----------------+ +----------------+
| 用户进行点赞、 | --> | 系统调用相应 | --> | 更新视频指标 |
| 收藏或投币操作 | | 的处理方法 | | (点赞数、收藏数 |
| | | | | 、投币数) |
+----------------+ +----------------+ +----------------+
^
|
| 点赞操作
v
+----------------+
| 检查用户是否 |
| 已经点赞 |
+----------------+
^
|
| 已点赞
v
+----------------+
| 取消点赞 |
+----------------+
^
|
| 未点赞
v
+----------------+
| 添加点赞 |
+----------------+
定时任务同步缓存到数据库流程图:
+----------------+ +----------------+ +----------------+
| 定时任务每5 | --> | 从Redis缓存中 | --> | 更新数据库中的 |
| 分钟执行一次 | | 获取所有视频 | | 相应指标 |
+----------------+ +----------------+ +----------------+
^
|
| 删除旧的
v
+----------------+
| 删除旧的缓存 |
| 信息 |
+----------------+
3. 详细描述
获取视频信息:
- 用户请求获取视频详细信息。
- 系统首先从 Redis 缓存中查找视频信息。
- 如果缓存中存在视频信息,直接返回给用户。
- 如果缓存中不存在视频信息,系统从数据库中获取视频信息。
- 将获取到的视频信息保存到 Redis 缓存中,以便后续请求可以直接从缓存中获取。
- 返回视频信息给用户。
用户行为处理:
- 用户进行点赞、收藏或投币操作。
- 系统根据用户操作类型,调用相应的处理方法。
- 对于点赞操作,系统检查用户是否已经点赞。如果已经点赞,则取消点赞;如果没有点赞,则添加点赞。
- 对于收藏操作,系统检查用户是否已经收藏。如果已经收藏,则取消收藏;如果没有收藏,则添加收藏。
- 对于投币操作,系统检查用户是否有足够的硬币。如果有足够的硬币,则进行投币操作;如果没有足够的硬币,则返回错误信息。
- 更新视频的相应指标(点赞数、收藏数、投币数)。
定时任务同步缓存到数据库:
- 定时任务每5分钟执行一次。
- 系统从 Redis 缓存中获取所有视频的指标信息。
- 对于每个视频,系统更新数据库中的相应指标。
- 删除旧的缓存信息,确保缓存的及时性和准确性。