chunkbyid比chunk更安全,因其基于主键范围查询(where id > ?)而非offset,避免高并发写入时漏行、重复或卡死;需确保id自增有序、不修改id、回调内按需小事务。

分片不是 Laravel 原生概念,它不直接提供“数据库分片”能力;处理海量并发数据时,真正可用且稳定的是分批(chunking)、游标(cursor)和租户隔离三类技术,混用或误用分片术语反而会导致架构错位。
chunkById 为什么比 chunk 更适合高并发写入场景
因为 chunk 依赖 OFFSET,在写入频繁的表中,OFFSET 查询会随新记录插入不断偏移,导致漏行、重复或查询卡死;chunkById 每次基于上一批最大 id 构造 WHERE id > ? ORDER BY id LIMIT,跳过 OFFSET,天然适配高并发写入。
- 必须确保主键是自增整型,且不被业务逻辑修改(改
id会破坏分片连续性) - 不要在回调里对同一批数据做
ORDER BY created_at或其他非主键排序,否则分片边界失效 - 每批处理完应立即记录
$chunk->last()->id到缓存,否则断点续传无法恢复 - 示例中若用
DB::transaction包裹整个chunkById调用,会锁表太久,应只在单条更新内开小事务
cursor() 在只读高并发导出中的真实表现
cursor() 不加载全量结果集,而是复用数据库连接逐行 fetch,内存占用恒定在 KB 级,比 chunkById 更轻量,但仅适用于只读场景——任何写操作(如 $user->update())都会让游标失效并抛出 PDOException。
- 导出 CSV 时,用
fputcsv($fp, [...])直接写文件句柄,别拼大字符串 - 禁止在
foreach (User::cursor() as $user)中调用$user->posts,会触发 N+1 查询,瞬间拖垮 DB 连接池 - 若需关联字段,改用
select('users.*', 'posts.title')+join()预加载,避免访问器和关系动态查询 - PHP-FPM 子进程超时(如
max_execution_time=30)会导致游标中断,建议配合set_time_limit(0)和队列运行
多租户环境下 chunkById 容易跨库拉取数据
当模型启用了租户作用域(如 BelongsToTenant trait),chunkById 默认不会自动注入租户过滤条件,若未显式调用 withGlobalScope() 或检查 static::$globalScopes,查询可能绕过租户隔离,从其他租户库中拉出数据。
- 务必在调用前确认全局作用域已激活:
User::withoutGlobalScopes()->tenant(1)->chunkById(...)是危险写法 - 不要在
chunkById回调里执行Tenant::set(2),这会污染后续批次的数据库连接,导致下一批查到错误租户数据 - 推荐方式:先用
DB::connection('tenant_1')显式切换连接,再构建查询实例 - 若使用 Horizon 队列,确保每个任务都携带租户上下文(如
tenant_id字段),而非依赖运行时全局状态
断点续传失败的三个隐藏原因
手动记录 last_id 后仍无法续传,大概率是以下三类问题之一:缓存未命中、主键非严格递增、或 WHERE 条件与原始查询不一致。
- 缓存 key 过期时间太短(如
cache()->put('last_id', $id, 60)),任务跑 2 小时就失效,应设为 24 小时以上 - 表中存在手动插入的
id小于当前最大值(如测试数据回填),导致下次WHERE id > $lastId跳过这部分 - 原始查询含
where('status', 'pending'),但续传时只写了User::where('id', '>', $lastId)->chunkById(...),漏掉 status 条件,数据范围已变 - Redis 缓存未持久化,服务重启后
last_id丢失,建议关键任务同时落库(如process_logs表)
真正难处理的不是“怎么分”,而是“分完之后状态怎么管”——ID 断点、租户上下文、缓存一致性、连接生命周期,这些细节一旦松动,分片就从优化手段变成故障放大器。











