一、Redis为什么快
Redis 之所以极快,核心是纯内存操作 + 高效 IO 模型 + 极简高效的数据结构与指令,主要有这几点:
- **完全基于内存操作。**数据直接存在内存,避免了磁盘I/O瓶颈。
- 单线程设计。线程处理所有网络 IO 和命令执行,无锁竞争、无线程切换损耗 。Redis 6.0+ 只是网络 IO 多线程,命令执行依然单线程
- IO 多路复用。 使用 epoll/kqueue/select 等机制,单线程同时监听大量连接
- 高效数据结构**。**底层使用 SDS、压缩列表、跳表、哈希表等优化结构。大部分命令都是 O (1) 或 O (logN) 时间复杂度。简单指令(如 GET/SET)几乎一步到位。
- 协议简单、序列化开销极低
注意:Redis 单线程指网络 IO + 命令执行,持久化、异步删除是后台线程。
二、5种基本数据结构
1. String(字符串)
- 最简单的 key-value 结构
- 可存:字符串、数字、二进制
- 常用命令:
SET、GET、INCR、DECR - 使用场景:缓存、计数器、分布式锁
2. Hash(哈希)
- 类似 Java 的
HashMap,一个 key 对应多个 field-value - 结构:
key → {field1:value1, field2:value2...} - 常用命令:
HSET、HGET、HMGET、HGETALL - 使用场景:存储对象(用户信息、商品详情)
3. List(列表)
- 有序可重复的字符串链表
- 支持两头操作:
LPUSH、RPUSH、LPOP、RPOP - 可做简单队列、栈
- 使用场景:消息队列、时间线、最新列表
4. Set(集合)
- 无序、不可重复的字符串集合
- 支持交集、并集、差集
- 常用命令:
SADD、SREM、SMEMBERS、SINTER - 场景场景:去重、共同好友、抽奖、点赞
5. ZSet(有序集合)
- 每个元素带 score(分数),按 score 排序
- 元素唯一,score 可重复
- 底层用跳表实现,查询高效
- 常用:
ZADD、ZRANGE、ZSCORE、ZRANK - 场景:排行榜、热度排序、延时队列
三、5种高级数据结构
1. HyperLogLog(基数统计)
- 作用:做海量数据的去重计数(UV(Unique Visitor)独立访客、日活)
- 特点:
- 占用内存极小
- 有微小误差(约 0.81%)
- 不保存具体数据,只计数
- 常用命令:
PFADD、PFCOUNT、PFMERGE - 使用场景:统计网站 UV、独立访客数
2. Bitmap(位图)
- 本质:String 类型的位操作扩展
- 特点:
- 极省空间,1 个 key 可存 512M 位
- 按 bit 位 0/1 表示状态
- 常用命令:
SETBIT、GETBIT、BITCOUNT、BITOP - 使用场景:用户签到、在线状态、活跃统计、布隆过滤器基础
- 使用位图统计的原理:Bitmap 利用 String 二进制位存储状态 ,1bit 代表一个用户 / 事件,通过 BITCOUNT、BITOP 实现高效统计,内存占用极低、速度极快。
- Bitmap VS HyperLogLog
| 维度 | Bitmap | HyperLogLog |
|---|---|---|
| 精准度 | 100% 精准 | 不精准,标准误差 ≈0.81% |
| 内存占用 | 与 用户 ID 最大值 相关1 亿用户 ≈ 12MB | 固定 12KB 左右无论多少数据都不变 |
| 能否查单个用户 | 可以:判断某个用户是否存在 | 不可以,只存统计信息 |
| 能否做交并差 | 支持:BITOP 做多天活跃、留存 | 不支持,只能合并计数 |
| 底层结构 | String 二进制位 | 概率算法 + 字节数组 |
| 统计上限 | 理论 2^32,受 String 512MB 限制 | 2^64,几乎无上限 |
| 适用场景 | 精准签到、留存、在线状态 | 海量 UV、日活、访客统计 |
总结:
- Bitmap:精准、占空间、支持位运算、适合签到留存。
- HyperLogLog:不精准、超省内存、只计数、适合海量 UV。
- 一句话:要精准用 Bitmap,要省空间用 HLL。
3. Geospatial(GEO 地理位置)
- 作用:存储经纬度,计算距离、附近的人
- 底层:ZSet 封装
- 常用命令:
GEOADD、GEORADIUS、GEODIST - 使用场景:附近的人、外卖配送距离、滴滴打车
4. Stream(消息流)
- 作用:持久化消息队列,支持多消费者、消费组
- 特点:
- 真正意义上的 MQ 结构
- 支持回溯、ACK、堆积
- 常用命令:
XADD、XREAD、XGROUP、XACK - 使用场景:异步任务、日志采集、高可靠消息
5. Bloom Filter(布隆过滤器)
- 作用:快速判断一个元素是否存在,防缓存穿透
- 原理:用多个哈希函数算出多个位置,全部置 1;判断时只要有一个是 0,就一定不存在;全是 1,可能存在。
- 特点:
- 空间极省、速度极快
- 有一定误判率:存在可能不存在,不存在一定不存在
- 常用命令:Redis 官方无内置命令,一般通过 Redisson / 自定义模块 实现
- 业务场景:防止缓存穿透(查询不存在的 key 不打数据库)、爬虫去重、黑名单过滤
四、过期策略
1. 惰性删除(Lazy Expiration)
- 用到 key 时才检查是否过期
- 过期就删,返回不存在
- 优点:省 CPU
- 缺点:占内存,过期 key 不用就一直占着
2. 定期删除(Periodic Expiration)
- 后台定时随机抽查一批 key
- 发现过期就删除
- 控制频率和数量,避免卡顿
- 平衡 CPU 和内存
五、淘汰策略
配置项:maxmemory-policy
1. 不淘汰(默认)
- noeviction
- 内存满了,新写入直接报错
2. 只针对设置了过期时间的 key
- volatile-lru: 淘汰最近最少使用的过期 key(最常用)
- volatile-lfu: 淘汰使用频率最少的过期 key
- **volatile-random:**随机淘汰过期 key
- volatile-ttl: 淘汰即将过期的 key(TTL 越小越先删)
3. 针对所有 key
- **allkeys-lru:**所有 key 里,淘汰最近最少使用(非常常用)
- **allkeys-lfu:**所有 key 里,淘汰使用频率最少
- **allkeys-random:**随机淘汰
注:LRU vs LFU 区别
- LRU:看最后一次使用时间
- LFU:看使用次数,更适合热点数据
六、数据与缓存一致性
Redis 是缓存,不是数据库,无法做到强一致 。所有方案目标都是:业务可接受的最终一致性,尽量减少不一致时间窗口。
1. 先更新数据库,再删除缓存(标准方案)
流程:
(1) 更新 DB
(2) 删除 Redis 缓存
(3) 下次查询时,自动从 DB 加载并回种缓存
为什么是删除,不是更新?
- 更新缓存浪费性能(可能根本没人读)
- 高并发下更容易不一致
- 懒加载更安全、更简单
为什么不推荐:先删缓存,再更新 DB?
容易出现脏数据:
① 线程 A 删缓存
② 线程 B 查询,缓存未命中,读旧 DB
③ 线程 A 更新 DB
④ 线程 B 把旧数据写回缓存→ 缓存脏数据,一直不一致
所以禁止使用。
高并发下,"先更 DB 再删缓存" 也会短暂不一致
场景:
① 线程 A 查询,缓存未命中,读 DB 旧值
② 线程 B 更新 DB,删除缓存
③ 线程 A 把旧值写入缓存→ 短暂不一致,但只会发生一次,下一次就正常
这种情况:
- 概率极低
- 时间窗口极短
- 属于最终一致性,业务可接受
2. 延迟双删(解决上面问题)
步骤:
① 删缓存
② 更新 DB
③ 延迟几百 ms(等并发读完成)
④ 再删一次缓存
优点:
- 极大降低不一致概率缺点:
- 引入延迟,耦合业务
3. 最可靠方案:异步更新缓存(订阅 binlog)
也就是 Canal + RabbitMQ/Kafka 机制:
① 业务只操作 DB
② Canal 监听 MySQL binlog
③ 解析后异步删除 / 更新 Redis
④ 保证顺序执行,最终一致
优点:
- 与业务解耦
- 一致性极高
- 高并发下最稳定
企业通用方案:缓存最终一致 + 兜底熔断
七、缓存3大问题
缓存最大、最核心的问题,是缓存与数据库的数据不一致 ,以及由此引发的缓存穿透、击穿、雪崩三大高可用问题**。**
1. 缓存穿透
- 现象:查根本不存在的数据
- 原因:查不存在的数据,请求直达数据库。数据库压力巨大,容易被打崩
- 解决:缓存空值、**布隆过滤器、**接口层参数校验
2. 缓存击穿
- 现象:某个热点 Key 过期
- 原因:热点 key 过期,高并发下大量请求同时穿透到数据库
- 解决:互斥锁(redlock/setnx)、热点 key 永不过期、后台定时刷新
3. 缓存雪崩
- 现象:大量 Key 同时过期 / Redis 节点宕机
- 原因:全部流量压到数据库,直接宕库
- 解决:
- 过期时间加随机值
- 集群高可用
- 多级缓存
- 服务降级、熔断限流