首页 文章 精选 留言 我的

精选列表

搜索[编译原理],共10000篇文章
优秀的个人博客,低调大师

高速服务框架 HSF 的基本原理

SOA解决方案——HSF(High-speedServiceFramework)是阿里系主要采用的服务框架,其目的是作为桥梁联通不同的业务系统,解耦系统之间的实现依赖。 HSF简介 ▐背景 单体应用的主要问题是不同的业务相互纠缠在一起,面对快速发展的业务,这种开发模型和架构不利于业务发展。为了解决这个问题,需要对应用进行拆分。将不同的业务分拆到多个应用中,让不同的应用分别承担不同的功能。 在单体应用时代,某个服务想要调用其他服务在本地就可以完成,而应用拆分为多个系统后,相互之间进行通信的方式就不能依赖本地,而必须要走远程,此时,一个高效、稳定的RPC框架就变得非常重要。 随着业务的不断发展,承载不同业务的应用数量以及单个应用下的服务数量都急剧膨胀,对于服务的管理变得愈发重要,在RPC框架刚开始使用的时候,可能只有几个应用,几十个服务,如果规模扩充到上万应用,几十万个服务,RPC调用反而不是重头戏,而重要的是如何能高效的组织这些服务。 一款优秀的RPC框架一般需要(或者最好能够具备)以下服务治理能力: 服务的方便检索 ,查询服务,包括服务的提供者与消费者信息 服务的快捷测试 ,能够简单、高效的进行服务测试 服务的路由 ,根据调用的服务名等运行时信息,服务消费方能够路由到对应的服务提供方指定的机器上 服务的归组 ,能够在统一机器资源维度上,让服务提供方具备服务自动归组的能力 这些特性都已经超越了一个普通RPC框架的范畴,而提供这些能力的RPC框架才能被称之为SOA(Service-Oriented Architecture)框架。 ▐是什么 阿里SOA解决方案——HSF(High-speed Service Framework),高速服务框架。该框架是阿里系主要采用的服务框架,其目的是作为桥梁联通不同的业务系统,解耦系统之间的实现依赖。其高速体现在底层的非阻塞I/O以及优秀的序列化机制上,实现了同步和异步调用方式,并且有一套软负载体系,实现分布式应用。 ▐特性 高性能的服务调用 低侵入,HSF基于Java接口完成透明的RPC调用,用户对服务是否在本地不做感知,不侵入用户代码。 高性能,HSF提供基于非阻塞I/O上的高性能调用。 多语言,多语言支持完善,提供了C++以及Node.js客户端,支持HTTP REST调用。 大流量的场景应对 客户端负载均衡,HSF在客户端基于服务地址列表做负载均衡,不需要借助其他负载均衡设备,高效完成负载均衡工作。 多种选址策略,HSF客户端在调用时提供了多种选址策略。 上下线策略,HSF提供了优雅上下线的能力,保证服务在重启时对客户端的影响面减到最小,客户端调用在服务端重启时表现平滑。 全方位的服务治理 服务管理功能,HSF运维平台提供了服务查询、测试和Mock功能,支持用户通过服务名(一般是接口名+版本号)查询服务的提供者,或者通过输入参数对已有的服务进行调用测试。 规则管理功能,HSF运维平台支持使用归组、路由以及同机房等规则对客户端发起的调用进行干预,使客户端调用变得更加智能。 ▐基本结构 功能结构图: HSF功能结构上分为6个部分,分别是:「服务消费方」、「服务提供方」、「地址注册中心」、「持久化配置中心」、「元数据存储中心」和「HSF运维平台」(HSF 控制台),它们组合在一起可以提供全功能的分布式服务,其中服务消费方、服务提供方和地址注册中心是必需的,上述功能结构的介绍如下表: ▐调用过程 作为服务消费方,客户端线程首先会将用户的参数也就是请求对象进行序列化,将序列化之后的内容放置到请求通信对象中,请求通信对象对应的是HSF协议,它包含诸如请求Id等多个与请求对象无关的内容。请求通信对象会提交给I/O线程,在I/O线程中完成编码,最终发送到服务提供方,此时客户端线程会等待结果返回,处于等待状态。 服务提供方的I/O线程接收到二进制内容,解码后生成通信请求对象并将其递交给HSF服务端线程,在HSF服务端线程完成反序列化还原成请求对象,然后发起反射调用,得到结果,也就是响应对象。响应对象会在HSF服务端线程中完成序列化,并放置到通信响应对象中。HSF服务端线程会将通信响应对象提交给I/O线程,在I/O线程中完成编码,最终发送回服务消费方。 服务消费方收到二进制内容,在I/O线程中完成解码,生成响应通信对象,并唤醒客户端线程,客户端线程会根据响应通信对象中的内容完成反序列化,最终拿到响应对象,一次远程调用结束。、 基本使用 ▐服务接口定义 在接口定义模块中定义接口,将其打为jar包,发布到Maven仓库中。 public interface HelloWorldService { /** * 根据参数中指定的名字,生成问候语 * * @param name 被问候的姓名 * @return 问候语 */ String sayHi(String name);} ▐业务代码实现 业务模块依赖接口定义模块,实现接口,编写业务代码。 <!-- 依赖接口定义模块 --><dependency> <groupId>com.alibaba.middleware</groupId> <artifactId>hsf-guide-api</artifactId></dependency> // 实现接口public class HelloWorldServiceImpl implements HelloWorldService { @Override public String sayHi(String name) { // 编写业务代码 if (name == null || name.length() == 0) { return null; } return "Hi, " + name + "! Welcome to the HSF world."; }} ▐服务发布 API的方式 <!-- 依赖业务模块 --><dependency> <groupId>com.alibaba.middleware</groupId> <artifactId>hsf-guide-biz</artifactId></dependency><!-- 依赖HSF --><dependency> <groupId>com.taobao.hsf</groupId> <artifactId>hsf-all</artifactId></dependency> 在Main方法中将服务发布出去。 // [定义] 服务的实现Object target = new HelloWorldServiceImpl();// [设置] HSF服务发布逻辑HSFApiProviderBean hsfApiProviderBean = new HSFApiProviderBean();// [设置] 发布服务的接口hsfApiProviderBean.setServiceInterface("com.alibaba.middleware.hsf.guide.api.service.HelloWorldService");// [设置] 服务的实现对象hsfApiProviderBean.setTarget(target);// [设置] 服务的版本hsfApiProviderBean.setServiceVersion("1.0.0");// [设置] 服务的归组hsfApiProviderBean.setServiceGroup("HSF");// [设置] 服务的响应时间hsfApiProviderBean.setClientTimeout(3000);// [设置] 服务传输业务对象时的序列化类型hsfApiProviderBean.setSerializeType("hessian2");// [发布] HSF服务hsfApiProviderBean.init(); 注解的方式 <!-- 依赖starter --><dependency> <groupId>com.alibaba.boot</groupId> <artifactId>pandora-hsf-spring-boot-starter</artifactId> <version>2023-04-release</version></dependency> 将@HSFProvider配置到业务模块的实现类上。 @HSFProvider(serviceInterface = HelloWorldService.class, serviceGroup = "HSF", serviceVersion = "1.0.0", clientTimeout = 3000, serializeType = "hessian2")public class HelloWorldServiceImpl implements HelloWorldService { @Override public String sayHi(String name) { if (name == null || name.length() == 0) { return null; } return "Hi, " + name + "! Welcome to the HSF world."; }} ▐服务调用 API的方式 在Main方法中调用服务端业务代码。 HSFApiConsumerBean hsfApiConsumerBean = new HSFApiConsumerBean();// [设置] 订阅服务的接口hsfApiConsumerBean.setInterfaceName("com.alibaba.middleware.hsf.guide.api.service.HelloWorldService");// [设置] 服务的版本hsfApiConsumerBean.setVersion("1.0.0");// [设置] 服务的组别hsfApiConsumerBean.setGroup("HSF");// [订阅] HSF服务,同步等待地址推送,默认false(异步),同步默认超时时间为3000mshsfApiConsumerBean.init(true);// [代理] 获取HSF代理HelloWorldService helloWorldService = (HelloWorldService) hsfApiConsumerBean.getObject();// [调用] 像调用本地接口一样,发起HSF调用String hi = helloWorldService.sayHi("松张");System.out.println(hi); 注解的方式 创 建配置类,用 @HSFConsumer 标记要调用的接口。 @Configurationpublic class HsfConfig { @HSFConsumer(serviceVersion = "1.0.0", serviceGroup = "HSF") HelloWorldService helloWorldService;} 通过注入的方式使用。 @AutowiredHelloWorldService helloWorldService; 调用方式 同步调用也可以叫阻塞调用,它将阻塞当前线程,然后执行调用,调用完毕后再继续向下进行。 HSF的IO操作都是异步的,客户端同步调用的本质是做future.get(timeout)操作,等待服务端的结果返回,这里的timeout就是客户端生效的超时时间(默认3000ms)。 HSF默认的同步调用时序图: 对于客户端来说,并不是所有的HSF服务都是需要同步等待服务端返回结果的,对于这些服务,HSF提供异步调用的形式,让客户端不必同步阻塞在HSF操作上。异步调用在发起调用时,HSF服务的调用结果都是返回类型的默认值,如返回类型是int,则会返回0,返回类型是Object,则会返回null。而真正的结果,是在HSFResponseFuture或者回调函数(callback)中获得的。 ▐Future异步调用 HSF发起调用后,用户可以在上下文中获取跟返回结果关联的HSFFuture对象,然后用户可以在任意时刻调用HSFFuture.getResponse(timeout)获取服务端的返回结果。Future异步调用时序图: API配置客户端Future异步调用。 HSFApiConsumerBean hsfApiConsumerBean = new HSFApiConsumerBean();hsfApiConsumerBean.setInterfaceName("com.alibaba.middleware.hsf.guide.api.service.HelloWorldService");hsfApiConsumerBean.setVersion("1.0.0");hsfApiConsumerBean.setGroup("HSF");// [设置] 异步future调用List<String> asyncCallMethods = new ArrayList<String>();// [格式] name:{methodName};type:futureasyncCallMethods.add("name:sayHi;type:future");hsfApiConsumerBean.setAsyncallMethods(asyncCallMethods);hsfApiConsumerBean.init(true);HelloWorldService helloWorldService = (HelloWorldService) hsfApiConsumerBean.getObject();String hi = helloWorldService.sayHi("松张");// 运行后控制台打印nullSystem.out.println(hi);// 及时在当前调用上下文中获取future对象;因为该对象是放在ThreadLocal中的,同一线程中后续调用会覆盖future对象,所以要及时取出HSFFuture hsfFuture = HSFResponseFuture.getFuture();// do something elsetry { // 这里才是真正地获取结果,如果调用还未完成,将阻塞等待结果,3000ms是等待结果的最大时间 System.out.println(hsfFuture.getResponse(3000));} catch (Throwable e) { e.printStackTrace();} ▐Callback异步调用 客户端配置为Callback方式调用时,需要配置一个实现了 HSFResponseCallback 接口的Listener,结果返回之后,HSF会调用 HSFResponseCallback 中的 onAppResponse 方法。 // 实现了HSFResponseCallback接口的Listenerpublic class MyCallbackHandler implements HSFResponseCallback { @Override public void onAppException(Throwable t) { t.printStackTrace(); } @Override public void onAppResponse(Object o) { // 取callback调用时设置的上下文 Object context = CallbackInvocationContext.getContext(); // 打印远程调用结果 + callback调用时设置的上下文 System.out.println(o.toString() + context); } @Override public void onHSFException(HSFException e) { e.printStackTrace(); }} Callback异步调用时序图: API配置客户端Callback异步调用: HSFApiConsumerBean hsfApiConsumerBean = new HSFApiConsumerBean();hsfApiConsumerBean.setInterfaceName("com.alibaba.middleware.hsf.guide.api.service.HelloWorldService");hsfApiConsumerBean.setVersion("1.0.0");hsfApiConsumerBean.setGroup("HSF");// [设置] 异步callback调用List<String> asyncCallMethods = new ArrayList<String>();// [格式] name:{methodName};type:callback;listener:{listenerFullyQualifiedName}asyncCallMethods.add("name:sayHi;type:callback;listener:com.alibaba.middleware.hsf.guide.client.handler.CallbackHandler");hsfApiConsumerBean.setAsyncallMethods(asyncCallMethods);hsfApiConsumerBean.init(true);HelloWorldService helloWorldService = (HelloWorldService) hsfApiConsumerBean.getObject();// 可选步骤,设置上下文。CallbackHandler中通过api可以获取到CallbackInvocationContext.setContext(" in callback");String hi = helloWorldService.sayHi("松张");// 运行后控制台打印nullSystem.out.println(hi);// 清理上下文CallbackInvocationContext.setContext(null);// do something else ▐泛化调用 相较于需要依赖业务模块API jar包的正常调用方式,泛化调用不需要依赖二方包,使用其特定的 GenericService 接口,传入需要调用的方法名、方法签名和参数值进行调用服务。泛化调用适用于一些网关应用(没办法依赖所有服务的二方包),其中HSF-OPS服务测试也是依赖泛化调用功能的。 API配置客户端泛化调用: HSFApiConsumerBean hsfApiConsumerBean = new HSFApiConsumerBean();hsfApiConsumerBean.setInterfaceName("com.alibaba.middleware.hsf.guide.api.service.HelloWorldService");hsfApiConsumerBean.setVersion("1.0.0");hsfApiConsumerBean.setGroup("HSF");// [设置] 泛化配置hsfApiConsumerBean.setGeneric("true");hsfApiConsumerBean.init(true);// 使用泛化接口获取代理GenericService genericHelloWorldService = (GenericService)hsfApiConsumerBean.getObject();// [调用] 发起HSF泛化调用,返回指定类型的resultString helloWorldStr = (String)genericHelloWorldService.$invoke("sayHi", // 方法入参类型数组(xxx.getClass().getName()) new String[] {String.class.getName()}, // 参数,如果是pojo,则需要转成Map new Object[] {"松张"});System.out.println(helloWorldStr); HSFConsumerBean设置generic为true,标识HSF客户端忽略加载不到接口的异常。 GenericService提供的$invoke方法包含了真实调用的方法名、入参类型和参数值,以便服务端找到该方法。由于没有依赖服务端的API jar包,传入的参数如果是自定义的DTO,需要转成客户端可以序列化的Map类型。 功能配置 ▐调用上下文 com.taobao.hsf.util.RequestCtxUtil 类中提供了基于ThreadLocal设置和获取调用上下文内容的静态方法,每次调用getXXX方法,在获取到XXX属性的值后会将该属性从ThreadLocal中remove掉,保证该属性值仅作用于当前线程的单次调用。 ▐序列化方式 为了在网络中传输数据,需要通过序列化将java对象转为byte数组,反序列化则相反。HSF支持的序列化方式有java、hessian、hessian2、json和kyro,默认使用的是hessian2。java的兼容性最好,kyro性能最强,hessian2和json比较均衡。 ▐超时配置 客户端和服务端都可以设置超时时间,客户端的优先级比服务端的高,默认的超时时间是3000ms。在设置超时时间时不仅要考虑业务执行时间,还需要加上序列化和网络通讯的时间。推荐根据业务需要为每个服务配置合适的超时时间。 不同方式设置超时时间的优先级、范围、作用域信息如下表。 即客户端优先于服务端,方法优先于接口。 ▐服务端线程池 服务端线程池是用来执行业务逻辑的线程池,默认设置如下表。 可以通过JVM启动参数和代码的方式进行配置。 ▐路由规则 HSF路由规则保存在Diamond持久化配置中心中。作用在消费者发起HSF服务调用的选址阶段,通过被客户端订阅的方式动态的更新规则内容。 规则以服务名.RULES作为DataId、服务的组别作为GroupId,采用Groovy脚本编写具体的规则内容。支持接口路由、方法路由和参数路由这三种路由方式。 一般推荐在HSF-OPS中进行路由规则的管理与配置。 一个简单的接口路由示例如下图所示: ▐归组规则 与路由规则一样,归组规则也保存在Diamond中。在HSF服务的发布期生效,将指定服务的组别修改为特定的值。这样,不同分组中的HSF服务实例就组成了以group为单位的集群,实现服务端集群的划分,从而仅针对部分客户端工作。 规则以应用名.GROUPINGRULE(JVM参数:-Dproject.name)作为DataId、「HSF」作为GroupId,采用XML格式编写具体的规则内容。 注意:在线上环境配置归组规则前要明确配置成功后产生的效果,避免出现消费者找不到服务提供方地址的问题。 一个简单的归组规则配置如下图所示: ▐同机房规则 同机房规则保存在Diamond中。作用在消费者发起HSF服务调用的选址阶段,根据机房网段信息优先选择同一个机房的服务提供方发起调用,从而减少跨机房流量的产生。 规则以服务名.RULES作为DataId、服务的组别作为GroupId,采用XML格式编写具体的规则内容。 注意: 同机房规则默认是关闭的 同机房规则是根据网段作为虚拟机房进行地址选取的 同机房规则与路由规则使用了相同的Diamond配置,如果已经配置了路由规则,在原有路由规则的基础上append即可 同机房规则配置如下图所示: 设计模式 设计模式是在软件设计中反复出现的一些问题的解决方案的经验总结。它们是一种被广泛接受的最佳实践,可用于解决特定类型的问题或完成特定类型的任务。 六大设计原则为设计模式提供理论支持,它们分别是: 单一职责原则 :应该有且仅有一个原因引起类的变更 里氏替换原则: 所有引用父类的地方必须能透明地使用其子类的对象 依赖倒置原则: 面向接口编程,依赖抽象而非细节 接口隔离原则:接口尽量细化,接口中的方法尽可能的少 迪米特法则: 一个对象应该对其他对象有最少的了解 开闭原则:对拓展开放,对修改关闭 HSF中使用了哪些经典的设计模式呢? ▐责任链模式 定义: 使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。 介绍: 想要在HSF流程发生的过程中对其进行拓展,需要实现 ProtocolInterceptor ,它的继承树如下图所示。 ProtocolInterceptor 实现了 Protocol 接口,里面只增加了一个方法 void setProtocol(Protocol protocol); ,其目的就是形成一个 Protocol 链条,这样就能将 ProtocolInterceptor 的扩展点拼装到流程链条上。 HSF提供了 AbstractDelegateProtocolInterceptor ,可以通过继承该抽象类,重写 List<ServiceURL> export(); 等方法,轻松的实现流程的扩展。 链的构建 为了链能正常运行起来,首先需要构建链。HSF是通过以下方式拼装流程这条链的: Protocol protocol = HSFServiceContainer.getInstance(Protocol.class);List<ProtocolInterceptor> handlers = HSFServiceContainer.getInstances(ProtocolInterceptor.class);//initProtocol last = protocol;for (int i = handlers.size() - 1; i >= 0; i--) { handlers.get(i).setProtocol(last); last = handlers.get(i);}return last; HSFServiceContainer.getInstances(ProtocolInterceptor.class); 会返回优先级从高到低的 xxxProtocolInterceptor (通过 @Order(int) 注解排序,值越小优先级越高),最后返回的是优先级最高的 ProtocolInterceptor 节点。 链的执行: 抽象类和具体的实现类在 export 服务导出这个场景下的执行流程如下。 public abstract class AbstractDelegateProtocolInterceptor implements ProtocolInterceptor { protected Protocol protocol; @Override public List<ServiceURL> export(ServiceMetadata serviceMetadata, InvocationHandler invocationHandler) { return protocol.export(serviceMetadata,invocationHandler); }} @Order(250)public class EagleEyeProtocolInterceptor extends AbstractDelegateProtocolInterceptor { /** * container信息(edas) */ private ContainerInfo containerInfo = HSFServiceContainer.getInstance(ContainerInfo.class); @Override public List<ServiceURL> export(ServiceMetadata serviceMetadata, InvocationHandler invocationHandler) { if (containerInfo.isSupportContainer()) { serviceMetadata.addProperty(HSFConstants.CONTAINER_ID_KEY, containerInfo.getContainerId()); } return protocol.export(serviceMetadata, invocationHandler); }} 此处具体的实现类以「EagleEye的启动阶段拦截」为例,需要在调用protocol.export(serviceMetadata, invocationHandler);之前编写业务代码。 ▐代理模式 定义: 为其他对象提供一种代理以控制对这个对象的访问。 介绍: 服务消费方使用代理模式调用服务提供方的方法,获取返回结果。 服务消费方通过代理类与服务提供方建立TCP连接,进行网络通信,将方法和入参传输给服务提供方后,服务提供方通过反射调用指定方法,得到结果,再通过网络将结果传给服务消费方。 创建代理对象: Object proxy = proxyFactory.getProxy(metadata, decorateInterfaces); public Object getProxy(ServiceMetadata metadata, Class<?>... interfacesArray) { try { JdkProxyInvocationHandler jdkProxyInvocationHandler = new JdkProxyInvocationHandler(metadata); Object instance = Proxy.newProxyInstance(metadata.getIfClazz().getClassLoader(), interfacesArray, jdkProxyInvocationHandler); jdkProxyInvocationHandler.init(instance); return instance; } catch (Throwable t) { throw new HSFException("failed to generate jdk proxy",t); }} 调用方法: public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { ApplicationModelFactory.setCurrentApplication(serviceMetadata.getApplicationModel()); ConsumerMethodModel methodModel = serviceMetadata.getConsumerServiceModel().getMethodModel(method); return InvocationUtil.invoke(methodModel, args);} ▐观察者模式 定义: 定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。 介绍: 以服务消费方监听路由规则为例,服务消费方通过RegistryProtocolInterceptor与注册中心进行交互时,会根据当前路由规则构建相应的监听器,监听路由规则的变化,保证调用服务提供方的方法时使用的是最新的路由规则。 注册监听器: public void registerListener(Object listener) { synchronized (eventBus) { if (lastRule != null) { eventBusHelp.register(listener); eventBusHelp.post(lastRule); eventBusHelp.unregister(listener); } eventBus.register(listener); }} ▐装饰模式 定义: 动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。 介绍: 服务消费方生成调用方法的代理对象的时候,会指定代理对象实现了哪些接口,这些接口都是经过装饰的。 装饰接口: /** * 使用该接口对客户端元数据进行处理,返回需要装饰的接口 */@Shared@Scope(Scope.Option.SINGLETON)public interface ProxyDecorator { /** * 用来装饰当前的调用接口 * * @param serviceMetadata 客户端元数据 * @return 装饰接口,如果不进行装饰返回null */ Class<?> decorate(ServiceMetadata serviceMetadata);} // 生成调用远程HSF服务的代理ProxyDecoratorGenerator proxyDecoratorGenerator = HSFServiceContainer.getInstance( ProxyDecoratorGenerator.class);// 获取装饰后的接口Class<?>[] decorateInterfaces = proxyDecoratorGenerator.getDecorateInterfaces(metadata);ProxyFactory proxyFactory = HSFServiceContainer.getInstance(ProxyFactory.class, metadata.getProxyStyle());Object proxy = proxyFactory.getProxy(metadata, decorateInterfaces);Method[] methods = proxyFactory.getMethods(proxy); 团队介绍 我们来自淘天集团的营销与平台策略技术。我们支撑大促 (双11、618 等)和日销业务;同时我们也直面竞对,深入参与淘宝好价、百亿补贴、聚划算等日销业务的价格心智打造。秉承“简单、开放、自驱”的技术文化,在使命与责任中互相成就,共同成长,践行让业务先赢,技术氛围浓郁。 ¤ 拓展阅读 ¤ 3DXR技术| 终端技术| 音视频技术 服务端技术|技术质量|数据算法 本文分享自微信公众号 - 大淘宝技术(AlibabaMTT)。 如有侵权,请联系 support@oschina.cn 删除。 本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

