天机学堂 第6天 点赞逻辑

news2024/9/25 5:23:54

首先我们来分析整理一下点赞业务的需求,一个通用点赞系统需要满足下列特性:

  • 通用:点赞业务在设计的时候不要与业务系统耦合,必须同时支持不同业务的点赞功能

  • 独立:点赞功能是独立系统,并且不依赖其它服务。这样才具备可迁移性。(可以发送消息队列通知其他微服务)

  • 并发:一些热点业务点赞会很多,所以点赞功能必须支持高并发

  • 安全:要做好并发安全控制,避免重复点赞

要保证安全,避免重复点赞,我们就必须保存每一次点赞记录。只有这样在下次用户点赞时我们才能查询数据,判断是否是重复点赞。同时,因为业务方经常需要根据点赞数量排序,因此每个业务的点赞数量也需要记录下来。

综上,点赞的基本思路如下:

如果业务方需要根据点赞数排序,就必须在数据库中维护点赞数字段(比如评论里面回复的点赞)。但是点赞系统无法修改其它业务服务的数据库,否则就出现了业务耦合。该怎么办呢?

学习评论的微服务

点赞业务本质

点赞记录本质就是记录谁给什么内容点了赞,所以核心属性包括:

  • 点赞目标id

  • 点赞人id(前端不用提交,后端直接判断)

不过点赞的内容多种多样,为了加以区分,我们还需要把点赞内的类型记录下来:

  • 点赞对象类型(为了通用性)

CREATE TABLE IF NOT EXISTS `liked_record` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `user_id` bigint NOT NULL COMMENT '用户id',
  `biz_id` bigint NOT NULL COMMENT '点赞的业务id',
  `biz_type` VARCHAR(16) NOT NULL COMMENT '点赞的业务类型',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_biz_user` (`biz_id`,`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='点赞记录表';

点赞或取消点赞

当用户点击点赞按钮的时候,第一次点击是点赞,按钮会高亮;第二次点击是取消,点赞按钮变灰:点赞就是新增一条点赞记录,取消就是删除这条记录

从后台实现来看,点赞就是新增一条点赞记录,取消就是删除这条记录。为了方便前端交互,这两个合并为一个接口即可。

因此,请求参数首先要包含点赞有关的数据,并且要标记是点赞还是取消:

  • 点赞的目标业务id:bizId

  • 谁在点赞(就是登陆用户,可以不用提交)

  • 点赞还是取消

除此以外,我们之前说过,在问答、笔记等功能中都会出现点赞功能,所以点赞必须具备通用性。因此还需要在提交一个参数标记点赞的类型:

  • 点赞目标的类型

返回值有两种设计:

  • 方案一:无返回值,200就是成功,页面直接把点赞数+1展示给用户即可

  • 方案二:返回点赞数量,页面渲染

这里推荐使用方案一,因为每次统计点赞数量也有很大的性能消耗。

 

 逻辑梳理

我们先梳理一下点赞业务的几点需求:

  • 点赞就新增一条点赞记录,取消点赞就删除记录

  • 用户不能重复点赞

  • 点赞数由具体的业务方保存,需要通知业务方更新点赞数

思路: 首先实现判断点赞或者取消赞是否成功(因为如果已经点过赞了(数据库存在点赞记录)再次点赞机会失败,取消赞同理,)成功了才取统计点赞业务的数量,然后发送mq取更新

