逐行更新千万行大表不锁全表:postgresql_cursor FOR UPDATE行锁技巧完全解析

发布时间:2026/8/23 12:35:38
逐行更新千万行大表不锁全表:postgresql_cursor FOR UPDATE行锁技巧完全解析 逐行更新千万行大表不锁全表postgresql_cursor FOR UPDATE行锁技巧完全解析【免费下载链接】postgresql_cursorActiveRecord PostgreSQL Adapter extension for using a cursor to return a large result set项目地址: https://gitcode.com/gh_mirrors/po/postgresql_cursorpostgresql_cursor是一个面向 ActiveRecord 的 PostgreSQL 适配器扩展 gem它让 Rails 程序通过数据库游标把大结果集按每 1000 行一批分块取回处理配合 PostgreSQL 的FOR UPDATE行锁就能实现逐行更新千万行大表而不锁全表更新期间其他业务照常读写。本文完全解析这套行锁技巧核心 3 行代码、锁的加锁与释放时机、block_size选择经验值以及一批容易踩的坑。一、为什么逐行更新千万行大表容易锁死整个表给千万行大表做逐行加工时常见的三种做法都有硬伤Product.all.each { |p| p.update(...) }ActiveRecord 会把所有行一次性拉回内存并逐行实例化内存直接爆掉find_each/find_in_batches只能按主键取数、主键必须是数字、每批都要重新执行查询复杂查询基本用不了一条SELECT ... FOR UPDATE抓全表千万行全部加锁整个表被冻结线上业务直接瘫痪。postgresql_cursor的思路是第三种的安全版用游标一批一批取每批只锁当前这一小撮行处理完立刻释放——任何时刻被锁住的只是几百行而不是全表。二、3 行代码实现 FOR UPDATE 逐行更新大表 使用 ActiveRecord 的lock方法AREL 会在查询末尾自动追加FOR UPDATE子句再交给each_instance游标迭代即可Product.lock.each_instance(block_size: 100) do |p| p.update(price: p.price * 1.05) end执行后查询语句变成SELECT ... FOR UPDATEgem 会在一个事务内完成以下循环对应 lib/postgresql_cursor/cursor.rb 中的open/fetch_block/closeSET cursor_tuple_fraction TO 1.0; DECLARE cursor_1 NO SCROLL CURSOR FOR SELECT ... FOR UPDATE; 循环: rows FETCH 100 FROM cursor_1; -- 这 100 行被加行锁 逐行执行块内逻辑逐行 update 直到某次 FETCH 返回不足 100 行 CLOSE cursor_1;关键点每次FETCH取回的那个块block_size行会被锁定供你更新当该块处理完、执行下一次FETCH或CLOSE时这些行的锁随即释放见 README.md 的 Locking and Updating Each Row 一节。于是更新大表时只有当前正在处理的小批次被锁住其余千万行对并发业务完全开放。迭代方法的注入位置分别在 lib/postgresql_cursor/active_record/relation/cursor_iterators.rbRelation 级别和 lib/postgresql_cursor/active_record/sql_cursor.rb类级别。三、block_size 怎么选记住一个经验值 block_size决定每次 FETCH 锁住多少行是并发与吞吐之间的调节阀场景建议block_size说明冷数据、批量离线跑批100~1000默认 1000吞吐优先业务高峰期更新热表10以下锁持有时间最短并发最安全每行处理很重发通知、调外部服务10以下避免大块行长时间持锁引发死锁官方提醒README 原话长时间锁定大块行可能引起死锁或其他性能问题——热表上、或每行处理耗时较长时请尝试block_size 10。四、常用选项速查表 所有each_row/each_instance/each_row_by_sql等方法都接受一个选项哈希选项作用参考值block_size: n每次 FETCH 取多少行即行锁粒度默认 1000热表 ≤10while: value块返回值等于它时继续循环按需until: value块返回值等于它时提前退出按需with_hold: boolean提交后仍保持游标打开跨提交手动fetch时用connection: conn指定使用的数据库连接按需fraction: floatcursor_tuple_fraction影响查询计划保持默认 1.0勿覆盖cursor_name: string给游标命名按需五、避坑清单与性能建议 ⚠️只把游标用于大结果集。小表直接where(...).each更快游标有额外开销each_row比each_instance快约 4 倍返回哈希、不实例化模型能用哈希处理就别建对象手动fetch必须在事务中用each_*系列方法则 gem 会自动管理事务见 lib/postgresql_cursor/cursor.rb 的with_optional_transaction用.select(:id, :name)只取需要的列大宽表上收益明显需要可运行的最小演示可参考 test-app/app.rb完整行为覆盖见 test/test_postgresql_cursor.rb。六、快速上手两条命令跑起来 ✅# Gemfile gem postgresql_cursor # 最小读取示例按批取哈希行 Product.where(id 0).order(name).each_row { |hash| Product.process(hash) }Product.lock.each_instance(block_size: 10) 游标分块就是这个 gem 对千万行大表逐行更新给出的标准答案锁的粒度 一个块块的寿命 一次处理循环。把block_size调小就把锁全表变成了锁几行更新与在线业务从此和平共处。【免费下载链接】postgresql_cursorActiveRecord PostgreSQL Adapter extension for using a cursor to return a large result set项目地址: https://gitcode.com/gh_mirrors/po/postgresql_cursor创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考