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

0 阅读7分钟

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

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

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


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

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

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

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

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


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

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

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

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

破解之道缓存空对象

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) ,让第一个请求去加载数据库,其余请求排队等。

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 缓存雪崩:大量数据同时过期

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

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

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,所有服务器共享同一份登录态:

// 登录成功时
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 脚本,且脚本在执行期间是原子的。这意味着你可以把“查询-判断-更新”这样多个步骤的操作打包,避免并发下的数据竞争。

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

-- 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 中调用:

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

缓存解决的是读多写少的场景,它不是银弹。
设计缓存时,永远先想好“怎么失效”和“怎么兜底”。

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