type
status
date
slug
summary
tags
category
icon
password
先给出一个整体的思考框架
绝对可以!让我从第一性原理构建Redis的完整思考框架。
第一性原理:Redis要解决的根本问题
核心矛盾:速度 vs 持久化
根本问题:
- 数据库太慢:磁盘IO成为瓶颈(10ms级别)
- 内存太贵:无法存储所有数据
- 缓存太复杂:需要处理一致性、过期、淘汰
Redis的使命:
在内存中提供接近数据库的功能,但保持极致的性能
Redis第一性原理思考框架
核心架构:分层解决不同问题
第1层:数据结构层 - 为什么需要丰富的数据结构?
根本问题:不同业务场景需要不同的数据组织方式
如果只有简单的key-value会怎样?
Redis数据结构的设计原理
1. String - 最基础的存储单元
底层实现:
设计优势:
- O(1)获取长度
- 预分配空间,减少内存重分配
- 二进制安全
2. Hash - 对象存储的最佳选择
底层实现:
设计优势:
- 内存占用少(相比多个String key)
- 局部更新效率高
- 自动优化存储结构
3. List - 队列和栈的完美实现
底层实现:
4. Set - 去重和集合运算
5. Sorted Set - 排行榜的天然选择
底层实现:
第2层:内存管理层 - 如何管理有限的内存?
根本问题:内存是有限的,如何高效利用?
1. 内存淘汰策略
问题:内存满了怎么办?
Redis的8种淘汰策略:
LRU vs LFU对比:
2. 过期键处理
三种删除策略:
实际工作流程:
第3层:持久化层 - 如何保证数据不丢失?
根本问题:内存数据如何持久化?
RDB vs AOF:两种不同的解决思路
RDB(Redis Database):快照方式
工作原理:
AOF(Append Only File):日志方式
工作原理:
混合持久化:最佳实践
优势:
- 恢复速度:RDB快照快速加载大量数据
- 数据完整性:AOF保证最新数据不丢失
- 文件大小:避免AOF文件过大
第4层:网络通信层 - 如何快速处理大量请求?
根本问题:网络IO成为瓶颈
单线程模型的设计哲学
为什么Redis选择单线程?
事件驱动模型
关键优化:
第5层:集群管理层 - 如何扩展容量和能力?
根本问题:单机容量和性能的限制
主从复制:解决读扩展问题
复制原理:
哨兵模式:解决可用性问题
工作原理:
集群模式:解决存储扩展问题
实际应用场景的解决方案
场景1:电商网站的商品缓存
场景2:社交媒体的消息队列
场景3:游戏排行榜
Redis完整思考框架
遇到Redis相关问题时的分析步骤:
1. 问题定位
- 性能问题:是CPU、内存、网络还是磁盘瓶颈?
- 功能问题:需要什么数据结构和操作?
- 可靠性问题:数据丢失、故障转移、一致性?
- 扩展性问题:容量不够、QPS不够?
2. 解决方案选择
- 数据结构层:选择合适的数据类型
- 内存管理层:配置淘汰策略和过期策略
- 持久化层:选择RDB、AOF或混合持久化
- 网络通信层:优化网络传输和协议
- 集群管理层:选择主从、哨兵或集群模式
3. 性能优化
- 减少网络往返:Pipeline、批量操作
- 优化数据结构:选择内存效率高的结构
- 合理使用内存:监控内存使用,避免大key
- 配置调优:根据场景调整参数
4. 运维监控
- 关键指标:QPS、延迟、内存使用、命中率
- 故障预防:监控报警、容量规划
- 故障处理:快速定位和恢复
核心总结
Redis的设计哲学:
- 简单就是美:单线程模型,避免复杂的并发控制
- 内存优先:所有数据在内存中,保证极致性能
- 数据结构丰富:不同场景使用不同的数据结构
- 持久化可选:根据业务需求选择持久化策略
- 扩展灵活:支持多种集群模式
实际应用原则:
- 选择合适的数据结构:String、Hash、List、Set、Sorted Set
- 合理配置内存管理:淘汰策略、过期策略
- 根据业务选择持久化方案:RDB、AOF、混合持久化
- 监控关键指标:性能、内存、可用性
- 设计容错和扩展方案:主从、哨兵、集群
Redis不仅仅是一个缓存,它是一个高性能的数据结构服务器,理解其设计原理有助于在实际项目中发挥其最大价值!
第一章 数据结构与对象
redis数据库里面的每个key-value pair都是对象组成的
key总是一个string obj
value可以是字符串,list obj, hash obj, set obj, sorted set obj
简单动态字符串
127.0.0.1:6379[15]> set msg "hello world"
OK
127.0.0.1:6379[15]> rpush fruits "apple" "banana"
(integer) 2
key是一个字符串对象
value存的字符串底层是SDS实现的
sds的定义
struct sdshdr {
int len; // 已使用的字节数
int free; // 未使用的字节数
char buf[]; // 字符数组,用于保存字符串
};
与C字符串区别:
SDS 相比传统 C 字符串有以下优点:
- 获取字符串长度的时间复杂度是 O(1),而不是 O(n)
- 杜绝缓冲区溢出
- 减少字符串修改时的内存重分配次数
- 二进制安全,可以存储任何二进制数据
- 兼容部分 C 字符串函数

获取字符串的时间复杂度不同
杜绝溢出


内存重分配是个耗时的操作,
sds利用空间预分配和惰性空间释放两个策略
空间预分配:
当 SDS 字符串需要增长时,Redis 不仅会分配所需的空间,还会额外分配一些预留空间。具体规则如下:
- 当新长度小于 1MB 时:额外分配与当前长度相同的未使用空间
- 例如:如果扩展后字符串长度为 11 字节,则会额外分配 11 字节的空闲空间,总容量为 22 字节
- 当新长度大于等于 1MB 时:额外固定分配 1MB 的未使用空间
- 例如:如果扩展后字符串长度为 5MB,则会额外分配 1MB 的空闲空间,总容量为 6MB
调用sdscat(s,”World”)
等额分配

链表
listNode结构:
typedef struct listNode {
struct listNode *prev; // 前置节点
struct listNode *next; // 后置节点
void *value; // 节点的值
} listNode;
list结构:
typedef struct list {
listNode *head; // 表头节点
listNode *tail; // 表尾节点
unsigned long len; // 链表所包含的节点数量
void *(*dup)(void *ptr);// 节点值复制函数
void (*free)(void *ptr);// 节点值释放函数
int (*match)(void *ptr, void *key);// 节点值比较函数
} list;
Redis 链表的特性
- 双向链表:每个节点都有指向前后节点的指针,支持双向遍历
- 无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,链表不会形成环
- 带链表长度计数器:通过
len
属性可以 O(1) 时间复杂度获取链表节点数量
- 多态:链表节点的 value 指针可以指向不同类型的值,通过 dup、free 和 match 函数可以针对不同类型的值进行适配

Hash

key 里面又有key-value 就是字典

哈希算法与索引计算
当要将一个新的键值对添加到字典中时,Redis 会执行以下步骤:
- 使用哈希函数计算键的哈希值
- 通过哈希值与 sizemask 进行位运算(按位与,&)得到索引值
- 根据索引值将包含键值对的节点放入哈希表数组的指定位置
// 计算哈希值与索引的伪代码
hash = dict->type->hashFunction(key);
index = hash & dict->ht[0].sizemask;
哈希冲突解决
Redis 使用链地址法(separate chaining)来解决哈希冲突。当两个或更多键被分配到哈希表数组的同一个索引上时,Redis 会将这些键值对以链表的形式连接起来。

渐进式rehash
当哈希表需要扩展或收缩时,Redis 不会一次性地将所有键值对从旧表迁移到新表,而是分多次、渐进式地完成。这样可以避免大字典的 rehash 操作阻塞服务器。

