達人探店
發布探店筆記
?改一下,圖片保存路徑就可以直接運行測試了。
查看探店筆記
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Resourceprivate IUserService userService;@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());}@Overridepublic Result queryHotBlog(Integer current) {// 根據用戶查詢Page<Blog> page = query().orderByDesc("liked").page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));// 獲取當前頁數據List<Blog> records = page.getRecords();// 查詢用戶records.forEach(this::queryBlogUser);return Result.ok(records);}
}
點贊
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Resourceprivate IUserService userService;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryBlogById(Long id) {//1.查詢blogBlog blog = getById(id);if(blog==null){return Result.fail("筆記不存在");}//2.查詢blog有關用戶queryBlogUser(blog);//3.查詢blog是否被點贊isBlogLiked(blog);return Result.ok(blog);}private void isBlogLiked(Blog blog) {//1.判獲取登錄用戶Long userId = UserHolder.getUser().getId();//2.判斷當前登錄用戶是否已經點贊String key=BLOG_LIKED_KEY+blog.getId();Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());blog.setIsLike(BooleanUtil.isTrue(isMember));}private void queryBlogUser(Blog blog) {Long userId = blog.getUserId();User user = userService.getById(userId);blog.setName(user.getNickName());blog.setIcon(user.getIcon());}@Overridepublic Result queryHotBlog(Integer current) {// 根據用戶查詢Page<Blog> page = query().orderByDesc("liked").page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));// 獲取當前頁數據List<Blog> records = page.getRecords();// 查詢用戶records.forEach(blog->{this.queryBlogUser(blog);this.isBlogLiked(blog);});return Result.ok(records);}@Overridepublic Result likeBlog(Long id) {//1.判獲取登錄用戶Long userId = UserHolder.getUser().getId();//2.判斷當前登錄用戶是否已經點贊String key=BLOG_LIKED_KEY+id;Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());if(BooleanUtil.isFalse(isMember)) {//3.如果未點贊//3.1數據庫點贊數+1boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();//3.2保存用戶到Redis的set集合if(isSuccess){stringRedisTemplate.opsForSet().add(key,userId.toString());}}else {//4.如果已點贊,取消點贊//4.1數據庫點贊數-1boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();//4.2把用戶從redis的set集合刪除stringRedisTemplate.opsForSet().remove(key,userId.toString());}return Result.ok();}
}
點贊排行榜
?為了將最早點贊的人擺在最前面,需要按照時間先后存儲分數,這里使用zset存儲時間戳作為zset的score作為排序的依據。
然后針對mysql的排序會亂序的問題需要自定義排序規則,將數據按照指定順序展示.
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Resourceprivate IUserService userService;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryBlogById(Long id) {//1.查詢blogBlog blog = getById(id);if(blog==null){return Result.fail("筆記不存在");}//2.查詢blog有關用戶queryBlogUser(blog);//3.查詢blog是否被點贊isBlogLiked(blog);return Result.ok(blog);}private void isBlogLiked(Blog blog) {//1.判獲取登錄用戶UserDTO user = UserHolder.getUser();if(user==null){//用戶未登錄,無需查詢是否點贊return;}Long userId = user.getId();//2.判斷當前登錄用戶是否已經點贊String key=BLOG_LIKED_KEY+blog.getId();Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());blog.setIsLike(score!=null);}private void queryBlogUser(Blog blog) {Long userId = blog.getUserId();User user = userService.getById(userId);blog.setName(user.getNickName());blog.setIcon(user.getIcon());}@Overridepublic Result queryHotBlog(Integer current) {// 根據用戶查詢Page<Blog> page = query().orderByDesc("liked").page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));// 獲取當前頁數據List<Blog> records = page.getRecords();// 查詢用戶records.forEach(blog->{this.queryBlogUser(blog);this.isBlogLiked(blog);});return Result.ok(records);}@Overridepublic Result likeBlog(Long id) {//1.判獲取登錄用戶Long userId = UserHolder.getUser().getId();//2.判斷當前登錄用戶是否已經點贊String key=BLOG_LIKED_KEY+id;Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());if(score==null) {//3.如果未點贊//3.1數據庫點贊數+1boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();//3.2保存用戶到Redis的set集合 zadd key value scoreif(isSuccess){stringRedisTemplate.opsForZSet().add(key,userId.toString(),System.currentTimeMillis());}}else {//4.如果已點贊,取消點贊//4.1數據庫點贊數-1boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();//4.2把用戶從redis的set集合刪除stringRedisTemplate.opsForZSet().remove(key,userId.toString());}return Result.ok();}@Overridepublic Result queryBlogLikes(Long id) {//1.查詢top5的點贊用戶 zrange key 0 4String key=BLOG_LIKED_KEY+id;Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);if(top5==null||top5.isEmpty()){return Result.ok(Collections.emptyList());}//2.解析出用戶idList<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());//將id拼成字符串String idStr = StrUtil.join(",", ids);//3.根絕用戶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());//4.返回return Result.ok(userDTOS);}
}
好友關注
關注和取關
一個接口實現關注和取關
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {/*** 關注取關功能* @param followUserId* @param isFollow* @return*/@Overridepublic Result follow(Long followUserId, Boolean isFollow) {//1.獲取登錄用戶Long userId = UserHolder.getUser().getId();//1.判斷到底是關注還是取關if (isFollow) {//2.關注,新增數據Follow follow = new Follow();follow.setUserId(userId);follow.setFollowUserId(followUserId);save(follow);}else{//3.取關,刪除 delete from tb_follow where userId = ? and follow_user_id = ?remove(new QueryWrapper<Follow>().eq("user_id",userId).eq("follow_user_id",followUserId));}return Result.ok();}@Overridepublic Result isFollow(Long followUserId) {//1.獲取登錄用戶Long userId = UserHolder.getUser().getId();//2.查詢是否關注 select count(*) from tb_follow where userId = ? and follow_user_id = ?Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();//3.判斷return Result.ok(count>0);}
}
共同關注
這兩個接口直接使用資料里的代碼片段
// UserController 根據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
@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);
}
?為了能用redis的set集合取交集求得共同關注的好友,這里要將一個用戶關注的所有人都存儲到redis里的set集合,,取關了從set集合里刪除。
代碼實現
@Overridepublic Result followCommons(Long id) {//1.獲取當前用戶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);}
關注推送
有點像觀察者模式,觀察者給監聽者集體發送消息。
?feed流實現方案分析?
拉模式下會將用戶關注的人的消息全都拉取到收件箱里面按照時間進行排序。?這個模式下,消息只會在作者的發件箱持久保存一份,用戶的收件箱每次打開都會去拉取一份新的,用完就刪。每次都讀取一堆,性能消耗大。
?推模式下會將消息發送到用戶的收件箱持久化保存。但是這個模式下一個消息會寫n份,內存占用高。
普通v直接用推模式,大v的活躍粉絲用推模式,普通粉絲數量多用拉模式。?
?基于推模式實現關注推送功能
這里選擇推送blog的id到用戶的收件箱,用戶要看時再現查。
這里使用Redis的Zset進行保存,基于時間戳進行排序,要進行分頁查詢時利用角標0~n。
這里傳統分頁好像是有個問題,會出現相同商品重復出現的情況。然后利用滾動分頁就不會有這種情況,但是這樣話新數據又去哪里了??
?
使用sortedSet實現的時候,每次分頁記住時間戳最小的那個,下次查就從更小的開始,就不會出現數據重復了。
推送代碼實現
改造新增blog的代碼,在新增成功的時候獲取所有粉絲id進行blog推送。
@Overridepublic Result saveBlog(Blog blog) {// 1.獲取登錄用戶UserDTO user = UserHolder.getUser();blog.setUserId(user.getId());// 2.保存探店博文boolean isSucess = save(blog);if(!isSucess){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獲取粉絲idLong userId = follow.getUserId();//4.2推送String key=FEED_KEY+userId;stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());}// 3.返回idreturn Result.ok(blog.getId());}
滾動分頁查詢收件箱思路
使用的是zrange,不能按照角標查詢,要按照score進行查詢.
所以這里使用按照分數查詢的命令.?但是這里也會存在問題,如果存在兩個相同score的話,可能也會出現重復,因為命令是按照小于等于。
滾動分頁查詢實現?
這個代碼好像還是有點問題,這里查詢了blog有關用戶和是否點贊卻沒有用上
@Data
public class ScrollResult {private List<?> list;private Long minTime;private Integer offset;
}
@GetMapping("/of/follow")public Result queryBlogOfFollow(@RequestParam("lastId") Long max, @RequestParam(value = "offset",defaultValue = "0") Integer offset){return blogService.queryBlogOfFollow(max,offset);}
@Overridepublic Result queryBlogOfFollow(Long max, Integer offset) {//1.獲取當前用戶Long userId = UserHolder.getUser().getId();//2.查詢收件箱 ZREVRANGBYSCORE key MAX MIN WITHSCORES LIMIT offset countString 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.解析數據: blogId ,mintime(時間戳),offsetList<Long> ids=new ArrayList<>(typedTuples.size());long minTime=0;int os=1;for (ZSetOperations.TypedTuple<String> tuple : typedTuples) { ////4.1獲取idids.add(Long.valueOf(tuple.getValue()));//4.2獲取分數(時間戳)long time=tuple.getScore().longValue();if(time==minTime){os++;}else{minTime=time;os=1;}}//5.根據id查詢blog 基于in語句查詢會打亂順序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 scrollResult = new ScrollResult();scrollResult.setList(blogs);scrollResult.setMinTime(minTime);scrollResult.setOffset(os);return Result.ok(scrollResult);}
附近商鋪
GEO數據結構的基本用法
?
導入店鋪數據到GEO
寫一個數據導入的測試類
?
@Testvoid loadShopDate(){//1.查詢店鋪信息List<Shop> list = shopService.list();//2.把店鋪分組,按照typeId分組,typeId一致的放到一個集合Map<Long,List<Shop>> map=list.stream().collect(Collectors.groupingBy(Shop::getTypeId));//3.分批完成寫入Redisfor (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {//3.1獲取類型idLong typeId = entry.getKey();String key= SHOP_GEO_KEY+typeId;//3.2獲取同類型的店鋪的集合List<Shop> value = entry.getValue();List<RedisGeoCommands.GeoLocation<String>> locations=new ArrayList<>();//3.3寫入redis geoadd key 經度 維度 memberfor (Shop shop : value) {//寫入一條數據//stringRedisTemplate.opsForGeo().add(key,new Point(shop.getX(),shop.getY()),shop.getId().toString());locations.add(new RedisGeoCommands.GeoLocation<>(shop.getId().toString(),new Point(shop.getX(),shop.getY())));}//一次寫入多條數據stringRedisTemplate.opsForGeo().add(key,locations);}}
實現附近商戶功能
用戶簽到
BitMap功能演示?
這個表格里的每一行數據就是用戶的一條簽到記錄,現在使用二進制壓縮來存儲.
實現簽到功能
?代碼實現
@Overridepublic Result sign() {//1.獲取當前登錄用戶Long userId = UserHolder.getUser().getId();//2.獲取日期LocalDateTime now = LocalDateTime.now();//3.拼接keyString keySuffix = now.format(DateTimeFormatter.ofPattern("yyyyMM"));String key=USER_SIGN_KEY+userId+keySuffix;//4.獲取今天是本月的第幾天int dayOfMonth = now.getDayOfMonth();//5.寫入Redis SETBIT key offset 1stringRedisTemplate.opsForValue().setBit(key, dayOfMonth-1,true);return Result.ok();}
統計連續簽到
代碼實現
@Overridepublic Result signCount() {//1.獲取當前登錄用戶Long userId = UserHolder.getUser().getId();//2.獲取日期LocalDateTime now = LocalDateTime.now();//3.拼接keyString keySuffix = now.format(DateTimeFormatter.ofPattern("yyyyMM"));String key=USER_SIGN_KEY+userId+keySuffix;//4.獲取今天是本月的第幾天int dayOfMonth = now.getDayOfMonth();//5.獲取本月截止今天為止的所有的簽到記錄,要返回的是一個十進制的數字 BITFIELD sign:8:202312 GET u14 0List<Long> result = stringRedisTemplate.opsForValue().bitField(key,BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0) //今天多少號就多少個bit位);if(result==null||result.isEmpty()){//沒有任何簽到結果return Result.ok(0);}Long num=result.get(0);if(num==null||num==0){return Result.ok(0);}//6.循環遍歷int count=0;while(true){//6.1讓這個數字與1做與運算,得到數字的最后一個bit位//判斷這個bit位是否為0if ((num&1)==0) {//如果為0,說明未簽到,結束break;}else{//如果不為0,說明已簽到,計數器+1count++;}//把數字右移一位,拋棄最后一個bit位,繼續下一個bit位num>>>=1;}return Result.ok(count);}
UV統計
HyperLogLog的用法
?
測試百萬數據的統計
@Testvoid testHyperLogLog(){//準備數組,裝用戶數據String[] users = new String[1000];//數據角標記int index=0;for(int i=1;i<=1000000;i++){//賦值users[index++]="user_"+i;//每1000條發送一次if(i%1000==0){index=0;stringRedisTemplate.opsForHyperLogLog().add("hl2",users);}}//統計數量Long hl2 = stringRedisTemplate.opsForHyperLogLog().size("hl2");System.out.println(hl2);}
這個測試方法一直運行失敗,我真的吐了.