Redis+Lua脚本解决高并发情况下库存超卖的问题

news2024/11/16 23:58:15

文章目录

  • 一、实现思路
  • 二、实现代码

一、实现思路

在这里插入图片描述

二、实现代码

order.lua脚本代码:

-- 参数列表
local productIdStr = ARGV[1]
local productNameStr = ARGV[2]
local cartQuantityStr = ARGV[3]
local orderId = ARGV[4]
local userId = ARGV[5]
local orderDate = ARGV[6]
local productPriceStr = ARGV[7]
local productPhotoStr = ARGV[8]
local cartIdStr = ARGV[9]
local stockKey = KEYS[1]
-- 函数列表
local Str_split = '根据字符串逗号分割成table'
function Str_split(str, splitStr)
    local subStrTable = {};
    while (true) do
        local pos = string.find(str, splitStr);
        if (not pos) then
            if str ~= "" then
                subStrTable[#subStrTable + 1] = str;
            end
            break;
        end
        local subStr = string.sub(str, 1, pos - 1);
        if subStr ~= "" then
            subStrTable[#subStrTable + 1] = subStr;
        end
        str = string.sub(str, pos + string.len(splitStr), #str);
    end
    return subStrTable;
end

-- 转成table
local productIdList = Str_split(productIdStr, ',')
local cartQuantityList = Str_split(cartQuantityStr, ',')
local productNameList = Str_split(productNameStr, ',')

-- 根据商品id获取每个商品的库存
local stockList =  redis.pcall('hmget', stockKey, unpack(productIdList))
-- 根据购买的每个商品库存是否足够
for i,v in ipairs(stockList) do
    if(tonumber(v) < tonumber(cartQuantityList[i])) then
        return '商品【' .. productNameList[i] .. '】库存不足!'
    end
end
-- 扣减库存
local payLoad = {};
for i,v in ipairs(stockList) do
    table.insert(payLoad, productIdList[i])
    table.insert(payLoad, tonumber(v) - tonumber(cartQuantityList[i]))
end
redis.pcall('hmset', stockKey, unpack(payLoad))

-- 写入订单消息队列 id: 是指 stream 中的消息 ID,通常使用 * 号表示自动生成
redis.call('xadd', 'stream.orders', '*', 'userId', userId,
        'productIdList', productIdStr, 'id', orderId,
        'cartQuantity', cartQuantityStr, 'orderDate', orderDate,
        'productNameList', productNameStr, 'productPriceList', productPriceStr,
        'productPhotoList', productPhotoStr, 'cartIdList', cartIdStr)

return '成功'

OrderServiceImpl.java文件代码:

package com.yjq.programmer.service.impl;

import cn.hutool.core.bean.BeanUtil;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.sun.deploy.util.StringUtils;
import com.yjq.programmer.bean.CodeMsg;
import com.yjq.programmer.bean.RedisConstant;
import com.yjq.programmer.dao.*;
import com.yjq.programmer.domain.*;
import com.yjq.programmer.dto.*;
import com.yjq.programmer.enums.OrderStateEnum;
import com.yjq.programmer.service.IOrderService;
import com.yjq.programmer.service.IUserService;
import com.yjq.programmer.utils.CommonUtil;
import com.yjq.programmer.utils.CopyUtil;
import com.yjq.programmer.utils.UuidUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;

/**
 * @author 杨杨吖
 * @QQ 823208782
 * @WX yjqi12345678
 * @create 2023-06-17 16:18
 */
@Service
@Transactional
public class OrderServiceImpl implements IOrderService {

    @Resource
    private OrderMapper orderMapper;

    @Resource
    private UserMapper userMapper;


    @Resource
    private OrderItemMapper orderItemMapper;

    @Resource
    private ProductMapper productMapper;

    @Resource
    private CartMapper cartMapper;

    @Resource
    private IUserService userService;


//    @Resource
//    private RedissonClient redissonClient;

    private static final DefaultRedisScript<String> SECKILL_SCRIPT;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

    private Logger log = LoggerFactory.getLogger(OrderServiceImpl.class);

    @PostConstruct
    private void init() {
        SECKILL_ORDER_EXECUTOR.submit(new OrderHandler());
    }

    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("lua/order.lua"));
        SECKILL_SCRIPT.setResultType(String.class);
    }

    private class OrderHandler implements Runnable {

        @Override
        public void run() {
            while (true) {
                try {
                    // 1.获取消息队列中的订单信息 XREAD GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.orders >
                    // ReadOffset.lastConsumed() 获取下一个未消费的订单
                    List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                            StreamOffset.create("stream.orders", ReadOffset.lastConsumed())
                    );
                    // 2.判断订单信息是否为空
                    if (list == null || list.isEmpty()) {
                        // 如果为null,说明没有消息,继续下一次循环
                        continue;
                    }
                    // 解析数据 获取一条数据  因为上面count(1)指定获取一条
                    MapRecord<String, Object, Object> record = list.get(0);
                    Map<Object, Object> value = record.getValue();
                    OrderDTO orderDTO = BeanUtil.fillBeanWithMap(value, new OrderDTO(), true);
                    // 3.创建订单
                    crateOrder(orderDTO);
                    // 4.确认消息 XACK
                    stringRedisTemplate.opsForStream().acknowledge("stream.orders", "g1", record.getId());
                } catch (Exception e) {
                    log.error("处理订单异常", e);
                    handlePendingList();
                }
            }
        }

        // 确认异常的订单再次处理
        private void handlePendingList() {
            while (true) {
                try {
                    // 1.获取pending-list中的订单信息 XREAD GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.orders 0
                    // ReadOffset.from("0") 从第一个消息开始
                    List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1),
                            StreamOffset.create("stream.orders", ReadOffset.from("0"))
                    );
                    // 2.判断订单信息是否为空
                    if (list == null || list.isEmpty()) {
                        // 如果为null,说明没有异常消息,结束循环
                        break;
                    }
                    // 解析数据
                    MapRecord<String, Object, Object> record = list.get(0);
                    Map<Object, Object> value = record.getValue();
                    OrderDTO orderDTO = BeanUtil.fillBeanWithMap(value, new OrderDTO(), true);
                    // 3.创建订单
                    crateOrder(orderDTO);
                    // 4.确认消息 XACK
                    stringRedisTemplate.opsForStream().acknowledge("stream.orders", "g1", record.getId());
                } catch (Exception e) {
                    log.error("处理订单异常", e);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException interruptedException) {
                        interruptedException.printStackTrace();
                    }
                }
            }
        }
    }

    private void crateOrder(OrderDTO orderDTO) {

//        // 创建锁对象
//        RLock redisLock = redissonClient.getLock("lock:order:" + userId);
//        // 尝试获取锁
//        boolean isLock = redisLock.tryLock();
//        // 判断
//        if (!isLock) {
//            // 获取锁失败,直接返回失败或者重试
//            log.error("不允许重复下单!");
//            return;
//        }
        try {
            // 业务逻辑

            String userId = orderDTO.getUserId();
            String orderId = orderDTO.getId();
            Date createTime = CommonUtil.getFormatterDate(orderDTO.getOrderDate(), "yyyy-MM-dd HH:mm:ss");
            String[] productIdList = orderDTO.getProductIdList().split(",");
            String[] productNameList = orderDTO.getProductNameList().split(",");
            String[] productPhotoList = orderDTO.getProductPhotoList().split(",");
            String[] productPriceList = orderDTO.getProductPriceList().split(",");
            String[] cartQuantityList = orderDTO.getCartQuantity().split(",");
            String[] cartIdList = orderDTO.getCartIdList().split(",");
            BigDecimal totalPrice = new BigDecimal("0");

            for(int i=0; i < productIdList.length; i++) {
                // 减去商品库存
                Product product = productMapper.selectByPrimaryKey(productIdList[i]);
                product.setStock(product.getStock() - Integer.parseInt(cartQuantityList[i]));
                // 增加商品销量
                product.setSellNum(product.getSellNum() + Integer.parseInt(cartQuantityList[i]));
                productMapper.updateByPrimaryKeySelective(product);
                // 插入订单详情数据
                OrderItem orderItem = new OrderItem();
                orderItem.setOrderId(orderId);
                orderItem.setId(UuidUtil.getShortUuid());
                orderItem.setQuantity(Integer.valueOf(cartQuantityList[i]));
                orderItem.setProductPrice(new BigDecimal(productPriceList[i]));
                orderItem.setProductPhoto(productPhotoList[i]);
                orderItem.setProductName(productNameList[i]);
                orderItem.setProductId(productIdList[i]);
                orderItemMapper.insertSelective(orderItem);
                totalPrice = totalPrice.add(new BigDecimal(productPriceList[i]).multiply(new BigDecimal(cartQuantityList[i])));
            }
            // 插入订单数据
            Order order = new Order();
            order.setId(orderId);
            order.setUserId(userId);
            order.setCreateTime(createTime);
            order.setState(OrderStateEnum.NO_PAY.getCode());
            order.setTotalPrice(totalPrice);
            orderMapper.insertSelective(order);
            // 清除购物车数据
            for(String cartId : cartIdList) {
                cartMapper.deleteByPrimaryKey(cartId);
            }
        } finally {
//            redisLock.unlock();
        }

    }

    /**
     * 提交订单操作处理
     * @param orderDTO
     * @return
     */
    @Override
    public ResponseDTO<OrderDTO> submitOrder(OrderDTO orderDTO) {
        UserDTO userDTO = new UserDTO();
        userDTO.setToken(orderDTO.getToken());
        ResponseDTO<UserDTO> loginUserResponse = userService.getLoginUser(userDTO);
        if(!CodeMsg.SUCCESS.getCode().equals(loginUserResponse.getCode())) {
            return ResponseDTO.errorByMsg(CodeMsg.USER_SESSION_EXPIRED);
        }
        // 获取登录用户信息
        userDTO = loginUserResponse.getData();
        AddressExample addressExample = new AddressExample();
        addressExample.createCriteria().andUserIdEqualTo(userDTO.getId());
        if(addressMapper.selectByExample(addressExample).size() == 0) {
            return ResponseDTO.errorByMsg(CodeMsg.ADDRESS_NOT_EXIST);
        }

        String[] cartIdList = orderDTO.getCartIdList().split(",");
        CartExample cartExample = new CartExample();
        cartExample.createCriteria().andIdIn(Arrays.stream(cartIdList).collect(Collectors.toList()));
        List<Cart> cartList = cartMapper.selectByExample(cartExample);
        List<String> productIdList = cartList.stream().map(Cart::getProductId).collect(Collectors.toList());
        ProductExample productExample = new ProductExample();
        productExample.createCriteria().andIdIn(productIdList);
        List<Product> productList = productMapper.selectByExample(productExample);
        List<String> productNameList = productList.stream().map(Product::getName).collect(Collectors.toList());
        List<String> productPriceList = productList.stream().map(Product::getPrice).map(String::valueOf).collect(Collectors.toList());
        List<String> productPhotoList = productList.stream().map(Product::getPhoto).map(String::valueOf).collect(Collectors.toList());
        List<String> cartQuantityList = cartList.stream().map(Cart::getQuantity).map(String::valueOf).collect(Collectors.toList());
        String orderId = UuidUtil.getShortUuid();
        String orderDate = CommonUtil.getFormatterDate(new Date(), "yyyy-MM-dd HH:mm:ss");
        // 执行lua脚本
        String result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.singletonList(RedisConstant.STOCK_REDIS_KEY_TEMPLATE),
                StringUtils.join(productIdList, ","),
                StringUtils.join(productNameList, ","),
                StringUtils.join(cartQuantityList, ","),
                orderId,
                userDTO.getId(),
                orderDate,
                StringUtils.join(productPriceList, ","),
                StringUtils.join(productPhotoList, ","),
                StringUtils.join(Arrays.asList(cartIdList), ",")
        );
        if(!"成功".equals(result)) {
            CodeMsg codeMsg = CodeMsg.PRODUCT_STOCK_OVER;
            codeMsg.setMsg(result);
            return ResponseDTO.errorByMsg(codeMsg);
        }
        orderDTO.setId(orderId);
        return ResponseDTO.success(orderDTO);
    }

 
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/696412.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android 控件颜色与实际不符「解决方案」

