当前位置: 首页 > news >正文

Redis实战-点赞的解决方案

1.修复发布探店笔记的接口

1.1探店笔记的数据库设计

        探店笔记的数据库数据结构如下:

        图片使用的是字符串,多张图片使用","进行分割。

        探店笔记数据表中遵循了第三范式设计,没有进行存储的相关数据,仅仅是通过userID进行存储到对应用户的ID数据,方便去进行查询。

1.2发布探店笔记的接口

        先进行上传图片,上传图片成功了以后,会收到上传成功后的一个图片连接,然后再点击发布按钮,会将图片连接一起携带过去。

1.2.1上传文件的接口

        上传文件的接口其实实现的是比较草率的,将文件上传到Nginx静态服务器相应的文件中,展示的时候也是从Nginx静态服务器中取数据进行展示。

        但是要注意把文件上传地址进行修改为自己部署的静态服务器的文件地址哦,否则会上传不到当前部署的前端服务器文件上。

1.2.2实现发布探店笔记的接口

1.2.2.1接口分析

        这个接口是/api/blog接口,是一个Post请求,前端会进行携带博客的相应信息到后端,后端只需要进行获取到用户信息并进行保存即可。

1.2.2.2接口实现

        前端已经把博客相关的数据提交上来了,现在要做的就是给Blog数据进行设置上UserID就可以了,然后再直接去保存,返回创建成功的博客ID。

        最好不要将这些逻辑直接写道Controller中,建议抽取到Service中去处理逻辑代码。

@PostMapping
public Result saveBlog(@RequestBody Blog blog) {// 获取登录用户UserDTO user = UserHolder.getUser();blog.setUserId(user.getId());// 保存探店博文blogService.save(blog);// 返回idreturn Result.ok(blog.getId());
}

1.3查询探店笔记详情的接口

1.3.1业务接口分析

        接口是/api/blog/{id},但是要注意的是,由于探店笔记需要展示不仅仅是博客信息,还需要进行展示博客对应的发布用户的用户数据,所以这个接口是需要去进行联表查询的。

1.3.2分析查询热点探店笔记的接口

        我们先去分析热点探店笔记的接口逻辑。

        热点数据的查询是直接进行去帖子数据库中查询的,根据liked点赞数进行排序,查询帖子数据。

        进行获取到查询的数据,进行遍历根据userID查询用户的数据,然后封装到数据中,进行返回。

        这里拼接数据的逻辑使用的是使用Java代码在内存中进行的封装,其实也可以直接联表查询,一次性查俩表数据,进行拼接起来,返回数据给前端。

    @GetMapping("/hot")public Result queryHotBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {// 根据用户查询Page<Blog> page = blogService.query().orderByDesc("liked").page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));// 获取当前页数据List<Blog> records = page.getRecords();// 查询用户records.forEach(blog ->{Long userId = blog.getUserId();User user = userService.getById(userId);blog.setName(user.getNickName());blog.setIcon(user.getIcon());});return Result.ok(records);}
}

1.3.3实现查询热点文案的接口

        热点文章的查询,前端携带Blog的ID而来,后端负责根据ID进行查询该数据,然后根据查询到的帖子数据去查询用户数据,融合到Blog对象中,进行统一返回即可。

        进行封装的根据查询的博客数据查询用户数据的函数:

// 根据博客查询用户数据
private void queryBlogUser(Blog blog) {Long userId = blog.getUserId();User blogUser = userService.getById(userId);blog.setName(blogUser.getNickName());blog.setIcon(blogUser.getIcon());
}

        进行封装接口:

/*** 根据ID查询博客详情** @param id* @return*/
@Override
public Result queryBlogById(Long id) {if (id == null) {return Result.fail("博客ID不能为空!!!");}// 根据ID查询博客数据Blog blog = getById(id);if (blog == null) {return Result.fail("查询的博客不存在哦!!!");}// 根据博客查询用户数据queryBlogUser(blog);return Result.ok(blog);
}

2.点赞

2.1点赞业务的介绍

        在首页的探店笔记排行榜和探店图文详情业务都有点在的功能。

2.1.1点赞需求的分析

        需求:1.同一个用户只能点赞一次,再次点击则取消点赞。2.如果用户已经点赞,就进行高亮显示。

