1.微服务与微服务架构
微服务
强调的是服务的大小,它关注的是某一个点,是具体解决某一个问题/提供落地对应服务的一个服务应用,
狭意的看,可以看作Eclipse里面的一个个微服务工程/或者Module
微服务架构
微服务架构是⼀种架构模式,它提倡将单⼀应⽤程序划分成⼀组⼩的服务,服务之间互相协调、互相配合,为⽤户提供最终价值。每个服务运⾏在其独⽴的进程中,服务与服务间采⽤轻量级的通信机制互相协作(通常是基于HTTP协议的RESTful API)。每个服务都围绕着具体业务进⾏构建,并且能够被独⽴的部署到⽣产环境、类⽣产环境等。另外,应当尽量避免统⼀的、集中式的服务管理机制,对具体的⼀个服务⽽⾔,应根据业务上下⽂,选择合适的语⾔、⼯具对其进⾏构建。
2.什么是Spring cloud alibaba
Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。 依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里分布式应用解决方案,通过阿里中间件来迅速搭建分布式应用系统
3.为什么要使用spring cloud alibaba
很多人可能会问,有了spring cloud这个微服务的框架,为什么又要使用spring cloud alibaba这个框架了?最重要的原因在于spring cloud中的几乎所有的组件都使用Netflix公司的产品,然后在其基础上做了一层封装。然而Netflix的服务发现组件Eureka已经停止更新,我们公司在使用的时候就发现过其一个细小的Bug;而其他的众多组件预计会在明年(即2020年)停止维护。所以急需其他的一些替代产品,也就是spring cloud alibaba,目前正处于蓬勃发展的态式。
4.注册中心nacos
nacos是阿里巴巴研发的一个集注册中心与配置中心于一体的管理平台,使用其他非常的简单。下载地址为:https://github.com/alibaba/nacos/releases
其中默认的登录名和密码是:nacos/nacos
4.1 nacos的用户名和密码修改
1.分别执行conf目录下的nacos-mysql.sql两个脚本文件
2. 重新配置密码
新建一个springboot项目,导入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
生成密码的代码
public static void main(String[] args) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
// 生成的密码为:$2a$10$04MGTL.cJNZPpR3rFt/I2.43F.V75NH.5wdK.jngaO9Mc91mfonAO
System.out.println(bCryptPasswordEncoder.encode("1"));
}
向数据库中插入相关的用户名和密码即可
3.配置数据库的连接
在conf目录下的application.properties目录下加入如下内容:
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://mysql:3306/cloud-alibaba?useSSL=false&serverTimezone=UTC&serverTimezone=Asia/Shanghai
db.user=root
db.password=
4.2 nacos集群配置
A. 将conf目录下的cluster.conf.example拷贝一份重命名为cluster.conf,在文件中加入所有集群节点的ip和端口号,文件内容如下:
127.0.0.1:8848
127.0.0.1:8849
B. 修改windows启动文件 startup.cmd
的配置,修改内容如下:
set MODE="standalone" #默认的配置
set MODE="cluster" #修改后的内容
注:如果是Linux环境不用作任何的修改。
C.启动两个nacos,界面中出现如下的内容,表示集群配置成功
4.3 nacos的应用
服务的提供方
jar包依赖
<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>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>0.9.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
application.yml配置文件
server:
port: 7071
spring:
application:
# 注册到注册中心的服务名
name: microservice-provider
cloud:
nacos:
discovery:
# 开启nacos的服务发现
enabled: true
server-addr: 192.168.176.1:8848,192.168.176.1:8849
启动服务的提供方
服务页面结果
服务的消费方
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-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>0.9.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
application.yml配置
server:
port: 8080
spring:
application:
# 服务名
name: alibaba-consumer
cloud:
nacos:
discovery:
server-addr: 172.18.96.177:8848,172.18.96.177:9948
# 不将自己的服务注册到注册中心
register-enabled: false
启动服务的消费方
服务页面结果
调用服务提供方的服务
@RequestMapping
public Object getUsers() {
List<ServiceInstance> list = discoveryClient.getInstances("alibaba-provider");
String targetUrl = list.stream().map(si -> si.getUri() + "/user").findFirst().get();
List<String> resultList = restTemplate.getForObject(targetUrl, List.class);
return resultList;
}
5.Ribbon负载均衡
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。其主要功能是提供客户端的负载均衡算法,并提供了完善的配置项如连接超时,重试等。简单的说,就是配置文件中列出Load Balancer后面所有的机器,Ribbon会自动的基于某种规则(如简单轮询,随机连接等)去连接这些机器,当然我们也可以使用Ribbon自定义负载均衡算法。
实现负载均衡
Ribbon只是一个客户端的负载均衡器工具,实现起来非常的简单,我们只需要在注入RestTemplate的bean上加上@LoadBalanced就可以了。如下
@Configuration
public class WebConfig {
**@LoadBalanced**
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
服务消费方的调用
// 直接写上服务名即可
List<String> resultList = restTemplate.getForObject("http://alibaba-provider/user", List.class);
负载均衡策略
Ribbon提供了一个很重要的接口叫做IRule,其中定义了很多的负载均衡策略,默认的是轮询的方式
改变Ribbon的负责负载均衡
@Bean
public IRule getRule() {
return new RandomRule();
}
自定义负载均衡策略
我们自定义的负载均衡策略需要继承AbstractLoadBalancerRule这个类,然后重写choose方法,然后将其注入到容器中。
将自定义负载均衡策略注入到容器中
@Configuration
public class ConsumerConfig {
@LoadBalanced //负载均衡
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
// 把自定义的负载均衡添加到容器中
@Bean
public IRule iRule(){
return new CustomizeRule();
}
}
自定义负载均衡
/**
* 自定义负载均衡策略
* 自定义负载均衡策略需要继承AbstractLoadBalancerRule这个类 重写choose方法 然后将其注入到容器中
*/
public class CustomizeRule extends AbstractLoadBalancerRule{
//定义每个服务可以接受请求的次数
private Integer limit=3;
/**
* map中的key是服务对应的名字 value是服务所对应的次数
*/
Map<String, ServerInfo> map=new ConcurrentHashMap<>();
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
/**
*返回值的意思是:当前方法的返回什么的时候,那么Ribbon或者feign就调用谁
* @param ket
* @return
*/
@Override
public Server choose(Object ket) {
//用来接收最后要返回的Server
Server finalServer=null;
ILoadBalancer iLoadBalancer=getLoadBalancer();
//获取所有的服务
List<Server> servers=iLoadBalancer.getAllServers();
//获取所有可用的服务
List<Server> serverList=iLoadBalancer.getReachableServers();
//获取所有服务的长度
int allServerLength=servers.size();
//获取可用服务的长度
int upCount=serverList.size();
//如果所有服务的长度和所有可用的长度都为0的时候,直接返回null
if (allServerLength==0||upCount==0){
return null;
}
for (int i=0;i<allServerLength;i++){
//获取当前的服务
Server server=servers.get(i);
//获取服务名
String instanceId=server.getMetaInfo().getInstanceId();
String serverName=instanceId.split("@@")[1];
//获取对应的服务
ServerInfo serverInfo=map.get(serverName);
//第一次调用
if (serverInfo==null){
serverInfo=new ServerInfo(server,1);
// serverInfo.setServer(server);
// serverInfo.setNum(1);
map.put(serverName,serverInfo);
finalServer=server;
break;
}else { //不是第一次调用,表示之前用服务调用过
//判断当前遍历的服务与正在调用的服务是同一个服务
if (server.getId().equals(serverInfo.getServer().getId())){
/**
* 1.如果服务调用的次数还没有达到次数,就继续走这个服务
* 2.如果服务调用的次数达到次数,就进行下一次服务调用 (这里需要判断是否有下一个服务)
*/
int num=serverInfo.getNum();
//表示当前服务调用的次数还没有到达
if (num<limit){
serverInfo.setNum(++num);
finalServer=server;
break;
}else { //表示当前的服务的调用次数已经到达次数
//判断是否有下一个服务
if (i==allServerLength-1){ //表示这个服务已经是最后一个服务了
//那么从第一个服务
Server newServer=servers.get(0);
ServerInfo firstServer=new ServerInfo(newServer,1);
map.put(serverName,firstServer);
finalServer=newServer;
}else{ //不是最后一个服务
Server nextServer=servers.get(i+1);
ServerInfo nextServerInfo=new ServerInfo(nextServer,1);
map.put(serverName,nextServerInfo);
finalServer=nextServer;
}
break;
}
}
}
}
return finalServer;
}
}
6.Feign负载均衡
feign是基于Ribbon的另外一个负载均衡的客户端框架,只需要在接口上定义要调用的服务名即可,使用起来非常的简单
pom.xml依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
启动类配置
@SpringBootApplication
@EnableFeignClients // 需要加该注解 开启Feign负载均衡
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
服务接口配置
@Service
@FeignClient(value = "microservice-provider",fallbackFactory = UserServiceFallback.class)
public interface UserService {
@GetMapping("/user")
ResponseData getAll();
@GetMapping("/user/info")
ResponseData getUserByInfo(@RequestParam Integer id, @RequestParam String name);
@GetMapping("/user/{id}")
ResponseData getById(@PathVariable Integer id);
@PutMapping("/user")
ResponseData updateUser(@RequestParam Integer id,@RequestParam String name);
@PostMapping("/user")
ResponseData addUser(User user);
@DeleteMapping("/user/{id}")
ResponseData deleteUser(@PathVariable Integer id);
}
7.熔断和服务降级
分布式系统中一个微服务需要依赖于很多的其他的服务,那么服务就会不可避免的失败。例如A服务依赖于B、C、D等很多的服务,当B服务不可用的时候,会一直阻塞或者异常,更不会去调用C服务和D服务。同时假设有其他的服务也依赖于B服务,也会碰到同样的问题,这就及有可能导致雪崩效应。
如下案例:一个用户通过通过web容器访问应用,他要先后调用A、H、I、P四个模块,一切看着都很美好。
由于某些原因,导致I服务不可用,与此同时我们没有快速处理,会导致该用户一直处于阻塞状态。
当其他用户做同样的请求,也会面临着同样的问题,tomcat支持的最大并发数是有限的,资源都是有限的,将整个服务器拖垮都是有可能的
Sentinel是一个用于分布式系统的延迟和容错的开源库,在分布式系统中,许多依赖会不可避免的调用失败,例如超时,异常等,Sentinel能保证在一个依赖出现问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
断路器本身是一种开关装置,当某个服务单元发生故障后,通过断路器的故障监控(类似于保险丝),向调用者返回符合预期的,可处理的备选响应,而不是长时间的等待或者抛出无法处理的异常,这样就保证了服务调用的线程不会被长时间,不必要的占用,从而避免故障在分布式系统中的蔓延,乃至雪崩。
Sentinel在网络依赖服务出现高延迟或者失败时,为系统提供保护和控制;可以进行快速失败,缩短延迟等待时间;提供失败回退(Fallback)和相对优雅的服务降级机制;提供有效的服务容错监控、报警和运维控制手段。
下载地址:https://github.com/alibaba/Sentinel/releases
jar包依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
**通常情况下我们只需要在服务提供方实现熔断或者服务降级即可,但是如果要相对服务消费方是实现限流,在服务的提供方和消费方都需要加如下配置。**
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8081
port: 8719
sentinel的控制面板讲解
实时监控
用于查看接口调用的QPS(Query Per Second)以及平均响应时间。
簇点链路
查看当前追踪的所有的访问接口,可以添加流量规则
、降级规则
、热点规则
、授权规则
。
流量规则
资源名:**是需要控制的链路的名字,例如/student/all等
针对来源: 默认为default表示所有,也可以针对特定的服务进行设置。
阈值类型: 是指如何进行限制,可以是QPS,也可以是线程。
单机阈值: 是控制QPS或者线程的数量。
流量模式: 直接表示只是针对指定资源进行限制;关联是指当被关联的资源达到阈值时候,指定资源被限制访问;链路是更加细粒度的控制,控制指定资源对链路的限制。
流控效果: 快速失败是指,当无法访问的时候立即给用户一个错误响应;Warm Up(预热)是指经过指定的时间后才达到指定的阈值(sentinel内有值为 coldFactor
为 3,即请求 QPS 从 threshold / 3
开始,经预热时长逐渐升至设定的 QPS 阈值,参考地址:https://github.com/alibaba/Sentinel/wiki/限流---冷启动);排队等待是指匀速的通过(每秒指定的QPS),其他的请求进行排队,但是并不会一直排下去,超过指定的时间就会失败。阈值类型必须设置为QPS,不能为线程。
降级规则
降级参数介绍
资源名 要实现降级的资源
RT(平均响应时间) 如果在一秒钟之内进入的请求的平均响应时间大于1ms,那么在未来5s钟之内所有的请求都会熔断降级。
异常比例 如果在一秒钟之内的请求数异常比例大于指定的数据,那么在未来的时间窗口内会一直熔断降级。统计单位为s.
异常数 如果在一分钟之内,异常数量大于指定的值,那么在指定的时间窗口内请求一直会熔断降级,注意时间窗口的值一般设置要大于60,因为设置如果小于60,可能会一直处于熔断状态。
热点规则
热点规则是针对具体的请求参数进行设置
@RequestMapping("/edit")
@SentinelResource("edit") //必须的有
public Object edit(@RequestParam(required = false) String id,
@RequestParam(required = false) Integer age) {
return this.studentService.commons();
}
资源名: 是@SentinelResource中设置的值
参数索引: 对那个参数进行QPS限制,通过索引来指定。
单机阈值: 指定在统计时长内的阈值。
统计窗口时长: 统计的QPS的时间。
授权规则
授权规则是指可以将特定的访问应用加入黑名单或者白名单,但是必须在访问的时候携带应用的名称。
@Component
public class SentinelOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
String origin = httpServletRequest.getParameter("origin");
if(StringUtil.isBlank(origin)) {
throw new IllegalArgumentException("origin parameter must be specified.");
}
return origin;
}
}
加上了来源解析后,在往后的访问中必须要携带origin参数
@SentinelResource
@SentinelResource是sentinel中非常重要的注解,提供了简单易用的功能。其中blockHandler注解是限流的处理方法,fallback是服务降级的处理方法。
public class FeignUserFallback {
public static Object getAllHandler(BlockException ex) {
Map<String, Object> map = new HashMap<>();
map.put("code", -1);
map.put("msg","手速太快,跟不上");
return map;
}
public Object getAllFallBack(BlockException e){
Map<String,Object> map=new HashMap<>();
map.put("code",-2);
map.put("msg","fallback错误");
return map;
}
}
/**
* 如果没有blockHandler,fallback无论是违反了什么规则,都执行fallback,但是无法区别它是什么规则
* blockHandler和fallback如果都配置,优先执行blockHandler,可以区分是什么规则
* blockHandler是将服务降级的方法与目标方法在同一个类中。如果不同的类中,有些方法需要使用相同的服务降级,
* 就可以使用blockHandlerClass来定义个一类,然后通过blockHandler来指定对应的降级的方法。方法必须是静态
* fallback可以处理非 sentinel 的异常。
*/
// @SentinelResource(value ="getAll",blockHandlerClass=FeignUserFallback.class,blockHandler="getAllHandler")
// @SentinelResource(value = "getAll",fallbackClass = FeignUserFallback.class,fallback = "getAllFallBack") 不可以?
@RequestMapping
//要求方法的类型和出现异常的类型要保持一致
public Object getAll(){ //true
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return userService.getAll();
}
8 Feign与sentinel的整合
yml文件的配置
feign:
sentinel:
# 默认是没有提示的
enabled: true
服务降级后的处理
可以在@FeignClient中配置fallback,来指定服务降级后给用户返回的什么样的数据,fallback的值为Class类型的对象,该类必须要实现该对应的接口。
@FeignClient(name="alibaba-provider", fallback = UserServiceFallback.class)
public interface UserService {
@RequestMapping("/user")
public List<String> getUsers();
}
对于fallback来讲,并不能追踪到异常信息,在实际的业务处理过程中,我们往往需要记录异常的信息,那么就要使用fallbackFactory属性来实现。
@Service
@FeignClient(value = "microservice-provider",fallbackFactory = UserServiceFallback.class)
public interface UserService {
@GetMapping("/user")
ResponseData getAll();
@GetMapping("/user/info")
ResponseData getUserByInfo(@RequestParam Integer id, @RequestParam String name);
}
}
@Component
@Slf4j
public class UserServiceFallback implements FallbackFactory<UserService> {
@Override
public UserService create(Throwable throwable) {
return new UserService(){
public ResponseData getData(){
ResponseData responseData=new ResponseData();
responseData.setCode(-1);
responseData.setMag("服务降级");
return responseData;
}
@Override
public ResponseData getAll() {
log.error("查询全部用户出现了异常:"+throwable.getMessage());
return getData();
}
@Override
public ResponseData getUserByInfo(Integer id, String name) {
log.error("根据条件查询用户出现了异常:"+throwable.getMessage());
return getData();
}
@Override
public ResponseData getById(Integer id) {
log.error("根胡用户Id查询用户出现了异常:"+throwable.getMessage());
return getData();
}
@Override
public ResponseData updateUser(Integer id, String name) {
log.error("更新用户出现了异常:"+throwable.getMessage());
return getData();
}
@Override
public ResponseData addUser(User user) {
log.error("添加用户出现了异常:"+throwable.getMessage());
return getData();
}
@Override
public ResponseData deleteUser(Integer id) {
log.error("删除用户出现了异常:"+throwable.getMessage());
return getData();
}
};
}
}
自定义注解@IgnoreWrapper 对数据进行二次封装
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreWrapper {
}
@ControllerAdvice
public class ResponseWrapperAdvisor implements ResponseBodyAdvice {
/**
* 如果该方法返回true 那么beforeBodyWrite才会执行
* @param returnType
* @param converterType
* @return
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
//如果注解加到类上 整个类志杰不进行封装
IgnoreWrapper ignoreWrapper=returnType.getMethod().getDeclaringClass().getAnnotation(IgnoreWrapper.class);
if (ignoreWrapper!=null){
return false;
}
ignoreWrapper=returnType.getMethodAnnotation(IgnoreWrapper.class);
//判断方法上是否有IgnoreWrapper注解 如果没有,则进行封装
boolean flag=ignoreWrapper==null;
System.out.println("flag:"+flag);
return flag;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//获取返回值类型
Class<?> clazz=returnType.getMethod().getReturnType();
//返回值类型为void或者ResponseData,则不进行封装
if (clazz==void.class|| ResponseData.class==clazz){
return body;
}
ResponseData responseData=new ResponseData();
responseData.setCode(1);
responseData.setMag("success");
responseData.setData(body);
return responseData;
}
}