Geo技術助力,讓風險定位更精準
1 業務背景
2 技術選型
- 2.1 MySQL
- 2.2 Redis
- 2.3 ElasticSearch
3 Coding
3.1 GEOADD
3.2 GEOPOS
3.3 GEODIST
3.4 GEORADIUS
4 原理解析
4.1 存儲結構
4.2 GeoHash編碼
4.3 編碼原理
4.4 總結
5 參考資料
1.業務背景
某天在工位上的我,正在敲著代碼,聽著歌,突然就被打斷了:
小G:快來看看!我們的訂單都被詐騙了!!!
我:What?什么情況?
小G:有些黑中介引導我們用戶下單租賃,把訂單機器寄到他們那里,拿到機器后再補貼給用戶一筆錢,然后這批機器我們就拿不回來啦!
我:emmmm...那這些訂單有沒有什么特征呢?
小G:噢也有,他們的下單的地址都是黑中介那邊指定的某地址,我們也是通過這部分集中下單的地址數據進行分析得知的。
我:噢那我有個想法,如果用戶的下單地址與黑中介指定下單地址相同,或者在其附近,是否就可以認為這個訂單有詐騙風險?
小G:可以!
于是,新的需求又開始了。
2.技術選型
要判斷用戶地址與中介地址是否相同,或者相近。那落到實際功能開發,其實就是計算兩個地址之間的距離,由距離長短決定是否相同,或相近。
那地址之間距離計算又如何實現呢?那當然是站在巨人的肩膀上開發啦,下面就來介紹下開發中常用的GEO(Geolocation)工具,以及他們之間的區別。
2.1 MySQL
2.1.1 優
- 兼容性:最常用的關系型數據之一。其與項目兼容度高,與其他業務數據(如用戶表、訂單表)天然集成,無需跨數據源查詢,通用性強。
- 持久性:數據持久化存儲,適合長期保存地址數據。
2.1.2 劣
- 性能:大數據量下(如百萬級以上的地址經緯度)的復雜查詢(地址空間計算)性能較低。
2.2 Redis
2.2.1 優
性能:基于內存存儲,查詢/數據操作延時極低,適合實時查詢/計算操作。GEO內部數據存儲結構為Sorted Set,支持高效的范圍查詢和排序。
擴展性:支持集群模式,適合分布式場景。
2.2.2 劣
- 存儲:內存容量有限,不適合長期存儲海量數據。
- 功能:不支持高級地理計算(如面積計算、地理圍欄計算)。
2.3 ElasticSearch
2.3.1 優
- 性能:分布式架構,適合海量數據和高并發場景。內部倒排索引和分片機制,優化查詢性能和保證容錯。
- 內置地址空間數據類型,支持復雜地址查詢(地理圍欄、距離排序、多邊形查詢等)
2.3.2 劣
- 復雜度:需要維護ES集群,開發學習成本較高
- 存儲:內存和磁盤占用較高,不適合小規模場景
3.Coding
經過上面的分析,結合需求的場景,首先保證性能,次要無須重量級的框架,開發成本低,同時還要能夠滿足地理計算的基本要求。于是,果斷選擇 Redis!
3.1 GEOADD
用于添加一個或多個地理位置信息(經緯度)
例子:添加一個key為gk,包含 天安門,故宮 的經緯度
圖片
3.2 GEOPOS
用于查詢某一個key中的指定地址經緯度
例子:查詢gk中 天安門 和 故宮 的經緯度
圖片
3.3 GEODIST
用于查詢同一個key兩個地址之間的距離
例子:查看gk中 天安門 和 故宮的距離(m)
圖片
3.4 GEORADIUS
用于查詢同一個key中指定地址范圍半徑內的地址
例子:查詢gk中以 天安門 為中心,半徑1000km的地址
圖片
Java代碼如下
@Resource
private Jodis jodis;
@Test
public void testGeo() {
String key = "gk";
String member1 = "TianAnMen";
String member2 = "GuGong";
// GEOADD
jodis.geoadd(key, 116.3974723219871521, 39.90882345602657466, member1);
jodis.geoadd(key, 116.39738649129867554, 39.91357605820034138, member2);
// GEOPOS
List<GeoCoordinate> geopos = jodis.geopos(key, member1, member2);
for (GeoCoordinate geopo : geopos) {
System.out.println(JSONUtil.toJsonStr(geopo));
}
// GEODIST
Double geodist = jodis.geodist(key, member1, member2);
System.out.println(geodist);
// GEORADIUS
List<GeoRadiusResponse> georadius = jodis.georadius(key, 116.39738649129867554, 39.91357605820034138, 1000, GeoUnit.KM);
for (GeoRadiusResponse georadiu : georadius) {
System.out.println(JSONUtil.toJsonStr(georadiu));
}
}
4.原理解析
從上面的示例來看,在使用的角度來說還是簡潔易懂的。所謂知其然,知其所以然,所以接下來我們再深究下,Redis的GEO是如何實現兩個地址的經緯度之間的距離計算的呢?
4.1 存儲結構
Redis的GEO底層實現采用的是Sorted Set有序集合結構,其中key存儲元素信息,value存儲經緯度(即權重)。而經緯度包含經度和緯度兩個信息,因此需要使用GeoHash編碼的方式將經緯度轉化成float類型進行存儲。
4.2 GeoHash編碼
上面提到了GeoHash編碼,其實是分別對經度和緯度進行編碼,然后再組合成一個新的編碼。這個方法叫:二分區間編碼
4.3 編碼原理
對于一個經緯度來說,經度的范圍是[-180, 180],緯度的的范圍是[-90, 90]。而GeoHash編碼針對兩個范圍進行N次(N可自定義)的二分區編碼,將其轉化成一個N位的二進制值。
以經度為例,在進行第一次二分區時,將經度范圍[-180, 180]進行二分,得到兩個區間 [-180, 0) 和 [0, 180]。然后判斷當前經度落在哪個區間,若落在左區間,則記錄為0;若落在右區間,則記錄為1。如此反復,每次都會得到一個二進制值。
例子:將經度(116.37)進行5次二分區后得到編碼值:11010(如圖下)
圖片
再將緯度(39.86)進行5次二分區后得到編碼值:10111(如圖下)
圖片
現在得到經緯度編碼之后的值,需要再將其組合成一個編碼。同時遵循組合規則(如圖下)
- 從左到右按順序,將經度編碼值逐個放入偶數位
- 從左到右按順序,將緯度編碼值逐個放入奇數位
圖片
最終兩個編碼值,轉化成了一個編碼值(1110011101),同時保存到Sorted Set的value中。至此,編碼完成。
4.4 總結
了解了GeoHash的編碼原理,那這樣編碼有什么用呢?下面來解答這個問題。
例子:我們把 經度區間[-180, 180],緯度區間[-90, 90] 都做一次二分區編碼,那么就會得到4個分區(如下圖)
經過一次二分區編碼后,本來是二維信息的經緯度,就簡化成了一維信息的編碼。換句話說,對于整個地理空間來說,所有的位置都能經過編碼變成平面上的一個點,多個點便能組成一條線,由此計算距離便有跡可循了。
而一次二分區的結果,便是圖中的4個方格,同時也對應了4個分區,每個分區都包含指定范圍的經緯度。那對于N次二分區來說,N越大,分區也越多,每個分區所包含的經緯度范圍就越小(所能覆蓋的地理空間越小),對應映射在一維空間上的點越小,點越小則越精準。
需要注意的是,雖然分區越多,經緯度在地理空間上代表的位置則越精準,但對于距離統計來說,并不是分區越多越好。
例子:還是延續上面一次二分區的例子進行舉例。這次我們把N+1,做二次二分區(如下圖)
圖片
上圖可以看到,經過二次二分區后,分區變成了16個。理論上對應地理空間上的位置更加精確了,那么將對應的編碼轉化為一維空間上的點后,連接成線。發現對于大部分的編碼值來說,在線上相鄰的編碼在空間上也是相鄰(如:0001,0010),但是對于某些編碼來說(如:0111,1000)在線上相鄰,但是在空間上卻相差較遠。因此,對于這兩個分區來說,如果只單純考慮計算一維空間上的距離,將會造成較大誤差。
所以基于以上情況,一般不會只計算編碼值的距離,還需要結合分區作為輔助計算。通常在計算過程中,會在經緯度指定的分區周圍同時再查詢附近的幾個分區,作為距離遠近的參考,提高距離計算的精度。
5.參考資料
[1] https://cloud.tencent.com/developer/article/1949540
關于作者
馮超,一名轉轉金融技術部后端開發程序猿