离线优先应用架构核心概念:本地缓存、队列、同步引擎、冲突检测
离线优先应用的核心在于 本地缓存、队列、同步引擎 和 冲突检测 四个支柱。它们共同构成了一个即便在网络不稳定或完全离线时仍能正常工作的数据闭环。理解每个模块的职责、实现方式以及它们之间的协作关系,是后续模拟故障测试的前提。
1. 本地缓存 —— 离线读取的基石
本地缓存负责在设备上持久化最近或全部所需的数据,使得应用在离线状态下仍能快速响应读取操作。常见的实现方案包括:
- 移动端:SQLite、Realm、Room(Android)、Core Data(iOS)
- Web 端:IndexedDB、LocalStorage(容量受限)、Cache API(配合 Service Worker)
- 桌面端:SQLite、LevelDB、RocksDB
以下示例展示了一个使用 SQLite 创建缓存表的 DDL 语句(适用于移动端或桌面端):
CREATE TABLE IF NOT EXISTS cache_products (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
price REAL NOT NULL,
category TEXT,
last_synced INTEGER DEFAULT 0
);
CREATE INDEX idx_cache_products_category ON cache_products(category);
缓存策略通常采用“先读缓存,失败后读网络”的退化方案,同时设置过期时间(TTL)避免数据过时。
2. 队列 —— 离线写入的缓冲
当用户发起写操作(新增、修改、删除)而网络不可用时,操作不能立即发往服务器,而是被存入一个 本地队列。队列采用 FIFO(先进先出)原则,并记录操作类型、请求体、时间戳和重试次数。同步时,队列按顺序逐条执行。
一个典型的队列数据结构(JSON 表示):
{
"queue": [
{
"id": "op_001",
"action": "CREATE",
"resource": "order",
"payload": { "userId": 123, "amount": 29.99 },
"timestamp": 1689900001000,
"retryCount": 0,
"status": "pending"
}
]
}
处理队列时需注意 幂等性:服务器应能识别重复提交,避免重复扣款或生成重复记录。常用手法包括在请求中加入唯一 idempotency_key。
3. 同步引擎 —— 数据流的中枢
同步引擎负责在网络可用时将本地队列中的操作提交到远程服务器,并从服务器拉取最新数据更新本地缓存。它是离线优先架构的“大脑”,通常运行在后台,并处理:
- 网络状态监听:从离线切回在线时自动触发同步。
- 增量同步:仅同步变更数据,减少带宽消耗。
- 同步策略:推拉结合,先推后拉或并行执行。
以下是一个简单的同步引擎伪代码片段:
async function runSync() {
// 1. 获取本地队列中待同步的操作
const operations = await localQueue.getPending();
// 2. 逐个发送到远程 API
for (const op of operations) {
try {
const resp = await api.sendOperation(op);
// 标记成功,更新本地缓存
await localCache.applyRemoteResult(resp);
await localQueue.markComplete(op.id);
} catch (error) {
// 网络失败或超时,保留在队列中,等待下次同步
if (error.isNetworkError) break; // 停止同步,等待下次触发
// 业务错误(如冲突)则转入冲突处理
await conflictDetector.handle(op, error);
}
}
// 3. 拉取服务器最新数据
const remoteChanges = await api.pullChanges();
await localCache.mergeRemoteChanges(remoteChanges);
}
4. 冲突检测 —— 数据一致的保障
当同一份数据在本地和远程都被修改时,同步过程会引发冲突。冲突检测模块评估差异并决定如何合并或提示用户。常见冲突解决策略包括:
| 策略 | 描述 | 适用场景 |
|---|---|---|
| 最后写入者获胜(LWW) | 以时间戳较新的版本为准 | 日志、评论等不可变或可覆盖数据 |
| 自动合并(CRDT) | 无冲突的复制数据类型,自动合并 | 计数器、集合、文本协同编辑 |
| 版本向量 | 记录每个节点的版本号,冲突时由用户选择 | 文档管理、CRM 字段冲突 |
| 领域特定规则 | 按业务逻辑合并(如金额累加) | 财务、库存系统 |
冲突检测必须在同步引擎内部集成。典型的实现是在推送时附带本地版本号,服务器返回 Conflict 响应后触发合并流程。
故障排查与常见错误
在实现四大核心模块时,易出现以下问题。下表可作为排查参考:
| 症状 | 可能原因 | 检查点 | 解决方案 |
|---|---|---|---|
| 离线写入丢失 | 队列未持久化或崩溃后未恢复 | 队列存储使用 SQLite / IndexedDB 事务了吗? | 确保每个写操作先入队再返回,使用 WAL 模式提高持久性。 |
| 同步后出现重复数据 | 队列操作不是幂等的 | 服务端是否检查了 idempotency_key? | 生成全局唯一操作 ID,服务端去重。 |
| 缓存数据长期陈旧 | 同步引擎拉取增量逻辑错误 | 检查拉取的 last_synced 时间戳是否更新 | 使用游标分页 + 本地最大时间戳的增量拉取。 |
| 冲突导致数据丢失 | 使用了错误的冲突策略或未处理冲突 | 版本号是否递增?服务器返回 409 时是否进入冲突处理? | 实现多版本并发控制(MVCC)或引入 CRDT 库。 |
| 同步时 UI 卡死 | 同步引擎在主线程执行网络请求 | 同步是否在子线程/Web Worker 中运行? | 将同步引擎移至后台线程,使用异步任务队列。 |
fake-online / fake-offline 开关模拟网络切换,可快速定位缓存与队列的交互异常。
四个模块必须协同工作——本地缓存和队列是“数据仓库”,同步引擎是“交通调度”,冲突检测是“规则法官”。测试离线优先应用时,应分别针对每个模块独立编写单元测试,再组合成集成测试(如:离线写入 + 恢复网络 → 数据一致)。