每日一博 | 理解 Mysql 索引原理及特性

作为开发人员,碰到了执行时间较长的sql时,基本上大家都会说”加个索引吧”。但是索引是什么东西,索引有哪些特性,下面和大家简单讨论一下。 1 索引如何工作,是如何加快查询速度 索引就好比书本的目录,提高数据库表数据访问速度的数据库对象。当我们的请求打过来之后,如果有目录,就会快速的定位到章节,再从章节里找到数据。如果没有目录,如大海捞针一般,难度可见一斑。这就是我们经常碰到的罪魁祸首,全表扫描。 一条索引记录中包含的基本信息包括:键值(即你定义索引时指定的所有字段的值)+逻辑指针(指向数据页或者另一索引页)。通常状况下,由于索引记录仅包含索引字段值(以及4-9字节的指针),索引实体比真实的数据行要小许多,索引页相较数据页来说要密集许多。一个索引页可以存储数量更多的索引记录,这意味着在索引中查找时在I/O上占很大的优势,理解这一点有助于从本质上了解使用索引的优势,也是大部分性能优化所需要切入的点。 1)没有索引的情况下访问数据: 2)使用平衡二叉树结构索引的情况下访问数据: 第一张图没有使用索引我们会进行顺序查找,依照数据顺序逐个进行匹配,进行了5次寻址才查询出所需数据,第二张图用了一个简单的平衡二叉树索引之后我们只用了3次,这还是数据量小的情况下,数据量大了效果更明显,所以总结来说创建索引就是为了加快数据查找速度; 2 索引的组成部分和种类 常见的索引的实现方式有很多种,比如hash、数组、树,下面为大家介绍下这几种模型使用上有什么区别 2.1 hash hash思路简单,就是把我们插入的key通过hash函数算法(以前一般是取余数,就好比hashmap的计算方式移位异或之类的),计算出对应的value,把这个value放到一个位置,这个位置叫做哈希槽。对应磁盘位置指针放入hash槽里面。一句话总结hash索引,就是存储了索引字段的hash值和数据所在磁盘文件指针。 但是不可避免的是,无论什么算法,数据量大了之后难免会出现不同的数据被放在一个hash槽里面。比如字典上的 “吴”和”武”就是同音,你查字典的时候到这里只能顺序往下去找了。索引的处理也是这样,会拉出一个链表,需要的时候顺序遍历即可。 缺点:无序索引,区间查询性能低,因为区间查询会造成多次访问磁盘,多次io耗时是很难接受的。 优点:insert迅速,只需往后补就行 场景:等值查询, 比如memcached 。不适用大量重复数据的列,避免hash冲突 总结:想成java的hashmap即可 2.2 有序数组 如果我们需要区间查询的时候,hash索引的性能就不尽如人意了。这个时候有序数组的优势就能体现出来了。 当我们需要从一个有序数组里取A和B之间的值时,只需要通过二分法定位到A的位置,时间复杂度O(log(N)),接着从A遍历到B即可,论速度的话,基本上可以说是最快的了。但是当我们需要更新的时候,需要进行的操作就很多了。如果需要插入一条数据,你需要挪动数据之后的所有数据,浪费性能。所以总结来说,只有不怎么变化的数据适合有序数组结构的索引。 缺点:insert新数据的时候,需要改变后续所有数据,成本略高。 优点:查询速度很快,理论最大值。 场景:归档查询,日志查询等极少变化的 总结:就是顺序排的数组 2.3 二叉搜索树 基本原则是树的左节点都小于父节点,右节点都大于父节点 这里我们就能看出来,二叉搜索树的查询效率原则上是O(log(N)),为了保证是平衡二叉树,更新效率也是O(log(N))。但是数据很多的情况树的高度会达到很高,过多次访问磁盘,是不可取的。并且极端情况下,树退化成链表,查询的复杂度会被拉成O(n)。 进化成多叉树,也就是多个子节点的时候,会大大的减少树的高度,降低访问磁盘。 缺点:数据量大的时候,树会过高,导致多次访问磁盘 优点:进化成多叉树,会降低树高,访问磁盘次数。 场景:适用很多场景 总结:左小右大的树 2.4 B树 在每个节点存储多个元素,在每个节点尽可能多的存储数据。每个节点可以存储1000个索引(16k/16=1000),这样就将二叉树改造成了多叉树,通过增加树的叉树,将树从高瘦变为矮胖。构建1百万条数据,树的高度只需要2层就可以(1000*1000=1百万),也就是说只需要2次磁盘IO就可以查询到数据。磁盘IO次数变少了,查询数据的效率也就提高了。 这种数据结构我们称为B树,B树是一种多叉平衡查找树 2.5 B+树 B+树和B树最主要的区别在于非叶子节点是否存储数据的问题。 B树:非叶子节点和叶子节点都会存储数据。 B+树:只有叶子节点才会存储数据,非叶子节点至存储键值。叶子节点之间使用双向指针连接,最底层的叶子节点形成了一个双向有序链表。 正是因为B+树的叶子节点是通过链表连接的,所以找到下限后能很快进行区间查询,比正常的中序遍历快 3 索引的维护 当你insert一条数据的时候,索引需要做出必要的操作来保证数据的有序型。一般自增数据直接在后面加就行了,特殊情况下如果数据加到了中间,就需要挪动后面所有的数据,这样效率比较受影响。 最糟糕的情况,如果当前的数据页(页是mysql存储的最小单位)存满了,需要申请一个新的数据页,这个过程被称为页分裂。如果造成了页分裂的话,势必会造成性能的影响。但是mysql并不是无脑的数据分裂,如果你是从中间进行数据分裂的话,对于自增主键,会导致一半的性能浪费。mysql会根据你的索引的类型,和追踪插入数据的情况决定分裂的方式,一般都存在mysql数据页的head里面,如果是零散的插入,会从中间分裂。如果是顺序插入,一般是会选择插入点开始分裂,或者插入点往后几行导致的。决定是否从中间分裂,还是从最后分裂。 如果插入的是不规则的数据,没法保证后一个值比前一个大,就会触发上面说的分裂逻辑,最后达到下面的效果 所以绝大多数情况下,我们都需要使用自增索引,除非需要业务自定义主键,最好能保证只有一个索引,且索引是唯一索引。这样可以避免回表,导致查询搜索两棵树。保证数据页的有序性,可以更好的使用索引。 4 回表 通俗的讲就是,如果索引的列在 select 所需获得的列中(因为在 mysql 中索引是根据索引列的值进行排序的,所以索引节点中存在该列中的部分值)或者根据一次索引查询就能获得记录就不需要回表,如果 select 所需获得列中有大量的非索引列,索引就需要先找到主键,再到表中找到相应的列的信息,这就叫回表。 要介绍回表自然就得介绍聚集索引和非聚集索引 InnoDB聚集索引的叶子节点存储行记录,因此, InnoDB必须要有,且只有一个聚集索引: 如果表定义了主键,则PK就是聚集索引; 如果表没有定义主键,则第一个非空唯一索引(not NULL unique)列是聚集索引; 否则,InnoDB会创建一个隐藏的row-id作为聚集索引; 当我们使用普通索引查询方式,则需要先搜索普通索引树,然后得到主键 ID后,再到 ID 索引树搜索一次。因为非主键索引的叶子节点里面,实际存的是主键的ID。这个过程虽然用了索引,但实际上底层进行了两次索引查询,这个过程就称为回表。也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。或者有高频请求时,合理建立联合索引,防止回表。 5 索引覆盖 一句话表达的话,是只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快。落实到sql上的话,只要执行计划里面的输出结果Extra字段为Using index时,能够触发索引覆盖。 常见的优化手段,就是上面提到的,将查询的字段都建到索引里面,至于dba愿不愿意让你建,那就需要你们自己battle了。 一般索引覆盖适用的场景包括 全表count查询优化、列查询回表、分页回表。高版本的mysql已经做了优化,当命中联合索引的其中一个字段,另外一个是id的时候,会自动优化,无需回表。因为二级索引的叶子上存了primary key,也算索引覆盖,无需额外成本。 6 最左匹配原则 简单来说,就是你使用 ‘xx%’的时候,符合条件的话也会使用索引。 如果是联合索引的话,我举个例子,创建一个(a,b)的联合索引 可以看到a的值是有顺序的,1,1,2,2,3,3,而b的值是没有顺序的1,2,1,4,1,2。但是我们又可发现a在等值的情况下,b值又是按顺序排列的,但是这种顺序是相对的。这是因为MySQL创建联合索引的规则是首先会对联合索引的最左边第一个字段排序,在第一个字段的排序基础上,然后在对第二个字段进行排序。所以b=2这种查询条件没有办法利用索引。举个例子,我弄一个索引, KEYidx_time_zone(time_zone,time_string) USING BTREE 执行第一条sql,全表扫描 执行第二条sql,可以看到使用了索引。 再看两条sql,建立的索引是 KEYidx_time_zone(time_zone,time_string) USING BTREE 按照正常逻辑来说,第二条sql是不符合索引字段的顺序的,应该不能使用索引才对,但是实际情况却和我们期望的不太一样,这是为啥呢? 从mysql被oracle收购以后,mysql加入了很多oracle以前的技术,高版本mysql自动优化了where条件的先后顺序。简单来说就是查询优化器做了这一步操作,sql会做预处理,那一条能更好的查询就会使用那种规则。 顺便提一下mysql的查询优化器能帮忙干的一些事 6.1 条件转化 例如where a=b and b=2,可以得到a=2,条件传递。最后的sql是 a=2 and b=2 > < = like 都可以传递 6.2 无效代码的排除 例如 where 1=1 and a=2, 1=1永远是正确的,所以最后会优化成 a=2 在比如 where 1=0 永远是false的,这样的也会被排除掉,整sql无效 或者非空字段 where a is null ,这样的也会被排除 6.3 提前计算 包含数学运算的部分,例如 where a= 1+2 会帮你算好,where a=3 6.4 存取类型 当我们评估一个条件表达式,MySQL判断该表达式的存取类型。下面是一些存取类型,按照从最优到最差的顺序进行排列: system系统表,并且是常量表 const 常量表 eq_ref unique/primary索引,并且使用的是’=’进行存取 ref 索引使用’=’进行存取 ref_or_null 索引使用’=’进行存取,并且有可能为NULL range 索引使用BETWEEN、IN、>=、LIKE等进行存取 index 索引全扫描 ALL 表全扫描 经常看执行计划的,一眼就能看出来这是啥意思,举个例子 where index_col=2 and normal_col =3 这里就会选用index_col=2 会作为驱动项。驱动项的意思是指一个sql选定他的执行计划的时候,可能有多条执行路径,一个是全表扫描,再过滤是否符合索引字段及非索引字段的值。另一种是通过索引字段,键值=2找到对应的索引树,过滤后的结果,再比较是否符合非索引字段的值。一般情况下,走索引都比全表扫描需要读取磁盘的次数少,所以称它为更好的执行路径,也就是通过索引字段,作为其驱动表达式 6.5 范围存取 简单来说,a in(1,2,3) 和 a=1 or a=2 or a=3 是一样的,between 1 and 2 和 a>1 and a<2也是一样的, 无需可以优化。 6.6 索引存取类型 避免使用相同前缀的索引,也就是一个字段不要在多个索引上有相同的前缀。比如一个字段已经建立了唯一索引,这个时候如果再给他建立一个联合索引,会导致优化器并不知道你要使用哪个索引。或者你建了前缀相同的一个单索引,一个联合索引,就算你写上了条件,也不一定能用上联合索引。当然,可以force,这就另说了。 6.7 转换 简单的表达式可以进行转换,比如 where -2 = a 会自动变成 where a= -2 ,但是如果牵扯到数学运算,就不能转换了 比如 where 2= -a 这时候不会自动转成 where a =-2. 第二条sql就可以使用索引 所以 我们在开发的过程中,需要注意sql的写法,自觉写成 where a=-2 6.8 and、union、order by、group by等 1)and and条件后,如果都没索引,扫描全表。有一个存取类型更好,见5.4 ,会使用存储类型更好的索引,如果都一样,哪个索引先创建,用哪个。 2)union union 每条语句单独优化 这里就会分别执行两条sql,用到索引,再合并结果集 3)order by order by 会过滤无效排序,比如一个字段本来就有索引 第二条sql和第一条的查询效果是一样的 所以,写sql的时候,不要写无用排序,比如order by ‘xxx’ 这样没有意义。 4)group by 简单来说 group by 的字段,有索引会走索引,group by a order by a 这里的order by等于没写,结果集已经是排序完毕的了,参考 6.8-3 order by select distinct col_a from table a 等价于 select col_a from a group by col_a 7 索引下推 主要的核心点就在于把数据筛选的过程放在了存储引擎层去处理,而不是像之前一样放到Server层去做过滤。 如果在一张表上,name和age都建立索引,查询条件为 where name like ‘xx%’ and age=11,在低版本的mysql(5.6 以下)的根据索引的最左匹配原则,可以得到放弃了age,只根据name过滤数据。根据name拿到所有的id之后,再根据id回表。 高版本mysql里,没有忽略age这个属性,带着age属性过滤,直接过滤掉了age为11的数据,假设不根据age过滤的数据为10条,过滤后只剩3条,就少了7次回表。减少了io会大量减少性能消耗 8 小表驱动大表 小表驱动大表,也是我们听惯了的话了,其含义主要是指小表的数据集驱动大表的数据集,减少连接次数。打个比方: 表A有1w数据,表B有100w数据,如果A表作为驱动表,处于循环的外层,那么只需要1w次的连接即可。如果B表在外层,那么则需要循环100w次。 下面我们实际测试看看,准备环境mysql 5.7+ 准备两张表,一张表 ib_asn_d 数据 9175, 一张表 bs_itembase_ext_attr 数据 1584115,都在商品编码字段上有索引。 首先小表驱动大表 多次反复测试,执行时间大概7秒。 接下来看看大表驱动小表。 将近300秒,不是一个量级的。 接下来分别分析执行计划,执行计划里第一条就是驱动表。 小表驱动大表,大表用了索引,小表全表扫描,只扫描8000多行 大表驱动小表,大表全表扫描,需要扫描147w行。 经过多次测试得出了结论: 当使用left join时,左表是驱动表,右表是被驱动表 ; 当使用right join时,右表是驱动表,左表是被驱动表 ; 当使用inner join时,mysql会选择数据量比较小的表作为驱动表,大表作为被驱动表 ; 驱动表索引不生效,非驱动表索引生效 保证小表是驱动表很重要。 9 总结 覆盖索引:如果查询条件使用的是普通索引(或是联合索引的最左原则字段),查询结果是联合索引的字段或是主键,不用回表操作,直接返回结果,减少IO磁盘读写读取整行数据,所以高频字段建立联合索引是很有必要的 最左前缀:联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。建立索引的时候,注意左前缀不要重复,避免查询优化器无法判定如何使用索引 索引下推:name like ‘hello%’and age >10 检索,MySQL 5.6版本之前,会对匹配的数据进行回表查询。5.6版本后,会先过滤掉age<10的数据,再进行回表查询,减少回表率,提升检索速度 作者:京东物流吴思维 来源:京东云开发者社区 转载请注明来源

