Redis實現好友關註的示例代碼

一、關註和取關

加載的時候會先發請求看是否關註瞭,來顯示是關註按鈕還是取關按鈕

當我們點擊關註或取關之後再發請求進行操作

數據庫表結構

關註表(主鍵、用戶id、關註用戶id)

需求

  • 關註和取關接口
  • 判斷是否關註接口
/**
  * 關註用戶
  * @param id
  * @param isFollow
  * @return
  */
@PutMapping("/{id}/{isFollow}")
public Result follow(@PathVariable("id") Long id, @PathVariable("isFollow") Boolean isFollow){
    return followService.follow(id,isFollow);
}
 
/**
  * 判斷是否關註指定用戶
  * @param id
  * @return
  */
@GetMapping("/or/not/{id}")
public Result isFollow(@PathVariable("id") Long id){
    return followService.isFollow(id);
}
/**
  * 關註用戶
  * @param id 
  * @param isFollow
  * @return
  */
@Override
public Result follow(Long id, Boolean isFollow) {
    //獲取當前用戶id
    Long userId = UserHolder.getUser().getId();
    //判斷是關註操作還是取關操作
    if(BooleanUtil.isTrue(isFollow)){
        //關註操作
        Follow follow = new Follow();
        follow.setUserId(userId);
        follow.setFollowUserId(id);
        save(follow);
    }else{
        //取關操作
        remove(new QueryWrapper<Follow>().eq("user_id",userId).eq("follow_user_id",id));
    }
    return Result.ok();
}
 
/**
  * 判斷是否關註指定用戶
  * @param id
  * @return
  */
@Override
public Result isFollow(Long id) {
    //獲取當前用戶id
    Long userId = UserHolder.getUser().getId();
    Integer count = query().eq("user_id", userId).eq("follow_user_id", id).count();
    if(count>0){
        return Result.ok(true);
    }
    return Result.ok(false);
}

二、共同關註                               

需求:利用redis中恰當的數據結構,實現共同關註功能,在博主個人頁面展示當前用戶和博主的共同好友

可以用redis中set結構的取交集實現

 先在關註和取關增加存入redis

/**
  * 關註用戶
  * @param id
  * @param isFollow
  * @return
  */
@Override
public Result follow(Long id, Boolean isFollow) {
    //獲取當前用戶id
    Long userId = UserHolder.getUser().getId();
    String key = "follow:" + userId;
    //判斷是關註操作還是取關操作
    if(BooleanUtil.isTrue(isFollow)){
        //關註操作
        Follow follow = new Follow();
        follow.setUserId(userId);
        follow.setFollowUserId(id);
        boolean success = save(follow);
        if(success){
            //插入set集合中
            stringRedisTemplate.opsForSet().add(key,id.toString());
        }
    }else{
        //取關操作
        boolean success = remove(new QueryWrapper<Follow>().eq("user_id", userId).eq("follow_user_id", id));
        //從set集合中移除
        if(success){
            stringRedisTemplate.opsForSet().remove(key,id.toString());
        }
    }
    return Result.ok();
}

然後就可以開始寫查看共同好友接口瞭

/**
  * 判斷是否關註指定用戶
  * @param id
  * @return
  */
@GetMapping("common/{id}")
public Result followCommons(@PathVariable("id") Long id){
    return followService.followCommons(id);
}
/**
  * 共同關註
  * @param id
  * @return
  */
@Override
public Result followCommons(Long id) {
    Long userId = UserHolder.getUser().getId();
    //當前用戶的key
    String key1 = "follow:" + userId;
    //指定用戶的key
    String key2 = "follow:" + id;
    //判斷兩個用戶的交集
    Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2);
    if(intersect==null||intersect.isEmpty()){
        //說明沒有共同關註
        return Result.ok();
    }
    //如果有共同關註,則獲取這些用戶的信息
    List<Long> userIds = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
    List<UserDTO> userDTOS = userService.listByIds(userIds).stream().map(item -> (BeanUtil.copyProperties(item, UserDTO.class))).collect(Collectors.toList());
    return Result.ok(userDTOS);
}

