|
去年冬天,实战案例我接手了一个小型电商后台的实战案例重构项目。代码库是实战案例三年前外包团队留下的,PHP混着jQuery,实战案例一个订单列表页加载要4.7秒。实战案例老板说“能用就行”,实战案例但运营那边已经骂了半年——每次大促前改个按钮颜色,实战案例都得找原班人马,实战案例一次报价八千。实战案例 我拉了个分支,实战案例没急着动数据库。实战案例先打开Chrome DevTools的实战案例Performance面板,录了一次页面加载。实战案例火焰图里最粗的实战案例那条是`getOrderList()`,里面套了三个foreach,实战案例每个循环里又查一次用户表。典型的N+1问题,但藏得深——它把查询写在一个helper函数里,名字叫`formatOrderData`,光看名字根本想不到会连数据库。 改法不复杂。把用户ID收集起来,一次查完,用array_column建索引,再循环组装。代码从47行缩到19行。本地跑一遍,列表加载掉到0.8秒。上线那天运营在群里发了个“?”,然后说“今天系统是不是坏了,怎么点什么都秒开”。 事情到这本来该结束了。但第二周,客服反馈说导出CSV功能偶尔超时。我查了日志,发现是同一个helper函数在导出时被循环调用了——导出两万条订单,就是两万次用户查询。之前没人发现,因为导出慢大家就干等,等不了就刷新,刷新就重来。典型的“能用就行”后遗症。 我把helper拆成两个:一个纯格式化,一个负责预加载关联数据。调用方显式传参,不再隐式查库。改完顺手加了个查询计数器,开发环境里超过50次查询就在控制台报警。后来这个计数器抓到过另一个问题:商品详情页的推荐模块,每次渲染都拉一次全量分类树。那棵树有1200个节点,内存里缓存一下会死吗?不会。但原开发者可能觉得“反正就一次”。 项目收尾时,老板问我要不要写个文档,把重构经验沉淀下来。我说不用,就一条规则:任何叫`format`、`parse`、`build`开头的函数,里面不许出现数据库查询。要查可以,函数名改成`fetchAndFormat`,让调用者知道代价。他听完笑了,说这规则太具体,不像架构原则。我说架构原则就是给具体问题擦屁股的,擦干净了,原则自然长出来。 后来那个计数器一直留着。有次新来的实习生写了个循环,里面调了三次`getUserInfo`,控制台直接刷屏。他跑来问我是不是环境坏了。我说没坏,是代码在喊疼。他愣了两秒,回去把循环拆了。那天下午他提交的代码里,多了一个`batchGetUsers`函数。 你看,工具不教人,但工具会让人难受。难受了,人就会自己找路。比写十页规范管用。 |