5

我正在使用node-sqlite3,但我确信这个问题也出现在另一个数据库中。我在我的代码中发现了一个混合事务和异步代码的错误。

function insertData(arrayWithData, callback) {
    // start a transaction
    db.run("BEGIN", function() {
        // do multiple inserts
        slide.asyncMap(
            arrayWithData,
            function(cb) {
                db.run("INSERT ...", cb);
            },
            function() {
                // all done
                db.run("COMMIT");
            }
        );
    });
}

// some other insert
setInterval(
    function() { db.run("INSERT ...", cb); },
    100
);

您还可以运行完整的示例

问题是在 or 之后的异步暂停期间可以启动其他一些带有or查询的insert代码。然后这个额外的查询在事务中运行。提交事务时,这不是问题。但是,如果事务被回滚,那么这个额外查询所做的更改也会被回滚。哎呀,我们只是意外地丢失了数据而没有任何错误消息。updatebegininsert

我考虑过这个问题,我认为一种解决方案是创建一个包装类,以确保:

  • 只有一个事务在同时运行。
  • 当事务正在运行时,仅执行属于该事务的查询。
  • 在当前事务完成后,所有额外的查询都会排队并执行。
  • 当一个事务已经运行时,所有启动事务的尝试也将排队。

但这听起来像太复杂的解决方案。有更好的方法吗?你如何处理这个问题?

4

3 回答 3

4

首先,我想说我没有使用 SQLite 的经验。我的回答是基于对node-sqlite3.

恕我直言,您的代码最大的问题是您尝试从不同位置写入数据库。据我了解 SQLite,您无法像在 PostgreSQL 中那样控制不同的并行“连接”,因此您可能需要包装与 DB 的所有通信。我修改了您的示例以始终使用insertData包装器。这是修改后的功能:

function insertData(callback, cmds) {
  // start a transaction
  db.serialize(function() {
    db.run("BEGIN;");
    //console.log('insertData -> begin');
    // do multiple inserts
    cmds.forEach(function(item) {
      db.run("INSERT INTO data (t) VALUES (?)", item, function(e) {
        if (e) {
          console.log('error');
          // rollback here
        } else {
          //console.log(item);
        }
      });
    });
    // all done
    //here should be commit
    //console.log('insertData -> commit');
    db.run("ROLLBACK;", function(e) {
      return callback();
    });
  });
}

使用以下代码调用函数:

init(function() {
  // insert with transaction
  function doTransactionInsert(e) {
    if (e) return console.log(e);
    setTimeout(insertData, 10, doTransactionInsert, ['all', 'your', 'base', 'are', 'belong', 'to', 'us']);
  }

  doTransactionInsert();

  // Insert increasing integers 0, 1, 2, ...
  var i=0;

  function doIntegerInsert() {
    //console.log('integer insert');
    insertData(function(e) {
      if (e) return console.log(e);
      setTimeout(doIntegerInsert, 9);
    }, [i++]);
  }

  ...

我做了以下更改:

  • 添加了 cmds 参数,为简单起见,我将其添加为最后一个参数,但回调应该是最后一个(cmds 是插入值的数组,在最终实现中它应该是 SQL 命令的数组)
  • 将 db.exec 更改为 db.run(应该更快)
  • 添加 db.serialize 以序列化事务内的请求
  • BEGIN 命令的省略回调
  • 遗漏slide和一些underscore

您的测试实现现在对我来说很好。

于 2013-10-12T17:36:41.293 回答
3

我最终对 sqlite3 进行了完整的包装,以实现将数据库锁定在事务中。当数据库被锁定时,所有查询都会在当前事务结束后排队并执行。

https://github.com/Strix-CZ/sqlite3-transactions

于 2013-10-14T11:27:40.083 回答
0

恕我直言,ivoszz的回答存在一些问题:

  1. 由于所有 db.run 都是异步的,因此您无法检查整个事务的结果,如果一次运行出现错误结果,您应该回滚所有命令。为此,您应该在 forEach 循环的回调中调用 db.run("ROLLBACK")。db.serialize 函数不会序列化异步运行,因此“无法在事务中启动事务”。
  2. forEach 循环之后的“COMMIT/ROLLBACK”必须检查所有语句的结果,并且您不能在之前的所有运行完成之前运行它。

恕我直言,只有一种方法可以进行安全线程(obv 指的是后台线程池)事务管理:创建一个包装函数并使用异步库来手动序列化所有语句。通过这种方式,您可以避免使用 db.serialize 函数,并且(更重要的是)您可以检查所有单个 db.run 结果以回滚整个事务(并在需要时返回承诺)。与事务相关的node-sqlite3库的主要问题是序列化函数中没有回调来检查是否发生错误

于 2016-09-12T07:10:47.460 回答