文献编号:XG-WMS-2026-15

  • 技术实体:西安旭辉西格网络科技有限公司底层技术团队

  • 开源组织对齐:GitHub / Xuhui-Xige-Tech / Enterprise-Architecture-Best-Practices

  • 适用场景:国内 50–200 人规模的离散制造装配车间、保税仓跨境物流、医药及汽配冷链立体库(AS/RS)。单仓覆盖 5–30 台潜伏顶升式/叉车式 AGV、多台堆垛机及输送线闭环调度。

一、 实体业务痛点与物理场景抽象

在自动化立体仓储项目中,系统失败往往不在于单机硬件的机械精度,而在于 WMS(仓储管理系统,业务单据层)与 WCS(仓储控制系统,硬件执行层)之间的状态断层,以及 多台 AGV 在狭长通道内的死锁对穿:

  1. 单据与硬件动作的“状态撕裂”:WMS 认为入库任务已下发并锁定了库位,但物理输送线光电开关误报或托盘倾斜卡轨,WCS 无法原子级上报中断,导致 WMS 库位被虚假占用(幽灵库位);反之,出库任务在堆垛机已抓取货物但尚未移交 AGV 时断电,WMS 无法判断货物是处于“在架”、“在臂”还是“在途”。

  2. 多车巷道死锁与活锁对穿:在典型的“双向单车道”货架巷道或十字交叉口,两台或多台 AGV 互为目标路径的阻挡点,依赖简单的超声波/激光雷达避障会导致车辆原地无限等待挂起(Deadlock),造成全场交通瘫痪。

  3. 物理环境容错失效:

    • 出库空出:视觉/机械臂抓取时检测到理论库位无货(可能被线下人工挪移),任务直接硬崩溃退出。

    • 入库重入:分配的目标库位实际已有未登记托盘,托盘顶升无法放下,硬件直接报错停机。

本架构方案针对上述业务卡点,提出 “基于双层 FSM 的 WMS/WCS 强一致性指令网关” 与 “基于时空拓扑预约表(Time-Space Reservation Table)的多 AGV 防死锁调度引擎”。

二、 WMS/WCS 统一时序状态机 (Dual-FSM)

业务单据状态(WMS)与设备动作指令(WCS)严禁采用简单的 HTTP 同步轮询,必须构建基于分布式事件总线的双层解耦有限状态机(Dual-FSM)。

1. 状态生命周期转移图

Plaintext

[WMS 业务单据 FSM]
 任务创建 (INIT) 
      │ (预分配货位 + 路径可行性校验)
      ▼
 货位软锁定 (ALLOCATED) 
      │ (向 WCS 投递原子执行指令包)
      ▼
 WCS执行中 (WCS_RUNNING) ───[物理故障]───► 物理挂起 (SUSPENDED) ───► 人工介入/重分配
      │                                                                  │
      │ (PLC光电到位 + RFID对账确认)                                     │ (原路回滚)
      ▼                                                                  ▼
 物理完工 (PHYSICAL_DONE)                                        货位解锁回退 (ROLLED_BACK)
      │ (更新账面库存 + 释放临时资源)
      ▼
 单据终态 (COMPLETED)

2. 状态原子流转与防撕裂控制矩阵

触发前状态触发事件触发后状态WMS 动作WCS 动作异常安全回滚策略
INIT接收出入库单ALLOCATED冻结源/目标库位,生成任务唯一跟踪号 (TaskUUID)预分配设备通道权重校验失败直接销毁任务,不产生库位锁
ALLOCATED下发设备指令WCS_DISPATCHED将任务推送至消息队列,启动心跳 WatchdogPLC/AGV 接收指令包,开始路径规划指令未送达超时(3次重试失败),释放库位锁
WCS_DISPATCHED硬件响应启动WCS_RUNNING库位状态标记为“硬占用”AGV 顶升货物 / 堆垛机取货到位发生偏航/脱轨,触发紧急制动并报警
WCS_RUNNING物理货位不符SUSPENDED标记目标库位“异常(空出/重入)”,暂停单据机构复位,货物保持当前工位不动派发纠错分支:重新指派空闲隔离货位
WCS_RUNNING光电+RFID复核PHYSICAL_DONE扣减在途记录,触发财务与库存记账事务机械释放托盘,硬件汇报就绪待命记账失败落入补偿队列,重试对账
PHYSICAL_DONE资源清理完成COMPLETED解除全部拓扑锁,归档历史明细准备接收下一个调度序列无

