[021-21].第10节:达人探店功能

我的后端学习大纲

我的Redis学习大纲


1.发布和查看探店笔记:

1.1.发布探店笔记:

此功能未用到redis

a.数据库表:

  • 1.探店相关表:
    在这里插入图片描述
    在这里插入图片描述

b.探店有关接口:

  • 1.图片上传和发布是调用了两个接口:
    在这里插入图片描述

注意:修改图片上传的地址,建议直接放到你的Ngixn下的imgs目录


1.2.查看探店笔记:

此功能未用到redis

a.需求说明:

  • 1.点击探店首页的笔记,会进入一个探店的详情页面,实现该页面的查询接口如下:
    在这里插入图片描述
    在这里插入图片描述

  • 2.实现查询博客详情和热点博客:

        /**
         * 根据id查询博客
         *
         * @param id
         * @return
         */
        @Override
        public Result queryBlogById(Long id) {
            // 查询博客信息
            Blog blog = this.getById(id);
            if (Objects.isNull(blog)){
                return Result.fail("笔记不存在");
            }
            // 查询blog相关的用户信息
            queryUserByBlog(blog);
            return Result.ok(blog);
        }
    
        /**
         * 查询热门博客
         *
         * @param current
         * @return
         */
        @Override
        public Result queryHotBlog(Integer current) {
            // 根据用户查询
            Page<Blog> page = this.query()
                    .orderByDesc("liked")
                    .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
            // 获取当前页数据
            List<Blog> records = page.getRecords();
            // 查询用户
            records.forEach(this::queryUserByBlog);
            return Result.ok(records);
        }
    
        /**
         * 查询博客相关用户信息
         * @param blog
         */
        private void queryUserByBlog(Blog blog) {
            Long userId = blog.getUserId();
            User user = userService.getById(userId);
            blog.setName(user.getNickName());
            blog.setIcon(user.getIcon());
        }
    
    

2.点赞与排行探店笔记:

2.1.点赞探店笔记:

a.接口说明:

  • 1.在首页的探店笔记的排行榜和探店图文详情页面都有点赞的功能
    在这里插入图片描述

b.问题分析:

  • 1.现在存在一个问题,一个用户可以无限点赞,这显然是不合理的,所以我们需要对点赞功能进行一个优化,实现一人只能点赞一次
  • 2.对于点赞这种高频变化的数据,如果我们使用MySQL是十分不理智的,因为MySQL慢、并且并发请求MySQL会影响其它重要业务,容易影响整个系统的性能,继而降低了用户体验。所以选择使用Redis,然后推荐使用Set类型,因为Set类型的数据结构具有
    • 不重复,符合业务的特点,一个用户只能点赞一次
    • 高性能,Set集合内部实现了高效的数据结构(Hash表)
    • 灵活性,Set集合可以实现一对多,一个用户可以点赞多个博客,符合实际的业务逻辑

当然也可以选择使用Hash(Hash占用空间比Set更小),如果想要点赞排序也可以选用Sorted Set

b.功能完善:

在这里插入图片描述

c.编码实现:

