Redis中的Bitmaps、HyperLogLog、Geospatial

目录

Bitmaps

如何选择集合类型

 判断用户登录状态

 小结

HyperLogLog

什么是基数

应用场景

命令

Geospatial

如何实现定位

Geohash技术

geohash的计算

geohash的精度

geohash的边界问题

Redis对地理位置的支持

指令操作


Bitmaps

在移动应用的业务场景中,我们需要保存这样的信息:一个 key 关联了一个数据集合。

例如:

  • 根据用户id判断登陆状态
  • 用户的签到情况

通常情况下,我们面临的用户数量以及访问量都是巨大的,比如百万、千万级别的用户数量,或者千万级别、甚至亿级别的访问信息。

所以,我们必须要选择能够非常高效地统计大量数据(例如亿级)的集合类型。

如何选择集合类型

假如我们在判断用户是否登陆的场景中使用 Redis 的 String 类型实现(key -> userId,value -> 0 表示下线,1 - 登陆),假如存储 100 万个用户的登陆状态,如果以字符串的形式存储,就需要存储 100 万个字符串了,内存开销太大。

String 类型除了记录实际数据以外,还需要额外的内存记录数据长度、空间使用等信息。所以除了记录实际数据以外,其他都是额外开销。

合理地使用操作位能够有效地提高内存使用率和开发效率。java中位与操作是最快的,在Redis中同样道理,Redis提供了Bitmaps这个“数据类型”可以实现对位的操作:

  • Bitmaps本身不是一 种数据类型,实际上它就是字符串( key-value ),但是它可以对字符串的位进行操作。
  • Bitmaps单独提供了一套命令,所以在Redis中使用Bitmaps和使用字符串的方法不太相同。可以把 Bitmaps想象成一个以位为单位的数组 ,数组的每个元只能存储0和1,数组的下标在Bitmaps中叫做偏移量。
  • 8 个 bit 组成一个 Byte,所以 Bitmap 会极大地节省存储空间。 这就是 Bitmap 的优势。 

 判断用户登录状态

Bitmap 提供了 GETBIT、SETBIT 操作,通过一个偏移值 offset 对 bit 数组的 offset 位置的 bit 位进行读写操作,需要注意的是 offset 从 0 开始。

只需要一个 key为login 表示存储用户登陆状态集合数据, 将用户 ID 作为 offset,在线就设置为 1,下线设置 0。通过 GETBIT判断对应的用户是否在线。50000 万 用户只需要 6 MB 的空间。

setbit <key> <offset>设置对应KEY值的偏移量的值

第一步:1代表用户已经登陆

SETBIT login 10001 1

第二步:检查该用户是否登陆,返回值 1 表示已登录

GETBIT login 10001

第三步:退出,将 offset 对应的 value 设置成 0

SETBIT login 10001 0

 小结

只需要一个 bit 位就能表示 0 和 1。在统计海量数据的时候将大大减少内存占用。

HyperLogLog

