资讯动态

Redis 入门到实战:数据类型、持久化与缓存三大问题详解

发布时间:2026/10/14 22:48:12 来源:尧图企业网站定制
大概两年前某开发群里有个急得团团转的同学他们的社区论坛一搞活动数据库就飙到100% CPU首页接口十几秒打不开运营在后台点个按钮都能把服务拖崩。我让他先把热帖数据放进 Redis 缓存极端情况再加一个降级开关结果半天时间接口延迟从 800ms 降到了 8ms。这个场景你应该不陌生。Redis 几乎成了后端架构里的标配零件面试问、项目用、排查问题也绕不开。但很多刚接触的人容易陷入两个极端要么把 Redis 当成一个高性能 Map只会 set/get要么一上来就搞集群、搞哨兵结果连单机版的持久化策略都没搞明白。这篇笔记就是写给两类人看的一是刚入门的后端开发想搞清楚 Redis 到底怎么用、为什么快、怎么选持久化方案二是已经会用基本命令但遇到缓存穿透、数据不一致、大 Key 问题时不知道怎么下手的新手。我会尽量把原理讲透但更偏重实战——毕竟我当年也是从用 Redis 存了个字符串开始一步步踩坑踩过来的。1. 为什么 Redis 能顶住每秒十万次请求先搞懂它的底层设计很多人第一次听说 Redis是被内存数据库几个字吸引的。但内存快只是表象真正让它能在单机上扛住极高并发的原因是一套非常克制的设计组合纯内存访问、单线程命令执行、IO 多路复用以及专门定制过的数据结构。1.1 单线程不是落后是刻意为之Redis 的核心命令执行是单线程的。听到这里习惯用多线程压榨 CPU 的朋友可能会皱眉单线程岂不是浪费多核但你要想清楚Redis 的瓶颈从来不在 CPU。一次简单的 GET 操作耗时主要集中在网络 IO 和内存访问上CPU 只负责做非常轻量的指令解析和数据搬运。在这种场景下单线程反而带来了巨大的好处不需要加锁没有锁竞争和上下文切换的开销不会有死锁问题数据读写天然串行不会出现并发修改同一份数据导致的不一致实现简单逻辑可预测性强排查问题容易得多。我用一组数据直观说明在一台普通的虚拟机上跑redis-benchmark测简单 GET/SET轻松能到 10 万 QPS 级别。这个量级对绝大多数业务系统来说已经绰绰有余。真到 CPU 成为瓶颈时你首先该做的是拆分数据、优化命令、减少大 Key而不是急着重写一个多线程 Redis。1.2 IO 多路复用一个服务员同时服务几百桌客人Redis 单线程能支撑大量并发连接的秘密在于它用 epoll在 BSD 和 macOS 上是 kqueue实现 IO 多路复用。这就像一个餐厅只雇一个服务员但他不挨桌站着等客人举手而是站在门口看哪桌按了铃才过去。绝大部分连接可能处于空闲状态Redis 只处理那些真正有数据可读、可写的连接这样单线程也能高效服务成千上万个客户端。这一点对入门者最大的启示是不要因为 Redis 是单线程就觉得它慢。真正应该避免的是在 Redis 里执行耗时操作比如KEYS命令遍历全部 key、一次存入超大 value、执行复杂的 Lua 脚本。这些操作会卡住整个事件循环后面所有请求都要排队。类似的情况我在第 5 章会详细展开。1.3 定制的数据结构不只是存在内存里那么简单Redis 里的字符串底层用的是 SDS简单动态字符串而不是 C 语言传统的以\0结尾的 char 数组。SDS 在头部记录了长度和可用空间这样获取字符串长度是 O(1) 的而 C 字符串要遍历到末尾才知道长度。追加内容时SDS 还能预分配空间减少频繁的内存重分配。类似的优化遍布每个数据类型List 底层可能是压缩列表或双向链表Hash 底层可能是压缩列表或哈希表ZSet 底层是跳表加哈希表。这些设计不是炫技而是为了解决实际问题在内存昂贵、CPU 资源宝贵的前提下让每种操作都能在尽可能小的空间和时间复杂度内完成。1.4 用自己的眼睛看性能跑一遍 redis-benchmark说话不如动手。装好 Redis 之后你可以直接体验一下单线程的性能redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 100000 -t get,set这条命令模拟 100 个并发连接执行 10 万次 GET 和 SET。跑完你能看到每秒请求数和平均延迟。我第一次跑的时候有点被吓到——一台普通 Linux 虚拟机SET 操作能跑到接近 9 万 QPS。从那以后我就明白了Redis 快不是传说是设计出来的。2. 五种基本数据类型用命令把业务场景翻译成代码Redis 的每种数据类型都不是摆设背后对应着一类经典业务场景。很多新手觉得命令太多记不住其实只要抓住这个类型最适合存储什么关系的数据命令自然就好记了。下面按常用程度逐个讲。2.1 String缓存、计数器、分布式锁都靠它String 是最基础的类型value 可以是字符串、数字或二进制数据大小上限 512MB。常见命令就几个SET、GET、DEL、INCR、DECR、EXPIRE。它是缓存业务的首选把数据库查出来的 JSON 序列化后直接SET key value EX 300五分钟内高频请求都走内存。INCR可以做自增计数比如文章的浏览量、活动的点赞数。要注意它和INCRBY都是原子操作多个客户端同时操作也不会互相覆盖。我做过一个活动页的浏览量统计用 Redis 计数每五分钟批量同步一次数据库压力小很多。SETNX是仅在 key 不存在时设置的意思它常用来实现分布式锁的雏形SET lock_order_123 unique_value NX EX 5 # 业务处理完毕 DEL lock_order_123当然这只是最基础的用法生产上还要考虑锁误删、看门狗续期等问题但概念可以给你打底。2.2 Hash存对象比 String 序列化更灵活Hash 类型适合存储对象每个 key 下可以挂多个 field-value。比如存放用户信息HSET user:101 name zhangsan age 25 city beijing HGET user:101 name HGETALL user:101相比把整个对象转成 JSON 再塞进 StringHash 最大的优势是你可以只读写某一个字段而不必拉取整个序列化结果。缺点是过期时间只能挂在 key 上整组 field 一起过期精细化控制不如 String。2.3 List消息队列和最新列表List 底层是双向链表或压缩列表支持从左右两端压入、弹出。LPUSH加BRPOP组合起来就是一个简单的阻塞式消息队列生产者从左边推入任务消费者在右边阻塞弹出没有任务时一直挂着不轮询。LPUSH task_queue sendEmail BRPOP task_queue 0另外一个常见用途是最新列表比如论坛的最新评论用LPUSH把新评论推到头部LRANGE取前 20 条。要提醒一句如果列表访问量极高并且只关心尾部数据List 可能不是最优解具体要看访问模式。2.4 Set去重、交并集运算一把好手Set 的元素无序、不重复天然适合做标签系统好友关系抽奖池。比如给一个用户加标签SADD user:101 tags java redis backend SISMEMBER user:101 tags java我们之前做过一个共同好友功能取两个用户的 Set直接SINTER求交集一个命令返回结果不用在业务代码里写循环对比。2.5 ZSet排序这件事尽量别交给数据库反复算ZSet 是有序集合每个元素有一个 scoreRedis 根据 score 排序。排行榜是最经典的例子ZADD leaderboard 1000 player_1 ZADD leaderboard 2000 player_2 ZREVRANGE leaderboard 0 9 WITHSCORES # 取分数最高的前 10ZSet 还适合做延时队列score 存执行时间戳后台线程通过ZRANGEBYSCORE取出到期的任务处理。给你一个综合场景某视频 App 的热门视频榜与我的收藏夹——热门榜用 ZSet 按播放量排序收藏夹用 Set 存用户收藏的视频 ID两套结构职责分明操作也很直接。下面这张表可以帮你快速回顾数据类型底层核心结构典型场景关键命令StringSDS缓存、计数器、分布式锁SET GET INCR SETNXHash哈希表/压缩列表对象属性存取HSET HGET HGETALLList双向链表/压缩列表消息队列、最新列表LPUSH RPOP LRANGE BRPOPSet哈希表/整数集合去重、标签、交并集SADD SINTER SPOPZSet跳表哈希表排行榜、延时队列ZADD ZRANGE ZREVRANGE入门阶段建议不要急着记全部命令先把这五个类型的核心命令和适用场景练熟。每个类型做一个小练习比如用 String 做登录验证码、用 Hash 做用户资料、用 ZSet 做积分榜大约一个下午就能建立直觉。3. 持久化必答题RDB 与 AOF 怎么选才不会丢数据Redis 是内存数据库那问题来了进程重启或机器断电内存里的数据怎么办这就是持久化要解决的问题。很多新手喜欢直接关掉持久化觉得Redis 不就是个缓存吗丢了重新从库里加载不就行了。但在某些场景下Redis 里存的是最新的计数、未入库的队列消息甚至业务配置丢了就是事故。Redis 提供了两种持久化机制RDB快照和 AOF追加日志。它们不是二选一的对立关系更多时候是搭配使用。3.1 RDB 快照像拍一张照片恢复快但可能丢掉最近的数据RDB 是 Redis 在指定时间间隔内把内存数据生成一份二进制快照保存到磁盘上的 dump.rdb 文件。触发方式有两种手动SAVE/BGSAVE或者按配置规则自动触发。实际环境里我们几乎只会用BGSAVE它通过 fork 子进程写文件主进程继续服务。这里的核心机制是 fork 后的写时复制——子进程刚 fork 出来时父子共享同一份内存物理页当主进程要修改某个页时才复制一份出来给子进程用。所以理论上BGSAVE不会阻塞正常服务但要注意如果实例内存特别大比如几十 GBfork 本身会短暂卡顿这个时间通常和内存大小、系统负载有关需要在维护窗口做实际测试。RDB 的缺点很明确默认配置下最多可能丢最近 5 分钟、甚至更长时间的数据。因为快照是一个时间点的完整拷贝不可能每秒都做一次。适合的场景是那些允许少量丢失的缓存数据、或者用于快速恢复大量数据到内存的冷备份。3.2 AOF 日志记录每一次写操作丢数据范围大幅缩小AOF 的原理是把所有写命令追加到文件末尾。宕机后重启 Redis 时会重放 AOF 文件里的命令把数据恢复到故障前的状态。它的核心参数是appendfsync控制何时把缓冲区里的命令实际刷到磁盘always每条写命令都刷盘理论上最安全但性能损耗最大everysec每秒刷一次最多丢 1 秒数据性能与安全性的平衡点生产环境默认推荐no交给操作系统决定何时刷盘性能最好但可能丢更多数据。我自己的选择几乎一直是everysec这也符合官方推荐。你需要在最多丢 1 秒数据和性能损耗之间做个判断。AOF 的坑是文件会持续增长。跑了很久的实例AOF 可能膨胀到几个 GB。所以 Redis 提供了 AOF 重写机制BGREWRITEAOF——它会遍历内存中的当前数据生成最小化的命令集重建 AOF相当于把历史操作合并成一条条最终状态的写入。重写也是后台进行的不会阻塞主进程。3.3 混合持久化4.0 之后的最佳选项Redis 4.0 开始引入了混合持久化机制开启后 AOF 文件里会先是一段 RDB 格式的二进制内容紧接着是增量命令。这样重启时加载速度接近 RDB同时数据丢失窗口又近似于 AOF。配置只需在 redis.conf 打开appendonly yes aof-use-rdb-preamble yes我个人建议的生产配置是RDB 和 AOF 同时开启AOF 使用everysec并且打开混合持久化开关。这样既有 RDB 的快速恢复能力又有 AOF 的低丢失风险。如果是纯缓存场景业务能接受重启后全部从数据库重建那你完全可以只开 RDB甚至关了持久化把磁盘读写负担省下来。有一个细节值得注意开启 AOF 后重启Redis 会优先加载 AOF 文件因为它通常包含更完整的数据。所以如果 AOF 文件损坏需要先用redis-check-aof --fix修复再启动否则数据加载会失败。4. 缓存翻车现场穿透、击穿、雪崩的成因与化解方案如果说数据类型是 Redis 的基本功那缓存穿透、缓存击穿、缓存雪崩这三件事就是生产环境里的必考大题。几乎每个用过 Redis 做缓存的团队都遇到过至少其中一种。我负责过的某电商项目三种全踩了一遍。4.1 缓存穿透天天打数据库因为查的 key 根本不存在穿透的典型场景黑客恶意请求一个不存在的商品 ID比如GET /product/999999。缓存里没有于是每次请求都穿透到数据库而数据库也没有这条记录查询结果也不会回填缓存。几千个恶意请求就能让数据库压力飙升。有两个主流解法缓存空值数据库查不到时在 Redis 里存一个空对象并设一个很短的过期时间比如 60 秒。下一次同样的请求直接命中空缓存不再打到数据库。缺点是需要维护大量空 key容易被恶意请求塞满内存所以通常配合短过期时间使用。布隆过滤器在缓存前置一个布隆过滤器快速判断这个 key一定不存在还是可能存在。不存在就拦截返回根本不落 Redis。布隆过滤器有误判但不会漏判代价是维护成本高一些。我的建议是小规模业务先用缓存空值顶上简单有效如果确实面临高频恶意扫描再考虑引入布隆过滤器组件。4.2 缓存击穿一个热点 key 扛着全站流量它过期那一瞬间就崩击穿和穿透的最大区别是穿透打的是不存在的 key击穿打的是大家都来查、但缓存刚好过期的热点 key。比如某个明星突然上了热搜对应的一条数据缓存过期结果十几万请求瞬间穿过 Redis 直击数据库。最常见的解法是互斥锁。查询缓存时如果没命中不是直接查库而是先尝试获取一把分布式锁只有拿到锁的请求才去查库并回写缓存其他请求先等锁等到了再去查缓存。伪代码思路长这样Java 风格String value redis.get(key); if (value null) { String lockKey lock: key; boolean locked redis.setIfAbsent(lockKey, token, 5, TimeUnit.SECONDS); if (locked) { try { value db.query(key); redis.set(key, value, 300, TimeUnit.SECONDS); } finally { // 只有持有锁的线程才能删除锁用自己的 token 校验 redis.deleteIfEquals(lockKey, token); } } else { Thread.sleep(50); value redis.get(key); // 重试实际生产中要控制重试次数 } }还有另一种思路叫逻辑过期缓存里存的数据没有物理过期时间而是附带一个业务过期时间戳。发现逻辑过期后先返回旧值后台异步更新缓存。这种方案体验更好但实现复杂度更高。新手入门先把互斥锁方案做对就行。4.3 缓存雪崩大面积 key 同时失效或者 Redis 直接宕机雪崩是击穿的放大版。常见原因是给一批 key 设置了相同的过期时间比如凌晨零点活动上线所有商品都设了 60 分钟过期凌晨 1 点整它们全部一起消失数据库瞬间被打穿。解法也直接过期时间加随机值SET key value EX 600 (rand() % 300)让过期时间在 5~10 分钟内错开热点数据不设过期时间改为后台异步更新再配合逻辑过期保护多级缓存兜底Redis 之外再加一层本地缓存如 CaffeineRedis 挂了还有本地内存挡一挡高可用与降级Redis 集群做主从启动熔断降级数据库侧加连接池保护。我们当时修复的方式很简单把所有商品的过期时间从固定 3600 秒改成3600 随机10%再对访问量最大的活动页规则做单独处理数据库压力立刻恢复平稳。别小看这行随机值它能解决很多看似复杂的故障。4.4 缓存和数据库的一致性先更新库还是先删缓存新手最容易踩的另一个坑是纠结到底先更新数据库还是先删缓存。你在网上会看到各种复杂的说法我推荐一套简洁可靠的做法读请求读缓存命中直接返回未命中则读数据库再写缓存写请求先更新数据库再删除缓存不是更新缓存。为什么是删而不是更新因为更新缓存需要把最新的 DB 值拼好再写进去如果并发写很多容易导致缓存里写入旧值。而删除缓存是幂等且简单的下次读请求会自然触发回填。如果担心更新数据库成功、删除缓存失败导致脏数据可以在删除失败后把 key 发到消息队列里做异步补偿或者用延时双删先删缓存、更新数据库、等几百毫秒再删一次。对大多数业务来说先更新数据库再删缓存配合一个简单的失败重试已经足够。5. 从命令行到生产环境新手常踩的六个坑这节是我自己亲身踩过、也看别人反复踩的坑的集合。每一个都真实发生在团队里你完全不必要再去重新体验一遍。5.1 别再乱用 KEYS 命令KEYS *可以把所有 key 列出来好像很方便。但它需要遍历 Redis 中全部 key单线程环境下会阻塞整个服务生产环境一个KEYS *就能让集群卡死几秒钟。排查 key 的正确姿势是使用SCAN命令它通过游标分批遍历每次返回少量数据不会阻塞服务。SCAN 0 MATCH user:* COUNT 100另一个相关问题是大 Key。String 类型虽然能存 512MB但一个 value 太大会导致网络传输耗时、内存淘汰策略执行缓慢、甚至主从复制的延迟增大。遇到大 Key先把数据拆小或者用 Hash 分片存储。想快速排查实例里有哪些大 Key可以用redis-cli --bigkeys它会扫描并报告出各类数据类型的最大 key。5.2 key 命名规范尽早建立不然一团乱麻Redis 没有表结构约束key 的命名规范全靠团队自觉。见过最乱的实例同一个业务域里的 key 有user-id-101、uid:101、user_101_info三种风格后面的人接手时根本不敢动。我推荐使用业务域:对象类型:id:属性的格式比如user:profile:101、order:20240115:8765。冒号分割在 Redis Desktop Manager 类工具里还能自动折叠分组视觉上非常清晰。另外不要放无限增长的字段在 key 里比如user:login:zhangsanexample.com这种 key 很长还夹带敏感信息既浪费内存也不安全。5.3 不设 maxmemory内存溢出后 Linux 直接把 Redis 杀了很多入门教程没有讲 maxmemory。如果不设置这个值Redis 使用的内存会一直增长直到操作系统触发 OOM Killer把 Redis 进程杀掉。这属于看起来是随机宕机其实是被系统杀掉的经典故障。正确的做法是先在 redis.conf 或启动参数里设置maxmemory 2gb maxmemory-policy allkeys-lrumaxmemory-policy是内存达到上限后的淘汰策略几种常用选项的区别如下策略作用范围淘汰逻辑noeviction全部不淘汰直接写命令报错allkeys-lru全部 key按 LRU 近似算法淘汰最久未用的 keyvolatile-lru设了过期时间的 key从过期的 key 里淘汰最久未用的allkeys-random全部 key随机淘汰volatile-ttl设了过期时间的 key优先淘汰剩余 TTL 最短的我自己一般用allkeys-lru因为缓存数据本来就能容忍淘汰。但如果你把 Redis 当存储用里面有不希望被随机淘汰的重要数据就要谨慎评估策略和业务容忍度。5.4 RedisDesktopManager 里看到一堆 \xAC\xED 乱码用 Java 的 Spring Data Redis 时很多人会碰到一个诡异的坑在客户端工具里看到的 key 和 value 不是user:101而是一堆类似\xAC\xED\x00\x05t\x00...的二进制乱码。原因是默认的 JdkSerializationRedisSerializer 会把对象先做 Java 序列化Redis 里存的就是一堆 Java 序列化字节流人眼自然看不懂。解决方法是改用 String 序列化器处理 key用 Jackson 或 GenericJackson2JsonRedisSerializer 处理 value。简单点的做法是直接用StringRedisTemplatekey 和 value 都是字符串省心且贴近原生命令若需要存对象再单独配置序列化器。这个坑不算难但特别影响排查效率。你想想出错的时候连 key 都看不清怎么定位问题5.5 客户端连接池配置别让连接成为瓶颈另一个容易被忽略的问题是连接管理。Redis 客户端Java 的 Lettuce、JedisGo 的 go-redis 等不会无限给你创建连接默认配置往往偏保守。高并发下如果连接池太小请求会排队等待延迟飙升。Java Spring Boot 默认使用 Lettuce底层基于 Netty本身是共享连接的模型多数场景不需要调得太激进。但如果用了 Jedis就要显式配置池大小spring: redis: jedis: pool: max-active: 200 max-idle: 50 min-idle: 10核心原则是连接数别大于数据库能承受的连接数也别小于业务 QPS 所需的并发量。最好做一次压测观察等待连接的时间和活跃连接数再反过来调整。5.6 监控自己的 Redisinfo、slowlog 和命中率排查 Redis 问题不是靠猜而是靠指标。至少要学会三样东西INFO命令能看内存使用量、连接数、命中率、过期 key 数等。重点看used_memory、total_commands_processed、keyspace_hits与keyspace_misses。慢查询日志SLOWLOG GET查看超过执行阈值的命令。平时把slowlog-log-slower-than设置成 10 毫秒就能抓到那些耗时的命令。MONITOR命令实时打印 Redis 收到的每条命令调试时很好用但生产环境千万别长期开因为它会把命令逐条输出本身就很耗性能。我之前排查过一次诡异的偶发超时用SLOWLOG GET发现有一条ZRANGEBYSCORE扫了几百万个成员单条命令执行了 200 多毫秒导致后续请求全部排队。定位到问题后把层级列表改成按时间分桶的多个小 ZSet问题立刻消失。6. 一条我建议的入门练习路线最后分享一条实操路线。这条路线不是我从书上看来的是在带过几个新人之后总结出来的照着走一遍基本能完成从会用命令到敢碰生产问题的跨越。第一步把官方文档的命令列表当成字典逐个类型过一遍不必背但要动手敲。推荐用redis-cli配合redis-server的本地环境每个类型做三个小练习比如String实现一个验证码五分钟内有效的功能Hash把用户注册页面的信息用 HSET 存起来再做一个登录校验逻辑ZSet做一个小排行榜支持按分数范围查询、按排名查询。第二步用一门你熟悉的语言把上面的练习改造成真实接口。Java 可以选 Spring Data RedisGo 可以选 go-redisPython 可以选 redis-py。重点体会序列化、连接池、超时配置这些客户端才有的烦恼。第三步看 production 视角下的 Redis 配置。打开 redis.conf逐个搜maxmemory、appendonly、save、requirepass等关键配置弄清楚每个值为什么这么设。不要 Copy 一份别人的配置就完事你得自己解释得出来。第四步动手模拟一次故障。把 AOF 文件删掉再重启看数据丢没丢关掉持久化跑一会儿再看进程重启后的表现。多折腾几次你对持久化的理解会远超那些只会背概念的人。最后如果你有时间推荐读一下《Redis 设计与实现》里关于 SDS、跳表、压缩列表的章节以及官方文档的 Replication、Sentinel 部分。当你理解了单线程模型、命令执行的过程以及主从复制的机制再去学集群、哨兵就不会觉得那些只是高大上的名词了。我在实际带人过程中有一个体会学 Redis 最忌讳背命令。命令是死的业务场景是活的。真正让你进步的不是记住了ZADD还是ZREVRANGE而是你能不能在看到排行榜、去重、消息队列这些需求时第一时间想到对应的数据结构和可能踩的坑。把这篇文章里的东西吃透再开始折腾集群和高可用设计路就顺了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