少年硬气。。。

Redis 完整知识体系:从入门到出门
Redis是什么
想象你是一个图书馆管理员。传统数据库(如 MySQL)像是一个巨大的地下仓库,每次找书都要走很远的路(磁盘 I/O)。而 Redis 就像是一个放在你办公桌上的记忆宫殿——所有数据都存在内存里,伸手就能拿到,速度极快。
Redis = Remote Dictionary Server,远程字典服务。本质是一个内存中的 Key-Value 数据库。
核心特性:高性能、丰富的数据结构、持久化、分布式支持。
第一幕:初识 Redis —— 基础配置与启动
1.1 安装与启动
# 启动 Redis 服务器
redis-server
# 后台运行(推荐生产环境)
redis-server --daemonize yes
# 使用配置文件启动
redis-server /etc/redis/redis.conf
# 客户端连接
redis-cli -h 127.0.0.1 -p 6379
1.2 核心配置文件 redis.conf
# 绑定 IP(默认只允许本地,生产环境需配置)
bind 127.0.0.1
# 端口
port 6379
# 密码认证
requirepass your_password
# 后台运行
daemonize yes
# 日志文件
logfile /var/log/redis/redis-server.log
# 内存限制(关键!防止 OOM)
maxmemory 256mb
# 内存淘汰策略(后面详细讲)
maxmemory-policy allkeys-lru
第二幕:数据结构 —— Redis 的"十八般武艺"
Redis 不只是简单的"键值对",它提供了丰富的数据结构,每种结构都有特定的应用场景。
mindmap
root((Redis<br/>数据结构))
String
缓存
计数器
分布式锁
会话
List
队列
栈
最新列表
Hash
对象存储
用户信息
Set
去重
共同好友
标签
ZSet
排行榜
延时队列
高级结构
Bitmap
HyperLogLog
GEO
Stream
2.1 String(字符串)—— 最基础也最常用
# 基础操作
SET user:1001 "张三" # 设置
GET user:1001 # 获取 → "张三"
# 计数器(原子递增)
INCR view:article:123 # 阅读量+1 → 1
INCR view:article:123 # → 2
DECR view:article:123 # → 1
# 批量操作(减少网络往返)
MSET user:1001 "张三" user:1002 "李四"
MGET user:1001 user:1002 # → ["张三", "李四"]
# 设置过期时间(缓存必备)
SET session:token:abc "user_data" EX 3600 # 1小时后自动删除
应用场景故事:电商网站的商品库存计数。每次下单,DECR stock:sku:8888,如果返回 -1 说明库存不足。
2.2 List(列表)—— 消息队列的雏形
# 队列操作(FIFO - 先进先出)
LPUSH queue:orders "order_001" # 左侧入队
LPUSH queue:orders "order_002"
RPOP queue:orders # 右侧出队 → "order_001"(先进入的先处理)
# 栈操作(LIFO - 后进先出)
LPUSH stack:history "page1"
LPUSH stack:history "page2"
LPOP stack:history # → "page2"(后进入的先出来)
# 获取范围(适合分页)
LRANGE queue:orders 0 9 # 获取前10个
BLPOP queue:orders 30 # 阻塞等待30秒,有数据立即返回
应用场景故事:微博的"最新动态"。每个用户有一个 list:user:1001:timeline,发推文时 LPUSH,看动态时 LRANGE 0 20 取前20条。
2.3 Hash(哈希)—— 存储对象的完美选择
# 存储一个用户对象(对比 String 的 JSON 序列化)
HSET user:1001 name "张三" age 25 city "北京"
HGET user:1001 name # → "张三"
HGETALL user:1001 # → {"name":"张三","age":"25","city":"北京"}
# 对比 String 方式
SET user:1001 '{"name":"张三","age":25,"city":"北京"}'
# 修改年龄时需要:1) GET 整个JSON 2) 解析 3) 修改 4) 序列化 5) SET
# Hash 直接:HSET user:1001 age 26 (只改一个字段,高效!)
应用场景故事:购物车功能。HSET cart:user:1001 sku:8888 2(2件商品),修改数量时只改一个字段,不用序列化整个购物车。
2.4 Set(集合)—— 去重与关系计算
# 添加标签
SADD tags:article:123 "redis" "database" "cache"
SADD tags:article:456 "redis" "tutorial"
# 判断元素是否存在
SISMEMBER tags:article:123 "redis" # → 1 (true)
# 交集 - 共同标签
SINTER tags:article:123 tags:article:456 # → ["redis"]
# 并集 - 所有标签
SUNION tags:article:123 tags:article:456 # → ["redis","database","cache","tutorial"]
# 随机抽奖(不重复)
SRANDMEMBER lottery:participants 3 # 抽3个幸运儿
应用场景故事:社交网站的"共同好友"。
- 用户A的好友:
SADD friends:user:A B C D E - 用户B的好友:
SADD friends:user:B C D F G - 共同好友:
SINTER friends:user:A friends:user:B→["C","D"]
2.5 Sorted Set(ZSet)—— 带权重的排行榜
# 添加成员及其分数
ZADD leaderboard 100 "player:张三" 85 "player:李四" 120 "player:王五"
# 按分数范围查询(排行榜)
ZREVRANGE leaderboard 0 2 WITHSCORES
# → ["player:王五","120","player:张三","100","player:李四","85"](从高到低)
# 查询某个玩家的排名
ZREVRANK leaderboard "player:张三" # → 1(第2名,从0开始)
# 范围查询(延时队列核心)
ZRANGEBYSCORE delay_queue 0 1716192000 # 获取所有到指定时间前需要执行的任务
应用场景故事:游戏排行榜、延时队列。
- 排行榜:实时更新分数,
ZADD自动排序。 - 延时队列:任务执行时间作为 score,定时扫描
ZRANGEBYSCORE获取到期任务。
2.6 高级数据结构
| 结构 | 命令 | 应用场景 |
|---|---|---|
| Bitmap | SETBIT/BITCOUNT | 签到(365天=365bit≈46字节)、活跃用户统计 |
| HyperLogLog | PFADD/PFCOUNT | UV 统计(误差0.81%,内存固定12KB) |
| GEO | GEOADD/GEORADIUS | 附近的人、地理位置服务 |
| Stream | XADD/XREAD/XGROUP | 消息队列(Kafka 的简化版) |
# Bitmap 签到示例(极省内存!)
SETBIT sign:2024:04:user:1001 0 1 # 4月1日签到
SETBIT sign:2024:04:user:1001 1 1 # 4月2日签到
BITCOUNT sign:2024:04:user:1001 # → 2(本月签到2天)
# 对比:用 String 存 "1,1,0,1..." 需要30字节,Bitmap 只需 4字节
第三幕:核心机制 —— Redis 的"生存法则"
数据存在内存里,断电就丢怎么办?内存满了怎么办?Redis 通过一套精妙的机制解决这些问题。
3.1 持久化 —— 数据不丢的"双保险"
RDB(Redis Database)—— 快照备份
# 配置自动触发(redis.conf)
save 900 1 # 900秒内至少1次修改,触发快照
save 300 10 # 300秒内至少10次修改
save 60 10000 # 60秒内至少10000次修改
# 手动触发
BGSAVE # 后台异步生成 RDB 文件
原理故事:就像给 Redis 拍照片。每隔一段时间,把内存中的所有数据拍成一张"照片"(二进制文件)保存到磁盘。恢复时直接把照片还原,速度很快。
优缺点:
- ✅ 恢复速度快,文件紧凑
- ❌ 最后一次快照到宕机之间的数据会丢失(可能丢几分钟数据)
AOF(Append Only File)—— 命令日志
# 配置(redis.conf)
appendonly yes
appendfsync everysec # 每秒同步(推荐,平衡性能与安全)
原理故事:像银行的交易流水账。每执行一个写命令,就把命令本身记录到文件里。恢复时重新执行这些命令,就能重建数据。
优缺点:
- ✅ 数据更安全,最多丢1秒数据
- ❌ 文件大,恢复慢(要重放所有命令)
混合持久化(Redis 4.0+)—— 鱼和熊掌兼得
# 配置
aof-use-rdb-preamble yes
原理故事:开头用 RDB 快照(快速加载基础数据),后面用 AOF 增量日志(保证最新数据不丢)。就像先快速搭好房子框架,再慢慢装修细节。
3.2 过期策略 —— 内存的"自动清理"
graph LR
A[数据设置过期时间] --> B{过期策略}
B -->|惰性删除| C[访问时检查<br/>过期则删除<br/>省CPU但可能堆积]
B -->|定期删除| D[定时随机抽样<br/>删除过期键<br/>平衡CPU与内存]
C --> E[最终清理]
D --> E
# 设置过期(多种方式)
EXPIRE session:token 3600 # 3600秒后过期
SET temp:key "value" EX 60 # 创建时设置60秒过期
TTL temp:key # 查看剩余生存时间 → 45
生活类比:惰性删除像"只在有人找这本书时才检查是否过期";定期删除像"图书管理员每天随机抽查一批书,清理过期的"。
3.3 内存淘汰机制 —— 内存满了怎么办?
当内存达到 maxmemory 限制,且数据都没过期时,Redis 必须选择淘汰哪些数据。
| 策略 | 说明 | 适用场景 |
|---|---|---|
volatile-lru | 从已设置过期的键中,淘汰最近最少使用 | 缓存场景 |
allkeys-lru | 从所有键中,淘汰最近最少使用 | 通用缓存(推荐) |
volatile-random | 从已设置过期的键中随机淘汰 | |
allkeys-random | 从所有键中随机淘汰 | |
volatile-ttl | 淘汰即将过期的键 | 优先保活久的 |
noeviction | 不淘汰,直接报错(默认) | 不允许丢数据 |
# 配置
maxmemory-policy allkeys-lru
故事理解:图书馆书架满了,LRU 策略就是"扔掉最久没人借的书",因为没人借说明价值低。
3.4 单线程与多线程 —— Redis 的"并发哲学"
核心结论:Redis 的命令执行始终是单线程的(保证原子性),6.0+ 只是网络读写用了多线程,解决高并发连接问题。
第四幕:事务与原子操作 —— 保证"要么全做,要么不做"
sequenceDiagram
participant Client
participant Redis
Client->>Redis: MULTI(开启事务)
Client->>Redis: SET balance:A 100
Client->>Redis: SET balance:B 200
Client->>Redis: EXEC(执行)
Redis-->>Client: OK(全部成功)
Note over Client,Redis: 如果中间出错<br/>DISCARD 回滚所有命令
# 事务示例:转账操作
MULTI
DECR balance:张三 100 # 扣100
INCR balance:李四 100 # 加100
EXEC # 原子执行
# 乐观锁:防止并发修改
WATCH balance:张三 # 监控这个键
MULTI
DECR balance:张三 100
EXEC
# 如果执行前 balance:张三 被其他客户端改了,EXEC 返回 nil,事务不执行
重要特点:
- Redis 事务不支持回滚(不同于数据库)。如果某个命令语法错误,整个事务会失败;但如果是运行时错误(如对一个 String 做 INCR),其他命令仍会执行。
- 用 Lua 脚本 可以实现真正的原子复杂逻辑。
# Lua 脚本保证原子性
EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 mykey myvalue
第五幕:高可用架构 —— 从单机到集群
graph TB
subgraph 主从复制
M[Master<br/>主节点] -->|同步数据| S1[Slave 1]
M -->|同步数据| S2[Slave 2]
end
subgraph 哨兵模式
Sen1[Sentinel 1] -->|监控| M
Sen2[Sentinel 2] -->|监控| M
Sen3[Sentinel 3] -->|监控| M
Sen1 -->|故障转移| S1
end
subgraph Cluster集群
N1[Node 1<br/>0-5460槽] --- N2[Node 2<br/>5461-10922槽]
N2 --- N3[Node 3<br/>10923-16383槽]
N1 -.->|MOVED重定向| C[客户端]
end
5.1 主从复制 —— 读写分离与数据备份
# 从节点配置
replicaof 192.168.1.10 6379 # 指定主节点
同步过程:
- 全量同步:首次连接,主节点生成 RDB 发送给从节点
- 增量同步:后续通过复制积压缓冲区,只传新命令
5.2 哨兵 Sentinel —— 自动故障转移
# sentinel.conf
sentinel monitor mymaster 192.168.1.10 6379 2 # 2个哨兵同意才故障转移
sentinel down-after-milliseconds mymaster 5000 # 5秒无响应认为下线
工作机制:
- 主观下线:某个哨兵觉得主节点挂了
- 客观下线:多个哨兵(达到配置数量)都认为挂了,才真的进行故障转移
- 自动选主:从从节点中选一个作为新主节点
5.3 Redis Cluster —— 真正的分布式
# 集群模式启动
redis-cli --cluster create 192.168.1.11:6379 192.168.1.12:6379 \
192.168.1.13:6379 --cluster-replicas 1
核心概念:
- 16384 个哈希槽:整个集群的键空间被分成 16384 个槽
- 数据分片:每个节点负责一部分槽,数据均匀分布
- MOVED 重定向:如果客户端请求的键不在当前节点,返回
MOVED告诉客户端去正确的节点
# 示例:计算键属于哪个槽
CLUSTER KEYSLOT user:1001 # → 5461(假设)
# 如果当前节点不负责 5461,返回:MOVED 5461 192.168.1.12:6379
第六幕:缓存三大问题 —— 面试必考!
graph TD
A[缓存三大问题] --> B[缓存穿透<br/>查不存在的数据]
A --> C[缓存击穿<br/>热点Key过期]
A --> D[缓存雪崩<br/>大量Key同时过期]
B -->|解决| B1[缓存空值<br/>布隆过滤器]
C -->|解决| C1[互斥锁<br/>逻辑过期<br/>永不过期]
D -->|解决| D1[随机过期时间<br/>多级缓存<br/>限流降级]
style B fill:#FFB6C1
style C fill:#FFE4B5
style D fill:#FF6B6B
6.1 缓存穿透 —— 查询"不存在的东西"
场景:黑客用不存在的 ID 疯狂请求,缓存里没有,数据库也没有,每次都要查数据库。
// 解决方案1:缓存空值
String data = redis.get(key);
if (data == null) {
// 查数据库
data = db.query(key);
if (data == null) {
redis.set(key, "null", 60); // 缓存空值,60秒过期
}
}
// 解决方案2:布隆过滤器(更优雅)
if (!bloomFilter.mightContain(key)) {
return null; // 肯定不存在,直接返回
}
// 可能存在,继续查缓存/数据库
6.2 缓存击穿 —— "明星离婚"热点事件
场景:某个热点 Key 突然过期,大量请求同时打到数据库。
// 解决方案1:互斥锁(只有一个线程去重建)
String data = redis.get(hotKey);
if (data == null) {
// 尝试获取锁
if (redis.setnx(lockKey, "1", 60)) {
// 只有获取到锁的线程去查数据库
data = db.query(hotKey);
redis.set(hotKey, data, 3600);
redis.del(lockKey); // 释放锁
} else {
// 其他线程短暂等待后重试
Thread.sleep(100);
return redis.get(hotKey); // 再次尝试获取
}
}
// 解决方案2:逻辑过期(永不过期,但标记过期时间)
// 缓存中存:{value: "实际数据", expireTime: 1716192000000}
// 发现逻辑过期了,后台异步更新,前台返回旧数据
6.3 缓存雪崩 —— "双十一零点"
场景:大量 Key 同时过期,或者 Redis 宕机,所有请求涌向数据库。
# 解决方案1:随机过期时间(避免同时失效)
EXPIRE key $((RANDOM % 300 + 300)) # 300-600秒随机
# 解决方案2:多级缓存
浏览器缓存 → CDN → Nginx缓存 → Redis → 数据库
# 解决方案3:限流降级
# 当流量过大,直接返回"系统繁忙",保护数据库
第七幕:分布式锁 —— 跨机器的"排他性"
sequenceDiagram
participant A as 服务A
participant R as Redis
participant B as 服务B
A->>R: SET lock:order:123 NX EX 30
R-->>A: OK(获取锁成功)
B->>R: SET lock:order:123 NX EX 30
R-->>B: nil(获取失败)
A->>A: 执行业务逻辑...
A->>R: DEL lock:order:123(释放锁)
Note over A,B: NX: 不存在才设置<br/>EX: 30秒自动过期<br/>防止死锁!
# 基础实现
SET lock:resource:123 "owner:A" NX EX 30
# NX = Not eXists,只有不存在才设置
# EX = EXpire,30秒自动过期(防止服务崩溃导致死锁)
# 进阶:Redisson 可重入锁(Java)
RLock lock = redisson.getLock("myLock");
lock.lock(); // 阻塞等待
try {
// 执行业务
} finally {
lock.unlock(); // 释放锁
}
关键点:
- 原子加锁:
SET NX EX一条命令完成 - 过期时间:必须设置,防止死锁
- 防误删:释放前检查是否是自己的锁(用唯一标识)
第八幕:发布订阅与管道 —— 高效通信
8.1 Pub/Sub —— 消息广播
# 客户端A:订阅频道
SUBSCRIBE news:sports
# 客户端B:发布消息
PUBLISH news:sports "姚明当选篮协主席"
# 客户端A立即收到消息
局限:消息不持久化,消费者掉线期间的消息会丢失。需要持久化消息队列用 Stream。
8.2 Pipeline —— 批量命令减少网络往返
# 普通方式:10000次操作 = 10000次网络往返
SET key1 value1
SET key2 value2
...
# Pipeline:一次发送,一次返回
# 伪代码
pipeline = redis.pipeline()
for i in range(10000):
pipeline.set(f"key:{i}", f"value:{i}")
pipeline.execute() # 一次性发送所有命令
效果:网络往返从 N 次降到 1 次,性能提升数十倍!
第九幕:性能优化 —— 让 Redis 飞起来
graph LR
A[性能优化] --> B[避免大Key<br/>String < 10KB<br/>Hash/List/Set/ZSet < 5000元素]
A --> C[避免长列表<br/>List/Hash 元素过多<br/>影响性能]
A --> D[禁用 KEYS /*<br/>使用 SCAN 渐进式遍历]
A --> E[批量命令<br/>MSET/MGET<br/>Pipeline]
A --> F[合理内存策略<br/>maxmemory-policy]
A --> G[系统优化<br/>关闭THP<br/>优化TCP<br/>连接复用]
关键优化点
# 1. 禁用 KEYS 命令(生产环境致命!)
KEYS user:* # 遍历全库,O(N),阻塞其他命令
# 改用 SCAN(渐进式,不阻塞)
SCAN 0 MATCH user:* COUNT 100
# 2. 批量操作
MSET user:1:name "张三" user:1:age 25 user:1:city "北京"
MGET user:1:name user:1:age user:1:city
# 3. 连接复用(避免频繁创建连接)
# 使用连接池,保持长连接
第十幕:典型业务场景 —— 学以致用
mindmap
root((Redis<br/>业务场景))
缓存
Cache Aside模式
读: 先缓存后DB
写: 先DB后删缓存
秒杀
分布式锁防超卖
限流保护系统
原子减库存 DECR
排行榜
ZSet 自动排序
实时更新分数
签到
Bitmap 极省内存
365天=46字节
UV统计
HyperLogLog
12KB固定内存
误差0.81%
延时任务
ZSet score=执行时间
定时扫描执行
分布式Session
String 存储
多服务共享登录态
10.1 缓存模式:Cache Aside
// 读操作
public String getData(String key) {
String data = redis.get(key);
if (data == null) {
data = db.query(key); // 查数据库
redis.set(key, data, 3600); // 放入缓存
}
return data;
}
// 写操作(关键:先更新数据库,再删缓存!)
public void updateData(String key, String value) {
db.update(key, value); // 先更新数据库
redis.del(key); // 再删缓存(不是更新缓存!)
// 下次读请求会自动从数据库加载最新数据到缓存
}
为什么删缓存而不是更新缓存?
- 并发场景下,"更新缓存"可能导致脏数据(A、B两个线程同时写,顺序错乱)
- "删缓存"简单有效,下次读会重新加载最新数据
10.2 秒杀系统 —— 分布式锁 + 限流 + 原子操作
// 1. 限流:令牌桶/漏桶算法(保护系统不被打垮)
// 2. 获取分布式锁(防止超卖)
if (redis.setnx("lock:sku:8888", "1", 10)) {
try {
// 3. 原子减库存
Long stock = redis.decr("stock:sku:8888");
if (stock >= 0) {
// 创建订单
createOrder(userId, skuId);
} else {
// 库存不足,恢复库存(或记录已超卖)
redis.incr("stock:sku:8888");
return "已售罄";
}
} finally {
redis.del("lock:sku:8888");
}
}
10.3 延时任务 —— ZSet 实现
# 添加延时任务(score = 执行时间戳)
ZADD delay_queue 1716192000 "task:send_email:123"
ZADD delay_queue 1716195600 "task:cancel_order:456"
# 定时脚本每秒执行:
ZRANGEBYSCORE delay_queue 0 1716192000 LIMIT 0 1
# 获取到期的任务,执行后删除
ZREM delay_queue "task:send_email:123"
知识串联总图
graph TB
subgraph "基础篇"
A1[Redis基础<br/>内存KV数据库] --> A2[安装配置<br/>redis.conf]
end
subgraph "数据结构篇"
B1[String<br/>缓存/计数器] --> B2[List<br/>队列/栈]
B2 --> B3[Hash<br/>对象存储]
B3 --> B4[Set<br/>去重/关系]
B4 --> B5[ZSet<br/>排行榜]
B5 --> B6[高级结构<br/>Bitmap/HLL/GEO/Stream]
end
subgraph "核心机制篇"
C1[持久化<br/>RDB/AOF/混合] --> C2[过期策略<br/>惰性+定期]
C2 --> C3[内存淘汰<br/>8种策略]
C3 --> C4[单线程<br/>6.0+网络多线程]
end
subgraph "进阶篇"
D1[事务<br/>MULTI/EXEC] --> D2[Lua脚本<br/>原子性]
D2 --> D3[分布式锁<br/>SET NX EX]
D3 --> D4[Pub/Sub<br/>Pipeline]
end
subgraph "架构篇"
E1[主从复制<br/>读写分离] --> E2[哨兵Sentinel<br/>自动故障转移]
E2 --> E3[Cluster集群<br/>16384槽分片]
end
subgraph "实战篇"
F1[缓存问题<br/>穿透/击穿/雪崩] --> F2[性能优化<br/>避免大Key/SCAN]
F2 --> F3[业务场景<br/>秒杀/签到/排行榜]
end
A1 --> B1
B6 --> C1
C4 --> D1
D4 --> E1
E3 --> F1
style A1 fill:#E1F5FE
style B3 fill:#E8F5E9
style C2 fill:#FFF3E0
style D3 fill:#F3E5F5
style E2 fill:#E8EAF6
style F1 fill:#FFEBEE