Commit 02fe48e408dfdfddc8b312b2f7109f7d5e56b42d
1 parent
4749a1e2
fix(wms): 简化库存余额并发补行处理
库存变更时,缺失库存余额行改为逐条插入;遇到唯一键冲突时回查并复用已有库存行,避免批量插入异常包装导致并发补行失败。
Showing
2 changed files
with
49 additions
and
15 deletions
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", "红富士苹果"); | ... | ... |