第3章 小功能大用处-Bitmaps、HyperLogLog、GEO


1.Bitmaps

1.1数据结构模型

现代计算机用二进制(位)作为信息的基础单位,1个字节等于8位,例如“big”字符串是由3个字节组成,但实际在计算机存储时将其用二进制表示,“big”分别对应的ASCII码分别是98、105、103,对应的二进制分别是01100010、01101001和01100111,如下图所示。
在这里插入图片描述
Redis提供了Bitmaps这个“数据结构”可以实现对位的操作。把数据结构加上引号主要因为:

  • Bitmaps本身不是一种数据结构,实际上它就是字符串,但是它可以对字符串的位进行操作。
  • Bitmaps单独提供了一套命令,所以在Redis中使用Bitmaps和使用字符串的方法不太相同。可以把Bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在Bitmaps中叫做偏移量
    在这里插入图片描述

1.2命令

1.2.1设置值:setbit key offset value

时间复杂度:O(1)
设置键的第offset个位的值(从0算起)
假设现在有20个用户,userid=0,5,11,15,19的用户对网站进行了访问,那么当前Bitmaps初始化结果如下图所示
在这里插入图片描述

127.0.0.1:6379> setbit unique:users:2016-04-05 0 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 5 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 11 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 15 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 19 1
(integer) 0

在第一次初始化Bitmaps时,假如偏移量非常大,那么整个初始化过程执行会比较慢,可能会造成Redis的阻塞。

1.2.2.获取值:gitbit key offset //获取键的第offset位的值(从0开始算)

时间复杂度:O(1)
操作获取id=8的用户是否在2016-04-05这天访问过,返回0说明没有访问:

127.0.0.1:6379> getbit unique:users:2016-04-05 8
(integer) 0

由于offset=1000000根本就不存在,所以返回结果也是0:

127.0.0.1:6379> getbit unique:users:2016-04-05 1000000
(integer) 0

1.2.3.获取Bitmaps指定范围值为1的个数:bitcount [start][end]

时间复杂度:O(N)
下面操作计算2016-04-05这天的独立访问用户数量:

127.0.0.1:6379> bitcount unique:users:2016-04-05
(integer) 5

[start]和[end]代表起始和结束字节数,下面操作计算用户id在第1个字节到第3个字节之间的独立访问用户数,对应的用户id是11,15,19。

127.0.0.1:6379> bitcount unique:users:2016-04-05 1 3
(integer) 3

1.2.4Bitmaps间的运算:bitop op destkey key[key…]

时间复杂度:O(N)
bitop是一个复合操作,它可以做多个Bitmaps的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destkey中。假设2016-04-04访问网站的userid=1,2,5,9,如下图所示。
在这里插入图片描述
and(交集)
下面操作计算出2016-04-04和2016-04-03两天都访问过网站的用户数量

127.0.0.1:6379> bitop and unique:users:and:2016-04-04_03 unique: users:2016-04-03
unique:users:2016-04-03
(integer) 2
127.0.0.1:6379> bitcount unique:users:and:2016-04-04_03
(integer) 2

在这里插入图片描述
or(并集)
如果想算出2016-04-04和2016-04-03任意一天都访问过网站的用户数量(例如月活跃就是类似这种),可以使用or求并集,具体命令如下:

127.0.0.1:6379> bitop or unique:users:or:2016-04-04_03 unique:
users:2016-04-03 unique:users:2016-04-03
(integer) 2
127.0.0.1:6379> bitcount unique:users:or:2016-04-04_03
(integer) 6

not(非)

127.0.0.1:6379> bitop not unique:users:not:2016-04-04 unique:users:2016-04-04
(integer) 2
127.0.0.1:6379> bitcount unique:users:not:2016-04-04
(integer) 12

因为unique:users:2016-04-04共有2字节,取非只取2字节内的。
xor(异或)

127.0.0.1:6379> bitop xor unique:users:xor:2016-04-03_04 unique:users:2016-04-03 unique:users:2016-04-04
(integer) 2
127.0.0.1:6379> bitcount unique:users:xor:2016-04-03_04
(integer) 4

1.2.5计算Bitmaps中第一个值为targetBit的偏移量

bitpos key targetBit [start] [end]
时间复杂度:O(N)
下面操作计算2016-04-04当前访问网站的最小用户id:

127.0.0.1:6379> bitpos unique:users:2016-04-04 1
(integer) 1

除此之外,bitops有两个选项[start]和[end],分别代表起始字节和结束字
节,例如计算第0个字节到第1个字节之间,第一个值为0的偏移量

127.0.0.1:6379> bitpos unique:users:2016-04-04 0 0 1
(integer) 0

1.3Bitmaps分析

