likes
comments
collection
share

【Redis应用】基于Redis实现排行榜,点赞,关注功能

作者站长头像
站长
· 阅读数 46

前言

redis的使用场景有很多,都取决于其支持丰富的数据结构。比如说上一篇基于redis实现的秒杀,我们对其进行优化采用redis中队列,实现异步秒杀。再还有就是我们利用redis最简单的数据结构string实现的缓存。除了上面的这个,其实redis中的set可以实现共同好友,点赞,关注等功能,其zset结构可以实现排行榜的功能。还有另外4种数据结构(BitMap,HyperLoglog等等),也是利用他们的特点优势,实现了其他场景。本章主要是学习利用reids实现排行榜,点赞,关注功能。

点赞/取消点赞的实现

需求:

  • 同一个用户只能点赞一次,再次点击则取消点赞
  • 如果当前用户已经点赞,则点赞按钮高亮显示(前端实现,判断字段Blog类的isLike属性)

实现步骤:

  1. 给Blog类中添加一个isLike字段,标示是否被当前用户点赞
  2. 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞则点赞-1;
  3. 修改根据id查询的业务,判断当前登录用户是否点赞过,赋值给isLike字段
  4. 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段

在Blog类中添加一个isLike字段

 /**
* 是否点赞
*/
@TableField(exist = false)
private Boolean isLike;

修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞则点赞-1

 /**
* 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞则点赞-1;
*/
@Override
public Result likeBlog(Long id) {
    // 1.获取登录用户
    Long userId = UserHolder.getUser().getId();
    // 2.判断用户是否点过赞
    Boolean isLike = stringRedisTemplate.opsForSet().isMember(RedisConstants.BLOG_LIKED_KEY + id, userId.toString());
    if (BooleanUtil.isTrue(isLike)){
        // 点赞 ; 取消用户点赞,
        boolean update = update().setSql("liked = liked - 1").eq("id", id).update();
        if (!update) {
            return Result.fail(SystemConstants.BLOG_CANCEL_LIKE_FAIL);
        }
        stringRedisTemplate.opsForSet().remove(RedisConstants.BLOG_LIKED_KEY+id,userId.toString());
    }else {
        boolean update = update().setSql("liked = liked + 1").eq("id", id).update();
        if (!update) {
            return Result.fail(SystemConstants.BLOG_LIKE_FAIL);
        }
        stringRedisTemplate.opsForSet().add(RedisConstants.BLOG_LIKED_KEY+id,userId.toString());
    }
    return Result.ok();
}

点赞排行榜的实现

一个事物的好坏,取决于客户对其的评价,所以在很多平台上面我们经常会看见评分,点赞等。最后通过统计,罗列出一个排行榜。排行榜的实现其实有很多的方法。在这里对最早点赞的人(top5)进行显示,前面我们使用的是redis中的set对用户,文章id进行存储,并没有排序功能,在想其实redis中有一种数据结构是可以在满足数据唯一的基础上实现排序的,也就是我们所说的zset。

需求:

在文章详情处,将点赞人数显示出来,比如最早(按时间)点赞的top5,形成点赞排行榜

接口设计

说明
请求方式GET
请求路径/blog/likes/{id}
请求参数id:blogId
返回值List 给这个笔记点赞的topn用户集合

代码实现

上面的点赞实现呢,我们是采用set的数据结构进行数据存储的,现在我们修改一下原有的数据结构,将数据结构转换成zset进行存储。

 /**
* 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞则点赞-1;
*/
@Override
public Result likeBlog(Long id) {
    // 1.获取登录用户
    Long userId = UserHolder.getUser().getId();
    // 2.判断用户是否点过赞
    Double score = stringRedisTemplate.opsForZSet().score(RedisConstants.BLOG_LIKED_KEY + id, userId.toString());
    if (score != null){
        // 点赞 ; 取消用户点赞,
        boolean update = update().setSql("liked = liked - 1").eq("id", id).update();
        if (!update) {
            return Result.fail(SystemConstants.BLOG_CANCEL_LIKE_FAIL);
        }
        stringRedisTemplate.opsForZSet().remove(RedisConstants.BLOG_LIKED_KEY+id,userId.toString());
    }else {
        boolean update = update().setSql("liked = liked + 1").eq("id", id).update();
        if (!update) {
            return Result.fail(SystemConstants.BLOG_LIKE_FAIL);
        }
        stringRedisTemplate.opsForZSet().add(RedisConstants.BLOG_LIKED_KEY+id,userId.toString(),System.currentTimeMillis());
    }
    return Result.ok();
}

