一、Redis 是什么
Redis(Remote Dictionary Server)是一个基于内存的键值数据库,数据存在内存中,读写速度极快(单机 10 万+ QPS)。
核心特点:
-
基于内存,读写极快
-
键值模型:Key 是字符串,Value 支持多种类型
-
单线程执行命令,天然避免并发竞争
-
支持持久化(RDB / AOF),重启不丢数据
-
支持主从复制、哨兵、集群等高可用方案
为什么 Redis 单线程还这么快?
-
纯内存操作
-
核心功能简单(增删改查没有复杂约束和索引)
-
I/O 多路复用(epoll),一个线程管理多个 Socket
-
避免线程切换和锁竞争开销
举例: 用户请求查商品详情 → 先查 Redis 缓存(命中直接返回,亚毫秒级)→ 未命中则查 MySQL → 写回 Redis → 返回。
多路复用
-
传统阻塞 I/O:餐厅里一个服务员(线程)只服务一桌客人(Socket)。客人看菜单半小时,服务员就在旁边傻站着等,效率极低。
-
I/O 多路复用 (epoll):餐厅里只有一个超级服务员(单线程),但他站在吧台看着所有桌子。哪桌客人举手说"我要点菜/结账"(数据就绪),服务员才过去处理。处理完立刻回到吧台。这样一个人就能高效管理成百上千桌。
二、Redis vs MySQL
| 维度 | MySQL | Redis |
|---|---|---|
| 存储位置 | 磁盘 | 内存 |
| 速度 | 毫秒级 | 微秒级 |
| 数据模型 | 关系表 | 键值对 |
| 容量 | TB 级 | 受内存限制,通常 GB 级 |
| 查询 | 复杂 SQL / JOIN | 仅按 Key 查找 |
| 事务 | ACID 完整事务 | 简单事务,无回滚 |
典型架构: MySQL 做主存储,Redis 做缓存层,两者配合。
三、五大基础数据类型
3.1 String(字符串)
核心: 一个 Key 对应一个二进制安全的字符串(最大 512MB)。
| 命令 | 作用 |
|---|---|
SET / GET |
设置、读取 |
MSET / MGET |
批量设置、读取 |
INCR / DECR |
原子自增/自减 |
SET key value EX seconds |
设置同时指定过期时间 |
SETNX |
仅 Key 不存在时才设置 |
举例:
SET product:2001 '{"name":"耳机","price":899}' EX 3600 # 缓存商品,1小时过期
INCR views:2001 # 浏览量 +1,返回 1
SETNX lock:order:1 "uuid-xxx" # 抢分布式锁
场景: 缓存、计数器、分布式锁、Session 存储。
3.2 Hash(哈希)
核心: 一个 Key 对应一个 field → value 映射表,适合存对象,可按字段读写。
| 命令 | 作用 |
|---|---|
HSET / HGET |
设置/获取单个字段 |
HMSET |
批量设置字段 |
HGETALL |
获取全部字段和值 |
HDEL |
删除字段 |
HEXISTS |
判断字段是否存在 |
举例:
HSET user:1001 name "小林" level 5 city "上海"
HGET user:1001 name # "小林"
HGETALL user:1001 # name、level、city 全部返回
HSET product:2001 stock 119 # 只改库存字段,不用重写整个 JSON
场景: 用户信息、商品详情、购物车。
3.3 List(列表)
核心: 双向链表,有序,可从两端压入/弹出。
| 命令 | 作用 |
|---|---|
LPUSH / RPUSH |
左侧/右侧插入 |
LPOP / RPOP |
左侧/右侧弹出 |
LRANGE start stop |
按索引范围查看(0 -1 = 全部) |
LLEN |
列表长度 |
BRPOP / BLPOP |
阻塞式弹出(可做队列消费) |
举例:
LPUSH feed:user:1001 "post:503" "post:502" # 发动态
LRANGE feed:user:1001 0 9 # 看最新 10 条
# 消息队列:生产者推入
RPUSH mq:order "order:9001"
# 消费者阻塞等待
BRPOP mq:order 0 # 返回 "order:9001"
场景: 消息队列(轻量)、最新动态时间线、秒杀排队。
3.4 Set(集合)
核心: 无序、不重复的字符串集合,支持交/并/差集运算。
| 命令 | 作用 |
|---|---|
SADD |
添加成员(自动去重) |
SMEMBERS |
列出所有成员 |
SISMEMBER |
判断是否为成员 |
SINTER / SUNION / SDIFF |
交集/并集/差集 |
SCARD |
集合大小 |
举例:
SADD tag:数码 phone audio laptop
SADD tag:音频 audio speaker
SINTER tag:数码 tag:音频 # 交集:audio(共同的标签)
SADD user:1001:follows 2002 2003 2005
SADD user:1002:follows 2003 2005 2008
SINTER user:1001:follows user:1002:follows # 共同好友:2003、2005
场景: 标签系统、共同好友、点赞用户去重、今日已领券用户。
3.5 Sorted Set(有序集合 / ZSet)
核心: 成员不重复,每个成员带一个 score(分数),按 score 自动排序。
| 命令 | 作用 |
|---|---|
ZADD |
添加成员及分数 |
ZRANGE / ZREVRANGE |
按排名升序/降序取 |
ZRANK / ZREVRANK |
查成员排名 |
ZSCORE |
查成员分数 |
ZRANGEBYSCORE |
按分数区间取成员 |
举例:
ZADD hot:sales 1200 product:A 8750 product:B 9800 product:C
ZREVRANGE hot:sales 0 2 WITHSCORES # 销量榜 Top 3
# 延迟队列:score = 到期时间戳
ZADD delay:task 1710000000 "refund:9001"
ZRANGEBYSCORE delay:task 0 1710000000 # 拉到期的任务
场景: 排行榜、延迟队列、按时间排序的消息流。
ZSet 的底层原理
ZSet 为什么查询和插入这么快? ==> 因为 跳表 Skip List
概念:它是在普通的有序链表上,增加了多级"索引",从而实现了类似二分查找的速度(时间复杂度O(log N))。
【跳表查询过程演示:查找元素 7】
Level 3: 1 ------------------------------------------> 10
Level 2: 1 ----------------> 5 ----------------------> 10
Level 1: 1 ----> 3 ----> 5 ----> 7 ----> 9 ----> 10
查找路径:
先看 L3:1 比 7 小,往右看是 10,比 7 大,于是降级到 L2。
再看 L2:从 1 走到 5,5 比 7 小,往右看是 10,比 7 大,降级到 L1。
最后看 L1:从 5 走到 7,找到目标!(跳过了 3, 9 的遍历)
数据类型选型口诀
单值缓存用 String → JSON 缓存、计数器
对象多字段用 Hash → 用户信息、商品详情
排队或时间线用 List → 消息队列、动态列表
去重和集合运算用 Set → 标签、共同好友
排序排名用 ZSet → 排行榜、延迟队列
四、三个进阶数据类型
4.1 Bitmap(位图)
核心: 把 String 当成二进制位数组,每个 bit = 0/1,极省空间。
举例:
SETBIT sign:user:1001:2025 0 1 # 第1天签到
SETBIT sign:user:1001:2025 2 1 # 第3天签到
BITCOUNT sign:user:1001:2025 # 统计签到总天数 → 2
场景: 签到统计、日活标记(一个用户一年只需 365 bit ≈ 46 字节)。
4.2 HyperLogLog(基数统计)
核心: 用固定极小的内存(约 12KB)估算"去重后有多少个不同元素",有约 0.81% 误差,不能列出具体元素。
举例:
PFADD uv:home:today user_1001 user_1002 user_1001
PFCOUNT uv:home:today # 估算 ≈ 2(重复 user_1001 只算一次)
PFMERGE uv:total uv:sectionA uv:sectionB # 合并多个 HLL
场景: 海量 UV 统计、去重计数(能接受误差)。
4.3 Stream(消息流,Redis 5.0+)
核心: 持久化的日志型消息流,带消息 ID、消费者组、ACK 确认机制。
举例:
XADD orders * type paid order_id 10001 # 生产消息
XREAD COUNT 2 STREAMS orders 0 # 从开头读2条
XGROUP CREATE orders cg1 0 MKSTREAM # 创建消费者组
XREADGROUP GROUP cg1 worker1 COUNT 10 STREAMS orders > # 组内消费
Stream vs List 做队列对比:
| 对比 | List | Stream |
|---|---|---|
| 消息标识 | 无内置 ID | 内置时间有序 ID |
| 消费语义 | 弹出即拿走 | 消费者组、pending、ACK |
| 历史回溯 | 弹出后难追溯 | 可保留日志、按范围查 |
场景: 需要可靠性消息队列的场景(简单场景用 List 就够了)。
五、Key 管理与过期策略
5.1 Key 命名规范
推荐格式: 业务:对象:id[:字段]
user:1001:name # 用户 1001 的姓名
order:20240324:items # 订单明细
cache:product:88392 # 商品缓存
-
好:
user:1001:name→ 一眼看出业务、实体、字段 -
坏:
a、x1、data→ 无从辨认
5.2 常用 Key 命令
| 命令 | 作用 |
|---|---|
EXPIRE key 秒 |
设置过期时间 |
TTL key |
查剩余过期秒数(-1 永不过期,-2 不存在) |
PERSIST key |
移除过期时间 |
EXISTS key |
判断是否存在 |
TYPE key |
查看 value 类型 |
DEL key |
删除 |
SCAN cursor MATCH pattern |
渐进式遍历(生产用,不阻塞) |
5.3 过期删除策略
惰性删除 + 定期删除 相结合:
-
惰性删除: 访问 Key 时才检查是否过期,过期则删 → 保证用到的 Key 一定正确
-
定期删除: Redis 周期性随机抽查一批 Key,删掉过期的 → 回收没人访问的过期 Key
举例: 一个过期 Key 一直没人访问,不会立刻被删,直到定期删除抽到它或被访问时惰性删除。
5.4 内存淘汰策略(maxmemory-policy)
当内存满了,按策略删除 Key 腾空间:
| 策略 | 说明 |
|---|---|
noeviction |
不淘汰,写命令报错 |
allkeys-lru |
从所有 Key 中淘汰最久未使用的(缓存场景首选) |
allkeys-lfu |
淘汰使用频率最低的 |
volatile-lru |
仅淘汰带过期时间的 Key |
volatile-ttl |
优先淘汰 TTL 更短的 |
六、持久化
6.1 RDB(快照)
原理: 定时把内存数据"拍一张照片"保存到 dump.rdb 文件。
触发方式:
-
SAVE:阻塞主进程(生产不用) -
BGSAVE:fork 子进程后台写,主进程继续服务 -
配置自动:
save 900 1(900秒内1次写就触发)
优点: 文件紧凑,恢复快。缺点: 两次快照之间的数据可能丢失。
6.2 AOF(追加日志)
原理: 每条写命令追加到 appendonly.aof 文件,重启时重放命令恢复数据。
三种刷盘策略:
| 策略 | 说明 | 数据安全 | 性能 |
|---|---|---|---|
always |
每条命令都刷盘 | 最高 | 最慢 |
everysec |
每秒刷一次(默认推荐) | 最多丢 1 秒 | 平衡 |
no |
交给 OS 决定 | 可能丢很多 | 最快 |
AOF 重写(瘦身): 对同一 Key 多次修改,只保留最终结果,生成更精简的 AOF。
6.3 混合持久化(Redis 4.0+)
AOF 文件前半段 = RDB 快照(紧凑),后半段 = 增量 AOF 命令 → 兼顾恢复速度与数据安全。
6.4 选择建议
| 场景 | 方案 |
|---|---|
| 能接受丢几分钟 | 纯 RDB |
| 几乎不能丢 | AOF everysec |
| 生产最佳实践 | RDB + AOF 都开,或混合持久化 |
| 纯缓存(丢了可重建) | 可关持久化 |
七、缓存三大经典问题
7.1 缓存穿透
问题: 查的数据数据库里也没有 → 每次穿透缓存打 DB。
解决:
-
缓存空值: 查不到也缓存一个占位(短 TTL),下次直接返回"不存在"
-
布隆过滤器: 先判断"可能存在吗",不可能直接返回
举例: 恶意请求查 id=-1 的商品,数据库没有 → 缓存空值 SET product:-1 "__NULL__" EX 60,60 秒内不再打库。
7.2 缓存击穿
问题: 热点 Key 过期瞬间,大量并发同时查库(单个热点问题)。
解决:
-
互斥锁: 只有一个线程去查库回写,其他等待(
SET lock:key uuid NX EX 10) -
逻辑过期: 值里带过期时间,过期后返回旧值 + 异步刷新
举例: 秒杀商品缓存过期 → 1000 个请求同时来 → 只有 1 个拿到锁去查库,999 个等缓存更新后直接读。
7.3 缓存雪崩
问题: 大量 Key 同时失效 或 Redis 挂了 → 请求全打数据库(面的问题)。
解决:
-
随机 TTL:
TTL = 3600 + random(0, 300)避免集体过期 -
高可用: 主从 + 哨兵 / 集群
-
限流降级: 保护数据库
举例: 100 个商品缓存都设了 1 小时过期 → 到点全部同时失效 → 加随机抖动后,过期时间分散在 60~65 分钟。
| 异常场景 | 核心问题 | 通俗理解 | 终极解决方案 |
|---|---|---|---|
| 缓存穿透 | 查根本不存在的数据 | 有人恶意狂刷 id=-999 的商品 |
1. 缓存空对象 (短TTL) 2. 布隆过滤器 (Bloom Filter)拦截 |
| 缓存击穿 | 某一个热点数据突然过期 | 微博热搜第一名突然过期,请求全砸向 DB | 1. 加互斥锁 (Redis SETNX) 2. 逻辑过期 (后台异步更新) |
| 缓存雪崩 | 大批量数据同时过期 / 节点宕机 | 早上 8 点大批缓存同时失效,DB 瞬间被压垮 | 1. 过期时间加随机数 (打散) 2. Redis 部署高可用集群 (防宕机) |
八、分布式锁
核心命令:
SET lock:order:123 <唯一UUID> NX EX 30
-
NX:Key 不存在才成功(互斥) -
EX 30:30 秒自动过期,防止死锁 -
唯一UUID:释放时校验 value,防止误删别人的锁
释放锁(Lua 保证原子性):
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
要点: 过期时间 > 业务执行时间;释放必须校验 value + Lua 原子操作。
九、分布式 Session 共享
问题: 负载均衡下,用户登录在 A 机,下次请求到 B 机 → Session 丢失。
解决: Session 外置到 Redis,所有应用服务器共享读写。
举例(Spring Boot):
// 引入 spring-session-data-redis,配置 Redis 连接
// 框架自动代理 HttpSession 读写到 Redis
session.setAttribute("user", userInfo); // 写入 Redis
session.getAttribute("user"); // 从 Redis 读取
十、接口限流(滑动窗口)
思路: 用 ZSet,score 存请求时间戳,每次请求:
-
删除窗口外的旧记录
-
统计窗口内记录数
-
超限则拒绝,否则加入当前请求
举例: 用户每分钟最多 5 次请求
ZREMRANGEBYSCORE rate:user:1001 0 <now-60> # 删60秒前的
ZCARD rate:user:1001 # 看当前窗口内次数
# 若 < 5 → ZADD rate:user:1001 <now> <uuid> # 放行并记录
十一、事务与 Lua 脚本
11.1 Redis 事务
| 命令 | 作用 |
|---|---|
MULTI |
开启事务,后续命令入队 |
EXEC |
一次性执行队列中所有命令 |
DISCARD |
放弃队列 |
WATCH key |
监视 Key,若在 EXEC 前被改 → EXEC 失败(乐观锁) |
关键区别:Redis 事务不支持回滚! 某条命令失败,其他命令照常执行。
举例:
WATCH inventory:sku001
GET inventory:sku001 # 读到库存 10
MULTI
DECRBY inventory:sku001 1 # 入队
EXEC # 若期间库存被改过 → 返回空 → 重试
11.2 Lua 脚本
什么时候用 Lua: 需要"读 → 判断 → 写"原子完成,中间不能被打断。
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey "hello"
Lua vs 事务:
| 维度 | 事务 | Lua 脚本 |
|---|---|---|
| 条件分支 | 不能(无法根据结果分支) | 可以 if/while |
| 原子性 | EXEC 时连续执行 | 整段脚本一次跑完 |
| 适用场景 | 简单批量 | 锁、限流、库存校验 |
十二、发布订阅(Pub/Sub)
模式: 发布者 → 频道 → 所有订阅者(广播)
| 命令 | 作用 |
|---|---|
SUBSCRIBE channel |
订阅频道 |
PUBLISH channel msg |
向频道发消息 |
PSUBSCRIBE pattern |
模式匹配订阅(如 news.*) |
举例:
# 终端1(订阅者)
SUBSCRIBE chat.room1
# 终端2(发布者)
PUBLISH chat.room1 "大家好" # 返回 1(1个订阅者收到)
局限:
-
消息不持久化,离线收不到
-
无 ACK 确认,可能丢消息
-
只适合实时广播,不适合可靠消息传递
Pub/Sub vs Stream: 要广播选 Pub/Sub,要可靠队列选 Stream。
十三、主从复制与高可用
13.1 主从复制
架构: 一个 Master(写)→ 多个 Replica(读,只读)
同步原理:
-
全量同步: 初次连接,主把 RDB 快照发给从
-
增量同步: 后续主上每条写命令持续同步到从
配置: REPLICAOF <master-ip> <port>(从节点执行)
注意: 主从复制是异步的,读从可能读到略微过期的数据。
13.2 哨兵模式(Sentinel)
解决什么问题: 主挂了,自动把从提升为新主,不需要人工改配置。
三大功能:
-
监控: 周期性检查主、从、哨兵状态
-
通知: 异常时通知管理员
-
自动故障转移: 主不可用时选举新主
部署: 多个哨兵(奇数个,如 3 个)投票决策,避免单哨兵误判。
13.3 集群模式(Cluster)
解决什么问题: 数据量大到单机内存装不下,需要分片。
原理: 16384 个哈希槽分布在多个主节点上,每个 Key 通过 CRC16(key) % 16384 定位到对应槽/节点。
限制: 跨槽的多 Key 操作受限(如事务、Lua、交集运算需同槽)。
13.4 选择建议
| 场景 | 方案 |
|---|---|
| 单机够用,需自动故障转移 | 主从 + 哨兵 |
| 数据量大或单主写瓶颈 | Redis Cluster |
| 学习测试 | 单机 |