关于点赞业务对MySQL和Redis和MongoDB的思考

点赞

​ 在我个人理解中,点赞业务比较频繁,很多人业务可能都会有这个,比如:博客,视频,文章,动态,评论等,但是不应该是核心业务,不应该大量地请求MySQL数据库,给数据库造成大量的资源消耗,MySQL的数据库是非常宝贵的.

以某音为例,当我去搜索的时候,全抖音比较高的点赞数目应该是在1200w - 2000w,我们自己的业务肯定答不到这么高的,但是假设有10个100w的点赞的博客,user_id为用户ID,publication_id为博客的id

  1. 第一种方式是直接操作数据库.每次有点赞或者取消点赞操作时,就更新MySQL数据库的点赞数.同时,插入或者更新一个user_id和publication_id的数据行,如果点赞操作非常频繁,会对数据库产生很大的压力.如果有大量的点赞记录,会对数据库产生很大的数据量,一篇文章,100w+的点赞的记录,对于MySQL来说,是非常恐怖的.

  2. 第二种方式是通过MySQL + Redis的Set来实现,具体代码如下,以下的代码为B站Redis黑马点评项目:

    java 复制代码
    @Override
    public Result likeBlog(Long id){
        // 1. 获取登录用户
        Long userId = UserHolder.getUser().getId();
    
        // 2. 判断当前登录用户是否已经点赞
        String key = BLOG_LIKED_KEY + id;
        Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
    
        if(BooleanUtil.isFalse(isMember)){
            // 3. 如果未点赞,可以点赞
            // 3.1 数据库点赞数+1
            boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
    
            // 3.2 保存用户到Redis的set集合
            if(isSuccess){
                stringRedisTemplate.opsForSet().add(key, userId.toString());
            }
        } else {
            // 4. 如果已点赞,取消点赞
            // 4.1 数据库点赞数-1
            boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
    
            // 4.2 把用户从Redis的set集合移除
            if(isSuccess){
                stringRedisTemplate.opsForSet().remove(key, userId.toString());
            }
        }
    }

    这样造成的问题是,Redis是内存数据库,点赞信息存储在内存中。当点赞数量非常大时,会占用大量内存。

    下面测试一下,一个key为"userId:114514:publication_id:738836",value为100000-1100000的数据

    • 数据量

      shell 复制代码
       scard userId:114514:publication_id:738836
    • 判断一个value是否存在这个set中-----(对应的业务为"判断当前登录用户是否已经点赞")

      java 复制代码
          @Test
          public void selectBigKey() {
              String key = "userId:114514:publication_id:738836";
              String value1 = "100000";
              String value2 = "5000000";
              // 记录开始时间
              long startTime = System.nanoTime();
      
              boolean cacheSet1 = RedisUtils.containsInCacheSet(key, value1);
              if (cacheSet1) {
                  System.out.println("代码2:" + "存在这个value");
              } else {
                  System.out.println("代码2:" + "不存在这个value");
              }
              // 记录结束时间
              long endTime = System.nanoTime();
      
              // 计算执行时间(以毫秒为单位)
              long executionTime = (endTime - startTime) / 1_000_000; // 将纳秒转换为毫秒
      
              System.out.println("代码执行时间1: " + executionTime + " 毫秒");
      
              // 记录开始时间
              long startTime2 = System.nanoTime();
      
              boolean cacheSet2 = RedisUtils.containsInCacheSet(key, value2);
              if (cacheSet2) {
                  System.out.println("代码2:" + "存在这个value");
              } else {
                  System.out.println("代码2:" + "不存在这个value");
              }
      
              // 记录结束时间
              long endTime2 = System.nanoTime();
      
              // 计算执行时间(以毫秒为单位)
              long executionTime2 = (endTime2 - startTime2) / 1_000_000; // 将纳秒转换为毫秒
      
              System.out.println("代码执行时间2: " + executionTime2 + " 毫秒");
      
          }

      可以看到,其实对于时间来说,61毫秒和66毫秒可以说时间非常短了,不愧是redis,即使数据量很大,但是查询一个value是否在比较大的set的效率是非常短的.

    • 往一个key中添加一个value,或者删除一个value--->(对应一个点赞,和取消点赞)

      java 复制代码
          @Test
          public void addAndRemoveElementFromBigKey() {
              String key = "userId:114514:publication_id:738836";
              String value1 = "10000000";
              String value2 = "200000";
      
              // 记录开始时间
              long startTime = System.nanoTime();
      
              boolean cacheSet1 = RedisUtils.addToCacheSet(key, value1);
      
              // 记录结束时间
              long endTime = System.nanoTime();
      
              // 计算执行时间(以毫秒为单位)
              long executionTime = (endTime - startTime) / 1_000_000; // 将纳秒转换为毫秒
      
              System.out.println("添加一个元素的执行时间: " + executionTime + " 毫秒");
      
              // 记录开始时间
              long startTime2 = System.nanoTime();
      
              boolean cacheSet2 = RedisUtils.removeFromCacheSet(key, value2);
      
              // 记录结束时间
              long endTime2 = System.nanoTime();
      
              // 计算执行时间(以毫秒为单位)
              long executionTime2 = (endTime2 - startTime2) / 1_000_000; // 将纳秒转换为毫秒
      
              System.out.println("删除一个元素的代码执行时间: " + executionTime2 + " 毫秒");
      
          }

      但从时间来讲,只有一个字:快

    • 查询占用的内存的空间

      shell 复制代码
      MEMORY USAGE  userId:114514:publication_id:738836

