40

朱晔的互联网架构实践心得 S2E1:业务代码究竟难不难写?

 5 years ago
source link: https://mp.weixin.qq.com/s/2zExPco6Ug_N2ztVL5ECtA?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

注意,这是我的架构实践心得的第二季的系列文章,第一季有10篇你也可以回顾。

最近我一直在思考几个问题:

  • 业务代码究竟难不难写?

  • 一直开发业务代码是不是完全学不到东西?

  • 5年+开发经验的老程序员的价值在哪里?

  • 如何通过面试来区分业务代码开发的水平?

其实,这几个问题或多或少是相互关联的。有的时候大家也会自嘲说,“程序员接手的代码永远是烂摊子,然后自己继续在这个烂摊子上产出代码,留给又一波后人接手”。十几年来经历过十来个公司,我看了不少差的代码,也看了不少好的代码,自己产出过垃圾代码,也带领团队实现过一些自认为不错的代码。

你可能会说,业务代码就是增删改查,和框架代码的难度不能比,完全是机械劳动,其实我觉得不完全是这样,甚至完全不是这样,我个人认为写出能跑的业务代码不难,但要写出好的业务代码其实是挺难的,更重要的是如果系统设计的足够好,在很长一段时间内系统的可维护性是可控的,只需要简单扩展即可,如果基础打的不够好,那么项目可能就是一次性项目,下面我列出业务系统我关注的一些点,你想想是不是有道理。

标准化

标准的项目结构

我自己非常注重搭建项目结构的起步过程,模块的划分、目录(包)的命名,我觉得非常重要,如果做的足够好,别人导入项目后可能只需要10分钟就可以大概了解结构了。

1、有些名词是约定俗成的,大家一眼就能看出是啥东西的,比如:

  • controllers

  • services

  • configs

  • utils

  • commons

  • jobs

比较重要的是确定先进行分类再分业务,还是先分业务再分类,在代码里混用这两种风格的结构就会很混乱:

  • controllers

    • order

    • user

  • jobs

    • order

    • user

  • order

    • services

    • mappers

  • user

    • services

    • mappers

对于直筒的三层架构的纯数据表驱动的代码我建议第一层是分类,第二层是业务功能:

  • 看一眼controllers目录我们知道项目对外的Api能力

  • 看一眼services目录我们知道项目的逻辑复杂度

  • 看一眼mappers目录我们知道项目的表结构

对于有一些项目,不一定每一个逻辑都涉及到三层架构,数据流量比较复杂,我建议是按照业务功能先来分,下一层视情况也不一定完全是需要按照组件类型分二级目录,也可以是按功能来分:

  • core

    • storage

    • service

  • dispatcher

    • engine

    • context

  • callback

    • gateway

    • handler

  • notification

    • sms

    • push

对于这种目录结构一眼望去就能知道大概项目数据流和架构了,core对内,dispatcher做分发的感觉,callback是外部来的回调数据,notification是通知外部的数据流。这种数据流向复杂的项目,使用这种结构会比前一种合理的多,因为我们需要先关注数据流,而不是三层结构的层次,甚至对于core、dispatcher、notification我们知道其实是没有controller的。

2、有些名词可能就需要内部有一个统一,比如不同的层次面向数据库,面向业务,面向UI,面向RPC需要有不同的POJO,我们需要明确一套对应的命名,能明确就好,比如下面的这些POJO我们其实挺难分辨其用途的,需要进行规范,并且放置于匹配的目录结构中:

  • CreateOrderRequest / CreateOrderResponse

  • CreateOrderParam / CreateOrderDto

我们可以约定第一组用于服务本身访问外部(的Rpc服务也好,REST服务也好),第二组用于服务本身对外提供的Web Api,比如:

  • controllers

    • queryOrder()

    • createOrder()

    • OrderController

    • QueryOrderParam

    • QueryOrderDto

    • CreateOrderParam

    • CreateOrderDto

  • rpcs

    • login()

    • register()

    • UserService

    • LoginRequest

    • LoginResponse

    • RegisterRequest

    • RegisterResponse

  • services

    • OrderService

    • OrderServiceImpl

    • OrderEntity

  • storages

    • OrderMapper

    • OrderModel

