前言
Redis 作为 Java 后端开发中最常用的 NoSQL 数据库,是面试和工作的核心考点 ------ 无论是高频的数据结构应用,还是生产环境必须关注的持久化、缓存策略,都是绕不开的重点。今天这篇日记,我会从「基础数据结构→核心持久化机制→实战缓存策略」三个维度,结合场景和代码案例,帮你把 Redis 的核心知识点吃透,既懂原理又会落地。
一、Redis 核心数据结构(基础 + 实战)
Redis 的核心优势在于丰富的高性能数据结构,不同结构适配不同业务场景,掌握 "结构 + 场景" 是基础。
1. 核心数据结构对比(必背)
| 数据结构 | 核心特性 | 典型应用场景 | 核心命令 |
|---|---|---|---|
| String(字符串) | 二进制安全,可存字符串 / 数字 / 二进制数据,最大 512MB | 缓存热点数据、计数器、分布式锁、Session 共享 | SET/GET/INCR/DECR/APPEND/EXPIRE |
| Hash(哈希) | 键值对集合,适合存储对象(如用户信息),节省内存 | 存储对象(用户 / 商品信息)、购物车 | HSET/HGET/HMGET/HDEL/HKEYS/HVALS |
| List(列表) | 双向链表,有序可重复,支持头尾操作 | 消息队列、最新列表(如朋友圈动态)、排行榜(简单版) | LPUSH/RPUSH/LPOP/RPOP/LRANGE/LLEN |
| Set(集合) | 无序、不可重复,支持交集 / 并集 / 差集 | 点赞 / 签到、抽奖、共同好友、去重 | SADD/SMEMBERS/SISMEMBER/SINTER/SUNION/SDIFF |
| Sorted Set(有序集合) | 有序、不可重复,通过 score 排序,底层跳表实现 | 排行榜(如销量 / 积分)、延迟队列 | ZADD/ZRANGE/ZRANK/ZSCORE/ZREVRANGE/ZREM |
| Bitmap(位图) | 基于 String 实现,按位存储,极致节省内存 | 签到统计、用户状态(在线 / 离线)、布隆过滤器 | SETBIT/GETBIT/BITCOUNT/BITOP |
| HyperLogLog(基数统计) | 极小内存统计基数(不重复元素数),误差约 0.81% | UV 统计(页面访问人数)、独立 IP 统计 | PFADD/PFCOUNT/PFMERGE |
2. 实战案例(核心结构)
(1)String:计数器(文章阅读量)
java
运行
java
// Spring Boot整合Redis(RedisTemplate)示例
@Resource
private StringRedisTemplate stringRedisTemplate;
// 阅读量+1(原子操作,避免并发问题)
public void incrArticleReadCount(Long articleId) {
String key = "article:read:count:" + articleId;
// INCR命令:原子递增,无需加锁
stringRedisTemplate.opsForValue().increment(key);
// 设置过期时间(可选,如7天)
stringRedisTemplate.expire(key, 7, TimeUnit.DAYS);
}
// 获取阅读量
public Long getArticleReadCount(Long articleId) {
String key = "article:read:count:" + articleId;
String count = stringRedisTemplate.opsForValue().get(key);
return count == null ? 0 : Long.parseLong(count);
}
(2)Hash:存储用户信息
java
运行
java
// 存储用户信息
public void saveUser(User user) {
String key = "user:info:" + user.getId();
HashOperations<String, String, String> hashOps = stringRedisTemplate.opsForHash();
hashOps.put(key, "username", user.getUsername());
hashOps.put(key, "nickname", user.getNickname());
hashOps.put(key, "age", user.getAge().toString());
hashOps.put(key, "email", user.getEmail());
// 设置过期时间
stringRedisTemplate.expire(key, 1, TimeUnit.DAYS);
}
// 获取用户信息
public User getUser(Long userId) {
String key = "user:info:" + userId;
HashOperations<String, String, String> hashOps = stringRedisTemplate.opsForHash();
Map<String, String> userMap = hashOps.entries(key);
if (userMap.isEmpty()) {
return null;
}
User user = new User();
user.setId(userId);
user.setUsername(userMap.get("username"));
user.setNickname(userMap.get("nickname"));
user.setAge(Integer.parseInt(userMap.get("age")));
user.setEmail(userMap.get("email"));
return user;
}
(3)Sorted Set:商品销量排行榜
java
运行
java
// 商品销量+1
public void incrProductSales(Long productId, int sales) {
String key = "product:sales:rank";
// ZADD:score为销量,member为商品ID
stringRedisTemplate.opsForZSet().incrementScore(key, productId.toString(), sales);
}
// 获取销量TOP10商品
public List<Long> getSalesTop10() {
String key = "product:sales:rank";
// ZREVRANGE:按score降序取前10(0-9)
Set<String> productIdSet = stringRedisTemplate.opsForZSet().reverseRange(key, 0, 9);
return productIdSet.stream().map(Long::parseLong).collect(Collectors.toList());
}
二、Redis 持久化机制(RDB vs AOF)
Redis 是内存数据库,重启后数据会丢失,持久化就是将内存数据写入磁盘的机制,核心有 RDB 和 AOF 两种方式。
1. RDB(Redis Database):快照持久化
(1)核心原理
- 定时将 Redis 内存中的全量数据 生成快照文件(
dump.rdb)保存到磁盘; - 触发方式:
- 手动触发:
SAVE(阻塞 Redis,不推荐)、BGSAVE(后台异步执行,推荐); - 自动触发:通过
redis.conf配置(如save 900 1:900 秒内至少 1 个键修改则触发)。
- 手动触发:
(2)核心配置(redis.conf)
conf
# 自动触发规则:save <秒> <修改次数>
save 900 1 # 900秒内至少1个键修改
save 300 10 # 300秒内至少10个键修改
save 60 10000 # 60秒内至少10000个键修改
# RDB文件名称
dbfilename dump.rdb
# RDB文件存储路径
dir ./
# BGSAVE失败时是否停止写入(推荐yes,避免数据不一致)
stop-writes-on-bgsave-error yes
# 压缩RDB文件(推荐yes,节省磁盘空间,轻微性能损耗)
rdbcompression yes
(3)优缺点
| 优点 | 缺点 |
|---|---|
| 1. 快照文件体积小,恢复速度快;2. 对性能影响小(BGSAVE 异步);3. 适合备份 / 灾备 | 1. 数据安全性低(可能丢失最后一次快照后的所有数据);2. 全量快照,数据量大时 BGSAVEfork 子进程耗时,阻塞短时间;3. 恢复大文件时可能阻塞 Redis |
2. AOF(Append Only File):追加日志持久化
(1)核心原理
- 记录 Redis 的所有写命令 (如 SET/HSET/LPUSH)到日志文件(
appendonly.aof),重启时重新执行命令恢复数据; - 触发方式:实时追加(默认),通过配置控制刷盘频率。
(2)核心配置(redis.conf)
conf
# 开启AOF(默认关闭)
appendonly yes
# AOF文件名称
appendfilename "appendonly.aof"
# 刷盘策略(核心)
# appendfsync always:每次写命令都刷盘,数据最安全,性能最差;
# appendfsync everysec:每秒刷盘一次,折中(推荐);
# appendfsync no:由操作系统决定刷盘时机,性能最好,数据安全性最低;
appendfsync everysec
# AOF文件重写(解决文件过大问题)
# 自动重写触发条件:当前AOF文件大小 > 基准大小*100% 且 增量 > 64MB
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
(3)AOF 重写
- 问题:AOF 文件会随写命令不断增大,恢复耗时;
- 原理:重写生成 "精简版" AOF 文件(如将 100 次 INCR 合并为 SET key 100),不影响原文件;
- 触发方式:手动
BGREWRITEAOF、自动触发(配置文件)。
(4)优缺点
| 优点 | 缺点 |
|---|---|
| 1. 数据安全性高(everysec 仅丢失 1 秒数据);2. 日志文件可读,可手动修复;3. 重写机制优化文件体积 | 1. AOF 文件体积远大于 RDB;2. 恢复速度比 RDB 慢;3. 刷盘策略对性能有影响(always 性能最差) |
3. RDB vs AOF 选型建议
| 场景 | 推荐方案 |
|---|---|
| 追求高性能、可接受少量数据丢失(如缓存) | 仅开启 RDB |
| 追求数据安全性、不可接受大量数据丢失(如支付、订单) | 开启 AOF(everysec)+ 关闭 RDB(或低频率 RDB 备份) |
| 生产环境最优解 | 开启 AOF(everysec)+ 定时手动 BGSAVE 做 RDB 备份(兼顾安全和恢复速度) |
4. 数据恢复
- RDB 恢复:将
dump.rdb放入 Redis 数据目录,启动 Redis 自动加载; - AOF 恢复:将
appendonly.aof放入 Redis 数据目录,启动 Redis 自动执行日志中的命令; - 优先级:AOF 开启时优先加载 AOF,否则加载 RDB。
三、Redis 缓存核心策略(实战避坑)
使用 Redis 做缓存时,必须解决缓存穿透、缓存击穿、缓存雪崩三大问题,否则会导致数据库压力剧增甚至服务崩溃。
1. 缓存穿透:请求不存在的 key,穿透到数据库
(1)问题原因
- 黑客攻击 / 恶意请求(如查询 id=-1 的用户),缓存中无数据,所有请求都打到数据库;
- 数据库也无数据,缓存无法缓存空结果,导致每次请求都穿透。
(2)解决方案(按优先级)
① 缓存空值:数据库查不到数据时,缓存空值(如 "")并设置短过期时间(如 5 分钟);
java
运行
java
public User getUserById(Long userId) {
String key = "user:info:" + userId;
// 1. 查缓存
String userJson = stringRedisTemplate.opsForValue().get(key);
if (userJson != null) {
// 空值判断
if ("".equals(userJson)) {
return null;
}
return JSON.parseObject(userJson, User.class);
}
// 2. 查数据库
User user = userMapper.selectById(userId);
// 3. 缓存空值(过期时间5分钟)
if (user == null) {
stringRedisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES);
return null;
}
// 4. 缓存正常数据(过期时间1小时)
stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(user), 1, TimeUnit.HOURS);
return user;
}
② 布隆过滤器 :提前将所有合法 key 存入布隆过滤器,请求先过过滤器,非法 key 直接拒绝(适合数据固定的场景,如商品 ID);③ 接口限流 / 防刷:对高频恶意请求做限流(如 IP 限流、接口 QPS 限制)。
2. 缓存击穿:热点 key 过期,大量请求穿透到数据库
(1)问题原因
- 某个热点 key(如秒杀商品)过期瞬间,大量请求同时命中该 key,缓存未命中,全部打到数据库。
(2)解决方案(按优先级)
① 热点 key 永不过期 :核心热点 key 不设置过期时间,通过后台定时任务更新缓存;② 互斥锁(分布式锁):缓存未命中时,先获取分布式锁,只有拿到锁的请求才查数据库,其他请求等待 / 重试;
java
运行
java
public User getHotUserById(Long userId) {
String key = "user:hot:info:" + userId;
String lockKey = "lock:user:" + userId;
// 1. 查缓存
String userJson = stringRedisTemplate.opsForValue().get(key);
if (userJson != null) {
return JSON.parseObject(userJson, User.class);
}
// 2. 获取分布式锁(SET NX EX:不存在则设置,过期时间30秒)
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(lock)) {
try {
// 3. 再次查缓存(避免锁等待期间已被其他请求更新)
userJson = stringRedisTemplate.opsForValue().get(key);
if (userJson != null) {
return JSON.parseObject(userJson, User.class);
}
// 4. 查数据库
User user = userMapper.selectById(userId);
if (user != null) {
// 5. 更新缓存(过期时间1小时)
stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(user), 1, TimeUnit.HOURS);
}
return user;
} finally {
// 6. 释放锁
stringRedisTemplate.delete(lockKey);
}
} else {
// 7. 未拿到锁,重试(休眠100ms后递归)
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getHotUserById(userId);
}
}
③ 缓存预热:提前将热点数据加载到缓存,避免运行时才加载。
3. 缓存雪崩:大量 key 同时过期,数据库压力剧增
(1)问题原因
- 大量缓存 key 设置了相同的过期时间,过期瞬间所有 key 失效,请求全部打到数据库;
- Redis 集群宕机,所有请求穿透到数据库。
(2)解决方案
① 过期时间加随机值:给每个 key 的过期时间增加随机值(如 1-30 分钟),避免同时过期;
java
运行
java
// 原过期时间:1小时
// 优化后:1小时 ± 随机30分钟
long expireTime = 3600 + new Random().nextInt(1800);
stringRedisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.SECONDS);
② Redis 集群高可用 :部署 Redis 主从 + 哨兵 / 集群,避免单点故障;③ 降级 / 熔断 :Redis 宕机时,通过熔断机制限制数据库请求(如返回默认值、提示 "服务繁忙");④ 多级缓存:增加本地缓存(如 Caffeine),减少 Redis 依赖。
4. 缓存更新策略
| 策略 | 实现方式 | 适用场景 | 优缺点 |
|---|---|---|---|
| 失效更新(Cache Aside) | 1. 读:先查缓存,miss 查数据库,更新缓存;2. 写:先更数据库,再删缓存 | 绝大多数业务场景(推荐) | 优点:简单易实现;缺点:可能存在短暂数据不一致 |
| 写穿(Write Through) | 写:先更缓存,缓存同步更数据库 | 数据一致性要求高的场景 | 优点:数据一致;缺点:写性能差(同步数据库) |
| 写回(Write Back) | 写:先更缓存,异步批量更数据库 | 写性能要求高的场景 | 优点:写性能好;缺点:数据可能丢失(缓存宕机) |
生产推荐:Cache Aside(失效更新)+ 删缓存而非更缓存(避免并发更新导致数据不一致)。
四、Redis 使用避坑指南
- 禁止使用 KEYS 命令:KEYS * 会遍历所有 key,阻塞 Redis(生产用 SCAN 命令分批遍历);
- 设置合理的过期时间:避免缓存永不过期导致内存溢出;
- 大 key 拆分:避免存储超大 String(如 100MB)、超大 Hash/List,删除大 key 会阻塞 Redis;
- 避免缓存与数据库数据不一致:写操作遵循 "先更数据库,再删缓存",而非 "先更缓存,再更数据库";
- Redis 序列化:SpringBoot 中 RedisTemplate 默认使用 JDK 序列化(乱码),推荐用 StringRedisTemplate(String)或自定义 Jackson2JsonRedisSerializer(JSON)。
总结
- Redis 核心数据结构需结合场景记忆:String 做计数器、Hash 存对象、Sorted Set 做排行榜,是开发中最常用的三类结构;
- 持久化机制:RDB 性能优但数据安全性低,AOF 数据安全但性能和文件体积差,生产环境推荐 AOF(everysec)+ 定时 RDB 备份;
- 缓存三大问题:穿透用缓存空值 / 布隆过滤器,击穿用分布式锁 / 热点 key 永不过期,雪崩用随机过期时间 / 集群高可用,Cache Aside 是最常用的缓存更新策略。
今天的内容覆盖了 Redis 的核心知识点和实战避坑点,建议结合项目场景多练手(比如用 Redis 实现点赞、排行榜),把知识点落地。后续会讲解 Redis 分布式锁、Redis 集群等进阶内容,关注专栏持续进阶~