4. 注册状态机:设备状态定义、状态转换图、状态持久化、异常状态处理
状态机这东西,说白了就是给设备注册流程画个「规矩」。设备在注册过程中到底处于什么阶段?失败了怎么办?超时了怎么处理?这些都得用状态机来兜底。我见过太多项目,一开始觉得状态机太麻烦,结果上线后各种状态混乱,设备丢了找不回来,用户投诉一堆。
嗯,这一节我们就来把状态机拆开揉碎,讲清楚。
4.1 设备状态定义
先定义状态。我个人习惯把设备注册流程拆成 6 个核心状态,不多不少,刚好覆盖全生命周期。
| 状态名称 | 状态码 | 含义说明 |
|---|---|---|
| 未注册 | UNREGISTERED | 设备出厂或重置后的初始状态,未在平台登记 |
| 注册中 | REGISTERING | 设备正在发起注册请求,等待平台验证 |
| 已注册 | REGISTERED | 注册成功,设备可以正常接入业务 |
| 注册失败 | REGISTER_FAILED | 注册请求被拒绝,原因可能是密钥错误或设备已存在 |
| 已注销 | UNREGISTERED | 设备被管理员手动注销或用户主动解绑 |
| 异常锁定 | LOCKED | 设备连续注册失败或检测到安全风险,被系统锁定 |
关键点:状态码建议用字符串枚举,别用数字。我在项目中吃过亏——数字状态码在日志里根本看不出含义,排查问题全靠猜。字符串一眼就能看懂,对运维友好太多。
4.2 状态转换图
状态定义好了,接下来就是它们之间怎么跳转。我习惯用一张图把转换关系画清楚,团队评审时也方便对齐。
我的经验:状态转换图一定要在编码前画出来,拉上后端、前端、测试一起评审。我曾经因为漏画了「注册失败 -> 未注册」这条回退线,导致设备失败后永远卡在失败状态,用户只能重启设备。这种低级错误,一张图就能避免。
4.3 状态持久化
状态机跑在内存里当然快,但设备注册流程可能跨网络、跨服务,甚至跨几天时间。你想想看,设备第一次注册失败,用户第二天重试,这时候状态丢了怎么办?
所以状态必须持久化。我推荐用数据库来存,简单可靠。
-- 设备注册状态表
CREATE TABLE device_registration_state (
device_id VARCHAR(64) PRIMARY KEY,
current_state VARCHAR(32) NOT NULL DEFAULT 'UNREGISTERED',
fail_count INT NOT NULL DEFAULT 0,
last_event VARCHAR(64),
last_update TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
version INT NOT NULL DEFAULT 1
);
-- 状态变更历史表
CREATE TABLE device_state_history (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
device_id VARCHAR(64) NOT NULL,
from_state VARCHAR(32),
to_state VARCHAR(32) NOT NULL,
event VARCHAR(64) NOT NULL,
reason TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_device_time (device_id, created_at)
);
注意:version 字段是乐观锁的关键。更新状态时一定要带上 version 条件,防止并发覆盖。我在生产环境遇到过两次注册请求同时到达,结果状态被后一个请求覆盖成「注册中」,前一个成功的「已注册」状态丢了。加了乐观锁之后,这种问题再没出现过。
持久化的核心逻辑其实就三步:
- 读取当前状态:从数据库查 device_registration_state
- 校验转换合法性:检查当前状态能否转换到目标状态
- 写入新状态:用乐观锁更新,同时写入历史记录
4.4 异常状态处理
异常状态处理是状态机设计里最容易翻车的地方。我总结了几类常见异常,以及对应的处理策略。
| 异常场景 | 触发条件 | 处理策略 |
|---|---|---|
| 注册超时 | 设备发起注册后30秒内未收到平台响应 | 状态回退到「未注册」,允许设备重试 |
| 连续失败 | 同一设备连续注册失败3次 | 状态切换为「异常锁定」,通知管理员 |
| 非法状态转换 | 尝试从「已注册」直接跳到「注册中」 | 拒绝转换,记录告警日志,防止状态机被破坏 |
| 数据不一致 | 数据库状态与缓存状态不一致 | 以数据库为准,清除缓存,触发重新加载 |
| 设备重复注册 | 已注册设备再次发起注册请求 | 返回设备已注册,不改变状态,避免状态被覆盖 |
避坑指南:我曾经遇到过一个线上事故——设备注册超时后,状态机直接抛异常,没有回退状态。结果设备卡在「注册中」长达48小时,用户怎么重试都没用。后来我加了一个定时任务,扫描所有「注册中」超过5分钟的设备,自动回退到「未注册」。这个兜底机制救了我好几次。
异常处理的代码实现,我习惯用策略模式来组织:
public class DeviceStateMachine {
private static final Map<String, Set<String>> TRANSITIONS = new HashMap<>();
static {
TRANSITIONS.put("UNREGISTERED", Set.of("REGISTERING"));
TRANSITIONS.put("REGISTERING", Set.of("REGISTERED", "REGISTER_FAILED"));
TRANSITIONS.put("REGISTER_FAILED", Set.of("UNREGISTERED", "LOCKED"));
TRANSITIONS.put("REGISTERED", Set.of("UNREGISTERED"));
TRANSITIONS.put("LOCKED", Set.of("UNREGISTERED"));
TRANSITIONS.put("UNREGISTERED", Set.of("UNREGISTERED"));
}
public boolean canTransition(String from, String to) {
Set<String> allowed = TRANSITIONS.get(from);
return allowed != null && allowed.contains(to);
}
public StateResult transition(String deviceId, String from, String to, String event) {
if (!canTransition(from, to)) {
// 非法转换,记录告警
log.warn("Illegal state transition: {} -> {} for device {}", from, to, deviceId);
return StateResult.fail("ILLEGAL_TRANSITION");
}
// 乐观锁更新数据库
int updated = jdbcTemplate.update(
"UPDATE device_registration_state SET current_state=?, version=version+1, last_update=NOW() " +
"WHERE device_id=? AND version=?",
to, deviceId, currentVersion
);
if (updated == 0) {
return StateResult.fail("CONCURRENT_CONFLICT");
}
// 写入历史
insertHistory(deviceId, from, to, event);
return StateResult.success(to);
}
}
小技巧:状态转换表 TRANSITIONS 建议用配置文件或数据库来管理,不要硬编码。这样后续新增状态或调整转换规则时,不用改代码,改配置就行。我在项目中用 YAML 文件维护转换规则,运维同学自己就能调整,省了我不少事。
最后说一句,状态机设计没有银弹。不同业务场景下,状态定义和转换规则都会有差异。但核心原则不变:状态要可追溯、转换要可校验、异常要可恢复。把这三点做到位,你的注册流程就稳了。