摘要:
本文介绍了黑马点评中点赞、关注和推送功能的实现方案。点赞功能采用Redis的ZSET结构存储用户点赞数据,实现点赞状态查询、热门博客排行和点赞用户展示。关注功能通过关系表和Redis集合实现用户关注关系管理,包含共同关注查询。推送功能采用推拉结合模式,活跃用户使用推模式实时接收关注用户的内容更新,通过ZSET存储并按时间排序实现滚动分页查询。
一,点赞和点赞排行榜功能
1,根据id查询博客和用户信息
不仅需要查询博客基本信息还要返回用户信息,方便用户知道博客发布者或者关注
@Override
public Result queryBlogById(Long id) {
Blog blog = getById(id);
if (blog==null) {
return Result.fail("博客不存在!");
}
queryUserById(blog);
isBlogLiked(blog);
return Result.ok(blog);
}
2,分页查询热点博客
查询结果根据点赞数“liked”排序,通过forEach循环查询每个热点博客的发布者
@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 -> {
this.isBlogLiked(blog);
this.queryUserById(blog);
});
return Result.ok(records);
}
3,判断当前用户是否点赞博客方法
判断用户为空是为了防止用户未登录报错,如果查询的score不为空说明该博客已经被当前用户点过赞
private void isBlogLiked(Blog blog) {
UserDTO user = UserHolder.getUser();
if (user==null) {
return;
}
Long userId = UserHolder.getUser().getId();
String key = BLOG_LIKED_KEY+blog.getId();
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
blog.setIsLike(score!=null);
}
4,点赞博客功能实现
使用redis的zset结构(key value1 score1 value2 score2......)存储博客点赞的用户信息(userId)和时间戳(System.currentTimeMillis())
(1)如果score为空说明未点赞 ,对应博客的点赞数字段+1,同时将当前用户id和时间戳存入redis的zset集合
(2)如果已点赞,点赞数字段-1,同时删除zset集合的对应元素
@Override
public Result likeBlog(Long id) {
Long userId = UserHolder.getUser().getId();
String key = BLOG_LIKED_KEY+id;
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
//如果未点赞
if (score==null) {
boolean success = this.update().setSql("liked = liked + 1").eq("id", id).update();
if (success) {
stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
}
}else{
boolean success = this.update().setSql("liked = liked - 1").eq("id", id).update();
if (success) {
stringRedisTemplate.opsForZSet().remove(key, userId.toString());
}
}
return Result.ok();
}
5,查询点赞时间前五用户信息(userId)
Zrange key 0 4命令查询zset集合根据score(点赞时间)返回前五的value
(1)因为使用stringRedisTemplate所以返回的是Set<String>集合,这里通过stream流的map映射将字符串转化为Long型
(2)使用通用mapper查询用户信息(ListById(集合))的sql语句使用in(2,3,1....)进行查询,但是mysql返回的结果是id有序的结果,与预想不符(先点赞的人先显示)
(3)所以使用order by field(id,id集合字符串),通过StrUtil将id集合转化为“,”分隔的字符串拼接到order by field中,这样就是按照id集合的顺序返回结果
@Override
public Result queryBlogLikes(Long id) {
//查询top5的点赞数Zrange key 0 4
String key = BLOG_LIKED_KEY+id;
Set<String> set = stringRedisTemplate.opsForZSet().range(key, 0, 4);
if (set==null||set.isEmpty()) {
return Result.ok(Collections.emptyList());
}
List<Long> listIds = set.stream().map(Long::valueOf).toList();
String join = StrUtil.join(",",listIds);
List<UserDTO> userDTOList = userService.query().in("id", listIds)
.last("order by field(id,"+ join +")").list()
.stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).toList();
return Result.ok(userDTOList);
}
二,关注,取关和共同关注
用户和关注的用户之间的联系作为一张表tb_follow表,关注成功将用户id拼接前缀作为key,关注用户id作为元素放到redis的set集合中。
1,判断当前用户是否关注某个用户
public Result isFollow(Long followUserId) {
Long userId = UserHolder.getUser().getId();
Long count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
return Result.ok(count > 0);
}
2,关注与取关
(1)如果关注,将关注信息(当前用户和关注用户)存入数据库,并且存入redis的set集合中
(2)如果取消关注,根据用户id和关注的拥护的id删除数据,并且清理set中的数据
public Result follow(Long followUserId, Boolean isFollow) {
Long userId = UserHolder.getUser().getId();
String key = FOLLOWS_KEY +userId;
if (isFollow) {
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
boolean success = this.save(follow);
if (success) {
stringRedisTemplate.opsForSet().add(key, followUserId.toString());
}
} else {
QueryWrapper<Follow> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId).eq("follow_user_id", followUserId);
boolean success = this.remove(wrapper);
if (success) {
stringRedisTemplate.opsForSet().remove(key, followUserId.toString());
}
}
return Result.ok();
}
3,共同关注
(1)通过当前用户关注表的key和该用户关注用户列表的key求共同关注用户id
(2)根据这些共同用户id查询用户信息,最后映射到UserDTO返回
public Result followCommons(Long id) {
Long userId = UserHolder.getUser().getId();
String key1 = FOLLOWS_KEY +userId;
String key2 = FOLLOWS_KEY +id;
//求当前登录用户和目标用户的交集
Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2);
if (intersect == null || intersect.isEmpty()) {
return Result.ok("还没有没有共同关注!");
}
List<Long> list = intersect.stream().map(Long::valueOf).toList();
List<UserDTO> userDTOList = userService.listByIds(list)
.stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).toList();
return Result.ok(userDTOList);
}
三,feed流实现推送
(1)拉模式(延时高,内存占用较低)
(2)推模式(实时性强,延时低,内存占用高)
(3)推拉结合模式
动态结合推送和拉取,根据用户状态选择最适合的机制,在用户活跃或需要实时更新时使用推送,其他情况则使用定期拉取。
1,基于推模式实现关注推送功能
zrevrangebyscore key max min withscores limit 0 3 滚动查询参数:
max:当前时间戳 | 上一次查询的最小的时间戳,min:0,offset:0 | 上一次查询一样的最小时间戳的个数,count:查询个数
2,发布博客并推送
(1)发布博客时,通过将当前用户id作为被关注id字段,查询笔记作者所有粉丝follows
(2)推送博客id到每一个粉丝收件箱(zset),粉丝id拼接前缀作为key,博客id作为value,且发布时间戳作为score保存到redis中
public Result saveBlog(Blog blog) {
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
boolean success = save(blog);
if (!success) {
return Result.fail("新增笔记失败!");
}
// 查询笔记作者所有粉丝 select userId from tb_follow where follow_user_id = userId
List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
//推送博客id到每一个粉丝收件箱(zset)
for (Follow follow : follows) {
Long followId = follow.getUserId();
String key = FEED_KEY+followId;
stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
}
return Result.ok(blog.getId());
}
3,获取关注的用户的博客信息并且实现滚动查询
(1)前端发送请求时携带变量max和offset,根据这些参数通过zrevrangebyscore命令返回value和score
Long userId = UserHolder.getUser().getId();
String key = FEED_KEY+userId;
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
.reverseRangeByScoreWithScores(key, 0, max, offset, 2);
if (typedTuples==null||typedTuples.isEmpty()) {
return Result.ok();
}
(2)offset默认为1(时间戳最小至少一个),minTime(此次查询的最小时间戳)为0;遍历zset集合取出value即博客id和score即时间戳,如果此次循环的时间戳与最小时间戳相等,累加最小时间戳个数,否则将当前时间戳覆盖最小时间戳并且重置最小时间戳个数为默认值1。
//解析zset中的offset,minTime和所有博客id
int os = 1;
long minTime = 0;
ArrayList<Long> ids = new ArrayList<>(typedTuples.size());
for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
ids.add(Long.valueOf(typedTuple.getValue()));
long time = typedTuple.getScore().longValue();
if (minTime==time) {
os++;
} else{
minTime = time;
os = 1;
}
}
(3)根据id查询blog,并且需按照ids集合里的数据顺序返回结果,且还需要查询每个博客的用户信息以及是否被当前用户点赞
String join = StrUtil.join(",",ids);
List<Blog> blogs = query().in("id", ids)
.last("order by field(id," + join + ")").list();
//查询每一个博客的用户信息和是否被点赞
for (Blog blog : blogs) {
queryUserById(blog);
isBlogLiked(blog);
}
(4)封装ScrollResult并返回前端,下次携带此次信息进行查询
ScrollResult result = new ScrollResult();
result.setList(blogs);
result.setOffset(os);
result.setMinTime(minTime);
return Result.ok(result);