Redis之GeoHash根据经纬度距离排序

【位置排序相关的需求】
其实这种需求是比较多的,这里举一个简单的场景。比如我们在全国各地有很多充电站,每个充电站在数据库里都有对应的省市县以及经度纬度,在对充电站维护的时候经常会依据电工的即时位置查看周边有哪些充电站,并对这些充电站的位置参照电工的即时位置计算距离,并根据距离大小进行排序。

首先,我们需要确定一个范围半径r,而后根据经纬度计算这个范围内的所有充电站,sql语句如下:
select * from charge_station where x0-r<longitude<x0+r and y0-r<latitude<y0+r
而后对这一批数据与电工的位置点进行距离计算并排序,但这样的查询语句无论是性能还是距离直观性以及分页按数据查找都不理想。
【GeoHash算法思路】
GeoHash的思路是将经纬度这种二维坐标映射到一维,也就是变成了一条线,二维平面上距离近的元素在一维线上距离也更近,此时只需要在这个一维的线上获取附近的站点就行了。
在Redis中,经纬度会被编码放入zset中,value是元素的key,score是GeoHash的52位编码。此时计算附近的站点只需要根据zset的score进行排序,而后还可以将score还原为坐标值。
【Geo指令】
添加:
geoadd station 116.48105 39.996794 station_id_one
geoadd station 116.48105 39.996794 station_id_two
距离:geodist指令可以用来计算两个已知元素的距离,如:
geodist station station_id_one station_id_two km #携带key,2个元素的value以及距离单位
还原坐标:
geopos station station_id_one
获取元素的hash值
geohash station station_id_one

【georadius指令】
单独看看最关键的附近的站点查询指令:
georadiusbymember station station_id_one 20 km count 3 asc
#表示站点station_id_one附近20km的距离正序排序,只取3个,它不会排除自身,当然也可换为desc倒序排列
可以添加的参数:
WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。
WITHCOORD: 将位置元素的经度和维度也一并返回。
WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
georadisbymember station station_id_one 20 km withcoord withdist withhash count 3 asc

另外,如果是电工的坐标,我们是不方便直接放入这个集合中了,redis也提供了根据坐标来查询附近的站点:
georadisbymember station 116.514202 39.905409 20 km withdist count 3 asc

【注意事项】
对于数据量很大的集合,最好不要全部投放在一个zset中,不利用集群的迁移。单个key中的数据不宜超过1MB。建议Geo数据使用单独的Redis实例部署,不使用集群环境。数据量过大时,我们可以按照行政单位等维度进行划分,可以显著降低单个zset集合大小。

【代码实现】

package com.redis.geohash;

import redis.clients.jedis.GeoCoordinate;
import redis.clients.jedis.GeoRadiusResponse;
import redis.clients.jedis.GeoUnit;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.GeoRadiusParam;

import java.util.List;

public class GeoHashTest {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        jedis.auth("123456");
        jedis.geoadd("station", 116.48105, 39.996794, "园艺山站点");
        jedis.geoadd("station", 116.514203, 39.905409, "游仙站点");
        jedis.geoadd("station", 116.514203, 39.905409, "石桥铺站点");
        jedis.geoadd("station", 116.562108, 39.787602, "花荄站点");
        jedis.geoadd("station", 116.334255, 40.027400, "三台县站点");

        Double geodist = jedis.geodist("station", "园艺山站点", "石桥铺站点");
        System.out.println("geodist = " + geodist);
        System.out.println("--------------------------------------");
        List<GeoCoordinate> geopos = jedis.geopos("station", "花荄站点", "三台县站点");
        System.out.println("geopos.toString() = " + geopos.toString());
        System.out.println("--------------------------------------");
        //计算100KM以内的所有站点
        List<GeoRadiusResponse> georadius = jedis.georadius("station", 116.334212, 39.992813, 100, GeoUnit.KM,
                GeoRadiusParam.geoRadiusParam().withDist().withCoord().sortAscending());
        for (int i = 0; i < georadius.size(); i++) {
            GeoRadiusResponse geoRadiusResponse = georadius.get(i);
            System.out.println("geoRadiusResponse.getMember() = " + geoRadiusResponse.getMemberByString());
            System.out.println("geoRadiusResponse.getDistance() = " + geoRadiusResponse.getDistance());
        }
    }
}

【参考】

《Redis深度历险 核心原理与应用实践》

原文地址:https://www.cnblogs.com/bruceChan0018/p/15685422.html