146人参与 • 2025-03-11 • Redis
在探店图文的详情页面中,可以关注发布笔记的作者:

实现两个接口:关注和取关接口、判断是否关注的接口
涉及到的 controller 层代码如下:
@restcontroller
@requestmapping("/follow")
public class followcontroller {
@resource
private ifollowservice followservice;
@putmapping("/{id}/{isfollow}")
public result follow(@pathvariable("id") long followuserid, @pathvariable("isfollow") boolean isfollow) {
return followservice.follow(followuserid, isfollow);
}
@getmapping("/or/not/{id}")
public result isfollow(@pathvariable("id") long followuserid) {
return followservice.isfollow(followuserid);
}
}涉及到的 service 层代码如下:
@service
public class followserviceimpl extends serviceimpl<followmapper, follow> implements ifollowservice {
@resource
private stringredistemplate stringredistemplate;
@resource
private iuserservice userservice;
@override
public result follow(long followuserid, boolean isfollow) {
// 1.获取登录用户
long userid = userholder.getuser().getid();
string key = "follows:" + userid;
// 1.判断到底是关注还是取关
if (isfollow) {
// 2.关注,新增数据
follow follow = new follow();
follow.setuserid(userid);
follow.setfollowuserid(followuserid);
boolean issuccess = save(follow);
if (issuccess) {
// 把关注用户的id,放入redis的set集合 sadd userid followeruserid
stringredistemplate.opsforset().add(key, followuserid.tostring());
}
} else {
// 3.取关,删除 delete from tb_follow where user_id = ? and follow_user_id = ?
boolean issuccess = remove(new querywrapper<follow>()
.eq("user_id", userid).eq("follow_user_id", followuserid));
if (issuccess) {
// 把关注用户的id从redis集合中移除
stringredistemplate.opsforset().remove(key, followuserid.tostring());
}
}
return result.ok();
}
@override
public result isfollow(long followuserid) {
// 1.获取登录用户
long userid = userholder.getuser().getid();
// 2.查询是否关注 select count(*) from tb_follow where user_id = ? and follow_user_id = ?
integer count = query().eq("user_id", userid).eq("follow_user_id", followuserid).count();
// 3.判断
return result.ok(count > 0);
}
}点击博主头像,可以进入博主首页:

利用 redis 中恰当的数据结构,实现共同关注功能。在博主个人页面展示出当前用户与博主的共同好友。
涉及到的 controller 层代码如下:
@restcontroller
@requestmapping("/follow")
public class followcontroller {
@resource
private ifollowservice followservice;
@getmapping("/common/{id}")
public result followcommons(@pathvariable("id") long id){
return followservice.followcommons(id);
}
}涉及到的 service 层代码如下:
@service
public class followserviceimpl extends serviceimpl<followmapper, follow> implements ifollowservice {
@resource
private stringredistemplate stringredistemplate;
@resource
private iuserservice userservice;
@override
public result followcommons(long id) {
// 1.获取当前用户
long userid = userholder.getuser().getid();
string key = "follows:" + userid;
// 2.求交集
string key2 = "follows:" + id;
set<string> intersect = stringredistemplate.opsforset().intersect(key, key2);
if (intersect == null || intersect.isempty()) {
// 无交集
return result.ok(collections.emptylist());
}
// 3.解析id集合
list<long> ids = intersect.stream().map(long::valueof).collect(collectors.tolist());
// 4.查询用户
list<userdto> users = userservice.listbyids(ids)
.stream()
.map(user -> beanutil.copyproperties(user, userdto.class))
.collect(collectors.tolist());
return result.ok(users);
}
}关注推送也叫做 feed 流,直译为投喂。为用户持续的提供 “沉浸式” 的体验,通过无限下拉刷新获取新的信息。

3.2.1 timeline
不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈。
优点:信息全面,不会有缺失。并且实现也相对简单
缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
3.2.2 智能排序
利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户。
优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
缺点:如果算法不精准,可能起到反作用
本例中的个人页面,是基于关注的好友来做 feed 流,因此采用 timeline 的模式。该模式的实现方案有三种:拉模式、推模式、推拉结合模式。
3.3.1 拉模式
拉模式也叫做读扩散,假设现在有三个 up 主,张三李四和王五,它们三个人将来会发一些消息,此时,给他们每个人都准备一个发件箱,将来它们发送消息的时候就会发送到发件箱里面去,发送的消息除了本身以外还需要携带一个时间戳,因为后面要按照时间排序。
此时有一个粉丝赵六,它有一个收件箱,平常是空的,只有他要去读消息的时候我们才会给他去拉取,即从它所关注的 up 主的发件箱去拉取消息,拉过来之后按照时间去排序,这样就可以去读取了。

这种模式的优点是节省内存空间,因为收件箱读取完毕之后就可以清空,下次再重新拉取。缺点是每次读消息的时候都要重新去拉取发件箱的消息,然后再做排序,这一系列的动作耗时会比较久,读取的延迟较高。
3.3.2 推模式
推模式也叫写扩散。假设此时有两个 up 主,张三和李四,粉丝1关注了张三、粉丝2关注了张三和李四、粉丝3也关注了张三和李四,假设此时张三要发送消息,它所发送的消息会直接推送到所有粉丝的收件箱里面去,然后对收件箱里面的消息进行排序,此时粉丝可以直接从收件箱里面读取消息

