电商系统开发中,真正决定大促高峰能否扛住的,往往不是“用了什么数据库”,而是数据库设计是否把读写路径、库存一致性、热点分布和故障降级拆开了。我的经验是:同样使用关系型数据库,单库单表、读写分离、分库分表、事件驱动和多级缓存,最终表现可能相差数十倍;但如果业务边界没有先理清,分库分表反而可能让一次简单下单变成跨库事务、库存超卖和对账困难的组合问题。
很多技术负责人一讨论大促,就先问“要不要分库分表”“要不要换成分布式数据库”。但在真实项目里,数据库被压垮之前,通常已经出现了更早的信号:商品详情接口重复查询促销规则、订单列表一次加载过多字段、库存扣减没有按商品维度隔离、后台报表直接扫描交易明细。
这些问题属于访问路径设计错误,不是数据库品牌或硬件规格不足。一个没有做读写拆分的单库系统,如果把商品详情、购物车、库存预扣、支付回调和经营报表全部压在同一组连接池上,即使增加 CPU 和内存,也只是把故障时间向后推。
我的核心判断是:先按业务读写特征拆路径,再决定数据库拓扑;先定义一致性边界,再决定是否跨库。数据库方案的复杂度,必须服务于业务峰值,而不能成为技术团队展示架构复杂度的方式。
| 方案 | 主要解决的问题 | 最容易出现的副作用 | 适合阶段 |
|---|---|---|---|
| 单库单表 | 快速交付、事务简单、运维成本低 | 容量、连接数和热点集中 | 早期验证、交易规模较小 |
| 读写分离 | 降低查询对主库的压力 | 复制延迟、读到旧数据 | 读多写少的成熟系统 |
| 分库分表 | 降低单表数据量和单实例压力 | 跨库查询、跨库事务、分片键选择 | 订单与用户数据持续增长 |
| 缓存加数据库 | 吸收高频读、保护主库 | 缓存击穿、脏数据、失效风暴 | 商品、活动、类目等高频读场景 |
| 事件驱动与异步化 | 削峰填谷、拆分非核心写入 | 最终一致、重复消费、补偿复杂 | 订单后置流程和营销业务 |
| 分布式数据库 | 横向扩展和多节点容灾 | 成本、兼容性和运维门槛上升 | 跨地域或超大规模业务 |
表格中的“适合阶段”不是绝对规则。一个大型电商系统的支付流水可能仍然采用单一逻辑数据库,而商品浏览数据则采用缓存和分布式存储。真正成熟的架构通常不是全站统一一种方案,而是按数据域采用不同策略。

只看 QPS,很容易把数据库评估带偏。电商高峰至少要同时看四组指标:请求吞吐量、端到端延迟、业务成功率和数据正确率。商品详情可以允许短时间读到几秒前的数据,但库存扣减、支付状态和退款结果不能以牺牲正确性为代价。
我在做容量评审时,会把“主库 CPU 不超过 60%”这类基础设施指标,和“库存扣减失败可重试”“支付回调重复到达不产生重复入账”这类业务指标放在同一张表里。因为高峰期间,数据库没有宕机并不等于系统可用。
日常电商系统的流量,通常由商品浏览、搜索、推荐、购物车、下单、支付和售后组成。大促时这些流量不会同步增长:活动开始前,商品详情和搜索先冲高;活动开始后,购物车和订单写入快速增加;支付窗口打开后,支付回调和订单状态更新形成第二个写入波峰。
如果把所有流量都简单归纳为“并发用户数”,就无法判断数据库真正承受的压力。例如,十万用户同时打开商品详情,可能只产生大量缓存读取;一万用户同时抢同一件商品,却会集中修改同一个库存记录,形成完全不同的锁竞争。
| 业务环节 | 主要操作 | 数据特征 | 典型风险 |
|---|---|---|---|
| 商品详情 | 查询商品、规格、价格、活动 | 读多写少、热点明显 | 缓存击穿、主库被重复查询 |
| 搜索筛选 | 关键词、类目、属性组合 | 条件复杂、结果集变化大 | 慢查询、深分页、排序开销 |
| 购物车 | 新增、修改数量、删除商品 | 用户维度写入、频繁更新 | 重复提交、库存展示不准确 |
| 秒杀下单 | 校验资格、扣减库存、创建订单 | 热点写入、并发集中 | 锁等待、超卖、重复订单 |
| 支付回调 | 更新支付状态、写入流水 | 重复通知、异步到达 | 重复入账、状态覆盖 |
| 运营报表 | 聚合订单、商品、用户数据 | 扫描量大、查询时间长 | 抢占交易资源 |
订单主表往往从一个简单的交易记录,逐渐变成一个巨大对象:买家信息、收货地址、商品快照、优惠明细、发票信息、物流状态、支付状态、售后状态、营销标签和客服备注都被塞进去。
这种设计在早期很方便,查询一张表就能展示订单详情。但当订单列表、客服查询、仓库拣货、支付回调和经营分析都访问这张表时,字段更新频率和查询模式会互相干扰。尤其是频繁变化的支付状态、物流状态,与很少变化的订单金额、商品快照混在一起,会增加行更新、索引维护和缓存失效成本。
订单主表应该只承载交易主事实,频繁变化的状态、扩展属性和分析字段应根据访问模式拆出去。这不等于盲目拆表,而是让不同生命周期的数据拥有不同的更新节奏。
技术团队常把热点理解为某个商品 ID 被大量访问。实际上,热点可能出现在一个固定活动 ID、一个默认租户、一个日期分区、一个库存行、一个自增主键尾部,甚至是某个被所有请求都要更新的统计字段。
例如,所有下单请求都更新“活动剩余库存”字段,数据库即使拥有很多节点,也可能因为同一行锁竞争而无法扩展。又如按照用户 ID 分库,但大促期间用户访问高度集中在少数活动商品,写入热点仍然集中在商品和库存维度。

