☰
千万级亿级数据优化案例:vivo订单中心的优化方案
2026/10/14 3:33:46 网站建设 项目流程

千万级亿级数据优化案例:vivo 订单中心的优化方案

vivo 官方商城从单体架构演进到订单中心的过程中,恰好同时遇到了数据量大、吞吐量高、跨存储一致、数据迁移和分布式事务这些典型问题。它的方案不一定最前沿,但胜在真实、可落地,而且每一项优化都能对上一个具体业务痛点。

本文把它整理成一条清晰的性能优化主线,方便阅读、复盘,也方便实践时作为参考展开。

一、先看结论:优化不是单点,而是七项组合拳

这个案例可以压缩成一张表:

问题核心手段关键取舍
单体系统越来越臃肿业务架构解耦、服务化模块、数据库、服务、团队四层解耦
单表数据达到千万级、亿级数据归档、水平分表冷热分离,降低 B+ 树高度
单机 MySQL 吞吐不足Redis 缓存、读写分离、分库分表订单场景优先分库分表,缓存只做辅助
后台复杂检索慢订单冗余到 Elasticsearch用 MQ 同步,而不是 Binlog
旧库迁移到新集群停机迁移夜间停机成本低,回滚路径清晰
微服务下跨库一致性本地消息表最终一致,定时任务补偿
生产稳定性行锁、幂等、熔断、监控在细节处避免重复更新和故障扩散

下面按这个顺序展开。每一项都会先说明为什么做,再说怎么做,最后点出这个案例里的真实取舍。

二、背景:4 亿用户背后,一个真实的订单系统

截至 2021 年,vivo 已经覆盖全球 4 亿多用户,服务 60 多个国家和地区。这个案例对应的方案大约来自 2018 年前后,当时订单数据已经达到千万级,后续会继续增长到亿级。

从 2017 年开始,vivo 官方商城 v1.0 的单体架构开始暴露四类问题:

  1. 模块越来越臃肿;
  2. 开发效率变低;
  3. 性能出现瓶颈;
  4. 系统维护越来越困难。

订单模块又是电商交易的核心,历史数据不断累积,单表很快逼近存储瓶颈,系统也很难支撑新品发布和大促期间的流量。因此,改造不是“要不要做”,而是“怎么分步骤做”。

这个数据量刚好适合用来讲性能优化:不是小到加个索引就结束,也不是大到只有顶级大厂才需要面对的规模。它足够真实,也足够通用。

三、优化全景:一条从单体到分布式的主线

单体商城
订单模块臃肿

业务架构解耦
订单独立成系统

数据库解耦
订单使用独立库

数据量与吞吐优化
归档、分库分表、缓存

查询能力补齐
订单冗余到 ES

迁移与一致性
停机迁移、本地消息表

生产细节加固
锁、幂等、熔断、监控

七项优化不是平铺的,而是有先后依赖:

  1. 先做业务和数据库解耦,让订单模块有独立的演进空间;
  2. 再解决数据量和大流量问题;
  3. 然后补上分库分表后的查询短板;
  4. 最后处理迁移、一致性和生产稳定性细节。

四、优化 1:业务架构解耦

业务架构解耦,本质上就是基于业务模块做垂直的物理拆分。

订单模块从商城中拆出来,独立为订单系统,向商城相关系统提供订单、支付、物流、售后等标准化服务。这个拆分会顺着四个层次同步展开:

模块解耦 -> 数据库解耦 -> 服务解耦 -> 团队解耦
层次动作结果
模块解耦订单从商城主站拆出职责边界清晰
数据库解耦订单使用独立数据库避免单库集中写入
服务解耦订单能力微服务化各业务线独立演进
团队解耦按业务线独立负责迭代效率提升

四层解耦之后,团队不再挤在一个大单体里互相等待,不同业务线也可以结合自己的领域特征选择更合适的存储、缓存和异步方案。

五、优化 2:数据量大,归档与分表

2017 年,MySQL 中订单表数据量已经达到千万级,后续订单数据还会继续增长到亿级。

InnoDB 使用 B+ 树存储数据,单表查询的时间复杂度是O(log n)。数据总量越大,树越高,磁盘 IO 次数越多。索引只能改善部分查询,不能从根本上解决单表数据量过大的问题。因此,最终手段仍然是减小单表数据量。