阶段 1:开始前的状态
如上图所示,在 rehash 开始前:
- dict 结构体中的 rehashidx 字段为 -1,表示没有在进行 rehash
- 字典只使用 ht[0] 哈希表,ht[1] 为空
- 假设当前有 3 个键值对存储在 ht[0] 中,ht[0].size = 4,负载因子为 0.75
阶段 2:rehash 开始
当负载因子超过阈值,rehash 开始:
- 为 ht[1] 分配空间:
- 扩容情况下,ht[1].size = 第一个 >= ht[0].used * 2 的 2^n (保证为 2 的幂)
- 缩容情况下,ht[1].size = 第一个 >= ht[0].used 的 2^n
- 在我们的例子中,ht[1].size = 8(因为 3 * 2 = 6,而 8 是大于 6 的最小 2 的幂)
- 设置 rehashidx = 0:
- 将 dict 的 rehashidx 设置为 0,标志着 rehash 正式开始
- rehashidx 表示当前正在迁移 ht[0] 中的哪个索引下的链表
阶段 3:渐进式迁移
在 rehash 进行期间,每次字典操作(如增删改查)都会顺带迁移一部分键值对:
- 迁移过程:
- 对 ht[0].table[rehashidx] 索引上的所有键值对,使用 ht[1] 的大小和掩码重新计算索引值
- 将这些键值对放到 ht[1] 的对应位置
- 清空 ht[0].table[rehashidx]
- rehashidx++ (将 rehashidx 加 1,表示处理下一个索引)
- 图中可视化:
- 在阶段 3 的图示中,rehashidx = 2,表示索引 0 和 1 已经完成迁移
- "name" 和 "score" 键值对已经被迁移到了 ht[1] 中
- "age" 键值对位于 ht[0] 的索引 2 上,还未被迁移
- 绿色虚线框表示已经迁移的索引,红色虚线框表示当前正在迁移的索引
- 查找过程:
- 在 rehash 期间,查找一个键时,会先在 ht[0] 中查找,如果没找到再到 ht[1] 中查找
- 这确保了在 rehash 过程中能够正确访问所有的键值对
- 新增键值对:
- 在 rehash 期间,新增的键值对会直接添加到 ht[1] 中
阶段 4:rehash 完成
当 ht[0] 中的所有键值对都迁移到 ht[1] 后:
- 释放 ht[0] 的内存空间
- 将 ht[1] 设置为 ht[0],创建新的空 ht[1]
- 将 rehashidx 重置为 -1,表示 rehash 完成
- 分摊时间复杂度:
- 避免了一次性迁移大量键值对造成的服务阻塞
- 将 O(n) 的操作分散到多次操作中进行,每次只处理一小部分
- 定时任务辅助迁移:
- 除了正常操作触发的迁移外,Redis 还会在定时任务中主动执行一定量的 rehash 工作
- 这确保了即使字典没有新的操作,rehash 也能逐步完成
- 双表并存期间的空间开销:
- 在 rehash 期间,需要同时维护两个哈希表,会占用更多内存
- 这是用空间换时间的一种策略

ZSet
用途:
- 有序集合(Sorted Set):跳表是Redis有序集合的底层实现之一,另一种是压缩列表(ziplist)。
- 当有序集合的元素数量较少且元素值较小时,Redis使用压缩列表
- 当元素数量增长或出现较大元素时,Redis会将其转换为跳表实现
- 排行榜功能:利用有序集合可以实现游戏排行榜、积分排名等功能。
- 范围查询:跳表特别适合范围查询操作,如ZRANGEBYSCORE命令
跳表为什么从最高层开始?
查找路径示例
核心原理:逐步缩小搜索范围
第一性原理:高层索引提供"粗定位",低层索引提供"精确定位"
- 高层:大跨度跳跃,快速接近目标
- 低层:小跨度移动,精确找到目标
这就像在地图上找位置:先看国家地图确定省份,再看省份地图确定城市,最后看城市地图找到具体街道。
Sorted Set核心总结
第一性原理:解决排序数据的多重访问需求
根本问题:业务需要同时支持三种操作模式
- 按排名访问(排行榜)
- 按成员访问(查分数)
- 按分数范围访问(范围查询)
核心矛盾:没有单一数据结构能同时高效支持这三种操作
设计哲学:双结构 + 概率平衡
1. 双数据结构设计
- 跳表:解决排序和范围查询(O(log n))
- 哈希表:解决随机访问(O(1))
- 代价:用空间换时间,数据存储两份
2. 跳表的概率平衡
- 多层索引:高层粗定位,低层精定位
- 随机化:避免最坏情况,保证平均性能
- 简单性:比红黑树实现简单,比B+树更适合内存
核心价值:时间局部性优化
业务特点:排序数据的访问模式
- 经常需要"前几名"(范围查询)
- 经常需要"某人排名"(随机访问)
- 经常需要"分数变化"(插入删除)
技术匹配:
- 跳表的链表结构天然支持范围遍历
- 哈希表提供O(1)的成员查找
- 双结构同步更新保证一致性
适用场景的本质
核心特征:需要"有序 + 快速访问"的业务场景
- 排行榜:按分数排序 + 快速查询排名
- 延迟队列:按时间排序 + 快速获取到期任务
- 权重系统:按权重排序 + 快速更新权重
设计权衡
优势:
- 所有主要操作都是O(log n)或O(1)
- 范围查询高效:O(log n + k)
- 实现相对简单,易于维护
代价:
- 内存开销:数据存储两份
- 更新复杂:需要同时维护两个结构
- 空间局部性:不如数组紧凑
第一性原理的体现
问题分解:将复杂需求分解为基本操作
- 排序 → 跳表解决
- 查找 → 哈希表解决
- 范围 → 跳表的链表特性解决
最优组合:选择各自最适合的数据结构组合
- 不追求单一结构的完美
- 追求组合结构的整体最优
核心洞察:Sorted Set的设计体现了系统设计的核心原则——针对具体问题选择最合适的解决方案组合,而不是试图用一种方案解决所有问题。这就是Redis数据结构设计的智慧所在。
业务背景
电商平台需要实时维护商品热销榜,支持以下核心功能:
- 实时更新销量:每次商品销售后更新热销分数
- 查询商品排名:用户查看某商品在热销榜中的排名
- 获取榜单数据:首页展示热销前20名商品
业务实现
核心观点体现
1. 三种访问模式的完美融合
- 更新销量:需要快速找到商品并更新分数
- 查排名:需要O(1)时间直接获取指定商品排名
- 取榜单:需要高效获取排名前N的商品
2. 双数据结构的价值
- 如果只用数组:更新销量需要O(n)时间重新排序
- 如果只用哈希表:获取榜单需要O(n log n)时间排序
- 跳表+哈希表:所有操作都在O(log n)以内完成
3. 业务场景的时间局部性
- 用户经常查看"热销前20名"(范围查询)
- 商家经常查看"自己商品排名"(随机访问)
- 系统经常更新"商品销量"(插入更新)
性能对比
操作 | 普通方案 | Sorted Set | 业务影响 |
更新销量 | O(n) | O(log n) | 高并发下性能差异巨大 |
查询排名 | O(n) | O(1) | 用户体验天壤之别 |
获取榜单 | O(n log n) | O(log n + k) | 首页加载速度显著提升 |
核心洞察:这个业务场景完美诠释了为什么需要Sorted Set——不是为了技术炫技,而是为了同时满足业务的三种不同访问模式,每种模式都需要高性能支持。这就是"业务驱动技术选型"的最佳实践。
SET
有序的整数集合
127.0.0.1:6379[15]> sadd numbers 1 3 5 7 9
(integer) 5
127.0.0.1:6379[15]> object encoding numbers
"intset"
结构:
typedef struct intset {
uint32_t encoding; // 编码方式
uint32_t length; // 集合包含的元素数量
int8_t contents[]; // 保存元素的数组(柔性数组成员)
} intset;

由encoding决定编码方式
升级
如果新元素添加到集合里,这个新元素比里面的都要长,那就要先升级
升级过程包括以下步骤:
- 根据新编码方式,扩展整数集合底层数组的空间
- 将现有的所有元素转换为新编码方式,并放到正确的位置上
- 将新元素添加到整数集合中