接下来就是接口的编写了

  1. 添加/likes/{id}的请求路径及方法
  2. 获取zset中排名top5的用户id
@Override
public Result queryBlogLikes(Long id) {
    // 根据文章id,查询redis中排名前五的用户信息
    Set<String> range = stringRedisTemplate.opsForZSet().range(RedisConstants.BLOG_LIKED_KEY + id, 0, 4);
    if (range == null || range.isEmpty()) {
        return Result.ok(Collections.emptyList());
    }
    // 获取用户id
    List<Long> ids = range.stream().map(Long::valueOf).toList();
    List<UserDTO> topUserList = new ArrayList<>();
    List<UserDTO> userDTOList = ids.stream().map(u -> {
        User user = userService.getById(u);
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        topUserList.add(userDTO);
        return userDTO;
    }).collect(Collectors.toList());
    return Result.ok(userDTOList);
}

关注/取关,共同关注(好友),关注推送功能

分析:

关注/取关:其实和点赞的实现是相似的。 (这边按照课程的方式做了分笔记,后期会使用redis的方式也做一遍

共同关注:其实就是在redis中set求交集

关注推送:

关注/取关

需求:基于该表数据结构,实现两个接口:

  • 关注和取关接口
  • 判断是否关注的接口
@Override
    public Result follow(Long followUserId, Boolean isFollow) {
        UserDTO user = UserHolder.getUser();
        if (user == null) {
            return Result.fail(SystemConstants.USER_UN_LOGIN);
        }
        // 1.判断到底是关注还是取关
        if (isFollow){
            // 2.关注,新增数据
            Follow follow = new Follow();
            follow.setUserId(user.getId());
            follow.setFollowUserId(followUserId);
            save(follow);
        }else {
            // 3.取关,删除
            remove(new QueryWrapper<Follow>().eq("user_id", user.getId()).eq("follow_user_id", followUserId));
//            remove(lambdaQuery().eq(Follow::getUserId,user.getId()).eq(Follow::getFollowUserId,followUserId));
        }
        return Result.ok();
    }

    @Override
    public Result isFollow(Long followUserId) {
        Long userId = UserHolder.getUser().getId();
        // 1.查询是否关注
        Long count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
        return Result.ok(count > 0);
    }

共同关注

@Override
    public Result follow(Long followUserId, Boolean isFollow) {
        UserDTO user = UserHolder.getUser();
        if (user == null) {
            return Result.fail(SystemConstants.USER_UN_LOGIN);
        }
        // 1.判断到底是关注还是取关
        if (isFollow){
            // 2.关注,新增数据
            Follow follow = new Follow();
            follow.setUserId(user.getId());
            follow.setFollowUserId(followUserId);
            boolean save = save(follow);
            if (save) {
                // 吧关注的用户id,放入redis的set集合 sadd userId followerUserId
                stringRedisTemplate.opsForSet().add(RedisConstants.USER_FOLLOW_KEY+user.getId(),followUserId.toString());
            }
        }else {
            // 3.取关,删除
            remove(new QueryWrapper<Follow>().eq("user_id", user.getId()).eq("follow_user_id", followUserId));
//            remove(lambdaQuery().eq(Follow::getUserId,user.getId()).eq(Follow::getFollowUserId,followUserId));
            stringRedisTemplate.opsForSet().remove(RedisConstants.USER_FOLLOW_KEY+user.getId());
        }
        return Result.ok();
    }

controller创建一个方法followCommons

@Override
public Result followCommon(Long id) {
    // 获取目标用户和当前用户的id
    Long userId = UserHolder.getUser().getId();

    String preKey = RedisConstants.USER_FOLLOW_KEY;
    // 先去redis缓存中查找,
    Set<String> intersect = stringRedisTemplate.opsForSet().intersect(preKey + id, preKey + userId);
    // 如果缓存没有再去redis查找 (以防redis宕机,数据无持久化)
    if (intersect == null || intersect.isEmpty()) {
        // todo 去数据库中查找数据(数据库有数据,将数据返回,数据库无数据空集)
log.info("去数据库中查找数据");
    }
    // 提取出id
    List<Long> ids = intersect.stream().map(Long::valueOf).toList();
    // 查询用户信息,并返回
    List<UserDTO> userDTOS = userService.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());
    return Result.ok(userDTOS);
}

