Commit d09efe6812f01d770e7f125f365459da25704d0d

Authored by 易文鹏
1 parent af09cffd

feat:并发问题:如果多个线程同时修改同一个shipmentDetail的状态,可能会导致数据不一致,用redis锁解决

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 *
... ...