基于 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 的粉丝很多,虽然粉丝多,但是粉丝存在差异,有活跃粉丝和一般粉丝。针对于活跃粉丝采用推模式,而针对于一般粉丝采取拉模式,因为大 V 需要有一个发件箱。

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

3.3.4 总结

3.4 Feed 流的分页问题

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

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

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

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

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

3.5 Feed 流的滚动分页

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

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

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

         此时的查询是不依赖于脚标的,所以数据不受影响。所以只能使用 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;
}
  • 19
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快乐的小三菊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值