展厅网站,建立网站买空间哪家好,南京招投标中心官网,163邮箱注册申请注册官网Redis实战篇笔记#xff08;七#xff09; 文章目录 Redis实战篇笔记#xff08;七#xff09;前言达人探店发布和查看探店笔记点赞点赞排行榜 好友关注关注和取关共同关注关注推送关注推荐的实现 总结 前言
本系列文章是Redis实战篇笔记的最后一篇#xff0c;那么到这里…Redis实战篇笔记七 文章目录 Redis实战篇笔记七前言达人探店发布和查看探店笔记点赞点赞排行榜 好友关注关注和取关共同关注关注推送关注推荐的实现 总结 前言
本系列文章是Redis实战篇笔记的最后一篇那么到这里Redis实战篇的内容就要结束了本系列文件涵盖了Redis作为缓存在实战项目中的大多数用法 达人探店 发布和查看探店笔记
这个两个功能就是普通的业务没有用到 redis所以我把他们合到一起了
发布探店笔记我们点击下面的加号就可以发布一篇探店笔记这里的上传图片如果是一般的业务会上传到一个文件服务器但是他这里选择上传到了我们的前端服务器这个 IMAGE_UPLOAD_DIR 就是前端服务器放图片的地方要改成自己所对应的位置我们像这样就写好了一篇探店笔记然后我们点发布就会跳转到个人中心并且在主页的最后也能看见我们发布的笔记
我们点击看我们刚发布的笔记会报错是因为我们还没有实现这个功能我们点击查看就可以看到前端访问的接口接下来我们就去实现它
**这就是 查看笔记的方法 ** queryBlogById 是我们要实现的方法 querHotBlog 是代码已经写好的。由于没有涉及到 Redis 的操作这里就不再过多解释了。
Resourceprivate IUserService userService;Overridepublic Result queryHotBlog(Integer current) {// 根据用户查询PageBlog page this.query().orderByDesc(liked).page(new Page(current, SystemConstants.MAX_PAGE_SIZE));// 获取当前页数据ListBlog records page.getRecords();// 查询用户records.forEach(this::queryBlogUser);return Result.ok(records);}Overridepublic Result queryBlogById(Long id) {// 1. 查询blogBlog blog getById(id);if(blognull){return Result.fail(笔记不存在);}// 2. 查询blog有关的用户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());}实现后效果就是这样了。 点赞
代码中实现的点赞是 连续点赞的功能但是这样的功能是不好的如果有人调用这个接口一直刷赞数据库直接就爆了所以我们要对这个功能进行改造
改造后的需求
同一个用户只能点赞一次再次点击则取消点赞如果当前用户已经点赞则点赞按钮高亮显示前端已实现判断Blog类的 isLike 属性
那我们怎样来标记用户是否点赞过 用 Redis 的 set集合可以实现set 集合我们已经用了好几次了set中的元素是不能重复的可以用来标记 Overridepublic Result likeBlog(Long id) {// 1.获取登录用户Long userId UserHolder.getUser().getId();// 2.判断当前登录用户是否已经点赞Boolean isMember stringRedisTemplate.opsForSet().isMember(BLOG_LIKED_KEY, userId.toString());// 3.如果未点赞可以点赞if(BooleanUtil.isFalse(isMember)){// 起始这里我觉得也可以做一个异步任务利用 Redis的高效性去实现与用户的交互// 用异步任务来去修改数据库感觉 Redis 和 数据库都可以这么来用// 3.1 数据库点赞数1boolean isUpdate update().setSql(likedliked1).eq(id, id).update();// 3.2 保存用户到 Redis的 set 集合if(isUpdate){stringRedisTemplate.opsForSet().add(BLOG_LIKED_KEY,userId.toString());}}else {// 4. 如果已经点赞取消点赞// 4.1 数据库点赞数 -1boolean isUpdate update().setSql(likedliked-1).eq(id, id).update();// 4.1 把用户从Redis的 set 集合移除if(isUpdate){stringRedisTemplate.opsForSet().remove(BLOG_LIKED_KEY,userId.toString());}}return Result.ok();}点赞排行榜
我们在探店笔记的详情页面应该按照点赞的时间显示出来比如最早点赞的 TOP5形成点赞排行榜但是我们刚才在实现点赞功能的时候用的是 set 集合但 set 集合中的元素是无序的这就不符合我们的功能所以我们要换一个数据结构即能保留 set 的无序特点又会使其中的元素有序那就是 Redis 的 SortedSet那接下来我们就要去改造一下我们之前写的点赞功能
其实就是把之前用 set 集合的操作 换成 SortSet Overridepublic Result likeBlog(Long id) {// 1.获取登录用户Long userId UserHolder.getUser().getId();// 2.判断当前登录用户是否已经点赞Double score stringRedisTemplate.opsForZSet().score(BLOG_LIKED_KEY, userId.toString());// 3.如果未点赞可以点赞if(scorenull){// 3.1 数据库点赞数1boolean isUpdate update().setSql(likedliked1).eq(id, id).update();// 3.2 保存用户到 Redis的 set 集合if(isUpdate){stringRedisTemplate.opsForZSet().add(BLOG_LIKED_KEY,userId.toString(),System.currentTimeMillis());}}else {// 4. 如果已经点赞取消点赞// 4.1 数据库点赞数 -1boolean isUpdate update().setSql(likedliked-1).eq(id, id).update();// 4.1 把用户从Redis的 set 集合移除if(isUpdate){stringRedisTemplate.opsForZSet().remove(BLOG_LIKED_KEY,userId.toString());}}return Result.ok();}private void isLiked(Blog blog){// 1.获取登录用户Long userId UserHolder.getUser().getId();// 2.判断当前登录用户是否已经点赞Double score stringRedisTemplate.opsForZSet().score(BLOG_LIKED_KEY, userId.toString());blog.setIsLike(score!null);}**那接下来我们就去实现这个点赞排行榜
Overridepublic Result queryBlogLikes(Long id) {String key BLOG_LIKED_KEYid;// 1. 查询 top5 的点赞用户 zrange key 0 4SetString top5 stringRedisTemplate.opsForZSet().range(key, 0, 4);if(top5null||top5.isEmpty()){return Result.ok(Collections.emptyList());}// 这里用了大量的 stream流来处理集合不太懂 stream流的朋友可以先去学习一下stream流ListLong ids top5.stream().map(Long::valueOf).collect(Collectors.toList());String idStr StrUtil.join(,, ids);// 这里的 sql没有默认的是因为默认的排序它会按 id 降序排列不符合我们的需求。ListUserDTO userDTOS userService.query().in(id, ids).last(ORDER BY FIELD(id, idStr )).list().stream() // 这里处理一下是脱敏.map(user - BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());return Result.ok(userDTOS);}
// 因为未登录用户会获取 user失败所以这里加一下其实这样做是不好的业务容易乱以后未登录
// 用户要使用的地方肯定还会有所以建议还是写在 拦截器里。private void isLiked(Blog blog){// 1.获取登录用户UserDTO user UserHolder.getUser();if (usernull){return;}Long userId user.getId();// 2.判断当前登录用户是否已经点赞Double score stringRedisTemplate.opsForZSet().score(BLOG_LIKED_KEY, userId.toString());blog.setIsLike(score!null);}至此达人探店的模块我们也学完了在这个模块我们主要去使用了 Redis 中关于 set 和 SortedSet 的使用。 好友关注 关注和取关
这个功能也没有用到 Redis 也只是简单的业务这里也就简单的记一下其实这也是个挺常见的功能不跟视频自己也可以手敲出来这里就只记录 service层的代码 Overridepublic Result follow(Long followUserId, Boolean isFollow) {Long userId UserHolder.getUser().getId();// 1.判断到底是关注还是取关if(isFollowtrue){Follow follow new Follow();follow.setUserId(userId);follow.setFollowUserId(followUserId);save(follow);}else {remove(new QueryWrapperFollow().eq(user_id,userId).eq(follow_user_id,followUserId));}return Result.ok();}Overridepublic Result isFollow(Long followUserId) {Long userId UserHolder.getUser().getId();// 1.查询是否关注Integer count query().eq(user_id, userId).eq(follow_user_id, followUserId).count();return Result.ok(count0);}共同关注
其实之后的课程有点为了练这个 Redis 而去开发的这个功能但还是学完吧也没有几节了既然要实现 共同关注那肯定要 先获取 两个用户的关注列表然后去求交集那在 Redis 中 set 集合是可以求交集的所以我们这次用 set 集合来实现求共同关注的功能。在实现这个功能之前我们先来实现下面两段代码这两段 代码与共同关注没有什么关系是用来完善用户的一些信息
// UserController
// 这个是用来点击头像进入主页
GetMapping({id})public Result queryUserById(PathVariable(id) Long userId){User user userService.getById(userId);if(usernull){return Result.ok();}UserDTO userDTO BeanUtil.copyProperties(user, UserDTO.class);return Result.ok(userDTO);}
// BlogController
// 这个是进入主页后显示这个博主的博客
GetMapping(/of/user)public Result queryBlogByUserId(RequestParam(value current,defaultValue 1)Integer current,RequestParam(id) Long id){PageBlog page blogService.query().eq(user_id, id).page(new Page(current, SystemConstants.MAX_PAGE_SIZE));ListBlog records page.getRecords();return Result.ok(records);}下面我们就来写共同关注的代码首先我们要把之前写的关注稍微改一下就是操作完数据库后把关注列表形成一个 set 添加到 Redis 中。
Overridepublic Result follow(Long followUserId, Boolean isFollow) {Long userId UserHolder.getUser().getId();// 1.判断到底是关注还是取关if(isFollowtrue){Follow follow new Follow();follow.setUserId(userId);follow.setFollowUserId(followUserId);boolean save save(follow);if(save){stringRedisTemplate.opsForSet().add(follows:userId,followUserId.toString());}}else {remove(new QueryWrapperFollow().eq(user_id,userId).eq(follow_user_id,followUserId));stringRedisTemplate.opsForSet().remove(follows:userId,followUserId.toString());}return Result.ok();}然后我们来写 共同关注
Overridepublic Result followCommons(Long id) {// 这里还是用来一些流操作的不熟悉的朋友还是建议去看看流。// 其实这里真正要说的也就是求交集了 set 的 intersect命令Long userId UserHolder.getUser().getId();String keyfollows:userId;String followedKeyfollows:id;SetString intersect stringRedisTemplate.opsForSet().intersect(key, followedKey);if(intersectnull||intersect.isEmpty()) return Result.ok(Collections.emptyList());ListLong ids intersect.stream().map(Long::valueOf).collect(Collectors.toList());ListUserDTO users userService.listByIds(ids).stream().map(user - BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());return Result.ok(users);}其实上面两个真的没有什么太多新的东西,用到的 Redis 的部分也是比较少的,想要学 Redis 的朋友也可以跳过这两节. 关注推送
关注推送也叫 Feed 流,直译为 投喂,为用户持续的提供 “沉浸式” 的体验,通过无限下拉获取新的信息
Feed 流产品有两种常见模式:
TimeLine: 不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注.例如朋友圈 优点: 信息全面,不会缺失.并且实现也相对简单缺点: 信息噪音较多,用户不一定感兴趣,内容获取效率低 智能排序: 利用智能算法屏蔽掉违规的,用户不感兴趣的内容,推送用户感兴趣的信息来吸引用户 优点: 投喂用户感兴趣信息,用户粘度高,容易沉迷缺点: 如果算法不精确,可能起反作用
我们这个 个人页面,是基于关注的好友来做 Feed 流,因此采用 TIMELine 的模式,该模式的实现有三种
拉模式推模式推拉结合
拉模式
每个博主都会有一个发件箱,当他们发布消息的时候,都会先发到他们自己的发件箱当中,并且都带上时间戳,然后当有一个用户下拉刷新它的收件箱的时候,这时候,系统会从这个用户所关注的博主的发件箱中拉取信息,然后按时间戳排序.下面这个国就演示了这个过程,但是我们想想,它每下拉一次我们就都要给它拉取一次,并且还要排序,那这样性能是不是就不是很好,那我们接下来继续看 推模式
推模式
而推模式就与拉模式不太一样了,每个博主没有收件箱 了,而是把信息直接发给粉丝的收件箱,并且在收件箱内部排好序,这样粉丝下来刷新的时间,就直接从收件箱中取就可以了.这样就弥补了拉模式的效率问题.但是推模式同样有一个问题,就是如果有一个博主的粉丝很多,那它要给粉丝发消息就要发多份,这个数据量上来了,系统也没法承受,那么能不能把 这两种模式的优点结合起来呢,那就是接下的推拉模式了
推拉模式
在推拉模式中,我们将博主分为 大V 和普通博主,大V的粉丝数很多,通常几千万,而普通博主的粉丝数就比较少了.我们也把粉丝分为普通粉丝和活跃粉丝.对于 大V来说,他的粉丝数很多,所以肯定不能用推模式,所以就用拉模式.但是对于一些活跃粉丝还是用推模式,因为这些活跃粉丝经常取看他们博主的信息,所以效率要高一些,而对于哪些普通粉丝就用拉模式,因为他们对博主的关注也不是很多,所以效率吗,慢一点也就慢一点了,而对于普通博主来说,他的粉丝数目没有那么多,所以用推模式也耗费不少资源.下面我们再来对比一下这三种模式的优缺点.那对于我们这个系统,不会有大v,所以我们这采用推模式来实现. 关注推荐的实现
需求
修改新增探店笔记的业务在保存 blog 到数据库的时候推送的收件箱收件箱满足可以根据时间戳排序必须用 Redis 的数据结构实现查询收件箱时可以实现分页查询。
我们先来修改新增博客的业务这个业务用到了 Redis的 set结构来作为用户的收件箱并把这个博客推送到这个博客的主人的粉丝的收件箱。 Overridepublic Result saveBlog(Blog blog) {// 获取登录用户UserDTO user UserHolder.getUser();blog.setUserId(user.getId());// 保存探店博文boolean isSuccess save(blog);if(!isSuccess){return Result.fail(新增笔记失败);}ListFollow follows followService.query().eq(follow_user_id, user.getId().toString()).list();for (Follow follow : follows) {//获取粉丝 idLong userId follow.getUserId();//推送String keyfeeds:userId;stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());}// 返回idreturn Result.ok(blog.getId());}然后我们再来实现粉丝查看自己的收件箱展示出这个用户所关注的博主的文章但是我们这里想一下这里的分页还能是传统的分页吗我们看下面这条图t1时查询5条但是 t2这是又传过来了一个数据t3这时候又查询5条数据从头开始查的话会查重一个。这就不是我们想要的那怎么解决查重就是滚动查询在滚动查询的时候t1 和 t2 都是与上述一样但是 t3时刻读取第二页是从上回 的lastId 的下一个开始查的这样就避免了查重但是在 Redis 中如何实现呢我们可以利用 SortedSet 来实现。**SortedSet 中有一条命令是 **ZREVRANGEBYSCORE 是用 score 来搜索其中 max ,min是排序的范围max是最大值min是最小值。WITHSCORES是返回时带着分数 offset是偏移量是从最大值的哪一个开始排序0就是从最大值开始1就是从最大值的下一个开始。count就是查几个。
那么我们就可以用时间戳来当分数最新的时间戳就是最大的分数排在第一位。第一次的时候可以拿当前时间戳因为对于当前来说当前时间戳是最大的最小值我们不关心就用 0. 第一次 offset 用0因为第一次分数最大的我们也要。然后往后 max 就应该是上回查询的最小分数最小还是0但是这时的 offset就应该是 1了因为这次的最小分数是上一会的最小元素我们上回已经查过了这次不需要了。所以 offset 要用 1. 具体如下但是如果两个元素的时间戳一样怎么办如果这个用户关注了很多博主这些博主可能会在同一时间发布文章都会推送到这个用户的收件箱。我们看下面的图看一下有什么问题
我们看一下m7和m6的分数都是 6第二次查看的时候还是出现了 6这是为什么因为我们第二次查询的时候 max 是上回的 min上回的 min是6而我们的第二次的 offser 是 1也就是 从分数为 6 的下一个开始那分数为6 的从上往下 第一个是 m7第二个是 m6那可不是要从 m6开始查嘛所以我们的 offset也要改就是上一次最小分数的个数是多少我们下一次的 offset就是多少还是这个我们来开6有两个那第二次我们的 offser 就是 2分数为6的第一个是m7,往下移动 2位不就刚好把 上一次我们查到的 分数为6的隔过去了嘛、有的朋友可能这里会有的疑惑如果我 m5 也是 6你offset不就是 3了不就把 m5 也隔过去了其实不是这样的查重复的我们只在上一次我们查到的里面查重复不是对于整个 set 查。下面看效果
那思路有了代码怎么实现呢我们先来看接口的规范
我们在一次查询中就要把 本次的最小时间戳和下一次要用的偏移量算出来传给前端前端下一次再调用这个接口的时候就用这两个。下面是具体代码实现 Overridepublic Result queryBlogOfFollow(Long max, Integer offset) {//1. 获取当前用户Long userId UserHolder.getUser().getId();//2. 查询收件箱 ZREVRANGEBYSCORE key max min LIMIT offset countString keyfeeds:userId;//3. 解析数据blogId,timestamp,offset// 这里的 TypedTuple 是一个元组里面有你要查的数据以及分数SetZSetOperations.TypedTupleString typedTuples stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 3);// 非空判断if(typedTuplesnull||typedTuples.isEmpty()){return Result.ok();}//4. 根据id查询blogListLong idsnew ArrayList(typedTuples.size());long minTime0;int os1;// 接下来就是算 mintime 和 offset,其实这里我们还是用了一点点小算法用一个 mintime// 变量来接受最小时间戳然后每次从元组获取到时间戳我们就赋给 mintime这样遍历完// 后mintime 就是最小的// 然后是 算 offset,这里我们根据 mintime我们刚才不是说了嘛遍历的过程中每获取一次// time 就赋给 mintime,那么我们在赋之前加一步判断当前获取的这个 time 与 mintime//是否相等相等就让 os不相等就让 minTimetime,最后重置 os到最后 os 一定是// 最小的时间戳的重复次数。// 其实else 里面 的赋值可以去掉因为最后还会赋值。for (ZSetOperations.TypedTupleString typedTuple : typedTuples) {//4.1 获取博客idids.add(Long.valueOf(typedTuple.getValue()));long time typedTuple.getScore().longValue();if(timeminTime){os;}else {minTimetime;os1;}minTime time;}//5. 封装并返回String idStr StrUtil.join(,, ids);ListBlog blogs query().in(id, ids).last(ORDER BY FIELD(id, idStr )).list();for (Blog blog : blogs) {// 查询 blog 有关的用户queryBlogUser(blog);// 查询blog 是否被点赞isLiked(blog);}ScrollResult scrollResult new ScrollResult();scrollResult.setList(blogs);scrollResult.setOffset(os);scrollResult.setMinTime(minTime);return Result.ok(scrollResult);}那么今天关于好友关注这个模块就学完了虽然前面的比较简单但是最后一个理解起来还是有一定难度的 总结
最后的最后还是希望Redis实战篇系列比较可以对大家的学习以及工作有一定的帮助那我们的实战篇笔记就到这里撒花完结了朋友们我们高级篇再见。
我是Mayphyr,从一点点到亿点点我们下次再见