网站扛不住并发,最常见的瓶颈不是 CPU 也不是带宽,而是数据库。一个高频访问的页面,如果每次都去查数据库,几千并发就能把数据库压到响应慢、连接堆积。而绝大多数这类查询的结果在几百毫秒甚至几秒内是不变的,完全没必要每次都查库。
Redis 就是来解决这个问题的:它是一个基于内存的键值数据库,读写都在内存里完成,速度比磁盘数据库快几个数量级。把热数据放进 Redis,应用先读 Redis,读不到再查数据库并回填,数据库的压力就能降下来。这篇文章讲清楚 Redis 的定位、怎么用(旁路缓存模式)、以及实际部署中必须注意的几件事:数据结构选择、过期策略、连接管理、和数据库的一致性。
一、Redis 为什么快,快在哪里
Redis 快的根本原因是它把数据放在内存里。内存的随机读写延迟在纳秒到微秒量级,而磁盘(即便是 SSD)也要几十到几百微秒,机械硬盘更是毫秒级。除此之外,Redis 还有几个设计上的加分项:单线程处理命令避免了锁竞争、基于 IO 多路复用能高效处理大量连接、协议简单、数据结构针对性强。
量级参考(经验值,实际取决于机器、网络、数据大小和命令复杂度):单实例 Redis 的读操作通常能做到每秒十万次量级,写操作也能到几万到十万次量级;一次本地 Redis 查询的耗时通常在亚毫秒级别,而一次数据库查询即便有索引也常在几毫秒到几十毫秒,复杂的关联查询更慢。这就是为什么把热点查询挪到 Redis 效果立竿见影。
但要清醒地认识到:Redis 是缓存,不是数据库的完全替代品。它的数据放在内存里,容量有限且成本较高,并且可能因为重启、淘汰等原因丢数据。它适合存"可以重算出来"的热数据,而不是唯一的权威数据源。
# 安装 Redis
# Debian/Ubuntu
apt update && apt install -y redis-server
# RHEL/CentOS/Rocky(需要 EPEL)
yum install -y epel-release && yum install -y redis
# 启动与开机自启
systemctl enable --now redis
# Debian/Ubuntu 服务名一般为 redis-server,RHEL 系为 redis
systemctl status redis-server 2>/dev/null || systemctl status redis
# 基本连通性测试
redis-cli ping # 期望返回 PONG
redis-cli info server | grep redis_version
二、旁路缓存模式:Cache Aside
最通用的用法是"旁路缓存"(Cache Aside),读和写的流程分别如下。
读流程:先查 Redis,命中就直接返回;未命中就查数据库,把结果写回 Redis,再返回。
写流程:先更新数据库,再删除 Redis 里对应的缓存(注意是删除,不是更新)。下次读的时候自然回填新数据。
为什么是"删缓存"而不是"更新缓存"?因为更新的成本更高、也更容易出错(比如并发下两次更新写入缓存的顺序可能颠倒)。删除更简单:删掉之后下一次读会把最新数据重新拉进去。代价是会有一次缓存未命中,但这通常可以接受。
读:
value = redis.get(key)
if value is None:
value = db.query(...)
redis.set(key, value, ttl)
return value
写:
db.update(...)
redis.delete(key) # 删除而非更新
这个模式简单有效,但有并发隐患:如果"更新数据库"和"删除缓存"之间刚好有读请求,可能出现旧值被写回缓存的情况。常见处理是"延迟双删":更新数据库后先删一次缓存,过一小段时间(比如几百毫秒)再删一次,把并发读可能写回来的旧值清掉。再进一步就是订阅数据库的 binlog 变更来主动失效缓存,一致性更好但实现更复杂。
三、数据结构与序列化选择
Redis 的价值不只是简单字符串,它提供了多种数据结构。选对结构能大幅降低内存占用和操作复杂度。
| 结构 | 适用场景 | 说明 |
|---|---|---|
| String | 缓存单个对象、计数器 | 最通用,能存序列化后的对象 |
| Hash | 缓存对象的多个字段 | 可以只读改某个字段,省内存 |
| List | 队列、最新列表 | 支持两端操作,可做简单消息队列 |
| Set | 去重、标签、好友关系 | 集合运算方便 |
| Sorted Set | 排行榜、延时任务 | 带分数排序 |
| Bitmap / HyperLogLog | 签到、UV 统计 | 极省内存的统计结构 |
对象缓存最常见的就是 String 或 Hash。存对象需要序列化,序列化方式直接影响性能和内存:
| 序列化方式 | 特点 | 建议 |
|---|---|---|
| JSON | 可读性好,跨语言,体积中等 | 通用首选,调试友好 |
| MessagePack | 二进制、体积比 JSON 小 | 追求体积和速度时用 |
| Protobuf | 体积小、速度快,需要 schema | 强类型、跨服务场景 |
| PHP serialize / Java 原生 | 与语言绑定,体积大 | 尽量避免 |
| 原始字符串 | 体积最小 | 只存纯值时用 |
一个务实的原则:能用 JSON 就用 JSON,除非确实对内存或性能有极致要求,再换成二进制格式。可读性在排障时的价值往往被低估——能直接在 redis-cli 里看懂内容,比省那点内存更实用。
四、key 设计与过期淘汰策略
Key 的命名要有规范,方便排查和批量管理。常见做法是"业务前缀:对象:标识",比如 user:profile:10086、article:detail:2048。统一前缀的好处是可以用 SCAN 按模式查找,也避免不同业务的 key 撞车。
要注意几个坑:key 不要设得太长(浪费内存),也不要用没有意义的名字;避免使用 KEYS * 这种在生产上会阻塞 Redis 的命令,改用 SCAN 游标遍历。
# 用 SCAN 渐进式遍历,避免阻塞(生产禁用 KEYS *)
redis-cli --scan --pattern 'user:profile:*' | head
# 查看某个 key 的剩余过期时间
redis-cli ttl user:profile:10086
# 给 key 设置过期时间(秒)
redis-cli expire user:profile:10086 3600
过期时间(TTL)是缓存安全的关键。给缓存设 TTL 有两个作用:一是让冷数据自动清理,释放内存;二是当一致性出现问题时,TTL 是最后的兜底——即使某次更新漏删了缓存,数据也会在 TTL 到期后被纠正。TTL 该设多长,取决于业务对数据新鲜度的容忍度:商品详情可以几十分钟到几小时,库存这类敏感数据可能只设几秒。
内存淘汰策略决定内存满了之后 Redis 怎么办。配置在 redis.conf 的 maxmemory-policy:
| 策略 | 行为 | 适用 |
|---|---|---|
| noeviction | 内存满后写操作报错 | 只做缓存且能容忍报错时慎用 |
| allkeys-lru | 所有 key 中淘汰最近最少使用的 | 纯缓存场景常用 |
| allkeys-lfu | 淘汰使用频率最低的 | 访问有明显冷热差异时 |
| volatile-lru | 只在设了 TTL 的 key 中淘汰 | 部分 key 需要持久保留 |
| volatile-ttl | 优先淘汰快过期的 | 希望尽快清掉临期数据 |
纯缓存用途推荐 allkeys-lru 或 allkeys-lfu,并设置合理的 maxmemory(比如给机器内存的六七成,留出余量给系统和其他进程)。
# 用 CONFIG SET 临时查看/修改(生产建议改配置文件持久化)
redis-cli config get maxmemory
redis-cli config get maxmemory-policy
redis-cli config set maxmemory-policy allkeys-lru
五、缓存预热与批量操作
缓存预热是在流量高峰前,主动把热点数据加载进 Redis,避免大量请求同时未命中、一起打到数据库(这就是"缓存击穿"的一种)。预热的方式可以是一个脚本定时把热点 key 写入,或者在服务启动时加载。
# 简单的预热思路:把热点数据批量写入
# 用 pipeline 一次性提交多条命令,减少往返
redis-cli -h 127.0.0.1 -p 6379 <<'EOF'
SET article:detail:1 "..." EX 3600
SET article:detail:2 "..." EX 3600
SET article:detail:3 "..." EX 3600
EOF
Pipeline 是把多条命令打包一次性发送,减少网络往返。对于需要批量读写的场景,性能提升明显。但要注意 pipeline 一次不要塞太多命令,避免单次响应过大或阻塞其他请求,通常几百到几千条一批比较合适。
应用层则要配置连接池:每次访问 Redis 都新建连接的开销很大,连接池复用连接能显著提升吞吐。连接池大小要按并发量和 Redis 的连接数上限来定,太小会排队,太大反而浪费。
# 查看当前连接数和连接池使用情况
redis-cli info clients
# connected_clients 是当前连接数,留意是否接近上限
redis-cli config get maxclients
六、缓存穿透、击穿与雪崩
这三个是 Redis 使用中最经典的坑,值得单独讲。
缓存穿透:请求的数据既不在缓存也不在数据库(比如恶意查询不存在的 ID),导致每次请求都打到数据库。解决办法:一是对查不到的结果也缓存一个空值(设短 TTL),二是对 ID 做合法性校验,三是用布隆过滤器提前拦截不存在的 key。
缓存击穿:某个热点 key 恰好过期,大量并发请求同时未命中,一起涌向数据库。解决办法:对热点 key 加互斥锁,只让一个请求去查库回填,其他请求等待;或者干脆不给这类热点 key 设过期时间,靠主动更新来维护。
缓存雪崩:大量 key 在同一时刻集体过期,瞬时把数据库打满。解决办法:给 TTL 加随机抖动,让过期时间分散开;对关键数据做多级缓存;数据库前面加限流保护。
避免 TTL 集体过期的小技巧:在基础 TTL 上加一个随机值
ttl = 基础过期时间 + random(0, 基础过期时间 * 0.1)
七、性能观察与常见优化
日常要盯几个指标:命中率、内存使用、连接数、慢查询、以及是否有大 key。
# 命中率相关指标
redis-cli info stats | grep -E 'keyspace_hits|keyspace_misses'
# 命中率 ≈ hits / (hits + misses),太低说明缓存没发挥作用
# 内存使用
redis-cli info memory | grep -E 'used_memory_human|maxmemory_human|mem_fragmentation_ratio'
# 慢查询日志(需要在配置里设置 slowlog-log-slower-than)
redis-cli slowlog get 10
# 找大 key(用 redis-cli --bigkeys 扫描,注意它会遍历,低峰期执行)
redis-cli --bigkeys
优化要点:避免单个 key 存过大的对象(几百 KB 以上的大 key 会阻塞单线程),必要时拆分;避免在循环里逐条读写,改用 pipeline 或批量命令;读写频繁但变化少的配置类数据适合常驻缓存不设过期;对一致性要求高的数据用主动失效而不是依赖过期。
# 低峰期检查大 key,注意 --bigkeys 会遍历 keyspace,可能影响性能
redis-cli --bigkeys -i 0.1 # -i 0.1 每扫描 100 条 sleep 0.1 秒,降低影响
常见问题
Q:Redis 挂了会不会影响业务?
纯缓存场景下,Redis 挂了应该降级到直接查数据库,业务能继续但会变慢。所以应用必须有降级逻辑,不能让 Redis 故障直接导致整站不可用。重要数据可考虑主从加哨兵做高可用。
Q:缓存和数据库不一致怎么办?
优先保证"最终一致":更新数据库后删缓存,配合理 TTL 兜底,一致性要求高就用延迟双删或订阅变更。绝对强一致很难做到,也没有必要——缓存本身就是牺牲一点一致性换性能。
Q:TTL 设多长合适?
取决于业务能容忍多旧的数据。更新不频繁、容忍度高的可以设几小时;敏感的设几秒到几十秒。同时要加随机抖动,避免集体过期。
Q:为什么推荐删缓存而不是更新缓存?
更新缓存在并发下容易乱序,且要做序列化、可能更新后马上又被读覆盖。删除更简单可靠,代价是一次未命中,通常可接受。
Q:内存满了 Redis 会怎样?
取决于 maxmemory-policy。设成 noeviction 时写操作会报错,可能与配置的淘汰策略不符;缓存场景建议设 allkeys-lru,并给 maxmemory 留出系统余量。
总结
Redis 用内存换速度,把重复的数据库查询挪到内存里,是提升网站并发能力最直接的手段之一。单实例读操作通常在每秒十万次量级,响应在亚毫秒级,比数据库快几个数量级。
标准用法是旁路缓存:读时先查 Redis、未命中查库并回填;写时更新数据库后删除缓存。删除而非更新,配合理 TTL 和必要时的延迟双删来处理一致性。
数据结构按场景选,对象缓存用 String 或 Hash,序列化优先 JSON;key 用统一前缀规范命名,生产禁用 KEYS *,用 SCAN 代替。
过期和淘汰策略要配对:TTL 给数据新鲜度兜底,淘汰策略(如 allkeys-lru)保证内存不会撑爆。TTL 加随机抖动可以避免雪崩。
别忘了缓存穿透、击穿、雪崩这三类问题,分别用空值缓存/布隆过滤器、互斥锁/不过期、TTL 打散来应对。日常盯命中率、内存、连接数和慢查询,避免大 key 和逐条读写。做好这些,Redis 才能真正稳稳地给网站提速。
企业QQ咨询




