Redis 为什么会出现
学习 Redis 之前,你先要了解一个知识点
之前文章中有讲过,内存 和 硬盘 的读写速度差异极大,这边再画个图理解一下

1ns = 十亿分之一秒
那么,既然有了 PostgreSql、MySQL 这些数据库了,为什么还需要 Redis?
因为传统数据库把数据存在 磁盘 上,这会导致一个问题
如果每次用户请求,后端都去磁盘读数据,这会导致响应时间会很长,影响用户体验
那么 Redis 来了。它最常见的用途确实是 缓存,但它不只是缓存。
和前端的 localStorage 大同小异,先看有没有缓存数据,有就直接用,区别在于
如果不配置持久化,Redis 重启后,内存里的数据就可能丢失。
断电后:
- localStorage:依旧保存
- Redis:内存数据可能丢失,是否能恢复取决于后面讲的持久化配置
因为一个存在 硬盘,一个存在 内存,至于为什么,简单来说,硬盘靠 磁畴的方向 存储,内存靠 电容里的电荷
总结一下 Redis 的本质
一个独立运行的、基于内存的、支持丰富数据结构的键值存储服务。它常被用来缓存热点数据、做计数器、队列、锁和排行榜,主要是在数据库之外补上更快的读写能力。
那么,假设一个数据库有 3TB 的数据,而一台服务器可能只有 64GB 内存,我们怎么决定,哪些数据放进 Redis 里面呢?
哪些数据适合放进 Redis
有三点:
- 高频访问优先:如购物软件中 首页 的 商品列表,几乎时刻有人看
- 高计算成本优先:如购物软件中 排行榜 ,计算量大,计算完就放入 Redis中,后面直接取
- 可以接受短暂不一致的数据优先:比如 商品库存,页面写着 剩余32件,但实际只剩30件,不及时更新,用户也能接受,但是 账户余额 差1分都不行
隐含原则:安全敏感数据要谨慎缓存,尤其不能把明文密码、长期密钥这类东西随便放进去。
Redis 的五种基础数据结构
Redis还不止提升速度,它还支持多种数据结构,让你不用把整份数据取出来,就能原地操作。
先看localStorage是怎么存的
js
//localStorage只能存字符串
localStorage.setItem("name","张三")
localStorage.setItem("list","[1,2,3]")//即使是数组,也序列化成字符串
怎么去拿数组中的第二个元素?
text
取出整个字符串 → JSON.parse → 取下标 → 改完 → JSON.stringify → 存回去
这里先讲 Redis 最常用的五种基础数据结构
- string
- List
- Hash
- Set
- Sorted Set
先是 String,可以理解成一个变量, 存一个值,去一个值
text
SET user:token:9527 "abc123xyz" ← 存
GET user:token:9527 ← 取,返回 "abc123xyz"
但它比普通变量多一个能力 原子计数:
text
SET article:1001:views "0"
INCR article:1001:views ← 变成 1
INCR article:1001:views ← 变成 2
INCR article:1001:views ← 变成 3
那问题来了,我为什么不直接在代码里用变量 count++?
因为后端通常有多个服务器实例同时运行,如果 3 台服务器各自在内存count++,会导致 数据分裂 ,打个比方
- 1号服务器最终 count = 100
- 2号 count = 80
- 3号 count =60
Redis作为独立的集中式服务,所有服务器都对同一个 Redis 加,INCR 操作是原子的,不会出现并发错乱
应用场景有:缓存、计数器、锁
再看到 List,可以理解成js中的数组,支持从两头 push/pop

应用场景:消息队列、最新动态列表
举例:微信朋友圈的 最新动态
- 每当有人发动态时, LPUSH 塞入列表头部
- 展示时用 LRANGE 0 19 取最新的20条
Hash,可以理解成一个 js对象或json
text
HGET user:9527 name → "张三" ← 只取一个字段
HSET user:9527 age "26" ← 只改一个字段
为什么不用 String 存整个 JSON 字符串?原因上面已经说过了
用String去存, 当你只想 其中某一个的值的时候,你还是会把所有的 JSON 取出来 -> 再解析 -> 再改 age -> 序列化 -> 存回去
而用 Hash,则一行直接完成
text
HSET user:9527 age "26"
应用场景:用户信息、商品详细
Set,可以理解成js的new Set(),去重
不可以有重复的元素,那么来看普通数组怎么做
js
let tags = []
tags.push("Redis")
tags.push("后端")
tags.push("Redis") //重复了
//手动去重
if (!tags.includes("Redis")) {
tags.push("Redis")
}
接着看 js 中的Set是怎么做的
js
let tags = new Set()
tags.add("Redis")
tags.add("后端")
tags.add("Redis")//因为上面已经有该元素了,所以会自动忽略
tags.has("Redis")// true --- 查一个元素在不在
tags.size // 2
Set的去重只是一个优点,最大的优点,集合运算
js
const my = new Set(['吃饭','睡觉','打游戏'])
const your = new Set(['吃饭','看书','睡觉'])
交集 (SINTER): {'吃饭','睡觉'}
并集 (SUNION): {'吃饭','睡觉','打游戏','看书'}
差集 (SDIFF): {'打游戏'} //我有但你没有
这就是Set存在的根本理由,让两堆数据之间一条命令算出关系
应用场景:标签、共同好友、去重
Sorted Set,可以理解成一个自动排行的 Set
对应:
text
ZADD school:student 100 "学生A"
ZADD school:student 80 "学生B"
ZADD school:student 90 "学生C"
那我们要取前三名的时候
text
ZREVRANGE school:student 0 2
-> ["学生A", "学生C", "学生B"] 自动按分数降序排好了
为什么不用数据库 ORDER BY score DESC LIMIT 3?
可以是可以,数据库也可以通过索引来优化排序。但如果排行榜更新频繁、读取又特别多,反复查库和排序的成本还是会很高。Sorted Set 内部维护有序性,插入和查询通常是 O(log N),取 Top N 会更方便。
应用场景:排行榜、延迟队列
内存数据怎么保存下来
持久化的问题,我们知道: Redis 主要把数据放在内存中,如果机器重启,内存数据就可能丢,那矛盾就出来了
既要内存的速度,又要磁盘的安全
那么 Redis 的两种持久化方案
- RDB:定时备份
那么只会丢失 10:00 - 10:30 之内的数据
- 优点:文件紧凑,恢复速度快,对性能影响小
- 缺点:两次快照之间的数据会丢
- AOF:增量追加
经过断电,恢复时会重放 AOF 里记录的命令。根据 AOF 的刷盘策略,通常可以把数据恢复到接近崩溃前的状态,但也可能丢失最后一小段还没刷到磁盘的数据。
- 优点:丢数据极少,文件是可读文本,可以人工检查
- 缺点:文件体积大,恢复速度慢
那么,如果一个AOF 文件无限扩大,会导致这个文件十分臃肿,那怎么解决?
有个机制: AOF Rewrite,原理是:
不管历史操作过程,只看当前最终状态,用最少的命令重新生成一份新 AOF 文件
还有个新思路,混合持久化
简单来说就是,先用 RDB 作为恢复的基准点,再接上后面较新的 AOF 命令,兼顾 RDB 的恢复速度和 AOF 的数据完整性。
一幅图总结就是:

如何选择用哪种模式?
- 数据丢失不严重: RDB
- 数据丢失造成损失: AOF 或 混合
缓存最怕出现什么问题
然后是 缓存穿透 、 缓存击穿 和 缓存雪崩
他们指向同一个问题,缓存未命中
text
正常状态:
用户请求 ──► Redis(命中) ──► 返回 数据库很轻松
三大问题的共同本质:
用户请求 ──► Redis(未命中) ──► 数据库 数据库被砸
缓存穿透:查询一个数据库里根本不存在的数据
用户请求一个商品id为: 99999999 的商品(根本没有这个商品)
- Redis查 -> 没有
- 数据库查 -> 没有
- 没查到,也没东西返回给 Redis
- 下次发来同样的请求,又去查
这导致每次都会穿透到数据库,如果攻击者故意用大量不存在的 ID 发请求,那会绕过缓存直接压垮数据库
解决方案
- 缓存空值
但这有个问题:如果攻击者用大量随机 ID,每个 ID 不同,你会缓存海量空值,浪费内存。
- 布隆过滤器
本质是一个 超级压缩的 合法 ID 名单,特性是:
- 判断不存在?那就一定不存在,直接拦截
- 判断可能存在?再放行去查 Redis 或数据库
它会有"误以为存在"的可能,但不会把一个真实存在的数据误判成不存在。
缓存击穿:一个超级热点 key 突然过期,瞬间大量请求同时打到数据库
举例:微博上一个明星突然上热搜,每秒一万人在看,然后这条微博的缓存突然过期了
text
过期前:
10000请求/秒 -> Redis命中
过期后的瞬间:
10000请求/秒 -> Redis 未命中
-> 10000请求同时砸向数据库
->数据库瞬间过载
它和 缓存穿透 不同,缓存穿透是查询不存在的数据,而缓存击穿是数据存在,但缓存恰好过期了
解决方案
- 互斥锁
text
10000个请求发现缓存过期
第 1 个请求抢到锁 → 查数据库 → 写回 Redis → 释放锁
第 2~10000 个请求 → 没抢到锁 → 等一会儿 → 重新读 Redis → 命中了
- 热点 key 永不过时
对于确定的超级热点数据,不设过期时间,数据更新时由后台程序主动刷新缓存,而不是等它自己过期
缓存雪崩:大量 key 在同一时间集体过期,或者 Redis 整体宕机
它比 缓存击穿 更严重,如果说 缓存击穿 是纸上出现了一个洞,那 缓存雪崩 就是纸都被烧没了

解决方案
- 过期时间加随机值
text
不要:EXPIRE key 3600 (所有 key 同时在 1 小时后过期)
而是:EXPIRE key 3600 + random(0, 600) (每个 key 在 60~70 分钟之间随机过期)
核心逻辑:把集体过期打散成分批过期,减少数据库压力最高值
- 多级缓存架构
text
用户 → 本地缓存(进程内) → Redis(分布式) → 数据库
即使 Redis 全崩,本地缓存还能扛一阵
分布式锁和原子操作
首先为什么需要锁?
因为多台服务器同时处理同一资源
text
场景:商品库存只剩 1 件,两个用户同时下单
服务器A:读库存 → 1 → 大于0,可以卖 → 扣减为0 → 卖出
服务器B:读库存 → 1 → 大于0,可以卖 → 扣减为0 → 卖出
结果:卖了 2 件,但只有 1 件库存 → 卖超了
问题在于,读 和 写 之间有时间差,别的服务器在这个间隙插进来了
解决办法:在操作前加锁,谁先拿到锁谁才能操作,操作完释放锁,注意:是所有服务器都认的锁,存在一个公共的地方,Redis就是这个公共的地方

