文章目录
- 前言
- [一、 介绍](#一、 介绍)
-
- [1. 简介](#1. 简介)
- [2. 核心特点](#2. 核心特点)
- [二、 应用场景](#二、 应用场景)
-
- [1. 应用场景](#1. 应用场景)
- [2. 数据类型作用场景](#2. 数据类型作用场景)
- [三、 性能特性](#三、 性能特性)
-
- [1. 内存](#1. 内存)
- [2. 高性能数据结构](#2. 高性能数据结构)
- [3. 单线程、多路复用](#3. 单线程、多路复用)
- [四、 异步持久化机制](#四、 异步持久化机制)
-
- [1. RDB(Redis Database)](#1. RDB(Redis Database))
- [2. AOF(Append-Only File)](#2. AOF(Append-Only File))
- [3. 持久化机制](#3. 持久化机制)
- [五、 缓存问题](#五、 缓存问题)
-
- [1. 缓存击穿](#1. 缓存击穿)
前言
Redis
Redis 是一个开源的、高性能的内存键值数据库,以速度极快、支持丰富的数据结构而闻名,是现代应用架构中非常流行的组件。
一、 介绍
1. 简介
Redis 是一个开源的、高性能的 内存键值数据库,Redis 的键值对中的 key 就是字符串对象,而 value 就是指Redis的数据类型,可以是String,也可以是List、Hash、Set、 Zset 的数据类型。Redis通常被用作数据库、缓存、消息中间件和实时数据处理引擎。它以速度极快、支持丰富的数据结构而闻名,是现代应用架构中非常流行的组件。
2. 核心特点
- 内存存储 (In-Memory):
- 数据主要存储在内存 (RAM) 中,这是 Redis 速度惊人的根本原因(读写操作通常在微秒级别)。
- 它也提供可选的持久化机制,可以将数据异步或同步保存到磁盘,防止服务器重启后数据丢失。
- 丰富的数据结构 (Data Structures):
不仅仅是简单的 Key-Value 字符串!Redis 支持多种高级数据结构:
- Strings: 最基本类型,可以存储文本、数字、二进制数据(如图片片段)。
- Lists: 有序的元素集合,可在头部或尾部插入/删除,适合实现队列、栈、时间线。
- Sets: 无序的唯一元素集合,支持交集、并集、差集等操作,适合标签、共同好友。
- Sorted Sets (ZSets): 带分数的有序唯一元素集合,元素按分数排序,完美适用于排行榜、优先级队列。
- Hashes: 存储字段-值对的集合,非常适合表示对象(如用户信息:field: name, value: "Alice"; field: age, value: 30)。
- Bitmaps / HyperLogLogs / Geospatial Indexes: 特殊用途的数据结构,用于位操作、基数统计(去重计数)、地理位置计算等。
- 高性能与低延迟:
- 内存访问 + 单线程架构 (核心命令执行是单线程,避免了锁竞争) + 高效的网络 I/O 模型 (epoll/kqueue) 使其拥有极高的吞吐量和极低的延迟。
- 持久化 (Persistence):
- RDB (Redis Database File): 在指定时间间隔生成整个数据集的内存快照。恢复快,文件紧凑。适合备份和灾难恢复。
- AOF (Append-Only File): 记录所有修改数据库状态的命令。更安全(最多丢失一秒数据),文件可读性强,但文件通常更大,恢复可能比 RDB 慢。可以同时开启或选择其一。
- 原子操作与事务:
- 所有单条命令的执行都是原子的。
- 支持简单的事务 (MULTI/EXEC),可以将一组命令打包执行(但不支持回滚 - 命令语法错误会导致整个事务不执行,运行时错误不影响其他命令执行)。
- Lua 脚本: 可以执行复杂的、需要多个命令且保证原子性的操作。
- 还有像发布订阅、集群架构等特性
二、 应用场景
1. 应用场景
-
缓存 (Caching): 最常见的用途。将频繁访问的热点数据(如数据库查询结果、页面片段、会话信息)存储在 Redis 中,显著减轻后端数据库压力,提升应用响应速度。
-
会话存储 (Session Store): 存储用户会话信息,易于在多服务器或微服务架构中实现会话共享。
-
排行榜/计数器 (Leaderboards / Counters): 利用 Sorted Sets 可以非常高效地实现实时排行榜。利用 INCR 等命令实现高并发下的计数器(如点赞数、浏览量)。
-
实时系统 (Real-time Systems):
- 消息队列 (Message Queue): 利用 Lists 或 Streams (一种更强大的持久化消息队列数据结构) 实现简单的消息队列。
- 实时分析: 处理实时事件流(如用户活动跟踪、监控数据)。
-
地理空间应用 (Geospatial): 存储地理位置坐标,执行附近位置查询、距离计算等。
-
速率限制 (Rate Limiting): 限制用户 API 调用频率或操作次数。
-
分布式锁 (Distributed Lock): 利用 Redis 的原子操作实现简单的跨进程/跨机器的互斥锁。
2. 数据类型作用场景
-
String可以用来做缓存、计数器、限流、分布式锁、分布式Session等。
-
Hash可以用来存储复杂对象。List可以用来做消息队列、排行榜、计数器、最近访问记录等。
-
Set可以用来做标签系统、好友关系、共同好友、排名系统、订阅关系等。
-
Zset可以用来做排行榜、最近访问记录、计数器、好友关系等。
-
Geo可以用来做位置服务、物流配送、电商推荐、游戏地图等。
-
HyperLogLog可以用来做用户去重、网站UV统计、广告点击统计、分布式计算等。
-
Bitmaps可以用来做在线用户数统计、黑白名单统计、布隆过滤器等。
三、 性能特性
Redis 可以达到极高的性能(官方测试读速度约 11 万次 / 秒,写速度约 8 万次 / 秒)!
1. 内存
基于内存操作
Redis将所有数据存储在内存中,避免了传统数据库的磁盘I/O瓶颈,内存的读写速度远高于磁盘,这使得Redis能够实现超高的响应速度。
内存的读写速度,和,磁盘读写速度的对比
- 最快情况下, 固态 硬盘 速度,大致是 内存速度的 百分之一,
- 最慢情况下, 机械 硬盘 速度,大致是 内存速度的 万分之一,
内存读写速度可以达到每秒数百GB,在微秒级别,而磁盘(特别机械硬盘) 读写速度通常只有数十MB,在毫秒级别, 是数千倍的差距。
对比与传统的关系型数据库比如说MySQL,需要从磁盘加载数据到内存缓冲区才能操作,Redis不需要这个步骤,就避免了磁盘 I/O 的延迟。
2. 高性能数据结构
高效的数据结构
Redis向我们用户提供了value为string, list, hash, set, zset五种基本数据类型来使用,还有几种高级的数据结构例如geo, bitmap, hyperloglog。本文只讨论基本的数据类型了。
本节分析一下底层实现,这些数据类型底层实现有如下这么些:
-
sds( 简单动态字符串)
-
ziplist(压缩列表)
-
linkedlist(双端链表)
-
hashtable(字典)
-
skiplist(跳表)
这些底层结构能够在内存中高效地存储和操作数据,为Redis的快速性能提供了坚实的基础。

3. 单线程、多路复用
- 单线程
Redis 的单线程设计是其高性能的核心支柱,但它并非字面意义上的"只有一个线程"。Redis 的工作线程(主线程)串行处理所有客户端命令,但存在辅助线程处理异步任务。
为什么坚持核心单线程呢?这可能是一种取舍,如果是多线程的话,需要考虑 共享数据结构需加锁(如 Mutex)、线程上下文切换需要消耗 CPU 周期、线程安全编程难度高一些。
如果采用单线程的话,天然无锁,可以保证操作的原子性;因为是单线程嘛,所以就没有县城上下文切换了;代码就更简单易懂了,也容易维护嘛。
单线程肯定会有不足的:比如说执行了慢命令,(如 KEYS *)会阻塞所有后续请求;单线程无法充分利用多核cpu。
所以可以得出:CPU 并不是Redis的瓶颈 → 避免锁/切换开销 → 单线程更高效
因为Redis使用内存存储数据,所以数据访问非常迅速,不会成为性能瓶颈。此外,Redis的数据操作大多数都是简单的键值对操作,不包含复杂计算和逻辑,因而CPU开销很小。相反,Redis的瓶颈在于内存的容量和网络的带宽,这些问题无法通过增加CPU核心来解决。
Redis 6.x开始引入了多线程, 但是多线程仅仅是在 处理网络IO,Redis 核心命令执行依然是单线程,确保性能和一致性。
- 多路复用
那么,现在说一下Redis 的网络 I/O 处理,采用 事件驱动的 Reactor 模式,结合 I/O 多路复用技术 和 渐进式多线程优化:
Redis 采用 Reactor 模式作为网络模型的基础架构,在这一模式下,Redis 通过一个主事件循环(Event Loop) 持续监听并分发网络事件。
首先,事件分发器:基于 I/O 多路复用技术(如 Linux 的 epoll)实现,负责监控所有客户端连接的 Socket 文件描述符(FD);
然后,事件处理器:为不同事件(如连接、读、写)绑定对应的回调函数。例如:连接事件触发 accept 处理器,创建新客户端连接;读事件触发命令请求处理器,解析并执行 Redis 命令;写事件触发响应回复处理器,将结果返回客户端。
通过 I/O 多路复用,单一线程可同时监听数万个 Socket,仅当 Socket 真正发生读写事件时才触发回调,避免了线程空转和阻塞,这种设计使得 Redis 在单线程下仍能高效处理高并发请求,尤其适合内存操作快速完成的场景
尽管单线程模型简化了数据一致性管理,但网络 I/O 瓶颈在高并发场景下逐渐显现。为此,Redis 从 6.0 版本开始引入渐进式多线程优化:新增的 I/O 线程仅负责网络数据的读取与发送,而命令解析与数据操作仍由主线程单线程执行,这种设计确保了核心数据操作的原子性,避免多线程竞争。

主要流程:
- 主线程接收新连接,将 Socket 分配至全局队列;
- I/O 线程池并行读取请求数据并解析为命令(若启用 io-threads-do-reads),或并行发送响应结果;
- 主线程按顺序执行所有命令,再将结果写入缓冲区供 I/O 线程发送
【用户可通过 io-threads 参数设置线程数(建议为 CPU 核数的 1~1.5 倍),并通过 io-threads-do-reads 控制是否启用读并行化,在高并发网络场景下,此优化可提升吞吐量 40%~50%,同时避免核心逻辑的锁竞争】
四、 异步持久化机制
单机的Redis速度已经独步天下了,倘若遇到系统错误,导致Redis应用程序中断了,由于数据是在内存里面,那不就全部丢失了吗?这就需要 说到Redis的持久化机制了。
Redis的持久化机制包含RDB和AOF两种方式,其核心设计原则是最大化性能,因此持久化操作本质是异步的(主线程非阻塞);
1. RDB(Redis Database)
- 机制
它是将内存中的数据以二进制格式生成全量快照(Snapshot),写入 dump.rdb 文件,通过 fork 子进程完成持久化,主进程继续处理请求,仅 fork 操作短暂阻塞(约 1-100ms)。
- 触发
-
自动触发(默认异步),在配置文件里面配置 save m n:如 save 900 1 表示 900 秒内至少 1 次键修改时触发;从节点全量复制时自动触发。
-
手动触发,就需要命令的形式了,SAVE:同步阻塞主线程,生成快照(不推荐生产环境使用);BGSAVE:异步生成快照,通过子进程完成(默认方式)。
- 影响
这种方式快速生成快照,对性能影响较小,此外呢,文件体积较小,适合备份和灾难恢复。但是,如果是 Redis 服务器在两次快照之间崩溃,可能会丢失部分数据

2. AOF(Append-Only File)
- 机制
它是将 Redis 执行的所有写命令(如SET、INCR)追加到日志文件(默认名为appendonly.aof),同时在AOF 文件过大时,通过BGREWRITEAOF命令对日志进行瘦身(合并重复命令):创建一个新的AOF文件来替代现有的AOF文件,新旧两个文件所保存的数据库状态是相同的,但 新 AOF文件 去掉 老的 冗余命令,通常体积会较旧AOF文件小很多,达到压缩 AOF 文件体积的目的。
- 触发
重写瘦身触发方式:
-
手动:BGREWRITEAOF 命令。
-
自动:满足 auto-aof-rewrite-percentage(默认 100%)和 auto-aof-rewrite-min-size(默认 64MB)条件时触发。
- 影响
这个重写操作是异步的吗?Redis是利用子线程进行复制拷贝,总结来说就是一个拷贝,两处日志。复制过程 不会卡主线程,整个过程是让子进程干活,主线程继续服务用户。
两处日志分别指:
【1】主线程正常处理新操作,把命令记录到 AOF 缓冲区 ,异步刷新到 原来的AOF日志里(比如每秒刷一次磁盘)。
【2】同时,新操作还会被额外记录到 AOF重做缓冲区,等小弟整理完旧日志后,这些新操作会被追加到新的AOF文件里,保证数据不丢失
那么AOF这种方式的异步点在哪里呢?
-
写操作:主线程将命令追加到 aof_buf 内存缓冲区(非阻塞)
-
刷盘策略:根据配置异步刷盘
- appendfsync always # 同步写盘(强一致,性能差)
- appendfsync everysec # 每秒异步刷盘(推荐-默认)
- appendfsync no # 依赖操作系统刷盘

这种持久化方式数据安全性高(最多丢失 1 秒数据),支持命令级恢复,但是文件体积大,恢复速度慢,因为是很多命令嘛。
3. 持久化机制
那么Redis是采用哪种方式呢?是同时开启 RDB 和 AOF,利用 RDB 的快速恢复能力和 AOF 的数据安全性,重启时,优先加载RDB恢复数据,再重放AOF增量操作。
五、 缓存问题
缓存问题Redis作为缓存的时候,会发生一些经典问题。下面就来分析一下:
-
缓存击穿: 单个热点 Key 失效瞬间遭遇 高并发。关键:单个热点 Key 失效 + 高并发。仓库门口堆放的一件特别抢手的热门商品刚好卖完(缓存失效),一大群等着买它的人瞬间冲进仓库抢购,把仓库管理员(DB)累垮了。其他商品还能正常在门口买。
-
缓存穿透: 查询 数据库中根本不存在 的数据。关键:数据不存在。 请求必定穿透缓存访问 DB。你要找的东西压根不存在于仓库(DB)里,每次都得翻遍仓库确认没有。仓库管理员(DB)每次都得白忙活一趟。不管门口有没有货架(缓存),都得进仓库。
-
缓存雪崩:大量 Key 同时失效 或 Redis 集群整体不可用。关键:大规模失效 + 高并发。仓库门口堆放的很多常用物品(缓存)在同一时间被清空了(同时失效),所有人(请求)涌进仓库(数据库)找东西,把仓库挤爆了,并且因为仓库瘫痪,导致整个商场(系统)停摆。
解决方面可以来一个兜底措施:比如说熔断降级 ,来防止数据库崩溃和雪崩扩散;服务限流 来控制流量洪峰、从而也可以保护数据库。
1. 缓存击穿
- 问题描述
缓存击穿是指一个访问量非常大(热点)的缓存 Key,在它过期失效(Expire)的瞬间,大量并发的请求同时发现缓存失效(Cache Miss),这些请求会击穿缓存层,直接去查询底层数据库(如 MySQL)。
- 影响
这会导致数据库在极短时间内承受巨大的、远超其处理能力的请求压力,可能导致数据库响应变慢、连接耗尽甚至崩溃,进而引发整个系统的连锁故障。
举个例子,就比如仓库门口堆放的一件特别抢手的热门商品刚好卖完(缓存失效),一大群等着买它的人瞬间冲进仓库抢购,把仓库管理员(DB)累垮了。

- 特征
- 它是针对单个 Key 问题集中在某一个特定的热点 Key 上, 这个 Key 对应的数据访问频率非常高;
- 时机发生在在过期瞬间,在该 Key 设置的过期时间刚好到达的那一刻,这个时刻有高并发请求,同时这个时刻请求全打到数据库了;
- 需要防止在缓存失效瞬间,大量请求同时访问数据库。
- 解决方案
- 方法一:互斥锁
核心思想:既然是大量请求都直接访问数据库了,那我就在数据库那一层加锁,保证同一时刻只有一个线程访问,这样就减轻数据库的压力。
双检锁
java
// 缓存击穿,解决方式1---双检锁
public GoodsgetGoodsDetailById(StringgoodsId) {
// 1.先从缓存里面查
GoodsgoodsObj= (Goods) valueOperations.get(RedisKey.GOODS_KEY+goodsId);
// 2.缓存没有再查数据库
if (goodsObj==null) {
synchronized ( goodsId.intern() ) {
goodsObj= (Goods) valueOperations.get(RedisKey.GOODS_KEY+goodsId);
}
if (goodsObj!=null) {
returngoodsObj;
}
Goodsgoods=getDbGoodsById(goodsId);
if (goods!=null) {
valueOperations.set(RedisKey.GOODS_KEY+goodsId, goods);
returngoods;
}
returnnull;
}
returngoodsObj;
}
加锁检查缓存(第一次检查) :当用户请求数据时,首先检查缓存中是否存在该数据。
加锁 :如果缓存中没有数据,那么走DB。但是,在尝试从数据库查询数据之前,使用本地锁(或者分布式锁)来确保只有一个请求能够执行数据库查询操作。
数据库查询 :如果成功获取到锁,那么第二次检查缓存,如果确实缓存中没有数据, 执行数据库查询操作,获取最新的数据。
更新缓存 :将查询到的数据写入缓存,并设置一个合理的过期时间。
释放锁 :完成缓存更新后,释放分布式锁,以便其他请求可以继续执行。
返回数据 :将查询到的数据返回给用户。
处理其他请求:对于在等待锁释放期间到达的请求,它们可以直接从缓存中获取数据,而不需要再次查询数据库。
上面的问题也很明显,那就是假如查数据库,写入缓存这俩步骤,如果耗时过长,前面的请求会被一直阻塞住的。
结论:治标不治本
-
方法二:key不过期
-
永不过期:设置key的时候,不让其过期,由于是永不过期的,故需要考虑更新这个key的值;
-
逻辑过期 :缓存 Key 不设置过期时间(或设置一个很长的过期时间),让其"永不过期"。但是我们可以在缓存 Value 中,额外存储一个逻辑过期时间戳 (例如 {value: obj, expireTime: 12345678910}),当应用读取缓存时,检查 Value 中的逻辑过期时间戳,如果未过期,直接返回数据;如果已过期,触发一个异步任务(如放入消息队列、启动一个线程池任务都可以)去更新缓存。当前请求可以直接返回已过期的旧数据 (业务允许短暂不一致)或尝试获取互斥锁进行同步更新(类似方法 1)。
-
优点
此方法缓存 Key 永不失效,彻底避免"失效瞬间"的问题,用户请求基本不受缓存更新影响,延迟低(直接返回旧数据或触发异步更新)。
缺点
但是问题也是非常的明显:首先就是业务要容忍数据的不一致性;需要实现异步更新机制(消息队列、线程池),增加了设计的复杂性;其次呢,如果异步更新失败或延迟过大,用户可能长时间读到旧数据;
- 方法三:热点key探测,提前刷新
在缓存热点 Key 即将过期之前(比如还剩 1 分钟时),主动触发 一个后台任务(定时任务、监控线程)去查询数据库并更新缓存,重置其 TTL。
这种方式看起来算是完美的,但是丢给了我们一个致命问题,那就是哪些key是热点key?
1.1.1 hotKey问题
首先我们要明白一点,HotKey是什么,如何定义HotKey?
HotKey:指的是 Redis 缓存中被高频访问的键,这类键的访问量远高于其他普通键。这类键称之为"热键"。
比如说,秒杀模块中,可能会将商品信息缓存在Redis中,那么,某些商品的skuId可能作为key,在秒杀这段时间里面,这类key的访问量可能会明显高于其他键;此外呢,如果有人恶意访问缓存,发送大量请求到指定key,也可能导致该key变成热键;还有像新闻网站上的突发新闻、论坛的热点文章帖子等等...
如果这个时候这个热键过期了,就会造成缓存击穿的问题,巨量请求直接到达了MySQL数据库层,很有可能会造成服务的不可用。
如何去监测这些热key呢,出现了热key问题,该怎么办?或者说,我们要如何去预防这类问题的出现?
可以由以下几步结合起来,给他来一套组合拳:
- 系统功能设计之初,凭借经验判断可能的热key:
好比秒杀系统,肯定就可以比较容易判断出哪些key可能变成热key了吧,电商系统中的商品详情页、或者是新上的活动促销、社交平台上的热门帖子等数据通常容易成为热点Key。
- Redis客户端代码层面增加访问次数记录:
在Redis客户端层面,我们手动记录key的访问次数、或者是xxx时间间隔的访问频率,如果有监控系统,我们可以将这类数据定时或者实时上报的监控系统,然后就可以在监控系统看到了;
- 在Redis服务端:
它本身提供了相应的功能:执行一些相关命令(如MONITOR、redis-cli --hotkeys等),通过分析这些命令,可以观察到哪些Key被频繁访问,识别出热点Key。
【MONITOR】 命令是 Redis 提供的一种实时查看 Redis 的所有操作的方式,可以用于临时监控 Redis 实例的操作情况,包括读写、删除等操作。由于该命令对 Redis 性能的影响比较大,被逼到墙角的时候,我们可以暂时使用这个命令,得到其输出之后,关闭它,然后对其输出归类分析。
同理,--hotkeys也会增加 Redis 实例的 CPU 和内存消耗(全局扫描),因此需要谨慎使用
上面好像说的都是监测key,检测到了能怎么办呢?
在单服务器承受范围之内,我们可以结合本节开始前的方法三,如果设置了过期时间,可以刷新其过期时间,可以避免缓存击穿的问题了。
在单个服务器承受范围之外: 这就涉及到Redis主从架构等等问题了,本文不做探讨。
增加从节点:
如果是用 : redis 主从架构,可以通过增加Redis集群中的从节点,增加 多个读的副本。通过对读流量进行 负载均衡, 将读流量 分散到更多的从节点 上,减轻单个节点的压力。
key拆分:
通过改变Key的结构(如添加随机前缀),将同一个热点Key拆分成多个Key,使其分布在不同的Redis节点上,从而避免所有流量集中在一个节点上。
本文的引用仅限自我学习如有侵权,请联系作者删除。
参考知识
Redis上篇--知识点总结
Redis中篇--应用