比較Java與Node.js的併發性和效能- maxant

banq發表於2021-11-24

想象一個簡單的市場,對同一產品感興趣的買家和賣家聚集在一起進行交易。對於市場上的每個產品,對該產品感興趣的買家可以形成一個有序的佇列,按照“先到先得”的原則進行排序。然後,每個買家可以接近最便宜的賣家並進行交易,以賣家指定的價格從賣家那裡購買儘可能多的產品。如果沒有賣家以足夠低的價格提供產品,買家可以讓開站到一邊,讓下一個買家有機會進行交易。一旦所有買家都有機會進行交易,當市場上的所有產品都經過迴圈後,整個過程可以重新開始,在滿意的買家和賣家離開後,新的迴圈開始。在網際網路時代,買家和賣家沒有理由不能坐在舒適的扶手椅上使用這種演算法在虛擬平臺上進行交易。事實上,這樣的交易平臺已經存在多年。
 
雖然很基本,但當用於構建基於計算機的交易引擎時,這類問題變得有趣。簡單的問題帶來挑戰:
市場如何跨多個核心擴充套件?
市場如何跨多臺機器擴充套件?
本質上,答案歸結為需要某種形式的併發性,以便這樣的交易引擎可以擴充套件。通常,我會使用執行池和synchronized 關鍵字來編寫基於 Java 的解決方案,以確保多個執行緒以有序的方式更新中心模型。但最近我開始嘗試使用 Node.js,這個平臺對解決上述問題很有趣,因為它是一個單執行緒非阻塞平臺。這個想法是程式設計師在設計和編寫演算法時沒有太多的理由,因為兩個執行緒可能想要同時訪問公共資料是沒有危險的。
  

NodeJs實現
我花時間用 JavaScript 對上述市場進行建模,交易功能如下(其餘部JavaScript 程式碼可以在這裡找到)。
 

  this.trade = function(){
        var self = this;
        var sales = [];
 
        var productsInMarket = this.getProductsInMarket().values();
...
        //trade each product in succession
        _.each(productsInMarket, function(productId){
            var soldOutOfProduct = false;
            logger.debug('trading product ' + productId);
            var buyersInterestedInProduct = self.getBuyersInterestedInProduct(productId);
            if(buyersInterestedInProduct.length === 0){
                logger.info('no buyers interested in product ' + productId);
            }else{
                _.each(buyersInterestedInProduct, function(buyer){
                    if(!soldOutOfProduct){
                        logger.debug('  buyer ' + buyer.name + ' is searching for product ' + productId);
                        //select the cheapest seller
                        var cheapestSeller = _.chain(self.sellers)
                                              .filter(function(seller){return seller.hasProduct(productId);})
                                              .sortBy(function(seller){return seller.getCheapestSalesOrder(productId).price;})
                                              .first()
                                              .value();
 
                        if(cheapestSeller){
                            logger.debug('    cheapest seller is ' + cheapestSeller.name);
                            var newSales = self.createSale(buyer, cheapestSeller, productId);
                            sales = sales.concat(newSales);
                            logger.debug('    sales completed');
                        }else{
                            logger.warn('    market sold out of product ' + productId);
                            soldOutOfProduct = true;
                        }
                    }
                });
            }
        });
 
        return sales;
    };


程式碼使用了Underscore.js庫,它提供了一堆有用的函式式助手,很像新增到Java 8 Streams 中的那些。下一步是建立一個交易引擎,它封裝了一個市場,如下面的程式碼片段所示, 透過刪除沒有合適的買家和賣家可以配對的超時銷售來準備第 1 行的市場;執行第3行的交易流程;註釋第 6 行的統計資料;並在第 8 行持續銷售。
 
