单体架构 ->

所有功能模块都在一个项目

优点:开发部署

缺点:无法应对高并发

域名,IP,节点,应用,数据库

集群架构 ->

解决大并发

问题1:模块化升级

订单功能经常升级:v1.0,v2.0

问题2:多语言团队分工协作

C++直播模块

副本,集群,路由,负载均衡,扩缩容

分布式架构

一个大型应用被拆分成很多小应用分布部署在各个机器;工作方式

微服务(自治)独立部署、数据隔离,副本,负载均衡,单点故障,远程调用(RPC),分布式事务,服务熔断,服务雪崩,,服务发现,服务注册,注册中心,配置中心,请求路由

微服务:SpringBoot

注册中心/配置中心:Spring Cloud Alibaba Nacos

网关:Spring Cloud Gateway

远程调用:Spring Cloud OpenFeign

服务熔断:Spring Cloud Alibaba Sentine

分布式事务:Spring Cloud Alibaba Seata

Nacos

注册中心

服务注册

  1. 启动微服务SpringBoot 微服务web项目启动
  2. 引入服务发现依赖spring-cloud-starter-alibaba-nacos-discovery
  3. 配置Nacos地址spring.cloud.nacos.server-addr=127.0.0.1:8848
  4. 查看注册中心效果访问 http://localhost:8848/nacos
  5. 集群模式启动测试单机情况下通过改变端口模拟微服务集群

服务发现

  1. 开启服务发现功能@EnableDiscoveryClient
  2. 测试服务发现API DiscoveryClient Or NacosServiceDiscovery

远程调用

以下订单为例

基本流程

下单场景

实现步骤

  1. 引入负载均衡依赖spring-cloud-starter-loadbalancer
  2. 测试负载均衡API LoadBalancerClient
  3. 测试远程调用RestTemplate
  4. 测试负载均衡调用@LoadBalanced

思考:注册中心宕机,远程调用还能成功吗?

配置中心

基本使用

  1. 启动Nacos

  2. 引入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
  3. application.properties配置

    1
    2
    3
    spring.cloud.nacos.server-addr=127.0.0.1:8848
    # 以逗号分隔可以导入多个配置文件
    spring.config.import=nacos:service-order.properties
  4. 创建data-id(数据集)

  5. 禁用导入检查

    1
    spring.cloud.nacos.config.import-check.enabled=false

动态刷新

  • @Value(“${xx}”) 获取配置 + @RefreshScope 实现自动刷新
  • @ConfigurationProperties(prefix="配置项前缀") 无感自动刷新(批量绑定,驼峰),注意需要加上@Component

配置监听

  • NacosConfigManager 监听配置变化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Bean
    ApplicationRunner applicationRunner(NacosConfigManager nacosConfigManager){
    return args -> {
    ConfigService configService = nacosConfigManager.getConfigService();
    configService.addListener("service-order.properties",
    "DEFAULT_GROUP", new Listener() {
    @Override
    public Executor getExecutor() {
    return Executors.newFixedThreadPool(4);
    }

    @Override
    public void receiveConfigInfo(String configInfo) {
    System.out.println("变化的配置信息:"+configInfo);
    System.out.println("邮件通知...");
    }
    });
    System.out.println("=========");
    };
    }

思考: Nacos中的数据集 和 application.properties 有相同的 配置项,哪个生效?

以配置中心为准

先导入优先,外部优先

数据隔离

  • 需求描述
    • 项目有多套环境:dev,test,prod
    • 每个微服务,同一种配置,在每套环境的值都不一样
      • 如:database.properties
      • 如:common.properties
    • 项目可以通过切换环境,加载本环境的配置
  • 难点
    • 区分多套环境
    • 区分多种微服务
    • 区分多种配置
    • 按需加载配置

namespace

动态切换

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
server:
port: 8000
spring:
profiles:
active: prod
include: feign
application:
name: service-order
cloud:
nacos:
server-addr: 127.0.0.1:8848
config:
import-check:
enabled: false
namespace: ${spring.profiles.active:public}

logging:
level:
top.qianqianzyk.order.feign: debug

---
spring:
config:
import:
- nacos:common.properties?group=order
- nacos:database.properties?group=order
activate:
on-profile: dev
---
spring:
config:
import:
- nacos:common.properties?group=order
- nacos:database.properties?group=order
- nacos:haha.properties?group=order
activate:
on-profile: test
---
spring:
config:
import:
- nacos:common.properties?group=order
- nacos:database.properties?group=order
- nacos:hehe.properties?group=order
activate:
on-profile: prod