@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {

    @Resource
    private IUserService userService;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 根据id查询博客
     *
     * @param id
     * @return
     */
    @Override
    public Result queryBlogById(Long id) {
        // 查询博客信息
        Blog blog = this.getById(id);
        if (Objects.isNull(blog)) {
            return Result.fail("笔记不存在");
        }
        // 查询blog相关的用户信息
        queryUserByBlog(blog);
        // 判断当前用户是否点赞该博客
        isBlogLiked(blog);
        return Result.ok(blog);
    }

    /**
     * 判断当前用户是否点赞该博客
     */
    private void isBlogLiked(Blog blog) {
        Long userId = ThreadLocalUtls.getUser().getId();
        String key = BLOG_LIKED_KEY + blog.getId();
        Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
        blog.setIsLike(BooleanUtil.isTrue(isMember));
    }

    /**
     * 查询热门博客
     *
     * @param current
     * @return
     */
    @Override
    public Result queryHotBlog(Integer current) {
        // 根据用户查询
        Page<Blog> page = this.query()
                .orderByDesc("liked")
                .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
        // 获取当前页数据
        List<Blog> records = page.getRecords();
        // 查询用户
        records.forEach(blog -> {
            this.queryUserByBlog(blog);
            this.isBlogLiked(blog);
        });
        return Result.ok(records);
    }

    /**
     * 点赞
     *
     * @param id
     * @return
     */
    @Override
    public Result likeBlog(Long id) {
        // 判断用户是否点赞
        Long userId = ThreadLocalUtls.getUser().getId();
        String key = BLOG_LIKED_KEY + blog.getId();
        // sismember key value
        Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
        boolean result;
        if (BooleanUtil.isFalse(isMember)) {
            // 用户未点赞,点赞数+1
            result = this.update(new LambdaUpdateWrapper<Blog>()
                    .eq(Blog::getId, id)
                    .setSql("liked = liked + 1"));
            if (result) {
                // 数据库更新成功,更新缓存  sadd key value
                stringRedisTemplate.opsForSet().add(key, userId.toString());
            }
        } else {
            // 用户已点赞,点赞数-1
            result = this.update(new LambdaUpdateWrapper<Blog>()
                    .eq(Blog::getId, id)
                    .setSql("liked = liked - 1"));
            if (result) {
                // 数据更新成功,更新缓存 srem key value
                stringRedisTemplate.opsForSet().remove(key, userId.toString());
            }
        }
        return Result.ok();
    }

    /**
     * 查询博客相关用户信息
     *
     * @param blog
     */
    private void queryUserByBlog(Blog blog) {
        Long userId = blog.getUserId();
        User user = userService.getById(userId);
        blog.setName(user.getNickName());
        blog.setIcon(user.getIcon());
    }
}

在这里插入图片描述


2.2.点赞排行榜笔记:

a.接口说明:

  • 1.在探店笔记详情的页面,应该把该笔记点赞的人数显示出来,形成点赞排行榜
    在这里插入图片描述

b.需求说明:

  • 1.返回点赞排名前5的用户:
    在这里插入图片描述

c.数据类型:

  • 1.SortedSet实现点赞排行榜
  • 1.在平常我们所使用的软件中(比如微信、QQ、抖音)的点赞功能都会默认按照时间顺序对点赞的用户进行一个排序,后点赞的用户会排在最前面,而Set是无序的,无法满足这个需求,虽然 List有序,但是不唯一,查找效率也比较低,所以也不推荐使用,此时我们就可以选择使用SortedSet这个数据结构,它完美的满足了我们所有的需求:唯一、有序、查找效率高
  • 2.相较于Set集合,SortedList有以下不同之处
    • 对于Set集合我们可以使用 isMember方法判断用户是否存在,对于SortedList我们可以使用ZSCORE方法判断用户是否存在
    • Set集合没有提供范围查询,无法获排行榜前几名的数据,SortedList可以使用ZRANGE方法实现范围查询

