Redis深度解析:把缓存核心讲透,吊打面试官
🎯 写在前面:Redis是后端开发的"核武器",面试必问、业务必用。但你真的理解Redis为什么这么快吗?缓存三兄弟(穿透、击穿、雪崩)到底怎么破?集群选型又该怎么选?这篇文章,带你彻底搞懂Redis!
前言
Redis,这个看起来简简单单的KV数据库,却是后端工程师必须掌握的核心技能。面试时,面试官总喜欢问:
"Redis为什么这么快?"
"缓存穿透、击穿、雪崩有什么区别?怎么解决?"
"Redis的数据结构有哪些?分别在什么场景下使用?"
"Redis集群怎么选?哨兵和集群模式有什么区别?"
这些问题,你能答上来几个?
今天这篇文章,我将从原理到实战,把Redis的核心知识点讲透,让你面试再也不慌。
一、Redis为什么这么快?—— 单线程I/O模型的秘密
1.1 面试高频问题
面试官灵魂拷问:
"Redis是单线程的,为什么还能这么快?"
1.2 答案解析
核心原因:单线程 + I/O多路复用
┌─────────────────────────────────────────────────────────────┐
│ Redis 单线程模型 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ 事件循环线程 │ │
│ │ (Event Loop) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Socket1 │ │ Socket2 │ │ Socket3 │ │
│ │ (GET) │ │ (SET) │ │ (DEL) │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
▲ ▲ ▲
│ │ │
客户端请求 客户端请求 客户端请求
1.3 I/O多路复用详解
什么是多路复用?
传统阻塞模型(效率低):
┌────────┐ ┌────────┐ ┌────────┐
│Client1 │────▶│Server │────▶│Client2 │
│Client3 │ │(Block) │ │Client4 │
└────────┘ └────────┘ └────────┘
问题:每个连接一个线程,线程切换开销大
I/O多路复用模型(Redis采用):
┌─────────────────────────────────────┐
│ 多路复用器 (Select/Epoll) │
├─────────────────────────────────────┤
│ Client1 ──▶│──▶ 可读事件队列 │
│ Client2 ──▶│──▶ 可写事件队列 │
│ Client3 ──▶│──▶ 错误事件队列 │
└─────────────┴───────────────────────┘
│
▼
┌───────────┐
│ 单线程处理 │
│ 事件循环 │
└───────────┘
1.4 Redis 6.0 多线程优化
// Redis 6.0 引入的 I/O 多线程
// 注意:这里的多线程只用于网络 I/O,不用于命令执行
// redis.conf 配置
io-threads 4 // I/O 线程数(建议 CPU 核数的一半)
io-threads-do-reads yes // 是否启用 I/O 线程读取
// 工作流程:
// 1. 主线程 accept() 接收连接
// 2. I/O 线程负责读取请求、解析命令
// 3. 主线程执行命令
// 4. I/O 线程负责写回响应
1.5 单线程的优势
| 优势 | 说明 |
|---|---|
| 无锁竞争 | 不需要考虑并发修改数据的问题 |
| CPU亲和 | 单线程绑定的CPU缓存命中率高 |
| 简单高效 | 避免了线程切换、锁等开销 |
| 原子操作 | 所有操作都是原子的 |
1.6 性能数据
Redis 性能指标:
├── QPS: 10万~100万+(单机)
├── 延迟: 亚毫秒级(< 1ms)
└── 内存: 基于内存操作
对比:
├── MySQL: 3000~5000 QPS
├── MongoDB: 1万~5万 QPS
└── Redis: 10万+ QPS
二、缓存三兄弟:穿透、击穿、雪崩
2.1 三兄弟全景图
┌────────────────────────────────────────────────────────────────┐
│ 缓存三兄弟对比图 │
├──────────┬──────────┬──────────┬──────────────────────────────┤
│ 类型 │ 定义 │ 发生场景 │ 核心特征 │
├──────────┼──────────┼──────────┼──────────────────────────────┤
│ 缓存穿透 │ 恶意攻击 │ 不存在数据│ 缓存和DB都没有,大量请求 │
│ 缓存击穿 │ 热点失效 │ 热点key过期│ 缓存过期瞬间,大量请求涌入 │
│ 缓存雪崩 │ 批量失效 │ 大量key过期│ 大量key同时过期,系统崩溃 │
└──────────┴──────────┴──────────┴──────────────────────────────┘
2.2 缓存穿透(Cache Penetration)
什么是缓存穿透?
请求流程:
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 请求 │───▶│ 缓存 │───▶│ DB │ │ 返回 │
│ │ │(miss) │ │ (miss) │ │ 空数据 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
问题:大量请求查询不存在的数据,每次都打到DB
解决方案1:接口参数校验
/**
* 方案一:基础参数校验
* 适用于:明显非法请求
*/
public User queryById(Long id) {
// 1. 参数校验
if (id == null || id <= 0) {
return null; // 非法参数直接返回
}
// 2. 查询缓存
User user = getFromCache(id);
if (user != null) {
return user;
}
// 3. 查询数据库
return queryFromDB(id);
}
解决方案2:布隆过滤器(推荐)
/**
* 方案二:布隆过滤器
* 适用于:数据量级大、存在合法/非法边界模糊的情况
*
* 原理:用一个大型bitmap,存储所有合法数据的指纹
* 特点:空间效率高,但存在误判(可能把不存在的判定为存在)
*/
// 引入依赖
// <dependency>
// <groupId>com.google.guava</groupId>
// <artifactId>guava</artifactId>
// <version>32.1.3-jre</version>
// </dependency>
import com.google.common.hash.BloomFilter;
@Service
public class BloomFilterService {
// 创建布隆过滤器,expectedInsertions=期望插入数量,fpp=误判率
private BloomFilter<Long> bloomFilter = BloomFilter.create(
Funnels.longFunnel(),
100000000, // 1亿数据
0.01 // 0.01% 误判率
);
/**
* 初始化布隆过滤器(从DB加载所有合法ID)
*/
@PostConstruct
public void init() {
List<Long> allUserIds = userMapper.selectAllIds();
allUserIds.forEach(bloomFilter::put);
}
/**
* 使用布隆过滤器判断
*/
public boolean mightContain(Long userId) {
return bloomFilter.mightContain(userId);
}
}
// 使用示例
public User queryUserById(Long userId) {
// 1. 先检查布隆过滤器
if (!bloomFilterService.mightContain(userId)) {
// 布隆过滤器说不存在,直接返回(不需要查DB)
return null;
}
// 2. 布隆过滤器说可能存在,继续查缓存和DB
User user = getFromCache(userId);
if (user != null) {
return user;
}
return queryFromDB(userId);
}
布隆过滤器参数选择
┌─────────────────────────────────────────────────────────────────┐
│ 布隆过滤器参数计算公式 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ n = 期望插入数量 │
│ p = 期望误判率 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ m = - (n * ln(p)) / (ln(2)^2) // 所需bit位 │ │
│ │ k = (ln(2) * m / n) // 哈希函数数量 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 示例:n=1亿,p=0.01% │
│ ├── m ≈ 1.44GB │
│ └── k = 17个哈希函数 │
│ │
│ 常见配置: │
│ ├── 1000万数据,1%误判 → 约140MB │
│ ├── 1亿数据,1%误判 → 约1.4GB │
│ └── 1亿数据,0.1%误判 → 约2.9GB │
│ │
└─────────────────────────────────────────────────────────────────┘
解决方案3:缓存空值
/**
* 方案三:缓存空值 + 短过期时间
* 适用于:不存在的数据是合法但暂时无数据的
*/
private static final String CACHE_NULL_PREFIX = "cache:null:";
private static final long CACHE_NULL_TTL = 60; // 空值缓存60秒
public User queryById(Long id) {
String cacheKey = "user:" + id;
// 1. 查询缓存
String cachedValue = redisTemplate.opsForValue().get(cacheKey);
if (cachedValue != null) {
// 2. 判断是否是空值
if ("NULL".equals(cachedValue)) {
return null; // 缓存的空值,直接返回
}
return JSON.parseObject(cachedValue, User.class);
}
// 3. 查询数据库
User user = userMapper.selectById(id);
// 4. 写入缓存(包含空值)
if (user == null) {
redisTemplate.opsForValue().set(cacheKey, "NULL", CACHE_NULL_TTL, TimeUnit.SECONDS);
} else {
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(user), CACHE_TTL, TimeUnit.HOURS);
}
return user;
}
2.3 缓存击穿(Cache Breakdown)
什么是缓存击穿?
热点key失效瞬间:
┌─────────────────────────────────┐
│ 大量请求同时涌入 │
└─────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 请求1 │ │ 请求2 │ │ 请求3 │
│(查DB) │ │(查DB) │ │(查DB) │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└───────────────┼───────────────┘
▼
┌─────────────────┐
│ 数据库被打爆 │
│ (连接池耗尽) │
└─────────────────┘
解决方案1:互斥锁(分布式锁)
/**
* 方案一:分布式锁 + 单线程回源
* 核心:只有一个请求去查DB,其他等待
*/
@Service
public class UserService {
@Autowired
private RedissonClient redissonClient;
public User queryUserById(Long id) {
String cacheKey = "user:" + id;
// 1. 先查缓存
User user = getFromCache(cacheKey);
if (user != null) {
return user;
}
// 2. 获取分布式锁
String lockKey = "lock:user:" + id;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁(等待3秒,锁自动过期30秒)
boolean locked = lock.tryLock(3, 30, TimeUnit.SECONDS);
if (locked) {
try {
// 3. 双检锁定:获取锁后再次检查缓存
user = getFromCache(cacheKey);
if (user != null) {
return user;
}
// 4. 查数据库
user = userMapper.selectById(id);
// 5. 写入缓存
if (user != null) {
setCache(cacheKey, user);
}
return user;
} finally {
lock.unlock();
}
} else {
// 没获取到锁,短暂等待后重试查缓存
Thread.sleep(100);
return queryUserById(id); // 递归重试
}
} catch (InterruptedException e) {
// 异常情况,直接查DB
return userMapper.selectById(id);
}
}
}
解决方案2:热点数据永不过期
/**
* 方案二:热点数据永不过期 + 异步更新
* 核心:用逻辑过期替代物理过期
*/
// 缓存数据结构
@Data
public class CacheData<T> {
private T data; // 真实数据
private long expireTime; // 逻辑过期时间
private long version; // 数据版本
}
// 使用示例
public User queryUserById(Long id) {
String cacheKey = "user:" + id;
// 1. 先查缓存
CacheData<User> cacheData = getFromCache(cacheKey);
if (cacheData != null) {
// 2. 检查是否逻辑过期
if (cacheData.getExpireTime() > System.currentTimeMillis()) {
// 没过期,直接返回
return cacheData.getData();
}
// 3. 逻辑过期了,开启异步更新
threadPool.execute(() -> {
// 只有一个线程能抢到锁执行更新
refreshCache(id);
});
// 4. 返回旧数据(短暂不一致可接受)
return cacheData.getData();
}
// 5. 缓存不存在,直接查DB并回填
User user = userMapper.selectById(id);
setCacheWithLogicExpire(cacheKey, user, 30 * 60 * 1000L); // 30分钟逻辑过期
return user;
}
2.4 缓存雪崩(Cache Avalanche)
什么是缓存雪崩?
大量key同时过期:
┌─────────────────────────────────────────────────────┐
│ 缓存中的热点key们 │
├─────────────────────────────────────────────────────┤
│ key1 ──▶ [过期时间 12:00] ──▶ 🔴 同时过期 │
│ key2 ──▶ [过期时间 12:00] ──▶ 🔴 同时过期 │
│ key3 ──▶ [过期时间 12:00] ──▶ 🔴 同时过期 │
│ ... │
│ key999 ─▶ [过期时间 12:00] ──▶ 🔴 同时过期 │
└─────────────────────────────────────────────────────┘
│
▼
所有请求同时打向数据库
│
▼
┌─────────────────────┐
│ 数据库扛不住 │
│ 系统雪崩 │
└─────────────────────┘
解决方案:随机过期时间 + 多级缓存
/**
* 解决方案一:随机过期时间
*/
public void setCache(String key, Object value) {
// 基础过期时间:30分钟
long baseExpire = 30 * 60;
// 随机偏移:±5分钟
int randomOffset = new Random().nextInt(10 * 60) - 5 * 60;
// 最终过期时间:25~35分钟
long expire = baseExpire + randomOffset;
redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
}
/**
* 解决方案二:多级缓存架构
* L1: Caffeine (本地缓存) → L2: Redis → L3: MySQL
*/
@Configuration
public class MultiLevelCacheConfig {
// L1 本地缓存(Caffeine)
@Bean
public Cache<Long, User> localCache() {
return Caffeine.newBuilder()
.maximumSize(10000) // 最多1万条
.expireAfterWrite(1, TimeUnit.MINUTES) // 1分钟过期
.build();
}
// L2 分布式缓存(Redis)
// ... 已有的Redis配置
}
@Service
public class MultiLevelUserService {
@Autowired
private Cache<Long, User> localCache;
public User queryUserById(Long id) {
// 1. 先查L1本地缓存
User user = localCache.getIfPresent(id);
if (user != null) {
return user;
}
// 2. 查L2 Redis缓存
String redisKey = "user:" + id;
String json = redisTemplate.opsForValue().get(redisKey);
if (json != null) {
user = JSON.parseObject(json, User.class);
// 回填L1
localCache.put(id, user);
return user;
}
// 3. 查MySQL
user = userMapper.selectById(id);
if (user != null) {
// 回填L2和L1
redisTemplate.opsForValue().set(redisKey, JSON.toJSONString(user), 1, TimeUnit.HOURS);
localCache.put(id, user);
}
return user;
}
}
/**
* 解决方案三:Redis集群 + 熔断降级
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate() {
// 开启事务
template.setEnableTransactionSupport(true);
return template;
}
}
/**
* 降级兜底:Redis挂了返回默认值
*/
public User queryUserById(Long id) {
try {
return redisUserService.queryUserById(id);
} catch (Exception e) {
// Redis异常,降级到DB
log.warn("Redis异常,降级到DB查询", e);
return userMapper.selectById(id);
}
}
2.5 三兄弟解决方案总结
┌────────────────────────────────────────────────────────────────┐
│ 缓存问题综合解决方案 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 穿透:布隆过滤器 + 接口校验 + 缓存空值 │
│ ├── 布隆过滤器:挡住不存在的请求 │
│ ├── 接口校验:拦住明显非法的请求 │
│ └── 空值缓存:缓存短时间不存在的值 │
│ │
│ 击穿:互斥锁 / 逻辑过期 / 永不过期 │
│ ├── 互斥锁:保证只有一个人回源 │
│ ├── 逻辑过期:返回旧数据 + 异步更新 │
│ └── 热点永不过期:用版本号控制更新 │
│ │
│ 雪崩:随机过期 + 多级缓存 + 熔断降级 │
│ ├── 随机过期:打散key的过期时间 │
│ ├── 多级缓存:L1本地 + L2Redis + L3DB │
│ └── 熔断降级:Redis挂了有兜底 │
│ │
└────────────────────────────────────────────────────────────────┘
三、Redis数据结构避坑地图
3.1 数据结构全景图
┌─────────────────────────────────────────────────────────────────┐
│ Redis 数据结构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ String (字符串) │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ List(列表) Hash(哈希) Set(集合) │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Sorted Set Stream(流) HyperLogLog │
│ (有序集合) (Redis5.0) (基数统计) │
│ │
└─────────────────────────────────────────────────────────────────┘
3.2 String(字符串)—— 最常用的数据结构
内部编码:
| 编码 | 条件 | 说明 |
|---|---|---|
| int | value是整数,且小于2^63 | 使用int类型存储 |
| embstr | value <= 39字节 | embstr编码的SDS |
| raw | value > 39字节 | raw编码的SDS |
/**
* String 类型的典型使用场景
*/
// 1. 缓存对象(最常见)
String userJson = JSON.toJSONString(user);
redisTemplate.opsForValue().set("user:" + id, userJson);
// 2. 分布式锁
Boolean success = redisTemplate.opsForValue()
.setIfAbsent("lock:" + key, "1", 30, TimeUnit.SECONDS);
// 3. 计数器
redisTemplate.opsForValue().increment("view:article:" + articleId);
// 4. 分布式session
redisTemplate.opsForValue().set("session:" + sessionId, userInfo, 2, TimeUnit.HOURS);
// 5. 限流
String key = "rate:limit:" + userId + ":" + minute;
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
redisTemplate.expire(key, 60, TimeUnit.SECONDS);
}
return count <= 100; // 每分钟最多100次
// 6. 验证码 / Token
redisTemplate.opsForValue().set("sms:code:" + phone, code, 5, TimeUnit.MINUTES);
String的"陷阱"
// ❌ 错误:把大对象存为String
String bigObject = JSON.toJSONString(hugeMap); // 可能超过512MB限制
// ✅ 正确:使用Hash存储大对象
redisTemplate.opsForHash().putAll("user:" + id, map);
// ❌ 错误:String做自增当ID
redisTemplate.opsForValue().increment("user:id"); // 并发不安全
// ✅ 正确:用UUID或号段服务
String userId = idGenerator.nextId();
3.3 Hash(哈希)—— 存储对象
底层实现:
| 元素数量 | encoding | 说明 |
|---|---|---|
| < 512 | ziplist | 压缩列表,内存紧凑 |
| >= 512 | hashtable | 哈希表,支持O(1)操作 |
/**
* Hash 类型的使用场景
*/
// 1. 存储对象(比String更节省空间)
Map<String, Object> userMap = new HashMap<>();
userMap.put("name", "张三");
userMap.put("age", "25");
userMap.put("city", "北京");
redisTemplate.opsForHash().putAll("user:h:" + id, userMap);
// 2. 获取单个字段
String name = (String) redisTemplate.opsForHash().get("user:h:" + id, "name");
// 3. 获取所有字段
Map<Object, Object> user = redisTemplate.opsForHash().entries("user:h:" + id);
// 4. 字段自增(适合计数器)
redisTemplate.opsForHash().increment("stats:article:" + id, "views", 1);
// 5. 购物车实现
// 添加商品
redisTemplate.opsForHash().put("cart:" + userId, productId, String.valueOf(count));
// 增加数量
redisTemplate.opsForHash().increment("cart:" + userId, productId, addCount);
// 删除商品
redisTemplate.opsForHash().delete("cart:" + userId, productId);
// 查看购物车
Map<Object, Object> cart = redisTemplate.opsForHash().entries("cart:" + userId);
Hash的"陷阱"
// ❌ 错误:hgetall获取大Hash的所有字段
Map<Object, Object> bigHash = redisTemplate.opsForHash().entries("big:hash"); // 内存爆炸
// ✅ 正确:分页获取
ScanOptions options = ScanOptions.scanOptions()
.match("field:*")
.count(100)
.build();
Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan("big:hash", options);
while (cursor.hasNext()) {
// 处理每个字段
}
// ❌ 错误:Hash的field没有过期功能
// Redis的Hash不支持对单个field设置过期时间
// ✅ 正确:想要field过期,用String + key组合
redisTemplate.opsForValue().set("user:" + id + ":name", name, 1, TimeUnit.HOURS);
3.4 List(列表)—— 队列和栈
底层实现:
| 编码 | 条件 | 说明 |
|---|---|---|
| ziplist | 元素个数 < 512,且每个元素 < 64字节 | 压缩列表 |
| linkedlist | 其他情况 | 双向链表 |
/**
* List 类型的使用场景
*/
// 1. 消息队列(简单版,不保证 Exactly-Once)
// 生产者
redisTemplate.opsForList().leftPush("queue:msg", message);
// 消费者
String msg = redisTemplate.opsForList().rightPop("queue:msg");
// 2. 关注列表 / 粉丝列表
// 添加关注
redisTemplate.opsForList().leftPush("user:" + userId + ":follows", followedUserId);
// 获取关注列表(前10个)
List<Object> follows = redisTemplate.opsForList().range("user:" + userId + ":follows", 0, 9);
// 3. 最新商品列表
redisTemplate.opsForList().leftPush("product:new", productId);
redisTemplate.opsForList().trim("product:new", 0, 99); // 只保留前100个
// 4. 日志收集
redisTemplate.opsForList().rightPush("logs", logMessage);
redisTemplate.opsForList().trim("logs", 0, 9999); // 限制长度
// 5. 实现分页(性能差,不推荐大数据量)
// 获取第N页
long start = (page - 1) * pageSize;
long end = start + pageSize - 1;
List<Object> pageData = redisTemplate.opsForList().range("list:key", start, end);
List的"陷阱"
// ❌ 错误:用List做分页(性能差)
// lrange是O(S+N)的,N越大越慢
// ✅ 正确:大数据量分页用Sorted Set
// ZADD score view_count
// ZREVRANGE 0 9 WITHSCORES
// ❌ 错误:没有消费者组概念
// 多个消费者可能消费同一条消息
// ✅ 正确:需要可靠队列用Stream
3.5 Set(集合)—— 无序去重
底层实现:
| 编码 | 条件 | 说明 |
|---|---|---|
| intset | 集合所有元素都是整数,且元素个数 < 512 | 整数集合 |
| hashtable | 其他情况 | 哈希表 |
/**
* Set 类型的使用场景
*/
// 1. 标签系统(去重)
redisTemplate.opsForSet().add("tags:article:" + articleId, "Java", "Redis", "面试");
redisTemplate.opsForSet().add("tags:user:" + userId, "Java", "架构", "微服务");
// 2. 关注列表(判断是否关注)
redisTemplate.opsForSet().add("follows:" + userId, followedUserId);
Boolean isFollowed = redisTemplate.opsForSet().isMember("follows:" + userId, followedUserId);
// 3. 抽奖系统
// 添加参与者
redisTemplate.opsForSet().add("lottery:" + activityId, userId);
// 抽取N个中奖者
List<Object> winners = redisTemplate.opsForSet().distinctRandomMembers("lottery:" + activityId, 3);
// 4. UV统计
redisTemplate.opsForSet().add("uv:2024:01:01", visitorId);
// 最终UV数量
Long uvCount = redisTemplate.opsForSet().size("uv:2024:01:01");
// 5. 好友关系(交集、并集、差集)
// 共同好友
Set<Object> mutualFriends = redisTemplate.opsForSet().intersect("friends:user1", "friends:user2");
// 我认识但他不认识的人(差集)
Set<Object> diff = redisTemplate.opsForSet().difference("friends:user1", "friends:user2");
3.6 Sorted Set(有序集合)—— 排行榜
底层实现:
| 编码 | 条件 | 说明 |
|---|---|---|
| ziplist | 元素个数 < 128,且每个member < 64字节 | 压缩列表 |
| skiplist + hashtable | 其他情况 | 跳表 + 哈希表 |
/**
* Sorted Set 类型的使用场景
*/
// 1. 排行榜(最经典场景)
// 增加用户积分
redisTemplate.opsForZSet().add("rank:score", userId, score);
// 更新积分
redisTemplate.opsForZSet().incrementScore("rank:score", userId, addedScore);
// 获取排名前10
Set<Object> top10 = redisTemplate.opsForZSet().reverseRange("rank:score", 0, 9);
// 获取用户排名
Long rank = redisTemplate.opsForZSet().reverseRank("rank:score", userId);
// 获取用户积分
Double score = redisTemplate.opsForZSet().score("rank:score", userId);
// 2. 延迟队列(按时间戳排序)
long delayTime = System.currentTimeMillis() + 5000; // 5秒后执行
redisTemplate.opsForZSet().add("delay:queue", taskId, delayTime);
// 消费者:获取到期的任务
Set<Object> expiredTasks = redisTemplate.opsForZSet().rangeByScore("delay:queue", 0, System.currentTimeMillis());
// 删除已执行的任务
redisTemplate.opsForZSet().remove("delay:queue", taskId);
// 3. 时间窗口去重
long windowStart = System.currentTimeMillis() - 60000; // 1分钟窗口
redisTemplate.opsForZSet().removeRangeByScore("unique:1min", 0, windowStart);
redisTemplate.opsForZSet().add("unique:1min", requestId, System.currentTimeMillis());
Long count = redisTemplate.opsForZSet().zCard("unique:1min");
// 4. 滑动窗口限流
long windowKey = System.currentTimeMillis() / 1000; // 按秒
String key = "rate:" + windowKey;
redisTemplate.opsForZSet().add(key, requestId, System.currentTimeMillis());
redisTemplate.opsForZSet().removeRangeByScore(key, 0, System.currentTimeMillis() - 60000); // 清理60秒前的
Long count = redisTemplate.opsForZSet().zCard(key);
return count <= 100;
3.7 Stream(流)—— Redis 5.0 消息队列
/**
* Stream 类型 - 真正的消息队列
*/
// 1. 创建消费者组
redisTemplate.opsForStream().createGroup("stream:msg", "group1", "0");
// 2. 生产消息
Map<String, Object> msg = new HashMap<>();
msg.put("user", "张三");
msg.put("content", "消息内容");
redisTemplate.opsForStream().add("stream:msg", msg);
// 3. 消费消息(阻塞)
StreamOffset<String> offset = StreamOffset.create("stream:msg", ReadOffset.lastConsumed());
List<MapRecord<String, Object, Object>> records =
redisTemplate.opsForStream().read(String.class, StreamReadOptions.empty().count(10).block(Duration.ofSeconds(2)), offset);
// 4. 确认消息
for (MapRecord<String, Object, Object> record : records) {
// 处理消息
processMessage(record.getValue());
// 确认
redisTemplate.opsForStream().acknowledge("stream:msg", "group1", record.getId());
}
// 5. 死信队列
// 消息处理失败后,放入死信队列
redisTemplate.opsForStream().add("stream:dlq", msg);
3.8 数据结构选择指南
┌─────────────────────────────────────────────────────────────────┐
│ Redis 数据结构选择指南 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 场景 推荐数据结构 备选方案 │
│ ──────────────────────────────────────────────────────────── │
│ 缓存对象 String(JSON) Hash │
│ 分布式锁 String - │
│ 计数器 String Hash │
│ 验证码/Token String - │
│ Session管理 String(JSON) Hash │
│ 购物车 Hash String(JSON) │
│ 消息队列 Stream List │
│ 标签系统 Set Sorted Set │
│ 关注列表 Set - │
│ 排行榜 Sorted Set - │
│ 延迟队列 Sorted Set Stream │
│ 去重 Set Sorted Set │
│ 抽奖 Set - │
│ UV统计 HyperLogLog Set │
│ 附近的人 GeoHash - │
│ │
└─────────────────────────────────────────────────────────────────┘
四、Redis集群与高可用
4.1 高可用方案对比
┌─────────────────────────────────────────────────────────────────┐
│ Redis 高可用方案对比 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 单机 ──▶ 主从复制 ──▶ 哨兵模式 ──▶ Redis集群 │
│ │
│ 适用:开发测试 适用:中小规模 适用:大规模 │
│ │
│ 优点:简单 优点:自动故障转移 优点:数据分片 │
│ 缺点:无HA 缺点:需要Sentinel 缺点:架构复杂 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.2 主从复制原理
┌─────────────────────────────────────────────────────────────────┐
│ 主从复制架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ │
│ │ Master │ (主节点,可读写) │
│ │ :6379 │ │
│ └────┬────┘ │
│ ┌─────────┼─────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ Slave1 │ │ Slave2 │ │ Slave3 │ │
│ │ :6380 │ │ :6381 │ │ :6382 │ │
│ └────────┘ └────────┘ └────────┘ │
│ │
│ 数据流:Master ──────────────────────▶ Slave │
│ (异步复制,有延迟) │
│ │
└─────────────────────────────────────────────────────────────────┘
主从复制配置:
# 从节点配置
replicaof <master-ip> <master-port>
replica-serve-stale-data yes # 从节点是否可读
replica-read-only yes # 从节点只读
repl-diskless-sync no # 是否无盘复制
4.3 哨兵模式(Sentinel)
哨兵的工作原理:
┌─────────────────────────────────────────────────────────────────┐
│ 哨兵模式架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ 应用程序 │ │
│ │ (客户端) │ │
│ └──────┬──────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Sentinel │ │ Sentinel │ │ Sentinel │ │
│ │ (主) │ │ (从) │ │ (从) │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └───────────────┼───────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Redis Master │ │
│ │ + Replicas │ │
│ └─────────────────┘ │
│ │
│ 哨兵职责: │
│ ├── 监控:监控Master/Slave健康状态 │
│ ├── 通知:发现问题通知客户端 │
│ ├── 自动故障转移:Master挂了,选举新Master │
│ └── 提供配置:客户端查询当前Master地址 │
│ │
└─────────────────────────────────────────────────────────────────┘
Sentinel配置:
# sentinel.conf
port 26379
# 监控的Master
sentinel monitor mymaster <master-ip> <master-port> 2
# 判定down的票数(至少2个哨兵认为down才down)
sentinel down-after-milliseconds mymaster 30000
# 故障转移超时时间
sentinel failover-timeout mymaster 180000
# 故障转移时,同时同步新Master的从节点数量
sentinel parallel-syncs mymaster 1
Java客户端连接哨兵:
/**
* Spring Boot 连接哨兵
*/
spring:
redis:
sentinel:
master: mymaster # Master名称
nodes: 192.168.1.1:26379,192.168.1.2:26379,192.168.1.3:26379
// Jedis连接
public JedisSentinelPool getJedisSentinelPool() {
Set<String> sentinels = new HashSet<>();
sentinels.add("192.168.1.1:26379");
sentinels.add("192.168.1.2:26379");
sentinels.add("192.168.1.3:26379");
return new JedisSentinelPool("mymaster", sentinels,
new JedisPoolConfig(), 3000, null);
}
4.4 Gossip协议 —— 集群内通信
Gossip协议工作原理:
┌─────────────────────────────────────────────────────────────────┐
│ Gossip 协议通信机制 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 每隔1秒,每个节点随机选择一个节点交换信息: │
│ │
│ 节点A ──────── ping ─────────▶ 节点B │
│ ◀──────── pong ──────── │
│ │
│ 信息传播: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 第1轮:A告诉B │ │
│ │ 第2轮:A告诉C,B告诉D │ │
│ │ 第3轮:A告诉E,C告诉F,B告诉G... │ │
│ │ 经过log(N)轮,所有节点都知道这个信息 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 配置参数: │
│ ├── cluster-node-timeout: 15000 # 节点超时时间 │
│ ├── cluster-gossip-interval: 100 # 每次通信间隔(ms) │
│ └── cluster-ping-timeout: 10000 # ping超时时间 │
│ │
└─────────────────────────────────────────────────────────────────┘
Gossip协议的问题 —— Gossip风暴:
/**
* Gossip风暴问题
*
* 当集群规模大时,Gossip消息会呈指数级增长
*
* 假设集群有N个节点:
* - 每秒每个节点发送 N-1 个Gossip消息
* - 每秒全集群产生 N*(N-1) 个消息
*
* 问题场景:
* - 100节点集群:每秒 9900 条消息
* - 500节点集群:每秒 249500 条消息
*
* 解决方案:
*/
// 1. 减少Gossip频率(牺牲实时性)
# redis.conf
cluster-gossip-interval 5000 # 从1秒改为5秒
// 2. 限制集群规模(推荐 < 100节点)
// 如果数据量大,用Codis或Twemproxy做代理分片
// 3. 使用更好的网络
// 集群节点尽量在同一个机房、同一个网段
// 4. 合理配置超时时间
cluster-node-timeout 30000 # 适当增大,避免误判
4.5 Redis Cluster(集群模式)
集群架构:
┌─────────────────────────────────────────────────────────────────┐
│ Redis Cluster 架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 客户端 ─────────────────────────▶ Redis Cluster │
│ │
│ Slot分布(16384个槽位): │
│ ┌────────┬────────┬────────┬────────┐ │
│ │Slot 0 │Slot 5460│Slot5461│Slot10922│ │
│ │ ~5460 │ │~10922 │~16383 │ │
│ └────────┴────────┴────────┴────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │Node A │ │Node B │ │Node C │ │
│ │Master │ │Master │ │Master │ │
│ └───┬────┘ └───┬────┘ └───┬────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │Slave A │ │Slave B │ │Slave C │ │
│ │(副本) │ │(副本) │ │(副本) │ │
│ └────────┘ └────────┘ └────────┘ │
│ │
│ 数据路由: │
│ key的CRC16(slot) = CRC16(key) % 16384 │
│ │
└─────────────────────────────────────────────────────────────────┘
集群槽位计算:
/**
* 槽位计算公式
*/
// Redis内部计算
slot = CRC16(key) % 16384
// Java中模拟
public static int calculateSlot(String key) {
// CRC16计算
int crc = CRC16_CCITT(key.getBytes());
return crc & 0x3FFF; // & 16383
}
// Hash_tag 确保同一批数据在同一个槽
// user:100:profile 和 user:100:orders 使用 {user:100}
// 这样两个key一定在同一个槽,可以做跨槽操作
Spring Boot连接集群:
# application.yml
spring:
redis:
cluster:
nodes:
- 192.168.1.1:6379
- 192.168.1.2:6379
- 192.168.1.3:6379
- 192.168.1.4:6379
- 192.168.1.5:6379
- 192.168.1.6:6379
max-redirects: 3 # 最大跳转次数
# Jedis连接池配置
redis:
pool:
max-total: 1000
max-idle: 200
min-idle: 50
max-wait: 3000
4.6 哨兵 vs 集群 —— 选型指南
┌─────────────────────────────────────────────────────────────────┐
│ 哨兵 vs 集群 选型指南 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┬─────────────────┬─────────────────────┐ │
│ │ 指标 │ 哨兵模式 │ Redis集群 │ │
│ ├───────────────┼─────────────────┼─────────────────────┤ │
│ │ 数据分片 │ 不支持 │ 支持 │ │
│ │ │ (所有数据在一组) │ (16384槽位分片) │ │
│ ├───────────────┼─────────────────┼─────────────────────┤ │
│ │ 数据量 │ ~20G │ 不限制 │ │
│ ├───────────────┼─────────────────┼─────────────────────┤ │
│ │ 写能力 │ 瓶颈在Master │ 可水平扩展 │ │
│ ├───────────────┼─────────────────┼─────────────────────┤ │
│ │ 架构复杂度 │ 低 │ 高 │ │
│ ├───────────────┼─────────────────┼─────────────────────┤ │
│ │ Gossip风暴 │ 无 │ 有 │ │
│ ├───────────────┼─────────────────┼─────────────────────┤ │
│ │ 故障转移 │ 自动 │ 自动 │ │
│ ├───────────────┼─────────────────┼─────────────────────┤ │
│ │ 跨槽操作 │ 支持 │ 部分不支持 │ │
│ └───────────────┴─────────────────┴─────────────────────┘ │
│ │
│ 选择建议: │
│ ├── 数据量 < 20G,QPS < 10万 → 哨兵模式 │
│ ├── 数据量 > 20G,QPS > 10万 → Redis集群 │
│ ├── 需要多key操作 → 哨兵(不支持跨槽) │
│ └── 追求高可用 + 扩展性 → Redis集群 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.7 集群问题与解决方案
/**
* Redis集群常见问题及解决方案
*/
// 问题1:跨槽位操作
// ❌ 错误:mset跨槽会报错
redisTemplate.opsForValue().multiSet(map); // map中key可能在不同槽
// ✅ 正确:使用Hash_tag确保同槽
// {user:1}:name = 张三
// {user:1}:age = 25
// 两个key都在同一个槽
// 问题2:批量操作效率低
// ❌ 错误:pipeline不支持跨槽
List<Object> results = redisTemplate.executePipelined((RedisCallback) connection -> {
for (String key : keys) {
connection.stringCommands().get(key.getBytes());
}
return null;
});
// ✅ 正确:按槽分组,分批执行
Map<Integer, List<String>> slotMap = new HashMap<>();
for (String key : keys) {
int slot = calculateSlot(key);
slotMap.computeIfAbsent(slot, k -> new ArrayList<>()).add(key);
}
for (List<String> sameSlotKeys : slotMap.values()) {
// 同一槽的key可以一次pipeline
}
// 问题3:热点key问题
// 热点key会集中在某个节点上
// ✅ 解决:客户端本地缓存 + Redis
public String getHotKey(String key) {
// 先查本地缓存(Caffeine)
String localValue = localCache.getIfPresent(key);
if (localValue != null) {
return localValue;
}
// 查Redis
String value = redisTemplate.opsForValue().get(key);
// 写入本地缓存
localCache.put(key, value);
return value;
}
五、实战经验总结
5.1 Redis开发避坑清单
┌─────────────────────────────────────────────────────────────────┐
│ Redis 开发避坑清单 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 【容量规划】 │
│ ├── 单实例内存建议 < 10GB │
│ ├── 大key拆分:单个key < 1MB │
│ └── 热点key分散:用Hash替代大String │
│ │
│ 【连接管理】 │
│ ├── 使用连接池,不要频繁创建连接 │
│ ├── Jedis: 建议 maxTotal = QPS / 1000 │
│ └── 记得关闭连接(try-with-resources) │
│ │
│ 【数据安全】 │
│ ├── 敏感数据加密存储 │
│ ├── 定期RDB/AOF备份 │
│ └── 开启密码认证(requirepass) │
│ │
│ 【性能优化】 │
│ ├── 避免使用keys * / flushall │
│ ├── 使用scan代替keys │
│ ├── Pipeline批量操作减少RTT │
│ └── Lua脚本保证原子性 │
│ │
│ 【内存管理】 │
│ ├── 设置maxmemory + 淘汰策略 │
│ ├── volatile-lru适合有过期时间的场景 │
│ └── 定期监控内存使用率 │
│ │
└─────────────────────────────────────────────────────────────────┘
5.2 Redis监控指标
/**
* 关键监控指标
*/
// INFO命令查看
redis-cli info
// 关键指标
// memory
// ├── used_memory: 当前内存使用
// ├── used_memory_peak: 峰值内存
// └── maxmemory: 最大内存配置
// stats
// ├── total_connections_received: 总连接数
// ├── instantaneous_ops_per_sec: QPS
// └── keyspace_hits_misses: 命中率
// replication
// ├── role: master/slave
// ├── master_link_status: 主从连接状态
// └── slave_repl_offset: 从节点偏移量
// Spring Boot Actuator 监控
@Bean
public MeterRegistry meterRegistry() {
return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
}
5.3 推荐配置
# redis.conf 推荐配置
# 内存配置
maxmemory 10gb
maxmemory-policy allkeys-lru
# 持久化配置
appendonly yes
appendfsync everysec
rdbcompression yes
rdbchecksum yes
# 网络配置
timeout 300
tcp-keepalive 60
# 慢查询日志
slowlog-log-slower-than 10000
slowlog-max-len 128
# 客户端
maxclients 10000
六、面试高频问题汇总
6.1 Redis基础
Q1: Redis为什么这么快?
A: 单线程+IO多路复用,基于内存操作,C语言实现,数据结构高效
Q2: Redis和Memcached的区别?
A:
├── 数据类型: Redis支持多种数据结构,Memcached只有String
├── 持久化: Redis支持RDB/AOF,Memcached不支持
├── 集群: Redis支持主从/哨兵/集群,Memcached不支持
└── 线程模型: Redis单线程,Memcached多线程+Libevent
6.2 缓存问题
Q3: 缓存穿透、击穿、雪崩的区别?
A:
├── 穿透: 查询不存在的数据 → 布隆过滤器
├── 击穿: 热点key过期瞬间 → 互斥锁/逻辑过期
└── 雪崩: 大量key同时过期 → 随机TTL/多级缓存
Q4: 布隆过滤器的原理和缺点?
A:
├── 原理: K个哈希函数,K个bit位,存在则一定存在
├── 缺点: 不存在可能被误判为存在(假阳性)
└── 适用: 数据量大的存在性判断
6.3 数据结构
Q5: Redis有哪些数据结构?底层实现?
A:
├── String: int/embstr/raw (SDS)
├── List: ziplist/linkedlist
├── Hash: ziplist/hashtable
├── Set: intset/hashtable
├── ZSet: ziplist/skiplist+hashtable
└── Stream: radix tree
Q6: ZSet为什么用跳表而不是红黑树?
A:
├── 跳表范围查询O(logN),红黑树也是O(logN)
├── 跳表实现简单,代码量少
├── 跳表区间遍历更简单
└── 跳表更容易做并发控制(锁粒度小)
6.4 集群
Q7: 哨兵和集群的区别?
A:
├── 哨兵: 无数据分片,数据量受限于单机
├── 集群: 16384槽分片,支持水平扩展
├── Gossip风暴: 集群节点多时消息量指数增长
└── 选型: 数据量<20G选哨兵,否则选集群
Q8: 主从复制原理?
A:
├── 全量同步: master RDB + buffer,slave保存并加载
├── 增量同步: master命令传播,slave回放
└── 断线重连: repl_backlog缓冲区
七、总结
7.1 知识图谱
┌─────────────────────────────────────────────────────────────────┐
│ Redis 知识全景图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ │
│ │ Redis │ │
│ └───────┬───────┘ │
│ ┌──────────┼──────────┐ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 核心原理 │ │ 数据结构 │ │
│ │ 单线程IO │ │ String │ │
│ │ 多路复用 │ │ Hash │ │
│ │ 内存模型 │ │ List │ │
│ │ │ │ Set/ZSet │ │
│ └─────────────┘ │ Stream │ │
│ │ └─────────────┘ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 缓存问题 │ │ 集群高可用 │ │
│ │ 穿透 │ │ 主从复制 │ │
│ │ 击穿 │ │ 哨兵模式 │ │
│ │ 雪崩 │ │ Redis集群 │ │
│ │ 布隆过滤器 │ │ Gossip协议 │ │
│ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
7.2 学习建议
1. 原理先行:理解单线程I/O模型和数据结构底层实现
2. 工具掌握:熟练使用redis-cli和客户端API
3. 实战积累:在项目中实践缓存、分布式锁等场景
4. 运维了解:掌握监控、备份、故障排查技能
5. 持续关注:Redis新版本特性(如Redis 7.0)
推荐学习路径:
├── 《Redis设计与实现》- 入门必读
├── Redis官网文档 - 权威参考
├── 源码阅读 - 深入理解
└── 源码天堂 - 博客实战
八、写在最后
Redis是后端工程师的必备技能,从简单的缓存到复杂的分布式系统,到处都有Redis的身影。
记住这些话:
"Redis不是银弹,不要把所有数据都往里塞"
"缓存只保证速度,不保证一致性"
"最好的优化是不需要优化——先想清楚要不要用Redis"
希望这篇文章能帮你深入理解Redis,在面试和工作中都能游刃有余!
📢 讨论话题:你在使用Redis时遇到过哪些"坑"?是怎么解决的?
👇 延伸阅读:
如果觉得有帮助,欢迎点赞、收藏、转发!