• 进入"运维那点事"后,希望您第一件事就是阅读“关于”栏目,仔细阅读“关于Ctrl+c问题”,不希望误会!

Redis数据结构学习

Redis 彭东稳 8年前 (2017-03-03) 22374次浏览 已收录 0个评论

一、概述

Redis有5个基本数据结构,string、list、hash、set和zset。它们是日常开发中使用频率非常高应用最为广泛的数据结构,把这5个数据结构都吃透了,你就掌握了Redis应用知识的一半了。在Redis 5.0版本又新增了一个stream数据类型,另起文章介绍。

掌握数据类型是怎么工作的并不是非常简单,从command reference获取解决问题的命令用法,所以这篇文章是介绍Redis数据类型的速成班和它们最常见的模式。下面所有的例子我们将使用redis-cli客户端演示。

Redis命令介绍中文网:http://doc.redisfans.com/index.html

二、Key(键)

Redis是一个开源的使用ANSI C语言编写的key-value数据库,我们可能会较为主观的认为Redis中的字符串就是采用了C语言中的传统字符串表示,但其实不然,Redis没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,可变的字节数组;并将SDS用作Redis 的默认字符串表示:

设置一个key = msg,value = hello world的新键值对,他们底层是数据结构将会是:

  • 键(key)是一个字符串对象,对象的底层实现是一个保存着字符串“msg” 的SDS;
  • 值(value)也是一个字符串对象,对象的底层实现是一个保存着字符串“hello world” 的SDS;

SDS的定义

Redis中定义动态字符串的结构:

用图描述如下:

Redis数据结构学习

len变量,用于记录 buf 中已经使用的空间长度(这里指出Redis 的长度为5)。

free变量,用于记录 buf 中还空余的空间(初次分配空间,一般没有空余,在对字符串修改的时候,会有剩余空间出现)。

buf字符数组,用于记录我们的字符串(记录Redis)。

二进制安全

Redis keys是二进制安全的,这意味着你可以使用任意的二进制序列作为key,从像‘foo’这样的字符串到JPEG文件的内容。空字符串也是有效的key。

在C字符串中的字符必须符合某种编码,并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存想图片,音频,视频,压缩文件这样的二进制数据。

但是在Redis中,不是靠空字符来判断字符串的结束的,而是通过len这个属性。那么,即便是中间出现了空字符对于SDS来说,读取该字符仍然是可以的。

关于key的一些规则

1. 非常长的key并不好,例如一个1024字节的key,不仅内存不合理,而且因为数据中查找key也是非常昂贵的。即使手头的任务有很大的值要匹配,最好是去哈希它,特别是从内存和带宽的角度。

2. 非常短的key一般也不好。有一个小点‘u1000flw’作为一个key,不如写成‘user:1000:followers’。后者更具有可读性,前者更省空间。这要在这中间做一个权衡。

3. 试着坚持使用schema,例如”object-type:id“,就行像”user:1000“。点或破折号经常用于多词字段,像”comment:1234:reply.to“或”comment:1234:reply-to“。

4. key最大允许的长度是512MB。

对于Key(字符串)操作的其他常用命令:

KEYS

语法:KEYS pattern

查找所有符合给定模式的key。

DEL

语法:DEL key [key …]

删除给定的一个或多个key,不存在的key会被忽略。

MOVE

语法:MOVE key db

将当前数据库的key移动到给定的数据库db(redis默认使用数据库0)当中,如果当前数据库(源数据库)和给定数据库(目标数据库)有相同名字的给定key,或者key不存在于当前数据库,那么MOVE没有任何效果。因此,也可以利用这一特性,将MOVE当作锁(locking)原语(primitive)。

EXPIRE

语法:EXPIRE key seconds

为给定key设置生存时间,当key过期时(生存时间为0 ),它会被自动删除。

EXPIREAT

语法:EXPIREAT key timestamp

EXPIREAT的作用和EXPIRE类似,都用于为key设置生存时间。不同在于EXPIREAT命令接受的时间参数是UNIX时间戳(unix timestamp)。

PERSIST

语法:PERSIST key

移除给定key的生存时间,将这个key从『易失的』(带生存时间key )转换成『持久的』(一个不带生存时间、永不过期的key )。

PEXPIRE

语法:PEXPIRE key milliseconds

这个命令和EXPIRE命令的作用类似,但是它以毫秒为单位设置key的生存时间,而不像EXPIRE命令那样,以秒为单位。

PEXPIREAT

语法:PEXPIREAT key milliseconds-timestamp

这个命令和EXPIREAT命令类似,但它以毫秒为单位设置key的过期unix时间戳,而不是像EXPIREAT那样,以秒为单位。

TTL

语法:TTL key

检查key的存活时间,永久存活为-1。

Redis key有效期是指定key的存活时间,它限制key的存活时间。当指定是时间过后,key就会自动销毁,就好像用户调用DEL命令一样。设置一个有效期非常简单:

key在两个Get之间消失了,从第二次调用已经延迟超过了5秒钟。在上面的实例中,我们使用了EXPIRE为了设置超时时间(也可以设置不同的超时时间在存在的key上,像PERSIST可用于移除超时时间并永远的持久化key)。然而我们也可以使用其他的命令设置有效期,例如使用SET选项:

上面的例子设置了一个value=100的字符串,有一个10秒的超时时间。然后TTL命令被调用,用于检查key剩下的存活时间。

生存时间可以通过使用DEL命令来删除整个key来移除,或者被SET和GETSET命令覆写(overwrite),这意味着,如果一个命令只是修改(alter)一个带生存时间的key的值而不是用一个新的key值来代替(replace)它的话,那么生存时间不会被改变。

比如说,对一个key执行INCR命令,对一个列表进行LPUSH命令,或者对一个哈希表执行HSET命令,这类操作都不会修改key本身的生存时间。

另一方面,如果使用RENAME对一个key进行改名,那么改名后的key的生存时间和改名前一样。RENAME命令的另一种可能是,尝试将一个带生存时间的key改名成另一个带生存时间的another_key ,这时旧的another_key (以及它的生存时间)会被删除,然后旧的key会改名为another_key,因此,新的another_key的生存时间也和原本的key 一样。

使用PERSIST命令可以在不删除key的情况下,移除key的生存时间,让key重新成为一个『持久的』(persistent) key 。

PTTL

语法:PTTL key

这个命令类似于TTL命令,但它以毫秒为单位返回key的剩余生存时间,而不是像TTL命令那样,以秒为单位。

TYPE

语法:TYPE key

返回key所储存的值的类型,返回值如下:

none (key不存在)

string (字符串)

list (列表)

set (集合

zset (有序集)

hash (哈希表)

RENAME

语法:RENAME key newkey

将key改名为newkey,当key和newkey相同,或者key不存在时,返回一个错误。当newkey已经存在时,RENAME命令将覆盖旧值。可以用来防止隐藏某些命令。

当key不存在时,返回错误。

newke已存在时,RENAME会覆盖旧newkey。

RENAMENX

语法:RENAMENX key newkey

仅当newkey不存在时,将key改名为newkey,当key不存在时,返回一个错误。

SORT

语法:SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern …]] [ASC | DESC] [ALPHA] [STORE destination]

返回或保存给定列表、集合、有序集合key中经过排序的元素。排序默认以数字作为对象,值被解释为双精度浮点数,然后进行比较。

最简单的SORT使用方法是SORT key(返回键值从小到大排序的结果)和SORT key DESC(返回键值从大到小排序的结果)。

DUMP

语法:DUMP key

序列化给定key,并返回被序列化的值,使用RESTORE命令可以将这个值反序列化为Redis键。

序列化生成的值有以下几个特点:

1)它带有64位的校验和,用于检测错误, RESTORE在进行反序列化之前会先检查校验和。

2)值的编码格式和RDB文件保持一致。

3)RDB版本会被编码在序列化值当中,如果因为Redis的版本不同造成RDB格式不兼容,那么Redis会拒绝对这个值进行反序列化操作。

序列化的值不包括任何生存时间信息。

RESTORE

语法:RESTORE key ttl serialized-value

反序列化给定的序列化值,并将它和给定的key关联。

参数ttl以毫秒为单位为key设置生存时间;如果ttl为0,那么不设置生存时间。

RESTORE在执行反序列化之前会先对序列化值的RDB版本和数据校验和进行检查,如果RDB版本不相同或者数据不完整的话,那么RESTORE会拒绝进行反序列化,并返回一个错误。

SCAN

语法:SCAN cursor [MATCH pattern] [COUNT count]

SCAN命令及其相关的SSCAN命令、HSCAN命令和ZSCAN命令都用于增量地迭代(incrementally iterate)一集元素(a collection of elements):

SCAN命令用于迭代当前数据库中的数据库键。

SSCAN命令用于迭代集合键中的元素。

HSCAN命令用于迭代哈希键中的键值对。

ZSCAN命令用于迭代有序集合中的元素(包括元素成员和元素分值)。

以上列出的四个命令都支持增量式迭代, 它们每次执行都只会返回少量元素,所以这些命令可以用于生产环境, 而不会出现像KEYS命令、SMEMBERS命令带来的问题 —— 当KEYS命令被用于处理一个大的数据库时, 又或者SMEMBERS命令被用于处理一个大的集合键时, 它们可能会阻塞服务器达数秒之久。

