10-应用 8:近水楼台 —— GeoHash
课程
1
开篇:授人以鱼不若授人以渔 —— Redis 可以用来做什么?
学习时长: 5分21秒
2
基础:万丈高楼平地起 —— Redis 基础数据结构
上次学到
学习时长: 16分14秒
3
应用 1:千帆竞发 —— 分布式锁
学习时长: 7分47秒
4
应用 2:缓兵之计 —— 延时队列
学习时长: 8分9秒
5
应用 3:节衣缩食 —— 位图
学习时长: 8分52秒
6
应用 4:四两拨千斤 —— HyperLogLog
学习时长: 14分17秒
7
应用 5:层峦叠嶂 —— 布隆过滤器
学习时长: 17分54秒
8
应用 6:断尾求生 —— 简单限流
学习时长: 4分37秒
9
应用 7:一毛不拔 —— 漏斗限流
学习时长: 7分22秒
10
应用 8:近水楼台 —— GeoHash
学习时长: 7分52秒
11
应用 9:大海捞针 —— Scan
学习时长: 8分42秒
12
原理 1:鞭辟入里 —— 线程 IO 模型
学习时长: 4分1秒
13
原理 2:交头接耳 —— 通信协议
学习时长: 3分34秒
14
原理 3:未雨绸缪 —— 持久化
学习时长: 5分27秒
15
原理 4:雷厉风行 —— 管道
学习时长: 3分51秒
16
原理 5:同舟共济 —— 事务
学习时长: 6分36秒
17
原理 6:小道消息 —— PubSub
学习时长: 7分7秒
18
原理 7:开源节流 —— 小对象压缩
学习时长: 7分14秒
19
原理 8:有备无患 —— 主从同步
学习时长: 4分9秒
20
集群 1:李代桃僵 —— Sentinel
学习时长: 3分52秒
21
集群 2:分而治之 —— Codis
学习时长: 7分28秒
22
集群 3:众志成城 —— Cluster
学习时长: 8分38秒
23
拓展 1:耳听八方 —— Stream
学习时长: 13分40秒
24
拓展 2:无所不知 —— Info 指令
学习时长: 4分4秒
25
拓展 3:拾遗补漏 —— 再谈分布式锁
学习时长: 2分18秒
26
拓展 4:朝生暮死 —— 过期策略
学习时长: 2分21秒
27
拓展 5:优胜劣汰 —— LRU
学习时长: 4分34秒
28
拓展 6:平波缓进 —— 懒惰删除
学习时长: 2分13秒
29
拓展 7:妙手仁心 —— 优雅地使用 Jedis
学习时长: 6分35秒
30
拓展 8:居安思危 —— 保护 Redis
学习时长: 2分19秒
31
拓展 9:隔墙有耳 —— Redis 安全通信
学习时长: 6分34秒
32
拓展 10:法力无边 —— Redis Lua 脚本执行原理
学习时长: 9分24秒
33
拓展 11:短小精悍 —— 命令行工具的妙用
学习时长: 9分21秒
34
源码 1:丝分缕析 —— 探索「字符串」内部
学习时长: 5分20秒
35
源码 2:循序渐进 —— 探索「字典」内部
学习时长: 7分24秒
36
源码 3:挨肩迭背 —— 探索「压缩列表」内部
学习时长: 10分42秒
37
源码 4:风驰电掣 —— 探索「快速列表」内部
学习时长: 3分49秒
38
源码 5:凌波微步 —— 探索「跳跃列表」内部
学习时长: 9分57秒
39
源码 6:破旧立新 —— 探索「紧凑列表」内部
学习时长: 2分42秒
40
源码 7:金枝玉叶 —— 探索「基数树」内部
学习时长: 5分36秒
41
源码 8:精益求精 —— LFU vs LRU
学习时长: 8分4秒
42
源码 9:如履薄冰 —— 懒惰删除的巨大牺牲
学习时长: 9分53秒
43
源码 10:跋山涉水 —— 深入字典遍历
学习时长: 9分24秒
44
源码 11:见缝插针 —— 探索 HyperLogLog 内部
学习时长: 13分3秒
45
尾声:百尺竿头 —— 继续深造指南
学习时长: 2分32秒
juejin_logo copyCreated with Sketch.

应用 8:近水楼台 —— GeoHash

Redis 在 3.2 版本以后增加了地理位置 GEO 模块,意味着我们可以使用 Redis 来实现摩拜单车「附近的 Mobike」、美团和饿了么「附近的餐馆」这样的功能了。

用数据库来算附近的人

