mobile wallpaper 1
mobile wallpaper 2
mobile wallpaper 3
mobile wallpaper 4
5607 字
15 分钟
一篇文章掌握 Redis:从五种数据结构到生产避坑
2026-09-01

Redis 面试必问,项目必用,但很多人的理解停在「缓存、快、单线程」六个字上。真到了线上排查大 key 拖垮集群、缓存雪崩打穿数据库的时候,才发现基础没打牢。

这篇分两部分:上篇从零开始,把 Redis 是什么、为什么快、五种数据结构怎么用讲清楚,读完能上手写业务;下篇进入 Pipeline、Lua 脚本、缓存三兄弟、集群架构和生产踩坑,读完能做选型、能排查问题。

Part 1:快速上手#

Redis 凭什么快:单线程 + 内存 + IO 多路复用#

Redis 是一个基于内存的键值数据库。三个设计决策让它快到可以做缓存层:

纯内存操作。 数据在内存里读写,不走磁盘 IO。一次内存随机读约 100ns,一次磁盘随机读约 10ms——差了 5 个数量级。

单线程执行命令。 Redis 用一个主线程串行执行所有命令。没有锁竞争、没有上下文切换、没有并发安全问题,CPU 缓存命中率也高。听起来反直觉——单线程怎么扛得住高并发?因为瓶颈不在 CPU,在网络 IO。内存操作本身就是纳秒级的,一个核跑满每秒能处理十几万条命令,绝大部分场景 CPU 根本不是瓶颈。

IO 多路复用。 Redis 用 epoll(Linux)/ kqueue(macOS)在一个线程里同时监听成千上万个客户端连接。哪个连接的数据到了就处理哪个,不用为每个连接开一个线程。

graph LR C1[客户端 1] --> EP[epoll 事件循环] C2[客户端 2] --> EP C3[客户端 N] --> EP EP -->|就绪的请求| T[单线程命令执行] T --> M[(内存)]

一个常见误解:Redis 不是完全单线程的。 Redis 4.0 开始,UNLINK(异步删大 key)、FLUSHDB ASYNC 等操作由后台线程执行;Redis 6.0 引入多线程 IO,让网络读写(解析请求、发送响应)可以由多个线程并行处理。但命令执行本身——读写数据的那一步——始终是单线程的,这保证了操作的原子性。

五种核心数据结构和典型场景#

Redis 不只是 SET key value。五种数据结构各有适用场景,选对结构是写好 Redis 代码的第一步。

String:缓存和计数器#

最基础的类型,值可以是字符串、整数或二进制数据,最大 512MB。

# 缓存:设置带过期时间的值
SET user:1001:name "Alice" EX 3600 # 1 小时过期
# 计数器:原子递增
SET article:2048:views 0
INCR article:2048:views # 返回 1
INCR article:2048:views # 返回 2
INCRBY article:2048:views 10 # 返回 12
# 分布式锁的基础
SET lock:order:1001 "uuid-abc" NX EX 30
# NX = 不存在才设置,EX = 过期秒数

INCR 是原子操作——单线程执行,不存在两个请求同时读到旧值再各自加 1 的问题。这是 Redis 做计数器比数据库 UPDATE views = views + 1 轻量得多的原因。

分布式锁用 SET NX EX 只是起点,完整的坑和方案见 分布式锁详解:数据库与 Redis 两条路线的实现、坑点与选型

List:队列和最新列表#

双向链表,两端操作 O(1),按索引访问 O(n)。

# 消息队列:左进右出
LPUSH queue:tasks "task-1"
LPUSH queue:tasks "task-2"
RPOP queue:tasks # 返回 "task-1"(FIFO)
# 阻塞弹出:队列为空时等待,最多 30 秒
BRPOP queue:tasks 30
# 最新列表:只保留最近 100 条
LPUSH feed:user:1001 "article-xxx"
LTRIM feed:user:1001 0 99 # 截断,只留前 100 个
LRANGE feed:user:1001 0 9 # 取最新 10 条