不过,增量式迭代命令也不是没有缺点的: 举个例子,使用SMEMBERS命令可以返回集合键当前包含的所有元素, 但是对于SCAN这类增量式迭代命令来说,因为在对键进行增量式迭代的过程中,键可能会被修改,所以增量式迭代命令只能对被返回的元素提供有限的保证 (offer limited guarantees about the returned elements)。

因为SCAN 、SSCAN 、HSCAN和ZSCAN四个命令的工作方式都非常相似, 所以这个文档会一并介绍这四个命令, 但是要记住:SSCAN命令、HSCAN命令和ZSCAN命令的第一个参数总是一个数据库键。而SCAN命令则不需要在第一个参数提供任何数据库键 —— 因为它迭代的是当前数据库中的所有数据库键。

SCAN命令的基本用法

SCAN命令是一个基于游标的迭代器(cursor based iterator): SCAN命令每次被调用之后,都会向用户返回一个新的游标,用户在下次迭代时需要使用这个新游标作为SCAN命令的游标参数,以此来延续之前的迭代过程。

当SCAN命令的游标参数被设置为0时, 服务器将开始一次新的迭代, 而当服务器向用户返回值为0的游标时, 表示迭代已结束。

以下是一个SCAN命令的迭代过程示例:

在上面这个例子中,第一次迭代使用0作为游标, 表示开始一次新的迭代。第二次迭代使用的是第一次迭代时返回的游标,也即是命令回复第一个元素的值 —— 1。

从上面的示例可以看到, SCAN命令的回复是一个包含两个元素的数组, 第一个数组元素是用于进行下一次迭代的新游标, 而第二个数组元素则是一个数组, 这个数组中包含了所有被迭代的元素。

在第二次调用SCAN命令时, 命令返回了游标0 , 这表示迭代已经结束,整个数据集(collection)已经被完整遍历过了。以0作为游标开始一次新的迭代,一直调用SCAN命令, 直到命令返回游标0,我们称这个过程为一次完整遍历(full iteration)。

三、String(字符串)

Redis字符串类型是可以用键关联的最简单的值类型,它是Memcached的唯一数据类型,所以它是新来者Redis很自然的支持类型。由于Redis的键是字符串,当我们也用字符串作为值的时候,我们从一个字符串映射另一个字符串。

Redis数据结构学习

Redis字符串表示的是一个可变的字节数组,也被称之为简单动态字符串(simple dynamic string)SDS,我们初始化字符串的内容、可以拿到字符串的长度,可以获取string的字串,可以覆盖string的字串内容,可以追加子串。

Redis数据结构学习

Redis的字符串是动态字符串,是可以修改的字符串,内部结构实现上采用预分配冗余空间的方式来减少内存的频繁分配,如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。

让我们测试一下字符串类型,初始化字符串需要提供「变量名称」和「变量的内容」:

就像你看到的一样我们使用SET和GET可以设置和检索字符串值。

SET

语法:SET key value [EX seconds] [PX milliseconds] [NX|XX]

将字符串值value关联到key,如果key已经持有其他值,SET就覆写旧值,无视类型。对于某个原本带有生存时间(TTL)的键来说, 当SET命令成功在这个键上执行时,这个键原有的TTL将被清除。值可以是各式各样的字符串,例如你可以存储一个jpeg图像进一个key,但一个值最大不能超过512MB。

从Redis 2.6.12版本开始,SET命令的行为可以通过一系列参数来修改,它提供附加的参数。例如,如果key已经存在就失败,或者如果key已经存在就成功。

NX :只在键不存在时,才对键进行设置操作。SET key value NX效果等同于SETNX key value 。

XX :只在键已经存在时,才对键进行设置操作。

EX second :设置键的过期时间为second秒。SET key value EX second效果等同于SETEX key second value 。

PX millisecond :设置键的过期时间为millisecond毫秒。SET key value PX millisecond效果等同于PSETEX key millisecond value 。

PS:因为SET命令可以通过参数来实现和SETNX 、SETEX和PSETEX三个命令的效果,所以将来的Redis版本可能会废弃并最终移除SETNX 、SETEX和PSETEX这三个命令。

GET

语法:GET key

返回key所关联的字符串值,如果key不存在那么返回特殊值nil。假如key储存的值不是字符串类型,返回一个错误,因为GET只能用于处理字符串值。

即使字符串是Redis的基本值,你可以使用有趣的操作选项执行,例如,一个是原子增量。

INCR

语法:INCR key

将key中储存的数字值加一,如果key不存在,那么key的值会先被初始化为0,然后再执行INCR操作。如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。

INCRBY

语法:INCRBY key increment

将key所储存的值加上增量元素值。

DECR

语法:DECR key

