Redis新的3种数据类型(三)

1. Bitmaps 位操作字符串

现代计算机使用二进制(位)作为信息的基本单位,1个字节=8位,例如”abc”字符串有3个字节组成,计算机存储是使用其二进制。
“abc”分别对应ASCII码:97,98,99,对应的二进制分别是:01100001、01100010、01100011,如下图

合理的使用位操作能够有效地提高内存使用率和开发效率。

Redis提供了Bitmaps这个”数据类型”可以实现对位的操作:

  • Bitmaps本身不是一种数据类型,实际上就是字符串(key-value),但它可以对字符串进行操作,字符串中每一个字符对应1个字节,也就是8位,一个字符可以存储8bit位信息

  • Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。

1.1 常用命令

  1. setbit:设置某个偏移量的值(0或1)

    1
    setbit key offset value

    设置offset偏移位的值为value,offset的值是从0开始的,n代表第n+1个bit位置的。 offset 参数必须大于或等于 0 ,小于 2^32 (bit 映射被限制在 512 MB 之内)。 value的值只能为0或1 返回值:指定偏移量原来储存的位。

    示例:

    1
    2
    3
    4
    5
    6
    127.0.0.1:6379> setbit bitkey 100 1
    (integer) 0
    127.0.0.1:6379> getbit bitkey 100
    (integer) 1
    127.0.0.1:6379> getbit bitkey 101 #bit默认初始化为 0
    (integer) 0

    每个独立用户是否访问国网站存放在bitmaps中,将访问的用户记录记做1,没有反问的记做0,用户ID作为offset。假设现在有20个用户,userId=1,6,11,15,19的用户访问了网站,那么当前的bitmaps初始化结果如图:

    users:20230225这个bitmaps中表示2023-02-25这天独立访问的用户,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    127.0.0.1:6379> setbit users:20230225 1 1
    (integer) 0
    127.0.0.1:6379> setbit users:20230225 6 1
    (integer) 0
    127.0.0.1:6379> setbit users:20230225 11 1
    (integer) 0
    127.0.0.1:6379> setbit users:20230225 15 1
    (integer) 0
    127.0.0.1:6379> setbit users:20230225 19 1
    (integer) 0
  2. getbit:获取某个偏移位的值

    1
    getbit key offset

    获取key锁对应的bitmaps中offset偏移为的值,返回0或者1

  1. bitcount:统计bit位都为1的数量

    1
    bitcount key [start] [end]

    统计bit被设置为1的数量,一般情况下,给定的整个字符串都会被进行统计,通过指定额外的start或者end参数,可以让计数只在特定位上进行

    start和end都可以使用负数,比如 -1表示最后一个,-2表示倒数第二个,以此类推

    注意:start、end是指bit数组的字节下标,一个字节对应8个bit,所以[a,b]对应的offset范围是[8a, 8b+7]

    示例:

    1
    2
    3
    4
    5
    # offset值为:1,6,11,15,19
    127.0.0.1:6379> bitcount users:20230225 # 获取user这个bitmaps中1的数量
    (integer) 5
    127.0.0.1:6379> bitcount users:20230225 0 1 # 获取[0,1]这个字节内bit位上1的数量,也就是offset是[0,15]的位置上1的数量,所以是4个
    (integer) 4
  1. bitop:对一个或者多个bitmaps执行位操作
    1
    bitop <operation> destkey key [key ....]

    对一个或多个保存二进制位的字符串key进行位元操作,并将结果保存到destkey上。

    operation可以是 AND 、OR、NOT、XOR中的一种:

    • BITOP AND destkey key [key …] ,对一个或多个 key 求逻辑并,并将结果保存到 destkey 。
    • BITOP OR destkey key [key …] ,对一个或多个 key 求逻辑或,并将结果保存到 destkey 。
    • BITOP XOR destkey key [key …] ,对一个或多个 key 求逻辑异或,并将结果保存到 destkey 。
    • BITOP NOT destkey key ,对给定 key 求逻辑非,并将结果保存到 destkey 。

    除了 NOT 操作之外,其他操作都可以接受一个或多个 key 作为输入。 返回值:保存到 destkey 的字符串的长度,和输入 key 中最长的字符串长度相等。

    示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    127.0.0.1:6379> setbit bits-1 0 1
    (integer) 0
    127.0.0.1:6379> setbit bits-1 3 1
    (integer) 0
    127.0.0.1:6379> setbit bits-2 0 1
    (integer) 0
    127.0.0.1:6379> setbit bits-2 3 1
    (integer) 0
    127.0.0.1:6379> bitop and result-and bits-1 bits-2
    (integer) 1

1.2 bitmaps与set比较

假设网站有1亿的用户,每天独立访问的用户有5千万,如果每天用集合类型和bitmaps分别存储活跃用户可以得到表:

数据类型 每个用户ID占用空间 需要存储的用户量 全部存储两
Set集合 64位 5千万 64位 * 50000000 = 400MB
Bitmaps 1位 1亿 1位 * 100000000 = 12.5MB

很明显, 这种情况下使用Bitmaps能节省很多的内存空间, 尤其是随着时间推移节省的内存还是非常可观的。

但是如果该网站每天独立访问用户很少,那么这两者对比起来,bitmaps就不太合适了,因为大部份位都是0;

2. HyperLoglog

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

