Redis 实战避坑指南:从缓存击穿到高可用,一文全搞定

不知道你有没有遇到过这样的场景:深夜正酣睡,忽然告警电话炸响------数据库 CPU 飙到 90%,服务濒临崩溃。排查后发现,不过是一个刚上线的活动页面,涌入了超出预期的流量。又或者,一名恶意用户拿着随机生成的 ID 疯狂请求,直接打穿了你的缓存层,数据库瞬间被拖垮。

这就是现实。企业级网站动辄每秒上万次查询,直接把所有压力甩给数据库无异于自掘坟墓。而 Redis,就是你手里的那张王牌。说白了,Redis 就是为扛住高并发而生的内存保险箱

今天这篇,我们不谈高深莫测的源码,只聊如何把 Redis 用对、用好,让你的系统稳得像块磐石。


1. 重新认识 Redis:不只是个缓存

Redis(Remote Dictionary Server),可以简单理解成一个放在内存 里的数据库。因为数据在内存,读写速度比 MySQL 快几个数量级。它通过 Key-Value 的方式存取数据,同时支持丰富的结构:StringHashListSetSorted Set。这些数据结构就是你解决各种并发难题的乐高积木。

但 Redis 的真正威力,在于它可以作为挡在数据库前面的盾牌

markdown 复制代码
用户请求  →  Redis(查缓存)  →  有数据,直接返回
                                →  无数据,查数据库 → 回写 Redis

这就是经典的 Cache Aside 模式,也是解决 80% 性能问题的银弹。


2. 缓存三大"夺命坑":穿透、击穿、雪崩

只要用缓存,这三个问题绕不过去。但别怕,每一个都有对症的解法。

2.1 缓存穿透:查一个根本不存在的数据

恶意用户拿一个 ID=-1 无限请求,缓存里没有,数据库里也没有。每次请求都直达数据库,这就是穿透。

破解之道缓存空对象

kotlin 复制代码
public Item getItem(Long id) {
    String cacheKey = "item:" + id;
    Item item = redis.get(cacheKey);
    if (item != null) {
        return item;
    }
    // 缓存未命中,查数据库
    item = db.query(id);
    if (item == null) {
        // 即使数据库也没有,也在 Redis 里存一个"空值标记",并设置较短过期时间
        redis.set(cacheKey, EMPTY_OBJECT, 60); // 60 秒过期
        return null;
    }
    redis.set(cacheKey, item, 3600);
    return item;
}

你也可以配合布隆过滤器 提前拦截非法 Key,但空对象方案最简单有效。记住一点:宁缓存一个空,也别放过一次无效的数据库查询

2.2 缓存击穿:热点数据突然过期

一个爆款商品的缓存刚好到期,瞬间成千上万的请求直接涌向数据库------这就是击穿。数据库能扛得住才怪。

破解之道互斥锁(Mutex) ,让第一个请求去加载数据库,其余请求排队等。

kotlin 复制代码
public Item getHotItem(Long id) {
    String cacheKey = "hot_item:" + id;
    Item item = redis.get(cacheKey);
    if (item != null) return item;

    // 获取分布式锁,只让一个请求重建缓存
    String lockKey = "lock:item:" + id;
    try {
        if (redis.setnx(lockKey, 1, 10)) { // 加锁,10秒自动过期
            // 双重检查,避免在加锁间隙有其他线程已重建
            item = redis.get(cacheKey);
            if (item != null) return item;
            
            item = db.query(id);
            redis.set(cacheKey, item, 3600);
        } else {
            // 没抢到锁的线程,休眠一会儿后重试(或直接走本地缓存兜底)
            Thread.sleep(50);
            return getHotItem(id);  // 递归重试
        }
    } finally {
        redis.del(lockKey);
    }
    return item;
}

有人可能会说递归重试会卡线程,实际你可以返回一个旧值或降级数据,但核心思想不变:第一个冲进去的兄弟扛下所有,其他人等着乘凉

2.3 缓存雪崩:大量数据同时过期

当你在同一时间点设置了大量缓存的相同过期时间,一旦到期,海量请求会像雪崩一样砸向数据库。

破解之道过期时间加随机盐

ini 复制代码
int baseExpire = 3600; // 基础1小时
int randomDelta = new Random().nextInt(600); // 随机0~600秒
redis.set(key, value, baseExpire + randomDelta);

这样缓存过期的时间被均匀打散,避免了集体失效。防雪崩的真谛就是------不要让大家的步调过于一致


3. 分布式 Session:让用户登录不再"串台"

在分布式架构下,用户第一次登录被负载均衡打到服务器 A,第二次请求却到了服务器 B,B 上没有 Session,用户被迫重复登录------体验直接崩盘。

把 Session 存入 Redis,所有服务器共享同一份登录态:

dart 复制代码
// 登录成功时
String sessionId = UUID.randomUUID().toString();
redis.setex("session:" + sessionId, 1800, userJson);

// 任意服务器校验
String userJson = redis.get("session:" + sessionId);
if (userJson != null) {
    // 登录有效,重置过期时间
    redis.expire("session:" + sessionId, 1800);
}

如此一来,用户状态不再与某台机器强绑定,水平扩展变得轻而易举。


4. Lua 脚本:保证操作的原子性

