日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關咨詢
選擇下列產(chǎn)品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關閉右側工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
簡單幾步,設計一個高性能的秒殺系統(tǒng)!

 本次采用循序漸進的方式逐步提高性能達到并發(fā)秒殺的效果,文章較長,請準備好瓜子板凳!

之前在 Java-Interview 中提到過秒殺架構的設計,這次基于其中的理論簡單實現(xiàn)了一下。

本文所有涉及的代碼:

  • https://github.com/crossoverJie/SSM
  • https://github.com/crossoverJie/distributed-redis-tool

首先來看看最終架構圖:

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

  • 前端請求進入 Web 層,對應的代碼就是 Controller。
  • 之后將真正的庫存校驗、下單等請求發(fā)往 Service 層,其中 RPC 調用依然采用的 Dubbo,只是更新為***版本。
  • Service 層再對數(shù)據(jù)進行落地,下單完成。

***制

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

  • 校驗庫存
  • 扣庫存
  • 創(chuàng)建訂單
  • 支付

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

還是和以前一樣:

  • 提供出一個 API 用于 Service 層實現(xiàn),以及 Web 層消費。
  • Web 層簡單來說就是一個 Spring MVC。
  • Service 層則是真正的數(shù)據(jù)落地。
  • SSM-SECONDS-KILL-ORDER-CONSUMER 則是后文會提到的 Kafka 消費。

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

 
 
 
 
  1. CREATE TABLE `stock` (
  2.   `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  3.   `name` varchar(50) NOT NULL DEFAULT '' COMMENT '名稱',
  4.   `count` int(11) NOT NULL COMMENT '庫存',
  5.   `sale` int(11) NOT NULL COMMENT '已售',
  6.   `version` int(11) NOT NULL COMMENT '樂觀鎖,版本號',
  7.   PRIMARY KEY (`id`)
  8. ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
  9. CREATE TABLE `stock_order` (
  10.   `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  11.   `sid` int(11) NOT NULL COMMENT '庫存ID',
  12.   `name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名稱',
  13.   `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '創(chuàng)建時間',
  14.   PRIMARY KEY (`id`)
  15. ) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=utf8;

Web 層 Controller 實現(xiàn):

 
 
 
 
  1. @Autowired
  2.    private StockService stockService;
  3.    @Autowired
  4.    private OrderService orderService;
  5.    @RequestMapping("/createWrongOrder/{sid}")
  6.    @ResponseBody
  7.    public String createWrongOrder(@PathVariable int sid) {
  8.        logger.info("sid=[{}]", sid);
  9.        int id = 0;
  10.        try {
  11.            id = orderService.createWrongOrder(sid);
  12.        } catch (Exception e) {
  13.            logger.error("Exception",e);
  14.        }
  15.        return String.valueOf(id);
  16.    }

其中 Web 作為一個消費者調用看 OrderService 提供出來的 Dubbo 服務。

Service 層, OrderService 實現(xiàn),首先是對 API 的實現(xiàn)(會在 API 提供出接口):

 
 
 
 
  1. @Service
  2. public class OrderServiceImpl implements OrderService {
  3.     @Resource(name = "DBOrderService")
  4.     private com.crossoverJie.seconds.kill.service.OrderService orderService ;
  5.     @Override
  6.     public int createWrongOrder(int sid) throws Exception {
  7.         return orderService.createWrongOrder(sid);
  8.     }
  9. }

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

DBOrderService 實現(xiàn):

 
 
 
 
  1. Transactional(rollbackFor = Exception.class)
  2. @Service(value = "DBOrderService")
  3. public class OrderServiceImpl implements OrderService {
  4.     @Resource(name = "DBStockService")
  5.     private com.crossoverJie.seconds.kill.service.StockService stockService;
  6.     @Autowired
  7.     private StockOrderMapper orderMapper;
  8.     @Override
  9.     public int createWrongOrder(int sid) throws Exception{
  10.         //校驗庫存
  11.         Stock stock = checkStock(sid);
  12.         //扣庫存
  13.         saleStock(stock);
  14.         //創(chuàng)建訂單
  15.         int id = createOrder(stock);
  16.         return id;
  17.     }
  18.     private Stock checkStock(int sid) {
  19.         Stock stock = stockService.getStockById(sid);
  20.         if (stock.getSale().equals(stock.getCount())) {
  21.             throw new RuntimeException("庫存不足");
  22.         }
  23.         return stock;
  24.     }
  25.     private int saleStock(Stock stock) {
  26.         stock.setSale(stock.getSale() + 1);
  27.         return stockService.updateStockById(stock);
  28.     }
  29.     private int createOrder(Stock stock) {
  30.         StockOrder order = new StockOrder();
  31.         order.setSid(stock.getId());
  32.         order.setName(stock.getName());
  33.         int id = orderMapper.insertSelective(order);
  34.         return id;
  35.     }        
  36. }

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

庫存表

訂單表

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

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

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

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

