☰
Redis实战指南:从核心数据结构到高可用架构与性能优化
2026/10/24 3:00:44 网站建设 项目流程

1. 为什么后端开发绕不开 Redis

做后端开发这几年,最常被问到一个问题:明明数据库都选好了,为什么还要在中间加一层 Redis?

这个问题的答案,其实就藏在业务场景里。Redis 全程约等于 Remote Dictionary Server,最初的本职工作是一个基于内存的键值存储系统。内存意味着读写速度快,键值意味着数据结构灵活,这两点叠加起来,就能解决很多关系型数据库不太好处理的事情。

举个最常见的例子:一个电商首页的推荐栏目,同一份数据每个用户进来都要查一次数据库,数据库还要排序、过滤、关联多张表,高峰期每秒几十上百次查询,压力一下子就上来了。这时候把结果缓存进 Redis,同一个 key 在几分钟内直接走内存返回,响应从几十毫秒降到毫秒级不说,数据库的连接和查询压力也大幅缓解。这个场景,几乎是每个后端初学者接触 Redis 的第一个入口,也是我对 Redis 的第一印象。

但如果你只把它当成缓存,那就亏了。慢慢摸熟之后会发现,它的数据类型、事务能力、发布订阅、慢查询分析这些机制,在队列、排行榜、计数器、防重、分布式锁等场景里都有一席之地。这篇笔记不会讲太底层的东西,主要是把我在学习和实际项目里常用的部分整理成一套可复用的思路:先搞清楚什么时候该用它,再上手不同数据结构的实操,然后把持久化、高可用这些保命配置补上,最后是日常排查问题的思路。适合刚接触 Redis 的后端读者,也适合已经用过一段时间但没系统梳理过的朋友。

2. 核心数据结构入门与实操细节

2.1 String 是基本功,但用法不止 set/get

String 是 Redis 里最简单的数据类型,value 可以是字符串、数字、甚至二进制序列化后的对象,最大 512MB。很多人一开始只会用 set key value 和 get key,这没问题,但它的高级用法才更见功力。

比如计数器场景:文章阅读数、商品库存扣减、短信发送频率限制,用 incr 和 decr 一行命令搞定,比先查出来再 set 回去安全得多,因为整个操作是原子的,天然避免了并发下的覆盖问题。我见过有人用先 get、再 set 的方式做计数器,并发高一点就会出现丢数据,明明 1000 次请求,最后只加了 800,这种问题用 incr 直接就不存在了。

再比如 setnx 和带过期时间的 set 命令,是分布式锁的雏形。Redis 2.6 之后提供了 set key value ex 秒数 nx 这种组合写法,一条命令完成加锁和设置过期时间,防止进程崩溃后锁永远不释放。实际项目里这么做还有边界情况要处理,后面单独讲。

需要注意一个细节:Redis 是单线程模型处理命令,所以单个命令一定是原子的,但一组命令组合起来就不是了,需要事务或 Lua 脚本兜底。String 类型本身没有太多花活,但这个原子性概念贯穿全部数据类型,理解透了对后面用 Hash、List 都有帮助。

2.2 Hash 比 String 更擅长存对象

如果缓存的是用户信息、订单快照这类结构化的数据,用 Hash 比用 String 更合适。String 存 JSON 序列化后的整体,改一个字段要全量取出、反序列化、再写回;Hash 可以把每个字段单独存,比如 hset user:1001 name "张三" age 25,改年龄就只要 hset user:1001 age 26,其他字段完全不受影响。

Hash 内部结构有两种编码方式:ziplist 和 hashtable。当字段数量少、值都比较短的时候,Redis 会用紧凑的内存布局,省空间;数据量大了才转为真正的哈希表。这个转换是自动的,你不需要关心,但要了解它的存在,因为涉及内存估算和大 key 问题时需要用到。

实操中常用的是 hmset 批量写入、hmget 批量读取。注意 hgetall 这个命令,虽然方便,但如果字段太多,一次取全量数据会占用大量网络和内存,不建议在线上高频率使用。我习惯的做法是:商品详情这种字段稳定的数据用 Hash 一次 hmget 指定字段;需要整体返回给前端的场景,反而直接用 String 存 JSON 更省事,因为序列化和反序列化的成本可以由业务端控制。

2.3 List、Set、ZSet 在日常业务里的经典用法

List 底层是双向链表,支持两端推入弹出,所以天然适合做消息队列和最新列表。lpush + rpop 或者 rpush + lpop 都能实现简单的队列语义。但要注意,这只是最简单的队列,没有 ACK 机制,数据被 pop 之后如果处理失败就丢了。做异步任务队列,最好还是用专门的消息中间件,或者用 Redis 的 List + BRPOPLPUSH 这种带备份队列的方式。BRPOPLPUSH 能把弹出的元素同时放进另一个备用队列,消费者处理成功后再移除,失败时可以从备用队列捞回来,算是在不引入外部依赖的情况下的一种补偿方案。

