springCloud中的组件学习
一.首先使用springCloud和通用Mapper实现一个简单的CRUD
1. 创建一个maven工程,作为父级。
导入所需依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.4.RELEASE</version> <relativePath/> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring-cloud.version>Finchley.SR1</spring-cloud.version><!--Greenwich.SR2 Finchley.SR1--> <mapper.starter.version>2.0.3</mapper.starter.version> <mysql.version>5.1.32</mysql.version> <pageHelper.starter.version>1.2.5</pageHelper.starter.version> </properties> <dependencyManagement> <dependencies> <!--spring-cloud spring-cloud-dependencies:综合管理spring-cloud子组件 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--通用mapper启动器--> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>${mapper.starter.version}</version> </dependency> <!--分页助手启动器--> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>${pageHelper.starter.version}</version> </dependency> <!--mysql驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
2.创建注册中心,可以选择使用初始化器创建,也可以创建maven项目,之后引入依赖即可。
依赖
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies>
yml
server: port: 10086 #服务名 spring: application: name: eureka-server #向自己注册服务 eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka #不向自己注册 register-with-eureka: false #服饰失效后的剔除时间 server: eviction-interval-timer-in-ms: 6000 #指定使用ip地址注册,并且指定ip #enable-self-preservation: false #关闭自我保护机制 instance: prefer-ip-address: true ip-address: 127.0.0.1
启动类
@EnableEurekaServer//服务中心 @SpringBootApplication public class EurekaServer { public static void main(String[] args) { SpringApplication.run(EurekaServer.class); } }
若不做集群处理,注册中心就已经搭建完成。
3. 创建提供者服务
引入相关依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> </dependency> <!--服务注册--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies>
yml
server: port: 8081 spring: application: name: user-service datasource: url: jdbc:mysql://localhost:3306/sys username: root password: sasa mybatis: type-aliases-package: cn.itcast.user.pojo #注册服务 eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka #http://127.0.0.1:10087/eureka instance: prefer-ip-address: true ip-address: 127.0.0.1 #配置心跳续约时间 #lease-renewal-interval-in-seconds: 30 #如果90秒后还没发送请求,则证明这个服务挂了 #lease-expiration-duration-in-seconds: 90
实体类
import lombok.Data;
import tk.mybatis.mapper.annotation.KeySql;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
@Data @Table(name = "tb_user") //表名 public class User { //id @Id //声明它是主键 @KeySql(useGeneratedKeys = true)//自增,可以回显 private Long id; //用户名 private String userName; //密码 private String password; }
mapper
import tk.mybatis.mapper.common.Mapper; public interface UserMapper extends Mapper<User> { /** * 导的是 tk.mybatis.mapper.common.Mapper 这个包 * 内置关于user的增删改查方法 */ }
service
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class UserService { @Autowired private UserMapper userMapper; /** * 查询 * @param id * @return */ public User queryById(long id){ return userMapper.selectByPrimaryKey(id); } /** * 新增 * @param user */ @Transactional public void insertUser(User user){ userMapper.insert(user); } /** * 修改 * @param user * @return */ @Transactional public int updateById(User user){ return userMapper.updateByPrimaryKey(user); } /** * 删除 * @param user * @return */ @Transactional public int deleteById(User user){ return userMapper.deleteByPrimaryKey(user.getId()); }
web
@Slf4j @RestController /* @Controller + @ResponseBody*/ @RequestMapping("user") public class HelloController { /* @Autowired private DataSource dataSource;*/ @Autowired private UserService userService; @RequestMapping("{id}") public User hello(@PathVariable("id") Long id){ /* try { Thread.sleep(3000);//模拟超时 } catch (InterruptedException e) { e.printStackTrace(); }*/ return userService.queryById(id); } @RequestMapping("save") public void save(@RequestBody User user){ userService.insertUser(user); } @RequestMapping("delete") public int delete(@RequestBody User user){ return userService.deleteById(user); } @RequestMapping("update") public int update(@RequestBody User user){ return userService.updateById(user); }
4. 创建消费者服务
依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-ribbon</artifactId>
</dependency>
</dependencies>
yml
server: port: 8088 #服务名 spring: application: name: consumer-servies #注册服务 eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka #配置消费方拉取服务列表周期时间 ,http://127.0.0.1:10087/eureka registry-fetch-interval-seconds: 3 instance: prefer-ip-address: true ip-address: 127.0.0.1
创建实体,主要用于接收返回值。
@Data public class User { //id private Long id; //用户名 private String userName; //密码 private String password; }
暂时先不使用feign框架,使用spring的restTemplate,在来感受一下feign的魅力。
所以在启动类需要注入restTemplate
@EnableDiscoveryClient//扫描注册的实例服务 @SpringBootApplication public class ConsumerApplication { @Bean @LoadBalanced public RestTemplate testTemplate(){ return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class); } }
web
@RestController @RequestMapping("consumer") public class ConsumerController { //用于跨域调用,使用feign框架之后不在需要这么复杂的写法。 @Autowired private RestTemplate restTemplate; @GetMapping("{id}") public User queryById(@PathVariable("id") Long id){ String url = "http://user-service/user/"+id; return restTemplate.getForObject(url, User .class); } @PostMapping("save") public void save(@RequestBody User user){ String url = "http://user-service/user/save"; restTemplate.postForEntity(url,user,null); } @PutMapping("update") public Integer update(@RequestBody User user){ String url = "http://user-service/user/update"; return restTemplate.postForObject(url,user, Integer.class); } @DeleteMapping("{id}") public Integer delete(@PathVariable("id") Long id){ String url = "http://user-service/user/delete"; return restTemplate.getForObject(url,Integer.class); } }
到此为止,CRUD已经完成。
二.引入断路器:Hystrix
Hystix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。
熔断器Hystrix是容错管理工具,作用是通过隔离、控制服务从而对延迟和故障提供更强大的容错能力,避免整个系统别拖垮。
复杂分布式架构通常都具有很多依赖,当一个应用高度耦合其他服务时非常危险且容易导致失败,这种失败很容易伤害服务的调用者,最后导致一个接一个的连续错误,,这种失败很容易伤害服务的调用者,最后导致一个接一个发生延迟,将会在数秒内导致所有应用资源被耗尽。如何处理这些问题是有关系统性能和效率的关键性问题。
当在系统高峰时期,大量对微服务的调用可能会堵塞远程服务器的线程池,如果这个线程池没有和主应用服务器的线程池隔离,就可能导致整个服务器挂机。
Hystrix使用自己的线程池,这样和主应用服务器线程池隔离,如果调用花费很长时间,会停止调用,不同的命令或命令组能够被配置使用它们各自的线程池,可以隔离不同的服务。
线程隔离,服务降级简述
我们在一个tomcat中部署了多个服务,tomcat的线程容量假设为1000,这些服务相互调用调用,当一个请求过来时,其中一个服务挂了,那么线程就会被堵在这里,这时,又有其他请求过来,线程依然被堵在这里,当线程被占满而又无法释放时,整个系统就会崩掉。
线程隔离:分配给每隔服务单独的线程,假设50个,这样就不会堵塞所有的线程。但是但这50时个线程也全都阻塞了改怎么办呢?
服务降级:当单独分配的线程也别占满时,那么其中的的线程如果超过一定的时间还没被响应,就返回一个自定义的失败响应,线程释放。请求访问不到原本想要服务时,返回一个自定的响应服务。直到请求成功,服务端才会继续提供这个服务,称为服务降级,相当于是一种力度降级,不再提供整套服务。超时以后返回友好错误信息也可称为服务降级。
优先保证核心服务,而非核心服务不可用或弱可用。
服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这依赖服务对应的线程池中的资源,对其他服务没有影响
触发Hystix服务降级的情况:
线程池已满
请求超时
服务降级结合restTemplate使用使用
应当是在消费方进行操作
1.在消费方引入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency
2. 在启动类上加上注解
@EnableCircuitBreaker//服务熔断和服务降级、
/*@EnableCircuitBreaker//服务熔断和服务降级 @SpringCloudApplication //该注解可代替以上三个注解
@EnableDiscoveryClient//扫描注册的实例服务
*/
//使用 SpringCloudApplication 代替以上三个注解
@SpringCloudApplication //该注解可代替以上三个注解 public class ConsumerApplication { @Bean @LoadBalanced//结合Ribbon开启负载均衡 public RestTemplate testTemplate(){ return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class); } }
3.配置默认超时时长,暂时不配置,使用默认的配置。
4. 修改web,编写失败返回信息,这里先只对查询做操作
@GetMapping("{id}") @HystrixCommand(fallbackMethod = "queryByIdFallBack")//无需在指定方法,只需要开启就行 public String queryById(@PathVariable("id") Long id){ String url = "http://user-service/user/"+id; return restTemplate.getForObject(url, String.class); } //成功方法和失败方法的返回值必须一致 public String queryByIdFallBack(Long id){ return "不好意思,服务器繁忙"; }
5.测试,可以在提供者服务的查询方法里设置休眠。
@GetMapping("{id}") public User hello(@PathVariable("id") Long id){ try { Thread.sleep(3000);//模拟超时 } catch (InterruptedException e) { e.printStackTrace(); } return userService.queryById(id); }
6 .针对所哟请求都开启服务降级处理。只需要要在类上加上@DefaultProperties 注解,并在每隔方法上开启服务降级。
@RestController @RequestMapping("consumer") @DefaultProperties(defaultFallback = "defaultFallback")//为这个类声明一个默认的降级方法 public class ConsumerController { @GetMapping("{id}") @HystrixCommand//无需在指定方法,只需要开启就行 public String queryById(@PathVariable("id") Long id){ String url = "http://user-service/user/"+id; return restTemplate.getForObject(url, String.class); } @PostMapping("save") @HystrixCommand public void save(@RequestBody User user){ String url = "http://user-service/user/save"; restTemplate.postForEntity(url,user,null); } //通用方法不能有参数,因为他多对很多方法做处理 public String queryByIdFallBack(){ return "不好意思,服务器繁忙"; } @PutMapping("update") @HystrixCommand public Integer update(@RequestBody User user){ String url = "http://user-service/user/update"; return restTemplate.postForObject(url,user, Integer.class); } @DeleteMapping("{id}") @HystrixCommand public Integer delete(@PathVariable("id") Long id){ String url = "http://user-service/user/delete"; return restTemplate.getForObject(url,Integer.class); } }
7. 优化,对超时时长进行设置,可以针对所有服务进行设置,也可以针对单独某个服务进行设置,yml文件。
#配置全局的超时时间 hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 1000 #可以针对于某一个服务来配置超时时间 user-service: execution: isolation: thread: timeoutInMilliseconds: 1000
熔断器的工作机制
服务熔断:当下游的服务因为某种原因突然变得不可用或响应过慢,上游服务为了保证自己整体服务的可用性,不再继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。Hystrix默认的超时时间为1s,请求时间超过1s,熔断次数加1。
熔断机制的原理很简单,像家里的电路熔断器,如果电路发生短路能立刻熔断电路,避免发生灾难。在分布式系统中应用这一模式之后,服务调用方可以自己进行判断某些服务反应慢或者存在大量超时的情况时,能够主动熔断,防止整个系统被拖垮。
不同于电路熔断只能断不能自动重连,Hystrix 可以实现弹性容错,当情况好转之后,可以自动重连。这就好比魔术师把鸽子变没了容易,但是真正考验技术的是如何把消失的鸽子再变回来。
通过断路的方式,可以将后续请求直接拒绝掉,一段时间之后允许部分请求通过,如果调用成功则回到电路闭合状态,否则继续断开。
熔断器关闭:请求可以通过
熔断器打开:请求不可以通过,迅速返回错误提示,提高服务可用性。默认情况下,如果最近的20次请求有50%都超时了,那么就会认为你的服务有问题,断路器打开。如果用户再来访问这个接口时,会快速返回失败,默认持续5秒,,5秒后进入半开状态。
半开:在进入该状态后会放入部分请求;判断请求是否成功,不成功,进入open状态,重新计时,进入halfopen状态;成功,进入closed状态。
1. 测试,修改默认阈值(请求次数),和休眠时间戳(默认5秒的时间),方便看到效果。
@GetMapping("{id}") @HystrixCommand(commandProperties = { @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "20000"),//设置请求超时 时间 @HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value = "10"),//每请求10次统计一次熔错率 @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value="10000"), //休眠时间(ms),默认5秒 @HystrixProperty(name="circuitBreaker.errorThresholdPercentage",value="60") }) public String queryById(@PathVariable("id") Long id){ //由我们在浏览器手动控制熔断触发,连续访问5次以上就会进入断开状态。 if(id % 2==0){ throw new RuntimeException("");//抛出异常时会触发熔断 } String url = "http://user-service/user/"+id; return restTemplate.getForObject(url, String.class); }
熔断的目的是为了提高服务的可用性,不会因为一个服务的阻塞而降低整个系统的性能。
三:引入Feign //TODO
在未引入feign之前,在消费者服务中调用方法是通过这样的方式进行调用方法的。
String url = "http://user-service/user/"+id; restTemplate.getForObject(url, String.class);
这样调用的话,一看就知道在干什么,不够安全,并且不利于维护,而且不够优雅。
Feign:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
2. 在启动类引入注解 :@EnableFeignClients
@SpringCloudApplication //该注解可代替以上三个注解 @EnableFeignClients //利用feign框架实现简答的远程调用 public class ConsumerApplication { @Bean @LoadBalanced public RestTemplate testTemplate(){ return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class); } }
3. 对请求地址进行伪装。实现原理参考:https://www.jianshu.com/p/8c7b92b4396c
/** * 告诉feign 请求所需的信息 * 1.路径 user/{id} * 2.参数 Long id * 3.返回结果 User * 4.请求方式 GetMapping
* 5.服务名:user-service */ @FeignClient(value = "user-service") public interface UserClient { @GetMapping("user/{id}") User queryById(@PathVariable("id") Long id); }
4. web层
@RestController @RequestMapping("consumer") public class ConsumerController { @Autowired private UserClient userClient; @GetMapping("{id}") public User queryById4(@PathVariable("id") Long id){
//从代码的角度来看,这只是调用了一个方法,但他已经实现了远程调用 return userClient.queryById(id); } }
5. feign的熔断机制
1. 首先加入自定义配置,feign中对ribbon的默认超时时长是1S,并且对hystrix是默认关闭的。
#在Feign里启动hystrix feign: hystrix: enabled: true #Ribbon需要在feign里配置的超时时长 Ribbon: ConnectionTimeOut: 500 #如果在500ms内没有建立连接,就抛出异常 ReadTimeOut: 2000 #如果建立连接后,在2秒内没有读取到数据也抛出异常
2. 既然开启了熔断,那么就肯定要做熔断处理,使用restTemplate结合hystrix时,是在controller类中加上全局的熔断方法,或者个某一个方法加上熔断处理。
使用feign实现远程调用时,可以不需要在单独引入hystrix和ribbon。所以 @DefaultProperties 注解也将失效。那么怎么做到熔断处理呢?
引入feign后的pom.xml
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies>
3.在feign的客户端上加上 fallback 属性
/** * 告诉feign 请求所需的信息 * 1.路径 user/{id} * 2.参数 Long id * 3.返回结果 User * 4.请求方式 GetMapping * 5.fallback 熔断返回处理 */ @FeignClient(value = "user-service",fallback = UserClientFallBack.class ) public interface UserClient { @GetMapping("user/{id}") User queryById(@PathVariable("id") Long id); }
4.编写客户端的实现类,其中的实现类就是对每一个请求的熔断处理。
@Component//熔断处理,注入到容器中 public class UserClientFallBack implements UserClient { /** * 在feign框架中实现 熔断 的支持, * 第一;在配置文件里启用hystrix,因为feign没有引用hystrix的启动器,所以没有提示,需要手写 * 第二:在该项目的启动器上配置 EnableCircuitBreaker * 第三:在实现了 告诉feign调用信息的接口 中写熔断处理,并将该类注入到spring容器中去。 * @param id * @return */ @Override public User queryById(Long id) { User user = new User(); user.setName("未查询到此用户"); return user; } }
三:引入zuul
zuul是什么?
Zuul包含了对请求的路由和过滤两个最主要的功能
路由:负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础
过滤:负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础
为什么需要路由网关?参考:https://www.jianshu.com/p/29e9c91e3f3e
不同的微服务一般会有不同的网络地址,而客户端可能需要调用多个服务接口才能完成一个业务需求,若让客户端直接与各个微服务通信,会有以下问题:
1.客户端会多次请求不同微服务,增加了客户端复杂性
2. 存在跨域请求,处理相对复杂
3. 证复杂,每个服务都需要独立认证
4. 难以重构,多个服务可能将会合并成一个或拆分成多个
而网关介于服务端与客户端的中间层,所有外部服务请求都会先经过微服务网关,客户只能跟微服务网关进行交互,无需调用特定微服务接口,使得开发得到简化。
引入zuul后的架构
<dependencies>
<!--网关
该依赖已经包含了web,hystrix,ribbon
-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
2.启动类
@EnableZuulProxy//开启网关功能 @SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class); } }
3. yml配置
server: port: 8989 #配置路由的规则,确保zuul能够确切的知道每一个请求 zuul: routes: #routes 下面可配置多条路由规则,格式为map格式。key(路由id) 可以随便写,值为用户实际请求的路径 zu1: #键,只要不重复,随便写.但习惯吧访问的路径写成键值
path: /user-service/** #用户访问的路径前缀 url: http://127.0.0.1:8081 #经过zuul将用户的请求转发到这个路径,这也就是匹配规则
#以上规则有漏洞,地址被写死了。
这样的配置,无法做到负载均衡,并且地址别写死了。我们应该面向服务路由配置。
引入依赖
<!--引入eureka
实现面向服务的路由,地址不用在写死
-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
yml配置,拉去服务列表
server: port: 8989 #配置路由的规则,确保zuul能够确切的知道每一个请求 zuul: routes: user-service:
path: /user-service/**
#转发路径通过服务id从服务列表获取 serviceId: user-service eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka instance: appname: gateway
简化配置,键不是很重要,并且配置时,我们一般都会将键写成服务名
server: port: 8989 #配置路由的规则,确保zuul能够确切的知道每一个请求 zuul: routes:
#服务id可以看作是路由id,因为服务id本身就具有唯一性,映射路径直接写在后面 user-service: /user-service/** eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka instance: appname: gateway
因为
#服务id可以看作是路由id,因为服务id本身就具有唯一性,映射路径直接写在后面
user-service: /user-service/**
这种配置非常常见,所以springCloud帮我们把这种常见的配置都做了。
配置如下
server: port: 8989 eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka instance: appname: gateway
但是默认的配置把所有的微服务都暴露出来了,而我们不先暴露这个服务,那么怎么做呢?
server: port: 8989 zuul: routes: ignored-services: #值为数组,忽略的服务 - consumer-servies eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka instance: appname: gateway
过滤器:参考 https://www.jianshu.com/p/ff863d532767
负载均衡和熔断
Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。因此建议我们手动进行配置:
zuul: retryable: true ribbon: ConnectTimeout: 250 # 连接超时时间(ms) ReadTimeout: 2000 # 通信超时时间(ms) OkToRetryOnAllOperations: true # 是否对所有操作重试 MaxAutoRetriesNextServer: 2 # 同一服务不同实例的重试次数 MaxAutoRetries: 1 # 同一实例的重试次数 hystrix: command: default: execution: isolation: thread: timeoutInMillisecond: 6000 # 熔断超时时长:6000ms