4. 请求队列与重试策略:离线队列数据结构、指数退避、幂等性保证
4.1 原理概述
离线优先应用在无网络时不能丢弃用户请求,必须将请求暂存到本地队列,待网络恢复后按顺序或优先级重试。核心挑战:队列持久化(断电不丢数据)、重试间隔控制(避免拥塞)、幂等性(防止重复执行导致业务数据错误)。
| 术语 | 说明 |
|---|---|
| 请求队列 | 以结构化数据(JSON)形式存储的未完成请求,通常按时间戳或优先级排序 |
| 指数退避 | 每次重试间隔倍数增长:interval = base * 2^attempt + jitter,防止服务器雪崩 |
| 幂等键 | 每个请求携带唯一ID(UUID v4),服务端通过该ID去重,确保多次执行结果一致 |
4.2 离线队列数据结构设计
使用 SQLite 作为持久化队列,避免进程重启丢失。一个请求记录包含:
CREATE TABLE pending_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
request_id TEXT UNIQUE NOT NULL, -- 幂等键
method TEXT NOT NULL, -- POST / PUT / DELETE
url TEXT NOT NULL,
headers TEXT, -- JSON 对象
body TEXT, -- 请求体(字符串化)
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
retry_count INTEGER NOT NULL DEFAULT 0,
next_retry_at INTEGER NOT NULL DEFAULT 0, -- 下次重试时间(unix秒)
status TEXT NOT NULL DEFAULT 'pending' -- pending | retrying | completed | failed
);
内存缓存层使用 PriorityQueue 或 SortedSet(如 Redis 风格的 ZADD),按 next_retry_at 排序,仅暂存最近100条,减少SQLite频繁读。
4.3 指数退避与抖动算法
标准指数退避公式:
delay = min(base * 2^attempt, max_delay) + random(0, jitter_ms)
// 示例参数
base = 1000 // 1秒
max_delay = 60000 // 1分钟
jitter_ms = 500 // 0~500ms随机
实现函数(JavaScript 示例):
function computeRetryDelay(attempt) {
const base = 1000; // 1s
const maxDelay = 60000; // 60s
const jitter = 500; // 0.5s
let delay = base * Math.pow(2, attempt);
delay = Math.min(delay, maxDelay);
delay += Math.floor(Math.random() * jitter);
return delay;
}
4.4 幂等性保证
客户端生成请求ID(UUID v4)并填入请求头 X-Idempotency-Key。服务端需实现分布式去重:
// 服务端伪代码
function handleRequest(req) {
const key = req.headers['X-Idempotency-Key'];
if (!key || alreadyProcessed(key)) {
return { status: 409, body: 'Duplicate request' };
}
markProcessing(key); // 原子性插入去重表
try {
const result = executeBusinessLogic(req);
markCompleted(key, result);
return result;
} catch (e) {
markFailed(key);
throw e;
}
}
去重表同样可用 SQLite,TTL 清理:
CREATE TABLE idempotency_keys (
key TEXT PRIMARY KEY,
status TEXT NOT NULL DEFAULT 'processing',
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
);
-- 清理已过期(如24小时前)的记录
DELETE FROM idempotency_keys WHERE created_at < strftime('%s','now') - 86400;
4.5 请求入队与调度流程
- 应用层产生请求:捕获异常(如
fetch返回TypeError: Failed to fetch)后不直接丢弃,而是调用enqueueRequest()。 - 入队函数:生成
request_id,插入 SQLite,同时更新内存优先级队列。 - 网络监听器(
navigator.onLine/NetInfo)变化时触发flushQueue()。 flushQueue()从队列选出next_retry_at <= now的请求,逐个尝试发送。- 发送成功:标记 status = 'completed',删除幂等键(或归档)。
- 发送失败:增加
retry_count,计算下次重试时间,更新next_retry_at,重新入队。
关键指标:队列最大长度建议控制在1000条以内,超出则拒绝新请求或弹出最旧请求。SQLite 写入频率应合并为批量插入,避免频繁 I/O。
4.6 流程图:离线请求队列与重试
4.7 调试与日志建议
在开发阶段,建议输出的日志行(JSON 格式):
{"event":"enqueue","request_id":"a1b2c3...","method":"POST","url":"/api/orders","retry_count":0}
{"event":"retry","request_id":"a1b2c3...","attempt":3,"delay_ms":4000,"next_retry_at":1715760000}
{"event":"completed","request_id":"a1b2c3...","status":201}
{"event":"max_retry_exceeded","request_id":"a1b2c3...","retry_count":5,"action":"discard"}
使用 adb logcat(Android)或 console.log(Web/Electron)实时查看队列状态。为避免刷屏,建议将日志级别分为 queue-debug(默认关闭)和 queue-info(默认开启)。
4.8 常见错误与解决方案
| 错误现象 | 原因 | 修复 |
|---|---|---|
| 队列请求丢失 | SQLite 写入未 COMMIT 或进程被异常 kill | 使用 PRAGMA journal_mode=WAL 并确保每次插入后立即 COMMIT |
| 重试风暴(Retry Storm) | 未加 jitter 或退避因子小,大量请求同时爆发 | 必须引入随机抖动,且 base 不低于1秒 |
| 幂等键重复导致业务报错 | 客户端未正确生成唯一 ID,或服务端去重表 TTL 过短 | 服务端设置至少 24h TTL,客户端使用 crypto.randomUUID() |
| 队列无限增长 | 网络长期不可用,重试次数用尽后未丢弃 | 设置最大重试次数(如5次)和队列最大长度(如500条),超过则丢弃 |
4.9 性能与稳定性优化
- 批量写入:将多个请求合并为一条 SQL 事务,减少 I/O 次数:
INSERT INTO pending_requests (...) VALUES ...; ...; - 内存索引:在
next_retry_at上建立索引,快速查询待重试请求:CREATE INDEX idx_next_retry ON pending_requests(next_retry_at); - 限制并发重试数:同一时刻只允许最多3个重试任务,防止 CPU 和网络线程过载。
- 队列快照:应用进入后台时,将内存中待重试列表持久化为 JSON 文件,恢复时比对 SQLite 消除冗余。
- 幂等键清理:使用定时任务(如每10分钟)删除
created_at超过7天的幂等键,减少存储占用。
生产级建议:如果跨设备同步,推荐使用 CRDT 风格的请求ID(如
device_id + timestamp + sequence)保证全局唯一性,而非纯随机 UUID,以支持离线冲突合并。4.10 部署检查清单
- 确认 SQLite 版本 >= 3.35(支持
UPSERT语法简化幂等键插入)。 - 检查应用权限:Android 需
android:usesCleartextTraffic或 Network Security Config;iOS 需NSAppTransportSecurity漏洞。 - 集成网络状态监听:Android 使用
ConnectivityManager.registerNetworkCallback,iOS 使用NWPathMonitor,Web 使用window.addEventListener('online', ...)。 - 配置最大重试次数和队列上限:在环境配置文件中统一管理,避免硬编码。