读写分离只能转移一部分“可以接受延迟的查询”。如果业务在写入订单后立即从只读节点查询订单状态,就可能读到旧数据;如果所有读请求都要求强一致,最终仍会回到主库,读写分离的收益会明显下降。
更隐蔽的问题是复制延迟。高峰期主库写入增长,日志传输、从库回放和索引维护都可能变慢。此时从库虽然在线,但读到的数据延迟可能从几十毫秒扩大到数秒。技术负责人必须为读请求分类,而不是简单地把数据库连接切到从库。
分库分表解决的是数据规模和并发访问集中度问题,不会自动解决慢查询、缓存失效、锁竞争和业务流程过长。分片后如果查询仍然无法命中分片键,系统可能需要向所有分片广播请求,最终延迟比单库更高。
一个典型例子是订单按用户 ID 分片,但运营人员需要按订单号、手机号、商品 ID 和时间范围检索。若没有全局索引或查询路由,后台查询就会访问多个分片。随着分片数量增加,任何一个慢分片都可能拖长整体响应时间。
分片键不是“让数据平均分布”这么简单,而是要同时满足写入均衡、核心查询可路由和生命周期管理三个条件。只看数据量均匀,不看访问路径,是分片设计最常见的错误。
缓存命中率只能说明某些请求被缓存吸收,不能证明数据一致性、失效策略和回源路径没有问题。大促开始时,运营人员批量修改价格或活动库存,如果缓存没有按版本或事件失效,用户可能看到旧价格;如果大量热点 Key 同时过期,又会出现缓存击穿。
我更关注四个缓存指标:命中率、回源请求峰值、单 Key 访问集中度和失效后的恢复时间。命中率 99% 但回源集中在一个爆款 Key 上,仍然可能压垮数据库。相反,命中率只有 92%,但回源均匀、数据库有足够余量,系统未必不稳定。
强一致不是免费的。它通常意味着更严格的事务范围、更少的异步空间、更高的锁等待和更复杂的跨节点协调。商品浏览次数、推荐热度、营销曝光统计没有必要和库存扣减使用同样的强一致级别。
我会把数据分为三类:必须强一致的数据、允许短延迟的数据、最终一致即可的数据。订单金额、支付流水、库存扣减属于第一类;商品搜索索引和物流轨迹通常属于第二类;浏览次数、推荐计数和经营看板可以属于第三类。
平均响应时间很容易掩盖问题。假设 99% 请求在 100 毫秒内完成,但 1% 请求需要 8 秒,那么在高峰时仍可能有大量用户卡在提交订单页面。更重要的是,数据库连接池、线程池和消息队列往往会因为少量长请求被逐步占满。
压测报告至少要同时展示 P50、P95、P99、错误率、锁等待、连接池使用率、慢查询数量、复制延迟和消息堆积量。没有这些数据,所谓“系统能承受多少并发”通常只是一个缺乏边界的宣传数字。

我通常会先把电商系统拆成商品、价格、库存、购物车、订单、支付、履约、售后、营销和分析十类数据域。每个数据域都要回答四个问题:谁产生数据、谁修改数据、谁最频繁读取、哪些字段必须和其他数据同时提交。
例如,商品描述和商品图片属于低频变更数据;价格属于强业务约束但可能批量变更;库存是高并发竞争数据;订单是交易事实;支付流水是资金事实;经营报表则是分析结果。它们的生命周期、读写比例和容错方式明显不同,不应天然共享同一套表结构。
| 一致性等级 | 典型数据 | 允许的延迟 | 推荐技术手段 |
|---|---|---|---|
| 强一致 | 支付流水、订单金额、库存扣减 | 通常不允许业务可见错误 | 本地事务、幂等键、状态机、唯一约束 |
| 会话一致 | 用户刚提交的订单、购物车变更 | 同一用户短时间内需看到最新状态 | 主库优先、会话粘滞、版本号 |
| 短延迟一致 | 商品索引、活动标签、物流轨迹 | 秒级或分钟级可接受 | 消息同步、增量索引、延迟副本 |
| 最终一致 | 浏览次数、推荐热度、经营看板 | 分钟级甚至更长 | 事件流、批处理、汇总表 |
一致性分级的价值在于,它给缓存、异步和分库提供了业务依据。没有分级时,所有团队都会倾向于“先保证强一致”,最后导致数据库连接长期等待,非核心流程也无法削峰。
高峰系统最怕的不是请求失败,而是请求重试后产生两次结果。下单按钮重复点击、网关超时重试、支付渠道重复通知、消息重复投递,都可能让同一个业务动作到达数据库多次。
我的做法是为不同动作分别定义幂等边界。创建订单可以使用用户请求号或业务订单号;支付回调使用渠道流水号;库存扣减使用订单号加商品明细号;优惠券核销使用用户和券实例的唯一组合。幂等键必须进入数据库约束或可持久化的去重表,不能只存在应用内存中。
CREATE TABLE inventory_deduction ( id BIGINT PRIMARY KEY, order_id VARCHAR(64) NOT NULL, sku_id BIGINT NOT NULL, quantity INT NOT NULL, status VARCHAR(20) NOT NULL, created_at TIMESTAMP NOT NULL, UNIQUE KEY uk_order_sku (order_id, sku_id) ); UPDATE sku_inventory SET available_stock = available_stock - :quantity, version = version + 1 WHERE sku_id = :sku_id AND available_stock >= :quantity AND version = :version;
上面的示例只表达设计思路,不代表所有项目都必须使用同样的乐观锁写法。库存极度热点时,单纯依赖数据库行锁可能不够,还需要前置限流、库存分桶、令牌预扣或按活动维度拆分。但无论采用哪种方式,最终都应有持久化事实用于校验和对账。
订单分片常见候选键包括用户 ID、订单号、商户 ID、下单时间和租户 ID。没有一种键能同时满足所有场景,因此要先列出核心查询,再对候选键评分。
如果一个查询无法从请求参数中直接计算出分片位置,就要设计全局订单索引、路由表或搜索系统。这个组件必须纳入容量评估和容灾设计,不能等分片上线后再补。
交易数据库擅长按主键查订单、更新状态和执行短事务,不擅长在大促当天扫描数亿条明细计算销售额、退款率和商品排行。报表查询即使没有锁住交易表,也会消耗缓存、磁盘带宽和连接资源。
更合理的路径是通过业务事件或增量同步,将订单、支付、退款和库存变化写入分析库或汇总表。实时看板可以使用按分钟聚合的事实表,财务对账则保留可追溯的明细数据。交易库负责记录事实,分析库负责解释事实。

单库单表适合业务早期。它有三个明显优点:事务边界清晰、开发效率高、故障排查路径短。订单、库存和支付在同一数据库内完成本地事务,技术团队可以用唯一约束、行锁和状态机建立相对可靠的交易流程。
它的边界也很明确。当数据量增长后,索引树变大、备份恢复时间变长、热点表缓存命中下降,写入和查询开始互相争抢。更危险的是,系统可能在低峰完全正常,只在大促时出现连接池耗尽和锁等待,导致团队没有足够时间迁移。
如果仍然采用单库,建议至少做以下隔离:
读写分离最适合商品、类目、内容和部分订单查询。它能通过多个只读节点吸收查询压力,但不会降低主库写入压力,也不会解决热点写入。如果主库的瓶颈是锁竞争或日志写入,增加只读节点帮助有限。
关键设计是读路由。可以按照接口类型、用户会话、事务阶段或数据新鲜度进行路由。刚下单后的短时间内读取订单,应优先主库;商品浏览和搜索结果可以读取副本。对关键页面,还可以携带数据版本号,当副本版本落后时自动回源主库。
| 读请求类型 | 是否适合只读节点 | 建议 |
|---|---|---|
| 商品详情 | 适合 | 优先缓存,副本作为回源路径 |
| 类目与品牌筛选 | 适合 | 使用索引或搜索服务,避免深分页 |
| 刚提交订单后的详情 | 谨慎 | 主库优先或使用会话粘滞 |
| 支付结果确认 | 不建议完全依赖 | 以支付事实和订单状态机为准 |
| 经营报表 | 不建议直接读取 | 使用分析库或预聚合结果 |
分库分表适合订单数据持续增长、单实例资源无法满足、且核心查询可以明确路由的系统。它能把单表索引、连接和磁盘压力拆开,也能降低单个实例故障影响范围。
但分片后,跨库事务、全局唯一号、跨片聚合、数据迁移和在线扩容都会变复杂。尤其是订单、库存和支付分别分片后,一次下单可能需要跨多个数据节点。如果没有清晰的最终一致策略,任何网络抖动都可能让订单状态和库存状态不一致。
我建议先做“逻辑分片、物理少分片”。应用层先具备路由能力,但初始物理节点不必一次扩到很大。这样可以通过迁移部分逻辑分片验证路由、重试、双写校验和回滚流程,避免首次上线就把所有数据切到复杂拓扑。
高峰系统通常需要把请求分成同步核心路径和异步后置路径。同步路径只保留资格校验、库存确认、订单主事实和支付必要信息;积分、营销统计、消息通知、推荐更新和报表汇总通过事件异步处理。
这种方案的性能优势来自两个方面:缓存吸收大量重复读取,消息队列吸收短时间写入波峰。它的代价是系统不再由一个数据库事务完成全部业务,必须增加幂等、重试、死信、补偿、事件版本和对账机制。
异步并不意味着所有事情都可以延迟。比如订单创建成功后,用户应立即获得订单号;但积分增加、营销标签更新和经营看板刷新可以延迟。技术负责人需要把“用户必须立即知道的结果”和“系统最终必须完成的动作”区分开。

