文档
Apache CouchDB 从零到实战:离线优先应用
1. 背景与概念
1.1 为什么选择 CouchDB?
传统 Web 应用假设始终在线。但移动端(快递员扫码、野外采集)需要离线可用 → 联网同步。CouchDB 的多主复制天然支持这种模式:
浏览器 PouchDB ────sync───▶ CouchDB Server ────sync───▶ 另一 CouchDB
(离线草稿) (线上汇总) (灾备/分析)
1.2 核心特性
| 特性 | 说明 |
|---|---|
| MVCC | 多版本并发控制,读不阻塞写 |
| _changes | 变更流,实时推送所有修改 |
| _rev | 文档版本号,乐观锁冲突检测 |
| Replication | 多主双向同步,支持过滤 |
| 附件 | 文档可直接关联二进制文件 |
2. 分步实战:离线笔记应用
场景
构建一个笔记应用:用户离线时可添加/编辑笔记,联网后自动同步到服务器。
步骤一:搭建后端 CouchDB
docker run -d -p 5984:5984 \
-e COUCHDB_USER=admin \
-e COUCHDB_PASSWORD=secret \
-v couch:/opt/couchdb/data \
couchdb:latest
# 创建数据库
curl -X PUT http://admin:secret@localhost:5984/notes
步骤二:前端 PouchDB 初始化
<!DOCTYPE html>
<html>
<head>
<title>离线笔记</title>
<script src="https://cdn.jsdelivr.net/npm/pouchdb@8.0.0/dist/pouchdb.min.js"></script>
</head>
<body>
<textarea id="input" placeholder="写点什么..."></textarea>
<button onclick="saveNote()">保存</button>
<ul id="notes"></ul>
<script>
// 初始化本地数据库(IndexedDB 存储)
const localDB = new PouchDB('notes_local');
const remoteDB = new PouchDB('http://admin:secret@localhost:5984/notes');
// 双向同步
const sync = localDB.sync(remoteDB, {
live: true, // 持续监听变更
retry: true // 断线重连
});
sync.on('change', (info) => {
console.log('同步变更:', info.direction);
loadNotes(); // 刷新 UI
});
sync.on('error', (err) => {
console.log('同步错误(可能离线):', err.message);
});
// 保存笔记
function saveNote() {
const text = document.getElementById('input').value;
if (!text.trim()) return;
localDB.put({
_id: new Date().toISOString(),
content: text,
createdAt: new Date().toISOString()
}).then(() => {
document.getElementById('input').value = '';
loadNotes();
console.log('已保存到本地,联网后自动同步');
});
}
// 加载所有笔记
function loadNotes() {
localDB.allDocs({ include_docs: true, descending: true })
.then(result => {
const list = document.getElementById('notes');
list.innerHTML = '';
result.rows.forEach(row => {
const li = document.createElement('li');
li.textContent = row.doc.content;
list.appendChild(li);
});
});
}
loadNotes();
</script>
</body>
</html>
步骤三:冲突处理
当离线编辑了同一条笔记时会产生冲突。CouchDB 保留所有冲突版本:
// 检测并处理冲突
async function resolveConflicts() {
const result = await localDB.allDocs({ include_docs: true, conflicts: true });
for (const row of result.rows) {
if (row.doc._conflicts) {
console.log('冲突文档:', row.doc._id);
// 策略:保留最新版本,删除旧版本
const allRevs = await localDB.get(row.doc._id, { open_revs: 'all' });
// 合并逻辑...
}
}
}
3. 思考题
- PouchDB 和 CouchDB 之间同步时,如果两边都修改了同一文档,
_rev机制如何检测冲突? - 设计一个"只同步自己部门的文档"的过滤复制策略。
- CouchDB 的
_changesfeed 与 MQ 消息队列相比,适用场景有何不同?