Redis进阶——点赞和点赞排行

2024-04-20 19:20
文章标签 进阶 redis 排行 点赞

本文主要是介绍Redis进阶——点赞和点赞排行,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

  • 发布达人探店笔记
    • 实现步骤
  • 查看探店笔记
  • 点赞功能
    • 问题分析:
    • 功能完善
    • 具体实现
  • 点赞排行榜
    • 实现需求
    • 实现步骤

发布达人探店笔记

实现类似于大众点评的发布个人笔记的效果
20240419-040548-CQ.png

实现步骤

  1. 准备数据表如下:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for tb_blog
-- ----------------------------
DROP TABLE IF EXISTS `tb_blog`;
CREATE TABLE `tb_blog` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',`shop_id` bigint(20) NOT NULL COMMENT '商户id',`user_id` bigint(20) unsigned NOT NULL COMMENT '用户id',`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '标题',`images` varchar(2048) NOT NULL COMMENT '探店的照片,最多9张,多张以","隔开',`content` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '探店的文字描述',`liked` int(8) unsigned DEFAULT '0' COMMENT '点赞数量',`comments` int(8) unsigned DEFAULT NULL COMMENT '评论数量',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT;-- ----------------------------
-- Table structure for tb_blog_comments
-- ----------------------------
DROP TABLE IF EXISTS `tb_blog_comments`;
CREATE TABLE `tb_blog_comments` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',`user_id` bigint(20) unsigned NOT NULL COMMENT '用户id',`blog_id` bigint(20) unsigned NOT NULL COMMENT '探店id',`parent_id` bigint(20) unsigned NOT NULL COMMENT '关联的1级评论id,如果是一级评论,则值为0',`answer_id` bigint(20) unsigned NOT NULL COMMENT '回复的评论id',`content` varchar(255) NOT NULL COMMENT '回复的内容',`liked` int(8) unsigned DEFAULT NULL COMMENT '点赞数',`status` tinyint(1) unsigned DEFAULT NULL COMMENT '状态,0:正常,1:被举报,2:禁止查看',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT;SET FOREIGN_KEY_CHECKS = 1;

tb_blog:
探店店笔记表,包含笔记中的标题、文字、图片等

tb_blog_comments:
其他用户对探店笔记的评价

  1. 对应的实体类:
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_blog")
public class Blog implements Serializable {private static final long serialVersionUID = 1L;/*** 主键*/@TableId(value = "id", type = IdType.AUTO)private Long id;/*** 商户id*/private Long shopId;/*** 用户id*/private Long userId;/*** 用户图标*/@TableField(exist = false)private String icon;/*** 用户姓名*/@TableField(exist = false)private String name;/*** 是否点赞过了*/@TableField(exist = false)private Boolean isLike;/*** 标题*/private String title;/*** 探店的照片,最多9张,多张以","隔开*/private String images;/*** 探店的文字描述*/private String content;/*** 点赞数量*/private Integer liked;/*** 评论数量*/private Integer comments;/*** 创建时间*/private LocalDateTime createTime;/*** 更新时间*/private LocalDateTime updateTime;
}
  1. 发布笔记接口
@PostMapping
public Result saveBlog(@RequestBody Blog blog) {// 获取登录用户UserDTO user = UserHolder.getUser();blog.setUserId(user.getId());// 保存探店博文blogService.save(blog);// 返回idreturn Result.ok(blog.getId());
}
  1. 上传图片的代码