Set 用来做去重和集合关系最顺手。点赞用户列表、抽奖用户池、共同好友这类场景,sadd 加成员,scard 拿数量,sinter 求交集。它的底层是哈希表,所以判断某个成员是否存在的时间复杂度是 O(1)。

ZSet 是我认为 Redis 中最值得深入研究的一个类型。每个成员关联一个 score,Redis 自动按 score 排序。排行榜就是它的主场:zadd rank:2024 100 "user1",要更新分数就直接 zadd 覆盖,要拿 Top N 用 zrevrange rank:2024 0 9 withscores。还有延迟队列,score 存到期时间戳,消费者定期用 zrangebyscore 拿到期数据,也算一个轻量方案。

这里有个容易混淆的点:Set 是集合,成员不会重复;List 是列表,可以有重复元素。业务选型时先想清楚数据是否需要去重、是否有顺序要求,再决定用哪个。顺序且不重复用 ZSet,顺序可重复用 List,无序去重用 Set,无序可重复基本不用 Redis 的集合类型。

2.4 使用中容易踩的几个坑

第一个坑是 key 设计。我见过 Redis 里有一堆类似 user:12345678 的 key,看着没毛病,但类型信息完全没体现,时间相关、业务维度也都没区分。规范一点的风格是把业务、对象、唯一标识、附加属性拼在一起,比如 order:pay:20240601:12345,用冒号做分隔符。这样既方便人读,也方便用 scan 按前缀排查。

第二个坑是过期时间。很多缓存 key 设了过期时间,但更新数据时直接 set 新值,忘了重新设置过期时间,结果 key 永不过期,内存越堆越多。我在写一个工具类时固定把所有 set 操作都包装成带过期时间的写法,习惯成自然,这个问题就很少再犯。

第三个坑是批量获取。线上出现过 Redis 响应变慢的案例,查到最后是应用里有循环 get 几千个 key,每个 key 一次网络往返,串行跑下来光网络耗时就要了几秒。用 mget 一次拿回来,或者用 pipeline 批量提交命令,能省掉绝大多数往返时间。这个优化思路在清理缓存场景也同样适用。

3. 持久化与高可用:从单机到集群的演进

3.1 RDB 和 AOF 怎么选

Redis 数据在内存里,一旦进程重启,数据就没了,所以持久化是必须面对的问题。Redis 提供两种方案:RDB 和 AOF。

RDB 是定期生成一个二进制快照文件,恢复速度快,适合做备份和灾难恢复。默认配置 save 900 1,意思是 900 秒内至少有 1 次写入就触发快照。缺点是两次快照之间的数据会丢,比如你配置 15 分钟一次快照,最后 14 分钟写入的数据在宕机后就没了。

AOF 则是把所有写命令追加到日志文件,可以做到每一秒甚至每一个写命令都落盘。数据安全度更高,但文件体积大,恢复速度也慢。Redis 提供了 AOF 重写机制,把里面对同一个 key 的重复写操作合并成一条,控制文件体积。实际使用时我一般两个都开:AOF 保证数据不丢,RDB 作为快速恢复和备份的手段。单靠 AOF 恢复大数据库时太慢,先从 RDB 加载再补充 AOF 增量,这是 Redis 4.0 之后混合持久化的思路,默认开启的话就省心很多。

有一点容易忽略:持久化触发的 fork 过程会短暂阻塞主线程,RDB 快照如果落在业务高峰期,可能导致几十毫秒甚至更长的延迟。有条件的话,把自动触发时间安排在低峰期,或者手动控制在确定的时间窗口执行。

3.2 主从复制与哨兵怎么配合

单机 Redis 挂掉就直接不可用了,后端项目至少要有主从结构。主节点处理写请求,从节点同步数据并处理读请求。实际效果有两个:一是读写分离,把读压力分摊到多个节点;二是故障切换的候补,主节点挂了可以从从节点里提拔一个上来。

同步机制在北伐里理解就行:主节点把写命令记录到一份叫 repl_backlog 的缓冲区和自己的 AOF/RDB 里,从节点先全量同步主节点的数据文件,再持续接收增量命令。这里容易踩坑的是主节点配置了 requirepass 密码,但忘了在从节点的配置里配 masterauth,结果从节点一直连接不上,日志里反复报错。

哨兵是专门负责监控和自动故障切换的进程。它和 Redis 节点是分开部署的一组进程,几个哨兵互相投票决定主节点是否真的挂了,然后执行切换。复制超时时间、心跳频率这些参数,在生产环境里都要调过,默认值偏向保守,有时候要等十几秒才反应。我一般把 down-after-milliseconds 调到 5000 左右,既避免网络抖动的误判,也能让切换足够快。

3.3 集群模式的设计逻辑

当单机内存和性能都不够用,或者需要高可用时,Redis Cluster 是常用的选择。它把所有 key 按照哈希槽分配到 16384 个槽里,每个节点负责一部分槽,数据自动分布在多台机器上。

注意一个关键限制:Cluster 模式支持的数据操作必须都在同一个 slot 里。如果一次 mset 的 key 分散在不同节点,Redis 会直接报错。解决办法是用 hashtag,比如把 key 写成 {user}:1001:profile 和 {user}:1001:order,花括号内的部分参与 hash 计算之后会落到同一个 slot。这个机制在业务设计阶段就要想清楚,否则后面拆分数据时就麻烦了。

Cluster 的实际维护会比单机复杂,比如某个节点的槽位迁移期间性能会受影响,在线扩容时要注意控制迁移速度,避免把网络带宽打满。早期我只在本地单机上玩 Redis,第一次接触集群是在一个流量翻倍的项目里,当时只关心扩展性,没细想客户端如何感知节点变化,结果扩容时应用连不上新节点。后来才意识到,Cluster 模式下客户端需要做 MOVED 重定向处理,库选对版本很重要。

4. 常见性能问题与排查思路

4.1 大 Key 和热 Key 怎么查

大 Key 是线上 Redis 性能下降的第一大元凶。value 特别大或者一个集合类型的元素特别多,都算大 Key。它带来的问题很典型:单次操作耗时高,占用网络带宽,持久化时也容易导致主线程阻塞。

排查方式有两种。第一种是 redis-cli --bigkeys,它会遍历整个库,统计各种类型中最大的几个 key;第二种是 scan 命令手动扫描,结合 strlen、llen、scard 等命令判断。注意这两种方式在高压力环境中要错峰执行,因为扫描本身也会消耗性能。

发现大 Key 后的处理,一般思路是拆分。比如某个 hash 里存了几十万个字段,按业务维度拆成多个 key;或者把大 value 压缩后再存储。还有一种是分批删除:用 hscan 拿到部分字段,每次删除一批,避免直接 del 一个大集合导致 Redis 阻塞。

热 Key 是另一个高频问题。某个突然爆火的新闻详情、某个大促秒杀商品,一瞬间所有请求打到同一个 key 上,即使 Redis 本身很快,单节点也会被打到 CPU 飙高。处理方案有本地缓存兜底、把热 key 复制成多个带随机后缀的副本分散读压力、或者直接限制并发读。具体选用哪种,要看业务对一致性的容忍度。

4.2 慢查询日志怎么看

Redis 支持记录执行时间超过阈值的命令,默认超过 10000 微秒的会记录到 slowlog。用 slowlog get 就能看到最近哪些命令拖慢了速度。

最常见的慢命令是对大集合的排序和聚合操作,比如 sort、keys、smembers 这类。尤其 keys 这个命令,在数据量大时会扫描全库,是线上故障的高发原因。生产环境我会直接改名或禁用它,需要匹配 key 时用 scan 代替。另一个容易忽视的是大量使用 expire 设置相同过期时间,导致同一秒内有海量 key 同时过期,Redis 要批量清理,引发阻塞。解决办法是给过期时间加上随机偏移量,让删除动作分散开。

4.3 内存和连接数管理

Redis 内存满了会怎样?默认是写不进去,读还可以。配置了 maxmemory-policy 之后,会根据淘汰策略自动删数据。常用策略有 allkeys-lru(所有 key 按最近最少使用淘汰)、volatile-lru(只淘汰带过期时间的)。业务上缓存类数据适合 allkeys-lru,但要保证被淘汰后还能从数据库恢复。如果存的是不可丢失的数据,那就不能让它走淘汰,得单独用持久化机制处理。

连接数方面,Redis 默认允许 10000 个客户端连接,但应用服务器连接池配置不当会出现问题。我遇到过连接池 maxTotal 设太大,应用重启瞬间几百个线程同时建连接,Redis 端文件描述符被耗尽的故障。合理做法是结合 QPS 和平均执行时间估算连接池大小,给单机 Redis 留足够余量。客户端连接异常断开的清理也是底层的事,大多数客户端库能处理,但要注意空闲连接检测的配置。

4.4 常用监控命令速查

实际工作中我最常用的几个排查命令,整理成一张速查表:

目的命令或方式
查看实例整体信息info
查看慢查询slowlog get
统计各类型大 Keyredis-cli --bigkeys
查看当前连接客户端client list
实时监控命令执行monitor
查看 key 剩余时间ttl key
查看内存占用分布memory usage key

