3. 调度引擎与主数据集成架构
好,咱们进入第三章。前面聊了调度引擎是什么、主数据为什么需要它,现在该动真格的了——把调度引擎和主数据系统真正捏到一起。
我见过不少团队,上来就写代码,结果搞到一半发现调度层和数据层缠成了死结。改一个定时任务,数据层得跟着重构;数据模型变了,调度逻辑全得重写。说白了,就是架构没想清楚。
这一章,我把自己的架构设计思路拆开给你看。整体长什么样、层怎么解耦、事件怎么驱动、API怎么管,一个一个说清楚。
3.1 整体架构设计:三层两总线
我个人习惯把主数据调度系统分成三个逻辑层,中间用两条总线串起来。你想想看,这样每层只干自己的事,改动不会波及隔壁。
| 层级 | 职责 | 典型组件 |
|---|---|---|
| 接入层 | 接收外部请求、鉴权、路由 | API Gateway、负载均衡 |
| 调度层 | 任务编排、触发、状态管理 | Quartz / XXL-JOB / 自研调度器 |
| 数据层 | 主数据存储、版本、血缘 | MySQL、Redis、HBase、消息队列 |
两层之间各有一条总线:事件总线和数据总线。事件总线传递任务状态变更,数据总线传递主数据变更内容。这样调度层不用直接碰数据库,数据层也不用关心谁在调度它。
核心原则:调度层只问「什么时候做」,数据层只答「做什么」。两者通过事件契约沟通,不共享任何内部状态。
下面这张图把三层两总线的结构画清楚了。我每次做架构评审都拿这张图出来,大家一看就明白。
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%。
事件驱动模型能解决这个问题。说白了就是:有事才动,没事睡觉。
我设计的模型包含三个核心角色:
- 事件生产者:数据变更、定时器到期、人工触发,都产生事件。
- 事件通道:用消息队列(Kafka / RocketMQ)做缓冲,削峰填谷。
- 事件消费者:调度器监听事件,按事件类型执行对应任务。
事件的结构我建议统一:
{
"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 网关管入口。各司其职,系统才能扛得住业务增长。