导读:本期聚焦于新井创作的《如何用分布式锁安全重建Redis缓存防止缓存击穿?》,敬请观看详情。热点数据缓存过期时,瞬间涌入的大量请求会直接穿透到数据库,这种缓存击穿问题该如何高效解决?本文从缓存击穿的成因出发,介绍基于Redis的分布式锁重建缓存方案。核心思路是只允许一个请求获取锁后执行数据库查询并回写缓存,其余请求等待或快速失败,从而保护后端存储。文章深入讲解setnx实现、双重检查、锁超时与续期机制,并对比逻辑过期、本地互斥锁等替代方案。通过完整的Java代码示例,帮助开发者掌握分布式锁在缓存重建中的落地细节,避免锁失效或并发重建带来的数据不一致风险。

缓存击穿是缓存系统中最棘手的场景之一。一个被高频访问的热点Key在缓存中过期消失,此时恰好有大量并发请求同时查询这个Key,由于缓存未命中,所有请求都会直接打到数据库上。数据库的承受能力有限,瞬时压力过大就会导致连接耗尽、响应变慢,甚至引发整个服务雪崩。解决这个问题的关键思路就是让重建缓存的动作只由一个请求完成,其他请求要么等待结果,要么快速返回旧值或默认值。

如何用分布式锁安全重建Redis缓存防止缓存击穿?

实现这个目标最常用的手段就是分布式锁。在分布式环境中,单机锁无法协调多个服务实例,而Redis天然支持原子操作,非常适合用来构建分布式锁。通过在重建缓存前获取一把针对该Key的锁,可以确保同一时刻只有一个线程去查询数据库并回写缓存,其他线程则进入等待或重试流程。这样数据库的负载就从并发峰值降到了单次查询,大大提升了系统的稳定性。

一、缓存击穿的本质:为什么热点过期会压垮数据库

缓存击穿和缓存穿透、缓存雪崩经常被混淆。穿透指的是查询一个根本不存在的Key,导致每次请求都打到数据库;雪崩指的是大量Key在同一个时间窗口内集中过期,造成数据库压力陡增。而击穿聚焦在单个热点Key上,这个Key本身是存在的,只是刚好在某个时刻过期了。由于热点Key的访问频率极高,可能每秒有数千甚至上万次请求,一旦缓存失效,这些请求会像洪水一样涌向数据库。

举一个典型的例子:某个爆款商品的详情页信息被缓存在Redis中,过期时间设置为1小时。在过期前,所有请求都能通过缓存快速返回。当过期时间到达的那一刻,几十个用户同时刷新页面,此时缓存中已经没有数据,每个请求都会执行数据库查询。如果数据库每秒只能承受几百次查询,却被瞬间灌入几千次请求,连接池很快会被占满,后续请求只能排队等待,最终导致整个商品服务不可用。

缓存击穿的危害并不仅仅是数据库压力增大。当数据库响应变慢后,请求线程会长时间占用Web服务器的连接资源,连接数耗尽又会导致其他接口也无法正常响应,形成连锁故障。因此,在缓存设计阶段就必须考虑热点Key过期时的并发控制策略。分布式锁重建缓存正是解决这一问题的经典方案之一。

二、分布式锁重建缓存的核心流程与代码实现

使用Redis实现分布式锁来重建缓存的基本流程很清晰:请求先查缓存,如果缓存命中直接返回;如果未命中,则尝试获取一个针对该Key的分布式锁。获取锁成功的请求负责查询数据库、写入缓存并释放锁;获取锁失败的请求可以选择短暂等待后重新查缓存,或者直接返回一个降级结果。整个流程的核心是保证只有一个请求能进入数据库查询阶段。

下面用Java代码展示基于Jedis的简单实现。使用Redis的set命令配合NX和PX参数可以原子性地完成锁的获取和过期时间设置,避免先set再expire带来的原子性问题。

