【Day61】Redis 深入:吃透数据结构、持久化(RDB/AOF)与缓存策略

前言

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 使用避坑指南

  1. 禁止使用 KEYS 命令:KEYS * 会遍历所有 key,阻塞 Redis(生产用 SCAN 命令分批遍历);
  2. 设置合理的过期时间:避免缓存永不过期导致内存溢出;
  3. 大 key 拆分:避免存储超大 String(如 100MB)、超大 Hash/List,删除大 key 会阻塞 Redis;
  4. 避免缓存与数据库数据不一致:写操作遵循 "先更数据库,再删缓存",而非 "先更缓存,再更数据库";
  5. Redis 序列化:SpringBoot 中 RedisTemplate 默认使用 JDK 序列化(乱码),推荐用 StringRedisTemplate(String)或自定义 Jackson2JsonRedisSerializer(JSON)。

总结

  1. Redis 核心数据结构需结合场景记忆:String 做计数器、Hash 存对象、Sorted Set 做排行榜,是开发中最常用的三类结构;
  2. 持久化机制:RDB 性能优但数据安全性低,AOF 数据安全但性能和文件体积差,生产环境推荐 AOF(everysec)+ 定时 RDB 备份;
  3. 缓存三大问题:穿透用缓存空值 / 布隆过滤器,击穿用分布式锁 / 热点 key 永不过期,雪崩用随机过期时间 / 集群高可用,Cache Aside 是最常用的缓存更新策略。

今天的内容覆盖了 Redis 的核心知识点和实战避坑点,建议结合项目场景多练手(比如用 Redis 实现点赞、排行榜),把知识点落地。后续会讲解 Redis 分布式锁、Redis 集群等进阶内容,关注专栏持续进阶~

相关推荐
独处东汉3 小时前
freertos开发空气检测仪之输入子系统结构体设计
数据结构·人工智能·stm32·单片机·嵌入式硬件·算法
放荡不羁的野指针3 小时前
leetcode150题-滑动窗口
数据结构·算法·leetcode
BHXDML3 小时前
数据结构:(一)从内存底层逻辑理解线性表
数据结构
小龙报3 小时前
【C语言进阶数据结构与算法】单链表综合练习:1.删除链表中等于给定值 val 的所有节点 2.反转链表 3.链表中间节点
c语言·开发语言·数据结构·c++·算法·链表·visual studio
lots洋4 小时前
使用docker-compose安装mysql+redis+nacos
redis·mysql·docker
Anastasiozzzz4 小时前
LeetCode Hot100 215. 数组中的第K个最大元素
数据结构·算法·leetcode
Jia ming5 小时前
TLB与高速缓存:加速地址与数据的双引擎
缓存·tlb
xuedingbue5 小时前
数据结构与顺序表:高效数据管理秘籍
数据结构·算法·链表
h7ml6 小时前
高并发场景下查券返利机器人的请求合并与缓存预热策略(Redis + Caffeine 实践)
数据库·redis·缓存