一、Redis 基础
1.1 Redis 是什么?有什么特点?
Redis(Remote Dictionary Server)是一个开源的、基于内存的高性能键值对数据库。主要特点:
| 特性 | 说明 |
|---|---|
| 高性能 | 基于内存操作,读写速度可达 10 万次/秒以上 |
| 数据结构丰富 | 支持 String、Hash、List、Set、Sorted Set 等类型 |
| 持久化 | 支持 RDB 快照和 AOF 日志两种持久化方式 |
| 单线程 | 核心操作使用单线程,避免锁竞争 |
| 支持事务 | 提供事务机制,保证命令的原子性执行 |
| 主从复制 | 支持主从架构,实现读写分离和高可用 |
1.2 为什么 Redis 这么快?
Redis 的高性能源于多个方面:
-
基于内存操作:数据存储在内存中,避免了磁盘 I/O 的开销
-
单线程模型:避免了多线程的上下文切换和锁竞争
-
I/O 多路复用:使用 epoll(Linux)实现高并发连接处理
-
高效的数据结构:针对不同场景优化了底层实现
传统数据库:客户端 → 网络 → 线程池 → 磁盘 I/O → 返回Redis:客户端 → 网络 → 单线程事件循环 → 内存操作 → 返回1.3 Redis 为什么采用单线程?
Redis 的单线程指的是网络请求处理使用单线程,而非整个 Redis 进程只有一个线程。实际上 Redis 还有后台线程处理持久化、异步删除等任务。
单线程的优势:
- 避免锁竞争:不需要加锁解锁,简化实现
- 避免上下文切换:CPU 不用在多线程间切换
- 代码简洁:更容易维护和调试
Redis 6.0 引入了多线程来处理网络 I/O,但核心命令执行仍是单线程。
二、数据结构
2.1 Redis 有哪些数据类型?各自的应用场景是什么?
| 类型 | 底层实现 | 典型场景 | 示例命令 |
|---|---|---|---|
| String | SDS(简单动态字符串) | 缓存、计数器、分布式锁 | SET、GET、INCR |
| Hash | Dict + listpack | 对象存储、购物车 | HSET、HGET、HMGET |
| List | QuickList | 消息队列、最新列表 | LPUSH、LRANGE、BRPOP |
| Set | Dict + intset | 标签、共同好友、抽奖 | SADD、SINTER、SRANDMEMBER |
| Sorted Set | Dict + SkipList | 排行榜、延迟队列 | ZADD、ZRANGE、ZSCORE |
2.2 String 类型的底层实现是什么?
Redis 3.2 之后,String 类型根据存储内容使用不同的编码:
// SDS(Simple Dynamic String)结构struct sdshdr { uint8_t len; // 已使用长度 uint8_t alloc; // 分配的总空间 uint8_t flags; // 类型标识 char buf[]; // 字节数组};编码类型:
| 编码类型 | 条件 | 说明 |
|---|---|---|
| int | 整数值,范围在 long 内 | 直接存储整数 |
| embstr | 字符串长度 ≤ 44 字节 | 连续内存,一次分配 |
| raw | 字符串长度 > 44 字节 | SDS 结构 |
SDS 相比 C 字符串的优势:
- O(1) 获取长度:直接读取
len属性 - 防止缓冲区溢出:自动扩展空间
- 减少内存分配:空间预分配和惰性释放
- 二进制安全:可以存储任意二进制数据
2.3 Hash 类型的底层实现?
Hash 类型根据数据量使用不同编码:
listpack 编码(小数据量):
- 元素数量 < 512,所有键值长度 < 64 字节
- 连续内存存储,紧凑高效
Dict 编码(大数据量):
- 超过阈值时转换为哈希表
- O(1) 时间复杂度的读写
# Hash 应用示例:购物车HSET cart:user:1001 item:001 2 # 商品 001,数量 2HSET cart:user:1001 item:002 1 # 商品 002,数量 1HGET cart:user:1001 item:001 # 获取商品 001 数量HGETALL cart:user:1001 # 获取整个购物车2.4 List 类型的底层实现?
Redis 3.2 之后,List 使用 QuickList(快速列表)作为底层实现:
QuickList 结构:┌─────────────────────────────────────────────┐│ Node 1 │ Node 2 │ Node 3 │ ... ││ (listpack)│ (listpack)│ (listpack)│ │└─────────────────────────────────────────────┘ ↕ ↕ ↕ 双向链表连接各节点QuickList 是双向链表 + listpack的组合:
- 双向链表:便于两端操作(LPUSH/RPUSH/LPOP/RPOP)
- listpack:每个节点存储多个元素,减少内存碎片
# List 应用示例:消息队列LPUSH queue:orders "order:001" # 生产者:左边推入LPUSH queue:orders "order:002"BRPOP queue:orders 5 # 消费者:右边阻塞弹出2.5 Set 和 Sorted Set 的底层实现?
Set 类型:
| 编码 | 条件 | 说明 |
|---|---|---|
| intset | 全是整数,数量 < 512 | 有序整数数组 |
| Dict | 其他情况 | 哈希表 |
Sorted Set 类型:
使用 Dict + SkipList(跳表) 的组合结构:
SkipList 结构示意:
Level 4: 1 ──────────────────────────→ 100Level 3: 1 ────────────→ 50 ────────→ 100Level 2: 1 ────→ 25 ───→ 50 ────→ 75 → 100Level 1: 1 → 10 → 25 → 50 → 60 → 75 → 100跳表的优势:
- O(logN) 查找:类似二分查找的效率
- 范围查询高效:底层双向链表便于遍历
- 实现简单:相比平衡树更容易实现
Dict 存储 member→score 映射,SkipList 按 score 排序存储:
# Sorted Set 应用示例:排行榜ZADD leaderboard 100 player:001ZADD leaderboard 150 player:002ZADD leaderboard 120 player:003ZREVRANGE leaderboard 0 9 WITHSCORES # 获取 Top 10三、持久化机制
3.1 Redis 持久化有哪些方式?
Redis 提供三种持久化方式:
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| RDB | 定时快照 | 恢复快、文件小 | 可能丢失数据 |
| AOF | 追加写命令日志 | 数据完整性好 | 文件大、恢复慢 |
| 混合 | RDB + AOF | 兼具两者优点 | 需要 Redis 4.0+ |
3.2 RDB 持久化是如何工作的?
RDB(Redis Database)是 Redis 的快照持久化方式:
触发方式:
# 配置自动触发save 900 1 # 900 秒内有 1 次写操作save 300 10 # 300 秒内有 10 次写操作save 60 10000 # 60 秒内有 10000 次写操作
# 手动触发SAVE # 阻塞主进程(不推荐)BGSAVE # 后台 fork 子进程执行执行流程:
1. Redis 主进程 fork 子进程2. 子进程将内存数据写入临时 RDB 文件3. 写入完成后,原子替换旧 RDB 文件4. 子进程退出特点:
- 适合备份:二进制格式,压缩率高
- 恢复快速:直接加载到内存
- 可能丢数据:两次快照之间的数据会丢失
3.3 AOF 持久化是如何工作的?
AOF(Append Only File)记录所有写命令:
工作流程:
写命令 → 写入 AOF 缓冲区 → 根据策略同步到磁盘同步策略:
| 策略 | 说明 | 性能 | 安全性 |
|---|---|---|---|
always | 每次写命令都同步 | 低 | 最高 |
everysec | 每秒同步一次(默认) | 中 | 高 |
no | 由操作系统决定 | 高 | 低 |
AOF 重写:
随着写操作增加,AOF 文件会越来越大。Redis 提供重写机制:
# 手动触发重写BGREWRITEAOF
# 自动触发配置auto-aof-rewrite-percentage 100 # 文件比上次重写后增长 100%auto-aof-rewrite-min-size 64mb # 文件至少 64MB重写原理:fork 子进程,遍历当前内存数据,生成最简命令序列。
3.4 混合持久化是什么?
Redis 4.0 引入混合持久化,结合 RDB 和 AOF 的优点:
# 开启混合持久化aof-use-rdb-preamble yesAOF 文件结构:
┌─────────────────────────────────────────┐│ RDB 格式数据(快照) │├─────────────────────────────────────────┤│ AOF 格式数据(增量命令) │└─────────────────────────────────────────┘优势:
- 快速恢复:先加载 RDB 快照
- 数据完整:追加 AOF 增量命令
- 文件紧凑:RDB 压缩率高
四、集群与高可用
4.1 Redis 主从复制是如何工作的?
主从复制实现数据冗余和读写分离:
复制流程:
1. 从节点发送 PSYNC 命令2. 主节点执行 BGSAVE 生成 RDB3. 主节点发送 RDB 给从节点4. 从节点加载 RDB5. 主节点发送增量写命令(replication buffer)复制类型:
| 类型 | 说明 |
|---|---|
| 全量复制 | 首次同步,传输完整 RDB |
| 部分复制 | 网络断开后重连,传输缺失的命令 |
关键配置:
replicaof <master-ip> <master-port> # 从节点配置masterauth <password> # 主节点密码replica-read-only yes # 从节点只读4.2 Redis Sentinel 是什么?
Sentinel(哨兵)是 Redis 的高可用解决方案:
核心功能:
- 监控:检测主从节点是否正常
- 通知:通知管理员或其他应用
- 自动故障转移:主节点故障时选举新主节点
工作原理:
┌─────────────┐│ Sentinel 1 │──┐├─────────────┤ │ ┌─────────┐│ Sentinel 2 │──┼───→│ Master │├─────────────┤ │ └─────────┘│ Sentinel 3 │──┘ │└─────────────┘ ┌────┴────┐ ↓ ↓ ┌─────────┐ ┌─────────┐ │ Slave 1 │ │ Slave 2 │ └─────────┘ └─────────┘故障转移流程:
- 多数 Sentinel 认为主观下线 → 客观下线
- Sentinel 集群选举领导者
- 从节点中选举新主节点
- 其他从节点复制新主节点
- 客户端更新连接
4.3 Redis Cluster 是什么?
Redis Cluster 是 Redis 的分布式解决方案,支持数据分片:
架构特点:
16384 个槽位均匀分布在多个节点
Node A: 0-5460 槽位Node B: 5461-10922 槽位Node C: 10923-16383 槽位
每个节点负责一部分槽位数据分片:
# 客户端请求SET user:1001 "data"↓CRC16("user:1001") % 16384 = 槽位号↓路由到对应节点特点:
- 去中心化:无中心节点,每个节点都知道完整拓扑
- 自动分片:数据自动分布到多个节点
- 高可用:每个主节点可以有多个从节点
- 自动故障转移:主节点故障时从节点升级
4.4 主从复制、Sentinel、Cluster 如何选择?
| 方案 | 数据量 | 特点 | 适用场景 |
|---|---|---|---|
| 主从复制 | 单机可承载 | 数据冗余、读写分离 | 读多写少 |
| Sentinel | 单机可承载 | 高可用、自动故障转移 | 需要高可用的场景 |
| Cluster | 超出单机 | 分布式、自动分片 | 大数据量、高并发 |
五、缓存问题
5.1 什么是缓存穿透?如何解决?
缓存穿透:查询不存在的数据,请求穿过缓存直接打到数据库。
客户端 → Redis(不存在)→ 数据库(也不存在)→ 返回空 ↑ 每次都穿透解决方案:
1. 布隆过滤器(Bloom Filter):
// 初始化时预热所有存在的 keyBloomFilter<String> filter = BloomFilter.create(Funnels.stringFunnel(), 1000000);for (String key : allKeys) { filter.put(key);}
// 查询时先判断if (!filter.mightContain(key)) { return null; // 一定不存在,直接返回}// 可能存在,继续查询缓存和数据库2. 缓存空值:
# 查询数据库为空时,也缓存一个空值SET nonexistent_key "" EX 60 # 设置较短过期时间5.2 什么是缓存击穿?如何解决?
缓存击穿:热点 key 在某一时刻过期,大量请求瞬间打到数据库。
热点 key 过期 ↓并发请求 → 缓存未命中 → 全部打到数据库解决方案:
1. 互斥锁:
public String getWithLock(String key) { String value = redis.get(key); if (value != null) { return value; }
// 尝试获取锁 String lockKey = "lock:" + key; if (redis.setnx(lockKey, "1", 10)) { // 10 秒超时 try { // 再次检查缓存(双检) value = redis.get(key); if (value == null) { value = db.query(key); // 查数据库 redis.set(key, value, 3600); // 写入缓存 } } finally { redis.del(lockKey); // 释放锁 } } else { // 等待后重试 Thread.sleep(50); return getWithLock(key); } return value;}2. 永不过期(逻辑过期):
# 物理上不设置过期时间SET hot_key "data"
# 数据中包含逻辑过期时间{ "data": "...", "expireTime": 1710000000 # 逻辑过期时间}
# 后台线程定期检查并异步更新5.3 什么是缓存雪崩?如何解决?
缓存雪崩:大量 key 同时过期,或 Redis 宕机导致大量请求打到数据库。
Redis 宕机或大量 key 同时过期 ↓请求 → 缓存失效 → 全部打到数据库 → 数据库崩溃解决方案:
1. 过期时间随机化:
// 基础过期时间 + 随机偏移int baseExpire = 3600;int randomExpire = baseExpire + new Random().nextInt(300); // 1 小时 + 0~5 分钟redis.set(key, value, randomExpire);2. 构建高可用集群:
Redis Cluster 或 Sentinel 模式 ↓主节点故障 → 自动切换到从节点3. 限流降级:
// 使用 Sentinel 或 Hystrix 进行熔断@SentinelResource(value = "getData", fallback = "getDataFallback")public String getData(String key) { return cacheService.get(key);}
public String getDataFallback(String key) { return "系统繁忙,请稍后重试"; // 降级返回}4. 缓存预热:
// 系统启动时预热热点数据@PostConstructpublic void warmUp() { List<String> hotKeys = getHotKeys(); for (String key : hotKeys) { String value = db.query(key); redis.set(key, value, getRandomExpire()); }}六、分布式锁
6.1 Redis 如何实现分布式锁?
基本实现:
# 加锁SET lock:resource unique_value NX PX 30000 # NX 不存在才设置,PX 过期时间
# 解锁(Lua 脚本保证原子性)if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1])else return 0end要点:
- NX:只有 key 不存在时才设置,保证互斥
- PX:设置过期时间,防止死锁
- 唯一值:使用 UUID 或线程 ID,防止误删其他客户端的锁
- Lua 解锁:检查和删除必须是原子操作
问题:
单节点 Redis 的锁在主从切换时可能不安全。如果客户端 A 加锁后,主节点宕机,从节点还没同步到锁信息就被提升为主节点,客户端 B 也能加锁成功。
6.2 什么是 RedLock?
RedLock(Redlock)是 Redis 作者提出的分布式锁算法,解决单节点故障问题:
算法步骤:
- 获取当前时间戳
- 按顺序向 N 个 Redis 节点请求加锁
- 计算加锁耗时,只有大多数节点加锁成功且耗时小于锁过期时间才算成功
- 加锁失败则向所有节点发送解锁请求
┌─────────┐│ Redis 1 │ ← 加锁成功├─────────┤│ Redis 2 │ ← 加锁成功├─────────┤│ Redis 3 │ ← 加锁失败├─────────┤│ Redis 4 │ ← 加锁成功├─────────┤│ Redis 5 │ ← 加锁成功└─────────┘
5 个节点,4 个成功 → 获得锁代码示例:
RedissonClient client1 = Redisson.create(config1);RedissonClient client2 = Redisson.create(config2);RedissonClient client3 = Redisson.create(config3);
RedissonMultiLock lock = new RedissonMultiLock(client1, client2, client3);lock.lock();try { // 执行业务逻辑} finally { lock.unlock();}6.3 Redisson 是什么?
Redisson 是 Redis 的 Java 客户端,提供了丰富的分布式对象和服务:
分布式锁实现:
// 可重入锁RLock lock = redisson.getLock("myLock");lock.lock();try { // 业务逻辑} finally { lock.unlock();}
// 公平锁RLock fairLock = redisson.getFairLock("myFairLock");
// 读写锁RReadWriteLock rwLock = redisson.getReadWriteLock("myRWLock");rwLock.readLock().lock();rwLock.writeLock().lock();
// 联锁(多锁组合)RLock lock1 = redisson.getLock("lock1");RLock lock2 = redisson.getLock("lock2");RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2);看门狗机制:
Redisson 提供自动续期功能,防止业务执行时间超过锁过期时间:
// 默认 30 秒过期,看门狗每 10 秒续期RLock lock = redisson.getLock("myLock");lock.lock(); // 自动续期
// 指定过期时间(不启用看门狗)lock.lock(10, TimeUnit.SECONDS);七、内存管理
7.1 Redis 的内存淘汰策略有哪些?
当内存使用达到上限(maxmemory)时,Redis 提供多种淘汰策略:
| 策略 | 说明 |
|---|---|
| noeviction | 不淘汰,写入报错(默认) |
| allkeys-lru | 从所有 key 中淘汰最近最少使用 |
| allkeys-lfu | 从所有 key 中淘汰最不常用 |
| allkeys-random | 从所有 key 中随机淘汰 |
| volatile-lru | 从设置了过期时间的 key 中淘汰 LRU |
| volatile-lfu | 从设置了过期时间的 key 中淘汰 LFU |
| volatile-random | 从设置了过期时间的 key 中随机淘汰 |
| volatile-ttl | 淘汰即将过期的 key |
配置方式:
maxmemory 4gb # 设置最大内存maxmemory-policy allkeys-lru # 设置淘汰策略7.2 LRU 和 LFU 有什么区别?
LRU(Least Recently Used):
淘汰最近最少使用的数据。基于时间局部性原理。
访问顺序:A → B → C → A → D内存满时淘汰:B(最久未被访问)
LRU 队列:[最近访问] D → A → C → B [最久访问]LFU(Least Frequently Used):
淘汰访问频率最低的数据。基于频率统计。
访问统计:A(3次) B(1次) C(2次) D(2次)内存满时淘汰:B(访问次数最少)Redis 实现:
Redis 4.0 引入 LFU,使用近似算法:
# 配置 LFU 相关参数lfu-log-factor 10 # 计数器增长因子lfu-decay-time 1 # 衰减时间(分钟)7.3 如何优化 Redis 内存使用?
1. 选择合适的数据结构:
# 小对象使用 Hash 优化# 原本:多个 StringSET user:1001:name "Alice"SET user:1001:age "25"
# 优化:单个 HashHSET user:1001 name "Alice" age "25"2. 使用压缩编码:
# 配置压缩阈值hash-max-listpack-entries 512hash-max-listpack-value 64zset-max-listpack-entries 128zset-max-listpack-value 643. 设置合理的过期时间:
# 避免数据堆积SET key value EX 3600 # 1 小时过期4. 避免大 Key:
# 检查大 Keyredis-cli --bigkeys
# 拆分大 Key# 原:一个大 HashHSET large:hash field1 value1 field2 value2 ... field1000 value1000
# 拆分:多个小 HashHSET large:hash:1 field1 value1HSET large:hash:2 field2 value2八、参考
参考
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






