SpringCloud05—服务容错保护:Spring Cloud Hystrix

org.springframework.cloud

spring-cloud-starter-netflix-hystrix-dashboard

2.2.6.RELEASE

com.netflix.hystrix

hystrix-javanica

1.5.18

@EnableCircuitBreaker

@MapperScan(“com.cloud.ribbonconsumer.mapper”)

@EnableDiscoveryClient

@SpringBootApplication

public class RibbonConsumerApplication {

@Bean

@LoadBalanced

RestTemplate restTemplate() {

return new RestTemplate();

}

public static void main(String[] args) {

SpringApplication.run(RibbonConsumerApplication.class, args);

}

}

  • 改造服务消费方式,新增HelloService类,注入RestTemplate实例。然后,将在ConsumerController中对RestTemplate的使用迁移到helloservice函数中,最后,在helloservice函数上增加@HysstrixCommand注解来指定回调方法:

public interface HelloService {

String getHelloService();

}

import com.cloud.ribbonconsumer.service.HelloService;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import org.springframework.web.client.RestTemplate;

@Service

public class HelloServiceImpl implements HelloService {

@Autowired

RestTemplate restTemplate;

@Override

@HystrixCommand(fallbackMethod = “helloFallBack”)

public String getHelloService() {

return restTemplate.getForEntity(“http://HELLO-SERVICE/hello”, String.class).getBody();

}

public String helloFallBack() {

return “服务中断”;

}

}

import com.cloud.ribbonconsumer.service.HelloService;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RestController;

@RestController

public class ConsumerController {

@Autowired

HelloService helloService;

@RequestMapping(value = “ribbon-consumer”, method = RequestMethod.GET)

public String helloConsumer() {

return helloService.getHelloService();

}

}

下面,我们来验证一下通过断路器实现的服务回调逻辑,重新启动之前关闭的8081端口的Hello-Service,确保此时服务注册中心、两个Hello-Service 以及 RIBBON-CONSUMER均已启动,访问http://localhost:9000/ribbon-consumer可以轮询两个HELLO-SERVICE并返回一些文字信息。

此时我们继续断开8081的HELLO-SERVICE,然后访问http://localhost:9000/ribbon-consumer,当轮询到8081服务端时,输出内容为error,不再是之前的错误内容,Hystrix的服务回调生效。

在这里插入图片描述

除了通过断开具体的服务实例来模拟某个节点无法访问的情况之外,我们还可以模拟一下服务阻塞(长时间未响应)的情况。

5.3 使用详解


在“快速入门”一节中我们已经使用过Hystrix中的核心注解@HystrixCommand,通过它创建了HystrixCommand的实现,同时利用fallback属性指定了服务降级的实现方法。然而这些还只是Hystrix使用的一小部分,在实现一个大型分布式系统时,往往还需要更多高级的配置功能。接下来我们将详细介绍Hystrix各接口和注解的使用方法。

5.3.1 创建请求命令

Hystrix命令就是我们之前所说的HystrixCommand,它用来封装具体的依赖服务调用逻辑。

我们可以通过继承的方式来实现,比如:

import com.cloud.ribbonconsumer.po.User;

import com.netflix.hystrix.HystrixCommand;

import org.springframework.web.client.RestTemplate;

public class UserCommand extends HystrixCommand {

private RestTemplate restTemplate;

private Long id;

public UserCommand(Setter setter, RestTemplate restTemplate, Long id) {

super(setter);

this.restTemplate = restTemplate;

this.id = id;

}

@Override

protected User run() throws Exception {

return restTemplate.getForObject(“http://HELLO-SERVICE/getUsers”, User.class, id);

}

}

通过上面实现的UserCommand,我们既可以实现请求的同步也可以实现异步执行

  • 同步执行: User u=new UserCommand(restTemplate,1L).excute();

  • 异步执行: Future futureUser=new UserCommand(restTemplate,1L)。queue();

异步执行的时候可以对返回的futureUser调用get方法来获取结果。

另外,也可以通过@HystrixcCommand注解来更为优雅地实现 Hystrix命令的定义,比如:

import com.cloud.ribbonconsumer.po.User;

import com.cloud.ribbonconsumer.service.HelloService;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import org.springframework.web.client.RestTemplate;

@Service

public class HelloServiceImpl implements HelloService {

@Autowired

RestTemplate restTemplate;

@Override

@HystrixCommand

public User getUserById(Long id) {

return restTemplate.getForObject(“http://HELLO-SERVICE/getUsers/{1}”, User.class, id);

}

}

虽然@HystrixCommand注解可以非常优雅地定义Hystrix 命令的实现,但是如上定义的getUserById方式只是同步执行的实现,若要实现异步执行则还需另外定义,比如:

@Override

@HystrixCommand

public Future getUserByIdAsync(String id) {

return new AsyncResult(){

@Override

public User invoke(){

return restTemplate.getForObject(“http://HELLO-SERVICE/getUsers/{1}”, User.class, id);

}

};

}

5.4 定义服务降级


fallback是 Hystrix命令执行失败时使用的后备方法,用来实现服务的降级处理逻辑。在HystrixCommand中可以通过重载getFallback ()方法来实现服务降级逻辑, Hystrix会在run ()执行过程中出现错误、超时、线程池

拒绝、断路器熔断等情况时,执行getFallback ()方法内的逻辑,比如我们可以用如下方式实现服务降级逻辑:

  • 1.继承HystrixCommand

import com.cloud.ribbonconsumer.po.User;

import com.netflix.hystrix.HystrixCommand;

import org.springframework.web.client.RestTemplate;

public class UserCommand extends HystrixCommand {

private RestTemplate restTemplate;

private Long id;

public UserCommand(Setter setter, RestTemplate restTemplate, Long id) {

super(setter);

this.restTemplate = restTemplate;

this.id = id;

}

@Override

protected User run() throws Exception {

return restTemplate.getForObject(“http://HELLO-SERVICE/getUsers/{1}”, User.class, id);

}

@Override

protected User getFallback() {

return new User();

}

}

  • 2.在 HystrixObservableCommand实现的Hystrix命令中,我们可以通过重载resumewithFallback 方法来实现服务降级逻辑。该方法会返回一个Observable对象,当命令执行失败的时候,Hystrix 会将observable中的结果通知给所有的订阅者。

import com.cloud.ribbonconsumer.po.User;

import com.cloud.ribbonconsumer.service.HelloService;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import org.springframework.web.client.RestTemplate;

@Service

public class HelloServiceImpl implements HelloService {

@Autowired

RestTemplate restTemplate;

@Override

@HystrixCommand(fallbackMethod = “defaultUser”)

public User getUserById(Long id) {

return restTemplate.getForObject(“http://HELLO-SERVICE/getUsers/{1}”, User.class, id);

}

public User defaultUser() {

return new User();

}

}

在使用注解来定义服务降级逻辑时,我们需要将具体的Hystrix命令与fallback实现函数定义在同一个类中,并且fallbackMethod的值必须与实现fallback方法的名字相同。由于必须定义在一个类中,所以对于fallback 的访问修饰符没有特定的要求,定义为private、protected、public均可。

在实际使用时,我们需要为大多数执行过程中可能会失败的Hystrix命令实现服务降级逻辑,但是也有一些情况可以不去实现降级逻辑,如下所示。

  • 执行写操作的命令:当Hystrix命令是用来执行写操作而不是返回一些信息的时候,通常情况下这类操作的返回类型是void或是为空的Observable,实现服务降级的意义不是很大。当写入操作失败的时候,我们通常只需要通知调用者即可。

  • 执行批处理或离线计算的命令:当Hystrix命令是用来执行批处理程序生成一份报告或是进行任何类型的离线计算时,那么通常这些操作只需要将错误传播给调用者,然后让调用者稍后重试而不是发送给调用者一个静默的降级处理响应。

不论Hystrix命令是否实现了服务降级,命令状态和断路器状态都会更新,并且我们可以由此了解到命令执行的失败情况。

5.5 异常处理


5.5.1 异常传播

在HystrixCommand实现的run()方法中抛出异常时,除了HystrixBadRequest-Exception之外,其他异常均会被ystrix认为命令执行失败并触发服务降级的处理逻辑,所以当需要在命令执行中抛出不触发服务降级的异常时来使用它。

而在使用注册配置实现Hystrix命令时,它还支持忽略指定异常类型功能,只需要通过

设置@HystrixCommand注解的ignoreExceptions参数,比如:

@HystrixCommand(ignoreExceptions = {HystrixBadRequestException.class})

public User getUserById(Long id) {

return restTemplate.getForObject(“http://HELLO-SERVICE/getUsers/{1}”, User.class, id);

}

如上面代码的定义,当getUserById方法抛出了类型为BadRequestException的异常时,Hystrix会将它包装在Hystri xBadRequestException中抛出,这样就不会触发后续的fallback 逻辑。

5.5.2 异常获取

使用注解配置方式可以很简单的实现异常的获取。只需要在fallback实现方法的参数中增加Throwable e对象的定义,这样在方法内部就可以获取触发服务降级的具体异常内容了,比如:

@HystrixCommand(fallbackMethod = “fallback1”)

public String getUserInfo() {

throw new RuntimeException(“getUserInfo command failed”);

}

String fallback1(Throwable e) {

assert “getUserInfo command failed”.equals(e.getMessage()) : “降级服务仍然异常”;

return null;

}

5.6 命令名称、分组以及线程池的划分


5.6.1 通过继承HystrixCommand来实现命令的设置,分组以及线程池的划分

public class UserCommand extends HystrixCommand {

private RestTemplate restTemplate;

private Long id;

public UserCommand() {

super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(“CommandGroupKey”))

.andCommandKey(HystrixCommandKey.Factory.asKey(“CommandKey”))

.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(“ThreadPoolKey”)));

}

}

从上面Setter的使用中可以看到,我们并没有直接设置命令名称,而是先调用了withGroupKey来设置命令组名,然后才通过调用andCommandKey来设置命令名

这是因为在Setter的定义中,只有withGroupKey静态函数可以创建Setter的实例,所以GroupKey是每个Setter必需的参数,而CommandKey则是-一个 可选参数。

通过设置命令组,Hystrix会根据组来组织和统计命令的告警、仪表盘等信息。那么为什么一定要设置命令组呢?

因为除了根据组能实现统计之外,Hystrix命令默认的线程划分也是根据命令分组来实现的。默认情况下,Hystrix会让相同组名的命令使用同一个线程池,所以我们需要在创建Hystrix命令时为其指定命令组名来实现默认的线程池划分。如果Hystrix的线程池分配仅仅依靠命令组来划分,那么它就显得不够灵活了,所以Hystrix还提供了HystrixThreadPoolKey 来对线程池进行设置,通过它我们可以实现更细粒度的线程池划分

如果在没有特别指定HystrixThreadPoolKey的情况下,依然会使用命令组的方式来划分线程池。通常情况下,尽量通过HystrixThreadPoolKey的方式来指定线程池的划分,而不是通过组名的默认方式实现划分,因为多个不同的命令可能从业务逻辑上来看属于同一个组,但是往往从实现本身上需要跟其他命令进行隔离。

5.6.2 使用注解设置命令,分组以及对线程池的划分

@HystrixCommand(commandKey = “getUdserInfo”, groupKey = “UserGroup”, threadPoolKey = “getUserInfoThread”, fallbackMethod = “fallback1”)

public String getUserInfo() {

throw new RuntimeException(“getUserInfo command failed”);

}

5.7 请求缓存


当系统用户不断增长时,每个微服务需要承受的并发压力也越来越大。在分布式环境下,通常压力来自于对依赖服务的调用,因为请求依赖服务的资源需要通过通信来实现,这样的依赖方式比起进程内的调用方式会引起一部分的性能损失,同时HTTP相比于其他高性能的通信协议在速度上没有任何优势,所以它有些类似于对数据库这样的外部资源进行读写操作,在高并发的情况下可能会成为系统的瓶颈。

既然如此,我们很容易地可以联想到,类似数据访问的缓存保护是否也可以应用到依赖服务的调用上呢?

答案显而易见,在高并发的场景之下,Hystrix中提供了请求缓存的功能,我们可以方便地开启和使用请求缓存来优化系统,达到减轻高并发时的请求线程消耗、降低请求响应时间的效果。

5.7.1 开启请求缓存的功能

Hystrix请求缓存的使用非常简单,我们只需要在实现HystrixCommand或HystrixObservableCommand时,通过重载getCacheKey ()方法来开启请求缓存,比如:

import com.cloud.ribbonconsumer.po.User;

import com.netflix.hystrix.HystrixCommand;

import com.netflix.hystrix.HystrixCommandGroupKey;

import com.netflix.hystrix.HystrixCommandKey;

import com.netflix.hystrix.HystrixThreadPoolKey;

import org.springframework.web.client.RestTemplate;

public class UserCommand extends HystrixCommand {

private RestTemplate restTemplate;

private Long id;

public UserCommand(Setter setter, RestTemplate restTemplate, Long id) {

super(setter);

this.restTemplate = restTemplate;

this.id = id;

}

@Override

protected User run() throws Exception {

return restTemplate.getForObject(“http://HELLO-SERVICE/getUsers/{1}”, User.class, id);

}

@Override

protected User getFallback() {

return new User();

}

@Override

protected String getCacheKey() {

//根据id置入缓存

return String.valueOf(id);

}

}

在上面的例子中,我们通过在getCacheKey方法中返回的请求缓存key值(使用了传入的获取User对象的id值),就能让该请求命令具备缓存功能。此时,当不同的外部请求处理逻辑调用了同一个依赖服务时,Hystrix 会根据getCacheKey方法返回的值来区分是否是重复的请求,如果它们的cacheKey相同,那么该依赖服务只会在第一个请求到达时被真实地调用一次,另外一个请求则是直接从请求缓存中返回结果,所以通过开启请求缓存可以让我们实现的Hystrix 命令具备下面几项好处:

  • 减少重复的请求数,降低依赖服务的并发度。

  • 在同一用户请求的上下文中,相同依赖服务的返回数据始终保持一致。

  • 请求缓存在run()和construct()执行之前生效,所以可以有效减少不必要的线程开销。

5.7.2 清理失效缓存

使用请求缓存时,如果只是读操作,那么就不需要考虑缓存中的内容是否正确的问题,但是如果请求中还有更新的写操作,那么缓存中的数据就需要我们在进行写操作时进行及时的处理以防止读操作的请求命令获取到失效的数据

在Hystrix中,我们可以通过HystrixRequestCache.clear()方法来进行缓存清理,如下所示:

import com.cloud.ribbonconsumer.po.User;

import com.netflix.hystrix.*;

import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategyDefault;

import org.springframework.web.client.RestTemplate;

public class UserGetCommand extends HystrixCommand {

private RestTemplate restTemplate;

private Long id;

private static final HystrixCommandKey GETTTER_KEY = HystrixCommandKey.Factory.asKey(“CommandKey”);

public UserGetCommand(RestTemplate restTemplate, Long id) {

super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(“GetSetGet”))

.andCommandKey(HystrixCommandKey.Factory.asKey(“GETTTER_KEY”))

.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(“ThreadPoolKey”)));

this.restTemplate = restTemplate;

this.id = id;

}

@Override

protected User run() throws Exception {

return restTemplate.getForObject(“http://HELLO-SERVICE/getUsers/{1}”, User.class, id);

}

@Override

protected User getFallback() {

return new User();

}

@Override

protected String getCacheKey() {

//根据id置入缓存

return String.valueOf(id);

}

public static void flushCache(Long id) {

//刷新缓存,根据id进行清理

HystrixRequestCache.getInstance(GETTTER_KEY, HystrixConcurrencyStrategyDefault.getInstance()).clear(String.valueOf(id));

}

}

import com.cloud.ribbonconsumer.po.User;

import com.netflix.hystrix.*;

import org.springframework.web.client.RestTemplate;

public class UserPostCommand extends HystrixCommand {

private RestTemplate restTemplate;

private User user;

private static final HystrixCommandKey CommandKey = HystrixCommandKey.Factory.asKey(“CommandKey”);

public UserPostCommand(RestTemplate restTemplate, User user) {

super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(“GetSetGet”)));

this.restTemplate = restTemplate;

this.user = user;

}

@Override

protected User run() throws Exception {

//写操作

User r = restTemplate.postForObject(“http://user-service/users”, user, User.class);

//刷新缓存,清理缓存中失效的user

UserGetCommand.flushCache(user.getId());

return r;

}

@Override

protected User getFallback() {

return new User();

}

}

该示例中主要有两个请求命令:

  • UserGetCommand 用于根据id获取User 对象

  • UserPostCommand用于更新User对象。

当我们对UserGetCommand命令实现了请求缓存之后,那么势必需要为UserPostCommand命令实现缓存的清理,以保证User被更新之后,Hystrix请求缓存中相同缓存Key的结果被移除,这样在下一次获取User的时候不会从缓存中获取到未更新的结果。

我们可以看到,在上面UserGetCommand的实现中,增加了一个静态方法flushCache,该访法通过HystrixRequestCache.getInstance (GETTER_ KEY, HystrixConcurrency-StrategyDefault. getInstance())方法从默认的Hystrix并发策略中根据GETTER_ KEY获取到该命令的请求缓存对象HystrixRequestCache的实例,然后再调用该请求缓存对象实例的clear方法,对Key为更新User的id值的缓存内容进行清理。

而在UserPostCommand 的实现中,在run 方法调用依赖服务之后,增加了对UserGetCommand中静态方法flushCache的调用,以实现对失效缓存的清理。

5.7.3 使用注解实现请求缓存

| 注解 | 描述 | 属性 |

| — | — | — |

| @CacheResult | 该注解用来标记请求命令返回的结果应该被缓存,它必须与@HystrixCommand注解结合使用 | cacheKeyMethod |

| @CacheRemove | 该注解用来让请求命令的缓存失效,失效的缓存根据定义的Key决定 | commandKey,cacheKeyMethod |

| @CacheKey | 该注解用来在请求命令的参数上标记,使其作为缓存的Key值,如果没有标注则会使用所有参数。如果同时还使用了@CacheResult和@CacheRemove注解的cacheKeyMethod方法指定缓存Key的生成,那么该注解将不会起作用 | value |

下面我们从几个方面的实例来看看这几个注解的具体使用方法。

1.设置请求缓存

通过注解为请求命令开启缓存功能非常简单,如下例所示,我们只需添加@CacheResult注解即可。当该依赖服务被调用并返回User对象时,由于该方法被@CacheResult注解修改,所以Hystrix 会将该结果置入请求缓存中,而它的缓存Key值会使用所有的参数,也就是这里Long类型的id值。

@CacheResult

@HystrixCommand

public User getUserById(Long id) {

return restTemplate.getForObject(“http://HELLO-SERVICE/getUsers/{1}”, User.class, id);

}

2.定义缓存Key

当使用注解来定义请求缓存时,若要为请求命令指定具体的缓存Key生成规则,我们可以使用@CacheResult和@CacheRemove 注解的cacheKeyMethod方法来指定具体的生成函数;也可以通过使用@CacheKey注解在方法参数中指定用于组装缓存Key的元素。

使用cacheKeyMethod方法的示例如下,它通过在请求命令的同-一个类中定义一个专门生成Key的方法,并用@CacheResult注解的cacheKeyMethod方法来指定它即可。它的配置方式类似于@HystrixCommand服务降级fallbackMethod的使用。

@CacheResult(cacheKeyMethod = “getUserByIdCacheKey”)

@HystrixCommand(ignoreExceptions = {HystrixBadRequestException.class})

public User getUserById(Long id) {

return restTemplate.getForObject(“http://HELLO-SERVICE/getUsers/{1}”, User.class, id);

}

private Long getUserByIdCacheKey(Long id) {

return id;

}

通过@CacheKey注解实现的方式更加简单,具体示例如下所示。但是在使用@CacheKey注解的时候需要注意,它的优先级比cacheKeyMethod的优先级低,如果已经使用了cacheKeyMethod指定缓存Key的生成函数,那么@CacheKey 注解不会生效。

@CacheResult

@HystrixCommand

public User getUserById(@CacheKey(“id”) Long id) {

return restTemplate.getForObject(“http://HELLO-SERVICE/getUsers/{1}”, User.class, id);

}

3.缓存清理

在之前的例子中,我们已经通过@CacheResult注解将请求结果置入Hystrix的请求缓存之中。若该内容调用了update 操作进行了更新,那么此时请求缓存中的结果与实际结果就会产生不一致 (缓存中的结果实际上已经失效了),所以我们需要在update类型的操作上对失效的缓存进行清理。

在Hystrix 的注解配置中,可以通过@CacheRemove注解来实现失效缓存的清理,比如下面的例子所示:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

架构学习资料

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

由于篇幅限制小编,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-SXrwJslb-1713417803647)]

[外链图片转存中…(img-ZL0eT2gd-1713417803647)]

[外链图片转存中…(img-PneoMMYH-1713417803647)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

架构学习资料

[外链图片转存中…(img-6PHj3CFS-1713417803648)]

[外链图片转存中…(img-gm84eqNk-1713417803648)]

[外链图片转存中…(img-GsRsAiqk-1713417803648)]

[外链图片转存中…(img-hpQVygwr-1713417803649)]

[外链图片转存中…(img-XINxTbs3-1713417803649)]

由于篇幅限制小编,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值