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

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

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

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

相关推荐
苍何1 小时前
用 AI 做了套品牌周边,感觉能挂Shopify开卖了
后端
用户298698530141 小时前
免费在线将 PDF 转为 Word:5 款好用的转换工具
人工智能·后端
newerp1 小时前
unsafe 包与底层编程
后端
orient1 小时前
并发问题排查实战
后端
枫叶V1 小时前
聊天框正在过时:Generative UI 与 A2UI/AG-UI 会怎样改变 AI 应用?
后端
Conan在掘金1 小时前
鸿蒙 7.0 分布式数据盾:DDO 跨端数据对象 + DID 数字身份——可信同步根因
后端
Conan在掘金1 小时前
鸿蒙 7.0 游戏快启:launchAcceleration 预启动 + 冷启预建链——秒开根因
后端
对象存储与RustFS1 小时前
为什么越来越多的企业选择RustFS作为对象存储?
后端·rust·开源
Conan在掘金1 小时前
鸿蒙 7.0 空间音频引擎:AudioSpatializationManager 空间渲染——立体声场根因
后端