将key中储存的数字值减一,如果key不存在,那么key的值会先被初始化为0,然后再执行DECR操作。如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。

DECRBY

语法:DECRBY key decrement

将key所储存的值减去增量元素值。

INCRBYFLOAT

语法:INCRBYFLOAT key increment

为key中所储存的值加上浮点数增量元素值。

原子意味着什么?即使多个客户端使用INCR执行相同的key,也不会竞争。例如,client1和client2不会同时读取到‘10’,两个都增加到11,并设置新值为11。当其他的客户端没有执行相同命令的情况下,最终的值将保持12。

有一些操作字符串的命令。例如GETSE命令设置一个新值,返回原来的值。你可以使用这个命令,例如,如果你有个系统每次接收到访客就调用INCR增加Redis key。你可能想每小时收集一次这个信息,在不损伤增量的情况下。你可以GETSET key,将Key设置为0,并读取旧值。

为了减少延迟,一条命令执行设置或获取多个key是非常有用的。基于这种情况有MSET和MGET命令:

MSET

语法:MSET key value [key value …]

同时设置一个或多个key-value对,如果某个给定key已经存在,那么MSET会用新值覆盖原来的旧值,如果这不是你所希望的效果,请考虑使用 MSETNX命令,它只会在所有给定key都不存在的情况下进行设置操作。MSET是一个原子性(atomic)操作,所有给定key都会在同一时间内被设置,某些给定key被更新而另一些给定key没有改变的情况,不可能发生。

MGET

语法:MGET key [key …]

返回所有(一个或多个)给定 key 的值。如果给定的key里面,有某个key不存在,那么这个key返回特殊值nil。因此,该命令永不失败。

修改和查询key空间,有一些没有定义在特殊类型的命令,但是非常有助于操作key空间,因此可以用于任何的key。例如EXISTS命令返回1或0,表示指定的key是否存在于数据库,当DEL命令删除key和相关的value,无论value是什么。

从上面的例子可以看出,EXITS命令返回1或0,取决于key是否存在。

对于String(字符串)操作的其他常用命令:

STRLEN

语法:STRLEN key

返回key所储存的字符串值的长度,当key储存的不是字符串值时,返回一个错误。

APPEND

语法:APPEND key value

如果key已经存在并且是一个字符串,APPEND命令将value追加到key原来的值的末尾。如果key不存在,APPEND就简单地将给定key设为value,就像执行SET key value一样。

GETSET

语法:GETSET key value

将给定key的值设为value,并返回key的旧值(old value)。当key存在但不是字符串类型时,返回一个错误。

SETRANGE

语法:SETRANGE key offset value

用value参数覆写(overwrite)给定key所储存的字符串值,从偏移量offset开始。不存在的key当作空白字符串处理。SETRANGE命令会确保字符串足够长以便将value设置在指定的偏移量上,如果给定key原来储存的字符串长度比偏移量小(比如字符串只有5个字符长,但你设置的offset是10 ),那么原字符和偏移量之间的空白将用零字节(zerobytes, “\x00” )来填充。

GETRANGE

语法:GETRANGE key start end

返回key中字符串值的子字符串,字符串的截取范围由start和end两个偏移量决定(包括start和end在内)。负数偏移量表示从字符串最后开始计数, -1表示最后一个字符,-2表示倒数第二个,以此类推。GETRANGE通过保证子字符串的值域(range)不超过实际字符串的值域来处理超出范围的值域请求。

四、List(列表)

Redis数据结构学习

Redis将列表数据结构命名为list而不是array,是因为列表的存储结构用的是链表(Linked List)而不是数组(Array),而且链表还是双向链表。因为它是链表,所以随机定位性能较弱,首尾插入删除性能较优(使用LPUSH命令往有10个元素的list里添加一个新元素和往有1000万的list在头部添加一个元素速度一样)。如果list的列表长度很长,使用时我们一定要关注链表相关操作的时间复杂度。

负下标,链表元素的位置使用自然数0,1,2,....n-1表示,还可以使用负数-1,-2,...-n来表示,-1表示「倒数第一」,-2表示「倒数第二」,那么-n就表示第一个元素,对应的下标为0

队列/堆栈,链表可以从表头和表尾追加和移除元素,结合使用rpush/rpop/lpush/lpop四条指令,可以将链表作为队列或堆栈使用,左向右向进行都可以。

RPUSH

语法:RPUSH key value [value …]

将一个或多个值插入到列表key的表头(最右边)。如果有多个值,那么各个值按从左到右的顺序依次插入到表头: 比如说,对空列表mylist执行命令RPUSH mylist a b c,列表的值将是a b c ,这等同于原子性地执行RPUSH mylist a 、RPUSH mylist b和RPUSH mylist c三个命令。如果key不存在,一个空列表会被创建并执行RPUSH操作。当key存在但不是列表类型时,返回一个错误。

RPUSHX

语法:RPUSHX key value

将值value插入到列表key的表尾,当且仅当key存在并且是一个列表。和RPUSH命令相反,当key不存在时,RPUSHX命令什么也不做。

LPUSH

语法:LPUSH key value [value …]

将一个或多个值插入到列表key的表头(最左边)。如果有多个值,那么各个值按从左到右的顺序依次插入到表头: 比如说,对空列表mylist执行命令LPUSH mylist a b c,列表的值将是c b a ,这等同于原子性地执行LPUSH mylist a 、LPUSH mylist b和LPUSH mylist c三个命令。如果key不存在,一个空列表会被创建并执行LPUSH操作。当key存在但不是列表类型时,返回一个错误。

LPUSHX

语法:LPUSHX key value

将值value插入到列表key的表头,当且仅当key存在并且是一个列表。和LPUSH命令相反,当key不存在时,LPUSHX命令什么也不做。

LRANGE

语法:LRANGE key start stop

返回列表中指定区间内的元素,区间以偏移量startstop指定。下标(index)参数start和stop都以0为底,也就是说,以0表示列表的第一个元素,以1表示列表的第二个元素,以此类推。你也可以使用负数下标,以-1表示列表的最后一个元素, -2表示列表的倒数第二个元素,以此类推。

另外注意,LRANGE超出范围的下标值不会引起错误。如果start下标比列表的最大下标end ( LLEN list减去1 )还要大,那么LRANGE返回一个空列表。如果stop下标比end下标还要大,Redis将stop的值设置为end 。

注意LRANGE有两个索引,第一个和最后一个范围内的元素将被返回。索引都可以为负数,告诉Redis从结束开始计数:所以-1是最后一个,-2是list的倒数第二个元素,等等。

在Redis lists上有一个重要的定义是有pop元素的能力。pop元素是从list检索元素同时从list里消除元素的操作。你可以从左边或者右边pop元素,类似于往list里插入元素。

RPOP

语法:RPOP key

移除并返回列表的尾元素。

LPOP

语法:LPOP key

移除并返回列表的头元素。

如果我们尝试pop一个空的或者不存在元素,这是我们得到的结果:

LREM

语法:LREM key count value

删除元素,列表的删除操作也不是通过指定下标来确定元素的,你需要指定删除的最大个数以及元素的值。就是根据参数count的值,移除列表中与参数value相等的元素。

count的值可以是以下几种:

count > 0 : 从表头开始向表尾搜索,移除与value相等的元素,数量为count 。

count < 0 : 从表尾开始向表头搜索,移除与value相等的元素,数量为count的绝对值。

count = 0 : 移除表中所有与value相等的值。

LTRIM

语法:LTRIM key start stop

对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。举个例子,执行命令LTRIM list 0 2 ,表示只保留列表的前三个元素,其余元素全部删除。下标(index)参数start和stop都以0为底,也就是说,以0表示列表的第一个元素,以1表示列表的第二个元素,以此类推。你也可以使用负数下标,以-1表示列表的最后一个元素, -2表示列表的倒数第二个元素,以此类推。当key不是列表类型时,返回一个错误。

示例会看的更清楚:

上面的LTRIM命令告诉Redis只展示从0到2元素,其他的将会被丢弃。这可让一个非常简单但有用的模式:做一个列表推送操作和一个列表裁剪操作,以增加新的元素和丢弃超额的元素。

在实际应用场景中,我们有时候会遇到「定长列表」的需求。比如要以走马灯的形式实时显示中奖用户名列表,因为中奖用户实在太多,能显示的数量一般不超过100条,那么这里就会使用到定长列表。

LLEN

语法:LLEN key

返回列表 key 的长度,如果key不存在,则key被解释为一个空列表,返回0。如果key不是列表类型,返回一个错误。

LINDEX

语法:LINDEX key index

可以使用LINDEX指令访问指定位置(下标)的元素,下标(index)参数start和stop都以0为底,也就是说,以0表示列表的第一个元素,以1表示列表的第二个元素,以此类推。你也可以使用负数下标,以-1表示列表的最后一个元素, -2表示列表的倒数第二个元素,以此类推。

如果key不是列表类型,返回一个错误。

LSET

语法:LSET key index value

使用LSET指令在指定位置(下标)修改元素。当下标index参数超出范围,或对一个空列表(key不存在)进行LSET时,返回一个错误。

RPOPLPUSH

语法:RPOPLPUSH source destination

命令RPOPLPUSH在一个原子时间内,执行以下两个动作:

1)将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端。

2)将source 弹出的元素插入到列表destination ,作为 destination 列表的的头元素。

