Redis服务器有什么作用?缓存架构、部署方式与应用场景

很多人的第一印象是"Redis 就是个缓存",加在 MySQL 前面挡一挡查询。这个理解只对了一部分。Redis 真正被广泛使用,是因为它把几种常用的数据结构做成了带持久化和原子操作的网络服务:缓存只是其中之一,会话存储、排行榜、分布式锁、限流、轻量级消息队列,都是它的主场。而在架构层面,Redis 往往还是那个"把数据库从读压力里解放出来"的关键组件。

这篇文章讲清楚 Redis 在系统中的位置、五大数据类型各自适合做什么、四种部署形态(单机、主从、哨兵、集群)怎么选、RDB 与 AOF 持久化的取舍,以及缓存三大问题(穿透、击穿、雪崩)和缓存与数据库一致性的常见处理方式。所有命令都是可以直接执行的,配置文件路径按发行版给了两套。

一、Redis 在系统架构里站在哪

典型的 Web 架构里,Redis 的位置大致是这样:

用户请求
  → 应用服务器(App)
      → Redis(先看有没有)
          ├ 命中 → 直接返回(亚毫秒级)
          └ 未命中 → 查 MySQL → 写回 Redis → 返回

核心价值是降低数据库的读压力和响应时间。 一次 MySQL 点查即使是走索引,也要几毫秒,还要消耗连接和 IO;Redis 的一次 GET 通常在几十到几百微秒,且不占数据库连接。热点数据命中缓存后,数据库只需要承担写入和少量未命中的读。

但要注意:引入缓存是有代价的。 它带来了数据一致性问题(数据库改了、缓存还是旧的)、可用性依赖(Redis 挂了会怎样)、以及运维复杂度(容量规划、淘汰策略、持久化配置)。读多写少、能容忍短暂不一致、且有明确热点的场景才适合加缓存。 一个每天只有几百次访问的管理后台,加 Redis 纯粹是给自己找麻烦。

Redis 单线程处理命令(6.0 之后的多线程只用于网络 IO),这是它设计的精髓:避免了锁的开销,单实例就能跑到十万级 QPS。也正因为如此,一条慢命令会阻塞后面所有命令——这点后面讲 bigkey 时会反复提到。

二、与 Memcached 怎么选

维度 Redis Memcached
数据类型 String、Hash、List、Set、ZSet 等丰富结构 只有 String
持久化 支持 RDB、AOF 不支持,纯内存
主从/高可用 原生主从、哨兵、集群 需要客户端或代理实现
单线程模型 单线程处理命令 多线程
大 value 场景 受单线程影响明显 多线程下表现更稳
单 key 大小限制 512 MB(不建议超过 1 MB) 默认 1 MB
适用场景 缓存 + 会话 + 队列 + 锁 + 排行榜 简单 KV 缓存、大 value 纯缓存

结论很直接:绝大多数新项目选 Redis。 Memcached 只在一种情况下还有优势——纯 KV 缓存、value 较大、且需要极致的多核吞吐,同时可以接受重启即丢全部数据。这类场景在现代架构里已经不多见了。

反过来,Redis 的短板也要知道:单线程意味着不能有慢命令(KEYS *、大 key 的 HGETALL、几万元素的 SMEMBERS 都会造成毫秒级到秒级的阻塞);内存成本高,数据量大时要考虑分片和冷热分离。

三、数据类型与典型场景

类型 结构 典型场景 常用命令
String 字符串/整数/二进制 缓存、计数器、分布式锁、限流 SET GET INCR SETEX SETNX
Hash 字段-值映射 对象缓存(用户信息、购物车) HSET HGET HGETALL HINCRBY
List 有序可重复 队列、消息流、最新动态 LPUSH RPOP BRPOP LRANGE
Set 无序去重 标签、共同好友、抽奖、去重 SADD SISMEMBER SINTER SRANDMEMBER
ZSet 有序带分数 排行榜、延迟队列、优先级队列 ZADD ZRANGE ZREVRANGE ZRANK

缓存(最基础用法)

SET user:1001 '{"name":"张三","vip":1}' EX 3600     # 带过期时间,单位秒
GET user:1001
TTL user:1001
EXPIRE user:1001 1800
DEL user:1001

一定要设过期时间。 不设 TTL 的缓存键会永久留在内存里,越积越多,最后触发内存淘汰甚至写满。用 SET ... EX 一次性设置值和过期时间是原子操作,比先 SET 再 EXPIRE 更安全(后者中间崩溃会留下永不过期的键)。

计数器与限流

INCR article:1001:pv                  # 阅读数
INCRBY user:1001:score 10             # 加分

# 简单限流:每分钟同一 IP 最多 100 次
INCR rl:1.2.3.4:1740
EXPIRE rl:1.2.3.4:1740 60

INCR 是原子操作,天然适合计数场景,不需要先读后写。限流的完整实现还需要判断阈值和设置过期,用 Lua 脚本保证原子性更稳妥。

分布式锁

SET lock:order:1001 <随机值> NX PX 30000     # NX:不存在才设,PX:毫秒过期
# 释放锁要用 Lua 脚本,先比对随机值再删除,避免误删别人的锁
DEL lock:order:1001

几个必须注意的点: 锁必须有过期时间(防止持有者崩溃后死锁);value 要用随机值(防止误释放别人的锁);释放锁要用 Lua 保证"比对 + 删除"的原子性。如果是 Redis 集群或者对可靠性要求极高,应该用 Redlock 或者干脆用 etcd、ZooKeeper 这类专门做分布式协调的组件。

排行榜(ZSet)

ZADD leaderboard 1000 "player:1"
ZADD leaderboard 1500 "player:2"
ZINCRBY leaderboard 50 "player:1"
ZREVRANGE leaderboard 0 9 WITHSCORES     # 前 10 名
ZRANK leaderboard "player:1"             # 我的排名(正序,从 0 开始)
ZREVRANK leaderboard "player:1"          # 倒序排名

ZSet 的排行榜是 Redis 最经典的用法之一,用 MySQL 做同样的实时排行要写复杂的排序查询,数据量大时根本跑不动。

会话存储

把 Session 从应用服务器的本地内存挪到 Redis,多台应用服务器就能共享会话,用户不会因为负载均衡被切到另一台机器而掉登录。

SETEX session:abc123 1800 '{"uid":1001,"role":"admin"}'
GET session:abc123

注意:会话数据放 Redis 后,Redis 的可用性就成了登录态的可用性。要么给 Redis 配高可用(哨兵或集群),要么在应用侧做降级(Redis 不可用时退回本地会话或要求重新登录)。

轻量级消息队列

LPUSH queue:email '{"to":"a@b.com","tpl":"welcome"}'
BRPOP queue:email 30        # 阻塞弹出,超时 30 秒

List 做队列简单好用,但没有 ack 机制,消费者崩溃后消息就丢了。对可靠性有要求的场景应该用 Stream(Redis 5.0+,支持消费组、ack、重试)或者专业的消息中间件(Kafka、RabbitMQ、RocketMQ)。

四、部署形态怎么选

形态 特点 高可用 容量扩展 适用
单机 一个实例 无 受单机内存限制 开发、测试、非关键缓存
主从复制 一主多从,读写分离 需手动切换 读扩展,写仍单机 读多写少,可容忍短暂中断
哨兵 Sentinel 主从 + 自动故障转移 自动切换 读扩展 中小规模,数据量单机放得下
集群 Cluster 分片,16384 个槽 自动故障转移 数据分片,可横向扩 大数据量或高吞吐

单机

最简单,开发环境和"缓存丢了也无所谓"的场景用它就够了。

# RHEL/CentOS/Rocky
sudo dnf install -y redis
sudo systemctl enable --now redis

