Oracle 迁移至 MySQL:50TB 级数据迁移方案
适用读者:DBA、架构师、项目经理 目标:提供超大规模(50TB+)Oracle 到 MySQL 数据迁移的完整方案,包括评估、架构设计、迁移实施、验证与优化。
1. 项目背景
1.1 迁移动机
- 成本优化:Oracle License 费用高昂(50TB 数据约需数百万/年)
- 云原生:MySQL 云服务成熟(AWS RDS/Aurora、阿里云 RDS)
- 开源生态:社区活跃,工具丰富
- 业务需求:微服务化改造,需要轻量级数据库
1.2 挑战与风险
| 挑战 | 影响 | 应对策略 |
|---|---|---|
| 数据量巨大(50TB) | 迁移时间长 | 分批迁移、并行传输 |
| 业务停机时间 | 服务中断 | 双写、逻辑复制、灰度切换 |
| 数据一致性 | 数据丢失/重复 | 校验工具、回滚预案 |
| 性能差异 | 查询变慢 | 索引优化、分库分表 |
| SQL 兼容性 | 应用改造 | SQL 改写、兼容层 |
| 存储过程迁移 | 业务逻辑重写 | 逐个迁移、测试 |
2. 迁移前评估
2.1 数据库对象清单
-- Oracle 端统计
-- 表数量与数据量
SELECT
owner,
COUNT(*) AS table_count,
ROUND(SUM(bytes)/1024/1024/1024/1024, 2) AS size_tb
FROM dba_segments
WHERE segment_type = 'TABLE'
AND owner NOT IN ('SYS', 'SYSTEM', 'SYSAUX')
GROUP BY owner
ORDER BY size_tb DESC;
-- 输出示例:
-- OWNER | TABLE_COUNT | SIZE_TB
-- APP_SCHEMA | 1200 | 35.6
-- LOG_SCHEMA | 450 | 12.3
-- ARCHIVE_SCHEMA | 230 | 2.1
-- 索引统计
SELECT
owner,
COUNT(*) AS index_count,
ROUND(SUM(bytes)/1024/1024/1024, 2) AS size_gb
FROM dba_segments
WHERE segment_type LIKE 'INDEX%'
AND owner NOT IN ('SYS', 'SYSTEM', 'SYSAUX')
GROUP BY owner;
-- 存储过程/函数/包
SELECT
owner,
object_type,
COUNT(*) AS object_count
FROM dba_objects
WHERE object_type IN ('PROCEDURE', 'FUNCTION', 'PACKAGE', 'TRIGGER')
AND owner NOT IN ('SYS', 'SYSTEM', 'SYSAUX')
GROUP BY owner, object_type
ORDER BY owner, object_type;
-- 分区表统计
SELECT
table_owner,
COUNT(DISTINCT table_name) AS partitioned_tables,
COUNT(*) AS partition_count
FROM dba_tab_partitions
WHERE table_owner NOT IN ('SYS', 'SYSTEM', 'SYSAUX')
GROUP BY table_owner;
2.2 业务分析
数据分类:
1. 核心业务数据(热数据):20TB
- 订单表、用户表、交易表
- 需要高可用、低延迟
- 优先迁移
2. 日志/归档数据(温数据):25TB
- 操作日志、审计日志
- 查询频率低
- 可考虑归档到对象存储(S3/OSS)
3. 历史数据(冷数据):5TB
- 3年前的历史记录
- 几乎不查询
- 迁移到冷存储或不迁移
2.3 兼容性评估
# 使用 MySQL Migration Toolkit 评估
# 或使用 AWS SCT (Schema Conversion Tool)
# 生成评估报告
aws sct create-assessment-report \
--source-endpoint oracle-source \
--target-endpoint mysql-target \
--output-file /tmp/assessment_report.pdf
# 报告包含:
# - 自动转换率(通常 70-85%)
# - 需要手动改写的对象列表
# - 预估工作量(人天)
# - 风险评估
3. 架构设计
3.1 目标架构
┌─────────────────────────────────────────────────────┐
│ 应用层(双写) │
│ Oracle (主) + MySQL (从) → MySQL (主) │
└─────────────────────────────────────────────────────┘
│
┌───────────────┼───────────────┐
│ │ │
┌───────▼──────┐ ┌──────▼──────┐ ┌─────▼───────┐
│ MySQL Shard1 │ │MySQL Shard2 │ │MySQL Shard3 │
│ (核心业务) │ │ (日志数据) │ │ (归档数据) │
│ 15TB │ │ 20TB │ │ 10TB │
└──────────────┘ └─────────────┘ └─────────────┘
│ │ │
└───────────────┼───────────────┘
│
┌───────▼────────┐
│ ProxySQL │
│ (读写分离) │
└────────────────┘
3.2 分库分表策略
-- 按业务模块分库
database_order -- 订单相关表
database_user -- 用户相关表
database_product -- 商品相关表
database_log -- 日志相关表
-- 大表分表(按时间/ID)
-- 订单表按月分表
orders_202501
orders_202502
orders_202503
...
-- 或按 ID 范围分表(Hash)
orders_0 -- user_id % 16 = 0
orders_1 -- user_id % 16 = 1
...
orders_15 -- user_id % 16 = 15
3.3 硬件规划
MySQL 集群配置(3个分片):
每个分片:
- 主库:64核 256GB 内存,20TB NVMe SSD(RAID 10)
- 从库1:64核 256GB 内存,20TB NVMe SSD
- 从库2:64核 256GB 内存,20TB NVMe SSD
网络:
- 万兆网卡(10Gbps)
- 专用迁移网络(避免影响生产)
存储:
- IOPS:50000+
- 吞吐量:2GB/s+
4. 迁移方案设计
4.1 迁移策略(分阶段)
阶段1:准备阶段(2周)
- 安装 MySQL 集群(主从复制)
- Schema 迁移(表、索引、约束)
- 应用代码适配(SQL 改写)
- 测试环境验证
阶段2:历史数据迁移(4周)
- 冷数据迁移(3年前数据)
- 温数据迁移(1-3年数据)
- 数据校验
阶段3:增量同步(2周)
- 使用 OGG/Debezium 实时同步
- 热数据迁移
- 双写验证
阶段4:灰度切换(1周)
- 10% 流量切换到 MySQL
- 50% 流量切换
- 100% 流量切换
- 监控告警
阶段5:稳定观察(2周)
- 性能监控
- 数据一致性校验
- Oracle 保留作为备份
4.2 数据迁移工具选型
| 工具 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| AWS DMS | 云上迁移 | 自动化、支持增量 | 需要 AWS 环境 |
| Oracle GoldenGate | 实时同步 | 低延迟、双向同步 | License 费用高 |
| Debezium (CDC) | 开源方案 | 免费、灵活 | 配置复杂 |
| DataX (阿里) | 批量迁移 | 高性能、并行 | 仅全量 |
| Kettle/Talend | ETL 工具 | 可视化、转换灵活 | 性能一般 |
| 自研脚本 | 定制需求 | 完全可控 | 开发成本高 |
推荐方案: - 全量迁移:DataX(并行)+ 自研脚本 - 增量同步:Oracle GoldenGate 或 Debezium
5. Schema 迁移
5.1 数据类型映射
| Oracle | MySQL | 说明 |
|---|---|---|
| NUMBER(p,s) | DECIMAL(p,s) | 精确数值 |
| NUMBER | DECIMAL(65,0) | 无精度 NUMBER |
| VARCHAR2(n) | VARCHAR(n) | 变长字符串 |
| CHAR(n) | CHAR(n) | 定长字符串 |
| DATE | DATETIME | Oracle DATE 包含时间 |
| TIMESTAMP | DATETIME(6) | 微秒精度 |
| CLOB | LONGTEXT | 大文本(4GB) |
| BLOB | LONGBLOB | 二进制(4GB) |
| RAW | VARBINARY | 二进制 |
| LONG | TEXT | 已废弃 |
5.2 自动化 Schema 转换
# 使用 AWS SCT 转换 Schema
aws sct convert-schema \
--source-endpoint oracle-source \
--target-endpoint mysql-target \
--output-dir /tmp/converted_schema
# 或使用 MySQL Workbench Migration Wizard
# GUI 工具,支持可视化转换
# 生成的 MySQL DDL 示例
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
amount DECIMAL(15,2),
status VARCHAR(20),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
5.3 分区表转换
-- Oracle 分区表
CREATE TABLE orders (
order_id NUMBER,
order_date DATE,
...
)
PARTITION BY RANGE (order_date) (
PARTITION p202301 VALUES LESS THAN (TO_DATE('2023-02-01', 'YYYY-MM-DD')),
PARTITION p202302 VALUES LESS THAN (TO_DATE('2023-03-01', 'YYYY-MM-DD')),
...
);
-- MySQL 分区表
CREATE TABLE orders (
order_id BIGINT,
order_date DATE,
...
)
PARTITION BY RANGE (TO_DAYS(order_date)) (
PARTITION p202301 VALUES LESS THAN (TO_DAYS('2023-02-01')),
PARTITION p202302 VALUES LESS THAN (TO_DAYS('2023-03-01')),
...
);
6. 数据迁移实施
6.1 使用 DataX 批量迁移
// datax_job.json
{
"job": {
"setting": {
"speed": {
"channel": 32,
"byte": 10485760
}
},
"content": [
{
"reader": {
"name": "oraclereader",
"parameter": {
"username": "app_user",
"password": "Oracle123#",
"column": ["order_id", "user_id", "amount", "created_at"],
"connection": [
{
"table": ["orders"],
"jdbcUrl": ["jdbc:oracle:thin:@192.168.1.10:1521:ORCL"]
}
],
"where": "created_at >= TO_DATE('2023-01-01', 'YYYY-MM-DD')"
}
},
"writer": {
"name": "mysqlwriter",
"parameter": {
"username": "root",
"password": "MySQL123#",
"column": ["order_id", "user_id", "amount", "created_at"],
"connection": [
{
"table": ["orders"],
"jdbcUrl": "jdbc:mysql://192.168.1.20:3306/mydb?useUnicode=true&characterEncoding=utf8"
}
],
"writeMode": "insert"
}
}
}
]
}
}
# 执行 DataX 任务
python datax.py datax_job.json
# 并行执行多个任务(按分区)
for i in {1..12}; do
python datax.py datax_job_2023_${i}.json &
done
wait
# 监控进度
tail -f datax.log
6.2 使用 Oracle GoldenGate 增量同步
# 在 Oracle 端配置 GoldenGate Extract
GGSCI> ADD EXTRACT ext1, TRANLOG, BEGIN NOW
GGSCI> ADD EXTTRAIL /u01/ogg/dirdat/lt, EXTRACT ext1
GGSCI> EDIT PARAMS ext1
# ext1.prm
EXTRACT ext1
USERID ggadmin, PASSWORD Oracle123#
EXTTRAIL /u01/ogg/dirdat/lt
TABLE app_schema.orders;
TABLE app_schema.users;
TABLE app_schema.products;
# 启动 Extract
GGSCI> START EXTRACT ext1
# 在 MySQL 端配置 Replicat
GGSCI> ADD REPLICAT rep1, EXTTRAIL /u01/ogg/dirdat/lt
GGSCI> EDIT PARAMS rep1
# rep1.prm
REPLICAT rep1
TARGETDB mydb, USERID root, PASSWORD MySQL123#
MAP app_schema.orders, TARGET mydb.orders;
MAP app_schema.users, TARGET mydb.users;
MAP app_schema.products, TARGET mydb.products;
# 启动 Replicat
GGSCI> START REPLICAT rep1
# 监控同步状态
GGSCI> INFO ALL
GGSCI> STATS EXTRACT ext1
GGSCI> STATS REPLICAT rep1
6.3 分批迁移脚本
#!/usr/bin/env python3
# migrate_batch.py
import cx_Oracle
import pymysql
from datetime import datetime, timedelta
# 配置
ORACLE_DSN = "192.168.1.10:1521/ORCL"
ORACLE_USER = "app_user"
ORACLE_PASS = "Oracle123#"
MYSQL_HOST = "192.168.1.20"
MYSQL_USER = "root"
MYSQL_PASS = "MySQL123#"
MYSQL_DB = "mydb"
BATCH_SIZE = 10000
TABLE_NAME = "orders"
def migrate_table_by_date_range(start_date, end_date):
"""按日期范围迁移数据"""
oracle_conn = cx_Oracle.connect(ORACLE_USER, ORACLE_PASS, ORACLE_DSN)
mysql_conn = pymysql.connect(host=MYSQL_HOST, user=MYSQL_USER,
password=MYSQL_PASS, database=MYSQL_DB)
oracle_cursor = oracle_conn.cursor()
mysql_cursor = mysql_conn.cursor()
# 查询 Oracle 数据
query = f"""
SELECT order_id, user_id, amount, status, created_at
FROM {TABLE_NAME}
WHERE created_at >= :start_date AND created_at < :end_date
ORDER BY order_id
"""
oracle_cursor.execute(query, start_date=start_date, end_date=end_date)
batch = []
total_rows = 0
while True:
rows = oracle_cursor.fetchmany(BATCH_SIZE)
if not rows:
break
# 批量插入 MySQL
insert_sql = f"""
INSERT INTO {TABLE_NAME}
(order_id, user_id, amount, status, created_at)
VALUES (%s, %s, %s, %s, %s)
"""
mysql_cursor.executemany(insert_sql, rows)
mysql_conn.commit()
total_rows += len(rows)
print(f"Migrated {total_rows} rows for {start_date} to {end_date}")
oracle_cursor.close()
mysql_cursor.close()
oracle_conn.close()
mysql_conn.close()
print(f"Completed: {total_rows} rows migrated")
# 按月分批迁移
start_date = datetime(2020, 1, 1)
end_date = datetime(2025, 12, 1)
current_date = start_date
while current_date < end_date:
next_date = current_date + timedelta(days=30)
print(f"Migrating data from {current_date} to {next_date}")
migrate_table_by_date_range(current_date, next_date)
current_date = next_date
7. 数据一致性校验
7.1 行数校验
#!/bin/bash
# check_row_count.sh
TABLES="orders users products transactions"
for table in $TABLES; do
echo "Checking table: $table"
# Oracle 行数
oracle_count=$(sqlplus -s app_user/Oracle123#@ORCL <<EOF
SET PAGESIZE 0 FEEDBACK OFF VERIFY OFF HEADING OFF ECHO OFF
SELECT COUNT(*) FROM $table;
EXIT;
EOF
)
# MySQL 行数
mysql_count=$(mysql -h192.168.1.20 -uroot -pMySQL123# -D mydb -N -e "SELECT COUNT(*) FROM $table")
echo "Oracle: $oracle_count, MySQL: $mysql_count"
if [ "$oracle_count" -eq "$mysql_count" ]; then
echo "✅ $table: PASS"
else
echo "❌ $table: FAIL (diff: $((oracle_count - mysql_count)))"
fi
done
7.2 数据校验(Checksum)
-- Oracle 端
SELECT
'orders' AS table_name,
COUNT(*) AS row_count,
SUM(ORA_HASH(order_id || user_id || amount)) AS checksum
FROM orders;
-- MySQL 端
SELECT
'orders' AS table_name,
COUNT(*) AS row_count,
SUM(CRC32(CONCAT(order_id, user_id, amount))) AS checksum
FROM orders;
7.3 使用 pt-table-checksum(Percona Toolkit)
# 需要在 Oracle 和 MySQL 之间建立数据桥接
# 或使用自定义脚本逐行对比
# 示例:对比关键字段
SELECT order_id, MD5(CONCAT(user_id, amount, status)) AS hash
FROM orders
ORDER BY order_id
LIMIT 1000;
8. 应用改造
8.1 SQL 改写
-- Oracle: DUAL 表
SELECT SYSDATE FROM DUAL;
-- MySQL
SELECT NOW();
-- Oracle: 序列
SELECT seq_order_id.NEXTVAL FROM DUAL;
-- MySQL: AUTO_INCREMENT
INSERT INTO orders (user_id, amount) VALUES (123, 99.99);
SELECT LAST_INSERT_ID();
-- Oracle: ROWNUM
SELECT * FROM orders WHERE ROWNUM <= 10;
-- MySQL: LIMIT
SELECT * FROM orders LIMIT 10;
-- Oracle: NVL
SELECT NVL(column_name, 'default') FROM table_name;
-- MySQL: IFNULL/COALESCE
SELECT IFNULL(column_name, 'default') FROM table_name;
-- Oracle: TO_DATE
SELECT * FROM orders WHERE created_at > TO_DATE('2025-01-01', 'YYYY-MM-DD');
-- MySQL: STR_TO_DATE
SELECT * FROM orders WHERE created_at > STR_TO_DATE('2025-01-01', '%Y-%m-%d');
-- Oracle: DECODE
SELECT DECODE(status, 1, 'Active', 2, 'Inactive', 'Unknown') FROM users;
-- MySQL: CASE
SELECT CASE status
WHEN 1 THEN 'Active'
WHEN 2 THEN 'Inactive'
ELSE 'Unknown'
END FROM users;
8.2 连接池配置
# Oracle (HikariCP)
spring.datasource.url=jdbc:oracle:thin:@192.168.1.10:1521:ORCL
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
# MySQL (HikariCP)
spring.datasource.url=jdbc:mysql://192.168.1.20:3306/mydb?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.maximum-pool-size=50
spring.datasource.hikari.minimum-idle=10
9. 性能优化
9.1 MySQL 参数调优(50TB 数据)
[mysqld]
# 内存配置(256GB 服务器)
innodb_buffer_pool_size = 180G # 70% 内存
innodb_log_file_size = 8G
innodb_log_buffer_size = 256M
# IO 优化
innodb_io_capacity = 20000
innodb_io_capacity_max = 40000
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 2 # 性能优先
# 并发优化
innodb_thread_concurrency = 0
innodb_read_io_threads = 16
innodb_write_io_threads = 16
max_connections = 2000
# 表空间
innodb_file_per_table = 1
innodb_data_file_path = ibdata1:1G:autoextend
9.2 索引优化
-- 分析慢查询
SELECT
query_time,
lock_time,
rows_examined,
sql_text
FROM mysql.slow_log
ORDER BY query_time DESC
LIMIT 20;
-- 创建复合索引
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at);
-- 删除未使用索引
SELECT
table_schema,
table_name,
index_name
FROM information_schema.statistics
WHERE table_schema = 'mydb'
AND index_name NOT IN (
SELECT DISTINCT index_name
FROM sys.schema_unused_indexes
);
10. 切换与回滚
10.1 灰度切换方案
第1天:10% 流量切换
- 配置 Nginx/HAProxy 路由规则
- 监控 MySQL 性能
- 对比 Oracle 和 MySQL 查询结果
第3天:30% 流量切换
- 扩大切换范围
- 持续监控
第7天:50% 流量切换
- 一半流量验证
第10天:100% 流量切换
- 全量切换
- Oracle 保留7天作为备份
10.2 回滚预案
# 触发条件:
# - 数据不一致
# - 性能严重下降(> 50%)
# - 功能异常
# 回滚步骤:
1. 立即切换流量回 Oracle
2. 停止 MySQL 写入
3. 分析失败原因
4. 修复问题后重新切换
11. 监控与告警
-- MySQL 关键指标
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';
SHOW GLOBAL STATUS LIKE 'Slow_queries';
-- 复制延迟(如果使用主从)
SHOW SLAVE STATUS\G
12. 最佳实践总结
- 分阶段迁移:历史数据 → 增量同步 → 灰度切换
- 并行传输:利用 DataX 多通道并行
- 数据校验:行数 + Checksum 双重校验
- 性能测试:迁移前后性能对比
- 回滚预案:保留 Oracle 数据7-14天
- 监控告警:实时监控 MySQL 性能指标
- 文档记录:记录所有改动和配置
参考工具: - AWS DMS: https://aws.amazon.com/dms/ - Oracle GoldenGate: https://www.oracle.com/goldengate/ - DataX: https://github.com/alibaba/DataX - Debezium: https://debezium.io/