欢迎光临
我们一直在努力

Redis 01 · 开篇:Redis 为什么快

凌晨零点,秒杀活动刚开场,商品详情页的请求量瞬间翻了几十倍。如果每个请求都去 MySQL 查商品、价格和活动信息,数据库很快就会被大量重复查询压住。于是我们把这些热点数据放进 Redis:请求先查 Redis,命中就直接返回,没命中才回源数据库。看起来只是多加了一层缓存,整个系统能承受的流量却完全不是一个量级。

但“Redis 快,因为它在内存里”只答对了第一层。内存数据库不止 Redis 一个,为什么 Redis 能用一个事件循环处理大量连接?大家常说的“单线程”究竟单在哪,Redis 6 引入 I/O 线程后它还是不是单线程?既然命令串行执行,为什么一个 KEYS * 或大 Key 删除又可能拖慢所有请求?“Redis 为什么快”也是面试里最经典的开场题之一,只答“纯内存 + 单线程”通常很快就会被追问到这些细节。

这篇是 Redis 系列的开篇。我们先把 Redis 放回真实的后端架构里,弄清它解决什么问题;再从内存、数据结构、命令执行模型、I/O 多路复用和网络往返五个层次拆解它为什么快;然后划清 Redis 的能力边界,最后给出整个系列的学习地图。全系列统一使用电商场景:商品详情缓存、秒杀库存、用户会话、排行榜和订单消息。

目录

  • 为什么数据库前面需要一层 Redis
  • Redis 到底是什么:不只是缓存
  • Redis 为什么快:五层原因叠加
  • “单线程”到底单在哪
  • Redis 适合解决哪些问题
  • Redis 不适合做什么
  • 生产使用前必须想清楚的七件事
  • 全系列地图:接下来要拆什么
  • 一、为什么数据库前面需要一层 Redis

    先从最常见的商品详情接口说起。没有缓存时,一次请求的路径大致是:

    客户端 → 应用服务 → MySQL → 磁盘/Buffer Pool → 应用服务 → 客户端

    商品名称、主图、价格和活动标签可能几分钟才变化一次,却会被成千上万个请求反复查询。MySQL 当然有 Buffer Pool,热点数据页也可能已经在内存里,但一次查询仍然要经过连接管理、SQL 解析、优化、执行、索引查找、行记录组装等流程。更重要的是,数据库还承担着事务、约束、持久化和复杂查询,它不应该把大部分资源浪费在重复回答同一个问题上。

    Redis 加进来以后,请求路径变成:

    命中
    客户端 → 应用服务 ───────→ Redis → 返回

    │ 未命中
    └────────→ MySQL → 回填 Redis → 返回

    对应的业务代码就是经典的 Cache Aside(旁路缓存) 模式:

    public Product getProduct(long productId) {
    String key = "product:detail:" + productId;

    Product cached = redis.get(key);
    if (cached != null) {
    return cached; // 命中:直接返回
    }

    Product product = productRepository.findById(productId);
    if (product != null) {
    redis.set(key, product, Duration.ofMinutes(10));
    }
    return product;
    }

    这层缓存换来的不只是“单次请求更快”,而是三项系统级收益:

  • 降低数据库负载。 相同数据不再被反复查询,数据库把资源留给真正需要事务和复杂查询的请求。
  • 缩短读取链路。 Redis 的常用操作直接在内存数据结构上完成,不需要走完整的关系型查询执行链。
  • 吸收流量尖峰。 热点读请求由 Redis 扛住,避免瞬时流量直接撞到数据库。
  • 但要注意,缓存只是把压力挡在了数据库前面,并没有让压力消失。当大量请求同时查一个刚过期的热点 Key,或者 Redis 自己不可用时,流量仍可能全部回到数据库。《Redis 11 · 缓存问题:穿透、击穿与雪崩》和《Redis 09 · 高可用:Sentinel 如何发现故障并完成切主》,解决的就是“这层挡板失效后怎么办”。

    二、Redis 到底是什么:不只是缓存

    Redis 官方把自己称为 data structure server(数据结构服务器)。这个定义比“内存缓存”更准确:客户端不是只能存取一段字符串,而是可以直接操作服务器里的 String、Hash、List、Set、Sorted Set、Stream 等数据结构。

    SET product:stock:1001 500
    HSET user:42 name "Alice" level "VIP"
    SADD product:1001:buyers 42 73 98
    ZADD sales:rank 9520 product:1001
    XADD order:events * orderId 90001 status created

    五条命令表达的是五种完全不同的业务语义:库存是一个数值,用户资料是一组字段,购买用户需要去重,销量榜需要按分数排序,订单事件则是一条可以持续追加和消费的消息流。Redis 的价值在于:这些常用数据模型已经被做成了原生命令,而且单条命令通常具备原子性。

    因此,Redis 在系统里可以扮演多种角色:

    角色典型场景需要额外考虑什么
    缓存 商品详情、配置、查询结果 过期、回源、一致性、缓存失效时的降级
    高速状态存储 会话、验证码、限流计数 内存容量、TTL、持久化要求
    实时数据结构 排行榜、集合关系、在线状态 命令复杂度、Key 和成员数量
    消息与事件流 Stream、消费者组 消费确认、积压、故障恢复
    分布式协调 锁、幂等标记、租约 超时、安全释放、主从切换与一致性边界

    Redis 可以是缓存,也可以承载业务状态,但两种角色的正确性要求完全不同。 缓存丢了通常可以从数据库重建;如果 Redis 是唯一数据源,持久化、复制、高可用和备份就不再是“性能选项”,而是数据能不能找回来的底线。设计时必须先回答一句:这份数据丢了,能不能重建?

    版本提示:本系列聚焦 Redis 7.x 与 Redis 8.x 共享的核心机制。Redis 8 把更多搜索、JSON、概率数据结构和向量能力纳入统一发行体系,但 String、Hash、持久化、复制、Sentinel、Cluster 等主线机制仍然是理解 Redis 的地基。涉及具体配置和行为变化时会按版本单独标注。

    三、Redis 为什么快:五层原因叠加

    Redis 的性能不是某一个”黑科技”换来的,而是五层设计叠加的结果。把它们串起来,才能看清一次命令为什么能走得这么短。

    在这里插入图片描述

    3.1 数据主要在内存里

    这是最直观的一层。Redis 的工作数据主要驻留在内存中,常规读写不需要先随机访问磁盘。内存访问的延迟远低于磁盘和远程存储,而且 Redis 对象可以直接被命令处理器操作,执行路径很短。

    但“内存数据库”不等于“完全不碰磁盘”。启用 RDB 或 AOF 后,Redis 仍然会生成快照、追加日志、执行 fsync 和重写文件,只是前台命令处理与大部分持久化工作被尽量解耦。配置不当、磁盘抖动或 fork 压力仍可能反过来影响延迟,第 5 篇会专门展开。

    3.2 数据结构是为具体操作设计的

    如果只把所有数据保存成字符串,服务端就得反复解析、遍历和重建。Redis 直接提供 Hash、Set、Sorted Set 等结构,让许多业务操作落到合适的算法上:

    • 判断一个用户是否领过券,可以用 Set 的成员判断,而不是查出整个用户列表再遍历;
    • 维护销量排行榜,可以用 Sorted Set 的有序索引,而不是每次读取所有商品再排序;
    • 对库存做增减,可以直接使用 INCRBY,而不是客户端先读、计算后再写回。

    更深一层,同一种 Redis 类型还可能根据数据规模使用不同的内部编码:小 Hash 和大 Hash 的存储方式不一定相同,List、Set、Sorted Set 背后也不是永远只有一种结构。《Redis 02 · 数据结构:String 到 Stream 怎么选》和《Redis 03 · 底层原理:RedisObject 与核心编码》,会把“对外数据类型”和“内部编码”分开拆解。

    3.3 核心命令串行执行,避免大范围锁竞争

    Redis 的核心命令执行路径长期采用单线程串行模型:一条命令执行完,再处理下一条。这样做的直接收益是,命令处理器不需要让大量线程围着同一份字典和对象频繁加锁,也没有复杂的共享状态竞争和线程切换。

    串行不代表处理能力一定低。对大量短小的内存操作来说,真正的瓶颈往往先出现在网络、内存带宽或某条慢命令上,而不是“少开了几个执行线程”。Redis 选择的是:先让单条命令的执行路径足够短、足够可预测,再通过复制、分片和集群把容量横向扩出去。

    这项设计同时带来一个重要约束:一条慢命令会挡住后面的所有命令。 对百万级 Key 执行 KEYS *、遍历超大集合、删除巨型 Key,或者运行耗时很长的 Lua 脚本,都可能把事件循环卡住。单线程既是 Redis 简洁高效的来源,也是使用者必须敬畏的边界。

    3.4 I/O 多路复用让一个线程管理大量连接

    服务器同时维护成千上万个连接,不意味着必须给每个连接分配一个线程。Redis 使用事件驱动模型和操作系统提供的 I/O 多路复用能力,等待“哪些 Socket 已经可读或可写”。

    可以把它理解成一个取号屏:事件循环不需要挨个询问每个连接“你有数据了吗”,而是让操作系统一次告诉它“这批连接已经就绪”。事件循环只处理真正有事可做的连接,避免大量线程阻塞等待网络数据,也减少上下文切换。

    很多客户端连接


    I/O 多路复用:找出已经就绪的 Socket


    事件循环:读取请求 → 解析命令 → 执行 → 写回响应

    这里要分清两件事:I/O 多路复用解决的是“如何高效等待大量连接”,单线程命令执行解决的是“如何简单地操作共享数据”。 它们经常被放在一句话里背,但解决的不是同一个问题。

    3.5 协议简单,Pipeline 还能摊薄网络往返

    一次 Redis 命令不仅有服务端执行时间,还有网络往返时间(RTT)。如果客户端发一条、等一条,再发下一条,即使命令本身很快,大量时间也可能耗在来回等待网络上。

    Pipeline 允许客户端连续发送多条命令,再批量读取响应:

    普通方式:请求1 → 响应1 → 请求2 → 响应2 → 请求3 → 响应3
    Pipeline: 请求1、请求2、请求3 → 响应1、响应2、响应3

    它没有把三条命令变成一个原子事务,也不会改变命令在服务端的执行语义;它优化的是网络往返和系统调用次数。所以评估 Redis 性能时,不能只看一条“每秒多少请求”的宣传数字,还要看连接数、数据大小、命令类型、是否 Pipeline、客户端和服务端距离等测试条件。

    把五层原因合起来就是:

    内存缩短数据访问路径,专用数据结构降低计算成本,串行命令执行避开复杂锁竞争,I/O 多路复用高效管理连接,Pipeline 摊薄网络往返。 Redis 快,是整条链路都在做减法,不是只靠“内存”两个字。

    四、“单线程”到底单在哪

    “Redis 是单线程的”是最常见、也最容易把人带偏的一句话。准确说法应该是:Redis 的核心命令执行与数据结构操作长期以主线程串行完成,但 Redis 整个进程并不是只有一个线程。

    Redis 还会使用后台线程或子进程处理其他工作,例如异步释放对象、关闭文件、AOF 刷盘,以及生成 RDB、执行 AOF 重写等。Redis 6 又引入了可配置的 I/O 线程,用于分担部分网络读写工作;命令对核心数据结构的执行仍然保持主线程串行这个基本模型。

    工作主要执行位置是否意味着命令并行修改同一份数据
    命令解析与核心数据结构操作 主事件线程 否,核心路径仍以串行为主
    Socket 读写 主线程,可配置 I/O 线程分担部分工作 否,I/O 并行不等于命令执行并行
    异步释放、部分文件处理 后台线程 不直接并行执行普通业务命令
    RDB、AOF 重写 通常由子进程完成主要工作 与主进程并行,但会带来内存和系统资源压力

    这也解释了两个看似矛盾的现象:

    • Redis 能维护大量连接,因为等待网络事件不需要“一连接一线程”;
    • Redis 又怕慢命令,因为核心执行线程一旦被占住,其他已经就绪的请求也只能排队。

    因此,判断一个命令危险不危险,不能只看它的平均耗时,还要看复杂度和数据规模。HGET 通常很轻,但对一个超大 Hash 执行全量读取就可能产生巨量响应;DEL 虽然只传入 Key,删除集合类对象时还要释放内部元素,实际成本会随对象规模上升;Lua 脚本具有原子执行优势,却也会在运行期间挡住其他命令。

    一句话校准心智模型:Redis 不是“靠单线程所以快”,而是“核心执行路径足够短,所以选择串行来换简单和可预测”;一旦单条路径变长,串行的代价就会立刻暴露。

    五、Redis 适合解决哪些问题

    理解了执行模型,再看选型就不会停留在“Redis 能存什么”,而会变成“哪种数据和访问模式适合放进 Redis”。

    5.1 高频读取的热点缓存

    商品详情、活动配置、店铺信息、权限配置都属于典型的读多写少数据。它们可以从数据库重建,放进 Redis 能显著减少重复查询。

    SET product:detail:1001 '{"name":"机械键盘","price":39900}' EX 600

    5.2 有明确过期时间的短期状态

    验证码、登录会话、幂等标记天然带生命周期,Redis 的 TTL 正好把“数据”和“什么时候失效”放在一起管理。

    SET login:code:13800000000 482913 EX 300
    SET order:idempotent:req-8f31 1 NX EX 60

    5.3 原子计数与限流

    浏览量、点赞数、接口窗口计数经常需要并发更新。Redis 的单条增减命令是原子的,比客户端“先读再加一再写回”可靠。

    INCR product:view:1001
    INCR rate:login:192.0.2.10
    EXPIRE rate:login:192.0.2.10 60

    上面的两条限流命令并不是一个不可分割的整体,生产实现需要用 Lua、事务或专门算法把操作组合起来。《Redis 07 · 原子操作:事务、Pipeline、Lua 与 Functions》会解释“单条命令原子”和“多条命令原子”之间的差别。

    5.4 集合关系与实时排行

    Set 适合去重、交并集和成员判断;Sorted Set 适合排行榜、优先队列和按分数范围查询。

    SADD coupon:88:users 42
    SISMEMBER coupon:88:users 42

    ZINCRBY sales:rank 1 product:1001
    ZRANGE sales:rank 0 9 REV WITHSCORES

    5.5 消息流与轻量级异步处理

    Redis Stream 提供消息 ID、消费者组、待确认列表等能力,适合事件通知和规模可控的异步任务。但它不是“用了就等于专业消息队列”,消息积压、磁盘容量、跨地域复制和运维生态仍要按业务要求评估。

    这些场景看起来跨度很大,背后却有同一个共同点:访问路径明确、单次操作短、数据规模可以被内存约束。 反过来,只要这三个前提不成立,就该开始审视 Redis 的边界。

    六、Redis 不适合做什么

    知道一个工具什么时候不该用,比会背它的命令更重要。Redis 至少有五条清晰边界。

    第一,不适合无边界地堆放低价值数据。 内存成本高于磁盘,数据只进不出、没有 TTL 和容量规划,迟早会触发淘汰或 OOM。Redis 是高速工作区,不是无限仓库。

    第二,不适合复杂关联查询。 Redis 擅长按 Key 或特定数据结构访问,不擅长临时拼出多表 JOIN、任意条件筛选和复杂聚合。如果业务查询模式经常变化,关系型数据库或搜索引擎通常更合适。

    第三,不适合直接照搬数据库事务心智。 Redis 的 MULTI/EXEC 不提供关系型数据库那套完整的回滚与隔离语义。跨多个实体的强一致业务仍应由数据库事务、幂等和补偿机制兜底。

    第四,不适合执行不可控的长任务。 全量扫描、超大范围集合运算、长 Lua 脚本都会占住核心执行线程。能分页就不要一次取完,能异步释放就不要同步删除巨型对象,线上禁止把 KEYS * 当查询接口使用。

    第五,不能在没有持久化和高可用设计时承担唯一数据源。 默认配置、单实例部署和“内存里有一份”都不是可靠性方案。即使打开 AOF,刷盘策略也意味着性能与丢失窗口的权衡;即使有主从复制,异步复制也存在故障切换时丢失最新写入的可能。

    选型时可以用四个问题快速判断:

  • 访问是不是围绕明确的 Key 或固定数据结构?
  • 工作集能不能被内存容量约束住?
  • 单次操作能不能保持足够短?
  • 数据丢失后能否重建;不能的话,持久化和高可用是否匹配业务目标?
  • 四个问题里有两个答不上来,就不该因为“Redis 快”而强行上 Redis。

    七、生产使用前必须想清楚的七件事

    Redis 接入代码很简单,生产稳定性却取决于代码之外的约束。下面七项最好在上线前写进设计评审。

    7.1 Key 要有命名规范

    推荐使用“业务域:实体:标识”的层级形式:

    product:detail:1001
    product:stock:1001
    user:session:8f31c2
    order:idempotent:req-8f31

    统一命名方便统计、迁移、排障和设置治理规则。Key 过长会浪费内存,过短又会失去可读性,关键是稳定且能表达归属。

    7.2 缓存必须有容量边界

    哪些 Key 必须设置 TTL,实例最大内存是多少,达到上限采用什么淘汰策略,都不能等内存告警后才想。没有 TTL 的缓存,本质上是在把 Redis 当数据库使用。

    7.3 超时要短,失败要能降级

    应用访问 Redis 也是一次网络调用。连接超时、命令超时、连接池等待时间都要设置上限,避免 Redis 变慢后把应用线程全部拖住。缓存不可用时,是回源数据库、返回旧值、限流,还是直接失败,要提前设计。

    7.4 防止缓存失效把数据库打穿

    热点 Key 重建要不要互斥,空结果要不要缓存,TTL 是否需要加入随机抖动,回源并发是否有限制——这些措施分别对应击穿、穿透和雪崩,不能混成一句“加个缓存就好了”。

    7.5 控制单条命令的最坏执行时间

    评审 Redis 命令时,要同时看时间复杂度和集合规模。禁止线上使用 KEYS 做全量查询;使用 SCAN 也不代表没有成本,它只是把遍历拆成多次渐进完成:

    KEYS product:* # ✗ 一次遍历整个键空间,可能长时间阻塞主线程
    SCAN 0 MATCH product:* COUNT 100 # ✓ 渐进遍历;仍要控制调用频率并正确处理游标

    对大 Key 的读取、删除和迁移同样要单独设计。

    7.6 监控平均值,也要监控尾延迟

    平均 1 毫秒不能说明没有问题,少量几十毫秒甚至秒级抖动就可能拖垮调用方。至少要关注命令延迟分布、慢日志、命中率、内存、淘汰量、连接数、阻塞客户端、复制延迟和持久化状态。

    7.7 先定义数据角色,再选择持久化与拓扑

    纯缓存可以容忍丢失并回源,核心状态则需要评估 RDB/AOF、主从、Sentinel 或 Cluster、备份恢复目标。不要先搭一套“标准 Redis 集群”,再倒推业务为什么需要它;拓扑应该由容量、可用性和数据丢失目标推导出来。

    到这里,我们已经从“为什么引入 Redis”走到了“怎样避免把它用成新的故障源”。最后把后续 13 篇放回一张地图里,看看这些问题会按什么顺序展开。

    八、全系列地图:接下来要拆什么

    这套 Redis 专栏共 14 篇,从单机内部一路讲到分布式与生产排障:

    篇目主题要回答的核心问题
    01 开篇:Redis 为什么快 Redis 解决什么问题,性能和边界从哪来
    02 数据结构 String、Hash、List、Set、Sorted Set、Stream 怎么选
    03 底层编码 SDS、dict、listpack、quicklist、skiplist 如何支撑上层类型
    04 执行模型 一条命令怎样经过 Socket、事件循环和 I/O 多路复用
    05 持久化 RDB、AOF、重写和刷盘策略怎么权衡
    06 内存管理 过期删除、淘汰策略、大 Key 和内存碎片怎么治理
    07 原子操作 事务、Pipeline、Lua 和 Functions 各自解决什么问题
    08 主从复制 全量同步、增量同步和复制延迟如何产生
    09 Sentinel 如何判断主节点故障、选主并完成切换
    10 Cluster 16384 个槽如何分片、路由、迁移和扩缩容
    11 缓存三大问题 穿透、击穿、雪崩分别怎么发生、怎么治理
    12 缓存一致性 数据库与缓存双写为什么会乱,怎样做到可接受的一致性
    13 分布式锁 SET NX PX、续期、Redlock 和 fencing token 的边界
    14 调优与排障 延迟、内存、热 Key、持久化抖动和复制故障怎么定位

    阅读顺序也对应一条清晰主线:先认识 Redis 提供的数据模型,再进入单机内部的执行、持久化和内存机制;随后把一台 Redis 扩展成主从、Sentinel 和 Cluster;最后处理缓存一致性、分布式锁与生产故障。


    这一篇先把 Redis 放回了它应有的位置。它不是“比 MySQL 更快的数据库”,也不是“接上就能解决高并发”的银弹,而是一台围绕内存数据结构和短命令执行路径设计的服务器。用它挡住热点读取、管理短期状态、做原子计数和实时集合,往往非常顺手;让它承担复杂查询、无边界存储或未经设计的强一致核心数据,就会迅速撞上边界。

    带走三句话:① Redis 快不是只因为内存,而是内存、专用数据结构、串行命令执行、I/O 多路复用和 Pipeline 共同缩短了整条链路;② “Redis 单线程”只概括核心命令执行路径,整个进程还有 I/O、持久化和后台任务,但慢命令仍会阻塞其他请求;③ 接入 Redis 前先定义数据角色、容量上限、失效回源和故障降级,否则缓存只是把数据库问题推迟到一次更猛烈的流量冲击里。

    下一篇,我们从 Redis 最直观、也最容易学成“命令大全”的部分开始——《Redis 02 · 数据结构:String 到 Stream 怎么选》。不按 API 罗列,而是从业务访问模式出发,看看 String、Hash、List、Set、Sorted Set 和 Stream 各自为了解决什么问题而存在,以及同一个需求为什么选错结构会让复杂度和内存占用完全不同。

    赞(0)
    未经允许不得转载:171主机测评 » Redis 01 · 开篇:Redis 为什么快
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址