Redis的数据类型详解和使用:HashSetSorted setBitmapHyperLogLog类型

Posted 刘Java

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis的数据类型详解和使用:HashSetSorted setBitmapHyperLogLog类型相关的知识,希望对你有一定的参考价值。

详细介绍了Redis的Hash、Set、Sorted set、Bitmap、HyperLogLog类型的常见命令和应用方式。

1 Redis Hash

Redis Hash是一个String 类型的 field 和 value 的映射表,底层是Redis自己实现的dict字典结构,类似于JDK1.7前的 HashMap,内部采用数组+链表结构,采用链地址法解决哈希冲突。Hash结构特别适合用于存储对象,后续操作的时候,可以直接仅仅修改这个对象中的某个字段的值。元素较少时,将会采用ziplist进行存储。

Redis 中每个 hash 可以存储 2^32 – 1个键值对(超过40亿),完全可被用来存储除了对象之外的其他数据。

HSET命令用于设置Hash的单个字段,而HMSET命令则用于设置Hash的多个字段,如果此前没有key会被创建,相同HashKey的value将会被覆盖。HGET检索单个字段,HMGET用于检索一组字段值,对于散列中不存在的每个字段,返回一个 nil 值。

127.0.0.1:6379> HSET a a aa
(integer) 1
127.0.0.1:6379> HMSET a a aaa b bb c cc
OK
127.0.0.1:6379> HGET a a
"aaa"
127.0.0.1:6379> HMGET a a b d
1) "aaa"
2) "bb"
3) (nil)

可以对Hash中的单个字段执行操作,例如 HINCRBY

127.0.0.1:6379> HSET inc 10 1000
(integer) 1
127.0.0.1:6379> HINCRBY inc 10 -1
(integer) 999

HGETALL用于获取key中所有Hash键值对,HKEYS用于获取所有的key,HVALS用于获取所有的value:

127.0.0.1:6379> HGETALL a
1) "a"
2) "aaa"
3) "b"
4) "bb"
5) "c"
6) "cc"
127.0.0.1:6379> HKEYS a
1) "a"
2) "b"
3) "c"
127.0.0.1:6379> HVALS a
1) "aaa"
2) "bb"
3) "cc"

2 Redis Set

Redis Set是无序且不会重复的字符串集合,类似于 Java 中的 HashSet,相当于一个特殊的Hash,存入的值被存储为key,而value都是一个null,返回数据的时候,就返回key。

Redis 中每个 Set可以存储 2^32 – 1个value(超过40亿)。

SADD命令用于将指定的多个元素添加到key对应的List集合中(Redis 2.4之前的版本每次调用只能添加一个元素),已存在的元素将不会被再次添加,返回添加成功的元素数量。

127.0.0.1:6379> SADD name hello
(integer) 1
127.0.0.1:6379> SADD name world
(integer) 1
127.0.0.1:6379> SADD name world
(integer) 0
127.0.0.1:6379> SMEMBERS name
1) "world"
2) "hello"

SMEMBERS用于获取所有的元素,SCARD用于获取元素数量,SISMEMBER用于判断某个元素是否存在:

127.0.0.1:6379> SMEMBERS name
1) "xx"
2) "world"
3) "hello"
127.0.0.1:6379> SCARD name
(integer) 3
127.0.0.1:6379> SISMEMBER name xx
(integer) 1
127.0.0.1:6379> SISMEMBER name xxx
(integer) 0

Redis Set支持集合交集、并集或差集等计算,因此可以用于快速的开发某些功能,比如求两个用户相同的关注。

  1. SINTER:返回多个集合的交集(在每个Set中都有出现的元素);
  2. SINTERSTORE destination:求多个集合的交集,并将结果存入指定的集合中;
  3. SDIFF:返回第一个集合与其他集合之间的差集(只有在第一个Set中出现,在其他任何Set中都没有出现的元素)。
  4. SDIFFSTORE destination:返回第一个集合与其他集合之间的差集,并将结果存入Set集合destination中。
  5. SUNION:返回多个集合的并集(去重);
  6. SUNIONSTORE destination:返回多个集合的并集,并将结果存入Set集合destination中。
