
交易所-域划分的一些思考做交易所系统的迟早会遇到“域划分”这个问题。尤其是当你的系统从单机Demo演进到多机房、多集群、多团队协作的时候域划分就不再只是代码目录怎么摆的问题而是关系到整个系统的扩展性、可维护性、甚至合规性的顶层设计。我前阵子刚把一个老项目从早期的“一锅烩”重构成了按域拆分的架构踩了不少坑也梳理出了一些清晰的思路今天就把这部分思考完整地分享出来。1. 域划分的本质从“功能模块”到“业务边界”1.1 为什么“按功能拆包”不够用很多团队一开始做交易系统最喜欢也是最自然的做法就是按技术功能来分模块比如通用的工具类、数据访问层、Web控制器、消息队列消费者这种分法在项目初期完全没问题代码量小、几个人改起来也不冲突。但一旦系统开始承载真实的交易逻辑——K线、深度、订单、撮合、清算、风控、行情推送模块之间的依赖就会变得极其混乱。你可能遇到一个最典型的情况行情模块的代码里直接写了订单模块的数据库表或者撮合引擎为了计算风控绕过接口直接调用对方内部服务。按技术分层划分的目录本质上没有形成真正的隔离只是“分文件夹”不是“划边界”。我推动域划分的第一个理由就是技术分层解决不了业务膨胀域边界才能。1.2 交易域划分的核心目标域划分要解决的不只是“代码放哪儿”而是下面这几个具体问题依赖方向清晰域与域之间只能通过明确的接口交互不能直接穿透内部实现。团队归属明确一个域由固定团队长期负责代码评审、变更通知都跟着域走。故障隔离可控一个域出问题比如行情源卡顿不能因为耦合拖垮整个系统。演进独立可行一个域可以单独做技术选型、数据库选型、扩容策略。权限边界合规交易、结算、运营等不同数据范围在域边界上就能控制访问权限。这些目标听起来都是常识但真正落地的时候最大的阻力往往不是技术而是历史包袱——老代码已经耦合到没法看甚至一个事务里既写订单又写结算想要拆开只能靠经验慢慢来。1.3 交易域划分的行业惯例不同交易所的域划分细节千差万别但大的方向基本一致。我梳理一下常见的顶层划分非常具有参考意义域核心职能典型子域行情域管理交易标的的价格、K线、tick、深度、市场统计数据接入、行情计算、行情推送、快照管理交易域用户下单、撤单、订单生命周期管理、撮合前状态维护订单管理、提交网关、风控前置、活动/优惠撮合域订单匹配、价格发现、交易生成、盘口维护匹配引擎、交易生成、内部队列、审计日志结算域交易生成后的清算与结算、账户余额变动清结算、账务处理、费用计算、对账资产域用户各类资产余额、持仓、冻结、充提记录资产账户、充值/提现、额度管理用户域用户身份与登录状态、KYC合规、权限用户注册、登录、KYC、OTA账户权限运营域公告、活动配置、币种上市/退市、市场管理公告中心、活动运营、币种管理风控域实时限制、大额监控、洗钱监测、交易行为风控风控规则、限额、监控、人工审核推送域实时推送用户订单状态、行情变化、公告触达WebSocket推送、App推送、站内信这些域之间会有依赖比如交易域依赖行情域的快照来展示依赖资产域来校验持仓但依赖方式应该通过接口而不是直接去查对方的数据表。2. 核心细节解析域划分的关键原则与取舍2.1 按“业务对象”而非“业务流程”划域最容易犯的错是按一条业务链路去划域。比如用户下单这个流程从用户请求、鉴权、查持仓、检查价格到写入订单、推送给撮合再把成交回传给用户这个流程非常长。如果给它建一个“下单流程域”你会发现后面撤单、改单、查询也都从这条链路里东拉西扯怎么都归不进这个域最终域变成了一个“伪域”——其实还是横跨多个系统的流程编排。我建议按“业务对象”来划域每个域围绕一组成熟稳定的业务对象展开订单、资产、行情、用户而跨域的流程如“下单”只是多个域通过接口合作完成的用例场景不属于某一个域本身。这样设计出来的域边界更稳定不会天天变。2.2 双向依赖是毒药单向依赖是框架域划分之后要立刻检查依赖。规则很简单不允许双向依赖A域不能反过来依赖B域的内部类、内部表。遇到双向依赖只有两个出路就一个域合并它们或者把公共的部分下沉为一个更小的子域然后两者都依赖这个子域。举个实际例子交易域需要“当前最新价”来做止损单判断行情域需要“最近成交”来做指标计算。如果两边各自引用对方循环依赖就产生了。我们的做法是把“市场交易对共识数据”交易对基础信息、价格精度、数量精度下沉成“标的域”或者“基础数据域”这样两个域各自依赖基础数据域问题就结束了价格数据通过消息推送解耦而不是强行拉引用。2.3 数据域内自治域间只走接口这也是我这次重构中最大的一个调整——域内的数据表只能由该域的代码写入和读取其他域需要数据必须通过该域提供的接口/服务/API。如果其他域为了省事直接查询你的表哪怕只是只读一旦表结构变化对方也会崩这种“数据耦合”比代码耦合更隐蔽也更危险。我在排查老代码的时候发现运营后台为了导数据直接连了交易库的只读实例查询订单表导致后来交易库加字段、分表运营后台的SQL全部报错排查了很久才发现。这是个血的教训。正确的做法是如果运营域需要导订单数据交易域提供一个导出接口带上筛选条件和分页参数由交易域自己查自己库里的数据再统一返回。这样至少有以下几个好处接口稳定、权限可控、可以加审计、可以限制导出量防止一把梭把库拖垮。2.4 域的大小与自治程度需要平衡域划得越细自治性越强但跨域通信的复杂度、团队的沟通成本、系统部署的粒度也会越高。在我做重构的时候团队内一直争论要不要把“清结算”从“交易域”再拆出去。我的意见是如果清结算的核心职责和交易域高度交织但团队还没准备好独立运维一套系统就先合在一个域里但代码目录上做好边界划分等稳定了再拆。说实话域划分不是越细越好。一个最基本的原则是每个域至少要有明确的所有者一个团队或一个人并有完整的业务闭环能力。如果一个域拆出来之后做任何一个功能都要跨三个域协作那说明拆错了应该合并。3. 实操过程从现状梳理到域边界落地3.1 现状梳理画清“现在到底有什么”我在启动这次重构时第一步不是写新代码而是“考古”。把现有代码全量扫了一遍画出所有的服务、数据库、接口、消息队列之间的依赖关系。这一步非常枯燥但也是最重要的——如果你连现状的依赖都理不清盲人摸象式的重构会把问题搞得更糟。工具上我用的是读代码逻辑 数据库元数据分析哪些表被哪些服务查过 接口调用链日志抽样。这些东西汇总完画一张大图把每条互相引用的线标出来。结果非常灾难——光订单相关的依赖就有几百条线很多是“查询对方私有表”这种级穿透。3.2 领域建模用事件风暴找出核心域领域建模我没有选特别重的方法论而是用了一个比较轻量但有效的方式——“事件风暴”。找上产品和核心研发把业务核心流程下单、成交、撤单、充值、体现、结算一帧一帧过每个步骤标上触发事件、产生命令、对应的聚合对象、最终产生的事件。这个过程非常能暴露业务理解的偏差也是让团队达成一致的好途径。所谓“聚合对象”你可以简单地理解为一组在业务上必须保持一致的实体集合。比如订单与其订单详情、交易记录这些必须在一个事务里更新就放一个聚合里在交易域内自治。而资产账户与持仓虽然是资产域内的对象但和订单有交互就通过“冻结、解冻、扣减”接口完成而不是直接改表。用事件风暴梳理下来之后核心领域名单就水落石出了订单域、资产域、行情域、结算域、用户域、运营工具域。每一个域都有了明确的业务边界和事件语义。3.3 依赖改造按“由内到外”的顺序切分定完域之后真正动手切分依赖我是按“基础域 → 核心交易域 → 边缘辅助域”的顺序推进的。为什么这个顺序重要因为核心交易域订单、撮合依赖的东西最多如果不先把基础域用户、行情、币种稳定下来后面一步三摇根本没法推进。基础域改造的实操思路用户域将用户基本信息、认证状态、权限角色的所有数据访问收拢到一个用户服务提供统一的“查询、注册、鉴权”接口其他域一律走接口。行情域将行情接入、K线合成、深度管理收拢到一个行情服务外部只通过网关消费不允许直接修改行情数据表。币种/交易对域新增一个轻量的基础数据服务管理交易对、币种精度、合约参数所有域需要这些基础数据时调一次接口并本地缓存。顺序定好之后每个域改造的流程都是先把内部代码按子模块整理 → 对外开放接口 → 收拢数据访问 → 修改其他域的调用方 → 关闭对外数据库读权限。3.4 数据库拆分物理分库与逻辑分域数据库层面我们推荐“先逻辑分域后物理分库”的渐进式方案。老系统最开始是一个核心大库里面订单表、行情表、用户表全混在一起改起来风险极大。我们先在代码里强制每个域只能访问自己的数据源配置用读写分离代理把同一物理库的不同表集合隔离成多个“逻辑库”等运行稳定后再把核心域的表物理迁移到独立实例。物理迁移这一步的关键点是**迁移过程中要保证双写或者平滑切换不能直接把表搬走否则查询全挂。**我们用的流程是用工具做全量数据复制并持续同步增量变更。新逻辑库存开启只读验证同时跑一段时间的规则校验比如订单数和金额逐日对账。切换写入流量到新库并保留一段时间的旧库只读观察日志与告警。确认稳定后彻底下线旧库的读写清理历史依赖。这个过程中的痛苦是一旦你发现某些查询“必须跨库JOIN”就说明之前的域边界划分有问题需要拆接口或者做数据冗余。这个过程是正常的不要怕返工。3.5 技术选型域与域可以不同但不宜过多域拆分后可能会有人提议撮合域要用C提升性能资产域用Java写容易行情域要上Go写高并发。原则上这没问题但我不建议一个中小团队立刻开启多语言多技术栈模式。域划分的好处之一是允许独立演进不代表所有域都要立刻异构。技术栈切换都是有代价的统一的技术栈维护与招聘成本最低。我们的选择是核心交易、资产、结算域保持Java技术栈Web框架轻量化数据存储按需选用MySQL、Redis、Kafka行情推送单独用Go写了一套基于WebSocket的推送服务因为这块的并发模型更吃连接数Go确实更好使。多语言从第二年后才引入且每个新语言只承担单一职责。4. 域接口设计的核心规范对外稳定对内灵活4.1 接口协议统一网关避免点对点域划分后域之间的交互方式需要统一。我们采用的是RESTful JSON作为主接口协议内部异步场景用Kafka消息实时性极高的场景再用短连接TCP比如撮合结果回传或长连接推送。强烈建议不要允许各个域之间直接使用对方内部RPC框架的私有协议不然排查问题时找依赖关系会想死。接口设计上的几个原则每个接口必须有明确的语义如“冻结资产”“查询订单快照”“推送成交回报”绝不允许出现“万能查询接口”。接口版本号从一开始就要带上路径里带版本如/api/v1/orders否则以后做升级兼容就是一场灾难。域间接口必须做限流与熔断不能让一个下游慢查询把上游拖死。4.2 事件的语义化域与域通过“事件”解耦除了接口请求/响应这种同步交互域间还有大量异步事件订单已创建、成交已发生、资产已冻结、行情已更新、结算已完成。这些事件非常有价值因为它们天然服务于解耦。但事件命名上要有规范。我遇到的常见问题就是事件名不规范比如“OrderCreated”和“order_created”混用语义模糊导致消费者订阅判断很痛苦。我们的规范是事件名采用“领域.对象.动作”的格式比如“trade.order.created”消息体里带上事件ID、时间戳、生产者、幂等键便于对账和重试。事件消息的版本兼容也是个大坑。消费者升级慢、生产者升级快你的事件体一改字段老消费者直接反序列化失败。我们用Protobuf序列化并且只增字段不删字段老版本消费者遇到未知字段直接忽略这样降低了升级摩擦。4.3 数据冗余与最终一致性分域之后“跨域查数据”是不被允许的但业务场景又确实需要。最典型的有成交记录表交易域内要展示下单用户的手机号用户域订单页面要显示价格精度基础数据域。如果每展示一个页面都跨3个域同步调接口性能会崩掉。我们的解决办法是“数据冗余最终一致更新”在需要展示的域内冗余一份对方域的基础数据快照比如用户手机号、交易对名称通过事件监听更新。查询走本地库更新由事件异步推送给订阅方。这种方式牺牲了实时一致性但换来了高可用和低延迟用户体验完全感知不到差异。唯一要特别注意的点就是冗余数据口径要对齐比如“用户手机号脱敏后”存还是明文存必须统一规范否则会出现同一个用户在订单域和用户域看到的号码不一样直接投诉。5. 交易域划分下来之后踩过的坑常见问题与排查思路5.1 跨域事务本地消息表 消息事务域划分最直接的痛点就是跨域事务。以前在一个库里面一个事务里把订单写进去了同时扣了资产现在域拆开后一个操作跨两个库没法保证原子性。我的经验是**能用最终一致性解决的绝对不要硬上分布式事务。**比如“下单冻结资产”这个操作正确流程是交易域先写订单状态为“待冻结”同时发一个“冻结请求”事件资产域消费事件扣减并冻结资产资产域再发送“资产已冻结”事件交易域收到后将订单状态更新为“已冻结”。如果资产冻结失败交易域会收到“冻结失败”事件把订单标记为失败。这个流程全是异步事件驱动的虽然状态多了些但工程上非常稳定。本地消息表的思路是在每个需要发事件的域内建一张“事件发送表”本地事务里同时写业务数据和事件记录由后台任务扫表发布到消息队列。这个做法比直接“事务内发消息”可靠得多因为事务提交失败时消息不会发出成功时消息不会丢。5.2 幂等交易系统一切重试的前提分域后为了追求可靠性消息重试是必然的于是幂等就成了交易系统的“命根子”。几乎所有接口只要可能重试都必须支持幂等。比如订单创建接口要支持客户端幂等键资产冻结接口要按冻结单号幂等成交回报消息必须按交易ID去重。幂等实现我推荐两种方式组合数据库唯一约束幂等键建唯一索引冲突时报主键冲突 Redis SETNX短时间窗口去重。对于消息消费者最简单可靠的做法是在本地建一张“消息消费记录表”消息ID做唯一键消费前先插入插入成功才执行真正的业务逻辑执行失败删除记录以便重试。这套幂等机制是在服务端做的不要指望客户端每次都传对了还是要去重校验。5.3 历史数据迁移表结构统一是关键拆分数据库、按域划分后历史数据的迁移是一个逃不掉的硬骨头。我总结了几条经验迁移前先清理脏数据否则很多规则对不平比如既有金额精度不统一导致对账差几分钱。写一个可反复执行的校验脚本迁移过程多跑几轮对账有问题及时回滚。迁移脚本要留好执行日志每一次执行的SQL都记录在案方便回查。迁移尽量在流量低谷执行并做好一键回滚方案。我见过没做回滚方案就直接迁移出了问题只能靠人工修复那几天晚上都会失眠。5.4 灰度发布与依赖升级改接口、切数据源、旧系统下线任何一个环节都可能影响线上用户。所以我们总体上遵循“宁可慢不可错”的节奏A域接口先灰度观察旧调用方与新调用方是否有差异切换数据源先对账对不上就停止下线旧服务先监控一周无报错再下线。这里要特别说一下如果两个域分别归属不同团队接口升级务必提前协商好排期不要“突然改合同”。后端接口一变前端页面、App、第三方客户端都会受影响线上问题大多是这种“静默升级”引起的。每改一次接口我都在发布群里同步“接口变更影响范围”让下游团队自己确认依赖。5.5 性能损耗跨域调用多了怎么顶住不骗人域拆分后单次业务的跨域调用次数确实变多了。比如简简单单打开一个订单详情页可能要聚合查询订单域、用户域、资产域、币种域的数据。如果每个都实时调接口性能立刻崩。我们的应对手段是数据冗余尽量让查询以本地为主前文已说。本地缓存Caffeine分布式缓存Redis多级缓存缓解高频查询。对外查询接口做结果合并网关一次请求批量化获取多个域数据减少网络RTT。对极低频的数据比如用户手机号可以用异步预热缓存避免页面卡顿。实测下来通过这套组合域拆分后查询接口的P99不但没有恶化反而因为缓存命中率高比原来高并发时还要稳一些。5.6 从研发配合到组织架构人和代码要对齐最后提醒一个容易被忽视的点**域划分最终要跟团队职责对齐否则代码划好了但人还是一锅端什么域都改照样乱。**我们重构后按域分配了Owner每个域Owner对代码质量、线上稳定性、接口兼容负责。跨域改动需要先提设计稿在评审会上说明影响范围再进排期。一个感触是域划分不是只有技术架构师的事它是组织结构、协作方式、变更流程的整体重塑。如果只做代码重组而不治理团队协作过半年你会看到新代码又变成了“伪域”——表面分目录内部依赖全部穿来穿去。6. 一些心里话域划分不是一劳永逸的银弹做了这一轮域划分重构我最大的感受是域划分不是终点只是起点。它划定的是“系统如何被理解、被变更、被扩展”的框架但框架不会自动帮你解决所有问题——比如撮合性能优化、风控策略升级、复杂活动逻辑这些还是得在各自的域内一点点磨。另外域划分的节奏一定要和团队成熟度匹配。如果团队对某个业务域的理解还不充分硬拆反而会增加协调成本。我们当时有一个子域运营后台一开始拆得很细后发现运营需求频繁且复杂跨域协作成本居高不下最后又合并成了一个后台域。所以我不建议一步到位拆到极致最好是“边拆边稳稳了再拆”。再分享一个小技巧代码仓库的目录结构要跟域划分严格一一对应尽量避免“域A的代码混在域B的模块里”这个从根目录就做得清晰后期新人上手才快。我在新项目里直接采用了模块化多仓库结构每个域一个独立仓库依赖通过发布版本号管理效果明显更好但要求团队的CI/CD能力和组件仓库能力跟得上。最后不管你的域划分方案经过多少评审、用了多漂亮的理论模型最终的检验标准只有一个**线上出问题时你是不是能在半小时内定位到影响域、责任人以及回滚方案。**如果每次故障你都要翻半天代码才能理清是谁的锅那你们的域划分肯定还有很大的优化空间。希望你做域划分的时候多想想“出问题怎么排障”这个视角而不是只盯着“怎么画图好看”。把域划分的功夫花在平时交易系统才能在大促和极端行情下跑得稳、扛得住。