点赞

​ 在我个人理解中,点赞业务比较频繁,很多人业务可能都会有这个,比如:博客,视频,文章,动态,评论等,但是不应该是核心业务,不应该大量地请求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黑马点评项目:

    @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的数据

    • 数据量

       scard userId:114514:publication_id:738836

    • 判断一个value是否存在这个set中-----(对应的业务为"判断当前登录用户是否已经点赞")

          @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--->(对应一个点赞,和取消点赞)

          @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 + " 毫秒"); }

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

    • 查询占用的内存的空间

      MEMORY USAGE  userId:114514:publication_id:738836

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

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

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

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

    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字段创建一个唯一索引。

    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将会阻止插入并抛出错误。

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

    详细代码如下:

    @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;
    }
    }
    } }

关于点赞业务对MySQL和Redis和MongoDB的思考的更多相关文章

  1. MySQL、Redis、MongoDB网络抓包工具

    简介 go-sniffer 可以抓包截取项目(MySQL.Redis.MongoDB)中的请求并解析成相应的语句,并格式化输出.类似于在之前的文章 MySQL抓包工具:MySQL Sniffer[转] ...

  2. spring boot多数据源配置(mysql,redis,mongodb)实战

    使用Spring Boot Starter提升效率 虽然不同的starter实现起来各有差异,但是他们基本上都会使用到两个相同的内容:ConfigurationProperties和AutoConfi ...

  3. 浅谈 Redis 与 MySQL 的耦合性以及利用管道完成 MySQL 到 Redis 的高效迁移

    http://blog.csdn.net/dba_waterbin/article/details/8996872 ㈠ Redis 与 MySQL 的耦合性            在业务架构早期.我们 ...

  4. laravel集成workerman,使用异步mysql,redis组件时,报错EventBaseConfig::FEATURE_FDS not supported on Windows

    由于laravel项目中集成了workerman,因业务需要,需要使用异步的mysql和redis组件. composer require react/mysql composer require c ...

  5. 【Docker】 使用Docker 在阿里云 Centos7 部署 MySQL 和 Redis (二)

    系列目录: [Docker] CentOS7 安装 Docker 及其使用方法 ( 一 ) [Docker] 使用Docker 在阿里云 Centos7 部署 MySQL 和 Redis (二) [D ...

  6. Mysql和Redis数据如何保持一致

    先阐明一下Mysql和Redis的关系:Mysql是数据库,用来持久化数据,一定程度上保证数据的可靠性:Redis是用来当缓存,用来提升数据访问的性能. 关于如何保证Mysql和Redis中的数据一致 ...

  7. linux安装和配置 mysql、redis 过程中遇到的问题记录

    linux下部署mysql和redis网上的教程很多,这里记录一下我部署.配置的过程中遇到的一些问题和解决办法. mysql ①安装完成后启动的时候报错 Starting MySQL.The serv ...

  8. Mysql与Redis的同步实践

    一.测试环境在Ubuntu kylin 14.04 64bit 已经安装Mysql.Redis.php.lib_mysqludf_json.so.Gearman. 点击这里查看测试数据库及表参考 本文 ...

  9. 通过Gearman实现MySQL到Redis的数据同步

    对于变化频率非常快的数据来说,如果还选择传统的静态缓存方式(Memocached.File System等)展示数据,可能在缓存的存取上会有很大的开销,并不能很好的满足需要,而Redis这样基于内存的 ...

  10. 一步完成 MySQL 向 Redis 迁移

    从mysql搬一个大表到redis中,你会发现在提取.转换或是载入一行数据时,速度慢的让你难以忍受.这里我就要告诉一个让你解脱的小技巧.使用“管道输出”的方式把mysql命令行产生的内容直接传递给re ...

随机推荐

  1. [USACO22DEC] Cow College B 题解

    洛谷 P8897 AcWing 4821 题目描述 有\(n\)头奶牛,每头奶牛愿意交的最大学费为\(c_i\),问如何设置学费,可以使赚到的钱最多. \(1\le n\le 10^5,1\le c_ ...

  2. go-zero 是如何实现计数器限流的?

    原文链接: 如何实现计数器限流? 上一篇文章 go-zero 是如何做路由管理的? 介绍了路由管理,这篇文章来说说限流,主要介绍计数器限流算法,具体的代码实现,我们还是来分析微服务框架 go-zero ...

  3. IDApython的学习

    IDApython的学习 我的IDA情况:IDA7.7,idapython3.8 这个可以作为文件导入和命令行内输入,我一般习惯命令行 这里要注意是python不是IDC 访问原数据 idc.get_ ...

  4. 领域驱动设计(DDD):三层架构到DDD架构演化

    三层架构的问题 在前文中,我从基础代码的角度探讨了如何运用领域驱动设计(DDD)来实现高内聚低耦合的代码.本篇文章将从项目架构的角度,继续探讨三层架构与DDD之间的演化过程,以及DDD如何优化架构的问 ...

  5. GIT提交修改的项目到远程仓库

    1.在项目目录下右键选择Git Bash. 2.执行提交命令三部曲 git add . //文件-暂存区,即将所有新增的文件添加到提交索引中,,add后面是"空格 点"就表示当前目 ...

  6. Hadoop NameNode启动后自动关闭解决方法

    Hadoop NameNode启动后过一会自动关闭了,查看日志文件报内存溢出异常: tail -100 /bigdata/logs/hadoop/hadoop-root-namenode-node1. ...

  7. 入门篇-其之二-Java基础知识

    目录 对第一个Java程序的思考 外层结构--类 内层结构--main方法 输出语句 注释 单行注释 多行注释 文档注释 文档注释常用标签 使用javadoc命令生成网页风格的文档 阿里巴巴Java开 ...

  8. vue + canvas 实现九宮格手势解锁器

    前言 专栏分享:vue2源码专栏,vue router源码专栏,玩具项目专栏,硬核推荐 欢迎各位 ITer 关注点赞收藏 此篇文章用于记录柏成从零开发一个canvas九宮格手势解锁器的历程,最终效果如 ...

  9. Nomad 系列-Nomad+Traefik+Tailscale 集成实现零信任安全

    系列文章 Nomad 系列文章 Traefik 系列文章 Tailscale 系列文章 概述 终于到了令人启动的环节了:Nomad+Traefik+Tailscale 集成实现零信任安全. 在这里: ...

  10. 使用 Sealos 一键部署高可用 MinIO,开启对象存储之旅

    大家好!今天这篇文章主要向大家介绍如何通过 Sealos 一键部署高可用 MinIO 集群. MinIO 对象存储是什么? 对象是二进制数据,例如图像.音频文件.电子表格甚至二进制可执行代码.对象的大 ...