d.点赞逻辑改善:

  • 1.由于Redis中存储点赞信息的数据类型发生变化,所以先修改点赞逻辑:
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {

    @Resource
    private IUserService userService;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 根据id查询博客
     *
     * @param id
     * @return
     */
    @Override
    public Result queryBlogById(Long id) {
        // 查询博客信息
        Blog blog = this.getById(id);
        if (Objects.isNull(blog)) {
            return Result.fail("笔记不存在");
        }
        // 查询blog相关的用户信息
        queryUserByBlog(blog);
        // 判断当前用户是否点赞该博客
        isBlogLiked(blog);
        return Result.ok(blog);
    }

    /**
     * 判断当前用户是否点赞该博客
     */
    private void isBlogLiked(Blog blog) {
        UserDTO user = ThreadLocalUtls.getUser();
        if (Objects.isNull(user)){
            // 当前用户未登录,无需查询点赞
            return;
        }
        Long userId = user.getId();
        String key = BLOG_LIKED_KEY + blog.getId();
        Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
        blog.setIsLike(Objects.nonNull(score));
    }

    /**
     * 查询热门博客
     *
     * @param current
     * @return
     */
    @Override
    public Result queryHotBlog(Integer current) {
        // 根据用户查询
        Page<Blog> page = this.query()
                .orderByDesc("liked")
                .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
        // 获取当前页数据
        List<Blog> records = page.getRecords();
        // 查询用户
        records.forEach(blog -> {
            this.queryUserByBlog(blog);
            this.isBlogLiked(blog);
        });
        return Result.ok(records);
    }

    /**
     * 点赞
     *
     * @param id
     * @return
     */
    @Override
    public Result likeBlog(Long id) {
        // 1、判断用户是否点赞
        Long userId = ThreadLocalUtls.getUser().getId();
        String key = BLOG_LIKED_KEY + id;
        // zscore key value
        Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
        boolean result;
        if (score == null) {
            // 1.1 用户未点赞,点赞数+1
            result = this.update(new LambdaUpdateWrapper<Blog>()
                    .eq(Blog::getId, id)
                    .setSql("liked = liked + 1"));
            if (result) {
                // 数据库更新成功,更新缓存 zadd key value score
                stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
            }
        } else {
            // 1.2 用户已点赞,点赞数-1
            result = this.update(new LambdaUpdateWrapper<Blog>()
                    .eq(Blog::getId, id)
                    .setSql("liked = liked - 1"));
            if (result) {
                // 数据更新成功,更新缓存 zrem key value
                stringRedisTemplate.opsForZSet().remove(key, userId.toString());
            }
        }
        return Result.ok();
    }

    /**
     * 查询所有点赞博客的用户
     *
     * @param id
     * @return
     */
    @Override
    public Result queryBlogLikes(Long id) {
        // 查询Top5的点赞用户 zrange key 0 4
        Long userId = ThreadLocalUtls.getUser().getId();
        String key = BLOG_LIKED_KEY + id;
        Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
        if (top5 == null || top5.isEmpty()) {
            return Result.ok(Collections.emptyList());
        }
        List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
        List<UserDTO> userDTOList = userService.listByIds(ids).stream()
                .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
                .collect(Collectors.toList());
        return Result.ok(userDTOList);
    }

    /**
     * 查询博客相关用户信息
     *
     * @param blog
     */
    private void queryUserByBlog(Blog blog) {
        Long userId = blog.getUserId();
        User user = userService.getById(userId);
        blog.setName(user.getNickName());
        blog.setIcon(user.getIcon());
    }
}

在这里插入图片描述

e.新的问题说明:

我们点赞后,发现先点赞的那个人的头衔排在了后面,这是不符合逻辑的

  • 如果我们的需求是:先点赞的排在前面,后点赞的排在后面该如何实现?这就需要涉及到MySQL的一些相关知识了,在MySQL中如果我们使用in进行条件查询,我们的查询默认是数据库顺序查询,数据库中的记录默认都是按照ID自增的,所以查出来的结果默认是按照ID自增排序的
    在这里插入图片描述
  • 2.解决办法:在查询的时候,指定按照某个字段的参数的顺序进行查询
select id, phone,password,nick_name,icon,create_time,update_time
from tb_user
where id in(1, 5)
order by field(id, 5, 1)

在这里插入图片描述

f.在编码中进行改善:

    /**
     * 查询所有点赞博客的用户
     *
     * @param id
     * @return
     */
    @Override
    public Result queryBlogLikes(Long id) {
        // 查询Top5的点赞用户 zrange key 0 4
        Long userId = ThreadLocalUtls.getUser().getId();
        String key = BLOG_LIKED_KEY + id;
        Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
        if (top5 == null || top5.isEmpty()) {
            return Result.ok(Collections.emptyList());
        }
        List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
        String idStr = StrUtil.join(",", ids);
        // 根据id降序排序 select * from tb_user where id in(1,5) order by field(id, 1, 5)
        List<UserDTO> userDTOList = userService.list(new LambdaQueryWrapper<User>()
                        .in(User::getId, ids)
                        .last("order by field (id," + idStr + ")"))
                .stream()
                .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
                .collect(Collectors.toList());
        return Result.ok(userDTOList);
    }

  • 2.点赞查询的结果:可以看出是按照顶赞的时间顺序进行排序的了
    在这里插入图片描述

3.好友关注:

3.1.关注和取关:

a.接口说明:

在这里插入图片描述
在这里插入图片描述

b.编码实现:

  • 1.Controller层接口:
@RestController
@RequestMapping("/follow")
public class FollowController {

    @Resource
    private IFollowService followService;

    /**
     * 关注用户
     * @param followUserId 关注用户的id
     * @param isFollow 是否已关注
     * @return
     */
    @PutMapping("/{id}/{isFollow}")
    public Result follow(@PathVariable("id") Long followUserId, @PathVariable Boolean isFollow){
        return followService.follow(followUserId, isFollow);
    }

    /**
     * 是否关注用户
     * @param followUserId 关注用户的id
     * @return
     */
    @GetMapping("/or/not/{id}")
    public Result isFollow(@PathVariable("id") Long followUserId){
        return followService.isFollow(followUserId);
    }
}

  • 2.Service层代码:
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {

    /**
     * 关注用户
     *
     * @param followUserId 关注用户的id
     * @param isFollow     是否已关注
     * @return
     */
    @Override
    public Result follow(Long followUserId, Boolean isFollow) {
        Long userId = ThreadLocalUtls.getUser().getId();
        if (isFollow) {
            // 用户为关注,则关注
            Follow follow = new Follow();
            follow.setUserId(userId);
            follow.setFollowUserId(followUserId);
            this.save(follow);
        } else {
            // 用户已关注,删除关注信息
            this.remove(new LambdaQueryWrapper<Follow>()
                    .eq(Follow::getUserId, userId)
                    .eq(Follow::getFollowUserId, followUserId));
        }
        return Result.ok();
    }

    /**
     * 是否关注用户
     *
     * @param followUserId 关注用户的id
     * @return
     */
    @Override
    public Result isFollow(Long followUserId) {
        Long userId = ThreadLocalUtls.getUser().getId();
        int count = this.count(new LambdaQueryWrapper<Follow>()
                .eq(Follow::getUserId, userId)
                .eq(Follow::getFollowUserId, followUserId));
        return Result.ok(count > 0);
    }
}

  • 3.页面上的关注测试:
    在这里插入图片描述

3.2.共同关注:

a.接口说明:

在这里插入图片描述
在这里插入图片描述