@Override
    @Transactional
    public void addLikeRecord(LikeRecordFormDTO recordDTO) {
        // 获取当前登录用户id
        Long userId = UserContext.getUser();
        // 点赞取消赞业务是否失败,失败了就不用统计点赞数量
        Boolean flag = false;
        //判断是否点赞
        if (recordDTO.getLiked()) {
            flag = liked(recordDTO, userId);
        } else {
            //取消赞逻辑,有点赞记录直接删除,没有什么也不做
            flag = cancelLiked(recordDTO, userId);
        }
        // 统计该业务的总点赞数
        if (flag) {
            Integer count = this.lambdaQuery()
                    .eq(LikedRecord::getBizId, recordDTO.getBizId())
                    .count();
            // 发送消息给mq
            rabbitMqHelper.send(
                    MqConstants.Exchange.LIKE_RECORD_EXCHANGE,
                    MqConstants.Key.QA_LIKED_TIMES_KEY,
                    LikedTimesDTO.builder().bizId(recordDTO.getBizId()).likedTimes(count).build()
            );
        }

    }

    private Boolean cancelLiked(LikeRecordFormDTO recordDTO, Long userId) {
        LikedRecord likedRecord = this.lambdaQuery()
                .eq(LikedRecord::getBizId, recordDTO.getBizId())
                .eq(LikedRecord::getUserId, userId)
                .one();
        if (likedRecord != null) {
            removeById(likedRecord.getId());
            return true;
        }
        return false;
    }

    private Boolean liked(LikeRecordFormDTO recordDTO, Long userId) {
        //点赞逻辑 查看有没有点赞记录,没有则新增
        LikedRecord likedRecord = this.lambdaQuery()
                .eq(LikedRecord::getBizId, recordDTO.getBizId())
                .eq(LikedRecord::getUserId, userId)
                .one();
        if (likedRecord == null) {
            likedRecord = new LikedRecord();
            likedRecord.setBizId(recordDTO.getBizId());
            likedRecord.setBizType(recordDTO.getBizType());
            likedRecord.setUserId(userId);
            save(likedRecord);
            return true;
        }
        return false;
    }

 其他微服务监听点赞状态变更的消息 

直接更新数据库即可

批量查询点赞状态

由于这个接口是供其它微服务调用,实现完成接口后,还需要定义对应的FeignClient

 前端发送一系列业务id,判断哪些是该用户点赞过的,返回用户点赞过的id

@Override
public Set<Long> isBizLiked(List<Long> bizIds) {
    // 1.获取登录用户id
    Long userId = UserContext.getUser();
    // 2.查询点赞状态
    List<LikedRecord> list = lambdaQuery()
            .in(LikedRecord::getBizId, bizIds)
            .eq(LikedRecord::getUserId, userId)
            .list();
    // 3.返回结果
    return list.stream().map(LikedRecord::getBizId).collect(Collectors.toSet());
}

点赞功能改进 

点赞是个很频繁,访问量很高的操作

新增点赞或取消赞改进

 用redis的set存储可以减少数据库查询,大大缓解压力

由于Redis本身具备持久化机制,AOF提供的数据可靠性已经能够满足点赞业务的安全需求,因此我们完全可以用Redis存储来代替数据库的点赞记录。

也就是说,用户的一切点赞行为,以及将来查询点赞状态我们可以都走Redis,不再使用数据库查询。

 有同学会担心,如果点赞数据非常庞大,达到数百亿,那么该怎办呢?

 代码实现

点赞次数

由于点赞次数需要在业务方持久化存储到数据库,因此Redis只起到缓存作用即可。

由于需要记录业务id、业务类型、点赞数三个信息:

  • 一个业务类型下包含多个业务id

  • 每个业务id对应一个点赞数。

使用zset来存储

  • zset(sorted set:有序集合): 类似于集合,但是每个元素都有一个分数(score)与之关联。

 通过定时任务定期将数据持久化到数据库

 

 定时任务批量处理更新到数据库

两个业务来回循环,每次取30条数据更新(避免压力过大)

 popmin 按照分值去取size大小的数据,取出来并返回

    public void readLikedTimesAndSendMessage(String bizType, int maxBizSize) {
        // 1.拼接key
        String bizTypeTotalLikeKey = RedisConstants.LIKES_TIMES_KEY_PREFIX + bizType;
        ArrayList<LikedTimesDTO> list = new ArrayList<>();
        // 2.从redis中的zset结构中取maxbizsize的业务点赞信息  popmin
        Set<ZSetOperations.TypedTuple<String>> typedTuples = redisTemplate.opsForZSet().popMin(bizTypeTotalLikeKey, maxBizSize);
        for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
            String bizId = typedTuple.getValue();
            Double likedTimes = typedTuple.getScore();
            if (StringUtils.isBlank(bizId) || likedTimes == null) {
                continue;
            }
            //3.封装LikedTimesDTO 消息数据
            LikedTimesDTO msg = new LikedTimesDTO();
            msg.setBizId(Long.valueOf(bizId));
            msg.setLikedTimes(likedTimes.intValue());
            list.add(msg);
        }
        // 4.发送消息到mq
        if (CollUtils.isNotEmpty(list)){
            log.debug("批量发送点赞消息,消息内容{}",list);
            String routingKey = StringUtils.format(MqConstants.Key.LIKED_TIMES_KEY_TEMPLATE, bizType);
            rabbitMqHelper.send(
                    MqConstants.Exchange.LIKE_RECORD_EXCHANGE,
                    routingKey,
                    list);
        }
    }

 发送器其他微服务取批量更新

批量查询点赞状态统计

点赞记录都缓存到redis中,直接去redis查询,又因为一个一个查太费劲使用redis的管道技术

    public Set<Long> getLikeStatusByzIds(List<Long> bizIds) {
        if (CollUtils.isEmpty(bizIds)) {
            return CollUtils.emptySet();
        }
        // 1.获取登录用户id
        Long userId = UserContext.getUser();
        // 2.查询点赞状态   短时间执行大量的查询的时候用
        List<Object> objects = redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            StringRedisConnection src = (StringRedisConnection) connection;
            for (Long bizId : bizIds) {
                String key = RedisConstants.LIKE_BIZ_KEY_PREFIX + bizId;
                src.sIsMember(key, userId.toString());
            }
            // 这个return没有意义,会把结果封装到集合中
            return null;
        });
        // 3.返回结果
        return IntStream.range(0, objects.size()) // 创建从0到集合size的流
                .filter(i -> (boolean) objects.get(i)) // 遍历每个元素,保留结果为true的角标i
                .mapToObj(bizIds::get)// 用角标i取bizIds中的对应数据,就是点赞过的id
                .collect(Collectors.toSet());// 收集
        /**
         * 传统的写法
         * // 获取用户id
         *         Long userId = UserContext.getUser();
         *         // 2.查询点赞状态
         *         List<LikedRecord> list = lambdaQuery()
         *                 .in(LikedRecord::getBizId, bizIds)
         *                 .eq(LikedRecord::getUserId, userId)
         *                 .list();
         *         // 3.返回结果
         *         return list.stream().map(LikedRecord::getBizId).collect(Collectors.toSet());
         */

    }

sentinal降级

1引入依赖 2降级类 3引用降级类 4配置文件中自动注入 5.开始远程降级服务 6 测试

2 编写降级配置类 

3 引用降级类

 4 配置文件中自动注入(因为是其他微服务引入,要想让其被spring管理,必须让spring扫描到这个类,在spring.factories)spring启动器只扫描与他在同一个包路径下的和指定的路径的,引入的Maven依赖需要在spring.factories中配置 要扫描哪些类。其他服务只要依赖了某个依赖就会扫描那个依赖中spring.factories写的bean

5.开启降级服务 

 一个微服务调用另一个微服务时,另一个微服务不能获取到threadlocal中的用户信息

 

解决办法,使用feign拦截器

有userid时就重新放入请求头中

发送feign的时候先获取用户,然后把用户放入请求头发出去

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1989253.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RM小陀螺技术经验与思考

移动小陀螺的原理&#xff1a; 先调好云台&#xff0c;车移动云台方向不动。然后可以用电机和底盘的机械角度来计算 涉及到两个知识点&#xff1a;速度闭环和变换矩阵。。。 把mpu的值映射到脉轮上面&#xff0c;就是说根据yaw电机编码器和mpu的差值来计算麦轮解算的x 和y的移…

开发android app用于移远模块读写IMEI 模组EC200DEULA-D08-SNNDA 支持socket连接读写IMEI