压缩列表
压缩列表(ziplist)是 Redis 中一种为节约内存而设计的特殊数据结构,主要用于存储少量数据项的列表和哈希表。
127.0.0.1:6379[15]> hmset profile "jacl" 28
OK
127.0.0.1:6379[15]> object encoding profile
"ziplist"

节点结构:


连锁更新

压缩列表的特性与优缺点
优点
- 内存效率高:通过特殊编码存储,节省了大量指针开销,并针对小整数值做了特殊优化
- 存取效率高:数据存储在连续内存空间中,有利于CPU缓存
- 实现简单:相比于其他复杂数据结构,实现较为简单
缺点
- 连锁更新问题:当一个节点的长度变化导致其prevlen字段编码方式改变时,可能会引发"连锁更新",最坏情况下的时间复杂度为O(N²)
- 不适合大量元素:随着元素增多,操作效率会降低
- 整体复制开销:任何修改操作可能需要重新分配内存并复制整个列表
对象
Redis 实现了一个对象系统,它在数据类型的底层实现和高层数据类型命令之间充当了中间层
typedef struct redisObject {
unsigned type:4; // 类型
unsigned encoding:4; // 编码
unsigned lru:LRU_BITS; // LRU时间(相对于全局lru_clock)或LFU数据
int refcount; // 引用计数
void *ptr; // 指向实际实现数据结构的指针
} robj;
Redis 支持五种主要的对象类型:
REDIS_STRING
: 字符串对象
REDIS_LIST
: 列表对象
REDIS_HASH
: 哈希对象
REDIS_SET
: 集合对象
REDIS_ZSET
: 有序集合对象
对象编码 (encoding)
每种对象类型可以由不同的底层数据结构实现,这些实现方式被称为对象的编码:
- 字符串对象:int、embstr、raw
- 列表对象:ziplist、linkedlist(3.2版本后改为quicklist)
- 哈希对象:ziplist、hashtable
- 集合对象:intset、hashtable
- 有序集合对象:ziplist、skiplist
Redis 使用引用计数来管理内存。每个对象都有一个引用计数,当计数变为0时,该对象会被释放。
这种机制支持对象共享——当创建一个新对象时,Redis 会尝试查找是否已经存在一个完全相同的对象,如果存在,则增加该对象的引用计数而不是创建新对象。
对象共享:

lru:
这个字段记录了对象最后一次被访问的时间(LRU模式)或者访问频率信息(LFU模式)。Redis 使用这个信息来实现内存回收策略,当内存超出限制时淘汰最近最少使用的对象。
对象系统的好处
- 节省内存:针对不同的使用场景选择最高效的数据结构实现
- 对象共享:通过引用计数实现对象共享,进一步节约内存
- 类型检查:确保对不同类型的键执行正确的命令
- 内存回收:通过引用计数实现内存回收机制
- 内存淘汰:通过LRU/LFU机制实现内存淘汰策略

第二章 redis实现单机数据库
数据库
typedef struct redisDb {
dict *dict; // 数据库键空间,保存所有键值对
dict *expires; // 键过期时间字典
dict *blocking_keys; // 处于阻塞状态的键
dict *ready_keys; // 可以解除阻塞的键
dict *watched_keys; // 被WATCH命令监视的键
int id; // 数据库号码
long long avg_ttl; // 数据库平均TTL统计
unsigned long expires_cursor; // 过期遍历的游标
list *defrag_later; // 推迟碎片整理的键列表
} redisDb;
默认16个数据库
select <dbid> 来切换
每个客户端都有一个
redisClient.db
指针,指向当前选择的数据库
数据库隔离,不同数据库的key空间隔离
数据库key空间例子

key的过期机制
127.0.0.1:6379[15]> EXPIRE key 5
(integer) 1
127.0.0.1:6379[15]> get key
"value"
127.0.0.1:6379[15]> get key
(nil)
127.0.0.1:6379[15]> set key value
OK
127.0.0.1:6379[15]> expire key 20
(integer) 1
127.0.0.1:6379[15]> ttl key
(integer) 17
- 过期字典
- 每个 Redis 数据库都有一个过期字典(
expires
) - 过期字典的键是数据库中的键,值是该键的过期时间(UNIX 时间戳)
- 设置过期时间
EXPIRE key seconds
:设置键的过期时间为指定的秒数PEXPIRE key milliseconds
:以毫秒为单位设置过期时间EXPIREAT key timestamp
:设置键过期的时间戳PEXPIREAT key milliseconds-timestamp
:以毫秒为单位设置过期时间戳
- 移除过期时间
PERSIST key
:移除键的过期时间,使其变为永久有效
- 查询过期时间
TTL key
:返回键的剩余生存时间(秒)PTTL key
:以毫秒为单位返回剩余生存时间
过期键清除策略和内存淘汰
问题的根本矛盾
Redis作为内存数据库面临有限资源与无限需求的矛盾:
- 内存是有限的,但数据需求可能是无限的
- CPU时间是有限的,但清理工作需要消耗CPU
- 用户体验要求连续性,但清理工作可能造成卡顿
过期删除:时间维度的资源管理
核心问题
如何在不影响性能的前提下,及时清理过期数据?
三种策略的权衡
1. 定时删除(理论上的完美方案)
优点:内存友好,过期立即清理
缺点:CPU不友好,大量定时器消耗资源
结论:理论完美,实际不可行
2. 惰性删除(最省CPU的方案)
优点:CPU友好,零额外开销
缺点:内存不友好,不访问的key永远不会被清理
结论:必要但不充分
3. 定期删除(现实的平衡方案)
优点:在CPU和内存之间找到平衡
缺点:需要精心设计抽样策略
结论:实际采用的核心策略
Redis的混合策略
惰性删除 + 定期删除的组合:
设计的精妙之处
1. 自适应清理强度
过期key越多,清理越积极;过期key越少,清理越温和。
2. 时间片限制
每次清理限制时间,避免长时间阻塞。
3. 概率保证
通过随机抽样,保证大部分过期key最终都会被清理。
内存淘汰:空间维度的资源管理
核心问题
当内存不足时,如何选择性地丢弃数据以保证系统继续运行?
淘汰策略的设计哲学
1. noeviction(拒绝服务)
适用场景:数据完整性要求极高的场景
权衡:宁可服务不可用,也不丢失数据
2. allkeys-lru(全局最近最少使用)
适用场景:缓存场景,热点数据访问模式明显
权衡:基于访问局部性原理,保留热点数据
3. allkeys-lfu(全局最近最少频率)
适用场景:访问模式相对稳定的场景
权衡:考虑访问频率而非时间,更适合某些业务模式
4. volatile-* 系列
适用场景:混合使用,部分数据是缓存,部分是持久数据
权衡:保护持久数据不被误删
LRU实现的工程权衡
传统LRU的问题
成本:每次访问都有额外开销
Redis的近似LRU
优点:
- 内存开销小(每key只需3字节)
- CPU开销小(不需要维护链表)
- 效果接近真正的LRU
LFU的巧妙设计
挑战:如何用有限位数记录访问频率?
对数计数的精妙
效果:访问次数越多,频率增长越慢,避免溢出
时间衰减机制
效果:让历史热点数据逐渐"冷却",适应访问模式变化
两个机制的协同关系
层次化的内存管理
时间维度的配合
设计原则的体现
1. 渐进式处理
无论是定期删除还是内存淘汰,都采用分批处理,避免长时间阻塞。
2. 概率保证
通过随机抽样和概率算法,在有限的计算资源下达到近似最优的效果。
3. 自适应调节
根据实际情况动态调整清理强度和策略。
4. 工程实用性
不追求理论最优,而是在性能、内存、复杂度之间找到最佳平衡点。
实际应用的启发
缓存场景
原因:缓存数据都是可丢失的,LRU符合访问局部性
混合场景
原因:只淘汰临时数据,保护重要数据
持久化场景
原因:数据不能丢失,宁可拒绝服务
本质思考
Redis的过期删除和内存淘汰体现了系统设计的核心智慧:
在有限资源下,如何做出最优决策。这不仅仅是技术问题,更是一个资源分配和优先级管理的问题。
这种设计思路在操作系统的内存管理、数据库的缓冲池管理、CDN的缓存策略等各个领域都有体现。理解其背后的原理,比记住具体的配置参数更有价值。
RDB 和 AOF 中的过期键处理
- RDB 文件
- 生成 RDB 文件时,Redis 不会保存已过期的键
- 加载 RDB 文件时:
- 如果是主服务器,会忽略已过期的键
- 如果是从服务器,会加载所有键,包括过期键
- AOF 文件
- 当键过期后,如果还没被删除(惰性或定期删除),AOF 文件不会因此产生任何影响
- 当过期键被删除时,Redis 会在 AOF 文件中追加一条 DEL 命令
- AOF 重写时会忽略已过期的键
RDB持久化
手动触发
- SAVE 命令:阻塞 Redis 服务器进程,直到 RDB 文件创建完成
- BGSAVE 命令:派生一个子进程来创建 RDB 文件,不会阻塞服务器进程

