调用一个RPC服务的三重境界

开篇词

毫无疑问微服务架构是目前最主流的大型互联网应用系统架构方式,因为一个大型系统被拆分为若干个子应用,故子应用之间相互调用进行数据读写这件事情变得像呼吸一样普遍。每个一个程序员都能够写代码实现一个RPC服务的调用,但不同的实现方式体现着程序员的不同境界,今天就来探讨一下调用一个RPC服务的三重境界。

初阶:平铺直叙

先上结论:平铺直叙的方式没有隔离技术复杂度与业务复杂度,随着时间的推移,很难从代码中还原业务场景。

简单需求

假定现在有这么一个很简单的需求:你需要根据交易单号查询到交易单,解析交易单获取到该笔交易单对应的收货地址信息,根据收货地址信息查询快递资源信息并返回。你可能不假思索地就写出如下代码。

public class ExpressBizService {@Autowiredprivate TradeRpcService tradeRpcService;@Autowiredprivate ResourceRpcService resourceRpcService;public ResourceInfo queryExpressResourceByTradeId(Long tradeId) {SingleQueryResultDO order = tradeRpcService.getOrderById(tradeId);LogisticsOrderDO logisticsOrder = order.getLogisticsOrder();String city = logisticsOrder.getCity();String area = logisticsOrder.getArea();String town = logisticsOrder.getTown();return resourceRpcService.queryResourceByAddressInfo(city, area, town);;}
}

但是RPC调用和本地调用相比,RPC调用是不可控的,第一要有网络开销,第二服务是别人提供的,万一被人挂了直接抛异常了怎么办?

那就加个try catch呗,把异常catch并处理一下。于是代码变成了这样:

public class ExpressBizService {@Autowiredprivate TradeRpcService tradeRpcService;@Autowiredprivate ResourceRpcService resourceRpcService;public ResourceInfo queryExpressResourceByTradeId(Long tradeId) {try {SingleQueryResultDO order = tradeRpcService.getOrderById(tradeId);LogisticsOrderDO logisticsOrder = order.getLogisticsOrder();String city = logisticsOrder.getCity();String area = logisticsOrder.getArea();String town = logisticsOrder.getTown();return resourceRpcService.queryResourceByAddressInfo(city, area, town);} catch (Throwable t) {// 网络异常进行重试if (t instanceof NetworkException) {throw new RetryException(t);}// 其他异常不重试return null;}}
}

加个缓存

上线运行了几天时间,你发现RPC查询速度太慢了,调用交易域提供的服务查询交易单需要15ms左右,导致queryExpressResourceByTradeId方法需要20ms左右。而业务预期是10ms左右,那怎么办?加缓存呗!