假设网站有1亿用户,每天独立访问的用户有5千万,如果每天用集合类型和Bitmaps分别存储活跃用户可以得到表3-3。
在这里插入图片描述
很明显,这种情况下使用Bitmaps能节省很多的内存空间,尤其是随着时间推移节省的内存还是非常可观的。
但Bitmaps并不是万金油,假如该网站每天的独立访问用户很少,例如只有10万(大量的僵尸用户),那么两者的对比如表3-5所示,很显然,这时候使用Bitmaps就不太合适了,因为基本上大部分位都是0。
在这里插入图片描述

2.HyperLogLog

HyperLogLog并不是一种新的数据结构(实际类型为字符串类型),而是一种基数算法,通过HyperLogLog可以利用极小的内存空间完成独立总数的统计,数据集可以是IP、Email、ID等。HyperLogLog提供了3个命令:pfadd、pfcount、pfmerge。
例如2016-03-06的访问用户是uuid-1、uuid-2、uuid-3、uuid-4,2016-03-05的访问用户是uuid-4、uuid-5、uuid-6、uuid-7。
在这里插入图片描述

2.1添加

pfadd key element [element …] //pfadd用于向HyperLogLog添加元素,如果添加成功返回1:
时间复杂度:O(1)

127.0.0.1:6379> pfadd 2016_03_06:unique:ids "uuid-1" "uuid-2" "uuid-3" "uuid-4"
(integer) 1
127.0.0.1:6379> pfadd 2016_03_06:unique:ids "uuid-1" "uuid-2" "uuid-3" "uuid-4"
(integer) 0
127.0.0.1:6379> pfcount 2016_03_06:unique:ids
(integer) 4

2.2计算独立用户数

pfcount key [key …] //pfcount用于计算一个或多个HyperLogLog的独立总数
时间复杂度:O(1),使用单个键调用时,平均常数时间非常小。O(N),其中N是键的个数,当调用多个键时,常数次数要大得多。

127.0.0.1:6379> pfadd 2016_03_05:unique:ids "uuid-4" "uuid-5" "uuid-6" "uuid-7"
(integer) 1
127.0.0.1:6379> pfcount 2016_03_05:unique:ids 2016_03_06:unique:ids
(integer) 7

2.3合并

pfmerge destkey sourcekey [sourcekey …] //pfmerge求多个HyperLogLog的并集并赋值给destkey
时间复杂度:O(N),合并N个hyperloglog,但是常数时间很高。
例如要计算2016年3月5日和3月6日的访问独立用户数,可以看到最终独立用户数是7:

127.0.0.1:6379> pfadd 2016_03_06:unique:ids "uuid-1" "uuid-2" "uuid-3" "uuid-4"
(integer) 1
127.0.0.1:6379> pfadd 2016_03_05:unique:ids "uuid-4" "uuid-5" "uuid-6" "uuid-7"
(integer) 1
127.0.0.1:6379> pfmerge 2016_03_05_06:unique:ids 2016_03_05:unique:ids
2016_03_06:unique:ids
OK
127.0.0.1:6379> pfcount 2016_03_05_06:unique:ids
(integer) 7

2.4.100万个用户放到HyperLogLog和set中的内存对比:

2.4.1.HyperLogLog:

下面使用shell脚本向HyperLogLog插入100万个id,插入前记录一下redis-cli端执行info memory:

127.0.0.1:6379> info memory
# Memory
used_memory:835144
used_memory_human:815.57K
......

在shell窗口执行下面shell命令

...向2016_05_01:unique:ids插入100万个用户,每次插入1000条:
elements=""
key="2016_05_01:unique:ids"
for i in `seq 1 1000000`
227
do
	elements="${elements} uuid-"${i}
	if [[ $((i%1000)) == 0 ]];
	then
		redis-cli  -a paassword pfadd ${key} ${elements}
		elements=""
	fi
done

当上述代码执行完成后,可以看到内存只增加了15K左右:

127.0.0.1:6379> info memory
# Memory
used_memory:850616
used_memory_human:830.68K
......

但是,同时可以看到pfcount的执行结果并不是100万:

127.0.0.1:6379> pfcount 2016_05_01:unique:ids
(integer) 1009838

2.4.2.set

可以对100万个uuid使用集合类型进行测试,代码如下:

elements=""
key="2016_05_01:unique:ids:set"
for i in `seq 1 1000000`
do
	elements="${elements} "${i}
	if [[ $((i%1000)) == 0 ]];
	then
		redis-cli -a password sadd ${key} ${elements}
		elements=""
	fi
done

当上述代码执行完成后,可以看到内存使用了84MB:

127.0.0.1:6379> info memory
# Memory
used_memory:88702680
used_memory_human:84.59M
......

但独立用户数为100万:

127.0.0.1:6379> scard 2016_05_01:unique:ids:set
(integer) 1000000

