如何使用Redis实现附近商家查询

导读

在日常生活中,我们经常能看见查询附近商家的功能。

常见的场景有,比如你在点外卖的时候,就可能需要按照距离查询附近几百米或者几公里的商家。

本文将介绍如何使用Redis实现按照距离查询附近商户的功能,并以SpringBoot项目作为举例。

想知道这样的功能是如何实现的吗?接着往下看吧!

Redis地理位置功能

Redis是一种高性能的键值存储数据库,具有快速读写能力和丰富的数据结构支持。在Redis 3.2版本之后,它引入了地理位置(Geospatial)功能,使其可以轻松处理与地理位置相关的数据。

地理位置功能的核心数据结构是有序集合(Sorted Set),它将元素与分数(score)关联起来。在地理位置功能中,分数表示地理位置的经度和纬度,而元素则是一个标识符,比如商户的ID。

我们只需要在数据库中存储商家的经纬度,以商家id作为key,经纬度作为value存入redis中,就可以通过redis命令来获得以某一个点为圆心一定范围内的商家,以及他们之间的距离。

常用命令

1. GEOADD:将地理位置添加到有序集合中

使用GEOADD命令,可以将一个或多个地理位置添加到有序集合中。语法如下:

复制代码
GEOADD key longitude latitude member [longitude latitude member ...]

示例:
   GEOADD stores 116.404 39.915 "storeA"
   GEOADD stores 116.418 39.917 "storeB"

2. GEODIST:计算两个位置之间的距离

GEODIST命令用于计算两个位置之间的距离,可以指定单位(米、千米、英里、英尺等)。

复制代码
   GEODIST key member1 member2 [unit]

   示例:

   GEODIST stores storeA storeB km

3. GEORADIUS:按照距离查询位置范围内的元素

GEORADIUS命令用于在指定的地理位置范围内查询元素。它可以按照经纬度坐标和半径来查询,还可以限制返回的结果数量。

复制代码
 GEORADIUS key longitude latitude radius unit [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key]

   示例:
 
   GEORADIUS stores 116.408 39.916 1 km WITHDIST COUNT 5

4. GEOHASH:获取位置的geohash值

GEOHASH命令用于获取指定位置的geohash值,geohash是一种将地理位置编码成字符串的方法,可以用于快速近似的位置计算。

复制代码
 GEOHASH key member [member ...]

   示例:

   GEOHASH stores storeA storeB

5. GEOPOS:获取一个或多个位置的经纬度坐标

GEOPOS命令用于获取一个或多个位置的经纬度坐标。

复制代码
   GEOPOS key member [member ...]

   示例:

   GEOPOS stores storeA storeB

6. GEORADIUSBYMEMBER:根据成员获取范围内的元素

这个命令与GEORADIUS类似,但是它以一个已有的成员作为中心点进行查询。

复制代码
   GEORADIUSBYMEMBER key member radius unit [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key]
  
   示例:
   
   GEORADIUSBYMEMBER stores storeA 1 km

地理位置功能不仅在查询附近商户等实际应用中非常有用,还可以应用于地理分析、位置推荐等领域。它通过利用Redis强大的有序集合数据结构,使得处理地理信息变得高效、灵活,并且易于集成到现有的应用中。无论是构建LBS应用还是处理位置相关数据,Redis的地理位置功能都能为开发者提供强大的支持。

Java代码实现

将数据库中的商家经纬度存入redis

数据库中有一张商家表,其中有经度,纬度这两个字段。我们可以通过单元测试批量将这些商家的经纬度数据存入redis。key为商家id,value为经纬度。

java 复制代码
/**
     * 将数据库中的商户坐标添加到缓存
     */
    @Test
    void addShopGeo2Redis(){
        //获取商户集合
        List<Shop> list = shopService.list();
        //根据商户类型分类
        Map<Long, List<Shop>> collect = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
        for (Map.Entry<Long, List<Shop>> longListEntry : collect.entrySet()) {
            Long typeId = longListEntry.getKey();
            String key = "shop:geo:" + typeId;
            //获取商户经纬度
            List<Shop> shopList = longListEntry.getValue();
            List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(shopList.size());
            for (Shop shop : shopList) {
//                stringRedisTemplate.opsForGeo().add(key,new Point(shop.getX(),shop.getY()),shop.getId().toString());
                //先收集完所有商户的地理位置,再一次性添加到redis
                locations.add(new RedisGeoCommands.GeoLocation<>(shop.getId().toString(),new Point(shop.getX(),shop.getY())));
            }
            stringRedisTemplate.opsForGeo().add(key,locations);
        }
    }

接口类:queryShopByType(typeId,current,x,y)

定义一个根据商家类型查询所有商家的接口,如果前端传来的参数中携带该用户的经纬度,则代表需要根据距离查询附近商家。

java 复制代码
  /**
     * 根据商铺类型分页查询商铺信息
     * @param typeId 商铺类型
     * @param current 页码
     * @return 商铺列表
     */
    @GetMapping("/of/type")
    public Result queryShopByType(
            @RequestParam("typeId") Integer typeId,
            @RequestParam(value = "current", defaultValue = "1") Integer current,
            @RequestParam(value = "x", required = false) Double x,
            @RequestParam(value = "y", required = false) Double y
    ) {

        return shopService.queryShopByType(typeId, current, x, y);
    }

服务类: queryShopByType( typeId,current,x,y)

1.首先判断是否经纬度参数x和y是否为空

2.计算分页参数(redis无法分页,需要手动分页)

3.查询redis

4.获取商户id集合

5.根据商户id查询数据库

6.返回

java 复制代码
  @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()
                .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 -> {
            String shopIdStr = result.getContent().getName();
            ids.add(Long.valueOf(shopIdStr));
            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);
    }
}

注意点

1.redis查询的结果是从第1条到第end条,不能直接返回第begin条到第end条。

那么如何跳过begin前面的记录呢?

可以使用stream()流的skip()方法,skip()方法中指定参数begin,就会跳过前面的begin条记录。

2.通过redis获取的ids集合,再使用mybatis-plus使用query().in()进行查询时,会破坏数据顺序,如何解决?

手动指定顺序。在后面加上last("ORDER BY FIELD(id," + idStr + ")").list()。而idStr = StrUtil.join(",",ids);

相关推荐
程序员小凯24 分钟前
Spring Boot缓存机制详解
spring boot·后端·缓存
TiAmo zhang27 分钟前
SQL Server 2019实验 │ 数据库和表的创建、修改与删除
数据库·oracle
wxweven28 分钟前
校招面试官揭秘:我们到底在寻找什么样的技术人才?
java·面试·校招
陈陈爱java1 小时前
新知识点背诵
java
失散131 小时前
分布式专题——39 RocketMQ客户端编程模型
java·分布式·架构·rocketmq
泽02021 小时前
Linux之环境变量
java·linux·redis
disanleya1 小时前
MySQL默认密码不安全?如何首次登录并强化?
数据库·mysql·安全
花开富贵贼富贵1 小时前
MySQL 核心高级特性
运维·数据库·mysql
hello 早上好1 小时前
深入 Spring 依赖注入底层原理
数据库·sql·spring
API快乐传递者1 小时前
抓取淘宝商品详情商品数据API接口调用说明文档|获取淘宝商品价格主图数据等
数据库