总之,虽然可能10+人在维护相同的项目,目录结构的风格、命名、专用名词的使用一定要统一。

统一的框架

这个需要在开展项目之前明确下来,我见过有项目中同时使用了Spring MVC和Jersey做Web API,同时使用了Spring Scheduler和Quartz做任务调度。最好是项目开展前明确框架的版本并且搭建好项目脚手架,大概涉及:

  • Web API / Web MVC

  • Job Scheduler

  • Micro Service

  • Config Center

  • Redis Client

  • Data Access

  • Entity Mapper

  • MQ Client

当然,我们也可以独立出依赖管理的项目,专门由独立模块进行依赖版本管理。最差也要在Wiki上进行明确。

统一的API

如果项目涉及到对外提供API,那么非常有必要在初期就规范API的框架定义,涉及到:

  • 包装类 Result<T> 的定义(见过一个项目用了三种包装类的)以及遇到错误的情况下,Http状态码的体现 比如这样的包装类格式:

public class ApiResult<T> {    
boolean success;
    String code;
    String message;
    String path;    
long time;
    T data;
}

我们可以这么和客户端的开发来明确:

1、即使遇到错误,Http状态码还是200,Http状态码如果是500或是404的话那一定是网关层面的错误了,这个错误不是后端服务返回的

2、在Http状态码还是200的时候代表收到了后端的返回,前端去按照ApiResult以Json格式反序列化Http Body的报文

3、然后查看success(如果没success也行,我们可以约定code是200就是成功),如果是success代表后端服务成功处理了请求,如果不成功,则根据后端给的错误代码映射表根据code进行处理或直接提示message中的内容。注意,这里的success只代表后端是否成功处理了请求,不代表请求代表的业务逻辑是否成功处理。举一个例子,如果这个请求是异步支付请求,那么success==true代表前端给的参数都正确,后端正确接受了支付申请,不代表支付成功

4、在success==true的情况下再去解析data中的内容,拿取客户端需要的信息,还是前面的例子,data里可以是 {"orderStatus":"PROCESSING", "orderId":"1234"} ,这个才是真正业务逻辑的数据和状态,success并不代表订单支付操作就是成功的,也可能是处理中的状态

所以这是几个层次的事情,Http Status->ApiResult.status->ApiResult.data.orderStatus

  • 加解密规范和签名规范 Api的加密解密以及签名最好在设计的时候就考虑进去,而且要仔细斟酌,否则以后很难变更,特别Api的使用方是客户端的情况,客户端很难轻易强更。如果做SAAS服务,建议参考大厂的规范,比如亚马逊AWS的API规范或阿里云API的规范,不建议自己造轮子,大厂做的API规范都是经过安全方面的专家深度思考的。

  • 版本管理规范(比如Url path路由还是Http header路由) 如果使用了老版本的话,是否需要在返回内容中提示新版本的Url、版本号、老版本最后维护时间呢?这个就不展开了

所以统一Api这个事情不仅仅是Api的格式还涉及到安全处理、版本处理、客户端操作方式等等。对于一些需要服务端驱动客户端的业务(UI逻辑动态)来说,我们可以定义一套更复杂的ApiResult,让服务端告知客户端这个时候应该是提示还是跳转还是返回等等。

统一的源码工作模式

现在大家都使用Git,分支如何管理每一个公司(在Gitflow的基础上)都会略有不同,也需要和大家明确:

  • 分支的定义(master、develop、release、hotfix、feature)

  • 分支命名规范

  • checkout、merge request流程

  • 提测流程

  • 上线流程

  • Hotfix流程

别小这个,虽然这个和代码质量和架构无关,但是梳理清楚可以:

  • 提高开发和测试的工作效率,人多也乱

  • 减少甚至杜绝代码管理导致的线上事故

  • 让项目管理者和架构师可以明确什么代码现在在哪里

  • 方便运维处理发布和回滚

  • 让项目的开发可以灵活适应多变的需求

容错性

见过一些项目在实现业务代码的时候是不考虑任何异常处理、事务处理、锁处理的,在流量小无并发的情况下,这些项目不容易爆发出严重的问题,基本能用。但是对于高并发的项目或将来可能会高速发展的项目,如果不考虑这些问题会死的很难看。

