Commit 02fe48e408dfdfddc8b312b2f7109f7d5e56b42d

Authored by YunaiV
1 parent 4749a1e2

fix(wms): 简化库存余额并发补行处理

库存变更时,缺失库存余额行改为逐条插入;遇到唯一键冲突时回查并复用已有库存行,避免批量插入异常包装导致并发补行失败。
yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImpl.java
... ... @@ -149,20 +149,6 @@ public class WmsInventoryServiceImpl implements WmsInventoryService {
149 149 }
150 150  
151 151 private List<WmsInventoryDO> createMissingInventoryList(List<WmsInventoryDO> missingInventories) {
152   - // 优先批量插入缺失库存行;并发唯一键冲突时,再逐个插入并回查冲突行。
153   - List<WmsInventoryDO> newInventories = convertList(missingInventories, missingInventory ->
154   - new WmsInventoryDO().setSkuId(missingInventory.getSkuId()).setWarehouseId(missingInventory.getWarehouseId())
155   - .setQuantity(BigDecimal.ZERO));
156   - try {
157   - inventoryMapper.insertBatch(newInventories);
158   - return newInventories;
159   - } catch (DuplicateKeyException ex) {
160   - // 并发事务可能已经补齐部分库存行,降级为逐个插入并回查唯一键冲突的行。
161   - log.warn("[createMissingInventoryList][missingInventories({}) 批量插入库存行冲突,降级为逐个插入]",
162   - missingInventories, ex);
163   - }
164   -
165   - // 批量插入失败后逐条补齐;单条唯一键冲突时,回查并使用并发事务创建的库存行。
166 152 List<WmsInventoryDO> createdInventories = new ArrayList<>(missingInventories.size());
167 153 for (WmsInventoryDO missingInventory : missingInventories) {
168 154 WmsInventoryDO inventory = new WmsInventoryDO().setSkuId(missingInventory.getSkuId())
... ... @@ -173,7 +159,7 @@ public class WmsInventoryServiceImpl implements WmsInventoryService {
173 159 } catch (DuplicateKeyException ex) {
174 160 inventory = inventoryMapper.selectBySkuIdAndWarehouseId(
175 161 missingInventory.getSkuId(), missingInventory.getWarehouseId());
176   - log.warn("[createMissingInventoryList][missingInventory({}) 插入库存行冲突,回查已有库存行]", missingInventory, ex);
  162 + log.warn("[createMissingInventoryList][missingInventory({}) 插入库存行冲突,回查已有库存行]", missingInventory);
177 163 if (inventory == null) {
178 164 throw ex;
179 165 }
... ...
yudao-module-wms/src/test/java/cn/iocoder/yudao/module/wms/service/inventory/WmsInventoryServiceImplTest.java
... ... @@ -25,10 +25,16 @@ import java.util.ArrayList;
25 25 import java.util.Collections;
26 26 import java.util.Comparator;
27 27 import java.util.List;
  28 +import java.util.concurrent.CountDownLatch;
  29 +import java.util.concurrent.ExecutorService;
  30 +import java.util.concurrent.Executors;
  31 +import java.util.concurrent.Future;
  32 +import java.util.concurrent.TimeUnit;
28 33  
29 34 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
30 35 import static cn.iocoder.yudao.module.wms.enums.ErrorCodeConstants.INVENTORY_QUANTITY_NOT_ENOUGH;
31 36 import static org.junit.jupiter.api.Assertions.assertEquals;
  37 +import static org.junit.jupiter.api.Assertions.assertTrue;
32 38 import static org.junit.jupiter.api.Assertions.assertNotNull;
33 39 import static org.junit.jupiter.api.Assertions.assertNull;
34 40 import static org.mockito.Mockito.when;
... ... @@ -220,6 +226,48 @@ public class WmsInventoryServiceImplTest extends BaseDbUnitTest {
220 226 }
221 227  
222 228 @Test
  229 + public void testChangeInventory_concurrentCreateSameInventoryOnlyOneBalance() throws Exception {
  230 + // mock 数据
  231 + WmsItemDO item = createItem("ITEM-001", "红富士苹果");
  232 + WmsItemSkuDO sku = createSku(item.getId(), "SKU-001", "10kg 箱装");
  233 + int threadCount = 4;
  234 + CountDownLatch readyLatch = new CountDownLatch(threadCount);
  235 + CountDownLatch startLatch = new CountDownLatch(1);
  236 + ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
  237 + List<Future<?>> futures = new ArrayList<>(threadCount);
  238 + for (int i = 0; i < threadCount; i++) {
  239 + futures.add(executorService.submit(() -> {
  240 + readyLatch.countDown();
  241 + try {
  242 + startLatch.await();
  243 + } catch (InterruptedException ex) {
  244 + Thread.currentThread().interrupt();
  245 + throw new IllegalStateException(ex);
  246 + }
  247 + inventoryService.changeInventory(createChangeReq(sku.getId(), 100L, "1.00"));
  248 + }));
  249 + }
  250 +
  251 + try {
  252 + // 调用
  253 + assertTrue(readyLatch.await(5, TimeUnit.SECONDS));
  254 + startLatch.countDown();
  255 + for (Future<?> future : futures) {
  256 + future.get(10, TimeUnit.SECONDS);
  257 + }
  258 + } finally {
  259 + executorService.shutdownNow();
  260 + }
  261 +
  262 + // 断言:并发补行后仍只有一条库存余额,数量累计正确
  263 + List<WmsInventoryDO> inventories = inventoryMapper.selectListByKeys(Collections.singletonList(
  264 + new WmsInventoryDO().setSkuId(sku.getId()).setWarehouseId(100L)));
  265 + assertEquals(1, inventories.size());
  266 + assertEquals(0, new BigDecimal("4.00").compareTo(inventories.get(0).getQuantity()));
  267 + assertEquals(threadCount, inventoryHistoryMapper.selectCount());
  268 + }
  269 +
  270 + @Test
223 271 public void testChangeInventory_quantityNotEnough() {
224 272 // mock 数据
225 273 WmsItemDO item = createItem("ITEM-001", "红富士苹果");
... ...