From addd8342b24c67943dbee53eb8d8ed581f388514 Mon Sep 17 00:00:00 2001 From: gaoqr <13665037151@163.com> Date: Wed, 5 Aug 2026 11:30:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=92=E5=8D=95=E9=94=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/admin/plan/PlanController.java | 6 +- .../admin/plan/vo/OrderPlanLockReqVO.java | 3 +- .../service/plan/PlanServiceImpl.java | 32 ++++++- .../service/plan/PlanServiceImplLockTest.java | 88 +++++++++++++++++++ 4 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 cf-module-prod-executor/cf-module-prod-executor-biz/src/test/java/com/cf/imes/module/executor/service/plan/PlanServiceImplLockTest.java diff --git a/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/java/com/cf/imes/module/executor/controller/admin/plan/PlanController.java b/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/java/com/cf/imes/module/executor/controller/admin/plan/PlanController.java index f7a5315ca..fe5654cb1 100644 --- a/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/java/com/cf/imes/module/executor/controller/admin/plan/PlanController.java +++ b/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/java/com/cf/imes/module/executor/controller/admin/plan/PlanController.java @@ -192,7 +192,8 @@ public class PlanController { @PostMapping("/lock") @Operation(summary = "获取排单锁") @PreAuthorize("@ss.hasAnyPermissions('placeorder:optimize','production:manager-list:calculate','manage:order-plan:process-scheme','producePlan:order-plan:process-scheme','producePlan:optimize')") - public CommonResult getPlanLock(@RequestBody OrderPlanLockReqVO orderPlanLockReqVO) { + public CommonResult getPlanLock( + @Valid @RequestBody OrderPlanLockReqVO orderPlanLockReqVO) { planService.getPlanLock(orderPlanLockReqVO); return success(true); } @@ -200,7 +201,8 @@ public class PlanController { @PostMapping("/release/lock") @Operation(summary = "释放排单锁") @PreAuthorize("@ss.hasAnyPermissions('placeorder:optimize','production:manager-list:calculate','manage:order-plan:process-scheme','producePlan:order-plan:process-scheme','producePlan:optimize')") - public CommonResult releasePlanLock(@RequestBody OrderPlanLockReqVO orderPlanLockReqVO) { + public CommonResult releasePlanLock( + @Valid @RequestBody OrderPlanLockReqVO orderPlanLockReqVO) { planService.releasePlanLock(orderPlanLockReqVO); return success(true); } diff --git a/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/java/com/cf/imes/module/executor/controller/admin/plan/vo/OrderPlanLockReqVO.java b/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/java/com/cf/imes/module/executor/controller/admin/plan/vo/OrderPlanLockReqVO.java index 76cb51f64..bad398d15 100644 --- a/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/java/com/cf/imes/module/executor/controller/admin/plan/vo/OrderPlanLockReqVO.java +++ b/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/java/com/cf/imes/module/executor/controller/admin/plan/vo/OrderPlanLockReqVO.java @@ -1,6 +1,7 @@ package com.cf.imes.module.executor.controller.admin.plan.vo; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -18,6 +19,6 @@ public class OrderPlanLockReqVO { private Long planId; @Schema(description = "排单优化本地值") - @NotNull(message = "排单优化本地值不能为空") + @NotBlank(message = "排单优化本地值不能为空") private String val; } diff --git a/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/java/com/cf/imes/module/executor/service/plan/PlanServiceImpl.java b/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/java/com/cf/imes/module/executor/service/plan/PlanServiceImpl.java index c857a09cf..21f6252fd 100644 --- a/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/java/com/cf/imes/module/executor/service/plan/PlanServiceImpl.java +++ b/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/java/com/cf/imes/module/executor/service/plan/PlanServiceImpl.java @@ -66,6 +66,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.aop.framework.AopContext; import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.ObjectUtils; @@ -81,6 +82,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; import static com.cf.imes.framework.common.pojo.PageParam.PAGE_SIZE_NONE; import static com.cf.imes.framework.common.util.json.JsonUtils.parseArray; import static com.cf.imes.framework.redis.constants.RedisKeyConstants.ORDER_PLAN_OPTIMIZE_KEY; @@ -97,6 +99,20 @@ import static com.cf.imes.module.executor.enums.ErrorCodeConstants.*; @Slf4j public class PlanServiceImpl implements PlanService { + /** 原子校验锁归属并删除,避免 GET 与 DELETE 之间锁过期后误删其他请求的新锁。 */ + private static final DefaultRedisScript RELEASE_PLAN_LOCK_SCRIPT; + + static { + RELEASE_PLAN_LOCK_SCRIPT = new DefaultRedisScript<>(); + RELEASE_PLAN_LOCK_SCRIPT.setScriptText(""" + if redis.call('get', KEYS[1]) == ARGV[1] then + return redis.call('del', KEYS[1]) + end + return 0 + """); + RELEASE_PLAN_LOCK_SCRIPT.setResultType(Long.class); + } + @Resource private PlanMapper planMapper; @@ -2189,6 +2205,7 @@ public class PlanServiceImpl implements PlanService { public void getPlanLock(OrderPlanLockReqVO orderPlanLockReqVO) { Long planId = orderPlanLockReqVO.getPlanId(); String lockValInput = orderPlanLockReqVO.getVal(); + validatePlanLockValue(lockValInput); PlanDO planDO = planMapper.selectById(planId); @@ -2200,7 +2217,7 @@ public class PlanServiceImpl implements PlanService { String lockVal = stringRedisTemplate.opsForValue().get(planLockKey); // 没有锁,尝试获取 - if (StrUtil.isBlank(lockVal)) { + if (lockVal == null) { Boolean success = stringRedisTemplate.opsForValue().setIfAbsent( planLockKey, @@ -2226,13 +2243,20 @@ public class PlanServiceImpl implements PlanService { public void releasePlanLock(OrderPlanLockReqVO orderPlanLockReqVO) { Long planId = orderPlanLockReqVO.getPlanId(); String lockValInput = orderPlanLockReqVO.getVal(); + validatePlanLockValue(lockValInput); String lockKey = String.format(ORDER_PLAN_OPTIMIZE_KEY, planId); - String lockVal = stringRedisTemplate.opsForValue().get(lockKey); + Long released = stringRedisTemplate.execute( + RELEASE_PLAN_LOCK_SCRIPT, Collections.singletonList(lockKey), lockValInput); + if (!Long.valueOf(1L).equals(released)) { + log.warn("释放排单锁失败,锁不存在或锁值不匹配,planId={}", planId); + } + } - if (Objects.equals(lockVal, lockValInput)) { - stringRedisTemplate.delete(lockKey); + private void validatePlanLockValue(String lockValue) { + if (StrUtil.isBlank(lockValue)) { + throw invalidParamException("排单优化本地值不能为空"); } } } diff --git a/cf-module-prod-executor/cf-module-prod-executor-biz/src/test/java/com/cf/imes/module/executor/service/plan/PlanServiceImplLockTest.java b/cf-module-prod-executor/cf-module-prod-executor-biz/src/test/java/com/cf/imes/module/executor/service/plan/PlanServiceImplLockTest.java new file mode 100644 index 000000000..2820d6fae --- /dev/null +++ b/cf-module-prod-executor/cf-module-prod-executor-biz/src/test/java/com/cf/imes/module/executor/service/plan/PlanServiceImplLockTest.java @@ -0,0 +1,88 @@ +package com.cf.imes.module.executor.service.plan; + +import com.cf.imes.framework.common.exception.ServiceException; +import com.cf.imes.module.executor.controller.admin.plan.vo.OrderPlanLockReqVO; +import com.cf.imes.module.executor.dal.dataobject.plan.PlanDO; +import com.cf.imes.module.executor.dal.mysql.plan.PlanMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.data.redis.core.script.DefaultRedisScript; + +import java.util.Collections; + +import static com.cf.imes.module.executor.enums.ErrorCodeConstants.ORDER_PLAN_LOCKED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PlanServiceImplLockTest { + + @Mock + private PlanMapper planMapper; + @Mock + private StringRedisTemplate stringRedisTemplate; + @Mock + private ValueOperations valueOperations; + + @InjectMocks + private PlanServiceImpl planService; + + @Test + void shouldRejectBlankLockValueInService() { + OrderPlanLockReqVO request = lockRequest(1L, " "); + + assertThatThrownBy(() -> planService.getPlanLock(request)) + .isInstanceOf(ServiceException.class) + .hasMessage("排单优化本地值不能为空"); + verifyNoInteractions(planMapper, stringRedisTemplate); + } + + @Test + void shouldNotTreatExistingEmptyValueAsUnlocked() { + Long planId = 1L; + PlanDO plan = new PlanDO(); + plan.setId(planId); + when(planMapper.selectById(planId)).thenReturn(plan); + when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations); + when(valueOperations.get("order_plan_optimize:" + planId)).thenReturn(""); + + assertThatThrownBy(() -> planService.getPlanLock(lockRequest(planId, "owner-1"))) + .isInstanceOfSatisfying(ServiceException.class, + ex -> assertThat(ex.getCode()).isEqualTo(ORDER_PLAN_LOCKED.getCode())); + verify(valueOperations, never()).setIfAbsent(anyString(), anyString(), any()); + } + + @Test + void shouldReleaseLockWithAtomicLuaScript() { + Long planId = 1L; + String lockKey = "order_plan_optimize:" + planId; + + planService.releasePlanLock(lockRequest(planId, "owner-1")); + + verify(stringRedisTemplate).execute( + any(DefaultRedisScript.class), + eq(Collections.singletonList(lockKey)), + eq("owner-1")); + verify(stringRedisTemplate, never()).opsForValue(); + verify(stringRedisTemplate, never()).delete(anyString()); + } + + private OrderPlanLockReqVO lockRequest(Long planId, String value) { + OrderPlanLockReqVO request = new OrderPlanLockReqVO(); + request.setPlanId(planId); + request.setVal(value); + return request; + } +}