无题
在前面第 8 讲《 实战应用篇:Hash 命令详解与实战(上)》中我们已经讲解了哈希表的结构,就如下图所示,这个 HashMap 有 8 个槽位,每个槽位下面挂一个链表。我们这一节就来看看 Redis 里面哈希表是怎么实现的。
在 Redis 里面,Hash 表底层依赖的数据结构其实有两种:第一种是前面介绍的 ziplist,Redis 7.0 之后呢,就是 listpack,总之就是一块连续的内存空间;另一种是我们今天这一节要介绍的 dict 结构,它就是哈希表的结构。
在后面介绍 redisObject 对象的时候,我们还会展开说明哈希表底层什么时候用 listpack、什么时候用 dict,现在你只需要知道:哈希表底层数据少、键值对都比较短的时候用 listpack 存,数据量大了之后就用 dict 存。
dict & dictEntry在 Redis 里面,哈希表对应的是 dict 这个结构体,你看其他 Redis 文章的时候,说到的字典结构,其实就是这个结构体。
我们打开 dict.h 这个头文件,找到字典这个结构体的定义,如下所示:
123456struct di ...
无题
在上一节中,我们详细介绍了 dict 和 dictType 这两个与 Redis 哈希表紧密相关的结构体实现。
在这一节中,我们将紧接上一节的内容,展开介绍 Redis 操作 dict 的核心方法,主要包括创建 dict、向 dict 中添加数据、从 dict 中查找数据、修改 dict 中的数据以及删除数据,其中还会展开介绍 dict 的扩容和渐进式 rehash 的内容。
创建 dict要使用 dict,第一步,就是创建一个 dict 实例,来看 dictCreate() 函数,它里面会调一下 malloc 函数,申请一个 dict 实例的空间,创建 dict 的事情就完成了。在 _dictInit() 这个函数中,会初始化 dict 实例里面各个字段的值,例如,dict->ht_table 中的两个 dictEntry 指针都会被初始化为 NULL,感兴趣的小伙伴可以展开看看这些字段的初始值到底是什么。
123456dict *dictCreate(dictType *type){ dict *d = zmalloc(sizeof(*d)); // 申请空间 ...
无题
在上一节中,我们详细分析了哈希表基础操作的实现,主要涉及如何增删改查哈希表中的数据以及渐进式 rehash 的核心原理。
在这一节,我们来分析一下 dict 的迭代器实现,HSCAN 命令底层就直接依赖 dict 迭代器进行实现。先来考虑一下 dict 迭代器出现的原因,是不是和 quicklist 的情况类似呢?dict 里面有两个 ht_table,而且每个 ht_table 是“数组 + 链表”的结构,所以加个迭代器,屏蔽一下这些底层复杂结构,也是非常合理的选择。
dict 迭代器如果要我们自己实现一个迭代器的话,需要维护什么信息呢?
我们至少需要知道四个值:自己迭代的是哪个 dict 对象,迭代哪个 ht_table,迭代哪个槽位,迭代到了哪个节点。来看一下 dictIterator 结构体,里面的 d、table、index、entry 就是我们需要的四个值。
1234567891011typedef struct dictIterator { dict *d; // 当前迭代的dict实例 long index; // 当前迭代到的槽位 // ...
无题
在 Java 里面,HashSet 底层是用 HashMap 实现的。在 Redis 里面也是类似的,Redis 里面的 Hash 底层结构是 dict,Set 底层的结构也是 dict。但是,在元素都是整数值的时候,Set 可以用一种更省空间的方式来存数据,这种省空间的方式就是这一节要说的 intset。
一个 Set 要用 intset 作为底层存储的话,需要满足两个条件:
一个就是前面说的,元素都是整数类型;
另一个条件是这个 Set 里面的元素个数,要少于 set-max-intset-entries 配置指定的这个值,这个值默认是 512。
一旦这个 Set 集合不满足这两个条件,就会切换成 dict 作为底层存储。Redis 之所以使用 intset 结构来进行优化,主要是为了减少内存碎片,提高查询效率,这也体现了 Redis 在空间占用和耗时等方面的折中和思考。
intset 结构体从名字就可以看出,intset 结构体是用来存储整数类型的集合,不仅如此,intset 中存储的整数还是有序的,这样我们就可以非常方便地使用二分查找来查找一个元素。下面来看 intse ...
无题
Redis 五大数据类型中有一种叫作 Sorted Set (有序集合)的类型,也经常被简称为“ZSet”。
ZSet 与我们常见的 Set 有一些类似,比如,Set 和 ZSet 里面存储的元素都不能重复。但是, ZSet 中的每个元素都会关联一个 double 类型的分数值(也称为 score),所有元素会按照 score 值的大小进行排序,这就是说 ZSet 是一个有序集合的原因。
在本节中,我们就从 ZSet 的底层存储开始,一步步抽丝剥茧,详细分析 Redis 中 ZSet 的核心原理和实现。
skiplist 基本概念Redis 底层使用跳跃表(skiplist) 这一数据结构作为 ZSet 的底层核心存储,当然,在一定条件下,Redis 会用 listpack 来优化 ZSet,在后面介绍 redisObject 的时候我们会单独拿出一节来详细讲解各个数据结构底层的这种优化。
skiplist 是基于随机算法的一种有序链表,其查询效率与红黑树的查询效率差不多,都是 O(logn),但是 skiplist 的结构远比复杂的红黑树简单很多。下面我们就深入介绍一下 skipli ...
无题
在前面的章节中,我们详细讲解了 listpack、ziplist、dict、skiplist 等一系列关键的数据结构,本节我们来介绍 Redis 中最后一个关键数据结构 —— Rax 树。
Rax 树可以帮助我们实现随机查找的能力,也是 Redis Stream 依赖的底层数据结构。Stream 是 Redis 用来实现消息队列能力的结构,Stream 的核心实现我们将在“模块九:生产者-消费者模式”中展开介绍,本节还是着重解析 Rax 的核心原理。
初识 Rax那什么是 Rax 呢?
Rax 是前缀压缩树的一种。具体来说就是,Rax 使用树型结构来存储字符串,对于有相同前缀的字符串,这相同前缀字符就会作为公共父节点进行存储。
下面举个例子,我们现在有 “foo”“foobar”“footer” 三个字符串,如果存储到前缀树中,结构就会如下图所示,每个节点存储一个字符,“f”“o”“o” 是三个字符串的公共前缀,在前缀树中是公共的前缀,其他非公共部分存储在树杈节点中。
那为什么说 Rax 是前缀压缩树呢?Rax 会将上面的压缩树中连续的、单字符的节点,压缩成了一个节点。上面的树型结构 ...
无题
在前面“数据结构篇”模块中,我们详细介绍了 Redis 底层依赖的数据结构以及操作这些数据结构的核心函数。
从本节开始,我们将介绍 Redis Server 的核心结构体,主要有 5 个:redisObject 是对所有 Redis Value 的封装,redisServer 是对 Redis 服务器的抽象,redisDB 表示的是对单个 Redis DB,client 抽象的是一个 Redis 客户端,redisCommand 则是 Redis 的命令。
这 5 个核心结构体分别表示 Redis 的数据、服务器、DB、客户端和命令,形成了一个有机的整体。
redisObject在 Redis 中有一个非常重要的结构体 —— robj,全称 redisObject,它用来表示一个 Redis 对象。虽然很多资料都说 C 语言没法做到真正意义的面向对象编程,但是我认为这种争论和分类是没什么意义的,你看 Redis 的 C 代码,照样能通过结构体等 C 的语法,实现面向对象的思想。
我们回到 robj 继续介绍,Redis 中 Key 只能是字符串类型,Value 可以是 sds、list ...
无题
Redis 五大基础数据结构里面,我们已经介绍完了 String、List 两个,这一节,我们就开始介绍一下哈希表部分的内容。
这里我们先简单介绍一下 Hash 的底层结构:Hash 由数组和链表两种数据结构组成,数组里面每个元素就是一个槽位,一个槽位下面挂了一个链表。写入 field-value 数据的时候,会计算 field 的 hash 值,然后对数据长度进行取模,找到对应的槽位,把 field-value 插入到这个链表里面。
了解了 Redis 里面哈希表的大概结构之后,我们再来看看 Hash 表的命令,大概分成三类,如下图所示(本节我们也将按照该图的思路去进行介绍):
读写操作哈希表里面最简单、最常用的操作,就是读写 field-value 数据,对应的就是 HGET、HSET命令。
下面先来演示一下 HSET 命令,HSET 命令的第一个参数是哈希表 Key 的名称,后面跟的才是 field-value 数据,示例中的 field 名称是 name,value 值是 kouzhaoxuejie。HSET 执行后的返回值为 1,表示成功写入了一个键值对。
12127.0 ...
无题
在上一节课中,我们介绍完了 Redis Hash 表中比较重要的命令。这一节紧接着上一节的内容,我们来介绍一下 Hash 结构在实战中的应用场景,并结合 Lettuce 客户端模拟这些应用场景的核心代码。
本节课程中,我们将使用 Redis 的哈希结构实现用户资料缓存以及购物车缓存两个实战应用场景。其中,用户资料缓存示例会结合哈希表的读写命令以及批量命令,演示用户资料如何在 Redis 中进行存储;购物车示例中,除了哈希表读写命令之外,还会使用到递增命令,以实现一个相对完整的、基于 Redis 的购物车功能。
用户资料缓存有开发经验的小伙伴可能知道,在开发系统的时候,用户模块是最基本、最基础的模块之一。用户模块设计简单点,可能存几列信息,例如存储用户名、手机号、密码之类的基本信息;设计复杂点,可能有四五十列,例如存储用户的头像、昵称、签名等信息。
一个 C 端产品的用户量一般都会比较大,每个用户每次登录的时候,都要校验密码,登录成功之后,返回头像、昵称这些基础信息,这个时候就比较适合用 Redis Hash 结构来存用户的这些信息,活跃用户的信息进 Redis 进行缓存,非活跃用户留 ...
无题
在前面的文章中,我们已经介绍完了 Redis 中的 String、List、Hash 三种核心数据结构的命令以及应用场景。这一节,我们将继续介绍 Redis 中另一个数据结构相关的命令 —— Set 集合。下图对 Set 相关的命令进行了一个简单的分类,本节也将按照这分类进行介绍。
Set 命令详解如上图所示,按照命令执行的效果,可以将 Set 相关的命令划分为基本操作和集合操作两大类,下面我们先来介绍基础命令这一个分类。
1. 基础命令Set 里面最基础的命令就是 SADD 命令,它会往 Set 集合里面添加新元素,SADD 命令后面可以跟多个元素。
在下面的示例中,就是用 SADD 命令往 myset 这个集合里面添加 kouzhao、xujie 两个字符串,第一次执行的时候,返回 2,意思就是成功添加了 2 个元素进去;第二次再执行这条命令的时候,返回 0 ,表示这两个元素添加失败了,这是因为 Set 集合的特性就是相同的元素只能有一个。
1234127.0.0.1:6379> SADD myset "kouzhao" "xuejie&quo ...
