从Redis核心原理到 IPTV 视频项目落地
- 约 21 分钟读完 - 全文 8121 字从Redis核心原理到 IPTV 视频项目落地
先系统梳理 Redis 的核心原理与面试高频知识点,再结合 IPTV(交互式网络电视)视频业务场景,给出可直接运行的 Demo。最后讨论 Redis 为什么不适合长期存储、常见踩坑点,以及 List / Pub/Sub / Stream 三种消息队列实现。
一、Redis 是什么?一句话定位
Redis(Remote Dictionary Server)是一个开源的、基于内存的高性能键值对数据库,支持多种数据结构、持久化、高可用与分布式扩展。
graph LR
A[Redis] --> B[内存存储]
A --> C[数据结构服务器]
A --> D[单线程命令执行]
A --> E[IO 多路复用]
A --> F[持久化 + 高可用]
B --> G[读写毫秒级]
C --> H[String/Hash/List/Set/ZSET/Stream/Bitmap/Geo/HyperLogLog]
F --> I[RDB/AOF/主从/哨兵/Cluster]
与 Memcached 等纯缓存不同,Redis 的核心价值在于丰富的数据结构与原子操作,可以直接在内存中完成排序、计数、限流、去重、队列等复杂逻辑。
二、Redis 为什么快?
面试常问「Redis 为什么快」,通常从四个层面回答:
| 原因 | 说明 |
|---|---|
| 内存操作 | 数据存于内存,无需磁盘寻道,访问速度是 SSD 的千倍以上。 |
| 单线程命令执行 | 避免多线程上下文切换、锁竞争,CPU 缓存命中率更高。 |
| IO 多路复用 | 基于 epoll/kqueue/select 用一个线程处理大量连接。 |
| 高效数据结构 | String 底层 SDS、ZSET 底层跳表+字典、List 底层 quicklist 等。 |
graph TB
subgraph Redis 6/7 线程模型
M[主线程<br/>命令执行 + IO 多路复用]
IO[IO 线程池<br/>仅网络读写]
BG[后台进程<br/>RDB fork / AOF rewrite]
end
C1[Client 1] --> M
C2[Client 2] --> M
C3[Client 3] --> M
M <--> IO
M -.-> BG
注意:Redis 6/7 引入的多线程只用于网络 IO 读写,命令执行依旧是单线程。所以单条慢命令仍会阻塞整个实例。
三、Redis 数据类型与使用场景
3.1 五种基本数据类型
| 类型 | 底层结构 | 典型场景 |
|---|---|---|
| String | SDS(简单动态字符串) | 缓存、计数器、分布式锁、Session |
| Hash | ziplist / hashtable | 对象缓存、用户信息、EPG 节目单 |
| List | quicklist(ziplist + 双向链表) | 消息队列、最新 N 条记录 |
| Set | intset / hashtable | 去重、共同关注、抽奖 |
| ZSET | ziplist / skiplist + dict | 排行榜、延时队列、范围查询 |
注:表中 ziplist 在 Redis 7 起已被 listpack 替代(动机见 §4.2),配置项改名
*-max-listpack-*,旧名仍兼容。
3.2 三种特殊数据类型
| 类型 | 说明 | 场景 |
|---|---|---|
| Bitmap | 位数组,按位操作 | 用户签到、在线状态、UV 统计 |
| HyperLogLog | 基数估计算法 | 海量 UV 去重计数,误差约 0.81% |
| Geo | 地理位置坐标 | 附近的人、POI 搜索 |
3.3 String:最常用却被低估
String 不仅能存字符串,还能存整数、浮点数、二进制数据。常用命令:
SET key value EX 3600
GET key
INCR view_count
INCRBY view_count 10
SETNX lock:order:123 true EX 30 # 分布式锁核心命令SDS 的优势(相对 C 字符串):
- O(1) 获取长度。
- 二进制安全,可存图片、序列化数据。
- 预分配空间,减少内存重分配次数。
- 杜绝缓冲区溢出。
3.4 Hash:对象缓存首选
Hash 适合存「一组字段」的对象,比 JSON String 更节省内存、更新更灵活。
HSET user:1001 name "张三" age 30 city "北京"
HGET user:1001 name
HGETALL user:1001
HINCRBY user:1001 age 13.5 ZSET:排行榜的王牌
ZSET 底层同时维护「跳表」和「字典」:
- 跳表(skiplist):支持 O(log N) 的范围查询和排序。
- 字典(dict):支持 O(1) 按 member 查 score。
ZADD hot:tv 1000 "庆余年"
ZADD hot:tv 800 "狂飙"
ZINCRBY hot:tv 50 "狂飙"
ZREVRANGE hot:tv 0 9 WITHSCORES
ZREVRANK hot:tv "狂飙"3.6 List 与 Stream:队列双雄
List 适合简单队列,Stream 适合生产级消息队列,后面第十四部分会详细对比。
3.7 Bitmap 与 HyperLogLog:海量 UV 统计的利器
Bitmap 为什么能天然去重?
Bitmap 本质是一个超长 bit 数组,每个 bit 只能是 0 或 1。把 user_id 作为偏移量(offset),用户访问一次就把对应位置 1:
- 天然去重:同一用户重复访问,置的都是同一个 bit,无需额外判断。
- 极致省内存:1 亿用户只需 1 亿 bit ≈ 12MB;而 Set 存 1 亿个 ID 要占用数 GB。
- 支持位运算:
BITOP AND/OR可以直接算多日留存(今天访问 AND 昨天也访问)。
SETBIT uv:20210928 10086 1 # 用户 10086 访问
SETBIT uv:20210928 10086 1 # 重复访问,结果不变
BITCOUNT uv:20210928 # 直接得出 UV 数局限:要求 user_id 是连续整数,ID 稀疏时浪费内存。
HyperLogLog 为什么 12KB 能数一亿?
HyperLogLog 基于概率估计算法:把每个元素 hash 成均匀分布的随机数,观察 hash 值二进制串中前导零的最大个数。
- 类比抛硬币:想连续抛出 N 次正面,平均要抛 2^N 次。反过来,如果见过最长连续 N 次正面,就估计大约抛了 2^N 次——前导零越多,说明见过的不同元素越多。
- 单次估计误差大,所以内部用 16384 个分桶(寄存器,每个 6 bit) 分别记录,再取调和平均数消除偶然性,最终误差稳定在 0.81% 左右。
- 内存恒定 约 12KB,与元素个数无关:统计 1 亿 UV 还是 10 亿 UV 都是 12KB。
PFADD uv:20210928 "user:10086"
PFADD uv:20210928 "user:10086" # 重复添加不影响计数
PFCOUNT uv:20210928 # 估算 UV局限:有误差、不可逆(不能取出具体是哪些用户)。
怎么选:
| 需求 | 推荐 |
|---|---|
| 精确计数、算多日留存(交并集)、ID 是连续整数 | Bitmap |
| 只要近似 UV、数据量极大、ID 是任意字符串 | HyperLogLog |
四、Redis 底层数据结构速览
graph TD
A[Redis 数据类型] --> B[String]
A --> C[Hash]
A --> D[List]
A --> E[Set]
A --> F[ZSET]
B --> G[SDS]
C --> H[ziplist]
C --> I[hashtable]
D --> J[quicklist]
E --> K[intset]
E --> L[hashtable]
F --> M[ziplist]
F --> N[skiplist + dict]
| 底层结构 | 特点 |
|---|---|
| SDS | 带长度、预分配、二进制安全。 |
| ziplist | 紧凑列表,小数据量时省内存;大数据量时改为 hashtable/skiplist。 |
| listpack | ziplist 的修正版(Redis 7+):entry 只记自身长度,消灭连锁更新。 |
| quicklist | List 的底层,由多个紧凑块(Redis 7 前是 ziplist,之后是 listpack)节点组成的双向链表。 |
| hashtable | 标准哈希表,渐进式 rehash 避免一次性阻塞。 |
| intset | Set 在元素全为整数且数量少时的紧凑实现。 |
| skiplist | 多层索引链表,实现 ZSET 的有序范围查询。 |
下面把 5 种核心结构「画」出来。
4.1 SDS(Simple Dynamic String)
Redis 没有直接用 C 字符串,而是包了 SDS:
flowchart LR
L["len<br/>已用长度(4B)<br/>= 5"] --- A["alloc<br/>总容量<br/>= 8"] --- F["flags<br/>编码位<br/>= 0"] --- B["buf[]<br/>实际字符/二进制数据<br/>H e l l o \0 ..."]
N1["O(1) 取长度"] -.-> L
N2["末尾保留 \0<br/>兼容 C 字符串函数"] -.-> B
len:已用长度,取长度O(1)。alloc:分配的总空间;free = alloc - len可减少内存重分配。- 二进制安全:内容中间可以有
\0,长度由len决定。 - 预分配:修改时若
free足够就不realloc;不足时按 2 倍或 1MB 上限预分配。
4.2 ziplist(压缩列表)
ziplist 是一块连续内存,把所有 entry 紧凑排列:
flowchart LR
A["zlbytes<br/>总字节数<br/>4B"] --- B["zltail<br/>尾偏移量<br/>4B"] --- C["zllen<br/>节点数<br/>2B"] --- D["entry1<br/>变长字节"] --- E["entry2<br/>变长字节"] --- F["..."] --- G["zlend<br/>0xFF<br/>1B"]
每个 entry 的内部结构:
flowchart LR
A["prevlen<br/>前节点长度<br/>1B 或 5B"] --- B["encoding<br/>编码/长度类型<br/>1B 或更多"] --- C["content<br/>实际数据<br/>变长"]
- 连续内存、无额外指针,所以省内存。
- 插入/删除需要整体
memmove,因此只用于小数据量。 Hash/List/ZSET元素少且单元素短时先用 ziplist,超过阈值升级为 hashtable/skiplist。
listpack(Redis 7 的替代者):ziplist 的致命伤就藏在 prevlen 里——每个 entry 要记录前一个 entry 的长度:前 entry 小于 254 字节时 prevlen 占 1B,达到 254 字节时占 5B。于是一个 entry 从 253 涨到 254 字节,会让后面每个 entry 的 prevlen 从 1B 膨胀到 5B,膨胀又可能触发下一个 entry 继续膨胀——连锁更新,最坏 O(N²)。
listpack 的修法:每个 entry 只记录自己的总长度,且存在 entry 末尾,entry 之间互不引用——改任何一个 entry 都不会波及邻居,连锁更新从根上消失。Redis 7 起 Hash/List/ZSET 的小对象编码全部换成 listpack,配置项同步改名(hash-max-listpack-entries 等,旧的 *-ziplist-* 名仍兼容)。面试回答「ziplist 和 listpack 的区别」,核心就是这一句:ziplist 记前驱长度所以连锁更新,listpack 记自身长度所以免疫。
4.3 quicklist(快速列表)
Redis 3.2 后,List 的底层是 quicklist = 双向链表 + ziplist:
flowchart LR
H["head"] --> A["ziplist A<br/>[a|b|c]<br/>count=3<br/>sz=..."] <--> B["ziplist B<br/>[d|e|f|g]<br/>count=4<br/>sz=..."] <--> C["..."]
N["每个节点是一个 ziplist<br/>节点之间用 prev/next 指针连接"] -.-> A
quicklistNode 结构:
flowchart LR
A["prev<br/>指针"] --- B["next<br/>指针"] --- C["zl<br/>ziplist"] --- D["sz<br/>字节数"] --- E["count<br/>元素个数"] --- F["encoding<br/>是否压缩"]
- 结合了双向链表「插入删除快」和 ziplist「省内存」的优点。
- 每个节点只存少量元素,避免单个 ziplist 太大导致
memmove开销。 - Redis 7 起节点从 ziplist 换成 listpack(动机见 §4.2),但 quicklist 的「链表 + 紧凑块」思想不变。
4.4 intset(整数集合)
Set 在元素全为整数且数量少时使用 intset,否则升级为 hashtable:
flowchart LR
A["encoding<br/>编码类型<br/>2B/4B/8B"] --- B["length<br/>元素个数<br/>4B"] --- C["contents[]<br/>有序整数数组<br/>[1] [3] [5] [7] [9] ..."]
- 有序数组,支持二分查找
O(log N)。 - 所有元素用同一 encoding;加入更大范围整数时可能整体升级(如 int16 → int32),升级是
O(N)。 - 无重复元素(Set 语义)。
4.5 skiplist(跳表)
ZSET 同时维护 skiplist + dict:
skiplist:按 score 排序,支持范围查询 / 排名O(log N)。dict:按 member 查 score,O(1)。
普通有序链表只能顺序扫描,跳表加了多层「快速通道」:
flowchart LR
subgraph L3["level 3"]
A3["1"] --> B3["9"] --> C3["15"]
end
subgraph L2["level 2"]
A2["1"] --> B2["5"] --> C2["9"] --> D2["13"] --> E2["15"]
end
subgraph L1["level 1"]
A1["1"] --> B1["3"] --> C1["5"] --> D1["7"] --> E1["9"] --> F1["11"] --> G1["13"] --> H1["15"]
end
A3 --- A2 --- A1
B3 --- C2 --- E1
C3 --- E2 --- H1
HEAD["头节点"] -.-> A1
H1 -.-> TAIL["尾节点"]
跳表节点结构:
flowchart LR
A["member<br/>成员字符串<br/>(sds)"] --- B["score<br/>分数<br/>(double)"] --- C["forward[] 数组<br/>[L1指针, L2指针, L3指针, ...]<br/>最高层数 = 随机 1~32"]
- 概率平衡:不用像红黑树那样做复杂旋转。
- 查找
score=11时,先在高层大步跳跃,再逐层下降,平均O(log N)。 - 配合 dict 实现
ZSCORE的O(1)查询。
五、Redis 持久化:RDB 与 AOF
Redis 提供两种持久化方式,各有优劣。
5.1 RDB(Redis Database)
在指定时间间隔内将内存数据集快照写入磁盘。
save 900 1 # 900 秒内至少 1 次修改则触发
save 300 10 # 300 秒内至少 10 次修改则触发
save 60 10000 # 60 秒内至少 10000 次修改则触发优点:
- 文件紧凑,适合备份和灾难恢复。
- 恢复速度快。
缺点:
- 两次快照之间数据可能丢失。
bgsave通过fork()子进程生成,大数据集时可能阻塞主线程。
5.2 AOF(Append Only File)
将每次写操作命令追加到日志文件。
appendonly yes
appendfsync everysec # 每秒刷盘一次(推荐)
# appendfsync always # 每次写都刷盘,最安全但最慢
# appendfsync no # 由操作系统决定,最快但不安全优点:
- 数据安全性更高,默认最多丢失 1 秒数据。
缺点:
- 文件体积大。
- 恢复速度慢。
- AOF 重写(rewrite)时会占用额外资源。
5.3 混合持久化(Redis 4.0+)
AOF 重写时先写入 RDB 全量数据,再追加增量 AOF,兼顾恢复速度和数据安全。
aof-use-rdb-preamble yesgraph LR
A[持久化选择] --> B[RDB]
A --> C[AOF]
A --> D[混合持久化]
B --> E[适合备份/快速恢复]
C --> F[适合低丢失容忍]
D --> G[生产推荐]
八股重点:Redis 的持久化是「灾难恢复手段」,不是「主存储方案」。
六、过期删除与内存淘汰
6.1 过期删除策略
Redis 对过期 Key 采用两种策略结合:
| 策略 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 惰性删除 | 访问 Key 时检查是否过期,过期则删除。 | CPU 开销小 | 过期 Key 可能长期占用内存 |
| 定期删除 | 每隔一段时间随机抽查部分 Key 删除。 | 平衡内存与 CPU | 仍可能有漏网之鱼 |
6.2 内存淘汰策略
当内存达到 maxmemory 时,根据 maxmemory-policy 决定如何淘汰:
| 策略 | 说明 |
|---|---|
noeviction |
不淘汰,写操作直接报错。 |
allkeys-lru |
所有 Key 中淘汰最近最少使用。 |
volatile-lru |
只在设置了 TTL 的 Key 中淘汰最近最少使用。 |
allkeys-random |
所有 Key 中随机淘汰。 |
volatile-random |
只在设置了 TTL 的 Key 中随机淘汰。 |
volatile-ttl |
淘汰即将过期的 Key。 |
allkeys-lfu |
所有 Key 中淘汰使用频率最低的。 |
volatile-lfu |
只在设置了 TTL 的 Key 中淘汰使用频率最低的。 |
生产建议:
- 必须设置
maxmemory。 - 缓存场景推荐
allkeys-lru或allkeys-lfu。 - 有冷热区分时可用
volatile-lru,但必须给 Key 设置 TTL。
七、Redis 高可用架构
7.1 主从复制(Replication)
一主多从,读流量分摊到从节点。
graph LR
M[Master<br/>写 + 读] -->|全量/增量同步| S1[Slave 1<br/>读]
M --> S2[Slave 2<br/>读]
M --> S3[Slave 3<br/>读]
复制过程:
- Slave 发送
PSYNC runId offset。 - 如果 runId 一致且 offset 在复制积压缓冲区中,则增量同步。
- 否则全量同步:Master 执行
bgsave生成 RDB,发送给 Slave。
复制延迟问题:
- 主从异步复制必然存在延迟。
- 读从库时可能读到旧数据,需要业务容忍或强制走主库。
7.2 哨兵(Sentinel)
实现自动故障转移,解决主节点宕机问题。
graph TB
subgraph Sentinel 集群
Se1[Sentinel 1]
Se2[Sentinel 2]
Se3[Sentinel 3]
end
M[Master] --> S1[Slave 1]
M --> S2[Slave 2]
Se1 -->|监控| M
Se2 -->|监控| M
Se3 -->|监控| M
Se1 -.-> Se2
Se2 -.-> Se3
核心机制:
- 主观下线(SDOWN):单个 Sentinel 认为 Master 不可用。
- 客观下线(ODOWN):多数 Sentinel 达成一致。
- Leader 选举:Sentinel 之间通过 Raft 算法选举出执行故障转移的 Leader。
- 故障转移:选一个 Slave 提升为 Master,通知其他 Slave 和客户端。
7.3 Cluster 集群
Redis Cluster 采用无中心架构,数据按槽位分布。
graph TB
subgraph Redis Cluster
N1[Node 1<br/>0-5460]
N2[Node 2<br/>5461-10922]
N3[Node 3<br/>10923-16383]
end
N1 <--> N2
N2 <--> N3
N1 <--> N3
关键概念:
- 集群共有 16384 个槽(slot)。
- 每个 Key 通过
CRC16(key) % 16384计算所属槽。 - 每个 Master 负责一部分槽,可配置 Slave 做高可用。
- 客户端访问错误节点时,会收到
MOVED或ASK重定向。
MOVED vs ASK:
- MOVED:槽已经永久迁移到目标节点。
- ASK:槽正在迁移中,先访问目标节点一次。
八、事务与 Lua 脚本
8.1 事务(MULTI/EXEC/DISCARD/WATCH)
MULTI
SET a 1
INCR b
EXEC特点:
- 命令按顺序入队,EXEC 时一次性执行。
- 执行期间不会被其他客户端打断。
- 但没有回滚机制:如果某条命令语法错误,整个事务不执行;如果运行时错误(如 INCR 字符串),后续命令仍会执行。
8.2 Lua 脚本
EVAL "return redis.call('incr', KEYS[1])" 1 counter特点:
- 整个脚本作为一个整体在单线程中原子执行。
- 适用于需要多个命令且要保证原子性的场景,如秒杀、分布式锁、限流。
- 集群模式下所有 Key 必须落在同一 slot,否则需要加 hash tag,如
{user}:1001:profile。
九、缓存经典问题
9.1 缓存穿透
问题:查询不存在的数据,缓存不命中,请求直接打到数据库。
解法:
- 缓存空值(设置短 TTL)。
- 使用布隆过滤器预先判断 Key 是否存在。
9.2 缓存击穿
问题:热点 Key 突然过期,大量请求同时打到数据库。
解法:
- 热点 Key 设置永不过期,异步更新。
- 加互斥锁(SETNX),只允许一个线程重建缓存。
9.3 缓存雪崩
问题:大量 Key 同时过期,或 Redis 宕机,数据库压力陡增。
解法:
- 给 TTL 加随机值,避免同时失效。
- 多级缓存(本地 + Redis)。
- 熔断降级。
9.4 缓存与数据库一致性
常见策略:
| 策略 | 说明 | 问题 |
|---|---|---|
| Cache Aside | 读:先读缓存,未命中读 DB 并写回缓存;写:先更新 DB,再删缓存。 | 短暂不一致 |
| Read/Write Through | 由缓存层统一维护 DB 和缓存。 | 实现复杂 |
| Write Behind | 先写缓存,异步写 DB。 | 可能丢数据 |
最常用的是 Cache Aside,配合「先更新数据库,再删除缓存」+ 延时双删 + 消息队列补偿,能在大多数场景下保证最终一致。
十、Redis 在 IPTV 系统中的角色
IPTV 系统的特点是:高并发读、强实时性、热点集中、写流量脉冲式。用户在换台、看 EPG、播放点播、抢购付费节目时,系统需要在毫秒级返回结果。
flowchart TB
subgraph 用户层
A[机顶盒 / 手机 / Web]
end
subgraph 接入层
B[CDN / 边缘节点]
C[API Gateway]
end
subgraph 应用层
D[鉴权服务]
E[EPG 服务]
F[播放调度服务]
G[订单 / 秒杀服务]
end
subgraph 数据层
H[(MySQL / 长视频元信息)]
I[(对象存储 OSS)]
J[(Redis 缓存 + 实时计算)]
end
A --> B --> C
C --> D & E & F & G
D --> J
E --> J
F --> J
G --> J
E --> H
F --> I
G --> H
J -.-> H
| 场景 | Redis 数据结构 | 作用 |
|---|---|---|
| 用户会话 / Token | String + EXPIRE | 毫秒级鉴权 |
| EPG 节目单缓存 | Hash / JSON String | 减少数据库查询 |
| 热播榜单 / 推荐位 | Sorted Set (ZSET) | 实时排序 |
| 播放限速 / 防刷 | Lua + INCR + EXPIRE | 滑动窗口限流 |
| 秒杀 / 付费解锁 | SETNX + DECR + Lua | 库存扣减 |
| 消息通知 / 任务队列 | List / Stream | 异步解耦 |
十一、IPTV 项目 Demo
11.1 用户会话缓存
import redis
import uuid
import time
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
def login(user_id: str, device_id: str, ttl: int = 3600) -> str:
token = str(uuid.uuid4())
key = f"session:{token}"
r.hset(key, mapping={
"user_id": user_id,
"device_id": device_id,
"login_at": str(time.time())
})
r.expire(key, ttl)
return token
def auth(token: str) -> dict | None:
return r.hgetall(f"session:{token}")11.2 EPG 节目单缓存
import json
from datetime import datetime
def cache_epg(channel_id: str, programs: list[dict]):
key = f"epg:{channel_id}:{datetime.today().strftime('%Y%m%d')}"
pipe = r.pipeline()
for p in programs:
pipe.hset(key, p['start_time'], json.dumps(p))
pipe.expire(key, 86400 * 2)
pipe.execute()
def get_program(channel_id: str, start_time: str) -> dict | None:
data = r.hget(f"epg:{channel_id}:{datetime.today().strftime('%Y%m%d')}", start_time)
return json.loads(data) if data else None11.3 热播排行榜
def record_view(program_id: str, score: float = 1.0):
r.zincrby("hot:tv:daily", score, program_id)
def top_n(n: int = 10) -> list[tuple[str, float]]:
return r.zrevrange("hot:tv:daily", 0, n - 1, withscores=True)11.4 滑动窗口限流
LIMIT_SCRIPT = """
local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local min = now - window
redis.call('zremrangebyscore', key, '-inf', min)
local count = redis.call('zcard', key)
if count < limit then
redis.call('zadd', key, now, now)
redis.call('expire', key, math.ceil(window / 1000))
return 1
else
return 0
end
"""
limiter = r.register_script(LIMIT_SCRIPT)
def allow(user_id: str, max_qps: int = 10, window_ms: int = 1000) -> bool:
key = f"rate_limit:{user_id}"
now = int(time.time() * 1000)
return bool(limiter(keys=[key], args=[window_ms, max_qps, now]))11.5 秒杀 / 限时解锁
SECKILL_SCRIPT = """
local key = KEYS[1]
local user_id = ARGV[1]
local stock = tonumber(redis.call('get', key) or '0')
if stock <= 0 then
return -1
end
if redis.call('sismember', key .. ':users', user_id) == 1 then
return 0
end
redis.call('decr', key)
redis.call('sadd', key .. ':users', user_id)
return 1
"""
seckill = r.register_script(SECKILL_SCRIPT)
def try_buy(activity_id: str, user_id: str, total_stock: int) -> int:
key = f"seckill:{activity_id}:stock"
r.setnx(key, total_stock)
return int(seckill(keys=[key], args=[user_id]))十二、为什么 Redis 不适合做长期存储?
12.1 持久化不是主存储
RDB 会丢数据,AOF 文件大且恢复慢。Redis 的持久化定位是「灾难恢复」,不是「可靠主存」。
12.2 内存成本过高
内存价格是 SSD 的 10-50 倍。IPTV 的媒资、播放记录、订单流水动辄几十 TB,全放 Redis 不现实。
12.3 单线程执行风险
大数据集下,一个 KEYS *、HGETALL 或慢查询可能阻塞整个实例。
12.4 缺乏复杂查询能力
Redis 不支持 SQL、JOIN、二级索引。长期业务数据需要分析统计时,应使用 MySQL / PostgreSQL / ClickHouse。
结论:
Redis 适合「热数据 + 临时状态 + 实时计算」,价值在于速度;长期存储应交给磁盘数据库 + 对象存储。
十三、Redis 容易踩坑的地方
| 坑 | 说明 | 解法 |
|---|---|---|
| 大 Key | String > 10MB,集合元素百万级 | 拆分、分片、UNLINK 异步删除 |
| 热 Key | 单个 Key 被高频访问 | 本地缓存、Key 分片、Cluster |
| 缓存穿透/击穿/雪崩 | 缓存失效导致 DB 被打挂 | 布隆过滤器、互斥锁、随机 TTL |
| 未设置 maxmemory | 内存打满后写操作报错 | 设置 maxmemory 和淘汰策略 |
| 误用 KEYS | KEYS * 扫描全库阻塞主线程 |
生产禁用,改用 SCAN |
| Lua 跨 slot | Cluster 下脚本 Key 不在同一 slot | 使用 hash tag {user}:1001:xxx |
| 事务没有回滚 | EXEC 后某命令失败不会撤销前面命令 | 业务层保证幂等,或用 Lua |
| 主从延迟 | 读从库可能读到旧数据 | 对一致性要求高的读走主库 |
十四、Redis 做消息队列的方法
14.1 List:轻量队列
flowchart LR
P[Producer] -->|LPUSH queue:transcode task| R[(Redis List)]
C[Consumer] -->|BRPOP queue:transcode 5| R
C --> S[执行转码]
r.lpush("queue:transcode", json.dumps({"video_id": "v123"}))
_, task = r.brpop("queue:transcode", timeout=5)优点:简单、阻塞读。 缺点:无 ACK、无多消费者组、无消息回溯。
14.2 Pub/Sub:实时广播
r.publish("live:start", json.dumps({"channel_id": "cctv1"}))优点:实时、多订阅者。 缺点:不持久化,消费者断线期间消息丢失。
14.3 Stream:生产级队列
r.xadd("mq:events", {"event": "video:play", "user_id": "u10086"})
r.xgroup_create("mq:events", "group_log", id="0", mkstream=True)
messages = r.xreadgroup(
groupname="group_log",
consumername="worker_1",
streams={"mq:events": ">"},
count=10,
block=5000
)
for stream, entries in messages:
for msg_id, fields in entries:
process(fields)
r.xack("mq:events", "group_log", msg_id)优点:支持消费者组、ACK、消息回溯。
| 方式 | 推荐场景 |
|---|---|
| List | 简单任务队列 |
| Pub/Sub | 实时广播 |
| Stream | 生产级消息队列 |
十五、总结
《Redis in Action》的核心思想可以概括为一句话:把 Redis 当成内存里的数据结构工具箱,而不是数据库。
Redis 适合:
- 缓存加速(会话、配置、热点数据)。
- 实时计算(排行榜、计数器、限流、秒杀)。
- 异步解耦(List / Pub/Sub / Stream)。
Redis 不适合:
- 长期持久化存储。
- 复杂查询与大数据分析。
- 超大数据集主存储。
避免踩坑的关键是:控制 Key 大小、设置合理 TTL、配置淘汰策略、慎用阻塞命令、理解持久化边界。Redis 用好了是神兵利器,用错了就是生产事故的导火索。