上海品茶

您的当前位置:上海品茶 > 报告分类 > PDF报告下载

淘系技术:2021技术人的百宝黑皮书(867页).pdf

编号:112161 PDF 867页 149.40MB 下载积分:VIP专享
下载报告请您先登录!

淘系技术:2021技术人的百宝黑皮书(867页).pdf

1、阿里淘系2021技术年货TAO TECHNOLOGY 2021淘系最新实战经验&技术解决方案年度必看的技术&行业报告2021新趋势技术人必备工具书单、实用项目、最新paper等技术人的百宝黑皮书 本书包含2021前言站在2021年时间点的我们,多模态、3D XR、音视频、认知计算与知识图谱等技术已经无声无息地浸润到了我们的生活。越是身处一线的技术工作者,大概越能深切感知到:技术真正在产业中发挥作用,不是去解决一个惊天动地的大问题,而是去打磨10000个琐碎的小问题。同时在足够的时间和空间里,去不断迭代人类自身与技术、与世界的关系。未来的世界里,技术会像暖流一样浸润到生活的每一处,变得可触摸、可

2、享受、可亲近。站在天猫双11的第13个年头的大淘宝技术,我们不仅希望为消费者带来购物的快乐,更推动着电商技术不断突破为More fun、More valuable、More future的硬核科技。同时,我们也关注技术新动向和行业新趋势,不断登顶在创新前沿,去探索数字时代最理想的生活方式。在这个不断攀索的过程中,我们和你相遇了。这里是阿里巴巴淘系技术2021一整年的精华技术内容合集,涵盖了大前端、音视频、推荐系统、客户端架构、服务端架构、技术质量、以及3D&AI&AR类等多个技术领域,沉淀了双11大促、淘宝直播、闲APP等多个业务的技术解决方案,并有2021年度必看的技术&行业报告、开源项目、

3、顶会 paper 等多度知识与价值的输出。更有趣的,我们今年新增了很多接地气的程序员学习成经验和人生话题,在似曾相识的人生经验里,能带给你相视一笑的启发和触动。本书内容数 860+,全部内容将近 40w 字。希望你喜欢,并分享给身边的朋友。新年快乐!当一项技术逐渐“看不见”的时候,就是这项技术真正走向成熟的时候。扫一扫,关注公众号【淘系技术】不错过下一年的技术景027CONTENTS目录第一部分年度精选技术栈内容后端篇技术经典总结谈谈Java接口Result设计DDD系列第五讲:聊聊如何避免写流水账代码浅谈系统实现层面稳定性保障Guava Cache

4、 原理分析与最佳实践一种低延迟的超时中心实现方式Java类加载器 classloader 的原理及应用如何在业务逻辑当中优雅引入重试机制后端人精彩问答Q:怎么提高自己的系统设计和架构理论水平?Q:公司项目并发量都特小,自己如何实际接触高并发项目?Q:为什么Go的web框架速度还不如Java?238249259Q:无代码编程会是以后的趋势吗?Q:2021年了,前端最应该学什么?Q:如何看待 svelte 这个前端框架?266算法篇技术经典总结SIGIR2021|超越I2I和向量内积,淘宝新一代召回范式:PDN模型SIGKDD202

5、1|淘宝搜索向量化召回实践情感计算在ugc应用进展EdgeRec:电商信息流的端上推荐系统端智能研发核心套件,MNN 工作台深度解析算法人精彩问答Q:计算广告与推荐系统有哪些区别?Q:目前工业界常用的推荐系统模型有哪些?Q:2021年,作为算法工程师的你们会在CV业务落地上用Transformer吗?前端篇技术经典总结前端智能化看“低代码/无代码”下一代 Web 建站技术栈 Jamstack前端页面双向滚动方案图片上传系统在淘系中的实践前端人精彩问答27429533473533606客户端篇技术经典总结iOS 性能检测新方式AnimationHitc

6、hes淘宝客户端诊断体系升级实践阿里的统一跨端 API 方案 Uni APIFlutter企业级应用开发在闲的落地一个 Crash 引发的血案测试篇技术经典总结软件测试需要学什么?基于强化学习的质量AI在淘系互动业务的实践之路“真正的机器人测试”-淘宝泛终端机器人自动化测试实践淘系产业级移动应用智能测试最新方案MonkeyBot音视频与图像技术篇技术经典总结TaoAudio:淘宝直播短视频音频解决方案淘宝直播低延迟架构演进和实践淘系的音视频编辑方案:非线性编辑引擎视频内容理解在淘宝逛逛中的应用与落地4245462473480淘系2021技术资讯重点2021双11淘系技术九

7、大技术亮点发布淘系发布业界首个基于神经渲染的3D建模产品Object Drawer淘系技术内容互动算法团队入选ACM MM的4篇论文介绍淘系多媒体算法CVPR2021竞赛3项国际冠军方案介绍技术人的通用干货tips程序命名的原则与重构如何用“心流”提升编码工作效率?如何做好一份程序员的工作汇报ppt?2021阿里淘系工程师推荐书单524529533539548565567577580585591495505518第第二部分职场成部分职场成长技巧与解惑技巧与解惑职场成长技巧&故事从文科生到前端专家-在转行时我想过的问题淘系工程师亲述经历:如何避免成为“技术工具人”?仅9天拿下CVPR竞赛冠军,家

8、里的狗狗都改了作息淘系工程师圆桌讨论小项目需要前后端分离吗?程序员是要专精,还是要广度?程序员如何在业余时间提升自己?程序员如何快速上手一个新项目?“程序员从幼稚到成熟的标志是什么”?一线职场&学习问答实录Q:为什么阿里巴巴超级喜欢java开发?Q:阿里P7、P8、P9级别需要多少年的工作经验,还是因能力而定?Q:去阿里淘系做技术怎么样?值得2021应届毕业生去吗?Q:面试官如何判断面试者的机器学习水平?Q:简历中怎么写项目经历最好?为什么?Q:我想学习后端,后端的学习路线是怎么样的呢?5985996006096603604605606607第三部分技术人必备工具书籍前端

9、篇后端篇客户端篇服务端篇测试篇多媒体技术篇计算机视觉篇数据挖掘与数据分析篇推荐系统篇3D&AR篇第四部分GitHub上的实用练手项目推荐21个GitHub上好用又有趣的移动端项目(涵盖初中高阶)推荐26个GitHub上好玩又有挑战的前端项目(涵盖初中高阶)推荐10个GitHub上适合练手的后端项目(涵盖初中高阶)638650662674685694703第五部分2021淘系A类顶会论文精选数据挖掘与机器学习领域ICDE 2021Large-scale Fake Click Detection for E-commerce Recommendation Syste-msATNN:Adversar

10、ial Two-Tower Neural Network for New Items Popularity Prediction in EcommerceBillion-scale Pre-trained E-commerce Product Knowledge Graph ModelKDD 2021SEMI:A Sequential Multi-Modal Information Transfer Network for E-Comm-erce MicroVideo RecommendationsAliCoCo2:Commonsense Knowledge Extraction,Repres

11、entation and Appli-cation in EcommerceLive-Streaming Fraud Detection:A Heterogeneous Graph Neural Network Approach Embedding-based Product Retrieval in Taobao Search7753769779机器学习、计算机视觉及多媒体领域NIPS 2021Mining the Benefits of Two-stage and One-stage HOI DetectionACM MM 2021Understanding Chin

12、ese Video and Language via Contrastive Multimodal Pre-Training Pre-training Graph Transformer with Multimodal Side Information for Rec-ommendationShape Controllable Virtual Try-on for Underwear ModelsCVPR 2021 Residential floor plan recognition and reconstructionICCV 2021Conditional Diffusion for In

13、teractive Segmentation交叉/综合/新兴领域AAAI 2021Exploiting Diverse Characteristics and Adversarial Ambivalence for Domain Adaptive Segmentation8278378805Personalized Adaptive Meta Learning for Cold-start User Preference Pre-dictionCapturing Delayed Feedback in Conversion Rate Prediction via Elap

14、sed-Time Sampling KGDet:Keypoint-Guided Fashion Detection数据库、知识管理、信息检索领域CIKM 2021 Gated Heterogeneous Graph Representation Learning for Shop Search in E-CommerceVLDB 2021EdgeDIPN:a Unified Deep Intent Prediction Network Deployed at the EdgeSIGIR 2021A General Method For Automatic Discovery of Powerf

15、ul Interactions In Click-Through Rate PredictionPath-based Deep Network for Candidate Item Matching in Recommenders8538568582021热主题特别篇:你值得关注的【元宇宙】附录8602021热技术新动向报告2021热行业新趋势报告第六部分年度必看的技术&行业报告年度精选技术栈内容第一部分后端篇算法篇前端篇客户篇测试篇音视频与图像技术篇淘系2021技术资讯重点技术人的通用干货tips技术人的百宝黑皮书2021版淘系技术出品01第一部分年度精选技术栈内容1谈谈Java接口Resu

16、lt设计这篇文章酝酿了很久,一直想写,却一直觉得似乎要讲的东西有点杂,又不是很容易讲清楚,又怕争议的地方很多,就一拖再拖。但是,每次看到不少遇到跟这个设计相关导致的问题,又忍不住跟人讨论,但又很难一次说清楚,于是总后悔没有及早把自己的观点写成文章。不管怎样,观点还是要表达的,无论对错。作者:书牧 出品:淘系技术故障的推手“Result先说结论:接口方法,尤其是对外HSF(开源版本即dubbo)api,接口异常建议不要使用Result,而应该使用异常。阿里内部的java编码,已经习惯性对外API一股脑儿使用“Result”设计这是导致许多故障的重要原因!/用户查询的HSF服务API,使用了Res

17、ult做为返回结果public interface UserService Result getUserById(Long userId);/一段客户端应用facade的调用示例。读写缓存逻辑部分省略,仅做示意public User testGetUser(Long userId)String userKey=userId-+userId;/先查缓存,如果命中则返回缓存中的user /cacheManager.get(123,userKey);/.try Result result=userService.getUserById(userId);if(result.isSuccess()cac

18、heManager.put(123,userKey,result.getData();一个简化的例子后端篇技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结8910112上面的代码很简单,客户端应用对User查询服务做了个缓存。有些同学可能一眼就看出来,这里隐藏的bug:第10行的“result.isSuccess()”为false的实际含义是什么?是服务端系统异常吗?还是用户不存在?光看API是很难确定的。不得不去找服务提供方或文档确认其逻辑,根据错误码进行区分。如果是服务端系统异常,那么第15行将导致线上bug,因为后续1小时对该用户的

19、请求都认为用户不存在了。严谨点的写法如果要写正确逻辑,那么代码可能会变成这样:技术人的百宝黑皮书2021版技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结74567895161718 return result.getData();/否则缓存空对象,代表用户不存在 cacheManager.put(123,userKey,NullCacheObject.getInstance(),3600);return null;catch(Exception e)/TODO log throw new De

20、moException(getUserById error.userId=+userId,e);public User testGetUser(Long userId)String userKey=userId-+userId;/先查缓存,如果命中则返回缓存中的user /cacheManager.get(123,userKey);/.try Result result=userService.getUserById(userId);if(result.isSuccess()cacheManager.put(123,userKey,result.getData();return result.

21、getData();if(USER_NOT_FOUND.equals(result.getCode()/否则缓存空对象,代表用户不存在 cacheManager.put(123,userKey,NullCacheObject.getInstance(),3600);else /可能是SYSTEM_ERROR、DB_ERROR等一些系统性的异常,TODO log throw new DemoException(getUserById error.userId=+userId+,result=+result);3很显然,代码变得复杂起来了,加上对外部调用的try catch异常处理,实际代码变相当

22、复杂繁琐。public User testGetUser(Long userId)String userKey=userId-+userId;/先查缓存,如果命中则返回缓存中的user /cacheManager.get(123,userKey);/.try User user=userService.getUserById(userId);if(user!=null)cacheManager.put(123,userKey,user);return user;else /否则缓存空对象,代表用户不存在 cacheManager.put(123,userKey,NullCacheObject.

23、getInstance(),3600);return null;catch(Exception e)/TODO log throw new DemoException(getUserById error.userId=+userId,e);61718192021public interface UserService User getUserById(Long userId)throws DemoAppException;2324252627不使用Result的例子这样一看,代码简洁清晰很多,也更符合对普通API的调用习惯。ca

24、tch(DemoException e)throw e;catch(Exception e)/TODO log throw new DemoException(getUserById error.userId=+userId,e);return null;技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结4我们看阿里巴巴Java开发手册的“异常处理”小节第13条:【推荐】对于公司外的http/api开放接口必须使用“错误码”;跨应用间HSF调用优先考虑使用Result方式,封装isSuccess()方法、“错误码”、“错误简短信息”;而应用内部推荐异常抛出。这条

25、推荐非常具有误导性,在2016年孤尽对于这条规范进行调研时的帖子:【开发规约热议投票02】HSF服务接口定义时,是Result+isSuccess方式返回,还是是抛异常的方式?有部分同学不建议使用Result,但大部分同学推荐了Result的做法。调用成本高:虽然通过对依赖的API深入了解异常设计,可以写出严谨的代码以避免出现bug,但是简单的逻辑,代码却变得复杂。换言之,调用的成本变高。但是很可惜,我们忘记判断而写成“一个简化的例子”这样是往往常事。无意义错误码:SYSTEM_ERROR、DB_ERROR等系统异常的错误码,虽然放在Result中了,但是调用方除了日志和监控作用外,业务逻辑永

26、远不会关心,也永远处理不了。而些错误码的处理分支,实际与抛异常的处理逻辑一样。既然如此,为何要将这些错误码放在返回值里?使用Result的几个问题为什么说这条规约具有误导性?1.2.关于阿里巴巴开发规约因为这个问题本身没有讲清楚“对什么东西的处理”要用Result还是异常的方式,即这里没有讲清楚我们要解决的问题是什么。事实上我们常说的“失败”,往往混淆了2种含义:1.2.系统异常:比如网络超时、DB异常、缓存超时等,调用方一般不太可能基于这些错误类型做不同的业务逻辑,常用用于日志和监控,方便定位排查。业务状态:比如业务规则拦截导致的失败,比如发权益时库存不足、用户限领等,为方便后文叙述和理解,

27、暂时称为“业务失败”。这类“失败”,从机器层面来看,严格来说不能算做是失败,这只是一种正常的业务结果,这和“调用成功”这个业务结果对系统来说没有任何区别,只是一个业务状态而已。调用方往往可能关心对应的错误码,以完成不同的业务逻辑。有经验的开发,都会意识到这2种含义的区别,这对于帮助我们理解接口的异常设计非常重要!对这条开发规约而言,如果是第2种,并没有什么大的问题,但如果是第1种,我则持相反的意见,因为这违背了java语言的基本设计,不符合java编码直觉,会潜移默化造成前面案例所示的理解和使用成本的问题。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结51.

28、1.2.2.3.当我们讨论要用Result代替Exception时,经常会以这是HSF接口为由,因为性能开销等等。我们常说HSF这种RPC框架,设计的目的就是为了看起来像本地调用。那么,这个“看起来像本地调用”到底指的是哪方面像呢?显然,编码时像,运行时不像。所以我们写调用HSF接口的代码时,感觉像在调用本地方法,那么我们的编码直觉和习惯也都应该是符合java的规范的。因此,至少有几点理由,对于系统异常,我们的HSF接口更应该使用Excep-tion,而非Result的方式:当然,由于“运行时不像”,对于HSF封装带来的抽象泄露,我们在使用异常时,需要关注几点问题:为什么针对HSF?只有同样遵

29、循本地方法调用的设计,来设计HSF的api,才能更好做到“像本地调用一样”,更符合HSF设计的初衷。HSF接口是往往用于对外部团队提供服务,更应该遵循java语法的设计,提供清晰的接口语义,降低调用方的使用成本,减少出bug的概率。Result 并无统一规范,而Exception则是语言标准,有利于中间件、框架代码的监控发现和异常重试等逻辑生效。异常要在接口显式声明,否则客户端可能会反序列化失败。尽可能不带原始堆栈,否则客户端也可能反序列化失败,或者堆栈过大导致性能问题。可以考虑异常中定义错误码以方便定位问题。讲到这里,我们发现,java的CheckedException的设计,作用上和反映业

30、务失败的Result很像。Result是强制调用方进行判断和识别,并根据不同的错误码进行判断和处理。而Checked Exception也是强制调用方进行处理,并且可能要对不同的异常做不同的处理。但是,基于前面的结论,业务失败应该通过返回值来表达,而不是异常;而异常是不应该用于做业务逻辑判断的,那么java的Checked Exception就变成奇怪的存在了。这里我明确我的观点,我们应该尽可能不使用Checked Exception。另外,Thinking in Java的作者 Bruce Eckel就曾经公开表示,Java语言中的Checked Exception是一个错误的决定,Java

31、应该移除它。C#之父Anders Hejlsberg也认同这个观点,因此C#中是没有Checked Exception的。结论无论是HSF接口,还是内部的API,都应该遵循java语言的编码直觉和习惯,业务结果(无论成功还是失败)都应该通过返回值返回,而系统异常,则应该使用抛出Exception的方式来实现。关于Checked Exception技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后短篇/技术经典总结6public class UserRegisterResult private String errorCode;private String errorMsg;pri

32、vate Long userId;/.public boolean isSuccess()return errorCode=null;/.11到底为什么会有“Result”这样的东西诞生呢?如果设计的方法返回值是Result类型,那么它必须能准确反应这个方法调用的结果。实际上,以上面的例子为例,这个时候的Result就是User类本身,User.status相当于Result.code。这听起来可能有点和直觉不符,这是为什么?Result的由来1.2.3.4.我们看看一个java方法的签名(省略修饰符部分):所以,返回值和方法功能必须是配套的,返回值类型,就是这个方法的功

33、能执行结果的准确表达,即返回值必须正好就是当前这个方法要做的事情的结果,必须满足这个方法语义,而不应该有超出这个语义外的东西存在。而异常,所说的“意外”,则是指超出这个方法语义之外的部分。这几句话有点拗口,举个例子来说,上面这个用户接口,语义就是要通过用户id查询用户,那么当服务端发生DB超时错误时,对于“通过用户id查询用户”这个语义来说,“DB超时错误”没有任何意义,使用异常是恰好合适的,如果我们把这个错误做为错误码放在返回值的Result里,那么就是增加了这个方法的使用成本。Reselt 的实质是什么?方法名:用于表达这个方法的功能参数:方法的输入返回值类型:方法的输出异常:方法中意外出

34、现的错误技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结7UserRegisterResult registerUser(User user)throws DemoAppException;1我们再来看看上面这个“注册用户”的方法声明,会发现,这个方法定义一个Result显得很合适。这是因为前一个例子,我们的方法是一个查询方法,返回值刚好可以用领域对象类型本身,而这个“注册用户”的方法,显然没有现成合适的类型可以使用,所以就需要定义一个新的类型来表达方法的执行结果。看到这里,我们会以为,对于“写”与“读”类型的方法有所差异,但实际上,对于java语言或者机器来

35、说,并无二致,第二个方法UserRegis-terResult的和第一个方法的User是同等地位。所以,最重要的还是一点:需要有一个合适的类型,做为返回值,用于准确表达方法执行的功能结果。而偏“写”类型,或者带业务校验的读接口,往往因为没有现成的类型可用,为了方便,常常会使用Result来代替。讲到这里,想想,当我们这种“需要Result”的方法有多个时,我们会说“我需要一个统一的Result类”时,实际上说的什么呢?1.我希望各种接口方法都统一同样的Result,方便使用 2.我希望有个类复用errorCode、errorMsg以及相关的getter/setter等代码显然,第1点理由经不起

36、推敲,为何“统一就方便使用”了?如果各种方法返回类型都一样,那就违背了“返回值要和方法功能配套”的结论,也不符合高内聚的设计原则。恰相反,返回值越是设计得专用,对调用方来说理解和使用成本越低。所以,我们实际想要的,仅仅是如何“偷懒”,也就是第2点理由。所以我们真正要做的是,只是在当前领域范围内,如何既满足让每个方法返回值专用以便使用,同时又可以偷懒复用部分代码即可。因此,绝不必要求大家都统一使用同一个Result类型。根据前文的结论,我们知道,对于接口方法的返回值和异常处理,最重要的是需要遵循方法的语义进行设计。以下是我梳理的一些设计上的原则和建议。是否有必要统一Result?接口响应按有业务

37、结果和未知业务结果分类,业务结果不管是业务成功还是业务规则导致的失败,都通过返回值返回;未知结果一般是系统性的异常导致,不要通过返回值错误码表达,而是通过抛出异常来表达。这里最关键一点,就是如何理解和区分某个“失败”是属于业务失败,还是属于系统异常。由于有时候这个区分并不是很容易,我们可以有一个比较简单的判断标准来确定:对响应合理分类接口返回设计建议技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结8User getUserById(Long userId)throws DemoAppException;1/UserRegisterResult、UserUpda

38、teResult可以继承Result类,减少工作量,但调用方不需要感知Result类的存在UserRegisterResult registerUser(User user)throws DemoAppException;UserUpdateResult updateUser(User user)throws DemoAppException;1234 1.如果一个错误,调用方只能通过人工介入的方式才能恢复,比如修改代码、改配置,或数据订正等处理,则必然属于异常 2.如果调用方无法使用代码逻辑处理消化使得自动恢复,而是只能通过重试的方式,依赖下游的恢复才能恢复,则属于异常普通查询接口,如无必要

39、,不要使用Result包装返回值。可以简单分为3类做为参考:普通读接口查询结果即是领域对象,无其他业务规则导致的失败:建议直接用领域对象类型做为返回值。如:写接口或者带业务规则的读接口:1.理想情况是专门封装一个返回值类,以降低调用方的使用成本。2.可考虑将返回值类继承Result,以复用errorCode和errorMsg等代码,减轻开发工作量。但注意这不是必要的。3.将本方法的错误码,直接定义到这个返回值类上(高内聚原则)。4.若有多个方法有共同的错误码,可以考虑通过将这部分错误码定义到一个Interface中,然后实现该接口。找到合适的场景技术人的百宝黑皮书2021版淘系技术出品01年度

40、精选技术栈内容后端篇/技术经典总结9/查询有效用户,如果用户存在但状态非有效状态则返回“用户状态错误”的错误码,如果不存在则返回nullResult getEffectiveUserWithStatusCheck(Long userId)throws DemoAppException;12Result registerUser(User user)throws DemoAppException;1对外接口,尤其是HSF,由于变更成本高,更要遵循前面的原则;内部方法,方法众多,如果完全遵循需要编码成本,这里需要做权衡,根据代码规模和发展阶段不断重构和调整即可。内外部区分我们对外的接口,返回值要避

41、免出现直接使用Result包装一个原生类型。比如:这样设计导致的结果是,扩展性很差。如果registerUser方法需要增加返回除了userId以外的其他字段时,就面临几个选择:1.让Result支持扩展参数,通过map来传递额外字段:可读性和使用成本很高 2.开发一个新的registerUser方法:显然,成本很高避免直接包装原生类型 带业务规则的的领域对象读接口完全遵循上面第2点,会给方法提供者带来一定的开发成本,权衡情况下可以考虑,套Result包装领域对象做为返回值。注意,对外不建议,可考虑用于内部方法。如下接口,“没有权限”是一个正常的业务失败,调用方可能会判断并做一定的业务逻辑处理

42、:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结10有人建议,做一个全局的错误码定义,以做统一,方便排查和定位。但这样做真的方便吗?这样做实际上有几个问题:1.完全违背了高内聚、低耦合的设计原则。这个“统一的定义”将与各个域都有耦合,同时对于某单个接口而言,则不够内聚。2.这个统一定义的错误码,一定会爆炸式增长,即便我们对其进行分类(非常依赖人的经验),迟早也会变得难以维护和理解。3.不要将系统异常类的错误码和业务失败错误码放在一起,这点其实和方法响应分类设计是一回事。我们在设计拉菲2权益平台的错误码时,就犯了这样的错误。现在这个“统一的”错误码已经超过40

43、0个,揉合了管理域、投发放域、离线域等各种不同域的业务失败、系统异常的错误码,不要说调用方,即便我们自己,也梳理不清楚了。而实际上,每个域、每个方法自己的业务失败是非常有限的,它的增长一定是随着业务需求本身的变化而增长的。现在如果有个业务方来问我,拉菲2的发放接口,有哪些错误码(这问的实际是业务失败,他也只关心业务失败),我几乎难以回答。很可惜,这块目前即便重构,难度也很大。避免所有错误码定义在一个类中 异常错误码前面我们讲到,即便是抛异常的形式,我们也可以为我们的异常类设计错误码,异常错误码的增加会很快,往往也和当前业务语义无关,因此千万不要和业务失败的错误码定义在一起。异常内的错误码主要用

44、于日志、监控等,核心原则就是,要方便定位问题。避免层层try catch到处充满异常处理的代码,会导致整个程序可读性变差,写起来也非常繁琐,可以遵循一定的原则:1.在原始发生错误的地方try catch,比如调用HSF接口的Facade层代码,主要目的是为了记录原始的错误以及出入参,方便定位问题,一般会打日志,并转换成本应用的异常类上抛 2.在应用的最顶层catch异常,打印统一日志,并根据“为什么针对HSF?”小节中的建议,处理成合适的异常后再抛出。对于HSF接口,可以直接实现HSF的“ServerFilter”来统一在框架层面处理。3.中间层的代码,不必再层层catch,比如domain层

45、,可以让代码逻辑更加清晰。异常处理机制技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结11 异常错误码抛异常的场景,除了前面说的系统性异常外,参数错误也推荐使用异常。原因如下:1.参数正确一般是我们当前上下文执行的前提条件,我们一般可以使用assert来保证其正确。即我们的后续逻辑是认为,当前的参数是不可能错误的,我们没必要为此写过多繁琐的防御性代码。2.一旦发生参数错误,则一定是调用方有代码bug,或者配置bug,应该通过抛出异常的方式,充分提前在开发或测试阶段暴露。3.参数错误对调用方来说,是无法处理的,程序不可能自动恢复,一定是会需要人工介入才可能恢复,

46、调用方不可能会“判断如果是xx参数错误,我就做某个业务逻辑”这样的代码,因此通过返回值定义参数错误码没有意义。系统异常和业务结果转换系统性异常并非一定是异常,因为有些层可能有能力处理某些异常,比如对于弱依赖的接口,异常是可以吞掉,转换成一个业务结果;相反,有些接口返回的一些业务失败,但调用方认为该业务失败不可能出现,出现也无法处理,那么这一层可以将其转换成异常。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结12DDD系列第五讲:聊聊如何避免写流水账代码过去一年里我们团队做了大量的老系统重构和迁移,其中有大量的代码属于流水账代码,通常能看到是开发在对外的API

47、接口里直接写业务逻辑代码,或者在一个服务里大量的堆接口,导致业务逻辑实际无法收敛,接口复用性比较差。所以这讲主要想系统性的解释一下如何通过DDD的重构,将原有的流水账代码改造为逻辑清晰、职责分明的模块。这里举一个简单的常见案例:下单链路。假设我们在做一个checkout接口,需要做各种校验、查询商品信息、调用库存服务扣库存、然后生成订单:一个比较典型的代码如下:作者:殷浩出品:淘系技术案例简介RestControllerRequestMapping(/)public class CheckoutController Resource private ItemService itemServic

48、e;Resource private InventoryService inventoryService;Resource private OrderRepository orderRepository;111213技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结13 PostMapping(checkout)public Result checkout(Long itemId,Integer quantity)/1)Session管理 Long userId=SessionUtils.getLoggedInUserId();if(use

49、rId=0)return Result.fail(Not Logged In);/2)参数校验 if(itemId=0|quantity=1000)return Result.fail(Invalid Args);/3)外部数据补全 ItemDO item=itemService.getItem(itemId);if(item=null)return Result.fail(Item Not Found);/4)调用外部服务 boolean withholdSuccess=inventoryService.withhold(itemId,quantity);if(!withholdSucces

50、s)return Result.fail(Inventory not enough);/5)领域计算 Long cost=item.getPriceInCents()*quantity;/6)领域对象操作 OrderDO order=new OrderDO();order.setItemId(itemId);order.setBuyerId(userId);order.setSellerId(item.getSellerId();order.setCount(quantity);order.setTotalCost(cost);/7)数据持久化 orderRepository.createOr

