异步简单实现一人一单

news2025/4/17 9:09:33

本项目码云地址:https://gitee.com/flowers-bloom-is-the-sea/distributeNodeSolvePessimisticLockByRedis/tree/version3/

项目前身:https://gitee.com/flowers-bloom-is-the-sea/distributeNodeSolvePessimisticLockByRedis/tree/version2.0/

异步实现一人一单

一、问题描述

对于一人一单的秒杀,因为这个项目之前本来就是没做异步处理的,因此对于一个业务,对于锁的操作其实可以放到其他异步线程执行。这样子就可以提高效率,而对于可以全部放到异步线程执行的操作也是根据实际业务来看的,总不能把所有业务都放异步处理吧。

对于原来的项目的具体过程是:

对于单个用户访问想买一个物品时,是先到redis里获取锁,如果获取锁成功,那么就去执行订单创建的业务,反之失败。

本篇文章是对于单个用户访问进行访问加速的,也就是当用户来操作这个业务时,使用异步线程来执行其他的操作,使用异步线程操作前就返回数据给用户使得用户不需要等待太久。

二、优化过程

第一步:

对于创建每一个物品,都将物品的库存信息放到缓存里吧。

对应的controller:

@Autowired
private GoodsOrderService goodsOrderService;
@PostMapping("addGoods")
public R addGoods(@RequestBody Goods goods) {
    goodsService.addGoods(goods);
    return R.success(goods.getId());
}

对应的impl:

@Override
public void addGoods(Goods goods) {
    save(goods);//1.save一下
    //2.TODO 添加lua脚本
    stringRedisTemplate.opsForValue().set("goods:stock:" + goods.getId(), goods.getStock().toString());
}

现在来测试一下:

`http://localhost:8081/goods/addGoods`
请求体:
{
    "id":2,
    "stock":100
}

预测:一个请求过去,正常情况下redis是有存储了该物品的库存信息了。另外数据库也添加一条id=2,stock = 100的物品信息了。

测试结果:

发现redis节点啊确实加入了库存信息

在这里插入图片描述

数据库信息:

    id   stock  
------  --------
     1       998
     2       100

结果评价:和预期的一样。

第二步:

对于一个用户来想购买一个物品,那么可以先返回结果,后面的加锁的操作再使用异步来实现。

先来实现一下:

1、首先肯定是先来查询redis一下看看有没有库存,如果有库存,那么再查一下该用户是否已经购买了该物品,如已经买过了,那么直接返回给用户:已购买,不可以买2次。如果没买,那么就操作redis,异步操作库存扣减和订单处理任务,返回已购买。

先写一下lua脚本吧:

-- 1.参数列表
-- 1.1.物品id
local goodsId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]

-- 2.数据key
-- 2.1.库存key
local stockKey = 'goods:stock:' .. goodsId
-- 2.2.订单key
local orderKey = 'goods:order:' .. userId

-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 3.2.库存不足,返回1
    return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.3.存在,说明是重复下单,返回2
    return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.用户-添加到-->hashset
redis.call('sadd', orderKey, userId)
return 0

感觉这里有漏洞,什么漏洞?如果有多个物品goods对吧,那么多个goods都要实现一人一单,假如说有2个物品,分别是:

goods1:{
	id:1,
	stock:1000
}

goods2:{
	id:2,
	stock:100
}

那么对于同一个用户,这个用户已经购买了物品1,接下来还想买物品2,

那么这个用户去查redis时,便会被判断已经买了物品2。这个不就是误判了吗,为什么会误判?觉得lua脚本有问题:

stockKey的定义是:goods:stock:goodsId没问题对吧;

但是orderKey的定义有问题,因为orderKey定义为:goods:order:userId这里不就是不能对唯一种类物品进行唯一标识吗?

应该这样设计orderKey定义:goods:order:goodsId:userId

经过分析,应该是可修改的

第三步:

对于要生成goodsOrder可以丢进一个队列里面,然后直接返回结果。也就是像下面一样:

@Override
public R buyOneGoods(Long goodsId, Long userId) {
    Long result = stringRedisTemplate.execute(
            GOODS_SCRIPT,
            Collections.emptyList(),
            goodsId.toString().intern(), userId.toString()
    );

    int r = result.intValue();

    // 2.判断结果是否为0
    if (r != 0) {
        // 2.1.不为0 ,代表没有购买资格
        return R.fail(r == 1 ? "库存不足" : "不能重复下单");
    }
    GoodsOrder goodsOrder = new GoodsOrder();
    goodsOrder.setGoodsId(goodsId);
    goodsOrder.setUserId(userId);
    //先丢到队列里面
    orderTasks.add(goodsOrder);

    proxy = (GoodsOrderService)AopContext.currentProxy();
    return R.success("buy one success");
}

交给队列后,队列的goodsOrder对象会被异步取出来,进行相应的处理:

因为在这整个业务开始处理的时候就有一个线程池开启一个异步线程来处理上面的goodsOrder

private static final ExecutorService GOODS_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
@PostConstruct
private void init(){
    GOODS_ORDER_EXECUTOR.submit(new GoodsOrderHandler());
}

这个线程创建的GoodsOrderHandler类里面就要一个方法专门处理goodsOrder的:

    private class GoodsOrderHandler implements Runnable{
        @Override
        public void run() {
            while (true){
                try {
                    // 1.获取队列中的订单信息
                    GoodsOrder goodsOrder = orderTasks.take();
                    // 2.创建订单
                    handleGoodsOrder(goodsOrder);
                } catch (Exception e) {
                    log.error("处理订单异常", e);
                }
            }
        }
    }
    private void handleGoodsOrder(GoodsOrder goodsOrder) {
        Long userId = goodsOrder.getUserId();
        // 创建锁对象
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        // 获取锁
        boolean isLock = lock.tryLock();
        // 判断是否获取锁成功
        if(!isLock){
            // 获取锁失败,返回错误或重试
            log.error("不允许重复下单");
            return;
        }
        try {
            // 获取代理对象(事务)
            proxy.createGoodsOrder(goodsOrder);
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

因为run里有一个死循环,于是会不断地获取队列里的goodsOrder对象,然后再在handleGoodsOrder方法里完成订单和数据库库存扣减的业务。

全都配置好后再来测试一下:

最终测试:

先用postman测试接口:http://localhost:8081/goods/buyGoodByUserId/2/1

结果:

{
  "code": 200,
  "data": "buy one success"
}

查看一下数据库goods_order表:

    id  user_id  goods_id  
------  -------  ----------
     7        1           2

goods表:

    id   stock  
------  --------
     1       998
     2        99

再来查看一下缓存:
在这里插入图片描述

在这里插入图片描述

发现没问题!

再将redis里的用户购买记录去掉,在数据库里删除goods_order里的数据,将goods里物品id=2的库存恢复到100,对100库存使用jmeter配置100线程对/goods/buyGoodByUserId/2/1测压:

对于数据库预测goods里的id为2的物品库存减1,goods_order里的数据新增一条,100线程只有1个通过。

上述的预测都成立,测试成功。

goods_order表:

    id  user_id  goods_id  
------  -------  ----------
     8        1           2

聚合报告:

在这里插入图片描述

结果确实是只有1个通过。

这里的线程还是不敢开太多的,因为毕竟队列还是使用java内部自带的队列,性能肯定是比不上市面上成熟了的消息队列。

三、反思与总结

对于这个业务也就是使用了异步来处理其他请求,而对于也就是将同一个业务的其他操作放到异步线程里执行,就这样。

对于消息队列,可以使用比较成熟的框架比如其他rabbitmq、卡夫卡等,这里就写得随便了,不管了,有点意思就可以了。

另外,对于其他的方面,比如对于多个物品的一人一单还是要优化的。这就不管了,因为本文重点在于异步实现。

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

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

相关文章

java易错题锦集系列五

接口中不能有构造方法&#xff0c;抽象类中可以有。抽象类中构造方法作用&#xff1a;初始化抽象类的成员&#xff1b;为继承它的子类使用 定义在同一个包&#xff08;package&#xff09;内的类是可以不经过import而直接相互使用 final修饰的方法可以被重载 但不能被重写 从…

论文阅读:Self-Supervised Monocular Depth Estimation with Internal Feature Fusion(DIFFNet)

中文标题&#xff1a;基于内部特征融合的自监督单目深度估计 创新点 参照HR-Net在网络上下采样的过程中充分利用语义信息。设计了一个注意力模块处理跳接。提出了一个扩展的评估策略&#xff0c;其中方法可以使用基准数据中的困难的情况进行进一步测试&#xff0c;以一种自我…

计算机科学导论笔记(一)

一、绪论 1.1 图灵模型 Alan Turing在1937年首次提出了一种通用计算设备的设想&#xff0c;他设想所有的计算都能在一种特殊的机器上执行&#xff0c;这就是现在所说的图灵机。但图灵机只是一种数学上的描述&#xff0c;并不是一种真正的机器。 1.1.1 数据处理器 在讨论图灵…

X和Ku波段小尺寸无线电设计

卫星通信、雷达和信号情报(SIGINT)领域的许多航空航天和防务电子系统早就要求使用一部分或全部X和Ku频段。随着这些应用转向更加便携的平台&#xff0c;如无人机(UAV)和手持式无线电等&#xff0c;开发在X和Ku波段工作&#xff0c;同时仍然保持极高性能水平的新型小尺寸、低功耗…

自动化测试——读写64位操作系统的注册表

非Web程序&#xff08;桌面程序&#xff09;的设置一般都存在注册表中。 给这些程序做自动化测试时&#xff0c; 需要经常要跟注册表打交道。 通过修改注册表来修改程序的设置。 本章介绍如何利用C#程序如何操作注册表&#xff0c; 特别是如何操作64位操作系统的注册表。 自动…

一文介绍Doris

文章目录一、架构介绍1.名词解释2.FE(Frontend)3.BE&#xff08;Backend&#xff09;4.元数据结构二、存储介绍1.DataPage2.Footer信息3.index pages三、索引介绍1.Ordinal Index(一级索引)2.Short Key Index 索引3.ZoneMap Index 索引4.BloomFilter索引5.Bitmap Index 索引6.索…

tensor常用代码

1.创建一个自定义形状的tensor&#xff0c;元素类型为int&#xff0c;并为随机数 a torch.randint(1, 10, size[4,2]) # 元素为1-10之间的随机数 2.将tensor中&#xff0c;元素类型改为float b a.float() b a.double() 3.在tensor的最外层增加一个维度 (tensor[None]) …

FATE数据上传、读取、训练、保存

fate如何安装&#xff1f;本文续这篇文章。 背景 fate是一个服务&#xff0c;还原联邦学习&#xff0c;所以分client和host两种身份&#xff0c;一般来说用户都是client&#xff0c;用户想要上传自己的数据&#xff0c;合并他人数据最终获得一个更好的模型&#xff0c;所以要…

织梦文章无图自动出图配图插件支持采集

织梦文章无图自动出图配图插件的优点 1、提高文章的可读性和吸引力&#xff1a;插入图片可以丰富文章的内容和形式&#xff0c;增强读者的阅读体验和吸引力&#xff0c;提高文章的点击率和转化率。 2、节省时间和精力&#xff1a;手动添加图片需要花费大量时间和精力去寻找和…

浅谈cocos2dx渲染方式

场景的渲染 Node:visit 其作用是遍历整个场景渲染树。 部分代码如下 if(!_children.empty()) {sortAllChildren();// draw children zOrder < 0for(auto size _children.size(); i < size; i){auto node _children.at(i);if (node && node->_localZOrder…

HU4056H耐压高达28V,具有电源OVP功能的1A单节锂离子电池线性充电IC

产品概述 HU4056H是一款完整的采用恒定电流/恒定电压的高压、大电流、单节锂离子电池线性充电 IC。最高耐压可达 28V&#xff0c; 6.5V 自动过压保护&#xff0c;充电电流可达 1A。 由于采用了内部 PMOSFET 架构&#xff0c;加上防倒充电路&#xff0c;所以不需要外部隔离二…

【博学谷学习记录】超强总结,用心分享丨人工智能 机器学习 集成学习错题总结

目录题目1&#xff1a;下面关于提升树的说法哪个是正确的&#xff1f;题目2&#xff1a;下面关于随机森林和梯度提升集成方法的说法哪个是正确的&#xff1f;集成学习主要有哪几种框架&#xff1f;工作过程是&#xff1f;题目1&#xff1a;下面关于提升树的说法哪个是正确的&am…

亿发软件:钉钉移动ERP业务在线,审批、管理更方便!

钉钉系统是企业级智能移动办公平台&#xff0c;平台覆盖大中小微各量级企业&#xff0c;帮助中国企业移动办公管理。企业无需复杂的部署操作&#xff0c;在对应的功能制定流程和相关负责人即可。 亿发企业ERP管理系统于2022年与钉钉系统做了对接&#xff0c;提供一站式的企业管…

VIF-Benchmark: All infrare and visible image fusion method in one framework

VIF_Benchmark Github 地址: https://github.com/Linfeng-Tang/VIF-Benchmark 完整Project下载地址&#xff1a;https://download.csdn.net/download/fovever_/87514164 我们把所有主流的基于深度学习的红外和可见光图像融合方法都集成在了这个框架中。 这些方法包括&#xff1…

MM32开发教程(LED灯)

文章目录前言一、MM32介绍和STM32的区别二、板载LED灯原理图三、代码编写总结前言 今天将为大家介绍一款性能高体积小的MM32&#xff0c;这款开发板出自百问网团队。他就是灵动的MM32F3273&#xff0c;他体积非常小便于携带。 有128KB的SRAM、512KB的Flash、而且还支持双TypeC…

Mutual-Structure for Joint Filtering

以前的联合/引导滤波器将参考图像中的结构信息直接传输到目标图像&#xff0c;它的主要缺点&#xff1a;两个图像中可能存在完全不同的边缘。简单地将所有图像传递给目标可能会出错。 作者对结构不一致性问题&#xff0c;提出了相互结构的概念&#xff0c;以增强基于目标图像和…

【项目管理】始于需求,而终于需求的最终落地

每个产品都是需要一系列需求的慢慢搭建&#xff0c;并且需求对于一个产品来说是非常重要的&#xff1b;我们对需求进行分配以及执行&#xff0c;需要一整个团队的配合以及执行&#xff0c;才可以最终达到一个好的效果&#xff1b; 项目一般是由一系列的需求组成的&#xff0c;需…

548、RocketMQ详细入门教程系列 -【消息队列之 RocketMQ (二)】 2023.02.28

目录一、Java 访问 RocketMQ 实例1.1 引入依赖1.2 消息生产者1.3 消息消费者1.4 启动 Name Server1.5 启动 Broker1.6 运行 Consumer1.7 运行 Producer二、参考链接一、Java 访问 RocketMQ 实例 RocketMQ 目前支持 Java、C、Go 三种语言访问&#xff0c;按惯例以 Java 语言为例…

SQL数据库权限管理-10个数据库角色

为便于管理数据库中的权限&#xff0c;SQL 数据库提供了服务器角色、数据库角色、用户等来划分不同用户拥有的权限差异。今天给大家介绍数据库角色对应的权限。 数据库级角色 存在两种类型的数据库级角色&#xff1a; 数据库中预定义的“固定数据库角色”可以创建的“用户定…

CSO面对面丨中核华辉刘博:应对大型央国企数字化转型道路上必须攻克的安全难题

“极致”&#xff0c;一直是大型央国企网络安全工作建设追求的目标。随着我国数字化转型全面走深向实&#xff0c;网络安全风险、数据管理、层出不穷的网络攻击&#xff0c;为各领域大型央国企数字化转型带来了更多的挑战。如何充分发挥优势、携手各方构筑网络安全屏障、提升安…