SpringCloud
五大组件: 注册中心, 负载均衡, 服务远程调用, 服务熔断与降级, 网关
微服务引入
restTemplate
RestTemplate提供了多种便捷访问远程Http服务的方法,
是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集
restTemplate常见方法: <SpringBoot的RestTemplate的各种请求(GET、POST、PUT、DELETE)用法示例_resttemplate的get请求一个参数-CSDN博客>
restTemplate配置
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
使用restTemlate例子(cloud2024文件)
从80消费者端通过restTemplate转发到8001支付端
public static final String PaymentSrv_URL = "http://localhost:8001";//先写死,硬编码
@Resource
private RestTemplate restTemplate;
@GetMapping("/consumer/pay/add")
public ResultData addOrder(@RequestBody PayDTO payDTO) {
return restTemplate.postForObject(PaymentSrv_URL + "/pay/add", payDTO, ResultData.class);
}
8001支付端add方法
@PostMapping(value = "/pay/add")
@Operation(summary = "新增",description = "新增支付流水方法,json串做参数")
public ResultData<String> addPay(@RequestBody Pay pay)
{
System.out.println(pay.toString());
int i = payService.add(pay);
return ResultData.success("成功插入记录,返回值:"+i);
}
Consul 服务的注册与发现
引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
配置文件
application.yml
server:
port: 8001
# ==========applicationName + druid-mysql8 driver===================
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/cloud?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: cqx
password: tinashe333
profiles:
active: dev # 多环境配置加载内容dev/prod,不写就是默认default配置 可用于consul中key/value读取
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.atguigu.cloud.entities
configuration:
map-underscore-to-camel-case: true
bootstrap.yml
spring:
application:
name: cloud-payment-service
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
config:
profile-separator: '-' # default value is ",",we update '-'
format: YAML
主启动类加入@EnableDiscoveryClient注解
Consul页面
使用页面地址: localhost:8500
服务注册到Consul的效果
Consul写入key/value效果
动态刷新
主启动类加@RefreshScope注解
bootstrap.yml配置文件配置刷新时间:
spring.cloud.consul.config.watch.wait-time: 刷新时间
用Consul注册service后,就不用写死端口号了,例:
public static final String PaymentSrv_URL = "http://cloud-payment-service";//先写死,硬编码
@Resource
private RestTemplate restTemplate;
@GetMapping("/consumer/pay/add")
public ResultData addOrder(@RequestBody PayDTO payDTO) {
return restTemplate.postForObject(PaymentSrv_URL + "/pay/add", payDTO, ResultData.class);
}
其中cloud-payment-service是注册到Consul服务的名称,在bootstrap中指定
LoadBanlance(客户端的负载均衡) 服务调用与负载均衡
区别 nginx(服务端的负载均衡)
引入依赖
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
演示负载均衡
将之前的8001支付端复制一份为8002,加入到8001的服务中,通过Consul页面,可以看到现在cloud-payment-service有两个实例,分别是8001,8002
后用 用户端调用支付端,会自动实现负载均衡。
负载均衡算法
种类:轮询,随机
算法切换: 在restTemplate中具体配置
原理:负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标 ,每次服务重启动后rest接口计数从1开始。
OpenFeign 服务调用与负载均衡
Feign接口公共方法方法参数一定要加@RequestParam/@RequestBody/@Pathvirable 啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊
主启动类加入@EnableFeignClients注解
架构说明图
原有消费端通过公共api调用服务端接口,服务端暴露出来的方法在公共api中,降低 代码冗余,解耦
主启动类加入@EnableFeignClients表示开启openfeign功能并激活(basepackage表示扫描feign接口所在的包,可选。 不加basepackage也会扫描该模块的所有包)
导入依赖
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
写入application.yml 注册到consul
server:
port: 80
spring:
application:
name: cloud-consumer-openfeign-order
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true #优先使用服务ip进行注册
service-name: ${spring.application.name}
feign接口的两种调用方式
- 所有请求过网关,网关配对应的映射路径,@FeignClient中 的value填网关在nacos中心注册的服务名
- 直接通过后台服务处理,@FeignClient中 的value填服务在nacos中心注册的服务名
配置服务接口
注解@FeignClient(value = “服务名称”)
在服务接口中暴露对应支付端方法,方法名要和对应服务controller中的方法名一致
@FeignClient(value = "cloud-payment-service")
public interface PayFeignApi {
/**
* 新增一条支付相关流水记录
* @param payDTO
* @return
*/
@PostMapping("/pay/add")
public ResultData addPay(@RequestBody PayDTO payDTO);
/**
* 按照主键记录查询支付流水信息
* @param id
* @return
*/
@GetMapping("/pay/get/{id}")
public ResultData getPayInfo(@PathVariable("id") Integer id);
/**
* openfeign天然支持负载均衡演示
* @return
*/
@GetMapping(value = "/pay/get/info")
public String mylb();
}
用户端用OpenFeign接口调用支付端方式(引入服务接口,通过服务接口方法调用支付端方法)
feign接口上的mapping路径和对应服务的完整路径必须一致
@RestController
public class OrderFeignController {
@Resource
private PayFeignApi payFeignApi;
@PostMapping("/feign/pay/add")
public ResultData addOrder(@RequestBody PayDTO payDTO) {
ResultData resultData = payFeignApi.addPay(payDTO);
return resultData;
}
@GetMapping("/feign/pay/get/{id}")
public ResultData getOrder(@PathVariable("id") Integer id) {
ResultData payInfo = payFeignApi.getPayInfo(id);
return payInfo;
}
@GetMapping("/feign/pay/get/info")
public String getInfo() {
String mylb = payFeignApi.mylb();
return mylb;
}
}
用户端feign80的controller类引入feignapi类,然后由80-》feignapi-》8001
高级特性
超时控制
默认超时时间是60s
在application.yml配置超时时间
全局配置
spring:
cloud:
openfeign:
client:
config:
default:
#连接超时时间
connectTimeout: 3000
#读取超时时间
readTimeout: 3000
指定配置
spring:
cloud:
openfeign:
client:
config:
cloud-payment-service:
#连接超时时间
connectTimeout: 5000
#读取超时时间
readTimeout: 5000
如果application.yml同时全局配置和指定配置,那么指定配置的服务端以指定配置为准。
重试机制
加入配置类
@Configuration
public class FeignConfig
{
@Bean
public Retryer myRetryer()
{
//return Retryer.NEVER_RETRY; //Feign默认配置是不走重试策略的
//最大请求次数为3(1+2),初始间隔时间为100ms,重试间最大间隔时间为1s
return new Retryer.Default(100,1,3);
}
}
HttpClient5 替换 HttpUrlConnection(提高性能)
引入依赖
<!-- httpclient5-->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3</version>
</dependency>
<!-- feign-hc5-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hc5</artifactId>
<version>13.1</version>
</dependency>
修改application.yml
# Apache HttpClient5 配置开启
spring:
cloud:
openfeign:
httpclient:
hc5:
enabled: true
请求回应压缩
修改application.yml
compression:
request:
enabled: true
min-request-size: 2048 #最小触发压缩的大小
mime-types: text/xml,application/xml,application/json #触发压缩数据类型
response:
enabled: true
日志打印
打印级别:NONE,BASIC,HEADERS,FULL.(逐级递增)
配置
openfeign配置类加入bean
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
修改application.yml
# feign日志以什么级别监控哪个接口
logging:
level:
com:
atguigu:
cloud:
apis:
PayFeignApi: debug
CircuitBreaker(熔断器) resilience4j 服务熔断和降级
状态:CLOSE, OPEN, HALF_OPEN, DISABLED, FORCE_OPEN
resilience4j为CircuitBreaker的具体实现
服务熔断配置参数
判断熔断器开关依据: 通过基于时间的滑动窗口, 通过基于访问次数的滑动窗口
引入依赖
<!--resilience4j-circuitbreaker-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<!-- 由于断路保护等需要AOP实现,所以必须导入AOP包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
疑问:怎么从half_open转换到close/open
消费者使用熔断器,需要加@CircuitBreaker注解到对应的方法上
fallbackMethod代表服务降级后的兜底处理方法
@GetMapping(value = "/feign/pay/circuit/{id}")
@CircuitBreaker(name = "cloud-payment-service", fallbackMethod = "myCircuitFallback")
public String myCircuitBreaker(@PathVariable("id") Integer id) {
return payFeignApi.myCircuit(id);
}
//myCircuitFallback就是服务降级后的兜底处理方法
public String myCircuitFallback(Integer id, Throwable t) {
// 这里是容错处理逻辑,返回备用结果
return "myCircuitFallback,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
}
配置文件
熔断器开启
circuitbreaker:
enabled: true
group:
基于次数的熔断器(COUNT_BASE),参数根据情况设定
# Resilience4j CircuitBreaker 按照次数:COUNT_BASED 的例子
# 6次访问中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。
# 等待5秒后,CircuitBreaker 将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。
# 如还是异常CircuitBreaker 将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。
resilience4j:
circuitbreaker:
configs:
default:
failureRateThreshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。
slidingWindowType: COUNT_BASED # 滑动窗口的类型
slidingWindowSize: 6 #滑动窗⼝的⼤⼩配置COUNT_BASED表示6个请求,配置TIME_BASED表示6秒
minimumNumberOfCalls: 6 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。如果minimumNumberOfCalls为10,则必须最少记录10个样本,然后才能计算失败率。如果只记录了9次调用,即使所有9次调用都失败,断路器也不会开启。
automaticTransitionFromOpenToHalfOpenEnabled: true # 是否启用自动从开启状态过渡到半开状态,默认值为true。如果启用,CircuitBreaker将自动从开启状态过渡到半开状态,并允许一些请求通过以测试服务是否恢复正常
waitDurationInOpenState: 5s #从OPEN到HALF_OPEN状态需要等待的时间
permittedNumberOfCallsInHalfOpenState: 2 #半开状态允许的最大请求数,默认值为10。在半开状态下,CircuitBreaker将允许最多permittedNumberOfCallsInHalfOpenState个请求通过,如果其中有任何一个请求失败,CircuitBreaker将重新进入开启状态。
recordExceptions:
- java.lang.Exception # 异常出现也计入服务熔断限定次数
instances:
cloud-payment-service:
baseConfig: default
基于时间的熔断器(TIME_BASE),参数根据情况设定。
例:超过timeout-duration则直接熔断,超过slowCallDurationThreshold不超过timeout-duration则视为慢调用,如果慢调用占总调用率超过slowCallRateThreshold也会进行熔断操作。
# Resilience4j CircuitBreaker 按照时间:TIME_BASED 的例子
resilience4j:
timelimiter:
configs:
default:
timeout-duration: 10s #神坑的位置,timelimiter 默认限制远程1s,超于1s就超时异常,配置了降级,就走降级逻辑
circuitbreaker:
configs:
default:
failureRateThreshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。
slowCallDurationThreshold: 2s #慢调用时间阈值,高于这个阈值的视为慢调用并增加慢调用比例。
slowCallRateThreshold: 30 #慢调用百分比峰值,断路器把调用时间⼤于slowCallDurationThreshold,视为慢调用,当慢调用比例高于阈值,断路器打开,并开启服务降级
slidingWindowType: TIME_BASED # 滑动窗口的类型
slidingWindowSize: 2 #滑动窗口的大小配置,配置TIME_BASED表示2秒
minimumNumberOfCalls: 2 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。
permittedNumberOfCallsInHalfOpenState: 2 #半开状态允许的最大请求数,默认值为10。
waitDurationInOpenState: 5s #从OPEN到HALF_OPEN状态需要等待的时间
recordExceptions:
- java.lang.Exception
instances:
cloud-payment-service:
baseConfig: default
SemaphoreBulkHead(信号量舱壁)
底层用信号量semaphore实现:
信号量舱壁(SemaphoreBulkhead)原理
当信号量有空闲时,进入系统的请求会直接获取信号量并开始业务处理。
当信号量全被占用时,接下来的请求将会进入阻塞状态,SemaphoreBulkhead提供了一个阻塞计时器,
如果阻塞状态的请求在阻塞计时内无法获取到信号量则系统会拒绝这些请求。
若请求在阻塞计时内获取到了信号量,那将直接获取信号量并执行相应的业务处理。
引入依赖
<!--resilience4j-bulkhead-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>
修改application.yml文件
####resilience4j bulkhead 的例子
resilience4j:
bulkhead:
configs:
default:
maxConcurrentCalls: 2 # 隔离允许并发线程执行的最大数量
maxWaitDuration: 1s # 当达到并发调用数量时,新的线程的阻塞时间,我只愿意等待1秒,过时不候进舱壁兜底fallback
instances:
cloud-payment-service:
baseConfig: default
timelimiter:
configs:
default:
timeout-duration: 20s
加入@Bulkhead注解,指定使用bulkhead服务名称,降级方法,底层实现种类
BulkHead业务实例
/**
*(船的)舱壁,隔离
* @param id
* @return
*/
@GetMapping(value = "/feign/pay/bulkhead/{id}")
@Bulkhead(name = "cloud-payment-service",fallbackMethod = "myBulkheadFallback",type = Bulkhead.Type.SEMAPHORE)
public String myBulkhead(@PathVariable("id") Integer id)
{
return payFeignApi.myBulkhead(id);
}
public String myBulkheadFallback(Throwable t)
{
return "myBulkheadFallback,隔板超出最大数量限制,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
}
FixedThreadPoolBulkHead(固定线程池舱壁)
固定线程池舱壁(FixedThreadPoolBulkhead)
FixedThreadPoolBulkhead的功能与SemaphoreBulkhead一样也是用于限制并发执行的次数的,但是二者的实现原理存在差别而且表现效果也存在细微的差别。FixedThreadPoolBulkhead使用一个固定线程池和一个等待队列来实现舱壁。
当线程池中存在空闲时,则此时进入系统的请求将直接进入线程池开启新线程或使用空闲线程来处理请求。
当线程池中无空闲时时,接下来的请求将进入等待队列,
若等待队列仍然无剩余空间时接下来的请求将直接被拒绝,
在队列中的请求等待线程池出现空闲时,将进入线程池进行业务处理。
另外:ThreadPoolBulkhead只对CompletableFuture方法有效,所以我们必创建返回CompletableFuture类型的方法
引入依赖
<!--resilience4j-bulkhead-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>
修改application.yml文件
thread-pool-bulkhead:
configs:
default:
core-thread-pool-size: 1
max-thread-pool-size: 1
queue-capacity: 1
instances:
cloud-payment-service:
baseConfig: default
BulkHead业务实例
和之前SemaphoreBulkHead类似,但是要把返回值改为CompletableFuture,type = Bulkhead.Type.THREADPOOL
限流
常见限流算法:漏斗算法(Leaky Bucket), 令牌算法(SoringCloud默认使用该算法), 滚动时间窗(tumbling time window), 滑动时间窗(sliding time window)
引入依赖
<!--resilience4j-ratelimiter-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
</dependency>
修改application.yml,配置限流器
####resilience4j ratelimiter 限流的例子
resilience4j:
ratelimiter:
configs:
default:
limitForPeriod: 2 #在一次刷新周期内,允许执行的最大请求数
limitRefreshPeriod: 1s # 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod
timeout-duration: 1 # 线程等待权限的默认等待时间
instances:
cloud-payment-service:
baseConfig: default
加入@RateLimiter注解(name服务名称,fallbackmethod降级方法)
业务实例
@GetMapping(value = "/feign/pay/ratelimit/{id}")
@RateLimiter(name = "cloud-payment-service", fallbackMethod = "myRatelimitFallback")
public String myBulkhead(@PathVariable("id") Integer id) {
return payFeignApi.myRatelimit(id);
}
public String myRatelimitFallback(Integer id, Throwable t) {
return "你被限流了,禁止访问/(ㄒoㄒ)/~~";
}
MicroMeter 分布式链路追踪
在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个前段请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。
分布式链路追踪原理
引入父工程依赖
properties
<micrometer-tracing.version>1.2.0</micrometer-tracing.version>
<micrometer-observation.version>1.12.0</micrometer-observation.version>
<feign-micrometer.version>12.5</feign-micrometer.version>
<zipkin-reporter-brave.version>2.17.0</zipkin-reporter-brave.version>
dependencies
<!--micrometer-tracing-bom导入链路追踪版本中心 1-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bom</artifactId>
<version>${micrometer-tracing.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--micrometer-tracing指标追踪 2-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
<version>${micrometer-tracing.version}</version>
</dependency>
<!--micrometer-tracing-bridge-brave适配zipkin的桥接包 3-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
<version>${micrometer-tracing.version}</version>
</dependency>
<!--micrometer-observation 4-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
<version>${micrometer-observation.version}</version>
</dependency>
<!--feign-micrometer 5-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId>
<version>${feign-micrometer.version}</version>
</dependency>
<!--zipkin-reporter-brave 6-->
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
<version>${zipkin-reporter-brave.version}</version>
</dependency>
服务提供者8001引入依赖
<!--micrometer-tracing指标追踪 1-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
</dependency>
<!--micrometer-tracing-bridge-brave适配zipkin的桥接包 2-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<!--micrometer-observation 3-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
</dependency>
<!--feign-micrometer 4-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId>
</dependency>
<!--zipkin-reporter-brave 5-->
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
修改application.yml
# ========================zipkin===================
management:
zipkin:
tracing:
endpoint: http://localhost:9411/api/v2/spans
tracing:
sampling:
probability: 1.0 #采样率默认为0.1(0.1就是10次只能有一次被记录下来),值越大收集越及时。
消费者(服务调用者80)引入依赖
<!--micrometer-tracing指标追踪 1-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
</dependency>
<!--micrometer-tracing-bridge-brave适配zipkin的桥接包 2-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<!--micrometer-observation 3-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
</dependency>
<!--feign-micrometer 4-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId>
</dependency>
<!--zipkin-reporter-brave 5-->
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
修改application.yml文件
# zipkin图形展现地址和采样率设置
management:
zipkin:
tracing:
endpoint: http://localhost:9411/api/v2/spans
tracing:
sampling:
probability: 1.0 #采样率默认为0.1(0.1就是10次只能有一次被记录下来),值越大收集越及时。
Gateway 服务网关
Gateway作用:反向代理, 鉴权,流量控制, 熔断, 日志监控
总结:Spring Cloud Gateway组件的核心是一系列的过滤器,通过这些过滤器可以将客户端发送的请求转发(路由)到对应的微服务。 Spring Cloud Gateway是加在整个微服务最前沿的防火墙和代理器,隐藏微服务结点IP端口信息,从而加强安全保护。Spring Cloud Gateway本身也是一个微服务,需要注册进服务注册中心。
Gateway三大核心
Route:它是构建网关的基本模块,包含ID、目标URI、一系列的断言和过滤器,当断言为真时,请求就会被匹配到相应的路由。
Predicate:它允许开发者根据HTTP请求的各种内容(如请求头或请求参数)进行匹配,只有当请求与断言相匹配时,才会进行路由。
Filter:它是Spring框架中GatewayFilter的实例,用于在请求被路由前或后对请求进行修改。过滤器可以用来执行各种操作,如参数校验、权限校验、流量监控、日志输出、协议转换等。
web前端请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。
predicate就是我们的匹配条件;
filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了
工作流程(路由转发+ 断言判断+ 执行过滤器链)
客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(Pre)或之后(Post)执行业务逻辑。
在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等;
在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。
流程: feign80 -》feignapi -》 gateway(动态获取端口) -》 8001
feignapi到gateway这一步,因为feignapi的feignclient注解改为了cloud-gateway,所以需要先在gateway的yml文件中配置对应的路由,如:
spring:
cloud:
routes:
- id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由,
入门配置
导入依赖
<dependencies>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--服务注册发现consul discovery,网关也要注册进服务注册中心统一管控-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- 指标监控健康检查的actuator,网关是响应式编程删除掉spring-boot-starter-web dependency-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
写入application.yml, 将网关注册到consul
server:
port: 9527
spring:
application:
name: cloud-gateway #以微服务注册进consul或nacos服务列表内
cloud:
consul: #配置consul地址
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
主启动类
@SpringBootApplication
@EnableDiscoveryClient //服务注册和发现
public class Main9527
{
public static void main(String[] args)
{
SpringApplication.run(Main9527.class,args);
}
}
基础映射
使用网关,使用网关端口也能调用服务提供者8001
服务提供者新建测试gateway controller
@RestController
public class PayGateWayController
{
@Resource
PayService payService;
@GetMapping(value = "/pay/gateway/get/{id}")
public ResultData<Pay> getById(@PathVariable("id") Integer id)
{
Pay pay = payService.getById(id);
return ResultData.success(pay);
}
@GetMapping(value = "/pay/gateway/info")
public ResultData<String> getGatewayInfo()
{
return ResultData.success("gateway info test:"+ IdUtil.simpleUUID());
}
}
修改application.yml
spring:
cloud:
gateway:
routes:
- id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
- id: pay_routh2 #pay_routh2 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/info/** # 断言,路径相匹配的进行路由
测试:
使用源地址 http://localhost:9527/pay/gateway/get/6 和网关地址 http://localhost:9527/pay/gateway/get/6 都能正确调用服务提供者8001
在payfeignapi中新增网关调用相关接口
/**
* GateWay进行网关测试案例01
* @param id
* @return
*/
@GetMapping(value = "/pay/gateway/get/{id}")
public ResultData getById(@PathVariable("id") Integer id);
/**
* GateWay进行网关测试案例02
* @return
*/
@GetMapping(value = "/pay/gateway/info")
public ResultData<String> getGatewayInfo();
payfeignapi中将@FeignClient的value改为cloud-gateway
ps: 如果同一家公司内部调用,feignclient可以使用微服务,例如cloud-payment-service
如涉及到外部调用,则应改为网关,例如cloud-gateway
在服务调用者80新增测试网关的controller
@RestController
public class OrderGateWayController
{
@Resource
private PayFeignApi payFeignApi;
@GetMapping(value = "/feign/pay/gateway/get/{id}")
public ResultData getById(@PathVariable("id") Integer id)
{
return payFeignApi.getById(id);
}
@GetMapping(value = "/feign/pay/gateway/info")
public ResultData<String> getGatewayInfo()
{
return payFeignApi.getGatewayInfo();
}
}
测试: 用服务调用者80调用localhost/feign/pay/gateway/info, 80调用feign调用网关调用8001,正常调用,一旦关闭网关则调用失败。
高级特性–Predicate
动态获取服务uri
配置地址: lb://servicename ,代表微服务且支持负载均衡
修改gateway的application.yml
spring:
cloud:
gateway:
routes:
- id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
- id: pay_routh2 #pay_routh2 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/info/** # 断言,路径相匹配的进行路由
配置gateway网关路由规则(根据host头部路由)
- id: 路由名
uri: lb://服务名
predicates:
- Host=对应的host信息
ps: nginx代理给网关的时候,会丢失请求的host信息,所以要在location信息中加入下列信息防止请求头丢失。
proxy_set_header Host $host;
Predicate断言
配置方法1:shortcutconfiguration
配置方法2:fullyexpandedarguments
After Route Predicate 在ZoneDateTime后允许访问
获取ZoneDateTime
ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
System.out.println(zbj);
修改网关的application.yml文件(-After)
spring:
cloud:
gateway:
routes:
- id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
*** - After=2024-03-28T19:47:56.830546100+08:00[Asia/Shanghai] ***
Before Route Predicate 在ZoneDateTime前允许访问
修改网关的application.yml文件(-Before)
- Before=2025-03-28T19:47:56.830546100+08:00[Asia/Shanghai]
Between Route Predicate 在指定ZoneDateTime范围内允许访问
修改网关的application.yml文件(-Between)
- Between=ZoneDateTime1,ZoneDateTime2
example:
- Between=2021-03-28T19:47:56.830546100+08:00[Asia/Shanghai],2022-03-28T19:47:56.830546100+08:00[Asia/Shanghai]
Cookie Route Predicate Cookies中需要携带固定参数才能访问
修改网关的application.yml文件(-Cookies)
- Cookie=paraName,paraValue
example
- Cookie=name,cqx
测试方法1:cmd窗口通过curl测试
curl
: 是一个命令行工具,用于传输数据,支持多种协议,包括 HTTP、HTTPS、FTP 等。
curl http://localhost:9527/pay/gateway/info --cookie "name=cqxx"
测试方法2:Postman
Cookie Route Predicate 请求头中需要携带固定参数才能访问
修改网关的application.yml文件(-Header)
- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
curl测试
curl http://localhost:9527/pay/gateway/get/1 -H "X-Request-Id:123456"
curl http://localhost:9527/pay/gateway/get/1 -H "X-Request-Id:123456c"
Host Route Predicate 请求头中需要携带固定域名才能访问
修改网关的application.yml文件(-Host)
- Host=**.cqx.com
curl测试
curl http://localhost:9527/pay/gateway/get/3 -H "Host:www.cqx.com"
curl http://localhost:9527/pay/gateway/get/3 -H "Host:www.cqx.ccom"
Path Route Predicate 路径相匹配的进行路由
修改网关的application.yml文件(-Path)
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
Query Route Predicate 路径相匹配的进行路由,即加上路径参数
修改网关的application.yml文件(-Query)
- Query=paraName, para条件 ,条件可以是正则表达式,也可以是某个值
- Query=username, \d+ # 要有参数名username并且值还要是整数才能路由
curl测试
http://localhost:9527/pay/gateway/get/3?username=123
http://localhost:9527/pay/gateway/get/3?username=abc 要有参数名username并且值还要是整数才能路由
RemoteAddr Route Predicate ip前x位必须固定
修改网关的application.yml文件(-RemoteAddr)
- RemoteAddr=192.168.124.1/24 # 外部访问我的IP限制,最大跨度不超过32,目前是1~24它们是 CIDR 表示法。
Method Route Predicate 访问方法限制
修改网关的application.yml文件(-Method)
- Method=方法1,方法2...
自定义断言
仿照官网断言写法
@Component
public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<MyRoutePredicateFactory.Config>
{
public MyRoutePredicateFactory()
{
super(MyRoutePredicateFactory.Config.class);
}
@Validated
public static class Config{
@Setter
@Getter
@NotEmpty
private String userType; //钻、金、银等用户等级
}
@Override
public Predicate<ServerWebExchange> apply(MyRoutePredicateFactory.Config config)
{
return new Predicate<ServerWebExchange>()
{
@Override
public boolean test(ServerWebExchange serverWebExchange)
{
//检查request的参数里面,userType是否为指定的值,符合配置就通过
String userType = serverWebExchange.getRequest().getQueryParams().getFirst("userType");
if (userType == null) return false;
//如果说参数存在,就和config的数据进行比较
if(userType.equals(config.getUserType())) {
return true;
}
return false;
}
};
}
/**
* 缺少该方法则不能使用shortcut的配置方式,只能用fully方式.
* @return
*/
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("userType");
}
}
修改application.yml文件
- My=cqx
测试
http://localhost:9527/pay/gateway/get/1?userType=cqx
高级特性 – Filter
流程 gateway-》8001
此次测试都是用http://localhost:9527/pay/filter进行,所以需要先在gateway的yml文件配置8001filter的对应路由
id: pay_routh3 #pay_routh3
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/filter/** # 断言,路径相匹配的进行路由
请求头相关组
服务提供者8001新增测试方法
@GetMapping(value = "/pay/gateway/filter")
public ResultData<String> getGatewayFilter(HttpServletRequest request)
{
String result = "";
Enumeration<String> headers = request.getHeaderNames();
while(headers.hasMoreElements())
{
String headName = headers.nextElement();
String headValue = request.getHeader(headName);
System.out.println("请求头名: " + headName +"\t\t\t"+"请求头值: " + headValue);
if(headName.equalsIgnoreCase("X-Request-atguigu1")
|| headName.equalsIgnoreCase("X-Request-atguigu2")) {
result = result+headName + "\t " + headValue +" ";
}
}
return ResultData.success("getGatewayFilter 过滤器 test: "+result+" \t "+ DateUtil.now());
}
加入请求头
修改网关application.yml
filters:
- AddRequestHeader=X-Request-atguigu1,atguiguValue1 # 请求头kv,若一头含有多参则重写一行设置
- AddRequestHeader=X-Request-atguigu2,atguiguValue2
删除请求头
修改网关application.yml
- RemoveRequestHeader=sec-fetch-site # 删除请求头sec-fetch-site
修改请求头
修改网关application.yml
- SetRequestHeader=sec-fetch-mode, Blue-updatebyzzyy # 将请求头sec-fetch-mode对应的值修改为Blue-updatebyzzyy
请求参数相关组
修改网关application.yml
新增请求参数默认值, 如果传递customerId,则以这个传递的值为主
- AddRequestParameter=customerId,9527001 # 新增请求参数Parameter:k ,v
删除请求参数默认值, customerName,则把这个传递的值设为null
- RemoveRequestParameter=customerName # 删除url请求参数customerName,你传递过来也是null
回应头相关组
修改网关application.yml
新增回应头请求参数
- AddResponseHeader=X-Response-atguigu, BlueResponse # 新增回应头请求参数X-Response-atguigu并设值为BlueResponse
设置回应头请求参数
- SetResponseHeader=Date,2099-11-11 # 设置回应头Date值为2099-11-11
删除回应头请求参数
- RemoveResponseHeader=Content-Type # 将默认自带Content-Type回应属性删除
前缀和路径相关组
prefixpath 前缀
- id: pay_routh3 #pay_routh3
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
#- Path=/pay/gateway/filter/** # 被分拆为: PrefixPath + Path
- Path=/gateway/filter/** # 断言,为配合PrefixPath测试过滤,暂时注释掉/pay
filters:
- AddRequestHeader=X-Request-atguigu1,atguiguValue1 #请求头kv,若一头含有多参则重写一行设置
- PrefixPath=/pay # http://localhost:9527/pay/gateway/filter
- Path=/gateway/filter/** # 断言,为配合PrefixPath测试过滤,暂时注释掉/pay
- PrefixPath=/pay # http://localhost:9527/pay/gateway/filter
也就是在地址栏只需输入localhost:9527/gateway/filter/*的就会自动添加前缀pay
所以地址为http://localhost:9527/pay/gateway/filter/*
SetPath 设置路径 可用于隐藏路径
- id: pay_routh3 #pay_routh3
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
#- Path=/pay/gateway/filter/** # 真实地址
#- Path=/gateway/filter/** # 断言,为配合PrefixPath测试过滤,暂时注释掉/pay
- Path=/XYZ/abc/{segment} # 断言,为配合SetPath测试,{segment}的内容最后被SetPath取代
filters:
- AddRequestHeader=X-Request-atguigu1,atguiguValue1 #请求头kv,若一头含有多参则重写一行设置
#- AddRequestHeader=X-Request-atguigu2,atguiguValue2
#- RemoveRequestHeader=sec-fetch-site # 删除请求头sec-fetch-site
#- SetRequestHeader=sec-fetch-mode, Blue-updatebyzzyy # 将请求头sec-fetch-mode对应的值修改为Blue-updatebyzzyy
#- AddRequestParameter=customerId,9527001 # 新增请求参数Parameter:k ,v
#- RemoveRequestParameter=customerName # 删除url请求参数customerName,你传递过来也是null
#- AddResponseHeader=X-Response-atguigu, BlueResponse # 新增请求参数X-Response-atguigu并设值为BlueResponse
#- SetResponseHeader=Date,2099-11-11 # 设置回应头Date值为2099-11-11
#- RemoveResponseHeader=Content-Type # 将默认自带Content-Type回应属性删除
#- PrefixPath=/pay # http://localhost:9527/pay/gateway/filter
- SetPath=/pay/gateway/{segment} # {segment}表示占位符,你写abc也行但要上下一致
- Path=/XYZ/abc/{segment} # 断言,为配合SetPath测试,{segment}的内容最后被SetPath取代
- SetPath=/pay/gateway/{segment} # {segment}表示占位符,你写abc也行但要上下一致
也就是地址栏输入localhost:9527/XYZ/abc/{seg},就会自动转换为localhost:9527/pay/gateway/{seg}
redirect重定向
- id: pay_routh3 #pay_routh3
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/filter/** # 真实地址
#- Path=/gateway/filter/** # 断言,为配合PrefixPath测试过滤,暂时注释掉/pay
#- Path=/XYZ/abc/{segment} # 断言,为配合SetPath测试,{segment}的内容最后被SetPath取代
filters:
- AddRequestHeader=X-Request-atguigu1,atguiguValue1 #请求头kv,若一头含有多参则重写一行设置
- RedirectTo=302, http://www.atguigu.com/ # 访问http://localhost:9527/pay/gateway/filter跳转到http://www.atguigu.com/
- Path=/pay/gateway/filter/** # 真实地址
- RedirectTo=302, http://www.atguigu.com/ # 访问http://localhost:9527/pay/gateway/filter跳转到http://www.atguigu.com/
访问http://localhost:9527/pay/gateway/filter跳转到http://www.atguigu.com/, 其中302是重定向响应状态码
全局default配置
application.yml
spring:
cloud:
gateway:
default-fliters:
加入任何想配置的过滤条件
自定义过滤器Filter
仿照官网过滤器写法
模板
@Component //不要忘记
@Slf4j
public class MyGlobalFilter implements GlobalFilter, Ordered
{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
return null;
}
@Override
public int getOrder()
{
return 0;
}
}
获取信息的自定义全局过滤器
@Component
@Slf4j
public class MyGlobalFilter implements GlobalFilter, Ordered
{
/**
* 数字越小优先级越高
* @return
*/
@Override
public int getOrder()
{
return 0;
}
private static final String BEGIN_VISIT_TIME = "begin_visit_time";//开始访问时间
/**
*第2版,各种统计
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//先记录下访问接口的开始时间
exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());
return chain.filter(exchange).then(Mono.fromRunnable(()->{
Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME);
if (beginVisitTime != null){
log.info("访问接口主机: " + exchange.getRequest().getURI().getHost());
log.info("访问接口端口: " + exchange.getRequest().getURI().getPort());
log.info("访问接口URL: " + exchange.getRequest().getURI().getPath());
log.info("访问接口URL参数: " + exchange.getRequest().getURI().getRawQuery());
log.info("访问接口时长: " + (System.currentTimeMillis() - beginVisitTime) + "ms");
log.info("我是美丽分割线: ###################################################");
System.out.println();
}
}));
}
}
配置到gateway的yml文件中的路径过滤器会自动生效。
自定义条件过滤器
仿照官网过滤器写法
例
@Component
public class MyGatewayFilterFactory extends AbstractGatewayFilterFactory<MyGatewayFilterFactory.Config>
{
public MyGatewayFilterFactory()
{
super(MyGatewayFilterFactory.Config.class);
}
@Override
public GatewayFilter apply(MyGatewayFilterFactory.Config config)
{
return new GatewayFilter()
{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
ServerHttpRequest request = exchange.getRequest();
System.out.println("进入了自定义网关过滤器MyGatewayFilterFactory,status:"+config.getStatus());
if(request.getQueryParams().containsKey("atguigu")){
return chain.filter(exchange);
}else{
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
return exchange.getResponse().setComplete();
}
}
};
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("status");
}
public static class Config
{
@Getter@Setter
private String status;//设定一个状态值/标志位,它等于多少,匹配和才可以访问
}
}
//单一内置过滤器GatewayFilter
application.yml
- My=xxx
SpringCloudAlibaba Nacos 服务的注册与发现
启动
startup.cmd -m standalone
新建服务提供者cloudalibaba-provider-payment9001
引入依赖
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
写入application.yml
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class Main9001
{
public static void main(String[] args)
{
SpringApplication.run(Main9001.class,args);
}
}
业务类
@RestController
public class PayAlibabaController
{
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/pay/nacos/{id}")
public String getPayInfo(@PathVariable("id") Integer id)
{
return "nacos registry, serverPort: "+ serverPort+"\t id"+id;
}
}
新建服务调用者83 cloudalibaba-consumer-nacos-order83
引入依赖
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
写入application.yml
server:
port: 83
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
#消费者将要去访问的微服务名称(nacos微服务提供者叫什么你写什么)
service-url:
nacos-user-service: http://nacos-payment-provider
主启动类
@EnableDiscoveryClient
@SpringBootApplication
public class Main83
{
public static void main(String[] args)
{
SpringApplication.run(Main83.class,args);
}
}
配置resttemplate, 暂时不使用openfeign
@Configuration
public class RestTemplateConfig
{
@Bean
@LoadBalanced //赋予RestTemplate负载均衡的能力
public RestTemplate restTemplate()
{
return new RestTemplate();
}
}
业务
@RestController
public class OrderNacosController
{
@Resource
private RestTemplate restTemplate;
@Value("${service-url.nacos-user-service}")
private String serverURL;
@GetMapping("/consumer/pay/nacos/{id}")
public String paymentInfo(@PathVariable("id") Integer id)
{
String result = restTemplate.getForObject(serverURL + "/pay/nacos/" + id, String.class);
return result+"\t"+" 我是OrderNacosController83调用者。。。。。。";
}
}
端口映射复制9001
建立服务配置中心cloudalibaba-config-nacos-client3377
引入配置中心依赖
<!--bootstrap-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
写入bootstrap.yml
# nacos配置
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置
# nacos端配置文件DataId的命名规则是:
# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# 本案例的DataID是:nacos-config-client-dev.yaml
写入application.yml
server:
port: 3377
spring:
profiles:
active: dev # 表示项目开发环境
#active: prod # 表示项目生产环境
#active: test # 表示项目测试环境
主启动类
@EnableDiscoveryClient
@SpringBootApplication
public class NacosConfigClient3377
{
public static void main(String[] args)
{
SpringApplication.run(NacosConfigClient3377.class,args);
}
}
业务类 @RefreshScope //在控制器类加入@RefreshScope注解使当前类下的配置支持Nacos**的动态刷新功能。
@value获取配置文件的值
@RestController
@RefreshScope //在控制器类加入@RefreshScope注解使当前类下的配置支持Nacos的动态刷新功能。
public class NacosConfigClientController
{
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}
配置到nacos的文件名(dataid): spring.application.name - spring.profiles.active.spring.cloud.nacos.config.file-extension
如果nacos配置中心和项目application.properties都配置了值,优先选用配置中心的值
Nacos的数据模型 NameSpace-Group-DataId
三种加载方案配置(spring.cloud.nacos.config.group/namespace)
dataid
根据$ { spring.application.name}- s p r i n g . p r o f i l e . a c t i v e . {spring.profile.active}. spring.profile.active.{spring.cloud.nacos.config.file-extension} 修改application.yml和bootstrap.yml的内容
Group
修改bootstrap.yml
group: Group名称
NameSpace
修改bootstrap.yml
namespace: 命名空间id
如何读取多个配置文件?
SpringCloudAlibaba Sentinel 流量控制,熔断降级
可直接通过localhosot:8080访问sentinel控制台
新建模块cloudalibaba-sentinel-service8401
引入依赖
<!--SpringCloud alibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
写入application.yml
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard控制台服务地址
port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
业务
@RestController
public class FlowLimitController
{
@GetMapping("/testA")
public String testA()
{
return "------testA";
}
@GetMapping("/testB")
public String testB()
{
return "------testB";
}
}
流控
流控模式
直接
限制每秒的流量/线程数
结果:Blocked by Sentinel (flow limiting)
关联
配置B访问阙值超过1次则A快速失败,通过jemeter模拟B快速访问
结果:Blocked by Sentinel (flow limiting)
链路
来自不同链路的请求针对同一个目标访问,实施针对性的放行策略
例: /testC和/testD链路都访问service层方法,只放行testD
修改application.yml
web-context-unify: false # controller层的方法对service层调用不认为是同一个根链路
增加服务层业务
@Service
public class FlowLimitService
{
@SentinelResource(value = "common")
public void common()
{
System.out.println("------FlowLimitService come in");
}
}
增加controller方法
/**流控-链路演示demo
* C和D两个请求都访问flowLimitService.common()方法,阈值到达后对C限流,对D不管
*/
@Resource private FlowLimitService flowLimitService;
@GetMapping("/testC")
public String testC()
{
flowLimitService.common();
return "------testC";
}
@GetMapping("/testD")
public String testD()
{
flowLimitService.common();
return "------testD";
}
设置sentienl
多次访问testC结果
{
"code": "500",
"message": null,
"data": null,
"timestamp": 1711715700907
}
流控效果
WarmUp预热
阈值除于冷却银子coldFactor(默认为3),0-预热时长阈值是阈值除于冷却银子,预热时长后阈值才达到设定的阈值
sentinel配置
排队等待
新增业务
@GetMapping("/testE")
public String testE()
{
System.out.println(System.currentTimeMillis()+" testE,排队等待");
return "------testE";
}
jemeter一秒钟发送20个请求
配置sentinel
sentinel在1s接收到20个请求,但是每秒阙值为1,所以0-10秒共接收11个请求,超过超时时间10s后的未处理的请求自动废弃
阙值类型
选择并发线程数则没有流控效果,默认是直接
熔断
慢调用比例
新增业务类
/**
* 新增熔断规则-慢调用比例
* @return
*/
@GetMapping("/testF")
public String testF()
{
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("----测试:新增熔断规则-慢调用比例 ");
return "------testF 新增熔断规则-慢调用比例";
}
配置sentinel
名词介绍
最大RT: 超过这个时长则被视为一次慢调用
比例阙值: 慢调用次数/ 总调用次数,
最小请求数:设置的调用最小请求数,上图比如1秒钟打进来10个线程(大于我们配置的5个了)调用被触发
统计时长: 在这个指定统计时长内确定慢调用
结论:多次循环,一秒钟打进来10个线程(大于5个了)调用/testF,我们希望200毫秒处理完一次调用,和谐系统;_
**假如在统计时长内,实际请求数目>最小请求数且慢调用比例>比例阈值 ,断路器打开(保险丝跳闸)微服务不可用(Blocked by Sentinel (flow limiting)),进入熔断状态5秒;**后续我停止jmeter,没有这么大的访问量了,单独用浏览器访问rest地址,断路器关闭(保险丝恢复,合上闸口),
微服务恢复OK
异常比例
sentinel配置
在1s中至少发了5个请求,其中异常请求/总请求大于比例阙值,则会进行熔断
异常数
sentinel配置
在1s中至少发了5个请求,其中异常数大于设定异常数,则会进行熔断
@SentinelResource
@SentinelResource
是 Sentinel 提供的注解,用于标识资源受 Sentinel 保护。在使用 Sentinel 进行限流、熔断、降级等功能时,我们可以通过 @SentinelResource
注解来定义资源,并对资源的访问进行监控和控制。
不加@SentinelResource
程序会自动走默认的限流机制,会返回Sentinel自带的限流处理结果(block by sentinel flow limiting)
Value
也就是配置熔断,流控等等配置页面的“资源名”, 一般设为: 业务方法名 + SentinelResource
blockHandler
主要针对sentinel配置后违规的情况处理, 一般设为: 处理的方法名
fallback
程序异常后JVM抛出异常的服务降级, 一般设为: 处理的方法名
热点
热点即经常访问的数据,很多时候我们希望统计或者限制某个热点数据中访问频次最高的TopN数据,并对其访问进行限流或者其它操作
前提
热点参数类型必须是String或者基本数据类型
限流模式只支持QPS模式,固定写死了。(这才叫热点)
新增测试业务
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey",blockHandler = "dealHandler_testHotKey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2){
return "------testHotKey";
}
public String dealHandler_testHotKey(String p1,String p2,BlockException exception)
{
return "-----dealHandler_testHotKey";
}
sentinel配置
其中参数索引是该资源名下方法参数的位置索引
参数例外项 当参数等于某值时调正阙值
授权规则
在某些场景下,需要根据调用接口的来源判断是否允许执行本次请求。此时就可以使用Sentinel提供的授权规则来实现,Sentinel的授权规则能够根据请求的来源判断是否允许本次请求通过。
在Sentinel的授权规则中,提供了 白名单与黑名单 两种授权类型。白放行、黑禁止
新增测试业务类
@RestController
@Slf4j
public class EmpowerController //Empower授权规则,用来处理请求的来源
{
@GetMapping(value = "/empower")
public String requestSentinel4(){
log.info("测试Sentinel授权规则empower");
return "Sentinel授权规则";
}
}
配置自定义的RequestOriginParser, 实现RequestOriginParser接口
@Component
public class MyRequestOriginParser implements RequestOriginParser
{
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
return httpServletRequest.getParameter("serverName");//根据serverName的值设置黑白名单
}
}
新增sentinel配置,流程应用为MyRequestOriginParser的parameter serverName
测试
localhost:8401/empower?serverName=xx, 通过
localhost:8401/empower?serverName=test, 不通过
localhost:8401/empower?serverName=test1, 不通过
规则的持久化
引入依赖
<!--SpringCloud ailibaba sentinel-datasource-nacos -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
修改application.yml
如果有多个规则,可以配置ds1,ds2…
spring:
cloud:
sentinel:
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow # com.alibaba.cloud.sentinel.datasource.RuleType
其中resulttype根据具体内容配置
在Nacos上新增规则配置
OpenFeign 和 Sentinel整合
修改服务提供者9001
依赖
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--alibaba-sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
修改application.yml
# 激活Sentinel对Feign的支持
feign:
sentinel:
enabled: true
主启动类添加@EnableFeignClients 启动feign的功能
修改公共接口cloud-api-commons
依赖
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--alibaba-sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
新增接口
@FeignClient(value = "nacos-payment-provider",fallback = PayFeignSentinelApiFallBack.class)
public interface PayFeignSentinelApi
{
@GetMapping("/pay/nacos/get/{orderNo}")
public ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo);
}
创建全局服务降级类PayFeignSentinelApiFallBack
@Component
public class PayFeignSentinelApiFallBack implements PayFeignSentinelApi
{
@Override
public ResultData getPayByOrderNo(String orderNo)
{
return ResultData.fail(ReturnCodeEnum.RC500.getCode(),"对方服务宕机或不可用,FallBack服务降级o(╥﹏╥)o");
}
}
修改服务调用者nacosorder83
依赖
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--alibaba-sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
修改application.yml
# 激活Sentinel对Feign的支持
feign:
sentinel:
enabled: true
主启动类添加@EnableFeignClients 启动feign的功能
业务
@RestController
public class OrderNacosController
{
@Resource
private RestTemplate restTemplate;
@Resource
private PayFeignSentinelApi payFeignSentinelApi;
@Value("${service-url.nacos-user-service}")
private String serverURL;
@GetMapping("/consumer/pay/nacos/{id}")
public String paymentInfo(@PathVariable("id") Integer id)
{
String result = restTemplate.getForObject(serverURL + "/pay/nacos/" + id, String.class);
return result+"\t"+" 我是OrderNacosController83调用者。。。。。。";
}
@GetMapping(value = "/consumer/pay/nacos/get/{orderNo}")
public ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo)
{
return payFeignSentinelApi.getPayByOrderNo(orderNo);
}
}
tips: springcloud alibaba没有及时更新,所以需要降低版本才能正确运行
<spring.boot.version>3.0.9</spring.boot.version>
<spring.cloud.version>2022.0.2</spring.cloud.version>
Openfeign 和 gateway整合
新建cloudalibaba-sentinel-gateway9528
修改pom
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.6</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
<version>1.8.6</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
写入application.yml
server:
port: 9528
spring:
application:
name: cloudalibaba-sentinel-gateway # sentinel+gataway整合Case
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes:
- id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:9001 #匹配后提供服务的路由地址
predicates:
- Path=/pay/** # 断言,路径相匹配的进行路由
主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class Main9528
{
public static void main(String[] args)
{
SpringApplication.run(Main9528.class,args);
}
}
配置config 参考github改写
package com.atguigu.cloud.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.*;
/**
* @auther zzyy
* @create 2023-12-01 15:38
* 使用时只需注入对应的 SentinelGatewayFilter 实例以及 SentinelGatewayBlockExceptionHandler 实例即可
*/
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer)
{
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
@Bean
@Order(-1)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
@PostConstruct //javax.annotation.PostConstruct
public void doInit() {
initBlockHandler();
}
//处理/自定义返回的例外信息
private void initBlockHandler() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("pay_routh1").setCount(2).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
BlockRequestHandler handler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
Map<String,String> map = new HashMap<>();
map.put("errorCode", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
map.put("errorMessage", "请求太过频繁,系统忙不过来,触发限流(sentinel+gataway整合Case)");
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(map));
}
};
GatewayCallbackManager.setBlockHandler(handler);
}
}
Seata 分布式事务
是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
XID: 是全局事务的唯一表示,可以在服务的调用链路中传递,绑定到服务的事务上下文中。
TC(Transacation Coordinator) 事务协调器:就是 Seata,负责维护全局事务和 分布事务的状态,驱动全局事务的提交或回滚。 唯一
TM(Transacation Manager) 事务管理器: 标注全局@GlobalTransactional启动入口动作的微服务模块, 负责定义全局事务的范围,并根据TC维护的全局事务和分支事务状态,做出开始事务,提交事务,回滚事务的决议
**RM (Resource Manager) - 资源管理器:**管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
订单库存账户开发微服务
mybatis自动生成
config.properties
#t_pay表包名
package.name=com.atguigu.cloud
# mysql8.0
jdbc.driverClass = com.mysql.cj.jdbc.Driver
jdbc.url= jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
jdbc.user = root
jdbc.password =123456
# seata_order
#jdbc.driverClass = com.mysql.cj.jdbc.Driver
#jdbc.url = jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
#jdbc.user = root
#jdbc.password =123456
# seata_storage
#jdbc.driverClass = com.mysql.cj.jdbc.Driver
#jdbc.url = jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
#jdbc.user = root
#jdbc.password =123456
# seata_account
#jdbc.driverClass = com.mysql.cj.jdbc.Driver
#jdbc.url = jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
#jdbc.user = root
#jdbc.password =123456
generatorConfig.xml
<!-- seata_order -->
<!--<table tableName="t_order" domainObjectName="Order">
<generatedKey column="id" sqlStatement="JDBC"/>
</table>-->
<!--seata_storage-->
<!--<table tableName="t_storage" domainObjectName="Storage">
<generatedKey column="id" sqlStatement="JDBC"/>
</table>-->
<!--seata_account-->
<!--<table tableName="t_account" domainObjectName="Account">
<generatedKey column="id" sqlStatement="JDBC"/>
</table>-->
添加Feign接口
@FeignClient(value = "seata-storage-service")
public interface StorageFeignApi
{
/**
* 扣减库存
*/
@PostMapping(value = "/storage/decrease")
ResultData decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
@FeignClient(value = "seata-account-service")
public interface AccountFeignApi
{
//扣减账户余额
@PostMapping("/account/decrease")
ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money);
}
新建模块seata-order-service2001
修改pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2024</artifactId>
<groupId>com.atguigu.cloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-order-service2001</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--alibaba-seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--cloud-api-commons-->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--通用Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
写入application.yml
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
# ==========applicationName + druid-mysql8 driver===================
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: 123456
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.atguigu.cloud.entities
configuration:
map-underscore-to-camel-case: true
# ========================seata===================
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
application: seata-server
tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
service:
vgroup-mapping: # 点击源码分析
default_tx_group: default # 事务组与TC服务集群的映射关系
data-source-proxy-mode: AT
logging:
level:
io:
seata: info
主启动类
@SpringBootApplication
@MapperScan("com.atguigu.cloud.mapper")
//import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient //服务注册和发现
@EnableFeignClients
public class SeataOrderMainApp2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMainApp2001.class, args);
}
}
Service相关
package com.atguigu.cloud.service;
import com.atguigu.cloud.entities.Order;
/**
* @author xqc
* @version 1.0
* @date 2024/3/30 15:57
*/
public interface OrderService {
/**
* 创建订单
*/
void create(Order order);
}
package com.atguigu.cloud.service.impl;
import com.atguigu.cloud.apis.AccountFeignApi;
import com.atguigu.cloud.apis.StorageFeignApi;
import com.atguigu.cloud.entities.Order;
import com.atguigu.cloud.mapper.OrderMapper;
import com.atguigu.cloud.service.OrderService;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import tk.mybatis.mapper.entity.Example;
/**
* @author xqc
* @version 1.0
* @date 2024/3/30 15:57
*/
@Slf4j
@Service
public class OrderServiceImpl implements OrderService
{
@Resource
private OrderMapper orderMapper;
@Resource//订单微服务通过OpenFeign去调用库存微服务
private StorageFeignApi storageFeignApi;
@Resource//订单微服务通过OpenFeign去调用账户微服务
private AccountFeignApi accountFeignApi;
@Override
@GlobalTransactional(name = "cqx-create-order",rollbackFor = Exception.class) //AT
//@GlobalTransactional @Transactional(rollbackFor = Exception.class) //XA
public void create(Order order) {
//xid检查
String xid = RootContext.getXID();
//1. 新建订单
log.info("==================>开始新建订单"+"\t"+"xid_order:" +xid);
//订单状态status:0:创建中;1:已完结
order.setStatus(0);
int result = orderMapper.insertSelective(order);
//插入订单成功后获得插入mysql的实体对象
Order orderFromDB = null;
if(result > 0)
{
orderFromDB = orderMapper.selectOne(order);
//orderFromDB = orderMapper.selectByPrimaryKey(order.getId());
log.info("-------> 新建订单成功,orderFromDB info: "+orderFromDB);
System.out.println();
//2. 扣减库存
log.info("-------> 订单微服务开始调用Storage库存,做扣减count");
storageFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getCount());
log.info("-------> 订单微服务结束调用Storage库存,做扣减完成");
System.out.println();
//3. 扣减账号余额
log.info("-------> 订单微服务开始调用Account账号,做扣减money");
accountFeignApi.decrease(orderFromDB.getUserId(), orderFromDB.getMoney());
log.info("-------> 订单微服务结束调用Account账号,做扣减完成");
System.out.println();
//4. 修改订单状态
//订单状态status:0:创建中;1:已完结
log.info("-------> 修改订单状态");
orderFromDB.setStatus(1);
Example whereCondition=new Example(Order.class);
Example.Criteria criteria=whereCondition.createCriteria();
criteria.andEqualTo("userId",orderFromDB.getUserId());
criteria.andEqualTo("status",0);
int updateResult = orderMapper.updateByExampleSelective(orderFromDB, whereCondition);
log.info("-------> 修改订单状态完成"+"\t"+updateResult);
log.info("-------> orderFromDB info: "+orderFromDB);
}
System.out.println();
log.info("==================>结束新建订单"+"\t"+"xid_order:" +xid);
}
}
controller
package com.atguigu.cloud.controller;
import com.atguigu.cloud.entities.Order;
import com.atguigu.cloud.resp.ResultData;
import com.atguigu.cloud.service.OrderService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author xqc
* @version 1.0
* @date 2024/3/30 16:00
*/
@RestController
public class OrderController {
@Resource
private OrderService orderService;
/**
* 创建订单
*/
@GetMapping("/order/create")
public ResultData create(Order order)
{
orderService.create(order);
return ResultData.success(order);
}
}
在需要事务的方法上加入@Transacational注解
通过undo_log回滚
ps: 事务涉及到的库不能太长,如果太长需拆分多个事务