2.1.2目前数据库分析

        可以发现目前数据库帖子中并没有进行设定判断用户是否点赞的字段。

        仅仅有一个liked字段,进行存储的是每个blog点赞的数量。

create table tb_blog
(id          bigint unsigned auto_increment comment '主键'primary key,shop_id     bigint                                   not null comment '商户id',user_id     bigint unsigned                          not null comment '用户id',title       varchar(255) collate utf8mb4_unicode_ci  not null comment '标题',images      varchar(2048)                            not null comment '探店的照片,最多9张,多张以","隔开',content     varchar(2048) collate utf8mb4_unicode_ci not null comment '探店的文字描述',liked       int unsigned default '0'                 null comment '点赞数量',comments    int unsigned                             null comment '评论数量',create_time timestamp    default CURRENT_TIMESTAMP   not null comment '创建时间',update_time timestamp    default CURRENT_TIMESTAMP   not null on update CURRENT_TIMESTAMP comment '更新时间'
)collate = utf8mb4_general_cirow_format = COMPACT;

2.1.3目前接口分析

        点赞接口:

        可以发现这个点赞接口十分垃圾。

@PutMapping("/like/{id}")
public Result likeBlog(@PathVariable("id") Long id) {// 修改点赞数量blogService.update().setSql("liked = liked + 1").eq("id", id).update();return Result.ok();
}

        判断用户是否点过赞的接口,没有进行设定,判定用户是否点过赞需要在获取热点帖子列表和获取博客详情的时候进行使用。

2.1.4数据库方案

2.1.4.1方案详细

        数据库实现点赞:在数据库中建一张表,关联User表和Blog表,如果用户进行点赞后就进行在这张表中进行新增数据,用户再次进行触发点赞接口的时候就进行取消点赞,在数据库中删除这条数据即可。

        数据库查询是否点赞:在查询帖子数据的时候,去数据库中查询一下当前登录用户是否对帖子点过赞,如果点过赞就给帖子字段isLiked进行设置为true。

2.1.4.2方案优点

        实现简单粗暴,不用引入其它中间件,只依赖于数据库,性能还行(MySQL单机可抗2000qps)。

2.1.4.3方案缺点

        MySQL实现起来稍微优点重了,不如Redis实现起来轻量级,性能高。

2.1.5Redis方案

        1.先给Blog类增加一个isLike字段,标识判断当前用户是否对帖子进行点过赞。

        2.修改点赞功能:利用Redis的set集合进行判断用户是否进行点赞,未点赞就点赞数+1,并且将用户放入set集合,已点赞就点赞数-1,并将用户移出set集合。

        3.在根据ID查询帖子/分页查询帖子的时候,去Redis中进行判断每个帖子是否点赞过,点赞过就给isLike字段赋值。

2.2实现Redis博客点赞

        适合使用Redis中的Set集合进行实现,因为Set集合有唯一性的原则,里面的数据不会存储重复的数据。

        Redis中Set的实现类似于Java中HashSet,增删改查数据基本上都是常数级的,效率不错。

2.2.1Redis实现博客点赞代码

        重点API:

        1.判断SET集合中是否有某字符串 => opsForSet().isMember(key,data)

        2.向SET集合中进行塞入数据 => opsForSet().add(key,data)

        3.从SET集合中进行删除数据 => opsForSet().remove(key,data)

/*** 博客点赞** @param id* @return*/
@Override
public Result likeBlog(Long id) {// 1. 获取登录用户Long userId = UserHolder.getUser().getId();// 2. 判断当前用户是否进行点过赞String key = "blog:liked:" + id;Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());if (BooleanUtil.isFalse(isMember)) {// 3. 未点赞, 可以进行点赞// 3.1 数据库点赞数 + 1boolean 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 数据库点赞数 - 1boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();// 4.2 取消点赞: 将用户从Redis的set集合中移出if (isSuccess) {stringRedisTemplate.opsForSet().remove(key, userId.toString());}}return Result.ok();
}