如果开启了aof,优先选择aof文件
save执行时,服务器会被阻塞;
bgsave fork了子进程;
rdb文件载入会一直处于阻塞状态
自动触发
Redis 配置文件中的
save
配置项可以设置自动触发 BGSAVE 的条件:其他自动触发情况:
- 执行
FLUSHALL
命令时(可配置)
- 执行主从复制时,如果主节点没有正在执行 BGSAVE,则自动触发
- 正常关闭 Redis 服务器
RDB 文件的结构包含以下部分:
- 文件头:包含 "REDIS" 字符串和 RDB 版本号
- 数据库部分:包含每个数据库中的键值对数据
- EOF 标记:表示文件结束
- 校验和:用于验证文件完整性的数据
RDB 持久化的优缺点
优点
- 文件紧凑:RDB 是一个紧凑的单一文件,非常适合于备份和恢复
- 恢复速度快:与 AOF 相比,RDB 文件恢复速度更快
- 性能影响小:子进程负责持久化工作,对主进程性能影响较小
- 适合灾难恢复:可以将 RDB 文件保存到远程位置用于灾难恢复
缺点
- 数据丢失风险:两次快照之间的数据可能丢失
- fork() 操作开销:在内存较大的情况下,fork() 可能导致服务短暂停顿
- 不适合实时持久化:无法做到像 AOF 那样的秒级或命令级持久化
RDB 文件的恢复
恢复 RDB 文件非常简单:
- 将 RDB 文件放置到 Redis 的工作目录中
- 启动 Redis 服务器
- Redis 会自动检测 RDB 文件并加载其中的数据

AOF
AOF 持久化的核心思想是将服务器执行的写命令以追加的方式记录到 AOF 文件,当 Redis 重启时,重新执行这些命令来恢复数据
AOF 持久化的三个步骤
- 命令追加(append):当 Redis 执行完写命令后,会将该命令追加到 AOF 缓冲区
- 文件写入(write):Redis 将 AOF 缓冲区中的内容写入 AOF 文件
- 文件同步(sync):Redis 调用 fsync() 或 fdatasync() 将 AOF 文件同步到磁盘
AOF 文件使用 Redis 命令请求协议(RESP)格式保存命令,这是一种纯文本格式,可以直接阅读和编辑。
例如,执行
SET key value
命令后,AOF 文件中会追加如下内容:这种格式的好处是:
- 具有可读性
- 与 Redis 通信协议一致,便于解析
- 支持增量追加
AOF 同步策略(fsync 策略)
Redis 提供了三种 AOF 同步策略,可以通过
appendfsync
配置项设置:- always:每次写命令执行完后都立即进行同步
- 最安全,最多只会丢失一个命令的数据
- 性能最差
- everysec(默认):每秒进行一次同步
- 在安全性和性能之间取得平衡
- 最多可能丢失 1 秒钟的数据
- no:由操作系统决定何时同步
- 性能最好
- 安全性最差,可能丢失较多数据
AOF文件的载入和数据还原:

AOF 重写的原理
AOF 重写不会读取和分析现有的 AOF 文件,而是通过读取服务器当前的数据库状态来实现的:
- Redis 创建一个新的 AOF 文件
- 遍历数据库中的所有键值对
- 用一条命令记录键的当前值,而不是之前的多条命令
- 用新的 AOF 文件替换旧的 AOF 文件
例如,如果一个键先后执行了
SET key value1
、SET key value2
、SET key value3
,重写后的 AOF 文件只会包含 SET key value3
。关键区别:AOF正常运行 vs AOF重写
这是两个完全不同的概念:
AOF正常运行时
确实记录所有操作过程:
AOF文件会忠实记录这4个操作,这就是"过程记录思维"。
AOF重写时
只保留最终状态:
这时候采用的是"状态快照思维"。
为什么会有这个矛盾?
AOF重写实际上是对AOF机制的优化,而不是AOF的核心特性。
让我重新整理一下逻辑:
AOF的核心特性(过程记录)
- 目的:保证数据完整性,不丢失任何操作
- 方法:记录每个写操作
- 结果:完整的操作历史
AOF重写的优化目标(结果导向)
- 目的:减少文件大小,提高恢复速度
- 方法:分析当前状态,生成最小命令集
- 结果:等价但更简洁的操作序列
更准确的表述
AOF本质上是过程记录,但通过重写机制来优化存储效率。
这样做的好处:
- 运行时:每个操作都被记录,保证数据安全
- 存储时:通过重写压缩文件,节省空间
- 恢复时:重放优化后的命令集,提高速度
类比理解
想象你在记录一个银行账户的变化:
正常记录(AOF运行)
账单整理(AOF重写)
账单整理时,我们不关心中间的存取过程,只关心如何从0到200的最简路径。但在日常运行中,每笔交易都必须记录。
重新总结AOF的特点
AOF = 过程记录 + 结果优化
- 过程记录:保证数据完整性和可审计性
- 结果优化:通过重写提高存储和恢复效率
这种设计体现了一个重要原则:在保证正确性的前提下,通过后期优化来提高效率。
感谢你提出这个问题,这让我意识到之前的表述确实容易产生混淆。AOF的精髓在于它是一个"过程记录系统",但它通过重写机制来获得"状态快照系统"的部分优势。
什么时候需要aof重写:
业务模式分析
写多读少的场景:
- AOF文件增长快
- 需要更频繁的重写
- 可以调低重写阈值
读多写少的场景:
- AOF文件增长慢
- 重写频率可以降低
- 可以调高重写阈值
硬件资源考虑
内存充足:
- 可以容忍更大的AOF文件
- 降低重写频率,减少CPU开销
磁盘空间紧张:
- 需要更频繁的重写
- 设置更严格的重写条件
CPU资源紧张:
- 避免频繁重写
- 可以选择业务低峰期手动重写
AOF 持久化的优缺点
优点
- 数据安全性高:根据同步策略的不同,最多只丢失 1 秒钟的数据
- 适合实时性要求高的应用:可以做到秒级或命令级持久化
- 文件具有可读性:AOF 文件是纯文本格式,可以直接查看和编辑
- 自动重写机制:避免 AOF 文件过大
缺点
- 文件体积大:通常比 RDB 文件大得多
- 恢复速度慢:需要重新执行所有命令,相比 RDB 恢复较慢
- 对性能有一定影响:尤其是使用 always 同步策略时
aof-use-rdb-preamble yes|no
:是否在 AOF 文件开头使用 RDB 格式(混合持久化)
事件
Redis 的事件处理系统主要处理两类事件:
- 文件事件 (File Events):处理 Redis 服务器与客户端的网络通信
- 时间事件 (Time Events):处理需要在特定时间执行的任务,如定期清理过期键
文件事件