三、關註推送(feed流)

關註推送也叫做fedd流,直譯為投喂。為用戶持續的提供"沉浸式"的體驗,通過無限下拉刷新獲取新的信息。feed模式,內容匹配用戶。

Feed流產品有兩種常見模式:

Timeline:不做內容篩選,簡單的按照內容發佈時間排序,常用於好友或關註。例如朋友圈

  • 優點:信息全面,不會有缺失。並且實現也相對簡單
  • 缺點:信息噪音較多,用戶不一定感興趣,內容獲取效率低

智能排序:利用智能算法屏蔽掉違規的、用戶不感興趣的內容。推送用戶感興趣信息來吸引用戶

  • 優點:投喂用戶感興趣信息,用戶粘度很高,容易沉迷
  • 缺點:如果算法不精準,可能起到反作用

本例中是基於關註的好友來做Feed流的,因此采用Timeline的模式。

1、Timeline模式的方案

該模式的實現方案有

  • 拉模式
  • 推模式
  • 推拉結合 

 拉模式

優點:節省內存消息,隻用保存一份,保存發件人的發件箱,要讀的時候去拉取就行瞭

缺點:每次讀取都要去拉,耗時比較久

推模式

優點:延遲低

缺點:太占空間瞭,一個消息要保存好多遍

推拉結合模式

推拉結合分用戶,比如大v很多粉絲就采用推模式,有自己的發件箱,讓用戶上線之後去拉取。普通人發的話就用推模式推給每個用戶,因為粉絲數也不多直接推給每個人延遲低。粉絲也分活躍粉絲和普通粉絲,活躍粉絲用推模式有主機的收件箱,因為他天天都看必看,而普通粉絲用拉模式,主動上線再拉取,僵屍粉直接不會拉取,就節省空間。

總結

 由於我們這點評網站,用戶量比較小,所以我們采用推模式(千萬以下沒問題)。

2、推模式實現關註推送

需求

(1)修改新增探店筆記的業務,在保存blog到數據庫的同時,推送到粉絲的收件箱

(2)收件箱滿足可以根據時間排序,必須用redis的數據結構實現

(3)查詢收件箱數據時,可以實現分頁查詢

要進行分頁查詢,那麼我們存入redis采用什麼數據類型呢,是list還是zset呢

feed流分頁問題

假如我們在分頁查詢的時候,這個時候加瞭新的內容11, 再查詢下一頁的時候,6就重復出現瞭,為瞭解決這種問題,我們必須使用滾動分頁

feed流的滾動分頁

滾動分頁就是每次都記住最後一個id,方便下一次進行查詢,用這種lastid的方式來記住,不依賴於角標,所以我們不會收到角標的影響。所以我們不能用list來存數據,因為他依賴於角標,zset可以根據分數值范圍查詢。我們按時間排序,每次都記住上次最小的,然後從比這小的開始。

實現推送到粉絲的收件箱

修改新增探店筆記的業務,在保存blog到數據庫的同時,推送到粉絲的收件箱

@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_KEY + userId;
        stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
    }
    // 5.返回id
    return Result.ok(blog.getId());
}

滾動分頁接收思路

第一次查詢是分數(時間)從1000(很大的數)開始到0(最小)這個范圍,然後限制查3個(一頁數量),偏移量是0,然後記錄結尾(上一次的最小值)

以後每次都是從上一次的最小值到0,限定查3個,偏移量是1(因為記錄的那個值不算),再記錄結尾的值。

但是有一種情況,如果有相同的時間,分數一樣的話,比如兩個6分,而且上一頁都顯示完,我們下一頁是按照第一個6分當結尾的,第二個6分可能會出現的,所以我們這個偏移量不能固定是1,要看有幾個和結尾相同的數,如果是兩個就得是2,3個就是3。

滾動分頁查詢參數:

  • 最大值:當前時間戳 | 上一次查詢的最小時間戳
  • 最小值:0
  • 偏移量:0 | 最後一個值的重復數
  • 限制數:一頁顯示的數