  1. 先查缓存,若命中缓存,则直接从缓存中解析地址信息,
  2. 若未命中缓存,则先查询交易单,再写缓存,再查询资源域并返回结果
  3. 处理缓存相关的异常

于是代码又进化成了这样:

public class ExpressBizService {@Autowiredprivate TradeRpcService tradeRpcService;@Autowiredprivate ResourceRpcService resourceRpcService;@Autowiredprivate RedisDataSource redisDataSource;public ResourceInfo queryExpressResourceByTradeId(Long tradeId) {try {String json = resourceRpcService.get(tradeId);// 若命中缓存,则直接从缓存中解析地址信息, if (StringUtils.isNotBlank(json)) {LogisticsOrderDO cache = JSONObject.parseObject(json, LogisticsOrderDO.class);return resourceRpcService.queryResourceByAddressInfo(cache.getCity(), cache.getArea(), cache.getTown());} else {//若未命中缓存,则先查询交易单,再写缓存,再查询资源域并返回结果SingleQueryResultDO order = tradeRpcService.getOrderById(tradeId);LogisticsOrderDO logisticsOrder = order.getLogisticsOrder();String city = logisticsOrder.getCity();String area = logisticsOrder.getArea();String town = logisticsOrder.getTown();resourceRpcService.put(tradeId, JSONObject.toJSONString(logisticsOrder));return resourceRpcService.queryResourceByAddressInfo(city, area, town);}} catch (Throwable t) {// RPC网络异常进行重试if (t instanceof NetworkException) {throw new RetryException(t);}// Redis异常进行重试if(t instanceof RedisBusyException){throw new RetryException(t);}// json解析异常要记录原因,秋后算账if(t instanceof JsonParseException){log.error("JsonParseException"+tradeId);}// 其他异常不重试return null;}}
}

加个鉴权

代码又在线上跑了几天,然后你收到一封交易域发来的邮件,因为数据安全原因,将不再提供明文的收货地址信息,需要先查询加密的收货地址信息,然后根据AK SK去解密出明文收货地址信息。

得嘞~这又得改动。于是代码又变成了这样:

public class ExpressBizService {@Autowiredprivate TradeRpcService tradeRpcService;@Autowiredprivate TradeDecryptRpcService tradeDecryptRpcService;@Autowiredprivate ResourceRpcService resourceRpcService;@Autowiredprivate RedisDataSource redisDataSource;private final String APP_KEY = "xxx";private final String SECRET_KEY = "xxx";public ResourceInfo queryExpressResourceByTradeId(Long tradeId) {try {String json = resourceRpcService.get(tradeId);// 若命中缓存,则直接从缓存中解析地址信息,然后查询资源域并返回结果if (StringUtils.isNotBlank(json)) {LogisticsOrderDO cache = JSONObject.parseObject(json, LogisticsOrderDO.class);return resourceRpcService.queryResourceByAddressInfo(cache.getCity(), cache.getArea(), cache.getTown());} else {//若未命中缓存,则先查询交易单,再写缓存,再查询资源域并返回结果SingleQueryResultDO order = tradeRpcService.getOrderById(tradeId);// 查询加密的物流信息EncryptedLogisticsOrderDO encryptedLogisticsOrderDO = order.getEncryptedLogisticsOrderDO();// 解密物流信息DecryptedLogisticsOrderDO decryptedLogisticsOrderDO = tradeDecryptRpcService.decryptLogisticsOrderDO(APP_KEY,SECRET_KEY,encryptedLogisticsOrderDO);// 写缓存resourceRpcService.put(tradeId, JSONObject.toJSONString(decryptedLogisticsOrderDO));String city = decryptedLogisticsOrderDO.getCity();String area = decryptedLogisticsOrderDO.getArea();String town = decryptedLogisticsOrderDO.getTown();// 查询资源并返回结果return resourceRpcService.queryResourceByAddressInfo(city, area, town);}} catch (Throwable t) {// RPC网络异常进行重试if (t instanceof NetworkException) {throw new RetryException(t);}// Redis异常进行重试if(t instanceof RedisBusyException){throw new RetryException(t);}// json解析异常要记录原因,秋后找负责写缓存的同学算账if(t instanceof JsonParseException){log.error("JsonParseException"+tradeId);}// 其他异常不重试return null;}}
}

模型变了

这时候你又发现,代码怎么这么呢?原因是因为之前缓存的明文的地址信息LogisticsOrderDO,现在交易域改动了,LogisticsOrderDO没有了,变成了EncryptedLogisticsOrderDO。代码里之前依赖的LogisticsOrderDO找不到了,所以代码一片红。所以又得适配交易域模型的变化。

于是写缓存的代码又要修改:

// 若命中缓存,则直接从缓存中解析地址信息,然后查询资源域并返回结果if (StringUtils.isNotBlank(json)) {DecryptedLogisticsOrderDO cache = JSONObject.parseObject(json, DecryptedLogisticsOrderDO.class);return resourceRpcService.queryResourceByAddressInfo(cache.getCity(), cache.getArea(), cache.getTown());}

腐化的味道

至此这段代码已经不能一眼看出最初的业务逻辑是什么了。而且这是在最初的业务逻辑特别简单的情况下,实际情况下业务逻辑可不会那么简单呀。后续如果有什么技术上面的变更,比如增加各种各样的灰度逻辑;为了应对高并发场景增加限流逻辑或者对交易提供的服务进行熔断;或者为了提高接口的处理效率将此接口变为批量接口,然后使用多线程查询交易单和资源信息,那代码会进一步复杂。

技术上变完了,业务逻辑也可能会变,业务做大了,各种By特殊场景的定制逻辑也来了,比如通用场景下 根据交易单收货地址信息查询资源域的快递资源信息,特殊case1 根据发货单地址信息查询资源域的快递资源信息,特殊case 2……。

技术在变复杂,业务也在变复杂,假设技术的复杂度为N1,业务的复杂度为N2。那么整个代码的复杂度不是N1+N2而是N1*N2。如果没有及时的治理与重构,那么后面的同学只能干一些屎上雕花的活了。

中阶:封装变化

先上结论:适配器防腐层。隔离技术复杂度与业务复杂度,隔离自己领域的模型与外部领域的模型。

定义模型,隔离依赖

我们再来看一遍这个需求:需要根据交易单号查询到交易单,解析交易单获取到该笔交易单对应的收货地址信息,根据收货地址信息查询快递资源信息并返回。

查询交易单并解析是一个行为,是一个技术手段,其目的在于获取收货地址信息。就相当于坐火车去北京,坐火车是一个去北京的方式,但坐火车或者可以分为坐卧铺、坐硬座、站票,甚至可以不做火车,坐飞机、坐轮船、坐火箭🚀。但不管怎样,北京这一目的地是不变的。如同上面一样,查交易单可以直接查询、可以先查缓存再查交易单、也可以先查缓存再查加密后的交易单再解密,但最后都为为了获取到收货地址信息。行为是多变的目的是相对稳定的(那如果整个需求都改了那就没办法了)。

所以,真正需要的是什么?是收货地址信息。在中国 省、市、区、街道这四个字段已经能描述一个较为准确的地址了,而且是不变的。虽然收货地址这一模型是稳定的,但获取收货地址的方式是多变的,那我们就把变化的部分封装起来,把变化控制在最小范围,不让其扩散污染到其他的代码。以此来保持主要业务逻辑代码是整洁清晰易于维护的。

这时候可以写一个包装类Wrapper),把查询交易单这个技术动作进行包装(封装技术细节)以及把交易单模型转化为业务所需的地址模型(防止外部模型污染内部业务逻辑)。同样地也可以对查询缓存的技术动作以及缓存模型进行封装。查询交易单,查询缓存都是为了获取地址信息地址,那就抽象出一个地址仓库Repository)把获取地址这段逻辑进行封装。(坐火车,坐飞机都是去北京,所以可以抽象出:乘坐交通工具去北京,后面如果想要打车去北京也可以说是乘坐交通工具去北京)所以从交易单、缓存查询地址的逻辑的代码结构会变成这样:

后面如果获取地址的方式再发生变化,只需要修改包装类以及仓库即可,不会影响到上层的业务逻辑。

打破封装,破窗效应

这个时候代码结构合理,模型内聚,ExpressBizService.java又恢复了一眼就能看懂业务逻辑的状态,然后因为业务调整,这块代码由小A交给小B同学了,小B同学接手后了一个新需求是根据快递信息(快递公司品牌、快递公司编码)查询快递资源详细信息并返回,而且快递信息就在交易单上面。因为是紧急需求,本着怎么来怎么快的原则,新同学没做抽象没搞封装直接又把tradeRpcService.java放进了ExpressBizService.java,(技术复杂度与业务复杂度又糅合在了一起)小B同学干了一段时间离职了,小C同学又接手了这段代码。这时候需求变更来了:

小A时代的逻辑:你需要根据交易单号查询到交易单,解析交易单获取到该笔交易单对应的收货地址信息,根据收货地址信息查询快递资源信息

小C时代的逻辑:你需要根据交易单号查询到交易单,解析交易单获取到该笔交易单对应的收货地址信息,根据收货地址信息查询快递资源信息。但是在预售场景下需要使用预售交易单。解析预售交易单获取到该笔交易对应的收货地址信息,根据收货地址信息查询快递资源信息。(假定针对预售场景创建一张预售单据是合理的,此处只做举例使用)。而tradeRpcService.javaqueryPreSaleOrder()方法查询预售交易单。

小C一看,tradeRpcService.java已经在ExpressBizService.java里面了呀。那我直接调用queryPreSaleOrder然后再处理就行了呗。于是即使ExpressBizService.javaqueryExpressResourceByTradeId()方法已经做到了结构合理、模型内聚,因为ExpressBizService.java被污染了导致queryExpressResourceByTradeId()方法也不能幸免。(风平浪静的大海掀起波涛,海上的小船怎么能独善其身呢?)这就是破窗效应。

破窗效应是社会学概念之一,也可以理解为“窗户理论”或“破窗理论”。它最早由美国学者威尔逊·凯利(Wilson Kelling)和乔治·凯利(George Kelling)于1982年提出。

破窗效应的基本观点是,如果一个社区或环境中存在一个被破坏的窗户,如果不及时修复,其他人很可能会受到影响,导致整个环境的质量逐渐下降。这是因为人们会认为这个社区或环境已经被人放任不管,没有秩序和规则,从而产生更多的破坏行为。

破窗效应主要强调环境的外在形象和秩序对个体行为产生的影响。它认为,人们的行为受到周围环境的影响。在一个有序、整洁、规范的社区中,人们更有可能遵守法律和规则,保持社会秩序。相反,在一个杂乱、破坏、缺乏秩序的环境中,人们更容易产生不良行为、违法行为甚至破坏行为。

后面的故事也很容易被猜到,出于性能考虑需要对于预售交易单加缓存,出于安全合格考虑,预售交易单上面的用户信息也需要加密解密,然后代码又走上了腐坏的道路……

高阶:依赖倒置

先上结论:高层代码定义标准,底层代码实现标准。

高层代码指的是那些容易变动的业务代码,底层代码指的较为稳定的基础代码(访问数据库、缓存、调用外域HSF接口)。在逻辑层面高层代码虽然要依赖底层代码对数据进行读写,但是数据的模型以及对数据操作的行为是高层代码所定义的

落实到代码层面是这样的:高层代码和底层代码分别位于两个模块之中,高层代码在应用层,底层代码在基础设施层。数据模型以及对数据的操作抽象成接口,且接口放在应用层,底层代码要实现接口,则基础设施层需要依赖应用层,这样应用层就不能再依赖基础设施层了(循环依赖,编译报错)。所以后面接手的同学很难去打破前人的合理设计。

这样在编码过程中,高层代码是不可能感知到底层代码的模型与行为的,因为已经通过接口隔离了。从而让高层代码专注业务逻辑,底层代码专注技术细节。程序运行时高层代码通过接口注入真正的底层实现类,在逻辑上完成调用底层代码。

正常情况下调用顺序和依赖顺序应该是一致,要调用基础设施层的代码就应该在POM文件里明确依赖基础设施层。但是在这里却是基础设施层依赖了应用层,这就是依赖倒置。通过依赖倒置就能实现高层代码定标准,底层代码做实现,同时两个层次的代码通过接口隔离,互不干涉影响。

面向过程vs面向对象

结论:有面向对象的思维才能写出面向对象的代码,即高内聚、低耦合、易拓展、易维护的代码

初阶与高阶看似是写代码的不同,实际上是背后思维方式的不同。 初阶是面向过程,高阶是面向对象。

从最开始学习Java之时,我们都知道Java是一门面向对象的语言,关于面向对象的理论大家都是滚瓜烂熟,信手拈来,但是当真正写代码的时候,却是基于贫血模型写了一段又一段冗长的面向过程的脚本代码。没有抽象,没有封装,也没有内聚,几经转手之后就变成了开发同学们所憎恶的“屎山代码”。

只具备面向过程思维的程序员,当面对一个需求时,首先考虑的是具体的实现步骤,先干嘛再干嘛最后干嘛。如果一开始就陷入了细节,那么产出的代码一定是没有抽象的过程式的脚步代码。

面向过程的思维方式在应对简单的业务逻辑之时毫无问题,但是当业务逻辑变复杂,且由多个程序员共同开发实现的时候,代码大概率会变得复杂难以维护。因为面向过程的代码,它描述的都是细节。人脑善于思考善于逻辑推理,但不善于存储,也就是说我们拥有一个非常牛逼的CPU,但是一级缓存、二级缓存、三级缓存以及内存都少得可怜。因此通读面向过程的代码很容易就迷失在细节里面,尤其是遇到多层方法嵌套的场景,不知不觉就陷入代码的泥潭里面。

换句话说,从抽象到具体,从概要到详细是容易理解的,但是从具体到抽象、从详细到概要是比较困难的。举个极端例子:你能很容易地根据E=M*C^2算出来一个苹果的能量(抽象-->具体),但是让你从复杂的现实世界推到出这个方程能做到吗?(具体-->抽象)

所以我们需要具备面向对象的思维方式来应对复杂的业务需求,当遇到一个问题时首先考虑的是这个问题域里面有哪些对象?这些对象有哪些属性以及行为?这些对象直接的关系如何?业务代码如何编排这些对象来解决问题?这种思维方式会引导者我们去分解问题,去不断剖析问题的本质,最终产出的代码也是从抽象到具体的。

具体的代码大家都会写,那么抽象的代码怎么写呢?其实业界也早就有了答案,简单来说就四个字“面向对象”。复杂来说就是solid原则、各种设计模式、领域驱动设计……首先要学会这些知识,然后尝试应用知识,应用之后再反过来体悟已学过的知识,如此反复螺旋式地不断加深理解,最终方能游刃有余地驾驭“面向对象”这把屠龙宝刀,在代码的江湖里面独占一席之地。
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/143339.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

天软特色因子看板 (2023.11 第10期)

该因子看板跟踪天软特色因子A05006(近一月单笔流入流出金额之比(%)该因子为近一个月单笔流入流出金额之比(%)均值因子,用以刻画 市场日内分时成交中流入、流出成交金额的差异性特点,发掘市场主力资金的作用机制。 今日为该因子跟踪第10期,跟踪…

HarmonyOS 学习记录

时光荏苒,岁月如梭,韶华不负,未来可期。转眼间已经30岁了,学习的重要性不言而喻,在接下来的日子里记录下自己学习HarmonyOS的过程。增加一下知识储备,防患于未然嘛 不得不说华为的开发文档写的不错,开发工具直接安装后自动配置环境…

【Ubuntu】Windows访问Ubuntu时“需要认证”界面卡住

情况描述 基本情况 本地电脑:Microsoft Windows [版本 10.0.19045.3570] 远程电脑:Ubuntu 20.04.6 LTS 远程电脑安装辅助远程工具:xrdp 0.9.12 问题描述:认证页面输入密码,点击认证以后认证按钮不可点击,无…

Vue修饰符(Vue事件修饰符、Vue按键修饰符)

目录 前言 Vue事件修饰符 列举较常用的事件修饰符 .stop .prevent .capture .once Vue按键修饰符 四个特殊键 获取某个键的按键修饰符 前言 本文介绍Vue修饰符,包括Vue事件修饰符以及按键修饰符 Vue事件修饰符 列举较常用的事件修饰符 .stop: …

半平面求交 - 洛谷 - P3194 [HNOI2008] 水平可见直线

欢迎关注更多精彩 关注我,学习常用算法与数据结构,一题多解,降维打击。 往期相关背景点击前往 题目大意 题目链接 https://www.luogu.com.cn/problem/P3194 在直角坐标系中给定一些直线,然后从Y轴无穷大处往0处看,…

EDA实验------数控分频器设计(QuartusII)

目录 一、实验目的 二、实验原理 三、实验内容 四、实验步骤 五、注意事项 六、思考题 七、实验过程 分频器的基本原理 什么是分频器? 如何去分频? 1.创建新项目 2.创建Verilog文件,写入代码 3.连接电路 ​编辑 锁相环的创建 4…

ubuntu18.04安装google浏览器

下载google安装包 wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb 安装google浏览器 sudo dpkg -i google-chrome-stable_current_amd64.deb 执行安装 sudo apt-get -f install 启动浏览器 在应用程序中找到google图标点击运行

物联网AI MicroPython学习之语法 GPIO输入输出模块

学物联网,来万物简单IoT物联网!! GPIO 介绍 模块功能: GPIO通用输入输出。 接口说明 GPIO - 构建GPIO对象 函数原型:Pin(port, dir , pull)参数说明: 参数类型必选参数?说明portintY对应开发板的引脚号…

基础课4——客服中心管理者面临的挑战

客服管理者在当今的数字化时代也面临着许多挑战。以下是一些主要的挑战: 同行业竞争加剧:客服行业面临着来自同行业的竞争压力。为了获得竞争优势,企业需要不断提高自身的产品和服务质量,同时还需要不断降低成本、提高效率。然而…

【入门Flink】- 11Flink实现动态TopN

基本处理函数(ProcessFunction) stream.process(new MyProcessFunction())方法需要传入一个 ProcessFunction 作为参数,ProcessFunction 不是接口 , 而是一个抽象类 ,继承了AbstractRichFunction,所有的处…

Oracle(2-2)Oracle Net Architecture

文章目录 一、基础知识1、Oracle Net Connections Oracle网络连接2、C/S Application Connection C/S应用程序连接3、OSI Communication Layers OSI通信层4、Oracle Protocol Support Oracle协议支持5、B/S Application Connections B/S应用程序连接6、TwoTypes JDBC Drivers 两…

Vue 2学习(路由、history 和 hash 模式、)-day014

一、路由简介 路由(route)就是一组 key-value 的对应关系多个路由,需要经过路由器(router)的管理 在 Vue 中也有路由,Vue 中的路由主要是通过 vue-rounter 这个插件库来实现,它的作用就是专门用…

力扣双周赛 -- 117(容斥原理专场)

class Solution { public:long long c2(long long n){return n > 1? n * (n - 1) / 2 : 0;}long long distributeCandies(int n, int limit) {return c2(n 2) - 3 * c2(n - limit 1) 3 * c2(n - 2 * limit) - c2(n - 3 * limit - 1);} };

Python+selenium自动化测试

批量执行完用例后,生成的测试报告是文本形式的,不够直观,为了更好的展示测试报告,最好是生成HTML格式的。 unittest里面是不能生成html格式报告的,需要导入一个第三方的模块:HTMLTestRunner 一、导入HTML…

Peoeasy机器人:原点无法重置问题

机械手在伺服关闭的模式下,插入定位插销,进入机构设定重置原点,发现PUU值没有变化 问题原因 台达软件版本比较多,每个版本重置原点的模式和马达偏差角的默认值是有一定差异的。再重置原点之前尽可能先确认一下重置原点的模式和马…

博流BL602芯片 - 烧录配置

硬件介绍 淘宝上买的核心板,大概结构如上。 直接插入电脑usb,即可实现供电、下载(控制BOOT/EN)、串口通讯 固件包 1、环境配置 1.1串口 开发板使用了 CH340G 的 USB 转串口芯片,自行安装CH340串口驱动。 1.2编译环境…

【Android】统一系统动画

需求:除panel动画效果为弹出之外,其余的应用效果为渐入渐出 从系统层面统一把控动画效果,而不是单个应用自己处理 Android系统版本:9.0 代码地址 \frameworks\base\core\res\res\values\styles.xml 当时看注释,以为…

iOS:何为空指针和野指针

一:什么是空指针和野指针 1、空指针 ①.没有存储任何内存地址的指针就成为空指针(NULL指针) ②.空指针就是被赋值为0的指针,在没有被具体初始化之前,其值为0. //以下都是空指针,eg: Person *p1 NULL; …

RGB转Bayer,一个小数点引发的血案

前几天写了一个RGB数据转Bayer格式的函数,经过测试功能正常。后来把这个函数用到一个数据库构建中,结果数据库出来的结果一直是一张黑图,追查了好几个小时,总算把这只虫子找出来了,原来是一个整数后面的小数点作祟。 …