有一点要说明:monitor 在生产环境不能长期开着,它会持续输出所有命令,对性能有明显影响,适合短期定位问题。

5. 实际项目中的最佳实践与避坑记录

5.1 缓存一致性,经典难题怎么破

缓存和数据库的数据不一致,是所有用 Redis 做缓存的后端开发都会遇到的问题。常见的做法是写操作先更新数据库,再删除缓存;读操作先读缓存,缓存没有就查数据库再回填缓存。

这个方案有个瑕疵:更新数据库和删除缓存不是原子的,删除缓存万一失败,缓存里还是旧数据。通常通过重试机制解决,比如把删除失败的消息丢到本地队列或者消息系统里反复尝试。

另一个更隐蔽的问题是并发写。线程 A 更新数据库为 100,线程 B 更新数据库为 200,由于先删缓存,再有一个读线程把旧值 100 回填进缓存,之后就一直是 100。这个问题没有一个不牺牲性能的完美解法,业界常见做法是给缓存加一个很短的过期时间兜底,保证最差情况下最终一致。真实项目里很少需要强一致,只要业务能容忍几秒内有旧数据,这套方案就完全够用。

5.2 缓存穿透、击穿、雪崩的三个解法

这三个词是面试题,也是线上事故的名字。

缓存穿透是指请求一个缓存里没有、数据库里也不存在的 key。每次请求都会打到数据库,白白消耗连接。解决办法有两种:一种是查询数据库为空时也缓存一个空值,加上短过期时间;另一种是布隆过滤器,把所有可能存在的 key 提前过滤一遍,不存在的请求直接拦截掉。

击穿是指某个热点 key 过期的一瞬间,大量请求同时冲进数据库。解决办法是互斥锁,只有拿到锁的线程去加载数据库数据,其他线程等一会再访问缓存;或者把这类热点 key 设置为逻辑过期,内部存一个时间戳,收到请求发现快过期时,后台异步刷新。

雪崩是指大量 key 在同一时间段集体过期,数据库扛不住。解决方法是过期时间加随机偏移量,这个前面提过,再加上多级缓存分担压力。这三个问题本质上都是让数据库别被瞬时流量穿透,理解了这一层,方案就是围绕缓存和阈值做文章。

5.3 分布式锁的正确姿势

用 Redis 做分布式锁,最基础的写法是 set key value ex 10 nx。但释放锁的时候要注意:不能误删别人的锁。如果线程 A 的锁过期了,线程 B 获得了锁,线程 A 此时来执行 del,就会把 B 的锁删掉。解决方法是 value 里放一个唯一标识,删除之前先 get 判断是不是自己的,再 append 删除。考虑到判断和删除是两个操作,不原子也可能出问题,所以一般用 Lua 脚本把 get、判断、del 一起执行。

还有一个问题是锁过期时间怎么定。设短了,业务没执行完锁就丢了;设长了,万一持有者崩溃,其他线程要等很久。有一种方案是给锁的线程加一个看门狗,定期续期;不过这个实现复杂度会上升。实际项目里如果业务是短事务,固定 10 秒足够;如果是长任务,尽量在任务结束时主动释放锁,并且把续期机制做成可选项。

5.4 监控和容量规划

不要等故障发生才去看 Redis。我给自己的项目最低要求是:云上 Redis 开启基础监控,CPU、内存、命中率、网络流入流出这几个指标必看。命中率低于 80%,说明缓存利用率偏低,可能是 key 过期太频繁或者缓存内容覆盖太快;内存接近上限时提前扩容或清理无效 key;网络指标在异常飙升时说明某个业务出现了批量拉取。

容量规划说起来很简单,实际上需要估算:业务每天产生的 key 有多少,每个平均多大,过期时间分布如何,希望缓存命中周期多长。我见过团队在项目上线前从未估算过内存,结果双十一前一周 Redis 内存告警,临时清缓存加节点,手忙脚乱。提前算好,留出 30% 的余量,后面会从容很多。

5.5 我个人的一点体会

初学 Redis 时,我一度以为把它当成缓存就是全部了。直到在延迟敏感的项目里发现 MySQL 查询慢,用 Redis 缓存结果后整个接口响应从 80 毫秒降到 3 毫秒,才真正理解"存储分层"这四个字的实际价值。后来开始研究数据结构和集群方案,基本是哪个模块遇到问题,就深入哪一个点:排行榜算法用 ZSet 解决,重复消息用 Set 去重,分布式锁用 Lua 脚本保证原子性。Redis 这条学习线拉得挺长,但每次解决一个真实问题时,那些记忆就会被彻底激活。没有什么比在故障排查时发现问题源于自己对底层机制的一知半解更让人印象深刻的了。希望这篇笔记,也能让你的 Redis 实践少踩几个坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询