互斥锁适合强一致性场景,逻辑过期更适合高并发、可容忍短暂脏数据的业务;两者依queryuser接口sla选择——要最新数据用互斥锁,要快且稳用逻辑过期。

直接说结论:互斥锁适合强一致性场景,逻辑过期更适合高并发、可容忍短暂脏数据的业务。两者不是二选一,而是根据 queryUser 这类接口的 SLA 要求来定——要最新数据就用互斥锁,要快且稳就用逻辑过期。
为什么 setIfAbsent 是互斥锁的核心
Redis 没有原生“分布式锁”命令,setIfAbsent(即 SET key value NX EX 10)是事实标准:它原子性地判断 key 是否存在,并在不存在时写入 + 设置过期时间。这一步必须带 EX(过期),否则一旦线程崩溃未释放锁,就会永久死锁。
- 过期时间不能拍脑袋定——得略大于数据库查询 + 序列化 + 写缓存的 P99 耗时,比如实测重建耗时 800ms,那就设
EX 2秒,留出缓冲 - 不要用
delete简单删 key 做解锁:必须校验锁的 value 是否为自己所设(防误删),否则 A 线程锁还没释放完,B 线程重建完成顺手删了锁,C 线程立刻抢到锁又查一次库 - Java 中
Boolean flag = opsForValue().setIfAbsent(...)可能返回null(网络超时或 Redis 不响应),必须用BooleanUtil.isTrue(flag)判空,不能直接if (flag)
逻辑过期怎么避免“永远不更新”
逻辑过期不是真永不过期,而是把过期时间藏在 value 里。比如存的是 JSON:{"data":{...},"expireTime":1743672300000},每次读取都要解析这个字段,和当前时间比对。关键点在于:这个 expireTime 是你代码控制的,不是 Redis 的 TTL。
- 写缓存时,value 必须包含
expireTime字段,且该时间应为“业务认为数据失效的绝对时间点”,比如活动结束时间戳 - 读取时先反序列化,再用
System.currentTimeMillis() > expireTime判断是否逻辑过期;别用LocalDateTime.now(),服务器时钟可能不同步 - 逻辑过期后触发的异步重建,必须确保只执行一次——仍需用
setIfAbsent尝试获取锁,失败就直接返回旧值,不重试
tryLock + double-check 的实际写法
所谓 double-check,是指“查缓存 → 未命中 → 尝试加锁 → 加锁成功 → 再查一次缓存 → 还没命中才查库”,第二次查缓存是为了防止加锁排队过程中,前面的线程已经把缓存写好了。
- 加锁失败后,不要立即
Thread.sleep(50)然后重试——这会拖慢整体响应,应改用短延时(如10ms)+ 最大重试次数(如 3 次),之后直接降级查库或返回默认值 - 锁 key 的命名要有业务上下文,比如商品用
lock:shop:123,别只用lock:123,避免不同业务冲突 - 如果用 Spring Cache,
@Cacheable默认不支持互斥锁,得自己封装CacheLoader或切面,别指望注解自动解决击穿
最容易被忽略的是时钟同步问题:逻辑过期依赖所有应用节点的系统时间一致,如果某台机器时间慢了 5 分钟,它会一直认为数据没过期,导致脏数据长期滞留。上线前务必跑 timedatectl status 确认 NTP 已同步。