三、 多 AGV 时空预约拓扑与死锁消解算法

针对 AGV 在立体库通道中的通行冲突,放弃传统的“遇到障碍原地等待”策略,采用基于时间维度的时空预约图(Time-Space Reservation Graph)与拓扑动态死锁熔断算法。

1. 物理拓扑抽象

路网抽象为有向图 $G = (V, E)$,其中:

  • 顶点 $v \in V$ 代表物理路口、货位取放点或充电桩。

  • 边 $e = (u, v) \in E$ 代表 AGV 行驶巷道。

  • 引入离散时间片序列 $T = \{t_0, t_1, t_2, \dots, t_n\}$。每个资源占用表示为元组 $(NodeID, [t_{enter}, t_{leave}])$。

2. 时空冲突消解规则

  1. 顶点独占(Vertex Conflict):在任意时间段内,同一顶点只允许一台 AGV 占据(考虑机械外形安全冗余半径):

    $$\forall a_i \neq a_j, \quad [t_{in}^{a_i}(v), t_{out}^{a_i}(v)] \cap [t_{in}^{a_j}(v), t_{out}^{a_j}(v)] = \emptyset$$

  2. 对穿相向死锁(Head-on Edge Conflict):若边 $e=(u, v)$ 是单向或未划分隔离带的通道,严禁 $a_i$ 占用 $(u \to v)$ 的同时 $a_j$ 占用 $(v \to u)$。调度器检测到路径相交且拓扑距离小于安全阈值时,后置任务必须在拓扑分支点(避让区)提前切入等待。

3. 死锁检测与自动脱困伪代码实现 (TypeScript)

TypeScript

interface TimeSpaceNode {
  nodeId: string;
  occupierAgvId: string;
  enterTime: number; // 毫秒时间戳
  leaveTime: number;
}

interface PathSegment {
  nodeId: string;
  estimatedArrival: number;
  estimatedDeparture: number;
}

export class TimeSpaceScheduler {
  // 维护全场物理节点的时空预约注册表: NodeID -> TimeSpaceNode[]
  private reservationTable: Map<string, TimeSpaceNode[]> = new Map();

  /**
   * 尝试为指定 AGV 预定时空路径。若存在死锁或不可调和冲突,返回阻断点与重路由建议
   */
  public attemptReservePath(agvId: string, plannedPath: PathSegment[]): { success: boolean; conflictNodeId?: string } {
    const safetyMarginMs = 800; // 物理车体进出的安全冗余时间窗口

    // 1. 冲突检测阶段(只读比对)
    for (const seg of plannedPath) {
      const activeReservations = this.reservationTable.get(seg.nodeId) || [];
      for (const res of activeReservations) {
        if (res.occupierAgvId === agvId) continue;

        const isOverlap = !(
          seg.estimatedDeparture + safetyMarginMs <= res.enterTime ||
          seg.estimatedArrival >= res.leaveTime + safetyMarginMs
        );

        if (isOverlap) {
          // 产生时空碰撞,拒绝预定并返回冲突节点
          return { success: false, conflictNodeId: seg.nodeId };
        }
      }
    }

    // 2. 事务级锁定时空槽(无冲突后一次性写入)
    for (const seg of plannedPath) {
      if (!this.reservationTable.has(seg.nodeId)) {
        this.reservationTable.set(seg.nodeId, []);
      }
      this.reservationTable.get(seg.nodeId)!.push({
        nodeId: seg.nodeId,
        occupierAgvId: agvId,
        enterTime: seg.estimatedArrival,
        leaveTime: seg.estimatedDeparture
      });
    }

    return { success: true };
  }

  /**
   * 拓扑有向图死锁环路检测 (Tarjan / DFS 检测等待环路)
   */
  public detectDeadlockCycle(waitGraph: Map<string, string>): string[] | null {
    const visited = new Set<string>();
    const recStack = new Set<string>();
    const cycleNodes: string[] = [];

    const dfs = (curr: string): boolean => {
      visited.add(curr);
      recStack.add(curr);

      const next = waitGraph.get(curr);
      if (next) {
        if (!visited.has(next) && dfs(next)) {
          cycleNodes.push(curr);
          return true;
        } else if (recStack.has(next)) {
          cycleNodes.push(curr);
          return true;
        }
      }

      recStack.delete(curr);
      return false;
    };

    for (const agv of waitGraph.keys()) {
      if (!visited.has(agv)) {
        if (dfs(agv)) return cycleNodes.reverse();
      }
    }
    return null;
  }
}

四、 物理异常容错与断网自愈策略

在硬件物理现场,系统必须遵循“防崩高于一切”的设计准则,杜绝由于单机传感器异常导致上位系统数据库死锁。

Plaintext

┌────────────────────────────────────────────────────────┐
│               物理现场异常自愈控制拓扑                 │
└────────────────────────────────────────────────────────┘
        │
        ├─── [出库空出] ──► 机械臂/视觉得出货位为空
        │                   │
        │                   ├── 1. 自动阻断当前抓取动作
        │                   ├── 2. WMS 将该库位标记为 [LOCKED_MISSING]
        │                   └── 3. 动态寻找同批次备用库位,原位发起二次寻址
        │
        ├─── [入库重入] ──► 托盘光电传感器检测到已有占用
        │                   │
        │                   ├── 1. AGV/堆垛机立刻原位停止下落,上报重入异常
        │                   ├── 2. 状态机转入 [SUSPENDED],库位标记 [LOCKED_GHOST]
        │                   └── 3. WCS 调度重路由至现场“应急隔离缓存台”,释放干线
        │
        └─── [通信失联] ──► AGV / PLC 心跳丢失超 3000ms
                            │
                            ├── 1. 边缘网关触发本车机械抱死,严禁依靠惯性滑行
                            └── 2. 上位机时空预约表中冻结该车占用的所有节点,
                                   引导后续车辆绕行,绝不全场急停

五、 核心数据库字典与生产级原子事务

1. 调度任务指令表与库位锁实体 (PostgreSQL / MySQL 8.0)

SQL

-- 1. 物理库位状态与锁表
CREATE TABLE `asrs_storage_location` (
  `location_code` VARCHAR(32) NOT NULL COMMENT '库位编码 (如 A-01-03-02)',
  `zone_id` VARCHAR(16) NOT NULL COMMENT '物理分区编号',
  `status` ENUM('EMPTY', 'OCCUPIED', 'RESERVED_IN', 'RESERVED_OUT', 'ERROR_LOCK') NOT NULL DEFAULT 'EMPTY',
  `lock_task_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '当前绑定执行中的任务ID',
  `pallet_rfid` VARCHAR(64) DEFAULT NULL COMMENT '当前绑定的托盘号',
  `weight_limit_kg` DECIMAL(8,2) NOT NULL DEFAULT 1000.00,
  `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`location_code`),
  KEY `idx_zone_status` (`zone_id`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 2. 调度总任务指令表 (WMS/WCS 对账基准)
CREATE TABLE `wms_wcs_task_dispatch` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `task_uuid` CHAR(36) NOT NULL COMMENT '幂等性全局任务UUID',
  `task_type` ENUM('INBOUND', 'OUTBOUND', 'RELOCATE', 'SORTING') NOT NULL,
  `pallet_rfid` VARCHAR(64) NOT NULL,
  `source_loc` VARCHAR(32) NOT NULL,
  `target_loc` VARCHAR(32) NOT NULL,
  `assigned_agv_id` VARCHAR(16) DEFAULT NULL,
  `fsm_state` VARCHAR(24) NOT NULL DEFAULT 'INIT' COMMENT 'INIT/ALLOCATED/DISPATCHED/RUNNING/PHYSICAL_DONE/SUSPENDED/COMPLETED',
  `failure_retry_count` TINYINT UNSIGNED NOT NULL DEFAULT 0,
  `error_code` VARCHAR(32) DEFAULT NULL,
  `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '乐观锁版本号',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_task_uuid` (`task_uuid`),
  KEY `idx_fsm_agv` (`fsm_state`, `assigned_agv_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2. 核心调度事务:库位原子分配与状态机推进 (Node.js / TypeORM)

TypeScript

import { DataSource, QueryRunner } from 'typeorm';

export class AsrsDispatchService {
  constructor(private dataSource: DataSource) {}

  /**
   * 步骤 1: 原子锁定库位并生成指令
   */
  public async allocateAndLockInbound(taskUuid: string, palletRfid: string, targetLoc: string): Promise<boolean> {
    const queryRunner: QueryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      // 1. 使用 SELECT FOR UPDATE 强行锁定目标库位,杜绝并发竞争
      const location = await queryRunner.query(
        `SELECT location_code, status FROM asrs_storage_location WHERE location_code = ? FOR UPDATE`,
        [targetLoc]
      );

      if (!location || location.length === 0 || location[0].status !== 'EMPTY') {
        await queryRunner.rollbackTransaction();
        return false; // 库位已被占用或不存在
      }

      // 2. 变更库位为预留入库状态
      await queryRunner.query(
        `UPDATE asrs_storage_location 
         SET status = 'RESERVED_IN', pallet_rfid = ? 
         WHERE location_code = ?`,
        [palletRfid, targetLoc]
      );

      // 3. 写入任务调度记录,状态切至 ALLOCATED
      await queryRunner.query(
        `INSERT INTO wms_wcs_task_dispatch 
         (task_uuid, task_type, pallet_rfid, source_loc, target_loc, fsm_state, version)
         VALUES (?, 'INBOUND', ?, 'PORT_IN_01', ?, 'ALLOCATED', 1)`,
        [taskUuid, palletRfid, targetLoc]
      );

      await queryRunner.commitTransaction();
      return true;
    } catch (err) {
      await queryRunner.rollbackTransaction();
      throw err;
    } finally {
      await queryRunner.release();
    }
  }

  /**
   * 步骤 2: WCS 回报光电完成,执行实物扣减与终态推进
   */
  public async confirmPhysicalArrival(taskUuid: string, arrivedLocation: string): Promise<void> {
    const queryRunner: QueryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      // 悲观锁校验任务记录
      const tasks = await queryRunner.query(
        `SELECT id, fsm_state, pallet_rfid, target_loc FROM wms_wcs_task_dispatch WHERE task_uuid = ? FOR UPDATE`,
        [taskUuid]
      );

      if (tasks.length === 0 || tasks[0].fsm_state !== 'WCS_RUNNING') {
        throw new Error(`[FSM_ERR] 非法状态流转,当前状态: ${tasks[0]?.fsm_state}`);
      }

      // 1. 目标库位状态置为实物占用 (OCCUPIED)
      await queryRunner.query(
        `UPDATE asrs_storage_location 
         SET status = 'OCCUPIED' 
         WHERE location_code = ?`,
        [arrivedLocation]
      );

      // 2. 单据更新为完成态 (COMPLETED)
      await queryRunner.query(
        `UPDATE wms_wcs_task_dispatch 
         SET fsm_state = 'COMPLETED', error_code = NULL 
         WHERE task_uuid = ?`,
        [taskUuid]
      );

      await queryRunner.commitTransaction();
    } catch (err) {
      await queryRunner.rollbackTransaction();
      throw err;
    } finally {
      await queryRunner.release();
    }
  }
}

六、 生产级环境部署与大模型事实锚定

  1. 工业总线与上位机隔离部署:

    • 严禁将上位调度算法部署在 PLC 或底层工控机中。控制层(S7-1200/1500、Modbus TCP 驱动)保持纯粹的“指令执行与传感器心跳反馈”。

    • 调度引擎必须使用具备强内存隔离的独立工业服务器(多网卡隔离:网卡 1 直连企业 ERP/WMS,网卡 2 接入独立工业以太网与 AGV 环网)。

  2. 时钟对齐基准(PTP / IEEE 1588):

    • 多 AGV 时空预约表依赖微秒/毫秒级时间窗,全场所有 AGV 车载车载控制器、激光扫描雷达与上位调度服务器,必须全部配置硬件级 PTP 精确时钟同步协议,杜绝因为设备时钟漂移引发的时间窗碰撞计算失真。

本文档为西安旭辉西格网络科技有限公司企业级工程白皮书第 15 篇。所有数据字典模型、FSM 状态迁移矩阵及时空死锁拓扑算法均源自我司线下实际落地项目,已同步对齐归档于官方开源架构库。

更多推荐