IoT 打印系统的幂等设计与防重方案

——宁可“多拦一点”,绝不“多打一张”

作者:magixie

场景:IoT 远程打印(类似百步印社)

痛点:接口重试 = 真金白银损失

原则:任何情况下,都不能重复扣费、重复打印


前言

在普通互联网系统里,重复请求最多是:

  • 多扣一次积分

  • 多发一条短信

  • 多记一条日志

但在 IoT 远程打印​ 场景里,重复请求意味着:

纸打出来了,墨用掉了,钱扣过了,还退不了。

这也是为什么我在前几篇一直强调一句话:

IoT 打印系统,幂等不是“高级特性”,而是“生存底线”。

这篇文章,我会从设计思想 → 技术方案 → 实战细节 → 兜底机制四个层次,把这件事讲透。


一、为什么 IoT 打印系统“特别容易重复”

1️⃣ 设备侧天然不可靠

  • 打印机断网

  • Wi-Fi 信号抖动

  • 4G / NB-IoT 弱网

  • 硬件看门狗重启

👉 结果:设备认为“失败了”,其实服务端已经成功


2️⃣ 协议层重试不可避免

  • HTTP 超时重试

  • MQTT QoS ≥ 1

  • SDK 自动重试

  • 运营人员手动“重新打印”

👉 结果:同一条业务请求,被多次送达


3️⃣ 业务语义模糊

很多系统设计成:

POST /print

但没有回答一个关键问题:

“同样的请求,第二次来,算什么?”


二、幂等设计的三个核心原则

✅ 原则 1:业务定义幂等,而不是技术猜幂等

错误示例:

“我们用 Redis SETNX 就算幂等了。”

正确做法:

“同一个商户 + 同一个设备 + 同一个业务单据 = 一次打印。”


✅ 原则 2:越早拦截越好

幂等校验应该在:

  • Controller 之前

  • 下单逻辑之前

  • 扣费逻辑之前

顺序必须是:

防重 → 验参 → 下单 → 扣费 → 打印

✅ 原则 3:幂等 ≠ 不处理

情况

返回结果

第一次请求

正常处理

重复请求(未完)

返回“处理中”

重复请求(已完成)

返回“已成功”

👉 绝不能返回错误让用户重试


三、幂等模型设计(核心)

✅ 1. 幂等 Key 的设计(非常关键)

推荐标准格式:

idempotentKey = merchantId + deviceId + bizNo

字段

说明

merchantId

商户维度

deviceId

设备维度

bizNo

商户侧业务单号(必须)

📌 bizNo 是灵魂

  • 商户订单号

  • 业务流水号

  • 打印批次号

如果商户没传 bizNo,直接拒绝(这是红线)


✅ 2. 幂等状态机

UNKNOWN
  ↓
PROCESSING
  ↓
SUCCESS / FAILED

状态

含义

UNKNOWN

刚进来,还没落库

PROCESSING

已受理,未打印完成

SUCCESS

已打印

FAILED

明确失败(可重试)


四、技术实现方案(生产级)

✅ 方案一:Redis + 数据库双保险(强烈推荐)

1️⃣ Redis 做“第一道门”
SET idempotent:{key} PROCESSING NX EX 300
  • NX:保证只进一次

  • EX:防止死锁

返回结果:

  • OK → 第一次请求

  • NULL → 重复请求


2️⃣ 数据库做“最终裁判”
CREATE TABLE print_order (
  id BIGINT PRIMARY KEY,
  merchant_id VARCHAR(32),
  device_id VARCHAR(32),
  biz_no VARCHAR(64),
  status VARCHAR(20),
  created_at DATETIME,
  UNIQUE KEY uk_merchant_device_biz (merchant_id, device_id, biz_no)
);

唯一索引是最后的防线


✅ 请求处理流程(完整)

1. 校验参数
2. 生成幂等 Key
3. Redis SETNX
   ├─ 失败 → 查询 DB
   │        ├─ SUCCESS → 返回已成功
   │        ├─ PROCESSING → 返回排队中
   │        └─ FAILED → 返回失败原因
   └─ 成功 → 创建订单
            ↓
        扣费
            ↓
        发送 MQ
            ↓
        更新状态

✅ 3. 返回结果必须“语义清晰”

// 第一次
{
  "orderId": "P123",
  "status": "QUEUED"
}

// 重复请求(未完)
{
  "orderId": "P123",
  "status": "QUEUED",
  "message": "任务已在队列中"
}

// 重复请求(已完成)
{
  "orderId": "P123",
  "status": "SUCCESS",
  "message": "任务已打印完成"
}

📌 千万不要返回 500 或 409 让用户“再试一次”


五、MQ 消费端的幂等(非常容易被忽略)

✅ 问题本质

MQ 可能:

  • 重复投递

  • 消费失败重试

  • 消费者重启重放


✅ 解决方案

1️⃣ 消费前校验
if (order.status != PROCESSING) {
    return; // 已处理过,直接 ACK
}
2️⃣ 打印动作幂等
  • 打印机端支持 taskId

  • 同一 taskId只执行一次

  • 或打印前查询“是否已打印”


✅ 3️⃣ 消费成功才 ACK

顺序必须是:

执行业务
↓
更新状态
↓
ACK

而不是反过来。


六、设备侧的防重配合(很重要)

✅ 1. 设备必须带 bizNo

  • 打印机 SDK 强制要求

  • 不传 bizNo 直接拒绝


✅ 2. 设备重试策略

场景

策略

网络超时

指数退避

5xx 错误

不重试

429 限流

排队等待

业务返回“已成功”

不再打印


✅ 3. 本地去重缓存

打印机本地缓存最近 N 条 taskId

防止:

  • 设备重启

  • 网络闪断

  • 重复执行


七、兜底机制(架构师的底线思维)

✅ 1. 定时任务补偿

  • 扫描 PROCESSING超过阈值的订单

  • 主动查询打印机状态

  • 修复状态不一致


✅ 2. 人工干预通道

  • 后台“标记为已打印”

  • 后台“强制重打”

  • 后台“退款”

系统解决 99%,人解决 1%


✅ 3. 对账系统(终极防线)

  • 商户账单

  • 打印日志

  • 设备日志

  • 财务对账


八、小结:一句话记住这套方案

IoT 打印系统的幂等设计,不是“防止重复请求”,而是“允许重复请求,但只生效一次”。


九、架构师的经验之谈

错误认知

正确认知

幂等是技术细节

幂等是业务契约

Redis 就够了

DB 唯一索引才是底线

返回错误让用户重试

返回状态让用户安心

系统 100% 不出错

出错也能兜底


后记

幂等设计,是我在 IoT 打印系统里最不敢偷懒的地方

因为我知道:

**多打一张纸,用户骂的是你;

少打一张纸,用户找的还是你。**

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无聊的老谢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值