亿级流量点赞系统 前两章笔记 完成第二章冷热分离扩展点

项目github地址https://github.com/xtyooo/ThumbSys

1 基础功能实现

包括 用户模块 点赞模块 博客模块(点赞的载体 可替换为短视频 图片等)

  1. 登录 由于不是本系统的重点 我们只需要用登录区分不同用户即可 不涉及密码
    故 设计登录接口 只需输入 用户的 id 即可登录 将用户 id 作为 session 存入浏览器
  2. 获取当前登录用户 :从 session 中获取
  3. 点赞:参数校验 博客是否存在校验 用户登录校验 用户是否已经点过赞校验 进行数据库操作
  4. 取消点赞:类似于点赞 (取消点赞 即帖子点赞数量 -1 并且 删除点赞表中的对应数据 删除方法有两种 可以根据点赞记录的 id 进行删除 也可以根据 用户 id +帖子 id 进行删除 前者不涉及回表 性能更高 但是需要得到点赞记录的 id 这一点也影响到后续 redis 数据结构的选择)
  5. 获取指定博客:根据博客 id 获取博客详细信息(包括 点赞量 点赞状态 如果用户已登录 还要返回当前用户是否已经将此博客点赞)
  6. 获取博客列表: 批量查询出博客,也要返回当前用户是否已经将某博客点赞

2 优化 1 当前用户是否点赞优化 查数据库改为查缓存

由于查询帖子是需要连带查询出当前用户是否点赞,就会造成多次查询数据库 设想 如果可以将这个查询操作是查缓存 那么就可以缓解数据库压力并加快查询速度

按照流程

  • 首先 当用户进行点赞时 将点赞记录先加入缓存再加入数据库
  • 接着 在查帖子时 判断当前用户是否点过赞就可以查询缓存 如果缓存中没有 就直接认为没有点过赞 不再重查数据库 以达到预期提高性能效果

选择 hash 作为存储的数据结构 key:userid field:帖子 id value:点赞记录的 id

这样就可以查询到点赞记录的 id 进行后续的删除操作

冷热分离 的策略。比如我们认为最近一个月新发的内容是热数据,那么可以让 Redis 中点赞记录的存在时间是帖子的发布时间 + 1 个月,如果点赞时该博客的发布时间不超过一个月,则查 Redis 校验是否已点赞;如果发布时间超过了一个月,则通过 MySQL 校验是否已点赞。还可以引入布隆过滤器,布隆过滤器中存在再进行后续步骤,否则直接返回未点赞。

关于扩展 根据帖子发布时间是否超过一个月来区分热帖进行冷热分离 查缓存 or 查数据库

前提是需要拿到帖子的发布时间 如果不是由前端提前传递 则还是需要多一次查询帖子的数据库操作

所以 可以扩展DoThumbRequest

因为查询帖子后 前端是拥有创建时间的 可以传给后端 避免查数据库获取创建时间

注:考虑 前端传的参数可能不安全 上述方案不采用

问题: 因为Redis 的哈希(Hash)结构本身不能针对单个字段(field)设置过期时间。Redis 的过期时间机制是基于键(key)的,也就是只能为整个键设置过期时间,而不能为哈希中的某个特定字段设置独立的过期时间。

所以以当前的结构 只能根据 userid 进行过期时间的设计 这与冷热帖子就无法对应上

需要考虑更换 key-field-value

