Skip to content

第 1 章 Redis 是什么 & 为什么这么快

学习目标:能用一句话向产品经理解释「Redis 是什么、为什么需要它」;能向面试官完整解释「Redis 为什么这么快」;能用 redis-cli 完成第一次 Hello World,并能用 nc 手动构造 RESP 协议报文。


1.1 缓存的本质:为什么我们需要 Redis

1.1.1 一个生活类比:图书馆借书

想象你在图书馆查资料:

你要查一本书:《三体》

方案 A(每次都跑书库)                  方案 B(先看桌上有没有)
┌─────────────────────────┐           ┌─────────────────────────┐
│  你 ── 去 5 楼书库 ──── │           │  你 ── 看桌上 ─────────  │
│       └ 找书架 ───────  │           │       └ 命中!直接读     │
│         └ 取书 ───────  │           │                          │
│           └ 回座位 ──── │           │  耗时:3 秒              │
│             └ 读书      │           └─────────────────────────┘
│                          │
│  耗时:5 分钟            │           没找到?再去书库拿,
└─────────────────────────┘           顺便放一本到桌上。
  • 5 楼书库 = 数据库(MySQL,磁盘 IO 慢)
  • 桌子 = 缓存(Redis,内存读写快)
  • 常看的书放桌上 = 把热点数据缓存起来

第一次找书会慢,但第二次、第三次……成千上万次访问都瞬间完成——这就是缓存的价值。

1.1.2 速度的鸿沟:内存 vs 磁盘

存储介质的速度差距比你想象的大得多:

存储介质速度对比(访问 1 KB 数据需要的时间):

  CPU 寄存器     ▏< 1 ns
  L1 Cache       ▏~ 1 ns
  L2 Cache       ▎~ 4 ns
  L3 Cache       ▍~ 10 ns
  内存 (DRAM)    ▌~ 100 ns           ← Redis 工作在这里
  SSD            ████ ~ 100,000 ns   (10 万 ns = 0.1 ms)
  机械硬盘       ████████████████ ~ 10,000,000 ns (10 ms)
  网络往返(同机房) ██████████ ~ 500,000 ns

把单位拉平到「秒」级别(×10^7 倍)做类比:
  内存读一次     = 1 秒
  SSD 读一次     = 17 分钟
  机械硬盘读一次  = 28 小时

结论:内存比磁盘快 5 个数量级(10 万倍)。把热点数据放内存里,性能立刻飞起。

1.1.3 Redis 的定位

Redis(REmote DIctionary Server,远程字典服务)是一个基于内存的 KV 数据库

                  ┌────────────────────────┐
                  │      你的 Web 应用       │
                  └────────┬──────────┬────┘
                           │          │
                  优先查 ▼          ▼ 兜底查
                  ┌──────────┐  ┌──────────┐
                  │  Redis   │  │  MySQL   │
                  │ (内存KV) │  │ (磁盘RDB)│
                  └──────────┘  └──────────┘
                  快:~ 0.1 ms   慢:~ 10 ms
                  小:贵的内存    大:便宜的磁盘
                  易丢:断电没    持久:永久保存
维度RedisMySQL
存储介质内存(可选持久化到磁盘)磁盘(带内存 Buffer Pool)
数据模型KV(值可以是 String/List/Hash/Set/ZSet 等)关系表(行 + 列)
查询语言200+ 个内置命令SQL
典型 QPS10 万 ~ 100 万几千 ~ 几万
典型场景缓存、计数器、排行榜、分布式锁、消息队列持久化业务数据(用户、订单、商品)

一句话总结:Redis 不是要替代 MySQL,而是站在 MySQL 前面挡掉绝大部分读请求,让你的系统能扛住高并发。


1.2 Redis 为什么这么快

这是面试 100% 会问的问题。标准答案是三大原因 + 一个加分项

┌────────────────────────────────────────────────────────┐
│              Redis 高性能的「四大金刚」                  │
│                                                         │
│   ① 数据在内存            (硬件层:消除磁盘 IO)        │
│   ② 单线程处理命令         (软件层:无锁、无上下文切换)  │
│   ③ IO 多路复用            (网络层:一个线程管 N 个连接)│
│   ④ 高效的数据结构          (算法层:SDS、跳表、压缩列表)│
└────────────────────────────────────────────────────────┘