这里做了两件事:数据归档和分表。

5.1 数据归档:利用订单的冷热特性

订单数据有明显的“热尾效应”:

  • 最近一段时间的订单访问频率高;
  • 较早的历史订单访问频率低。

按照二八定律,系统大部分性能开销集中在少量热点业务上。订单系统通常会限制用户默认查询最近 3 个月的订单,历史订单需要单独进入历史订单页面。

技术上,可以把新老数据分开存储:

  1. 经常被访问的近期订单留在主表;
  2. 使用频率较低的历史订单迁移到历史表;
  3. 代码中的查询模块根据时间范围路由到不同表。

这样就能减少主表的数据量,同时保留历史数据可查。

5.2 数据分表:优先水平分表

分表有两种:

类型拆分方式主要目的
水平分表同一个库内,按规则拆到多张表降低单表行数,降低 B+ 树高度
垂直分表按字段拆到多张表减少单行大小,减少查询读入的列

这里主要考虑的是水平分表。业内参考标准中,单表数据量控制在 500 万到 1000 万时,B+ 树高度通常保持在 2 到 3 层,只需要 2 到 3 次 IO 就能读到记录。

不过,分表和分库通常会一起设计,所以下面结合吞吐量优化一起看。

六、优化 3:吞吐量大,缓存、读写分离、分库分表

用户量和订单量快速增长后,应用层和 MySQL 的吞吐都会上涨。单机 MySQL 处理能力有限,压力过大时,所有请求的响应时间都会拉长,严重时可能把数据库拖垮。

这个案例依次评估了三种手段。

6.1 Redis 缓存:能挡一部分,但不是订单场景的主牌

高并发架构通常先想到缓存。Redis 作为 MySQL 前置缓存,可以挡住大量查询请求并降低响应时延。

但缓存更适合局部热点、周期性热点明显的系统,例如商品系统、优惠券系统、活动系统。这些场景可以叠加一级缓存、二级缓存甚至三级缓存。

订单系统的特点是“每个用户的订单数据都不一样”,全局热点不明显,缓存命中率不会太高。因此订单系统不追求多级缓存,只用 Redis 缓存最近的订单,把用户最可能再次访问的数据先接住,为数据库分担一部分压力。

结论是:缓存要上,但不要把它当成订单系统唯一的性能答案。

6.2 读写分离:能分担查询,但解决不了下单高峰

读写分离的典型结构是:

写请求 -> 主库 读请求 -> 从库 主库 -> Binlog 同步 -> 从库

主库负责数据更新,从库负责分担查询。但订单系统有两个现实问题:

  1. 订单更新操作较多,下单高峰时主库压力依然存在;
  2. 主从同步存在延迟,即使通常小于 1ms,也可能出现某个时刻数据不一致。

因此,读写分离可以缓解查询压力,但不能单独解决下单高峰的主库写入压力。业务上还需要对延迟做兼容,例如下单成功后先进入下单成功页,用户主动查看订单时才读取最新状态。

6.3 分库分表:最终落在核心方案上

分库也分两种:

类型拆分方式
垂直分库按业务把表分到不同库,专库专用
水平分库把同一张表的数据按规则拆到不同库

在订单场景中:

  • 分库解决整体高吞吐;
  • 分表解决单表高吞吐;
  • 二者结合,才能同时解决数据量和并发写入。

综合考虑改造成本、优化效果和对现有业务的影响,最终选择直接上分库分表。

6.4 Sharding-JDBC:技术选型和路由策略

分库分表的常见技术路线有客户端 SDK、代理中间件、公司自研框架和自己造轮子。结合已有项目经验和公司中间件团队的建议,这里选用了开源的 Sharding-JDBC。

它的几个特点比较契合场景:

  • 以 Jar 包方式提供,属于客户端分片;
  • 支持 XA 事务;
  • 社区活跃,网络资料、源码解析和 Demo 较丰富。

路由策略以用户标识作为分片键:

dbIndex = hash(userId) / m % n tableIndex = hash(userId) % m

其中n是库数量,m是每个库的表数量。

6.5 分库分表之后的三个现实挑战

分库分表解决了数据量和并发问题,但会限制数据库的关联查询能力。原来很简单的 Join,在分片后可能无法直接执行,需要改写 SQL,或者用其他方案补齐。

这里重点处理了三个问题。

全局唯一 ID

分库分表后,数据库自增主键不再全局唯一,不能直接作为订单号。同时,很多内部系统之间只传订单号,没有用户标识这个分片键,无法直接定位库表。

解决方案是在生成订单号时,把库表编号隐含在其中。这样即使没有userId,也能从订单号解析出库表位置。

全局 ID 还需要考虑高并发、高性能和时钟回拨等问题,常见方案包括 Twitter Snowflake、百度 UidGenerator、Sharding-JDBC 的雪花 ID 等。

历史订单号没有隐含库表信息

历史订单号无法反向解析出库表位置。做法是用一张映射表单独保存历史订单号和用户标识的对应关系。随着时间推移,这些旧订单逐渐退出系统间交互,映射表的访问频率会逐步降低。

管理后台复杂筛选与分页查询

分库分表后,管理后台如果还要按多种条件筛选、分页查询所有订单,会非常困难。

方案是把订单数据冗余到 Elasticsearch,专门服务后台查询。

七、优化 4:MySQL 与 Elasticsearch 的一致性

为了支撑后台的聚合搜索和高性能检索,订单数据会冗余一份到 Elasticsearch。接下来的问题是:MySQL 数据变更后,如何同步到 ES。

要同时考虑同步时效、一致性、业务代码侵入和对服务性能的影响。备选方案主要有两个。

7.1 MQ 方案

订单服务
更新 MySQL

发送 MQ 消息

ES 更新服务
消费消息

更新 Elasticsearch

优点是延迟较低,链路直观;缺点是业务代码需要主动发消息。

7.2 Binlog 方案

MySQL 数据变更

产生 Binlog

Canal 伪装从库
解析 Binlog

ES 更新服务

更新 Elasticsearch

优点是业务代码侵入小,不直接影响服务本身;缺点是链路更复杂。

最终选择了 MQ 方案。原因是订单在 ES 中的数据主要给管理后台使用,对可靠性和实时性的要求不是特别高。为了应对宕机和消息丢失,后台还提供了按条件手动同步 ES 的功能作为补偿。

这个取舍很典型:不是所有同步都要追求 Binlog 这种通用方案,关键看消费方对实时性和可靠性的真实要求。

八、优化 5:数据迁移,选择“能回滚”的方案

从单实例数据库迁移到新集群,既要保证数据正确,也要保证每一步出问题后都能快速回滚。

8.1 不停机迁移

大致流程是:

  1. 把旧库数据复制到新库,启动同步程序;
  2. 上线双写服务,先只读写旧库;
  3. 开启双写,停止同步程序,启动对比补偿;
  4. 逐步把读请求切到新库;
  5. 把读写都切到新库,继续对比补偿;
  6. 下线旧库和双写逻辑。

不停机迁移体验更好,但改造复杂,过程中要维护同步、双写、对比补偿等多个组件。

8.2 停机迁移

这个案例的实际步骤是:

  1. 上线新订单系统,先把两个月前的订单同步到新库并稽核;
  2. 将商城 v1 应用停机,保证旧库不再变化;
  3. 迁移剩余订单并稽核;
  4. 上线商城 v2 应用,验证失败则回退到 v1。

夜间停机对商城业务损失不大,而停机迁移的链路更短,回滚也更直接,所以最终选择停机迁移。

这再次说明,生产方案未必追求“更高大上”,更重要的是成本、风险和可回滚性是否匹配业务。

九、优化 6:分布式事务,用本地消息表做最终一致

从单体拆成微服务后,数据库的 ACID 事务无法继续覆盖跨服务调用。例如:

  • 用户支付成功后,需要通知发货系统发货;
  • 用户确认收货后,需要通知积分系统赠送积分。

业界方案很多。强一致性方向有 2PC、3PC,最终一致性方向有 TCC、本地消息表、事务消息和最大努力通知等。

高并发场景下,这里选择了本地消息表:

本地事务内: 1. 执行业务操作 2. 将需要异步执行的后续动作写入本地消息表 3. 提交事务 事务外: 定时任务扫描未成功消息并补偿

本地消息表的核心思想是,把“业务变更”和“待发送的异步操作”放在同一个本地事务里,保证它们要么都成功,要么都失败。之后再由消费端或定时任务完成最终送达。

这种方式不追求强一致,但实现简单,可控性强,适合订单完成通知积分这类最终一致场景。

十、优化 7:生产细节加固

除了大的架构调整,还有一批细节决定系统能否长期稳定运行。

10.1 网络隔离

只有极少数第三方接口可以通过外网访问,并且都会验证签名。内部系统交互使用内网域名和 RPC 接口,不重复做签名,既提升性能,也提升安全性。

10.2 数据库行级锁

分布式场景下,同一个订单可能被并发更新。任何订单更新操作前,都通过数据库行级锁加以限制,避免并发写冲突。

10.3 接口幂等

网络超时重试可能导致同一个订单被重复更新。所有接口都具备幂等能力,减少重试带来的副作用。

10.4 熔断保护

使用 Hystrix 对外部系统的实时调用加熔断保护,防止某个系统故障扩散为整体雪崩。

10.5 监控告警

通过日志平台的错误日志告警、调用链服务分析,以及公司中间件和基础组件的监控告警,第一时间发现异常。

10.6 MQ 同步 ES 的消息有序性

用 MQ 同步订单到 ES 时,多个消费者线程可能乱序执行,导致旧数据覆盖新数据。

解决方案是:查询订单数据时加行锁,整个业务在事务中执行,完成后再处理下一条,保证同一个订单的同步顺序。

10.7 Sharding-JDBC 分页问题

在 Sharding-JDBC 3.1.1 中,当group by和order by字段或顺序不一致时,SQL 改写可能把分页数量置为Integer.MAX_VALUE,导致分页失效。

例如:

SELECTaFROMtempGROUPBYa,bORDERBYaDESCLIMIT1,10;

优化后的写法可以减少分页失效风险:

SELECTaFROMtempGROUPBYaDESC,bLIMIT1,10;

实践中遇到分页异常时,除了检查业务 SQL,也要关注分片中间件版本和 SQL 改写逻辑。

10.8 ES 分页召回问题

ES 分页查询如果排序字段存在重复值,翻页时可能漏数据或重复数据。

解决方法是增加一个唯一字段作为第二排序条件,或者直接使用唯一值排序。例如订单创建时间在同一时刻可能重复,就应该再叠加订单号等唯一字段,保证排序稳定。

十一、优化结果与官方建议

这次升级的最终结果是:

  • 一次性上线成功,稳定运行一年多;
  • 核心服务性能提升十倍以上;
  • 系统解耦后,迭代效率大幅提升;
  • 架构能够支撑商城至少五年的高速发展。

更值得学习的是设计态度。vivo 商城团队没有一味追求前沿技术,而是根据业务实际状况选择最合适的方案。他们强调,好的系统不是一开始就被“大牛”设计出来的,而是随着业务发展逐步迭代出来的。

十二、小结

千万级亿级数据优化主线:

先判断问题在哪一层: 1. 单表数据量大:归档冷数据 + 水平分表,降低 B+ 树高度; 2. 并发吞吐高:缓存 + 读写分离 + 分库分表; 3. 分片后复杂查询难:订单冗余到 ES,管理后台走搜索; 4. MySQL 到 ES 的一致性:MQ 或 Binlog,按实时性和侵入性选型; 5. 迁移和跨服务事务:停机迁移保证可回滚,本地消息表保证最终一致; 6. 生产细节:行锁、幂等、熔断、监控、消息有序、分页排序。

选型理由:订单系统缓存命中率低,所以缓存只辅助,分库分表才是主力;ES 数据只给后台使用,所以选 MQ 而不是 Binlog;夜间停机成本低,所以迁移不追求不停机。

总结

这个案例最有价值的地方,不是它用了多少新技术,而是它把每个问题都对应到了具体的业务约束和现实取舍。

可以记住一句话:

生产方案通常不是完美的。真正能落地的方案,都是在成本、风险和收益之间反复权衡后的结果。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询