关注推送

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

应用持续自动的匹配用户行为

Feed流

Feed流的模式:

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

    • 优点:信息全面,不会有缺失,并且实现也相对简单
    • 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
  • 智能排序:利用智能算法屏蔽违规的,用户不感兴趣的内容,推送用户感兴趣的信息来吸引用户

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

Feed流的实现方案

  1. 拉模式:读扩散
  2. 推模式:写扩散
  3. 推拉结合模式(读写混合)
拉模式推模式推拉模式
写比例
读比例
用户读取延迟
实现难度复杂简单很复杂
使用场景很少使用用户量少,没有大v过千万的用户量,有大v

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

需求:

  1. 原来我们的文章发布业务是直接将数据保存到数据库的,现在需要将其进行修改,在保存到数据库的同时,将文章推送到粉丝邮箱
  2. 收件箱满足可以根据时间搓排序,必须用redis的数据结构实现
  3. 查询收件箱数据时,可以实现分页查询(采用滚动分页的形式进行数据查询)

接口设计:

说明
请求方式GET
请求路径/blog/of/follow
请求参数lastId:上一次查询的最小时间戳,offset:偏移量
返回值List :小于指时间戳的笔记集合;minTime:本次查询的推送的最小时间戳;offset:偏移量
@Override
public Result saveBlog(Blog blog) {
    // 获取登录用户
    UserDTO user = UserHolder.getUser();
    blog.setUserId(user.getId());
    // 保存探店博文
    boolean saved = save(blog);
    if (!saved) {
        return Result.fail(SystemConstants.BLOG_SAVE_FAIL);
    }
    // 保存成功将笔记id发送给所有粉丝
    List<Follow> followList = followService.lambdaQuery().eq(Follow::getFollowUserId, user.getId()).list();
    // 查询笔记作者的所有粉丝
    for (Follow follow : followList) {
        Long userId = follow.getUserId();
        // 数据推送        // 将笔记id 发送给所有粉丝
        stringRedisTemplate.opsForZSet().add(RedisConstants.BLOG_FEED_KEY+userId,blog.getId().toString(),System.currentTimeMillis());
    }
    return Result.ok(blog.getId());
}
 /**
* 3. 查询收件箱数据时,可以实现分页查询(采用滚动分页的形式进行数据查询)
* @param max
* @param offset
* @return
*/
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
    // 1.获取当前用户id
    Long userId = UserHolder.getUser().getId();
    // 2.查询收件箱
    Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().
            reverseRangeByScoreWithScores(RedisConstants.BLOG_FEED_KEY + userId, 0, max, offset, 2L);
    if (typedTuples == null || typedTuples.isEmpty()){
        return Result.ok();
    }
    // 解析数据: blogId,minTime(时间搓),offset
    List<Long> ids = new ArrayList<>(typedTuples.size());
    long minTime = 0;
    int os = 1;
    for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {
        // 获取用户id
       ids.add(Long.valueOf(Objects.requireNonNull(tuple.getValue())));
        // 获取时间搓
        long time = Objects.requireNonNull(tuple.getScore()).longValue();
        if (time == minTime){
            os++;
        }else {
            minTime = time;
            os = 1;
        }
    }
    // 根据id查询blog
    List<Blog> blogList = new ArrayList<>();
    List<Blog> blogs = ids.stream().map(id -> {
        Blog blog = getById(id);
        // 查询blog相关用户
        queryBlogUser(blog);
        // 查询blog是否被点赞
        isBlogLiked(blog);
        blogList.add(blog);
        return blog;
    }).collect(Collectors.toList());
    // 滚动分页的封装
    ScrollResult scrollResult = new ScrollResult();
    scrollResult.setList(blogs);
    scrollResult.setOffset(os);
    scrollResult.setMinTime(minTime);
    return Result.ok(scrollResult);
}
转载自:https://juejin.cn/post/7351728888983224356
评论
请登录