樂觀鎖更新

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

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

 
 
 
 
  1. @Override
  2.    public int createOptimisticOrder(int sid) throws Exception {
  3.        //校驗庫存
  4.        Stock stock = checkStock(sid);
  5.        //樂觀鎖更新庫存
  6.        saleStockOptimistic(stock);
  7.        //創(chuàng)建訂單
  8.        int id = createOrder(stock);
  9.        return id;
  10.    }
  11.    private void saleStockOptimistic(Stock stock) {
  12.        int count = stockService.updateStockByOptimistic(stock);
  13.        if (count == 0){
  14.            throw new RuntimeException("并發(fā)更新庫存失敗") ;
  15.        }
  16.    }

對應的 XML:

 
 
 
 
  1.        update stock
  2.        
  3.            sale = sale + 1,
  4.            version = version + 1,
  5.        
  6.        WHERE id = #{id,jdbcType=INTEGER}
  7.        AND version = #{version,jdbcType=INTEGER}
  8.    

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

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

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

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

提高吞吐量

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

  • Web 利用 Nginx 進行負載。
  • Service 也是多臺應用。

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

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

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

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

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

構建 Web:

 
 
 
 
  1. #!/bin/bash
  2. # 構建 web 消費者
  3. #read appname
  4. appname="consumer"
  5. echo "input="$appname
  6. PID=$(ps -ef | grep $appname | grep -v grep | awk '{print $2}')
  7. # 遍歷殺掉 pid
  8. for var in ${PID[@]};
  9. do
  10.     echo "loop pid= $var"
  11.     kill -9 $var
  12. done
  13. echo "kill $appname success"
  14. cd ..
  15. git pull
  16. cd SSM-SECONDS-KILL
  17. mvn -Dmaven.test.skip=true clean package
  18. echo "build war success"
  19. 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
  20. echo "cp tomcat-dubbo-consumer-8083/webapps ok!"
  21. 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
  22. echo "cp tomcat-dubbo-consumer-7083-slave/webapps ok!"
  23. sh /home/crossoverJie/tomcat/tomcat-dubbo-consumer-8083/bin/startup.sh
  24. echo "tomcat-dubbo-consumer-8083/bin/startup.sh success"
  25. sh /home/crossoverJie/tomcat/tomcat-dubbo-consumer-7083-slave/bin/startup.sh
  26. echo "tomcat-dubbo-consumer-7083-slave/bin/startup.sh success"
  27. echo "start $appname success"

構建 Service:

 
 
 
 
  1. # 構建服務提供者
  2. #read appname
  3. appname="provider"
  4. echo "input="$appname
  5. PID=$(ps -ef | grep $appname | grep -v grep | awk '{print $2}')
  6. #if [ $? -eq 0 ]; then
  7. #    echo "process id:$PID"
  8. #else
  9. #    echo "process $appname not exit"
  10. #    exit
  11. #fi
  12. # 遍歷殺掉 pid
  13. for var in ${PID[@]};
  14. do
  15.     echo "loop pid= $var"
  16.     kill -9 $var
  17. done
  18. echo "kill $appname success"
  19. cd ..
  20. git pull
  21. cd SSM-SECONDS-KILL
  22. mvn -Dmaven.test.skip=true clean package
  23. echo "build war success"
  24. 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
  25. echo "cp tomcat-dubbo-provider-8080/webapps ok!"
  26. 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
  27. echo "cp tomcat-dubbo-provider-7080-slave/webapps ok!"
  28. sh /home/crossoverJie/tomcat/tomcat-dubbo-provider-8080/bin/startup.sh
  29. echo "tomcat-dubbo-provider-8080/bin/startup.sh success"
  30. sh /home/crossoverJie/tomcat/tomcat-dubbo-provider-7080-slave/bin/startup.sh
  31. echo "tomcat-dubbo-provider-8080/bin/startup.sh success"
  32. echo "start $appname success"

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

樂觀鎖更新 + 分布式限流

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

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

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

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

因為 Service 是兩個應用:

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

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

①distributed-redis-tool ?v1.0.3

為此還對 https://github.com/crossoverJie/distributed-redis-tool 進行了小小的升級。

因為加上該組件之后所有的請求都會經(jīng)過 Redis,所以對 Redis 資源的使用也是要非常小心。

②API 更新

修改之后的 API 如下:

 
 
 
 
  1. @Configuration
  2. public class RedisLimitConfig {
  3.     private Logger logger = LoggerFactory.getLogger(RedisLimitConfig.class);
  4.     @Value("${redis.limit}")
  5.     private int limit;
  6.     @Autowired
  7.     private JedisConnectionFactory jedisConnectionFactory;
  8.     @Bean
  9.     public RedisLimit build() {
  10.         RedisLimit redisLimit = new RedisLimit.Builder(jedisConnectionFactory, RedisToolsConstant.SINGLE)
  11.                 .limit(limit)
  12.                 .build();
  13.         return redisLimit;
  14.     }
  15. }

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

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

