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,7 +131,7 @@ public class ShiroConfig { | ||
131 | public SpringSessionValidationScheduler sessionValidationScheduler() { | 131 | public SpringSessionValidationScheduler sessionValidationScheduler() { |
132 | SpringSessionValidationScheduler sessionValidationScheduler = new SpringSessionValidationScheduler(); | 132 | SpringSessionValidationScheduler sessionValidationScheduler = new SpringSessionValidationScheduler(); |
133 | // 相隔多久检查一次session的有效性,单位毫秒,默认就是10分钟 | 133 | // 相隔多久检查一次session的有效性,单位毫秒,默认就是10分钟 |
134 | - sessionValidationScheduler.setSessionValidationInterval(validationInterval * 60 * 1000); | 134 | + sessionValidationScheduler.setSessionValidationInterval(validationInterval * 60 * 4000); |
135 | // 设置会话验证调度器进行会话验证时的会话管理器 | 135 | // 设置会话验证调度器进行会话验证时的会话管理器 |
136 | sessionValidationScheduler.setSessionManager(sessionValidationManager()); | 136 | sessionValidationScheduler.setSessionManager(sessionValidationManager()); |
137 | return sessionValidationScheduler; | 137 | return sessionValidationScheduler; |
@@ -148,7 +148,7 @@ public class ShiroConfig { | @@ -148,7 +148,7 @@ public class ShiroConfig { | ||
148 | // 删除过期的session | 148 | // 删除过期的session |
149 | manager.setDeleteInvalidSessions(true); | 149 | manager.setDeleteInvalidSessions(true); |
150 | // 设置全局session超时时间 | 150 | // 设置全局session超时时间 |
151 | - manager.setGlobalSessionTimeout(expireTime * 60 * 1000); | 151 | + manager.setGlobalSessionTimeout(expireTime * 60 * 4000); |
152 | // 去掉 JSESSIONID | 152 | // 去掉 JSESSIONID |
153 | manager.setSessionIdUrlRewritingEnabled(false); | 153 | manager.setSessionIdUrlRewritingEnabled(false); |
154 | // 是否定时检查session | 154 | // 是否定时检查session |
@@ -171,7 +171,7 @@ public class ShiroConfig { | @@ -171,7 +171,7 @@ public class ShiroConfig { | ||
171 | // 删除过期的session | 171 | // 删除过期的session |
172 | manager.setDeleteInvalidSessions(true); | 172 | manager.setDeleteInvalidSessions(true); |
173 | // 设置全局session超时时间 | 173 | // 设置全局session超时时间 |
174 | - manager.setGlobalSessionTimeout(expireTime * 60 * 1000); | 174 | + manager.setGlobalSessionTimeout(expireTime * 60 * 4000); |
175 | // 去掉 JSESSIONID | 175 | // 去掉 JSESSIONID |
176 | manager.setSessionIdUrlRewritingEnabled(false); | 176 | manager.setSessionIdUrlRewritingEnabled(false); |
177 | // 定义要使用的无效的Session定时调度器 | 177 | // 定义要使用的无效的Session定时调度器 |
@@ -288,7 +288,7 @@ public class ShiroConfig { | @@ -288,7 +288,7 @@ public class ShiroConfig { | ||
288 | //filterChainDefinitionMap.put("/mobile/inventory/completeTaskListByWMS", "anon"); | 288 | //filterChainDefinitionMap.put("/mobile/inventory/completeTaskListByWMS", "anon"); |
289 | //filterChainDefinitionMap.put("/receipt/receiving/saveBatch", "anon"); | 289 | //filterChainDefinitionMap.put("/receipt/receiving/saveBatch", "anon"); |
290 | //filterChainDefinitionMap.put("/config/zone/getAllFlatLocation", "anon"); | 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,7 +337,7 @@ public class ShiroConfig { | ||
337 | cookie.setDomain(domain); | 337 | cookie.setDomain(domain); |
338 | cookie.setPath(path); | 338 | cookie.setPath(path); |
339 | cookie.setHttpOnly(httpOnly); | 339 | cookie.setHttpOnly(httpOnly); |
340 | - cookie.setMaxAge(maxAge * 24 * 60 * 60); | 340 | + cookie.setMaxAge(maxAge * 24 * 60 * 240); |
341 | return cookie; | 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,6 +531,9 @@ public class ReceiptTaskService { | ||
531 | .eq(InventoryDetail::getReceiptDetailId, taskDetail.getBillDetailId()); | 531 | .eq(InventoryDetail::getReceiptDetailId, taskDetail.getBillDetailId()); |
532 | InventoryDetail inventoryDetail = inventoryDetailService.getOne(inventory); | 532 | InventoryDetail inventoryDetail = inventoryDetailService.getOne(inventory); |
533 | Material material = materialService.getMaterialByCode(receiptDetail.getMaterialCode(), warehouseCode); | 533 | Material material = materialService.getMaterialByCode(receiptDetail.getMaterialCode(), warehouseCode); |
534 | + if (material == null) { | ||
535 | + throw new ServiceException("物料不存在," + receiptDetail.getMaterialCode()); | ||
536 | + } | ||
534 | /*单位换算*/ | 537 | /*单位换算*/ |
535 | BigDecimal receiptQty = taskDetail.getQty(); | 538 | BigDecimal receiptQty = taskDetail.getQty(); |
536 | if (StringUtils.isNotEmpty(receiptDetail.getMaterialUnit()) && StringUtils.isNotEmpty(material.getUnit()) && !receiptDetail.getMaterialUnit().equals(material.getUnit())) { | 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,6 +46,9 @@ import com.huaheng.pc.task.taskHeader.domain.ShipmentTaskCreateModel; | ||
46 | import com.huaheng.pc.task.taskHeader.domain.TaskHeader; | 46 | import com.huaheng.pc.task.taskHeader.domain.TaskHeader; |
47 | import org.slf4j.Logger; | 47 | import org.slf4j.Logger; |
48 | import org.slf4j.LoggerFactory; | 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 | import org.springframework.stereotype.Service; | 52 | import org.springframework.stereotype.Service; |
50 | import org.springframework.transaction.annotation.Transactional; | 53 | import org.springframework.transaction.annotation.Transactional; |
51 | 54 | ||
@@ -53,6 +56,9 @@ import javax.annotation.Resource; | @@ -53,6 +56,9 @@ import javax.annotation.Resource; | ||
53 | import javax.validation.constraints.NotNull; | 56 | import javax.validation.constraints.NotNull; |
54 | import java.math.BigDecimal; | 57 | import java.math.BigDecimal; |
55 | import java.util.*; | 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,6 +108,9 @@ public class ShipmentTaskService { | ||
102 | @Resource | 108 | @Resource |
103 | private StationService stationService; | 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,46 +507,110 @@ public class ShipmentTaskService { | ||
498 | 507 | ||
499 | //修改出库单状态 | 508 | //修改出库单状态 |
500 | List<TaskDetail> taskDetailList = taskDetailService.findByTaskId(task.getId()); | 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 | for (TaskDetail taskDetail : taskDetailList) { | 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 | if (shipmentDetail.getQty().compareTo(shipmentDetail.getTaskQty()) == 0) { | 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 | shipmentDetail.setStatus(QuantityConstant.SHIPMENT_HEADER_COMPLETED); | 543 | shipmentDetail.setStatus(QuantityConstant.SHIPMENT_HEADER_COMPLETED); |
518 | } else { | 544 | } else { |
519 | shipmentDetail.setStatus(QuantityConstant.SHIPMENT_HEADER_GROUPDISK); | 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 | Map<String, Integer> status = shipmentDetailService.selectStatus(shipmentHeader.getId()); | 607 | Map<String, Integer> status = shipmentDetailService.selectStatus(shipmentHeader.getId()); |
533 | Integer maxStatus = status.get("maxStatus"); | 608 | Integer maxStatus = status.get("maxStatus"); |
534 | Integer minStatus = status.get("minStatus"); | 609 | Integer minStatus = status.get("minStatus"); |
535 | 610 | ||
536 | // 检查 maxStatus 和 minStatus 是否不为空 | 611 | // 检查 maxStatus 和 minStatus 是否不为空 |
537 | if (maxStatus != null && minStatus != null) { | 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 | shipmentHeader.setFirstStatus(QuantityConstant.SHIPMENT_HEADER_COMPLETED); | 614 | shipmentHeader.setFirstStatus(QuantityConstant.SHIPMENT_HEADER_COMPLETED); |
542 | } | 615 | } |
543 | 616 | ||
@@ -548,26 +621,22 @@ public class ShipmentTaskService { | @@ -548,26 +621,22 @@ public class ShipmentTaskService { | ||
548 | } | 621 | } |
549 | } | 622 | } |
550 | shipmentHeader.setLastUpdated(new Date()); | 623 | shipmentHeader.setLastUpdated(new Date()); |
624 | + | ||
625 | + // 使用乐观锁更新出库单头信息 | ||
551 | if (!shipmentHeaderService.updateById(shipmentHeader)) { | 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 | * |