性能优化是每个后端开发者都会遇到的问题,很多人一提到性能优化就想到换更好的服务器、加缓存、上集群,其实很多时候,代码层面的重构和优化就能带来巨大的性能提升,而且成本更低。最近我接手了一个老项目,首页加载要5秒多,用户体验很差,我通过一系列代码重构和性能优化,把首页加载时间降到了500毫秒以内,提升了10倍。今天就来分享一下这次性能优化的实战过程。

一、先测量,再优化,不要瞎猜

很多人做性能优化,上来就改代码,这里加个缓存,那里改个SQL,改了半天也不知道有没有效果,甚至可能越改越慢。我以前也犯过这个错,后来学乖了,性能优化的第一步永远是测量,找到真正的瓶颈在哪里,然后针对性地优化。

这次我先用Xdebug加Webgrind做了性能分析,把首页的执行过程全部记录下来,看看时间都花在哪里了。分析结果一目了然:数据库查询占了60%的时间,模板渲染占了20%,文件读取占了10%,其他占了10%。

然后我又开了MySQL的慢查询日志,把所有执行时间超过100毫秒的SQL都记录下来,发现首页有好几个SQL查询都要几百毫秒,还有一个SQL居然要2秒多。找到瓶颈之后,优化就有方向了,不用瞎猜。

所以性能优化的第一步永远是测量,用数据说话,不要凭感觉。你以为的瓶颈不一定是真的瓶颈,测量出来的才是。

二、数据库优化:最容易出效果的地方

数据库是Web应用最常见的瓶颈,这次也不例外。我先从数据库入手,效果最明显。

1. 给查询加索引

慢查询日志里有好几个SQL都是全表扫描,因为没有加索引。比如文章列表页,按分类和时间排序查询,分类ID和创建时间字段都没有索引,每次查询都要扫全表,数据量大了当然慢。我给这些常用的查询字段都加上了索引,查询速度直接从几百毫秒降到了几毫秒,效果立竿见影。

加索引是最简单、性价比最高的数据库优化手段,但是很多人就是不重视,表建好了就不管了,查询慢了也不知道加索引。当然,索引也不是越多越好,写操作的时候要维护索引,会影响写入性能,所以要根据实际查询情况来加,常用的查询字段加,不常用的不要加。

2. 消灭N+1查询

这是ORM和循环查询最容易犯的错。首页要显示文章列表,每篇文章要显示分类名称和作者名称,原来的代码是先查文章列表,然后循环每篇文章,再去查分类表和用户表,10篇文章就是20次额外查询,这就是典型的N+1问题。

我把它改成了联表查询,一次SQL就把文章、分类、作者信息都查出来了,查询次数从21次降到了1次,性能提升非常明显。很多时候,应用慢不是因为单次查询慢,而是因为查询次数太多,几十次上百次查询加起来,时间就上去了。

所以写代码的时候一定要注意,不要在循环里做数据库查询,尽量用联表或者子查询一次查出来。

3. 只查需要的字段

原来的代码里,很多查询都是SELECT *,把表的所有字段都查出来,但是实际上很多字段根本用不到。比如文章列表页,只需要标题、摘要、封面、时间、分类、作者这些字段,但是原来的查询把正文也查出来了,正文是大文本,非常占内存和传输时间。

我把所有SELECT *都改成了只查需要的字段,数据传输量小了很多,内存占用也降下来了,查询速度自然就快了。这是一个很小的改动,但是效果不错,而且代码更清晰,别人一看就知道你用了哪些字段。

4. 优化慢SQL

有一个SQL特别慢,要2秒多,我拿出来用EXPLAIN分析了一下,发现是一个很复杂的联表查询,联了五张表,还有子查询,执行计划很糟糕。我把它拆成了两个简单的查询,先查主表,拿到ID列表,再用IN去查关联表,虽然查询次数从1次变成了2次,但是总时间从2秒降到了100毫秒以内,快了20倍。

所以不是联表查询就一定比多次查询好,复杂的联表查询可能性能更差,要根据实际情况来选择。复杂的SQL可以拆成简单的SQL,有时候反而更快。

三、缓存优化:用空间换时间

数据库优化完之后,首页加载时间从5秒降到了2秒,还是不够快。接下来我加了缓存,这是性能优化的大杀器。

1. 页面缓存

首页的内容不是实时变化的,文章发布之后就不会频繁改动,完全可以把整个页面缓存起来。我用Redis做了页面缓存,用户第一次访问的时候,把渲染好的HTML存到Redis里,设置10分钟的过期时间,后面的用户直接从Redis里读HTML,不用查数据库,不用渲染模板,速度直接从2秒降到了50毫秒以内。

页面缓存是效果最明显的缓存方式,对于不频繁变化的页面,一定要加。当然,要注意缓存更新的问题,文章发布、修改、删除的时候,要把对应的缓存清掉,不然用户会看到旧内容。

2. 数据缓存

除了页面缓存,我还加了数据缓存。比如分类列表、热门文章、标签云这些数据,变化不频繁,但是每个页面都要用到,我把它们缓存到Redis里,设置1小时过期,不用每次都查数据库。

还有文章详情页,我把文章内容缓存起来,用户访问的时候先从缓存读,没有再查数据库,这样大部分访问都命中缓存,数据库压力就小了很多。

3. 注意缓存穿透和雪崩

加缓存的时候要注意两个常见问题:缓存穿透和缓存雪崩。缓存穿透是指查询一个不存在的数据,缓存里没有,每次都去查数据库,被恶意攻击的话数据库会挂。我的解决方法是,不存在的数据也缓存一个空值,设置短一点的过期时间,这样就不会每次都查数据库了。

缓存雪崩是指大量缓存同时过期,这时候所有请求都打到数据库,数据库压力骤增可能会挂。我的解决方法是,给缓存的过期时间加一个随机值,比如本来是10分钟,加个0到5分钟的随机数,这样缓存就不会同时过期了,避免了雪崩。

四、代码层面的优化

数据库和缓存优化完之后,性能已经很好了,但是我还做了一些代码层面的优化,进一步提升性能。

1. 减少文件包含和重复计算

原来的代码里,有些配置文件和工具类被重复包含了好几次,虽然PHP有include_once不会真的重复加载,但是还是有开销。我把公共的包含都放到入口文件里,统一加载,减少了重复包含。

还有一些计算,比如获取当前用户信息,原来的代码里在好几个地方都调用了同一个函数,每次都去查数据库或者缓存,我把它改成了在控制器里查一次,然后传给视图,或者用静态变量缓存起来,避免重复计算。

2. 优化模板渲染

模板渲染占了20%的时间,原来的模板里有很多复杂的逻辑,还有嵌套循环,渲染起来比较慢。我把模板里的业务逻辑都移到了控制器里,模板只做简单的输出,这样模板渲染速度就快了很多。

还有,原来的模板引擎每次都要重新编译模板,我开了模板编译缓存,编译过的模板就不用再编译了,直接执行PHP代码,速度快了不少。

3. 延迟加载和按需加载

有些功能不是每次都需要的,比如侧边栏的统计数据,只有部分页面需要,原来的代码是在基类里就加载了,不管用不用都查数据库。我改成了按需加载,只有用到的时候才去查,这样大部分页面都省了这些查询。

还有图片,我用了懒加载,页面滚动到可视区域的时候才加载图片,减少了首屏的HTTP请求数,首屏加载速度更快了。

五、优化效果和总结

经过这一系列的优化,首页加载时间从原来的5秒多,降到了500毫秒以内,提升了10倍以上,用户体验好了很多,服务器的负载也降下来了,原来高峰期CPU经常跑到80%以上,现在基本在30%左右。

总结一下这次性能优化的经验:

  1. 先测量再优化,用数据找到真正的瓶颈,不要瞎猜
  2. 数据库优化是基础,加索引、消灭N+1、只查需要的字段、优化慢SQL
  3. 缓存是性能优化的大杀器,页面缓存、数据缓存都要加,注意穿透和雪崩问题
  4. 代码层面的小优化积累起来也有效果,减少重复计算、优化模板、按需加载
  5. 性能优化是一个持续的过程,不是一次就完了,要定期监控,发现问题及时优化

很多人觉得性能优化很难,要很高级的技术,其实不是,大部分性能问题都是基础没做好,索引没加、查询写得烂、没加缓存。把这些基础的东西做好,性能就能提升一大截。希望这篇实战经验能对大家有所帮助。