地图元素的位置数据使用二维的经纬度表示,经度范围 (-180, 180],纬度范围 (-90, 90],纬度正负以赤道为界,北正南负,经度正负以本初子午线 (英国格林尼治天文台) 为界,东正西负。比如掘金办公室在望京 SOHO,它的经纬度坐标是 (116.48105,39.996794),都是正数,因为中国位于东北半球。

当两个元素的距离不是很远时,可以直接使用勾股定理就能算得元素之间的距离。我们平时使用的「附近的人」的功能,元素距离都不是很大,勾股定理算距离足矣。不过需要注意的是,经纬度坐标的密度不一样 (地球是一个椭圆),勾股定律计算平方差时之后再求和时,需要按一定的系数比加权求和,如果不求精确的话,也可以不必加权。

问题:经度总共360度,维度总共只有180度,为什么距离密度不是2:1?

现在,如果要计算「附近的人」,也就是给定一个元素的坐标,然后计算这个坐标附近的其它元素,按照距离进行排序,该如何下手?

如果现在元素的经纬度坐标使用关系数据库 (元素 id, 经度 x, 纬度 y) 存储,你该如何计算?

首先,你不可能通过遍历来计算所有的元素和目标元素的距离然后再进行排序,这个计算量太大了,性能指标肯定无法满足。一般的方法都是通过矩形区域来限定元素的数量,然后对区域内的元素进行全量距离计算再排序。这样可以明显减少计算量。如何划分矩形区域呢?可以指定一个半径 r,使用一条 SQL 就可以圈出来。当用户对筛出来的结果不满意,那就扩大半径继续筛选。

select id from positions where x0-r < x < x0+r and y0-r < y < y0+r

为了满足高性能的矩形区域算法,数据表需要在经纬度坐标加上双向复合索引 (x, y),这样可以最大优化查询性能。

但是数据库查询性能毕竟有限,如果「附近的人」查询请求非常多,在高并发场合,这可能并不是一个很好的方案。

GeoHash 算法

业界比较通用的地理位置距离排序算法是 GeoHash 算法,Redis 也使用 GeoHash 算法。GeoHash 算法将二维的经纬度数据映射到一维的整数,这样所有的元素都将在挂载到一条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近。当我们想要计算「附近的人时」,首先将目标位置映射到这条线上,然后在这个一维的线上获取附近的点就行了。

那这个映射算法具体是怎样的呢?它将整个地球看成一个二维平面,然后划分成了一系列正方形的方格,就好比围棋棋盘。所有的地图元素坐标都将放置于唯一的方格中。方格越小,坐标越精确。然后对这些方格进行整数编码,越是靠近的方格编码越是接近。那如何编码呢?一个最简单的方案就是切蛋糕法。设想一个正方形的蛋糕摆在你面前,二刀下去均分分成四块小正方形,这四个小正方形可以分别标记为 00,01,10,11 四个二进制整数。然后对每一个小正方形继续用二刀法切割一下,这时每个小小正方形就可以使用 4bit 的二进制整数予以表示。然后继续切下去,正方形就会越来越小,二进制整数也会越来越长,精确度就会越来越高。

上面的例子中使用的是二刀法,真实算法中还会有很多其它刀法,最终编码出来的整数数字也都不一样。

编码之后,每个地图元素的坐标都将变成一个整数,通过这个整数可以还原出元素的坐标,整数越长,还原出来的坐标值的损失程度就越小。对于「附近的人」这个功能而言,损失的一点精确度可以忽略不计。

GeoHash 算法会继续对这个整数做一次 base32 编码 (0-9,a-z 去掉 a,i,l,o 四个字母) 变成一个字符串。在 Redis 里面,经纬度使用 52 位的整数进行编码,放进了 zset 里面,zset 的 value 是元素的 key,score 是 GeoHash 的 52 位整数值。zset 的 score 虽然是浮点数,但是对于 52 位的整数值,它可以无损存储。

在使用 Redis 进行 Geo 查询时,我们要时刻想到它的内部结构实际上只是一个 zset(skiplist)。通过 zset 的 score 排序就可以得到坐标附近的其它元素 (实际情况要复杂一些,不过这样理解足够了),通过将 score 还原成坐标值就可以得到元素的原始坐标。

Redis 的 Geo 指令基本使用

Redis 提供的 Geo 指令只有 6 个,读者们瞬间就可以掌握。使用时,读者务必再次想起,它只是一个普通的 zset 结构。

增加

geoadd 指令携带集合名称以及多个经纬度名称三元组,注意这里可以加入多个三元组

127.0.0.1:6379> geoadd company 116.48105 39.996794 juejin
(integer) 1
127.0.0.1:6379> geoadd company 116.514203 39.905409 ireader
(integer) 1
127.0.0.1:6379> geoadd company 116.489033 40.007669 meituan
(integer) 1
127.0.0.1:6379> geoadd company 116.562108 39.787602 jd 116.334255 40.027400 xiaomi
(integer) 2

