分布式系统核心技术完整梳理含分库分表、分布式事务、熔断补偿做了这么多年后端我越来越觉得分布式系统这东西不是看几篇博客就能hold住的它是由一个个具体问题堆起来的数据量大了库扛不住怎么办一个请求跨好几个服务数据怎么保持一致下游挂了怎么不让整条链路跟着一起殉葬。前阵子刚好有个朋友在架构选型时跟我聊到这块我把这几年在订单、库存、支付这类核心链路上沉淀的东西完整梳理了一遍。这篇内容会比较长围绕三个核心主题展开分库分表、分布式事务、熔断补偿。如果你正在做系统拆分、数据量增长遇到瓶颈或者准备从单体迈向微服务这篇文章应该能给你一条比较清晰的落地路径。先说说为什么这三个问题要放在一起聊。很多团队一开始是单库单表业务跑得挺顺突然某天数据库CPU飙到90%慢查询一堆你一看订单表几千万行还在以每天几十万的速度增长。这时候你第一反应是加缓存、加索引、读写分离但治标不治本因为核心矛盾是单机数据量和写入压力已经超出物理极限了。于是你开始分库分表。数据拆开了新的问题又来了一次下单操作要同时扣库存、创建订单、记录流水以前在一个事务里就能搞定现在数据分散在多个库多张表事务怎么保证再往后服务越来越多某个服务依赖的下游接口突然响应超时线程池被打满请求雪崩这时候你才意识到光把数据拆好还不够整个链路的稳定性同样要命。分库分表解决的是数据容量和写入性能问题分布式事务解决的是跨服务数据一致性问题熔断补偿解决的是局部故障被无限放大的问题。这三件事本质上就是分布式系统从单机走向集群之后必须跨过的三道坎。1. 先搞清楚分库分表到底在解决什么问题很多新人容易把分库分表当成一种“炫技”觉得表一拆、库一分架构就高级了。但实际上分库分表是一个非常被动的动作它是被数据量和写入压力逼出来的。我在决定拆分之前一般会用一组数据来判断到底要不要分、怎么分。比如订单表假设单行记录1KB左右一天新增50万单一年就是1.8亿行光数据就近180GB再算上索引单表随便就到300GB以上。InnoDB的B树在数据量达到这个级别之后索引深度增加、缓冲池命中率下降写入时刷盘压力也大。更麻烦的是单库的写入TPS也有上限一般MySQL单库在常规配置下稳定支撑的写TPS也就几千到一两万遇到大促峰值翻倍直接就扛不住。如果你的表数据量在千万级以下、单库写TPS还能扛住峰值其实没必要拆。一旦数据量过了亿、日增达到几十万上百万或者峰值写TPS明显超过单库能力那才到了必须拆的时候。拆的方式也有讲究。一种是大方向上先分库就是把订单库、用户库、商品库分开这是业务维度的垂直拆分属于微服务化之后几乎必做的事情。但垂直拆分解决不了单表数据量过大的问题比如订单库还是在那只是从一个大库变成了一个小一点的库订单表照样上亿行。这时候就要做水平拆分也就是分表。按哪个键分、分多少片、用什么路由规则这些都是先要定下来的事不能拍脑袋。还有一个常见的误区分库分表之后不是一劳永逸而是把你从存储瓶颈暂时解放出来同时引入了一大堆分布式场景下的新问题跨库join基本别想了、分布式事务要额外处理、全局主键不能再用数据库自增、分页查询变得很麻烦、数据迁移扩容更是噩梦。所以“能不拆就不拆要拆就要一步设计到位”是我这些年最深刻的体会。1.1 分片键选错后面全是坑分片键是整个分库分表设计里最不能做错的选择没有之一。它决定了你的数据能不能被均匀打散也决定了你后续的业务查询能不能走到正确的分片上。最常见的分片键是用户ID和订单ID。如果是C端业务用户维度查询多查某个用户的所有订单按用户ID分片就很自然。但按用户ID分片之后如果你想按订单号查单就麻烦了因为订单号和用户ID之间没有直接的映射关系你必须引入“订单号-用户ID”的反查索引或者干脆把用户ID冗余到订单号里比如订单号里带一段用户ID的低位。如果是后台运营需要按商户、按时间维度查单按用户ID分片也支持不好这就是为什么很多系统会再做一套从库专门按时间维度归档。另一个问题是数据倾斜。我见过一个案例按用户ID哈希分64片结果某几个大用户一天产生几十万条订单这几个用户所在的分片就成了热分片其他分片闲得要死。哈希取模的均匀性只能保证在数据量足够大时整体近似均匀但极端的头部用户永远能打破这个假设。后来他们的优化方案是把大用户单独标记走独立的高性能存储这已经是架构层面的事情了。路由规则方面主流方案就是哈希取模、范围分片、一致性哈希。哈希取模实现简单但扩容时要重新分布数据处理不好就是灾难范围分片按时间、按ID段对扩容友好新增分片不用迁移老数据但容易造成热点比如按时间分片永远是最近的分片在抗压一致性哈希在集群节点变化时迁移量最小但实现复杂度高、查找效率略低。中小团队我比较推荐先按时间范围或者按月份分表因为运维简单、扩容方便业务上也通常有自然的时间维度如果并发和容量真的到了靠时间分片解决不了的程度再上哈希而且分配时要留够未来三年的余量。1.2 全局主键和ID生成别再用数据库自增了分表之后每个表的自增ID一定会在合并时重复这是常识。替代方案我实际用下来比较推荐三种雪花算法、号段模式、UUID仅限非核心链路。核心链路基本就是雪花ID和号段模式二选一。雪花IDSnowflake由一个64位的整数组成时间戳机器ID序列号单机每毫秒能生成4096个ID全局有序、趋势递增。用它的好处是生成速度快、不依赖数据库、ID里带上时间信息方便排查问题。但要注意时钟回拨会导致ID重复这是个经典坑。我当时的做法是给ID生成器加了一个“时钟回拨检查”如果发现当前时间比上次生成时小就拒绝服务或者等待时间追平能有效规避重复ID。号段模式是另一种思路数据库里维护一张发号表每次取一段号比如10000个应用层在内存里分配用完了再去取下一段。它的好处是发号逻辑完全可控、ID连续、方便业务理解缺点是多了一次数据库交互且号段取多了没发出去会浪费。号段模式特别适合订单号这种需要“可读、可追踪”的场景因为订单号往往要用于客服查询、财务对账。我做过的一个订单系统实际用的方案是订单号时间戳秒级用户ID后几位随机序列单独建了一张全局ID映射表做反查。加一层映射自然会有写入成本但换来了极低的方案复杂度。很多团队一开始图省事直接用UUID做主键结果B树因为随机写导致页分裂严重、性能骤降这个坑我真见了不少。记住数据库主键尽量用有序的、单调递增的ID这是InnoDB的物理组织方式决定的最优解。1.3 扩容迁移双写、回放、灰度一步都不能少分库分表里最痛的环节就是扩容尤其是你已经跑了很久、数据量巨大之后再扩。最理想的方案就是在第一次拆分时就预留足够的片数比如32库×32表先用不满后面不够了再垂直拆分。但现实往往是刚开始只分几个库跑着跑着又不够了这时候只能做水平扩容。水平扩容最稳妥的路径是双写 binlog回放 灰度切换。具体操作是在旧库旧表正常服务的同时新库新表开始接收双写流量也就是代码里同时往新旧两套写入然后开启旧库的binlog监听把增量数据同步到新库接着用数据迁移工具把存量数据全量导过去校验一致后再切读流量最后才是切写流量。整个过程有一个关键点一定要先切读再切写并且观察一段时间因为读流量切错了还能快速回滚写流量切错了是会污染数据的。我踩过的一个大坑是全量迁移过程中旧库的数据还在持续变化如果没有一个可靠的回放机制就会出现“全量搬过去的数据 增量更新丢失”的情况最终新库数据和旧库不一致。后来我们给迁移工具加了一个基于数据版本号的校验环节每条记录带一个更新时间戳在回放阶段只处理时间戳比全量快照新的变更才算把一致性兜住。这块没有捷径只能靠脚本校验灰度一步步啃。2. 数据拆完了分布式事务怎么办数据拆完之后你第一个撞上的就是事务问题。以前一个下单操作订单表、库存表、账户流水表在一个库里一条SQL的事一个事务就提交了。拆分之后订单在订单库、库存在库存库、账户在账户库一个事务管不了了这就是典型的分布式事务问题。分布式事务的核心难点在于无法用单库的ACID保证跨库操作的原子性。这里要区分一下CAP和BASE的适用场景。像转账、下单这种核心交易理论上希望强一致但互联网的实践告诉我们真正的强一致比如2PC两阶段提交在高并发场景下基本不可用因为它要求所有参与者都锁定资源、同步等待性能损失太大而且协调者一挂整个事务卡死。所以在绝大多数业务系统里我们追求的不是强一致而是最终一致性。我之前处理过一个订单与库存的分布式事务问题下完单之后要锁定库存如果订单超时未支付要释放库存如果支付成功要扣减库存。这个场景如果硬上2PC库存资源被锁定的时间会很长并发一高就会出现大量锁等待甚至死锁。后来改成了“本地消息表 定时任务重试”的最终一致性方案下单和写库存消息在同一个本地事务里然后有个独立任务去推送消息处理库存处理成功的标记状态没处理成功的反复重试。这个方案虽然有一段时间窗口的库存不准确但最终是一致的而且性能很好这就是BASE思想的体现。2.1 最终一致性的三种主流程套路分布式事务业界有几种主流的实现套路我按实际使用频率排个序事务消息、本地消息表、最大努力通知。TCC和Seata AT模式放到下一节单独说。事务消息RocketMQ事务消息是我个人用得最多的方案。它的原理是先发一个半消息此时消费者不可见然后执行本地事务本地事务成功就commit消息变为可见失败就rollback消息直接丢弃。RocketMQ还提供了事务回查机制如果长时间没收到commit/rollbackBroker会主动回查本地事务状态避免“本地事务成功了但消息没发出去”这种问题。这套方案适合那种“核心链路写完本地数据之后要可靠地触发下游动作”的场景比如下单后要发消息通知积分服务加积分。它的核心优势是解耦了本地事务和消息发送不用手动建消息表、不用自己写重试任务但它要求你引入RocketMQ并理解半消息机制。本地消息表是经典的“老派”方案原理是在业务库里建一张消息表业务操作和消息写入在同一个本地事务里提交然后由一个异步任务扫描消息表把消息投递到MQ或者直接调下游接口下游处理完了回调更新消息状态。它和事务消息本质上思想相同区别是事务消息把这个流程从业务库里挪到了MQ内部责任更清晰。本地消息表的劣势是消息表和业务表在同一个库里随着数据量增长又是一张超大表需要定期清理优势是只依赖一个普通数据库很多老系统没有MQ用这个方案也能跑。最大努力通知适用于弱一致性场景比如支付回调通知商户系统、退款结果通知用户。核心思想就是“能通知上就通知通知不上就算了靠对账来兜底”。应用层只保证一定次数的重试比如5次、10次不保证最终一定送达但提供一个查询接口让对方主动来查。这个方案成本最低但只适用于可以接受“短暂不一致靠业务侧补偿”的场景。三个方案怎么选我的经验是核心交易链路钱、库存用事务消息或本地消息表这类可靠投递方案非核心、可容忍丢失的用最大努力通知同时所有重要场景都要设计一个“对账任务”作为最终兜底别怕多写代码对账机制往往是救命的最后一根稻草。2.2 TCC与Seata AT模式适合什么场景聊到分布式事务绕不开TCCTry-Confirm-Cancel和Seata AT模式。这两者都属于“补偿型事务”执行过程中先做预留操作最终提交时再做确认失败时做补偿回滚。TCC的理解方式很简单Try阶段做资源检查和预留比如扣库存时先不真正扣减而是在库存表旁边维护一个“预占库存”字段Confirm阶段执行真正的业务操作把预占变成实扣Cancel阶段释放预留资源。每个参与分支的业务代码都要自己实现这三个方法对业务侵入性很强代码量也大但好处是性能好、控制力强。TCC适合那种“并发高、必须实时、可以接受实现复杂”的场景比如下单锁定库存、账户预扣款。Seata AT模式是另一个思路它对业务侵入性很小你正常写CRUDSeata通过代理数据源拦截SQL在底层自动记录数据变更的前后镜像事务提交时利用这些镜像做自动补偿。AT模式看起来“无侵入”但底层是用全局锁和undo log来实现的会在数据库层面引入额外的锁竞争并发高了性能下降明显。而且AT模式对SQL有一些限制比如某些复杂SQL不支持、跨库join处理不了。它的适用场景是业务逻辑简单、对性能要求不极端、想把分布式事务的成本控制在较低水平的团队。我给的选型建议是这样的如果你的团队有足够的中间件能力并且核心链路并发很高优先TCC但只把TCC用在最关键的几个业务动作上不要把整个业务全塞进TCC如果团队规模不大、业务场景常规可以用Seata AT模式先跑起来同时留意锁竞争问题。不要一上来就全链路TCC我见过一个团队把十来个服务全都改造TCC结果确认和取消逻辑反复调整维护成本爆炸。2.3 一个完整的订单与库存分布式事务案例我把我自己做过的下单与库存扣减案例完整拆一遍这应该也是很多电商后端都绕不开的经典场景。整个流程可以理解为用户提交订单 - 尝试验证库存并锁定冻结库存 - 支付成功后扣减实际库存 - 超时未支付则释放冻结库存。初始化状态这里我用一张库存表来体现库存表里至少要有总库存、锁定库存、已售库存三个字段每一笔预占操作就是在锁定库存上增加一个数值而不是直接减总库存。这样做的好处是在下单和支付之间库存并没有被真正扣走只是被“冻结”了如果用户取消订单或者超时只要把锁定库存释放回去就可以了不会出现“库存虚减但钱没收到”的问题。整个链路的实现细节如下第一步创建订单时用一个Transactional包裹的本地事务在订单库写入订单记录订单状态为“待支付”同时在库存库调用预占库存接口把库存记录锁定字段1。这里我用的是“本地消息表”的思路订单表和一份“库存预占消息表”放在同一个库里同一个本地事务里写订单和消息消息状态是“待处理”。如下图所示这个过程里订单库和库存库的修改虽然不在同一个数据库事务里但通过预占消息表的状态流转最终保持了数据的一致性。第二步异步任务扫描“库存预占消息表”把待处理的消息拿出来调用库存服务执行预占操作。预占成功后把消息状态改成“已处理”如果库存不足则把订单状态改为“已取消”并把失败原因记录到日志里。这一步实际上是把“预占库存”从下单流程里解耦了出来不再需要用户在请求链路里同步等待库存结果。第三步用户支付成功后支付回调会触发订单状态变更同时发一个“扣减库存”的事件。这个事件被库存服务消费后执行的是真正的库存扣减总库存减少、锁定库存减少、已售库存增加。再一次强调这里的扣减是在支付完成之后才做的而不是下单时做的这就能避免大量无效订单长时间占用库存导致超卖假象。第四步用户超时未支付时有个定时任务扫描订单表把超时订单置为“已关闭”同时释放锁定库存。这就是“补偿”的雏形你不需要在下单时想尽办法保证库存永久锁定只需要在超时时把它释放掉就可以了。这个方案最大的好处是下单接口不需要同步等待库存事务提交并发能力显著提升因为订单服务和库存服务都只在自己本地事务内操作不会出现跨库死锁而且整个过程是事件驱动的任何一环挂了可以重试最后看消息表状态就能确定哪里有问题。它的代价是下单后到“预占库存成功”之间会有几十毫秒的短暂状态不一致你可以接受用户在这个窗口里看到“订单已创建但未锁定库存”这种中间状态通常我们在前端会引导用户“正在锁定库存请稍候”。3. 链路稳定性熔断、降级、补偿怎么闭环数据一致性问题理清之后还有一个同样要命的主题链路的稳定性。分布式系统有一个特性叫“故障放大”一个下游服务慢了几十毫秒上游线程池就可能被占满然后整个系统雪崩。如果你做过线上告警肯定见过这样的链路图A服务调用BB调用CC调用DD挂了B的线程池被打满A的请求开始排队最终表现为“全员超时”。解决这个问题的三板斧就是超时、重试、熔断。再加一个降级和补偿就构成了完整的韧性体系。我经常跟团队里的同学说不要小看“超时重试”这四个字它是最容易被忽略也是最容易出事的地方。比如下游接口正常响应是200ms你给调用方设置的超时时间是5秒非常宽松看起来没问题但当下游积压开始变慢到2秒时上游线程池已经积压了一大批请求最终还是会打满。超时时间一定要贴近真实P99耗时并且留出20%~30%的余量不要随便放大。3.1 超时、重试与幂等的“三角关系”重试是分布式系统里最常用的容错手段但重试不是无脑重试它有一个必须满足的前提你的接口必须幂等。什么叫幂等同一个请求执行一次和执行一百次结果是一样的。如果不做幂等重试就会变成灾难——比如支付接口重试了三次用户被扣了三笔钱。我做过一个统一的幂等方案所有写接口要求调用方传一个幂等键比如订单号操作类型服务端收到请求时先查一下幂等表如果这个键已经处理过就直接返回上次结果否则正常处理并记录。具体实现上幂等表用唯一键约束并发重复请求只有一条能插入成功其他全部走“已处理”分支。这个方案看着简单但非常有效我们线上所有核心写链路都强制接入了幂等键。超时和重试的搭配建议是超时时间要短、重试次数要少一般1~2次、重试之间要有间隔比如指数退避200ms、400ms、800ms而且重试要限定在“非幂等不可”的前提下。很多人把重试当成灵丹妙药下游一超时就重试结果下游本来就半死不活你重试反而是加大它的压力。正确的做法是先熔断别打了让下游喘口气。3.2 熔断与降级实战一次线上故障的复盘说一个我自己经历过的线上事故。某个周五晚高峰用户下单链路里依赖的优惠券服务某台机器挂了优惠券服务是个集群其他机器还活着但有一台机器的异常导致部分请求超时。我们的调用方设置了超时时间1秒重试1次结果在业务高峰期这些超时请求在调用线程池里疯狂堆积线程池被打满紧接着整条下单链路全部超时最后连订单库的写入都开始变慢。事后复盘我们做对了什么其实什么都没做对我们就是那种光靠超时重试硬扛的典型反面教材。后来我们把熔断机制加了上去用Hystrix或者Resilience4j给每个远程调用配置熔断策略。熔断的核心逻辑是在窗口期比如10秒内如果请求错误率超过阈值比如50%熔断器就打开后续请求直接快速失败不再打到下游经过一个休眠窗口比如5秒之后放一个探测请求进去如果成功就关闭熔断器恢复正常。这个机制本质上就是给系统装上了一个“短路保护器”避免一个局部故障拖垮整个链路。降级和熔断不一样。熔断是“我不调用你了快速失败”降级是“我调用你但用一个简化的逻辑代替”。比如优惠券服务不可用时下单链路可以降级为“默认不给优惠但下单流程继续”推荐服务不可用时商品详情页降级为“展示默认推荐位”。降级的关键在于你要预先知道哪个环节是可以丢的、哪个环节是无论如何都不能丢的。核心交易链路永远不能降级但推荐、个性化、日志这类辅助服务完全可以先保住主干再说。我给的降级思路是把服务依赖分为“强依赖”和“弱依赖”。强依赖挂了就熔断告警宁可快速失败也不能拖垮主流程弱依赖挂了就降级到默认策略让主流程继续跑。团队里一定要有这个分类共识最好在架构评审的时候就把依赖分级写进文档里而不是等故障来了再现场拍板。3.3 补偿机制怎么设计才能兜住底最后这块是最容易被忽视的补偿机制。很多团队以为有了事务消息、有了熔断降级系统就稳了其实真正出事的时候你会发现你还需要一个“最终对账人工介入”的兜底手段。我讲一个真实发生的场景用户下单成功订单状态是“待支付”支付回调因为网络问题没有送达用户的款其实已经扣了但订单系统不知道。这时候自动重试、事务消息都拉不回来这个状态怎么办我们当时设计了一个“支付对账任务”每天凌晨跑一次从支付平台拉取当天所有支付成功的订单号和自己订单表里的状态做比对发现“支付平台已扣款但订单还是待支付”的就把订单状态改成“支付成功”。这个任务不依赖任何消息链路直接面对最源头的支付数据所以保险系数是最高的。对账任务的设计上不能只停留在比对状态还要能自动触发后续动作。比如对账发现订单已支付但库存还没扣减那就自动重新投递一个“扣减库存”的消息发现退款单处理失败超过三次就把这个退款单标记为“人工处理”推送给财务人工介入。这套“对账重试人工兜底”的机制是分布式系统里最后一道防线也是我强烈建议每个团队都要有的。补偿还有一种常见形态在代码里写“反向操作”。比如发起一笔转账A扣款、B加款B加款失败了就自动发起一笔反向转账把A的钱退回来。这种补偿逻辑一定要放在独立的任务或消息消费者里不要放在失败请求的同步返回路径里因为同步返回路径很可能也超时了你不知道它到底执行了没有。我见过一个团队在try-catch里直接调用反向接口结果因为原请求已经超时但实际执行成功反向操作又执行了一遍导致两次退款简直惨案。4. 避坑指南这些经典问题我帮你实验过了前面把三大主题都过了一遍这个部分把我踩过、看过的那些“最容易翻车”的细节集中整理一下如果能让你少走几段弯路这篇就很值了。4.1 分页查询与聚合统计怎么优雅落地分库分表之后跨分片的分页查询是一个老大难。比如按订单号查一个用户的历史订单列表走用户ID分片就能直接定位到某一片没问题但如果是一个后台管理页面要按店铺和时间范围查所有订单那就得先并行查所有分片然后把结果归并排序最后取第N页。数据量一大深翻页page10000基本就是灾难因为每一页都要把所有分片的所有数据捞出来排序。常规解法是禁止深翻页改用游标分页传入上一页最后一条记录的排序字段值查询大于这个值的数据或者“时间分片快速跳过”。我们在后台管理页面的实践方案是把分页限制在前100页超过100页的查询直接提示用户加筛选条件对比数据量大的运营系统来说这个体验是可以接受的。跨分片的聚合统计就别指望SQL了老老实实用ES或者单独建一张宽表来做。我遇到过很多人问“订单表分片了我要按天统计GMV怎么办”你不可能在分片上百个的分片上写count、sum。我们的做法是订单服务实时往Elasticsearch同步一份精简订单数据运营看板、报表都走ES查询效率和灵活性远超MySQL。记住分库分表后的MySQL只负责在线事务处理OLTP一切分析型查询OLAP交给其他存储这是明确的分工。4.2 全局ID生成器挂了怎么办前面提到号段模式它依赖一个发号数据库表那如果发号数据库挂了怎么办我们当时的做法是号段缓存在应用层一次取20000个号正常情况下应用层的号根本用不完即使发号数据库短期不可用也能撑一段时间。同时我们把发号数据库做了主备切换主库挂了自动切备库。雪花算法不依赖数据库看起来更稳但机器时钟回拨的问题永远在那里如果部署时间不同步就会出现随机回拨。我的建议是建立一套全局ID生成服务所有服务通过RPC调用获取ID而不是每个服务各自实现一套这样出了问题治理起来也方便。4.3 一库分表后单库写入还是瓶颈怎么办分表解决的是单表数据量大的问题但如果你只分表不分库所有表还在同一个MySQL实例里写入压力还是集中在那一个实例上尤其是磁盘I/O和Binlog写入。所以真正的水平扩展一定是分库分表配合使用先按业务拆库再在库内分表。比如订单库拆成32个库每个库里订单表拆成32张表路由规则是用户ID取模1024这样的话单实例写入压力只有原来的1/32并且可以从一台物理机扩展到32台物理机。这个方案查询时要算清楚库序号哈希值 % 库数量表序号哈希值 / 库数量 % 表数量路由计算是分库分表中间件的基础逻辑你不需要自己写但一定要理解。4.4 数据同步、回查状态表为什么必不可少我在分布式事务部分几次提到“消息表”“状态表”这里再说一个细节状态表不能只记录成功/失败最好把每次重试的时间、次数、异常信息都记下来方便定位问题。我见过很多系统的消息表就一个state字段重试失败十次也不知道失败原因是什么还要去翻应用日志费时费力。正确的设计是status、retry_count、next_retry_time、last_error_msg、create_time、update_time扫描任务只查next_retry_time小于当前时间的数据配合索引设计任务扫描效率很高。另外对账任务和重试任务之间要有“分布式锁”机制不然多个实例同时扫一张表同一个消息会被处理两遍。我们用的是基于数据库行的乐观锁update ... where status待处理或者Redis分布式锁保证同一时刻只有一个实例在处理某一条消息。这些细节不写好到了流量一大一定会出各种妖蛾子。4.5 压测没问题的系统为什么一到晚高峰就挂这个现象我见过太多次了压测时并发3000系统稳如老狗晚上8点业务高峰实际QPS才800系统挂了。原因往往不是系统容量不够而是压测时你用的是“均匀的、理想的”请求模型线上却是“毛刺状的、突刺的”流量模型。比如秒杀场景下一秒钟涌入几万个请求线程池瞬间被打满GC、锁竞争、慢SQL接踵而至。所以压测不能只压平均QPS要压“峰值并发的瞬时冲击”还要压“故障注入场景”比如下游宕机、网络延迟抖动这才是分布式系统韧性测试的价值所在。熔断、降级策略是否生效也只有在故障注入下才能验证平时不演练真出事了就是一片混乱。4.6 中间件选型别为了分布式事务而上分布式事务最后想泼一盆冷水很多团队引入分布式事务中间件并不是因为业务真的需要而是觉得“别人用了我也得用”结果把简单问题复杂化了。如果你只有一个操作跨了两个服务你完全可以用MQ解耦或者干脆把这两个服务合并成一个别为了技术面子给自己加负担。分布式事务的每一个参与者本质上都是系统中的一个故障点参与越多链路越脆。能不能用“本地事务消息表”实现就不要上TCC能不能用“对账人工兜底”实现就不要追求绝对强一致。架构选型里我最信奉的一句话就是简单是稳定的前提。5. 常见问题速查表分页查询在分片场景下变慢怎么办 答禁止深翻页改用游标分页运营报表走ES或者宽表。分片键选错导致数据倾斜怎么办 答先识别大用户/热数据单独走缓存或独立存储长期方案是设计分片键时兼顾“均匀分散”和“业务查询路径”。全局ID生成器时钟回拨怎么办 答生成器内部维护lastTimestamp发现当前时间小于lastTimestamp时等待时间追平或拒绝服务。本地消息表投递失败一直重试堆积了怎么办 答要区分“重试次数过多”的消息超过N次自动进入死信队列由对账任务和人工介入避免消息表无限膨胀。Buffered异步任务重复消费消息怎么办 答消费逻辑必须做幂等消费前先查消息表状态状态变更用乐观锁/分布式锁防止并发重复。压测时一切正常线上却雪崩怎么办 答补“瞬时高并发冲击”和“故障注入”两类压测场景验证熔断、降级、线程池隔离是否真的能扛住异常。要不要一开始就上分布式事务中间件 答不要。先用消息重试对账解决90%的问题剩下10%真正需要强一致的时候再考虑TCC或Seata。分库分表后跨库join怎么办 答大部分场景不做了。要么在上游做数据聚合要么冗余字段要么同步到ES或宽表非核心的复杂查询可以走离线的ETL方案。6. 个人经验能不分就不分该分时别犹豫最后这段算是给前面所有内容做一个感性的收尾。我在实际做架构的时候最大的体会是分库分表、分布式事务、熔断补偿这三板斧本身不复杂复杂的是你要在正确的时机、用正确的幅度去引入它们。我见过一个项目用户量才几万订单一天几千单就已经把全链路TCC、几十个分片全铺上了最后代码跑起来像在跳芭蕾随时随地要防着哪个环节状态没对上。反过来说我也见过一个项目数据已经到了上亿行还在硬扛单库每天靠凌晨删数据过日子那种“能用就行”的思维最终害的还是自己。我自己的判断标准很简单数据量、写入峰值、依赖数量这三个指标没到阈值坚决不拆一旦到了阈值要拆就一步到位分片规则、全局ID、分布式事务方案、补偿机制全部纳入整体设计不要走一步看一步。分布式系统就是这样前期的过度设计是浪费后期的补丁式改造是灾难找到那个平衡点就是架构师的经验所在。如果这篇文章对你有点帮助或者你有不同的看法欢迎在评论区交流。分布式系统的水深得很一个人摸石头过河容易踩坑多聊聊总是好的。