diff --git a/mall-oms/oms-boot/src/main/java/com/youlai/mall/oms/service/app/impl/OrderServiceImpl.java b/mall-oms/oms-boot/src/main/java/com/youlai/mall/oms/service/app/impl/OrderServiceImpl.java index 77ba6ace5..ed28775bb 100644 --- a/mall-oms/oms-boot/src/main/java/com/youlai/mall/oms/service/app/impl/OrderServiceImpl.java +++ b/mall-oms/oms-boot/src/main/java/com/youlai/mall/oms/service/app/impl/OrderServiceImpl.java @@ -44,7 +44,7 @@ import com.youlai.mall.oms.service.app.CartService; import com.youlai.mall.oms.service.app.OrderItemService; import com.youlai.mall.oms.service.app.OrderService; import com.youlai.mall.pms.api.SkuFeignClient; -import com.youlai.mall.pms.model.dto.LockedSkuDTO; +import com.youlai.mall.pms.model.dto.LockSkuDTO; import com.youlai.mall.pms.model.dto.SkuInfoDTO; import com.youlai.mall.ums.api.MemberFeignClient; import com.youlai.mall.ums.dto.MemberAddressDTO; @@ -199,11 +199,11 @@ public class OrderServiceImpl extends ServiceImpl impleme } // 3. 校验库存并锁定库存 - List lockedSkuList = orderItems.stream() - .map(item -> new LockedSkuDTO(item.getSkuId(), item.getQuantity(), item.getSkuSn())) + List lockSkuList = orderItems.stream() + .map(item -> new LockSkuDTO(item.getSkuId(), item.getQuantity())) .collect(Collectors.toList()); - boolean lockStockResult = skuFeignClient.lockStock(orderToken, lockedSkuList); + boolean lockStockResult = skuFeignClient.lockStock(orderToken, lockSkuList); Assert.isTrue(lockStockResult, "订单提交失败:锁定商品库存失败!"); // 4. 生成订单 diff --git a/mall-oms/oms-boot/src/test/java/com/youlai/mall/oms/controller/OrderControllerTests.java b/mall-oms/oms-boot/src/test/java/com/youlai/mall/oms/controller/OrderControllerTest.java similarity index 99% rename from mall-oms/oms-boot/src/test/java/com/youlai/mall/oms/controller/OrderControllerTests.java rename to mall-oms/oms-boot/src/test/java/com/youlai/mall/oms/controller/OrderControllerTest.java index f3cad1e43..37029c135 100644 --- a/mall-oms/oms-boot/src/test/java/com/youlai/mall/oms/controller/OrderControllerTests.java +++ b/mall-oms/oms-boot/src/test/java/com/youlai/mall/oms/controller/OrderControllerTest.java @@ -38,7 +38,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @SpringBootTest @AutoConfigureMockMvc @Slf4j -public class OrderControllerTests { +public class OrderControllerTest { @Autowired diff --git a/mall-pms/pms-api/src/main/java/com/youlai/mall/pms/api/SkuFeignClient.java b/mall-pms/pms-api/src/main/java/com/youlai/mall/pms/api/SkuFeignClient.java index b97c2e775..2d81ead34 100644 --- a/mall-pms/pms-api/src/main/java/com/youlai/mall/pms/api/SkuFeignClient.java +++ b/mall-pms/pms-api/src/main/java/com/youlai/mall/pms/api/SkuFeignClient.java @@ -1,7 +1,7 @@ package com.youlai.mall.pms.api; import com.youlai.common.web.config.FeignDecoderConfig; -import com.youlai.mall.pms.model.dto.LockedSkuDTO; +import com.youlai.mall.pms.model.dto.LockSkuDTO; import com.youlai.mall.pms.model.dto.SkuInfoDTO; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.*; @@ -31,7 +31,7 @@ public interface SkuFeignClient { * 锁定商品库存 */ @PutMapping("/app-api/v1/skus/lock") - boolean lockStock(@RequestParam String orderToken, @RequestBody List lockedSkuList); + boolean lockStock(@RequestParam String orderToken, @RequestBody List lockSkuList); /** * 解锁商品库存 diff --git a/mall-pms/pms-api/src/main/java/com/youlai/mall/pms/model/dto/LockedSkuDTO.java b/mall-pms/pms-api/src/main/java/com/youlai/mall/pms/model/dto/LockSkuDTO.java similarity index 80% rename from mall-pms/pms-api/src/main/java/com/youlai/mall/pms/model/dto/LockedSkuDTO.java rename to mall-pms/pms-api/src/main/java/com/youlai/mall/pms/model/dto/LockSkuDTO.java index b37d55db6..2f510f962 100644 --- a/mall-pms/pms-api/src/main/java/com/youlai/mall/pms/model/dto/LockedSkuDTO.java +++ b/mall-pms/pms-api/src/main/java/com/youlai/mall/pms/model/dto/LockSkuDTO.java @@ -14,7 +14,7 @@ import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor -public class LockedSkuDTO { +public class LockSkuDTO { /** * 商品ID @@ -27,11 +27,4 @@ public class LockedSkuDTO { private Integer quantity; - /** - * 商品编码 - */ - private String skuSn; - - - } diff --git a/mall-pms/pms-boot/src/main/java/com/youlai/mall/pms/controller/app/SkuController.java b/mall-pms/pms-boot/src/main/java/com/youlai/mall/pms/controller/app/SkuController.java index 76f2bd028..2fc85bd44 100644 --- a/mall-pms/pms-boot/src/main/java/com/youlai/mall/pms/controller/app/SkuController.java +++ b/mall-pms/pms-boot/src/main/java/com/youlai/mall/pms/controller/app/SkuController.java @@ -1,7 +1,7 @@ package com.youlai.mall.pms.controller.app; import com.youlai.common.result.Result; -import com.youlai.mall.pms.model.dto.LockedSkuDTO; +import com.youlai.mall.pms.model.dto.LockSkuDTO; import com.youlai.mall.pms.model.dto.SkuInfoDTO; import com.youlai.mall.pms.service.SkuService; import io.swagger.v3.oas.annotations.tags.Tag; @@ -48,9 +48,9 @@ public class SkuController { @PutMapping("/lock") public Result lockStock( @RequestParam String orderToken, - @RequestBody List lockedSkuList + @RequestBody List lockSkuList ) { - boolean lockStockResult = skuService.lockStock(orderToken,lockedSkuList); + boolean lockStockResult = skuService.lockStock(orderToken,lockSkuList); return Result.success(lockStockResult); } diff --git a/mall-pms/pms-boot/src/main/java/com/youlai/mall/pms/service/SkuService.java b/mall-pms/pms-boot/src/main/java/com/youlai/mall/pms/service/SkuService.java index a276cfd22..762e0bac1 100644 --- a/mall-pms/pms-boot/src/main/java/com/youlai/mall/pms/service/SkuService.java +++ b/mall-pms/pms-boot/src/main/java/com/youlai/mall/pms/service/SkuService.java @@ -1,7 +1,7 @@ package com.youlai.mall.pms.service; import com.baomidou.mybatisplus.extension.service.IService; -import com.youlai.mall.pms.model.dto.LockedSkuDTO; +import com.youlai.mall.pms.model.dto.LockSkuDTO; import com.youlai.mall.pms.model.dto.SkuInfoDTO; import com.youlai.mall.pms.model.entity.PmsSku; @@ -36,10 +36,10 @@ public interface SkuService extends IService { * 校验并锁定库存 * * @param orderToken 订单临时编号 (此时订单未创建) - * @param lockedSkuList 锁定商品库存信息列表 + * @param lockSkuList 锁定商品库存信息列表 * @return true/false */ - boolean lockStock(String orderToken,List lockedSkuList); + boolean lockStock(String orderToken,List lockSkuList); /** * 解锁库存 diff --git a/mall-pms/pms-boot/src/main/java/com/youlai/mall/pms/service/impl/SkuServiceImpl.java b/mall-pms/pms-boot/src/main/java/com/youlai/mall/pms/service/impl/SkuServiceImpl.java index 4e1916d1b..04add8633 100644 --- a/mall-pms/pms-boot/src/main/java/com/youlai/mall/pms/service/impl/SkuServiceImpl.java +++ b/mall-pms/pms-boot/src/main/java/com/youlai/mall/pms/service/impl/SkuServiceImpl.java @@ -9,14 +9,12 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.youlai.mall.pms.constant.ProductConstants; import com.youlai.mall.pms.converter.SkuConverter; import com.youlai.mall.pms.mapper.PmsSkuMapper; -import com.youlai.mall.pms.model.dto.LockedSkuDTO; +import com.youlai.mall.pms.model.dto.LockSkuDTO; import com.youlai.mall.pms.model.dto.SkuInfoDTO; import com.youlai.mall.pms.model.entity.PmsSku; import com.youlai.mall.pms.service.SkuService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.redisson.api.RLock; -import org.redisson.api.RedissonClient; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -35,7 +33,6 @@ import java.util.List; public class SkuServiceImpl extends ServiceImpl implements SkuService { private final RedisTemplate redisTemplate; - private final RedissonClient redissonClient; private final SkuConverter skuConverter; @@ -65,39 +62,30 @@ public class SkuServiceImpl extends ServiceImpl implements /** * 校验并锁定库存 * - * @param orderToken 订单临时编号 (此时订单未创建) - * @param lockedSkuList 锁定商品库存信息列表 + * @param orderToken 订单临时编号 (此时订单未创建) + * @param lockSkuList 锁定商品库存列表 * @return true/false */ @Override @Transactional - public boolean lockStock(String orderToken, List lockedSkuList) { - Assert.isTrue(CollectionUtil.isNotEmpty(lockedSkuList), "订单({})未包含任何商品", orderToken); + public boolean lockStock(String orderToken, List lockSkuList) { + log.info("订单({})锁定商品库存:{}", orderToken, JSONUtil.toJsonStr(lockSkuList)); + Assert.isTrue(CollectionUtil.isNotEmpty(lockSkuList), "订单({})未包含任何商品", orderToken); // 校验库存数量是否足够以及锁定库存 - for (LockedSkuDTO lockedSku : lockedSkuList) { - Long skuId = lockedSku.getSkuId(); - RLock lock = redissonClient.getLock(ProductConstants.SKU_LOCK_PREFIX + skuId); // 构建商品锁对象 - try { - lock.lock(); - - Integer quantity = lockedSku.getQuantity(); // 订单的商品数量 - // 库存足够 - boolean lockResult = this.update(new LambdaUpdateWrapper() - .setSql("locked_stock = locked_stock + " + quantity) // 修改锁定商品数 - .eq(PmsSku::getId, lockedSku.getSkuId()) - .apply("stock - locked_stock >= {0}", quantity) // 剩余商品数 ≥ 订单商品数 - ); - Assert.isTrue(lockResult, "商品({})库存不足", lockedSku.getSkuSn()); - } finally { - if (lock.isLocked()) { - lock.unlock(); - } - } + for (LockSkuDTO lockedSku : lockSkuList) { + Integer quantity = lockedSku.getQuantity(); // 订单的商品数量 + // 库存足够 + boolean lockResult = this.update(new LambdaUpdateWrapper() + .setSql("locked_stock = locked_stock + " + quantity) // 修改锁定商品数 + .eq(PmsSku::getId, lockedSku.getSkuId()) + .apply("stock - locked_stock >= {0}", quantity) // 剩余商品数 ≥ 订单商品数 + ); + Assert.isTrue(lockResult, "商品库存不足"); } // 锁定的商品缓存至 Redis (后续使用:1.取消订单解锁库存;2:支付订单扣减库存) - redisTemplate.opsForValue().set(ProductConstants.LOCKED_SKUS_PREFIX + orderToken, lockedSkuList); + redisTemplate.opsForValue().set(ProductConstants.LOCKED_SKUS_PREFIX + orderToken, lockSkuList); return true; } @@ -110,8 +98,9 @@ public class SkuServiceImpl extends ServiceImpl implements * @return true/false */ @Override + @Transactional public boolean unlockStock(String orderSn) { - List lockedSkus = (List) redisTemplate.opsForValue().get(ProductConstants.LOCKED_SKUS_PREFIX + orderSn); + List lockedSkus = (List) redisTemplate.opsForValue().get(ProductConstants.LOCKED_SKUS_PREFIX + orderSn); log.info("释放订单({})锁定的商品库存:{}", orderSn, JSONUtil.toJsonStr(lockedSkus)); // 库存已释放 @@ -119,20 +108,13 @@ public class SkuServiceImpl extends ServiceImpl implements return true; } - // 遍历恢复锁定的商品库存 - for (LockedSkuDTO lockedSku : lockedSkus) { - RLock lock = redissonClient.getLock(ProductConstants.SKU_LOCK_PREFIX + lockedSku.getSkuId()); // 获取商品分布式锁 - try { - lock.lock(); - this.update(new LambdaUpdateWrapper() - .setSql("locked_stock = locked_stock - " + lockedSku.getQuantity()) - .eq(PmsSku::getId, lockedSku.getSkuId()) - ); - } finally { - if (lock.isLocked()) { - lock.unlock(); - } - } + // 解锁商品库存 + for (LockSkuDTO lockedSku : lockedSkus) { + boolean unlockResult = this.update(new LambdaUpdateWrapper() + .setSql("locked_stock = locked_stock - " + lockedSku.getQuantity()) + .eq(PmsSku::getId, lockedSku.getSkuId()) + ); + Assert.isTrue(unlockResult, "解锁商品库存失败"); } // 移除 redis 订单锁定的商品 redisTemplate.delete(ProductConstants.LOCKED_SKUS_PREFIX + orderSn); @@ -148,29 +130,21 @@ public class SkuServiceImpl extends ServiceImpl implements * @return ture/false */ @Override + @Transactional public boolean deductStock(String orderSn) { // 获取订单提交时锁定的商品 - List lockedSkus = (List) redisTemplate.opsForValue().get(ProductConstants.LOCKED_SKUS_PREFIX + orderSn); + List lockedSkus = (List) redisTemplate.opsForValue().get(ProductConstants.LOCKED_SKUS_PREFIX + orderSn); log.info("订单({})支付成功,扣减订单商品库存:{}", orderSn, JSONUtil.toJsonStr(lockedSkus)); Assert.isTrue(CollectionUtil.isNotEmpty(lockedSkus), "扣减商品库存失败:订单({})未包含商品"); - for (LockedSkuDTO lockedSku : lockedSkus) { - - RLock lock = redissonClient.getLock(ProductConstants.SKU_LOCK_PREFIX + lockedSku.getSkuId()); // 获取商品分布式锁 - - try { - lock.lock(); - this.update(new LambdaUpdateWrapper() - .setSql("stock = stock - " + lockedSku.getQuantity()) - .setSql("locked_stock = locked_stock - " + lockedSku.getQuantity()) - .eq(PmsSku::getId, lockedSku.getSkuId()) - ); - } finally { - if (lock.isLocked()) { - lock.unlock(); - } - } + for (LockSkuDTO lockedSku : lockedSkus) { + boolean deductResult = this.update(new LambdaUpdateWrapper() + .setSql("stock = stock - " + lockedSku.getQuantity()) + .setSql("locked_stock = locked_stock - " + lockedSku.getQuantity()) + .eq(PmsSku::getId, lockedSku.getSkuId()) + ); + Assert.isTrue(deductResult, "扣减商品库存失败"); } // 移除订单锁定的商品 diff --git a/mall-pms/pms-boot/src/test/java/com/youlai/mall/pms/controller/app/SkuControllerTest.java b/mall-pms/pms-boot/src/test/java/com/youlai/mall/pms/controller/app/SkuControllerTest.java new file mode 100644 index 000000000..88bc2feb6 --- /dev/null +++ b/mall-pms/pms-boot/src/test/java/com/youlai/mall/pms/controller/app/SkuControllerTest.java @@ -0,0 +1,20 @@ +package com.youlai.mall.pms.controller.app; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SkuControllerTest { + + @Test + void lockStock() { + } + + @Test + void unlockStock() { + } + + @Test + void deductStock() { + } +} \ No newline at end of file diff --git a/mall-pms/pms-boot/src/test/java/com/youlai/mall/pms/service/impl/SkuServiceImplTest.java b/mall-pms/pms-boot/src/test/java/com/youlai/mall/pms/service/impl/SkuServiceImplTest.java new file mode 100644 index 000000000..09a3ea1e5 --- /dev/null +++ b/mall-pms/pms-boot/src/test/java/com/youlai/mall/pms/service/impl/SkuServiceImplTest.java @@ -0,0 +1,63 @@ +package com.youlai.mall.pms.service.impl; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import com.youlai.mall.pms.model.dto.LockSkuDTO; +import com.youlai.mall.pms.service.SkuService; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@Slf4j +class SkuServiceImplTest { + + @Autowired + SkuService skuService; + + /** + * 模拟并发锁定库存 + */ + @Test + void lockStock() throws InterruptedException { + TimeInterval timer = DateUtil.timer(); + + int numberOfThreads = 50; + CountDownLatch countDownLatch = new CountDownLatch(numberOfThreads); + ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads); + + List lockedSkuList = Arrays.asList(new LockSkuDTO(1L, 1)); + + for (int i = 0; i < numberOfThreads; i++) { + executorService.submit(() -> { + try { + skuService.lockStock("20231122000001", lockedSkuList); + } finally { + countDownLatch.countDown(); + } + }); + } + + countDownLatch.await(); // Wait for all threads to finish + executorService.shutdown(); + log.info("锁定商品库存耗时:{}ms", timer.interval()); + } + + + @Test + void unlockStock() { + } + + @Test + void deductStock() { + } +} \ No newline at end of file