举个例子,你有两个列表source和destination,source列表有元素a, b, c ,destination列表有元素 x, y, z ,执行RPOPLPUSH source destination之后,source列表包含元素a, b , destination列表包含元素c, x, y, z ,并且元素c会被返回给客户端。

如果source不存在,值nil被返回,并且不执行其他动作。

如果source和destination 相同,则列表中的表尾元素被移动到表头,并返回该元素,可以把这种特殊情况视作列表的旋转(rotation)操作。

BRPOPLPUSH

语法:BRPOPLPUSH source destination timeout

BRPOPLPUSH是RPOPLPUSH的阻塞版本,当给定列表source不为空时, BRPOPLPUSH的表现和RPOPLPUSH一样。当列表source为空时, BRPOPLPUSH命令将阻塞连接,直到等待超时,或有另一个客户端对source执行LPUSH或RPUSH命令为止。

超时参数timeout接受一个以秒为单位的数字作为值。超时参数设为0表示阻塞时间可以无限期延长(block indefinitely) 。

五、Hash(哈希表)

Hash存储结构具有两大优点,首先就是查询飞快,众所周知Hash表的查询复杂度为O(1),可以提供快速查询的业务。第二则是节约内存,业务场景中如果存储的是数据集,则可以存入Hash数组节约内存。

Redis数据结构学习

哈希等价于Java语言的HashMap或者是Python语言的dict,在实现结构上它使用二维结构,第一维是数组,第二维是链表,hash的内容key和value存放在链表中,数组里存放的是链表的头指针。通过key查找元素时,先计算key的hashcode,然后用hashcode对数组的长度进行取模定位到链表的表头,再对链表进行遍历获取到相应的value值,链表的作用就是用来将产生了「hash碰撞」的元素串起来。

Redis Hash是字符串字段和字符串值之间的映射,因此它们是表示对象的完美数据类型,例如,具有多个字段(如姓名,姓氏,年龄等)的用户信息,用户ID作为key,用户的所有属性序列化成json存储为value,如下:

这样的存储结构,我们就可以直接修改对应的属性了,并且不会带来序列化和反序列化的开销。

并且具有几个字段的哈希以占用很少空间的方式存储,因此你可以将数百万个对象存储在一个小的Redis实例中。虽然Hash主要用于表示对象,但它们能够存储许多元素,因此你还可以使用Hash进行许多其他任务。每个哈希可以存储多达2**32 – 1个字段值对(超过40亿)。

可以使用HSET一次增加一个键值对,也可以使用HMSET一次增加多个键值对。可以通过HGET定位具体key对应的value,可以通过HMGET获取多个key对应的value,可以使用HGETALL获取所有的键值对,可以使用HKEYS和HVALS分别获取所有的key列表和value列表。

HSET

语法:HSET key field value

将哈希表key中的域field的值设为value。如果key不存在,一个新的哈希表被创建并进行HSET操作。如果域field已经存在于哈希表中,旧值将被覆盖。

HGET

语法:HGET key field

返回哈希表key中给定域field的值。当给定域不存在或是给定key不存在时,返回nil 。

HGETALL

语法:HGETALL key

返回哈希表key中,所有的域和值。在返回值里,紧跟每个域名(field name)之后是域的值(value),所以返回值的长度是哈希表大小的两倍。

HMSET

语法:HMSET key field value [field value …]

同时将多个field-value (域-值)对设置到哈希表key中。此命令会覆盖哈希表中已存在的域。如果key不存在,一个空哈希表被创建并执行HMSET操作。

HMGET

语法:HMGET key field [field …]

返回哈希表key中,一个或多个给定域的值。如果给定的域不存在于哈希表,那么返回一个nil值。因为不存在的key被当作一个空哈希表来处理,所以对一个不存在的key进行HMGET操作将返回一个只带有nil值的表。

HDEL

语法:HDEL key field [field …]

删除哈希表key中的一个或多个指定域,不存在的域将被忽略。

HEXISTS

语法:HEXISTS key field

查看哈希表key中,给定域field是否存在。

HKEYS

语法:HKEYS key

返回哈希表key中的所有域。

HLEN

语法:HLEN key

返回哈希表key中域的数量。

HVALS

语法:HVALS key

返回哈希表key中所有域的值。

HINCRBY

语法:HINCRBY key field increment

为哈希表key中的域field的值加上增量increment 。增量也可以为负数,相当于对给定域进行减法操作。

如果key不存在,一个新的哈希表被创建并执行HINCRBY命令。如果域field不存在,那么在执行命令前,域的值被初始化为0 。

对一个储存字符串值的域field执行HINCRBY命令将造成一个错误。本操作的值被限制在 64 位(bit)有符号数字表示之内。