2.2.2实现博客点赞的整体流程

        1.进行获取到登录用户。

        2.进行去判断当前用户是否点过赞,去Redis中根据Key查询Set集合中是否有用户数据。

        key的设计:blog:liked:id,大业务类型:小业务:id,大业务类型 => blog,小业务 => liked,使用id进行分辨不同博客,使用set进行存储这些点赞的id用户(存储的是String类型的id)。

        使用opsForSet().isMember(key, userId.toString()),进行判断key对应的Set集合中,是否有相应的userId数据。

        使用Hutool的工具类提供的BooleanUtil进行判断返回值是否为False,主要是RedisTemplate进行isMember判断的时候进行返回的是Boolean包装类型,有可能是一个null,如果直接放到if中进行拆箱判断,就会出现NullPointerException空指针异常,使用这些工具类可以避免这些问题,包括也可以使用JDK包装类内置的Boolean.TRUE.equals(Boolean对象)。

boolean isSuccess = Boolean.FALSE.equals(isMember);

        3.未点赞的情况

        判断出来用户未点赞的时候,去更新数据库数据,更新成功点赞数据后,向Redis的Set集合中进行添加用户ID数据,表示用户已经进行点赞过了。

        4.已点赞的情况

        判断出来用户已经点过赞的时候,去更新数据库的数据,更新成功点赞数据后,向Redis的集合中进行删除用户ID数据,表示用户进行取消点赞了。

2.3实现Redis查看博客是否点赞

2.3.1判断用户是否对博客点赞过的函数封装

// 判断用户是否对博客点赞过
private void isBlogLiked(Blog blog) {// 1. 获取登录用户Long userId = UserHolder.getUser().getId();// 2. 从redis中的Set中进行判断是否进行点赞过String key = "blog:liked:" + blog.getId();Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());blog.setIsLike(Boolean.TRUE.equals(isMember));
}

        1.获取到登录用户的ID。

        2.进行定义好Key,去对应博客的点赞Set集合中去查询userId是否存在。

        3.由于返回值是一包装数据类型,所以为了进行防止出现空指针异常(拆箱时可能会出现,使用Boolean.TRUE.equals进行判断,可以防止空指针异常)

2.3.2在查询博客的时候进行判断当前用户是否点赞

        直接调用即可。

/*** 查询热点博客** @param current* @return*/
@Override
public Result queryHotBlog(Integer current) {// 根据用户查询Page<Blog> page = query().orderByDesc("liked").page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));// 获取当前页数据List<Blog> records = page.getRecords();// 查询用户records.forEach((blog -> {queryBlogUser(blog);isBlogLiked(blog);}));return Result.ok(records);
}/*** 根据ID查询博客详情** @param id* @return*/
@Override
public Result queryBlogById(Long id) {if (id == null) {return Result.fail("博客ID不能为空!!!");}// 1. 根据ID查询博客数据Blog blog = getById(id);if (blog == null) {return Result.fail("查询的博客不存在哦!!!");}// 2. 根据博客查询用户数据queryBlogUser(blog);// 3. 查询Blog是否被点赞isBlogLiked(blog);return Result.ok(blog);
}

2.3.3问题解决:用户未登录时查询数据报错

        报错空指针异常,因为用户未登录时进行获取的ThreadLocal中的UserDTO数据是null,去调用getId的时候会抛出NullPointerException空指针异常。

// 判断用户是否对博客点赞过
private void isBlogLiked(Blog blog) {// 1. 获取登录用户UserDTO user = UserHolder.getUser();if (user == null) {return;}Long userId = user.getId();// 2. 从redis中的Set中进行判断是否进行点赞过String key = "blog:liked:" + blog.getId();Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());blog.setIsLike(Boolean.TRUE.equals(isMember));
}

2.4分析点赞排行榜业务

        点赞排行榜是典型的Top几问题,这里进行展示如何使用Redis进行实现。

2.4.1点赞排行榜是什么

        就是在整个探店的详情页面,将最早点赞的TOP5展示处理啊。

        这个使用Set集合进行是实现比较困难,如果想要实现建议使用ZSET,这个集合具有Score权重,可以进行排行,适合解决这种Top排行问题。

        接口设计,这是一个单独的接口,并没有跟随查询Blog的接口进行融合在一起,进行了接口分离,其实还是建议接口融合,这样可以减少请求数量,一次请求解决问题。

        接口路径:/blog/likes/{id},返回List<UserDTO>。

2.4.2点赞用户列表数据结构的选型

        List => 按顺序进行存储,数据不唯一,List底层是链表进行实现的,按索引进行查找或者从头/尾进行遍历。

        Set => 无法排序,数据唯一,根据元素进行查找(底层采用Hash表的数据结构)。

        SortedSet => 根据Score权重值存储,数据唯一,根据元素查找(底层采用Hash表的数据结构)。

        明显可以看出来SortedSet才是我们需要的数据结构,使用时间戳作为Score,时间早的就会在上面,这样就可以进行返回Top5了。

2.5实现使用SortedSet实现点赞/Top排序业务

2.5.1进行实现点赞业务

        就是使用ZST进行取代了SET:

        1.opsForSet().isMember(key, data) => opsForZSet().score(key, data)(使用score进行根据key => 定位SortedSet,根据数据进行查询到Score,返回的是一个Double包装对象,如果不存在这个包装对象就是null,如果查询到就是有数据)

        2.使用opsForSet().add(key, data) => opsForZSet().add(key, data, score) => 进行想ZSET中进行添加数据的时候,需要进行指定Score。

        3.使用opsForSet().remove(key, data) => opsForZSet().remove(key, data) => remove的API没有怎么发生变化,直接进行指定Key和data数据即可。

        这里使用的是System.currentTimeMillis()生成时间戳,进行作为score,ZSET中的排序是按照score进行从小到大进行排序,那么在ZSET中进行排序出来的用户数据,就是根据点赞时间的从早到晚进行排序的。

/*** 博客点赞** @param id* @return*/
@Override
public Result likeBlog(Long id) {// 1. 获取登录用户Long userId = UserHolder.getUser().getId();// 2. 判断当前用户是否进行点过赞String key = "blog:liked:" + id;Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());if (Objects.isNull(score)) {// 3. 未点赞, 可以进行点赞// 3.1 数据库点赞数 + 1boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();// 3.2 点赞: 保存用户到Redis的set集合if (isSuccess) {stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());}} else {// 4. 如果点过赞, 就进行取消点赞// 4.1 数据库点赞数 - 1boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();// 4.2 取消点赞: 将用户从Redis的set集合中移出if (isSuccess) {stringRedisTemplate.opsForZSet().remove(key, userId.toString());}}return Result.ok();
}

2.5.2进行实现点赞TOP排行业务

2.5.2.1定义查询TOP的接口
/*** 查询最早点赞的TOP5** @param id* @return*/
@GetMapping("/likes/{id}")
private Result queryBlogTopLikes(@PathVariable Long id) {return blogService.queryBlogTopLikes(id);
}

2.5.2.2实现TOP的业务逻辑

        抓住几个重点聊一下。

/***  查询最早点赞的TOP5*  * @param id* @return*/
@Override
public Result queryBlogTopLikes(Long id) {// 1. 从Redis的ZSET中进行查询TOP5的点赞数据String key = "blog:liked:" + id;Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);if (top5 == null || top5.isEmpty()) {return Result.ok(Collections.emptyList());}// 2. 解析出来其中的IDList<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());// 3. 根据ID查询用户数据List<UserDTO> userDTOS = userService.listByIds(ids).stream().map(item -> BeanUtil.copyProperties(item, UserDTO.class)).collect(Collectors.toList());// 4. 返回return Result.ok(userDTOS);
}

        1.查询SortedSet中TOP5的数据。

        使用ZSet进行存储点赞用户数据的时候,进行使用opsForZSet().range(key, 0, 4) => 这样就可以进行查询出来ZSet集合中的前五个数据,返回的是一个Set集合String数据(这个集合数据是按顺序进行返回的,所以在逻辑上是有序的)。

        2.进行判断是否有空数据。

        进行判断Set集合是否为null或者为空,如果符合判空条件就进行调用Collections.emptyList()生成一个空List集合进行返回数据。

        3.解析出ID,转换为Long。

        在Set中存储的数据是String数据,需要进行转换为存储Long的List集合,方便进行查询数据库使用。

     这里使用的是Collections集合都有的stream流,调用map进行转换,最后调用collect(Collectors.toList())进行转换为List集合。

        4.遍历List集合调用数据库查询数据得到List<UserDTO>。

        使用userService.listByIds(ids):listByIds()接收一个List集合,进行根据里面的数据查询User数据,返回的是List<User>,不能直接进行返回User数据,要进行脱敏处理,所以调用stream().map()进行遍历转换。

        使用Hutool工具类的BeanUtil.copyProperties(item, UserDTO.class),这个进行返回的是一个UserDTO对象,经过Stream流转换最终使用collect收集为List<UserDTO>,进行返回即可。

