数据结构及命令
# 概览
# Redis常用命令
# 1. 常用命令
DEL key:该命令用于在 key 存在时删除 key
DUMP key:序列化给定key,返回被序列化的值
EXISTS key:检查key是否存在
# 这个过期时间的命令常用
EXPIRE key second:为key设定过期时间(秒级别)
PEXPIRE key millsecond:设置毫秒级别的过期时间
TTL key:返回key剩余时间
PERSIST key:移除key的过期时间,key将持久保存
KEY pattern:查询所有符号给定模式的key
RANDOM key:随机返回一个key
RANAME key newkey:修改key的名称
MOVE key db:移动key至指定数据库中
TYPE key:返回key所储存的值的类型
# 2. Redis多数据库
Redis下,数据库是由一个整数索引标识,而不是由一个数据库名称。默认情况下,一个客户端连接到数据库0。
- redis配置文件中下面的参数来控制数据库总数:**database 16 //(从0开始 1 2 3 …15)
- 数据库的切换:
select 数据库
- 移动数据(将当前key移动另个库):move key名称 数据库
- 数据库清空:
flushdb //清除当前数据库的所有key flushall //清除整个Redis的数据库所有key
# 3. Expire使用场景:
EXPIR key seconds
1. 限时优惠活动
2. 网站数据缓存(对于一些需要定时更新的数据,例如:积分排榜榜)
3. 手机验证码
4. 限制网站访客频率(例如:1分钟最多访问 10次)
# Redis底层数据结构
# 字符串
Redis 没有直接使用 C 语言传统的字符串表示(以空字符结尾的字符数组,以下简称 C 字符串), 而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型, 并将 SDS 用作 Redis 的默认字符串表示。
当 Redis 需要的不仅仅是一个字符串字面量, 而是一个可以被修改的字符串值时, Redis 就会使用 SDS 来表示字符串值: 比如在 Redis 的数据库里面, 包含字符串值的键值对在底层都是由 SDS 实现的。
# 为什么使用SDS?
常数复杂度获取字符串长度
由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。而对于 C 语言,获取字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。通过 strlen key 命令可以获取 key 的字符串长度。
杜绝缓冲区溢出
我们知道在 C 语言中使用 strcat 函数来进行两个字符串的拼接,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。而对于 SDS 数据类型,在进行字符修改的时候,会首先根据记录的 len 属性检查内存空间是否满足需求,如果不满足,会进行相应的空间扩展,然后在进行修改操作,所以不会出现缓冲区溢出。
减少修改字符串的内存重新分配次数
C语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。
而对于SDS,由于len属性和alloc属性的存在,对于修改字符串SDS实现了空间预分配和惰性空间释放两种策略:
空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。
惰性空间释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 alloc 属性将这些字节的数量记录下来,等待后续使用。(当然SDS也提供了相应的API,当我们有需要时,也可以手动释放这些未使用的空间。)
二进制安全
因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而所有 SDS 的API 都是以处理二进制的方式来处理 buf 里面的元素,并且 SDS 不是以空字符串来判断是否结束,而是以 len 属性表示的长度来判断字符串是否结束。
- 兼容部分 C 字符串函数
虽然 SDS 是二进制安全的,但是一样遵从每个字符串都是以空字符串结尾的惯例,这样可以重用 C 语言库<string.h> 中的一部分函数。
# 链表
链表提供了高效的节点重排能力, 以及顺序性的节点访问方式, 并且可以通过增删节点来灵活地调整链表的长度。
链表在 Redis 中的应用非常广泛, 比如列表键的底层实现之一就是链表: 当一个列表键包含了数量比较多的元素, 又或者列表中包含的元素都是比较长的字符串时, Redis 就会使用链表作为列表键的底层实现。
Redis 的链表实现的特性可以总结如下:
- 双端: 链表节点带有 prev 和 next 指针, 获取某个节点的前置节点和后置节点的复杂度都是 O(1) 。
- 无环: 表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL , 对链表的访问以 NULL 为终点。
- 带表头指针和表尾指针: 通过 list 结构的 head 指针和 tail 指针, 程序获取链表的表头节点和表尾节点的复杂度为 O(1) 。
- 带链表长度计数器: 程序使用 list 结构的 len 属性来对 list 持有的链表节点进行计数, 程序获取链表中节点数量的复杂度为 O(1) 。
- 多态: 链表节点使用 void* 指针来保存节点值, 并且可以通过 list 结构的 dup 、 free 、 match 三个属性为节点值设置类型特定函数, 所以链表可以用于保存各种不同类型的值。
# 字典
字典在 Redis 中的应用相当广泛, 比如 Redis 的数据库就是使用字典来作为底层实现的, 对数据库的增、删、查、改操作也是构建在对字典的操作之上的。
举个例子, 当我们执行命令:
redis> SET msg "hello world" OK
在数据库中创建一个键为 "msg" , 值为 "hello world" 的键值对时, 这个键值对就是保存在代表数据库的字典里面的。
除了用来表示数据库之外, 字典还是哈希键的底层实现之一: 当一个哈希键包含的键值对比较多, 又或者键值对中的元素都是比较长的字符串时, Redis 就会使用字典作为哈希键的底层实现。
# 字典数据结构实现
# rehash操作
随着操作的不断执行, 哈希表保存的键值对会逐渐地增多或者减少, 为了让哈希表的负载因子(load factor)维持在一个合理的范围之内, 当哈希表保存的键值对数量太多或者太少时, 程序需要对哈希表的大小进行相应的扩展或者收缩。
扩展和收缩哈希表的工作可以通过执行 rehash (重新散列)操作来完成, Redis 对字典的哈希表执行 rehash 的步骤如下:
为字典的 ht[1] 哈希表分配空间, 这个哈希表的空间大小取决于要执行的操作, 以及 ht[0] 当前包含的键值对数量 (也即是 ht[0].used 属性的值):
- 如果执行的是扩展操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used * 2 的 2^n (2 的 n 次方幂);
- 如果执行的是收缩操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n 。
将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面: rehash 指的是重新计算键的哈希值和索引值, 然后将键值对放置到 ht[1] 哈希表的指定位置上。
当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后 (ht[0] 变为空表), 释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备。
# 哈希表的扩展与收缩
当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩展操作:
服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1 ;
服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5 ;
其中哈希表的负载因子可以通过公式计算得出。:
# 负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size
根据 BGSAVE 命令或 BGREWRITEAOF 命令是否正在执行, 服务器执行扩展操作所需的负载因子并不相同, 这是因为在执行 BGSAVE 命令或 BGREWRITEAOF 命令的过程中, Redis 需要创建当前服务器进程的子进程, 而大多数操作系统都采用写时复制(copy-on-write (opens new window))技术来优化子进程的使用效率, 所以在子进程存在期间, 服务器会提高执行扩展操作所需的负载因子, 从而尽可能地避免在子进程存在期间进行哈希表扩展操作, 这可以避免不必要的内存写入操作, 最大限度地节约内存。
另一方面, 当哈希表的负载因子小于 0.1 时, 程序自动开始对哈希表执行收缩操作。
# 跳跃表
核心:类似于二分,通过层数来减少遍历的次数,每层需要遍历的节点都更少,一般是下一层的一半。
跳跃表(skiplist)是一种有序数据结构, 它通过在每个节点中维持多个指向其他节点的指针, 从而达到快速访问节点的目的。跳跃表支持平均 O(log N) 最坏 O(N) 复杂度的节点查找, 还可以通过顺序性操作来批量处理节点。
在大部分情况下, 跳跃表的效率可以和平衡树相媲美, 并且因为跳跃表的实现比平衡树要来得更为简单, 所以有不少程序都使用跳跃表来代替平衡树。
Redis 使用跳跃表作为有序集合键的底层实现之一: 如果一个有序集合包含的元素数量比较多, 又或者有序集合中元素的成员(member)是比较长的字符串时, Redis 就会使用跳跃表来作为有序集合键的底层实现。
和链表、字典等数据结构被广泛地应用在 Redis 内部不同, Redis 只在两个地方用到了跳跃表, 一个是实现有序集合键, 另一个是在集群节点中用作内部数据结构, 除此之外, 跳跃表在 Redis 里面没有其他用途。
位于 zskiplist 结构右方的是四个 zskiplistNode 结构, 该结构包含以下属性:
- 层(level):节点中用 L1 、 L2 、 L3 等字样标记节点的各个层, L1 代表第一层, L2 代表第二层,以此类推。每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离。在上面的图片中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。
- 后退(backward)指针:节点中用 BW 字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。
- 分值(score):各个节点中的 1.0 、 2.0 和 3.0 是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。
- 成员对象(obj):各个节点中的 o1 、 o2 和 o3 是节点所保存的成员对象。
# 跳跃表节点
typedef struct zskiplistNode {
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
# 层
跳跃表节点的 level 数组可以包含多个元素, 每个元素都包含一个指向其他节点的指针, 程序可以通过这些层来加快访问其他节点的速度, 一般来说, 层的数量越多, 访问其他节点的速度就越快。
每次创建一个新跳跃表节点的时候, 程序都根据幂次定律 (power law (opens new window),越大的数出现的概率越小) 随机生成一个介于 1 和 32 之间的值作为 level 数组的大小, 这个大小就是层的“高度”。
# 前进指针
每个层都有一个指向表尾方向的前进指针(level[i].forward 属性), 用于从表头向表尾方向访问节点。
从表头向表尾方向, 遍历跳跃表中所有节点的路径:
- 迭代程序首先访问跳跃表的第一个节点(表头), 然后从第四层的前进指针移动到表中的第二个节点。
- 在第二个节点时, 程序沿着第二层的前进指针移动到表中的第三个节点。
- 在第三个节点时, 程序同样沿着第二层的前进指针移动到表中的第四个节点。
- 当程序再次沿着第四个节点的前进指针移动时, 它碰到一个 NULL , 程序知道这时已经到达了跳跃表的表尾, 于是结束这次遍历。
# 跨度
层的跨度(level[i].span 属性)用于记录两个节点之间的距离:
- 两个节点之间的跨度越大, 它们相距得就越远。
- 指向 NULL 的所有前进指针的跨度都为 0 , 因为它们没有连向任何节点。
初看上去, 很容易以为跨度和遍历操作有关, 但实际上并不是这样 —— 遍历操作只使用前进指针就可以完成了, 跨度实际上是用来计算排位(rank)的: 在查找某个节点的过程中, 将沿途访问过的所有层的跨度累计起来, 得到的结果就是目标节点在跳跃表中的排位。
# 后退指针
节点的后退指针(backward 属性)用于从表尾向表头方向访问节点: 跟可以一次跳过多个节点的前进指针不同, 因为每个节点只有一个后退指针, 所以每次只能后退至前一个节点。
图 5-6 用虚线展示了如果从表尾向表头遍历跳跃表中的所有节点: 程序首先通过跳跃表的 tail 指针访问表尾节点, 然后通过后退指针访问倒数第二个节点, 之后再沿着后退指针访问倒数第三个节点, 再之后遇到指向 NULL 的后退指针, 于是访问结束。
# 分值和成员
节点的分值(score 属性)是一个 double 类型的浮点数, 跳跃表中的所有节点都按分值从小到大来排序。
节点的成员对象(obj 属性)是一个指针, 它指向一个字符串对象, 而字符串对象则保存着一个 SDS 值。
在同一个跳跃表中, 各个节点保存的成员对象必须是唯一的, 但是多个节点保存的分值却可以是相同的: 分值相同的节点将按照成员对象在字典序中的大小来进行排序, 成员对象较小的节点会排在前面(靠近表头的方向), 而成员对象较大的节点则会排在后面(靠近表尾的方向)。
# 整数集合
整数集合(intset)是集合键的底层实现之一: 当一个集合只包含整数值元素, 并且这个集合的元素数量不多时, Redis 就会使用整数集合作为集合键的底层实现。
整数集合(intset)是 Redis 用于保存整数值的集合抽象数据结构, 它可以保存类型为 int16_t 、 int32_t 或者 int64_t 的整数值, 并且保证集合中不会出现重复元素。
每个 intset.h/intset 结构表示一个整数集合:
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
contents 数组是整数集合的底层实现: 整数集合的每个元素都是 contents 数组的一个数组项(item), 各个项在数组中按值的大小从小到大有序地排列, 并且数组中不包含任何重复项。length 属性记录了整数集合包含的元素数量, 也即是 contents 数组的长度。
# 压缩列表
压缩列表(ziplist)是列表键和哈希键的底层实现之一。
当一个列表键只包含少量列表项, 并且每个列表项要么就是小整数值, 要么就是长度比较短的字符串, 那么 Redis 就会使用压缩列表来做列表键的底层实现。
另外, 当一个哈希键只包含少量键值对, 并且每个键值对的键和值要么就是小整数值, 要么就是长度比较短的字符串, 那么 Redis 就会使用压缩列表来做哈希键的底层实现。
所以,redis的底层对象并不是只有唯一一种实现方式,从list和hash就能看出来。
# Redis 对象
在前面的数个章节里, 我们陆续介绍了 Redis 用到的所有主要数据结构, 比如简单动态字符串(SDS)、双端链表、字典、压缩列表、整数集合, 等等。
Redis 并没有直接使用这些数据结构来实现键值对数据库, 而是基于这些数据结构创建了一个对象系统, 这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象, 每种对象都用到了至少一种我们前面所介绍的数据结构。
通过这五种不同类型的对象, Redis 可以在执行命令之前, 根据对象的类型来判断一个对象是否可以执行给定的命令。 使用对象的另一个好处是, 我们可以针对不同的使用场景, 为对象设置多种不同的数据结构实现, 从而优化对象在不同场景下的使用效率。
除此之外, Redis 的对象系统还实现了基于引用计数技术的内存回收机制: 当程序不再使用某个对象的时候, 这个对象所占用的内存就会被自动释放; 另外, Redis 还通过引用计数技术实现了对象共享机制, 这一机制可以在适当的条件下, 通过让多个数据库键共享同一个对象来节约内存。
# 对象的类型与编码
Redis 使用对象来表示数据库中的键和值, 每次当我们在 Redis 的数据库中新创建一个键值对时, 我们至少会创建两个对象, 一个对象用作键值对的键(键对象), 另一个对象用作键值对的值(值对象)。
举个例子, 以下 SET 命令在数据库中创建了一个新的键值对, 其中键值对的键是一个包含了字符串值 "msg" 的对象, 而键值对的值则是一个包含了字符串值 "hello world" 的对象:
redis> SET msg "hello world" OK
Redis 中的每个对象都由一个 redisObject 结构表示, 该结构中和保存数据有关的三个属性分别是 type 属性、 encoding 属性和 ptr 属性:
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 指向底层实现数据结构的指针
void *ptr;
// ...
} robj;
# 类型
对象的 type 属性记录了对象的类型, 这个属性的值可以是表 8-1 列出的常量的其中一个。
表 8-1 对象的类型
类型常量 | 对象的名称 |
---|---|
REDIS_STRING | 字符串对象 |
REDIS_LIST | 列表对象 |
REDIS_HASH | 哈希对象 |
REDIS_SET | 集合对象 |
REDIS_ZSET | 有序集合对象 |
对于 Redis 数据库保存的键值对来说, 键总是一个字符串对象, 而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种, 因此:
- 当我们称呼一个数据库键为“字符串键”时, 我们指的是“这个数据库键所对应的值为字符串对象”;
- 当我们称呼一个键为“列表键”时, 我们指的是“这个数据库键所对应的值为列表对象”,
诸如此类。
# 编码和底层实现
对象的 ptr 指针指向对象的底层实现数据结构, 而这些数据结构由对象的 encoding 属性决定。
encoding 属性记录了对象所使用的编码, 也即是说这个对象使用了什么数据结构作为对象的底层实现, 这个属性的值可以是表 8-3 列出的常量的其中一个。
表 8-3 对象的编码
编码常量 | 编码所对应的底层数据结构 |
---|---|
REDIS_ENCODING_INT | long 类型的整数 |
REDIS_ENCODING_EMBSTR | embstr 编码的简单动态字符串 |
REDIS_ENCODING_RAW | 简单动态字符串 |
REDIS_ENCODING_HT | 字典 |
REDIS_ENCODING_LINKEDLIST | 双端链表 |
REDIS_ENCODING_ZIPLIST | 压缩列表 |
REDIS_ENCODING_INTSET | 整数集合 |
REDIS_ENCODING_SKIPLIST | 跳跃表和字典 |
每种类型的对象都至少使用了两种不同的编码, 表 8-4 列出了每种类型的对象可以使用的编码。
表 8-4 不同类型和编码的对象
类型 | 编码 | 对象 |
---|---|---|
REDIS_STRING | REDIS_ENCODING_INT | 使用整数值实现的字符串对象。 |
REDIS_STRING | REDIS_ENCODING_EMBSTR | 使用 embstr 编码的简单动态字符串实现的字符串对象。 |
REDIS_STRING | REDIS_ENCODING_RAW | 使用简单动态字符串实现的字符串对象。 |
REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的列表对象。 |
REDIS_LIST | REDIS_ENCODING_LINKEDLIST | 使用双端链表实现的列表对象。 |
REDIS_HASH | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的哈希对象。 |
REDIS_HASH | REDIS_ENCODING_HT | 使用字典实现的哈希对象。 |
REDIS_SET | REDIS_ENCODING_INTSET | 使用整数集合实现的集合对象。 |
REDIS_SET | REDIS_ENCODING_HT | 使用字典实现的集合对象。 |
REDIS_ZSET | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的有序集合对象。 |
REDIS_ZSET | REDIS_ENCODING_SKIPLIST | 使用跳跃表和字典实现的有序集合对象。 |
通过 encoding 属性来设定对象所使用的编码, 而不是为特定类型的对象关联一种固定的编码, 极大地提升了 Redis 的灵活性和效率, 因为 Redis 可以根据不同的使用场景来为一个对象设置不同的编码, 从而优化对象在某一场景下的效率。
举个例子, 在列表对象包含的元素比较少时, Redis 使用压缩列表作为列表对象的底层实现:
- 因为压缩列表比双端链表更节约内存, 并且在元素数量较少时, 在内存中以连续块方式保存的压缩列表比起双端链表可以更快被载入到缓存中;
- 随着列表对象包含的元素越来越多, 使用压缩列表来保存元素的优势逐渐消失时, 对象就会将底层实现从压缩列表转向功能更强、也更适合保存大量元素的双端链表上面;
其他类型的对象也会通过使用多种不同的编码来进行类似的优化。
# 对象与命令的处理
当执行一个处理数据类型命令的时候,redis执行以下步骤
- 根据给定的key,在数据库字典中查找和他相对应的redisObject,如果没找到,就返回NULL;
- 检查redisObject的type属性和执行命令所需的类型是否相符,如果不相符,返回类型错误;
- 根据redisObject的encoding属性所指定的编码,选择合适的操作函数来处理底层的数据结构;
- 返回数据结构的操作结果作为命令的返回值。
# 4.1 String 类型(虽然Redis不区分大小写,但是key区分大小写)
# 是什么?(简介)
- string是 redis的最基本的类型key对应一个vaue
- string类型是二进制安全的,意思是 redis的 string可以包含任何数据,比如jg图拼啊或者序列化的对象
- string类型是 redis最基本的数据类型,一个键最大能存储512MB
- 二进制安全是指,在传输数据时,在传输二进制的信息安全,也就是不被篡改、破译等,如果被攻击,能够即使检测出来。
- 特点:1. 编码、解码发生在客户端完成,执行效率高。 2.不需要平凡的编解码,不会出现乱码。
# 使用方法
赋值语句:
set key_name value:命令不区分大小写,但是key_name区分大小写
SETNX key value:当key不存在时设置key的值。(SET if Not eXists)
取值语句:
get key_name
GETRANGE key start end:获取key中字符串的子字符串,从start开始,end结束
MGET key1 [key2 …]:获取多个key
GETSET KEY_NAME VALUE:设定key的值,并返回key的旧值。当key不存在,返回nil
STRLEN key:返回key所存储的字符串的长度
删除语句:
DEL KEY_NAME:删除指定的 key , 如果存在,返回数值类型。
自增自减:
INCR KEY_NAME :INCR命令key中存储的值+1,如果不存在key,则key中的值话先被初始化为0再加1
INCRBY KEY_NAME 增量:可以指定一个增量值
DECR KEY_NAME:key中的值自减一
DECRBY KEY_NAME 减量:指定一个减量
append key_name value:字符串拼接,追加至末尾,如果不存在,为其赋值
# 应用场景
1、String通常用于保存单个字符串或JSON字符串数据
2、因为String是二进制安全的,所以可以把保密要求高的图片文件内容作为字符串来存储
3、计数器:常规Key-Value缓存应用,如微博数、粉丝数。INCR本身就具有原子性特性,所以不会有线程安全问题
# 4.2 Hash 类型
# 是什么?
- Redis hash是一个string类型的field和value的映射表,hash特别适用于存储对象。
- 每个hash可以存储2^32-1键值对。可以看成KEY和VALUE的MAP容器。
- 相比于JSON,hash占用很少的内存空间。
# 使用方法:
赋值语句:
HSET key_name field value:为指定的key设定field和value
hmset key field value [field1,value1]:举例: hmset users:1 id 1 uname zhangsan age 22
取值语句:
hget key field
hmget key field[field1]:返回hash表中field的值
hgetall key:返回hash表中所有字段和值
hkeys key:获取hash表所有字段
hlen key:获取hash表中的字段数量
删除语句:
hdel key field [field1]:删除一个或多个hash表的字段
其他语句:
HSETNX key field value :只有在字段 field 不存在时, 设置哈希表的字段值
HINCRBY key field increment:为哈希表 key 中的指定字段的浮点数值加上增量 increment
HINCRBYFLOAT key field increment :为哈希表 key 中国呢的指定字段的浮点数值增加上增量 increment
HEXISTS key field :查看哈希表 key 中,指定字段是否存在
# 应用场景
Hash的应用场景,通常用来存储一个用户信息的对象数据。
1、相比于存储对象的string类型的json串,json串修改单个属性需要将整个值取出来。而hash不需要。
2、相比于多个key-value存储对象,hash节省了很多内存空间
3、如果hash的属性值被删除完,那么hash的key也会被redis删除
1. 适合存一个对象,比如存储用户信息,订单信息等
2. 为什么不用 string 存储一个对象?
* hash 是最捷径关系型数据结构的数据类型,可以将一个库一条记录或存续中一个对象转换为 hashmap 存放在 redis中。
* 用户id为查找的key, 存储的 value 用户对象包含:姓名,年龄,生日等信息,如果用普通的key/value 结构来存储,主要有2种方式。
- 第一种方式将用户ID作为查找 KEY 把其他信息封装成一个对象以序列化的方式存储, 这种方式的缺点是,增加了序列化,反序列化的开销,并且在需要修改其中一项信息的时候,需要把整个对象取出来,并且修改操作需要对并发进行保护,引入CAS等复杂问题。
- 第二种方式将这个用户对象有多少个成员就存成多少个key-value对,用户ID+对应属性的名称作为 唯一标示来取得对应的属性值,虽然省取了序列化开销和并发问题,但是用户ID为重复存储,如果存在大量这样的数据,内存浪费还是非常客观的。
# 小总结
- Redis 提供的 HASH 很好的解决了这个问题, Redis 的 Hash 世纪是内部存储的== Value 为一个 HashMap , 并且提供了直接存取这个Map 的成员接口。
- Redis 不会保留没有 Key 的 Hash。
# 4.3 List 类型
# 是什么:
类似于Java中的LinkedList。
# 使用方法:
- lpush key value1 [value2]
- rpush key value1 [value2]
- lpushx key value:从左侧插入值,如果list不存在,则不操作
- rpushx key value:从右侧插入值,如果list不存在,则不操作
- llen key:获取列表长度
- lindex key index:获取指定索引的元素
- lrange key start stop:获取列表指定范围的元素(包括stop)
- lpop key :从左侧移除第一个元素
- prop key:移除列表最后一个元素
- blpop key [key1] timeout:移除并获取列表第一个元素,如果列表没有元素会阻塞列表到等待超时或发现可弹出元素为止
- brpop key [key1] timeout:移除并获取列表最后一个元素,如果列表没有元素会阻塞列表到等待超时或发现可弹出元素为止
- ltrim key start stop :对列表进行修改,让列表只保留指定区间的元素,不在指定区间的元素就会被删除
- lset key index value :指定索引的值
- linsert key before|after world value:在列表元素前或则后插入元素
# 使用场景:
1、对数据大的集合数据删减
列表显示、关注列表、粉丝列表、留言评价...分页、热点新闻等
2、任务队列
list通常用来实现一个消息队列,而且可以确保先后顺序,不必像MySQL那样通过order by来排序
# 4.4 Set 类型
# 是什么:唯一,无序
# 使用方法:
- sadd key value1[value2]:向集合添加成员,因为是set,所以可以添加多个value值,但是value不可以重复,若果key不存在,则新建一个key,否则如果value与原有的set容器里的重复则不添加,否则添加
- scard key:返回集合成员数
- smembers key:返回集合中所有成员
- sismember key member:判断memeber元素是否是集合key成员的成员
- srandmember key [count]:返回集合中一个或多个随机数
- srem key member1 [member2]:移除集合中一个或多个成员
- spop key:移除并返回集合中的一个随机元素
- smove source destination member:将member元素从source集合移动到destination集合
- sdiff key1 [key2]:返回所有集合的差集
- sdiffstore destination key1[key2]:返回给定所有集合的差集并存储在destination中
# 应用场景:
对两个集合间的数据[计算]进行交集、并集、差集运算
1、以非常方便的实现如共同关注、共同喜好、二度好友等功能。对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存储到一个新的集合中。
2、利用唯一性,可以统计访问网站的所有独立 IP
# 4.5 ZSet (sorted set) 类型
# 是什么:有序且不重复(自增的时候是自增的score)
每个元素都会关联一个double类型的分数,Redis通过分数进行从小到大的排序。分数可以重复。
# 使用方法:
- ZADD key score1 memeber1
- ZCARD key :获取集合中的元素数量
- ZCOUNT key min max 计算在有序集合中指定区间分数的成员数
- ZCOUNT key min max 计算在有序集合中指定区间分数的成员数
- ZRANK key member:返回有序集合指定成员的索引
- ZREVRANGE key start stop :返回有序集中指定区间内的成员,通过索引,分数从高到底
- ZREM key member [member …] 移除有序集合中的一个或多个成员
- ZREMRANGEBYRANK key start stop 移除有序集合中给定的排名区间的所有成员(第一名是0)(低到高排序)
- ZREMRANGEBYSCORE key min max 移除有序集合中给定的分数区间的所有成员
- zincrby key 4 value value的分数值加4,输入分数值为5
# 应用场景:
常用于排行榜:
1. 如推特可以以发表时间作为score来存储
2. 存储成绩
3. 还可以用zset来做带权重的队列,让重要的任务先执行
# 类型检查与命令多态
- Redis 中用于操作键的命令基本上可以分为两种类型。
其中一种命令可以对任何类型的键执行, 比如说 DEL 命令、 EXPIRE 命令、 RENAME 命令、 TYPE 命令、 OBJECT 命令, 等等。
而另一种命令只能对特定类型的键执行, 比如说:
- SET 、 GET 、 APPEND 、 STRLEN 等命令只能对字符串键执行;
- HDEL 、 HSET 、 HGET 、 HLEN 等命令只能对哈希键执行;
- RPUSH 、 LPOP 、 LINSERT 、 LLEN 等命令只能对列表键执行;
- SADD 、 SPOP 、 SINTER 、 SCARD 等命令只能对集合键执行;
- ZADD 、 ZCARD 、 ZRANK 、 ZSCORE 等命令只能对有序集合键执行;
# 类型检查
为了确保只有指定类型的键可以执行某些特定的命令, 在执行一个类型特定的命令之前, Redis 会先检查输入键的类型是否正确, 然后再决定是否执行给定的命令。
类型特定命令所进行的类型检查是通过 redisObject 结构的 type 属性来实现的:
- 在执行一个类型特定命令之前, 服务器会先检查输入数据库键的值对象是否为执行命令所需的类型, 如果是的话, 服务器就对键执行指定的命令;
- 否则, 服务器将拒绝执行命令, 并向客户端返回一个类型错误。
举个例子, 对于 LLEN 命令来说:
- 在执行 LLEN 命令之前, 服务器会先检查输入数据库键的值对象是否为列表类型, 也即是, 检查值对象 redisObject 结构 type 属性的值是否为 REDIS_LIST , 如果是的话, 服务器就对键执行 LLEN 命令;
- 否则的话, 服务器就拒绝执行命令并向客户端返回一个类型错误;
# 多态命令的实现
Redis 除了会根据值对象的类型来判断键是否能够执行指定命令之外, 还会根据值对象的编码方式, 选择正确的命令实现代码来执行命令。
举个例子, 在前面介绍列表对象的编码时我们说过, 列表对象有 ziplist 和 linkedlist 两种编码可用, 其中前者使用压缩列表 API 来实现列表命令, 而后者则使用双端链表 API 来实现列表命令。
现在, 考虑这样一个情况, 如果我们对一个键执行 LLEN 命令, 那么服务器除了要确保执行命令的是列表键之外, 还需要根据键的值对象所使用的编码来选择正确的 LLEN 命令实现:
- 如果列表对象的编码为 ziplist , 那么说明列表对象的实现为压缩列表, 程序将使用 ziplistLen 函数来返回列表的长度;
- 如果列表对象的编码为 linkedlist , 那么说明列表对象的实现为双端链表, 程序将使用 listLength 函数来返回双端链表的长度;
借用面向对象方面的术语来说, 我们可以认为 LLEN 命令是多态(polymorphism (opens new window))的: 只要执行 LLEN 命令的是列表键, 那么无论值对象使用的是 ziplist 编码还是 linkedlist 编码, 命令都可以正常执行。
# 内存回收
- 因为 C 语言并不具备自动的内存回收功能, 所以 Redis 在自己的对象系统中构建了一个引用计数(reference counting (opens new window))技术实现的内存回收机制, 通过这一机制, 程序可以通过跟踪对象的引用计数信息, 在适当的时候自动释放对象并进行内存回收。
- 每个对象的引用计数信息由 redisObject 结构的 refcount 属性记录:
typedef struct redisObject {
// ...
// 引用计数
int refcount;
// ...
} robj;
- 对象的引用计数信息会随着对象的使用状态而不断变化:
- 在创建一个新对象时, 引用计数的值会被初始化为 1 ;
- 当对象被一个新程序使用时, 它的引用计数值会被增一;
- 当对象不再被一个程序使用时, 它的引用计数值会被减一;
- 当对象的引用计数值变为 0 时, 对象所占用的内存会被释放。