@PostMapping("blog")
public Result uploadImage(@RequestParam("file") MultipartFile image) {try {// 获取原始文件名称String originalFilename = image.getOriginalFilename();// 生成新文件名String fileName = createNewFileName(originalFilename);// 保存文件image.transferTo(new File(SystemConstants.IMAGE_UPLOAD_DIR, fileName));// 返回结果log.debug("文件上传成功,{}", fileName);return Result.ok(fileName);} catch (IOException e) {throw new RuntimeException("文件上传失败", e);}
}

注意:这里需要修改SystemConstants.IMAGE_UPLOAD_DIR 为自己图片所在的地址,在实际开发中图片一般会放在nginx上或者是云存储上。

查看探店笔记

需求:点击首页的探店笔记,会进入详情页面,我们现在需要实现页面的查询接口

20240419-041556-C0.png

实现代码:
BlogController

@RestController
@RequestMapping("/blog")
public class BlogController {@Resourceprivate IBlogService blogService;@PostMappingpublic Result saveBlog(@RequestBody Blog blog) {// 获取登录用户UserDTO user = UserHolder.getUser();blog.setUserId(user.getId());// 保存探店博文blogService.save(blog);// 返回idreturn Result.ok(blog.getId());}@PutMapping("/like/{id}")public Result likeBlog(@PathVariable("id") Long id) {// 修改点赞数量blogService.update().setSql("liked = liked + 1").eq("id", id).update();return Result.ok();}@GetMapping("/of/me")public Result queryMyBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {// 获取登录用户UserDTO user = UserHolder.getUser();// 根据用户查询Page<Blog> page = blogService.query().eq("user_id", user.getId()).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));// 获取当前页数据List<Blog> records = page.getRecords();return Result.ok(records);}@GetMapping("/hot")public Result queryHotBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {return blogService.queryHotBlog(current);}@GetMapping("/{id}")public Result queryById(@PathVariable Integer id){return blogService.queryById(id);}
}

BlogServiceImpl:

@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Resourceprivate IUserService userService;@Overridepublic 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(this::queryBlogUser);return Result.ok(records);}@Overridepublic Result queryById(Integer id) {Blog blog = getById(id);if (blog == null) {return Result.fail("评价不存在或已被删除");}queryBlogUser(blog);return Result.ok(blog);}private void queryBlogUser(Blog blog) {Long userId = blog.getUserId();User user = userService.getById(userId);blog.setName(user.getNickName());blog.setIcon(user.getIcon());}
}

上述的功能实现是简单的添加和查询的mysql实现,但是功能并不完善,主要是为之后功能加入Redis做准备

点赞功能

需求:点击点赞按钮,查看发送的请求

请求网址: http://localhost:8080/api/blog/like/4
请求方法: PUT

之前BlogController中的like方法,源码如下:

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

问题分析:

这种方式会导致一个用户无限点赞,明显是不合理的
造成这个问题的原因是,我们现在的逻辑,发起请求只是给数据库+1,所以才会出现这个问题

功能完善

需求:

  1. 同一个用户只能对同一篇笔记点赞一次,再次点击则取消点赞
  2. 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的isLike属性)

实现步骤:

  1. 修改点赞功能,利用Redis中的set集合来判断是否点赞过,未点赞则点赞数+1,已点赞则点赞数-1
  2. 修改根据id查询的业务,判断当前登录用户是否点赞过,赋值给isLike字段
  3. 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段

具体实现

  1. Controller层
    具体业务逻辑写在Service层
@PutMapping("/like/{id}")
public Result likeBlog(@PathVariable("id") Long id) {return blogService.likeBlog(id);
}
  1. BlogServiceImpl业务实现
    在BlogService接口中创建对应方法,在Impl中实现
@Override
public Result likeBlog(Long id) {//1. 获取当前用户信息Long userId = UserHolder.getUser().getId();//2. 如果当前用户未点赞,则点赞数 +1,同时将用户加入set集合String key = BLOG_LIKED_KEY + id;Boolean isLiked = stringRedisTemplate.opsForSet().isMember(key, userId.toString());if (BooleanUtil.isFalse(isLiked)) {//点赞数 +1boolean success = update().setSql("liked = liked + 1").eq("id", id).update();//将用户加入set集合if (success) {stringRedisTemplate.opsForSet().add(key, userId.toString());}//3. 如果当前用户已点赞,则取消点赞,将用户从set集合中移除}else {//点赞数 -1boolean success = update().setSql("liked = liked - 1").eq("id", id).update();if (success){//从set集合移除stringRedisTemplate.opsForSet().remove(key, userId.toString());}}return Result.ok();
}

修改完毕之后,页面上还不能立即显示点赞完毕的后果,我们还需要修改查询Blog业务,判断Blog是否被当前用户点赞过

  1. 查询业务修改
