热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

在闲鱼,我们如何用Dart做高效后端开发?

背景像阿里其他技术团队以及业界的做法一样,闲鱼的大多数后端应用都是全部使用java来实现的。java易用、丰富的库、结构容易设计的特性决定了它是进行业务开发的最好语言之一。后端应用

背景

像阿里其他技术团队以及业界的做法一样,闲鱼的大多数后端应用都是全部使用java来实现的。java易用、丰富的库、结构容易设计的特性决定了它是进行业务开发的最好语言之一。后端应用中数据的存储、访问、转换、输出虽然都属于后端的范畴,但是其中变更的频率是不同的。通常领域对象确定之后,它的变化是很少的,但是客户端展示的变化很多,导致接口层(或者叫粘连前台和后台的胶水层)的变化非常快。大多数web应用采用统一的技术栈来实现后端,胶水层跟领域层使用统一技术,这样的做法仍然有可以优化的地方:



  • 在预发环境中验证调试比较困难:一方面,每次提交代码、构建、部署、验证的总时间相对较长;另一方面,多人共用一个部署环境,相互干扰(代码冲突和部署冲突),增加了成本。后端开发人员都渴望有一个独立、高效的开发环境,就像开发一个前端页面那样

  • 前台(java、object-c,Javascript)和后台(java)的技术不同,导致前台同学很难开发后端程序,闲鱼技术团队为了追求更高的开发效率,希望能够跨越服务端开发与客户端、前端的界限,让前台开发人员也能够写后端代码

  • 胶水层通常依赖很多后端服务,计算比较简单,是IO密集型的任务。我们理想中的编程框架是能够像写同步代码一样简单,但是享受异步的好处。目前的方案还无法完全做到这一点。


为什么选择dart

闲鱼技术团队选择使用dart作为胶水层的实现语言。



  • dart是一种静态类型语言,在编译器就能完全确定变量的类型。它是支持泛型的面向对象语言,任何变量都是对象,不存在java中的原始类型。跟Javascript类似,它是一种单线程语言,对异步的支持非常好(async/await)。dart的语法与主流开发语言(java,python,c/c++,Javascript)很类似, 在主流的语言语法基础上,dart增加了很多语法结构,getter/setter、方法级联、函数式编程、闭包,这些语法让允许开发人员更加容易地写出简洁的代码;全面易用的类库也是dart能够作为flutter开发语言的重要原因。

  • flutter证明了dart在客户端开发上的成功,闲鱼不仅走在flutter开发的前列,也正在尝试使用dart开发后端应用;语法跟Javascript,java相近,有人形容这门语言是傻瓜式的简单(stupid-simple to learn),无论是java后端开发人员,还是客户端开发同学,亦或是前端开发同学,都能够快速上手写出生产级的代码。所有技术同学都能够开发后端接口在闲鱼是可以做到的。

  • dart对异步化的良好支持对业务开发是强大助力。后端应用胶水层代码大多数IO密集型的任务,使用异步化技术可以把多个IO请求的总RT,从所有请求RT之和,降低为所有请求中最高RT。dart对异步有良好的支持,开发同学使用dart可以以近乎同步的代码风格取得异步的性能。我们以闲鱼宝贝详情页的代码举例,对比不同的编码方式。

// java同步代码
ItemDetailDO queryItemDetail(Long itemId) {
ItemDetailDO data = new ItemDetailDO(www.meiwanyule.cn  );
data.setBrowseCount(IdleItemBrowseService.count(itemId));// 多少人看过
data.setFavorCount(IdleItemFavorService.count(itemId));// 多少人点赞
return data;
}
// dart异步代码
ItemDetailDO queryItemDetail(int itemId) async {
var data = new ItemDetailDO();
await Future.wait([
IdleItemBrowseService.count(itemId).then((count) => data.browseCount = count)
.catchError((exp, stackTrace) => logError('$exp $stackTrace')),
IdleItemFavorService.count(itemId).then((count) => data.favorCount = count)
.catchError((exp, stackTrace) => logError('$exp $stackTrace'))
]);
return data;
}
// rxjava异步代码
ItemDetailDO queryItemDetail(Long itemId) {
ItemDetailDO data = new ItemDetailDO();
Flowable browseCountFlow = Flowable.fromCallable(
() => IdleItemBrowseService.count(itemId)
).onErrorReturn(t ->www.chengmingdl.com 0).subscribeOn(Schedulers.io(www.chengmyuLegw.cn));
Flowable favorCountFlow = Flowable.fromCallable(
() => IdleItemFavorService.count(itemId)
).onErrorReturn(t -www.cmyLgw.cn> 0).subscribeOn(Schedulers.io());
Flowable.zip(browseCountFlow, favorCountFlow, (browseCount, favorCount) -> {
data.setBrowseCount(browseCount);
data.setFavorCount(favorCount);
}).blockingFirst();
}

