分布式锁测试必须基于真实redis并覆盖互斥性、自动释放、防误删、可重入四类行为,重点验证异常竞争下是否守得住底线,而非仅功能通路。

直接测分布式锁的加锁和释放逻辑,不能只靠常规单元测试跑一次就完事——因为它的核心行为(互斥、超时、可重入、防误删)依赖外部组件(如 Redis 或数据库)和并发环境。纯 in-memory 模拟容易漏掉关键边界,比如锁过期后被其他线程续命、释放时校验 owner 失败、Lua 脚本原子性缺失等。
重点测什么:不是“能不能跑通”,而是“会不会错”
分布式锁的单元测试目标不是验证功能存在,而是验证它在异常、竞争、超时、误操作下仍能守住底线。需覆盖以下四类行为:
- 互斥性:两个线程/客户端同时 tryLock,必须只有一个成功,另一个阻塞或失败
- 自动释放:锁设置 3 秒过期,3 秒后调用 isLocked() 应返回 false,且后续加锁不再受旧锁影响
- 防误删:线程 A 拿到锁但执行超时,锁自动过期;线程 B 成功加锁;此时线程 A 执行 unlock(),不能删掉线程 B 的锁
- 可重入安全(如支持):同一线程重复 lock() 应成功,且 unlock() 需配对调用,最终才真正释放
推荐测试策略:分层 + 真实依赖 + 并发驱动
不建议完全 mock Redis 客户端(如 Jedis 或 Lettuce),mock 容易掩盖原子性缺陷(比如 SETNX+EX 不是原子的,而 SET key val NX PX 是)。更稳妥的做法是:
- 本地启动一个真实 Redis 实例(用 Testcontainers 或 redis-server 命令行),让测试连接它
- 用
CountDownLatch或CyclicBarrier控制多线程并发时机,精准触发竞争场景 - 加锁后主动 sleep 模拟业务耗时,再检查锁状态或触发超时
- 对 unlock 方法,传入错误 requestId 或已过期的锁 key,验证是否静默失败而非抛异常或误删
一个可落地的 Redis 分布式锁测试片段示例
假设你用的是基于 SET key value NX PX 的简单实现(非 Redisson):
// 测试:两个线程争同一把锁,仅一个应成功
@Test
void testLockMutualExclusion() throws InterruptedException {
String lockKey = "test:order:1001";
String ownerIdA = "node-a-123";
String ownerIdB = "node-b-456";
AtomicBoolean gotLockA = new AtomicBoolean(false);
AtomicBoolean gotLockB = new AtomicBoolean(false);
Thread t1 = new Thread(() -> {
gotLockA.set(lockService.tryLock(lockKey, ownerIdA, 5000));
});
Thread t2 = new Thread(() -> {
// 确保 t1 先执行加锁逻辑
try { Thread.sleep(10); } catch (InterruptedException ignored) {}
gotLockB.set(lockService.tryLock(lockKey, ownerIdB, 5000));
});
t1.start(); t2.start();
t1.join(); t2.join();
assertTrue(gotLockA.get()); // 至少一个拿到
assertFalse(gotLockB.get()); // 另一个必须失败
}
注意:该测试依赖真实 Redis,且未使用 @Transactional 或静态共享 state,避免测试污染。
别忽略的收尾验证
每次测试结束后,建议显式清理锁资源,防止干扰后续测试:
- 在
@AfterEach中调用redisTemplate.delete(lockKey) - 或统一使用随机 lockKey(如
"test:" + UUID.randomUUID()),天然隔离 - 对数据库锁实现,测试完 truncate 锁表,而不是 delete —— 避免唯一索引残留影响下一轮
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