@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);//追加判断blog是否被当前用户点赞,逻辑封装到isBlogLiked方法中isBlogLiked(blog);});return Result.ok(records);
}@Override
public Result queryById(Integer id) {Blog blog = getById(id);if (blog == null) {return Result.fail("评价不存在或已被删除");}queryBlogUser(blog);//追加判断blog是否被当前用户点赞,逻辑封装到isBlogLiked方法中isBlogLiked(blog);return Result.ok(blog);
}private void isBlogLiked(Blog blog) {//1. 获取当前用户信息Long userId = UserHolder.getUser().getId();//2. 判断当前用户是否点赞String key = BLOG_LIKED_KEY + blog.getId();Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());//3. 如果点赞了,则将isLike设置为trueblog.setIsLike(BooleanUtil.isTrue(isMember));
}

至此通过简单的Redis的Set数据结构实现了点赞的功能,set的键值为"blogliked:笔记id",值为点赞的用户id。

点赞排行榜

实现需求

当我们点击探店笔记详情页面时,应该按点赞顺序展示点赞用户,比如显示最早点赞的TOP5,形成点赞排行榜,就跟QQ空间发的说说一样,可以看到有哪些人点了赞。
之前的点赞是放到Set集合中,但是Set集合是无序的存储的,所以这个时候,我们就可以改用SortedSet(Zset)

对比一下这些集合的区别:
20240419-043552-fX.png

实现步骤

  1. 修改BlogServiceImpl
    由于ZSet没有isMember方法,所以这里只能通过查询score来判断集合中是否有该元素,如果有该元素,则返回值是对应的score,如果没有该元素,则返回值为null
@Override
public Result likeBlog(Long id) {//1. 获取当前用户信息Long userId = UserHolder.getUser().getId();//2. 如果当前用户未点赞,则点赞数 +1,同时将用户加入set集合String key = BLOG_LIKED_KEY + id;//尝试获取scoreDouble score = stringRedisTemplate.opsForZSet().score(key, userId.toString());//为null,则表示集合中没有该用户if (score == null) {//点赞数 +1boolean success = update().setSql("liked = liked + 1").eq("id", id).update();//将用户加入set集合if (success) {stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());}//3. 如果当前用户已点赞,则取消点赞,将用户从set集合中移除} else {//点赞数 -1boolean success = update().setSql("liked = liked - 1").eq("id", id).update();if (success) {//从set集合移除stringRedisTemplate.opsForZSet().remove(key, userId.toString());}}return Result.ok();
}
  1. 同时修改isBlogLiked方法,在原有逻辑上,判断用户是否已登录,登录状态下才会继续判断用户是否点赞
private void isBlogLiked(Blog blog) {//1. 获取当前用户信息UserDTO userDTO = UserHolder.getUser();//当用户未登录时,就不判断了,直接return结束逻辑if (userDTO == null) {return;}//2. 判断当前用户是否点赞String key = BLOG_LIKED_KEY + blog.getId();Double score = stringRedisTemplate.opsForZSet().score(key, userDTO.getId().toString());blog.setIsLike(score != null);
}
  1. 继续完善显示点赞列表功能,查看浏览器请求,这个请求目前应该是404的,因为我们还没有写,他需要一个list返回值,显示top5点赞的用户

请求网址: http://localhost:8080/api/blog/likes/4
请求方法: GET

在Controller层中编写对应的方法,点赞查询列表,具体逻辑写到BlogServiceImpl中

@GetMapping("/likes/{id}")
public Result queryBlogLikes(@PathVariable Integer id){return blogService.queryBlogLikes(id);
}

具体逻辑如下:

@Override
public Result queryBlogLikes(Integer id) {String key = BLOG_LIKED_KEY + id;//zrange key 0 4  查询zset中前5个元素Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);//如果是空的(可能没人点赞),直接返回一个空集合if (top5 == null || top5.isEmpty()) {return Result.ok(Collections.emptyList());}List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());//将ids使用`,`拼接,SQL语句查询出来的结果并不是按照我们期望的方式进行排//所以我们需要用order by field来指定排序方式,期望的排序方式就是按照查询出来的id进行排序String idsStr = StrUtil.join(",", ids);//select * from tb_user where id in (ids[0], ids[1] ...) order by field(id, ids[0], ids[1] ...)List<UserDTO> userDTOS = userService.query().in("id", ids).last("order by field(id," + idsStr + ")").list().stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());return Result.ok(userDTOS);
}

需要注意的点是:zrange key 0 4 查询zset中前5个元是我们需要的已经排好序的用户id,但是使用该list拼接sql使用in(,)的时候,返回的结果并不是按照我们期望的方式进行排序,所以我们需要用order by field来指定排序方式,期望的排序方式就是按照查询出来的id进行排序