public String getData(String key) {
    String value = redis.get(key);
    if (value != null) {
        return value;
    }
    String lockKey = "lock:" + key;
    String requestId = UUID.randomUUID().toString();
    try {
        // 尝试获取分布式锁,NX表示不存在才设置,PX表示设置毫秒级过期时间
        boolean locked = redis.set(lockKey, requestId, "NX", "PX", 10000);
        if (locked) {
            // 双重检查:获取锁后再次查询缓存,防止其他线程已经完成重建
            value = redis.get(key);
            if (value != null) {
                return value;
            }
            // 查询数据库
            value = db.query(key);
            // 写入缓存,设置过期时间
            redis.setex(key, 3600, value);
            return value;
        } else {
            // 获取锁失败,短暂等待后重试
            Thread.sleep(50);
            return getData(key);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return null;
    } finally {
        // 释放锁时校验requestId,防止误删其他线程的锁
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        redis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
    }
}

这段代码有几个关键细节需要注意。首先,锁的value必须是一个唯一标识,比如UUID,这样在释放锁时可以校验这把锁是否还属于当前线程。如果不做校验,可能出现A线程持有锁超时被自动释放,B线程获取到锁后,A线程执行完数据库查询再去释放锁,结果把B线程的锁误删了。使用Lua脚本可以保证读取value和删除key这两个操作在Redis中原子执行。

其次,锁的过期时间设置需要谨慎。如果设置得太短,数据库查询还没完成锁就过期了,其他请求会重新获取锁并再次查询数据库,导致并发重建;如果设置得太长,持有锁的线程宕机后其他请求需要等待很久才能获取锁。通常可以设置一个相对合理的初始超时时间,并配合锁续期机制来动态延长。

三、双重检查与锁续期:避免锁失效导致并发重建

双重检查是分布式锁重建缓存中非常重要的一步。在上面的代码中,获取锁成功后并没有直接查询数据库,而是再次查询了一次缓存。这是因为在等待获取锁的过程中,前一个持有锁的线程可能已经完成了数据库查询并写入了缓存。如果跳过这次检查,第二个获取到锁的线程就会重复查询数据库,失去分布式锁保护的意义。双重检查能有效降低数据库的无效压力。

锁续期是另一个需要重点关注的环节。如果数据库查询耗时超过锁的过期时间,锁会自动释放,其他请求就有机会进入临界区。为了解决这个问题,可以使用Redisson这类客户端提供的看门狗机制,它会定期检查锁是否仍然被当前线程持有,并自动延长过期时间。下面展示使用Redisson的写法,代码更加简洁且具备锁续期能力。

public String getDataWithRedisson(String key) {
    String value = redis.get(key);
    if (value != null) {
        return value;
    }
    RLock lock = redisson.getLock("lock:" + key);
    lock.lock(10, TimeUnit.SECONDS);
    try {
        // 双重检查
        value = redis.get(key);
        if (value != null) {
            return value;
        }
        value = db.query(key);
        redis.setex(key, 3600, value);
        return value;
    } finally {
        lock.unlock();
    }
}

Redisson的lock方法默认启动看门狗,每隔一段时间检查锁是否仍然持有,如果持有则自动续期到指定时间,从而避免业务执行时间超过锁过期时间的问题。不过需要注意的是,如果业务逻辑中包含了非常耗时的外部调用,建议评估锁的持有时间是否合理,或者考虑使用逻辑过期方案来降低锁等待时间。

锁的粒度也很关键。分布式锁应该针对具体的缓存Key来设置,而不是使用一把全局锁。如果所有缓存重建都争抢同一把锁,那么不同业务之间会互相阻塞,锁竞争会非常激烈。针对每个Key生成独立的lockKey,可以实现细粒度的并发控制,只有访问同一个热点Key的请求才会争抢同一把锁。

四、缓存重建的替代方案与选型建议

分布式锁重建缓存并不是解决缓存击穿的唯一手段。逻辑过期方案也是一种常见思路:缓存数据本身不设置过期时间,而是额外保存一个逻辑过期时间字段。请求发现数据逻辑过期后,先返回旧数据,同时异步启动一个线程去更新缓存。这种方式不会阻塞用户请求,但需要维护额外的过期字段和异步更新任务,实现复杂度更高。

本地互斥锁适用于单实例服务内的缓存击穿场景。由于不需要跨进程协调,性能比分布式锁更好,实现也简单。但在微服务架构下,多个实例之间无法共享本地锁,仍然可能出现多个实例同时重建缓存的情况。因此,如果服务部署了多个副本,就必须使用分布式锁或者引入集中式的协调机制。

还有一种方案是对热点Key做永不过期处理,配合后台定时刷新逻辑。例如在活动开始前提前预热缓存,并在活动期间通过定时任务每隔几分钟更新一次缓存,避免Key自然过期。这种方案对业务侵入较小,适合可预测的热点数据。对于突发性的热点Key,可以结合限流组件在缓存未命中时限制进入数据库的请求数量。

综合来看,分布式锁重建缓存方案在实现难度、通用性和实时性之间取得了较好的平衡,适合大多数业务场景。如果请求量特别大且允许短暂的数据不一致,逻辑过期方案能提供更好的响应性能;如果服务是单实例且热点不多,本地锁可能更简单高效。

五、总结

缓存击穿是热点数据过期时最容易引发故障的问题之一。通过Redis分布式锁控制缓存重建的并发度,可以有效保护数据库免受瞬时高并发的冲击。实现时需要重点关注锁的唯一标识、原子释放、锁超时与续期、双重检查以及锁粒度等细节,任何一个环节处理不当都可能导致锁失效或并发重建。

实际生产环境中,建议优先使用成熟的分布式锁实现如Redisson,避免自己封装Redis命令时遗漏边界情况。同时,分布式锁只解决了重建过程的互斥问题,还需要结合合理的缓存过期时间、热点数据预热、限流降级等策略,构建完整的缓存稳定性体系。只有将多种手段配合使用,才能在高并发场景下保障系统的平稳运行。

Redis缓存击穿分布式锁缓存重建修改时间:2026-08-23 03:55:18

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。