Redis 实现好友关注和关注推送的示例代码

 更新时间:2025年03月11日 10:20:56   作者:快乐的小三菊  
本文介绍了使用Redis实现好友关注和关注推送功能,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

一、关注和取关

1.1 简介

在探店图文的详情页面中,可以关注发布笔记的作者:

1.2 需求描述

实现两个接口:关注和取关接口、判断是否关注的接口

1.3 代码实现

涉及到的 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);
    }
}

二、共同关注

2.1 简介

点击博主头像,可以进入博主首页:

2.2 需求描述

利用 Redis 中恰当的数据结构,实现共同关注功能。在博主个人页面展示出当前用户与博主的共同好友。

2.3 代码实现

涉及到的 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);
    }
}

三、关注推送

3.1 简介

关注推送也叫做 Feed 流,直译为投喂。为用户持续的提供 “沉浸式” 的体验,通过无限下拉刷新获取新的信息。

3.2 Feed 流的模式

3.2.1 TimeLine

不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈。

优点:信息全面,不会有缺失。并且实现也相对简单

缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低

3.2.2 智能排序

利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户。

优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷

缺点:如果算法不精准,可能起到反作用

3.3 实现方案

本例中的个人页面,是基于关注的好友来做 Feed 流,因此采用 Timeline 的模式。该模式的实现方案有三种:拉模式、推模式、推拉结合模式。

3.3.1 拉模式

拉模式也叫做读扩散,假设现在有三个 up 主,张三李四和王五,它们三个人将来会发一些消息,此时,给他们每个人都准备一个发件箱,将来它们发送消息的时候就会发送到发件箱里面去,发送的消息除了本身以外还需要携带一个时间戳,因为后面要按照时间排序。

此时有一个粉丝赵六,它有一个收件箱,平常是空的,只有他要去读消息的时候我们才会给他去拉取,即从它所关注的 up 主的发件箱去拉取消息,拉过来之后按照时间去排序,这样就可以去读取了。

这种模式的优点是节省内存空间,因为收件箱读取完毕之后就可以清空,下次再重新拉取。缺点是每次读消息的时候都要重新去拉取发件箱的消息,然后再做排序,这一系列的动作耗时会比较久,读取的延迟较高。 

3.3.2 推模式

推模式也叫写扩散。假设此时有两个 up 主,张三和李四,粉丝1关注了张三、粉丝2关注了张三和李四、粉丝3也关注了张三和李四,假设此时张三要发送消息,它所发送的消息会直接推送到所有粉丝的收件箱里面去,然后对收件箱里面的消息进行排序,此时粉丝可以直接从收件箱里面读取消息

此种模式的优点是延迟非常的低,弥补了拉模式的缺点。缺点是由于没有了发件箱,不得不把消息发送给每一个粉丝,内存占用会很高,即一个消息要写N份 

3.3.3 推拉结合模式

推拉结合模式也叫做读写混合,兼具推和拉两种模式的优点。假设现在有一个大 V 和普通人张三,还有两个普通粉丝和一个活跃粉丝,每个粉丝都有自己的收件箱。假设此时普通人张三要发送消息,他的粉丝很少,就可以采用推模式,即直接将消息推送到每一个粉丝的收件箱里面。

而大 V 则不一样,大 的粉丝很多,虽然粉丝多,但是粉丝存在差异,有活跃粉丝和一般粉丝。针对于活跃粉丝采用推模式,而针对于一般粉丝采取拉模式,因为大 V 需要有一个发件箱。

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

3.3.4 总结

3.4 Feed 流的分页问题

Feed 流中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式。接下来我们分析下原因。

以下图为例,横线代表时间轴,此时时间来到了 t1 时刻,收件箱里面已经有了 10 条消息了,数字越大代表越新,读取第一页数据没啥问题。

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

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

这就是 Feed 流不能采用传统的分页模式的原因。

3.5 Feed 流的滚动分页

此时就需要采用 Feed 流的滚动分页,即记录每次分页的最后一条,下次再从这个位置开始查。第一次读取时 lastId 设置成无穷大就可以了,如下图:

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

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

此时的查询是不依赖于脚标的,所以数据不受影响。所以只能使用 zset 结构。

3.6 需求描述

基于推模式实现关注推送功能:

1、修改新增探店笔记的业务,在保存 blog 到数据库的同时,推送到粉丝的收件箱

2、收件箱满足可以根据时间戳排序,必须用 Redis 的数据结构实现

3、查询收件箱数据时,可以实现分页查询

3.7 代码实现

首先完成修改新增探店笔记的业务,在保存 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 好友关注和关注推送内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

相关文章

  • Govern Service 基于 Redis 的服务治理平台安装过程详解

    Govern Service 基于 Redis 的服务治理平台安装过程详解

    Govern Service 是一个轻量级、低成本的服务注册、服务发现、 配置服务 SDK,通过使用现有基础设施中的 Redis 不用给运维部署带来额外的成本与负担,接下来通过本文给大家分享Govern Service 基于 Redis 的服务治理平台的相关知识,感兴趣的朋友一起看看吧
    2021-05-05
  • Redis精确去重计数方法(咆哮位图)

    Redis精确去重计数方法(咆哮位图)

    这篇文章主要给大家介绍了关于Redis精确去重计数方法(咆哮位图)的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Redis具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-06-06
  • redis连接报错error:NOAUTH Authentication required

    redis连接报错error:NOAUTH Authentication required

    本文主要介绍了redis连接报错error:NOAUTH Authentication required,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-05-05
  • Redis凭啥可以这么快

    Redis凭啥可以这么快

    本文详细的介绍了为啥使用Redis的时候,可以做到非常快的读取速度,对于大家学习Redis非常有帮助,希望大家喜欢
    2021-02-02
  • redis中使用bloomfilter的白名单功能解决缓存穿透问题

    redis中使用bloomfilter的白名单功能解决缓存穿透问题

    本文主要介绍了redis中使用bloomfilter的白名单功能解决缓存穿透问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-07-07
  • Redis 中的布隆过滤器的实现

    Redis 中的布隆过滤器的实现

    这篇文章主要介绍了Redis 中的布隆过滤器的实现,详细的介绍了什么是布隆过滤器以及如何实现,非常具有实用价值,需要的朋友可以参考下
    2018-10-10
  • Redis 使用跳表实现有序集合的方法

    Redis 使用跳表实现有序集合的方法

    Redis有序集合底层为什么使用跳表而非其他数据结构如平衡树、红黑树或B+树的原因在于其特殊的设计和应用场景,跳表提供了与平衡树类似的效率,同时实现更简单,调试和修改也更加容易,感兴趣的朋友一起看看吧
    2024-09-09
  • 巧用Redis实现分布式锁详细介绍

    巧用Redis实现分布式锁详细介绍

    大家好,本篇文章主要讲的是巧用Redis实现分布式锁详细介绍,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下,方便下次浏览
    2021-12-12
  • RediSearch加RedisJSON大于Elasticsearch的搜索存储引擎

    RediSearch加RedisJSON大于Elasticsearch的搜索存储引擎

    这篇文章主要为大家介绍了RediSearch加RedisJSON大于Elasticsearch的王炸使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • 浅谈Redis处理接口幂等性的两种方案

    浅谈Redis处理接口幂等性的两种方案

    本文主要介绍了浅谈Redis处理接口幂等性的两种方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08

最新评论