Redis 實(shí)現(xiàn)好友關(guān)注和關(guān)注推送的示例代碼
一、關(guān)注和取關(guān)
1.1 簡介
在探店圖文的詳情頁面中,可以關(guān)注發(fā)布筆記的作者:
1.2 需求描述
實(shí)現(xiàn)兩個(gè)接口:關(guān)注和取關(guān)接口、判斷是否關(guān)注的接口
1.3 代碼實(shí)現(xiàn)
涉及到的 controller 層代碼如下:
@RestController @RequestMapping("/follow") public class FollowController { @Resource private IFollowService followService; @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); } }
涉及到的 service 層代碼如下:
@Service public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService { @Resource private StringRedisTemplate stringRedisTemplate; @Resource private IUserService userService; @Override public Result follow(Long followUserId, Boolean isFollow) { // 1.獲取登錄用戶 Long userId = UserHolder.getUser().getId(); String key = "follows:" + userId; // 1.判斷到底是關(guān)注還是取關(guān) if (isFollow) { // 2.關(guān)注,新增數(shù)據(jù) Follow follow = new Follow(); follow.setUserId(userId); follow.setFollowUserId(followUserId); boolean isSuccess = save(follow); if (isSuccess) { // 把關(guān)注用戶的id,放入redis的set集合 sadd userId followerUserId stringRedisTemplate.opsForSet().add(key, followUserId.toString()); } } else { // 3.取關(guān),刪除 delete from tb_follow where user_id = ? and follow_user_id = ? boolean isSuccess = remove(new QueryWrapper<Follow>() .eq("user_id", userId).eq("follow_user_id", followUserId)); if (isSuccess) { // 把關(guān)注用戶的id從Redis集合中移除 stringRedisTemplate.opsForSet().remove(key, followUserId.toString()); } } return Result.ok(); } @Override public Result isFollow(Long followUserId) { // 1.獲取登錄用戶 Long userId = UserHolder.getUser().getId(); // 2.查詢是否關(guān)注 select count(*) from tb_follow where user_id = ? and follow_user_id = ? Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count(); // 3.判斷 return Result.ok(count > 0); } }
二、共同關(guān)注
2.1 簡介
點(diǎn)擊博主頭像,可以進(jìn)入博主首頁:
2.2 需求描述
利用 Redis 中恰當(dāng)?shù)臄?shù)據(jù)結(jié)構(gòu),實(shí)現(xiàn)共同關(guān)注功能。在博主個(gè)人頁面展示出當(dāng)前用戶與博主的共同好友。
2.3 代碼實(shí)現(xiàn)
涉及到的 controller 層代碼如下:
@RestController @RequestMapping("/follow") public class FollowController { @Resource private IFollowService followService; @GetMapping("/common/{id}") public Result followCommons(@PathVariable("id") Long id){ return followService.followCommons(id); } }
涉及到的 service 層代碼如下:
@Service public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService { @Resource private StringRedisTemplate stringRedisTemplate; @Resource private IUserService userService; @Override public Result followCommons(Long id) { // 1.獲取當(dāng)前用戶 Long userId = UserHolder.getUser().getId(); String key = "follows:" + userId; // 2.求交集 String key2 = "follows:" + id; Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2); if (intersect == null || intersect.isEmpty()) { // 無交集 return Result.ok(Collections.emptyList()); } // 3.解析id集合 List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList()); // 4.查詢用戶 List<UserDTO> users = userService.listByIds(ids) .stream() .map(user -> BeanUtil.copyProperties(user, UserDTO.class)) .collect(Collectors.toList()); return Result.ok(users); } }
三、關(guān)注推送
3.1 簡介
關(guān)注推送也叫做 Feed 流,直譯為投喂。為用戶持續(xù)的提供 “沉浸式” 的體驗(yàn),通過無限下拉刷新獲取新的信息。
3.2 Feed 流的模式
3.2.1 TimeLine
不做內(nèi)容篩選,簡單的按照內(nèi)容發(fā)布時(shí)間排序,常用于好友或關(guān)注。例如朋友圈。
優(yōu)點(diǎn):信息全面,不會(huì)有缺失。并且實(shí)現(xiàn)也相對(duì)簡單
缺點(diǎn):信息噪音較多,用戶不一定感興趣,內(nèi)容獲取效率低
3.2.2 智能排序
利用智能算法屏蔽掉違規(guī)的、用戶不感興趣的內(nèi)容。推送用戶感興趣信息來吸引用戶。
優(yōu)點(diǎn):投喂用戶感興趣信息,用戶粘度很高,容易沉迷
缺點(diǎn):如果算法不精準(zhǔn),可能起到反作用
3.3 實(shí)現(xiàn)方案
本例中的個(gè)人頁面,是基于關(guān)注的好友來做 Feed 流,因此采用 Timeline 的模式。該模式的實(shí)現(xiàn)方案有三種:拉模式、推模式、推拉結(jié)合模式。
3.3.1 拉模式
拉模式也叫做讀擴(kuò)散,假設(shè)現(xiàn)在有三個(gè) up 主,張三李四和王五,它們?nèi)齻€(gè)人將來會(huì)發(fā)一些消息,此時(shí),給他們每個(gè)人都準(zhǔn)備一個(gè)發(fā)件箱,將來它們發(fā)送消息的時(shí)候就會(huì)發(fā)送到發(fā)件箱里面去,發(fā)送的消息除了本身以外還需要攜帶一個(gè)時(shí)間戳,因?yàn)楹竺嬉凑諘r(shí)間排序。
此時(shí)有一個(gè)粉絲趙六,它有一個(gè)收件箱,平常是空的,只有他要去讀消息的時(shí)候我們才會(huì)給他去拉取,即從它所關(guān)注的 up 主的發(fā)件箱去拉取消息,拉過來之后按照時(shí)間去排序,這樣就可以去讀取了。
這種模式的優(yōu)點(diǎn)是節(jié)省內(nèi)存空間,因?yàn)槭占渥x取完畢之后就可以清空,下次再重新拉取。缺點(diǎn)是每次讀消息的時(shí)候都要重新去拉取發(fā)件箱的消息,然后再做排序,這一系列的動(dòng)作耗時(shí)會(huì)比較久,讀取的延遲較高。
3.3.2 推模式
推模式也叫寫擴(kuò)散。假設(shè)此時(shí)有兩個(gè) up 主,張三和李四,粉絲1關(guān)注了張三、粉絲2關(guān)注了張三和李四、粉絲3也關(guān)注了張三和李四,假設(shè)此時(shí)張三要發(fā)送消息,它所發(fā)送的消息會(huì)直接推送到所有粉絲的收件箱里面去,然后對(duì)收件箱里面的消息進(jìn)行排序,此時(shí)粉絲可以直接從收件箱里面讀取消息
此種模式的優(yōu)點(diǎn)是延遲非常的低,彌補(bǔ)了拉模式的缺點(diǎn)。缺點(diǎn)是由于沒有了發(fā)件箱,不得不把消息發(fā)送給每一個(gè)粉絲,內(nèi)存占用會(huì)很高,即一個(gè)消息要寫N份
3.3.3 推拉結(jié)合模式
推拉結(jié)合模式也叫做讀寫混合,兼具推和拉兩種模式的優(yōu)點(diǎn)。假設(shè)現(xiàn)在有一個(gè)大 V 和普通人張三,還有兩個(gè)普通粉絲和一個(gè)活躍粉絲,每個(gè)粉絲都有自己的收件箱。假設(shè)此時(shí)普通人張三要發(fā)送消息,他的粉絲很少,就可以采用推模式,即直接將消息推送到每一個(gè)粉絲的收件箱里面。
而大 V 則不一樣,大 V 的粉絲很多,雖然粉絲多,但是粉絲存在差異,有活躍粉絲和一般粉絲。針對(duì)于活躍粉絲采用推模式,而針對(duì)于一般粉絲采取拉模式,因?yàn)榇?strong> V 需要有一個(gè)發(fā)件箱。
這種推拉結(jié)合的模式,既節(jié)省了內(nèi)存又照顧了活躍用戶的感受
3.3.4 總結(jié)
3.4 Feed 流的分頁問題
Feed 流中的數(shù)據(jù)會(huì)不斷更新,所以數(shù)據(jù)的角標(biāo)也在變化,因此不能采用傳統(tǒng)的分頁模式。接下來我們分析下原因。
以下圖為例,橫線代表時(shí)間軸,此時(shí)時(shí)間來到了 t1 時(shí)刻,收件箱里面已經(jīng)有了 10 條消息了,數(shù)字越大代表越新,讀取第一頁數(shù)據(jù)沒啥問題。
此時(shí)來到了 t2 時(shí)刻,有人插入了一條新的數(shù)據(jù),此時(shí)我們的 11 數(shù)據(jù)就跑到了最前面去了
等到來到了 t3 時(shí)刻,此時(shí)需要讀取第二頁的內(nèi)容,此時(shí)就會(huì)出現(xiàn)重復(fù)讀取的問題,分頁就出現(xiàn)了混亂。
這就是 Feed 流不能采用傳統(tǒng)的分頁模式的原因。
3.5 Feed 流的滾動(dòng)分頁
此時(shí)就需要采用 Feed 流的滾動(dòng)分頁,即記錄每次分頁的最后一條,下次再從這個(gè)位置開始查。第一次讀取時(shí) lastId 設(shè)置成無窮大就可以了,如下圖:
到了 t2 時(shí)刻有人插入了新的數(shù)據(jù) 11,如下圖:
等到 t3 時(shí)刻,讀取第二頁的時(shí)候,讓 lastId = 6,向后查 5 條就不會(huì)出現(xiàn)問題了,如下圖:
此時(shí)的查詢是不依賴于腳標(biāo)的,所以數(shù)據(jù)不受影響。所以只能使用 zset 結(jié)構(gòu)。
3.6 需求描述
基于推模式實(shí)現(xiàn)關(guān)注推送功能:
1、修改新增探店筆記的業(yè)務(wù),在保存 blog 到數(shù)據(jù)庫的同時(shí),推送到粉絲的收件箱
2、收件箱滿足可以根據(jù)時(shí)間戳排序,必須用 Redis 的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)
3、查詢收件箱數(shù)據(jù)時(shí),可以實(shí)現(xiàn)分頁查詢
3.7 代碼實(shí)現(xiàn)
首先完成修改新增探店筆記的業(yè)務(wù),在保存 blog 到數(shù)據(jù)庫的同時(shí),推送到粉絲的收件箱功能,涉及到的 controller 層代碼如下:
@RestController @RequestMapping("/blog") public class BlogController { @Resource private IBlogService blogService; @PostMapping public Result saveBlog(@RequestBody Blog blog) { return blogService.saveBlog(blog); } }
涉及到的 service 層代碼如下:
@Service public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService { @Resource private IUserService userService; @Resource StringRedisTemplate stringRedisTemplate; @Resource IFollowService followService; @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:" + userId; stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis()); } // 5.返回id return Result.ok(blog.getId()); } }
接下來完成剩下的兩小點(diǎn)需求,界面請(qǐng)求的參數(shù)如下:
涉及到的 controller 層代碼如下:
@RestController @RequestMapping("/blog") public class BlogController { @Resource private IBlogService blogService; @PostMapping public Result saveBlog(@RequestBody Blog blog) { return blogService.saveBlog(blog); } @GetMapping("/of/follow") public Result queryBlogOfFollow( @RequestParam("lastId") Long max, @RequestParam(value = "offset", defaultValue = "0") Integer offset){ return blogService.queryBlogOfFollow(max, offset); } }
涉及到的 service 層代碼如下:
@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; } } // 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); }
封裝返回給前端的實(shí)體類如下:
@Data public class ScrollResult { private List<?> list; private Long minTime; private Integer offset; }
到此這篇關(guān)于 Redis 實(shí)現(xiàn)好友關(guān)注和關(guān)注推送的示例代碼的文章就介紹到這了,更多相關(guān) Redis 好友關(guān)注和關(guān)注推送內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Govern Service 基于 Redis 的服務(wù)治理平臺(tái)安裝過程詳解
Govern Service 是一個(gè)輕量級(jí)、低成本的服務(wù)注冊(cè)、服務(wù)發(fā)現(xiàn)、 配置服務(wù) SDK,通過使用現(xiàn)有基礎(chǔ)設(shè)施中的 Redis 不用給運(yùn)維部署帶來額外的成本與負(fù)擔(dān),接下來通過本文給大家分享Govern Service 基于 Redis 的服務(wù)治理平臺(tái)的相關(guān)知識(shí),感興趣的朋友一起看看吧2021-05-05redis連接報(bào)錯(cuò)error:NOAUTH Authentication required
本文主要介紹了redis連接報(bào)錯(cuò)error:NOAUTH Authentication required,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05redis中使用bloomfilter的白名單功能解決緩存穿透問題
本文主要介紹了redis中使用bloomfilter的白名單功能解決緩存穿透問題,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07Redis 使用跳表實(shí)現(xiàn)有序集合的方法
Redis有序集合底層為什么使用跳表而非其他數(shù)據(jù)結(jié)構(gòu)如平衡樹、紅黑樹或B+樹的原因在于其特殊的設(shè)計(jì)和應(yīng)用場景,跳表提供了與平衡樹類似的效率,同時(shí)實(shí)現(xiàn)更簡單,調(diào)試和修改也更加容易,感興趣的朋友一起看看吧2024-09-09巧用Redis實(shí)現(xiàn)分布式鎖詳細(xì)介紹
大家好,本篇文章主要講的是巧用Redis實(shí)現(xiàn)分布式鎖詳細(xì)介紹,感興趣的同學(xué)趕快來看一看吧,對(duì)你有幫助的話記得收藏一下,方便下次瀏覽2021-12-12RediSearch加RedisJSON大于Elasticsearch的搜索存儲(chǔ)引擎
這篇文章主要為大家介紹了RediSearch加RedisJSON大于Elasticsearch的王炸使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07