ServerValue.increment 网络中断时无法正常工作

ServerValue.increment doesn't work properly when Internet goes down

添加 ServerValue.increment() (Add increment() for atomic field value increments #2437) 是一个好消息,因为它允许在 Firebase RTDB 中自动增加字段值。

我有一个保留库存的应用程序,这个功能一直很关键,因为它允许更新库存,而不管用户有时是否离线。但是,我开始注意到有时该函数会执行两次,这完全以错误的方式错误地陈述了库存。

为了隔离问题,我决定进行以下测试,结果表明当连接从在线转到离线时 ServerValue.Increment() 工作不正常:

  1. 从 1 到 200 做一个 for loop function:

    for (var i = 1; i <= 200; i++) {
      testBloc.incrementTest(i);
      print('Pos: $i');
    }
    
  2. 函数 incrementTest(i) 必须递增两个变量:position(从 1 到 200)和 sum(加 1 + 2 + 3 , ..., + 200 结果应该是 20,100)

        Future<bool> incrementTest(int value) async {    
         try {    
    
           db.child('test/position')
             .set(ServerValue.increment(1));     
    
           db.child('test/sum')
             .set(ServerValue.increment(value));     
    
         } catch (e) {
           print(e);
         }  
    
    
          return true;
     }
    

请注意,db 指的是 Firebase 实例 (FirebaseDatabase.instance.reference())

有了这个,测试就来了:

测试 1:100% 在线。通过

该函数正常工作,使两个变量得到正确的结果(在 Firebase 控制台中):

position: 200

sum: 20100

测试 2:100% 离线。通过

为此,我在飞行模式下使用了物理设备,然后我执行了 for loop function,当函数执行完毕后,我停用了飞行模式并在 firebase 控制台中检查了结果,结果令人满意:

position: 200

sum: 20100

测试 3:开始在线,然后转到离线。失败

这是网络连接中断时的典型操作场景。更糟糕的是,当连接断断续续时,您正乘坐地铁旅行或处于低覆盖率站点,而离线持久性是理想的功能。为了模拟它,我所做的是在线模式下 运行 for loop function,在完成之前,我将物理设备置于飞行模式。后来我去Online完成了测试,在Firebase控制台看到了结果。在所有情况下获得的结果都是不正确的。以下是部分结果:

如您所见,Increment 被错误地重复了 10、18 和 9 次。

如何避免这种行为?

是否有任何其他方法可以在 Firebase 中自动递增一个在线/离线正常工作的数字?

firebaser 在这里

这是增量行为中一个有趣的边缘案例。客户端和服务器之间都无法确定是否执行了增量,因此最终会在重新连接时从客户端重试。据我所知,这个问题只会发生在增量操作上,因为所有其他写操作都是幂等的,除了事务,但那些在离线时不起作用。

可以确保每个增量只发生一次,但这需要一些工作:

  1. 首先,为写入操作添加一个随机数,唯一标识此操作。您可以为此使用按键,但任何其他 UUID 也可以正常工作。将其与您最初的 set() 调用合并为一个多路径 update 调用,将随机数写入顶级节点,并将服务器端时间戳作为其值。
  2. 现在在顶级位置的安全规则中,只有在没有现有数据的情况下才允许写入。这确保了您看到的次级写入被拒绝,并且由于安全规则是在整个多路径更新中检查的,因此错误的增量也会被拒绝。
  3. 您可能希望根据其中的时间戳值使用随机数键定期清理节点。这对性能无关紧要(因为在清理期间您永远不会在此处搜索),但可能有助于控制随机数的存储成本。

我还没有将这种方法用于这个特定的用例,但已经为其他人使用过。如果您包含客户端重试,则上述内容实质上构建了您自己的多路径事务机制,这正是我过去所需要的。但是因为你在这里不需要它,所以没有它更简单。

根据@puf的回答,您可以进行如下处理:

  Future<bool> incrementTest(int value, int dateOfToday) async {

    var id = db.push().key;

    Map<String, dynamic> _updates = {
      'test/position':            ServerValue.increment(1), 
      'test/sum':                 ServerValue.increment(value),  
      'test/nonce/$id':           dateOfToday,
    };

      db.child('previousPath').update(_updates)
         .catchError((error) => print('Increment Duplication Rejected ${error.message}'));

    return true;
  }

然后,在 Firebase 安全规则中,您需要在 test/nonce/id 位置添加一条规则。内容如下:

{
  "previousPath": {
    "test": {
      ".read": "auth != null",  //It depends on your root rules
      ".write": "auth != null", //It depends on your root rules
      "nonce": {
        "$nonce_id": {
          ".validate": "!data.exists()" //THE MAGIC IS HERE
        }
      }
    }
  }
}

这样,当设备再次尝试(错误地)写入数据库时​​,Firebase 将拒绝它,因为它之前已经用相同的 ID 进行了写入。

我希望它对其他人有用!!!