最近在几个金融信创迁移的项目里,我发现不少从 Oracle 转过来的兄弟都在叫苦。以前在 Oracle 里,我们要搞个开发环境或者做个架构同步,一个 expdp content=metadata_only 就能深藏身名。但在 MySQL 这边,很多同学只会一个简单的 mysqldump,结果导出来的东西“缺胳膊少腿”:要么漏了触发器,要么存储过程在目标库跑不起来,最头疼的是用户权限,导过去之后应用死活连不上。
引言:别让元数据成了你迁移路上的“绊脚石”
兄弟们,干咱们这行,迁移数据不难,难的是“平滑”。
我曾经见过一个哥们,做核心库迁移,几百个 GB 的数据导了十几个小时,结果最后发现所有的 Trigger(触发器) 全没带过去,导致业务逻辑在凌晨上线时直接宕机,冷汗瞬间就下来了。还有的同学,导出了结构,结果目标库的 DEFINER(定义者) 全是原库的账号,新库根本没这人,存储过程一调一个报错。
在金融级场景下,手写脚本、精准控制每一个对象,这才是 DBA 的浪漫。今天我们要搞定的,不仅是表结构,还有:
- Indexes(索引)
- Stored Procedures & Functions(存储过程与函数)
- Triggers(触发器)
- Events(事件调度器)
- Users & Privileges(用户与权限)
今天,老班长就给大家好好盘一盘,如何在 Red Hat 8 + MySQL 8.0 的生产环境下,像专家一样优雅地实现“纯元数据”导出。咱们的目标只有一个:要把 1 年的经验,用出 18 年架构师的效果。
1 搭建“金融级”实验靶场
别空谈,直接上手。我们先在源库模拟一个稍微复杂的金融核心环境,包含多张表、存储过程、触发器、事件以及不同的用户角色。
- 实战脚本:创建模拟环境对象
-- 1. 创建核心库
CREATE DATABASE IF NOT EXISTS fin_prod CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
USE fin_prod;
-- 2. 创建用户基础档案表 (包含主键、唯一索引、备注)
CREATE TABLE `sys_user` (
`user_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` VARCHAR(50) NOT NULL COMMENT '登录名',
`real_name` VARCHAR(100) DEFAULT NULL COMMENT '真实姓名',
`id_card_mask` VARCHAR(20) DEFAULT NULL COMMENT '脱敏身份证',
`status` TINYINT DEFAULT '1' COMMENT '1:活跃, 0:禁用',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`user_id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB COMMENT='用户信息表';
-- 3. 创建账户余额表 (模拟金融计算)
CREATE TABLE `acc_balance` (
`acc_no` VARCHAR(32) NOT NULL COMMENT '账号',
`user_id` BIGINT NOT NULL COMMENT '所属用户',
`balance` DECIMAL(18,2) DEFAULT '0.00' COMMENT '余额',
`currency` VARCHAR(10) DEFAULT 'CNY',
`update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`acc_no`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB COMMENT='账户余额表';
-- 4. 创建交易流水表 (大数据量模拟对象)
CREATE TABLE `trx_order` (
`order_id` VARCHAR(64) NOT NULL COMMENT '订单号',
`acc_no` VARCHAR(32) NOT NULL,
`direction` ENUM('IN','OUT') NOT NULL,
`amt` DECIMAL(18,2) NOT NULL,
`trx_date` DATE NOT NULL,
`remark` TEXT,
PRIMARY KEY (`order_id`),
KEY `idx_trx_date` (`trx_date`),
KEY `idx_acc_no_date` (`acc_no`, `trx_date`)
) ENGINE=InnoDB COMMENT='交易流水表';
-- 5. 插入少量测试数据 (验证 Metadata Only 是否生效)
INSERT INTO sys_user (username, real_name, id_card_mask) VALUES
('xiang_sheng', '香生', '4401**********1234'),
('tester_01', '测试员01', '3101**********5678');
INSERT INTO acc_balance (acc_no, user_id, balance) VALUES
('622202000001', 1, 1000000.00),
('622202000002', 2, 500.50);
INSERT INTO trx_order (order_id, acc_no, direction, amt, trx_date, remark) VALUES
(REPLACE(UUID(),'-',''), '622202000001', 'OUT', 100.00, CURDATE(), '买咖啡'),
(REPLACE(UUID(),'-',''), '622202000001', 'IN', 50000.00, CURDATE(), '发工资');
-- 6. 创建自定义函数:格式化脱敏姓名
DELIMITER //
CREATE FUNCTION fn_mask_name(p_name VARCHAR(100)) RETURNS VARCHAR(100)
DETERMINISTIC
BEGIN
RETURN CONCAT(LEFT(p_name, 1), REPEAT('*', CHAR_LENGTH(p_name)-1));
END //
DELIMITER ;
-- 7. 创建存储过程:模拟转账逻辑 (包含事务处理)
DELIMITER //
CREATE PROCEDURE sp_transfer(
IN p_from_acc VARCHAR(32),
IN p_to_acc VARCHAR(32),
IN p_amt DECIMAL(18,2),
OUT p_res VARCHAR(20)
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK;
START TRANSACTION;
UPDATE acc_balance SET balance = balance - p_amt WHERE acc_no = p_from_acc;
UPDATE acc_balance SET balance = balance + p_amt WHERE acc_no = p_to_acc;
INSERT INTO trx_order (order_id, acc_no, direction, amt, trx_date, remark)
VALUES (REPLACE(UUID(),'-',''), p_from_acc, 'OUT', p_amt, CURDATE(), '转账支出');
COMMIT;
SET p_res = 'SUCCESS';
END //
DELIMITER ;
-- 8. 创建触发器:更新余额自动记录审计日志 (仅结构)
CREATE TABLE `sys_audit_log` (
`log_id` INT AUTO_INCREMENT PRIMARY KEY,
`op_type` VARCHAR(20),
`op_content` JSON,
`op_time` DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_after_balance_update
AFTER UPDATE ON acc_balance FOR EACH ROW
BEGIN
INSERT INTO sys_audit_log(op_type, op_content)
VALUES ('UPDATE_BALANCE', JSON_OBJECT('old', OLD.balance, 'new', NEW.balance));
END;
-- 9. 创建定时任务:凌晨清理审计日志
CREATE EVENT ev_audit_cleanup
ON SCHEDULE EVERY 1 DAY STARTS '2026-01-01 02:00:00'
DO DELETE FROM sys_audit_log WHERE op_time < NOW() - INTERVAL 7 DAY;
-- 10. 创建用户及复杂的权限体系 (MySQL 8.0 风格)
-- 应用用户:增删改查
CREATE USER 'app_user'@'%' IDENTIFIED BY 'App123456!';
GRANT SELECT, INSERT, UPDATE, DELETE ON fin_prod.* TO 'app_user'@'%';
GRANT EXECUTE ON PROCEDURE fin_prod.sp_transfer TO 'app_user'@'%';
-- 审计用户:只读 + 查看定义
CREATE USER 'audit_user'@'%' IDENTIFIED BY 'Audit999#';
GRANT SELECT ON fin_prod.* TO 'audit_user'@'%';
GRANT SHOW VIEW, TRIGGER ON fin_prod.* TO 'audit_user'@'%';
-- 模拟开发用户:只能看结构导不出数据
CREATE USER 'dev_user'@'%' IDENTIFIED BY 'Dev888888!';
GRANT SELECT ON fin_prod.sys_user TO 'dev_user'@'%';
2 mysqldump 的“移魂大法”
虽然现在工具有很多,但 mysqldump 依然是每个 DBA 的随身瑞士军刀。要实现 metadata_only 的效果,你得把参数玩出花来。
假设我们要导出库名为 fin_prod 的所有元数据:
# 这一行建议直接“抄作业”存入你的脚本库
mysqldump -u root -p \
--no-data \
--routines \
--events \
--triggers \
--set-gtid-purged=OFF \
--column-statistics=0 \
--single-transaction \
--databases fin_prod > fin_prod_metadata.sql
核心参数深度拆解(避坑指南) :
--no-data(或-d) :这是复刻metadata_only的核心,告诉工具:我只要 DDL,不要那几千万行 INSERT。--routines(-R) :导出存储过程和自定义函数。默认是不导出的,坑就在这!--events(-E) :导出事件调度器。如果你的系统有定时清理任务,这个必带。--triggers:虽然默认是开启的,但笔者习惯显式加上,求个稳。--set-gtid-purged=OFF: > ⚠️ 重点! 如果你的环境开启了 GTID(金融环境标配),不加这个参数,导出的文件开头会带上一句SET @@GLOBAL.GTID_PURGED。当你把这个文件往测试库导时,如果测试库已经有 GTID 记录,直接报错,甚至会破坏测试库的主从关系。--single-transaction:保证导出期间的一致性快照。虽然没数据,但为了防止导出 DDL 时表结构被修改导致报错,加上它是好习惯。
⚠️ 老兵提醒 - 备库备份注意事项: 如果你在 从库(Replica)上执行导出,强烈建议加上
--master-data=2(在8.0.26后改为--source-data=2)。这样导出的文件里会记录当前备库回放到的主库位点。另外,如果备库压力大,记得确认slave_parallel_workers,有时候为了绝对的一致性快照,可以临时STOP SLAVE SQL_THREAD;,导完再开。
导出内容留白(文件长啥样?): 打开 fin_prod_metadata.sql,你会发现:
-
没有一行
INSERT INTO...。 -
每个表后面紧跟
CREATE TABLE。 -
文件末尾会有大量的
/*!50003 CREATE*/ /*!50003 TRIGGER...这样的注释块,那是 MySQL 在保护不同版本间的兼容性。
3 现代派 - MySQL Shell (mysqlsh) 降维打击
兄弟们,如果你还在死守 mysqldump,那可能真的要落伍了。MySQL Shell (mysqlsh) 才是现在的“正宫娘娘”。
MySQL Shell 是官方推出的高级客户端,支持 JS/Python/SQL 三种模式。它的 Dump & Load 实用程序比传统 dump 快几个量级,因为它:
-
下载资源: MySQL 官网下载页(选择对应的 RHEL8 版本)。
Archives中可以找到历史版本
解压安装
# tar -zxf mysql-shell-8.0.22-linux-glibc2.12-x86-64bit.tar.gz
# ls mysql-shell-8.0.22-linux-glibc2.12-x86-64bit
bin lib share
# mv mysql-shell-8.0.22-linux-glibc2.12-x86-64bit /usr/local/mysql-shell-8.0.22
# export PATH=/usr/local/mysql-shell-8.0.22/bin:$PATH
# mysqlsh --version
mysqlsh Ver 8.0.22 for Linux on x86_64 - for MySQL 8.0.22 (MySQL Community Server (GPL))
使用 mysqlsh 导出元数据
在 Linux Shell 下调用 mysqlsh 的 API,正确姿势是使用 -- 触发 API 命令行集成,或者用 -e 执行 JS 脚本。
# 注意:JSON 数组参数需要用引号包裹 - 导出schema
mysqlsh meb_backup@192.168.31.102:3306 -e "
util.dumpSchemas(['fin_prod'], '/tmp/metadata_dump', {
ddlOnly: true,
threads: 4
})"
## 如果碰到报错 MySQL Error 1045: Access denied for user 'root'@'::1' (using password: YES)
## 说明客户端实际连接到了 IPv6 的 ::1, 要单独给 root@::1授权或者走本地socket
## JS 脚本执行模式
mysqlsh --socket=/tmp/mysql.sock root@localhost
Please provide the password for 'meb_backup@192.168.31.102:3306': ******************
Save password for 'meb_backup@192.168.31.102:3306'? [Y]es/[N]o/Ne[v]er (default No):
Acquiring global read lock
Global read lock acquired
All transactions have been started
Locking instance for backup
Global read lock has been released
Writing global DDL files
Writing DDL for schema `fin_prod`
Writing DDL for table `fin_prod`.`acc_balance`
Writing DDL for table `fin_prod`.`sys_audit_log`
Writing DDL for table `fin_prod`.`sys_user`
Writing DDL for table `fin_prod`.`trx_order`
Running data dump using 4 threads.
NOTE: Progress information uses estimated values and may not be accurate.
0% (0 rows / ~6 rows), 0.00 rows/s, 0.00 B/s uncompressed, 0.00 B/s compressed
Duration: 00:00:00s
Schemas dumped: 1
Tables dumped: 4
Uncompressed data size: 0 bytes
Compressed data size: 0 bytes
Compression ratio: 0.0
Rows written: 0
Bytes written: 0 bytes
Average uncompressed throughput: 0.00 B/s
Average compressed throughput: 0.00 B/s
执行完后,进入 /tmp/metadata_dump,你会看到:
fin_prod.json:库的定义。fin_prod@sys_user.sql:单独的表 DDL 文件。@.json:整个导出任务的元数据。@.sql:用户授权相关的语句(如果开启了 users 选项)。
# ls -l /tmp/metadata_dump/
total 60
-rw-r----- 1 root root 108 Jan 16 13:36 @.done.json
-rw-r----- 1 root root 645 Jan 16 13:36 fin_prod@acc_balance.json
-rw-r----- 1 root root 1023 Jan 16 13:36 fin_prod@acc_balance.sql
-rw-r----- 1 root root 593 Jan 16 13:36 fin_prod.json
-rw-r----- 1 root root 4662 Jan 16 13:36 fin_prod.sql
-rw-r----- 1 root root 622 Jan 16 13:36 fin_prod@sys_audit_log.json
-rw-r----- 1 root root 823 Jan 16 13:36 fin_prod@sys_audit_log.sql
-rw-r----- 1 root root 673 Jan 16 13:36 fin_prod@sys_user.json
-rw-r----- 1 root root 1137 Jan 16 13:36 fin_prod@sys_user.sql
-rw-r----- 1 root root 662 Jan 16 13:36 fin_prod@trx_order.json
-rw-r----- 1 root root 1027 Jan 16 13:36 fin_prod@trx_order.sql
-rw-r----- 1 root root 648 Jan 16 13:36 @.json
-rw-r----- 1 root root 245 Jan 16 13:36 @.post.sql
-rw-r----- 1 root root 245 Jan 16 13:36 @.sql
可以参考如下带用户和排除系统数据库的脚本 - 导出实例
mysqlsh --socket=/tmp/mysql.sock root@localhost -e "
util.dumpInstance('/tmp/metadata_full_dump', {
excludeSchemas: ['mysql','information_schema','performance_schema','sys'],
users: true,
ddlOnly: true,
events: true,
routines: true,
triggers: true,
threads: 4
})"
## 结果如下
Please provide the password for 'root@/tmp%2Fmysql.sock': ****************
Save password for 'root@/tmp%2Fmysql.sock'? [Y]es/[N]o/Ne[v]er (default No):
Acquiring global read lock
Global read lock acquired
All transactions have been started
Locking instance for backup
Global read lock has been released
Writing global DDL files
Writing users DDL
Writing DDL for schema `fin_prod`
Writing DDL for table `fin_prod`.`acc_balance`
Writing DDL for table `fin_prod`.`sys_audit_log`
Writing DDL for table `fin_prod`.`sys_user`
Writing DDL for table `fin_prod`.`trx_order`
Writing DDL for schema `sbtest`
...
Running data dump using 4 threads.
NOTE: Progress information uses estimated values and may not be accurate.
0% (0 rows / ~987.13K rows), 0.00 rows/s, 0.00 B/s uncompressed, 0.00 B/s compressed
Duration: 00:00:00s
Schemas dumped: 2
Tables dumped: 14
Uncompressed data size: 0 bytes
Compressed data size: 0 bytes
Compression ratio: 0.0
Rows written: 0
Bytes written: 0 bytes
Average uncompressed throughput: 0.00 B/s
Average compressed throughput: 0.00 B/s
# ls -l /tmp/metadata_full_dump
total 156
-rw-r----- 1 root root 108 Jan 16 13:31 @.done.json
-rw-r----- 1 root root 645 Jan 16 13:31 fin_prod@acc_balance.json
-rw-r----- 1 root root 1018 Jan 16 13:31 fin_prod@acc_balance.sql
-rw-r----- 1 root root 593 Jan 16 13:31 fin_prod.json
-rw-r----- 1 root root 4647 Jan 16 13:31 fin_prod.sql
-rw-r----- 1 root root 622 Jan 16 13:31 fin_prod@sys_audit_log.json
-rw-r----- 1 root root 818 Jan 16 13:31 fin_prod@sys_audit_log.sql
-rw-r----- 1 root root 673 Jan 16 13:31 fin_prod@sys_user.json
-rw-r----- 1 root root 1132 Jan 16 13:31 fin_prod@sys_user.sql
-rw-r----- 1 root root 662 Jan 16 13:31 fin_prod@trx_order.json
-rw-r----- 1 root root 1022 Jan 16 13:31 fin_prod@trx_order.sql
-rw-r----- 1 root root 911 Jan 16 13:31 @.json
-rw-r----- 1 root root 240 Jan 16 13:31 @.post.sql
-rw-r----- 1 root root 796 Jan 16 13:31 sbtest.json
-rw-r----- 1 root root 588 Jan 16 13:31 sbtest@sbtest10.json
...
-rw-r----- 1 root root 805 Jan 16 13:31 sbtest@sbtest9.sql
-rw-r----- 1 root root 561 Jan 16 13:31 sbtest.sql
-rw-r----- 1 root root 240 Jan 16 13:31 @.sql
-rw-r----- 1 root root 6167 Jan 16 13:31 @.users.sql
4 用户与权限的“完美克隆”
很多兄弟发现,mysqldump 导完了,表也有了,过程也有了,结果应用连不上。为什么?因为 mysqldump 默认是不导 mysql.user 表和权限的!
在 Oracle 里,权限随 Schema 走;但在 MySQL 里,用户和权限是全局的。
方案 A:利用 SQL 拼接(老兵常用骚操作)
如果是在 MySQL 8.0 环境下,我们可以利用 information_schema 配合 SHOW CREATE USER。这里笔者给大家贡献一个珍藏的 SQL 脚本,直接执行,它会生成全库所有用户的创建语句和授权语句。
-- 生成创建用户和授权的 SQL
SELECT CONCAT('SHOW CREATE USER ''', user, '''@''', host, ''';') AS _sql FROM mysql.user
WHERE user NOT IN ('mysql.sys', 'mysql.session', 'mysql.infoschema', 'root')
UNION ALL
SELECT CONCAT('SHOW GRANTS FOR ''', user, '''@''', host, ''';') FROM mysql.user
WHERE user NOT IN ('mysql.sys', 'mysql.session', 'mysql.infoschema', 'root');
# 这个命令会自动提取非系统用户,生成他们的创建和授权语句并存入脚本
# 1. 生成可执行文件 user_ddl.sql
mysql -uroot -p'RootPassword123!' -BNe \
"SELECT CONCAT('SHOW CREATE USER ''', user, '''@''', host, ''';') FROM mysql.user \
WHERE user NOT IN ('mysql.sys','mysql.session','mysql.infoschema','root');" \
| mysql -uroot -p'RootPassword123!' -BN \
| sed 's/Create user://I;s/^ */CREATE /;s/$/;/' \
> user_ddl.sql
# 2. 生成授权语句追加到同一文件
mysql -uroot -p'RootPassword123!' -BNe \
"SELECT CONCAT('SHOW GRANTS FOR ''', user, '''@''', host, ''';') FROM mysql.user \
WHERE user NOT IN ('mysql.sys','mysql.session','mysql.infoschema','root');" \
| mysql -uroot -p'RootPassword123!' -BN \
| grep -v '^Grants for ' \
>> user_ddl.sql
注意:针对于导出来的密码字段检查下,是否会生成乱码或者换行,导入时会出现报错!
执行这些生成的语句,你会得到完整的 CREATE USER 和 GRANT 语句。
方案 B:MySQL Shell 完美克隆
其实 mysqlsh 的 dumpInstance 是可以包含用户的,只要你别把用户过滤掉:
mysqlsh meb_backup@192.168.31.102:3306 -e "
util.dumpInstance('/tmp/user_dump', {
includeUsers: ['app_user', 'report_user'],
ddlOnly: true
})"
解决 DEFINER 权限溢出问题的“神操作”
这是迁移中最容易踩的坑。导出来的存储过程、视图开头往往带个 DEFINER=root@10.0.0.1”。到新环境后,可能根本没这个 IP,导致执行报错。
笔者教你一招:批量替换。
在 Linux 命令行下,对导出的 .sql 文件直接进行 sed 替换:
CODE_BLOCK_9
5 恢复实操与一致性验证
导出是为了更好的恢复。我们模拟将元数据恢复到一个全新的 Red Hat 8 实例中。
恢复实操
- 方法 A:传统恢复(对应 mysqldump)
mysql -u root -p < fin_prod_metadata.sql
- 方法 B:高速恢复(对应 mysqlsh) CODE_BLOCK_11
终极验证:如何确保“一模一样”?
迁移完结构,不能光凭感觉。我们要从对象数量、关键属性、权限覆盖三个维度进行“像素级”对账。
对账 SQL 清单(在源库和目标库分别执行并对比):
(1) 全量对象对账 (表、视图) CODE_BLOCK_12
(2) 逻辑对象对账 (存储过程、函数、触发器、事件) CODE_BLOCK_13
(3) 用户与权限对账 CODE_BLOCK_14
(4) 结构一致性深度验证 (进阶) 如果担心某个表的字段类型或者索引丢了,可以利用 information_schema.COLUMNS 计算一个简单的 Checksum: CODE_BLOCK_15
推荐使用开源工具 mysqldiff(MySQL Utilities 里的)或者业界标配的 pt-table-checksum 的变种逻辑。如果没有工具,可以手动对两个库的 information_schema.columns 进行 MD5 摘要比对。
总结
兄弟们,总结一下:
- 小规模、快速操作:用
mysqldump,记得带上-d -R -E --triggers。 - 大规模、追求效率与云原生:首选
MySQL Shell,ddlOnly: true是你的好帮手。 - 用户权限:别指望 dump 能帮你搞定一切,学会 SQL 拼接或者用 shell 的
includeUsers。 - 最重要的一点:永远不要忘记处理 DEFINER 问题。
备份结构是第一步,能跑通业务才是真功夫。
迁移元数据就像是给新房子搭框架,框架不稳,装修(导入数据)再漂亮也没用。希望今天这篇实战笔记能帮大家少走弯路,把迁移做得丝滑顺畅。