prepareMarket(self.market, timeout);
 
        var sales = self.market.trade();
        logger.info('trading completed');
 
        noteMarketPricesAndVolumes(self.marketPrices, self.volumeRecords, sales);
 
        persistSale(sales, function(err){
            if(err) logger.warn(err);
            else {
                logger.info('persisting completed, notifying involved parties...');
                _.each(sales, function(sale){
                    if(sale.buyer.event) sale.buyer.event(exports.EventType.PURCHASE, sale);
                    if(sale.seller.event) sale.seller.event(exports.EventType.SALE, sale);
                });
            }
            ...
            setTimeout(loop, 0 + delay); //let the process handle other stuff too
            ...
        });
    }

到目前為止,我們還沒有看到任何真正有趣的程式碼,除了上面的第 8 行,銷售是持久的。Sales 被插入到一個表中,該表包含有關銷售 ID(自動遞增的主鍵)、產品 ID、銷售訂單 ID 和採購訂單 ID(來自程式)的索引。對persistSale(...)函式的呼叫會呼叫 MySQL 資料庫,所使用的庫在呼叫資料庫時使用非阻塞 I/O。它必須這樣做,因為在 Node.js 中,程式中沒有其他可用的執行緒以及所有內容在程式中執行會在等待資料庫插入結果時阻塞。實際發生的是 Node.js 程式觸發插入請求,其餘程式碼立即執行,直至完成。
如果您檢查JavaScript 程式碼的其餘部分,您會注意到實際上沒有其他程式碼在呼叫該persistSale(...)函式之後執行。那時,Node.js 會進入事件佇列並尋找其他事情要做。
為了使交易引擎有用,我決定在我的環境中將它構建為一個獨立的元件,並將其介面公開為一個簡單的 HTTP 服務。透過這種方式,我可以透過多種方式獲利,例如擁有一個可部署單元,該單元可以透過將其部署在叢集中的多個節點上來向外擴充套件,並使後端與我尚未建立的任何前端分離。命名的指令碼trading-engine-parent3.js依賴於一個名為express的小網路框架,該指令碼的相關部分如下所示:

logger.info('setting up HTTP server for receiving commands');
 
var express = require('express')
var app = express()
var id = 0;
app.get('/buy', function (req, res) {
    logger.info(id + ') buying "' + req.query.quantity + '" of "' + req.query.productId + '"');
    ...
});
app.get('/sell', function (req, res) {
    logger.info(id + ') selling "' + req.query.quantity + '" of "' + req.query.productId + '" at price "' + req.query.price + '"');
    ...
});
app.get('/result', function (req, res) {
    var key = parseInt(req.query.id);
    var r = results.get(key);
    if(r){
        results.delete(key);
        res.json(r);
    }else{
        res.json({msg: 'UNKNOWN OR PENDING'});
    }
});
 
var server = app.listen(3000, function () {
  logger.warn('Trading engine listening at http://%s:%s', host, port)
});


第 8 行和第 12 行呼叫引擎並分別新增採購訂單/銷售訂單。我們將很快檢查某事究竟如何。第 16 行顯示了我在設計中做出的一個重要選擇,即 HTTP 請求在等待交易訂單結果時不保持開啟狀態。最初我嘗試保持請求開啟,但在負載測試期間我遇到了經典的死鎖問題。市場包含訂單,但沒有匹配的產品,並且伺服器在其TCP 積壓填充後不會接受新請求(另請參見此處),因此其他客戶端無法建立新的採購和銷售訂單,因此市場沒有'不包含能讓銷售持續流動所必需的產品。
因此,讓我們回到保持交易銷售後發生的情況。由於持久化是非同步的,我們在前一個指令碼 (trading-engine-loop.js) 的第 8-20 行提供了一個回撥函式,該函式透過向買方/賣方傳送適當的事件(第 13-14 行)來處理結果setTimeout(loop, 0+delay)告訴 Node.jsloop在至少delay幾毫秒後執行該函式的呼叫。該setTimeout函式將這項工作放到事件佇列中。透過呼叫此函式,我們允許 Node.js 為已放置在事件佇列中的其他工作提供服務,例如新增採購或銷售訂單的 HTTP 請求,或者確實呼叫該loop函式以再次開始交易。
 
