SpringCloudAlibaba
一、SpringCloud基本介绍
我们为什么要使用SpringCloud?
SpringCloud并不是rpc远程调用框架,而是一套全家桶的微服务解决框架,理念就是解决我们在微服务架构中遇到的任何问题。例如:服务注册中心、分布式配置、服务保护等。
SpringCloud 微服务架构思想:解决我们在微服务架构中遇到难题。
-
分布式服务注册中心(服务治理) Eureka、Zookeeper、Consule、Nacos、Redis、数据库等;
-
分布式配置中心 SpringCloud Config、携程阿波罗、Nacos Config;
-
分布式事务解决方案(MQ最终一致性/LCN(已经淘汰)/ Seata(阿里背书))
-
分布式任务调度平台(xxl-job、elastic job、阿里巴巴Scheduler)
-
分布式日志采集系统ELK+Kafka;
-
分布式服务追踪与调用链Zipkin、skywalking等。
-
分布式锁(Redis(Redisson)/Zookeeper(Curator)实现分布式锁);
-
服务的接口保护(hystrix/sentinel);
SpringCloud第一代:
SpringCloud Config 分布式配置中心
SpringCloud Netflix 核心组件
Eureka:服务治理
Hystrix:服务保护框架
Ribbon:客户端负载均衡器
Feign:基于ribbon和hystrix的声明式服务调用组件
Zuul::网关组件,提供智能路由、访问过滤等功能。
SpringCloud第二代(自己研发)和优秀的组件组合:
Spring Cloud Gateway 网关
Spring Cloud Loadbalancer 客户端负载均衡器
Spring Cloud r4j(Resilience4J) 服务保护
Spring Cloud Alibaba Nacos 服务注册
Spring Cloud Alibaba Nacos 分布式配置中心
Spring Cloud Alibaba Sentinel服务保护
SpringCloud Alibaba Seata分布式事务解决框架
Alibaba Cloud OSS 阿里云存储
Alibaba Cloud SchedulerX 分布式任务调度平台
Alibaba Cloud SMS 分布式短信系统
SpringCloud与alibaba相结合,技术上有人负责更新新的组件,也可以继续使用Spring社区的技术,阿里另外一方面也可以推广阿里云和各种商业软件,双赢局面。于是SpringCloud Alibaba诞生了。
二、微服务架构中常用名词
-
生产者:提供接口;
-
消费者:调用生产者提供的接口;
-
服务注册:当我们服务启动时会将服务的ip和端口注册存放在注册中心上,以key-vakue的形式存放,key为服务的名称,value为服务的ip和端口号;
-
服务发现:消费者调用接口时根据服务名称去注册中心查找该对应服务接口地址,在本地实现RPC远程调用;
三、微服务服务注册中心
例如我们自己开发了一个项目,想在项目中获取天气预报信息,就可以直接调用中国天气预报接口:
//香港:101320101 城市代码 http://www.weather.com.cn/data/cityinfo/101320101.html //接口返回Json信息
为了方便查看json,可以使用在线json格式化网站对json进行格式化:
https://www.sojson.com/
接口:
接口协议:ip:端口/接口名称?传递参数
接口协议有多种,此处只介绍http协议:httpclient或者okhttp。在我们的微服务架构中采用最多的也是http + json格式。(json格式比xml格式更加轻量级)。传递一个参数直接在?后拼接,多个参数采用json传递。
-
RPC远程调用演示
-
创建SpringBoot项目
-
Maven依赖:
<!-- httpclient依赖 --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.5</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.66</version> </dependency>
-
HttpClient工具类:
public class HttpClientUtils { private static final CloseableHttpClient httpClient; public static final String CHARSET = "UTF-8"; private static final Log log = LogFactory.getLog(HttpClientUtils.class); // 采用静态代码块,初始化超时时间配置,再根据配置生成默认httpClient对象 static { RequestConfig config = RequestConfig.custom().setConnectTimeout(60000).setSocketTimeout(15000).build(); httpClient = HttpClientBuilder.create().setDefaultRequestConfig(config).build(); } public static String doGet(String url, Map<String, String> params) { return doGet(url, params, CHARSET); } public static String doPost(String url, Map<String, String> params) throws IOException { return doPost(url, params, CHARSET); } /** * HTTP Get 获取内容 * * @param url 请求的url地址 ?之前的地址 * @param params 请求的参数 * @param charset 编码格式 * @return 页面内容 */ public static String doGet(String url, Map<String, String> params, String charset) { try { if (params != null && !params.isEmpty()) { List<NameValuePair> pairs = new ArrayList<NameValuePair>(params.size()); for (Map.Entry<String, String> entry : params.entrySet()) { String value = entry.getValue(); if (value != null) { pairs.add(new BasicNameValuePair(entry.getKey(), value)); } } // 将请求参数和url进行拼接 url += "?" + EntityUtils.toString(new UrlEncodedFormEntity(pairs, charset)); } HttpGet httpGet = new HttpGet(url); CloseableHttpResponse response = httpClient.execute(httpGet); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode != 200) { httpGet.abort(); throw new RuntimeException("HttpClient,error status code :" + statusCode); } HttpEntity entity = response.getEntity(); String result = null; if (entity != null) { result = EntityUtils.toString(entity, "utf-8"); } EntityUtils.consume(entity); response.close(); return result; } catch (Exception e) { log.error("请求服务器端出错:" + e); return null; } } /** * HTTP Post 获取内容 * * @param url 请求的url地址 ?之前的地址 * @param params 请求的参数 * @param charset 编码格式 * @return 页面内容 * @throws IOException */ public static String doPost(String url, Map<String, String> params, String charset) throws IOException { List<NameValuePair> pairs = null; if (params != null && !params.isEmpty()) { pairs = new ArrayList<NameValuePair>(params.size()); for (Map.Entry<String, String> entry : params.entrySet()) { String value = entry.getValue(); if (value != null) { pairs.add(new BasicNameValuePair(entry.getKey(), value)); } } } HttpPost httpPost = new HttpPost(url); if (pairs != null && pairs.size() > 0) { httpPost.setEntity(new UrlEncodedFormEntity(pairs, CHARSET)); } CloseableHttpResponse response = null; try { response = httpClient.execute(httpPost); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode != 200) { httpPost.abort(); throw new RuntimeException("HttpClient,error status code :" + statusCode); } HttpEntity entity = response.getEntity(); String result = null; if (entity != null) { result = EntityUtils.toString(entity, "utf-8"); } EntityUtils.consume(entity); return result; } catch (ParseException e) { log.error("请求服务器端出错:" + e); return null; } finally { if (response != null) response.close(); } }
-
TestController类
@RestController public class TestController { @RequestMapping("/getWeather") public Object getWeather(String code){ //code 城市代码 StringBuilder url = new StringBuilder(); url.append("http://www.weather.com.cn/data/cityinfo/"); url.append(code); url.append(".html"); String result = HttpClientUtils.doGet(url.toString(),null); return JSONObject.parse(result); } }
-
浏览器测试即可显示天气信息。
-
-
代码演示
订单服务调用会员服务(思考:是先写服务提供者接口还是消费者接口)
-
mumber-producer服务
会员服务提供接口:
/** * 会员接口 */ @RestController public class MemberService { /** * 会员服务提供接口 * * @return */ @RequestMapping("/getMember") public String getMember() { return "我是会员服务,我提供接口"; } }
-
order-consumer服务
订单服务调用接口:
- 未使用注册中心—HttpClient实现RPC远程调用
/** * 订单服务调用会员服务接口 */ @RestController public class OrderToMemberService { @RequestMapping("/orderToMember") public String orderToMember() { @Autowired private DiscoveryClient discoveryClient; /** * 根据服务名称从注册中心获取接口地址 * 服务提供启动多个集群 * */ List<ServiceInstance> instances = discoveryClient.getInstances("member-producer"); ServiceInstance serviceInstance = instances.get(0); //默认取第一个服务 //会员服务的ip和端口 String memberUrl = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/" + "getMember"; //或者一行代码简单解决 //String memberUrl = "http://localhost:8080/getMember"; return "订单服务调用会员服务:" + HttpClientUtil.doGet(memberUrl, null); //HttpClient 工具类 实现RPC远程调用 }
直接RPC远程调用缺点很多:
-
如提供者修改了端口号,消费者也要跟着改动,并且消费者需要重启生效。
-
如果提供者宕机,消费者就会调用失败。
-
不能做集群处理。
- 使用nacos注册中心
推荐使用nacos2.x + 版本,比1.x版本更强大。nacos官方文档:
https://nacos.io/zh-cn/docs/what-is-nacos.html
nacos安装详情看我另一篇笔记:
https://blog.csdn.net/weixin_44780078/article/details/127412739?spm=1001.2014.3001.5501
-
未使用注册中心—使用RestTemplate(底层也是HttpClient实现)
RestTemplate简介:
RestTemplate 是从 Spring3.0 开始支持的一个 HTTP 请求工具,它提供了常见的REST请求方案的模版,例如 GET 请求、POST 请求、PUT 请求、DELETE 请求以及一些通用的请求执行方法 exchange 以及 execute。RestTemplate 继承自 InterceptingHttpAccessor 并且实现了 RestOperations 接口,其中 RestOperations 接口定义了基本的 RESTful 操作,这些操作在 RestTemplate 中都得到了实现。
@RestController public class OrderToMemberService { @Autowired private RestTemplate restTemplate; @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } @RequestMapping("/orderToMember") public String orderToMember() { /** * 根据服务名称从注册中心获取接口地址 * 服务提供启动多个集群 * */ List<ServiceInstance> instances = discoveryClient.getInstances("member-producer"); ServiceInstance serviceInstance = instances.get(0); //默认取第一个服务 //会员服务的ip和端口 String memberUrl = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/" + "getMember"; String result = restTemplate.getForObject(memberUrl, String.class); return "订单服务调用会员服务:" + result; } }
-
四、手写本地负载均衡
同时启动两个提供者接口:
消费者获取到注册中心多个地址,到底应该选择哪个地址呢?因此引入负载均衡,负载均衡算法:轮询、随机、权重等。
- 轮询:假设a服务调用b服务,a有n个集群,b有m个集群,a服务每一次调用时轮询算法采用的策略是当前调用次数%m;(如果m=3)即
第一次:1 % 3 = 1(调用第1个服务)
第二次:2 % 3 = 2(调用第2个服务)
第三次:3 % 3 = 0(调用第0个服务)
…
第n次:n % 3 = x (调用第x个服务) - 随机:顾名思义,随机调用。
- 权重:给每个服务设置权重,权重范围[0,1],0最小,1最大,权重越大调用机率就越大。
代码演示:
父接口LoadBalance.java
public interface LoadBalance {
/**
* 使用负载均衡算法 实现轮询机制
* @param serviceId
* @return
*/
ServiceInstance getInstances(String serviceId);
}
-
轮询算法(在调用者编写):
RoundLoadBalance.java
@Component public class RoundLoadBalance implements LoadBalance{ @Autowired private DiscoveryClient discoveryClient; //AtomicInteger:原子类 private AtomicInteger atomicCount = new AtomicInteger(-1); @Override public ServiceInstance getInstances(String serviceId) { //查询出所有服务 List<ServiceInstance> instances = discoveryClient.getInstances("mumber-producer"); if (instances == null || instances.size() == 0) { return null; } //轮询算法实现 int index = atomicCount.incrementAndGet() % instances.size(); return instances.get(index); } }
即可实现轮询调用会员服务。
-
随机算法:
@Component public class RandomLoadBalance implements LoadBalance { @Autowired private DiscoveryClient discoveryClient; public ServiceInstance getInstances(String serviceId) { List<ServiceInstance> instances = discoveryClient.getInstances("mumber-producer"); if (instances == null || instances.size() == 0) { return null; } Random r = new Random(); int index = r.nextInt(instances.size() ); //r.nextInt(); //返回一个随机整数 //r.nextInt(10);//返回一个10以内的随机整数,不包括10 //r.nextDouble(); //返回一个0~1之间的double类型的随机小数 return instances.get(index); } }
-
权重算法:
- 可以直接在nacos控制台设置每个服务的权重实现
权重范围[0, 1],0最小,访问机率也最小,反之亦然。假如会员有两个服务,
端口号分别为8080和8081,8080权重为1,8081权重为0.5,即二者之比为2:1
第一次访问:访问8080
第二次访问:访问8080
第三次访问:访问8081
第四次访问:访问8080
…
第n次访问:访问谁按比例推算。
-
代码
WeightLoadbanlance.java/** * 权重算法 */ @Component public class WeightLoadbanlance implements LoadBalance{ @Autowired private DiscoveryClient discoveryClient; private AtomicInteger countAtomicInteger = new AtomicInteger(-1); @Override public ServiceInstance getInstances(String serviceId) { //获取注册中心的所有实例 List<ServiceInstance> instances = discoveryClient.getInstances(serviceId); if(instances == null){ return null; } ArrayList<ServiceInstance> newInstances = new ArrayList<>(); instances.forEach(service -> { //获取该服务实例对应的权重比例 Double weight = Double.parseDouble(service.getMetadata().get("nacos.weight")); for (int i=0; i<weight; i++){ newInstances.add(service); } }); return newInstances.get(countAtomicInteger.getAndIncrement() % newInstances.size()); } }
五、服务宕机、故障转移
如果有服务宕机,只需写个for循环,try/catch捕获异常,一次宕机,自动换下一个,一直自动验证下去;
@RequestMapping("/orderToMember")
public String orderToMember() {
List<ServiceInstance> instances = discoveryClient.getInstances("mumber-producer");
for (int i = 0; i < instances.size(); i++) {
ServiceInstance serviceInstance = instances.get(i);
String memberUrl = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/" + "getMember";
try {
ResponseEntity<String> response = restTemplate.getForEntity(memberUrl, String.class);
if (response == null) {
continue;
}
return "订单服务调用会员服务:" + response.getBody() + serviceInstance.getPort();
} catch (RestClientException e) {
log.error("rpc远程调用发送故障,开始故障转移 e:{}", e);
}
}
return "fail";
}
六、负载均衡Ribbon
Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡(Load Balance) 工具,它基于Netflix Ribbon实现。通过 Spring Cloud 的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。
Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个 Spring Cloud 构建的微服务和基础设施中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过 Ribbon 来实现的,包括后续我们将要介绍的 Feign,它也是基于 Ribbon实现的工具。所以,对 Spring Cloud Ribbon 的理解和使用,对于我们使用 Spring Cloud来构建微服务非常重要。SpringCloud默认已经引入了Ribbon依赖。
负载均衡默认采用轮询算法。
LoadBalancerClient负载均衡器
@RequestMapping("/loadBalancerClient")
public Object loadBalancerClient() {
return loadBalancerClient.choose("mumber-member");
}
- 本地负载均衡器与Nginx 的区别
-
Nginx是客户端所有的请求统一都交给我们的Nginx处理,让后在由Nginx实现负载均衡转发,属于服务器端负载均衡器。
-
本地负载均衡器是从注册中心获取到集群地址列表,本地实现负载均衡算法,既本地负载均衡器。
-
Nginx属于服务器负载均衡,应用于Tomcat/Jetty服务器等,而我们的本地负载均衡器,应用于在微服务架构中rpc框架中:openfeign、dubbo等。
- Ribbon负载均衡器策略
- RoundRobinRule:轮询
- RandomRule:随机
- RetryRule:先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内进行重试,获取可用的服务。
- WeightedResponseTimeRule:对RoundRobinRule的扩展,响应速度快的实例选择权重越大,越容易被选择。
- BestAvailableRule:会过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。
- AvailablityFilteringRule:先过滤掉故障实例,在选择并发较小的实例。
- ZoneAvoidanceRule:默认规则,符合判断server所在区的性能和server的可能性选择服务器。
七、分布式配置中心
思考:为什么使用分布式配置中心?
分布式配置中心可以实现不需要重启服务器,动态的修改配置文件内容,常见的配置中心有如下:
- 携程的阿波罗(构建环境非常复杂—底层的架构粒度拆分的非常细)。
- SpringCloud Config(没有任何界面,将配置文件内容存放在git上面)—不如我们自己写。
- Nacos注册中心/分布式配置中心(属于轻量级)—建议直接学习nacos。
在我们的传统项目中,配置文件内容直接写死存放在项目中,不可动态更改,极为不方便。分布式配置中心即为解决这一缺点,分布式配置中心的配置文件是存放在服务端。
-
在nacos控制台创建配置文件方法:
- 配置管理—>配置列表—>右边导入配置 “+” 图标。
此处的配置内容优先级最高,其次是bootstrap.yml,application.yml次之。
在配置bootstrap.yml的过程中有可能会遇到一个bug:
java Param ‘serviceName‘ is illegal, serviceName is blank
如不幸遇到,可参考如下解决方案:
https://blog.csdn.net/wyz0923/article/details/118303072
在nacos控制台配置的配置文件会默认缓存到电脑的如下路径:
C:\Users\admin\nacos\config\fixed-localhost_8848_nacos\snapshot\DEFAULT_GROUP
因此就算我们的nacos宕机了,在本地也有配置文件的缓存,服务也能正常运行。
-
配置刷新
当在nacos控制台如果修改了配置文件,本地的缓存也会立马刷新,但是如果我们通过@Value(“${fll.address}”)获取配置文件中的内容,会发现获取得到的还是未修改之前的内容。要想同步刷新,只需加上如下注解:
@RestController @Slf4j /** * @RefreshScope 或 @Scope(value = "prototype") * 同步刷新配置文件 */ @Scope(value = "prototype") public class OrderToMemberService { /** * 读取本地配置的文件内容 * bean对象为单例 */ @Value("${fll.address}") private String address; @GetMapping("/address") public String getAddress(){ return address; } }
@Scope(value = “prototype”) 代表原型模式:每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。
@Scope(value = “singleton”) 代表单例模式:在整个应用中,只创建bean的一个实例。
但是上述注解不推荐使用@Scope(value = “prototype”),很明显创建多个bean会占用服务器的堆内存。
-
nacos如何判断配置文件内容发生变化