ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Hibernate实战:MySQL到Oracle数据迁移的踩坑与性能优化

Hibernate实战:MySQL到Oracle数据迁移的踩坑与性能优化 上个月帮朋友公司做了一次从MySQL到Oracle的数据迁移整个过程大概花了两周期间Hibernate这个ORM框架算是我又爱又恨的主角——用得好它能把你从逐表手写SQL的泥潭里拉出来用不好它能在300万行数据面前直接OOM给你看。数据迁移这类活儿基本上是后端开发里最不受待见、却又绕不开的日常之一。今天这篇就专门聊聊怎么把Hibernate真正用在数据迁移场景里从环境准备、核心代码、性能调优到那些只有实操作业才能发现的坑一次性整理出来。如果你最近也在筹备数据库搬家或者只是单纯想看看Hibernate除了日常CRUD还能干什么这篇应该能给你一些可落地的参考。1. 数据迁移的总体思路Hibernate适不适合干这个活1.1 数据迁移到底难在哪先说说数据迁移这件事本身。很多人第一反应是“不就是把数据从A库拷到B库吗”真上手了才发现这句话能把自己坑死。数据迁移的难点从来不在“拷”这个动作而在“映射”和“一致性”。映射指的是源库的表结构、字段类型、主键策略、关系约束跟目标库往往不对齐。比如MySQL的TINYINT(1)在Oracle里没有完全对等的类型DATETIME在Oracle里通常要变成TIMESTAMPAUTO_INCREMENT跟Oracle的SEQUENCE完全是两套逻辑。你以为是表跟表之间复制实际上是两套数据字典在做翻译。一致性就更麻烦了。迁移过程中数据不能丢、不能重、不能错主外键关系要保持完整自增ID要延续关联表的数据顺序不能乱这几个条件叠加在一起任何一步失误都会让下游业务直接翻车。还有一个隐患是数据量。几百行数据的“搬家”跟几百万行的“迁移”是两个物种前者怎么写都行后者要是不考虑批处理、事务边界、内存占用跑一半就给你抛OutOfMemoryError或者数据库连接池被打满整个应用假死。1.2 什么时候该用Hibernate做迁移什么时候不该先说结论Hibernate不是所有数据迁移场景的最优解但它在一个特定区间内非常顺手。如果你是一个遗留系统业务逻辑全部跑在Hibernate的实体模型之上项目里已经有几十个实体类、映射关系、HQL查询这时候用Hibernate做迁移是最省力的——因为实体映射早就写好了你只需要写一套“读旧库、写新库”的逻辑不用重新发明轮子。尤其是表关系复杂的系统手动维护外键顺序会让你崩溃而Hibernate的级联操作和对象图遍历能帮你省掉大量排序工作。反过来如果你的迁移非常简单就是几张大宽表做全量复制或者需要做非常复杂的ETL清洗、聚合转换那我建议你直接用原生SQL、DataX、Kettle之类的工具没必要让ORM掺和进来。Hibernate的优势在对象模型不在SQL性能和复杂转换上。用错地方只会给自己添堵。一句话总结你要迁移的数据如果业务上已经有一层完整的Hibernate实体模型可以复用那就用得上如果迁移本身只是一次性的临时脚本目标也不是复用领域对象那别硬蹭。1.3 迁移方案的总体框架不管用不用Hibernate数据迁移项目的骨架通常都是这五步盘点梳理源库表清单、数据量、主外键关系、字段类型差异。映射定义源到目标的对象映射规则包括类型转换、默认值填充、字段改名。执行按依赖顺序分批次读取源数据、写入目标库。校验对比源和目标的数据条数、关键字段摘要、关联完整性。切换停止写入、做增量补数、切换数据源、回归验证。Hibernate在这个框架里主要承担第2步和第3步的“对象模型翻译”角色。你只要把映射关系配好剩下的读写逻辑可以用一套通用的代码模板反复复用。2. 迁移前准备依赖、连接池与双数据源配置2.1 依赖组合与版本选型Hibernate的版本选择在第一道关口就会劝退一批人。目前市面上存量项目里Hibernate 3、4、5都还大量存在尤其是老一点的技术栈常见组合是Struts2 Hibernate c3p0这种搭配在中小型遗留系统里非常经典。如果你正好接手这种项目建议不要贸然升级Hibernate版本迁移工具跟业务代码跑在同一个容器里升级带来的兼容性问题远比你解决数据迁移本身更耗时。如果是新写一个迁移工具直接用Hibernate 5.6.x或者6.x都可以。5.6是最后一个支持javax.persistence的稳定分支6.x全面切到jakarta.persistence选哪个取决于你项目的其他依赖是否已经迁移到Jakarta命名空间。跨界迁移工具我一般建议保守一点5.6.15.Final就够用了。依赖层面核心就是这几样dependency groupIdorg.hibernate/groupId artifactIdhibernate-core/artifactId version5.6.15.Final/version /dependency dependency groupIdorg.hibernate/groupId artifactIdhibernate-c3p0/artifactId version5.6.15.Final/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.oracle.database.jdbc/groupId artifactIdojdbc8/artifactId version21.9.0.0/version /dependency2.2 连接池选型c3p0还是HikariCP老项目里c3p0太常见了因为它跟Hibernate的整合历史最久配置简单。但说实话c3p0在高并发下的性能表现一般连接回收机制偶尔会抽风。我自己在迁移工具里会更倾向于HikariCP如果你读过它的文档就知道它把一个连接从池里拿出来的耗时压到了微秒级对迁移这种高频读写场景非常友好。当然如果你迁移工具要跑在已有的Struts2 Hibernate c3p0项目里那就别另外再造一套连接池直接用项目里的c3p0配置改一改就行。迁移脚本不是高并发业务c3p0的稳定性完全够用只是要把超时时间调长一点防止大事务把连接占用太久导致连接池饿死。c3p0的Hibernate配置长这样hibernate.c3p0.min_size2 hibernate.c3p0.max_size20 hibernate.c3p0.timeout300 hibernate.c3p0.max_statements50 hibernate.c3p0.idle_test_period120 hibernate.c3p0.acquire_increment2有一个比较容易忽略的坑迁移大表的时候单条事务可能持续几十秒甚至几分钟如果连接池里的连接被timeout回收了Hibernate会报Connection is closed之类的异常。所以做迁移时把timeout和idle_test_period适当调大或者干脆临时把连接池上限放大迁移完再改回来。2.3 双数据源的Hibernate配置迁移工具的核心就是同时连接源库和目标库。Hibernate本身只支持单一SessionFactory对应一个数据库所以最直接的做法是配置两个SessionFactory一个指向源库一个指向目标库。如果项目接入了Spring可以这么做bean idsourceSessionFactory classorg.springframework.orm.hibernate5.LocalSessionFactoryBean property namedataSource refsourceDataSource/ property namepackagesToScan valuecom.example.entity.source/ property namehibernateProperties props prop keyhibernate.dialectorg.hibernate.dialect.MySQL8Dialect/prop prop keyhibernate.show_sqlfalse/prop /props /property /bean bean idtargetSessionFactory classorg.springframework.orm.hibernate5.LocalSessionFactoryBean property namedataSource reftargetDataSource/ property namepackagesToScan valuecom.example.entity.target/ property namehibernateProperties props prop keyhibernate.dialectorg.hibernate.dialect.Oracle12cDialect/prop prop keyhibernate.show_sqlfalse/prop /props /property /bean需要注意packagesToScan要区分开。如果源库和目标库的实体类都叫User、Order包的路径就必须分开否则Hibernate扫描到同名类会直接启动失败。我习惯的做法是源库实体放entity.source包目标库实体放entity.target包两边代码长得几乎一样但类名确实不冲突。如果不经Spring用Hibernate原生API创建两个SessionFactory也完全可行Configuration sourceCfg new Configuration().configure(hibernate-source.cfg.xml); SessionFactory sourceFactory sourceCfg.buildSessionFactory(); Configuration targetCfg new Configuration().configure(hibernate-target.cfg.xml); SessionFactory targetFactory targetCfg.buildSessionFactory();核心就是两套独立的配置文件、两个独立的SessionFactory各管各的库。3. 核心迁移逻辑的完整实现3.1 实体映射与字段对齐迁移的第一步是核对源库实体和目标库实体之间的字段映射。这个地方最容易犯的错误是“觉得两个字段名字一样就一定是同一个意思”。举个例子源库的User表有个字段叫status类型是TINYINT0表示正常、1表示禁用。目标库的User表也有个status字段但类型是VARCHAR(10)存的是字符串ACTIVE、DISABLED。这种情况在Hibernate实体里怎么映射最稳妥的做法是在源实体的getter里做转换或者写一个字段对齐的转换器。public class TargetUser { private Long id; private String userName; private String status; // 省略其他字段 } // 转换逻辑 TargetUser toTarget(SourceUser src) { TargetUser t new TargetUser(); t.setUserName(src.getUserName()); t.setStatus(src.getStatus() 0 ? ACTIVE : DISABLED); return t; }字段对齐时还要注意数据库方言差异。比如MySQL的DATETIME映射到Java的java.util.Date写进Oracle的时候Oracle方言可能会自动转成TIMESTAMP这没问题。但如果你源库字段是TIMESTAMP且带小数秒而目标库字段是DATEOracle里DATE不带时间只有时分秒那就要小心精度丢失。迁移前最好把源和目标的时间字段精度清单拉出来逐一对一遍。3.2 分页读取与批量写入核心迁移逻辑我推荐一个模式源库分页读取 目标库批量写入。第一步在源库用Hibernate做分页查询设置合理的setFirstResult和setMaxResults每批读500到1000条。第二步把这些对象转成目标实体通过目标SessionFactory批量保存。int pageSize 500; int page 0; long totalMigrated 0; while (true) { Session sourceSession sourceFactory.openSession(); Session targetSession targetFactory.openSession(); Transaction tx targetSession.beginTransaction(); try { QuerySourceUser query sourceSession.createQuery(from SourceUser, SourceUser.class); query.setFirstResult(page * pageSize); query.setMaxResults(pageSize); ListSourceUser list query.list(); if (list.isEmpty()) { tx.commit(); break; } for (SourceUser src : list) { TargetUser target toTarget(src); targetSession.save(target); } tx.commit(); totalMigrated list.size(); page; } catch (RuntimeException e) { tx.rollback(); throw e; } finally { sourceSession.close(); targetSession.close(); } if (totalMigrated % 5000 0) { System.out.println(已迁移: totalMigrated); } }这一段代码看起来很朴素但有几个细节非常重要。第一个细节每个批次都要新开Session。Hibernate的Session不是线程安全的也不该跨大量数据复用。每批开启新Session处理完就关掉能避免一级缓存越来越大导致内存溢出。第二个细节每个批次内用独立事务。如果500条一批内出问题回滚的代价是可接受的。千万别把百万条数据放在一个事务里提交否则数据量大时目标数据库的undo/redo日志会爆炸而且一旦出错全盘回滚等于白跑。第三个细节page号不要用setFirstResult自己算。上面代码里我用了page * pageSize其实还有一种更稳妥的游标方式下面会讲。3.3 迁移过程中的状态记录与断点续传大表迁移最怕跑到一半崩了。别天真地以为不会崩——内存溢出、数据库锁冲突、网络超时任何一个都能让迁移过程挂掉。如果迁移脚本没有记录状态挂了之后只能从头跑那心情基本是崩溃的。我建议在任何超过10万条数据的迁移脚本里都加一张“迁移进度表”记录每个表的迁移进度和最后处理的ID位置。比如CREATE TABLE migration_progress ( table_name VARCHAR(100) PRIMARY KEY, last_processed_id BIGINT, total_count BIGINT, status VARCHAR(20), updated_time TIMESTAMP );每次批处理完成后更新last_processed_id下次启动时从这里继续。配合where id last_processed_id的方式翻页而不是用offset翻页因为offset在数据量大的时候性能会越来越差而且如果源库数据还在变会翻出重复或遗漏。QuerySourceUser query sourceSession.createQuery( from SourceUser where id :lastId order by id asc, SourceUser.class); query.setParameter(lastId, lastProcessedId); query.setMaxResults(pageSize);这种“键集分页”的方式比setFirstResult靠谱得多。MySQL和Oracle都支持唯一的要求就是你得有一个稳定的排序字段通常是主键。4. 性能优化如何让迁移不再慢如蜗牛4.1 批量插入参数调优Hibernate的批量写入如果不做配置默认是一条一条insert的性能惨不忍睹。几千条数据感觉还行几十万条的时候能跑到你怀疑人生。要让Hibernate真正执行批量插入需要在hibernate配置里打开几个开关hibernate.jdbc.batch_size50 hibernate.order_insertstrue hibernate.order_updatestrue hibernate.jdbc.batch_versioned_datatrueorder_insertstrue的作用很关键它让Hibernate把相同类型的insert语句排在一起这样JDBC驱动才能真正复用PreparedStatement达到批量提交的效果。如果不开启Hibernate可能会把不同实体的insert穿插执行批量就失效了。batch_size不是越大越好。50是一个比较稳妥的起步值Oracle和MySQL的驱动对batch size的容忍度不太一样建议从50试起逐步加大到200左右观察数据库负载和迁移速率找一个性能曲线不再上升的拐点那就是最适合你的值。4.2 关闭二级缓存与查询缓存迁移脚本里二级缓存和查询缓存完全用不上反而白白增加内存消耗和序列化开销。如果你在项目里启用了这些缓存迁移时一定要关掉hibernate.cache.use_second_level_cachefalse hibernate.cache.use_query_cachefalse hibernate.cache.region.factory_classorg.hibernate.cache.internal.NoCachingRegionFactory关闭缓存还有另一个好处避免把旧库的数据缓存内容带到迁移流程里减少脏数据出现的可能性。尤其当你循环读取源库数据时不希望Hibernate从缓存里返回与数据库不一致的数据。4.3 合理设置事务边界事务边界的设计直接影响迁移效率和失败恢复的复杂度。我见过有人图省事每读取一条数据就提交一次事务结果几百万条数据的迁移跑了十几个小时而且每次commit都要刷新磁盘日志性能损失巨大。反过来把整个表包在一个大事务里又会在目标库产生巨大的undo日志。我个人的经验值是每500到1000条数据提交一次事务这个区间在“提交频率”和“回滚粒度”之间比较平衡。如果单条记录字段特别多、数据特别大那就下调到200条如果是简单的宽表可以上提到2000条。事务边界的另一个考虑因素是被迁移表是否存在自增主键或者序列。如果每次提交之后序列会跳号那也没关系迁移过程中主键连续本来就不是必须的只要不冲突就行。4.4 大数据量下的流式读取分页查询在数据量特别大的时候有一个隐患Hibernate会先把结果加载到内存再返回。即使你设置了setMaxResults(1000)它内部依然会建一个包含全部结果集的PreparedStatement只是通过数据库分页来限制返回的行数。这个在MySQL和Oracle下都还好毕竟LIMIT和ROWNUM是数据库层面处理的。但如果你要遍历一张全表做数据校正根本不想分页直接用list()一次性把500万条数据装进内存那就等着OOM吧。这种情况下可以用ScrollableResults它通过数据库游标逐行读取内存占用是恒定的Session session sourceFactory.openSession(); ScrollableResults results session.createQuery(from SourceUser) .setFetchSize(200) .scroll(ScrollMode.FORWARD_ONLY); int count 0; while (results.next()) { SourceUser src (SourceUser) results.get(0); TargetUser target toTarget(src); targetSession.save(target); count; if (count % 500 0) { targetSession.flush(); targetSession.clear(); } } results.close(); session.close();setFetchSize告诉JDBC驱动每次从数据库捞多少行到客户端Oracle的默认fetch size是10小到令人发指。我一般会设置成100到200。另外用完ScrollableResults一定要close不然数据库游标资源会一直挂着。5. 常见问题与排查实录5.1 日期夏令时与时区问题“hibernate日期夏令时报错”这个话题在网络上热度一直不低我实际迁移时也踩过类似的坑。现象很典型源库存的时间是2023-03-12 02:30:00Hibernate读出来成2023-03-12 03:30:00或者反过来少了1小时夏令时切换前后时间对不上后续业务数据计算全乱。这个坑的根源在于Java的Date本身跟时区绑定而数据库的DATETIME/DATE在MySQL里通常不包含时区信息。Hibernate在读写时需要决定怎么转换一旦hibernate.jdbc.time_zone配置不对或者连接串里的serverTimezone没设置好就会出现小时偏移。我建议的排查顺序是先看数据库连接串里是否配置了时区比如MySQL的jdbc:mysql://host:3306/db?serverTimezoneAsia/Shanghai。再看Hibernate配置里是否有hibernate.jdbc.time_zoneAsia/Shanghai。最后看实体字段用的是什么Java类型java.util.Date、java.time.LocalDateTime还是java.time.ZonedDateTime三种类型对时区的处理不一样。如果你用的是LocalDateTime那它本身不携带时区概念Hibernate读写时会直接映射数据库的日期时间基本不会出现偏移。但如果你用了ZonedDateTime或者Date就得保证源、目标数据库和JVM时区三者一致。实际迁移时我会统一把时间字段用LocalDateTime接收、写入彻底规避夏令时和时区换算问题。顺带说一个身边的类比手机上迁移照片到新手机图库显示的拍摄日期不对也经常是这个原因——照片元数据里存的是UTC时间手机系统按本地时区展示版本差异导致换算逻辑不同。数据迁移里的日期问题也是一样的道理存储层和展示层必须约定好时区。5.2 主键策略冲突主键冲突是迁移里出现频率最高的错误没有之一。源库MySQL用AUTO_INCREMENTID从1开始按顺序增长目标库Oracle用SEQUENCE如果你没处理好Hibernate会在插入时调nextval生成新的ID跟源库ID对不上关联表的外键也就全乱了。解决办法有两个第一种目标库也建立与源库ID范围一致的序列。比如源库ID最大值是100000那就创建Oracle序列从100001开始自增同时插入时显式指定ID。// 目标实体类中设置ID TargetUser target toTarget(src); target.setId(src.getId()); // 显式保留源ID targetSession.save(target);这样做的前提是实体里的GeneratedValue要改成GenerationType.IDENTITY或者ASSIGNED让Hibernate不要接管ID生成。显式赋值时我遇到过一个问题Oracle的trigger或sequence还是会在insert时自动覆盖ID这时候要去检查表上的触发器或者修改insert语句。第二种如果业务不要求保留源ID那就直接让目标库重新生成主键ID但你必须重新处理所有外键关联关系。这个方案只适合“关系很浅、可以全量重建”的数据主外键复杂的表不推荐。5.3 字符集乱码与特殊字符另一个高频问题就是乱码。MySQL到Oracle的迁移源库可能是utf8mb4目标库可能默认是AL32UTF8理论上是兼容的但实际总会遇到一些奇怪字符例如Emoji、生僻汉字、特殊符号。我建议迁移前先做一次字符集预检扫描源库里有没有超出目标库字符集范围的字符记录QuerySourceUser query sourceSession.createQuery(from SourceUser where userName is not null, SourceUser.class);然后循环检查userName里是否有不可见字符或代理对surrogate pair。如果发现异常先清洗再迁移不要在迁移过程中让Hibernate去处理因为框架报错信息往往不直观一条脏数据能让整个批次回滚。另外要特别检查应用服务器的文件编码尤其是Windows环境默认GBK经常会坑人。启动迁移工具时建议显式加上-Dfile.encodingUTF-8避免配置文件、日志文件、代码里的字符串常量出现隐式乱码。5.4 惰性加载与Session关闭迁移场景下最容易出现的一个问题是LazyInitializationException。当你从源库查出一个实体关闭了源Session再去访问它的关联集合属性时Hibernate会直接抛异常因为关联数据还没加载而Session已经没了。解决办法有三种在一个事务内完成读取和关联数据的访问不要提前关Session。查询时用JOIN FETCH或者Hibernate.initialize()把需要的关联属性提前加载。在映射的关联集合上设置fetch FetchType.EAGER但全局影响大迁移工具里不推荐。最稳妥的就是用JOIN FETCH。需要注意的是如果一个实体有多个关联集合同时对两个集合做JOIN FETCH会产生笛卡尔积数据量会成倍膨胀这种时候就得分步查询。5.5 常见问题速查表问题现象原因解决方案插入时主键冲突源库ID和目标库序列冲突显式赋值ID或重建序列日期差8小时/1小时时区或夏令时配置不一致统一用LocalDateTime配置serverTimezone和hibernate.jdbc.time_zone内存溢出OOMlist()一次性加载全部数据改用分页或ScrollableResultsLazyInitializationExceptionSession关闭后访问懒加载属性用JOIN FETCH或事务内访问乱码字符集不一致或启动参数问题预检字符加-Dfile.encodingUTF-8空指针出现在flush()之后批量模式下实体状态被clear及时保存需要的数据避免继续访问旧对象SQL方言不兼容MySQL和Oracle的Dialect差异目标库用正确的Dialect必要时调整映射类型6. 一个完整的迁移实例从MySQL到Oracle的Hibernate迁移6.1 场景描述朋友公司的项目是典型的Struts2 Hibernate c3p0老技术栈运行了五六年MySQL里的订单表已经到了800多万行。这次迁移要做的就是把这套系统从MySQL搬到Oracle不停机是不可能的但可以接受业务低峰期迁移加几个小时的维护窗口。我接到任务后第一步就是盘点。订单主表、订单明细表、用户表、商品表一共十几个核心表。数据量最大的订单明细表有2000多万行主表800多万行。关联关系复杂订单主表跟明细表是一对多用户表和订单表是一对多。面对这种规模我确定了几条基本原则用Hibernate复用现有实体模型但针对迁移建一套独立的目标实体。键集分页读取每次500条一批一批地迁移。目标库写入时显式保留源ID保证外键关联不被打乱。迁移前先处理字符集与时区问题。6.2 核心代码骨架整个迁移工具的主流程是这样组织的public class OrderMigrationRunner { private SessionFactory sourceFactory; private SessionFactory targetFactory; public void migrateOrders() { long lastId 0; int pageSize 500; int count 0; while (true) { try (Session srcSession sourceFactory.openSession(); Session tgtSession targetFactory.openSession()) { Transaction tx tgtSession.beginTransaction(); QuerySourceOrder q srcSession.createQuery( from SourceOrder where id :lastId order by id, SourceOrder.class); q.setParameter(lastId, lastId); q.setMaxResults(pageSize); ListSourceOrder orders q.list(); if (orders.isEmpty()) { tx.commit(); break; } for (SourceOrder src : orders) { TargetOrder tgt convertOrder(src); tgtSession.save(tgt); } lastId orders.get(orders.size() - 1).getId(); tx.commit(); count orders.size(); log.info(已迁移订单{}, count); } } } private TargetOrder convertOrder(SourceOrder src) { TargetOrder tgt new TargetOrder(); tgt.setId(src.getId()); tgt.setUserId(src.getUserId()); tgt.setOrderNo(src.getOrderNo()); tgt.setAmount(src.getAmount()); tgt.setStatus(src.getStatus() 1 ? PAID : UNPAID); tgt.setCreateTime(src.getCreateTime()); return tgt; } }明细表迁移时要注意先迁主表再迁明细表因为明细表外键指向主表。如果主表和明细表之间没有外键约束那倒是可以并行跑但并行迁移时目标库的写锁和序列竞争可能会成为瓶颈。6.3 过程中碰到的实际问题这次迁移里遇到的最恶心的问题跟数据本身无关而是Oracle的保留字。订单表里有个字段叫comment在MySQL不是保留字但在Oracle里是保留字。Hibernate生成的SQL里直接拼了comment导致Oracle报错“invalid identifier”。最后只能把目标实体的字段映射用Column(name comment_text)显式重命名才把问题解决。经验迁移前把源库的所有列名拉出来过一遍对照目标库的保留字列表提前处理冲突不要等跑批跑到一半才爆出来。还有一个教训是关于统计报表数据的。有个统计表的数据是聚合历史生成的迁移时我原以为是纯冗余可以直接跳过但后来发现报表页面对这些历史数据有依赖。还好我在迁移前跟业务方对了一次否则数据丢失可不是小事。数据迁移前业务依赖清单比技术方案还重要。总结一点个人体会把Hibernate用在数据迁移这件事上最核心的心得就是它带来的价值不是性能而是对象模型的一致性。当你的源系统本身就是Hibernate长期支撑的业务系统实体关系、级联逻辑、字段映射都已经经历过业务打磨迁移时直接复用这套模型远远比用SQL重写一遍更不容易出错。但也正是因为这样你更要盯紧那些Hibernate不会替你处理的部分——主键策略、时区、保留字、大事务边界。把这些坑提前填平迁移过程就会顺很多。最后再分享一个小技巧正式迁移前不管时间多紧都先在测试环境完整跑一遍小数据量的迁移演练把日志、耗时、数据校验都记录下来。演练一遍跟直接上生产的感觉完全不一样很多问题在演练时就会提前现形。数据迁移永远是“想得越细跑得越稳”这句话我每次做完一个迁移项目都想再说一遍。
返回列表