【Java开发岗:Spring Cloud篇】

点击:【第一章:Java开发岗:基础篇

计算机基础问题、HashMap、Fail-safe机制/Fail-fast机制、Synchronized、ThreadLocal、AQS、线程池、JVM内存模型、内存屏障、class文件结构、类加载 机制、双亲委派、垃圾回收算法、垃圾回收器、空间分配担保策略、安全点、JIT技术、可达性分析、强软弱虚引用、gc的过程、三色标记、跨代引用、 逃逸分析、 内存泄漏与溢出、JVM线上调优、CPU飙高系统反应慢怎么排查。

点击:【第二章:Java开发岗:MySQL篇

隔离级别、ACID底层实现原理、 一致性非锁定读(MVCC的原理)、BufferPool缓存机制、filesort过程、 离散读、ICP优化、全文检索、 行锁、表锁、间隙锁、死锁、主键自增长实现原理、索引数据结构、SQL优化、索引失效的几种情况、聚集索引、辅助索引、覆盖索引、联合索引、redo log、bin log、undolog、分布式事务、SQL的执行流程、重做日志刷盘策略、有MySQL调优、分库分表、主从复制、读写分离、高可用。

点击:【第三章:Java开发岗:Redis篇

多路复用模式、单线程模型、简单字符串、链表、字典、跳跃表、压缩列表、encoding属性编码、持久化、布隆过滤器、分布式寻址算法、过期策略、内存淘汰策略 、Redis与数据库的数据一致性、Redis分布式锁、热点数据缓存、哨兵模式、集群模式、多级缓存架构、并发竞争、主从架构、集群架构及高可用、缓存雪崩、 缓存穿透、缓存失效。

点击:【第四章:Java开发岗:MQ篇

RabbitMQ、RockerMQ、Kafka 三种消息中间件出现的消息可靠投递、消息丢失、消息顺序性、消息延迟、过期失效、消息队列满了、消息高可用等问题的解决方案。RabbitMQ的工作模式,RocketMQ的消息类型,Kafka消费模式、主题/分区/日志、核心总控制器以及它的选举机制、Partition副本选举Leader机制、消费者消费消息的offset记录机制、消费者Rebalance机制、Rebalance分区分配策略、Rebalance过程、 producer发布消息机制、HW与LEO、日志分段存储、十亿消息数据线上环境规划、JVM参数设置。

点击:【第五章:Java开发岗:Spring篇

SpringBean生命周期、Spring循环依赖、Spring容器启动执行流程、Spring事务底层实现原理、Spring IOC容器加载过程、Spring AOP底层实现原理、Spring的自动装配、Spring Boot自动装配、Spring Boot启动过程、SpringMVC执行流程、Mybatis的缓存机制。

点击:【第六章:Java开发岗:SpringCloud篇

微服务构建、客户端负载均衡、服务治理、服务容错保护、声明式服务调用、API网关服务、分布式配置中心、消息总线、消息驱动、分布式服务追踪、分布式事务、流量控制。

点击:【第七章:Java开发岗:项目篇

简历上面的项目经历怎么写(项目介绍、负责模块、使用技术),面试项目实战(秒杀下单设计、权限设计、红包雨设计)


系列文章:每篇文章字数都是大几万,保证质量,文章以备战面试为背景,薪资参考坐标:上海;每个地方,每个时间段薪资待遇都不一样,文章仅做面试参考,具体能否谈到理想的薪资取决于面试表现、平时的积累、市场行情、机遇。
提示:系列文章还未全部完成,后续的文章,会慢慢补充进去的。

文章目录

这里总结一下35k的Java开发岗需要掌握的面试题,帮助大家快速复习,突破面试瓶颈。本章主讲SpringCloud知识点,知识点有:微服务构建、客户端负载均衡、服务治理、服务容错保护、声明式服务调用、API网关服务、分布式配置中心、消息总线、消息驱动、分布式服务追踪。大致估算可以讲三小时左右,作为备战面试的Spring相关知识点还是很不错的。35k薪资参考的坐标:上海,参考时间:2022年8月。

微服务构建

微服务架构设计原则

一般来说,在设计微服务体系结构的时候,遵循业务边界的概念,按照业务进行拆分、同时隐藏实现细节、把内容组件化模块化、可伸缩性可扩展性要求较高、并且可以实现隔离应用故障,避免整体系统不可用、要求独立部署,持续交付。

对于用户激增、并发量较高、数据量较大还得考虑:

  • 水平复制因素,就是单个服务多运行几个实例。
  • 数据分区因素,按照用户区域进行数据分区,比如北京、上海、广州等多建几个集群。
  • 并发请求因素,如果产品数量多,用户群体多的情况下,使用多级缓存处理高并发,数据量小,数据访问很高,适合存储热点数据的,可以用Nignx缓存、数据量一般,访问量大,比较热门的数据,比如首页这种的,可以使用本地缓存、数据量很大,访问量一般,比如一般的商品这种,可以使用Redis缓存。产品数量不多,用户群体大,可以考虑将页面静态化,把页面放到CDN里面,图片放到云存储里面。

微服务构建技术选型

一般来说,利用Spring Boot快速构建应用,利用Spring Cloud Alibaba Nacos实现动态服务发现、服务配置管理、服务及流量管理,利用Open feign实现与其他系统进行交互,利用Hystrix 实现熔断和错误处理,利用Ribbon实现客户端负载均衡,利用 Nginx 实现服务端负载均衡,利用 Gateway管理外部系统访问、利用Spring Security Oauth2作为权限框架进行请求校验,权限拦截、利用Seata作为分布式事务组件、利用Zipkin/Skywalking作为链路追踪、利用Sentinel作为服务降级、使用arthas/VM作为Java诊断工具、引入swagger作为在线文档、使用Redis作为分布式缓存、使用MySQL作为关系型数据库、使用MongoDB作为非关系型数据库、使用ElasticSearch作为全文搜索、有大数据的情况下使用Spark或者炎凰数仓进行读时建模。

前后端分离架构

对于一些老的项目或者特定业务的项目可能还是没有分离前后端,不过目前主流基本都是前后端分离,前后端交互更清晰,就剩下了接口模型,后端的接口更简洁明了,更容易维护,前端多渠道集成场景更容易。

后端采用统一的数据模型,支持多个前端,比如:H5前端、PC前端、安卓前端、IOS前端。

对于请求方式,比如GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源,当然也需要根据自己项目的实际情况出发,请求方式尽量统一起来。另外需要考虑后期接口参数是否会新增,如果后期参数不确定,尽量使用POST,方便后期扩展参数。

对于返回响应的实体类,后端响应统一起来,不能每个后端都用自己的响应实体类,这样前端会炸的。另外对于后端抛出来的错也需要拦截封装,给到前端一个友好的响应,而不是直接抛出去,这样前端那边的感知不友好。

对于前端请求,一般会携带几个校验参数放到请求头中,比如:App私钥(前后端约定好)、时间戳、Token令牌、校验码等等,后端一般会在网关服务或者权限服务里面,将更新数据的请求,比如POST,PUT,DELETE请求方式,对这些请求进行校验,请求头里App私钥、时间戳、Token令牌通过某种运算或者算法计算出结果,最后通过加密的方式加密这个结果,作为最终的校验码,对比前后端的校验码是否一致,判断这个请求是否合法、请求的Token是否过期/失效。

项目部署安全方面

对于项目系统部署安全方面,建议改私有网络+堡垒机+密码复杂度,建立锁机机制使用第三方知名云厂商托管数据库,降低运维复杂度,将项目部署在VPC私网内,使用cloudwatch Log+lambda+Network Firewall服务,检查mysql连续登录失败的IP次数,触发lambda执行脚本更新Network Firewall规则,禁用该IP访问。出于安全考虑,服务器只允许通过堡垒机进行运维。在没有提供安全访问策略表的情况下,除了被堡垒机访问之外,所有虚拟机无法访问任何主机,也无法被任何主机访问。
项目架构

客户端负载均衡

负载均衡(服务端/客户端)

一般我们所说的负载均衡通常都是服务器端负载均衡,服务器端负载均衡又分为两种,一种是硬件负载均衡,还有一种是软件负载均衡。

硬件负载均衡主要通过在服务器节点之前安装专门用于负载均衡的设备,常见的如:F5。

软件负载均衡则主要是在服务器上安装一些具有负载均衡功能的软件来完成请求分发进而实现负载均衡,常见的如:LVS 、 Nginx 。

微服务为负载均衡的实现提供了另外一种思路:把负载均衡的功能以库的方式集成到服务的消费方,不再是由一台指定的负载均衡设备集中提供。这种方案称为软负载均衡客户端负载均衡。常见的如:Spring Cloud中的 Ribbon。

当我们将Ribbon和Eureka一起使用时,Ribbon会到Eureka注册中心去获取服务端列表,然后进行轮询访问以到达负载均衡的作用,客户端负载均衡也需要心跳机制去维护服务端清单的有效性,当然这个过程需要配合服务注册中心一起完成。

实现Ribbon

底层实现

Ribbon的负载均衡,主要通过LoadBalancerClient来实现的,而LoadBalancerClient具体交给了ILoadBalancer来处理,ILoadBalancer通过配置IRule、IPing等信息,并向EurekaClient获取注册列表的信息,并默认10秒一次向EurekaClient发送“ping”,进而检查是否更新服务列表,最后,得到注册列表后,ILoadBalancer根据IRule的策略进行负载均衡。

而RestTemplate 被@LoadBalance注解后,能过用负载均衡,主要是维护了一个被@LoadBalance注解的RestTemplate列表,并给列表中的RestTemplate添加拦截器,进而交给负载均衡器去处理。

快速集成

在 pom.xml 文件中引入依赖

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.6.RELEASE</version>
	</parent>
 
	<properties>
		<spring-cloud.version>Finchley.SR2</spring-cloud.version>
	</properties>
 
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- Eureka-Client 依赖 -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
	</dependencies>
 
	<dependencyManagement>
		<dependencies>
			<!-- SpringCloud 版本控制依赖 -->
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

然后在启动类里面向Spring容器中注入一个带有@LoadBalanced注解的RestTemplate Bean

import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
 
@SpringBootApplication
@EnableEurekaClient
public class MessageCenterApplication {
 
	@Bean
	@LoadBalanced
	public RestTemplate restTemplate() {
		return new RestTemplate();
	}
	public static void main(String[] args) {
		new SpringApplicationBuilder(MessageCenterApplication.class).web(WebApplicationType.SERVLET).run(args);
	}
}

调用那些需要做负载均衡的服务时,用上面注入的RestTemplate Bean进行调用就可以了

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
 
@RestController
@RequestMapping("/api/v1/center")
public class MessageCenterController {
 
	@Autowired
	private RestTemplate restTemplate;
 
	@GetMapping("/msg/get")
	public Object getMsg() {
 
		String msg = restTemplate.getForObject("http://message-service/api/v1/msg/get", String.class);
		return msg;
 
	}
}

在application.yml配置文件里添加好配置,比如:

spring:
  application:
    name: message-service
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

启动的时候,Run As --> Run Configurations->VM arguments,分别使用 8771、8772、8773 三个端口各启动一个MessageApplication应用。

-Dserver.port=8771
-Dserver.port=8772
-Dserver.port=8773

三个服务启动完成后,浏览器输入:http://localhost:8761/
在这里插入图片描述
应用启动之后,连续三次请求地址 http://localhost:8781/api/v1/center/msg/get
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

Ribbon负载均衡策略

  • RoundRobinRule: 轮询策略,Ribbon以轮询的方式选择服务器,这个是默认值。启动的服务会被循环访问。

  • RandomRule: 随机策略,也就是说Ribbon会随机从服务器列表中选择一个进行访问。

  • BestAvailableRule: 最大可用策略,先过滤出故障服务器后,选择一个当前并发请求数最小的。

  • WeightedResponseTimeRule: 带有加权的轮询策略,对各个服务器响应时间进行加权处理,然后在采用轮询的方式来获取相应的服务器。

  • AvailabilityFilteringRule: 可用过滤策略,先过滤出故障的或并发请求大于阈值的一部分服务实例,然后再以线性轮询的方式从过滤后的实例清单中选出一个。

  • ZoneAvoidanceRule: 区域感知策略,先使用主过滤条件(区域负载器,选择最优区域)对所有实例过滤并返回过滤后的实例清单,依次使用次过滤条件列表中的过滤条件对主过滤条件的结果进行过滤,判断最小过滤数(默认1)和最小过滤百分比(默认0),最后对满足条件的服务器则使用RoundRobinRule(轮询方式)选择一个服务器实例。

例如,message-service的负载均衡策略设置为随机访问RandomRule,application.yml配置如下

message-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

脱离Eureka使用Ribbon

如果你不用Eureka,也可以继续使用Ribbon和Feign。
假设不使用Eureka,通过将ribbon.eureka.enabled 属性设置为 false, 可以在Ribbon中禁用Eureka,用@RibbonClient声明了一个"stores"服务,这个时候Ribbon Client 默认会引用一个配置好的服务列表,你可以在application.yml进行配置:

ribbon:
  eureka:
   enabled: false
stores:
  ribbon:
    listOfServers: example.com,google.com

服务治理

为什么需要服务治理

公司的系统是由几百个微服务构成的,每一个微服务又有多个实例,服务数量较多,服务之间的相互依赖成网状,所以微服务系统需要服务注册中心来统一管理微服务实例,方便查看每一个微服务实例的健康状态。

服务治理的解决方案

  • 服务注册:服务提供者主动自报家门

服务提供者将自己的服务信息(比如服务名、IP地址等)告诉服务注册中心。

  • 服务发现:服务消费者拉取注册数据

当服务消费者需要消费另外一个服务时,服务注册中心需要告诉服务消费者它所要消费服务的实例信息(如服务名、IP地址等)。

  • 心跳检测、服务续约和服务剔除

服务注册中心会检查注册的服务是否可用,通常一个服务实例注册后,会定时向服务注册中心提供“心跳”,以表明自己还处于可用的状态。如果一个服务实例停止向服务注册中心提供心跳一段时间后,服务注册中心会认为这个服务实例不可用,会把这个服务实例从服务注册列表中剔除。如果这个被剔除掉的服务实例过一段时间后继续向注册中心提供心跳,那么服务注册中心会把这个服务实例重新加入服务注册中心的列表中。

  • 服务下线:服务提供者主动发起下线

服务治理的技术选型

服务治理的技术选型

Eureka

Eureka Client:负责将这个服务的信息注册到Eureka Server中
Eureka Server:注册中心,里面有一个注册表,保存了各个服务所在的机器和端口号
自我保护机制产生的原因

Eureka的自我保护特性主要用于减少在网络分区或者不稳定状况下的不一致性问题,默认情况下,如果Server在一定时间内没有接收到某个服务实例的心跳(默认周期为30秒),Server将会注销该实例。如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,启动自我保护机制。

自我保护机制
  • Eureka不再从注册表中移除因为长时间没有收到心跳而过期的服务。
  • Eureka仍然能够接受新服务注册和查询请求,但是不会被同步到其它节点上,保证当前节点依然可用。
  • 当网络稳定时,当前实例新注册的信息会被同步到其它节点中。
在什么环境下开启自我保护机制
本地环境

建议关闭自我保护机制。因为在本地开发环境中,EurekaServer端相对来说重启频率不高,但是在EurekaClient端,可能改动代码之后需要重启,频率相对来说比较高;那么EurekaClient端重启之后就不会及时去向EurekaServer端发送心跳包,EurekaServer端就会认为是网络延迟或者其他原因,不会剔除服务,这样的话就会影响开发效率。

生产环境

建议开启自我保护机制。因为生产环境不会频繁重启服务器,并且EurekaClient端与EurekaServer端存在网络延迟的几率较高,所以需要开启自我保护机制避免误删服务。

Eureka Server端:配置关闭自我保护,并按需配置Eureka Server清理无效节点的时间间隔。

eureka.server.enable-self-preservation	# 设为false,关闭自我保护
eureka.server.eviction-interval-timer-in-ms # 清理间隔(单位毫秒,默认是60*1000

Eureka Client端:配置开启健康检查,并按需配置续约更新时间和到期时间

eureka.instance.lease-renewal-interval-in-seconds	# 续约更新时间间隔(默认30秒)
eureka.instance.lease-expiration-duration-in-seconds # 续约到期时间(默认90秒)

公司client 的配置:

eureka:
  instance:
    prefer-ip-address: true
    instance-id: ${spring.application.name}:${spring.client.ipAdress}:${server.port}
    lease-expiration-duration-in-seconds: 30 #服务过期时间配置,超过这个时间没有接收到心跳EurekaServer就会将这个实例剔除
    lease-renewal-interval-in-seconds: 10 #服务刷新时间配置,每隔这个时间会主动心跳一次

Nacos

Nacos的动态更新如何实现?什么是长轮询/如何实现长轮询?配置和服务器之间的配置变更整个方案如何实现?Nacos如何实现配置的变更和对比?

Nacos采用长轮训机制来实现数据变更的同步
在这里插入图片描述
Nacos是采用长轮训的方式向Nacos Server端发起配置更新查询的功能。所谓长轮训就是客户端发起一次轮训请求到服务端,当服务端配置没有任何变更的时候,这个连接一直打开,直到服务端有配置或者连接超时后返回。Nacos Client端需要获取服务端变更的配置,前提是要有一个比较,也就是拿客户端本地的配置信息和服务端的配置信息进行比较。

一旦发现和服务端的配置有差异,就表示服务端配置有更新,于是把更新的配置拉到本地。在这个过程中,有可能因为客户端配置比较多,导致比较的时间较长,使得配置同步较慢的问题。于是Nacos针对这个场景,做了两个方面的优化。

  • 减少网络通信的数据量,客户端把需要进行比较的配置进行分片,每一个分片大小是3000,也就是说,每次最多拿3000个配置去Nacos Server端进行比较。

  • 分阶段进行比较和更新,

    第一阶段,客户端把这3000个配置的key以及对应的value值的md5拼接成一个字符串,然后发送到Nacos Server端

    进行判断,服务端会逐个比较这些配置中md5不同的key,把存在更新的key返回给客户端。

    第二阶段,客户端拿到这些变更的key,循环逐个去调用服务单获取这些key 的value值。

这两个优化,核心目的是减少网络通信数据包的大小,把一次大的数据包通信拆分成了多次小的数据包通信。虽然会增加网络通信次数,但是对整体的性能有较大的提升。最后,再采用长连接这种方式,既减少了pull轮询次数,又利用了长连接的优势,很好的实现了配置的动态更新同步功能。

基于HTTP的长轮询简单实现

web客户端代码

//向后台长轮询消息
    function longPolling(){
        $.ajax({
            async : true,//异步
            url : 'longPollingAction!getMessages.action', 
            type : 'post',
            dataType : 'json',
            data :{},
            timeout : 30000,//超时时间设定30秒
            error : function(xhr, textStatus, thrownError) {
                longPolling();//发生异常错误后再次发起请求
            },
            success : function(response) {
                message = response.data.message;
                if(message!="timeout"){
                    broadcast();//收到消息后发布消息
                }
                longPolling();
            }
        });
    }

web服务器端代码

public class LongPollingAction extends BaseAction {
    private static final long serialVersionUID = 1L;
    private LongPollingService longPollingService;
    private static final long TIMEOUT = 20000;// 超时时间设置为20秒

