第2篇:应付百万并发商品系统之需求文档

提醒,是付费专栏,但是在知识星球里是免费的。

这不是一份产品经理写的功能需求文档。商品系统的重构需求来自技术团队,触发原因是一次大促事故。重构的范围不只是商品系统,而是公司所有核心系统从PHP到Java的整体迁移。后续的每一个技术方案,都要回到这份文档里找依据。

文档概要

项目 内容
系统名称 C端商品系统
需求类型 技术重构(非业务功能需求)
需求发起方 CTO发起,技术委员会推动
重构目标 将商品系统从PHP迁移到Java,重建数据模型和缓存体系
涉及团队 重构部(商品/库存组)、数据架构、基础架构、业务架构、QA

需求背景

业务现状

公司是一家几亿用户规模的互联网公司,业务覆盖多个C端渠道。当时所有核心系统都是PHP实现的,商品、库存、订单、支付、用户、营销,全部是PHP。

商品系统是公司最底层的基础数据服务。首页、搜索、商详页、购物车、下单、收藏夹、推荐,几乎所有面向C端用户的业务系统都从商品系统读取数据。平时流量就不小,一到大促,商品域的流量在整个公司排第一。

PHP时期的商品系统,日常流量下能用,瓶颈已经很明显了。

触发事件

某一年的大促活动,系统直接瘫痪了。

不是某个接口响应慢、某个页面打不开,是核心交易链路整体不可用,持续了半个多小时。用户打开APP什么都干不了,不能浏览商品,不能下单,不能支付。

公司为这场大促投了大量广告费做预热和推广。活动还没开始多久,系统就挂了。CEO说了一句话:公司花了那么多广告费来做这场大促,结果没取得预期的效果。

这次事故的直接后果:

  • 大促期间的预期营收目标没有达成
  • 大量用户涌入后遇到系统不可用,用户体验严重受损
  • 运营团队此后对大规模活动持谨慎态度,不敢轻易放量
  • 技术团队事后复盘确认:PHP系统在架构层面已经无法承载当前的业务规模

重构决策

那场事故之后,CTO做了一个决定:用Java重构公司所有核心系统。

这不是修修补补式的优化,是一次彻底的技术革命。

CTO成立了一个专门的重构部门,50号人,从业界挖了不少在高并发、大数据量场景下有实战经验的技术专家。各路架构师全部上场:数据架构师负责新的数据模型设计,业务架构师负责业务领域的拆分和梳理,基础架构师负责中间件和基础设施的选型与搭建。

新的业务需求基本暂停了,所有技术资源集中在重构上。从立项到核心系统全部切换到Java,耗时差不多一年。

重构全景

公司整体的「电商」业务从技术视角可以划分为三条线。

选购线。 用户浏览、搜索、查看商品详情、收藏的完整链路。商品系统和库存系统是这条线的核心。

下单支付线。 用户从购物车发起下单、选择支付方式、完成支付的完整链路。订单系统和支付系统是这条线的核心。

用户营销会员线。 用户注册、登录、会员等级、积分、优惠券、营销活动等。用户系统和营销系统是这条线的核心。

三条线不是完全独立的。下单需要读取商品数据(名称、价格、库存),营销活动需要关联商品信息。商品系统作为基础数据源,被其他两条线的多个系统依赖。

我当时在重构部,负责选购线中的商品系统和库存系统的重构。这个专栏聚焦商品系统,库存系统的重构不在范围内。

商品系统的重构范围:

  • 商品数据模型重新设计(从PHP的宽表迁移到EAV模型)
  • C端商品读服务全量重写
  • 多级缓存体系建设(OHC堆外缓存 + Redis中央缓存)
  • 读接口API重新设计
  • 十几亿条商品数据的迁移
  • 灰度上线和数据质检

商品系统的业务职责

在讲技术问题之前,先把商品系统在业务上干的事情说清楚。

商品系统管理的是公司所有在售商品的基础数据。这些数据对C端用户是只读的,由运营人员在后台维护。C端用户通过各种入口浏览和查询商品信息,不会修改数据。

