該文使用原始碼地址:地址
為什麼會有這個實驗
由於cnode上的一篇提問 node.js單執行緒,是不是就不用訊息佇列了啊?
我當時的回答是
舉個例子,你要在資料表查是否有同名的,沒同名則插入。
因為node無法按整個請求的sql塊作為執行單元,而是按sql條作為了執行單元,那麼按上面的來說兩個select會先後到達,而第一條insert並沒有插入,導致第二個select查詢為空,還會繼續插入使用者。(如果表小可能不會出現,表大必現)
而訊息佇列則相當於是已將請求的整個sql塊作為執行單元,即完成了整個請求的select和insert,才會執行下一個請求的select和insert。
複製程式碼
這個回答是根據我以往在node遇到的坑,來回答的。貌似很有道理,畢竟我們平時執行node都是按條作為非同步的最小單元執行的。
但是事後我想,那為什麼不能將node塊做為順序執行單位呢?
沒錯,它確實可以
複製程式碼
遂在當天晚上做了一個簡單的塊執行庫,簡單測試了一下,感覺好像是實現了,但由於業務繁忙(產品需求激增),所以一直沒有時間去做優化和針對資料庫的實際的測試!遂於週末來測試一下。
我這邊實現了三套針對資料庫的併發測試
- 樂觀鎖抗併發
- 事務加樂觀鎖抗併發
- 塊執行加樂觀鎖抗併發
主要程式碼如下
樂觀鎖抗併發
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
failed
res
事務加樂觀鎖抗併發(和樂觀幾乎一致,有時事務結果好一個兩個)
ab
failed
res
塊執行加樂觀鎖抗併發
ab
failed (一開始看到沒有資料失敗,感覺還挺神奇的)
res (看來神奇是必然的,嘿嘿)
到這裡有人會說了,你這個ab壓力太小了,當然沒什麼了,其實我想說,主要還是資料和樂觀鎖結果太難看了,我要照顧一下。
大家想看塊執行的牛逼之處就讓大家看個痛快
塊執行加樂觀鎖抗併發(併發升級版本)
ab 直接上 -n 10000 -c 100
failed (怎麼還沒更新失敗?神奇?)
res (看來神奇又是必然的,嘿嘿)
最後
還有誰不服!簡直就是併發小神奇啊!如果是個人建站抗併發的話足夠了!無須事務照樣抗併發,效能槓槓的!
對結果有疑問的同學可以自行測試,注意兩點: