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

相关文章

Redis 基本数据类型和使用详解

《Redis基本数据类型和使用详解》String是Redis最基本的数据类型,一个键对应一个值,它的功能十分强大,可以存储字符串、整数、浮点数等多种数据格式,本文给大家介绍Redis基本数据类型和... 目录一、Redis 入门介绍二、Redis 的五大基本数据类型2.1 String 类型2.2 Hash

Redis中Hash从使用过程到原理说明

《Redis中Hash从使用过程到原理说明》RedisHash结构用于存储字段-值对,适合对象数据,支持HSET、HGET等命令,采用ziplist或hashtable编码,通过渐进式rehash优化... 目录一、开篇:Hash就像超市的货架二、Hash的基本使用1. 常用命令示例2. Java操作示例三

Redis中Set结构使用过程与原理说明

《Redis中Set结构使用过程与原理说明》本文解析了RedisSet数据结构,涵盖其基本操作(如添加、查找)、集合运算(交并差)、底层实现(intset与hashtable自动切换机制)、典型应用场... 目录开篇:从购物车到Redis Set一、Redis Set的基本操作1.1 编程常用命令1.2 集

Redis中的有序集合zset从使用到原理分析

《Redis中的有序集合zset从使用到原理分析》Redis有序集合(zset)是字符串与分值的有序映射,通过跳跃表和哈希表结合实现高效有序性管理,适用于排行榜、延迟队列等场景,其时间复杂度低,内存占... 目录开篇:排行榜背后的秘密一、zset的基本使用1.1 常用命令1.2 Java客户端示例二、zse

Redis中的AOF原理及分析

《Redis中的AOF原理及分析》Redis的AOF通过记录所有写操作命令实现持久化,支持always/everysec/no三种同步策略,重写机制优化文件体积,与RDB结合可平衡数据安全与恢复效率... 目录开篇:从日记本到AOF一、AOF的基本执行流程1. 命令执行与记录2. AOF重写机制二、AOF的

Redis高性能Key-Value存储与缓存利器常见解决方案

《Redis高性能Key-Value存储与缓存利器常见解决方案》Redis是高性能内存Key-Value存储系统,支持丰富数据类型与持久化方案(RDB/AOF),本文给大家介绍Redis高性能Key-... 目录Redis:高性能Key-Value存储与缓存利器什么是Redis?为什么选择Redis?Red

Redis 的 SUBSCRIBE命令详解

《Redis的SUBSCRIBE命令详解》Redis的SUBSCRIBE命令用于订阅一个或多个频道,以便接收发送到这些频道的消息,本文给大家介绍Redis的SUBSCRIBE命令,感兴趣的朋友跟随... 目录基本语法工作原理示例消息格式相关命令python 示例Redis 的 SUBSCRIBE 命令用于订

sky-take-out项目中Redis的使用示例详解

《sky-take-out项目中Redis的使用示例详解》SpringCache是Spring的缓存抽象层,通过注解简化缓存管理,支持Redis等提供者,适用于方法结果缓存、更新和删除操作,但无法实现... 目录Spring Cache主要特性核心注解1.@Cacheable2.@CachePut3.@Ca

Redis实现高效内存管理的示例代码

《Redis实现高效内存管理的示例代码》Redis内存管理是其核心功能之一,为了高效地利用内存,Redis采用了多种技术和策略,如优化的数据结构、内存分配策略、内存回收、数据压缩等,下面就来详细的介绍... 目录1. 内存分配策略jemalloc 的使用2. 数据压缩和编码ziplist示例代码3. 优化的

redis-sentinel基础概念及部署流程

《redis-sentinel基础概念及部署流程》RedisSentinel是Redis的高可用解决方案,通过监控主从节点、自动故障转移、通知机制及配置提供,实现集群故障恢复与服务持续可用,核心组件包... 目录一. 引言二. 核心功能三. 核心组件四. 故障转移流程五. 服务部署六. sentinel部署