Redis基础
Redis数据类型
核心数据类型 (5种)
- String (字符串): 最简单的类型,可存文本、整数或浮点数,二进制安全。支持自增/自减操作。
- Hash (哈希): 字段-值对的集合,非常适合存储对象。
- List (列表): 按插入顺序排序的字符串元素列表,基于双向链表实现,可用作栈或队列。
- Set (集合): 无序的、元素唯一的字符串集合,支持交集、并集、差集等运算。
- Sorted Set (有序集合): 类似 Set,但每个元素关联一个分数(score),按分数排序,元素唯一,适用于排行榜。
扩展数据类型 (基于核心类型实现)
- Bitmaps (位图): 本质是 String,提供位级别的操作(如设置、统计)。极其节省空间,适用于二值状态统计(如用户签到)。
- HyperLogLogs (基数统计): 本质是 String,用于估计一个集合中不重复元素的基数(数量)。提供近似去重计数,标准误差 <1%,极其节省内存。
- Streams (流): 本质是 List 的增强版,提供了一个完整的、支持多消费者的消息队列功能。支持消息持久化、消费者组、消息确认机制。
- Geo(地理坐标):Redis3.2 推出的,地理位置定位,用于存储地理位置信息,并对存储的信息进行操作。
/* Redis 对象结构体定义 */
typedef struct redisObject {
// 对象类型(4位),如字符串、列表、哈希等
unsigned type:4;
// 编码方式(4位),标识底层数据结构实现,如int、embstr、raw等
unsigned encoding:4;
// LRU/LFU 信息(24位):
// - 若配置LRU策略:记录以秒为单位的最近一次访问时间
// - 若配置LFU策略:高16位记录分钟级访问时间,低8位记录逻辑访问次数
unsigned lru:LRU_BITS;
// 引用计数,用于内存回收(计数为0时对象可被回收)
int refcount;
// 指向实际存储数据的指针
void *ptr;
} robj;LFU(最不经常使用)计数机制
LFU 的访问次数称为逻辑访问次数,其更新规则如下:
- 生成随机数:生成一个
0~1之间的随机数R。 - 计算概率值:计算
1/(旧次数 * lfu_log_factor + 1),记为P(lfu_log_factor默认为 10)。 - 计数递增:如果
R < P,则计数器 +1(最大不超过 255)。 - 计数衰减:访问次数会随时间衰减。距离上一次访问时间每间隔
lfu_decay_time分钟(默认 1),计数器减 1。
核心要点总结
- RedisObject 是 Redis 数据存储的底层通用结构。
lru字段具有双重含义,取决于 Redis 使用的淘汰策略:- 配置为 LRU 时,记录以秒为单位的最近访问时间。
- 配置为 LFU 时,高16位记录最近访问时间(分钟级),低8位存储逻辑访问次数。
- LFU 的计数是一个概率性的近似值,而非精确计数,通过随机数和概率公式控制增长,并会随时间自动衰减,从而更精准地识别热点数据。
为什么String类型用SDS实现,而不是C字符串?
SDS (Simple Dynamic String) 相比C原生字符串有巨大优势:
- O(1)获取长度:SDS有
len属性,直接获取字符串长度,而C字符串需要遍历。 - 避免缓冲区溢出:SDS API会先检查空间是否足够,不足则自动扩展。
- 二进制安全:SDS可以存储任意二进制数据,包括
\0,因为它靠len判断结束,而不是空字符。 - 内存预分配:减少修改字符串时带来的内存重分配次数。
ZSet的底层实现是什么?为什么用跳跃表?
- 底层实现:跳跃表 (skiplist) + 哈希表 (dict) 的混合结构。
- 为什么用跳跃表:
- 支持范围查询:跳跃表基于排序的链表结构,可以高效地实现
ZRANGE,ZRANK等范围操作,效率远高于平衡树。 - 实现相对简单:比平衡树(如AVL、红黑树)更容易实现和调试。
- 平均性能好:插入、删除、查找的平均时间复杂度都是 O(log N),性能与平衡树相当。
- 支持范围查询:跳跃表基于排序的链表结构,可以高效地实现
Redis 高性能原因
- 基于内存: 数据主要存储在内存中,读写操作直接操作内存,避免了磁盘 I/O 瓶颈。
- 单线程架构:
- 避免了多线程的上下文切换和竞争开销。
- 保证了原子操作的线程安全,简化了数据结构的实现。
- 高效的数据结构:
- 设计了多种精心优化的底层数据结构(如 SDS、跳跃表、压缩列表等),最大限度利用内存并减少操作时间。
- I/O 多路复用:
- 使用
epoll等机制处理大量连接,保证在单线程下也能高效进行网络 I/O。
- 使用
- 其他优化:
- 虚拟内存机制: 应对物理内存不足。
- 渐进式 rehash: 在扩容时避免长时间阻塞。
总结: Redis为基于内存的非关系型数据库,故瓶颈不在CPU,而是内存和网络IO。内存访问 + 单线程避免竞争 + I/O 多路复用 + 优化数据结构 共同造就了其极致性能。
Redis 持久化机制
Redis 提供两种核心持久化机制:RDB 和 AOF,可单独或组合使用,确保数据在重启后不丢失。
RDB(Redis Database)
RDB全称(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是在指定时间间隔内,将内存中的数据集生成一个全量快照,存储到二进制压缩文件(默认 dump.rdb)。
执行时机:
手动触发:
SAVE(同步阻塞)或BGSAVE(后台异步)。自动触发:通过redis.conf配置规则(如
save 900 1表示 900 秒内至少 1 个键变更则触发BGSAVE)。关机时:如果开启了RDB,正常关闭服务时会自动执行一次
SAVE。
RDB bgsave 基本流程
- Fork子进程:主进程创建一个子进程,仅
fork时有短暂阻塞。 - 子进程写文件:子进程把当前内存中的数据复制并写入到一个新的临时RDB文件中。
- 替换旧文件:写完後,用新文件替换掉旧的RDB文件。
基于 fork + copy-on-write。父进程继续服务,子进程遍历内存数据写入 RDB 文件。利用写时复制技术保证子进程数据是 fork 时刻的快照。
优点:
- 性能最大化:
bgsave方式由子进程负责持久化,主进程无需进行任何磁盘 I/O 操作,保证了 Redis 的高性能。 - 恢复速度快:相对于 AOF,RDB 是紧凑的二进制压缩文件。在重启恢复大数据集时,速度比 AOF 快很多。
- 适合灾难备份:RDB 文件是一个单一的紧凑文件,非常适合用于备份、全量复制和灾难恢复。例如,可以把每小时的一个 RDB 文件存档。
- 数据文件更小:在恢复时,RDB 文件比 AOF 文件体积更小。
缺点:
- 数据安全性低:因为每隔一段时间才备份一次,如果中途宕机,从上一次备份到宕机时的数据会丢失。(这是最主要的缺点)
- fork 操作的潜在风险:虽然
bgsave是后台操作,但 fork 子进程的过程可能会阻塞主进程(尽管通常很短)。如果数据量巨大,fork 耗时较长,且内存数据越大,阻塞时间越长。
save 60 1000 代表什么?
代表:在 60秒内,如果发生了至少 1000次数据修改,则自动触发一次
BGSAVE。
AOF(Append Only File)
AOF全称为Append Only File(追加文件)。以命令日志文件的形式,记录Redis处理的每一个写命令,重启服务时重新执行这些命令来恢复数据。如果两个都配了优先加载AOF。
#指定 AOF 持久化文件的文件名
appendfilename "appendonly.aof"执行流程
- 命令追加:写命令执行后追加到 AOF 缓冲区。
- 文件写入:根据
appendfsync策略将缓冲区内容写入内核页面缓存。 - 文件同步:根据策略将内核缓存数据同步(
fsync)到磁盘。
执行时机,appendfsync 策略
- always:每个写命令同步到磁盘。最安全,性能最低。
- everysec(默认):每秒同步一次。均衡策略,最多丢失 1 秒数据。
- no:由操作系统决定同步时机。性能最好,数据最不安全。
#表示每执行一次写命令,立即记录到AOF文件
appendfsync always
#写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案。
appendfsync everysec
#写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no优点
- 数据安全性高:根据策略最多丢失 1 秒数据(甚至不丢失)。
- 可读性强:文本协议格式,可手动查看(不建议修改)。
缺点
- 文件体积大:通常比同数据 RDB 文件大。
- 恢复速度慢:需逐条执行命令,比 RDB 慢。
- 性能影响较大:即使使用
everysec,性能仍略低于 RDB。
AOF 重写(Rewrite)
因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。目的:解决 AOF 文件膨胀问题,生成最小命令集合的新文件。
重写基本流程
Fork 子进程:主进程创建一个子进程。
子进程写新文件:子进程读取当前数据库的数据,并为每个键值对生成一条最新的、最简洁的写入命令,写入到一个临时的新 AOF 文件中。
- 注意:子进程是基于当前数据快照来生成命令的,而不是分析旧的 AOF 文件。
追加新命令:在子进程重写期间,所有新的写命令不仅会追加到原来的 AOF 缓冲区,也会追加到一个重写缓冲区。
替换旧文件:子进程完成新文件的写入后,通知主进程。主进程将重写缓冲区中的命令追加到新的 AOF 文件中,然后原子性地替换掉旧的 AOF 文件。
触发方式:手动(
BGREWRITEAOF)或自动(配置auto-aof-rewrite-percentage和auto-aof-rewrite-min-size)。实现:
fork子进程完成,不影响主进程。新写命令同时写入旧 AOF 缓冲和重写缓冲,确保数据一致性。
#AOF文件比上次文件增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
#AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mbRDB 与 AOF 对比
| 特性 | RDB (快照模式) | AOF (日志模式) |
|---|---|---|
| 持久化原理 | 定时对内存做全量快照 | 记录每一次写命令 |
| 数据一致性 | 弱,可能丢失最后一次快照后的所有数据 | 强,最多丢失一秒的数据(默认配置) |
| 文件大小 | 小(二进制压缩格式) | 大(日志文本格式,即使重写后也可能比RDB大) |
| 恢复速度 | 快(直接加载数据到内存) | 慢(需要逐条执行命令) |
| 性能影响 | fork子进程时内存开销大,可能阻塞 | 持续写入开销,但重写时也会有fork开销 |
| 优先级 | 低 | 高(如果同时开启,Redis重启优先使用AOF恢复) |
| 适用场景 | 追求快速重启、允许分钟级数据丢失 | 要求数据安全、允许牺牲一些性能和磁盘空间 |
混合持久化
混合持久化是结合了 RDB 和 AOF 两种方式优点的一种持久化策略,能很好地兼顾重启速度和数据安全性。Redis 4.0 版本之后默认开启了混合持久化。
工作原理
- AOF 重写时,子进程先将当前数据以 RDB 格式 写入新 AOF 文件前半部分。
- 再将重写缓冲区的增量命令以 AOF 格式 追加到文件后半部分。
- 最终生成混合格式文件(RDB + AOF)。
优点
- 重启效率高:先加载 RDB 部分快速恢复大部分数据,再重放增量命令。
- 数据安全:结合 AOF 实时记录优势,保证了增量数据不丢失。
- **文件体积可控:通过重写机制避免无限膨胀。
#混合持久化开启配置,注意:此功能需要同时开启 AOF,即 appendonly yes
aof-use-rdb-preamble yesRedis过期键删除策略
Redis 采用 惰性删除 和 定期删除 两种策略相结合的方式。
惰性删除 (Lazy Expiration)
- 做法:只有当客户端访问一个 key 时,Redis 才会检查这个 key 是否已过期。如果过期,则立即删除它,并且向客户端返回空值。
- 优点:对 CPU 最友好。只会对访问的 key 进行过期检查,不会在无关的 key 上浪费 CPU 时间。
- 缺点:对内存不友好。如果一个 key 已经过期,但永远不再被访问,那么它就永远不会被删除,会一直占用着内存,相当于内存泄漏。
定期删除 (Periodic Expiration)
- 做法:Redis 每隔一段时间(默认每秒 10 次,可配置)会随机抽取一批设置了过期时间的 key,检查它们是否过期。如果过期,则删除。
SLOW模式:定时任务,默认10Hz(100ms一次),每次耗时不超过25ms。FAST模式:在事件循环中尝试执行,间隔≥2ms,耗时**≤1ms**,执行频率不固定
- 优点:通过限制执行的时长和频率,减少了对 CPU 的影响,同时也能定期清理掉过期的 key,弥补了惰性删除的不足。
- 缺点:难以确定删除操作执行的时长和频率。太频繁会卡顿,太少了又和惰性删除一样。
小结:Redis 同时使用这两种策略,利用它们各自的优点,以达到在合理使用 CPU 时间和避免内存浪费之间取得平衡。
Redis内存淘汰策略
Redis内存淘汰策略(Memory Eviction Policy),当 Redis 的内存使用达到 maxmemory 配置的上限时,会根据配置的淘汰策略来释放内存。
在 redis.conf 文件中通过 maxmemory-policy 配置项设置。常用的策略有:
| 策略 | 含义 | 淘汰范围 | 特点 | 适用场景 |
|---|---|---|---|---|
| noeviction | 不淘汰。新命令返回错误。 | 不适用 | 默认策略。保证数据不丢失,但会牺牲写入可用性。 | 数据绝对不能丢失,将 Redis 作为数据库而非缓存的场景。 |
| volatile-ttl | 优先淘汰剩余存活时间最短的键。 | 仅 expire键 | 根据 TTL 值决定,越快过期的越先被删除。 | 希望尽量保留那些还能存活很久的缓存数据。 |
| volatile-random | 随机淘汰一个设置了过期时间的键。 | 仅 expire键 | 实现简单,没有算法开销。 | 几乎不用。除非完全随机且不关心哪些数据被删除。 |
| volatile-lru | 淘汰最近最少使用的键。 | 仅 expire键 | 基于访问时间,维护访问顺序。淘汰最久未访问的冷数据。 | 常用。明确区分“缓存”和“持久数据”,只淘汰缓存。 |
| volatile-lfu | 淘汰访问频率最低的键。 | 仅 expire键 | 基于访问次数,统计访问频率。淘汰最不活跃的冷数据。 | 访问模式难以预测,希望淘汰那些长期“无人问津”的冷数据。 |
| allkeys-random | 随机淘汰任意一个键。 | 所有键 | 实现简单,无差别随机删除。 | 几乎不用。 |
| allkeys-lru | 淘汰所有键中最近最少使用的键。 | 所有键 | 最常用、最通用的策略。基于访问时间淘汰冷数据。 | 生产环境首选。数据访问符合幂律分布(有热点数据),不确定用什么时选它。 |
| allkeys-lfu | 淘汰所有键中访问频率最低的键。 | 所有键 | 基于访问频率,能更好区分热点和偶然访问的数据。 | 对冷热数据区分要求极高,有大量扫描式访问的场景。 |
建议:通常推荐使用 allkeys-lru 策略,因为它能很好地利用“二八定律”,将最常访问的数据保留在内存中。
LRU(LeastRecentlyUsed),最少最近使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
LFU(Least Frequently Used),最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。

过期字典 (Expires Dictionary)
Redis是如何知道一个key是否过期呢?Redis 通过其内部的 过期字典(expires dictionary) 来高效地管理和检查 key 的过期时间。(可以想象成一个哈希表)。
- 键 (Key):指向真正数据库里那个 key 的指针。
- 值 (Value):一个
long long类型的整数,保存了该 key 的绝对过期时间戳(以毫秒为单位)。
例如,当你执行命令 SET mykey "hello" EX 60:
- Redis 会将
mykey和它的值"hello"存入主数据库字典。 - 同时,它会计算一个 60 秒后的绝对时间戳(比如
1646123456000)。 - 然后将
mykey和这个时间戳1646123456000作为一个键值对存入过期字典。
检查过期的过程
当需要判断一个 key 是否过期时(无论是在访问时还是定期删除时),Redis 只需要:
- 检查存在性:首先检查这个 key 是否存在于过期字典中。
- 如果不存在,说明该 key没有设置过期时间,永不过期。
- 比对时间戳:如果存在,则取出过期字典中存储的时间戳,与当前的系统时间戳进行比对。
- 如果 当前时间 > 过期时间戳,则该 key已过期。
- 如果 当前时间 <= 过期时间戳,则该 key未过期。
这个过程是一次高效的哈希查找和一次整数比较,速度极快。
