Firebase Realtime Database根节点等大数据节点执行事务是否合理
对根节点、或者任何存储了大量数据的父节点执行runTransaction属于明确的不良实践,你对该方法运行机制、权限要求的理解没有偏差,问题出在事务作用节点的选择错误,而非runTransaction本身在生产环境实用性低。
- 执行
runTransaction时,SDK会先拉取你传入引用指向节点下的全量数据到本地,运行自定义更新逻辑后,再将该节点的完整新值回写到服务端;如果回写时发现该节点数据已被其他客户端修改,会自动重新拉取最新值重复整个流程,直到写入成功或达到重试上限。 - 权限要求上,事务的执行主体必须对传入的引用节点拥有完整的读、写权限。如果在根节点执行事务,就必须给执行方开放整库的读写权限,不管是下发给客户端还是给服务端使用,这个权限粒度都粗到完全不可接受。
- 性能层面,只要事务指向的节点下数据量稍大,拉取数据的流量开销、序列化/反序列化的计算成本、冲突后的重试成本都会急剧升高,甚至会直接触发数据库单次操作大小、带宽的限制,导致操作直接失败。
你认为涉及users、groups两个路径的修改就必须找它们的公共父节点(根节点)跑事务,这是非常常见的认知误区:
Firebase Realtime Database 原生支持多路径原子更新,单次请求可以同时写入任意多个不同路径的字段,服务端保证所有写入要么全部成功、要么全部失败,本身就具备原子性,完全不需要套事务覆盖公共父节点。
这个场景的正确实现代码非常简单,不需要拉取任何多余数据:
// 构造要更新的多路径键值对 const updates = {}; // 给用户的所属分组列表加对应分组 updates[`users/alovelace/groups/techpioneers`] = true; // 给分组的成员列表加对应用户 updates[`groups/techpioneers/members/alovelace`] = true; // 根节点发起原子更新,不需要读权限,只需要对两个目标写入路径有写权限即可 update(ref(database), updates);
如果你确实需要做依赖现有值的校验(比如判断分组人数是否达到上限、用户是否已加入分组),也不需要拉取根节点数据:只需要在对应数据所在的细粒度子节点上跑事务即可。比如要校验分组人数不超过100,就仅对groups/techpioneers/members节点执行事务,拉取该节点下的成员列表完成校验和更新,再搭配多路径更新完成用户侧的字段写入即可。
runTransaction在生产环境实用性很高,但仅适合作用在数据量极小、并发写入冲突概率高的浅层级/叶子节点,常见的合理使用场景包括:
- 计数器类节点:比如内容的点赞数、评论数、分组的当前成员总数,节点下仅存储一个数值,数据量极小,并发写入频繁,用事务做自增/自减可以避免并发覆盖问题
- 短列表并发修改:比如最多存储几十条记录的最近访客列表、操作日志列表,并发追加内容时用事务保证不会覆盖其他客户端的写入
- 唯一性校验:比如全局用户名、手机号的占用映射表,注册时对
usernames/xxx这类单条记录节点跑事务,判断值不存在时再写入关联用户ID,避免重名冲突
通用判断标准:如果你准备给
runTransaction传入的引用节点,其下存储的数据量可能超过几十KB,就说明你选的节点层级太高了,应该下钻到更细粒度的子节点执行事务,其余关联字段的写入用多路径原子更新完成即可。
内容的提问来源于stack exchange,提问作者Francesco

