拓冰建站拓冰建站
首页 / 资讯中心 / 正文

数据库专题21:MongoDB 保存文章版本——从“覆盖更新”到可恢复历史

数据库专题21MongoDB 保存文章版本——从“覆盖更新”到可恢复历史在博客项目中PostgreSQL 的articles表只保存当前正文。用户连续修改文章时旧内容会被覆盖编辑器也就无法实现“查看历史”和“恢复到上一版”。这一篇把 MongoDB 放到一个明确的位置它不是替代主库而是保存每次发布后的完整快照。读者会从文档模型、索引、幂等写入和失败补偿四个角度做出一个可以运行的版本服务。上一篇练习讲解缓存失败时为什么要回源上一章要求实现随机 TTL 和 Redis 超时降级。一个可接受的答案是数据库事务提交成功后删除缓存删除超时写入重试队列读取时 Redis 超时则在 200ms 内回源 PostgreSQL。这里有一个重要前提——缓存只是加速层不能作为历史正文的唯一来源。MongoDB 版本集合也遵循同样的原则主库提交失败时不能写“成功版本”Mongo 写入失败则由 outbox 重试。1. 先画清楚数据边界一次发布包含两个事实articles的当前状态以及article_versions的历史快照。PostgreSQL 负责作者权限、文章状态和版本号的递增MongoDB 负责保存标题、正文和渲染元数据。这样设计的原因是权限判断必须和业务主库处于同一个事务中而版本文档适合整体读取MongoDB 的文档结构比多张历史表更直观。编辑请求 │ ├─ PostgreSQL 事务校验作者、更新正文、version 1 │ │ commit │ ▼ └─ outbox 事件 ─────── MongoDB article_versions 失败可重试不反向修改已经提交的主库PostgreSQL 中至少要有(article_id, version)唯一约束。它保证两个 worker 不会生成相同版本MongoDB 中使用相同的复合唯一索引保证重试不会重复插入。2. 启动 MongoDB 并建立集合在projects/database-platform目录执行docker compose up-d mongo docker compose exec mongo mongosh--quiet--evaldb.runCommand({ping:1})预期输出包含{ ok: 1 }。第一次初始化时执行下面的脚本。索引不是“可有可无的优化”而是把业务不变量交给数据库守护。// mongo-init.jsmongosh 中执行use blog db.article_versions.createIndex({article_id:1,version:1},{unique:true,name:uk_article_version})db.article_versions.createIndex({article_id:1,changed_at:-1},{name:ix_article_changed_at})db.article_versions.createIndex({search.title:text,search.body:text},{name:ix_version_search})uk_article_version用于幂等ix_article_changed_at支持“最近版本”列表。全文索引只用于后台搜索生产环境中文分词要求时应换 Elasticsearch不能假设 MongoDB text index 能解决所有中文搜索问题。3. 文档结构让一次读取拿到完整快照{_id:ObjectId(66a000000000000000000042),article_id:42,version:7,author_id:3,title:数据库中的事务,body:这一版正文……,summary:解释提交、回滚与重试,changed_by:3,changed_at:ISODate(2025-01-01T10:00:00Z),reason:补充隔离级别示例,search:{title:数据库中的事务,body:提交 回滚 隔离},source_event_id:article-42-version-7}这里没有把正文拆成段落集合因为版本详情的主要访问模式是“一次打开整篇”。source_event_id是事件的业务幂等键时间使用 UTC展示时再转为用户时区。不要把 PostgreSQL 的外键硬搬到 MongoDBauthor_id只作为引用最终权限仍在 PostgreSQL 检查。4. 用 Python 写入版本并处理重复投递下面的代码可以直接在 Python 3.11 环境运行先安装pymongo。DuplicateKeyError不应该被简单吞掉需要读取已有文档并比较内容确认是同一个事件重试而不是两个不同正文错误地使用了同一个版本号。fromdatetimeimportdatetime,timezonefrompymongoimportMongoClientfrompymongo.errorsimportDuplicateKeyError clientMongoClient(mongodb://localhost:27017,serverSelectionTimeoutMS1500)versionsclient.blog.article_versionsdefsave_version(event:dict)-str:保存发布快照。 event_id 由主库文章 id 和版本号组成因此 worker 重试时会命中唯一索引。 发生重复键时比较正文摘要防止把真正的数据冲突误判为成功。 doc{article_id:event[article_id],version:event[version],author_id:event[author_id],title:event[title],body:event[body],summary:event.get(summary,),changed_by:event[changed_by],changed_at:event.get(changed_at,datetime.now(timezone.utc)),reason:event.get(reason,编辑正文),source_event_id:farticle-{event[article_id]}-version-{event[version]},}try:versions.insert_one(doc)returncreatedexceptDuplicateKeyError:oldversions.find_one({article_id:doc[article_id],version:doc[version]},{body:1,title:1},)ifoldandold[title]doc[title]andold[body]doc[body]:returnalready_exists# 同一事件重试返回幂等成功raiseRuntimeError(版本号冲突主库事件内容与 MongoDB 已有快照不一致)event{article_id:42,version:7,author_id:3,changed_by:3,title:数据库中的事务,body:这一版正文,}print(save_version(event))运行两次第一次输出created第二次输出already_exists。如果第二次把body改成别的内容程序应抛出版本冲突这比悄悄覆盖历史更安全。5. 查询、恢复与容量控制deflist_versions(article_id:int,page:int1,size:int20)-list[dict]:按版本倒序分页限制 size 避免一次返回几百 MB 正文。sizemax(1,min(size,50))cursor(versions.find({article_id:article_id}).sort(version,-1).skip((page-1)*size).limit(size))returnlist(cursor)defget_version(article_id:int,version:int)-dict|None:returnversions.find_one({article_id:article_id,version:version})恢复操作不能直接由 MongoDB 反写 PostgreSQL。正确流程是读取快照 → 在 PostgreSQL 事务内再次检查作者权限和当前版本 → 生成新的版本号并更新正文 → 提交后投递新的article.published事件。这样“恢复到第 3 版”最终会产生第 8 版历史仍然是单调追加的不会破坏审计链。验收记录docker compose exec mongo mongosh --quiet --eval db.article_versions.countDocuments({article_id:42}) 1 Python 第一次 save_version - created Python 第二次 save_version - already_exists 修改同版本正文 - RuntimeError: 版本号冲突若看到ServerSelectionTimeoutError先检查docker compose ps和端口 27017若插入中文后搜索结果为空不要急着改代码先确认 text index 的分词能力是否满足需求。课后练习实现GET /articles/{id}/versions/{version}和“恢复版本”服务恢复前校验作者恢复后只新增版本不覆盖旧文档为同一事件重复投递、不同正文冲突各写一个测试。下一篇会在这套版本集合上增加不可篡改的审计日志。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门