很抱歉, 标识出错了,下面的k代表的是字节,b代表的bit, 不要被我图中误导
说数据类型之前, 优先说一个对象
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
// 引用计数
int refcount;
// 指向实际值的指针
void *ptr;
} robj;
其中type可以取值枚举如下:
TYPE枚举 | VALUE值 | 代表 |
---|---|---|
OBJ_STRING | 0 | STRING |
OBJ_LIST | 1 | LIST |
OBJ_SET | 2 | SET |
OBJ_ZSET | 3 | ZSET |
OBJ_HASH | 4 | HASH |
OBJ_MODULE | 5 | - |
OBJ_STREAM | 6 | - |
不同类型Type,它的底层实现/编码方式也是不一样的。枚举类型如下:
encoding枚举 | VALUE值 | str描述 |
---|---|---|
OBJ_ENCODING_RAW | 0 | raw,简单动态字符串 |
OBJ_ENCODING_INT | 1 | int long类型证书 |
OBJ_ENCODING_HT | 2 | hashtable 字典 |
OBJ_ENCODING_ZIPMAP | 3 | 不再使用, 转为ZIPLIST |
OBJ_ENCODING_LINKEDLIST | 4 | 不再使用,转为QUICKLIST |
OBJ_ENCODING_ZIPLIST | 5 | ziplist 压缩列表 |
OBJ_ENCODING_INTSET | 6 | intset 整数集合 |
OBJ_ENCODING_SKIPLIST | 7 | skiplist 跳跃表和字典 |
OBJ_ENCODING_EMBSTR | 8 | embstr 编码的简单动态字符串 |
OBJ_ENCODING_QUICKLIST | 9 | quicklist |
OBJ_ENCODING_STREAM | 10 | — |
通过上图了解到, redis的键值的值 是一个 redisObject 对象, 他有type: 0-4是我们常用的数据类型。encoding 是在type基础上又细化出来的类型, 和type的关系如下
以下是redis-server 收到命令,执行的一个过程
参考文章:
redis源码分析(3)——请求处理
Redis里一个简单请求如何被处理
【Redis源码分析】Redis命令处理生命周期
STRING
- 分别看下3string的3种编码所对应的数据结构:
- int 做了优化, ptr 直接指向一个long类型的数字, 在不同的系统, LONG_MAX的大小不一样, 这个要注意,如果超过了 LONG_MAX所能表示的大小, 则会转成raw
o->encoding = OBJ_ENCODING_INT; o->ptr = (void*) value;
优点:快, 无需解释
- embstr 会直接分配一个相邻的内存存储字符串, 但是如果长度 > OBJ_ENCODING_EMBSTR_SIZE_LIMIT 就会转成 raw
robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1); o->ptr = sh+1;
优点:
1.embstr的创建只需分配一次内存,而raw为两次(一次为sds分配对象,另一次为redisObject分配对象,embstr省去了第一次)。
2. 相对地,释放内存的次数也由两次变为一次。
3. embstr的redisObject和sds放在一起,更好地利用缓存带来的优势
缺点:
redis并未提供任何修改embstr的方式,即embstr是只读的形式。对embstr的修改实际上是先转换为raw再进行修改3 . raw 是 sds 【简单动态字符串】结构存储,是redis 最底层实现
robj *createRawStringObject(const char *ptr, size_t len) { return createObject(OBJ_STRING, sdsnewlen(ptr,len)); }
新版本也对sds结构做了优化, 分成了以下几种【长度在0 和 2^5-1 之间,选用SDS_TYPE_5类型的header。 长度在 2^5 和 2^8-1 之间,选用SDS_TYPE_8类型的header。 长度在2^8 和 2^16-1 之间,选用SDS_TYPE_16类型的header。 长度在 2^16 和 2^32-1 之间,选用SDS_TYPE_32类型的header。 长度大于2^32 的,选用SDS_TYPE_64类型的header。能表示的最大长度为2^64-1。】
>> 最新版本的数据结构定义如下:除了sdshdr5之外,其它4个header的结构都包含3个字段:len: 表示字符串的真正长度(不包含NULL结束符在内)。 alloc: 表示字符串的最大容量(不包含最后多余的那个字节)。 flags: 总是占用一个 字节。其中的最低3个bit用来表示header的类型。
优点:
- 常数复杂度获取字符串长度。
sds.len 会保存字符串长度- 杜绝缓冲区溢出。
使用C字符串的操作时,如果字符串长度增加(如strcat操作)而忘记重新分配内存,很容易造成缓冲区的溢出;而SDS由于记录了长度,相应的操作在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。- 减少修改字符串长度时所需的内存重分配次数。
当执行sdstrim(截取字符串)之后,SDS不会立马释放多出来的空间,如果下次再进行拼接字符串操作,且拼接的没有刚才释放的空间大,则那些未使用的空间就会排上用场。通过惰性释放空间避免了特定情况下操作字符串的内存重新分配操作。- 二进制安全。
除int以外, 所有字符串都是以字符编码记录的, 所以不存在因为某种特殊的标志导致异常
- 几种命令的应用场景
- 通用命令: GET,SET,MGET, MSET,GETSET
- 带锁功能:MSETNX(原子锁),SETNX
- 字符串操作: APPEND,STRLEN ,GETRANGE,SETRANGE(这个命令对空字符串操作需要注意)
- 数字增减: INCR,INCRBY,INCRBYFLOAT, DECR, DECRBY
- 位操作:BITCOUNT,BITOP,GETBIT,SETBIT
- 带有效期的:PSETEX(毫秒),SETEX(秒)
- 对于 BIT 需要特殊说明一下
bit 是按位存储的, 因此他会根据传递的偏移,来决定开辟的存储空间,所以 strlen 计算长度的时候 1-7是一个字节, 20位是3个字节。如图
使用场景也很广泛, 主要思路是在这里插入代码片
绘制bitmap图
比如
- 某一天,有多少个用户在线, 思路:每一个玩家表示特定的某1位,然后直接使用 BITCOUNT就可以得到
- 统计某个用户的某一周的运动次数, 思路:没运动表示0, 运动表示1
list
- list的数据结构还是很好理解的:假设如图:【有个柜子, 柜子里面有很多个抽屉,抽屉随便放东西】
3.2 之后, redis采用 quicklist 作为list的基础数据结构,从数据结构的角度来说quicklist结合了ziplist(压缩包)和adlist(双向表)两种数据结构的优缺点,复杂但是实用:链表在插入,删除节点的时间复杂度很低;但是内存利用率低,且由于内存不连续容易产生内存碎片
压缩表内存连续,存储效率高;但是插入和删除的成本太高,需要频繁的进行数据搬移、释放或申请内存
上图是插入第一个元素时候的数据结构,在第一插入的时候,他新建了3个节点1. quicklist, 2. quicklistNode , ziplist。 quicklist管理quicklistNode 【双向列表的第一个节点】, quicklistNode 管理ziplist。
这里新增了2个结构typedef struct quicklist { quicklistNode *head; //head,tail:分别指向头、尾 quicklistNode *tail; unsigned long count; // 所有ziplist数据项总和。 unsigned long len; // quicklistNode的总个数 int fill : 16; // ziplist大小设置。存放list-max-ziplist-size参数的值 unsigned int compress : 16; //压缩深度设置。存放list-compress-depth参数的值。 } quicklist; typedef struct quicklistNode { struct quicklistNode *prev; // 前一个节点 struct quicklistNode *next; // 后一个节点 unsigned char *zl; // ziplist unsigned int sz; // ziplist的实际内存大小.即使被压缩,也仍旧是未压缩的大小。 unsigned int count : 16; // zpilist中数据项的个数 unsigned int encoding : 2; // 1为ziplist 2是LZF压缩存储方式 unsigned int container : 2; //预留字段,存放数据的方式,1--NONE,2--ziplist unsigned int recompress : 1; // 压缩标志, 为1 是压缩 unsigned int attempted_compress : 1; // 节点是否能够被压缩,只用在测试 unsigned int extra : 10; /*扩展字段。 */ } quicklistNode;
下面是
lpush xx ab
命令后 ziplist的结构【实际内存存储是高位字节放在右边,低位字节放在左边,这个主意,这么表示是内了方便看】
字段 zlentry 含义 zlbytes - 4个字节。表示整个压缩链表占用的字节数(包括它自己)。 zltail - 4个字节。压缩链表头部到最后一个 zlentry 的偏移量,在快速跳转到链表尾部的场景使用。 zllen - 占用2个字节。用于存储压缩链表中包含的 zlentry 总数。 - prelen 上一个 zlentry 的字节数 - [code]长度 [字符编码]长度, 根据实际大小分成1-5字节 - data 真实数据 zlend - 压缩链表的末尾。一个字节,值为255(0xFF)。 插入第二个元素的时候又分两种情况, 这里都是以lpush来演示的, rpush反过来就可以了
- 插入元素的大小超过 ziplist 能分配的大小
- 插入的元素大小能使用ziplist分配
实际上 ziplist 比上面描述更复杂一些, 主要体现在以下几点:
- 数字的存储
- 字符串长度的压缩
- 插入一个超长字符串, 对 prelen的调整
- 命令的应用场景
- 栈 先进后出
- 队列 先进先出
- 数组
- 阻塞,单播队列FIFO (BRPOPLPUSH, BRPOP, BLPOP)
- 源码分析参考:
Redis 源码分析(五) :ziplist
redis源码笔记-内存管理zmalloc.c
redis源码解读(六):基础数据结构之quicklist
redis源码解读(五):基础数据结构之ziplist
学习redis源码():quicklist