Redis 支持执行 Lua 脚本,且脚本在执行期间是原子的。这意味着你可以把"查询-判断-更新"这样多个步骤的操作打包,避免并发下的数据竞争。

比如一个抢优惠券的场景,避免超发:

lua 复制代码
-- KEYS[1]: 优惠券库存 key, ARGV[1]: 用户ID
local stock = tonumber(redis.call('get', KEYS[1]))
if stock > 0 then
    redis.call('decr', KEYS[1])
    redis.call('sadd', 'coupon:users', ARGV[1])
    return 1  -- 抢券成功
else
    return 0  -- 已抢光
end

在 Java 中调用:

ini 复制代码
String script = "..."; // 上面的 Lua 脚本
Long result = (Long) jedis.eval(script, Collections.singletonList("coupon:stock"), 
                                Collections.singletonList(userId));

你会发现,用 Lua 脚本能轻松实现乐观锁、限流等复杂逻辑,而且性能极高。当你需要多个 Redis 命令同生共死时,请想起 Lua


5. 高并发下的组合拳:多级缓存与预热

多级缓存

浏览器缓存 → CDN → Nginx 本地缓存 → Redis → 数据库。层层设防,每一层都能挡下一部分流量。常见做法是先用 Caffeine 在应用内做一级缓存(进程内,速度极快),Redis 做二级缓存,可以大幅降低 Redis 的压力。

热点数据预热

大促来临前,你肯定不会等到秒杀那一刻才去加载商品详情。提前把热数据灌进 Redis,并设置为常驻内存(过期时间设得很长或手动延期),流量洪峰到来时直接命中缓存,数据库毫发无伤。


6. 持久化:别让你的数据"一重启就蒸发"

Redis 数据在内存,一宕机就全丢。务必开启持久化:

  • RDB(快照) :定期将内存全量快照写入硬盘。恢复速度快,但可能丢失最后一次快照之后的数据。
  • AOF(追加日志) :记录每一个写操作命令,重启时通过回放日志恢复数据。数据更安全,但文件体积大,恢复慢。

生产环境通常会同时开启两者,用 RDB 做冷备,AOF 保底,牺牲一点写入性能换来数据不丢。


7. 高可用:主从 + 哨兵,宕机也不怕

单机 Redis 挂了就是灭顶之灾,所以你需要:

  • 主从复制:一主多从,主负责写,从负责读。主从之间通过复制协议保持数据一致。
  • 哨兵(Sentinel) :监控所有节点,一旦主节点失联,自动发起投票,将从节点提升为新主,并通知客户端切换连接。

如果你的数据量太大单机放不下,那就需要 Redis Cluster,它把数据分片存储在多台机器上,同时兼具高可用能力。


8. 内存不够用?淘汰策略来兜底

Redis 配置了最大内存后,当数据写满时,会根据淘汰策略清理部分数据。常见策略:

  • noeviction:拒绝写入,直接报错(生产不推荐)。
  • allkeys-lru:在全体 Key 中,淘汰最近最少使用的(适合缓存场景)。
  • allkeys-lfu:淘汰使用频率最低的(适合有冷热数据区分的场景)。
  • volatile-ttl:在设置了过期时间的 Key 中,优先淘汰剩余存活时间最短的。

你可以在 redis.conf 里这样配:

复制代码
maxmemory 4gb
maxmemory-policy allkeys-lru

选策略就像过日子:该丢的丢,把空间留给最有用的数据。


写在最后

Redis 用得好,它是性能怪兽、并发利器;用得不好,它也会成为数据的坟墓、故障的源头。记住下面这条心法:

缓存解决的是读多写少的场景,它不是银弹。

设计缓存时,永远先想好"怎么失效"和"怎么兜底"。

把这些实战技巧融入你的项目,你不仅能解决面试中八股文般的问题,更能真正交付一个稳定、抗造的系统。如果觉得有帮助,不妨点个赞收藏,咱们下篇见!

相关推荐
笃行35017 小时前
向量数据库单独建一套,这笔账划不划算
后端
weixin_4316004418 小时前
Agent Workflow 学习向:Code 节点,比模板更强的变量加工
后端·python·学习·ai·
Rain的Java大神之路18 小时前
短信接口被狂刷怎么处理
java·运维·后端·web安全·面试·架构·xss
2602_9599609219 小时前
谢飞机面试大厂:Spring Boot、JVM、Redis、Kafka、微服务与音视频搜索场景求生实录
java·jvm·spring boot·redis·面试题
Lost of 程序猿19 小时前
AOP 实战:面向切面编程从理论到落地
后端·asp.net·aop·面向切片变成
Lost of 程序猿19 小时前
ASP.NET Core 认证授权实战:从 JWT 到 Policy,设计一套企业级 RBAC 权限系统
后端·asp.net
Lost of 程序猿19 小时前
ASP.NET Core 后台任务全景:从 BackgroundService 到 Channel 队列,再到分布式调度
后端·asp.net·.netcore
智购科技无人售货机厂家20 小时前
2026自动售货机实时库存管理系统:从Redis缓存到持久化的数据架构演进~YH
redis·缓存·架构
倾颜20 小时前
NestJS 核心概念梳理:从 Module、Controller 到 Guard、Interceptor
后端·node.js·nestjs
nvd1120 小时前
LiteLLM 如何使用 Redis:从部署位置到精确响应缓存
redis·缓存·gateway