java单机秒杀扛1万并发方案和代码

news2024/11/24 7:29:18

我们先来看普通的加锁加事务秒杀性能,

说明:

1.这里的秒杀业务执行一次耗时100毫秒

2.电脑配置16g内存 4核8线程 cpu i7 7代,数据库连接池max=20 


    @RequestMapping("/purchase2")
    public ResultJson purchase2( Long productId){
        int userId = new Random().nextInt(10000);
        UserInfo.setUserId(userId);
        RLock lock = redissonClient.getLock("LOCK");
        lock.lock();
        OrderVO purchase = null;
        try {
            purchase = tOrderService.purchase2(productId);
            if(purchase.isSuccess()){
                return new ResultJson<>(200, "成功下单", purchase);
            }else{
                return new ResultJson<>(500, purchase.getMsg(), purchase);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        lock.unlock();
        return new ResultJson<>(500, "下单失败", null);
    }

100个商品 8000并发秒杀

1. 普通加锁加事务 100个商品8000并发,入库100条需要13秒,吞吐量一秒9次,这里jmeter只跑了2000个线程就停止了因为实在是太慢了不想等了

 

1000个商品,8000并发秒杀

1. 普通加锁加事务 1000个商品8000并发,入库1000条需要199秒这里jmeter只跑了2000个线程就停止了因为实在是太慢了不想等了

以下是我优化的方案,自定义了Seckill注解和ProductIdMark注解采用aop处理

    @RequestMapping("/purchase")
    @Seckill(tableName = "products", inventoryColumn = "prod_num" ,productIdColumn="id" )
    public ResultJson purchase(@ProductIdMark Long productId){
        OrderVO purchase = null;
        try {
            purchase = tOrderService.purchase(productId);
            if(purchase.isSuccess()){
                return new ResultJson<>(200, "成功下单", purchase);
            }else{
                return new ResultJson<>(500, purchase.getMsg(), purchase);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new ResultJson<>(500, "下单失败", null);
}

100个商品 8000并发秒杀

1. 普通加锁加事务 100个商品8000并发,入库100条需要4秒,吞吐量一秒1034次

 

1000个商品 8000并发秒杀

1. 普通加锁加事务 1000个商品8000并发,入库1000条需要7秒,吞吐量一秒615次

 

分析

100商品优化前: 入库13秒,吞吐9/秒

100商品优化后: 入库4秒,吞吐1034/秒

1000商品优化前:入库199秒,吞吐9/秒

1000商品优化后:入库7秒,吞吐615/秒

可以看得出差别还是很大的

实现思路:所有请求被aop拦截,aop将用户存储到redis中,1000商品购买率是70%所以redis只要存储1500个用户的id即可,多余的直接返回商品售空,接下来轮到1500个用户竞争1000个商品,预热时候把1000个商品分成20份也就是每份50个商品,将20份商品存入库中作为lockName锁使用,同步轮训获取数据库中的lockName,获取到对应的lockName即可对该记录的库存进行扣减操作大致流程就是这样

下面是aop的主要实现:

@Aspect
@Configuration
public class SeckillAspect {

    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private ProductsSubsectionService productsSubsectionService;

    @Pointcut("@annotation(seckill)")
    public void pointCut(Seckill seckill) {
    }

    private final String infoKey= "info:productId:";
    private final String indexKey= "index:productId:";
    private final Long time=60*10L;
    @Around("pointCut(seckill)")
    public Object around(ProceedingJoinPoint joinPoint,Seckill seckill) throws Throwable {
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        String appName = className +":"+ methodName;
        Long productId = getProductId(joinPoint);
        //模拟用户
        int userId = new Random().nextInt(100000);
        UserInfo.setUserId(userId);
        //初始化商品数据
        init(appName,productId);

        if(stopRun(appName,productId)){
            return new ResultJson<>(500, "商品售空", null);
        }

        //防止同一个用户使用外挂疯狂点击
        RMap<Integer, Integer> map = redissonClient.getMap(appName+":userClicks");
        RLock lockClicks = redissonClient.getLock("LOCK:"+appName+":5");
        try {
            lockClicks.lock();
            map.put(userId,map.get(userId)+1);
        }finally {
            lockClicks.unlock();
        }

        if(map.get(userId) > 1){
            return new ResultJson<>(500, "请勿重复提交", null);
        }

        RLock lock = redissonClient.getLock("LOCK:"+appName+":2");
        lock.lock();
        if(isUpdatePrimaryTable(appName,productId)){
            updatePrimaryTable(seckill,productId);
        }
        String lockName = getLockName(appName, productId);
        RLock lock4 = redissonClient.getLock(lockName);
        try {
            lock4.lock();
            lock.unlock();
            ProductsSubsection productsSubsection = productsSubsectionService.queryByLockMark(lockName);
            if(productsSubsection.getNumber()==0){
                return new ResultJson<>(500, "商品售空", null);
            }
            Object proceed = joinPoint.proceed();
            productsSubsection.setNumber(productsSubsection.getNumber()-1);
            productsSubsectionService.update(productsSubsection);
            return proceed;
        }finally {
            lock4.unlock();
        }
    }

    private Long getProductId(ProceedingJoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        //一维数组是注解位置,二维数组是注解个数
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        Long productId=null;
        boolean noProductId = true;
        for (int i = 0; i < parameterAnnotations.length; i++) {
            Annotation[] annotation = parameterAnnotations[i];
            for (Annotation var : annotation) {
                if (var.annotationType().equals(ProductIdMark.class)) {
                    productId= (Long) args[i];
                    noProductId = false;
                    break;
                }
            }
        }
        if(noProductId){
            throw new RuntimeException("形参未指定注解:ProductId");
        }
        return productId;
    }

    private boolean init(String appName, Long productId){

        if(!redissonClient.getMap(appName).isEmpty()){
            //已初始化
            return false;
        }
        RLock lock = redissonClient.getLock("LOCK:"+appName+":2");
        try {
            lock.lock();
            if(redissonClient.getMap(appName).isEmpty()){
                List<ProductsSubsection> list = productsSubsectionService.queryByProdId(productId);
                int inventory =0 ;
                LinkedHashSet<String> locks = new LinkedHashSet<>();
                for (ProductsSubsection subsection : list) {
                    inventory+=subsection.getNumber();
                    locks.add(subsection.getLockMark());
                }
                CommodityInfo info = new CommodityInfo();
                info.setLockNames(locks);
                info.setInventory(inventory);
                RMap<String, Object> map = redissonClient.getMap(appName);
                map.expire(time, TimeUnit.SECONDS);
                String key= infoKey+ productId;
                String key2= indexKey+ productId;
                map.put(key,info);
                map.put(key2,0);
            }
        }finally {
            lock.unlock();
        }
        return true;
    }

    //统计用户点击数
    private Integer userClicks(String appName,int size){
        RMap<Integer, Integer> user = redissonClient.getMap(appName+":userClicks");
        user.expire(time, TimeUnit.SECONDS);

        RLock lock = redissonClient.getLock("LOCK:"+appName+":3");
        int userLength = user.size();
        if(userLength >= size){
            return user.size();
        }
        try {
            lock.lock();
            if(user.size() < size){
                //初始化用户点击次数
                int userId = UserInfo.getUserId();
                Integer clicks = user.get(userId);
                if( clicks == null){
                    user.put(userId,0);
                }else{
                    user.put(userId,clicks+1);
                }
            }
        } finally {
            lock.unlock();
        }
        return user.size();
    }

    private boolean stopRun(String appName,Long productId){
        RMap<String, Object> map = redissonClient.getMap(appName);
        map.expire(time, TimeUnit.SECONDS);
        CommodityInfo info = (CommodityInfo) map.get( infoKey+ productId);
        double size = info.getProbability() *  info.getInventory();
        RMap<Integer, Integer> user = redissonClient.getMap(appName+":userClicks");
        user.expire(time, TimeUnit.SECONDS);

        if(userClicks(appName, (int) size) >= size && !user.containsKey(UserInfo.getUserId())){
            return true;
        }
        return false;
    }

    private String getLockName(String appName,Long productId){
        String key= infoKey+ productId;
        String key2= indexKey+ productId;
        RMap<Object, Object> map = redissonClient.getMap(appName);
        map.expire(time, TimeUnit.SECONDS);
        CommodityInfo  info = (CommodityInfo) map.get(key);
        Integer index = (Integer) map.get(key2);
        List<String> lockNamesList = new ArrayList<>(info.getLockNames());
        map.put(key2,index+1);
        return lockNamesList.get(index % lockNamesList.size());
    }



    private boolean isUpdatePrimaryTable(String appName,Long productId){
        String key= infoKey+ productId;
        String key2= indexKey+ productId;
        RMap<Object, Object> map = redissonClient.getMap(appName);
        map.expire(time, TimeUnit.SECONDS);
        CommodityInfo  info = (CommodityInfo) map.get(key);
        Integer index = (Integer) map.get(key2);
        return index==info.getInventory();
    }

    //更新主表
    private void updatePrimaryTable(Seckill seckill,Long productId){
        // 获取注解的值
        String tableName = seckill.tableName();
        String inventoryColumn = seckill.inventoryColumn();
        String productIdColumn = seckill.productIdColumn();
        System.out.println("开始更新"+tableName+"主表数据");
        productsSubsectionService.updatePrimaryTable(tableName,inventoryColumn,productIdColumn,productId);
    }
}

项目地址:

通用所有秒杀业务,只要商品表中有库存,商品id即可

使用规则:

1.创建分段表

CREATE TABLE `products_subsection` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `prod_id` bigint(20) DEFAULT NULL,
  `number` int(11) DEFAULT NULL,
  `lock_mark` varchar(20) DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4

2.在Controller的方法中使用Seckill注解和@ProductIdMark

//商品表名,库存字段名,商品id名
@Seckill(tableName = "products", inventoryColumn = "prod_num" ,productIdColumn="id" )
//用于表示商品id
@ProductIdMark

访问后的效果

架构图

 

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

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

相关文章

2 常见模块库(2)

2.5 复用器与分路器模块 Mux是一种用于将多个信号组合成一个信号的模块。Mux模块的名称来源于多路复用器&#xff08;Multiplexer&#xff09;。 使用Mux可以将多个输入信号组合成一个向量或矩阵&#xff0c;以便在模型中传递和处理。Mux模块可以接受任意数量的输入信号&#x…

Visio Studio 2017利用Qt插件开发Qt应用的安装方法

Visio Studio 2017利用Qt插件开发Qt应用的安装方法 1 安装Visio Studio 20172 安装QT3 在Visio Studio 2017中安装Qt插件 本教程介绍如何利用Visio Studio 2017&#xff0c;开发Qt.5.14.2的Qt应用 1 安装Visio Studio 2017 链接&#xff1a;https://pan.baidu.com/s/1t9j1fFj3…

Linux --- 简介、安装

一、Linux简介 1.1、主流操作系统 不同领域的主流操作系统&#xff0c;主要分为以下这么几类&#xff1a; 桌面操作系统、服务器操作系统、移动设备操作 系统、嵌入式操作系统。接下来&#xff0c;这几个领域中&#xff0c;代表性的操作系统是那些? 1、桌面操作系统 2、服务…

2023年农牧行业数字化:7大CRM软件、5大场景盘点

目录 一、5大业务场景能力&#xff0c;解密农牧行业持续增长秘籍 1、营销获客 2、客户管理 3、商机管理 4、生态“互联”能力 5、业财一体化 二、农牧行业企业CRM选型指南 1、SaaS模式或私有部署 2、是否具有行业成功“经验” 3、可扩展性 4、以营销为主题的体系建设…

【MySQL】基础介绍及表操作

目录 1.MySQL是什么&#xff1f; 2.为什么要学习数据库呢&#xff1f; 内存和硬盘的区别 3.数据库基本操作 1.创建数据库 2.使用数据库 3.删除数据库&#xff08;慎用&#xff09; 4.查看警告信息 5.查询当前数据可服务使用的编码集 7.表操作 1.创建一个学生成绩表 2…

Spring Cloud微服务网关Zuul过滤链和整合OAuth2+JWT入门实战

一、Spring Cloud Zuul 过滤链 1.1 工作原理 Zuul的核心逻辑是由一系列的Filter来实现的&#xff0c;他们能够在进行HTTP请求或者相应的时候执行相关操作。Zuul Filter的主要特性有一下几点&#xff1a; Filter的类型&#xff1a;Filter的类型决定了它在Filter链中的执行顺序…

解决Ubunt20.04安装Sogou输入法失败进不去桌面 及 中文输入法安装

目录解决Ubunt20.04安装Sogou输入法失败进不去桌面中文输入法安装解决wps无法输入中文解决Ubunt20.04安装Sogou输入法失败进不去桌面 问题&#xff1a; Ubuntu20.04 安装了 fcitx 和 sogou 输入法&#xff1b;键盘输入法系统由 IBus 改成了 fcitx&#xff1b;重启后可以出现登…

文心一言 VS chatgpt (11)-- 算法导论2.3 7题 3.1 4~5题

四、2 ^ (n1)O(2 ^ n)成立吗? 2^(2n)O(2 ^ n)成立吗? 文心一言&#xff1a; chatgpt&#xff1a; 第一个式子不成立&#xff0c;第二个式子成立。 2 ^ (n1) O(2 ^ n) 该式不成立。按照大O符号的定义&#xff0c;如果存在正常数c和n0&#xff0c;使得n>n0时&#xf…

旋转矩阵与欧拉角

其他相关的内容网上很多&#xff0c;这里就简单记录一下不同欧拉角分解顺序时&#xff0c;对应的角度怎么计算 #include <opencv2/opencv.hpp> #include <iostream>using namespace cv; using namespace std; #define PI acos(-1)void getEulerAngles(Mat& ro…

Spring项目整合Minio分布式的对象存储系统

文章目录 安装Docker安装Docker-compose安装 SpringBoot集成引入依赖初始化客户端存储桶的CRUD存储桶的文件操作存储桶生命周期配置 安装 Docker安装 docker run -p 9000:9000 -p 9090:9090 \--nethost \--name minio \-d --restartalways \-e "MINIO_ACCESS_KEYadmin&q…

01-Linux-磁盘分区与目录配置

1. 主引导纪录和磁盘分区表 1.1 MBR分区表 启动引导程序记录区与分区表都放在磁盘的第一个扇区&#xff08;512B&#xff09; 由于分区表仅占 64B&#xff0c;因此最多能有四组记录区&#xff0c;每组记录区记录了该区段的起始与结束的柱面号码。 缺点如下&#xff1a; 操作…

设计模式-创建型模式之工厂方法模式(Factory Method Pattern)

3.工厂方法模式(Factory Method Pattern)3.1. 模式动机现在对该系统进行修改&#xff0c;不再设计一个按钮工厂类来统一负责所有产品的创建&#xff0c;而是将具体按钮的创建过程交给专门的工厂子类去完成&#xff0c;我们先定义一个抽象的按钮工厂类&#xff0c;再定义具体的工…

vue项目导入excel成功后下载导入结果(后端返回的list数组)

需求&#xff1a; 点击批量导入按钮&#xff0c;弹出弹窗。 下载模板如图二 上传后&#xff0c;如果有错误&#xff0c;会弹出提示&#xff0c;如图三 点击查看失败原因&#xff0c;会自动下载失败的excel如图四。 请求参数和返回结果 1. vue项目导出表格功能实现步骤 np…

十七、市场活动明细:添加备注

功能需求 用户在市场活动明细页面,输入备注内容,点击"保存"按钮,完成添加市场活动备注的功能. *备注内容不能为空 *添加成功之后,清空输入框,刷新备注列表 *添加失败,提示信息,输入框不清空,列表也不刷新 功能分析 流程图 代码实现 一、ActivityRemarkMapper 1.Ac…

笔记-Samba服务器的安装与配置

引言 代码编写我们是在Windows系统下的VS Code来编辑&#xff0c;但是代码在虚拟机的Ubuntu系统中&#xff0c;所以我们要先实现如何将在Ubuntu下的项目映射到Windows系统中&#xff0c;这时我们可以使用到samba服务器。 一、安装samba服务器 sudo apt-get install samba我这里…

【瑞吉外卖】003 -- 后台退出功能开发

本文章为对 黑马程序员Java项目实战《瑞吉外卖》的学习记录 目录 一、需求分析 二、代码开发 三、功能测试 四、分析后台页面构成和效果展示 1、Vue & Element 2、iframe 一、需求分析 前端页面分析&#xff1a; 前端页面&#xff0c;点击事件 点击事件 logout()&#x…

使用vscode写UML图

文章目录 环境配置关键字多图注释Title多行title图注头部或尾部添加注释多行header/footer放大率类图接口抽象类枚举 类型关系泛化关系&#xff0c;箭头指向父类实现关系&#xff0c;箭头指向接口依赖关系&#xff0c;箭头指向被依赖关系关联关系&#xff0c;指向被拥有者可以双…

QML控件--DelayButton

文章目录 一、控件基本信息二、控件使用三、属性四、信号 一、控件基本信息 Import Statement&#xff1a;import QtQuick.Controls 2.14 Since&#xff1a;Qt 5.9 Inherits&#xff1a;AbstractButton 二、控件使用 DelayButton是一个延时按钮&#xff0c;需要长按才能触发&…

WPF教程(二)--Application WPF程序启动方式

1.Application介绍 WPF与WinForm一样有一个 Application对象来进行一些全局的行为和操作&#xff0c;并且每个 Domain &#xff08;应用程序域&#xff09;中仅且只有一个 Application 实例存在。和 WinForm 不同的是WPF Application默认由两部分组成 : App.xaml 和 App.xaml.…

SpringBoot单元测试断言 assertions

断言 断言&#xff08;assertions&#xff09;是测试方法中的核心部分&#xff0c;用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别&#xff1a; 1、简单断言 2、数组断言 通过 …