fix: 锁定、解锁、扣减库存原子操作移除分布式锁

This commit is contained in:
郝先瑞 2023-11-22 21:12:52 +08:00
parent 36879b9066
commit 075287129b
9 changed files with 131 additions and 81 deletions

View File

@ -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.OrderItemService;
import com.youlai.mall.oms.service.app.OrderService; import com.youlai.mall.oms.service.app.OrderService;
import com.youlai.mall.pms.api.SkuFeignClient; 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.pms.model.dto.SkuInfoDTO;
import com.youlai.mall.ums.api.MemberFeignClient; import com.youlai.mall.ums.api.MemberFeignClient;
import com.youlai.mall.ums.dto.MemberAddressDTO; import com.youlai.mall.ums.dto.MemberAddressDTO;
@ -199,11 +199,11 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, OmsOrder> impleme
} }
// 3. 校验库存并锁定库存 // 3. 校验库存并锁定库存
List<LockedSkuDTO> lockedSkuList = orderItems.stream() List<LockSkuDTO> lockSkuList = orderItems.stream()
.map(item -> new LockedSkuDTO(item.getSkuId(), item.getQuantity(), item.getSkuSn())) .map(item -> new LockSkuDTO(item.getSkuId(), item.getQuantity()))
.collect(Collectors.toList()); .collect(Collectors.toList());
boolean lockStockResult = skuFeignClient.lockStock(orderToken, lockedSkuList); boolean lockStockResult = skuFeignClient.lockStock(orderToken, lockSkuList);
Assert.isTrue(lockStockResult, "订单提交失败:锁定商品库存失败!"); Assert.isTrue(lockStockResult, "订单提交失败:锁定商品库存失败!");
// 4. 生成订单 // 4. 生成订单

View File

