一条 git reset --hard,我的数据库空了!
2026 年 7 月 9 日 · 一次惊心动魄的线上事故
事故发生
今天凌晨在检查博客的 RSS 订阅功能时,顺手测了一下 API,结果看到了令人血压飙升的一幕:
// GET /api/articles?page=1&limit=2
{"rows":[],"total":0,"page":1,"limit":2}
// GET /api/moments?limit=2
{"rows":[],"total":0,"page":"2","totalPages":0}
所有数据都没了。
文章、动态、评论——全部返回空数组。赶紧登上服务器查数据库:
$ node -e "import Database from 'better-sqlite3';
const db = new Database('data/blog.db', { readonly: true });
console.log('articles:', db.prepare('SELECT COUNT(*) as c FROM articles').get().c);
console.log('moments:', db.prepare('SELECT COUNT(*) as c FROM moments').get().c);"
articles: 0
moments: 0
数据库文件还在(7.7MB),表结构也都在,但所有业务表的数据——全部清零。只有 experiments(实验功能)和 visitor_logs(访客日志)还有数据。
那一刻的内心:45 篇文章,160+动态。写了两个月,全没了?
排查过程
第一步:检查 PM2 进程状态
$ pm2 list
┌────┬──────────────┬──────────┬──────┬──────────┬──────┐
│ id │ name │ status │ ↺ │ uptime │ cpu │
├────┼──────────────┼──────────┼──────┼──────────┼──────┤
│ 1 │ blog-server │ online │ 47 │ 4m │ 0% │
└────┴──────────────┴──────────┴──────┴──────────┴──────┘
进程是 online,但重启了 47 次。错误日志里全是:
❌ Error: no such column: m.lang
❌ Error: no such column: m.lang
❌ Error: no such column: m.lang
...(重复几十次)
第二步:检查数据库文件
$ ls -la data/
-rw-r--r-- 1 root root 7737344 Jul 9 08:49 blog.db
-rw-r--r-- 1 root root 32768 Jul 9 09:31 blog.db-shm
-rw-r--r-- 1 root root 1178352 Jul 9 09:31 blog.db-wal
-rw-r--r-- 1 root root 7528448 Jul 2 12:25 blog.db.bak.20260702
WAL 文件有 1.1MB,说明可能有未合并的数据。尝试 checkpoint:
$ node -e "db.pragma('wal_checkpoint(TRUNCATE)')"
articles: 0
moments: 0
WAL 合并后数据还是空的。数据确实丢了,不是 WAL 没合并的问题。
第三步:检查备份
幸好服务器上有自动备份脚本,每天凌晨 3 点跑一次:
$ ls /opt/blog/backups/blog_*.db
blog_20260626_150403.db
blog_20260627_030001.db
...
blog_20260709_030001.db ← 今天凌晨的备份
验证备份数据:
$ node -e "const db = new Database('../backups/blog_20260709_030001.db', {readonly:true});
console.log('articles:', db.prepare('SELECT COUNT(*) as c FROM articles').get().c);
console.log('moments:', db.prepare('SELECT COUNT(*) as c FROM moments').get().c);"
articles: 45
moments: 167
comments: 143
备份是完整的!悬着的心放下一半。
数据恢复
立即停服、替换数据库、重启:
$ pm2 stop blog-server
$ cp data/blog.db data/blog.db.bak.20260709_empty # 保留现场
$ cp ../backups/blog_20260709_030001.db data/blog.db
$ rm -f data/blog.db-wal data/blog.db-shm # 删除旧 WAL
$ pm2 start blog-server
验证恢复结果:
// GET /api/articles?page=1&limit=1
{"rows":[{"id":45,"title":"博客 V2.0 正式发布:我给这个小破站做了一次大手术"...}]}
// GET /api/moments?limit=1
{"rows":[{"id":209,"content":"都来测一个😊..."}]}
✅ 数据恢复成功
定位根因
数据虽然恢复了,但如果不找到根因,下次部署还会丢。于是开始逆向排查。
关键线索:git 跟踪了不该跟踪的文件
$ git ls-files | grep 'data/.*\.db'
server/data/blog.db-shm
server/data/blog.db-wal
找到了! blog.db-wal 和 blog.db-shm 竟然被 git 跟踪了!
再看 .gitignore:
# .gitignore
*.db
*.sqlite
*.sqlite3
Thumbs.db
只忽略了 *.db,没有忽略 *.db-wal 和 *.db-shm!
事故还原
整个事故的链条非常清晰:
1. 某次提交时,
blog.db-wal和blog.db-shm被git add加入了版本控制2. 之后每次部署都执行
git reset --hard master3.
git reset --hard会把工作区所有被跟踪的文件恢复到仓库中的版本4. 仓库里的
blog.db-wal是某个旧版本,覆盖了服务器上当前的 WAL 文件5. SQLite 的 WAL 模式下,数据变更先写 WAL,再 checkpoint 到主库
6. WAL 被旧文件覆盖后,未 checkpoint 的数据丢失,主库可能也被搞乱了
7. 服务重启时数据库重新初始化,发现 WAL/SHM 不匹配,数据状态错乱
简单说就是:每次 git reset --hard 部署,都用 git 仓库里的旧 WAL 文件覆盖了服务器上正在使用的 WAL 文件,导致数据丢失。
修复
1. 从 git 移除 WAL/SHM 文件
git rm --cached server/data/blog.db-shm server/data/blog.db-wal
echo "*.db-wal" >> .gitignore
echo "*.db-shm" >> .gitignore
git add .gitignore
git commit -m "fix: 从git移除WAL/SHM文件防止git reset覆盖导致数据丢失"
2. 验证修复效果
推送后在服务器执行 git reset --hard master,然后检查数据:
// GET /api/articles?page=1&limit=1
{"rows":[{"id":45,"title":"博客 V2.0 正式发布..."}]}
// 数据没有丢失!git reset 不再覆盖数据库文件了。
经验教训
1. .gitignore 要覆盖所有数据库相关文件
SQLite 的 WAL 模式会产生三个文件:
blog.db ← 主数据库文件
blog.db-wal ← Write-Ahead Log(预写日志)
blog.db-shm ← Shared Memory(共享内存索引)
.gitignore 必须三个都忽略:
*.db
*.db-wal
*.db-shm
2. 永远不要把数据库文件放进 git
Git 是版本控制工具,不是备份工具。数据库文件是二进制的,每次变更都会产生一个全新的 blob,仓库会迅速膨胀。更重要的是——git reset --hard 会覆盖工作区文件,这对正在运行的数据库是致命的。
3. 自动备份是救命稻草
这次能恢复数据,全靠每天凌晨 3 点的自动备份脚本。一个简单的 crontab + cp 就能救命:
# /etc/cron.d/blog-backup
0 3 * * * root cp /opt/blog/server/data/blog.db /opt/blog/backups/blog_$(date +\%Y\%m\%d)_$(date +\%H\%M\%S).db
4. 部署时检查 git status
如果部署前看一眼 git status,会发现 blog.db-wal 出现在跟踪文件列表里。养成部署前检查的习惯,能避免很多事故。
总结
这次事故的根因很简单:一个不完整的 .gitignore 规则。但影响是灾难性的——两个月的心血差点付之一炬。
好在有备份,数据完整恢复了。修复也很简单:从 git 移除 WAL/SHM 文件,补全 .gitignore。
备份数据,检查 gitignore,别让 git reset 变成删库跑路。
评论 (0)