redis源码系列之~简介

本月将持续更新redis源码系列~~

相关文章链接:

redis源码系列之一-sds :https://blog.csdn.net/zhangxiaomin1992/article/details/119680802

redis源码系列之三-整数集合insert https://mp.csdn.net/mp_blog/creation/editor/119680377

redis常见数据类型,命令和底层数据结构简介

一:string 常见命令:

redis> SET db redis
OK

redis> GET db
"redis"

底层结构:

这里创建了2个sds,一个是"db"的sds,另一个是值"redis"的sds。

3.2版本之前redis简易版的结构:

struct sdshdr{
  // 记录buf数组已使用字节数量
  int len;
  // 记录buf数组中未使用字节的数量
  int free;
  // 字节数组,用于保存字符串
  char buf[];
}

sds设计相比string字符串的优点?

1)有单独的统计变量len和free(称为头部)。可以很方便地得到字符串长度。
2)内容存放在柔性数组buf中,SDS对上层暴露的指针不是指向结构体SDS的指针,而是直接指向柔性数组buf的指针。上层可像读取C字符串一样读取SDS的内容,兼容C语言处理字符串的各种函数。
3)由于有长度统计变量len的存在,读写字符串时不依赖“\0”终止符,保证了二进制安全。 

3.2之后的结构:

针对不同长度考虑使用不同的类型表示len+free:

短字符串1个字节表示,3位表示类型,容纳2的5次方存储长度的大小也就是len(不是表示内容),长度小于等于(2的5次方-1)个字符串,对应sdshdr5

长字符串 2个字节,len占1字节,free占1字节,容纳长度小于等于(2的8次方-1)个字符串,对应sdshdr8

再长字符串 4个字节,len和free各自占2字节,容纳长度小于等于(2的16次方-1)个字符串,对应sdshdr16

更长字符串 8个字节,len和free各自占4字节,容纳长度小于等于(2的32次方-1)个字符串,对应sdshdr32

struct __attribute__ ((__packed__))sdshdr5 {
    unsigned char flags; /* 低3位存储类型, 高5位存储长度 */
    char buf[];/*柔性数组,存放实际内容*/
};
struct __attribute__((__packed__))sdshdr8 {
    uint8_t len; /* 已使用长度,用1字节存储 */
    uint8_t alloc; /* 总长度,用1字节存储*/
    unsigned char flags; /* 低3位存储类型, 高5位预留 */
    char buf[];/*柔性数组,存放实际内容*/
};
struct __attribute__((__packed__))sdshdr16 {
    uint16_t len; /*已使用长度,用2字节存储*/
    uint16_t alloc; /* 总长度,用2字节存储*/
    unsigned char flags; /* 低3位存储类型, 高5位预留 */
    char buf[];/*柔性数组,存放实际内容*/
};
struct __attribute__((__packed__))sdshdr32 {
uint32_t len; /*已使用长度,用4字节存储*/
uint32_t alloc; /* 总长度,用4字节存储*/
unsigned char flags;/* 低3位存储类型, 高5位预留 */
char buf[];/*柔性数组,存放实际内容*/
};
struct __attribute__((__packed__))sdshdr64 {
uint64_t len; /*已使用长度,用8字节存储*/
uint64_t alloc; /* 总长度,用8字节存储*/
unsigned char flags; /* 低3位存储类型, 高5位预留 */
char buf[];/*柔性数组,存放实际内容*/
};

 api源码详见:https://blog.csdn.net/zhangxiaomin1992/article/details/119680802

二、list常见命令

# 加入多个元素
redis> LPUSH mylist a b c
(integer) 3

redis> LRANGE mylist 0 -1
1) "c"
2) "b"
3) "a"

底层结构:

当元素个数比较少(少于512个)并且元素长度比较小(小于64字节)时,Redis采用ziplist作为其底层存储;否则采用quiklist存储。

list-max-ziplist-value 64

list-max-ziplist-entries 512

介绍ziplist:

从宏观上看,ziplist的内存结构,各个部分在内存上是前后相邻的

1)zlbytes: 压缩列表的字节长度,占4个字节,因此压缩列表最多有232 -1个字节。
2)zltail: 压缩列表尾元素相对于压缩列表起始地址的偏移量,占4个字节。
3)zllen: 压缩列表的元素个数,占2个字节。zllen无法存储元素个数超过65535(216 -1)的
压缩列表,必须遍历整个压缩列表才能获取到元素个数。
4)entryX: 压缩列表存储的元素,可以是字节数组或者整数,长度不限。每一个entry的构成:

<prevrawlen><len><data>,

  • <prevrawlen>: 表示前一个数据项占用的总字节数。这个字段的用处是为了让ziplist能够从后向前遍历(从后一项的位置,只需向前偏移prevrawlen个字节,就找到了前一项)。这个字段采用变长编码。
  • <len>: 表示当前数据项的数据长度(即<data>部分的长度)。也采用变长编码。
  • <Data>:真实的数据

5)zlend: 压缩列表的结尾,占1个字节,恒为0xFF

那么<prevrawlen>和<len>是怎么进行变长编码的呢?

先说<prevrawlen>。它有两种可能,或者是1个字节,或者是5个字节:

如果前一个数据项占用字节数小于254,那么<prevrawlen>就只用一个字节来表示,这个字节的值就是前一个数据项的占用字节数。

如果前一个数据项占用字节数大于等于254,那么<prevrawlen>就用5个字节来表示,其中第1个字节的值是254(作为这种情况的一个标记),而后面4个字节组成一个整型值,来真正存储前一个数据项的占用字节数。

有人会问了,为什么没有255的情况呢?

这是因为:255已经定义为ziplist结束标记<zlend>的值了。在ziplist的很多操作的实现中,都会根据数据项的第1个字节是不是255来判断当前是不是到达ziplist的结尾了,因此一个正常的数据的第1个字节(也就是<prevrawlen>的第1个字节)是不能够取255这个值的,否则就冲突了。

而<len>字段就更加复杂了,它根据第1个字节的不同,总共分为9种情况:

|00pppppp| 第1个字节最高两个bit是00,那么<len>字段只有1个字节,剩余的6个bit用来表示长度值,最高可以表示63 (2^6-1)的长度大小

|01pppppp|qqqqqqqq| - 2 bytes。第1个字节最高两个bit是01,那么<len>字段占2个字节,总共有14个bit用来表示长度值,最高可以表示16383 (2^14-1)的长度大小。

|10__|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes。第1个字节最高两个bit是10,那么len字段占5个字节,总共使用32个bit来表示长度值(_表示的那6个bit舍弃不用),最高可以表示 2^32-1的长度大小。需要注意的是:在前三种情况下,<data>都是按字符串来存储的;从下面第4种情况开始,<data>开始变为按整数来存储了。

|11000000| - 1 byte。<len>字段占用1个字节,值为0xC0,后面的数据<data>存储为2个字节的int16_t类型。

|11010000| - 1 byte。<len>字段占用1个字节,值为0xD0,后面的数据<data>存储为4个字节的int32_t类型。

|11100000| - 1 byte。<len>字段占用1个字节,值为0xE0,后面的数据<data>存储为8个字节的int64_t类型。

|11110000| - 1 byte。<len>字段占用1个字节,值为0xF0,后面的数据<data>存储为3个字节长的整数。

|11111110| - 1 byte。<len>字段占用1个字节,值为0xFE,后面的数据<data>存储为1个字节的整数。

|1111xxxx| - - (xxxx的值在0001和1101之间)。这是一种特殊情况,xxxx从1到13一共13个值,这时就用这13个值来表示真正的数据。

注意,这里是表示真正的数据,而不是数据长度了。也就是说,在这种情况下,后面不再需要一个单独的<data>字段来表示真正的数据了,而是<len>和<data>合二为一了。

另外,由于xxxx只能取0001和1101这13个值了(其它可能的值和其它情况冲突了,比如0000和1110分别同前面第7种第8种情况冲突,1111跟结束标记冲突),

而小数值应该从0开始,因此这13个值分别表示0到12,即xxxx的值减去1才是它所要表示的那个整数数据的内容。

