4、唯一ID生成:UUID、GUID、芯片序列号、自定义ID的生成策略与冲突处理

设备身份的核心,说白了就是那个「唯一ID」。

你想想看,从芯片烧录到最终注销,整个生命周期里所有环节——生产、入库、激活、OTA升级、售后维修——全靠这个ID来索引。ID一旦重复,轻则数据错乱,重则整个设备网络崩溃。

我在一个智能门锁项目里就吃过这个亏。当时用了某款MCU内置的序列号,结果同一批次芯片序列号竟然有重复。嗯,从那以后我对ID生成策略就格外敏感了。

4.1 四种主流ID生成方式

先看一张总览图,把四种方式的定位搞清楚。

设备唯一ID生成策略全景 UUID / GUID 128位,纯软件生成 芯片序列号 硬件固化的唯一标识 自定义ID 按业务规则组合生成 混合策略 多种方式组合使用 关键对比维度:唯一性保证 | 生成时机 | 存储开销 | 冲突概率 | 适用场景 概率唯一,无中心 运行时生成 16字节 硬件保证唯一 出厂即固定 4~16字节 业务保证唯一 烧录时生成 可自定义长度 多重保证 分阶段生成 8~32字节 选择建议 • 消费类IoT产品:芯片序列号 + 自定义前缀(混合策略) • 云端设备管理:UUID v4 或 v5 • 高安全场景:芯片序列号 + 密钥派生ID

4.2 UUID / GUID:概率唯一的利与弊

UUID(Universally Unique Identifier)是嵌入式领域最常见的ID方案。128位长度,理论上重复概率极低——低到什么程度?每秒生成10亿个UUID,持续100年才可能有冲突。

但「概率唯一」不等于「绝对唯一」。我在一个NB-IoT水表项目里就遇到过UUID冲突。当时用了UUID v4(随机版本),生产了50万台设备,结果后台发现两台设备的UUID完全一样。查了半天,发现是某批次烧录工具的随机数发生器种子没重置。

UUID版本选择要点:

  • v4(随机): 最常用,122位随机数 + 6位版本标识。适合大多数场景。
  • v5(SHA-1哈希): 基于命名空间和名称生成,相同输入产生相同输出。适合需要确定性ID的场景。
  • v1(时间+MAC): 包含MAC地址,有隐私泄露风险。嵌入式设备慎用。

注意: 嵌入式MCU的随机数发生器质量参差不齐。有些低端芯片的硬件RNG在启动初期输出的随机数熵不足,生成的UUID容易重复。我建议在烧录工位增加一次「UUID唯一性校验」,用数据库或布隆过滤器做去重。

4.3 芯片序列号:硬件固化的双刃剑

很多MCU和安全芯片内置了唯一序列号(Unique ID),比如STM32的96位UID、ATECC608A的序列号。这东西的好处是——出厂就有了,不用你操心生成。

但坑也不少。我曾经在一个项目中用了某国产MCU的UID,结果发现同一晶圆批次出来的芯片,UID只有最后8位不同。更离谱的是,有些芯片的UID竟然可以全片擦除!

芯片类型 UID长度 唯一性保证 常见问题
STM32系列 96位(12字节) 较好,晶圆级唯一 部分型号可软件修改
ATECC608A 64位(8字节) 优秀,出厂激光刻录 成本较高
国产MCU(如GD32) 80~128位 参差不齐 批次内重复风险
NXP i.MX RT 128位(16字节) 良好,OTP存储 读取需解锁

我的经验: 使用芯片序列号时,一定要做三件事:

  1. 读取后立即校验长度和格式,防止全0或全F的无效值。
  2. 与自定义前缀组合使用,避免不同型号芯片的UID冲突。
  3. 在产线烧录环节做一次「UID去重校验」,防止芯片出厂缺陷。

4.4 自定义ID:灵活但需精心设计

当UUID和芯片序列号都不满足需求时,就得自己设计ID了。我见过最离谱的自定义ID方案——直接用设备MAC地址当ID。结果呢?同一批次网卡的MAC地址竟然有重复,后台直接炸了。

一个靠谱的自定义ID,通常包含以下字段:

// 自定义ID结构示例(16字节)
typedef struct {
    uint8_t  version;       // 版本号(1字节)
    uint16_t vendor_id;     // 厂商ID(2字节)
    uint16_t product_id;    // 产品型号(2字节)
    uint32_t batch_num;     // 生产批次(4字节)
    uint32_t serial_num;    // 流水号(4字节)
    uint8_t  checksum;      // 校验和(1字节)
    uint8_t  reserved;      // 保留位(2字节)
} custom_id_t;

// 生成函数示例
uint64_t generate_custom_id(uint16_t vendor, uint16_t product, uint32_t batch) {
    static uint32_t counter = 0;
    uint64_t id = 0;
    
    id |= ((uint64_t)vendor & 0xFFFF) << 48;
    id |= ((uint64_t)product & 0xFFFF) << 32;
    id |= ((uint64_t)batch & 0xFFFFFFFF) << 0;
    id |= (counter++ & 0xFFFF);  // 流水号
    
    return id;
}

设计原则:

  • 分层结构: 厂商ID + 产品ID + 批次号 + 流水号,便于追溯。
  • 预留扩展位: 至少留2~4字节给未来需求。
  • 加入校验: CRC或简单校验和,防止传输错误。
  • 避免敏感信息: 不要包含MAC地址、IMEI等可追踪个人信息的字段。

4.5 冲突处理:不可避免的工程现实

不管你用哪种方案,ID冲突都是概率问题。关键在于——冲突发生后怎么办?

我经历过最惨的一次:某智能锁项目上线后,用户反馈「我的锁怎么控制不了邻居家的门?」查了三天,发现是云端数据库里两个设备的ID完全一样,导致指令路由错了。

从那以后,我总结了一套冲突处理流程:

  1. 生成阶段预防: 产线烧录时做「ID唯一性校验」,用布隆过滤器或Redis Set做去重。
  2. 注册阶段检测: 设备首次激活时,云端检查ID是否已存在。如果冲突,拒绝注册并触发告警。
  3. 运行时冲突处理:
    • 如果发现两个设备ID相同,立即隔离其中一个。
    • 生成临时ID,通知设备重新注册。
    • 记录冲突日志,用于产线追溯。
  4. 事后追溯: 通过批次号、烧录时间、工位编号定位问题根源。

千万不要做的事:

  • 不要用「随机延迟重试」来解决冲突——在高并发产线场景下,这只会让问题更糟。
  • 不要依赖「概率极低」就不做冲突检测——百万分之一的概率,在百万级出货量下就是必然事件。
  • 不要在设备端做「自动重生成ID」——这会导致设备身份丢失,后续无法追溯。

4.6 我的推荐方案

做了这么多年嵌入式安全,我个人最推荐的是「混合策略」:

  • 基础层: 使用芯片序列号作为ID的不可变部分。
  • 业务层: 加上厂商代码、产品型号、生产批次等自定义字段。
  • 安全层: 对组合后的ID做HMAC签名,防止篡改。
  • 校验层: 在产线和云端各做一次唯一性校验。

这样设计出来的ID,既有硬件唯一性保证,又有业务可读性,还能防伪造。嗯,虽然实现起来多花点功夫,但比起线上出事故再补救,这点成本不值一提。