redis實現(xiàn)好友關注&消息推送的方法示例
關注和取消關注
在查看筆記詳情時,會自動發(fā)送請求,調(diào)用接口來檢查當前用戶是否已經(jīng)關注了筆記作者,我們要實現(xiàn)這兩個接口
需求:基于該表數(shù)據(jù)結構,實現(xiàn)兩個接口:
- 關注和取關接口
- 判斷是否關注的接口
關注是User之間的關系,是博主與粉絲的關系,數(shù)據(jù)庫中有一張tb_follow表來標識,我們要將用戶與博主關聯(lián)起來,只需要向這張表插入數(shù)據(jù)即可,需要將主鍵設為自增加,降低開發(fā)難度
FollowController
//關注 @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); }
判斷當前用戶與博主是否已經(jīng)關注了博主我們只需要查詢follow表中是否有記錄即可,等值查詢建議進行非空判斷,防止出現(xiàn)異常
@Override public Result isFollow(Long followUserId) { Long userId = UserHolder.getUser().getId(); LambdaQueryWrapper<Follow> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(followUserId != null, Follow::getFollowUserId, followUserId) .eq(userId != null, Follow::getUserId, userId); Integer count = followMapper.selectCount(queryWrapper); return Result.ok(count > 0); }
前端發(fā)送請求時,會發(fā)送isFollow這個boolean值,用來讓后端判斷當前用戶是否已經(jīng)關注了博主,如果為true,則直接插入數(shù)據(jù)庫,表示關注成功,如果為false,表示已經(jīng)關注要取消關注了
@Override public Result follow(Long followUserId, Boolean isFollow) { Long userId = UserHolder.getUser().getId(); String key = "follows:" + userId; if (isFollow) { //封裝follow對象 Follow follow = new Follow(); follow.setFollowUserId(followUserId); follow.setUserId(userId); follow.setCreateTime(LocalDateTime.now()); //插入數(shù)據(jù)庫 boolean isSuccess = save(follow); } else { LambdaQueryWrapper<Follow> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Follow::getFollowUserId, followUserId) .eq(Follow::getUserId, userId); boolean isSuccess = remove(queryWrapper); } return Result.ok(); }
共同關注
共同關注的好友,需要首先進入到這個頁面,這個頁面會發(fā)起兩個請求
1、去查詢用戶的詳情
2、去查詢用戶的筆記
// UserController 根據(jù)id查詢用戶 @GetMapping("/{id}") public Result queryUserById(@PathVariable("id") Long userId){ // 查詢詳情 User user = userService.getById(userId); if (user == null) { return Result.ok(); } UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); // 返回 return Result.ok(userDTO); } // BlogController 根據(jù)id查詢博主的探店筆記 @GetMapping("/of/user") public Result queryBlogByUserId( @RequestParam(value = "current", defaultValue = "1") Integer current, @RequestParam("id") Long id) { // 根據(jù)用戶查詢 Page<Blog> page = blogService.query() .eq("user_id", id).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE)); // 獲取當前頁數(shù)據(jù) List<Blog> records = page.getRecords(); return Result.ok(records); }
共同關注的需求:利用Redis中恰當?shù)臄?shù)據(jù)結構,實現(xiàn)共同關注功能。在博主個人頁面展示出當前用戶與博主的共同關注。在這里我們選用set集合,因為在set集合中,有交集并集補集的api,我們可以把兩人的關注的人分別放入到一個set集合中,然后再通過api去查看這兩個set集合中的交集數(shù)據(jù)。
首先對添加關注進行改造,在插入數(shù)據(jù)的同時也要將當前用戶關注的博主用戶id插入到set集合中去,用于查找交集
@Override public Result follow(Long followUserId, Boolean isFollow) { Long userId = UserHolder.getUser().getId(); String key = "follows:" + userId; if (isFollow) { //封裝follow對象 Follow follow = new Follow(); follow.setFollowUserId(followUserId); follow.setUserId(userId); follow.setCreateTime(LocalDateTime.now()); //插入數(shù)據(jù)庫 boolean isSuccess = save(follow); if (isSuccess) { stringRedisTemplate.opsForSet().add(key, followUserId.toString()); } } else { LambdaQueryWrapper<Follow> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Follow::getFollowUserId, followUserId) .eq(Follow::getUserId, userId); boolean isSuccess = remove(queryWrapper); if (isSuccess) { stringRedisTemplate.opsForSet().remove(key); } } return Result.ok(); }
controller層,定義common接口,傳入的id是筆記博主的id
@GetMapping("/common/{id}") public Result common(@PathVariable("id") Long id){ System.out.println(id); return followService.commonFollow(id); }
service層
@Override public Result commonFollow(Long id) { //獲取當前用戶 Long userId = UserHolder.getUser().getId(); String key1 = "follows:" + userId; String key2 = "follows:" + id; Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2); if(intersect == null || intersect.isEmpty()) { // 無交集 return Result.ok(Collections.emptyList()); } //解析用戶id集合 List<Long> userIds = intersect.stream().map(item -> Long.valueOf(item)).collect(Collectors.toList()); List<UserDTO> users = userService.selectByIds(userIds).stream().map( user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList()); return Result.ok(users); }
Feed流實現(xiàn)方案
當我們關注了用戶后,這個用戶發(fā)了動態(tài),那么我們應該把這些數(shù)據(jù)推送給用戶,這個需求,其實我們又把他叫做Feed流,關注推送也叫做Feed流,直譯為投喂。為用戶持續(xù)的提供“沉浸式”的體驗,通過無限下拉刷新獲取新的信息。
對于傳統(tǒng)的模式的內(nèi)容解鎖:我們是需要用戶去通過搜索引擎或者是其他的方式去解鎖想要看的內(nèi)容
對于新型的Feed流的的效果:不需要我們用戶再去推送信息,而是系統(tǒng)分析用戶到底想要什么,然后直接把內(nèi)容推送給用戶,從而使用戶能夠更加的節(jié)約時間,不用主動去尋找。類似B站,抖音等平臺的大數(shù)據(jù)推送機制
Feed流的兩種模式
Feed流的實現(xiàn)有兩種模式:
Feed流產(chǎn)品有兩種常見模式: Timeline:不做內(nèi)容篩選,簡單的按照內(nèi)容發(fā)布時間排序,常用于好友或關注。例如朋友圈
- 優(yōu)點:信息全面,不會有缺失。并且實現(xiàn)也相對簡單
- 缺點:信息噪音較多,用戶不一定感興趣,內(nèi)容獲取效率低
智能排序:利用智能算法屏蔽掉違規(guī)的、用戶不感興趣的內(nèi)容。推送用戶感興趣信息來吸引用戶
- 優(yōu)點:投喂用戶感興趣信息,用戶粘度很高,容易沉迷
- 缺點:如果算法不精準,可能起到反作用。 本例中的個人頁面,是基于關注的好友來做Feed流,因此采用Timeline的模式。該模式的實現(xiàn)方案有三種
Timeline模式的實現(xiàn)方案
拉模式:也叫做讀擴散
該模式的核心含義就是:當張三和李四和王五發(fā)了消息后,都會保存在自己的郵箱中,假設趙六要讀取信息,那么他會從讀取他自己的收件箱,此時系統(tǒng)會從他關注的人群中,把他關注人的信息全部都進行拉取,然后在進行排序
優(yōu)點:比較節(jié)約空間,因為趙六在讀信息時,并沒有重復讀取,而且讀取完之后可以把他的收件箱進行清楚。
缺點:比較延遲,當用戶讀取數(shù)據(jù)時才去關注的人里邊去讀取數(shù)據(jù),假設用戶關注了大量的用戶,那么此時就會拉取海量的內(nèi)容,對服務器壓力巨大。
推模式:也叫做寫擴散。
推模式是沒有寫郵箱的,當張三寫了一個內(nèi)容,此時會主動的把張三寫的內(nèi)容發(fā)送到他的粉絲收件箱中去,假設此時李四再來讀取,就不用再去臨時拉取了
優(yōu)點:時效快,不用臨時拉取
缺點:內(nèi)存壓力大,假設一個大V寫信息,很多人關注他, 就會寫很多分數(shù)據(jù)到粉絲那邊去
推拉結合模式:也叫做讀寫混合,兼具推和拉兩種模式的優(yōu)點。
推拉模式是一個折中的方案,站在發(fā)件人這一段,如果是個普通的人,那么我們采用寫擴散的方式,直接把數(shù)據(jù)寫入到他的粉絲中去,因為普通的人他的粉絲關注量比較小,所以這樣做沒有壓力,如果是大V,那么他是直接將數(shù)據(jù)先寫入到一份到發(fā)件箱里邊去,然后再直接寫一份到活躍粉絲收件箱里邊去,現(xiàn)在站在收件人這端來看,如果是活躍粉絲,那么大V和普通的人發(fā)的都會直接寫入到自己收件箱里邊來,而如果是普通的粉絲,由于他們上線不是很頻繁,所以等他們上線時,再從發(fā)件箱里邊去拉信息。即根據(jù)用戶的活躍程度來判斷是推模式還是拉模式
推送到粉絲收件箱
需求:
- 修改新增探店筆記的業(yè)務,在保存blog到數(shù)據(jù)庫的同時,推送到粉絲的收件箱
- 收件箱滿足可以根據(jù)時間戳排序,必須用Redis的數(shù)據(jù)結構實現(xiàn)
- 查詢收件箱數(shù)據(jù)時,可以實現(xiàn)分頁查詢
分頁查詢方案
Feed流中的數(shù)據(jù)會不斷更新,所以數(shù)據(jù)的角標也在變化,因此不能采用傳統(tǒng)的分頁模式。
傳統(tǒng)了分頁在feed流是不適用的,因為我們的數(shù)據(jù)會隨時發(fā)生變化
假設在t1 時刻,我們?nèi)プx取第一頁,此時page = 1 ,size = 5 ,那么我們拿到的就是10~6 這幾條記錄,假設現(xiàn)在t2時候又發(fā)布了一條記錄,此時t3 時刻,我們來讀取第二頁,讀取第二頁傳入的參數(shù)是page=2 ,size=5 ,那么此時讀取到的第二頁實際上是從6 開始,然后是6~2 ,那么我們就讀取到了重復的數(shù)據(jù),所以feed流的分頁,不能采用原始方案來做。
Feed流的滾動分頁
我們需要記錄每次操作的最后一條,然后從這個位置開始去讀取數(shù)據(jù)
舉個例子:我們從t1時刻開始,拿第一頁數(shù)據(jù),拿到了10~6,然后記錄下當前最后一次拿取的記錄,就是6,t2時刻發(fā)布了新的記錄,此時這個11放到最頂上,但是不會影響我們之前記錄的6,此時t3時刻來拿第二頁,第二頁這個時候拿數(shù)據(jù),還是從6后一點的5去拿,就拿到了5-1的記錄。我們這個地方可以采用sortedSet來做,可以進行范圍查詢,并且還可以記錄當前獲取數(shù)據(jù)時間戳最小值,就可以實現(xiàn)滾動分頁了,11這條新插入的數(shù)據(jù)進行上拉刷新的時候就會刷新,不必擔心漏讀
分頁演示
首先創(chuàng)建一個Zset集合
通過這一命令我們可以通過score值降序排的方式取出前三條記錄,這么看似乎沒有問題,我們下一頁要查詢的就是后四條記錄,但feed流的數(shù)據(jù)是不斷更新,恰好此時set集合中加入m8
zrevrange z1 0 2 withscores
后三條記錄的查詢理論上是再查三條,這時候按照角標查m5就已經(jīng)重復查詢了,這在業(yè)務中是不允許的
按照score值來查就可以避免該問題,我們只需要記住上一次查詢的最小分數(shù),讓它在下次查詢時最為max,即可實現(xiàn)分頁查詢,limit 后面的第一個參數(shù)即是偏移量,0表示要包含max,1表示小于max分數(shù)的下一個元素
zrevrangebyscore z1 6 0 withscores limit 1 3
推送收件箱
當用戶發(fā)送完筆記后,也要將筆記推送粉絲的收件箱中,即用戶id為key的zest集合中
@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()); }
實現(xiàn)分頁查詢收郵箱
在個人主頁的“關注”卡片中,查詢并展示推送的Blog信息:
具體操作如下:
1、每次查詢完成后,我們要分析出查詢出數(shù)據(jù)的最小時間戳,這個值會作為下一次查詢的條件,即下一次查詢的最大時間戳
2、我們需要找到與上一次查詢相同的查詢個數(shù)作為偏移量,下次查詢時,跳過這些查詢過的數(shù)據(jù),拿到我們需要的數(shù)據(jù)
綜上:我們的請求參數(shù)中就需要攜帶 lastId:上一次查詢的最小時間戳和偏移量這兩個參數(shù)。
這兩個參數(shù)第一次會由前端來指定,以后的查詢就根據(jù)后臺結果作為條件,再次傳遞到后臺。
定義返回實體類
@Data public class ScrollResult { private List<?> list; private Long minTime; private Integer offset; }
BlogController
注意:RequestParam 表示接受url地址欄傳參的注解,當方法上參數(shù)的名稱和url地址欄不相同時,可以通過RequestParam 來進行指定
@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) { // 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.解析數(shù)據(jù):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.獲取分數(shù)(時間戳) long time = tuple.getScore().longValue(); if(time == minTime){ os++; }else{ minTime = time; os = 1; } } os = minTime == max ? os : os + offset; // 5.根據(jù)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); }
到此這篇關于redis實現(xiàn)好友關注&消息推送的方法示例的文章就介紹到這了,更多相關redis 好友關注&消息推送內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
基于redis實現(xiàn)世界杯排行榜功能項目實戰(zhàn)
前段時間,做了一個世界杯競猜積分排行榜。對世界杯64場球賽勝負平進行猜測,猜對+1分,錯誤+0分,一人一場只能猜一次。下面通過本文給大家分享基于redis實現(xiàn)世界杯排行榜功能項目實戰(zhàn),感興趣的朋友一起看看吧2018-10-10從MySQL到Redis的簡單數(shù)據(jù)庫遷移方法
這篇文章主要介紹了從MySQL到Redis的簡單數(shù)據(jù)庫遷移方法,注意Redis數(shù)據(jù)庫基于內(nèi)存,并不能代替?zhèn)鹘y(tǒng)數(shù)據(jù)庫,需要的朋友可以參考下2015-06-06淺談RedisTemplate和StringRedisTemplate的區(qū)別
本文主要介紹了RedisTemplate和StringRedisTemplate的區(qū)別及個人見解,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-06-06