跳到主要内容

Redis 核心概念与架构

为什么用 Redis​

Redis 是一个把数据放在内存里的键值数据库,官方给自己的定位是「数据结构服务器」——不只是简单的 key-value,value 可以是字符串、哈希、列表、集合、有序集合,还能做位图、HyperLogLog、GEO、Stream。运维视角看,它出现在架构图里通常只有四类位置:

  • 缓存:最主流的用法。把 MySQL 里的热点数据、复杂查询结果、页面片段放进 Redis,读请求先打 Redis,未命中再回源。单机十万级 QPS 很轻松,挡住大部分读流量后数据库压力立刻下来。典型写法是 Cache-Aside:读时先查缓存,写时先更新库再删缓存。
  • 会话(Session):Web 服务多实例部署后,本地 session 就不成立了——第一次请求落在 A 实例,第二次落到 B 实例,登录态就丢了。把 session 存进 Redis,所有实例共享,服务本身变成无状态,扩缩容随意。Spring Session、PHP 的 session handler 都是这个套路。
  • 队列:用 List 的 LPUSH + BRPOP 做简单任务队列,或用 Stream 做带消费组、ACK 的消息队列。适合「轻量、允许少量丢失、不想为它单独维护一套 MQ」的场景;要做可靠投递、削峰填谷、多消费组广播,还是交给 Kafka。
  • 计数器:INCR 是原子操作,天然适合点赞数、库存扣减、接口限流、UV 统计。配合 EXPIRE 做固定窗口计数,就是最常用的限流实现。
它不适合什么

Redis 是内存数据库,不是「什么都能存」的万能存储。全量业务数据、需要复杂事务和 join 的关系型数据、需要持久化承诺的账务数据,都不该只有 Redis 一份。把它当缓存和加速层,永远保留回源能力,是最稳的姿势。

整体架构​

Redis 架构

从下往上看这张图,Redis 一个进程里其实就四层:

  • 网络层:监听 TCP 端口(默认 6379),用 epoll 做 IO 多路复用,把成千上万个连接上的可读事件收集起来,交给主线程按顺序处理。
  • 命令执行层:单线程串行执行命令,一条命令从解析、查找 key、执行到写回响应,全程不切线程。慢命令会堵住后面所有请求,这是 Redis 最重要的脾气。
  • 数据结构层:对外是五种基本类型,对内是一堆精心设计的编码(SDS、listpack、quicklist、intset、跳表等),同一种类型在不同数据量下会自动换编码,在不同场景下用空间换时间或反过来。
  • 存储与持久化层:数据主体在内存里,由 jemalloc 管理;RDB 快照和 AOF 日志负责落盘;主从复制和 Cluster 负责把数据摊到多台机器上。

单线程为什么快​

「单线程还快」是 Redis 最常被问的问题。快不是因为单线程,而是因为它把该省的成本全省了:

  • 纯内存操作:一次内存访问是纳秒级,一次机械盘随机读写是毫秒级,差了几个数量级。Redis 的命令绝大多数只碰内存,不走磁盘(AOF 的 fsync 由后台线程或子进程处理),这是快的根本原因。
  • IO 多路复用:一个线程用 epoll 同时盯住几万个 socket,谁有数据就处理谁,不需要「一个连接一个线程」。没有线程创建销毁、没有上下文切换,几万并发连接下 CPU 依然平稳。
  • 无锁:命令串行执行,同一时刻只有一个命令在改数据,天然没有竞态,也就不需要任何加锁、解锁、CAS 重试。省掉的不只是锁开销,还有排查死锁的头发。
  • 协议和实现都很轻:RESP 协议解析极简,核心操作是哈希表 O(1) 查找,数据结构编码针对小对象做了大量优化。
单线程的代价

单线程意味着一条慢命令会阻塞所有客户端。KEYS * 扫百万 key、HGETALL 拿几万个字段、DEL 一个几百万元素的集合、复杂 Lua 脚本死循环,都会让整个实例卡住。生产上禁 KEYS、禁大 key、给 Lua 加超时,都是从这条特性推出来的规矩。

6.0 之后的 IO 多线程

Redis 6.0 引入了 IO 多线程:读 socket、解析协议、写响应可以分给多个线程并行做,但命令执行仍然是单线程。所以「单线程」这个结论对数据一致性模型依然成立,只是网络收发的瓶颈被缓解了。相关配置是 io-threads,一般 4 核以上、QPS 打满单核时才值得开。

五大数据结构​

每种类型一句话,先建立直觉:

类型一句话底层编码(小对象到大对象)典型用途
String一个 key 对应一段字节,能存文本、JSON、数字、二进制int / embstr / raw(SDS)缓存对象、计数器、分布式锁、位图
Hash一个 key 下再挂一层 field-value,像一行记录listpack 到 hashtable存对象的多个字段、购物车
List双向链表,两头进出都快listpack 到 quicklist简单队列、最新列表、时间线
Set无序不重复集合,支持交并差intset / listpack 到 hashtable去重、标签、共同好友
ZSet每个元素带 score,按 score 自动排序listpack 到 skiplist + dict排行榜、延时队列、范围查询

选型口诀:

  • 存整个对象、只读不改 → String 存 JSON,一次网络往返拿全量,简单直接。
  • 对象字段频繁单独更新 → Hash,HSET user:1 age 20 只改一个字段,不用读出整个 JSON 再写回。
  • 要顺序、要两端进出 → List;要「多个消费者各消费一份、支持 ACK 和回溯」→ Stream。
  • 只要去重、要集合运算 → Set。
  • 要排名、要按分数范围取 → ZSet。
别忽略这几个扩展类型

Bitmap 用位存签到、活跃标记,一亿用户一年只要 4.5 GB;HyperLogLog 用 12 KB 估算亿级 UV,误差 0.81%;GEO 底层就是 ZSet,存经纬度算附近的人;Stream 是 5.0 引入的正经消息队列。它们不算「五大数据结构」,但都是同一套 key 体系下的命令组,遇到对应场景优先用,别自己拿 String 硬拼。

过期与淘汰​

过期删除​

EXPIRE key 60 给 key 挂一个 TTL,到期后 Redis 用两种方式清理:

  • 惰性删除:访问到这个 key 时先检查是否过期,过期就删掉并返回空。好处是不用扫全库,坏处是「再也不被访问的过期 key」会一直占内存。
  • 定期删除:后台每隔一段时间随机抽一批设置了 TTL 的 key,删掉其中过期的;如果这批里过期比例超过 25%,就继续抽下一批。抽样强度由 hz(默认 10)和 active-expire-effort(默认 1,可调到 10)控制。

两种策略配合,过期 key 不会长期堆积,但内存不会立刻归还给操作系统——jemalloc 释放的内存留在自己的池子里复用,used_memory_rss 一时降不下来是正常现象。

淘汰策略​

内存不是无限的,maxmemory 划出上限后,写满时就得按 maxmemory-policy 决定删谁。Redis 一共 8 种:

策略淘汰范围挑选方式适用场景
noeviction不淘汰写入直接报错数据不能丢、宁可写失败(默认值)
allkeys-lru所有 key最近最少使用纯缓存,最常用
allkeys-lfu所有 key访问频率最低热点集中、有长尾冷数据
allkeys-random所有 key随机访问概率均匀,几乎不用
volatile-lru只挑设了 TTL 的最近最少使用缓存和持久数据混布
volatile-lfu只挑设了 TTL 的访问频率最低同上,对热点敏感
volatile-random只挑设了 TTL 的随机同上,几乎不用
volatile-ttl只挑设了 TTL 的剩余 TTL 最短优先希望快过期的先走