总结

注册中心

  1. 引入 spring-cloud-starter-alibaba-nacosdiscovery依赖,配置Nacos地址
  2. @EnableDiscoveryClient 开启服务发现功能
  3. 扩展:
    • DiscoveryClient 获取服务实例列表
    • LoadBalancerClient 负载均衡选择一个实例
      (需要引入 spring-cloud-starter-loadbalancer)
    • RestTemplate 可以发起远程调用

配置中心

  1. 引入 spring-cloud-starter-alibaba-nacosconfig依赖,配置Nacos地址
  2. 添加 数据集(data-id),使用spring.config.import 导入数据集
  3. @Value + @RefreshScope 取值 + 自动刷新
  4. @ConfigurationProperties批量绑定自动刷新
  5. NacosConfigManager 监听配置变化
  6. 扩展:
  • 配置优先级;namespace区分环境、group区分
  • 微服务、data-id区分配置 实现 数据隔离+环境切换

OpenFeign

声明式REST客户端 @EnableFeignClients 开启Feign远程调用功能

注解驱动

  • 指定远程地址:@FeignClient
  • 指定请求方式:@GetMapping、@PostMapping、@DeleteMapping …
  • 指定携带数据:@RequestHeader、@RequestParam、@RequestBody …
  • 指定结果返回:响应模型
1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

远程调用

业务API

如果调用自己写的API,直接把对应的Controller签名复制过来即可

1
2
3
4
5
6
7
8
@FeignClient(value = "service-product") // feign客户端
public interface ProductFeignClient {
// mvc注解的两套使用逻辑
//1、标注在Controller上,是接受这样的请求
//2、标注在FeignClient上,是发送这样的请求
@GetMapping("/product/{id}")
Product getProductById(@PathVariable("id") Long id, @RequestHeader("token") String token);
}

第三方API

1
2
3
4
5
6
7
8
9
@FeignClient(value = "weather-client", url = "http://aliv18.data.moji.com")
public interface WeatherFeignClient {


@PostMapping("/whapi/json/alicityweather/condition")
String getWeather(@RequestHeader("Authorization") String auth,
@RequestParam("token") String token,
@RequestParam("cityId") String cityId);
}

思考:客户端负载均衡与服务端负载均衡区别

日志

1
2
3
logging:
level:
top.qianqianzyk.order.feign: debug
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class OrderConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}

@LoadBalanced //注解式负载均衡
@Bean
RestTemplate restTemplate(){
return new RestTemplate();
}
}

超时控制

  • connectTimeout 连接超时(默认10s)
  • readTimeout 读取超时(默认60s)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
spring:
cloud:
openfeign:
client:
config:
default:
logger-level: full
connect-timeout: 1000
read-timeout: 2000
service-product:
logger-level: full
connect-timeout: 3000
read-timeout: 5000
sentinel:
transport:
dashboard: localhost:8080
eager: true
web-context-unify: false
# request-interceptors:
# - com.atguigu.order.interceptor.XTokenRequestInterceptor
# retryer: feign.retryer.Default

feign:
sentinel:
enabled: true

重试机制

远程调用超时失败后,还可以进行多次尝试,如果某次成功返回ok,如果多次依然失败则结束调用,返回错误

默认NEVER.RETRY

超过了一秒就以一秒计

1
retryer: feign.retryer.Default

Or

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class OrderConfig {
@Bean
Retryer retryer(){
return new Retryer.Default();
}

@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}

@LoadBalanced //注解式负载均衡
@Bean
RestTemplate restTemplate(){
return new RestTemplate();
}
}

拦截器

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 只要有RequestInterceptor,容器会自动应用,spring自动扫描,或者在yaml加上
// request-interceptors:
// - top.qianqianzyk.order.interceptor.XTokenRequestInterceptor
@Component
public class XTokenRequestInterceptor implements RequestInterceptor {


/**
* 请求拦截器
* @param template 请求模板
*/
@Override
public void apply(RequestTemplate template) {
System.out.println("XTokenRequestInterceptor ....... ");
template.header("X-Token", UUID.randomUUID().toString());
}
}

Fallback

Fallback:兜底返回
注意:此功能需要整合 Sentinel 才能实现

1
2
3
feign:
sentinel:
enabled: true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@FeignClient(value = "service-product",fallback = ProductFeignClientFallback.class) // feign客户端
public interface ProductFeignClient {
//mvc注解的两套使用逻辑
//1、标注在Controller上,是接受这样的请求
//2、标注在FeignClient上,是发送这样的请求
@GetMapping("/product/{id}")
Product getProductById(@PathVariable("id") Long id);
}

@Component
public class ProductFeignClientFallback implements ProductFeignClient {
@Override
public Product getProductById(Long id) {
System.out.println("兜底回调....");
Product product = new Product();
product.setId(id);
product.setPrice(new BigDecimal("0"));
product.setProductName("未知商品");
product.setNum(0);

return product;
}
}

Sentinel

服务保护(限流、熔断降级)

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Spring Cloud Alibaba Sentinel 以流量为切入点,从流量控制、流量路由、熔断降级、系统自适应过载保护、热点流量防护等多个维度保护服务的稳定性

  • 定义资源:
    • 主流框架自动适配(Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux、Reactor);所有Web接口均为资源
    • 编程式:SphU API
    • 声明式:@SentinelResource
  • 定义规则:
    • 流量控制(FlowRule)
    • 熔断降级(DegradeRule)
    • 系统保护(SystemRule)
    • 来源访问控制(AuthorityRule)
    • 热点参数(ParamFlowRule)
1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
1
2
3
4
5
sentinel:
transport:
dashboard: localhost:8080
eager: true # 提前加载
web-context-unify: false

声明资源

1
@SentinelResource(value = "createOrder")

启动控制器

1
java -jar sentinel-dashboard-1.8.8.jar
1
SphU.entry()

异常处理

Web接口

model下的common

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import lombok.Data;

@Data
public class R {

private Integer code;
private String msg;
private Object data;


public static R ok() {
R r = new R();
r.setCode(200);
return r;
}

public static R ok(String msg,Object data) {
R r = new R();
r.setCode(200);
r.setMsg(msg);
r.setData(data);
return r;
}

public static R error() {
R r = new R();
r.setCode(500);
return r;
}

public static R error(Integer code,String msg){
R r = new R();
r.setCode(code);
r.setMsg(msg);
return r;
}
}

自定义BlockExceptionHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
String resourceName, BlockException e) throws Exception {
response.setStatus(429); //too many requests
response.setContentType("application/json;charset=utf-8");

PrintWriter writer = response.getWriter();


R error = R.error(500, resourceName + " 被Sentinel限制了,原因:" + e.getClass());

String json = objectMapper.writeValueAsString(error);
writer.write(json);

writer.flush();
writer.close();
}
}

@SentinelResource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@GetMapping("/seckill")
@SentinelResource(value = "seckill-order",fallback = "seckillFallback")
public Order seckill(@RequestParam(value = "userId",required = false) Long userId,
@RequestParam(value = "productId",defaultValue = "1000") Long productId){
Order order = orderService.createOrder(productId, userId);
order.setId(Long.MAX_VALUE);
return order;
}

public Order seckillFallback(Long userId,Long productId, Throwable exception){
System.out.println("seckillFallback....");
Order order = new Order();
order.setId(productId);
order.setUserId(userId);
order.setAddress("异常信息:"+exception.getClass());
return order;
}

OpenFeign

如果有fallback方法,则会invoke执行兜底回调,否则会抛给spring Boot全局异常处理

流控规则(FlowRule)

限制多余请求,从而保护系统资源不被耗尽

属性 说明 默认值
resource 资源名,资源名是限流规则的作用对象
count 限流阈值
grade 限流阈值类型,QPS 模式(1)或并发线程数模式(0) QPS 模式
limitApp 流控针对的调用来源 default,代表不区分调用来源
strategy 调用关系限流策略:直接、链路、关联 直接(根据资源本身)
controlBehavior 流控效果(直接拒绝/WarmUp/匀速+排队等待),不支持按调用关系限流 直接拒绝
clusterMode 是否集群限流

阈值类型

  • QPS:
    • 统计每秒请求数
  • 并发线程数:
    • 统计并发线程数

流控模式

调用关系包括调用方、被调用方;一个方法又可能会调用其它方法,形成一个调用链路的层次关系;有了调用链路的统计信息,我们可以衍生出多种流量控制手段

直接

直接对某个资源进行流量控制

链路

举个例子,我们正常创建订单的时候不做限制,但是涉及到秒杀的时候就会进行流量控制

要链路控制,就需要关闭统一的默认上下文

1
2
sentinel:
web-context-unify: false

关联

当写的请求量不大的时候,读不会受到限制;如果此时有大量写的请求,Sentinel就会限制读的请求,来将更多的资源分配给写

流控效果

直接 - 快速失败

如果流量符合流控规则,则交给业务处理;如果超出限制,则会抛出BlockException异常,对于这个异常我们可以自定义进行处理,比如封装成json返回

Warm Up - 只适用于直接的流控模式

如果遇见高峰流量请求,会经过一段时间的冷启动,根据QPS(峰值)和Period,逐步上调当前QPS直到达到阈值

匀速排队(Leaky Bucket算法和虚拟队列等) - 只适用于直接的流控模式

QPS=2时,每秒两个,则500ms放行一个,不支持QPS>1000(因为1s=1000ms,QPS大于1000后精度会失效)

每秒放行两个,多余的请求会排队,如果排队的时间达到了timeout的设定值,该请求会被丢弃

熔断规则- 熔断降级(DegradeRule)

  • 切断不稳定调用
  • 快速返回不积压
  • 避免雪崩效应

熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置

属性 说明 默认值
resource 资源名,必填
grade 熔断策略,支持慢调用比例/异常比例/异常数策略 慢调用比例
count 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值
timeWindow 熔断时长,单位为 s
minRequestAmount 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) 5
statIntervalMs 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) 1000 ms
slowRatioThreshold 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入)

断路器

慢调用比例

  • 最大RT(ms):只要超过设定值则会认为是慢请求
  • 比例阈值:如果慢请求比例达到了设定值,则会开启熔断
  • 统计时长,最小请求数:只有在统计时长内达到了最小请求数,才会开始统计
  • 熔断时长

异常比例

异常数

热点规则

热点参数

需求1:每个用户秒杀 QPS 不得超过 1(秒杀下单 userId 级别)
效果:携带此参数的参与流控,不携带不流控
需求2:6号用户是vvip,不限制QPS(例外情况)
需求3:666号是下架商品,不允许访问

热点规则可以对请求参数做限制,也就是说当某一参数携带特定的值时,热点限流将起作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@GetMapping("/seckill")
@SentinelResource(value = "seckill-order",fallback = "seckillFallback")
public Order seckill(@RequestParam(value = "userId",required = false) Long userId,
@RequestParam(value = "productId",defaultValue = "1000") Long productId){
Order order = orderService.createOrder(productId, userId);
order.setId(Long.MAX_VALUE);
return order;
}

// 如果兜底回调设置成fallback,接受异常设置成Throwable,则该兜底回调不仅可以服务业务异常也可以服务因热点限流导致的异常
public Order seckillFallback(Long userId,Long productId, Throwable exception){
System.out.println("seckillFallback....");
Order order = new Order();
order.setId(productId);
order.setUserId(userId);
order.setAddress("异常信息:"+exception.getClass());
return order;
}

Gateway

路由

需求

  1. 客户端发送 /api/order/** 转到 service-order
  2. 客户端发送 /api/product/** 转到 service-product
  3. 以上转发有负载均衡效果
1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
spring:
profiles:
include: route
application:
name: gateway
cloud:
nacos:
server-addr: 127.0.0.1:8848

# localhost/api/order
server:
port: 80

application-route.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowed-origin-patterns: '*'
allowed-headers: '*'
allowed-methods: '*'

routes:
# 只要有一条匹配后面的将不会再匹配,可以设置order
- id: bing-route
uri: https://cn.bing.com/
predicates:
- name: Path
args:
patterns: /search
- name: Query
args:
param: q
regexp: haha
# - Vip=user,leifengyang
- name: Vip
args:
param: user
value: leifengyang
order: 10
metadata:
hello: world
- id: order-route
uri: lb://service-order
predicates:
- name: Path
args:
patterns: /api/order/**
matchTrailingSlash: true
filters:
- RewritePath=/api/order/?(?<segment>.*), /$\{segment}
- OnceToken=X-Response-Token, jwt
order: 1
- id: product-route
uri: lb://service-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/product/?(?<segment>.*), /$\{segment}
order: 2
default-filters:
- AddResponseHeader=X-Response-Abc, 123

断言

长短写法

1
2
3
4
5
6
7
8
predicates:
- Path=/api/order/**

predicates:
- name: Path
args:
patterns: /api/order/**
matchTrailingSlash: true

所有的断言都是RoutePredicateFactory的实现,ctrl + H

Query

参数(个数/类型) 作用
After 1/datetime 在指定时间之后
Before 1/datetime 在指定时间之前
Between 2/datetime 在指定时间区间内
Cookie 2/string,regexp 包含cookie名且必须匹配指定值
Header 2/string,regexp 包含请求头且必须匹配指定值
Host N/string 请求host必须是指定枚举值
Method N/string 请求方式必须是指定枚举值
Path 2/List<String>,bool 请求路径满足规则,是否匹配最后的/
Query 2/string,regexp 包含指定请求参数
RemoteAddr 1/List<String> 请求来源于指定网络域(CIDR写法)
Weight 2/string,int 按指定权重负载均衡
XForwarded RemoteAddr 1/List<string> 从X-Forwarded-For请求头中解析请求来源,并判断是否来源于指定网络域

自定义断言工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Component
public class VipRoutePredicateFactory extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {


public VipRoutePredicateFactory() {
super(Config.class);
}

@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
ServerHttpRequest request = serverWebExchange.getRequest();

String first = request.getQueryParams().getFirst(config.param);

return StringUtils.hasText(first) && first.equals(config.value);
}
};
}

@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("param", "value");
}

/**
* 可以配置的参数
*/
@Validated
public static class Config {

@NotEmpty
private String param;


@NotEmpty
private String value;

public @NotEmpty String getParam() {
return param;
}

public void setParam(@NotEmpty String param) {
this.param = param;
}

public @NotEmpty String getValue() {
return value;
}

public void setValue(@NotEmpty String value) {
this.value = value;
}
}
}

过滤器

路径重写

1
2
3
4
5
6
7
8
# /api/order/read -> /read
filters:
- RewritePath=/api/order/?(?<segment>.*), /$\{segment}
- OnceToken=X-Response-Token, jwt

# 默认过滤器
default-filters:
- AddResponseHeader=X-Response-Abc, 123

GlobalFilter

对所有请求进行拦截,可以在请求到达微服务之前(前置逻辑)和返回给客户端之前(后置逻辑)执行自定义操作

常见用途:

  • 日志记录
  • 权限校验
  • 请求修改(比如添加或修改请求头)
  • 响应修改(比如统一包装返回格式)
  • 限流
  • 负载均衡
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Component
@Slf4j
public class RtGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();

String uri = request.getURI().toString();
long start = System.currentTimeMillis();
log.info("请求【{}】开始:时间:{}",uri,start);
//========================以上是前置逻辑=========================

// 响应式编程,需要使用doFinally,否则后置逻辑会异步实现,无法达到效果
Mono<Void> filter = chain.filter(exchange)
.doFinally((result)->{
//=======================以下是后置逻辑=========================
long end = System.currentTimeMillis();
log.info("请求【{}】结束:时间:{},耗时:{}ms",uri,end,end-start);
}); //放行 10s



return filter;
}

@Override
public int getOrder() {
return 0;
}
}

自定义过滤器工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Component
public class OnceTokenGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public GatewayFilter apply(NameValueConfig config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//每次响应之前,添加一个一次性令牌,支持 uuid,jwt等各种格式
return chain.filter(exchange).then(Mono.fromRunnable(()->{
ServerHttpResponse response = exchange.getResponse();
HttpHeaders headers = response.getHeaders();
String value = config.getValue();
if ("uuid".equalsIgnoreCase(value)){
value = UUID.randomUUID().toString();
}

if ("jwt".equalsIgnoreCase(value)){
value = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
}

headers.add(config.getName(),value);
}));
}
};
}
}

1
2
filters:
- OnceToken=X-Response-Token, jwt

全局跨域

单体服务

  • 可以在每个Controller上加上@CrossOrigin注解
  • 或者使用CorsFilter方法

Gateway

1
2
3
4
5
6
7
8
9
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': # 表示对所有路径生效
allowed-origin-patterns: '*' # 允许所有来源(跨域请求)
allowed-headers: '*' # 允许所有请求头
allowed-methods: '*' # 允许所有请求方法(GET, POST, PUT, DELETE等)

思考:微服务之间的调用经过网关吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@FeignClient(value = "service-product",fallback = ProductFeignClientFallback.class) // feign客户端
public interface ProductFeignClient {


//mvc注解的两套使用逻辑
//1、标注在Controller上,是接受这样的请求
//2、标注在FeignClient上,是发送这样的请求
@GetMapping("/product/{id}")
Product getProductById(@PathVariable("id") Long id);


}

