达人探店
发布探店笔记
改一下,图片保存路径就可以直接运行测试了。
查看探店笔记
@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);}
这个测试方法一直运行失败,我真的吐了.