Redis 设计与实现读书笔记
一、简单动态字符串 SDS
- 常数复杂度获取字符串长度
- 减少修改字符串时内存重新分配的次数
- 空间预分配
- 惰性空间释放
- 二进制安全(通过 len 字段读出来所有数据,不会对数据做任何处理,写的时候是什么样子,读的时候就是什么样子)
- 兼容 C 语言的字符串函数
比原始的 C 字符串操作更安全便捷
struct sdshdr {
// 记录 buf 数组中已使用字节的数量
// 等于 SDS 所保存字符串的长度
int len;
// 记录 buf 数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
};
二、双向链表 List
应用于:列表键、慢查询、监视器等
三、字典 Hash
应用于:字典、数据库 redisDb 结构等
- 根据负载因子决定是否扩容(负载因子=总键值对数/箱子个数)
Rehash 简单总结
/* 字典结构定义 */
typedef struct dict {
dictType *type; // 字典类型
void *privdata; // 私有数据
dictht ht[2]; // 哈希表[两个][为了后续字典的扩展作Rehash之用]
long rehashidx; // 记录rehash 进度的标志,值为-1表示rehash未进行
int iterators; // 当前正在迭代的迭代器数
} dict;
/* Hash 表数据结构 */
typedef struct dictht {
dictEntry **table; // 哈希表数组
unsigned long size; // 哈希表的大小
unsigned long sizemask; // 哈希表大小掩码
unsigned long used; // 哈希表现有节点的数量
} dictht;
/* 哈希桶数据结构 */
typedef struct dictEntry {
void *key; // 键定义
// 值定义
union {
void *val; // 自定义类型
uint64_t u64; // 无符号整形
int64_t s64; // 有符号整形
double d; // 浮点型
} v;
struct dictEntry *next; //指向下一个哈希表节点
} dictEntry;
四、跳跃表
用于:有序集合
跳表具有如下性质:
(1) 由很多层结构组成
(2) 每一层都是一个有序的链表
(3) 最底层(Level 1)的链表包含所有元素
(4) 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现
(5) 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素
(6) 通过一个随机函数,来决定将这个结点插入到哪几级索引中
五、整数集合
集合键的底层实现,当集合只包含整数值元素,且数量不多的时候使用
typedef struct intset { unit32_t encoding; //编码方式 int16_t int32_t 或者 int64_t unit32_t length; //集合包含的元素数量 int8_t contents[]; //重点:保存元素的数数组(数字真正的类型取决于 encoding 属性) }intset;
升级操作(不支持降级):
- 触发条件:当添加一个新的数据超出了当前编码类型的长度时
- 操作:扩容 + 将现有数据转化到其他的位置 + 添加新元素到末尾
- 优势:灵活、节省内存
六、压缩列表
用于实现:列表和字典类型
应用:hash、list、zset 容器对象中,在元素个数较少的时候,会使用ziplist进行存储
遍历:通过 zltail 获取到队尾节点,之后根据偏移量获取上一个节点
更新:增加元素可能造成拓展内存或者重新分配内存
struct ziplist
它有点儿类似数组,通过一片连续的内存空间,来存储数据。不过,它跟数组不同之处在于:
- 允许存储的数据大小不同
- 可以存储不同类型的数据
我们在遍历节点的之后就知道每个节点的长度(占用内存的大小),就可以很容易计算出下一个节点再内存中的位置。这种结构就像一个简单的压缩列表了。
七、Redis 对象
Redis的每种对象其实都由对象结构(redisObject) 与 对应编码的数据结构组合而成
redisObject 是 Redis 类型系统的核心, 数据库中的每个键、值, 以及 Redis 本身处理的参数
- 使用引用计数进行内存回收
- 使用对象共享节省内存
typedef struct redisObject {
unsigned type:4; // 类型
unsigned encoding:4; // 编码方式
unsigned lru:LRU_BITS; // LRU - 24位, 记录最末一次访问时间(相对于lru_clock); 或者 LFU(最少使用的数据:8位频率,16位访问时间)
int refcount; //引用计数
void *ptr; //指向底层数据结构实例
} robj;
八、Redis DB结构
Redis中存在“数据库”的概念,该结构由redis.h中的redisDb定义。
- 当Redis 服务器初始化时,会预先分配 16 个数据库,所有数据库保存到结构 redisServer 的一个成员 redisServer.db 数组中
- redisClient中存在一个名叫db的指针指向当前使用的数据库
typedef struct redisDb {
int id; //id是数据库序号,为0-15(默认Redis有16个数据库)
long avg_ttl; //存储的数据库对象的平均ttl(time to live),用于统计
dict *dict; //存储数据库所有的key-value(重要)
dict *expires; //存储key的过期时间(重要)
dict *blocking_keys;//blpop 存储阻塞key和客户端对象
dict *ready_keys;//阻塞后push 响应阻塞客户端 存储阻塞后push的key和客户端对象
dict *watched_keys;//存储watch监控的的key和客户端对象
} redisDb;
Redis 过期键的删除策略
上面 redisDb 结构中的 expires 字典保存了数据库中所有键的过期时间,redis 使用下面两种方式删除过期数据
- 惰性删除,碰到过期键的时候才进行删除(CPU 友好型)
- 定期删除:每隔一段时间主动查找并删除一定数量过期 key (内存友好型)
九、事务
将多条命令请求打包,然后一次性、按顺序地执行多个命令的机制(服务器不会中断事务而去执行其他客户端的命令请求)
事务执行过程
- 客户端连接执行 multi 命令进入一个事务上下文。
- 接受命令,把命令存储进队列中。
- 接受到 exec 指令执行队列命令,接受到 discard 指令清空队列并推出事务上下文。
十、数据持久化
内存快照 RDB持久化
把内存中的数据以快照的方式写入二进制文件中,默认文件为 dump.rdb 。
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。
日志追加 aof
把增加、修改数据的命令通过 write 函数追加到文件尾部(默认为 appendonly.aof ),Redis重启时读取文件把数据写入内存。
重写机制:当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof。Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发
十一、Redis 集群常用集群方案
- 主从模式
- 需要人工干预把从机改为主机
- 哨兵模式
- 每个主机都存了全量的数据,只有主机接受写操作
- redis代理分片用得最多的就是Twemproxy,由Twitter开源的Redis代理
- 无法平滑地扩容/缩容
- 每个请求都经过Twemproxy代理才能到达Redis服务器
- Redis Cluster(3.0 上)
- 完全去中心化,采用多主多从