Redis 的文件事件处理器由四个组件组成:
- 套接字 (Socket):客户端与服务器之间的连接
- I/O 多路复用程序 (I/O Multiplexing):监听多个套接字的状态变化
- 文件事件分派器 (Event Dispatcher):接收 I/O 多路复用程序传来的套接字事件,并根据事件类型调用相应的事件处理器
- 事件处理器 (Event Handlers):处理具体事件的函数
常见的文件事件处理器包括:
- 连接应答处理器:处理客户端连接请求
- 命令请求处理器:处理客户端发送的命令请求
- 命令回复处理器:将命令执行结果返回给客户端
- 复制处理器:处理主从复制相关的网络事件
过程:
- I/O 多路复用程序监听套接字状态变化
- 当套接字变为可读或可写时,产生相应的文件事件
- 文件事件分派器接收事件并调用相应的事件处理器
- 事件处理器处理事件,如接受连接、读取命令、返回结果等
时间事件
- 定时事件:在指定时间后执行一次,然后被删除
- 周期性事件:按照一定周期重复执行
实际上,Redis 主要使用周期性事件,定时事件很少被使用。
主要的时间事件:
- serverCron:Redis 服务器的周期性函数,执行以下操作:
- 更新服务器统计信息
- 清理过期键值对
- 执行客户端超时检测
- 执行数据库大小调整
- 执行 AOF/RDB 持久化操作
- 如果是主服务器,对从服务器进行定期同步
- 如果是集群模式,执行集群操作
默认情况下,serverCron 每 100 毫秒执行一次


