一、GEO 数据结构的基本用法

1.1 GEO 数据结构

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 新功能

1.2 练习 Redis 的 GEO 功能

需求:
1、添加下面几条数据:

  • 北京南站(116.378248 39.865275)
  • 北京站(116.42803 39.903738)
  • 北京西站(116.322287 39.893729)

2、计算北京西站到北京站的距离
3、搜索天安门(116.397904 39.909005)附近 10km 内的所有火车站,并按照距离升序排序

GEOADD 命令
在这里插入图片描述
添加北京南站、北京站、北京西站的坐标数据
在这里插入图片描述
使用 RPM 查看存入的数据
在这里插入图片描述
可以看出 GEO 底层是基于 SortedSet 实现的,Redis 将地理坐标转换成了 score 值存入到了 SortedSet 中。

GEODIST 命令
在这里插入图片描述
计算北京西站到北京站的距离
在这里插入图片描述
GEORADIUS 命令(已废弃)
在这里插入图片描述
GEOSEARCH 命令
在这里插入图片描述
返回使用 GEOADD 向 SortedSet 中添加的 member,这些 member 位于给定形状指定的区域的边界内。该命令对GEORADIUS命令进行了扩展,除了支持在圆形区域内搜索外,它还支持在矩形区域内搜索。

应使用此命令代替已弃用的GEORADIUS和GEORADIUSBYMEMBER命令。

查询的中心点由以下强制选项之一提供:

FROMMEMBER: 使用给定的且存在于SortedSet中的 member 的位置。
FROMLONLAT:使用给定的 longitude 和 latitude 位置。

查询的形状由以下强制选项之一提供:
BYRADIUS: 类似GEORADIUS,根据给定的圆形区域内搜索 radius。
BYBOX:在轴对齐的矩形内搜索,由 height 和 width 确定。

该命令可以选择使用以下选项返回附加信息:
WITHDIST: 返回匹配项到指定中心点的距离。返回的距离单位与半径或高度和宽度参数指定的单位相同。
WITHCOORD: 返回匹配项的经度和纬度。
WITHHASH:以 52 位无符号整数的形式返回项目的原始 geohash 编码排序集分数。

默认情况下,匹配项未排序返回。要对它们进行排序,请使用以下两个选项之一:
ASC:相对于中心点,从最近到最远对返回的项目进行排序。
DESC:相对于中心点,从最远到最近对返回的项目进行排序。

默认返回所有匹配项。如果想要将结果限制为前 N 个匹配项,可以使用COUNT选项。使用ANY选项时,只要找到足够的匹配项,该命令就会返回。

搜索天安门(116.397904 39.909005)附近 10km 内的所有火车站,并按照距离升序排序
在这里插入图片描述
GEOPOS 和 GEOHASH 命令:
在这里插入图片描述

二、导入店铺数据到 GEO

在这里插入图片描述
按照商户类型做分组,类型相同的商户作为同一组,以 typeId 作为 key 存入同一个 GEO 集合中。
在这里插入图片描述
HmDianPingApplicationTests

@SpringBootTest
class HmDianPingApplicationTests {

    @Autowired
    private ShopServiceImpl shopService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void loadShopData(){
        // 1、查询店铺信息
        List<Shop> list = shopService.list();
        // 2、把店铺分组,按照 typeId 分组,typeId 一致的放到一个集合中
        Map<Long, List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
        // 3、分批完成写入 Redis
        for (Map.Entry<Long, List<Shop>> longListEntry : map.entrySet()) {
            Long typeId = longListEntry.getKey();
            List<Shop> value = longListEntry.getValue();
            List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(value.size());
            for (Shop shop : value) {
                locations.add(new RedisGeoCommands.GeoLocation<>(
                        shop.getId().toString(),
                        new Point(shop.getX(), shop.getY())
                ));
            }
            stringRedisTemplate.opsForGeo().add(RedisConstants.SHOP_GEO_KEY + typeId, locations);
        }

    }
}

三、实现附近商户功能

SpringDataRedis 的 2.3.9 版本并不支持 Redis6.2 提供的 GEOSEARCH 命令,因此我们需要提示版本,修改自己的 POM 文件。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <artifactId>spring-data-redis</artifactId>
            <groupId>org.springframework.data</groupId>
        </exclusion>
        <exclusion>
            <artifactId>lettuce-core</artifactId>
            <groupId>io.lettuce</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>2.6.2</version>
</dependency>
<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>6.1.6.RELEASE</version>
</dependency>

ShopController

@RestController
@RequestMapping("/shop")
public class ShopController {

    @Resource
    public IShopService shopService;

	/**
     * 根据商铺类型分页查询商铺信息
     * @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);
    }
}

IShopService

public interface IShopService extends IService<Shop> {
    Result queryShopByType(Integer typeId, Integer current, Double x, Double y);
}

ShopServiceImpl

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

	@Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        // 判断是否需要根据坐标查询
        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());
        }
        // 计算分页参数
        int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE;

        // 查询 Redis,按照距离排序、分页。
        GeoResults<RedisGeoCommands.GeoLocation<String>> search = stringRedisTemplate.opsForGeo().
                search(RedisConstants.SHOP_GEO_KEY + typeId,
                        GeoReference.fromCoordinate(x, y),
                        new Distance(5000),
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end));

        if(search == null){
            return Result.ok(Collections.emptyList());
        }

        // 查询 Redis,按照距离排序、分页
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = search.getContent();
        if(from >= content.size()){
            return Result.ok(Collections.emptyList());
        }

        List<Long> ids = new ArrayList<>(content.size());
        Map<String, Distance> distanceMap = new HashMap<>(content.size());
        // 截取 from ~ end 的部分
        content.stream().skip(from).forEach(result -> {
            // 获取店铺 id
            String shopIdStr = result.getContent().getName();
            ids.add(Long.valueOf(shopIdStr));
            // 获取距离
            Distance distance = result.getDistance();
            distanceMap.put(shopIdStr, distance);
        });
        String join = StrUtil.join(",", ids);
        // 根据 id 查询 shop
        List<Shop> shopList = query().in("id", ids).last("order by field(" + join + ")").list();

        for (Shop shop : shopList) {
           shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        }
        
        return Result.ok(shopList);
    }
}
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