在 nodejs expressjs 中使用 async-lock 模块实现多线程安全代码

  • fennng 

模块链接

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
        });


发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注