【问题标题】:Node.js + SQLite async transactionsNode.js + SQLite 异步事务
【发布时间】:2013-10-16 19:41:00
【问题描述】:

我正在使用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
);

你也可以运行the full example

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

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

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

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

【问题讨论】:

    标签: node.js asynchronous sqlite transactions


    【解决方案1】:

    首先,我想说我没有使用 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

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

    【讨论】:

    • 所以基本上你的意思是 - 消除事务期间的所有异步操作。似乎是非常优雅的解决方案,我会尝试一下。谢谢!
    • 它对我有用,谢谢提示
    【解决方案2】:

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

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

    【讨论】:

    • 但是,我会接受 Ivoszz 的回答,因为它对于大多数用例来说已经足够了。
    • 另一种可能性是使用支持 SQLite3 和事务的现有 ORM,例如。 node-persist
    【解决方案3】:

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-06-14
      • 1970-01-01
      • 2016-10-24
      • 2010-12-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多