🦏 第17章 数据备份
📄 一句话概括

MySQL 用 mysqldump -u 用户 -p 库名 > 文件.sql 备份, mysql -u 用户 -p 库名 < 文件.sql 还原。 它们是命令行程序,不是 SQL 语句。 本沙箱的 iterdump() 就是这里的 mysqldump:返回一串「能重建整个库」的 SQL, 用 executescript 灌回去就还原了。 直接拷 .db 文件不总是安全(可能拷到半截);c.backup(d) 才是「在线一致备份」。

📝 本章和前面几章的关系

上一篇讲「为什么」,这一篇讲「怎么做」,而且两边都真跑。 iterdump 这个 API 很值得记住:它能让你在没有任何外部工具的情况下,把整个库导成文本。 排查问题、做测试数据、迁移环境,都用得上。

一、MySQL 里长什么样

备份:mysqldump -u root -p mydb > mydb_20261101.sql; 还原:mysql -u root -p mydb < mydb_20261101.sql。 两个都是命令行程序,不是 SQL 语句—— 所以它们写在 >/< 前后,而不是在客户端里敲。 这一点常让人困惑。 还原之前要先把库建出来(create database mydb)—— mysqldump 默认不含 create database 那一条, 要含就得加 --databases 参数。 不加它的话,mysql < dump.sql 会报「no database selected」。 两个常用参数:--no-data 只导结构(建空库用)、 --no-create-info 只导数据(往已有的表里灌)。 这两个参数配合起来,就是「表结构变更」和「数据迁移」分开做的基础。

二、本沙箱的 mysqldump:iterdump

list(c.iterdump()) 返回一串字符串, 就是「重建这个库需要的全部 SQL」。 Demo 里那个库是 11 行。 结构很清楚:第一行 begin transaction、最后一行 commit; 每条 create table 后面紧跟着它的 insert。 为什么要包在 begin transaction 里? 因为「导到一半出错」时,要能整体作废, 不能留下一个「表建了、数据只灌了一半」的库。 这和事务的原子性是一回事。 表名被双引号包起来了(insert into "students")——这是为了防表名里有特殊字符(空格、 关键字)。 所以你导出的文本里会看到一堆双引号,那是正常的。 它就是本沙箱版的 mysqldump: 一样的思路——把数据变成「能重新执行一遍的 SQL」。 理解了这一点,mysqldump 也就不神秘了。

三、还原:把那串 SQL 执行一遍

r.executescript("\n".join(c.iterdump())),一行就还原完了。 Demo 里还原到空库,五行学生、两个班都在。 注意这是还原到一个「空库」里。 如果目标库里已经有同名表,create table 会报「already exists」。 所以还原的正确姿势是: 要么还原到一个全新的库,要么先 drop 掉旧的。 直接往生产库上 executescript 一份 dump,是会把东西撞坏的。 mysqldump 那边同理—— 它的 dump 里也没有 drop table if exists,要加得自己写 --add-drop-table。

四、热备份:源库还在写也能拿到一致快照

直接拷 .db 文件有个大坑: 拷到一半源库正在写,拷出来的可能是「半截」的。 数据库文件不是普通文件,它不是「一个整体」 ——中间可能有未提交的页、可能有日志还没合并。 Demo 里用 c.backup(d):它在源库上加短暂的读锁、把数据一页一页复制到目标库, 最后得到一份一致的快照。 源库在这期间可以继续读,写会等一小会儿。 结果里包含了刚插进去的那个学生,说明备份拿到的不是「旧快照」, 而是「备份开始那一刻的一致状态」。 MySQL 那边的对应物是 xtrabackup(物理热备份,不锁表), 不用它的话「拷数据文件」必须停服务。 所以「热备份工具」这件事,两个数据库都需要。

五、三种备份方式,怎么选

拷文件:最快、最简单, 但必须停服务,或者确认没人在写。 适合「小工具、单机、夜里没人用」的场景。 iterdump 导 SQL:慢,但能看懂、能改、能跨版本。 适合「留一份能长期保存的底」,也适合「只捞其中几张表的数据」。 backup() 在线备份:又快又一致,运行中也能做。 适合「服务不能停」的场景——这是默认该用的那一个。 一句话: 能停服务就拷文件,不能停就用 backup(),要长期归档就导 SQL。 三种都做一遍也不费多少事,关键是「真的要能恢复」。