為此 Node.js 解決方案編寫的程式碼具有非阻塞非同步性質,因此確實不需要更多執行緒。除了……我們如何擴大程式並使用機器上的其他核心?Node.js 支援建立子程式,這樣做確實非常容易,如下面的程式碼片段所示。

// ////////////////
// Parent
// ////////////////
...
var cp = require('child_process');
...
//TODO use config to decide how many child processes to start
var NUM_KIDS = 2;
var PRODUCT_IDS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 
                   '10', '11', '12', '13', '14', '15', '16', '17', '18', '19',
                   ...
                  ];
 
var chunk = PRODUCT_IDS.length / NUM_KIDS;
var kids = new Map();
for (var i=0, j=PRODUCT_IDS.length; i<j; i+=chunk) {
    var n = cp.fork('./lib/trading-engine-child.js');
    n.on('message', messageFromChild);
    var temparray = PRODUCT_IDS.slice(i,i+chunk);
    logger.info('created child process for products ' + temparray);
    _.each(temparray, function(e){
        logger.debug('mapping productId "' + e + '" to child process ' + n.pid);
        kids.set(e, n);
    });
}
...

// ////////////////
// Child
// ////////////////
process.on('message', function(model) {
    logger.debug('received command: "' + model.command + '"');
    if(model.command == t.EventType.PURCHASE){
        var buyer = ...
        var po = new m.PurchaseOrder(model.what.productId, model.what.quantity, model.what.maxPrice, model.id);
        buyer.addPurchaseOrder(po);
    }else if(model.command == t.EventType.SALE){
        ...
    }else{
        var msg = 'Unknown command ' + model.command;
        process.send({id: model.id, err: msg});            
    }
});

第 5 行匯入用於處理子程式的 API,我們透過在第 14-25 行對產品 ID 進行分組來劃分市場。對於每個分割槽,我們啟動一個新的子程式(第 17 行)並在第 18 行註冊一個回撥,用於接收從子程式傳送回父程式的資料。我們將對子程式的引用貼上到以產品 ID 為鍵的對映中在第23行,使我們可以透過呼叫例如傳送的訊息:n.send(someObject)。如何簡單地傳送和接收物件以及它們如何作為(大概)JSON 傳輸是非常漂亮的——它與 Java 中的 RMI 呼叫非常相似。透過上述解決方案,交易引擎可以透過新增子程式來垂直擴充套件,也可以透過在多個節點上部署交易引擎父級(包括其 Web 伺服器)並使用負載均衡器將基於產品 ID 的請求分發到正確的節點處理該產品的交易。
如果您想知道買家是否可以出現在多個市場中,那麼答案當然是肯定的 - 市場是虛擬的,買家不受現實生活中物理位置的限制:-)
  

Java實現
 Java 解決方案是什麼樣的,它會如何執行?完整的Java 程式碼可在此處獲得
從市場及其trade()方法,Java 程式碼看起來類似於 JavaScript 版本,使用 Java 8 Streams 而不是 Underscore 庫。有趣的是,它在程式碼行數上幾乎相同,或者更主觀地說,可維護性。

public List<Sale> trade() {
    List<Sale> sales = new ArrayList<>();
    Set<String> productsInMarket = getProductsInMarket();
    collectMarketInfo();

    // trade each product in succession
    productsInMarket.stream()
        .forEach(productId -> {
            MutableBoolean soldOutOfProduct = new MutableBoolean(false);
            LOGGER.debug("trading product " + productId);
            List<Buyer> buyersInterestedInProduct = getBuyersInterestedInProduct(productId);
            if (buyersInterestedInProduct.size() == 0) {
                LOGGER.info("no buyers interested in product " + productId);
            } else {
                buyersInterestedInProduct.forEach(buyer -> {
                    if (soldOutOfProduct.isFalse()) {
                        LOGGER.debug("  buyer " + buyer.getName() + " is searching for product " + productId);
                        // select the cheapest seller
                        Optional<Seller> cheapestSeller = sellers.stream()
                            .filter(seller -> { return seller.hasProduct(productId);})
                            .sorted((s1, s2) -> 
                                Double.compare(s1.getCheapestSalesOrder(productId).getPrice(),
                                               s2.getCheapestSalesOrder(productId).getPrice()))
                            .findFirst();
                        if (cheapestSeller.isPresent()) {
                            LOGGER.debug("    cheapest seller is " + cheapestSeller.get().getName());
                            List<Sale> newSales = createSale(buyer, cheapestSeller.get(), productId);
                            sales.addAll(newSales);
                            LOGGER.debug("    sales completed");
                        } else {
                            LOGGER.warn("    market sold out of product " + productId);
                            soldOutOfProduct.setTrue();
                        }
                    }
                });
            }
        });
    return sales;
}


正如我幾年前在我的書中所寫的那樣,如今編寫多正規化解決方案很正常,函數語言程式設計用於資料操作,物件導向用於封裝買方、賣方或市場,正如我們將看到的很快,面向服務和方面的程式設計用於將複雜的框架程式碼粘合到位以提供類似 REST 的 HTTP 服務。接下來run是Java中交易引擎的方法,只要引擎處於執行狀態就進行交易:

public void run() {
    while (running) {
        prepareMarket();
 
        List<Sale> sales = market.trade();
        LOGGER.info("trading completed");
 
        noteMarketPricesAndVolumes(sales);
 
        persistSale(sales);
        LOGGER.info("persisting completed, notifying involved parties...");
        sales.stream().forEach(sale -> {
            if (sale.getBuyer().listener != null)
                sale.getBuyer().listener.onEvent(EventType.PURCHASE, sale);
            if (sale.getSeller().listener != null)
                sale.getSeller().listener.onEvent(EventType.SALE, sale);
        });
        ...
        try {
            Thread.sleep(delay);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


Java 設計與 Node.js 設計略有不同,因為我建立了一個簡單的方法,命名為run我將呼叫一次。只要命名的布林欄位為running真,它就會一遍又一遍地執行。我可以在 Java 中做到這一點,因為我可以利用其他執行緒與交易並行工作。為了調整引擎,我在每次迭代結束時引入了一個短暫的可配置延遲,執行緒暫停。在我所做的所有測試中,它被設定為暫停 3 毫秒,這與 JavaScript 解決方案使用的相同。
現在我剛剛提到使用執行緒來擴充套件系統。在這種情況下,執行緒類似於 Node.js 解決方案中使用的子程式。正如在 Node.js 解決方案中一樣,Java 解決方案按產品 ID 劃分市場,但不使用子程式,Java 解決方案在不同的執行緒上執行每個交易引擎(封裝市場)。理論上,最佳分割槽數將與核心數相似,但經驗表明,它還取決於執行緒被阻塞的程度,例如在資料庫中持久銷售。被阻塞的執行緒為其他執行緒在它們的位置上執行騰出空間,但執行緒過多會降低效能,因為執行緒之間的上下文切換變得更加相關。執行緒只是將執行委託給引擎,如上所示,它在迴圈中執行,直到它關閉。

public class TradingEngineThread extends Thread {
    private final TradingEngine engine;
 
    public TradingEngineThread(long delay, long timeout, Listener listener) throws NamingException {
        super("engine-" + ID++);
        engine = new TradingEngine(delay, timeout, listener);
    }
 
    @Override
    public void run() {
        engine.run();
    }



對於 Java 解決方案,我使用 Tomcat 作為 Web 伺服器並建立了一個簡單HttpServlet的處理請求以建立採購和銷售訂單。servlet 劃分市場並建立相關執行緒並啟動它們(請注意,更好的方法是在 servlet 啟動時啟動執行緒並在 servlet 停止時關閉引擎 - 顯示的程式碼未準備好生產!)。以下程式碼的第 15 行啟動了上一個程式碼段中顯示的執行緒。

@WebServlet(urlPatterns = { "/sell", "/buy", "/result" })
public class TradingEngineServlet extends HttpServlet {
    private static final Map<String, TradingEngineThread> kids = new HashMap<>();
    static {
        int chunk = PRODUCT_IDS.length / NUM_KIDS;
        for (int i = 0, j = PRODUCT_IDS.length; i < j; i += chunk) {
            String[] temparray = Arrays.copyOfRange(PRODUCT_IDS, i, i + chunk);
            LOGGER.info("created engine for products " + temparray);
            TradingEngineThread engineThread = new TradingEngineThread(DELAY, TIMEOUT, (type, data) -> event(type, data));
            for (int k = 0; k < temparray.length; k++) {
                LOGGER.debug("mapping productId '" + temparray[k] + "' to engine " + i);
                kids.put(temparray[k], engineThread);
            }
            LOGGER.info("---started trading");
            engineThread.start();
        }


servlet 處理購買和銷售請求如下:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String path = req.getServletPath();
    LOGGER.debug("received command: '" + path + "'");

    String who = req.getParameter("userId");
    String productId = req.getParameter("productId");
    TradingEngineThread engine = kids.get(productId);
    int quantity = Integer.parseInt(req.getParameter("quantity"));
    int id = ID.getAndIncrement();

    // e.g. /buy?productId=1&quantity=10&userId=ant
    if (path.equals("/buy")) {
        PurchaseOrder po = engine.addPurchaseOrder(who, productId, quantity, id);
        resp.getWriter().write("\"id\":" + id + ", " + String.valueOf(po));
    } else if (path.equals("/sell")) {


在第 8 行查詢相關引擎,並給出詳細資訊,例如在第 14 行建立採購訂單。現在它最初看起來好像我們擁有 Java 解決方案所需的一切,但在我將負載載入到伺服器上之後,我遇到了ConcurrentModificationExceptions,很明顯發生了什麼:上面程式碼片段中的第 14 行正在向引擎中的模型新增採購訂單,同時市場說迭代買家採購訂單以確定哪些買家感興趣在哪些產品中。
  

不同點
Node.js 正是透過其單執行緒方法避免了這種問題。這也是在 Java 世界中很難解決的問題!以下提示可能會有所幫助:
使用synchronized關鍵字來確保對給定(資料)物件的同步訪問,
如果您只需要讀取資料並對其做出反應,請複製資料,
為您的資料結構使用執行緒安全集合,
修改設計。
第一個技巧可能會導致死鎖,並且在 Java 世界中有些臭​​名昭著。第二個技巧有時很有用,但涉及複製資料的開銷。第三個技巧有時會有所幫助,但請注意 Javadocs 中包含的以下注釋java.util.Collections#synchronizedCollection(Collection):
返回由指定集合支援的同步(執行緒安全)集合...當遍歷返回的集合時,使用者必須手動同步它...不遵循此建議可能會導致非確定性行為。
使用執行緒安全的集合是不夠的,與第一個技巧相關的問題不會像人們希望的那樣簡單地消失。這就留下了第四個技巧。
  

Actor模型
如果你回顧一下上面的程式碼,你會發現一個名為 的方法prepareMarket()。為什麼我們不在自己的模型中儲存所有采購和銷售訂單,直到在其自己的執行緒中執行的交易引擎到達需要準備市場的程度,然後將所有這些未結訂單新增進來到市場模型,在交易開始之前?這樣我們就可以避免來自多個執行緒的併發訪問以及同步資料的需要。當您檢視所有 Java 原始碼時,您會看到TradingEngine使用名為newPurchaseOrders和的兩個欄位來執行此操作newSalesOrders。
這種設計的有趣之處在於它非常類似於actor模型,並且已經存在完美的Java庫,即Akka. 因此,我向使用 Akka 而不是執行緒的應用程式新增了第二個 servlet,以展示它如何解決併發問題。基本上,actor 是一個包含狀態(資料)、行為和訊息收件箱的物件。除了演員之外,沒有人可以訪問狀態,因為它應該是演員私有的。Actor 響應收件箱中的訊息並根據訊息告訴它做什麼來執行它的行為。Actor 保證它在任何時候都只會讀取和響應一條訊息,因此不會發生併發的狀態修改。新的 servlet 在第 13 行使用第 4 行建立的角色系統建立新角色,如下所示。因為actor系統應該在servlet啟動時啟動,而不是在如下所示的靜態上下文中啟動,並且應該在servlet停止時關閉。第 19 行向新建立的 actor 傳送一條訊息,告訴它啟動它包含的交易引擎。

@WebServlet(urlPatterns = { "/sell2", "/buy2", "/result2" })
public class TradingEngineServletWithActors extends HttpServlet {
 
    private static final ActorSystem teSystem = ActorSystem.create("TradingEngines");
    private static final Map<String, ActorRef> kids = new HashMap<>();
 
    static {
        int chunk = PRODUCT_IDS.length / NUM_KIDS;
        for (int i = 0, j = PRODUCT_IDS.length; i < j; i += chunk) {
            String[] temparray = Arrays.copyOfRange(PRODUCT_IDS, i, i + chunk);
            LOGGER.info("created engine for products " + temparray);
     
            ActorRef actor = teSystem.actorOf(Props.create(TradingEngineActor.class), "engine-" + i);
            for (int k = 0; k < temparray.length; k++) {
                LOGGER.debug("mapping productId '" + temparray[k] + "' to engine " + i);
                kids.put(temparray[k], actor);
            }
            LOGGER.info("---started trading");
            actor.tell(TradingEngineActor.RUN, ActorRef.noSender());
        }
        



Actor類如下所示,其資料和行為被封裝在其交易引擎例項中。

private static class TradingEngineActor extends AbstractActor {
 
    // STATE
    private TradingEngine engine = new TradingEngine(DELAY, TIMEOUT, (type, data) -> handle(type, data), true);
 
    public TradingEngineActor() throws NamingException {
 
        // INBOX
        receive(ReceiveBuilder
            .match(SalesOrder.class, so -> {
                // BEHAVIOUR (delegated to engine)
                engine.addSalesOrder(so.getSeller().getName(),
                    so.getProductId(),
                    so.getRemainingQuantity(),
                    so.getPrice(), so.getId());
                })
            .match(PurchaseOrder.class, po -> {
                ...
            .match(String.class, s -> RUN.equals(s), command -> {
                engine.run();
            })
            .build());
    } 
}

你可以看到 actor 類的第 4 行的交易引擎是私有的,只有在接收到訊息時才會使用,例如在第 12、18 或 20 行。這樣,保證沒有兩個執行緒可以同時訪問它時間是可以堅持的,對我們來說更重要的是,完全不需要在引擎上同步,這意味著我們的併發推理能力得到了極大的提升!請注意,為了允許處理收件箱中的訊息,交易引擎執行一個交易會話,然後將新的“執行”訊息推送到收件箱。這樣,在交易繼續之前,首先處理來自 HTTP 伺服器的任何新增採購/銷售訂單的訊息。
 

效能測試
現在是開始檢視負載下設計效能的時候了。我可以使用三臺機器:

  • 具有 16 GB RAM 執行 Linux(Fedora Core 20)的“高效能”6 核 AMD 處理器,
  • “中等效能”四核 I5 處理器,4GB RAM,執行 Windows 7,以及
  • 具有 4GB RAM 的“低效能”Intel Core 2 Duo 處理器也執行 Linux。

三臺機器連線在一個 100 兆位/秒的有線網路上。負載測試客戶端是一個定製的 Java 程式它使用執行池執行 50 個並行執行緒,連續進行隨機採購和銷售訂單。在請求之間,客戶端暫停。暫停時間經過調整,以便 Java 和 Node.js 程式效能較差的程式可以跟上負載,但接近它們開始滯後的臨界點,並記錄在下面的結果中。在持續至少 50 萬銷售額之前,以及在吞吐量穩定之前(想想熱點最佳化),才會記錄結果。吞吐量是使用插入資料庫的行數來衡量的,而不是程式輸出的不可靠的統計資料。
結果:
  • 案例 1 - 200ms 客戶端等待時間,4 個交易引擎快速交易引擎,慢速資料庫


吞吐量(每分鐘銷售額) 同步Java:5,100 帶有Akka的Java:5,000 NodeJS:6,400
帶有交易引擎的機器上的平均 CPU  同步Java:<50% 帶有Akka的Java:<40% NodeJS:40-60%

  • 案例 2 - 50ms 客戶端等待時間,2 個交易引擎慢速交易引擎,快速資料庫

吞吐量(每分鐘銷售額) 同步Java:32,800 帶有Akka的Java:30,100 NodeJS:15,000
帶有交易引擎的機器上的平均 CPU  同步Java:85% 帶有Akka的Java:90% NodeJS:>95%


在第一種情況下,交易引擎不受 CPU 限制。在案例二中,交易引擎受 CPU 限制,但整個系統的執行速度比案例一要快。在這兩種情況下,系統網路都不受限制,因為我測量的最大傳輸速度為每秒 300 千位元組,不到網路容量的 3%。在第一種情況下,資料庫是最慢的元件,交易引擎似乎受 I/O 限制,等待資料庫插入的結果。由於 Node.js 對其所有程式碼使用非阻塞正規化,因此它的效能優於 Java 解決方案。
當我使用帶有預配置非阻塞 (NIO) 聯結器的 Tomcat 8 時,MySQL 驅動程式是標準的 JDBC 阻塞版本。在第二種情況下,資料庫速度更快,交易引擎受 CPU 限制,Java 解決方案執行速度更快。
 
我的結果實際上並不那麼令人驚訝——眾所周知,Node.js 表現良好,尤其是在阻塞條件下。有關我認為與我的結果非常相關的結果,請參閱以下兩個連結: What Makes Node.js Faster Than Java?和 PayPal 的 Node-vs-Java 基準分析。第二個連結末尾的評論非常有趣,我覺得大部分都是有效的觀點。
 
我沒有嘗試透過使永續性也非阻塞來最佳化 Java 解決方案,因此它也是一個完全非阻塞的解決方案。這是可能的,因為存在非阻塞(儘管是非 JDBC)MySQL 驅動程式. 但它也需要改變 Java 解決方案的設計。
而且,如在上面的連結一個評論指出,這也許重新設計將是最具挑戰性的部分的平均Java 程式設計師,直到最近(如果有的話)從未在非同步非阻塞正規化中程式設計。不是難,而是不同,我懷疑隨著最近 Node.js 的成功,越來越多的非同步 Java 庫會開始出現。
請注意,最後一段並不意味著會引發任何型別的爭論——我絕不是說 Java、JavaScript、JVM 或 Node.js 中的任何一個更好。我要說的是
  • a) 我曾經是 Java 及其生態系統的堅定支持者,在過去的幾年裡,我已經成熟到意識到其他平臺也很棒,並且
  • b) 為手頭的工作選擇正確的工具使用概念驗證進行評估,例如我在這裡所做的。


請注意,本文中提供的程式碼不適用於任何目的,而且肯定不是生產就緒的,也不代表我可能會專業生產的東西
 

相關文章