Redis不是一个简单的键值对存储,它实际上是一个支持各种类型数据结构的存储。在传统的键值存储中,是将字符串键关联到字符串值,但是在Redis中,这些值不仅限于简单的字符串,还可以支持更复杂的数据结构。下面就是Redis支持的数据结构:
欢迎关注微信公众号: 万猫学社 ,每周一分享Java技术干货。
键
是二进制安全的,这意味着您可以使用任何二进制序列作为键,可以是 OneMoreStudy
这样的字符串,也可以使图片文件的内容,空字符串也是有效的 键
。不过,还有一些其他规则:
键
,比如一个1KB的键。不仅是多占内存方面的问题,而是在数据集中查找 键
可能需要进行一些耗时的 键
比较。如果真的有比较大的 键
,先对它进行哈希(比如: MD5
、 SHA1
)是一个好主意。 键
,比如: OMS100f
,相对于 one-more-study:100:fans
,后者更具有可读性。可能会占用更多内存,但是相对于值所占的内存, 键
所增加的内存还是小很多的。我们要找到一个平衡点,不长也不短。 one-more-study:100:fans
,或者 one.more.study:100:fans
。 键
允许的最大值为512MB。 欢迎关注微信公众号: 万猫学社 ,每周一分享Java技术干货。
字符串类型是和 键
关联的最简单的类型。它是Memcached中唯一的数据类型,因此对于新手来说,在Redis中使用它也是很容易的。 键
是字符串类型,当我们也使用字符串类型作为值时,我们会可以从一个字符串映射到另一个字符串。 字符串数据类型有很多应用场景,例如缓存HTML片段或页面。
下面简单介绍一下字符串的命令(在redis-cli中使用):
> set one-more-key OneMoreStudy OK > get one-more-key "OneMoreStudy"
使用 SET
和 GET
命令来设置和查询字符串值的方式。需要注意的是,如果当前 键
已经和字符串值相关联, SET
命令将会替换已存储在 键
中的现有值。字符串可以是任意的二进制数据,比如jpeg图像。字符串最多不能大于512MB。 SET
命令还有一些实用的可选参数,比如:
> set one-more-key Java nx #如果key存在,则设置失败。 (nil) > set one-more-key Java xx #如果key存在,才设置成功。 OK
欢迎关注微信公众号: 万猫学社 ,每周一分享Java技术干货。
虽然字符串是Redis的基本值,但也可以使用它们执行一些实用的操作。 比如:
> set one-more-counter 50 OK > incr one-more-counter #自增加1 (integer) 51 > incr one-more-counter #自增加1 (integer) 52 > incrby one-more-counter 5 #自增加5 (integer) 57
INCR
命令将字符串值解析为整数,将其自增加1,最后将获得的值设置为新值。 还有其他类似的命令,例如 INCRBY
, DECR
和 DECRBY
等命令。 INCR
命令是原子操作,即时有多个客户端同时同一个key的 INCR
命令,也不会进入竞态条件。比如,上面的例子先设置 one-more-counter
的值为50,即使两个客户端同时发出INCR命令,那么最后的值也肯定是52。
可以使用 MSET
和 MGET
命令在单个命令中设置或查询多个 键
的值,对于减少延迟也很有用。比如:
> mset a 1 b 2 c 3 OK > mget a b c 1) "1" 2) "2" 3) "3"
使用 MGET
命令时,Redis返回一个值的数组。
欢迎关注微信公众号: 万猫学社 ,每周一分享Java技术干货。
使用DEL命令可以删除 键
和相关联的值,存在指定的 键
则返回1,不存在指定的 键
则返回0。使用 EXISTS
命令判断Redis中是否存在指定的 键
,存在指定的 键
则返回1,不存在指定的 键
则返回0。比如:
> set one-more-key OneMoreStudy OK > exists one-more-key (integer) 1 > del one-more-key (integer) 1 > exists one-more-key (integer) 0
使用 TYPE
命令,可以返回存储在指定key的值的数据类型,比如:
> set one-more-key OneMoreStudy OK > type one-more-key string > del one-more-key (integer) 1 > type one-more-key none
在讨论更复杂的数据结构之前,我们需要讨论另一个功能,该功能无论值类型是什么都适用,它就是 EXPIRE
命令。 它可以为 键
设置到期时间,当超过这个到期时间后,该 键
将自动销毁,就像对这个 键
调用了 DEL
命令一样。比如:
> set one-more-key OneMoreStudy OK > expire one-more-key 5 (integer) 1 > get one-more-key #立刻调用 "OneMoreStudy" > get one-more-key #5秒钟后调用 (nil)
上面的例子,适用了 EXPIRE
命令设置了过期时间,也可以使用 PERSIST
命令移除 键
的过期时间,这个 键
将持久保持。除了 EXPIRE
命令,还可以使用SET命令设置过期时间,比如:
> set one-more-key OneMoreStudy ex 10 #设置过期时间为10秒 OK > ttl one-more-key (integer) 9
上面的例子,设置了一个字符串值 OneMoreStudy
的 one-more-key
,该 键
的到期时间为10秒。之后,调用 TTL
命令以检查该 键
的剩余生存时间。
欢迎关注微信公众号: 万猫学社 ,每周一分享Java技术干货。
到期时间可以使用秒或毫秒精度进行设置,但到期时间的分辨率始终为1毫秒。实际上,Redis服务器上存储的不是到期时间长度,而是该 键
到期的时间。
欢迎关注微信公众号: 万猫学社 ,每周一分享Java技术干货。
Redis列表是使用链表实现的,这就意味着在头部或尾部增加或删除一个的元素的时间复杂度是O(1),非常快的。不过,按索引查询对应元素的时间复杂度就是O(n),慢很多。如果想快速查询大量数据,可以使用有序集合,后面会有介绍。
LPUSH
命令将一个新元素添加到列表的左侧(顶部),而 RPUSH
命令将一个新元素添加到列表的右侧(底部)。最后, LRANGE
命令可以从列表中按范围提取元素。比如:
> rpush one-more-list A (integer) 1 > rpush one-more-list B (integer) 2 > lpush one-more-list first (integer) 3 > lrange one-more-list 0 -1 1) "first" 2) "A" 3) "B"
LRANGE
命令需要另外两个参数,要返回的第一个元素的索引和最后一个元素的索引。如果索引为负值,Redis将从末尾开始计数,-1是列表的最后一个元素,-2是列表的倒数第二个元素,依此类推。
LPUSH
和 RPUSH
命令支持多个参数,可以使用一次命令添加多个元素,比如:
> rpush one-more-list 1 2 3 4 5 "last" (integer) 9 > lrange one-more-list 0 -1 1) "first" 2) "A" 3) "B" 4) "1" 5) "2" 6) "3" 7) "4" 8) "5" 9) "last"
在Redis列表上,也可以移除并返回元素。 与 LPUSH
和 RPUSH
命令,对应的就是 LPOP
和 RPOP
命令, LPOP
命令是将列表的左侧(顶部)的元素移除并返回, RPOP
命令是将列表的右侧(底部)的元素移除并返回。比如:
> rpush one-more-list a b c (integer) 3 > rpop one-more-list "c" > rpop one-more-list "b" > rpop one-more-list "a"
我们添加了三个元素,并移除并返回了三个元素,此时列表为空,没有任何元素。如果再使用 RPOP
命令,会返回一个 NULL
值:
> rpop one-more-list (nil)
使用 RPUSH
和 RPOP
命令,或者 LPUSH
和 LPOP
命令可以实现栈的功能,使用 LPUSH
和 RPOP
命令,或者 RPUSH
和 LPOP
命令可以实现队列的功能。也可以实现生产者和消费者模式,比如多个生产者使用 LPUSH
命令将任务添加到列表中,多个消费者使用 RPOP
命令将任务从列表中取出。但是,有时列表可能为空,没有任何要处理的任务,因此 RPOP
命令仅返回 NULL
。在这种情况下,消费者被迫等待一段时间,然后使用 RPOP
命令重试。这就暴露了有几个缺点:
NULL
。 NULL
之后会等待一段时间,因此会增加任务处理的延迟。为了减小延迟,我们可以在两次调用 RPOP
之间等待更少的时间,这就扩大了更多对Redis的无用调用。
有什么办法可以解决呢?使用 BRPOP
和 BLPOP
的命令,它们和 RPOP
和 LPOP
命令类似,唯一的区别是:如果列表为空时,命令会被阻塞,直到有新元素添加到列表中,或指定的超时时间到了时,它们才会返回到调用方。比如:
> brpop tasks 5
它含义是,列表为空时,等待列表中的元素,但如果5秒钟后没有新的元素被添加,则返回。您可以将超时时间传入0,表示永远等待元素添加。也可以传入多个列表,这时会按参数先后顺序依次检查各个列表,返回第一个非空列表的尾部元素。另外还有以下3点需要注意的:
RPOP
命令相比有所不同,它是一个包含两个元素的数组,包含key和对应的元素,因为 BRPOP
和 BLPOP
命令能够阻止等待来自多个列表的元素。 NULL
。 欢迎关注微信公众号: 万猫学社 ,每周一分享Java技术干货。
列表的创建和删除都是由Redis自动完成的,当尝试向不存在的 键
添加元素时,Redis会自动创建一个空的列表;当最后一个元素被移除时,Redis会自动删除这个列表。这不是特定于列表的,它适用于由多个元素组成的所有Redis数据类型,比如集合、有序集合、哈希,它们都有3条规则:
键
不存在,则在添加元素之前会创建一个空的聚合数据类型。比如: > del one-more-list (integer) 1 > lpush one-more-list 1 2 3 (integer) 3
欢迎关注微信公众号: 万猫学社 ,每周一分享Java技术干货。
但是,在 键
存在时,就不能操作错误的数据类型了,比如:
> set one-more-key OneMoreStudy OK > lpush one-more-key 1 2 3 (error) WRONGTYPE Operation against a key holding the wrong kind of value > type one-more-key string
> lpush one-more-list 1 2 3 (integer) 3 > exists one-more-list (integer) 1 > lpop one-more-list "3" > lpop one-more-list "2" > lpop one-more-list "1" > exists one-more-list (integer) 0
LLEN
命令,获取列表长度)或写命令(如 LPOP
命令)时,都会返回空聚合数据类型的结果。比如: > del one-more-list (integer) 0 > llen one-more-list (integer) 0 > lpop one-more-list (nil)
Redis为了追求高性能,列表的内部实现不是一个简单的链表,这里先卖个关子,后续的文章会详细介绍。
欢迎关注微信公众号: 万猫学社 ,每周一分享Java技术干货。
集合是一个字符串的无序集合, SADD
命令可以将新元素添加到集合中。还可以对集合进行许多其他操作,例如:判断给定元素是否已存在、执行多个集合之间的交集、并集或差等等。比如:
> sadd one-more-set 1 2 3 (integer) 3 > smembers one-more-set 1) "1" 2) "3" 3) "2"
在上面的例子中,在集合中添加了三个元素,并让Redis返回所有元素。 正如你所见,返回的元素是没有排序的。在每次调用时,元素的顺序都有可能不一样。
还可以使用 SISMEMBER
命令判断给定元素是否已存在,比如:
> sismember one-more-set 3 (integer) 1 > sismember one-more-set 30 (integer) 0
在上面的例子中,3在集合中,所以返回1;而30不在集合中,所以返回0。
可以使用 SINTER
命令,计算出多个集合的交集;使用 SUNION
命令,计算多个集合的并集;使用 SPOP
命令,移除并返回集合中的一个随机元素;使用 SCARD
命令,计算集合中的元素的数量。比如:
> sadd one-more-set1 1 2 3 (integer) 3 > sadd one-more-set2 2 3 4 (integer) 3 > sinter one-more-set1 one-more-set2 #交集 1) "3" 2) "2" > sunion one-more-set1 one-more-set2 #并集 1) "1" 2) "3" 3) "2" 4) "4" > spop one-more-set1 #随机移除一个元素 "3" > scard one-more-set1 #元素数量 (integer) 2
欢迎关注微信公众号: 万猫学社 ,每周一分享Java技术干货。
有序集合是一种类似于集合和哈希之间混合的数据类型。像集合一样,有序集合中由唯一的、非重复的字符串元素组成,因此从某种意义上说,有序集合也是一个集合。但是集合中的元素是没有排序的,而有序集合中的每个元素都与一个称为 分数
(score)的浮点值相关联,这就是为什么有序集合也类似于哈希的原因,因为每个元素都映射到一个值。有序集合的排序规则如下:
我们来举个例子,把 王者荣耀 战队的名字和积分添加到有序集合中,其中把战队的名字作为值,把战队的积分作为分数。
> zadd kpl 12 "eStarPro" (integer) 1 > zadd kpl 12 "QGhappy" (integer) 1 > zadd kpl 10 "XQ" (integer) 1 > zadd kpl 8 "EDG.M" (integer) 1 > zadd kpl 8 "RNG.M" (integer) 1 > zadd kpl 4 "TES" (integer) 1 > zadd kpl 2 "VG" (integer) 1
如上所示, ZADD
命令和 SADD
命令相似,但是多了一个额外的参数(在要添加的元素的前面)作为分数。 ZADD
命令也支持多个参数,虽然在上面的例子中未使用它,但你也可以指定多个分数和值对。使用有序集合,快速地返回按其积分排序的战队列表,因为实际上它们已经被排序了。
需要注意的是,为了快速获取有序集合中的元素,每次添加元素的时间复杂度都为O(log(N)),这是因为有序集合是同时使用跳跃表和字典来实现的,具体原理这里先卖个关子,后续的文章会详细介绍。
可以使用 ZRANGE
命令按照升序获取对应的值,比如:
> zrange kpl 0 -1 1) "VG" 2) "TES" 3) "EDG.M" 4) "RNG.M" 5) "XQ" 6) "QGhappy" 7) "eStarPro"
0和-1代表查询从第一个到最后一个的元素。还可以使用 ZREVRANGE
命令按照降序获取对应的值,比如:
> zrevrange kpl 0 -1 1) "eStarPro" 2) "QGhappy" 3) "XQ" 4) "RNG.M" 5) "EDG.M" 6) "TES" 7) "VG"
加上 WITHSCORES
参数,就可以连同分数一起返回,比如:
> zrange kpl 0 -1 withscores 1) "VG" 2) "2" 3) "TES" 4) "4" 5) "EDG.M" 6) "8" 7) "RNG.M" 8) "8" 9) "XQ" 10) "10" 11) "QGhappy" 12) "12" 13) "eStarPro" 14) "12"
有序集合还有更强大的功能,比如在分数范围内操作,让我们获取小于10(含)的战队,使用 ZRANGEBYSCORE
命令:
> zrangebyscore kpl -inf 10 1) "VG" 2) "TES" 3) "EDG.M" 4) "RNG.M" 5) "XQ"
这就是获取分数从负无穷到10所对应的值,同样的我们也可以获取分数从4到10所对应的值:
> zrangebyscore kpl 4 10 1) "TES" 2) "EDG.M" 3) "RNG.M" 4) "XQ"
另外有用的命令: ZRANK
命令,它可以返回指定值的升序排名(从0开始); ZREVRANK
命令,它可以返回指定值的降序排名(从0开始),比如:
> zrank kpl "EDG.M" (integer) 2 > zrevrank kpl "EDG.M" (integer) 4
有序集合的分数是随时更新的,只要对已有的有序集合调用 ZADD
命令,就会以O(log(N))时间复杂度更新其分数和排序。这样,当有大量更新时,有序集合是合适的。由于这种特性,常见的场景是排行榜,可以方便地显示排名前N位的用户和用户在排行榜中的排名。
欢迎关注微信公众号: 万猫学社 ,每周一分享Java技术干货。
Redis的哈希和人们期望的“哈希”结构是一样的,它是一个无序哈希,内部存储了很多键值对,比如:
> hmset one-more-fans:100 name Lily age 25 OK > hget one-more-fans:100 name "Lily" > hget one-more-fans:100 age "25" > hgetall one-more-fans:100 1) "name" 2) "Lily" 3) "age" 4) "25"
尽管哈希很容易用来表示对象,但是实际上可以放入哈希中的字段数是没有实际限制的,因此您可以以更多种的不同方式使用哈希。除了 HGET
命令获取单个字段对应的值,也可以使用 HMSET
命令获取多个字段及对应的值,它返回的是一个数组,比如:
> hmget one-more-fans:100 name age non-existent-field 1) "Lily" 2) "25" 3) (nil)
还可以使用 HINCRBY
命令,为指定字段的值做增量,比如:
> hget one-more-fans:100 age "25" > hincrby one-more-fans:100 age 3 (integer) 28 > hget one-more-fans:100 age "28"
Redis哈希的实现结构,和Java中的HashMap是一样的,也是“数组+链表”的结构,当发生数组位置碰撞是,就会将碰撞的元素用链表串起来。不过Redis为了追求高性能,rehash的方式不太一样,这里先卖个关子,后续的文章会详细介绍。
欢迎关注微信公众号: 万猫学社 ,每周一分享Java技术干货。
位图不是实际的数据类型,而是在String类型上定义的一组面向位的操作。 由于字符串是二进制安全的,并且最大长度为512MB,因此可以设置多达2^32个不同的位。位图操作分为两类:固定单个位操作,比如将一个位设置为1或0或获取其值;对位组的操作,比如计算给定位范围内设置位的数量。
位图的最大优点之一是,它们在存储信息时通常可以节省大量空间。例如,在以增量用户ID位标识表示用户是否要接收新闻通讯,仅使用512 MB内存就可以记住40亿用户的一位信息。
使用 SETBIT
和 GETBIT
命令来设置和获取指定位,比如:
> setbit one-more-key 10 1 (integer) 0 > getbit one-more-key 10 (integer) 1 > getbit one-more-key 11 (integer) 0
SETBIT
命令将位号作为其第一个参数,将其设置为1或0的值作为其第二个参数。如果位号超出当前字符串长度,该命令将会自动扩大字符串。 GETBIT
命令只是返回指定位号的位的值,如果位号超出存储的字符串长度则会返回0。
对位组的操作有以下3个命令:
BITOP BITCOUNT BITPOS
> set one-more-key "/x13/x7f" #二进制为0001 0011 0111 1111 OK > bitcount one-more-key #整个字符串中1的位数 (integer) 10 > bitcount one-more-key 0 0 #第一个字符(0001 0011)中1的位数 (integer) 3 > bitcount one-more-key 1 1 #第二个字符(0111 1111)中1的位数 (integer) 7 > bitpos one-more-key 0 #整个字符串中第一个0位 (integer) 0 > bitpos one-more-key 1 #整个字符串中第一个1位 (integer) 3 > bitpos one-more-key 1 0 0 #第一个字符(0001 0011)中第一个1位 (integer) 3 > bitpos one-more-key 1 1 1 #第二个字符(0111 1111)中第一个1位 (integer) 9
位图可以应用于各类实时分析,也可以节省空间高效地存储位信息。比如,记录用户每天的签到数据,每一个位表示用户是否签到过,这样就可以计算出某个时间段用户签到了几次,某个时间段用户第一次签到是哪一天。
欢迎关注微信公众号: 万猫学社 ,每周一分享Java技术干货。
HyperLogLog是一种概率数据结构,用于统计唯一元素的数量,也可以理解为估计集合中元素的个数。
通常情况下,对唯一元素进行统计数量时,需要使用与要统计的元素数量成比例的内存量,因为需要记住过去已经看到的元素,以避免多次对其进行统计。但是,有一组算法可以以内存换取精度,最终会得到带有标准误差的估计数量,在Redis的HyperLogLogs中,该误差小于1%。
这个算法的神奇之处在于,不再需要使用与所统计元素数量成比例的内存量,而可以使用恒定数量的内存。在最坏的情况下占据12KB的内存空间,Redis对HyperLogLog的存储进行了优化,在计数比较少时,占据的内存空间会更小,这里先卖个关子,后续的文章会详细介绍其中原理。
在集合中,可以将每个元素添加到集合中,并使用 SCARD
命令获取集合中的元素数量,因为 SADD
命令不会重新添加现有元素,所以元素都是唯一的。HyperLogLog的操作和集合比较类似,使用 PFADD
命令将元素添加到HyperLogLog中,类似于集合的 SADD
命令;使用 PFCOUNT
命令获取HyperLogLog中的唯一元素的当前近似值数量,类似于集合的 SCARD
命令。比如:
> pfadd one-more-hll a b c d e (integer) 1 > pfcount one-more-hll (integer) 5
Redis中的HyperLogLog尽管在技术上是不同的数据结构,但被编码为字符串,因此可以调用 GET
命令来序列化HyperLogLog,然后调用 SET
命令来将其反序列化回服务器。
欢迎关注微信公众号: 万猫学社 ,每周一分享Java技术干货。
Redis提供更加丰富的数据结构, 键 (Key)和 字符串 (String),都是二进制安全的字符串; 列表 (List),根据插入顺序排序的字符串元素列表,基于链表实现; 集合 (Set),唯一的乱序的字符串元素的集合; 有序集合 (Sorted Set),与 集合 类似,但是每个字符串元素都与一个称为score的数字相关联; 哈希 (Hash),由字段与值相关联组成的映射,字段和值都是字符串; 位图 (Bitmap),像操作位数组一样操作字符串值,可以设置和清除某个位,对所有为1的位进行计数,找到第一个设置1的位,找到第一个设置0的位等等; HyperLogLogs ,一种概率数据结构,使用较小的内存空间来统计唯一元素的数量,误差小于1%。
欢迎关注微信公众号: 万猫学社 ,每周一分享Java技术干货。