【Redis应用】基于Redis实现排行榜,点赞,关注功能
前言
redis的使用场景有很多,都取决于其支持丰富的数据结构。比如说上一篇基于redis实现的秒杀,我们对其进行优化采用redis中队列,实现异步秒杀。再还有就是我们利用redis最简单的数据结构string实现的缓存。除了上面的这个,其实redis中的set可以实现共同好友,点赞,关注等功能,其zset结构可以实现排行榜的功能。还有另外4种数据结构(BitMap,HyperLoglog等等),也是利用他们的特点优势,实现了其他场景。本章主要是学习利用reids实现排行榜,点赞,关注功能。
点赞/取消点赞的实现
需求:
- 同一个用户只能点赞一次,再次点击则取消点赞
- 如果当前用户已经点赞,则点赞按钮高亮显示(前端实现,判断字段Blog类的isLike属性)
实现步骤:
- 给Blog类中添加一个isLike字段,标示是否被当前用户点赞
- 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞则点赞-1;
- 修改根据id查询的业务,判断当前登录用户是否点赞过,赋值给isLike字段
- 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段
在Blog类中添加一个isLike字段
/**
* 是否点赞
*/
@TableField(exist = false)
private Boolean isLike;
修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞则点赞-1
/**
* 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞则点赞-1;
*/
@Override
public Result likeBlog(Long id) {
// 1.获取登录用户
Long userId = UserHolder.getUser().getId();
// 2.判断用户是否点过赞
Boolean isLike = stringRedisTemplate.opsForSet().isMember(RedisConstants.BLOG_LIKED_KEY + id, userId.toString());
if (BooleanUtil.isTrue(isLike)){
// 点赞 ; 取消用户点赞,
boolean update = update().setSql("liked = liked - 1").eq("id", id).update();
if (!update) {
return Result.fail(SystemConstants.BLOG_CANCEL_LIKE_FAIL);
}
stringRedisTemplate.opsForSet().remove(RedisConstants.BLOG_LIKED_KEY+id,userId.toString());
}else {
boolean update = update().setSql("liked = liked + 1").eq("id", id).update();
if (!update) {
return Result.fail(SystemConstants.BLOG_LIKE_FAIL);
}
stringRedisTemplate.opsForSet().add(RedisConstants.BLOG_LIKED_KEY+id,userId.toString());
}
return Result.ok();
}
点赞排行榜的实现
一个事物的好坏,取决于客户对其的评价,所以在很多平台上面我们经常会看见评分,点赞等。最后通过统计,罗列出一个排行榜。排行榜的实现其实有很多的方法。在这里对最早点赞的人(top5)进行显示,前面我们使用的是redis中的set对用户,文章id进行存储,并没有排序功能,在想其实redis中有一种数据结构是可以在满足数据唯一的基础上实现排序的,也就是我们所说的zset。
需求:
在文章详情处,将点赞人数显示出来,比如最早(按时间)点赞的top5,形成点赞排行榜
接口设计
说明 | |
---|---|
请求方式 | GET |
请求路径 | /blog/likes/{id} |
请求参数 | id:blogId |
返回值 | List 给这个笔记点赞的topn用户集合 |
代码实现
上面的点赞实现呢,我们是采用set的数据结构进行数据存储的,现在我们修改一下原有的数据结构,将数据结构转换成zset进行存储。
/**
* 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞则点赞-1;
*/
@Override
public Result likeBlog(Long id) {
// 1.获取登录用户
Long userId = UserHolder.getUser().getId();
// 2.判断用户是否点过赞
Double score = stringRedisTemplate.opsForZSet().score(RedisConstants.BLOG_LIKED_KEY + id, userId.toString());
if (score != null){
// 点赞 ; 取消用户点赞,
boolean update = update().setSql("liked = liked - 1").eq("id", id).update();
if (!update) {
return Result.fail(SystemConstants.BLOG_CANCEL_LIKE_FAIL);
}
stringRedisTemplate.opsForZSet().remove(RedisConstants.BLOG_LIKED_KEY+id,userId.toString());
}else {
boolean update = update().setSql("liked = liked + 1").eq("id", id).update();
if (!update) {
return Result.fail(SystemConstants.BLOG_LIKE_FAIL);
}
stringRedisTemplate.opsForZSet().add(RedisConstants.BLOG_LIKED_KEY+id,userId.toString(),System.currentTimeMillis());
}
return Result.ok();
}
接下来就是接口的编写了
- 添加/likes/{id}的请求路径及方法
- 获取zset中排名top5的用户id
@Override
public Result queryBlogLikes(Long id) {
// 根据文章id,查询redis中排名前五的用户信息
Set<String> range = stringRedisTemplate.opsForZSet().range(RedisConstants.BLOG_LIKED_KEY + id, 0, 4);
if (range == null || range.isEmpty()) {
return Result.ok(Collections.emptyList());
}
// 获取用户id
List<Long> ids = range.stream().map(Long::valueOf).toList();
List<UserDTO> topUserList = new ArrayList<>();
List<UserDTO> userDTOList = ids.stream().map(u -> {
User user = userService.getById(u);
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
topUserList.add(userDTO);
return userDTO;
}).collect(Collectors.toList());
return Result.ok(userDTOList);
}
关注/取关,共同关注(好友),关注推送功能
分析:
关注/取关:其实和点赞的实现是相似的。 (这边按照课程的方式做了分笔记,后期会使用redis的方式也做一遍
共同关注:其实就是在redis中set求交集
关注推送:
关注/取关
需求:基于该表数据结构,实现两个接口:
- 关注和取关接口
- 判断是否关注的接口
@Override
public Result follow(Long followUserId, Boolean isFollow) {
UserDTO user = UserHolder.getUser();
if (user == null) {
return Result.fail(SystemConstants.USER_UN_LOGIN);
}
// 1.判断到底是关注还是取关
if (isFollow){
// 2.关注,新增数据
Follow follow = new Follow();
follow.setUserId(user.getId());
follow.setFollowUserId(followUserId);
save(follow);
}else {
// 3.取关,删除
remove(new QueryWrapper<Follow>().eq("user_id", user.getId()).eq("follow_user_id", followUserId));
// remove(lambdaQuery().eq(Follow::getUserId,user.getId()).eq(Follow::getFollowUserId,followUserId));
}
return Result.ok();
}
@Override
public Result isFollow(Long followUserId) {
Long userId = UserHolder.getUser().getId();
// 1.查询是否关注
Long count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
return Result.ok(count > 0);
}
共同关注
@Override
public Result follow(Long followUserId, Boolean isFollow) {
UserDTO user = UserHolder.getUser();
if (user == null) {
return Result.fail(SystemConstants.USER_UN_LOGIN);
}
// 1.判断到底是关注还是取关
if (isFollow){
// 2.关注,新增数据
Follow follow = new Follow();
follow.setUserId(user.getId());
follow.setFollowUserId(followUserId);
boolean save = save(follow);
if (save) {
// 吧关注的用户id,放入redis的set集合 sadd userId followerUserId
stringRedisTemplate.opsForSet().add(RedisConstants.USER_FOLLOW_KEY+user.getId(),followUserId.toString());
}
}else {
// 3.取关,删除
remove(new QueryWrapper<Follow>().eq("user_id", user.getId()).eq("follow_user_id", followUserId));
// remove(lambdaQuery().eq(Follow::getUserId,user.getId()).eq(Follow::getFollowUserId,followUserId));
stringRedisTemplate.opsForSet().remove(RedisConstants.USER_FOLLOW_KEY+user.getId());
}
return Result.ok();
}
controller创建一个方法followCommons
@Override
public Result followCommon(Long id) {
// 获取目标用户和当前用户的id
Long userId = UserHolder.getUser().getId();
String preKey = RedisConstants.USER_FOLLOW_KEY;
// 先去redis缓存中查找,
Set<String> intersect = stringRedisTemplate.opsForSet().intersect(preKey + id, preKey + userId);
// 如果缓存没有再去redis查找 (以防redis宕机,数据无持久化)
if (intersect == null || intersect.isEmpty()) {
// todo 去数据库中查找数据(数据库有数据,将数据返回,数据库无数据空集)
log.info("去数据库中查找数据");
}
// 提取出id
List<Long> ids = intersect.stream().map(Long::valueOf).toList();
// 查询用户信息,并返回
List<UserDTO> userDTOS = userService.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());
return Result.ok(userDTOS);
}
关注推送
关注推送,也叫做Feed流(投喂),为用户持续的提高“沉浸式”的体验,通过无限下拉刷新获取新的信息
应用持续自动的匹配用户行为
Feed流
Feed流的模式:
Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注,例如朋友圈
- 优点:信息全面,不会有缺失,并且实现也相对简单
- 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
智能排序:利用智能算法屏蔽违规的,用户不感兴趣的内容,推送用户感兴趣的信息来吸引用户
- 优点:投喂用户感兴趣的信息,用户黏度很高,容易沉迷
- 缺点:如果算法不精准,可能起到反作用
Feed流的实现方案
- 拉模式:读扩散
- 推模式:写扩散
- 推拉结合模式(读写混合)
拉模式 | 推模式 | 推拉模式 | |
---|---|---|---|
写比例 | 低 | 高 | 中 |
读比例 | 高 | 低 | 中 |
用户读取延迟 | 高 | 低 | 低 |
实现难度 | 复杂 | 简单 | 很复杂 |
使用场景 | 很少使用 | 用户量少,没有大v | 过千万的用户量,有大v |
基于推模式实现关注推送功能
需求:
- 原来我们的文章发布业务是直接将数据保存到数据库的,现在需要将其进行修改,在保存到数据库的同时,将文章推送到粉丝邮箱
- 收件箱满足可以根据时间搓排序,必须用redis的数据结构实现
- 查询收件箱数据时,可以实现分页查询(采用滚动分页的形式进行数据查询)
接口设计:
说明 | |
---|---|
请求方式 | GET |
请求路径 | /blog/of/follow |
请求参数 | lastId:上一次查询的最小时间戳,offset:偏移量 |
返回值 | List :小于指时间戳的笔记集合;minTime:本次查询的推送的最小时间戳;offset:偏移量 |
@Override
public Result saveBlog(Blog blog) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 保存探店博文
boolean saved = save(blog);
if (!saved) {
return Result.fail(SystemConstants.BLOG_SAVE_FAIL);
}
// 保存成功将笔记id发送给所有粉丝
List<Follow> followList = followService.lambdaQuery().eq(Follow::getFollowUserId, user.getId()).list();
// 查询笔记作者的所有粉丝
for (Follow follow : followList) {
Long userId = follow.getUserId();
// 数据推送 // 将笔记id 发送给所有粉丝
stringRedisTemplate.opsForZSet().add(RedisConstants.BLOG_FEED_KEY+userId,blog.getId().toString(),System.currentTimeMillis());
}
return Result.ok(blog.getId());
}
/**
* 3. 查询收件箱数据时,可以实现分页查询(采用滚动分页的形式进行数据查询)
* @param max
* @param offset
* @return
*/
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
// 1.获取当前用户id
Long userId = UserHolder.getUser().getId();
// 2.查询收件箱
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().
reverseRangeByScoreWithScores(RedisConstants.BLOG_FEED_KEY + userId, 0, max, offset, 2L);
if (typedTuples == null || typedTuples.isEmpty()){
return Result.ok();
}
// 解析数据: blogId,minTime(时间搓),offset
List<Long> ids = new ArrayList<>(typedTuples.size());
long minTime = 0;
int os = 1;
for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {
// 获取用户id
ids.add(Long.valueOf(Objects.requireNonNull(tuple.getValue())));
// 获取时间搓
long time = Objects.requireNonNull(tuple.getScore()).longValue();
if (time == minTime){
os++;
}else {
minTime = time;
os = 1;
}
}
// 根据id查询blog
List<Blog> blogList = new ArrayList<>();
List<Blog> blogs = ids.stream().map(id -> {
Blog blog = getById(id);
// 查询blog相关用户
queryBlogUser(blog);
// 查询blog是否被点赞
isBlogLiked(blog);
blogList.add(blog);
return blog;
}).collect(Collectors.toList());
// 滚动分页的封装
ScrollResult scrollResult = new ScrollResult();
scrollResult.setList(blogs);
scrollResult.setOffset(os);
scrollResult.setMinTime(minTime);
return Result.ok(scrollResult);
}
转载自:https://juejin.cn/post/7351728888983224356