4、唯一ID生成:UUID、GUID、芯片序列号、自定义ID的生成策略与冲突处理
设备身份的核心,说白了就是那个「唯一ID」。
你想想看,从芯片烧录到最终注销,整个生命周期里所有环节——生产、入库、激活、OTA升级、售后维修——全靠这个ID来索引。ID一旦重复,轻则数据错乱,重则整个设备网络崩溃。
我在一个智能门锁项目里就吃过这个亏。当时用了某款MCU内置的序列号,结果同一批次芯片序列号竟然有重复。嗯,从那以后我对ID生成策略就格外敏感了。
4.1 四种主流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存储 | 读取需解锁 |
我的经验: 使用芯片序列号时,一定要做三件事:
- 读取后立即校验长度和格式,防止全0或全F的无效值。
- 与自定义前缀组合使用,避免不同型号芯片的UID冲突。
- 在产线烧录环节做一次「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完全一样,导致指令路由错了。
从那以后,我总结了一套冲突处理流程:
- 生成阶段预防: 产线烧录时做「ID唯一性校验」,用布隆过滤器或Redis Set做去重。
- 注册阶段检测: 设备首次激活时,云端检查ID是否已存在。如果冲突,拒绝注册并触发告警。
- 运行时冲突处理:
- 如果发现两个设备ID相同,立即隔离其中一个。
- 生成临时ID,通知设备重新注册。
- 记录冲突日志,用于产线追溯。
- 事后追溯: 通过批次号、烧录时间、工位编号定位问题根源。
千万不要做的事:
- 不要用「随机延迟重试」来解决冲突——在高并发产线场景下,这只会让问题更糟。
- 不要依赖「概率极低」就不做冲突检测——百万分之一的概率,在百万级出货量下就是必然事件。
- 不要在设备端做「自动重生成ID」——这会导致设备身份丢失,后续无法追溯。
4.6 我的推荐方案
做了这么多年嵌入式安全,我个人最推荐的是「混合策略」:
- 基础层: 使用芯片序列号作为ID的不可变部分。
- 业务层: 加上厂商代码、产品型号、生产批次等自定义字段。
- 安全层: 对组合后的ID做HMAC签名,防止篡改。
- 校验层: 在产线和云端各做一次唯一性校验。
这样设计出来的ID,既有硬件唯一性保证,又有业务可读性,还能防伪造。嗯,虽然实现起来多花点功夫,但比起线上出事故再补救,这点成本不值一提。