在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV( PageView页面访问量) ,可以使用Redis的incr、incrby 轻松实现。
但像UV ( UniqueVisitor ,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题

解决技术问题的方案有很多种:

  • 数据存储在MYSQL表中,使用distinct count计算不重复个数
  • 使用Redis提供的hash、set、bitmaps等数据结构来梳理

以上的方案结果精确,但随着数据不断增加,导致空间占用越来越大,对于非常大的数据集是不切实际的。

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

什么是基数

比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。

应用场景

 HyperLogLog 主要的应用场景就是进行基数统计。这个问题的应用场景其实是十分广泛的。例如:对于 Google 主页面而言,同一个账户可能会访问 Google 主页面多次。于是,在诸多的访问流水中,如何计算出 Google 主页面每天被多少个不同的账户访问过就是一个重要的问题。那么对于 Google 这种访问量巨大的网页而言,其实统计出有十亿 的访问量或者十亿零十万的访问量其实是没有太多的区别的,因此,在这种业务场景下,为了节省成本,其实可以只计算出一个大概的值,而没有必要计算出精准的值。

对于上面的场景,可以使用HashMapBitMapHyperLogLog 来解决。对于这三种解决方案,这边做下对比:

  • HashMap:算法简单,统计精度高,对于少量数据建议使用,但是对于大量的数据会占用很大内存空间;
  • BitMap:位图算法,具体内容可以参考我的这篇文章,统计精度高,虽然内存占用要比HashMap少,但是对于大量数据还是会占用较大内存;
  • HyperLogLog :存在一定误差,占用内存少,稳定占用 12k 左右内存,可以统计 2^64 个元素,对于上面举例的应用场景,建议使用。

命令

pfadd <key> <element> [element ...] 添加指定元素到 HyperLogLog中,将所有元素添加到指定HyperLogLog数据结构中。如果执行命令后HLL估计的近似基数发生变化,则返回1,否则返回0。

 pfcount<key> [key ..]计算HLL的近似基数,可以计算多个HLL ,比如用HLL存储每天的UV ,计算一周的UV可以使用7天的UV合并计算即可

 pfmerge<destkey> <sourcekey> [sourcekey ..]将一个或多 个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来给并计算可得

 比如在网站中我们有两个内容差不多的页面,运营说需要这两个页面的数据进行合并。其中页面的 UV 访问量也需要合并,那这个时候 PFMERGE 就可以派上用场了,也就是同样的用户访问这两个页面则只算做一次

Geospatial

Redis 3.2中增加了对GEO类型的支持。GEO , Geographic ,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。

如何实现定位

说到定位,很多人第一反应应该是,实时上报经纬度,数据库中提前存储好所有的经纬度,然后用上报的经纬度和数据库中的经纬度进行比较,计算出附近的人或共享单车。这种做法需要循环遍历,数据库中的数据量大,查询慢,效率低。

那么,这些app是如何做到既能够精确定位,又能够实时查询的呢?答案就是使用geohash。redis的"数据类型"geospatial就能计算出geohash。redis使用geohash技术将实时上报的精度和纬度,通过一定的算法转化成最长12个字符的字符串,两个位置的经纬度计算的字符串的前缀越相同,则两个位置离得越近。这样一来就可以通过数据库的like加上geohash的前几位模糊查询数据库的数据了。比如ofo共享单车,数据库中用一张表t_bike专门存储ofo的每一辆车的编号no、经度longitude 、纬度latitude、geohash等字段,当每一辆车上报自己的经纬度时,同时计算一个geohash存到表中;当用户要用车时,上报用户的实时位置的经纬度,并计算一个hash值,比如hash=efgrtv98fjng,那么可以使用:

 select * from t_bike where geohash like 'efgrtv98%'

就可以找到附近有多少车了。like后面使用的hash位数越多,查找的范围越准确。

Geohash技术

geohash技术就是将经纬度转换成最长12个字符的字符串,同时两个位置越近,生成的字符串的前缀越一致。这是如何实现的呢?

例如,东方明珠的经纬度,东经121.506377,北纬31.245105。

下面就以东方明珠为例,简单说一下如何将这两个经纬度计算成一个hash字符串的。

geohash的计算

1.使用二分法生成二进制

将纬度(-90,90)分成两个区间,(-90,0)和(0,90),如果目标纬度落在左边区间则记为0,否则记为1;再将目标纬度所在的那个区间在通过二分法分成两个相等的区间,如果目标纬度落在左边区间则记为0,否则记为1,以此类推。

同样的,将经度(-180,180)也通过这种方式计算。

最终,经度和纬度计算后,分别得到一个由0和1组成的二进制。

假如,东方明珠的经纬度计算后,得到两个二进制位:

 经度:110101100101001110111100011010
 纬度:101011000101010000110101100101

 2.合并二进制

将上面的两个二进制按照“偶数位放经度,奇数位放纬度”的原则,从0位开始数起,合并成一个二进制。

可以理解成将纬度向后移动一位,然后将两行压成一行。

 结果:  111001100111100000110011000110101000111110110001011010011001

3.二进制转换成十进制

把上面合并后的60位二进制,按照从左往右,每5位划分成1个组,如果最后一组如果不足5位就用0补齐到5位。分组后所示:

 分组结果:  11100  11001  11100  00011  00110  00110  10100  01111  10110  00101  10100  11001

将上面的每组二进制分别转成十进制:

 十进制结果:  28  25  28  3  6  6  20  15  22  5  20  25

 4.十进制转base32字符串

使用base32编码表,将每个十进制数替换成编码表中的字符,获得一个字符串。

 转化后的字符串:

 base32字符串:4Z4CGGUPWFUZ

这就是模拟东方明珠的经纬度生产的geohash的值(不是真实值)。

geohash的精度

hash的字符串长1位-12位,对应精度的级别1-12级。字符串越长,位置越精确。

geohash的边界问题

比如现在是y1,根据geohash算法定位到y1所在的矩形块,返回附近的点就会得到y2。但是我们发现实际情况是,x, z虽然不在我们所在的那个矩形区域,但是x, z显然离我们更近。这就是geohash的边界问题,就是如果我们刚好处在矩形的边界处,那么离我们最近的点不一定是和我们在同一个矩形框中的点,很可能是旁边的矩形框中的某些点离我们更近。

 其实,就是将该区块上下左右以及四个对角的8个区块的hash都计算一遍,分别计算和自己之间的距离,找到最近的一个。因为这是的数据量已经非常小了,计算周边的8个块也很快。

Redis对地理位置的支持

可以通过编程的方式实现上面的计算过程,当然也可以直接使用redis计算geohash。

redis中的geospatial本质是使用sorted set存储的。使用的也是geohash技术。经度二进制和纬度二进制,通过上面介绍的“偶数位放经度,奇数位放纬度”的原则,形成一个独特的52位二进制。sorted set存储每一个成员时,会给每一个成员一个分数用于排序,分数值是一个双精度的64位浮点型数字字符串,它能包括的整数范围是-(2^53)+(2^53)。所以使用sorted set存储52位的二进制不会丢失精度。同时,这种格式的数据通过半径查询时,支持查询中间的1个区块和周边的8个区块,并丢弃半径意外的元素,可以解决geohash的边界问题。

实际使用时,可以提前将一份各地区的经纬度表格,通过redis命令导入到redis内存中,然后可以通过相关的redis命令计算每个地区的geohash,并且可以搜索指定范围内,redis中存在的地区有哪些,同时也可以计算两个经纬度之间的距离。

指令操作

1、geoadd<key> < longitude> <latitude> <member> [longitude latitude member..]添加地理位置(经度,纬度,名称)。

2、geopos <key><name> 获取制定城市的坐标

3、geodist <key> <member1> <member2> [m]|km|ft|mi ]获取两个位置 之间的直线距离,不指定单位参数,默认用米 

 4、georadius <key>< longitude> <latitude>radius m|km|ft|mi 以给定的经纬度为中心 ,找出某一半径内的元素

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

过街的老鼠

感谢你对诗仙女的打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值