
每秒几十万笔交易这话放在大部分系统里都够吓人的可在某些头部支付平台的大促高峰期这就是常态。很多人听见这个数字的第一反应是“那得买多少台服务器”但真把系统扛起来的并不是简单的堆机器而是一套叫 LDC逻辑数据中心的分区架构。LDC 的全称是 Logical Data Center它不是某个具体的中间件而是一种把“数据、流量、计算”整个打包成一个个独立单元来运转的架构思想。核心不是把数据库拆开而是把一整座大商场改造成几十个各自拥有仓库和柜台的小商场顾客按规则自动进入属于自己的那个小商场。这样每个单元都能独立扛住自己的流量互不拖累整体容量可以随单元数量线性扩展。这篇文章不打算做泛泛的介绍我会按工程落地的思路把 LDC 的模型、路由、发号、分库分表、跨域一致性、容灾灰度这些环节拆开讲每一步都结合支付链路里真实会遇到的问题来说。适合正在搞分布式架构、打算做单元化改造或者纯粹想搞懂高并发系统底层逻辑的读者。1. 每秒几十万笔交易为什么“加机器”治标不治本1.1 传统单库架构的瓶颈到底卡在哪网上讨论高并发时最常见的说法是“水平扩展、加机器就好了”。这句话只在应用层成立。应用层无状态多加几个节点确实可以分摊请求但数据库是强状态的。数据库能支撑的并发远低于应用服务器原因有三第一单库的 QPS 上限天然存在。一个配置还不错的 MySQL 实例纯读场景勉强可以跑到每秒几千到一万 QPS一旦涉及写操作尤其是行锁冲突频繁的支付扣款场景实际吞吐会掉得更低。几十万笔交易如果落进同一个库等于让一个窗口处理整栋大楼的排队长龙。第二连接数是硬限制。数据库连接池不可能无限大每个连接都占用内存和 CPU连接数上去之后上下文切换和锁等待反而会拖垮整个实例。应用层可以随便加 Pod但数据库的连接一涨最先崩的往往是数据库。第三单机房部署天然有容灾死角。哪怕你给数据库配了主备甚至做了跨机房同步只要整个机房的网络或电力出问题所有流量会同时受影响。对大促期间动辄数十万 TPS 的系统来说这种风险无法接受。1.2 分库分表治好了读写压力却带来了跨库难题分库分表可以看作 LDC 的“前辈”。把一个大库拆成几百个小库每个库只承担一小部分数据确实解决了容量问题。可用过的人都知道这条路走到后面会碰到不少麻烦。最典型的是跨库查询。订单表按照用户 ID 分成 1024 张表之后想一次性查出一个商家的全部订单或者统计平台上所有待结算金额就必须跑聚合任务去几百张表里扫。如果目标库数据量动辄上亿这种扫描基本不可能实时完成。更麻烦的是跨库事务。一次支付操作可能要扣用户的钱、加商家的余额、更新平台的营销活动记录这三张表被分到了不同的库里任何一个库成功、另一个库失败账就对不上。分布式事务的方案不是没有但在支付这种强一致要求的场景里协调成本高得惊人性能衰减也很明显。分库分表还带来了扩容迁移的痛。数据分了 100 个库结果业务涨到需要 200 个库时得把数据重新打散这个过程一旦出现不一致就是灾难。很多团队不敢轻易做这种操作于是数据库容量永远踩在危险边缘。1.3 LDC 的思路把“数据流量计算”一起分片这就是 LDC 出现的根本原因。分库分表解决的是“数据的拆分”但它没有解决“流量与数据之间的关系”。LDC 做了一个更彻底的抽象把一个系统按某个核心维度通常是用户 ID切分成 N 个逻辑分区每个分区内部包含一套完整的应用集群和一套对应的数据库集群。所有进来的流量在入口处根据路由规则直接进入自己所属的那个分区。这样分库分表时期望做到但没做到的事情被 LDC 补上了——请求和数据天然在同一区域内闭环。比如一个用户的下单请求经过网关后根据该用户 ID 命中 set-7那么这个请求的所有业务处理、数据库读写全部在 set-7 内部完成。不会出现用户的数据在 A 库、请求却去了 B 库的情况。每个 set 就是一个天然的分布式事务边界绝大部分业务操作根本不需要跨区因为数据都在自己家里。所以与其说 LDC 是一种数据库拆分方案不如说它是一套完整的“区域自治”架构。它给后续要讲的单元化、灰度、容灾打下了基础。2. LDC 的分区模型流量和数据如何被关进“各自的辖区”2.1 逻辑数据中心到底“逻辑”在哪这里的“逻辑”两个字是针对“物理数据中心”来说的。物理数据中心是一栋楼、一个机房逻辑数据中心则是一个抽象概念它可能只占物理机房的一部分也可能横跨多个机房。在 LDC 架构里最小的自治单元叫 set也叫单元/分区。一个 set 包含了一组应用实例和一组数据库实例能够独立完成某个业务域的大部分功能。把若干个 set 组合起来才是完整的一套逻辑数据中心。set 和 set 之间的数据是严格隔离的。数据按路由键一般是 user_id打散到不同的 set每个 set 只有整个系统一部分业务数据的全集。这种设计带来的好处是想扩展系统容量时不需要动现有 set直接增加新的 set 并把一部分用户数据迁过去就行。2.2 路由规则从 userId 到 set 的映射路由规则是 LDC 最核心的机制它决定了每个请求应该进入哪个 set。业内最常见的做法是哈希取模也可以用一致性哈希。伪代码很简单// 定义路由规则100 个逻辑分片映射到 10 个 set shard userId % 100 if shard in [0, 9]: targetSet set-0 elif shard in [10, 19]: targetSet set-1 elif shard in [20, 29]: targetSet set-2 ... else: targetSet set-9路由规则不是写在某个业务类里的静态代码而是配置在配置中心由所有入口网关、应用框架共同读取运行时会根据最新规则实时生效。比如要把原本属于 set-1 的 10 个 shard 拆一半到新扩容的 set-10只需要调整配置不需要重新发布所有应用。这种动态路由能力对支付平台极其重要因为大促期间的流量峰值不是均匀分布的。某些热点活动可能让特定人群的并发量暴涨这时候如果能把这些用户关联的 shard 临时切换到专用 set就能避免热点拖垮整个集群。2.3 物理部署形态每个 set 都是多机房多副本逻辑上 set 是隔离的但物理上并非每个 set 独占一台物理机。实际部署中通常会让 set 的主副本固定在某一个机房同时在另一个机房部署只读副本或备副本做跨机房容灾。假设一个支付平台部署了两地三机房set-0 的主库在机房 A备库在机房 Bset-1 的主库在机房 B备库在机房 A。这样任何一个机房整体故障最多损失一半 set 的写入能力另外一半 set 还可以继续服务。再配合流量调度系统把故障 set 的读写流量整体切换到备份机房就能在分钟级完成容灾切换。一个 set 内部还会有更细的分片。比如 set-0 内部的订单表还会再拆成 20 张物理表。这里的逻辑就变成了两层第一层是 LDC 路由决定进入哪个 set第二层是传统分库分表决定写入 set 内哪张表。两层配合才能无限水平扩展。3. 支撑 LDC 运转的三驾马车路由、发号器、跨域异步3.1 全局唯一发号器没有它分片就是一场灾难数据被拆到多个 set 之后数据库自增 ID 必然不可用——两个 set 的订单表可能同时产生 ID1001 的订单。一旦后续要做数据合并、对账、跨区查询这种冲突就是噩梦。所以 LDC 架构里必须有全局唯一的发号器。支付场景常用两种方案一种是集中式发号服务各业务方启动时批量申请一段 ID内存里用完再申请下一段。类似数据库连接池的思想减少与发号中心交互的次数。假设一次申请 1000 个 ID哪怕发号中心每秒钟只处理 100 次请求也能支撑每秒 10 万个新 ID 的生成读写分离后扛几十万完全没问题。另一种是雪花算法。64 位 ID 由时间戳、机器编号、序列号组成机器本地生成不依赖网络调用。但雪花算法有一个非常著名的坑时钟回拨。如果机器的系统时间偷偷往回跳生成的 ID 就可能重复所以实际落地时都会在算法外层加上“发现时钟回拨立即拒绝服务或等待校准”的保护逻辑。对支付系统来说我建议优先选集中式发号器并使用双 buffer 预取模式。所谓双 buffer就是本地保持两个号段缓存一个正在使用一个预备当前号段用到一定比例时立刻异步加载预备号段。这样即使发号中心短暂抖动也不会影响业务。3.2 跨域异步化能不同步调的调用坚决异步LDC 要求尽量把事务控制在单 set 内部但总有一些操作天然要跨 set比如一个用户买了一个商家的产品订单按买家 ID 路由到了 set-0而按商家 ID 分片的数据在 set-3。如果这时候强制让 set-0 和 set-3 的数据库做跨区事务成本会非常高。支付系统通常的做法是把跨域调用从同步变成异步通过消息中间件传递最终一致的数据。扣款这个动作必须强一致必须用本地事务。但扣款之后要更新商家余额、累计用户积分、通知营销系统这些都是可以后置的步骤。操作大概是用户订单在 set-0 内部完成扣款订单库和平台余额表写入成功。同一事务内往本地消息表插入一条“待发送商家入账消息”。事务提交后异步任务扫描消息表并投递到消息中间件。商家余额系统可能在另一个 set消费消息给商家加钱并写幂等表。如果投递或消费失败事务会自动重试直到成功幂等表保证不会重复加钱。这套方案没有分布式事务锁性能损耗小还能保证最终一致。代价是业务代码里必须接受“有一定延迟”的现实通常延迟在毫秒到秒级对支付场景完全可以接受。3.3 三者的协作顺序一次完整的支付请求在 LDC 架构下的处理路径大致是网关拿到请求中的 user_id查询路由表确定目标 set。应用调用发号器获取全局唯一订单号。在目标 set 内完成所有强一致事务。事务内产生跨域消息写入本地消息表。异步任务投递消息跨域系统消费并执行最终一致操作。路由规则决定“去哪”发号器决定“怎么编号”跨域异步化决定“如果必须跨区怎么办”。三者缺一不可。4. 落到数据库层面单元化之后的分库分表与数据迁移4.1 set 内部依然要分库分表两层分片叠加LDC 出现之后很多人误以为“做单元化就不用分库分表了”这个理解是错的。LDC 解决的是流量入口和事务边界的问题而单个 set 内部的数据库容量依然是有限的。一个合理的做法是两层分片。第一层按 user_id 把用户分布到 N 个 set第二层每个 set 内部再按业务表把数据切分成 m 个物理分片。举一个实际数字模型。假设系统总共有 10 个 set每个 set 内部按订单表拆成 10 个库、每库 10 张表整体物理分片就是 10×10×101000 个。单库单表能扛每秒 200 QPS理论上整套架构就能扛 20 万 QPS。当然这只是理想值实际会有热点、锁竞争、网络开销等损耗但数量级已经足够说明问题。4.2 全局维度查询LDC 架构的软肋与解法LDC 的致命弱点是“按路由键之外的维度查询”。数据按 user_id 分散到 1000 个分片之后想按商家 ID 查交易明细怎么办业界的标准解法是“维度路由 全局索引”。也就是说除了按 user_id 做的分区表还会额外维护一套以 merchant_id 为路由键的索引表。这套索引表不需要保存全量交易数据只需要保存“交易 ID 所属 set 标识 原始表位置”。当需要按商家维度查询时先查索引定位到对应 set再回源请求真实数据。为了避免这套索引表被高频更新拖垮通常采用异步构建业务事务里不写索引通过订阅消息或定时任务把交易事件同步到索引服务批量写入。可能大家已经发现了本质上就是一个“牺牲实时一致性换取跨维度查询能力”的 CQRS 变体在支付场景很实用。4.3 数据迁移与切流LDC 变更里最危险的动作扩容、重新分片或调整路由映射时需要把一部分数据从一个 set 迁到另一个 set。这个动作风险极高因为一旦路由和数据的迁移顺序搞反就会出现流量去了新 set、数据还在老 set 的尴尬局面。标准操作流程包括三个阶段先把目标 set 的数据库结构建好开启数据同步任务从源 set 搬运历史数据到目标 set通常配合订阅源库 binlog 做增量同步把两层的数据拉平。然后灰度切流。将少量真实用户的 shard 映射规则改到目标 set此时新老 set 同时存在需要开启双写校验。同一个新增交易同时写入源 set 和目标 set对比两边结果不一致就告警。这一步一般持续几天确认数据一致率 100% 后才算过。最后全量切换。把路由规则中所有 shard 指向新 set并关闭源 set 的写入。做这一步之前必须准备好回滚预案一旦新 set 出现问题立即把路由规则回切并处理增量期间的消息丢失。整个过程说起来并不复杂但实操里最耗时间的是第一阶段同步。如果源 set 有上亿行交易数据全量同步本身就是一场硬仗。5. 跨调用域的一致性最终还是落在幂等与对账上5.1 本地消息表与事务消息最终一致的经典解法支付业务的特点决定了它不能随便用最终一致但也不能所有环节都用强一致分布式事务。我的经验是把操作分两类涉及资金变动的核心扣款操作必须强一致围绕核心操作的外围动作如积分、通知、报表可以最终一致。本地消息表是实现最终一致最可靠的方案之一。前面已经在跨域异步里提过大致流程这里再补充一个关键细节消息表的投递必须和业务事务在同一条数据库连接里提交否则会出现“业务成功但消息没发出去”的洞。投递本身要做重试启动一个后台任务定期扫描消息表中状态为“待发送”且未发送成功的记录重新推送。接收方消费消息时第一件事就是查幂等表判断这条消息是否处理过。幂等键通常取业务 ID 加动作类型比如“订单号 商家入账”只要之前处理过就直接返回成功不重复执行。5.2 TCC 只用于关键路径本地消息表适合异步场景但如果某一次操作必须实时返回结果给用户比如银行卡扣款加余额就不能接受“先返回成功再慢慢入账”。这时候需要 TCCTry-Confirm-Cancel方案。TCC 的原理是把一个分布式事务拆成三个接口Try 阶段预扣资源Confirm 阶段提交资源Cancel 阶段释放资源。还是拿用户买商品举例扣款接口 Try 里只做用户余额的预扣不真正扣减商家加钱接口 Try 里只做资金预占整体 Try 都成功后再统一执行 Confirm 真正入账。如果某个 Try 失败就执行所有已成功的 Try 进行 Cancel回滚预扣资源。TCC 代码量不小开发成本显著高于普通事务所以只建议用在资金链路上最核心的几处。非资金场景一律优先考虑本地消息表。这里的取舍原则值得每位架构师牢记越强的保证往往意味着越大的复杂度和性能开销分布式事务没有银弹。5.3 对账体系是最后一道大坝分布式系统再精细也难免出现极端情况消息中间件消费了重复消息、事务挂起后一致性校验没跑完、网络分区导致两端数据短暂不一致。这时候唯一能兜底的是高频率、自动化的对账任务。LDC 架构下的对账有一个天然优势每个 set 内部先做本地对账把该 set 自己的交易流水和账户余额对平然后汇总到总账中心再做一次全局对账。全局对账不用扫 1000 张表只需要处理 set 之间可能存在的跨域流水规模小了很多。我见过很多团队的对账任务只在凌晨跑一次间隔太长出了问题很难定位。建议日终对账保留但也要增加一个高频对账任务至少做到分钟级校验关键资金流水的一致性。这套体系对于支付类系统来说可以说是最后的生命线。6. 灰度、容灾与容量评估LDC 带来的工程红利6.1 按 set 灰度新版本不再需要“全量上线”传统的灰度发布一般是在路由层按流量百分比控制。LDC 架构下灰度变得更简单直接把某个 set 切到新版本让这个 set 承载真实流量验证没问题后再逐 set 放量。这种“按区域灰度”的方式天然隔离了风险面。比如新版本对账引擎只改了 set-3 和 set-7 两个 set 的路由配置那么这两个 set 的用户会跑到新引擎上其他 set 用户继续走旧引擎运行。观察指标时只需要对比新老引擎在相同压力下的成功率、耗时和资金差错率就能快速判断新版本是否值得全量推广。6.2 容灾切换的最小单元就是 set没有 LDC 之前容灾切换最小单位是机房。一个机房整体故障要么全站切换要么接一半流量操作粒度很粗且影响范围大。LDC 之后容灾单元被细化到 set能做到“哪里坏了切哪里”。实际演练中比较常见的场景是机房 A 故障关闭所有指向机房 A 的 set 入口流量同时把对应的数据库主备角色在机房 B 激活流量切到支撑机房 B 的 set。从监控发现异常到全部切换完成通常在数分钟到十几分钟以内。达到这个水平需要前期大量的故障演练尤其是切换期间新产生的交易不能丢、消息不能积压太多。6.3 容量评估不能靠猜要用分层压测LDC 极大简化了容量估算因为有了一条从单 set 外推整体的公式总容量 单 set 能承受的极限 × set 数量。大促准备时不用再对整站做一次大型全链路压测只需要找 1 到 2 个承压最重的 set 做极限压测再按比例扩展到总量。极限压测的场景设计要特别用心把当年大促预估的核心链路占比、热点商品抢购、优惠券发放等条件都模拟进去不能只测简单的下单接口。同时要分层看数据网关层、应用层、数据库层、消息队列每层的水位都要记录任何一层提前打满都要继续优化直到所有层都能同步吃到极限。7. 落地 LDC 最容易踩的坑及实战建议7.1 路由键选错后面寸步难行路由键是整个 LDC 的最核心的决策比技术选型更容易犯错。订单按 user_id 路由那么“商家整体交易报表”就成了跨域查询如果按 merchant_id 路由那么“用户所有订单查询”又会拆散。个人建议优先按用户维度路由因为支付场景里高频操作大多由用户发起。商家维度的跨域查询就用前面讲的维度索引去兜底不要因为个别的查询需求改变主干逻辑。路由键一旦定了几乎很难改——改路由键等于把所有历史数据重新打散迁移代价不可估量。7.2 跨域消息容易失控必须设监控红线业务团队为了图省事有时候会把不该异步的调用做成异步或者把跨域消息越做越多。等意识到问题的时候消息中间件已经成了新的瓶颈。我在项目中硬性要求两条监控红线跨域消息占比不能超过总消息量的某个阈值每个 set 的消息积压延迟不能超过规定时间。每次新功能评审都要问一句这个数据是不是必须从别的 set 拿能缓存就缓存能本地化就本地化实在不行再谈跨域。7.3 发号器一旦不可用全盘瘫痪发号器看似轻量实际上整个系统的命门。如果它挂了新订单、新流水、新消息全都造不出来。因此发号器必须做集群化部署并且跨机房容灾。另一个很容易忽略的问题是发号器与业务系统的时钟关系。应用服务器的时间如果和发号中心不一致生成的 ID 会出现时间维度上的漂移影响按时间排序的功能。部署时一定要统一各节点的 NTP 校准并且发号器本身要具备时钟拨回去重保护。7.4 别把“单元化”理解成“分区就万事大吉”有一种心态很危险系统切成了 10 个 set就觉得自己高枕无忧了。实际上 LDC 只是把压力分散了并没有消灭压力本身。单个 set 内部的数据库依然会面临热点商品、热点用户、热门活动带来的局部热点还可能出现某些 shard 数据倾斜严重的情况。踩过几次坑之后我仍然在强调基础功夫数据库索引、SQL 质量、缓存设计、连接池参数这些基本功在单元化架构里一个都不能少。LDC 只是让系统在架构层多了容错和扩展能力它从来不是某一处细节优化的替代品。从我个人的实际体验来讲LDC 最迷人的地方在于它把一套庞大到无法直视的系统拆成了一个个可以独立理解、独立运维、独立扩展的逻辑单元。每一个 set 内部的开发模式本质上和做一个“单体系统”没有区别这让新同学更容易上车也让老团队可以在可控范围内不断演进架构。如果你所在的系统每天已经需要处理千万级以上的请求或者正在为数据库容量和容灾能力发愁仔细研究并尝试这套思路非常值得。