Redis实现好友关注、共同关注、Feed流推送

目录

一、实现关注/取关

二、查看是否关注

三、查看共同关注

四、关注推送

五、实现分页查询收邮箱

流程

1. 获取登录用户的 ID

2. 查询用户的收件箱

3. 解析查询结果

4. 根据博客 ID 查询博客信息

5. 查询博客相关信息

6. 封装并返回结果


一、实现关注/取关

前端请求:/follow/2/true --> 后端:@PutMapping("/{id}/{isFollow}")

redis使用:set的add方法,存入key,value(followerId)(关注的人的id)

stringRedisTemplate.opsForSet().add(key, followUserId.toString());

    @Override
    public Result follow(Long followUserId, Boolean isFollow) {
        // 1. 获取当前登录用户
        long userId = UserHolder.getUser().getId();
        String key = FOLLOW_USER_KEY + userId;

        // 2. 判断是否关注
        if (isFollow) {
            // 未关注,进行关注操作
            Follow follow = new Follow().setUserId(userId).setFollowUserId(followUserId).setCreateTime(LocalDateTime.now());
            boolean isSuccess = save(follow);
            if (isSuccess) {
                // 成功的话就放在redis里面
                stringRedisTemplate.opsForSet().add(key, followUserId.toString());
            } else {
                return Result.fail("关注失败,请稍后再试!");
            }
        } else {
            // 关注,进行取关操作  :数据库--redis
            // remove(new QueryWrapper<Follow>().eq("user_id", userId).eq("follow_user_id", followUserId));
            stringRedisTemplate.opsForSet().remove(key, followUserId.toString());
        }
        return Result.ok();
    }

二、查看是否关注

前端请求:follow/or/not/1 --> 后端:@GetMapping("or/not/{id}")

redis使用:set的isMember方法,判断是否存在某个value,传入key、value(followUserId)

Boolean member = stringRedisTemplate.opsForSet().isMember(FOLLOW_USER_KEY + userId, followUserId.toString());
    @Override
    public Result isFollow(Long followUserId) {
        // 1. 获取当前登录用户
        long userId = UserHolder.getUser().getId();
        // 2. 从数据库中查是否有关注数据   --> 从redis中查
        // Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
        Boolean member = stringRedisTemplate.opsForSet().isMember(FOLLOW_USER_KEY + userId, followUserId.toString());
        return Result.ok(member);
    }

三、查看共同关注

前端请求:follow/common/1 --> 后端:@GetMapping("/common/{id}")

redis使用:set的intersect方法查询交集,传入两个key查找交集value

Set<String> intersect = stringRedisTemplate.opsForSet().intersect(userKey, otherKey);

难点:

        1. 获取的交集是Set 类型的,如何转换成List 类型方便后续查询具体用户信息时调用

List<Long> ids = intersect
    .stream()
    .map(Long::valueOf)
    .collect(Collectors.toList());

        2. 将获取到的user转换为userDTO

// 查询用户
// listByIds返回的是一个List<User>,所以需要进行转换
// .stream() 方法将集合 ids 转换为一个 Java 8 流(Stream)
// .map(user -> BeanUtil.copyProperties(user, UserDTO.class)) 是一个映射操作,它将集合中的每个元素从 User 类型转换为 UserDTO 类型。
// .collect(Collectors.toList()将流中的映射后的 UserDTO 对象收集到一个新的 List<UserDTO> 中。这个操作将流转换为一个列表,其中包含了经过映射后的 UserDTO 对象。
List<UserDTO> users = userService
    .listByIds(ids)
    .stream()
    .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
    .collect(Collectors.toList());

代码实现 

    @Override
    public Result followCommons(Long otherUserId) {
        // 1. 获取当前登录用户
        long userId = UserHolder.getUser().getId();
        String userKey = FOLLOW_USER_KEY + userId;
        String otherKey = FOLLOW_USER_KEY + otherUserId;

        //求交集
        Set<String> intersect = stringRedisTemplate.opsForSet().intersect(userKey, otherKey);
        // 没有的话直接返回空列表
        if (intersect == null || intersect.isEmpty()) {
            return Result.ok(Collections.emptyList());
        }

        // 解析id集合
        List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());

        // 查询用户
        List<UserDTO> users = userService.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());
        return Result.ok(users);
    }

四、关注推送

Feed流实现方案

Feed流产品有两种常见模式:

  1. Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈
优点信息全面,不会有缺失。并且实现也相对简单
缺点信息噪音较多,用户不一定感兴趣,内容获取效率低

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

优点投喂用户感兴趣信息,用户粘度很高,容易沉迷
缺点如果算法不精准,可能起到反作用

采用Timeline的模式。该模式的实现方案有三种:

1. 拉模式:只有粉丝⽤户在读取收件箱的时候, 才会根据其关注的⽤户进⾏拉取,把博主发件箱⾥的消息拉取到粉丝⽤户的收件箱⾥,然后对收件箱⾥的消息按时 间戳进⾏排序。

优点:节约空间

缺点:比较延迟,假设用户关注了大用户,此时就会拉取海量的内容,对服务器压力巨大。

2. 推模式:当⽤户(博主)发送消息时,会把消息+时间戳直接发送到所有粉丝⽤户的收件箱中,并按时间戳进⾏排序。当粉 丝⽤户在读取收件箱的消息时,直接读取。
优点: 延迟低
缺点: 发消息时,内容占⽤较⾼。因为每个粉丝都会保留⼀份消息。

3. 推拉模式:对于粉丝少的博主⽤户,采⽤推模式。 对于粉丝多的博主⽤户,根据粉丝⽤户类型进⾏判断: 活跃度⾼的粉丝⽤户,采⽤推模式 活跃度低的粉丝⽤户,采⽤拉模式

推拉模式兼具推和拉两种模式的优点

 总结

需求:

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

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

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

注意:

当粉丝⽤户需要按分页模式来读取收件箱的信息时,不能采⽤传统的分页模式(按数据的⾓标开始查)。因为Feed 流中的数据会不断更新,所以数据的⾓标也在不断变化。传统的分页模式,会出现消息重复读的问题。

redis实现:用zset方便排序,时间戳做score

    @Override
    public Result saveBlog(Blog blog) {
        if (blog.getShopId() == null || blog.getTitle() == null || blog.getContent() == null) {
            return Result.fail("提交前情把Blog全部信息填写完整(●'◡'●)");
        }
        // 1. 获取登录用户
        UserDTO user = UserHolder.getUser();
        blog.setUserId(user.getId());
        // 2.保存探店笔记
        boolean isSuccess = save(blog);
        if (!isSuccess) {
            return Result.fail("新增笔记失败!");
        }
        // 3.查询笔记作者的所有粉丝
        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.推送 (思路就是只把blog的id传到redis里面,到时候再调用bolg的query方法获取详情)
            String key = FEED_KEY + userId;
            // 还是要按时间戳当作value,因为要进行排序
            stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
        }
        return Result.ok(blog.getId());
    }

五、实现分页查询收邮箱

具体操作如下:

1、每次查询完成后,我们要分析出查询出数据的最小时间戳,这个值会作为下一次查询的条件

2、我们需要找到与上一次查询相同的查询个数作为偏移量,下次查询时,跳过这些查询过的数据,拿到我们需要的数据

综上:我们的请求参数中就需要携带 lastId:上一次查询的最小时间戳minTime和偏移量offset这两个参数。

这两个参数第一次会由前端来指定,以后的查询就根据后台结果作为条件,再次传递到后台。

redis使用:opsForZSet的reverseRangeByScoreWithScores方法,传入key、minTime、maxTime、offset、每次查询数量

Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 2);
@Data
public class ScrollResult {
    // 查询的Blog结果
    private List<?> list;
    // 上次查询的最小时间戳
    private Long minTime;
    // 偏移量
    private Integer offset;
}
    @Override
    public Result queryBlogOfFollow(Long max, Integer offset) {
        // 1.获取登录用户
        Long userId = UserHolder.getUser().getId();

        // 2. 查询自己的收件箱
        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(Collections.emptyList());
        }

        // 解析数据:blogId、minTime(时间戳)、offset
        ArrayList<Long> ids = new ArrayList<>(typedTuples.size());
        long minTime = 2;
        int os = 1;
        for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {
            ids.add(Long.valueOf(tuple.getValue()));
            long time = tuple.getScore().longValue();
            if (time == minTime) {
                os++;
            } else {
                minTime = time;
                os = 1;
            }
        }
        os = minTime == max ? os : os + offset;
        // 根据id查blog
        String idStr = StrUtil.join(",", ids);
        List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
        // 查询blog相关信息
        for (Blog blog : blogs) {
            queryBlogUser(blog);
            isBlogLike(blog);
        }
        // 封装并返回
        ScrollResult r = new ScrollResult();
        r.setList(blogs);
        r.setOffset(os);
        r.setMinTime(minTime);

        return Result.ok(r);
    }