# Debian/Ubuntu
sudo apt-get install -y redis-server
sudo systemctl enable --now redis-server

连接验证:

redis-cli ping                 # 返回 PONG
redis-cli info server | head

主从复制

在从节点的配置文件里指定主节点:

# /etc/redis/redis.conf(从节点)
replicaof 192.168.1.10 6379
masterauth <主节点密码>
replica-read-only yes

也可以在线生效(重启后失效):

redis-cli REPLICAOF 192.168.1.10 6379

查看复制状态:

redis-cli info replication

重点看 master_link_status(应为 up)和 slave_repl_offset 与主库 master_repl_offset 的差距(延迟)。主从复制是异步的,主库宕机可能丢失最后一部分未同步的写入,这一点要有心理预期。

从库默认只读,可以用来分担读压力。但要注意从库读到的是可能有延迟的旧数据,对一致性敏感的读不能走从库。

哨兵 Sentinel

哨兵是一组独立进程,负责监控主从健康、自动完成故障转移、并通知客户端新的主节点地址。

# /etc/redis/sentinel.conf
sentinel monitor mymaster 192.168.1.10 6379 2
sentinel auth-pass mymaster <密码>
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1

mymaster 是主节点别名;末尾的 2 是"判定客观下线所需的哨兵票数"。哨兵本身也要部署奇数个(至少 3 个)且分布在不同机器上,否则哨兵自己会成为单点。

启动:

# RHEL/CentOS/Rocky
sudo systemctl enable --now redis-sentinel
# Debian/Ubuntu
sudo apt-get install -y redis-sentinel
sudo systemctl enable --now redis-sentinel

查看:

redis-cli -p 26379 sentinel masters
redis-cli -p 26379 sentinel get-master-addr-by-name mymaster

客户端必须支持哨兵协议(能订阅哨兵拿到新主节点地址),否则切换后应用还是连着旧主库。主流语言的客户端基本都支持。

集群 Cluster

数据量超过单机内存、或者写吞吐超过单实例能力时,用集群分片。集群把键空间分成 16384 个哈希槽,分配到多个主节点。

# 每个节点都要开
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 15000

创建集群(3 主 3 从):

redis-cli --cluster create \
  192.168.1.11:6379 192.168.1.12:6379 192.168.1.13:6379 \
  192.168.1.14:6379 192.168.1.15:6379 192.168.1.16:6379 \
  --cluster-replicas 1
redis-cli --cluster info 192.168.1.11:6379
redis-cli --cluster check 192.168.1.11:6379

集群的几个限制要提前知道: 不支持跨槽的多键操作(除非用 hash tag 把相关键强制分到同一槽);KEYS、FLUSHALL 这类全局命令只在单节点生效;客户端必须支持集群协议(能处理 MOVED 重定向);扩容缩容要迁移槽,是有风险的操作。

用 hash tag 保证相关键在同一槽:

MSET user:{1001}:name "张三" user:{1001}:age 20     # 花括号内相同则同槽

选型建议

  • 数据量小于 10G、可以接受分钟级中断:主从 + 哨兵,够用且运维简单
  • 数据量超过单机内存的 60% 到 70%:上集群
  • 纯缓存、丢了能重建:甚至可以不做持久化,配合主从即可
  • 做会话存储、分布式锁等对可用性要求高的:至少哨兵,并且要在应用侧设计降级方案

五、持久化:RDB、AOF 与混合

维度 RDB AOF
原理 某一时刻的内存快照(二进制) 追加写每一条写命令
文件格式 紧凑二进制 文本协议,可读
数据安全性 可能丢失最后一次快照之后的写入 取决于 appendfsync 策略,最多丢 1 秒
文件大小 小 大(有 rewrite 机制瘦身)
恢复速度 快 慢(要重放命令)
对性能影响 fork 时有短暂停顿 每次写入都要落盘
适合 备份、快速恢复 要求少丢数据

配置

