Spring Boot 2 實戰:利用Redis的Geo功能實現查詢附近的位置
阿新 • • 發佈:2020-06-19
![](https://img2020.cnblogs.com/other/1739473/202006/1739473-20200619114304616-406392139.jpg)
## 1. 前言
老闆突然要上線一個需求,獲取當前位置方圓一公里的業務代理點。明天上線!當接到這個需求的時候我差點吐血,這時間也太緊張了。趕緊去查相關的技術選型。經過一番折騰,終於在晚上十點完成了這個需求。現在把大致實現的思路總結一下。
![圖1](https://img2020.cnblogs.com/other/1739473/202006/1739473-20200619114304882-1963036223.png)
## 2. MySQL 不合適
> 遇到需求,首先要想到現有的東西能不能滿足,成本如何。
**MySQL**是我首先能夠想到的,畢竟大部分資料要持久化到**MySQL**。但是使用**MySQL**需要自行計算**Geohash**。需要使用大量數學幾何計算,並且需要學習地理相關知識,門檻較高,短時間內不可能完成需求,而且長期來看這也不是**MySQL**擅長的領域,所以沒有考慮它。
> **Geohash** 參考 https://www.cnblogs.com/LBSer/p/3310455.html
## 2. Redis 中的GEO
**Redis**是我們最為熟悉的**K-V**資料庫,它常被拿來作為高效能的快取資料庫來使用,大部分專案都會用到它。從**3.2**版本開始它開始提供了**GEO**能力,用來實現諸如附近位置、計算距離等這類依賴於地理位置資訊的功能。**GEO**相關的命令如下:
| Redis命令 | 描述 |
| :---------------- | :-------------------------------------------------------- |
| GEOHASH | 返回一個或多個位置元素的 **Geohash** 表示 |
| GEOPOS | 從key裡返回所有給定位置元素的位置(經度和緯度) |
| GEODIST | 返回兩個給定位置之間的距離 |
| GEORADIUS | 以給定的經緯度為中心, 找出某一半徑內的元素 |
| GEOADD | 將指定的地理空間位置(緯度、經度、名稱)新增到指定的key中 |
| GEORADIUSBYMEMBER | 找出位於指定範圍內的元素,中心點是由給定的位置元素決定 |
> Redis會假設地球為完美的球形, 所以可能有一些位置計算偏差,據說<=0.5%,對於有嚴格地理位置要求的需求來說要經過一些場景測試來檢驗是否能夠滿足需求。
### 2.1 寫入地理資訊
那麼如何實現目標單位半徑內的所有元素呢?我們可以將所有的位置的經緯度通過上表中的`GEOADD`將這些地理資訊轉換為52位的**Geohash**寫入**Redis**。
該命令格式:
```bash
geoadd key longitude latitude member [longitude latitude member ...]
```
對應例子:
```bash
redis> geoadd cities:locs 117.12 39.08 tianjin 114.29 38.02 shijiazhuang
(integer) 2
```
意思是將經度為`117.12`緯度為`39.08`的地點`tianjin`和經度為`114.29`緯度為`38.02`的地點`shijiazhuang`加入**key**為`cities:locs`的 **sorted set**集合中。可以新增一到多個位置。然後我們就可以藉助於其他命令來進行地理位置的計算了。
>有效的經度從-180度到180度。有效的緯度從-85.05112878度到85.05112878度。當座標位置超出上述指定範圍時,該命令將會返回一個錯誤。
### 2.2 統計單位半徑內的地區
我們可以藉助於`GEORADIUS`來找出以給定經緯度,某一半徑內的所有元素。
該命令格式:
```bash
georadius key longtitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC]
```
這個命令比`GEOADD`要複雜一些:
- **radius** 半徑長度,必選項。後面的`m`、`km`、`ft`、`mi`、是長度單位選項,四選一。
- **WITHCOORD** 將位置元素的經度和維度也一併返回,非必選。
- **WITHDIST** 在返回位置元素的同時, 將位置元素與中心點的距離也一併返回。 距離的單位和查詢單位一致,非必選。
- **WITHHASH** 返回位置的52位精度的**Geohash**值,非必選。這個我反正很少用,可能其它一些偏向底層的**LBS**應用服務需要這個。
- **COUNT** 返回符合條件的位置元素的數量,非必選。比如返回前10個,以避免出現符合的結果太多而出現效能問題。
- **ASC|DESC** 排序方式,非必選。預設情況下返回未排序,但是大多數我們需要進行排序。參照中心位置,從近到遠使用**ASC** ,從遠到近使用**DESC**。
例如,我們在 `cities:locs` 中查詢以(115.03,38.44)為中心,方圓`200km`的城市,結果包含城市名稱、對應的座標和距離中心點的距離(km),並按照從近到遠排列。命令如下:
```bash
redis> georadius cities:locs 115.03 38.44 200 km WITHCOORD WITHDIST ASC
1) 1) "shijiazhuang"
2) "79.7653"
3) 1) "114.29000169038772583"
2) "38.01999994251037407"
2) 1) "tianjin"
2) "186.6937"
3) 1) "117.02000230550765991"
2) "39.0800000535766543"
```
> 你可以加上 `COUNT 1`來查詢最近的一個位置。
## 3. 基於Redis GEO實戰
大致的原理思路說完了,接下來就是實操了。結合**Spring Boot**應用我們應該如何做?
### 3.1 開發環境
需要具有**GEO**特性的**Redis**版本,這裡我使用的是**Redis 4** 。另外我們客戶端使用 `spring-boot-starter-data-redis` 。這裡我們會使用到 `RedisTemplate`物件。
### 3.2 批量新增位置資訊
第一步,我們需要將位置資料初始化到**Redis**中。在**Spring Data Redis**中一個位置座標`(lng,lat)` 可以封裝到`org.springframework.data.geo.Point`物件中。然後指定一個名稱,就組成了一個位置**Geo**資訊。`RedisTemplate`提供了批量新增位置資訊的方法。我們可以將**章節2.1**中的新增命令轉換為下面的程式碼:
```jav