    public String getMessages() {
        long requestTime = System.currentTimeMillis();
        result.clear();
        try {
            String msg = null;

            while ((System.currentTimeMillis() - requestTime) < TIMEOUT) {
                msg = longPollingService.getMessages();
                if (msg != null) {
                    break; // 跳出循环,返回数据
                } else {
                    Thread.sleep(1000);// 休眠1秒
                }
            }
            if (msg == null) {
                result.addData("message", "timeout");// 超时
            } else {
                result.addData("message", msg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return SUCCESS;
    }
    
    public LongPollingService getLongPollingService() {
        return longPollingService;
    }

    public void setLongPollingService(LongPollingService longPollingService) {
        this.longPollingService = longPollingService;
    }

}

Nacos、Eureka与Zookeeper区别

相同点:

  • 都可以实现分布式注册中心框架

不同点:

  • Zookeeper采用CP保证数据的一致性的问题,原理是采用ZAB原子广播协议。当我们ZK领导者宕机或出现了故障,会自动重新实现选举新的领导角色,整个选举的过程中为了保证数据一致性的问题,整个微服务无法实现通讯,可运行的节点必须满足过半机制,整个zk才可以使用,要不然会奔溃。

  • Eureka采用AP设计理念架构注册中心,相互注册完全去中心化,也就是没有主从之分,只要有一台Eureka节点存在整个微服务就可以实现通讯。Eureka中会定时向注册中心发送心跳,如果在短期内没有发送心跳,则就会直接剔除。会定时向注册中心定时拉去服务,如果不主动拉去服务,注册中心不会主动推送。

  • Nacos中注册中心会定时向消费者主动推送信息 ,这样就会保持数据的准时性。它会向注册中心发送心跳,但是它的频率要比Eureka快。Nacos从1.0版本选择Ap和CP混合形式实现注册中心,默认情况下采用Ap保证服务可用性,CP形式底层采用Raft协议保证数据的一致性问题。默认采用AP方式,当集群中存在非临时实例时,采用CP模式。选择Ap模式,在网络分区的的情况允许注册服务实例。选择CP模式,在网络分区的产生了抖动情况下不允许注册服务实例。

服务容错保护

服务雪崩

在微服务架构中,一个请求要调用多个服务是非常常见的。如客户端访问A服务,A服务访问B服务,B服务调用C服务,由于网络原因或者自身的原因,如果B服务或者C服务不能及时响应,A服务将处于阻塞状态,直到B服务C服务响应。此时若有大量的请求涌入,容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,造成连锁反应,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩效应”。

在高并发的情况下,一个服务的延迟可能导致所有服务器上的所有资源在数秒内饱和。比起服务故障,更糟糕的是这些应用程序还可能导致服务之间的延迟增加,导致整个系统出现更多级联故障。

造成服务雪崩的原因可以归结为以下三点:

  • 服务提供者不可用(硬件故障,程序bug,缓存击穿,用户大量请求等)
  • 重试加大流量(用户重试,代码逻辑重试)
  • 服务消费者不可用(同步等待造成的资源耗尽)

解决方案:

  • 服务隔离:限制调用分布式服务的资源,某一个调用的服务出现问题不会影响到其他服务调用
  • 服务熔断:牺牲局部服务,保全整体系统稳定性
  • 服务降级:服务熔断以后,客户端调用自己本地方法返回缺省值

所谓的容错处理其实就是捕获异常了,不让异常影响系统的正常运行,正如java中的try catch一样。在微服务调用中,自身异常可自行处理外,对于依赖的服务发生错误,或者调用异常,或者调用时间过长等原因时,为了避免长时间等待,造成系统资源耗尽, 一般上都会通过设置请求的超时时间,如http请求中的ConnectTimeout和ReadTimeout;而微服务提供了Hystrix熔断器,隔离问题服务,防止级联错误的发生。

Hystrix

Hystrix是一个实现了超时机制和断路器模式的工具类库,用于隔离访问远程系统、服务或第三方库,提升系统的可用性和容错性。

Hystrix容错机制:
  • 包裹请求:使用HystrixCommand包裹对依赖的调用逻辑,每个命令在独立线程中执行,这是用到了设计模式“命令模式”。
  • 跳闸机制:当某服务的错误率超过一定阈值时,Hystrix可以自动或手动跳闸,停止请求该服务一段时间。
  • 资源隔离:Hystrix为每个依赖都维护了一个小型的线程池,如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等候,从而加速判定失败。
  • 监控:Hystrix可以近乎实时的监控运行指标和配置的变化。如成功、失败、超时、被拒绝的请求等。
  • 回退机制:当请求失败、超时、被拒绝,或当断路器打开时,可以执行回退逻辑。
  • 自我修复:断路器打开一段时间后,会自动进入半开状态,断路器打开、关闭、半开的逻辑转换。
熔断器的工作原理

每个请求都会在 hystrix 超时之后返回 fallback,每个请求时间延迟就是近似 hystrix 的超时时间,假设是 5 秒,那么每个请求都要延迟 5 秒后才返回。当熔断器在 10 秒内发现请求总数超过 20,并且错误百分比超过 50%,此时熔断打开。

熔断打开之后,再有请求调用的时候,将不会调用主逻辑,而是直接调用降级逻辑,这个时候就会快速返回,而不是等待 5 秒才返回 fallback。通过断路器,实现了自动发现错误并将降级逻辑切为主逻辑,减少响应延迟。

当断路器打开,主逻辑被熔断后,hystrix 会启动一个休眠时间窗,在这个时间窗内,降级逻辑就是主逻辑;当休眠时间窗到期,断路器进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求返回正常,那么断路器将闭合,主逻辑恢复,如果这次请求依然失败,断路器继续打开,休眠时间窗重新计时。

信号量隔离

请求并发大,耗时短,采用信号量隔离,因为这类服务的返回通常很快,不会占用线程太长时间,而且也减少了线程切换的开销。

每个请求线程通过计数信号进行限制,当信号量大于了最大请求数maxConcurrentRequest时,调用fallback接口快速返回。另外由于通过信号量计数器进行隔离,它只是个计数器,资源消耗小。

信号量的调用是同步的,每次调用都得阻塞调用方的线程,直到有结果才返回,这样就导致了无法对访问做超时处理,只能依靠协议超时,无法主动释放。

实现方式

@HystrixCommand注解实现线程池隔离,通过配置超时时间,信号量隔离,信号量最大并发,以及回退方法,基于注解就可以对方法实现服务隔离。

// 信号量隔离
@HystrixCommand(
        commandProperties = {
                // 超时时间,默认1000ms
                @HystrixProperty(name = HystrixPropertiesManager.
                	EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS, value = "5000"),
                // 信号量隔离
                @HystrixProperty(name = HystrixPropertiesManager.
                	EXECUTION_ISOLATION_STRATEGY, value = "SEMAPHORE"),
                // 信号量最大并发
                @HystrixProperty(name = HystrixPropertiesManager.
                	EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS, value = "5")
        },
        fallbackMethod = "selectProductByIdFallBack"
)
@Override
public Product selectProductById(Integer id) {
    System.out.println(Thread.currentThread().getName());
    return productClient.selectProductById(id);
}

线程池隔离

请求并发大,耗时长,采用线程池隔离策略。这样可以保证大量的线程可用,不会由于服务原因一直处于阻塞或等待状态,快速失败返回。还有就是对依赖服务的网络请求涉及超时问题的都使用线程隔离。

优缺点

优点:

  • 使用线程池隔离可以安全隔离依赖的服务,减少所依赖的服务发生故障时的影响。比如A服务发生异常,导致请求大量超时,对应的线程池被打满,这时并不影响在其他线程池中的C、D服务的调用。
  • 当失败的服务再次变得可用时,线程池将清理并立即恢复,而不需要一个长时间的恢复。
  • 独立的线程池提高了并发性。

缺点:

  • 请求在线程池中执行,肯定会带来任务调度、排队个上下文切换带来的CPU开销。
  • 因为涉及到跨线程,那么就存在ThreadLocal数据传递的问题,比如在主线程初始化的ThreadLocal变量,在线程池中无法获取。
实现方式

@HystrixCommand注解实现线程池隔离,通过配置服务名称,接口名称,线程池,以及回退方法,基于注解就可以对接口实现服务隔离。

    // 线程池隔离
    @HystrixCommand(groupKey = "productServiceSinglePool", // 服务名称,相同名称使用同一个线程池
            commandKey = "selectProductById",              // 接口名称,默认为方法名
            threadPoolKey = "productServiceSinglePool",    // 线程池名称,相同名称使用同一个线程池
            commandProperties = {
                    // 超时时间,默认1000ms
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")
            },
            threadPoolProperties = {
           			 // 线程池大小
                    @HystrixProperty(name = "coreSize", value = "10"),  
                    // 等待队列长度(最大队列长度,默认值-1)
                    @HystrixProperty(name = "maxQueueSize", value = "100"), 
                     // 线程存活时间,默认1min 
                    @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
                    // 超出等待队列阈值执行拒绝策略
                    @HystrixProperty(name = "queueSizeRejectionThreshold", value = "100")
            },
            fallbackMethod = "selectProductByIdFallBack"
    )
    @Override
    public Product selectProductById(Integer id) {
        System.out.println(Thread.currentThread().getName());
        return productClient.selectProductById(id);
    }

    private Product selectProductByIdFallBack(Integer id) {
        return new Product(888, "未知商品", 0, 0d);
    }

    // 线程池隔离
    @HystrixCommand(groupKey = "productServiceListPool", // 服务名称,相同名称使用同一个线程池
            commandKey = "selectByIds",              // 接口名称,默认为方法名
            threadPoolKey = "productServiceListPool",    // 线程池名称,相同名称使用同一个线程池
            commandProperties = {
            // 超时时间,默认1000ms
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")
            },
            threadPoolProperties = {
           			 // 线程池大小
                    @HystrixProperty(name = "coreSize", value = "5"),  
                    // 等待队列长度(最大队列长度,默认值-1)
                    @HystrixProperty(name = "maxQueueSize", value = "100"), 
                     // 线程存活时间,默认1min 
                    @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
                    // 超出等待队列阈值执行拒绝策略
                    @HystrixProperty(name = "queueSizeRejectionThreshold", value = "100")
            },
            fallbackMethod = "selectByIdsFallback"
    )
    @Override
    public List<Product> selectByIds(List<Integer> ids) {
        System.out.println(Thread.currentThread().getName());
        return productClient.selectPhoneList(ids);
    }

    private List<Product> selectByIdsFallback(List<Integer> ids) {
        System.out.println("==call method selectByIdsFallback==");
        return Arrays.asList(new Product(999, "未知商品", 0, 0d));
    }
服务熔断

服务熔断一般是指软件系统中,由于某些原因使得服务出现了过载现象,为了防止造成整个系统故障,从而采用的一种保护措施,所以很多地方也把熔断称为过载保护。

实现方式

使用@HystrixProperty注解,通过配置请求数阈值、错误百分比阈值、快照时间窗口,基于注解就可以对方法实现服务熔断。

    // 服务熔断
    @HystrixCommand(
            commandProperties = {
					// 请求数阈值:在快照时间窗口内,必须满足请求阈值数才有资格熔断。打开断路器的最少请求数,默认20个请求。
					//意味着在时间窗口内,如果调用次数少于20次,即使所有的请求都超时或者失败,断路器都不会打开
                    @HystrixProperty(name = HystrixPropertiesManager.
                    	CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD, value = "10"),
                    // 错误百分比阈值:当请求总数在快照内超过了阈值,且有一半的请求失败,这时断路器将会打开。默认50%
                    @HystrixProperty(name = HystrixPropertiesManager.
                    	CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE, value = "50"),
                    // 快照时间窗口:断路器开启时需要统计一些请求和错误数据,统计的时间范围就是快照时间窗口,默认5秒
                    @HystrixProperty(name = HystrixPropertiesManager.
                    	CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS, value = "5000")
            },
            fallbackMethod = "selectProductByIdFallBack"
    )
    @Override
    public Product selectProductById(Integer id) {
        System.out.println(Thread.currentThread().getName()+ 
        LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME));
        if (id == 1) {
            throw new RuntimeException("模拟查询ID为1导致异常");
        }
        return productClient.selectProductById(id);
    }
服务降级

开启条件

  • 方法抛出HystrixBadRequestException异常
  • 方法调用超时
  • 熔断器开启拦截调用
  • 线程池、队列、信号量跑满
方法服务降级
// 服务降级
@HystrixCommand(fallbackMethod = "selectProductByIdFallBack")
@Override
public Product selectProductById(Integer id) {
    System.out.println(Thread.currentThread().getName()+ 
    LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME));
    if (id == 1) {
        throw new RuntimeException("模拟查询ID为1导致异常");
    }
    return productClient.selectProductById(id);
}

类全局服务降级
在类中添加注解@DefaultProperties(defaultFallback = "selectProductByIdFallback")
在这里插入图片描述
在需要降级的方法上添加注解@HystrixCommand
在这里插入图片描述

创建一个全局fallbakc方法

public Product selectProductByIdFallback(){
    return new Product(999, "undefined", 0, 0d);
}
Feign中使用断路器

当我们方法很多时,要是分别编写一个fallback估计也是崩溃的,虽然可以使用一个通用的fallback,但未进行特殊设置下,也是无法知道具体是哪个方法发生熔断的。

而对于Feign,我们可以使用一种更加优雅的形式进行。我们可以指定@FeignClient注解的fallback属性,或者是fallbackFactory属性,后者可以获取异常信息的。Feign是自带断路器的,在D版本的Spring Cloud中,它没有默认打开。

需要在配置文件中配置打开它,在配置文件加以下代码:

feign.hystrix.enabled=true

需要在FeignClient的SchedualServiceHi接口的注解中加上fallback的指定类就行了

@FeignClient(value = "service-hi",fallback = SchedualServiceHiHystric.class)
public interface SchedualServiceHi {
    @RequestMapping(value = "/hi",method = RequestMethod.GET)
    String sayHiFromClientOne(@RequestParam(value = "name") String name);
}

SchedualServiceHiHystric需要实现SchedualServiceHi 接口,并注入到Ioc容器中

@Component
public class SchedualServiceHiHystric implements SchedualServiceHi {
    @Override
    public String sayHiFromClientOne(String name) {
        return "sorry "+name;
    }
}

servcie-feign工程,浏览器打开http://localhost:8765/hi?name=forezp,注意此时service-hi工程没有启动,网页显示:

sorry forezp

打开service-hi工程,再次访问,浏览器显示:

hi forezp,i am from port:8762

这证明断路器起到作用了。

服务监控

除了实现服务容错之外,Hystrix还提供了近乎实时的监控功能,将服务执行结果、运行指标、请求数量、成功数量等这些状态通过Actuator进行收集,然后访问/actuator/hystrix.stream即可看到实时的监控数据。

实现方式
添加依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
添加配置
management:
  endpoints:
    web:
      exposure:
        include: hystrix.stream
启动类

添加@EnableHystrix注解

访问

http://localhost:9090/actuator/hystrix.stream

查看数据

在这里插入图片描述

监控中心

Hystrix提供的一套可视化系统,Hystrix-Dashboard,可以非常友好的看到当前环境中服务运行的状态。Hystrix-Dashboard是一款针对Hystrix进行实时监控的工具,通过Hystrix-Dashboard我们可以直观地看到各Hystrix Command的请求响应时间,请求成功率等数据。

实现过程
添加依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
启动类添加注解@EnableHystrixDashboard
// 开启数据监控
@EnableHystrixDashboard
// 开启熔断器
@EnableHystrix
// 开启缓存注解
@EnableCaching
@EnableFeignClients
@SpringBootApplication
public class ServiceConsumerApplication
{
    public static void main( String[] args )
    {
        SpringApplication.run(ServiceConsumerApplication.class);
    }
}

访问:http://localhost:9090/hystrix,控制中心界面如下:
在这里插入图片描述
在这里插入图片描述

聚合监控中心
实现过程

Turbine是聚合服务器发送事件流数据的一个工具,dashboard只能监控单个节点,实际生产环境中都为集群,因此可以通过Turbine来监控集群服务。

新建一个聚合监控项目,添加依赖

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-turbine</artifactId>
</dependency>

添加配置文件

server:
  port: 8181

spring:
  application:
    name: eureka-turbine

eureka:
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/


turbine:                                            # 聚合监控
  app-config: service-consumer,service-provider     # 监控的服务列表
  cluster-name-expression: "'default'"              # 指定集群名称

启动类添加注解

@EnableTurbine
@EnableHystrix
@EnableHystrixDashboard
@SpringBootApplication
public class App 
{
    public static void main( String[] args )
    {
        SpringApplication.run(App.class);
    }
}

访问http://localhost:8181/hystrix
在这里插入图片描述
在这里插入图片描述

Hystrix工作流程
  1. 构造一个HystrixCommand或者HystrixObservableCommand对象
  2. 执行command命令
  3. 结果是否缓存
  4. 熔断器是否打开
  5. 线程池、队列、信号量是否打满
  6. 执行对应的构造方法或者run方法
  7. 计算熔断器状态开启还是关闭
  8. 获取fallback返回
  9. 返回成功响应
    Hystrix工作流程

声明式服务调用

比如一个订单服务知道库存服务、积分服务、仓库服务在哪里了,同时也监听着哪些端口号了。但是新问题又来了:难道订单服务要自己写一大堆代码,建立连接、构造请求、接着发送请求过去、解析响应等等。

使用Feign组件直接就是用注解定义一个 FeignClient接口,然后调用那个接口就可以了。人家Feign Client会在底层根据你的注解,跟你指定的服务建立连接、构造请求、发起靕求、获取响应、解析响应,等等。这一系列脏活累活,人家Feign全给你干了。

对某个接口定义了@FeignClient注解,Feign就会针对这个接口创建一个动态代理,接着你要是调用那个接口,本质就是会调用 Feign创建的动态代理,这是核心中的核心,Feign的动态代理会根据你在接口上的@RequestMapping等注解,来动态构造出你要请求的服务的地址,最后针对这个地址,发起请求、解析响应。

在这里插入图片描述

API网关服务

请求拦截、服务分发、统一的降级、限流、认证授权、安全

关于业务网关,市场上也有蛮多的技术。一些大的公司一般选择定制化开发。但是从开发语言,可维护性上出发,能选的只有getway和zuul,但是zuul使用的阻塞IO,损失性能极大,虽然新版本有支持,但是spring并没有很好的支持升级后的zuul。gatway是spring做出来的,性能也比较好,支持长连接。
在这里插入图片描述
Spring Cloud Gateway明确区分了Router和Filter,位于请求接入:作为所有API接口服务请求的接入点

比如可以基于Header、Path、Host、Query自由路由。

gateway的组成