③限流實現(xiàn)

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

 
 
 
 
  1. /**
  2.   * limit traffic
  3.   * @return if true
  4.   */
  5.  public boolean limit() {
  6.      //get connection
  7.      Object connection = getConnection();
  8.      Object result = limitRequest(connection);
  9.      if (FAIL_CODE != (Long) result) {
  10.          return true;
  11.      } else {
  12.          return false;
  13.      }
  14.  }
  15.  private Object limitRequest(Object connection) {
  16.      Object result = null;
  17.      String key = String.valueOf(System.currentTimeMillis() / 1000);
  18.      if (connection instanceof Jedis){
  19.          result = ((Jedis)connection).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit)));
  20.          ((Jedis) connection).close();
  21.      }else {
  22.          result = ((JedisCluster) connection).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit)));
  23.          try {
  24.              ((JedisCluster) connection).close();
  25.          } catch (IOException e) {
  26.              logger.error("IOException",e);
  27.          }
  28.      }
  29.      return result;
  30.  }
  31.  private Object getConnection() {
  32.      Object connection ;
  33.      if (type == RedisToolsConstant.SINGLE){
  34.          RedisConnection redisConnection = jedisConnectionFactory.getConnection();
  35.          connection = redisConnection.getNativeConnection();
  36.      }else {
  37.          RedisClusterConnection clusterConnection = jedisConnectionFactory.getClusterConnection();
  38.          connection = clusterConnection.getNativeConnection() ;
  39.      }
  40.      return connection;
  41.  }

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

實際使用如下,Web 端:

 
 
 
 
  1. /**
  2.      * 樂觀鎖更新庫存 限流
  3.      * @param sid
  4.      * @return
  5.      */
  6.     @SpringControllerLimit(errorCode = 200)
  7.     @RequestMapping("/createOptimisticLimitOrder/{sid}")
  8.     @ResponseBody
  9.     public String createOptimisticLimitOrder(@PathVariable int sid) {
  10.         logger.info("sid=[{}]", sid);
  11.         int id = 0;
  12.         try {
  13.             id = orderService.createOptimisticOrder(sid);
  14.         } catch (Exception e) {
  15.             logger.error("Exception",e);
  16.         }
  17.         return String.valueOf(id);
  18.     }

Service 端就沒什么更新了,依然是采用的樂觀鎖更新數(shù)據(jù)庫。

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

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

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

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

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

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

由于我們的應用是分布式的,所以堆內緩存顯然不合適,Redis 就非常適合。

  • 這次主要改造的是 Service 層:
  • 每次查詢庫存時走 Redis。
  • 扣庫存時更新 Redis。

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

主要代碼如下:

 
 
 
 
  1. @Override
  2.   public int createOptimisticOrderUseRedis(int sid) throws Exception {
  3.       //檢驗庫存,從 Redis 獲取
  4.       Stock stock = checkStockByRedis(sid);
  5.       //樂觀鎖更新庫存 以及更新 Redis
  6.       saleStockOptimisticByRedis(stock);
  7.       //創(chuàng)建訂單
  8.       int id = createOrder(stock);
  9.       return id ;
  10.   }
  11.   private Stock checkStockByRedis(int sid) throws Exception {
  12.       Integer count = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_COUNT + sid));
  13.       Integer sale = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_SALE + sid));
  14.       if (count.equals(sale)){
  15.           throw new RuntimeException("庫存不足 Redis currentCount=" + sale);
  16.       }
  17.       Integer version = Integer.parseInt(redisTemplate.opsForValue().get(RedisKeysConstant.STOCK_VERSION + sid));
  18.       Stock stock = new Stock() ;
  19.       stock.setId(sid);
  20.       stock.setCount(count);
  21.       stock.setSale(sale);
  22.       stock.setVersion(version);
  23.       return stock;
  24.   }    
  25.   /**
  26.    * 樂觀鎖更新數(shù)據(jù)庫 還要更新 Redis
  27.    * @param stock
  28.    */
  29.   private void saleStockOptimisticByRedis(Stock stock) {
  30.       int count = stockService.updateStockByOptimistic(stock);
  31.       if (count == 0){
  32.           throw new RuntimeException("并發(fā)更新庫存失敗") ;
  33.       }
  34.       //自增
  35.       redisTemplate.opsForValue().increment(RedisKeysConstant.STOCK_SALE + stock.getId(),1) ;
  36.       redisTemplate.opsForValue().increment(RedisKeysConstant.STOCK_VERSION + stock.getId(),1) ;
  37.   }    

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

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

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

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

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

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

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

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

總結

經(jīng)過上面的一頓優(yōu)化總結起來無非就是以下幾點:

  • 盡量將請求攔截在上游。
  • 還可以根據(jù) UID 進行限流。
  • ***程度的減少請求落到 DB。
  • 多利用緩存。
  • 同步操作異步化。
  • Fail Fast,盡早失敗,保護應用。

碼字不易,這應該是我寫過字數(shù)最多的了,想想當年高中 800 字的作文都憋不出來,可想而知是有多難得了。

以上內容歡迎討論,感興趣的朋友可以看下:

 
 
 
 
  1. https://github.com/crossoverJie/SSM/tree/master/SSM-SECONDS-KILL/SSM-SECONDS-KILL-ORDER-CONSUMER

當前題目:簡單幾步,設計一個高性能的秒殺系統(tǒng)!
當前URL:http://m.5511xx.com/article/dhpdedj.html