51、der(order);/8)返回 return Result.success(order);9202272829303373839404474849505技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结14为什么这种典型的流水账代码在实际应用中会有问题呢?其本质问题是违背了SRP(Single Responsbility Principle)单一职责原则。这段代码里混杂了业务计算、校验逻辑、基础设施、和通信协议等,在未来无论哪一部分的逻辑变更都会直接影

52、响到这段代码,长期当后人不断的在上面叠加新的逻辑时,会造成代码复杂度增加、逻辑分支越来越多,最终造成bug或者没人敢重构的历史包袱。所以我们才需要用DDD的分层思想去重构一下以上的代码,通过不同的代码分层和规范,拆分出逻辑清晰,职责明确的分层和模块,也便于一些通用能力的沉淀。主要的几个步骤分为:分离出独立的Interface接口层,负责处理网络协议相关的逻辑从真实业务场景中,找出具体用例(Use Cases),然后将具体用例通过专用的Command指令、Query查询、和Event事件对象来承接分离出独立的Application应用层,负责业务流程的编排,响应Command、Query和Eve

53、nt。每个应用层的方法应该代表整个业务流程中的一个节点处理一些跨层的横切关注点,如鉴权、异常处理、校验、缓存、日志等下面会针对每个点做详细的解释。随着REST和MVC架构的普及,经常能看到开发同学直接在Controller中写业务逻辑,如上面的典型案例,但实际上MVC Controller不是唯一的重灾区。以下的几种常见的代码写法通常都可能包含了同样的问题:HTTP 框架:如Spring MVC框架,Spring Cloud等 RPC 框架:如Dubbo、HSF、gRPC等 消息队列MQ的“消费者”:比如JMS的 onMessage,RocketMQ的MessageListener等 Sock

54、et通信:Socket通信的receive、WebSocket的onMessage等 文件系统:WatcherService等 分布式任务调度:SchedulerX等这些的方法都有一个共同的点就是都有自己的网络协议,而如果我们的业务代码和网络协议混杂在一起,则会直接导致代码跟网络协议绑定,无法被复用。所以,在DDD的分层架构中,我们单独会抽取出来Interface接口层,作为所有对外的门户,将网络协议和业务逻辑解耦。Interface接口层技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结15接口层主要由以下几个功能组成:1.网络协议的转化:通常这个已经由各种框

55、架给封装掉了,我们需要构建的类要么是被注解的bean,要么是继承了某个接口的bean。2.统一鉴权:比如在一些需要AppKey+Secret的场景,需要针对某个租户做鉴权的,包括一些加密串的校验 3.Session管理:一般在面向用户的接口或者有登陆态的,通过Session或者RPC上下文可以拿到当前调用的用户,以便传递给下游服务。4.限流配置:对接口做限流避免大流量打到下游服务 5.前置缓存:针对变更不是很频繁的只读场景,可以前置结果缓存到接口层 6.异常处理:通常在接口层要避免将异常直接暴露给调用端,所以需要在接口层做统一的异常捕获,转化为调用端可以理解的数据格式 7.日志:在接口层打调用

56、日志,用来做统计和debug等。一般微服务框架可能都直接包含了这些功能。当然,如果有一个独立的网关设施/应用,则可以抽离出鉴权、Session、限流、日志等逻辑,但是目前来看API网关也只能解决一部分的功能,即使在有API网关的场景下,应用里独立的接口层还是有必要的。在interface层,鉴权、Session、限流、缓存、日志等都比较直接,只有一个异常处理的点需要重点说下。接口层的组成返回值和异常处理规范,Result vs Exception注:这部分主要还是面向REST和RPC接口,其他的协议需要根据协议的规范产生返回值。在我见过的一些代码里,接口的返回值比较多样化,有些直接返回DTO甚

57、至DO,另一些返回Result。接口层的核心价值是对外,所以如果只是返回DTO或DO会不可避免的面临异常和错误栈泄漏到使用方的情况,包括错误栈被序列化反序列化的消耗。所以,这里提出一个规范:规范:Interface层的HTTP和RPC接口,返回值为Result,捕捉所有异常 规范:Application层的所有接口返回值为DTO,不负责处理异常Application层的具体规范等下再讲,在这里先展示Interface层的逻辑。举个例子:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结16PostMapping(checkout)public Result ch