  • 路由 : 网关的基本模块,有ID,目标URI,一组断言和一组过滤器组成
  • 断言:就是访问该请求的访问规则,可以用来匹配来自http请求的任何内容,例如headers或者参数
  • 过滤器:这个就是我们平时说的过滤器,用来过滤一些请求的,也可以自定义过滤器,但是要实现两个接口,ordered和globalfilter。

分布式配置中心

config配置中心

  • 在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,需要分布式配置中心组件。

  • 支持配置服务放在配置服务的内存中(即本地),也支持放在远程Git仓库中。config 组件中,分两个角色,一是config server,二是config client。config-client可以从config-server获取配置属性。

消息总线

Bus数据总线:将分布式的节点用轻量的消息代理连接起来。它可以用于广播配置文件的更改或者服务之间的通讯,也可以用于监控。

应用场景:实现通知微服务架构的配置文件的更改。去代码仓库将foo的值改为“foo version 4”,即改变配置文件foo的值。如果是传统的做法,需要重启服务,才能达到配置文件的更新。我们只需要发送post请求:http://localhost:8881/bus/refresh,会发现config-client会重现肚脐配置文件,重新读取配置文件。

案例:当git文件更改的时候,通过pc端用post 向端口为8882的config-client发送请求/bus/refresh/;此时8882端口会发送一个消息,由消息总线向其他服务传递,从而使整个微服务集群都达到更新配置文件。

消息驱动

SpringCloud Stream:SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性,通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(rabbitmq切换为kafka),使得微服务开发的高度解耦,服务可以关注更多自己的业务流程。