优秀的个人博客,低调大师

每日一博 | Promise 规范与原理解析

摘要 Promise对象用于清晰的处理异步任务的完成,返回最终的结果值,本次分享主要介绍Promise的基本属性以及Promise内部的基础实现,能够帮我们更明确使用场景、更快速定位问题。 Promise出现的原因 首先我们先来看一段代码:异步请求的层层嵌套 function fn1(params) { const xmlHttp = new XMLHttpRequest(); xmlHttp.onreadystatechange = function(){ if (xmlHttp.readyState === 4 && xmlHttp.status === 200) { const fn1Data = {name: 'fn1'} console.log(fn1Data, 'fn1Data'); // 请求2 (function fn2() { xmlHttp.onreadystatechange = function(){ if (xmlHttp.readyState === 4 && xmlHttp.status === 200) { const fn2Data = {name: `${fn1Data.name}-fn2`} console.log(fn2Data, 'fn2Data'); // 请求3 (function fn2() { xmlHttp.onreadystatechange = function(){ if (xmlHttp.readyState === 4 && xmlHttp.status === 200) { const fn3Data = {name: `${fn2Data.name}-fn3`} console.log(fn3Data, 'fn3Data'); } } xmlHttp.open("GET","https://v0.yiketianqi.com/api?unescape=1&version=v61", true); xmlHttp.send(); })() } } xmlHttp.open("GET","https://v0.yiketianqi.com/api?unescape=1&version=v61", true); xmlHttp.send(); })() } } xmlHttp.open("GET","https://v0.yiketianqi.com/api?unescape=1&version=v61", true); xmlHttp.send(); } fn1() 或者我们可以将上面的代码优化为下面这样 function fn1(params) { console.log(`我是fn1,我在函数${params}中执行!!!`); } function fn2(params) { try { const xmlHttp = new XMLHttpRequest(); xmlHttp.onreadystatechange = function(){ if (xmlHttp.readyState === 4 && xmlHttp.status === 200) { console.log(`我是fn2,我在函数${params}中执行!!!结果是:`,params.data); fn1('fn2') } } xmlHttp.open("GET","https://v0.yiketianqi.com/api?unescape=1&version=v61", true); xmlHttp.send(); } catch (error) { console.error(error); } } function fn3() { try { const xmlHttp = new XMLHttpRequest(); xmlHttp.onreadystatechange = function(){ if (xmlHttp.readyState === 4 && xmlHttp.status === 200) { console.log('fn3请求已完成'); fn2('fn3') } } xmlHttp.open("GET","https://v0.yiketianqi.com/api?unescape=1&version=v61", true); xmlHttp.send(); console.log('我是f3函数呀'); } catch (error) { console.error(error); } } fn3() 由上面的两种写法的请求可见,在promise之前,为了进行多个异步请求并且依赖上一个异步请求的结果时,我们必须进行层层嵌套,大多数情况下,我们又对异步结果进行数据处理,这样使得我们的代码非常难看,并且难以维护,这就形成了回调地狱,由此Promise开始出现了。 回调地狱缺点 代码臃肿 可读性差 耦合性高 不好进行异常处理 Promise的基本概念 含义 ES6将其写进了语言标准里统一了用法,是一个构造函数,用来生成Promise实例 参数为一个执行器函数(执行器函数是立即执行的),该函数有两个函数作为参数,第一个参数是成功时的回调,第二个参数是失败时的回调 函数的方法有resolve(可以处理成功和失败)、reject(只处理失败)、all等方法 then、catch、finally方法为Promise实例上的方法 状态 pending --- 等待状态 Fulfilled --- 执行状态 (resolve回调函数,then) Rejected --- 拒绝状态 (reject回调函数,catch) 状态一旦改变就不会再变,状态只可能是两种改变,从pending->Fulfilled,pending->Rejected 有两个关键的属性:PromiseState --- 状态改变,PromiseResult --- 结果数据改变 const p1 = Promise.resolve(64) const p2 = Promise.reject('我错了') const p3 = Promise.then() const p4 = Promise.catch() // 状态改变PromiseState 结果改变PromiseResult console.log(new Promise(()=>{}), 'Promise'); // PromiseState='pending' PromiseResult=undefined console.log(p1,'p1'); // PromiseState='Fulfilled' PromiseResult=64 console.log(p2,'p2'); // PromiseState="Rejected" PromiseResult='我错了' console.log(p3, 'p3'); // then为实例上的方法,报错 console.log(p4, 'p4'); // catch为实例上的方法,报错  特点 1. 错误信息清晰定位:可以在外层捕获异常信息(网络错误、语法错误都可以捕获),有“冒泡”性质,会一直向后传递,直到被捕获,所以在最后写一个catch就可以了 2. 链式调用:每一个then和catch都会返回一个新的Promise,把结果传递到下一个then/catch中,因此可以进行链式调用 --- 代码简洁清晰 结果由什么决定 resolve 如果传递的参数是非Promise类型的对象,则返回的结果是成功状态的Promise对象,进入下一个then里面 如果传递的参数是Promise类型的对象,则返回的结果由返回的Promise决定,如果返回的是resolve则是成功的状态,进入下一个then里,如果返回的是reject则是失败的状态,进入下一个catch里 reject 如果传递的参数是非Promise类型的对象,则返回的结果是拒绝状态的Promise对象,进入下一个catch里面或者是下一个then的第二个参数reject回调里面 如果传递的参数是Promise类型的对象,则返回的结果由返回的Promise决定,如果返回的是resolve则是成功的状态,进入下一个then里,如果返回的是reject则是拒绝的状态,进入下一个catch里面或者是下一个then的第二个参数reject回调里面 这在我们自己封装的API里面也有体现:为什么code为1时都是then接收,其他都是catch接收,就是因为在then里面也就是resolve函数中对code码进行了判断,如果是1则返回Promise.resolve(),进入then里处理,如果是非1则返回Promise.reject(),进入catch里处理。 流程图  简单使用 // 模拟一个promise的get请求 let count = 0 function customGet(url){ count += 1 return new Promise((resolve, reject)=>{ const xmlHttp = new XMLHttpRequest(); xmlHttp.open("GET",url, true); xmlHttp.onload = ()=>{ console.log(xmlHttp, 'xmlHttp---onload'); if (xmlHttp.readyState === 4 && xmlHttp.status === 200) { console.log('customGet请求成功了'); // 返回非Promise,结果为成功状态 resolve({data:`第${count}次请求获取数据成功`}) // 返回Promise,结果由Promise决定 // resolve(Promise.reject('resolve中返回reject')) } else { reject('customGet请求错误了') } } // Promise状态改变就不会再变 // onreadystatechange方法会被执行四次 // 当地次进来的时候,readyState不等于4,执行else逻辑,执行reject,状态变为Rejected,所以即使再执行if,状态之后不会再改变 // xmlHttp.onreadystatechange = function(){ // console.log(xmlHttp,'xmlHttp---onreadystatechange') // if (xmlHttp.readyState === 4 && xmlHttp.status === 200) { // console.log('customGet请求成功了'); // resolve({data:`第${count}次请求获取数据成功`}) // } else { // reject('customGet请求错误了') // } // } xmlHttp.send(); }) } // 使用Promise,并且进行链式调用 customGet('https://v0.yiketianqi.com/api/cityall?appid=&appsecret=').then((res)=>{ console.log(res.data); return '第一次请求处理后的数据' }).then((data)=>{ console.log(data) // console.log(data.toFixed()); return customGet('https://v0.yiketianqi.com/api/cityall?appid=&appsecret=') }).then((res)=>{ console.log(res.data); }).catch((err)=>{ // 以类似'冒泡'的性质再外层捕获所有的错误 console.error(err, '这是catch里的错误信息'); }) 手写实现简单的Promise 通过上面的回顾,我们已经了解了Promise的关键属性和特点,下面我们一起来实现一个简单的Promise吧 // 1、封装一个Promise构造函数,有一个函数参数 function Promise(executor){ // 7、添加对象属性PromiseState PromiseResult this.PromiseState = 'pending' this.PromiseResult = null // 14、创建一个保存成功失败回调函数的属性 this.callback = null // 8、this指向问题 const that = this // 4、executor有两个函数参数(resolve,reject) function resolve(data){ // 10、Promise状态只能修改一次(同时记得处理reject中的状态) if(that.PromiseState !== 'pending') return // console.log(this, 'this'); // 5、修改对象的状态PromiseState that.PromiseState = 'Fulfilled' // 6、修改对象的结果PromiseResult that.PromiseResult = data // 15、异步执行then里的回调函数 if(that.callback?.onResolve){ that.callback.onResolve(that.PromiseResult) } } function reject(data){ console.log(that.PromiseState, 'that.PromiseState'); if(that.PromiseState !== 'pending') return // 9、处理失败函数状态 that.PromiseState = 'Rejected' that.PromiseResult = data console.log(that.PromiseResult, 'that.PromiseResult'); console.log(that.PromiseState, 'that.PromiseState'); // 16、异步执行then里的回调函数 if(that.callback?.onReject){ that.callback.onReject(that.PromiseResult) } } // 3、执行器函数是同步调用的,并且有两个函数参数 executor(resolve,reject) } // 2、函数的实例上有方法then Promise.prototype.then = function(onResolve,onReject){ // 20、处理onReject没有的情况 if(typeof onReject !== 'function'){ onReject = reason => { throw reason } } // 21、处理onResolve没有的情况 if(typeof onResolve !== 'function'){ onResolve = value => value } // 17、每一个then方法都返回一个新的Promise,并且把上一个then返回的结果传递出去 return new Promise((nextResolve,nextReject)=>{ // 11、处理成功或失败 if(this.PromiseState === 'Fulfilled'){ // 12、将结果传递给函数 // onResolve(this.PromiseResult) // 18、拿到上一次执行完后返回的结果,判断是不是Promise const result = onResolve(this.PromiseResult) if(result instanceof Promise){ result.then((v)=>{ nextResolve(v) },(r)=>{ nextReject(r) }) } else { nextResolve(result) } } // 当你一步步写下来的时候有没有怀疑过为什么不用else if(this.PromiseState === 'Rejected'){ // 第12步同时处理此逻辑 // onReject(this.PromiseResult) // 22、处理catch异常穿透捕获错误 try { const result = onReject(this.PromiseResult) if(result instanceof Promise){ result.then((v)=>{ nextResolve(v) }).catch((r)=>{ nextReject(r) }) } else { nextReject(result) } } catch (error) { nextReject(this.PromiseResult) } } // 13、异步任务时处理成功或失败,想办法等异步任务执行完成后才去执行这两个函数 if(this.PromiseState === 'pending'){ this.callback = { onResolve, onReject } console.log(this.callback, 'this.callback'); } }) } // 19、函数实例上有方法catch Promise.prototype.catch = function(onReject) { return this.then(null,onReject) } // 使用自定义封装的Promise const customP = new Promise((resolve,reject)=>{ // 模拟异步执行请求 // const xmlHttp = new XMLHttpRequest(); // xmlHttp.open("GET",'https://v0.yiketianqi.com/api/cityall?appid=&appsecret=', true); // xmlHttp.onload = ()=>{ // if (xmlHttp.readyState === 4 && xmlHttp.status === 200) { // resolve('success') // } else { // reject('error') // } // } // xmlHttp.send(); // 同步执行 resolve('success') // reject('error') }) console.log(customP, 'customP'); customP.then((res)=>{ console.log(res, 'resolve回调'); return '第一次回调' // return new Promise((resolve,reject)=>{ // reject('错错错') // }) },(err)=>{ console.error(err, 'reject回调'); return '2121' }).then(()=>{ console.log('then里面输出'); }).then().catch((err)=>{ console.error(err, 'catch里的错误'); }) 针对resolve中返回Promise对象时的内部执行顺序  总结 以上就是我们常用的Promise基础实现,在实现过程中对比了Promise和函数嵌套处理异步请求的优缺点,Promise仍存在缺点,但是的确方便很多,同时更清晰的理解到错误处理如何进行异常穿透的,也能帮助我们更规范的使用Promise以及快速定位问题所在。 作者:京东物流孙琦 来源:京东云开发者社区 自猿其说Tech 转载请注明来源

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册