关于Nodejs并发的一些疑问

关于Nodejs并发的一些疑问

我是刚学习nodejs不久的菜鸟,最近学习的时候遇到个问题,想问一下: 我一个项目里面,项目里有奖品,奖品有1个。当为0个时就领取失败 前端有个按钮可以点击领取奖品,此时同时有2个人点击领取。我在后台的写法就是,先查询奖品个数,也就是 先conn.query(“select * from giftinfo” , function(row){ }) 获取到奖品个数,然后就在回调中进行计算,把奖品个数减一再更新上去。也就是在回调里再进行conn.query(“update …” , function(){})

我自己的项目里在回调中还有几个数据库更新操作,现在的情况就是,同时点击的话,有时会出现奖品只有1个,但是却2个人都领到了奖品。 我自己想了一下,是不是当同时点击时,首先一个人先触发conn.query方法查询,此时查询结果还没返还的时候,nodejs又去执行第二个人的query,于是在两个的查询回调当中,奖品的数量都是一致的,所以都是可以领取。

请问这种情况一般是如何处理的呢?


10 回复

你所描述的问题是一个典型的并发控制问题,特别是在处理有限资源(如奖品)时。在Node.js中,由于其非阻塞I/O模型,多个请求可能会几乎同时到达服务器,导致并发问题。解决这个问题的一个常见方法是使用事务或者锁机制来确保同一时间只有一个操作能够修改数据库中的数据。

解决方案

使用事务(Transaction)

在MySQL等支持事务的数据库中,你可以使用事务来确保一系列的操作要么全部成功,要么全部失败。这可以防止并发问题。

const mysql = require('mysql');
const connection = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'my_database'
});

connection.connect();

connection.beginTransaction(function(err) {
  if (err) { throw err; }

  connection.query('SELECT COUNT(*) AS count FROM giftinfo', function (error, results, fields) {
    if (error) { 
      return connection.rollback(function() {
        throw error;
      }); 
    }
    
    const count = results[0].count;

    if (count > 0) {
      // 更新库存并插入记录
      connection.query('UPDATE giftinfo SET count = count - 1 WHERE id = ?', [1], function (error, results, fields) {
        if (error) { 
          return connection.rollback(function() {
            throw error;
          });
        }
        
        // 提交事务
        connection.commit(function(err) {
          if (err) { 
            return connection.rollback(function() {
              throw err;
            });
          }
          console.log('奖品已成功领取');
        });
      });
    } else {
      console.log('奖品已领完');
      connection.rollback();
    }
  });
});

使用乐观锁(Optimistic Locking)

另一种方法是使用乐观锁,在更新数据前检查数据是否被其他进程修改过。这通常通过添加一个版本号字段来实现。

CREATE TABLE giftinfo (
  id INT PRIMARY KEY,
  count INT,
  version INT DEFAULT 0
);

在Node.js代码中:

connection.query('SELECT count, version FROM giftinfo WHERE id = ?', [1], function (error, results, fields) {
  if (error) throw error;

  const currentCount = results[0].count;
  const currentVersion = results[0].version;

  if (currentCount > 0) {
    const newVersion = currentVersion + 1;
    connection.query('UPDATE giftinfo SET count = ?, version = ? WHERE id = ? AND version = ?', [currentCount - 1, newVersion, 1, currentVersion], function (error, results, fields) {
      if (error) throw error;

      if (results.affectedRows === 0) {
        console.log('奖品已被其他人领取');
      } else {
        console.log('奖品已成功领取');
      }
    });
  } else {
    console.log('奖品已领完');
  }
});

总结

上述两种方法都可以有效解决并发问题。使用事务可以确保数据的一致性,而乐观锁则可以在不阻塞的情况下提高系统的吞吐量。选择哪种方法取决于你的具体需求和数据库的支持情况。


想起之前看的课堂,将的是如何处理12306抢票的问题。

貌似用的解决办法是锁什么的。

var events= require(‘events’); var x = new events.EventEmitter(); var jpCount = 5; //conn.query(“select * from giftinfo” , function(row){ })

x.on(‘lingjiang’,lingjiangHandle); function lingjiang(user) { if (jpCount > 0) x.emit(‘lingjiang’, user); else { console.log(‘奖品已领玩’); } }

var lingjiangHandle = function(user) { jpCount–; if (lingjiangSucceed) { console.log(user + ‘领奖成功’); } else { console.log(user + ‘领奖失败’); jpCount++; } }

mongo没有锁的概念,这时候可以考虑使用redis

这么做是不正确的。

很明显,你对数据库做了写操作,单单查询来判断是不够的。必须修改数据库成功,才能为用户颁发奖品,如果失败,则意味奖品不存在。

还有一种就是同步缓存,在nodejs服务器记录一个奖品数作为查询和递减,数据库服务器只负责修改。但是nodejs服务器挂掉会导致这个缓存消失,每次重新启动nodejs服务器,需要先去数据库服务器确认当前可以领的奖品数。

mongodb是存在写锁的。

用缓存。缓存对于事件I/O几乎0延迟。

看着这个issue之后, 解决掉了机器人并发刷邮件的严重问题. 多谢4楼.

楼上各位说得真好

可以用一个锁,点击后,只有抢到锁的请求可以继续执行数据库操作。 具体锁怎么实现就多种多样了:

  1. 可以使用一个互斥队列,rabbitmq。
  2. 可以利用mongodb更新单个数据是原子操作这一特性,建一张表,里面放一条数据,有一个列为locked,默认是false,每次点击首先去执行更新这条数据的操作,将locked=true,更新成功的即为抢到了锁。

你遇到的问题确实是因为并发请求导致的数据竞争条件。Node.js 是单线程的异步非阻塞 I/O 模型,当两个请求几乎同时到达时,它们可能会同时查询数据库并得到相同的结果,从而导致两个用户都认为自己可以领取奖品。

解决方案

解决这种数据竞争问题的一种常见方法是使用数据库事务或者乐观锁(乐观锁通常通过版本号实现)。这里我们以 MySQL 数据库为例,展示如何使用事务来避免这种情况。

使用事务

MySQL 支持事务,可以通过事务来确保一系列操作要么全部成功,要么全部不成功。这样可以避免多个请求同时修改同一行数据时产生的数据竞争。

const mysql = require('mysql');

// 创建数据库连接
const connection = mysql.createConnection({
  host: 'localhost',
  user: 'yourusername',
  password: 'yourpassword',
  database: 'yourdatabase'
});

connection.connect();

function claimPrize(userId, callback) {
  // 开始事务
  connection.beginTransaction(function (err) {
    if (err) { 
      return callback(err); 
    }

    connection.query('SELECT COUNT(*) as count FROM giftinfo', function (err, rows) {
      if (err) {
        connection.rollback(function () {
          return callback(err);
        });
        return;
      }

      const prizeCount = rows[0].count;

      if (prizeCount > 0) {
        // 更新奖品数量
        connection.query('UPDATE giftinfo SET count = count - 1 WHERE count > 0', function (err) {
          if (err) {
            connection.rollback(function () {
              return callback(err);
            });
            return;
          }

          // 提交事务
          connection.commit(function (err) {
            if (err) {
              connection.rollback(function () {
                return callback(err);
              });
              return;
            }
            callback(null, true); // 领取成功
          });
        });
      } else {
        // 如果奖品数量为0,则回滚事务
        connection.rollback(function () {
          callback(new Error("No prizes available"));
        });
      }
    });
  });
}

claimPrize(1, function(err, result) {
  if (err) {
    console.error(err);
  } else {
    console.log(result ? "Prize claimed" : "Failed to claim prize");
  }
});

解释

  1. 开始事务:使用 beginTransaction 方法开始一个新的事务。
  2. 查询当前奖品数量:查询数据库中的奖品数量。
  3. 检查奖品数量:如果奖品数量大于 0,则继续执行后续操作;否则,回滚事务。
  4. 更新奖品数量:更新奖品数量(减一)。
  5. 提交事务:如果所有操作都成功,则提交事务。如果有任何错误发生,则回滚事务。

通过这种方式,可以确保在高并发情况下,两个请求不会同时获得相同的奖品数量,并且只有在奖品数量大于零的情况下才会更新奖品数量,从而保证数据一致性。

回到顶部