几个实操要点:

  • 纯缓存实例用 allkeys-lru,别再给每个 key 都设 TTL 又用 volatile-*——一旦漏设了 TTL,那部分 key 就永远淘汰不掉。
  • volatile-* 在没有任何 key 设 TTL 时退化成 noeviction,写满后写入直接报 OOM,这是线上最常见的「莫名其妙写不进去」。
  • LRU 是近似 LRU:每个 key 记一个 24 位的时钟,淘汰时随机采样 maxmemory-samples(默认 5)个 key 挑最旧的,采样数越大越接近真 LRU,代价是 CPU。
  • LFU 需要额外维护访问频率计数器,适合「少量热点被反复访问、大量冷数据偶尔被扫」的分布,从 4.0 开始支持。
# 查看当前策略与上限
redis-cli CONFIG GET maxmemory maxmemory-policy

# 在线调整:4GB 上限 + allkeys-lru
redis-cli CONFIG SET maxmemory 4gb
redis-cli CONFIG SET maxmemory-policy allkeys-lru

持久化:RDB 与 AOF​

内存数据断电就没,持久化解决「重启后数据还在不在」。Redis 给了两条路,可以单用也可以混用。

RDB 快照​

某一时刻把整个内存 dump 成二进制文件(默认 dump.rdb)。触发方式有三种:SAVE(前台阻塞,生产禁用)、BGSAVE(fork 子进程写,主进程继续服务)、配置文件里的定时规则(如 save 900 1,900 秒内至少 1 次写就触发)。

优点是文件紧凑、加载极快,适合做备份和全量同步;缺点是两次快照之间的数据会丢,fork 时还有一次明显的延迟毛刺——这两点第 6 篇会展开。

AOF 日志​

把每条写命令追加到日志文件,重启时重放。appendfsync 决定落盘时机:

  • always:每条命令都 fsync,最安全,性能最差(QPS 掉一个数量级)。
  • everysec:每秒 fsync 一次,最多丢 1 秒数据,生产默认选它。
  • no:交给操作系统,通常 30 秒左右才落盘,性能最好,丢数据最多。

AOF 文件会越写越大,Redis 通过重写(BGREWRITEAOF)把它压缩成「恢复当前数据集所需的最小命令集」。7.0 起 AOF 变成多部分(multi-part):一个 base 文件(RDB 或 AOF 格式)+ 若干 incr 增量文件 + 一个 manifest 清单,统一放在 appenddirname(默认 appendonlydir)目录里,重写时不再需要把整个文件读进内存拼一遍。

怎么选​

对比项RDBAOF
数据安全差,丢最后一次快照之后的数据好,everysec 最多丢 1 秒
恢复速度快,直接加载二进制慢,逐条重放命令
文件体积小,压缩存储大,重写后才收敛
对性能影响fork + COW,有延迟毛刺持续追加写,everysec 影响小
适用场景备份、主从全量同步、能容忍丢数据对数据安全有要求的业务实例

生产推荐两个都开:appendonly yes + 保留 RDB 定时快照。7.x 默认 aof-use-rdb-preamble yes,AOF 的 base 文件本身就是 RDB 格式,重启时先加载 RDB 再重放增量,兼顾恢复速度和数据安全。

# 当前持久化状态
redis-cli INFO persistence

# 手动触发快照 / AOF 重写
redis-cli BGSAVE
redis-cli BGREWRITEAOF

小结​

  • Redis 的定位是内存数据结构服务器,主业是缓存、会话、轻量队列、计数器四类场景,不适合当唯一数据源。
  • 快的原因是纯内存 + IO 多路复用 + 无锁串行;代价是单线程怕慢命令,大 key 和 KEYS 是头号杀手。
  • 五种基本类型各有适用面,选型看「读全量还是改字段」「要不要顺序」「要不要排序」。
  • 过期靠惰性 + 定期两种删除,写满靠 8 种 maxmemory-policy 淘汰,纯缓存认准 allkeys-lru。
  • 持久化两条路:RDB 快、AOF 稳,生产建议都开,7.x 的 AOF 已经是多部分结构。

概念清楚了,下一篇直接动手:用 apt、源码、Docker 三种方式把 Redis 装起来,跑通第一次连接,见 安装与快速上手。