数据存储形式
Redis
按照商户类型做分组,类型相同的作为一组,以typeId为key存入同一个GEO集合中
Key |
Value |
Score |
shop:gep:美食 |
麻辣香锅 海底捞火锅 |
4054135079882095 4054135064014509 |
shop:geo:KTV |
星聚会KTV 开乐迪 KTV |
4054135063424376 4054134341336239 |
MySQL
CREATE TABLE `tb_shop` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(128) NOT NULL COMMENT '商铺名称', `type_id` bigint unsigned NOT NULL COMMENT '商铺类型的id', `images` varchar(1024) NOT NULL COMMENT '商铺图片,多个图片以'',''隔开', `area` varchar(128) DEFAULT NULL COMMENT '商圈,例如陆家嘴', `address` varchar(255) 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) 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 '更新时间', PRIMARY KEY (`id`) USING BTREE, KEY `foreign_key_type` (`type_id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=15 ROW_FORMAT=COMPACT;
|
数据准备
先将MySQL中的店铺信息存储到Redis中
@Test void loadShopData() { List<Shop> list = shopService.list(); Map<Long, List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId)); for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) { Long typeId = entry.getKey(); String key = "shop:geo:" + typeId; List<Shop> value = entry.getValue(); List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(); for (Shop shop : value) { locations.add(new RedisGeoCommands.GeoLocation<>(String.valueOf(shop.getId()), new Point(shop.getX(), shop.getY()))); } stringRedisTemplate.opsForGeo().add(key, locations); } }
|
实现附近商户功能
接口说明
|
说明 |
请求方式 |
GET |
请求路径 |
/shop/of/type |
请求参数 |
typeId:商户类型 current:页码,滑动刷新 x:经度 y:纬度 |
返回值 |
List:符合要求的商户信息 |
版本说明
SpringDataReids的2.3.9版本不支持Redis6.2提供的GEOSEARCH命令,因此需要修改版本。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <artifactId>lettuce-core</artifactId> <groupId>io.lettuce</groupId> </exclusion> <exclusion> <artifactId>spring-data-redis</artifactId> <groupId>org.springframework.data</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>
|
更多GEOSEARCH
操作见:
实体类
点击查看实体类
@Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("tb_shop") public class Shop implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO) private Long id;
private String name;
private Long typeId;
private String images;
private String area;
private String address;
private Double x;
private Double y;
private Long avgPrice;
private Integer sold;
private Integer comments;
private Integer score;
private String openHours;
private LocalDateTime createTime;
private LocalDateTime updateTime;
@TableField(exist = false) private Double distance; }
|
Controller
其中x, y可以不传入
@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> records = shopService.queryShopByType(typeId, current, x, y); return Result.ok(records); }
|
ServiceImpl
@Resource private StringRedisTemplate stringRedisTemplate;
public static final int DEFAULT_PAGE_SIZE = 5; public static final String SHOP_GEO_KEY = "shop:geo:";
@Override public List<Shop> 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, DEFAULT_PAGE_SIZE)); return page.getRecords(); } int from = (current - 1) * DEFAULT_PAGE_SIZE; int end = current * DEFAULT_PAGE_SIZE; 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)); if (results == null) { return Collections.emptyList(); } List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent(); if (list.size() <= from) { return Collections.emptyList(); } Map<String, Distance> distanceMap = new HashMap<>(list.size()); ArrayList<Long> ids = new ArrayList<>(); list.stream().skip(from).forEach(result -> { String shopIdStr = result.getContent().getName(); ids.add(Long.valueOf(shopIdStr)); Distance distance = result.getDistance(); distanceMap.put(shopIdStr, distance); }); 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()); } return shops; }
|
由于limit()
只能传入查询数量,所以需要做逻辑分页,使用stream().skip(from)
可以跳过from以及之前的。