并发的场景下,用户在生成订单时,需要进行使用分布式锁来锁定商品库存,避免出现超卖情况
以下是三大优选团购系统中完整的下单处理流程和逻辑
1. 确认订单处理逻辑(生成唯一标识订单号,存入redis,用于生成订单时进行验证)
//确认订单
@Override
public OrderConfirmVo confirmOrder() {
//获取用户id
Long userId = AuthContextHolder.getUserId();
//获取用户对应团长信息
LeaderAddressVo leaderAddressVo =
userFeignClient.getUserAddressByUserId(userId);
//获取购物车里面选中的商品
List<CartInfo> cartInfoList = cartFeignClient.getCartCheckedList(userId);
//唯一标识订单号,存入redis
String orderNo = System.currentTimeMillis() + "";
redisTemplate.opsForValue().set(RedisConst.ORDER_REPEAT + orderNo, orderNo,
24, TimeUnit.HOURS);
//获取购物车满足条件活动和优惠卷信息
OrderConfirmVo orderConfirmVo = activityFeignClient.findCartActivityAndCoupon(cartInfoList, userId);
//封装其他值
orderConfirmVo.setLeaderAddressVo(leaderAddressVo); //团长地址
orderConfirmVo.setOrderNo(orderNo); //订单号
return orderConfirmVo;
}
2. 生成订单处理逻辑
//生成订单
@Transactional
@Override
public Long submitOrder(OrderSubmitVo orderParamVo) {
//第一步 设置给哪个用户生成订单 设置orderParamVo的userId
Long userId = AuthContextHolder.getUserId();
orderParamVo.setUserId(userId);
//第二步 订单不能重复提交,重复提交验证
// 通过redis + lua脚本进行判断
lua脚本保证原子性操作
//1 获取传递过来的订单 orderNo
String orderNo = orderParamVo.getOrderNo();
if(StringUtils.isEmpty(orderNo)) {
throw new SdyxException(ResultCodeEnum.ILLEGAL_REQUEST);
}
//2 拿着 orderNo 到 redis 进行查询,
String script = "if(redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) else return 0 end";
//3 如果redis有相同orderNo(确认订单成功存到redis),表示正常提交订单,把redis的orderNo删除(即生成订单后需要把锁定的订单从redis删除)
Boolean flag = (Boolean)redisTemplate
.execute(new DefaultRedisScript(script, Boolean.class),
Arrays.asList(RedisConst.ORDER_REPEAT + orderNo), orderNo);
//4 如果redis没有相同orderNo,表示重复提交了,不能再往后进行
if(!flag) {
throw new SdyxException(ResultCodeEnum.REPEAT_SUBMIT);
}
//第三步 验证库存 并且 锁定库存
// 比如仓库有10个西红柿,我想买2个西红柿
// ** 验证库存,查询仓库里面是是否有充足西红柿
// ** 库存充足,库存锁定 2 锁定(目前没有真正减库存)
//1、远程调用service-cart模块,获取当前用户购物车商品(选中的购物项)
List<CartInfo> cartInfoList =
cartFeignClient.getCartCheckedList(userId);
//2、购物车有很多商品,商品不同类型,重点处理普通类型商品
List<CartInfo> commonSkuList = cartInfoList.stream()
.filter(cartInfo -> cartInfo.getSkuType() == SkuType.COMMON.getCode())
.collect(Collectors.toList());
//3、把获取购物车里面普通类型商品list集合,
// List<CartInfo>转换List<SkuStockLockVo> 便于验证sku库存和锁定sku库存
if(!CollectionUtils.isEmpty(commonSkuList)) {
List<SkuStockLockVo> commonStockLockVoList = commonSkuList.stream().map(item -> {
SkuStockLockVo skuStockLockVo = new SkuStockLockVo();
skuStockLockVo.setSkuId(item.getSkuId());
skuStockLockVo.setSkuNum(item.getSkuNum());
return skuStockLockVo;
}).collect(Collectors.toList());
//4、远程调用service-product模块实现锁定商品
验证库存并锁定库存,保证具备原子性
Boolean isLockSuccess =
productFeignClient.checkAndLock(commonStockLockVoList, orderNo);
if(!isLockSuccess) { //库存锁定失败
throw new SdyxException(ResultCodeEnum.ORDER_STOCK_FALL);
}
}
//第四步 下单过程
//1 向两张表添加数据
// order_info 和 order_item
Long orderId = this.saveOrder(orderParamVo, cartInfoList);
//下单完成,删除购物车记录
//发送mq消息(交换机、路由键、消息)
rabbitService.sendMessage(MqConst.EXCHANGE_ORDER_DIRECT, MqConst.ROUTING_DELETE_CART, orderParamVo.getUserId());
//第五步 返回订单id
return orderId;
}
2.1 验证商品库存并锁定库存
/**
* 验证和锁定库存
* @param skuStockLockVoList
* @param orderNo
* @return 订单中所有商品锁定成功,返回true, 只要有一个商品锁定失败,返回false
*/
@Override
public Boolean checkAndLock(List<SkuStockLockVo> skuStockLockVoList, String orderNo) {
//1 判断skuStockLockVoList集合是否为空
if(CollectionUtils.isEmpty(skuStockLockVoList)) {
throw new SdyxException(ResultCodeEnum.DATA_ERROR);
}
//2 遍历skuStockLockVoList得到每个商品,验证库存并锁定库存,具备原子性
skuStockLockVoList.stream().forEach(skuStockLockVo -> {
this.checkLock(skuStockLockVo);
});
//3 只要有一个商品锁定失败(isLock字段值为false),所有锁定成功的商品都解锁
boolean flag = skuStockLockVoList.stream()
.anyMatch(skuStockLockVo -> !skuStockLockVo.getIsLock());
if(flag) {
//所有锁定成功的商品都解锁
skuStockLockVoList.stream().filter(SkuStockLockVo::getIsLock)
.forEach(skuStockLockVo -> {
baseMapper.unlockStock(skuStockLockVo.getSkuId(), skuStockLockVo.getSkuNum());
});
//返回失败的状态
return false;
}
//4 如果所有商品都锁定成功了,redis缓存相关数据,为了方便后面解锁和减库存
redisTemplate.opsForValue()
.set(RedisConst.SROCK_INFO + orderNo, skuStockLockVoList);
return true;
}
2.2 使用Redisson的公平锁实现锁定库存
//2 遍历skuStockLockVoList得到每个商品,验证库存并锁定库存,具备原子性
private void checkLock(SkuStockLockVo skuStockLockVo) {
// 获取锁
// 公平锁
RLock rLock =
this.redissonClient.getFairLock(RedisConst.SKUKEY_PREFIX + skuStockLockVo.getSkuId());
//加锁
rLock.lock();
try {
//验证库存
SkuInfo skuInfo =
baseMapper.checkStock(skuStockLockVo.getSkuId(), skuStockLockVo.getSkuNum());
//判断没有满足条件商品,设置isLock值false,返回
if(skuInfo == null) {
skuStockLockVo.setIsLock(false); //设置锁定失败
return;
}
//有满足条件商品
//锁定库存:update
Integer rows =
baseMapper.lockStock(skuStockLockVo.getSkuId(), skuStockLockVo.getSkuNum());
if(rows == 1) {
skuStockLockVo.setIsLock(true); //设置锁定成功
}
} finally {
//解锁
rLock.unlock();
}
}
2.3 验证库存、锁定库存、解锁库存sql语句
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gao.sdyx.product.mapper.SkuInfoMapper">
<resultMap id="skuInfoMap" type="com.gao.sdyx.model.product.SkuInfo" autoMapping="true"></resultMap>
<!--//验证库存-->
<select id="checkStock" resultMap="skuInfoMap">
select
id,category_id,sku_type,sku_name,img_url,per_limit,publish_status,
check_status,is_new_person,sort,sku_code,price,market_price,stock,
lock_stock,low_stock,sale,ware_id,create_time,update_time,is_deleted
from sku_info
where id = #{skuId} and stock-lock_stock > #{skuNum} for update
</select>
<!--//锁定库存:update-->
<update id="lockStock">
update sku_info set lock_stock = lock_stock + #{skuNum} where id = #{skuId}
</update>
<!--//解锁库存-->
<update id="unlockStock">
update sku_info set lock_stock = lock_stock - #{skuNum} where id = #{skuId}
</update>
<!--减库存-->
<update id="minusStock">
update sku_info set stock = stock - #{skuNum},lock_stock = lock_stock - #{skuNum},
sale = sale + #{skuNum} where id=#{skuId}
</update>
</mapper>
2.4 下单过程,向订单表和订单项表两表中添加数据(保证事务的原子性)
//1 向两张表添加数据
// order_info 和 order_item
/**
* 保存订单内容,向表中添加数据
* @param orderParamVo 提交的订单基本信息
* @param cartInfoList 当前用户购物车商品(选中的购物项)
* @return 订单id
*/
@Transactional(rollbackFor = {Exception.class})
public Long saveOrder(OrderSubmitVo orderParamVo, List<CartInfo> cartInfoList) {
if(CollectionUtils.isEmpty(cartInfoList)) {
throw new SdyxException(ResultCodeEnum.DATA_ERROR);
}
//查询用户提货点和团长信息
Long userId = AuthContextHolder.getUserId();
LeaderAddressVo leaderAddressVo = userFeignClient.getUserAddressByUserId(userId);
if(leaderAddressVo == null) {
throw new SdyxException(ResultCodeEnum.DATA_ERROR);
}
//计算金额
//分摊后营销活动减少的金额
Map<String, BigDecimal> activitySplitAmount = this.computeActivitySplitAmount(cartInfoList);
//分摊后优惠卷减少的金额
Map<String, BigDecimal> couponInfoSplitAmount = this.computeCouponInfoSplitAmount(cartInfoList, orderParamVo.getCouponId());
//封装订单项数据
List<OrderItem> orderItemList = new ArrayList<>();
for(CartInfo cartInfo:cartInfoList) {
OrderItem orderItem = new OrderItem();
orderItem.setId(null); //订单项id
orderItem.setCategoryId(cartInfo.getCategoryId()); //订单项商品所属分类
if(cartInfo.getSkuType() == SkuType.COMMON.getCode()) {
orderItem.setSkuType(SkuType.COMMON); //商品类型
} else {
orderItem.setSkuType(SkuType.SECKILL);
}
orderItem.setSkuId(cartInfo.getSkuId()); //订单项中商品skuId
orderItem.setSkuName(cartInfo.getSkuName()); //商品名称
orderItem.setSkuPrice(cartInfo.getCartPrice()); //商品单价
orderItem.setImgUrl(cartInfo.getImgUrl()); //图片
orderItem.setSkuNum(cartInfo.getSkuNum()); //数量
orderItem.setLeaderId(orderParamVo.getLeaderId()); //团长id
//营销活动金额
BigDecimal activityAmount =
activitySplitAmount.get("activity:" + orderItem.getSkuId());
if(activityAmount == null) {
activityAmount = new BigDecimal(0);
}
orderItem.setSplitActivityAmount(activityAmount); //设置订单中每个商品参与活动分摊减少的金额
//优惠卷金额
BigDecimal couponAmount = couponInfoSplitAmount.get("coupon:" + orderItem.getSkuId());
if(couponAmount == null) {
couponAmount = new BigDecimal(0);
}
orderItem.setSplitCouponAmount(couponAmount); //设置订单中每个商品参与优惠券后分摊减少的金额
//总金额
BigDecimal skuTotalAmount =
orderItem.getSkuPrice().multiply(new BigDecimal(orderItem.getSkuNum()));
//优惠之后金额
BigDecimal splitTotalAmount =
skuTotalAmount.subtract(activityAmount).subtract(couponAmount);
orderItem.setSplitTotalAmount(splitTotalAmount); //每个订单项分摊优惠之后的金额
orderItemList.add(orderItem);
}
//封装订单OrderInfo数据
OrderInfo orderInfo = new OrderInfo();
orderInfo.setUserId(userId); //用户id
orderInfo.setOrderNo(orderParamVo.getOrderNo()); //订单号 唯一标识
orderInfo.setCouponId(orderParamVo.getCouponId()); //下单使用的优惠券id
orderInfo.setOrderStatus(OrderStatus.UNPAID); //订单状态,生成成功未支付
orderInfo.setLeaderId(orderParamVo.getLeaderId()); //团长id
orderInfo.setLeaderName(leaderAddressVo.getLeaderName()); //团长名称
orderInfo.setLeaderPhone(leaderAddressVo.getLeaderPhone()); //团长手机号
orderInfo.setTakeName(leaderAddressVo.getTakeName()); //提货点名称
orderInfo.setReceiverName(orderParamVo.getReceiverName()); //收货人姓名
orderInfo.setReceiverPhone(orderParamVo.getReceiverPhone()); //收货人手机号
orderInfo.setReceiverProvince(leaderAddressVo.getProvince()); //收货人省份(与团长在同一个区域)
orderInfo.setReceiverCity(leaderAddressVo.getCity()); //城市
orderInfo.setReceiverDistrict(leaderAddressVo.getDistrict()); //区
orderInfo.setReceiverAddress(leaderAddressVo.getDetailAddress()); //详细地址
orderInfo.setWareId(cartInfoList.get(0).getWareId()); //仓库id
orderInfo.setProcessStatus(ProcessStatus.UNPAID); //设置订单状态为 未支付状态
//计算订单原始金额
BigDecimal originalTotalAmount = this.computeTotalAmount(cartInfoList);
BigDecimal activityAmount = activitySplitAmount.get("activity:total");
if(null == activityAmount) activityAmount = new BigDecimal(0);
BigDecimal couponAmount = couponInfoSplitAmount.get("coupon:total");
if(null == couponAmount) couponAmount = new BigDecimal(0);
BigDecimal totalAmount = originalTotalAmount.subtract(activityAmount).subtract(couponAmount);
//计算订单金额
orderInfo.setOriginalTotalAmount(originalTotalAmount); //设置订单原始金额
orderInfo.setActivityAmount(activityAmount); //设置活动减小金额
orderInfo.setCouponAmount(couponAmount); //优惠券减少金额
orderInfo.setTotalAmount(totalAmount); //所有优惠后总金额
//计算团长佣金
BigDecimal profitRate = new BigDecimal(0); //orderSetService.getProfitRate();
BigDecimal commissionAmount = orderInfo.getTotalAmount().multiply(profitRate);
orderInfo.setCommissionAmount(commissionAmount);
//添加数据到订单基本信息表
baseMapper.insert(orderInfo);
//添加订单里面订单项
orderItemList.forEach(orderItem -> {
orderItem.setOrderId(orderInfo.getId()); //为每个订单项设置订单id字段
orderItemMapper.insert(orderItem);
});
//如果当前订单使用优惠卷,更新优惠卷状态
if(orderInfo.getCouponId() != null) {
activityFeignClient.updateCouponInfoUseStatus(orderInfo.getCouponId(), userId, orderInfo.getId());
}
//下单成功,记录用户购物商品数量到redis
//hash类型 key(userId) - field(skuId) - value(skuNum)
String orderSkuKey = RedisConst.ORDER_SKU_MAP + orderParamVo.getUserId();
BoundHashOperations<String, String, Integer> hashOperations = redisTemplate.boundHashOps(orderSkuKey);
cartInfoList.forEach(cartInfo -> {
if(hashOperations.hasKey(cartInfo.getSkuId().toString())) {
Integer orderSkuNum = hashOperations.get(cartInfo.getSkuId().toString()) + cartInfo.getSkuNum();
hashOperations.put(cartInfo.getSkuId().toString(), orderSkuNum);
}
});
redisTemplate.expire(orderSkuKey, DateUtil.getCurrentExpireTimes(), TimeUnit.SECONDS);
//订单id
return orderInfo.getId();
}
2.5 下单完成,通过mq异步方式删除该用户购物车本次选中的购物数据(之前用户购物车数据存在redis中,使用hash类型存储,键key表示用户id,值为(skuId,skuInfo))
2.5.1 往购物车中添加数据流程(下单过程不涉及此步骤,只是为了对比下单完整删除购物车数据)
/**
* 添加商品到购物车
* @param userId
* @param skuId
* @param skuNum
*/
@Override
public void addToCart(Long userId, Long skuId, Integer skuNum) {
//1 因为购物车数据存储到redis里面,
// 从redis里面根据key获取数据,这个key包含userId(区分不同用户的购物车)
String cartKey = this.getCartKey(userId);
//redis中存储的格式为:userId, skuId, 购物车sku数据对象
BoundHashOperations<String, String, CartInfo> hashOperations =
redisTemplate.boundHashOps(cartKey);
//2 根据第一步查询出来的结果,得到是skuId + skuNum关系
CartInfo cartInfo = null;
// 目的:判断是否是第一次添加这个商品到购物车
// 进行判断,判断结果里面,是否有skuId
if(hashOperations.hasKey(skuId.toString())) {
//3 如果结果里面包含skuId,不是第一次添加
//3.1 根据skuId,获取对应数量,更新数量
cartInfo = hashOperations.get(skuId.toString());
//把购物车存在商品之前数量获取数量,在进行数量更新操作
Integer currentSkuNum = cartInfo.getSkuNum() + skuNum;
if(currentSkuNum < 1) {
return;
}
//更新cartInfo对象
cartInfo.setSkuNum(currentSkuNum);
cartInfo.setCurrentBuyNum(currentSkuNum);
//判断商品数量不能大于限购数量
Integer perLimit = cartInfo.getPerLimit();
if(currentSkuNum > perLimit) {
throw new SdyxException(ResultCodeEnum.SKU_LIMIT_ERROR);
}
//更新其他值
cartInfo.setIsChecked(1);
cartInfo.setUpdateTime(new Date());
} else {
//4 如果结果里面没有skuId,就是第一次添加
//4.1 直接添加
skuNum = 1;
//远程调用根据skuId获取skuInfo
SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);
if(skuInfo == null) {
throw new SdyxException(ResultCodeEnum.DATA_ERROR);
}
//封装cartInfo对象
cartInfo = new CartInfo();
cartInfo.setSkuId(skuId);
cartInfo.setCategoryId(skuInfo.getCategoryId());
cartInfo.setSkuType(skuInfo.getSkuType());
cartInfo.setIsNewPerson(skuInfo.getIsNewPerson());
cartInfo.setUserId(userId);
cartInfo.setCartPrice(skuInfo.getPrice());
cartInfo.setSkuNum(skuNum);
cartInfo.setCurrentBuyNum(skuNum);
cartInfo.setSkuType(SkuType.COMMON.getCode());
cartInfo.setPerLimit(skuInfo.getPerLimit());
cartInfo.setImgUrl(skuInfo.getImgUrl());
cartInfo.setSkuName(skuInfo.getSkuName());
cartInfo.setWareId(skuInfo.getWareId());
cartInfo.setIsChecked(1);
cartInfo.setStatus(1);
cartInfo.setCreateTime(new Date());
cartInfo.setUpdateTime(new Date());
}
//5 更新redis缓存
hashOperations.put(skuId.toString(), cartInfo);
//6 设置有效时间
this.setCartKeyExpire(cartKey);
}
2.5.2 下单完成,删除选中的购物车数据(生产者发送mq消息)
//下单完成,删除购物车记录
//发送mq消息(交换机、路由键、消息)
rabbitService.sendMessage(MqConst.EXCHANGE_ORDER_DIRECT, MqConst.ROUTING_DELETE_CART, orderParamVo.getUserId());
2.5.3 消费者接收mq消息
package com.gao.sdyx.cart.receiver;
import com.gao.sdyx.cart.service.CartInfoService;
import com.gao.sdyx.mq.constant.MqConst;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class CartReceiver {
@Autowired
private CartInfoService cartInfoService;
/**
* 下单完成,通过mq异步方式删除购物车中本地下单的商品(购物车中选中的数据)
* 下单完整后 消费者端从mq根据路由键从消息队列中接收消息
* @param userId
* @param message
* @param channel
* @throws IOException
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = MqConst.QUEUE_DELETE_CART, durable = "true"),
exchange = @Exchange(value = MqConst.EXCHANGE_ORDER_DIRECT),
key = {MqConst.ROUTING_DELETE_CART}
))
public void deleteCart(Long userId, Message message, Channel channel) throws IOException {
if(userId != null) {
cartInfoService.deleteCartChecked(userId);
}
//手动确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
2.5.4 根据userId删除选中的购物数据
//获取当前用户购物车选中购物项
@Override
public List<CartInfo> getCartCheckedList(Long userId) {
String cartKey = this.getCartKey(userId);
BoundHashOperations<String,String,CartInfo> boundHashOperations =
redisTemplate.boundHashOps(cartKey);
List<CartInfo> cartInfoList = boundHashOperations.values();
//isChecked = 1购物项选中
List<CartInfo> cartInfoListNew = cartInfoList.stream()
.filter(cartInfo -> {
return cartInfo.getIsChecked().intValue() == 1;
}).collect(Collectors.toList());
return cartInfoListNew;
}
//根据userId删除选中购物车记录
@Override
public void deleteCartChecked(Long userId) {
//根据userid查询选中购物车记录
List<CartInfo> cartInfoList = this.getCartCheckedList(userId);
//查询list数据处理,得到skuId集合
List<Long> skuIdList = cartInfoList.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
//构建redis的key值
// hash类型 key filed-value
String cartKey = this.getCartKey(userId);
//根据key查询filed-value结构
BoundHashOperations<String,String,CartInfo> hashOperations =
redisTemplate.boundHashOps(cartKey);
//根据filed(skuId)删除redis数据
skuIdList.forEach(skuId -> {
hashOperations.delete(skuId.toString());
});
}