# RDB:900 秒内至少 1 次改动、300 秒内 10 次、60 秒内 10000 次,满足任一就触发
save 900 1
save 300 10
save 60 10000
dbfilename dump.rdb
dir /var/lib/redis

# AOF
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec          # always | everysec | no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

# 混合持久化(Redis 4.0+,AOF 文件里包含 RDB 格式的全量 + AOF 格式的增量)
aof-use-rdb-preamble yes

推荐配置:appendonly yes + appendfsync everysec + 开启混合持久化。 everysec 在性能和安全之间取得了不错的平衡,最坏情况丢 1 秒的写入;always 每个命令都刷盘,安全性最高但性能损失明显;no 完全交给操作系统,丢数据风险最大。

混合持久化让 AOF 文件的前半部分是 RDB 二进制(恢复快),后半部分是增量命令(数据新),兼顾了两者的优点,是现在的主流选择。

手动触发与恢复

redis-cli BGSAVE               # 后台生成 RDB 快照
redis-cli LASTSAVE             # 上次成功保存的时间戳
redis-cli BGREWRITEAOF         # 手动触发 AOF 重写
redis-cli info persistence     # 看持久化状态

RDB 快照是在子进程里 fork 出来的,采用写时复制。fork 瞬间如果内存很大,可能造成毫秒级到秒级的停顿,且内存占用峰值会翻倍风险(理论上)。 内存超过几十 G 的实例要注意 vm.overcommit_memory 的设置和 THP 的关闭。

重要提醒

持久化不等于备份。 RDB 和 AOF 文件都在 Redis 所在的那台机器上,机器宕机、误删、磁盘损坏时它们一样会没。真正的备份是把 RDB 文件定时拷到别的机器或对象存储上,并保留多份历史版本。

另外,恢复前一定要先备份当前的 RDB/AOF 文件,避免恢复失败后连原来的数据都丢了。

六、缓存三大问题与应对

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

请求 id = -1 或者一个伪造的 ID,缓存没有 → 查数据库也没有 → 每次都打到数据库。如果是被恶意刷,数据库会被拖垮。

应对办法:

  1. 缓存空值:数据库查不到时,也在 Redis 里存一个空标记,并设较短的过期时间(比如 60 秒)
SET user:-1 "" EX 60
  1. 参数校验:在接口层就拒绝明显非法的参数(负数 ID、超长字符串)

  2. 布隆过滤器:在缓存之前加一层布隆过滤器,判断"这个 key 一定不存在",直接返回。适合数据量巨大、空值缓存成本高的场景。Redis 本身可以用位图简易实现,也可以用客户端本地布隆过滤器。

缓存击穿:一个热点 key 过期了

某个超热的 key(比如首页头条)在过期的瞬间,成千上万的请求同时发现缓存没有,全部涌向数据库。

应对办法:

  1. 永不过期 + 后台更新:对极热数据不设 TTL,由定时任务异步刷新
  2. 互斥重建:缓存失效时,用分布式锁保证只有一个线程去查数据库并重建,其他线程等待或返回旧值
  3. 逻辑过期:value 里存一个过期时间戳,物理上不过期;发现逻辑过期后,拿锁的线程去异步更新,其他线程先返回旧数据
# 互斥重建的锁
SET rebuild:lock:hot:1001 1 NX PX 5000

缓存雪崩:大批 key 同时过期

如果一大批 key 在同一时刻被写入、TTL 又一样,就会同时过期,瞬间全部请求打到数据库,数据库被打垮。

应对办法:

  1. TTL 加随机抖动:基础 TTL 加上一个随机值,打散过期时间
# 基础 3600 秒,加 0 到 600 秒随机
EXPIRE cache:key $((3600 + RANDOM % 600))
  1. 多级缓存:本地缓存(Caffeine/Guava)+ Redis,即使 Redis 那一层空了,本地还有一层
  2. Redis 高可用:避免 Redis 整体不可用导致的"雪崩"(有时候雪崩指的是 Redis 挂了)
  3. 熔断与降级:数据库压力过大时,应用侧主动熔断,返回兜底数据或错误页,保护数据库不被拖死

七、过期策略与内存淘汰

Redis 对过期键采用惰性删除 + 定期采样删除的组合:

  • 惰性删除:访问某个 key 时才检查它是否过期,过期就删。好处是不浪费 CPU,坏处是过期但没人访问的 key 会一直占内存
  • 定期采样:Redis 每秒多次随机抽取一部分带过期时间的 key 检查并删除,控制内存占用和 CPU 消耗的平衡

所以过期 key 不一定会在过期时刻立即释放内存,这是 used_memory 与预期不符的常见原因之一。

当内存达到 maxmemory 上限时,会按 maxmemory-policy 淘汰键。这部分内容比较多,是下一篇(065)的主题,这里只给结论:

  • 纯缓存场景:allkeys-lru
  • 希望保留没有设过期时间的键:volatile-lru
  • 有明确的热度区分(LFU 更准):allkeys-lfu
  • 绝不能丢数据(比如做分布式锁、会话):noeviction,但必须配合监控和扩容,否则会开始报写错
redis-cli config get maxmemory
redis-cli config get maxmemory-policy
redis-cli info memory

八、缓存与数据库一致性

Cache Aside(旁路缓存)模式

这是最常用的模式,读写规则很简单:

读:先读缓存 → 命中则返回 → 未命中则读数据库 → 写入缓存 → 返回

写:先更新数据库 → 再删除缓存

注意是删除缓存而不是更新缓存。原因有两个:一是更新缓存可能涉及复杂计算(比如要重新聚合多个表),而这个值可能很久没人读;二是并发写时,更新缓存的顺序可能与数据库提交的顺序不一致,导致缓存里留下旧值。

为什么先更新数据库再删缓存?考虑这个反例:如果先删缓存再更新数据库,在两步之间有一个读请求进来,它会发现缓存没有 → 读数据库(还是旧值)→ 把旧值写回缓存,之后数据库的更新才完成。这样缓存里就永久留下了一个错误的值,直到下次过期。先更新数据库再删缓存,即使删缓存失败,最坏情况也只是缓存里是旧值,且会在 TTL 到期后修正。

延迟双删

对于上面的极端并发场景(读请求在数据库更新前读到旧值并写回缓存),可以用延迟双删缓解:

1. 删除缓存
2. 更新数据库
3. sleep 一小段时间(比如 500 毫秒,要大于一次读请求的耗时)
4. 再次删除缓存

第二次删除是为了清掉步骤 1 到 2 之间可能被写回的脏数据。它的缺点是要 sleep,会拖慢写接口,且这个 sleep 时长本身就是个拍脑袋的值。 实际项目中更常见的做法是:

更实际的方案

  • 给缓存设合理的 TTL:即使出现不一致,也会在 TTL 内自动修正。这是最简单有效的兜底
  • 用 binlog 订阅(CDC)异步更新缓存:用 Canal、Debezium 等工具订阅 MySQL 的 binlog,数据变更时异步删除或更新缓存。解耦彻底,且不侵入业务代码,是较大规模系统的常见选择
  • 删除失败要重试:把删除失败的 key 投递到消息队列重试,避免因为一次网络抖动留下脏数据
  • 对一致性要求极高的场景:不要走缓存,直接读数据库主库(比如账户余额、库存扣减)

要接受一个现实:在引入缓存之后,强一致性是做不到的。 能做的是把不一致的时间窗口压到最小,并让系统有自动修正的能力。

常见问题

Redis 要单独部署一台服务器吗?

生产环境建议单独部署,至少不要和数据库挤在同一台机器上。Redis 和 MySQL 都是内存与 IO 密集的服务,混部会互相抢资源,而且 Redis 触发 fork 或者被 OOM Killer 杀掉时,可能连带影响数据库。如果 Redis 只做缓存且量很小,和应用同机部署也可以接受,但要给它设好 maxmemory。

