Redis底层整体结构
- RedisServer 里面有个 redisDb *db 数组,该数组中有默认16个 redisDb 。
- redisDb 中有个 dict *dict 键空间(字典)。字典的 dictEntry 就是具体的redis数据,key为redis键,value是一个redisObject。
- redisObject存储了:对象的类型(上层容器),字符串/列表/集合/哈希表、底层的数据结构、LRU 时间、引用计数、真实数据的指针等信息,加上留空的两位,一共32位
redisServer、redisDb、dict、dictEntry、redisObject介绍
redisServer 结构保存服务器的状态
Redis 服务器使用 redisServer 结构保存服务器的状态。记录着 服务器的数据库数量 和 redisDb 类型的数组保存着所有的数据
```c
struct redisServer {
//服务器的数据库数量
int dbnum;
//一个数组,保存所有数据库对象
redisDb *db;
}
```
- Redis 是一个键值对 (key-value pair) 数据库服务器,服务器中的每个数据库都由一个 redisDb 结构表示,其中,redisDb 结构的 dict 字典保存了数据库中的所有键值对,即键空间。
redisDb 结构,表示 Redis 数据库的结构
typedef struct redisDb {
dict *dict;//保存了当前数据库的键空间
dict *expires;//键空间中所有键的过期时间
int id;//Database ID 从0开始
} redisDb;
dict(字典)结构、键空间简介
redis中的dict(字典)是一个安全的实现了rehash的多态哈希桶,是一个性能较高的数据结构,用来保存键值对,类似其他高级语言的map。
- 键空间键:就是redis的key,每一个键都是一个字符串对象,而且唯一
- 键空间值:是数据库的值,每一个值都是一个redisObject,可以是Redis的各种类型(字符串、列表、集合、有序集合、哈希)。
推荐个详细的:redis源码学习-dict篇
redisObject结构,值就保存于此
redis每个对象(key-value的value)都由一个redisObject的结构表示:
//刚刚好32 bits
typedef struct redisObject {
unsigned type:4;// 对象的类型(上层容器),字符串/列表/集合/哈希表
unsigned notused:2;// 对齐位,未使用的两个位
unsigned encoding:4;// 编码方式,也就数据结构
unsigned lru:22;//// LRU 时间(相对于 server.lruclock),当内存紧张,淘汰数据的时候用到
int refcount;// 引用计数
void *ptr;// 数据指针,指向对象的值
}
命令请求处理流程
流程图
详细流程:
- client与server建立连接
readQueryFromClient()函数解析请求命令:该方法会从socket中读取数据放到输入缓冲区querybuf中,接着会调用processInputBuffer()方法按照Redis采用自定义的RESP协议来解析参数。processCommand()函数处理具体的命令:- 解析完参数之后会调用processCommand()方法执行具体的命令。
- 在处理命令请求之前会有很多校验,如:是否是quit命令;命令是否存在;参数数目是否合法;客户端是否认证通过;内存限制校验、集群相关校验、持久化相关校验、主从复制相关校验、发布订阅相关校验及事务操作等
- 根据命令名称找到对应的命令并调用命令的call()完成具体的操作
- addReply()方法返回执行结果:命令在执行完成之后都会调用addReply()方法返回执行结果。
- Redis服务器根据不同的返回类型选择不同的协议格式 ,客户端根据返回结果的第一个字符判断返回类型。
状态回复,第一个字符是"+"; 错误回复,第一个字符是"-"; 整数回复,第一个字符是":"; 批量回复,第一个字符是"$"; 多条批量回复,第一个字符是"*"; - addReply()方法只是把返回的数据写入到输出缓冲区
client->buf或 输出链表client->reply中(通常缓存数据时都会先尝试缓存到buf输出缓冲区,如果失败会再次尝试缓存到reply输出链表),并不执行实际的网络发送操作。
- Redis服务器根据不同的返回类型选择不同的协议格式 ,客户端根据返回结果的第一个字符判断返回类型。
- 发送数据给客户端:实际的网络发送数据操作在beforeSleep()方法中完成。
- beforeSleep()会在handleClientsWithPendingWrites()/handleClientsWithPendingWritesUsingThread()中遍历clients_pending_write链表中每一个客户端节点,并调用writeToClient()方法把输出缓冲区client->buf和client->reply中的数据通过socket发送给客户端。
- 当返回结果数据量非常大时,writeToClient()执行之后,客户端输出缓冲区或者输出链表中可能还有部分数据未发送给客户端。这是就需要 监听当前客户端socket文件描述符的可写事件,当客户端可写时,sendReplyToClient()会发送剩余部分的数据给客户端。
其他人整理的版本:redis之命令请求执行过程
读写键空间的维护操作
- 读取一个键之后,服务器会根据键是否存在来更新服务器的键空间命中 (hit) 次数或键空间不命中 (miss) 次数。还会更新键的 LRU 时间,该值可以用于计算键的闲置时间。
- 如果服务器读取一个键时发现该键已经过期,那么会先删除这个过期键,然后才执行其他操作。
- 服务器每次修改一个键之后,都会对脏键(dirty)计数器的值增 1,这个计数器会触发服务器的持久化以及复制操作。
- 如果有客户端使用 WATCH 命令监视了某个键,那么服务器在对被监视的键进行修改之后,会将这个键标记为脏 (dirty),从而让事务程序注意到这个键已经被修改过。
- 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知。
Redis数据结构
概括
redis为缓存kv数据库,数据库的概念体现在db结构中。redis中的db默认分为0-15共16(conf文件可配置)个,默认使用0号数据库,每个数据库是互不干涉的单线程结构。在cluster模式中,所有节点全部使用db0。
Redis数据类型与数据结构的对应
- 字符串 String:SDS(
Simple Dynamic String)简单动态字符串。 - 列表 List:双向链表&压缩列表(ziplist)。新版本改成 快速列表(V3.2新增结构
qulcklist) - 哈希 Hash:哈希表(
HashTable)&压缩列表。新版本改成 哈希表&紧凑列表(V5新增结构listpack) - 集合 Set:哈希表&整数集合(
IntSet) - 有序集合 Zset:跳表(
skiplist)&压缩列表。版本改成 跳表(skiplist)&紧凑列表(listpack)
SDS(Simple Dynamic String)简单动态字符串
Redis是由C语言实现的,但是其没有用C传统的字符串表示,而是自己构建了一种名为简单动态字符串(Simple Dynamic String) SDS 的抽象类型来最为Redis的默认字符串标识。
3.2以前 结构
struct sdshdr{
//记录buf数组中已使用的字节数----等于SDS保存的字符串长度
unsigned int len;
//记录buf数组中未使用的字节数
unsigned int free;
//保存字符串
char buf[];
}
Len和free都用的int,比较浪费空间。
3.2以后版本 结构
属性有所变化
- len:已使用的字符串长度
- alloc:数组总长度
- flags:SDS类型
- buf[]:字节数组
SDS五种类型
| SDS类型 | C类型 | len&alloc类型 | 长度 | Flags |
|---|---|---|---|---|
| SDS5 | 1<<5 2^5 32 | |||
| SDS8 | char | uint8_t | 1<<8 2^8 256 | |
| SDS16 | short | Uint16_t | 1<<16 2^16 65535 | |
| SDS32 | int | Uint32_t | 1<<32 2^32 | |
| SDS64 | long | Uint64_t | 1<<64 2^64 |
flags解释
是char类型,一个字节,8位。定义如下
- define SDS_TYPE_5 0
- define SDS_TYPE_8 1
- define SDS_TYPE_16 2
- define SDS_TYPE_32 3
- define SDS_TYPE_64 4
由于一共有5种类型的结构,所以至少要3位来区分(2^ 3=8>5)
结构解释
SDS5
struct _attribute_((_paked_)) sdshdr5{
unsigned char flags;//低三位表示类型;高五位表示字符串长度
char bug[];
}
对于长度小于32位的字符串,redis设计的数据结构中为了节省存储空间,并没有设置len和alloc字段。 flags剩下的5位(2^5=32)用来表示字符串的长度。
SDS8、16、32、64
N对应SDS类型8,16,32,64
struct _attribute_((_paked_)) sdshdrN{
uintN_t len;//已使用长度
uintN_t alloc;//数组总长度
unsigned char flags;//第三位表示类型;高五位未使用(预留空间)
char bug[];
}
扩容
当SDS中剩余空间(avail)小于或等于新增内容长度(addlen)时:
- 新增后总长度(len+addlen)<1M:按新长度两倍扩容。
- 新增后总长度(len+addlen)>1M:按新长度加1M扩容。
新长度:len + addlen后的长度。
空间预分配策略
由于预先分配了更多的空间,如果再加入数据,还能放得下,就不用再做扩容操作,从而减少空间分配次数提高性能。
惰性空间释放
如果SDS内容变少了,不会马上回收多余空间,只会更新len长度。调用sdsRemoveFreeSpace()方法会回收SDS字符串的空白空间,将free设置为0或更新alloc(取决于版本)
与C字符串对比
- C字符串二进制不安全。C使用N+1长度的字符串数组表示N长度的字符串,最后一个元素总是空字串‘\0’。(C语言\0表示空字串),如果存二进制,到0会被截断;SDS二进制安全,根据len取字符串
- C时间复杂度为O(n)。字符串不记录自身长度。获取长度需要遍历整个字符串,直到遇到‘\0’;SDS时间复杂度为O(1),len属性记录了字串长度。
- C会出现缓冲区溢出导致覆盖内存里相邻的内容;SDS会自动扩容
- SDS减少修改字符串时带来的内存重分配次数
- SDS兼容部分C字符串函数