核心机制
一、数据结构与底层实现
1.1 五种基本数据结构的底层实现
String(字符串)
底层实现:SDS(Simple Dynamic String)
struct sdshdr {
int len; // 字符串长度
int free; // 未使用空间
char buf[]; // 字节数组
};
SDS 相比 C 字符串的优势:
- O(1) 获取长度:不需要遍历整个字符串
- 防止缓冲区溢出:修改前会检查空间是否足够
- 减少内存重分配:空间预分配和惰性释放
- 二进制安全:可以存储任意二进制数据
编码方式:
- int:存储整数值(long 类型)
- embstr:存储小于等于 39 字节的字符串
- raw:存储大于 39 字节的字符串
Hash(哈希)
底层实现:
- ziplist(压缩列表):哈希对象保存的键值对数量小于 512 个,且所有键值对的键和值的字符串长度都小于 64 字节
- hashtable(哈希表):不满足上述条件时使用
// 哈希表节点
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next;
} dictEntry;
// 哈希表
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
List(列表)
底层实现:
- ziplist(压缩列表):列表元素数量小于 512 个,且所有元素长度小于 64 字节
- linkedlist(双向链表):不满足上述条件时使用
- quicklist(快速列表):Redis 3.2+,结合了 ziplist 和 linkedlist 的优点
quicklist 结构:
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count;
unsigned long len;
} quicklist;
typedef struct quicklistNode {
quicklistNode *prev;
quicklistNode *next;
unsigned char *zl;
unsigned int sz;
unsigned int count : 16;
unsigned int encoding : 2;
} quicklistNode;
Set(集合)
底层实现:
- intset(整数集合):集合中所有元素都是整数值,且元素数量不超过 512 个
- hashtable(哈希表):不满足上述条件时使用
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
Sorted Set(有序集合)
底层实现:
- ziplist:元素数量小于 128 个,且所有元素长度小于 64 字节
- skiplist + dict(跳跃表 + 字典):不满足上述条件时使用
跳跃表结构:
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
面试题:为什么 Sorted Set 同时使用 skiplist 和 dict?
答案:
- dict:O(1) 时间复杂度查找 member 对应的 score
- skiplist:O(log N) 时间复杂度进行范围操作和排序
- 两种结构通过指针共享数据,不浪费内存
二、持久化机制
2.1 RDB(Redis Database)
工作原理:
- Redis 会单独创建(fork)一个子进程来进行持久化
- 子进程将内存数据写入临时 RDB 文件
- 完成后用新文件替换旧 RDB 文件
触发方式:
-
自动触发:
- 配置文件中
save 900 1(900 秒内至少 1 个 key 变化) save 300 10(300 秒内至少 10 个 key 变化)save 60 10000(60 秒内至少 10000 个 key 变化)
- 配置文件中
-
手动触发:
SAVE命令:阻塞主进程,直到持久化完成BGSAVE命令:fork 子进程异步持久化
优点:
- 文件紧凑,适合备份和灾难恢复
- 恢复速度快,直接加载到内存
- 对性能影响小(子进程处理)
缺点:
- fork 子进程时会有短暂阻塞
- 数据可能丢失(最后一次快照后的修改)
- 不适合实时持久化
2.2 AOF(Append Only File)
工作原理:
- 记录所有修改命令(类似 MySQL binlog)
- 文件格式:Redis 协议格式
AOF 重写机制:
- 当 AOF 文件过大时,Redis 会重写 AOF 文件
- 重写不是读取原文件,而是读取内存数据生成新命令
- 触发条件:文件大小比上次重写后增长了一倍,且大于 64MB
刷盘策略(appendfsync):
- always:每个写命令都立即刷盘(最安全,性能最差)
- everysec:每秒刷盘一次(折中方案,推荐)
- no:由操作系统决定(性能最好,可能丢失数据)
优点:
- 数据安全性高,最多丢失 1 秒数据
- AOF 文件可读,便于误操作恢复
- 自动重写机制防止文件过大
缺点:
- 文件体积大
- 恢复速度慢于 RDB
- 性能开销高于 RDB
2.3 混合持久化(RDB + AOF)
Redis 4.0+ 支持:
- AOF 重写时,将 RDB 内容写入 AOF 文件开头
- 重写后的新命令继续追加
- 兼顾 RDB 的恢复速度和 AOF 的数据安全性
配置:
aof-use-rdb-preamble yes
面试题:RDB 和 AOF 如何选择?
答案:
- 只用于缓存:关闭持久化或使用 RDB
- 不能接受数据丢失:使用 AOF + everysec
- 追求恢复速度:使用混合持久化
- 数据量很大:优先使用 RDB
三、过期键删除策略
3.1 设置过期时间
EXPIRE key seconds # 设置过期时间(秒)
PEXPIRE key milliseconds # 设置过期时间(毫秒)
EXPIREAT key timestamp # 设置过期时间戳(秒)
PEXPIREAT key milliseconds-timestamp # 设置过期时间戳(毫秒)
3.2 三种删除策略
1. 惰性删除(Lazy Expiration)
工作原理:
- 访问 key 时才检查是否过期
- 过期则删除,不过期则返回数据
**优点:**CPU 开销小 **缺点:**浪费内存,已过期但未被访问的 key 占用空间
2. 定期删除(Periodic Expiration)
工作原理:
- 隔一段时间随机抽取一批 key 检查
- 删除过期的 key
- 如果过期 key 比例超过 25%,重复执行
**优点:**通过限制删除时长避免性能影响 **缺点:**仍可能残留过期 key
3. 主动删除(Active Expiration)
Redis 采用的策略:惰性删除 + 定期删除
面试题:Redis 如何保证过期 key 能被及时删除?
答案:
- 主策略:惰性删除,访问时检查
- 辅助策略:定期删除,每秒 10 次随机抽查
- **内存不足时:**主动删除 + 内存淘汰策略
定期删除的执行流程:
- 随机抽取 20 个 key
- 删除其中过期的 key
- 如果过期 key 比例 > 25%,重复步骤 1-2
- 最多执行 25ms(避免阻塞主线程)
四、内存淘汰策略
4.1 最大内存设置
maxmemory 256mb
4.2 淘汰策略分类
不淘汰数据
- noeviction:不淘汰,写入时返回错误
####淘汰所有 key
- allkeys-lru:优先淘汰最少使用的 key(LRU 算法)
- allkeys-lfu:优先淘汰最不经常使用的 key(LFU 算法,Redis 4.0+)
- allkeys-random:随机淘汰 key
- allkeys-volatile-lru:只在设置了过期时间的 key 中淘汰(LRU)
- volatile-lru:在设置了过期时间的 key 中淘汰(LRU)
- volatile-lfu:在设置了过期时间的 key 中淘汰(LFU)
- volatile-random:在设置了过期时间的 key 中随机淘汰
- volatile-ttl:优先淘汰即将过期的 key
4.3 LRU 算法实现
Redis 的近似 LRU 算法:
- 不是维护完整的 LRU 链表
- 每个 key 记录访问时间戳(24 位,约 19 天循环)
- 随机采样 5 个 key,淘汰最久未使用的
- 可以通过
maxmemory-samples调整采样数量(默认 5)
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:24; // LRU 时间戳
int refcount;
void *ptr;
} robj;
4.4 LFU 算法实现
Redis 4.0+ 的 LFU 算法:
- 使用计数器 + 衰减因子
- 访问时计数器增加
- 定期衰减(随着时间推移降低计数)
lru 字段的复用(LFU 模式):
- 高 16 位:计数器
- 低 8 位:衰减时间
面试题:为什么 Redis 不使用精确的 LRU 算法?
答案:
- 性能考虑:精确 LRU 需要维护双向链表,每次访问都要更新链表
- 内存考虑:额外存储链表指针占用大量内存
- 效果接近:采样算法在实际场景中效果接近精确 LRU
面试题:如何选择合适的淘汰策略?
答案:
- 缓存场景:allkeys-lru(保留热点数据)
- 需要区分热点:allkeys-lfu(更精准的热点识别)
- 有 TTL 优先:volatile-ttl(优先删除即将过期)
- 随机场景:allkeys-random(性能最好)
五、缓存异常问题
5.1 缓存穿透
问题描述:
- 查询不存在的数据
- 缓存中没有,直接查询数据库
- 恶意攻击导致数据库压力过大
解决方案:
1. 缓存空对象
# 查询不存在时缓存空值,设置短过期时间
SET key "" EX 60
2. 布隆过滤器(Bloom Filter)
- 将所有可能的 key 存入布隆过滤器
- 查询前先判断 key 是否存在
- 存在一定的误判率
// Guava BloomFilter 示例
BloomFilter<String> filter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
10000, // 预期元素数量
0.01 // 误判率
);
// 添加所有可能的 key
filter.put("user:1001");
// 查询前先检查
if (!filter.mightContain("user:1001")) {
return null; // 不存在,直接返回
}
3. 接口限流
- 对请求频率进行限制
- 使用 Redis + Lua 实现滑动窗口限流
面试题:布隆过滤器的原理和优缺点?
答案:
- 原理:位数组 + 多个哈希函数
- 优点:空间效率高,查询速度快
- 缺点:存在误判(false positive),不支持删除
5.2 缓存击穿
问题描述:
- 热点 key 过期的瞬间
- 大量请求直接打到数据库
解决方案:
1. 热点数据永不过期
# 不设置过期时间,后台异步更新
SET hot_key value
2. 互斥锁(Mutex Lock)
public String get(String key) {
// 1. 查询缓存
String value = redis.get(key);
if (value != null) {
return value;
}
// 2. 获取分布式锁
String lockKey = "lock:" + key;
try {
if (redis.setnx(lockKey, "1", 10)) {
// 3. 双重检查(防止并发重复查询)
value = redis.get(key);
if (value != null) {
return value;
}
// 4. 查询数据库
value = database.query(key);
// 5. 写入缓存
redis.setex(key, value, 3600);
return value;
}
} finally {
redis.del(lockKey);
}
// 6. 未获取到锁,短暂等待后重试
Thread.sleep(100);
return get(key);
}
3. 逻辑过期
# value 中包含过期时间
SET hot_key '{"value":"data","expire":1704067200}'
面试题:缓存穿透和缓存击穿的区别?
答案:
- 缓存穿透:查询不存在的数据,绕过缓存直接查数据库
- 缓存击穿:热点 key 过期,大量请求同时查询数据库
- 共同点:都会导致数据库压力过大
- 区别:击穿是热点 key 过期,穿透是查询不存在的 key
5.3 缓存雪崩
问题描述:
- 大量 key 同时过期
- 或 Redis 实例宕机
- 导致大量请求打到数据库
解决方案:
1. 过期时间加随机值
// 基础过期时间 + 随机 0-300 秒
int expire = baseExpire + new Random().nextInt(300);
redis.setex(key, value, expire);
2. 多级缓存
- 本地缓存(Caffeine/Guava Cache)
- 分布式缓存(Redis)
- 数据库
public String get(String key) {
// 1. 本地缓存
String value = localCache.get(key);
if (value != null) {
return value;
}
// 2. Redis 缓存
value = redis.get(key);
if (value != null) {
localCache.put(key, value, 60);
return value;
}
// 3. 查询数据库
value = database.query(key);
// 4. 写入多级缓存
redis.setex(key, value, 3600);
localCache.put(key, value, 60);
return value;
}
3. Redis 高可用
- 主从复制 + 哨兵
- Redis Cluster 集群
4. 服务降级与限流
// Hystrix 示例
@HystrixCommand(fallbackMethod = "getFallback")
public String get(String key) {
return redis.get(key);
}
public String getFallback(String key) {
// 降级逻辑:返回默认值或错误提示
return "Service Unavailable";
}
面试题:如何设计一个高可用的缓存架构?
答案:
- 多级缓存:本地缓存 + Redis 缓存
- 缓存预热:系统启动时加载热点数据
- 过期策略:基础过期时间 + 随机值
- 高可用:主从 + 哨兵/集群
- 监控告警:监控缓存命中率、响应时间
- 降级限流:异常情况下降级服务
- 数据备份:RDB + AOF 持久化
六、事务机制
6.1 事务命令
MULTI # 开启事务
EXEC # 执行事务
DISCARD # 取消事务
WATCH key # 乐观锁(监听 key)
UNWATCH # 取消监听
6.2 事务示例
# 开启事务
MULTI
# 添加命令(入队,不立即执行)
SET key1 value1
SET key2 value2
GET key1
# 执行事务
EXEC
6.3 事务特性
ACID 分析:
| 特性 | Redis 支持 | 说明 |
|---|---|---|
| 原子性(A) | 不支持 | 命令出错后仍会继续执行后续命令 |
| 一致性(C) | 支持 | 单机环境下一致 |
| 隔离性(I) | 支持 | 单线程命令执行,天然隔离 |
| 持久性(D) | 取决于配置 | 取决于持久化策略 |
事务错误处理:
- 语法错误(入队前):所有命令都不执行
MULTI
SET key value
WRONGCOMMAND # 语法错误
EXEC
# 输出:(error) EXECABORT Transaction discarded
- 运行时错误(执行中):跳过错误命令,继续执行
MULTI
SET key value
LPOP key # 类型错误
SET key2 value2
EXEC
# 输出:OK, (error), OK
6.4 WATCH 机制(乐观锁)
# 监听 key
WATCH balance
# 开启事务
MULTI
DECRBY balance 100
EXEC
# 如果 balance 被其他客户端修改,EXEC 返回 null
实现原理:
- WATCH 会监听 key,保存 key 的旧值
- EXEC 时检查 key 是否被修改
- 如果被修改,拒绝执行事务
面试题:Redis 事务和关系型数据库事务的区别?
答案:
- Redis 事务:不保证原子性,不支持回滚
- 关系型数据库事务:完整的 ACID 特性,支持回滚
- 使用场景:Redis 事务用于批量执行命令,不用于复杂业务逻辑
面试题:如何实现 Redis 乐观锁?
答案: 使用 WATCH 命令监听 key,配合事务实现:
public boolean transfer(String from, String to, int amount) {
while (true) {
// 1. 监听账户
redis.watch(from);
// 2. 检查余额
int balance = Integer.parseInt(redis.get(from));
if (balance < amount) {
redis.unwatch();
return false;
}
// 3. 开启事务
Transaction tx = redis.multi();
tx.decrBy(from, amount);
tx.incrBy(to, amount);
// 4. 执行事务
List<Object> result = tx.exec();
if (result != null) {
return true; // 成功
}
// 失败则重试
}
}
七、高可用与集群
7.1 主从复制
工作原理:
- 建立连接:从节点发送
SYNC命令 - 全量同步:主节点生成 RDB 文件并发送给从节点
- 增量同步:主节点记录写命令到复制缓冲区
- 命令传播:主节点执行写命令后发送给从节点
配置:
# 从节点配置
replicaof <masterip> <masterport>
masterauth <master-password>
replica-serve-stale-data yes
复制类型:
- 全量复制:初次连接或复制偏移量丢失
- 增量复制:基于复制偏移量的增量同步
面试题:主从复制的缺点是什么?
答案:
- 主节点故障:需要手动切换
- 单点写入:所有写操作都在主节点
- 复制延迟:从节点存在数据延迟
7.2 哨兵模式(Sentinel)
作用:
- 监控:监控主从节点健康状态
- 自动故障转移:主节点故障时自动选举新的主节点
- 配置中心:提供主节点地址信息
- 通知:故障时通知管理员
工作原理:
-
主观下线(SDOWN)
- 单个哨兵认为主节点下线
- 条件:
down-after-milliseconds内无响应
-
客观下线(ODOWN)
- 多个哨兵(
quorum)认为主节点下线 - 达到法定人数时触发
- 多个哨兵(
-
故障转移
- 选举领头哨兵
- 选择新主节点(优先级、复制偏移量、run ID)
- 从节点升级为主节点
- 通知其他从节点复制新主节点
配置:
# 哨兵端口
port 26379
# 监控主节点
sentinel monitor mymaster 127.0.0.1 6379 2
# 故障判断时间
sentinel down-after-milliseconds mymaster 30000
# 故障转移超时时间
sentinel failover-timeout mymaster 180000
# 同时同步的从节点数量
sentinel parallel-syncs mymaster 1
选举新主节点的优先级:
- slave-priority(优先级,默认 100)
- 复制偏移量(数据越新越优先)
- run ID(字典序越小越优先)
面试题:哨兵模式如何保证高可用?
答案:
- 多哨兵部署:至少 3 个节点,避免单点故障
- 奇数节点:2n+1 个节点可以容忍 n 个故障
- 自动故障转移:主节点故障时自动选举新主节点
- 客户端通知:提供 Pub/Sub 机制通知客户端主节点变更
7.3 Redis Cluster 集群
分片策略:
- 槽位(Slot):16384 个槽位
- Key 分布:
CRC16(key) % 16384 - 节点分配:每个节点负责部分槽位
集群架构:
节点A:槽位 0-5460
节点B:槽位 5461-10922
节点C:槽位 10923-16383
配置:
# 开启集群模式
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
# 集群总线端口
port 6379
cluster-port 16379
搭建集群:
# 创建集群
redis-cli --cluster create \
127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 127.0.0.1:7003 \
127.0.0.1:7004 127.0.0.1:7005 \
--cluster-replicas 1
集群特性:
- 自动分片:数据自动分布到不同节点
- 高可用:每个主节点有从节点
- 在线扩容:动态添加节点
- 客户端重定向:
MOVED和ASK重定向
重定向示例:
# 请求到错误节点
127.0.0.1:7000> GET key
# 返回:MOVED 12539 127.0.0.1:7002
# 客户端自动连接到 7002 节点
面试题:为什么 Redis Cluster 有 16384 个槽位?
答案:
- 足够多:保证数据均匀分布
- 不会太多:槽位信息占用内存(每个槽位 2KB)
- 计算方便:2^14,便于位运算和模运算
- 心跳包大小:槽位信息需要在节点间传输,不能过大
面试题:Redis Cluster 如何保证可用性?
答案:
- 主从复制:每个主节点配置从节点
- 故障检测:节点间通过 Gossip 协议交换状态
- 自动故障转移:主节点故障时从节点升级为主节点
- 槽位迁移:将故障主节点的槽位迁移到其他节点
八、性能优化
8.1 性能指标
关键指标:
- 响应时间:通常 < 1ms
- 内存使用率:建议 < 80%
- 命中率:缓存命中率 > 90%
- QPS:单机可达 10万+ QPS
- 连接数:避免连接数过多
8.2 性能优化技巧
1. 避免 bigkey
问题:
- 内存占用大
- 删除时阻塞主线程(DEL 命令)
- 网络传输慢
解决方案:
# 查找 bigkey
redis-cli --bigkeys
# 分批删除大 Hash
HSCAN key 0 MATCH field* COUNT 100
HDEL key field1 field2 ...
# 使用 UNLINK 替代 DEL(异步删除)
UNLINK bigkey
2. 批量操作
# Pipeline 减少网络往返
echo -en '*2\r\n$3\r\nGET\r\n$3\r\nkey1\r\n*2\r\n$3\r\nGET\r\n$3\r\nkey2\r\n' | nc localhost 6379
# Lua 脚本原子执行
EVAL "return redis.call('GET', KEYS[1])" 1 mykey
3. 合理使用数据结构
# 使用 Hash 代替多个 String
# 不推荐
SET user:1:name "Alice"
SET user:1:age 20
SET user:1:city "Beijing"
# 推荐
HMSET user:1 name "Alice" age 20 city "Beijing"
4. 选择合适过期策略
# 热点数据:永不过期
SET hot_key value
# 普通数据:基础过期时间 + 随机值
EXPIRE key 3600 + random(0, 300)
5. 使用连接池
// Lettuce 连接池
RedisClient client = RedisClient.create("redis://localhost");
RedisURI uri = RedisURI.builder()
.withHost("localhost")
.withPort(6379)
.build();
StatefulRedisConnection<String, String> connection = client.connect(uri);
面试题:如何监控和优化 Redis 性能?
答案:
监控工具:
- redis-cli
# 查看性能指标
redis-cli info stats
redis-cli info memory
# 慢查询
redis-cli slowlog get 10
- Redis Monitor
# 实时监控命令执行(谨慎使用)
redis-cli monitor
- Prometheus + Grafana
- 使用 redis_exporter 导出指标
- Grafana 可视化监控
优化策略:
- 避免慢查询:
KEYS、HGETALL、SMEMBERS - 使用 Pipeline:减少网络往返
- Lua 脚本:原子执行多个命令
- 控制连接数:使用连接池
- 内存优化:设置最大内存,避免 OOM
- 持久化优化:RDB + AOF 混合持久化
- 主从读写分离:主节点写,从节点读
九、常见面试题汇总
9.1 基础题
Q1: Redis 为什么这么快?
答案:
- 纯内存操作:内存访问速度快
- 单线程模型:避免线程切换和锁竞争
- IO 多路复用:epoll/kqueue 实现高并发
- 高效数据结构:跳表、压缩列表等
Q2: Redis 是单线程的吗?
答案:
- 网络请求处理:单线程(Redis 6.0 之前)
- 持久化:fork 子进程处理
- Redis 6.0+:引入多线程处理网络 IO(命令执行仍是单线程)
Q3: Redis 如何实现分布式锁?
答案:
# 1. 加锁(SET NX + 过期时间)
SET lock_key unique_value NX EX 10
# 2. 释放锁(Lua 脚本保证原子性)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
改进方案(Redlock):
- 多个 Redis 节点(5 个)
- 在大多数节点上获取锁成功才算成功
- 防止单点故障
9.2 进阶题
Q4: 如何保证缓存和数据库一致性?
答案:
方案 1:Cache Aside Pattern(旁路缓存)
// 读:先读缓存,没有则读数据库,再写入缓存
public User get(Long id) {
User user = redis.get("user:" + id);
if (user == null) {
user = database.query(id);
redis.setex("user:" + id, user, 3600);
}
return user;
}
// 写:先更新数据库,再删除缓存
public void update(User user) {
database.update(user);
redis.del("user:" + user.getId());
}
方案 2:延迟双删
public void update(User user) {
// 1. 删除缓存
redis.del("user:" + user.getId());
// 2. 更新数据库
database.update(user);
// 3. 延迟再删除缓存
Thread.sleep(1000);
redis.del("user:" + user.getId());
}
方案 3:订阅 Binlog(Canal)
- 监听 MySQL binlog
- 解析变更后更新缓存
- 最终一致性
Q5: 如何设计一个本地缓存 + 分布式缓存架构?
答案:
架构设计:
┌─────────┐
│ Client │
└────┬────┘
│
↓
┌─────────────────┐
│ Local Cache │ ← Caffeine/Guava(L1 缓存)
│ (热点数据) │
└────────┬────────┘
│ Miss
↓
┌─────────────────┐
│ Redis Cache │ ← Redis(L2 缓存)
│ (普通数据) │
└────────┬────────┘
│ Miss
↓
┌─────────────────┐
│ Database │
└─────────────────┘
实现示例:
public class MultiLevelCache {
private Cache<String, Object> localCache;
private RedisTemplate redisTemplate;
public Object get(String key) {
// L1: 本地缓存
Object value = localCache.getIfPresent(key);
if (value != null) {
return value;
}
// L2: Redis 缓存
value = redisTemplate.opsForValue().get(key);
if (value != null) {
localCache.put(key, value);
return value;
}
// L3: 数据库
value = database.query(key);
redisTemplate.opsForValue().set(key, value, 3600);
localCache.put(key, value);
return value;
}
}
缓存同步:
- 主动更新:写操作时删除所有层级的缓存
- 过期更新:本地缓存设置较短过期时间
- 消息通知:Redis Pub/Sub 通知其他节点更新本地缓存
Q6: Redis 如何实现消息队列?
答案:
方案 1:List(简单队列)
# 生产者
LPUSH queue:tasks task1
# 消费者
RPOP queue:tasks
# 阻塞式消费
BRPOP queue:tasks 0
方案 2:Pub/Sub(发布订阅)
# 订阅者
SUBSCRIBE channel:news
# 发布者
PUBLISH channel:news "message"
**缺点:**消息不持久化,离线消费者会丢失消息
方案 3:Stream(消息流,Redis 5.0+)
# 生产者
XADD stream:orders * name "order1" amount 100
# 消费者组
XGROUP CREATE stream:orders group1 0
# 消费消息
XREADGROUP GROUP group1 consumer1 COUNT 1 STREAMS stream:orders >
# ACK 确认
XACK stream:orders group1 message_id
特性:
- 支持消费者组
- 消息持久化
- 支持 ACK 确认机制
- 支持消息回溯
9.3 场景题
Q7: 设计一个秒杀系统(使用 Redis)
答案:
核心流程:
public class SeckillService {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private StringRedisTemplate stringRedis;
// 1. 预热库存到 Redis
public void preloadStock(Long productId, int stock) {
stringRedis.opsForValue().set("seckill:stock:" + productId, String.valueOf(stock));
}
// 2. 秒杀抢购
public boolean seckill(Long userId, Long productId) {
String key = "seckill:stock:" + productId;
String userKey = "seckill:ordered:" + productId + ":" + userId;
// 2.1 检查是否已抢购
if (Boolean.TRUE.equals(stringRedis.hasKey(userKey))) {
return false; // 重复抢购
}
// 2.2 Lua 脚本原子操作(减库存 + 记录用户)
String luaScript =
"if redis.call('get', KEYS[1]) == '0' then " +
" return 0 " +
"else " +
" redis.call('decr', KEYS[1]) " +
" redis.call('set', KEYS[2], '1') " +
" redis.call('expire', KEYS[2], 3600) " +
" return 1 " +
"end";
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long result = stringRedis.execute(script, Arrays.asList(key, userKey));
if (result == 1) {
// 2.3 发送消息到 MQ 异步处理订单
sendOrderMessage(userId, productId);
return true;
}
return false;
}
}
架构设计:
用户请求
↓
Nginx 负载均衡
↓
Redis(扣减库存 + 去重)
↓
消息队列(削峰填谷)
↓
订单服务(异步处理)
↓
数据库(最终一致)
关键点:
- 库存预热:提前加载到 Redis
- Lua 脚本:保证原子性
- 用户去重:避免重复抢购
- 消息队列:异步处理订单
- 限流降级:保护系统
Q8: 设计一个排行榜(Redis Sorted Set)
答案:
public class LeaderboardService {
@Autowired
private RedisTemplate redisTemplate;
// 1. 增加分数
public void addScore(String leaderboard, String member, double score) {
redisTemplate.opsForZSet().incrementScore(leaderboard, member, score);
}
// 2. 获取排名
public Long getRank(String leaderboard, String member) {
Long rank = redisTemplate.opsForZSet().reverseRank(leaderboard, member);
return rank != null ? rank + 1 : null;
}
// 3. 获取 Top N
public List<Map<String, Object>> getTopN(String leaderboard, int n) {
Set<ZSetOperations.TypedTuple<String>> set =
redisTemplate.opsForZSet().reverseRangeWithScores(leaderboard, 0, n - 1);
List<Map<String, Object>> result = new ArrayList<>();
int rank = 1;
for (ZSetOperations.TypedTuple<String> tuple : set) {
Map<String, Object> item = new HashMap<>();
item.put("rank", rank++);
item.put("member", tuple.getValue());
item.put("score", tuple.getScore());
result.add(item);
}
return result;
}
// 4. 获取用户范围排名(例如:我的排名前后 5 名)
public List<Map<String, Object>> getAroundRank(String leaderboard, String member, int n) {
Long rank = redisTemplate.opsForZSet().reverseRank(leaderboard, member);
if (rank == null) {
return Collections.emptyList();
}
long start = Math.max(0, rank - n);
long end = Math.min(redisTemplate.opsForZSet().size(leaderboard) - 1, rank + n);
Set<String> members = redisTemplate.opsForZSet().reverseRange(leaderboard, start, end);
// 查询分数
List<Map<String, Object>> result = new ArrayList<>();
for (String m : members) {
Double score = redisTemplate.opsForZSet().score(leaderboard, m);
Map<String, Object> item = new HashMap<>();
item.put("member", m);
item.put("score", score);
result.add(item);
}
return result;
}
}
使用场景:
- 游戏排行榜
- 直播榜
- 销售榜
- 点赞榜
十、总结
10.1 Redis 核心知识点回顾
- 数据结构:String、Hash、List、Set、Sorted Set
- 持久化:RDB、AOF、混合持久化
- 过期策略:惰性删除 + 定期删除
- 淘汰策略:LRU、LFU、随机等
- 缓存问题:穿透、击穿、雪崩
- 事务:MULTI/EXEC、WATCH
- 高可用:主从、哨兵、集群
- 性能优化:避免 bigkey、Pipeline、Lua 脚本
10.2 面试准备建议
- 理解原理:不要死记硬背,理解底层原理
- 对比分析:对比不同方案的优缺点
- 场景应用:结合实际场景理解
- 实践经验:分享实际项目中的使用经验
- 源码阅读:有余力可以阅读 Redis 源码
10.3 学习资源
- 官方文档:https://redis.io/documentation
- 源码分析:《Redis 设计与实现》(黄健宏)
- 实战案例:《Redis 开发与运维》(付磊)
- 在线教程:Redis 官方教程
参考文档:
- Redis 官方文档
- Redis 中文文档
- 《Redis 设计与实现》
- 《Redis 开发与运维》