2.5.3BUG的出现

2.5.3.1调研问题

        可以发现在SortedSet中存储的数据,确实是按时间戳进行排序的,1010用户先于2号用户。

        但是查看排行榜会发现排行出来的数据,2号用户在1010号用户前面:

2.5.3.2代码复现

        可以使用redis进行查询出来的Set,Set中查询回来的数据是一个有顺序的数据。

        SQL语句 => 可以发现listByIds进行使用ID集合查询数据的时候,进行使用的是WHERE id IN 用户ID,使用IN进行查询数据,IN进行查询数据的时候,不会按传给IN的顺寻进行排序的,而是按照ID进行升序排序的。

        所以这就是问题的根源,查询数据的时候,不会进行按照IN传入的顺序进行对查询的数据排序。

        重点:IN进行查询的数据是无序的!!!

2.5.3.3修复思路 => ORDER BY FIELD自定义排序

        如何有序呢?当然是进行使用order进行解决这个问题。

        order by FIELD() => 进行使用FIELD进行自定义一个顺序进行按照这个顺序进行排序。

        FIELD的语法很简单 => FIELD("字段K", A, B, C),这样查询出来的数据就是按字段K,以ABC的顺序进行排序了。

2.5.3.4代码实现

        使用Hutool的StrUtil拼接工具,StrUtil.join(",", ids) => 进行将集合拼接为"1,2,3"这种数据。

        由于MyBatis-Plus没有进行提供order() + FIELD函数配合使用,所以需要进行自己编写SQL语句。

        使用query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list()进行查询出来数据。

        使用last在最后进行拼接上ORDER + FIELD排序语句。

String idStr = StrUtil.join(",", ids);
// 3. 根据ID查询用户数据
List<UserDTO> userDTOS = userService.query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list().stream().map(item -> BeanUtil.copyProperties(item, UserDTO.class)).collect(Collectors.toList());

http://www.dtcms.com/a/354423.html

相关文章:

  • vue布局
  • LightGBM 在金融逾期天数预测任务中的经验总结
  • 2025年渗透测试面试题总结-36(题目+回答)
  • 2025年渗透测试面试题总结-37(题目+回答)
  • vue3 数据库 内的 字符 显示 换行符
  • LeetCode-238除自身以外数组的乘积
  • 基于单片机步进电机控制电机正反转加减速系统Proteus仿真(含全部资料)
  • codeforces(1045)(div2) E. Power Boxes
  • 2024年09月 Python(三级)真题解析#中国电子学会#全国青少年软件编程等级考试
  • Kubernetes 的20 个核心命令分类详解
  • 深度学习11 Deep Reinforcement Learning
  • 基于视觉的网页浏览Langraph Agent
  • 【RAG知识库实践】向量数据库VectorDB
  • Linux应用软件编程---网络编程(TCP并发服务器构建:[ 多进程、多线程、select ])
  • Spring Start Here 读书笔记:第15 章 Testing your Spring app
  • 【PyTorch】基于YOLO的多目标检测项目(二)
  • vue2 watch 的使用
  • Xshell 自动化脚本大赛技术文章大纲
  • TypeScript:重载函数
  • 《Linux 网络编程四:TCP 并发服务器:构建模式、原理及关键技术(select )》
  • oceanbase-部署
  • yolo ultralytics之yolov8.yaml文件简介
  • 《信息检索与论文写作》实验报告三 中文期刊文献检索
  • Linux 云服务器内存不足如何优化
  • LinuxC系统多线程程序设计
  • C语言:数据在内存中的存储
  • nginx referer-policy 和 referer
  • redis集群分片策略
  • 【温室气体数据集】NOAA CCGG 飞机观测温室气体
  • 2025年06月 Python(三级)真题解析#中国电子学会#全国青少年软件编程等级考试