task) {
LockHandle handle = acquire(lockKey, waitTimeout, leaseTimeout);
try {
@@ -70,24 +91,46 @@ public class RedisLockExecutor {
* @return 锁句柄
*/
public LockHandle acquire(String lockKey, Duration waitTimeout, Duration leaseTimeout) {
+ LockHandle handle = tryAcquire(lockKey, waitTimeout, leaseTimeout);
+ if (handle == null) {
+ throw new IllegalStateException("获取分布式锁失败,请稍后重试,lockKey=" + lockKey);
+ }
+ return handle;
+ }
+
+ /**
+ * 尝试获取显式释放的分布式锁句柄。
+ *
+ * 返回 {@code null} 表示锁当前被其他节点持有。Redis 访问失败或等待过程被中断仍会抛出异常,
+ * 调用方可据此区分“正常跳过”和“基础设施异常”。
+ *
+ * @param lockKey 锁 key
+ * @param waitTimeout 等待时间
+ * @param leaseTimeout 租约时间
+ * @return 获取成功时返回锁句柄,否则返回 null
+ */
+ public LockHandle tryAcquire(String lockKey, Duration waitTimeout, Duration leaseTimeout) {
String lockValue = UUID.randomUUID().toString();
boolean acquired = false;
long deadline = System.nanoTime() + waitTimeout.toNanos();
try {
- while (System.nanoTime() <= deadline) {
+ do {
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, leaseTimeout);
if (Boolean.TRUE.equals(success)) {
acquired = true;
break;
}
+ if (System.nanoTime() >= deadline) {
+ break;
+ }
Thread.sleep(RETRY_INTERVAL_MILLIS);
- }
+ } while (System.nanoTime() <= deadline);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("等待分布式锁被中断,lockKey=" + lockKey, e);
}
if (!acquired) {
- throw new IllegalStateException("获取分布式锁失败,请稍后重试,lockKey=" + lockKey);
+ return null;
}
return new LockHandle(lockKey, lockValue, leaseTimeout);
}
diff --git a/easyflow-commons/easyflow-common-cache/src/test/java/tech/easyflow/common/cache/DistributedScheduledLockAspectTest.java b/easyflow-commons/easyflow-common-cache/src/test/java/tech/easyflow/common/cache/DistributedScheduledLockAspectTest.java
new file mode 100644
index 0000000..29e098d
--- /dev/null
+++ b/easyflow-commons/easyflow-common-cache/src/test/java/tech/easyflow/common/cache/DistributedScheduledLockAspectTest.java
@@ -0,0 +1,108 @@
+package tech.easyflow.common.cache;
+
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.Signature;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.data.redis.core.script.RedisScript;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * {@link DistributedScheduledLockAspect} 回归测试。
+ */
+public class DistributedScheduledLockAspectTest {
+
+ /**
+ * 验证未抢到调度锁时跳过原方法。
+ *
+ * @throws Throwable 切面执行异常
+ */
+ @Test
+ public void aroundShouldSkipTaskWhenLockIsHeld() throws Throwable {
+ RedisLockExecutor executor = createExecutor(false);
+ DistributedScheduledLockAspect aspect = new DistributedScheduledLockAspect(executor);
+ AtomicInteger proceedCount = new AtomicInteger();
+
+ Object result = aspect.around(
+ mockJoinPoint(proceedCount),
+ annotatedMethod("lockedTask").getAnnotation(DistributedScheduledLock.class)
+ );
+
+ Assert.assertNull(result);
+ Assert.assertEquals(0, proceedCount.get());
+ }
+
+ /**
+ * 验证抢到调度锁时执行原方法并释放锁。
+ *
+ * @throws Throwable 切面执行异常
+ */
+ @Test
+ public void aroundShouldProceedAndReleaseWhenLockAcquired() throws Throwable {
+ RedisLockExecutor executor = createExecutor(true);
+ DistributedScheduledLockAspect aspect = new DistributedScheduledLockAspect(executor);
+ AtomicInteger proceedCount = new AtomicInteger();
+
+ Object result = aspect.around(
+ mockJoinPoint(proceedCount),
+ annotatedMethod("lockedTask").getAnnotation(DistributedScheduledLock.class)
+ );
+
+ Assert.assertEquals("ok", result);
+ Assert.assertEquals(1, proceedCount.get());
+ }
+
+ @DistributedScheduledLock(key = "easyflow:test:scheduled", leaseSeconds = 30L)
+ private void lockedTask() {
+ }
+
+ private Method annotatedMethod(String methodName) throws NoSuchMethodException {
+ Method method = DistributedScheduledLockAspectTest.class.getDeclaredMethod(methodName);
+ method.setAccessible(true);
+ return method;
+ }
+
+ private ProceedingJoinPoint mockJoinPoint(AtomicInteger proceedCount) throws Throwable {
+ ProceedingJoinPoint joinPoint = Mockito.mock(ProceedingJoinPoint.class);
+ Signature signature = Mockito.mock(Signature.class);
+ Mockito.when(signature.toShortString()).thenReturn("lockedTask()");
+ Mockito.when(joinPoint.getSignature()).thenReturn(signature);
+ Mockito.when(joinPoint.proceed()).thenAnswer(invocation -> {
+ proceedCount.incrementAndGet();
+ return "ok";
+ });
+ return joinPoint;
+ }
+
+ @SuppressWarnings("unchecked")
+ private RedisLockExecutor createExecutor(boolean acquired) throws Exception {
+ StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class);
+ ValueOperations valueOperations = Mockito.mock(ValueOperations.class);
+ Mockito.when(valueOperations.setIfAbsent(
+ ArgumentMatchers.anyString(),
+ ArgumentMatchers.anyString(),
+ ArgumentMatchers.any(Duration.class)
+ )).thenReturn(acquired);
+ Mockito.when(redisTemplate.opsForValue()).thenReturn(valueOperations);
+ Mockito.when(redisTemplate.execute(
+ ArgumentMatchers.>any(),
+ ArgumentMatchers.>any(),
+ ArgumentMatchers.