# 07 异步轮询、失败重试、本地幂等与换房补偿设计 (Retry, Idempotency and Compensation) > **可靠性第一原则**:**DMS 外部设备故障绝不允许导致本地住宿安排事实发生回滚 (`DMS Unavailable Principle`)**。 > 住宿事实分配成功后,外部设备权限下发即使遭遇网络分区、网关断电或服务不可用,也只在门禁层体现为 `PENDING` 或 `ISSUE_FAILED`,供后台调度补偿或人工排障。 --- ## 1. 异步轮询调度策略 (`Polling Strategy`) ### 1.1 严禁主线程阻塞等待 - 官方文档明示:**“门锁的实际执行结果,务必在调用相关操作后 10 秒查询”**。 - **严重红线**:绝对禁止在 Web 请求线程(Controller/Service)中执行 `Thread.sleep(10000)`!这会迅速耗尽 Tomcat 工作线程池,引发系统雪崩。 - **轮询架构设计**: ```mermaid flowchart TD Req["Web 请求 (如确认入住)"] --> LocalDB["本地事务持久化 (Commit Local DB)
Accommodation (已入住) + DoorAuthorization (ISSUING)"] --> AsyncDms["异步派发 DMS 创建钥匙 (耗时约200ms)
记录 ExternalOperationRecord (status=ACCEPTED, 10s后轮询)
快速返回 HTTP 200 (提示:授权下发中)"] --> Scheduler["后台调度引擎轮询 (Dedicated IO Pool)
扫描到期任务调用 /doorlock/query-operation-status
操作成功推为 ACTIVE"] ``` ### 1.2 门锁 vs 网关轮询参数隔离 官方文档中存在两套截然不同的轮询规约,严禁混淆: - **门锁操作轮询(密码/卡片/远程开锁)**: - `INITIAL_DELAY`: **10 秒**(门锁低功耗唤醒通常需 5~8 秒)。 - `POLL_INTERVAL`: **5 秒**。 - `MAX_POLL_ATTEMPTS`: **3 次**(总超时窗口约 25 秒)。 - **网关在线状态轮询(网关刷新)**: - `INITIAL_DELAY`: **3 秒**。 - `POLL_INTERVAL`: **1 秒**。 - `MAX_POLL_ATTEMPTS`: **5 次**。 --- ## 2. 失败分类与重试策略 (`Retry Strategy`) **严禁**只要发生错误就盲目重新调用 `create` 接口,这会导致同一学员在门锁中堆积多张相同密码或卡片,占满锁具内存。必须对故障进行严格分类: ```mermaid flowchart TD FAIL[发生异常 / 收到非成功结果] --> CHECK{判断故障类型} CHECK -->|A. 请求明确未发送\n(连接超时/本地校验失败)| RETRY_A[安全重试创建: 指数退避\n重试间隔 2s, 4s, 8s, 上限3次] CHECK -->|B. DMS明确返回业务拒绝\n(400/Invalid Room/Invalid Lock)| FAIL_B[不可恢复业务失败\n标记 ISSUE_FAILED, 记录错误日志\n停止重试, 提示管理员核对房源] CHECK -->|C. 成功获得 operationId\n但轮询返回 undone| WAIT_C[设备执行中: 继续等待下一次轮询\n未超上限前保持 PENDING] CHECK -->|D. 网络读取超时(Read Timeout)\n无法确认DMS是否已受理| RECONCILE[状态未决 (UNKNOWN)\n禁止立即重新创建!\n先执行对账查询或主动撤销后新建] ``` ### 2.1 未决状态对账策略 (`UNKNOWN / RECONCILE`) - 当本地向 DMS 发送 `POST /online-password-key/create` 时遭遇 `SocketTimeoutException`(网络读取超时): - **风险**:DMS 服务端可能已经处理成功并把钥匙下发到了锁端,只是响应包丢失。若直接重发创建请求,门锁内将存在两套密码。 - **对账方案**: 1. 标记当前操作为 `RECONCILE_PENDING`。 2. 调度器调用开门钥匙列表或按房间查询当前已有钥匙。 3. 若能识别出刚才持有人及手机号生成的有效钥匙,则绑定该钥匙 ID 并恢复轮询。 4. 若确认未成功生成,方可发起二次 `create`。 --- ## 3. 本地业务幂等设计 (`Idempotency Strategy`) - **外部约束**:DMS API 原生并不支持 `Idempotency-Key` 请求头。 - **本地防护机制**: - 幂等联合主键定义:`IdempotencyKey = Hash(accommodation_id + credential_type + grant_version)`。 - 在 `train_door_authorization` 表中建立唯一约束或在 Service 缓存层加分布式互斥锁。 - **业务效果**:无论操作员在前端如何频繁、多次点击“下发权限”按钮,对于同一个住宿安排的同一个版本,**有且仅有一条处于 `ISSUING` 或 `ACTIVE` 状态的授权记录**,绝不产生重复钥匙。 --- ## 4. 业务触发与撤权决策比选 (Business Triggers) ### 4.1 门禁授权下发触发时机比选 (`ACCESS_GRANT_TRIGGER_RECOMMENDATION`) | 候选方案 | 业务流程说明 | 优势 | 风险与缺点 | 推荐结论 | | :--- | :--- | :--- | :--- | :--- | | **方案 A:住宿分配后立即下发** | 管理员排宿完成即刻向门锁下发限时密码 | 提前就绪,报到现场零等待 | 若学员临时取消参训,需频繁撤权;提前泄露密码可能导致未报到人员提前进入 | 不推荐 | | **方案 B:报到核验确认后触发 (首选)** | 学员现场在《报到管理》通过人脸/刷卡核验后,异步触发门禁下发 | **安全严密**:确保人已到校才给钥匙;与现场报到事件紧密联动 | 报到高峰期对 DMS 带来并发请求压力,需队列平滑削峰 | **(推荐) 提交甲方确认** | | **方案 C:临近住宿开始时间下发** | 开班前 2 小时定时扫描下发 | 自动化高 | 无法过滤未报到/缺勤学员 | 备选 | ### 4.2 授权有效时间窗口缓冲设计 (`Grant Validity Buffer`) - 严禁将门锁密码生效时间与培训班作息严格卡死到分钟(例如培训班 9:00 开始,密码 9:00:00 才生效)。 - **缓冲机制 (Buffer)**: - `accessStartOffset`(提早进房时间):建议可配置提前 **2~4 小时**(允许学员提早入住休整)。 - `accessEndOffset`(退宿宽限时间):建议培训班结束当天可配置延后 **2~4 小时**(允许学员打包行李离开)。 - 有效期计算公式: $$\text{validStart} = \text{Accommodation.assignedStart} - \text{accessStartOffset}$$ $$\text{validUntil} = \text{Accommodation.assignedEnd} + \text{accessEndOffset}$$ ### 4.3 培训结训与学员退训撤权策略 1. **培训计划结训 (`Plan Completion`)**: - **主动撤权为主**: - 流程:`Plan Complete` → 对仍处于 `ACTIVE` 状态的 `Door Authorization` 发起主动 `Revoke` → 派发 DMS `delete` 指令 → 保存 `operationId` → 后台异步轮询确认设备执行结果。 - **`validUntil` 有效期边界兜底**: - DMS Credential 的 `validUntil` 提供授权有效期边界;培训结束时 RailwaySystem 仍坚决主动发起撤权。 - 设备离线情况下的本地过期机制、RTC 硬件时钟行为和离线保证级别需供应商确认(禁止假设设备端 RTC 机制已完成实物验证)。 2. **学员中途退训 (`Withdrawal`)**: - **强制触发主动撤销 (`WITHDRAW_REVOKE_RECOMMENDATION = IMMEDIATE_REVOKE`)**。学员一旦退训,立即将其名下处于 `ACTIVE` 的门锁授权状态置为 `REVOKING` 并派发删除指令。 3. **学员请假离校 (`Leave`)**: - **`LEAVE_REVOKE_RECOMMENDATION = NEEDS_BUSINESS_CONFIRMATION`**。请假期间是否收回房间门禁权限,取决于公寓管理条例(是否保留个人物品及床位),需交由业务部门决策。 --- ## 5. 调房换房补偿策略 (`TRANSFER_COMPENSATION_STRATEGY`) 在学员需要从原宿舍 A 调整至新宿舍 B 的业务场景下,涉及两把不同门锁的设备指令,**绝不可塞进一个本地数据库事务中同步执行**。 ```mermaid sequenceDiagram autonumber actor Admin as 管理员/宿管 participant Service as 住宿调度服务 participant DB as 本地数据库 participant Gateway as 门禁网关 participant DMS as DMS 平台 Admin->>Service: 发起换房 (学员 X: 房间 A -> 房间 B) Service->>DB: 开启本地事务 Service->>DB: 更新原住宿记录 A (status=已退宿/调出) Service->>DB: 创建新住宿记录 B (status=已入住/调入) Service->>DB: 提交本地事务 (业务事实先落盘) Note over Service,Gateway: 阶段一:先发新房权限 (保障学员有地方住) Service->>Gateway: grantAccess(新房 B, 学员 X) Gateway->>DMS: POST /online-password-key/create (锁 B) DMS-->>Gateway: 返回 accepted (新钥匙 ID_B) alt 新房权限下发成功 Note over Service,Gateway: 阶段二:安全撤销旧房权限 Service->>Gateway: revokeAccess(旧房 A, 旧钥匙 ID_A) Gateway->>DMS: POST /online-password/delete (锁 A) DMS-->>Gateway: 返回 accepted (撤销流水) Service-->>Admin: 换房完成,新房密码已生效,旧房权限已注销 else 新房权限下发遭遇不可恢复故障 (如锁B损坏/网络中断) Note over Service: 触发补偿流程 (Compensation) Service->>DB: 记录调房门禁异常报警 Service->>Service: 绝不撤销旧房权限 (保留原钥匙 A,学员不流落走廊) Service-->>Admin: 换房业务记录已建立,但新宿舍门锁下发失败,请人工介入处理 end ``` ### 5.1 换房核心准则总结 - **“先发新、后撤旧”原则**:绝不能先删除了旧房权限再去发新房。若新房门锁出现硬件故障或网络不通,学员将两头落空无法进门。 - **降级容错策略**:新房授权失败时,保留旧房权限,并在前端控制台突出显示“门禁换房未同步”,由管理员一键重试或安排备用钥匙。 --- ## 6. Wave 1 Implementation Result (Wave 1 implemented) - **阶段状态**:Wave 1 implemented。 - **本地幂等模型落地 (`IDEMPOTENCY_MODEL`)**: - 物理约束:`train_external_operation` 设立 `uq_ext_op_idempotency` 唯一索引,数据库级别杜绝重复写入。 - 服务防重:`ExternalOperationServiceImpl.createOperation` 在插入前先查幂等键,命中已有记录直接返回已有实体,防止重复派发异步调用。 - **重试审计与不可变性**: - 终态记录不可覆写:每次重试调用通过 `createRetryOperation` 独立创建自增 `attempt_count` 的新记录。 - 保留完整排障轨迹:`last_error_code` 与 `last_error_message` 详细保留每次失败现场,满足段级安全审计与对账需求。 --- ## 7. Wave 2 Implementation Result (Wave 2 implemented against Mock DMS only) - **阶段状态**:Wave 2 implemented against Mock DMS only。 - **重试与幂等安全落地**: - `AMBIGUOUS_MUTATION_AUTO_RETRY = NO`:写操作(如密码创建)发生网络读取超时(Read Timeout)时,底层 `DmsHttpClient` 坚决抑制自动重试,`passwordCreatePostCounter = 1`,避免在门锁端产生重复凭据。 - 轮询指数退避:`DmsOperationPoller` 在遭遇网络抖动时将状态置为 `RETRY_WAIT`,基于 `Math.pow(2, attemptCount) * 5` 计算下次轮询时间并递增 `attempt_count`,防止高频风暴轮询。 - 401 集中重试:仅在明确认证失败且为安全查询时才发起 1 次 Token 刷新与重试;写操作 401 则集中失效并向外抛错,由上层流水对账接管。 - **迁移标记**: - `008_RUNTIME_FIRST_RUN = NOT_EXECUTED`,`008_RUNTIME_SECOND_RUN = NOT_EXECUTED`。 - `RUNTIME_MIGRATION_GATE_BEFORE_PRODUCTION = REQUIRED`。 --- ## 8. Wave 3A Implementation Result (Wave 3A implemented against Mock DMS only) - **阶段状态**:Wave 3A Access Authorization Orchestration Dry-Run implemented against Mock DMS only。 - **状态驱动对账模型 (`RECONCILIATION_MODEL`)**: - 核心编排组件:`AccessAuthorizationOrchestrator.reconcile(enrollmentId)`。 - 统一检查业务事实并决策输出:`NO_ACTION`、`WAITING_FOR_PREREQUISITE`、`GRANT_REQUIRED`、`REVOKE_REQUIRED`。 - 彻底解耦业务事件发生时序: - Scenario A(先分配住宿,后报到确认):报到确认后触发对账 -> GRANT -> 轮询成功 -> ACTIVE。 - Scenario B(先报到确认,后分配住宿):初次对账 WAITING -> 分配住宿后触发对账 -> GRANT -> 轮询成功 -> ACTIVE。 - **业务主事务完全解耦与独立性 (`LONG_DB_TRANSACTION_AROUND_HTTP = NO`)**: - 严禁长事务包住远程 HTTP 调用:本地写授权记录与写操作流水为短事务,随后进行外部 Gateway 调用,后续再短事务更新受理结果。 - `REPORT_CONFIRM_EXTERNAL_FAILURE_ROLLBACK = NO`:DMS 网络超时或故障时,报到确认(`status 0 -> 1`)、退宿(`status 1 -> 2`)、结训归档(`status 1 -> 4`)等本地主业务事务绝对不会回滚,外部门禁自动转入待重试/挂起态。 - **对账幂等与防重机制 (`RECONCILE_IDEMPOTENT`)**: - 连续多次触发对账(如学员重复点击或短时多事件触发),业务事实未变化时返回 `NO_ACTION`,绝不重复创建业务授权、绝不重复调用 DMS 发放密码凭据、绝不创建重复操作流水。 - **最少特权门锁选择策略 (`LOCK_SELECTION_POLICY = PRIMARY_LOCK_ONLY`)**: - 严格限制自动发码范围为房间的唯一定义主门锁: - 0 主锁:阻断发码,返回 `WAITING_FOR_PREREQUISITE`。 - 1 主锁:正常放行下发凭据。 - >1 主锁:配置歧义错误阻断,保护硬件资源。 - 次锁:不额外自动发码,保留未来扩展能力。 - **调房安全策略 (`TRANSFER_POLICY = GRANT_NEW_THEN_REVOKE_OLD`)**: - 先为新宿舍发起凭据下发并等待轮询确认生效(ACTIVE)。 - 只有在新宿舍确认生效后,才对旧宿舍发起撤销调用。 - `TRANSFER_FAILURE_PRESERVES_OLD_ACCESS = YES`:若新宿舍凭据下发失败,旧宿舍授权绝对保持 ACTIVE,杜绝学员两头落空进不了门。 - 审计确认存在短暂合理的 `dual-access window`,优先保障可用性。