​ 其实可以看到,大概是占用66mb,如果用户的id为雪花算法的id,那可能占用的内存100mb

以上来说,主要还是一个bigkey的问题,如果点赞的数量过大,占用的内存过大,宝贵的内存不应该给这种业务.

  1. 自然而然,我们想到用非关系型数据库,但是不要是基于内存的,我想到的是用MongoDB的方案

    我们可以往MongoDB中插入一条这样的数据:

    javascript 复制代码
    db.collectionName.insertOne({
      "id": "yourIdValue",
      "userId": yourUserIdValue,
      "type": yourTypeValue,
      "likedItemId": yourLikedItemIdValue,
      "createTime": new Date("yourCreateTimeValue")
    });

    id 主键id,userId为用户的ID,type为文章或者动态或者其他的类型,likedItemId为文章或者动态或者其他的类型的主键ID,createTime为点赞时间

    在MongoDB中,可以使用createIndex方法来创建唯一索引。为userId,typelikedItemId字段创建一个唯一索引。

    javascript 复制代码
    db.collectionName.createIndex(
      { "userId": 1, "type": 1, "likedItemId": 1 },
      { unique: true, name: "unique_index_name" }
    );

    详细解释:

    • collectionName:集合名称。
    • unique_index_name:你想要给索引起的名字,可以根据你的需求替换为其他名称。

    这个命令将在collectionName集合上创建一个名为unique_index_name的唯一索引,涵盖了userIdtypelikedItemId字段。 1表示升序,如果需要降序索引,可以使用-1unique: true选项确保索引是唯一的。

    执行这个命令后,如果有重复的组合出现在这三个字段上,MongoDB将会阻止插入并抛出错误。

    即如果里面有记录为已经点过赞,点赞就是往里面加记录,取消点赞就是删除记录

    详细代码如下:

    java 复制代码
    @Service
    public class LikeServiceImpl implements LikeService {
        @Autowired
        private MongoTemplate mongoTemplate;
    
        @Autowired
        private PublicationService publicationService;
    
        /**
         * 为动态或者文章点赞
         *
         * @param publicationId 动态或者文章的ID
         * @param userId        用户的ID
         * @param type          类型,区分是文章还是动态
         * @return 点赞总数
         */
        @Override
        public Integer likePublication(Long publicationId, Long userId, Integer type) {
            // 构建查询条件
            Criteria criteria = Criteria.where("userId").is(userId)
                    .and("type").is(type)
                    .and("likedItemId").is(publicationId);
            // 创建查询对象并应用查询条件
            Query query = new Query(criteria);
            boolean isExists = mongoTemplate.exists(query, PublicationLike.class);
    
            if (isExists) {
                Asserts.fail("重复点赞");
            }
            //将点赞记录保存到mongodb
            PublicationLike publicationLike = new PublicationLike();
            publicationLike.setType(type);
            publicationLike.setCreateTime(DateUtil.date());
            publicationLike.setLikedItemId(publicationId);
            publicationLike.setUserId(userId);
            PublicationLike savedLike = mongoTemplate.save(publicationLike);
            //点赞数统计
    
            String redisLikeCountKey = String.format(RedisConstant.PUBLICATION_LIKE_COUNT, publicationId, userId, type);
            Long likeCount = RedisUtils.getAtomicValueWithDefault(redisLikeCountKey);
            //如果没有缓存过点赞数,则查询数据库
            if (likeCount.equals(-1L)) {
                Publication publication = publicationService.getById(publicationId);
                RedisUtils.setAtomicValue(redisLikeCountKey, publication.getLikeCount());
                return publication.getLikeCount();
            } else {
                //返回点赞数+1
                return Math.toIntExact(RedisUtils.incrAtomicValue(redisLikeCountKey));
            }
    
    
        }
    
        @Override
        public Integer unlikePublication(Long publicationId, Long userId, Integer type) {
            // 构建查询条件
            Criteria criteria = Criteria.where("userId").is(userId)
                    .and("type").is(type)
                    .and("likedItemId").is(publicationId);
            // 创建查询对象并应用查询条件
            Query query = new Query(criteria);
            boolean isExists = mongoTemplate.exists(query, PublicationLike.class);
    
            if (!isExists) {
                Asserts.fail("未点赞过该内容,无法取消点赞");
            }
    
            // 从MongoDB中删除点赞记录
            mongoTemplate.remove(query, PublicationLike.class);
    
            // 更新点赞数统计
            String redisLikeCountKey = String.format(RedisConstant.PUBLICATION_LIKE_COUNT, publicationId, userId, type);
            Long likeCount = RedisUtils.getAtomicValueWithDefault(redisLikeCountKey);
    
            // 如果点赞数存在于缓存中,减少点赞数并返回
            if (!likeCount.equals(-1L)) {
                long newLikeCount = RedisUtils.decrAtomicValue(redisLikeCountKey);
                return Math.toIntExact(newLikeCount);
            } else {
                // 如果点赞数没有缓存,查询数据库并更新缓存
                Publication publication = publicationService.getById(publicationId);
                if (publication != null) {
                    RedisUtils.setAtomicValue(redisLikeCountKey, publication.getLikeCount());
                    return publication.getLikeCount();
                } else {
                    Asserts.fail("无法获取点赞数");
                    return 0;
                }
            }
        }
    
    }
相关推荐
带刺的坐椅20 小时前
SpringBoot3 使用 SolonMCP 开发 MCP
java·ai·springboot·solon·mcp
LUCIAZZZ2 天前
JVM之虚拟机运行
java·jvm·spring·操作系统·springboot
堕落年代2 天前
SpringSecurity当中的CSRF防范详解
前端·springboot·csrf
一零贰肆2 天前
深入理解SpringBoot中的SpringCache缓存技术
java·springboot·springcache·缓存技术
LUCIAZZZ4 天前
JVM之内存管理(一)
java·jvm·spring·操作系统·springboot
LUCIAZZZ5 天前
JVM之内存管理(二)
java·jvm·后端·spring·操作系统·springboot
天上掉下来个程小白6 天前
缓存套餐-01.Spring Cache入门案例
java·redis·spring·缓存·springboot·springcache
程序员buddha8 天前
SpringBoot+Dubbo+Zookeeper实现分布式系统步骤
分布式·zookeeper·dubbo·springboot
天上掉下来个程小白8 天前
缓存套餐-03.功能测试
redis·缓存·springboot·苍穹外卖·springcache
Ten peaches9 天前
苍穹外卖(订单状态定时处理、来单提醒和客户催单)
java·数据库·sql·springboot