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在内存的占用量上有很大的优势
2.5GEO
Redis3.2版本提供了GEO(地理信息定位)功能,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能,对于需要实现这些功能的开发者来说是一大福音。
2.5.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
2.5.2.获取地理位置信息
geopos key member [member …]
时间复杂度:O(1)
127.0.0.1:6379> geopos cities:locations tianjin
1) 1) "117.12000042200088501"
2) "39.0800000535766543"
2.5.3.获取两个地理位置的距离。
geodist key member1 member2 [m|km|ft|mi] //[米|公里|英里|尺]
时间复杂度:O(1)
127.0.0.1:6379> geodist cities:locations tianjin beijing km
"89.2061"
2.5.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"
2.5.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的若干命令。
2.5.6.删除地理位置信息
zrem key member
GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset,所以可以借用zrem命令实现对地理位置信息的删除