BRPOP 让消费者不需要轮询,队列空了就阻塞等待,有数据就立刻返回。比 while True: RPOP + sleep 省 CPU 也少延迟。

Hash:对象存储#

一个 key 下挂多个 field-value 对,适合存结构化对象。

# 存储用户信息
HSET user:1001 name "Alice" age 28 city "Shanghai"
# 读单个字段
HGET user:1001 name # 返回 "Alice"
# 读所有字段
HGETALL user:1001
# 返回:name Alice age 28 city Shanghai
# 修改单个字段,不用覆盖整个对象
HINCRBY user:1001 age 1 # age 变成 29

和用 String 存 JSON 相比,Hash 的优势是可以读写单个字段,不用每次序列化/反序列化整个对象。对象有 20 个字段但每次只改 1 个的场景,Hash 明显更合适。

Hash 的内存优化

当 field 数量少且值短时(默认 128 个 field 以内,每个值不超过 64 字节),Redis 底层用 listpack(旧版本是 ziplist)而不是哈希表存储,内存占用显著更小。这也是为什么「把大对象拆成小 Hash」是常见的内存优化手段。

Set:去重和集合运算#

无序集合,元素唯一,支持交集、并集、差集。

# 文章标签
SADD article:2048:tags "Redis" "NoSQL" "缓存"
SMEMBERS article:2048:tags # 返回所有标签
# 去重:记录今天访问过的用户
SADD uv:2026-09-01 "user:1001" "user:1002" "user:1001"
SCARD uv:2026-09-01 # 返回 2(自动去重)
# 共同关注
SADD follow:alice "bob" "charlie" "dave"
SADD follow:bob "alice" "charlie" "eve"
SINTER follow:alice follow:bob # 返回 "charlie"(交集)

SINTERSUNIONSDIFF 是 Redis 在集合运算上的独特优势。社交场景的「共同好友」「可能认识的人」,用 Set 一条命令搞定,自己写代码要拉两个列表再循环比对。

Sorted Set(ZSet):排行榜和延迟队列#

和 Set 一样元素唯一,但每个元素关联一个 score(浮点数),按 score 排序。底层是跳表 + 哈希表,插入和查询都是 O(log N)。

# 排行榜
ZADD leaderboard 95.5 "alice" 88.0 "bob" 92.3 "charlie"
# 取 Top 3(分数从高到低)
ZREVRANGE leaderboard 0 2 WITHSCORES
# 返回:alice 95.5, charlie 92.3, bob 88.0
# 查排名(从 0 开始,降序)
ZREVRANK leaderboard "bob" # 返回 2(第三名)
# 延迟队列:score 存执行时间戳
ZADD delay:queue 1725148800 "order:timeout:1001"
ZADD delay:queue 1725148830 "order:timeout:1002"
# 消费者轮询:取出已到期的任务
ZRANGEBYSCORE delay:queue 0 1725148810
# 返回 score ≤ 当前时间的元素

延迟队列是 ZSet 的经典用法:订单超时关闭、定时通知,都可以把执行时间戳当 score 塞进去,消费者定时用 ZRANGEBYSCORE 捞到期的任务。比起 TimerScheduledExecutorService,好处是重启不丢、多实例共享。

不止五种:三个实用扩展类型#

HyperLogLog:用 12KB 估算上亿 UV#

统计页面独立访客数(UV),用 Set 存用户 ID 准确但吃内存。HyperLogLog 是概率数据结构,固定占 12KB 内存,标准误差 0.81%,可以统计 2^64 个不同元素。

PFADD uv:page:home "user:1001" "user:1002" "user:1001"
PFCOUNT uv:page:home # 返回 2
# 合并多天的统计
PFMERGE uv:page:home:week uv:page:home:mon uv:page:home:tue

代价是只能近似计数,不能取出具体元素。适合对精度要求不高但数据量巨大的统计场景。

Bitmap:签到和状态标记#

把 String 的每一位当布尔值用,一个 bit 表示一个状态。

# 用户签到:bit 偏移量 = 今天是今年的第几天
SETBIT sign:user:1001:2026 244 1 # 第 244 天签到
GETBIT sign:user:1001:2026 244 # 返回 1
# 统计全年签到天数
BITCOUNT sign:user:1001:2026 # 返回签到总天数