58、eckout(Long itemId,Integer quantity)try CheckoutCommand cmd=new CheckoutCommand();OrderDTO orderDTO=checkoutService.checkout(cmd);return Result.success(orderDTO);catch(ConstraintViolationException cve)/捕捉一些特殊异常,比如Validation异常 return Result.fail(cve.getMessage();catch(Exception e)/兜底异常捕获 return Resul

59、t.fail(e.getMessage();11121314Target(ElementType.METHOD)Retention(RetentionPolicy.RUNTIME)public interface ResultHandler AspectComponentpublic class ResultAspect Around(annotation(ResultHandler)public Object logExecutionTime(ProceedingJoinPoint joinPoint)throws Throwable Object proceed=nu

60、ll;try proceed=joinPoint.proceed();catch(ConstraintViolationException cve)return Result.fail(cve.getMessage();catch(Exception e)return Result.fail(e.getMessage();return proceed;62当然,每个接口都要写异常处理逻辑会比较烦,所以可以用AOP做个注解然后最终代码则简化为:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结17/

61、可以是RPC Provider 或者 Controllerpublic interface CardService /1)统一接口,参数膨胀 Result openCard(int petType,int babyAge);/2)统一泛化接口,参数语意丢失 Result openCardV2(Map params);/3)不泛化,同一个类里的接口膨胀 Result openPetCard(int petType);Result openBabyCard(int babyAge);111213PostMapping(checkout)ResultHandlerpublic

62、Result checkout(Long itemId,Integer quantity)CheckoutCommand cmd=new CheckoutCommand();OrderDTO orderDTO=checkoutService.checkout(cmd);return Result.success(orderDTO);1234567在传统REST和RPC的接口规范中,通常一个领域的接口,无论是REST的Resource资源的GET/POST/DELETE,还是RPC的方法,是追求相对固定的,统一的,而且会追求统一个领域的方法放在一个领域的服务或Controller中。但是我发现在

63、实际做业务的过程中,特别是当支撑的上游业务比较多时,刻意去追求接口的统一通常会导致方法中的参数膨胀,或者导致方法的膨胀。举个例子:假设有一个宠物卡和一个亲子卡的业务公用一个开卡服务,但是宠物需要传入宠物类型,亲子的需要传入宝宝年龄。可以看出来,无论是怎么操作,都有可能导致CardService这个服务未来越来越难以维护,方法越来越多,一个业务的变更有可能会导致整个服务/Controller的变更,最终变得无法维护。我曾经参与过的一个服务,提供了几十个方法,上万行代码,可想而知无论是使用方对接口的理解成本还是对代码的维护成本都是极高的。所以,这里提出另一个规范:接口层的接口的数量和业务间的隔离技

64、术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结18public interface PetCardService Result openPetCard(int petType);public interface BabyCardService Result openBabyCard(int babyAge);1234567 规范:Interface层的HTTP和RPC接口,返回值为Result,捕捉所有异常 规范:Application层的所有接口返回值为DTO,不负责处理异常基于上面的这个规范,可以发现宠物卡和亲子卡虽然看起来像是类似的需求,但并非是“同样需

65、求”的,可以预见到在未来的某个时刻,这两个业务的需求和需要提供的接口会越走越远,所以需要将这两个接口类拆分开:这个的好处是符合了Single Responsibility Principle单一职责原则,也就是说一个接口类仅仅会因为一个(或一类)业务的变化而变化。一个建议是当一个现有的接口类过度膨胀时,可以考虑对接口类做拆分,拆分原则和SRP一致。也许会有人问,如果按照这种做法,会不会产生大量的接口类,导致代码逻辑重复?答案是不会,因为在DDD分层架构里,接口类的核心作用仅仅是协议层,每类业务的协议可以是不同的,而真实的业务逻辑会沉淀到应用层。也就是说Interface和Application

66、的关系是多对多的:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结19Application层Application层的组成部分Command、Query、Event对象Application层的几个核心类:ApplicationService应用服务:最核心的类,负责业务流程的编排,但本身不负责任何业务逻辑 DTO Assembler:负责将内部领域模型转化为可对外的DTO Command、Query、Event对象:作为ApplicationService的入参 返回的DTO:作为ApplicationService的出参Application层最核心的对

67、象是ApplicationService,它的核心功能是承接“业务流程“。但是在讲Applica-tionService的规范之前,必须要先重点的讲几个特殊类型的对象,即Command、Query和Event。因为业务需求是快速变化的,所以接口层也要跟着快速变化,通过独立的接口层可以避免业务间相互影响,但我们希望相对稳定的是Application层的逻辑。所以我们接下来看一下Application层的一些规范。从本质上来看,这几种对象都是Value Object,但是从语义上来看有比较大的差异:Command指令:指调用方明确想让系统操作的指令,其预期是对一个系统有影响,也就是写操作。通常来讲

68、指令需要有一个明确的返回值(如同步的操作结果,或异步的指令已经被接受)。Query查询:指调用方明确想查询的东西,包括查询参数、过滤、分页等条件,其预期是对一个系统的数据完全不影响的,也就是只读操作。Event事件:指一件已经发生过的既有事实,需要系统根据这个事实作出改变或者响应的,通常事件处理都会有一定的写操作。事件处理器不会有返回值。这里需要注意一下的是,Application层的Event概念和Domain层的DomainEvent是类似的概念,但不一定是同一回事,这里的Event更多是外部一种通知机制而已。简单总结下:语意读/写返回值Command”希望“能触发的操作写DTO 或 Bo

69、oleanQuery各种条件的查询只读DTO 或 CollectionEvent已经发生过的事情通常是写Void技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结20Result checkout(Long itemId,Integer quantity);1Result checkout(Long itemId,Integer quantity);Result checkout(Long itemId,Integer quantity,Integer channel);12 为什么要用CQE对象?通常在很多代码里,能看到接口上有多个参数,比如上文中的案例:如果

70、需要在接口上增加参数,考虑到向前兼容,则需要增加一个方法:List queryByItemId(Long itemId);List queryBySellerId(Long sellerId);List queryBySellerIdWithPage(Long sellerId,int currentPage,int pageSize);123或者常见的查询方法,由于条件的不同导致多个方法:可以看出来,传统的接口写法有几个问题:1.接口膨胀:一个查询条件一个方法 2.难以扩展:每新增一个参数都有可能需要调用方升级 3.难以测试:接口一多,职责随之变得繁杂,业务场景各异,测试用例难以维护但是另外

71、一个最重要的问题是:这种类型的参数罗列,本身没有任何业务上的”语意“,只是一堆参数而已,无法明确的表达出来意图。CQE的规范所以在Application层的接口里,强力建议的一个规范是:规范:ApplicationService的接口入参只能是一个Command、Query或Event对象,CQE对象需要能代表当前方法的语意。唯一可以的例外是根据单一ID查询的情况,可以省略掉一个Query对象的创建按照上面的规范,实现案例是:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结21public interface CheckoutService OrderDTO

72、checkout(Valid CheckoutCommand cmd);List query(OrderQuery query);OrderDTO getOrder(Long orderId);/注意单一ID查询可以不用QueryDatapublic class CheckoutCommand private Long userId;private Long itemId;private Integer quantity;Datapublic class OrderQuery private Long sellerId;private Long itemId;private int curre

73、ntPage;private int pageSize;617181920这个规范的好处是:提升了接口的稳定性、降低低级的重复,并且让接口入参更加语意化。CQE vs DTO从上面的代码能看出来,ApplicationService的入参是CQE对象,但是出参却是一个DTO,从代码格式上来看都是简单的POJO对象,那么他们之间有什么区别呢?CQE:CQE对象是ApplicationService的输入,是有明确的”意图“的,所以这个对象必须保证其”正确性“。DTO:DTO对象只是数据容器,只是为了和外部交互,所以本身不包含任何逻辑,只是贫血对象。但可

74、能最重要的一点:因为CQE是”意图“,所以CQE对象在理论上可以有”无限“个,每个代表不同的意图;但是DTO作为模型数据容器,和模型一一对应,所以是有限的。CQE的校验CQE作为ApplicationService的输入,必须保证其正确性,那么这个校验是放在哪里呢?技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结22Validated/Spring的注解public class CheckoutServiceImpl implements CheckoutService OrderDTO checkout(Valid CheckoutCommand cmd)/

75、这里Valid是JSR-303/380的注解 /如果校验失败会抛异常,在interface层被捕捉 Datapublic class CheckoutCommand NotNull(message=用户未登陆)private Long userId;NotNull Positive(message=需要是合法的itemId)private Long itemId;NotNull Min(value=1,message=最少1件)Max(value=1000,message=最多不能超过1000件)private Integer quantity;61

76、71819202122if(itemId=0|quantity=1000)return Result.fail(Invalid Args);123在最早的代码里,曾经有这样的校验逻辑,当时写在了服务里:这种做法的好处是,让ApplicationService更加清爽,同时各种错误信息可以通过Bean Validation的API做各种个性化定制。这种代码在日常非常常见,但其最大的问题就是大量的非业务代码混杂在业务代码中,很明显的违背了单一职责原则。但因为当时入参仅仅是简单的int,所以这个逻辑只能出现在服务里。现在当入参改为了CQE之后,我们可以利用java标准JSR303或JSR380的Be

77、an Validation来前置这个校验逻辑。规范:CQE对象的校验应该前置,避免在ApplicationService里做参数的校验。可以通过JSR303/380和Spring Validation来实现前面的例子可以改造为:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结23public interface CheckoutService OrderDTO checkout(Valid CheckoutCommand cmd);OrderDTO updateOrder(Valid UpdateOrderCommand cmd);Datapublic cla

78、ss UpdateOrderCommand NotNull(message=用户未登陆)private Long userId;NotNull(message=必须要有OrderID)private Long orderId;NotNull Positive(message=需要是合法的itemId)private Long itemId;NotNull Min(value=1,message=最少1件)Max(value=1000,message=最多不能超过1000件)private Integer quantity;622

79、324 避免复用CQE因为CQE是有“意图”和“语意”的,我们需要尽量避免CQE对象的复用,哪怕所有的参数都一样,只要他们的语意不同,尽量还是要用不同的对象。规范:针对于不同语意的指令,要避免CQE对象的复用 反例:一个常见的场景是“Create创建”和“Update更新”,一般来说这两种类型的对象唯一的区别是一个ID,创建没有ID,而更新则有。所以经常能看见有的同学用同一个对象来作为两个方法的入参,唯一区别是ID是否赋值。这个是错误的用法,因为这两个操作的语意完全不一样,他们的校验条件可能也完全不一样,所以不应该复用同一个对象。正确的做法是产出两个对象:技术人的百宝黑皮书2021版淘系技术出

80、品01年度精选技术栈内容后端篇/技术经典总结24ApplicationService负责了业务流程的编排,是将原有业务流水账代码剥离了校验逻辑、领域计算、持久化等逻辑之后剩余的流程,是“胶水层”代码。参考一个简易的交易流程:在这个案例里可以看出来,交易这个领域一共有5个用例:下单、支付成功、支付失败关单、物流信息更新、关闭订单。这5个用例可以用5个Command/Event对象代替,也就是对应了5个方法。我见过3种ApplicationService的组织形态:ApplicationServicepublic interface CheckoutService /下单 OrderDTO che

81、ckout(Valid CheckoutCommand cmd);/支付成功 OrderDTO payReceived(Valid PaymentReceivedEvent event);/支付取消 OrderDTO payCanceled(Valid PaymentCanceledEvent event);/发货 OrderDTO packageSent(Valid PackageSentEvent event);/收货一个ApplicationService类是一个完整的业务流程,其中每个方法负责处理一个Use Case。这种的好处是可以完整的收

82、敛整个业务逻辑,从接口类即可对业务逻辑有一定的掌握,适合相对简单的业务流程。坏处就是对于复杂的业务流程会导致一个类的方法过多,有可能代码量过大。这种类型的具体案例如:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结25Componentpublic class CheckoutCommandHandler implements CommandHandler Override public OrderDTO handle(CheckoutCommand cmd)/public class CheckoutServiceImpl implements Checko

83、utService Resource private CheckoutCommandHandler checkoutCommandHandler;Override public OrderDTO checkout(Valid CheckoutCommand cmd)return checkoutCommandHandler.handle(cmd);617 OrderDTO delivered(Valid DeliveredEvent event);/批量查询 List query(OrderQuery query);/单个查询 OrderDTO ge

84、tOrder(Long orderId);122232针对于比较复杂的业务流程,可以通过增加独立的CommandHandler、EventHandler来降低一个类中的代码量:3比较激进一点,通过CommandBus、EventBus,直接将指令或事件抛给对应的Handler,EventBus比较常见。具体案例代码如下,通过消息队列收到MQ消息后,生成Event,然后由EventBus做路由到对应的Handler:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结26/Application层/在这里框架通常可以根据接口识别到这个负责处理P

85、aymentReceivedEvent/也可以通过增加注解识别Componentpublic class PaymentReceivedHandler implements EventHandler Override public void process(PaymentReceivedEvent event)/Interface层,这个是RocketMQ的Listenerpublic class OrderMessageListener implements MessageListenerOrderly Resource private EventBus eventBus;Override

86、public ConsumeOrderlyStatus consumeMessage(List msgs,ConsumeOrderlyContext context)PaymentReceivedEvent event=new PaymentReceivedEvent();eventBus.dispatch(event);/不需要指定消费者 return ConsumeOrderlyStatus.SUCCESS;6223242526 不建议这种做法可以实现Interface层和某个具体的ApplicationService或Ha

87、ndler的完全静态解藕,在运行时动态dispatch,做的比较好的框架如AxonFramework。虽然看起来很便利,但是根据我们自己业务的实践和踩坑发现,当代码中的CQE对象越来越多,handler越来越复杂时,运行时的dispatch缺乏了静态代码间的关联关系,导致代码很难读懂,特别是当你需要trace一个复杂调用链路时,因为dispatch是运行时的,很难摸清楚具体调用到的对象。所以我们虽然曾经有过这种尝试,但现在已经不建议这么做了。Application Service 是业务流程的封装,不处理业务逻辑虽然之前曾经无数次重复ApplicationService只负责业务流程串联,不负

88、责业务逻辑,但如何判断一段代码到底是业务流程还是逻辑呢?举个之前的例子,最初的代码重构后:!技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结27ServiceValidatedpublic class CheckoutServiceImpl implements CheckoutService private final OrderDtoAssembler orderDtoAssembler=OrderDtoAssembler.INSTANCE;Resource private ItemService itemService;Resource private

89、InventoryService inventoryService;Resource private OrderRepository orderRepository;Override public OrderDTO checkout(Valid CheckoutCommand cmd)ItemDO item=itemService.getItem(cmd.getItemId();if(item=null)throw new IllegalArgumentException(Item not found);boolean withholdSuccess=inventoryService.with

90、hold(cmd.getItemId(),cmd.getQuantity();if(!withholdSuccess)throw new IllegalArgumentException(Inventory not enough);Order order=new Order();order.setBuyerId(cmd.getUserId();order.setSellerId(item.getSellerId();order.setItemId(item.getItemId();order.setItemTitle(item.getTitle();order.setItemUnitPrice

91、(item.getPriceInCents();order.setCount(cmd.getQuantity();Order savedOrder=orderRepository.save(order);return orderDtoAssembler.orderToDTO(savedOrder);622324252627282930337技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结28Datapublic class Order private Long itemUn

92、itPrice;private Integer count;/把原来一个在ApplicationService的计算迁移到Entity里 public Long getTotalCost()return itemUnitPrice*count;11boolean withholdSuccess=inventoryService.withhold(cmd.getItemId(),cmd.getQuantity();if(!withholdSuccess)throw new IllegalArgumentException(Inventory not enough);1234

93、/5)领域计算Long cost=item.getPriceInCents()*quantity;order.setTotalCost(cost);1231不要有if/else分支逻辑:也就是说代码的Cyclomatic Complexity(循环复杂度)应该尽量等于1通常有分支逻辑的,都代表一些业务判断,应该将逻辑封装到DomainService或者Entity里。但这不代表完全不能有if逻辑,比如,在这段代码里:虽然CC 1,但是仅仅代表了中断条件,具体的业务逻辑处理并没有受影响。可以把它看作为Precondition。2不要有任何计算:在最早的代码里有这个计算:通过将这个计算逻辑封装到实

94、体里,避免在ApplicationService里做计算 判断是否业务流程的几个点:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结29order.setItemUnitPrice(item.getPriceInCents();order.setCount(cmd.getQuantity();121314OrderDTO dto=orderDtoAssembler.orderToDTO(savedOrder);13一些数据的转化可以交给其他对象来做:比如DTO Assembler,将对象间转化的逻辑沉淀在单独的类中,降低ApplicationService的复

95、杂度我们可以看出来,ApplicationService的代码通常有类似的结构:AppService通常不做任何决策(Precondition除外),仅仅是把所有决策交给DomainService或Entity,把跟外部交互的交给Infrastructure接口,如Reposi-tory或防腐层。一般的“套路”如下:准备数据:包括从外部服务或持久化源取出相对应的Entity、VO以及外部服务返回的DTO。执行操作:包括新对象的创建、赋值,以及调用领域对象的方法对其进行操作。需要注意的是这个时候通常都是纯内存操作,非持久化。持久化:将操作结果持久化,或操作外部系统产生相应的影响,包括发消息等异步

96、操作。如果涉及到对多个外部系统(包括自身的DB)都有变更的情况,这个时候通常处在“分布式事务”的场景里,无论是用分布式TX、TCC、还是Saga模式,取决于具体场景的设计,在此处暂时略过。常用的ApplicationService“套路”DTO Assembler一个经常被忽视的问题是 ApplicationService应该返回 Entity 还是 DTO?这里提出一个规范,在DDD分层架构中:ApplicationService应该永远返回DTO而不是Entity为什么呢?技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结30import org.mapst

97、ruct.Mapper;Mapperpublic interface OrderDtoAssembler OrderDtoAssembler INSTANCE=Mappers.getMapper(OrderDtoAssembler.class);OrderDTO orderToDTO(Order order);public class CheckoutServiceImpl implements CheckoutService private final OrderDtoAssembler orderDtoAssembler=OrderDtoAssembler.INSTANCE;Overrid

98、e public OrderDTO checkout(Valid CheckoutCommand cmd)/.Order order=new Order();/.Order savedOrder=orderRepository.save(order);return orderDtoAssembler.orderToDTO(savedOrder);6171819 1.构建领域边界:ApplicationService的入参是CQE对象,出参是DTO,这些基本上都属于简单的POJO,来确保Application层的内外互相不影响。2.降低规则依赖:Ent

99、ity里面通常会包含业务规则,如果ApplicationService返回Entity,则会导致调用方直接依赖业务规则。如果内部规则变更可能直接影响到外部。3.通过DTO组合降低成本:Entity是有限的,DTO可以是多个Entity、VO的自由组合,一次性封装成复杂DTO,或者有选择的抽取部分参数封装成DTO可以降低对外的成本。因为我们操作的对象是Entity,但是输出的对象是DTO,这里就需要一个专属类型的对象叫DTO Assembler。DTO Assembler的唯一职责是将一个或多个Entity/VO,转化为DTO。注意:DTO Assembler通常不建议有反操作,也就是不会从DT

100、O到Entity,因为通常一个DTO转化为Entity时是无法保证Entity的准确性的。通常,Entity转DTO是有成本的,无论是代码量还是运行时的操作。手写转换代码容易出错,为了节省代码量用Reflection会造成极大的性能损耗。所以这里我还是不遗余力的推荐MapStruct这个库。MapStruct通过静态编译时代码生成,通过写接口和配置注解就可以生成对应的代码,且因为生成的代码是直接赋值,其性能损耗基本可以忽略不计。通过MapStruct,代码即可简化为:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结31ItemDO item=itemServi

101、ce.getItem(cmd.getItemId();boolean withholdSuccess=inventoryService.withhold(cmd.getItemId(),cmd.getQuantity();12结合之前的Data Mapper,DTO、Entity和DataObject之间的关系如下图:Result vs Exception最后,上文曾经提及在Interface层应该返回Result,在Application层应该返回DTO,在这里再次重复提出规范:Application层只返回DTO,可以直接抛异常,不用统一处理。所有调用到的服务也都可以直接抛异常,除非 需要

102、特殊处理,否则不需要刻意捕捉异常异常的好处是能明确的知道错误的来源,堆栈等,在Interface层统一捕捉异常是为了避免异常堆栈信息泄漏到API之外,但是在Application层,异常机制仍然是信息量最大,代码结构最清晰的方法,避免了Result的一些常见且繁杂的Result.isSuccess判断。所以在Application层、Domain层,以及Infrastructure层,遇到错误直接抛异常是最合理的方法。Anti-Corruption Layer防腐层本文仅仅简单描述一下ACL的原理和作用,具体的实施规范可能要等到另外一篇文章。在ApplicationService中,经常会依赖

103、外部服务,从代码层面对外部系统产生了依赖。比如上文中的:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结32会发现我们的ApplicationService会强依赖ItemService、InventoryService以及ItemDO这个对象。如果任何一个服务的方法变更,或者ItemDO字段变更,都会有可能影响到ApplicationService的代码。也就是说,我们自己的代码会因为强依赖了外部系统的变化而变更,这个在复杂系统中应该是尽量避免的。那么如何做到对外部系统的隔离呢?需要加入ACL防腐层。ACL防腐层的简单原理如下:对于依赖的外部对象,我们抽取出

104、所需要的字段,生成一个内部所需的VO或DTO类 构建一个新的Facade,在Facade中封装调用链路,将外部类转化为内部类 针对外部系统调用,同样的用Facade方法封装外部调用链路无防腐层的情况:有防腐层的情况:具体简单实现,假设所有外部依赖都命名为ExternalXXXService:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结33/自定义的内部值类Datapublic class ItemDTO private Long itemId;private Long sellerId;private String title;private Long p

105、riceInCents;/商品Facade接口public interface ItemFacade ItemDTO getItem(Long itemId);/商品facade实现Servicepublic class ItemFacadeImpl implements ItemFacade Resource private ExternalItemService externalItemService;Override public ItemDTO getItem(Long itemId)ItemDO itemDO=externalItemService.getItem(itemId);i

106、f(itemDO!=null)ItemDTO dto=new ItemDTO();dto.setItemId(itemDO.getItemId();dto.setTitle(itemDO.getTitle();dto.setPriceInCents(itemDO.getPriceInCents();dto.setSellerId(itemDO.getSellerId();return dto;return null;/库存Facadepublic interface InventoryFacade boolean withhold(Long itemId,Integer quantity);S

107、ervicepublic class InventoryFacadeImpl implements InventoryFacade Resource 622324252627282930337383940414243技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结34Servicepublic class CheckoutServiceImpl implements CheckoutService Resource private ItemFacade itemFacade

108、;Resource private InventoryFacade inventoryFacade;Override public OrderDTO checkout(Valid CheckoutCommand cmd)ItemDTO item=itemFacade.getItem(cmd.getItemId();if(item=null)throw new IllegalArgumentException(Item not found);boolean withholdSuccess=inventoryFacade.withhold(cmd.getItemId(),cmd.getQuanti

109、ty();if(!withholdSuccess)throw new IllegalArgumentException(Inventory not enough);/.6223 private ExternalInventoryService externalInventoryService;Override public boolean withhold(Long itemId,Integer quantity)return externalInventoryService.withhold(itemId,quantity);

110、44454647484950通过ACL改造之后,我们ApplicationService的代码改为:很显然,这么做的好处是ApplicationService的代码已经完全不再直接依赖外部的类和方法,而是依赖了我们自己内部定义的值类和接口。如果未来外部服务有任何的变更,需要修改的是Facade类和数据转化逻辑,而不需要修改ApplicationService的逻辑。Repository可以认为是一种特殊的ACL,屏蔽了具体数据操作的细节,即使底层数据库结构变更,数据库类型变更,或者加入其他的持久化方式,Repository的接口保持稳定,ApplicationService就能保持不变。技术人

111、的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结35在一些理论框架里ACL Facade也被叫做Gateway,含义是一样的。Orchestration vs Choreography在本文最后想聊一下复杂业务流程的设计规范。在复杂的业务流程里,我们通常面临两种模式:Orchestration 和 Choreography。很无奈,这两个英文单词的百度翻译/谷歌翻译,都是“编排”,但实际上这两种模式是完全不一样的设计模式。Orchestration的编排(比如SOA/微服务的服务编排Service Orchestration)是我们通常熟悉的用法,Choreogra

112、phy是最近出现了事件驱动架构EDA才慢慢流行起来。网上可能会有其他的翻译,比如编制、编舞、协作等,但感觉都没有真正的把英文单词的意思表达出来,所以为了避免误解,在下文我尽量还是用英文原词。如果谁有更好的翻译方法欢迎联系我。模式简介Orchestration:通常出现在脑海里的是一个交响乐团(Orchestra,注意这两个词的相似性),如下图。交响乐团的核心是一个唯一的指挥家Conductor,在一个交响乐中,所有的音乐家必须听从Conductor的指挥做操作,不可以独自发挥。所以在Orchestration模式中,所有的流程都是由一个节点或服务触发的。我们常见的业务流程代码,包括调用外部服务

113、,就是Orchestration,由我们的服务统一触发。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结36Choreography:通常会出现在脑海的场景是一个舞剧(来自于希腊文的舞蹈,Choros),如下图。其中每个不同的舞蹈家都在做自己的事,但是没有一个中心化的指挥。通过协作配合,每个人做好自己的事,整个舞蹈可以展现出一个完整的、和谐的画面。所以在Choreography模式中,每个服务都是独立的个体,可能会响应外部的一些事件,但整个系统是一个整体。案例用一个常见的例子:下单后支付并发货如果这个案例是Orchestration,则业务逻辑为:下单时从一个

114、预存的账户里扣取资金,并且生成物流单发货,从图上看是这样的:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结37如果这个案例是Choreography,则业务逻辑为:下单,然后等支付成功事件,然后再发货,类似这样:从代码依赖关系来看:Orchestration:涉及到一个服务调用到另外的服务,对于调用方来说,是强依赖的服务提供方。Choreography:每一个服务只是做好自己的事,然后通过事件触发其他的服务,服务之间没有直接调用上的依赖。但要注意的是下游还是会依赖上游的代码(比如事件类),所以可以认为是下游对上游有依赖。从代码灵活性来看:Orchestrat

115、ion:因为服务间的依赖关系是写死的,增加新的业务流程必然需要修改代码。Choreography:因为服务间没有直接调用关系,可以增加或替换服务,而不需要改上游代码。从调用链路来看:Orchestration:是从一个服务主动调用另一个服务,所以是Command-Driven指令驱动的。Choreography:是每个服务被动的被外部事件触发,所以是Event-Driven事件驱动的。从业务职责来看:Orchestration:有主动的调用方(比如:下单服务)。无论下游的依赖是谁,主动的调用方都需要为整个业务流程和结果负责。Choreography:没有主动调用方,每个服务只关心自己的触发条件

116、和结果,没有任何一个服务会为整个业务链路负责模式的区别和选择虽然看起来这两种模式都能达到一样的业务目的,但是在实际开发中他们有巨大的差异:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结38另外需要重点明确的:“指令驱动”和“事件驱动”的区别不是“同步”和“异步”。指令可以是同步调用,也可以是异步消息触发(但异步指令不是事件);反过来事件可以是异步消息,但也完全可以是进程内的同步调用。所以指令驱动和事件驱动差异的本质不在于调用方式,而是一件事情是否“已经”发生。小结:这里给出两个判断方法:1、明确依赖的方向:在代码中的依赖是比较明确的:如果你是下游,上游对你无

117、感知,则只能走事件驱动;如果上游必须要对你有感知,则可以走指令驱动。反过来,如果你是上游,需要对下游强依赖,则是指令驱动;如果下游是谁无所谓,则可以走事件驱动。2.找出业务中的“负责人”:所以在日常业务中当你碰到一个需求时,该如何选择是用Orchestration还是Choreography?驱动力OrchestrationChoreography无直接调用依赖但是有代码依赖可以认为是下游依赖上游调用依赖灵活性业务职责上游为业务负责较差上游强依赖下游指令驱动Command-Driven无全局责任人较高事件驱动Event-Driven技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容

118、后端篇/技术经典总结39第二种方法是根据业务场景找出其中的“负责人”。比如,如果业务需要通知卖家,下单系统的单一职责不应该为消息通知负责,但订单管理系统需要根据订单状态的推进主动触发消息,所以是这个功能的负责人。在一个复杂业务流程里,通常两个模式都要有,但也很容易设计错误。如果出现依赖关系很奇怪,或者代码里调用链路/负责人梳理不清楚的情况,可以尝试转换一下模式,可能会好很多。很显然,没有最好的模式,只有最合适自己业务场景的模式。反例:最近几年比较流行的Event-Driven Architecture(EDA)事件驱动架构,以及Reactive-Program-ming响应式编程(比如RxJa

119、va),虽然有很多创新,但在一定程度上是“当你有把锤子,所有问题都是钉子”的典型案例。他们对一些基于事件的、流处理的问题有奇效,但如果拿这些框架硬套指令驱动的业务,就会感到代码极其“不协调”,认知成本提高。所以在日常选型中,还是要先根据业务场景梳理出来是哪些流程中的部分是Orchestration,哪些是Choreography,然后再选择相对应的框架。哪个模式更好?跟DDD分层架构的关系最后,讲了这么多O vs C,跟DDD有啥关系?很简单:O&C其实是Interface层的关注点,Orchestration=对外的API,而Choreography=消息或事件。当你决策了O还是C之后,需要

120、在interface层承接这些“驱动力”。无论O&C如何设计,Application层都“无感知”,因为ApplicationService天生就可以处理Command、Query和Event,至于这些对象怎么来,是Interface层的决策。所以,虽然Orchestration 和 Choreography是两种完全不同的业务设计模式,但最终落到Application层的代码应该是一致的,这也是为什么Application层是“用例”而不是“接口”,是相对稳定的存在。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结40 入参:具像化Command、Query

121、、Event对象作为ApplicationService的入参,唯一可以的例外是单ID 查询的场景。CQE的语意化:CQE对象有语意,不同用例之间语意不同,即使参数一样也要避免复用。入参校验:基础校验通过Bean Validation api解决。Spring Validation自带Validation的AOP,也可以 自己写AOP。出参:统一返回DTO,而不是Entity或DO。DTO转化:用DTO Assembler负责Entity/VO到DTO的转化。异常处理:不统一捕捉异常,可以随意抛异常。职责:主要负责承接网络协议的转化、Session管理等 接口数量:避免所谓的统一API,不必人

122、为限制接口类的数量,每个/每类业务对应一套接口即可,接口参 数应该符合业务需求,避免大而全的入参 接口出参:统一返回Result 异常处理:应该捕捉所有异常,避免异常信息的泄漏。可以通过AOP统一处理,避免代码里有大量重复代 码。总结只要是做业务的,一定会需要写业务流程和服务编排,但不代表这种代码一定质量差。通过DDD的分层架构里的Interface层和Application层的合理拆分,代码可以变得优雅、灵活,能更快的响应业务但同时又能更好的沉淀。本文主要介绍了一些代码的设计规范,帮助大家掌握一定的技巧。Interface层:Application层:部分Infra层:用ACL防腐层将外部依

123、赖转化为内部代码,隔离外部的影响业务流程设计模式:没有最好的模式,取决于业务场景、依赖关系、以及是否有业务“负责人”。避免拿着锤子找钉子。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结41 CQRS是Application层的一种设计模式,是基于Command和Query分离的一种设计理念,从最简单的对象分离,到目前最复杂的Event-Sourcing。这个topic有很多需要深入的点,也经常可以被用到,特别是结合复杂的Aggregate。后面单独会拉出来讲,标题暂定为CQRS的7层境界 在当今复杂的微服务开发环境下,依赖外部团队开发的服务是不可避免的,但强

124、耦合带来的成本(无论是变更、代码依赖、甚至Maven Jar包间接依赖)是一个复杂系统长期不可忽视的点。ACL防腐层是一种隔离理念,将外部耦合去除,让内部代码更加纯粹。ACL防腐层可以有很多种,Repository是一种特殊的面相数据持久化的ACL,K8S-sidecar-istio 可以说是一种网络层的ACL,但在Java/Spring里可以有比Istio更高效、更通用的方法,待后文介绍。当你开始用起来DDD时,会发现很多代码模式都非常类似,比如主子订单就是总分模式、类目体系的CPV模式也可以用到一些活动上,ECS模式可以在互动业务上发挥作用等等。后面会尝试总结出一些通用的领域设计模式,他们

125、的设计思路、可以解决的问题类型、以及实践落地的方法。前瞻预告技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结42浅谈系统实现层面稳定性保障上海外滩建筑群包括古典主义风格的亚细亚大楼(1915年),英国古典式的上海总会大楼(1911年),欧洲古典折中主义的海关大楼(1925年),仿意大利文艺复兴风格的汇中饭店大楼(1906年)等,这些建筑历经百年风雨,仍巍然屹立固若堡垒,保持其原本风貌,让今人得以深切领略一个世纪以前的壮丽繁华。这一切,除得益于人为保护之外,最主要原因是建筑自身主体结构具备较高的稳定可靠性。而相比传统建筑工程,软件工程有两个显著特点,一是具备规模

126、化快速复制扩散的能力,二是在竣工之后依然可以被改造并保持高速进化。这也就导致了软件工程一点点的不足也可能被快速扩散、无限放大造成大规模损失,进化中既有的稳定性结构可能会被不断打破,可能导致大量的救火投入,疲于奔命,极大地消耗有效而宝贵研发资源,阻碍软件的进行甚至带来灾难性后果。所以对于任何一家上规模的软件开发的企业来说,稳定性保障都是必须面对和解决好的课题。我个人加入阿里之初是在国际支付宝核心团队长期负责金融系统稳定性保障,其后扎根淘系技术三年有余,参与了多种不同类型系统设计与稳定性建设,以及大促稳定性保障工作。对比总结下来无论电商、金融、物流、ERP型软件工程,其稳定性保障是策略是有较多共性

127、经验的。本文主要结合金融、电商两种场景下的个人以及所在团队实践,谈谈稳定性保障的一些思路方法。作者:高丙寅出品:淘系技术导读稳定性保障涉及机房、网络、硬件部署到业务场景、交互设计,再到应用架构代码质量、流量与封网管控、攻防复盘等,是一项非常系统化的工程。而分工协作使得上述大部分工作流程化标准化,比如硬件问题、网络问题、Jvm监控等均可以由专业化团队提供统一配套方案,做为业务开发主要是利用这些工具更快的发现问题,干预度相对有限。但具体业务系统实现距离拖拽式批量生产尚需时日,最终产出实现方案差异可能很大,所以所有稳定性工作的核心还是围绕系统与代码本身来开展,如下图所示,无论是质量团队、各方管控平台

128、,还是研发流程保障机制,最终的稳定性还是体现在系统代码上。因此,对于业务开发的来说,稳定性保障更应该着眼于系统设计实现环节的控制,如下橙色部分:稳定性保障围绕的核心技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结43而正是需求不受控、架构局限、代码扩展性不足、监控日志不统一、下游依赖不统一之类问题导致了每到大促前夕的紧张慌乱,重复的梳理、反复的打补丁,需要额外的进行压力测试,预案配置演练,消耗巨大且效果有限。所以我们需要贯穿于系统实现各环节,来制定合理的稳定性策略。系统实现核心环节与稳定性关系如下图所示:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术

129、栈内容后端篇/技术经典总结44接下来着重针对上图所示各环节,做一下总结。一个人的命运必须要考虑历史进程影响,谈系统稳定性则不可以忽略大的业务环境,业务环境是指所在的部门业务现状,以及自己所在业务部门与其它业务部门的合作现状,可能发生的业务调整重组。有时候为了满足业务上的发展目标,老板们不得不频繁的做业务调整。随之而来的是技术团队与系统架构的拥抱变化,系统拆分、合并、重建一些列工作,必然会极大地打乱既有的稳定性保障策略,所以必须要从业务环境业务架构的层面来审视并调整以跟上变化。比如天猫技术部和淘宝技术部合并、拆分,洋淘业务下线,逛逛品牌上线,躺平业务独立,导致原有支持的统一社区运营工作台不再wo

130、rk,各自垂直业务线因发展需要或者被迫不得不独立出自己的运营后台管理,自己的索引,迁移兼容老数据,影响不可谓不深远。再比如淘宝社交账号体系原来是作为淘宝内部的一个体系,仅仅面向淘宝会员服务,但随着闲鱼、躺平的独立,需要延伸出新的特性化需求,那么以现有的形态继续支持跨BU的业务发展是否是最优的方案。如果继续支持面临人力、机器资源的保障的投入,那这些与当前BU的价值与方向是否吻合?这些是我们业务上要去考虑的问题。平台化产品思维下,所有人都想建平台并急于让更多的人接入进来,以发挥自己平台影响力,体现自身价值。而一旦业务目标调整,团队方向跟着调整,平台这一块儿不再是重点,角色反转,之前苦苦拉过来的客户

131、被告知不再提供服务,甚至限期迁移,这对自己、对客户方的系统稳定性都极其不利。所以,越往上层的团队,建平台越审慎,先做好自己的主业而不是盲目扩展边界。一个基于短期目的,或者缺乏长远规划的所谓平台直接提供给别人用,也是不负责任的表现。而对于一个新的业务来说,我们也需要要求产品运营不仅仅是提出一个商业需求商业模式,而是要系统化思考一个业务的心智及演变趋势。从天马行空的想法到一个可行性方案,技术人员需要发挥自身优势,和产品运营同学一起思考,将商业模式进行细化,想清楚,想透彻,往往业务上一点点的权衡或者调整对技术上的改变将会带来四两拨千斤的效果。系统是业务的直观反馈,业务架构很大程度上决定了进一步的技术

132、架构。如果产研团队对业务理解不深刻不透彻,只着眼于未来一年甚至几个月短期需求与利益,想到哪里做到哪里,那么技术层面就无法做好提前的布局与设计,堆砌、重复就会随之而来。大的环境背景不稳则限制了天花板,后续实现过程上无论怎么努力只能治标难以治本!所以对当前身处的业务的环境整体大图轮廓的认知,是建立全局稳定性思考的前提。业务大环境与业务架构稳定性技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结45电商系统业务架构示意电商系统业务架构示意如果想做一个最小型的电商网站,至少也需要包含这几个业务模块,会员、商户库存、下单模块、店铺管理中心,规模稍大之后每个模块可能要支持独

133、立的运营,可能还要考虑支持商业化接入,还可能延伸出物流、售后、资金管理等延伸模块,那么这些模块将怎么去协作,这些东西我们需要在业务大图中有一定预见。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结46导购场页面架构示意比如泰国用户小A在登录速卖通()后,使用支付宝支付泰铢,购买了一双鞋子。那么这个支付过程至少就会涉及到上面几方。在这个过程中,速卖通是做为一个外部商户之一,支付宝要服务好千万个这样的商户。就需要独立的收单对接团队,独立出收单模块。在用户支付的过程中,还可能根据与银行之间支付服务费差异,营销活动等决策要给用户推荐的支付方式,那么就需要一个运营可干预

134、的支付路由模块,而下游同样提供泰铢支付的泰国支付机构也有很多家,我们对于每一家的接入都是要有商务协议和技术准入,那么把金融渠道看做一个业务单元,既然有资金流动那么独立的财务核算模块就是必不可少的,包括围绕这个支付活动可以沉淀下来的会员体系,决定了在支付平台中要有用户模块。类似于手机淘宝中上海品茶、我的淘宝、收藏夹这类通用的强入口,这种兵家必争之地更需要技术、产品、运营一起考虑清楚业务架构。尽可能提供通用的UI标准与接入方案,前瞻性考虑页面业务架构,能保证我们的页面复杂度不随业务方呈现线性增强趋势,业务接入可以批量化、可配置化。而如果局限于应付单次的特性化需求堆砌,几经迭代,业务逻辑越来越臃肿,每次

135、调整将牵一发而动全身,最终难以为继。产品拥抱变化,加之人员调整频繁,新一批的产品、技术没有人能讲清楚这个页面的真实运行情况。业务架构并不只是部门老板的运筹决策,也不是必须面对特别大的场景才需要考虑。即使我们只是一个小页面甚至小区块的产品、开发,那么对这个页面这个组件也可以有很好的业务规划,比如如下区块虽小,但做为一个产品的流量入口来说却具有决定性意义。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结47和产品设计达成一致后可以抽象出稳定模型如下:可分为:logo区、数字标、小红点、头像区,引导文案区、图片氛围区、色彩氛围区。基于这个稳定的区块交互架构,我们定义

136、统一标准协议,之后服务端就可以前瞻性规划,考虑各类营销场景、AB方案,大做文章以支持多样化业务运营需求且不会因为频繁的前后端联动修改引入稳定性问题。先要清晰认识了解所在部门大的业务环境、背景、未来趋势。确保我们和业务方能建立业务架构上的共识,目标一致。以此为基石,保证方向正确的前提下,贴合实际去考虑系统架构与边界问题,做配套的稳定性方案,才具有更好的可行性!技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结48业务需求把控与稳定性关系从业务需求层面考虑稳定性,主要是做两个方面,一是业务需求过滤(价值判断),二是需求模型简化。关于需求价值判断,尤其是对于创新型产品

137、,产品设计同学思维很发散,天马星空idea层出不穷,这是创新源动力,是好事,但作为技术人员精力很有限,必须脚踏实地的思考可行性问题,必须要对需求方原始需求进行合理质疑,砍掉一些表面上的浮华,实际没有核心价值的伪需求,同时需求精简模型,将有限的研发精力投入到真正有业务价值的地方。做价值判断,必要时候用数据分析、数据驱动手段去证明,很可能分析结论发现整个盘子的天花板就在那里,那么一些需求就自然没有存在的理由,无价值的需求上去了除了浪费开发资源,还会带来系统复杂度的无意义升高,稳定性风险自然就升高。有些时候业务方倾向于把一个需求方案复杂化,我们需要做模型简化,考虑是不是有必要设计如此复杂的规则。如果

138、我们把规则简化到20%以下,是否可以满足90%以上的需求了,而剩下10%是否可以有更轻量的方式去解决。如下针对淘友圈所在的我的淘宝入口,和业务上达成一致后,简化后的逻辑实现复杂度下降50%,那么稳定性风险也会下降50%,且对业务上带来的是同样的用户体验,体感如下:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结49业务领域模型稳定性基于对业务的理解把控,我们拿到一个相对靠谱的业务需求,开始抽象领域模型。领域模型是从纯粹客户需求转化为技术人员可理解的语言,是对业务的高度抽象,根本目的是帮助我们理解和分析业务,以指导进一步的技术实现。这个阶段需要与产品经理反复对焦

139、,充分理解题意深挖出潜在逻辑(实际必须要支持的逻辑,但是局限于需求方认知在需求阶段没有提到),使用uml工具梳理出面向对象编程中的对象,以及对象之间的转化关系,需要抓住整体而不是一上来就陷入细节。比如我们建一个面向页面资源位的投放系统,分析后可以得到如下:如今纷繁复杂的无线页面形态并不是越绚越好,而是需要真正找到对用户有吸引力,有价值的点。正如逍遥子和蒋凡在2020年双11所要求的,简单、好逛。很多时候,技术必须要协助产品去做减法,而业务上的去伪存真化繁为简对于系统复杂度往往带来决定性影响,做稳定性保障必须重视这一环节的把控。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇

140、/技术经典总结50领域模型示意领域模型分析不等于数据库ER图设计,但有了清晰准确的领域模型,再细化出ER图就变得很确定性了。这一步是从微观层面理解业务,基于业务架构抽象出了业务的核心主体及其主体之间的协作关系,确保清晰准确的理解了需求现在要做,未来要做,未来可能做的事情有哪些,基于这个充分的理解,设计稳定可靠可扩展的模型,确保业务领域模型的稳定。系统领域模块划分示意技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结51技术架构与选型稳定性技术架构与选型这一步需要确定编程语言,数据库,系统拆分,以及你的系统之间大致是如何产生关联作用,并最终提供完整的业务能力。技术

141、选型主要包括:技术框架选型,数据存储索引选型,数据交互流转选型等大的模块。对配置有专门中间件团队的公司,出于效率与技术统一性考虑,代码框架一般都有一套成熟稳定的配套方案,业务团队无需在这个上面过渡投入。比如在阿里,可以在一站式研发协同平台aone上一键创建最新应用。主要精力是花在数据存储与数据交互流转技术的选择上,需要结合自身业务特点来做选择。基于业务架构与领域建模、数据规模、未来趋势、团队能力限制综合决策权衡选择合适的架构,没有最好的架构,只有最合适的架构。同时需要有对业务的预判能力,至少产品主线上可能出现的较大变化,要预留架构上的可能性。比如系统是否有必要一开始就应该考虑到读写分离,拆分为

142、几个系统。比如基于业务特性拆分成了读写分离的A,B两个系统,A主要做了门面抗流量,B主要异步任务,定时写入,这样避免因为瞬时异步流量过高影响生产读服务。但因为项目人员变动较大,时间紧任务重,导致没有坚守,有些服务似乎直接写在A中更加节省工作量,妥协一下,这样A与B的职责越来越模糊,久而久之A系统中也有了较多的瞬时流量风险,风险就不可控。这就要求必须从架构上确定系统职责边界,该撕的一定要撕,如果表面总是一团和气,必然是在某些方面做了一些放弃,把所有的毛刺都按在床底下,日积月累总有一天会爆发。而如果拆成读写分离,势必会导致有些其实都强依赖的模块,不太好界定的模块也要远程通信。那么可以达成共识,统一

143、提供一个非client类型的公共二方包,在B中开发,同时供给A、B使用。而如果你做为上游入口,是否考虑提供spi机制,避免后续接入N个下游,就要引入N个二方包的情况。比如考虑一致性保障策略是用分布式事务,还是使用差错补偿机制来处理。比如关于存储,我们是需要用nosql存储还是关系型数据库,还是按照业务特性写多份异构存储来提供更好的性能。做这一步决策就需要综合理解mysql,lindrom特性及应用场景,lindorm与redis做为索引的差异化,以及opensearch的可靠性、延时性问题。数据库设计考虑不足,导致容量瓶颈。数据库的分库分表,则应该考虑未来5至10年的业务规模。关于存储与索引技

144、术的选型,是整个技术选型中的核心,后续将另谋篇幅详谈。技术架构及选型直接决定了我们的系统结构上是否稳定合理,决定了在未来可预期的时间段内是否会被推到重来,是系统稳定的基础。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结52代码实现规范稳定性业务架构-领域建模-技术架构与选型决定了我们整个工程的宏观可行性与各个关键节点的解决方案。接下来进入到编码环节,但这个环节来说,不同的施工队、不同的人员操作上是不一致的,可能一两个点的细节看不出主要影响,但是两边引起质变,最终会决定成败。我们需要有一套规范来保障细节的可控与标准化,来确保系统微观层面的稳定性,比如:比如包装

145、类型与基本类型使用场景,判断对象相等方法,对象做json序列化注意事项。金额字段处理,统一规范的封装工具类。金额统一收口服务端处理。响应给上游的result到底是代表通信成功,还是业务成功。这个在注释上必要详尽说明,避免异常情况下扯 皮。数据库,核心资源操作应始终对照:一锁二判三更新的基本原则。尽量避免使用|、&、异或()、位运算(),因为可读性较差,代码的可读性可维护性是除了代码本身业务价 值外,技术人员对公司最大的贡献。关于代码规范的制定,推荐阿里巴巴Java开发手册一书,它是阿里内部Java工程师所遵循的开发规范,涵盖编程规约、单元测试规约、异常日志规约、MySQL规约、工程规约、安全规

146、约等,这是近万名阿里Java开发人员经验总结,并经历了多次大规模一线实战检验及完善,具备较强的参考意义。代码通用模块稳定性结合自身系统特点,理清楚变与不变。把可以反复使用的部分、易出错的部分以及系统核心引擎抽象出来,做为系统不变的部分。把伴随着业务的变化或者新业务方接入而不断调整的部分提取出来,做为系统中可变的部分。对于不变的部分,运用设计模式及常用套路将其固化下来形成公共模块,降低类似功能重复编写带来的风险,提高增量业务迭代效率。这部分代码投入核心精力让它像工具类一样稳定可靠,之后反复运行。这部分的抽象决定了系统的核心代码层次,保障大楼的上相似的模块统一稳定,有统一的管控手段。对于变的部分,

147、提供扩展点。这部分是会随着业务的变化而不断迭代,同时要考虑让变化的部分具备隔离性。即当改动一个子业务需求时,尽可能从从架构上限定住它的影响范围。追求高内聚,低耦合,满足开闭原则易扩展易维护的代码层次结构。当然这里要避免一个误区,即滥用设计模式,或者为了模式而模式。比如在一个小小的项目里本来简单一个方法调用就能实现,确要过度套用设计模式去编码,折腾出来好几层,开发成本高且给代码的维护带来了困难。私下练手技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结53可以。但在工程层面,则要更多的考虑实用价值,实际一定要结合场景需要。下面简要列举几个常见的点:统一对外服务层异

148、常兜底对于多数应用来说为上游提供hsf是其主要职责。标准是当service层抛异常,我们需要自己处理掉异常,以Result结果中的结果码的形式与外界通信,而不是直接抛运行时异常给业务方。那么我们可以抽象出AOP层,对Service层的异常统一捕获,返回兜底错误码给上游。统一抽象摘要日志模块方法的核心出入参,是我们监控的关键。但是穿插在业务代码中打印,总是容易遗漏而且侵入性很强。那么可以抽象摘要日志注解模块,无侵入的打印方法的摘要日志。统一下游依赖模块在淘系,我们做导购型产品,基本绕不开淘系3C,即UIC(用户中心)、TC(交易中心)、IC(商品中心),可能还会有一个SC(店铺中心),而这些中心

149、又因为历史的原因提供了多套对接查询方法。曾经看到一个系统中对IC的直接依赖有10+处之多,同样是查询商品对象,但由于不同的开发人员依赖了不同的方法。那么每当大促链路梳理的时候,或者IC包做升级的时候,就需要梳理回归多个入口,给系统的链路梳理带来了极大的不确定性。正确的做法是我们对于同一个下游入口,包装出唯一的代理类,唯一的方法,统一维护、监控,反复使用。这里的下游不仅仅指业务系统,同时包括对于一些中间件的依赖,比如:针对hbase、redis、ldb、opensearch、odps的访问封装。Java线程池使用单个应用对异步线程的管理,应该有统一的类收口,使用者只需要传递线程池名及所需要的变量

150、即可。这样通过统一的监控配置一目了然就能看到整个应用对异步线程池的使用管理情况,快速诊断出是哪里的线程池使用不合理导致的系统线程数飙高报警,也便于后续的交接维护。这个类就可以完成对线程池的所有幻想,屏蔽掉对线程池工具类的直接访问,至少包括这些类的行为:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结54/*说明:通用线程池工具类*/public class CommonExecutorManager public static final String POOL_DEFAULT=POOL_DEFAULT;/*线程池map */private Map threa

151、dPoolMap=null;/*默认线程池 */private ExecutorService defaultExecutor=null;PostConstruct public void init()threadPoolMap=new HashMap(2);/*默认线程池 */ThreadFactory defFactory=new mon.util.concurrent.ThreadFactoryBuilder().setNameFor mat(AsyncManager-default-%d).build();this.defaultExecutor=new ThreadPoolExecu

152、tor(8,16,100L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue(1024),defFactory,new ThreadPoolExecutor.DiscardPolicy();threadPoolMap.put(POOL_DEFAULT,defaultExecutor);PreDestroy public void close()if(defaultExecutor!=null)defaultExecutor.shutdown();622324252627282930313

153、2333435361)创建指定线程池2)并发执行/单个执行3)同步执行/异步执行统一的线程池管理工具类示例:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结55 for(ExecutorService pool:threadPoolMap.values()pool.shutdown();/*异步执行supplier *param poolName 线程池 *param supplier supplier *param 结果类型 *return */public CompletableFuture supplyAsync(String poolName,Supp

154、lier supplier)if(poolName=null)poolName=POOL_DEFAULT;ExecutorService executorService=threadPoolMap.getOrDefault(poolName,defaultExecutor);return CompletableFuture.supplyAsync(supplier,executorService);/*并发执行supplier,并等待结束 *param poolName 线程池 *param supplierList supplier list *param 结果类型 *return 结果列表

155、 */public List supplyListSync(String poolName,ListSupplier supplierList)if(poolName=null)poolName=POOL_DEFAULT;ExecutorService executorService=threadPoolMap.getOrDefault(poolName,defaultExecutor);ListCompletableFuture futures=supplierList.stream().map(supplier-CompletableFuture .supplyAsync(supplier

156、,executorService).collect(Collectors.toList();return futures.stream().map(CompletableFuture:join).collect(Collectors.toList();373839404474849505575859606676869707777879技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结56 /*查询线程池 *param poolName 线程池名称 *return */publ

157、ic ExecutorService getExecutorService(String poolName)return threadPoolMap.getOrDefault(poolName,defaultExecutor);/*创建线程池 *param corePoolSize 核心线程数 *param maxSize 最大线程数 *param threadName 线程名称 *param daemon *return */public static ExecutorService createThreadPool(int corePoolSize,int maxSize,String t

158、hreadName,boolean d aemon)ExecutorService executorService=new ThreadPoolExecutor(corePoolSize,maxSize,5,TimeUnit.MINUTES,new LinkedBlockingQueue(),new mon.util.concurrent.ThreadFactoryBuilder().setNameFo rmat(threadName+-%d).setDaemon(daemon).build();return executorService;/*param nThreads *param th

159、readName *return */public static ScheduledThreadPoolExecutor createScheduledPool(int nThreads,String threadName)return new ScheduledThreadPoolExecutor(nThreads,new mon.util.concurrent.ThreadFactoryBuilder().setNameFormat(threadName+-%d).build();public static ExecutorService newCachedThreadPool(Threa

160、dFactory threadFactory)80887888990997989937技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结57public abstract class AbstractMapJob extends MapJobProcessor Override public ProcessResult process(JobContext context)throws Exception tr

161、y String taskName=context.getTaskName();Object task=context.getTask();if(isRootTask(context)/生成子任务 List childTasks=generateChildTasks(MsgSwitch.childTaskCount,context);return map(childTasks,getChildTaskName();else if(getChildTaskName().equals(taskName)try /子任务处理 handleChildTask(task);return new Proc

162、essResult(true);catch(Throwable t)log.error(handleChildTask error.,t);return new ProcessResult(false);catch(Exception e)log.error(process error,e);return new ProcessResult(true);6223242526 return new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new Syn

163、chronousQueue(),threadFactory);2123124定时任务使用同管理线程池一样,单个应用对定时任务的使用,是可以有通用的部分抽象出来,比如对于网格任务中的根任务与子任务的识别。具体的定时任务实例只需要聚焦在子任务的特性读取与单条任务的计算处理上。这样通过统一的监控配置一目了然就能看到整个应用定时任务执行的情况,快速诊断出是哪个定时任务的运行导致系统资源报警。统一的job抽象类示例:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结58 /*根据总记录数拆分生成子任务 *param taskCount 要拆分的子任务

164、个数 *return *throws Exception */private List generateChildTasks(int taskCount,JobContext context)throws Exception long totalCount=getTotalCount(context);log.info(total count:,totalCount);if(totalCount=0)return Collections.emptyList();long taskSize=(long)Math.ceil(totalCount*1.0/taskCount);List taskLi

165、st=new ArrayList();for(long i=0;i taskCount;i+)long start=i*taskSize;IndexChildTask childTask;if(i=taskCount-1)childTask=new IndexChildTask(start,totalCount-(i*taskSize);else childTask=new IndexChildTask(start,taskSize);taskList.add(childTask);return taskList;/*获取要处理的总记录数 *return *throws Exception *

166、/public abstract long getTotalCount(JobContext context)throws Exception;/*处理子任务 *param task */public abstract void handleChildTask(Object task);/*272829303373839404474849505575859606676869技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结59public abstract class Met

167、aqCommonConsumer implements MessageListenerConcurrently private MetaPushConsumer consumer;private String topic;private String tag;private String consumerGroup;/*具体的消息实体类型 */protected Class messageObjClass;public MetaqCommonConsumer()/获取父类带泛型的类型 Type genericSuperclassType=getClass().getGenericSupercl

168、ass();ParameterizedType parameterizedType=(ParameterizedType)genericSuperclassType;Type actualTypeArguments=parameterizedType.getActualTypeArguments();messageObjClass=(Class)actualTypeArguments0;Override public ConsumeConcurrentlyStatus consumeMessage(List list,ConsumeConcurrentlyContext c ontext)if

169、(list=null|list.isEmpty()return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;for(MessageExt msg:list)return consumeSingleMsg(msg,context);62232425262728 *获取子任务名称 *return */public abstract String getChildTaskName();7071727374消息订阅处理将消息接收的注册、序列化为业务类型,异常处理,监控日志做统一的封装,收口,使得消

170、息的消费监控变得简单易于执行,同时便于后续的开发维护,示例类:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结60 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;/*消费消息 *param msg *return */private ConsumeConcurrentlyStatus consumeSingleMsg(MessageExt msg,ConsumeConcurrentlyContext con text)if(msg=null)return ConsumeConcurrentlyStatus.C

171、ONSUME_SUCCESS;try String bodyJsonStr=new String(msg.getBody(),UTF-8);/转换为实际类型 T messageObj=(T)JSON.parseObject(bodyJsonStr,messageObjClass);return process(messageObj,msg,context);catch(Exception e)log.error(consumeMessage error,JSON.toJSONString(msg),e);return ConsumeConcurrentlyStatus.CONSUME_SUCC

172、ESS;/*消息处理方法 *param messageObj 转换为具体类型的消息obj *param messageExt 完整的消息体 *return */protected abstract ConsumeConcurrentlyStatus process(T messageObj,MessageExt messageExt,ConsumeConcurrentlyContext context);/*初始化 */public void start()try consumer=new MetaPushConsumer(consumerGroup);293033738

173、3940447484950557585960667686970技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结61 consumer.subscribe(topic,tag);consumer.registerMessageListener(this);consumer.start();log.info(start success consumerGroup:,topic:,tag:,consumerGroup,topic,tag);catch(Exception e)log.error(sta

174、rt consumer error consumerGroup:,topic:,tag:,errorMsg:,consumerGroup,topic,tag,e.getMessage();public void setTopic(String topic)this.topic=topic;public void setTag(String tag)this.tag=tag;public void setConsumerGroup(String consumerGroup)this.consumerGroup=consumerGroup;777787980818283848

175、58687888990919293报文网关交互比如报文网关系统无论和外部怎样的通信区别变化的部分在于报文格式不同需要有不同的行处理逻辑,不变的是通过http进行request、response的通信逻辑,以及对json、xml格式报文的解析方式。文件解析模块抽取而文件网关系统则一定是现将文件转换成流读取到内存,再转换成一行,针对这一行做特性化处理,最后关闭流操作,那么我们可以把文件处理的的逻辑抽成模板方法,后续来一种新的文件,只需要实现模板方法,专注处理某一行的业务逻辑就可以了。通过通用模块的抽取,我们可以极大的规避同一类的风险,也为后续的统一发现能力的建设提供了基础。技术人的百宝黑皮书202

176、1版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结62代码实现规范稳定性这里主要强调为了做发现能力建设,系统需要满足怎样的设计更合理,围绕代码实现谈谈建设思路。在前述架构合理、分层得当、代码规范到位、兜底保护完善的情况下确保我们项目可以顺利验收上线运行。但在运行过程中都会受到各种因素的影响,难免还是会出问题,出问题并不可怕,关键是能否尽可能早的在第一时间发现问题并解决问题,而发现问题往往比解决问题更具有挑战性。试想如果不该打印日志的地方打印了日志,不该配置报警的地方配置报警,错误码粒度杂乱无法区分,那么真正的问题就不能被及时发现。对于一个直面无线app的服务端应用,系统层面很容易得到如

177、下通用视角:抽象来看系统层面发现能力建设主要围绕以下4个要素:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结63绝大部分的稳定工作都是围绕上述四个要素在投入,梳异常链路、映射错误码、加日志、加监控。针对这些问题,我们需要有统一的、尽可能低成本低侵入的解决方案和标准,在日常的研发过程中就尽量随时准备好。统一错误码标准需要建立错误码标准,分外部错误码、内部错误码,建立错误码统一维护策略。需要上游业务感知,或者方便上下游之间定位问题,可以定义外部错误码,这部分错误码的变化需要非常谨慎,特别是上游已有引用已有理解的情况下。而内部错误码是指系统的各个层次之间内部执行异

178、常得到的错误码,通常不对外输出,主要用于业务场景监控与异常定位。错误码的设计要结合自身业务特点,制定适合自身业务的错误码。比如银行的结果码细分品类繁多,不得不用数字码来替代,但导购应用需要前端感知的结果码很有限,这个时候可读性占据主导地位,就不建议输出使用数字编码,这样的格式机器可读,但人不可读。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结64统一异常处理机制面向异常编程,定义统一业务异常类,通过业务错误码进行异常分级分类。当抛出原生运行时必须打印完整堆栈。当抛出系统业务异常,对于业务预期之中的则权衡是否需要堆栈信息。异常分类的方式有两种,一种是通过继承设

179、计不同的异常类来做异常区分,另一种是通过在异常中增加异常结果码。实践上,推荐使用异常结果码这种比较灵活和方便的方式。统一日志输出规范日志是我们发现、定位问题的基础,几乎绝大部分的稳定性工作都是在围绕着日志进行。基本原则1)鹰眼id、压测标、ip、时间、线程等业务无关的必要信息,由日志框架层统一解决。2)日志文件功能分级,在默认application.log之外至少分出方法xflush摘要日志文件、异常日志文件。3)统一监控日志格式,针对xflush日志文件,定义行列结构4)谨慎打印日志,不多打,也不少打,注意日志打爆磁盘的问题。设计示例格式化摘要日志:日期|eagleyeId|ip|压测标识位

180、|日志级别|预留1|预留2|线程号|(层级场景)(类名方法名业务成功表示位结果码列异常根原因耗时)(监控属性1监控属性2监控属性3)(业务属性1业务属性2业务属性N)其中监控属性区:存放比如用户是否种子用户,返回动态条数等,可能用于监控分析的字段,主要用于监控。业务属性区:存放比如userId,动态id,红包id之类的字段,主要用于异常定位。灵活而业务无侵入的使用方式:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结65/*构造开圈页面入口的动态消息 *param friendPageDTO *return */Digest(logger=COMMON-XFL

181、USH,layer=manager,success=!ret?.isEmpty(),monitor=ret?.size(),attrs=userId )public List getTaoMomentsOpenPageTips(Long userId,OptionalfriendPageD TO)/do something 11输出效果:2020-12-22 16:40:44.618|0b264438145928e77fb|11.23.98.0|WARN|HSFBizPro-cessor-DEFAULT-8-thread-47|(manager-)(

182、TaoMomentsEnterTipsManagergetTaoMoment-sOpenPageTipsY-86ms)(2-)(2200782267981)统一贴合业务的监控机制监控是手段和目标,需要在错误码、异常、日志三个要素的基础之上基于业务特点细化到每一段核心代码逻辑,能够及时发现预期之外的运行逻辑,报警精准程度则直接决定了线上运维成本。根据适用场景可以把分为通知型与大盘型,通知型是指直接通过某种通讯工具报警触达至接收者,而大盘型是指用户定期主动浏览,通过实时或者T+1(h+1)报表能够进行业务统揽以发现关键问题。通用监控告警层这里抛开硬件及基础设施监控先不谈,仅看系统代码层面的情况。通

183、用监控是指具有业务无关性,只要是无线服务端应用都会需要的监控。比如mtop层方法执行超过500ms、错误码环比大涨、异常量大涨、出现未知异常。这类异常应该由统一解决方案,无需按照业务模块重复建设。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结66特定业务场景告警层结合系统所承载的业务本身,对关键链路做特定监控,比如淘友圈特定业务场景监控:建立业务系统统一大盘针对一个系统,我们会有aop监控层、各个子业务场景监控点。随着功能的叠加监控点会越来越多。这些监控点会在具体场景出问题时报警给我们。但假设就在某一个时刻,想知道系统各业务是否都运行正常,就需要有全局视角的

184、业务大盘。在这个大盘上可以覆盖系统的主要业务场景,承担系统晴雨表的效果,关键时候可以一目了然、心中有数。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结67恢复能力基础建设这里主要强调为了做发现能力建设,系统需要满足怎样的设计更合理。当通过发现能力发现问题后,进一步面临的是如何恢复的问题,一般有如下几种手段:业务降级当发现服务接口超时,定位到是某一个下游依赖所致。且判断该依赖并非产品核心流程,此时需要一键关闭掉对该服务的调用,进而保障主流程能够正常工作。要求我们在编码的时候能够梳理强弱依赖,对弱依赖增加必要的降级开关,并形成预案。重试补偿与人工介入重试补偿分自

185、动重试补偿和人工补偿两种情形。自动重试如消息重试,线程内循环重试,一般是因为机器进程中断,网络延时等原因导致的差错场景,这类场景经过系统自动重试机制是可以完成的。而人工补偿主要面对人为引起的脏数据,或者依赖外部机构带来的不可控因素。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结68极限兜底保护稳定性汽车的安全气囊,虽然阻止不了车祸,但很可能会挽救一次生命。兜底保护相当于一些安全手段,比如紧急情况事故无可避免,但是我们可以把损失降低到最小,比如无论在任何情况下,当前端或者服务端出现任何异常的情况下,不允许用户看到诸如500,404,空白页之类的页面。这需要全面

186、的异常分支分析+与产品人员的充分对焦+充分的兜底场景测试。1)单机qps限流2)norya自适应系统load限流3)消息接收处理线程数控制,就是无论什么时候,要保障你的系统是活着的,研发人员必须建立这样的认识。使用meta的线程数来进行削峰平谷,保护应用。比如一个客户的支付单据,因为国外银行端的接口bug,导致返回了不确定的流水单号或者金额数据或者指定导致校验不通过,因而支付状态未推进。这种情况可能必须要通过客服手段去联系银行人员,因为确认后我们需要手工触发单据到支付完成状态。如果偶尔几笔可以由开发人员通过数据订正手段完成,如果这样的情况时有发生,那么就需要考虑是否开发差错恢复功能,让业务运营

187、可以在限定条件下直接介入推进单据状态,完成状态补偿。自动切换这里想说的其实类似于分布式服务发现Zookeeper的自动探测能力,zk通过探测容器是否存活决定是否将容器从可用列表中移除或增加。而对于业务接口我们有时也需要这种探测能力,通过探测接口的可用性,决定是否启用。尤其在有多个下游服务竞争,且稳定性得不到充分保障的情况下诉求较为强烈,比如用户钱包有N个银行卡可用,当某个银行接口出现故障的时候,我们应该具备自动识别的能力。比如超过一定的错误阀值及失败率,自动将这个银行接口切换为不可用将支付方式置灰并给用户以文案提示,每隔一定的时间再做自动的探测尝试,发现成功后再自动切换为可用状态。技术人的百宝

188、黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结69资金操作稳定性资金操作稳定性保障即资损防控1)如何定义资损?广义的资损应该包括资金流转活动中,资金未按照业务规则预期流动的情况,导致业务参与方中的任何一方或多方遭受了资金损失。比如红包活动中,如果因为技术故障给用户少发了5毛或者多发了5毛,无论谁吃了亏,沾了光,只要是不符合预期的,均定义为资损,都应该界定为资损。2)资损也可以归属于稳定性范畴,为什么要单独拎出来?个人理解资损是稳定性问题中危害最大波及面最广的一种,尤其是对金融系统来说。我曾经在支付宝金融核心组参与研发工作4年,看过公司内外不少案例。深刻体会到资金风险可能

189、给业务及公司带来的灾难性后果,金融软件要求我们在所有的地方都做到最好,容不得半点损失,多一分钱少一分钱都要复盘追责。彼时资损案例的发生直接与绩效直接挂钩,每一行代码都需要被自动化脚本覆盖到,花5分钟写的代码,可能需要花2小时来编写、运行测试脚本,要求极其严苛,但在金融系统中,这一切都是值得的。3)电商团队资损防控看法近两年以来,淘系对于资损重视程度逐渐收紧,不少防控机制从蚂蚁引入。因为虽然电商不属于金融系统,但做为技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结70持续重构与稳定性的关系在业务先赢的时代背景下,特定时候不得不做妥协,不得不上临时方案,这样导致即

190、使原本优雅稳定的系统实现,依然会被一点点扭曲,形成越来越多的技术债。这些技术债会成为越来越大的绊脚石,除了会障碍业务快速发展,更会直接带来额外稳定性风险和巨大的人力开销。我们必须要做持续的重构来偿还,下面举几个示例:无线页面接口爆炸问题比如下图所示的某产品上海品茶,一旦进去上海品茶会同时打开8个接口,而这种暴涨增长的接口,势必给后续的开发迭代及稳定性工作带来较大挑战,同时也影响了低端机用户体验。你可能说这一个业务很复杂,所以不得不使用8个。但我们抓取手淘上海品茶接口,发现那么复杂的页面结构,只要一个接口搞定了,因为统一的协议标准定义的科学、通用。而抽象出这样的接口,是需要对产品形态及未来趋势、前后端研发协

191、作机制有充分考虑。一个经济体,任何细小的波动都可能给品牌形象带来巨大破坏和影响。但无论怎样,其要求的严苛程度与金融软件是不一样的,不能所有的问题都一刀切,不断叠加的机制、流程会带来新的问题。尤其针对电商创新型业务,是需要结合具体业务场景具体分析,在稳定性保障与业务创新迭代效率之间找到一个平衡点,定义最合适粒度的资损防控策略。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结71可压测性能力建设这里主要强调为了支持可压测,系统需要满足怎样的设计更合理。导购型业务流量大是其最主要特点,当我们开发新功能的时候,很容易因为节奏的问题而急于上线,完全不考虑这个代码在双11

192、的情况下是否OK。导致到了压测的时候才发现这段逻辑没有识别压测流量,可能会导致将压测数据写入到正式库中。理想的状况是在项目一开始,就考虑对压测的支持方案,预留扩展点,项目时间紧可以理解,但是一定要能提前布局。否则会因为突击压测能力改造导致代码侵入极强,时间上比较被动,最后临近封网,有些流程其实没有经过压测,也只好硬着头皮上线,具有较大不确定性。服务端CI能力建设这里主要强调服务端CI能力,依然是我们系统工程中需要代码实现的部分。对于服务端开发来说,自动化回归主要关注单元测试和接口集成测试。单元测试关注单个类单个方法的逻辑正确系统对商品服务的依赖问题比如对IC、主搜索依赖,全站点下对商品的状态、

193、价格统一处理的问题,这些问题在一两个功能下都不需考虑太多问题。但功能点越来越多,问题就会逐步出现,就需要进行统一收口,把复杂易出错的问题集中在一个地方,集中精力解决一次。feeds流加载逻辑问题:做为一个朋友圈形态的电商内容场的页面,其feeds块类型包括商品、红包、游戏、玩法各种类型,需要从商品、营销、关系、互动获取各种类型数据来透出,那么怎样去组织代码结构,才能在后续尽量小的侵入去接入新品类,需要斟酌推敲。我的淘宝动态入口读写模式问题:当你进入我的淘宝,过去的做法是直接pull的形式,拉取你的好友的动态,为了保护我淘页面的性能,超时设置为100ms。但随着动态类型越来越多,超时的情况时有发

194、生,这个时候就需要基于稳定性与可持续扩展层,提出新的方案并改进既有代码。诸如上述所列,有些重构可能和短期的业务目标无直接的联系,尤其是产品运营同学不能直接感知的情况,很容易被忽略,得不到重视。但如果不做,坑就会在那里越积越多,引发线上的故障额概率就会越来越大。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结72上下游SLA稳定性这里主要强调系统之间的SLA,自身做的再好,合作方上下游没有规范好是不够的,依然隐患重重。这里上下游是指使用你系统服务的前端、上游服务端,以及被你调用的下游服务。SLA只是一种约定,目的是确保接口提供方和使用方能够对接口有一致的理解,能

195、够更好的保障业务活动进行,同时在发生线上故障必须要进行定责的时候,是一份参照标准,一份免责声明。个人认为可以不局限于形式,但一定要可追溯,文档或者清晰的接口注释都具备同样效力。与上下游之间做好SLA协议签订,在金融级系统几乎是必须要做的事情。但在淘系这边业务开发的过程中,发现有时候两个团队的开发一碰头,或者口头约定或者钉钉上沟通,就开始调用接口了。也不管result.success到底代表是业务执行成功,业务受理成功,还是不确定。或者当时沟通清楚了,但是也没有关键性文档沉淀下来,就发上线了,潜在风险很大。比如A依赖B以check是否可以给B用户发红包,那就必须对B的入参、出参,结果码有100%

196、的理解,且有明确文档落下来。性。接口集成测试一般关注单个系统单个service方法的逻辑正确性。具体这个层面做到什么程度需要依赖业务场景而定,比如高风险、模型稳定的核心系统就必须需要严格要求自动化覆盖率,而创新型快速迭代的新业务讲究快跑覆盖核心流程比较合适。比如支付宝多数系统都与资金相关稳定性要求极高,要求每次代码变更的自动化行覆盖率甚至分支覆盖率不能下降,且手工测试不计入内,似乎为了规避风险无论怎样严苛的卡控都是一种政治正确。曾有人做过局部统计,发现写1行业务代码需要5行测试代码来覆盖,改一行代码需要重跑整个系统的数百个用例可能耗时一天,以保障其业务系统高度稳定可用,但弊端是降低了生产效率。

197、而电商内容业务创新型团队,业务变化极快。产品经理idea天马星空,更看重的是业务快速上线快速试错的能力,我们需要两害相权取其轻做决策。针对这样情况建议梳理出业务核心主流程,针对主流程建立自动化回归用例持续运行。长远来看,服务端CI自动化能力是保障系统核心模块稳定性不可获取的手段,但需要视场景、合理规划。忌不切合和业务实际的一刀切要求。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结73/*用户资质检查服务*/public interface CheckService Result check(QueryParam queryParam,UserInfo use

198、rInfo);123456/*开放API2.0_活动投放开放API*/public interface DeliveryOpenService /*投放活动-支持单资源位单实例 *1)result.isSuccess=ture代表业务上数据写入成功,此时data为工匠平台的活动id,建议业务方做存储,便于后续问题的 定位排查 *2)result.isSuccess=false情况下,errorCode会有相信错误信息,业务方需做监控并采取必要的处理 *param singleTypeActivityRequest 活动对象 *return */OpenResultMap createActiv

199、ity(SingleTypeActivityRequest singleTypeActivityRequest);1112总结对比一下,如下Result,就很清晰明了,让人一看就很有确定性:上医治未病,中医治欲病,下医治已病。本文大致围绕系统设计实现各环节,分析了各环节对稳定性保障的影响,同时介绍了对应的一些套路和经验。着眼点主要在于未病、欲病阶段的思考投入,尽可能把一些问题的解决和规避前置均摊到业务需求受理把控、系统分析设计与编码实现阶段,同时持续改进健全,最终达到提高全局稳定性保障工作效能的目的。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技

200、术经典总结74Guava Cache 原理分析与最佳实践在大部分互联网架构中 Cache 已经成为了必可不少的一环。常用的方案有大家熟知的 NoSQL 数据库(Re-dis、Memcached),也有大量的进程内缓存比如 EhCache、Guava Cache、Caffeine 等。本系列文章会选取本地缓存和分布式缓存(NoSQL)的优秀框架比较他们各自的优缺点、应用场景、项目中的最佳实践以及原理分析。本文主要针对本地 Cache 的老大哥 Guava Cache 进行介绍和分析。Guava Cache 通过简单好用的 Client 可以快速构造出符合需求的 Cache 对象,不需要过多复杂的

201、配置,大多数情况就像构造一个 POJO 一样的简单。这里介绍两种构造 Cache 对象的方式:CacheLoader 和 CallableCacheLoader构造 LoadingCache 的关键在于实现 load 方法,也就是在需要访问的缓存项不存在的时候 Cache 会自动调用 load 方法将数据加载到 Cache 中。这里你肯定会想假如有多个线程过来访问这个不存在的缓存项怎么办,也就是缓存的并发问题如何怎么处理是否需要人工介入,这些在下文中也会介绍到。除了实现 load 方法之外还可以配置缓存相关的一些性质,比如过期加载策略、刷新策略。作者:梓川出品:淘系技术基本用法private

202、static final LoadingCache CACHE=CacheBuilder .newBuilder()/最大容量为 100 超过容量有对应的淘汰机制,下文详述 .maximumSize(100)/缓存项写入后多久过期,下文详述 .expireAfterWrite(60*5,TimeUnit.SECONDS)/缓存写入后多久自动刷新一次,下文详述 .refreshAfterWrite(60,TimeUnit.SECONDS)/创建一个 CacheLoader,load 表示缓存不存在的时候加载到缓存并返回 .build(new CacheLoader()/加载缓存数据的方法 Ove

203、rride public String load(String key)return cache +key+;11121314技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结75如果某个缓存过期了或者缓存项不存在于缓存中,而恰巧此此时有大量请求过来请求这个缓存项,如果没有保护机制就会导致大量的线程同时请求数据源加载数据并生成缓存项,这就是所谓的“缓存击穿”。举个简单的例子,某个时刻有 100 个请求同时请求 KEY_25487 这个缓存项,而不巧这个缓存项刚好失效了,那么这 100 个线程(如果有这么多机器和流量的话)就会同时从 DB 加

204、载这个数据,很可怕的点在于就算某一个线程率先获取到数据生成了缓存项,其他的线程还是继续请求 DB 而不会走到缓存。缓存项加载机制/注意返回值是 Cacheprivate static final Cache SIMPLE_CACHE=CacheBuilder .newBuilder().build();public void getTest1()throws Exception String key=KEY_25487;/get 缓存项的时候指定 callable 加载缓存项 SIMPLE_CACHE.get(key,()-cache +key+); );public v

205、oid getTest()throws Exception CACHE.get(KEY_25487);0Callable除了在构造 Cache 对象的时候指定 load 方法来加载缓存外,我们亦可以在获取缓存项时指定载入缓存的方法,并且可以根据使用场景在不同的位置采用不同的加载方式。比如在某些位置可以通过二级缓存加载不存在的缓存项,而有些位置则可以直接从 DB 加载缓存项。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结76看到上面这个图或许你已经有方法解这个问题了,如果多个线程过来如果我们只让一个线程去加载数据生成缓存项,其他线程等待然

206、后读取生成好的缓存项岂不是就完美解决。那么恭喜你在这个问题上,和 Google 工程师的思路是一致的。不过采用这个方案,问题是解了但没有完全解,后面会说到它的缺陷。其实 Guava Cache 在 load 的时候做了并发控制,在多个线程请求一个不存在或者过期的缓存项时保证只有一个线程进入 load 方法,其他线程等待直到缓存项被生成,这样就避免了大量的线程击穿缓存直达 DB。不过试想下如果有上万 QPS 同时过来会有大量的线程阻塞导致线程无法释放,甚至会出现线程池满的尴尬场景,这也是说为什么这个方案解了“缓存击穿”问题但又没完全解。上述机制其实就是 expireAfterWrite/expi

207、reAfterAccess 来控制的,如果你配置了过期策略对应的缓存项在过期后被访问就会走上述流程来加载缓存项。【缓存击穿图例】技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结77缓存项的刷新和加载看起来是相似的,都是让缓存数据处于最新的状态。区别在于:1.缓存项加载是一个被动的过程,而缓存刷新是一个主动触发动作。如果缓存项不存在或者过期只有下次 get 的时候才会触发新值加载。而缓存刷新则更加主动替换缓存中的老值。2.另外一个很重要点的在于,缓存刷新的项目一定是存在缓存中的,他是对老值的替换而非是对 NULL 值的替换。由于缓存项刷新的前提是该缓存项存在于缓

208、存中,那么缓存的刷新就不用像缓存加载的流程一样让其他线程等待而是允许一个线程去数据源获取数据,其他线程都先返回老值直到异步线程生成了新缓存项。这个方案完美解决了上述遇到的“缓存击穿”问题,不过他的前提是已经生成缓存项了。在实际生产情况下我们可以做 缓存预热,提前生成缓存项,避免流量洪峰造成的线程堆积。这套机制在 Guava Cache 中是通过 refreshAfterWrite 实现的,在配置刷新策略后,对应的缓存项会按照设定的时间定时刷新,避免线程阻塞的同时保证缓存项处于最新状态。但他也不是完美的,比如他的限制是缓存项已经生成,并且如果恰巧你运气不好,大量的缓存项同时需要刷新或者过期,就会

209、有大量的线程请求 DB,这就是常说的“缓存血崩”。缓存项刷新机制上面说到缓存项大面积失效或者刷新会导致雪崩,那么就只能限制访问 DB 的数量了,位置有三个地方:1.源头:因为加载缓存的线程就是前台请求线程,所以如果控制请求线程数量的确是减少大面积失效对 DB 的请求,那这样一来就不存在高并发请求,就算不用缓存都可以。2.中间层缓冲:因为请求线程和访问 DB 的线程是同一个,假如在中间加一层缓冲,通过一个后台线程池去异步刷新缓存所有请求线程直接返回老值,这样对于 DB 的访问的流量就可以被后台线程池的池大小控住。3.底层:直接控 DB 连接池的池大小,这样访问 DB 的连接数自然就少了,但是如果

210、大量请求到连接池发现获取不到连接程序一样会出现连接池满的问题,会有大量连接被拒绝的异常。所以比较合适的方式是通过添加一个异步线程池异步刷新数据,在 Guava Cache 中实现方案是重写 Cache-Loader 的 reload 方法。缓存项异步刷新机制技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结78private static final LoadingCache ASYNC_CACHE=CacheBuilder.newBuilder().build(CacheLoader.asyncReloading(new CacheLoader()Overri

211、de public String load(String key)return key;Override public ListenableFuture reload(String key,String oldValue)throws Exception return super.reload(key,oldValue);,new ThreadPoolExecutor(5,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue();6LocalCache 源码分析先整体看下 Cache

212、的类结构,下面的这些子类表示了不同的创建方式本质还都是 LocalCache核心代码都在 LocalCache 这个文件中,并且通过这个继承关系可以看出 Guava Cache 的本质就是 Concur-rentMap。【Cache 类图】【LocalCache 继承与实现】技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结79V get(K key,CacheLoader loader)throws ExecutionException int hash=hash(checkNotNull(key);/根据 hash 获取对应的 segment 然后从 seg

213、ment 获取具体值 return segmentFor(hash).get(key,hash,loader);12345V get(K key,int hash,CacheLoader loader)throws ExecutionException checkNotNull(key);checkNotNull(loader);try /count 表示在这个 segment 中存活的项目个数 if(count!=0)123456在看源码之前先理一下流程,先理清思路。如果想直接看源码理解流程可以先跳过这张图 这里核心理一下 Get 的流程,put 阶段比较简单就不做分析了。LocalCach

214、e#getSegment#get【get 缓存数据流程图】技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结80 /获取 segment 中的元素(ReferenceEntry)包含正在 load 的数据 ReferenceEntry e=getEntry(key,hash);if(e!=null)long now=map.ticker.read();/获取缓存值,如果是 load,invalid,expired 返回 null,同时检查是否过期了,过期移除并返回 null V value=getLiveValue(e,now);if(value!=null)/

215、记录访问时间 recordRead(e,now);/记录缓存命中一次 statsCounter.recordHits(1);/刷新缓存并返回缓存值,后面展开 return scheduleRefresh(e,key,hash,value,now,loader);ValueReference valueReference=e.getValueReference();/如果在 loading 等着,后面展开 if(valueReference.isLoading()return waitForLoadingValue(e,key,valueReference);/走到这说明从来没写入过值 或者 值

216、为 null 或者 过期(数据还没做清理),后面展开 return lockedGetOrLoad(key,hash,loader);catch(ExecutionException ee)Throwable cause=ee.getCause();if(cause instanceof Error)throw new ExecutionError(Error)cause);else if(cause instanceof RuntimeException)throw new UncheckedExecutionException(cause);throw ee;finally postRea

217、dCleanup();7895272829303373839404142技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结81/mon.cache.LocalCache.Segment#scheduleRefreshV scheduleRefresh(ReferenceEntry entry,K key,int hash,V oldValue,long now,CacheLoader loader)if(/配置了刷新策略 refreshAfterWrite map.refre

218、shes()/到刷新时间了&(now-entry.getWriteTime()map.refreshNanos)/没在 loading&!entry.getValueReference().isLoading()/开始刷新,下面展开 V newValue=refresh(key,hash,loader,true);if(newValue!=null)return newValue;return oldValue;/mon.cache.LocalCache.Segment#refreshV refresh(K key,int hash,CacheLoader loader,boolean che

219、ckTime)/插入 loading 节点 final LoadingValueReference loadingValueReference=insertLoadingValueReference(key,hash,checkTime);if(loadingValueReference=null)return null;/异步刷新,下面展开 ListenableFuture result=loadAsync(key,hash,loadingValueReference,loader);if(result.isDone()622

220、32425262728293033738394041Segment#scheduleRefresh技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结82 try return Uninterruptibles.getUninterruptibly(result);catch(Throwable t)/dont let refresh exceptions propagate;error was already logged return null;/mon.cache.LocalCache.Segment#loadAsyncListenab

221、leFuture loadAsync(final K key,final int hash,final LoadingValueReference loadingValueReference,CacheLoader loader)/通过 loader 异步加载数据,下面展开 final ListenableFuture loadingFuture=loadingValueReference.loadFuture(key,loader);loadingFuture.addListener(new Runnable()Override public void run()try getAndReco

222、rdStats(key,hash,loadingValueReference,loadingFuture);catch(Throwable t)logger.log(Level.WARNING,Exception thrown during refresh,t);loadingValueReference.setException(t);,directExecutor();return loadingFuture;/mon.cache.LocalCache.LoadingValueReference#loadFuturepublic ListenableFuture loadFuture(K

223、key,CacheLoader loader)try stopwatch.start();/oldValue 指在写入 loading 节点前这个位置的值,如果这个位置之前没有值 oldValue 会被赋值为 UNSET /UNSET.get()值为 null,所以这个缓存项从来没有进入缓存需要同步 load 具体原因前面提到了,如果通过 /异步 reload,由于没有老值会导致其他线程返回的都是 null V previousValue=oldValue.get();42434445464748495055758596066768697071727

224、37475767778798081828384技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结83V waitForLoadingValue(ReferenceEntry e,K key,ValueReference valueReference)throws ExecutionException /首先你要是一个 loading 节点 if(!valueReference.isLoading()throw new AssertionError();checkState(!Thread.holdsLock(e),Recursive load of:%s,key

225、);/dont consider expiration as were concurrent with loading try if(previousValue=null)V newValue=loader.load(key);return set(newValue)?futureValue:Futures.immediateFuture(newValue);/异步 load ListenableFuture newValue=loader.reload(key,previousValue);if(newValue=null)return Futures.immedia

226、teFuture(null);/To avoid a race,make sure the refreshed value is set into loadingValueReference /*before*returning newValue from the cache query.return transform(newValue,new mon.base.Function()Override public V apply(V newValue)LoadingValueReference.this.set(newValue);return newValue;,directExecuto

227、r();catch(Throwable t)ListenableFuture result=setException(t)?futureValue:fullyFailedFuture(t);if(t instanceof InterruptedException)Thread.currentThread().interrupt();return result;8586878889909979899371112113Segment#waitForLoadingValue技术人的百宝黑皮书2021版淘系技术出品

228、01年度精选技术栈内容后端篇/技术经典总结84 V value=valueReference.waitForValue();if(value=null)throw new InvalidCacheLoadException(CacheLoader returned null for key +key+.);/re-read ticker now that loading has completed long now=map.ticker.read();recordRead(e,now);return value;finally statsCounter.recordMisses(1);/mon

229、.cache.LocalCache.LoadingValueReference#waitForValuepublic V waitForValue()throws ExecutionException return getUninterruptibly(futureValue);/mon.util.concurrent.Uninterruptibles#getUninterruptiblypublic static V getUninterruptibly(Future future)throws ExecutionException boolean interrupted=false;try

230、 while(true)try /hang 住,如果该线程被打断了继续回去 hang 住等结果,直到有结果返回 return future.get();catch(InterruptedException e)interrupted=true;finally if(interrupted)Thread.currentThread().interrupt();62232425262728293033738394044748技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结85V

231、 lockedGetOrLoad(K key,int hash,CacheLoader loader)throws ExecutionException ReferenceEntry e;ValueReference valueReference=null;LoadingValueReference loadingValueReference=null;boolean createNewEntry=true;/要对 segment 写操作,先加锁 lock();try /re-read ticker once inside the lock long now=map.ticker.read()

232、;preWriteCleanup(now);/这里基本就是 HashMap 的代码,如果没有 segment 的数组下标冲突了就拉一个链表 int newCount=this.count-1;AtomicReferenceArrayReferenceEntry table=this.table;int index=hash&(table.length()-1);ReferenceEntry first=table.get(index);for(e=first;e!=null;e=e.getNext()K entryKey=e.getKey();if(e.getHash()=hash&entry

233、Key!=null&map.keyEquivalence.equivalent(key,entryKey)valueReference=e.getValueReference();/如果在加载中 不做任何处理 if(valueReference.isLoading()createNewEntry=false;else V value=valueReference.get();/如果缓存项为 null 数据已经被删除,通知对应的 queue if(value=null)enqueueNotification(entryKey,hash,value,valueReference.getWeight

234、(),RemovalCause.COLLECTED);/这个是 double check 如果缓存项过期 数据没被删除,通知对应的 queue else if(map.isExpired(e,now)/This is a duplicate check,as preWriteCleanup already purged expired /entries,but lets accommodate an incorrect expiration queue.enqueueNotification(entryKey,hash,value,valueReference.getWeight(),Remo

235、valCause.EXPIRED);62232425262728293033738394041Segment#lockedGetOrLoad技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结86 /再次看到的时候这个位置有值了直接返回 else recordLockedRead(e,now);statsCounter.recordHits(1);return value;/immediately reuse invalid entries writeQueue.remove(

236、e);accessQueue.remove(e);this.count=newCount;/write-volatile break;/没有 loading,创建一个 loading 节点 if(createNewEntry)loadingValueReference=new LoadingValueReference();if(e=null)e=newEntry(key,hash,first);e.setValueReference(loadingValueReference);table.set(index,e);else e.setValueReference(loadingValueR

237、eference);finally unlock();postWriteCleanup();if(createNewEntry)try /Synchronizes on the entry to allow failing fast when a recursive load is /detected.This may be circumvented when an entry is copied,but will fail fast most /of the time.synchronized(e)return loadSync(key,hash,loadingValueReference,

238、loader);finally statsCounter.recordMisses(1);42434445464748495055758596066768697077778798081828384技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结87结合上面图以及源码我们发现在整个流程中 GuavaCache 是没有额外的线程去做数据清理和刷新的,基本都是通过 Get 方法来触发这些动作,减少了设计的复杂性和降低了系统开销。简单回顾下 Get 的流程以及在每个阶段做的事情,返回的值。首先判断缓存是

239、否过期然后判断是否需要刷新,如果过期了就调用 loading 去同步加载数据(其他线程阻塞),如果是仅仅需要刷新调用 reloading 异步加载(其他线程返回老值)。所以如果 refreshTime expireTime 意味着永远走不到缓存刷新逻辑,缓存刷新是为了在缓存有效期内尽量保证缓存数据一致性所以在配置刷新策略和过期策略时一定保证 refreshTime expireTime。最后关于 Guava Cache 的使用建议(最佳实践):1.如果刷新时间配置的较短一定要重载 reload 异步加载数据的方法,传入一个自定义线程池保护 DB 2.失效时间一定要大于刷新时间 3.如果是常驻内

240、存的一些少量数据失效时间可以配置的较长刷新时间配置短一点(根据业务对缓存失效容忍度)总结 else /The entry already exists.Wait for loading.return waitForLoadingValue(e,key,valueReference);858687888990技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结88一种低延迟的超时中心实现方式高延迟方案与低延迟方案的区别。在很多产品中都存在生命周期相关的设计,时间节点到了之后需要做对应的事情。超时中心(TimeOutCenter,TOC)负责存储和调度生命周期节点上面

241、的超时任务,当超时任务设置的超时时间到期后,超时中心需要立即调度处理这些超时任务。对于一些需要低延迟的超时场景,超时中心调度延迟会给产品带来不可估量的影响。因此本文提出一种低延迟的超时中心实现方式,首先介绍传统的超时中心的实现方案,以及传统方案中的缺点,然后介绍低延迟的方案,说明如何解决传统方案中的延迟问题。作者:默达出品:淘系技术背景整体框架传统的超时中心整体框架如下所示,任务输入后存储在超时任务库中,定时器触发运行数据库扫描器,数据库扫描器从超时任务库中扫描已经到达超时时间的任务,已经到达超时时间的任务存储在机器的内存队列中,等待交给业务处理器进行处理,业务处理器处理完成后更新任务状态。在

242、大数据时代,超时任务数量肯定是很大的,传统的超时中心通过分库分表支持存储海量的超时任务,定时器触发也需要做相应的改变,需要充分利用集群的能力,下面分别从超时任务库和定时器触发两方面详细介绍。传统高延迟方案技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结89任务库计划任务库数据模型如下所示,采用分库分表存储,一般可设计为8个库1024个表,具体可以根据业务需求调整。biz_id为分表键,job_id为全局唯一的任务ID,status为超时任务的状态,action_time为任务的执行时间,attribute存储额外的数据。只有当action_time小于当前时间

243、且status为待处理时,任务才能被扫描器加载到内存队列。任务被处理完成后,任务的状态被更新成已处理。定时调度设计定时调度流程图如下所示,定时器每间隔10秒触发一次调度,从集群configserver中获取集群ip列表并为当前机器编号,然后给所有ip分配表。分配表时需要考虑好几件事:一张表只属于一台机器,不会出现重复扫描;机器上线下线需要重新分配表。当前机器从所分配的表中扫描出所有状态为待处理的超时任务,遍历扫描出的待处理超时任务。对于每个超时任务,当内存队列不存在该任务且内存队列未满时,超时任务才加入内存队列,否则循环检查等待。job_id bigint unsigned 超时任务的ID,全

244、局唯一gmt_create datetime 创建时间gmt_modified datetime 修改时间biz_id bigint unsigned 业务id,一般为关联的主订单或子订单idbiz_type bigint unsigned 业务类型status tinyint 超时任务状态(0待处理,2已处理,3取消)action_time datetime 超时任务执行时间attribute varchar 额外数据12345678技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结90缺点 需要定时器定时调度,定时器调度间隔时间加长了超时任务处理的延迟时间;

245、数据库扫描器为避免重复扫描数据,一张表只能属于一台机器,任务库分表的数量就是任务处理的并发 度,并发度受限制;当单表数据量庞大时,即使从单张表中扫描所有待处理的超时任务也需要花费很长的时间;本方案总体处理步骤为:先扫描出所有超时任务,再对单个超时任务进行处理;超时任务处理延迟时间需 要加上超时任务扫描时间;技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结91任务输入后分为两个步骤。第一个步骤是将任务存储到任务库,本方案的任务库模型设计和上面方案中的任务库模型设计一样;第二步骤是任务定时,将任务的jobId和actionTime以一定方式设置到Redis集群中,

246、当定时任务的超时时间到了之后,从Redis集群pop超时任务的jobId,根据jobId从任务库中查询详细的任务信息交给业务处理器进行处理,最后更新任务库中任务的状态。本方案与上述方案最大的不同点就是超时任务的获取部分,上述方案采用定时调度扫描任务库,本方案采用基于Redis的任务定时系统,接下来将具体讲解任务定时的设计。低延迟方案 本方案处理超时任务的最小延迟为定时器的定时间隔时间,在任务数量庞大的情况下,本方案可能存在较 大延迟。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结92Topic的定义有三部分组成,topic表示主题名称,slotAmount表

247、示消息存储划分的槽数量,topicType表示消息的类型。主题名称是一个Topic的唯一标示,相同主题名称Topic的slotAmount和topicType一定是一样的。消息存储采用Redis的Sorted Set结构,为了支持大量消息的堆积,需要把消息分散存储到很多个槽中,slotAmount表示该Topic消息存储共使用的槽数量,槽数量一定需要是2的n次幂。在消息存储的时候,采用对指定数据或者消息体哈希求余得到槽位置。Redis存储设计Topic的设计技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结93上图中topic划分了8个槽位,编号0-7。计算消息

248、体对应的CRC32值,CRC32值对槽数量进行取模得到槽序号,SlotKey设计为#topic_#index(也即Redis的键),其中#表示占位符。StoreQueue结构采用Redis的Sorted Set,Redis的Sorted Set中的数据按照分数排序,实现定时消息的关键就在于如何利用分数、如何添加消息到Sorted Set、如何从Sorted Set中弹出消息。定时消息将时间戳作为分数,消费时每次弹出分数小于当前时间戳的一个消息。StoreQueue的设计为了保障每条消息至少消费一次,消费者不是直接pop有序集合中的元素,而是将元素从StoreQueue移动到PrepareQue

249、ue并返回消息给消费者,等消费成功后再从PrepareQueue从删除,或者消费失败后从Prea-preQueue重新移动到StoreQueue,这便是根据二阶段提交的思想实现的二阶段消费。在后面将会详细介绍二阶段消费的实现思路,这里重点介绍下PrepareQueue的存储设计。StoreQueue中每一个Slot对应PrepareQueue中的Slot,PrepareQueue的SlotKey设计为prepare_#topic#index。PrepareQueue采用Sorted Set作为存储,消息移动到PrepareQueue时刻对应的(秒级时间戳*1000+重试次数)作为分数,字符串存

250、储的是消息体内容。这里分数的设计与重试次数的设计密切相关,所以在重试次数设计章节详细介绍。PrepareQueue的SlotKey设计中需要注意的一点,由于消息从StoreQueue移动到PrepareQueue是通过Lua脚本操作的,因此需要保证Lua脚本操作的Slot在同一个Redis节点上,如何保证PrepareQueue的SlotKey和对应的StoreQueue的SlotKey被hash到同一个Redis槽中呢。Redis的hash tag功能可以指定SlotKey中只有某一部分参与计算hash,这一部分采用包括,因此PrepareQueue的SlotKey中采用包括了StoreQu

251、eue的SlotKey。PrepareQueue的设计技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结94消息重试消费16次后,消息将进入DeadQueue。DeadQueue的SlotKey设计为prepare#topic#index,这里同样采用hash tag功能保证DeadQueue的SlotKey与对应StoreQueue的SlotKey存储在同一Redis节点。DeadQueue的设计生产者的任务就是将消息添加到StoreQueue中。首先,需要计算出消息添加到Redis的SlotKey,如果发送方指定了消息的slotBasis(否则采用conte

252、nt代替),则计算slotBasis的CRC32值,CRC32值对槽数量进行取模得到槽序号,SlotKey设计为#topic_#index,其中#表示占位符。发送定时消息时需要设置actionTime,actionTime必须大于当前时间,表示消费时间戳,当前时间大于该消费时间戳的时候,消息才会被消费。因此在存储该类型消息的时候,采用actionTime作为分数,采用命令zadd添加到Redis。定时消息生产每台机器将启动多个Woker进行超时消息消费,Woker即表示线程,定时消息被存储到Redis的多个Slot中,因此需要zookeeper维护集群中Woker与slot的关系,一个Slot

253、只分配给一个Woker进行消费,一个Woker可以消费多个Slot。Woker与Slot的关系在每台机器启动与停止时重新分配,超时消息消费集群监听了zookeeper节点的变化。Woker与Slot关系确定后,Woker则循环不断地从Redis拉取订阅的Slot中的超时消息。在StoreQueue存储设计中说明了定时消息存储时采用Sorted Set结构,采用定时时间actionTime作为分数,因此定时消息按照时间大小存储在Sorted Set中。因此在拉取超时消息进行只需采用Redis命令ZRANGEBYSCORE弹出分数小于当前时间戳的一条消息。超时消息消费技术人的百宝黑皮书2021版淘

254、系技术出品01年度精选技术栈内容后端篇/技术经典总结95为了保证系统的可用性,还需要考虑保证定时消息至少被消费一次以及消费的重试次数,下面将具体介绍如何保证至少消费一次和消费重试次数控制。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结96至少消费一次的问题比较类似银行转账问题,A向B账户转账100元,如何保障A账户扣减100同时B账户增加100,因此我们可以想到二阶段提交的思想。第一个准备阶段,A、B分别进行资源冻结并持久化undo和redo日志,A、B分别告诉协调者已经准备好;第二个提交阶段,协调者告诉A、B进行提交,A、B分别提交事务。本方案基于二阶段提

255、交的思想来实现至少消费一次。Redis存储设计中PrepareQueue的作用就是用来冻结资源并记录事务日志,消费者端即是参与者也是协调者。第一个准备阶段,消费者端通过执行Lua脚本从StoreQueue中Pop消息并存储到PrepareQueue,同时消息传输到消费者端,消费者端消费该消息;第二个提交阶段,消费者端根据消费结果是否成功协调消息队列服务是提交还是回滚,如果消费成功则提交事务,该消息从PrepareQueue中删除,如果消费失败则回滚事务,消费者端将该消息从PrepareQueue移动到StoreQueue,如果因为各种异常导致PrepareQueue中消息滞留超至少消费一次技术

256、人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结97技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结98采用二阶段消费方式,需要将消息在StoreQueue和PrepareQueue之间移动,如何实现重试次数控制呢,其关键在StoreQueue和PrepareQueue的分数设计。PrepareQueue的分数需要与时间相关,正常情况下,消费者不管消费失败还是消费成功,都会从Prepare-Queue删除消息,当消费者系统发生异常或者宕机的时候,消息就无法从PrepareQueue中删除,我们也不知道消费者是否消费成功,为保障消息

257、至少被消费一次,我们需要做到超时回滚,因此分数需要与消费时间相关。当PrepareQueue中的消息发生超时的时候,将消息从PrepareQueue移动到StoreQueue。因此PrepareQueue的分数设计为:秒级时间戳*1000+重试次数。定时消息首次存储到StoreQueue中的分数表示消费时间戳,如果消息消费失败,消息从PrepareQueue回滚到StoreQueue,定时消息存储时的分数都表示剩余重试次数,剩余重试次数从16次不断降低最后为0,消息进入死信队列。消息在StoreQueue和PrepareQueue之间移动流程如下:消费重试次数控制技术人的百宝黑皮书2021版淘

258、系技术出品01年度精选技术栈内容后端篇/技术经典总结99技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结100 消费低延迟:采用基于Redis的定时方案直接从Redis中pop超时任务,避免扫描任务库,大大减少了延迟时间。可控并发度:并发度取决于消息存储的Slot数量以及集群Worker数量,这两个数量都可以根据业务需要进行调控,传统方案中并发度为分库分表的数量。高性能:Redis单机的QPS可以达到10w,Redis集群的QPS可以达到更高的水平,本方案没有复杂查询,消费过程中从Redis拉取超时消息的时间复杂度为O(1)。高可用:至少消费一次保障了定时消息

259、一定被消费,重试次数控制保证消费不被阻塞。优点技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结101Java类加载器 classloader 的原理及应用作者:金雅博(行泽)出品:淘系技术classloader顾名思义,即是类加载。虚拟机把描述类的数据从class字节码文件加载到内存,并对数据进行检验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。了解java的类加载机制,可以快速解决运行时的各种加载问题并快速定位其背后的本质原因,也是解决疑难杂症的利器。因此学好类加载原理也至关重要。classloader的加载过程类

260、从被加载到虚拟机内存到被卸载,整个完整的生命周期包括:类加载、验证、准备、解析、初始化、使用和卸载七个阶段。其中验证,准备,解析三个部分统称为连接。接下来我们可以详细了解下类加载的各个过程。什么是classloader技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结102classloader的整个加载过程还是非常复杂的,具体的细节可以参考深入理解java虚拟机进行深入了解。为了方便记忆,我们可以使用一句话来表达其加载的整个过程,“家宴准备了西式菜”,即家(加载)宴(验证)准备(准备)了西(解析)式(初始化)菜。保证你以后能够很快的想起来。虽然classloa

261、der的加载过程有复杂的5步,但事实上除了加载之外的四步,其它都是由JVM虚拟机控制的,我们除了适应它的规范进行开发外,能够干预的空间并不多。而加载则是我们控制classloader实现特殊目的最重要的手段了。也是接下来我们介绍的重点了。classloader双亲委托机制classloader的双亲委托机制是指多个类加载器之间存在父子关系的时候,某个class类具体由哪个加载器进行加载的问题。其具体的过程表现为:当一个类加载的过程中,它首先不会去加载,而是委托给自己的父类去加载,父类又委托给自己的父类。因此所有的类加载都会委托给顶层的父类,即Bootstrap Classloader进行加载,

262、然后父类自己无法完成这个加载请求,子加载器才会尝试自己去加载。使用双亲委派模型,Java类随着它的加载器一起具备了一种带有优先级的层次关系,通过这种层次模型,可以避免类的重复加载,也可以避免核心类被不同的类加载器加载到内存中造成冲突和混乱,从而保证了Java核心库的安全。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结103整个java虚拟机的类加载层次关系如上图所示,启动类加载器(Bootstrap Classloader)负责将/lib目录下并且被虚拟机识别的类库加载到虚拟机内存中。我们常用基础库,例如java.util.*,java.io.*,java.

263、lang.*等等都是由根加载器加载。扩展类加载器(Extention Classloader)负责加载JVM扩展类,比如swing系列、内置的js引擎、xml解析器等,这些类库以javax开头,它们的jar包位于/lib/ext目录中。应用程序加载器(Application Classloader)也叫系统类加载器,它负责加载用户路径(ClassPath)上所指定的类库。我们自己编写的代码以及使用的第三方的jar包都是由它来加载的。自定义加载器(Custom Classloader)通常是我们为了某些特殊目的实现的自定义加载器,后面我们得会详细介绍到它的作用以及使用场景。双亲委托机制看起来比较

264、复杂,但是其本身的核心代码逻辑却是非常的清晰简单,我们着重抽取了类加载的双亲委托的核心代码如下,不过二十行左右。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结104类加载器是java语言的一项创新,也是java语言流行的重要原因这一。通过灵活定义classloader的加载机制,我们可以完成很多事情,例如解决类冲突问题,实现热加载以及热部署,甚至可以实现jar包的加密保护。接下来,我们会针对这些特殊场景进行逐一介绍。依赖冲突做过多人协同开发的大型项目的同学可能深有感触。基于maven的pom进制可以方便的进行依赖管理,但是由于maven依赖的传递性,会导致我

265、们的依赖错综复杂,这样就会导致引入类冲突的问题。最典型的就是NoSuchMethodError错误。在阿里平时的项目开发中是否也会遇到类似的问题吗,答案是肯定的。例如阿里内部也很多成熟的中间件,由不同的中间件团队来负责。那么当一个项目引入不同的中间件的时候,该如何避免依赖冲突的问题呢?首先我们用一个非常简单的场景来描述为什么会出现类冲突的问题。某个业务引用了消息中间件(例如metaq)和微服务中间件(例如dubbo),这两个中间件也同时引用了fastjson-2.0和fastjson-3.0版本,而业务自己本身也引用了fastjson-1.0版本。这三个版本表现不同之处在于classA类中方法

266、数目不相同,我们根据maven依赖处理的机制,引用路径最短的fastjson-1.0会真正作为应用最终的依赖,其它两个版本的fastjson则会被忽略,那么中间件在调用method2()方法的时候,则会抛出方法找不到异常。或许你会说,将所有依赖fastjson的版本都升级到3.0不是就能解解决问题吗?确实这样能够解决问题,但是在实际操作中不太现实,首先,中间件团队和业务团队之间并不是一个团队,并不能做到高效协同,classloader的应用场景技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结105其次是中间件的稳定性是需要保障的,不可能因为包冲突问题,就升级版

267、本,更何况一个中间件依赖的包可能有上百个,如果纯粹依赖包升级来解决,不仅稳定性难以保障,排包耗费的时间恐怕就让人窒息了。那如何解决包冲突的问题呢?答案就是pandora(潘多拉),通过自定义类加载器,为每个中间件自定义一个加载器,这些加载器之间的关系是平行的,彼此没有依赖关系。这样每个中间件的classloader就可以加载各自版本的fastjson。因为一个类的全限定名以及加载该类的加载器两者共同形成了这个类在JVM中的惟一标识,这也是阿里pandora实现依赖隔离的基础。可能到这里,你又会有新的疑惑,根据双亲委托模型,App Classloader分别继承了Custom Classload

268、er.那么业务包中的fastjson的class在加载的时候,会先委托到Custom ClassLoader。这样不就会导致自身依赖的fastjson版本被忽略吗?确实如此,所以潘多拉又是如何做的呢?-技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结106首先每个中间件对应的ModuleClassLoader在加载中间对应的class文件的同时,根据中间件配置的export.index负责将要需要透出的class(主要是中间件api接口的相关类)索引到exportedClassHashMap中,然后应用程序的类加载器会持有这个exportedClassHash

269、Map,因此应用程序代码在loadClass的时候,会优先判断exportedClassHashMap是否存在当前类,如果存在,则直接返回,如果不存在,则再使用传统的双亲委托机制来进行类加载。这样中间件MoudleClassloader不仅实现了中间件的加载,也实现了中间件关键服务类的透出。我们可以大概看下应用程序类加载的过程:技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结107在开发项目的时候,我们需要频繁的重启应用进行程序调试,但是java项目的启动少则几十秒,多则几分钟。如此慢的启动速度极大地影响了程序开发的效率,那是否可以快速的进行启动,进而能够快速

270、的进行开发验证呢?答案也是肯定的,通过classloader我们可以完成对变更内容的加载,然后快速的启动。常用的热加载方案有好几个,接下来我们介绍下spring官方推荐的热加载方案,即spring boot devtools。热加载技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结108首先我们需要思考下,为什么重新启动一个应用会比较慢,那是因为在启动应用的时候,JVM虚拟机需要将所有的应用程序重新装载到整个虚拟机。可想而知,一个复杂的应用程序所包含的jar包可能有上百兆,每次微小的改动都是全量加载,那自然是很慢了。那么我们是否可以做到,当我们修改了某个文件后,

271、在JVM中替换到这个文件相关的部分而不全量的重新加载呢?而spring boot devtools正是基于这个思路进行处理的。如上图所示,通常一个项目的代码由以上四部分组成,即基础类、扩展类、二方包/三方包、以及我们自己编写的业务代码组成。上面的一排是我们通常的类加载结构,其中业务代码和二方包/三方包是由应用加载器加载的。而实际开发和调试的过程中,主要变化的是业务代码,并且业务代码相对二方包/三方包的内容来说会更少一些。因此我们可以将业务代码单独通过一个自定义的加载器Custom Classloader来进行加载,当监控发现业务代码发生改变后,我们重新加载启动,老的业务代码的相关类则由虚拟机的

272、垃圾回收机制来自动回收。其工程流程大概如下。有兴趣的同学可以去看下源码,会更加清楚。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结109RestartClassLoader为自定义的类加载器,其核心是loadClass的加载方式,我们发现其通过修改了双亲委托机制,默认优先从自己加载,如果自己没有加载到,从从parent进行加载。这样保证了业务代码可以优先被RestartClassLoader加载。进而通过重新加载RestartClassLoader即可完成应用代码部分的重新加载。热部署本质其实与热加载并没有太大的区别,通常我们说热加载是指在开发环境中进行的c

273、lassloader加载,而热部署则更多是指在线上环境使用classloader的加载机制完成业务的部署。所以这二者使用的技术并没有本质的区别。那热部署除了与热加载具有发布更快之外,还有更多的更大的优势就是具有更细的发布粒度。我们可以想像以下的一个业务场景。热部署技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结110假设某个营销投放平台涉及到4个业务方的开发,需要对会场业务进行投放。而这四个业务方的代码全部都在一个应用里面。因此某个业务方有代码变更则需要对整个应用进行发布,同时其它业务方也需要跟着回归。因此每个微小的发动,则需要走整个应用的全量发布。这种方式带

274、来的稳定性风险估且不说,整个发布迭代的效率也可想而知了。这在整个互联网里,时间和效率就是金钱的理念下,显然是无法接受的。那么我们完全可以通过类加载机制,将每个业务方通过一个classloader来加载。基于类的隔离机制,可以保障各个业务方的代码不会相互影响,同时也可以做到各个业务方进行独立的发布。其实在移动客户端,每个应用模块也可以基于类加载,实现插件化发布。本质上也是一个原理。在阿里内部像阿拉丁投放平台,以及crossbow容器化平台,本质都是使用classloader的热加载技术,实现业务细粒度的开发部署以及多应用的合并部署。加密保护众所周期,基于java开发编译产生的jar包是由.cla

275、ss字节码组成,由于字节码的文件格式是有明确规范的。因此对于字节码进行反编译,就很容易知道其源码实现了。因此大致会存在如下两个方面的诉求。例如在服务端,我们向别人提供三方包实现的时候,不希望别人知道核心代码实现,我们可以考虑对jar包进行加密,在客户端则会比较普遍,那就是我们打包好的apk的安装包,不希望被人家反编译而被人家翻个底朝天,我们也可以对apk进行加密。jar包加密的本质,还是对字节码文件进行操作。但是JVM虚拟机加载class的规范是统一的,因此我们在最终加载class文件的时候,还是需要满足其class文件的格式规范,否则虚拟机是不能正常加载的。因此我们可以在打包的时候对clas

276、s进行正向的加密操作,然后,在加载class文件之前通过自定义classloader先进行反向的解密操作,然后再按照标准的class文件标准进行加载,这样就完成了class文件正常的加载。因此这个加密的jar包只有能够实现解密方法的classloader才能正常加载。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结111这样整个jar包的安全性就有一定程度的提高,至于更高安全的保障则取决于加密算法的安全性了以及如何保障加密算法的密钥不被泄露的问题了。这有种套娃的感觉,所谓安全基本都是相对的。并且这些方法也不是绝对的,例如可以通过对classloader进行插码

277、,对解密后的class文件进行存储;另外大多数JVM本身并不安全,还可以修改JVM,从ClassLoader之外获取解密后的代码并保存到磁盘,从而绕过上述加密所做的一切工作,当然这些操作的成本就比单纯的class反编译就高很多了。所以说安全保障只要做到使对方破解的成本高于收益即是安全,所以一定程度的安全性,足以减少很多低成本的攻击了。我们可以贴一下简单的实现方案:本文对classloader的加载过程和加载原理进行了介绍,并结合类加载机制的特征,介绍了其相应的使用场景。由于篇幅限制,并没有对每种场景的具体实现细节进行介绍,而只是阐述了其基本实现思路。或许大家觉得classloader的应用有些

278、复杂,但事实上只要大家对class从哪里加载,搞清楚loadClass的机制,就已经成功了一大半。正所谓万变不离其宗,抓住了本质,其它问题也就迎刃而解了。案例简介技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结112如何在业务逻辑当中优雅引入重试机制我们首先看看正常的业务系统交互流程,就像下面图中所示一样,我们自己开发的系统通过HTTP接口或者通过RPC去访问其他业务系统,其他系统在没出现任何问题的情况下会返回给我们需要的数据,状态为success。但大家在日常的开发工作当中应该碰到过不少这样的问题:自己应用因为业务需求需要调其他关联应用的接口或二方包,而其他

279、应用的接口稳定性不敢过分恭维,老是出一些莫名奇妙的幺蛾子,比如由于接口暂时升级维护导致的短暂不可用,又或者网络抖动因素导致的单次接口请求失败。作者:殷浩出品:淘系技术为什么要引入重试机制诸如此类的麻烦问题会因为业务强依赖致使我们自己维护的系统也跟着陷入一种不稳定的状态(当然这个强依赖是没有办法的事情,毕竟业务之间需要解耦独立开发维护)。所以也就是说重试的使用场景大多是因为我们的系统依赖了其他的业务,或者是由于我们自己的业务需要通过网络请求去获取数据这样的场景。既然一次请求结果的状态非常不可控、不稳定,那么一个非常自然的想法就是多试几次,就能很好的避开网络抖动或其他关联应用暂时down机维护带来

280、的系统不可用问题。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结113当然,这里也有几个引入重试机制以后需要考虑的问题。我们应该重试几次?每次重试的间隔设置为多少合适?如果所有重试机会都用完了还是不成功怎么办?下面我们就这几个问题展开分析一下。通常来说我们单次重试所面临的情况就如上面我们分析的一样,有很大的不可确定性,那到底多少次是比较合理的次数呢?这个就要“具体业务具体分析”了,但一般来说3次重试就差不多可以满足大多数业务需求了,当然,这是需要结合后面要说的重试间隔一起讨论的。为什么说3次就基本够了呢,因为如果被请求系统实在处于长时间不可用状态。我们重试多

281、次是没有什么意义的。重试几次合适技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结114如果重试间隔设置得太小,可能被调用系统还没来得及恢复过来我们就又发起调用,得到的结果肯定还是Fail;如果设置的太大,我们自己的系统就会牺牲掉不少数据时效性。所以,重试间隔也要根据被调用的系统平均恢复时间去正确估量,通常而言这个平均恢复时间很难统计到,所以一般的经验值是3至5分钟。重试间隔设置为多少合适这种情况也是需要认真考虑的,因为不排除被调用系统真的起不来的情况,这时候就需要采取一定的补偿措施了。首先要做的就是在我们自己的系统增加错误报警机制,这样我们才能即时感知到应用发

282、生了不可自恢复的调用异常。其次就是在我们的代码逻辑中加入触发手动重试的开关,这样在发生异常情况以后我们就可以方便的修改触发开关然后手动重试。在这里还有一个非常重要的问题需要考虑,那就是接口调用的幂等性问题,如果接口不是幂等的,那我们手动重试的时候就很容易发生数据错乱相关的问题。重试机会用完以后依旧Fail怎么办Spring为我们提供了原生的重试类库,我们可以方便地引入到工程当中,利用它提供的重试注解,没有太多的业务逻辑侵入性。如下,我们先引入依赖包。然后在启动类或者配置类上添加EnableRetry注解,并在需要重试的方法上添加Retryable注解。Spring重试工具包 org.sprin

283、gframework.retry spring-retry org.aspectj aspectjweaver12345678Retryablepublic String hello()long times=helloTimes.incrementAndGet();log.info(hello times:,times);if(times%4!=0)log.warn(发生异常,time:,LocalTime.now();123456技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结115更详细的用法和属性大家参阅Spring Retry的文档就好了,解释的非常清

284、楚。这里要说的一点是,Spring的重试机制也还是存在一定的不足,只支持对异常进行捕获,而无法对返回值进行校验。相比Spring Retry,Guava Retry具有更强的灵活性,可以根据返回值校验来判断是否需要进行重试。我们依然需要先引入它的依赖包。在用的时候也很简单,先创建一个Retryer实例,然后使用这个实例对需要重试的方法进行调用,可以通过很多方法来设置重试机制,比如使用retryIfException来对所有异常进行重试,使用retryIfExceptionOfType方法来设置对指定异常进行重试,使用retryIfResult来对不符合预期的返回结果进行重试,使用retryIf

285、Runtime-Exception方法来对所有RuntimeException进行重试。Guava RetryTestpublic void guavaRetry()Retryer retryer=RetryerBuilder.newBuilder().retryIfExceptionOfType(HelloRetryException.class).retryIfResult(StringUtils:isEmpty).withWaitStrategy(WaitStrategies.fixedWait(3,TimeUnit.SECONDS).withStopStrategy(StopStrat

286、egies.stopAfterAttempt(3).build();try retryer.call()-helloService.hello();catch(Exception e)e.printStackTrace();1112131415 com.github.rholder guava-retrying 2.0.012345 throw new HelloRetryException(发生Hello异常);return hello +nameService.getName();78910技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典

287、总结116上面针对我们为什么要引入重试机制,引入重试机制需要思考的几个核心问题,以及为重试机制提供良好支持的工具类库都分别作了简单介绍,相信大家在今后的开发工作中遇到类似场景也能驾轻就熟地使用思考了。我们日常工作中有很多“大”的业务场景需要我们集中精力去突破、去思考,但也有很多类似的“小”点需要我们去打穿、吃透,大家共勉。小结相比Spring,Guava Retry提供了几个核心特性。可以设置任务单次执行的时间限制,如果超时则抛出异常。可以设置重试监听器,用来执行额外的处理工作。可以设置任务阻塞策略,即可以设置当前重试完成,下次重试开始前的这段时间做什么事情。可以通过停止重试策略和等待策略结合

288、使用来设置更加灵活的策略,比如指数等待时长并最多10次调用,随机等待时长并永不停止等等。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/技术经典总结117怎么提高自己的系统设计和架构理论水平?业务系统设计的关键是在于如何定义系统的模型以及模型之间的关系,其中主要是领域模型的定义,当我们在模型确定之后,模型之间的关系也会随之明确。模型设计可以参考领域模型的经典书籍Domain-Driven Design一书,通过这个基本可以对领域定义、防腐层、贫血模型等概念有一个较为清晰的认识了。单个应用内的领域模型系统也需要注意领域分层,作为开发大家是不是见过、重构过很多Controll

289、er-Ser-vice-DAO 样式的代码分层设计?往往在在做重构的时候会令人吐血。设计较好的领域设计这里给一个分层建议:接口层 Interface:主要负责与外部系统进行交互&通信,比如一些 dubbo服务、Restful API、RMI等,这一层主要包括 Facade、DTO还有一些Assembler。应用层 Application:这一层包含的主要组件就是 Service 服务,但是要特别注意,这一层的Service不是 简单的DAO层的包装,在领域驱动设计的架构里面,Service层只是一层很“薄”的一层,它内部并不实 现任何逻辑,只是负责协调和转发、委派业务动作给更下层的领域层。领域

290、层 Domain:Domain 层是领域模型系统的核心,负责维护面向对象的领域模型,几乎全部的业务逻 辑都会在这一层实现。内部主要包含Entity(实体)、ValueObject(值对象)、Domain Event(领域 事件)和 Repository(仓储)等多种重要的领域组件。基础设施层 Infrastructure:它主要为 Interfaces、Application 和 Domain 三层提供支撑。所有与具 体平台、框架相关的实现会在 Infrastructure 中提供,避免三层特别是 Domain 层掺杂进这些实现,从 而“污染”领域模型。Infrastructure 中最常见的

291、一类设施是对象持久化的具体实现。出品:淘系技术作者:勇剑常规业务系统设计关键-领域模型在面试中是不是经常被问到一个问题:如果你系统的流量增加 N 倍你要怎么重新设计你的系统?这个高并发的问题可以从各个层面去解,比如高并发系统设计技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/后端人精彩回答118Application层代码层面:锁优化(采用无锁数据结构),主要是 concurrent 包下面的关于 AQS 锁的一些内容 数据库缓存设计(降低数据库并发争抢压力),这里又会有缓存、DB 数据不一致的问题,在实际使用 中,高并发系统和数据一致性系统采用的策略会截然相反。数据更新

292、时采用合并更新,可以在应用层去做更新合并,同一个 Container 在同一时间只会有一个 DB 更新请求。其他的比如基于 BloomFilter 的空间换时间、通过异步化降低处理时间、通过多线程并发执行等等。数据库层面:根据不同的存储诉求来进行不同的存储选型,从早期的 RDBMS,再到 NoSql(KV存储、文档数据库、全文索引引擎等等),再到最新的NewSql(TiDB、Google spanner/F1 DB)等等。表数据结构的设计,字段类型选择与区别。索引设计,需要关注聚簇索引原理与覆盖索引消除排序等,至于最左匹配原则都是烂大街的常识了,高级 一点索引消除排序的一些机制等等,B+树与B

293、树的区别。最后的常规手段:分库分表、读写分离、数据分片、热点数据拆分等等,高并发往往会做数据分桶,这里 面往深了去说又有很多,比如分桶如何初始化、路由规则、最后阶段怎么把数据合并等等,比较经典的方 式就是把桶分成一个主桶+N个分桶。架构设计层面:分布式系统为服务化 无状态化支持水平弹性扩缩容 业务逻辑层面 failfast 快速失败 调用链路热点数据前置 多级缓存设计 提前容量规划等等对于可用性要求非常高的系统,一般我们都说几个9的可用率,比如 99.999%等。面对高可用系统设计也可以从各个方面来进行分析代码层面:需要关注分布式事务问题,CAP理论是面试的常规套路软件层面:应用支持无状态化,

294、部署的多个模块完全对等,请求在任意模块处理结果完全一致=模块不存储上下文信息,只根据请求携带的参数进行处理。目的是为了快速伸缩,服务冗余。常见的比如session问题等。负载均衡问题:软件部署多份之后,如何保证系统负载?如何选择调用机器?也就是负载均衡问题技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/后端人精彩回答119业务层面的幂等设计本质上是分布式锁问题,什么是分布式锁?分布式环境下锁的全局唯一资源,使请求串行化,实际表现互斥锁,解决业务层幂等问题。常见的解决方式是基于 Redis 缓存的 setnx 方法,但作为技术人员应该清楚这其中还存在单点问题、基于超时时间无

295、法续租问题、异步主从同步问题等等,更深一点,CAP理论,一个AP系统本质上无法实现一个AP需求,即使是 RedLock 也不行。那我们如何去设计一个分布式锁呢?强一致性、服务本身要高可用是最基本的需求,其他的比如支持自动续期,自动释放机制,高度抽象接入简单,可视化、可管理等。基于存储层的可靠的解决方案比如 zookeeper:CP/ZAB/N+1可用:基于临时节点实现和Watch机制。ETCD:CP or AP/Raft/N+1可用:基于 restful API;KV存储,强一致性,高可用,数据可靠:持久 化;Client TTL 模式,需要心跳CAS 唯一凭证 uuid。上面提负载均衡的时候

296、,广义负载均衡需要完成自动重试机制,那么在业务上,我们就必须保证幂等设计。这里可以从2个层面来进行考虑:请求层面:由于请求会重试所以必须做幂等,需要保证请求重复执行和执行一次的结果完全相同。请求层 面的幂等设计需要在数据修改的层做幂等,也就是数据访问层读请求天然幂等,写请求需要做幂等。读请 求一般是天然幂等的,无论查询多少次返回的结果都是一致。这其中的本质实际上是分布式事务问题,这 里下面再详细介绍。业务层面:不幂等会造成诸如奖励多发、重复下单等非常严重的问题。业务层面的幂等本质上是分布式锁 的问题,后面会介绍。如何保证不重复下单?这里比如token机制等等。如何保证商品不超卖?比如乐观 锁等

297、。MQ消费方如何保证幂等等都是面试的常见题。狭义上的负载均衡按照类型可以分为这几种:硬件负载:比如F5等 软件负载:比如 LVS、Ngnix、HaProxy、DNS等。当然,还有代码算法上的负载均衡,比如Random、RoundRobin、ConsistentHash、加权轮训等等算 法 广义上的负载均衡可以理解为负载均衡的能力,比如一个负载均衡系统需要如下4个能力:故障机器自动发现 故障服务自动摘除(服务熔断)请求自动重试 服务恢复自动发现幂等设计问题分布式锁技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/后端人精彩回答120服务整体负载超出预设的上限,或者即将到来的流

298、量预计将会超过阀值,为了保证重要或者基本的服务能够正常运行,拒绝部分请求或者将一些不重要的不紧急的服务或任务进行服务的延迟使用或暂停使用主要的手段如下:服务层降级,主要手段:拒绝部分请求(限流),比如缓存请求队列,拒绝部分等待时间长的请求;根据Head,来拒绝非核心请 求;还有其他通用算法上的限流比如令牌桶、漏桶算法等等。关闭部分服务:比如双11大促0点会关闭逆向退款服务等等。分级降级:比如自治式服务降级,从网关到业务到DB根据拦截、业务规则逐渐降低下游请求量,体现上是 从上到下的处理能力逐渐下降。数据层降级 比如流量大的时候,更新请求只缓存到MQ,读请求读缓存,等流量小的时候,进行补齐操作(

299、一般数据访 问层如果做了降级,就没必要在数据层再做了)柔性可用策略 比如一些指定最大流量的限流工具,又或是根据CPU负载的限流工具等,需要保证自动打开,不依赖于人 工。微服务化之后,系统分布式部署,系统之间通过 RPC 通讯,整个系统发生故障的概率随着系统规模的增长而增长,一个小的故障经过链路传导放大,有可能造成更大的故障。希望在调用服务的时,在一些非关键路径服务发生服务质量下降的情况下,选择尽可能地屏蔽所造成的影响。大部分熔断返回默认值 null,也可以定制,RPCClient 原生支持最好,业务方少改代码(熔断放的地方),进入熔断时,打印熔断日志,同时返回 Exception(业务方定制熔

300、断方法),需要有服务治理平台,可以看到服务的状态、是否降级、是否熔断、可以实时下发阀值配置等。服务的熔断发布方式也是影响高可用的一个点,哈哈,以前还经历过一些线上直接停机发布的案例(银行内部系统),不过作为高大上的互联网,主要会采用这几种发布方式:灰度发布、蓝绿发布、金丝雀发布等等。发布方式引发的可用性问题服务降级技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/后端人精彩回答121一般一些金融、账务系统对这一块要求会非常严格,下面主要介绍下这里面涉及到的事务一致性、一致性算法等内容。数据一致性系统设计在 DB 层面,一般通过 刚性事务 来实现数据一致性,主要通过 预写日志

301、(WAL)的方式来实现,WAL(write ahead logging)预写日志的方式。就是所有对数据文件的修改,必须要先写日志,这样,即使在写数据的时候崩溃了,也能通过日志文件恢复,传统的数据库事务就是基于这一个机制(REDO 已提交事务的数据也求改 UNDO 未提交事务的回滚)。除了这个方式之外,还有一个就是通过 影子数据块 来进行数据备份,提前记录被修改的数据块的修改前的状态,备份起来,如果需要回滚,直接用这个备份的数据块进行覆盖就好了。其他的就是基于二阶段提交的 XA模型 了。但是目前互联网系统,已经广泛采用分布式部署模式了,传统的刚性事务无法实现,所以 柔性事务成了目前主流的分布式事

302、务解决防范,主要的模式有下面几种:TCC 模式/或者叫2阶段模式:在 try 阶段预扣除资源(但是不锁定资源,提升可用性),在Confirm 或者 Cancel 阶段进行数据提交或者回滚。一般需要引入协调者,或者叫事务管理器。SAGA模式:业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,支持向前或者向后补偿。MQ的事务消息:就是先发 halfMsg,在处理完之后,再发送 commit 或者 rollback Msg,然后 MQ 会定期询问 producer,halfMsg 能不能 commit 或者 rollback,最终实现事务的最终一致性。实际上是把补

303、偿的动作委托给了 RocketMQ。分段事物(异步确保):基于可靠消息+本地事务消息表+消息队列重试机制。目前这也是一些大厂的主流方案,内部一般称为分段事物。柔性事务基本都是基于最终一致性去实现,所以肯定会有 补偿 动作在里面,在达到最终一致性之前,对用户一般展示 软状态。需要注意的一点是,并不是所有的系统都适合引入数据一致性框架,比如用户可以随时修改自己发起的请求的情况,例如,商家设置后台系统,商户会随时修改数据,这里如果涉及到一致性的话,引入一致性框架会导致补偿动作达到最终一致性之前,资源锁会阻塞用户后续的请求。导致体验较差。这种情况下就需要通过其他手段来保障数据一致性了,比如数据对账等操

304、作。事务一致性问题技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/后端人精彩回答122从早期的 Paxos 算法,再到后面衍生的 zab 协议(参考:A simple totally ordered broadcast proto-col),提供了当下可靠的分布式锁的解决方案。再到后来的 Raft 算法(In Search of an Understandable Consensus Algorithm),也都是分布式系统设计里面需要了解到的一些知识要点。一致性算法这里简单介绍了不同系统设计的时候会面临的一些难点,基本里面每一个点,都是前人在解决各种疑难问题的道路上不断探

305、索,最终才得出的这些业界解决方案,呈现在大家眼前,作为一个技术人员,学会这些技术点只是时间问题,但这种发现问题、直面问题、再到解决问题的能力和精神才是我们最值得学习的地方,也是做为一个系统设计人员或者说是架构师的必要能力。最后,祝进步!最后技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/后端人精彩回答123公司项目并发量都特小,自己如何实际接触高并发项目?想提升高并发系统的设计和开发能力,有2个方面:一个是系统的学习相关理论;一个是找一个目标系统,不断想办法去提升他的性能。前者是后者的理论基础。如果想从事一个高并发系统开发的岗位,要学习的相关技术其实是很多的,这些技术核心

306、就是解决高并发情况下如何保持系统的高可用和低延迟。以Java工程师为例,互联网程序员面试中经常会考察的内容包括:(1)架构设计:高可用与稳定性、事务一致性、多副本一致性、CAP理论。(2)相关技术:多线程(JUC/AQS/线程池)、RPC调用及框架(如Thrift)、NIO及NIO框架(如Netty)、高并发框架(如Disruptor)、微服务框架(SpringBoot)、微服务治理(Spring Cloud)、数据库相关技术(如:索引优化、分库分表、读写分离)、分布式缓存(如redis)、消息中间件系统(如RabbitMQ)、容器技术(如docker)。(3)工具:系统性能查看(top、up

307、time、vmstat、iostat)、压测工具(如ab、locust、Jmeter、go)、线程分析(如jps、jstack)等。作者:昭明出品:淘系技术“如何设计一个好的秒杀系统“,一定是互联网大厂面试中最常问的一个问题。所以从设计一个秒杀系统开始实践,是个不错的选择。当然,一开始,我们不可能逐一把这些技能全部掌握,我们可以从一个实际项目入手,不断的把这些技术用上去,发现哪些知识不足,再去补充相关的知识。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/后端人精彩回答124 (1)瞬时并发量大 秒杀时会有大量用户在同一时间进行抢购,瞬时并发访问量突增 10 倍,甚至 1

308、00 倍以上都有。(2)库存量少 一般秒杀活动商品量很少,这就导致了只有极少量用户能成功购买到。(3)业务简单 流程比较简单,一般都是下订单、扣库存、支付订单。秒杀系统的特点:(1)限流由于活动库存量一般都是很少,对应的只有少部分用户才能秒杀成功。所以我们需要限制大部分用户流量,只准少量用户流量进入后端服务器。(2)削峰秒杀开始的那一瞬间,会有大量用户冲击进来,所以在开始时候会有一个瞬间流量峰值。如何把瞬间的流量峰值变得更平缓,是能否成功设计好秒杀系统的关键因素。实现流量削峰填谷,一般的采用缓存和 MQ 中间件来解决。(3)异步秒杀其实可以当做高并发系统来处理,在这个时候,可以考虑从业务上做兼

309、容,将同步的业务,设计成异步处理的任务,提高网站的整体可用性。(4)缓存秒杀系统的瓶颈主要体现在下订单、扣减库存流程中。在这些流程中主要用到 OLTP 的数据库,类似 MySQL、Oracle。由于数据库底层采用 B+树的储存结构,对应我们随机写入与读取的效率,相对较低。如果我们把部分业务逻辑迁移到内存的缓存或者 Redis 中,会极大的提高并发效率。从0到1搭建一个秒杀系统,也并不容易,涉及到很多前端、后端、中间件的技术。这个跟其实是所有公司的工作常态,大部分时间也是在搭架子,真正做技术优化的时间并不多,经常是在业务量突增或者大促活动来临时,集中搞一波性能优化。设计秒杀系统的关键点:技术人的

310、百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/后端人精彩回答125所以,如果没有实际的高并发项目可做,自己弄个秒杀系统自娱自乐也是不错的。搭建系统-压测-发现问题-学习知识-优化系统,通过这样的循环,相信你一定既能体验到学习的乐趣,同时实力也大幅提升。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/后端人精彩回答126为什么Go的web框架速度还不如Java?首先描述一下一个简单的 web server 的请求处理过程:出品:淘系技术作者:风弈http server 概述技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/后端人精彩回答1

311、27Net 层读取数据包后经过 HTTP Decoder 解析协议,再由 Route 找到对应的 Handler 回调,处理业务逻辑后设置相应 Response 的状态码等,然后由 HTTP Encoder 编码相应的 Response,最后由 Net 写出数据。而 Net 之下的一层由内核控制,虽然也有很多优化策略,但这里主要比较 web 框架本身,那么暂时不考虑 Net 之下的优化。看了下 techempower 提供的压测框架源码,各类框架基本上都是基于 epoll 的处理,那么各类框架的性能差距主要体现在上述这些模块的性能了。我们再看 techempower 的各项性能排名,有JSON

312、 serialization,Single query,Multiple queries,Cached queries,Fortunes,Data updates 和 Plaintext 这几大类的排名。其中 JSON serialization 是对固定的 Json 结构编码并返回(message:hello word),Single query 是单次 DB 查询,Multiple queries 是多次 DB 查询,Cached queries 是从内存数据库中获取多个对象值并以json返回,Fortunes 是页面渲染后返回,Data updates 是对 DB 的写入,Plainte

313、xt 是最简单的返回固定字符串。这里的 json 编码,DB 操作,页面渲染和固定字符串返回就是相应的业务逻辑,当业务逻辑越重(耗时越大)时,则相应的业务逻辑逐渐就成为了瓶颈,例如 DB 操作其实主要是在测试相应 DB 库和 DB 本身处理逻辑的性能,而框架本身的基础功能消耗随着业务逻辑的繁重将越来越忽略不计(Round 19 中物理机下 Plaintext 下的 QPS 在七百万级,而 Data updates 在万级别,相差百倍以上),所以这边主要分析 Json serialization 和 Plaintext两种相对能比较体现出框架本身 http 性能的排名。在 Round 19 Js

314、on serialization 中 Java 性能最高的框架是 firenio-http-lite(QPS:1,587,639),而 Go 最高的是 fasthttp-easyjson-prefork(QPS:1,336,333),按照这里面的数据是Java性能高。关于各类压测的简述技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/后端人精彩回答128从 fasthttp-easyjson-prefork 的 pprof 看除了 read 和 write 外,json(相当于 Business logic)占了 4.5%,fasthttp 自身(HTTP Decoder,

315、HTTP Encoder,Router)占了 15%,仅看 Json serialization 似乎会有一种 Java 比 Go 性能高的感觉。那我们继续把业务逻辑简化,看一下 Plaintext 的排名,Plaintext 模式其实是在使用 HTTP pipeline 模式下压测的,在 Round 19 中 Java 和 Go 已经几乎一样的 QPS 了,在 Round 19 之后的一次测试中 gnet 已经排在所有语言的第二,但是前几个框架QPS其实差别很微小。这时候其实主要瓶颈都在 net 层,而 go 官方的 net 库包含了处理 goroutine 相关的逻辑,像 gonet 之类

316、的直接操作 epoll 的会少一些这方面的消耗,Java 的 nio 也是直接操作的 epoll。拿了 gnet 的测试源码跑了下压测,看到 pprof 如下,其实这里 gnet 还有更进一步的性能优化空间:time.-Time.AppendFormat 占用 30%CPU。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/后端人精彩回答129var timetick atomic.Valuefunc NowTimeFormat()byte return timetick.Load().(byte)func tickloop()timetick.Store(nowForma

317、t()for range time.Tick(time.Second)timetick.Store(nowFormat()func nowFormat()byte return byte(time.Now().Format(Mon,02 Jan 2006 15:04:05 GMT)func init()timetick.Store(nowFormat()go tickloop()可以使用如下提前 Format,允许减少获取当前时间精度的情况下大幅减少这部分的消耗。这样优化后接下来的瓶颈在于 runtime 的内存分配,是由于这个压测代码中还存在下面的部分没有复用内存:技术人的百宝黑皮书2021

318、版淘系技术出品01年度精选技术栈内容后端篇/后端人精彩回答130分析对于这里面测试的框架,影响因素主要如下:2、zero copy 和内存复用:内部处理字节的 0 拷贝(go 官方 http 库为了减少开发者的出错概率,没有使用 zero copy,否则开发者可能在无意中引用了已经放回 buff 池内的的数据造成没有意识到的并发问题等等),而内存复用,大部分框架或多或少都已经做了。其实 gnet 本身的消耗已经做到非常小了,而 c+的 ulib 也是类似这样使用的非常简单的 HTTP 编解码操作来压测。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/后端人精彩回答1313

319、、prefork:注意到 go 框架中有使用了 prefork 进程的方式(比如 fasthttp-prefork),这是 fork 出多个子进程,共享同一个 listen fd,且每个进程使用单核但并发(1 个 P)处理的逻辑可以避免 go runtime 内部的锁竞争和 goroutine 调度的消耗(但是 go runtime 中为了并发和 goroutine 调度而存在的相关“无用”代码的消耗还是会有一些)4、语言本身的性能差异对于第一点,其实简化了各种编解码和路由之后,虽然提高了性能,但是往往会降低框架的易用性,对于一般的业务而言,不会出现如此高的QPS,同时选择框架的时候往往还需要

320、考虑易用性和可扩展性等,同时还需要考虑到公司内部原有中间件或者 SDK 所使用的框架集成复杂度。对于第二点,如果是作为一个网络代理而言,没有业务方的开发,往往可以使用真正的完全 zero copy,但是作为业务开发框架提供出去的话是需要考虑一定的业务出错概率,往往牺牲一部分性能是划算的。第三点 prefork,java netty 等是直接对于线程操作,可以更加定制化的优化性能,而 go 的 goroutine 需要的是一个通用协程,目的是降低编写并发程序的难度,在这个层次上难免性能比不上一个优化的非常出色的 Java 基于线程操作的框架;但是直接操作线程的话需要合理控制好线程数,这是个比较头

321、疼的调优问题(特别是对于新手来说),而 goroutine 则可以不关心池子的大小,使得代码更加优雅和简洁,这对于工程质量保障其实是一个提升。另外这里存在 prefork 是由于 go 没法直接操作线程,而 fasthttp 提供了 prefork 的能力,使用多进程方式来对标 Java 的多线程来进一步提高性能。第四点,语言本身来说 Java 还是更加的成熟,包括 JVM 的 Jit 能力也使得在热代码中和 Go 编译型语言的差异不大,何况 Go 本身的编译器还不是特别成熟,比如逃逸分析等方面的问题,Go 本身的内存模型和 GC 的成熟度也比不上 Java。还有很重要的一点,Go 的框架成熟

322、度和 Java 也不在一个级别,但相信这些都会随着时间逐步成熟。总之,对于这个框架压测数据意义在于了解性能天花板,判断继续优化的空间和ROI(投入产出比)。具体选择框架还是要根据使用场景,性能,易用性,可扩展性,稳定性以及公司内部的生态等作出选择,语言和性能分别只是其中一个因素。各种框架的应用场景不同导致其优化侧重点不同,如 spring web 为了易用性,可扩展性和稳定性而牺牲了性能,但它同样拥有庞大的社区和用户。再比如 Service Mesh Sidecar 场景下 Go 的天然并发编程上的优势,以及小内存占用,快速启动,编译型语言等特点使得比 Java 更加适合。(附:其实我使用上述

323、代码和 dockerfile 构建,并且使用同样的压测脚本,在阿里云4核独享机器测试下 go fasthttp-easyjson-prefork 框架 Json serialization 的性能要高于 Java wizzardo-http 和 firenio-http-lite 30%以上且延迟更低的,这可能和内核有关)。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容后端篇/后端人精彩回答132SIGIR2021|超越I2I和向量内积,淘宝新一代召回范式:PDN模型作者:济贤、仙基、肖荣出品:淘系技术摘要一般来说业务的推荐系统的常用的召回算法有两个范式,相似度索引范式(如I2

324、I),EBR范式(如Deep-Match)。I2I范式缺点在于对共现少的pair难以泛化,难以建模U2I部分,从而模型缺乏准确和个性化。EBR范式虽建模了U2I部分,将用户的兴趣整合成了一个向量。但却无法建模用户每一个行为和打分item之间的关系(类似于Target Attention),从而召回即缺乏多样性。为了融合两者的优点,尽可能的减少两者的缺点,我们提出了一种新的范式Path based Deep Network(PDN)。PDN模型用TriggerNet建模U2I的部分,SimNet建模I2I的部分,进而端到端的建模U2I2I。目前PDN模型已经在手淘上海品茶内容信息流场景上全量,成为线

325、上最主要的召回源,带来了20%左右的点击个数、GMV、多样性的提升。同时PDN也被SIGIR2021高分录取。背景推荐技术在淘宝中的应用是十分重要和普遍的,目的在于建立一个桥梁使得用户可以直达他们感兴趣的商品以提高用户的体验及效益转换。一般的推荐系统主要包含召回,粗排,精排和重排四大环节。由于召回环节处在整条推荐链路的最底层,决定了推荐效果的瓶颈及上限,因此本次工作主要针对有好货场景的该环节进行优化。召回环节的主要任务是高效地从整个商品池中筛选出一小部分(一般来说是千十万级)用户可能感兴趣的商品供其他环节进行筛选和排序。工业界的召回链路大致包含两类算法:相关索引召回范式,向量化召回范式(Em-

326、bedding Based Retrieval,EBR)。目前工业界,相关索引召回以Item2Item范式为主。具体做法是:Step1、离线阶段,基于一些商品相似度衡量指标(如皮尔逊相关系数)去构建倒排索引表;Step2、服务阶段,利用用户的历史行为序列直接查表进行检索。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容算法篇/技术经典总结算法篇133摘要如图1所示,我们将推荐问题归纳为基于二度图的链式预测问题:其中,N(u)表示用户交互过的商品集合。现有的大多数工作,包括i2i的协同过滤方法和双塔模型都能看作上述公式的特例。例如,基于回归的商品协同滤波方法可以被定义为:其中,定义

327、为预测用户对交互商品感兴趣程度的函数,表示交互商品与目标商品的相关性程度。因此,该方法可以看作对n条二跳路径的求和,每条路径的权重为 。此外基于向量召回的方法,例如MF,可以被定义为:Item2Item范式的优势在于:1、可以保证用户兴趣的相关性;2、行为丰富的用户召回也是多样的;3、可以捕捉用户的实时兴趣。但是存在以下四点问题:1、往往I2I的索引是基于一种共现的统计,可能出现冷门商品排不上,新品排不了的问题;2、如何即考虑I2I的共现信息,又考虑Item两端的Side Info;3、如何将这种索引的建立和多样的业务目标关联;4、如何考虑多个Trigger指向相同的一个Item的联合概率。向

328、量化召回模型(EBR)可以利用Side Info,也试图去建模用户多个行为的联合概率,因此近年受到更多的关注。简单来说,该算法分别得到用户表示和商品表示后,在服务的时候利用近邻搜索实现召回。当然,这类算法也存在不足,主要有两点,一个是这类算法仅用一个或若干个(类似于MIND)向量对用户进行表示,无法像i2i那样,逐商品细粒度的表示用户的多维兴趣;另一个是由于商品端和用户端是并行架构,难以引入目标商品与交互过商品的共现信息。总体来说,由于受到现有召回模型框架的约束,双塔模型采用了用户信息和商品Profile信息,却无法显式地利用商品共现信息。I2I索引主要利用采用了商品共现信息,但是忽略了用户和

329、商品Profile信息,且无法考虑行为序列对目标商品的综合影响。同时,由于相似度计算方法有所不同,线上往往有多种I2I索引同时工作,我们希望找到一种方法能统一这种I2I相似度,并且尽可能的解决上述提到的四点问题。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容算法篇/技术经典总结134方法为了保证召回的时候能结合个性化的用户细粒度多峰兴趣,我们基于如图1所示的二度图构建了新一代召回框架。该框架克服了之前框架无法使用所有信息的劣势,并且融入了i2i和双塔模型各自的优势实现统一的优化。其中,图1包含n(历史行为序列长度)条二跳路径及1条直接路径(user&position bias)

330、。二跳路径中的第一跳表示为用户对交互商品的感兴趣程度,第二跳表示为交互商品与目标商品的相似度,因此,与双塔模型不同的是,我们细粒度的独立建模了用户的多峰兴趣(每个交互商品建立一个兴趣表示路径),解决了单一向量难以表达多维兴趣的问题。直接路径表示了用户对目标商品的直观喜爱程度,例如女生可能对服饰更感兴趣,男生可能其中,qi、pu、pj分别表示目标商品,用户信息和交互商品的特征向量。MF可以看作是对二度图的n+1条路径进行求和,具体来说,qi、pu表示直接路径的权重,表示二跳路径的权重。同样的,MF的深度化版本YotubeDNN可以被定义为:已有召回方法受到召回效率、模型结构的限制,难以使用到图中

331、的所有信息。例如,I2I范式缺少了用户信息和商品信息,EBR范式没有显式地建模商品共现信息。因此,我们提出了一种新型框架path-based deep network(PDN),来合理使用所有信息以实现低时延的个性化用户多峰兴趣召回。其中第一跳表示用户对交互商品的喜爱程度,第二跳表示交互商品与目标商品的相似程度。其中,zu表示用户u的用户信息(id,性别等),表示用户交互过的n个商品的商品信息(id,类目等),xi表示目标商品的商品信息,表示用户对第k个交互商品的行为信息(停留时长,购买次数等),表示第k个交互商品和目标商品的相关性信息(共现次数等),边的粗细表示该条边的权重大小。图1:将用户

332、对目标商品的喜爱程度解耦成二度图技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容算法篇/技术经典总结135PDN整体概述图2展示了我们所提出的召回框架PDN,主要包含Embedding Layer,Trigger Net(TrigNet),Similarity Net(SimNet),Direct&Bias Net 四个模块。PDN的前向过程可以概括为:对电子产品更感兴趣。具体来说,对于n条二跳路径,我们的框架(1)基于用户信息,行为信息和交互商品信息,采用一个TriggerNet建模用户对每一个交互过商品的喜爱程度,最终得到一个变长的用户表示向量(维度为1n),其中,第k个维度

333、表示用户对第k个交互商品的喜爱程度;(2)基于交互商品与目标商品的信息,相关性信息,采用Similarity Net建模交互商品与目标商品的相似度,最终得到一个变长的目标商品表示向量,其中,第k维表示第k个交互商品和目标商品的相似度。最后综合n+1条路径的权重,预测最后对目标商品的喜爱程度。技术人的百宝黑皮书2021版淘系技术出品01年度精选技术栈内容算法篇/技术经典总结136Embedding Layer如图1所示,PDN主要使用四类特征,包括用户信息zu,商品信息x,行为信息 以及商品相关性信息 。PDN通过Embedding Layer将其转化为编码向量:Trigger Net&Similarity Net经过编码层后,PDN计算用户与目标商品间的每条二跳路径。对于第一跳,PDN利用TrigNet计算用户对每个交互商品的喜爱程度来补获用户的多峰兴趣。具体来说,给定用户u及他的交互商品

友情提示

1、下载报告失败解决办法
2、PDF文件下载后,可能会被浏览器默认打开,此种情况可以点击浏览器菜单,保存网页到桌面,就可以正常下载了。
3、本站不支持迅雷下载,请使用电脑自带的IE浏览器,或者360浏览器、谷歌浏览器下载即可。
4、本站报告下载后的文档和图纸-无水印,预览文档经过压缩,下载后原文更清晰。

本文(淘系技术:2021技术人的百宝黑皮书(867页).pdf)为本站 (颜如玉) 主动上传,三个皮匠报告文库仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。 若此文所含内容侵犯了您的版权或隐私,请立即通知三个皮匠报告文库(点击联系客服),我们立即给予删除!

温馨提示:如果因为网速或其他原因下载失败请重新下载,重复下载不扣分。
会员购买
客服

专属顾问

商务合作

机构入驻、侵权投诉、商务合作

服务号

三个皮匠报告官方公众号

回到顶部