PHP中如何并发运行同一函数实现并行请求与数据库批量更新
游标分页接口并发处理方案
你要的「拿到next游标立刻发起下一次请求、不等待数据库写入、同时3路并行处理」的效果完全可以实现,多线程是可选方案之一,但不是性价比最高的选择。你之前用header递归跳转的方式本身设计就有问题,出现too_many_redirects是必然结果,完全不适合百万级数据的长任务处理。
为什么重定向方案不可行
- 不管是web服务端、浏览器还是CLI环境,都有默认的最大重定向次数阈值,一般在10~20次,触发阈值就会直接抛出重定向过多错误
- Web请求有严格的超时限制,PHP默认请求超时时间为30秒,根本撑不到百万级数据处理完成
- 每次跳转都会重新初始化PHP运行环境,额外开销极大,处理效率极低
可选实现方案
1. 多进程/多线程实现
如果不想引入额外组件,可以直接通过PHP的进程/线程扩展实现:
- 优先选
pcntl_fork多进程方案:CLI模式下PHP默认支持pcntl扩展,不需要线程安全版本的PHP,兼容绝大多数现有扩展,坑比多线程少。 - 如果一定要用多线程,可以选
parallel扩展,注意必须使用ZTS(线程安全)版本的PHP,且要确认你用的数据库、HTTP请求扩展都兼容线程安全模式。 - 核心注意点:必须用共享存储(推荐Redis,轻量快速)维护游标队列,避免多个worker拿到同一个游标重复请求。每次worker拿到游标发起请求后,立刻把接口返回的
next游标写回队列,再执行数据库更新操作,完全不用等写入完成。
参考伪代码:
// 初始化Redis游标队列,首次运行写入初始空游标 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); if (!$redis->exists('cursor_queue')) { $redis->lPush('cursor_queue', ''); } // 启动3个worker进程 for ($i = 0; $i < 3; $i++) { $pid = pcntl_fork(); if ($pid == 0) { // 子进程worker逻辑 while (true) { // 阻塞等待获取游标,1秒拿不到就退出 $cursorRes = $redis->brPop('cursor_queue', 1); if (!$cursorRes) break; $currentCursor = $cursorRes[1]; // 发起接口请求 $result = doRequest($currentCursor); // 拿到next游标立刻写回队列,不等待数据库操作 if (!empty($result['next'])) { $redis->lPush('cursor_queue', $result['next']); } // 执行数据库批量更新 updateTheDatabaseTable($result['items']); } exit(0); } } // 主进程等待所有子进程执行完成 while (pcntl_waitpid(0, $status) != -1);
2. 消息队列解耦方案(生产环境首选,稳定性最高)
如果任务要长期跑、对稳定性要求高,优先选这个方案,比多进程多线程好维护太多:
- 拆分两个角色:1个生产者,N个消费者(你要3并发就开3个消费者)
- 生产者只做一件事:从游标队列拿游标→发接口请求→把返回的next游标立刻放回队列→把接口返回的items拆成批量写入任务推送到数据队列,全程不碰数据库写入
- 消费者只做一件事:从数据队列拿任务,批量写入/更新数据库,消费者可以用supervisor守护,挂了自动重启,后续要加并发直接加消费者数量就行,不用改核心逻辑
- 这个方案完全解耦了接口请求和数据库写入,接口请求速度不会被慢查询、数据库写入瓶颈阻塞,游标不会重复,失败重试逻辑也好做。
3. 多Cron实例+分布式锁方案(零扩展依赖,上手最快)
不想装扩展、不想搭队列的话可以用这个方案,零成本实现:
- 把单次任务逻辑改成:通过Redis的
setnx原子抢占游标锁→拿到锁后读取当前存储的最新游标→发接口请求→把返回的next游标更新回共享存储→释放锁→执行当前批次的数据库更新 - Crontab里配置3个相同的调度任务,或者在任务里加判断,当前运行的实例数达到3个就直接退出
- 注意:读取游标、更新next游标的整个过程必须加锁,锁超时时间设为10秒左右,防止进程中途挂掉导致锁死。
优化注意事项
- 数据库写入一定要用批量upsert语法,不要单条循环update,200条数据做批量写入的话,性能比单条写高50倍以上
- 加简单的请求限流,每个接口请求间隔100~200毫秒,避免把目标接口打挂被封
- 记录已经处理过的游标,出现接口返回重复游标时直接跳过,避免重复写入数据
- 加失败重试逻辑,接口请求失败、数据库写入失败时,把当前游标放回队列等待下次重试,避免丢数据
内容的提问来源于stack exchange,提问作者emrez
相关产品推荐
相关产品推荐