  • 通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。

  • Binder可以生成Binding,Binding用来绑定消息容器的生产者和消费者,它有两种类型,INPUT和OUTPUT,INPUT对应于消费者,OUTPUT对应于生产者。

设计思想:Stream中的消息通信方式遵循了发布-订阅模式,Topic主题进行广播,在RabbitMQ就是Exchange,在Kakfa中就是Topic。

基础组件

  • Binder: 很方便的连接中间件,屏蔽差异
  • Channel:通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置
  • Source和Sink: 简单的可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入。

分布式服务追踪

微服务架构上通过业务来划分服务的,通过REST调用,对外暴露的一个接口,可能需要很多个服务协同才能完成这个接口功能,如果链路上任何一个服务出现问题或者网络超时,都会形成导致接口调用失败。随着业务的不断扩张,服务之间互相调用会越来越复杂。一个 HTTP 请求会调用多个不同的微服务来处理返回最后的结果,在这个调用过程中,可能会因为某个服务出现网络延迟过高或发送错误导致请求失败,所以需要对服务追踪分析,提供一个可视化页面便于排查问题所在。

Sleuth 整合 Zipkin,可以使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的 REST API 接口来辅助查询跟踪数据以实现对分布式系统的监控程序,从而及时发现系统中出现的延迟过高问题。除了面向开发的 API 接口之外,它还提供了方便的 UI 组件来帮助我们直观地搜索跟踪信息和分析请求链路明细,比如可以查询某段时间内各用户请求的处理时间等。

Skywalking是本土开源的基于字节码注入的调用链路分析以及应用监控分析工具,特点是支持多种插件,UI功能较强,接入端无代码侵入。

CAT是由国内美团点评开源的,基于Java语言开发,目前提供Java、C/C++、Node.js、Python、Go等语言的客户端,监控数据会全量统计,国内很多公司在用,例如美团点评、携程、拼多多等,CAT跟下边要介绍的Zipkin都需要在应用程序中埋点,对代码侵入性强。

在这里插入图片描述

性能对比:skywalking探针对吞吐量影响最小,zipkin对吞吐量影响适中,pinpoint的探针对吞吐量影响最大。对于内存和cpu的使用,都差不多,相差在10%之内。

分布式事务

Seata 的架构

在 Seata 的架构中,一共有三个角色:
TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。TC 为单独部署的 Server 服务端。
TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。TM为嵌入到应用中的 Client 客户端。
RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。RM 为嵌入到应用中的 Client 客户端。

存在的问题

性能损耗

一条Update的SQL,则需要全局事务xid获取(与TC通讯)、before image(解析SQL,查询一次数据库)、after image(查询一次数据库)、insert undo log(写一次数据库)、before commit(与TC通讯,判断锁冲突),这些操作都需要一次远程通讯RPC,而且是同步的。另外undo log写入时blob字段的插入性能也是不高的。每条写SQL都会增加这么多开销,粗略估计会增加5倍响应时间。

性价比

为了进行自动补偿,需要对所有交易生成前后镜像并持久化,可是在实际业务场景下,这个是成功率有多高,或者说分布式事务失败需要回滚的有多少比率?按照二八原则预估,为了20%的交易回滚,需要将80%的成功交易的响应时间增加5倍,这样的代价相比于让应用开发一个补偿交易是否是值得?

全局锁
热点数据

相比XA,Seata 虽然在一阶段成功后会释放数据库锁,但一阶段在commit前全局锁的判定也拉长了对数据锁的占有时间,这个开销比XA的prepare低多少需要根据实际业务场景进行测试。全局锁的引入实现了隔离性,但带来的问题就是阻塞,降低并发性,尤其是热点数据,这个问题会更加严重。

回滚锁释放时间

Seata在回滚时,需要先删除各节点的undo log,然后才能释放TC内存中的锁,所以如果第二阶段是回滚,释放锁的时间会更长。

死锁问题

Seata的引入全局锁会额外增加死锁的风险,但如果出现死锁,会不断进行重试,最后靠等待全局锁超时,这种方式并不优雅,也延长了对数据库锁的占有时间。

事务模式

Seata 将为用户提供了 AT、TCC、SAGA 和XA 事务模式,为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式,阿里云上有商用版本的GTS。

XA 事务模式

基于XA协议的两阶段提交
XA是一个分布式事务协议,由Tuxedo提出。XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。,XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。

XA 模式其实就是 Seata 底层利用了 XA 接口,在一阶段二阶段时自动处理。如一阶段时,XA 的 RM 通过代理用户数据源,创建 XAConnection,进行开启 XA 事务(XA start)和 XA-prepare(此时 XA 的任何操作都会被持久化,即便宕机也能恢复),在二阶段时,TC 通知 RM 进行 XA 分支的 Commit/Rollback 操作。

AT事务模式

AT模式的核心是对业务无侵入,是一种改进后的两阶段提交。

第一阶段

业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。核心在于对业务sql进行解析,转换成undolog,并同时入库。

业务 sql:update product set name = ‘GTS’ where name = ‘TXC’。

一阶段的执行过程对用户是无感知的,用户侧的业务 sql 保持不变,而 AT 模式下一阶段具体发生了什么?接下来,简单说下。

解析 sql 并查询得到前镜像:select id, name, since from product where name = ‘TXC’。
执行业务 sql。
查询执行后的数据作为后镜像:select id, name, since from product where id = 1。

第二阶段

分布式事务操作成功,则TC通知RM异步删除undolog。
分布式事务操作失败,TM向TC发送回滚请求,RM 收到协调器TC发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。

提交:仅需把事务相关信息删除即可(理论上不删除也没问题)。
回滚:取出前镜像进行回滚。

优点

Seata架构的亮点主要有几个:

  • 应用层基于SQL解析实现了自动补偿,从而最大程度的降低业务侵入性;
  • 将分布式事务中TC(事务协调者)独立部署,负责事务的注册、回滚;
  • 通过全局锁实现了写隔离与读隔离。

通过上述简单的例子,其实可以发现,AT 模式就是自动补偿式事务,那 AT 具体都做了哪些呢?下文将会讲述。

AT 如何保证分布式事务一致性?

在这里插入图片描述
可能很多人刚看到上图会有疑问,其实这个就是无侵入式 AT 模式的做法示意图。首先用户还是从接口进入,到达事务发起方,此时对业务开发者来说,这个发起方入口就是一个业务接口罢了,一样地执行业务 sql,一样地 return 响应信息给客户端并没有什么改变。而背后就是用户的 sql 被 Seata 代理所托管,Seata-AT 模式能感知到用户的所有 sql,并对之进行操作,来保证一致性。

Seata-AT 是怎么做到无侵入的呢?

在这里插入图片描述应用启动时 Seata 会自动把用户的 DataSource 代理,对 JDBC 操作熟悉的用户其实对 DataSource 还是比较熟悉的,拿到了 DataSource,就等于掌握了数据源连接,也就能在背后做些“小动作”,此时对用户来讲也是无感知无入侵。

之后业务有请求进来,执行业务 sql 时,Seata 会解析用户的 sql,提取出表元数据,生成前镜像,再通过执行业务 sql,保存执行 sql 后的后镜像(至于后镜像的介绍之后会讲到),生成行锁之后在注册分支时携带到 Seata-Server,也就是 TC 端。

到此为止,在 Client 端的一阶段操作就已经完成了,无感知、无入侵。此时如果思考下,会发现这里其实有一个行锁,这个行锁是干什么用的呢?这就是要接着讲到 Seata-AT 是如何保证分布式下的事务隔离性,这里直接拿官网的示例来说。

写隔离
一阶段本地事务提交前,需要确保先拿到全局锁 。
拿不到全局锁,不能提交本地事务。
拿全局锁的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
以一个示例来说明:

两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。

tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的全局锁,本地提交释放本地锁。tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的全局锁,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待**全局锁 **。

tx1 二阶段全局提交,释放全局锁 。tx2 拿到全局锁提交本地事务。

如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。

此时如果 tx2 仍在等待该数据的全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的全局锁等锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。

因为整个过程全局锁在 tx1 结束前一直是被 tx1 持有的,所以不会发生脏写的问题。

这个时候隔离性想必大家已经比较明白了,此时一阶段的大部分操作相信大家也比较明白了,接下来我们继续往下一阶段解析。

AT 模式二阶段处理
在这里插入图片描述

在二阶段提交时,TC 仅是下发一个通知 :把之前一阶段做记录的 undoLog 删除,并把相关事务信息如:行锁删除,之后让因为在竞争锁被阻塞的事务顺利进行。

而二阶段是回滚时,则要多做一些处理。
在这里插入图片描述首先在 Client 端收到 TC 告知的二阶段是回滚时,会去查到对应的事务的 undolog,取出后镜像,对比当前的数据(因为 SeataAT 是从业务应用层面进行保护分布式事务,如果此时在数据库层面直接修改了库内信息,这个时候 SeataAT 的行锁不起隔离性作用),如果出现了在全局事务以外的数据修改,此时判定为脏写,而 Seata 因为无法感知这个脏写如何发生,此时只能打印日志和触发异常通知,告知用户需要人工介入(规范修改数据入口可避免脏写)。

而如果没有发生脏写就比较简单了,拿出前镜像,众所皆知事务是需要有原子性的,要么一起发生,要么都不发生,此时前镜像记录了发生之前的数据,进行回滚后,就达到了类似本地事务那样的原子性效果。回滚后,再把事务相关信息,如 undolog,行锁进行删除。二阶段回滚算是告一段落了。

既然介绍完了 AT 模式的一阶段及二阶段的原理思想方式,那么 AT 在 Seata 的分布式事务框架下是怎么样的呢?
可以看到,AT 与其它事务模式在 Seata 事务框架中,会多出一个 undolog 的表(相对其它模式的入侵点),但是除此之外,对业务来说,几乎是零入侵性,这也就是为什么 AT 模式在 Seata 中受众广泛的原因。

AT 模式与 Seata 支持的其它二阶段模式区别

首先应该明白,目前为止,不存在有任何一种分布式事务的可以满足所有场景。

无论 AT 模式、TCC 模式还是 Saga 模式,这些模式的提出,本质上都源自 XA 规范对某些场景需求的无法满足。

目前分为 3 点来做出对比:

数据锁定
AT 模式使用全局锁保障基本的写隔离,实际上也是锁定数据的,只不过锁在 TC 侧集中管理,解锁效率高且没有阻塞的问题。

TCC 模式无锁,利用本地事务排他锁特性,可预留资源,在全局事务决议后执行相应操作。

XA 模式在整个事务处理过程结束前,涉及数据都被锁定,读写都按隔离级别的定义约束起来。

死锁(协议阻塞)
XA 模式 prepare 后(老版本的数据库中,需要 XA END 后,再下发 prepare <三阶段由来>),分支事务进入阻塞阶段,收到 XA commit 或 XA rollback 前必须阻塞等待。

AT 可支持降级,因为锁存储在 TC 侧,如果 Seata 出现 bug 或者其它问题,可直接降级,对后续业务调用链无任何影响。

TCC 无此问题。

性能
性能的损耗主要来自两个方面:一方面,事务相关处理和协调过程,增加单个事务的 RT;另一方面,并发事务数据的锁冲突,降低吞吐。其实主要原因就是上面的协议阻塞跟数据锁定造成。

XA 模式它的一阶段不提交,在大并发场景由于锁存储在多个资源方(数据库等),加剧了性能耗损。

AT 模式锁粒度细至行级(需要主键),且所有事务锁存储在 TC 侧,解锁高效迅速。

TCC 模式性能最优,仅需些许 RPC 开销,及 2 次本地事务的性能开销,但是需要符合资源预留场景,且是对业务侵入性较大(需要业务开发者每个接口分为 3 个,一个 try,2 个二阶段使用的 confirm 和 cancel )。
可能很多同学对 XA 和 AT 的锁 & 协议阻塞不是特别理解,那么直接来看下图:
在这里插入图片描述

可以试着猜一下是哪个是 XA?其实下图的是 XA,因为它带来的锁粒度更大,且锁定时间更久,导致了并发性能相对 AT 事务模型来说,差的比较多,所以至今XA模式的普及度都不很太高。

总结:关于seata是可以做到对项目代码无入侵,代价是需要部署和维护一个中间件,关于at和xa模式对比从概念上看很难区别,我的理解差异点在于AT模式的隔离就是靠全局锁来保证,粒度细至行级,锁信息存储在Seata-Server一侧。

XA模式的隔离性就是由本地数据库保证,锁存储在各个本地数据库中。由于XA模式一旦执行了prepare后,再也无法重入这个XA事务,也无法跟其他XA事务共享锁。因为XA协议,仅是通过XID来start一个xa事务,本身它不存在所谓的分支事务说法,它本事就是一个XA事务而已,也就是说它只管它自己。at模式的undolog就是把本地事务作用中的undolog,利用他的原理,做到了分布式事务中,来保证了分布式事务下的事务一致性。

目前使用:目前是结合sharding在使用,如xxljob跑任务会用到一些订单实时报价并修改用户订单概览等信息,需要与第三方系统交互,系统就需要保证数据的最终一致性。

流量控制

Sentinel 是面向分布式服务架构的高可用流量防护组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。

实现原理

启动并且初始化Sentinel

Sentinel本质上只是一个运行在特殊模式下的Redis服务器,当一个Sentinel启动时,它需要执行以下步骤:
1)初始化服务器。
2)将普通Redis服务器使用的代码替换成Sentinel专用代码。
3)初始化Sentinel状态。
4)根据给定的配置文件,初始化Sentinel的监视主服务器列表。
5)创建连向主服务器的网络连接。
对于每个被Sentinel监视的主服务器来说,Sentinel会创建两个连向主服务器的异步网络连接:
❑一个是命令连接,这个连接专门用于向主服务器发送命令,并接收命令回复。
❑另一个是订阅连接,这个连接专门用于订阅主服务器的__sentinel__:hello频道。