下面使用一个中型综合电商项目的情景案例说明设计过程。该项目日常订单量约 8 万笔,活动日订单量约 45 万笔,峰值下单请求约为平日的 12 倍。商品浏览量很大,但真正集中写入的是库存、购物车、订单和支付状态。
项目早期采用单库单表,商品详情直接查询数据库,订单列表与运营报表共享连接池。低峰时平均接口延迟约 90 毫秒,活动预演时 P99 达到 2 秒以上;最先出现的不是数据库宕机,而是订单列表变慢、报表查询超时和支付回调积压。
团队最初提出立即分成 32 个库、每库 16 张表。但经过查询审计后发现,真正消耗资源的前三项分别是商品详情重复回源、订单列表未命中复合索引,以及报表扫描订单明细。此时直接分片会增加复杂度,却不能优先解决主要瓶颈。
第一阶段没有改变订单主库拓扑,而是完成四项调整:商品详情和活动规则增加缓存;订单列表改为按用户和时间范围查询;报表改为读取按日汇总表;支付回调使用独立连接池和幂等流水表。
同时,团队把订单详情中的物流、发票和营销字段拆到扩展表,减少订单主表被频繁更新的字段数量。这个动作没有增加数据库节点,却降低了行宽、索引维护和无关字段读取成本。
第二阶段将商品读模型、搜索索引、库存服务和交易订单进行逻辑隔离。商品搜索不再直接依赖订单库,库存扣减保留强一致事务,营销统计通过事件异步汇总。
订单仍然以单一逻辑数据库对外提供服务,但内部开始记录全局订单号、用户路由信息和数据版本。这为未来按用户或商户分片留下迁移条件,也避免在业务高峰前仓促切换。
当订单明细和售后记录持续增长后,团队才对历史订单和高增长明细进行分片。当前订单、支付流水和售后状态并没有全部采用同一分片策略,而是根据访问路径分别处理:用户中心偏向用户路由,商家后台偏向商户路由,财务对账则依靠按日归档和分析库。
这套方案的关键不是“用了多少分片”,而是让不同查询不再争抢同一张热点表。对于跨路由查询,系统使用订单路由表或专用搜索索引,避免让应用层遍历所有分片。

在这个案例中,团队把高峰保障指标增加为“故障后的恢复时间”。缓存集群部分节点异常时,商品详情是否会全部回源;只读副本延迟升高时,哪些请求自动切回主库;消息队列积压时,订单主流程是否仍可完成;分片迁移中断后,路由表是否可以回滚。
从运维角度看,恢复速度比单次压测中的最高吞吐更能反映架构质量。一个系统即使峰值吞吐很高,但发生缓存击穿后需要人工重启、清理队列和修复数据,实际业务风险仍然很大。
索引能够提高查询速度,但每一次写入都要维护相关索引。订单表如果同时建立用户、手机号、商户、商品、活动、支付状态、物流状态和时间等多个索引,写入成本会持续增加,批量状态更新也更容易产生锁竞争。
索引设计应围绕真实查询建立。复合索引的字段顺序要考虑等值条件、范围条件和排序要求;不要因为某字段区分度高,就把它放在所有索引最前面。更不能只看执行计划中的“使用了索引”,还要观察扫描行数、回表次数、临时表和排序操作。
使用大页码查询订单列表时,数据库往往需要先扫描并跳过大量记录,再返回当前页。随着页码增加,延迟会持续上升。后台人员翻到很后面的页面时,可能触发长查询,和高峰交易请求争抢资源。
更适合大数据量场景的是基于游标或最后一条记录的分页。例如按照创建时间和订单号组成稳定排序,每次携带上一页最后一条记录作为下一页起点。这样查询可以利用索引继续向后扫描,而不是从头跳过大量数据。
“下单前实时计算用户过去一年消费金额”“详情页实时统计全站销量”“购物车实时汇总所有促销规则”这类需求,很容易把复杂聚合放进同步链路。它们在数据规模较小时没有问题,规模增长后却会成为高峰延迟的主要来源。
可以采用预计算、分层缓存和异步刷新。对于必须实时计算的部分,应限制数据范围、设置超时和降级结果。宁可在非核心展示区域显示“数据更新中”,也不要让一个统计接口拖慢库存和订单提交。
价格、库存和活动规则变更时,版本号比单纯依赖时间戳更容易判断新旧数据。读请求携带版本或缓存对象包含版本信息,写入成功后发布失效事件。消费者发现事件版本低于当前版本时,可以丢弃旧消息,避免乱序到达导致缓存回退。
版本号不能替代事务和幂等,但它能帮助系统识别“这条更新是否仍然有效”。在消息重试和多节点并发更新场景中,这个细节往往比简单删除缓存更可靠。
压测不能只用平均流量乘以一个倍数。真实大促具有明显的热点分布:少量爆款承载大部分访问,少量用户可能产生大量重试,支付回调具有延迟波动,后台任务会在整点集中执行。
建议至少模拟以下条件:
一个商品详情请求和一次库存扣减请求,对数据库的压力完全不同。容量评估应分别统计每秒商品读取、购物车更新、库存尝试、订单创建、支付状态更新和消息消费,而不是只给出一个笼统的接口 QPS。
我会为每种业务动作设置独立的容量预算。例如库存扣减不仅看每秒请求数,还要看同一 SKU 的并发集中度、锁等待时间、失败重试比例和库存校验耗时。只有这样,才能判断系统是被总流量压垮,还是被单个热点对象压垮。
如果请求已经进入订单服务、查询用户信息、加载促销规则,最后才被判断为无库存,那么数据库已经承担了大量无效工作。活动资格、登录状态、令牌和库存预占可以在更前面完成,尽早过滤没有资格的请求。
限流还要区分用户、IP、商品、活动和接口维度。只按 IP 限流可能误伤共享网络用户,只按接口限流又无法防止单个爆款拖垮库存服务。限流规则需要支持动态调整,并且要有明确的超限返回和用户提示。
降级策略应该在代码、配置和演练中真实存在。商品详情可以关闭实时销量,推荐可以返回缓存结果,营销标签可以延迟刷新,后台报表可以暂停;但支付确认、库存扣减和订单主事实不能随意关闭。
| 故障或压力状态 | 可以降级的部分 | 不应降级的部分 | 验证方式 |
|---|---|---|---|
| 缓存命中率下降 | 推荐、销量、非核心标签 | 价格校验、库存事实 | 模拟批量失效并观察回源峰值 |
| 只读副本延迟 | 搜索、内容、部分列表 | 支付确认、订单关键状态 | 注入复制延迟并验证路由 |
| 消息消费积压 | 积分、营销统计、通知 | 订单主事实和资金流水 | 暂停消费者并验证补偿机制 |
| 交易库资源紧张 | 报表、导出、历史查询 | 下单、支付、库存 | 限制后台连接池并观察核心成功率 |

如果订单量还不大、团队规模有限,建议采用结构清晰的关系型数据库,保持订单、支付和库存的事务边界简单。不要为了预想中的千万级数据,提前引入大量分片、消息和跨地域组件。
但“暂时不分片”不等于不做准备。早期就应保留全局业务号、明确数据域、控制表宽、禁止报表直查交易表,并为订单归档和后续路由留下字段。这样未来扩展时可以渐进迁移,而不是重写全部业务。
当商品访问量和订单量明显增长时,优先检查读写比例、热点 Key、慢查询、索引数量和报表资源占用。通常这阶段最有价值的动作不是立刻分片,而是缓存高频读、读写分离可延迟查询、拆出分析查询并隔离连接池。
如果主库写入仍然是瓶颈,应进一步检查是否存在无意义字段更新、过宽行、过多索引和同步后置流程。很多写入压力其实来自订单创建后同步更新积分、优惠统计、推荐标签和通知记录,这些都可以通过事件异步化降低核心事务长度。
如果业务平时流量不大,但每年有少数几次极高峰,架构重点应放在峰值保护而不是全天过度扩容。提前预热缓存、限制活动入口、拆分爆款库存、隔离支付连接池和暂停非核心任务,往往比永久增加数据库节点更经济。
此类项目要特别关注高峰前后的数据一致性。活动开始前的价格和库存预热、活动结束后的订单补偿、超卖校验和支付对账,都需要明确的批处理与人工介入流程。性能稳定但对账出错,仍然不是成功的大促。
多商户系统不能只按全平台订单量来设计。大型商户可能产生极高的局部热点,小商户则需要低成本共享资源。可以考虑租户分层、逻辑分片和大租户独立路由,但必须处理跨租户运营分析、平台级订单查询和租户迁移。
租户隔离还涉及权限和数据安全。数据库分片只能解决部分性能问题,不能替代应用层鉴权、字段脱敏和审计。技术负责人应把租户路由、备份恢复和数据删除流程一起设计。
跨地域部署并不自动等于高可用。若订单、库存和支付需要跨地域强一致,延迟和故障处理会变得复杂;若采用地域内写入和异步同步,则必须接受部分数据的最终一致。
在没有明确业务需求前,不建议为了“架构先进”而做全量多活。可以先实现只读内容就近访问、静态资源多地域分发、分析数据异步汇聚,再逐步评估交易数据的地域策略。
| 选择 | 得到什么 | 牺牲什么 | 我会在什么条件下选择 |
|---|---|---|---|
| 单库优先 | 事务简单、开发快、排障容易 | 横向扩展受限 | 业务规模可预测,团队需要快速验证 |
| 读写分离 | 查询容量提升 | 接受复制延迟和读路由 | 查询远多于写入,数据可短暂延迟 |
| 分库分表 | 容量和写入横向扩展 | 路由、迁移和跨库事务复杂 | 单实例已成为明确瓶颈,查询可按键路由 |
| 缓存优先 | 显著降低重复读取 | 一致性、失效和回源治理 | 热点读明显,数据允许短延迟 |
| 异步优先 | 缩短同步链路、削峰填谷 | 最终一致、补偿和监控成本 | 后置流程多,业务可拆分 |
| 分布式数据库 | 多节点扩展和容灾能力 | 成本、兼容性和运维复杂度 | 规模、地域或可用性目标已超过传统方案边界 |

监控不能只展示数据库 CPU 和内存。建议建立从用户请求到数据结果的完整链路,至少包括接口业务成功率、数据库 P99、锁等待、慢查询、连接池占用、缓存命中率、回源峰值、消息堆积、订单状态停留时间和对账差异数量。
其中,“订单状态停留时间”是非常有价值但常被忽视的指标。订单接口返回成功,不代表后续支付、履约和库存流程都正常。某个状态持续积压,往往比单纯的错误率更早暴露异步链路故障。
电商系统开发的数据库方案,真正的难点不是列出多少种技术名词,而是判断哪些数据必须同时成功、哪些数据可以延迟、哪些查询必须路由、哪些流量应该在入口被拒绝。
如果这些问题没有答案,直接分库分表只会把问题分散到更多节点;直接引入缓存只会增加数据失效风险;直接使用异步也可能让用户看到订单成功却迟迟没有库存结果。
在业务早期,用简单事务把交易做对;在增长阶段,用缓存、读写分离和分析隔离吸收主要压力;在数据规模和热点真正超过单实例边界后,再按数据域和查询路径分片;当异步链路增加后,用幂等、补偿和对账保证最终正确。
数据库架构不是一次性设计,而是一套随着业务证据逐步升级的约束系统。每一次复杂化都应回答:它消除了哪个已被观测到的瓶颈?增加了什么新的故障模式?团队有没有能力监控、演练和回滚?
最后,我的判断可以浓缩成一句话:高峰性能不是数据库单点能力的竞赛,而是让不同数据按照不同速度、不同一致性和不同故障边界运行。当技术负责人能够解释每个数据域为什么这样存、每个请求为什么这样路由、每个异常为什么这样恢复时,数据库方案才真正具备保障高峰性能的能力。
我负责过一次大促前的电商系统改造,团队当时一看到订单量上涨就想直接分库分表,但我更担心的是改造复杂度和数据一致性。单库优化、读写分离、分区表和分库分表到底应该按什么顺序做,我希望能得到一套可执行的判断方法。
我的判断是:绝大多数电商系统不应该把分库分表作为第一步。高峰性能问题通常先暴露在慢查询、索引回表、锁等待、连接池耗尽和促销接口调用链过长,而不是数据库物理容量已经不够。我曾经处理过一个日常订单量并不算大的系统,峰值每秒写入约700笔订单。
团队最初计划按用户ID拆成16个库,但压测发现真正的瓶颈是订单列表查询没有覆盖索引,单次查询需要扫描大量历史记录,数据库CPU长期超过85%。补齐联合索引、限制分页深度、把统计查询移出主链路后,单库峰值提升到约1800笔写入每秒,p99响应时间从1.8秒降到260毫秒。
因此,建议按照“查询路径治理,容量扩展,架构拆分”的顺序推进。先确认慢查询占比、锁等待时间、连接池使用率和磁盘IO,再判断是否真的需要拆分。
方案主要收益典型代价更适合的阶段 单库单表优化改造小,事务和查询最简单受单机CPU、IO和存储容量限制中小规模及早期增长阶段 分区表降低历史数据扫描范围,保留相对简单的SQL分区键设计错误会造成查询失效订单、日志等按时间增长的数据 读写分离缓解读请求压力存在复制延迟,不能解决写热点读多写少的商品和内容场景 分库分表突破单机容量与并发上限跨库事务、分页、聚合和运维明显变复杂数据规模和写入压力都已超过单库边界 我通常把分库分表的启动条件设为三个同时满足:核心表持续接近存储或IO上限;
经过索引和SQL治理后,单库仍无法满足峰值p99目标;业务能够接受按明确分片键访问,且跨分片查询已经有独立方案。如果只是因为未来可能变大就提前拆分,往往会先得到一套难以排障的系统。还有一个容易被忽略的指标是“最热分片”。平均流量被16个库平均分摊,并不代表系统安全。
如果某个大客户、热门商品或秒杀活动集中落在同一个分片,单个分片可能先被打满。因此,分片方案必须同时做均衡性模拟,至少用历史订单和活动流量回放检查分片最大值、平均值与最大偏差。
我测试过一套主库加多个只读库的电商架构,订单创建成功后,用户立刻刷新页面,却偶尔看到订单不存在或仍显示待支付。团队一开始认为是缓存问题,后来才发现数据库复制延迟也会制造这种体验,想知道读写分离应该如何设计才不会影响核心流程。
读写分离不是简单地把所有查询随机发到只读库。它解决的是读压力扩展问题,却天然引入了复制延迟;在支付、库存、订单状态这类刚刚发生写入的场景中,延迟几十到几百毫秒都可能被用户感知。我在一次压测中观察到,平时只读库延迟约20毫秒,大促流量上来后,复制延迟在高峰窗口扩大到1.6秒。
订单写入主库后,前端马上请求订单详情,如果路由到只读库,就会出现“提交成功但页面查不到”的假象。这个问题不是数据库查询慢,而是读取了尚未追平的数据。更稳妥的做法是按业务一致性等级路由,而不是按接口名称机械分流。
业务读取建议读取位置原因 创建订单后的确认页主库,或携带会话粘滞标记用户刚完成写入,必须优先看到最新状态 支付回调后的订单状态主库或经过位点确认的副本避免支付成功却显示未支付 商品详情和评价列表只读库或缓存通常允许短时间延迟 运营报表和趋势分析分析库或离线数仓不能让复杂聚合拖慢交易库 我更推荐使用“写后读一致性令牌”。
写入主库成功后,服务端返回一个包含数据库日志位点或时间戳的令牌;后续短时间内的关键查询,只有在副本追平该位点后才允许路由到副本,否则暂时读取主库。这样比简单设置几秒钟的主库粘滞更精确,也不会让所有用户长期压在主库上。
如果系统暂时没有实现位点路由,至少要为下单、支付、退款和库存扣减接口设置主库读取规则,并在副本延迟超过阈值时自动摘除异常只读节点。我实际使用过的告警阈值是:普通内容查询延迟超过500毫秒告警,交易相关副本延迟超过100毫秒就停止承接强一致读取。最后不要只验证平均响应时间。
读写分离是否可用,应重点压测写入后的1秒内连续查询、支付回调与用户刷新同时发生、只读节点延迟突然升高这三种场景。很多架构在常规压测中表现很好,正是在这三个瞬间出现业务错误。
我在设计电商下单链路时,曾经把订单、库存、优惠券和支付预单全部放进一个大事务,结果高峰期锁等待迅速增加,数据库连接池也被占满。后来拆开事务后吞吐量提高了,但又担心出现扣库存成功、订单创建失败的问题,这类数据到底该如何划分一致性边界?
不建议把订单、库存、优惠券和支付全部塞进一个长事务。技术上看似一致,实际上会把多个高竞争资源绑定在同一个锁生命周期里;高峰期只要支付接口、风控接口或优惠计算变慢,数据库事务就会持锁等待,最终形成连接池堆积。
我曾做过两组对比压测:第一组把检查库存、计算优惠、创建订单和写支付预单放在一个事务中,峰值约900次下单请求每秒时,锁等待p95达到420毫秒,数据库连接池使用率超过90%。
第二组把外部调用移出事务,只在数据库内完成库存条件扣减和订单落库,达到相同流量时锁等待p95降到70毫秒,失败重试也更容易控制。关键不是追求所有表的强一致,而是定义每个动作的业务不变量。例如库存不能扣成负数、订单不能无库存成立、支付成功最终必须驱动订单变为已支付。
这些不变量可以用短事务、状态机和可靠消息共同保证。
数据动作推荐一致性方式需要防范的问题 库存扣减数据库条件更新,例如可用库存大于购买数量时才扣减超卖、重复扣减 订单创建与库存预占在同一核心短事务内完成订单成功但库存未锁定 支付预单订单落库后异步或独立调用支付服务外部接口超时导致事务长时间持锁 优惠券核销独立状态机加幂等号,必要时通过消息最终一致重复核销和回滚困难 一个实用的下单流程是:先在短事务中用订单号作为幂等键锁定库存、写入订单草稿和库存流水;
事务提交后,再调用优惠和支付等外部服务;后续通过消息或任务补偿推进订单状态。如果消息发送失败,使用本地消息表或事务消息保证事件不会悄悄丢失。数据库设计上,库存扣减不要先查询再更新。更安全的写法是直接执行带条件的更新,并检查影响行数。影响行数为0时,统一返回库存不足或重复请求,而不是继续创建订单。
订单、库存流水和请求幂等表也应分别建立唯一约束,让数据库成为最后一道防线。我对“是否放进同一事务”的判断标准是:两个动作是否必须在同一瞬间完成,且是否属于同一个数据库资源边界。如果涉及外部服务、长时间计算或低频补偿,就不应为了形式上的强一致把它们绑进核心事务。
我遇到过一个很典型的问题:订单表已经按月份分区,整体查询速度不错,但某个爆款商品上线时,库存表的一行记录被数千个请求同时更新,数据库CPU和锁等待一起上升。为什么已经做了分区仍然会被热点拖垮,应该怎样改造表结构和写入方式?
分区解决的是数据范围管理,不等于自动解决并发竞争。一个表即使分成几十个分区,只要所有请求都更新同一个商品库存行,热点仍然集中在同一个记录、索引页甚至同一个数据库节点上。我在一次活动压测中看到,订单表按月分区后,历史查询扫描量下降了约80%,但库存扣减接口的p99仍从110毫秒升到1.2秒。
定位后发现,约65%的请求集中更新前20个热门SKU,其中一个SKU占全部库存写请求的18%。这说明“表分区有效”和“写入均衡”是两个完全不同的问题。首先,订单表应围绕主要访问路径设计,而不是只按数据量分区。常见的订单列表查询通常带有用户ID和创建时间,因此需要保证用户维度索引有效;
后台按时间查订单,则需要单独考虑时间范围索引或分析库,不能期待一个索引同时满足所有场景。
热点来源表现改进方式 单个SKU库存行更新锁集中,等待时间上升库存拆分为多个桶或多个库存分片,扣减时选择可用桶 自增主键写入末端页高并发插入集中在索引尾部优化批量写入、检查索引数量,必要时采用合理的分片键 热门商品详情缓存失效大量请求回源数据库设置随机过期、请求合并和单飞机制 按时间分区但查询不带分区键发生跨分区扫描改写查询条件,或建立面向访问路径的汇总表 库存分桶是我更常用的高峰方案。
比如一个SKU有10000件库存,可以拆成20个库存桶,每桶记录可售数量,应用层通过固定规则或随机策略选择桶扣减。这样单个记录的竞争会被分散,但要注意不能只靠随机选择,否则某些桶可能提前耗尽。扣减失败后必须重试其他桶,并用库存流水记录实际扣减来源。分桶并不是所有商品都适合。
低库存商品、需要严格顺序扣减的商品和强依赖单仓库库存的场景,可能更适合串行化队列或单线程库存服务。我的经验是,先根据压测统计出热点SKU占比,再决定是否只对头部商品启用分桶,而不是把所有库存模型复杂化。另外,订单写入不要在主交易表里同步维护过多统计字段。
销售量、排行榜和商品热度可以通过增量事件更新汇总表;如果每创建一笔订单都同步更新商品主表的多个计数器,就会把原本分散的订单写入重新汇聚成一个热点行。判断改造是否有效时,除了看数据库总CPU,还要观察行锁等待、单分区QPS、热点SKU的请求分布、写入失败重试次数和缓存回源率。
平均指标掩盖热点,是电商高峰排障中最常见的误区之一。


读者评论
文章把读多写少、库存扣减和支付回调分开讨论,这一点比较贴近实际。尤其是只看QPS而忽略P99、业务成功率和数据正确率,确实容易误判系统高峰表现。
分库分表并不是性能问题的万能解法,分片键与查询路径的分析很关键。文中提到运营查询可能需要跨分片检索,说明方案落地时还要同步考虑后台和对账场景。
对读写分离和缓存一致性风险的说明比较客观。电商系统可以接受商品信息短暂延迟,但库存、支付状态不能简单依赖副本或缓存,这种数据分级思路有参考价值。