diff --git a/notes/Redis - 进阶.md b/notes/Redis - 进阶.md
new file mode 100644
index 00000000..bd5066ac
--- /dev/null
+++ b/notes/Redis - 进阶.md
@@ -0,0 +1,128 @@
+# Redis - 进阶
+
+* [Redis - 进阶](#redis---进阶)
+ * [四、数据结构](#四数据结构)
+ * [对象编码](#对象编码)
+ * [字典](#字典)
+ * [十、事件](#十事件)
+ * [文件事件](#文件事件)
+ * [时间事件](#时间事件)
+ * [事件的调度与执行](#事件的调度与执行)
+ * [附录:实测](#附录实测)
+ * [A1 SDS 的空间预分配](#a1-sds-的空间预分配)
+ * [参考资料](#参考资料)
+
+
+
+## 四、数据结构
+
+### 对象编码
+
+主线见[Redis · 对象编码](Redis.md#对象编码),这里接着 SDS 的空间预分配,看追加时实际分配了多少内存。
+
+例如对 60 字节、没有剩余空间的字符串再 APPEND 1 字节,SDS 按 2 × 61 = 122 字节申请(内存分配器向上取整为 128 字节),追加后剩余 63 字节;对 2040000 字节的字符串追加后剩余约 1 MB(Redis 7.4 实测,见[附录 A1](#a1-sds-的空间预分配))。
+
+### 字典
+
+主线见[Redis · 字典](Redis.md#字典),这里展开渐进式 rehash 的具体过程:
+
+- 字典中有两张哈希表 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 7.4 的 dict.c。Redis 7.0 之前的两张表是 dict 中的 dictht ht[2],渐进式 rehash 的过程相同。
+
+## 十、事件
+
+主线见[Redis · 十、事件](Redis.md#十事件),以下三节讲 Redis 的事件循环是怎样实现的;单线程的 Redis 为什么快,见主线的[单线程为什么快](Redis.md#单线程为什么快)。
+
+### 文件事件
+
+服务器通过套接字与客户端或者其它服务器进行通信,文件事件就是对套接字操作的抽象。
+
+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)
+ # 处理所有已产生的文件事件
+ processFileEvents()
+ # 处理所有已到达的时间事件
+ processTimeEvents()
+```
+
+将 aeProcessEvents 函数置于一个循环里面,加上初始化和清理函数,就构成了 Redis 服务器的主函数,伪代码如下:
+
+```python
+def main():
+ # 初始化服务器
+ init_server()
+ # 一直处理事件,直到服务器关闭为止
+ while server_is_not_shutdown():
+ aeProcessEvents()
+ # 服务器关闭,执行清理操作
+ clean_server()
+```
+
+从事件处理的角度来看,服务器运行流程如下:
+
+
+
+## 附录:实测
+
+以下实验的环境为 Apple M1 Pro(10 核)、macOS,客户端和服务器在同一台机器上,Redis 7.4.1 从源码编译,使用默认配置,不开启持久化。
+
+### A1 SDS 的空间预分配
+
+Redis 7.4.1(内存分配器为 libc),启动时加 --enable-debug-command yes,用 DEBUG SDSLEN 查看值的 len、avail(剩余空间)和实际分配的内存:
+
+```text
+SET s(50 字节) val_sds_len:50, val_sds_avail:10, val_zmalloc: 64 encoding raw
+APPEND s 10 字节 val_sds_len:60, val_sds_avail:0, val_zmalloc: 64
+APPEND s 1 字节 val_sds_len:61, val_sds_avail:63, val_zmalloc: 128
+SET big(2000000 字节) val_sds_len:2000000, val_sds_avail:31606, val_zmalloc: 2031616
+APPEND big 40000 字节 val_sds_len:2040000, val_sds_avail:1072950, val_zmalloc: 3112960
+SET 44 字节 encoding embstr,SET 45 字节 encoding raw
+```
+
+对应的源码在 sds.c 的 _sdsMakeRoomFor():剩余空间足够时直接返回;否则新长度小于 SDS_MAX_PREALLOC(1 MB)时乘以 2,不小于时加上 1 MB。SET 50 字节时的 10 字节、SET 大字符串时的 31606 字节剩余空间来自内存分配器的取整和协议缓冲区,不是预分配。
+
+## 参考资料
+
+- [黄健宏. Redis 设计与实现 [M]. 机械工业出版社, 2014.](http://redisbook.com/index.html)
+- [Redis 7.4.1 源码 dict.c](https://github.com/redis/redis/blob/7.4.1/src/dict.c)
+- [Redis 7.4.1 源码 sds.c](https://github.com/redis/redis/blob/7.4.1/src/sds.c)