第1章:Redis 术语全景与单线程工作原理

0 阅读20分钟

1. 项目背景

1.1 业务场景

公司准备把一个日活百万的传统电商系统改造成"缓存优先"的高并发架构。产品需求文档里列了一长串功能:登录验证码要5分钟过期、商品详情要缓存加速、购物车要支持实时修改、积分排行榜要实时展示、附近门店要按距离排序、订单状态变更要异步通知下游系统。

技术评审会上,每个人对 Redis 的认知都不一样。后端开发小王说:"Redis 就是个带 TTL 的 HashMap,我把对象序列化一下存进去就行。"测试主管张姐问:"那 Redis 挂了数据会丢吗?如果会丢,我们的测试用例怎么验证数据一致性?"运维老李直接摇头:"我们之前线上 Redis 内存满了直接 OOM,重启后发现一部分 key 消失了,你们知道是为什么吗?"

1.2 痛点

这种认知差异会直接带来生产事故:

认知误区可能的后果真实案例
Redis=HashMap把所有业务数据塞进一个大 key,单次 GET 返回 50MB某电商大促期间,商品详情接口的 Redis 响应包超过网络带宽限制
Redis=数据库依赖 Redis 强持久化,不做数据库兜底Redis 崩溃后依赖 AOF 恢复,发现最后 2 秒数据丢失
单线程一定快线上写 KEYS * 排查问题,整个 Redis 阻塞 3 秒某社交 App 用户搜索接口大面积超时
不加过期时间计数器 key 无限增长,内存从 4GB 涨到 32GB某内容平台接口调用计数 key 达到 8000 万个
忽视内存成本所有数据都放 Redis,不做容量规划某 SaaS 平台 Redis 月度成本超过 RDS 成本

更麻烦的是,一旦线上出现慢请求,团队第一反应是怀疑"Redis 单线程扛不住",却没有真正理解 Redis 的请求处理链路——问题可能是网络延迟、客户端连接池耗尽、慢命令阻塞、fork 子进程导致的写时复制、或者是 Cluster 重定向过多。

1.3 本章目标

本章不是背命令手册,而是建立一张 Redis 的"房屋结构图":知道门在哪里(客户端连接)、客厅怎么走(命令执行)、每个房间放什么(数据结构)、水电气怎么通(持久化、复制、集群)。只有先知道每个术语在系统里所处的位置,后续学习 String、Hash、Stream、Cluster、源码时才不会碎片化。

另一个重要目标是让读者直观感受 Redis 的请求处理链路。我们会用 Docker 启动一个 Redis 8.6,执行一组基础命令,用调试工具追踪一次请求的生命周期,最后用一张文本架构图把"一条命令从客户端到 Redis 再返回"的过程串起来。


2. 项目设计

2.1 第一幕:Redis 到底是个什么东西?

小胖(嘴里还嚼着薯片):"我就不明白了,Redis 不就是个会过期的 Map 吗?我 SET user:1 张三,然后 GET user:1,这和 Java 里的 HashMap 到底有啥区别?为什么面试的时候问 Redis,面试官老爱追问什么内存模型、线程模型、数据结构?"

小白(放下手中的技术书,推了推眼镜):"如果只是 HashMap,那它怎么支持网络访问?怎么让几十个客户端同时读写?怎么在你重启电脑后数据还在?怎么让数据在多个服务器之间自动同步?再说,HashMap 里的 Key 可以是任何对象,Redis 的 Key 只能是字符串——这本身就是设计取舍。"

小胖:"那你说它到底是什么?缓存?数据库?消息队列?网上有人把它当缓信用,有人把它当临时数据库用,还有人用它做分布式锁和限流器,它到底是干啥的?"

大师(把白板推到中间,拿起记号笔):"好问题。你们先放下技术偏见。我把 Redis 画成四层楼,你们就清楚了。"

┌─────────────────────────────────────────────┐
│  客户端层 (Client Layer)                     │
│  redis-cli / Jedis / Lettuce / Redis-py     │
│  redis-benchmark / RESP 协议                │
└──────────────────┬──────────────────────────┘
                   │ TCP / TLS / Unix Socket
                   ▼
