redis實(shí)現(xiàn)好友關(guān)注&消息推送的方法示例
關(guān)注和取消關(guān)注
在查看筆記詳情時(shí),會(huì)自動(dòng)發(fā)送請求,調(diào)用接口來檢查當(dāng)前用戶是否已經(jīng)關(guān)注了筆記作者,我們要實(shí)現(xiàn)這兩個(gè)接口
需求:基于該表數(shù)據(jù)結(jié)構(gòu),實(shí)現(xiàn)兩個(gè)接口:
- 關(guān)注和取關(guān)接口
- 判斷是否關(guān)注的接口
關(guān)注是User之間的關(guān)系,是博主與粉絲的關(guān)系,數(shù)據(jù)庫中有一張tb_follow表來標(biāo)識(shí),我們要將用戶與博主關(guān)聯(lián)起來,只需要向這張表插入數(shù)據(jù)即可,需要將主鍵設(shè)為自增加,降低開發(fā)難度
FollowController
//關(guān)注 @PutMapping("/{id}/{isFollow}") public Result follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") Boolean isFollow) { return followService.follow(followUserId, isFollow); } //取消關(guān)注 @GetMapping("/or/not/{id}") public Result isFollow(@PathVariable("id") Long followUserId) { return followService.isFollow(followUserId); }
判斷當(dāng)前用戶與博主是否已經(jīng)關(guān)注了博主我們只需要查詢follow表中是否有記錄即可,等值查詢建議進(jìn)行非空判斷,防止出現(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ā)送請求時(shí),會(huì)發(fā)送isFollow這個(gè)boolean值,用來讓后端判斷當(dāng)前用戶是否已經(jīng)關(guān)注了博主,如果為true,則直接插入數(shù)據(jù)庫,表示關(guān)注成功,如果為false,表示已經(jīng)關(guān)注要取消關(guān)注了
@Override public Result follow(Long followUserId, Boolean isFollow) { Long userId = UserHolder.getUser().getId(); String key = "follows:" + userId; if (isFollow) { //封裝follow對(duì)象 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(); }
共同關(guān)注
共同關(guān)注的好友,需要首先進(jìn)入到這個(gè)頁面,這個(gè)頁面會(huì)發(fā)起兩個(gè)請求
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)); // 獲取當(dāng)前頁數(shù)據(jù) List<Blog> records = page.getRecords(); return Result.ok(records); }
共同關(guān)注的需求:利用Redis中恰當(dāng)?shù)臄?shù)據(jù)結(jié)構(gòu),實(shí)現(xiàn)共同關(guān)注功能。在博主個(gè)人頁面展示出當(dāng)前用戶與博主的共同關(guān)注。在這里我們選用set集合,因?yàn)樵趕et集合中,有交集并集補(bǔ)集的api,我們可以把兩人的關(guān)注的人分別放入到一個(gè)set集合中,然后再通過api去查看這兩個(gè)set集合中的交集數(shù)據(jù)。
首先對(duì)添加關(guān)注進(jìn)行改造,在插入數(shù)據(jù)的同時(shí)也要將當(dāng)前用戶關(guān)注的博主用戶id插入到set集合中去,用于查找交集
@Override public Result follow(Long followUserId, Boolean isFollow) { Long userId = UserHolder.getUser().getId(); String key = "follows:" + userId; if (isFollow) { //封裝follow對(duì)象 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) { //獲取當(dāng)前用戶 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流實(shí)現(xiàn)方案
當(dāng)我們關(guān)注了用戶后,這個(gè)用戶發(fā)了動(dòng)態(tài),那么我們應(yīng)該把這些數(shù)據(jù)推送給用戶,這個(gè)需求,其實(shí)我們又把他叫做Feed流,關(guān)注推送也叫做Feed流,直譯為投喂。為用戶持續(xù)的提供“沉浸式”的體驗(yàn),通過無限下拉刷新獲取新的信息。
對(duì)于傳統(tǒng)的模式的內(nèi)容解鎖:我們是需要用戶去通過搜索引擎或者是其他的方式去解鎖想要看的內(nèi)容
對(duì)于新型的Feed流的的效果:不需要我們用戶再去推送信息,而是系統(tǒng)分析用戶到底想要什么,然后直接把內(nèi)容推送給用戶,從而使用戶能夠更加的節(jié)約時(shí)間,不用主動(dòng)去尋找。類似B站,抖音等平臺(tái)的大數(shù)據(jù)推送機(jī)制
Feed流的兩種模式
Feed流的實(shí)現(xiàn)有兩種模式:
Feed流產(chǎn)品有兩種常見模式: Timeline:不做內(nèi)容篩選,簡單的按照內(nèi)容發(fā)布時(shí)間排序,常用于好友或關(guān)注。例如朋友圈
- 優(yōu)點(diǎn):信息全面,不會(huì)有缺失。并且實(shí)現(xiàn)也相對(duì)簡單
- 缺點(diǎn):信息噪音較多,用戶不一定感興趣,內(nèi)容獲取效率低
智能排序:利用智能算法屏蔽掉違規(guī)的、用戶不感興趣的內(nèi)容。推送用戶感興趣信息來吸引用戶
- 優(yōu)點(diǎn):投喂用戶感興趣信息,用戶粘度很高,容易沉迷
- 缺點(diǎn):如果算法不精準(zhǔn),可能起到反作用。 本例中的個(gè)人頁面,是基于關(guān)注的好友來做Feed流,因此采用Timeline的模式。該模式的實(shí)現(xiàn)方案有三種
Timeline模式的實(shí)現(xiàn)方案
拉模式:也叫做讀擴(kuò)散
該模式的核心含義就是:當(dāng)張三和李四和王五發(fā)了消息后,都會(huì)保存在自己的郵箱中,假設(shè)趙六要讀取信息,那么他會(huì)從讀取他自己的收件箱,此時(shí)系統(tǒng)會(huì)從他關(guān)注的人群中,把他關(guān)注人的信息全部都進(jìn)行拉取,然后在進(jìn)行排序
優(yōu)點(diǎn):比較節(jié)約空間,因?yàn)橼w六在讀信息時(shí),并沒有重復(fù)讀取,而且讀取完之后可以把他的收件箱進(jìn)行清楚。
缺點(diǎn):比較延遲,當(dāng)用戶讀取數(shù)據(jù)時(shí)才去關(guān)注的人里邊去讀取數(shù)據(jù),假設(shè)用戶關(guān)注了大量的用戶,那么此時(shí)就會(huì)拉取海量的內(nèi)容,對(duì)服務(wù)器壓力巨大。
推模式:也叫做寫擴(kuò)散。
推模式是沒有寫郵箱的,當(dāng)張三寫了一個(gè)內(nèi)容,此時(shí)會(huì)主動(dòng)的把張三寫的內(nèi)容發(fā)送到他的粉絲收件箱中去,假設(shè)此時(shí)李四再來讀取,就不用再去臨時(shí)拉取了
優(yōu)點(diǎn):時(shí)效快,不用臨時(shí)拉取
缺點(diǎn):內(nèi)存壓力大,假設(shè)一個(gè)大V寫信息,很多人關(guān)注他, 就會(huì)寫很多分?jǐn)?shù)據(jù)到粉絲那邊去
推拉結(jié)合模式:也叫做讀寫混合,兼具推和拉兩種模式的優(yōu)點(diǎn)。
推拉模式是一個(gè)折中的方案,站在發(fā)件人這一段,如果是個(gè)普通的人,那么我們采用寫擴(kuò)散的方式,直接把數(shù)據(jù)寫入到他的粉絲中去,因?yàn)?strong>普通的人他的粉絲關(guān)注量比較小,所以這樣做沒有壓力,如果是大V,那么他是直接將數(shù)據(jù)先寫入到一份到發(fā)件箱里邊去,然后再直接寫一份到活躍粉絲收件箱里邊去,現(xiàn)在站在收件人這端來看,如果是活躍粉絲,那么大V和普通的人發(fā)的都會(huì)直接寫入到自己收件箱里邊來,而如果是普通的粉絲,由于他們上線不是很頻繁,所以等他們上線時(shí),再從發(fā)件箱里邊去拉信息。即根據(jù)用戶的活躍程度來判斷是推模式還是拉模式
推送到粉絲收件箱
需求:
- 修改新增探店筆記的業(yè)務(wù),在保存blog到數(shù)據(jù)庫的同時(shí),推送到粉絲的收件箱
- 收件箱滿足可以根據(jù)時(shí)間戳排序,必須用Redis的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)
- 查詢收件箱數(shù)據(jù)時(shí),可以實(shí)現(xiàn)分頁查詢
分頁查詢方案
Feed流中的數(shù)據(jù)會(huì)不斷更新,所以數(shù)據(jù)的角標(biāo)也在變化,因此不能采用傳統(tǒng)的分頁模式。
傳統(tǒng)了分頁在feed流是不適用的,因?yàn)槲覀兊臄?shù)據(jù)會(huì)隨時(shí)發(fā)生變化
假設(shè)在t1 時(shí)刻,我們?nèi)プx取第一頁,此時(shí)page = 1 ,size = 5 ,那么我們拿到的就是10~6 這幾條記錄,假設(shè)現(xiàn)在t2時(shí)候又發(fā)布了一條記錄,此時(shí)t3 時(shí)刻,我們來讀取第二頁,讀取第二頁傳入的參數(shù)是page=2 ,size=5 ,那么此時(shí)讀取到的第二頁實(shí)際上是從6 開始,然后是6~2 ,那么我們就讀取到了重復(fù)的數(shù)據(jù),所以feed流的分頁,不能采用原始方案來做。
Feed流的滾動(dòng)分頁
我們需要記錄每次操作的最后一條,然后從這個(gè)位置開始去讀取數(shù)據(jù)
舉個(gè)例子:我們從t1時(shí)刻開始,拿第一頁數(shù)據(jù),拿到了10~6,然后記錄下當(dāng)前最后一次拿取的記錄,就是6,t2時(shí)刻發(fā)布了新的記錄,此時(shí)這個(gè)11放到最頂上,但是不會(huì)影響我們之前記錄的6,此時(shí)t3時(shí)刻來拿第二頁,第二頁這個(gè)時(shí)候拿數(shù)據(jù),還是從6后一點(diǎn)的5去拿,就拿到了5-1的記錄。我們這個(gè)地方可以采用sortedSet來做,可以進(jìn)行范圍查詢,并且還可以記錄當(dāng)前獲取數(shù)據(jù)時(shí)間戳最小值,就可以實(shí)現(xiàn)滾動(dòng)分頁了,11這條新插入的數(shù)據(jù)進(jìn)行上拉刷新的時(shí)候就會(huì)刷新,不必?fù)?dān)心漏讀
分頁演示
首先創(chuàng)建一個(gè)Zset集合
通過這一命令我們可以通過score值降序排的方式取出前三條記錄,這么看似乎沒有問題,我們下一頁要查詢的就是后四條記錄,但feed流的數(shù)據(jù)是不斷更新,恰好此時(shí)set集合中加入m8
zrevrange z1 0 2 withscores
后三條記錄的查詢理論上是再查三條,這時(shí)候按照角標(biāo)查m5就已經(jīng)重復(fù)查詢了,這在業(yè)務(wù)中是不允許的
按照score值來查就可以避免該問題,我們只需要記住上一次查詢的最小分?jǐn)?shù),讓它在下次查詢時(shí)最為max,即可實(shí)現(xiàn)分頁查詢,limit 后面的第一個(gè)參數(shù)即是偏移量,0表示要包含max,1表示小于max分?jǐn)?shù)的下一個(gè)元素
zrevrangebyscore z1 6 0 withscores limit 1 3
推送收件箱
當(dāng)用戶發(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()); }
實(shí)現(xiàn)分頁查詢收郵箱
在個(gè)人主頁的“關(guān)注”卡片中,查詢并展示推送的Blog信息:
具體操作如下:
1、每次查詢完成后,我們要分析出查詢出數(shù)據(jù)的最小時(shí)間戳,這個(gè)值會(huì)作為下一次查詢的條件,即下一次查詢的最大時(shí)間戳
2、我們需要找到與上一次查詢相同的查詢個(gè)數(shù)作為偏移量,下次查詢時(shí),跳過這些查詢過的數(shù)據(jù),拿到我們需要的數(shù)據(jù)
綜上:我們的請求參數(shù)中就需要攜帶 lastId:上一次查詢的最小時(shí)間戳和偏移量這兩個(gè)參數(shù)。
這兩個(gè)參數(shù)第一次會(huì)由前端來指定,以后的查詢就根據(jù)后臺(tái)結(jié)果作為條件,再次傳遞到后臺(tái)。
定義返回實(shí)體類
@Data public class ScrollResult { private List<?> list; private Long minTime; private Integer offset; }
BlogController
注意:RequestParam 表示接受url地址欄傳參的注解,當(dāng)方法上參數(shù)的名稱和url地址欄不相同時(shí),可以通過RequestParam 來進(jìn)行指定
@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.獲取當(dāng)前用戶 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(時(shí)間戳)、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.獲取分?jǐn)?shù)(時(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有關(guān)的用戶 queryBlogUser(blog); // 5.2.查詢blog是否被點(diǎn)贊 isBlogLiked(blog); } // 6.封裝并返回 ScrollResult r = new ScrollResult(); r.setList(blogs); r.setOffset(os); r.setMinTime(minTime); return Result.ok(r); }
到此這篇關(guān)于redis實(shí)現(xiàn)好友關(guān)注&消息推送的方法示例的文章就介紹到這了,更多相關(guān)redis 好友關(guān)注&消息推送內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Centos7.3安裝Redis4.0.6詳細(xì)圖文教程
這篇文章主要介紹了Centos7.3安裝Redis4.0.6詳細(xì)教程圖解,本文圖文并茂給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-10-10基于redis實(shí)現(xiàn)世界杯排行榜功能項(xiàng)目實(shí)戰(zhàn)
前段時(shí)間,做了一個(gè)世界杯競猜積分排行榜。對(duì)世界杯64場球賽勝負(fù)平進(jìn)行猜測,猜對(duì)+1分,錯(cuò)誤+0分,一人一場只能猜一次。下面通過本文給大家分享基于redis實(shí)現(xiàn)世界杯排行榜功能項(xiàng)目實(shí)戰(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ū)別及個(gè)人見解,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06Redis sentinel節(jié)點(diǎn)如何修改密碼
這篇文章主要介紹了Redis sentinel節(jié)點(diǎn)如何修改密碼問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01詳解基于redis實(shí)現(xiàn)的四種常見的限流策略
限流算法在分布式領(lǐng)域是一個(gè)經(jīng)常被提起的話題,當(dāng)系統(tǒng)的處理能力有限時(shí), 如何阻止計(jì)劃外的請求繼續(xù)對(duì)系統(tǒng)施壓,這是一個(gè)需要重視的問題。除了控制流量,限流還有一個(gè)應(yīng)用目的是控制用戶行為,避免垃圾請求2021-06-06