Firebase数据库如何防止并发读取?

时间:2018-06-01 06:14:49

标签: node.js firebase firebase-realtime-database google-cloud-functions

使用案例

用户将在特定位置(例如Set<ConstraintViolation<SecondaryDTO>> violations = validator.validate(userDTO.getSecondaryDTO()); violations.forEach(violation -> constraintValidatorContext .buildConstraintViolationWithTemplate(violation.getMessage()) .addConstraintViolation()); )获取门的密码(例如door2,密码222)。之后,云功能将从LocationA文档中删除门并将其添加到empty文档中。

初始数据库:

occupied

用户获取空门密码后

"LocationA" : {
  "empty" : {
    "door2" : {
      "password" : "222"
    },
    "door3" : {
      "password" : "333"
    }
  },
  "occupied" : {
    "door1" : {
      "password" : "111"
    }
  }
}

问题:

如果有2个用户同时获得"LocationA" : { "empty" : { "door3" : { "password" : "333" } }, "occupied" : { "door1" : { "password" : "111" }, "door2" : { "password" : "222" } } } 密码怎么办?这种情况会发生吗?

我希望用户1分别获得door2而用户2分别获得door2

这是我用来启动大门的代码:

door3

更新了Grimthorr回答的基础

// Read Lockers QR User(CRUD)
exports.getQRCode = functions.https.onRequest((req, res) => {
    admin.database().ref('lockers/' + 'LocationA/' + 'empty').limitToFirst(1).once("value",snap=> {
        console.log('QR Code for door:',snap.val());
        var qrCodesForDoor = snap.val();
        res.send(qrCodesForDoor); 
    });
});

1 个答案:

答案 0 :(得分:1)

您描述的内容类似于race condition

  

软件的行为,其输出取决于其他不可控事件的顺序或时间。当事件没有按程序员的意图发生时,它就会成为一个错误。

这在使用实时数据库时似乎不太可能,特别是在云功能中使用时,但并非完全不可能。

Firebase SDK提供transaction operations,可用于避免并发修改。对于您的方案,使用Node.js中的Admin SDK,您可以执行以下操作:

// Read Lockers QR User(CRUD)
exports.getQRCode = functions.https.onRequest((req, res) => {
    admin.database().ref('lockers/LocationA/empty').limitToFirst(1).once("value", (snap) => {
        if (!snap.hasChildren()) {
            res.send("No doors available.");
            return;
        }
        // Get the name of the first available door and use a transaction to ensure it is not occupied
        var door = Object.keys(snap.val())[0]; // The limitToFirst always returns a list (even with 1 result), so this will select the first result
        var occupiedRef = admin.database().ref('lockers/LocationA/occupied/'+door);
        occupiedRef.transaction((currentData) => {
            if (currentData === null) {
                console.log("Door does not already exist under /occupied, so we can use this one.");
                return snap.val(); // Save the chosen door to /occupied
            } else {
                console.log('The door already exists under /occupied.');
                return; // Abort the transaction by returning nothing
            }
        }, (error, committed, snapshot) => {
            if (error) {
                console.log('Transaction failed abnormally!', error);
                res.send("Unknown error."); // This handles any abormal error
            } else if (!committed) {
                console.log('We aborted the transaction (because the door is already occupied).');
                res.redirect(req.originalUrl); // Refresh the page so that the request is retried
            } else {
                // The door is not occupied, so can be given to this user
                admin.database().ref('lockers/LocationA/empty/'+door).remove(); // Delete the door from /empty
                console.log('QR Code for door:',snapshot.val());
                var qrCodesForDoor = snapshot.val();
                res.send(qrCodesForDoor); // Send the chosen door as the response
            }
        });
    });
});

这使用您现有的代码来获取下一个可用的门,不同之处在于它只会在/occupied节点下尚未存在的情况下选择此门。它通过在选择之前使用事务来检查/occupied/door#节点的值来实现这一点,并应用以下逻辑:

  • 如果/occupied下的门不存在,我们可以安全地选择此门,将其保存到/occupied并从/empty删除。
  • 如果<{1}}下的确实存在,那么其他人就已经打败了我们,所以请求页面会刷新以再次触发该功能,因此(希望)选择不同的门下一次。