在这里插入图片描述

获取主服务器信息

通过分析主服务器返回的INFO命令回复,Sentinel可以获取以下两方面的信息:
❑一方面是关于主服务器本身的信息,包括run_id域记录的服务器运行ID,以及role域记录的服务器角色;
❑另一方面是关于主服务器属下所有从服务器的信息,每个从服务器都由一个"slave"字符串开头的行记录,每行的ip=域记录了从服务器的IP地址,而port=域则记录了从服务器的端口号。根据这些IP地址和端口号,Sentinel无须用户提供从服务器的地址信息,就可以自动发现从服务器。

获取从服务器信息

在这里插入图片描述
❑从服务器的运行ID run_id。
❑从服务器的角色role。
❑主服务器的IP地址master_host,以及主服务器的端口号master_port。
❑主从服务器的连接状态master_link_status。
❑从服务器的优先级slave_priority。
❑从服务器的复制偏移量slave_repl_offset。

向主服务器和从服务器发送信息,接收来自主服务器和从服务器的频道信息

对于监视同一个服务器的多个Sentinel来说,一个Sentinel发送的信息会被其他Sentinel接收到,这些信息会被用于更新其他Sentinel对发送信息Sentinel的认知,也会被用于更新其他Sentinel对被监视服务器的认知。
在这里插入图片描述
当Sentinel通过频道信息发现一个新的Sentinel时,它不仅会为新Sentinel在sentinels字典中创建相应的实例结构,还会创建一个连向新Sentinel的命令连接,而新Sentinel也同样会创建连向这个Sentinel的命令连接,最终监视同一主服务器的多个Sentinel将形成相互连接的网络:Sentinel A有连向Sentinel B的命令连接,而Sentinel B也有连向Sentinel A的命令连接。
在这里插入图片描述

检测主观下线状态

Sentinel配置文件中的down-after-milliseconds选项指定了Sentinel判断实例进入主观下线所需的时间长度:如果一个实例在down-after-milliseconds毫秒内,连续向Sentinel返回无效回复,那么Sentinel会修改这个实例所对应的实例结构,在结构的flags属性中打开SRI_S_DOWN标识,以此来表示这个实例已经进入主观下线状态。多个Sentinel设置的主观下线时长可能不同

检查客观下线状态

当Sentinel将一个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线了,它会向同样监视这一主服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。当Sentinel从其他Sentinel那里接收到足够数量的已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器执行故障转移操作。当认为主服务器已经进入下线状态的Sentinel的数量,超过Sentinel配置中设置的quorum参数的值,那么该Sentinel就会认为主服务器已经进入客观下线状态。不同Sentinel判断客观下线的条件可能不同。

选举领头的Sentinel

当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel,并由领头Sentinel对下线主服务器执行故障转移操作。
以下是Redis选举领头Sentinel的规则和方法:
❑所有在线的Sentinel都有被选为领头Sentinel的资格,换句话说,监视同一个主服务器的多个在线Sentinel中的任意一个都有可能成为领头Sentinel。
❑每次进行领头Sentinel选举之后,不论选举是否成功,所有Sentinel的配置纪元(configuration epoch)的值都会自增一次。配置纪元实际上就是一个计数器,并没有什么特别的。
❑在一个配置纪元里面,所有Sentinel都有一次将某个Sentinel设置为局部领头Sentinel的机会,并且局部领头一旦设置,在这个配置纪元里面就不能再更改。
❑每个发现主服务器进入客观下线的Sentinel都会要求其他Sentinel将自己设置为局部领头Sentinel。
❑当一个Sentinel(源Sentinel)向另一个Sentinel(目标Sentinel)发送SENTINEL is-master-down-by-addr命令,并且命令中的runid参数不是*符号而是源Sentinel的运行ID时,这表示源Sentinel要求目标Sentinel将前者设置为后者的局部领头Sentinel。
❑Sentinel设置局部领头Sentinel的规则是先到先得:最先向目标Sentinel发送设置要求的源Sentinel将成为目标Sentinel的局部领头Sentinel,而之后接收到的所有设置要求都会被目标Sentinel拒绝。
❑目标Sentinel在接收到SENTINEL is-master-down-by-addr命令之后,将向源Sentinel返回一条命令回复,回复中的leader_runid参数和leader_epoch参数分别记录了目标Sentinel的局部领头Sentinel的运行ID和配置纪元。
❑源Sentinel在接收到目标Sentinel返回的命令回复之后,会检查回复中leader_epoch参数的值和自己的配置纪元是否相同,如果相同的话,那么源Sentinel继续取出回复中的leader_runid参数,如果leader_runid参数的值和源Sentinel的运行ID一致,那么表示目标Sentinel将源Sentinel设置成了局部领头Sentinel。
❑如果有某个Sentinel被半数以上的Sentinel设置成了局部领头Sentinel,那么这个Sentinel成为领头Sentinel。举个例子,在一个由10个Sentinel组成的Sentinel系统里面,只要有大于等于10/2+1=6个Sentinel将某个Sentinel设置为局部领头Sentinel,那么被设置的那个Sentinel就会成为领头Sentinel。
❑因为领头Sentinel的产生需要半数以上Sentinel的支持,并且每个Sentinel在每个配置纪元里面只能设置一次局部领头Sentinel,所以在一个配置纪元里面,只会出现一个领头Sentinel。
❑如果在给定时限内,没有一个Sentinel被选举为领头Sentinel,那么各个Sentinel将在一段时间之后再次进行选举,直到选出领头Sentinel为止。

故障转移

1)在已下线主服务器属下的所有从服务器里面,挑选出一个从服务器,并将其转换为主服务器。
2)让已下线主服务器属下的所有从服务器改为复制新的主服务器。
3)将已下线主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器。

新的服务器是怎样挑选出来的?
领头Sentinel会将已下线主服务器的所有从服务器保存到一个列表里面,然后按照以下规则,一项一项地对列表进行过滤:
1)删除列表中所有处于下线或者断线状态的从服务器,这可以保证列表中剩余的从服务器都是正常在线的。
2)删除列表中所有最近五秒内没有回复过领头Sentinel的INFO命令的从服务器,这可以保证列表中剩余的从服务器都是最近成功进行过通信的。
3)删除所有与已下线主服务器连接断开超过down-after-milliseconds10毫秒的从服务器:down-after-milliseconds选项指定了判断主服务器下线所需的时间,而删除断开时长超过down-after-milliseconds10毫秒的从服务器,则可以保证列表中剩余的从服务器都没有过早地与主服务器断开连接,换句话说,列表中剩余的从服务器保存的数据都是比较新的。
之后,领头Sentinel将根据从服务器的优先级,对列表中剩余的从服务器进行排序,并选出其中优先级最高的从服务器。
如果有多个具有相同最高优先级的从服务器,那么领头Sentinel将按照从服务器的复制偏移量,对具有相同最高优先级的所有从服务器进行排序,并选出其中偏移量最大的从服务器(复制偏移量最大的从服务器就是保存着最新数据的从服务器)。
最后,如果有多个优先级最高、复制偏移量最大的从服务器,那么领头Sentinel将按照运行ID对这些从服务器进行排序,并选出其中运行ID最小的从服务器。

在这里插入图片描述

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java程序员廖志伟

赏我包辣条呗

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值