问题复现 背景色设置为 绿色&#xff0c;然而 Button 控件却显示 紫色 解决方案 这是由于 Theme 修改导致&#xff0c;只需要修改配置文件 themes.xml 中 parent 属性 即可 如果找不到该文件&#xff0c;先将工程结构展示改为 Project 即可 原配置&#xff1a; <style nam…

【网络智能化】网络杂谈(9)之如何做到网络管理智能化

涉及知识点 什么是网络管理智能化&#xff0c;基于专家系统的网络管理&#xff0c;基于智能 Agent 的网络管理&#xff0c;基于计算智能的宽带网络管理&#xff0c;深入了解网络管理智能化技术。 原创于&#xff1a;CSDN博主-《拄杖盲学轻声码》&#xff0c;更多内容可去其主页…

软件业务连续性管理的意义是什么?

软件业务连续性管理是指在软件开发和运营过程中&#xff0c;确保业务能够持续进行的管理方法和实践&#xff0c;它是一种综合性的管理方法&#xff0c;旨在确保在软件系统出现故障、灾难或其他不可预测的情况时&#xff0c;能够快速、有效地恢复业务&#xff0c;以最大程度地减…

【数据分享】全国县市2000-2021年教育、卫生和社会保障数据(excel\shp格式)

《中国县域统计年鉴》是一部全面反映我国县域社会经济发展状况的资料性年鉴&#xff0c;收录了上一年度全国2000多个县域单位的基本情况、综合经济、农业、工业、教育、卫生、社会保障等方面的资料。 之前基于《中国县域统计年鉴》我们分享了2000至2021年的综合经济数据和农业…

