修复 Claude Code 的并发会话问题:使用 SQLite WAL 模式实现 Memory MCP
It looks like only the source line was provided. Could you please share the rest of the text you’d like translated? Once I have the full content, I’ll translate it into Simplified Chinese while preserving the formatting and code blocks as requested.
问题
你是否在运行多个 Claude Code 会话时看到过此错误?
Error: database is locked
是的,我的第一反应也是:“等等,真的?”
Memory MCP 是一个在 Claude Code 会话之间共享知识的极其强大的工具,但官方实现不支持并发访问。因此,我使用 SQLite 的 Write‑Ahead Logging (WAL) 模式构建了一个版本,以彻底解决此问题。
谁适合阅读?
- 在日常工作流中使用 Claude Code 的开发者
- 对模型上下文协议(MCP)感兴趣的任何人
- 希望并行运行多个 AI 会话的团队
- 寻找实用 SQLite 实现模式的开发者
为什么官方 Memory MCP 不足
官方 Memory MCP(@modelcontextprotocol/server-memory)将知识图存储在 JSONL 文件中。这种做法存在一些严重的局限性:
// Simplified view of the official implementation
const data = await fs.readFile('memory.jsonl', 'utf-8');
// ... process data ...
await fs.writeFile('memory.jsonl', newData);
- 没有文件锁 – 多个会话的并发写入可能会损坏数据。
- 线性搜索 – 实现会把所有数据读取到内存中;随着图的增大,查询会变得更慢。
- 没有原子更新 – 部分失败会导致文件处于不一致状态,这对生产环境来说是致命的。
我的解决方案:基于 SQLite 的内存 MCP
SQLite 的 WAL 模式确实改变了游戏规则:
| 特性 | WAL 为您提供的功能 |
|---|---|
| 并发读/写 | 多个读取者 + 一个写入者可以同时访问数据库 |
| 崩溃恢复 | 即使进程在写入过程中崩溃,也能保证数据完整性 |
| 更快的写入 | 基于日志的方法优于传统的回滚日志 |
这正是那种“简单”特性,却能改变一切。
import Database from 'better-sqlite3';
import path from 'path';
import fs from 'fs';
export class KnowledgeGraphStore {
private db: Database.Database;
constructor(dbPath: string) {
// Create directory if it doesn’t exist
const dir = path.dirname(dbPath);
if (dir && dir !== '.') {
fs.mkdirSync(dir, { recursive: true });
}
this.db = new Database(dbPath);
// Enable WAL mode for concurrent access
this.db.pragma('journal_mode = WAL');
// Set busy timeout to wait 5 seconds on lock contention
this.db.pragma('busy_timeout = 5000');
this.initSchema();
}
}
关键点
journal_mode = WAL– 启用并发读/写访问。busy_timeout = 5000– 在抛出锁错误前等待 5 秒。
如果没有 busy timeout,您会立即收到锁错误。相信我,务必设置它。
架构设计
我使用三张表来高效管理知识图谱:
private initSchema(): void {
this.db.exec(`
-- Entities (concepts) management
CREATE TABLE IF NOT EXISTS entities (
name TEXT PRIMARY KEY,
entity_type TEXT NOT NULL
);
-- Observations and facts about entities
CREATE TABLE IF NOT EXISTS observations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
entity_name TEXT NOT NULL,
content TEXT NOT NULL,
FOREIGN KEY (entity_name) REFERENCES entities(name) ON DELETE CASCADE,
UNIQUE(entity_name, content) -- Prevent duplicates
);
-- Relationships between entities
CREATE TABLE IF NOT EXISTS relations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_entity TEXT NOT NULL,
to_entity TEXT NOT NULL,
relation_type TEXT NOT NULL,
FOREIGN KEY (from_entity) REFERENCES entities(name) ON DELETE CASCADE,
FOREIGN KEY (to_entity) REFERENCES entities(name) ON DELETE CASCADE,
UNIQUE(from_entity, to_entity, relation_type) -- Prevent duplicates
);
-- Indexes for performance optimization
CREATE INDEX IF NOT EXISTS idx_observations_entity ON observations(entity_name);
CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity);
CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity);
`);
}
设计原则
| 原则 | 为什么重要 |
|---|---|
| CASCADE | 当实体被删除时自动删除相关数据(避免孤立记录)。 |
| UNIQUE 约束 | 防止出现重复数据。 |
| 索引 | 大幅提升查询性能。这些索引至关重要——没有它们,查询会非常慢。 |
插入数据 – 事务很重要
createEntities(entities: Entity[]): Entity[] {
const insertEntity = this.db.prepare(
'INSERT OR IGNORE INTO entities (name, entity_type) VALUES (?, ?)'
);
const insertObservation = this.db.prepare(
'INSERT OR IGNORE INTO observations (entity_name, content) VALUES (?, ?)'
);
const created: Entity[] = [];
// Process everything in a transaction
const transaction = this.db.transaction((entities: Entity[]) => {
for (const entity of entities) {
insertEntity.run(entity.name, entity.entityType);
for (const obs of entity.observations) {
insertObservation.run(entity.name, obs);
}
created.push(entity);
}
});
transaction(entities);
return created;
}
为什么使用事务?
- 原子性 – 失败的操作不会留下部分更改。
- 一致性 – 多表更新能够保持引用完整性。
- 性能 – 批量提交显著更快。
如果不使用事务,错误时可能会导致数据损坏——我就是吃了这口苦才明白的。
入门
我已将其发布为 npm 包,任何人都可以轻松使用:
npm install @pepk/mcp-memory-sqlite
将服务器添加到你的 ~/.claude.json(或全局配置)中:
{
"mcpServers": {
"memory": {
"command": "npx",
"args": ["@pepk/mcp-memory-sqlite"],
"env": {
"MEMORY_DB_PATH": "./.claude/memory.db"
}
}
}
}
配置提示
| Variable | Recommendation |
|---|---|
MEMORY_DB_PATH | 每个项目的数据库,例如 ./.claude/memory.db,更易管理。 |
| Global sharing | 如需所有项目共用一个数据库,可设为 ~/memory.db(或类似路径)。 |
功能比较
| 功能 | 官方 (JSONL) | 此实现 (SQLite + WAL) |
|---|---|---|
| 并发访问 | ❌ 不支持 | ✅ 支持 (WAL) |
| 事务 | ❌ 无 | ✅ ACID 保证 |
| 搜索速度 | 缓慢 (线性) | 快速 (已索引) |
数据完整性
| 等级 | 描述 |
|---|---|
| 弱 | – |
| 强 | (外键) |
Crash Recovery
| Feature | Status |
|---|---|
| 手动修复 | ❌ |
| 自动(WAL) | ✅ |
差异相当显著。
“database is locked”
如果在启用 WAL 模式 时仍然出现此错误,请尝试以下操作:
// Increase the busy timeout
this.db.pragma('busy_timeout = 10000'); // 10 seconds
如果 5 秒 不够,尝试 10 秒。
运行检查点以截断 WAL 文件
sqlite3 memory.db "PRAGMA wal_checkpoint(TRUNCATE);"
定期运行此操作可保持 WAL 文件大小受控。
内存 MCP 实现(SQLite WAL)
关键要点
- WAL 模式 允许并发读/写访问。
- 事务保证数据一致性。
- 索引提供快速搜索性能。
WAL 模式的并发访问支持是此处的真正突破。现在您可以安全地运行多个会话,而无需担心数据损坏。
npm 包
@pepk/mcp-memory-sqlite
仓库
文档(日文)
- Qiita: 阅读日文版
- Zenn: 阅读 Zenn 版
- note(Story): 开发故事
关于作者
Daichi Kudo
- CEO at Cognisant LLC – 构建人类与 AI 共同创造的未来
- CTO at M16 LLC – AI、创意与工程
如果这能帮助解决您的 Claude Code 并发会话问题,欢迎告诉我!Issues 和 PR 始终欢迎。