Apache CouchDB

技术栈
数据库
nosqldocumenthttp-apireplicationoffline-first

概览

Apache CouchDB 是一个面向文档的 NoSQL 数据库,使用 JSON 存储数据并通过 HTTP REST API 进行所有操作。核心特性包括多主复制(支持离线优先应用)、变更通知(_changes feed)、MapReduce 视图。特别适合需要多端同步的 Web 和移动应用(与 PouchDB 配合使用)。用 Erlang 编写,具有出色的容错性和并发能力。

安装

Apache CouchDB 安装指南

1. 环境准备

要求 说明
操作系统 Linux、macOS、Windows(Docker 推荐)
内存 最低 512 MB,推荐 2 GB+
磁盘 取决于数据量,SSD 更佳
端口 5984(HTTP API)、4369(Erlang EPMD)、9100(集群)

2. 安装命令

Docker(最简单)

# 单节点启动
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",...}

Ubuntu/Debian

# 添加仓库
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/

macOS

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

3. 常见安装问题

Q1: 安装后 5984 无法访问

检查绑定地址:

# 编辑 local.ini
sudo nano /opt/couchdb/etc/local.ini

# 修改 bind_address
[chttpd]
bind_address = 0.0.0.0

# 重启
sudo systemctl restart couchdb

Q2: "Erlang cookie mismatch"

集群节点间需要相同的 Erlang cookie:

echo "my-secret-cookie" >; /opt/couchdb/.erlang.cookie
chmod 600 /opt/couchdb/.erlang.cookie

Q3: Docker 容器权限错误

# 确保挂载卷有正确权限
sudo chown -R 5984:5984 /path/to/couchdb_data

Q4: Fauxton 管理界面无法登录

Fauxton 地址为 http://localhost:5984/_utils/,使用安装时设置的 admin 密码登录。若忘记密码,可在 local.ini[admins] 段重置。

示例

CouchDB Hello World:REST API 操作数据库

目标

通过 CouchDB 的 HTTP REST API 创建数据库、插入 JSON 文档、查询和更新,理解其纯 HTTP 操作范式。

完整代码

使用 curl(无需任何驱动)

# 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

Python 版本

# 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"]
}

关键点

  • CouchDB 一切操作皆 HTTP,无需 SQL
  • 每次更新需要提供 _rev(乐观锁机制)
  • _all_docs 返回所有文档,include_docs=true 包含完整内容

教程

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. 思考题

  1. PouchDB 和 CouchDB 之间同步时,如果两边都修改了同一文档,_rev 机制如何检测冲突?
  2. 设计一个"只同步自己部门的文档"的过滤复制策略。
  3. CouchDB 的 _changes feed 与 MQ 消息队列相比,适用场景有何不同?

参考资料

  1. [1] J. Chris Anderson, Jan Lehnardt, Noah Slater. CouchDB: The Definitive Guide. 2010.
  2. [2] Apache Software Foundation. Apache CouchDB 官方文档. 2024.
  3. [3] PouchDB Team. PouchDB 官方指南(浏览器端 CouchDB). 2024.