上一个电商项目的反思

加入中科软已经有了一个年头,从去年实习到今年转正,陆陆续续接触了大概四个项目。有电商类,互联网保险类,也经历过管理系统。幸运的是,这些项目都是从零开始,避免了让我去维护不堪入目的老旧系统。而这么多项目中令我印象最深刻的,就要属上一个电商项目了。这也是我接触到的真正意义的第一个微服务项目,到今天回首去看曾经的这个项目,有很多突破性地尝试,同时不可避免地也踩入了一些坑点,毕竟摸着石头过河。今天想聊聊我对上一个电商项目的反思。

项目简介

准确的说是一个第三方的电商项目,商品来源是由主流电商的 http 接口提供(目前接入了京东,苏宁),打造我们自己的商城体系。使用的技术包括 springboot,jpa,rpc 框架使用的是 motan,数据库使用的是 oracle,基本都还算是主流的技术。

盲目地拆分微服务

使用了 springboot 就是微服务了吗?使用 rpc 通信就是微服务了吗?刚接触到所谓的微服务架构时,无疑是让人兴奋的,但也没有太多的经验,以至于每提出一个新的需求,几乎就会新建一个服务。没有从宏观去思考如何拆分服务,那时还没有项目组成员尝试去使用领域驱动设计的思想去划分服务的边界,会议室中讨论最多的话题也是:我们的数据库该如何设计,而不是我们的领域该如何划分。项目初期,使用单体式的思想开发着分布式项目,新技术的引入还只是使人有点稍微的不顺手,但是项目越做越大后,越来越大的不适感逐渐侵蚀着我们的开发速度。

说道微服务的拆分,有很多个维度,这里主要谈两个维度:

  • 系统维度:业务功能不同的需求,交给不同的系统完成,如订单,商品,地址,用户等系统需要拆分。
  • 模块维度:基础架构层(如公用 util),领域层,接口层,服务层,表现层的拆分。

在项目的初期,我们错误地认为微服务的拆分仅仅是系统维度的拆分,如商品系统和订单系统,而在模块维度上,缺少拆分的意识,如订单模块的表现层和服务层,我们虽然做了隔离(两个独立的 tomcat)。但在后来,业务添加了一个新的需求:商城增加积分支持,让用户可以使用积分购买商品。我们突然发现,所谓的服务层和表现层严重的耦合,仅仅是在物理上进行了隔离,逻辑层面并没有拆分,这导致新的积分服务模块从原先的订单服务层拷贝了大量的代码。吸取了这个教训后,我们新的项目中采取了如下的分层方式:

新的架构分层

其中比较关键的一点便是表现层与应用层的完全分离,交互完全使用 DTO 对象。不少同事产生了困惑,抱怨在表现层不能访问数据库,这让他们获取数据变得十分“麻烦”,应用层和表现层还多了一次数据拷贝的工作,用于将 DO 持久化对象转换成 DTO 对象。但这样的好处从长远来看,是不言而喻的。总结为以下几点:

1 应用层高度重用,没有表现形式的阻碍,PC 端,移动端,外部服务都可以同时接入,需要组装什么样的数据,请自行组装。

2 应用层和领域层可以交由经验较为丰富的程序员负责,避免了一些低性能的数据操作,错误的并发控制等等。

3 解决远程调用数据懒加载的问题。从前的设计中,表现层拿到了领域层的对象,而领域层会使用懒加载技术,当表现层想要获取懒加载属性时,或得到一个 no session 的异常。在没有这个分层之前,如何方便地解决这个问题一度困扰了我们很长的一段时间。

数据库的滥用

项目使用了 oracle,我们所有的数据都存在于同一个 oracle 实例中,各个系统模块并没有做到物理层面的数据库隔离。这并不符合设计,一方面这给那些想要跨模块执行 join 操作的人留了后门,如执行订单模块和用户模块的级联查询;另一方面,还困扰了一部分对微服务架构不甚了解的程序员,在他们的想法中,同一个数据库实例反而方便了他们的数据操作。

严格意义上,不仅仅是不同系统之间的数据库不能互相访问。同一个系统维度的不同模块也应当限制,正如前面一节的分层架构中,表现层(web 层)是不应该出现 DAO 的,pom 文件中也不应该出现任何 JPA,Hibernate,Mybatis 一类的依赖,它所有的数据来源,必须是应用层。

另外一方面,由于历史遗留问题,需要对接一个老系统,他们的表和这个电商的 oracle 实例是同一个,而我竟然在他们的表上发现了触发器这种操作… 在新的项目中,我们已经禁止使用数据库层面的触发器和物理约束。

在新的项目中,我们采用了阿里云的 RDS(mysql) 作为 oracle 的替代品,核心业务数据则放到了分布式数据库 DRDS 中,严格做到了数据库层面的拆分。

并发的控制

电商系统不同于 OA 系统,CMS 系统,余额,订单等等操作都是敏感操作,实实在在跟钱打交道的东西容不得半点马虎,然而即使是一些有经验的程序员,也写出了这样的扣减余额操作:

1
2
3
4
5
6
7
8
public void reduce(String accountId,BigDecimal cost){
Account account = accountService.findOne(accountId);
BigDecimal balance = account.getBalance();
if(balance > cost)
balance = balance - cost;// 用四则运算代替 BigDecimal 的 api,方便表达
account.setBalance(balance);
accountService.save(account);
}

很多人没有控制并发的意识,即使意识到了,也不知道如何根据业务场景采取合适的手段控制并发,是使用 JPA 中的乐观锁,还是使用数据库的行级自旋锁完成简单并发控制,还是 for update 悲观锁(这不建议被使用),还是基于 redis 或 zookeeper 一类的分布式锁?

这种错误甚至都不容许等到 code revivew 时才被发现,而应该是尽力地杜绝。

代码规范

小到 java 的变量的驼峰命名法,数据库中用‘_’分割单词,到业务代码该如何规范的书写,再到并发规范,性能调优。准确的说,没有人管理这些事,这样的工作落到了每个有悟性的开发者身上。模块公用的常量,系统公用的常量应当区分放置,禁止使用魔鬼数字,bool 变量名不能以 is 开头等等细小的但是重要的规范,大量的条件查询 findByxxx 污染了 DAO 层,完全可以被 predicates,criteria 替代,RESTFUL 规范指导设计 web 接口等等…

在新的项目中,一条条规范被逐渐添加到了项目单独的模块 READ.me 中。作为公司的一个 junior developer,在建议其他成员使用规范开发项目时,得到的回应通常是:我的功能都已经实现了,干嘛要改;不符合规范又怎么样,要改你改时。有时候也是挺无力的,算是个人的一点牢骚吧。

软件设计的一点不足

还是拿订单系统和商品系统来说事,虽然两个系统在物理上被拆分开了,但如果需要展示订单列表,订单详情,如今系统的设计会发起多次的远程调用,用于查询订单的归属商品,这是违背领域驱动设计的。订单中的商品就应当是归属于订单模块,正确的设计应该是使用冗余,代替频繁的跨网络节点远程调用。

另外一点便是高可用,由于机器内存的限制,所有的系统都只部署了单个实例,这其实并不是微服务的最佳实践。从系统应用,到 zookeeper,redis,mq 等中间件,都应当保证高可用,避免单点问题。没有真正实现做到横向扩展(知识理论上实现了),实在是有点遗憾。

系统没有熔断,降级处理,在新的项目中,由于我们引入了 Spring Cloud,很多地方都可以 out of box 式使用框架提供的 fallback 处理,而这上一个电商项目由于框架的限制以及接口设计之初就没有预想到要做这样的操作,使得可靠性再减了几分。

自动化运维的缺失

单体式应用的美好时代,只需要发布同一份 war 包。而微服务项目中,一切都变得不同,在我们这个不算特别庞大的电商系统中,需要被运行的服务模块也到达了 30-40 个。由于这个电商系统是部署在甲方自己的服务器中,一方面是业务部门的业务审批流程,一方面是如此众多的 jar 包运行,没有自动发布,没有持续集成。令我比较难忘的是初期发布版本,始终有一两个服务莫名奇妙的挂掉,对着终端中的服务列表,一个个排查,这种痛苦的经历。至今,这个系统仍然依靠运维人员,手动管理版本。

上一个项目有一些不可控的项目因素,而新的项目中,系统服务全部在阿里云上部署,也引入了 Jenkins,一切都在逐渐变好,其他的 devops 工具仍然需要完善,以及 docker 一类的容器技术还未在计划日程之内,这些都是我们今年努力的目标。

总结

原本积累了很多自己的想法,可惜落笔之后能够捕捉到一些点,便只汇聚成了上述这些,而这上一个电商项目在逐渐的迭代开发之后也变得越来越好了(我去了新的项目组后,其他同事负责了后续的开发)。这个经历,于我是非常珍贵的,它比那些大牛直接告诉我微服务设计的要素要更加有意义。知道了不足之处,经历了自己解决问题的过程,才会了解到好的方案的优势,了解到开源方案到底是为了解决什么样的问题而设计的。

分享到