当前位置:

首页 > 编程开发 > java怎么实现高性能的秒杀系统

java怎么实现高性能的秒杀系统

首先来看看最终架构图:先简单根据这个图谈下请求的流转,因为后面不管怎么改进,这些都是不变的:前端请求进入Web层,对应的代码就是Controller。之后将真正的库存校验、下单等请求发往Service层,其中RPC调用依然采用的Dubbo,只是更新为***版本。Service层再对数据进行落地,下单完成。***制抛开秒杀这个场景来说,正常的一个下单流程可以简单分为以下几步:校验库存扣库存创建订单支付基于上文的架构,我们有了以下实现,先看看实际项目的结构:还是和以前一样:提供出一个API用于Service层

本文目录

    首先来看看最终架构图:

    java怎么实现高性能的秒杀系统

    先简单根据这个图谈下请求的流转,因为后面不管怎么改进,这些都是不变的:

    • 前端请求进入 Web 层,对应的代码就是 Controller。

    • 之后将真正的库存校验、下单等请求发往 Service 层,其中 RPC 调用依然采用的 Dubbo,只是更新为***版本。

    • Service 层再对数据进行落地,下单完成。

    ***制

    抛开秒杀这个场景来说,正常的一个下单流程可以简单分为以下几步:

    • 校验库存

    • 扣库存

    • 创建订单

    • 支付

    基于上文的架构,我们有了以下实现,先看看实际项目的结构:

    java怎么实现高性能的秒杀系统

    还是和以前一样:

    • 提供出一个 API 用于 Service 层实现,以及 Web 层消费。

    • Web 层简单来说就是一个 Spring MVC。

    • Service 层则是真正的数据落地。

    • SSM-SECONDS-KILL-ORDER-CONSUMER 则是后文会提到的 Kafka 消费。

    数据库也是只有简单的两张表模拟下单:

    CREATE TABLE `stock` (   `id` int(11) unsigned NOT NULL AUTO_INCREMENT,   `name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称',   `count` int(11) NOT NULL COMMENT '库存',   `sale` int(11) NOT NULL COMMENT '已售',   `version` int(11) NOT NULL COMMENT '乐观锁,版本号',   PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;   CREATE TABLE `stock_order` (   `id` int(11) unsigned NOT NULL AUTO_INCREMENT,   `sid` int(11) NOT NULL COMMENT '库存ID',   `name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名称',   `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',   PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=utf8;

    Web 层 Controller 实现:

    @Autowired    private StockService stockService;     @Autowired    private OrderService orderService;     @RequestMapping("/createWrongOrder/{sid}")    @ResponseBody    public String createWrongOrder(@PathVariable int sid) {        logger.info("sid=[{}]", sid);        int id = 0;        try {            id = orderService.createWrongOrder(sid);        } catch (Exception e) {            logger.error("Exception",e);        }        return String.valueOf(id);    }

    其中 Web 作为一个消费者调用看 OrderService 提供出来的 Dubbo 服务。

    Service 层, OrderService 实现,首先是对 API 的实现(会在 API 提供出接口):

    @Service public class OrderServiceImpl implements OrderService {      @Resource(name = "DBOrderService")     private com.crossoverJie.seconds.kill.service.OrderService orderService ;      @Override     public int createWrongOrder(int sid) throws Exception {         return orderService.createWrongOrder(sid);     } }

    这里只是简单调用了 DBOrderService 中的实现,DBOrderService 才是真正的数据落地,也就是写数据库了。

    DBOrderService 实现:

    Transactional(rollbackFor = Exception.class) @Service(value = "DBOrderService") public class OrderServiceImpl implements OrderService {     @Resource(name = "DBStockService")     private com.crossoverJie.seconds.kill.service.StockService stockService;      @Autowired     private StockOrderMapper orderMapper;      @Override     public int createWrongOrder(int sid) throws Exception{          //校验库存         Stock stock = checkStock(sid);          //扣库存         saleStock(stock);          //创建订单         int id = createOrder(stock);          return id;     }      private Stock checkStock(int sid) {         Stock stock = stockService.getStockById(sid);         if (stock.getSale().equals(stock.getCount())) {             throw new RuntimeException("库存不足");         }         return stock;     }      private int saleStock(Stock stock) {         stock.setSale(stock.getSale() + 1);         return stockService.updateStockById(stock);     }      private int createOrder(Stock stock) {         StockOrder order = new StockOrder();         order.setSid(stock.getId());         order.setName(stock.getName());         int id = orderMapper.insertSelective(order);         return id;     }          }

    预先初始化了 10 条库存。手动调用下 createWrongOrder/1 接口发现:

    java怎么实现高性能的秒杀系统

    库存表

    java怎么实现高性能的秒杀系统

    订单表

    一切看起来都没有问题,数据也正常。但是当用 JMeter 并发测试时:

    java怎么实现高性能的秒杀系统

    测试配置是:300 个线程并发。测试两轮来看看数据库中的结果:

    java怎么实现高性能的秒杀系统

    java怎么实现高性能的秒杀系统

    java怎么实现高性能的秒杀系统

    java怎么实现高性能的秒杀系统

    请求都响应成功,库存确实也扣完了,但是订单却生成了 124 条记录。这显然是典型的超卖现象。

    其实现在再去手动调用接口会返回库存不足,但为时晚矣。

    乐观锁更新

    怎么来避免上述的现象呢?最简单的做法自然是乐观锁了,来看看具体实现:

    其实其他的都没怎么改,主要是 Service 层:

    @Override    public int createOptimisticOrder(int sid) throws Exception {         //校验库存        Stock stock = checkStock(sid);         //乐观锁更新库存        saleStockOptimistic(stock);         //创建订单        int id = createOrder(stock);         return id;    }     private void saleStockOptimistic(Stock stock) {        int count = stockService.updateStockByOptimistic(stock);        if (count == 0){            throw new RuntimeException("并发更新库存失败") ;        }    }

    对应的 XML:

            update stock                    sale = sale + 1,            version = version + 1,                 WHERE id = #{id,jdbcType=INTEGER}        AND version = #{version,jdbcType=INTEGER}     

    同样的测试条件,我们再进行上面的测试 /createOptimisticOrder/1:

    java怎么实现高性能的秒杀系统

    java怎么实现高性能的秒杀系统

    java怎么实现高性能的秒杀系统

    这次发现无论是库存订单都是 OK 的。

    查看日志发现:

    java怎么实现高性能的秒杀系统

    很多并发请求会响应错误,这就达到了效果。

    提高吞吐量

    为了进一步提高秒杀时的吞吐量以及响应效率,这里的 Web 和 Service 都进行了横向扩展:

    • Web 利用 Nginx 进行负载。

    • Service 也是多台应用。

    java怎么实现高性能的秒杀系统

    java怎么实现高性能的秒杀系统

    再用 JMeter 测试时可以直观的看到效果。

    由于我是在阿里云的一台小水管服务器进行测试的,加上配置不高、应用都在同一台,所以并没有完全体现出性能上的优势( Nginx  做负载转发时候也会增加额外的网络消耗)。

    Shell 脚本实现简单的 CI

    由于应用多台部署之后,手动发版测试的痛苦相信经历过的都有体会。

    这次并没有精力去搭建完整的 CICD,只是写了一个简单的脚本实现了自动化部署,希望给这方面没有经验的同学带来一点启发。

    构建 Web:

    #!/bin/bash  # 构建 web 消费者  #read appname  appname="consumer" echo "input="$appname  PID=$(ps -ef | grep $appname | grep -v grep | awk '{print $2}')  # 遍历杀掉 pid for var in ${PID[@]}; do     echo "loop pid= $var"     kill -9 $var done  echo "kill $appname success"  cd ..  git pull  cd SSM-SECONDS-KILL  mvn -Dmaven.test.skip=true clean package  echo "build war success"  cp /home/crossoverJie/SSM/SSM-SECONDS-KILL/SSM-SECONDS-KILL-WEB/target/SSM-SECONDS-KILL-WEB-2.2.0-SNAPSHOT.war /home/crossoverJie/tomcat/tomcat-dubbo-consumer-8083/webapps echo "cp tomcat-dubbo-consumer-8083/webapps ok!"  cp /home/crossoverJie/SSM/SSM-SECONDS-KILL/SSM-SECONDS-KILL-WEB/target/SSM-SECONDS-KILL-WEB-2.2.0-SNAPSHOT.war /home/crossoverJie/tomcat/tomcat-dubbo-consumer-7083-slave/webapps echo "cp tomcat-dubbo-consumer-7083-slave/webapps ok!"  sh /home/crossoverJie/tomcat/tomcat-dubbo-consumer-8083/bin/startup.sh echo "tomcat-dubbo-consumer-8083/bin/startup.sh success"  sh /home/crossoverJie/tomcat/tomcat-dubbo-consumer-7083-slave/bin/startup.sh echo "tomcat-dubbo-consumer-7083-slave/bin/startup.sh success"  echo "start $appname success"

    构建 Service:

    # 构建服务提供者  #read appname  appname="provider"  echo "input="$appname   PID=$(ps -ef | grep $appname | grep -v grep | awk '{print $2}')  #if [ $? -eq 0 ]; then #    echo "process id:$PID" #else #    echo "process $appname not exit" #    exit #fi  # 遍历杀掉 pid for var in ${PID[@]}; do     echo "loop pid= $var"     kill -9 $var done  echo "kill $appname success"   cd ..  git pull  cd SSM-SECONDS-KILL  mvn -Dmaven.test.skip=true clean package  echo "build war success"  cp /home/crossoverJie/SSM/SSM-SECONDS-KILL/SSM-SECONDS-KILL-SERVICE/target/SSM-SECONDS-KILL-SERVICE-2.2.0-SNAPSHOT.war /home/crossoverJie/tomcat/tomcat-dubbo-provider-8080/webapps  echo "cp tomcat-dubbo-provider-8080/webapps ok!"  cp /home/crossoverJie/SSM/SSM-SECONDS-KILL/SSM-SECONDS-KILL-SERVICE/target/SSM-SECONDS-KILL-SERVICE-2.2.0-SNAPSHOT.war /home/crossoverJie/tomcat/tomcat-dubbo-provider-7080-slave/webapps  echo "cp tomcat-dubbo-provider-7080-slave/webapps ok!"  sh /home/crossoverJie/tomcat/tomcat-dubbo-provider-8080/bin/startup.sh echo "tomcat-dubbo-provider-8080/bin/startup.sh success"  sh /home/crossoverJie/tomcat/tomcat-dubbo-provider-7080-slave/bin/startup.sh echo "tomcat-dubbo-provider-8080/bin/startup.sh success"  echo "start $appname success"

    之后每当我有更新,只需要执行这两个脚本就可以帮我自动构建。都是最基础的 Linux 命令,相信大家都看得明白。

    乐观锁更新 + 分布式限流

    上文的结果看似没有问题,其实还差得远呢。这里只是模拟了 300 个并发没有问题,但是当请求达到了 3000,3W,300W 呢?

    虽说可以横向扩展支撑更多的请求,但是能不能利用最少的资源解决问题呢?

    仔细分析下会发现:假设我的商品一共只有 10 个库存,那么无论你多少人来买其实最终也最多只有 10 人可以下单成功。所以其中会有 99%  的请求都是无效的。

    大家都知道:大多数应用数据库都是压倒骆驼的***一根稻草。通过 Druid 的监控来看看之前请求数据库的情况:

    因为 Service 是两个应用:

    java怎么实现高性能的秒杀系统

    java怎么实现高性能的秒杀系统

    java怎么实现高性能的秒杀系统

    数据库也有 20 多个连接。怎么样来优化呢?其实很容易想到的就是分布式限流。

    我们将并发控制在一个可控的范围之内,然后快速失败这样就能***程度的保护系统。

    ①distributed-redis-tool ⬆v1.0.3

    因为加上该组件之后所有的请求都会经过 Redis,所以对 Redis 资源的使用也是要非常小心。

    ②API 更新

    修改之后的 API 如下:

    @Configuration public class RedisLimitConfig {      private Logger logger = LoggerFactory.getLogger(RedisLimitConfig.class);      @Value("${redis.limit}")     private int limit;       @Autowired     private JedisConnectionFactory jedisConnectionFactory;      @Bean     public RedisLimit build() {         RedisLimit redisLimit = new RedisLimit.Builder(jedisConnectionFactory, RedisToolsConstant.SINGLE)                 .limit(limit)                 .build();          return redisLimit;     } }

    这里构建器改用了 JedisConnectionFactory,所以得配合 Spring 来一起使用。

    并在初始化时显示传入 Redis 是以集群方式部署还是单机(强烈建议集群,限流之后对 Redis 还是有一定的压力)。

    ③限流实现

    既然 API 更新了,实现自然也要修改:

    /**   * limit traffic   * @return if true   */  public boolean limit() {       //get connection      Object connection = getConnection();       Object result = limitRequest(connection);       if (FAIL_CODE != (Long) result) {          return true;      } else {          return false;      }  }   private Object limitRequest(Object connection) {      Object result = null;      String key = String.valueOf(System.currentTimeMillis() / 1000);      if (connection instanceof Jedis){          result = ((Jedis)connection).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit)));          ((Jedis) connection).close();      }else {          result = ((JedisCluster) connection).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit)));          try {              ((JedisCluster) connection).close();          } catch (IOException e) {              logger.error("IOException",e);          }      }      return result;  }   private Object getConnection() {      Object connection ;      if (type == RedisToolsConstant.SINGLE){          RedisConnection redisConnection = jedisConnectionFactory.getConnection();          connection = redisConnection.getNativeConnection();      }else {          RedisClusterConnection clusterConnection = jedisConnectionFactory.getClusterConnection();          connection = clusterConnection.getNativeConnection() ;      }      return connection;  }

    如果是原生的 Spring 应用得采用 @SpringControllerLimit(errorCode=200) 注解。

    实际使用如下,Web 端:

    /**      * 乐观锁更新库存 限流      * @param sid      * @return      */     @SpringControllerLimit(errorCode = 200)     @RequestMapping("/createOptimisticLimitOrder/{sid}")     @ResponseBody     public String createOptimisticLimitOrder(@PathVariable int sid) {         logger.info("sid=[{}]", sid);         int id = 0;         try {             id = orderService.createOptimisticOrder(sid);         } catch (Exception e) {             logger.error("Exception",e);         }         return String.valueOf(id);     }

    Service 端就没什么更新了,依然是采用的乐观锁更新数据库。

    再压测看下效果 /createOptimisticLimitOrderByRedis/1:

    java怎么实现高性能的秒杀系统

    java怎么实现高性能的秒杀系统

    java怎么实现高性能的秒杀系统

    java怎么实现高性能的秒杀系统

    java怎么实现高性能的秒杀系统

    首先是看结果没有问题,再看数据库连接以及并发请求数都有明显的下降。

    乐观锁更新+分布式限流+Redis 缓存

    仔细观察 Druid 监控数据发现这个 SQL 被多次查询:

    java怎么实现高性能的秒杀系统

    其实这是实时查询库存的 SQL,主要是为了在每次下单之前判断是否还有库存。

    这也是个优化点。这种数据我们完全可以放在内存中,效率比在数据库要高很多。

    由于我们的应用是分布式的,所以堆内缓存显然不合适,Redis 就非常适合。

    • 这次主要改造的是 Service 层:

    • 每次查询库存时走 Redis。

    • 扣库存时更新 Redis。

    需要提前将库存信息写入 Redis。(手动或者程序自动都可以)

    主要代码如下:

    @Override   public int createOptimisticOrderUseRedis(int sid) throws Exception {       //检验库存,从 Redis 获取       Stock stock = checkStockByRedis(sid);        //乐观锁更新库存 以及更新 Redis       saleStockOptimisticByRedis(stock);        //创建订单       int id = createOrder(stock);       return id ;   }     private Stock checkStockByRedis(int sid) throws Exception {       Integer count = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_COUNT + sid));       Integer sale = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_SALE + sid));       if (count.equals(sale)){           throw new RuntimeException("库存不足 Redis currentCount=" + sale);       }       Integer version = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_VERSION + sid));       Stock stock = new Stock() ;       stock.setId(sid);       stock.setCount(count);       stock.setSale(sale);       stock.setVersion(version);        return stock;   }         /**    * 乐观锁更新数据库 还要更新 Redis    * @param stock    */   private void saleStockOptimisticByRedis(Stock stock) {       int count = stockService.updateStockByOptimistic(stock);       if (count == 0){           throw new RuntimeException("并发更新库存失败") ;       }       //自增       redisTemplate.opsForValue().increment(RedisKeysConstant.STOCK_SALE + stock.getId(),1) ;       redisTemplate.opsForValue().increment(RedisKeysConstant.STOCK_VERSION + stock.getId(),1) ;   }

    压测看看实际效果 /createOptimisticLimitOrderByRedis/1:

    java怎么实现高性能的秒杀系统

    java怎么实现高性能的秒杀系统

    java怎么实现高性能的秒杀系统

    java怎么实现高性能的秒杀系统

    ***发现数据没问题,数据库的请求与并发也都下来了。

    乐观锁更新+分布式限流+Redis 缓存+Kafka 异步

    ***的优化还是想如何来再次提高吞吐量以及性能的。我们上文所有例子其实都是同步请求,完全可以利用同步转异步来提高性能啊。

    这里我们将写订单以及更新库存的操作进行异步化,利用 Kafka 来进行解耦和队列的作用。

    每当一个请求通过了限流到达了 Service 层通过了库存校验之后就将订单信息发给 Kafka ,这样一个请求就可以直接返回了。

    消费程序再对数据进行入库落地。因为异步了,所以最终需要采取回调或者是其他提醒的方式提醒用户购买完成。

    这里代码较多就不贴了,消费程序其实就是把之前的 Service 层的逻辑重写了一遍,不过采用的是 Spring Boot。

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Java
    相关文章 更多
    Java单元测试入门:使用JUnit 5编写第一个测试用例
    Java单元测试入门:使用JUnit 5编写第一个测试用例

    面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

    Java变量未初始化报错解析:局部变量与成员变量默认值差异
    Java变量未初始化报错解析:局部变量与成员变量默认值差异

    深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

    详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

    Java中final与static final常量的定义及初始化规则
    Java中final与static final常量的定义及初始化规则

    深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

    Java抽象方法与接口方法的区别及选型建议
    Java抽象方法与接口方法的区别及选型建议

    深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

    Java实现HTTP请求与JSON解析的完整代码示例
    Java实现HTTP请求与JSON解析的完整代码示例

    本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

    Java递归出现StackOverflowError的原因分析及栈空间优化方案
    Java递归出现StackOverflowError的原因分析及栈空间优化方案

    详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

    Java空指针异常(NPE)的代码排查与修复实例
    Java空指针异常(NPE)的代码排查与修复实例

    针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

    selenium上传文件教程本地文件路径设置方法
    selenium上传文件教程本地文件路径设置方法

    深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

    针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程
    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版
    macOS

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版
    Windows

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。