HSETNX

语法:HSETNX key field value

将哈希表key中的域field的值设置为value,当且仅当域field不存在。若域field已经存在,该操作无效。如果key不存在,一个新哈希表被创建并执行HSETNX命令。

扩容:当hash内部的元素比较拥挤时(hash碰撞比较频繁),就需要进行扩容。扩容需要申请新的两倍大小的数组,然后将所有的键值对重新分配到新的数组下标对应的链表中(rehash)。如果hash结构很大,比如有上百万个键值对,那么一次完整rehash的过程就会耗时很长。这对于单线程的Redis里来说有点压力山大。所以Redis采用了渐进式rehash的方案。它会同时保留两个新旧hash结构,在后续的定时任务以及hash结构的读写指令中将旧结构的元素逐渐迁移到新的结构中。这样就可以避免因扩容导致的线程卡顿现象。

缩容:Redis的hash结构不但有扩容还有缩容,从这一点出发,它要比Java的HashMap要厉害一些,Java的HashMap只有扩容。缩容的原理和扩容是一致的,只不过新的数组大小要比旧数组小一倍。

六、Set(集合)

Redis集合是一个无序的字符串集合。Redis集合具有不允许重复成员的属性,多次添加相同的元素将导致一个集合具有该元素的单个副本。实际上这意味着添加成员不需要检查是否存在,然后执行添加操作。

关于Redis集合的一个非常有趣的事情是,它们支持多个服务器端命令来从现有集合中开始计算集合,因此你可以在很短的时间内进行差集、交集、并集的计算。你可以使用Redis Sets做许多有趣的事情,例如你可以使用Redis Sets跟踪独特的事物。想知道访问给定博客文章的所有唯一IP地址?每次处理页面浏览时,只需使用SADD,确定重复的IP不会被插入。

Redis的set结构内部也使用hash结构,所有的value都指向同一个内部值,只不过所有的value都指向同一个对象。

SADD

语法:SADD key member [member …]

将一个或多个member元素加入到集合key当中,已经存在于集合的member元素将被忽略。假如key不存在,则创建一个只包含member元素作成员的集合。

当key不是集合类型时,返回一个错误。

SCARD

语法:SCARD key

返回集合key的基数(集合中元素的数量)。

SDIFF

语法:SDIFF key [key …]

返回一个集合的全部成员,该集合是所有给定集合之间的差集,不存在的key被视为空集。

SDIFFSTORE

语法:SDIFFSTORE destination key [key …]

这个命令的作用和SDIFF类似,但它将结果保存到destination集合,而不是简单地返回结果集。如果destination集合已经存在,则将其覆盖。destination可以是key本身。

七、SortedSet(有序集合)

与Redis Sets类似,Redis SortedSet不是重复的字符串集合。不同之处在于,有序集合的每个成员都与得分相关联,可以给每一个元素赋予一个权重score,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。虽然成员是独一无二的,但可能会重复分数。

有序集合底层实现使用了两个数据结构,第一个是hash,第二个是跳跃列表,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。跳跃列表的目的在于给元素value排序,根据score的范围获取元素列表。

使用有序集合,你可以以非常快速的方式添加,删除或更新元素(在与元素数量的对数成正比的时间内)。由于元素是按顺序进行的,因此你也可以通过分数或排名(位置)以非常快的速度获得范围。访问有序集合的中间位置也非常快,因此你可以使用有序集合作为非重复元素的智能列表,你可以快速访问所需的所有内容:元素顺序,快速存在测试,快速访问中间元素!简而言之,使用有序集合,你可以做很多任务,具有很好的性能,难以在其他类型的数据库中建模。

ZADD

语法:ZADD key score member [[score member] [score member] …]

将一个或多个member元素及其score值加入到有序集key当中。如果某个member已经是有序集的成员,那么更新这个member的score值,并通过重新插入这个member元素,来保证该member在正确的位置上。score值可以是整数值或双精度浮点数。

如果key不存在,则创建一个空的有序集并执行ZADD操作。当key存在但不是有序集类型时,返回一个错误。

ZCARD

语法:ZCARD key

返回有序集key的个数,当key存在且是有序集类型时,返回有序集的个数。当key不存在时,返回0。

ZCOUNT

语法:ZCOUNT key min max

返回有序集key中,score值在min和max之间(默认包括score值等于min或max)的成员的数量。

ZSCORE

语法:ZSCORE key member

返回有序集key中,成员member的score值。如果member元素不是有序集key的成员,或key不存在,返回nil。

ZRANGE

语法:ZRANGE key start stop [WITHSCORES]

返回有序集key中,指定区间内的成员。其中成员的位置按score值递增(从小到大)来排序。具有相同score值的成员按字典序(lexicographical order)来排列。如果你需要成员按score值递减(从大到小)来排列,请使用ZREVRANGE命令。

