一)基于Set集合实现点赞功能:
在我们的博客表当中,每一篇博客信息都有一个like字段,表示点赞的数量
需求:
1)同一个用户只能点赞一次,再次进行点赞则会被取消;
2)如果当前用户已经点赞过了,那么点赞按钮高亮显示,这个是先是依靠前端来进行实现的,是通过判断Blog类的isLike属性;
实现步骤:
1)给Blog类中添加一个isLike字段,
表示当前用户是否被当前用户点过赞
,如果在前端判断这个isLike存在,那么说明用户点赞过,那么直接将用户按钮变亮,如果不存在那么直接变黑,我们其实可以再次创建一张表,这张表就去记录blogID和给这个Blog点赞的userID,那么每当点赞一次,这张表就纪录了一次;
2)修改点赞功能,利用Redis的Set集合判断是否被点赞过,没有点赞过那么点赞数+1,将用户ID存储到Set集合里面,如果已经点赞过,那么点赞数-1,将用户ID从Set集合中去除
3)
在博客列表页和博客详情页里面,前端就要进行查询blog中的islike字段,来进行判断当前用户是否被点赞过
@RequestMapping @Controller public class UserController { @Autowired private StringRedisTemplate template; @Autowired private DemoMapper mapper; @RequestMapping("/Java100") @ResponseBody public void InsertLike(Integer userID,Integer blogID){ //1.正常情况下应该从session中获取到当前用户信息,此处为了方便实现直接从前端传递userID //2.判断当前用户是否已经针对这一篇文章点过赞了 String key="blog:liked"+blogID; Boolean flag=template.opsForSet().isMember(key,userID.toString()); if(!flag){ //1.如果查询不到,说明当前用户没有点过赞,没有点过赞,点赞数量+1 mapper.updateAddlike(blogID); //2.向redis中的set集合存放用户信息 template.opsForSet().add(key, String.valueOf(userID)); }else{ //1.如果查询到了,说明用户已经点过赞了,点赞数-1 mapper.updateSublike(blogID); //2.将用户ID从redis的有序集合中去除 template.opsForSet().remove(key,String.valueOf(userID)); } } public boolean islike(int blogID,int userID){ String key="blog:liked"+blogID; Boolean flag=template.opsForSet().isMember(key,String.valueOf(userID)); //最终这个返回值要被返回的blog中设置,blog就携带了这个islike信息,前端进行接收到这个islike信息的时候,如果为true,那么按钮变亮,如果为false,那么按钮置灰 return flag; } }
二)基于SortedSet或者list实现点赞排行榜,就是找到最新点赞的5个人
@RequestMapping @Controller public class UserController { @Autowired private StringRedisTemplate template; @Autowired private DemoMapper mapper; @RequestMapping("/Java100") @ResponseBody public String InsertLike(Integer userID,Integer blogID){ //1.正常情况下应该从session中获取到当前用户信息,此处为了方便实现直接从前端传递userID //2.判断当前用户是否已经针对这一篇文章点过赞了 String key="blog:liked:"+blogID; Double score=template.opsForZSet().score(key,userID.toString()); if(score==null){ //1.如果查询不到,说明当前用户没有点过赞,没有点过赞,点赞数量+1 mapper.updateAddlike(blogID); //2.向redis中的set集合存放用户信息,SortedSet的命令是zadd key value score template.opsForZSet().add(key,userID.toString(),System.currentTimeMillis()); return "点赞成功"; }else{ //1.如果查询到了,说明用户已经点过赞了,点赞数-1 mapper.updateSublike(blogID); //2.将用户ID从redis的有序集合中去除 template.opsForZSet().remove(key,String.valueOf(userID)); return "取消点赞成功"; } } public boolean islike(int blogID,int userID){ String key="blog:liked:"+blogID; Double score=template.opsForZSet().score(key,String.valueOf(userID)); if(score==null){ return false; } return true; } @RequestMapping("/Java300") @ResponseBody public List<Integer> getLastLikeUser(int blogID){ //1.先进行生成key String key="blog:liked:"+blogID; //2.查询redis中SoreedSet最先点赞的前五名,zrange key 0 4 Set<String> userIDs=template.opsForZSet().range(key,0,4); //3.解析到其中的用户ID,查找用户信息,封装到List集合中进行返回,正常是应该返回user信息的 List<Integer> list=new ArrayList<>(); userIDs.forEach(new Consumer<String>() { @Override public void accept(String s) { list.add(Integer.parseInt(s)); } }); return list; } }
二)实现关注和取消关注功能
接口1:请求格式:127.0.0.1:8080/Java100?userID=5&isFollow=true
这里面的userID表示被关注的人的ID,这里面的isFollow是表示是关注还是取关,关注就是true,取消关注就是false
接口2:查看登陆用户是否已经关注指定用户:
127.0.0.1:8080/IsFollow/10,10表示被指定的用户
@RequestMapping("/Follow/{userID}/{isFollow}") @ResponseBody public String Follow(@PathVariable("userID") Integer userID, @PathVariable("isFollow") Boolean isFollow, HttpSession session){ //1.判断到底是关注还是取关 User user= (User) session.getAttribute("user"); //2.关注直接新增数据,取关注解删除数据 if(isFollow){ //还应该在来进行判断一下当前follow是否存在,防止重复进行关注 Follow flag=mapper.selectFollow(user.getUserID(),userID); if(flag!=null){ return "当前您已经关注了,不能重复进行关注"; } Follow follow=new Follow(); follow.setFollowUserID(userID); follow.setUserID(user.getUserID()); mapper.InsertFollow(follow); return "关注成功"; }else{ int data=mapper.deleteFollow(user.getUserID(),userID); if(data==0) { return "取关成功"; }else{ return "不能重复取关"; } } } @RequestMapping("/IsFollow/{userID}") @ResponseBody public String IsFollow(@PathVariable("userID") Integer followedUserID,HttpSession session){ // 1.先获取到登录用户 User user= (User) session.getAttribute("user"); //2.判断用户是否关注 Follow follow=mapper.selectFollow(user.getUserID(),followedUserID); if(follow==null){ return "当前登录用户没有关注"; } return "当前用户已经关注了"; } @RequestMapping("/login") @ResponseBody public String login(String username,String password,HttpSession session){ User user=mapper.login(username,password); if(user!=null) { session.setAttribute("user",user); return "登陆成功"; } return "登陆失败"; }
三)实现共同关注功能
需求:基于redis的恰当的数据结构,来实现共同关注功能,在博主个人主页的展现出当前用户和博主的共同好友
首先应该把一个用户关注的列表保存到redis当中,
比如说当前用户关注了那些人保存到redis集合中,目标用户关注了哪些人,也需要保存到redis集合里面,这样我们进行查找共同关注好友的时候,我们可以直接进行查询两个人共同关注的好友,直接取交集就可以了,所以我们应该又该以前的接口,每当我们关注到一个人的时候,我们都应该把他存放到Set集合里面,这样真正来进行查询共同关注的好友的时候,就十分方便了,
关注的用户我们不光要把她存放到数据库里面,也需要把他存放到redis里面,key就是当前登陆用户的ID,value就是
当前登录用户所进行关注的用户的ID
程序传入指定的用户ID,程序就会查询这个用户ID和当前已经登陆过的用户的共同关注好友
1)进行改造过的JAVA代码:
@RequestMapping("/Follow/{userID}/{isFollow}") @ResponseBody public String Follow(@PathVariable("userID") Integer userID, @PathVariable("isFollow") Boolean isFollow, HttpSession session){ //1.判断到底是关注还是取关 User user= (User) session.getAttribute("user"); String key="follow:"+user.getUserID(); //2.关注直接新增数据,取关注解删除数据 if(isFollow){ //还应该在来进行判断一下当前follow是否存在,防止重复进行关注 Follow flag=mapper.selectFollow(user.getUserID(),userID); if(flag!=null){ return "当前您已经关注了,不能重复进行关注"; } Follow follow=new Follow(); follow.setFollowUserID(userID); follow.setUserID(user.getUserID()); mapper.InsertFollow(follow); //把当前被关注的用户ID存放到Set集合里面,zadd template.opsForSet().add(key,String.valueOf(follow.getFollowUserID())); return "关注成功"; }else{ int data=mapper.deleteFollow(user.getUserID(),userID); template.opsForSet().remove(key,String.valueOf(userID)); if(data==0) { return "取关成功"; }else{ return "不能重复取关"; } } }
2)现在用户进行查询
127.0.0.1:8080/common?userID=2
@RequestMapping("/common") @ResponseBody public List<Integer> GetCommon(Integer userID,HttpSession session){ //1.先生成key String key1="follow:"+userID; User user= (User) session.getAttribute("user"); String key2="follow:"+user.getUserID(); Set<String> set=template.opsForSet().intersect(key1,key2); if(set==null||set.isEmpty()){ return null; } //2.获取两人共同关注的好友 List<Integer> list=new ArrayList<>(); set.forEach(new Consumer<String>() { @Override public void accept(String s) { list.add(Integer.parseInt(s)); } }); return list; }
关注推送功能:
关注推送也叫做Feed流,直译为是投喂的意思,主要是为了给用户提供沉浸式的体验,通过无线刷信赖给用户提供新的信息,
就比如说你刷抖音,你进行下拉,总是有新的内容,而是应用程序自动根据用户的行为去匹配更适合用户的内容,从而减少了用户思考和查找的时间,可以大大的节省用户的时间
Feed流产品通常有两种常见模式:
1)TimeLine:不做内容筛选
,
筛选条件就是好友或者关注,
简单的按照
内容发布时间来进行排序,常常用于好友或者关注,比如说朋友圈有人给你点赞了,你进行点击,就可以看到点赞的先后顺序,
只是进行展示你朋友和好友的朋友圈,陌生人发送的不能看到;
优点:信息全面不会有丢失,并且实现起来也是非常简单的
缺点:信息噪音比较多,用户不一定感兴趣,内容获取效率比较低
2)智能排序:利用智能算法屏蔽掉违规的,用户不感兴趣的内容,来进行推送用户感兴趣的内容来进行吸引用户
优点:投喂用户感兴趣的信息,用户粘度很高,容易沉迷
缺点:如果算法不精准,可能会起到反作用
Feed流实现方式1:拉模式,也叫做读扩散,写少读多
张三李四王五发消息的时候要给他们每一个人准备一个发件箱,将来他们发消息的时候,就会发送到这个发件箱里面,
这个消息除了本身的内容以外,还要带上一个时间戳,因为TimeLine本身就要使用时间来进行排序,先进行发送的文章时间戳比较小,后发送的文章时间戳比较大;
这个时候有一个粉丝赵六,那么这个粉丝也是具有一个收件箱的,那么这个收件箱是空的,只要每一次读数据的时候收件箱才会拉取关注的人的数据到收件箱里面,当找刘进行读取的时候,收件箱就会寻找他所进行关注的所有发件人的消息一个一个的拉取到赵六的收件箱里面,
并按照时间戳来进行排序,这样赵六就可以读到信息了,每一次读的时候才会读一个副本回来;
优点:节省内存空间,只有读的时候,才会读一个副本回来,所以叫做读扩散,真正的信息只是保存了一份在发件箱里面,收件箱里面的信息读完之后就会被销毁
缺点:每一次进行读取收件箱的时候,都要重新的来进行读取收件箱的消息,然后才能做时间戳的排序,耗时比较长,所以读取的延时往往比较高,
假设如果一个人关注了成百上千的人,那么每一次进行读取的时候耗时就会变得更高,所以拉模式的最主要的缺点就是延时时间比较长,假设这个人关注了好多人有1000个人,那么延时就会比较长
Feed流实现方式2:推模式,也叫做写扩散,写多读少
在这个模式中取消了发件箱,当一个用户进行发送消息的时候,会推送到他所有的粉丝的收件箱里面,那么这个消息会写好几份,有几个粉丝写几份,这个时候当粉丝进行查看数据的时候,看到的就是发送人写的消息,并且已经排序好了,就非常快
缺点:写的频率有点高,写了好几份,内存占用会比较高,比如说大V有成千上万的粉丝,那么当这个大V进行发送文章的时候,要写成千上万份
Feed流实现方式3:推拉结合模式
1)当张三进行发送消息的时候,他的粉丝非常少,所以我们可以使用推模式,因为他的粉丝少,咱们多写几份也没有什么问题,所以张三没有发件箱,当张三进行发送消息的时候,会直接把这个消息写到每一位粉丝的收件箱里面就行了
2)大V的粉丝有很多粉丝,针对活跃粉丝,就可以使用推模式,延时肯定要低一些,针对僵尸粉丝,直接使用拉模式,临时的拉取大V的发件箱的消息
基于推模式实现消息推送功能(推送模式没有发件箱)
1)修改博客系统的业务,在保存到blog到数据库的同时,必须推送到粉丝的收件箱;
2)收件箱满足可以根据时间戳来进行排序,必须使用redis的数据结构来进行实现
3)再进行查询收件箱的时候,必须实现分页查询
注意:既然数据库中已经有完整的内容了,那么推送到粉丝的收件箱的时候,我们就可以推送blogID到收件箱里面了,起到一个将来排序的作用,那么将来用户想要查询具体信息的时候就可以直接拿着ID来进行查询即可
上面就读到了重复的数据
滚动分页:我们的时间戳从小到大进行排列,我们每一次分页的时候找一个小的时间戳,每一次查询的时候找一个比当前时间戳更小的,这样就可以实现滚动分页了,查询的数据也不会重复了
实现功能1:登陆用户提交博客信息,博客信息直接推送到粉丝信息那里:
127.0.0.1:8080/InsertBlog?blogcontent=”CPP“&blogtitle=”likeyu”
@RequestMapping("/InsertBlog") @ResponseBody public String InsertBlog(Blog blog,HttpSession session){ //1.首先获取到登录用户 User user= (User) session.getAttribute("user"); blog.setUserID(user.getUserID()); //2.保存博客到数据库里面 mapper.insertBlog(blog); //3.进行查询当前作者的粉丝并进行推送 List<Follow> list=mapper.selectFollowUsers(user.getUserID()); //4.获取所有粉丝的ID for(Follow follow:list){ //4.1获取到粉丝的ID int userID=follow.getUserID(); //4.2进行推送,他的每一位粉丝都有一个收件箱,在收件箱里面存放的就是他所关注的人发送的博客信息,其实每一位用户都有一个收件箱 String key="feed:"+userID; template.opsForZSet().add(key,blog.getBlogID().toString(),System.currentTimeMillis()); } return "新增博客成功"; }
实现功能2:粉丝用户去查看所关注的人的信息,实现滚动分页查询
因为我们每一位用户都有着自己的收件箱,收件箱里面的信息全部是关注的人发送过来的信息,里面是使用一个SortedSet集合来进行存储的,里面不仅存放了要关注的人发布的文章的ID还存放了时间戳,可以根据时间来进行排序,后发布的时间戳比较大,就可以排在前面
普通的分页查询的弊端,因为数据有可能会出现查询重复
正常查询出来的结果应该是这样子的,每一页查询三条
但是在第一个语句和第二个语句之间又有新的作者来进行推送文章的时候,那么此时在第一页和第二页的时候有查询到了重复的数据,就会出现问题
滚动查询的思路
1_zrevrange sortedset 0 5 withscores,
这是来根据时间戳查询最先发布文章的前五名信息,
滚动分页的思路是记到上一次查询到哪里了,然后这一次从上一次记录的位置开始继续查询特定的条数
2)这一次进行查询时间戳的最大值是上一次查询时间戳的最小值,但是第一次查询没有第一次,所以给当前时间戳的最大值
zrevrangebyscore sortedset 当前时间戳(最大) 0
withscores+查询的条数(limit) count(要查询几条) offset(偏移量)
3)
此时就算新增了数据,查询的数据也是不会重复的
变化的值:需要进行变化的值:当前开始的时间戳,还有limit的值
offset应该跳过的是和上一次时间戳最小值大小相同的所有的元素的个数而不应该是1
zrevrangebyscore 集合名字 上一次查询的时间戳的最小值(最为此处查询的最大值,第一次查询给当前时间就是最大值) 0(时间戳的最小值)
offset(第一次查询直接给0,如果没有重复数据直接给1,如果有重复数据,在上一次查询中和时间戳最小值相等的元素有几个) count(一页中查询几条)
请求格式:
127.0.0.1:8080/GetFollowedBlogs?maxTime= 1684118164463&offset=1
@RequestMapping("/GetFollowedBlogs") @ResponseBody //offset是和我上次查询的最小时间戳相同的元素的个数 public Result GetFollowedBlogs(Long maxTime,Integer offset,HttpSession session){ if(offset==null){ System.out.println("当前查询的是第一页"); offset=0; } if(maxTime==null||maxTime.equals("")){ maxTime= Long.MAX_VALUE; } //1.获取到用户从而获取到key,从而查询收件箱中的信息 User user= (User) session.getAttribute("user"); String key="feed:"+user.getUserID(); System.out.println(key); //2.查询收件箱对应的文章ID,也就是blogID Set<ZSetOperations.TypedTuple<String>> blogList=template.opsForZSet() .reverseRangeByScoreWithScores(key,0,maxTime,offset,3); //3.进行非空判断,如果blogList为空,那么说明关注的人的blogID没有发布过 if(blogList.isEmpty()||blogList==null){ System.out.println("当前并没有文章发布"); return null; } //4.获取到收件箱里面的blogID和里面对应的分数,解析数据获取到blogID,minTime和offset,最小时间就是最后一个时间戳 //这里买你的os就是收件箱中所有时间戳等于最小时间戳的元素的个数 int os=1;//因为和最小时间戳相等的至少是1个 long minTime=0;//minTIme此时一定是最小值,最终一定会替换成最小时间戳 List<Long> ids=new ArrayList<>(blogList.size()); for(ZSetOperations.TypedTuple<String> s:blogList){ //4.1获取到里面的blogID String blogID=s.getValue(); ids.add(Long.valueOf(blogID)); //4.2获取到里面的分数 long time=s.getScore().longValue(); if(time==minTime){ os++; }else{ minTime=time; os=1; } } //5.根据Blog来进行查询最终结果,查询当前博客是否已经被点赞 List<Blog> blogs=new ArrayList<>(); for(long id:ids){ Blog blog=mapper.SelectBlogByID(id); blog.setIslike(islike((int) id,user.getUserID())); blogs.add(blog); } //6.封装结果并进行返回博客信息,offset和当前最小时间戳 Result result=new Result(); result.setList(blogs); result.setOffset(os); result.setMinTime(minTime); return result; } }