调用方

开篇词里提过,商品系统被公司几乎所有C端业务系统依赖。具体的调用方和各自需要的数据:

调用方 需要的数据 调用特征
首页 商品名称、图片、价格 高频,集中在少数热门商品
搜索 分类、标签、名称、价格 中频,查询条件多变
商详页 全量商品信息(基础属性 + 销售属性 + 价格 + 库存状态) 高频,单品查询
购物车 名称、图片、价格、库存状态 中频,批量查询
下单 价格、库存、商品状态 低频,对一致性要求最高
收藏夹 名称、图片、价格 低频,大促期间会突增
推荐 分类、标签、销售属性 中频,批量查询

不同调用方需要的字段差异很大。这个特征直接影响了后面读接口的API设计方式。

数据类型

商品系统管理的数据分三类。

基础信息。 商品名称、描述、图片、分类、品牌。创建后很少修改。

销售属性。 运营会频繁新增的字段:是否爆款、是否支持某种折扣、是否在特定频道展示、适用人群标签。这类属性的特点是数量不固定,运营每隔一段时间就会提需求要加新的属性。

价格与状态。 价格、上下架状态、可售渠道。修改频率中等,对一致性要求高。

流量特征

商品系统的流量模式是读多写少。C端用户只读不写,写操作来自运营后台,量很小。读流量有两个特征。

日常流量持续且不低。 不像秒杀系统有明确的时间窗口,商品数据的读取是全天候的。任何时候用户打开APP、浏览商品、搜索,都在读商品数据。

大促期间出现明显的流量脉冲。 用户提前收藏商品,活动一开始从收藏夹涌入商品详情页,短时间内集中读取同一批商品。开篇词里提过的峰值数据:32台机器上的70万并发读请求。

现有PHP系统的问题

重构不是因为PHP这个语言有问题,是因为当时的PHP系统在那个业务规模下已经到了瓶颈。

性能瓶颈

PHP-FPM的每个请求由一个独立的Worker进程处理,进程之间不共享内存。高并发场景下,能开多少Worker进程直接决定了并发处理能力的上限。进程数到一定程度后,操作系统在进程切换上的开销会明显增加,吞吐量反而下降。

当时的商品系统在日常流量下已经需要大量机器来撑。大促时流量翻几十倍,靠加机器已经不现实了。

缓存能力受限

PHP进程之间内存不共享,这一点影响很大。Java应用可以在JVM层面做本地缓存,商品数据加载到内存后,同一个JVM里的所有请求都能直接读取,不需要走网络。PHP做不到,每个请求要么从数据库读,要么走Redis这类外部缓存。

商品数据体量有好几个G。全部走Redis,网络开销在高并发下会成为瓶颈。Java可以用OHC这类堆外缓存把热数据放在本机内存里,极大减少对外部缓存的依赖。PHP没有对等的技术方案。

数据模型僵化

PHP时期的商品表是传统的宽表结构。每新增一个销售属性,需要DDL改表结构,加一列。表结构改动意味着走发版流程:协调DBA审核SQL、选择低峰期执行、验证线上数据。运营新增属性的需求很频繁,加一个字段的周期如果是一周,一个月要加三四次,这个流程的效率问题就暴露出来了。

团队结构

公司的技术团队以Java工程师为主,PHP工程师的招聘难度越来越大。核心系统用PHP,意味着一个很小的人才池在维护一个很关键的系统,人员风险很高。

重构目标

技术指标

维度 PHP时期 重构目标
单机并发处理能力 几百QPS 几万QPS
大促峰值支撑 扛不住,靠堆机器 32台机器扛住70万并发读
本地缓存 无(进程间内存不共享) OHC堆外缓存,G级别数据量
新增销售属性 DDL改表,走发版流程 数据库加一行记录,不需要发版
读接口灵活性 全量返回或定制接口 按需返回,调用方声明需要哪些字段

架构目标

读写分离。 C端读服务和运营后台写服务分开部署,读服务可以独立扩容,不受写操作影响。

多级缓存。 OHC堆外缓存作为一级缓存,Redis作为中央缓存,MySQL作为数据源。三层之间的加载、更新、失效策略要形成一套完整的体系。

流量隔离。 大促流量和日常流量走不同的处理通道,互不影响。

平滑迁移。 不能停服务直接割接。需要双写、灰度、回滚的完整机制,保证迁移过程中线上业务不受影响。

核心技术需求

这一章把重构需要解决的关键技术问题列出来。每个问题在专栏后续的文章里会有完整的设计和实现,这里只做概述。

数据模型:EAV

传统宽表模式下,每个销售属性是表里的一列。商品表有几十列甚至上百列,大部分在某个具体商品上是空值。每加一个新属性就要DDL。

重构后采用EAV模型(Entity-Attribute-Value)。每个属性是表里的一行记录,而不是一列。记录里包含属性名称、属性类型、属性值等元数据信息。新增属性只需要插入一条记录,不需要改表结构。

EAV的代价是查询和代码层面的复杂度增加。查一个商品的完整信息,需要聚合多行属性记录。参数校验不能靠字段类型硬编码,要先读属性的元数据再做校验。

缓存体系

商品数据量大(G级别),读频率高(日常持续加上大促脉冲),对缓存体系的要求比一般系统高很多。

本地缓存:OHC。 商品数据放JVM堆内会严重影响GC停顿。OHC把数据存在JVM堆外的直接内存中,不参与垃圾回收扫描。需要自己处理序列化和反序列化,每种商品数据类型有独立的Loader。

中央缓存:Redis。 本地缓存未命中的请求走Redis。需要设计Key命名规则、过期策略、大Value拆分方案、防穿透和防击穿机制、热Key处理。

预热与更新。 本地缓存的预热策略、过期后的更新方式、运营修改数据后各层缓存的一致性处理。

读接口设计

商品系统被七八个业务系统调用,每个调用方需要的字段不同。需要一种API设计方式,让调用方声明自己需要哪些数据字段,系统按需组装和返回。避免每个调用方定制一个接口导致接口数量爆炸,也避免一个全量接口把大量无用数据传给调用方。

高并发支撑

数据库一主多从。 读请求分散到多个从库。写操作走主库后,主从之间存在同步延迟。运营刚修改了商品信息,C端用户可能读到旧数据。主从延迟的处理需要专门的方案。

大促流量隔离。 大促开始的瞬间,收藏夹的流量涌入商品详情页,集中在少数热门商品上。如果和正常浏览走同一条链路,正常请求会被挤压。需要在流量层面做隔离。

数据迁移

商品数据量在十几亿级别。旧表是PHP的宽表结构,新表是EAV模型,表结构完全不同。迁移要解决几个问题:

  • 新旧表的字段映射关系
  • 双写方案:迁移期间新老系统同时写,保证数据一致
  • 十几亿数据的全量迁移,不能影响线上服务
  • 灰度切换:按比例把流量从旧系统切到新系统
  • 回滚方案:灰度过程中发现问题能快速回退
  • 数据质检:迁移完成后验证数据的完整性和准确性

约束条件

约束 说明
不能停服 迁移和切换全程在线进行,用户无感知
数据零丢失 十几亿条数据,迁移后一条都不能少
可回滚 灰度过程中任何阶段发现问题,能在分钟级别回退到旧系统
新需求暂停 重构期间不做新业务需求,避免新老系统同时改造
跨团队协调 所有依赖商品数据的业务系统需要配合接入新接口

验收标准

性能

指标 标准
大促峰值支撑 32台机器扛住70万并发读请求
P99响应时间 读接口 ≤ 10ms(缓存命中场景)
缓存命中率 本地缓存 + Redis整体命中率 ≥ 99%

数据

指标 标准
迁移完整性 十几亿条数据零丢失
双写期间一致性 新旧系统数据对账差异为零
灰度回滚时间 ≤ 5分钟