Redis 挂了业务会怎样?

取决于你怎么设计。如果缓存层没有降级方案,Redis 挂掉会导致所有请求打到数据库,数据库很可能接着被打垮——这就是雪崩的一种。正确的做法是:给 Redis 配高可用,在应用侧做熔断(Redis 不可用时直接查数据库或返回兜底数据,但要限流保护数据库),并监控缓存命中率的突然下跌。

缓存命中率多少算正常?

一般希望 90% 以上,热点数据为主的场景应该做到 95% 以上。命中率突然大幅下降通常意味着:有大量新 key 进来(比如改了 key 的命名)、发生了大批量过期、或者 Redis 刚重启过。命中率用 info stats 里的 keyspace_hits 和 keyspace_misses 计算。

一个 key 存多大的 value 合适?

String 类型的 value 建议控制在 10 KB 以内,最大不要超过 1 MB。大 value 会带来几个问题:网络传输慢、单线程下阻塞其他命令、内存碎片增加、以及 RDB 序列化耗时。对象类型建议用 Hash 拆分存储,方便部分读写。

为什么不能用 KEYS 命令?

KEYS * 会遍历整个键空间,键越多耗时越长,而 Redis 单线程处理命令,遍历期间其他所有命令都要排队。一个百万键的实例上执行 KEYS *,可能造成秒级的全面阻塞。 生产环境要禁用它,改用 SCAN 增量迭代:

redis-cli --scan --pattern 'user:*' --count 1000

也可以在配置文件里禁用危险命令:

rename-command KEYS ""
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG ""

注意:rename-command 改了命令名后,运维脚本和监控也要同步改,且从节点和哨兵的配置要保持一致,否则主从同步会出问题。

集群模式下批量操作怎么处理?

多键命令(MGET、MSET、SUNION 等)要求所有 key 在同一个哈希槽,否则报 CROSSSLOT。解决办法是用 hash tag 让相关 key 落在同一槽,或者在客户端按槽分组后分批执行。

总结

  • Redis 不只是缓存:String 做缓存与计数、Hash 存对象、List 做队列、Set 做去重与交集、ZSet 做排行榜,加上原子操作和过期机制,它覆盖的场景远超"挡在数据库前面"。选 Redis 而不是 Memcached 基本是默认结论,除非是纯大 value KV 且不在意持久化的特殊场景。
  • 部署形态按数据量和可用性选:数据量小、可容忍分钟级中断用主从 + 哨兵(哨兵至少 3 个、奇数、跨机器);数据量超过单机内存 60% 到 70% 或写吞吐超单实例能力时上集群,但要接受跨槽操作限制和更高的运维复杂度。
  • 持久化推荐混合模式:appendonly yes + appendfsync everysec + aof-use-rdb-preamble yes,兼顾恢复速度与数据安全性。持久化不等于备份,RDB/AOF 文件必须定期拷到别的机器或对象存储,恢复前先备份现有文件。
  • 三大缓存问题各有招:穿透用空值缓存 + 参数校验 + 布隆过滤器;击穿用热点永不过期或互斥重建;雪崩用 TTL 随机抖动、多级缓存、熔断降级。生产禁用 KEYS,用 SCAN 代替。
  • 一致性用 Cache Aside 模式:读先查缓存未命中再查库并回写,写先更新数据库再删除缓存(不是更新缓存)。延迟双删引入 sleep、价值有限;更实用的是合理 TTL 兜底 + binlog 订阅异步更新 + 删除失败重试。加了缓存就做不到强一致,目标是缩小不一致窗口并让系统自动修正。
  • 容量与安全要给约束:maxmemory 必须设置(建议留 20% 到 30% 余量),maxmemory-policy 按场景选;大 value 控制在 10 KB 级别;危险命令用 rename-command 禁用时记得同步所有运维脚本和从节点配置。