发生了什么

系统判断设备在线、展示心跳时间,原来读的是一个旧字段;协议精简后改用一个靠多种事件刷新的新字段。研发把后台展示从旧字段切到新字段,并把旧字段直接下线(不再读写),以为所有设备都会更新新字段。但有一批很老的存量设备只上报旧字段、根本不刷新新字段,旧字段一下线,这批设备就没心跳数据可读,后台展示成空。实际设备在线、用户使用不受影响,只是后台看不到心跳。测试没发现,是因为测试环境里压根没有这种老型号设备。最终靠恢复旧字段读写、展示时取新旧两个字段里较新的时间来兼容,才修好。

本质

被破坏的约束是切换数据源时,新源必须已经覆盖了所有存量数据,才能安全下线旧源。这里新字段并没有覆盖那批只认旧字段的老设备,却提前把旧字段下线了,等于给一部分存量数据断了唯一的供给来源。根子是做新老替换时只考虑了主流或新设备的理想路径,没有盘点还有谁只依赖旧源,把新的能用直接当成了旧的可以删。

下次遇到这些场景要警惕

任何用新字段或新数据源替换旧的改造,下线旧源前先盘清楚:存量数据里有没有一部分只喂旧源、不产生新源数据(尤其是老型号、老版本、历史遗留的那批)。安全做法是过渡期两个源都读、展示取二者中较新或较全的值(取最大值、合并兜底),等确认新源确实全量覆盖后再下线旧源,而不是切换和下线一步到位。测试上要特别警惕测试环境没有老样本这个盲区,存量兼容类改动要么在预发或灰度用真实存量数据验证,要么专门造老型号或老数据的用例,别用清一色的新数据得出一切正常的结论。下线动作本身也要灰度和可回滚,先小批量观察这类边缘数据是否受影响。

下面用代码把直接切换下线和兼容兜底讲清楚。

问题版本:展示直接改读新字段,旧字段下线,老设备读不到值。

// 切换前:读旧字段
// long ts = redis.get("heartbeat:old:" + deviceId);

// 切换后:只读新字段,且旧字段已下线不再读写
Long ts = redis.get("active:new:" + deviceId);
// 老设备不上报新字段,ts 为 null,展示成空
return ts == null ? "--" : format(ts);

修复版本:过渡期两源都读,取较新的时间兜底,旧字段暂不下线。

Long newTs = redis.get("active:new:" + deviceId);   // 新源
Long oldTs = redis.get("heartbeat:old:" + deviceId); // 旧源恢复读写,暂不下线

// 取两者中较新的时间,兼容只喂旧源的老设备
Long ts = maxNullable(newTs, oldTs);
return ts == null ? "--" : format(ts);

// 等确认新源已全量覆盖所有存量设备后,再评估下线旧源

前后端边界:这条主要是后端或数据源改造问题,后端把展示依赖的旧字段下线了,导致只喂旧字段的老设备数据断供。后台展示端只是如实把后端返回的空值渲染成占位符,本身没错。修复在后端:恢复旧字段、展示取新旧较新值做兼容,待新源全量覆盖再考虑下线。前端无需改动。