2010年毕业后,加入了一家传统行业的公司做Java开发。转眼八年过去了,产品架构在近一两年也开始由单体架构转变为微服务架构。重点还是想回顾下单体架构下开发的一些心路历程。
刚加入公司,幸福的入职培训过后开始加入了项目。那时还没有开始敏捷开发,一个人负责一个模块。公司有严格的KPI考核,个人业绩与个人所负责模块的质量直接挂钩。所以一名新人加入公司,通常会分配到谁都不想接的模块。当然,这是我后来才知道的。
当时有位三年经验的老员工即将内部调动到别的城市工作,他负责的模块交到了我的手上。计算机专业科班出身,并且在校时熟读《Java编程思想》两遍,内心觉得不是啥事。等看到代码后,直接傻眼了。代码行数倒不是很多,不到5000行的样子。其中一个for
循环占了大概3000行!你没看错,是一个for
循环。本身对项目的业务知识不熟悉,代码里又各种特殊处理,只能一点点啃了。每天用eclipse debug,两周后,逐渐理清了这个循环的基本逻辑。如果是现在,我肯定会先做一些不改变代码逻辑的重构,拆分小方法,抽取重复逻辑等等,当时是不具备这个意识和能力的。很快,这个模块发布到外场商用了。这样的代码在商用环境的表现可想而知了,各种严重的故障单纷至沓来。前面说过,个人业绩和个人所负责模块的质量直接挂钩,可能读者会认为我的业绩很差。恰恰相反,我半年考核得了优秀!并且成为了部门的优秀毕业生!除了公司鼓励新员工的考虑以及领导对我工作态度的认可,负责的这个模块也贡献不少。虽然质量很差,但是问题严重啊,问题严重到领导经常被投诉,严重到领导经常来我工位前督促,这个故障什么时候能解决。虽然问题严重,但是我解决问题速度快啊。
如果没有维护过这种代码,你不会体会到《重构》《代码整洁之道》这些书中的观点的可贵。阿里去年推出的《阿里巴巴Java开发手册》,里边的很多条目我们都踩过坑。比如
【强制】ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException
异常,即 java.util.RandomAccessSubList cannot be cast to java.util.ArrayList.
说明:subList 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList ,而是
ArrayList 的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表上。
【强制】在 subList 场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、删除均会产生 ConcurrentModificationException 异常。
还有,曾经维护过一段代码,这段代码使用了一个数据结构Map<String, Map<Sting, Map<String, Object>>>
,业务逻辑主要围绕这个数据结构展开。笔者为了搞懂这个数据结构,在公司熬到凌晨两点,无奈放弃!还有,对HashMap
的理解,对面向对象的理解,都是在这个过程中不断加深的。
很多知识点你只是看过是无感的,只有犯过错、被坑过才会掌握!
去年阿里推出这个手册后,我一直在思考为什么我们或者其他公司没有推出这个手册?我想一方面是阿里的影响力,但是也能看出阿里对待技术的严谨以及对业界的责任感。这样的企业是值得尊敬的!
前面的那个模块在维护了一年多后跟随项目终止了,若干年后和那个项目的项目经理在一张桌上吃饭,他告诉我说:“那个模块的代码已经不能用烂字来形容了”。
后来开始开发一个消息驱动的业务模块,大概流程是这样的(我们的代码主要在红色框中):
阶段1:流程是这样的,我就是这样做的。来一条消息,按这个流程一条道走到黑。
这种设计很快暴露了问题:
1.处理效率低
2.系统A不仅对接系统B一个系统,由于系统B的处理效率低下,经常导致和系统A对接的其他系统出现问题。
效率低怎么办,上多线程啊!
阶段2:系统A来一条消息,系统B启动一个线程处理
由于刚开始商用,消息量不是很大,效率问题暂时解决了,也不会阻塞系统A了。
大概半年后,业务量上来后,这种设计的弊端呈现了:线程启动过多,OOM了。
也就是从这个时候开始,逐渐开始系统学习Java的多线程知识。
阶段3:系统B用线程池处理
这种设计不会OOM了,但是效率问题没有根本解决。这个时候开始了各种性能优化的探索:
1.优化各种JVM参数
2.系统C查询数据库效率低,所以我们直接连接系统C的数据库,用各种SQL关联查询提高效率。
3.数据生效到系统A效率低,我们做消息收集批量处理。
这样优化后,系统性能还算差强人意,在外场商用了近三年。
在此期间,运维的同事反馈了不少问题:
1.有时候只能处理消息A的请求,消息B的请求处理不了
2.数据库查询经常超时,导致整个业务请求处理失败。
研发侧也暴露了一些问题:
数据模型一旦发生变更,查询数据库的SQL语句就必须重写,工作量很大。
为了解决上述问题,有了如下的设计:
阶段4:线程池分类,消息处理设置超时机制。
主要优化点:
1.按照消息类型,每种消息设置一个独立的线程池处理,线程池的配置通过properties
文件动态生效。
2.按照业务功能,查询数据库和数据生效设置专门的线程池。
3.每条消息处理设置超时机制,避免局部失败影响整体。
这两步优化完成后,市场的反馈已经非常好了,各种性能问题不复存在。
考虑到后续业务量进一步增加的可能性以及架构的稳定性,继续改进:
阶段5:引入数据库缓存和领域模型。
主要优化点:
1.引入缓存机制,进一步提升数据查询效率
2.建立领域模型,业务处理与底层数据库模型解耦
至此,单体架构下的系统B设计告一段落。
近两年,产品开始向微服务架构转型。引入Spring Cloud
系列技术,当笔者第一次看到Hystrix
框架,看到舱壁模式、断路器模式等概念的时候,颇有点相见恨晚!原来我们折腾这么多年的设计,业界早已实现。接触到Rxjava
对线程池的分类,也与我们按照业务功能对线程池分类异曲同工。我们自己实现的缓存机制,业界更是有很多优秀的实现。至于我们痛定思痛后抽取的领域模型,在《实践领域驱动设计》等书中早已阐述明白。贫血模型到充血模型的演进,服务层的提取,等等。
不得不承认,在传统行业八年的技术成长与互联网公司是没法比。互联网公司的技术之所以先进,我想主要是他们面临的外部压力更多,促使他们不得不去改变,不得不不断研究新的技术方案。但是,如果在互联网公司只是做一个小螺丝钉,一入职就是在成熟的技术框架内开发,也未必会对这些技术有多么深刻的理解。为什么要有领域模型?为什么要用缓存?为什么用Hystrix
?甚至为什么面向对象设计要遵循SOLID五原则?这些问题笔者在八年的职业生涯不断挖坑,不断填坑的过程中领会到的自然要比直接从书本上看来要深刻的多。
现在几乎各个公司、各个项目都在上微服务架构,都在玩docker
。跟随技术潮流本身值得鼓励,但是笔者始终认为,如果你的问题在单体架构下没有想明白,只是引入了各种容器技术,微服务技术,你的问题不会得到根本的解决。比如,我的阶段1设计完全可以放在一个微服务中实现,以容器的形态发布。