@ -38,7 +38,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@SpringBootTest @SpringBootTest
@AutoConfigureMockMvc @AutoConfigureMockMvc
@Slf4j @Slf4j
public class OrderControllerTests { public class OrderControllerTest {
@Autowired @Autowired

View File

@ -1,7 +1,7 @@
package com.youlai.mall.pms.api; package com.youlai.mall.pms.api;
import com.youlai.common.web.config.FeignDecoderConfig; 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 com.youlai.mall.pms.model.dto.SkuInfoDTO;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -31,7 +31,7 @@ public interface SkuFeignClient {
* 锁定商品库存 * 锁定商品库存
*/ */
@PutMapping("/app-api/v1/skus/lock") @PutMapping("/app-api/v1/skus/lock")
boolean lockStock(@RequestParam String orderToken, @RequestBody List<LockedSkuDTO> lockedSkuList); boolean lockStock(@RequestParam String orderToken, @RequestBody List<LockSkuDTO> lockSkuList);
/** /**
* 解锁商品库存 * 解锁商品库存

View File

@ -14,7 +14,7 @@ import lombok.NoArgsConstructor;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class LockedSkuDTO { public class LockSkuDTO {
/** /**
* 商品ID * 商品ID
@ -27,11 +27,4 @@ public class LockedSkuDTO {
private Integer quantity; private Integer quantity;
/**
* 商品编码
*/
private String skuSn;
} }

View File

@ -1,7 +1,7 @@
package com.youlai.mall.pms.controller.app; package com.youlai.mall.pms.controller.app;
import com.youlai.common.result.Result; 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.model.dto.SkuInfoDTO;
import com.youlai.mall.pms.service.SkuService; import com.youlai.mall.pms.service.SkuService;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@ -48,9 +48,9 @@ public class SkuController {
@PutMapping("/lock") @PutMapping("/lock")
public Result<?> lockStock( public Result<?> lockStock(
@RequestParam String orderToken, @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); return Result.success(lockStockResult);
} }

View File

@ -1,7 +1,7 @@
package com.youlai.mall.pms.service; package com.youlai.mall.pms.service;
import com.baomidou.mybatisplus.extension.service.IService; 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.dto.SkuInfoDTO;
import com.youlai.mall.pms.model.entity.PmsSku; import com.youlai.mall.pms.model.entity.PmsSku;
@ -36,10 +36,10 @@ public interface SkuService extends IService<PmsSku> {
* 校验并锁定库存 * 校验并锁定库存
* *
* @param orderToken 订单临时编号 (此时订单未创建) * @param orderToken 订单临时编号 (此时订单未创建)
* @param lockedSkuList 锁定商品库存信息列表 * @param lockSkuList 锁定商品库存信息列表
* @return true/false * @return true/false
*/ */
boolean lockStock(String orderToken,List<LockedSkuDTO> lockedSkuList); boolean lockStock(String orderToken,List<LockSkuDTO> lockSkuList);
/** /**
* 解锁库存 * 解锁库存

View File

@ -9,14 +9,12 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.mall.pms.constant.ProductConstants; import com.youlai.mall.pms.constant.ProductConstants;
import com.youlai.mall.pms.converter.SkuConverter; import com.youlai.mall.pms.converter.SkuConverter;
import com.youlai.mall.pms.mapper.PmsSkuMapper; 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.dto.SkuInfoDTO;
import com.youlai.mall.pms.model.entity.PmsSku; import com.youlai.mall.pms.model.entity.PmsSku;
import com.youlai.mall.pms.service.SkuService; import com.youlai.mall.pms.service.SkuService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; 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.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -35,7 +33,6 @@ import java.util.List;
public class SkuServiceImpl extends ServiceImpl<PmsSkuMapper, PmsSku> implements SkuService { public class SkuServiceImpl extends ServiceImpl<PmsSkuMapper, PmsSku> implements SkuService {
private final RedisTemplate redisTemplate; private final RedisTemplate redisTemplate;
private final RedissonClient redissonClient;
private final SkuConverter skuConverter; private final SkuConverter skuConverter;
@ -65,39 +62,30 @@ public class SkuServiceImpl extends ServiceImpl<PmsSkuMapper, PmsSku> implements
/** /**
* 校验并锁定库存 * 校验并锁定库存
* *
* @param orderToken 订单临时编号 (此时订单未创建) * @param orderToken 订单临时编号 (此时订单未创建)
* @param lockedSkuList 锁定商品库存信息列表 * @param lockSkuList 锁定商品库存列表
* @return true/false * @return true/false
*/ */
@Override @Override
@Transactional @Transactional
public boolean lockStock(String orderToken, List<LockedSkuDTO> lockedSkuList) { public boolean lockStock(String orderToken, List<LockSkuDTO> lockSkuList) {
Assert.isTrue(CollectionUtil.isNotEmpty(lockedSkuList), "订单({})未包含任何商品", orderToken); log.info("订单({})锁定商品库存:{}", orderToken, JSONUtil.toJsonStr(lockSkuList));
Assert.isTrue(CollectionUtil.isNotEmpty(lockSkuList), "订单({})未包含任何商品", orderToken);
// 校验库存数量是否足够以及锁定库存 // 校验库存数量是否足够以及锁定库存
for (LockedSkuDTO lockedSku : lockedSkuList) { for (LockSkuDTO lockedSku : lockSkuList) {
Long skuId = lockedSku.getSkuId(); Integer quantity = lockedSku.getQuantity(); // 订单的商品数量
RLock lock = redissonClient.getLock(ProductConstants.SKU_LOCK_PREFIX + skuId); // 构建商品锁对象 // 库存足够
try { boolean lockResult = this.update(new LambdaUpdateWrapper<PmsSku>()
lock.lock(); .setSql("locked_stock = locked_stock + " + quantity) // 修改锁定商品数
.eq(PmsSku::getId, lockedSku.getSkuId())
Integer quantity = lockedSku.getQuantity(); // 订单的商品数量 .apply("stock - locked_stock >= {0}", quantity) // 剩余商品数 订单商品数
// 库存足够 );
boolean lockResult = this.update(new LambdaUpdateWrapper<PmsSku>() Assert.isTrue(lockResult, "商品库存不足");
.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();
}
}
} }
// 锁定的商品缓存至 Redis (后续使用1.取消订单解锁库存2支付订单扣减库存) // 锁定的商品缓存至 Redis (后续使用1.取消订单解锁库存2支付订单扣减库存)
redisTemplate.opsForValue().set(ProductConstants.LOCKED_SKUS_PREFIX + orderToken, lockedSkuList); redisTemplate.opsForValue().set(ProductConstants.LOCKED_SKUS_PREFIX + orderToken, lockSkuList);
return true; return true;
} }
@ -110,8 +98,9 @@ public class SkuServiceImpl extends ServiceImpl<PmsSkuMapper, PmsSku> implements
* @return true/false * @return true/false
*/ */
@Override @Override
@Transactional
public boolean unlockStock(String orderSn) { 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)); log.info("释放订单({})锁定的商品库存:{}", orderSn, JSONUtil.toJsonStr(lockedSkus));
// 库存已释放 // 库存已释放
@ -119,20 +108,13 @@ public class SkuServiceImpl extends ServiceImpl<PmsSkuMapper, PmsSku> implements
return true; return true;
} }
// 遍历恢复锁定的商品库存 // 解锁商品库存
for (LockedSkuDTO lockedSku : lockedSkus) { for (LockSkuDTO lockedSku : lockedSkus) {
RLock lock = redissonClient.getLock(ProductConstants.SKU_LOCK_PREFIX + lockedSku.getSkuId()); // 获取商品分布式锁 boolean unlockResult = this.update(new LambdaUpdateWrapper<PmsSku>()
try { .setSql("locked_stock = locked_stock - " + lockedSku.getQuantity())
lock.lock(); .eq(PmsSku::getId, lockedSku.getSkuId())
this.update(new LambdaUpdateWrapper<PmsSku>() );
.setSql("locked_stock = locked_stock - " + lockedSku.getQuantity()) Assert.isTrue(unlockResult, "解锁商品库存失败");
.eq(PmsSku::getId, lockedSku.getSkuId())
);
} finally {
if (lock.isLocked()) {
lock.unlock();
}
}
} }
// 移除 redis 订单锁定的商品 // 移除 redis 订单锁定的商品
redisTemplate.delete(ProductConstants.LOCKED_SKUS_PREFIX + orderSn); redisTemplate.delete(ProductConstants.LOCKED_SKUS_PREFIX + orderSn);
@ -148,29 +130,21 @@ public class SkuServiceImpl extends ServiceImpl<PmsSkuMapper, PmsSku> implements
* @return ture/false * @return ture/false
*/ */
@Override @Override
@Transactional
public boolean deductStock(String orderSn) { 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)); log.info("订单({})支付成功,扣减订单商品库存:{}", orderSn, JSONUtil.toJsonStr(lockedSkus));
Assert.isTrue(CollectionUtil.isNotEmpty(lockedSkus), "扣减商品库存失败:订单({})未包含商品"); Assert.isTrue(CollectionUtil.isNotEmpty(lockedSkus), "扣减商品库存失败:订单({})未包含商品");
for (LockedSkuDTO lockedSku : lockedSkus) { for (LockSkuDTO lockedSku : lockedSkus) {
boolean deductResult = this.update(new LambdaUpdateWrapper<PmsSku>()
RLock lock = redissonClient.getLock(ProductConstants.SKU_LOCK_PREFIX + lockedSku.getSkuId()); // 获取商品分布式锁 .setSql("stock = stock - " + lockedSku.getQuantity())
.setSql("locked_stock = locked_stock - " + lockedSku.getQuantity())
try { .eq(PmsSku::getId, lockedSku.getSkuId())
lock.lock(); );
this.update(new LambdaUpdateWrapper<PmsSku>() Assert.isTrue(deductResult, "扣减商品库存失败");
.setSql("stock = stock - " + lockedSku.getQuantity())
.setSql("locked_stock = locked_stock - " + lockedSku.getQuantity())
.eq(PmsSku::getId, lockedSku.getSkuId())
);
} finally {
if (lock.isLocked()) {
lock.unlock();
}
}
} }
// 移除订单锁定的商品 // 移除订单锁定的商品

View File

@ -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() {
}
}

View File

@ -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() {
}
}