逻辑过期方案需验证expiretime字段判断与异步重建:测试中应手动设expiretime为过去时间,断言返回旧数据且线程池提交重建任务;互斥锁测试需模拟多线程竞争,断言数据库仅被查询一次且所有线程返回一致缓存值。

如何用单元测试验证逻辑过期是否真正生效
逻辑过期方案的核心不是 Redis key 的 TTL,而是 value 里嵌套的 expireTime 字段是否被正确读取、比较和触发异步重建。单元测试必须绕过“真实时间流逝”,否则无法稳定复现过期分支。
关键做法是:用 LocalDateTime.now() 的可替换方式(如注入 Clock)或直接 mock 时间判断逻辑。例如在测试中把 RedisData.expireTime 设为过去时间,并确保 isAfter(LocalDateTime.now()) 返回 false。
- 测试前手动写入一条带已过期
expireTime的RedisData到 Redis(用stringRedisTemplate.opsForValue().set()) - 调用
queryWithLogicalExpire(id),断言返回的是旧数据(非 null),且不抛异常 - 检查线程池是否提交了任务(可用
CountDownLatch+ 自定义ThreadPoolExecutor捕获任务) - 不要依赖“等 1 秒再查缓存”——这是不可靠的时序测试
互斥锁能否被并发请求正确拦截?测什么
互斥锁方案的单元测试重点不是“锁本身是否加成功”,而是高并发下是否只触发一次数据库查询、其余请求是否都返回缓存值(哪怕是旧值)。真实并发难模拟,但可以用多线程 + CountDownLatch 模拟竞争场景。
典型错误是只测单线程下的 tryLock 返回值,却没验证锁释放后其他线程是否能拿到新数据。更隐蔽的问题是:锁 key 拼接错误(比如漏了前缀)、锁过期时间太短导致提前释放、unlock 未校验锁所有权(误删他人锁)。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 启动 5 个线程同时调用
queryWithMutex(id) - 在 DAO 层打点(如
AtomicInteger dbCallCount = new AtomicInteger(0)),断言最终值为 1 - 检查所有线程返回的
Shop对象是否一致(说明都从缓存读,而非各自查库) - 故意让第一个线程在加锁后 sleep 200ms,验证后续线程是否等待而非立即失败
为什么测试时容易忽略“空值缓存”和“null 值处理”
缓存击穿测试常聚焦于“热点 key 过期”,但实际线上最棘手的是:key 存在但 value 是空字符串 "" 或 null,这既不是命中也不是穿透,而是逻辑漏洞。很多实现用 StrUtil.isBlank(json) 判断,但若 Redis 中存了 "null" 字符串,isBlank 会返回 false,导致反序列化失败抛异常。
- 显式写入
stringRedisTemplate.opsForValue().set("shop:1", ""),验证是否返回null而非抛JSONException - 写入
"null"字符串,确认反序列化逻辑有兜底(如JSONUtil.toBean(json, RedisData.class)是否能容忍) - 测试数据库查不到数据时,是否把
null写入缓存(应避免,否则下次永远走空逻辑)
测试环境 Redis 容器要不要配集群或哨兵
不需要。单元测试阶段的 Redis 只需满足基础命令支持即可,推荐用 redis-testcontainers 启一个单节点 Docker 实例,或者用 embedded-redis(注意 JDK 17+ 兼容性)。集群/哨兵带来的网络分区、重定向、failover 等行为,属于集成测试范畴,会干扰对“业务逻辑是否按预期走分支”的验证。
真正要警惕的是本地开发时误连了共享测试环境 Redis,导致测试污染数据或被他人测试干扰。务必在 @BeforeEach 清空相关 key,或使用随机 key 前缀(如 "test:" + UUID.randomUUID() + ":shop:1")。
逻辑过期和互斥锁的差异不在 Redis 部署形态,而在代码对 value 结构、锁生命周期、线程协作的控制精度——这些全在应用层,测试也该聚焦于此。










