Redis实战-附近的人实现的解决方案
1.GEO数据结构
1.1实现附近的人的数据结构
Redis提供的专用的数据结构来实现附近的人的操作,这也是企业的主流解决方案,建议使用这种解决方案。
GEO就是Redis提供的地理坐标计算的一个数据结构,可以很方便的计算出来两个地点的地理坐标,实现相应的功能。
1.2GEO数据结构
GEO就是Geolocation的简写形式,代表地理坐标。Redis在3.2版本中加入了对GEO的支持,允许存储地理坐标信息,帮助我们根据经纬度来检索数据。
要点:可以使用GEO来进行存储地理坐标信息(允许通过经纬度进行检索数据)
常见的命令:
GEOADD:添加一个地理空间信息,包含:经度(longitude),纬度(latitude),值(member)
GEODIST:计算指定两点的距离并返回(根据经纬度进行计算)
GEOHASH:将指定member的坐标转换为hash字符串形式并进行返回。
GEOPOS:返回指定member的坐标。
GEORADIUS:指定圆心,半径,找到该圆内包含的所有member,并按照与圆心之间的距离排序后返回(6.2之后已经废弃)
GEOSEARCH:在指定范围内搜索member,并按照与指定点之间的距离排序后返回。范围可以是圆形或者是矩形(6.2废弃GEORADIUS后新增的功能)】
GEOSEARCHSTORE:与GEOSEARCH的功能一致,不过GEOSEARCHSTORE可以将结果存储一个指定的key中,6.2增加的新功能。
1.3案例学习
1.3.1案例
北京南站(116.378248, 39.8)’
北京站(116.42803,39.903738)
北京西站(116.322287, 39.893729)
任务1:将这几个站点的地理位置坐标存储以GEO的形式存储到Redis中。
任务2:计算北京西站到北京站的距离。
任务3:搜索天安门(116.397904, 39.909005)附近10km内所有的火车站,并按照距离升序排序。
1.3.2完成任务1
需要使用GEO提供的GEOADD指令进行添加GEO数据。
Redis的GEOADD指令是通过一个key可以进行存储多个GEO数据(即一个key对应的是一个GEO数据列表),一个GEO数据是以()
GEOADD key 经度 纬度 member [经度 纬度 member...]
使用GEOADD进行对key为g1的GEO列表进行添加GEO数据·。
可以看到三个GEO数据已经添加成功了,Member是值,Score是Redis底层根据指定的经纬度进行计算出来的数据。
1.3.3完成任务2
使用GEODIST key member1 member2 [unit单位]进行计算一个GEOLIST中的两个member的经纬度计算出来的距离,可以通过unit进行指定单位,也可以不进行指定单位,默认单位是m。
可以发现,当不指定单位的时候,默认是m,也可以将unit指定为km,计算出来就是以千米为单位的数据:
1.3.4完成任务3
任务三是需要搜索以天安门为中心点,半径为五千米的圆范围内的数据。
命令操作如下:
GEOSEARCH key [FROMMEMBER member] [FROMLONLAT longitude latitude] [BYRADIUS radisu m|km|ft|mi] [BYBOX width height m|km|ft|mi] [ASC|DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST] [WITHHASH]
1.搜索中心有两种指定方式:
FROMMEMBER:从已经存在的key中读取经纬度。
FROMLONLAT:用户参数传入经纬度
2.搜索条件也有两种方式:
BYRADIUS:根据给定半径长度按圆形进行搜素,命令等同于使用GEORADIUS
BYBOX:根据给定width和height按照举行进行搜索,矩形是轴对称矩形。
3.更多可选参数
WITHCOORD 返回匹配的经纬度坐标。
WITHDIST:返回距离,距离单位按照radius或者height/width单位转换。
WITHHASH:但会GEOHASH计算的值(就是将经纬度进行计算为HASH值进行返回)
COUNT count:只返回count个元素。注意,这里的count是全部搜索完成才进行过滤的,也就是不同减少搜索的CPU消耗,但是返回的元素少,可以利用此来降低网络带宽的利用率(使用COUNT限制返回的数量,借此来进行降低网络IO的压力)
ASC|DESC:对满足条件的点进行按按照距离升序/降序排序。
使用GEOSEARCH进行查询指定的以北京天安门为中心的,10公里的车站。
1.3.5查看北京西的坐标数据
查看一个GEO集合中的GEO数据的经纬度的时候,要进行使用GEOPOS进行查看。
GEOPOS的使用命令如下:
GEOPOS key member [member...]
使用GEOPOS的时候可以一次返回一个member对应的经纬度,也可以一次返回多个member对应的经纬度:
1.3.6查看三个站点经纬度坐标数据对应的HASH数据
查看一个GEO集合中的GEO数据的经纬度对应的HASH数据的时候,要进行使用GEOHASH来进行他查看。
GEOHASH的使用命令如下:
GEOHASH key member [member...]
使用GEOHASH可以一次返回一个member对应的经纬度的HASH数据,也可以一次返回多个:
2.附近商户搜索的业务实现
2.1业务介绍
在首页中点击某个频道,即可看到频道下的商户:
查看频道下的商户的时候,要根据商户的经纬度坐标,以及前端提交上来的用户所在的经纬度坐标进行计算个人与商户的距离,并按照距离进行排名,升序返回。
要点:前端进行传递到后端经纬度坐标,后端去进行查询前端传上来的经纬度坐标与相关频道的餐厅数据的距离,进行一个距离排序后,按距离升序排序进行返回。
2.2将商户数据的经纬度信息导入到Redis中
业务主要就是频道的数据是按店铺的类型进行展示的,需要进行获取坐标信息的业务也是频道,所以需要店铺的经纬度数据按店铺类型type进行缓存,查询频道的时候,也是按店铺类型type进行查询的。
挑几个code中实现的比较好的部分说一下。
import com.hmdp.entity.Shop;
import com.hmdp.service.IShopService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;/*** 测试将频道中的商铺数据添加到Redis中* 以GEO的形式进行存储到数据库中*/
@SpringBootTest
public class TestLoadShopData {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate IShopService shopService;@Testvoid loadShopData() {// 1. 查看店铺信息List<Shop> list = shopService.list();// 2. 把店铺进行分组, 按照typeId进行分组, typeId一致的进行放入一个集合中Map<Long, List<Shop>> groupByTypeId = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));// 3. 分批将数据进行写入到Redis中for (Map.Entry<Long, List<Shop>> group : groupByTypeId.entrySet()) {// 3.1 获取改组的类型IDLong typeId = group.getKey();String key = "shop:geo:" + typeId;// 3.2 获取同类型的店铺的集合List<Shop> shopList = group.getValue();List<RedisGeoCommands.GeoLocation<String>> geoLocations = shopList.stream().map(shop -> new RedisGeoCommands.GeoLocation<>(shop.getName(), new Point(shop.getX(), shop.getY()))).collect(Collectors.toList());stringRedisTemplate.opsForGeo().add(key, geoLocations);}}}
2.2.1分组操作
使用Stream流进行分组操作,使用List集合的Stream流,将List集合按照一个分组规则,转变为一个Map<Group标识, List>,完成分组存储数据的操作。
当所有的店铺数据从数据库中查询回来后,使用list.stream().collect(Collectors.groupingBy(Shop::getTypeId)),进行根据Shop的TypeId进行分组转换为Map<Long, List<Shop>>进行返回。
要点:当我们要将List根据List中数据的类别特征转换为Map的时候,就可以使用stream流中提供的collect(Collectors.groupingBy(item => item中的字段/getter方法))进行转换,最终进行转换为Map。
// 2. 把店铺进行分组, 按照typeId进行分组, typeId一致的进行放入一个集合中
Map<Long, List<Shop>> groupByTypeId = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
2.2.2RedisKey的设计
根据业务的设计,要根据Shop的type进行区分,设置对应的GEO缓存,所以key中要涉及到shop的typeId。
设计为:shop:geo:shopTypeId作为Redis中的缓存数据的key。
String key = "shop:geo:" + typeId;
2.2.3将Shop数据以GEO数据的形式存储到Redis中
存储GEO数据的时候,要使用RedisTemplate.opsForGeo().add(key, GeoLocation对象/Iterator<GeoLocation>对象),也就是说可以一次性向一个GEO集合中添加单条数据/多条数据。
List<RedisGeoCommands.GeoLocation<String>> geoLocations = shopList.stream().map(shop -> new RedisGeoCommands.GeoLocation<>(shop.getName(), new Point(shop.getX(), shop.getY()))).collect(Collectors.toList());
stringRedisTemplate.opsForGeo().add(key, geoLocations);
GeoLocation中需要进行构造两个字段:Member和org.springframework.data.geo.Point(double x, double y),存储的是member数据和Point坐标数据。
2.3完成根据店铺类型查询数据(根据频道查询数据)
2.3.1目前现状
目前这个接口仅仅是根据类型(频道)分页查询店铺数据,并没有进行根据GEO存储的地理坐标位置进行查询数据排序。
/*** 根据商铺类型分页查询商铺信息* @param typeId 商铺类型* @param current 页码* @return 商铺列表*/
@GetMapping("/of/type")
public Result queryShopByType(@RequestParam("typeId") Integer typeId,@RequestParam(value = "current", defaultVqqqqqqqqqqqqqqqqalue = "1") Integer current
) {// 根据类型分页查询Page<Shop> page = shopService.query().eq("type_id", typeId).page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));// 返回数据return Result.ok(page.getRecords());
}
2.3.2排除依赖
目前进行使用spring-boot-starter-parent的版本是2.3.12.RELEASE版本:
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.12.RELEASE</version><relativePath/> <!-- lookup parent from repository -->
</parent>
其中内部进行集成的spring-boot-starter-data-redis版本也是2.3.12RELEASE版本的,这个版本的spring-boot-startter-data-redis是不支持GeoLocation中的GEOSEARCH功能的:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>2.3.12.RELEASE</version>
</dependency>
解决方法就是将当前SpringBoot进行集成的SpringDataRedis依赖进行排除,引入支持GEOSEARCH的SpringDataRedis版本(2.6.2版本)
排除当前SpringBoot集成的SpringDataRedis的依赖:
需要在spring-boot-starter-data-redis中进行使用exclusion进行排除spring-data-redis和io.letture的依赖(spring-data-redis和io.lettuce要配套使用)
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclusion><groupId>org.springframework.data</groupId><artifactId>spring-data-redis</artifactId></exclusion><exclusion><groupId>io.lettuce</groupId><artifactId>io.lettuce</artifactId></exclusion></exclusions>
</dependency>
2.3.3引入SpringDataRedis2.6.2
SpringDataRedis2.6.2版本是支持GEOSEARCH指令的使用的,所以可以引入SpringDataRedis2.6.2,如果要进行使用redis线程池lettuce,需要使用相应的配套版本6.1.6.RELEASE。
<!-- SpringDataRedis和Redis连接池 -->
<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>
更换了依赖之后就可以进行使用RedisTemplate.opsForGeo().search()方法了。
2.4完成附近商户搜索的功能
2.4.1Controller层和Service层接口的定义
Controller层的定义:
进行定义接收了四个参数:typeId(店铺类型ID),current(当前分页 => SpringDataRedis中的opsForGeo的search方法并没有实现相应的分页功能,地理位置的相关的分页查询需要自己实现),x(纬度),y(经度)。
x和y这两个参数均使用了@RequestParam(required = false)进行注解,标识该参数不是必须的。
/*** 根据商铺类型分页查询商铺信息* @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.queryShopByTypeAndGeo(typeId, current, x, y);
}
Service层接口的定义:
/*** 根据店铺位置和地理位置进行查询数据** @param typeId* @param current* @param x* @param y* @return*/
Result queryShopByTypeAndGeo(Integer typeId, Integer current, Double x, Double y);
Serivice层的实现比较复杂,进行分开进行分析:
2.4.2经纬度参数不全
当经纬度参数不全的时候,则直接使用分页查询即可,不要进行走redis中的GEO地理位置查询的功能。
page中接收一个com.baomidou.mybatisplus.extension.plugins.pagiination.Page的对象参数,这个Page对象在进行实例化的时候需要接收current(当前分页)和size(本页查询的数据数量)。
// 1.判断是否要进行结合地理位置进行查询
if (x == null || y == null) {Page<Shop> page = this.query().eq("typeId", typeId).page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));return Result.ok(page.getRecords());
}
2.4.3计算分页参数
计算分页参数是计算出从什么位置开始,到什么位置结束,其实是到end - 1的位置结束,后面也是这样做的。
// 2. 计算分页参数
int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
2.4.4从Redis中查询符合条件的GEO数据
从key对应的GEO列表中进行查询符合条件的GEO数据。
调用的是RedisTemplate.opsForGeo().search()方法进行检索附近的店铺位置,对应的就是Redis命令中的GEOSEARCH指令。
这里指定给GEOSEARCH的参数是,查询key对应的GEOLIST,使用GeoReference.fromCoordinate(x, y)进行指定中心点,直接传入Distance指定范围为圆形,半径为5km,使用RedisCeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeCoordinates().limit(end)指定查询的结果包含经纬度数据,并且限制网络IO仅往回返回end个数据(主要是用于分页查询数据)
没有指定查询数据的排序,默认就是以Distance升序进行返回的(默认顺序ASC)
// 3. 查询redis, 按照距离和分页进行查询。结果: shopId和distance。
String key = SHOP_GEO_KEY + typeId;
// 查询指定key中的GEO数据 使用指定x和y坐标进行查询 距离在5000m内 限定查询end个
GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo().search(key,GeoReference.fromCoordinate(x, y),new Distance(5000),RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeCoordinates().limit(end)
);
接下来先进行分析一下RedisTemplate.opsForGeo().search方法:
2.4.4.1最原始的Search接口
先进行分析Redis的原始指令:
Redis的原始GEOSEARCH指令,需要进行指定的是:1.复用key对应的GEOList中GEO数据的坐标/指定相应的坐标。2.使用Circle/Box进行规定查找范围的类型以及返回的Distance的单位。3.查询出来的数据根据distance进行升序/降序。4.限制返回多少条数据(查询出数据后,在CPU计算完需要返回什么数据后,再进行截取返回数据)。5.是否返回匹配到数据的经纬度坐标。6.是否返回匹配到数据的距离数据。7.距离数据是否以Hash的形式返回。
GEOSEARCH key [FROMMEMBER member] [FROMLONLAT longitude latitude] [BYRADIUS radisu m|km|ft|mi] [BYBOX width height m|km|ft|mi] [ASC|DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST] [WITHHASH]
再看SpringDataRedis中进行封装的原始接口:
里面接收四个参数:key,GeoReference,GeoShape,RedisGeoCommands。
简述一下四个参数的作用:
key => 从Redis中找到对应的GEOList
GeoReference => 指定使用GEOList中已经有的Member对应经纬度数据/传入一个经纬度数据进行使用。
GeoShape => 圆形范围/矩形范围。
RedisGeoCommands => 指定GEOSEARCH中其它的参数,例如:查询回去的数据的顺序问题(升序 or 降序),是否在网路传输时使用count限制查询数据的数量,受否携带坐标返回,是否携带距离返回,是否将返回的Distance距离数据转换为Hash数据进行返回。
@Nullable
GeoResults<RedisGeoCommands.GeoLocation<M>> search(K key, GeoReference<M> reference, GeoShape geoPredicate, RedisGeoCommands.GeoSearchCommandArgs args);
2.4.4.2GeoReference参数的封装
只看最核心的部分即可,GeoReference就是用来指定GEOSEARCH方法中作为中心点的经纬度数据。
GeoReference是一个接口,进行定义了一堆将拥有默认实现的静态方法:
1.fromMember => 从key对应的GRO列表中找出member为指定值的GEO数据的经纬度作为中心点坐标。
fromMember可以直接接收一个T类型的member数据,或者一个GeoLocation数据(GeoLocation中封装了Point对象和member数据),指定中心点。
static <T> GeoReference<T> fromMember(T member) {Assert.notNull(member, "Geoset member must not be null");return new GeoMemberReference<>(member);
}static <T> GeoReference<T> fromMember(RedisGeoCommands.GeoLocation<T> member) {Assert.notNull(member, "GeoLocation must not be null");return new GeoMemberReference<>(member.getName());
}
2.fromCircle => 允许指定一个有范围的圆作为中心点,向外扩展。
fromCircle可以接收一个Circle类型的对象作为参数,这个对象可以进行设置Point对象(经纬度)和radius(Distance类型,半径)作为中心点。
static <T> GeoReference<T> fromCircle(Circle within) {Assert.notNull(within, "Circle must not be null");return fromCoordinate(within.getCenter());
}
3.fomCoordinate => 允许指定具体的经纬度坐标作为中心点。
fromCoordinate可以接收三种那参数:
1.longitude(经度)和latitude(纬度),两者均是double类型的。
2.GeoLocation(内置Point字段和member字段)
3.Point(内置经度x和纬度y)
static <T> GeoReference<T> fromCoordinate(double longitude, double latitude) {return new GeoCoordinateReference<>(longitude, latitude);
}static <T> GeoReference<T> fromCoordinate(RedisGeoCommands.GeoLocation<?> location) {Assert.notNull(location, "GeoLocation must not be null");Assert.notNull(location.getPoint(), "GeoLocation point must not be null");return fromCoordinate(location.getPoint());
}static <T> GeoReference<T> fromCoordinate(Point point) {Assert.notNull(point, "Reference point must not be null");return fromCoordinate(point.getX(), point.getY());
}
2.4.4.3GeoShape参数的封装
GeoShape参数是被用来指定范围形状的,自从redis6.2开始,新增的GEOSEARCH开始支持以圆形/矩形作为范围形状。
GeoShape是一个接口,继承自Shape接口,Shape接口是一个高层抽象接口,继承了序列化的功能:
import java.io.Serializable;public interface Shape extends Serializable {
}
GeoShape主要进行定义了一些具有默认实现的static静态方法:
1.GeoShape.byRadius(Distance radius)这个方法进行指定以圆形作为一个范围时,圆形的半径时是多少。
static GeoShape byRadius(Distance radius) {return new RadiusShape(radius);
}
2.GeoShape.byBox(double width, double height, DistanceUnit distanceUnit)/GeoShape.byBox(BoundingBox boundingBox)
byBox的重载方法是一样的,第一个重载方法传递进来三个参数,主要利用三个参数进行构建BoudingBox对象。
GeoShape.byBox()方法主要就是指定以矩形作为一个范围的(指定矩形的width和height)
static GeoShape byBox(double width, double height, DistanceUnit distanceUnit) {return byBox(new BoundingBox(width, height, distanceUnit));
}static GeoShape byBox(BoundingBox boundingBox) {return new BoxShape(boundingBox);
}
2.4.4.4GeoSearchCommandArgs的封装
GEOSEARCH中除了最重要的中心点的指定和范围形状的指定,其它的一些参数封装在RedisGeoCommands.GeoSearchCommandArgs中。
RedisGeoCommands.GeoSearchCommandArgs中可以进行指定count参数(通过limit指定),sort参数(ASC升序,DESC降序),includeCoordinates(返回数据的时候是否携带includeCoordinate进行返回)
使用的时候很简单,调用RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs()构造出一个GeoSearchCommandArgs类型的对象,再进行链式调用limit,sort,includeCoordinate方法进行指定args参数即可。
RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().limit().sort().includeCoordinates();
2.4.4.5GeoOperation中封装的search默认实现
其实在GeoOperation接口中封装的search的默认实现都是简化了search方法的调用而已,参数还是哪些参数,只是简化了调用。
注意:接口中封装的字段,默认都是public static final 静态常量。
1.想要直接以一个有范围的圆作为中心点,可以使用search(K key,Circle within)这个重载方法:
@Nullable
default GeoResults<GeoLocation<M>> search(K key, Circle within) {return search(key, GeoReference.fromCircle(within), GeoShape.byRadius(within.getRadius()),GeoSearchCommandArgs.newGeoSearchArgs());
}
2.想要直接以一个经纬度坐标作为圆心,radius作为范围半径(范围形状为圆形),可以使用search(K key, GeoReference<M> reference, Distance radius)或者search(K key, GeoReference<M> reference, Distance radius, GeoSearchCommandArgs args)这两个重载方法,一个需要传入额外的args参数,一个不需要传入额外的args参数,按需选择即可:
@Nullable
default GeoResults<GeoLocation<M>> search(K key, GeoReference<M> reference, Distance radius) {return search(key, reference, radius, GeoSearchCommandArgs.newGeoSearchArgs());
}@Nullable
default GeoResults<GeoLocation<M>> search(K key, GeoReference<M> reference, Distance radius,GeoSearchCommandArgs args) {return search(key, reference, GeoShape.byRadius(radius), args);
}
3.和上面两个方法同理,想要以矩形作为范围形状时可以使用下面的两个方法:
@Nullable
default GeoResults<GeoLocation<M>> search(K key, GeoReference<M> reference,BoundingBox boundingBox) {return search(key, reference, boundingBox, GeoSearchCommandArgs.newGeoSearchArgs());
}@Nullable
default GeoResults<GeoLocation<M>> search(K key, GeoReference<M> reference, BoundingBox boundingBox,GeoSearchCommandArgs args) {return search(key, reference, GeoShape.byBox(boundingBox), args);
}
2.4.4.6返回的参数GeoResults
使用GEOSEARCH进行搜索返回的GeoResults封装了一个数据结构,就是查询回来的数据列表(使用getContent进行获取)
GeoResults的getContent源码:
这个方法会将查询回来的数据,以List形式的数据结构返回。
使用GEOSEARCH查询的数据据返回的是List<GeoResult<RedisCommands.GeoLocation<String>>>
public List<GeoResult<T>> getContent() {return Collections.unmodifiableList(results);
}
2.4.5解析出返回数据中的ShopId和Distance数据
解析出数据的代码流程:
1.先对search查询出的GeoResults数据进行判空
2.使用GeoReults对象的getContent获取查询回来的GeoList数据
3.再对List数据进行判空,以及判断查询回来的数据有from个嘛
4.准备一个ArrayList解析ShopId数据,准备一个HashMap解析每个ShopId对应的Distance数据(数据库中没有Distance数据,需要给Shop实体进行设置Distance数据)
5.截取出form ~ end部分的id和distance数据
要点:为什么要进行判断数据是否有from个?为什么要进行截取from ~ end部分的数据?
因为GEOSEARCH进行查询数据后,会调度CPU查询出所有的数据,最终智能借助limit截取数量,并不支持智能的分页查询等,只能自己通过这种方式实现分页的功能呢。
6.根据通过GEOSEARCH查询出的数据的ShopId,从数据库中查询数据,聚合返回。
// 4.解析出ID
if (results == null) {return Result.ok(Collections.emptyList());
}
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
// 如果分页查询发现没有数据了, 就直接返回一个空数据即可
if (list.size() < from) {return Result.ok(Collections.emptyList());
}
// 4.1 截取出from - end的部分
ArrayList<Long> ids = new ArrayList<>(list.size());
Map<String, Distance> distanceMap = new HashMap<>(list.size());
list.stream().skip(from).forEach(result -> {// 4.2 获取到shopIdString idStr = result.getContent().getName();ids.add(Long.valueOf(idStr));// 4.3 获取到distanceDistance distance = result.getDistance();distanceMap.put(idStr, distance);
});
// 5. 根据id查询Shop
String idStr = StrUtil.join(",", ids);
List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list().stream().map(shop -> {shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());return shop;
}).collect(Collectors.toList());
截取from ~ end部分的代码
要点1:借助stream流的skip(int number)进行跳过集合中的from个数据,前from个数据就不进行遍历了。
要点2:遍历出的每一条数据都是GeoReuslt<RedisCommands.GeoLoacation>,想要获取member数据要使用:getContent().getName进行获取,想要获取Distance数据要使用:getDistance进行获取。
list.stream().skip(from).forEach(result -> {// 4.2 获取到shopIdString idStr = result.getContent().getName();ids.add(Long.valueOf(idStr));// 4.3 获取到distanceDistance distance = result.getDistance();distanceMap.put(idStr, distance);
});