一个用户一年的签到记录只需要 365 bit ≈ 46 字节。100 万用户一年的签到数据约 44MB,用 Set 存每天的日期字符串,内存消耗是这个数字的几十倍。

Stream:带消费者组的消息队列#

Redis 5.0 引入,弥补了 List 做队列和 Pub/Sub 的缺陷:支持消费者组、消息确认(ACK)、持久化、历史回溯。

# 生产者:追加消息
XADD mystream * name "task-1" data "payload"
# * 让 Redis 自动生成 ID(时间戳-序号)
# 创建消费者组,从头开始消费
XGROUP CREATE mystream grp1 0
# 消费者读取(阻塞模式)
XREADGROUP GROUP grp1 consumer-1 COUNT 1 BLOCK 5000 STREAMS mystream >
# > 表示只读新消息
# 处理完后确认
XACK mystream grp1 "1725148800000-0"

Part 2 会展开 Stream 和 Pub/Sub 的对比。

持久化:RDB 快照 vs AOF 日志#

Redis 数据在内存里,进程一挂数据就没了。持久化解决的就是这个问题,有两种机制。

RDB:定时快照#

把某一时刻的全量数据序列化成二进制文件(dump.rdb)。

# redis.conf 配置
save 900 1 # 900 秒内至少 1 次写入,触发快照
save 300 10 # 300 秒内至少 10 次写入
save 60 10000 # 60 秒内至少 10000 次写入

优点: 文件紧凑,恢复速度快(直接加载二进制),适合做备份。

缺点: 两次快照之间的数据会丢失。save 60 10000 意味着最多丢 60 秒数据。而且 BGSAVE 需要 fork 子进程,内存大时 fork 本身就慢(后面踩坑部分会细说)。

AOF:追加写日志#

每条写命令追加到日志文件(appendonly.aof),重启时重放命令恢复数据。

# redis.conf 配置
appendonly yes
appendfsync everysec # 每秒刷盘(默认,折中方案)
# appendfsync always # 每条命令都刷盘,最安全但最慢
# appendfsync no # 交给操作系统,最快但可能丢几秒

优点: 最多丢 1 秒数据(everysec 模式),数据安全性高。

缺点: 文件比 RDB 大,恢复慢(要重放所有命令)。虽然有 AOF 重写(BGREWRITEAOF)来压缩文件,但日常体积仍然比 RDB 大。

混合持久化(推荐)#

Redis 4.0 引入,AOF 重写时不再纯追加命令,而是先写一份 RDB 快照,再追加快照之后的增量命令。恢复时先加载 RDB 部分(快),再重放增量(少),兼顾了速度和安全性。

aof-use-rdb-preamble yes # 默认开启(Redis 4.0+)

选型建议: 生产环境开混合持久化。不在乎丢几分钟数据的(比如纯缓存场景)可以只开 RDB。两种都不开,Redis 就是纯内存数据库,重启即清空。

过期与内存淘汰策略#

过期删除:惰性 + 定期#

给 key 设了 TTL 之后,Redis 不会在到期的瞬间立刻删除——那样需要一个定时器盯着每个 key,开销太大。实际是两种策略配合:

  • 惰性删除: 访问一个 key 时检查是否过期,过期了才删。不访问就不删,可能有过期 key 占着内存。
  • 定期删除: 每秒 10 次(默认),随机抽查一批设了 TTL 的 key,删掉已过期的。如果过期比例超过 25%,立刻再抽一批,直到比例降下来。

两者结合基本够用,但如果过期 key 积累速度超过定期清理速度(比如大量 key 同时到期),内存还是会涨。这时候靠内存淘汰策略兜底。

八种淘汰策略#

当 Redis 内存达到 maxmemory 限制时,新写入会触发淘汰。maxmemory-policy 决定淘汰谁:

策略淘汰范围规则
noeviction不淘汰写入直接报错(默认)
allkeys-lru所有 key淘汰最近最少使用的
allkeys-lfu所有 key淘汰使用频率最低的
allkeys-random所有 key随机淘汰
volatile-lru设了 TTL 的 key淘汰最近最少使用的
volatile-lfu设了 TTL 的 key淘汰使用频率最低的
volatile-random设了 TTL 的 key随机淘汰
volatile-ttl设了 TTL 的 key淘汰剩余 TTL 最短的

怎么选:

  • 纯缓存场景(数据丢了能回源),用 allkeys-lruallkeys-lfu。LFU 更适合有热点数据的场景——热 key 即使短时间没被访问,频率计数器也不会马上归零。
  • 缓存和持久数据混存(部分 key 不能丢),用 volatile-lru,只淘汰设了 TTL 的缓存 key,没设 TTL 的持久数据不会被动。
  • noeviction 适合数据不能丢且宁可拒绝写入也不丢的场景,比如用 Redis 做消息队列。

Part 2:进阶实战#

Pipeline:一次网络往返做一批事#

Redis 命令执行本身是微秒级的,但每条命令都要走一次网络往返(RTT)。本地局域网 RTT 约 0.1ms,看着不多,但 1 万条命令串行发就是 1 秒。

Pipeline 的思路很简单:客户端把一批命令打包发过去,Redis 顺序执行后把结果一次性返回。省掉了中间的 N-1 次网络往返。

import redis
r = redis.Redis()
# 不用 Pipeline:10000 次网络往返
for i in range(10000):
r.set(f"key:{i}", i)
# 用 Pipeline:1 次网络往返
pipe = r.pipeline()
for i in range(10000):
pipe.set(f"key:{i}", i)
pipe.execute() # 一次性发送,一次性返回

实测差异通常在 5-10 倍以上,RTT 越大差距越明显。

Pipeline 不是事务。 它只是批量发送,命令之间没有原子性保证——中间可能穿插其他客户端的命令。需要原子性,用事务或 Lua 脚本。

Pipeline 也不要一次塞太多命令(几万条以上),否则 Redis 的输出缓冲区会暴涨。建议按批次分片,每批几百到几千条。

事务与 Lua 脚本:原子操作的两条路#

MULTI/EXEC 事务#

MULTI # 开启事务
SET balance:alice 100
SET balance:bob 200
EXEC # 原子执行

MULTIEXEC 之间的命令会被排队,EXEC 时一次性串行执行,中间不会被其他命令插入。

但 Redis 事务有两个关键限制:

  1. 不支持回滚。 如果中间某条命令执行失败(比如对 String 执行 INCR 但值不是数字),其他命令照样执行。这和 MySQL 的事务完全不同。
  2. 命令入队时不执行,拿不到中间结果。 你没法在事务里读一个值、根据值做判断、再写入。WATCH + MULTI/EXEC 可以实现乐观锁,但用起来比较绕。

Lua 脚本:更灵活的原子操作#

Redis 保证 Lua 脚本的执行是原子的——整个脚本跑完之前不会执行其他命令。而且脚本里可以读值、做判断、再写入,比事务灵活得多。

# 库存扣减:检查库存 → 扣减,原子完成
EVAL "
local stock = tonumber(redis.call('GET', KEYS[1]) or '0')
if stock < tonumber(ARGV[1]) then
return -1
end
return redis.call('DECRBY', KEYS[1], ARGV[1])
" 1 stock:sku:1001 1

这个脚本在 分布式锁详解:数据库与 Redis 两条路线的实现、坑点与选型 里的秒杀场景中出现过:把库存检查和扣减焊成一个原子操作,不需要分布式锁。

EVALSHA 优化: 每次发完整脚本浪费带宽。先用 SCRIPT LOAD 把脚本缓存到 Redis 拿到 SHA1,之后用 EVALSHA sha1 ... 调用,只传 40 字节的哈希值。

注意: Lua 脚本执行期间 Redis 不处理任何其他命令。脚本跑太久(比如里面有大循环)会阻塞整个 Redis。生产环境建议设 lua-time-limit(默认 5 秒),超时后其他客户端的命令会返回 BUSY 错误(但脚本不会被自动杀掉,需要 SCRIPT KILL 或等它跑完)。

发布订阅 vs Stream:消息队列两代方案#

Pub/Sub:轻量但脆弱#

# 订阅者
SUBSCRIBE channel:order
# 发布者(另一个客户端)
PUBLISH channel:order '{"orderId": 1001, "action": "created"}'

Pub/Sub 是实时推送模型:消息发了就推给在线的订阅者,不存储。

三个致命缺陷:

  1. 无持久化。 订阅者不在线时的消息直接丢失,没有重试。
  2. 无 ACK 机制。 发布者不知道消息有没有被处理成功。
  3. 无消费者组。 同一条消息会推给所有订阅者(广播),做不了负载均衡。

适合做实时通知(比如配置变更广播、WebSocket 消息分发),不适合做可靠消息队列。

Stream:Redis 版的消息队列#

Stream 解决了上面三个问题:

# 生产者
XADD orders * orderId 1001 action "created"
XADD orders * orderId 1002 action "created"
# 创建两个消费者组,模拟不同的下游服务
XGROUP CREATE orders payment-group 0
XGROUP CREATE orders notify-group 0
# 支付服务的消费者(负载均衡:同组内多个消费者分摊消息)
XREADGROUP GROUP payment-group pay-worker-1 COUNT 1 BLOCK 5000 STREAMS orders >
# 处理完确认
XACK orders payment-group "1725148800000-0"
# 查看未确认的消息(排查卡住的消费者)
XPENDING orders payment-group
能力Pub/SubStream
消息持久化有,存在 Redis 里
消费者组有,同组内负载均衡
ACK 机制有,未确认可重消费
历史回溯有,XRANGE 读历史
阻塞读取

Stream 和 Kafka 的定位差异: Stream 适合中小规模、对延迟敏感、不想引入额外组件的场景。Kafka 在吞吐量(百万级 TPS)、多副本持久化、生态集成上是另一个量级。如果系统里已经有 Kafka,没必要用 Stream 替代;如果只是想在 Redis 里做一个轻量队列,Stream 够用。

缓存三兄弟:穿透、击穿、雪崩#

这三个问题的共性是:请求没被缓存挡住,直接打到数据库。

缓存穿透:查的数据根本不存在#

恶意请求或代码 bug 导致大量查询一个数据库里也没有的 key。缓存查不到 → 穿透到数据库 → 数据库也没有 → 不写缓存 → 下次还是穿透。

graph LR A[请求 id=-1] --> B{缓存有?} B -->|没有| C[查数据库] C -->|也没有| D[不写缓存] D --> E[下次照样穿透]

解法:

  1. 缓存空值。 数据库查不到时,缓存一个空标记(比如空字符串),设短 TTL(如 60 秒)。简单有效,但如果攻击者遍历大量不存在的 key,缓存会被垃圾数据撑满。
  2. 布隆过滤器。 在缓存前面加一层布隆过滤器(Redis 的 RedisBloom 模块,或客户端侧实现),请求先过布隆过滤器——不存在的 key 直接拦截,存在的才查缓存和数据库。误判率可控(通常设 1% 以内),不会漏掉真实数据。

缓存击穿:热 key 过期瞬间被打穿#

一个高频访问的 key 过期的那一刻,大量并发请求同时发现缓存没了,全部涌向数据库。

解法:

  1. 互斥锁。 缓存未命中时,先用 SET lock NX EX 抢锁,抢到的那个请求去查数据库并回写缓存,其他请求等待或重试。
  2. 逻辑过期。 缓存不设真实 TTL(永不过期),而是在值里存一个逻辑过期时间。发现逻辑过期后,异步开一个线程去更新缓存,当前请求直接返回旧数据。用户看到的数据可能晚几秒,但不会打穿数据库。

两种方案取舍:互斥锁保证数据一致但有短暂延迟,逻辑过期保证可用性但数据可能短暂不一致。

缓存雪崩:大面积 key 同时过期#

不是一个 key 的问题,而是大批 key 在同一时刻过期(比如缓存预热时统一设了相同的 TTL),或者 Redis 节点宕机。

解法:

  1. TTL 加随机偏移。 不要给同类 key 设一样的过期时间,加一个随机值打散:TTL = base + random(0, 300)
  2. 多级缓存。 本地缓存(如 Caffeine)+ Redis,Redis 挂了本地缓存还能顶一阵。
  3. 熔断降级。 数据库压力达到阈值时,直接返回默认值或错误提示,不再继续查询。保护数据库不被打死,等缓存恢复后再放开。

主从复制 → 哨兵 → Cluster:高可用三级跳#

单节点 Redis 有两个硬伤:挂了就没服务,容量上限就是单机内存。三种架构分别解决不同层面的问题。

主从复制:读写分离#

一个主节点负责写,多个从节点复制主节点的数据,负责读。

Master (读写) ──复制──> Slave 1 (只读)
──复制──> Slave 2 (只读)

复制过程: 从节点第一次连接主节点时,主节点执行 BGSAVE 生成 RDB 快照发过去(全量同步);之后主节点的每条写命令实时发给从节点(增量同步)。

局限: 主节点挂了,没有自动切换机制——需要人工把从节点提升为主节点,然后改所有客户端的连接地址。凌晨三点告警的话,这个手动操作的时间就是业务中断的时间。

哨兵(Sentinel):自动故障转移#

哨兵是一组独立的监控进程,盯着主节点:

  1. 主节点宕机,多个哨兵投票确认(避免误判)
  2. 从节点中选一个提升为新主节点
  3. 通知其他从节点改跟新主节点
  4. 通知客户端连接新主节点
Sentinel 1 ──监控──> Master ──复制──> Slave 1
Sentinel 2 ──监控──> ──复制──> Slave 2
Sentinel 3 ──监控──>

哨兵解决了高可用问题,但没解决容量问题——数据还是在一台机器的内存里。

Redis Cluster:分片 + 高可用#

Cluster 把数据分散到多个节点上,总容量 = 各节点内存之和。

核心机制: 16384 个哈希槽(slot),每个 key 通过 CRC16(key) % 16384 算出属于哪个槽,每个主节点负责一部分槽。

graph TD subgraph Redis Cluster M1["主节点 1\n槽 0-5460"] --> S1["从节点 1"] M2["主节点 2\n槽 5461-10922"] --> S2["从节点 2"] M3["主节点 3\n槽 10923-16383"] --> S3["从节点 3"] end C[客户端] -->|"key=user:1001\nCRC16 → 槽 5894"| M2

客户端路由: 客户端发命令到任意节点,如果 key 不在该节点的槽上,节点返回 MOVED 重定向告诉客户端去找谁。智能客户端(如 Jedis、Lettuce)会缓存槽映射关系,大部分请求一次就到位。

多 key 操作的限制: MGET、Lua 脚本里涉及多个 key 时,这些 key 必须在同一个槽上,否则报错。可以用 Hash Tag 强制路由:{user:1001}:name{user:1001}:age 中大括号里的部分相同,一定落在同一个槽。

选型:

场景方案
数据量小,需要高可用主从 + 哨兵
数据量超过单机内存Cluster
纯缓存,丢了能回源单节点也行,加哨兵更稳

生产环境的五个高频坑#

1. 大 key 拖垮集群#

一个 Hash 里塞了 100 万个 field,一个 List 存了几十万条记录——读写它的时候,序列化、网络传输、内存分配都是放大的。DEL 一个大 key 甚至会阻塞 Redis 几秒(单线程,删除期间其他命令排队)。

怎么发现:

redis-cli --bigkeys # 扫描各类型的最大 key
redis-cli --memkeys # 按内存占用排序

怎么治:

  • 拆分:把一个大 Hash 按前缀拆成多个小 Hash。
  • 删除用 UNLINK 代替 DEL,异步释放内存,不阻塞主线程。
  • 预防:代码审查时就控制集合类型的元素数量上限。

2. 热 key 打穿单片#

Cluster 模式下,一个被高频访问的 key 只在一个节点上。该节点的网络带宽和 CPU 被打满,其他节点闲着。

解法:

  • 本地缓存:热 key 在应用侧缓存一份(Caffeine / Guava Cache),TTL 设短(比如 1 秒),大部分请求不到 Redis。
  • 读写分离:热 key 的读请求分散到从节点。
  • key 拆分:hot_key 拆成 hot_key:1hot_key:2hot_key:N,随机读其中一个,请求打散到不同槽。

3. 慢查询:KEYS 和全量遍历#

KEYS * 遍历整个键空间,O(N),key 多了直接卡死 Redis。生产环境绝对不要用。

替代: SCAN 命令,游标式分批遍历,每次只返回一小批,不阻塞。

SCAN 0 MATCH user:* COUNT 100 # 从游标 0 开始,每批约 100 个
# 返回下一个游标和匹配的 key,游标为 0 表示遍历完成

同理,SMEMBERS(返回集合所有元素)在大 Set 上也是灾难,用 SSCAN 替代。HGETALL 同理,用 HSCAN

4. 持久化 fork 卡顿#

BGSAVE(RDB 快照)和 BGREWRITEAOF 都需要 fork 子进程。fork 的耗时和 Redis 占用的内存量成正比——内存 10GB 时 fork 可能要几十毫秒,内存 50GB 时可能到几百毫秒。fork 期间主线程完全阻塞,所有请求排队。

缓解:

  • 控制单个 Redis 实例的内存上限(建议不超过 10-15GB),数据量大时用 Cluster 分片。
  • 在从节点上做 BGSAVE,不影响主节点服务。
  • 关闭 THP(Transparent Huge Pages),它会让 fork 后的 copy-on-write 内存开销放大。
echo never > /sys/kernel/mm/transparent_hugepage/enabled

5. 连接池耗尽#

Redis 处理快,但 TCP 连接的建立和销毁不快。高并发时如果每个请求都新建连接,连接数打满 Redis 的 maxclients(默认 10000),后续请求直接被拒绝。

做法: 用连接池。Java 用 JedisPool / Lettuce(Lettuce 默认连接复用),Python 用 redis.ConnectionPool,Go 用 go-redis 的内置池。

import redis
# 连接池:最多维持 20 个连接
pool = redis.ConnectionPool(host='localhost', port=6379, max_connections=20)
r = redis.Redis(connection_pool=pool)

连接池大小不是越大越好。Redis 单线程处理命令,连接再多也是排队。经验值:连接数 ≈ 业务线程数 + 少量冗余,通常 20-50 足够。

收尾:Redis 选型决策树#

Redis 功能多,但不是所有场景都适合。

graph TD A[需要高性能键值存取?] -->|是| B{数据能丢吗?} B -->|丢了能回源| C[纯缓存:Redis + allkeys-lru] B -->|不能丢| D{数据量多大?} D -->|单机内存能装下| E[Redis + 哨兵 + 混合持久化] D -->|超过单机| F[Redis Cluster] A -->|不是键值场景| G{需要什么?} G -->|排行榜/延迟队列| H[Redis Sorted Set] G -->|轻量消息队列| I[Redis Stream] G -->|可靠消息 + 高吞吐| J[用 Kafka / RocketMQ] G -->|事务 + 复杂查询| K[用关系数据库]

不该用 Redis 的场景:

  • 数据量大但访问频率低——内存贵,这种数据放磁盘数据库更合适。
  • 需要复杂查询(JOIN、聚合、全文搜索)——Redis 的查询能力有限,硬搞不如用 MySQL / Elasticsearch。
  • 对数据一致性要求极高——Redis 的主从复制是异步的,故障切换时可能丢几秒数据。金融交易的资金流水不要只放 Redis 里。

一句话总结:Redis 是「快」和「灵活数据结构」的交汇点。用对了是利器,用错了是内存黑洞。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

一篇文章掌握 Redis:从五种数据结构到生产避坑
https://l1ngg.info/posts/tech/redis-complete-guide/
作者
L1ngg
发布于
2026-09-01
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录