From d09efe6812f01d770e7f125f365459da25704d0d Mon Sep 17 00:00:00 2001 From: yiwenpeng <ywp303@163.com> Date: Mon, 3 Jun 2024 23:08:27 +0800 Subject: [PATCH] feat:并发问题:如果多个线程同时修改同一个shipmentDetail的状态,可能会导致数据不一致,用redis锁解决 --- src/main/java/com/huaheng/framework/config/ShiroConfig.java | 10 +++++----- src/main/java/com/huaheng/pc/task/taskHeader/service/ReceiptTaskService.java | 3 +++ src/main/java/com/huaheng/pc/task/taskHeader/service/ShipmentTaskService.java | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------- 3 files changed, 114 insertions(+), 42 deletions(-) diff --git a/src/main/java/com/huaheng/framework/config/ShiroConfig.java b/src/main/java/com/huaheng/framework/config/ShiroConfig.java index 46e64c8..d1eda63 100644 --- a/src/main/java/com/huaheng/framework/config/ShiroConfig.java +++ b/src/main/java/com/huaheng/framework/config/ShiroConfig.java @@ -131,7 +131,7 @@ public class ShiroConfig { public SpringSessionValidationScheduler sessionValidationScheduler() { SpringSessionValidationScheduler sessionValidationScheduler = new SpringSessionValidationScheduler(); // 相隔多久检查一次session的有效性,单位毫秒,默认就是10分钟 - sessionValidationScheduler.setSessionValidationInterval(validationInterval * 60 * 1000); + sessionValidationScheduler.setSessionValidationInterval(validationInterval * 60 * 4000); // 设置会话验证调度器进行会话验证时的会话管理器 sessionValidationScheduler.setSessionManager(sessionValidationManager()); return sessionValidationScheduler; @@ -148,7 +148,7 @@ public class ShiroConfig { // 删除过期的session manager.setDeleteInvalidSessions(true); // 设置全局session超时时间 - manager.setGlobalSessionTimeout(expireTime * 60 * 1000); + manager.setGlobalSessionTimeout(expireTime * 60 * 4000); // 去掉 JSESSIONID manager.setSessionIdUrlRewritingEnabled(false); // 是否定时检查session @@ -171,7 +171,7 @@ public class ShiroConfig { // 删除过期的session manager.setDeleteInvalidSessions(true); // 设置全局session超时时间 - manager.setGlobalSessionTimeout(expireTime * 60 * 1000); + manager.setGlobalSessionTimeout(expireTime * 60 * 4000); // 去掉 JSESSIONID manager.setSessionIdUrlRewritingEnabled(false); // 定义要使用的无效的Session定时调度器 @@ -288,7 +288,7 @@ public class ShiroConfig { //filterChainDefinitionMap.put("/mobile/inventory/completeTaskListByWMS", "anon"); //filterChainDefinitionMap.put("/receipt/receiving/saveBatch", "anon"); //filterChainDefinitionMap.put("/config/zone/getAllFlatLocation", "anon"); - //filterChainDefinitionMap.put("/mobile/", "anon"); + //filterChainDefinitionMap.put("/mobile/getModules2", "anon"); // 系统权限列表 @@ -337,7 +337,7 @@ public class ShiroConfig { cookie.setDomain(domain); cookie.setPath(path); cookie.setHttpOnly(httpOnly); - cookie.setMaxAge(maxAge * 24 * 60 * 60); + cookie.setMaxAge(maxAge * 24 * 60 * 240); return cookie; } diff --git a/src/main/java/com/huaheng/pc/task/taskHeader/service/ReceiptTaskService.java b/src/main/java/com/huaheng/pc/task/taskHeader/service/ReceiptTaskService.java index e2fe2fe..9717d98 100644 --- a/src/main/java/com/huaheng/pc/task/taskHeader/service/ReceiptTaskService.java +++ b/src/main/java/com/huaheng/pc/task/taskHeader/service/ReceiptTaskService.java @@ -531,6 +531,9 @@ public class ReceiptTaskService { .eq(InventoryDetail::getReceiptDetailId, taskDetail.getBillDetailId()); InventoryDetail inventoryDetail = inventoryDetailService.getOne(inventory); Material material = materialService.getMaterialByCode(receiptDetail.getMaterialCode(), warehouseCode); + if (material == null) { + throw new ServiceException("物料不存在," + receiptDetail.getMaterialCode()); + } /*单位换算*/ BigDecimal receiptQty = taskDetail.getQty(); if (StringUtils.isNotEmpty(receiptDetail.getMaterialUnit()) && StringUtils.isNotEmpty(material.getUnit()) && !receiptDetail.getMaterialUnit().equals(material.getUnit())) { diff --git a/src/main/java/com/huaheng/pc/task/taskHeader/service/ShipmentTaskService.java b/src/main/java/com/huaheng/pc/task/taskHeader/service/ShipmentTaskService.java index 1356529..4c63755 100644 --- a/src/main/java/com/huaheng/pc/task/taskHeader/service/ShipmentTaskService.java +++ b/src/main/java/com/huaheng/pc/task/taskHeader/service/ShipmentTaskService.java @@ -46,6 +46,9 @@ import com.huaheng.pc.task.taskHeader.domain.ShipmentTaskCreateModel; import com.huaheng.pc.task.taskHeader.domain.TaskHeader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -53,6 +56,9 @@ import javax.annotation.Resource; import javax.validation.constraints.NotNull; import java.math.BigDecimal; import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; /** * 入库任务创建和完成 @@ -102,6 +108,9 @@ public class ShipmentTaskService { @Resource private StationService stationService; + @Resource + private RedisTemplate<String, Object> redisTemplate; + /** * 创建出库任务 * @@ -498,46 +507,110 @@ public class ShipmentTaskService { //修改出库单状态 List<TaskDetail> taskDetailList = taskDetailService.findByTaskId(task.getId()); - HashSet<Integer> ids = new HashSet<>(); + // 使用 Set 存储 billDetailId 和 shipmentId,避免重复查询 + Set<Integer> billDetailIds = new HashSet<>(); + Set<Integer> shipmentIds = new HashSet<>(); for (TaskDetail taskDetail : taskDetailList) { - ShipmentDetail shipmentDetail = shipmentDetailService.getById(taskDetail.getBillDetailId()); - if (StringUtils.isNotNull(shipmentDetail)) { + billDetailIds.add(taskDetail.getBillDetailId()); + } + + // 批量查询出库单明细 + Map<Integer, ShipmentDetail> shipmentDetailMap = shipmentDetailService.listByIds(billDetailIds) + .stream().collect(Collectors.toMap(ShipmentDetail::getId, Function.identity())); + + // 添加 shipmentId 到 shipmentIds 中 + for (TaskDetail taskDetail : taskDetailList) { + ShipmentDetail shipmentDetail = shipmentDetailMap.get(taskDetail.getBillDetailId()); + if (shipmentDetail != null) { + shipmentIds.add(shipmentDetail.getShipmentId()); + } + } + + // 批量查询出库单头信息 + Map<Integer, ShipmentHeader> shipmentHeaderMap = shipmentHeaderService.listByIds(shipmentIds) + .stream().collect(Collectors.toMap(ShipmentHeader::getId, Function.identity())); + + for (TaskDetail taskDetail : taskDetailList) { + ShipmentDetail shipmentDetail = shipmentDetailMap.get(taskDetail.getBillDetailId()); + if (shipmentDetail != null) { if (shipmentDetail.getQty().compareTo(shipmentDetail.getTaskQty()) == 0) { - //一条单据明细可能有多条组盘多条任务 - List<ShipmentContainerDetail> list = shipmentContainerDetailService.list(new LambdaQueryWrapper<ShipmentContainerDetail>() - .eq(ShipmentContainerDetail::getShipmentDetailId, shipmentDetail.getId())); - boolean flag = true; - for (ShipmentContainerDetail shipmentContainerDetail : list) { - if (shipmentContainerDetail.getStatus() != 20) { - flag = false; - break; - } - } - if (flag) { + // 一条单据明细,可能有多条组盘多条任务 + List<ShipmentContainerDetail> list = shipmentContainerDetailService.list( + new LambdaQueryWrapper<ShipmentContainerDetail>().eq(ShipmentContainerDetail::getShipmentDetailId, shipmentDetail.getId())); + boolean allCompleted = list.stream().allMatch(detail -> detail.getStatus() == 20); + + if (allCompleted) { shipmentDetail.setStatus(QuantityConstant.SHIPMENT_HEADER_COMPLETED); } else { shipmentDetail.setStatus(QuantityConstant.SHIPMENT_HEADER_GROUPDISK); } - shipmentDetailService.updateById(shipmentDetail); + + // 使用 Redis 处理并发更新 shipmentDetail + updateShipmentDetailWithRedis(shipmentDetail); + } + // 更新出库单头信息 + updateShipmentHeaderStatus(shipmentDetail.getShipmentId(), shipmentHeaderMap); + } + } + //删除自建单据物料 + for (String materialCode : materialCodes) { + List<InventoryDetail> list = inventoryDetailService.list(new LambdaUpdateWrapper<InventoryDetail>().eq(InventoryDetail::getMaterialCode, materialCode)); + if (list.isEmpty()) { + Material material = materialService.getMaterialByCode(materialCode, "CS0001"); + if (material != null && material.getSelfCreated()) { + materialService.removeById(material); } - ids.add(shipmentDetail.getShipmentId()); } } - /* 更新出库单状态 */ - for (Integer id : ids) { - ShipmentHeader shipmentHeader = shipmentHeaderService.getById(id); - if (shipmentHeader != null) { + return AjaxResult.success("完成出库任务成功"); + } + + // 使用 Redis 处理并发更新 shipmentDetail + private void updateShipmentDetailWithRedis(ShipmentDetail shipmentDetail) { + String lockKey = "shipmentDetailLock:" + shipmentDetail.getId(); // Redis 锁的 key + ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue(); + Boolean lockAcquired = valueOperations.setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS); + + if (lockAcquired != null && lockAcquired) { + try { + // 获取锁成功,进行更新操作 + shipmentDetailService.updateById(shipmentDetail); // 直接更新数据库 + } finally { + redisTemplate.delete(lockKey); // 释放锁 + } + } else { + // 获取锁失败,可以考虑重试或其他处理 + throw new ServiceException("更新出库单明细失败,请稍后再试"); + } + } + + // 更新出库单头信息 + private void updateShipmentHeaderStatus(Integer shipmentId, Map<Integer, ShipmentHeader> shipmentHeaderMap) { + // 构造 Redis 锁的 key + String lockKey = "shipmentHeaderLock:" + shipmentId; + // 获取 Redis 操作对象 + ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue(); + + // 尝试获取锁 + Boolean lockAcquired = valueOperations.setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS); + + if (lockAcquired != null && lockAcquired) { + try { + ShipmentHeader shipmentHeader = shipmentHeaderMap.get(shipmentId); + if (shipmentHeader == null) { + return; + } + + // 获取状态信息并更新出库单头逻辑 Map<String, Integer> status = shipmentDetailService.selectStatus(shipmentHeader.getId()); Integer maxStatus = status.get("maxStatus"); Integer minStatus = status.get("minStatus"); // 检查 maxStatus 和 minStatus 是否不为空 if (maxStatus != null && minStatus != null) { - boolean isStatusCompleted = QuantityConstant.SHIPMENT_HEADER_COMPLETED.equals(maxStatus); - - if (isStatusCompleted) { + if (QuantityConstant.SHIPMENT_HEADER_COMPLETED.equals(maxStatus)) { shipmentHeader.setFirstStatus(QuantityConstant.SHIPMENT_HEADER_COMPLETED); } @@ -548,26 +621,22 @@ public class ShipmentTaskService { } } shipmentHeader.setLastUpdated(new Date()); + + // 使用乐观锁更新出库单头信息 if (!shipmentHeaderService.updateById(shipmentHeader)) { - throw new ServiceException("更新入库单头表失败"); + throw new ServiceException("更新出库单头表失败"); } + } finally { + // 释放锁 + redisTemplate.delete(lockKey); } + } else { + // 获取锁失败,可以考虑重试或者其他处理方式 + throw new ServiceException("获取锁失败,请稍后再试"); } - //删除自建单据物料 - for (String materialCode : materialCodes) { - List<InventoryDetail> list = inventoryDetailService.list(new LambdaUpdateWrapper<InventoryDetail>().eq(InventoryDetail::getMaterialCode, materialCode)); - if (list.isEmpty()) { - Material material = materialService.getMaterialByCode(materialCode, "CS0001"); - if (material != null && material.getSelfCreated()) { - materialService.removeById(material); - } - } - - } - - return AjaxResult.success("完成出库任务成功"); } + /** * 出库任务完成时更新库位和容器状态 * -- libgit2 0.22.2