开放权限 adb kill-serveradb rootadb shell setenforce 0adb install -t app-debug.apkadb shell am start -n com.azhon.spplus/.MainActivity::F310A_WriteIMEI -DWadb.exe forward tcp:5902 tcp:5902pause写读IMEI ADB socket协议 TCP 127.0.0.1:5902 PC与终端APP之间 j…

商业数据分析PPT制作大纲系列一进入数据分析世界(需PPT私)

PART 1数据挖掘:从海量信息中淘出真金 数据挖掘是在海量的数据中发现有价值信息和知识的过程。它就像是一位经验丰富的矿工,在堆积如山的数据矿石中,精准地筛选出珍贵的金子。 引言: 数据挖掘的定义与价值技术概览: 关键算法(如决策树、K-means聚类、关联规则); 步骤…

简单搭建vue项目

1.先安装node.js和vite&#xff0c;具体参考&#xff1a; 2.管理员身份运行cmd&#xff0c;跳转到node安装目录&#xff1a; 输入&#xff1a; npm create vitelatest 输入项目名称&#xff0c;选择vue和JavaScript 2.VisualStudioCode打开(可能需要管理员权限)创建的文件夹,点…

Sqlserver 备份表

Sqlserver 备份表 1、右键数据库->任务->生成脚本 2、在引导界面点击下一步 3、选择需要导出的表 4、在高级里面选择备份数据与架构&#xff0c;然后再单选每个对象一个文件 每个对象一个文件是有多个表的情况下备份成多个文件&#xff0c;否则所有表都在一个文件中 架…

告别盲目找货!以图搜货神器,精准定位全网低价同款货源

做生意进入图搜源头时代&#xff0c;图搜进货实现了商机“所见即所得”。一位短视频平台的服装商家说&#xff0c;平时她看到同行的一个爆款&#xff0c;不好意思打听货源&#xff0c;也很难打听到&#xff0c;现在只要截个图一搜&#xff0c;就能找到1688对应的源头工厂。 今…

【第17章】Spring Cloud之Gateway服务调用

文章目录 前言一、用户服务二、网关服务1. 负载均衡2. 服务调用3. 登录拦截器 三、单元测试1. 启动服务2. 用户不存在3. 正常登录 总结 前言 在上一章我们使用JWT简单完成了用户认证&#xff0c;【第16章】Spring Cloud之Gateway全局过滤器(安全认证)&#xff0c;上一章内容已…

Node.js是什么?如何安装

目录 一、前言 1、JavaScript语言-----前端开发 2、JavaScript语言-----后端开发 总结&#xff1a;如果我们写了一段 js 代码&#xff0c;把他放到浏览器中执行&#xff0c;是在做前端开发&#xff1b;如果放在Node.js下运行&#xff0c;是在做后端开发。 二、安装 1、打开…

GHOST重装系统后的分区失踪:数据恢复实战指南

一、引言&#xff1a;GHOST重装引发的数据隐忧 在计算机维护的众多手段中&#xff0c;GHOST重装系统以其高效、便捷的特点深受用户喜爱。然而&#xff0c;这一过程往往伴随着风险&#xff0c;其中之一便是分区丢失的隐患。当GHOST重装操作不当或遭遇意外情况时&#xff0c;原本…

制作喇叭接口拓展

今天发现音响只有两个音频输出口&#xff0c;而喇叭有三个&#xff0c;就想着改装成可以装三个&#xff0c;电脑桌上一个&#xff0c;脚底下两个&#xff0c;从抽屉里翻出来了一个电视上拆下来的三色莲花口&#xff0c;它本来是一个视频输入&#xff0c;两个音频输入&#xff0…

硬币计数器——Arduino

硬币计数器——Arduino 硬币计数盒模型计数传感器硬币计数盒接线计数器程序 硬币计数盒模型 计数传感器 硬币计数盒接线 计数器程序 // 包含TM1637库&#xff0c;这是一个用于驱动TM1637数码管的模块 #include <TM1637.h>// 使用volatile关键字声明布尔变量jishu&#x…

Redis远程字典服务器(1)—— 初识Redis

目录 一&#xff0c;关于Redis 二&#xff0c;Redis特性介绍 2.1 In-memory data structures&#xff08;在内存中存储数据&#xff09; 2.2 Programmablilty&#xff08;编程能力&#xff09; 2.3 Extensibility&#xff08;扩展能力&#xff09; 2.4 Persistence&#…

食家巷小程序:传统面点与平凉特产的美味盛宴

在美食的世界里&#xff0c;总有一些角落等待着我们去探索&#xff0c;而食家巷小程序就是这样一个为您开启美食宝藏的钥匙。 一、传统面点&#xff0c;传承千年的美味 食家巷小程序为您呈现了种类丰富的传统面点&#xff0c;每一款都蕴含着深厚的历史和文化底蕴。 平凉锅盔&…

x-cmd mod | x jina - 为 jina.ai 打造的命令行工具,提供获取网页内容、生成向量数据等等

目录 简介主要特点子命令例子 简介 Jina.ai 是一家专注于大型语言模型和媒体处理公司。基于 jina.ai 公司的接口&#xff0c;jina 模块主要提供了以下功能&#xff1a; 网页内容获取生成文本向量相关信息检索排序检索 主要特点 通过 jina模块的 Reader 功能&#xff0c;我们…

QT(2.0)

1.常用控件的介绍 1.1 TextEdit QTextEdit表示多行输入框&#xff0c;也是一个富文本&markdown编辑器&#xff0c;并且能在内容超出编辑框范围时自动提供滚动条。 核心属性 属性 说明 markdown 输入框内持有的内容&#xff0c;支持markdown格式&#xff0c;能够自动的…

[Leetcode 215][Medium]-数组中的第K个最大元素-快排/小根堆/堆排序

一、题目描述 原题地址 二、整体思路 &#xff08;1&#xff09;快排 对于SELECT K问题&#xff0c;可以通过三路快排解决&#xff0c;快排可以把一个元素放至按升序排序的数组正确的位置&#xff0c;左边为小于该元素的元素集合&#xff0c;右边为大于该元素的元素集合。 三…

朋克养生,现代男人为何对生可乐泡枸杞情有独钟

在当今快节奏、高压力的社会环境中&#xff0c;朋克养生这一独特的养生方式悄然兴起&#xff0c;尤其在年轻男性群体中备受青睐。其中&#xff0c;生可乐泡枸杞这一不乏创意的养生方法&#xff0c;更是成为不少现代男人追求健康与乐趣并存的象征。朋克养生不仅仅是一种外在的行…

lvs的防火墙标记解决轮询调度问题

错误示范 ipvsadm -A -t 192.168.0.200:80 -s rr ipvsadm -a -t 192.168.0.200:80 -r 192.168.0.10:80 -g ipvsadm -a -t 192.168.0.200:80 -r 192.168.0.20:80 -g ipvsadm -A -t 192.168.0.200:443 -s rr ipvsadm -a -t 192.168.0.200:443 -r 192.168.0.10:80 -g ipvsadm -a …

CVE-2024-39877:Apache Airflow 任意代码执行

Apache Airflow 是一个开源平台&#xff0c;用于以编程方式编写、调度和监控工作流。虽然它提供了管理复杂工作流的强大功能&#xff0c;但它也存在安全漏洞。一个值得注意的漏洞 CVE-2024-39877 是 DAG&#xff08;有向无环图&#xff09;代码执行漏洞。这允许经过身份验证的 …

游戏ttf字体瘦身脚本

游戏中通常会用到某种特定字体&#xff0c;而某些字体动则10M&#xff0c;对某些游戏(尤其是小游戏)来讲是无法忍受的&#xff0c;此文章主要讲述上个项目中制作的字体裁剪脚本 工具git地址 配置信息(config.json) { // 文本内容(可能为多语言表导出的内容)"txtFile&qu…