六、动手 Demo:iterdump 当 mysqldump,backup 当热备份

Demo 三段:iterdump() 导出一串 SQL 并打印前几行、executescript 还原到空库、 backup() 做一次在线备份。 第一段那个「11 行」很值得数一遍—— begin 一行、两条 create table、七条 insert、commit 一行。 导出文件的大小,大致就按这个比例估。

import sqlite3
import _q46 as Q
c = Q.con()

# 1) 像 mysqldump 一样,把整库导成一串 SQL
lines = list(c.iterdump())
print("一共 %d 行" % len(lines))
for ln in lines[:7]:
    print("  " + ln)

# 2) 还原:把那串 SQL 灌进一个空库
r = sqlite3.connect("restore.db")
r.executescript("\n".join(c.iterdump()))
Q.show(r, "SELECT COUNT(*) AS 学生数 FROM students")

# 3) 热备份:源库还在写,也能拿到一致快照
d = sqlite3.connect("hot.db")
c.execute("INSERT INTO students (name, height, class_id) "
          "VALUES ('周杰', 172.0, 1)")
c.commit()
c.backup(d)
Q.show(d, "SELECT COUNT(*) AS 备份里的学生数 FROM students")

实跑输出:

--------------------------------------------------------------------
第七十九课:数据库的备份与还原
--------------------------------------------------------------------

  MySQL 里长什么样
  MySQL 原码(本沙箱不执行):
  | # 备份整个库(导出成 SQL 文本)
  | mysqldump -u root -p mydb > mydb_20261101.sql
  | 
  | # 还原
  | mysql -u root -p mydb < mydb_20261101.sql
  | 
  | # 只备份结构,不带数据
  | mysqldump -u root -p --no-data mydb > mydb_schema.sql
  | 
  | # 只备份数据,不带建表语句
  | mysqldump -u root -p --no-create-info mydb > mydb_data.sql
  **`mysqldump` 是个命令行程序,不是 SQL 语句**——
  **所以它写在 `>` 前面,输出重定向到文件。** **这一点常让人困惑:它不是你在客户端里敲的东西。**
  **还原用 `mysql` 客户端加 `<` 重定向**,
  **或者进客户端之后 `source 文件名`。** **注意还原之前要先把库建出来**
  (`create database mydb`)——**`mysqldump` 默认不含 `create database` 那一条**,
  **要含就得加 `--databases` 参数。**

  本沙箱的「mysqldump」:iterdump
  **`c.iterdump()` 返回一组字符串,就是「重建这个库需要的全部 SQL」。**
  **一共 11 行,前面几行是这样:**
    BEGIN TRANSACTION;
    CREATE TABLE classes (id INTEGER PRIMARY KEY, name TEXT);
    INSERT INTO "classes" VALUES(1,'一班');
    INSERT INTO "classes" VALUES(2,'二班');
    CREATE TABLE students (id INTEGER PRIMARY KEY, name TEXT,
                               height REAL, class_id INTEGER);
    INSERT INTO "students" VALUES(1,'张伟',175.5,1);
    INSERT INTO "students" VALUES(2,'李娜',162.0,1);
    ……(后面还有 4 行)
  **注意三件事**:
  **第一行是 `begin transaction`、最后一行是 `commit`**(**保证导出的东西是「一个整体」**);
  **每条 `create table` 后面紧跟着它的 `insert`**;
  **表名被双引号包起来了**(`insert into "students"`)——**这是为了防止表名里有特殊字符。**
  **它就是本沙箱版的 `mysqldump`**:**一样的思路——把数据变成「能重新执行一遍的 SQL」。**

  还原:把那串 SQL 执行一遍
  **`executescript(整段 dump)`**,一行代码就还原完了。
  SQL> SELECT COUNT(*) AS 学生数 FROM students
  学生数
  -------
  5
  (1 行)
  SQL> SELECT id, name FROM classes
  id     name
  ------------
  1      一班
  2      二班
  (2 行)
  **五行学生、两个班都在。** **注意这是还原到一个「空库」里**——
  **如果目标库里已经有同名表,`create table` 会报「already exists」。**
  **所以还原的正确姿势是**:
  **要么还原到一个全新的库,要么先 `drop` 掉旧的。**
  **直接往生产库上 `executescript` 一份 dump,是会把东西撞坏的。**

  热备份:源库还在写,也能拿到一份一致的快照
  **直接拷 `.db` 文件有个大坑**:
  **拷到一半源库正在写,拷出来的可能是「半截」的。**
  **数据库文件和普通文件不一样,它不是「一个整体」**——
  **中间可能有未提交的页、可能有日志还没合并。**
  SQL> INSERT INTO students (name, height, class_id) VALUES ('周杰', 172.0, 1)
  影响 1 行
  **`c.backup(d)` 是「在线备份」**:**它会在源库上加短暂的读锁、把数据一页一页复制到目标库**,
  **最后得到一份一致的快照。** **源库在这期间可以继续读,写会等一小会儿。**
  SQL> SELECT COUNT(*) AS 备份里的学生数 FROM students
  备份里的学生数
  ---------------
  6
  (1 行)
  **六行——包括刚插进去的那个。** **这比「用文件管理器复制粘贴」可靠得多**,
  **尤其是在有人正在用的时候。**
  **MySQL 那边的对应物是 `xtrabackup`**(**物理热备份,不锁表**),
  **不用它的话「拷数据文件」必须停服务。** **所以「热备份工具」这件事,两个数据库都需要。**

  三种备份方式,怎么选
  **拷文件**:**最快、最简单**,
  **但必须停服务,或者确认没人在写。** **适合「小工具、单机、夜里没人用」的场景。**
  **`iterdump` 导 SQL**:**慢,但能看懂、能改、能跨版本**。
  **适合「留一份能长期保存的底」**,**也适合「只捞其中几张表的数据」。**
  **`backup()` 在线备份**:**又快又一致**,
  **运行中也能做。** **适合「服务不能停」的场景**——**这是默认该用的那一个。**
  **一句话**:
  **能停服务就拷文件,不能停就用 `backup()`,要长期归档就导 SQL。**
  **三种都做一遍也不费多少事,关键是「真的要能恢复」。**

iterdump() 一共 11 行: begin transaction + 两条 create table + 七条 insert + commit。 建表语句原样保留了换行和缩进, 所以打印出来会看到一行带缩进的 height REAL, class_id INTEGER);。 还原后 5 名学生、2 个班都在。 热备份拿到 6 行——包括备份前刚插的那个周杰。

30 秒实验

  1. 往同一个库连续 executescript 两次同一个 dump,会报什么?
  2. iterdump() 的结果里有没有 drop table if exists?这会导致什么后果?
  3. 为什么「直接拷文件」在有人写入时不可靠,而 backup() 可靠?

常见误区

  1. 以为 mysqldump 是 SQL 语句。 它是命令行程序,写在 > 前面,不能进客户端敲。
  2. dump 直接往生产库上灌。 里面有 create table,表已存在就报错, 要先建空库或加 --add-drop-table。
  3. 用文件管理器拷正在被写的 .db。 可能拷到半截,要用 backup() 或先停写。
自测三问(点开对答案) 1. `iterdump()` 的结果为什么包在 `begin transaction` 里? —— 为了「导到一半出错时整体作废」,避免留下「表建了、数据只灌一半」的库。 2. `mysqldump` 还原前为什么要先建库? —— 它默认不包含 `create database` 语句,要包含得加 `--databases` 参数; 否则 `mysql < dump.sql` 会报没选库。 3. 为什么不能直接拷正在被写入的 `.db` 文件? —— 数据库文件不是「一个整体」,拷到一半可能有未提交的页或未合并的日志,得到的是不一致的半截。 要用 `backup()` 或停写。

整库会备了,下一篇缩小到一张表:17.3 表的备份与还原。

相关笔记