┌─────────────────────────────────────────────┐
│  网络接入层 (Networking Layer)               │
│  连接管理、RESP 协议解析、IO 多路复用          │
│  readQueryFromClient → processInputBuffer   │
└──────────────────┬──────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────┐
│  命令执行层 (Command Execution Layer)        │
│  命令查找表、权限校验(ACL)、Lua 脚本引擎       │
│  processCommand → lookupCommand → call      │
│  慢日志记录、统计信息收集                     │
└──────────────────┬──────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────┐
│  数据结构层 (Data Structures Layer)          │
│  String / Hash / List / Set / ZSet          │
│  Stream / Bitmap / HyperLogLog / GEO        │
│  RedisObject (type + encoding + lru + ptr)  │
└──────────────────┬──────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────┐
│  生命周期与可靠性层 (Lifecycle & Durability)  │
│  过期删除 (Expire) / 内存淘汰 (Eviction)      │
│  RDB 快照 / AOF 日志 / 混合持久化             │
│  主从复制 / Sentinel 高可用 / Cluster 集群    │
│  模块扩展 (Module: JSON / Search / Vector)   │
└─────────────────────────────────────────────┘

大师:"Redis 是一家高效奶茶店。客户端是顾客,网络层是点单窗口——支持多个窗口同时排队但只有一个操作台。命令执行层就是操作台后的茶饮师,按照配方表(命令表)快速制作。数据结构层是身后的原料架——糖、奶、茶、珍珠,每种原料就是 Redis 的一个数据结构。生命周期层意味着:珍珠放久了会扔掉(过期),糖不够了要计划进货(淘汰),每天打烊后把营业记录写进账本(持久化),分店之间互相同步配方(复制/集群)。"

小胖:"嘶——这么说我就懂了!那为什么大家都说 Redis 是单线程?一个茶饮师能忙过来吗?"

技术映射:Redis 的核心执行引擎是单线程事件循环(Event Loop),所有命令在单个线程内排队执行。每一笔操作都必须在微秒级完成,否则后续所有请求都会等待。

2.2 第二幕:单线程为什么快?

小白:"对啊,这也是我最大的疑问。Java 的 HashMap 是线程不安全的,多个线程同时写会出问题。如果 Redis 是单线程,那它怎么处理高并发?现在服务器都是多核的,只用一个核不是浪费吗?"

大师:"问得好。Redis 快不是因为它无脑单线程,而是因为它做对了几件事。"

他用白板画了第二个图:

┌──────────────────────────────────────────────────┐
│                Redis Event Loop                   │
│  ┌──────────────────────────────────────────┐    │
│  │           IO 多路复用 (epoll/kqueue)       │    │
│  │  ┌─────┐  ┌─────┐  ┌─────┐  ┌─────┐     │    │
│  │  │ fd1 │  │ fd2 │  │ fd3 │  │ fd4 │ ... │    │
│  │  └──┬──┘  └──┬──┘  └──┬──┘  └──┬──┘     │    │
│  └─────┼────────┼────────┼────────┼─────────┘    │
│        │        │        │        │               │
│        ▼        ▼        ▼        ▼               │
│  ┌──────────────────────────────────────────┐    │
│  │         命令队列(按序执行)              │    │
│  │  GET user:1 → SET order:1001 → INCR cnt  │    │
│  └──────────────────────────────────────────┘    │
│                      │                            │
│                      ▼                            │
│  ┌──────────────────────────────────────────┐    │
│  │          内存数据操作(纳秒~微秒级)       │    │
│  │  哈希表查找 → 编码转换 → 值读写 → 响应   │    │
│  └──────────────────────────────────────────┘    │
└──────────────────────────────────────────────────┘

大师:"Redis 的快来自三个核心设计。第一,纯内存操作——数据都在内存里,一次 GET 的内存访问时间大约 100 纳秒,而一次磁盘访问大约 10 毫秒,差了 10 万倍。第二,IO 多路复用——Redis 用 epoll(Linux)或类似的系统调用同时监听成千上万个连接。当一个连接有数据到达,epoll 通知 Redis,Redis 读取并处理,然后立刻处理下一个。这就像茶饮师同时盯着 10 个顾客,哪个顾客举手了就立刻服务他,而不是一个一个轮着问'你要点什么'。第三,数据结构紧凑、编码优化——小 String 用 embstr 编码,小 Hash 用 listpack 编码,小 ZSet 用 listpack 替代跳表,每一项设计都在省内存、省时间。"

小白:"但我还是觉得单线程有风险。如果一个命令执行了 100 毫秒,那这 100 毫秒内所有其他客户端都在干等?"

大师:"完全正确。这就是 Redis 最大的坑——阻塞命令。"

技术映射:Redis 的单线程模型本质是"用极简换取极快",但它要求使用者必须了解每个命令的复杂度,避免在单线程上执行长耗时操作。

2.3 第三幕:Redis 8.x 能力地图

小胖(挠头):"那 Redis 8.x 到底能干什么?除了缓存,还有什么大招?"

大师:"Redis 8.x 已经远不是'缓存中间件'了。我把它的能力地图画出来:"

能力域核心能力典型场景涉及章节
缓存加速String/Hash 缓存、TTL、淘汰策略、旁路缓存商品详情、用户会话、配置热加载第3-4章、第12章
计数器与状态INCR/DECR、Bitmap 签到、HyperLogLog 去重统计浏览量、月活、接口 UV第3章、第8-9章
关系与排序Set 集合运算、ZSet 排行榜、GEO 地理位置社交关系、积分榜、附近门店第6-7章、第10章
队列与消息List 队列、Stream 消息流、Pub/Sub 广播异步任务、订单事件、配置推送第5章、第20-21章
原子操作Lua 脚本、事务(WATCH/MULTI)、分布式锁库存扣减、限购、防重提交第13-14章、第18章
持久化RDB 快照、AOF 日志、混合持久化数据恢复、迁移、灾备第22章
高可用主从复制、Sentinel 自动故障转移、Cluster 分片读写分离、自动切主、水平扩容第23-25章
可观测性INFO/CLIENT LIST/SLOWLOG/LATENCY/MONITOR慢查询治理、容量规划、告警第15章、第27-28章
安全ACL 用户体系、TLS 加密、命令禁用多租户隔离、合规审计第29章
扩展与 AIModule API、JSON/Query/Vector/TimeSeries语义缓存、RAG、实时索引第38-39章

小胖(眼睛瞪大):"这么多?!我以为 Redis 就是用来做缓存的……"

大师:"这就是为什么我们第一课要画地图。你不必记住每一条命令,但必须知道'遇到什么问题该去 Redis 地图的哪个区域找答案'。后面 40 章就是带你一步步走完这张地图。"

小白:"那我问一个落地问题——Redis 8.6 相比旧版本有什么值得关注的变化?"

大师:"几个关键点。第一,读写 IO 多线程支持(Redis 6 开始引入,到 8.x 更加成熟),这解决的不是命令执行慢的问题,而是网络读写的大包传输问题,尤其是大规模 Pipeline 和 LRANGE 大返回时。第二,ACL v2 支持更细粒度的 key 前缀权限控制和通道权限,多租户场景不再只能靠命名约定隔离。第三,Functions(Redis Function)作为 Lua 脚本的演进方案,有更好的版本管理能力和库依赖。第四,字段级过期(HEXPIRE/HPEXPIRE)让 Hash 的字段可以独立过期。第五,内存效率的持续优化——listpack 逐步替代 ziplist,降低了内存碎片风险。"

技术映射:Redis 8.x 的核心演进方向是"更安全(ACL/TLS)、更高效(IO 多线程/listpack)、更灵活(Functions/字段过期)、更智能(Vector/Query)"。

2.4 第四幕:一条命令的完整旅程

小胖:"那我就想搞清楚一件事——我敲一个 SET user:1 xiaopang,Redis 内部到底发生了啥?"

大师:"好,这是今天最重要的图。跟着我的笔走:"

Step 1: 客户端发送
  ┌──────────────┐
  │ redis-cli    │  序列化: *3\r\n$3\r\nSET\r\n$6\r\nuser:1\r\n$8\r\nxiaopang\r\n
  │ (客户端)      │────── TCP Socket ──────▶
  └──────────────┘

Step 2: 网络层接收
  ┌──────────────────────────────────────┐
  │ Redis Server: networking.c           │
  │ epoll 通知 fd 可读                   │
  │ acceptTcpHandler → createClient      │
  │ readQueryFromClient                  │
  │ → 解析 RESP 协议: argc=3, argv=["SET","user:1","xiaopang"]
  └──────────────────────────────────────┘

Step 3: 命令执行
  ┌──────────────────────────────────────┐
  │ Redis Server: server.c               │
  │ processCommand                       │
  │ → lookupCommand("SET") → setCommand  │
  │ → 权限校验 (ACL check)               │
  │ → 参数校验 (arity check)             │
  │ → call(c, CMD_CALL_FULL)             │
  └──────────────────────────────────────┘

Step 4: 数据操作
  ┌──────────────────────────────────────┐
  │ Redis Server: t_string.c / db.c      │
  │ setGenericCommand                    │
  │ → 创建/更新 RedisObject              │
  │   type: OBJ_STRING                   │
  │   encoding: OBJ_ENCODING_EMBSTR      │
  │   ptr → "xiaopang"                   │
  │   lru: 当前时间戳                     │
  │ → dbAdd(c->db, key, val)             │
  │   → dictAdd(db->dict, key, val)      │
  │ → 如果设置了 EX/PX,写入 expire 字典  │
  └──────────────────────────────────────┘

Step 5: 响应返回
  ┌──────────────────────────────────────┐
  │ Redis Server: networking.c           │
  │ addReply(c, shared.ok)  // "+OK\r\n" │
  │ → 写入 client->buf 或 reply list     │
  │ → 注册可写事件                       │
  │ → sendReplyToClient → write 系统调用  │
  └──────────────────────────────────────┘

大师:"整个过程在单线程中一气呵成。从 RESP 解析到哈希表写入到 +OK 返回,没有任何锁竞争、没有上下文切换、没有磁盘 IO。这就是为什么 SET 能跑到 10 万 QPS 以上。"

小白:"那如果我在 SET 的同时有人 GET user:1,会不会读到不完整的数据?"

大师:"不会。因为所有命令都是串行执行的。SET 执行完才轮到下一条 GET,GET 读到的要么是旧值要么是新值,不存在'写了一半'的问题。这是单线程模型最优雅的地方——你用不着考虑数据竞争和加锁。"

技术映射:Redis 的单线程执行模型天然保证了单个命令的原子性,无需开发者在应用层做额外并发控制。


3. 项目实战

3.1 环境准备

启动 Redis 8.6(使用 Docker):

# 拉取镜像
docker pull redis:8.6

# 启动容器(不带密码,方便初学体验)
docker run --name redis-lab-ch01 -p 6379:6379 -d redis:8.6

# 验证容器运行状态
docker ps --filter "name=redis-lab-ch01"

# 进入 Redis CLI
docker exec -it redis-lab-ch01 redis-cli

如果已经运行过其他 Redis 容器,端口可能冲突。换一个端口:

docker run --name redis-lab-ch01 -p 6380:6379 -d redis:8.6
docker exec -it redis-lab-ch01 redis-cli -p 6379

3.2 执行第一组命令:感受 Redis 的响应速度

# 基础连通性测试
127.0.0.1:6379> PING
PONG

# 写入一个 key
127.0.0.1:6379> SET user:1 "xiaopang"
OK

# 读取这个 key
127.0.0.1:6379> GET user:1
"xiaopang"

# 查看 key 是否存在
127.0.0.1:6379> EXISTS user:1
(integer) 1

# 查看 key 的类型
127.0.0.1:6379> TYPE user:1
string

# 查看 key 的内部编码
127.0.0.1:6379> OBJECT ENCODING user:1
"embstr"

# 设置 60 秒过期
127.0.0.1:6379> EXPIRE user:1 60
(integer) 1

# 查看剩余时间
127.0.0.1:6379> TTL user:1
(integer) 56

# 删除 key
127.0.0.1:6379> DEL user:1
(integer) 1
127.0.0.1:6379> GET user:1
(nil)

预期现象与解释:

  • PING 返回 PONG:客户端和 Redis 服务端通信正常,RESP 协议解析正确。
  • OBJECT ENCODING user:1 返回 "embstr":短字符串(小于 44 字节)使用嵌入式字符串编码,对象头和内容分配在同一块内存中,减少内存碎片。
  • TTL 返回的整数值每秒递减,到期后 key 自动删除(惰性删除或定期删除)。

3.3 观察 Redis 运行状态

# 服务端基本信息
127.0.0.1:6379> INFO server
# 重点关注: redis_version, redis_mode, os, process_id, tcp_port

# 客户端连接信息
127.0.0.1:6379> INFO clients
# 重点关注: connected_clients, maxclients

# 内存使用
127.0.0.1:6379> INFO memory
# 重点关注: used_memory_human, maxmemory_human, mem_fragmentation_ratio

# 统计信息
127.0.0.1:6379> INFO stats
# 重点关注: total_commands_processed, instantaneous_ops_per_sec,
#           keyspace_hits, keyspace_misses, expired_keys, evicted_keys

# 查看所有连接的客户端
127.0.0.1:6379> CLIENT LIST
# 输出: id=3 addr=127.0.0.1:xxxxx fd=8 name= age=10 idle=0 ...

# 查看当前所有 key
127.0.0.1:6379> KEYS *
# 注意: 仅学习环境使用,生产环境应使用 SCAN

3.4 用 redis-benchmark 感受 Redis 吞吐量

redis-benchmark 是 Redis 自带的性能压测工具。退出 redis-cli(按 Ctrl+C),在容器内运行:

# 进入容器 bash
docker exec -it redis-lab-ch01 bash

# 基础压测:10 万请求,50 并发连接,只测 SET 和 GET
redis-benchmark -n 100000 -c 50 -t set,get -q

# 输出示例(实际数值因机器性能而异):
# SET: 85470.09 requests per second, p50=0.271 msec
# GET: 90909.09 requests per second, p50=0.239 msec

参数解释:

  • -n 100000:总请求数 10 万。
  • -c 50:并发 50 个客户端连接。
  • -t set,get:只测试 SET 和 GET 命令。
  • -q:安静模式,只显示 QPS 结果。

加上延迟分位数:

redis-benchmark -n 100000 -c 50 -t set,get --latency

额外测试几个常用命令的性能差异:

# 测试 PING(最快,纯协议响应)
redis-benchmark -n 100000 -c 50 -t ping -q

# 测试 INCR(原子自增)
redis-benchmark -n 100000 -c 50 -t incr -q

# 测试 LPUSH(List 写入)
redis-benchmark -n 100000 -c 50 -t lpush -q

观察到的性能排序通常是:PING > SET/GET > INCR > LPUSH。这是因为不同命令涉及的数据结构操作复杂度不同:PING 几乎不操作数据,SET/GET 只做哈希表查找,INCR 需要读取、自增、写回,LPUSH 需要操作链表节点。

3.5 亲手触发一次"阻塞"

这个实验非常重要——让你直观感受慢命令对 Redis 的影响。

在一个终端执行慢命令(使用 Lua 模拟一个耗时操作):

docker exec -it redis-lab-ch01 redis-cli

先写入一些数据:

127.0.0.1:6379> SET test:1 "hello"
OK
127.0.0.1:6379> SET test:2 "world"
OK

打开第二个终端,执行一个耗时 3 秒的 Lua 脚本(Redis 的 Lua 脚本会阻塞事件循环):

终端 B(先准备好,但不要回车):

