diff --git a/notes/Redis.md b/notes/Redis.md
index c669b03c..cc59f702 100644
--- a/notes/Redis.md
+++ b/notes/Redis.md
@@ -49,6 +49,12 @@
* [文章信息](#文章信息)
* [点赞功能](#点赞功能)
* [对文章进行排序](#对文章进行排序)
+ * [附录:实测](#附录实测)
+ * [A1 MySQL 与 Redis 的主键查询](#a1-mysql-与-redis-的主键查询)
+ * [A2 排行榜](#a2-排行榜)
+ * [A3 单线程的吞吐量和 CPU 时间](#a3-单线程的吞吐量和-cpu-时间)
+ * [A4 Redis Cluster](#a4-redis-cluster)
+ * [A5 Sentinel 故障转移](#a5-sentinel-故障转移)
* [参考资料](#参考资料)
@@ -57,7 +63,7 @@
Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。
-键的类型只能为字符串,值支持五种基本数据类型:字符串、列表、集合、散列表、有序集合,此外还有位图、HyperLogLog、地理空间、流(Stream)等类型。
+键的类型只能为字符串,值支持五种基本数据类型:字符串、列表、集合、哈希、有序集合,此外还有位图、HyperLogLog、地理空间、流(Stream)等类型。
Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。
@@ -68,19 +74,17 @@ Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,
- 事务:MySQL 的事务满足 ACID,出错可以回滚;Redis 的事务只保证一组命令依次执行、不被其它客户端的命令插入,其中一条命令执行出错时,其它命令不会回滚,见 [九、事务](#九事务);
- 持久性:MySQL 默认在事务提交时把 redo log 刷到磁盘,提交成功的数据不会因为宕机丢失;Redis 即使开启 AOF 并设置为 everysec,宕机时也可能丢失最近一秒左右的写入。
-数据都在内存中时,MySQL 按主键查询也很快。在本机用 50 个连接分别压测同一条主键查询和同一个键的 GET,各执行 20 万次:MySQL 每秒约 16 万次,用了约 6 个 CPU 核心,每次查询消耗约 36 微秒的 CPU 时间;Redis 每秒约 11 万次,只用了 1 个核心,每次约 8.7 微秒。Redis 的优势在于开销小、延迟稳定,以及计数器、排行榜这类 MySQL 不擅长的数据结构;需要持久保存、按多种条件查询的业务数据仍然放在 MySQL 中。两者通常一起使用,Redis 作为缓存,见 [缓存与数据库的一致性](缓存.md#缓存一致性)。
+数据都在内存中时,MySQL 按主键查询也很快。用 50 个连接分别执行 20 万次同一条主键查询和同一个键的 GET,两者每秒都能处理 13 万~17 万次,但 MySQL 用了约 6 个 CPU 核心,每次查询消耗约 36 微秒的 CPU 时间;Redis 只用 1 个核心,每次约 7 微秒(MySQL 8.0、Redis 7.4 实测,见[附录 A1](#a1-mysql-与-redis-的主键查询))。Redis 的优势在于开销小、延迟稳定,以及计数器、排行榜这类 MySQL 不擅长的数据结构;需要持久保存、按多种条件查询的业务数据仍然放在 MySQL 中。两者通常一起使用,Redis 作为缓存,见 [缓存与数据库的一致性](缓存.md#缓存一致性)。
## 二、数据类型
| 数据类型 | 可以存储的值 | 操作 |
| :--: | :--: | :--: |
-| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作\ 对整数和浮点数执行自增或者自减操作 |
-| LIST | 列表 | 从两端压入或者弹出元素 \ 对单个或者多个元素进行修剪,\ 只保留一个范围内的元素 |
-| SET | 无序集合 | 添加、获取、移除单个元素\ 检查一个元素是否存在于集合中\ 计算交集、并集、差集\ 从集合里面随机获取元素 |
-| HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对\ 获取所有键值对\ 检查某个键是否存在|
-| ZSET | 有序集合 | 添加、获取、删除元素\ 根据分值范围或者成员来获取元素\ 计算一个键的排名 |
-
-> [What Redis data structures look like](https://redis.io/ebook/part-1-getting-started/chapter-1-getting-to-know-redis/1-2-what-redis-data-structures-look-like/)
+| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作
对整数和浮点数执行自增或者自减操作 |
+| LIST | 列表 | 从两端压入或者弹出元素
对单个或者多个元素进行修剪,
只保留一个范围内的元素 |
+| SET | 无序集合 | 添加、获取、移除单个元素
检查一个元素是否存在于集合中
计算交集、并集、差集
从集合里面随机获取元素 |
+| HASH | 包含键值对的无序哈希表 | 添加、获取、移除单个键值对
获取所有键值对
检查某个键是否存在|
+| ZSET | 有序集合 | 添加、获取、删除元素
根据分值范围或者成员来获取元素
计算一个键的排名 |
### STRING
@@ -219,135 +223,50 @@ OK
### 字典
-dictht 是一个散列表结构,使用拉链法解决哈希冲突。
+键空间、HASH 和 SET 的 hashtable 编码、ZSET 中按成员查分数的部分,都是用字典(dict)实现的。字典是一个哈希表,用拉链法解决哈希冲突:桶的数量是 2 的幂,键的哈希值与桶数减 1 做位与得到桶的下标,落在同一个桶的键用链表连起来。
-```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;
-```
+键越来越多时链表变长,查找变慢,需要扩大桶数组并把所有键重新放一遍,称为 rehash。Redis 用一个线程执行命令,一个有几百万个键的字典一次性 rehash 会让服务器停顿很久,所以采用渐进式 rehash:
-```c
-typedef struct dictEntry {
- void *key;
- union {
- void *val;
- uint64_t u64;
- int64_t s64;
- double d;
- } v;
- struct dictEntry *next;
-} dictEntry;
-```
+- 字典中有两张哈希表 ht_table[0] 和 ht_table[1],平时只用 ht_table[0];
+- 键的数量达到桶数时扩容,ht_table[1] 的大小为不小于键数加 1 的 2 的幂;有子进程在执行 BGSAVE 或 AOF 重写时,为了减少写时复制的内存页,要达到桶数的 4 倍才扩容;
+- 键的数量不超过桶数的 1/8 时缩容,过程相同;
+- rehashidx 记录迁移到了哪个桶,之后每次对这个字典增删改查,都顺便把一个桶中的所有键迁移到 ht_table[1],serverCron 也会定期花不超过 1 毫秒继续迁移;
+- 迁移期间新写入的键只放入 ht_table[1],查找和删除时两张表都可能要查;
+- 全部迁移完成后,ht_table[1] 变为 ht_table[0],rehashidx 恢复为 -1。
-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;
-}
-```
+以上依据 Redis 7.4 的 dict.c。Redis 7.0 之前的两张表是 dict 中的 dictht ht[2],渐进式 rehash 的过程相同。
### 跳跃表
-是有序集合的底层实现之一。
+跳跃表(skiplist)是有序集合在成员较多时的底层结构,它在一条有序链表上增加了多层索引。
-跳跃表是基于多指针有序链表实现的,可以看成多个有序链表。
+最底层是包含所有成员、按分值排序的链表。每个节点插入时随机决定层数:以 1/4 的概率升高一层,最多 32 层。所以大约每 4 个节点有 1 个出现在第 2 层,每 16 个节点有 1 个出现在第 3 层,平均每个节点约 1.33 个指针。
-在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。下图演示了查找 26 的过程。
+查找时从最高层开始向右走,下一个节点比目标大时下降一层,直到最底层。每层平均只走几步,查找、插入和删除的期望时间复杂度都是 O(logN)。下图演示了查找 26 的过程。
-与红黑树等平衡树相比,跳跃表具有以下优点:
+与红黑树等平衡树相比,跳跃表有以下优点:
-- 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性;
-- 更容易实现;
-- 支持无锁操作。
+- 插入和删除只需要修改每一层前驱节点的指针,不需要旋转等操作来维护平衡;
+- 范围查询方便,找到起点后沿最底层链表向后遍历即可,ZRANGEBYSCORE 就是这样实现的;
+- 实现和调试更简单。
+
+Redis 的跳跃表还在每个指针上记录了跨度,即这一步跳过了多少个节点,查找时把经过的跨度相加就得到排名,所以 ZRANK 也是 O(logN)。
## 四、使用场景
### 计数器
-可以对 String 进行自增自减运算,从而实现计数器功能。
+可以对字符串执行 INCR、INCRBY 等命令实现计数器,例如文章的阅读数、接口的调用次数。
-Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
+命令在服务器上原子执行,多个客户端同时加 1 不会丢失更新;数据在内存中,也适合这种频繁读写的场景。
### 缓存
-将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。
+将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率,见 [缓存](缓存.md)。
### 查找表
@@ -357,9 +276,9 @@ Redis 这种内存型数据库的读写性能非常高,很适合存储频繁
### 消息队列
-List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息
+List 可以用 LPUSH 写入消息,用 BRPOP 阻塞地读取消息。但消息被取出后就从列表中删除,消费者处理失败时消息会丢失,也不能让多个消费者组各自消费一遍。
-不过最好使用 Kafka、RabbitMQ 等消息中间件。
+Redis 5.0 加入的流(Stream)支持消费者组和确认:XREADGROUP 读取的消息在 XACK 确认之前一直保存在待处理列表中,消费者宕机后可以由其它消费者认领。对可靠性和消息堆积量要求高的场景,仍然使用 Kafka、RocketMQ 等消息中间件,见 [消息队列](消息队列.md)。
### 会话缓存
@@ -383,7 +302,7 @@ ZREVRANGE rank:likes 0 9 WITHSCORES -- 前 10 名
ZREVRANK rank:likes article:1001 -- 第几名,从 0 开始
```
-在本机向一个有序集合写入 100 万个成员,占用约 105 MB 内存;查前 10 名、查某个成员的排名和加分都是 O(logN) 的操作,SLOWLOG 记录的服务器执行时间大多在 4~10 微秒之间。
+向一个有序集合写入 100 万个成员,占用约 105 MB 内存;查前 10 名、查某个成员的排名和加分都是 O(logN) 的操作,SLOWLOG 记录的服务器执行时间只有几微秒(Redis 7.4 实测,见[附录 A2](#a2-排行榜))。
分值相同时,Redis 按成员名的字典序排列,而业务上通常要求先达到这个分数的排在前面。可以把时间编进分值:分值 = 点赞数 × 1010 + (1010 − 达到这个点赞数的秒级时间戳)。例如 article:1001 和 article:1002 都是 520 个赞,时间戳分别为 1793000000 和 1793000300,分值为 5208207000000 和 5208206999700,先达到的 article:1001 排在前面。需要注意的是,分值是双精度浮点数,只能精确表示 253 以内的整数,这种写法下点赞数不能超过约 90 万,更大时要缩短时间部分的位数。
@@ -393,8 +312,6 @@ ZREVRANK rank:likes article:1001 -- 第几名,从 0 开始
Set 可以实现交集、并集等操作,从而实现共同好友等功能。
-ZSet 可以实现有序性操作,从而实现排行榜等功能。
-
## 五、Redis 与 Memcached
两者都是非关系型内存键值数据库,主要有以下不同:
@@ -415,15 +332,14 @@ Redis Cluster 实现了分布式的支持。
### 内存管理机制
-- Redis 和 Memcached 的数据都一直存储在内存中。Redis 曾经支持把很久没用的 value 交换到磁盘(虚拟内存),但是该特性已经在 2.6 版本移除。
-
-- Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题。但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
+- Redis 和 Memcached 的数据都一直存储在内存中。Redis 曾经支持把很久没用的 value 交换到磁盘(虚拟内存),但是该特性已经在 2.6 版本移除;
+- Memcached 将内存分割成特定长度的块来存储数据,避免了频繁分配和释放造成的内存碎片。但是这种方式会使得内存的利用率不高,例如块的大小为 128 字节,只存储 100 字节的数据,那么剩下的 28 字节就浪费掉了。
## 六、键的过期时间
-Redis 可以为每个键设置过期时间。过期的键不会在过期的瞬间被删除,而是通过两种策略清除:惰性删除,在访问键时检查它是否过期,过期就删除;定期删除,后台定时随机抽取一部分设置了过期时间的键进行检查,删除其中已经过期的键。
+Redis 可以为每个键设置过期时间。过期的键不会在过期的瞬间被删除,而是通过两种策略清除:惰性删除,在访问键时检查它是否过期,过期就删除;定期删除,后台每秒执行 10 次(由 hz 配置),每次随机抽取一部分设置了过期时间的键进行检查,删除其中已经过期的键。
-对于散列表这种容器,Redis 7.4 之前只能为整个键设置过期时间,7.4 开始可以使用 HEXPIRE 等命令为单个字段设置过期时间。
+对于 HASH 这种容器,Redis 7.4 之前只能为整个键设置过期时间,7.4 开始可以使用 HEXPIRE 等命令为单个字段设置过期时间。
## 七、数据淘汰策略
@@ -442,7 +358,9 @@ Redis 4.0 之后具体有 8 种淘汰策略:
| allkeys-lfu | 从所有数据集中挑选最不经常使用的数据淘汰 |
| noeviction | 禁止驱逐数据 |
-作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法实际实现上并非针对所有 key,而是抽样一小部分并且从中选出被淘汰的 key。
+默认策略是 noeviction:内存达到上限后,写命令返回错误,读命令照常执行。
+
+作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法实际实现上并非针对所有 key,而是每次抽样一小部分(maxmemory-samples,默认 5 个)并且从中选出被淘汰的 key。
使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
@@ -476,7 +394,7 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需
| everysec | 每秒同步一次 |
| no | 让操作系统来决定何时同步 |
-- always 选项会严重减低服务器的性能;
+- always 选项会严重降低服务器的性能;
- everysec 选项比较合适,可以保证系统崩溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量。
@@ -618,9 +536,9 @@ Redis 用一个线程执行所有命令,仍然很快,原因主要有以下
- I/O 多路复用让一个线程同时处理大量连接;
- 按数据的大小选择不同的编码,例如元素少时用紧凑的 listpack。
-在本机(Apple M1 Pro,Redis 7.4)用 redis-benchmark 测试,50 个连接逐条发送 GET,每秒约 13 万次;每次连续发送 16 条命令(流水线)时,每秒约 140 万次。逐条发送 100 万次 GET 时,服务器消耗的 CPU 时间中内核态 6.35 秒、用户态 1.18 秒:大部分时间花在网络读写的系统调用上,解析和执行一条命令只需要 1 微秒左右。
+用 redis-benchmark 测试,50 个连接逐条发送 GET,每秒约 16 万次;每次连续发送 16 条命令(流水线)时,每秒约 160 万次。逐条发送 100 万次 GET 时,服务器消耗的 CPU 时间中内核态 5.1 秒、用户态 1.0 秒:大部分时间花在网络读写的系统调用上,解析和执行一条命令只需要 1 微秒左右(Redis 7.4 实测,见[附录 A3](#a3-单线程的吞吐量和-cpu-时间))。
-所以 Redis 的瓶颈通常是网络和内存,而不是 CPU。Redis 6.0 加入了 I/O 多线程(io-threads,默认关闭),只用多个线程读写套接字,命令仍由主线程逐个执行,单条命令的原子性不变。此外,关闭文件、AOF 刷盘和 UNLINK 释放大键的内存这些耗时操作由后台线程完成。
+所以 Redis 的瓶颈通常是网络和内存,而不是 CPU。Redis 6.0 加入了 I/O 多线程(io-threads,默认关闭),开启后默认只把写回客户端交给 I/O 线程,读取和解析请求还需要再打开 io-threads-do-reads;命令仍由主线程逐个执行,单条命令的原子性不变。此外,关闭文件、AOF 刷盘和 UNLINK 释放大键的内存这些耗时操作由后台线程完成。
## 十一、复制
@@ -660,7 +578,7 @@ Sentinel(哨兵)可以监听集群中的服务器,并在主服务器进入
-实验中另一个从服务器 6401 改为复制 6402 时,6402 的日志为 Partial resynchronization request from 127.0.0.1:6401 accepted,只发送了 156 字节的积压数据;而重启的 6400 复制 ID 不匹配,只能全量复制。
+down-after-milliseconds 为 5 秒时,从主服务器宕机到客户端查到新地址约 6 秒,其中 5 秒是超时检测。新的主服务器保留了旧的复制 ID,另一个从服务器转向它时只需要部分重同步,只发送了 156 字节的积压数据;重启的旧主服务器复制 ID 不匹配,只能全量复制(Redis 7.4 实测,见[附录 A5](#a5-sentinel-故障转移))。
需要注意的是,主从之间是异步复制,切换时还没有复制到从服务器的写入会丢失。网络分区时,旧的主服务器还可能继续接受写入(脑裂),恢复后它变为从服务器,这段时间的写入也会丢失。可以配置 min-replicas-to-write 1 和 min-replicas-max-lag 10:延迟不超过 10 秒的从服务器少于 1 个时,主服务器拒绝写入并返回 NOREPLICAS,把丢失的范围限制在 10 秒左右。
@@ -675,8 +593,8 @@ Sentinel(哨兵)可以监听集群中的服务器,并在主服务器进入
根据执行分片的位置,可以分为三种分片方式:
-- 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。
-- 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。
+- 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点;
+- 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上;
- 服务器分片:Redis Cluster,使用 CRC16(key) % 16384 计算键所属的哈希槽,再把 16384 个哈希槽分配给各个节点。
### Redis Cluster
@@ -687,13 +605,13 @@ Redis 有三种常见的部署方式,解决的问题不同:
- Sentinel:在主从复制的基础上自动监控和故障转移,但仍然只有一个主服务器负责写入,数据量受单机内存限制;
- Redis Cluster:把数据分散到多个主节点上,每个主节点有自己的从节点,同时解决容量和高可用的问题。
-Redis Cluster 把键空间分为 16384 个哈希槽,每个主节点负责一部分槽。下面是在本机创建的 3 主 3 从的集群:
+Redis Cluster 把键空间分为 16384 个哈希槽,每个主节点负责一部分槽。下图是一个 3 主 3 从的集群(Redis 7.4 实测,见[附录 A4](#a4-redis-cluster)):
- 客户端向不负责这个槽的节点发送命令时,会收到 MOVED 重定向。例如 user:1001 所在的槽 5712 属于 7002,发给 7001 会收到 MOVED 5712 127.0.0.1:7002。客户端会缓存槽与节点的对应关系,之后直接访问正确的节点;
- 一条命令涉及多个键时,这些键必须在同一个槽中,否则报错 CROSSSLOT,Lua 脚本也是如此。可以用哈希标签(hash tag)让相关的键落到同一个槽:只有 {} 中的部分参与计算,{user:1001}:cart 和 {user:1001}:orders 都在槽 5712;
-- 主节点宕机后,它的一个从节点会被选为新的主节点。在本机把 cluster-node-timeout 设为 5 秒,停掉一个主节点后,这部分槽约 9 秒无法写入。
+- 主节点宕机后,它的一个从节点会被选为新的主节点。cluster-node-timeout 为 5 秒时,停掉一个主节点后,这部分槽约 7~9 秒无法写入。
需要注意的是,主从之间是异步复制,主节点宕机前刚写入、还没有复制到从节点的数据会丢失,所以 Redis Cluster 不保证强一致性。
@@ -727,19 +645,187 @@ Redis 没有关系型数据库中的表这一概念来将同种类型的数据
+## 附录:实测
+
+以下实验的环境均为 Apple M1 Pro(10 核)、macOS,客户端和服务器在同一台机器上,Redis 7.4.1 从源码编译,使用默认配置,不开启持久化。
+
+### A1 MySQL 与 Redis 的主键查询
+
+MySQL 8.0.44,orders 表有 200 万行,要查的行已在缓冲池中。两边都用 50 个连接执行 20 万次,各做 3 轮。MySQL 的 CPU 时间是 mysqld 进程执行前后的 CPU 时间之差,Redis 的是 INFO cpu 中 used_cpu_sys 与 used_cpu_user 之和的差。
+
+```bash
+mysqlslap -uroot --concurrency=50 --iterations=1 --number-of-queries=200000 \
+ --create-schema=demo --query="SELECT * FROM orders WHERE id = 1024"
+redis-benchmark -c 50 -n 200000 -q GET order:1024
+```
+
+```text
+== 第 1 轮 MySQL
+ Average number of seconds to run all queries: 1.155 seconds
+墙钟 1.21 s,mysqld CPU 7.20 s,QPS 165707,每次 36.0 us,平均占用 6.0 核
+== 第 1 轮 Redis
+GET order:1024: 165425.97 requests per second, p50=0.231 msec
+redis-server CPU 1.21 s,每次 6.0 us
+== 第 2 轮 MySQL
+ Average number of seconds to run all queries: 1.111 seconds
+墙钟 1.16 s,mysqld CPU 7.34 s,QPS 172026,每次 36.7 us,平均占用 6.3 核
+== 第 2 轮 Redis
+GET order:1024: 153727.91 requests per second, p50=0.247 msec
+redis-server CPU 1.30 s,每次 6.5 us
+== 第 3 轮 MySQL
+ Average number of seconds to run all queries: 1.101 seconds
+墙钟 1.15 s,mysqld CPU 7.16 s,QPS 173683,每次 35.8 us,平均占用 6.2 核
+== 第 3 轮 Redis
+GET order:1024: 131926.12 requests per second, p50=0.287 msec
+redis-server CPU 1.49 s,每次 7.4 us
+```
+
+### A2 排行榜
+
+用 redis-py 写入同分排序的例子和 100 万个成员,分值是 0~100 万的随机整数,用流水线每 1 万条提交一次:
+
+```python
+def score(likes, ts):
+ return likes * 10**10 + (10**10 - ts)
+
+r.zadd('rank:likes', {'article:1001': score(520, 1793000000),
+ 'article:1002': score(520, 1793000300),
+ 'article:1003': score(980, 1793000600)})
+print(r.zrevrange('rank:likes', 0, 2, withscores=True))
+
+pipe = r.pipeline(transaction=False)
+for i in range(1_000_000):
+ pipe.zadd('rank:big', {f'u:{i}': random.randint(0, 10**6)})
+ if i % 10000 == 9999:
+ pipe.execute()
+pipe.execute()
+```
+
+内存是写入前后 INFO memory 中 used_memory 的差。之后执行 CONFIG SET slowlog-log-slower-than 0,让 SLOWLOG 记录所有命令,再把 ZREVRANGE rank:big 0 9 WITHSCORES、ZREVRANK rank:big u:123456、ZINCRBY rank:big 1 u:123456 各执行 5 次:
+
+```text
+[('article:1003', 9808206999400.0), ('article:1001', 5208207000000.0), ('article:1002', 5208206999700.0)]
+100 万成员内存 104.7 MB
+OBJECT ENCODING rank:big -> skiplist
+SLOWLOG 中的执行时间(微秒)
+zrevrange [5, 3, 2, 3, 4]
+zrevrank [4, 3, 3, 3, 3]
+zincrby [5, 5, 4, 6, 7]
+```
+
+### A3 单线程的吞吐量和 CPU 时间
+
+```bash
+redis-benchmark -c 50 -n 1000000 -t get -P 1 -q
+redis-benchmark -c 50 -n 1000000 -t get -P 16 -q
+# 每次测试前后各执行一次 INFO cpu,取 used_cpu_sys、used_cpu_user 的差
+```
+
+```text
+== -P 1
+GET: 162522.34 requests per second, p50=0.231 msec
+服务器 CPU:used_cpu_sys +5.13 s,used_cpu_user +1.00 s
+== -P 16
+GET: 1618123.00 requests per second, p50=0.415 msec
+服务器 CPU:used_cpu_sys +0.28 s,used_cpu_user +0.34 s
+```
+
+### A4 Redis Cluster
+
+6 个节点 7001~7006 都以 --cluster-enabled yes --cluster-node-timeout 5000 启动,然后创建 3 主 3 从的集群:
+
+```bash
+redis-cli --cluster create 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 \
+ 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 --cluster-replicas 1 --cluster-yes
+```
+
+槽的分配为 7001:0~5460,7002:5461~10922,7003:10923~16383。
+
+```text
+> redis-cli -p 7001 cluster keyslot user:1001
+5712
+> redis-cli -p 7001 cluster keyslot {user:1001}:cart
+5712
+> redis-cli -p 7001 cluster keyslot {user:1001}:orders
+5712
+> redis-cli -p 7001 set user:1001 alice
+MOVED 5712 127.0.0.1:7002
+> redis-cli -p 7002 set user:1001 alice
+OK
+> redis-cli -p 7002 mset user:1001:cart a user:1001:orders b
+CROSSSLOT Keys in request don't hash to the same slot
+> redis-cli -p 7002 mset {user:1001}:cart a {user:1001}:orders b
+OK
+> redis-cli -p 7002 eval "return redis.call('mset', KEYS[1], 'a', KEYS[2], 'b')" 2 user:1001:cart user:1001:orders
+CROSSSLOT Keys in request don't hash to the same slot
+```
+
+然后 kill -9 7002,每 100 毫秒向它的从节点写一次 user:1001,直到返回 OK。下面是第 3 次实验的输出,3 次实验分别在 7.1、8.7、8.8 秒后恢复写入:
+
+```text
+== 7002 的从节点:7006
+8.8 s 后写入成功,之前失败 73 次,最后一次失败前的返回为 CLUSTERDOWN The cluster is down
+* FAIL message received from ... about ...
+* Start of election delayed for 738 milliseconds (rank #0, offset 138).
+* Starting a failover election for epoch 7.
+* Failover election won: I'm the new master.
+> redis-cli -p 7006 get user:1001
+bob
+```
+
+### A5 Sentinel 故障转移
+
+1 主 6400、2 从 6401 和 6402,3 个哨兵 26400~26402,哨兵的配置:
+
+```text
+sentinel monitor mymaster 127.0.0.1 6400 2
+sentinel down-after-milliseconds mymaster 5000
+sentinel failover-timeout mymaster 60000
+```
+
+向 6400 写入 order:1024 后 kill -9 6400,每 100 毫秒用 SENTINEL get-master-addr-by-name mymaster 查询主服务器地址。下面的时间相对于 kill -9,日志取自 3 个哨兵中最先记录该事件的一个:
+
+```text
+== kill -9 6400 后 6.20 s,SENTINEL get-master-addr-by-name 返回 127.0.0.1:6401
+ 5.02 s 哨兵 26402 # +sdown master mymaster 127.0.0.1 6400
+ 5.10 s 哨兵 26400 # +odown master mymaster 127.0.0.1 6400 #quorum 2/2
+ 5.18 s 哨兵 26400 # +elected-leader master mymaster 127.0.0.1 6400
+ 5.24 s 哨兵 26400 # +selected-slave slave 127.0.0.1:6401 127.0.0.1 6401 @ mymaster 127.0.0.1 6400
+ 6.13 s 哨兵 26400 # +promoted-slave slave 127.0.0.1:6401 127.0.0.1 6401 @ mymaster 127.0.0.1 6400
+ 6.18 s 哨兵 26401 # +switch-master mymaster 127.0.0.1 6400 127.0.0.1 6401
+> GET order:1024(新的主服务器 6401)
+paid
+```
+
+重启 6400 后,新的主服务器 6401 的日志:
+
+```text
+* Partial resynchronization request from 127.0.0.1:6402 accepted. Sending 156 bytes of backlog starting from offset 2495.
+* Partial resynchronization not accepted: Replication ID mismatch (Replica asked for 'f16e5fc0...', my replication IDs are 'ac108f0e...' and '48696b86...')
+> 6400 的角色
+role:slave
+master_port:6401
+> CONFIG SET min-replicas-to-write 3 后写入(只有 2 个从服务器)
+NOREPLICAS Not enough good replicas to write.
+```
+
## 参考资料
-- Carlson J L. Redis in Action[J]. Media.johnwiley.com.au, 2013.
+- Carlson J L. Redis in Action [M]. Manning, 2013.
- [黄健宏. Redis 设计与实现 [M]. 机械工业出版社, 2014.](http://redisbook.com/index.html)
- [REDIS IN ACTION](https://web.archive.org/web/20210421115011/https://redislabs.com/ebook/foreword/)
-- [Skip Lists: Done Right](http://ticki.github.io/blog/skip-lists-done-right/)
+- [Skip Lists: Done Right](https://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)
+- [Key eviction | Redis](https://redis.io/docs/latest/develop/reference/eviction/)
+- [EXPIRE | Redis](https://redis.io/docs/latest/commands/expire/)
+- [Redis Streams](https://redis.io/docs/latest/develop/data-types/streams/)
- [Redis: Scale with Redis Cluster](https://redis.io/docs/latest/operate/oss_and_stack/management/scaling/)
- [High availability with Redis Sentinel](https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/)
- [Redis replication](https://redis.io/docs/latest/operate/oss_and_stack/management/replication/)
- [Redis persistence](https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/)
- [Transactions | Redis](https://redis.io/docs/latest/develop/interact/transactions/)
- [Scripting with Lua | Redis](https://redis.io/docs/latest/develop/interact/programmability/eval-intro/)
+- [Redis 7.4.1 源码 dict.c](https://github.com/redis/redis/blob/7.4.1/src/dict.c)
+- [Redis 7.4.1 源码 t_zset.c](https://github.com/redis/redis/blob/7.4.1/src/t_zset.c)
diff --git a/notes/pics/redis-cluster.png b/notes/pics/redis-cluster.png
index 4603be93..1c9b343d 100644
Binary files a/notes/pics/redis-cluster.png and b/notes/pics/redis-cluster.png differ
diff --git a/notes/pics/redis-rehash.png b/notes/pics/redis-rehash.png
new file mode 100644
index 00000000..24b4580c
Binary files /dev/null and b/notes/pics/redis-rehash.png differ
diff --git a/notes/pics/redis-sentinel-failover.png b/notes/pics/redis-sentinel-failover.png
index d634440e..250604eb 100644
Binary files a/notes/pics/redis-sentinel-failover.png and b/notes/pics/redis-sentinel-failover.png differ