diff --git a/工作日志/2021年8月31日-九月份计划.md b/工作日志/2021年8月31日-九月份计划.md index 32410535..9063940d 100644 --- a/工作日志/2021年8月31日-九月份计划.md +++ b/工作日志/2021年8月31日-九月份计划.md @@ -50,10 +50,13 @@ ### 腾讯 - +* 岗位 + * 智能车(被捞起来了) + * 微信支付(投了微信支付,但应该会走完流程 * 流程 - * [ ] fuck 居然杯智能车捞起来了,着实,不太行。 + * [x] 居然智能车捞起来了,着实,不太行。 + * [ ] 腾讯笔试:09-05 20:00:00 -- 22:00:00https://qq.nowcoder.com/cts/17036433/summary?id=B2EAF4DC35F7F779 ### 百度 @@ -72,6 +75,7 @@ * 流程 * [x] 建立投递https://talent.baidu.com/external/baidu/campus.html#/individualCenter * [x] 百度一面2021-08-30 14:00-15:00:https://code.meideng.net/ykl1 + * [x] 建立共享当中,应该是面试挂掉了 @@ -109,7 +113,8 @@ * [x] 官网投递https://campus.meituan.com/apply-record * [x] 美团笔试2021-08-29 10:00 星期天 * [x] 美团一面:北京时间08月31日 19:00,面试类型:视频面试,面试地址:http://dpurl.cn/0wnxIGBz - * [ ] 美团二面:北京时间09月02日 14:00,http://dpurl.cn/4knwRG4z + * [x] 美团二面:北京时间09月02日 14:00,http://dpurl.cn/4knwRG4z + * [ ] 美团三面:北京时间09月06日 10:30。电话面试 ### 网易 * 岗位: diff --git a/数据库/Redis/01 Redis概述.md b/数据库/Redis/01 Redis概述.md index cf98cd0a..3da3eb71 100644 --- a/数据库/Redis/01 Redis概述.md +++ b/数据库/Redis/01 Redis概述.md @@ -1,10 +1,31 @@ # Redis概述 -## 概述 +## 1 概述 Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。 键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。 -Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。 \ No newline at end of file +Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。 + + + +## 2 Redis 与 Memcached + +两者都是非关系型内存键值数据库,主要有以下不同: + +* 数据类型。 + * Memcached 仅支持字符串类型, + * Redis 支持五种不同的数据类型,可以更灵活地解决问题。 +* 数据持久化。 + * Redis 支持两种持久化策略:RDB 快照和 AOF 日志 + * Memcached 不支持持久化。 +* 分布式。 + * Memcached 不支持分布式,只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。 + * Redis Cluster 实现了分布式的支持。 + +* 内存管理机制 + - 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘,而 Memcached 的数据则会一直在内存中。 + + - Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题。但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。 diff --git a/数据库/Redis/08 对象.md b/数据库/Redis/02 对象类型.md similarity index 67% rename from 数据库/Redis/08 对象.md rename to 数据库/Redis/02 对象类型.md index fbbf85c5..eab4c0f3 100644 --- a/数据库/Redis/08 对象.md +++ b/数据库/Redis/02 对象类型.md @@ -1,4 +1,66 @@ -Redis并没有使用SDS、双端链表、字典、压缩列表、整数集合来实现键值对数据库,而是基于这些数据结构创建了一个对象系统。这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象。 +# 数据类型 + +- [数据类型](#数据类型) + - [0 数据类型](#0-数据类型) + - [对象说明](#对象说明) + - [对象代码实现](#对象代码实现) + - [对象编码encoding](#对象编码encoding) + - [对象类型type](#对象类型type) + - [1 字符串对象STRING](#1-字符串对象string) + - [字符串实现](#字符串实现) + - [编码的转换](#编码的转换) + - [字符串命令](#字符串命令) + - [2 列表对象LIST](#2-列表对象list) + - [列表实现](#列表实现) + - [编码转换](#编码转换) + - [列表命令](#列表命令) + - [3 哈希对象HASH](#3-哈希对象hash) + - [哈希实现](#哈希实现) + - [编码转换](#编码转换-1) + - [哈希命令](#哈希命令) + - [4 集合对象SET](#4-集合对象set) + - [集合实现](#集合实现) + - [编码的转换](#编码的转换-1) + - [集合命令](#集合命令) + - [5 有序集合的对象ZSET](#5-有序集合的对象zset) + - [编码的转换](#编码的转换-2) + - [有序集合命令](#有序集合命令) + - [6 类型检查与命令多态](#6-类型检查与命令多态) + - [7 内存回收](#7-内存回收) + - [8 对象共享](#8-对象共享) + - [9 对象的空转时长](#9-对象的空转时长) + + +## 0 数据类型 + +关于数据类型的说明。从C++标准库的角度来说,实现的数据结构主要可以如下方式进行划分 +* 线性数据结构 + * vector(数组) + * list(链表) + * deque(双端队列) +* 非线性数据结构 + * set(集合) + * set红黑树实现的有序set + * unordered_set哈希实现的无序的set + * map(映射map、字典dict、哈希hash) + * map红黑树实现的有序map + * unordered_set哈希实现的无序的map +* 扩展数据结构 + * stack(栈) + * queue(队列) + * priority_queue(优先队列、堆) + + +这里实现的数据类型,在底层的数据结构,抽象出来的新的数据结构。作为初学者,不需要了解底层数据类型转换,只需要建立起唯一的常见的对应关系即可。 +* 字符串对象——SDS动态字符串 +* 链表对象——双向链表 +* 集合对象——字典dict(只有键。键的哈希函数值) +* 哈希对象——字典dict(键值对。键的哈希函数值,哈希函数值对应的对象) +* 有序链表——跳表 + +### 对象说明 + +Redis并没有使用SDS、双端链表、字典、跳表、压缩列表、整数集合来实现键值对数据库,而是基于这些数据结构创建了一个对象系统。这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象。 通过这五种类型的对象,Redis可以在执行命令之前,根据对象的类型判断一个对象是否执行给定的命令。使用对象的好处是,可以针对不同的场景,为对象设置多种不同的数据结构的实现,从而优化使用效率。 @@ -6,7 +68,8 @@ Redis并没有使用SDS、双端链表、字典、压缩列表、整数集合来 最后,Redis的对象带有访问时间记录信息,空转时长较大的键可能被优先删除。 -# 8.1 对象的类型和编码 + +### 对象代码实现 Redis使用对象来表示数据库中的键和值。创建一个新键值对时,至少会创建两个对象,一个对象用作键,一个对象用作值。每个对象都由一个redisObject结构表示: @@ -19,23 +82,7 @@ typedef struct redisObject { } robj; ``` -## 类型 - -对象的type记录了对象的类型,它的值可以使 - -| type常量 | 对象的名称 | -| ------------- | ------ | -| REDIS\_STRING | 字符串对象 | -| REDIS\_LIST | 列表对象 | -| REDIS\_HASH | 哈希对象 | -| REDIS\_SET | 集合对象 | -| REDIS\_ZSET | 有序集合对象 | - -键总是一个字符串对象,值可以是字符串对象、列表对象、哈希对象、集合对象、有序集合对象。 - -但数据库执行TYPE命令时,返回的结果为数据库键对应的值对象的类型,而不是键对象的类型。 - -## 编码和底层实现 +### 对象编码encoding 对象的ptr指向对象的底层实现数据结构,而这些数据结构由对象的encoding决定,它可以是: @@ -50,17 +97,34 @@ typedef struct redisObject { | REDIS\_ENCODING\_INTSET | 整数集合 | | REDIS\_ENCODING\_SKIPLIST | 跳跃表和字典 | -每种类型的对象至少使用了两种编码。 +* 每种类型的对象至少使用了两种编码。 +* 使用OBJECT ENCODING命令可以查看一个数据库键的值对象的编码。 -使用OBJECT ENCODING命令可以查看一个数据库键的值对象的编码。 -# 8.2 字符串对象 +### 对象类型type -字符串对象的编码可以使int、raw或embstr。 +| 对象类型type | 可以存储的值 | 操作 | +| :--: | :--: | -- | +| REDIS_STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作
对整数和浮点数执行自增或者自减操作 | +| REDIS_LIST | 有序列表 | 从两端压入或者弹出元素
对单个或者多个元素进行修剪,
只保留一个范围内的元素 | +| REDIS_SET | 无序集合 | 添加、获取、移除单个元素
检查一个元素是否存在于集合中
计算交集、并集、差集
从集合里面随机获取元素 | +| REDIS_HASH | 无序字典 | 添加、获取、移除单个键值对
获取所有键值对
检查某个键是否存在| +| REDIS_ZSET | 有序集合 | 添加、获取、删除元素
根据分值范围或者成员来获取元素
计算一个键的排名 | -1. 如果字符串对象保存的是整数值,且可以用long类型表示,那么字符串对象会将整数值保存在ptr中(将void* 转换成 long),并将编码设置为int。 -2. 如果字符串对象保存到是一个字符串值,且长度大于32字节,那么字符串对象使用SDS来保存这个字符串值,并将编码设置为raw。 -3. 如果字符串对象保存到是一个字符串值,且长度小于等于32字节,那么字符串对象使用embstr编码的方式来存储这个字符串值。 +* 对象的键总是一个字符串对象,值可以是字符串对象、列表对象、哈希对象、集合对象、有序集合对象。 +* 但数据库执行TYPE命令时,返回的结果为数据库键对应的值对象的类型,而不是键对象的类型。 + + +## 1 字符串对象STRING + +### 字符串实现 +![](image/2021-09-02-16-24-45.png) + +字符串对象的编码可以是**int、raw或embstr**。 + +1. 如果字符串对象保存的是整数值,且可以用long类型表示,那么字符串对象会将整数值保存在ptr中(将void* 转换成 long),并将编码设置为**int**。 +2. 如果字符串对象保存到是一个字符串值,且长度大于32字节,那么字符串对象使用**SDS**来保存这个字符串值,并将编码设置为raw。 +3. 如果字符串对象保存到是一个字符串值,且长度小于等于32字节,那么字符串对象使用**embstr编码**的方式来存储这个字符串值。 embstr编码是专门用来保存短字符串的优化方式。和raw编码一样,都是用redisObject结构和sdshdr结构来表示字符串对象,但raw会调用两次内存分配函数分别创建redisObject结构和sdshdr结构,而embstr则通过一次内存分配一块连续空间,依次包含两个结构: @@ -76,21 +140,33 @@ embstr的好处: PS. 用`long double`类型表示的浮点数在Redis中也是作为字符串值存储的。程序会先将浮点数转成字符串值,然后再保存转换的字符串值。 -## 编码的转换 +### 编码的转换 int编码和embstr编码的字符串对象可以被转换为raw编码的字符串对象。 1. 对int编码的字符串对象执行一些命令,可使其不再是整数值,而是字符串值,那么编码也就变为raw了。如APPEND。 2. 对embstr编码的字符串,执行修改命令,也会变成raw对象。如APPEND。 -## 字符串命令的实现 +### 字符串命令 用于字符串键的所有命令都是针对字符串对象来构建的。 -| 命令 | int编码的实现方法 | embstr编码的实现方法 | raw编码的实现方法 | -| ---------- | ---------------------------------------- | ---------------------------------------- | ---------------------------------------- | -| SET | int编码保存值 | embstr编码保存值 | raw编码保存值 | -| GET | 拷贝对象所保存的整数值,将这个拷贝转换为字符串值,然后向客户端返回这个字符串值 | 直接向客户端返回字符串值 | 直接向客户端返回字符串值 | + +```html +> set hello world +OK +> get hello +"world" +> del hello +(integer) 1 +> get hello +(nil) +``` + +| 命令 | int编码的实现方法| embstr编码的实现方法| raw编码的实现方法| +| ---------- | --------|--------- | ---------- | +| SET | int编码保存值 | embstr编码保存值 | raw编码保存值 | +| GET | 拷贝对象所保存的整数值,将这个拷贝转换为字符串值,然后向客户端返回这个字符串值 | 直接向客户端返回字符串值 | 直接向客户端返回字符串值 | | APPEND | 将对象转换为raw编码,然后按raw方式执行此操作 | 将对象转换为raw编码,然后按raw方式执行此操作 | 调用sdscatlen函数,将给定字符串追加到现有字符串的末尾 | | INCBYFLOAT | 取出整数值并将其转换为long double的浮点数,对这个浮点数进行加法计算,然后将结果保存起来 | 取出整数值并将其转换为long double的浮点数,对这个浮点数进行加法计算,然后将结果保存起来。如果字符串值不能被转换为浮点数,那么客户端会报错 | 取出整数值并将其转换为long double的浮点数,对这个浮点数进行加法计算,然后将结果保存起来。如果字符串值不能被转换为浮点数,那么客户端会报错 | | INCBY | 对整数值进行加法计算,得出的结果作为整数被保存起来 | 不能执行此命令,客户端报错 | 不能执行此命令,客户端报错 | @@ -99,13 +175,17 @@ int编码和embstr编码的字符串对象可以被转换为raw编码的字符 | SETRANGE | 将对象转换为raw编码,然后按raw方式执行此命令 | 将对象转换为raw编码,然后按raw方式执行此命令 | 将字符串特定索引上的值设置为给定的字符 | | GETRANGE | 拷贝对象保存的整数值,将这个拷贝转换为字符串,然后取出返回字符串指定索引上的字符 | 直接取出并返回给定索引上的字符 | 直接取出并返回给定索引上的字符 | -# 8.3 列表对象 +## 2 列表对象LIST -列表对象的编码是ziplist或linkedlist。 +### 列表实现 +![](image/2021-09-02-23-00-43.png) +列表对象的编码是**ziplist或linkedlist**。 -使用ziplist时,每个压缩列表的节点保存了一个列表元素。使用linkedlist时,每个链表节点保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。(字符串对象是Redis五种类型的对象中唯一一种会被嵌套的对象。) +使用ziplist时,每个压缩列表的节点保存了一个列表元素。使用linkedlist时,每个链表节点保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。 -## 编码转换 +**字符串对象是Redis五种类型的对象中唯一一种会被嵌套的对象**。注意其他类型不会被嵌套。也就是说,字典中不可能包含链表,链表中的元素不可能是字典!!! + +### 编码转换 当列表对象同时满足以下两个条件时,使用ziplist编码: @@ -116,7 +196,33 @@ int编码和embstr编码的字符串对象可以被转换为raw编码的字符 > 以上两个条件的上限可以修改,使用list-max-ziplist-value选项和list-max-ziplist-entries选项。 -## 列表命令的实现 +### 列表命令 + + +```html +> rpush list-key item +(integer) 1 +> rpush list-key item2 +(integer) 2 +> rpush list-key item +(integer) 3 + +> lrange list-key 0 -1 +1) "item" +2) "item2" +3) "item" + +> lindex list-key 1 +"item2" + +> lpop list-key +"item" + +> lrange list-key 0 -1 +1) "item2" +2) "item" +``` + | 命令 | ziplist编码的实现 | linkedlist编码的实现 | | ------- | ---------------------------------------- | ---------------------------------------- | @@ -131,7 +237,11 @@ int编码和embstr编码的字符串对象可以被转换为raw编码的字符 | LTRIM | 调用ziplistDeleteRange函数删除不再指定索引范围内的节点 | 遍历节点,调用listDelNode | | LSET | 调用ziplistDelete,先删除给定索引上的节点,然后调用ziplistInsert插入新节点 | 调用listIndex函数,定位给定索引上的节点,然后通过赋值操作更新节点的值 | -# 8.4 哈希对象 +## 3 哈希对象HASH + +### 哈希实现 + +![](image/2021-09-02-23-16-42.png) 哈希对象的编码可以是ziplist或hashtable。 @@ -142,14 +252,14 @@ int编码和embstr编码的字符串对象可以被转换为raw编码的字符 ![](img/chap8/img0.png) -使用hashtable时,哈希对象中的每个键值对都使用一个字典键值对来保存: +使用hashtable时,哈希对象中的每个**键值对都使用一个字典键值对**来保存: - 字典的每个键都是一个字符串对象,对象中保存了键值对的键。 -- 字典的每个值都是一个字符串独显,对象中保存了键值对的值。 +- 字典的每个值都是一个字符串对象,对象中保存了键值对的值。 ![](img/chap8/img1.png) -## 编码转换 +### 编码转换 当哈希对象同时满足以下两个条件时,使用ziplist编码: @@ -160,7 +270,35 @@ int编码和embstr编码的字符串对象可以被转换为raw编码的字符 > 以上两个条件的上限可以修改,使用hash-max-ziplist-value选项和hah-max-ziplist-entries选项。 -## 哈希命令的实现 +### 哈希命令 + + +```html +> hset hash-key sub-key1 value1 +(integer) 1 +> hset hash-key sub-key2 value2 +(integer) 1 +> hset hash-key sub-key1 value1 +(integer) 0 + +> hgetall hash-key +1) "sub-key1" +2) "value1" +3) "sub-key2" +4) "value2" + +> hdel hash-key sub-key2 +(integer) 1 +> hdel hash-key sub-key2 +(integer) 0 + +> hget hash-key sub-key1 +"value1" + +> hgetall hash-key +1) "sub-key1" +2) "value1" +``` | 命令 | ziplist编码的实现 | hashtable编码的实现 | | ------- | ---------------------------------------- | -------------------------------- | @@ -171,9 +309,11 @@ int编码和embstr编码的字符串对象可以被转换为raw编码的字符 | HLEN | ziplistLen,然后除以2 | dictSize | | HGETALL | 遍历ziplist,ziplistGet返回所有的键和值 | 遍历字典,dictGetKey返回键,dictGetVal返回值 | -# 8.5 集合对象 +## 4 集合对象SET - 集合对象的编码可以使intset或hashtable。 +### 集合实现 +![](image/2021-09-02-23-16-20.png) +集合对象的编码可以使intset或hashtable。 1. inset编码,集合对象的所有元素都被保存在整数集合中。 @@ -183,7 +323,7 @@ int编码和embstr编码的字符串对象可以被转换为raw编码的字符 ![](img/chap8/img3.png) -## 编码的转换 +### 编码的转换 当集合对象同时满足一下两个条件时,使用inset编码: @@ -192,7 +332,37 @@ int编码和embstr编码的字符串对象可以被转换为raw编码的字符 > 第二个的上限修改,查看set-max-intset-entries选项。 -## 集合命令的实现 +### 集合命令 + +```html +> sadd set-key item +(integer) 1 +> sadd set-key item2 +(integer) 1 +> sadd set-key item3 +(integer) 1 +> sadd set-key item +(integer) 0 + +> smembers set-key +1) "item" +2) "item2" +3) "item3" + +> sismember set-key item4 +(integer) 0 +> sismember set-key item +(integer) 1 + +> srem set-key item2 +(integer) 1 +> srem set-key item2 +(integer) 0 + +> smembers set-key +1) "item" +2) "item3" +``` | 命令 | intset编码的实现 | hashtable编码的实现 | | ----------- | --------------------------- | ----------------------------- | @@ -204,14 +374,14 @@ int编码和embstr编码的字符串对象可以被转换为raw编码的字符 | SPOP | intsetRandom,然后intsetRemove | dictGetRandomKey,然后dictDelete | | SREM | intsetRemove | dictDelete | -# 8.6 有序集合的对象 - +## 5 有序集合的对象ZSET +![](image/2021-09-02-23-17-35.png) 有序集合的编码是ziplist或skiplist。 1. ziplist编码:每个集合元素使用两个紧挨在一起的ziplist节点来存储。第一个节点保存元素的成员(member),第二元素保存元素的分值(score)。元素按分值的从小到大排序。 2. skiplist编码:一个zset结构同时包含一个字典和一个跳跃表。跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素,节点的object保存了元素的成员,score保存了元素的分值。字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素,键保存了元素的成员,值保存了元素的分值。 -## 编码的转换 +### 编码的转换 有序集合满足以下两个条件时,使用ziplist编码: @@ -220,7 +390,36 @@ int编码和embstr编码的字符串对象可以被转换为raw编码的字符 > 两个条件的上限参考zset-max-ziplist-entries和zset-max-ziplist-value选项。 -## 有序集合命令的实现 +### 有序集合命令 + + +```html +> zadd zset-key 728 member1 +(integer) 1 +> zadd zset-key 982 member0 +(integer) 1 +> zadd zset-key 982 member0 +(integer) 0 + +> zrange zset-key 0 -1 withscores +1) "member1" +2) "728" +3) "member0" +4) "982" + +> zrangebyscore zset-key 0 800 withscores +1) "member1" +2) "728" + +> zrem zset-key member1 +(integer) 1 +> zrem zset-key member1 +(integer) 0 + +> zrange zset-key 0 -1 withscores +1) "member0" +2) "982" +``` | 命令 | ziplist编码的实现 | zset编码的实现 | | --------- | --------------------------- | -------------------------------------- | @@ -234,7 +433,7 @@ int编码和embstr编码的字符串对象可以被转换为raw编码的字符 | ZREM | 遍历,删除包含给定成员的节点及旁边的分值节点 | 遍历跳跃表,删除节点,并在字典中解除被删除元素的成员和分值的关联 | | ZSCORE | 遍历查找成员节点,返回旁边的分值节点 | 从字典中取出给定成员的分值 | -# 8.7 类型检查与命令多态 +## 6 类型检查与命令多态 在执行一个类型特定的命令之前,Redis会先检查输入键的类型是否正确,然后再决定是否执行。类型检查是通过redisObject的type属性来的。 @@ -244,7 +443,7 @@ LLEN命令的执行过程: ![](img/chap8/img4.png) -# 8.8 内存回收 +## 7 内存回收 Redis为对象系统构建了一个引用计数垃圾回收。每个对象的引用计数由redisObject结构的refcount保存。 @@ -256,7 +455,7 @@ Redis为对象系统构建了一个引用计数垃圾回收。每个对象的引 当计数变为0时,对象占用的内存就会被释放。 -# 8.9 对象共享 +## 8 对象共享 refcount还可用于对象共享: @@ -271,16 +470,8 @@ Redis在初始化服务器时,创建了10000个字符串对象,包含0 ~ 999 **但Redis只对包含整数值的字符串对象共享**。即只有共享对象和目标对象完全相同的情况下。一个共享对象保存的值越复杂,验证共享对象和目标对象是否相同的操作也就越复杂。 -# 8.10 对象的空转时长 +## 9 对象的空转时长 redisObject最后一个属性lru,记录了对象最后一次被访问的时间,用OBJECT IDLETIME可查看。 如果服务器打开了maxmemory属性,lru对象可用于回收内存。 - -# 导航 - -[目录](README.md) - -上一章:[7. 压缩列表](ch7.md) - -下一章:[9. 数据库](ch9.md) diff --git a/数据库/Redis/02 数据类型.md b/数据库/Redis/02 数据类型.md deleted file mode 100644 index 859da104..00000000 --- a/数据库/Redis/02 数据类型.md +++ /dev/null @@ -1,15 +0,0 @@ -# 数据类型 - -## 0 数据类型 - - -| 数据类型 | 可以存储的值 | 操作 | -| :--: | :--: | -- | -| String | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作
对整数和浮点数执行自增或者自减操作 | -| List | 列表 | 从两端压入或者弹出元素
对单个或者多个元素进行修剪,
只保留一个范围内的元素 | -| Set | 无序集合 | 添加、获取、移除单个元素
检查一个元素是否存在于集合中
计算交集、并集、差集
从集合里面随机获取元素 | -| Hash | 无序字典 | 添加、获取、移除单个键值对
获取所有键值对
检查某个键是否存在| -| Zset | 有序集合 | 添加、获取、删除元素
根据分值范围或者成员来获取元素
计算一个键的排名 | - - -![](image/2021-09-02-16-24-45.png) diff --git a/数据库/Redis/03 数据结构.md b/数据库/Redis/03 数据结构.md index 7d75960c..23494b90 100644 --- a/数据库/Redis/03 数据结构.md +++ b/数据库/Redis/03 数据结构.md @@ -1,7 +1,40 @@ # 底层数据结构 + +- [底层数据结构](#底层数据结构) + - [1 简单动态字符串SDS](#1-简单动态字符串sds) + - [SDS 代码实现](#sds-代码实现) + - [SDS 特点(与C字符串的不同)](#sds-特点与c字符串的不同) + - [SDS API](#sds-api) + - [2 链表](#2-链表) + - [双向链表 代码实现](#双向链表-代码实现) + - [链表 特点](#链表-特点) + - [链表 API](#链表-api) + - [3 字典](#3-字典) + - [字典的实现](#字典的实现) + - [哈希算法](#哈希算法) + - [解决键冲突](#解决键冲突) + - [rehash](#rehash) + - [哈希表的扩展与收缩](#哈希表的扩展与收缩) + - [渐进式rehash](#渐进式rehash) + - [字典API](#字典api) + - [4 跳表](#4-跳表) + - [跳表 代码实现](#跳表-代码实现) + - [跳表使用](#跳表使用) + - [跳跃表API](#跳跃表api) + - [5 整数集合——特殊情况](#5-整数集合特殊情况) + - [整数集合的实现](#整数集合的实现) + - [升级](#升级) + - [升级的好处](#升级的好处) + - [降级](#降级) + - [整数集合API](#整数集合api) + - [6 压缩列表——特殊情况](#6-压缩列表特殊情况) + - [压缩列表的构成](#压缩列表的构成) + - [压缩列表节点的构成](#压缩列表节点的构成) + - [连锁更新](#连锁更新) + - [压缩列表API](#压缩列表api) + + ## 1 简单动态字符串SDS - - Redis构建了 简单动态字符串(simple dynamic string,SDS)来表示字符串值。 SDS还被用作缓冲区:AOF缓冲区,客户端状态中的输入缓冲区。 @@ -144,9 +177,9 @@ typedef struct list { ## 3 字典 -Redis的数据库就是使用字典来作为底层实现的,对数据库的增删改查都是构建在字典的操作之上。 +**Redis的数据库就是使用字典来作为底层实现的**,对数据库的增删改查都是构建在字典的操作之上。 -字典还是哈希键的底层实现之一,但一个哈希键包含的键值对比较多,又或者键值对中的元素都是较长的字符串时,Redis就会用字典作为哈希键的底层实现。 +**字典还是哈希键的底层实现之一**,但一个哈希键包含的键值对比较多,又或者键值对中的元素都是较长的字符串时,Redis就会用字典作为哈希键的底层实现。 ### 字典的实现 @@ -157,7 +190,7 @@ Redis字典所用的**哈希表**由dict.h/dictht结构定义: ```c typedef struct dictht { // 哈希表数组 - dict Entry **table; + dictEntry **table; // 哈希表大小 unsigned long size; // 哈希表大小掩码,用于计算索引值,总是等于size - 1 @@ -262,7 +295,7 @@ Redis使用链地址法解决键冲突,每个哈希表节点都有个next指 3. 释放ht[0],将ht[1]设置为ht[0],并为ht[1]新建一个空白哈希表 -## 哈希表的扩展与收缩 +### 哈希表的扩展与收缩 满足一下任一条件,程序会自动对哈希表执行扩展操作: @@ -278,7 +311,7 @@ load_factor = ht[0].used / ht[0].size 注:执行BGSAVE或BGREWRITEAOF过程中,Redis需要创建当前服务器进程的子进程,而多数操作系统都是用写时复制来优化子进程的效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能地避免在子进程存在期间扩展哈希表,避免不避免的内存写入,节约内存。 -# 4.5 渐进式rehash +### 渐进式rehash 将ht[0]中的键值对rehash到ht[1]中的操作不是一次性完成的,而是分多次渐进式的: @@ -289,7 +322,7 @@ load_factor = ht[0].used / ht[0].size 渐进式rehash过程中,字典会有两个哈希表,字典的增删改查会在两个哈希表上进行。 -# 4.6 字典API +### 字典API | 函数 | 作用 | 时间复杂度 | | ---------------- | --------------- | ----- | @@ -298,3 +331,250 @@ load_factor = ht[0].used / ht[0].size | dictReplace | 添加键值对,如已存在,替换原有 | O(1) | | dictFetchValue | 返回给定键的值 | O(1) | | dictGetRandomKey | 随机返回一个键值对 | O(1) | + + +## 4 跳表 + +跳跃表是一种**有序数据结构**,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问的目的。 + +跳跃表支持**平均O(logN)**、**最坏O(N)** 的查找,还可以通过顺序性操作来批量处理节点。 + +Redis使用跳跃表作为**有序集合键的底层实现**之一,如果有序集合包含的元素数量较多,或者有序集合中元素的成员是比较长的字符串时,Redis使用跳跃表来实现有序集合键。 + +在集群节点中,**跳跃表也被Redis用作内部数据结构**。 + +### 跳表 代码实现 +![](image/2021-09-02-23-20-54.png) + + +Redis的跳跃表由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义,其中zskiplistNode代表跳跃表节点,zskiplist保存跳跃表节点的相关信息,比如节点数量、以及指向表头/表尾结点的指针等。 + +跳跃表是基于多指针有序链表实现的,可以看成多个有序链表。 +![skiplist](img/chap5/skiplist.png) + +```c +typedef struct zskiplist { + struct zskiplistNode *header, *tail; + unsigned long length; + int leve; +} zskiplist; +``` + +zskiplist结构包含: + +- header:指向跳跃表的表头结点 +- tail:指向跳跃表的表尾节点 +- level:记录跳跃表内,层数最大的那个节点的层数(表头结点不计入) +- length:记录跳跃表的长度, 即跳跃表目前包含节点的数量(表头结点不计入) + +```c +typedef struct zskiplistNode { + struct zskiplistLevel { + struct zskiplistNode *forward; + unsigned int span; // 跨度 + } level[]; + + struct zskiplistNode *backward; + double score; + robj *obj; +} zskiplistNode; +``` + +zskiplistNode包含: + +- level:节点中用L1、L2、L3来标记节点的各个层,每个层都有两个属性:前进指针和跨度。前进指针用来访问表尾方向的其他节点,跨度记录了前进指针所指向节点和当前节点的距离(图中曲线上的数字)。 + - level数组可以包含多个元素,每个元素都有一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点。层数越多,访问速度就越快。没创建一个新节点的时候,根据幂次定律(越大的数出现的概率越小)随机生成一个介于1-32之间的值作为level数组的大小。这个大小就是层的高度。 + - 跨度用来计算排位(rank):在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到就是目标节点的排位。 + + +- 后退指针:BW,指向位于当前节点的前一个节点。只能回退到前一个节点,不可跳跃。 +- 分值(score):节点中的1.0/2.0/3.0保存的分值,节点按照各自保存的分值从小到大排列。节点的分值可以相同。 +- 成员对象(obj):节点中的o1/o2/o3。它指向一个字符串对象,字符串对象保存着一个SDS值。 + +> 注:表头结点也有后退指针、分值和成员对象,只是不被用到。 + + +与红黑树等平衡树相比,跳跃表具有以下优点: + +- 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性; +- 更容易实现; +- 支持无锁操作。 + +### 跳表使用 + +**遍历所有节点的路径** +1. 访问跳跃表的表头,然后从第四层的前景指正到表的第二个节点。 +2. 在第二个节点时,沿着第二层的前进指针到表中的第三个节点。 +3. 在第三个节点时,沿着第二层的前进指针到表中的第四个节点。 +4. 但程序沿着第四个程序的前进指针移动时,遇到NULL。结束遍历。 + +**查找指定的值** +在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。 +![](image/2021-09-02-23-19-57.png) + +### 跳跃表API + +| 函数 | 作用 | 时间复杂度 | +| ------------------------------- | ------------------------------- | ------------------ | +| zslCreate | 创建一个跳跃表 | O(1) | +| zslFree | 释放跳跃表,以及表中的所有节点 | O(N) | +| zslInsert | 添加给定成员和分值的新节点 | 平均O(logN),最坏O(N) | +| zslDelete | 删除节点 | 平均O(logN),最坏O(N) | +| zslGetRank | 返回包含给定成员和分值的节点在跳跃表中的排位 | 平均O(logN),最坏O(N) | +| zslGetElementByRank | 返回给定排位上的节点 | 平均O(logN),最坏O(N) | +| zslIsInRange | 给定一个range,跳跃表中如果有节点位于该range,返回1 | O(1),通过表头结点和表尾节点完成 | +| zslFirstInRange, zslLastInRange | 返回第一个/最后一个符合范围的节点 | 平均O(logN),最坏O(N) | +| zslDeleteRangeByScore | 删除所有分值在给定范围内的节点 | O(N) | +| zslDeleteRangeByRank | 删除所有排位在给定范围内的节点 | O(N) | + + +## 5 整数集合——特殊情况 + +整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且数量不多时,Redis采用整数集合作为集合键的底层实现。 + +### 整数集合的实现 + +整数集合,可以保存int16\_t、int32\_t或者int64\_t的整数值,且元素不重复,intset.h/intset结构表示一个整数集合: + +```c +typedef struct intset { + uint32_t encoding; // 决定contents保存的真正类型 + uint32_t length; + int8_t contents[]; // 各项从小到大排序 +} inset; +``` + +![five-int16](img/chap6/five-int16.png) + +上图中,contents数组的大小为sizeof(int16\_t) * 5 = 80位。 + +### 升级 + +每当添加一个新元素到整数集合中,且新元素的类型比现有所有元素的类型都要长时,整数集合需要先升级(update),然后才能添加新元素: + +1. 根据新元素的类型,扩展底层数组的空间大小,并未新元素分配空间。 +2. 将底层数组现有元素转换成与新元素相同的类型,并放置在正确的位置上(从后向前遍历)。放置过程中,维持底层数组的有序性质不变。 +3. 将新元素添加到底层数组里。 + +因为每次升级都可能对所有元素进行类型转换,所以复杂度为*O(N)*。 + +PS. 因为引发升级的新元素长度比当前元素都大,所以它的值要么大于当前所有元素,要么就小于。前种情况放置在底层数组的末尾,后种情况放置在头部。 + +### 升级的好处 + +升级有两个好处 + +1. 提升整数集合的灵活性 + + 我们可以随意地将int16\_t、int32\_t添加到集合中,不必担心出现类型错误,毕竟C是个静态语言。 + +2. 尽可能解约内存 + + 避免用一个int64\_t的数组包含所有元素 + +### 降级 + +**整数集合不支持降级**。 + +### 整数集合API + +| 函数 | 作用 | 时间复杂度 | +| ------------- | ---------- | ------------------ | +| intsetNew | 创建一个新的整数集合 | O(1) | +| intsetAdd | 添加指定元素 | O(N) | +| intsetRemove | 移除指定元素 | O(N) | +| intsetFind | 检查给定值是否存在 | 因为底层数组有序,所以O(logN) | +| insetRandom | 随机返回一个元素 | O(1) | +| intsetGet | 返回给定索引上的元素 | O(1) | +| intsetLen | 返回元素个数 | O(1) | +| intsetBlobLen | 返回占用的内存字节数 | O(1) | + + +## 6 压缩列表——特殊情况 + +压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表键,并且每个列表项要么就是小整数值,要么就是长度较短的字符串,那么Redis就会使用压缩列表来实现列表键。 + +当一个哈希键只包含少量键值对,并且每个键值对要么是小整数值,要么是长度较短的字符串,Redis就会使用压缩列表来实现哈希键。 + +### 压缩列表的构成 + +压缩列表是Redis为了节约内存而开发的,由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。一个压缩列表可以包含多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。 + +压缩列表的各组成部分: + +> zlbytes | zltail | zllen | entry1 | entry2 | … | entryN | zlend + +其中, + +| 属性 | 类型 | 长度 | 用途 | +| ------- | --------- | ---- | ---------------------------------------- | +| zlbytes | uint32\_t | 4字节 | 记录压缩列表占用的内存字节数:在内存重分配,或计算zlend的位置时使用 | +| zltail | uint32\_t | 4字节 | 记录表尾结点距离起始地址的字节数:通过这个偏移量,程序可以直接确定表尾结点的地址 | +| zllen | uint16\_t | 2字节 | 记录节点数量:但这个属性小于UINT16\_MAX(65535)时,这个属性的值就是节点的数量。如果等于UINT16\_MAX,节点的真实数量要遍历整个压缩列表才能得到 | +| entryX | 列表节点 | 不定 | 各个节点,节点的长度由保存的内容决定 | +| zlend | uint8\_t | 1字节 | 特殊值0xFF,标记压缩列表的尾端 | + +### 压缩列表节点的构成 + +压缩列表的节点可以保存一个字节数组或者一个整数值。压缩节点的各个组成部分: +* previous_entry_length +* encoding +* content + +**previous_entry_length** + +previous_entry_length以字节为单位,记录前一个节点的长度。previous_entry_length属性的长度可以是1字节或5字节: + +1. 若前一节点的长度小于254字节,那么previous_entry_length属性的长度就是1字节。前一节点的长度保存在其中。 +2. 若前一节点的长度大于254字节,那么previous_entry_length属性的长度就是5字节:其中属性的第一个字节被设置为0xFE(十进制254),而之后的四个字节则用于保存前一节点的长度。 + +程序可以通过指针运算,根据当前节点的起始地址来计算出前一个结点的起始地址。压缩列表的从尾向头遍历就是据此实现的。 + +**encoding** + +节点的encoding记录了节点的content属性所保存的数据的类型和长度: + +- 1字节、2字节或者5字节长,值的最高位为00、01或10的是字节数组编码:这种编码表示节点的content保存的是字节数组,数组的长度由编码除去最高两位置后的其他位记录。 +- 1字节长。值的最高位以11开头的是整数编码:表示content保存着整数值,整数值的类型和长度由编码除去最高两位之后的其他位记录。 + +**content** + +content保存节点的值,可以使字节数组或整数,值的类型和长度由encoding属性决定。 + +保存字节数组“hello world”的节点: + +| previoid_entry_length | encoding | content | +| --------------------- | -------- | ------------- | +| ... | 00001011 | "hello world" | + +保存整数10086的节点: + +| previoid_entry_length | encoding | content | +| --------------------- | -------- | ------- | +| ... | 11000000 | 10086 | + +### 连锁更新 + +因为previoid_entry_length的长度限制,添加或删除节点都有可能引发「连锁更新」。在最坏的情况下,需要执行*N*次重分配操作,而每次空间重分配的最坏复杂度是*O(N)*,合起来就是*O(N^2)*。 + +尽管如此,连锁更新造成性能问题的概率还是比较低的: + +1. 压缩列表里有多个连续的、长度介于250和253字节之间的节点,连锁更新才有可能触发。 +2. 即使出现连锁更新,只要需要更新的节点数量不多,性能也不会受影响。 + +### 压缩列表API + +| 函数 | 作用 | 复杂度 | +| ------------------ | ---------------------- | ---------------------------------------- | +| ziplistNew | 创建新的压缩列表 | O(1) | +| ziplistPush | 创建一个包含给定值的新节点,并添加到表头或尾 | 平均O(N),最坏O(N^2) | +| ziplistInsert | 将包含给定值的新节点插入到给定节点之后 | 平均O(N),最坏O(N^2) | +| ziplistIndex | 返回给定索引上的节点 | O(N) | +| ziplistFind | 查找并返回给定值的节点 | 因为节点的值可能是一个数组,所以检查节点值和给定值是否相同的复杂度为O(N),查找整个列表的复杂度为O(N^2) | +| ziplistNext | 返回给定节点的下一个节点 | O(1) | +| ziplistPrev | 返回给定节点的前一个节点 | O(1) | +| ziplistGet | 获取给定节点所保存的值 | O(1) | +| ziplistDelete | 删除给定节点 | 平均O(N),最坏O(N^2) | +| ziplistDeleteRange | 删除在给定索引上的连续多个节点 | 平均O(N),最坏O(N^2) | +| ziplistBlobLen | 返回压缩列表占用的内存字节数 | O(1) | +| ziplistLen | 返回包含的节点数量 | 节点数量小于65535时为O(1),否则为O(N) | diff --git a/数据库/Redis/04 字典.md b/数据库/Redis/04 字典.md deleted file mode 100644 index 05ab8483..00000000 --- a/数据库/Redis/04 字典.md +++ /dev/null @@ -1,8 +0,0 @@ - -# 导航 - -[目录](README.md) - -上一章:[3. 链表](ch3.md) - -下一章:[5. 跳跃表](ch5.md) diff --git a/数据库/Redis/14 服务器.md b/数据库/Redis/04 数据库.md similarity index 52% rename from 数据库/Redis/14 服务器.md rename to 数据库/Redis/04 数据库.md index 83990f4a..a9f25454 100644 --- a/数据库/Redis/14 服务器.md +++ b/数据库/Redis/04 数据库.md @@ -1,6 +1,194 @@ +# 数据库 +## 1 数据库的实现 + +### 服务器数据库 Redis服务器负责与多个客户端建立连接,处理客户端的命令请求,在数据库中保存命令产生的数据,并通过资源管理来维持服务器自身的运转。 -# 14.1 命令请求的执行过程 +Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中,数组的每个项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库: + +```c +struct redisServer { + // ... + redisDb *db; + int dbnum; // 数据库的数量 + // ... + list *clients; + redisClient *lua_client; // Lua伪客户端,服务器运行时一直存在 + // ... + // ... +}; +``` +* 其中dbnum的值有服务器配置的database选项决定,默认为16。 +* `redisServer`结构保存了一个`clients`链表,保存了所有连接的客户端的状态信息。 + +### 客户端数据库 + +默认情况下,Redis客户端的目标数据库是0号数据库,客户端可以执行`SELECT`命令来切换。 + +服务器内部,客户端状态`redisClient`结构的db属性记录了客户端当前的当前信息,以及执行相关功能需要的数据结构: + +- 客户端的套接字描述符 +- 客户端的名字 +- 客户端的标志值(flag) +- 客户端正在使用的数据库的指针及号码 +- 客户端当前要执行的命令、参数 +- 客户端的输入输出缓冲区 +- 客户端的复制状态信息 +- 客户端的事务状态 +- 客户端执行发布与订阅功能用到的数据结构 +- 客户端的身份验证标识 +- 客户端的统计信息,如创建时间、最后一次通行时间、缓冲区大小超出限制的时间 +```c +typedef struct redisClient { + /* + fd记录客户端正在使用的套接字描述符 + 伪客户端的fd为-1,不需要套接字连接,目前用于 1. AOF还原, 2. 执行Lua脚本的Redis命令 + 普通客户端为大于-1的整数。CLIENT list命令可以查看当前正在使用的套接字描述符 + */ + int fd; + + // 连接到服务器的客户端默认没有名字,CLIENT setname可以设置一个名字。 + robj *name; + + /* + flags记录了客户端的role,以及目前所处的状态 + 所以,flags可以是多个二进制或,所有标志在redis.h中定义 + */ + int flags; + + // 输入缓冲区用于保存客户端发送的命令请求 + sds querybuf; + + // 解析querybuf的请求,得出命令参数及命令个数 + // argv是个数组,每个元素都是一个字符串对象,其中argv[0]是要执行的命令 + robj **argv; + int argc; + + // redisCommand保存了命令的实现函数,标识、参数个数、总执行次数等统计信息 + struct redisCommand *cmd; + + // 输出缓冲区保存命令的回复,其中 + // 1. buf是固定缓冲区,用于保存长度较小的回复 + // 2. reply可变缓冲区,保存长度较大的回复 + char bug[REDIS_REPLY_CHUNK_BYTES]; + int bufpos; + list *reply; + + // 记录客户端是否通过了验证 + int authenticated; + + time_t ctime; + time lastinteraction; + time_t obuf_soft_limit_reached_time; + + // ... +} redisClient; +``` + +**创建客户端** + +客户端使用connect函数连接到服务器,服务器就会调用连接事件处理器,为客户端创建相应的客户端状态,并添加到链表的末尾。 + +**关闭客户端** + +- 客户端进程被杀死 +- 发送的协议不符合格式 +- 客户端成了`CLIENT KILL`命令的目标 +- 服务器配置了timeout选项,客户端空转被断开 +- 超出输入/输出缓冲区限制。输出缓冲区的限制包括:硬性限制、弱性限制。超过软性限制一段时间,客户端也会被关闭。 + + +### 数据库键空间 + +Redis是一个键值对(key-value pair)数据库服务器。redisDb结构的dict字典保存了数据库的所有键值对,这个字典就是键空间: + +```c +typedef struct redisDb { + // ... + dict *dict; + // ... +} redisDb; +``` + +键空间和用户所见的数据库是直接对应的: + +- 键空间的键也就是数据库的键。每个键都是一个字符串对象。 +- 键空间的值也是数据库的值。每个值可以使字符串对象、列表对象、哈希表对象、集合对象、有序集合对象。 + +![](img/chap9/img0.png) + +所有针对数据库的操作,实际上都是通过键空间字典来实现。 + +## 2 数据库的操作 + +### 数据库基本操作 +> 增删查改 +* 添加新键。添加一个新键值对到数据库,就是将新键值对添加到键空间字典中。 +* 删除键。删除数据库中的一个键,就是在键空间中删除键所对应的键值对对象。 +* 更新键。更新数据库的一个键,就是对键空间里键所对应的值对象进行更新。根据值对象类型的不同,更新的具体方法也不同。 +* 对键取值。对一个数据库键取值,就是在键空间中取出键所对应的值对象。 + +### 读写键空间时的维护操作 + +当Redis对数据库读写时,不仅对键空间执行指定的操作,还会执行一些额外的维护: + +1. 读取一个键后,更新服务器的键命中次数或不命中次数。这两个值可通过`INFO stats`命令查看。 +2. 读取一个键后,更新LRU时间。`OBJECT idletime `查看。 +3. 读取键时发现已过期,删除。 +4. 如果有客户端`WATCH`了某个键,修改后将键标记为dirty,从而让事物程序注意到它。 +5. 每次修改一个键后,将dirty键计数器的值+1,这个计数器会触发服务器的持久化和赋值操作。 +6. 如果服务器开启了通知功能,键修改后,服务器会按照配置发送通知。 + +## 3 服务器初始化过程 + +### 初始化服务器状态结构 + +初始化服务器的第一步就是创建一个`redisServer`类型的实例变量`server`,并为结构中的各个属性设置默认值。这个工作由`redis.c/initServerConfig`函数完成: + +- 设置服务器运行id +- 为id加上结尾字符 +- 设置默认的配置文件路径 +- 设置默认服务器频率 +- 设置服务器的运行架构,64位 or 32位 +- 设置服务器的默认端口 +- 设置服务器的默认RDB和AOF条件 +- 初始化服务器的LRU时钟 +- 创建命令表 + +### 载入配置选项 + +启动服务器时,用户可以通过配置参数或者配置文件来修改服务器的默认配置。 + +`redis.c/initServerConfig`函数初始化完`server`变量后,开始载入用户给定的配置。 + +### 初始化服务器数据结构 + +载入用户的配置选项之后,才能正确地初始化数据结构,由`initServer`函数负责: + +- `server.clients`链表 +- `server.db`数组 +- `server.pubsub_channels`字典 +- `server.lua`Lua环境 +- `server.slowlog` + +除此之外,`initServer`还: + +- 为服务器设置进程信号处理器 +- 创建共享对象 +- 打开服务器的监听端口,并为套接字关联应答事件处理器 +- 为`serverCron`函数创建时间事件 +- 打开或创建的AOF文件 +- 初始化后台I/O模块 + +### 还原数据库状态 + +初始化完`server`后,服务器要载入RDB或AOF文件,还原数据库状态 + +### 执行事件循环 + +开始执行服务器的loop。 + +## 4 命令请求的执行过程 `SET KEY VALUE`命令的执行过程: @@ -9,13 +197,13 @@ Redis服务器负责与多个客户端建立连接,处理客户端的命令请 3. 服务器将`OK`发送给客户端。 4. 客户端接收服务器返回的命令`OK`,并打印给用户。 -## 发送命令请求 +### 发送命令请求 用户:键入命令请求 客户端:将命令请求转换为协议格式然后发送给服务器 -## 读取命令请求 +### 读取命令请求 当连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器执行以下操作: @@ -23,13 +211,13 @@ Redis服务器负责与多个客户端建立连接,处理客户端的命令请 2. 对输入缓冲区的命令请求进行分析,提取命令参数及其个数,保存到客户端状态的argv和argc属性。 3. 调用命令执行器,执行指定的命令。 -## 命令执行器(1):查找命令实现 +### 命令执行器(1):查找命令实现 命令执行器要做的第一件事是根据客户端状态的`argv[0]`参数,在命令表(command table)中查找参数指定的命令,并将其保存到客户端状态的`cmd`属性里。 命令表是一个字典,键是命令名字,值是一个`redisCommand`结构。命令表使用的是**大小写无关**的查找算法。 -## 命令执行器(2):执行预备操作 +### 命令执行器(2):执行预备操作 有了执行命令所需的命令实现函数、参数、参数个数,但程序还需要一些预备操作: @@ -44,7 +232,7 @@ Redis服务器负责与多个客户端建立连接,处理客户端的命令请 - 如果客户端正在执行事务,那么服务器只会执行客户端发来的`EXEC`、 `DISCARD`、 `MULTI`、 `WATCH`命令,其余命令进入事务队列。 - 如果服务器打开监视器功能,要将执行的命令和参数等信息发给监视器,其后才真正执行命令。 -## 命令执行器(3):调用命令的实现函数 +### 命令执行器(3):调用命令的实现函数 > client->cmd->proc(client); @@ -54,26 +242,27 @@ Redis服务器负责与多个客户端建立连接,处理客户端的命令请 命令回复会保存在输出缓冲区,之后实现函数还会为套接字关联命令回复处理器,将回复返回给客户端。 -## 命令执行器(5):执行后续工作 +### 命令执行器(5):执行后续工作 - 如果开启了慢查询,添加新的日志。 - `redisCommand`结构的`calls`计数器+1。 - 写入AOF缓冲区。 - 同步从服务器。 -## 将命令回复发送给客户端 +### 将命令回复发送给客户端 当客户端套接字变为可写时,服务器将输出缓冲区的命令发送给客户端。发送完毕后,清空输出缓冲区。 -## 客户端接收并打印命令回复 +### 客户端接收并打印命令回复 服务器:回复处理器将协议格式的命令返回给客户端。 客户端:将回复格式化成人类可读的格式,打印。 -# 14.2 serverCron函数 -## 更新服务器时间缓存 +## 4 serverCron函数执行过程 + +### 更新服务器时间缓存 每次获取系统的当前时间都要执行一次系统调用,为了减少系统调用,服务器状态中保存了当前时间的缓存: @@ -88,7 +277,7 @@ struct redisServer { `serverCron`默认会100毫秒更新一次这两个属性,所以它们的精确度并不高。对于一些高精度要求的操作,还是会再次执行系统调用。 -## 更新LRU时钟 +### 更新LRU时钟 ```objective-c struct redisServer { @@ -103,7 +292,7 @@ typedef struct redisObject { } robj; ``` -## 更新服务器每秒执行命令次数 +### 更新服务器每秒执行命令次数 `serverCron`函数中的`trackOperationPerSecond`函数以每100毫秒一次的频率执行,该函数以抽样计算的方式,估算并记录服务器在最近一秒内处理的命令请求数量,这个值可以用过`INFO status`命令查看。 @@ -127,7 +316,7 @@ struct redisServer { 客户端执行`INFO`命令,服务器会调用`getOperationsPerSecond`函数,根据`ops_sec_samples`中的抽样结果,计算出`instantaneous_ops_per_sec`属性的值。 -## 更新服务器内存峰值记录 +### 更新服务器内存峰值记录 ```c struct redisServer { @@ -138,7 +327,7 @@ struct redisServer { 每次`serverCron`执行,程序都会查看当前的内存数量,更新`stat_peak_memory`。`INFO memory`可查看。 -## 处理SIGTERM信号 +### 处理SIGTERM信号 启动时,Redis会为服务器进程的`SIGTERM`信号关联处理器`sigtermHandler`函数。它在接到该信号后,打开服务器状态的`shutdown_asap`标识。每次`serverCron`执行,程序都会检查该标识,并决定是否关闭服务器。 @@ -149,18 +338,18 @@ struct redisServer { }; ``` -## 管理客户端资源 +### 管理客户端资源 `serverCron`每次都会调用`clientsCron`函数,后者会对一定数量的客户端作如下检查: - 连接是否超时 - 输入缓冲区是否超过长度,如果是,新建缓冲区 -## 管理数据库资源 +### 管理数据库资源 `serverCron`每次都会调用`databasesCron`函数,检查一部分的数据库,删除过期键,对字典进行收缩等。 -## 执行被延迟的BGREWRITEAOF +### 执行被延迟的BGREWRITEAOF 服务器执行`BGSAVE`期间,会阻塞`BGREWRITEAOF`命令。 @@ -171,7 +360,7 @@ struct redisServer { }; ``` -## 检查持久化操作的运行状态 +### 检查持久化操作的运行状态 ```c struct redisServer { @@ -193,7 +382,7 @@ struct redisServer { ![](img/chap14/img0.png) -## serverCron的其他操作: +### serverCron的其他操作: - 将AOF缓冲区的内容写入AOF文件 @@ -201,59 +390,36 @@ struct redisServer { - 增加cronloops计数器(它的唯一作用就是复制模块中实现『每执行`serverCron`函数N次就执行一次指定代码』的功能”) -# 14.3 初始化服务器 +## 5 数据库通知功能 -## 初始化服务器状态结构 +数据库通知是Redis 2.8新增加的功能,让客户端通过订阅可给定的频道或模式,来获取数据库中键的变化,以及数据库命令的执行情况。 -初始化服务器的第一步就是创建一个`redisServer`类型的实例变量`server`,并为结构中的各个属性设置默认值。这个工作由`redis.c/initServerConfig`函数完成: +“某个键执行了什么命令”的通知成为「键空间通知」。“某个命令被什么键执行了”是「键时间通知」。服务器配置的notify-keyspace-events选项决定了服务器发送通知的类型。 -- 设置服务器运行id -- 为id加上结尾字符 -- 设置默认的配置文件路径 -- 设置默认服务器频率 -- 设置服务器的运行架构,64位 or 32位 -- 设置服务器的默认端口 -- 设置服务器的默认RDB和AOF条件 -- 初始化服务器的LRU时钟 -- 创建命令表 +发送通知的功能由notify.h/notifyKeyspaceEvent函数实现的: -## 载入配置选项 +```c +void notifyKeyspaceEvent(int type, char *event, int dbid); +``` -启动服务器时,用户可以通过配置参数或者配置文件来修改服务器的默认配置。 +伪代码如下: -`redis.c/initServerConfig`函数初始化完`server`变量后,开始载入用户给定的配置。 +```python +def notifyKeyspaceEvent(type, event, key, bdid): + if not (server.notify_keyspace_events & type): + return + + # 发送键空间通知 + if server.notify_keyspace_events & REDIS_NOTIFY_KEYSPACE: + # 将通知发送给频道 __keyspace@__: + chan = "_keyspace@{bdid}__:{key}".format(dbid_dbid, key=key) + pubsubPublishMessage(chan, event) + + # 发送键时间通知 + if server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT: + chan = "_keyspace@{bdid}__:{event}".format(dbid_dbid, event=event) + pubsubPublishMessage(chan, event) + pubsubPublishMessage(chan, key) +``` -## 初始化服务器数据结构 -载入用户的配置选项之后,才能正确地初始化数据结构,由`initServer`函数负责: - -- `server.clients`链表 -- `server.db`数组 -- `server.pubsub_channels`字典 -- `server.lua`Lua环境 -- `server.slowlog` - -除此之外,`initServer`还: - -- 为服务器设置进程信号处理器 -- 创建共享对象 -- 打开服务器的监听端口,并为套接字关联应答事件处理器 -- 为`serverCron`函数创建时间事件 -- 打开或创建的AOF文件 -- 初始化后台I/O模块 - -## 还原数据库状态 - -初始化完`server`后,服务器要载入RDB或AOF文件,还原数据库状态 - -## 执行事件循环 - -开始执行服务器的loop。 - -# 导航 - -[目录](README.md) - -上一章:[13. 客户端](ch13.md) - -下一章:[15. 复制](ch15.md) diff --git a/数据库/Redis/09 数据库.md b/数据库/Redis/05 生命周期.md similarity index 54% rename from 数据库/Redis/09 数据库.md rename to 数据库/Redis/05 生命周期.md index 3ae6c30b..4afe8907 100644 --- a/数据库/Redis/09 数据库.md +++ b/数据库/Redis/05 生命周期.md @@ -1,79 +1,14 @@ -# 9.1 服务器中的数据库 +# 生命周期 -Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中,数组的每个项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库: -```c -struct redisServer { - // ... - redisDb *db; - int dbnum; // 数据库的数量 - // ... -}; -``` +## 1 数据库键生命周期 +### 概述 -其中dbnum的值有服务器配置的database选项决定,默认为16。 +Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。 -# 9.2 切换数据库 +对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。 -默认情况下,Redis客户端的目标数据库是0号数据库,客户端可以执行`SELECT`命令来切换。 - -服务器内部,客户端状态`redisClient`结构的db属性记录了客户端当前的目标数据库: - -```c -typedef struct redisClient { - redisDb *db; // 指向redisServer.db数组中的一个元素 -} redusClient; -``` - -# 9.3 数据库键空间 - -Redis是一个键值对(key-value pair)数据库服务器。redisDb结构的dict字典保存了数据库的所有键值对,这个字典就是键空间: - -```c -typedef struct redisDb { - // ... - dict *dict; - // ... -} redisDb; -``` - -键空间和用户所见的数据库是直接对应的: - -- 键空间的键也就是数据库的键。每个键都是一个字符串对象。 -- 键空间的值也是数据库的值。每个值可以使字符串对象、列表对象、哈希表对象、集合对象、有序集合对象。 - -![](img/chap9/img0.png) - -所有针对数据库的操作,实际上都是通过键空间字典来实现。 - -## 添加新键 - -添加一个新键值对到数据库,就是将新键值对添加到键空间字典中。 - -## 删除键 - -删除数据库中的一个键,就是在键空间中删除键所对应的键值对对象。 - -## 更新键 - -更新数据库的一个键,就是对键空间里键所对应的值对象进行更新。根据值对象类型的不同,更新的具体方法也不同。 - -## 对键取值 - -对一个数据库键取值,就是在键空间中取出键所对应的值对象。 - -## 读写键空间时的维护操作 - -当Redis对数据库读写时,不仅对键空间执行指定的操作,还会执行一些额外的维护: - -1. 读取一个键后,更新服务器的键命中次数或不命中次数。这两个值可通过`INFO stats`命令查看。 -2. 读取一个键后,更新LRU时间。`OBJECT idletime `查看。 -3. 读取键时发现已过期,删除。 -4. 如果有客户端`WATCH`了某个键,修改后将键标记为dirty,从而让事物程序注意到它。 -5. 每次修改一个键后,将dirty键计数器的值+1,这个计数器会触发服务器的持久化和赋值操作。 -6. 如果服务器开启了通知功能,键修改后,服务器会按照配置发送通知。 - -# 9.4 设置键的生存时间或过期时间 +### 设置键的生存时间或过期时间 `EXPIRE`或`PEXPIRE`命令让客户端可以以秒或者毫秒进度为某个键设置生存时间。经过指定的时间后,服务器会自动删除生存时间为0的键。 @@ -81,9 +16,9 @@ typedef struct redisDb { `TTL`和`PTTL`命令可查看某个键的剩余生存时间。 -实际上,`EXPIRE`、`PEXPIRE`、`EXPIREAT`三个命令都是使用`PEXPIREAT`来实现的。 +实际上,`EXPIRE`、`PEXPIRE`、`EXPIREAT`三个命令都是使用`PEXPIREAT`来实现的。 -## 保存过期时间 +### 保存过期时间 redisDb结构的expires字典保存了所有键的过期时间: @@ -113,7 +48,7 @@ def PEXPIREAT(key, expire_time_in_ms): return 1 ``` -## 移除过期时间 +### 移除过期时间 `PERSIST`可以移除一个键的过期时间,它在过期字典中找到给定的键,解除键和值(过期时间)的关联。 @@ -126,7 +61,7 @@ def PERSIST(key): return 1 ``` -## 计算并返回剩余生存时间 +### 计算并返回剩余生存时间 `TTL`和`PTTL`都是通过计算键的过期时间和当前时间的差来实现的: @@ -143,14 +78,14 @@ def PTTL(key): return expire_time_in_ms - now_ms ``` -## 过期键的判定 +### 过期键的判定 通过过期字典,程序可通过以下步骤来判定键是否过期: 1. 检查给定键是否存在于过期字典,如果存在,取得其过期时间 2. 检查当前UNIX时间戳是否大于其过期时间 -# 9.5 过期键的删除策略 +## 2 过期键的删除策略 有三种不同的键删除策略: @@ -160,17 +95,14 @@ def PTTL(key): | 惰性删除 | 每次从键空间获取键时,检查其是否过期,过期则删除;否则就返回该键。 | 对CPU时间最友好 | 对内存不友好 | | 定期删除 | 每隔一段时间,对数据库进行一次检查,删除所有的过期键。 | 上述两种策略的整合和折中 | 难点在于确定删除的时长和频率 | -# 9.6 Redis的过期键删除策略 -Redis服务器使用的是惰性删除和定期删除两种策略。 - -## 惰性删除的实现 +### 惰性删除的实现 惰性删除的策略由db.c/exipireIfNeeded函数实现,所有读写数据库的Redis命令都会在执行前调用该函数。 ![](img/chap9/img2.png) -## 定期删除的实现 +### 定期删除的实现 定期删除的策略由redis.c/activeExpireCycle函数实现,每当Redis服务器周期性操作redis.c/serverCron函数执行时,该函数会被调用。它在规定时间内,分多次遍历各个数据库,检查过期时间并删除过期键。 @@ -211,9 +143,9 @@ activeExpireCycle的工作模式总结如下: - 全局变量current\_db记录当前检查的进度,并在下一次调用时接着处理上一次的进度。 - 随着activeExpireCycle的不断执行,所有数据库都会被检查一遍,这是current\_db重置为0,再次开始新一轮动机检查。 -# 9.7 AOF、RDB和复制功能对过期键的处理 +## 3 AOF、RDB和复制功能对过期键的处理 -## RDB文件生成和载入 +### RDB文件生成和载入 执行SAVE或BGSAVE命令时会创建一个新的RDB文件,已过期的键不会保存到RDB中。 @@ -222,56 +154,38 @@ activeExpireCycle的工作模式总结如下: - 如果服务器以主服务器模式运行,那么载入RDB时,会检查文件中的键,过期键会被忽略。 - 如果服务器以从服务器模式运行,那么载入RDB时,不管键是否过期,一律载入。其后,在主从服务器同步时,从服务器的数据库就会被清空。 -## AOF文件写入和重写 +### AOF文件写入和重写 服务器以AOF持久化模式运行时,如果某个键已过期,但还没有被删除,那么AOF文件不会因为这个过期键而产生任何影响。但过期键被删除后,程序会向AOF文件追加一条DEL命令,显式记录该键已被删除。 AOF重写过程中,程序会对键进行检查,已过期的键不会被保存到重写后的AOF文件中。 -## 复制 +### 复制 当服务器处于复制模式下时,过期键删除动作由主服务器控制,这就保证了一致性: - 主服务器删除一个过期键后,显式向从服务器发送DEL命令 -- 从服务器执行客户端发送的杜明令时,即时碰到过期键也不会删除,而是像初期未过期的键一样 +- 从服务器执行客户端发送的命令时,即使碰到过期键也不会删除,而是像初期未过期的键一样 - 从服务器接到主服务器的DEL命令后,才会删除过期键 -# 9.8 数据库通知 -数据库通知是Redis 2.8新增加的功能,让客户端通过订阅可给定的频道或模式,来获取数据库中键的变化,以及数据库命令的执行情况。 +## 4 数据淘汰策略 +> 与过期键的删除策略不同。数据淘汰策略主要是因为内存占用过高导致,而过期键的删除是由于数据已经过期。 -“某个键执行了什么命令”的通知成为「键空间通知」。“某个命令被什么键执行了”是「键时间通知」。服务器配置的notify-keyspace-events选项决定了服务器发送通知的类型。 -发送通知的功能由notify.h/notifyKeyspaceEvent函数实现的: +可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。Redis 具体有 6 种淘汰策略: -```c -void notifyKeyspaceEvent(int type, char *event, int dbid); -``` +| 策略 | 描述 | +| :--: | :--: | +| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 | +| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 | +|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 | +| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 | +| allkeys-random | 从所有数据集中任意选择数据进行淘汰 | +| noeviction | 禁止驱逐数据 | -伪代码如下: +作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法实际实现上并非针对所有 key,而是抽样一小部分并且从中选出被淘汰的 key。 -```python -def notifyKeyspaceEvent(type, event, key, bdid): - if not (server.notify_keyspace_events & type): - return - - # 发送键空间通知 - if server.notify_keyspace_events & REDIS_NOTIFY_KEYSPACE: - # 将通知发送给频道 __keyspace@__: - chan = "_keyspace@{bdid}__:{key}".format(dbid_dbid, key=key) - pubsubPublishMessage(chan, event) - - # 发送键时间通知 - if server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT: - chan = "_keyspace@{bdid}__:{event}".format(dbid_dbid, event=event) - pubsubPublishMessage(chan, event) - pubsubPublishMessage(chan, key) -``` +使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。 -# 导航 - -[目录](README.md) - -上一章:[8. 对象](ch8.md) - -下一章:[10. RDB持久化](ch10.md) +Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。 diff --git a/数据库/Redis/05 跳跃表.md b/数据库/Redis/05 跳跃表.md deleted file mode 100644 index 97c36f5b..00000000 --- a/数据库/Redis/05 跳跃表.md +++ /dev/null @@ -1,85 +0,0 @@ -跳跃表是一种**有序数据结构**,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问的目的。跳跃表支持平均*O(logN)*、最坏*O(N)*的查找,还可以通过顺序性操作来批量处理节点。 - -Redis使用跳跃表作为有序集合键的底层实现之一,如果有序集合包含的元素数量较多,或者有序集合中元素的成员是比较长的字符串时,Redis使用跳跃表来实现有序集合键。 - -在集群节点中,跳跃表也被Redis用作内部数据结构。 - -# 5.1 跳跃表的实现 - -Redis的跳跃表由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义,其中zskiplistNode代表跳跃表节点,zskiplist保存跳跃表节点的相关信息,比如节点数量、以及指向表头/表尾结点的指针等。 - -![skiplist](img/chap5/skiplist.png) - -```c -typedef struct zskiplist { - struct zskiplistNode *header, *tail; - unsigned long length; - int leve; -} zskiplist; -``` - -zskiplist结构包含: - -- header:指向跳跃表的表头结点 -- tail:指向跳跃表的表尾节点 -- level:记录跳跃表内,层数最大的那个节点的层数(表头结点不计入) -- length:记录跳跃表的长度, 即跳跃表目前包含节点的数量(表头结点不计入) - -```c -typedef struct zskiplistNode { - struct zskiplistLevel { - struct zskiplistNode *forward; - unsigned int span; // 跨度 - } level[]; - - struct zskiplistNode *backward; - double score; - robj *obj; -} zskiplistNode; -``` - -zskiplistNode包含: - -- level:节点中用L1、L2、L3来标记节点的各个层,每个层都有两个属性:前进指针和跨度。前进指针用来访问表尾方向的其他节点,跨度记录了前进指针所指向节点和当前节点的距离(图中曲线上的数字)。 - - level数组可以包含多个元素,每个元素都有一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点。层数越多,访问速度就越快。没创建一个新节点的时候,根据幂次定律(越大的数出现的概率越小)随机生成一个介于1-32之间的值作为level数组的大小。这个大小就是层的高度。 - - 跨度用来计算排位(rank):在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到就是目标节点的排位。 - - -- 后退指针:BW,指向位于当前节点的前一个节点。只能回退到前一个节点,不可跳跃。 -- 分值(score):节点中的1.0/2.0/3.0保存的分值,节点按照各自保存的分值从小到大排列。节点的分值可以相同。 -- 成员对象(obj):节点中的o1/o2/o3。它指向一个字符串对象,字符串对象保存着一个SDS值。 - -注:表头结点也有后退指针、分值和成员对象,只是不被用到。 - -遍历所有节点的路径: - -1. 访问跳跃表的表头,然后从第四层的前景指正到表的第二个节点。 -2. 在第二个节点时,沿着第二层的前进指针到表中的第三个节点。 -3. 在第三个节点时,沿着第二层的前进指针到表中的第四个节点。 -4. 但程序沿着第四个程序的前进指针移动时,遇到NULL。结束遍历。 - -# 5.2 跳跃表API - -| 函数 | 作用 | 时间复杂度 | -| ------------------------------- | ------------------------------- | ------------------ | -| zslCreate | 创建一个跳跃表 | O(1) | -| zslFree | 释放跳跃表,以及表中的所有节点 | O(N) | -| zslInsert | 添加给定成员和分值的新节点 | 平均O(logN),最坏O(N) | -| zslDelete | 删除节点 | 平均O(logN),最坏O(N) | -| zslGetRank | 返回包含给定成员和分值的节点在跳跃表中的排位 | 平均O(logN),最坏O(N) | -| zslGetElementByRank | 返回给定排位上的节点 | 平均O(logN),最坏O(N) | -| zslIsInRange | 给定一个range,跳跃表中如果有节点位于该range,返回1 | O(1),通过表头结点和表尾节点完成 | -| zslFirstInRange, zslLastInRange | 返回第一个/最后一个符合范围的节点 | 平均O(logN),最坏O(N) | -| zslDeleteRangeByScore | 删除所有分值在给定范围内的节点 | O(N) | -| zslDeleteRangeByRank | 删除所有排位在给定范围内的节点 | O(N) | - -# 导航 - -[目录](README.md) - -上一章:[4. 字典](ch4.md) - -下一章:[6. 整数集合](ch6.md) - diff --git a/数据库/Redis/10 RDB持久化.md b/数据库/Redis/06 RDB持久化.md similarity index 71% rename from 数据库/Redis/10 RDB持久化.md rename to 数据库/Redis/06 RDB持久化.md index 03443d01..f07a890e 100644 --- a/数据库/Redis/10 RDB持久化.md +++ b/数据库/Redis/06 RDB持久化.md @@ -1,8 +1,12 @@ +# RDB持久化 +## 1 RDB持久化方法 + +### 概念 RDB持久化可将内存中的数据库状态保存到磁盘上,避免数据丢失。持久化可以手动,也可以根据服务器配置选项定期执行。 RDB持久化生成的RDB文件是一个压缩过的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态。 -# 10.1 RDB文件的创建于载入 +### RDB文件的创建与载入 有两个命令可以生成RDB文件: @@ -11,18 +15,14 @@ RDB持久化生成的RDB文件是一个压缩过的二进制文件,通过该 > 在BGSAVE命令执行期间,服务器处理SAVE、GBSAVE、BGREWRITEAOF命令会被拒绝执行。 -创建RDB文件的操作由`rdb.c/rdbSave`函数完成。 - -RDB文件的载入工作在服务器启动时自动执行。 - -另外,AOF文件的更新频率比RDB文件要高,所以: +创建RDB文件的操作由`rdb.c/rdbSave`函数完成。RDB文件的载入工作在服务器启动时自动执行。另外,AOF文件的更新频率比RDB文件要高,所以: - 如果服务器开启了AOF,那么优先用AOF来还原数据库。 - 只有在AOF关闭时,服务器才会用RDB来还原数据库。 载入RDB文件的工作由`rdb.c/rdbLoad`函数完成。载入RDB文件期间,服务器一直处于阻塞状态。 -# 10.2 自动间隔性保存 +## 2 自动间隔性保存 Redis允许用户通过设置服务器配置的save选项,每隔一段时间执行一次BGSAVE命令。配置如下: @@ -60,59 +60,36 @@ struct saveparam { - dirty记录上一次成功`SAVE`或`BGSAVE`之后,服务器对数据库状态进行了多少次修改。 - lastsave是一个UNIX时间戳,记录了服务器上一次成功`SAVE`或`BGSAVE`的时间。 -## 检查保存条件是否满足 +### 检查保存条件是否满足 服务器的周期性操作函数`serverCron`默认每个100毫秒就会执行一次,其中一项工作是检查save选项所设置的保存条件是否满足。 -# 10.3 RDB文件结构 +## 3 RDB文件结构 RDB文件的各个部分包括: -> REDIS | db_version | databases | EOF | check_sum - -## REDIS - -开头是REDIS部分,长度为5。保存了五个字符,以便载入时确认是否为RDB文件。 - -## db_version - -db\_version长4字节,是一个字符串表示的整数,记录了RDB文件的版本号。 - -## databases - -databases部分包含了0个或多个数据库,以及各个数据库中的键值对数据。一个保存了0号和3号数据库的RDB文件如下: - -> REDIS | db_version | database 0 | databse 3 | EOF | check_sum +``` +REDIS | db_version | databases | EOF | check_sum +``` +* REDIS。开头是REDIS部分,长度为5。保存了五个字符,以便载入时确认是否为RDB文件。 +* db_version。db_version长4字节,是一个字符串表示的整数,记录了RDB文件的版本号。 +* databases。databases部分包含了0个或多个数据库,以及各个数据库中的键值对数据。 +* EOF。长度为1字节,标识RDB文件结束。 +* check_sum。8字节的无符号整数,保存着一个前面四个部分的校验和。 每个非空数据库在RDB文件中都可保存为以下三部分: -> SELECTDB | db_number | key_value_pairs - +``` +SELECTDB | db_number | key_value_pairs +``` - SELECTEDB。1字节。但程序遇到这个值的时候,它就知道接下来要读入的将是一个数据库号码。 -- db\_number。读取号码之后,服务器会调用`SELECT`命令切换数据库。 +- db_number。读取号码之后,服务器会调用`SELECT`命令切换数据库。 - key_value_pairs。不带过期时间的键值对在RDB文件中包括TYPE、key、value。TYPE的值决定了如何读入和解释value的数据。带过期时间的键值对增加了EXPIRETIME_MS和ms。前者告知程序接下来要读入一个UNIX时间戳。 -## EOF - -长度为1字节,标识RDB文件结束。 - -## check_sum - -8字节的无符号整数,保存着一个前面四个部分的校验和。 - -# 10.4 分析RDB文件 +## 4 分析RDB文件 od命令分析RDB文件。-c参数可以以ASCII编码打印文件。比如一个数据库状态为空的RDB文件: ![](img/chap10/img0.png) Redis自带的文件检查工具是redis-check-dump。 - -# 导航 - -[目录](README.md) - -上一章:[9. 数据库](ch9.md) - -下一章:[11. AOF持久化](ch11.md) - diff --git a/数据库/Redis/06 整数集合.md b/数据库/Redis/06 整数集合.md deleted file mode 100644 index fe18883c..00000000 --- a/数据库/Redis/06 整数集合.md +++ /dev/null @@ -1,66 +0,0 @@ -整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且数量不多时,Redis采用整数集合作为集合键的底层实现。 - -# 6.1 整数集合的实现 - -整数集合,可以保存int16\_t、int32\_t或者int64\_t的整数值,且元素不重复,intset.h/intset结构表示一个整数集合: - -```c -typedef struct intset { - uint32_t encoding; // 决定contents保存的真正类型 - uint32_t length; - int8_t contents[]; // 各项从小到大排序 -} inset; -``` - -![five-int16](img/chap6/five-int16.png) - -上图中,contents数组的大小为sizeof(int16\_t) * 5 = 80位。 - -# 6.2 升级 - -每当添加一个新元素到整数集合中,且新元素的类型比现有所有元素的类型都要长时,整数集合需要先升级(update),然后才能添加新元素: - -1. 根据新元素的类型,扩展底层数组的空间大小,并未新元素分配空间。 -2. 将底层数组现有元素转换成与新元素相同的类型,并放置在正确的位置上(从后向前遍历)。放置过程中,维持底层数组的有序性质不变。 -3. 将新元素添加到底层数组里。 - -因为每次升级都可能对所有元素进行类型转换,所以复杂度为*O(N)*。 - -PS. 因为引发升级的新元素长度比当前元素都大,所以它的值要么大于当前所有元素,要么就小于。前种情况放置在底层数组的末尾,后种情况放置在头部。 - -# 6.3 升级的好处 - -升级有两个好处 - -1. 提升整数集合的灵活性 - - 我们可以随意地将int16\_t、int32\_t添加到集合中,不必担心出现类型错误,毕竟C是个静态语言。 - -2. 尽可能解约内存 - - 避免用一个int64\_t的数组包含所有元素 - -# 6.4 降级 - -**整数集合不支持降级**。 - -# 6.5 整数集合API - -| 函数 | 作用 | 时间复杂度 | -| ------------- | ---------- | ------------------ | -| intsetNew | 创建一个新的整数集合 | O(1) | -| intsetAdd | 添加指定元素 | O(N) | -| intsetRemove | 移除指定元素 | O(N) | -| intsetFind | 检查给定值是否存在 | 因为底层数组有序,所以O(logN) | -| insetRandom | 随机返回一个元素 | O(1) | -| intsetGet | 返回给定索引上的元素 | O(1) | -| intsetLen | 返回元素个数 | O(1) | -| intsetBlobLen | 返回占用的内存字节数 | O(1) | - -# 导航 - -[目录](README.md) - -上一章:[5. 跳跃表](ch5.md) - -下一章:[7. 压缩列表](ch7.md) diff --git a/数据库/Redis/11 AOF持久化.md b/数据库/Redis/07 AOF持久化.md similarity index 85% rename from 数据库/Redis/11 AOF持久化.md rename to 数据库/Redis/07 AOF持久化.md index 16feabd7..5d109ac8 100644 --- a/数据库/Redis/11 AOF持久化.md +++ b/数据库/Redis/07 AOF持久化.md @@ -1,10 +1,17 @@ +# AOF持久化 + +## 1 AOF持久化方法 + +### 概念 AOF(Append Only File)持久化,与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF保存Redis所执行的写命令来记录数据库状态。被写入AOF文件的命令都是以Redis的命令请求协议格式保存的,纯文本格式,打开即可查看。 -# 11.1 AOF持久化的实现 +**AOF持久化的实现**三个步骤。 -AOF持久化功能的实现可分为命令追加(append)、文件写入、文件同步(sync)三个步骤。 +* 命令追加(append)、 +* 文件写入、 +* 文件同步(sync) -## 命令追加 +### 命令追加 如果打开AOF功能,服务器在执行完一个写命令后,会以协议格式将被执行的命令追加到服务器状态的`aof_buf`缓冲区的末尾。 @@ -16,7 +23,7 @@ struct redisServer { }; ``` -## AOF文件的写入与同步 +### 文件写入与同步 Redis的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接受客户端的请求,并向客户端发送回复,而时间事件则负责执行像`serverCron`函数这样的定时任务。 @@ -24,7 +31,7 @@ Redis的服务器进程就是一个事件循环(loop),这个循环中的 > flushAppendOnlyFile函数的行为由服务器配置的`appendfsync`选项的值来决定:always、everysec(默认)、no。 -# 11.2 AOF文件的载入与数据还原 +### AOF文件的载入与数据还原 服务器只要读入并重新执行一遍AOF文件中的写命令,就可以还原服务器关闭之前的数据库状态: @@ -33,11 +40,11 @@ Redis的服务器进程就是一个事件循环(loop),这个循环中的 3. 使用伪客户端执行被读出的命令 4. 一直执行步骤2和3,知道AOF文件中的所有命令都被处理完位置。 -# 11.3 AOF重写 +## 2 AOF重写 -为了解决AOF文件体积膨胀的问题,Redis提供了AOF重写功能。通过该功能,Redis可以创建一个新的AOF文件来替代现有的AOF文件,新文件不会包含荣誉命令,体积也会小很多。 +为了解决AOF文件体积膨胀的问题,Redis提供了AOF重写功能。通过该功能,Redis可以创建一个新的AOF文件来替代现有的AOF文件,新文件不会包含冗余命令,体积也会小很多。 -## 实现 +### 实现 AOF文件重写不需要对现有AOF文件做任何读取、分析或写入操作,而是通过读取服务器当前的数据库状态实现的。首先从数据库中读取现在的键,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令。这就是AOF重写的实现原理。 @@ -45,11 +52,3 @@ Redis服务器采用单个线程来处理命令请求,所以将AOF重写程序 1. 将AOF重写缓冲区的内容写入新AOF文件。 2. 对新的AOF文件改名,覆盖现有的AOF文件。 - -# 导航 - -[目录](README.md) - -上一章:[10. RDB持久化](ch10.md) - -下一章:[12. 事件](ch12.md) diff --git a/数据库/Redis/07 压缩列表.md b/数据库/Redis/07 压缩列表.md deleted file mode 100644 index 426d3042..00000000 --- a/数据库/Redis/07 压缩列表.md +++ /dev/null @@ -1,93 +0,0 @@ -压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表现,并且每个列表项要么就是小整数值,要么就是长度较短的字符串,那么Redis就会使用压缩列表来实现列表键。 - -当一个哈希键只包含少量键值对,并且每个键值对要么是小整数值,要么是长度较短的字符串,Redis就会使用压缩列表来实现哈希键。 - -# 7.1 压缩列表的构成 - -压缩列表是Redis为了节约内存而开发的,由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。一个压缩列表可以包含多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。 - -压缩列表的各组成部分: - -> zlbytes | zltail | zllen | entry1 | entry2 | … | entryN | zlend - -其中, - -| 属性 | 类型 | 长度 | 用途 | -| ------- | --------- | ---- | ---------------------------------------- | -| zlbytes | uint32\_t | 4字节 | 记录压缩列表占用的内存字节数:在内存重分配,或计算zlend的位置时使用 | -| zltail | uint32\_t | 4字节 | 记录表尾结点距离起始地址的字节数:通过这个偏移量,程序可以直接确定表尾结点的地址 | -| zllen | uint16\_t | 2字节 | 记录节点数量:但这个属性小于UINT16\_MAX(65535)时,这个属性的值就是节点的数量。如果等于UINT16\_MAX,节点的真实数量要遍历整个压缩列表才能得到 | -| entryX | 列表节点 | 不定 | 各个节点,节点的长度由保存的内容决定 | -| zlend | uint8\_t | 1字节 | 特殊值0xFF,标记压缩列表的尾端 | - -# 7.2 压缩列表节点的构成 - -压缩列表的节点可以保存一个字节数组或者一个整数值。压缩节点的各个组成部分: - -> previous_entry_length | encoding | content - -## previous_entry_length - -previous_entry_length以字节为单位,记录前一个节点的长度。previous_entry_length属性的长度可以是1字节或5字节: - -1. 若前一节点的长度小于254字节,那么previous_entry_length属性的长度就是1字节。前一节点的长度保存在其中。 -2. 若前一节点的长度大于254字节,那么previous_entry_length属性的长度就是5字节:其中属性的第一个字节被设置为0xFE(十进制254),而之后的四个字节则用于保存前一节点的长度。 - -程序可以通过指针运算,根据当前节点的起始地址来计算出前一个结点的起始地址。压缩列表的从尾向头遍历就是据此实现的。 - -## encoding - -节点的encoding记录了节点的content属性所保存的数据的类型和长度: - -- 1字节、2字节或者5字节长,值的最高位为00、01或10的是字节数组编码:这种编码表示节点的content保存的是字节数组,数组的长度由编码除去最高两位置后的其他位记录。 -- 1字节长。值的最高位以11开头的是整数编码:表示content保存着整数值,整数值的类型和长度由编码除去最高两位之后的其他位记录。 - -## content - -content保存节点的值,可以使字节数组或整数,值的类型和长度由encoding属性决定。 - -保存字节数组“hello world”的节点: - -| previoid_entry_length | encoding | content | -| --------------------- | -------- | ------------- | -| ... | 00001011 | "hello world" | - -保存整数10086的节点: - -| previoid_entry_length | encoding | content | -| --------------------- | -------- | ------- | -| ... | 11000000 | 10086 | - -# 7.3 连锁更新 - -因为previoid_entry_length的长度限制,添加或删除节点都有可能引发「连锁更新」。在最坏的情况下,需要执行*N*次重分配操作,而每次空间重分配的最坏复杂度是*O(N)*,合起来就是*O(N^2)*。 - -尽管如此,连锁更新造成性能问题的概率还是比较低的: - -1. 压缩列表里有多个连续的、长度介于250和253字节之间的节点,连锁更新才有可能触发。 -2. 即使出现连锁更新,只要需要更新的节点数量不多,性能也不会受影响。 - -# 7.4 压缩列表API - -| 函数 | 作用 | 复杂度 | -| ------------------ | ---------------------- | ---------------------------------------- | -| ziplistNew | 创建新的压缩列表 | O(1) | -| ziplistPush | 创建一个包含给定值的新节点,并添加到表头或尾 | 平均O(N),最坏O(N^2) | -| ziplistInsert | 将包含给定值的新节点插入到给定节点之后 | 平均O(N),最坏O(N^2) | -| ziplistIndex | 返回给定索引上的节点 | O(N) | -| ziplistFind | 查找并返回给定值的节点 | 因为节点的值可能是一个数组,所以检查节点值和给定值是否相同的复杂度为O(N),查找整个列表的复杂度为O(N^2) | -| ziplistNext | 返回给定节点的下一个节点 | O(1) | -| ziplistPrev | 返回给定节点的前一个节点 | O(1) | -| ziplistGet | 获取给定节点所保存的值 | O(1) | -| ziplistDelete | 删除给定节点 | 平均O(N),最坏O(N^2) | -| ziplistDeleteRange | 删除在给定索引上的连续多个节点 | 平均O(N),最坏O(N^2) | -| ziplistBlobLen | 返回压缩列表占用的内存字节数 | O(1) | -| ziplistLen | 返回包含的节点数量 | 节点数量小于65535时为O(1),否则为O(N) | - -# 导航 - -[目录](README.md) - -上一章:[6. 整数集合](ch6.md) - -下一章:[8. 对象](ch8.md) diff --git a/数据库/Redis/19 事务.md b/数据库/Redis/08 事务.md similarity index 87% rename from 数据库/Redis/19 事务.md rename to 数据库/Redis/08 事务.md index fa37a762..0d3f9561 100644 --- a/数据库/Redis/19 事务.md +++ b/数据库/Redis/08 事务.md @@ -1,33 +1,20 @@ +# 事务 + +## 1 事务简介 + Redis通过`MULTI`、`EXEC`、`WATCH`等命令实现事务(transaction)功能。事务提供一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制。在事务执行期间,服务器不会中断事务去执行其他客户端的命令请求。 事务以`MULTI`开始,接着是多个命令放入事务之中,最后由`EXEC`将这个事务提交(commit)到服务器执行。 -# 19.1 事务的实现 -一个事务从开始到结束经历三个阶段: +一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。 -1. 事务开始 -2. 命令入队 -3. 事务执行 +事务中的多个命令被一次性发送给服务器,而不是一条一条发送,这种方式被称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。 -## 事务开始 +Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。 -`MULTI`命令标志着事务的开始,它将客户端从非事务状态切换到事务状态,即打开客户端状态的`flags`属性的`REDIS_MULTI`标识: -```python -def MULTI(): - client.flags |= REDIS_MULTI - replyOK() -``` - -## 命令入队 - -客户端切换到事务状态后,服务器会根据不同的命令执行不同的操作: - -- `EXEC`、`DISCARD`、`WATCH`、`MULTI`其中一个,服务器立即执行该命令。 -- 否则,服务器将命令放入一个事务队列,然后向客户端返回`QUEUED`回复。 - -## 事务队列 +### 事务队列 每个Redis客户端都有自己的事务状态,保存在客户端状态的`mstate`属性中: @@ -55,8 +42,33 @@ typedef struct multiCmd { struct redisCommand *cmd; } multiCmd; ``` +### 事务实现 -## 执行事务 +一个事务从开始到结束经历三个阶段: + +1. 事务开始 +2. 命令入队 +3. 事务执行 + +### 事务开始 + +`MULTI`命令标志着事务的开始,它将客户端从非事务状态切换到事务状态,即打开客户端状态的`flags`属性的`REDIS_MULTI`标识: + +``` +def MULTI(): + client.flags |= REDIS_MULTI + replyOK() +``` + +### 命令入队 + +客户端切换到事务状态后,服务器会根据不同的命令执行不同的操作: + +- `EXEC`、`DISCARD`、`WATCH`、`MULTI`其中一个,服务器立即执行该命令。 +- 否则,服务器将命令放入一个事务队列,然后向客户端返回 `QUEUED` 回复。 + + +### 执行事务 服务器收到`EXEC`命令后,会遍历客户端的事务列表,执行其中的所有命令。最后将执行所得的结果返回给客户端。 @@ -82,11 +94,11 @@ def EXEC(): send_reply_to_client(client, reply_queue) ``` -# 19.2 WATCH 命令的实现 +## 2 WATCH命令的实现 `WATCH`命令是个乐观锁,它可以再`EXEC`执行之前,监视任意数量的数据库键,并在`EXEC`执行时,检查被监视的键是否至少有一个已经被修改过了。如果是,服务器将拒绝执行事务,并返回客户端事务执行失败的空回复。 -## 使用 WATCH 命令监视数据库键 +### 使用 WATCH 命令监视数据库键 每个Redis数据库都保存了一个`watched_keys`字典,键是某个被`WATCH`的数据库键,值是一个链表,记录了所有监视该键的客户端: @@ -96,49 +108,46 @@ typedef struct redisDb { } redisDb; ``` -## 监视机制的触发 +### 监视机制的触发 所有对数据库进行修改的命令,执行之后都会调用`multi.h/touchWatchKey`函数对`watched_keys`字典进行检查。如果被监视的键被修改,那么打开监视该键的客户端的`REDIS_DIRTY_CAS`标识,表示该客户端的事务安全性已遭破坏。 -## 判断事务是否安全 +### 判断事务是否安全 服务器收到`EXEC`命令后,根据这个客户端是否打开了`REDIS_DIRTY_CAS`标识来决定是否执行事务。 -# 19.3 事务的ACID性质 +## 3 事务的ACID性质 Redis的事务总是具有原子性(atomicity)、一致性(consistency)、隔离性(isolation),且当Redis运行在某种特定的持久化模式下,事务也具有耐久性(durability)。 -## 原子性 +### 原子性 事务的原子性是指,事务中的多个操作当做一个整体来执行,要么执行所有,要么一个也不执行。 Redis的事务与传统关系型数据库事务的区别在于,Redis不支持事务的回滚机制(rollback),即使事务队列中的某个命令执行出现错误,整个事务也会继续执行下去,直到所有命令执行完毕。 -## 一致性 +### 一致性 事务的一致性是指,如果数据库在事务执行前是一致的,那么执行后,无论事务是否执行成功,数据库也应该是一致的。「一致」是数据符合数据库本身的定义和要求,没有包含非法或无效的错误数据。 Redis通过谨慎的错误检测和简单的设计来保证事务的一致性。 -1. 入队错误 - +1. 入队错误。 如果事务在入队命令的过程中,出现了命令不存在,或者命令格式不正确等情况,Redis会拒绝执行该事务。 -2. 执行错误 - +2. 执行错误。 执行过程中的错误是不能再入队时被服务器发现的,这些错误只会在命令实际执行时被触发。事务的执行过程中出现错误,服务器也不会中断事务的执行,而是继续执行其他命令,一致性的命令不会被出错的命令影响。 -3. 服务器停机 - +3. 服务器停机。 执行事务的过程中停机,不管服务器使用的何种持久化模式,Redis总能保持重启后的数据库一致性。 -## 隔离性 +### 隔离性 事务的隔离性是指,即使数据库中有多个事务并发执行,各个事务之间不会相互影响,且与串行执行的结果相同。 Redis采用单线程执行事务,所以事务总是以串行的方式执行,也当然具有隔离性。 -## 持久性 +### 持久性 事务的持久性是指,一个事务执行完毕后,结果已经被保存到永久性存储介质中。即使服务器停机,执行事务所得的结果也不会丢失。 @@ -149,11 +158,3 @@ Redis没有为事务提供额外的持久化功能,事务的持久化由Redis - AOF持久化,且`appendfsync`选项为`always`时:程序执行命令后会调用同步操作,将命令数据保存到硬盘。这时事务是有持久性的。 - AOF持久化,且`appendfsync`选项为`everysec`时:每秒一次同步命令数据到硬盘,事务也不具有持久性。 - AOF持久化,且`appendfsync`选项为`no`时:程序交由操作系统来决定何时同步到硬盘,事务也不具有持久性。 - -# 导航 - -[目录](README.md) - -上一章:[18. 发布与订阅](ch18.md) - -下一章:[20. Lua脚本](ch20.md) diff --git a/数据库/Redis/12 事件.md b/数据库/Redis/09 事件.md similarity index 76% rename from 数据库/Redis/12 事件.md rename to 数据库/Redis/09 事件.md index 35bae8aa..32961685 100644 --- a/数据库/Redis/12 事件.md +++ b/数据库/Redis/09 事件.md @@ -1,39 +1,35 @@ +# 事件 + +## 0 概述 Redis服务器是一个事件驱动程序,需要处理以下两类事件: - 文件事件(file event):Redis服务器通过socket与客户端连接,文件事件就是对套接字操作的对象。服务器与客户端的通信会产生相应的文件事件,服务器监听并处理这些事件来完成一系列的网络通信操作。 - 时间事件(time event):Redis服务器的一些操作(如`serverCron`函数)需要在特定时间点执行,时间事件就是对这类定时任务的抽象。 -# 12. 1 文件事件 - +## 1 文件事件 +### 原理 Redis基于Reactor模式开发了自己的网络事件处理器,称为『文件事件处理器』,文件事件处理器以单线程方式运行。 +Redis 基于 Reactor 模式开发了自己的网络事件处理器,使用 I/O 多路复用程序来同时监听多个套接字,并将到达的事件传送给文件事件分派器,分派器会根据套接字产生的事件类型调用相应的事件处理器。 + ![](img/chap12/img0.png) 文件事件处理器的四个组成部分: -- 套接字。 - - 当被监听的套接字准备好执行accept、read、write、close等操作时,与操作相对应的文件事件就会产生。 - -- I/O多路复用程序。 - - 使用I/O多路复用程序同时监听多个套接字,并向文件分派器传送那些产生了事件的套接字(使用队列)。 - -- 文件事件分派器 - - 根据套接字的事件类型,调用相应的事件处理器。 - +- 套接字。当被监听的套接字准备好执行accept、read、write、close等操作时,与操作相对应的文件事件就会产生。 +- I/O多路复用程序。使用I/O多路复用程序同时监听多个套接字,并向文件分派器传送那些产生了事件的套接字(使用队列)。 +- 文件事件分派器。根据套接字的事件类型,调用相应的事件处理器。 - 事件处理器 -## I/O多路复用程序的实现 +### 实现 -Redis的I/O多路复用包装了常见的select、poll、evport和kqueue等函数库来实现的,每个函数库的在Redis源码中都有一个独立的文件。 +* Redis的I/O多路复用包装了常见的select、poll、evport和kqueue等函数库来实现的,每个函数库的在Redis源码中都有一个独立的文件。 -## 事件的类型 +**事件的类型**: -I/O多路复用程序可以监听多个套接字的ae.h/AE_READABLE和ae.h/AE_WRITABLE事件。两种事件可以同时监听,但会优先处理AE_READABLE事件。 +* I/O多路复用程序可以监听多个套接字的ae.h/AE_READABLE和ae.h/AE_WRITABLE事件。两种事件可以同时监听,但会优先处理AE_READABLE事件。 -## API +### API | 函数 | 参数 | 作用 | | ---------------------- | ----------------- | ---------------------------------------- | @@ -45,7 +41,7 @@ I/O多路复用程序可以监听多个套接字的ae.h/AE_READABLE和ae.h/AE_WR | ae.c/aeProcessEvents | | 先调用aeApiPoll来等待事件,然后遍历所有事件,调用相应的事件处理器 | | ae.c/aeGetApiName | | 返回I/O多路复用程序底层使用的函数库名称:epoll、select等 | -## 文件事件的处理器 +### 文件事件的处理器 Redis为文件事件编写了多个处理器,分别用于实现不同的网络通信需求: @@ -63,8 +59,8 @@ Redis为文件事件编写了多个处理器,分别用于实现不同的网络 - 复制处理器:主从服务器的复制操作。 -# 12.2 时间事件 - +## 2 时间事件 +## 原理 Redis的时间事件分为两类: - 定时事件:在指定一段时间后执行一次。 @@ -81,11 +77,11 @@ Redis的时间事件分为两类: - 返回ae.h/AE\_NOMORE就是定时事件,到达一次后就删除 - 返回非AE\_NOMORE的整数值就是周期性事件,事件到达后,根据返回值对when属性进行更新。 -## 实现 +### 实现 服务器的所有时间事件存放在一个无序链表(*不按when属性排序*)中,每当时间事件处理器运行时,遍历整个链表,找到已到达的事件,调用相应的事件处理器。 -## API +### API | 函数 | 参数 | 作用 | | ------------------------- | ----------- | ---------------- | @@ -94,7 +90,7 @@ Redis的时间事件分为两类: | ae.c/aeSearchNearestTimer | | 返回到达时间最近的事件 | | ae.c/processTimeEvents | | 时间事件的执行器,遍历并调用事件 | -## serverCron函数 +### serverCron函数 serverCron函数的工作包括: @@ -105,7 +101,7 @@ serverCron函数的工作包括: - 如果是主服务器,对从服务器定期同步 - 如果是集群模式,对集群进行同步和测试连接 -# 12.3 事件的调度与执行 +## 3 事件的调度与执行 调度和执行由ae.c/aeProcessEvents函数负责。 @@ -136,6 +132,7 @@ def aeProcessEvents(): ``` + 调度和执行的规则如下: - aeApiPoll函数的最大阻塞时间由到达时间最接近当前时间的事件决定,避免服务器的频繁轮询。 @@ -143,10 +140,21 @@ def aeProcessEvents(): - 对事件的处理都是同步、有序、原子地执行。不会中断、抢占事件处理。 - 时间事件的处理时间,通常比其设定的到达时间晚一些。 -# 导航 -[目录](README.md) -上一章:[11. AOF持久化](ch11.md) +将 aeProcessEvents 函数置于一个循环里面,加上初始化和清理函数,就构成了 Redis 服务器的主函数,伪代码如下: -下一章:[13. 客户端](ch13.md) +```python +def main(): + # 初始化服务器 + init_server() + # 一直处理事件,直到服务器关闭为止 + while server_is_not_shutdown(): + aeProcessEvents() + # 服务器关闭,执行清理操作 + clean_server() +``` + +从事件处理的角度来看,服务器运行流程如下: + +![](image/2021-09-03-00-19-56.png) \ No newline at end of file diff --git a/数据库/Redis/15 复制.md b/数据库/Redis/10 复制.md similarity index 74% rename from 数据库/Redis/15 复制.md rename to 数据库/Redis/10 复制.md index 1f5a4f68..30ea5bdc 100644 --- a/数据库/Redis/15 复制.md +++ b/数据库/Redis/10 复制.md @@ -1,15 +1,21 @@ -Redis中,用户可以执行`SAVEOF`命令或设置`saveof`选项,让一个服务器去复制(replicate)另一个服务器。被复制的服务器叫做master,对master进行复制的服务器叫做slave。 +# 复制 + +## 0 概述 +Redis中,用户可以执行`saveof`命令或设置`saveof`选项,让一个服务器去复制(replicate)另一个服务器。被复制的服务器叫做master,对master进行复制的服务器叫做slave。 进行复制中的master和slave应该保存相同的数据,这称作“数据库状态一致”。 -## 15.1 旧版复制功能的实现 + +一个从服务器只能有一个主服务器,并且不支持主主复制。 + +## 1 旧版复制功能的实现 Redis的复制功能分为同步(sync)和命令传播(command propagate)两个操作: - 同步用于将slave的数据库状态更新至master当前所处的数据库状态。 - 命令传播用于master的数据块状态被修改,导致和lsave的数据库状态不一致时,让两者的数据库重回一致状态。 -## 同步 +### 同步 复制开始时,slave会先执行同步操作,步骤如下: @@ -18,11 +24,11 @@ Redis的复制功能分为同步(sync)和命令传播(command propagate) - master的`BGSAVE`执行完毕后,将生成的RDB文件发送给slave,slave接收并载入这个RDB,更新自己的数据库状态 - master将记录在缓冲区中的所有写命令发送给slave,后者执行这些操作,再次更新自己的数据库状态 -## 命令传播 +### 命令传播 同步完成后,主从服务器的一致状态仍有可能改变,每当master执行写命令时,主从服务器的状态就会不一致。为此,master执行写命令,并将其发送给slave一并执行。 -# 15.2 旧版复制功能的缺陷 +### 旧版复制功能的缺陷 Redis的复制可以分为两种情况: @@ -31,22 +37,17 @@ Redis的复制可以分为两种情况: 对于初次复制,旧版复制功能可以很好完成。但是断线后复制,效率却很低,因为重连后会浪费一次`SYNC`操作。 -# 15.3 新版复制功能的实现 +## 2 新版复制功能的实现 为了解决旧版复制功能在断线后的低效问题,Redis从2.8之后,使用`PSYNC`代替`SYNC`执行复制时的同步操作。`PSYNC`具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式: - 完整重同步用于处理初次复制,执行步骤和`SYNC`命令基本一样。 -- 部分重同步用于处理断线后重复制,重连后,如果条件允许,master可以将断开期间的谢明令发送给slave执行。 +- 部分重同步用于处理断线后重复制,重连后,如果条件允许,master可以将断开期间的谢明令发送给slave执行。部分重同步功能有三个部分组成: + - master和slave的复制偏移量(replication offset) + - master的复制积压缓冲区(replication backlog) + - 服务器的运行ID(run ID) -# 15.4 部分重同步的实现 - -部分重同步功能有三个部分组成: - -- master和slave的复制偏移量(replication offset) -- master的复制积压缓冲区(replication backlog) -- 服务器的运行ID(run ID) - -## 复制偏移量 +### 复制偏移量 master和slave分别维护一个复制偏移量: @@ -55,7 +56,7 @@ master和slave分别维护一个复制偏移量: 对比两者的复制偏移量,就知道它们是否处于一致状态。 -## 复制积压缓冲区 +### 复制积压缓冲区 复制积压缓冲区是master维护的一个固定长度的FIFO队列,默认大小为1MB。当服务器进行命令传播时,不仅会将命令发送给所有slave,还会入队到积压缓冲区。因此,积压缓冲区保存了最近被传播的写命令,且为队列中的每个字节记录相应的复制偏移量。 @@ -64,7 +65,7 @@ slave重连上master时,slave通过`PSYNC`将自己的复制偏移量offset发 - 如果offset之后的数据仍在复制积压缓冲区中,执行部分重同步操作。 - 否则,执行完整重同步操作。 -## 服务器运行ID +### 服务器运行ID 部分重同步还要用到服务器运行ID,主从服务器都有自己的ID。初次复制时,master将自己的ID传给slave,后者将其保存。 @@ -73,7 +74,7 @@ slave重连上master时,slave通过`PSYNC`将自己的复制偏移量offset发 - master发现接收的ID和自己的相同,那么说明断线之前复制的就是自己,继续执行部分重同步。 - 如果不同,完整重同步啦! -# 15.5 PSYNC命令的实现 +### PSYNC命令的实现 `PSYNC`的调用方式有两种: @@ -82,11 +83,13 @@ slave重连上master时,slave通过`PSYNC`将自己的复制偏移量offset发 ![](img/chap15/img0.png) -# 15.6 复制的实现 +## 3 复制的实现 通过向slave发送`SLAVEOF`命令,可以让slave复制master -## 步骤1:设置master的地址和端口 + + +### 步骤1:设置master的地址和端口 命令`slave 127.0.0.1 6379`会设置服务器状态的以下两个属性: @@ -97,24 +100,24 @@ struct redisServer { }; ``` -## 步骤2:建立套接字连接 +### 步骤2:建立套接字连接 如果slave的套接字能成功连接到master,那么slave会为这个套接字关联一个专门用于处理复制工作的文件事件处理器,它将负责处理后续的复制工作。 master接收到客户端的套接字连接之后,为其创建相应的客户端状态,这时slave同时有server和client两个身份。 -## 步骤3:发送PING命令 +### 步骤3:发送PING命令 slave成为master的客户端之后,紧接着就向其发送`PING`命令,那么: ![](img/chap15/img1.png) -## 步骤4:身份验证 +### 步骤4:身份验证 收到master的“PONG”回复后,slave要检查自己的`masterauth`选项决定是否进行身份验证。如果需要验证,slave会向master发送一条`AUTH`命令,参数为`masterauth`选项的值,接下来: ![](img/chap15/img2.png) -## 步骤5:发送端口信息 +### 步骤5:发送端口信息 身份验证之后,slave将执行`REPLCONF listening-port `,向master发送slave的监听端口号。master收到后,会将端口号放到客户端状态的`slave_listening_por`t属性中该属性的唯一作用就是master执行`INFO replication`命令时打印slave的端口号。 @@ -124,32 +127,35 @@ typdef struct redisClient { } redisClient; ``` -## 步骤6:同步 +### 步骤6:同步 这一步,slave发送`PSYNC`,执行同步操作。执行同步之后,master也成了slave的客户端,master发送写命令来改变slave的数据库状态。 -## 步骤7:命令传播 +### 步骤7:命令传播 完成同步之后,主从服务器就进入命令传播阶段,master将自己执行写命令发送给slave,slave接到后就执行,这样两者的状态就一直保持一致了。 -# 15.7 心跳检测 +## 4 其他内容 + +### 心跳检测 命令传播阶段,slave默认每秒给master发送一次命令:`REPLCONF ACK `,其中replication_offset对应当前slave的复制偏移量。该命令有三个作用: - 检测网络连接状态 +- 辅助实现min-slaves选项。该选项防止master在不安全的情况下执行写命令,比如slave数量小于3的时候。 +- 检测命令丢失。这个根据复制偏移量来判断,如果两者不一致,master就会把复制积压缓冲区的命令重新发送。 -- 辅助实现min-slaves选项 - 该选项防止master在不安全的情况下执行写命令,比如slave数量小于3的时候。 +### 连接过程 -- 检测命令丢失 +1. 主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令; - 这个根据复制偏移量来判断,如果两者不一致,master就会把复制积压缓冲区的命令重新发送。 +2. 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令; -# 导航 +3. 主服务器每执行一次写命令,就向从服务器发送相同的写命令。 -[目录](README.md) +### 主从链 -上一章:[14. 服务器](ch14.md) +随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。 -下一章:[16. Sentinel](ch16.md) +![](image/2021-09-03-00-42-41.png) \ No newline at end of file diff --git a/数据库/Redis/16 Sentinel.md b/数据库/Redis/11 Sentinel.md similarity index 93% rename from 数据库/Redis/16 Sentinel.md rename to 数据库/Redis/11 Sentinel.md index 0d5f1b09..7a166d09 100644 --- a/数据库/Redis/16 Sentinel.md +++ b/数据库/Redis/11 Sentinel.md @@ -1,6 +1,10 @@ +# Sentinel + Sentinel(哨兵)是Redis的高可用性解决方案,由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个master以及属下的所有slave。Sentinel在被监视的master下线后,自动将其属下的某个slave升级为新的master,然后由新的master继续处理命令请求。 -# 16.1 启动并初始化Sentinel +## 1 Sentinel初始化过程 + +### 启动并初始化Sentinel 启动一个Sentinel可以使用命令: @@ -18,7 +22,7 @@ Sentinel(哨兵)是Redis的高可用性解决方案,由一个或多个Sent 4. 根据配置文件,初始化监视的master列表 5. 创建与master的网络连接 -## 初始化服务器 +### 初始化服务器 Sentinel本质上是一个运行在特殊模式下的Redis服务器,它的初始化过程与普通Redis服务器并不相同: @@ -33,11 +37,11 @@ Sentinel本质上是一个运行在特殊模式下的Redis服务器,它的初 | 文件事件处理器(发送命令请求,处理命令回复) | Sentinel内部使用 | | 时间事件处理器 | Sentinel内部使用,`serverCron`会用`sentinel.c/sentinelTimer`函数 | -## 使用Sentinel专用代码 +### 使用Sentinel专用代码 将一部分普通Redis服务器的代码替换为Sentinel专用代码,比如端口号,命令表。 -## 初始化Sentinel状态 +### 初始化Sentinel状态 接下来,服务器会初始化一个`sentinel.c/sentinelState`结构,它保存了服务器有关Sentinel的状态: @@ -66,7 +70,7 @@ struct sentinelState { } sentinel; ``` -## 初始化Sentinel状态的masters属性 +### 初始化Sentinel状态的masters属性 sentinelRedisInstance结构代表一个被监视的Redis服务器实例,可以是master、slave、或者另一个Sentinel。 @@ -115,21 +119,23 @@ typedef struct sentinelAddr { } sentinelAddr; ``` -## 创建与master的网络连接 +### 创建与master的网络连接 连接建立后,Sentinel将成为master的客户端,可以向其发送命令。对于被监视的master来说,Sentinel会创建两个异步网络连接: - 命令连接,用于发送和接收命令。 - 订阅连接。用于订阅master的`__sentinel__:hello`频道。 -# 16.2 获取master信息 + +## 2 信息传递过程 +### 获取master信息 Sentinel以默认10秒一次的频率,向master发送`INFO`命令,获取其当前信息: - master本身的信息,包括运行ID、role等。据此,Sentinel更新master实例的结构。 - master的slave信息。据此,Sentinel更新master实例的slaves字典。 -# 16.3 获取slave信息 +### 获取slave信息 Sentinel发现master有新的slave时,除了会为这个slave创建相应的实例结构外,还会创建到它的命令连接和订阅连接。 @@ -142,7 +148,7 @@ Sentinel发现master有新的slave时,除了会为这个slave创建相应的 - slave的优先级 - slave的复制偏移量 -# 16.4 向master和slave发送信息 +### 向master和slave发送信息 默认情况下,Sentinel会以两秒一次的频率,通过命令连接向所有被监视的master和slave发送: @@ -150,7 +156,7 @@ Sentinel发现master有新的slave时,除了会为这个slave创建相应的 其中以s\_开头的参数表示Sentinel本身的信息,m\_开头的参数是master的信息。如果Sentinel正在监视的是slave,那就是slave正在复制的master信息。 -# 16.5 接收来自master和slave的频道信息 +### 接收来自master和slave的频道信息 当Sentinel与一个master或slave建立订阅连接后,会向服务器发送以下命令: @@ -160,7 +166,7 @@ Sentinel对\_\_sentinel\_\_:hello频道的订阅会持续到两者的连接断 对于监视同一个server的多个Sentinel来说,一个Sentinel发送的信息会被其他Sentinel收到。这些信息用于更新其他Sentinel队发送信息Sentinel和被监视Server的认知。 -## 更新sentinels字典 +### 更新sentinels字典 Sentinel为master创建的实力结构中,有sentinels字典保存了其他监视这个master的Sentinel: @@ -174,11 +180,12 @@ Sentinel为master创建的实力结构中,有sentinels字典保存了其他监 根据提取的参数,目标Sentinel会在自己的Sentinel状态中更新sentinels和masters字典。 -## 创建连向其他Sentinel的命令连接 +### 创建连向其他Sentinel的命令连接 Sentinel通过频道信息发现一个新的Sentinel时,不仅会为其创建新的实例结构,还会创建一个连向新Sentinel的命令连接,新的Sentinel也会创建连向这个Sentinel的命令连接,最终,监视同一master的多个Sentinel成为相互连接的网络。各个Sentinel可以通过发送命令请求来交换信息。 -# 16.6 检测主观下线状态 +## 3 Sentinel检测状态 +### 检测主观下线状态 默认情况下,Sentinel会每秒一次地向所有与它创建了嘛命令连接的实例(master、slave、其他sentinel)发送`PING`命令,并通过回复来判断其是否在线。只有+PONG/-LOADING/-MASERDOWN三种有效回复。 @@ -186,11 +193,11 @@ Sentinel的配置文件中`down-after-milliseconds`选项指定了判断实例 注意:多个Sentinel设置的`down-after-milliseconds`可能不同。 -# 16.7 检查客观下线时长 +### 检查客观下线时长 当Sentinel将一个master判断为主观下线后,为了确认是真的下线,会向监视这一master的其他Sentinel询问。有足够数量(quorum)的已下线判断后,Sentinel会将master判定为客观下线,并对master执行故障转移。 -# 16.8 选举领头Sentinel +### 选举领头Sentinel master被判定为客观下线后,监视这个master的所有Sentinel会进行协商,选举一个领头Sentinel,并由其对该master执行故障转移。选举的规则如下: @@ -206,7 +213,7 @@ master被判定为客观下线后,监视这个master的所有Sentinel会进行 - 因为领头Sentinel需要半数以上的支持,且每个Sentinel在每个配置纪元里只设置一次局部领头,所以一个配置纪元里,只能有一个领头。 - 如果给定时限内,没有产生领头Sentinel,那么各个Sentinel过段时间再次选举,知道选出领头为止。 -# 16.8 故障转移 +### 故障转移 领头Sentinel会对已下线的master执行故障转移,包括以下三个步骤: @@ -214,7 +221,7 @@ master被判定为客观下线后,监视这个master的所有Sentinel会进行 - 让已下线master属下的所有slave改为新复制新的master。 - 让已下线master成为新master的slave,重新上线后就是新slave。 -## 选出新的master +### 选出新的master 新master的挑选规则: @@ -226,19 +233,11 @@ master被判定为客观下线后,监视这个master的所有Sentinel会进行 Sentinel向salve发送`SLAVEOF no one`命令将其转换为master。 -## 修改salve的复制目标 +### 修改salve的复制目标 同样通过`SLAVEOF`命令实现。 -## 将旧的master变为slave +### 将旧的master变为slave 同样通过`SLAVEOF`命令实现。 - -# 导航 - -[目录](README.md) - -上一章:[15. 复制](ch15.md) - -下一章:[17.集群](ch17.md) diff --git a/数据库/Redis/17 集群.md b/数据库/Redis/12 集群与分片.md similarity index 91% rename from 数据库/Redis/17 集群.md rename to 数据库/Redis/12 集群与分片.md index 301dcbaa..70cd5ee1 100644 --- a/数据库/Redis/17 集群.md +++ b/数据库/Redis/12 集群与分片.md @@ -1,12 +1,16 @@ +# 集群分片 + +## 0 概述 + Redis集群是分布式的数据库方案,通过分片(sharing)来进行数据共享,并提供复制或故障转移功能。 -# 17.1 节点 +## 1 节点 一个Redis集群通常由多个节点(node)组成。开始时每个node都是独立的,要将其连接起来: > CLUSTER MEET -## 启动节点 +### 启动节点 一个节点就是运行在集群模式下的Redis服务器,根据`cluster-endabled`配置选项是否为yes来决定是否开启集群模式。 @@ -22,7 +26,7 @@ Redis集群是分布式的数据库方案,通过分片(sharing)来进行 节点会继续使用`redisServer`结构保存服务器的状态,`redisClient`结构保存客户端的状态,集群模式下的数据,保存在`cluster.h/clusterNode`、`cluster.h/clusterLink`、`cluster.h/clusterState`结构中。 -## 集群数据结构 +### 集群数据结构 `cluster.h/clusterNode`保存了一个节点的当前状态,如节点的创建时间、名字、配置纪元、IP和端口等。每个节点都有一个自己的`clusterNode`结构,并为集群中的其它节点创建一个相应的`clusterNode`结构。`clusterNode`结构的`link`属性是一个`clusterLink`结构,保存了连接节点所需的有关信息,如套接字、缓冲区。 @@ -84,7 +88,7 @@ typedef struct clusterState { } clusterState; ``` -## CLUSTER MEET命令的实现 +### CLUSTER MEET命令的实现 通过向节点A发送`CLUSTER MEET`命令,客户端可以让接受命令的节点A将另一个节点B接入到A所在的集群中。 @@ -100,7 +104,7 @@ typedef struct clusterState { ![](img/chap17/img0.png) -# 17.2 槽指派 +## 2 槽指派 Redis集群通过分片的方式保存数据库中的键值对:集群中的整个数据库被分为16384个槽(slot),数据库中的每个键都属于其中的一个,集群中的每个节点可以处理0个或最多16384个槽。 @@ -110,7 +114,7 @@ Redis集群通过分片的方式保存数据库中的键值对:集群中的整 > CLUSTER ADDSLOTS [slot ...] -## 记录节点的槽指派信息 +### 记录节点的槽指派信息 ```c struct clusterNode { @@ -121,7 +125,7 @@ struct clusterNode { `slots`数组中的索引`i`上的二进制位的值来判断节点是否负责处理槽`i`。`numslots`记录节点负责处理的槽的数量,即`slots`数组中二进制1的数量。 -## 传播节点的槽指派信息 +### 传播节点的槽指派信息 一个节点除了会将自己处理的槽记录在`clusterNode`结构中的`slots`和`numslots`属性之外,还会将自己的`slots`数组通过消息发送给集群中的其它节点。 @@ -129,7 +133,7 @@ struct clusterNode { 最终,集群中的每个节点都知道数据库中的16384个槽分别被指派给了哪些节点。 -## 记录集群所有槽的指派信息 +### 记录集群所有槽的指派信息 `clusterState`结构中的`slots`数组记录了所有16384个槽的指派信息: @@ -145,7 +149,7 @@ typedef struct clusterState { - 传播节点的槽指派信息时,只需要发送`clusterNode.slots`即可。 -## CLUSTER ADDSLOTS命令的实现 +### CLUSTER ADDSLOTS命令的实现 `CLUSTER ADDSLOTS`命令接受一个或多个槽作为参数,并将所有输入的槽指派给接收该命令的节点负责: @@ -170,14 +174,14 @@ def CLUSTER_ADDSLOTS(*all_input_slots): ``` -# 17.3 在集群中执行命令 +## 3 在集群中执行命令 客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的键属于哪个槽,并检查这个槽是否被指派给了自己: - 如果指派给了自己,节点直接执行命令。 - 否则,节点向客户端返回一个`MOVED`错误,指引客户端转向(redirect)到正确的节点,再次发送命令。 -## 计算键属于哪个槽 +### 计算键属于哪个槽 ```python def slot_number(key): @@ -186,11 +190,11 @@ def slot_number(key): 使用`CLUSTER KEYSLOT `能查看键属于哪个槽。 -## 判断槽是否由当前节点负责处理 +### 判断槽是否由当前节点负责处理 节点计算出键所属的槽`i`之后,会检查自己在`clusterState.slots`数组中的第`i`项,判断键所在的槽是不是自己负责。 -## MOVED错误 +### MOVED错误 `MOVED`错误的格式为: @@ -198,7 +202,7 @@ def slot_number(key): 客户端通常会与集群中的多个节点创建套接字连接,所谓的节点转向就是换一个套接字来发送命令。 -## 节点数据库的实现 +### 节点数据库的实现 节点与单击服务器的一个区别是:节点只能使用0号数据库。 @@ -217,7 +221,7 @@ typdef struct clusterState { 通过在`slots_to_keys`中记录各个数据库键所属的槽,节点可以很方便地对属于某个槽的键进行批量操作,如`CLUSTER GETKEYINSLOT `。 -# 17.4 重新分片 +## 4 重新分片 Redis集群的重新分片指的是将任意数量已经指派给某个节点的槽改为指派给另一个节点,且相关槽所属的键也从源节点移动到目标节点。重新分片可以在线(online)进行,分片过程中,集群不需要下线,且源节点和目标节点都可以继续处理命令请求。 @@ -232,7 +236,7 @@ Redis集群的重新分片指的是将任意数量已经指派给某个节点的 ![](img/chap17/img1.png) -# 17.5 ASK 错误 +## 5 ASK 错误 在重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现:属于被迁移槽的一部分键值对保存在源节点中,而另一部分保存在目标节点中。 @@ -241,7 +245,7 @@ Redis集群的重新分片指的是将任意数量已经指派给某个节点的 - 源节点现在自己的数据库中查找键,如果找到,直接执行命令。 - 否则,源节点向客户端返回`ASK`错误,指引客户端转向正在导入槽的目标节点,再次发送命令。 -## CLUSTER SETSLOT IMPORTING 命令的实现 +### CLUSTER SETSLOT IMPORTING 命令的实现 `clusterState`结构的`importing_slots_from`数组记录了当前节点正在从其它节点导入的槽: @@ -255,7 +259,7 @@ typedef struct clusterState { `CLUSTER SETSLOT IMPORTING ` 命令,可以将目标节点的`importing_slots_from[i]`置为`source_id`所代表节点的`clusterNode`结构。 -## CLSUTER SETSLOT MIGRATING 命令的实现 +### CLSUTER SETSLOT MIGRATING 命令的实现 `clusterState`结构的`migrating_slots_to`数组记录了当前节点正在迁移至其它节点的槽: @@ -269,7 +273,7 @@ typedef struct clusterState { `CLUSTER SETSLOT MIGRATING ` 命令,可以将源节点的`migrating_slots_to[i]`置为`target_id`所代表节点的`clusterNode`结构。 -## ASK 错误 +### ASK 错误 节点收到一个关于键`key`的命令请求,先查找`key`所属的槽`i`是否自爱自己的数据库里,如果在,直接执行命令。 @@ -277,20 +281,20 @@ typedef struct clusterState { 接到`ASK`错误的客户端根据错误提供的IP地址和端口,转向目标节点,先向其发送一个`ASKING`命令,之后再重新发送原来要执行的命令。如果不先发送一个`ASKING`命令,那么会被节点拒绝执行,并返回MOVED错误。 -## ASKING 命令 +### ASKING 命令 `ASKING`命令唯一要做的就是打开发送该命令的客户端的`REDIS_ASKING`标识。该标识是一次性标识,节点执行了一个带有该标识的客户端发来的命令后,标识就被移除。 -## ASK 错误和MOVED 错误的区别 +### ASK 错误和MOVED 错误的区别 - `MOVED`错误代表槽的负责权已经转移。 - `ASK`错误是迁移槽过程中的临时措施。接收`ASK`指引的转向,不会对客户端今后发送关于槽`i`的命令请求有任何影响,客户端仍会将请求发送至目前负责处理槽`i`的节点,除非`ASK`错误再次出现。 -# 17.6 复制与故障转移 +## 6 复制与故障转移 Redis集群中的master用于处理槽,slave用于复制某个master,并在被复制的master下线时,代替master继续处理命令请求。 -## 设置slave +### 设置slave 向一个节点发送命令: @@ -314,7 +318,7 @@ struct clusterNode { }; ``` -## 故障检测 +### 故障检测 集群中的每个节点都会定期向其它节点发送`PING`消息,检测对方是否在线。各个节点都会通过消息来交换其它节点的状态信息。 @@ -341,7 +345,7 @@ struct clusterNodeFailReport { 如果在一个集群里,半数以上负责处理槽的master都将某个master X报告为疑似下线,那么X就被标记为下线。将X标记为下线的节点向集群广播关于X的`FAIL`消息,收到消息的节点会立即将X标记为已下线。 -## 故障转移 +### 故障转移 当一个slave发现自己正在复制的master已下线,会开始对其进行故障转移: @@ -351,7 +355,7 @@ struct clusterNodeFailReport { 4. 新master向集群广播一条`PONG`消息,宣布自己成为master。 5. 新master开始接收和处理自己负责的槽有关的命令请求。 -## 选举新的master +### 选举新的master 新的master是选举产生的: @@ -365,7 +369,7 @@ struct clusterNodeFailReport { 8. 因为每个配置纪元里,拥有投票权的master只有一票,因此新的master只会有一个。 9. 如果一个配置纪元中没有选举出新master,那么集群进入一个新的配置纪元,继续选举。 -# 17.7 消息 +## 7 消息 集群中的节点通过消息来通信,消息主要分为以下5种: @@ -377,7 +381,7 @@ struct clusterNodeFailReport { 一个消息由消息头(header)和消息正文(body)组成。 -## 消息头 +### 消息头 每个消息头都由一个`cluster.h/clusterMsg`结构表示: @@ -443,7 +447,7 @@ union clusterMsgData { `clusterMsg`结构的`currentEpoch`、`sender`、`myslots`等属性记录了发送者的节点信息,接收者可以根据这些信息,在自己的`clusterState.nodes`字典中找到发送者对应的`clusterNode`结构进行更新。 -## MEET、PING、PONG 消息的实现 +### MEET、PING、PONG 消息的实现 Redis集群中的各个节点通过Gossip协议来交换节点的状态信息,其中Gossip协议由`MEET`、`PING`、`PONG`三种消息实现,这三种消息的正文都是由两个`cluster.h/clusterMsgDataGossip`结构组成。 @@ -476,7 +480,7 @@ typedef struct { - 被选中节点不存在于接收者的已知节点列表:根据IP和端口跟其握手。 - 被选中节点存在于接收者的已知节点列表:根据`clusterMsgDataGossip`记录的信息,更新被选中节点的`clusterNode`结构。 -## FAIL 信息的实现 +### FAIL 信息的实现 当集群里的master A将master B标记为已下线(FAIL)时,A将集群广播关于B的`FAIL`消息,接收到消息的节点都将B标记为已下线。为了避免Gossip协议的延迟,`FAIL`消息正文采用`cluster.h/clusterMsgDataFail`结构表示: @@ -486,7 +490,7 @@ typedef struct { } clusterMsgDataFail; ``` -## PUBLISH 消息的实现 +### PUBLISH 消息的实现 向某个节点发送: @@ -507,10 +511,17 @@ typedef struct { } clusterMsgDataPublish; ``` -# 导航 +## 8 分片 -[目录](README.md) +分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,这种方法在解决某些问题时可以获得线性级别的性能提升。 -上一章:[16. Sentinel](ch16.md) +假设有 4 个 Redis 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... ,有不同的方式来选择一个指定的键存储在哪个实例中。 -下一章:[18. 发布与订阅](ch18.md) +- 最简单的方式是范围分片,例如用户 id 从 0~1000 的存储到实例 R0 中,用户 id 从 1001~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。 +- 还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。 + +根据执行分片的位置,可以分为三种分片方式: + +- 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。 +- 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。 +- 服务器分片:Redis Cluster。 \ No newline at end of file diff --git a/数据库/Redis/18 发布与订阅.md b/数据库/Redis/13 发布与订阅.md similarity index 89% rename from 数据库/Redis/18 发布与订阅.md rename to 数据库/Redis/13 发布与订阅.md index 49d69fd9..324843a8 100644 --- a/数据库/Redis/18 发布与订阅.md +++ b/数据库/Redis/13 发布与订阅.md @@ -1,10 +1,13 @@ +# 发布与订阅 + +## 0 概述 Redis的发布与订阅功能由`PUBLISH`、`SUBSCRIBE`、`PSUBSCRIBE`等命令组成。 通过执行`SUBSCRIBE`命令,客户端可以订阅一个或多个频道,成为这些频道的订阅者(subscriber):每当有其他客户端向被订阅的频道发送消息时,频道的所有订阅者都会收到这条消息。 客户端还可以通过`PSUBSCRIBE`订阅一个或多个模式:每当有其他客户端向某个频道发送消息,消息不仅会发送给这个频道的订阅者,还会发送给与这个频道相匹配的模式的订阅者。 -# 18.1 频道的订阅与退订 +## 1 频道的订阅与退订 Redis将所有频道的订阅关系都保存在服务器状态的`pubsub_channles`字典中,键是某个被订阅的频道,值是一个链表,里面记录了所有订阅这个频道的客户端: @@ -14,21 +17,21 @@ struct redisServer { }; ``` -## 订阅频道 +### 订阅频道 每当客户端执行`SUBSCRIBE`命令时,服务器都会将客户端与被订阅的频道在`pubsub_channles`字典中关联: - 如果频道已有其他订阅者,将当前客户端添加到订阅者链表的末尾。 - 如果频道未有订阅者,则在`pubsub_channles`字典中创建一个键,并将客户端添加至链表。 -## 退订频道 +### 退订频道 `UNSUBSCRIBE`命令让客户端退订某个频道,服务器从`pubsub_channles`字典中解除关联: - 根据被退订频道的名字,在`pubsub_channles`字典中找到订阅者链表,移除退订客户端的信息。 - 如果链表变成了空,则从`pubsub_channles`字典中删除频道对应的键。 -# 18.2 模式的订阅与退订 +## 2 模式的订阅与退订 服务器将所有模式订阅关系保存在`pubsub_patterns`属性中: @@ -50,53 +53,47 @@ typedef struct pubsubPattern { } pabsubPattern; ``` -## 订阅模式 +### 订阅模式 客户端执行`PSUBSCRIBE`订阅某个模式时,服务器会对被订阅的模式执行以下操作: 1. 新建一个`pubsubPattern`结构,初始化`pattern`和`client`值。 2. 将`pubsubPattern`结构添加到`pubsub_patterns`链表末尾。 -## 退订模式 +### 退订模式 客户端执行`PUNSUBSCRIBE`退订某些模式的时候,服务器在`pubsub_patterns`链表中查找并删除那些`pattern`属性为被退订模式,且`client`属性为执行退订命令的客户端的节点。 -# 18.3 发送消息 +## 3 发送消息 Redis客户端执行`PUBLISH `命令,将消息发送给频道时,服务器执行以下两个操作: 1. 将`message`消息发送给`channel`频道的所有订阅者。 2. 如果一个或多个模式`pattern`与频道`channel`匹配,那么将消息`message`发送给`pattern`模式的订阅者。 -## 将消息发送给频道订阅者 +### 将消息发送给频道订阅者 在`pubsub_channles`字典中找到频道`channel`的订阅者名单,然后将消息发送给名单中的所有客户端。 -## 将消息发送给模式订阅者 +### 将消息发送给模式订阅者 遍历整个`pubsub_patterns`链表,查找那些与`channel`频道相匹配的模式,然后将消息发送给订阅了这些模式的客户端。 -# 18.4 查看订阅消息 +## 4 查看订阅消息 `PUBSUB`命令可以查看频道或模式的相关信息。 -## PUBSUB CHANNELS +### PUBSUB CHANNELS `PUBSUB CHANNELS [pattern]`用于返回服务器当前被订阅的频道,其中`pattern`参数可选。这个命令遍历`pubsub_channles`字典的所有键,然后记录并返回符合条件的频道。 -## PUBSUB NUMSUB +### PUBSUB NUMSUB `PUBSUB NUMSUB [channel-1 channel-2 … channel-N]`返回这些频道的订阅者的数量,也是在`pubsub_channles`字典中找到频道对应的订阅者链表,然后返回链表的长度。 -## PUBSUB NUMPAT +### PUBSUB NUMPAT `PUBSUB NUMPAT`返回服务器当前被订阅的模式的数量,返回`pubsub_patterns`链表的长度。 -# 导航 -[目录](README.md) - -上一章:[17. 集群](ch17.md) - -下一章:[19. 事务](ch19.md) diff --git a/数据库/Redis/13 客户端.md b/数据库/Redis/13 客户端.md deleted file mode 100644 index bd50addb..00000000 --- a/数据库/Redis/13 客户端.md +++ /dev/null @@ -1,103 +0,0 @@ -Redis服务器为客户端建立了相应的redis.h/redisClient结构,保存了客户端的当前信息,以及执行相关功能需要的数据结构: - -- 客户端的套接字描述符 -- 客户端的名字 -- 客户端的标志值(flag) -- 客户端正在使用的数据库的指针及号码 -- 客户端当前要执行的命令、参数 -- 客户端的输入输出缓冲区 -- 客户端的复制状态信息 -- 客户端的事务状态 -- 客户端执行发布与订阅功能用到的数据结构 -- 客户端的身份验证标识 -- 客户端的统计信息,如创建时间、最后一次通行时间、缓冲区大小超出限制的时间 - -`redisServer`结构保存了一个`clients`链表,保存了所有连接的客户端的状态信息。 - -```c -struct redisServer { - // ... - list *clients; - redisClient *lua_client; // Lua伪客户端,服务器运行时一直存在 - // ... -} -``` - -# 13.1 客户端属性 - -```c -typedef struct redisClient { - /* - fd记录客户端正在使用的套接字描述符 - 伪客户端的fd为-1,不需要套接字连接,目前用于 1. AOF还原, 2. 执行Lua脚本的Redis命令 - 普通客户端为大于-1的整数。CLIENT list命令可以查看当前正在使用的套接字描述符 - */ - int fd; - - // 连接到服务器的客户端默认没有名字,CLIENT setname可以设置一个名字。 - robj *name; - - /* - flags记录了客户端的role,以及目前所处的状态 - 所以,flags可以是多个二进制或,所有标志在redis.h中定义 - */ - int flags; - - // 输入缓冲区用于保存客户端发送的命令请求 - sds querybuf; - - // 解析querybuf的请求,得出命令参数及命令个数 - // argv是个数组,每个元素都是一个字符串对象,其中argv[0]是要执行的命令 - robj **argv; - int argc; - - // redisCommand保存了命令的实现函数,标识、参数个数、总执行次数等统计信息 - struct redisCommand *cmd; - - // 输出缓冲区保存命令的回复,其中 - // 1. buf是固定缓冲区,用于保存长度较小的回复 - // 2. reply可变缓冲区,保存长度较大的回复 - char bug[REDIS_REPLY_CHUNK_BYTES]; - int bufpos; - list *reply; - - // 记录客户端是否通过了验证 - int authenticated; - - time_t ctime; - time lastinteraction; - time_t obuf_soft_limit_reached_time; - - // ... -} redisClient; -``` - -# 13.2 客户端的创建于关闭 - -## 创建客户端 - -客户端使用connect函数连接到服务器,服务器就会调用连接事件处理器,为客户端创建相应的客户端状态,并添加到链表的末尾。 - -## 关闭客户端 - -一个普通客户端可因为多种原因关闭: - -- 客户端进程被杀死 - -- 发送的协议不符合格式 - -- 客户端成了`CLIENT KILL`命令的目标 - -- 服务器配置了timeout选项,客户端空转被断开 - -- 超出输入/输出缓冲区限制 - - > 输出缓冲区的限制包括:硬性限制、弱性限制。超过软性限制一段时间,客户端也会被关闭。 - -# 导航 - -[目录](README.md) - -上一章:[12. 事件](ch12.md) - -下一章:[14. 服务器](ch14.md) diff --git a/数据库/Redis/15 Redis使用场景.md b/数据库/Redis/15 Redis使用场景.md new file mode 100644 index 00000000..5c8f0d31 --- /dev/null +++ b/数据库/Redis/15 Redis使用场景.md @@ -0,0 +1,72 @@ +> https://blog.csdn.net/zh15732621679/article/details/80614091 +## 1 Redis使用场景 + + +### 计数器 + +可以对 String 进行自增自减运算,从而实现计数器功能。 + +Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。 + +### 缓存 + +将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。 +![](image/2021-04-07-23-46-25.png) + +### 查找表 + +例如 DNS 记录就很适合使用 Redis 进行存储。 + +查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。 + +### 消息队列 + +List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息 + +不过最好使用 Kafka、RabbitMQ 等消息中间件。 + +### 会话缓存 + +可以使用 Redis 来统一存储多台应用服务器的会话信息。 + +当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。 + +### 分布式锁实现 + +在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。 + +可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。 + +### 其它 + +Set 可以实现交集、并集等操作,从而实现共同好友等功能。 + +ZSet 可以实现有序性操作,从而实现排行榜等功能。 + + +## 2 一个简单的论坛系统分析 + +该论坛系统功能如下: + +- 可以发布文章; +- 可以对文章进行点赞; +- 在首页可以按文章的发布时间或者文章的点赞数进行排序显示。 + +### 文章信息 + +文章包括标题、作者、赞数等信息,在关系型数据库中很容易构建一张表来存储这些信息,在 Redis 中可以使用 HASH 来存储每种信息以及其对应的值的映射。 + +Redis 没有关系型数据库中的表这一概念来将同种类型的数据存放在一起,而是使用命名空间的方式来实现这一功能。键名的前面部分存储命名空间,后面部分的内容存储 ID,通常使用 : 来进行分隔。例如下面的 HASH 的键名为 article:92617,其中 article 为命名空间,ID 为 92617。 +![](image/2021-09-03-00-51-35.png) + +### 点赞功能 + +当有用户为一篇文章点赞时,除了要对该文章的 votes 字段进行加 1 操作,还必须记录该用户已经对该文章进行了点赞,防止用户点赞次数超过 1。可以建立文章的已投票用户集合来进行记录。 + +为了节约内存,规定一篇文章发布满一周之后,就不能再对它进行投票,而文章的已投票集合也会被删除,可以为文章的已投票集合设置一个一周的过期时间就能实现这个规定。 +![](image/2021-09-03-00-51-44.png) + +### 对文章进行排序 + +为了按发布时间和点赞数进行排序,可以建立一个文章发布时间的有序集合和一个文章点赞数的有序集合。(下图中的 score 就是这里所说的点赞数;下面所示的有序集合分值并不直接是时间和点赞数,而是根据时间和点赞数间接计算出来的) +![](image/2021-09-03-00-51-53.png) diff --git a/数据库/Redis/image/2021-09-02-23-00-43.png b/数据库/Redis/image/2021-09-02-23-00-43.png new file mode 100644 index 00000000..b9ee8cc2 Binary files /dev/null and b/数据库/Redis/image/2021-09-02-23-00-43.png differ diff --git a/数据库/Redis/image/2021-09-02-23-16-20.png b/数据库/Redis/image/2021-09-02-23-16-20.png new file mode 100644 index 00000000..9072db6d Binary files /dev/null and b/数据库/Redis/image/2021-09-02-23-16-20.png differ diff --git a/数据库/Redis/image/2021-09-02-23-16-42.png b/数据库/Redis/image/2021-09-02-23-16-42.png new file mode 100644 index 00000000..2405f4c0 Binary files /dev/null and b/数据库/Redis/image/2021-09-02-23-16-42.png differ diff --git a/数据库/Redis/image/2021-09-02-23-17-35.png b/数据库/Redis/image/2021-09-02-23-17-35.png new file mode 100644 index 00000000..95a44ad6 Binary files /dev/null and b/数据库/Redis/image/2021-09-02-23-17-35.png differ diff --git a/数据库/Redis/image/2021-09-02-23-19-57.png b/数据库/Redis/image/2021-09-02-23-19-57.png new file mode 100644 index 00000000..dc3de7e4 Binary files /dev/null and b/数据库/Redis/image/2021-09-02-23-19-57.png differ diff --git a/数据库/Redis/image/2021-09-02-23-20-54.png b/数据库/Redis/image/2021-09-02-23-20-54.png new file mode 100644 index 00000000..e7dc2355 Binary files /dev/null and b/数据库/Redis/image/2021-09-02-23-20-54.png differ diff --git a/数据库/Redis/image/2021-09-03-00-19-56.png b/数据库/Redis/image/2021-09-03-00-19-56.png new file mode 100644 index 00000000..cd3a2501 Binary files /dev/null and b/数据库/Redis/image/2021-09-03-00-19-56.png differ diff --git a/数据库/Redis/image/2021-09-03-00-42-41.png b/数据库/Redis/image/2021-09-03-00-42-41.png new file mode 100644 index 00000000..5a50b233 Binary files /dev/null and b/数据库/Redis/image/2021-09-03-00-42-41.png differ diff --git a/数据库/Redis/image/2021-09-03-00-51-35.png b/数据库/Redis/image/2021-09-03-00-51-35.png new file mode 100644 index 00000000..74982577 Binary files /dev/null and b/数据库/Redis/image/2021-09-03-00-51-35.png differ diff --git a/数据库/Redis/image/2021-09-03-00-51-44.png b/数据库/Redis/image/2021-09-03-00-51-44.png new file mode 100644 index 00000000..b88e5ee1 Binary files /dev/null and b/数据库/Redis/image/2021-09-03-00-51-44.png differ diff --git a/数据库/Redis/image/2021-09-03-00-51-53.png b/数据库/Redis/image/2021-09-03-00-51-53.png new file mode 100644 index 00000000..3676b3a7 Binary files /dev/null and b/数据库/Redis/image/2021-09-03-00-51-53.png differ diff --git a/数据库/Redis/附录2 Redis笔记.md b/数据库/Redis/附录1 Redis笔记.md similarity index 100% rename from 数据库/Redis/附录2 Redis笔记.md rename to 数据库/Redis/附录1 Redis笔记.md diff --git a/数据库/Redis/附录1 Redis详解.md b/数据库/Redis/附录1 Redis详解.md deleted file mode 100644 index a1a53fe1..00000000 --- a/数据库/Redis/附录1 Redis详解.md +++ /dev/null @@ -1,591 +0,0 @@ -# Redis - -- [Redis](#redis) - - [二、数据类型](#二数据类型) - - [STRING](#string) - - [LIST](#list) - - [SET](#set) - - [HASH](#hash) - - [ZSET](#zset) - - [三、数据结构](#三数据结构) - - [字典](#字典) - - [跳跃表](#跳跃表) - - [四、使用场景](#四使用场景) - - [计数器](#计数器) - - [缓存](#缓存) - - [查找表](#查找表) - - [消息队列](#消息队列) - - [会话缓存](#会话缓存) - - [分布式锁实现](#分布式锁实现) - - [其它](#其它) - - [五、Redis 与 Memcached](#五redis-与-memcached) - - [数据类型](#数据类型) - - [数据持久化](#数据持久化) - - [分布式](#分布式) - - [内存管理机制](#内存管理机制) - - [六、键的过期时间](#六键的过期时间) - - [七、数据淘汰策略](#七数据淘汰策略) - - [八、持久化](#八持久化) - - [RDB 持久化](#rdb-持久化) - - [AOF 持久化](#aof-持久化) - - [九、事务](#九事务) - - [十、事件](#十事件) - - [文件事件](#文件事件) - - [时间事件](#时间事件) - - [事件的调度与执行](#事件的调度与执行) - - [十一、复制](#十一复制) - - [连接过程](#连接过程) - - [主从链](#主从链) - - [十二、Sentinel](#十二sentinel) - - [十三、分片](#十三分片) - - [十四、一个简单的论坛系统分析](#十四一个简单的论坛系统分析) - - [文章信息](#文章信息) - - [点赞功能](#点赞功能) - - [对文章进行排序](#对文章进行排序) - - [参考资料](#参考资料) - - - -## 二、数据类型 - -### STRING - -

- -```html -> set hello world -OK -> get hello -"world" -> del hello -(integer) 1 -> get hello -(nil) -``` - -### LIST - -

- -```html -> rpush list-key item -(integer) 1 -> rpush list-key item2 -(integer) 2 -> rpush list-key item -(integer) 3 - -> lrange list-key 0 -1 -1) "item" -2) "item2" -3) "item" - -> lindex list-key 1 -"item2" - -> lpop list-key -"item" - -> lrange list-key 0 -1 -1) "item2" -2) "item" -``` - -### SET - -

- -```html -> sadd set-key item -(integer) 1 -> sadd set-key item2 -(integer) 1 -> sadd set-key item3 -(integer) 1 -> sadd set-key item -(integer) 0 - -> smembers set-key -1) "item" -2) "item2" -3) "item3" - -> sismember set-key item4 -(integer) 0 -> sismember set-key item -(integer) 1 - -> srem set-key item2 -(integer) 1 -> srem set-key item2 -(integer) 0 - -> smembers set-key -1) "item" -2) "item3" -``` - -### HASH - -

- -```html -> hset hash-key sub-key1 value1 -(integer) 1 -> hset hash-key sub-key2 value2 -(integer) 1 -> hset hash-key sub-key1 value1 -(integer) 0 - -> hgetall hash-key -1) "sub-key1" -2) "value1" -3) "sub-key2" -4) "value2" - -> hdel hash-key sub-key2 -(integer) 1 -> hdel hash-key sub-key2 -(integer) 0 - -> hget hash-key sub-key1 -"value1" - -> hgetall hash-key -1) "sub-key1" -2) "value1" -``` - -### ZSET - -

- -```html -> zadd zset-key 728 member1 -(integer) 1 -> zadd zset-key 982 member0 -(integer) 1 -> zadd zset-key 982 member0 -(integer) 0 - -> zrange zset-key 0 -1 withscores -1) "member1" -2) "728" -3) "member0" -4) "982" - -> zrangebyscore zset-key 0 800 withscores -1) "member1" -2) "728" - -> zrem zset-key member1 -(integer) 1 -> zrem zset-key member1 -(integer) 0 - -> zrange zset-key 0 -1 withscores -1) "member0" -2) "982" -``` - -## 三、数据结构 - -### 字典 - -dictht 是一个散列表结构,使用拉链法解决哈希冲突。 - -```c -/* This is our hash table structure. Every dictionary has two of this as we - * implement incremental rehashing, for the old to the new table. */ -typedef struct dictht { - dictEntry **table; - unsigned long size; - unsigned long sizemask; - unsigned long used; -} dictht; -``` - -```c -typedef struct dictEntry { - void *key; - union { - void *val; - uint64_t u64; - int64_t s64; - double d; - } v; - struct dictEntry *next; -} dictEntry; -``` - -Redis 的字典 dict 中包含两个哈希表 dictht,这是为了方便进行 rehash 操作。在扩容时,将其中一个 dictht 上的键值对 rehash 到另一个 dictht 上面,完成之后释放空间并交换两个 dictht 的角色。 - -```c -typedef struct dict { - dictType *type; - void *privdata; - dictht ht[2]; - long rehashidx; /* rehashing not in progress if rehashidx == -1 */ - unsigned long iterators; /* number of iterators currently running */ -} dict; -``` - -rehash 操作不是一次性完成,而是采用渐进方式,这是为了避免一次性执行过多的 rehash 操作给服务器带来过大的负担。 - -渐进式 rehash 通过记录 dict 的 rehashidx 完成,它从 0 开始,然后每执行一次 rehash 都会递增。例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],这一次会把 dict[0] 上 table[rehashidx] 的键值对 rehash 到 dict[1] 上,dict[0] 的 table[rehashidx] 指向 null,并令 rehashidx++。 - -在 rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式 rehash。 - -采用渐进式 rehash 会导致字典中的数据分散在两个 dictht 上,因此对字典的查找操作也需要到对应的 dictht 去执行。 - -```c -/* Performs N steps of incremental rehashing. Returns 1 if there are still - * keys to move from the old to the new hash table, otherwise 0 is returned. - * - * Note that a rehashing step consists in moving a bucket (that may have more - * than one key as we use chaining) from the old to the new hash table, however - * since part of the hash table may be composed of empty spaces, it is not - * guaranteed that this function will rehash even a single bucket, since it - * will visit at max N*10 empty buckets in total, otherwise the amount of - * work it does would be unbound and the function may block for a long time. */ -int dictRehash(dict *d, int n) { - int empty_visits = n * 10; /* Max number of empty buckets to visit. */ - if (!dictIsRehashing(d)) return 0; - - while (n-- && d->ht[0].used != 0) { - dictEntry *de, *nextde; - - /* Note that rehashidx can't overflow as we are sure there are more - * elements because ht[0].used != 0 */ - assert(d->ht[0].size > (unsigned long) d->rehashidx); - while (d->ht[0].table[d->rehashidx] == NULL) { - d->rehashidx++; - if (--empty_visits == 0) return 1; - } - de = d->ht[0].table[d->rehashidx]; - /* Move all the keys in this bucket from the old to the new hash HT */ - while (de) { - uint64_t h; - - nextde = de->next; - /* Get the index in the new hash table */ - h = dictHashKey(d, de->key) & d->ht[1].sizemask; - de->next = d->ht[1].table[h]; - d->ht[1].table[h] = de; - d->ht[0].used--; - d->ht[1].used++; - de = nextde; - } - d->ht[0].table[d->rehashidx] = NULL; - d->rehashidx++; - } - - /* Check if we already rehashed the whole table... */ - if (d->ht[0].used == 0) { - zfree(d->ht[0].table); - d->ht[0] = d->ht[1]; - _dictReset(&d->ht[1]); - d->rehashidx = -1; - return 0; - } - - /* More to rehash... */ - return 1; -} -``` - -### 跳跃表 - -是有序集合的底层实现之一。 - -跳跃表是基于多指针有序链表实现的,可以看成多个有序链表。 - -

- -在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。下图演示了查找 22 的过程。 - -

- -与红黑树等平衡树相比,跳跃表具有以下优点: - -- 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性; -- 更容易实现; -- 支持无锁操作。 - -## 四、使用场景 - -### 计数器 - -可以对 String 进行自增自减运算,从而实现计数器功能。 - -Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。 - -### 缓存 - -将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。 - -### 查找表 - -例如 DNS 记录就很适合使用 Redis 进行存储。 - -查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。 - -### 消息队列 - -List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息 - -不过最好使用 Kafka、RabbitMQ 等消息中间件。 - -### 会话缓存 - -可以使用 Redis 来统一存储多台应用服务器的会话信息。 - -当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。 - -### 分布式锁实现 - -在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。 - -可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。 - -### 其它 - -Set 可以实现交集、并集等操作,从而实现共同好友等功能。 - -ZSet 可以实现有序性操作,从而实现排行榜等功能。 - -## 五、Redis 与 Memcached - -两者都是非关系型内存键值数据库,主要有以下不同: - -### 数据类型 - -Memcached 仅支持字符串类型,而 Redis 支持五种不同的数据类型,可以更灵活地解决问题。 - -### 数据持久化 - -Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化。 - -### 分布式 - -Memcached 不支持分布式,只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。 - -Redis Cluster 实现了分布式的支持。 - -### 内存管理机制 - -- 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘,而 Memcached 的数据则会一直在内存中。 - -- Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题。但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。 - -## 六、键的过期时间 - -Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。 - -对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。 - -## 七、数据淘汰策略 - -可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。 - -Redis 具体有 6 种淘汰策略: - -| 策略 | 描述 | -| :--: | :--: | -| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 | -| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 | -|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 | -| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 | -| allkeys-random | 从所有数据集中任意选择数据进行淘汰 | -| noeviction | 禁止驱逐数据 | - -作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法实际实现上并非针对所有 key,而是抽样一小部分并且从中选出被淘汰的 key。 - -使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。 - -Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。 - -## 八、持久化 - -Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。 - -### RDB 持久化 - -将某个时间点的所有数据都存放到硬盘上。 - -可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。 - -如果系统发生故障,将会丢失最后一次创建快照之后的数据。 - -如果数据量很大,保存快照的时间会很长。 - -### AOF 持久化 - -将写命令添加到 AOF 文件(Append Only File)的末尾。 - -使用 AOF 持久化需要设置同步选项,从而确保写命令同步到磁盘文件上的时机。这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。有以下同步选项: - -| 选项 | 同步频率 | -| :--: | :--: | -| always | 每个写命令都同步 | -| everysec | 每秒同步一次 | -| no | 让操作系统来决定何时同步 | - -- always 选项会严重减低服务器的性能; -- everysec 选项比较合适,可以保证系统崩溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响; -- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量。 - -随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。 - -## 九、事务 - -一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。 - -事务中的多个命令被一次性发送给服务器,而不是一条一条发送,这种方式被称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。 - -Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。 - -## 十、事件 - -Redis 服务器是一个事件驱动程序。 - -### 文件事件 - -服务器通过套接字与客户端或者其它服务器进行通信,文件事件就是对套接字操作的抽象。 - -Redis 基于 Reactor 模式开发了自己的网络事件处理器,使用 I/O 多路复用程序来同时监听多个套接字,并将到达的事件传送给文件事件分派器,分派器会根据套接字产生的事件类型调用相应的事件处理器。 - -