此种模式的优点是延迟非常的低,弥补了拉模式的缺点。缺点是由于没有了发件箱,不得不把消息发送给每一个粉丝,内存占用会很高,即一个消息要写n份
3.3.3 推拉结合模式
推拉结合模式也叫做读写混合,兼具推和拉两种模式的优点。假设现在有一个大 v 和普通人张三,还有两个普通粉丝和一个活跃粉丝,每个粉丝都有自己的收件箱。假设此时普通人张三要发送消息,他的粉丝很少,就可以采用推模式,即直接将消息推送到每一个粉丝的收件箱里面。
而大 v 则不一样,大 v 的粉丝很多,虽然粉丝多,但是粉丝存在差异,有活跃粉丝和一般粉丝。针对于活跃粉丝采用推模式,而针对于一般粉丝采取拉模式,因为大 v 需要有一个发件箱。

这种推拉结合的模式,既节省了内存又照顾了活跃用户的感受
3.3.4 总结

feed 流中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式。接下来我们分析下原因。
以下图为例,横线代表时间轴,此时时间来到了 t1 时刻,收件箱里面已经有了 10 条消息了,数字越大代表越新,读取第一页数据没啥问题。

此时来到了 t2 时刻,有人插入了一条新的数据,此时我们的 11 数据就跑到了最前面去了

等到来到了 t3 时刻,此时需要读取第二页的内容,此时就会出现重复读取的问题,分页就出现了混乱。

这就是 feed 流不能采用传统的分页模式的原因。
此时就需要采用 feed 流的滚动分页,即记录每次分页的最后一条,下次再从这个位置开始查。第一次读取时 lastid 设置成无穷大就可以了,如下图:

到了 t2 时刻有人插入了新的数据 11,如下图:

等到 t3 时刻,读取第二页的时候,让 lastid = 6,向后查 5 条就不会出现问题了,如下图:

此时的查询是不依赖于脚标的,所以数据不受影响。所以只能使用 zset 结构。
基于推模式实现关注推送功能:
1、修改新增探店笔记的业务,在保存 blog 到数据库的同时,推送到粉丝的收件箱
2、收件箱满足可以根据时间戳排序,必须用 redis 的数据结构实现
3、查询收件箱数据时,可以实现分页查询
首先完成修改新增探店笔记的业务,在保存 blog 到数据库的同时,推送到粉丝的收件箱功能,涉及到的 controller 层代码如下:
@restcontroller
@requestmapping("/blog")
public class blogcontroller {
@resource
private iblogservice blogservice;
@postmapping
public result saveblog(@requestbody blog blog) {
return blogservice.saveblog(blog);
}
}涉及到的 service 层代码如下:
@service
public class blogserviceimpl extends serviceimpl<blogmapper, blog> implements iblogservice {
@resource
private iuserservice userservice;
@resource
stringredistemplate stringredistemplate;
@resource
ifollowservice followservice;
@override
public result saveblog(blog blog) {
// 1.获取登录用户
userdto user = userholder.getuser();
blog.setuserid(user.getid());
// 2.保存探店笔记
boolean issuccess = save(blog);
if(!issuccess){
return result.fail("新增笔记失败!");
}
// 3.查询笔记作者的所有粉丝 select * from tb_follow where follow_user_id = ?
list<follow> follows = followservice.query().eq("follow_user_id", user.getid()).list();
// 4.推送笔记id给所有粉丝
for (follow follow : follows) {
// 4.1.获取粉丝id
long userid = follow.getuserid();
// 4.2.推送
string key = "feed:" + userid;
stringredistemplate.opsforzset().add(key, blog.getid().tostring(), system.currenttimemillis());
}
// 5.返回id
return result.ok(blog.getid());
}
}接下来完成剩下的两小点需求,界面请求的参数如下:

涉及到的 controller 层代码如下:
@restcontroller
@requestmapping("/blog")
public class blogcontroller {
@resource
private iblogservice blogservice;
@postmapping
public result saveblog(@requestbody blog blog) {
return blogservice.saveblog(blog);
}
@getmapping("/of/follow")
public result queryblogoffollow(
@requestparam("lastid") long max, @requestparam(value = "offset", defaultvalue = "0") integer offset){
return blogservice.queryblogoffollow(max, offset);
}
}涉及到的 service 层代码如下:
@override
public result queryblogoffollow(long max, integer offset) {
// 1.获取当前用户
long userid = userholder.getuser().getid();
// 2.查询收件箱 zrevrangebyscore key max min limit offset count
string key = feed_key + userid;
set<zsetoperations.typedtuple<string>> typedtuples = stringredistemplate.opsforzset()
.reverserangebyscorewithscores(key, 0, max, offset, 2);
// 3.非空判断
if (typedtuples == null || typedtuples.isempty()) {
return result.ok();
}
// 4.解析数据:blogid、mintime(时间戳)、offset
list<long> ids = new arraylist<>(typedtuples.size());
long mintime = 0; // 2
int os = 1; // 2
for (zsetoperations.typedtuple<string> tuple : typedtuples) { // 5 4 4 2 2
// 4.1.获取id
ids.add(long.valueof(tuple.getvalue()));
// 4.2.获取分数(时间戳)
long time = tuple.getscore().longvalue();
if(time == mintime){
os++;
}else{
mintime = time;
os = 1;
}
}
// 5.根据id查询blog
string idstr = strutil.join(",", ids);
list<blog> blogs = query().in("id", ids).last("order by field(id," + idstr + ")").list();
for (blog blog : blogs) {
// 5.1.查询blog有关的用户
querybloguser(blog);
// 5.2.查询blog是否被点赞
isblogliked(blog);
}
// 6.封装并返回
scrollresult r = new scrollresult();
r.setlist(blogs);
r.setoffset(os);
r.setmintime(mintime);
return result.ok(r);
}封装返回给前端的实体类如下:
@data
public class scrollresult {
private list<?> list;
private long mintime;
private integer offset;
}到此这篇关于 redis 实现好友关注和关注推送的示例代码的文章就介绍到这了,更多相关 redis 好友关注和关注推送内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论