實現滾動分頁查詢

前端需要傳來兩條數據,分別是lastId和offset,如果是第一次查詢,那麼這兩個值是固定的,會由前端來指定,lastId是發起查詢時的時間戳,而offset就是零,當後端查詢完分頁信息後需要返回三條數據,第一條自然就是分頁信息,第二條是此次分頁查詢數據中最後一條數據的時間戳,第三條信息是偏移量,我們需要在分頁查詢後計算有多少條信息的時間戳與最後一條是相同的,作為偏移量來返回。而前端拿到這後兩個參數之後就會分別保存在前端的lastId和offset中,下一次分頁查詢時就會將這兩條數據作為請求參數來訪問,然後不斷循環上述過程,這樣也就實現瞭分頁查詢。

定義返回值實體類

@Data
public class ScrollResult {
    private List<?> list;
    private Long minTime;
    private Integer offset;
}

Controller

@GetMapping("/of/follow")
public Result queryBlogOfFollow(
    @RequestParam("lastId") Long max, @RequestParam(value = "offset", defaultValue = "0") Integer offset){
    return blogService.queryBlogOfFollow(max, offset);
}

BlogServiceImpl

@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
    //獲取當前用戶
    Long userId = UserHolder.getUser().getId();
    //組裝key
    String key = RedisConstants.FEED_KEY + userId;
    //分頁查詢收件箱,一次查詢兩條 ZREVRANGEBYSCORE key Max Min LIMIT offset count
    Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 2);
    //若收件箱為空則直接返回
    if (typedTuples == null || typedTuples.isEmpty()) {
        return Result.ok();
    }
    //通過上述數據獲取筆記id,偏移量和最小時間
    ArrayList<Long> ids = new ArrayList<>();
    long minTime = 0;
    //因為這裡的偏移量是下一次要傳給前端的偏移量,所以初始值定為1
    int os = 1;
    for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
        //添加博客id
        ids.add(Long.valueOf(typedTuple.getValue()));
        //獲取時間戳
        long score = typedTuple.getScore().longValue();
        //由於數據是按時間戳倒序排列的,因此最後被賦值的就是最小時間
        if (minTime == score) {
            //如果有兩個數據時間戳相等,那麼偏移量開始計數
            os++;
        } else {
            //如果當前數據的時間戳與已經記錄的最小時間戳不相等,則說明當前時間小於已記錄的最小時間戳,將其賦給minTime
            minTime = score;
            //偏移量重置
            os = 1;
        }
    }
    //需要考慮到時間戳相等的消息數量大於2的情況,這時候偏移量就需要加上上一頁查詢時的偏移量
    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();
    //這裡還需要查詢博客作者的相關信息,這裡對比視頻中,用一次查詢代替瞭多次查詢,提高效率
    List<Long> blogUserIds = blogs.stream().map(blog -> blog.getUserId()).collect(Collectors.toList());
    String blogUserIdStr = StrUtil.join(",", blogUserIds);
    HashMap<Long, User> userHashMap = new HashMap<>();
    userService.query().in("id", blogUserIds).last("ORDER BY FIELD(id," + blogUserIdStr + ")").list().
        stream().forEach(user -> {
        userHashMap.put(user.getId(), user);
    });
    //為blog封裝數據
    Iterator<Blog> blogIterator = blogs.iterator();
    while (blogIterator.hasNext()) {
        Blog blog = blogIterator.next();
        User user = userHashMap.get(blog.getUserId());
        blog.setName(user.getNickName());
        blog.setIcon(user.getIcon());
        blog.setIsLike(isLikeBlog(blog.getId()));
    }
    //返回封裝數據
    ScrollResult scrollResult = new ScrollResult();
    scrollResult.setList(blogs);
    scrollResult.setMinTime(minTime);
    scrollResult.setOffset(os);
    return Result.ok(scrollResult);
}

到此這篇關於Redis實現好友關註的示例代碼的文章就介紹到這瞭,更多相關Redis 好友關註內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: