4. 事务(Transaction)基础:事务ID的生成与管理、单块与多块消息事务、超时与重试机制

在 SECS/GEM 协议中,事务(Transaction)是消息交换的基本单元。每一个事务都由一个唯一的事务 ID(System Bytes)标识,确保请求与响应能够正确配对。本章将深入剖析事务 ID 的生成与管理策略、单块与多块消息的事务处理差异,以及超时与重试机制的设计原则。

4.1 事务ID(System Bytes)的生成与管理

事务 ID 在 SECS 消息头中占用 4 个字节(System Bytes),是消息的唯一标识。HSMS 和 RS-232 协议均使用该字段来关联 Primary Message 与 Secondary Message。

4.1.1 事务ID的生成规则

  • 递增分配:通常从 0x00000001 开始,每发起一个新事务,ID 加 1。
  • 循环使用:达到最大值 0xFFFFFFFF 后,回绕到 0x00000001(0 保留给测试或特殊用途)。
  • 线程安全:在多线程或并发环境下,必须使用原子操作或互斥锁保护 ID 生成器。
  • 唯一性保证:在同一通信会话中,不能同时存在两个相同的事务 ID。
工程要点: 事务 ID 的生成器应独立于消息发送线程,避免因锁竞争导致性能瓶颈。推荐使用无锁原子计数器(如 std::atomic)实现。

4.1.2 事务ID的生命周期管理

阶段 状态 说明
分配 Pending 生成新 ID,记录到待响应映射表,启动超时计时器。
匹配 Active 收到 Secondary Message,System Bytes 匹配成功,进入处理流程。
完成 Completed 事务正常结束,从映射表中移除 ID,释放资源。
超时 Timeout 未在 T3 时间内收到响应,触发重试或异常处理。
取消 Cancelled 上层应用主动取消事务,清理映射表。
管理策略: 使用哈希表(如 std::unordered_map)存储待响应事务,查找时间复杂度为 O(1)。定期清理超时条目,防止内存泄漏。

4.1.3 事务ID管理架构图

事务ID管理架构图 事务ID管理架构 应用层 发起/取消事务 ID生成器 原子计数器 + 回绕 事务映射表 unordered_map 超时管理器 T3定时器 + 重试逻辑 说明: 1. 应用层发起事务时,ID生成器分配唯一ID,同时注册到事务映射表和超时管理器。 2. 响应到达时,通过映射表查找对应事务上下文;超时触发重试或异常回调。

4.2 单块消息与多块消息事务

SECS 消息根据数据长度分为单块消息(Single-block)和多块消息(Multi-block)。两者的差异直接影响事务的处理方式和超时策略。

4.2.1 单块消息事务

  • 定义:整个消息内容在一个数据块中传输完毕。
  • 特点:一次发送,一次确认;事务生命周期短。
  • 典型场景:S1F1(Are You There)、S2F17(Date Time Request)等短消息。
  • 事务ID使用:Primary Message 和 Secondary Message 使用相同的 System Bytes。

4.2.2 多块消息事务

  • 定义:消息内容被拆分为多个数据块(Block),依次发送。
  • 特点:需要块编号(Block Number)标识顺序,接收方需重组。
  • 典型场景:大容量数据上传(如配方文件、日志记录)。
  • 事务ID使用:同一事务的所有块共享相同的 System Bytes,块编号区分顺序。
注意: 多块消息中,如果某个块丢失或损坏,整个事务必须重试。不能只重传单个块,因为接收方无法部分重组。

4.2.3 单块与多块消息对比

特性 单块消息 多块消息
数据块数量 1 ≥2
块编号 不需要 必须(从1开始递增)
事务ID 唯一 唯一,所有块共享
确认机制 一次ACK/NACK 每块ACK + 最终事务ACK
超时风险 低 高(块间间隔可能超时)
典型长度 < 4KB ≥ 4KB 或自定义阈值

4.2.4 多块消息事务流程

多块消息事务流程图 多块消息事务流程 发送方 (Host) 接收方 (Equipment) Block #1 (Sys=0x1001) ACK Block #1 Block #2 (Sys=0x1001) ACK Block #2 所有块共享同一事务ID (0x1001),块编号递增。接收方重组完成后发送最终事务响应。

4.3 事务超时与重试机制

超时是 SECS/GEM 通信中最常见的异常场景。合理的超时与重试策略是系统稳定性的基石。

4.3.1 关键超时参数

参数 默认值 说明
T3 45 秒 等待 Secondary Message 的超时时间(Reply Timeout)
T5 10 秒 连接建立超时(Connect Timeout)
T6 5 秒 控制事务超时(Control Transaction Timeout)
T7 10 秒 连接空闲超时(Connection Idle Timeout)
T8 5 秒 网络字符超时(Network Character Timeout)

4.3.2 重试策略

  • 固定间隔重试:每次重试等待相同时间(如 5 秒),简单但可能加重网络负载。
  • 指数退避:重试间隔逐渐增加(如 1s → 2s → 4s → 8s),减少拥塞风险。
  • 最大重试次数:通常设置为 3~5 次,超过后上报异常。
  • 事务ID复用:重试时使用相同的事务 ID,确保接收方能识别为同一事务。
最佳实践: 在测试工程中,建议将 T3 设置为 60 秒(略大于默认值),避免因设备负载波动导致误超时。重试次数设为 3 次,采用指数退避(1s, 2s, 4s)。

4.3.3 超时重试状态机

事务超时重试状态机 事务超时重试状态机 IDLE SENT WAITING TIMEOUT RETRY COMPLETED 响应到达 状态转换说明: IDLE → SENT:发送Primary Message;SENT → WAITING:等待Secondary Message; WAITING → TIMEOUT:T3超时;TIMEOUT → RETRY:重试次数未达上限;RETRY → SENT:重新发送。

4.3.4 重试逻辑代码示例

// 事务重试管理器伪代码
class TransactionRetryManager {
public:
    bool send_with_retry(Message& msg, int max_retries = 3) {
        uint32_t sys_id = id_generator_.next();
        msg.set_system_bytes(sys_id);
        
        for (int attempt = 0; attempt < max_retries; ++attempt) {
            if (attempt > 0) {
                // 指数退避:1s, 2s, 4s
                int delay_ms = 1000 * (1 << (attempt - 1));
                std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
            }
            
            if (send_message(msg)) {
                // 等待 T3 超时
                auto response = wait_for_response(sys_id, T3_TIMEOUT_MS);
                if (response.has_value()) {
                    return true;  // 成功
                }
                // 超时,继续重试
                log_warning("Transaction %u timeout, attempt %d/%d", sys_id, attempt+1, max_retries);
            }
        }
        
        log_error("Transaction %u failed after %d retries", sys_id, max_retries);
        return false;
    }
    
private:
    AtomicCounter id_generator_;
    // ... 其他成员
};
测试验证要点:
  • 验证事务 ID 在回绕后不会与未完成事务冲突。
  • 验证多块消息中所有块使用相同事务 ID,且块编号连续。
  • 验证 T3 超时后重试次数和退避间隔符合配置。
  • 验证最大重试次数耗尽后,事务状态正确上报。