1.2.1 原因一:数据在内存

这点最直观,前面已经讲过:内存比磁盘快 10 万倍。

但要注意一个常见误区:

❌ "Redis 因为是 C 写的所以快"

✅ "Redis 快的核心是数据在内存,C 语言只是锦上添花"

1.2.2 原因二:单线程模型

很多人第一次听到「Redis 是单线程」都会震惊:单线程怎么可能快?

单的是什么?

Redis 进程内部其实有多个线程:

┌─────────────────────────────────────────────────┐
│                Redis 进程                        │
│                                                  │
│   ┌──────────────────────────────┐              │
│   │ 主线程(单线程,处理客户端请求)│  ← "单线程"  │
│   │  ─ 接收命令                    │     指的是它  │
│   │  ─ 解析命令                    │              │
│   │  ─ 执行命令(操作数据)         │              │
│   │  ─ 返回结果                    │              │
│   └──────────────────────────────┘              │
│                                                  │
│   ┌────────────┐  ┌────────────┐  ┌──────────┐ │
│   │ BIO 线程    │  │ AOF 刷盘    │  │ 持久化    │ │
│   │ (关闭fd)   │  │ 线程         │  │ 子进程    │ │
│   └────────────┘  └────────────┘  └──────────┘ │
│                                                  │
└─────────────────────────────────────────────────┘

「单线程」专指处理客户端命令的主线程。其他后台任务(关闭文件描述符、AOF 刷盘、生成 RDB 快照)都是另开线程或子进程。

单线程为什么不慢?

对比图:多线程 vs 单线程处理 3 个请求

多线程(假设 2 个工作线程):
  时间 →
  T1: ████ 命令A ████ 命令B
  T2:     ▓▓▓▓ 命令C
              ↑          ↑
              加锁       上下文切换
              竞争       开销

单线程(Redis):
  时间 →
  Main: ████ A ████ B ████ C
              ↑    ↑    ↑
              一个接一个,无锁、无切换

单线程的好处

  • 无锁:所有数据操作天然串行,不需要 mutex/spinlock
  • 无上下文切换:CPU 缓存命中率高
  • 代码简单:不用考虑并发 Bug

单线程的前提:每条命令必须执行得足够快(< 1 微秒级),否则后面的请求都被堵住——这就是为什么 Redis 强调「禁用 KEYS * 等慢命令」。

Redis 6.0 的「多线程」改了什么?

Redis 6.0 之前(纯单线程):
  主线程 ─ 读 socket ─ 解析 ─ 执行 ─ 写 socket
           └─────────────────────────┘
                   全部都是主线程做

Redis 6.0+ (IO 多线程):
  IO 线程 ─ 读 socket ─ 解析  ┐
                              ├─→  主线程:执行命令(仍然单线程!)
  IO 线程 ─ 读 socket ─ 解析  ┘

  IO 线程 ←── 写 socket ←──────── 主线程

⚠️ 关键:Redis 6.0 的多线程只用在网络 IO 阶段(读写 socket、协议解析),命令执行依然是单线程。所以「Redis 单线程」这个说法到现在依然成立。

1.2.3 原因三:IO 多路复用

这是最容易被误解的概念。我们用一个生活类比来理解。

生活类比:餐厅服务员

场景:餐厅有 100 桌客人,要点单。

方案 A(一桌一个服务员)= 多线程
  100 桌 → 雇 100 个服务员
  问题:成本爆炸,服务员之间还可能撞车

方案 B(一个服务员守一桌,按顺序问)= 阻塞 IO
  服务员 → 走到 1 号桌:「您点好了吗?」
                  客人:「还在看……」(服务员傻等 5 分钟)
  问题:1 号桌没好,2~100 号桌全部饿死

方案 C(服务员在中央,谁举手他过去)= IO 多路复用 ✅
  服务员 → 站在中央,盯着所有桌
  3 号桌客人举手 → 过去点单
  7 号桌客人举手 → 过去点单
  ...

  关键:客人「举手」这个动作 = 操作系统的 epoll
  服务员一直问操作系统:「哪些 socket 有数据可读?」
  操作系统直接告诉他:「3、7、25 号有」

Linux 三种多路复用机制对比

