手機app網(wǎng)站建設seo推廣價格
Redis實戰(zhàn)篇筆記(七)
文章目錄
- Redis實戰(zhàn)篇筆記(七)
- 前言
- 達人探店
- 發(fā)布和查看探店筆記
- 點贊
- 點贊排行榜
- 好友關注
- 關注和取關
- 共同關注
- 關注推送
- 關注推薦的實現(xiàn)
- 總結
前言
本系列文章是Redis實戰(zhàn)篇筆記的最后一篇,那么到這里Redis實戰(zhàn)篇的內(nèi)容就要結束了,本系列文件涵蓋了Redis作為緩存在實戰(zhàn)項目中的大多數(shù)用法
達人探店
發(fā)布和查看探店筆記
這個兩個功能就是普通的業(yè)務,沒有用到 redis,所以我把他們合到一起了
發(fā)布探店筆記
我們點擊下面的加號就可以發(fā)布一篇探店筆記
這里的上傳圖片,如果是一般的業(yè)務會上傳到一個文件服務器,但是他這里選擇上傳到了我們的前端服務器
這個 IMAGE_UPLOAD_DIR 就是前端服務器放圖片的地方,要改成自己所對應的位置
我們像這樣就寫好了一篇探店筆記,然后我們點發(fā)布,就會跳轉到個人中心,并且在主頁的最后也能看見我們發(fā)布的筆記
我們點擊看我們剛發(fā)布的筆記,會報錯,是因為我們還沒有實現(xiàn)這個功能,我們點擊查看,就可以看到前端訪問的接口,接下來我們就去實現(xiàn)它
**這就是 查看筆記的方法 ** queryBlogById 是我們要實現(xiàn)的方法, querHotBlog 是代碼已經(jīng)寫好的。由于沒有涉及到 Redis 的操作,這里就不再過多解釋了。
@Resourceprivate IUserService userService;@Overridepublic Result queryHotBlog(Integer current) {// 根據(jù)用戶查詢Page<Blog> page = this.query().orderByDesc("liked").page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));// 獲取當前頁數(shù)據(jù)List<Blog> records = page.getRecords();// 查詢用戶records.forEach(this::queryBlogUser);return Result.ok(records);}@Overridepublic Result queryBlogById(Long id) {// 1. 查詢blogBlog blog = getById(id);if(blog==null){return Result.fail("筆記不存在");}// 2. 查詢blog有關的用戶queryBlogUser(blog);return Result.ok(blog);}private void queryBlogUser(Blog blog) {Long userId = blog.getUserId();User user = userService.getById(userId);blog.setName(user.getNickName());blog.setIcon(user.getIcon());}
實現(xiàn)后,效果就是這樣了。
點贊
代碼中實現(xiàn)的點贊是 連續(xù)點贊的功能,但是這樣的功能是不好的,如果有人調用這個接口,一直刷贊,數(shù)據(jù)庫直接就爆了,所以我們要對這個功能進行改造
改造后的需求:
- 同一個用戶只能點贊一次,再次點擊則取消點贊
- 如果當前用戶已經(jīng)點贊,則點贊按鈕高亮顯示(前端已實現(xiàn),判斷Blog類的 isLike 屬性)
那我們怎樣來標記用戶是否點贊過? 用 Redis 的 set集合可以實現(xiàn),set 集合我們已經(jīng)用了好幾次了,set中的元素是不能重復的,可以用來標記
@Overridepublic Result likeBlog(Long id) {// 1.獲取登錄用戶Long userId = UserHolder.getUser().getId();// 2.判斷當前登錄用戶是否已經(jīng)點贊Boolean isMember = stringRedisTemplate.opsForSet().isMember(BLOG_LIKED_KEY, userId.toString());// 3.如果未點贊,可以點贊if(BooleanUtil.isFalse(isMember)){// 起始這里我覺得也可以做一個異步任務,利用 Redis的高效性,去實現(xiàn)與用戶的交互// 用異步任務來去修改數(shù)據(jù)庫,感覺 Redis 和 數(shù)據(jù)庫都可以這么來用// 3.1 數(shù)據(jù)庫點贊數(shù)+1boolean isUpdate = update().setSql("liked=liked+1").eq("id", id).update();// 3.2 保存用戶到 Redis的 set 集合if(isUpdate){stringRedisTemplate.opsForSet().add(BLOG_LIKED_KEY,userId.toString());}}else {// 4. 如果已經(jīng)點贊,取消點贊// 4.1 數(shù)據(jù)庫點贊數(shù) -1boolean isUpdate = update().setSql("liked=liked-1").eq("id", id).update();// 4.1 把用戶從Redis的 set 集合移除if(isUpdate){stringRedisTemplate.opsForSet().remove(BLOG_LIKED_KEY,userId.toString());}}return Result.ok();}
點贊排行榜
我們在探店筆記的詳情頁面,應該按照點贊的時間顯示出來,比如最早點贊的 TOP5,形成點贊排行榜
但是我們剛才在實現(xiàn)點贊功能的時候,用的是 set 集合,但 set 集合中的元素是無序的,這就不符合我們的功能,所以我們要換一個數(shù)據(jù)結構,即能保留 set 的無序特點,又會使其中的元素有序,那就是 Redis 的 SortedSet
那接下來,我們就要去改造一下我們之前寫的點贊功能
其實就是把之前用 set 集合的操作 換成 SortSet
@Overridepublic Result likeBlog(Long id) {// 1.獲取登錄用戶Long userId = UserHolder.getUser().getId();// 2.判斷當前登錄用戶是否已經(jīng)點贊Double score = stringRedisTemplate.opsForZSet().score(BLOG_LIKED_KEY, userId.toString());// 3.如果未點贊,可以點贊if(score==null){// 3.1 數(shù)據(jù)庫點贊數(shù)+1boolean isUpdate = update().setSql("liked=liked+1").eq("id", id).update();// 3.2 保存用戶到 Redis的 set 集合if(isUpdate){stringRedisTemplate.opsForZSet().add(BLOG_LIKED_KEY,userId.toString(),System.currentTimeMillis());}}else {// 4. 如果已經(jīng)點贊,取消點贊// 4.1 數(shù)據(jù)庫點贊數(shù) -1boolean isUpdate = update().setSql("liked=liked-1").eq("id", id).update();// 4.1 把用戶從Redis的 set 集合移除if(isUpdate){stringRedisTemplate.opsForZSet().remove(BLOG_LIKED_KEY,userId.toString());}}return Result.ok();}private void isLiked(Blog blog){// 1.獲取登錄用戶Long userId = UserHolder.getUser().getId();// 2.判斷當前登錄用戶是否已經(jīng)點贊Double score = stringRedisTemplate.opsForZSet().score(BLOG_LIKED_KEY, userId.toString());blog.setIsLike(score!=null);}
**那接下來我們就去實現(xiàn)這個點贊排行榜
@Overridepublic Result queryBlogLikes(Long id) {String key= BLOG_LIKED_KEY+id;// 1. 查詢 top5 的點贊用戶 zrange key 0 4Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);if(top5==null||top5.isEmpty()){return Result.ok(Collections.emptyList());}// 這里用了大量的 stream流來處理集合,不太懂 stream流的朋友可以先去學習一下stream流List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());String idStr = StrUtil.join(",", ids);// 這里的 sql,沒有默認的是因為,默認的排序它會按 id 降序排列,不符合我們的需求。List<UserDTO> userDTOS = userService.query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list().stream() // 這里處理一下是脫敏.map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());return Result.ok(userDTOS);}
// 因為未登錄用戶會獲取 user失敗,所以這里加一下,其實這樣做是不好的,業(yè)務容易亂,以后未登錄
// 用戶要使用的地方肯定還會有,所以建議還是寫在 攔截器里。private void isLiked(Blog blog){// 1.獲取登錄用戶UserDTO user = UserHolder.getUser();if (user==null){return;}Long userId = user.getId();// 2.判斷當前登錄用戶是否已經(jīng)點贊Double score = stringRedisTemplate.opsForZSet().score(BLOG_LIKED_KEY, userId.toString());blog.setIsLike(score!=null);}
至此,達人探店的模塊我們也學完了,在這個模塊,我們主要去使用了 Redis 中關于 set 和 SortedSet 的使用。🤗
好友關注
關注和取關
這個功能也沒有用到 Redis, 也只是簡單的業(yè)務,這里也就簡單的記一下
其實這也是個挺常見的功能,不跟視頻自己也可以手敲出來,這里就只記錄 service層的代碼
@Overridepublic Result follow(Long followUserId, Boolean isFollow) {Long userId = UserHolder.getUser().getId();// 1.判斷到底是關注還是取關if(isFollow==true){Follow follow = new Follow();follow.setUserId(userId);follow.setFollowUserId(followUserId);save(follow);}else {remove(new QueryWrapper<Follow>().eq("user_id",userId).eq("follow_user_id",followUserId));}return Result.ok();}@Overridepublic Result isFollow(Long followUserId) {Long userId = UserHolder.getUser().getId();// 1.查詢是否關注Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();return Result.ok(count>0);}
共同關注
其實之后的課程有點為了練這個 Redis 而去開發(fā)的這個功能😢,但還是學完吧,也沒有幾節(jié)了
既然要實現(xiàn) 共同關注,那肯定要 先獲取 兩個用戶的關注列表,然后去求交集,那在 Redis 中 set 集合是可以求交集的,所以我們這次用 set 集合來實現(xiàn)求共同關注的功能。在實現(xiàn)這個功能之前,我們先來實現(xiàn)下面兩段代碼
這兩段 代碼與共同關注沒有什么關系,是用來完善用戶的一些信息
// UserController
// 這個是用來點擊頭像,進入主頁
@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
// 這個是進入主頁后,顯示這個博主的博客
@GetMapping("/of/user")public Result queryBlogByUserId(@RequestParam(value = "current",defaultValue = "1")Integer current,@RequestParam("id") Long id){Page<Blog> page = blogService.query().eq("user_id", id).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));List<Blog> records = page.getRecords();return Result.ok(records);}
下面我們就來寫共同關注的代碼,首先我們要把之前寫的關注稍微改一下,就是操作完數(shù)據(jù)庫后,把關注列表形成一個 set 添加到 Redis 中。
@Overridepublic Result follow(Long followUserId, Boolean isFollow) {Long userId = UserHolder.getUser().getId();// 1.判斷到底是關注還是取關if(isFollow==true){Follow follow = new Follow();follow.setUserId(userId);follow.setFollowUserId(followUserId);boolean save = save(follow);if(save){stringRedisTemplate.opsForSet().add("follows:"+userId,followUserId.toString());}}else {remove(new QueryWrapper<Follow>().eq("user_id",userId).eq("follow_user_id",followUserId));stringRedisTemplate.opsForSet().remove("follows:"+userId,followUserId.toString());}return Result.ok();}
然后我們來寫 共同關注
@Overridepublic Result followCommons(Long id) {// 這里還是用來一些流操作的,不熟悉的朋友還是建議去看看流。// 其實這里真正要說的也就是求交集了 set 的 intersect命令Long userId = UserHolder.getUser().getId();String key="follows:"+userId;String followedKey="follows:"+id;Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, followedKey);if(intersect==null||intersect.isEmpty()) return Result.ok(Collections.emptyList());List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());List<UserDTO> users = userService.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());return Result.ok(users);}
其實上面兩個真的沒有什么太多新的東西,用到的 Redis 的部分也是比較少的,想要學 Redis 的朋友也可以跳過這兩節(jié).
關注推送
關注推送也叫 Feed 流,直譯為 投喂,為用戶持續(xù)的提供 “沉浸式” 的體驗,通過無限下拉獲取新的信息
Feed 流產(chǎn)品有兩種常見模式:
- TimeLine: 不做內(nèi)容篩選,簡單的按照內(nèi)容發(fā)布時間排序,常用于好友或關注.例如朋友圈
- 優(yōu)點: 信息全面,不會缺失.并且實現(xiàn)也相對簡單
- 缺點: 信息噪音較多,用戶不一定感興趣,內(nèi)容獲取效率低
- 智能排序: 利用智能算法屏蔽掉違規(guī)的,用戶不感興趣的內(nèi)容,推送用戶感興趣的信息來吸引用戶
- 優(yōu)點: 投喂用戶感興趣信息,用戶粘度高,容易沉迷
- 缺點: 如果算法不精確,可能起反作用
我們這個 個人頁面,是基于關注的好友來做 Feed 流,因此采用 TIMELine 的模式,該模式的實現(xiàn)有三種
- 拉模式
- 推模式
- 推拉結合
拉模式
每個博主都會有一個發(fā)件箱,當他們發(fā)布消息的時候,都會先發(fā)到他們自己的發(fā)件箱當中,并且都帶上時間戳,然后當有一個用戶下拉刷新它的收件箱的時候,這時候,系統(tǒng)會從這個用戶所關注的博主的發(fā)件箱中拉取信息,然后按時間戳排序.下面這個國就演示了這個過程,但是我們想想,它每下拉一次我們就都要給它拉取一次,并且還要排序,那這樣性能是不是就不是很好,那我們接下來繼續(xù)看 推模式
推模式
而推模式就與拉模式不太一樣了,每個博主沒有收件箱 了,而是把信息直接發(fā)給粉絲的收件箱,并且在收件箱內(nèi)部排好序,這樣粉絲下來刷新的時間,就直接從收件箱中取就可以了.這樣就彌補了拉模式的效率問題.但是推模式同樣有一個問題,就是如果有一個博主的粉絲很多,那它要給粉絲發(fā)消息就要發(fā)多份,這個數(shù)據(jù)量上來了,系統(tǒng)也沒法承受,那么能不能把 這兩種模式的優(yōu)點結合起來呢,那就是接下的推拉模式了
推拉模式
在推拉模式中,我們將博主分為 大V 和普通博主,大V的粉絲數(shù)很多,通常幾千萬,而普通博主的粉絲數(shù)就比較少了.
我們也把粉絲分為普通粉絲和活躍粉絲.
對于 大V來說,他的粉絲數(shù)很多,所以肯定不能用推模式,所以就用拉模式.但是對于一些活躍粉絲還是用推模式,因為這些活躍粉絲經(jīng)常取看他們博主的信息,所以效率要高一些,而對于哪些普通粉絲就用拉模式,因為他們對博主的關注也不是很多,所以效率嗎,慢一點也就慢一點了,而對于普通博主來說,他的粉絲數(shù)目沒有那么多,所以用推模式也耗費不少資源.
下面我們再來對比一下這三種模式的優(yōu)缺點.
那對于我們這個系統(tǒng),不會有大v,所以我們這采用推模式來實現(xiàn).
關注推薦的實現(xiàn)
需求:
- 修改新增探店筆記的業(yè)務,在保存 blog 到數(shù)據(jù)庫的時候,推送的收件箱
- 收件箱滿足可以根據(jù)時間戳排序,必須用 Redis 的數(shù)據(jù)結構實現(xiàn)
- 查詢收件箱時,可以實現(xiàn)分頁查詢。
我們先來修改新增博客的業(yè)務,這個業(yè)務用到了 Redis的 set結構來作為用戶的收件箱,并把這個博客推送到這個博客的主人的粉絲的收件箱。
@Overridepublic Result saveBlog(Blog blog) {// 獲取登錄用戶UserDTO user = UserHolder.getUser();blog.setUserId(user.getId());// 保存探店博文boolean isSuccess = save(blog);if(!isSuccess){return Result.fail("新增筆記失敗");}List<Follow> follows = followService.query().eq("follow_user_id", user.getId().toString()).list();for (Follow follow : follows) {//獲取粉絲 idLong userId = follow.getUserId();//推送String key="feeds:"+userId;stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());}// 返回idreturn Result.ok(blog.getId());}
然后我們再來實現(xiàn)粉絲查看自己的收件箱,展示出這個用戶所關注的博主的文章
但是我們這里想一下,這里的分頁還能是傳統(tǒng)的分頁嗎?
我們看下面這條圖,t1時,查詢5條,但是 t2這是又傳過來了一個數(shù)據(jù),t3這時候又查詢5條數(shù)據(jù),從頭開始查的話,會查重一個。這就不是我們想要的,那怎么解決查重,就是滾動查詢
在滾動查詢的時候
t1 和 t2 都是與上述一樣,但是 t3時刻讀取第二頁,是從上回 的lastId 的下一個開始查的,這樣就避免了查重,但是在 Redis 中如何實現(xiàn)呢,我們可以利用 SortedSet 來實現(xiàn)。
**SortedSet 中有一條命令是 **
ZREVRANGEBYSCORE 是用 score 來搜索
其中 max ,min是排序的范圍,max是最大值,min是最小值。WITHSCORES是返回時帶著分數(shù) offset是偏移量,是從最大值的哪一個開始排序,0就是從最大值開始,1就是從最大值的下一個開始。count就是查幾個。
那么我們就可以用時間戳來當分數(shù),最新的時間戳就是最大的分數(shù),排在第一位。第一次的時候可以拿當前時間戳,因為對于當前來說,當前時間戳是最大的,最小值我們不關心,就用 0. 第一次 offset 用0,因為第一次分數(shù)最大的我們也要。
然后往后 max 就應該是上回查詢的最小分數(shù),最小還是0,但是這時的 offset就應該是 1了,因為這次的最小分數(shù)是上一會的最小元素,我們上回已經(jīng)查過了,這次不需要了。所以 offset 要用 1. 具體如下
但是如果兩個元素的時間戳一樣怎么辦?如果這個用戶關注了很多博主,這些博主可能會在同一時間發(fā)布文章,都會推送到這個用戶的收件箱。我們看下面的圖看一下有什么問題
我們看一下,m7和m6的分數(shù)都是 6,第二次查看的時候還是出現(xiàn)了 6,這是為什么,因為,我們第二次查詢的時候 max 是上回的 min,上回的 min是6,而我們的第二次的 offser 是 1,也就是 從分數(shù)為 6 的下一個開始,那分數(shù)為6 的從上往下 第一個是 m7,第二個是 m6,那可不是要從 m6開始查嘛,所以我們的 offset也要改,就是上一次最小分數(shù)的個數(shù)是多少,我們下一次的 offset就是多少,還是這個我們來開,6有兩個,那第二次我們的 offser 就是 2,分數(shù)為6的第一個是m7,往下移動 2位,不就剛好把 上一次我們查到的 分數(shù)為6的隔過去了嘛、
有的朋友可能這里會有的疑惑,如果我 m5 也是 6,你offset不就是 3了,不就把 m5 也隔過去了?
其實不是這樣的,查重復的,我們只在上一次我們查到的里面查重復,不是對于整個 set 查。下面看效果
那思路有了,代碼怎么實現(xiàn)呢,我們先來看接口的規(guī)范
我們在一次查詢中,就要把 本次的最小時間戳和下一次要用的偏移量算出來,傳給前端,前端下一次再調用這個接口的時候,就用這兩個。下面是具體代碼實現(xiàn)
@Overridepublic Result queryBlogOfFollow(Long max, Integer offset) {//1. 獲取當前用戶Long userId = UserHolder.getUser().getId();//2. 查詢收件箱 ZREVRANGEBYSCORE key max min LIMIT offset countString key="feeds:"+userId;//3. 解析數(shù)據(jù):blogId,timestamp,offset// 這里的 TypedTuple 是一個元組,里面有你要查的數(shù)據(jù),以及分數(shù)Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 3);// 非空判斷if(typedTuples==null||typedTuples.isEmpty()){return Result.ok();}//4. 根據(jù)id查詢blogList<Long> ids=new ArrayList<>(typedTuples.size());long minTime=0;int os=1;// 接下來就是算 mintime 和 offset,其實這里我們還是用了一點點小算法,用一個 mintime// 變量來接受最小時間戳,然后每次從元組獲取到時間戳,我們就賦給 mintime,這樣遍歷完// 后,mintime 就是最小的// 然后是 算 offset,這里我們根據(jù) mintime,我們剛才不是說了嘛,遍歷的過程中每獲取一次// time ,就賦給 mintime,那么我們在賦之前,加一步,判斷當前獲取的這個 time 與 mintime//是否相等,相等,就讓 os++,不相等就讓 minTime=time,最后重置 os,到最后 os 一定是// 最小的時間戳的重復次數(shù)。// 其實else 里面 的賦值可以去掉,因為最后還會賦值。for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {//4.1 獲取博客idids.add(Long.valueOf(typedTuple.getValue()));long time = typedTuple.getScore().longValue();if(time==minTime){os++;}else {minTime=time;os=1;}minTime = time;}//5. 封裝并返回String idStr = StrUtil.join(",", ids);List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();for (Blog blog : blogs) {// 查詢 blog 有關的用戶queryBlogUser(blog);// 查詢blog 是否被點贊isLiked(blog);}ScrollResult scrollResult = new ScrollResult();scrollResult.setList(blogs);scrollResult.setOffset(os);scrollResult.setMinTime(minTime);return Result.ok(scrollResult);}
那么今天關于好友關注這個模塊就學完了,雖然前面的比較簡單,但是最后一個理解起來還是有一定難度的
總結
最后的最后,還是希望Redis實戰(zhàn)篇系列比較可以對大家的學習以及工作有一定的幫助,那我們的實戰(zhàn)篇筆記就到這里撒花完結了,朋友們,我們高級篇再見。
我是Mayphyr,從一點點到億點點,我們下次再見