修复 Claude Code 的并发会话问题:使用 SQLite WAL 模式实现 Memory MCP

发布: (2026年1月1日 GMT+8 16:55)
8 min read
原文: Dev.to

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

配置提示

VariableRecommendation
MEMORY_DB_PATH每个项目的数据库,例如 ./.claude/memory.db,更易管理。
Global sharing如需所有项目共用一个数据库,可设为 ~/memory.db(或类似路径)。

功能比较

功能官方 (JSONL)此实现 (SQLite + WAL)
并发访问❌ 不支持✅ 支持 (WAL)
事务❌ 无✅ ACID 保证
搜索速度缓慢 (线性)快速 (已索引)

数据完整性

等级描述
(外键)

Crash Recovery

FeatureStatus
手动修复
自动(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 始终欢迎。

Back to Blog

相关文章

阅读更多 »

SQLite 中缓存的效率

SQLite 中 Cache 的效率!封面图片用于 “SQLite 中 Cache 的效率” https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=aut...