快速掌握 Redis 五种基本数据类型的原理
文章目录
- 快速掌握 Redis 五种基本数据类型的原理
- 类型与编码
- 类型
- 编码
- 类型与编码映射
- 字符串 STRING
- 1. int
- 2. raw
- 3. embstr
- 转换
- 对象共享
- 列表对象 LIST
- 1. ziplist
- 2. linkedlist
- 3. quicklist (Redis 3.2)
- 哈希对象 HASH
- 1. ziplist
- 2. hashtable
- 集合 SET
- 1. intset
- 2. hashtable
- 有序集合 ZSET
- 1. ziplist
- 2. skiplist
- 字符串类型
- 内存空间预分配
- 惰性空间释放
- 其他数据类型
正文
使用 Redis ,离不开这五种基本的数据对象类型——字符串、列表、哈希、集合、有序集合。通常在程序设计中,我们会按图索骥,各取所需。但是每个数据类型他们的底层是怎样的呢?Redis 又对这些数据类型做了哪些优化呢?接下来让我们一起寻求这一答案。
Redis 对象数据结构
typedef struct redisObject{
// 类型
unsigned type: 4;
// 编码
unsigned encoding: 4;
// 指向底层实现数据结构的指针
void *ptr;
//...
}
类型与编码
类型
类型 | 对象 |
STRING | 字符串对象 |
LIST | 列表对象 |
HASH | 哈希对象 |
SET | 集合对象 |
ZSET | 有序集合对象 |
编码
编码 | 编码对应的底层数据结构 |
INT | long 类型的整数 |
EMBSTR | embstr 编码的简单动态字符串 |
RAW | 简单动态字符串 |
HT | 字典 HASH_TABLE |
LINKEDLIST | 双端链表 |
ZIPLIST | 压缩列表 |
INTSET | 整数集合 |
SKIPLIST | 跳跃表和字典 |
类型与编码映射
类型 | 编码 | 编码对应的底层数据结构 |
STRING | INT | long 类型的整数 |
STRING | EMBSTR | embstr 编码的简单动态字符串 |
STRING | RAW | 简单动态字符串 |
LIST | ZIPLIST | 压缩列表 |
LIST | QUICKLIST | 快速列表 |
LIST | LINKEDLIST | 双端链表 |
HASH | ZIPLIST | 压缩列表 |
HASH | HT | 字典 |
SET | INTSET | 整数集合 |
SET | HT | 字典 |
ZSET | ZIPLIST | 压缩列表 |
ZSET | SKIPLIST | 跳跃表和字典 |
⚠️ 每种类型对象都至少使用了两种不同的编码
字符串 STRING
1. int
如果一个字符串对象保存的是整数值并且可用long
类型来表示
- long 整数
2. raw
如果一个字符串对象保存的是字符串值,并且这个字符串值的长度大于 32 字节
两次内存分配 分别创建 redisObject 和 SDSHdr(Redis 自己实现的字符串结构 Simple Dynamic String Header)
- 大于 32 字符
3. embstr
如果一个字符串对象保存的是字符串值,并且这个字符串值的长度小于 32 字节
通过一次内存分配,同时分配 redisObject 和 SDSHdr
- 小于 32 字节
- 只分配 1 次内存
⚠️ 浮点数也是用字符串来存储,取出时再从字符串转为浮点数
转换
单向转换
- int -> raw 字符串操作导致值不再满足 long 类型整数条件
- embstr -> raw 字符串操作导致不再满足 字符串 小于 32 字节条件
对象共享
Redis 初始化时会创建好 0 到 9999 的整数字符串。所有 0 - 9999 的整数值内容都直接共享对象(包括 LIST、HASH、SET 中的整数值)而不单独存储整数字符串。
【问题】为什么不共享字符串值的字符串对象?(点击展开答案)
- 整数值的字符串对象验证操作复杂度 O(1)
- 字符串 验证复杂度 O(N)
列表对象 LIST
1. ziplist
- 元素数量小于 512 个
- 每个元素的长度都小于 64 字节
2. linkedlist
3. quicklist (Redis 3.2)
- Redis 3.2 引入 quicklist 代替 ziplist 和 linkedlist 作为 LIST 底层实现
- 由节点 ziplist 组成的双向链表
- 解决了 ziplist 的连锁更新问题
哈希对象 HASH
1. ziplist
同一个键值对的两个节点紧挨在一起,键在前,值在后。
- 键值对小于 512 个
- 键和值的字符串长度都要小于 64 字节
2. hashtable
键值都是字符串对象
集合 SET
1. intset
- 所有元素都是整数
- 元素数量不超过 512 个
2. hashtable
- value 是 Null
有序集合 ZSET
1. ziplist
- 集合所有元素数量小于 128 个
- 每个成员对象小于 64 字节
2. skiplist
typedef struct zset {
// 跳跃表结构
zskiplist *zsl;
// 字典
dict *dict
}
zsl
数据包含成员(字符串对象)和分数(Double 浮点数)
dict
包括成员和分数
【问题】会不会有数据不同步的问题?
他们通过指针共享成员和分数值,从而避免数据不同步的情况。
- zset
- dict 字典
- zsl (skiplist) 跳跃表
- 通过指针共享数据,不产生两份数据
- 成员 STRING 字符串对象
- 分值 Double 浮点数类型
字符串类型
Redis 自己实现的「简单动态字符串」 simple dynamic string 简称 SDS
struct sdshdr {
// 记录 buf 已使用字节长度,也就是字符串长度
int len;
// 记录 buf 未使用字节的数量
int free;
char buf[];
}
简单的字符串结构
free: 5
len: 5
buf: R e d i s \0 _ _ _ _ _
⚠️ \0
不计入 len 属性里面
【问题】为什么不直接用 C 字符串?
不能保证字符串的安全性。通过 len 来判断有效字符串长度,而不是通过 `\0`。
【问题】但是为什么要加入 `\0` 的设计?
兼容 C 的一些字符串现有函数
内存空间预分配
- 小于 1MB 扩张二倍
- 大于等于 1MB 扩张 1MB
注意,还需要外加 1 byte 的 \0
的位置
惰性空间释放
减少字符串数量,并不会导致空间被收回。
可通过 sdsfree 释放空间。
其他数据类型
除了最基础的这五种数据类型,Redis 还提供如下四种针对特殊场景的四种特殊的数据结构
- bitmap 提供位与或非操作
- hyperLogLog 提供不精确的去重计数方案
- bloomFilter 布隆过滤器
- GeoHash 附近的人