客户端
常见的客户端redis-cli
内部实现:
typedef struct client {
int fd; // 客户端套接字描述符
robj *name; // 客户端名字
int flags; // 客户端状态标志
sds querybuf; // 查询缓冲区
size_t querybuf_peak; // 查询缓冲区峰值
int argc; // 参数个数
robj **argv; // 参数数组
struct redisCommand *cmd; // 命令指针
list *reply; // 回复链表
unsigned long long reply_bytes; // 回复链表中的字节数
size_t sentlen; // 已发送字节数
time_t ctime; // 客户端创建时间
time_t lastinteraction; // 最后一次交互时间
// 其他字段...
} client;
输入缓冲区:
客户端的输入缓冲区用于存储客户端发送的命令。Redis 服务器通过读取这个缓冲区来获取并执行命令。
输入缓冲区特点:
- 没有大小限制,但会在超过 1GB 时关闭客户端连接
- 缓冲区过大可能导致内存占用过多
输出缓冲区:
输出缓冲区用于存储服务器返回给客户端的数据。Redis 使用了两种输出缓冲区:
- 固定大小缓冲区:用于小型回复,通常是一些状态回复
- 可变大小缓冲区:用于较大的回复,如大型列表、集合等
输出缓冲区可以通过
client-output-buffer-limit
配置项来限制大小,防止单个客户端占用过多内存。管道化(Pipelining)
管道化允许客户端一次性发送多个命令,而不必等待每个命令的响应:
事务(Transactions)
Redis 事务允许客户端一次性执行多个命令:
事务的特点:
- 命令在 EXEC 后一次性执行
- 执行过程中不会被其他客户端打断
- 没有回滚机制,即使有命令失败
服务端
服务器初始化与启动
Redis 服务器的启动过程大致包括以下步骤:
- 初始化服务器配置:设置默认配置,然后加载配置文件
- 初始化数据结构:创建命令表、客户端链表、数据库等
- 创建事件循环:初始化事件处理机制
- 创建监听套接字:绑定端口,准备接受客户端连接
- 初始化数据库:创建指定数量的数据库实例
- 加载持久化数据:如果有 AOF 或 RDB 文件,从中恢复数据
- 初始化后台任务:设置定时任务,如清理过期键、持久化等
- 进入事件循环:开始处理网络事件和时间事件
当客户端发送命令到 Redis 服务器时,服务器的处理流程如下:
- 接收请求:服务器接收客户端发送的命令请求
- 解析命令:将命令请求转换为命令对象
- 查找命令:在命令表中查找命令对应的实现函数
- 执行预备操作:权限检查、内存检查等
- 调用命令处理函数:执行具体命令
- 执行后续操作:更新统计信息、记录慢查询等
- 返回结果:将命令执行结果返回给客户端
Redis为什么快
第一性原理:什么决定了数据库的速度?
数据库的响应时间本质上由几个因素决定:
- 数据访问时间:从存储介质读取数据的时间
- 计算时间:处理数据的CPU时间
- 网络时间:数据传输时间
- 等待时间:各种锁、队列、IO等待时间
Redis的核心设计决策
1. 内存存储 - 消除最大的瓶颈
传统数据库的最大瓶颈是磁盘IO。机械硬盘的随机读写速度约为100-200 IOPS,而内存的访问速度是纳秒级别。Redis选择全内存存储,直接消除了这个最大的性能瓶颈。
这个决策的代价是数据容量受限于内存大小,但对于缓存场景,这是一个合理的权衡。
2. 单线程模型 - 消除并发复杂性
这是Redis最反直觉的设计。多线程看似能提高性能,但实际上带来了:
- 锁竞争的开销
- 上下文切换的开销
- 缓存一致性问题
- 复杂的并发控制
Redis采用单线程 + 事件循环的模型,所有操作都是原子的,没有锁竞争。由于数据都在内存中,CPU很少成为瓶颈,网络IO才是限制因素。
3. 高效的数据结构
Redis不是简单的key-value存储,而是针对不同场景优化了数据结构:
动态类型选择:同一种逻辑类型会根据数据量选择不同的底层实现。比如List在元素少时用ziplist(连续内存),元素多时用双向链表。
内存对齐优化:数据结构设计考虑了CPU缓存行的特性,提高缓存命中率。
4. 事件驱动的网络模型
Redis使用epoll/kqueue等高效的IO多路复用技术,单个线程可以处理大量的并发连接。这避免了传统多线程模型中为每个连接创建线程的开销。
从系统角度的优化
CPU缓存友好
Redis的数据结构设计考虑了现代CPU的特性:
- 连续内存访问模式
- 减少指针跳跃
- 利用CPU prefetch机制
网络协议优化
Redis协议(RESP)设计简单,解析开销小。同时支持pipeline操作,可以批量发送命令,减少网络往返时间。
内存管理
Redis实现了自己的内存分配器,减少了系统调用的开销,同时针对小对象分配进行了优化。
与其他方案的对比
传统关系数据库:需要考虑ACID特性,有复杂的锁机制,磁盘IO是瓶颈。
多线程内存数据库:虽然也是内存存储,但多线程带来的同步开销抵消了部分性能优势。
分布式缓存:网络开销和一致性维护成本高。
设计哲学的体现
Redis的设计体现了几个重要原则:
做减法而非加法:去掉了传统数据库的很多特性(事务、关系查询、持久化保证),专注于速度。
单一职责:专注于缓存和简单数据结构操作,不试图成为万能的数据库。
面向使用场景优化:针对互联网应用的访问模式(读多写少、热点数据)进行优化。
权衡与代价
Redis的高性能是有代价的:
- 数据容量受限于内存
- 持久化能力有限
- 单点故障风险
- 复杂查询能力弱
但在其目标场景(缓存、会话存储、简单数据结构操作)中,这些权衡是合理的。
Redis的速度本质上来自于明确的定位和一致的设计决策:选择内存换取速度,选择简单换取性能,选择专一换取极致。这种设计哲学让它在特定场景下达到了极致的性能表现。
第三章 多机数据库的实现
第四章 独立功能的一些实现
Redis集群是解决单点限制问题的系统性方案,让我从第一性原理来分析它的设计思路。
问题的根本矛盾
单机Redis面临三个核心限制:
- 容量限制:单机内存有上限
- 性能限制:单机QPS有瓶颈
- 可用性限制:单点故障风险
集群设计的三种思路
1. 主从复制:解决可用性问题
核心思想
数据冗余 + 读写分离
设计机制
复制原理
优缺点
优点:
- 提高读性能(读写分离)
- 提高可用性(主从切换)
- 实现简单
缺点:
- 不解决容量问题
- 写性能未提升
- 数据一致性问题
2. 哨兵模式:解决自动故障转移
核心思想
监控 + 自动故障转移
设计机制
故障转移流程
优缺点
优点:
- 自动故障转移
- 高可用性
- 配置管理
缺点:
- 仍不解决容量和写性能问题
- 增加系统复杂性
- 需要奇数个哨兵节点
为什么需要Redis Cluster?
主从复制的局限性
Cluster要解决的核心问题
如何让多台机器协同工作,就像一台大机器一样?
Cluster的基本思路:数据分片
最朴素的分片思路
这种方式的问题
Redis Cluster的解决方案:槽位(Slot)
引入中间层的思路
槽位分配示例
当需要扩容时
16384这个数字的深层含义
为什么不是65536?
为什么不是1024?
客户端如何找到数据?
初始连接
查找数据的过程
客户端优化
数据迁移的详细过程
迁移一个slot的步骤
迁移期间的访问处理
集群节点间的通信
Gossip协议的工作原理
故障检测
实际部署示例
最小集群配置
创建集群
限制和注意事项
不支持的操作
解决方案:Hash Tag
集群 vs 其他方案
集群 vs 主从
集群 vs 客户端分片
总结:Redis Cluster的本质
Redis Cluster的核心思想是:通过引入slot中间层,实现数据的灵活分片和平滑扩展。
它就像一个分布式的大哈希表,让多台机器协同工作,对外表现得像一台大机器。虽然增加了复杂性,但换来了水平扩展的能力。
理解了这个本质,Redis Cluster的其他细节就都是为了支撑这个核心目标的技术实现。
集群的权衡与挑战
1. 一致性问题
最终一致性
脑裂问题
2. 性能问题
跨节点操作限制
网络延迟
3. 运维复杂性
容量规划
扩缩容
不同场景的选择策略
小规模高可用场景
大规模高并发场景
极致性能场景
现代架构的演进
云原生时代的变化
1. 托管服务
2. 容器化部署
3. 多云和混合云
设计原则的体现
1. 渐进式复杂性
从单机 → 主从 → 哨兵 → 集群,复杂性逐步递增,用户可以根据需求选择。
2. 去中心化设计
Redis Cluster没有中心节点,避免了单点瓶颈,提高了可扩展性。
3. 最终一致性
在性能和一致性之间选择了性能,适合大多数缓存场景。
4. 简单优于复杂
虽然提供了集群能力,但保持了Redis简单易用的特点。
实际应用建议
架构选择
监控重点
运维策略
本质思考
Redis集群的设计体现了分布式系统的核心思想:通过冗余和分片来解决单点限制。
这种设计在很多系统中都有体现:
- 数据库的分库分表
- 消息队列的分区
- 文件系统的分布式存储
理解Redis集群的设计原理,有助于我们更好地理解分布式系统的通用模式和权衡策略。关键是要根据具体场景选择合适的方案,而不是盲目追求最复杂的架构。
为什么说redis支持高并发
Redis声称支持高并发,背后的原理值得深入分析。让我从第一性原理来解释这个问题。
高并发的定义和衡量标准
什么是高并发?
高并发 = 在单位时间内处理大量请求
衡量指标:
- QPS/TPS:每秒查询/事务数
- 并发连接数:同时连接的客户端数量
- 响应时间:请求处理延迟
Redis的性能表现
高并发的底层原理
1. 内存存储:消除IO瓶颈
传统数据库的瓶颈
Redis的优势
2. 单线程模型:避免并发复杂性
多线程的隐性开销
Redis单线程的优势
3. 高效的网络模型:IO多路复用
传统BIO模型的问题
Redis的IO多路复用
4. 优化的数据结构
针对性能优化的设计
内存对齐和局部性优化
高并发支持的技术细节
1. 命令处理优化
命令解析和执行
命令查找优化
2. 内存管理优化
内存分配器
对象共享
3. 网络通信优化
输出缓冲区管理
Pipeline支持
实际并发能力分析
理论分析
实际测试结果
影响因素
高并发的局限性
1. CPU密集型操作
2. 内存限制
3. 网络瓶颈
优化建议
1. 硬件优化
2. 配置优化
3. 应用层优化
与其他系统的对比
vs MySQL
vs Nginx
本质思考
Redis支持高并发的本质是:通过消除主要瓶颈,让CPU成为唯一限制因素。
传统系统的瓶颈链条:
Redis的优化链条:
当系统中只有CPU是瓶颈时,性能自然就达到了理论上限。这就是Redis能够支持高并发的根本原因。
这种设计思路在很多高性能系统中都有体现:消除次要瓶颈,专注于核心瓶颈的优化。
单线程vs多线程
这是个非常好的问题!让我从最基础的概念开始,用通俗易懂的方式来解释。
最直观的理解
单线程就像一个人
多线程就像多个人
程序运行的角度
单线程程序
多线程程序
具体例子:下载文件
单线程下载
多线程下载
为什么会有这样的差异?
CPU的工作原理
时间片轮转
现实中的对比
单线程的例子
多线程的例子
代码层面的对比
单线程代码
多线程代码
各自的优缺点
单线程
优点:
缺点:
多线程
优点:
缺点:
为什么Redis选择单线程?
Redis的特殊情况
类比理解
什么时候用单线程,什么时候用多线程?
适合单线程的场景
适合多线程的场景
总结
单线程和多线程的本质区别:
- 执行方式:单线程是顺序执行,多线程是并行执行
- 资源利用:单线程利用率低,多线程利用率高
- 复杂度:单线程简单,多线程复杂
- 适用场景:单线程适合简单快速的任务,多线程适合复杂耗时的任务
选择的原则:
- 如果任务简单且快速,单线程可能更好
- 如果任务复杂且耗时,多线程通常更好
- 如果对数据一致性要求极高,单线程更安全
- 如果需要充分利用硬件资源,多线程更合适
Redis选择单线程是因为它的任务特点(内存操作,快速简单)使得单线程的优势(简单、无锁、高效)更加突出,而多线程的优势(并行处理)在这个场景下收益不大。
事件处理
事件处理是一个核心概念,让我从最基础的地方开始解释。
什么是"事件"?
生活中的事件
计算机中的事件
传统的处理方式 vs 事件驱动
传统方式:主动询问(轮询)
事件驱动方式:被动响应
网络编程中的事件处理
传统的BIO(阻塞IO)模式
事件驱动的NIO(非阻塞IO)模式
Redis中的事件处理
Redis的事件类型
Redis的事件循环
具体的事件处理流程
epoll:高效的事件检测机制
什么是epoll?
epoll的工作原理
事件驱动的优势
1. 高并发处理能力
2. 资源利用率高
3. 编程模型简单
事件驱动的缺点
1. 不适合CPU密集型任务
2. 调试相对困难
现实中的应用
1. Web服务器
2. 数据库
3. 游戏服务器
总结
事件处理的本质:
- 不是主动去找工作,而是等工作来找你
- 有事件发生时才处理,没事件时就等待
- 一个线程可以处理多个事件源
为什么高效:
- 避免了无效的等待和检查
- 减少了线程切换的开销
- 提高了资源利用率
Redis的成功:
- 选择了适合的场景(简单快速的操作)
- 使用了高效的事件处理机制
- 避免了传统多线程的复杂性
事件驱动就像一个高效的管家,不会无聊地四处巡视,而是静静等待,一旦有事情发生就立即高效处理。这就是为什么Redis能用单线程支撑高并发的核心原因。
大key和热key
大key和热key的本质问题:
- 大key:单个数据对象过大,影响操作性能
- 热key:访问频率过高,造成资源竞争
解决思路:
- 大key:拆分、压缩、异步处理
- 热key:分散、缓存、限流
mysql和redis的数据不一致
你说得对,让我聚焦到最核心的问题和最实用的方案。
核心问题:为什么会不一致?
最常见的不一致场景
根本原因
两个独立的系统无法保证原子性操作
两种主流解决思路
思路1:先更新数据库,再删除缓存
为什么删除而不是更新?
- 删除操作更简单,不容易出错
- 即使删除失败,数据也只是暂时不一致
- 更新操作可能因为数据格式等问题失败
思路2:延迟双删
为什么需要延迟删除?
解决这个时序问题: 就是最后缓存可能是空的
延迟双删的核心思想:
- 第一次删除:清理可能的旧缓存
- 更新数据库:保证数据源正确
- 延迟删除:清理并发查询可能写入的脏数据
本质上是一种"补偿"机制:
- 承认在高并发下可能出现时序问题
- 通过延迟删除来"补偿"这个时序问题
- 用时间换取数据一致性
实际生产中的最佳实践
方案1:简单场景 - Cache Aside + 重试
方案2:高可靠场景 - 消息队列异步处理
这个方案的原因:
问题: 如果缓存删除失败了,数据就不一致了,而且你可能都不知道!
把"删除缓存"这件事记录下来,确保它一定会被执行
不同业务场景的选择
1. 用户资料、商品信息 → 方案1
2. 订单状态、支付结果 → 方案2
3. 计数器、统计数据 → 直接更新缓存
监控和故障处理
简单的一致性检查
故障降级
核心建议
1. 选择策略
2. 实现要点
3. 避免过度设计
总结:数据一致性的核心是在业务需求、系统复杂度、开发成本之间找平衡。大多数场景下,简单的"更新DB+删除缓存"就足够了。
缓存击穿,雪崩,穿透
让我用最直观的方式来解释这三个经典的缓存问题。
缓存穿透(Cache Penetration)
什么是缓存穿透?
查询一个根本不存在的数据
生活例子
技术场景
危害
解决方案
1. 缓存空值
2. 布隆过滤器
缓存雪崩(Cache Avalanche)
什么是缓存雪崩?
大量缓存同时失效,导致请求全部打到数据库
生活例子
技术场景
危害
解决方案
1. 错开过期时间
2. 多级缓存
3. 熔断器机制
缓存击穿(Cache Breakdown)
什么是缓存击穿?
某个热点数据的缓存失效,导致大量请求同时查询数据库
生活例子
技术场景
危害
解决方案
1. 分布式锁
2. 异步更新缓存
3. 永不过期 + 逻辑过期
三者对比总结
问题 | 原因 | 影响 | 解决方案 |
缓存穿透 | 查询不存在的数据 | 每次都查数据库 | 缓存空值、布隆过滤器 |
缓存雪崩 | 大量缓存同时失效 | 数据库压力暴增 | 错开过期时间、多级缓存 |
缓存击穿 | 热点数据缓存失效 | 热点数据查询压力大 | 分布式锁、异步更新 |
实际应用建议
1. 预防为主
2. 监控告警
核心思想:
- 缓存穿透:防止无效查询
- 缓存雪崩:避免集中失效
- 缓存击穿:保护热点数据
预防原则:
- 设计时就要考虑这些问题
- 多种方案组合使用
- 建立监控和告警机制
bloom filter
让我用最简单的方式来解释布隆过滤器(Bloom Filter)。
什么是布隆过滤器?
生活中的例子
布隆过滤器的本质
用少量的空间来快速判断"某个元素肯定不存在"
工作原理
1. 数据结构
2. 添加元素
3. 查询元素
具体例子
场景:检查用户是否存在
为什么会有误判?
误判的原因
误判类型
在缓存中的应用
解决缓存穿透
布隆过滤器的优缺点
优点
缺点
实际应用场景
1. 数据库查询优化
2. 网络爬虫去重
3. 垃圾邮件过滤
性能对比
传统方法 vs 布隆过滤器
总结
布隆过滤器的核心价值:
- 用很少的内存快速判断"肯定不存在"
- 大大减少无效的数据库查询
- 提高系统整体性能
适用场景:
- 缓存穿透防护
- 去重判断
- 黑名单过滤
- 推荐系统中的已读判断
使用原则:
- 能接受一定的误判率
- 主要关心"不存在"的情况
- 数据量大,对空间敏感
- 不需要精确的存在性判断
记住:布隆过滤器说"不存在"绝对准确,说"存在"可能误判!
常用的操作
好的,Redis 是一个非常流行的键值(Key-Value)存储系统,它支持多种数据结构。以下是 Redis 最常见的五种基本数据类型及其常用的命令行操作(使用 redis-cli):
1. String (字符串)
- 描述: 最基本的数据类型,一个 Key 对应一个 Value。Value 不仅可以是字符串,也可以是数字(Redis 可以对其进行原子性的增加/减少操作)。最大能存储 512MB。
- 常见用途: 缓存、计数器、分布式锁、存储 Session 信息等。
- 常用命令:
- SET key value [EX seconds | PX milliseconds | KEEPTTL] [NX | XX]:设置指定 key 的值。
- EX seconds: 设置过期时间(秒)。
- PX milliseconds: 设置过期时间(毫秒)。
- NX: 只在 key 不存在时设置。
- XX: 只在 key 存在时设置。
- KEEPTTL: 保留 key 原有的 TTL。
- 示例: SET mykey "hello"
- 示例 (带过期时间): SET counter 10 EX 60
- GET key: 获取指定 key 的值。
- 示例: GET mykey
- GETSET key value: 设置 key 的新值,并返回旧值。
- 示例: GETSET mykey "world"
- MSET key value [key value ...]: 同时设置一个或多个 key-value 对。
- 示例: MSET key1 "v1" key2 "v2"
- MGET key [key ...]: 获取一个或多个 key 的值。
- 示例: MGET key1 key2 non_existing_key
- INCR key: 将 key 中储存的数字值增一(如果 key 不存在,则初始化为 0 再执行 INCR)。
- 示例: INCR page_views
- DECR key: 将 key 中储存的数字值减一。
- 示例: DECR items_left
- INCRBY key increment: 将 key 所储存的值加上指定的增量值。
- 示例: INCRBY user_score 10
- DECRBY key decrement: 将 key 所储存的值减去指定的减量值。
- 示例: DECRBY user_score 5
- STRLEN key: 返回 key 所储存的字符串值的长度。
- 示例: STRLEN mykey
- APPEND key value: 如果 key 已经存在并且是一个字符串,将 value 追加到 key 原来的值的末尾。
- 示例: APPEND mykey "!!!"
2. List (列表)
- 描述: 简单的字符串列表,按照插入顺序排序。可以在列表的头部(Lef)或尾部(Righ)添加元素。底层实际是个双向链表或压缩列表。
- 常见用途: 消息队列、栈、最新 N 个数据(如用户最近访问的文章)。
- 常用命令:
- LPUSH key element [element ...]: 将一个或多个值插入到列表头部。
- 示例: LPUSH mylist "world" "hello" (列表内容: "hello", "world")
- RPUSH key element [element ...]: 将一个或多个值插入到列表尾部。
- 示例: RPUSH mylist "!" (列表内容: "hello", "world", "!")
- LPOP key [count]: 移除并获取列表的第一个元素(可指定数量)。
- 示例: LPOP mylist (返回 "hello")
- RPOP key [count]: 移除并获取列表的最后一个元素(可指定数量)。
- 示例: RPOP mylist (返回 "!")
- LLEN key: 获取列表的长度。
- 示例: LLEN mylist
- LRANGE key start stop: 获取列表指定范围内的元素(0 是第一个,-1 是最后一个)。
- 示例: LRANGE mylist 0 -1 (获取所有元素)
- 示例: LRANGE mylist 0 1 (获取前两个元素)
- LINDEX key index: 通过索引获取列表中的元素。
- 示例: LINDEX mylist 0 (获取第一个元素)
- LSET key index element: 通过索引设置列表元素的值。
- 示例: LSET mylist 0 "new_hello"
- LREM key count element: 根据参数 count 的值,移除列表中与参数 element 相等的元素。
- count > 0: 从表头开始向表尾搜索,移除与 element 相等的元素,数量为 count。
- count < 0: 从表尾开始向表头搜索,移除与 element 相等的元素,数量为 -count。
- count = 0: 移除表中所有与 element 相等的值。
- 示例: LREM mylist 1 "world"
- LTRIM key start stop: 对一个列表进行修剪,只保留指定区间内的元素。
- 示例: LTRIM mylist 0 99 (保留最新的 100 个元素)
- BLPOP key [key ...] timeout: 阻塞式列表的弹出原语,从列表头部弹出。如果列表为空,会阻塞连接直到等待超时或发现可弹出元素为止。
- BRPOP key [key ...] timeout: 阻塞式列表的弹出原语,从列表尾部弹出。
3. Hash (哈希/字典)
- 描述: 一个键值(key-value)对集合,其中 key 是唯一的。非常适合用于存储对象。
- 常见用途: 存储用户信息(如用户ID为Key,用户信息字段如name, age, email为field-value)、存储对象。
- 常用命令:
- HSET key field value [field value ...]: 将哈希表 key 中的字段 field 的值设为 value (如果字段不存在,则创建)。
- 示例: HSET user:1000 name "Alice" age 30 email "[email protected]"
- HGET key field: 获取存储在哈希表中指定字段的值。
- 示例: HGET user:1000 name
- HMGET key field [field ...]: 获取所有给定字段的值。
- 示例: HMGET user:1000 name age non_existing_field
- HGETALL key: 获取在哈希表中指定 key 的所有字段和值。
- 示例: HGETALL user:1000
- HDEL key field [field ...]: 删除一个或多个哈希表字段。
- 示例: HDEL user:1000 email
- HLEN key: 获取哈希表中字段的数量。
- 示例: HLEN user:1000
- HEXISTS key field: 查看哈希表的指定字段是否存在。
- 示例: HEXISTS user:1000 age
- HKEYS key: 获取哈希表中的所有字段名(keys)。
- 示例: HKEYS user:1000
- HVALS key: 获取哈希表中的所有值(values)。
- 示例: HVALS user:1000
- HINCRBY key field increment: 为哈希表 key 中的指定字段的整数值加上增量 increment。
- 示例: HINCRBY user:1000 age 1
4. Set (集合)
- 描述: String 类型的无序集合。集合成员是唯一的,不允许重复。
- 常见用途: 标签(tagging)、共同好友、独立 IP 访问统计、判断用户是否点赞/收藏。
- 常用命令:
- SADD key member [member ...]: 向集合添加一个或多个成员。
- 示例: SADD myset "a" "b" "c" "a" (最终集合为 "a", "b", "c")
- SMEMBERS key: 返回集合中的所有成员。
- 示例: SMEMBERS myset
- SISMEMBER key member: 判断 member 元素是否是集合 key 的成员。
- 示例: SISMEMBER myset "b"
- SCARD key: 获取集合的成员数(基数)。
- 示例: SCARD myset
- SREM key member [member ...]: 移除集合中一个或多个成员。
- 示例: SREM myset "c"
- SPOP key [count]: 移除并返回集合中的一个或多个随机元素。
- 示例: SPOP myset
- SRANDMEMBER key [count]: 返回集合中一个或多个随机元素(不移除)。
- 示例: SRANDMEMBER myset 2
- SINTER key [key ...]: 返回给定所有集合的交集。
- 示例: SADD set1 "a" "b" "c", SADD set2 "c" "d" "e", SINTER set1 set2 (返回 "c")
- SUNION key [key ...]: 返回给定所有集合的并集。
- 示例: SUNION set1 set2 (返回 "a", "b", "c", "d", "e")
- SDIFF key [key ...]: 返回第一个集合与其他集合之间的差集。
- 示例: SDIFF set1 set2 (返回 "a", "b")
5. Sorted Set (ZSet / 有序集合)
- 描述: 和 Set 一样也是 String 类型的元素的集合, 且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数(score)。Redis 正是通过分数来为集合中的成员进行从小到大排序。成员是唯一的, 但分数(score)可以重复。
- 常见用途: 排行榜、带权重的消息队列、范围查找(如查找积分在某个范围的用户)。
- 常用命令:
- ZADD key [NX|XX] [CH] [INCR] score member [score member ...]: 向有序集合添加一个或多个成员,或者更新已存在成员的分数。
- 示例: ZADD leaderboard 100 "player1" 95 "player2" 110 "player3"
- ZRANGE key start stop [WITHSCORES]: 通过索引区间返回有序集合成指定区间内的成员(按分数从小到大)。
- 示例: ZRANGE leaderboard 0 -1 (获取所有成员,按分升序)
- 示例: ZRANGE leaderboard 0 -1 WITHSCORES (获取所有成员及分数)
- ZREVRANGE key start stop [WITHSCORES]: 返回有序集中指定区间内的成员,通过索引,分数从高到底。
- 示例: ZREVRANGE leaderboard 0 2 WITHSCORES (获取排名前三的玩家及其分数)
- ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]: 通过分数区间返回有序集合的成员(默认从小到大)。
- min 和 max 可以是 -inf 和 +inf。
- 使用 ( 表示不包含边界,例如 (90 100 表示分数大于90小于100。
- 示例: ZRANGEBYSCORE leaderboard 90 100 WITHSCORES
- 示例: ZRANGEBYSCORE leaderboard (90 +inf LIMIT 0 10 WITHSCORES (获取分数大于90的前10名)
- ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]: 返回有序集合中指定分数区间的成员,分数从高到低排序。
- ZCARD key: 获取有序集合的成员数。
- 示例: ZCARD leaderboard
- ZSCORE key member: 返回有序集中,成员的分数值。
- 示例: ZSCORE leaderboard "player1"
- ZREM key member [member ...]: 移除有序集合中的一个或多个成员。
- 示例: ZREM leaderboard "player2"
- ZCOUNT key min max: 计算在有序集合中指定区间分数的成员数。
- 示例: ZCOUNT leaderboard 90 100
- ZINCRBY key increment member: 对有序集合中指定成员的分数加上增量 increment。
- 示例: ZINCRBY leaderboard 5 "player1"
- ZRANK key member: 返回有序集合中指定成员的排名(按分数从小到大,排名从 0 开始)。
- 示例: ZRANK leaderboard "player1"
- ZREVRANK key member: 返回有序集合中指定成员的排名(按分数从大到小,排名从 0 开始)。
- 示例: ZREVRANK leaderboard "player1"
通用 Key 操作命令:
- DEL key [key ...]: 删除一个或多个 key。
- 示例: DEL mykey mylist user:1000
- EXISTS key [key ...]: 检查给定 key 是否存在。
- 示例: EXISTS mykey
- KEYS pattern: 查找所有符合给定模式 pattern 的 key (生产环境慎用,可能阻塞)。
- 示例: KEYS user:*
- TYPE key: 返回 key 所储存的值的类型。
- 示例: TYPE mykey (可能返回 "string", "list", "hash", "set", "zset")
- EXPIRE key seconds: 为给定 key 设置生存时间(秒)。
- 示例: EXPIRE mykey 3600 (1小时后过期)
- TTL key: 以秒为单位,返回给定 key 的剩余生存时间。
- 示例: TTL mykey (返回 -1 表示永不过期,-2 表示 key 不存在)
- PERSIST key: 移除给定 key 的生存时间,将这个 key 从“易失的”(带生存时间 key )转换成“持久的”(一个不带生存时间、永不过期的 key )。
- 示例: PERSIST mykey
- RENAME key newkey: 修改 key 的名称。
- 示例: RENAME mykey my_new_key
- FLUSHDB: 清空当前数据库中的所有 key (慎用!)
- FLUSHALL: 清空所有数据库中的所有 key (极度慎用!)
这些是 Redis 最核心的数据类型和常用命令,掌握它们就能应对大部分常见的 Redis 应用场景。你可以通过 redis-cli 连接到 Redis 服务器,然后直接输入这些命令进行尝试。输入 HELP @<datatype> (例如 HELP @string) 可以获取该数据类型相关的命令帮助。