一、为什么需要跨设备数据同步
想象一下这样的场景:你正在用手机记录今天的开支,晚上回到家想用电脑继续整理账单,却发现数据根本没同步过来。这种多设备间的数据不一致问题,在移动互联网时代越来越常见。
SQLite作为轻量级数据库,被广泛应用在移动端和桌面端。但它本身只是个单机数据库,没有内置的同步功能。当我们需要在多个设备间保持数据一致时,就得自己动手实现同步方案。
二、SQLite同步的核心挑战
实现SQLite跨设备同步,主要面临三个技术难点:
- 冲突解决:当两个设备同时修改同一条记录时,该听谁的?
- 网络不可靠:移动设备经常断网,如何保证数据最终一致?
- 性能考量:同步过程不能影响主线程,尤其是移动端要保持流畅体验。
下面我们用一个实际的例子来演示解决方案。技术栈选择Node.js + SQLite,因为这个组合在服务端和客户端都能运行,非常适合演示跨平台场景。
三、基于时间戳的同步方案实现
这里介绍一种简单可靠的同步策略:时间戳标记法。每个记录都带有两个时间戳:
// Node.js + SQLite示例
const sqlite3 = require('sqlite3').verbose();
// 创建带有时间戳的表
const db = new sqlite3.Database(':memory:');
db.serialize(() => {
db.run(`
CREATE TABLE expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
amount REAL NOT NULL,
category TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
sync_flag INTEGER DEFAULT 0 // 0-未同步 1-已同步
)
`);
// 添加触发器自动更新updated_at
db.run(`
CREATE TRIGGER update_timestamp
AFTER UPDATE ON expenses
FOR EACH ROW
BEGIN
UPDATE expenses SET updated_at = CURRENT_TIMESTAMP WHERE id = OLD.id;
END
`);
});
同步逻辑的关键代码:
// 同步函数示例
async function syncData(lastSyncTime) {
return new Promise((resolve, reject) => {
// 获取需要同步的数据
db.all(
`SELECT * FROM expenses WHERE updated_at > ? OR sync_flag = 0`,
[lastSyncTime],
(err, localChanges) => {
if (err) return reject(err);
// 这里应该是发送到服务器的网络请求
console.log('需要上传的变更:', localChanges);
// 模拟服务器返回远程变更
const remoteChanges = [
{id: 5, amount: 99.9, category: '餐饮', updated_at: '2023-05-20 15:30:00'}
];
// 处理冲突
remoteChanges.forEach(remote => {
db.get(
`SELECT * FROM expenses WHERE id = ?`,
[remote.id],
(err, local) => {
if (!local || new Date(remote.updated_at) > new Date(local.updated_at)) {
// 远程数据更新则覆盖本地
db.run(
`INSERT OR REPLACE INTO expenses VALUES (?, ?, ?, ?, ?, 1)`,
[remote.id, remote.amount, remote.category, remote.created_at, remote.updated_at]
);
}
}
);
});
// 更新已同步标记
db.run(
`UPDATE expenses SET sync_flag = 1 WHERE updated_at <= ?`,
[new Date().toISOString()]
);
resolve();
}
);
});
}
四、进阶方案:操作日志同步
对于更复杂的场景,可以考虑**操作日志(Operation Log)**方案。每次数据变更都记录操作指令,通过重放指令实现同步:
// 操作日志表
db.run(`
CREATE TABLE IF NOT EXISTS operation_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_name TEXT NOT NULL,
operation_type TEXT CHECK(operation_type IN ('INSERT', 'UPDATE', 'DELETE')),
record_id INTEGER,
operation_content TEXT, // JSON格式记录变更内容
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// 示例:记录插入操作
function logInsert(table, id, content) {
db.run(
`INSERT INTO operation_log (table_name, operation_type, record_id, operation_content)
VALUES (?, 'INSERT', ?, ?)`,
[table, id, JSON.stringify(content)]
);
}
五、技术选型与注意事项
适用场景
- 个人记账/笔记类应用
- 小型团队的任务管理系统
- 物联网设备数据采集
方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 时间戳 | 实现简单 | 冲突解决不够精细 |
| 操作日志 | 可追溯所有变更 | 存储开销较大 |
注意事项
- 时区问题:所有设备必须使用UTC时间
- 数据加密:敏感数据在传输时需要TLS加密
- 性能优化:首次同步大数据量时分批传输
六、完整实现示例
下面给出一个更完整的同步服务示例,包含客户端和服务端代码:
// 服务端代码 (Node.js)
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
// 模拟服务端数据库
let serverData = {
expenses: [
{id: 1, amount: 100, category: '交通', updated_at: '2023-05-18T10:00:00Z'}
]
};
// 同步接口
app.post('/sync', (req, res) => {
const {lastSyncTime, changes} = req.body;
// 1. 接收客户端变更
changes.forEach(change => {
const existing = serverData.expenses.find(x => x.id === change.id);
if (!existing || new Date(change.updated_at) > new Date(existing.updated_at)) {
serverData.expenses = serverData.expenses.filter(x => x.id !== change.id);
serverData.expenses.push(change);
}
});
// 2. 返回服务端变更
const serverChanges = serverData.expenses.filter(
x => new Date(x.updated_at) > new Date(lastSyncTime)
);
res.json({changes: serverChanges, newSyncTime: new Date().toISOString()});
});
app.listen(3000);
// 客户端代码
async function fullSync() {
const lastSyncTime = localStorage.getItem('lastSync') || '1970-01-01T00:00:00Z';
// 获取本地变更
const localChanges = await new Promise(resolve => {
db.all(
`SELECT * FROM expenses WHERE updated_at > ? OR sync_flag = 0`,
[lastSyncTime],
(err, rows) => resolve(rows)
);
});
// 发送到服务端
const response = await fetch('http://localhost:3000/sync', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
lastSyncTime,
changes: localChanges
})
});
const {changes: serverChanges, newSyncTime} = await response.json();
// 应用服务端变更
await applyRemoteChanges(serverChanges);
// 更新同步时间
localStorage.setItem('lastSync', newSyncTime);
}
async function applyRemoteChanges(changes) {
changes.forEach(item => {
db.run(
`INSERT OR REPLACE INTO expenses VALUES (?, ?, ?, ?, ?, 1)`,
[item.id, item.amount, item.category, item.created_at, item.updated_at]
);
});
}
七、总结与展望
实现SQLite的跨设备同步,关键在于选择适合业务场景的同步策略。对于个人开发者,时间戳方案简单易实现;而企业级应用可能需要更复杂的操作日志方案。
未来可以考虑:
- 集成WebSocket实现实时同步
- 使用CRDT数据结构解决复杂冲突
- 结合IndexedDB实现浏览器端存储
无论选择哪种方案,都要记得做好数据备份。同步过程中的错误处理也很重要,建议实现断点续传功能,避免网络中断导致重新传输大量数据。
评论