表3-6列出了使用集合类型和HperLogLog统计百万级用户的占用空间对比。
在这里插入图片描述
可以看到,HyperLogLog内存占用量小得惊人,但是用如此小空间来估算如此巨大的数据,必然不是100%的正确,其中一定存在误差率。Redis官方给出的数字是0.81%的失误率。
HyperLogLog内存占用量非常小,但是存在错误率,开发者在进行数据结构选型时只需要确认如下两条即可:

  • 只为了计算独立总数,不需要获取单条数据。
  • 可以容忍一定误差率,毕竟HyperLogLog在内存的占用量上有很大的优势

3.GEO

Redis3.2版本提供了GEO(地理信息定位)功能,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能,对于需要实现这些功能的开发者来说是一大福音。

3.1增加地理位置信息

geoadd key [NX|XX] [CH] longitude latitude member [longitude latitude member …]

  • XX: 只更新已经存在的元素。永远不要添加元素。
  • NX: 不要更新已经存在的元素。总是添加新元素。
  • XX和NX选项互斥。
  • CH: 将返回值从添加的新元素数修改为更改的元素总数(CH是changed的缩写)。更改的元素是添加的新元素和坐标已更新的现有元素。因此,在命令行中指定的具有与过去相同分数的元素不会被计算在内。注意:通常,GEOADD的返回值只计算添加的新元素的数量。
  • longitude、latitude、member分别是该地理位置的经度、纬度、成员,
    时间复杂度:O(log(N)) ,对于添加的每一项,其中N是排序集中元素的个数。
127.0.0.1:6379> geoadd cities:locations 116.28 39.55 beijing 117.12 39.08 tianjin
(integer) 2

3.2.获取地理位置信息

geopos key member [member …]
时间复杂度:O(1)

127.0.0.1:6379> geopos cities:locations tianjin
1) 1) "117.12000042200088501"
2) "39.0800000535766543"

33.获取两个地理位置的距离。

geodist key member1 member2 [m|km|ft|mi] //[米|公里|英里|尺]
时间复杂度:O(1)

127.0.0.1:6379> geodist cities:locations tianjin beijing km
"89.2061"

3.4.获取指定位置范围内的地理信息位置集合

georadius key longitude latitude radiusm|km|ft|mi [withcoord] [withdist][withhash] [COUNT count] [asc|desc] [store key] [storedist key]
georadiusbymember key member radiusm|km|ft|mi [withcoord] [withdist][withhash] [COUNT count] [asc|desc] [store key] [storedist key]
georadius和georadiusbymember两个命令的作用是一样的,都是以一个地理位置为中心算出指定半径内的其他地理信息位置,不同的是georadius命令的中心位置给出了具体的经纬度,georadiusbymember只需给出成员即可。其中radiusm|km|ft|mi是必需参数,指定了半径(带单位),这两个命令有很多可选参数,如下所示:

  • withcoord:返回结果中包含经纬度。
  • withdist:返回结果中包含离中心节点位置的距离。
  • withhash:返回结果中包含geohash,有关geohash后面介绍。
  • COUNT count:指定返回结果的数量。
  • asc|desc:返回结果按照离中心节点的距离做升序或者降序。
  • store key:将返回结果的地理位置信息保存到指定键。
  • storedist key:将返回结果离中心节点的距离保存到指定键。
    时间复杂度:O(N+log(M)) N为圆心和半径划定的圆形区域边界框内的元素个数,M为索引内的项数。
127.0.0.1:6379> GEORADIUS Sicily 15 37 200 km WITHDIST WITHCOORD
1) 1) "Palermo"
   2) "190.4424"
   3) 1) "13.36138933897018433"
      2) "38.11555639549629859"
2) 1) "Catania"
   2) "56.4413"
   3) 1) "15.08726745843887329"
      2) "37.50266842333162032"
127.0.0.1:6379> georadiusbymember cities:locations beijing 150 km
1) "beijing"
2) "tianjin"
3) "tangshan"
4) "baoding"

3.5.获取geohash

geohash key member [member …]
时间复杂度:O(1)

127.0.0.1:6379> geohash cities:locations beijing
1) "wx4ww02w070"
127.0.0.1:6379> type cities:locations
zset

geohash有如下特点:

  • GEO的数据类型为zset,Redis将所有地理位置信息的geohash存放在zset中。
  • 字符串越长,表示的位置更精确,表3-8给出了字符串长度对应的精度,例如geohash长度为9时,精度在2米左右
    在这里插入图片描述
  • 两个字符串越相似,它们之间的距离越近,Redis利用字符串前缀匹配算法实现相关的命令。
  • geohash编码和经纬度是可以相互转换的。
  • Redis正是使用有序集合并结合geohash的特性实现了GEO的若干命令。

3.6.删除地理位置信息

zrem key member
GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset,所以可以借用zrem命令实现对地理位置信息的删除

  • 27
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值