解决基数问题有很多种方案:

  • 数据存储在 MySQL 表中,使用 distinct count 计算不重复个数。

  • 使用 Redis 提供的 hash、set、bitmaps 等数据结构来处理。

以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。能否能够降低一定的精度来平衡存储空间?Redis 推出了 HyperLogLog。

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

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

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

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

2.1 常用命令

  1. pfadd:添加多个元素

    1
    pfadd key element [element ....]

    向HyperLoglog类型key添加一个或者多个元素,1添加成功, 0添加失败

    示例:

    1
    2
    3
    4
    5
    6
    127.0.0.1:6379> pfadd program java php js node # program中添加4个元素[java,php,js,node],添加成功,返回1
    (integer) 1
    127.0.0.1:6379> pfadd program java #再次添加java,由于已经存在,所以添加失败,返回0
    (integer) 0
    127.0.0.1:6379> pfadd program java c++ # 再次添加2个元素,java已经存在了,但是c++不存在,添加成功,返回1
    (integer) 1
  2. pfcount:获取多个HLL合并后元素的个数

    1
    pfcount key1 key2

    统计一个或者多个key去重后元素的数量

    示例:

    1
    2
    3
    4
    5
    6
    127.0.0.1:6379> pfadd k1 a b c d java   #k1中5个元素:[a,b,c,d,java],其中Java在program中存在
    (integer) 1
    127.0.0.1:6379> pfcount k1
    (integer) 5
    127.0.0.1:6379> pfcount k1 program 获取k1和program去重之后数量合集:[a,b,c,d,java,php,js,node,c++],数量为9
    (integer) 9
  3. pfmerge:将多个HLL合并后元素放入另一个HLL

    1
    pfmerge <destkey> <sourcekey> [sourcekey ....]

    将过个sourcekey合并放到destkey中

    示例:

    1
    2
    3
    4
    127.0.0.1:6379> pfmerge mergekey k1 program    #将k1和program合并后放入mergekey
    OK
    127.0.0.1:6379> pfcount mergekey #mergekey中元素个数为9
    (integer) 9

3. Geographic

Reids3.2 中增加了对GEO类型的支持,GEO(Geographic),地理信息的缩写。

该类型,就是元素的2维坐标,在地图上就是经纬度,redis基于该类型,提供了经纬度设置、查询、范围查询、距离查询,经纬度Hash等常见操作。

3.1 常用命令

  1. geoadd:添加多个位置的经纬度

    1
    geoadd key longitude latitude member [longitude latitude member...]

    longitude latitude member:经度 纬度 名称

    geo实际上使用的是zset类型存储的

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai #添加上海的经纬度
    (integer) 1
    127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing #添加重庆经纬度
    (integer) 1
    127.0.0.1:6379> type china:city #查看类型,发现geo实际上使用zset类型存储的
    zset
    127.0.0.1:6379> zrange china:city 0 -1 #查询key的全部元素
    1) "chongqing"
    2) "shanghai"
    127.0.0.1:6379> zrange china:city 0 -1 withscores # 查询key的全部元素包含score
    1) "chongqing"
    2) "4026042091628984"
    3) "shanghai"
    4) "4054803462927619"
    • 两级无法直接添加,一般会下载城市数据,直接通过java程序一次性导入。

    • 有效的经纬度从-180度到180度,有效的维度从-85.05112878度到85.05112878度。

    • 当坐标位置超出指定范围时,该命令将会返回一个错误。

    • 已经添加的数据,是无法再次往里面添加的。

  2. geopos:获取多个位置的坐标值

    1
    geopos key member [member.....]

    示例:

    1
    2
    3
    4
    5
    6
    127.0.0.1:6379> geopos china:city shanghai chongqing wuhan  #获取上海、重庆、武汉3个城市的坐标,由于没有添加武汉的数据,所以没有获取到,其他2个获取到了
    1) 1) "121.47000163793563843"
    2) "31.22999903975783553"
    2) 1) "106.49999767541885376"
    2) "29.52999957900659211"
    3) (nil)
  3. geodist:获取两个位置的直线距离

    1
    2
    3
    geodist key member1 member2 [m | km | ft | mi]

    #单位:[m|km|ft|mi] -> [米|千米|英里|英尺],默认为米

    示例:

    1
    2
    3
    4
    5
    6
    127.0.0.1:6379> zrange china:city 0 -1
    1) "chongqing"
    2) "shanghai"
    127.0.0.1:6379> geodist china:city shanghai chongqing #获取上海到重庆的直线距离
    "1447673.6920"
    127.0.0.1:6379>
  4. georadius:以给定的经纬度为中心,找出某一半径内的元素(附近的人)

    1
    2
    3
    georadius key longitude latitude radius m |km |ft|mi

    #单位:[m|km|ft|mi] -> [米|千米|英里|英尺],默认为米

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    127.0.0.1:6379> geoadd china:city 114.05 22.52 shenzhen 116.38 39.90 beijing  #添加深圳、北京2个城市的经纬度
    (integer) 2
    127.0.0.1:6379> zrange china:city 0 -1 #输出key中的元素,里面包含了重庆,深圳,上海,北京4个地方的经纬度
    1) "chongqing"
    2) "shenzhen"
    3) "shanghai"
    4) "beijing"
    127.0.0.1:6379> georadius china:city 110 30 1000 km #在china:city中检索:以经纬度(110,30)为中心,半径为1000km内的位置列表
    1) "chongqing"
    2) "shenzhen"