在java中我们也广泛使用RxJava这种强大的响应式扩展实施异步操作:RxJava作为java的响应式编程扩展,功能非常强大全面,它使用流的概念封装所有的异步操作。需要注意的是这里的两个服务调用都被放到一个IO线程池中运行, 这个线程池是无界的,容易消耗线程这种系统稀缺的资源。这意味着当流量非常大的时候,系统的线程池很容易被打满,需要设置合理的背压策略。
从上面的代码中可以看到“数据获取”,“数据组装”的逻辑非常清晰,不像同步代码分散在各处;相比于同步操作,dart的异步操作允许我们同时等待多个IO事件,降低总的响应时间。dart的异步代码拥有同步代码的简洁容易理解的优点,又具有异步编程的性能优势。
dart异步的原理也是容易理解的。作为单线程语言,dart依靠事件循环运行代码。dart从main函数开始执行,我们在main函数里面创建Future,相当于在一个dart内部维护的事件队列(event queue)中添加计划任务(添加的任务并不会立即执行)。main中的代码执行完之后,dart事件循环开始从事件队列中依次获取任务执行。async/await是dart的语法糖,它允许开发人员能够以书写同步代码的方式来实施异步编程(在C#、Javascript中也有类似实现)。被async修饰的方法返回一个Future,调用这样的方法,相当于创建一个Future。await一个Future,相当于把await之后的代码打包放在Future.then()的代码块里,这样就保证之后的代码在Future之后执行。由于任务存储于事件队列,dart在流量大的时候,内存消耗较大,也需要我们前期合理评估需求和分配系统资源。


dart后端开发实战

为了提高开发效率,我们利用dart的特性构建了一套高效的隔离开发环境。在业务开发实践中,我们总结出基本的开发架构和代码模式。在这些技术基础上,开发了闲鱼宝贝详情页的主干业务。下面逐一介绍。


高效的隔离开发环境

我们以往的开发场景是:提交代码 -> 代码冲突(多人共用一个部署环境) -> 构建/部署 -> 通过接口验证 -> 提交fix -> 构建/部署 -> 验证 的迭代。在这个过程中,开发人员有可能需要亲自解决代码冲突,或者依赖别人解决代码冲突,需要等待构建/部署的时间(少则5分钟,多则十几分钟)。而且这个过程可能需要迭代多次,时间成本很高,如果因为其他开发人员的代码分支的问题导致部署失败,那么等待验证的时间成倍增加。这样的开发效率显然不是特别理想。

在闲鱼的dart应用中,这种问题会得到缓解。每个开发人员使用自己独立的开发环境,开发环境使用每个人的工号唯一识别。在不需要提交代码的情况下,开发人员把代码部署到远程预发环境中,并在本地调用预发服务,查看服务的输出,做到本地验证调试的效果,极大地提高了开发效率。因为只会有开发自己单一分支的代码部署,不会牵扯到代码冲突。整个过程,部署、服务调用过程十分快速,可以在10秒内完成。验证和调试的效率非常高。

每个开发人员的独立开发环境对应预发机器上的一个isolate。dart的isolate相当于一个线程,但是不会和其他isolate共享内存,isolate之间的通信通过发送、接收消息完成。闲鱼技术团队使用每个开发人员的代码创建一个isolate,使用工号作为标识,代码可以全量替换掉运行中的isolate,也可以使用热部署增量替换掉isolate中更改的功能。整个过程非常快。在早期使用dart原生的编译器,发现速度较慢(10多秒)后,我们对dart编译器做了裁剪和优化,把编译时间从10多秒降低到几百毫秒(简单来说就是,把dart原生的编译器的附加功能,重新封装,然后通过JIT/AOT生成新的编译工具)。经过我们对dart开发环境的增强,现在开发dart胶水层接口,只需要点击开发工具上的一个按钮,就可以把修改的代码,在几秒内部署到远程的预发环境,并调用当前的开发接口,在本地查看输出。获得和在预发环境上验证一样的效果,但是体验就像在开发一个完全不依赖外部的本地应用程序。


业务开发架构

业务开发中最重要的部分是分离出变化和不变的部分,变化的部分用最灵活、快捷的方式实现(变的最多的地方当然用最快的方式处理),不变的部分使用稳定、高效的方式实现。我们已经把dart建设成为一种能够高效开发,并且适合客户端、前端、后端技术人员共同使用的技术。这种技术最适合应用于发生快速变化的接口层,也就是客户端和后端交互的地方,业务需求的变化导致这里的数据结构快速变化,也称之为胶水层。对于相对稳定的数据服务,我们使用java实现为领域服务。

上图是服务之间的交互图,实现方式如下图所示:

胶水层dart应用以HTTP协议方式作为MTOP接口提供给客户端调用,往下使用HSF从Java应用中获取数据。
通常先定义并开发好领域服务,然后再与客户端对接开发出接口,领域服务提供的接口,包含了获取基础数据的所有方法,开发好之后,很少发生变化;胶水层获取领域服务提供的数据,对数据进行加工、裁剪、组装,输出为客户端能够解析的视图数据,客户端解析、渲染、展示为页面。胶水层的代码大致可以分为:获取数据,然后数据处理和组装。抽象出代码模式如下所示:

// 数据处理和组装
void putTiger(Zoo zoo, Tiger tiger) => zoo.tiger = tiger;
void putDophin(Zoo zoo, Dophin dophin) => zoo.dophin = dophin;
void putRatel(Zoo zoo, Ratel ratel) => zoo.ratel = ratel;
// 发起多个异步请求,所有请求完成后返回所有数据
Future catchError(Future future) {
return future.catchError((e,www.wangcaigw.cn st) => LogUtils.error('$e $st'));
}
Future> waitFutures(List> futures) {
Future> future = Future.wait(futures.map(catchError));
return catchError(future);
}
// 服务接口
Future process(Parameter param) async {
var zoo = new Zoo(www.enzuo2pdL.cn);
// 数据获取
await waitFutures(
Service1.invoke(param).then((animal) -> putTiger(zoo, animal)),
Service2.invoke(param).then((animal) -> putLion(zoo, dophin)),
Service3.invoke(param).then((animal) -> putRatel(zoo, animal))
);
return finalData;
}

为了使用java的领域服务,我们首先解决了dart和java之间数据交互问题,主要是通过序列化对java类文件和dart类文件进行合理的转换,保证dart能够透明、简洁地使用java的数据结构,调用java的远程服务;在调用链路上设置全局唯一的上下文id,跨越dart和java调用栈,支持全链路排查;对所有的服务的成功率,rt和额外业务参数有详细的日志,可以配置以日志为数据源的监控告警等等(后续的文章将详细介绍我们对这些问题的详细解决方案,请持续关注哦)。


服务化详情页主干开发

闲鱼宝贝详情页是我们使用dart开发的一个重要项目。最早的闲鱼宝贝详情页把各个业务的代码逻辑耦合在一起,导致维护和变更困难,稳定性也难以保证。我们设计的swak框架(更多细节请查看文章swak框架),能够分离垂直业务的共性和差异性,把闲鱼宝贝详情页的实现分割成主干实现和垂直业务实现两块。我们使用自己开发的dart后端开发框架,对swak框架做了最小实现。项目完成了详情页主干的完整功能和基础优化:



  • 垂直业务路由:我们使用dart中的zone存储每个闲鱼商品的业务标识,代码生成的静态代理类依据业务标识调用相应的服务,在主干数据里填充各个业务的独有数据。zone是dart异步代码的执行环境,能够缓存一些可重用数据(业务代码里除非非此不可,尽量不要多用)

  • 作为远程服务的提供方:在hsfcpp对hessian协议的实现基础上做开发,dart也能成为远程服务的提供方

  • 服务调用的优化: 对java远程服务的代理做了优化,隔离业务层面对框架层的感知,做到透明调用

  • 解决缓存调用的差异性:我们依赖缓存的c++接口访问缓存,但是仍然需要处理java/c++缓存读写不兼容问题完成dart和java对同一缓存的同时读写
    项目流程图可见下图:

    ![dart-detail-flow.png](http://www.honglpt.cn gw.alicdn.com/mt/TB1Pyv6V9zqK1RjSZFjXXblCFXa-558-561.png)



实际效果

目前该项目已经上线超过6周,qps最高可达400,成功率在99.5%以上。整个调用链路的RT与同样功能的java应用持平。由于前期的精心设计,领域服务很少改动,大部分变更发生在dart胶水层。从上线后经历的若干次变更来看,dart胶水层从修改代码结束到提供给客户端使用总耗时不超过2分钟,而相同功能的java应用需要10分钟以上。


总结

dart是一门简洁、容易上手、对异步支持良好的编程语言,在flutter的开发中大放异彩。在我们的努力下,dart用于后端开发的支持逐渐完善,前台开发同学和后端开发人员快速高效地开发胶水层接口。我们在很多生产项目中使用了dart用于后端开发,性能、稳定性良好,开发效率大大提高。未来我们会着力于进一步改善dart开发体验、与java项目的兼容性、提升dart远程服务的性能,挖掘dart在后端开发中更大的潜力。


推荐阅读
  • ZSI.generate.Wsdl2PythonError: unsupported local simpleType restriction ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • flowable工作流 流程变量_信也科技工作流平台的技术实践
    1背景随着公司业务发展及内部业务流程诉求的增长,目前信息化系统不能够很好满足期望,主要体现如下:目前OA流程引擎无法满足企业特定业务流程需求,且移动端体 ... [详细]
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • YOLOv7基于自己的数据集从零构建模型完整训练、推理计算超详细教程
    本文介绍了关于人工智能、神经网络和深度学习的知识点,并提供了YOLOv7基于自己的数据集从零构建模型完整训练、推理计算的详细教程。文章还提到了郑州最低生活保障的话题。对于从事目标检测任务的人来说,YOLO是一个熟悉的模型。文章还提到了yolov4和yolov6的相关内容,以及选择模型的优化思路。 ... [详细]
  • Final关键字的含义及用法详解
    本文详细介绍了Java中final关键字的含义和用法。final关键字可以修饰非抽象类、非抽象类成员方法和变量。final类不能被继承,final类中的方法默认是final的。final方法不能被子类的方法覆盖,但可以被继承。final成员变量表示常量,只能被赋值一次,赋值后值不再改变。文章还讨论了final类和final方法的应用场景,以及使用final方法的两个原因:锁定方法防止修改和提高执行效率。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • JavaScript设计模式之策略模式(Strategy Pattern)的优势及应用
    本文介绍了JavaScript设计模式之策略模式(Strategy Pattern)的定义和优势,策略模式可以避免代码中的多重判断条件,体现了开放-封闭原则。同时,策略模式的应用可以使系统的算法重复利用,避免复制粘贴。然而,策略模式也会增加策略类的数量,违反最少知识原则,需要了解各种策略类才能更好地应用于业务中。本文还以员工年终奖的计算为例,说明了策略模式的应用场景和实现方式。 ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • Imtryingtofigureoutawaytogeneratetorrentfilesfromabucket,usingtheAWSSDKforGo.我正 ... [详细]
  • 基于Axis、XFire、CXF的webservice客户端调用示例
    本文介绍了如何使用Axis、XFire、CXF等工具来实现webservice客户端的调用,以及提供了使用Java代码进行调用的示例。示例代码中设置了服务接口类、地址,并调用了sayHello方法。 ... [详细]
  • 本文介绍了如何在Azure应用服务实例上获取.NetCore 3.0+的支持。作者分享了自己在将代码升级为使用.NET Core 3.0时遇到的问题,并提供了解决方法。文章还介绍了在部署过程中使用Kudu构建的方法,并指出了可能出现的错误。此外,还介绍了开发者应用服务计划和免费产品应用服务计划在不同地区的运行情况。最后,文章指出了当前的.NET SDK不支持目标为.NET Core 3.0的问题,并提供了解决方案。 ... [详细]
author-avatar
宝贝娜娜121_562
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有