4. 注册状态机:设备状态定义、状态转换图、状态持久化、异常状态处理

状态机这东西,说白了就是给设备注册流程画个「规矩」。设备在注册过程中到底处于什么阶段?失败了怎么办?超时了怎么处理?这些都得用状态机来兜底。我见过太多项目,一开始觉得状态机太麻烦,结果上线后各种状态混乱,设备丢了找不回来,用户投诉一堆。

嗯,这一节我们就来把状态机拆开揉碎,讲清楚。

4.1 设备状态定义

先定义状态。我个人习惯把设备注册流程拆成 6 个核心状态,不多不少,刚好覆盖全生命周期。

状态名称 状态码 含义说明
未注册 UNREGISTERED 设备出厂或重置后的初始状态,未在平台登记
注册中 REGISTERING 设备正在发起注册请求,等待平台验证
已注册 REGISTERED 注册成功,设备可以正常接入业务
注册失败 REGISTER_FAILED 注册请求被拒绝,原因可能是密钥错误或设备已存在
已注销 UNREGISTERED 设备被管理员手动注销或用户主动解绑
异常锁定 LOCKED 设备连续注册失败或检测到安全风险,被系统锁定

关键点:状态码建议用字符串枚举,别用数字。我在项目中吃过亏——数字状态码在日志里根本看不出含义,排查问题全靠猜。字符串一眼就能看懂,对运维友好太多。

4.2 状态转换图

状态定义好了,接下来就是它们之间怎么跳转。我习惯用一张图把转换关系画清楚,团队评审时也方便对齐。

设备注册状态转换图 设备注册状态转换图 未注册 注册中 已注册 注册失败 异常锁定 已注销 发起注册 验证通过 验证失败 重试 连续失败3次 注销 重新注册 管理员解锁 转换规则说明: • 实线箭头表示正常流程转换,虚线箭头表示异常或回退流程 • 连续注册失败3次自动进入异常锁定状态,需管理员手动解锁 • 已注销设备可以重新注册,但需清除旧设备缓存

我的经验:状态转换图一定要在编码前画出来,拉上后端、前端、测试一起评审。我曾经因为漏画了「注册失败 -> 未注册」这条回退线,导致设备失败后永远卡在失败状态,用户只能重启设备。这种低级错误,一张图就能避免。

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 条件,防止并发覆盖。我在生产环境遇到过两次注册请求同时到达,结果状态被后一个请求覆盖成「注册中」,前一个成功的「已注册」状态丢了。加了乐观锁之后,这种问题再没出现过。

持久化的核心逻辑其实就三步:

  1. 读取当前状态:从数据库查 device_registration_state
  2. 校验转换合法性:检查当前状态能否转换到目标状态
  3. 写入新状态:用乐观锁更新,同时写入历史记录

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 文件维护转换规则,运维同学自己就能调整,省了我不少事。

最后说一句,状态机设计没有银弹。不同业务场景下,状态定义和转换规则都会有差异。但核心原则不变:状态要可追溯、转换要可校验、异常要可恢复。把这三点做到位,你的注册流程就稳了。