SpringCloud
微服务
服务架构演变
单体架构
将业务的所有功能集中在一个项目中开发并打包部署
分布式架构
根据业务功能对系统进行拆分,每个业务模块作为一个独立项目开发
问题
- 服务拆分的粒度
- 服务地址的维护
- 服务远程的调用
- 服务状态的感知
微服务
微服务
是一种经过良好架构设计的分布式
架构设计方案。
微服务架构的特征:
- 单一职责:微服务拆分粒度更小,每个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
- 面向服务:每个微服务对外暴露业务接口
- 独立自治:开发独立,技术独立,数据独立,部署独立
- 级联隔离:服务调用要做好隔离、容错、降级,避免出现级联问题
微服务的优缺点:
- 优点:拆分粒度更小,服务更加独立,耦合度更低
- 缺点:架构非常复杂,运维、监控、部署难度高
微服务技术对比:
SpringCloud
SpringCloud集成了各种微服务组件,并基于SpringBoot实现组件的自动装配,从而提供开箱即用的体验
SpringCloud和SpringBoot版本对应关系
服务拆分
- 不同微服务单一职责,不会重复开发相同的业务
- 不同微服务数据独立,不会访问其他服务的数据库
- 微服务将业务暴露为接口,供其他微服务调用
远程调用
RestTemplate
基于RestTemplate发起的http请求实现的远程调用与语言就无关,只要请求的ip地址、端口、接口路径以及请求参数即可。
导入Ribbon依赖
1 2 3 4
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency>
|
注册RestTemplate
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Configuration public class RestTemplateConfig {
@Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }
|
使用RestTemplate发送http请求
1 2
| UmsMember umsMember = restTemplate.getForObject("http://localhost:8001/member/" + omsOrder.getMemberId(), UmsMember.class);
|
提供者与消费者
- 服务提供者:暴露接口给其他微服务调用
- 服务消费者:调用其他微服务提供的接口
- 提供者与消费者角色是相对的
Eureka
RestTemplate调用出现的问题
- 硬编码方式不易于修改请求地址
- 集群环境不能获取地址和负载均衡
- 不能监测服务提供者的健康状态
Eureka的作用
Spring Cloud Eureka是Spring Cloud Netflix 子项目的核心组件之一,主要用于微服务架构中的服务治理。本文将对搭建Eureka注册中心,搭建Eureka客户端,搭建Eureka集群及给Eureka注册中心添加登录认证进行介绍。
在微服务架构中往往会有一个注册中心,每个微服务都会向注册中心去注册自己的地址及端口信息,注册中心维护着服务名称与服务实例的对应关系。每个微服务都会定时从注册中心获取服务列表,同时汇报自己的运行情况,这样当有的服务需要调用其他服务时,就可以从自己获取到的服务列表中获取实例地址进行调用,Eureka实现了这套服务注册与发现机制。
搭建Eureka服务端
父工程引入spring-cloud依赖
1 2 3 4 5 6 7 8
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency>
|
创建子工程
1 2 3 4
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>
|
启动类添加@EnableEurekaServer启用Eureka注册中心
1 2 3 4 5 6 7
| @EnableEurekaServer @SpringBootApplication public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
|
配置注册中心
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| server: port: 7001
spring: application: name: eureka-server
eureka: instance: hostname: localhost client: fetch-registry: false register-with-eureka: false server: enable-self-preservation: false
|
搭建Eureka客户端
引入依赖
1 2 3 4 5
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
|
启动类添加@EnableDiscoveryClient启用Eureka服务发现
1 2 3 4 5 6 7
| @EnableDiscoveryClient @SpringBootApplication public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); } }
|
配置编辑
1 2 3 4 5 6 7
| eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka/
|
查看注册信息
多实例部署
多实例部署,修改端口设置,避免端口冲突
拉取服务
1 2
| CommonResult commonResult = restTemplate.getForObject("http://demo-user/member/" + omsOrder.getMemberId(), CommonResult.class);
|
负载均衡
1 2 3 4 5
| @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }
|
Eureka集群搭建
由于所有服务都会注册到注册中心去,服务之间的调用都是通过从注册中心获取的服务列表来调用,注册中心一旦宕机,所有服务调用都会出现问题。所以我们需要多个注册中心组成集群来提供服务,下面将搭建一个双节点的注册中心集群。
通过多个注册中心互相注册,搭建了注册中心的多节点集群。
编辑配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| server: port: 7001
spring: application: name: eureka-server
eureka: instance: hostname: localhost client: serviceUrl: defaultZone: http://localhost:7002/eureka/, http://localhost:7003/eureka/ fetch-registry: true register-with-eureka: true
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| server: port: 7002
spring: application: name: eureka-server
eureka: instance: hostname: localhost client: serviceUrl: defaultZone: http://localhost:7001/eureka/, http://localhost:7003/eureka/ fetch-registry: true register-with-eureka: true
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| server: port: 7003
spring: application: name: eureka-server
eureka: instance: hostname: localhost client: serviceUrl: defaultZone: http://localhost:7001/eureka/, http://localhost:7002/eureka/ fetch-registry: true register-with-eureka: true
|
复制运行配置
编辑配置文件
Eureka添加认证
添加依赖
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
|
配置WebSecurityConfig
默认情况下添加SpringSecurity依赖的应用每个请求都需要添加CSRF token才能访问,Eureka客户端注册时并不会添加,所以需要配置/eureka/**路径不需要CSRF token。
1 2 3 4 5 6 7 8
| @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().ignoringAntMatchers("/eureka/**"); super.configure(http); } }
|
编辑配置文件
配置文件中需要修改注册中心地址格式
1
| http://${username}:${password}@${hostname}:${port}/eureka/
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| server: port: 7003
spring: application: name: eureka-server security: user: name: admin password: 123456
eureka: instance: hostname: localhost client: serviceUrl: defaultZone: http://admin:123456@localhost:7001/eureka/, http://admin:123456@localhost:7002/eureka/ fetch-registry: true register-with-eureka: true
|
Eureka常见配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:8001/eureka/ enabled: true registry-fetch-interval-seconds: 30 instance: lease-renewal-interval-in-seconds: 30 lease-expiration-duration-in-seconds: 90 metadata-map: zone: jiangsu hostname: localhost prefer-ip-address: false server: enable-self-preservation: false
|
总结
搭建Eureka服务端
- 引入eureka-server依赖
- 添加@EnableEurekaServer注解
- 在application.yml配置eureka地址
搭建Eureka服务端
- 引入eureka-client依赖
- 添加@EnableDiscoveryClient注解
- 在application.yml配置eureka地址
服务发现
- RestTemplate添加@LoadBalanced注解
- 用服务提供者的服务名称远程调用
Ribbon
Spring Cloud Ribbon 是Spring Cloud Netflix 子项目的核心组件之一,主要给服务间调用及API网关转发提供负载均衡的功能。
在微服务架构中,很多服务都会部署多个,其他服务去调用该服务的时候,如何保证负载均衡是个不得不去考虑的问题。负载均衡可以增加系统的可用性和扩展性,当我们使用RestTemplate来调用其他服务时,Ribbon可以很方便的实现负载均衡功能。
负载均衡流程
负载均衡策略
Ribbon的负载均衡规则由一个IRule的接口定义,每一个接口都是一种规则
所谓的负载均衡策略,就是当A服务调用B服务时,此时B服务有多个实例,这时A服务以何种方式来选择调用的B实例,ribbon可以选择以下几种负载均衡策略。
- com.netflix.loadbalancer.RandomRule:从提供服务的实例中以随机的方式;
- com.netflix.loadbalancer.RoundRobinRule:以线性轮询的方式,就是维护一个计数器,从提供服务的实例中按顺序选取,第一次选第一个,第二次选第二个,以此类推,到最后一个以后再从头来过;
- com.netflix.loadbalancer.RetryRule:在RoundRobinRule的基础上添加重试机制,即在指定的重试时间内,反复使用线性轮询策略来选择可用实例;
- com.netflix.loadbalancer.WeightedResponseTimeRule:对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择;
- com.netflix.loadbalancer.BestAvailableRule:选择并发较小的实例;
- com.netflix.loadbalancer.AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例;
- com.netflix.loadbalancer.ZoneAwareLoadBalancer:采用双重过滤,同时过滤不是同一区域的实例和故障实例,选择并发较小的实例。
Ribbon常用配置
全局配置
1 2 3 4 5 6 7
| ribbon: ConnectTimeout: 1000 ReadTimeout: 3000 OkToRetryOnAllOperations: true MaxAutoRetriesNextServer: 1 MaxAutoRetries: 1 NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
|
指定服务配置
1 2 3 4 5 6 7 8
| user-service: ribbon: ConnectTimeout: 1000 ReadTimeout: 3000 OkToRetryOnAllOperations: true MaxAutoRetriesNextServer: 1 MaxAutoRetries: 1 NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
|
修改负载均衡算法
定义新IRule并注入Bean
1 2 3 4
| @Bean public IRule randomRule() { return new RandomRule(); }
|
修改配置文件
1 2
| ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
|
饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才会创建LoadBalanceClient负载均衡客户端,请求时间会较长。
饥饿加载会在项目启动时创建,降低第一次访问的耗时。
开启饥饿加载
1 2 3 4
| ribbon: eager-load: enabled: true clients: userservice
|
总结
Nacos
Nacos简介
Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案,Nacos 作为其核心组件之一,可以作为注册中心
和配置中心
使用,本文将对其用法进行详细介绍。
Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos 具有如下特性:
- 服务发现和服务健康监测:支持基于
DNS
和基于RPC
的服务发现,支持对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求;
- 动态配置服务:动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置;
- 动态 DNS 服务:动态 DNS 服务支持权重路由,让您更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单DNS解析服务;
- 服务及其元数据管理:支持从微服务平台建设的视角管理数据中心的所有服务及元数据。
Nacos安装
拉取Nacos镜像
1
| docker pull nacos/nacos-server
|
运行Nacos容器
1
| docker run --env MODE=standalone --name nacos -d -p 8848:8848 nacos/nacos-server
|
访问地址
http://xx.xx.xx.xxx:8848/nacos/
初始账号和密码:nacos/nacos
应用注册
添加SpringCloudAlibaba依赖
1 2 3 4 5 6 7 8 9 10 11
| <dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
|
添加Nacos服务发现依赖
1 2 3 4
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
|
修改配置文件
1 2 3 4 5
| spring: cloud: nacos: discovery: server-addr: localhost:8848
|
服务分级存储模型
服务调用尽量选择本地集群的服务,跨地域集群调用延迟较高。
使用服务分级存储模型,当本地集群不可访问时,再去访问其他集群。
修改配置文件
添加spring.cloud.nacos.discovery.cluster-name属性
1 2 3 4 5
| spring: cloud: nacos: discovery: cluster-name: JIANGSU
|
复制运行配置
集群负载均衡
设置负载均衡IRule为NacosRule
1 2 3
| userservice: ribbon: NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
|
NacosRule负载均衡策略
- 优先选择同集群服务实例列表
- 本地集群无法找到服务提供者,才去其他集群寻找并报告警告
- 确定可用实例列表,采用随机负载均衡策略挑选实例
权重负载均衡
由于服务器设备性能存在差异,部分实例所在机器性能较好,需要承担更多的用户请求。
Nacos提供了权重配置来控制访问频率,权重越大,访问频率越高。
实例权重控制
- Nacos控制台可以设置实例的权重值(0-1之间)
- 同集群内的多个实例,权重越高被访问的频率越高
- 权重设置为0,则完全不会被访问
环境隔离
Nacos中服务存储和数据存储的最外层是namespace,用来最外层隔离,隔离不同环境。
创建命名空间
修改配置文件
1 2 3 4
| cloud: nacos: discovery: namespace: 3c30a7da-2359-490d-90f3-c67f0f64d477
|
启动服务
Nacos配置中心
Nacos和Eureka的区别
临时实例和非临时实例
配置非临时实例
1 2 3 4 5
| spring: cloud: nacos: discovery: ephemeral: false
|
临时实例宕机时,从nacos的服务列表中剔除,而非实例则不会
总结
Nacos与Eureka的共同点
- 支持服务注册和服务拉取
- 支持服务提供者以心跳方式做健康检测
Nacos与Eureka的区别点
- Nacos支持服务端主动检测提供者状态:
临时实例
采用心跳
模式,非临时实例
采用主动检测
模式
- 临时实例心跳不正常会被剔除,非临时实例不正常时不会被剔除
- Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
- Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
配置管理
统一配置管理
添加配置
Nacos的dataid的组成格式以及springboot配置文件中属性对应关系
1 2
| ${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
|
获取配置步骤
引入Nacos配置管理客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency>
|
添加配置文件bootstrap.yml
bootstrap.yml文件是引导文件,优先级高于application.yml,主要对Nacos的作为配置中心的功能进行配置
1 2 3 4 5 6 7 8 9 10 11 12
| spring: application: name: demo-order profiles: active: dev cloud: nacos: discovery: server-addr: 1.117.34.49:8848 config: server-addr: 1.117.34.49:8848 file-extension: yaml
|
创建ConfigClientController从Nacos配置中心获取配置信息
1 2 3 4 5 6 7 8 9 10
| @RestController public class ConfigClientController { @Value("${config.info}") private String configInfo;
@GetMapping("/configInfo") public String getConfigInfo() { return configInfo; } }
|
配置热更新
Nacos中的配置文件变更后,微服务无需重启就可以感知
- 方式一:在
@Value
注入变量所在类上添加注解@RefreshScope
- 方式二:使用
@ConfigurationProperties
注解
1 2 3 4 5 6
| @Data @Component @ConfigurationProperties(prefix = "config") public class ConfigInfoProperties { private String info; }
|
1 2 3 4 5 6 7 8 9 10
| @RestController public class ConfigClientController { @Resource private ConfigInfoProperties configInfoProperties;
@GetMapping("/configInfo") public String getConfigInfo() { return configInfoProperties.getInfo(); } }
|
多环境配置共享
项目启动时会从Nacos读取多个配置文件,无论profile如何变化,[spring.application.name].[spring.cloud.nacos.config.file-extension]一定会被加载
- ```yml
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}1 2 3
| - ``` ${spring.application.name}.${spring.cloud.nacos.config.file-extension}
|
1 2 3 4 5 6 7 8
| @Data @Component @ConfigurationProperties(prefix = "config") public class ConfigInfoProperties { private String info; private String shared; }
|
1 2 3 4 5 6 7
| @RestController public class ConfigClientController { @GetMapping("/shared") public String shared() { return configInfoProperties.getShared(); } }
|
多种配置的优先级
- 服务名称-profile.yaml > 服务名称.yaml > 本地配置
Nacos集群搭建
Nacos节点
节点 |
ip地址 |
port |
nacos1 |
1.117.34.49 |
8845 |
nacos2 |
1.117.34.49 |
8846 |
nacos3 |
1.117.34.49 |
8847 |
集群搭建
docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| version: '3' services: docker-nacos-server-1: image: nacos/nacos-server container_name: nacos-server-1 ports: - "8845:8848" - "9555:9555" restart: on-failure network_mode: host environment: SPRING_DATASOURCE_PLATFORM: mysql NACOS_SERVERS: 1.117.34.49:8845 1.117.34.49:8846 1.117.34.49:8847 NACOS_APPLICATION_PORT: 8845 MYSQL_SERVICE_HOST: 1.117.34.49 MYSQL_SERVICE_PORT: 3306 MYSQL_SERVICE_DB_NAME: nacos_config MYSQL_SERVICE_USER: root MYSQL_SERVICE_PASSWORD: root JVM_XMS: 256m JVM_XMX: 256m JVM_XMN: 256m volumes: - /mydata/nacos-cluster/nacos1/logs:/home/nacos/logs - /mydata/nacos-cluster/nacos1/init.d/custom.properties:/home/nacos/init.d/custom.properties docker-nacos-server-2: image: nacos/nacos-server container_name: nacos-server-2 ports: - "8846:8848" restart: on-failure network_mode: host environment: SPRING_DATASOURCE_PLATFORM: mysql NACOS_SERVERS: 1.117.34.49:8845 1.117.34.49:8846 1.117.34.49:8847 NACOS_APPLICATION_PORT: 8845 MYSQL_SERVICE_HOST: 1.117.34.49 MYSQL_SERVICE_PORT: 3306 MYSQL_SERVICE_DB_NAME: nacos_config MYSQL_SERVICE_USER: root MYSQL_SERVICE_PASSWORD: root JVM_XMS: 256m JVM_XMX: 256m JVM_XMN: 256m volumes: - /mydata/nacos-cluster/nacos2/logs:/home/nacos/logs - /mydata/nacos-cluster/nacos2/init.d/custom.properties:/home/nacos/init.d/custom.properties docker-nacos-server-3: image: nacos/nacos-server container_name: nacos-server-3 ports: - "8847:8848" restart: on-failure network_mode: host environment: SPRING_DATASOURCE_PLATFORM: mysql NACOS_SERVERS: 1.117.34.49:8845 1.117.34.49:8846 1.117.34.49:8847 NACOS_APPLICATION_PORT: 8845 MYSQL_SERVICE_HOST: 1.117.34.49 MYSQL_SERVICE_PORT: 3306 MYSQL_SERVICE_DB_NAME: nacos_config MYSQL_SERVICE_USER: root MYSQL_SERVICE_PASSWORD: root JVM_XMS: 256m JVM_XMX: 256m JVM_XMN: 256m volumes: - /mydata/nacos-cluster/nacos3/logs:/home/nacos/logs - /mydata/nacos-cluster/nacos3/init.d/custom.properties:/home/nacos/init.d/custom.properties
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| upstream nacos-cluster { server 1.117.34.49:8845; server 1.117.34.49:8846; server 1.117.34.49:8847; }
server { listen 80; listen [::]:80; server_name localhost; location /nacos { proxy_pass http://nacos-cluster } }
|
Feign
远程调用
RestTemplate调用存在的问题
1 2
| CommonResult commonResult = restTemplate.getForObject("http://demo-user/member/" + omsOrder.getMemberId(), CommonResult.class); UmsMember umsMember = BeanUtil.toBean(commonResult.getData(), UmsMember.class);
|
Fegin介绍
Spring Cloud OpenFeign 是声明式的服务调用工具,它整合了Ribbon和Hystrix,拥有负载均衡和服务容错功能。
Feign是声明式的服务调用工具,我们只需创建一个接口并用注解的方式来配置它,就可以实现对某个服务接口的调用,简化了直接使用RestTemplate来调用服务接口的开发量。Feign具备可插拔的注解支持,同时支持Feign注解、JAX-RS注解及SpringMVC注解。当使用Feign时,Spring Cloud集成了Ribbon和Eureka以提供负载均衡的服务调用及基于Hystrix的服务容错保护功能。
添加依赖
1 2 3 4
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
|
启动类添加@EnableFeignClients来启动Feign客户端功能
1 2 3 4 5 6 7 8
| @EnableFeignClients @EnableDiscoveryClient @SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }
|
添加UserService接口完成服务接口绑定
我们通过@FeignClient注解实现了一个Feign客户端,其中的value为user-service表示这是对demo-user服务的接口调用客户端。我们可以回想下user-service中的UserController,只需将其改为接口,保留原来的SpringMvc注释即可。
1 2 3 4 5
| @FeignClient("demo-user") public interface UserService { @GetMapping(value = "/member/{id}") CommonResult detail(@PathVariable Long id); }
|
调用UserService实现服务调用
1 2
| CommonResult commonResult = userService.detail(omsOrder.getMemberId()); UmsMember umsMember = BeanUtil.toBean(commonResult.getData(), UmsMember.class);
|
自定义配置
Feign自定义配置覆盖默认配置
类型 |
作用 |
说明 |
feign.Logger.Level |
修改日志级别 |
四种不同的级别:NONE、BASIC、HEADERS、FULL |
feign.codec.Decoder |
响应结果的解析器 |
解析HTTP远程调用的返回结果 |
feign.codec.Encoder |
请求参数编码 |
编码HTTP远程调用的请求参数 |
feign.Contract |
支持的注解格式 |
默认是SpringMVC注解 |
feign.Retryer |
失败重试机制 |
请求失败的重试机制,默认没有,使用Ribbon的重试机制 |
Feign自定义配置
配置方式
1 2 3 4 5
| feign: client: config: default: # 用default是全局配置,用服务名称是局部服务配置 loggerLevel: FULL # 日志级别
|
1 2 3 4 5
| feign: client: config: demo-user: # 用default是全局配置,用服务名称是局部服务配置 loggerLevel: FULL # 日志级别
|
Bean方式
配置类声明一个Bean
1 2 3 4 5 6
| public class FeignClientConfiguration { @Bean public Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } }
|
- 全局配置,启动类@EnableFeignClient注解中
1
| @EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)
|
1
| @FeignClient(value = "demo-user", configuration = FeignClientConfiguration.class)
|
性能优化
Feign底层客户端实现
- URLConnection:默认实现,不支持连接池
- Apache HttpClient:支持连接池
- OKHttp:支持连接池
优化Feign的性能主要包括
- 使用
连接池
代替默认的URLConnection
- 日志级别,最好使用basic或none
添加HttpClient的支持
引入依赖
1 2 3 4 5
| <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>
|
配置连接池
1 2 3 4 5
| feign: httpclient: enabled: true max-connections: max-connections-per-route: 50
|
最佳实践
- 方式一-继承:给消费者的FeignClient和提供者的Controller定义统一的父接口作为标准
- 方式二-抽取:将FeignClient抽取为独立通用模块,默认的Feign都放到这个模块中提供给消费者使用
当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用
1
| @EnableFeignClients(basePackages = "com.example.demo.feign")
|
1
| @EnableFeignClients(clients={UserService.class})
|
服务降级
Feign中的服务降级只需要Feign客户端定义的接口添加一个服务降级处理的实现类即可
导入依赖
1 2 3 4 5
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
|
添加服务降级实现类UserFallbackService
继承接口并对接口中的每个实现方法进行了服务降级逻辑的实现
1 2 3 4 5 6 7 8
| @Component public class UserFallbackService implements UserService { @Override public CommonResult detail(Long id) { UmsMember defaultMember = new UmsMember(); return CommonResult.success(defaultMember); } }
|
设置服务降级处理类为UserFallbackService
修改@FeignClient注解中的参数,设置fallback为UserFallbackService.class即可。
1 2 3 4 5
| @FeignClient(value = "demo-user", fallback = UserFallbackService.class) public interface UserService { @GetMapping(value = "/member/{id}") CommonResult detail(@PathVariable Long id); }
|
修改配置文件
1 2 3
| feign: sentinel: enabled: true
|
Gateway
Spring Cloud Gateway 为 SpringBoot 应用提供了API网关支持,具有强大的智能路由与过滤器功能。
Gateway简介
Gateway是在Spring生态系统之上构建的API网关服务,基于Spring 5,Spring Boot 2和 Project Reactor等技术。Gateway旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能, 例如:熔断、限流、重试等。
网关功能
- 身份认证和权限校验
- 服务路由和负载均衡
- 请求限流和服务熔断
Spring Cloud Gateway 具有如下特性:
- 基于Spring Framework 5, Project Reactor 和 Spring Boot 2.0 进行构建;
- 动态路由:能够匹配任何请求属性;
- 可以对路由指定 Predicate(断言)和 Filter(过滤器);
- 集成Hystrix的断路器功能;
- 集成 Spring Cloud 服务发现功能;
- 易于编写的 Predicate(断言)和 Filter(过滤器);
- 请求限流功能;
- 支持路径重写。
相关概念
- Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由;
- Predicate(断言):指的是Java 8 的 Function Predicate。 输入类型是Spring框架中的ServerWebExchange。 这使开发人员可以匹配HTTP请求中的所有内容,例如请求头或请求参数。如果请求与断言相匹配,则进行路由;
- Filter(过滤器):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前后对请求进行修改。
搭建网关模块
添加依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>
|
添加路由配置
Gateway提供了两种不同的方式用于配置路由,一种是通过yml文件来配置,另一种通过Java Bean来配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| server: port: 9001
spring: application: name: gateway-service cloud: nacos: server-addr: 1.117.34.49:8848 gateway: routes: - id: demo-user uri: lb://demo-user predicates: - Path=/member/**
|
1 2 3 4 5 6 7 8 9 10 11
| @Configuration public class GatewayConfig { @Bean public RouteLocator customizeRouteLocator(RouteLocatorBuilder routeLocatorBuilder) { return routeLocatorBuilder.routes() .route("demo-order", r -> r.path("/order/**") .uri("lb://demo-order") ).build(); } }
|
路由配置
- 路由id:路由唯一标识
- uri:路由目的地,支持lb和http两种
- predicates:路由断言,判断请求是否符合要求,符合则转发到路由目的地
- filters:路由过滤器,处理请求或响应
路由断言工厂
配置文件的断言规则是字符串,这些字符串是被Predicate Factory读取并处理,转变为路由判断的条件。
Path按照路径匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory
类来处理。
PredicateFacrtory
的作用是:读取用户定义的断言条件,对于请求做出判断。
Route Predicate的使用
Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapping基础架构的一部分。 Spring Cloud Gateway包括许多内置的Route Predicate工厂。 所有这些Predicate都与HTTP请求的不同属性匹配。 多个Route Predicate工厂可以进行组合,下面我们来介绍下一些常用的Route Predicate。
After Route Predicate
在指定时间之后的请求会匹配该路由
1 2
| predicates: - After=2019-09-24T16:30:00+08:00[Asia/Shanghai]
|
Before Route Predicate
在指定时间之前的请求会匹配该路由
1 2
| predicates: - Before=2019-09-24T16:30:00+08:00[Asia/Shanghai]
|
Between Route Predicate
在指定时间区间内请求会匹配该路由
1 2
| predicates: - Between=2019-09-24T16:30:00+08:00[Asia/Shanghai], 2019-09-25T16:30:00+08:00[Asia/Shanghai]
|
Cookie Route Predicate
带有指定Cookie的请求会匹配该路由
1 2
| predicates: - Cookie=username,user
|
带有指定请求头的请求会匹配该路由
1 2
| predicates: - Header=X-Request-Id, \d+
|
Host Route Predicate
带有指定Host的请求会匹配该路由
1 2
| predicates: - Host=**.somehost.com
|
Method Route Predicate
发送指定方法的请求会匹配该路由
1 2
| predicates: - Method=Get
|
Path Route Predicate
发送指定路径的请求会匹配该路由
1 2
| predicates: - Path=/user/**
|
Query Route Predicate
带指定查询参数的请求可以匹配该路由
1 2
| predicates: - Query=username
|
RemoteAddr Route Predicate
从指定远程地址发起的请求可以匹配该路由
1 2
| predicates: - RemoteAddr=192.168.1.1/24
|
Weight Route Predicate
使用权重来路由相应请求,以下表示有80%的请求会被路由到localhost:8201,20%会被路由到localhost:8202。
1 2 3 4 5 6 7 8 9 10 11 12
| spring: cloud: gateway: routes: - id: weight_high uri: http://localhost:8201 predicates: - Weight=group1, 8 - id: weight_low uri: http://localhost:8202 predicates: - Weight=group1, 2
|
过滤器工厂
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理。
路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用。Spring Cloud Gateway 内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生。
配置方式
1 2 3 4 5 6 7 8
| spring: cloud: gateway: routes: - id: demo-user uri: lb://demo-user filters: - AddRequestParameter=username, user
|
1 2
| default-filters: - AddRequestParameter=username, user
|
AddRequestParameter GatewayFilter
给请求添加参数的过滤器
1 2 3 4 5 6 7 8 9 10
| spring: cloud: gateway: routes: - id: demo-user uri: lb://demo-user filters: - AddRequestParameter=username, user predicates: - Path=/member/**
|
1 2
| filters: - AddRequestParameter=username, user
|
StripPrefix GatewayFilter
对指定数量的路径前缀进行去除的过滤器
1 2
| filters: - StripPrefix=2
|
全局过滤器
全局过滤器的作用是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用。区别在于GatewayFilter通过配置定义,处理逻辑是固定的。定义方式是实现GlobalFilter接口。
简单拦截认证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Order(0) @Component public class AuthorizeFilter implements GlobalFilter {
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); MultiValueMap<String, String> queryParams = request.getQueryParams(); String authorization = queryParams.getFirst("authorization"); if ("admin".equals(authorization)) { return chain.filter(exchange); } exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); return exchange.getResponse().setComplete(); } }
|
总结
全局过滤器的作用
对所有路由生效的过滤器并可以自定义处理逻辑
实现全局过滤器的步骤
- 继承GlobalFilter接口并实现方法自定义处理逻辑
- 添加@Order注解或实现Ordered接口说明过滤链顺序
过滤器链
请求进入网关会遇到三种类型的路由器:当前路由的过滤器、DefaultFilter以及GlobalFilter。
请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter合并到一个过滤器链(集合)中,排序后依次执行每个过滤器。
过滤器执行顺序
- 每个过滤器必须指定一个int类型的order值,ordre值越小,优先级越高,执行顺序越靠前
- GlobalFilter通过实现ordered接口或者添加@Order注解来指定order值
- 路由过滤器和defaultFilter的order由spring指定,默认是按照声明顺序从1递增
- 当过滤器的order值一样时,会按照defaultFilter > 路由过滤器 > GlobaFilter的顺序执行
总结
- order值越小,优先级越高
- 当order值一样时,顺序是defaultFilter最先,然后是局部路由过滤器,最后是全局过滤器
跨域问题
跨域问题:协议、主机以及端口任一不同,浏览器禁止请求的发起者和服务端发生跨域请求。请求被浏览器拦截的问题。
跨越问题处理
网关处理跨域同样采用CORS方案,只需简单的配置即可实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| spring: gateway: globalcors: add-to-simple-url-handler-mapping: true cors-configurations: '[/**]': allowedOrigins: - "http://1.117.34.49:9001" allowedMethods: - "GET" - "POST" - "DELETE" - "PUT" - "OPTIONS" allowedHeaders: "*" allowCredentials: true maxAge: 360000
|
网关跨域配置核心参数
- 允许跨域的域名
- 允许跨域的请求头
- 允许跨域的请求方式
- 是否允许使用cookie
- 跨域的有效期
Seata
Seata是Alibaba开源的一款分布式解决方案,致力于提供高性能和简单易用的分布式事务服务。
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
分布式事务问题
事务ACID原则
在微服务架构中由于全局数据一致性没法保证产生的问题就是分布式事务问题。简单来说,在分布式系统下,一次业务操作需要操作多个数据源或需要进行远程调用,每个服务是一个分支事务,要保证所有分支事务最终状态一致性,就会产生分布式事务问题。
单体应用中,一个业务操作需要调用三个模块完成,此时数据的一致性由本地事务
保证。
随着业务需求的变化,单体应用被拆分为微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用独立的数据源,业务操作需要调用三个服务完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题无法保证。
微服务下单业务中,下单时会调用订单服务,创建订单并写入数据库。然后订单服务调用账户服务和库存服务:
- 账户服务负责扣减用户余额
- 库存服务负责扣减商品库存
理论基础
CAP定理
1998年,加州大学的计算机科学家Eric Brewer提出,分布式系统有三个指标:
- Consistency(一致性)
- Availiability(可用性)
- Partition tolerance(分区容错性)
分布式系统无法同时满足这三个指标,这个结论即CAP定理
Consistency
Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须是一致的。
Availability
Availablility(可用性):用户访问集群中的任意健康节点,必须得到响应,而不是超时或者拒绝
Partition tolerance
Partion(分区):因为网络故障或者其他原因导致分布式系统中的部分节点与其他节点失去连接,形成独立分区。
Tolerance(容错):在集群出现分区时,整个系统要持续对外提供服务。
总结
CAP定理内容
分布式系统节点通过网络连接,一定会出现分区问题
当分区出现时,系统的一致性和可用性无法同时满足
思考:ElasticSearch集群时CP还是AP
ElasticSearch集群出现分区时,故障节点会被剔除集群,数据分配会重新分配到其他节点,保证数据一致。因此是低可用性,高一致性,属于CP
BASE理论
Base理论是对CAP的一种解决思路,包含三个思想:
- Basically Available(基本可用):分布式系统中出现故障时,允许损失部分可用性,即保证核心可用
- Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态
- Eventually Consistent(最终一致性):虽然无法保证一致性,但是在软状态结束后,最终达到数据一致
分布式事务最大的问题是各个子事务的一致性问题,可以借鉴CAP定理和BASE理论:
- AP模式:各个事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致
- CP模式:各个子事务执行后相互等待,同时提交,同时回滚,达成强一致,但事务等待过程中,处于弱可用状态
分布式事务模型
解决分布式事务,各个子系统之间必须可以感知到彼此的事务状态,才能保证状态一致,因此需要一个事务协调者来协调每一个事务的参与者(即子系统事务,称为分支事务,有关联的各个分支事务一起称为全局事务)。
总结
BASE理论的三个思想:
解决分布式事务的思想和模型:
- 全局事务:整个分布式事务
- 分支事务:分布式事务中包含的每个子系统的事务
- 最终一致思想:各分支事务分别执行并提交,如果有不一致的情况,再采用弥补措施恢复数据,实现最终一致
- 强一致思想:各分支事务执行完业务不要提交,等待彼此结果后统一提交或回滚
分布式事务解决方案
二阶段提交-2PC
2PC(2 prepare-commit)通过引入一个事务协调器
来协调各个本地事务即数据参与者
的提交和回滚。
第一阶段-准备阶段:
事务协调器会向每个事务参与者发起一个开启事务的命令,每个事务参与者执行准备操作,然后再向事务协调者回复是否准备完成。此阶段不会提交本地事务,但是资源会被锁住。
第二阶段-提交阶段:
事务协调器收到每个事务参与者的回复后,统计每个参与者的回复,如果每个参与者都回复可以提交,那么事务协调器会发送提交命令,参与者正式提交本地事务,释放所有资源,结束全局事务。如果有一个参与者拒绝提交,那么事务协调器发送回滚命令,所有参与者回滚本地事务,待全部回滚完成,释放资源,取消全局事务。
事务提交流程
事务回滚流程
问题
同步阻塞
会消耗性能
协调者故障问题
,一旦协调器发生故障,所有参与者处理资源锁定状态,那么所有参与者处理资源锁定状态,所有参与者都会被阻塞
网络问题
导致命令没有收到,导致某些事务执行者没有提交
三阶段提交-3PC
三阶段提交时在二阶段提交基础上的改进版本,引入了超时机制,解决了二阶段单点故障的问题,。3PC分为三个阶段:CanCommit
,PreCommit
,DoCommit
。核心思想是在PreCommited时并不会锁定资源,除非所有参与者都同意了,才开始锁资源。
第一阶段-CanCommit阶段:
协调者向执行者发送CanCommit请求,询问是否可以执行事务提交操作,然后开始等待执行者响应。执行者接收到请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应并进入预备状态,否则返回No。
第二阶段-PreCommit阶段:
协调者根据执行者的反应情况来决定是否可以进入第二阶段事务的PreCommit操作。如果所有的执行者都返回Yes,则协调者向所有执行者发送PreCommit请求,并进入Prepared阶段,执行者接收到请求后,会执行事务操作,并将undo和redo信息记录到事务日志中,如果成功执行,则返回成功响应。
如果所有执行者至少有一个返回No,则协调者向所有执行者发送abort请求,所有的执行者在收到请求超过一段时间没有收到任何请求时,会直接中断事务。
第三阶段-DoCommit阶段:
此阶段是进行真正的事务提交阶段。协调者接收到所有执行者发送的成功响应。阶段从
TTC事务模型
TTC补偿事务:Try、Confirm、Cancel
Try:尝试,即尝试预留资源,锁定资源
Confirm:确认,即执行预留的资源,如果执行失败会重试
Cancel:取消,撤销预留的资源,如果执行失败会重试
Seata
Seata是2019年1月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案致力于提供高可用和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。
角色
Seata事务管理中有三个重要的角色:
- TC(Transaction Coordinatoe)-事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚
- TM(Transaction Manager)-事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务
- RM(Resource Manager)-资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务的状态,并驱动分支事务提交或回滚
Seata提供了四种不同的分布式事务解决方案:
- XA模式:强一致性分阶段事务模型,牺牲了一定的可用性,无业务侵入
- TTC模式:最终一致的分阶段事务模式,有业务侵入
- AT模式:最终一致的分阶段事务模式,无业务侵入
- SAGA模式:长事务模式,有业务侵入
部署Seata Server
MySQL创建seata
数据库并创建表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| CREATE TABLE IF NOT EXISTS `global_table` ( `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `status` TINYINT NOT NULL, `application_id` VARCHAR(32), `transaction_service_group` VARCHAR(32), `transaction_name` VARCHAR(128), `timeout` INT, `begin_time` BIGINT, `application_data` VARCHAR(2000), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`xid`), KEY `idx_status_gmt_modified` (`status` , `gmt_modified`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE IF NOT EXISTS `branch_table` ( `branch_id` BIGINT NOT NULL, `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `resource_group_id` VARCHAR(32), `resource_id` VARCHAR(256), `branch_type` VARCHAR(8), `status` TINYINT, `client_id` VARCHAR(64), `application_data` VARCHAR(2000), `gmt_create` DATETIME(6), `gmt_modified` DATETIME(6), PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE IF NOT EXISTS `lock_table` ( `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(128), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking', `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_status` (`status`), KEY `idx_branch_id` (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE IF NOT EXISTS `distributed_lock` ( `lock_key` CHAR(20) NOT NULL, `lock_value` VARCHAR(20) NOT NULL, `expire` BIGINT, primary key (`lock_key`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
|
nacos新增配置文件seataServer.properties
docker部署seata
1 2 3
| docker run --name seata \ -p 8091:8091 \ -d seataio/seata-server:1.4.2
|
拷贝容器配置文件到宿主机
1
| docker cp seata:/seata-server/resources/registry.conf /mydata/seata/conf/
|
编辑配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| registry { # 注册中心:file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos"
nacos { application = "seata-server" serverAddr = "1.117.34.49:8848" group = "DEFAULT_GROUP" namespace = "" cluster = "default" username = "nacos" password = "nacos" } }
config { # 配置中心:file、nacos 、apollo、zk、consul、etcd3 type = "nacos"
nacos { serverAddr = "1.117.34.49:8848" namespace = "" group = "DEFAULT_GROUP" username = "nacos" password = "nacos" dataId = "seataServer.properties" } }
|
在/mydata/seata/目录下创建docker-compose.yml文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| version: "3" services: seata-server: image: seataio/seata-server:1.4.2 ports: - "8091:8091" environment: - SEATA_IP=1.117.34.49 - SEATA_PORT=8091 volumes: - "/mydata/seata/conf/registry.conf:/seata-server/resources/registry.conf" expose: - 8091 container_name: seata-server
|
重新运行seata
1 2 3
| docker rm -f seata cd /mydata/seata/ docker-compose up -d
|
查看注册服务
Seata的集成
引入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </exclusion> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.5.1</version> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>1.5.1</version> </dependency>
|
编辑配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| seata: registry: type: nacos nacos: server-addr: 1.117.34.49:8848 namespace: "" group: DEFAULT_GROUP application: seata-server username: nacos password: nacos tx-service-group: fsp_tx_droup service: vgroup-mapping: fsp_tx_droup: default
|
总结
nacos服务名称组成包括:namespace+group+serviceName+cluster
seata客户端获取tc的cluster名称方式:以tx-group-service的值为key到vgroupMapping中查找
Seata实践
XA模式
XA规范是X/Open组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA规范描述全局TM与局部RM之间的接口,几乎所有主流的数据库对XA规范提供了支持。
seata的XA模式
总结
优点:
- 事务强一致性,满足ACID原则
- 常用数据库都支持,实现简单,并且没有代码侵入
缺点:
- 一阶段需要锁定数据库资源,等待二阶段结束才释放资源,性能较差
- 依赖关系性数据库实现事务
实现
1、修改配置文件,开启XA模式
1 2
| seata: data-source-proxy-mode: XA
|
2、全局事务的入口方法添加@GlobalTransactional注解
1 2 3 4 5
| @Override @GlobalTransactional public int insert(OmsOrder omsOrder) { }
|
AT模式
AT模式是分阶段提交的事务模型,与XA模式相比弥补了资源锁定周期过长的缺陷
AT模式原理
AT模式和XA模式最大的区别
AT模式的脏写问题
AT模式的写隔离
在提交事务释放DB锁时,获取全局锁,由tc记录当前正在操作行数据的seata事务,该事务持有全局锁,具备执行权。
当非seata事务管理的全局事务操作数据时,会导致不用获取全局锁就修改掉正在被seata全局锁锁住的数据,如果seata的事务回滚,会导致非seata管理的事务丢失更新,seata基于这种情况,会生成两份快照,一份时seata事务执行前的,一份是seata事务生成后的。当seata管理的事务回滚时,会用seata执行后的事务快照和当前数据进行比较,如果发生不一致的情况,即存在其他非seata事务操作数据会进行记录异常,发送警告,人工介入。
AT模式的优缺点
优点:
- 一阶段直接提交事务,释放数据库资源,性能比较好
- 利用全局锁实现读写隔离
- 没有代码侵入,框架自动完成回滚和提交
缺点:
- 两阶段之间属于软状态,属于最终一致
- 框架的快照功能会影响性能,但是比XA模式要好很多
实现AT模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
TCC模式
TTC模式与AT模式相似,每个阶段都是独立事务,不同的是TTC通过人工编码来实现数据恢复。
- Try:资源的检测和预留
- Confirm:完成资源操作业务(如果Try成功Confirm一定成功)
- Cancel:预留资源的释放(理解为Try的反向操作)
TCC模式原理
TCC工作模型图
总结
TCC模式的每个阶段
- Try:资源检查和预留
- Confirm:业务执行和提交
- Cancel:预留资源的释放
TCC优点:
- 一阶段直接提交事务,释放数据库资源,性能好
- 相比AT模型,无需生成快照,无需使用全局锁,性能最好
- 不依赖数据库事务,而是依赖补偿操作,可以用于非事务数据库
TCC的缺点:
- 有代码侵入,需要人为编写Try、Confirm和Cancel接口
- 软状态,事务最终一致性
- 需要考虑Confirm和Cancel是失败情况,做好冥等性处理
TTC模式的实现
TCC的空回滚:当某分支事务的try阶段阻塞时,可能会导致全局事务超时而触发二阶段的cancel操作,在未执行try操作时先执行了cancel操作,这时cancel不能回滚,要允许空回滚。
对于已经空回滚的业务,如果继续执行try,就永远不可能confirm或cancel,这就是业务悬挂,应当阻止执行空回滚后的try操作,避免悬挂。
1 2 3 4 5 6 7 8
| create table account_freeze_tbl ( xid varchar(128) not null comment '事务id' primary key, member_id bigint not null comment '会员id', freeze_amount decimal(10, 2) unsigned default 0.00 not null comment '冻结金额', state tinyint null comment '状态(0->try 1->confirm 2->cancel)' );
|
声明TCC接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @LocalTCC public interface PmsProductStockTCCService {
@TwoPhaseBusinessAction(name = "prepare", commitMethod = "commit", rollbackMethod = "rollback") int prepare(@BusinessActionContextParameter(paramName = "productId") Long productid, @BusinessActionContextParameter(paramName = "deduction") Integer deduction);
boolean commit(BusinessActionContext businessActionContext);
boolean rollback(BusinessActionContext businessActionContext); }
|
声明TCC接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| @Service public class PmsProductStockTCCServiceImpl implements PmsProductStockTCCService { @Resource private PmsProductService pmsProductService; @Resource private StockFreezeTblMapper stockFreezeTblMapper;
@Override @Transactional(rollbackFor = Exception.class) public int prepare(Long productid, Integer deduction) { String xid = RootContext.getXID();
StockFreezeTbl stockFreezeTbl = stockFreezeTblMapper.selectById(xid); if (stockFreezeTbl != null) { return 0; }
pmsProductService.deduct(productid, deduction); stockFreezeTbl = StockFreezeTbl.builder() .xid(xid) .productId(productid) .freezeStock(deduction) .state(StockFreezeTbl.state.TRY) .build(); return stockFreezeTblMapper.insert(stockFreezeTbl); }
@Override public boolean commit(BusinessActionContext businessActionContext) { String xid = RootContext.getXID(); return stockFreezeTblMapper.deleteById(xid) > 0; }
@Override @Transactional(rollbackFor = Exception.class) public boolean rollback(BusinessActionContext businessActionContext) { String xid = RootContext.getXID();
StockFreezeTbl stockFreezeTbl = stockFreezeTblMapper.selectById(xid); if (stockFreezeTbl == null) { stockFreezeTbl = StockFreezeTbl.builder() .xid(xid) .productId(Long.valueOf(businessActionContext.getActionContext().get("productId").toString())) .freezeStock(Integer.valueOf(businessActionContext.getActionContext().get("deduction").toString())) .state(StockFreezeTbl.state.CANCEL) .build(); stockFreezeTblMapper.insert(stockFreezeTbl); return true; } if (AccountFreezeTbl.State.CANCEL.equals(stockFreezeTbl.getState())) { return true; }
pmsProductService.refund(stockFreezeTbl.getProductId(), stockFreezeTbl.getFreezeStock()); stockFreezeTbl.setFreezeStock(Integer.valueOf(0)) .setState(StockFreezeTbl.state.CANCEL); stockFreezeTblMapper.updateById(stockFreezeTbl); return false; } }
|
SAGA模式
Saga模式是seata提供的长事务解决方案,分为两个阶段
- 一阶段:直接提交本地事务
- 二阶段:成功则什么都不做,失败则编写补偿业务进行回滚
优点:
- 事务参与者可以基于时间驱动实现异步,吞吐能力高
- 一阶段直接提交事务,没有添加锁,性能好
- 不用编写TTC模式的try、confirm、rollback的三个阶段的逻辑代码,实现简单
缺点:
- 软状态持续时间不稳定,时效性差
- 没有锁,事务没有隔离,会有脏写的情况发生
四种模式的对比
Sentinel
初始Sentinel
雪崩问题
雪崩问题:微服务调用链路中的某个服务故障,引起整个链路中的所有微服务不可用
解决方式
- 超时处理:设定超时时间,请求超过一定的时间没有响应将会返回错误的信息,不会无休止的等待
- 舱壁模式(线程隔离):限定每个业务的线程数,避免耗尽整个Tomcat资源
- 熔断降级:由断路器统计业务执行的异常比例,如果超过阈值则将该业务熔断,拦截该业务的一切的请求,无需等待快速释放
- 流量控制:限制业务访问的QPS,避免服务流量突增而带来的故障
总结
服务保护技术对比
隔离策略
Hystrix默认使用线程池隔离,而Sentinel只支持信号量隔离
线程池隔离
当一个业务请求进入Tomcat后,Tomcat会为每一个隔离的业务创建一个独立的线程池进行资源隔离,保证线程隔离。但是随着线程数量的增加,CPU会带来额外上下文切换的消耗,这会影响整个服务的性能。
线程隔离又被称为舱壁隔离
,类似于将船体内部空间区隔划分成若干个隔舱,一旦某几个隔舱发生破损进水,水流不会在其间相互流动,相互隔离。
信号量隔离
本质上就是限制每个业务可以使用的线程数量,公用一个线程池,减少线程的创建和上下文的切换,但是线程的隔离性要比线程隔离差一点
熔断降级策略
Hystrix
限制对某个资源调用的异常比例
Sentinel
在限制上提供了更多的策略选择,基于慢调用比例
,异常比例
和异常数
限流策略
Hystrix
限流策略有限,Sentinel
可以基于 QPS
支持基于调用关系的限流
流量整形
Sentinel
支持流量的整形,支持慢启动,匀速排队模式
控制台
Sentinel
开箱即用,可配置规则,查看秒级监控,机器发现等
Sentinel
Sentinel简介
Sentinel安装
下载jar包
1
| 下载地址:https://github.com/alibaba/Sentinel/releases
|
编辑DockerFile文件
1 2 3 4 5 6 7 8 9 10 11
| FROM java:8
VOLUME /mydata/cloud/sentinel
ADD *.jar /sentinel-dashboard.jar
ENV TZ=Asia/Shanghai
EXPOSE 8080 ENTRYPOINT ["java", "-jar","-Dserver.port=8080","-Dproject.name=sentinel-dashboard","-Dcsp.sentinel.dashboard.server=localhost:8080","-Dsentinel.dashboard.auth.username=sentinel","-Dsentinel.dashboard.auth.password=sentinel","/sentinel-dashboard.jar"]
|
创建并运行容器
1 2
| docker build -t sentinel-server . docker run --name sentinel -d -p 8080:8080 -p 8719:8719 --restart=always --privileged=true -d sentinel-server
|
访问控制台
http://{host}:8858/ 账号:sentinel 密码:sentinel
修改配置
可以修改sentinel的默认端口,账号,密码等
1
| java -jar sentinel-dashboard-1.8.6.jar -Dserver.port=808
|
使用Sentinel
在 order-service
中整合 sentinel
,并且配置 sentinel 客户端的地址
引入依赖
1 2 3 4 5
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
|
配置控制台地址
1 2 3 4 5
| spring: cloud: sentinel: transport: dashboard: localhost:8858
|
访问端点,触发监控
默认情况下 sentinel 会监控 SpringMVC
的 每一个端点(Endpoint)
限流规则
入门
簇点链路
簇点链路:应用内的调用链路,链路中被监控的每一个接口是一个资源。默认情况下,Sentinel
会监控 SpringMVC
的每一个端点(EndPoint
),因此 SpringMVC
的每一个端点(EndPoint
)就是调用链路的一个资源
熔断、流控都是针对簇点链路中的资源来设置的,可以点击对应资源后面的按钮来设置规则
设置流控规则
点击资源/product/{id}
后面的流控按钮,就可以弹出表单,在表单中添加流控规则
上述含义限制/product/{id}
资源的单机QPS为5,即每秒只允许5次请求,超过的请求会被拦截并报错
使用 jmeter
并发测试工具
发现部分请求被 Blocked by Sentinel
即 被 Sentinel
限流
查看 Sentinel
的实时监控
流控模式
在添加限流规则时,点击高级选项,可以选择三种流控模式
- 直接(默认):统计当前资源的请求,触发阈值时对当前资源直接限流
- 关联:统计与当前资源相关的另一个资源,触发阈值时,会对当前资源限流
- 链路:统计从指定链路访问到本资源的请求,触发阈值时,对指定链路限流
关联模式
- 关联模式:统计对于当前资源相关的另一个资源,触发阈值时,对当前资源限流
- 使用场景:比如当用户支付时需要修改订单的状态,如果同时用户需要查询订单,那么查询和更新订单的操作会争抢数据库资源以及数据库锁,产生竞争关系。业务需求是优先支付和更新订单的业务,因此当修改订单业务触发阈值时,需要对查询订单的业务进行限流。
- 实战场景:当/write资源访问量触发阈值时,就会对于/read资源进行限流,避免影响/write资源
添加模拟接口
新增关联模式
给 query 请求新增流控规则
当 update 请求超过 阈值 时 会对于 query 请求进行限流 来保护 update 请求
小结
满足下面条件可以使用关联模式:两个有竞争关系的资源,一个优先级高一个优先级低
链路模式
流控效果
热点参数限流
流量控制
隔离和降级
授权规则
规则持久化