{
"git": {
"branch": "0e9bff9f3008546899af1a871def8e3a9cc852ff",
"commit": {
"id": "0e9bff9",
"time": "2023-06-12T10:28:25Z"
}
},
"build": {
"version": "0.0.1-SNAPSHOT",
"artifact": "start-site",
"versions": {
"spring-boot": "3.1.0",
"initializr": "0.20.0-SNAPSHOT"
},
"name": "start.spring.io website",
"time": "2023-06-12T10:30:20.458Z",
"group": "io.spring.start"
},
"bom-ranges": {
"codecentric-spring-boot-admin": {
"2.4.3": "Spring Boot >=2.3.0.M1 and <2.5.0-M1",
"2.5.6": "Spring Boot >=2.5.0.M1 and <2.6.0-M1",
"2.6.8": "Spring Boot >=2.6.0.M1 and <2.7.0-M1",
"2.7.4": "Spring Boot >=2.7.0.M1 and <3.0.0-M1",
"3.0.4": "Spring Boot >=3.0.0-M1 and <3.1.0-M1"
},
"solace-spring-boot": {
"1.1.0": "Spring Boot >=2.3.0.M1 and <2.6.0-M1",
"1.2.2": "Spring Boot >=2.6.0.M1 and <3.0.0-M1",
"2.0.0": "Spring Boot >=3.0.0-M1"
},
"solace-spring-cloud": {
"1.1.1": "Spring Boot >=2.3.0.M1 and <2.4.0-M1",
"2.1.0": "Spring Boot >=2.4.0.M1 and <2.6.0-M1",
"2.3.2": "Spring Boot >=2.6.0.M1 and <3.0.0-M1",
"3.0.0": "Spring Boot >=3.0.0-M1"
},
"spring-cloud": {
"Hoxton.SR12": "Spring Boot >=2.2.0.RELEASE and <2.4.0.RELEASE",
"2020.0.6": "Spring Boot >=2.4.0.RELEASE and <2.6.0",
"2021.0.7": "Spring Boot >=2.6.0 and <3.0.0",
"2022.0.3": "Spring Boot >=3.0.0 and <3.2.0-M1"
},
"spring-cloud-azure": {
"4.8.0": "Spring Boot >=2.5.0.M1 and <3.0.0-M1",
"5.2.0": "Spring Boot >=3.0.0-M1 and <3.1.0-M1"
},
"spring-cloud-gcp": {
"2.0.11": "Spring Boot >=2.4.0-M1 and <2.6.0-M1",
"3.5.1": "Spring Boot >=2.6.0-M1 and <3.0.0-M1",
"4.3.1": "Spring Boot >=3.0.0-M1 and <3.1.0-M1"
},
"spring-cloud-services": {
"2.3.0.RELEASE": "Spring Boot >=2.3.0.RELEASE and <2.4.0-M1",
"2.4.1": "Spring Boot >=2.4.0-M1 and <2.5.0-M1",
"3.3.0": "Spring Boot >=2.5.0-M1 and <2.6.0-M1",
"3.4.0": "Spring Boot >=2.6.0-M1 and <2.7.0-M1",
"3.5.0": "Spring Boot >=2.7.0-M1 and <3.0.0-M1",
"4.0.0": "Spring Boot >=3.0.0 and <3.1.0-M1"
},
"spring-shell": {
"2.1.10": "Spring Boot >=2.7.0 and <3.0.0-M1",
"3.0.4": "Spring Boot >=3.0.0 and <3.1.0-M1",
"3.1.0": "Spring Boot >=3.1.0 and <3.2.0-M1"
},
"vaadin": {
"14.10.1": "Spring Boot >=2.1.0.RELEASE and <2.6.0-M1",
"23.2.15": "Spring Boot >=2.6.0-M1 and <2.7.0-M1",
"23.3.13": "Spring Boot >=2.7.0-M1 and <3.0.0-M1",
"24.0.6": "Spring Boot >=3.0.0-M1 and <3.1.0-M1",
"24.1.0": "Spring Boot >=3.1.0-M1 and <3.2.0-M1"
},
"wavefront": {
"2.0.2": "Spring Boot >=2.1.0.RELEASE and <2.4.0-M1",
"2.1.1": "Spring Boot >=2.4.0-M1 and <2.5.0-M1",
"2.2.2": "Spring Boot >=2.5.0-M1 and <2.7.0-M1",
"2.3.4": "Spring Boot >=2.7.0-M1 and <3.0.0-M1",
"3.0.1": "Spring Boot >=3.0.0-M1 and <3.1.0-M1"
}
},
"dependency-ranges": {
"okta": {
"1.4.0": "Spring Boot >=2.2.0.RELEASE and <2.4.0-M1",
"1.5.1": "Spring Boot >=2.4.0-M1 and <2.4.1",
"2.0.1": "Spring Boot >=2.4.1 and <2.5.0-M1",
"2.1.6": "Spring Boot >=2.5.0-M1 and <3.0.0-M1",
"3.0.4": "Spring Boot >=3.0.0-M1 and <3.2.0-M1"
},
"mybatis": {
"2.1.4": "Spring Boot >=2.1.0.RELEASE and <2.5.0-M1",
"2.2.2": "Spring Boot >=2.5.0-M1 and <2.7.0-M1",
"2.3.1": "Spring Boot >=2.7.0-M1 and <3.0.0-M1",
"3.0.2": "Spring Boot >=3.0.0-M1"
},
"pulsar": {
"0.2.0": "Spring Boot >=3.0.0 and <3.1.0-M1"
},
"pulsar-reactive": {
"0.2.0": "Spring Boot >=3.0.0 and <3.1.0-M1"
},
"camel": {
"3.5.0": "Spring Boot >=2.3.0.M1 and <2.4.0-M1",
"3.10.0": "Spring Boot >=2.4.0.M1 and <2.5.0-M1",
"3.13.0": "Spring Boot >=2.5.0.M1 and <2.6.0-M1",
"3.17.0": "Spring Boot >=2.6.0.M1 and <2.7.0-M1",
"3.20.5": "Spring Boot >=2.7.0.M1 and <3.0.0-M1",
"4.0.0-M3": "Spring Boot >=3.0.0-M1 and <3.1.0-M1"
},
"picocli": {
"4.7.0": "Spring Boot >=2.5.0.RELEASE and <3.1.0-M1"
},
"open-service-broker": {
"3.2.0": "Spring Boot >=2.3.0.M1 and <2.4.0-M1",
"3.3.1": "Spring Boot >=2.4.0-M1 and <2.5.0-M1",
"3.4.1": "Spring Boot >=2.5.0-M1 and <2.6.0-M1",
"3.5.0": "Spring Boot >=2.6.0-M1 and <2.7.0-M1"
}
}
}
学习推荐版本,由SpringCloud来决定SpringBoot的版本。
包含两个组件:Eureka Server和Eureka Client
各个微服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。
是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载具法的顶载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心
跳,EurekaServer将会从服务注册表中把这个服务节点移除(默认90秒)
组件名 | 语言 | CAP | 服务健康检查 | 对外暴露接口 | SpringCloud集成 |
---|---|---|---|---|---|
Eureka | Java | AP(高可用) | 可配支持 | HTTP | 已集成 |
Zookeeper | Go | CP(数据一致性) | 支持 | HTTP/DNS | 已集成 |
Consul | Java | CP(数据一致性) | 支持 | 客户端 | 已集成 |
Nacos | AP(高可用) | 支持 | 客户端 | 已集成 |
最多只能同时较好的满足两个。
CAP理论的核心是: 一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求。
CAP理论关注粒度是数据,而不是整体系统设计的策略。
因此,根据CAP原理将NoSQL数据库分成了满足CA原则、满足CP原则和满足AP原则三大类:
当网络分区出现后,为了保证一致性,就必须拒绝接请求,否则无法保证一致性。
结论:违背了可用性A的要求,只满足一致性和分区容错,即CP。
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。
Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。
Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。
在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。
我们很容易使用Ribbon实现自定义的负载均衡算法。
LB负载均衡(Load Balance)是什么?
将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。常见的负载均衡有软件Nginx,LVS,硬件F5等。
集中式LB Load Balance
即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5,也可以是软件,1如nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方;
Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡区别
Nginx是服务器负载均衡,客户端所有请求都会交给Nginx,然后由Nginx实现转发请求。即负载均衡是由服务端实现的。
Ribbon本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
Ribbon其实就是一个软负载均衡的客户端组件,
他可以和其他所需请求的客户端结合使用,和eureka结合只是其中的一个实例。
Ribbon在工作时分成两步:
com.netflix.loadbalancer.RoundRobinRule:
轮询
com.netflix.loadbalancer.RandomRule:
随机
com.netflix.loadbalancer.RetryRule:
先按照RoundRobinRule的策骼获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务
weightedResponseTimeRule:
对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择
BestAvailableRuleo:
会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发是最小的服务
vailabilityFilteringRule:
先过滤掉故障实例,再选择并发较小的实例:
zoneAvoidanceRule
默认规则,复合判断server所在区域的性能和server的可用性选择服务器
官方文档明确给出了警告:
这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,
否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。
/**
* Myrule : Ribbon 自定义负载均衡算法配置类
*
* @author zyw
* @create 2023/6/18
*/
@Configuration
public class Myrule {
@Bean
public IRule getIRule(){
//定义为随机
return new RandomRule();
}
}
自定义需要指定的服务
/**
* OderMain80 :
*
* @author zyw
* @create 2023/6/16
*/
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOULD-PAYMENT-SERVICE",configuration = Myrule.class)
@MapperScan("com.zyw.springcloud.dao")
public class OderMain80 {
public static void main(String[] args) {
SpringApplication.run(OderMain80.class,args);
}
}
public class RoundRobinRule extends AbstractLoadBalancerRule {
private AtomicInteger nextServerCyclicCounter;
public RoundRobinRule() {
nextServerCyclicCounter = new AtomicInteger(0);
}
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
//获取有正常运行的(可达的)服务集合
List<Server> reachableServers = lb.getReachableServers();
//获取可负载服务集合
List<Server> allServers = lb.getAllServers();
//获取有正常运行的(可达的)服务的数量
int upCount = reachableServers.size();
//获取可负载服务的数量 == 服务器集群总数量
int serverCount = allServers.size();
if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
int nextServerIndex = incrementAndGetModulo(serverCount);
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}
//自旋锁
//rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,每次服务器重启后rest接口计数从1开始。
private int incrementAndGetModulo(int modulo) {
for (;;) {
//获取当前值
int current = nextServerCyclicCounter.get();
//计算下次值
int next = (current + 1) % modulo;
//比较并交换
if (nextServerCyclicCounter.compareAndSet(current, next))
//得到当前下标值
return next;
}
}
}
/**
* LoadBalancer : 自定义负载均衡算法
*
* @author zyw
* @create 2023/6/20
*/
public interface LoadBalancer {
//收集Eurek上所有活着的服务总数
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
/**
* MyLB : 自定义负载均衡算法实现类
*
* @author zyw
* @create 2023/6/20
*/
@Component
public class MyLB implements LoadBalancer {
private AtomicInteger atomicInteger = new AtomicInteger(0);
public final int getAndIncrement() {
int currrnt;
int next;
do {
currrnt = this.atomicInteger.get();
//Integer.MAX_VALUE = 2147483647
next = currrnt >= 2147483647 ? 0 : currrnt + 1;
//自选锁,直到得到期望值
} while (!this.atomicInteger.compareAndSet(currrnt, next));
System.out.println("第" + next + "次访问");
return next;
}
/**
* rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,每次服务器重启后rest接口计数从1开始。
* @param serviceInstances
* @return
*/
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
}
/**
* OrderController : 订单系统控制层
*
* @author zyw
* @create 2023/6/16
*/
@Slf4j
@RestController
@RequestMapping("consumer/orderController")
@Api(tags={"订单系统控制层"})
public class OrderController {
@Resource
private RestTemplate restTemplate;
@Resource
private DiscoveryClient discoveryClient;
@Resource
private LoadBalancer loadBalancer;
@GetMapping("/lb")
@ApiOperation(value = "获取负载服务的端口号", response = String.class)
public String getPaymentBl(){
List<ServiceInstance> serviceInstances = discoveryClient.getInstances("CLOULD-PAYMENT-SERVICE");
if (serviceInstances == null || serviceInstances.size() <= 0){
return null;
}
ServiceInstance serviceInstance = loadBalancer.instances(serviceInstances);
URI uri = serviceInstance.getUri();
return restTemplate.getForObject(uri+"paymentController/lb",String.class);
}
}
Feign是一个声明式WebService客户端。使用Feign能让编写Web Service客户端更加简单。
它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可拔插式的编码器和解码器。
Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。
Feign可以与Eureka和Ribbon组合使用以支持负载均衡
Feign旨在使编写Java Http客户端变得更容易。
前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon时,自动封装服务调用客户端的开发量。
利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。
而与Ribbon不同的是,通过feign只需要定义
服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
//开启Feign
@EnableFeignClients
@SpringBootApplication
public class OderOpenFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(OderOpenFeignMain80.class, args);
}
}
@Component
@FeignClient(value = "CLOULD-PAYMENT-SERVICE")
public interface PaymentFeginService {
@GetMapping("/paymentController/getById")
public CommonResult getById(@RequestParam("id") Long id);
@GetMapping("/paymentController/feign/timeout")
public String paymentFeignTimeout();
}
默认Feign客户端只等待一秒钟,但是服务端处理需要超过1秒钟,导致Feign客户端不想等待了,直接返回报错。
为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制。
# 设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
# 指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ReadTimeout: 5000
# 指的是建立俩进阶后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000
级别 | 内容 |
---|---|
NONE | 默认的,不显示任何日志 |
BASIC | 仅记录请求方法、URL、响应状态码及执行时间 |
HEADERS | 除了BASIC中定义的信息之外,还有请求和响应的头信息 |
FULL | 除了HEADERS 中定义的信息之外,还有请求和响应的正文及元数据 |
@Configuration
public class FeginConfig {
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
logging:
level:
# feign 日志以什么级别监控哪个接口
com.zyw.springcloud.service.PaymentFeginService: debug
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的**“扇出"**。
如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。
比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。
这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或
者叫雪崩。
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等Hystrix能够保证在一个依赖出
问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
"断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应
(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布
式系统中的蔓延,乃至雪崩。
服务不可用了,不让客户端等待,并立刻返回一个友好提示,fallback。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
超时导致服务器变慢(转圈)-》超时不再等待-》服务降级
出错(宕机或程序运行出错)-》出错要有兜底-》服务降级
生产者正常,消费者自己出现故障或有自我要求(自己的等待时间小于服务提供者响应时间)
设置自身调用后超时时间的峰值,超过峰值做服务降级fallback
一旦调用服务方法失败并抛出错误信息后,会自动调用@HystrixCommand标注好的fallbackMethod调用类中的指定方法
系统运行报错也会走fallbackMethod标注的方法。
/**
* 超时
*
* @param id
* @return
*/
//@HystrixCommand 服务降级规则 响应超过3000毫秒,则执行paymentInfo_TimeOutHandler方法
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
@Override
public String paymentInfo_TimeOut(Integer id) {
int timeNumber = 5;
try {
TimeUnit.SECONDS.sleep(timeNumber);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池:" + Thread.currentThread().getName() + " paymentInfo_OK,id: " + id + "\t" + "O(∩_∩)O哈哈~" + ",耗时" + timeNumber + "秒钟";
}
public String paymentInfo_TimeOutHandler(Integer id) {
return "o(╥﹏╥)o\r\n调用支付接口超时或异常:\t" + "\t当前线程池名称" + Thread.currentThread().getName();
}
主启动类添加@EnableCircuitBreaker注解
@EnableCircuitBreaker
@EnableEurekaClient
@SpringBootApplication
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class,args);
}
}
配置文件中开启 feign 对 hystrix 的支持
feign:
hystrix:
# 开启 feign 对 hystrix 的支持
enabled: true
主启动类添加@EnableHystrix注解
@EnableHystrix
@EnableFeignClients
@SpringBootApplication
public class OrderHystrixMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain80.class,args);
}
}
HystrixCommand:
@GetMapping("/hystrix/TimeOut/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
})
@ApiOperation(value = "超时接口", response = String.class)
public String paymentInfo_TimeOut(@PathVariable("id")Integer id){
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
public String paymentTimeOutFallbackMethod(Integer id) {
return "我是消费者80,对方支付系统繁忙,请10秒后重试\r\no(╥﹏╥)o";
}
需要设置熔断器超时峰值,否则会报错
hystrix:
command:
default:
execution:
isolation:
thread:
# 设置熔断器超时峰值
timeoutInMilliseconds: 5000
@RestController
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
@RequestMapping("consumer")
@Api(tags = {"整合Hystrix订单系统控制层"})
public class OrderHystirxController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/hystrix/OK/{id}")
@HystrixCommand
@ApiOperation(value = "正常接口", response = String.class)
public String paymentInfo_OK(@PathVariable("id") Integer id){
String result = paymentHystrixService.paymentInfo_OK(id);
return result;
}
/**
* 全局fallback方法
* @return
*/
public String payment_Global_FallbackMethod(){
return "payment系统繁忙,请联系客服处理\n" +
"o(╥﹏╥)o";
}
}
如果配置了@HystrixCommand中的fallbackMethod属性,则走专属配置的,没有则走全局的。
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
@GetMapping("/payment/hystrix/OK/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/TimeOut/{id}")
public String paymentInfo_TimeOut(@PathVariable("id")Integer id);
}
@Component
public class PaymentFallbackService implements PaymentHystrixService {
@Override
public String paymentInfo_OK(Integer id) {
return "-----PaymentFallbackService fall back-paymentInfo_OK\r\no(╥﹏╥)o";
}
@Override
public String paymentInfo_TimeOut(Integer id) {
return "-----PaymentFallbackService fall back-paymentInfo_TimeOut\r\no(╥﹏╥)o";
}
}
类似于保险丝达到最大服务访问量后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。
当检测到该节点微服务调用响应正常后,恢复调用链路。
在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,
当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand。
//=====服务熔断=====
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback", commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),//是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),//请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),//时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60")//失败率达到多少后跳闸
})
@Override
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
涉及到断路器的三个重要参数:快照时间窗、请求总数阀值、错误百分比阀值。
1: 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
2∶请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。
3∶错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过
50%的错误百分比,在默认设定50%阀值情况下,这时候就会将断路器打开。
@Service
public class PaymentServiceImpl implements PaymentService {
//=====服务熔断=====
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback", commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),//是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),//请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),//时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60")//失败率达到多少后跳闸
})
@Override
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
if (id < 0) {
throw new RuntimeException("******id不能负数");
}
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName() + "\t" + "调用成功,流水号: " + serialNumber;
}
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {
return "id不能负数,请稍后再试,/(ToT)/~~id: " + id;
}
}
@RestController
@Slf4j
@RequestMapping("/payment")
@Api(tags = {"整合hystrix支付系统控制层"})
public class PaymentController {
@Resource
private PaymentService paymentService;
//=====服务熔断=====
@GetMapping("/hystrix/circuit/{id}")
@ApiOperation(value = "服务熔断接口", response = String.class)
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
String result = paymentService.paymentCircuitBreaker(id);
log.info("result:" + result);
return result;
}
}
对于这一问题,hystrix也为我们实现了自动恢复功能。
当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。
秒杀、高并发等操作,严禁其一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行。
除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard),Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。
Netfiix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboardartifactId>
dependency>
@SpringBootApplication
@EnableHystrixDashboard//开启图形化监控界面
public class HystrixDashboardMain9001 {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardMain9001.class,args);
}
}
http://localhost:9001/hystrix
@EnableCircuitBreaker//开启熔断器
@EnableEurekaClient
@SpringBootApplication
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
/**
* 此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的玩
* ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
* 只要在自己的项目里配置上下面的servlet就可以了
*/
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}
Gateway是在Spring生态系统之上构建的API网关服务,基于Spring 5,Spring Boot 2和Project Reactor等技术。
Gateway旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如:熔断、限流、重试等。
SpringCloud Gateway是Spring Cloud的一个全新项目,基于Spring 5.0+Spring Boot 2.0和Project Reactor等技术开发的网关,它旨在为微服务架构提供—种简单有效的统—的API路由管理方式。
SpringCloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Zuul,在Spring Cloud 2.0以上版本中,没有对新版本的Zuul 2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 1.x非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
Spring Cloud Gateway的目标提供统一的路由方式且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
在SpringCloud Finchley正式版之前,Spring Cloud 推荐的网关是 Netflix提供的Zuul:
传统的Web框架,比如说: struts2,springmvc等都是基于Servlet APl与Servlet容器基础之上运行的。
但是在Servlet3.1之后有了异步非阻塞的支持。而WebFlux是一个典型非阻塞异步的框架,它的核心是基于Reactor的相关API实现的。相对于传统的web框架来说,它可以运行在诸如Netty, Undertow及支持Servlet3.1的容器上。非阻塞式+函数式编程(Spring5必须让你使用java8)
Spring WebFlux是 Spring 5.0引入的新的响应式框架,区别于Spring MVC,它不需要依赖Servlet APl,它是完全异步非阻塞的,并且基于Reactor来实现响应式流规范。
路由是构建网关的基本模块,它由ID,目标URl,一系列的断言和过滤器组成,如果断言为true则匹配该路由
参考的是Java8的java.util.functjon.Predicate
开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
server:
port: 9527
eureka:
instance:
# eureka 服务端的实例名称
hostname: cloud-gateway-service
client:
# false 表示自己端就是注册中心,我的职责就是维护服务实例,而不需要去检索服务
fetch-registry: true
service-url:
# 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址
defaultZone: http://eureka7001.com:7001/eureka
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
- id : payment_routh # 路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 # 匹配后提供服务的路由地址
predicates:
- Path=/payment/getById/** # 断言,路径相匹配的进行路由
- id: payment_routh2
uri: http://localhost:8001 # 匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
@Configuration
public class GateWayConfig {
/**
* 配置了一个id为route-name的路由规则
* 当访问地址 https://localhost:9527/guoji 时会自动转发到地址:https://news.baidu.com/guonei
* @param routeLocatorBuilder
* @return
*/
@Bean
//路由定位器
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("path_route_zyw",
r -> r.path("/guonei")
.uri("https://news.baidu.com/guonei")).build();
return routes.build();
}
}
默认情况下,GateWay会根据注册中心注册的服务列表,以注册中心上微服务名称为路径创建动态路由进行转发。
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id : payment_routh # 路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment-service # 匹配后提供微服务的路由地址
predicates:
- Path=/payment/getById/** # 断言,路径相匹配的进行路由
- id: payment_routh2
uri: lb://cloud-payment-service # 匹配后提供微服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
需要两个参数,Cookie name和正则表达式
路由规则会通过获取对应的Cookie name值和正则表达式去匹配,如果匹配上就会执行路由。
属性名+正则表达式
规定请求方式
规定必传参数
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id : payment_routh # 路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment-service # 匹配后提供微服务的路由地址
predicates:
- Path=/payment/** # 断言,路径相匹配的进行路由
- After=2023-06-26T15:32:05.258+08:00[Asia/Shanghai] # 配置启用时间
- Before=2123-06-26T15:32:05.258+08:00[Asia/Shanghai] # 配置停用时间
- Between=2023-06-26T15:32:05.258+08:00[Asia/Shanghai],2123-06-28T15:32:05.258+08:00[Asia/Shanghai] # 配置可用时间范围
- Cookie=username,zyw
- Header=X-Request-Id,\d+ # 请求头要有 X-Request-Id属性并且值为整数的正则表达式
- Method=Get # 规定请求的方式
- Query=idCard,\d+ # 参数名要有idCard且值必须为整数
时间格式生成工具类:
import java.time.ZonedDateTime;
public class DateTest {
public static void main(String[] args) {
ZonedDateTime zbj = ZonedDateTime.now();
System.out.println("zbj = " + zbj);
}
}
生命周期:pre ==》 post
种类:GatewayFilter(单一的)、GlobalFilter(全局的)
SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。
分为服务端和客户端
服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口
客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容
组件:
一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
Nacos = Eureka + Config + Bus
父工程
com.alibaba.cloud
spring-cloud-alibaba-dependencies
2.1.0.RELEASE
pom
import
子工程
<!-- SpringCloud alibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-nacos-discovery</artifactId>
</dependency>
server:
port: 9001
servlet:
context-path: /provider
spring:
# 服务名称
application:
# 服务名称
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
C :所有节点在同一时间看到的数据是一致的
A:所有请求都会收到响应
一般来说,
如果不需要存储服务级别的信息且服务实例是通过nacos-cient注册,并能够保持心跳上报,那么就可以选择AP模式。当前主流的服务如Spring cloud 和Dubo服务,都适用于AP模式,AP模式为了服务的可能性而减弱了一致性,因此AP模式下只支持注册临时实例。
如果需要在服务级别编辑或者存储配置信息,那么CP是必须,K8S服务和DNS服务则适用于CP模式。
CP模式下则支持注册持久化实例,此时则是以Raft协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误。
Nacos同springcloud-config一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动。
springboot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application
修饰Controller层可以支持Nacos的动态刷新功能
bootstarp.yml
server:
port: 3377
spring:
# 服务名称
application:
# 订单服务
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 # Nacos 服务注册中心地址
config:
server-addr: localhost:8848 # Nacos 配置中心地址
file-extension: yaml # 指定yaml格式的配置
group: TEST_GROUP
namespace: 6536b558-4546-48a3-ba53-eaf9e264006d
applicaton.yml
spring:
profiles:
active: test # 表示测试环境
# active: dev # 表示开发环境
最后公式: s p r i n g . a p p l i c a t i o n . n a m e − {spring.application.name}- spring.application.name−{spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
namespace用于区分部署环境
Group和DataID逻辑上区分两个目标对象
默认情况:Namespace=public,Group=DEFAULT_GROUP,默认Cluster是DEFAULT
Nacos默认的命名空间是public,Namespace主要用来实现隔离。
比方说我们现在有三个环境:开发、测试、生产环境,我们就可以创建三个Namespace,不同的Namespace之间是隔离的。
Group默认是DEFAULT_GROUP,Group可以把不同的微服务划分到同一个分组里面去
Service就是微服务;一个Service可以包含多个Cluster (集群),Nacos默认Cluster是DEFAULT,Cluster是对指定微服务的一个虚拟划分。
比方说为了容灾,将Service微服务分别部署在了杭州机房和广州机房,
这时就可以给杭州机房的Service微服务起一个集群名称(HZ) ,
给广州机房的Service微服务起一个集群名称(GZ),还可以尽量让同一个机房的微服务互相调用,以提升性能。
最后是lnstance,就是微服务的实例。
默认Nacos使用嵌入式数据库实现数据的存储。所以,如果启动多个默认配置下的Nacos节点,数据存储是存在一致性问题的。
为了解决这个问题,Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储。
分布式系统的流量防卫兵
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。
Sentinel以流星为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
丰富的应用场景: Sentinel承接了阿里巴巴近10年的双十一大促流量的核心场景例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
完备的实时监控: Sentinel同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至500台以下规模的集群的汇总运行情况。
广泛的开源生态: Sentinel提供开箱即用的与其它开源框架/库的整合模块,例如与Spring Cloud、Dubbo、gRPC的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入Sentinel。
完善的SPI扩展点: Sentinel提供简单易用、完善的SPI扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
Hystrix | Sentinel |
---|---|
需要手工搭建监控平台:DashBoard | 单独一个组件独立出来 |
没有一套Web界面可以进行更加颗粒化的配置流量监控、速率控制、服务熔断、服务降级 | 支持界面化的细粒度统一配置 |
核心库(Java客户端)不依赖任何框架/库,能够运行于所有Java运行时环境,同时对 Dubbo /Spring Cloud 等框架也有较好的支持。
控制台(Dashboard)基于Spring Boot 开发,打包后可以直接运行,不需要额外的Tomcat等应用容器。
启动命令:
java -jar sentinel-dashboard-1.7.1.jar
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
server:
port: 8401
servlet:
context-path: /sentinel
spring:
# 服务名称
application:
# 订单服务
name: nacos-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 # Nacos 服务注册中心地址
namespace: 13e20987-6fcc-41d6-a358-e16b86bce522
config:
server-addr: localhost:8848 # Nacos 配置中心地址
file-extension: yaml # 指定yaml格式的配置
group: DEV_GROUP
namespace: 13e20987-6fcc-41d6-a358-e16b86bce522
sentinel:
transport:
# 配置Sentinel dashboard地址
dashboard: localhost:8080
# 默认8719端口,加入被占用会自动从8719开始依次+1扫描,知道找到未被占用的端口
port: 8719
management:
endpoints:
web:
exposure:
include: "*"
资源名: 唯一名称,默认请求路径
针对来源: Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
阈值类型/单机阈值:
是否集群: 不需要集群
流控模式:
流控效果:
==》快速失败
Warm Up ( RuleConstant.CONTROL_BEHAVIOR_MARM_UuP)方式,即预热/冷启动方式。
当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
默认 coldFactor为3,即请求QPS从threshold / 3 开始,经预热时长逐渐升致设定的QPS阈值。
匀速排队( RuleConstant.CONTROL_BEHAVIOR_RATE_LINITER )方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
用于处理间隔性突发的流量,例如消息队列。
在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积。Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出DegradeException )。
Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错谒。
当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出DegradeException) 。
平均响应时间(DEGRADE GRADE_RT ):当1s内持续进入5个请求,对应时刻的平均响应时间(秒级)均超过阈值( count,以ms为单位),那么在接下的时间窗口( DegradeRule中的timewindow,以s 为单位)之内,对这个方法的调用都会自动地熔断(抛出DegradeException )。注意Sentinel默认统计的RT上限是4900ms,超出此阈值的都会算作4900ms,若需要变更此上限可以通过启动配置项-Dcsp.sentinel.statistic.max.rt=xxx来配置。
每秒钟进来10个线程,我们Sentinel上配置的是希望200毫秒处理完本次任务,如果没有处理完,在未来1秒钟的时间窗口内,断路器打开,微服务不可用。
QPS >= 5且异常比例(秒级统计)超过阈值时,触发降级;时间窗口结束后,关闭降级
异常比例( DEGRADE_GRADE_EXCEPTION_RATIO ):当资源的每秒请求量>= 5,并且每秒异常总数占通过量的比值超过阈值( DegradeRule中的 count )之后,资源进入降级状态,即在接下的时间窗口( DegradeRule中的 timewindow,以s为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是[0.0,1.0],代表0%- 100%。
异常数(分钟统计)超过阈值时,触发降级;时间窗口结束后,关闭降级
异常数( DE6RADE_GRADE_EXCEPTION_COUNT ):当资源近1分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timewindow小于60s,则结束熔断状态后仍可能再进入熔断状态。
时间窗口一定要大于等于60S。
半开状态:系统自动会去检测是否请求有异常,没有异常就关闭断路器恢复使用,有异常则继续打开断路器不可用。(Hystrix)
注意:异常降级仅针对业务异常,对Sentinel限流降级本身的异常(BlockException)不生效。为了统计异常比例或异常数,需要通过Tracer.trace(ex)记录业务异常。
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的Top K数据,并对其访问进行限制。
比如:
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
Sentinel利用LRU策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。
源码:
Entry entry = null;
try {
entry = SphU.entry(resourceName,EntryType.IN,1,paramA,paramB);
// Your logic here.
}catch (BlockException ex) {
//Handle request rejection.
}finally{
if (entry != null) {
entry.exit(1,paramA,paramB);
}
}
测试案例:
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey",blockHandler = "deal_testHotKey")
public String testHotKey(@RequestParam(value = "p1",required = false)String p1,
@RequestParam(value = "p2",required = false)String p2){
return "----testHotKey";
}
//兜底方法
public String deal_testHotKey(String p1, String p2, BlockException exception){
//Sentinel系统默认提示:Blocked by Sentinel (flow limiting)
return "deal_testHotKey,o(╥﹏╥)o";
}
@SentinelResource(value = “testHotKey”,blockHandler = “deal_testHotKey”)
第一个参数只要QPS超过每秒1次,马上降级处理,用自定义的deal_testHotKey兜底方法
@SentinelResource
处理的是Sentinel控制台配置的违规情况,有blockHandler 方法配置的兜底处理
主管配置出错,运行出错该走异常走异常
Sentinel系统自适应限流从整体维度对应更入口流量进行控制,结合应用的Load、CPU使用率、总体平均RT、入口QPS和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统规则支持以下的模式:
自定义异常兜底处理类
public class CustomerBlockFallback {
public static CommonResult handlerFallback(@PathVariable("id") Long id,Throwable e){
return CommonResult.BlockHandler("兜底异常--Fallback,exception内容:"+e.getMessage());
}
}
自定义Sentinel控制台配置违规处理类
public class CustomerBlockHandler {
public static CommonResult handlerblock(@PathVariable("id") Long id, BlockException exception) {
return CommonResult.BlockHandler(exception.getMessage());
}
}
业务层
@Service
public class PaymentServiceImpl {
@Resource
private PaymentService paymentService;
@SentinelResource(value = "getPaymentById",
fallbackClass = CustomerBlockFallback.class,
fallback = "handlerFallback", //fallback只负责业务异常
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "handlerblock", //只负责Sentinel控制台配置违规
exceptionsToIgnore = {IllegalArgumentException.class} //排除该异常的兜底方法
)
public CommonResult<Payment> getPaymentById(Long id) {
if (id<0){
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常");
}
CommonResult<Payment> paymentById = paymentService.getPaymentById(id);
if (paymentById.getData()==null){
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}
return paymentById;
}
}
Sentinel | Hystrix | resilience4j | |
---|---|---|---|
隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔离/信号量隔离 | 信号量隔离 |
熔断降级策略 | 基于响应时间、异常比例、异常数 | 基于异常比例 | 基于异常比例、响应时间 |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于RxJava) | Ring Bit Buffer |
动态规则配置 | 支持多种数据源 | 支持多种数据源 | 有限支持 |
扩展性 | 多个扩展点 | 插件的形式 | 接口的形式 |
基于注解的支持 | 支持 | 支持 | 支持 |
限流 | 基于QPS,支持基于调用关系的限流 | 有限的支持 | Rate Limiter |
解决方案:保存进Nacos(官方要求)、Mysql、Redis
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
11.13.2 配置文件
spring:
# 服务名称
application:
# 订单服务
name: nacos-payment-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848 # Nacos 服务注册中心地址
namespace: 13e20987-6fcc-41d6-a358-e16b86bce522
config:
server-addr: localhost:8848 # Nacos 配置中心地址
file-extension: yaml # 指定yaml格式的配置
group: DEV_GROUP
namespace: 13e20987-6fcc-41d6-a358-e16b86bce522
sentinel:
transport:
# 配置Sentinel dashboard地址
dashboard: localhost:8080
# 默认8719端口,加入被占用会自动从8719开始依次+1扫描,知道找到未被占用的端口
port: 8719
datasource:
dsl:
nacos:
server-addr: localhost:8848
namespace: 13e20987-6fcc-41d6-a358-e16b86bce522
dataId: nacos-payment-consumer
groupId: DEV_GROUP
data-type: json
rule-type: flow
resource: 资源名称;
limitApp: 来源应用;
grade: 阈值类型,0表示线程数,1表示QPS;
count: 单机阈值;
strategy: 流控模式,0表示直接,1表示关联,2表示链路;
controlBehavior: 流控效果,0表示快速失败,1表示Warm Up,2表示排队等待;
clusterMode: 是否集群。
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。
此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
一次业务操作需要跨多个数据源或者需要跨多个系统进行远程调用,就会产生分布式事务问题。
案例:
用户购买商品的业务逻辑:
概念:Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
分布式事务的处理过程:1 ID+ 3 组件模型
1 ID:全剧唯一的事务ID
术语3组件:
# Copyright 1999-2019 Seata.io Group.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
server:
port: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: ${user.home}/logs/seata
extend:
logstash-appender:
destination: 127.0.0.1:4560
kafka-appender:
bootstrap-servers: 127.0.0.1:9092
topic: logback_to_logstash
console:
user:
username: seata
password: seata
seata:
config:
# support: nacos, consul, apollo, zk, etcd3
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: c7e4e5e4-8693-40e1-b75e-1ce1b46bf976
group: SEATA_GROUP
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key: ""
#secret-key: ""
data-id: seataServer.yaml
username: nacos
password: nacos
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: nacos
# preferred-networks: 30.240.*
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace: c7e4e5e4-8693-40e1-b75e-1ce1b46bf976
cluster: default
username: nacos
password: nacos
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key: ""
#secret-key: ""
store:
# support: file 、 db 、 redis
mode: db
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true&serverTimezone=GMT
user: root
password: 123456
min-conn: 5
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
query-limit: 100
max-wait: 5000
# server:
# service-port: 8091 #If not configured, the default is '${server.port} + 1000'
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
metrics:
enabled: false
exporterList: prometheus
exporterPrometheusPort: 9898
registryType: compact
server:
maxCommitRetryTimeout: -1
maxRollbackRetryTimeout: -1
recovery:
asynCommittingRetryPeriod: 3000
committingRetryPeriod: 3000
rollbackingRetryPeriod: 3000
timeoutRetryPeriod: 3000
rollbackRetryTimeoutUnlockEnable: false
undo:
logDeletePeriod: 86400000
logSaveDays: 7
store:
db:
branchTable: branch_table
datasource: druid
dbType: mysql
driverClassName: com.mysql.cj.jdbc.Driver
globalTable: global_table
lockTable: lock_table
maxConn: 30
maxWait: 5000
minConn: 5
password: root
queryLimit: 100
url: jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true&serverTimezone=GMT
user: root
mode: db
transport:
compressor: none
serialization: seata
#For details about configuration items, see https://seata.io/zh-cn/docs/user/configurations.html
#Transport configuration, for client and server
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableTmClientBatchSendRequest=false
transport.enableRmClientBatchSendRequest=true
transport.enableTcServerBatchSendResponse=false
transport.rpcRmRequestTimeout=30000
transport.rpcTmRequestTimeout=30000
transport.rpcTcRequestTimeout=30000
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
transport.serialization=seata
transport.compressor=none
#Transaction routing rules configuration, only for the client
service.vgroupMapping.my_test_tx_group=default
#If you use a registry, you can ignore it
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
#Transaction rule configuration, only for the client
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=true
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.sagaJsonParser=fastjson
client.rm.tccActionInterceptorOrder=-2147482648
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.tm.interceptorOrder=-2147482648
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
#For TCC transaction mode
tcc.fence.logTableName=tcc_fence_log
tcc.fence.cleanPeriod=1h
#Log rule configuration, for client and server
log.exceptionRate=100
#Transaction storage configuration, only for the server. The file, db, and redis configuration values are optional.
store.mode=db
store.lock.mode=db
store.session.mode=db
#Used for password encryption
store.publicKey=
#If `store.mode,store.lock.mode,store.session.mode` are not equal to `file`, you can remove the configuration block.
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
#These configurations are required if the `store mode` is `db`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `db`, you can remove the configuration block.
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
#These configurations are required if the `store mode` is `redis`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `redis`, you can remove the configuration block.
store.redis.mode=single
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
store.redis.sentinel.masterName=
store.redis.sentinel.sentinelHosts=
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
store.redis.password=123456
store.redis.queryLimit=100
#Transaction rule configuration, only for the server
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.distributedLockExpireTime=10000
server.xaerNotaRetryTimeout=60000
server.session.branchAsyncQueueSize=5000
server.session.enableBranchAsyncRemove=false
server.enableParallelRequestHandle=false
#Metrics configuration, only for the server
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<artifactId>seata-allartifactId>
<groupId>io.seatagroupId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>1.6.0version>
dependency>
spring:
# 服务名称
application:
# 订单服务
name: seata-order-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 # Nacos 服务注册中心地址
namespace: c7e4e5e4-8693-40e1-b75e-1ce1b46bf976
config:
server-addr: localhost:8848 # Nacos 配置中心地址
file-extension: yaml # 指定yaml格式的配置
group: SEATA_GROUP
namespace: c7e4e5e4-8693-40e1-b75e-1ce1b46bf976
sentinel:
transport:
# 配置Sentinel dashboard地址
dashboard: localhost:8080
# 默认8719端口,加入被占用会自动从8719开始依次+1扫描,知道找到未被占用的端口
port: 8719
alibaba:
seata:
# 自定义事务组名称需要与seata-server中的对应
tx-service-group: default_tx_group
# seata 配置, 代替file.conf和registry.conf配置
sfs:
nacos:
server-addr: 127.0.0.1:8848
namespace: c7e4e5e4-8693-40e1-b75e-1ce1b46bf976
group: SEATA_GROUP
username: nacos
password: nacos
seata:
enabled: true
application-id : ${spring.application.name}
tx-service-group: default_tx_group
use-jdk-proxy: true
enable-auto-data-source-proxy: true
registry:
type: nacos
nacos:
application: seata-server
server-addr: ${sfs.nacos.server-addr}
namespace: ${sfs.nacos.namespace}
group: ${sfs.nacos.group}
username: ${sfs.nacos.username}
password: ${sfs.nacos.username}
config:
type: nacos
nacos:
server-addr: ${sfs.nacos.server-addr}
namespace: ${sfs.nacos.namespace}
group: ${sfs.nacos.group}
username: ${sfs.nacos.username}
password: ${sfs.nacos.username}
service:
vgroupMapping:
default_tx_group: default
@Service
@Slf4j
public class TOrderServiceImpl implements TOrderService {
@Resource
private TOrderDao tOrderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
/**
* 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
* @param tOrder
* @return
*/
@Override
public boolean create(TOrder tOrder) {
//1.创建订单
log.info("----->开始创建订单");
tOrder.setStatus(0);
tOrderDao.insert(tOrder);
//2.扣减库存
log.info("----->订单微服务开始调用库存,做扣减Count");
storageService.decrease(tOrder.getProductId(),tOrder.getCount());
log.info("----->订单微服务开始调用库存,做扣减end");
//3.扣减账户
log.info("----->订单微服务开始调用账户,做扣减Money");
accountService.decrease(tOrder.getUserId(),tOrder.getMoney());
log.info("----->订单微服务开始调用账户,做扣减end");
//4.修改订单状态,0=>1
log.info("----->修改订单状态开始");
boolean flag = this.updateStatus(tOrder.getUserId(), 1);
log.info("----->修改订单状态结束");
log.info("----->下订单,结束了,O(∩_∩)O哈哈~");
return flag;
}
@Override
public boolean updateStatus(Long userId,Integer status) {
QueryWrapper<TOrder> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId);
TOrder tOrder = new TOrder();
tOrder.setStatus(status);
return tOrderDao.update(tOrder, wrapper)>0;
}
}
@Service
@Slf4j
public class TStorageServiceImpl implements TStorageService {
@Resource
private TStorageDao dao;
/**
* 扣减库存
*
* @param productId
* @param count
* @return
*/
@Override
public boolean decrease(Long productId, Integer count) {
log.info("------>seata-storage-service中扣减库存开始");
QueryWrapper<TStorage> wrapper = new QueryWrapper<>();
wrapper.eq("product_id", productId);
TStorage tStorage = dao.selectOne(wrapper);
TStorage newTStorage = new TStorage();
newTStorage.setUsed(tStorage.getUsed() + count);
newTStorage.setResidue(tStorage.getResidue() - count);
return dao.update(newTStorage, wrapper) > 0;
}
}
12.4.5 账户服务 TStorageServiceImpl
@Service
@Slf4j
public class TAccountServiceImpl implements TAccountService {
@Resource
private TAccountDao dao;
@Override
public Boolean decrease(Long userId, BigDecimal money) {
log.info("------>开始扣减账户");
//模拟超时异常,全局事务回滚
try {
TimeUnit.SECONDS.sleep(20);
}catch (InterruptedException e){
e.printStackTrace();
}
QueryWrapper<TAccount> param = new QueryWrapper<>();
param.eq("user_id",userId);
TAccount tAccount = dao.selectOne(param);
QueryWrapper<TAccount> wapper = new QueryWrapper<>();
wapper.eq("user_id",userId);
TAccount newTAccount = new TAccount();
newTAccount.setUsed(tAccount.getUsed().add(money));
newTAccount.setResidue(new BigDecimal(Double.toString(tAccount.getResidue().subtract(money).doubleValue())));
log.info("------>结束扣减账户");
return dao.update(newTAccount,wapper)>0;
}
}
当库存和账户扣减后,订单状态并没有改变,二期由于Feign的重试机制,账户余额还有可能重复扣减
MyBatis版
@Configuration
public class DataSourceMyBatisConfig {
@Value("${mybatis-Plus.mapper-locations}")
private String mapperLocations;
@Bean
@Primary//让MyBatis-Plus优先使用我们配置的数据源
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDatasource() {
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
MyBatis-Plus版
/**
* DataSourceProxyConfig :
* 使用Seata对数据源进行代理
*
* @author zyw
* @create 2023/7/10
*/
@Configuration
@MapperScan("com.zyw.springcloud.dao")
public class DataSourceMyBatisPlusConfig {
@Value("${mybatis-Plus.mapper-locations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Primary
@Bean("dataSource")
public DataSourceProxy dataSource(DataSource druidDataSource){
return new DataSourceProxy(druidDataSource);
}
/**
* 配置mybatis-plus的分页
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));//指定数据库
return interceptor;
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy)throws Exception{
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources(mapperLocations));
// 配置spring的本地事务
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
// 配置mybatis-plus的log打印
MybatisConfiguration cfg = new MybatisConfiguration();
cfg.setJdbcTypeForNull(JdbcType.NULL);
cfg.setMapUnderscoreToCamelCase(true);
cfg.setCacheEnabled(false);
cfg.setLogImpl(StdOutImpl.class);
sqlSessionFactoryBean.setConfiguration(cfg);
return sqlSessionFactoryBean.getObject();
}
}
前提:
给予支持本地ACID事务的关系型数据库。
Java应用,通过JDBC访问数据库。
整体机制:
一阶段:业务数据和回归日志记录在同一个本地事务中提交,释放本地锁和连接资源
二阶段:提交异步化,非常快速的完成;回滚通过一阶段的回滚日志进行反向补偿
在一阶段,Seata会拦截“业务SQL” ,
1解析SQL语义,找到“业务SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,(前置镜像)
2执行“业务SQL”更新业务数据,在业务数据更新之后,
3其保存成“after image”,最后生成行锁。
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性
因为业务SQL在一阶段已经提交至数据库,所有Seata框架只需将一阶段保存的快照数据和行锁删除,完成数据清理即可。
二阶段如果是回滚的话,Seata就需要回滚一阶段已经执行的“业务SQL”,还原业务数据。
回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和“after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。