mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-10-30 18:23:52 +00:00
Update Redis.md
为面试中常问的小节加上高频标记;补充 HASH 编码转换的示意图和实测(附录 A14),以及单线程模型的示意图。
This commit is contained in:
+61
-17
@@ -3,7 +3,7 @@
|
||||
* [Redis](#redis)
|
||||
* [一、概述](#一概述)
|
||||
* [与 MySQL 的区别](#与-mysql-的区别)
|
||||
* [二、数据类型](#二数据类型)
|
||||
* [二、数据类型](#二数据类型) 🔥🔥
|
||||
* [STRING](#string)
|
||||
* [LIST](#list)
|
||||
* [SET](#set)
|
||||
@@ -11,7 +11,7 @@
|
||||
* [ZSET](#zset)
|
||||
* [BITMAP](#bitmap)
|
||||
* [HyperLogLog 与 GEO](#hyperloglog-与-geo)
|
||||
* [三、使用场景](#三使用场景)
|
||||
* [三、使用场景](#三使用场景) 🔥
|
||||
* [计数器](#计数器)
|
||||
* [缓存](#缓存)
|
||||
* [查找表](#查找表)
|
||||
@@ -21,7 +21,7 @@
|
||||
* [排行榜](#排行榜)
|
||||
* [其它](#其它)
|
||||
* [四、数据结构](#四数据结构)
|
||||
* [对象编码](#对象编码)
|
||||
* [对象编码](#对象编码) 🔥🔥
|
||||
* [字典](#字典)
|
||||
* [跳跃表](#跳跃表)
|
||||
* [五、Redis 与 Memcached](#五redis-与-memcached)
|
||||
@@ -30,20 +30,20 @@
|
||||
* [分布式](#分布式)
|
||||
* [内存管理机制](#内存管理机制)
|
||||
* [六、键的过期时间](#六键的过期时间)
|
||||
* [七、数据淘汰策略](#七数据淘汰策略)
|
||||
* [八、持久化](#八持久化)
|
||||
* [七、数据淘汰策略](#七数据淘汰策略) 🔥
|
||||
* [八、持久化](#八持久化) 🔥🔥
|
||||
* [RDB 持久化](#rdb-持久化)
|
||||
* [AOF 持久化](#aof-持久化)
|
||||
* [九、事务](#九事务)
|
||||
* [Lua 脚本](#lua-脚本)
|
||||
* [Lua 脚本](#lua-脚本) 🔥
|
||||
* [十、事件](#十事件)
|
||||
* [单线程为什么快](#单线程为什么快)
|
||||
* [单线程为什么快](#单线程为什么快) 🔥
|
||||
* [十一、复制](#十一复制)
|
||||
* [连接过程](#连接过程)
|
||||
* [主从链](#主从链)
|
||||
* [十二、Sentinel](#十二sentinel)
|
||||
* [十三、分片](#十三分片)
|
||||
* [Redis Cluster](#redis-cluster)
|
||||
* [Redis Cluster](#redis-cluster) 🔥
|
||||
* [十四、一个简单的论坛系统分析](#十四一个简单的论坛系统分析)
|
||||
* [文章信息](#文章信息)
|
||||
* [点赞功能](#点赞功能)
|
||||
@@ -62,6 +62,7 @@
|
||||
* [A11 持久化与脚本的默认配置](#a11-持久化与脚本的默认配置)
|
||||
* [A12 长时间运行的 Lua 脚本](#a12-长时间运行的-lua-脚本)
|
||||
* [A13 槽迁移与 ASK 重定向](#a13-槽迁移与-ask-重定向)
|
||||
* [A14 HASH 编码的转换](#a14-hash-编码的转换)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
@@ -83,7 +84,7 @@ Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,
|
||||
|
||||
数据都在内存中时,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#缓存一致性)。
|
||||
|
||||
## 二、数据类型
|
||||
## 二、数据类型 🔥🔥
|
||||
|
||||
| 数据类型 | 可以存储的值 | 操作 |
|
||||
| :--: | :--: | :--: |
|
||||
@@ -309,7 +310,7 @@ HyperLogLog(HLL)用于估计一个集合中不同元素的个数,例如页
|
||||
|
||||
GEO 用于“附近的门店”这类查询:GEOADD 保存经纬度,GEOSEARCH 查找某个点一定半径或矩形内的成员并按距离排序,GEODIST 计算两点间的距离。它底层就是一个 ZSET,经纬度交错编码成 52 位的 Geohash 整数作为分值,相邻的点分值相近,查询时只需查找覆盖查询范围的 9 个区域对应的分值区间;代价是距离和范围有误差,官方文档说明最坏可达 0.5%。
|
||||
|
||||
## 三、使用场景
|
||||
## 三、使用场景 🔥
|
||||
|
||||
### 计数器
|
||||
|
||||
@@ -373,7 +374,7 @@ Set 可以实现交集、并集等操作,从而实现共同好友等功能。
|
||||
|
||||
## 四、数据结构
|
||||
|
||||
### 对象编码
|
||||
### 对象编码 🔥🔥
|
||||
|
||||
同一种数据类型会根据元素的数量和大小选择不同的底层结构,称为编码,可以用 OBJECT ENCODING 查看。元素少时使用 listpack,它把所有元素连续地存放在一块内存中,节省内存,查找时虽然要逐个比较,但元素很少时并不慢;元素增多后转换为查找更快的结构。以下是在 Redis 7.4 上用默认配置验证的结果:
|
||||
|
||||
@@ -387,6 +388,10 @@ Set 可以实现交集、并集等操作,从而实现共同好友等功能。
|
||||
|
||||
例如一个 HASH 有 512 个字段时编码为 listpack,再写入一个字段就变为 hashtable。ZSET 转换后同时使用跳跃表和字典:字典按成员查分数,跳跃表按分数排序和范围查询。
|
||||
|
||||
<div align="center"> <img src="pics/redis-hash-encoding.png" width="760px"/> </div><br>
|
||||
|
||||
转换后内存明显增加:512 个字段的 HASH 用 listpack 约占 10 KB,再写入一个字段转为 hashtable 后约占 40 KB(Redis 7.4 实测,见[附录 A14](#a14-hash-编码的转换))。
|
||||
|
||||
Redis 7.0 之前用的是压缩列表(ziplist),每个元素要记录前一个元素的长度,这个长度占 1 或 5 个字节,在中间插入或删除元素时可能让后面的元素依次变长,称为连锁更新;listpack 的每个元素只记录自己的长度,避免了这个问题。
|
||||
|
||||
字符串的内容保存在简单动态字符串(Simple Dynamic String, SDS)中,它在字符数组前面记录了已用的长度 len 和分配的容量 alloc:
|
||||
@@ -461,7 +466,7 @@ Redis 可以为每个键设置过期时间,过期时间保存在一个单独
|
||||
|
||||
对于 HASH 这种容器,Redis 7.4 之前只能为整个键设置过期时间,7.4 开始可以使用 HEXPIRE 等命令为单个字段设置过期时间。
|
||||
|
||||
## 七、数据淘汰策略
|
||||
## 七、数据淘汰策略 🔥
|
||||
|
||||
可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。
|
||||
|
||||
@@ -492,7 +497,7 @@ LFU 复用这 24 位:高 16 位是上次访问的时间(分钟),低 8
|
||||
- 同一个实例既存缓存又存不能丢的数据时,给缓存设置过期时间并用 volatile-lru,只淘汰缓存,官方文档更建议把两类数据放到两个实例中;
|
||||
- 当作数据库使用、数据不能被淘汰时用 noeviction,内存满时写入报错,由应用发现并处理。
|
||||
|
||||
## 八、持久化
|
||||
## 八、持久化 🔥🔥
|
||||
|
||||
Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
|
||||
|
||||
@@ -573,7 +578,7 @@ QUEUED
|
||||
|
||||
WATCH 可以实现乐观锁:EXEC 之前被 WATCH 的键被其它客户端修改,EXEC 返回 nil,事务中的命令都不执行,由客户端决定是否重试。
|
||||
|
||||
### Lua 脚本
|
||||
### Lua 脚本 🔥
|
||||
|
||||
事务中的命令在 EXEC 时才一起执行,不能根据前一条命令的结果决定下一条做什么。Lua 脚本可以做到这一点,并且整个脚本原子执行:Redis 用一个线程执行命令,执行脚本时不处理其它客户端的命令,整个脚本就像一条命令。用事务实现“先读后写”时,只能用 WATCH 检测冲突后重试;流水线只是减少网络往返,几条命令之间仍可能插入其它客户端的命令。
|
||||
|
||||
@@ -606,7 +611,7 @@ return n + 1
|
||||
|
||||
Redis 服务器是一个事件驱动程序:主线程在一个循环中用 I/O 多路复用等待套接字上的读写事件(文件事件),处理完这些事件后再执行到期的定时任务(时间事件)。事件的分类和调度过程见[进阶:事件](Redis%20-%20进阶.md#十事件)。
|
||||
|
||||
### 单线程为什么快
|
||||
### 单线程为什么快 🔥
|
||||
|
||||
Redis 用一个线程执行所有命令,仍然很快,原因主要有以下几点:
|
||||
|
||||
@@ -617,6 +622,8 @@ Redis 用一个线程执行所有命令,仍然很快,原因主要有以下
|
||||
|
||||
用 redis-benchmark 测试,50 个连接逐条发送 GET,每秒约 16 万次;每次连续发送 16 条命令(流水线)时,每秒约 160 万次。逐条发送 100 万次 GET 时,服务器消耗的 CPU 时间中内核态 5.1 秒、用户态 1.0 秒:大部分时间花在网络读写的系统调用上,解析和执行一条命令只需要 1 微秒左右(Redis 7.4 实测,见[附录 A3](#a3-单线程的吞吐量和-cpu-时间))。
|
||||
|
||||
<div align="center"> <img src="pics/redis-single-thread.png" width="760px"/> </div><br>
|
||||
|
||||
所以 Redis 的瓶颈通常是网络和内存,而不是 CPU。Redis 6.0 加入了 I/O 多线程(io-threads,默认关闭),开启后默认只把写回客户端交给 I/O 线程,读取和解析请求还需要再打开 io-threads-do-reads;Redis 8.0 重新实现了 I/O 多线程,I/O 线程同时负责读取、解析和写回,io-threads-do-reads 被废弃。两种实现中命令都由主线程逐个执行,不会有多个线程同时修改一个键,单条命令的原子性不变。此外,关闭文件、AOF 刷盘和 UNLINK 释放大键的内存这些耗时操作由后台线程完成。
|
||||
|
||||
需要注意的是,单线程也意味着一条慢命令会让所有客户端等待:执行 KEYS *、对大 Key 执行 HGETALL 或 DEL、运行很长的 Lua 脚本时,其它命令都在排队,见 [大 Key 与热 Key](缓存.md#大-key-与热-key)。
|
||||
@@ -637,7 +644,7 @@ Redis 用一个线程执行所有命令,仍然很快,原因主要有以下
|
||||
|
||||
Redis 7.0 起默认不把快照写入磁盘,而是直接通过网络发送给从服务器(repl-diskless-sync yes)。
|
||||
|
||||
以上是第一次连接时的全量复制。从服务器断线重连后,会带上复制 ID 和自己已经复制到的偏移量发送 PSYNC 命令:如果主服务器的复制积压缓冲区(默认 1 MB)中还保存着之后的写命令,就只发送缺少的部分,称为部分重同步;否则重新进行全量复制。写入量大、断线时间长时,可以调大 repl-backlog-size,避免频繁全量复制。
|
||||
以上是第一次连接时的全量复制。从服务器断线重连后,会带上复制 ID 和下一个需要的偏移量(已经复制到的偏移量加 1)发送 PSYNC 命令:如果复制 ID 与主服务器一致(故障转移后,新的主服务器也接受旧主服务器的复制 ID),并且这个偏移量还在主服务器的复制积压缓冲区(默认 1 MB)中,就只发送缺少的部分,称为部分重同步;否则重新进行全量复制。写入量大、断线时间长时,可以调大 repl-backlog-size,避免频繁全量复制。
|
||||
|
||||
### 主从链
|
||||
|
||||
@@ -678,7 +685,7 @@ down-after-milliseconds 为 5 秒时,从主服务器宕机到客户端查到
|
||||
- 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上;
|
||||
- 服务器分片:Redis Cluster,使用 CRC16(key) % 16384 计算键所属的哈希槽,再把 16384 个哈希槽分配给各个节点。
|
||||
|
||||
### Redis Cluster
|
||||
### Redis Cluster 🔥
|
||||
|
||||
Redis 有三种常见的部署方式,解决的问题不同:
|
||||
|
||||
@@ -1134,6 +1141,43 @@ OK
|
||||
MOVED 5712 127.0.0.1:7003
|
||||
```
|
||||
|
||||
### A14 HASH 编码的转换
|
||||
|
||||
默认配置下 hash-max-listpack-entries 为 512、hash-max-listpack-value 为 64。用 redis-py 的流水线写入字段 field1~fieldN、值 value1~valueN,记录写入前后 INFO memory 中 used_memory 的差,并用 MEMORY USAGE 对照:
|
||||
|
||||
```python
|
||||
def used(): return r.info('memory')['used_memory']
|
||||
for n in (3, 512, 513):
|
||||
key = f'user:1001:h{n}'
|
||||
before = used()
|
||||
p = r.pipeline(transaction=False)
|
||||
for i in range(1, n + 1): p.hset(key, f'field{i}', f'value{i}')
|
||||
p.execute()
|
||||
print(f'{n} 个字段:{r.object("encoding", key)},used_memory 增加 {used() - before} 字节,MEMORY USAGE {r.memory_usage(key, samples=0)} 字节')
|
||||
key = 'user:1001:h512'
|
||||
print('对 512 个字段的键再 HSET 一个字段:')
|
||||
before = r.memory_usage(key, samples=0); b2 = used()
|
||||
r.hset(key, 'field513', 'value513')
|
||||
print(f' 编码 {r.object("encoding", key)},MEMORY USAGE {before} → {r.memory_usage(key, samples=0)} 字节,used_memory 增加 {used() - b2} 字节')
|
||||
r.hset('user:1001', mapping={'name': 'Alice', 'city': 'Shanghai'})
|
||||
print('user:1001', r.object('encoding', 'user:1001'))
|
||||
r.hset('user:1001', 'bio', 'x' * 65)
|
||||
print('写入 65 字节的值后', r.object('encoding', 'user:1001'))
|
||||
```
|
||||
|
||||
```text
|
||||
hash-max-listpack-entries {'hash-max-listpack-entries': '512'} hash-max-listpack-value {'hash-max-listpack-value': '64'}
|
||||
3 个字段:listpack,used_memory 增加 74288 字节,MEMORY USAGE 120 字节
|
||||
512 个字段:listpack,used_memory 增加 10304 字节,MEMORY USAGE 10296 字节
|
||||
513 个字段:hashtable,used_memory 增加 41120 字节,MEMORY USAGE 37032 字节
|
||||
对 512 个字段的键再 HSET 一个字段:
|
||||
编码 hashtable,MEMORY USAGE 10296 → 37032 字节,used_memory 增加 30848 字节
|
||||
user:1001 listpack
|
||||
写入 65 字节的值后 hashtable
|
||||
```
|
||||
|
||||
3 个字段那一行的 used_memory 增量包含了客户端缓冲区等一次性的分配,不代表这个键的大小。
|
||||
|
||||
## 参考资料
|
||||
|
||||
- Carlson J L. Redis in Action [M]. Manning, 2013.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 192 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 205 KiB |
Reference in New Issue
Block a user