也许你会问为什么 Redis 没有提供 geo 删除指令?前面我们提到 geo 存储结构上使用的是 zset,意味着我们可以使用 zset 相关的指令来操作 geo 数据,所以删除指令可以直接使用 zrem 指令即可。

距离

geodist 指令可以用来计算两个元素之间的距离,携带集合名称、2 个名称和距离单位。

127.0.0.1:6379> geodist company juejin ireader km
"10.5501"
127.0.0.1:6379> geodist company juejin meituan km
"1.3878"
127.0.0.1:6379> geodist company juejin jd km
"24.2739"
127.0.0.1:6379> geodist company juejin xiaomi km
"12.9606"
127.0.0.1:6379> geodist company juejin juejin km
"0.0000"

我们可以看到掘金离美团最近,因为它们都在望京。距离单位可以是 m、km、ml、ft,分别代表米、千米、英里和尺。

获取元素位置

geopos 指令可以获取集合中任意元素的经纬度坐标,可以一次获取多个。

127.0.0.1:6379> geopos company juejin
1) 1) "116.48104995489120483"
   2) "39.99679348858259686"
127.0.0.1:6379> geopos company ireader
1) 1) "116.5142020583152771"
   2) "39.90540918662494363"
127.0.0.1:6379> geopos company juejin ireader
1) 1) "116.48104995489120483"
   2) "39.99679348858259686"
2) 1) "116.5142020583152771"
   2) "39.90540918662494363"

我们观察到获取的经纬度坐标和 geoadd 进去的坐标有轻微的误差,原因是 geohash 对二维坐标进行的一维映射是有损的,通过映射再还原回来的值会出现较小的差别。对于「附近的人」这种功能来说,这点误差根本不是事。

获取元素的 hash 值

geohash 可以获取元素的经纬度编码字符串,上面已经提到,它是 base32 编码。 你可以使用这个编码值去 geohash.org/${hash}中进行直… geohash 的标准编码值。

127.0.0.1:6379> geohash company ireader
1) "wx4g52e1ce0"
127.0.0.1:6379> geohash company juejin
1) "wx4gd94yjn0"

让我们打开地址 geohash.org/wx4g52e1ce0…

很好,就是这个位置,非常准确。

附近的公司

georadiusbymember 指令是最为关键的指令,它可以用来查询指定元素附近的其它元素,它的参数非常复杂。

# 范围 20 公里以内最多 3 个元素按距离正排,它不会排除自身
127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 asc
1) "ireader"
2) "juejin"
3) "meituan"
# 范围 20 公里以内最多 3 个元素按距离倒排
127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 desc
1) "jd"
2) "meituan"
3) "juejin"
# 三个可选参数 withcoord withdist withhash 用来携带附加参数
# withdist 很有用,它可以用来显示距离
127.0.0.1:6379> georadiusbymember company ireader 20 km withcoord withdist withhash count 3 asc
1) 1) "ireader"
   2) "0.0000"
   3) (integer) 4069886008361398
   4) 1) "116.5142020583152771"
      2) "39.90540918662494363"
2) 1) "juejin"
   2) "10.5501"
   3) (integer) 4069887154388167
   4) 1) "116.48104995489120483"
      2) "39.99679348858259686"
3) 1) "meituan"
   2) "11.5748"
   3) (integer) 4069887179083478
   4) 1) "116.48903220891952515"
      2) "40.00766997707732031"

除了 georadiusbymember 指令根据元素查询附近的元素,Redis 还提供了根据坐标值来查询附近的元素,这个指令更加有用,它可以根据用户的定位来计算「附近的车」,「附近的餐馆」等。它的参数和 georadiusbymember 基本一致,除了将目标元素改成经纬度坐标值。

127.0.0.1:6379> georadius company 116.514202 39.905409 20 km withdist count 3 asc
1) 1) "ireader"
   2) "0.0000"
2) 1) "juejin"
   2) "10.5501"
3) 1) "meituan"
   2) "11.5748"

小结 & 注意事项

在一个地图应用中,车的数据、餐馆的数据、人的数据可能会有百万千万条,如果使用 Redis 的 Geo 数据结构,它们将全部放在一个 zset 集合中。在 Redis 的集群环境中,集合可能会从一个节点迁移到另一个节点,如果单个 key 的数据过大,会对集群的迁移工作造成较大的影响,在集群环境中单个 key 对应的数据量不宜超过 1M,否则会导致集群迁移出现卡顿现象,影响线上服务的正常运行。

所以,这里建议 Geo 的数据使用单独的 Redis 实例部署,不使用集群环境。

