核心目标:理解 Redis 解决什么、不解决什么;看懂"客户端 → RESP 协议 → 事件循环 → 数据结构"的命令执行链路;用 redis-py 完成一次可观察、可验证的最小读写,并亲手触发类型错误与连接失败两类异常。
前置知识:能编写基础 Python(类型标注、异常处理、上下文管理器);理解 TCP 客户端-服务器模型和 HTTP/Web 服务的基本生命周期;不要求任何 Redis 使用经验。
验证环境 :Redis 8.10.0(cygwin 移植版,Windows 服务方式运行于
127.0.0.1:6379)、redis-py 8.1.0、Python 3.11.6、Windows 11;所有实验在逻辑 db 15 隔离执行。最后复核日期:2026-08-07。
0. 本篇问题场景:一次 SET 后面究竟发生了什么
python
r = redis.Redis(host="127.0.0.1", port=6379, db=15)
r.set("greeting", "hello from redis-py")
print(r.get("greeting")) # b'hello from redis-py'(默认 bytes 模式,见 4.4)
这段代码看起来像是把一对键值"放进了内存字典"。但它没有碰任何 Python 字典,也没有直接调用 malloc。真正发生的是一条更长的链路:
#mermaid-svg-F1ycMjULnn7YvACP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-F1ycMjULnn7YvACP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-F1ycMjULnn7YvACP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-F1ycMjULnn7YvACP .error-icon{fill:#552222;}#mermaid-svg-F1ycMjULnn7YvACP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-F1ycMjULnn7YvACP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-F1ycMjULnn7YvACP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-F1ycMjULnn7YvACP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-F1ycMjULnn7YvACP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-F1ycMjULnn7YvACP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-F1ycMjULnn7YvACP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-F1ycMjULnn7YvACP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-F1ycMjULnn7YvACP .marker.cross{stroke:#333333;}#mermaid-svg-F1ycMjULnn7YvACP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-F1ycMjULnn7YvACP p{margin:0;}#mermaid-svg-F1ycMjULnn7YvACP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-F1ycMjULnn7YvACP .cluster-label text{fill:#333;}#mermaid-svg-F1ycMjULnn7YvACP .cluster-label span{color:#333;}#mermaid-svg-F1ycMjULnn7YvACP .cluster-label span p{background-color:transparent;}#mermaid-svg-F1ycMjULnn7YvACP .label text,#mermaid-svg-F1ycMjULnn7YvACP span{fill:#333;color:#333;}#mermaid-svg-F1ycMjULnn7YvACP .node rect,#mermaid-svg-F1ycMjULnn7YvACP .node circle,#mermaid-svg-F1ycMjULnn7YvACP .node ellipse,#mermaid-svg-F1ycMjULnn7YvACP .node polygon,#mermaid-svg-F1ycMjULnn7YvACP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-F1ycMjULnn7YvACP .rough-node .label text,#mermaid-svg-F1ycMjULnn7YvACP .node .label text,#mermaid-svg-F1ycMjULnn7YvACP .image-shape .label,#mermaid-svg-F1ycMjULnn7YvACP .icon-shape .label{text-anchor:middle;}#mermaid-svg-F1ycMjULnn7YvACP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-F1ycMjULnn7YvACP .rough-node .label,#mermaid-svg-F1ycMjULnn7YvACP .node .label,#mermaid-svg-F1ycMjULnn7YvACP .image-shape .label,#mermaid-svg-F1ycMjULnn7YvACP .icon-shape .label{text-align:center;}#mermaid-svg-F1ycMjULnn7YvACP .node.clickable{cursor:pointer;}#mermaid-svg-F1ycMjULnn7YvACP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-F1ycMjULnn7YvACP .arrowheadPath{fill:#333333;}#mermaid-svg-F1ycMjULnn7YvACP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-F1ycMjULnn7YvACP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-F1ycMjULnn7YvACP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-F1ycMjULnn7YvACP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-F1ycMjULnn7YvACP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-F1ycMjULnn7YvACP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-F1ycMjULnn7YvACP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-F1ycMjULnn7YvACP .cluster text{fill:#333;}#mermaid-svg-F1ycMjULnn7YvACP .cluster span{color:#333;}#mermaid-svg-F1ycMjULnn7YvACP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-F1ycMjULnn7YvACP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-F1ycMjULnn7YvACP rect.text{fill:none;stroke-width:0;}#mermaid-svg-F1ycMjULnn7YvACP .icon-shape,#mermaid-svg-F1ycMjULnn7YvACP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-F1ycMjULnn7YvACP .icon-shape p,#mermaid-svg-F1ycMjULnn7YvACP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-F1ycMjULnn7YvACP .icon-shape .label rect,#mermaid-svg-F1ycMjULnn7YvACP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-F1ycMjULnn7YvACP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-F1ycMjULnn7YvACP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-F1ycMjULnn7YvACP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} redis-py 客户端
redis.Redis 对象
TCP 连接(Socket)
127.0.0.1:6379
RESP 协议
序列化命令与解析结果
Redis 事件循环
aeEventLoop 单线程
命令分发
SET → t_string.c
内存中的数据结构
RedisObject + SDS
一条 SET 命令从发出到返回 OK,经历了:客户端把命令编码成 RESP 协议字节 → 经 TCP 发送 → 服务端事件循环把读到的请求交给命令处理器 → 在内存数据结构中写入并应答 → 应答经协议编码沿原路返回。r.get("greeting") 再走一遍同样的链路读回数据。
本篇先把这条链路建成心智模型,再完成三件事:
- 说清 Redis 的定位:它是什么、不是什么,为什么"快";
- 完成最小闭环:用 redis-cli 与 redis-py 各做一次可观察的读写;
- 触发两类失败 :类型错误(
WRONGTYPE)与连接失败(ConnectionError/TimeoutError),并理解它们来自链路的哪一环。
1. Redis 是什么,不是什么
1.1 官方定位
Redis 的正式定位是 In-memory data structure store------一个内存中的数据结构服务器。它同时常被用作缓存、消息中间件和数据库的补充层,但"内存"与"数据结构"这两个词才是它的本质:
- 内存:数据主要存放在内存中,因此读写延迟在微秒~亚毫秒级;
- 数据结构:它不只是"key-value 字典",而是按数据类型(String、Hash、List、Set、ZSet 等)组织并提供对应操作命令的服务器。
与常见系统的边界对比:
| 系统 | 数据存储位置 | 持久化 | 核心能力 | 与 Redis 的关系 |
|---|---|---|---|---|
| MySQL / PostgreSQL | 磁盘(页缓存之上) | 强,事务化 | SQL、约束、复杂查询 | Redis 通常是它前面的缓存层,不是替代品 |
| Memcached | 内存 | 无 | 简单的 key-value 缓存 | Redis 是超集:数据结构、持久化、高可用 |
| Kafka / RabbitMQ | 磁盘日志 | 按保留策略 | 消息可靠投递、堆积 | Redis Stream/List 适合轻量队列,不承诺可靠堆积 |
| Redis | 内存 | RDB/AOF(Part 5) | 数据结构命令、原子性、发布订阅 | 本篇主角 |
守住的边界是:Redis 不替代数据库的持久化与事务语义,也不替代消息中间件的可靠投递。它的定位是"把访问最热的、需要低延迟的数据放到内存里,用数据结构命令高效处理"。
1.2 为什么"快"
三个主要因素,缺一不可:
- 内存访问:数据在内存中,没有磁盘寻道;
- 单线程事件循环:所有命令在同一个线程按顺序执行,没有锁竞争、没有上下文切换的开销(详见 2.2);
- 简洁的数据结构与命令模型 :每条命令都对应明确的内存操作,复杂度可预期(
SET/GET是 O(1))。
但"快"是有边界的,本系列后续会反复看到:任何 O(N) 命令、任何阻塞操作(大 key 传输、bgsave fork、主从全量同步)都会让"快"变成"所有命令一起变慢"。单线程是"简单"的代价,也是"公平"的代价------后文会展开。
2. 命令执行链路
2.1 从 TCP 到内存的四层
一次完整的命令往返分四层:
| 层 | 负责什么 | 关键事实 |
|---|---|---|
| 客户端层 | redis-py 把方法调用变成命令 | r.set(k, v) 等价于向服务器发送 SET k v |
| 协议层 | RESP 序列化/反序列化 | 命令和响应都编码成文本帧,见下 |
| 事件循环层 | 读写 socket、分发命令 | 单线程,同一时刻只执行一条命令 |
| 数据结构层 | 按类型执行内存操作 | SET 写入的是 RedisObject + SDS(Part 3 详解) |
RESP(REdis Serialization Protocol)是 Redis 与客户端之间的文本协议。SET greeting hello 在线上实际是:
text
*3\r\n$3\r\nSET\r\n$8\r\ngreeting\r\n$5\r\nhello\r\n
其中 *3 表示后面有 3 个参数,$n 表示后面是一个 n 字节的字符串(greeting 8 字节、hello 5 字节)。redis-py 负责把这串字节拼出来、发出去,再把响应解析成 Python 对象。理解这层的作用:不管用什么语言的客户端,最终都翻译成同一套协议字节 ,所以故障排查时可以脱离客户端直接用 redis-cli 复现。
2.2 单线程事件循环,以及 6.0+ 的"多线程"边界
Redis 服务端是一个事件循环:主线程不断做"读请求 → 执行命令 → 写响应"的循环。因为同一时刻只执行一条命令,所以:
- 不需要锁,代码简单、行为可预测;
- 一条慢命令会阻塞后面所有命令------这是理解 Redis 故障的起点;
- 单线程的瓶颈是 CPU 与网络 I/O,而不是内存带宽。
Redis 6.0 引入了多线程 I/O,但边界必须说清:
主线程(单线程执行命令) I/O 线程池(6.0+) 客户端 B 客户端 A 主线程(单线程执行命令) I/O 线程池(6.0+) 客户端 B 客户端 A #mermaid-svg-Qpb9rc3WS1ZjkSxf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Qpb9rc3WS1ZjkSxf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .error-icon{fill:#552222;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .marker.cross{stroke:#333333;}#mermaid-svg-Qpb9rc3WS1ZjkSxf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Qpb9rc3WS1ZjkSxf p{margin:0;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Qpb9rc3WS1ZjkSxf text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Qpb9rc3WS1ZjkSxf .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Qpb9rc3WS1ZjkSxf #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .sequenceNumber{fill:white;}#mermaid-svg-Qpb9rc3WS1ZjkSxf #sequencenumber{fill:#333;}#mermaid-svg-Qpb9rc3WS1ZjkSxf #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .messageText{fill:#333;stroke:none;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .labelText,#mermaid-svg-Qpb9rc3WS1ZjkSxf .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .loopText,#mermaid-svg-Qpb9rc3WS1ZjkSxf .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Qpb9rc3WS1ZjkSxf .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .noteText,#mermaid-svg-Qpb9rc3WS1ZjkSxf .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .actorPopupMenu{position:absolute;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Qpb9rc3WS1ZjkSxf .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Qpb9rc3WS1ZjkSxf .actor-man circle,#mermaid-svg-Qpb9rc3WS1ZjkSxf line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Qpb9rc3WS1ZjkSxf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 多个线程并行读取网络字节 执行永远是单线程、串行 发送 SET k1 v1 发送 SET k2 v2 排队进入命令队列 依次执行 SET k1 v1 依次执行 SET k2 v2 写入两段响应 响应 响应
多线程只发生在 socket 的读写(系统调用)上,命令解析和执行仍然是单线程串行的。所以"6.0 多线程"不等于"命令并行执行",也没有改变"慢命令阻塞全局"的性质。
3. 安装与验证环境
3.1 官方不支持 Windows
Redis 官方不提供 Windows 版本。Windows 上常见三种方式:
- WSL2 + Linux:官方支持度最高,适合与生产行为一致;
- Docker :
docker run -d --name redis -p 6379:6379 redis:7.4-alpine,本系列计划的主验证方式; - 第三方 Windows 移植版:如 cygwin 编译版、Memurai(兼容 Redis 7.x API)。本机当前采用的就是这一类。
3.2 本机验证环境
本机运行的是 Redis 8.10.0(cygwin 移植版,以 Windows 服务方式运行)。用 redis-cli 核对版本与运行信息:
text
$ redis-cli --version
redis-cli 8.10.0 (git:84b0e0ab)
$ redis-cli info server | grep -E "redis_version|redis_mode|os|tcp_port|hz|config_file"
redis_version:8.10.0
redis_mode:standalone
os:CYGWIN_NT-10.0-26200 3.6.10-1.x86_64 x86_64
tcp_port:6379
hz:10
config_file:/Redis-8.10.0-Windows-x64-cygwin-with-Service/redis.conf
三个字段值得记住:
redis_mode:standalone------单机模式(Part 10/11 会变成 sentinel/cluster);hz:10------服务器每秒执行后台任务的次数(过期扫描等,Part 6 会用到);tcp_port:6379------默认端口,连接串的默认值都指向它。
先做一个连通性测试:
text
$ redis-cli ping
PONG
PING 是 Redis 的心跳命令,返回 PONG 说明服务在运行、端口可达、协议解析正常。它是排障的第一条命令。
如果你使用 Docker/WSL 安装,请以你环境中的
redis_version为准;本系列所有命令以"Redis 7.4+ 行为"为准,8.x 的差异会在每篇"版本与环境差异"一节单独说明。
4. 第一个 redis-py 闭环
4.1 安装
bash
pip install redis
验证安装(注意:redis-py 的导入名是 redis,与包名一致):
text
$ python -c "import redis; print(redis.__version__)"
8.1.0
4.2 连接串
redis-py 的连接串(URL)格式:
text
redis://[user]:[password]@host:port/db
| 片段 | 含义 | 本机默认值 |
|---|---|---|
host |
服务器地址 | 127.0.0.1 |
port |
端口 | 6379 |
/db |
逻辑数据库编号 | 15(本系列实验库,避免污染 db 0) |
逻辑 db(database)是 Redis 的键空间隔离机制:同一进程内有 16 个(默认 databases 16)互不相通的键空间,用 SELECT n 切换。它们是"逻辑"隔离,不是独立进程------所有 db 共享同一份内存和同一套持久化(Part 5 会看到这意味着什么)。本系列实验统一使用 db 15,互不干扰。
4.3 最小闭环代码与真实输出
python
import redis
r = redis.Redis(host="127.0.0.1", port=6379, db=15)
print("ping:", r.ping())
print("set:", r.set("greeting", "hello from redis-py"))
print("get:", r.get("greeting"))
print("ttl:", r.ttl("greeting"))
本机真实输出:
text
ping: True
set: True
get: b'hello from redis-py'
ttl: -1
逐行解读:
r.ping()→True:链路连通;r.set(...)→True:Redis 的SET成功时返回OK,redis-py 转成True。SET的语义是"立即写入内存",此后对该键的读就能看到新值;复杂度 O(1);r.get(...)→b'hello from redis-py':注意返回的是 bytes 而不是 str------见 4.4;r.ttl(...)→-1:-1表示该键没有设置过期时间 (-2表示键不存在)。
4.4 decode_responses:bytes 还是 str
Redis 协议本身不知道"字符串",它只传输字节。redis-py 默认返回 bytes,让调用方自己决定如何解码------这是最不隐式的选择,但业务代码里到处都是 b'...' 很烦。设置 decode_responses=True 后,字符串命令自动按 UTF-8 解码成 str:
python
r_bytes = redis.Redis(host="127.0.0.1", port=6379, db=15, decode_responses=False)
r_str = redis.Redis(host="127.0.0.1", port=6379, db=15, decode_responses=True)
print(repr(r_bytes.get("greeting"))) # b'hello from redis-py'
print(repr(r_str.get("greeting"))) # 'hello from redis-py'
真实输出:
text
b'hello from redis-py'
'hello from redis-py'
本系列业务代码统一使用 decode_responses=True(见 §8 的 get_redis),并在需要字节语义的地方显式切换。
5. 键空间与过期预告
5.1 键的生命周期命令
用 redis-cli 观察键的增删查与过期:
text
$ redis-cli SET hello world
OK
$ redis-cli GET hello
world
$ redis-cli EXISTS hello
1
$ redis-cli TYPE hello
string
$ redis-cli TTL hello
-1
$ redis-cli EXPIRE hello 100
1
$ redis-cli TTL hello
100
$ redis-cli PERSIST hello
1
$ redis-cli TTL hello
-1
$ redis-cli DEL hello
1
$ redis-cli EXISTS hello
0
$ redis-cli DEL hello
0
每个命令回答"改变了什么状态、何时生效、复杂度多少":
| 命令 | 状态变化 | 生效时机 | 复杂度 |
|---|---|---|---|
SET k v |
键存在并绑定值 | 立即 | O(1) |
GET k |
无变化 | 立即读取 | O(1) |
EXISTS k |
无变化 | 立即读取 | O(1) |
EXPIRE k s |
给键挂上倒计时 | 立即开始计时,到点删除 | O(1) |
PERSIST k |
移除过期时间 | 立即 | O(1) |
DEL k |
删除键,返回实际删除的键数量 | 立即 | O(1)(大集合除外,Part 4) |
带过期写入的便捷命令 SETEX(等价于 SET + EXPIRE 两步的原子版本):
text
$ redis-cli SETEX token_abc 60 "tok-123"
OK
$ redis-cli GET token_abc
tok-123
$ redis-cli TTL token_abc
60
过期机制预告 :TTL 到点后 Redis 并非"立刻物理删除",而是惰性删除 + 定期删除的组合策略;过期键还会影响持久化和主从复制。这些在 Part 6 完整展开,本篇只需记住
-1 = 无过期、-2 = 键不存在。
5.2 key 命名规范
键名是 Redis 唯一可检索的入口,命名约定直接影响后续所有设计。本系列采用冒号分层:
text
业务域:对象:属性:标识
例:cart:user:1001 (用户 1001 的购物车,Hash)
例:rank:product:sales (商品销量榜,ZSet)
规则:小写、冒号分隔、从通用到具体。这样 SCAN "cart:user:*" 之类的模式匹配(Part 2 讲,不要用 KEYS)才能高效且安全。
6. 失败实验与根因
两个失败实验各揭示链路的一环。
6.1 类型错误:WRONGTYPE
对 String 键执行 List 命令:
text
$ redis-cli SET str_key "i am a string"
OK
$ redis-cli LPUSH str_key item1
(error) WRONGTYPE Operation against a key holding the wrong kind of value
用 redis-py 触发同样错误:
python
try:
r.lpush("greeting", "item")
except redis.ResponseError as e:
print("ResponseError:", e)
text
ResponseError: WRONGTYPE Operation against a key holding the wrong kind of value
根因 :键的"值"在内存中是以某种数据结构 (RedisObject,Part 3)存储的,LPUSH 要求 List 结构,而 SET 创建的是 String 结构。Redis 不会猜测或自动转换------类型不匹配直接报错。
这揭示了 Redis 的一个重要设计:键值类型在写入时就固定了 ,后续命令必须匹配。这与关系型数据库"列有类型"类似,但发生在服务器端,任何客户端(包括 redis-cli)都无法绕过。所以类型错误通常意味着业务代码的 bug,而不是环境问题------排查方向应该回到写入这条键的代码。
6.2 连接失败:ConnectionError 与 TimeoutError
场景一:主机名解析失败(立即失败):
python
bad = redis.Redis(host="no-such-host.invalid", port=6379, socket_connect_timeout=2)
bad.ping() # 抛异常
text
redis.exceptions.ConnectionError: Error 11001 connecting to no-such-host.invalid:6379.
getaddrinfo failed.
根因 :客户端在建立 TCP 连接前先要 DNS 解析(getaddrinfo),主机名不存在直接失败------失败发生在协议层之前,Redis 服务端甚至没有收到任何请求。
场景二:端口未监听(本机实测为超时):
python
bad = redis.Redis(host="127.0.0.1", port=6399, socket_connect_timeout=2)
bad.ping() # 抛异常
text
redis.exceptions.TimeoutError: Timeout connecting to server
根因 :127.0.0.1:6399 没有服务在监听。在部分 Windows 网络栈/防火墙配置下,这类连接被静默丢弃而不是立即拒绝,于是客户端等到 socket_connect_timeout 超时后抛出 TimeoutError。(在多数 Linux 环境下同样场景会更快收到 Connection refused 而抛 ConnectionError。)
这两类异常的排查含义 :ConnectionError/TimeoutError 说明"链路没通"------先查服务是否在运行、端口是否监听、连接串是否写对;WRONGTYPE 说明"链路通了但用法错了"------回去查写入该键的代码。把这两类问题分开,排障就完成了一半。
7. 版本与环境差异
| 差异点 | 本系列默认 | 你的环境需要注意 |
|---|---|---|
| Redis 版本 | 7.4 LTS 为写作基线 | 本机是 8.10.0(cygwin 移植版);7.4 与 8.x 的命令/配置差异在每篇单独标注 |
| Redis 发行版 | 官方 Linux/Docker | cygwin 移植版的网络栈、后台任务调度行为可能与官方版有细微差异(如 6.2 的超时行为) |
| redis-py | 5.x 起稳定 | 本机是 8.1.0;setex 调用会触发 DeprecationWarning(8.1.0 实测,提示改用 set),建议 r.set(k, v, ex=60),本文所有示例已按新写法 |
| 逻辑 db | 实验统一用 db 15 | 生产环境通常只用 db 0;多 db 共享受限内存与同一持久化 |
| 认证 | 未配置密码 | 生产必须配置 requirepass 并使用连接串密码段 |
8. shop-lab 工程实战:连接封装与测试
从本篇开始,全系列使用统一示例项目 shop-lab ("电商 + 社区"场景,目录 redis/shop-lab/)。Part 1 阶段它只有两个职责:统一连接入口 + 可回归的最小测试。
8.1 连接封装
python
# src/shop_lab/redis_client.py
import os
import redis
DEFAULT_REDIS_URL = "redis://127.0.0.1:6379/15" # 实验库 db 15
_pool_cache = {} # (url, decode_responses) -> ConnectionPool
def get_redis(decode_responses: bool = True, url: str | None = None) -> redis.Redis:
# 连接串在调用时读取环境变量(优先级:环境变量 > 默认值)
url = url or os.environ.get("SHOP_REDIS_URL", DEFAULT_REDIS_URL)
key = (url, decode_responses)
pool = _pool_cache.get(key)
if pool is None:
pool = _pool_cache[key] = redis.ConnectionPool.from_url(
url, decode_responses=decode_responses
)
return redis.Redis(connection_pool=pool)
三个约定:
- 连接串走环境变量
SHOP_REDIS_URL,代码与文档不硬编码密码; - 默认
decode_responses=True,业务层统一 str; - 连接池按
(url, decode_responses)缓存复用。
两个设计要点:连接串在 get_redis() 调用时 读取环境变量(而不是模块导入时固定),测试与部署环境可以随时覆盖;连接池需要显式缓存------redis-py 8.x 的 from_url 每次都会新建连接池 (实测两次 from_url 不是同一池),高频调用不缓存会堆积连接。
8.2 测试
测试全部跑在 db 15,每个用例前清空实验库,保证独立可重复。测试覆盖四组语义:连接与连通性、最小读写、TTL 过期、失败路径。
text
$ cd redis/shop-lab && python -m pytest
................. [100%]
17 passed in 30.04s
17 个用例中与本篇直接相关的几个:
python
def test_wrongtype_raises_response_error(r):
r.set("key", "i am a string")
with pytest.raises(redis.ResponseError) as exc:
r.lpush("key", "item")
assert "WRONGTYPE" in str(exc.value)
def test_dns_failure_raises_connection_error():
bad = redis.Redis(host="no-such-host.invalid", port=6379, socket_connect_timeout=2)
with pytest.raises(redis.ConnectionError):
bad.ping()
def test_unlistened_port_raises_timeout_error():
bad = redis.Redis(host="127.0.0.1", port=6399, socket_connect_timeout=2)
with pytest.raises(redis.exceptions.TimeoutError):
bad.ping()
测试依赖真实 Redis 服务运行(
fakeredis等替身无法验证协议与类型错误行为,本系列集成测试一律打真实服务)。
9. 常见误区
-
"Redis 是多线程的,所以能并行执行命令"
错。6.0+ 的多线程只发生在 socket 读写层,命令解析与执行永远是单线程串行(见 2.2)。
-
"
SET返回 1/True 表示设置成功,返回 0 表示失败"不准确。
SET成功返回OK(redis-py 转True);它没有"0/失败"分支。与SETNX(不存在才写)的语义不要混淆,后者在 Part 9 分布式锁会重点讲。 -
"bytes 返回是 bug"
不是。这是 redis-py 的默认且无歧义的行为,协议层本来就只有字节;
decode_responses=True只是约定性便利,不是修复。 -
"逻辑 db 可以做租户隔离"
不推荐。所有 db 共享内存、同一套持久化与淘汰策略,无法独立管理。生产租户隔离应该用独立实例或集群(Part 11)。
-
"
KEYS *可以拿来查有哪些键"危险。
KEYS是 O(N) 且会阻塞整个单线程服务器,生产是事故源头。Part 2 会给出SCAN替代方案。 -
"连接失败报
ConnectionError就去重启 Redis"先区分阶段:DNS 失败、端口未监听、认证失败、超时,分别对应配置、服务、密码、网络四类问题(见 6.2)。
10. 本篇小结
回到开篇的问题:一次 SET 后面发生了什么?
- redis-py 把方法调用编码成 RESP 协议字节,经 TCP 连接发到 6379 端口;
- 服务器事件循环读到请求,在单线程上执行命令:在内存中创建一个 RedisObject(String 类型、SDS 编码,Part 3 展开);
- 应答沿同一条链路返回,redis-py 解析成
True。
本篇建立的三个基线:
- 定位基线:Redis 是内存数据结构服务器,不是数据库也不是消息中间件的替代品;
- 链路基线:客户端 → RESP → 事件循环(单线程)→ 数据结构;慢命令阻塞全局由此而来;
- 排障基线 :
WRONGTYPE查业务代码,ConnectionError/TimeoutError查链路与环境。
动手验收清单(全部完成才算过关):
- 能画出并解释"命令执行链路图"(客户端 → 协议 → 事件循环 → 数据结构);
- 用 redis-cli 完成
SET/GET/EXPIRE/TTL/PERSIST/DEL一轮操作,并解释TTL的-1/-2含义; - 用 redis-py 完成最小闭环,并能解释
decode_responses的影响; - 亲手触发一次
WRONGTYPE并说出根因; - 亲手触发
ConnectionError与TimeoutError,并能说出两者在链路上的位置差异; -
cd redis/shop-lab && python -m pytest全部通过(17 passed)。
下一篇 Part 2:数据结构与命令体系 将回答:"我有这些访问模式,该选哪种数据结构?"------五种核心结构、命令复杂度表与键设计规范。
11. 官方资料
- Redis Documentation:https://redis.io/docs/
- Redis Commands(命令参考):https://redis.io/docs/latest/commands/
- RESP 协议说明:https://redis.io/docs/latest/develop/reference/protocol-spec/
- redis-py 文档:https://redis-py.readthedocs.io/
- Redis 许可与版本说明(7.4 RSALv2/SSPLv1、8.0 AGPLv3):https://redis.io/legal/