// 如果需要通过网关,则需要进行修改
@FeignClient(value = "gateway",fallback = ProductFeignClientFallback.class) // feign客户端
public interface ProductFeignClient {


// 补充至全部路径
@GetMapping("/api/product/{id}")
Product getProductById(@PathVariable("id") Long id);


}

Seata

解决分布式事务

TC (Transaction Coordinator)- 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚

TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务

RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状
态,并驱动分支事务提交或回滚

Seata Java Download | Apache Seata

1
seata-server.bat

配置file.conf(每个服务)

1
2
3
4
5
6
7
8
9
10
service {
#transaction service group mapping
vgroupMapping.default_tx_group = "default"
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}

为每个分支事务配置好RM后,只需要在最大的入口配置全局事务注解即可@GlobalTransactional

二阶提交协议

四种事务模式

AT

AT 模式是 Seata 最常用的事务模式,适用于基于关系型数据库(如 MySQL、PostgreSQL)的业务场景

工作原理

  • AT 模式基于 二阶段提交协议(2PC),但对数据库操作进行了无侵入增强
  • 由Seata 代理业务SQL语句,在本地事务中自动维护回滚日志,实现自动补偿

执行流程

  1. 第一阶段(Try)—— 业务 SQL 执行,Seata 记录回滚日志
    • 业务代码执行普通的 INSERT/UPDATE/DELETE 语句;
    • Seata 通过数据代理,在执行 SQL 之前,生成一份回滚日志,记录数据变更前的状态。
    • SQL 语句执行完成后,事务暂不提交。
  2. 第二阶段(Confirm 或 Rollback)
    • 提交:直接提交本地事务,无需额外操作。
    • 回滚:Seata 通过回滚日志生成补偿 SQL,自动恢复数据。
  • 无侵入
  • 自动回滚
  • 性能较优:相比 XA 模式,不锁住整个事务时间,只在本地数据库生效

TCC

TCC 模式是一种业务层面的二阶段提交,由开发者手动实现补偿逻辑,适用于跨数据库、跨服务的分布式事务。

工作原理

TCC 也是基于二阶段提交协议(2PC):

  1. Try(尝试)
    • 业务执行前,预留资源。
  2. Confirm(确认)
    • 业务执行成功后,真正提交事务。
  3. Cancel(取消)
    • 业务失败或事务回滚时,释放资源。
  • 无数据库限制,适用于 NoSQL、RPC 等非关系型数据存储。
  • 业务自定义控制,可以优化性能,减少数据库锁定时间。

Saga

Saga 是基于 长事务 的补偿机制,适用于长时间运行的业务(如机票预订、订单支付)。

工作原理

Saga 采用 事务编排 或 事务悬挂 的方式,分多个子事务依次执行:

  • 正向事务(S1 → S2 → S3):逐步执行业务操作
  • 回滚事务(C3 → C2 → C1):当某个事务失败时,执行补偿操作(撤销之前的步骤)

执行流程

  1. 事务链执行

    • 例如:用户下单(扣款) → 预定酒店 → 预定机票。
    • 每个操作都是独立事务,彼此没有锁定关系。
  2. 事务回滚

    • 如果某一步失败(如机票预订失败),则执行

      反向补偿事务:

      • 取消酒店预订
      • 退还用户扣款

适用场景

  • 长时间事务(如电商订单、机票酒店预订)。
  • 可补偿的业务流程(每个子事务必须有对应的撤销逻辑)。

优点

没有全局锁,适用于长事务场景,性能较优。
适合业务编排,可以灵活控制事务流程。


XA

XA 是 强一致性 事务,基于 XA 协议(2PC),由数据库本身提供 分布式事务管理。

工作原理

XA 也是 二阶段提交:

  1. 第一阶段(Prepare)
    • 事务管理器(TM)通知各数据库准备提交。
    • 各数据库锁定资源,但不提交事务。
  2. 第二阶段(Commit 或 Rollback)
    • Commit:所有数据库都正式提交事务。
    • Rollback:某个数据库失败,则所有数据库回滚事务。

适用场景

  • 适用于强一致性需求(如金融交易)。
  • 适用于支持 XA 事务的数据库(如 MySQL InnoDB、PostgreSQL)。

优点

数据强一致性,适用于金融等高要求场景

缺点

性能低,因为数据库需要长时间锁定资源。
需要数据库支持 XA 事务,适用范围受限。