350 lines
15 KiB
Java
350 lines
15 KiB
Java
package com.suisung.mall.common.utils;
|
||
|
||
import com.suisung.mall.core.web.service.RedisService;
|
||
import org.junit.jupiter.api.BeforeEach;
|
||
import org.junit.jupiter.api.Test;
|
||
import org.mockito.Mock;
|
||
import org.mockito.MockitoAnnotations;
|
||
import org.springframework.test.util.ReflectionTestUtils;
|
||
|
||
import java.util.concurrent.CountDownLatch;
|
||
import java.util.concurrent.ExecutorService;
|
||
import java.util.concurrent.Executors;
|
||
import java.util.concurrent.TimeUnit;
|
||
import java.util.concurrent.atomic.AtomicInteger;
|
||
|
||
import static org.junit.jupiter.api.Assertions.*;
|
||
import static org.mockito.Mockito.*;
|
||
|
||
/**
|
||
* DistributedLockHelper综合测试套件
|
||
*/
|
||
public class DistributedLockHelperTest {
|
||
|
||
private DistributedLockHelper lockHelper;
|
||
|
||
@Mock
|
||
private RedisService redisService;
|
||
|
||
@BeforeEach
|
||
public void setUp() {
|
||
MockitoAnnotations.openMocks(this);
|
||
lockHelper = new DistributedLockHelper();
|
||
ReflectionTestUtils.setField(lockHelper, "redisService", redisService);
|
||
System.out.println("=== 初始化测试环境 ===");
|
||
}
|
||
|
||
@Test
|
||
public void testTryLock_Success() {
|
||
System.out.println("开始执行: testTryLock_Success");
|
||
|
||
// 给定条件
|
||
String lockKey = "test_lock";
|
||
long expireTimeSec = 30L;
|
||
String expectedKey = "distributed_lock:" + lockKey;
|
||
System.out.println("测试参数 - 锁键: " + lockKey + ", 过期时间: " + expireTimeSec + "秒");
|
||
|
||
// 当调用时
|
||
when(redisService.set(anyString(), anyString(), anyLong())).thenReturn(true);
|
||
boolean result = lockHelper.tryLock(lockKey, expireTimeSec);
|
||
System.out.println("获取锁结果: " + (result ? "成功" : "失败"));
|
||
|
||
// 那么验证
|
||
assertTrue(result, "锁应该成功获取");
|
||
verify(redisService).set(eq(expectedKey), anyString(), eq(expireTimeSec));
|
||
System.out.println("验证通过: Redis set 方法被正确调用");
|
||
System.out.println("测试完成: testTryLock_Success\n");
|
||
}
|
||
|
||
@Test
|
||
public void testTryLock_Failure() {
|
||
System.out.println("开始执行: testTryLock_Failure");
|
||
|
||
// 给定条件
|
||
String lockKey = "test_lock";
|
||
long expireTimeSec = 30L;
|
||
System.out.println("测试参数 - 锁键: " + lockKey + ", 过期时间: " + expireTimeSec + "秒");
|
||
|
||
// 当调用时
|
||
when(redisService.set(anyString(), anyString(), anyLong())).thenThrow(new RuntimeException("Redis错误"));
|
||
System.out.println("模拟Redis异常: Redis错误");
|
||
|
||
// 那么验证
|
||
assertThrows(RuntimeException.class, () -> {
|
||
lockHelper.tryLock(lockKey, expireTimeSec);
|
||
}, "应该抛出RuntimeException");
|
||
System.out.println("验证通过: 正确捕获了RuntimeException");
|
||
System.out.println("测试完成: testTryLock_Failure\n");
|
||
}
|
||
|
||
@Test
|
||
public void testReleaseLock_Success() {
|
||
System.out.println("开始执行: testReleaseLock_Success");
|
||
|
||
// 给定条件
|
||
String lockKey = "test_lock";
|
||
String expectedKey = "distributed_lock:" + lockKey;
|
||
String lockValue = "uuid:1";
|
||
System.out.println("测试参数 - 锁键: " + lockKey + ", 锁值: " + lockValue);
|
||
|
||
// 设置线程本地变量
|
||
ReflectionTestUtils.setField(lockHelper, "LOCK_VALUE_HOLDER",
|
||
new ThreadLocal<String>() {
|
||
@Override
|
||
protected String initialValue() {
|
||
return lockValue;
|
||
}
|
||
});
|
||
|
||
when(redisService.get(expectedKey)).thenReturn(lockValue);
|
||
System.out.println("模拟Redis get返回值: " + lockValue);
|
||
|
||
// 当调用时
|
||
lockHelper.releaseLock(lockKey);
|
||
System.out.println("执行锁释放操作");
|
||
|
||
// 那么验证
|
||
verify(redisService).del(expectedKey);
|
||
System.out.println("验证通过: Redis del 方法被正确调用");
|
||
System.out.println("测试完成: testReleaseLock_Success\n");
|
||
}
|
||
|
||
@Test
|
||
public void testReleaseLock_WrongOwner() {
|
||
System.out.println("开始执行: testReleaseLock_WrongOwner");
|
||
|
||
// 给定条件
|
||
String lockKey = "test_lock";
|
||
String expectedKey = "distributed_lock:" + lockKey;
|
||
String currentValue = "uuid:1";
|
||
String threadValue = "uuid:2"; // 不同的值
|
||
System.out.println("测试参数 - 当前锁值: " + currentValue + ", 线程锁值: " + threadValue);
|
||
|
||
// 设置线程本地变量
|
||
ReflectionTestUtils.setField(lockHelper, "LOCK_VALUE_HOLDER",
|
||
new ThreadLocal<String>() {
|
||
@Override
|
||
protected String initialValue() {
|
||
return threadValue;
|
||
}
|
||
});
|
||
|
||
when(redisService.get(expectedKey)).thenReturn(currentValue);
|
||
System.out.println("模拟Redis中当前锁值: " + currentValue);
|
||
|
||
// 当调用时
|
||
lockHelper.releaseLock(lockKey);
|
||
System.out.println("执行锁释放操作");
|
||
|
||
// 那么验证
|
||
verify(redisService, never()).del(anyString());
|
||
System.out.println("验证通过: Redis del 方法未被调用(防止误删)");
|
||
System.out.println("测试完成: testReleaseLock_WrongOwner\n");
|
||
}
|
||
|
||
@Test
|
||
public void testConcurrentAccess_SingleLockAcquirer() throws InterruptedException {
|
||
System.out.println("开始执行: testConcurrentAccess_SingleLockAcquirer");
|
||
|
||
// 给定条件
|
||
String lockKey = "concurrent_test_lock";
|
||
int threadCount = 10;
|
||
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
||
CountDownLatch latch = new CountDownLatch(threadCount);
|
||
AtomicInteger successCounter = new AtomicInteger(0);
|
||
System.out.println("测试参数 - 并发线程数: " + threadCount + ", 锁键: " + lockKey);
|
||
|
||
// 模拟Redis并发访问行为
|
||
when(redisService.set(anyString(), anyString(), anyLong()))
|
||
.thenAnswer(invocation -> {
|
||
// 只有第一次调用成功,其余都失败
|
||
boolean result = successCounter.get() == 0;
|
||
if (result) {
|
||
System.out.println("线程[" + Thread.currentThread().getName() + "] 成功获取锁");
|
||
}
|
||
return result;
|
||
});
|
||
System.out.println("模拟Redis行为: 只允许一个线程获取锁");
|
||
|
||
// 当执行时
|
||
for (int i = 0; i < threadCount; i++) {
|
||
final int threadId = i;
|
||
executor.submit(() -> {
|
||
try {
|
||
System.out.println("启动线程[" + threadId + "]");
|
||
if (lockHelper.tryLock(lockKey, 30)) {
|
||
int count = successCounter.incrementAndGet();
|
||
System.out.println("线程[" + threadId + "] 获取锁成功,当前成功计数: " + count);
|
||
lockHelper.releaseLock(lockKey);
|
||
System.out.println("线程[" + threadId + "] 释放锁");
|
||
} else {
|
||
System.out.println("线程[" + threadId + "] 获取锁失败");
|
||
}
|
||
} finally {
|
||
latch.countDown();
|
||
System.out.println("线程[" + threadId + "] 执行完毕,剩余线程数: " + (latch.getCount()));
|
||
}
|
||
});
|
||
}
|
||
|
||
boolean completed = latch.await(5, TimeUnit.SECONDS);
|
||
executor.shutdown();
|
||
System.out.println("所有线程执行完成: " + (completed ? "正常结束" : "超时结束"));
|
||
|
||
// 那么验证
|
||
int successCount = successCounter.get();
|
||
assertEquals(1, successCount, "应该只有一个线程获得锁");
|
||
System.out.println("验证通过: 实际成功获取锁的线程数为 " + successCount);
|
||
System.out.println("测试完成: testConcurrentAccess_SingleLockAcquirer\n");
|
||
}
|
||
|
||
@Test
|
||
public void testAutoRenewalFunctionality() throws InterruptedException {
|
||
System.out.println("开始执行: testAutoRenewalFunctionality");
|
||
|
||
// 给定条件
|
||
String lockKey = "auto_renew_test_lock";
|
||
long expireTimeSec = 5L;
|
||
long maxHoldTimeSec = 10L;
|
||
System.out.println("测试参数 - 锁键: " + lockKey + ", 过期时间: " + expireTimeSec + "秒, 最大持有时间: " + maxHoldTimeSec + "秒");
|
||
|
||
when(redisService.set(anyString(), anyString(), anyLong())).thenReturn(true);
|
||
when(redisService.get(anyString())).thenReturn("mock-value");
|
||
System.out.println("模拟Redis set/get操作成功");
|
||
|
||
// 当调用时
|
||
DistributedLockHelper.LockHolder lockHolder =
|
||
lockHelper.tryLockWithAutoRenew(lockKey, expireTimeSec, maxHoldTimeSec);
|
||
System.out.println("获取带自动续期功能的锁");
|
||
|
||
assertNotNull(lockHolder, "锁持有者不应为空");
|
||
System.out.println("锁持有者创建成功");
|
||
|
||
// 等待可能的续期操作
|
||
System.out.println("等待3秒观察自动续期行为...");
|
||
Thread.sleep(3000);
|
||
|
||
// 那么验证
|
||
verify(redisService, atLeastOnce()).expire(anyString(), anyLong());
|
||
System.out.println("验证通过: Redis expire 方法至少被调用一次");
|
||
|
||
// 清理
|
||
lockHolder.close();
|
||
System.out.println("关闭锁持有者,清理资源");
|
||
System.out.println("测试完成: testAutoRenewalFunctionality\n");
|
||
}
|
||
|
||
@Test
|
||
public void testLockHolderAutoClose() {
|
||
System.out.println("开始执行: testLockHolderAutoClose");
|
||
|
||
// 给定条件
|
||
String lockKey = "auto_close_test_lock";
|
||
System.out.println("测试参数 - 锁键: " + lockKey);
|
||
|
||
when(redisService.set(anyString(), anyString(), anyLong())).thenReturn(true);
|
||
when(redisService.get(anyString())).thenReturn("mock-value");
|
||
System.out.println("模拟Redis操作成功");
|
||
|
||
// 当调用时
|
||
System.out.println("使用try-with-resources语法获取锁");
|
||
try (DistributedLockHelper.LockHolder holder =
|
||
lockHelper.tryLockWithAutoRenew(lockKey, 30, 300)) {
|
||
assertNotNull(holder, "锁持有者不应为空");
|
||
System.out.println("锁持有者创建成功");
|
||
}
|
||
System.out.println("try-with-resources块结束,自动调用close方法");
|
||
|
||
// 那么验证
|
||
verify(redisService).del(anyString());
|
||
System.out.println("验证通过: Redis del 方法被正确调用");
|
||
System.out.println("测试完成: testLockHolderAutoClose\n");
|
||
}
|
||
|
||
@Test
|
||
public void testMultipleLocksIndependence() {
|
||
System.out.println("开始执行: testMultipleLocksIndependence");
|
||
|
||
// 给定条件
|
||
String lockKey1 = "lock_1";
|
||
String lockKey2 = "lock_2";
|
||
System.out.println("测试参数 - 锁键1: " + lockKey1 + ", 锁键2: " + lockKey2);
|
||
|
||
when(redisService.set(anyString(), anyString(), anyLong())).thenReturn(true);
|
||
System.out.println("模拟Redis set操作成功");
|
||
|
||
// 当调用时
|
||
boolean lock1Acquired = lockHelper.tryLock(lockKey1, 30);
|
||
System.out.println("获取第一个锁: " + (lock1Acquired ? "成功" : "失败"));
|
||
|
||
boolean lock2Acquired = lockHelper.tryLock(lockKey2, 30);
|
||
System.out.println("获取第二个锁: " + (lock2Acquired ? "成功" : "失败"));
|
||
|
||
// 那么验证
|
||
assertTrue(lock1Acquired, "第一个锁应该成功获取");
|
||
assertTrue(lock2Acquired, "第二个锁应该成功获取");
|
||
System.out.println("两个锁都成功获取");
|
||
|
||
// 验证它们是不同的键
|
||
verify(redisService).set(eq("distributed_lock:" + lockKey1), anyString(), eq(30L));
|
||
verify(redisService).set(eq("distributed_lock:" + lockKey2), anyString(), eq(30L));
|
||
System.out.println("验证通过: 两个独立的锁键都被正确设置");
|
||
System.out.println("测试完成: testMultipleLocksIndependence\n");
|
||
}
|
||
|
||
@Test
|
||
public void testThreadIsolation() throws InterruptedException {
|
||
System.out.println("开始执行: testThreadIsolation");
|
||
|
||
// 给定条件
|
||
String lockKey = "thread_isolation_lock";
|
||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||
CountDownLatch latch = new CountDownLatch(2);
|
||
AtomicInteger[] results = new AtomicInteger[]{new AtomicInteger(0), new AtomicInteger(0)};
|
||
System.out.println("测试参数 - 锁键: " + lockKey + ", 线程数: 2");
|
||
|
||
when(redisService.set(anyString(), anyString(), anyLong())).thenReturn(true);
|
||
System.out.println("模拟Redis set操作成功");
|
||
|
||
// 当执行时
|
||
System.out.println("启动两个线程竞争同一个锁");
|
||
for (int i = 0; i < 2; i++) {
|
||
final int threadIndex = i;
|
||
executor.submit(() -> {
|
||
try {
|
||
System.out.println("线程[" + threadIndex + "] 尝试获取锁");
|
||
if (lockHelper.tryLock(lockKey, 30)) {
|
||
int count = results[threadIndex].incrementAndGet();
|
||
System.out.println("线程[" + threadIndex + "] 获取锁成功,计数: " + count);
|
||
// 持有锁一段时间
|
||
Thread.sleep(100);
|
||
System.out.println("线程[" + threadIndex + "] 持有锁100ms后释放");
|
||
lockHelper.releaseLock(lockKey);
|
||
System.out.println("线程[" + threadIndex + "] 释放锁");
|
||
} else {
|
||
System.out.println("线程[" + threadIndex + "] 获取锁失败");
|
||
}
|
||
} catch (InterruptedException e) {
|
||
System.out.println("线程[" + threadIndex + "] 被中断");
|
||
Thread.currentThread().interrupt();
|
||
} finally {
|
||
latch.countDown();
|
||
System.out.println("线程[" + threadIndex + "] 执行完毕");
|
||
}
|
||
});
|
||
}
|
||
|
||
boolean completed = latch.await(5, TimeUnit.SECONDS);
|
||
executor.shutdown();
|
||
System.out.println("所有线程执行完成: " + (completed ? "正常结束" : "超时结束"));
|
||
|
||
// 那么验证 - 两个线程都应该能够在不同时间获得锁
|
||
int result0 = results[0].get();
|
||
int result1 = results[1].get();
|
||
assertEquals(1, result0, "线程0应该获得一次锁");
|
||
assertEquals(1, result1, "线程1应该获得一次锁");
|
||
System.out.println("验证通过: 线程0获得锁次数=" + result0 + ", 线程1获得锁次数=" + result1);
|
||
System.out.println("测试完成: testThreadIsolation\n");
|
||
}
|
||
}
|