下面看一个真实的分析ziplist的例子:

  • 这个ziplist一共包含33个字节。字节编号从byte[0]到byte[32]。图中每个字节的值使用16进制表示。
  • 头4个字节(0x21000000)是按小端(little endian)模式存储的<zlbytes>字段。什么是小端呢?就是指数据的低字节保存在内存的低地址中。因此,这里<zlbytes>的值应该解析成0x00000021,用十六进制表示正好就是33,表示整个zipilist占用33个字节。
  • 接下来4个字节(byte[4..7])是<zltail>,用小端存储模式来解释,它的值是0x0000001D(值为29),表示最后一个数据项在byte[29]的位置(那个数据项为0x05 FE 14)可以看出这个是相对于首地址的偏移量,也可以理解为最后一个元素首地址在zilipst中的位置。
  • 再接下来2个字节(byte[8..9]),值为0x0004,表示这个ziplist里一共存有4项数据。
  • 接下来6个字节(byte[10..15])是第1个数据项。其中,prevrawlen=0,因为它前面没有数据项;len=4,用16进吱表示为0000 0010 ,最高位的前2个bit为00,相当于前面定义的9种情况中的第1种,表示后面4个字节按字符串存储数据,a的ASCLL值为97,A的ASCLL值为65,所以61(16*6+1=97)表示字符a,整体数据的值为”name”。
  • 接下来8个字节(byte[16..23])是第2个数据项,prevrawlen=6,表示前一个数据占6个字节(前一个数据项的prevrawlen占1个字节,根据len第1个字节最高两个bit是00,那么<len>字段只有1个字节,name占4个字节),len=06,表示后面6个字节是第二项数据的内容,存储的是1个字符串”tielei”。
  • 接下来5个字节(byte[24..28])是第3个数据项,prevrawlen=8,表示前一个数据占8个字节(前一个数据项的prevrawlen占1个字节,len占1个字节,内容占6个字节),len=03,表示后面3个字节是存储的内容,存储1个字符串”age”。
  • 接下来3个字节(byte[29..31])是最后一个数据项,其中,第1个字节prevrawlen=5,表示前一个数据项占用5个字节;第2个字节=FE,用16进制表示为14+16*15=254 为11111110,相当于前面定义的9种情况中的第8种,所以后面还有1个字节用来表示真正的数据,并且以整数表示。它的值是20(0x14)。
  • 最后1个字节(byte[32])表示<zlend>,是固定的值255(0xFF)。

可以猜测这个是命令 hset name tielei age 20插入的数据。

下面引入一篇关于ziplist插入和删除的数据图,这篇文章讲的不错

https://www.cnblogs.com/chinxi/p/12272173.html

三、set常见命令:

# 添加单个元素
redis> SADD bbs "discuz.net"
(integer) 1
# 添加重复元素
redis> SADD bbs "discuz.net"
(integer) 0


# 添加多个元素
redis> SADD bbs "tianya.cn" "groups.google.com"
(integer) 2
redis> SMEMBERS bbs
1) "discuz.net"
2) "groups.google.com"
3) "tianya.cn"

 底层实现:

set集合对象的编码可以是 intset 或者 hashtable。当集合类型的元素都是整数并且都处在64位有符号整数范围之内时,并且元素个数不超过512个的时候,使用结构体insert存储,否则使用hashtable存储。

inset数据结构:

 

 hashtable数据结构参考后续文章~~

四、zset常见命令:

# 添加多个元素
redis> ZADD page_rank 9 baidu.com 8 bing.com
(integer) 2

redis> ZRANGE page_rank 0 -1 WITHSCORES
1) "bing.com"
2) "8"
3) "baidu.com"
4) "9"
5) "google.com"
6) "10"

底层实现:

底层分别使用ziplist(压缩链表)和skiplist(跳表)实现,当zset满足以下两个条件(保存的元素少于128个并且保存的所有元素大小都小于64字节)的时候,使用ziplist

不满足这两个条件则使用skiplist。(注意:这两个数值是可以通过redis.conf的zset-max-ziplist-entries和 zset-max-ziplist-value选项 进行修改。)

跳跃表数据结构参考后续文章~~

五、hash常见命令:

//1个值
redis> HSET site redis redis.com
(integer) 1
redis> HGET site redis
"redis.com"

//多个值
redis> HMSET website google www.google.com yahoo www.yahoo.com
OK
redis> HGET website google
"www.google.com"
redis> HGET website yahoo
"www.yahoo.com"

底层实现:

redis的哈希对象的底层存储可以使用ziplist(压缩列表)和hashtable。

当hash对象可以同时满足一下两个条件时,哈希对象使用ziplist编码。否则使用hashtable

哈希对象保存的所有键值对的键和值的字符串长度都小于64字节

哈希对象保存的键值对数量小于512个

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值