gitee删除已上传的废弃工程

第一步:打开Git Bash或者右击电脑桌面打开也可以做到 第二步:cd到指定的工程文件夹内 dir 看一下&#xff0c;下面的项目是否正确 第三步:输入命令 git rm -r 想要删除的项目名称 第四步&#xff1a;输入命令 git commit -m 备注 第五步&#xff1a;输入命令保存 git pu…

Day.1 LeetCode刷题练习(最长公共前缀 C/C++两种解法)

题目&#xff1a; 例子&#xff1a; 分析题目&#xff1a; 主要目的&#xff1a;求出各个字符串的公共前缀 思路&#xff08;本人解法&#xff09;&#xff1a; 用所给实例来看&#xff0c;不难看出我们可以直接以竖着对应来查看是否是公共前缀 &#xff0c; 这样就有了一…

android的PopupWindow透明弹窗

1.要实现这种效果 2.可以使用这种方式 View v LayoutInflater.from(mContext).inflate(R.layout.ceshi_01, null);PopupWindow popupWindow new PopupWindow(v, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, true);popupWindow.showAsDropDo…

云原生(第一篇)k8s-组件说明

k8s是什么&#xff1f; go语言开发的开源的跨主机的容器编排工具&#xff1b;全称是kubernetes&#xff1b; k8s的组件&#xff1a; master&#xff1a; ①kube-apiserver 所有服务统一的访问入口&#xff0c;无论对内还是对外&#xff1b; ②kube-controller-manager 资源控…

