溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

發(fā)布時間:2022-03-31 10:54:13 來源:億速云 閱讀:991 作者:iii 欄目:開發(fā)技術(shù)

這篇“java怎么實現(xiàn)高性能的秒殺系統(tǒng)”文章的知識點大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“java怎么實現(xiàn)高性能的秒殺系統(tǒng)”文章吧。

首先來看看最終架構(gòu)圖:

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

先簡單根據(jù)這個圖談下請求的流轉(zhuǎn),因為后面不管怎么改進,這些都是不變的:

  • 前端請求進入 Web 層,對應(yīng)的代碼就是 Controller。

  • 之后將真正的庫存校驗、下單等請求發(fā)往 Service 層,其中 RPC 調(diào)用依然采用的 Dubbo,只是更新為***版本。

  • Service 層再對數(shù)據(jù)進行落地,下單完成。

***制

拋開秒殺這個場景來說,正常的一個下單流程可以簡單分為以下幾步:

  • 校驗庫存

  • 扣庫存

  • 創(chuàng)建訂單

  • 支付

基于上文的架構(gòu),我們有了以下實現(xiàn),先看看實際項目的結(jié)構(gòu):

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

還是和以前一樣:

  • 提供出一個 API 用于 Service 層實現(xiàn),以及 Web 層消費。

  • Web 層簡單來說就是一個 Spring MVC。

  • Service 層則是真正的數(shù)據(jù)落地。

  • SSM-SECONDS-KILL-ORDER-CONSUMER 則是后文會提到的 Kafka 消費。

數(shù)據(jù)庫也是只有簡單的兩張表模擬下單:

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 '創(chuàng)建時間',   PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=utf8;

Web 層 Controller 實現(xiàn):

@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 作為一個消費者調(diào)用看 OrderService 提供出來的 Dubbo 服務(wù)。

Service 層, OrderService 實現(xiàn),首先是對 API 的實現(xiàn)(會在 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);     } }

這里只是簡單調(diào)用了 DBOrderService 中的實現(xiàn),DBOrderService 才是真正的數(shù)據(jù)落地,也就是寫數(shù)據(jù)庫了。

DBOrderService 實現(xiàn):

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);          //創(chuàng)建訂單         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;     }          }

預(yù)先初始化了 10 條庫存。手動調(diào)用下 createWrongOrder/1 接口發(fā)現(xiàn):

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

庫存表

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

訂單表

一切看起來都沒有問題,數(shù)據(jù)也正常。但是當(dāng)用 JMeter 并發(fā)測試時:

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

測試配置是:300 個線程并發(fā)。測試兩輪來看看數(shù)據(jù)庫中的結(jié)果:

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

請求都響應(yīng)成功,庫存確實也扣完了,但是訂單卻生成了 124 條記錄。這顯然是典型的超賣現(xiàn)象。

其實現(xiàn)在再去手動調(diào)用接口會返回庫存不足,但為時晚矣。

樂觀鎖更新

怎么來避免上述的現(xiàn)象呢?最簡單的做法自然是樂觀鎖了,來看看具體實現(xiàn):

其實其他的都沒怎么改,主要是 Service 層:

@Override    public int createOptimisticOrder(int sid) throws Exception {         //校驗庫存        Stock stock = checkStock(sid);         //樂觀鎖更新庫存        saleStockOptimistic(stock);         //創(chuàng)建訂單        int id = createOrder(stock);         return id;    }     private void saleStockOptimistic(Stock stock) {        int count = stockService.updateStockByOptimistic(stock);        if (count == 0){            throw new RuntimeException("并發(fā)更新庫存失敗") ;        }    }

對應(yīng)的 XML:

<update id="updateByOptimistic" parameterType="com.crossoverJie.seconds.kill.pojo.Stock">        update stock        <set>            sale = sale + 1,            version = version + 1,        </set>         WHERE id = #{id,jdbcType=INTEGER}        AND version = #{version,jdbcType=INTEGER}     </update>

同樣的測試條件,我們再進行上面的測試 /createOptimisticOrder/1:

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

這次發(fā)現(xiàn)無論是庫存訂單都是 OK 的。

查看日志發(fā)現(xiàn):

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

很多并發(fā)請求會響應(yīng)錯誤,這就達到了效果。

提高吞吐量

為了進一步提高秒殺時的吞吐量以及響應(yīng)效率,這里的 Web 和 Service 都進行了橫向擴展:

  • Web 利用 Nginx 進行負(fù)載。

  • Service 也是多臺應(yīng)用。

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

再用 JMeter 測試時可以直觀的看到效果。

由于我是在阿里云的一臺小水管服務(wù)器進行測試的,加上配置不高、應(yīng)用都在同一臺,所以并沒有完全體現(xiàn)出性能上的優(yōu)勢( Nginx  做負(fù)載轉(zhuǎn)發(fā)時候也會增加額外的網(wǎng)絡(luò)消耗)。

Shell 腳本實現(xiàn)簡單的 CI

由于應(yīng)用多臺部署之后,手動發(fā)版測試的痛苦相信經(jīng)歷過的都有體會。

這次并沒有精力去搭建完整的 CICD,只是寫了一個簡單的腳本實現(xiàn)了自動化部署,希望給這方面沒有經(jīng)驗的同學(xué)帶來一點啟發(fā)。

構(gòu)建 Web:

#!/bin/bash  # 構(gòu)建 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"

構(gòu)建 Service:

# 構(gòu)建服務(wù)提供者  #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"

之后每當(dāng)我有更新,只需要執(zhí)行這兩個腳本就可以幫我自動構(gòu)建。都是最基礎(chǔ)的 Linux 命令,相信大家都看得明白。

樂觀鎖更新 + 分布式限流

上文的結(jié)果看似沒有問題,其實還差得遠呢。這里只是模擬了 300 個并發(fā)沒有問題,但是當(dāng)請求達到了 3000,3W,300W 呢?

雖說可以橫向擴展支撐更多的請求,但是能不能利用最少的資源解決問題呢?

仔細分析下會發(fā)現(xiàn):假設(shè)我的商品一共只有 10 個庫存,那么無論你多少人來買其實最終也最多只有 10 人可以下單成功。所以其中會有 99%  的請求都是無效的。

大家都知道:大多數(shù)應(yīng)用數(shù)據(jù)庫都是壓倒駱駝的***一根稻草。通過 Druid 的監(jiān)控來看看之前請求數(shù)據(jù)庫的情況:

因為 Service 是兩個應(yīng)用:

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

數(shù)據(jù)庫也有 20 多個連接。怎么樣來優(yōu)化呢?其實很容易想到的就是分布式限流。

我們將并發(fā)控制在一個可控的范圍之內(nèi),然后快速失敗這樣就能***程度的保護系統(tǒng)。

①distributed-redis-tool ?v1.0.3

因為加上該組件之后所有的請求都會經(jīng)過 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;     } }

