redis+lua脚本实现订单创建

一、实体类与controller层

@Data
public class OrderVo {

	@ApiModelProperty(value = "使用预生产订单号防重")
	private String orderNo;

	@ApiModelProperty(value = "用户id")
	private Long userId;

	@ApiModelProperty(value = "收货人姓名")
	private String receiverName;

	@ApiModelProperty(value = "收货人电话")
	private String receiverPhone;

	@ApiModelProperty(value = "下单选中的优惠券id")
	private Long couponId;

}
@ApiOperation("生成订单")  //swagger注解
@PostMapping("auth/submitOrder")
public Result submitOrder(@RequestBody OrderVo orderVo) {
    Long orderId = orderInfoService.submitOrder(orderVo);
    return Result.ok(orderId);
}

 接口实现类实现细节:

        第一步:获取用户, 确定用户的订单

        第二步:确保订单的唯一性, 订单不能重复提交,重复提交验证,通过redis + lua脚本进行判断

        第三步:lua脚本保证原子性操作

        第四步:验证库存 并且 锁定库存

     示例:比如仓库有5个耳机,我想买2个
     验证库存,查询仓库里面是是否有充足耳机
     若库存充足,库存锁定 2 (目前没有真正减库存)

 代码中的lua脚本: String script = "if(redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) else return 0 end";

       解释:  如果redis有相同orderNo,表示正常提交订单,把redis的orderNo删除。

                    传入

 具体代码:

    @Override
    public Long ordersSubmit(OrderVo orderVo) {
        //获取用户,确定用户的订单
        Long userId = AuthContextHolder.getUserId();
        orderVo.setUserId(userId);
        //确保订单的唯一性,
        String orderNo = orderVo.getOrderNo();
        if (StringUtils.isEmpty(orderNo)) {
            throw new SsyxException(ResultCodeEnum.ILLEGAL_REQUEST);
        }

        // 拿着orderNo 到 redis进行查询,
        String script = "if(redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) else return 0 end";
        // 如果redis有相同orderNo,表示正常提交订单,把redis的orderNo删除
        Boolean b = (Boolean) redisTemplate.execute(new DefaultRedisScript(script, boolean.class),
                Arrays.asList(RedisConst.ORDER_REPEAT + orderNo), orderNo);
        if (!b) {
            //重复提交抛出异常
          throw new SsyxException(ResultCodeEnum.REPEAT_SUBMIT);
        }

        //第三步 远程调用cartFeignClient ,验证库存 并且 锁定库存
 
        List<CartInfo> cartInfoList = cartFeignClient.getCartCheckedProduct(userId);

        // 获取普通商品
        List<CartInfo> commonList = cartInfoList.stream()
                .filter(cartInfo -> cartInfo.getSkuType() == SkuType.COMMON.getCode())
                .collect(Collectors.toList());

        //3、把获取购物车里面普通类型商品list集合,List<CartInfo>转换List<SkuStockLockVo>
        if (!CollectionUtils.isEmpty(commonList)) {
            List<SkuStockLockVo> skuStockLockVoList = commonList.stream().map(common -> {
                SkuStockLockVo skuStockLockVo = new SkuStockLockVo();
                BeanUtils.copyProperties(common, skuStockLockVo);
                return skuStockLockVo;
            }).collect(Collectors.toList());

            //4、远程调用service-product模块实现锁定商品
            // 验证库存并锁定库存,保证具备原子性
            Boolean isLockSuccess =
                    productFeignClient.checkLock(skuStockLockVoList, orderNo);
            if(!isLockSuccess) {//库存锁定失败
                throw new SsyxException(ResultCodeEnum.ORDER_STOCK_FALL);
            }
        }

        //第四步 下单步骤
        //1 向两张表添加数据
        // order_info 和 order_item
        Long orderId = this.saveOrder(orderVo,cartInfoList);

        //下单完成,删除购物车记录
        //发送mq消息
        rabbitService.sendMessage(MqConst.EXCHANGE_ORDER_DIRECT,
                MqConst.ROUTING_DELETE_CART,orderVo.getUserId());
        // 返回订单Id

        return orderId;
    }
@Data
public class CartInfo extends BaseEntity {

	private static final long serialVersionUID = 1L;

	@ApiModelProperty(value = "用户id")
	private Long userId;

	@ApiModelProperty(value = "分类id")
	private Long categoryId;

	@ApiModelProperty(value = "商品类型:0->普通商品 1->秒杀商品")
	private Integer skuType;

	@ApiModelProperty(value = "是否新人专享:0->否;1->是")
	private Integer isNewPerson;

	@ApiModelProperty(value = "sku名称 (冗余)")
	private String skuName;

	@ApiModelProperty(value = "skuid")
	private Long skuId;

	@ApiModelProperty(value = "放入购物车时价格")
	private BigDecimal cartPrice;

	@ApiModelProperty(value = "数量")
	private Integer skuNum;

	@ApiModelProperty(value = "限购个数")
	private Integer perLimit;

	@ApiModelProperty(value = "图片文件")
	private String imgUrl;

	@ApiModelProperty(value = "isChecked")
	private Integer isChecked;

	@ApiModelProperty(value = "状态")
	private Integer status;

	@ApiModelProperty(value = "秒杀开始时间")
	@JsonFormat(pattern = "HH:mm:ss")
	private Date startTime;

	@ApiModelProperty(value = "秒杀结束时间")
	@JsonFormat(pattern = "HH:mm:ss")
	private Date endTime;

	@ApiModelProperty(value = "仓库")
	private Long wareId;

}
@Data
public class SkuStockLockVo implements Serializable {

	@ApiModelProperty(value = "skuId")
	private Long skuId;

	@ApiModelProperty(value = "sku个数")
	private Integer skuNum;

	@ApiModelProperty(value = "是否锁定")
	private Boolean isLock = false;
}

 远程服务 CartFeignClient :

@FeignClient("service-cart")
public interface CartFeignClient {
    @GetMapping("/cart/getCartCheckedList/{userId}")
    List<CartInfo> getCartCheckedProduct(Long userId);
}
    @GetMapping("/cart/getCartCheckedList/{userId}")
    public List<CartInfo> getCartCheckedList(@PathVariable("userId") Long userId) {
        return cartInfoService.getCartCheckedProduct(userId);
    }
    //获取当前用户购物车选中购物项
    @Override
    public List<CartInfo> getCartCheckedProduct(Long userId) {
        String cartKey = this.getCartKey(userId);
        BoundHashOperations<String,String,CartInfo> boundHashOperations =
                redisTemplate.boundHashOps(cartKey);
        List<CartInfo> cartInfoProduct = boundHashOperations.values();
        //isChecked = 1表示选中的商品
        List<CartInfo> cartInfoList = cartInfoList.stream()
                .filter(cartInfo -> {
                    return cartInfo.getIsChecked().intValue() == 1;
                }).collect(Collectors.toList());
        return cartInfoList ;
    }

ProductFeginClient 服务具体实现

@FeignClient(value = "service-product")
public interface ProductFeignClient {

    @PostMapping("product/checkAndLock/{orderNo}")
    public  Boolean checkLock(List<SkuStockLockVo> skuStockLockVoList, @PathVariable("orderNo") String orderNo);

}

controller层:  controller中的请求接口要与ProductFeignClient的保持一致

    @ApiOperation(value = "锁定库存")
    @PostMapping("product/checkAndLock/{orderNo}")
    public Boolean checkAndLock(@RequestBody List<SkuStockLockVo> skuStockLockVoList,
                                @PathVariable String orderNo) {
        return skuInfoService.checkLock(skuStockLockVoList,orderNo);
    }
    //验证和锁定库存
    @Override
    public Boolean checkLock(List<SkuStockLockVo> skuStockLockVoList,
                                String orderNo) {
        //1 判断skuStockLockVoList集合是否为空
        if (CollectionUtils.isEmpty(skuStockLockVoList)) {
            throw new SsyxException(ResultCodeEnum.DATA_ERROR);
        }

        //2 遍历skuStockLockVoList得到每个商品,验证库存并锁定库存,具备原子性
        skuStockLockVoList.stream().forEach(skuStockLockVo -> {
            this.checkLock(skuStockLockVo);
        });

        //3 只要有一个商品锁定失败,所有锁定成功的商品都解锁
        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;
    }

实现分布式限流可以使用 RedisLua 脚本来完成。以下是可能的实现方案: 1. 使用 Redis 的 SETNX 命令来实现基于令牌桶算法的限流 令牌桶算法是一种常见的限流算法,它可以通过令牌的放置和消耗来控制流量。在 Redis 中,我们可以使用 SETNX 命令来实现令牌桶算法。 具体实现步骤如下: - 在 Redis创建一个有序集合,用于存储令牌桶的令牌数量和时间戳。 - 每当一个请求到达时,我们首先获取当前令牌桶中的令牌数量和时间戳。 - 如果当前时间戳与最后一次请求的时间戳之差大于等于令牌桶中每个令牌的发放时间间隔,则将当前时间戳更新为最后一次请求的时间戳,并且将令牌桶中的令牌数量增加相应的数量,同时不超过最大容量。 - 如果当前令牌桶中的令牌数量大于等于请求需要的令牌数量,则返回 true 表示通过限流,将令牌桶中的令牌数量减去请求需要的令牌数量。 - 如果令牌桶中的令牌数量不足,则返回 false 表示未通过限流。 下面是使用 RedisLua 脚本实现令牌桶算法的示例代码: ```lua -- 限流的 key local key = KEYS[1] -- 令牌桶的容量 local capacity = tonumber(ARGV[1]) -- 令牌的发放速率 local rate = tonumber(ARGV[2]) -- 请求需要的令牌数量 local tokens = tonumber(ARGV[3]) -- 当前时间戳 local now = redis.call('TIME')[1] -- 获取当前令牌桶中的令牌数量和时间戳 local bucket = redis.call('ZREVRANGEBYSCORE', key, now, 0, 'WITHSCORES', 'LIMIT', 0, 1) -- 如果令牌桶为空,则初始化令牌桶 if not bucket[1] then redis.call('ZADD', key, now, capacity - tokens) return 1 end -- 计算当前令牌桶中的令牌数量和时间戳 local last = tonumber(bucket[2]) local tokensInBucket = tonumber(bucket[1]) -- 计算时间间隔和新的令牌数量 local timePassed = now - last local newTokens = math.floor(timePassed * rate) -- 更新令牌桶 if newTokens > 0 then tokensInBucket = math.min(tokensInBucket + newTokens, capacity) redis.call('ZADD', key, now, tokensInBucket) end -- 检查令牌数量是否足够 if tokensInBucket >= tokens then redis.call('ZREM', key, bucket[1]) return 1 else return 0 end ``` 2. 使用 RedisLua 脚本实现基于漏桶算法的限流 漏桶算法是另一种常见的限流算法,它可以通过漏桶的容量和漏水速度来控制流量。在 Redis 中,我们可以使用 Lua 脚本实现漏桶算法。 具体实现步骤如下: - 在 Redis创建一个键值对,用于存储漏桶的容量和最后一次请求的时间戳。 - 每当一个请求到达时,我们首先获取当前漏桶的容量和最后一次请求的时间戳。 - 计算漏水速度和漏水的数量,将漏桶中的容量减去漏水的数量。 - 如果漏桶中的容量大于等于请求需要的容量,则返回 true 表示通过限流,将漏桶中的容量减去请求需要的容量。 - 如果漏桶中的容量不足,则返回 false 表示未通过限流。 下面是使用 RedisLua 脚本实现漏桶算法的示例代码: ```lua -- 限流的 key local key = KEYS[1] -- 漏桶的容量 local capacity = tonumber(ARGV[1]) -- 漏水速度 local rate = tonumber(ARGV[2]) -- 请求需要的容量 local size = tonumber(ARGV[3]) -- 当前时间戳 local now = redis.call('TIME')[1] -- 获取漏桶中的容量和最后一次请求的时间戳 local bucket = redis.call('HMGET', key, 'capacity', 'last') -- 如果漏桶为空,则初始化漏桶 if not bucket[1] then redis.call('HMSET', key, 'capacity', capacity, 'last', now) return 1 end -- 计算漏水的数量和漏桶中的容量 local last = tonumber(bucket[2]) local capacityInBucket = tonumber(bucket[1]) local leak = math.floor((now - last) * rate) -- 更新漏桶 capacityInBucket = math.min(capacity, capacityInBucket + leak) redis.call('HSET', key, 'capacity', capacityInBucket) redis.call('HSET', key, 'last', now) -- 检查容量是否足够 if capacityInBucket >= size then return 1 else return 0 end ``` 以上是使用 RedisLua 脚本实现分布式限流的两种方案,可以根据实际需求选择适合的方案。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值