模块链接
https://www.npmjs.com/package/async-lock
昨天正式发布了用nodejs 重写的 Telegram抽奖机器人 @fengdrawbot 后, 马上在第一次被使用中就发现了一超级大BUG。
由于机器人有人数到了就会自动开奖的机制, 所以在每次有人加入抽奖的时候就会判断是否达到人数, 达到人数后就会开奖。
当即将开奖的时候, 如果有几人同时参加抽奖, 由于是几乎同时参加的, 所以有机率同时触发抽奖代码使机器人开奖多次。
原来机器人是在Google App Script (GAS) 上运行,使用的也是GAS上面的锁 (LockService),除了慢点并没有什么问题。当我改用nodejs 后, 我使用了 async-lock 这个模块。 但是其实我在使用的过程中,代码没有使用正确,而我也没有模拟多人同时参与的情况进行测试,所以出现了一个大BUG。
上一篇博客我写了如何使用fiddler 进行BUG的复原。这一篇博客我想讲讲 async-lock 的具体用法, 官方的说明文档太简短,我当时没有完全看懂, 所以才会犯错。
async-lock 的安装和导入请直接看官方文档。 我这里给出示例代码来讲讲它的用法。
// 设置一个用来锁的对像,我这里使用抽奖码来锁 // 用抽奖码的好处是,每个抽奖都有独立的锁 // 当有好几个抽奖同时开奖的时候,它们互不干涉 let key = draw.joinCode; // 这个只是我用来测试的,标记当前的线程 let uuid = uuidv4(); // 拿锁, 只有拿到锁才能执行里面的代码 // 没拿到锁的线程会在这里卡住,直到拿到锁为止 lock.acquire(key, function(done) { // async work // 拿到锁后, 这个匿名函数里面的代码才会被执行 console.log("got lock" + uuid); // Draw the result and modify payloads array let task = DrawCommon.drawResult(draw, payloads, payload5, uuid); task.then(r => { // I previous used done in a wrong place, so the lock didn't work console.log("release lock" + uuid); // 这里使用 done 这个 callback 来释放锁, 释放后,别的线程才能进来 // 这个时候,之前的线程已经开奖完毕, 新进来的进程会重新查数据库,如果 // 发现开奖完毕则不会做任何事, 避免了多次开奖 // 如果没有锁的话, 所以进程一起进到这里面, 当它们查询数据库的时候发现还没有开奖, // 就会一起执行开奖的代码 done("no error", "ok"); }); tasks.push(task); }, function(err, ret) { // lock released }, null);
上面的这个例子是使用了传进来的done 这个callback函数来释放锁的, 如果我的开奖函数不需要返回值则使用这面的方法就可以。 但是我的 drawResult 这个函数会返回一个 promise, 而且我要反这个promise 再返回到上一级函数, 所以我需要 lock.acquire 能够返回 promise. 幸运的是 async-lock 也有 promise 模式, 看下面的例子。
// promise 模式会返回一个 promise, 在 promise 模式中, 不需要传入 // done 这个 callback 去释放锁, 锁会自动被释放 return lock.acquire(key, function () { // return value or promise // async work console.log("got lock" + uuid); // Draw the result and modify payloads array // 调用 drawResult 函数, 这个函数返回一个 promise // 这样 promise 里面的 payloads 可以被传到上一级函数 return DrawCommon.drawResult(draw, payloads, payload5, uuid); //这里timeout 设定了多久超时, 如果5秒内拿不到锁就超时 }, {timeout: 5000}).catch(function(err) { // 超时后会跑这里面的代码 console.log(err.message); // output: error });