评审会上两张表争起来:订单表要不要冗余一份用户名,DBA 说违反范式,产品说列表页每次都要 join 用户表太慢。

数据冗余指同一份信息在多处保存。它本身是中性的,关键看换来了什么、付出了什么。

几种常见的冗余

  • 字段冗余:订单表存 user_name,免掉 join。用户改名后历史订单显示旧名,这往往正是业务想要的。
  • 统计冗余:文章表存 comment_count,省掉每次 count。代价是评论增删都要同步更新,得靠事务或者触发器保证。
  • 存储冗余:主从复制、多副本、RAID、异地备份,为的是可用性,不是查询性能。
  • 校验冗余:奇偶校验位、CRC、纠删码,用额外的比特换检错纠错能力。

数仓里的宽表是另一种极端:把维度全拍平进一张表,查询不用 join,代价是存储翻几倍、更新链路变长。离线分析场景这么干很常见,因为查询次数远大于写入次数。

代价在哪里

写放大和一致性。同一份数据存在两处,更新就得写两次;两次之间失败,两边就对不上。所以冗余字段一定要有明确的写入方,别让多个服务都能改同一个字段。

同步方式通常是三种:应用层双写、数据库触发器、异步任务补偿。双写最简单也最容易漏,触发器隐式难排查,异步有延迟。选一种,并且配一个对账脚本定期比对差异。

还有缓存层的冗余:Redis 里存一份热数据,数据库才是源。缓存失效策略没写对,读到的就是过期值,这类问题最难复现,因为过一会儿它自己又对了。

冗余和备份不是一回事

主从复制是冗余,不是备份。从库同步了主库的误删,一样没数据可恢复。备份要求有一份某个时间点的快照,能回到过去。把这两个混为一谈的团队,出事时最难受。

什么时候该反范式

读多写少、join 成本高、列表页 QPS 高的场景,冗余是合理的。写密集、一致性要求强(账务、库存扣减)的场景,宁可多 join 也别冗余,除非补偿逻辑已经想清楚。

顺序建议:先按范式设计,等有了真实的慢查询和压测数据,再针对具体接口做冗余。凭感觉提前冗余,后面全是对账的活。