大多数人初次接触 Redis 时把它当作"一个缓存",但这低估了它。Redis 是一个内存数据结构存储:它把丰富的数据类型——字符串、哈希、列表、集合、有序集合、流——保存在内存里,并通过网络对外暴露针对这些结构的原子操作。数据结构加微秒级延迟的组合,正是为什么缓存、会话存储、限流器、排行榜、队列、发布/订阅常常都落在 Redis 上,甚至同一个系统里全都用它。这就是我们在缓存和排行榜文章里反复用到的技术,下面讲讲它到底怎么工作。
- 不只是缓存,而是数据结构服务器。字符串、哈希、列表、集合、有序集合、流、位图、HyperLogLog、地理位置,各自带有原子的 O(1)/O(log n) 操作。
- 内存 + 单线程执行命令——内存速度加上无锁,意味着操作简单、可预测、极快(单节点约每秒 10 万+ 操作)。
- 持久化是可选且可调的——RDB 快照(紧凑、重启快、可能丢最近的写)对比 AOF 追加日志(更持久、文件更大);常常两者都开。
- 淘汰策略让它成为缓存——设置
maxmemory和策略(LRU/LFU/TTL),内存压力下 Redis 自动淘汰。 - 高可用靠复制 + Sentinel;扩展靠 Cluster——Cluster 把键空间分散到 16384 个哈希槽。
- 当心大 key、热 key 和阻塞命令——一条慢的 O(n) 命令会卡住单线程,殃及所有客户端。
Redis 把带类型的数据结构放在内存里,在单线程上执行命令,所以操作天然原子且飞快。根据能容忍多少数据丢失,用 RDB(快照)和/或 AOF(写日志)持久化。用淘汰策略把它当缓存跑,用复制 + Sentinel 做高可用,用 Cluster(哈希槽)做横向扩展。它的数据类型解锁的远不止缓存:排行榜(有序集合)、限流、队列、发布/订阅、分布式锁。
为什么是内存 + 单线程
两个设计选择解释了 Redis 的速度。第一,数据放在内存(RAM)里,读写完全不碰磁盘——延迟是微秒级而非毫秒级。第二,命令执行是单线程的:一次只在一个核上跑一条命令,通过事件循环处理。这听起来像限制,其实是特性:没有锁、命令之间没有竞态,每个操作天然原子。你永远不会拿到一个执行了一半的自增。对内存存储来说 CPU 很少是瓶颈——网络和内存带宽才是——所以单线程也能轻松扛住每秒 10 万+ 操作。(现代 Redis 确实用线程做 I/O 和后台任务,但核心命令执行仍是单线程。)
因为一个线程服务所有人,一条慢命令会阻塞其他所有客户端。在一百万元素的集合上跑 KEYS * 或大 SMEMBERS,会让服务器卡住整段时间。运维 Redis 的第一准则就是:让单条命令保持快速,避免在热路径上对大结构做 O(n) 操作。
数据结构
Redis 与 Memcached 这类纯键值缓存的区别,在于它的带类型的值以及在服务端执行的操作:
- 字符串(String)——字节、整数(原子
INCR)或二进制块。最基本的缓存值。 - 哈希(Hash)——字段→值的映射;不必序列化整个对象就能存对象。
- 列表(List)——链表;
LPUSH/RPOP可做简单的队列和栈。 - 集合(Set)——无序去重成员;成员判断、并集/交集(如共同好友)。
- 有序集合(ZSET)——按分数排序的成员;排行榜和限流器背后的引擎。
- 流(Stream)——带消费者组的追加日志;一个轻量的类 Kafka 原语。
- 位图 / HyperLogLog / 地理位置——省空间的计数器、近似基数统计、地理查询。
SET session:42 "..." EX 3600 # 字符串 + 1 小时 TTL
INCR views:home # 原子计数器
HSET user:42 name "Ada" tier gold # 用哈希存对象
ZADD board 9820 alice # 有序集合 → 排行榜
ZREVRANK board alice # O(log n) 求排名
LPUSH jobs "task1" / BRPOP jobs 0 # 简单队列
持久化:RDB 对比 AOF
内存存储不一定意味着易失。Redis 提供两种持久化机制,让它能在重启后存活:
| 方面 | RDB(快照) | AOF(追加文件) |
|---|---|---|
| 存什么 | 数据集的某个时间点完整转储 | 每条写命令,启动时重放 |
| 持久性 | 可能丢上次快照以来的写 | 最多丢约 1 秒(everysec fsync) |
| 文件大小/重启 | 紧凑;重启快 | 较大;重放较慢 |
| 成本 | 周期性 fork 可能让内存陡增 | 持续写入 + 重写/压实 |
RDB 适合备份和快速重启;AOF 在你无法容忍丢超过一秒的写时更合适。常见的生产配置是两者都开——AOF 保证持久性,RDB 做快速备份——很多还用混合格式。如果纯把 Redis 当缓存用,可以完全关掉持久化,把数据当作可重新计算的。
淘汰与过期
要把 Redis 当缓存用,你用 maxmemory 给它的内存设上限,并选一个内存满时的淘汰策略:allkeys-lru(淘汰最久未用)、allkeys-lfu(淘汰最少使用——对倾斜访问更好)、volatile-ttl(在设了 TTL 的键里淘汰最快到期的),或 noeviction(拒绝写)。另外,键可以通过 EXPIRE 带上 TTL。Redis 同时用惰性(访问时)和主动(后台采样删除过期键)两种方式过期,在 CPU 与内存间权衡。淘汰(注意:Redis 用采样来近似 LRU/LFU,不是完美的全局排序)正是它能充当我们缓存深入讲解里那一层缓存的原因。
复制与高可用
单个 Redis 节点是单点故障。复制增加一个或多个副本,异步拷贝主节点的数据——可用于读扩展和热备。要做自动故障转移,Redis Sentinel 监控主从;主挂了,Sentinel 达成法定多数,提升一个副本为主并重新配置客户端。由于复制是异步的,故障转移可能丢失尚未到达被提升副本的最后几条写——这正是分布式系统里普遍存在的持久性与可用性权衡(见我们的 DDIA 复制笔记)。
Redis Cluster:横向扩展
当数据集或写吞吐超出单节点,Redis Cluster 把键空间分片到多个主节点。它把键空间划分为 16384 个哈希槽;每个键通过 CRC16(key) mod 16384 映射到一个槽,每个主节点拥有一段槽区间。加一个节点意味着把一部分槽迁移过去——只有受影响的键迁移,而不是整个键空间。
slot(key) = CRC16(key) mod 16384
节点 A: 槽 0 – 5460 (+ 副本)
节点 B: 槽 5461 – 10922 (+ 副本)
节点 C: 槽 10923 – 16383 (+ 副本)
多键操作必须落在同一个槽 → 用 {hash tag}:
MGET {cart:42}:items {cart:42}:total # 两者都对 "cart:42" 取哈希
有个坑:多键操作(以及事务)只有在所有键落在同一个槽时才有效,你用哈希标签(hash tag)来强制这一点——{} 里的部分才是用来取哈希的。这也是让应用代码必须"感知 Cluster"的主要原因。
Redis 实际拿来干什么
这些数据结构直接对应了一大批用途:
- 缓存——最经典的:数据库前面的 cache-aside。
- 会话存储——快速、带 TTL 的用户会话,在无状态应用服务器间共享。
- 限流器——原子
INCR配EXPIRE,或用有序集合做滑动窗口。 - 排行榜——有序集合给出 O(log n) 的排名和 top-k(见排行榜设计)。
- 队列——列表(
LPUSH/BRPOP)或带消费者组的流。 - 发布/订阅——为实时功能做轻量的扇出消息。
- 分布式锁——
SET key val NX EX当作租约;Redlock 算法把它扩展到多节点(虽有争议——单实例锁配合 fencing 往往就够了)。
Redis 对比 Memcached
| 方面 | Redis | Memcached |
|---|---|---|
| 数据类型 | 丰富(列表、集合、ZSET、流……) | 仅字符串/二进制块 |
| 持久化 | RDB + AOF | 无(纯缓存) |
| 复制/高可用 | 有(副本、Sentinel、Cluster) | 无内建复制 |
| 线程模型 | 核心单线程 | 多线程 |
| 最适合 | 多样数据结构、需要持久化 | 简单的大型多核键值缓存 |
常见坑
- 大 key——单个巨大的值(百万元素的列表)会让对它的每个操作都变慢并卡住线程;拆分或重构。
- 热 key——某个键流量畸高会把负载集中到它所在的节点;客户端缓存或拆 key 可缓解。
- 阻塞命令——生产中避免
KEYS、大SMEMBERS/HGETALL;遍历用SCAN。 - 内存与持久化的相互作用——RDB 的 fork 会瞬时让内存翻倍;预留相应余量。
把 Redis 理解成一个单线程、内存的数据结构服务器最为贴切——这个框架解释了它的速度(内存 + 无锁 + 原子操作)、它的主要风险(一条慢命令卡住所有人)以及它的多才多艺(选对数据类型,Redis 就变成缓存、队列、排行榜或锁)。需要持久性时上 RDB/AOF,需要故障转移时上 Sentinel,单节点不够时上 Cluster(哈希槽)。
单线程的 Redis 为什么快?数据在内存里、一次只跑一条命令,所以操作原子且无锁;瓶颈是网络/内存而非 CPU。
RDB 对比 AOF?RDB = 紧凑的时间点快照,重启快,可能丢最近的写;AOF = 追加每条写,约 1 秒持久性,文件更大、重放更慢。常常两者都开。
Redis Cluster 怎么分片?16384 个哈希槽;键经 CRC16 mod 16384 映射到槽;每个主拥有一段槽区间。多键操作需要用 {hash tag} 落到同一个槽。
Redis 对比 Memcached?Redis 有丰富数据类型、持久化和复制;Memcached 是更简单的多线程块缓存。除非只要纯缓存,否则选 Redis。
最大的运维风险?一条慢的 O(n) 命令(KEYS、大集合读)阻塞单线程,殃及所有客户端。