稳定性

指标 标准
系统可用性 ≥ 99.99%
大促流量隔离 大促期间对正常浏览请求零影响
主从延迟处理 运营修改后,C端最迟1秒内读到最新数据

风险评估

风险项 影响程度 发生概率 应对措施
数据迁移过程中数据丢失 全量对账加增量校验,发现差异立即补偿
双写导致新老系统数据不一致 定时对账任务,异常数据自动告警
灰度切换后新系统性能不达标 灰度比例从1%开始逐步放量,每个阶段充分观察
EAV模型查询性能不如宽表 用缓存体系弥补,热数据走本地缓存,数据库查询频率极低
OHC序列化反序列化的CPU开销 提前做性能测试,选择高效的序列化方案
调用方切换新接口出现兼容性问题 提供详细的接口文档,安排专人对接每个调用方
重构周期过长导致业务需求积压 核心模块分批交付,优先完成读服务切换

术语表

术语 定义
EAV Entity-Attribute-Value,实体-属性-值。一种数据模型,每个属性存储为表中的一行记录而非一列。适用于属性数量不固定、需要频繁新增的场景
OHC Off-Heap Cache,堆外缓存。将数据存储在JVM堆外的直接内存中,不参与垃圾回收
Loader 缓存加载器。负责从数据源加载数据并写入缓存。商品系统中每种数据类型有独立的Loader
双写 迁移期间的数据写入策略。一次写操作同时写入新旧两套系统,保证两边数据一致
灰度 按比例将线上流量从旧系统逐步切换到新系统的过程
数据质检 迁移完成后对比新旧系统数据、确认完全一致的验证过程
宽表 传统的数据库表设计方式,所有字段作为列。属性固定时适用,频繁加列时效率低

小结

技术重构和新建系统有一个根本区别:新建系统的约束少,方案可以自由发挥;重构是在不影响存量业务的前提下替换底层实现,约束条件决定了方案的复杂度上限。不能停服、数据不能丢、要能回滚、调用方无感切换,这些约束叠加在一起,技术方案的复杂度远超从零开始建一个新系统。

这份文档把重构的背景、范围、目标、约束和验收标准写清楚了。后面的技术方案设计,每一个选择都能在这份文档里找到对应的依据。EAV模型对应的是运营频繁新增属性的需求,OHC堆外缓存对应的是商品数据量大到堆内放不下的现实,流量隔离对应的是大促流量会冲击正常浏览的问题。

技术方案不是凭空设计的,它是从需求和约束条件里推导出来的。

所有的代码都可以在知识星球里获取。这个专栏在星球里是免费的,也可以接受无限次的咨询。后续新写的所有付费专栏,在知识星球里都是免费的。我的星球是:

  • 老码头的技术浮生录

专栏发布在知乎,欢迎订阅,我的知乎名是:

  • SamDeepThinking
相关推荐
血小溅7 小时前
Git Submodule 实战指南:从基础概念到 AI-Native 项目落地
后端
用户21991679703917 小时前
基于.Net的NetCoreKevin框架中AgentFramework实现AI智能体Skill和工具动态管理和加载
后端
日月云棠7 小时前
6 高级配置:Spring Boot整合、泛化调用与配置指南
java·后端
SE_NAK7 小时前
go-zero 两个限流器都踩了坑,最后自行实现了一个分布式令牌桶
后端
云烟成雨TD7 小时前
Spring AI Alibaba 1.x 系列【58】Spring AI Alibaba Builtin Nodes 模块介绍
java·人工智能·spring
wyu729617 小时前
SpringBoot学习记录,一个小项目实战
java
苏三说技术7 小时前
Durid和HikariCP,哪个连接池更好?
后端
思考着亮7 小时前
1.DDL(数据定义语言)
后端
小江的记录本7 小时前
【Java基础】反射与注解:核心原理、自定义注解、注解解析方式(附《思维导图》+《面试高频考点清单》)
java·数据结构·python·mysql·spring·面试·maven