- -### 时间事件 - -服务器有一些操作需要在给定的时间点执行,时间事件是对这类定时操作的抽象。 - -时间事件又分为: - -- 定时事件:是让一段程序在指定的时间之内执行一次; -- 周期性事件:是让一段程序每隔指定时间就执行一次。 - -Redis 将所有时间事件都放在一个无序链表中,通过遍历整个链表查找出已到达的时间事件,并调用相应的事件处理器。 - -### 事件的调度与执行 - -服务器需要不断监听文件事件的套接字才能得到待处理的文件事件,但是不能一直监听,否则时间事件无法在规定的时间内执行,因此监听时间应该根据距离现在最近的时间事件来决定。 - -事件调度与执行由 aeProcessEvents 函数负责,伪代码如下: - -```python -def aeProcessEvents(): - # 获取到达时间离当前时间最接近的时间事件 - time_event = aeSearchNearestTimer() - # 计算最接近的时间事件距离到达还有多少毫秒 - remaind_ms = time_event.when - unix_ts_now() - # 如果事件已到达,那么 remaind_ms 的值可能为负数,将它设为 0 - if remaind_ms < 0: - remaind_ms = 0 - # 根据 remaind_ms 的值,创建 timeval - timeval = create_timeval_with_ms(remaind_ms) - # 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 决定 - aeApiPoll(timeval) - # 处理所有已产生的文件事件 - procesFileEvents() - # 处理所有已到达的时间事件 - processTimeEvents() -``` - -将 aeProcessEvents 函数置于一个循环里面,加上初始化和清理函数,就构成了 Redis 服务器的主函数,伪代码如下: - -```python -def main(): - # 初始化服务器 - init_server() - # 一直处理事件,直到服务器关闭为止 - while server_is_not_shutdown(): - aeProcessEvents() - # 服务器关闭,执行清理操作 - clean_server() -``` - -从事件处理的角度来看,服务器运行流程如下: - -

- -## 十一、复制 - -通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。 - -一个从服务器只能有一个主服务器,并且不支持主主复制。 - -### 连接过程 - -1. 主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令; - -2. 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令; - -3. 主服务器每执行一次写命令,就向从服务器发送相同的写命令。 - -### 主从链 - -随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。 - -

- -## 十二、Sentinel - -Sentinel(哨兵)可以监听集群中的服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。 - -## 十三、分片 - -分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,这种方法在解决某些问题时可以获得线性级别的性能提升。 - -假设有 4 个 Redis 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... ,有不同的方式来选择一个指定的键存储在哪个实例中。 - -- 最简单的方式是范围分片,例如用户 id 从 0~1000 的存储到实例 R0 中,用户 id 从 1001~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。 -- 还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。 - -根据执行分片的位置,可以分为三种分片方式: - -- 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。 -- 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。 -- 服务器分片:Redis Cluster。 - -## 十四、一个简单的论坛系统分析 - -该论坛系统功能如下: - -- 可以发布文章; -- 可以对文章进行点赞; -- 在首页可以按文章的发布时间或者文章的点赞数进行排序显示。 - -### 文章信息 - -文章包括标题、作者、赞数等信息,在关系型数据库中很容易构建一张表来存储这些信息,在 Redis 中可以使用 HASH 来存储每种信息以及其对应的值的映射。 - -Redis 没有关系型数据库中的表这一概念来将同种类型的数据存放在一起,而是使用命名空间的方式来实现这一功能。键名的前面部分存储命名空间,后面部分的内容存储 ID,通常使用 : 来进行分隔。例如下面的 HASH 的键名为 article:92617,其中 article 为命名空间,ID 为 92617。 - -

- -### 点赞功能 - -当有用户为一篇文章点赞时,除了要对该文章的 votes 字段进行加 1 操作,还必须记录该用户已经对该文章进行了点赞,防止用户点赞次数超过 1。可以建立文章的已投票用户集合来进行记录。 - -为了节约内存,规定一篇文章发布满一周之后,就不能再对它进行投票,而文章的已投票集合也会被删除,可以为文章的已投票集合设置一个一周的过期时间就能实现这个规定。 - -

- -### 对文章进行排序 - -为了按发布时间和点赞数进行排序,可以建立一个文章发布时间的有序集合和一个文章点赞数的有序集合。(下图中的 score 就是这里所说的点赞数;下面所示的有序集合分值并不直接是时间和点赞数,而是根据时间和点赞数间接计算出来的) - -

- -## 参考资料 - -- Carlson J L. Redis in Action[J]. Media.johnwiley.com.au, 2013. -- [黄健宏. Redis 设计与实现 [M]. 机械工业出版社, 2014.](http://redisbook.com/index.html) -- [REDIS IN ACTION](https://redislabs.com/ebook/foreword/) -- [Skip Lists: Done Right](http://ticki.github.io/blog/skip-lists-done-right/) -- [论述 Redis 和 Memcached 的差异](http://www.cnblogs.com/loveincode/p/7411911.html) -- [Redis 3.0 中文版- 分片](http://wiki.jikexueyuan.com/project/redis-guide) -- [Redis 应用场景](http://www.scienjus.com/redis-use-case/) -- [Using Redis as an LRU cache](https://redis.io/topics/lru-cache) diff --git a/数据库/Redis/附录5 Redis使用场景.md b/数据库/Redis/附录5 Redis使用场景.md deleted file mode 100644 index 4c3327cb..00000000 --- a/数据库/Redis/附录5 Redis使用场景.md +++ /dev/null @@ -1,3 +0,0 @@ -> https://blog.csdn.net/zh15732621679/article/details/80614091 -### Redis使用场景 -![](image/2021-04-07-23-46-25.png) \ No newline at end of file