附近商铺
大约 5 分钟项目实战Redis项目实战
01.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.新功能。
不同的版本命令可能有所不同,下面是Redis 5.0 windows的命令:

02.导入店铺数据到GEO
按照商户类型做分组,类型相同的商户作为同一组,以typeId为key存入同一个GEO集合中即可。
@Test
public void loadShopData(){
// 1.查询店铺信息
List<Shop> list = shopService.list();
// 2.店铺按typeId分组
Map<Long, List<Shop>> map = list
.stream()
.collect(Collectors.groupingBy(Shop::getTypeId));
// 3.分批完成写入Redis
for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {
// 3.1获取类型id
Long typeId = entry.getKey();
String key = RedisConstants.SHOP_GEO_KEY + typeId;
// 3.2获取同类型的店铺集合
List<Shop> shopList = entry.getValue();
List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(shopList.size());
// 3.3写入Redis
for (Shop shop : shopList) {
// stringRedisTemplate.opsForGeo().add(key, new Point(shop.getX(), shop.getY()), shop.getId().toString());
locations.add(new RedisGeoCommands.GeoLocation<>(
shop.getId().toString(),
new Point(shop.getX(), shop.getY())));
}
stringRedisTemplate.opsForGeo().add(key, locations);
}
}
查看Redis中的数据:

03.实现附近商户功能
接口设计
完整请求URL:http://localhost:8080/api/shop/of/type?&typeId=1¤t=1&x=120.149993&y=30.334229
请求地址:
/shop/of/type
请求方式:
GET
请求参数
参数 说明 typeId 商户类型id current 页码 x 商户位置:经度 y 商户位置:维度 响应格式
{ "success": true, "data": [ { "id": 2, "name": "蔡馬洪涛烤肉·老北京铜锅涮羊肉", "typeId": 1, "images": "https://p0.meituan.net/bbia/c1870d570e73accbc9fee90b48faca41195272.jpg,http://p0.meituan.net/mogu/397e40c28fc87715b3d5435710a9f88d706914.jpg,https://qcloud.dpfile.com/pc/MZTdRDqCZdbPDUO0Hk6lZENRKzpKRF7kavrkEI99OxqBZTzPfIxa5E33gBfGouhFuzFvxlbkWx5uwqY2qcjixFEuLYk00OmSS1IdNpm8K8sG4JN9RIm2mTKcbLtc2o2vmIU_8ZGOT1OjpJmLxG6urQ.jpg", "area": "拱宸桥/上塘", "address": "上塘路1035号(中国工商银行旁)", "x": 120.151505, "y": 30.333422, "avgPrice": 85, "sold": 2160, "comments": 1460, "score": 46, "openHours": "11:30-03:00", "createTime": "2021-12-22T19:00:13", "updateTime": "2022-01-11T16:12:26", "distance": 170.4498 }, { "id": 9, "name": "羊老三羊蝎子牛仔排北派炭火锅(运河上街店)", "typeId": 1, "images": "https://p0.meituan.net/biztone/163160492_1624251899456.jpeg,https://img.meituan.net/msmerchant/e478eb16f7e31a7f8b29b5e3bab6de205500837.jpg,https://img.meituan.net/msmerchant/6173eb1d18b9d70ace7fdb3f2dd939662884857.jpg", "area": "运河上街", "address": "台州路2号运河上街购物中心F5", "x": 120.150598, "y": 30.325251, "avgPrice": 101, "sold": 2763, "comments": 1363, "score": 44, "openHours": "11:00-21:30", "createTime": "2021-12-22T19:53:59", "updateTime": "2022-01-11T16:13:34", "distance": 1000.3119 }, { "id": 8, "name": "浅草屋寿司(运河上街店)", "typeId": 1, "images": "https://img.meituan.net/msmerchant/cf3dff697bf7f6e11f4b79c4e7d989e4591290.jpg,https://img.meituan.net/msmerchant/0b463f545355c8d8f021eb2987dcd0c8567811.jpg,https://img.meituan.net/msmerchant/c3c2516939efaf36c4ccc64b0e629fad587907.jpg", "area": "运河上街", "address": "拱墅区金华路80号运河上街B1", "x": 120.150526, "y": 30.325231, "avgPrice": 88, "sold": 2406, "comments": 1206, "score": 46, "openHours": " 11:00-21:30", "createTime": "2021-12-22T19:51:06", "updateTime": "2022-01-11T16:13:25", "distance": 1002.1991 }, { "id": 3, "name": "新白鹿餐厅(运河上街店)", "typeId": 1, "images": "https://p0.meituan.net/biztone/694233_1619500156517.jpeg,https://img.meituan.net/msmerchant/876ca8983f7395556eda9ceb064e6bc51840883.png,https://img.meituan.net/msmerchant/86a76ed53c28eff709a36099aefe28b51554088.png", "area": "运河上街", "address": "台州路2号运河上街购物中心F5", "x": 120.151954, "y": 30.32497, "avgPrice": 61, "sold": 12035, "comments": 8045, "score": 47, "openHours": "10:30-21:00", "createTime": "2021-12-22T19:10:05", "updateTime": "2022-01-11T16:12:42", "distance": 1046.9829 }, { "id": 6, "name": "幸福里老北京涮锅(丝联店)", "typeId": 1, "images": "https://img.meituan.net/msmerchant/e71a2d0d693b3033c15522c43e03f09198239.jpg,https://img.meituan.net/msmerchant/9f8a966d60ffba00daf35458522273ca658239.jpg,https://img.meituan.net/msmerchant/ef9ca5ef6c05d381946fe4a9aa7d9808554502.jpg", "area": "拱宸桥/上塘", "address": "金华南路189号丝联166号", "x": 120.148603, "y": 30.318618, "avgPrice": 130, "sold": 9531, "comments": 7324, "score": 46, "openHours": "11:00-13:50,17:00-20:50", "createTime": "2021-12-22T19:24:53", "updateTime": "2022-01-11T16:13:09", "distance": 1741.5767 } ] }
实现
ShopController
/**
* 根据商铺类型分页查询商铺信息
* @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);
}
/**
* 根据店铺类型和地理坐标分页查询店铺信息
*
* @param typeId 店铺类型id
* @param current 页码
* @param x x
* @param y y
* @return R
*/
@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 = RedisConstants.SHOP_GEO_KEY + typeId;
GeoResults<RedisGeoCommands.GeoLocation<String>> results =
stringRedisTemplate.opsForGeo().radius(key,
new Circle(new Point(x, y), new Distance(5000, RedisGeoCommands.DistanceUnit.METERS)),
RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().limit(end));
if (null == results){
return Result.ok(Collections.emptyList());
}
// 4.解析id
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);
}
结果:
