Firebase实时数据库如何结构化存储城市基础与每日变动数据
你一开始设想的拆分独立存储路径的思路,就是Firebase Realtime Database这类NoSQL数据库的标准最优实践,完全不用怀疑。反倒是把不变的基础数据嵌套在每日更新的动态数据里重复存储,是NoSQL设计里非常典型的反模式。
先回答你第一个问题:不拆分结构能不能实现baseData仅在不存在/变更时写入?
技术上能跑通,但非常不推荐。
你确实可以在每次写入当日CityData前先发一次查询请求,判断对应城市的baseData节点是否存在、内容是否和最新拉取的数据一致,再决定要不要写入这个字段。但这个方案有两个绕不开的硬伤:
- 每次写入多了一次前置查询的网络开销,写入逻辑变复杂,高并发场景下还容易出现写入时序问题导致脏数据
- 本质上还是把冷数据(极少变更的baseData)和热数据(每日更新的动态数据)存在同一条记录下,后续不管是更新baseData(比如城市人口统计值调整),还是定期清理超过7天的过期历史数据,都要遍历所有关联的历史记录做批量修改,维护成本会随着数据量上涨越来越高。
为什么拆分路径是最优方案?
很多NoSQL新手会有个误区,觉得NoSQL不能做关联、必须把所有数据嵌在一条记录里才对——实际上NoSQL的设计核心从来不是“杜绝关联”,而是根据实际查询模式平衡读写成本、存储成本和维护成本。你担心的“每次查询要拼接两类数据”的问题,实际落地的时候成本极低:
- baseData属于极少变动的冷数据,完全可以在App启动后一次性拉取全量城市的baseData,存在本地内存或者本地缓存里,后续所有要用到城市名称、人口这类基础信息的场景,直接读本地缓存就行,根本不需要每次查动态数据的时候再重复请求baseData
- 就算不做本地预拉取,Firebase SDK本身对同路径数据有内置的离线缓存机制,重复拉取baseData的开销,远小于你每天给每个城市重复存7份一模一样的baseData浪费的存储成本和下行流量成本
- 拆分之后的维护成本会大幅下降:
- 动态数据路径下只存日期、城市ID、病例数这类每日变动的字段,存7天历史的时候每条记录只存变动内容,没有冗余
- baseData需要更新的时候,只需要修改baseData路径下对应城市ID的单条记录,所有历史、当日的动态数据关联的基础信息自动同步,不用批量修改N条历史记录
- 清理过期历史数据的时候,直接删除动态数据路径下超过7天的时间节点即可,完全不会影响baseData
推荐的落地结构参考
你原来设想的双路径结构已经很合理,只需要稍微优化字段设计即可:
// 基础数据存储路径:/cityBaseData/{cityId} { "cityId": 1, "cityName": "埃森", "population": 582567, "lastUpdateTimestamp": 1717209600000 } // 每日动态数据存储路径:/cityDailyData/{yyyyMMdd}/{cityId} { "cityId": 1, "confirmedCases": 127, "recordTimestamp": 1717209600000 }
代码层不需要做太大改动:原来的CityData类去掉嵌套的BaseData字段,只保留cityId做关联即可。业务层取数的时候,根据cityId从本地缓存的baseData映射里取出对应的基础信息,组装成需要的展示对象就行,逻辑比你每次写入前判断baseData是否要更新简单得多。
实操小技巧
如果你有列表页一次性展示所有城市当日数据+名称的高频场景,可以做极低代价的字段冗余:只把展示频率最高的cityName字段存在每日动态数据节点里,人口这类低频次使用的字段还是放在baseData路径下。这样列表展示的时候连缓存关联都不需要做,冗余成本也只是每条记录多存一个短字符串,完全可以接受。
不要为了省几行业务层的拼接逻辑,走回全量嵌套重复存储的老路——NoSQL场景下允许针对高频查询做小字段冗余,但全量重复存储整份不变的冷数据,绝对是得不偿失的选择。
内容的提问来源于stack exchange,提问作者RuhrpottDev