实际加锁通常会写成这样:
text
SET lock:order:1001 "我的唯一ID" NX PX 10000
NX 保证只有一个请求能成功写入,其他请求发现 Key 已存在就拿不到锁;PX 10000 则给锁加上过期时间,防止服务崩掉后锁永远不释放。这一整条 SET 命令是原子执行的,不会出现两个请求同时加锁成功的可能。
操作不当会有三个陷阱
- 忘记设过期时间 -> 死锁
text
服务器A 加锁成功 → 还没释放就崩溃了 → 锁永远不会被删除
→ 所有其他服务器永远拿不到锁 → 业务卡死
解法:加锁必须带上过期时间
- 释放别人的锁
text
时间线:
0s 服务器A 加锁成功,过期时间10秒
... 服务器A 操作很慢,执行了12秒
10s 锁自动过期了!
11s 服务器B 加锁成功(A的锁已经过期了)
12s 服务器A 终于做完了,执行 DEL 释放锁
→ 但它删的是 B 的锁
解法:锁的 value 里存自己的唯一标识,释放前先检查 value 是不是自己的
text
//打个比方
if (GET lock:order:1001 == "我的ID") {
DEL lock:order:1001
}
- 检查和删除不是原子的
先 GET 检查,在 DEL 删除,这是两步操作,中间可能被插入别的请求,解决方案是用 Lua 脚本,把这两步合成一个原子操作
text
// Redis Lua 脚本(原子执行):
if redis.call("GET", key) == my_id then
return redis.call("DEL", key)
end
Redis 一台机器不够时怎么办
前面所讲,Redis 都是单机,一台服务器跑一个 Redis 实例,这会有两个问题:
- 单点故障
text
这台机器挂了 → Redis 没了 → 缓存全没了 → 数据库被砸 → 系统崩溃
- 性能瓶颈
text
一台机器的内存和 CPU 是有限的 → 请求量超过上限就扛不住了
那么有三个方案,分别来解决不同层级的问题
主从复制:一台写,多台读

就像老师在黑板上写,下面几十个同学同时在抄,任何人想看笔记,找一个同学借就行,不需要每次都去看黑板
优点:
- 分担读压力:客户端可以把一部分读请求分到多个 Slave 上
- 数据冗余: Master 的数据在每个 Slave 上都有一份备份
缺点:
- Master 挂了怎么办? Slave 只能读,这导致整个系统的写入能力瞬间归零,手动切换新Master太慢了, 那就需要 哨兵 了
哨兵:自动监控,自动故障转移

当Master挂了时:
- Sentinel 们检测到 Master 无响应
- 多个 Sentinel 投票确认:"Master 确实挂了"(防止误判)
- Sentinel 从 Slave 中选一个提升为新 Master
- 其他 Slave 自动改为跟随新 Master
- 客户端自动切换到新 Master
整个过程自动完成,就像一个公司有一个老板和几个副总,哨兵就是董事会,老板出事了,董事会投票决定哪个副总接班,不需要员工(客户端)自己去判断该找谁
哨兵通常会部署多个,并通过 quorum 来确认故障。实际部署时常见奇数个哨兵,这样更容易形成多数派,但关键还是要满足配置好的 quorum。
集群:把数据分散到多台机器上存储
主从 + 哨兵解决了高可用,但没有解决容量问题,所有的数据还是在一台 Master 内存中,如果数据量有200G,一台机器只有64G内存呢?

数据怎么知道该存到哪台机器?
text
对 key 做 CRC16 哈希运算 → 对 16384 取余 → 落在某个槽位
→ 这个槽位归哪个 Master 管,数据就存在哪
举例:
text
"user:1001" → CRC16 → 某个槽位 → Master C 管
"user:1002" → CRC16 → 某个槽位 → Master A 管
总结
最后,如果只用一句话理解 Redis,那就是:它不是用来取代 MySQL、PostgreSQL 的数据库,而是放在它们前面或旁边,专门承担"更快、更高并发、更适合临时状态"的那部分工作。
缓存解决热点数据读得慢的问题,数据结构解决"原地操作"的问题,RDB/AOF 解决内存数据怎么尽量保存下来,分布式锁解决多台服务器怎么协调,主从、哨兵和集群则解决 Redis 本身怎么扛住故障和容量增长。
所以 Redis 真正厉害的地方,不只是快,而是它把很多高并发系统里最麻烦的"快、稳、协调"问题,都给了比较顺手的工具。