整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

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

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

像阿里其他技术团队以及业界的做法一样,闲鱼的大多数后端应用都是全部使用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();
 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<Long> browseCountFlow=Flowable.fromCallable(
 ()=> IdleItemBrowseService.count(itemId)
 ).onErrorReturn(t -> 0).subscribeOn(Schedulers.io());
 Flowable<Long> favorCountFlow=Flowable.fromCallable(
 ()=> IdleItemFavorService.count(itemId)
 ).onErrorReturn(t -> 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<T> catchError<T>(Future<T> future) {
 return future.catchError((e, st)=> LogUtils.error('$e $st'));
}
Future<List<T>> waitFutures<T>(List<Future<T>> futures) {
 Future<List<T>> future=Future.wait(futures.map(catchError));
 return catchError(future);
}
// 服务接口
Future<Zoo> process(Parameter param) async {
 var zoo=new Zoo();
 // 数据获取
 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://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在后端开发中更大的潜力。

作者:闲鱼技术-临耕



面主要讲了Dart的一些基础用法,今天主要讲解Dart的函数部分。

一、Dart函数概述

1、Dart是一门面向对象的语言,而且是完全面向对象的。所谓的完全面向对象就是函数也是对象,函数可以被声明成变量,函数也可以作为另外一个函数的参数使用,同时也可以像调用函数一样调用类的实例变量,函数这些特性和JavaScript中函数第一等公民很像。

2、和其他语言不太一样的是,Dart所有的函数都有返回值,如果没有指明返回值,函数返回null;会默认的拼接到函数体。

3、Dart中的函数如果只有一行表达式的,可以使用尖头语法简写

4、函数有两种参数类型:规定参数和可选参数。先列出规定参数,可选参数跟随其后。命名成可选的参数也可以被标记为规定参数。



二、函数的种类和定义

1、系统内置函数

比如print()'

2、自定义函数

自定义函数的基本格式:

返回类型 函数名称(函数参数1,函数参数2,...){

函数体

return 返回值;

}

void printData(){
  print('我是一个自定义函数');
}
printData();

3、可选参数的函数

在函数参数中用[]符号包裹可选的参数

String printUserInfo(String username,[String sex='男',int age]){  //形式参数
  if(age!=null){
    return "姓名:$username---性别:$sex--年龄:$age";
  }
  return "姓名:$username---性别:$sex--年龄保密";
}

print(printUserInfo('张三'));
print(printUserInfo('小李','女'));
print(printUserInfo('小李','女',30));

4、带默认参数的函数

可以使用=来指明参数的默认值。默认值必须是编译时常量。如果没有默认值,默认值就是null。

String printUserInfo(String username,{int age,String sex='男'}){  //形式参数
    if(age!=null){
       return "姓名:$username---性别:$sex--年龄:$age";
     }
    return "姓名:$username---性别:$sex--年龄保密";
}

print(printUserInfo('张三',age:20,sex:'未知'));

5、命名参数的函数

定义函数的时候使用{param1, param2, …}来明确参数

//函数1
func1(){
  print('func1');
 }

//函数2
func2(func){
  func();
}

//调用func2这个函数 把func1这个函数当做参数传入
func2(func1);

6、函数作为参数的函数

var fn=(){
  print('我是一个匿名函数');
};      
fn();



7、匿名函数

Dart也可以创建匿名函数,可以给一个变量赋值一个匿名函数。匿名函数看起来像一个有名称的函数零个或多个参数在圆括号中用逗号或中括号分隔,代码块在函数体后边。

list.forEach((value){
    print(value);
});

list.forEach((value)=>print(value));

list.forEach((value)=>{
  print(value)
});

8、箭头函数

((int n){
   print(n);
   print('我是自执行方法');
})(12);

9、自执行函数

var sum=1;			
func(n){
  sum*=n;
  if(n==1){
    return ;
  }         
  func(n-1);
}

func(5);      
print(sum);

10、递归函数

var sum=1;			
func(n){
  sum*=n;
  if(n==1){
    return ;
  }         
  func(n-1);
}

func(5);      
print(sum);

11、main函数

每个app必须有一个顶级的main()函数,提供程序的入口。main()函数返回void类型并且有一个list<String>类型的可选参数。

void main() {

}



三、全局变量、局部变量和闭包

Dart是一个静态作用域的语言,意味着变量的作用域是在写代码的时候就提前定义好的。可以看一个函数是否在花括号里边来看它的作用域,比如全局变量或者局部变量。

1、全局变量

特点: 全局变量常驻内存、全局变量污染全局

var data=123;

void main(){
  print(a);

  func(){
    data++;
    print(data);
  }
  func();
}

2、局部变量

特点:不常驻内存会被垃圾机制回收、不会污染全局

var data=123;

void main(){
  print(a);

  func(){
    data++;
    print(data);
  }
  func();
  func();
  
   printData(){
      var myData=123;
      myData++;
      print(myData);
   }
   printData();
   printData();
}

3、闭包

背景:

  • 常驻内存
  • 不污染全局

为了实现这个需求,产生了闭包。

闭包: 函数嵌套函数, 内部函数会调用外部函数的变量或参数, 变量或参数不会被系统回收(不会释放内存)

闭包的写法: 函数嵌套函数,并return 里面的函数,这样就形成了闭包。

Google(谷歌)公司开发的Dart语言迎来了2.5版本的更新。本次更新提供了ML Complete(由机器学习驱动的代码补全功能)和dart:ffi 外部函数接口(用来直接从 Dart 调用 C 语言代码)。

Dart 2.5

类型化编程语言的核心优势之一,就是在类型中附带的信息使得 IDE / 编辑器能够在键入代码时提供强大的代码补全功能,从而帮助开发者提高效率。通过代码补全,开发者只需要输入代码的开头部分即可从提供的选项中进行选择,从而避免拼写错误,也便于探索各种 API。

但随着 API 数量的增长,探索 API 也变得愈发困难,因为补全功能提供的列表太长,开发者无法按照字母顺序去逐一浏览。在过去的一年里,我们一直在努力让机器学习来解决这个问题。简单地讲,我们通过分析 GitHub 上大量开源的 Dart 代码来训练一个模型,用以分析特定上下文时不同代码成员的出现模式。这个基于 TensorFlow Lite 打造的模型在被训练成型后,可以在开发者编写代码时预测接下来需要用到的代码内容。这个新功能我们称之为 ML Complete。以下是使用 Flutter 框架开发新的 MyHome widget 的示例:

使用 ML Complete 开发 Flutter widget 时的示例

  • 用于分析的大量 GitHub 开源 Dart 代码
  • https://console.cloud.google.com/marketplace/details/github/github-repos
  • TensorFlow Lite
  • https://www.tensorflow.org/lite

让我们来深入了解一下它的运行机制。假设您正在编写一个小程序来计算从当前时间开始一天后的时间。使用 ML Complete,您将获得下图这样迅捷的开发体验。

使用 ML Complete 编写代码的体验

不使用 ML Complete 编写同样代码的体验

首先,请注意 ML Complete 会根据开发者输入的变量名称 now 自动给出 DateTime.now() 的建议。当第一行输入完成后,请注意我们在开发者输入第二个变量名时,也给出了 tomorrow 这个变量名建议。最后,基于 now 这个变量给出了第二个补全建议 add(…)。而在上图的非 ML Complete 体验中,我们必须手动键入 DateTime,而且在键入 tomorrow 变量名时没有补全提示,另外 now 的 add(…) 方法在推荐列表更下面的位置才出现。

许多开发者要求我们为从 Dart 调用 C 代码提供更好的支持。一个非常明确的信号,是在 Flutter 问题反馈专区里 C 语言互操作是呼声最高的功能请求,得票数超过 600。这些功能请求背后有许多有趣的用例,包括调用低级平台 API (如 stdlib.h 或 Win32),调用现有的跨平台库以及用 C 语言编写的实用程序 (如 TensorFlow、Realm 和 SQLite) 等。

  • Flutter 功能请求列表
  • https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc
  • stdlib.h
  • https://pubs.opengroup.org/onlinepubs/009695399/basedefs/stdlib.h.html
  • Win32
  • https://en.wikipedia.org/wiki/Windows_API

目前,直接从 Dart 调用 C 的支持仅限于使用原生扩展与 Dart VM 进行深度集成。或者,Flutter 应用可以通过平台通道调用 host,并从那里来间接调用 C。这种两层的间接调用表现并不理想,我们希望提供一种新的机制,能够提供出色的性能,易于使用,并可以在许多支持 Dart 的平台和编译器上运行。

  • 原生扩展
  • https://dart.dev/server/c-interop-native-extensions
  • 平台通道
  • https://flutter.dev/docs/development/platform-integration/platform-channels
  • 支持 Dart 的平台和编译器
  • https://dart.dev/platforms

Dart-C 互操作支持两种主要场景:

  • 在操作系统 (OS) 上调用基于 C 的系统 API
  • 调用基于 C 的代码库,该代码库可以基于单个操作系统,也可以是跨平台的

我们来看看第一个互操作场景。我们将调用 Linux 命令 system,它可以执行任何系统命令; 传递给它的参数实际上是传递给了 shell/terminal,并在那里运行。这个指令的 C 语言头部如下所示:

// C header: int system(const char *command) in stdlib.h

任何互操作机制的核心挑战都是处理两种语言的语义差异。在 dart:ffi 这里,Dart 代码需要处理好两件事:

  1. C 语言函数及其参数的类型,以及返回类型
  2. 与之对应的 Dart 函数及其类型

我们通过定义两个 typedef 来做到这一点:

// C header typedef:
typedef SystemC=ffi.Int32 Function(ffi.Pointer<Utf8> command);
// Dart header typedef:
typedef SystemDart=int Function(ffi.Pointer<Utf8> command);

下面我们需要加载代码库,并查找我们要调用的函数。具体做法取决于操作系统,在下面这个例子中,我们使用的是 macOS。

// Load `stdlib`. On MacOS this is in libSystem.dylib.
final dylib=ffi.DynamicLibrary.open('/usr/lib/libSystem.dylib');
// Look up the system function.
final systemP=dylib.lookupFunction<SystemC, SystemDart>('system');

您可以在 GitHub 上找到可供所有三种操作系统 (macOS、Windows、Linux) 执行的完整示例。

  • macOS 示例
  • https://github.com/dart-lang/samples/blob/master/ffi/system-command/macos.dart
  • Windows 示例
  • https://github.com/dart-lang/samples/blob/master/ffi/system-command/windows.dart
  • Linux 示例
  • https://github.com/dart-lang/samples/blob/master/ffi/system-command/linux.dart

接下来,我们使用与特定操作系统相关的编码对字符串参数进行编码,调用该函数,并再次释放参数内存:

// Allocate a pointer to a Utf8 array containing our command.
final cmdP=Utf8.toUtf8('open http://dart.dev');
// Invoke the command.
systemP(cmdP);
// Free the pointer.
cmdP.free();

这段代码会执行系统命令,使用系统默认浏览器打开 dart.dev 网页:

通过 dart:ffi 使用系统 API 打开默认浏览器。

关注【GeekYawei】,获取更多开发相关资讯。