?? 嗨,您好 ?? 我是 vnjohn,在互聯(lián)網(wǎng)企業(yè)擔(dān)任 Java 開發(fā),CSDN 優(yōu)質(zhì)創(chuàng)作者
?? 推薦專欄:Spring、MySQL、Nacos、Java,后續(xù)其他專欄會(huì)持續(xù)優(yōu)化更新迭代
??文章所在專欄:MySQL、Redis、業(yè)務(wù)設(shè)計(jì)
?? 我當(dāng)前正在學(xué)習(xí)微服務(wù)領(lǐng)域、云原生領(lǐng)域、消息中間件等架構(gòu)、原理知識(shí)
?? 向我詢問任何您想要的東西,ID:vnjohn
??覺得博主文章寫的還 OK,能夠幫助到您的,感謝三連支持博客??
?? 代詞: vnjohn
? 有趣的事實(shí):音樂、跑步、電影、游戲
目錄
前言
在企業(yè)開發(fā)中,例如:附近服務(wù)門店/網(wǎng)點(diǎn)查詢、附近服務(wù)工人派單查詢,若沒有合理去設(shè)計(jì)地理位置的這塊查詢性能提升的功能時(shí),都是會(huì)去數(shù)據(jù)庫(kù)層面采用函數(shù)計(jì)算出來(lái),這種方式本來(lái)就存在一定的弊端
1、數(shù)據(jù)庫(kù)層面是性能瓶頸,將所有的壓力放在數(shù)據(jù)庫(kù)中,必然會(huì)給系統(tǒng)帶來(lái)災(zāi)難級(jí)的響應(yīng),例如:當(dāng)同時(shí)訪問的用戶量遞增時(shí),數(shù)據(jù)庫(kù)連接池打滿 > CPU 飄升 > 系統(tǒng)長(zhǎng)時(shí)間停留在數(shù)據(jù)庫(kù)層面無(wú)法及時(shí)響應(yīng)給用戶
2、當(dāng)服務(wù)門店/網(wǎng)點(diǎn)數(shù)據(jù)量越來(lái)越大時(shí)、服務(wù)工人數(shù)據(jù)越來(lái)越龐大時(shí),在使用函數(shù)計(jì)算篩選出附近的數(shù)據(jù),必然會(huì)造成數(shù)據(jù)庫(kù)的全表掃描 >explain type:ALL
3、當(dāng)最近的服務(wù)門店/網(wǎng)點(diǎn)、服務(wù)工人不滿足用戶的需求對(duì)象時(shí),會(huì)一直向下拉取下一頁(yè)的數(shù)據(jù),直至篩選到滿足自己的服務(wù)對(duì)象才停止,每一段的篩選都是一次性能極差的 SELECT
故而言之,因?yàn)檫@種問題的出現(xiàn),不得已而從其他方面去考慮來(lái)提升地理位置這塊的篩選動(dòng)作,由數(shù)據(jù)庫(kù)「磁盤存儲(chǔ)經(jīng)緯度」改為緩存「內(nèi)存存儲(chǔ)經(jīng)緯度」來(lái)提升重復(fù)的查詢操作
該文會(huì)演示從數(shù)據(jù)庫(kù)層面 > 緩存層面,地理位置的優(yōu)化提升改造
MySQL 數(shù)據(jù)庫(kù)
現(xiàn)大部分企業(yè)都采用 MySQL 作為數(shù)據(jù)庫(kù)存儲(chǔ),所以以 MySQL 8.0 為例,演練在它里面如何采用函數(shù)來(lái)完成地理位置的計(jì)算
表結(jié)構(gòu)
CREATE TABLE `shop` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '門店id',
`shop_no` varchar(64) NOT NULL COMMENT '門店編碼',
`shop_name` varchar(50) NOT NULL COMMENT '門店名稱',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '啟用狀態(tài):1-啟用、0-',
`logo` varchar(255) DEFAULT NULL COMMENT '門店Logo',
`introduce` text COMMENT '門店介紹',
`longitude` double NOT NULL COMMENT '經(jīng)度',
`latitude` double NOT NULL COMMENT '緯度',
`trade_start_time` time DEFAULT NULL COMMENT '營(yíng)業(yè)開始時(shí)間',
`trade_end_time` time DEFAULT NULL COMMENT '營(yíng)業(yè)結(jié)束時(shí)間',
`contacts` varchar(20) DEFAULT NULL COMMENT '聯(lián)系人',
`telephone` varchar(50) DEFAULT NULL COMMENT '商家聯(lián)系電話',
`province_id` bigint DEFAULT NULL COMMENT '省id',
`province` varchar(255) DEFAULT NULL COMMENT '省',
`city_id` bigint DEFAULT NULL COMMENT '市id',
`city` varchar(255) DEFAULT NULL COMMENT '市',
`area_id` bigint DEFAULT NULL COMMENT '區(qū)id',
`area` varchar(255) DEFAULT NULL COMMENT '區(qū)',
`address` varchar(255) DEFAULT NULL COMMENT '門店詳細(xì)地址',
`created_time` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間',
`updated_time` datetime DEFAULT NULL COMMENT '更新時(shí)間',
`is_deleted` tinyint(1) DEFAULT '0' COMMENT '是否刪除',
PRIMARY KEY (`id`),
UNIQUE KEY `uni_shop_no` (`shop_no`) COMMENT '門店編碼唯一索引'
) ENGINE=InnoDB AUTO_INCREMENT=45 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商家信息';
首先創(chuàng)建一張商家表「門店/服務(wù)網(wǎng)點(diǎn)」涉及到地理位置比較重要的兩個(gè)字段,longitude > 經(jīng)度、latitude > 緯度
經(jīng)度的最大值:180°
緯度的最大值:90°
模擬數(shù)據(jù)
使用存儲(chǔ)函數(shù),模擬生成十萬(wàn)條商家數(shù)據(jù)
CREATE DEFINER = `root` @`localhost` PROCEDURE `batchInsert` ( IN args INT ) BEGIN
DECLARE
-- 開啟事務(wù)
i INT DEFAULT 1;
START TRANSACTION;
WHILE
i <= args DO
INSERT INTO shop ( shop_no, shop_name, `status`, longitude, latitude )
VALUE
(
ROUND( RAND() * 99999 ),
concat( "商家-", i ),1,
-- 隨機(jī)生成經(jīng)緯度
(RAND() * ( 179.077090052913654 - 0.477040512464626 )) + 0.477040512464626,
(RAND() * ( 89.9172823750000134 - - 1.8840792500000134 )) + - 1.8840792500000134
);
SET i = i + 1;
END WHILE;
COMMIT;
END
call batchInsert(100000);
數(shù)據(jù)庫(kù)查詢
不加索引
先使用「經(jīng)緯度」字段不加索引的方式執(zhí)行 SQL
EXPLAIN SELECT
*
FROM
( SELECT id, ST_DISTANCE_SPHERE ( POINT ( 114.112808, 22.544977 ), POINT ( longitude, latitude )) AS distance FROM shop WHERE `STATUS` = 1 ) temp
WHERE
ROUND( distance / 1000, 2 ) BETWEEN 0 AND 20
ORDER BY distance ASC
LIMIT 5
執(zhí)行計(jì)劃結(jié)果如下:
加索引
alter table shop add index `idx_location` (`longitude`,`latitude`) USING BTREE;
再次執(zhí)行 SQL,如下:
```sql
EXPLAIN SELECT
*
FROM
( SELECT id, ST_DISTANCE_SPHERE ( POINT ( 114.112808, 22.544977 ), POINT ( longitude, latitude )) AS distance FROM shop WHERE `STATUS` = 1 ) temp
WHERE
ROUND( distance / 1000, 2 ) BETWEEN 0 AND 20
ORDER BY distance ASC
LIMIT 5
執(zhí)行計(jì)劃結(jié)果如下:
直譯函數(shù)
MySQL 官方直譯 ST_DISTANCE_SPHERE 函數(shù)說明
語(yǔ)法:ST_Distance_Sphere(g1, g2 [, radius])
說明:
返回球體之間 Point 或 MultiPoint 參數(shù)之間的最小球面距離(以米為單位)可選 radius 參數(shù)應(yīng)以米為單位給出
如果兩個(gè)幾何參數(shù)都是有效的笛卡爾參數(shù) Point 或 MultiPoint SRID 0 中的值,則返回值是具有所提供半徑的球體上兩個(gè)幾何之間的最短距離。如果省略,則默認(rèn)半徑為 6,370,986 米,點(diǎn) X 和 Y 坐標(biāo)分別解釋為經(jīng)度和緯度(以度為單位)
如果任何參數(shù)的經(jīng)度或緯度超出范圍,則會(huì)發(fā)生錯(cuò)誤:
1、若經(jīng)度值不在 (?180, 180] 范圍內(nèi),則會(huì)發(fā)生 ER_GEOMETRY_PARAM_LONGITUDE_OUT_OF_RANGE 錯(cuò)誤(在 MySQL 8.0.12 ER_LONGITUDE_OUT_OF_RANGE 之前)
2、若緯度值不在 [?90, 90] 范圍內(nèi),則會(huì)發(fā)生 ER_GEOMETRY_PARAM_LATITUDE_OUT_OF_RANGE 錯(cuò)誤(在 MySQL 8.0.12 ER_LATITUDE_OUT_OF_RANGE 之前)
小結(jié)
從以上數(shù)據(jù)庫(kù)做地理位置篩選的結(jié)果來(lái)看,無(wú)論是否追加索引,似乎對(duì)數(shù)據(jù)庫(kù)的查詢性能來(lái)說,并沒有提升
使用數(shù)據(jù)庫(kù)做地理位置篩選,基于以下幾種情況可以考慮使用該方式進(jìn)行處理
商家「服務(wù)門店/網(wǎng)點(diǎn)」數(shù)據(jù)量不多
商家「服務(wù)門店/網(wǎng)點(diǎn)」模塊提供給用戶服務(wù)的入口較小
Redis 緩存
基于 Redis API 實(shí)現(xiàn)地理位置使用 GEO 有兩種方式
1、org.springframework.data.redis.core.RedisTemplate
2、org.redisson.api.RedissonClient
Redis GEO 客戶端
該篇節(jié),先告知大家如何應(yīng)用 Redis 客戶端的 GEO 類型,API 會(huì)基于客戶端的函數(shù)進(jìn)行一次封裝,先了解底層開始再到最后的高級(jí) API 實(shí)踐
1、查看 Redis 版本
redis-cli -v
2、連接 Redis 客戶端
1、redis-cli
2、無(wú)密碼直接登錄,有密碼通過:auth 明文密碼
3、查看 GEO、ZSet 幫助文檔
help @GEO
help @sorted-set
127.0.0.1:6379> help @GEO
# GEO 指定的緩存 Key 追加 1~N 條經(jīng)緯度地理位置信息
GEOADD key [NX|XX] [CH] longitude latitude member [longitude latitude member ...]
summary: Add one or more geospatial items in the geospatial index represented using a sorted set
since: 3.2.0
# GEO 指定的緩存 Key 兩個(gè)成員之間的距離
# M|KM|FT|MI:米、公里、英里、英尺
GEODIST key member1 member2 [M|KM|FT|MI]
summary: Returns the distance between two members of a geospatial index
since: 3.2.0
# GEO 指定緩存 Key 地理位置索引 > 標(biāo)準(zhǔn)地理散列字符串
GEOHASH key member [member ...]
summary: Returns members of a geospatial index as standard geohash strings
since: 3.2.0
# GEO 指定緩存 Key 地理位置索引 > 成員對(duì)應(yīng)的經(jīng)緯度
GEOPOS key member [member ...]
summary: Returns longitude and latitude of members of a geospatial index
since: 3.2.0
# GEO 指定緩存 Key:查詢表示地理空間索引的排序集,以傳入的經(jīng)緯度來(lái)獲取與點(diǎn)的給定最大距離匹配的成員,可按升序、降序排序
GEORADIUS key longitude latitude radius M|KM|FT|MI [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]
summary: Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point
since: 3.2.0
# GEO 指定緩存 Key: 查詢表示地理空間索引的排序集,以傳入的指定成員經(jīng)緯度來(lái)獲取與點(diǎn)的給定最大距離匹配的成員,可按升序、降序排序
GEORADIUSBYMEMBER key member radius M|KM|FT|MI [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]
summary: Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member
since: 3.2.0
# GEO 指定緩存 Key: 查詢表示地理空間索引的排序集,以傳入的指定成員經(jīng)緯度來(lái)獲取與點(diǎn)的給定最大距離匹配的成員,可按升序、降序排序,只支持可讀
GEORADIUSBYMEMBER_RO key member radius M|KM|FT|MI [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC]
summary: A read-only variant for GEORADIUSBYMEMBER
since: 3.2.10
# GEO 指定緩存 Key:查詢表示地理空間索引的排序集,以傳入的經(jīng)緯度來(lái)獲取與點(diǎn)的給定最大距離匹配的成員,可按升序、降序排序,只支持可讀
GEORADIUS_RO key longitude latitude radius M|KM|FT|MI [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC]
summary: A read-only variant for GEORADIUS
since: 3.2.10
# GEO 指定緩存 Key:查詢表示地理空間索引的排序集,以獲取「成員或指定經(jīng)緯度」最大距離匹配的成員,可按升序、降低排序,不支持存儲(chǔ)
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]
summary: Query a sorted set representing a geospatial index to fetch members inside an area of a box or a circle.
since: 6.2.0
# GEO 指定緩存 Key:查詢表示地理空間索引的排序集,以「獲取成員或指定經(jīng)緯度」最大距離匹配的成員,可按升序、降低排序,支持存儲(chǔ)至 ZSet Key
GEOSEARCHSTORE destination source FROMMEMBER member|FROMLONLAT longitude latitude BYRADIUS radius M|KM|FT|MI|BYBOX width height M|KM|FT|MI [ASC|DESC] [COUNT count [ANY]] [STOREDIST]
summary: Query a sorted set representing a geospatial index to fetch members inside an area of a box or a circle, and store the result in another key.
since: 6.2.0
引入 Spring、Redisson 配置
1、maven 依賴配置
<properties>
<spring.boot.version>2.6.7</spring.boot.version>
<redisson.version>3.17.5</redisson.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
2、Redis 核心配置類,如下:
/**
* Redis 核心配置類
*
* @author vnjohn
* @since 2023
*/
@Configuration
public class RedisConfig {
@Resource
private RedisConnectionFactory factory;
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.setStringSerializer(new StringRedisSerializer());
redisTemplate.setDefaultSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
@Bean
public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForHash();
}
@Bean
public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
return redisTemplate.opsForValue();
}
@Bean
public GeoOperations<String, String> geoOperations(RedisTemplate<String, String> redisTemplate) {
return redisTemplate.opsForGeo();
}
@Bean
public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForList();
}
@Bean
public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForSet();
}
@Bean
public ZSetOperations<String, String> zSetOperations(RedisTemplate<String, String> redisTemplate) {
return redisTemplate.opsForZSet();
}
}
在本文,我們會(huì)用到 GeoOperations、ZSetOperations 操作類去調(diào)用 API
RedisTemplate API 操作
RedisTemplate 操作工具類,如下:
@Resource
private GeoOperations<String, String> geoOperations;
@Resource
private ZSetOperations<String, String> zSetOperations;
// ============================ sorted-set =============================
public ZSetOperations.TypedTuple<String> redisTemplateZSetPopMinScore(String key) {
return zSetOperations.popMin(key);
}
// ============================ Geo =============================
/**
* 新增 Geo 某個(gè) Key 成員的經(jīng)緯度信息
*
* @param key Redis 緩存 Key
* @param longitude 經(jīng)度
* @param latitude 緯度
* @param member 成員
*/
public void geoAdd(String key, Double longitude, Double latitude, String member) {
Point point = new Point(longitude, latitude);
geoOperations.add(key, point, member);
}
/**
* 刪除 Geo 某個(gè) Key 成員的經(jīng)緯度信息
*
* @param key Redis 緩存 Key
* @param member 成員
*/
public void geoRemove(String key, String member) {
geoOperations.remove(key, member);
}
/**
* 以半徑為單位,「千米」為計(jì)算單位展開,以倒序的方式展示對(duì)應(yīng)的信息
*
* @param key 緩存 Key
* @param longitude 經(jīng)度
* @param latitude 緯度
* @param distanceNum 距離,單位:KM
*/
public GeoResults<RedisGeoCommands.GeoLocation<String>> geoRadiusWithKilometers(String key, Double longitude,
Double latitude, Double distanceNum) {
return geoRadiusWithKilometers(key, longitude, latitude, distanceNum, null, Boolean.TRUE);
}
/**
* 以半徑為單位,「千米」為計(jì)算單位展開,以倒序的方式展示對(duì)應(yīng)的信息
*
* @param key 緩存 Key
* @param longitude 經(jīng)度
* @param latitude 緯度
* @param limit 篩選條數(shù)
* @param distanceNum 距離,單位:KM
*/
public GeoResults<RedisGeoCommands.GeoLocation<String>> geoRadiusWithKilometersLimit(String key, Double longitude, Double latitude,
Integer limit, Double distanceNum) {
return geoRadiusWithKilometers(key, longitude, latitude, distanceNum, limit, Boolean.TRUE);
}
/**
* 以半徑為單位,「千米」為計(jì)算單位展開,以自定義順序方式展示對(duì)應(yīng)的信息
*
* @param key 緩存 Key
* @param longitude 經(jīng)度
* @param latitude 緯度
* @param limit 篩選條數(shù)
* @param distanceNum 距離,單位:KM
* @param ascOrder 是否按升序排
*/
public GeoResults<RedisGeoCommands.GeoLocation<String>> geoRadiusWithKilometers(String key, Double longitude, Double latitude,
Double distanceNum, Integer limit, Boolean ascOrder) {
Point point = new Point(longitude, latitude);
Distance radius = new Distance(distanceNum, Metrics.KILOMETERS);
Circle within = new Circle(point, radius);
RedisGeoCommands.GeoRadiusCommandArgs geoRadiusCommandArgs = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeCoordinates().includeDistance();
if (null != limit) {
geoRadiusCommandArgs = geoRadiusCommandArgs.limit(limit);
}
geoRadiusCommandArgs = ascOrder ? geoRadiusCommandArgs.sortAscending() : geoRadiusCommandArgs.sortDescending();
return geoOperations.radius(key, within, geoRadiusCommandArgs);
}
/**
* 以半徑為單位,「米」為計(jì)算單位展開,以倒序的方式展示對(duì)應(yīng)的信息
*
* @param key 緩存 Key
* @param longitude 經(jīng)度
* @param latitude 緯度
* @param distanceNum 距離,單位:M
*/
public GeoResults<RedisGeoCommands.GeoLocation<String>> geoRadiusWithMeters(String key, Double longitude,
Double latitude, Double distanceNum) {
return geoRadiusWithMeters(key, longitude, latitude, distanceNum, true);
}
/**
* 以半徑為單位,「米」為計(jì)算單位展開,以自定義順序方式展示對(duì)應(yīng)的信息
*
* @param key 緩存 Key
* @param longitude 經(jīng)度
* @param latitude 緯度
* @param distanceNum 距離,單位:M
* @param ascOrder 是否按升序排
*/
public GeoResults<RedisGeoCommands.GeoLocation<String>> geoRadiusWithMeters(String key, Double longitude,
Double latitude, Double distanceNum,
Boolean ascOrder) {
Point point = new Point(longitude, latitude);
Distance radius = new Distance(distanceNum, Metrics.NEUTRAL);
Circle within = new Circle(point, radius);
RedisGeoCommands.GeoRadiusCommandArgs geoRadiusCommandArgs = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeCoordinates().includeDistance();
geoRadiusCommandArgs = ascOrder ? geoRadiusCommandArgs.sortAscending() : geoRadiusCommandArgs.sortDescending();
return geoOperations.radius(key, within, geoRadiusCommandArgs);
}
public Long redisTemplateStoreSortedSearchTo(String destName, String key, Double longitude, Double latitude,
Double distanceNum, Integer limit, Boolean ascOrder) {
Distance distance = new Distance(distanceNum, Metrics.KILOMETERS);
RedisGeoCommands.GeoSearchStoreCommandArgs geoSearchStoreCommandArgs = RedisGeoCommands.GeoSearchStoreCommandArgs.newGeoSearchStoreArgs();
geoSearchStoreCommandArgs.limit(limit);
geoSearchStoreCommandArgs.sort(ascOrder ? Sort.Direction.ASC : Sort.Direction.DESC);
GeoReference geoReference = GeoReference.fromCoordinate(longitude, latitude);
Long searchAndStore = geoOperations.searchAndStore(key, destName, geoReference, distance, geoSearchStoreCommandArgs);
return searchAndStore;
}
1、geoAdd 方法 -> GEOADD 函數(shù)
2、geoRemove 方法 -> ZREM 函數(shù)
GEO 存儲(chǔ)起來(lái)以后放在 Redis 中是以 ZSet 結(jié)構(gòu)進(jìn)行存儲(chǔ)的,所以將 GEO 某個(gè)元素刪除時(shí),就調(diào)用 ZREM 函數(shù)進(jìn)行刪除即可
3、geoRadiusWithKilometers、geoRadiusWithMeters 方法操作的都是相同的函數(shù),只是篩選距離的單位不同,一個(gè)是千米、一個(gè)是米,它們對(duì)應(yīng)的函數(shù)有兩個(gè),GEORADIUS — 篩選附近距離的滿足元素、GEORADIUS_RO — 篩選附近距離的滿足元素,只支持可讀
具體的方法執(zhí)行邏輯可以查看以下方法源碼:RedisGeoCommands#GeoRadiusCommandArgs,該方法主要對(duì)我們傳入的參數(shù)進(jìn)行一次封裝,轉(zhuǎn)換為 Redis 中可識(shí)別的函數(shù)參數(shù)可選項(xiàng)
public GeoResults<GeoLocation<byte[]>> geoRadius(byte[] key, Circle within, GeoRadiusCommandArgs args) {
List<Object> params = new ArrayList<Object>();
params.add(key);
params.add(convert(within.getCenter().getX()));
params.add(convert(within.getCenter().getY()));
params.add(within.getRadius().getValue());
params.add(getAbbreviation(within.getRadius().getMetric()));
RedisCommand<GeoResults<GeoLocation<byte[]>>> command;
if (args.getFlags().contains(GeoRadiusCommandArgs.Flag.WITHCOORD)) {
command = new RedisCommand<GeoResults<GeoLocation<byte[]>>>("GEORADIUS_RO", postitionDecoder);
params.add("WITHCOORD");
} else {
MultiDecoder<GeoResults<GeoLocation<byte[]>>> distanceDecoder = new ListMultiDecoder2(new GeoResultsDecoder(within.getRadius().getMetric()), new GeoDistanceDecoder());
command = new RedisCommand<GeoResults<GeoLocation<byte[]>>>("GEORADIUS_RO", distanceDecoder);
params.add("WITHDIST");
}
if (args.getLimit() != null) {
params.add("COUNT");
params.add(args.getLimit());
}
if (args.getSortDirection() != null) {
params.add(args.getSortDirection().name());
}
return read(key, ByteArrayCodec.INSTANCE, command, params.toArray());
}
引入 RedisTemplate API 有一些特性,我們?cè)趯?shí)際應(yīng)用中可能應(yīng)用不到,如:
1、當(dāng) GEO 中某個(gè)成員不知道它是否存在,當(dāng)不存在時(shí)可以直接新增,存在時(shí)不做任何變更,RedisTemplate API 需要操作兩次函數(shù):geoRemove、geoAdd,而下面要講解的 Redisson API 直接可以通過一個(gè)函數(shù)搞定,好處:減少一次與 Redis 之間的連接,提高操作效率
2、使用 Redisson 客戶端,實(shí)現(xiàn)「搜索滿足距離條件的成員列表」功能時(shí)更加的便捷
若 Redisson 版本不對(duì)時(shí),會(huì)在操作 redisTemplateZSetPopMinScore 方法時(shí),出現(xiàn)如下異常:
java.lang.StackOverflowError: null at org.springframework.data.redis.connection.DefaultedRedisConnection.zPopMin(DefaultedRedisConnection.java:973)
解決辦法:將 Redisson 版本降低到 3.15.6
Redisson API 操作
Redisson 操作工具類,如下:
private static final StringCodec REDISSON_CODE_C = new StringCodec();
@Resource
private RedissonClient redissonClient;
// ============================ ZSet Redisson =============================
public String redissonZSetPopMinScore(String key) {
RScoredSortedSet<Object> scoredSortedSet = redissonClient.getScoredSortedSet(key, REDISSON_CODE_C);
return (String) scoredSortedSet.pollFirst();
}
// ============================ Geo Redisson =============================
/**
* 獲取 Redisson GEO 類型客戶端實(shí)例
*
* @param key 緩存 Key
* @return 基于 Redisson GEO 操作的客戶端實(shí)例
*/
private RGeo<String> getRGeoClient(String key) {
return redissonClient.getGeo(key, REDISSON_CODE_C);
}
/**
* 若存在的話,替換 Geo 某個(gè) Key 成員的經(jīng)緯度信息
*/
public Boolean redissonGeoAddIfExists(String key, Object member, Double longitude, Double latitude) {
RGeo<String> geo = getRGeoClient(key);
return geo.addIfExists(new GeoEntry(longitude, latitude, member)) > 0;
}
/**
* 刪除指定 Key > 多個(gè) Member 元素
*
* @param key 緩存 Key
* @param members 成員列表
*/
public void redissonGeoRemove(String key, List<Long> members) {
RGeo<String> geo = getRGeoClient(key);
geo.removeAll(members);
}
/**
* 新增 Geo 某個(gè) Key 成員的經(jīng)緯度信息
*
* @param key 緩存 Key
* @param member 成員
* @param longitude 經(jīng)度
* @param latitude 緯度
*/
public void redissonGeoAdd(String key, Object member, Double longitude, Double latitude) {
RGeo<String> geo = getRGeoClient(key);
geo.add(new GeoEntry(longitude, latitude, member));
}
/**
* 搜索滿足距離條件的成員列表
*
* @param key 緩存 Key
* @param longitude 經(jīng)度
* @param latitude 緯度
* @param distanceNum 距離:KM
* @return 匹配到的成員記錄及距離
* @see GeoUnit geoUnit
* 以半徑為中心距離,「geoUnit」為計(jì)算單位展開,以距離優(yōu)先展示對(duì)應(yīng)的信息
*/
public Map<String, Double> searchWithDistance(String key, Double longitude, Double latitude, Double distanceNum) {
return searchWithDistance(key, longitude, latitude, distanceNum, GeoUnit.KILOMETERS, null);
}
/**
* 搜索滿足距離條件的成員列表
*
* @param key 緩存 Key
* @param longitude 經(jīng)度
* @param latitude 緯度
* @param distanceNum 距離:KM
* @return 匹配到的成員記錄及距離
* @see GeoUnit geoUnit
* 以半徑為中心距離,「geoUnit」為計(jì)算單位展開,以距離優(yōu)先展示對(duì)應(yīng)的信息
*/
public Map<String, Double> searchWithDistance(String key, Double longitude, Double latitude, Double distanceNum, Integer limit) {
return searchWithDistance(key, longitude, latitude, distanceNum, GeoUnit.KILOMETERS, limit);
}
/**
* 搜索滿足距離條件的成員列表
*
* @param key 緩存 Key
* @param longitude 經(jīng)度
* @param latitude 緯度
* @param distanceNum 距離
* @param geoUnit 距離單位
* @param limit 篩選條數(shù)
* @return 匹配到的成員記錄及距離
* @see GeoUnit geoUnit
* 以半徑為中心距離,「geoUnit」為計(jì)算單位展開,以距離優(yōu)先展示對(duì)應(yīng)的信息
*/
public Map<String, Double> searchWithDistance(String key, Double longitude, Double latitude, Double distanceNum, GeoUnit geoUnit, Integer limit) {
RGeo<String> geo = getRGeoClient(key);
GeoSearchArgs args;
if (null != limit) {
args = GeoSearchArgs.from(longitude, latitude).radius(distanceNum, geoUnit).order(GeoOrder.ASC).count(limit);
} else {
args = GeoSearchArgs.from(longitude, latitude).radius(distanceNum, geoUnit).order(GeoOrder.ASC);
}
return geo.searchWithDistance(args);
}
/**
* 存儲(chǔ)搜索滿足條件的成員列表
*
* @param destName 存儲(chǔ) ZSet Key
* @param key 搜索目標(biāo) Key
* @param longitude 經(jīng)度
* @param latitude 緯度
* @param distanceNum 距離
*/
public Boolean storeSortedSearchTo(String destName, String key, Double longitude, Double latitude,
Double distanceNum) {
return storeSortedSearchTo(destName, key, longitude, latitude, distanceNum, GeoUnit.KILOMETERS, null);
}
/**
* 此處的應(yīng)用場(chǎng)景:
* 1、當(dāng)用戶下單以后,通過該方法將用戶下單所在經(jīng)緯度最近的工人都統(tǒng)計(jì)出來(lái)
* 2、統(tǒng)計(jì)出來(lái)的數(shù)據(jù)再次進(jìn)行一次比對(duì),若工人未開啟接單,那么該工人所在元素會(huì)被移除掉
* 3、當(dāng)工人端拒絕接單,那么該工人所在元素從 ZSET 中移除
* 4、當(dāng)工人端已接單并且開始服務(wù),那么該用戶所在的統(tǒng)計(jì)數(shù)據(jù)可被移除
*
* @param destName 存儲(chǔ) ZSet Key
* @param key 搜索目標(biāo) Key
* @param longitude 經(jīng)度
* @param latitude 緯度
* @param distanceNum 距離
* @param geoUnit 距離單位
* @param limit 條數(shù)
*/
public Boolean storeSortedSearchTo(String destName, String key, Double longitude, Double latitude,
Double distanceNum, GeoUnit geoUnit, Integer limit) {
RGeo<String> geo = getRGeoClient(key);
GeoSearchArgs args;
if (null != limit) {
args = GeoSearchArgs.from(longitude, latitude)
.radius(distanceNum, geoUnit)
.order(GeoOrder.ASC)
.count(limit);
} else {
args = GeoSearchArgs.from(longitude, latitude)
.radius(distanceNum, geoUnit)
.order(GeoOrder.ASC);
}
return geo.storeSortedSearchTo(destName, args) > 0;
}
Redisson 中對(duì)不同的編碼還進(jìn)行了優(yōu)化,若知道當(dāng)前存儲(chǔ)或查詢的元素屬于非字符類型,可以通過以下類型來(lái)指定:
1、字符型:StringCodec,默認(rèn)使用 UTF-8 編碼方式
2、字節(jié)數(shù)組型:ByteArrayCodec
3、整型:IntegerCodec
4、浮點(diǎn)型:DoubleCodec
…
它們共同的父類為 BaseCodec,除了字符型,其他的編碼類型都有實(shí)現(xiàn)各自的解碼器
1、redissonGeoAdd 方法 -> GEOADD 函數(shù)
2、redissonGeoRemove 方法 -> ZREM 函數(shù)
與 RedisTemplate API 一致,GEO 存儲(chǔ)起來(lái)以后放在 Redis 中是以 ZSet 結(jié)構(gòu)進(jìn)行存儲(chǔ)的,所以將 GEO 某個(gè)元素刪除時(shí),就調(diào)用 ZREM 函數(shù)進(jìn)行刪除即可
3、redissonGeoAddIfExists -> GEOPOS、GEOADD 函數(shù)一起組合使用的
可觀察該方法的實(shí)現(xiàn):
RedissonGeo#addIfExistsAsync
,內(nèi)部使用 Redis Lua 腳本實(shí)現(xiàn)了這兩個(gè)函數(shù)的組合運(yùn)用,當(dāng) GEOPOS 返回的數(shù)據(jù)為真時(shí),那么就調(diào)用 GEOADD 函數(shù)將當(dāng)前元素存入 GEO Key 中
4、searchWithDistance 方法,它對(duì)應(yīng)的函數(shù)有兩個(gè),GEORADIUS — 篩選附近距離的滿足元素、GEORADIUS_RO — 篩選附近距離的滿足元素,只支持可讀
5、storeSortedSearchTo 方法,將篩選出來(lái)的內(nèi)容存儲(chǔ)到一個(gè)新的 ZSet Key 中
應(yīng)用場(chǎng)景如下:當(dāng)用戶在某個(gè)地點(diǎn)下單以后,需要篩選它附近可派單的工人,可篩選指定人數(shù)(只要滿足服務(wù)距離條件)存儲(chǔ)到新的 Key 中,當(dāng)存儲(chǔ)完成以后,即使第一個(gè)被派單的工人取消服務(wù)了,可以利用 ZSet 作為一個(gè)棧的結(jié)構(gòu),按照最近或最遠(yuǎn)的方式進(jìn)行一個(gè)一個(gè)的彈出來(lái) > Pop,結(jié)合 redissonZSetPopMinScore 方法天衣無(wú)縫!!
小結(jié)
1、若要使用 RedisTemplate API 中的 redisTemplateStoreSortedSearchTo 方法或者使用 Redisson API 中的 storeSortedSearchTo 方法,Redis 服務(wù)端的版本必須高于或等于 6.2.0
這兩個(gè)方法對(duì)應(yīng) Redis 中的 GEOSEARCHSTORE 函數(shù),可以使用 help GEOSEARCHSTORE
命令,結(jié)合幫助文檔運(yùn)用起來(lái)
2、在如何考慮是否引入一個(gè)新的組件,來(lái)減少對(duì)數(shù)據(jù)庫(kù)造成的壓力,就需要看地理位置這塊篩選的工作數(shù)據(jù)量有多大了,數(shù)據(jù)量大的話,寧愿基于內(nèi)存來(lái)完成地理位置篩選,也不要將查詢數(shù)據(jù)壓力放在基于磁盤的數(shù)據(jù)庫(kù)
3、引入一個(gè)新的組件,必然而然會(huì)考慮到引入這個(gè)組件會(huì)帶來(lái)哪些問題,那么又要解決好組件給我們的問題了,數(shù)據(jù)存儲(chǔ)到內(nèi)存中并不可靠,所以在對(duì)引入 Redis 組件時(shí),我們要把它的持久化機(jī)制考慮進(jìn)去,結(jié)合 Redis 保證地理位置查詢性能高效、持久化機(jī)制保證數(shù)據(jù)可靠
Redis 持久化機(jī)制類型:AOF、RDB
1、采用 AOF 方式進(jìn)行持久化,一行一行 Redis 命令會(huì)入文件,會(huì)導(dǎo)致文件過大,從而造成恢復(fù)數(shù)據(jù)速度會(huì)很慢,也會(huì)給機(jī)器磁盤帶來(lái)存儲(chǔ)壓力,好處就是能保證數(shù)據(jù)基本不丟失
2、采用 RDB 方式進(jìn)行持久化,會(huì)導(dǎo)致一部分?jǐn)?shù)據(jù)在瞬時(shí)丟失,從而就導(dǎo)致了數(shù)據(jù)存儲(chǔ)不可靠,好處就是恢復(fù)速率快
3、結(jié)合以上兩種方式都有缺點(diǎn),AOF+RDB 結(jié)合作為持久化方式,不僅僅用到了 AOF 數(shù)據(jù)可靠性也用到了 RDB 恢復(fù)數(shù)據(jù)的效率性
Redis 持久化機(jī)制 AOF、RDB、AOF+RDB 方式的詳細(xì)內(nèi)容,會(huì)在后續(xù)有文章進(jìn)行介紹,敬請(qǐng)期待?。?/p>
總結(jié)
該篇博文,主要先是進(jìn)行「地理位置」生產(chǎn)性能問題的全流程演化,從 MySQL -> +索引 -> 不 + 索引,使用了案例 SQL 進(jìn)行執(zhí)行計(jì)劃的分析,從而得出了 MySQL 在特殊場(chǎng)景下不適用于做地理位置的篩選工作「因?yàn)樗旧砘诖疟P的,在大數(shù)據(jù)量情況下,不能肆意打壓瓶頸」;隨即采用了 Redis GEO 類型來(lái)優(yōu)化了地理位置的篩選工作,結(jié)合 RedisTemplate、Redisson 客戶端 API 實(shí)戰(zhàn)函數(shù)進(jìn)行講解,從零到一教你如何運(yùn)用程序結(jié)合 Redis GEO 數(shù)據(jù)類型完成地理位置的優(yōu)化工程,希望此博文你能夠喜歡!
??????愿你我都能夠在寒冬中相互取暖,互相成長(zhǎng),只有不斷積累、沉淀自己,后面有機(jī)會(huì)自然能破冰而行!
博文放在 Redis 專欄里,歡迎訂閱,會(huì)持續(xù)更新!
如果覺得博文不錯(cuò),關(guān)注我 vnjohn,后續(xù)會(huì)有更多實(shí)戰(zhàn)、源碼、架構(gòu)干貨分享!
推薦專欄:Spring、MySQL,訂閱一波不再迷路文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-714461.html
大家的「關(guān)注?? + 點(diǎn)贊?? + 收藏?」就是我創(chuàng)作的最大動(dòng)力!謝謝大家的支持,我們下文見!文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-714461.html
到了這里,關(guān)于Redis GEO 類型與 API 結(jié)合,地理位置優(yōu)化的絕佳實(shí)踐的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!