┌─────────┬──────────────┬──────────────┬──────────────────┐
│  机制   │   select     │    poll      │      epoll       │
├─────────┼──────────────┼──────────────┼──────────────────┤
│ 上限    │ 1024(FD_SETSIZE)│ 无          │ 无               │
│ 复杂度  │ O(N) 轮询     │ O(N) 轮询     │ O(1) 事件回调     │
│ 数据结构 │ 位图          │ 数组          │ 红黑树 + 就绪链表 │
│ 拷贝    │ 每次拷贝全量FD│ 每次拷贝全量FD│ 注册一次,事件回调│
│ Redis   │ 兜底          │ 备选          │ Linux 默认 ✅     │
└─────────┴──────────────┴──────────────┴──────────────────┘

epoll 工作方式(伪代码)

c
// 1. 创建 epoll 实例
int epfd = epoll_create(1024);

// 2. 注册感兴趣的 socket
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &event);

// 3. 事件循环:阻塞等待事件
while (1) {
    int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
    for (int i = 0; i < n; i++) {
        // 处理已就绪的 socket(不会被某一个 socket 卡住)
        handle_event(events[i]);
    }
}

Redis 的事件循环

                ┌────────────────────────────────┐
                │   Redis 主线程的事件循环          │
                └─────────────┬──────────────────┘

                   ┌──────────▼───────────┐
                   │ epoll_wait()         │ ← 阻塞等待
                   │ "有事件来吗?"        │
                   └──────────┬───────────┘
                              │  有事件
                ┌─────────────┼─────────────┐
                │             │             │
        ┌───────▼─────┐ ┌────▼─────┐ ┌────▼──────┐
        │ 文件事件     │ │ 文件事件  │ │ 时间事件   │
        │ (新连接到达) │ │ (有数据可读)│ │ (定时任务) │
        └───────┬─────┘ └────┬─────┘ └────┬──────┘
                │            │            │
                ▼            ▼            ▼
            accept       read+exec    serverCron
            注册新fd     +write       (过期清理等)
                │            │            │
                └────────────┼────────────┘


                       回到 epoll_wait

1.2.4 原因四:高效的数据结构

Redis 为每种类型都准备了「内存友好 + 时间复杂度低」的数据结构。后面 第 2 章 会详细讲。先剧透一下:

数据类型底层编码关键设计
StringSDS动态扩容、O(1) 取长度
Listquicklist链表 + ziplist,平衡内存与速度
Hashlistpack / hashtable小数据用紧凑列表,大数据用哈希表
Setintset / hashtable全是整数时用有序数组节省内存
ZSet跳表 + hashtable跳表 O(log N) 范围查询 + 哈希 O(1) 查值

1.3 RESP 协议:和 Redis 对话的「黑话」

Redis 客户端和服务端之间用 RESP(REdis Serialization Protocol) 通信。这是一个人类可读的文本协议——你完全可以用手敲。

1.3.1 协议格式

RESP 用第一个字节区分类型:

第一字节 │ 类型              │ 例子
────────┼──────────────────┼────────────────────
   +    │ Simple String    │ +OK\r\n
   -    │ Error            │ -ERR unknown command\r\n
   :    │ Integer          │ :1000\r\n
   $    │ Bulk String      │ $5\r\nhello\r\n
   *    │ Array            │ *2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n

所有行都用 \r\n(CRLF)结尾。

1.3.2 一次 SET name Alice 的完整对话

客户端发送(按 RESP 编码 ["SET", "name", "Alice"]):

  *3\r\n          ← 数组,有 3 个元素
  $3\r\n          ← 第 1 个元素是长度 3 的字符串
  SET\r\n         ← 内容
  $4\r\n          ← 第 2 个元素是长度 4 的字符串
  name\r\n        ← 内容
  $5\r\n          ← 第 3 个元素是长度 5 的字符串
  Alice\r\n       ← 内容

服务端返回:

  +OK\r\n         ← 简单字符串 "OK"

1.3.3 用 nc 手敲 RESP(练习题)

bash
# 安装 nc(已装可跳过)
# Ubuntu: apt install netcat
# CentOS: yum install nc

# 连接到 Redis(注意:要按 Ctrl+V Ctrl+M 输入 \r,或用 printf)
printf '*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$5\r\nAlice\r\n' | nc 127.0.0.1 6379
# 输出:+OK

