以下是我测试的案例:
使用 transaction
调用增加一个值:
ref.transaction(function(value) {
return (value || 0) + 1;
});
使用新的increment
操作符来增加一个值: ref.set(admin.database.ServerValue.increment(1));
虽然增量操作更快并不会让人感到惊讶,但实际上速度有多快呢?
结果:
我在我的2016款MacBook Pro上运行了测试,并将以上内容包装在一个简单的Node.js脚本中,该脚本使用客户端Node SDK。操作的包装脚本也非常基本:
timer = setInterval(function() {
... the increment or transaction from above ...
}, 100);
setTimeout(function() {
clearInterval(timer);
process.exit(1);
}, 60000)
所以:每秒增加值10次,1分钟后停止。我使用以下脚本生成了此过程的实例:
for instance in {1..10}
do
node increment.js &
done
这将使用increment
操作符运行10个并行进程,每秒增加值10次,总共每秒增加100次。然后我更改了实例的数量,直到“每秒增量”达到峰值。
我随后在jsbin上编写了一个小脚本script on jsbin监听值,并通过简单的低通移动平均滤波器确定每秒增量。我在这里遇到了一些麻烦,所以不确定计算是否完全正确。根据我的测试结果,它们已经足够接近,但如果有人想编写一个更好的观察者:请随意。 :)
关于测试需要注意的事项:
increment
吞吐量应始终显着高于transaction
。无论您得到什么结果,请分享它们。:)要理解transaction
和increment
之间的性能差异,真正有帮助的是了解这些操作在内部如何工作。对于Firebase实时数据库,“内部”意味着在客户端和服务器之间通过Web套接字连接发送的命令和响应。
事务在Firebase中使用比较并设置方法。每当我们开始像上面这样的事务时,客户端都会猜测节点的当前值。如果它以前从未看到过该节点,则猜测为null
。它调用我们的事务处理程序,并将该猜测返回给我们的代码,然后我们的代码返回新值。客户端将猜测和新值发送到服务器,服务器执行比较并设置操作:如果猜测是正确的,请设置新值。如果猜测不正确,则服务器拒绝操作并将实际当前值返回给客户端。
在完美的情况下,初始猜测是正确的,并且立即将值写入服务器的磁盘上(之后发送到所有侦听器)。在流程图中,它看起来像这样:
Client Server
+ +
transaction() | |
| |
null | |
+---<-----+ |
| | |
+--->-----+ |
1 | (null, 1) |
+--------->---------+
| |
+---------<---------+
| (ack, 3) |
| |
v v
但如果节点在服务器上已经有一个值,它会拒绝写入并发送实际的值回来,客户端会再次尝试:
Client Server
+ +
transaction() | |
| |
null | |
+---<-----+ |
| | |
+--->-----+ |
1 | |
| (null, 1) |
+--------->---------+
| |
+---------<---------+
| (nack, 2) |
| |
2 | |
+---<-----+ |
| | |
+--->-----+ |
3 | (2, 3) |
+--------->---------+
| |
+---------<---------+
| (ack, 3) |
| |
| |
v v
这并不太糟糕,只多了一次往返。即使Firebase使用悲观锁定,也需要这个往返,所以我们没有失去任何东西。
问题在于如果多个客户端同时修改相同的值,就会出现所谓的节点争用,它看起来像这样:
Client Server Client
+ + +
transaction() | | |
| | | transaction()
null | | |
+---<-----+ | | null
| | | +--->----+
+--->-----+ | | |
1 | | +---<----+
| (null, 1) | | 1
+--------->---------+ (null, 1) |
| |---------<---------+
+---------<---------+ |
| (nack, 2) |--------->---------+
| | (nack, 2) |
2 | | |
+---<-----+ | | 2
| | | |--->----+
+--->-----+ | | |
3 | (2, 3) | |---<----+
+--------->---------+ | 3
| | |
+---------<---------+ |
| (ack, 3) | (2, 3) |
| |---------<---------+
| | |
| |--------->---------+
| | (nack, 3) |
| | | 3
| | |--->----+
| | | |
| | |---<----+
| | | 4
| | (3, 4) |
| |---------<---------+
| | |
| |--------->---------+
| | (ack, 4) |
| | |
v v v
TODO: 更新上面的图表,以使服务器上的操作不重叠。
第二个客户端必须重新尝试其操作,因为在它的第一次尝试和第二次尝试之间,服务器端的值已被修改。我们有越来越多的客户端写入此位置,就越有可能看到重试。Firebase客户端会自动执行这些重试,但经过多次重试后,它会放弃并向应用程序引发Error:maxretry
异常。
这就是为什么我每秒只能增加计数器60-70次的原因:如果有更多的写入,节点上的争用就太大了。
增量操作本质上是原子性的。您告诉数据库:无论当前值是什么,都将其增加x
。这意味着客户端永远不必知道节点的当前值,因此它也不可能猜错。它只是告诉服务器要做什么。
使用increment
时,我们具有多个客户端的流程图如下:
Client Server Client
+ + +
increment(1) | | |
| | | increment(1)
| (increment, 1) | |
+--------->---------+ (increment, 1) |
| |---------<---------+
+---------<---------+ |
| (ack, 2) |--------->---------+
| | (ack, 3) |
| | |
v v v
这两个最后的流程图的长度已经足以解释为什么在这种情况下increment
如此快了:该操作是为此而设计的,因此电线协议更接近于我们试图实现的目标。这种简单性导致在我的简单测试中,性能差异达到3倍至4倍,在生产场景中可能会更高。
当然,事务仍然有用,因为除了增量/减量操作之外还有许多原子操作。
0
。是的,这是手动工作,所以额外的赞总是受欢迎的。 :) - Frank van Puffelen