流程

1. 获取登录用户的 ID

Long userId = UserHolder.getUser().getId();

2. 查询用户的收件箱

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(Collections.emptyList());
}
  • 构建 Redis 的键名,FEED_KEY 是一个常量,feed:123,表示用户 ID 为 123 的收件箱。
  • 使用 stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores 方法从 Redis 的有序集合(Sorted Set)中查询数据。reverseRangeByScoreWithScores 方法的参数含义如下:
    • key:要查询的有序集合的键名。
    • 0:分数的最小值,表示从分数为 0 开始查询。
    • max:分数的最大值,表示查询分数小于等于 max 的元素。
    • offset:偏移量,表示从第 offset 个元素开始查询。
    • 2:查询的元素数量,表示最多查询 2 个元素。
  • 该方法返回一个 Set<ZSetOperations.TypedTuple<String>> 集合,其中每个 TypedTuple 对象包含元素的值(博客 ID)和对应的分数(时间戳)。
  • 如果查询结果为空,则直接返回一个空列表。

3. 解析查询结果

ArrayList<Long> ids = new ArrayList<>(typedTuples.size());
long minTime = 2;
int os = 1;
for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {
    ids.add(Long.valueOf(tuple.getValue()));
    long time = tuple.getScore().longValue();
    if (time == minTime) {
        os++;
    } else {
        minTime = time;
        os = 1;
    }
}
os = minTime == max ? os : os + offset;
  • 创建一个 ArrayList 用于存储博客 ID。
  • 初始化 minTime 为 2os 为 1minTime 用于记录查询结果中最小的时间戳,os 用于记录偏移量。
  • 遍历查询结果,将每个 TypedTuple 对象中的博客 ID 转换为 Long 类型并添加到 ids 列表中。
  • 获取每个 TypedTuple 对象的分数(时间戳),如果当前时间戳等于 minTime,则 os 加 1;否则,更新 minTime 为当前时间戳,并将 os 重置为 1
  • 根据 minTime 和 max 的关系更新 os 的值,如果 minTime 等于 max,则保持 os 不变;否则,将 os 加上传入的 offset

4. 根据博客 ID 查询博客信息

String idStr = StrUtil.join(",", ids);
List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
  • 使用 StrUtil.join 方法将 ids 列表中的元素用逗号连接成一个字符串,例如 1,2,3
  • 使用 MyBatis-Plus 的 query 方法构建查询条件,通过 in 方法筛选出 id 字段在 ids 列表中的博客记录。
  • 使用 last 方法在 SQL 语句末尾添加自定义的排序条件,ORDER BY FIELD(id," + idStr + ")" 表示按照 id 列表中的顺序对查询结果进行排序,确保返回的博客列表顺序与 Redis 中查询结果的顺序一致。
  • 调用 list 方法执行查询并返回博客列表。

5. 查询博客相关信息

for (Blog blog : blogs) {
    queryBlogUser(blog);
    isBlogLike(blog);
}
  • 遍历查询到的博客列表,对每篇博客调用 queryBlogUser 方法查询博客作者的用户信息,并将用户信息设置到博客对象中。
  • 调用 isBlogLike 方法判断当前登录用户是否点赞了该博客,并将判断结果设置到博客对象的 isLike 属性中。

6. 封装并返回结果

ScrollResult r = new ScrollResult();
r.setList(blogs);
r.setOffset(os);
r.setMinTime(minTime);
return Result.ok(r);
  • 创建一个 ScrollResult 对象,用于封装查询结果。
  • 将查询到的博客列表设置到 ScrollResult 对象的 list 属性中。
  • 将计算得到的偏移量 os 设置到 ScrollResult 对象的 offset 属性中。
  • 将最小时间戳 minTime 设置到 ScrollResult 对象的 minTime 属性中。
  • 使用 Result.ok 方法将 ScrollResult 对象封装成统一的返回结果并返回。

综上,该方法通过从 Redis 中查询用户收件箱信息,获取博客 ID,再根据博客 ID 从数据库中查询博客信息,并补充博客相关的用户信息和点赞状态,最后将结果封装成 ScrollResult 对象返回,实现了滚动分页查询用户关注的博主发布的博客的功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值