docker exec -it redis-lab-ch01 redis-cli EVAL "local start=redis.call('TIME')[1]; while(redis.call('TIME')[1]-start<3) do end; return 'slow done'" 0

终端 A(先执行一个带时间戳的命令来校准):

docker exec -it redis-lab-ch01 redis-cli TIME

然后在终端 B回车执行慢脚本,同时在终端 A立即执行:

docker exec -it redis-lab-ch01 redis-cli PING

你会发现 PING 等了大约 3 秒才返回 PONG。这就是单线程模型的"副作用"——一个慢命令会拖慢所有其他请求。

这个实验的价值在于:它让你亲眼看见了"慢命令阻塞"不是理论,而是真实可复现的。后续学习时,你会对 KEYS *、大 key 删除、复杂 Lua 脚本保持敬畏。

3.6 查看慢查询日志

Redis 内置了慢查询日志功能,可以记录执行时间超过阈值的命令:

# 配置慢查询阈值为 1000 微秒(1 毫秒)
127.0.0.1:6379> CONFIG SET slowlog-log-slower-than 1000

# 配置慢查询日志最多保存 128 条
127.0.0.1:6379> CONFIG SET slowlog-max-len 128

# 查看最近的慢查询
127.0.0.1:6379> SLOWLOG GET 5
# 1) 1) (integer) 3          # 日志 ID
#    2) (integer) 1680000000  # 时间戳
#    3) (integer) 3012541     # 执行耗时(微秒)
#    4) 1) "EVAL"             # 命令
#       2) "local start=..."  # 参数
#       3) "0"

# 查看当前慢查询数量
127.0.0.1:6379> SLOWLOG LEN
# 重置慢查询日志
127.0.0.1:6379> SLOWLOG RESET

3.7 用 MONITOR 实时观察命令流

MONITOR 可以实时看到 Redis 收到的每一条命令——这是学习和调试的利器(但生产环境慎用,本身有性能开销):

# 终端 A:开启 MONITOR
127.0.0.1:6379> MONITOR
OK
# 终端 B:执行一些命令
docker exec -it redis-lab-ch01 redis-cli SET key1 val1
docker exec -it redis-lab-ch01 redis-cli GET key1
docker exec -it redis-lab-ch01 redis-cli INCR counter

回到终端 A,你会看到实时输出:

1680000000.123456 [0 127.0.0.1:54321] "SET" "key1" "val1"
1680000000.234567 [0 127.0.0.1:54322] "GET" "key1"
1680000000.345678 [0 127.0.0.1:54323] "INCR" "counter"

MONITOR 的每一行包含时间戳、数据库编号、客户端地址和完整命令。

3.8 探索 Redis 配置文件

查看当前运行配置:

# 查看所有配置
127.0.0.1:6379> CONFIG GET *
# (输出会很长,建议按需检索)

# 查看特定配置
127.0.0.1:6379> CONFIG GET maxmemory
127.0.0.1:6379> CONFIG GET save
127.0.0.1:6379> CONFIG GET appendonly
127.0.0.1:6379> CONFIG GET timeout

# 修改配置(运行时生效,重启后丢失)
127.0.0.1:6379> CONFIG SET maxmemory 256mb
OK

3.9 常见坑

坑1:KEYS * 生产事故。KEYS 命令会遍历整个键空间,复杂度 O(N),100 万个 key 时可能阻塞数秒。应使用 SCAN 替代:

# 危险!
127.0.0.1:6379> KEYS user:*

# 安全替代
127.0.0.1:6379> SCAN 0 MATCH user:* COUNT 100

坑2:无视 OBJECT ENCODING 的陷阱。同一个 SET 命令,短字符串和长字符串的内部编码完全不同:

127.0.0.1:6379> SET short "hi"
OK
127.0.0.1:6379> OBJECT ENCODING short
"embstr"

127.0.0.1:6379> SET long "这是一段超过44字节的字符串,会触发不同编码..."
OK
127.0.0.1:6379> OBJECT ENCODING long
"raw"

如果你不了解编码差异,在做内存估算时会严重低估或高估。

坑3:INFO 输出误读。used_memory_human 显示的是 Redis 当前使用的内存,但不包括内存碎片。真正需要关注的是 used_memory_rss_human(操作系统视角的物理内存)和 mem_fragmentation_ratio(碎片率,等于 used_memory_rss / used_memory)。


4. 项目总结

4.1 本章核心收获

维度核心认知
Redis 定位以内存数据结构为核心、基于事件驱动的高性能服务端系统,远不止是"带 TTL 的 HashMap"
单线程模型核心命令执行路径是单线程串行的,借助 IO 多路复用和纯内存操作达到微秒级延迟
性能关键快不是魔法——紧凑编码+内存访问+无锁设计+IO 多路复用;慢命令是最大杀手
请求链路客户端 → RESP 编码 → epoll 通知 → 命令解析 → 权限校验 → 数据操作 → RESP 响应
Redis 8.x 能力从缓存扩展到消息流、向量检索、字段级过期、ACL v2、Functions、JSON 索引等

4.2 优点与缺点

优点缺点
命令模型简单,学习曲线平缓单线程执行路径害怕慢命令、大 key、复杂脚本
数据结构贴近业务,建模自然内存成本高,需要容量和淘汰策略规划
纯内存访问,常规命令延迟微秒级复杂查询和多表关联不是 Redis 的强项
支持持久化、复制、集群和模块扩展初学者容易忽视编码差异、key 规范和 TTL 策略
原子命令天然线程安全,无锁编程单个命令原子 ≠ 多命令事务原子(需要 Lua 或 MULTI/EXEC)

4.3 适用与不适用场景

适用场景:

  1. 热点数据缓存(商品详情、用户会话、配置信息)
  2. 临时数据存储(验证码、Token、一次性票据)
  3. 高性能计数器(浏览量、点赞数、接口调用次数)
  4. 排行榜与排序(积分榜、热榜、时间线)
  5. 轻量消息队列(异步任务通知、事件传递)
  6. 分布式协调(分布式锁、限流器、开关控制)
  7. 实时状态存储(在线用户、设备状态、会话管理)

不适用场景:

  1. 需要复杂 SQL 查询和关联统计的业务数据——应使用关系数据库
  2. 单条记录超大(>10MB)的数据——会导致网络传输和内存碎片问题
  3. 需要强事务一致性(ACID)的核心交易数据——Redis 不应替代关系数据库
  4. 大量冷数据需要廉价存储——内存成本远高于磁盘

4.4 常见踩坑经验

案例表象根因解法
某电商大促 P99 延迟暴涨商品详情接口从 5ms 涨到 2s运维用 KEYS * 排查问题,阻塞整个 Redis 3 秒禁用 KEYS,改用 SCAN;配置 ACL 禁止危险命令
某社交 App 内存持续上涨Redis 内存从 8GB 涨到 32GB大量计数器 key 未设 TTL,永不过期制定 key 命名规范和 TTL 策略;定期巡检无 TTL key
某支付系统 Redis "幽灵 key"某些 key 超 TTL 后仍能读到主从复制中,从节点用绝对过期时间,删除命令延迟到达校验数据时增加 TTL 二次判断;用 Lua 读写前先检查 TTL

4.5 思考题

  1. 设计题:如果你需要设计一个"最近 7 天每个用户每天发送短信次数"的计数器系统,你会如何设计 Redis key 和过期策略?写出 key 格式和到期逻辑。

  2. 排查题:线上 Redis 的 instantaneous_ops_per_sec 突然从 5 万降到 2 千,但 connected_clients 没有明显变化。请列出至少 5 种可能导致此现象的根因,并说明优先排查哪一项。

(答案提示将在下一章末尾给出。)

下一章预告:第2章——Windows、WSL2 与 Docker 环境搭建。我们将编写一份 Docker Compose 文件,启动带密码、AOF、数据挂载和 RedisInsight 的本地实验环境,为后续所有章节打好基础。

延伸阅读与资源

Redis 8 实战精讲:从 CRUD 到源码,构建高可用缓存系统