SOA架构
SpringCloud和SprinvCloudAlibaba有什么区别?
SpringCloudAlibaba实际上对我们的SpringCloud2.x和1.x实现拓展组件功能。
nacos是分布式配置中心+分布式注册中心=Eureka+config。
研发SpringCloudAlibaba目的是为了推广阿里的产品,如果使用了SpringCloudAlibaba,最好使用alibaba整个体系产品。
面试题:
本地负载均衡器和 服务器负载均衡器有什么区别?
本地负载均衡器 负载均衡算法都是在本地实现
依赖注册中心
应用场景:Dubbo,feign客户端/rpc远程调用框架
框架:客户端Ribbon
服务器负载均衡器 负载均衡算法都是服务器端实现
依赖于Nginx
场景:tomcat 负载均衡
框架:nginx lvs
Nacos和Eureka区别
最大区别:Nacos支持两种模式CP/AP模式 从nacos1.0版本开始,注意:默认就说AP模式
注册中心有哪些?又有什么区别?
这里先谈到Eureka与zookeeper中的ap和cp,然后再谈到nacos的双模式
Eureka,nacos,consul,zookeeper
核心:
- Eureka与Zookeeper实现注册的区别
- Eureka与Nacos实现注册区别
CAP定律概念
一致性(C):在分布式相同中,如果服务器是集群的情况下,每个节点同一时刻查询的数据必须保存一致性的问题。
可用性(A):集群节点中,部分的节点出现了故障后任然可以使用
分区容错性(P):在分布式系统中网络存在脑裂(网络故障)的问题,部分的server与整个集群失去联系无法形成一个群体。
取舍:只有CP/AP平衡点
采用:
Cp情况下 虽然我们服务不能用,但是需要要保证数据的一致性
Ap情况下 可以短暂保证数据不一致性,但是最终可以一致性,不管怎么样,能够保证我们的服务可用。
大多数的注册中心都是使用的Ap
相同点,不同点。中心实现
相同点:
都是可用实现分布式注册中心
不同点:
zookeeper采用CP保证数据的一致性,原理是Zab原子广播协议,当我们zk主服务因为某种原因宕机的情况下,会自动重新选举一个主服务,整个选举的过程中保证数据一致性的问题,在选举的过程中整个zk环境是不可以使用的,可能断站无法使用的zk,意味着微服务采用该模式的情况下,可能无法实现通讯。(本地有缓存的除外)
Eureka采用ap的设计模式注册中心,完全去中心化思想,也就没有只从之分,每个节点都是同等的,采用相互注册原理,你中有我我中有你,只要最后一个eureka节点存在就可以保证整个微服务可用实现通讯。
我们在使用注册中心时,可用性在优先级最高,可用读取数据暂时不一致,但是至少要能够保证注册中心可用性
中心化:
集群中要有一个老大,其他服务器都要听这个老大的,如果老大死了,那么就需要重新选举老大
去中心化:
所有人都一样,没有级别之分,只要还有一个节点,那么整个微服务都可以正常使用。
Nacos与Eureka区别:
Nacos从1.0开始就支持混合型,即支持Ap也支持Cp模式,默认支采用的时Ap模式保证服务的高可用性,Cp的模式底层集群raft(选举模式)保证数据强一致性的问题。
如果我们采用Ap模式的情况下,出现网络波动任然可用实现我们注册中心注册服务列表。
那么如果选择Cp默认必须保证数据一致性问题,如果网络产生波动的情况下,是无法注册到我们的服务列表的,选择cp模式可以注册实例持久。
什么情况下选择Ap和Cp呢?
如果我们系统需要保证强一致性的问题,那么我们可以采用Cp模式,一般我们都是选择Ap模式保证高可用
BUG
调用Feign客户端没有在启动类上添加@EnableFeignClients注解
Description:
Field memberServiceFeign in com.mayikt.service.impl.order.orderServiceImpl required a bean of type 'com.mayikt.service.openfeign.MemberServiceFeign' that could not be found.
Action:
Consider defining a bean of type 'com.mayikt.service.openfeign.MemberServiceFeign' in your configuration.
Feign1.1.4客户端有bug,必须要提交post才行,解决方案:在我们feign接口方法参数上都要加@RequestParam(“对应参数名称”)
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Wed Mar 09 09:56:54 CST 2022
There was an unexpected error (type=Internal Server Error, status=500).
status 405 reading MemberServiceFeign#getUser(Integer); content: {"timestamp":"2022-03-09T01:56:54.751+0000","status":405,"error":"Method Not Allowed","message":"Request method 'POST' not supported","path":"/getUser"}
feign客户端不支持使用下划线命名的服务远程调用
java.lang.IllegalStateException: Service id not legal hostname (mykt_member)
at org.springframework.util.Assert.state(Assert.java:73) ~[spring-core-5.0.4.RELEASE.jar:5.0.4.RELEASE]
再gateway中引入spring-cloud-starter-gateway就不要再引入spring-boot-starter-web依赖,不然会出现冲突
Parameter 0 of method modifyRequestBodyGatewayFilterFactory in org.springframework.cloud.gateway.config.GatewayAutoConfiguration required a bean of type 'org.springframework.http.codec.ServerCodecConfigurer' that could not be found.
Action:
Consider defining a bean of type 'org.springframework.http.codec.ServerCodecConfigurer' in your configuration.
Nacos
分布式配置中心和分布式服务注册中
分布式服务注册中心
RPC远程调用存在问题:
-
超时时间 5s,3s
超时问题 客户端已经发送请求到服务器端,服务器端没有及时的响应请求给客户端。
防止客户端一直阻塞等待
1.1接口的超时时间与链接不上区别:
链接不上:服务器已经宕机
接口超时:客户端已经发送请求到达了服务器端:只是服务器没有及时的响应给客户端
-
安全控制
加密,https,令牌传输,限流,服务保护中心等
-
服务治理
在分布式和微服务中,服务与服务之间依赖关系非常复杂的,接口调用如果写死的情况下后期接口地址发生变化的情况下,需要人工刷新修改地址。
老式实现服务治理:使用数据库
建立一张表
Redis 缓存
ServoceId ServiceIp
admin 192.168.0.11:8099
缺点:没有真正动态化,不支持的节点扩容与缩容,以及很消耗资源
注册中心:实际上就说存放接口的调用地址
只会存放IP和端口后,接口名称一般不会存放,因为接口名称一般不会改变
接口地址:IP:端口号/接口名称
知名注册中心:注册中心:Dubbo依赖zookeeper,Eureka,Consul,Nacos,Redis,数据库
特性:能够实现动态感知。
注册中心底层实现原理
-
微服务架构常用名称
生产者:提供接口被其他服务调用
消费者:调用别人写好的接口
注册中心:存放调用接口地址和动态感知
- 我们服务启动的时候去自动去注册中心去注册
- 如果我的服务宕机了,注册中心会自动检测我们服务的心跳,进行踢出服务
-
会员启动的时候,会将该服务的接口地址注册到服务注册中心存放
key:服务名称 value:接口地址
-
服务注册中心 采用key=服务名称 value服务器接口的地址列表
Map<String,List>
key=xcwl.member value=[ip1,ip2,ip3]
-
消费者调用生产者接口的时候,从注册中心上获取接口调用地址列表并不是一个地址
key=xcwl-member value IP1,IP2,IP3
-
消费者采用负载均衡器,选择一个地址,实现本地RPC远程调用
RPC远程调用
导包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>mykt-order-2021</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>0.2.2.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
</dependencies>
</project>
RPC远程调用
package com.order;
import com.Balancing.Balancing;
import com.Balancing.PollService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.hypermedia.DiscoveredResource;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.net.URI;
import java.util.List;
@RestController
public class OrderService {
//获取Nacos服务对象
@Autowired
private DiscoveryClient discoveryClient;
//远程调用对象
@Autowired
private RestTemplate restTemplate;
//手写负载均衡算法
@Autowired
private Balancing balancing;
//使用loadBalancerClient对象实现负载均衡算法
@Autowired
private LoadBalancerClient loadBalancerClient;
//通过HttpClient对象RPC远程调用接口
@RequestMapping("getOrder")
public String getOrder(){
//通过服务名称先去注册中心获取接口列表
List<ServiceInstance> instances = discoveryClient.getInstances("mykt-member");
// //通过下标去获取注册中服务的接口
ServiceInstance serviceInstance = instances.get(0);
//通过httpclient远程调用
//先获负载均衡的服务接口地址
URI uri = serviceInstance.getUri();
//远程调用
String result = restTemplate.getForObject(uri + "/getUser", String.class);
return result;
}
//在HttpClient对象上实现负载均衡
@RequestMapping("getOrder1")
public String getOrder1(){
//通过服务名称先去注册中心获取接口列表
List<ServiceInstance> instances = discoveryClient.getInstances("mykt-member");
//然后通过负载均衡器选择接口
ServiceInstance poll = balancing.poll(instances); //轮询
// ServiceInstance random = balancing.random(instances); //随机算法
//通过httpclient远程调用
//先获负载均衡的服务接口地址
URI uri = poll.getUri();
//远程调用
String result = restTemplate.getForObject(uri + "/getUser", String.class);
return result;
}
//使用loadBalancerClient对象实现负载均衡算法
@RequestMapping("getOrder2")
public Object getOrder2(){
//使用choose自动获取对应的接口返回,并且自动实现轮询
ServiceInstance choose = loadBalancerClient.choose("mykt-member");
return restTemplate.getForObject(choose.getUri() + "/getUser", String.class);
}
}
配置文件 application.yml
server:
port: 9000
spring:
application:
name: mayikt-order #服务名称 在 注册中心展示服务名称 --
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
启动类
package com;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
本地负载均衡算法
接口
package com.Balancing;
import org.springframework.cloud.client.ServiceInstance;
import java.util.List;
//负载均衡接口
public interface Balancing {
//轮询
ServiceInstance poll(List<ServiceInstance> list);
//随机
ServiceInstance random(List<ServiceInstance> list);
}
package com.Balancing;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@Component
public class PollService implements Balancing{
//创建一个原子性对象,如果我们自己创建一个变量的话可能会出现线程安全问题
private AtomicInteger atomicInteger=new AtomicInteger(0);
//随机数对象
private static Random random=new Random();
//轮询算法
@Override
public ServiceInstance poll(List<ServiceInstance> list) {
//计数,执行一次,计数+1
int count = atomicInteger.incrementAndGet();
//通过当前计数%list的大小,获得下标
int index = count%list.size();
//通过下标拿list的元素
return list.get(index);
}
@Override
public ServiceInstance random(List<ServiceInstance> list) {
return list.get(random.nextInt(list.size()));
}
}
使用Feign客户端调用
Feign客户端自动实现 负载均衡
原理: 在我们接口的类上添加@FeignClient(“mykt-member”) 表示去对应的nacos注册中心寻找对应的服务地址
接口的抽象方法上使用@RequestMapping(“/getUser”) 表示服务的接口
最后Feign将我们的 服务的地址+接口名称拼 接就可以了.
导包
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
接口
package com.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
@FeignClient("mykt-member")
public interface UserService {
@RequestMapping("/getUser")
String getUser();
}
实现接口
package com.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 通过Feign客户端远程调用
*/
@RestController
public class UserServiceImpl {
@Autowired
private UserService userService;
@RequestMapping("/getuserfeign")
public String getUser(){
return userService.getUser();
}
}
注意: 需要在启动类上添加@EnableFeignClients注解
Nacos 分布式配置中心
什么是配置中心?
使用专门的服务器统一存放关联我们整个的微服务的配置文件,能够完全动态实现对配置文件修改,新增,是不需要重启我们的服务器。
分布式配置中心框架有哪些:
携程阿波罗(重量级),Nacos(轻量级),SpringCloud Config()没有可视化界面,disConfig
轻量级:部署,架构设计原理比较简单,学习成本也比较低;
重量级:部署,架构设计都是非常麻烦,学习成本是比较高的;
配置中心原理
-
本地一样读取我们云端分布式配置中心文件(第一次建立长连接)
-
本地一样读取到配置文件后,本地的内存和硬盘中会各存放一份
-
本地一样与分布式配置中心服务器端一直保持长连接
-
当我们配置中心文件发送变化(MD5|版本号)实现区分,然后将变化通知的结果发送给我们本地的应用,进行刷新我们的配置文件。
完全百分比实现动态化修改我们配置文件。
我们nacos的门户网站和中心服务器(接口)以及注册中心全部都整合在一起了
Nscos配置中心发布规则:
Dataid名称:默认的情况下使用 服务名称-版本.yml|properties
应用读取配置文件
添加注解
先创建一个云端配置文件
文件格式
groudid对应服务名称.文件格式
注意:配置文件格式一定要相同
配置成功后
代码
多环境配置
nacos链接数据库
假如在我们在集群的情况下怎么实现多个nacos数据同步呢?
nacos解决方案是:链接mysql数据库
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos-config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=0000
Nacos集群
- nacos集群部署有哪些原理
- nacos集群部署在不同环境下需要注意那些问题
- nacosClient如果链接我们nacos集群
- nacos与eureka,zookeeper的区别
集群搭建
在Windos情况下只能启动单机,伪集群。
在Iinux版本的中运行的时候默认是集群模式,如果需要改为单机启动,修改配置。
注意:
我们在搭建集群后,我们不需要链接数据库,也可以实现数据同步
- nacos在windos版本下运行默认是单机版本 需要指定 startup.cmd -m cluster
- nacos在linux版本下运行默认是集群版本,如果想链接单机版本 startup.cmd -m standalone
Windos
-
我们复制多个nacos文件
-
修改集群ip,并且将cluster.conf.example修改成cluster.conf
-
我们进入每个nacos中使用cmd集群运行
-
进入nacos后台
注意:
集群的ip地址不能采用127.0.0.1 集群选举是用心跳模式
分布式一致性算法raft协议
- 分布式系统中一致性协议有哪些
- 如果理解Eureka的PeerToPeer集群架构
- ZAB协议与Paxos协议实现的区别
- NacoasRaft一致性心跳的实现原理
分布式事务一致性框架与分布式系统一致性算法有哪些区别?
分布式事务一致性框架:
核心解决我们再实际系统中产生跨事务问题,核心靠的就说最终一致性:比如rockrtmq事务消息,rebbitmq补单,lcn,seata等。
分布式一致性算法
解决我们系统之间集群之后每个节点数据保持一致性。
比如:raft(nacos),zab(zookeeper),paxos等
Geteway
微服务网关是整个微服务API接口的入口,可以实现过滤API接口。
作用:就是可以实现用户的登录验证,解决跨域,日志拦截,权限控制,限流,熔断,负载均衡,黑名单和白名单机制等。
微服务中的结果模式采用前后端分离,前端调用接口地址都能够被抓包分析到。
传统的方式我们跨域使用过滤器拦截用户会话信息,这个过程所有的服务器都必须要写入验证会话登录的代码,很冗余。
过滤器适合于单个服务实现过滤请求,
网关拦截整个微服务实现过滤请求,能够解决整个微服务中的冗余代码
过滤器只能局部拦截,网关跨域全局拦截。
Zuul与GateWay有哪些区别
Zuul网关属于NetFix公司开源框架,属于第一代微服务网关
GateWay属于SpringCloud自己研发的网关框架,属于第二代微服务网关
相比来说GateWay比Zuul网关的性能要好很多。
主要:
Zuul网关底层基于Servlet实现,阻塞式api,不支持长连接 依赖SpringBoot-Web
SpringCloudGateWay基于Spring5构建,能够实现响应式非阻塞式api,支持长连接,能够更好的支持Spring体系产品,依赖SpringBoot-WabFux
搭建网关
网关一般端口号为80或者443
新建一个maven项目
依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
</dependencies>
application.yml
server:
port: 80
####服务网关名称
spring:
application:
name: mayikt-gateway
cloud:
gateway:
###路由策略
routes:
###路由id
- id: mayikt
####转发http://www.mayikt.com/ 这里一定要加/不然会报错
uri: http://www.mayikt.com/
###匹配规则
predicates:
- Path=/xcwl/**
### 127.0.0.1/xcwl 转发到http://www.mayikt.com/
项目结构
网关转发微服务接口
这里我们开启了两个mykt-member服务,然后通过网关转发,我们会发现网关也会自动帮我们做负载均衡
server:
port: 80
####服务网关名称
spring:
application:
name: mayikt-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
###路由策略
routes:
###路由id
- id: mykt-member
####转发 lb://注册中心服务名称
uri: lb://mykt-member
filters:
- StripPrefix=1
###匹配规则
predicates:
- Path=/mamber/**
### 127.0.0.1/mamber
# 运行通过注册中心获取地址
discovery:
locator:
enabled: true
网关和Nginx有哪些区别?
- 微服务网关能够做的事情,Nginx也可以实现
相同点:
都是开源实现Api的拦截,负载均衡,反向代理,请求过滤,可以完全和网关实现一样的效果
不同点
Nginx采用整个c语言编写的
再每个编程语言中,都有微服务的概念,比如我们使用java构建微服务项目,Gateway也是java语言编写的
毕竟GateWay属于我们java语言编写的,能够更好的对我们的微服务实现拓展功能,相比Nginx如果实现拓展功能的话,我们必须要要学习lua语言或者c语言,那么这样的话我们学习成本就变高了。
网关全局过滤
我们需要定义一个类并且实现GlobalFilter接口重写里面的filter方法,如果想要过滤器级别高一点,再实现Ordered 即可
package com.mayikt.filter;
import ch.qos.logback.core.status.Status;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
//实现网关拦截,需要实现GlobalFilter接口重新里面的filter方法
@Component
public class TokenGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//通过exchange获取req和resp对象,再通过getQueryParams().getFirst方法获取参数
String token = exchange.getRequest().getQueryParams().getFirst("token");
//判断当前token为空的情况
if (token==null){
//获取响应对象
ServerHttpResponse response = exchange.getResponse();
//给响应对象结果内容
String msg="token is null";
//设置响应状态号
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
//将内存转换成DataBuffer对象
DataBuffer wrap = response.bufferFactory().wrap(msg.getBytes());
//最后返回响应对象
return response.writeWith(Mono.just(wrap));
}
//如果不为空,就运行通行
return chain.filter(exchange);
}
}
网关集群
在我们微服务项目中,网关绝对不可以为单机版本,一定要做集群,不然一旦宕机了,整个微服务项目使用不了
集群可以使用Nginx实现
网关实现了集群如何当客户端访问能够网关?
使用nginx或者lvs虚拟vip技术
环境配置:
在集群环境下,网关就不能为80了,因为我们需要配合nginx
网关1 127.0.0.1:81
网关2 127.0.0.1:82
网关3 127.0.0.1:83
Nginx服务器 127.0.0.1:80
我们实现nginx给网关做负载均衡,然后网关在给我们微服务做负载均衡
- 我们在网关转发微服务的时候在请求头传递一个当前网关端口号
- 我们微服务在请求头中拿到网关端口号
网关:我们启动两个网关端口号为81和82,共nginx做负载均衡
//如果不为空,就运行通行,将请求头中存放一个当前网关的端口号
ServerHttpRequest gatewayPort = exchange.getRequest().mutate().header("gatewayPort", Port).build();
return chain.filter(exchange.mutate().request(gatewayPort).build());
微服务,也启动两个并且注册到注册中心,共网关负载均衡
@GetMapping("/get")
public String get(HttpServletRequest request){
return "我是订单服务,端口号"+port+",网关端口"+request.getHeader("gatewayPort");
}
Nginx
到这里我们一共开启了一共nginx,一个网关集群,两个服务集群
动态网关
所谓的动态网关,就是我们网关的配置存放者远程,这样我们任何配置修改的时候,网关就不需要重启。
实现思路:
- 分布式配置中心,不建议使用 阅读性差 需要定义json格式配置 阅读性差
- 基于数据库表结构设计 特别建议 阅读性比较高。
基于数据库表形式的设计
网关以及提供了api接口
- 直接新增
- 直接修改
思路:
默认加载时候
1. 当我们的网关服务启动的时候,从我们数据库查询网关的配置
1. 将数据库的内容读取到网关内存中
网关配置要更新的
伪代码:
1. 更新数据库
1. 调用网关api更新
依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>mayikt-gateway</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>0.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<!-- mysql 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 阿里巴巴数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.14</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
数据库
CREATE TABLE `mayikt_gateway` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`route_id` varchar(11) DEFAULT NULL,
`route_name` varchar(255) DEFAULT NULL,
`route_pattern` varchar(255) DEFAULT NULL,
`route_type` varchar(255) DEFAULT NULL,
`route_url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1;
pojo
package com.mayikt.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GateWay {
private int id;
private String route_id;
private String route_name;
private String route_pattern;
private String route_type;
private String route_url;
}
mapper
package com.mayikt.mapper;
import com.mayikt.pojo.GateWay;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import java.util.List;
@Component
public interface GateWayMapper {
List<GateWay> queryGateWayList();
}
mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mayikt.mapper.GateWayMapper">
<select id="queryGateWayList" resultType="gateWay" parameterType="gateWay">
select * from mayikt_gateway
</select>
</mapper>
service
package com.mayikt.service;
import com.mayikt.mapper.GateWayMapper;
import com.mayikt.pojo.GateWay;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
public class GatewayService implements ApplicationEventPublisherAware {
//mapper
@Autowired
private GateWayMapper gateWayMapper;
//请了解下Spring中事件机制:发布ApplicationEventPublisher,实现监听ApplicationEvent。结合异步操作,哎呀,真香!你值得拥有!
private ApplicationEventPublisher publisher;
//网关
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
// 重写ApplicationEventPublisherAware接口中方法
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
//链接数据库,查询出的的数据,循环写入到网关中
public void initAllRoute(){
List<GateWay> gateWays = gateWayMapper.queryGateWayList();
for (GateWay gateWay : gateWays) {
loadRoute(gateWay);
}
}
//写入网关配置方法
public String loadRoute(GateWay gateWay) {
RouteDefinition definition = new RouteDefinition();
Map<String, String> predicateParams = new HashMap<>(8);
PredicateDefinition predicate = new PredicateDefinition();
FilterDefinition filterDefinition = new FilterDefinition();
Map<String, String> filterParams = new HashMap<>(8);
// 如果配置路由type为0的话 则从注册中心获取服务
URI uri=null;
if ("0".equals(gateWay.getRoute_type())){
uri = UriComponentsBuilder.fromUriString("lb://"+gateWay.getRoute_name()).build().toUri();
}else{
uri = UriComponentsBuilder.fromHttpUrl(gateWay.getRoute_url()).build().toUri();
}
// 定义的路由唯一的id
definition.setId(gateWay.getRoute_id());
predicate.setName("Path");
//路由转发地址
predicateParams.put("pattern", gateWay.getRoute_pattern());
predicate.setArgs(predicateParams);
// 名称是固定的, 路径去前缀
filterDefinition.setName("StripPrefix");
filterParams.put("_genkey_0", "1");
filterDefinition.setArgs(filterParams);
definition.setPredicates(Arrays.asList(predicate));
definition.setFilters(Arrays.asList(filterDefinition));
definition.setUri(uri);
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
}
}
controller
package com.mayikt.controller;
import com.mayikt.service.GatewayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RouterController {
@Autowired
private GatewayService gatewayService;
@RequestMapping("/sycGateWay")
public String sycGateWay(){
try {
gatewayService.initAllRoute();
return "success";
}catch (Exception e){
return e.getMessage();
}
}
}
application.yml
server:
port: 82
####服务网关名称
spring:
# 配置数据库
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/cloud-gateway
username: root
password: "0000"
application:
name: mayikt-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
# 运行通过注册中心获取地址
discovery:
locator:
enabled: true
mybatis:
type-aliases-package: com.mayikt.pojo
mapper-locations: classpath:com/mayikt/mapper/*.xml
app
package com.mayikt;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.mayikt.mapper")
public class app {
public static void main(String[] args) {
SpringApplication.run(app.class);
}
}
源码分析
- GateWay的谓词有哪些
- GateWay的整体执行流程
- GateWay深度源码分析
- GateWay解决跨域问题
GateWay主要组成
route(路由),Predicate(谓词),Filter(过滤器)
路由:路由名称,路由转发的url,匹配规则
谓词:匹配规则
过滤器:可以过滤我们的请求,比如客户端进入网关这里我们过滤不存在token的请求
核心配置分析
源码分析
作用检查我们是否配置了webfux依赖,
org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration,
加载了我们Gateay需要注入的类(bean)
org.springframework.cloud.gateway.config.GatewayAutoConfiguration,\
根据服务名称去注册中心查询接口,并且支持负载均衡
org.springframework.cloud.gateway.config.GatewayLoadBalancerClientAutoConfiguration,\
网关整合Redis+Lua实现限流
org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration,\
服务注册与发现,将服务注册到nacos中
org.springframework.cloud.gateway.discovery.GatewayDiscoveryClientAutoConfiguration
Sentinel
服务保护有哪些方案:
- 黑名单和白名单
- 对IP实现限流,熔断机制
- 服务降级
- 服务隔离机制
服务限流
目的式为了保护我们服务,在高并发的情况下,如果客户端的请求服务器到一定的极限(阈值),请求的数据超出我们设置的阈值,开启我们的自我保护机制。直接执行我们的服务降级方法,不会执行我们业务逻辑,走本地falback方法。
限流有两种就是QPS和线程数,pqs表示每秒可以支持访问多少次,线程数表示当前接口有最大多少个线程,如果超出了阈值那么就走falback方法返回友好提示
服务降级
在高并发的情况下,为了防止用户一直等待,采用限流或者熔断机制,保护我们服务,不会执行我们的业务逻辑,走本地的falback方法,返回一个友好提示给客户端。
比如:当前排队人数过多,等稍后重试。
服务雪崩效应
默认的情况下,tomcat和jetty服务器只会有一个线程处理所有接口的请求。
这样在高并发情况下所有的请求都堆积到同一个接口上,那么会产生该服务器的所有线程都在处理该接口,可能会导致其他的接口无法访问,短暂没有线程处理。
如何去证明我们的tomcat服务器只有一个线程处理我们所有接口的请求
打印线程名称 现场名称组合:线程池名称+线程id名称。
解决方案:
服务隔离机制:
- 线程池隔离
- 信号量隔离
线程池隔离
每个接口都有自己独立的线程池维护我们的请求,每个线程互不影响,缺点:占用服务器内存非常大,如果我们有一万个接口,那么就需要开启一万个线程池,非常消耗内存资源
信号量隔离
设置最多允许我们某个接口有一定的阈值的线程数量处理我们的接口,如果超出该线程数量,则拒绝访问。
Sentinel和Hystrix区别
限流规则
依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel</artifactId>
<version>0.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
手动代码实现
package com.mayikt.service.impl.order;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.mayikt.service.openfeign.MemberServiceFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
@RestController
public class orderServiceImpl {
//限流规则名称
private static final String GETORDER_KEY = "getOrder";
//限流接口
@GetMapping("/getUser")
public String getUser(){
Entry entry=null;
try {
entry=SphU.entry("getOrder");
return "我是订单服务,端口号"+port;
}catch (Exception e){
e.printStackTrace();
return "当前访问人数过多,请稍等重试!";
}finally {
// SphU.entry(xxx) 需要与 entry.exit() 成对出现,否则会导致调用链记录异常
if (entry != null) {
entry.exit();
}
}
}
//限流配置方法,需要先执行这个方法才能配置限流
@RequestMapping("/initFlowQpsRule")
public String initFlowQpsRule() {
List<FlowRule> rules = new ArrayList<FlowRule>();
FlowRule rule1 = new FlowRule();
rule1.setResource(GETORDER_KEY);
// QPS控制在1以内
rule1.setCount(1);
// QPS限流
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule1.setLimitApp("default");
rules.add(rule1);
FlowRuleManager.loadRules(rules);
return "....限流配置初始化成功..";
}
}
启动自动执行规则方法
实现ApplicationRunner接口重写里面的run方法即可
package com.mayikt.service.config;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
@Slf4j
public class SentinelApplicationRunner implements ApplicationRunner {
private static final String GETORDER_KEY = "getOrder";
@Override
public void run(ApplicationArguments args) throws Exception {
List<FlowRule> rules = new ArrayList<FlowRule>();
FlowRule rule1 = new FlowRule();
rule1.setResource(GETORDER_KEY);
// QPS控制在1以内
rule1.setCount(1);
// QPS限流
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule1.setLimitApp("default");
rules.add(rule1);
FlowRuleManager.loadRules(rules);
log.info("限流启动成功!");
}
}
注解实现
缺点:没有办法修改限流的大小
package com.mayikt.service.impl.order;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.mayikt.service.openfeign.MemberServiceFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
@RestController
public class orderServiceImpl {
//规则名称
private static final String GETORDER_KEY="getOrder";
@SentinelResource(value = GETORDER_KEY,blockHandler ="fallback")
@RequestMapping("/order")
public String Order(){
return "订单服务哦";
}
//限流后的提示方法
public String fallback(BlockException e){
e.printStackTrace();
return "接口限流了";
}
}
可视化平台
Sentinel 环境快速搭建
下载对应Sentinel-Dashboard
https://github.com/alibaba/Sentinel/releases/tag/1.7.1 运行即可。
运行执行命令
java -Dserver.port=8718 -Dcsp.sentinel.dashboard.server=localhost:8718 -Dproject.name=sentinel-dashboard -Dcsp.sentinel.api.port=8719 -jar
8718属于 界面端口号 8719 属于api通讯的端口号
浏览器打开登录
登录进入:
SpringBoot链接
注意:
如果我们的接口没有使用@SentinelResource注解,我们添加限流规则的时候适用接口的url地址做名称,如果使用的注解,那么就使用value中的值做名称
没有加注解,流控名称对应接口名称,不加注解没办法执行返回内容
加了注解,可以自定义限流名称和自定义返回信息
sentienl限流的规则默认情况下式没有持久化的,如果需要持久化就需要配合zookeeper,nacos,携程阿波罗等!
QPS,线程数
QPS,每秒可以访问多少次
QPS限流
线程数,信号隔离,就表示当前接口有多少个线程在执行
线程限流
Sentinel持久化
其实我们服务里面定义的规则都是保存在服务的内存中,而sentinel可视化平台读取的就是我们服务内存中的规则展现出来,然而我们服务关闭后,sentinel可视化平台就读取不到我们服务了,那么就显示不了我们的服务和规则了。
sentinel规则是保存在我们服务的内存中,我们服务重启后就全部没有了。
所以我们要整合nacos的分布式配置中心,nacos是可以绑定我们数据库做持久化的,在我们服务中绑定nacos配置中心,让我们服务读取云端规则保存在内存,然后sentinel可视化平台在读取我们服务内存的规则展现出来。
操作流程
- 依赖
<!--sentinel 整合nacos配置中心 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.5.2</version>
</dependency>
- 在nacos上编写配置文件
resource:资源名,即限流规则的作用对象
limitApp:流控针对的调用来源,若为 default 则不区分调用来源
grade:限流阈值类型(QPS 或并发线程数);0代表根据并发数量来限流,1代表根据QPS来进行流量控制
count:限流阈值
strategy:调用关系限流策略
controlBehavior:流量控制效果(直接拒绝、Warm Up、匀速排队)
clusterMode:是否为集群模式
- 在服务中配置
# sentinel整合nacos配置中心
datasource:
ds:
nacos:
### nacos连接地址
server-addr: 127.0.0.1:8848
## nacos连接的分组
group-id: DEFAULT_GROUP
###路由存储规则
rule-type: flow
### 读取配置文件的 data-id
data-id: mayikt-order-sentienl
### 读取培训文件类型为json
data-type: json
-
启动项目在sentinel可视化平台查看
注意:
我们服务在使用nacos配置中心的时候,本地代码禁止启动类上添加规则
否者就会配置文件就会失效,获取不到。
熔断降级
降级和熔断是配合使用的,现有熔断才有降级的。当接口熔断后才会进行降级走本地fallback方法
熔断
类似于保险丝,如果超出了我们的阈值的情况下,在一定的时间内不会执行我们的业务逻辑直接执行我们的服务降级的方法。
服务降级:走本地fallback方法,返回一个有好的提示给客户端,不会真实的执行我们的业务逻辑
服务降级策略
- rt(平均响应时间)
- 异常比例
- 异常次数(当我们的接口一直报异常,那么就会走我们的服务降级)
RT平均响应时间 单位为秒
假如我当前方法执行需要300毫秒,但是我配置了降级策略,配置当前接口在1秒内访问5次平均执行时间超过10毫秒,那么就会触发熔断机制走降级的本地rallback方法,持续60秒
异常比例,单位为秒
假如当我服务接口1秒内访问5次出现异常比例大于我们配置的比例那么就会触发熔断和执行降级策略走本地的fallbock方法并且在规定的时间内无法访问
异常比例 单位为分钟,可以配合异常比例使用
假如我们当前接口一分钟内出现5次异常,那么就执行触发熔断和执行降级策略走本地的fallbock方法并且在规定的时间内无法访问
跨域问题
package com.xcwl.config;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class CrossOriginFilter implements GlobalFilter {
//解决跨域问题
/**
*最重要的就是添加响应头为*全部接口
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
HttpHeaders headers = exchange.getResponse().getHeaders();
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS,"true");
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,"*");
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS,"POST,GET,PUT,DELETE");
headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS,"*");
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,"*");
return chain.filter(exchange);
}
}