java
复制代码
/** * @author xtyooo */ @Service @Slf4j @RequiredArgsConstructor public class ThumbServiceImpl extends ServiceImpl<ThumbMapper, Thumb> implements ThumbService { private final UserService userService; private final BlogService blogService; private final TransactionTemplate transactionTemplate; private final RedisTemplate<String, Object> redisTemplate; @Override public Boolean doThumb(DoThumbRequest doThumbRequest, HttpServletRequest request) { if (doThumbRequest == null || doThumbRequest.getBlogId() == null) { throw new RuntimeException("参数错误"); } User loginUser = userService.getLoginUser(request); if (loginUser == null) { throw new RuntimeException(ErrorCode.NOT_LOGIN_ERROR.getMessage()); } // 加锁 synchronized (loginUser.getId().toString().intern()) { // 编程式事务 return transactionTemplate.execute(status -> { Long blogId = doThumbRequest.getBlogId(); boolean exists = false; Object o = redisTemplate.opsForHash().get(ThumbConstant.USER_THUMB_KEY_PREFIX + loginUser.getId(), blogId.toString()); if (o==null){//说明不在缓存中 可能是没点赞 也可能是帖子发布时间超过一个月了缓存被删除了 exists = this.lambdaQuery() .eq(Thumb::getUserId, loginUser.getId()) .eq(Thumb::getBlogId, blogId) .exists(); if (exists){//点过赞 但超过一个月 记录从redis中删除 throw new RuntimeException("用户已点赞"); }else {//没点过赞 执行点赞逻辑 boolean update = blogService.lambdaUpdate() .eq(Blog::getId, blogId) .setSql("thumbCount = thumbCount + 1") .update(); LambdaQueryWrapper<Blog> select = new LambdaQueryWrapper<Blog>().eq(Blog::getId, blogId) .select(Blog::getCreateTime); Blog blog = blogService.getOne(select); Thumb thumb = new Thumb(); thumb.setUserId(loginUser.getId()); thumb.setBlogId(blogId); // 更新成功执行加入缓存操作 boolean isSuccess = update && this.save(thumb); if (isSuccess) { ThumbInfo thumbInfo = new ThumbInfo(); thumbInfo.setThumbId(thumb.getId()); thumbInfo.setExpireTime(blog.getCreateTime().getTime()+ ThumbConstant.THUMB_EXPIRE_TIME); String key = ThumbConstant.USER_THUMB_KEY_PREFIX + loginUser.getId().toString(); redisTemplate.opsForHash().put(key, blogId.toString(), thumbInfo); } return isSuccess; } }else {//在缓存中 点过赞了 但还需要 需要判断是否过期 过期的话要删除缓存 ThumbInfo thumbInfo = (ThumbInfo) o; if (thumbInfo.getExpireTime()<System.currentTimeMillis()){ redisTemplate.opsForHash().delete(ThumbConstant.USER_THUMB_KEY_PREFIX + loginUser.getId(), blogId.toString()); } throw new RuntimeException("用户已点赞"); } }); } } /** * 取消点赞 * @param doThumbRequest * @param request * @return */ @Override public Boolean undoThumb(DoThumbRequest doThumbRequest, HttpServletRequest request) { if (doThumbRequest == null || doThumbRequest.getBlogId() == null) { throw new RuntimeException("参数错误"); } User loginUser = userService.getLoginUser(request); // 加锁 synchronized (loginUser.getId().toString().intern()) { // 编程式事务 return transactionTemplate.execute(status -> { Long blogId = doThumbRequest.getBlogId(); Object o = redisTemplate.opsForHash().get(ThumbConstant.USER_THUMB_KEY_PREFIX + loginUser.getId(), blogId.toString()); boolean success = false; if (o==null){//说明不在缓存中 可能是没点赞 也可能是帖子发布时间超过一个月了缓存被删除了 LambdaQueryWrapper<Thumb> lambdaQueryWrapper = new LambdaQueryWrapper<Thumb>() .eq(Thumb::getUserId, loginUser.getId()) .eq(Thumb::getBlogId, blogId); Thumb thumb = this.getOne(lambdaQueryWrapper); if (thumb == null){//没点过赞 throw new RuntimeException("用户未点赞"); } //点过赞 但超过一个月 记录从redis中删除 取消点赞需要从数据库中删除 //博客点赞数-1 UpdateWrapper<Blog> updateWrapper = new UpdateWrapper<Blog>().eq("id", blogId).setSql("thumbCount = thumbCount - 1"); boolean update = blogService.update(updateWrapper); //点赞表删除对应点赞记录 success = update && this.removeById(thumb.getId()); }else {//在缓存中 点过赞了 删除缓存 和 数据库的点赞记录 ThumbInfo thumbInfo = (ThumbInfo) o; //博客点赞数-1 UpdateWrapper<Blog> updateWrapper = new UpdateWrapper<Blog>().eq("id", blogId).setSql("thumbCount = thumbCount - 1"); boolean update = blogService.update(updateWrapper); //点赞表删除对应点赞记录 success = update && this.removeById(thumbInfo.getThumbId()); // 点赞记录从 Redis 删除 if (success) { redisTemplate.opsForHash().delete(ThumbConstant.USER_THUMB_KEY_PREFIX + loginUser.getId(), blogId.toString()); } } return success; }); } } @Override public Boolean hasThumb(Long blogId, Long userId) { String key = ThumbConstant.USER_THUMB_KEY_PREFIX + userId.toString(); return redisTemplate.opsForHash().hasKey(key, blogId.toString()); } }
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
夏天
下载 APP