printf '*2\r\n$3\r\nGET\r\n$4\r\nname\r\n' | nc 127.0.0.1 6379
# 输出:$5
#       Alice

🎉 看,你已经手动「扮演」了一回 Redis 客户端!


1.4 实操:第一次 Hello Redis

1.4.1 用 redis-cli 命令行

bash
# 进入交互式命令行
$ redis-cli
127.0.0.1:6379> PING
PONG

127.0.0.1:6379> SET greeting "Hello, Redis!"
OK

127.0.0.1:6379> GET greeting
"Hello, Redis!"

127.0.0.1:6379> TYPE greeting
string

127.0.0.1:6379> OBJECT ENCODING greeting
"embstr"

127.0.0.1:6379> DEL greeting
(integer) 1

127.0.0.1:6379> EXIT

1.4.2 用 Python 客户端

完整可运行的代码见配套文件 01_intro/code/hello_redis.py。核心片段:

python
import redis

r = redis.Redis(host='127.0.0.1', port=6379, decode_responses=True)

r.set('greeting', 'Hello, Redis!')
print(r.get('greeting'))  # → Hello, Redis!

r.set('counter', 0)
r.incr('counter')
r.incr('counter')
print(r.get('counter'))   # → 2

1.4.3 浏览器可视化演示

打开配套文件 01_intro/demo.html,可以点击按钮一步步看

  • 阻塞 IO vs IO 多路复用的对比动画
  • epoll 事件循环的完整流程
  • RESP 协议报文逐字节解析

1.5 本章小结

┌─────────────────────────────────────────────────────┐
│                    本章核心要点                       │
├─────────────────────────────────────────────────────┤
│                                                      │
│  ① Redis = 内存 KV 数据库 = MySQL 前面的「快桌子」    │
│                                                      │
│  ② Redis 快的「四大金刚」:                            │
│     • 内存                                            │
│     • 单线程(无锁、无切换)                            │
│     • IO 多路复用(epoll)                             │
│     • 高效数据结构                                     │
│                                                      │
│  ③ 「单线程」专指处理命令的主线程;                     │
│     6.0+ 的多线程只优化网络 IO,命令执行依然单线程       │
│                                                      │
│  ④ RESP 协议人类可读,掌握它能更好地理解性能与排障       │
│                                                      │
└─────────────────────────────────────────────────────┘

1.6 面试高频题

Q1:Redis 为什么这么快?

考察点:对 Redis 整体架构的理解深度。

标准答案(按重要性排序):

  1. 内存存储:避免磁盘 IO,是性能的根本保证。
  2. 单线程模型:避免锁竞争和上下文切换开销,CPU 缓存命中率高。
  3. IO 多路复用:基于 epoll 实现「一个线程管 N 个连接」,单机轻松支撑数万连接。
  4. 高效数据结构:SDS、跳表、压缩列表、哈希表等,每种类型都为「内存 + 时间复杂度」做了优化。
  5. 简洁的协议(RESP):解析开销小。

加分项:提一句 Redis 6.0 引入了多线程 IO,进一步利用多核 CPU 的网络处理能力,但命令执行仍然是单线程

易错点:不要只说「因为是 C 语言写的」——这只是次要因素。


Q2:Redis 是单线程的,那为什么还能这么快?

考察点:单线程模型的本质。

标准答案

  1. Redis 的瓶颈通常是网络 IO 和内存带宽,而不是 CPU;
  2. 单线程避免了多线程的锁竞争、上下文切换、CPU 缓存失效等开销;
  3. Redis 操作的绝大部分命令都是 O(1) 或 O(log N),单条命令执行极快(微秒级),单线程足够;
  4. 通过 IO 多路复用(epoll),单线程也能并发处理数万个客户端连接

加分项:提一下「单线程的代价」——必须避免慢命令(如 KEYS *、大 Key 操作),否则会阻塞所有其他请求。


Q3:Redis 6.0 的多线程是真的多线程吗?解决了什么问题?

考察点:对 Redis 演进的了解。

标准答案

  1. 6.0 引入的多线程只用于网络 IO 阶段(读 socket、协议解析、写 socket);
  2. 命令执行依然是单线程,所以不存在并发安全问题,原有的单线程优势保留;
  3. 解决的问题:在万兆网卡 + 高 QPS 场景下,单线程处理网络 IO 成为瓶颈,多线程 IO 能更好利用多核 CPU;
  4. 默认是关闭的,需要在 redis.conf 中配置 io-threads-do-reads yesio-threads 4