我们来想一下,如果现在在设计一个订单服务,如果因为网络问题、并发问题导致数据错乱、服务中断的可能在千分之一,如果一个业务一天只有1000次请求,1天才遇到这样1次问题,即使遇到了问题用户也不一定会来反馈,即使来反馈往往客服也能通过后台取消订单等操作来处理,这个问题不会爆发出来,如果一天的单量是1000万,那么每天可能就会有10000单异常的订单,这个可能就超过了客服的处理能力了。

很少有项目真正100%完全做好了所有细节,只不过往往是因为量小得不到大家的重视罢了。但我们想一下,如果遇到数据库或中间件级别大规模故障的情况下,如果一致性处理的不好,那么数据库恢复后可能会留下一大堆异常数据需要修复,如果处理的好,业务数据不会错乱,数据库恢复后业务马上可以恢复。在遇到事故的时候,系统这方面的设计功力就体现出来了。

一致性处理

在实现代码的时候需要考虑如下事情:

  • 本地事务处理:见过一些代码完全不考虑事务,或者是只是知其然使用 @Transactional ,但是方法内部完全catch了所有异常的情况

    • 事务包含的方法块

    • 嵌套事务、事务传播

    • 什么时候遇到什么异常应该回滚

    • @Transactional 是否真正生效了?

  • 外部服务调用的事务问题

    • 调用外部服务出现异常的时候,本地事务怎么处理

    • 调用的外部服务是否允许重试(幂等调用)

    • 调用外部服务出现未知结果后,怎么进行补偿

    • 补偿是否有上限,是否存在死信数据卡死补偿的情况?

    • 如果有2+外部服务连同本地数据库存储都需要有事务性,怎么实现

  • 数据重复和顺序问题

    • 先本地事务提交还是先调用外部接口(如果先调用外部接口,可能会遇到外部回调的时候本地事务还没提交找不到数据的情况)

    • 从MQ收到的消息顺序问题怎么解决?

    • 重新入MQ的延迟消息或重试消息乱序是否会有问题?

    • 对外提供的Api或回调方法是否支持幂等?

  • 锁的问题

    • 哪个层面做锁?服务层分布式锁还是数据库层面锁?

    • 乐观锁还是悲观锁?

    • 你确信你的Redis锁方案是可靠的吗?

    • 你是否知道多少请求再排队等待,又是为什么?

这些要做好真的很难,每一步都需要认证考虑,但是很遗憾见过的很多具有复杂业务的代码,在Service中一连串调用了N个外部服务进行写操作,方法内也实现了N个表的写操作,即不考虑外部服务的事务和补偿问题,本地也没有事务控制,出了错只是打印了堆栈然后客户端看到的是一个服务器忙。

异常处理

异常处理不仅仅是狭义上遇到了Exception怎么去处理,还有各种业务逻辑遇到错误的时候我们怎么去处理。 就拿记日志这一件事情来说:

  • WARN和ERROR的选择需要好好考虑,WARN一般我倾向于记录可自恢复但值得关注的错误,ERROR代表了不能自己恢复的错误。对于业务处理遇到问题用ERROR不合理,对于catch到了异常也不是全用ERROR。

  • 记录哪些信息,最好打印一定的上下文(用户Id、订单Id、外部传来的关键数据)而不仅仅是打印线程栈。

  • 记录了上下问信息,是否要考虑日志脱敏问题?可以在框架层面实现,比如自定义实现logback的 ClassicConverter

我们知道catch到了异常或遇到了业务错误,我们除了记录日志还有很多选择,也需要认真考虑什么时候应该做什么:

  • 直接返回

  • 抛出异常

  • 重试处理

  • 恢复处理

  • 熔断处理

  • 降级处理

  • 甚至关闭业务

这又涉及到了弹力设计的话题,我们的系统往往会对接各种外部服务、Api,大部分服务都不会有SLA,即使有在大并发下我们也需要考虑外部服务不可用对自己的影响,会不会把自己拖死。我们总是希望:

  • 尽可能以小的代价通过尝试让业务可以完成

  • 如果外部服务基本不可用,而我们又同步调用外部服务的话,我们需要进行自我保护直接熔断,否则在持续的并发的情况下自己就会垮了

  • 如果外部服务特别重要,我们往往会考虑引入多个同类型的服务,根据价格、服务标准做路由,在出现问题的时候自动降级

