文章目录
-
- 一、项目背景:为什么迁移数据库比迁移应用更需要谨慎?
- 二、迁移前的风险分析与方案设计
-
- [1. 数据覆盖风险](#1. 数据覆盖风险)
- [2. Flyway 版本冲突](#2. Flyway 版本冲突)
- [3. 数据完整性风险](#3. 数据完整性风险)
- [4. 回退风险](#4. 回退风险)
- 三、第一步:备份原数据库
-
- [3.1 创建备份目录](#3.1 创建备份目录)
- [3.2 备份完整数据库](#3.2 备份完整数据库)
- [3.3 计算校验和](#3.3 计算校验和)
- 四、第二步:比较源库和目标库的表结构
- 五、第三步:只导出业务数据,不重复导入结构和迁移历史
-
- [5.1 生成业务数据备份](#5.1 生成业务数据备份)
- [5.2 为什么不能只检查 SQL 文件是否存在?](#5.2 为什么不能只检查 SQL 文件是否存在?)
- [六、第四步:在 K3s 中导入业务数据](#六、第四步:在 K3s 中导入业务数据)
-
- [6.1 执行数据导入](#6.1 执行数据导入)
- [6.2 关于重复执行导入](#6.2 关于重复执行导入)
- 七、第五步:逐表验证数据一致性
-
- [7.1 在目标库统计行数](#7.1 在目标库统计行数)
- [7.2 为什么行数一致还不够?](#7.2 为什么行数一致还不够?)
- [八、第六步:检查主键和 AUTO_INCREMENT](#八、第六步:检查主键和 AUTO_INCREMENT)
- 九、第七步:排查中文乱码,区分存储问题和连接问题
-
- [9.1 检查数据库字符集](#9.1 检查数据库字符集)
- [9.2 显式指定 UTF-8 查询](#9.2 显式指定 UTF-8 查询)
- [十、第八步:让 Spring Boot 重新连接目标数据库](#十、第八步:让 Spring Boot 重新连接目标数据库)
-
- [10.1 检查数据库连接和 Flyway](#10.1 检查数据库连接和 Flyway)
- [10.2 检查 Kubernetes Service 和 Endpoints](#10.2 检查 Kubernetes Service 和 Endpoints)
- [十一、第九步:通过实际 API 验证迁移结果](#十一、第九步:通过实际 API 验证迁移结果)
-
- [标签 API 的 403 如何处理?](#标签 API 的 403 如何处理?)
- 十二、如何判断这次迁移是否真正成功?
- [十三、这次实践中最值得沉淀的 DevOps 经验](#十三、这次实践中最值得沉淀的 DevOps 经验)
- 十四、如果将这次迁移进一步升级为生产级方案
- 十五、面试总结:如何讲好这次迁移经历?
- 结语
一、项目背景:为什么迁移数据库比迁移应用更需要谨慎?
在逐浪笔记的 Kubernetes 迁移实践中,我原本已经通过 Docker Compose 部署了 Spring Boot + Vue + MySQL 博客系统。为了积累 Kubernetes 实战经验,同时为 DevOps 面试准备一个完整的迁移案例,我决定在保留原有部署环境的前提下,将业务数据库迁移到 K3s 集群中。
本次迁移不是简单地启动一个新的 MySQL 容器,而是将原数据库中的真实业务数据迁移到 Kubernetes 管理的 MySQL 实例,并验证应用在新环境中能否正常访问这些数据。
原环境和目标环境如下:
| 项目 | 原环境 | 目标环境 |
|---|---|---|
| 应用部署 | Docker Compose | K3s / Kubernetes |
| 数据库 | MySQL 8.0.46 | MySQL 8.0 |
| 数据库名称 | blog |
blog_k8s_test |
| 数据持久化 | Docker Compose 数据卷 | Kubernetes PVC |
| 数据库连接 | Compose 服务名 mysql |
Kubernetes Service blog-k8s-mysql |
| 应用连接地址 | mysql:3306 |
blog-k8s-mysql:3306 |
目标数据库使用独立的 blog_k8s_test 库,避免直接覆盖原数据库。原 Docker Compose 环境继续保留,作为迁移失败时的回退依据。
本次迁移的核心目标
- 保留原有业务数据,不因环境迁移造成文章、分类、标签或用户数据丢失。
- 复用目标库已有的表结构和 Flyway 迁移历史,避免重复执行数据库初始化。
- 确保主键、自增值、字符集及表间关联数据保持合理。
- 让 Spring Boot 在 K3s 中重新连接数据库,并通过实际 API 验证迁移结果。
- 保留原环境,降低迁移风险,确保出现问题时仍有恢复路径。
这次实践最重要的原则是:先备份,再确认目标库结构;先验证数据,再切换业务流量。
二、迁移前的风险分析与方案设计
正式执行迁移之前,我先明确了几个主要风险。
1. 数据覆盖风险
如果直接把完整数据库备份恢复到目标库,可能覆盖已有数据、表结构或者迁移记录。因此,必须先确认目标库的状态,再决定迁移哪些内容。
2. Flyway 版本冲突
Spring Boot 项目使用 Flyway 管理数据库结构变更。目标库已经存在表结构和 flyway_schema_history,如果直接重复导入完整 SQL,可能造成重复建表、迁移历史不一致或应用启动失败。
因此,本次迁移采取:
- 目标库保留已有表结构。
- 保留目标库已有的 Flyway 历史。
- 只迁移业务数据,不重复导入数据库结构和 Flyway 历史。
3. 数据完整性风险
仅仅看到 mysql 命令没有报错,并不能证明迁移成功。
例如,文章数据可能缺失,关联表可能没有导入,或者自增值可能小于现有最大主键。因此,需要在迁移前后对数据进行可比较的验证。
4. 回退风险
迁移过程中不应立即删除原数据库或原部署环境。本次保留原 Docker Compose 数据库及应用环境,并在迁移前创建独立备份。
需要强调的是:保留原环境不等于自动具备完整回滚能力。 如果迁移期间新旧环境都允许写入,还需要考虑数据同步或明确写入切换策略,否则回退时可能出现新数据丢失。
三、第一步:备份原数据库
3.1 创建备份目录
在服务器上创建独立备份目录:
bash
sudo mkdir -p /opt/devops/backups
sudo chmod 700 /opt/devops/backups
数据库备份包含用户信息和业务内容,需要限制文件访问权限。
3.2 备份完整数据库
本次创建的完整数据库备份文件为:
text
/opt/devops/backups/blog-20261019-115025.sql
完整备份可用于保留原数据库的恢复依据。
实际执行时,应根据原 MySQL 容器名称和数据库连接方式调整命令。例如,原数据库运行在 Docker Compose 的 mysql 容器中,可以使用:
bash
BACKUP_FILE="/opt/devops/backups/blog-full-$(date +%Y%m%d-%H%M%S).sql"
docker exec mysql sh -c '
exec mysqldump \
--single-transaction \
--quick \
--hex-blob \
--routines \
--events \
--triggers \
-uroot \
-p"$MYSQL_ROOT_PASSWORD" \
--databases blog
' > "$BACKUP_FILE"
test -s "$BACKUP_FILE" || {
echo "Backup file is empty"
exit 1
}
chmod 600 "$BACKUP_FILE"
sha256sum "$BACKUP_FILE"
注意: 上述命令适用于 MYSQL_ROOT_PASSWORD 已存在于原容器环境变量中的情况。若源库凭据或容器配置不同,应使用实际配置。生产环境还应根据权限和业务特点确认是否需要备份存储过程、事件、触发器及其他数据库对象。
--single-transaction 适合 InnoDB 表的一致性备份,但不代表它能为所有非事务表或并发 DDL 提供一致性保证。
3.3 计算校验和
本次完整备份的 SHA-256 为:
text
14dc66ec0e325c6c9d3d353ed8d2578db2eee8ef0952778c531c8ca71c0b9df6
校验和可以用于后续确认备份文件是否被意外修改。
需要注意,校验和只能证明文件字节是否一致,不能单独证明 SQL 一定可以成功恢复。因此,完整的备份验证还应包括检查 SQL 文件内容,以及在隔离环境中进行恢复测试。
四、第二步:比较源库和目标库的表结构
在迁移业务数据前,先确认目标库是否已经存在所需表结构。
本次目标库为 blog_k8s_test,其中已经存在业务表和 Flyway 迁移历史。
可以通过以下 SQL 检查表是否存在:
sql
SHOW TABLES;
查看关键表结构:
sql
SHOW CREATE TABLE article;
SHOW CREATE TABLE category;
SHOW CREATE TABLE tag;
SHOW CREATE TABLE article_tag;
查看 Flyway 迁移记录:
sql
SELECT installed_rank,
version,
description,
success
FROM flyway_schema_history
ORDER BY installed_rank;
本次核对结果显示:
- 源库 Flyway 版本 V1 至 V10 均成功。
- 目标库已经存在对应的迁移历史。
- 目标库的数据库结构与源库相匹配。
- 目标库当前 Flyway 版本为 V10,Spring Boot 启动时校验通过,没有需要执行的新迁移。
这里有一个很重要的工程实践:数据库迁移不应把结构迁移和数据迁移混为一谈。
如果目标环境已经通过 Flyway 初始化了正确的结构,就应明确哪些内容需要迁移,避免重复导入结构 SQL。
五、第三步:只导出业务数据,不重复导入结构和迁移历史
经过前面的检查,本次选择只导出七张业务表的数据:
articlecategorytagsys_userarticle_tagarticle_likearticle_comment
不包含 flyway_schema_history。
5.1 生成业务数据备份
本次实际生成的文件为:
text
/opt/devops/backups/blog-business-data-20261019.sql
对应的导出命令如下:
bash
docker exec mysql sh -c '
exec mysqldump \
--no-create-info \
--skip-triggers \
--single-transaction \
--quick \
--hex-blob \
-uroot \
-p"$MYSQL_ROOT_PASSWORD" \
blog \
article \
category \
tag \
sys_user \
article_tag \
article_like \
article_comment
' > /opt/devops/backups/blog-business-data-20261019.sql
test -s /opt/devops/backups/blog-business-data-20261019.sql || {
echo "Business data dump is empty"
exit 1
}
chmod 600 /opt/devops/backups/blog-business-data-20261019.sql
sha256sum /opt/devops/backups/blog-business-data-20261019.sql
其中有几个关键参数值得解释:
| 参数 | 作用 |
|---|---|
--no-create-info |
不导出建表语句,只导出数据 |
--skip-triggers |
不导出触发器定义 |
--single-transaction |
对 InnoDB 表进行一致性快照导出 |
--quick |
逐行读取结果,降低大表导出时的内存占用 |
--hex-blob |
以十六进制形式输出二进制字段 |
因为目标库的表结构已经存在,本次不需要再次执行 CREATE TABLE。
同样重要的是,不能把源库的 flyway_schema_history 一并覆盖到目标库,否则可能导致数据库实际结构与 Flyway 记录不一致。
5.2 为什么不能只检查 SQL 文件是否存在?
我在本次迁移中还检查了业务数据 SQL 的内容,确认:
- 七张业务表均有数据导出语句。
- 不包含建表语句。
- 不包含
flyway_schema_history的数据导入。 - SQL 文件非空,且具备后续校验所需的数据内容。
这一阶段的重点是把迁移范围控制清楚,而不是导出一个越大的 SQL 文件就越安全。
六、第四步:在 K3s 中导入业务数据
目标 MySQL 部署名称为 blog-k8s-mysql,目标数据库为 blog_k8s_test。
导入前,需要再次确认目标库是预期的迁移目标,并确保导入期间没有应用或其他任务向目标库写入相同业务数据。
对于正式迁移,应提前安排写入冻结或维护窗口,并确保应用不会在导入过程中启动初始化写入。
6.1 执行数据导入
本次实际使用的命令如下:
bash
sudo k3s kubectl exec -i \
-n my-blog-k8s \
deployment/blog-k8s-mysql \
-- sh -c '
export MYSQL_PWD="$MYSQL_ROOT_PASSWORD"
exec mysql \
--default-character-set=utf8mb4 \
-uroot blog_k8s_test
' < /opt/devops/backups/blog-business-data-20261019.sql
这里有两个需要特别说明的地方。
第一,使用 kubectl exec -i 将本地 SQL 文件通过标准输入传入目标 MySQL 容器,而不是将备份文件复制到容器内部。
第二,导入时显式使用 utf8mb4,避免客户端连接字符集不一致导致中文数据出现转换问题。
本次导入命令执行后没有报错,但我没有仅凭命令退出就认定迁移成功,而是继续进行数据库级别的验证。
6.2 关于重复执行导入
这类 SQL 文件包含业务数据的 INSERT 语句,不是可以无条件重复执行的幂等操作。
如果第一次导入已经成功,再执行一次可能产生主键冲突、重复记录或部分导入失败。因此,出现导入异常时,应先确认哪些语句已经提交、目标表当前数据状态以及事务边界,再制定恢复方案。
不要为了排查问题而反复执行同一份导入文件。
七、第五步:逐表验证数据一致性
数据迁移完成后,最重要的是证明目标库的数据没有出现明显缺失。
本次源库业务表行数如下:
| 表名 | 源库行数 | 目标库行数 |
|---|---|---|
article |
18 | 18 |
category |
7 | 7 |
tag |
3 | 3 |
sys_user |
9 | 9 |
article_tag |
1 | 1 |
article_like |
2 | 2 |
article_comment |
1 | 1 |
flyway_schema_history |
10 | 10 |
其中七张业务表通过数据导入迁移;flyway_schema_history 保留在目标库原有状态,并通过查询确认仍有 10 条迁移记录。
7.1 在目标库统计行数
可以执行:
sql
SELECT 'article', COUNT(*) FROM article
UNION ALL
SELECT 'category', COUNT(*) FROM category
UNION ALL
SELECT 'tag', COUNT(*) FROM tag
UNION ALL
SELECT 'sys_user', COUNT(*) FROM sys_user
UNION ALL
SELECT 'article_tag', COUNT(*) FROM article_tag
UNION ALL
SELECT 'article_like', COUNT(*) FROM article_like
UNION ALL
SELECT 'article_comment', COUNT(*) FROM article_comment;
将目标库结果与迁移前记录的源库行数逐项对比。
这里的关键不是统计总行数,而是按表比较。即使总数相同,也可能出现一张表多数据、另一张表少数据的情况。
7.2 为什么行数一致还不够?
行数相同并不意味着所有字段都一致。例如:
- 某些记录内容可能发生变化。
- 中文字段可能被错误转换。
- 关联表数据可能丢失或重复。
- 主键和自增值可能不合理。
因此,还需要进一步检查主键范围、自增值、关键业务字段以及应用实际读取结果。
八、第六步:检查主键和 AUTO_INCREMENT
迁移数据时,如果目标表的自增值没有正确推进,后续新增记录可能遇到主键冲突。
本次检查了七张业务表的最大 ID 和自增值。
| 表名 | 最大 ID | AUTO_INCREMENT | 结果 |
|---|---|---|---|
article |
18 | 19 | 正常 |
article_comment |
1 | 2 | 正常 |
article_like |
4 | 6 | 正常 |
article_tag |
4 | 5 | 正常 |
category |
7 | 8 | 正常 |
sys_user |
14 | 15 | 正常 |
tag |
3 | 4 | 正常 |
这些表的自增值均大于现有最大 ID,因此不需要手动修改。
可以通过以下方式检查:
sql
SELECT MAX(id) FROM article;
SHOW TABLE STATUS LIKE 'article';
对其他表执行相同检查即可。
需要注意,自增值高于最大 ID 只是基础合理性检查。如果表使用复合主键、UUID 或其他 ID 生成策略,就应根据实际设计选择不同的验证方法。
九、第七步:排查中文乱码,区分存储问题和连接问题
在本次迁移验证中,我发现一个很有代表性的现象:查询文章标题时,部分中文显示为 ????。
最初不能直接认定数据损坏,因为字符集问题可能发生在客户端连接、查询结果传输或终端显示环节。
9.1 检查数据库字符集
执行:
sql
SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE 'collation%';
SHOW CREATE TABLE article;
检查结果显示:
character_set_database为utf8mb4。character_set_server为utf8mb4。article表使用DEFAULT CHARSET=utf8mb4。- 但原查询连接中的
character_set_client、character_set_connection和character_set_results为latin1。
这意味着数据库默认字符集与客户端连接字符集不一致,需要进一步验证实际存储内容。
9.2 显式指定 UTF-8 查询
重新查询时使用:
bash
mysql --default-character-set=utf8mb4 \
-uroot --batch --skip-column-names blog_k8s_test \
-e "SELECT id, title FROM article WHERE id IN (1, 2, 18);"
随后,中文标题能够正常显示,例如:
- 我的博客系统:从 0 到 1 的 DevOps 实践之路
- 蛙泳,站在岸上也能学
- 从 Docker Compose 到 K3s:逐浪博客首次完整部署与故障排查实战
这说明本次观察到的中文显示异常,是客户端连接字符集不匹配导致的显示问题,并非这些文章标题在目标数据库中已经损坏。
经验总结:遇到中文乱码时,先检查字符集和实际存储字节,再决定是否修复数据。不要在没有证据的情况下批量转换字符集或重新导入数据。
十、第八步:让 Spring Boot 重新连接目标数据库
数据迁移完成后,还需要验证应用能否正常使用目标数据库。
本次 K3s 后端使用以下连接配置:
yaml
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: MYSQL_HOST
value: "blog-k8s-mysql"
- name: SPRING_DATASOURCE_URL
value: "jdbc:mysql://blog-k8s-mysql:3306/blog_k8s_test?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai"
数据库用户名和密码通过 Kubernetes Secret 提供,避免把密码直接写进镜像或代码仓库。
需要注意:上面展示的是连接配置示例,实际部署中应以当前 Deployment 的完整配置为准,并确认 spring.datasource 的最终配置确实使用了目标地址。
10.1 检查数据库连接和 Flyway
查看后端日志:
bash
sudo k3s kubectl logs \
deployment/my-blog-backend \
-n my-blog-k8s \
--since=15m
本次日志确认:
- Hikari 数据库连接池启动成功。
- Spring Boot 成功连接
blog_k8s_test。 - Flyway 成功校验已有迁移记录。
- 当前数据库版本为 V10。
- 数据库结构已是最新,不需要执行新的迁移。
BlogBackendApplication启动成功。
这些信息说明,应用不只是找到了数据库服务,而且已经完成数据库连接和结构版本校验。
10.2 检查 Kubernetes Service 和 Endpoints
可以执行:
bash
sudo k3s kubectl get deployment,svc,endpoints \
-n my-blog-k8s
本次后端、前端和 MySQL 的 Deployment 均达到 1/1 Ready,相关 Service 也存在可用的后端 Endpoint。
这一步主要用于排除 Kubernetes 服务发现、Pod 就绪状态和服务端口配置方面的问题。
十一、第九步:通过实际 API 验证迁移结果
数据库连接正常不等于业务一定正常,因此我继续通过后端 API 验证业务数据。
使用端口转发访问 K3s 后端:
bash
sudo k3s kubectl port-forward \
-n my-blog-k8s \
svc/my-blog-backend \
18080:8080
保持端口转发运行,在另一个终端中执行:
bash
curl -i http://127.0.0.1:18080/api/categories
本次分类 API 返回 HTTP 200,并且返回了真实的分类数据。
再检查文章 API:
bash
curl -i http://127.0.0.1:18080/api/articles
接口正常返回 HTTP 200,分页结果中的 total 为 15。
为什么数据库中有 18 篇文章,而 API 只返回 15 篇?
继续在数据库中检查:
sql
SELECT status, deleted, COUNT(*)
FROM article
GROUP BY status, deleted
ORDER BY status, deleted;
结果为:
text
PUBLISHED 0 15
PUBLISHED 1 3
也就是说,18 篇文章中有 15 篇未逻辑删除,另有 3 篇虽然状态为 PUBLISHED,但 deleted = 1。
因此,API 返回的 15 篇文章与当前数据库中的逻辑删除情况相吻合。这个现象不应误判为迁移丢失了 3 篇文章。
不过,API 返回值与数据库统计相吻合,只能说明结果符合预期;如果需要进一步证明查询条件的具体实现,还应检查对应的 Mapper SQL、Service 或 Repository。
标签 API 的 403 如何处理?
本次验证中:
text
GET /api/categories -> HTTP 200
GET /api/articles -> HTTP 200
GET /api/tags -> HTTP 403
标签 API 返回 403,说明该请求被拒绝访问,但不能仅凭这个状态码就断定是 Spring Security 中哪一条规则造成的。
合理的排查顺序是:
- 检查 Tag Controller 的实际路由和请求方法。
- 检查
SecurityFilterChain中对应的授权规则。 - 检查 JWT 过滤器及其他自定义认证、授权逻辑。
- 对比分类、文章和标签接口的访问策略。
- 在不关闭整体安全机制的前提下修正授权配置,再次验证。
这也是迁移实践的重要经验:不要把所有 API 异常都归因于数据库。 数据库迁移成功后,应用层仍可能存在独立的认证、授权或业务逻辑问题。
本次 /api/tags 的 403 尚待定位,因此不将其描述为已经修复。
十二、如何判断这次迁移是否真正成功?
我把迁移验收分为四个层次。
| 验证层次 | 核心检查 | 本次结果 |
|---|---|---|
| 数据层 | 逐表行数、主键、自增值、中文字段 | 已通过基础核对 |
| 数据库层 | MySQL 连接、表结构、Flyway 版本 | 已通过 |
| Kubernetes 层 | Deployment、Service、Endpoint | 已通过基础检查 |
| 应用层 | 分类 API、文章 API、标签 API | 分类与文章通过;标签仍需排查 |
这里必须区分两个结论:
第一,MySQL 业务数据迁移已经完成基础验证。 七张业务表的行数与源库一致,目标库的 Flyway 历史也得到保留,中文标题已能够正常读取。
第二,整个博客系统的迁移验收尚未全部完成。 标签 API 仍返回 403,其他写入接口、文章详情、评论和点赞等功能也需要根据业务需求继续验证。
只有将数据层、数据库层、基础设施层和应用层的结果分别记录,才能避免因为一个接口返回 200 就宣布整个系统迁移成功。
十三、这次实践中最值得沉淀的 DevOps 经验
经验一:迁移前先明确回退策略
回退不是出问题后再临时想办法,而是在迁移开始前就明确:
- 原数据库是否保留。
- 备份存放在哪里。
- 备份能否读取和恢复。
- 哪个环境接受业务写入。
- 如果迁移失败,如何恢复服务。
对于数据库迁移,恢复旧应用并不一定等于恢复旧数据,尤其是在新环境已经接收写入之后。
经验二:数据库结构和业务数据分开处理
目标库已有 Flyway 管理的表结构时,不应无差别地重复导入完整数据库 SQL。
明确结构迁移和数据迁移的边界,能降低重复建表、迁移历史冲突和环境漂移的风险。
经验三:验证必须有多个维度
本次没有把 mysqldump 和 mysql 命令成功执行作为最终依据,而是继续检查:
- 表级行数。
- 最大主键和自增值。
- Flyway 迁移记录。
- 中文数据的实际读取结果。
- Spring Boot 数据库连接日志。
- Kubernetes Service 和 Endpoint。
- 实际业务 API 返回值。
这些证据组合起来,才形成更可信的迁移验收结果。
经验四:区分数据故障、连接故障和应用故障
中文标题显示为问号,最终定位到客户端连接字符集不一致;文章 API 数量少于数据库总行数,最终发现有 3 篇逻辑删除文章;标签 API 返回 403,则应转向应用授权配置继续排查。
这三个问题看起来都像"迁移后数据不对",但根因完全不同。
排障的关键不是尽快修改配置,而是通过证据逐步缩小问题范围。
十四、如果将这次迁移进一步升级为生产级方案
这次实践已经完成了基础数据迁移和多层验证。如果迁移对象是高可用、持续有业务写入的生产数据库,还需要补充以下环节:
- 明确停写窗口: 迁移前冻结业务写入,或设计增量同步和最终切换机制。
- 备份恢复演练: 不仅保存 SQL 文件,还要在隔离环境实际恢复并验证。
- 一致性校验增强: 在行数之外,增加关键字段、主键集合或按业务范围计算的校验值。
- 切换与回退演练: 明确 DNS、Service、连接配置和流量切换步骤,并验证回退后的数据一致性。
- 可观测性补齐: 监控数据库连接池、慢查询、应用错误率和业务接口可用性。
- 安全与权限治理: 使用 Kubernetes Secret 或外部密钥管理服务,避免明文凭据进入代码、镜像和日志。
- 自动化验收: 将逐表行数、Flyway 版本和关键 API 检查整理为可重复执行的验证脚本。
需要强调的是,本次实践完成了真实数据迁移和基础验证,但没有据此声称已经完成在线增量同步、生产级自动回滚或完整的零停机切换。
十五、面试总结:如何讲好这次迁移经历?
面试官如果问:"你有没有做过数据库迁移?怎么保证迁移成功?"
可以这样回答:
我在自己的 Spring Boot + Vue 博客项目中,实际做过一次从 Docker Compose 到 K3s 的 MySQL 数据迁移。
我没有直接把完整 SQL 覆盖到目标库,而是先保留原环境并创建完整备份,再核对目标库的表结构和 Flyway 历史。由于目标库已经存在正确的结构,我采用数据与结构分离的方式,只导出七张业务表的数据,不重复导入
flyway_schema_history。数据导入后,我没有只看命令是否执行成功,而是逐表核对源库和目标库的行数,同时检查主键、自增值、数据库连接和 Flyway 版本。随后启动 K3s 中的 Spring Boot 后端,通过分类和文章 API 验证业务数据是否能够正常读取。
排查过程中,我还发现了两个容易误判的问题:一是 MySQL 客户端使用
latin1导致中文标题显示为问号,显式使用utf8mb4后恢复正常;二是文章表有 18 条记录,但 API 只返回 15 篇,进一步检查发现另外 3 篇属于逻辑删除数据,并不是迁移丢失。这次经历让我认识到,数据库迁移的关键不只是备份和导入,而是迁移范围控制、数据一致性验证、应用层验收,以及事先准备好的回退策略。
面试官可能继续追问
Q1:为什么不直接把整个数据库 dump 导入新环境?
因为目标库已经存在 Flyway 管理的结构和迁移历史,直接恢复完整 SQL 可能造成结构冲突或迁移记录不一致。我先核对目标库,再明确只迁移业务数据。
Q2:如何证明数据没有丢失?
逐表比较源库和目标库行数,再检查主键、自增值、关键字段和实际 API。对于要求更高的场景,还需要增加数据校验和恢复演练。
Q3:mysqldump 成功是否代表备份可靠?
不代表。文件非空和校验和只能完成基础检查,还需要确认 SQL 内容完整,并在隔离环境进行恢复测试。
Q4:为什么 API 返回 15 篇文章,而数据库有 18 篇?
通过 GROUP BY status, deleted 发现 15 篇未删除、3 篇已逻辑删除,API 返回数量与数据分布吻合。若需要确认具体过滤规则,还要检查 Mapper SQL 或 Repository 查询条件。
Q5:如果迁移后发现数据不一致,如何回退?
首先停止目标环境写入,判断问题属于导入失败、数据不一致还是应用连接错误。若需要回退到原环境,应使用已经验证的备份和原部署配置,并处理新环境产生的写入数据。不能简单地把应用切回去,就认为数据已经回滚。
结语
这次 MySQL 迁移实践让我把数据库备份、数据导入、Flyway、Kubernetes Service、Spring Boot 配置和 API 验证串成了一个完整的 DevOps 故障排查案例。
真正有价值的不是记住几条命令,而是能够解释每一步为什么这样做、如何判断成功、失败后如何恢复,以及如何通过证据区分数据问题、环境问题和应用问题。
对于 DevOps 工程师而言,迁移成功不只是服务启动,而是数据可信、业务可用、风险可控,并且具备明确的恢复路径。