跳到主要内容

核心机制

一、数据结构与底层实现

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 文件

触发方式:

  1. 自动触发

    • 配置文件中 save 900 1(900 秒内至少 1 个 key 变化)
    • save 300 10(300 秒内至少 10 个 key 变化)
    • save 60 10000(60 秒内至少 10000 个 key 变化)
  2. 手动触发

    • 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 次随机抽查
  • **内存不足时:**主动删除 + 内存淘汰策略

定期删除的执行流程:

  1. 随机抽取 20 个 key
  2. 删除其中过期的 key
  3. 如果过期 key 比例 > 25%,重复步骤 1-2
  4. 最多执行 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";
}

面试题:如何设计一个高可用的缓存架构?

答案:

  1. 多级缓存:本地缓存 + Redis 缓存
  2. 缓存预热:系统启动时加载热点数据
  3. 过期策略:基础过期时间 + 随机值
  4. 高可用:主从 + 哨兵/集群
  5. 监控告警:监控缓存命中率、响应时间
  6. 降级限流:异常情况下降级服务
  7. 数据备份: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)取决于配置取决于持久化策略

事务错误处理:

  1. 语法错误(入队前):所有命令都不执行
MULTI
SET key value
WRONGCOMMAND # 语法错误
EXEC
# 输出:(error) EXECABORT Transaction discarded
  1. 运行时错误(执行中):跳过错误命令,继续执行
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 主从复制

工作原理:

  1. 建立连接:从节点发送 SYNC 命令
  2. 全量同步:主节点生成 RDB 文件并发送给从节点
  3. 增量同步:主节点记录写命令到复制缓冲区
  4. 命令传播:主节点执行写命令后发送给从节点

配置:

# 从节点配置
replicaof <masterip> <masterport>
masterauth <master-password>
replica-serve-stale-data yes

复制类型:

  • 全量复制:初次连接或复制偏移量丢失
  • 增量复制:基于复制偏移量的增量同步

面试题:主从复制的缺点是什么?

答案:

  • 主节点故障:需要手动切换
  • 单点写入:所有写操作都在主节点
  • 复制延迟:从节点存在数据延迟

7.2 哨兵模式(Sentinel)

作用:

  • 监控:监控主从节点健康状态
  • 自动故障转移:主节点故障时自动选举新的主节点
  • 配置中心:提供主节点地址信息
  • 通知:故障时通知管理员

工作原理:

  1. 主观下线(SDOWN)

    • 单个哨兵认为主节点下线
    • 条件:down-after-milliseconds 内无响应
  2. 客观下线(ODOWN)

    • 多个哨兵(quorum)认为主节点下线
    • 达到法定人数时触发
  3. 故障转移

    • 选举领头哨兵
    • 选择新主节点(优先级、复制偏移量、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

选举新主节点的优先级:

  1. slave-priority(优先级,默认 100)
  2. 复制偏移量(数据越新越优先)
  3. 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

集群特性:

  • 自动分片:数据自动分布到不同节点
  • 高可用:每个主节点有从节点
  • 在线扩容:动态添加节点
  • 客户端重定向MOVEDASK 重定向

重定向示例:

# 请求到错误节点
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 性能?

答案:

监控工具:

  1. redis-cli
# 查看性能指标
redis-cli info stats
redis-cli info memory

# 慢查询
redis-cli slowlog get 10
  1. Redis Monitor
# 实时监控命令执行(谨慎使用)
redis-cli monitor
  1. Prometheus + Grafana
  • 使用 redis_exporter 导出指标
  • Grafana 可视化监控

优化策略:

  1. 避免慢查询KEYSHGETALLSMEMBERS
  2. 使用 Pipeline:减少网络往返
  3. Lua 脚本:原子执行多个命令
  4. 控制连接数:使用连接池
  5. 内存优化:设置最大内存,避免 OOM
  6. 持久化优化:RDB + AOF 混合持久化
  7. 主从读写分离:主节点写,从节点读

九、常见面试题汇总

9.1 基础题

Q1: Redis 为什么这么快?

答案:

  1. 纯内存操作:内存访问速度快
  2. 单线程模型:避免线程切换和锁竞争
  3. IO 多路复用:epoll/kqueue 实现高并发
  4. 高效数据结构:跳表、压缩列表等

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(扣减库存 + 去重)

消息队列(削峰填谷)

订单服务(异步处理)

数据库(最终一致)

关键点:

  1. 库存预热:提前加载到 Redis
  2. Lua 脚本:保证原子性
  3. 用户去重:避免重复抢购
  4. 消息队列:异步处理订单
  5. 限流降级:保护系统

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 核心知识点回顾

  1. 数据结构:String、Hash、List、Set、Sorted Set
  2. 持久化:RDB、AOF、混合持久化
  3. 过期策略:惰性删除 + 定期删除
  4. 淘汰策略:LRU、LFU、随机等
  5. 缓存问题:穿透、击穿、雪崩
  6. 事务:MULTI/EXEC、WATCH
  7. 高可用:主从、哨兵、集群
  8. 性能优化:避免 bigkey、Pipeline、Lua 脚本

10.2 面试准备建议

  1. 理解原理:不要死记硬背,理解底层原理
  2. 对比分析:对比不同方案的优缺点
  3. 场景应用:结合实际场景理解
  4. 实践经验:分享实际项目中的使用经验
  5. 源码阅读:有余力可以阅读 Redis 源码

10.3 学习资源

  • 官方文档https://redis.io/documentation
  • 源码分析:《Redis 设计与实现》(黄健宏)
  • 实战案例:《Redis 开发与运维》(付磊)
  • 在线教程:Redis 官方教程

参考文档: