在实现一个软件系统时,作为系统的设计、实现人员,我们往往需要在选择一个好的方案或者说设计。有些选择针对的是诸如框架等大方向的设计,但更多的时候我们面临的则是针对某个具体模块或函数等小问题的解决方案的选择。
任何一个有经验的程序员都明白,并不是所有的选择都需要经过详细的考虑(否则我们可能陷入设计陷阱而永远无法正真实现我们所需要的功能),但这并不代表对那些小问题的设计方案选择上我们可以做随意的选择。古人说过,勿以善小而不为,勿以恶小而为之;万物皆是相同的,这句话应用到软件设计中,我想就是:这些小问题上,我们不能因为它在系统中起的作用很小要就可以做出草率的决策,但也不必担心这些小模块可能会严重影响系统性能而花费大量的时间和精力来权衡设计选择。如果在任何问题上都花费大量时间和精力去做“完美”的设计,期结果好一点的无非是一个华而不实的over design,但更糟糕的则是可能导致项目延期或最终流产(毕竟任何一个设计方案都有它的局限性,要想通过设计保证程序的效率、可维护性、可扩展性等等诸多方面是不可能的);但如果完全放弃对这些小问题设计上的权衡,我们又往往可能面临是灾难性的后果。
我在最近的一个项目总就犯了一个“勿以善小而不为”的错误:在我们的系统中,由于需要大量重复访问某类资源,而每次访问这些资源都相当消耗时间。一个自然的做法是在程序中引入一个cache,这样在访问资源时我们可以先查询cache,如果所需访问的资源存在cache中了,则直接返回所需的资源,否则才做一次真正的访问并把访问到的结果放到cache中去。从框架的角度来说,引入cache从设计的角度来说是经过比较详细的考虑的:比如哪些资源需要cache,cache的时效性如何处理等等。但在考虑用什么数据结构来做这个cache时,我草率的决定使用ConcurrentHashMap。
在我们的测试环境中,这个cache产生的效果是显而易见的:资源访问对的速度能有数量级的提升,而在cache本身引入的内存开销也没有产生什么异常。但当代码部署到生产环境中后,我们观察到有些客户的主机内存消耗会突然产生一个爆发式的增长,这时由于Java GC的开销,程序性能急剧下降!究其原因,则是这个用于作为cache实现的数据结构是可以无限增长的:在客户环境下,在一段时间内有数百万条记录被存放到了这个cache中!这个问题我在最初决定使用ConcurrentHashMap就意识到过,但考虑到我们会定期的让cache失效(即清空map里面的所有元素),我并没有打算采用稍微复杂点的数据结构来处理这个问题,最终导致了在这种极端情况下出现极其糟糕的结果。