如果数据量过亿甚至更大,就需要对 Geo 数据进行拆分,按国家拆分、按省拆分,按市拆分,在人口特大城市甚至可以按区拆分。这样就可以显著降低单个 zset 集合的大小。

留言
Ctrl + Enter
全部评论(82)
BinK_1783的头像
删除
海水有尽头,月亮有圆缺,人间有不足,有你都能弥补
点赞
回复
optional的头像
删除
开发狗 @ haha
大key确实是一个问题
点赞
回复
亽閑屁亊哆的头像
删除
Java Siege Lion @ IT Zoo
GeoHash 适用于附近人/车辆/餐馆等场景
2
1
删除
lzy 😂
点赞
回复
moyuhunrizi的头像
删除
之前用redis做过附近的人,后来门店列表因为想把门店的一些显示信息也和经纬度存在一起,权衡之后用mongodb实现的
1
2
删除
如果遇到高并发查询导致反复查库怎么办?
点赞
回复
删除
redis的geohash功能获得附近门店ID列表, 然后用ID列表查数据库获得详情, 这样不香么?
4
回复
柳双六的头像
删除
少讲了一个:
**附近成员 GEORADIUS**
```
GEORADIUS key longitude latitude radius unit [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count]
```
以给定的经纬度为中心, 返回位于指定半径内的成员。
展开
2
1
删除
优秀
点赞
回复
lubang34的头像
删除
民工 @ 民工
感觉用es做比在redis做好
点赞
1
删除
可以说一下好在哪里吗?
1
回复
一块豆腐皮的头像
删除
老钱,那么这个georadius company 116.514202 39.905409 20 km withdist count 3 asc 如果找所有呢?如何做到分页查询?
1
回复
一块豆腐皮的头像
删除
老钱,那么这个georadius company 116.514202 39.905409 20 km withdist count 3 asc 如果找所有呢?
1
回复
eukire的头像
删除
Ctrl+C/V熟练工 @ 某不知名搬砖工厂
如果数据量过亿甚至更大,就需要对 Geo 数据进行拆分,按国家拆分、按省拆分,按市拆分,在人口特大城市甚至可以按区拆分。这样就可以显著降低单个 zset 集合的大小。
------------------
如果按省拆分了,两个省相邻的地方怎么办?很好奇...
21
1
删除
维护一个图结构,同时查询多个zset,整合结果应该就可以了
点赞
回复
挡不住的牛味浓的头像
删除
java开发工程师 @ 喜马拉雅
跟mongodb比起来哪个效率更高呢
点赞
回复
鱼丸粗面不想说话的头像
删除
java开发 @ 保密
学习了,受益匪浅
2
回复
小圆子同学的头像
删除
我是测试同学,老师写的让我知道以后在公司更好的和开发交流,更好的去积累这块的只是,谢谢老钱
点赞
回复
海子同志的头像
删除
数据大的时候需要拆分set,这样会有问题,如果按区域,区域与区域结合处是有问题的,不同的set应该无法参与共同计算?
点赞
回复
pengmingfa521的头像
删除
很666...以前都不知道附近的人..这个功能是怎么实现了.现在明白啦..哈哈
1
回复
钱同志的头像
删除
很棒,特定结构针对特定问题给出适当的解决方案。
Redis Geo适合解决地图坐标相关的问题,本质是zset,提供一些特殊的指令。针对附近的人、医院、银行、餐馆、公园、交通站等问题,适合解决。目前还未涉及到相关问题,先扩张知识面。
5
回复
王维同学41025的头像
删除
georadiusbymember company ireader 20 km count 3 desc 我想问这个是以ireader为中心圆形范围吗,如果是的话,20km是半径还是直径?
点赞
1
删除
半径,radius 看单词的意思明白了。
点赞
回复
守♥护的头像
删除
将二维处理到一维没问题,但地球是三维的,除非将球面展开为二维,否则不是必然会牺牲一维的精度吗
5
1
删除
这就是地理上的问题了 实际上经纬就是将三维二维化了
点赞
回复
深红色水杯的头像
删除
请问这个地理坐标取得是什么地图标准的?
3
回复
linuxea的头像
删除
著名开发工程师
集群迁移????请问不应该是分布式吗?
点赞
1
删除
正是因为分布式,可能会集群扩容或其他情况导致集合的迁移。
点赞
回复
凌晨十三点的头像
删除
一直用的ES集群做经纬度的运算。在百亿数据量情况下运算也不会影响用户体验。只是配置ES集群的时候和建索引的时候需要考虑到你的数据的体量来配置分片量
8
1
删除
能分享下嘛
点赞
回复

查看全部 82 条回复