排单锁优化

This commit is contained in:
gaoqr
2026-08-05 11:30:54 +08:00
parent e291dbdb56
commit addd8342b2
4 changed files with 122 additions and 7 deletions
@@ -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<Boolean> getPlanLock(@RequestBody OrderPlanLockReqVO orderPlanLockReqVO) {
public CommonResult<Boolean> 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<Boolean> releasePlanLock(@RequestBody OrderPlanLockReqVO orderPlanLockReqVO) {
public CommonResult<Boolean> releasePlanLock(
@Valid @RequestBody OrderPlanLockReqVO orderPlanLockReqVO) {
planService.releasePlanLock(orderPlanLockReqVO);
return success(true);
}
@@ -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;
}
@@ -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<Long> 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("排单优化本地值不能为空");
}
}
}
@@ -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<String, String> 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;
}
}