redis的geo结构实现[附近商铺]功能

发布于:2025-04-04 ⋅ 阅读:(28) ⋅ 点赞:(0)

先上结论

geo地理位置算出来是不准的

实现思路

redis6.2+支持了经纬度数据格式 支持经纬度检索 需要将redis升级 否则会报错不支持命令
pom文件如果spring-data-redis是2.7.9的boot版本则要改一下支持geo:

<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>

redis的geo数据结构本质是一个zset有序集合 key就是坐标对应的业务数据 value就是坐标的hash值 在这里可以将店铺分类id做为一个数据集的key 然后里面存店铺id和经纬度的集合 查询时根据分类查出下面的店铺 然后使用geosearch命令去检索出范围内的店铺

相关的redis命令

用于添加多个坐标 GEOADD
GEOADD key longitude latitude member [longitude latitude member ...]
用于根据经纬度检索 并支持分页和测距 GEOSEARCH
GEOSEARCH key <FROMMEMBER member | FROMLONLAT longitude latitude>
  <BYRADIUS radius <M | KM | FT | MI> | BYBOX width height <M | KM |
  FT | MI>> [ASC | DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST]
  [WITHHASH]

店铺表结构:

CREATE TABLE `tb_shop` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '商铺名称',
  `type_id` bigint unsigned NOT NULL COMMENT '商铺类型的id',
  `images` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '商铺图片,多个图片以'',''隔开',
  `area` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '商圈,例如陆家嘴',
  `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '地址',
  `x` double unsigned NOT NULL COMMENT '经度',
  `y` double unsigned NOT NULL COMMENT '维度',
  `avg_price` bigint unsigned DEFAULT NULL COMMENT '均价,取整数',
  `sold` int(10) unsigned zerofill NOT NULL COMMENT '销量',
  `comments` int(10) unsigned zerofill NOT NULL COMMENT '评论数量',
  `score` int(2) unsigned zerofill NOT NULL COMMENT '评分,1~5分,乘10保存,避免小数',
  `open_hours` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '营业时间,例如 10:00-22:00',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `open_start_time` time DEFAULT NULL COMMENT '开始营业时间',
  `open_end_time` time DEFAULT NULL COMMENT '结束营业时间',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `foreign_key_type` (`type_id`) USING BTREE,
  KEY `idx_is_deleted` (`is_deleted`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT

先写个用例提前将店铺数据导入redis

@SpringBootTest
public class GEOLocationTest {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private ShopMapper shopMapper;

    @Test
    public void insertShop() {
        List<Shop> all = shopMapper.findAll();
        Map<Long, List<Shop>> typeMap = all.stream().collect(Collectors.groupingBy(Shop::getTypeId));
        Set<Map.Entry<Long, List<Shop>>> entries = typeMap.entrySet();

        for (Map.Entry<Long, List<Shop>> entry : entries) {
            String key = "shop:geo:" + entry.getKey();
            // 店铺id就是zset的值 店铺经纬度的hash就是分数
            List<RedisGeoCommands.GeoLocation<String>> geoLocations = entry.getValue()
                    .stream()
                    .map(shop -> new RedisGeoCommands.GeoLocation<>(shop.getId().toString(), new Point(shop.getX().doubleValue(), shop.getY().doubleValue()))).collect(Collectors.toList());
            stringRedisTemplate.opsForGeo().add(key, geoLocations);
        }
    }
}

跑完redis就有数据了:
在这里插入图片描述
接口入参:

@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){
    List<Shop> shopList = shopService.queryShopByType(typeId, current, x, y);
    return Result.ok(shopList);
}

service方法:

@Override
public List<Shop> queryShopByType(Integer typeId, Integer current, Double x, Double y) {
    if (Objects.isNull(x) || Objects.isNull(y)) {
        // 没有传x, y 传统分页
        PageHelper.startPage(current, MAX_PAGE_SIZE);
        Page<Shop> shops = shopMapper.findByType(typeId);
        return shops;
    }
    // 如传了x,y 使用GEOSEARCH进行检索
    String key = "shop:geo:" + typeId;
    // 计算分页起始量
    long from = (current - 1) * MAX_PAGE_SIZE;
    long to = current * MAX_PAGE_SIZE;
    // 查询xy坐标500米内的素有店铺并返回它们之间的距离
    GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = stringRedisTemplate.opsForGeo().search(
            key,
            GeoReference.fromCoordinate(x, y),
            new Distance(500), // 默认单位是米
            RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(to) // 返回包含距离值
    );
    List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = geoResults.getContent();
    // 如果数据量不足起始值说明没有数据了 直接返回空
    if (content.size() <= from) {
        return Collections.emptyList();
    }
    // 拿到所有店铺id去查询店铺数据并设置距离值
    List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = content.stream().skip(from).collect(Collectors.toList());
    HashMap<Long, BigDecimal> distanceMap = new HashMap<>();
    for (GeoResult<RedisGeoCommands.GeoLocation<String>> geoLocationGeoResult : list) {
        String id = geoLocationGeoResult.getContent().getName(); // 获取存的店铺id
        double distance = geoLocationGeoResult.getDistance().getValue(); // 距离
        distanceMap.put(Long.valueOf(id), new BigDecimal(distance).setScale(2, BigDecimal.ROUND_HALF_UP));
    }
    List<Shop> shops = shopMapper.findByIds(distanceMap.keySet());
    for (Shop shop : shops) {
        shop.setDistance(distanceMap.get(shop.getId()));
    }
    return shops;
}

调用接口查到一条匹配的数据:
在这里插入图片描述
实际上我用我周围的坐标测 距离都是不准的 这个geo的数据存入和取出是不一致的会有误差