針對使用非塊執行和塊執行併發壓測對比

zy445566發表於2018-04-15

該文使用原始碼地址:地址

為什麼會有這個實驗

由於cnode上的一篇提問 node.js單執行緒,是不是就不用訊息佇列了啊?
我當時的回答是

舉個例子,你要在資料表查是否有同名的,沒同名則插入。
因為node無法按整個請求的sql塊作為執行單元,而是按sql條作為了執行單元,那麼按上面的來說兩個select會先後到達,而第一條insert並沒有插入,導致第二個select查詢為空,還會繼續插入使用者。(如果表小可能不會出現,表大必現)
而訊息佇列則相當於是已將請求的整個sql塊作為執行單元,即完成了整個請求的select和insert,才會執行下一個請求的select和insert。
複製程式碼

這個回答是根據我以往在node遇到的坑,來回答的。貌似很有道理,畢竟我們平時執行node都是按條作為非同步的最小單元執行的。
但是事後我想,那為什麼不能將node塊做為順序執行單位呢?

沒錯,它確實可以
複製程式碼

遂在當天晚上做了一個簡單的塊執行庫,簡單測試了一下,感覺好像是實現了,但由於業務繁忙(產品需求激增),所以一直沒有時間去做優化和針對資料庫的實際的測試!遂於週末來測試一下。
我這邊實現了三套針對資料庫的併發測試

  1. 樂觀鎖抗併發
  2. 事務加樂觀鎖抗併發
  3. 塊執行加樂觀鎖抗併發

主要程式碼如下

樂觀鎖抗併發

async function sqlCommon(sqlCommonName = 'sqlCommon')
{
    let conn;
    try{
        conn = await getConn();
        let testSelete = await queryPromise(
            conn,
            'SELECT * FROM `test` WHERE `name`= ? AND `version`=?',
            [getName(sqlCount),sqlCount]
        );
        if(testSelete.length>0)
        {
            let testRes = await queryPromise(
                conn,
                'UPDATE `test` SET `name`= ?,`version`=?+1 WHERE `version`=?',
                [getName(sqlCount+1),sqlCount,sqlCount]
            );
            if(testRes.affectedRows>0)
            {
                sqlCount++;
            } else {
                console.log(`${sqlCommonName} failed:${sqlCount}`)
            }
        } else {
            console.log(`${sqlCommonName} failed:${sqlCount}`)
        }
        conn.release();
    } catch(e){
        console.log(`${sqlCommonName} failed:${sqlCount}`)
        conn.release();
        // console.log(e.stack)
    }
}
複製程式碼

事務加樂觀鎖抗併發

async function sqlTrans()
{
    let conn;
    try{
        conn = await getConn();
        await queryPromise(conn,'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;')
        beginTransaction(conn);
        let testSelete = await queryPromise(
            conn,
            'SELECT * FROM `test` WHERE `name`= ? AND `version`=?',
            [getName(sqlCount),sqlCount]
        );
        if(testSelete.length>0)
        {
            let testRes = await queryPromise(
                conn,
                'UPDATE `test` SET `name`= ?,`version`=?+1 WHERE `version`=?',
                [getName(sqlCount+1),sqlCount,sqlCount]
            );
            if(testRes.affectedRows>0)
            {
                sqlCount++;
            } else {
                console.log(`sqlTrans failed:${sqlCount}`)
                rollback(conn);
            }
        } else {
            console.log(`sqlTrans failed:${sqlCount}`)
        }
        commit(conn);
        conn.release();
    } catch(e){
        console.log(`sqlTrans failed:${sqlCount}`)
        // console.log(e.stack);//這裡會爆出很多事務鎖錯誤
        rollback(conn);
        conn.release();
    }
}
複製程式碼

塊執行加樂觀鎖抗併發(僅僅是將樂觀鎖,放如塊執行的某個渠道,改造很簡單)

樂觀鎖需要的包地址
樂觀鎖需要的倉庫地址

function sqlBlock()
{
    return new Promise ((reslove,rejected)=>{
        BlockRun.run('sqlBlockChannel1',async ()=>{
            await sqlCommon('sqlBlock');
            reslove(true)
        },3000);
    });
}
複製程式碼

主服務入口

http.createServer( async (request, response) => {
    try {
    let pathname = url.parse(request.url).pathname;
    //console.log(`url:http://127.0.0.1:${port}{$pathname}`)
    let showText = 'test';
    switch (pathname)
    {
        case '/clear':
            await sqlClear();
            showText = 'clear';
            break;
        case '/common':
            await sqlCommon();
            showText = 'common';
            break;
        case '/trans':
            await sqlTrans();
            showText = 'trans';
            break;
        case '/block':
            await sqlBlock();
            showText = 'block';
            break;
    }
    response.writeHead(200, {'Content-Type': 'text/html'});    
    response.write(showText);
    response.end();
    } catch(e) {
        console.log(e.stack)
    }
 }).listen(port);
複製程式碼

其他程式碼請看

其他程式碼檔案地址

執行結果

樂觀鎖抗併發

ab

optimisticOnly-ab

failed

optimisticOnly-failed

res

optimisticOnly-res

事務加樂觀鎖抗併發(和樂觀幾乎一致,有時事務結果好一個兩個)

ab

optimisticTrans-ab

failed

optimisticTrans-failed

res

optimisticTrans-res

塊執行加樂觀鎖抗併發

ab

optimisticBlock-ab

failed (一開始看到沒有資料失敗,感覺還挺神奇的)

optimisticBlock-failed

res (看來神奇是必然的,嘿嘿)

optimisticBlock-res

到這裡有人會說了,你這個ab壓力太小了,當然沒什麼了,其實我想說,主要還是資料和樂觀鎖結果太難看了,我要照顧一下。
大家想看塊執行的牛逼之處就讓大家看個痛快

塊執行加樂觀鎖抗併發(併發升級版本)

ab 直接上 -n 10000 -c 100

optimisticBlockSuper-ab

failed (怎麼還沒更新失敗?神奇?)

optimisticBlockSuper-failed

res (看來神奇又是必然的,嘿嘿)

optimisticBlockSuper-res

最後

還有誰不服!簡直就是併發小神奇啊!如果是個人建站抗併發的話足夠了!無須事務照樣抗併發,效能槓槓的!
對結果有疑問的同學可以自行測試,注意兩點:

  1. 測試前要 curl http://127.0.0.1:8088/clear 保證資料庫沒有被之前測試汙染
  2. sql和全部程式碼都在此處
  3. 測試前要 npm install
  4. 此次測試使用的塊執行庫是block-run的1.0.8版本: 塊執行包地址 塊執行倉庫地址
  5. 補充一下,如果要涉及資料回滾,最好在塊執行中要加上事務,塊執行高效的原因其實就在於保證成功率,這裡沒有測加事務的塊執行的版本,我相信加事務的塊執行的效率還是會比非塊執行的事務版本要高,有興趣的可以測試一下

相關文章