SpringCloud
SpringCloud解决的四大难题
-
模块拆分后服务变得很多,客户端怎么访问?
-
服务这么多,服务之间怎么通信?
-
这么多服务,怎么治理?
-
服务挂了怎么办?
组件(解决方案)
Eureka(注册与发现中心)
作用
-
概念
- 服务端:注册中心,专门用于存储客户端服务列表和管理客户端的一个服务器,一般称为服务端
- 客户端:专门服务于用户的服务器,里面存储的是各个请求接口用于返回数据。这个名字容易被混淆,被误认为用户端
-
存放和调度服务,实现服务和注册中心,服务和服务之间的相互通信
手写Eureka思路
- 在注册中心中需要一个服务列表(容器)保存应用(各个服务)信息
- 应用下线了或者挂了服务列表需要整理
- 主动下线:服务自己关掉链接
- 被动下线:由于服务长时间没有与注册中心交互(心跳机制:每次心跳就是一次请求),被注册中心剔除了
- 服务与服务之间的通信模式
- 当服务A需要访问服务B时,正常流程时服务A先到注册中心拿到服务B的相关信息,再访问服务B。
- 为了解决这种模式,让服务每隔一段时间从注册中心拉取服务列表并缓存到本次,从而实现快速查到其它服务的IP、端口。但容易出现脏读
- 怎么解决(缓解)脏读问题?
- 缓解脏读问题,拉取时间间隔越少脏读越少,随之性能消耗就更大
Eureka集群
集群作用
- 当其中一台注册中心服务器挂掉之后,不影响使用
集群的实现
- 就是开多台机子(设置相同的应用名称:${spring.application.name}),使用不同的端口
集群的实现方式
- 主从模式
- 从多台服务器中选一台主服务器实现读写分离和数据同步
- 哨兵模式
- 主从模式的升级版,当主服务器挂了(没有向从服务器发出PING命令),则所有从服务器开启投票,即所有服务器进行初始化,先初始化的从机则先开启投票,也就成为了新的主机
- 去中心化模式
Eureka源码分析
-
服务注册
-
当客户端向服务端发送注册请求时,将客户端的配置文件数据封装到instanceInfo中,并封装请求发送到服务端(根据服务端serviceUrl找到)
boolean register() throws Throwable { logger.info(PREFIX + "{}: registering service...", appPathIdentifier); EurekaHttpResponse<Void> httpResponse; try { httpResponse = eurekaTransport.registrationClient.register(instanceInfo); } catch (Exception e) { public EurekaHttpResponse<Void> register(InstanceInfo info) { String urlPath = "apps/" + info.getAppName(); ClientResponse response = null; try { Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder(); addExtraHeaders(resourceBuilder); response = resourceBuilder .header("Accept-Encoding", "gzip") .type(MediaType.APPLICATION_JSON_TYPE) .accept(MediaType.APPLICATION_JSON) .post(ClientResponse.class, info); return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build(); } finally { -
将客户端配置文件数据封装到map
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) { this.read.lock(); try { Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(registrant.getAppName()); EurekaMonitors.REGISTER.increment(isReplication); if (gMap == null) { ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap(); gMap = (Map)this.registry.putIfAbsent(registrant.getAppName(), gNewMap); if (gMap == null) { gMap = gNewMap; } }
-
-
服务续约
-
服务下线
- 主动下线
- 被动下线
服务发现
-
解释:各个客户端如何实现通信
-
实现:通过服务的应用名称找到服务的具体实例
@Autowired DiscoveryClient discoveryClient; @GetMapping("/test") public String doDiscovery(String serviceName){ // 服务发现:通过服务的具体名称找到服务的具体信息 List<ServiceInstance> instances = discoveryClient.getInstances(serviceName); instances.forEach(System.out::println); return instances.get(0).toString();
RestTemplate
-
作用:实现使用Java代码发送请求或请求一个页面
void contextLoads() { // 在java中发送一个请求 请求一个页面 RestTemplate restTemplate = new RestTemplate(); String url="https://www.baidu.com"; String forObject = restTemplate.getForObject(url, String.class); System.out.println(forObject);
Ribbon(负载均衡)
- 简介:ribbon只是用于负载均衡,但不能直接链接到各个客户端,因此需要集成一个发送请求的工具RestTemplate用于找到对应的客户端从而进行负载均衡
Feign(远程调用)
-
简介:该组件可以对客户端进行远程调用,也可以说代替了RestTemplate。同时feign集成了ribbon,所以feign主要用于远程调用客户端并对客户端进行负载均衡
-
调用
- 本地调用:用一些操作系统内核函数时,虽然不是同一个进程中,但也属于同一个机器上
- 远程调用:调用双方并不在同一个机器上
-
注意点
- ribbon默认的最大调用等待时间时1秒,如果需要修改,需要通过修改ribbon配置文件
-
手写feign
-
思路:使用JDK动态代理实现,增强嘛,在方法之前找到对应的客户端进行增强
- 调用条件:获取到客户端的IP,接口路径,端口
- ribbon:组成"http://"+注册中心的应用名称+ 接口路径即可。ribbo通过应用名称找到该客户点的配置文件信息并封装到路径中
-
通过@FeignClient("")注解找到对应客户端主机名
-
通过@GetMapping("")注解找到对应路径
-
封装url并发送请求到指定客户端并返回
-
// 手写feign :jdk动态代理 @Autowired RestTemplate restTemplate; @Test void contextLoads() { UserOrderFeign o=(UserOrderFeign) Proxy.newProxyInstance(UserController.class.getClassLoader(), new Class[]{UserOrderFeign.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 调用:拿到ip、端口 组成"http://"+注册中心的应用名称+ 接口路径,String.class // 注册中心的应用名称:@FeignClient("order-service") // 接口路径:@GetMapping("/order") // 拿名称 Class<?> aClass = method.getDeclaringClass(); FeignClient feignClient = aClass.getAnnotation(FeignClient.class); String appName = feignClient.value(); // 拿路径 GetMapping annotation = method.getAnnotation(GetMapping.class); String[] paths = annotation.value(); String path = paths[0]; // 组路径 String url="http://" + appName + "/" + path; // 发送 String forObject = restTemplate.getForObject(url, String.class); return forObject; } }); System.out.println(o.order()); }
-
-
feign的参数处理
-
构建者模式:在实体类中加@Builder注解,可以不用再使用set方法值给属性赋值
-
@Data @AllArgsConstructor @NoArgsConstructor @Builder public class DoMin { private int id; private String name; private int age; } class test{ public static void main(String[] args) { DoMin liny = DoMin.builder() .id(1) .name("liny") .age(19) .build(); System.out.println(liny); } } -
注意点
-
每个参数都需要加上对应注解,如果不加会报错。
-
不要直接传时间参数,会有误差,一般转为字符串传参
String format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
-
-
-
feign源码分析
-
feign的日志处理
-
feign自带日志,需要手动开启
-
选择对应的打印级别
public enum Level { /** * No logging. */ NONE, /** * Log only the request method and URL and the response status code and execution time. */ BASIC, /** * Log the basic information along with request and response headers. */ HEADERS, /** * Log the headers, body, and metadata for both requests and responses. */ FULL } -
放入IOC容器中
@Bean public Logger.Level level(){ return Logger.Level.FULL; } -
配置yml文件设置日志级别
logging: level: com.liny.feign.UserOrderFeign: debug #我需要打印这个接口下的日志
-
-
Hystrix(熔断器)
- 服务雪崩
- 服务调用之间是单线程吗?
- 当服务A调用服务B,服务B会分配一个线程支持服务A的调用,服务B发现需要完成服务A的操作需要去调用服务C,但是服务C宕机了,服务B不知道,服务B就只能等待超时后再返回数据。
- 引起服务雪崩:服务B进行等待超时这段时间服务A和服务B的线程都被占用,如果现在有很多请求需要调用A或B,就会开始排队,当排队请求过量后,服务就会出现故障。
- 简介
- 当上一级服务发现下一级服务宕机后进行熔断操作
- 熔断操作
- 发现被调用服务宕机后不进行等待超时之间返回
- 还可以设置备用方案
- 手写熔断器思路
Feign的工程化案例‘
- 实际公司项目结构
链路追踪
admin监控
- 监控的本质:心跳检测、打乒乓球
- admin分为服务端与客户端。只要服务端可以获取所有客户端的ip和端口号,就可以向客户端发送检测信号。所以之间把admin服务端往注册中心注册,就可以拉取所有客户端的ip和端口号
浙公网安备 33010602011771号