先上结论
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的数据存入和取出是不一致的会有误差