b.编码实现:

  • 1.我们想要查询出两个用户的共同关注对象,这就需要使用求交集,对于求交集,我们可以使用Set集合
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private IUserService userService;

    /**
     * 关注用户
     *
     * @param followUserId 关注用户的id
     * @param isFollow     是否已关注
     * @return
     */
    @Override
    public Result follow(Long followUserId, Boolean isFollow) {
        Long userId = ThreadLocalUtls.getUser().getId();
        String key = FOLLOW_KEY + userId;
        if (isFollow) {
            // 用户为关注,则关注
            Follow follow = new Follow();
            follow.setUserId(userId);
            follow.setFollowUserId(followUserId);
            boolean isSuccess = this.save(follow);
            if (isSuccess) {
                // 用户关注信息保存成功,把关注的用户id放入Redis的Set集合中,
                stringRedisTemplate.opsForSet().add(key, followUserId.toString());
            }
        } else {
            // 用户已关注,删除关注信息
            boolean isSuccess = this.remove(new LambdaQueryWrapper<Follow>()
                    .eq(Follow::getUserId, userId)
                    .eq(Follow::getFollowUserId, followUserId));
            if (isSuccess) {
                stringRedisTemplate.opsForSet().remove(key, followUserId.toString());
            }
        }
        return Result.ok();
    }

    /**
     * 是否关注用户
     *
     * @param followUserId 关注用户的id
     * @return
     */
    @Override
    public Result isFollow(Long followUserId) {
        Long userId = ThreadLocalUtls.getUser().getId();
        int count = this.count(new LambdaQueryWrapper<Follow>()
                .eq(Follow::getUserId, userId)
                .eq(Follow::getFollowUserId, followUserId));
        return Result.ok(count > 0);
    }

    /**
     * 查询共同关注
     *
     * @param id
     * @return
     */
    @Override
    public Result followCommons(Long id) {
        Long userId = ThreadLocalUtls.getUser().getId();
        String key1 = FOLLOW_KEY + userId;
        String key2 = FOLLOW_KEY + id;
        // 查询当前用户与目标用户的共同关注对象
        Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2);
        if (Objects.isNull(intersect) || intersect.isEmpty()) {
            return Result.ok(Collections.emptyList());
        }
        List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
        // 查询共同关注的用户信息
        List<UserDTO> userDTOList = userService.listByIds(ids).stream()
                .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
                .collect(Collectors.toList());
        return Result.ok(userDTOList);
    }
}

在这里插入图片描述
在这里插入图片描述


3.3.Feed流关注推送

a.什么是Feed流?

  • 1.关注推送也叫做Feed流,直译为投喂。为用户持续的提供“沉浸式”的体验,通过无限下拉刷新获取新的信息。
  • 2.Feed流是一种基于用户个性化需求和兴趣的信息流推送方式,常见于社交媒体、新闻应用、音乐应用等互联网平台。
  • 3.Feed流通过算法和用户行为数据分析,动态地将用户感兴趣的内容以流式方式呈现在用户的界面上。
    在这里插入图片描述

b.Feed流产品有两种常见模式:

b1.时间排序(Timeline):
  • 1.不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈
    • 优点:信息全面,不会有缺失。并且实现也相对简单
    • 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
b2.智能排序:
  • 1.利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户
    • 优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
    • 缺点:如果算法不精准,可能起到反作用

c.时间排序(Timeline)的实现方式:

  • 1.本例中的个人页面,是基于关注的好友来做Feed流,因此采用Timeline的模式。该模式的实现方案有三种:
    在这里插入图片描述
c1.拉模式:

1.拉模式概念:

  • 1.拉模式也叫做读扩散。
  • 2.在拉模式中,终端用户或应用程序主动发送请求来获取最新的数据流。它是一种按需获取数据的方式,用户可以在需要时发出请求来获取新数据。
  • 3.在Feed流中,数据提供方将数据发布到实时数据源中,而终端用户或应用程序通过订阅或请求来获取新数据。

2.拉模式优点:

  • 1.节约空间,可以减少不必要的数据传输,只需要获取自己感兴趣的数据,因为赵六在读信息时,并没有重复读取,而且读取完之后可以把他的收件箱进行清楚。

3.拉模式缺点:

  • 1.延迟较高,当用户读取数据时才去关注的人里边去读取数据,假设用户关注了大量的用户,那么此时就会拉取海量的内容,对服务器压力巨大
    在这里插入图片描述
c2.推模式:

1.推模式概念:

  • 1.推模式也叫做写扩散。在推模式中,数据提供方主动将最新的数据推送给终端用户或应用程序。数据提供方会实时地将数据推送到终端用户或应用程序,而无需等待请求。

2.推模式优点:

  • 1.优点:数据延迟低,不用临时拉取

2.推模式缺点:

  • 1.内存耗费大,假设一个大V写信息,很多人关注他, 就会写很多份数据到粉丝那边去
    在这里插入图片描述
c3.推拉结合:

定义:

  • 1.也叫做读写混合,兼具推和拉两种模式的优点。
  • 2.在推拉结合模式中,数据提供方会主动将最新的数据推送给终端用户或应用程序,同时也支持用户通过拉取的方式来获取数据。这样可以实现实时的数据更新,并且用户也具有按需获取数据的能力。
  • 3.推拉模式是一个折中的方案,站在发件人这一段:
    • 如果是个普通的人,那么我们采用写扩散的方式,直接把数据写入到他的粉丝中去,因为普通的人他的粉丝关注量比较小,所以这样做没有压力
    • 如果是大V,那么他是直接将数据先写入到一份到发件箱里边去,然后再直接写一份到活跃粉丝收件箱里边去
  • 4.现在站在收件人这端来看:
    • 如果是活跃粉丝,那么大V和普通的人发的都会直接写入到自己收件箱里边来
    • 而如果是普通的粉丝,由于他们上线不是很频繁,所以等他们上线时,再从发件箱里边去拉信息
      在这里插入图片描述

d.本案例模式选择:

在这里插入图片描述

  • 1.当前项目用户量比较小,所以这里我们选择使用推模式,延迟低、内存占比也没那么大
  • 2.由于我们需要实现分页查询功能,这里我们可以选择 list 或者 SortedSet,而不能使用Set,因为Set是无序的, list是有索引的,SortedSet 是有序的,那么我们该如何选择呢?
  • 3.如果我们选择 list 会存在索引漂移现象(这个在Vue中也存在),从而导致读取重复数据,所以我们不能选择使用 list
    在这里插入图片描述
  • 4.我们可以选择使用滚动分页,我们使用SortedSet,如果使用排名和使用角标是一样的,但是SortedSet可以按照Score排序(Score默认按照时间戳生成,所以是固定的),每次我们可以选择比之前Score较小的,这样就能够实现滚动排序,从而防止出现问题
    在这里插入图片描述

e.编码实现:

  • 1.代码实现:在BlogServiceImpl中修改原有的保存探店笔记的方法:
    /**
     * 保存探店笔记
     *
     * @param blog
     * @return
     */
    @Override
    public Result saveBlog(Blog blog) {
        Long userId = ThreadLocalUtls.getUser().getId();
        blog.setUserId(userId);
        // 保存探店笔记
        boolean isSuccess = this.save(blog);
        if (!isSuccess){
            return Result.fail("笔记保存失败");
        }
        // 查询笔记作者的所有粉丝
        List<Follow> follows = followService.list(new LambdaQueryWrapper<Follow>()
                .eq(Follow::getFollowUserId, userId));
        // 将笔记推送给所有的粉丝
        for (Follow follow : follows) {
            // 获取粉丝的id
            Long id = follow.getUserId();
            // 推送笔记
            String key = FEED_KEY + id;
            stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
        }
        return Result.ok(blog.getId());
    }

3.4.实现关注推送页面的分页查询:

a.滚动分页查询收件箱的思路:

  • 1.Redis中的数据样例:
    在这里插入图片描述
  • 2.角标查询及其问题演示:
    在这里插入图片描述
  • 3.滚动查询演示:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

b.接口介绍:

在这里插入图片描述

b.编码实现:

    /**
     * 关注推送页面的笔记分页
     *
     * @param max
     * @param offset
     * @return
     */
    @Override
    public Result queryBlogOfFollow(Long max, Integer offset) {
        // 1、查询收件箱
        Long userId = ThreadLocalUtls.getUser().getId();
        String key = FEED_KEY + userId;
        // ZREVRANGEBYSCORE key Max Min LIMIT offset count
        Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
                .reverseRangeByScoreWithScores(key, 0, max, offset, 2);
        // 2、判断收件箱中是否有数据
        if (typedTuples == null || typedTuples.isEmpty()) {
            return Result.ok();
        }

        // 3、收件箱中有数据,则解析数据: blogId、minTime(时间戳)、offset
        List<Long> ids = new ArrayList<>(typedTuples.size());
        long minTime = 0; // 记录当前最小值
        int os = 1; // 偏移量offset,用来计数
        for (ZSetOperations.TypedTuple<String> tuple : typedTuples) { // 5 4 4 2 2
            // 获取id
            ids.add(Long.valueOf(tuple.getValue()));
            // 获取分数(时间戳)
            long time = tuple.getScore().longValue();
            if (time == minTime) {
                // 当前时间等于最小时间,偏移量+1
                os++;
            } else {
                // 当前时间不等于最小时间,重置
                minTime = time;
                os = 1;
            }
        }

        // 4、根据id查询blog(使用in查询的数据是默认按照id升序排序的,这里需要使用我们自己指定的顺序排序)
        String idStr = StrUtil.join(",", ids);
        List<Blog> blogs = this.list(new LambdaQueryWrapper<Blog>().in(Blog::getId, ids)
                .last("ORDER BY FIELD(id," + idStr + ")"));
        // 设置blog相关的用户数据,是否被点赞等属性值
        for (Blog blog : blogs) {
            // 查询blog有关的用户
            queryUserByBlog(blog);
            // 查询blog是否被点赞
            isBlogLiked(blog);
        }

        // 5、封装并返回
        ScrollResult scrollResult = new ScrollResult();
        scrollResult.setList(blogs);
        scrollResult.setOffset(os);
        scrollResult.setMinTime(minTime);

        return Result.ok(scrollResult);
    }

在这里插入图片描述
在这里插入图片描述

\


4. 附近商铺搜索

4.1.GEO数据结构

  • 1.GEO就是Geolocation的简写形式,代表地理坐标。Redis在3.2版本中加入了对GEO的支持,允许存储地理坐标信息,帮助我们根据经纬度来检索数据。常见的命令有:
    • GEOADD:添加一个地理空间信息,包含:经度(longitude)、纬度(latitude)、值(member)
    • GEODIST:计算指定的两个点之间的距离并返回
    • GEOHASH:将指定member的坐标转为hash字符串形式并返回
    • GEOPOS:返回指定member的坐标
    • GEORADIUS:指定圆心、半径,找到该圆内包含的所有member,并按照与圆心之间的距离排序后返回。6.2以后已废弃
    • GEOSEARCH:在指定范围内搜索member,并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2.新功能
    • GEOSEARCHSTORE:与GEOSEARCH功能一致,不过可以把结果存储到一个指定的key。 6.2.新功能

4.2.练习:

在这里插入图片描述

# 添加坐标数据
GEOADD g1 116.378248 39.865275 bjnz 116.42803 39.903738 bjz 116.322287 39.893729 bjxz
# 计算北京西站到北京站的距离
GEODIST g1 bjnz bjxz km
# 搜索天安门附近10km内的所有火车站,并按照距离升序排序
GEOSEARCH g1 FROMLONLAT 116.397904 39.909005 BYRADIUS 10 km WITHDIST

在这里插入图片描述

  • 1.备注:
    • 一定要登录Redis,如果有密码一定要输入密码登录,否则添加数据会报错 unauthenticated multibulk length
    • GEODIST计算距离,默认的单位是米

4.3.附近商户搜索:

a.接口说明:

在这里插入图片描述

b.细节:

  • 1.GEO存储经度(longitude)和维度(latitude)还有值(member),为了节约内存,我们在memboer中值存储店铺id
  • 2.由于前端传来一个type参数,但是GEO没有type数据,所以我们按照商铺类型进行分组,类型相同的商户分为一组,以typeId作为key同时存入一个GEO集合中

c.编码实现:

  • 1.数据预热。将店铺数据按照 typeId 批量存入Redis
    /**
     * 预热店铺数据,按照typeId进行分组,用于实现附近商户搜索功能
     */
    @Test
    public void loadShopListToCache() {
        // 1、获取店铺数据
        List<Shop> shopList = shopService.list();
        // 2、根据 typeId 进行分类
//        Map<Long, List<Shop>> shopMap = new HashMap<>();
//        for (Shop shop : shopList) {
//            Long shopId = shop.getId();
//            if (shopMap.containsKey(shopId)){
//                // 已存在,添加到已有的集合中
//                shopMap.get(shopId).add(shop);
//            }else{
//                // 不存在,直接添加
//                shopMap.put(shopId, Arrays.asList(shop));
//            }
//        }
        // 使用 Lambda 表达式,更加优雅(优雅永不过时)
        Map<Long, List<Shop>> shopMap = shopList.stream()
                .collect(Collectors.groupingBy(Shop::getTypeId));

        // 3、将分好类的店铺数据写入redis
        for (Map.Entry<Long, List<Shop>> shopMapEntry : shopMap.entrySet()) {
            // 3.1 获取 typeId
            Long typeId = shopMapEntry.getKey();
            List<Shop> values = shopMapEntry.getValue();
            // 3.2 将同类型的店铺的写入同一个GEO ( GEOADD key 经度 维度 member )
            String key = SHOP_GEO_KEY + typeId;
            // 方式一:单个写入(这种方式,一个请求一个请求的发送,十分耗费资源,我们可以进行批量操作)
//            for (Shop shop : values) {
//                stringRedisTemplate.opsForGeo().add(key, new Point(shop.getX(), shop.getY()),
//                shop.getId().toString());
//            }
            // 方式二:批量写入
            List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>();
            for (Shop shop : values) {
               locations.add(new RedisGeoCommands.GeoLocation<>(shop.getId().toString(),
                       new Point(shop.getX(), shop.getY())));
            }
            stringRedisTemplate.opsForGeo().add(key, locations);
        }
    }

  • 2.在ShopServiceImpl中编写查询代码:
@Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        // 1.判断是否需要根据坐标查询
        if (x == null || y == null) {
            // 不需要坐标查询,按数据库查询
            Page<Shop> page = query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            // 返回数据
            return Result.ok(page.getRecords());
        }

        // 2.计算分页参数
        int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE;

        // 3.查询redis、按照距离排序、分页。结果:shopId、distance
        String key = SHOP_GEO_KEY + typeId;
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo() // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
                .search(
                        key,
                        GeoReference.fromCoordinate(x, y),
                        new Distance(5000),
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
                );
        // 4.解析出id
        if (results == null) {
            return Result.ok(Collections.emptyList());
        }
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
        if (list.size() <= from) {
            // 没有下一页了,结束
            return Result.ok(Collections.emptyList());
        }
        // 4.1.截取 from ~ end的部分
        List<Long> ids = new ArrayList<>(list.size());
        Map<String, Distance> distanceMap = new HashMap<>(list.size());
        list.stream().skip(from).forEach(result -> {
            // 4.2.获取店铺id
            String shopIdStr = result.getContent().getName();
            ids.add(Long.valueOf(shopIdStr));
            // 4.3.获取距离
            Distance distance = result.getDistance();
            distanceMap.put(shopIdStr, distance);
        });
        // 5.根据id查询Shop
        String idStr = StrUtil.join(",", ids);
        List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
        for (Shop shop : shops) {
            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        }
        // 6.返回
        return Result.ok(shops);
    }

  • 3.注意:还需要再Controller层中新增两个参数,经度x和纬度y

在这里插入图片描述

  • 4.备注:前端数据是写死的,无论是哪一个用户,发送的地理坐标都是一样的,数据库中的店铺数据的坐标也是也是假的
    在这里插入图片描述
  • 18
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值