概览
Apache CouchDB 是一个面向文档的 NoSQL 数据库,使用 JSON 存储数据并通过 HTTP REST API 进行所有操作。核心特性包括多主复制(支持离线优先应用)、变更通知(_changes feed)、MapReduce 视图。特别适合需要多端同步的 Web 和移动应用(与 PouchDB 配合使用)。用 Erlang 编写,具有出色的容错性和并发能力。
Apache CouchDB 是一个面向文档的 NoSQL 数据库,使用 JSON 存储数据并通过 HTTP REST API 进行所有操作。核心特性包括多主复制(支持离线优先应用)、变更通知(_changes feed)、MapReduce 视图。特别适合需要多端同步的 Web 和移动应用(与 PouchDB 配合使用)。用 Erlang 编写,具有出色的容错性和并发能力。
| 要求 | 说明 |
|---|---|
| 操作系统 | Linux、macOS、Windows(Docker 推荐) |
| 内存 | 最低 512 MB,推荐 2 GB+ |
| 磁盘 | 取决于数据量,SSD 更佳 |
| 端口 | 5984(HTTP API)、4369(Erlang EPMD)、9100(集群) |
# 单节点启动
docker run -d --name couchdb \
-p 5984:5984 \
-e COUCHDB_USER=admin \
-e COUCHDB_PASSWORD=password \
-v couchdb_data:/opt/couchdb/data \
couchdb:latest
# 验证
curl http://admin:password@localhost:5984/
# 输出: {"couchdb":"Welcome","version":"3.x.x",...}
# 添加仓库
echo "deb https://apache.jfrog.io/artifactory/couchdb-deb/ $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/couchdb.list
curl -L https://couchdb.apache.org/repo/bintray-pubkey.asc | sudo apt-key add -
sudo apt update
sudo apt install -y couchdb
# 安装过程中会提示:
# - 选择 standalone 或 clustered 模式
# - 设置 admin 密码
# - 绑定地址(0.0.0.0 或 127.0.0.1)
# 验证
curl http://localhost:5984/
brew install couchdb
brew services start couchdb
wget https://archive.apache.org/dist/couchdb/source/3.3.3/apache-couchdb-3.3.3.tar.gz
tar -xzf apache-couchdb-3.3.3.tar.gz
cd apache-couchdb-3.3.3
./configure
make release
cd rel/couchdb
bin/couchdb
检查绑定地址:
# 编辑 local.ini
sudo nano /opt/couchdb/etc/local.ini
# 修改 bind_address
[chttpd]
bind_address = 0.0.0.0
# 重启
sudo systemctl restart couchdb
集群节点间需要相同的 Erlang cookie:
echo "my-secret-cookie" > /opt/couchdb/.erlang.cookie
chmod 600 /opt/couchdb/.erlang.cookie
# 确保挂载卷有正确权限
sudo chown -R 5984:5984 /path/to/couchdb_data
Fauxton 地址为 http://localhost:5984/_utils/,使用安装时设置的 admin 密码登录。若忘记密码,可在 local.ini 的 [admins] 段重置。
通过 CouchDB 的 HTTP REST API 创建数据库、插入 JSON 文档、查询和更新,理解其纯 HTTP 操作范式。
# 1. 创建数据库
curl -X PUT http://admin:password@localhost:5984/library
# 2. 插入文档(POST 自动生成 _id)
curl -X POST http://admin:password@localhost:5984/library \
-H "Content-Type: application/json" \
-d '{
"title": "深入理解计算机系统",
"author": "Randal E. Bryant",
"year": 2015,
"pages": 1080,
"tags": ["cs", "systems", "textbook"]
}'
# 返回: {"ok":true,"id":"<uuid>","rev":"1-xxx"}
# 3. 指定 _id 插入
curl -X PUT http://admin:password@localhost:5984/library/book_001 \
-H "Content-Type: application/json" \
-d '{
"title": "算法导论",
"author": "Thomas H. Cormen",
"year": 2009,
"pages": 1312,
"tags": ["algorithms", "textbook"]
}'
# 返回: {"ok":true,"id":"book_001","rev":"1-xxx"}
# 4. 获取文档
curl http://admin:password@localhost:5984/library/book_001
# 5. 更新文档(必须带当前 _rev)
curl -X PUT http://admin:password@localhost:5984/library/book_001 \
-H "Content-Type: application/json" \
-d '{
"_rev": "1-xxx",
"title": "算法导论(第3版)",
"author": "Thomas H. Cormen",
"year": 2009,
"pages": 1312,
"tags": ["algorithms", "textbook", "bestseller"]
}'
# 6. 删除文档
curl -X DELETE http://admin:password@localhost:5984/library/book_001?rev=<当前_rev>
# 7. 查询全部文档
curl http://admin:password@localhost:5984/library/_all_docs?include_docs=true
# pip install couchdb
import couchdb
# 连接服务器
server = couchdb.Server('http://admin:password@localhost:5984/')
# 创建数据库
db = server.create('library')
# 插入文档
doc_id, rev = db.save({
'title': '深入理解计算机系统',
'author': 'Randal E. Bryant',
'year': 2015,
'pages': 1080,
'tags': ['cs', 'systems', 'textbook']
})
print(f"保存成功: id={doc_id}, rev={rev}")
# 查询
doc = db[doc_id]
print(f"书名: {doc['title']}")
# 使用 Mango 查询(需要先创建索引)
db.create_index(['author'])
results = db.find({'selector': {'author': 'Randal E. Bryant'}})
for row in results:
print(row)
# 更新
doc['pages'] = 1100
db.save(doc)
# 删除
db.delete(doc)
// POST 创建文档返回
{"ok":true,"id":"abc123...","rev":"1-xxx"}
// GET 获取文档返回
{
"_id": "book_001",
"_rev": "2-yyy",
"title": "算法导论(第3版)",
"author": "Thomas H. Cormen",
"year": 2009,
"pages": 1312,
"tags": ["algorithms", "textbook", "bestseller"]
}
_rev(乐观锁机制)_all_docs 返回所有文档,include_docs=true 包含完整内容传统 Web 应用假设始终在线。但移动端(快递员扫码、野外采集)需要离线可用 → 联网同步。CouchDB 的多主复制天然支持这种模式:
浏览器 PouchDB ────sync───▶ CouchDB Server ────sync───▶ 另一 CouchDB
(离线草稿) (线上汇总) (灾备/分析)
| 特性 | 说明 |
|---|---|
| MVCC | 多版本并发控制,读不阻塞写 |
| _changes | 变更流,实时推送所有修改 |
| _rev | 文档版本号,乐观锁冲突检测 |
| Replication | 多主双向同步,支持过滤 |
| 附件 | 文档可直接关联二进制文件 |
构建一个笔记应用:用户离线时可添加/编辑笔记,联网后自动同步到服务器。
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
<!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' });
// 合并逻辑...
}
}
}
_rev 机制如何检测冲突?_changes feed 与 MQ 消息队列相比,适用场景有何不同?