我正在Firebase中构建一个实时轮询。每个投票都存储在列表字段中。为了防止为了计数而不得不将每一张选票都拉到客户端,我将每一个选择的计数缓存在计数器字段中。
poll1
counts
choice1: 5
choice2: 2
choice3: 10
choice4: 252
voters
uid1 : choice1
uid6 : choice3
uid25: choice2
uid31: choice1
我目前正在用以下事务更新计数器:
var fireCountPush = new Firebase(self.fireCountUrl+node+id);
fireCountPush.transaction(function(current_value) {
return current_value + 1;
}, function(error, committed, snapshot) {
if(committed) {
var fireVote = new Firebase(self.fireVotesUrl);
fireVote.child(self.user.uid).set(id);
}
});
但是,我希望原子式地将用户添加到投票者列表中,最好是在同一事务中。不幸的是,按照我现在的方式,我必须在事务成功提交后添加用户。这是一个巨大的安全问题,因为通过编辑脚本可以很容易地在浏览器中禁用它。
是否有任何方法可以更新计数器和将用户添加到投票者列表中,而不必下载事务中的整个对象?
这里最简单的答案是让中立方(服务器脚本)监视投票者列表并增加计数器。然后只需要确保用户通过他们的uid添加自己,并且只能添加一次。
我相信还有一些绝妙的方法可以完全利用安全规则来做到这一点。不幸的是,我并没有那么出色,但如果你真的想要一个只针对客户的解决方案,这里有一种蛮力方法可以改进。
计划:
- 强制用户先写入审核记录
- 迫使他们把自己的名字排在第二位
- 允许他们在这两个记录都存在并且与投票号码匹配时更新计数器
模式:
/audit/<$x>/<$user_id>
/voters/$user_id/<$x>
/total/<$x>
我们阻止用户修改审核/如果他们已经投票(投票者/$user_id存在),或者如果审核记录已经存在(有人已经声称计数),或者投票没有增加一:
"audit": {
"$x": {
".write": "newData.exists() && !data.exists()", // no delete, no overwrite
".validate": "!root.child('voters/'+auth.uid).exists() && $x === root.child('total')+1"
}
}
您将在事务中更新audit
,本质上是尝试"声明"每个增量,直到成功,并在要添加的记录不为空(有人已经声明了它)时取消事务(通过返回未定义)。这将为您提供唯一的投票号码。
为了防止任何有趣的事情,我们存储了一个选民列表,强制每个选民只在审计中写入一次。只有在我以前从未投票过的情况下,并且只有在已经用我的唯一投票号码创建了审计记录的情况下我才能写信给选民:
"voters": {
"$user_id": {
".write": "newData.exists() && !data.exists()", // no delete, no replace
".validate": "newData.isNumber() && root.child('audit/'+newData.val()).val() === $user_id"
}
}
最后,但并非最不重要的是,我们更新计数器以匹配我们声称的投票id。它必须与我的投票号码匹配,而且可能只会增加。这防止了一个用户创建审计记录和选民记录,但在我完成三步之前,其他人已经增加了总数的竞争情况。
"total": {
".write": "newData.exists()", // no delete
".validate": "newData.isNumber() && newData.val() === root.child('audit/'+auth.uid).val() && newData.val() > data.val()"
}
更新总数,就像添加初始审计记录一样,将在事务中完成。如果总数的当前值大于我分配的投票数,那么我就取消与undefined
的交易,因为其他人已经在我之后投票并将其更新得更高。没问题。
接着我的第二条评论-为什么要单独计数?
在您推送的UniqueId记录中保留一个计数(每个新记录递增),不要关注计数的变化,而是关注UniqeID记录上的"add_child",限制为1。这将返回当前总数。
/userId/=计数
作业完成-一次更新。
编辑
如果你还在考虑。。。我会给你一个设计模式,以减轻它缺乏原子性。。。
"喷涂和模具更新"
我们有无法驱动更新的验证,但知道更新应该是什么(计数应该增加一个,并且只有一个)——这是被动模板,只是过滤掉不应该通过的内容。
我们有一个客户端,它可以驱动更新,但不能完全确定更新应该是什么。它想将计数更新一,但自上次读取以来,计数可能会增加!这将无法通过验证,它将不得不再次尝试,但同样的情况也可能发生,我们有比赛条件。。。
尝试一次更新并检查结果就像使用带有sencil的精细笔刷一样——这是多余的。因此,让我们将更新变成一个循环中的"喷雾",无需检查结果,只需调用更新-尝试将计数更新1、2、3、4、5、6…其中一个将是正确的并更新!其他的将失败,要么是因为用户已经投票(之前的更新有效),要么是由于索引错误!只要在读取最后一个索引和喷洒更新之间,喷洒的大小(尝试写入的次数)大于其他投票者的数量,你就100%肯定会成功。
老派纯粹主义者不喜欢这个想法,但他们将SVN优先于GIT,并将使用SQL而不是firebase:)