易错点:不要说「Redis 6.0 之后变成多线程数据库了」——这是误解。


Q4:什么是 IO 多路复用?select / poll / epoll 有什么区别?

考察点:网络编程基础。

标准答案

IO 多路复用:用一个线程同时监听多个 socket 的状态,哪个 socket 就绪就处理哪个。本质是把「轮询是否有数据」的工作下放给操作系统内核,应用层只在有事件时被唤醒。

维度selectpollepoll
文件描述符上限1024
时间复杂度O(N)O(N)O(1)
数据结构位图链表红黑树 + 就绪链表
内核到用户态拷贝全量全量仅就绪事件
触发方式水平触发水平触发水平触发 + 边缘触发

Redis 的选择

  • Linux 优先用 epoll
  • macOS / BSD 用 kqueue
  • Solaris 用 evport
  • 兜底用 select(封装在 ae.c,对应 ae_epoll.c / ae_kqueue.c 等)

Q5:Redis 是 KV 数据库,那是不是只能存字符串?

考察点:对 Redis 数据类型的认知(钓鱼题)。

标准答案

不是。Redis 的「Key 是字符串,但 Value 可以是 5 种基础类型 + 3 种特殊类型」:

  • 基础类型:String、List、Hash、Set、ZSet(Sorted Set)
  • 特殊类型:Bitmap、HyperLogLog、Geo
  • 新增类型:Stream(5.0+)、Bloom Filter(模块)

每种类型都有针对性的命令和优化的底层编码(这是后面章节的内容)。

加分项:能举一个场景——例如「ZSet 用于排行榜」「Bitmap 用于签到」「HyperLogLog 用于 UV 统计」。


Q6:Redis 和 Memcached 怎么选?

考察点:技术选型能力。

标准答案

维度RedisMemcached
数据结构5+ 种丰富类型仅字符串
持久化支持 RDB/AOF不支持
集群原生 Cluster客户端分片
多线程6.0+ IO 多线程原生多线程
内存效率一般更高(slab 分配)
场景缓存 + 数据结构服务纯字符串缓存

结论:99% 的新项目直接选 Redis;只有「极简纯字符串缓存 + 极致内存效率」的场景才考虑 Memcached。


📌 下一章预告:第 2 章我们会深入 Redis 的五大基础数据类型(String / List / Hash / Set / ZSet),看看它们底层用了什么数据结构、为什么这么设计,以及在生产环境中的最佳实践。

🎬 可视化演示

演示加载缓慢或样式异常?点此在新标签页打开 ↗

💻 示例代码

python
"""
第 1 章配套实战代码 —— Hello Redis

演示内容:
1. 基本连接与 PING 探活
2. String 类型的基础操作(SET/GET/INCR)
3. 用 socket 手动发送 RESP 协议报文,模拟一个最小客户端
4. 简单的性能基准测试,直观感受 Redis 的速度

运行前提:
    1) 本机已有可用的 Redis 服务(127.0.0.1:6379)
    2) pip install redis

运行方式:
    python hello_redis.py
"""

import socket
import time

import redis


HOST = "127.0.0.1"
PORT = 6379


# ----------------------------------------------------------------------
# Demo 1:使用 redis-py 客户端
# ----------------------------------------------------------------------
def demo_basic_client() -> None:
    print("\n" + "=" * 60)
    print("Demo 1: 使用 redis-py 客户端的基本操作")
    print("=" * 60)

    # decode_responses=True 让返回值自动从 bytes 解码为 str
    r = redis.Redis(host=HOST, port=PORT, decode_responses=True)

    # 探活
    print(f"PING -> {r.ping()}")  # True

    # String 类型
    r.set("greeting", "Hello, Redis!")
    print(f"GET greeting -> {r.get('greeting')}")

    # 计数器(INCR 是原子操作)
    r.set("counter", 0)
    r.incr("counter")
    r.incr("counter")
    r.incr("counter")
    print(f"counter after 3 INCR -> {r.get('counter')}")

    # 查看类型与底层编码(验证文档中说的 embstr)
    print(f"TYPE greeting           -> {r.type('greeting')}")
    print(f"OBJECT ENCODING greeting -> {r.object('encoding', 'greeting')}")

    # 设置过期时间(10 秒后自动消失)
    r.set("temp_key", "will expire", ex=10)
    print(f"TTL temp_key -> {r.ttl('temp_key')} 秒")

    # 清理
    r.delete("greeting", "counter", "temp_key")