這里構(gòu)建器改用了 JedisConnectionFactory,所以得配合 Spring 來一起使用。

并在初始化時顯示傳入 Redis 是以集群方式部署還是單機(強烈建議集群,限流之后對 Redis 還是有一定的壓力)。

③限流實現(xiàn)

既然 API 更新了,實現(xiàn)自然也要修改:

/**   * 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 應(yīng)用得采用 @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 端就沒什么更新了,依然是采用的樂觀鎖更新數(shù)據(jù)庫。

再壓測看下效果 /createOptimisticLimitOrderByRedis/1:

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

首先是看結(jié)果沒有問題,再看數(shù)據(jù)庫連接以及并發(fā)請求數(shù)都有明顯的下降。

樂觀鎖更新+分布式限流+Redis 緩存

仔細觀察 Druid 監(jiān)控數(shù)據(jù)發(fā)現(xiàn)這個 SQL 被多次查詢:

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

其實這是實時查詢庫存的 SQL,主要是為了在每次下單之前判斷是否還有庫存。

這也是個優(yōu)化點。這種數(shù)據(jù)我們完全可以放在內(nèi)存中,效率比在數(shù)據(jù)庫要高很多。

由于我們的應(yīng)用是分布式的,所以堆內(nèi)緩存顯然不合適,Redis 就非常適合。

  • 這次主要改造的是 Service 層:

  • 每次查詢庫存時走 Redis。

  • 扣庫存時更新 Redis。

需要提前將庫存信息寫入 Redis。(手動或者程序自動都可以)

主要代碼如下:

@Override   public int createOptimisticOrderUseRedis(int sid) throws Exception {       //檢驗庫存,從 Redis 獲取       Stock stock = checkStockByRedis(sid);        //樂觀鎖更新庫存 以及更新 Redis       saleStockOptimisticByRedis(stock);        //創(chuàng)建訂單       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;   }         /**    * 樂觀鎖更新數(shù)據(jù)庫 還要更新 Redis    * @param stock    */   private void saleStockOptimisticByRedis(Stock stock) {       int count = stockService.updateStockByOptimistic(stock);       if (count == 0){           throw new RuntimeException("并發(fā)更新庫存失敗") ;       }       //自增       redisTemplate.opsForValue().increment(RedisKeysConstant.STOCK_SALE + stock.getId(),1) ;       redisTemplate.opsForValue().increment(RedisKeysConstant.STOCK_VERSION + stock.getId(),1) ;   }

壓測看看實際效果 /createOptimisticLimitOrderByRedis/1:

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

java怎么實現(xiàn)高性能的秒殺系統(tǒng)

***發(fā)現(xiàn)數(shù)據(jù)沒問題,數(shù)據(jù)庫的請求與并發(fā)也都下來了。

樂觀鎖更新+分布式限流+Redis 緩存+Kafka 異步

***的優(yōu)化還是想如何來再次提高吞吐量以及性能的。我們上文所有例子其實都是同步請求,完全可以利用同步轉(zhuǎn)異步來提高性能啊。

這里我們將寫訂單以及更新庫存的操作進行異步化,利用 Kafka 來進行解耦和隊列的作用。

每當(dāng)一個請求通過了限流到達了 Service 層通過了庫存校驗之后就將訂單信息發(fā)給 Kafka ,這樣一個請求就可以直接返回了。

消費程序再對數(shù)據(jù)進行入庫落地。因為異步了,所以最終需要采取回調(diào)或者是其他提醒的方式提醒用戶購買完成。

這里代碼較多就不貼了,消費程序其實就是把之前的 Service 層的邏輯重寫了一遍,不過采用的是 Spring Boot。

以上就是關(guān)于“java怎么實現(xiàn)高性能的秒殺系統(tǒng)”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對大家有幫助,若想了解更多相關(guān)的知識內(nèi)容,請關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI