Redis 的数据类型可谓是 Redis 的精华所在,作为一款 QPS 能达到 10w 级别的内存数据库,具有如此高性能的原因有很多。除了所有的操作都在内存中进行之外,其数据类型的底层设计也起到了很大的作用。
我们知道 Redis 中有 5 种基础数据类型,分别是:String(字符串)、List(列表)、Hash(哈希)、Set(集合)和 ZSet(有序集合)。而为了支持这些数据类型,Redis 使用了多种数据结构来作为这些类型的底层结构。
这些数据结构是我们后续的重点,当然啦 Redis 的数据类型还不止这五种,还有几个更高级的类型,我们后续也会说,但最常用的还是上面五种。
不过这里我主要是想介绍一下 Redis 是怎么存储键值对的。
127.0.0.1:6379> SET name satori OK 127.0.0.1:6379> LPUSH scores 90 95 93 (integer) 3 127.0.0.1:6379> HSET students satori 99 (integer) 1 127.0.0.1:6379>
这里的 name、scores、students 都属于 key,它们的 value 是不同的类型,那么问题来了,这些 key、value 存在什么地方呢?其实不难得出,既然 Redis 是键值对数据库,查询的效率又这么高,那么肯定是存在哈希表当中的,事实上也确实如此。
以上代码位于 Redis 源码的 src/server.c 文件中,代码里面的 server 就相当于 Redis 的服务端。
另外我们知道 Redis 默认有 16 个库,而 server.db[j] 就是获取 j 号库,所以此时相信你应该知道上面的代码所做的事情是什么了,就是为每一个库创建多个哈希表。而我们使用命令创建的 key、value 都会存储在 server.db[j].dict 里面,就是绿色框框里面创建的哈希表。
所以结论很清晰了,每一个库都有一个全局的哈希表 server.db[j].dict,专门负责存储设置的 key、value。其中 key 永远为 String 类型,value 可以是任意类型。这也侧面说明了我们设置的 key 不会重复,因为哈希表里面的 key 是不重复的。
一开始 hello 这个 key 不存在,那么会自动往全局的哈希表中加入一个键值对,key 为 hello,值的类型为 String。然后执行 lpush 的时候,由于 hello 这个 key 已存在,那么再执行就报错了。
现在我们知道 key、value 是存在哈希表里面了,但具体是怎么存储的呢?我们来看一张图:
一个哈希表可以是一个数组,数组里面的每一个存储单元都叫做哈希桶(Bucket),比如数组第一个位置(索引为 0)被编为哈希桶 0,第二个位置(索引为 1)被编为哈希桶 1,以此类推。
当我们写入一个键值对的时候,会根据 key 和 value 的指针构建一个 dictEntry 结构体实例。然后通过对 key 进行哈希运算来计算出桶的位置,最后将 dictEntry 结构体实例的指针写入哈希表中。
关于 Redis 哈希表的具体细节后续再聊,目前只需要知道 Redis 的 key、value 是存在哈希表中的即可。当然啦,更准确地说应该是 key、value 的指针所构建的 dictEntry 结构体实例的指针是存在哈希表当中的。
当我们通过 key 进行查找的时候,会先对 key 进行哈希运算,找到对应的哈希桶中存储的 dictEntry *,然后再根据 value 获取对应的值。
最后还需要特别补充一下,Redis 中所有的对象其实都是一个 redisObject 结构体实例,其结构如下:
不管是 String 对象、还是 List 对象、Hash 对象等等,它们其实都是一个 redisObject 对象,里面有一个 type 字段来标识这个对象的所属类型。
127.0.0.1:6379> type name string 127.0.0.1:6379> type students hash 127.0.0.1:6379> type scores list
我们可以用 type 命令查看对象的类型,虽然显示的类型不同,但本质上它们都是 redisObject 这个结构体的实例对象。内部的 encoding 表示该类型的对象所使用的底层数据结构,ptr 表示指向底层数据结构的指针,而根据 encoding 和 ptr 的不同,type 可以是 string, list, hash 等等。
比如我们执行 sadd box 1 2 3 1,我们会说 box 的值是一个 Set 对象,这种说法是正确的。但是我们应该知道它其实是一个 redisObject,里面的 type 字段等于 "set",这背后的细节要搞清楚。不过后续我们还是会按照 String、List、Hash 之类的来称呼,就不用 redisObject 了,只要明白背后的关系就行。
如果你看过 Python 源代码的话,你会发现 redisObject 和 PyObject 在设计上有着异曲同工之妙。
小结
到目前为止,我们算是对 Redis 的键值对有了一个全局的认识。
Redis 内部有一个 redisObject,所有的键值对都是 redisObject 对象。只不过根据其内部 encoding 和 ptr 的不同,我们划分出了 String, List, Hash, Set, ZSet 等类型。键永远是 String 类型,值可以是任意类型,但不管哪一种类型,它们本质上都是 redisObject 对象。
下一篇文章我们就来详细解析这些数据类型是怎么实现的,先拿字符串开刀。