下标参数start和stop都以0为底,也就是说,以0表示有序集第一个成员,以1表示有序集第二个成员,以此类推。你也可以使用负数下标,以-1表示最后一个成员,-2表示倒数第二个成员,以此类推。超出范围的下标并不会引起错误。比如说,当start的值比有序集的最大下标还要大,或是start > stop时,ZRANGE命令只是简单地返回一个空列表。另一方面,假如stop参数的值比有序集的最大下标还要大,那么Redis将stop当作最大下标来处理。可以通过使用WITHSCORES选项,来让成员和它的score值一并返回,返回列表以value1,score1,…,valueN,scoreN的格式表示。客户端库可能会返回一些更复杂的数据类型,比如数组、元组等。

ZRANGEBYSCORE

语法:ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

返回有序集key中,所有score值介于min和max之间(包括等于min或max)的成员。有序集成员按score值递增(从小到大)次序排列。

可选的LIMIT参数指定返回结果的数量及区间(就像SQL中的 SELECT LIMIT offset, count),注意当offset很大时,定位offset的操作可能需要遍历整个有序集,此过程最坏复杂度为O(N)时间。可选的WITHSCORES参数决定结果集是单单返回有序集的成员,还是将有序集成员及其score值一起返回。

min和max可以是-inf和+inf,这样一来,你就可以在不知道有序集的最低和最高score值的情况下,使用ZRANGEBYSCORE这类命令。默认情况下,区间的取值使用闭区间(小于等于或大于等于),你也可以通过给参数前增加符号来使用可选的开区间(小于或大于)。

ZREVRANGE

语法:ZREVRANGE key start stop [WITHSCORES]

返回有序集key中,指定区间内的成员。其中成员的位置按score值递减(从大到小)来排列。具有相同score值的成员按字典序的逆序(reverse lexicographical order)排列。除了成员按score值递减的次序排列这一点外,ZREVRANGE命令的其他方面和ZRANGE命令一样。

ZREVRANGEBYSCORE

语法:ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]

返回有序集key中,score值介于max和min之间(默认包括等于max或min)的所有的成员。有序集成员按score值递减(从大到小)的次序排列。除了成员按score值递减的次序排列这一点外,ZREVRANGEBYSCORE命令的其他方面和ZRANGEBYSCORE命令一样。

ZRANK

语法:ZRANK key member

返回有序集key中成员member的排名,其中有序集成员按score值递增(从小到大)顺序排列。排名以0为底,也就是说,score值最小的成员排名为0。使用ZREVRANK命令可以获得成员按score值递减(从大到小)排列的排名。

ZREVRANK

语法:ZREVRANK key member

返回有序集key中成员member的排名,其中有序集成员按score值递减(从大到小)排序。排名以0为底,也就是说,score值最大的成员排名为0 。使用ZRANK命令可以获得成员按score值递增(从小到大)排列的排名。

ZINCRBY

语法:ZINCRBY key increment member

为有序集key的成员member的score值加上增量increment。可以通过传递一个负数值increment,让score减去相应的值,比如ZINCRBY key -5 member ,就是让member的score值减去5。

当key不存在,或member不是key的成员时, ZINCRBY key increment member等同于ZADD key increment member。当key不是有序集类型时,返回一个错误。score 值可以是整数值或双精度浮点数。

ZREM

语法:ZREM key member [member …]

移除有序集key中的一个或多个成员,不存在的成员将被忽略。当key存在但不是有序集类型时,返回一个错误。

ZREMRANGEBYRANK

语法: ZREMRANGEBYRANK key start stop

移除有序集key中,指定排名(rank)区间内的所有成员。区间分别以下标参数start和stop指出,包含start和stop在内。下标参数start和stop都以0为底,也就是说,以0表示有序集第一个成员,以1表示有序集第二个成员,以此类推。你也可以使用负数下标,以-1表示最后一个成员,-2表示倒数第二个成员,以此类推。

ZREMRANGEBYSCORE

语法:ZREMRANGEBYSCORE key min max

移除有序集key中,所有score值介于min和max之间(包括等于min或max)的成员。自版本2.1.6开始,score值等于min或max的成员也可以不包括在内,详情请参见ZRANGEBYSCORE命令。

<扩展>

通俗易懂的Redis数据结构基础教程

深入浅出Redis-redis底层数据结构(上)

深入浅出Redis-redis底层数据结构(下)

<使用场景>

Redis String存储结构

Redis List存储结构

Redis Hash存储结构

Redis SortedSet存储结构


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (2)
[资助本站您就扫码 谢谢]
分享 (0)

您必须 登录 才能发表评论!