select * from tb_user where id in (ids[0], ids[1] ...) order by field(id, ids[0], ids[1] ...)

这篇关于Redis进阶——点赞和点赞排行的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/921108

相关文章

RedHat运维-Linux文本操作基础-AWK进阶

你不用整理,跟着敲一遍,有个印象,然后把它保存到本地,以后要用再去看,如果有了新东西,你自个再添加。这是我参考牛客上的shell编程专项题,只不过换成了问答的方式而已。不用背,就算是我自己亲自敲,我现在好多也记不住。 1. 输出nowcoder.txt文件第5行的内容 2. 输出nowcoder.txt文件第6行的内容 3. 输出nowcoder.txt文件第7行的内容 4. 输出nowcode

【Linux进阶】UNIX体系结构分解——操作系统,内核,shell

1.什么是操作系统? 从严格意义上说,可将操作系统定义为一种软件,它控制计算机硬件资源,提供程序运行环境。我们通常将这种软件称为内核(kerel),因为它相对较小,而且位于环境的核心。  从广义上说,操作系统包括了内核和一些其他软件,这些软件使得计算机能够发挥作用,并使计算机具有自己的特生。这里所说的其他软件包括系统实用程序(system utility)、应用程序、shell以及公用函数库等

Windows/macOS/Linux 安装 Redis 和 Redis Desktop Manager 可视化工具

本文所有安装都在macOS High Sierra 10.13.4进行,Windows安装相对容易些,Linux安装与macOS类似,文中会做区分讲解 1. Redis安装 1.下载Redis https://redis.io/download 把下载的源码更名为redis-4.0.9-source,我喜欢跟maven、Tomcat放在一起,就放到/Users/zhan/Documents

为什么要做Redis分区和分片

Redis分区(Partitioning)和分片(Sharding)是将数据分布在多个Redis实例或多个节点上的做法。这种技术用于提高性能、可扩展性和可用性。以下是执行Redis分区和分片的主要原因: 1. **提高吞吐量**:    - 通过将数据分散到多个节点,可以并行处理更多的操作,从而提高整体吞吐量。 2. **内存限制**:    - 单个Redis实例的内存是有限的。分区允许数据

如何理解redis是单线程的

写在文章开头 在面试时我们经常会问到这样一道题 你刚刚说redis是单线程的,那你能不能告诉我它是如何基于单个线程完成指令接收与连接接入的? 这时候我们经常会得到沉默,所以对于这道题,笔者会直接通过3.0.0源码分析的角度来剖析一下redis单线程的设计与实现。 Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源

Redis-在springboot环境下执行lua脚本

文章目录 1、什么lua2、创建SpringBoot工程3、引入相关依赖4、创建LUA脚本5、创建配置类6、创建启动类7、创建测试类 1、什么lua “Lua”的英文全称是“Lightweight Userdata Abstraction Layer”,意思是“轻量级用户数据抽象层”。 2、创建SpringBoot工程 3、引入相关依赖 <?xml version

redis切换数据库的方法【jedis】

package com.test;import redis.clients.jedis.Jedis;public class readredis {public static void main(String[] args) {// 连接本地的 Redis 服务Jedis jedis = new Jedis("127.0.0.1", 6379);jedis.select(10);String v

Redis 高性能基本操作

单元素操作是基础 单元素操作,是指每一种集合类型对单个数据实现增删改查 例如,Hash 类型的 HGET、HSET 和 HDEL,Set 类型的 SADD、SREM、SRANDMEMBER 等这些操作的复杂度由集合采用的数据结构决定,例如,HGET、HSET 和 HDEL 是对哈希表做操作,所以它们的复杂度都是 O(1)Set 类型用哈希表作为底层数据结构时,它的 SADD、SREM、SRAN

redis高级用法

redis 慢日志查询 配置参数 slowlog-log-slower-than 10000 #单位微秒 slowlog-max-len 选项指定服务器最多保存多少条慢查询日志 redis-cli slowlog get #获取慢日志1) 1) (integer) 4 # 日志的唯一标识符(uid)2) (integer) 1378781447 # 命令执

redis在window下安装

1、redis简介 redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hashs(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redi