PHP渲染百万级数据,你踩过这些坑吗?

最近接到了一个新需求,要在PHP里渲染一个超大的数据集,数据量大概在百万级别。作为一个老PHP,我一开始以为这不就是个简单的foreach循环,结果现实给我上了一课。今天就来聊聊PHP大数据渲染的那些坑,以及我是怎么填这些坑的。

先说说最初的想法:直接从数据库里拉出来所有数据,然后扔给PHP处理,循环输出到页面上。代码大概长这样:

$data = $db->query("SELECT FROM huge_table");

foreach ($data as $row) {

echo "<div>{ $row['name'] }</div>";

}

看起来很简单?结果服务器直接崩了。为啥?内存爆炸了。PHP脚本默认的内存限制是128M,但百万条记录一上来就是几百兆,直接爆了。

所以,填坑第一步:控制内存。我决定分批处理数据,比如每次只处理1000条。于是代码改成了这样:

$offset = 0;

$limit = 1000;

do {

$data = $db->query("SELECT

FROM huge_table LIMIT { $limit } OFFSET { $offset }");

foreach ($data as $row) {

echo "<div>{ $row['name'] }</div>";

}

$offset += $limit;

} while (!empty($data));

这下内存是稳住了,但新问题又来了:渲染时间太长。百万条记录,每批1000条,要渲染1000次,每次都要跟数据库交互,速度实在是太慢了。于是,填坑第二步:减少数据库查询次数。

我决定用游标(cursor)来处理数据,一次性从数据库拉取数据,然后逐条处理。代码是这样的:

$stmt = $db->query("SELECT FROM huge_table");

while ($row = $stmt->fetch()) {

}

这样减少了数据库查询次数,速度确实快了不少。但问题又来了:PHP脚本执行时间太长,超时了。PHP默认的最大执行时间是30秒,而我这个脚本跑了1分钟还没完。于是,填坑第三步:调整脚本执行时间。

我在脚本开头加了一句:

set_time_limit(0);

这样脚本就不会超时了。但新的问题又出现了:页面加载时间太长,用户会以为网页挂了。于是,填坑第四步:异步渲染。

我决定用AJAX来分批渲染数据,先把页面框架加载出来,然后通过AJAX请求分批获取数据并渲染。前端代码大概长这样:

function loadMore(offset, limit) {

fetch(/api/getData?offset=${ offset }&limit=${ limit })

.then(response => response.json())

.then(data => {

data.forEach(row => {

document.getElementById('container').innerHTML += &lt;div&gt;${ row.name }&lt;/div&gt;;

});

if (data.length > 0) {

loadMore(offset + limit, limit);

});

}

loadMore(0, 1000);

后端API代码大概是这样的:

$offset = $_GET['offset'];

$limit = $_GET['limit'];

$data = $db->query("SELECT

FROM huge_table LIMIT { $limit } OFFSET { $offset }");

echo json_encode($data);

这下用户体验好多了,页面先加载出来,数据慢慢渲染。但问题又来了:如果用户快速滚动页面,AJAX请求会堆积,导致服务器压力过大。于是,填坑第五步:限制并发请求。

我决定在前端加一个队列,每次只允许一个AJAX请求,等上一个请求完成了再发起下一个。代码大概是这样的:

let isRequesting = false;

if (isRequesting) return;

isRequesting = true;

isRequesting = false;

}

这下服务器压力小多了,但新问题又来了:如果用户不滚动页面,数据就不会加载,导致页面一开始是空的。于是,填坑第六步:预加载数据。

我决定在页面加载时先加载几批数据,这样用户一进来就能看到内容。loadMore(0, 5000); // 先加载5000条

这样用户体验更好了,但问题又来了:如果用户只看前几页,后面的数据就白白加载了,浪费资源。于是,填坑第七步:仅渲染可视区域。

我决定用虚拟滚动技术,只渲染用户当前可见的部分,其他部分等用户滚动到那里时再渲染。前端代码大概是这样的:

const container = document.getElementById('container');

const visibleItems = 20;

const itemHeight = 50;

let scrollTop = 0;

function render() {

const start = Math.floor(scrollTop / itemHeight);

const end = start + visibleItems;

fetch(/api/getData?offset=${ start }&limit=${ visibleItems })

container.innerHTML = '';

data.forEach((row, index) => {

const top = (start + index) itemHeight;

container.innerHTML += &lt;div style="position: absolute; top: ${ top }px;"&gt;${ row.name }&lt;/div&gt;;

}

container.addEventListener('scroll', () => {

scrollTop = container.scrollTop;

render();

});

render();

这下资源利用率大大提高了,但问题又来了:快速滚动时,渲染跟不上,导致页面卡顿。于是,填坑第八步:优化渲染性能。

我决定用节流(throttle)来控制渲染频率,比如每100ms最多渲染一次。let lastRenderTime = 0;

const throttleTime = 100;

const now = Date.now();

if (now - lastRenderTime > throttleTime) {

lastRenderTime = now;

scrollTop = container.scrollTop;

render();

}

});

这下页面流畅多了,但新问题又来了:如果用户快速滚动到页面底部,需要渲染的数据量过大,导致渲染时间过长。于是,填坑第九步:分块渲染。

我决定把数据分成多个块,每个块单独渲染,减少单次渲染的数据量。const chunkSize = 1000;

let currentChunk = 0;

function renderChunk() {

const start = currentChunk

chunkSize;

const end = start + chunkSize;

fetch(/api/getData?offset=${ start }&limit=${ chunkSize })

currentChunk++;

requestAnimationFrame(renderChunk);

}

if (container.scrollTop + container.clientHeight >= container.scrollHeight) {

renderChunk();

}

});

这样即使是百万级别的数据,也能流畅地渲染出来。不过,问题又来了:如果用户频繁滚动,可能会导致重复渲染。于是,填坑第十步:缓存已渲染的数据。

我决定用个数组来记录哪些数据已经渲染过了,避免重复渲染。const rendered = [];

if (rendered.includes(currentChunk)) return;

rendered.push(currentChunk);

}

这下性能终于达到了预期,用户可以流畅地浏览百万级别的数据了。

总结一下,PHP大数据渲染的坑确实不少,但通过分批处理、异步渲染、虚拟滚动、分块渲染等技术手段,最终还是填上了这些坑。希望这篇分享能帮到同样遇到这类问题的朋友。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值