ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Redis入门到实战:五大数据类型、单线程模型与缓存穿透击穿雪崩全解析

Redis入门到实战:五大数据类型、单线程模型与缓存穿透击穿雪崩全解析 我可以直接告诉你Redis 在如今的后端项目里几乎已经是“标配中的标配”。你很难找到一个正经的业务系统里面连 Redis 的影子都见不到。我第一次接触 Redis 时感觉这东西不过是个“高级HashMap”后来在生产环境里因为一条KEYS *命令差点把整个服务拖垮才意识到自己当初有多天真。随着踩的坑越来越多我对它的看法也越来越复杂它简单到新手半小时能跑通又深奥到老手都不敢说完全吃透。这篇是《Redis(1) 初识Redis》我尽量不按教科书顺序走而是从一个做过几年一线开发、也服务过线上事故的角度把 Redis 最核心的东西讲清楚。包括它为什么快、五种数据类型到底怎么选、环境怎么搭、踩过的坑以及缓存穿透、击穿、雪崩这些互联网面试和实战都绕不开的话题。这系列内容适合刚入门的同学也适合那些已经用过 Redis 但对内部机制似懂非懂的开发者。看的时候不用急先把基础打牢后面的分布式锁、集群、持久化才有讨论的根基。1. 为什么一个面试八股文常客值得我们从头认识1.1 数据库世界里的“C位”其实是内存很多业务刚开始时数据量不大MySQL 单机完全够用。可一旦用户量上来请求量变大数据库就成了最容易被压垮的那一环。你想想看一张用户表几千万行每次登录都要按user_id去查如果所有请求都压在同一台 MySQL 上磁盘 IO、SQL 解析、行锁竞争这些问题就会接踵而来。哪怕数据库连接池开到 200最终等待的还是磁盘寻址的那几毫秒。这时候最常见的折中方案就是把热点数据提前放到一个读写更快的地方。这个“更快的地方”就是内存而 Redis 就是专门为这个场景设计出来的。和传统数据库相比Redis 的数据主要放在内存中。内存随机访问的速度通常在 100 纳秒级别而磁盘哪怕是 SSD也普遍是微秒甚至毫秒级别。别看这些数字小落到高并发接口上差异会被放大到非常可观的程度。1.2 Redis 不是万能缓存先划清边界正因为 Redis 快、好用很多人容易把它神化遇到什么性能问题都想塞给 Redis。但在我眼里Redis 只适合解决特定类型的问题它有几条很明显的边界。第一Redis 不是你的“数据保险箱”。虽然它支持持久化但设计定位始终是“缓存加强版存储”一般不会把它当作唯一的数据源来用。重要数据在 Redis 和数据库之间必须能容忍一小段时间的不一致。第二内存是有限且昂贵的。一台 8G 内存的服务器去掉系统占用可能只有 5G 能分给 Redis。如果硬塞 50G 数据还没等你看到性能收益内存就已经报警了。第三Redis 的强项是“让读更快”不是“让复杂查询更牛”。你想拿它做多表 JOIN、复杂分组聚合那是自讨苦吃。适合 Redis 的场景是 key-value 访问、排行榜、计数器、分布式锁、过期缓存这类对简单读写要求极高的地方。所以学 Redis 的第一课不是学会怎么用而是先学会不滥用。2. 快不是魔法Redis 单线程与多路复用的底层逻辑2.1 内存访问和磁盘 IO 差着数量级Redis 快首先得益于它把数据放在内存。为了让你有个直观感受我列一组常见硬件访问延迟的数据操作类型耗时数量级CPU 寄存器约 1 ns内存访问约 100 nsSSD 随机读约 0.1 ms机械硬盘随机读约 10 ms内存比 SSD 快三个数量级左右比机械硬盘快五个数量级左右。高并发情况下同样一条查询走 Redis 和走后端 MySQL响应时间可能是几百微秒对几十毫秒的差距。这也是为什么很多架构图里Redis 永远被画在数据库前面那层。它就是用“内存换时间”的典型代表。2.2 单线程怎么抗住十万级并发很多人刚接触 Redis 时最困惑的问题就是Redis 是单线程的怎么还能支撑高并发这里要区分两个概念单线程指的是“执行命令的线程只有一个”但这不代表 Redis 只能同时服务一个客户端。Redis 在操作系统层使用了 I/O 多路复用机制。你可以把它理解成一个大楼前台一个人同时接待几十上百个访客。谁到了就记一下有谁的消息来了就处理谁不用为每个访客单独配一个接待员。在 Linux 上Redis 使用 epoll 事件驱动模型能同时监听大量客户端的连接事件。CPU 只在有事件需要处理时才来干活而不是傻等某个 socket 阻塞住。再加上命令执行本身非常快——简单的GET、SET命令执行时间常常是微秒级别单线程在绝大多数业务场景下根本不会成为瓶颈。2.3 单线程模型同样有代价单线程模型不是没有代价。正因为只有一个线程在跑命令任何一条耗时很长的命令都会把后面所有请求卡住。这就是为什么 Redis 社区和文档一直反复警告比如KEYS *、大范围的SMEMBERS、一次拿几十万元素的LRANGE这些命令在生产环境都属于高危操作。一个不小心Redis 线程就会被卡住几秒钟后面的请求全部阻塞排队。Redis 6.x 开始引入了 I/O 多线程主要用于网络数据读写但命令的实际执行仍然保持单线程。这既提升了高并发下的吞吐能力又保留了单线程模型的简单性。所以理解 Redis 快不能只盯着“内存”两个字还要理解事件驱动的设计思路。只有这样你在后续排查延迟问题时才不会乱套。3. 五大数据类型每一类都有它的脾气3.1 String最朴素也最常用的容器String 是 Redis 里最基本的类型也能承载最多的日常场景。平时我们用SET存一个字符串用GET取出来这是最基础的用法。但它不只是字符串还能通过INCR、DECR做自增自减这个特性天然适合做计数器。常见的应用包括# 缓存用户信息 SET user:10001 张三 EX 300 # 统计接口访问次数 INCR page:view # 简单分布式锁占位配合过期时间 SET lock:order:10001 1 NX EX 10如果你在面试中被问到 Redis 最简单的数据类型能做什么最好别只说“存字符串”。能想到缓存、计数、分布式锁的占位逻辑才算真正理解 String 的应用边界。3.2 List不只是消息队列的雏形List 在 Redis 里是有序的字符串列表底层实现是双向链表或者压缩列表支持头尾操作。常用命令有LPUSH、RPUSH、LPOP、RPOP、LRANGE。它天然适合做“最近 N 条记录”这类场景比如用户消息流、操作日志、文章评论的翻页数据。LPUSH article:10005:comments 评论内容1 LRANGE article:10005:comments 0 -1List 还有阻塞版的BLPOP、BRPOP可以实现简单的生产者消费者模式。但我要提醒一句它毕竟是内存里的线性结构如果列表无限增长会占用非常多内存。生产环境一定要约定好最大长度或者定期裁剪。3.3 Hash对象属性缓存的优选Hash 就相当于一个小型 map适合存储对象的多个字段省掉“把整个对象序列化成一个大字符串”的操作。比如用户资料包含昵称、年龄、城市你可以这么做HSET user:10001 nickname 小王 age 25 city 杭州 HGET user:10001 nickname HGETALL user:10001Hash 的好处是单个字段更新时不用把整个对象读出来再写回。比如只需要更新年龄直接HSET一个字段就行在网络开销和内存占用上都更划算。购物车、用户属性、商品详情等场景都非常适合 Hash。不过要留意如果字段特别多HGETALL也是一个 O(N) 操作用多了会影响性能。3.4 Set去重和集合运算的瑞士军刀Set 是无序、不重复的字符串集合最适合做“必须去重”这件事。它天然支持SADD、SREM、SISMEMBER还支持集合运算SADD user:10001:tags Java Redis MySQL SINTER tag:java tag:redis # 求同时会 Java 和 Redis 的人实际上Set 最常见的应用就是抽奖去重和标签系统。比如某天一个用户把userId添加到活动集合里下次再参加时通过SISMEMBER一判断就能拦截重复参与。但需要注意SMEMBERS会让全量元素数据量大时有阻塞风险。需要遍历集合时建议用SSCAN分批取。3.5 ZSet带权重的有序集合ZSet 和 Set 的区别在于每个元素都带有一个 score 分数Redis 根据分数自动排序。底层实现是跳表加哈希表所以既能保证访问速度又能在排序上表现出色。经典场景就是排行榜ZADD leaderboard:day 100 user_001 ZADD leaderboard:day 80 user_002 ZREVRANGE leaderboard:day 0 2 # 前三名它还可以实现延迟队列把任务执行时间作为 score启动一个线程不断ZRANGEBYSCORE取出到期的任务执行。这类玩法在中间件里很普遍。3.6 类型使用场景速查数据类型底层逻辑常见场景String简单字符串/整数缓存、计数、验证码、锁List双向链表消息流、最近记录、队列Hash小 map对象属性缓存、购物车Set无序去重集合标签、抽奖、共同好友ZSet有序集合排行榜、延迟队列摸清楚这五类类型各自的脾气后续学持久化、集群、分布式锁才不会一头雾水。4. 从下载到启动多平台安装与客户端连接实录4.1 Windows 下的 Redis 安装方式官方 Redis 其实没有正式的 Windows 版本。生产环境中 Windows 用得少但本地开发时很多人还是习惯在 Windows 上直接跑。目前常见的做法是使用第三方编译版本比如 GitHub 上维护的tporadowski/redis它提供了 Windows 下的压缩包和安装包。下载后直接解压在目录里执行redis-server.exe redis.windows.conf这样可以快速启动一个单机实例。如果想把它注册成 Windows 服务可以用项目里自带的工具或使用WinSW之类的方式。另一个选择是 Memurai它是兼容 Redis 协议的 Windows 原生实现适合更偏商业环境的场景。Windows 上有一条要特别注意默认配置里的protected-mode是开启的本地连接没问题但局域网其他机器想连很容易报DENIED Redis is running in protected mode。这时候需要手动修改bind 0.0.0.0并设置密码别图省事不做防护。4.2 macOS 装 Redis 最顺的一条路macOS 上有 Homebrew 的话安装 Redis 是最省心的brew install redis装完之后可以手动启动验证redis-server也可以让 Homebrew 把 Redis 作为后台服务跑brew services start redis配置文件在 Intel 型号上一般是/usr/local/etc/redis.confApple Silicon 上则通常是/opt/homebrew/etc/redis.conf。需要开持久化、设置密码、调整 maxmemory 时都是改这个文件然后重启服务。自测时我遇到过一个小坑使用brew services start redis后明明服务进程在但redis-cli ping偶尔会连不上。这种时候先看日志八成是端口被旧的实例占用或者配置文件里daemonize yes和brew services的启动方式产生了重复杀掉之前的实例就好。4.3 Linux 服务器与 Docker 容器里的 RedisLinux 是 Redis 的主战场。Debian/Ubuntu 系统直接apt update apt install redis-server systemctl enable redis-server systemctl start redis-serverCentOS 则用yum install redis systemctl enable redis systemctl start redis不过系统自带的包版本可能偏旧。如果希望用更新的 Redis 特性建议走 Dockerdocker run -d --name redis \ -p 6379:6379 \ -v /data/redis:/data \ redis:7.2 redis-server --appendonly yes这里敲一下黑板-p 6379:6379表示将容器内 6379 映射到宿主机-v是挂载数据卷不然容器重启后数据就没了。有些人在 Windows 上安装 Docker Desktop 后执行docker search redis会报一个request returned 500 internal server error之类的错误。大多数情况下这不是 Redis 的问题而是 Docker Desktop 的引擎还没启动成功或者引擎内部服务异常。先把 Docker Desktop 完全退出再启动确认 whale 图标稳定再重新拉镜像。拉不下来的情况记得检查镜像加速源是否可用。4.4 可视化工具怎么选命令行用多了其实很顺手但很多初学者刚接触时还是希望看到一个图形界面。目前常见的三款工具我可以给出非常主观的建议工具特点适合人群Redis Desktop Manager老牌工具新版收费旧版免费只是偶尔看 keyAnother Redis Desktop Manager免费、跨平台支持中文大多数日常开发场景Redis Insight官方出品支持分析、命令建议想深入分析性能的开发者我个人更推荐 Another Redis Desktop Manager 作为入门首选。它免费用起来也没啥功能阉割在 Redis 版本特性支持方面更新很快。不过要记住一点可视化工具负责“看”但真正定位线上问题还是要回到redis-cli配合命令比如INFO、SLOWLOG、MONITOR这些。工具看得再花哨也比不过一条准确的命令。4.5 第一次连接前的配置细节启动成功后会有一个默认的空实例这时候我建议先设置密码。改 Redis 配置文件requirepass your_strong_password protected-mode yes然后重启服务。用客户端连接时redis-cli -h 127.0.0.1 -p 6379 -a your_strong_password生产环境里我通常不推荐直接在命令行用-a传密码因为会被 shell 历史记录截获。更好的方式是export REDISCLI_AUTHyour_strong_password redis-cli另外如果你只在本机调试bind 127.0.0.1保持不动会安全很多。一旦需要对外开放端口必须用防火墙限制来源 IP再叠加密码保护。5. Redis-cli 实战常用命令和三个新手容易踩的坑5.1 常用命令的完整闭环安装完、连上之后先用几条命令建立手感# 测试连通性 PING # 写入和读取 SET key:hello world GET key:hello # 设置过期时间 SET key:code 123456 EX 60 TTL key:code # 自增计数器 INCR click:homepage # 查看 key 类型 TYPE key:hello # 获取所有 key生产环境慎用建议用 SCAN SCAN 0 MATCH user:* COUNT 100建议新手把这张清单敲一遍尤其是TTL和SCAN这两个命令。前者能验证过期时间是否生效后者则是生产环境里安全的 key 遍历方式。5.2 坑一键过期时间不知道怎么设很多人一开始会把EXPIRE和SETEX搞混或者干脆不设置过期时间导致 Redis 里堆积大量无效 key。实际上给 key 设置过期时间有三种很常见的方式# 第一种SET 时直接带上过期时间 SET user:10001 张三 EX 300 # 第二种创建之后再设置 SET user:10001 张三 EXPIRE user:10001 300 # 第三种SETEX 命令字符串专用 SETEX user:10001 300 张三如果你把 Redis 当缓存用那么“过期时间”必须成为一种习惯否则内存迟早爆炸。注意PERSIST可以移除过期时间但你别在不需要的时候乱设置。5.3 坑二Java 客户端报 Command timed out用 Spring Boot 自带 Spring Data Redis 时默认客户端是 Lettuce。很多人在某个流量突发时会看到这样的报错RedisCommandTimeoutException: Redis command timed out这个异常字面上是命令超时了但真实原因分为好几种情况第一Redis 服务端正在执行慢命令导致后续请求排队超过客户端等待阈值。解决办法是用SLOWLOG GET查看最近慢命令然后把KEYS等危险操作停掉。第二Lettuce 默认共享连接如果某个耗时操作霸占了连接其他请求会跟着受影响。这时可以考虑给 Lettuce 配置连接池让多个请求使用多个连接而不是死等一个连接。第三Java 应用所在机器到 Redis 的网络延迟过高或者 Redis 主机负载异常CPU 被打满这样即使命令很快也会超时。这种情况需要检查redis-cli --latency和服务器监控。当年我排查最近一次超时最后发现是某个大 key 执行GET都要几百毫秒把所有请求拖死。后来通过拆分大 key、加连接池问题才彻底消失。5.4 坑三序列化导致看不出真数据如果你用过 Spring Boot 的 RedisTemplate可能会发现控制台里存进去的内容变成了\xAC\xED\x00\x05t...这类乱码或者用 Redis Desktop Manager 打开看到一堆二进制。这是 JDK 序列化在作怪。Spring Boot 默认使用 JdkSerializationRedisSerializer存进去的自然不是明文。解决的思路很简单自己配置RedisTemplateString, Object把 value 序列化改成 Jackson 或 Fastjson2key 用 StringRedisSerializer。不过这里我也要补充一句不要只图“能看懂”。如果后续要考虑多语言客户端互读、或大数据量下的序列化性能最好统一约定为 JSON 或其他轻量格式并注意类名冗余。这样既能看到真实数据也能提升存储效率。5.5 提升效率的两个小技巧经验多了之后我总结出两个对日常很实用的技巧。一是用--bigkeys扫描大 key。命令如下redis-cli --bigkeys它会内部用SCAN方式扫描全部 key并给出各类 key 中占内存最大的那一个。对排查内存暴涨很有帮助。二是用管道批量执行命令。如果要发很多条SET别一条一条地发而是cat commands.txt | redis-cli --pipe这在刷测试数据时能省下大量时间。6. 缓存三大痛穿透、击穿、雪崩的前置认知6.1 这三者到底怎么区分很多面试题里都会出现“缓存穿透、缓存击穿、缓存雪崩”。刚接触的人总容易搞混我建议用一个最简单的判断方式问题一句话描述核心破坏点缓存穿透查询的数据根本不存在缓存也就一直没数据每次请求直接打到数据库缓存击穿某个热点 key 过期瞬间大量并发同时去数据库重建单一热点 key 被打崩缓存雪崩大面积 key 同时过期或者 Redis 整体不可用整体缓存失效数据库压力瞬间拉满搞清楚三者差异接下来怎么应对就容易了。6.2 缓存穿透查了一个不存在的东西缓存穿透是指恶意或偶然的大量请求都落在“数据库里原本就没有的数据”上。比如查用户user:999999999缓存里没有数据库里也没有。每次查询都会越过 Redis 直接访问数据库这不是缓存能兜住的。标准解法有两个一是把空值也缓存起来。数据库查不到时在 Redis 里写一个特殊空标记并设置较短过期时间比如 3 到 5 分钟。这样相同请求短时间内就不会再打数据库了。二是用布隆过滤器。把所有可能存在的数据提前初始化到布隆过滤器里请求进来先判断“这个 key 是否存在”不存在就直接返回一个默认结果。布隆过滤器虽然会有一定误判率但在大量数据下能大幅削减无效查询。从我的实践经验看最简单可靠的做法往往是“空值缓存 参数校验”布隆过滤器适合那些数据量极大、活跃穿插很严重的系统。6.3 缓存击穿热点 key 过期瞬间缓存击穿最关键的特征是“热点”和“瞬间”。比如一个秒杀商品的详情页平时不管多少人访问数据都在 Redis 里扛着。但只要这个 key 的过期时间到了刚好又有几万个请求同时拥进来它们发现缓存没命中就全部跑到数据库去查商品详情了。应对思路常见两种第一种互斥锁。当缓存失效时只允许一个线程去数据库加载数据其他线程等待等锁释放后直接读缓存。实现简单但要注意死锁和等待时间。第二种逻辑过期。热点 key 不设置物理过期时间而是在 value 里存储一个逻辑过期时间戳。后台异步线程发现过期后就重新加载数据并更新缓存。访问请求永远读到旧数据但不会压垮数据库。这种方案对延迟敏感的场景特别有效。6.4 缓存雪崩大面积同时失效雪崩的杀伤力通常是三者中最大的因为它不是单一 key 的问题而是整个缓存层的一次集体崩塌。常见原因有两个第一大量 key 设置了相同的过期时间。比如缓存初始化任务把一批热点数据都设成了 1 小时过期到了整点同一秒过期一瞬间所有请求都打到数据库。解决办法是给过期时间加随机值long base 3600; long random ThreadLocalRandom.current().nextLong(600); redisTemplate.opsForValue().set(key, value, base random, TimeUnit.SECONDS);第二Redis 本身挂掉了。比如内存被打满或者机房网络抖动或者主节点宕机后没有及时完成切换。这种情况下即使 key 没过期客户端也拿不到数据。所以高可用设计、哨兵/集群、多级缓存、服务熔断降级都需要提前想好。6.5 面试时如何讲这三座山聊到这块时我建议别只背概念。面试官想听到的是你能表达“不同问题需要不同机制”的思考过程。比如穿透要判断“数据是否存在”击穿要保护“热点 key 重建过程”雪崩要解决“集体失效或整体不可用”。把这三个问题的本质差异说出来再配合你实际用过的空值缓存、逻辑过期、随机过期时间这个话题就过了。7. 学 Redis 的下一步持久化、集群与分布式锁7.1 RDB 和 AOF数据安全的基本盘Redis 虽然以内存为主但它确实提供了持久化机制。初识阶段你至少要搞懂两个词RDB 和 AOF。方式机制优点缺点RDB定期生成数据快照启动恢复快文件紧凑两次快照之间数据可能丢失AOF记录每次写命令数据丢失概率低文件体积大恢复相对慢生产环境里一般根据业务容忍度来选。如果只丢几秒数据没关系用 RDB如果有严格的数据一致性要求就用 AOF或者两者结合。Redis 7 引入的混合持久化也是在尽量平衡这两者。7.2 主从复制与集群从单机到分布式的必经之路单机 Redis 再快也总有内存容量和服务可用性的上限。这时就需要引入主从复制和集群。主从复制的作用是让数据有多个副本主节点负责写从节点负责读。一旦主节点宕机可以人工或通过哨兵自动切换。集群则把数据分片到多个节点上每个节点只保存一部分数据。这样内存可以水平扩展整体吞吐量也更大。不过集群不是“一键开启”就万事大吉。比如 key 路由、跨 slot 操作、批量命令限制、客户端集群模式适配每个都值得单独写一篇。这也是我后续系列要展开的重点。7.3 分布式锁面试高频但别上来就 Redisson分布式锁可能是 Redis 在面试里最常被问的方向之一。很多人一听到分布式锁就背出“SET NX EX”。但真正的分布式锁远没有这么简单你要考虑加锁和设置过期时间是否原子、锁超时了怎么办、业务没执行完怎么续期、删除锁时怎么防止误删别人的锁。如果自己用 Redis 命令实现一般用 Lua 脚本保证“判断和删除”的原子性。更成熟的方案是在 Java 里用 Redisson它内置了看门狗机制会自动续期。这也是为什么我不建议初学者一上来就整 Redisson因为不了解续期机制的话出了问题根本定位不到。7.4 最终的学习路线参考如果你看完这篇准备继续深入我给你一个可以参考的顺序先熟练五种数据类型和常见命令再动手配置持久化然后搭建一主两从加哨兵的环境之后再去研究集群切片和缓存治理最后才碰分布式锁和中间件玩法。高阶的内容不需要一次全部塞进脑子先用透一个再扩展下一个。我自己在长时间实践里的体会是Redis 最值得投入时间去理解的部分不是那些命令的参数而是它“单线程 事件驱动”的设计思想。把这个底层逻辑刻进脑子里很多困惑都会迎刃而解。
返回列表