阿里云Redis GEO地理位置功能上线啦

    xiaoxiao2026-05-12  2

    Redis 3.2版本一个重大的更新是新增了GEO地理位置相关的命令。ApsaraDB for Redis对地理位置的支持对应的版本也已经发布了,目前可以通过提工单升级版本来支持。目前Redis对地理位置支持提供了一下6个命令:

    geoadd: 增加地理位置的坐标。geodist: 获取两个地理位置的距离。geohash: 获取地理位置的GeoHash值。geopos: 获取地理位置的坐标。georadius: 根据给定经纬度坐标获取指定范围内的地理位置集合。georadiusbymember: 根据给定地理位置获取指定范围内的地理位置集合。

    GeoHash基本原理

    GeoHash是一种地址编码,通过切分地图区域为小方块(切分次数越多,精度越高),它能把二维的经纬度编码成一维的字符串。也就是说,理论上geohash字符串表示的并不是一个点,而是一个矩形区域,只要矩形区域足够小,达到所需精度即可。

    编码过程

    以经纬度(116.3906,39.92324)为例:

    对于维度39.92324, 39.92324属于(0, 90),所以取编码为1。然后再将(0, 90)分成 (0, 45), (45, 90)两个区间,而39.92324位于(0, 45),所以编码为0。以此类推,直到精度符合要求为止,得到纬度编码为1011 1000 1100 0111 1001。经度也用同样的算法,对(-180, 180)依次细分,得到116.3906的编码为1101 0010 1100 0100 0100。接下来将经度和纬度的编码合并,奇数位是纬度,偶数位是经度,得到编码 11100 11101 00100 01111 00000 01101 01011 00001。最后,用0-9、b-z(去掉a, i, l, o)这32个字母进行base32编码,得到(39.92324, 116.3906)的编码为wx4g0ec1。

    (116.3906,39.92324)对应的地图位置就是http://geohash.org/wx4g0ec1经纬度为,降低一些精度,就会是http://geohash.org/wx4g0ec,再降低一些精度,就会是http://geohash.org/wx4g0。

    编码特性

    不难看出这样的编码方式仅用一个字符串保存经纬度信息,并且精度由字符串从头到尾的长度决定,编码长度越长,精度越高。GeoHash值的前缀相同的位数越多,代表的位置越接近,可以方便索引。(反之不成立,位置接近的GeoHash值不一定相似).但这种方案的缺点是:从geohash的编码算法中可以看出,靠近每个方块边界两侧的点虽然十分接近,但所属的编码会完全不同。实际应用中,需要通过去搜索环绕当前方块周围的8个方块来解决该问题。除此之外,这个方案也无法直接得到距离,需要程序协助进行后续的排序计算。

    具体的可以参考一下几个文档:

    GeoHash核心原理解析几个地理位置信息处理方案的对比和分析geohash算法原理及实现方式GeoHash wikiRedis Geo

    Redis Geo命令实现

    Redis将地理位置的52位GeoHash值作为有序集合的score,将地理位置存放在有序集合中进行保存。后续按位置搜索时,依据GeoHash的特性搜索当前方块与环绕当前方块的8个方块来搜索目标位置集合。

    GEOADD

    增加地理位置坐标,命令格式如下:

    GEOADD key longitude latitude member [longitude latitude member ...]

    Redis中接受的有效的精度范围为-180到180度,有效维度范围为-85.05112878到 85.05112878度(靠近南北极的一小块地方是无法生成索引的)。

    实现方式:

    Redis内部使用有序集合来保存key,每一个member的score大小为一个52位的Geohash值(double类型精度为52位)。实际上Redis内部实现的时候就是将GEOADD命令转换成ZADD命令来实现的。(这也解释了为什么没有专门的georem命令,地理位置信息是通过使用ZREM命令来删除成员。)GEOADD命令的实现如下

    void geoaddCommand(client *c) { ... int elements = (c->argc - 2) / 3; int argc = 2+elements*2; /* ZADD key score ele ... */ robj **argv = zcalloc(argc*sizeof(robj*)); argv[0] = createRawStringObject("zadd",4); argv[1] = c->argv[1]; /* key */ incrRefCount(argv[1]); /* Create the argument vector to call ZADD in order to add all * the score,value pairs to the requested zset, where score is actually * an encoded version of lat,long. */ int i; for (i = 0; i < elements; i++) { double xy[2]; if (extractLongLatOrReply(c, (c->argv+2)+(i*3),xy) == C_ERR) { for (i = 0; i < argc; i++) if (argv[i]) decrRefCount(argv[i]); zfree(argv); return; } /* Turn the coordinates into the score of the element. */ GeoHashBits hash; geohashEncodeWGS84(xy[0], xy[1], GEO_STEP_MAX, &hash); GeoHashFix52Bits bits = geohashAlign52Bits(hash); robj *score = createObject(OBJ_STRING, sdsfromlonglong(bits)); robj *val = c->argv[2 + i * 3 + 2]; argv[2+i*2] = score; argv[3+i*2] = val; incrRefCount(val); } /* Finally call ZADD that will do the work for us. */ replaceClientCommandVector(c,argc,argv); zaddCommand(c); }

    例子

    redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" (integer) 2 redis> GEODIST Sicily Palermo Catania "166274.15156960039" redis> GEORADIUS Sicily 15 37 100 km 1) "Catania" redis> GEORADIUS Sicily 15 37 200 km 1) "Palermo" 2) "Catania"

    GEODIST

    返回两点间距离,命令格式如下

    GEODIST key member1 member2 [unit]

    单位可选项为m(米,默认值), km(千米),mi(英里),ft(英尺)。返回double值,若有member不存在,则返回NULL.

    实现方式:

    使用WGS84坐标系统,计算距离时使用Haversine公式。由于地球并不是严格标准的,计算出来的距离有最大约0.5%的误差。

    例子:

    redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" (integer) 2 redis> GEODIST Sicily Palermo Catania "166274.15156960039" redis> GEODIST Sicily Palermo Catania km "166.27415156960038" redis> GEODIST Sicily Palermo Catania mi "103.31822459492736" redis> GEODIST Sicily Foo Bar (nil)

    GEOHASH

    返回key中对应成员的geohash值。命令格式如下:

    GEOHASH key member [member ...]

    实现方式:

    Redis在内部生成有序集合成员score时的geohash值与标准的算法略有差异(Redis内部使用-85,85作为维度范围,标准使用-90,90)。这个命令返回的是标准值,与https://en.wikipedia.org/wiki/Geohash中标准算法和geohash.org网站的结果一致。代码如下:

    /* Get Score */ zsetScore(zobj, c->argv[j], &score); /* The internal format we use for geocoding is a bit different * than the standard, since we use as initial latitude range * -85,85, while the normal geohashing algorithm uses -90,90. * So we have to decode our position and re-encode using the * standard ranges in order to output a valid geohash string. */ /* Decode... */ double xy[2]; if (!decodeGeohash(score,xy)) { addReply(c,shared.nullbulk); continue; } /* Re-encode */ GeoHashRange r[2]; GeoHashBits hash; r[0].min = -180; r[0].max = 180; r[1].min = -90; r[1].max = 90; geohashEncode(&r[0],&r[1],xy[0],xy[1],26,&hash);

    例子

    redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" (integer) 2 redis> GEOHASH Sicily Palermo Catania 1) "sqc8b49rny0" 2) "sqdtr74hyu0"

    GEOPOS

    获取地理位置的经纬度坐标,命令格式如下:

    GEOPOS key member [member ...]

    经纬度坐标是被转成52位的GeoHash保存起来的,返回的时候重新解码成经纬度坐标。由于精度问题,返回值可能与设置的值略有差异。

    例子

    redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" (integer) 2 redis> GEOPOS Sicily Palermo Catania NonExisting 1) 1) "13.361389338970184" 2) "38.115556395496299" 2) 1) "15.087267458438873" 2) "37.50266842333162" 3) (nil)

    GEORADIUS, GEORADIUSBYMEMBER

    获取指定范围内的地理位置集合,命令格式如下:

    GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key] GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]

    GEORADIUS与GEORADIUSBYMEMBER,前一个是获取任意经纬度周围的地理集合,后一个是获取某个地理位置周围的地理位置集合。它们的内部实现和可选参数是一致的。可选项:WITHDIST: 同时返回地理位置与给定位置的距离WITHCOORD: 同时返回地理位置的经纬度坐标WITHHASH: 同时返回Redis内部的GeoHash值(非标准算法值),一般用于debugASC|DESC:结果按距离升降序排序STORE|STOREDIST: 结果存到新的有序集合中,前者以GeoHash值做score,后者以与指定位置的距离作score,该选项与WITH[DIST|COORD|HASH]选项冲突

    实现方式:

    GeoHash值的前缀相同的位数越多,代表的位置越接近,可以方便索引。但反之不成立,位置接近的GeoHash值不一定相似。靠近每个方块边界两侧的点虽然十分接近,但所属的编码会完全不同。实际应用中,需要通过去搜索环绕当前方块周围的8个方块来解决该问题。搜索的时候会检查挡墙方块+8个覆盖整个搜索半径的区域,不断的去除geohash的低位,直到这9个方块能覆盖搜索半径位置。再一次搜索计算每个位置的距离。

    例子:

    GEORADIUS:

    redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" (integer) 2 redis> GEORADIUS Sicily 15 37 200 km WITHDIST 1) 1) "Palermo" 2) "190.4424" 2) 1) "Catania" 2) "56.4413" redis> GEORADIUS Sicily 15 37 200 km WITHCOORD 1) 1) "Palermo" 2) 1) "13.361389338970184" 2) "38.115556395496299" 2) 1) "Catania" 2) 1) "15.087267458438873" 2) "37.50266842333162" redis> GEORADIUS Sicily 15 37 200 km WITHDIST WITHCOORD 1) 1) "Palermo" 2) "190.4424" 3) 1) "13.361389338970184" 2) "38.115556395496299" 2) 1) "Catania" 2) "56.4413" 3) 1) "15.087267458438873" 2) "37.50266842333162"

    GEORADIUSBYMEMBER:

    redis> GEOADD Sicily 13.583333 37.316667 "Agrigento" (integer) 1 redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" (integer) 2 redis> GEORADIUSBYMEMBER Sicily Agrigento 100 km 1) "Agrigento" 2) "Palermo" 相关资源:python入门教程(PDF版)
    最新回复(0)