java8新特性---lambda表达式

1、 Lambda是什么&#xff1f; Lambda 是一个匿名函数&#xff0c;我们可以把 Lambda表达式理解为是一段可以传递的代码&#xff08;将代码像数据一样进行传递&#xff09;。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格&#xff0c;使Java的语言表达能力得到了提…

linux配置git账号密码

HTTP/HTTPS Linux 在Linux环境下使用http/https协议clone/pull GitLab仓库的代码遇到这个问题。以下解决方案也适合GitHub或其他仓库如码云&#xff0c;coding。 解决方案&#xff1a; 在home目录下创建.git-credentials文件vim .git-credentials&#xff0c;输入形如https:…

重装pycharm后一直在Updating indexes

今天卸载pycharm社区版&#xff0c;安装好pycharm专业版之后&#xff0c;右下角一直显示Updating indexes 解决步骤&#xff1a; 先卸载pycharm&#xff1b;打开cmd&#xff0c;键入regedit&#xff0c;打开“注册表编辑器”&#xff1b;在“注册表编辑器”中按ctrlF&#xff…

spring是如何解析@Transactional属性的

1、查看value调用 发现并无地方调用 2、查看 transactionManager调用 发现并无地方调用 3、直接查看transaction被解析方法 SpringTransactionAnnotationParser#parseTransactionAnnotation(java.lang.reflect.AnnotatedElement) 4、查看返回的解析类 Tra…

C++——二分法求多项式曲线区间极值

二分法求多项式曲线区间极值 二分法求解&#xff0c;设置left为左边界&#xff0c;right为右边界&#xff0c;则解一定位于left和right之间&#xff0c;当左右边界之间的差值小于某一精确度时&#xff0c;就认为找到了解。具体操作如下&#xff0c;若是先减后增&#xff0c;首先…

Unity UGUI 实现一个拖拽一个物体到另一个物体上 并返回两个物体是否相交或者是否在对方物体的中心点

Unity版本 2021.3.25f1c1 首先创建一个碰撞管理器 ColliderNodeManager.cs 具体代码实现如下 using System; using UnityEngine;/// <summary> /// 碰撞检测管理器 /// </summary> public class ColliderNodeManager : MonoBehaviour {public static ColliderN…

Qt错误: warning: ignoring old recipe for target ‘childwnd.o‘.

Qt中这样的错误 &#xff1a; 错误原因 &#xff1a; 工程文件, 也就是 *.pro文件, 其中的源文件*.cpp, 头文件*.h, ui文件*.ui以及资源文件*.qrc 重复了, 删掉就好. *.pro文件就是这玩意 &#xff1a; *.pro文件中的文件重复&#xff1a; 如果还是出现这样的错误警告&#…

Flutter中的Alignment是怎么回事?

我们知道Flutter中的Container有个alignment的属性&#xff0c;我们可以设置一些topLeft,center之类的位置名字的值&#xff0c;也可以设置Alignment(x,y)这种具体数值的值。那么Align子widget的位置跟x、y是什么关系呢&#xff1f;以水平方向为例&#xff0c;我们知道x-1表示子…

vs+qt 给打包程序添加图标

1、在创建的qt工程文件中添加如下代码&#xff1a; this->setWindowIcon(QIcon(":/new/prefix1/ico"));//设置软件图标 this->setWindowTitle("XXX软件名 ");//设置软件标题 运行后软件的左上角会有图标 2、让编译成的exe带图标 生成…

dup、dup2、F_DUPFD、dup3、F_DUPFD_CLOEXEC:实例

本实例将输出和错误输出&#xff0c;重定向到文件outlog.txt文件。 dup实现 测试代码&#xff1a; #include <sys/types.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h>…

基于win10环境搭建图片服务器的两种方式

简述 这几天接到一种需求&#xff0c;需要在window环境搭建图片服务&#xff0c;去网上搜一下&#xff0c;最终想出两种方式一种是Nginx方式 &#xff0c;一种是公司常用的iis服务方式&#xff0c;最终使用iis方式&#xff0c;这里简单记录一下。 Nginx nginx方式很简单&#…

机器学习模型优化器Adam报错:ValueError: decay is deprecated in the new Keras optimizer

文章目录 深度学习模型优化器报错&#xff1a;报错原因&#xff1a; 解决方案&#xff1a; 深度学习模型优化器报错&#xff1a; ValueError: decay is deprecated in the new Keras optimizer, pleasecheck the docstring for valid arguments, or use the legacy optimizer,…