我在实际给大家分享的时候有放一些电影片段来做对照。写代码好像做翻译一样,就是把需求翻译成代码。能体现创造性吗?
可以。
产品经理不会要求但是不得不做的隐形需求:系统健壮性、稳定性、扩展性。数据的一致性、准确性。资源方面:系统开销、容量评估。
基础方面
系统日志有没有打好。打的太多,日志重复多,滚的太快,不容易追踪。打得少,遇到问题没有抓手。我之前的开发习惯是在每个考虑的点都加日志。我能够看日志就能顺下来整个流程思路。在测试阶段再做减法。
TDD,测试驱动开发。你的测试有多细,展示了你的思考有多细。
三种设计方法
n+m返回值设计
概述
一个接口入参有n个值,需要的返回值是m个值。在返回值中可以把入参和返回值一起返回。
这种设计便于追踪和排查问题:
为了防止调用方没有日志追踪号、打印日志过多等原因造成不方便跟踪请求的问题,可以设计返回值将入参一起打印。这样调用方可以通过一条日志方便的获取到入参和返回值。线上排查问题会非常方便。
建议使用场景
假设有个场景,调用方的系统设计不方便请求追踪,比如没有线程追踪号。在高并发量场景下可能日志是这样的:
1、thread-1|ClassName|参数1:11,参数2:11
2、thread-2|ClassName|参数1:22,参数2:22
3、thread-1|ClassName|参数1:33,参数2:33
4、thread-2|ClassName|返回值:44
5、thread-1|ClassName|返回值:55
6、thread-1|ClassName|返回值:66
你能决定55和66对应的谁是1,谁是3的返回值吗?
但是如果返回值包含了入参,就好办多了,举个返回值例子:
thread-1|ClassName|{参数1:11,参数2:11,返回值:55}
这样就一目了然。有的时候可能是调用方本身的设计问题,但是如果被调用方能通过巧妙的设计帮助调用方。一旦遇到问题,调用方可以自己先进行排查,不用马上联系被调用方帮忙。即节约了自身的成本,又体现了专业性,何乐不为。
不建议使用场景
1、公司内有规范的、完善的、各个部门严格执行的全链路追踪标准
2、入参很大,不建议全量打印,可选取部分关键或者完全不用
终态设计
概述
在《实战并发-使用分布式缓存和有限状态机》里我讲过有限状态自动机。有限状态机涉及状态流转。状态从分类上可以分成三种:初始状态、中间状态和终态。
有限状态机的重点在于有限,要有起点和终点。也就是一定要有终态。在《稳定性三十六计-超时处理》我讲过:
在传统的单机系统中,调用一个函数,要么返回成功,要么返回失败。这就是两态系统(2-state system)。
在分布式系统中,由于系统是分布在不同机器上的。还可能有一种状态叫:超时。成功、失败和超时是分布式系统调用的三态。
超时不是终态,而是一种中间状态:最终有可能下游是成功了,也有可能是失败了。这时候我们需要在超时之后推定一种状态,推定成功或者失败。究竟是成功还是失败因功能而定。
建议使用场景
比如付款操作,不知道是否成功就推定是成功的,那用户可能没有付款就拿到了商品或者享受了服务。商家就会资金损失。所以一般会推定失败。让用户再次支付。最终通过查询或者对账发现用户实际是支付成功的,可以再把钱给用户退回去,保证交易的公平性。
退款恰恰相反,需要推定成功。告诉用户,钱退给你了。最终通过查询或者对账发现实际是退款失败了,可以系统重新发起退款,直到真正退成功为止。
后台管理系统也很需要这种终态设计。比如发布系统,发布了一个功能,发布系统如果出现了问题,这次发布没有结束。用户可能没有办法进行下一次发布。这时候可以设置超时自动结束,防止未结束的流程始终在那里,起码会干扰视线,增加判断成本。
不建议使用场景
这十几年的开发还没有遇到过什么场景不需要终态。但是有些终态是隐式的。举个例子:
在《一个反直觉的sql》里提到的事件溯源模式,就是说比如在一张数据表中,只做数据记录用,只有插入操作没有更新操作。比如一个用户行为记录表,会记录用户什么时候登陆,因为用户是APP登陆的,所以2年都没有登出操作。是不是就没有终态呢?我们从这张表本身来看,因为是事件记录,所以落库的那一瞬间,状态就是“记录完成”,本身就是终态。
两码一态
概述
两码是指系统码和业务码,一态是说通过两码就可以确定最终的状态。
很多情况下咱们是已经隐式的使用了两码一态,比如自己设计了一个单点登录接口给外部调用。一般是这样设计:
实际上这个接口有个返回值,比如登录成功会返回用户信息或者直接返回是否成功的布尔值。但是这个接口是给外部调用的,需要包装一层,最终就是返回Result<User>或者Result<Boolean>。
@Data
public class Result<T> {
private boolean isSuccess;
private String errorCode;
private String errorMessage;
private T data;
}
有没有感觉和自己平时写的代码很像?
分析一下:最外层isSuccess很多人都把它当成最终的状态来使用。比如在这个登录的场景下就代表了登录成功。但是如果isSuccess=true但是data==null?这就让人很迷惑,究竟是成功还是没有成功呢。成功了为啥我的数据没有吐给我?
稍微合理一点的是isSuccess=true代表系统处理成功,就是没有抛出什么异常。究竟业务是否成功看data。
在更加规范的场合是这么定义的:
@Data
public class Result<T> {
private String sysCode;
private String sysMessage;
private String bizCode;
private String bizMessage;
private T data;
}
如果系统码sysCode为成功,是否业务成功需要看bizCode;如果sysCode失败,bizCode就不用看了,也很可能根本就没有拿到;如果sysCode为失败,看系统码是多少就可以定位问题范围,或者至少说这个问题开发人员应该查查;如果bizCode为失败,如果业务码也能定位问题范围,而这种问题一般不需要开发人员来处理。
建议使用场景
比如后台管理系统中,可以定义:连接数据库异常、空指针异常等为系统失败,对两种异常分配不同的系统码,比如S001、S002;系统正常码可以分配S000;必填参数为空、参数校验失败为业务码,比如B001、B002;业务正常可以分配B000。最终状态由两个码共同决定。当系统码异常开发人员需要处理。当业务码异常可以联系运营人员培训一下怎么老填错或者不处理。
比如我在之前的文章《一个http请求进来都经过了什么(2021版)》中设立过一个场景
假设我在超市买了我喜爱的经典搭配:烤肠+酸奶。然后我就微信扫码付款了。付款时序图大体是这样的:
不用理解这个图,关系不大。我要说的是像支付这种场景后面会涉及多个环节,比如微信支付自身还要调用银行呢。
这时候的系统码和状态码中可以加一位,代表那个环节。比如系统码定义:
WS000、WS001……BS000、BS001……
S代表系统。W代表微信,比如下游请求银行,银行处理成功了,自身异常了,会设置这个码;B代表银行,比如银行自身异常了会设置这个码。
同理,业务码定义:
WB000、WB001……BB000、BB001……
第二个B代表业务。W代表微信,比如自身校验没有通过,根本不会请求银行,会设置这个码;第一个B代表银行,比如银行最后发现用户余额不足会设置这个码。
不建议使用场景
如果是给一个进程内部自身使用的,当然就没有必要定义这么复杂。