搜尋
首頁Javajava教程如何使用Java實現高效的秒殺系統?

首先來看看最終架構圖:

如何使用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>        update stock        <set>            sale = sale + 1,            version = version + 1,        </set>         WHERE id = #{id,jdbcType=INTEGER}        AND version = #{version,jdbcType=INTEGER}     </update>

同樣的測試條件,我們再進行上面的測試/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。

以上是如何使用Java實現高效的秒殺系統?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文轉載於:亿速云。如有侵權,請聯絡admin@php.cn刪除
如何將Maven或Gradle用於高級Java項目管理,構建自動化和依賴性解決方案?如何將Maven或Gradle用於高級Java項目管理,構建自動化和依賴性解決方案?Mar 17, 2025 pm 05:46 PM

本文討論了使用Maven和Gradle進行Java項目管理,構建自動化和依賴性解決方案,以比較其方法和優化策略。

如何使用適當的版本控制和依賴項管理創建和使用自定義Java庫(JAR文件)?如何使用適當的版本控制和依賴項管理創建和使用自定義Java庫(JAR文件)?Mar 17, 2025 pm 05:45 PM

本文使用Maven和Gradle之類的工具討論了具有適當的版本控制和依賴關係管理的自定義Java庫(JAR文件)的創建和使用。

如何使用咖啡因或Guava Cache等庫在Java應用程序中實現多層緩存?如何使用咖啡因或Guava Cache等庫在Java應用程序中實現多層緩存?Mar 17, 2025 pm 05:44 PM

本文討論了使用咖啡因和Guava緩存在Java中實施多層緩存以提高應用程序性能。它涵蓋設置,集成和績效優勢,以及配置和驅逐政策管理最佳PRA

如何將JPA(Java持久性API)用於具有高級功能(例如緩存和懶惰加載)的對象相關映射?如何將JPA(Java持久性API)用於具有高級功能(例如緩存和懶惰加載)的對象相關映射?Mar 17, 2025 pm 05:43 PM

本文討論了使用JPA進行對象相關映射,並具有高級功能,例如緩存和懶惰加載。它涵蓋了設置,實體映射和優化性能的最佳實踐,同時突出潛在的陷阱。[159個字符]

Java的類負載機制如何起作用,包括不同的類載荷及其委託模型?Java的類負載機制如何起作用,包括不同的類載荷及其委託模型?Mar 17, 2025 pm 05:35 PM

Java的類上載涉及使用帶有引導,擴展程序和應用程序類負載器的分層系統加載,鏈接和初始化類。父代授權模型確保首先加載核心類別,從而影響自定義類LOA

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
1 個月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
1 個月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
1 個月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.聊天命令以及如何使用它們
1 個月前By尊渡假赌尊渡假赌尊渡假赌

熱工具

Atom編輯器mac版下載

Atom編輯器mac版下載

最受歡迎的的開源編輯器

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

WebStorm Mac版

WebStorm Mac版

好用的JavaScript開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)