资讯动态

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

发布时间:2026/10/14 3:33:55 来源:尧图企业网站定制
千万级亿级数据优化案例vivo 订单中心的优化方案vivo 官方商城从单体架构演进到订单中心的过程中恰好同时遇到了数据量大、吞吐量高、跨存储一致、数据迁移和分布式事务这些典型问题。它的方案不一定最前沿但胜在真实、可落地而且每一项优化都能对上一个具体业务痛点。本文把它整理成一条清晰的性能优化主线方便阅读、复盘也方便实践时作为参考展开。一、先看结论优化不是单点而是七项组合拳这个案例可以压缩成一张表问题核心手段关键取舍单体系统越来越臃肿业务架构解耦、服务化模块、数据库、服务、团队四层解耦单表数据达到千万级、亿级数据归档、水平分表冷热分离降低 B 树高度单机 MySQL 吞吐不足Redis 缓存、读写分离、分库分表订单场景优先分库分表缓存只做辅助后台复杂检索慢订单冗余到 Elasticsearch用 MQ 同步而不是 Binlog旧库迁移到新集群停机迁移夜间停机成本低回滚路径清晰微服务下跨库一致性本地消息表最终一致定时任务补偿生产稳定性行锁、幂等、熔断、监控在细节处避免重复更新和故障扩散下面按这个顺序展开。每一项都会先说明为什么做再说怎么做最后点出这个案例里的真实取舍。二、背景4 亿用户背后一个真实的订单系统截至 2021 年vivo 已经覆盖全球 4 亿多用户服务 60 多个国家和地区。这个案例对应的方案大约来自 2018 年前后当时订单数据已经达到千万级后续会继续增长到亿级。从 2017 年开始vivo 官方商城 v1.0 的单体架构开始暴露四类问题模块越来越臃肿开发效率变低性能出现瓶颈系统维护越来越困难。订单模块又是电商交易的核心历史数据不断累积单表很快逼近存储瓶颈系统也很难支撑新品发布和大促期间的流量。因此改造不是“要不要做”而是“怎么分步骤做”。这个数据量刚好适合用来讲性能优化不是小到加个索引就结束也不是大到只有顶级大厂才需要面对的规模。它足够真实也足够通用。三、优化全景一条从单体到分布式的主线单体商城订单模块臃肿业务架构解耦订单独立成系统数据库解耦订单使用独立库数据量与吞吐优化归档、分库分表、缓存查询能力补齐订单冗余到 ES迁移与一致性停机迁移、本地消息表生产细节加固锁、幂等、熔断、监控七项优化不是平铺的而是有先后依赖先做业务和数据库解耦让订单模块有独立的演进空间再解决数据量和大流量问题然后补上分库分表后的查询短板最后处理迁移、一致性和生产稳定性细节。四、优化 1业务架构解耦业务架构解耦本质上就是基于业务模块做垂直的物理拆分。订单模块从商城中拆出来独立为订单系统向商城相关系统提供订单、支付、物流、售后等标准化服务。这个拆分会顺着四个层次同步展开模块解耦 - 数据库解耦 - 服务解耦 - 团队解耦层次动作结果模块解耦订单从商城主站拆出职责边界清晰数据库解耦订单使用独立数据库避免单库集中写入服务解耦订单能力微服务化各业务线独立演进团队解耦按业务线独立负责迭代效率提升四层解耦之后团队不再挤在一个大单体里互相等待不同业务线也可以结合自己的领域特征选择更合适的存储、缓存和异步方案。五、优化 2数据量大归档与分表2017 年MySQL 中订单表数据量已经达到千万级后续订单数据还会继续增长到亿级。InnoDB 使用 B 树存储数据单表查询的时间复杂度是O(log n)。数据总量越大树越高磁盘 IO 次数越多。索引只能改善部分查询不能从根本上解决单表数据量过大的问题。因此最终手段仍然是减小单表数据量。这里做了两件事数据归档和分表。5.1 数据归档利用订单的冷热特性订单数据有明显的“热尾效应”最近一段时间的订单访问频率高较早的历史订单访问频率低。按照二八定律系统大部分性能开销集中在少量热点业务上。订单系统通常会限制用户默认查询最近 3 个月的订单历史订单需要单独进入历史订单页面。技术上可以把新老数据分开存储经常被访问的近期订单留在主表使用频率较低的历史订单迁移到历史表代码中的查询模块根据时间范围路由到不同表。这样就能减少主表的数据量同时保留历史数据可查。5.2 数据分表优先水平分表分表有两种类型拆分方式主要目的水平分表同一个库内按规则拆到多张表降低单表行数降低 B 树高度垂直分表按字段拆到多张表减少单行大小减少查询读入的列这里主要考虑的是水平分表。业内参考标准中单表数据量控制在 500 万到 1000 万时B 树高度通常保持在 2 到 3 层只需要 2 到 3 次 IO 就能读到记录。不过分表和分库通常会一起设计所以下面结合吞吐量优化一起看。六、优化 3吞吐量大缓存、读写分离、分库分表用户量和订单量快速增长后应用层和 MySQL 的吞吐都会上涨。单机 MySQL 处理能力有限压力过大时所有请求的响应时间都会拉长严重时可能把数据库拖垮。这个案例依次评估了三种手段。6.1 Redis 缓存能挡一部分但不是订单场景的主牌高并发架构通常先想到缓存。Redis 作为 MySQL 前置缓存可以挡住大量查询请求并降低响应时延。但缓存更适合局部热点、周期性热点明显的系统例如商品系统、优惠券系统、活动系统。这些场景可以叠加一级缓存、二级缓存甚至三级缓存。订单系统的特点是“每个用户的订单数据都不一样”全局热点不明显缓存命中率不会太高。因此订单系统不追求多级缓存只用 Redis 缓存最近的订单把用户最可能再次访问的数据先接住为数据库分担一部分压力。结论是缓存要上但不要把它当成订单系统唯一的性能答案。6.2 读写分离能分担查询但解决不了下单高峰读写分离的典型结构是写请求 - 主库 读请求 - 从库 主库 - Binlog 同步 - 从库主库负责数据更新从库负责分担查询。但订单系统有两个现实问题订单更新操作较多下单高峰时主库压力依然存在主从同步存在延迟即使通常小于 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专门服务后台查询。七、优化 4MySQL 与 Elasticsearch 的一致性为了支撑后台的聚合搜索和高性能检索订单数据会冗余一份到 Elasticsearch。接下来的问题是MySQL 数据变更后如何同步到 ES。要同时考虑同步时效、一致性、业务代码侵入和对服务性能的影响。备选方案主要有两个。7.1 MQ 方案订单服务更新 MySQL发送 MQ 消息ES 更新服务消费消息更新 Elasticsearch优点是延迟较低链路直观缺点是业务代码需要主动发消息。7.2 Binlog 方案MySQL 数据变更产生 BinlogCanal 伪装从库解析 BinlogES 更新服务更新 Elasticsearch优点是业务代码侵入小不直接影响服务本身缺点是链路更复杂。最终选择了 MQ 方案。原因是订单在 ES 中的数据主要给管理后台使用对可靠性和实时性的要求不是特别高。为了应对宕机和消息丢失后台还提供了按条件手动同步 ES 的功能作为补偿。这个取舍很典型不是所有同步都要追求 Binlog 这种通用方案关键看消费方对实时性和可靠性的真实要求。八、优化 5数据迁移选择“能回滚”的方案从单实例数据库迁移到新集群既要保证数据正确也要保证每一步出问题后都能快速回滚。8.1 不停机迁移大致流程是把旧库数据复制到新库启动同步程序上线双写服务先只读写旧库开启双写停止同步程序启动对比补偿逐步把读请求切到新库把读写都切到新库继续对比补偿下线旧库和双写逻辑。不停机迁移体验更好但改造复杂过程中要维护同步、双写、对比补偿等多个组件。8.2 停机迁移这个案例的实际步骤是上线新订单系统先把两个月前的订单同步到新库并稽核将商城 v1 应用停机保证旧库不再变化迁移剩余订单并稽核上线商城 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夜间停机成本低所以迁移不追求不停机。总结这个案例最有价值的地方不是它用了多少新技术而是它把每个问题都对应到了具体的业务约束和现实取舍。可以记住一句话生产方案通常不是完美的。真正能落地的方案都是在成本、风险和收益之间反复权衡后的结果。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