3. 调度引擎与主数据集成架构

好,咱们进入第三章。前面聊了调度引擎是什么、主数据为什么需要它,现在该动真格的了——把调度引擎和主数据系统真正捏到一起。

我见过不少团队,上来就写代码,结果搞到一半发现调度层和数据层缠成了死结。改一个定时任务,数据层得跟着重构;数据模型变了,调度逻辑全得重写。说白了,就是架构没想清楚。

这一章,我把自己的架构设计思路拆开给你看。整体长什么样、层怎么解耦、事件怎么驱动、API怎么管,一个一个说清楚。

3.1 整体架构设计:三层两总线

我个人习惯把主数据调度系统分成三个逻辑层,中间用两条总线串起来。你想想看,这样每层只干自己的事,改动不会波及隔壁。

层级 职责 典型组件
接入层 接收外部请求、鉴权、路由 API Gateway、负载均衡
调度层 任务编排、触发、状态管理 Quartz / XXL-JOB / 自研调度器
数据层 主数据存储、版本、血缘 MySQL、Redis、HBase、消息队列

两层之间各有一条总线:事件总线和数据总线。事件总线传递任务状态变更,数据总线传递主数据变更内容。这样调度层不用直接碰数据库,数据层也不用关心谁在调度它。

核心原则:调度层只问「什么时候做」,数据层只答「做什么」。两者通过事件契约沟通,不共享任何内部状态。

下面这张图把三层两总线的结构画清楚了。我每次做架构评审都拿这张图出来,大家一看就明白。

调度引擎与主数据集成架构(三层两总线) 接入层 API Gateway · 鉴权 · 限流 · 路由 事件总线(任务状态变更 · 调度事件) 调度层 任务编排 · 触发 · 状态管理 数据总线(主数据变更 · 版本 · 血缘) 数据层 主数据存储 · 版本管理 · 血缘追踪 说明:调度层与数据层不直接通信,全部通过事件总线和数据总线解耦

3.2 调度层与数据层解耦:别让它们谈恋爱

很多系统出问题,根源就是调度层和数据层「谈恋爱」了——调度器里直接写 SQL,数据表结构一变,调度任务全挂。我曾经接手过一个项目,调度代码里嵌了二十多条原生 SQL,每次数据模型升级都得改调度代码,苦不堪言。

怎么解耦?我建议用任务定义与数据映射分离的策略。

  • 任务定义:只描述「做什么操作、用什么参数、期望什么结果」,不关心数据存在哪张表。
  • 数据映射:单独维护一份配置,把任务参数翻译成实际的数据源、表名、字段。

举个例子,一个「同步客户主数据」的任务,定义是这样的:

{
  "taskCode": "SYNC_CUSTOMER",
  "operation": "UPSERT",
  "sourceSystem": "CRM",
  "targetSystem": "MDM",
  "params": {
    "batchSize": 1000,
    "retryCount": 3
  }
}

而数据映射配置长这样:

{
  "taskCode": "SYNC_CUSTOMER",
  "source": {
    "datasource": "ds_crm",
    "table": "t_customer",
    "fields": ["id", "name", "phone", "level"]
  },
  "target": {
    "datasource": "ds_mdm",
    "table": "mdm_customer",
    "fields": ["customer_id", "customer_name", "mobile", "customer_level"]
  }
}

你看,任务定义里没有出现任何表名。哪天 CRM 表结构变了,只需要改映射配置,调度层完全不用动。这就是解耦。

我的经验:映射配置一定要支持热加载。我习惯用 Apollo 或 Nacos 做配置中心,改完映射不用重启调度器,实时生效。线上出问题时,这个能力能救命。

3.3 事件驱动模型:让系统自己「动」起来

传统调度是「轮询式」的——调度器每隔几秒扫一次数据库,看有没有新任务。数据量大了以后,轮询就是灾难。我见过一个系统,每秒扫一次任务表,数据库 CPU 直接飙到 80%。

事件驱动模型能解决这个问题。说白了就是:有事才动,没事睡觉。

我设计的模型包含三个核心角色:

  1. 事件生产者:数据变更、定时器到期、人工触发,都产生事件。
  2. 事件通道:用消息队列(Kafka / RocketMQ)做缓冲,削峰填谷。
  3. 事件消费者:调度器监听事件,按事件类型执行对应任务。

事件的结构我建议统一:

{
  "eventId": "evt_20250321143001",
  "eventType": "MDM_DATA_CHANGE",
  "source": "CRM",
  "timestamp": 1742545801000,
  "payload": {
    "entityType": "Customer",
    "entityId": "CUST001234",
    "changeType": "UPDATE",
    "changedFields": ["phone", "level"]
  }
}

调度器收到这个事件,就知道「客户 CUST001234 的 phone 和 level 变了」,然后触发对应的同步任务。整个过程不需要轮询数据库,事件到了自然就干活。

注意:事件驱动不是银弹。如果事件量太大(比如全量初始化),队列会积压。我建议加一个「事件限流」机制,同一实体类型每秒最多处理 500 个事件,超出的直接丢弃并记录日志。等高峰期过了,再补一次全量同步。

3.4 API 网关设计:统一的门面

调度引擎对外暴露的接口不能太散。你想想看,如果每个任务都直接暴露一个 HTTP 接口,客户端得记几十个 URL,鉴权也得写几十遍。所以,我坚持用 API 网关做统一入口。

网关的核心职责就三个:

职责 说明 实现方式
路由 根据请求路径分发到不同调度服务 /task/* → 调度服务;/monitor/* → 监控服务
鉴权 校验调用方身份和权限 JWT + 应用级
协议转换 将外部 HTTP 请求转为内部事件 HTTP → Kafka 消息

我举个例子。外部系统想创建一个定时任务,它只需要调用网关的一个接口:

POST /api/v1/task/create
Content-Type: application/json
Authorization: Bearer <>

{
  "taskName": "每日客户同步",
  "cronExpr": "0 0 2 * * ?",
  "taskType": "SYNC_CUSTOMER",
  "params": {
    "batchSize": 500
  }
}

网关收到请求后,先校验 ,然后校验参数合法性,最后把请求转成一条「任务创建事件」发到 Kafka。调度器消费到事件后,才真正创建任务。整个过程是异步的,网关不阻塞。

关键设计:网关只做「转译」和「校验」,不做「执行」。这样网关可以保持无状态,水平扩展非常容易。我线上部署了 4 个网关实例,前面挂一个 Nginx 做负载均衡,单机 QPS 能到 5000+。

嗯,架构设计这块,说白了就是「分清楚谁该干什么」。调度层管时间,数据层管内容,事件总线管通信,API 网关管入口。各司其职,系统才能扛得住业务增长。