mirror of
https://gitee.com/youlaitech/youlai-mall.git
synced 2025-01-03 09:32:21 +08:00
fix: 锁定、解锁、扣减库存原子操作移除分布式锁
This commit is contained in:
parent
36879b9066
commit
075287129b
@ -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<OrderMapper, OmsOrder> impleme
|
||||
}
|
||||
|
||||
// 3. 校验库存并锁定库存
|
||||
List<LockedSkuDTO> lockedSkuList = orderItems.stream()
|
||||
.map(item -> new LockedSkuDTO(item.getSkuId(), item.getQuantity(), item.getSkuSn()))
|
||||
List<LockSkuDTO> 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. 生成订单
|
||||
|
@ -38,7 +38,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@Slf4j
|
||||
public class OrderControllerTests {
|
||||
public class OrderControllerTest {
|
||||
|
||||
|
||||
@Autowired
|
@ -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<LockedSkuDTO> lockedSkuList);
|
||||
boolean lockStock(@RequestParam String orderToken, @RequestBody List<LockSkuDTO> lockSkuList);
|
||||
|
||||
/**
|
||||
* 解锁商品库存
|
||||
|
@ -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;
|
||||
|
||||
|
||||
|
||||
}
|
@ -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<LockedSkuDTO> lockedSkuList
|
||||
@RequestBody List<LockSkuDTO> lockSkuList
|
||||
) {
|
||||
boolean lockStockResult = skuService.lockStock(orderToken,lockedSkuList);
|
||||
boolean lockStockResult = skuService.lockStock(orderToken,lockSkuList);
|
||||
return Result.success(lockStockResult);
|
||||
}
|
||||
|
||||
|
@ -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<PmsSku> {
|
||||
* 校验并锁定库存
|
||||
*
|
||||
* @param orderToken 订单临时编号 (此时订单未创建)
|
||||
* @param lockedSkuList 锁定商品库存信息列表
|
||||
* @param lockSkuList 锁定商品库存信息列表
|
||||
* @return true/false
|
||||
*/
|
||||
boolean lockStock(String orderToken,List<LockedSkuDTO> lockedSkuList);
|
||||
boolean lockStock(String orderToken,List<LockSkuDTO> lockSkuList);
|
||||
|
||||
/**
|
||||
* 解锁库存
|
||||
|
@ -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<PmsSkuMapper, PmsSku> implements SkuService {
|
||||
|
||||
private final RedisTemplate redisTemplate;
|
||||
private final RedissonClient redissonClient;
|
||||
private final SkuConverter skuConverter;
|
||||
|
||||
|
||||
@ -66,21 +63,17 @@ public class SkuServiceImpl extends ServiceImpl<PmsSkuMapper, PmsSku> implements
|
||||
* 校验并锁定库存
|
||||
*
|
||||
* @param orderToken 订单临时编号 (此时订单未创建)
|
||||
* @param lockedSkuList 锁定商品库存信息列表
|
||||
* @param lockSkuList 锁定商品库存列表
|
||||
* @return true/false
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean lockStock(String orderToken, List<LockedSkuDTO> lockedSkuList) {
|
||||
Assert.isTrue(CollectionUtil.isNotEmpty(lockedSkuList), "订单({})未包含任何商品", orderToken);
|
||||
public boolean lockStock(String orderToken, List<LockSkuDTO> 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();
|
||||
|
||||
for (LockSkuDTO lockedSku : lockSkuList) {
|
||||
Integer quantity = lockedSku.getQuantity(); // 订单的商品数量
|
||||
// 库存足够
|
||||
boolean lockResult = this.update(new LambdaUpdateWrapper<PmsSku>()
|
||||
@ -88,16 +81,11 @@ public class SkuServiceImpl extends ServiceImpl<PmsSkuMapper, PmsSku> implements
|
||||
.eq(PmsSku::getId, lockedSku.getSkuId())
|
||||
.apply("stock - locked_stock >= {0}", quantity) // 剩余商品数 ≥ 订单商品数
|
||||
);
|
||||
Assert.isTrue(lockResult, "商品({})库存不足", lockedSku.getSkuSn());
|
||||
} finally {
|
||||
if (lock.isLocked()) {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
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<PmsSkuMapper, PmsSku> implements
|
||||
* @return true/false
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean unlockStock(String orderSn) {
|
||||
List<LockedSkuDTO> lockedSkus = (List<LockedSkuDTO>) redisTemplate.opsForValue().get(ProductConstants.LOCKED_SKUS_PREFIX + orderSn);
|
||||
List<LockSkuDTO> lockedSkus = (List<LockSkuDTO>) redisTemplate.opsForValue().get(ProductConstants.LOCKED_SKUS_PREFIX + orderSn);
|
||||
log.info("释放订单({})锁定的商品库存:{}", orderSn, JSONUtil.toJsonStr(lockedSkus));
|
||||
|
||||
// 库存已释放
|
||||
@ -119,20 +108,13 @@ public class SkuServiceImpl extends ServiceImpl<PmsSkuMapper, PmsSku> implements
|
||||
return true;
|
||||
}
|
||||
|
||||
// 遍历恢复锁定的商品库存
|
||||
for (LockedSkuDTO lockedSku : lockedSkus) {
|
||||
RLock lock = redissonClient.getLock(ProductConstants.SKU_LOCK_PREFIX + lockedSku.getSkuId()); // 获取商品分布式锁
|
||||
try {
|
||||
lock.lock();
|
||||
this.update(new LambdaUpdateWrapper<PmsSku>()
|
||||
// 解锁商品库存
|
||||
for (LockSkuDTO lockedSku : lockedSkus) {
|
||||
boolean unlockResult = this.update(new LambdaUpdateWrapper<PmsSku>()
|
||||
.setSql("locked_stock = locked_stock - " + lockedSku.getQuantity())
|
||||
.eq(PmsSku::getId, lockedSku.getSkuId())
|
||||
);
|
||||
} finally {
|
||||
if (lock.isLocked()) {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
Assert.isTrue(unlockResult, "解锁商品库存失败");
|
||||
}
|
||||
// 移除 redis 订单锁定的商品
|
||||
redisTemplate.delete(ProductConstants.LOCKED_SKUS_PREFIX + orderSn);
|
||||
@ -148,29 +130,21 @@ public class SkuServiceImpl extends ServiceImpl<PmsSkuMapper, PmsSku> implements
|
||||
* @return ture/false
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean deductStock(String orderSn) {
|
||||
// 获取订单提交时锁定的商品
|
||||
List<LockedSkuDTO> lockedSkus = (List<LockedSkuDTO>) redisTemplate.opsForValue().get(ProductConstants.LOCKED_SKUS_PREFIX + orderSn);
|
||||
List<LockSkuDTO> lockedSkus = (List<LockSkuDTO>) 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<PmsSku>()
|
||||
for (LockSkuDTO lockedSku : lockedSkus) {
|
||||
boolean deductResult = this.update(new LambdaUpdateWrapper<PmsSku>()
|
||||
.setSql("stock = stock - " + lockedSku.getQuantity())
|
||||
.setSql("locked_stock = locked_stock - " + lockedSku.getQuantity())
|
||||
.eq(PmsSku::getId, lockedSku.getSkuId())
|
||||
);
|
||||
} finally {
|
||||
if (lock.isLocked()) {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
Assert.isTrue(deductResult, "扣减商品库存失败");
|
||||
}
|
||||
|
||||
// 移除订单锁定的商品
|
||||
|
@ -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() {
|
||||
}
|
||||
}
|
@ -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<LockSkuDTO> 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() {
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user