缓存击穿:当热点Key失效时,如何用Lua脚本守住数据库大门? 先明确一个核心场景:一个热点Key在过期瞬间,海量并发请求同时发现缓存“空窗”,于是齐刷刷涌向数据库,导致瞬时压力爆表。这就是缓存击穿。那么,单靠SETNX命令能防住吗?答案是不够。 问题就出在“判断”和“写入”这两步是分离的。多个请求

先明确一个核心场景:一个热点Key在过期瞬间,海量并发请求同时发现缓存“空窗”,于是齐刷刷涌向数据库,导致瞬时压力爆表。这就是缓存击穿。那么,单靠SETNX命令能防住吗?答案是不够。
长期稳定更新的攒劲资源: >>>点此立即查看<<<
问题就出在“判断”和“写入”这两步是分离的。多个请求几乎在同一毫秒发现缓存缺失,都去执行SETNX,结果可能都失败——因为第一个写入还没来得及完成。于是,大家又都掉头去查数据库,击穿依然发生。这背后的根本症结在于,整个操作缺乏原子性保障。
加分布式锁?当然是一种思路。但锁的获取、业务查询、缓存写入这条链路太长,锁的持有时间难以控制,万一中间环节异常,还可能引发锁无法释放的新问题。
所以,解决思路其实很直接:能不能把“检查缓存、决定是否回源、写入新缓存”这一连串依赖Redis状态的操作,打包成一个不可分割的整体来执行?这就引出了我们今天的主角——Lua脚本。
缓存击穿指某个热点 key 过期瞬间,大量并发请求同时发现缓存缺失,全部穿透到数据库,造成瞬时压力激增。单纯用 SETNX 判断是否存在再 set,存在竞态:多个请求几乎同时 SETNX 失败(因为还没来得及写入),又同时去查 DB,结果还是打穿了。
根本问题在于「判断缓存是否存在」和「写入新缓存」这两步不是原子的。哪怕加锁(如 Redis 分布式锁),锁的获取、业务查询、缓存写入整个链路长,锁持有时间不可控,还可能因异常导致锁未释放。
解决思路很直接:把「检查 + 设置 + 回源逻辑」收束进一次 Redis 操作里——这就轮到 Lua 脚本上场了。
Redis执行Lua脚本的原子性,是解决这个问题的关键。脚本里的所有Redis命令会排好队顺序执行,期间不会有任何其他客户端的命令插队。我们可以利用这个特性,设计一个两阶段方案。
第一阶段,脚本只做读取和判断:
redis.call("GET", KEYS[1])尝试获取缓存值。nil,脚本就返回一个特殊标记(比如"MISS"),这相当于告诉客户端:“缓存是空的,该你上场去查数据库了。”第二阶段,由客户端执行回源写入:
"MISS"信号后,去查询数据库,拿到最新数据。NX条件的SET命令),尝试将数据写回Redis。这里的NX参数至关重要,它确保只有第一个执行写入的客户端能成功,后续客户端的写入请求会被忽略,从而避免重复回源和覆盖。这个方案的精妙之处在于,它通过原子性的脚本判断,将“是否回源”的决策收归一处;再通过客户端的NX写入,保证了回源动作的唯一性。既避免了在Redis内执行长事务的风险,又有效防止了数据库被重复击穿。
这是一个非常普遍的误解。必须明确一点:Lua脚本在Redis服务端运行,它的执行环境是沙箱化的,没有网络I/O能力。这意味着脚本无法发起HTTP请求,也无法连接MySQL等外部数据库。所有需要与外部系统交互的操作,都必须由客户端来完成。
有人可能会想,能不能在Lua脚本里再调用EVAL去执行另一个脚本呢?答案也是否定的。Redis明确禁止在Lua脚本中递归调用EVAL或EVALSHA,尝试这样做会直接收到ERR recursive script detected的错误。
因此,实际可行的架构永远是清晰的“三段论”:EVAL(原子读判断)→ 客户端(查库计算)→ SET NX或EVAL(原子写回填)。中间那个最耗时的数据库查询环节,注定要放在应用进程中执行。
方案设计好了,但在生产环境落地时,魔鬼往往藏在细节里。下面这三个细节,稍不注意就可能踩坑。
第一,脚本SHA1缓存的复用问题。EVALSHA命令通过传递脚本的SHA1摘要来执行,比每次都传递完整脚本的EVAL要高效。但前提是,这个脚本必须已经通过SCRIPT LOAD命令预加载到Redis服务器中。如果上线时忘了这一步,EVALSHA执行失败后会降级为EVAL,性能优化就打了水漂。
第二,Key过期时间的硬编码陷阱。很多开发者会把TTL(生存时间)直接写在Lua脚本里,例如redis.call("SET", KEYS[1], ARGV[1], "EX", 3600)。这带来一个维护难题:哪天业务需要调整缓存时间,你就得修改脚本、重新加载、更新所有客户端引用的SHA1值。更优雅的做法是把过期时间作为参数(比如ARGV[2])动态传入,让脚本保持通用和灵活。
第三,Lua返回值类型的处理。在Redis的Lua环境中,nil和空字符串""是两种完全不同的类型。redis.call("GET", ...)在Key不存在时返回的是nil。如果你在脚本里错误地使用if res == "" then进行判断,这个条件永远都不会为真。正确的判断方式应该是if not res then或者显式地检查if res == false or res == nil then。
说到底,技术方案从理论到实践,中间隔着的就是对这些细微之处的把握。用好Lua脚本这把原子操作利器,再避开这些常见的“坑”,你的缓存系统在面对热点击穿时,才能真正做到固若金汤。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述