架构设计

表的设计和Api的定义类似,属于那种开头没有开好,以后改变需要花10x代价的,我知道,一开始在业务不明确的情况下,设计出良好的一步到位的表结构很困难,但是至少我们可以做的是有一个好的标准:

  • 统一的附加字段,create_time,update_time,version等

  • 表的命名标准,比如 [domain]_[tablename]_[tabletype]

  • 字段类型、长度标准

  • 虽没有外键,但是外表关联字段和主表字段的命名标准

  • _id还是_no等字段命名的区别

除了标准,尽可能需要结合业务以及业务可能的扩展思考一下:

  • 1:N的可能性,是有1就足够了,还是一开始就要设计1:N的层次关系

  • 如果表字段可能会很多,业务变化多,是否考虑1:1甚至1:N的扩展表,把扩展字段从主表分开

  • 表的领域职责,表可能也会分上游、中游、下游,什么字段应该在哪里太重要了(我觉得表的领域相当于之前提到的项目结构中的包的分类,这个最好一开始定义清楚)

  • 关联表字段冗余冗余到什么程度,冗余字段的同步

  • 枚举的维护方式,是否考虑字典表?

对于表结构文档,我觉得列出字段类型、长度、说明是不够的,如果能结合业务代码梳理清楚下面这些,那这个文档就是真正有价值的 表结构文档

  • 记录由哪个业务模块创建

  • 数据重要程度

  • 数据归档方案

  • 字段数据源头

  • 字段会由谁更改

  • 字段可能会在哪里缓存

设计模式

我想90%的业务项目都是所谓的三层结构,Web层处理参数调用Service层做Db层的聚合,Db层基本就是代码生成或Orm框架补充少量的手写SQL。对于这样的项目,大部分人认为是没有设计的,也不需要设计。我认为那是因为没有好好思考:

  • 在我们写下if-else的时候,我们就可以考虑使用抽象类+具体实现类的方式来替代

  • 在实现层次化业务处理的时候,就可以考虑使用Filter或职责链模式来实现

  • 在封装外部Api的时候与其每次都写一套解析逻辑,我们是否考虑进行动态封装呢

  • 在数据改变后我们要记录改版轨迹,与其复制粘贴是否考虑过发布订阅模式

说白了就是利用各种设计模式和OO思想,来尽可能在业务变化需要扩展的时候:

  • 只是新增代码而不是修改代码

  • 尽可能减少重复代码复制粘贴

  • 尽可能让同类代码都呆在一起

  • 尽可能让直筒式的代码有层次

往大了说

在一个公司层面,如果有几十个,几百个业务项目,我们看这个公司的技术水平到了什么程度,我个人认为不仅仅是用了什么新技术,而是是否:

  • 具有统一的开发、服务框架

  • 具有统一的运维、监控、中间件、测试平台

  • 具有清晰的纵向领域划分

  • 具有清晰的横向基础平台服务和基础业务服务

  • 具有统一的代码工作模式

最简单的一个例子,一个业务从前到后跨10个事业部,100个服务,实现灰度测试,想想这件事情有多难?整个公司层面要实现步调一致的这些东西还确实很难,不仅仅是技术能力的体现,没有良好的组织架构,人心不齐,恐怕这些无法实现,实现了也无法推广,推广了也无法持续……当然,这些已经超出个人能做的了,作为程序员的我们应该从我做起,认真考虑前面提到的这些问题,至少在项目内部做良好的设计。

再来看看文首的问题,你看,虽然只是写业务代码,如果要写的足够好,必须要了解设计模式、理解各种弹力设计、理解事务、熟悉框架、了解中间件原理,怎么可能学不到东西,要实现健壮的业务代码,其实很难,要考虑的东西太多了,如果说写框架我们需要考虑不同的使用方和使用环境,这很难,写业务代码我们要考虑到千奇百怪的使用行为,要考虑到层次不起的对接方,这不比写框架简单。对于5年+经验丰富的程序员应当有能力开一个好头,或者说愿意在老代码上去做一些改变,否则你的价值在哪里呢?

本文只是展开了一些想到的内容,每一点都有很多东西可以写,也没时间一些子展开说太多,这些细节留在今后的文章慢慢展开了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK