Commit d09efe6812f01d770e7f125f365459da25704d0d
1 parent
af09cffd
feat:并发问题:如果多个线程同时修改同一个shipmentDetail的状态,可能会导致数据不一致,用redis锁解决
Showing
3 changed files
with
114 additions
and
42 deletions
src/main/java/com/huaheng/framework/config/ShiroConfig.java
... | ... | @@ -131,7 +131,7 @@ public class ShiroConfig { |
131 | 131 | public SpringSessionValidationScheduler sessionValidationScheduler() { |
132 | 132 | SpringSessionValidationScheduler sessionValidationScheduler = new SpringSessionValidationScheduler(); |
133 | 133 | // 相隔多久检查一次session的有效性,单位毫秒,默认就是10分钟 |
134 | - sessionValidationScheduler.setSessionValidationInterval(validationInterval * 60 * 1000); | |
134 | + sessionValidationScheduler.setSessionValidationInterval(validationInterval * 60 * 4000); | |
135 | 135 | // 设置会话验证调度器进行会话验证时的会话管理器 |
136 | 136 | sessionValidationScheduler.setSessionManager(sessionValidationManager()); |
137 | 137 | return sessionValidationScheduler; |
... | ... | @@ -148,7 +148,7 @@ public class ShiroConfig { |
148 | 148 | // 删除过期的session |
149 | 149 | manager.setDeleteInvalidSessions(true); |
150 | 150 | // 设置全局session超时时间 |
151 | - manager.setGlobalSessionTimeout(expireTime * 60 * 1000); | |
151 | + manager.setGlobalSessionTimeout(expireTime * 60 * 4000); | |
152 | 152 | // 去掉 JSESSIONID |
153 | 153 | manager.setSessionIdUrlRewritingEnabled(false); |
154 | 154 | // 是否定时检查session |
... | ... | @@ -171,7 +171,7 @@ public class ShiroConfig { |
171 | 171 | // 删除过期的session |
172 | 172 | manager.setDeleteInvalidSessions(true); |
173 | 173 | // 设置全局session超时时间 |
174 | - manager.setGlobalSessionTimeout(expireTime * 60 * 1000); | |
174 | + manager.setGlobalSessionTimeout(expireTime * 60 * 4000); | |
175 | 175 | // 去掉 JSESSIONID |
176 | 176 | manager.setSessionIdUrlRewritingEnabled(false); |
177 | 177 | // 定义要使用的无效的Session定时调度器 |
... | ... | @@ -288,7 +288,7 @@ public class ShiroConfig { |
288 | 288 | //filterChainDefinitionMap.put("/mobile/inventory/completeTaskListByWMS", "anon"); |
289 | 289 | //filterChainDefinitionMap.put("/receipt/receiving/saveBatch", "anon"); |
290 | 290 | //filterChainDefinitionMap.put("/config/zone/getAllFlatLocation", "anon"); |
291 | - //filterChainDefinitionMap.put("/mobile/", "anon"); | |
291 | + //filterChainDefinitionMap.put("/mobile/getModules2", "anon"); | |
292 | 292 | |
293 | 293 | |
294 | 294 | // 系统权限列表 |
... | ... | @@ -337,7 +337,7 @@ public class ShiroConfig { |
337 | 337 | cookie.setDomain(domain); |
338 | 338 | cookie.setPath(path); |
339 | 339 | cookie.setHttpOnly(httpOnly); |
340 | - cookie.setMaxAge(maxAge * 24 * 60 * 60); | |
340 | + cookie.setMaxAge(maxAge * 24 * 60 * 240); | |
341 | 341 | return cookie; |
342 | 342 | } |
343 | 343 | |
... | ... |
src/main/java/com/huaheng/pc/task/taskHeader/service/ReceiptTaskService.java
... | ... | @@ -531,6 +531,9 @@ public class ReceiptTaskService { |
531 | 531 | .eq(InventoryDetail::getReceiptDetailId, taskDetail.getBillDetailId()); |
532 | 532 | InventoryDetail inventoryDetail = inventoryDetailService.getOne(inventory); |
533 | 533 | Material material = materialService.getMaterialByCode(receiptDetail.getMaterialCode(), warehouseCode); |
534 | + if (material == null) { | |
535 | + throw new ServiceException("物料不存在," + receiptDetail.getMaterialCode()); | |
536 | + } | |
534 | 537 | /*单位换算*/ |
535 | 538 | BigDecimal receiptQty = taskDetail.getQty(); |
536 | 539 | if (StringUtils.isNotEmpty(receiptDetail.getMaterialUnit()) && StringUtils.isNotEmpty(material.getUnit()) && !receiptDetail.getMaterialUnit().equals(material.getUnit())) { |
... | ... |
src/main/java/com/huaheng/pc/task/taskHeader/service/ShipmentTaskService.java
... | ... | @@ -46,6 +46,9 @@ import com.huaheng.pc.task.taskHeader.domain.ShipmentTaskCreateModel; |
46 | 46 | import com.huaheng.pc.task.taskHeader.domain.TaskHeader; |
47 | 47 | import org.slf4j.Logger; |
48 | 48 | import org.slf4j.LoggerFactory; |
49 | +import org.springframework.beans.factory.annotation.Autowired; | |
50 | +import org.springframework.data.redis.core.RedisTemplate; | |
51 | +import org.springframework.data.redis.core.ValueOperations; | |
49 | 52 | import org.springframework.stereotype.Service; |
50 | 53 | import org.springframework.transaction.annotation.Transactional; |
51 | 54 | |
... | ... | @@ -53,6 +56,9 @@ import javax.annotation.Resource; |
53 | 56 | import javax.validation.constraints.NotNull; |
54 | 57 | import java.math.BigDecimal; |
55 | 58 | import java.util.*; |
59 | +import java.util.concurrent.TimeUnit; | |
60 | +import java.util.function.Function; | |
61 | +import java.util.stream.Collectors; | |
56 | 62 | |
57 | 63 | /** |
58 | 64 | * 入库任务创建和完成 |
... | ... | @@ -102,6 +108,9 @@ public class ShipmentTaskService { |
102 | 108 | @Resource |
103 | 109 | private StationService stationService; |
104 | 110 | |
111 | + @Resource | |
112 | + private RedisTemplate<String, Object> redisTemplate; | |
113 | + | |
105 | 114 | /** |
106 | 115 | * 创建出库任务 |
107 | 116 | * |
... | ... | @@ -498,46 +507,110 @@ public class ShipmentTaskService { |
498 | 507 | |
499 | 508 | //修改出库单状态 |
500 | 509 | List<TaskDetail> taskDetailList = taskDetailService.findByTaskId(task.getId()); |
501 | - HashSet<Integer> ids = new HashSet<>(); | |
510 | + // 使用 Set 存储 billDetailId 和 shipmentId,避免重复查询 | |
511 | + Set<Integer> billDetailIds = new HashSet<>(); | |
512 | + Set<Integer> shipmentIds = new HashSet<>(); | |
502 | 513 | for (TaskDetail taskDetail : taskDetailList) { |
503 | - ShipmentDetail shipmentDetail = shipmentDetailService.getById(taskDetail.getBillDetailId()); | |
504 | - if (StringUtils.isNotNull(shipmentDetail)) { | |
514 | + billDetailIds.add(taskDetail.getBillDetailId()); | |
515 | + } | |
516 | + | |
517 | + // 批量查询出库单明细 | |
518 | + Map<Integer, ShipmentDetail> shipmentDetailMap = shipmentDetailService.listByIds(billDetailIds) | |
519 | + .stream().collect(Collectors.toMap(ShipmentDetail::getId, Function.identity())); | |
520 | + | |
521 | + // 添加 shipmentId 到 shipmentIds 中 | |
522 | + for (TaskDetail taskDetail : taskDetailList) { | |
523 | + ShipmentDetail shipmentDetail = shipmentDetailMap.get(taskDetail.getBillDetailId()); | |
524 | + if (shipmentDetail != null) { | |
525 | + shipmentIds.add(shipmentDetail.getShipmentId()); | |
526 | + } | |
527 | + } | |
528 | + | |
529 | + // 批量查询出库单头信息 | |
530 | + Map<Integer, ShipmentHeader> shipmentHeaderMap = shipmentHeaderService.listByIds(shipmentIds) | |
531 | + .stream().collect(Collectors.toMap(ShipmentHeader::getId, Function.identity())); | |
532 | + | |
533 | + for (TaskDetail taskDetail : taskDetailList) { | |
534 | + ShipmentDetail shipmentDetail = shipmentDetailMap.get(taskDetail.getBillDetailId()); | |
535 | + if (shipmentDetail != null) { | |
505 | 536 | if (shipmentDetail.getQty().compareTo(shipmentDetail.getTaskQty()) == 0) { |
506 | - //一条单据明细可能有多条组盘多条任务 | |
507 | - List<ShipmentContainerDetail> list = shipmentContainerDetailService.list(new LambdaQueryWrapper<ShipmentContainerDetail>() | |
508 | - .eq(ShipmentContainerDetail::getShipmentDetailId, shipmentDetail.getId())); | |
509 | - boolean flag = true; | |
510 | - for (ShipmentContainerDetail shipmentContainerDetail : list) { | |
511 | - if (shipmentContainerDetail.getStatus() != 20) { | |
512 | - flag = false; | |
513 | - break; | |
514 | - } | |
515 | - } | |
516 | - if (flag) { | |
537 | + // 一条单据明细,可能有多条组盘多条任务 | |
538 | + List<ShipmentContainerDetail> list = shipmentContainerDetailService.list( | |
539 | + new LambdaQueryWrapper<ShipmentContainerDetail>().eq(ShipmentContainerDetail::getShipmentDetailId, shipmentDetail.getId())); | |
540 | + boolean allCompleted = list.stream().allMatch(detail -> detail.getStatus() == 20); | |
541 | + | |
542 | + if (allCompleted) { | |
517 | 543 | shipmentDetail.setStatus(QuantityConstant.SHIPMENT_HEADER_COMPLETED); |
518 | 544 | } else { |
519 | 545 | shipmentDetail.setStatus(QuantityConstant.SHIPMENT_HEADER_GROUPDISK); |
520 | 546 | } |
521 | - shipmentDetailService.updateById(shipmentDetail); | |
547 | + | |
548 | + // 使用 Redis 处理并发更新 shipmentDetail | |
549 | + updateShipmentDetailWithRedis(shipmentDetail); | |
550 | + } | |
551 | + // 更新出库单头信息 | |
552 | + updateShipmentHeaderStatus(shipmentDetail.getShipmentId(), shipmentHeaderMap); | |
553 | + } | |
554 | + } | |
555 | + //删除自建单据物料 | |
556 | + for (String materialCode : materialCodes) { | |
557 | + List<InventoryDetail> list = inventoryDetailService.list(new LambdaUpdateWrapper<InventoryDetail>().eq(InventoryDetail::getMaterialCode, materialCode)); | |
558 | + if (list.isEmpty()) { | |
559 | + Material material = materialService.getMaterialByCode(materialCode, "CS0001"); | |
560 | + if (material != null && material.getSelfCreated()) { | |
561 | + materialService.removeById(material); | |
522 | 562 | } |
523 | - ids.add(shipmentDetail.getShipmentId()); | |
524 | 563 | } |
525 | 564 | |
526 | 565 | } |
527 | 566 | |
528 | - /* 更新出库单状态 */ | |
529 | - for (Integer id : ids) { | |
530 | - ShipmentHeader shipmentHeader = shipmentHeaderService.getById(id); | |
531 | - if (shipmentHeader != null) { | |
567 | + return AjaxResult.success("完成出库任务成功"); | |
568 | + } | |
569 | + | |
570 | + // 使用 Redis 处理并发更新 shipmentDetail | |
571 | + private void updateShipmentDetailWithRedis(ShipmentDetail shipmentDetail) { | |
572 | + String lockKey = "shipmentDetailLock:" + shipmentDetail.getId(); // Redis 锁的 key | |
573 | + ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue(); | |
574 | + Boolean lockAcquired = valueOperations.setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS); | |
575 | + | |
576 | + if (lockAcquired != null && lockAcquired) { | |
577 | + try { | |
578 | + // 获取锁成功,进行更新操作 | |
579 | + shipmentDetailService.updateById(shipmentDetail); // 直接更新数据库 | |
580 | + } finally { | |
581 | + redisTemplate.delete(lockKey); // 释放锁 | |
582 | + } | |
583 | + } else { | |
584 | + // 获取锁失败,可以考虑重试或其他处理 | |
585 | + throw new ServiceException("更新出库单明细失败,请稍后再试"); | |
586 | + } | |
587 | + } | |
588 | + | |
589 | + // 更新出库单头信息 | |
590 | + private void updateShipmentHeaderStatus(Integer shipmentId, Map<Integer, ShipmentHeader> shipmentHeaderMap) { | |
591 | + // 构造 Redis 锁的 key | |
592 | + String lockKey = "shipmentHeaderLock:" + shipmentId; | |
593 | + // 获取 Redis 操作对象 | |
594 | + ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue(); | |
595 | + | |
596 | + // 尝试获取锁 | |
597 | + Boolean lockAcquired = valueOperations.setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS); | |
598 | + | |
599 | + if (lockAcquired != null && lockAcquired) { | |
600 | + try { | |
601 | + ShipmentHeader shipmentHeader = shipmentHeaderMap.get(shipmentId); | |
602 | + if (shipmentHeader == null) { | |
603 | + return; | |
604 | + } | |
605 | + | |
606 | + // 获取状态信息并更新出库单头逻辑 | |
532 | 607 | Map<String, Integer> status = shipmentDetailService.selectStatus(shipmentHeader.getId()); |
533 | 608 | Integer maxStatus = status.get("maxStatus"); |
534 | 609 | Integer minStatus = status.get("minStatus"); |
535 | 610 | |
536 | 611 | // 检查 maxStatus 和 minStatus 是否不为空 |
537 | 612 | if (maxStatus != null && minStatus != null) { |
538 | - boolean isStatusCompleted = QuantityConstant.SHIPMENT_HEADER_COMPLETED.equals(maxStatus); | |
539 | - | |
540 | - if (isStatusCompleted) { | |
613 | + if (QuantityConstant.SHIPMENT_HEADER_COMPLETED.equals(maxStatus)) { | |
541 | 614 | shipmentHeader.setFirstStatus(QuantityConstant.SHIPMENT_HEADER_COMPLETED); |
542 | 615 | } |
543 | 616 | |
... | ... | @@ -548,26 +621,22 @@ public class ShipmentTaskService { |
548 | 621 | } |
549 | 622 | } |
550 | 623 | shipmentHeader.setLastUpdated(new Date()); |
624 | + | |
625 | + // 使用乐观锁更新出库单头信息 | |
551 | 626 | if (!shipmentHeaderService.updateById(shipmentHeader)) { |
552 | - throw new ServiceException("更新入库单头表失败"); | |
627 | + throw new ServiceException("更新出库单头表失败"); | |
553 | 628 | } |
629 | + } finally { | |
630 | + // 释放锁 | |
631 | + redisTemplate.delete(lockKey); | |
554 | 632 | } |
633 | + } else { | |
634 | + // 获取锁失败,可以考虑重试或者其他处理方式 | |
635 | + throw new ServiceException("获取锁失败,请稍后再试"); | |
555 | 636 | } |
556 | - //删除自建单据物料 | |
557 | - for (String materialCode : materialCodes) { | |
558 | - List<InventoryDetail> list = inventoryDetailService.list(new LambdaUpdateWrapper<InventoryDetail>().eq(InventoryDetail::getMaterialCode, materialCode)); | |
559 | - if (list.isEmpty()) { | |
560 | - Material material = materialService.getMaterialByCode(materialCode, "CS0001"); | |
561 | - if (material != null && material.getSelfCreated()) { | |
562 | - materialService.removeById(material); | |
563 | - } | |
564 | - } | |
565 | - | |
566 | - } | |
567 | - | |
568 | - return AjaxResult.success("完成出库任务成功"); | |
569 | 637 | } |
570 | 638 | |
639 | + | |
571 | 640 | /** |
572 | 641 | * 出库任务完成时更新库位和容器状态 |
573 | 642 | * |
... | ... |