跳转到主内容
websoft网络软件专家 - 深耕网络技术,打造实用软件!

Redis如何解决缓存击穿_为什么使用分布式锁比同步块更高效

缓存击穿需用分布式锁而非synchronized,Redis SETNX+EX+校验value+退避重试是手写底线;Redisson因看门狗续期、阻塞等待、可重入更可靠;逻辑过期靠应用层判断expireTime并容忍脏数据。 缓存击穿发生时,
GET
返回 null 后直接查 DB 是最危险的路径 缓存击穿本质是:一个高并发访问的热点
key
刚好过期,大量请求同时发现缓存未命中,全部涌向数据库。此时如果每个请求都独立执行
SELECT
,DB 瞬间被打满,连接池耗尽、慢 SQL 堆积、服务雪崩。 很多人第一反应是加
synchronized
,比如用
static Object lock = new Object()
包住 DB 查询逻辑。但这只在单 JVM 进程内有效——现代微服务基本都是多实例部署,
synchronized
对其他机器上的线程完全无效,锁形同虚设。 真正要拦住的是「跨进程、跨机器」的并发请求,必须靠分布式协调机制。Redis 的
SETNX
(或更安全的
SET key value EX seconds NX
)天然适合做这层拦截,因为它由 Redis 单点原子性保障,所有客户端都服从同一把锁。
SETNX
+
DEL
手动实现分布式锁的三个硬约束 不用框架也能快速落地,但必须守住三条底线,否则等于没锁:
SET
必须带
NX
(不存在才设置)和
EX
(自动过期),缺一不可。只写
SETNX
不设超时,一旦业务异常没释放锁,就永久死锁
DEL
删除锁前必须校验 value 是否匹配(防止 A 加锁、B 超时释放了 A 的锁),纯
DEL key
是严重误删 获取锁失败后不能无限自旋,必须
Thread.sleep(10)
或指数退避,否则千个线程空转 CPU,服务先于 DB 崩溃 示例关键逻辑:
Boolean locked = redisTemplate.opsForValue() .setIfAbsent("lock:user:1001", "uuid-abc", 10, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { User user = db.selectById(1001); redisTemplate.opsForValue().set("user:1001", toJson(user), 3600, TimeUnit.SECONDS); } finally { // Lua 脚本保证原子性删除 redisTemplate.execute(delLockScript, Collections.singletonList("lock:user:1001"), "uuid-abc"); } }
为什么
Redisson
的
tryLock
比手写更可靠 手写锁容易漏掉几个关键细节: Redis 8.2.3 Redis 8.2.3 是一款安全优先的高性能键值存储系统。该版本紧急修复了可能引发远程代码执行(RCE)的高危漏洞(CVE-2025-62507),并解决了 HyperLogLog 及 Cuckoo Filter 等数据结构在特定场景下的崩溃问题。建议所有用户立即升级,以保障生产环境的系统稳定与数据安全。 下载 锁续期问题:
业务查询 DB + 写缓存
耗时超过 10 秒,而锁已过期,其他线程趁虚而入 ——
Redisson
的看门狗(watchdog)会自动给未释放锁续命 阻塞等待逻辑:手写需自己循环
tryLock
+
sleep
,而
tryLock(3, 10, TimeUnit.SECONDS)
直接封装了「最多等 3 秒,锁最长持有 10 秒」的语义 可重入支持:同一个线程重复加锁不阻塞,手写需额外维护线程 ID 和重入计数,极易出错 如果你的项目已引入
redisson-spring-boot-starter
,直接用
RLock lock = redissonClient.getLock("lock:user:1001")
,比反复核对 Lua 脚本安全得多。 逻辑过期方案里,
SET
不带
EX
是故意的 逻辑过期不是不设过期,而是把过期判断从 Redis 移到应用层。典型结构是存一个 JSON:
{"data": {"id": 1001, "name": "Alice"}, "expireTime": 1743825600000}
这时
SET user:1001 '{...}'
确实不带
EX
,因为你要它「永不过期」——但必须配合内存淘汰策略(如
maxmemory-policy allkeys-lru
),否则内存迟早爆掉。 真正关键的是读取时的双重判断: 先
GET user:1001
,解析出
expireTime
若
System.currentTimeMillis() > expireTime
,才去抢互斥锁、异步刷新 抢锁失败?直接返回旧数据,不阻塞 这个方案的代价很明确:用户可能看到最多
expireTime
周期内的脏数据。如果业务容忍不了(比如余额、库存),就别用逻辑过期,老实用互斥锁+强一致性。

相关文章