文章目录
1. 基本对象
- 字符串(int,embstr, sds)
- 列表(现在是quicklist,早期也就是书里是ziplist, linkedlist)
- 哈希表(ziplist, hashtable)
- 集合(intset, hashtable)
- 有序集合(ziplist, skiplist)
- HyperLogLog(存储用sds)
2. 底层数据结构
- sds:简单动态字符串(字符串,HyperLogLog)
- int:整数(字符串)
- embstr(字符串)
- ziplist:压缩列表(列表,哈希表,有序集合)
- linkedlist:链表(列表)
- hashtable:字典(哈希表,集合)
- intset:整数集合(集合)
- skiplist:跳跃表(有序集合)
3. 对象
- 对象系统实现了基于引用计数的内存回收机制,并基于引用计数实现了对象共享。
- 对象带有访问时间记录信息,用于计算键的空转时长。在服务器启动maxmemory功能的情况下,空转时间长的键优先删除
Redis使用对象来表示数据库中的键和值,新建键值对时,至少会创建2个对象(键和值对象)。每个对象都由redisObject结构体表示。
键总是一个字符串对象
3.1. 字符串对象
编码可以是:
- int(即整数)
- embstr
- raw(即sds)
对于浮点数值,其保存方式是字符串,每次对浮点数操作时,都要转换。
3.1.1. int
如果一个字符串对象保存的是整数值,且可以用long类型来表示时使用。
会保存在对象结构的ptr属性里(将void*转成long),并将编码设为int
3.1.2. embstr
若保存的是一个字符串值,且长度小于32字节,将用embstr编码来保存。
3.1.3. raw
若保存的是一个字符串值,且长度超过32字节(32bit),则用SDS保存字符串值。
int和embstr编码的字符串条件满足的情况下会转换成raw(如append)。
因为redis没有为embstr编写任何相应的修改程序,所以每次对embstr修改都会转换成raw编码。
3.2. 列表对象
(现在显示的是quicklist,不会显示ziplist,版本变化)
编码可以是
- ziplist
- linkedlist(链表)
现在直接是quicklist,quicklist就相当于是块状链表,一个quicklist里面就像链表一样连着多个ziplist。
3.3. 哈希对象
编码可以是:
- ziplist
- hashtable(字典)
使用ziplist编码的条件:
- 所有键值对的键和值的字符串长度都小于64字节
- 键值对数量小于512个
不满足就转换成hashtable
3.4. 集合对象
编码可以是:
- intset
- hashtable(字典)
当使用hashtable时,每个键的值都被设置为null
使用intset编码的条件:
- 集合中都是整数值
- 元素数量不超过512个
3.5. 有序集合对象
编码可以为:
- ziplist
- skiplist
使用ziplist编码条件:
- 元素数量小于128个
- 所有元素成员长度都小于64字节
3.6. 对象共享
仅对包含整数值的字符串共享。
Redis会在初始化服务器时,创建一万个字符串对象,这些对象包含从0到9999的所有整数值,当服务器需要用到值为0到9999的字符创对象时,会共享这些对象。
4. SDS (simple dynamic string)
简单动态字符串
//链表节点
struct sdshdr {
int len;//记录所保存字符串(字节的形式)的长度,末尾的`\0`不会计算
int free;//保存buf中未使用的字节数
char buf[];//用于保存的字节数组
}
- 二进制安全,所以可以保存任意二进制数据
- 末尾必然添加
'\0'
字符,这样跟C库兼容。
5. linkedlist
//链表节点
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void * value;
}
//链表结构
typedef struct list {
listNode *head;
listNode *tail;
unsigned long len;
void *(*dup)(void *ptr);
void (*free)(void *ptr);
int (*match)(void *ptr, void *key);
}
特点:
- 双端
- 无环
- 带有head和tail
- 有链表长度计数器
- 多态(链表节点使用void*保存value),可保存不同类型数据
6. hashtable
渐进式rehash
7. intset
typedef struct intset {
uint32_t encoding;//元素(占用字节)类型
uint32_t length;//元素数量
int8_t contents[];//保存元素的(字节)数组
} intset;
是集合键的实现之一,当一个集合只包含整数元素,且元素不多时,就会使用此实现。(较多时使用hashtable),实际是用数组实现,里面从小到大排列
7.1. 升级
当新类型比整数集合现有元素类型长时,会进行元素类型升级,无论怎样的情况都不会进行降级。
8. 压缩列表(ziplist)
列表键和哈希键的底层实现之一。
- 当列表包含少量列表项时使用。
- 当hashtable包含少量键值对时使用。
- 可能产生O(n^2)连锁更新
- 其每个节点可以保存一个字节数组或整数值
9. 数据库
/* Redis database representation. There are multiple databases identified
* by integers from 0 (the default database) up to the max configured
* database. The database number is the 'id' field in the structure. */
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
过期时间的设置:expire, pexpire,expireat,pexpireat底层都是pexpireat实现
expire字典保存了数据库中所有键的过期时间。
数据库主要由:
- dict(键值对字典)
- expires(键过期时间字典)
- watched_keys(watch的字典)
9.1. 过期键的删除策略
- 定时删除
- 惰性删除
- 定期删除
Redis本身使用的是惰性删除和定期删除策略
9.1.1. 定时删除
内存友好
设置键过期的同时,创建一个定时器,在键过期时立即删除
9.1.2. 惰性删除
CPU友好
放任键过期不管,获取键时,检查是否过期。过期则删除,否则返回该键
9.1.3. 定期删除
折中
隔一段时间,检查并删除过期键。检查多少数据库和删除多少键由算法决定。
10. RDB持久化
生成RDB文件时过期键不会被保存。
RDB文件是一个经过压缩的二进制文件。通过save和bgsave命令创建
11. AOF(Append Of File)持久化
过期键不会被保存。
AOF文件载入时,会根据AOF文件创建一个伪客户端然后一条条执行指令。
appendfsync选项:
- always:每次事件循环都刷盘,最慢
- everysec:每隔一秒刷入磁盘
- no:保存时有操作系统决定何时刷入磁盘
11.1. AOF文件重写
利用bgaofrewrite
为解决AOP文件体积膨胀问题,可以创建一个新的AOF文件替代现有AOF文件,同时保存的数据库状态相同。
实际上并不需要对现有AOF文件分析,只要根据现有数据库状态导出即可。
12. RDB 和 AOF 混合持久化
将 RDB 的文件和局部增量的 AOF 文件相结合。RDB 可以使用相隔较长的时间保存策略,AOF 不需要是全量日志,只需要保存前一次 RDB 存储开始到这段时间增量 AOF 日志即可
13. 事件
Redis是事件驱动程序,需处理以下2类事件:
- 文件事件
- 时间事件
Redis基于Reactor模式开发了自己的网络事件处理器。采用IO多路复用的方式来同时监听多个套接字。
14. 服务器
serverCron函数默认每100毫秒执行一次,工作包括:
- 更新服务器状态信息
- 处理服务器接收的SIGTERM信号
- 管理客户端资源和数据库状态
- 检查并执行持久化操作
服务器的启动:
- 初始化服务器的状态
- 载入服务器配置
- 初始化服务器数据结构
- 还原数据库状态
- 执行事件循环
15. 复制
新版复制功能采用psync来执行复制时的同步操作,psync具有完整重同步和部分重同步2种模式。
15.1. 部分重同步的实现
由以下3个部分构成:
- 主从服务器的复制偏移量
- 主服务器的复制积压缓冲区
- 服务器的运行ID
利用复制偏移量来判断主从是否不一致。
利用复制积压缓冲区(默认1MB)来实现部分重同步,如果偏移量不在复制积压缓冲区,则要进行完全重同步。
利用运行ID判断是否是原来的主服务器
16. Sentinel
- 使用了和普通模式不同的命令表,所以与普通的Redis服务器能够使用的命令不同
- Sentinel给主服务器发送INFO命令来获取从服务器信息
- 一般情况下以10秒一次的频率向主服务器和从服务器发送INFO命令,主服务器下线或故障转移时每秒1次
- Sentinel之间会以每2秒一次的频率通过__sentinel__:hello频道宣告自己的存在
- 当Sentinel收集到足够多的主观下线投票后,会将主服务器判断为客观下线,并发起一次针对主服务器的故障转移操作
- Sentinel选举领头Sentinel的方法是利用Raft算法。由领头Sentinel执行故障转移操作
17. 集群
集群通过分片(sharding)进行数据共享,并提供复制和故障转移功能。
根据cluster-enabled配置是否为yes决定是否开启服务器的集群模式。
集群的整个数据库被分成16384(二进制:11111111111111)个槽,数据库中的每个键都属于这16384中的一个。当每个槽都有节点在处理时,集群处于上线状态(OK),反之下线状态(fail)
利用char数组中为0或1来判断槽被那个节点处理(桶排序思想)
利用CRC16(key) & 16383
来计算给定key属于哪个槽
利用redis-trib进行分片,将属于某个槽的所有键值对从一个节点转移到另一个节点
集群里从节点用于复制主节点,主节点下线时,选出一个从节点代替主节点继续处理请求。
集群中的节点通过发送和接收消息来进行通信,常见消息包括MEET,PING,PONG,PUBLISH,FAIL五种。
17.1. ASK 错误与 MOVED错误的区别
都会让客户端转向
区别在于:
- MOVED代表槽的负责权从一个节点转移到另一个节点。今后就关于这个槽的处理都发往另一个节点
- ASK错误是迁移槽时的一种临时措施,代表本节点无法处理,该找另一个节点处理(因为可能迁移往另一个节点了)。今后依然会将这个槽的处理发往这个节点
18. 事务
利用multi、exec、watch来实现。
18.1. 实现
- 事务开始(multi)
- 事务入队
- 事务执行(exec)
18.2. ACID属性
不支持事务回滚机制,即使执行出错,后续指令继续执行。
- 原子性:不符合。具备了一定的原子性,但不支持回滚
- 一致性:符合
- 隔离性:符合
- 持久性:并不完全符合
18.3. watch 的实现
所有对数据库进行修改的命令,执行后都会利用multi.c/touchWatchKey
函数对watched_keys字典进行检查,看是否有客户端正在监视刚刚被命令修改的键,如果有的话,将被修改键的客户端的redis_dirty_cas标识打开,表示客户端的事务安全性已被破坏。
watch命令实际实现了一个乐观锁
19. 位数组
使用字符串对象表示位数组,即利用SDS保存位数组
20. 慢查询日志
- slow-log-slower-than选项指定执行时间超过多少微妙的命令请求会被记录到日志上。
- slow-log-max-len指定服务器上最多保存多少条慢查询日志。
- 慢查询日志类似队列,达到最大长度时,最旧的被删除
- 打印和删除慢查询日志可以通过遍历slowlog链表完成
21. 监视器(Monitor)
通过执行monitor指令,客户端可以实时地接收并打印出服务器当前处理的命令请求相关信息。