127.0.0.1:6379> SADD a a b c
(integer) 3
127.0.0.1:6379> SADD b b c d
(integer) 3
127.0.0.1:6379> SADD c c d e
(integer) 3
127.0.0.1:6379> SINTER a b c
1) "c"
127.0.0.1:6379> SDIFF a b c
1) "a"
127.0.0.1:6379> SUNION a b c
1) "c"
2) "d"
3) "b"
4) "a"
5) "e"

可以使用 Set的 SPOPSRANDMEMBER 命令随机提取元素,SPOP将会弹出元素,而SRANDMEMBER则不会:

127.0.0.1:6379> SADD a a b c d e f g
(integer) 7
127.0.0.1:6379> SRANDMEMBER a
"c"
127.0.0.1:6379> SRANDMEMBER a
"f"
127.0.0.1:6379> SRANDMEMBER a
"b"

3 Redis Sorted set

Redis Sorted Set 类似于 Redis Set,都是非重复的字符串集合。不同之处在于,Sorted Set 的每个成员都与一个double类型的score 分数相关联,实际上是一个按照分数从最小到最大排序的有序集合。同样可用于排名、排行榜等业务。

Redis 中每个 Sorted Set可以存储 2^32 – 1个score-value对(超过40亿)。

由于集合元素有序,因此可以根据score或排名(位置)非常快的获取获取范围的元素。另外,虽然有序集合的元素是唯一的,但分数(score)却可以重复。如果 A 和 B 是具有不同score的两个元素,如果 A.score 是 > B.score,则 A > B。如果 A 和 B 的score完全相同,则按照字符串按字典顺序比较A和B并排序。

Redis Sorted Set的数据结构比较复杂,由ziplist、zset(dict和skiplist)组成,可以说是最高级的 Redis 数据类型:

  1. 当数据较少时,Sorted Set是由一个ziplist来实现的。ziplist使用整块内存,元素之间非常紧凑,每个存入的(元素,score)对将会被存储为两个ziplist节点,元素在前,score在后。ziplist更加节省内存,但是只能进行顺序或者倒序依次查找,插入、删除元素时,涉及到其他元素位置移动,因此ziplist只适用于元素数量较少、元素较小的情况。
  2. 当数据多的时候,Sorted Set采用zset来实现,这个zset内部包含一个dict(类似JDK1.7的hashmap)和一个skiplist(跳跃表)数据结构。skiplist的每个结点都保存了元素和score,并且根据score来进行了排序,可用来根据score查询元素,比如score范围查找。而dict则保存了元素到分数(score)的对应关系,实际上一个skiplist就能满足需求,为什么要使用dict呢,采用dict就可以使用map的特性了,比如快速根据元素找到它的score,时间复杂度为O(1),如果只使用skiplist,则根据元素查找score则非常耗时,需要O(N)。

关于跳跃表skiplist,它也是一种有序的查找数据结构,它采用以空间换时间的方法,通过添加“索引”(消耗空间)来实现数据的快速查找,并且支排序,支持范围查找,相比于平衡二叉树实现起来更加简单,占用的内存也不会比平衡二叉树大多少。在Java中有ConcurrentSkipListMap与其对应,skiplist的具体原理我们在ConcurrentSkipListMap的文章中就讲过了,在此不再赘述!

某个Sorted Set由ziplist结构转换为zset结构的要求是:

  1. Sorted Set保存的元素数量小于128个。
  2. Sorted Set保存的所有元素的长度小于64字节。

使用ZADD可以添加多个元素到Sorted Set,这与SADD类似,但是每一个元素必须关联一个score,score在前,即score-value 对。由于元素唯一,因为相同的value的分数将会进行替换。

使用ZRANGE展示全部value,该命令类似于LRANGE,使用该命令时Redis并不会排序,因为每一个元素插入时,已经排好序了。使用ZREVRANGE则可以查看倒序的结果。

127.0.0.1:6379> ZADD a 1 aa 3 bb 2 cc 7 dd 8.1 ee
(integer) 5
127.0.0.1:6379> ZRANGE a 0 -1
1) "aa"
2) "cc"
3) "bb"
4) "dd"
5) "ee"
127.0.0.1:6379> ZREVRANGE a 0 -1
1) "ee"
2) "dd"
3) "bb"
4) "cc"
5) "aa"

在最后添加WITHSCORES,即可返回每一个value对应的socre:

127.0.0.1:6379> ZREVRANGE a 0 -1 WITHSCORES
 1) "ee"
 2) "8.0999999999999996"
 3) "dd"
 4) "7"
 5) "bb"
 6) "3"
 7) "cc"
 8) "2"
 9) "aa"
10) "1"

ZSCORE用于返回某个元素的score,ZREM用于删除某个元素:

127.0.0.1:6379> ZADD b 1 aa 3 bb 2 cc 7 dd 8.1 ee
(integer) 5
127.0.0.1:6379> ZSCORE b cc
"2"
127.0.0.1:6379> ZREM b cc
(integer) 1
127.0.0.1:6379> ZRANGE b 0 -1
1) "aa"
2) "bb"
3) "dd"
4) "ee"

使用Sorted Set可以执行范围操作。ZRANGEBYSCORE获取指定分数之间的元素(范围默认是为闭区间,可以通过给某个端点前增加 ( 符号来使用可选的开区间=),-inf表示负无穷,+inf表示正无穷。

127.0.0.1:6379> ZRANGEBYSCORE a 2 3
1) "cc"
2) "bb"
127.0.0.1:6379> ZRANGEBYSCORE a -inf 3
1) "aa"
2) "cc"
3) "bb"
127.0.0.1:6379> ZRANGEBYSCORE a -inf (3
1) "aa"
2) "cc"

ZRANGEBYSCORE后还可以跟limit offset count命令,用于进行分页查询,如limit 0 1就表示返回最小的一条数据。

ZRANGEBYSCORE可以用于实现简单的Redis版本的延时队列,我们使用ZADD添加延时消息,将score设置为一个表示关联消息的未来执行时间点的时间戳,然后使用一个线程轮询的通过ZRANGEBYSCORE命令获取score小于等于当前时间戳的最小score的消息进行消费,消费完毕再使用ZREM删除该消息。当然,还需要考虑多客户端实例(加分布式锁)、消费ack机制(需要额外的确认队列,消费失败重试)等问题,比较繁琐。

ZREMRANGEBYSCORE类似于上的ZRANGEBYSCORE,但它会移除范围内的元素,并返回移除的数量:

127.0.0.1:6379> ZREMRANGEBYSCORE a 2 3
(integer) 2
127.0.0.1:6379> ZRANGE a 0 -1
1) "aa"
2) "dd"
3) "ee"

ZRANK用于获取某个元素的排序索引位置(从0开始),ZREVRANK则获取倒序顺序:

127.0.0.1:6379> ZRANK a ee
(integer) 2
127.0.0.1:6379> ZRANK a aa
(integer) 0
127.0.0.1:6379> ZREVRANK a aa
(integer) 2

4 Bitmap

Bitmap不是实际的数据类型,而是在 String 类型上定义的一组面向位(bit)的操作。由于String的最大长度为 512 MB,因此一个String可以设置多达 2^32 个不同的位。

位操作分为两种:一种是恒定时间的单个位操作,例如将某个位设置为 1 或 0,或者获取某个位的值,另一种是对位组的操作,例如计算给定位范围内设置为1的位的数量。

Bitmap的最大优点之一是它们在存储信息时通常可以极大地节省空间。例如,在不同用户由自增用户ID表示的系统中,仅使用 512 MB 内存就可以记住 40 亿用户的单个位信息。例如,记录用户是否想要接收实时通讯,1表示是,0表示否,或者记录用户是否签到、是否登陆、是否活跃等各种布尔状态信息。

使用 SETBIT 命令设置某个偏移量的位的值(0或1),返回原始值,使用 GETBIT 检索某个偏移量的位的值,偏移量offset从0开始,小于等于2^32:

127.0.0.1:6379> SETBIT a 0 1
(integer) 0
127.0.0.1:6379> SETBIT a 3 1
(integer) 0
127.0.0.1:6379> GETBIT a 0
(integer) 1

在SETBIT设置值时,底层字符串长度是可变的,长度为目前最大的位,如果设置的位超出当前字符串长度,该命令会自动扩大字符串。而如果通过GEIBIT获取的索引位置时超出范围的位(查找存储在目标键中的字符串长度之外的位)总是被认为是0。

BITOP命令用于对多个BitMap的key执行按位运算,支持AND, OR, XOR 和NOT,并将结果存储在destkey中:

BITOP AND destkey srckey1 srckey2 srckey3 ... srckeyN
BITOP OR destkey srckey1 srckey2 srckey3 ... srckeyN
BITOP XOR destkey srckey1 srckey2 srckey3 ... srckeyN
BITOP NOT destkey srckey

BITCOUNT命令执行计数操作,返回设置为 1 的位的数量。

127.0.0.1:6379> SETBIT a 1 1
(integer) 0
127.0.0.1:6379> SETBIT a 2 1
(integer) 0
127.0.0.1:6379> SETBIT a 3 1
(integer) 0
127.0.0.1:6379> SETBIT b 2 1
(integer) 0
127.0.0.1:6379> SETBIT b 3 1
(integer) 0
127.0.0.1:6379> SETBIT b 4 1
(integer) 0
127.0.0.1:6379> BITOP AND c a b
(integer) 1
127.0.0.1:6379> BITCOUNT c
(integer) 2
127.0.0.1:6379> BITOP OR c a b
(integer) 1
127.0.0.1:6379> BITCOUNT c
(integer) 4

BITPOS命令查找指定值为 0 或 1 的第一位的索引:

127.0.0.1:6379> BITPOS c 0
(integer) 0
127.0.0.1:6379> BITPOS c 1
(integer) 1

5 HyperLogLog

HyperLogLog 是Redis 在 2.8.9版本开始添加的一种用于计算集合基数的概率型数据结构。所谓基数,就是指一个集合中不重复的元素的个数,基数是一个估计值而不是一个准确值,因此有一定范围的误差,但可以接受。

基数的应用非常多,比如,在MySQL中也有基数(Cardinality)的概念,索引基数是数据列所包含的不同值的数量,索引的基数相对于总数据量的占比(索引选择性)越高,那么说明索引不重复的概率就越大,索引效率就越高,否则说明索引效率越低,比如十万行数据的性别字段,就不适用与建立索引(大部分情况,但有例外),因为性别只有男/女,两种,基数与总数的占比过低,索引作用不大!

通常,计算基数需要使用的内存量与要计算的元素数量成正比,因为需要记住过去已经计算过的元素,以避免对它们进行多次计数。但是,有一组算法可以用内存换取精度:以带有标准误差的估计基数值,在 Redis 实现的情况下,该标准误差小于 1%(注:mysql中索引基数计算的也是一个估算值)。

Redis的HyperLogLog算法的神奇之处在于不再需要使用与元素数量成正比的内存量,而是可以使用恒定的内存!在最坏的情况下(大量元素)内存占用为12kB,如果元素更少,则HyperLogLog花费的内存更少。

Redis 中的 HLL (HyperLogLog)虽然在技术上是一种不同的数据结构,但被编码为 Redis String类型,因此可以调用GET来序列化 HLL,并调用 SET 将其反序列化回服务器(同理,Redis的Bitmap也是以使用SET和GET命令)。

使用PFADD命令向某个key添加元素,但实际上并没有真正将元素添加到 HLL 中,因为数据结构中仅包含没有实际元素的状态信息。使用PFCOUNT命令对添加的元素进行基数近似值统计:

127.0.0.1:6379> PFADD a xx
(integer) 1
127.0.0.1:6379> PFADD a yy
(integer) 1
127.0.0.1:6379> PFADD a xxx
(integer) 1
127.0.0.1:6379> PFADD a xx
(integer) 0
127.0.0.1:6379> PFADD a zz cc dd
(integer) 1
127.0.0.1:6379> PFCOUNT a
(integer) 6

HyperLogLog可以用来做很多事,比如统计用户在搜索框中执行的不同查询的次数,或者计算某个页面每天不同的访客数,这种结果不需要精确值。

相关文章:

  1. https://redis.io/topics/data-types
  2. https://redis.io/topics/data-types-intro

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

以上是关于Redis的数据类型详解和使用:HashSetSorted setBitmapHyperLogLog类型的主要内容,如果未能解决你的问题,请参考以下文章

Redis的数据类型详解和使用:List类型

Redis 基础 -- Redis数据类型概述和string类型详解redis的key的设置约定

Redis 详解 redis的五大数据类型实现原理

Redis五种数据类型详解

Redis原理详解

Redis五个基本数据类型详解