Redis(一)入门:五大数据类型的学习和理解
概述
- Redis是什么?
Redis(Remote Dictionary Server ),即远程字典服务 !
是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
免费和开源!是当下最热门的 NoSQL 技术之一!也被人们称之为结构化数据库!
- Redis能干嘛?
1、内存存储、持久化,内存中是断电即失、所以说持久化很重要(rdb、aof) 2、效率高,可以用于高速缓存
3、发布订阅系统
4、地图信息分析
5、计时器、计数器(浏览量!)
6、…
Redis的基本了解
- 首先我们可以看下官方文档是如何介绍Redis的:
①、英文文档 点击跳转.
②、中文文档 点击跳转.
- Redis-Key
简单介绍一下Redis中对Key的操作命令。希望大家可以跟着注释敲一遍,简单记一下,都是最常用的命令!
1 | 127.0.0.1:6379> ping #查看当前连接是否正常,正常返回PONG |
其他命令:
- DB 切换
select
默认使用的是 0 号 DB,可以通过 select db 索引来切换 DB。
dbsize
命令可以查看当前数据库中 key 的数量
删除当前库中数据
flushdb
退出客户端命令
exit
或quit
keys
- 格式:
KEYS pattern
- 功能:查找所有符合给定模式 pattern 的 key,pattern 为正则表达式。
- 说明:KEYS 的速度非常快,但在一个大的数据库中使用它可能会阻塞当前服务器的服务。所以生产环境中一般不使用该命令,而使用 scan 命令代替。
- exists
- 格式:
EXISTS key
- 功能:检查给定 key 是否存在。
- 说明:若 key 存在,返回 1 ,否则返回 0 。
- del
- 格式:
DEL key [key ...]
- 功能:删除给定的一个或多个 key 。不存在的 key 会被忽略。
- 说明:返回被删除 key 的数量。
- rename
- 格式:
RENAME key newkey
- 功能:将 key 改名为 newkey。
- 说明:当 key 和 newkey 相同,或者 key 不存在时,返回一个错误。当 newkey 已经存在时, RENAME 命令将覆盖旧值。改名成功时提示 OK ,失败时候返回一个错误。
- move
- 格式:
MOVE key db
- 功能:将当前数据库的 key 移动到给定的数据库 db 当中。
- 说明:如果当前数据库(源数据库)和给定数据库(目标数据库)有相同名字的给定 key ,或者 key 不存在于当前数据库,那么 MOVE 没有任何效果。移动成功返回 1 ,失败则返回 0 。
- type
- 格式:
TYPE key
- 功能:返回 key 所储存的值的类型。
- 说明:返回值有以下六种
- none (key 不存在)
- string (字符串)
- list (列表)
- set (集合)
- zset (有序集)
- hash (哈希表)
- expire 与 pexpire
- 格式:
EXPIRE key seconds
- 功能:为给定 key 设置生存时间。当 key 过期时(生存时间为 0),它会被自动删除。expire 的时间单位为秒,pexpire 的时间单位为毫秒。在 Redis 中,带有生存时间的 key被称为"易失的"(volatile)。
- 说明:生存时间设置成功返回 1。若 key 不存在时返回 0 。rename 操作不会改变 key的生存时间。
- ttl 与 pttl
- 格式:
TTL key
- 功能:TTL, time to live,返回给定 key 的剩余生存时间。
- 说明:其返回值存在三种可能:
- 当 key 不存在时,返回 -2 。
- 当 key 存在但没有设置剩余生存时间时,返回 -1 。
- 否则,返回 key 的剩余生存时间。ttl 命令返回的时间单位为秒,而 pttl 命令返回的时间单位为毫秒。
- persist
- 格式:
PERSIST key
- 功能:去除给定 key 的生存时间,将这个 key 从"易失的"转换成"持久的"。
- 说明:当生存时间移除成功时,返回 1;若 key 不存在或 key 没有设置生存时间,则返回 0。
- randomkey
- 格式:
RANDOMKEY
- 功能:从当前数据库中随机返回(不删除)一个 key。
- 说明:当数据库不为空时,返回一个 key。当数据库为空时,返回 nil。
- scan
- 格式:SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
- 功能:用于迭代数据库中的数据库键。其各个选项的意义为:
- cursor:本次迭代开始的游标。
- pattern :本次迭代要匹配的 key 的模式。
- count :本次迭代要从数据集里返回多少元素,默认值为 10 。
- type:本次迭代要返回的 value 的类型,默认为所有类型。
SCAN 命令是一个基于游标 cursor 的迭代器:SCAN 命令每次被调用之后,都会向用户返回返回一个包含两个元素的数组, 第一个元素是用于进行下一次迭代的新游标,而第二个元素则是一个数组, 这个数组中包含了所有被迭代的元素。用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数,以此来延续之前的迭代过程。当SCAN 命令的游标参数被设置为 0 时,服务器将开始一次新的迭代。如果新游标返回 0表示迭代已结束。
- 说明:使用间断的、负数、超出范围或者其他非正常的游标来执行增量式迭代不会造成服务器崩溃。
当数据量很大时,count 的数量的指定可能会不起作用,Redis 会自动调整每次的遍历数目。由于 scan 命令每次执行都只会返回少量元素,所以该命令可以用于生产环境,而不会出现像 KEYS 命令带来的服务器阻塞问题。
增量式迭代命令所使用的算法只保证在数据集的大小有界的情况下迭代才会停止,换句话说,如果被迭代数据集的大小不断地增长的话,增量式迭代命令可能永远也无法完成一次完整迭代。即当一个数据集不断地变大时,想要访问这个数据集中的所有元素就需要做越来越多的工作, 能否结束一个迭代取决于用户执行迭代的速度是否比数据集增长的速度更快。 - 相关命令:另外还有 3 个 scan 命令用于对三种类型的 value 进行遍历。
- hscan:属于 Hash 型 Value 操作命令集合,用于遍历当前 db 中指定 Hash 表的所有 field-value 对。
- sscan:属于 Set 型 Value 操作命令集合,用于遍历当前 db 中指定 set 集合的所有元素
- zscan:属于 ZSet 型 Value 操作命令集合,用于遍历当前 db 中指定有序集合的所有元素(数值与元素值)
Redis的五大数据类型
String(字符串)
Redis 存储数据的 Value 可以是一个 String 类型数据。String 类型的 Value 是 Redis 中最基本,最常见的类型。String 类型的 Value 中可以存放任意数据,包括数值型,甚至是二进制的图片、音频、视频、序列化对象等。一个 String 类型的 Value 最大是 512M 大小。
快速上手
①添加、查询、追加、获取长度,判断是否存在的操作
1 | 127.0.0.1:6379> set name dingdada #插入一个key为'name'值为'dingdada'的数据 |
②自增、自减操作
1 | 127.0.0.1:6379> set num 0 #插入一个初始值为0的数据 |
③截取、替换字符串操作
1 | #截取 |
④设置过期时间、不存在设置操作
1 | #设置过期时间,跟Expire的区别是前者设置已存在的key的过期时间,而setex是在创建的时候设置过期时间 |
⑤mset、mget操作
1 | 127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 #插入多条数据 |
⑥添加获取对象、getset操作
1 | #这里其实本质上还是字符串,但是我们讲其key巧妙的设计了一下。 |
⑦总结
String是Redis中最常用的一种数据类型,也是Redis中最简单的一种数据类型。首先,表面上它是字符串,但其实他可以灵活的表示字符串、整数、浮点数3种值。Redis会自动的识别这3种值。
详细了解
- set
- 格式:
SET key value [EX seconds | PX milliseconds] [NX|XX]
- 功能:SET 除了可以直接将 key 的值设为 value 外,还可以指定一些参数。
- EX seconds:为当前 key 设置过期时间,单位秒。等价于 SETEX 命令。
- PX milliseconds:为当前 key 设置过期时间,单位毫秒。等价于 PSETEX 命令。
- NX:指定的 key 不存在才会设置成功,用于添加指定的 key。等价于 SETNX 命令。
- XX:指定的 key 必须存在才会设置成功,用于更新指定 key 的 value。
- 说明:如果 value 字符串中带有空格,则该字符串需要使用双引号或单引号引起来,否则会认为 set 命令的参数数量不正确,报错。
- setex 与 psetex
- 格式:
SETEX/PSETEX key seconds value
- 功能:set expire,其不仅为 key 指定了 value,还为其设置了生存时间。setex 的单位为秒,psetex 的单位为毫秒。
- 说明:如果 key 已经存在, 则覆写旧值。该命令类似于以下两个命令,不同之处是,SETEX 是一个原子性操作,关联值和设置生存时间两个动作会在同一时间内完成,该命令在 Redis 用作缓存时,非常实用。
SET key value
EXPIRE key seconds # 设置生存时间
- setnx
- 格式:
SETNX key value
- 功能:SET if Not eXists,将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key已经存在,则 SETNX 不做任何动作。成功,返回 1,否则,返回 0。
- 说明:该命令等价于 set key value nx
- getset
- 格式:
GETSET key value
- 功能:将给定 key 的值设为 value ,并返回 key 的旧值。
- 说明:当 key 存在但不是字符串类型时,返回一个错误;当 key 不存在时,返回 nil 。
- mset 与 msetnx
- 格式:
MSET/MSETNX key value [key value ...]
- 功能:同时设置一个或多个 key-value 对。
- 说明:如果某个给定 key 已经存在,那么 MSET 会用新值覆盖原来的旧值,如果这不是你所希望的效果,请考虑使用 MSETNX 命令:它只会在所有给定 key 都不存在的情况下进行设置操作。MSET/MSETNX 是一个原子性(atomic)操作,所有给定 key 都会在同一时间内被设置,某些给定 key 被更新而另一些给定 key 没有改变的情况不可能发生。该命令永不失败。
- mget
- 格式:
MGET key [key ...]
- 功能:返回所有(一个或多个)给定 key 的值。
- 说明:如果给定的 key 里面,有某个 key 不存在,那么这个 key 返回特殊值 nil 。因此,该命令永不失败。
- append
- 格式:
APPEND key value
- 功能:如果 key 已经存在并且是一个字符串, APPEND 命令将 value 追加到 key 原来的值的末尾。如果 key 不存在, APPEND 就简单地将给定 key 设为 value ,就像执行 SET key value 一样。
- 说明:追加 value 之后, key 中字符串的长度。
- incr 与 decr
格式:
INCR key 或 DECR key
功能:increment,自动递增。将 key 中存储的数字值增一。
decrement,自动递减。将 key 中存储的数字值减一。
说明:如果 key 不存在,那么 key 的值会先被初始化为 0,然后再执行增一/减一操作。如果值不能表示为数字,那么返回一个错误提示。如果执行正确,则返回增一/减一后的值。
- incrby 与 decrby
- 格式:
INCRBY key increment
或DECRBY key decrement
- 功能:将 key 中存储的数字值增加/减少指定的数值,这个数值只能是整数,可以是负数,但不能是小数。
- 说明:如果 key 不存在,那么 key 的值会先被初始化为 0,然后再执行增/减操作。如果值不能表示为数字,那么返回一个错误提示。如果执行正确,则返回增/减后的值。
- incrbyfloat
- 格式:
INCRBYFLOAT key increment
- 功能:为 key 中所储存的值加上浮点数增量 increment 。
- 说明:与之前的说明相同。没有 decrbyfloat 命令,但 increment 为负数可以实现减操作效果。
- strlen
- 格式:
STRLEN key
- 功能:返回 key 所储存的字符串值的长度。
- 说明:当 key 储存的不是字符串值时,返回一个错误;当 key 不存在时,返回 0 。
- getrange
- 格式:
GETRANGE key start end
- 功能:返回 key 中字符串值的子字符串,字符串的截取范围由 start 和 end 两个偏移量决定,包括 start 和 end 在内。
- 说明:end 必须要比 start 大。支持负数偏移量,表示从字符串最后开始计数,-1 表示最后一个字符,-2 表示倒数第二个,以此类推。
- setrange
- 格式:
SETRANGE key offset value
- 功能:用 value 参数替换给定 key 所储存的字符串值 str,从偏移量 offset 开始。
- 说明:当 offset 值大于 str 长度时,中间使用零字节\x00 填充,即 0000 0000 字节填充;对于不存在的 key 当作空串处理。
- 位操作命令
名称中包含 BIT 的命令,都是对二进制位的操作命令,例如,setbit、getbit、bitcount、bittop、bitfield,这些命令不常用。
典型应用场景
Value 为 String 类型的应用场景很多,这里仅举这种典型应用场景的例子:
- 数据缓存
Redis 作为数据缓存层,MySQL 作为数据存储层。应用服务器首先从 Redis 中获取数据,如果缓存层中没有,则从 MySQL 中获取后先存入缓存层再返回给应用服务器。
- 计数器
在 Redis 中写入一个 value 为数值型的 key 作为平台计数器、视频播放计数器等。每个有效客户端访问一次,或视频每播放一次,都是直接修改 Redis 中的计数器,然后再以异步方式持久化到其它数据源中,例如持久化到 MySQL。
- 共享 Session
对于一个分布式应用系统,如果将类似用户登录信息这样的 Session 数据保存在提供登录服务的服务器中,那么如果用户再次提交像收藏、支付等请求时可能会出现问题:在提供收藏、支付等服务的服务器中并没有该用户的 Session 数据,从而导致该用户需要重新登录。对于用户来说,这是不能接受的。
此时,可以将系统中所有用户的 Session 数据全部保存到 Redis 中,用户在提交新的请求后,系统先从 Redis 中查找相应的 Session 数据,如果存在,则再进行相关操作,否则跳转到登录页面。这样就不会引发"重新登录"问题。
- 限速器
现在很多平台为了防止 DoS(Denial of Service,拒绝服务)攻击,一般都会限制一个 IP不能在一秒内访问超过 n 次。而 Redis 可以可以结合 key 的过期时间与 incr 命令来完成限速功能,充当限速器。
其无法防止 DDoS(Distributed Denial of Service,分布式拒绝服务)攻击。
1 | // 客户端每提交一次请求,都会执行下面的代码 |
List(列表)
Redis 存储数据的 Value 可以是一个 String 列表类型数据。即该列表中的每个元素均为String 类型数据。列表中的数据会按照插入顺序进行排序。不过,该列表的底层实际是一个无头节点的双向链表,所以对列表表头与表尾的操作性能较高,但对中间元素的插入与删除的操作的性能相对较差。
快速上手
①lpush(左插入)、lrange(查询集合)、rpush(右插入)操作
1 | #lpush |
②lpop(左移除)、rpop(右移除)操作
1 | #lpop |
③lindex(查询指定下标元素)、llen(获取集合长度) 操作
1 | #lindex |
④lrem(根据value移除指定的值)
1 | 127.0.0.1:6379> LRANGE list 0 -1 |
⑤ltrim(截取元素)、rpoplpush(移除指定集合中最后一个元素到一个新的集合中)操作
1 | #ltrim |
⑥lset(更新)、linsert操作
1 | #lset |
⑦小结:
- 实际上是一个链表,before Node after , left,right 都可以插入值
- 如果key 不存在,创建新的链表
- 如果key存在,新增内容
- 如果移除了所有值,空链表,也代表不存在!
- 在两边插入或者改动值,效率最高! 中间元素,相对来说效率会低一点~
- 消息排队!消息队列 (Lpush Rpop), 栈( Lpush Lpop)!
详细了解
- lpush/rpush
- 格式:
LPUSH key value [value ...]
或RPUSH key value [value ...]
- 功能:将一个或多个值 value 插入到列表 key 的表头/表尾(表头在左表尾在右)
- 说明:如果有多个 value 值,对于 lpush 来说,各个 value 会按从左到右的顺序依次插入到表头;对于 rpush 来说,各个 value 会按从左到右的顺序依次插入到表尾。如果 key不存在,一个空列表会被创建并执行操作。当 key 存在但不是列表类型时,返回一个错误。执行成功时返回列表的长度。
- llen
- 格式:
LLEN key
- 功能:返回列表 key 的长度。
- 说明:如果 key 不存在,则 key 被解释为一个空列表,返回 0 。如果 key 不是列表类型,返回一个错误。
- lindex
- 格式:
LINDEX key index
- 功能:返回列表 key 中,下标为 index 的元素。列表从 0 开始计数。
- 说明:如果 index 参数的值不在列表的区间范围内(out of range),返回 nil 。
- lset
- 格式:
LSET key index value
- 功能:将列表 key 下标为 index 的元素的值设置为 value 。
- 说明:当 index 参数超出范围,或对一个空列表(key 不存在)进行 LSET 时,返回一个错误。
- lrange
- 格式:
LRANGE key start stop
- 功能:返回列表 key 中指定区间[start, stop]内的元素,即包含两个端点。
- 说明:List 的下标从 0 开始,即以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。超出范围的下标值不会引起错误。如果 start 下标比列表的最大下标 还要大,那么 LRANGE 返回一个空列表。如果 stop 下标比最大下标还要大,Redis 将 stop 的值设置为最大下标。
- lpushx 与 rpushx
- 格式:
LPUSHX key value
或RPUSHX key value
- 功能:将值 value 插入到列表 key 的表头/表尾,当且仅当 key 存在并且是一个列表。
- 说明:当 key 不存在时,命令什么也不做。若执行成功,则输出表的长度。
- linsert
- 格式:
LINSERT key BEFORE|AFTER pivot value
- 功能:将值 value 插入到列表 key 当中,位于元素 pivot 之前或之后。
- 说明:当 pivot 元素不存在于列表中时,不执行任何操作,返回-1;当 key 不存在时,key 被视为空列表,不执行任何操作,返回 0;如果 key 不是列表类型,返回一个错误;如果命令执行成功,返回插入操作完成之后,列表的长度。
- lpop / rpop
- 格式:
LPOP key [count]
或RPOP key [count]
- 功能:从列表 key 的表头/表尾移除 count 个元素,并返回移除的元素。count 默认值 1
- 说明:当 key 不存在时,返回 nil
- blpop / brpop
- 格式:
BLPOP key [key ...] timeout
或BRPOP key [key ...] timeout
- 功能:BLPOP/BRPOP 是列表的阻塞式(blocking)弹出命令。它们是 LPOP/RPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP/BRPOP 命令阻塞,直到等待 timeout 超时或发现可弹出元素为止。当给定多个 key 参数时,按参数 key的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。timeout 为阻塞时长,单位为秒,其值若为 0,则表示只要没有可弹出元素,则一直阻塞。
- 说明:假如在指定时间内没有任何元素被弹出,则返回一个 nil 和等待时长。反之,返回一个含有两个元素的列表,第一个元素是被弹出元素所属的 key ,第二个元素是被弹出元素的值。
- rpoplpush
格式:
RPOPLPUSH source destination
功能:命令 RPOPLPUSH 在一个原子时间内,执行以下两个动作:
- 将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端。
- 将 source 弹出的元素插入到列表 destination ,作为 destination 列表的的头元素。
如果 source 不存在,值 nil 被返回,并且不执行其他动作。如果 source 和 destination相同,则列表中的表尾元素被移动到表头,并返回该元素,可以把这种特殊情况视作列表的旋转(rotation)操作。
- brpoplpush
- 格式:
BRPOPLPUSH source destination timeout
- 功能:BRPOPLPUSH 是 RPOPLPUSH 的阻塞版本,当给定列表 source 不为空时,BRPOPLPUSH 的表现和 RPOPLPUSH 一样。当列表 source 为空时, BRPOPLPUSH 命令将阻塞连接,直到等待超时,或有另一个客户端对 source 执行 LPUSH 或 RPUSH 命令为止。timeout 为阻塞时长,单位为秒,其值若为 0,则表示只要没有可弹出元素,则一直阻塞。
- 说明:假如在指定时间内没有任何元素被弹出,则返回一个 nil 和等待时长。反之,返回一个含有两个元素的列表,第一个元素是被弹出元素的值,第二个元素是等待时长。
- lrem
- 格式:
LREM key count value
- 功能:根据参数 count 的值,移除列表中与参数 value 相等的元素。count 的值可以是以下几种:
- count > 0 : 从表头开始向表尾搜索,移除与 value 相等的元素,数量为 count 。
- count < 0 : 从表尾开始向表头搜索,移除与 value 相等的元素,数量为 count 的绝对值。
- count = 0 : 移除表中所有与 value 相等的值。
- 说明:返回被移除元素的数量。当 key 不存在时, LREM 命令返回 0 ,因为不存在的 key 被视作空表(empty list)。
- ltrim
- 格式:
LTRIM key start stop
- 功能:对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
- 说明:下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。当 key 不是列表类型时,返回一个错误。如果 start 下标比列表的最大下标 end ( LLEN list 减去 1 )还要大,或者 start > stop , LTRIM 返回一个空列表,因为 LTRIM 已经将整个列表清空。如果 stop 下标比 end 下标还要大,Redis 将 stop 的值设置为 end 。
应用场景
Value 为 List 类型的应用场景很多,主要是通过构建不同的数据结构来实现相应的业务功能。这里仅对这些数据结构的实现方式进行总结,不举具体的例子。
- 栈
通过 lpush + lpop 可以实现栈数据结构效果:先进后出。通过 lpush 从列表左侧插入数据,通过 lpop 从列表左侧取出数据。当然,通过 rpush + rpop 也可以实现相同效果,只不过操作的是列表右侧。
- 队列
通过 lpush + rpop 可以实现队列数据结构效果:先进先出。通过 lpush 从列表左侧插入数据,通过 rpop 从列表右侧取出数据。当然,通过 rpush + lpop 也可以实现相同效果,只不过操作的方向正好相反。
- 阻塞式消息队列
通过 lpush + brpop 可以实现阻塞式消息队列效果。作为消息生产者的客户端使用 lpush从列表左侧插入数据,作为消息消费者的多个客户端使用 brpop 阻塞式"抢占"列表尾部数据进行消费,保证了消费的负载均衡与高可用性。brpop 的 timeout 设置为 0,表示只要没有数据可弹出,就永久阻塞。
- 动态有限集合
通过 lpush + ltrim 可以实现有限集合。通过 lpush 从列表左侧向列表中添加数据,通过ltrim 保持集合的动态有限性。像企业的末位淘汰、学校的重点班等动态管理,都可通过这种动态有限集合来实现。当然,通过 rpush + ltrim 也可以实现相同效果,只不过操作的方向正好相反。
Set(集合)元素唯一不重复
Redis 存储数据的 Value 可以是一个 Set 集合,且集合中的每一个元素均 String 类型。Set与 List 非常相似,但不同之处是 Set 中的元素具有无序性与不可重复性,而 List 则具有有序性与可重复性。
Redis 中的 Set 集合与 Java 中的 Set 集合的实现相似,其底层都是 value 为 null 的 hash表。也正因为此,才会引发无序性与不可重复性。
快速上手
①sadd(添加)、smembers(查看所有元素)、sismember(判断是否存在)、scard(查看长度)、srem(移除指定元素)操作
1 | #set中所有的元素都是唯一的不重复的! |
②srandmember(抽随机)操作
1 | 127.0.0.1:6379> sadd myset 1 2 3 4 5 6 7 #在set中添加7个元素 |
③spop(随机删除元素)、smove(移动指定元素到新的集合中)操作
1 | 127.0.0.1:6379> SMEMBERS myset |
④sdiff(差集)、sinter(交集)、sunion(并集)操作
1 | 127.0.0.1:6379> sadd myset1 1 2 3 4 5 |
⑤总结:可实现共同好友、共同关注等需求。
详细了解
- sadd
- 格式:
SADD key member [member ...]
- 功能:将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member元素将被忽略。
- 说明:假如 key 不存在,则创建一个只包含 member 元素作成员的集合。当 key 不是集合类型时,返回一个错误。
- smembers
- 格式:
SMEMBERS key
- 功能:返回集合 key 中的所有成员。
- 说明:不存在的 key 被视为空集合。若 key 中包含大量元素,则该命令可能会阻塞 Redis服务。所以生产环境中一般不使用该命令,而使用 sscan 命令代替。
- scard
- 格式:
SCARD key
- 功能:返回 Set 集合的长度
- 说明:当 key 不存在时,返回 0 。
- sismember
- 格式:
SISMEMBER key member
- 功能:判断 member 元素是否集合 key 的成员。
- 说明:如果 member 元素是集合的成员,返回 1 。如果 member 元素不是集合的成员,或 key 不存在,返回 0 。
- smove
- 格式:
SMOVE source destination member
- 功能:将 member 元素从 source 集合移动到 destination 集合。
- 说明:如果 source 集合不存在或不包含指定的 member 元素,则 SMOVE 命令不执行任何操作,仅返回 0 。否则, member 元素从 source 集合中被移除,并添加到destination 集合中去,返回 1。当 destination 集合已经包含 member 元素时, SMOVE命令只是简单地将 source 集合中的 member 元素删除。当 source 或 destination 不是集合类型时,返回一个错误。
- srem
- 格式:
SREM key member [member ...]
- 功能:移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略,且返回成功移除的元素个数。
- 说明:当 key 不是集合类型,返回一个错误。
- srandmember
- 格式:
SRANDMEMBER key [count]
- 功能:返回集合中的 count 个随机元素。count 默认值为 1。
- 说明:若 count 为正数,且小于集合长度,那么返回一个包含 count 个元素的数组,数组中的元素各不相同。如果 count 大于等于集合长度,那么返回整个集合。如果count 为负数,那么返回一个包含 count 绝对值个元素的数组,但数组中的元素可能会出现重复。
- spop
- 格式:
SPOP key [count]
- 功能:移除并返回集合中的 count 个随机元素。count 必须为正数,且默认值为 1。
- 说明:如果 count 大于等于集合长度,那么移除并返回整个集合。
- sdiff / sdiffstore
- 格式:
SDIFF key [key ...]
或SDIFFSTORE destination key [key ...]
- 功能:返回第一个集合与其它集合之间的差集。差集,difference。
- 说明:这两个命令的不同之处在于,sdiffstore 不仅能够显示差集,还能将差集存储到指定的集合 destination 中。如果 destination 集合已经存在,则将其覆盖。不存在的 key 被视为空集。
- sinter / sinterstore
- 格式:
SINTER key [key ...]
或SINTERSTORE destination key [key ...]
- 功能:返回多个集合间的交集。交集,intersection。
- 说明:这两个命令的不同之处在于,sinterstore 不仅能够显示交集,还能将交集存储到指定的集合 destination 中。如果 destination 集合已经存在,则将其覆盖。不存在的 key被视为空集。
- sunion / sunionstore
- 格式:
SUNION key [key ...]
或SUNIONSTORE destination key [key ...]
- 功能:返回多个集合间的并集。并集,union。
- 说明:这两个命令的不同之处在于,sunionstore 不仅能够显示并集,还能将并集存储到指定的集合 destination 中。如果 destination 集合已经存在,则将其覆盖。不存在的 key被视为空集。
应用场景
Value 为 Set 类型的应用场景很多,这里对这些场景仅进行总结。
- 动态黑白名单
例如某服务器中要设置用于访问控制的黑名单。如果直接将黑名单写入服务器的配置文件,那么存在的问题是,无法动态修改黑名单。此时可以将黑名单直接写入 Redis,只要有客户端来访问服务器,服务器在获取到客户端 IP 后先从 Redis 的黑名单中查看是否存在该 IP,如果存在,则拒绝访问,否则访问通过。
- 有限随机数
有限随机数是指返回的随机数是基于某一集合范围内的随机数,例如抽奖、随机选人。通过 spop 或 srandmember 可以实现从指定集合中随机选出元素。
- 用户画像
社交平台、电商平台等各种需要用户注册登录的平台,会根据用户提供的资料与用户使用习惯,为每个用户进行画像,即为每个用户定义很多可以反映该用户特征的标签,这些标签就可以使用 sadd 添加到该用户对应的集合中。这些标签具有无序、不重复特征。
同时平台还可以使用 sinter/sinterstore 根据用户画像间的交集进行好友推荐、商品推荐、客户推荐等。
Hash(哈希)
Redis 存储数据的 Value 可以是一个 Hash 类型。Hash 类型也称为 Hash 表、字典等。
Hash 表就是一个映射表 Map,也是由键-值对构成,为了与整体的 key 进行区分,这里的键称为 field,值称为 value。
Redis 的 Hash 表中的 field-value 对均为 String 类型。
快速上手
①hset(添加hash)、hget(查询)、hgetall(查询所有)、hdel(删除hash中指定的值)、hlen(获取hash的长度)、hexists(判断key是否存在)操作
1 | 127.0.0.1:6379> hset myhash name dingdada age 23 #添加hash,可多个 |
②hkeys(获取所有key)、hvals(获取所有value)、hincrby(给值加增量)、hsetnx(存在不添加)操作
1 | 127.0.0.1:6379> hset myhash age 23 high 173 |
③总结:比String更加适合存对象~
详细了解
- hset
- 格式:
HSET key field value
- 功能:将哈希表 key 中的域 field 的值设为 value 。
- 说明:如果 key 不存在,一个新的哈希表被创建并进行 HSET 操作。如果域 field 已经存在于哈希表中,旧值将被覆盖。如果 field 是哈希表中的一个新建域,并且值设置成功,返回 1 。如果哈希表中域 field 已经存在且旧值已被新值覆盖,返回 0 。
- hget
- 格式:
HGET key field
- 功能:返回哈希表 key 中给定域 field 的值。
- 说明:当给定域不存在或是给定 key 不存在时,返回 nil 。
- hmset
- 格式:
HMSET key field value [field value ...]
- 功能:同时将多个 field-value (域-值)对设置到哈希表 key 中。
- 说明:此命令会覆盖哈希表中已存在的域。如果 key 不存在,一个空哈希表被创建并执行 HMSET 操作。如果命令执行成功,返回 OK 。当 key 不是哈希表(hash)类型时,返回一个错误。
- hmget
- 格式:
HMGET key field [field ...]
- 功能:按照给出顺序返回哈希表 key 中一个或多个域的值。
- 说明:如果给定的域不存在于哈希表,那么返回一个 nil 值。因为不存在的 key 被当作一个空哈希表来处理,所以对一个不存在的 key 进行 HMGET 操作将返回一个只带有 nil 值的表。
- hgetall
- 格式:
HGETALL key
- 功能:返回哈希表 key 中所有的域和值。
- 说明:在返回值里,紧跟每个域名(field name)之后是域的值(value),所以返回值的长度是哈希表大小的两倍。若 key 不存在,返回空列表。若 key 中包含大量元素,则该命令可能会阻塞 Redis 服务。所以生产环境中一般不使用该命令,而使用 hscan 命令代替。
- hsetnx
- 格式:
HSETNX key field value
- 功能:将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在。
- 说明:若域 field 已经存在,该操作无效。如果 key 不存在,一个新哈希表被创建并执行 HSETNX 命令。
- hdel
- 格式:
HDEL key field [field ...]
- 功能:删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
- 说明:返回被成功移除的域的数量,不包括被忽略的域。
- hexits
- 格式:
HEXISTS key field
- 功能:查看哈希表 key 中给定域 field 是否存在。
- 说明:如果哈希表含有给定域,返回 1 。如果不含有给定域,或 key 不存在,返回 0 。
- hincrby 与 hincrbyfloat
- 格式:
HINCRBY key field increment
- 功能:为哈希表 key 中的域 field 的值加上增量 increment 。hincrby 命令只能增加整数值,而 hincrbyfloat 可以增加小数值。
- 说明:增量也可以为负数,相当于对给定域进行减法操作。如果 key 不存在,一个新的哈希表被创建并执行 HINCRBY 命令。如果域 field 不存在,那么在执行命令前,域的值被初始化为 0。对一个储存字符串值的域 field 执行 HINCRBY 命令将造成一个错误。
- hkeys 与 hvals
- 格式:
HKEYS key
或HVALS key
- 功能:返回哈希表 key 中的所有域/值。
- 说明:当 key 不存在时,返回一个空表。
- hlen
- 格式:
HLEN key
- 功能:返回哈希表 key 中域的数量。
- 说明:当 key 不存在时,返回 0 。
- hstrlen
- 格式:
HSTRLEN key field
- 功能:返回哈希表 key 中, 与给定域 field 相关联的值的字符串长度(string length)。
- 说明:如果给定的键或者域不存在, 那么命令返回 0 。
应用场景
Hash 型 Value 非常适合存储对象数据。key 为对象名称,value 为描述对象属性的 Map,对对象属性的修改在 Redis 中就可直接完成。其不像 String 型 Value 存储对象,那个对象是序列化过的,例如序列化为 JSON 串,对对象属性值的修改需要先反序列化为对象后再修改,修改后再序列化为 JSON 串后写入到 Redis。
zSet(有序集合)
Redis 存储数据的 Value 可以是一个有序 Set,这个有序 Set 中的每个元素均 String 类型。有序 Set 与 Set 的不同之处是,有序 Set 中的每一个元素都有一个分值 score,Redis 会根据score 的值对集合进行由小到大的排序。其与 Set 集合要求相同,元素不能重复,但元素的score 可以重复。由于该类型的所有命令均是字母 z 开头,所以该 Set 也称为 ZSet。
快速上手
①zadd(添加)、zrange(查询)、zrangebyscore(排序小-大)、zrevrange(排序大-小)、zrangebyscore withscores(查询所有值包含key)操作
1 | 127.0.0.1:6379> zadd myzset 1 one 2 two 3 three #添加zset值,可多个 |
②zrem(移除元素)、zcard(查看元素个数)、zcount(查询指定区间内的元素个数)操作
1 | 127.0.0.1:6379> zadd myset 1 v1 2 v2 3 v3 4 v4 |
③总结:成绩表排序,工资表排序,年龄排序等需求可以用zset来实现!
详细了解
- zadd
- 格式:
ZADD key score member [[score member] [score member] ...]
- 功能:将一个或多个 member 元素及其 score 值加入到有序集 key 中的适当位置。
- 说明:score 值可以是整数值或双精度浮点数。如果 key 不存在,则创建一个空的有序集并执行 ZADD 操作。当 key 存在但不是有序集类型时,返回一个错误。如果命令执行成功,则返回被成功添加的新成员的数量,不包括那些被更新的、已经存在的成员。若写入的 member 值已经存在,但 score 值不同,则新的 score 值将覆盖老 score。
- zrange 与 zrevrange
- 格式:
ZRANGE key start stop [WITHSCORES]
或ZREVRANGE key start stop [WITHSCORES]
- 功能:返回有序集 key 中,指定区间内的成员。zrange 命令会按 score 值递增排序,zrevrange 命令会按 score 递减排序。具有相同 score 值的成员按字典序/逆字典序排列。可以通过使用 WITHSCORES 选项,来让成员和它的 score 值一并返回。
- 说明:下标参数从 0 开始,即 0 表示有序集第一个成员,以 1 表示有序集第二个成员,以此类推。也可以使用负数下标,-1 表示最后一个成员,-2 表示倒数第二个成员,以此类推。超出范围的下标并不会引起错误。例如,当 start 的值比有序集的最大下标还要大,或是 start > stop 时, ZRANGE 命令只是简单地返回一个空列表。再比如 stop 参数的值比有序集的最大下标还要大,那么 Redis 将 stop 当作最大下标来处理。
若 key 中指定范围内包含大量元素,则该命令可能会阻塞 Redis 服务。所以生产环境中如果要查询有序集合中的所有元素,一般不使用该命令,而使用 zscan 命令代替。
- zrangebyscore 与 zrevrangebyscore
格式:
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
功能:返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增/递减次序排列。具有相同 score 值的成员按字典序/逆字典序排列。可选的 LIMIT 参数指定返回结果的数量及区间(就像 SQL 中的SELECT LIMIT offset, count ),注意当 offset 很大时,定位 offset 的操作可能需要遍历整个有序集,此过程效率可能会较低。可选的 WITHSCORES 参数决定结果集是单单返回有序集的成员,还是将有序集成员及其 score 值一起返回。
说明:min 和 max 的取值是正负无穷大的。默认情况下,区间的取值使用闭区间 (小于等于或大于等于),也可以通过给参数前增加左括号"("来使用可选的开区间 (小于或大于)。
- zcard
- 格式:
ZCARD 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 。
- zincrby
- 格式:
ZINCRBY key increment member
- 功能:为有序集 key 的成员 member 的 score 值加上增量 increment 。increment 值可以是整数值或双精度浮点数。
- 说明:可以通过传递一个负数值 increment ,让 score 减去相应的值。当 key 不存在,或 member 不是 key 的成员时, ZINCRBY key increment member 等同于 ZADD keyincrement member 。当 key 不是有序集类型时,返回一个错误。命令执行成功,则返回 member 成员的新 score 值。
- zrank 与 zrevrank
- 格式:
ZRANK key member
或ZREVRANK key member
- 功能:返回有序集 key 中成员 member 的排名。zrank 命令会按 score 值递增排序,zrevrank 命令会按 score 递减排序。
- 说明:score 值最小的成员排名为 0 。如果 member 不是有序集 key 的成员,返回 nil 。
- zrem
- 格式:
ZREM key member [member ...]
- 功能:移除有序集 key 中的一个或多个成员,不存在的成员将被忽略。
- 说明:当 key 存在但不是有序集类型时,返回一个错误。执行成功,则返回被成功移除的成员的数量,不包括被忽略的成员。
- zremrangebyrank
- 格式:
ZREMRANGEBYRANK key start stop
- 功能:移除有序集 key 中,指定排名(rank)区间内的所有成员。
- 说明:排名区间分别以下标参数 start 和 stop 指出,包含 start 和 stop 在内。排名区间参数从 0 开始,即 0 表示排名第一的成员, 1 表示排名第二的成员,以此类推。也可以使用负数表示,-1 表示最后一个成员,-2 表示倒数第二个成员,以此类推。命令执行成功,则返回被移除成员的数量。
- zremrangebyscore
- 格式:
ZREMRANGEBYSCORE key min max
- 功能:移除有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。
- 说明:命令执行成功,则返回被移除成员的数量。
- zrangebylex
- 格式:
ZRANGEBYLEX key min max [LIMIT offset count]
- 功能:该命令仅适用于集合中所有成员都具有相同分值的情况。当有序集合的所有成员都具有相同的分值时,有序集合的元素会根据成员的字典序(lexicographical ordering)来进行排序。即这个命令返回给定集合中元素值介于 min 和 max 之间的成员。如果有序集合里面的成员带有不同的分值, 那么命令的执行结果与 zrange key 效果相同。
- 说明:合法的 min 和 max 参数必须包含左小括号"(“或左中括号”[“,其中左小括号”(“表示开区间, 而左中括号”[“则表示闭区间。min 或 max 也可使用特殊字符”+“和”-",分别表示正无穷大与负无穷大。
- zlexcount
- 格式:
ZLEXCOUNT key min max
- 功能:该命令仅适用于集合中所有成员都具有相同分值的情况。该命令返回该集合中元素值本身(而非 score 值)介于 min 和 max 范围内的元素数量。
- zremrangebylex
- 格式:
ZREMRANGEBYLEX key min max
- 功能:该命令仅适用于集合中所有成员都具有相同分值的情况。该命令会移除该集合中元素值本身介于 min 和 max 范围内的所有元素。
应用场景
有序 Set 最为典型的应用场景就是排行榜,例如音乐、视频平台中根据播放量进行排序的排行榜;电商平台根据用户评价或销售量进行排序的排行榜等。将播放量作为 score,将作品 id 作为 member,将用户评价积分或销售量作为 score,将商家 id 作为 member。使用zincrby 增加排序 score,使用 zrevrange 获取 Top 前几名,使用 zrevrank 查询当前排名,使用zscore 查询当前排序 score 等。
benchmark 测试工具
简介
在 Redis 安装完毕后会自动安装一个 redis-benchmark 测试工具,其是一个压力测试工具,用于测试 Redis 的性能。
1 | ll /usr/local/bin | grep redis |
通过 redis-benchmark –help
命令可以查看到其用法:
测试 1
命令解析
1 | redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 100000 -d 8 |
以上命令中选项的意义:
- -h:指定要测试的 Redis 的 IP,若为本机,则可省略
- -p:指定要测试的 Redis 的 port,若为 6379,则可省略
- -c:指定模拟有客户端的数量,默认值为 50
- -n:指定这些客户端发出的请求的总量,默认值为 100000
- -d:指定测试 get/set 命令时其操作的 value 的数据长度,单位字节,默认值为 3。在测试其它命令时该指定没有用处。
以上命令的意义是,使用 100 个客户端连接该 Redis,这些客户端总共会发起 100000个请求,set/get 的 value 为 8 字节数据。
测试结果分析
该命令会逐个测试所有 Redis 命令,每个命令都会给出一份测试报告,每个测试报告由四部分构成:
- 测试环境报告
- 延迟百分比分布
这是按照百分比进行的统计报告:每完成一次剩余测试量的 50%就给出一个统计数据。
- 延迟的累积分布
这是按照时间间隔统计的报告:基本是每 0.1 毫秒统计一次。
- 总述报告
并且会在当前目录下生成 dump.rdb
持久化文件。
测试 2
1 | redis-benchmark -t set,lpush,sadd -c 100 -n 100000 -q |
以上命令中选项的意义:
- -t:指定要测试的命令,多个命令使用逗号分隔,不能有空格
- -q:指定仅给出总述性报告
测试结果:
简单动态字符串 SDS
SDS 简介
无论是 Redis 的 Key 还是 Value,其基础数据类型都是字符串。例如,Hash 型 Value 的field 与 value 的类型、List 型、Set 型、ZSet 型 Value 的元素的类型等都是字符串。虽然 Redis是使用标准 C 语言开发的,但并没有直接使用 C 语言中传统的字符串表示,而是自定义了一种字符串。这种字符串本身的结构比较简单,但功能却非常强大,称为简单动态字符串,Simple Dynamic String,简称 SDS。
Redis 中的所有字符串并不都是 SDS,也会出现 C 字符串。C 字符串只会出现在字符串"字面常量"中,并且该字符串不可能发生变更。
redisLog(REDIS_WARNNING, "sdfsfsafsafds");
SDS 结构
SDS 不同于 C 字符串。C 字符串本身是一个以双引号括起来,以空字符’\0’结尾的字符序列。但 SDS 是一个结构体,定义在 Redis 安装目录下的 src/sds.h
中:
1 | struct sdshdr { |
例如执行 SET country "China"命令时,键 country 与值"China"都是 SDS 类型的,只不过一个是 SDS 的变量,一个是 SDS 的字面常量。"China"在内存中的结构如下:
通过以上结构可以看出,SDS 的 buf 值实际是一个 C 字符串,包含空字符’\0’共占 6 个字节。但 SDS 的 len是不包含空字符’\0’的。
该结构与前面不同的是,这里有 3 字节未使用空间。
SDS 的优势
C 字符串使用 Len+1 长度的字符数组来表示实际长度为 Len 的字符串,字符数组最后以空字符’\0’结尾,表示字符串结束。这种结构简单,但不能满足 Redis 对字符串功能性、安全性及高效性等的要求。
- 防止"字符串长度获取"性能瓶颈
对于 C 字符串,若要获取其长度,则必须要通过遍历整个字符串才可获取到的。对于超长字符串的遍历,会成为系统的性能瓶颈。
但,由于 SDS 结构体中直接就存放着字符串的长度数据,所以对于获取字符串长度需要消耗的系统性能,与字符串本身长度是无关的,不会成为 Redis 的性能瓶颈。
- 保障二进制安全
C 字符串中只能包含符合某种编码格式的字符,例如 ASCII、UTF-8 等,并且除了字符串末尾外,其它位置是不能包含空字符’\0’的,否则该字符串就会被程序误解为提前结束。而在图片、音频、视频、压缩文件、office 文件等二进制数据中以空字符’\0’作为分隔符的情况是很常见的。故而在 C 字符串中是不能保存像图片、音频、视频、压缩文件、office 文件等二进制数据的。
但 SDS 不是以空字符’\0’作为字符串结束标志的,其是通过 len 属性来判断字符串是否结束的。所以,对于程序处理 SDS 中的字符串数据,无需对数据做任何限制、过滤、假设,只需读取即可。数据写入的是什么,读到的就是什么。
- 减少内存再分配次数
SDS 采用了空间预分配策略与惰性空间释放策略来避免内存再分配问题。
空间预分配策略是指,每次 SDS 进行空间扩展时,程序不但为其分配所需的空间,还会为其分配额外的未使用空间,以减少内存再分配次数。而额外分配的未使用空间大小取决于空间扩展后 SDS 的 len 属性值。
- 如果 len 属性值小于 1M,那么分配的未使用空间 free 的大小与 len 属性值相同。
- 如果 len 属性值大于等于 1M ,那么分配的未使用空间 free 的大小固定是 1M。
SDS 对于空间释放采用的是惰性空间释放策略。该策略是指,SDS 字符串长度如果缩短,那么多出的未使用空间将暂时不释放,而是增加到 free 中。以使后期扩展 SDS 时减少内存再分配次数。
如果要释放 SDS 的未使用空间,则可通过 sdsRemoveFreeSpace()函数来释放。
- 兼容 C 函数
Redis 中提供了很多的 SDS 的 API,以方便用户对 Redis 进行二次开发。为了能够兼容 C函数,SDS 的底层数组 buf[]中的字符串仍以空字符’\0’结尾。
现在要比较的双方,一个是 SDS,一个是 C 字符串,此时可以通过 C 语言函数
strcmp(sds_str->buf,c_str)
常用的 SDS 操作函数
下表列出了一些常用的 SDS 操作函数及其功能描述。
函数 | 功能描述 |
---|---|
sdsnew() | 使用指定的 C 字符串创建一个 SDS |
sdsempty() | 创建一个不包含任何字符串数据的 SDS |
sdsdup() | 创建一个指定 SDS 的副本 |
sdsfree() | 释放指定的 SDS |
sdsclear() | 清空指定 SDS 的字符串内容 |
sdslen() | 获取指定 SDS 的已使用空间 len 值 |
sdsavail() | 获取指定 SDS 的未使用空间 free 值 |
sdsMakeRoomFor() | 使指定的 SDS 的 free 空间增加指定的大小 |
sdsRemoveFreeSpace() | 释放指定 SDS 的 free 空间 |
sdscat() | 将指定的 C 字符串拼接到指定 SDS 字符串末尾 |
sdscatsds() | 将指定的 SDS 的字符串拼接到另一个指定 SDS 字符串末尾 |
sdscpy() | 将指定的 C 字符串复制到指定的 SDS 中,覆盖原 SDS 字符串内容 |
sdsgrouzero() | 扩展 SDS 字符串到指定长度。这个扩展是使用空字符’\0’填充 |
sdsrange() | 截取指定 SDS 中指定范围内的字符串 |
sdstrim() | 在指定 SDS 中删除所有指定 C 字符串中出现的所有字符 |
sdsemp() | 对比两个给定的 SDS 字符串是否相同 |
sdstolow() | 将指定 SDS 字符串中的所有字母变为小写 |
sdstoupper() | 将指定 SDS 字符串中的所有字母变为大写 |
集合的底层实现原理
Redis 中对于 Set 类型的底层实现,直接采用了 hashTable。但对于 Hash、ZSet、List 集合的底层实现进行了特殊的设计,使其保证了 Redis 的高性能。
两种实现的选择
对于 Hash 与 ZSet 集合,其底层的实现实际有两种:压缩列表 zipList,与跳跃列表 skipList。这两种实现对于用户来说是透明的,但用户写入不同的数据,系统会自动使用不同的实现。只有同时满足以配置文件 redis.conf 中相关集合元素数量阈值与元素大小阈值两个条件,使用的就是压缩列表 zipList,只要有一个条件不满足使用的就是跳跃列表 skipList。例如,对于ZSet 集合中这两个条件如下:
- 集合元素个数小于 redis.conf 中 zset-max-ziplist-entries 属性的值,其默认值为 128
- 每个集合元素大小都小于 redis.conf 中 zset-max-ziplist-value 属性的值,其默认值为 64字节
- 什么是 zipList
zipList,通常称为压缩列表,是一个经过特殊编码的用于存储字符串或整数的双向链表。其底层数据结构由三部分构成:head、entries 与 end。这三部分在内存上是连续存放的。
- head
head 又由三部分构成:
- zlbytes:占 4 个字节,用于存放 zipList 列表整体数据结构所占的字节数,包括 zlbytes本身的长度。
- zltail:占 4 个字节,用于存放 zipList 中最后一个 entry 在整个数据结构中的偏移量(字节)。该数据的存在可以快速定位列表的尾 entry 位置,以方便操作。
- zllen:占 2 字节,用于存放列表包含的 entry 个数。由于其只有 16 位,所以 zipList 最多可以含有的 entry 个数为 216-1 = 65535 个。
- entries
entries 是真正的列表,由很多的列表元素 entry 构成。由于不同的元素类型、数值的不同,从而导致每个 entry 的长度不同。
每个 entry 由三部分构成:
- prevlength:该部分用于记录上一个 entry 的长度,以实现逆序遍历。默认长度为 1 字节,只要上一个 entry 的长度<254 字节,prevlength 就占 1 字节,否则其会自动扩展为 3 字节长度。
- encoding:该部分用于标志后面的 data 的具体类型。如果 data 为整数类型,encoding固定长度为 1 字节。如果 data 为字符串类型,则 encoding 长度可能会是 1 字节、2 字节或 5 字节。data 字符串不同的长度,对应着不同的 encoding 长度。
- data:真正存储的数据。数据类型只能是整数类型或字符串类型。不同的数据占用的字节长度不同。
- end
end 只包含一部分,称为 zlend。占 1 个字节,值固定为 255,即二进制位为全 1,表示一个 zipList 列表的结束。
listPack
对于 ziplist,实现复杂,为了逆序遍历,每个 entry 中包含前一个 entry 的长度,这样会导致在 ziplist 中间修改或者插入 entry 时需要进行级联更新。在高并发的写操作场景下会极度降低 Redis 的性能。为了实现更紧凑、更快的解析,更简单的实现,重写实现了 ziplist,并命名为 listPack。
在 Redis 7.0 中,已经将 zipList 全部替换为了 listPack,但为了兼容性,在配置中也保留了 zipList 的相关属性。
- 什么是 listPack
listPack 也是一个经过特殊编码的用于存储字符串或整数的双向链表。其底层数据结构也由三部分构成:head、entries 与 end,且这三部分在内存上也是连续存放的。
listPack 与 zipList 的重大区别在 head 与每个 entry 的结构上,表示列表结束的 end 与 zipList的 zlend 是相同的,占一个字节,且 8 位全为 1。
- head
head 由两部分构成:
- totalBytes:占 4 个字节,用于存放 listPack 列表整体数据结构所占的字节数,包括totalBytes 本身的长度。
- elemNum:占 2 字节,用于存放列表包含的 entry 个数。其意义与 zipList 中 zllen 的相同。
与 zipList 的 head 相比,没有了记录最后一个 entry 偏移量的 zltail。
- entries
entries 也是 listPack 中真正的列表,由很多的列表元素 entry 构成。由于不同的元素类型、数值的不同,从而导致每个 entry 的长度不同。但与 zipList 的 entry 结构相比,listPack的 entry 结构发生了较大变化。
其中最大的变化就是没有了记录前一个 entry 长度的 prevlength,而增加了记录当前entry 长度的 element-total-len。而这个改变仍然可以实现逆序遍历,但却避免了由于在列表中间修改或插入 entry 时引发的级联更新。
每个 entry 仍由三部分构成:
- encoding:该部分用于标志后面的 data 的具体类型。如果 data 为整数类型,encoding长度可能会是 1、2、3、4、5 或 9 字节。不同的字节长度,其标识位不同。如果 data为字符串类型,则 encoding 长度可能会是 1、2 或 5 字节。data 字符串不同的长度,对应着不同的 encoding 长度。
- data:真正存储的数据。数据类型只能是整数类型或字符串类型。不同的数据占用的字节长度不同。
- element-total-len:该部分用于记录当前 entry 的长度,用于实现逆序遍历。由于其特殊
的记录方式,使其本身占有的字节数据可能会是 1、2、3、4 或 5 字节。
skipList
- 什么是 skipList
skipList,跳跃列表,简称跳表,是一种随机化的数据结构,基于并联的链表,实现简单,查找效率较高。简单来说跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能。也正是这个跳跃功能,使得在查找元素时,能够提供较高的效率。
- skipList 原理
假设有一个带头尾结点的有序链表。
在该链表中,如果要查找某个数据,需要从头开始逐个进行比较,直到找到包含数据的那个节点,或者找到第一个比给定数据大的节点,或者找到最后尾结点,后两种都属于没有找到的情况。同样,当我们要插入新数据的时候,也要经历同样的查找过程,从而确定插入位置。
为了提升查找效率,在偶数结点上增加一个指针,让其指向下一个偶数结点。
这样所有偶数结点就连成了一个新的链表(简称高层链表),当然,高层链表包含的节点个数只是原来链表的一半。此时再想查找某个数据时,先沿着高层链表进行查找。当遇到第一个比待查数据大的节点时,立即从该大节点的前一个节点回到原链表中进行查找。例如,若想插入一个数据 20,则先在(8,19,31,42)的链表中查找,找到第一个比 20 大的节点 31,然后再在高层链表中找到 31 节点的前一个节点 19,然后再在原链表中获取到其下一个节点值为 23。比 20 大,则将 20 插入到 19 节点与 23 节点之间。若插入的是 25,比节点23 大,则插入到 23 节点与 31 节点之间。
该方式明显可以减少比较次数,提高查找效率。如果链表元素较多,为了进一步提升查找效率,可以将原链表构建为三层链表,或再高层级链表。
层级越高,查找效率就会越高。
- 存在的问题
这种对链表分层级的方式从原理上看确实提升了查找效率,但在实际操作时就出现了问题:由于固定序号的元素拥有固定层级,所以列表元素出现增加或删除的情况下,会导致列表整体元素层级大调整,但这样势必会大大降低系统性能。
例如,对于划分两级的链表,可以规定奇数结点为高层级链表,偶数结点为低层级链表。对于划分三级的链表,可以按照节点序号与 3 取模结果进行划分。但如果插入了新的节点,或删除的原来的某些节点,那么定会按照原来的层级划分规则进行重新层级划分,那么势必会大大降低系统性能。
- 算法优化
为了避免前面的问题,skipList 采用了随机分配层级方式。即在确定了总层级后,每添加一个新的元素时会自动为其随机分配一个层级。这种随机性就解决了节点序号与层级间的固定关系问题。
上图演示了列表在生成过程中为每个元素随机分配层级的过程。从这个 skiplist 的创建和插入过程可以看出,每一个节点的层级数都是随机分配的,而且新插入一个节点不会影响到其它节点的层级数。只需要修改插入节点前后的指针,而不需对很多节点都进行调整。这就降低了插入操作的复杂度。
skipList 指的就是除了最下面第 1 层链表之外,它会产生若干层稀疏的链表,这些链表里面的指针跳过了一些节点,并且越高层级的链表跳过的节点越多。在查找数据的时先在高层级链表中进行查找,然后逐层降低,最终可能会降到第 1 层链表来精确地确定数据位置。在这个过程中由于跳过了一些节点,从而加快了查找速度。
quickList
- 什么是 quickList
quickList,快速列表,quickList 本身是一个双向无循环链表,它的每一个节点都是一个zipList。从 Redis3.2 版本开始,对于 List 的底层实现,使用 quickList 替代了 zipList 和 linkedList。
zipList 与 linkedList 都存在有明显不足,而 quickList 则对它们进行了改进:吸取了 zipList和 linkedList 的优点,避开了它们的不足。
quickList 本质上是 zipList 和 linkedList 的混合体。其将 linkedList 按段切分,每一段使用 zipList 来紧凑存储若干真正的数据元素,多个 zipList 之间使用双向指针串接起来。当然,对于每个 zipList 中最多可存放多大容量的数据元素,在配置文件中通过 list-max-ziplist-size属性可以指定。
- 检索操作
为了更深入的理解 quickList 的工作原理,通过对检索、插入、删除等操作的实现分析来加深理解。
对于 List 元素的检索,都是以其索引 index 为依据的。quickList 由一个个的 zipList 构成,每个 zipList 的 zllen 中记录的就是当前 zipList 中包含的 entry 的个数,即包含的真正数据元素的个数。根据要检索元素的 index,从 quickList 的头节点开始,逐个对 zipList 的 zllen 做 sum求和,直到找到第一个求和后 sum 大于 index 的 zipList,那么要检索的这个元素就在这个zipList 中。
- 插入操作
由于 zipList 是有大小限制的,所以在 quickList 中插入一个元素在逻辑上相对就比较复杂一些。假设要插入的元素的大小为 insertBytes,而查找到的插入位置所在的 zipList 当前的大小为 zlBytes,那么具体可分为下面几种情况:
- 情况一:当 insertBytes + zlBytes <= list-max-ziplist-size 时,直接插入到 zipList 中相应位置即可
- 情况二:当 insertBytes + zlBytes > list-max-ziplist-size,且插入的位置位于该 zipList 的首部位置,此时需要查看该 zipList 的前一个 zipList 的大小 prev_zlBytes。
- 若 insertBytes + prev_zlBytes<= list-max-ziplist-size 时,直接将元素插入到前一个zipList 的尾部位置即可
- 若 insertBytes + prev_zlBytes> list-max-ziplist-size 时,直接将元素自己构建为一个新的 zipList,并连入 quickList 中
- 情况三:当 insertBytes + zlBytes > list-max-ziplist-size,且插入的位置位于该 zipList 的尾部位置,此时需要查看该 zipList 的后一个 zipList 的大小 next_zlBytes。
- 若 insertBytes + next_zlBytes<= list-max-ziplist-size 时,直接将元素插入到后一个zipList 的头部位置即可
- 若 insertBytes + next_zlBytes> list-max-ziplist-size 时,直接将元素自己构建为一个新的 zipList,并连入 quickList 中
- 情况四:当 insertBytes + zlBytes > list-max-ziplist-size,且插入的位置位于该 zipList 的中间位置,则将当前 zipList 分割为两个 zipList 连接入 quickList 中,然后将元素插入到分割后的前面 zipList 的尾部位置
- 删除操作
对于删除操作,只需要注意一点,在相应的 zipList 中删除元素后,该 zipList 中是否还有元素。如果没有其它元素了,则将该 zipList 删除,将其前后两个 zipList 相连接。
key 与 value 中元素的数量
前面讲述的 Redis 的各种特殊数据结构的设计,不仅极大提升了 Redis 的性能,并且还使得 Redis 可以支持的 key 的数量、集合 value 中可以支持的元素数量可以非常庞大。
- Redis 最多可以处理 232 个 key(约 42 亿),并且在实践中经过测试,每个 Redis 实例至少可以处理 2.5 亿个 key。
- 每个 Hash、List、Set、ZSet 集合都可以包含 232 个元素。