async-lockを使った排他制御
async-lockを使ってサーバサイドで排他制御させる方法です。排他制御を使えば同時アクセスよるデータの不整合を防げます。本記事で扱うモジュールのバージョンは次のとおりです。
- Node.js - 14.16.0
- express - 4.17.1
- async-lock - 1.3.0
インストール
次のコマンドを実行します。
以降は使い方になります。
execute function を使う場合
コールバック関数を抜けてからロックが解放されます。execute function
でのエラーをコールバック関数で判定する必要があります。
/** | |
* async-lock sample. | |
* use execute function. | |
*/ | |
"use strict"; | |
//const fs = require('fs'); | |
const path = require('path'); | |
//const https = require('https'); | |
const express = require('express'); | |
const app = express(); | |
app.use(express.static(path.join(__dirname, 'public'))); | |
// let webServer = https.createServer({ | |
// key: fs.readFileSync('server-key.pem'), | |
// cert: fs.readFileSync('server-crt.pem'), | |
// maxVersion: 'TLSv1.3', | |
// minVersion: 'TLSv1', | |
// }, app) | |
// .listen(8443, function() { | |
// console.log('start. (exit: Ctrl + C)'); | |
// } | |
// ); | |
let webServer = app.listen(8080, () => { | |
console.log('start. (exit: Ctrl + C)'); | |
}); | |
const AsyncLock = require('async-lock'); | |
const lock = new AsyncLock({ timeout: 60000 }); | |
const lockKey = 'key'; | |
let count = 0; | |
// sleep function. | |
const sleep = (ms) => { | |
return new Promise((resolve, reject) => { | |
setTimeout(() => { | |
resolve(); | |
}, ms); | |
}); | |
}; | |
// execute function. | |
async function execute(done) { | |
console.log(Date.now() + ' execute'); | |
await sleep(5000); | |
console.log(Date.now() + ' sleep end'); | |
count++; | |
done(undefined, 'execute end. count=' + count); | |
} | |
app.get('/start-lock', (request, response) => { | |
lock.acquire(lockKey, execute, (err, result) => { | |
console.log(Date.now() + ' start-lock execute end.'); | |
if (err) { | |
console.error(err.message); | |
response.send('error.'); | |
// lock released | |
return; | |
} | |
console.log(Date.now() + ' response send start.'); | |
response.send(result); | |
console.log(Date.now() + ' response send end.'); | |
// lock released | |
}); | |
}); | |
app.get('/start-lock2', (request, response) => { | |
lock.acquire(lockKey, execute, (err, result) => { | |
console.log(Date.now() + ' start-lock2 execute end.'); | |
if (err) { | |
console.error(err.message); | |
response.send('error.'); | |
// lock released | |
return; | |
} | |
console.log(Date.now() + ' response send start.'); | |
response.send(result); | |
console.log(Date.now() + ' response send end.'); | |
// lock released | |
}); | |
}); |
実行結果
node async-lock-sample-execute.js
を実行し、ブラウザから/start-lock
> /start-lock2
の順に実行した場合の実行結果(ログ)です。
エラーを発生させる
execute function
でエラーを発生させたい場合は次のようにdone()
を実行します。
エラー発生時の実行結果
execute function
でエラーを発生させた場合の実行結果です。
Promise を使う場合
then()
またはcatch()
がコールされるタイミングでロックは解除されます。
/** | |
* async-lock sample. | |
* use promise. | |
*/ | |
"use strict"; | |
//const fs = require('fs'); | |
const path = require('path'); | |
//const https = require('https'); | |
const express = require('express'); | |
const app = express(); | |
app.use(express.static(path.join(__dirname, 'public'))); | |
// let webServer = https.createServer({ | |
// key: fs.readFileSync('server-key.pem'), | |
// cert: fs.readFileSync('server-crt.pem'), | |
// maxVersion: 'TLSv1.3', | |
// minVersion: 'TLSv1', | |
// }, app) | |
// .listen(8443, function() { | |
// console.log('start. (exit: Ctrl + C)'); | |
// } | |
// ); | |
let webServer = app.listen(8080, () => { | |
console.log('start. (exit: Ctrl + C)'); | |
}); | |
const AsyncLock = require('async-lock'); | |
const lock = new AsyncLock({ timeout: 60000 }); | |
const lockKey = 'key'; | |
let count = 0; | |
// sleep function. | |
const sleep = (ms) => { | |
return new Promise((resolve, reject) => { | |
setTimeout(() => { | |
resolve(); | |
}, ms); | |
}); | |
}; | |
// execute function. | |
async function execute(done) { | |
console.log(Date.now() + ' execute'); | |
await sleep(5000); | |
console.log(Date.now() + ' sleep end'); | |
count++; | |
done(undefined, 'execute end. count=' + count); | |
} | |
app.get('/start-lock', (request, response) => { | |
lock.acquire(lockKey, execute) | |
.then(result => { | |
// lock released. | |
console.log(Date.now() + ' start-lock execute end.'); | |
console.log(Date.now() + ' response send start.'); | |
response.send(result); | |
console.log(Date.now() + ' response send end.'); | |
}).catch(err => { | |
// lock released. | |
console.error(err.message); | |
response.send('error.'); | |
}); | |
}); | |
app.get('/start-lock2', (request, response) => { | |
lock.acquire(lockKey, execute) | |
.then(result => { | |
// lock released. | |
console.log(Date.now() + ' start-lock2 execute end.'); | |
console.log(Date.now() + ' response send start.'); | |
response.send(result); | |
console.log(Date.now() + ' response send end.'); | |
}).catch(err => { | |
// lock released. | |
console.error(err.message); | |
response.send('error.'); | |
}); | |
}); |
実行結果
node async-lock-sample-promise.js
を実行し、ブラウザから/start-lock
> /start-lock2
の順に実行した場合の実行結果(ログ)です。
コールバック関数のみの場合
ロック解除のタイミングはPromise
を使う場合と変わりません。
/** | |
* async-lock sample. | |
* only callback. | |
*/ | |
"use strict"; | |
//const fs = require('fs'); | |
const path = require('path'); | |
//const https = require('https'); | |
const express = require('express'); | |
const app = express(); | |
app.use(express.static(path.join(__dirname, 'public'))); | |
// let webServer = https.createServer({ | |
// key: fs.readFileSync('server-key.pem'), | |
// cert: fs.readFileSync('server-crt.pem'), | |
// maxVersion: 'TLSv1.3', | |
// minVersion: 'TLSv1', | |
// }, app) | |
// .listen(8443, function() { | |
// console.log('start. (exit: Ctrl + C)'); | |
// } | |
// ); | |
let webServer = app.listen(8080, () => { | |
console.log('start. (exit: Ctrl + C)'); | |
}); | |
const AsyncLock = require('async-lock'); | |
const lock = new AsyncLock({ timeout: 60000 }); | |
const lockKey = 'key'; | |
let count = 0; | |
const sleep = (ms) => { | |
return new Promise((resolve, reject) => { | |
setTimeout(() => { | |
resolve(); | |
}, ms); | |
}); | |
}; | |
app.get('/start-lock', (request, response) => { | |
lock.acquire(lockKey, async (done) => { | |
console.log(Date.now() + ' execute'); | |
await sleep(5000); | |
console.log(Date.now() + ' sleep end'); | |
count++; | |
done(undefined, 'execute end. count=' + count); | |
}).then(result => { | |
// lock released. | |
console.log(Date.now() + ' start-lock execute end.'); | |
console.log(Date.now() + ' response send start.'); | |
response.send(result); | |
console.log(Date.now() + ' response send end.'); | |
}).catch(err => { | |
// lock released. | |
console.error(err.message); | |
response.send('error.'); | |
}); | |
}); | |
app.get('/start-lock2', (request, response) => { | |
lock.acquire(lockKey, async (done) => { | |
console.log(Date.now() + ' execute'); | |
await sleep(5000); | |
console.log(Date.now() + ' sleep end'); | |
count++; | |
done(undefined, 'execute end. count=' + count); | |
}).then(result => { | |
// lock released. | |
console.log(Date.now() + ' start-lock2 execute end.'); | |
console.log(Date.now() + ' response send start.'); | |
response.send(result); | |
console.log(Date.now() + ' response send end.'); | |
}).catch(err => { | |
// lock released. | |
console.error(err.message); | |
response.send('error.'); | |
}); | |
}); |
実行結果
node async-lock-sample-only-callback.js
を実行し、ブラウザから/start-lock
> /start-lock2
の順に実行した場合の実行結果(ログ)です。
ロック処理内でのエラーについて
execute function
を使う場合、Promise
を使う場合、コールバック関数のみの場合問わず、ロック処理内でエラーを発生させたい場合は次のようにdone()
を実行します。
次のようにthrow
を使うとすると実行エラーになるので注意してください。