大概在五年前,我接手了一套遗留系统。这套系统是公司初创时期,为了保证迅速上线而找外包公司开发的,当时已经日益成为业务增长的瓶颈。我们要做的就是“开着飞机修飞机”,让这个系统变得稳定、健壮、安全,用我的话说,就是“把不正常变为正常”。

有过类似经验的人都知道,这样系统的内幕是相当骇人的。更糟糕的是,骇人的内幕刚刚对我露出一个角,业务那边就扔过来一个大雷:每天都需要大量用到的某个页面查询很慢,结果大家整天在各种群里大呼小叫,搞得IT部门灰头土脸,老大说不着急,其实脸色也不好看。赶紧把查询速度提升上去,就成了当务之急。

从软件开发的角度来看,问题真是太多了:

  • 架构一团糟,各级缓存失效,大量业务逻辑直接落在数据库层;

  • 数据库连接池的分配有问题,不是按业务操作来分配连接,而是按照HTTP请求来分配连接;

  • 数据库的表结构一团糟,拆分起来简直要了命了;

  • 命名空间有问题,经常有两个一模一样的类,干的却是不同的事情;

总之,随便从哪套软件工程的基本观点来看,都会知道现在的系统真是千疮百孔、千奇百怪,能跑到现在简直是奇迹了。“把不正常变正常”的选择也相当自然:釜底抽薪,来个彻底改造,否则不但风险巨大,自己看着都相当不爽,道不同,不相为谋嘛。

然而,现实情况却是紧张而急迫的。我根本没有这么多时间,公司也压根没有那么多资源,甚至合格的程序员都没多少。就是得在这些前提下,迅速、有效地提高这个页面的查询速度。这个问题搞不定,估计工作都难保,更谈不上施展软件开发的基本功了。

怎么办呢?

首先是了解,给系统加上日志,看看每天大家都是以什么样的方式访问这个页面,进行怎样的查询?然后是观察,观察到底是什么时候速度会慢,怎么慢下来的?再次在分析,分析速度慢的原因,想想要如何解决。最后,就是实施了。

通过分析日志,我们发现,这个页面主要是针对一张表的查询,这张表大概有200万条数据,最高查询速度不超过10次/秒。查询中每次要出现的条件是创建时间,其它条件是订单号、包裹号、客户代码之中的一个(坑爹的是这是个模糊查询,程序员偷懒直接用like “%xx%” 去匹配每个字段)。另外,查询结果每页显示15条,有一半的查询停止在第一页,剩下大概还有30%停止在第二页,15%左右停止在第三页。

看来这个性能问题并没有超越单机性能的物理限制,所以各种表分区、分布式暂时都用不上(而且时间和资源也不容许)。螺狮壳里做道场,把几个要紧的点抓到,应该就好了。

创建时间与订单ID(主键)是单调增加的,也就是说,订单ID靠后的,创建时间一定靠后。既然表上面已经有太多索引了,订单ID又是主键,查询的最小时间单位是天而不是时分秒,那么索性把创建时间的索引拿掉,另建一张索引表,记录每天最小的订单ID,这样200万数据的表,只需要一个不到400条数据(1年多)的索引,程序启动时一次性都出来放在内存里,用程序每次把创建时间查询映射为ID范围查询。

既然用户输入的主要内容是订单号、包裹号、客户代码,那么针对订单号、包裹号、客户代码定做三套专门优化的查询逻辑,前端根据输入简单判断一下到底走哪个分支。界面上看起来还是模糊查询,真正的实现却大有不同。

超过90%的查询都只要显示3页。那么好办,把每页显示的记录数从15条调整为45条,一次就显示原来3页的内容,数据还是那些数据,查询的压力却减少了一大半。

另外,每次打开这页面时,默认是显示时间上最近的那一页。仔细了解之后发现这个功能对用户的意义只是告诉大家“系统没坏”,既然创建时间和订单ID(主键)的单调增加的,那么干脆专门写个“第一次打开(没有选择任何限制条件)”的逻辑,按ID从大到小选择45条显示出来即可。

数据库的表结构基本没动,代码也几乎是原来一团糟的代码,结果却相当让人满意:整个开发时间不到2天,查询速度却提高了100倍——对你没有看错,就是100倍。

然后,我们可以赢得很多时间,来做数据库的优化,来做程序结构的优化,来让系统看起来更正常一些。

我本来以为这没什么好稀奇的,后来发现似乎又不是这样,这样的“雕虫小技”说出来很多人都觉得太简单,他们心心念念就是要做各种百年大计。仔细问原因,往往是“网上(书上)说了,就应该这么做”。

当然,我自己也不能保证自己就做得很好:不久前Caoz分享的经验真是让我拍案叫绝,因为数据库的主从同步可能会有1秒的延迟,为避免论坛用户发帖之后看不到自己的帖,先给用户看一个“发帖成功,3秒后跳转”的页面——这个巧妙的思路,我一时还真想不到。

回想这些问题的时候,我常常会想到将近一百年前的“问题与主义”之争。

那时候,胡适先生主张“多研究问题,少谈主义”,从一点一滴做起;相反,李大钊先生主张对各种问题“应当有一个根本的解决”,也就是“主义”优先。

按照历史书的说法,这是“马克思主义与非马克思主义在中国的第一场交锋”,“标志着马克思主义者与改良派的彻底决裂”。结果,许多简单问题复杂化的来源就在“主义”优先:种庄稼这么简单的问题,也要区分主义——宁要社会主义的草,不要资本主义的苗。

痴迷于争论“宁要社会主义的草,不要资本主义的苗”,却对“种庄稼”的问题视而不见,今天人人都觉得可笑。但这种可笑很可能来自“社会主义”和“资本主义”,而不是来自问题本身。在软件开发中,我们不是经常面对这种争论吗?

  • Java好还是.NET好?

  • Windows好还是Linux好?

  • 一方说这样性能不好,另一方说性能没问题。争来争去,引用各种资料,找来各种名人言论站台,就是没有人想过,我们到底在解决什么类型的问题,对这个问题来说什么样的性能是足够的……

  • 一套系统用了很强壮的框架,有很完美的设计……总之,各方面都相当不错,唯一的问题就是产不出问题。如果你问问“你这个系统到底要解决什么问题,能不能真正解决呢”,多半得不到满意的答复。

怎么解决这类问题?秦晖老师曾经提到过一个说法,我非常赞同:主义可拿来,问题须土产。如果不愿意拿来其他人的主义,就会掉进“自己发明轮子”的陷阱;但如果问题却不能一起拿来,原有的问题脱离了所处环境充其量是个标本,新环境里的真正问题没有被发现,结果多半是南辕北辙。

“主义可拿来,问题须土产”。在下次大家面对技术争论不知所措的时候,不妨在心里把这句话默念几遍,看看有没有真正土产的问题。如果没有,这样的争论,不关心也罢。