Redis實現(xiàn)好友關(guān)注的示例代碼
一、關(guān)注和取關(guān)
加載的時候會先發(fā)請求看是否關(guān)注了,來顯示是關(guān)注按鈕還是取關(guān)按鈕
當我們點擊關(guān)注或取關(guān)之后再發(fā)請求進行操作
數(shù)據(jù)庫表結(jié)構(gòu)
關(guān)注表(主鍵、用戶id、關(guān)注用戶id)
需求
- 關(guān)注和取關(guān)接口
- 判斷是否關(guān)注接口
/** * 關(guān)注用戶 * @param id * @param isFollow * @return */ @PutMapping("/{id}/{isFollow}") public Result follow(@PathVariable("id") Long id, @PathVariable("isFollow") Boolean isFollow){ return followService.follow(id,isFollow); } /** * 判斷是否關(guān)注指定用戶 * @param id * @return */ @GetMapping("/or/not/{id}") public Result isFollow(@PathVariable("id") Long id){ return followService.isFollow(id); }
/** * 關(guān)注用戶 * @param id * @param isFollow * @return */ @Override public Result follow(Long id, Boolean isFollow) { //獲取當前用戶id Long userId = UserHolder.getUser().getId(); //判斷是關(guān)注操作還是取關(guān)操作 if(BooleanUtil.isTrue(isFollow)){ //關(guān)注操作 Follow follow = new Follow(); follow.setUserId(userId); follow.setFollowUserId(id); save(follow); }else{ //取關(guān)操作 remove(new QueryWrapper<Follow>().eq("user_id",userId).eq("follow_user_id",id)); } return Result.ok(); } /** * 判斷是否關(guān)注指定用戶 * @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); }
二、共同關(guān)注
需求:利用redis中恰當?shù)臄?shù)據(jù)結(jié)構(gòu),實現(xiàn)共同關(guān)注功能,在博主個人頁面展示當前用戶和博主的共同好友
可以用redis中set結(jié)構(gòu)的取交集實現(xiàn)
先在關(guān)注和取關(guān)增加存入redis
/** * 關(guān)注用戶 * @param id * @param isFollow * @return */ @Override public Result follow(Long id, Boolean isFollow) { //獲取當前用戶id Long userId = UserHolder.getUser().getId(); String key = "follow:" + userId; //判斷是關(guān)注操作還是取關(guān)操作 if(BooleanUtil.isTrue(isFollow)){ //關(guān)注操作 Follow follow = new Follow(); follow.setUserId(userId); follow.setFollowUserId(id); boolean success = save(follow); if(success){ //插入set集合中 stringRedisTemplate.opsForSet().add(key,id.toString()); } }else{ //取關(guān)操作 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(); }
然后就可以開始寫查看共同好友接口了
/** * 判斷是否關(guān)注指定用戶 * @param id * @return */ @GetMapping("common/{id}") public Result followCommons(@PathVariable("id") Long id){ return followService.followCommons(id); }
/** * 共同關(guān)注 * @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()){ //說明沒有共同關(guān)注 return Result.ok(); } //如果有共同關(guān)注,則獲取這些用戶的信息 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); }
三、關(guān)注推送(feed流)
關(guān)注推送也叫做fedd流,直譯為投喂。為用戶持續(xù)的提供"沉浸式"的體驗,通過無限下拉刷新獲取新的信息。feed模式,內(nèi)容匹配用戶。
Feed流產(chǎn)品有兩種常見模式:
Timeline:不做內(nèi)容篩選,簡單的按照內(nèi)容發(fā)布時間排序,常用于好友或關(guān)注。例如朋友圈
- 優(yōu)點:信息全面,不會有缺失。并且實現(xiàn)也相對簡單
- 缺點:信息噪音較多,用戶不一定感興趣,內(nèi)容獲取效率低
智能排序:利用智能算法屏蔽掉違規(guī)的、用戶不感興趣的內(nèi)容。推送用戶感興趣信息來吸引用戶
- 優(yōu)點:投喂用戶感興趣信息,用戶粘度很高,容易沉迷
- 缺點:如果算法不精準,可能起到反作用
本例中是基于關(guān)注的好友來做Feed流的,因此采用Timeline的模式。
1、Timeline模式的方案
該模式的實現(xiàn)方案有
- 拉模式
- 推模式
- 推拉結(jié)合
拉模式
優(yōu)點:節(jié)省內(nèi)存消息,只用保存一份,保存發(fā)件人的發(fā)件箱,要讀的時候去拉取就行了
缺點:每次讀取都要去拉,耗時比較久
推模式
優(yōu)點:延遲低
缺點:太占空間了,一個消息要保存好多遍
推拉結(jié)合模式
推拉結(jié)合分用戶,比如大v很多粉絲就采用推模式,有自己的發(fā)件箱,讓用戶上線之后去拉取。普通人發(fā)的話就用推模式推給每個用戶,因為粉絲數(shù)也不多直接推給每個人延遲低。粉絲也分活躍粉絲和普通粉絲,活躍粉絲用推模式有主機的收件箱,因為他天天都看必看,而普通粉絲用拉模式,主動上線再拉取,僵尸粉直接不會拉取,就節(jié)省空間。
總結(jié)
由于我們這點評網(wǎng)站,用戶量比較小,所以我們采用推模式(千萬以下沒問題)。
2、推模式實現(xiàn)關(guān)注推送
需求
(1)修改新增探店筆記的業(yè)務,在保存blog到數(shù)據(jù)庫的同時,推送到粉絲的收件箱
(2)收件箱滿足可以根據(jù)時間排序,必須用redis的數(shù)據(jù)結(jié)構(gòu)實現(xiàn)
(3)查詢收件箱數(shù)據(jù)時,可以實現(xiàn)分頁查詢
要進行分頁查詢,那么我們存入redis采用什么數(shù)據(jù)類型呢,是list還是zset呢
feed流分頁問題
假如我們在分頁查詢的時候,這個時候加了新的內(nèi)容11, 再查詢下一頁的時候,6就重復出現(xiàn)了,為了解決這種問題,我們必須使用滾動分頁
feed流的滾動分頁
滾動分頁就是每次都記住最后一個id,方便下一次進行查詢,用這種lastid的方式來記住,不依賴于角標,所以我們不會收到角標的影響。所以我們不能用list來存數(shù)據(jù),因為他依賴于角標,zset可以根據(jù)分數(shù)值范圍查詢。我們按時間排序,每次都記住上次最小的,然后從比這小的開始。
實現(xiàn)推送到粉絲的收件箱
修改新增探店筆記的業(yè)務,在保存blog到數(shù)據(jù)庫的同時,推送到粉絲的收件箱
@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()); }
滾動分頁接收思路
第一次查詢是分數(shù)(時間)從1000(很大的數(shù))開始到0(最?。┻@個范圍,然后限制查3個(一頁數(shù)量),偏移量是0,然后記錄結(jié)尾(上一次的最小值)
以后每次都是從上一次的最小值到0,限定查3個,偏移量是1(因為記錄的那個值不算),再記錄結(jié)尾的值。
但是有一種情況,如果有相同的時間,分數(shù)一樣的話,比如兩個6分,而且上一頁都顯示完,我們下一頁是按照第一個6分當結(jié)尾的,第二個6分可能會出現(xiàn)的,所以我們這個偏移量不能固定是1,要看有幾個和結(jié)尾相同的數(shù),如果是兩個就得是2,3個就是3。
滾動分頁查詢參數(shù):
- 最大值:當前時間戳 | 上一次查詢的最小時間戳
- 最小值:0
- 偏移量:0 | 最后一個值的重復數(shù)
- 限制數(shù):一頁顯示的數(shù)
實現(xiàn)滾動分頁查詢
前端需要傳來兩條數(shù)據(jù),分別是lastId和offset,如果是第一次查詢,那么這兩個值是固定的,會由前端來指定,lastId是發(fā)起查詢時的時間戳,而offset就是零,當后端查詢完分頁信息后需要返回三條數(shù)據(jù),第一條自然就是分頁信息,第二條是此次分頁查詢數(shù)據(jù)中最后一條數(shù)據(jù)的時間戳,第三條信息是偏移量,我們需要在分頁查詢后計算有多少條信息的時間戳與最后一條是相同的,作為偏移量來返回。而前端拿到這后兩個參數(shù)之后就會分別保存在前端的lastId和offset中,下一次分頁查詢時就會將這兩條數(shù)據(jù)作為請求參數(shù)來訪問,然后不斷循環(huán)上述過程,這樣也就實現(xiàn)了分頁查詢。
定義返回值實體類
@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(); } //通過上述數(shù)據(jù)獲取筆記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(); //由于數(shù)據(jù)是按時間戳倒序排列的,因此最后被賦值的就是最小時間 if (minTime == score) { //如果有兩個數(shù)據(jù)時間戳相等,那么偏移量開始計數(shù) os++; } else { //如果當前數(shù)據(jù)的時間戳與已經(jīng)記錄的最小時間戳不相等,則說明當前時間小于已記錄的最小時間戳,將其賦給minTime minTime = score; //偏移量重置 os = 1; } } //需要考慮到時間戳相等的消息數(shù)量大于2的情況,這時候偏移量就需要加上上一頁查詢時的偏移量 os = minTime == max ? os : os + offset; //根據(jù)id查詢blog String idStr = StrUtil.join(",", ids); //查詢時需要手動指定順序 List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list(); //這里還需要查詢博客作者的相關(guān)信息,這里對比視頻中,用一次查詢代替了多次查詢,提高效率 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封裝數(shù)據(jù) 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())); } //返回封裝數(shù)據(jù) ScrollResult scrollResult = new ScrollResult(); scrollResult.setList(blogs); scrollResult.setMinTime(minTime); scrollResult.setOffset(os); return Result.ok(scrollResult); }
到此這篇關(guān)于Redis實現(xiàn)好友關(guān)注的示例代碼的文章就介紹到這了,更多相關(guān)Redis 好友關(guān)注內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
window下創(chuàng)建redis出現(xiàn)問題小結(jié)
這篇文章主要介紹了window下創(chuàng)建redis出現(xiàn)問題總結(jié),本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-10-10基于redis實現(xiàn)世界杯排行榜功能項目實戰(zhàn)
前段時間,做了一個世界杯競猜積分排行榜。對世界杯64場球賽勝負平進行猜測,猜對+1分,錯誤+0分,一人一場只能猜一次。下面通過本文給大家分享基于redis實現(xiàn)世界杯排行榜功能項目實戰(zhàn),感興趣的朋友一起看看吧2018-10-10Redis分布式鎖的實現(xiàn)方式(redis面試題)
這篇文章主要介紹了Redis分布式鎖的實現(xiàn)方式(面試常見),需要的朋友可以參考下2020-01-01