Ruby on Rails如何跟踪表数据变更迁移 支持失败断点续跑
Rails下该场景的最优实现(符合Rails惯例)
你的核心思路(断点续跑、幂等、任务和migration解耦)是对的,只是有几个点可以调整得更贴合Rails设计习惯,同时规避生产环境常见坑:
- 不建议单独建
MigrationTrack表做进度跟踪:最轻量的方式是直接给要更新的目标表TableToUpdate加一个临时布尔字段做标记,比跨表join查询效率高很多,后续清理也更方便。 - 绝对不要一次性把10万行数据全量拉到内存遍历,Rails内置了批量遍历的
find_each方法,默认按主键分批每批加载1000条,完全不会撑爆应用内存,是Rails里处理大表批量更新的标准写法。 - 不需要为临时跟踪逻辑单独建持久化Model文件,避免后续清理的时候漏删文件留垃圾代码。
- 不要把数据更新逻辑直接写在migration文件里执行,长时任务很容易触发部署超时、数据库连接中断问题,独立Rake任务的思路是完全正确的。
具体落地步骤
整个流程拆成三次上线动作,完全兼容常规Rails发布流程,业务无感知:
- 第一个Migration上线:给
TableToUpdate加临时标记字段
class AddBinaryMigratedFlagToTableToUpdate < ActiveRecord::Migration[7.0] def change add_column :table_to_updates, :binary_migrated, :boolean, default: false, null: false add_index :table_to_updates, :binary_migrated end end
这个migration执行极快,上线后新老代码完全兼容,不会影响正常业务逻辑。
- 编写独立Rake任务执行数据更新,天然支持断点续跑
namespace :data do desc "转换TableToUpdate表二进制列内容" task migrate_table_binary: :environment do # 强制走主库,避免读写分离场景下从库同步延迟导致重复处理 ActiveRecord::Base.connected_to(role: :writing) do # 只捞未处理的记录,批量遍历 TableToUpdate.where(binary_migrated: false).find_each(batch_size: 500) do |record| # 单条记录的更新和标记打在同一个事务里,保证原子性 ActiveRecord::Base.transaction do # 替换成你自己的二进制内容转换逻辑 converted_binary = convert_legacy_binary(record.original_binary_col) record.update!( target_binary_col: converted_binary, binary_migrated: true ) end # 简单进度打印,不需要额外引入进度条gem print "." if record.id % 100 == 0 end end puts "\n二进制列迁移全部完成" end end
这个任务可以后台跑,哪怕中途因为服务重启、逻辑报错中断,下次重新执行只会捞取还没打binary_migrated: true标记的记录,完全不会重复处理已经完成的行。如果二进制转换逻辑比较耗时,可以把batch_size调得更小,比如200,减少单次数据库锁占用时间。
- 确认所有记录都处理完成(可以在控制台跑一句
TableToUpdate.where(binary_migrated: false).count确认结果为0),再上线第二个Migration清理临时字段即可:
class RemoveBinaryMigratedFlagFromTableToUpdate < ActiveRecord::Migration[7.0] def change remove_column :table_to_updates, :binary_migrated, :boolean end end
同类场景落地经验
我自己在生产环境处理过单表200万行的同类二进制格式转换,用的就是这套方案,前前后后中断重跑了三次,没有出现数据不一致或者重复处理的问题,全程业务侧无感知。几个避坑提醒:
- 不要在遍历循环里做额外的关联查询,如果转换逻辑需要依赖其他表数据,记得在查询时提前
includes关联表,避免N+1查询把数据库打满。 - 如果你实在不想给业务表加临时字段,再考虑单独建跟踪表,这种场景也不需要把跟踪表的Model放到
app/models目录下,直接在Rake任务文件里定义一个临时的轻量Model类就行,任务跑完删表的时候直接把这个临时类删掉,不会污染项目模型层。 - 如果你的二进制内容转换可以直接用数据库内置函数实现,不需要Ruby层做特殊处理,那直接写
update_all语句就行,执行速度比逐行Ruby处理快两个量级,不需要做进度跟踪,一条SQL跑完就完事,不过二进制列的格式转换大多需要Ruby侧处理,这个方案按需选用。
内容的提问来源于stack exchange,提问作者Marcos
相关产品推荐
相关产品推荐

