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);
});

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/95825.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/95825.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/95825.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

HTML第七課:發展史

HTML第七課&#xff1a;發展史發展史快速學習平臺發展史 示例 HTML 發展史 前端三件套&#xff1a;html 、css、javascript(Js) HTML 發展史 HTML 1.0&#xff08;1993 年&#xff09; 蒂姆伯納斯 - 李&#xff08;Tim Berners - Lee&#xff09;發明了萬維網&#xff0c;同…

中國生成式引擎優化(GEO)市場分析:領先企業格局與未來趨勢分析

一、GEO市場變革中國生成式引擎優化&#xff08;Generative Engine Optimization, GEO&#xff09;市場正經歷一場深刻的變革&#xff0c;其核心在于生成式人工智能&#xff08;Generative AI&#xff09;對傳統搜索引擎和數字營銷模式的顛覆性影響。傳統搜索引擎以“提供鏈接”…

好看的背景顏色 uniapp+小程序

<view class"bg-decoration"><view class"circle-1"></view><view class"circle-2"></view><view class"circle-3"></view> </view>/* 背景裝飾 */.container{background: linear-gr…

《駕馭云原生復雜性:隱性Bug的全鏈路防御體系構建》

容器、服務網格、動態配置等抽象層為系統賦予了彈性與效率,但也像深海中的暗礁,將技術風險隱藏在標準化的接口之下。那些困擾開發者的隱性Bug,往往并非源于底層技術的缺陷,而是對抽象層運行邏輯的理解偏差、配置與業務特性的錯配,或是多組件交互時的協同失效。它們以“偶發…

vosk語音識別實戰

一、簡介 Vosk 是一個由 Alpha Cephei 團隊開發的開源離線語音識別&#xff08;ASR&#xff09;工具包。它的核心優勢在于完全離線運行和輕量級&#xff0c;使其非常適合在資源受限的環境、注重隱私的場景或需要低延遲的應用中使用。 二、核心特點 離線運行 (Offline) 這是…

鴻蒙ABC開發中的名稱混淆與反射處理策略:安全與效率的平衡

在當今的軟件開發中&#xff0c;代碼安全是一個至關重要的議題。隨著鴻蒙系統&#xff08;HarmonyOS&#xff09;的廣泛應用&#xff0c;開發者們在追求功能實現的同時&#xff0c;也必須考慮如何保護代碼不被輕易破解。名稱混淆是一種常見的代碼保護手段&#xff0c;但當反射機…

css頁面頂部底部固定,中間自適應幾種方法

以下是實現頁面頂部和底部固定、中間內容自適應的幾種常見方法&#xff0c;附代碼示例和適用場景分析&#xff1a;方法一&#xff1a;Flexbox 彈性布局 <body style"margin:0; min-height:100vh; display:flex; flex-direction:column;"><header style"…

徹底拆解 CSS?accent-color:一個屬性,省下一堆“重造輪子”的苦工

我有一支技術全面、經驗豐富的小型團隊&#xff0c;專注高效交付中等規模外包項目&#xff0c;有需要外包項目的可以聯系我既要原生控件、又要品牌配色&#xff0c;還不想偽造組件&#xff1f;能不能講透 accent-color。下面給出一版盡量“到骨頭里”的解析&#xff1b;對討厭從…

在選擇iOS代簽服務前,你必須了解的三大安全風險

選iOS代簽服務&#xff1f;這三個安全坑千萬別踩&#xff01;關于iOS代簽那些你可能忽略的安全風險。多少次因為測試設備限制、緊急分發或者企業賬號年費肉疼&#xff0c;我們不得不考慮第三方代簽服務&#xff1f;但這里頭的水&#xff0c;比想象中深。風險一&#xff1a;證書…

GitHub 熱榜項目 - 日榜(2025-09-04)

GitHub 熱榜項目 - 日榜(2025-09-04) 生成于&#xff1a;2025-09-04 統計摘要 共發現熱門項目&#xff1a;20 個 榜單類型&#xff1a;日榜 本期熱點趨勢總結 本期GitHub熱榜呈現三大技術熱點&#xff1a;AI智能體開發、架構工程化和開發者工具革新。JetBrains Koog、DeepC…

在 vue-vben-admin(v5 版本)中,使用 ECharts 圖表(豆包版)

在 vue-vben-admin&#xff08;v5版本&#xff09;中&#xff0c;使用 ECharts 圖表的方式已通過框架封裝的 vben/plugins/echarts 模塊簡化&#xff0c;結合官方示例&#xff0c;具體使用步驟如下&#xff1a; 1. 核心組件與工具導入 框架提供了封裝后的 EchartsUI 組件&#…

本地 Ai 離線視頻去水印字幕!支持字幕、動靜態水印去除!

這款功能強大的AI視頻處理工具&#xff0c;能夠有效地去除視頻中的靜態水印、動態水印以及字幕。 針對不同類型的水印和字幕&#xff0c;提供了多種去除方式&#xff0c;操作簡單&#xff0c;效果顯著。 首先【打開視頻】&#xff0c;然后在識別模式里面選擇識別模式&#xf…

1個工具管好15+網盤(批量轉存/分享實測)工具實測:批量轉存 + 自動換號 + 資源監控 賬號添加失敗 / 轉存中斷?這樣解決(含功能詳解)

電腦里裝了N個網盤客戶端&#xff1a;百度網盤存工作文件、阿里云盤放家庭照片、夸克網盤塞學習資料&#xff0c;還有迅雷、天翼云盤散落在各處——每次找文件要在5個軟件間反復切換&#xff0c;手動轉存10個文件得點幾十次鼠標&#xff0c;網盤多了反倒成了“數字負擔”。直到…

2025-09-04 CSS2——常見選擇器

文章目錄1 元素選擇器2 id 選擇器3 class 選擇器4 通用選擇器5 子元素選擇器6 后代選擇器7 相鄰兄弟選擇器8 后續兄弟選擇器9 偽類選擇器10 偽元素選擇器11 屬性選擇器11.1 [attribute]11.2 [attribute"value"]11.3 [attribute~"value"]與[attribute*"…

計算機網絡:概述層---OSI參考模型

&#x1f310; OSI七層參考模型詳解&#xff1a;從物理層到應用層的完整剖析 &#x1f4c5; 更新時間&#xff1a;2025年9月3日 &#x1f3f7;? 標簽&#xff1a;OSI模型 | 網絡協議 | 七層模型 | 計算機網絡 | 網絡架構 | 協議棧 | 王道考研 摘要: 本文將用最通俗易懂的語言&…

JVM相關 2|Java 垃圾回收機制(GC算法、GC收集器如G1、CMS)的必會知識點匯總

目錄&#xff1a;&#x1f9e0; 一、GC基礎概念1. 什么是垃圾回收&#xff08;Garbage Collection, GC&#xff09;&#xff1f;2. 判斷對象是否為垃圾的方法&#x1f9e9; 二、GC核心算法1. 標記-清除算法&#xff08;Mark-Sweep&#xff09;2. 標記-整理算法&#xff08;Mark…

04 - 【HTML】- 常用標簽(下篇)

表格標簽 1 表格 table 在HTML中&#xff0c;表格是通過<table>標簽來創建的&#xff0c;它允許在html中以行和列的形式組織數據。HTML提供了一套完整的標簽來創建功能豐富的表格。 2 表格的 結構 3 表格table代碼結構 4 表格結構解析 <thead></thead>&…

nVisual從入門到精通—應用實例

五、應用實例 5.1 數據中心的規劃設計 5.1.1 規劃設計流程5.1.2 創建模型庫 5.1.2.1 設備模型庫 設備模型庫基于組織內實際使用的設備型號進行構建&#xff0c;主要包含以下對象類型&#xff1a;機柜、網絡設備、板卡、組合模型。 設備屬性字段&#xff1a;除系統保留字段&…

代碼可讀性的詳細入門

&#x1f3e0;個人主頁&#xff1a;塵覺主頁 文章目錄前言一、可讀性的重要性二、用名字表達代碼含義三、避免名字歧義四、良好的代碼風格五、注釋的價值六、如何編寫注釋七、提高控制流的可讀性八、拆分長表達式九、變量與可讀性十、抽取函數十一、一次只做一件事十二、用自然…

輪軌法向接觸斑計算

輪軌法向接觸斑計算 &#xff0c;同時輸出 接觸斑面積、長軸 a、短軸 b、最大 Hertz 壓力 pmax 等關鍵指標 算法基于 Hertz 接觸理論&#xff08;適用于單點橢圓接觸&#xff09;&#xff0c;并給出如何擴展到 非 Hertz / 有限元驗證的提示。1 理論回顧&#xff08;Hertz 橢圓…