# ----------------------------------------------------------------------
# Demo 2:手动构造 RESP 协议报文
# ----------------------------------------------------------------------
def encode_resp(*args: str) -> bytes:
    """把命令编码成 RESP 协议字节流。

    示例:encode_resp("SET", "name", "Alice")
        -> b'*3\\r\\n$3\\r\\nSET\\r\\n$4\\r\\nname\\r\\n$5\\r\\nAlice\\r\\n'
    """
    parts: list[bytes] = [f"*{len(args)}\r\n".encode()]
    for a in args:
        ab = a.encode()
        parts.append(f"${len(ab)}\r\n".encode())
        parts.append(ab + b"\r\n")
    return b"".join(parts)


def demo_raw_resp() -> None:
    print("\n" + "=" * 60)
    print("Demo 2: 用 socket 手敲 RESP 协议(模拟最小客户端)")
    print("=" * 60)

    # 建立 TCP 连接
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((HOST, PORT))

    # 发送 SET name Alice
    request = encode_resp("SET", "name", "Alice")
    print("发送字节流:")
    print(f"  {request!r}")
    sock.sendall(request)
    response = sock.recv(1024)
    print(f"收到回复:{response!r}    # +OK\\r\\n 表示成功")

    # 发送 GET name
    request = encode_resp("GET", "name")
    print("\n发送字节流:")
    print(f"  {request!r}")
    sock.sendall(request)
    response = sock.recv(1024)
    print(f"收到回复:{response!r}    # $5\\r\\nAlice\\r\\n")

    # 清理 + 关闭
    sock.sendall(encode_resp("DEL", "name"))
    sock.recv(1024)
    sock.close()


# ----------------------------------------------------------------------
# Demo 3:感受一下 Redis 的速度
# ----------------------------------------------------------------------
def demo_benchmark(n: int = 10_000) -> None:
    print("\n" + "=" * 60)
    print(f"Demo 3: 简单基准测试({n} 次 SET + {n} 次 GET)")
    print("=" * 60)

    r = redis.Redis(host=HOST, port=PORT, decode_responses=True)

    # 1) 单条命令循环
    start = time.perf_counter()
    for i in range(n):
        r.set(f"k:{i}", i)
    set_cost = time.perf_counter() - start

    start = time.perf_counter()
    for i in range(n):
        r.get(f"k:{i}")
    get_cost = time.perf_counter() - start

    print(f"单条命令 {n} 次 SET 耗时 {set_cost * 1000:.2f} ms"
          f"  → QPS ≈ {n / set_cost:,.0f}")
    print(f"单条命令 {n} 次 GET 耗时 {get_cost * 1000:.2f} ms"
          f"  → QPS ≈ {n / get_cost:,.0f}")

    # 2) Pipeline 批处理(提前剧透:第 7 章会详讲)
    start = time.perf_counter()
    pipe = r.pipeline()
    for i in range(n):
        pipe.set(f"k:{i}", i)
    pipe.execute()
    pipe_cost = time.perf_counter() - start
    print(f"Pipeline {n} 次 SET 耗时 {pipe_cost * 1000:.2f} ms"
          f"  → QPS ≈ {n / pipe_cost:,.0f}"
          f"  (提速 {set_cost / pipe_cost:.1f}x)")

    # 清理
    keys = [f"k:{i}" for i in range(n)]
    r.delete(*keys)


# ----------------------------------------------------------------------
if __name__ == "__main__":
    try:
        demo_basic_client()
        demo_raw_resp()
        demo_benchmark()
        print("\n✅ 所有 Demo 执行完毕。"
              "\n   下一步:打开 ../demo.html 在浏览器中可视化探索 IO 多路复用!")
    except redis.ConnectionError as e:
        print(f"❌ 连接 Redis 失败:{e}")
        print(f"   请确认 {HOST}:{PORT} 上有可用的 Redis 服务。")

hello_redis.py ↗