13 KiB
13 KiB
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 工作线程池,引发系统雪崩。 - 轮询架构设计:
flowchart TD Req["Web 请求 (如确认入住)"] --> LocalDB["本地事务持久化 (Commit Local DB)<br/>Accommodation (已入住) + DoorAuthorization (ISSUING)"] --> AsyncDms["异步派发 DMS 创建钥匙 (耗时约200ms)<br/>记录 ExternalOperationRecord (status=ACCEPTED, 10s后轮询)<br/>快速返回 HTTP 200 (提示:授权下发中)"] --> Scheduler["后台调度引擎轮询 (Dedicated IO Pool)<br/>扫描到期任务调用 /doorlock/query-operation-status<br/>操作成功推为 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 接口,这会导致同一学员在门锁中堆积多张相同密码或卡片,占满锁具内存。必须对故障进行严格分类:
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 服务端可能已经处理成功并把钥匙下发到了锁端,只是响应包丢失。若直接重发创建请求,门锁内将存在两套密码。
- 对账方案:
- 标记当前操作为
RECONCILE_PENDING。 - 调度器调用开门钥匙列表或按房间查询当前已有钥匙。
- 若能识别出刚才持有人及手机号生成的有效钥匙,则绑定该钥匙 ID 并恢复轮询。
- 若确认未成功生成,方可发起二次
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 培训结训与学员退训撤权策略
- 培训计划结训 (
Plan Completion):- 主动撤权为主:
- 流程:
Plan Complete→ 对仍处于ACTIVE状态的Door Authorization发起主动Revoke→ 派发 DMSdelete指令 → 保存operationId→ 后台异步轮询确认设备执行结果。
- 流程:
validUntil有效期边界兜底:- DMS Credential 的
validUntil提供授权有效期边界;培训结束时 RailwaySystem 仍坚决主动发起撤权。 - 设备离线情况下的本地过期机制、RTC 硬件时钟行为和离线保证级别需供应商确认(禁止假设设备端 RTC 机制已完成实物验证)。
- DMS Credential 的
- 主动撤权为主:
- 学员中途退训 (
Withdrawal):- 强制触发主动撤销 (
WITHDRAW_REVOKE_RECOMMENDATION = IMMEDIATE_REVOKE)。学员一旦退训,立即将其名下处于ACTIVE的门锁授权状态置为REVOKING并派发删除指令。
- 强制触发主动撤销 (
- 学员请假离校 (
Leave):LEAVE_REVOKE_RECOMMENDATION = NEEDS_BUSINESS_CONFIRMATION。请假期间是否收回房间门禁权限,取决于公寓管理条例(是否保留个人物品及床位),需交由业务部门决策。
5. 调房换房补偿策略 (TRANSFER_COMPENSATION_STRATEGY)
在学员需要从原宿舍 A 调整至新宿舍 B 的业务场景下,涉及两把不同门锁的设备指令,绝不可塞进一个本地数据库事务中同步执行。
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 主锁:配置歧义错误阻断,保护硬件资源。
- 次锁:不额外自动发码,保留未来扩展能力。
- 0 主锁:阻断发码,返回
- 严格限制自动发码范围为房间的唯一定义主门锁:
- 调房安全策略 (
TRANSFER_POLICY = GRANT_NEW_THEN_REVOKE_OLD):- 先为新宿舍发起凭据下发并等待轮询确认生效(ACTIVE)。
- 只有在新宿舍确认生效后,才对旧宿舍发起撤销调用。
TRANSFER_FAILURE_PRESERVES_OLD_ACCESS = YES:若新宿舍凭据下发失败,旧宿舍授权绝对保持 ACTIVE,杜绝学员两头落空进不了门。- 审计确认存在短暂合理的
dual-access window,优先保障可用性。