认识微服务

单体架构

将业务的所有功能集中在一个项目中开发,打成一个包部署

优点

  1. 架构简单
  2. 部署成本低啊

    缺点

  3. 团队协作成本高
  4. 系统发布效率低
  5. 系统可用性差

    微服务架构

    把单体架构的功能模块拆分成多个独立的项目

    优点

  6. 粒度小: 拆分的项目可以可以只是一个单独的功能
  7. 团队自治
  8. 服务自治

    拆分黑马商城

    原单体架构的目录结构

    拆分商品功能为一个微服务

    本项目利用maven模块拆分,亦可以创建多个项目实例

    创建maven模块(item-service)

    将item商品的有关代码复制粘贴到item-service中
    需要创建启动类和修改yaml文件(端口号,和swagger的扫描路径)和添加pom的依赖

    创建item数据库

    因为是微服务和原本的项目已经隔离开了,所以每一个微服务都要需要对应的mysql表

    同理拆分购物车

注册中心

微服务之间是互相隔离的,相当于独立的小项目,所以微服务之间不能直接相互调用
但在实际业务中,经常会需要商品业务的业务逻辑中需要调用购物车的api,同理购物车也要商品的api等
注册中心就可以解决微服务之间互相调用的问题

注册中心原理


流程如下:

  • 服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
  • 调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
  • 调用者自己对实例列表负载均衡,挑选一个实例
  • 调用者向该实例发起远程调用

当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?

  • 服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)
  • 当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
  • 当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
  • 当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表

Nacos注册中心

简介 Nacos 有阿里开发
实际上 Nacos也需要一台独立的计算机来部署,为了方便学习,我们在虚拟机上利用Docker部署Nacos

创建Nacos数据库

数据库sql可在官方文档中查找

Docker部署Nacos

nacos的数据库配置传入虚拟机

创造容器并执行

1
2
3
4
5
6
7
8
docker run -d \
--name nacos \
--env-file ./nacos/custom.env \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
--restart=always \
nacos/nacos-server:v2.1.0-slim

部署成功页面

服务注册

  1. 添加依赖
    1
    2
    3
    4
    5
    <!--nacos 服务注册发现-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
  2. yaml文件配置地址和端口号
    1
    2
    3
    4
    5
    6
    spring:
    application:
    name: item-service # 服务名称
    cloud:
    nacos:
    server-addr: 192.168.217.128:8848 # nacos地址

服务发现

  1. 添加依赖
    1
    2
    3
    4
    5
    <!--nacos 服务注册发现-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
  2. yaml文件配置地址和端口号
    1
    2
    3
    4
    5
    6
    spring:
    application:
    name: item-service # 服务名称
    cloud:
    nacos:
    server-addr: 192.168.217.128:8848 # nacos地址

    服务调用

  3. 添加依赖
    1
    2
    3
    4
    5
    <!--nacos 服务注册发现-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
  4. yaml文件配置地址和端口号
    1
    2
    3
    4
    5
    6
    spring:
    application:
    name: item-service # 服务名称
    cloud:
    nacos:
    server-addr: 192.168.217.128:8848 # nacos地址
  5. 利用DiscoveryClient工具
    以下代码可以获取到nacos中的一个item-service的实例,然后获取该实例url发送请求即可
    1
    2
    3
    4
    5
    6
    7
    //获取实例
    List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
    if (CollUtils.isEmpty(instances)) {
    return;
    }
    //负载均衡 选择一个实例
    ServiceInstance serviceInstance = instances.get(RandomUtil.randomInt(instances.size()));

    OpenFeign

    OpenFeign 用于简化服务调用的过程

    OpenFeign快速入门

    1. 引入依赖

    在cart-service服务的pom.xml中引入OpenFeign的依赖和loadBalancer依赖
    loadBalancer依赖是用与负载均衡的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!--openFeign-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--负载均衡器-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>

    2.启动OpenFeign功能

    接下来,我们在cart-service的CartApplication启动类上添加注解,启动OpenFeign功能

3.编写FeignClient

  1. @GetMapping(“/items”)
    ListqueryItemByIds(@RequestParam(“ids”) Collectionids);
    这个方法使需要从item-service模块的controller找到对应的方法
    例如这是未拆分前的购物车中调用item的方法这是一个通过id查询商品的功能,
    如果需要拆分调用,我们就需要在item模块的controller中找到通过id查询商品的接口,将这个接口抽取出来

    如果需要拆分调用,我们就需要在item模块的controller中找到通过id查询商品的接口,将这个接口抽取出来

    抽取到一个公共模块中

  2. @FeignClient(“item-service”)的作用是获取nacos中item-service的实例

  3. @GetMapping(“/items”)的作用:声明请求路径,会在获得实例的http路径后加上/items
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package com.hmall.cart.client;

    import com.hmall.cart.domain.dto.ItemDTO;
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;

    import java.util.List;

    @FeignClient("item-service")
    public interface ItemClient {

    @GetMapping("/items")
    List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
    }

    4.调用 ItemClient

    1
    List<ItemDTO> items = itemClient.queryItembyidList(itemIds);
    相比于之前代码比较
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /获取实例
    List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
    if (CollUtils.isEmpty(instances)) {
    return;
    }
    //负载均衡 选择一个实例
    ServiceInstance serviceInstance = instances.get(RandomUtil.randomInt(instances.size()));
    ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
    serviceInstance.getUri() + "/items?ids={ids}",
    HttpMethod.GET,
    null,
    new ParameterizedTypeReference<List<ItemDTO>>() {},
    Map.of("ids", CollUtils.join(itemIds, ","))
    );
    List<ItemDTO> items =null;
    if (response.getStatusCode().is2xxSuccessful()) {
    items = response.getBody();
    }

    OpenFeign 的连接池

    Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括:
  • HttpURLConnection:默认实现,不支持连接池
  • Apache HttpClient :支持连接池
  • OKHttp:支持连接池

因此我们通常会使用带有连接池的客户端来代替默认的HttpURLConnection。比如,我们使用OK Http.

引入依赖

1
2
3
4
5
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>

开启连接池 在yaml文件中

1
2
3
feign:
okhttp:
enabled: true # 开启OKHttp功能

抽取OpenFeign为公共模块

因为很多功能模块都需要用到OpenFeign,抽取成公共模块可以使代码更优雅
抽取成如下,同时需要添加nacos,OpenFeign等需要的依赖

当其他功能模块需要使用远程调用的时候导入hm-api的坐标即可
这个错误表示查找不到对应的Bean,说明扫描包并没有扫描到hm-api

在启动类添加注解 @EnableFeignClients(basePackages = “com.heima.api.client”) 即可

网关

网关 就是网络的关口,负责请求的路由,转发,身份校验。
网关一般用于微服务中,微服务中每个微服务都有各自的ip地址请求路径等,

  1. 不使用网关的情况下 前端每次发送请求都需要发送到微服务的IP地址上,并且都需要身份验证,这样会很麻烦。
  2. 使用网关的情况下,前端的所有请求都可以发送到网关上,由网关分析,身份验证,处理然后转发到对应的微服务上
    网关的作用我觉得和单体架构的拦截器有点像,都可以拦截请求路径,然后处理判断,再发送到具体的请求方法上,
    不过网关更加强大,可以拦截不同IP地址的请求,再转发
    网关也更像小区保安 : 一个前端请求过来,网关就会判断请求转发的IP地址,在身份确认后转发该请求

    网关快速入门

    网关本身也是一个微服务,所以需要先创建网关微服务
  • 创建网关
  • 引入网关和负载均衡的依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!--网关-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!--nacos discovery-->
    <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-loadbalancer</artifactId>
    </dependency>
  • 创建启动类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package com.hmall.gateway;

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;

    @SpringBootApplication
    public class GatewayApplication {
    public static void main(String[] args) {
    SpringApplication.run(GatewayApplication.class, args);
    }
    }

  • 配置yaml文件

网关登录验证

使用网关进行登录校验,首先我们需要先了解网关的执行流程

网关的执行流程

当前端发来请求到网关,网关会先执行断言(HandlerMapper),断言的作用是根据前端传来的请求,来判断目的地微服务,断言结束后,会执行过滤器处理器,里面会有很多的过滤器,会依次执行这些过滤器,NettyRoutingFilter是最后一个过滤器,它负责将处理后的请求转发到微服务上
所以网关登录校验即是自定义一个过滤器用来判断身份

自定义网关过滤器

GlobalFilter : 全局过滤器,作用范围i是所有路由,声明后自动生效
以下是一个自定义的网关过滤器:

  1. 该过滤器实现了 GlobalFilter接口,其中ServerWebExchange exchange变量是存储过滤器中的数据的,所以每一个过滤器调用数据都需要从exchange中调取,经过过滤器处理后需要得到一个新的ServerWebExchange类型的变量例如exchange1,然后 利用GatewayFilterChain chain的chain.filter(exchange1)方法将处理后的数据传入下一个过滤器
  2. 该过滤器实现了 Ordered接口 ,是用来规定过滤器的执行顺序的,Ordered的方法返回值越小,则越先执行
  3. 该过滤器的作用:先获取请求头,从请求头中的路径判断是否需要拦截(因为有些功能不需要拦截,例如注册登录),需要拦截则获取请求头Authorization中的token,如果token为空则返会401,token不为空则解析token,获得token中的userId,然后将userId封装到user-info请求头中,并放入新的exchange1 中,传入下一个过滤器,最终转发到微服务上
    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
    @RequiredArgsConstructor
    @Component
    public class AuthGlobalFilter implements GlobalFilter, Ordered {
    private final AuthProperties authProperties;
    private final JwtTool jwtTool;
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    // 1,获取请求头
    ServerHttpRequest request = exchange.getRequest();
    // 判断是否需要拦截
    if(isExclude(request.getPath().toString()))
    {
    return chain.filter(exchange);
    }
    // 获取token
    String token =null;
    List<String> authorization = request.getHeaders().get("Authorization");
    if (authorization != null && authorization.size() > 0) {
    token = authorization.get(0);
    }
    // 解析校验token
    Long userId = null;
    try {
    userId= jwtTool.parseToken(token);
    }catch (UnauthorizedException e)
    {
    ServerHttpResponse response = exchange.getResponse();
    response.setStatusCode(HttpStatus.UNAUTHORIZED);
    return response.setComplete();
    }
    //传递用户信息
    String userInfo = userId.toString();
    ServerWebExchange exchange1 = exchange.mutate()
    .request(builder -> builder.header("user-info", userInfo))
    .build();
    return chain.filter(exchange1);
    }

    private boolean isExclude(String path) {
    for(String pathPattern : authProperties.getExcludePaths()){
    if (antPathMatcher.match(pathPattern, path)) {
    return true;
    }
    }
    return false;
    }


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

    网关传递用户信息

    1. 在自定义网关过滤器中,已经将用户Id传入请求头中,所以在微服务中需要拦截器获取用户Id
    2. 在公共类中创建拦截器,因为在网关中已经做过获取和解析token了,所以这个请求头里并没有加密,只有用户id
    3. 注册拦截器,拦截器需要一个配置类注册才能生效
      需要多加一个注解@ConditionalOnClass(DispatcherServlet.class),该注解的意思是这个配置类生效的范围是拥有DispatcherServlet.class的模块,因为网关的依赖的中并没有springmvc,其他模块微服务都是基于springmvc的,而DispatcherServlet.class正是springmvc的核心之一,所以这个配置类将不会再网关中生效

      OpenFeign的补充

    4. 前面已经实现了用OpenFeign进行微服务之间的调用,还有一个问题微服务之间调用的同时传入用户信息需要OpenFeign解决
    5. 我们利用网关将用户登录的信息转发到了微服务上,但是微服务和微服务之间相互调用的时候并不能传入用户的登录信息,
    6. 我们又知道微服务之间的远程调用时利用OpenFeign做到的,所以OpenFeign提供了拦截器为我们解决这个问题

      OpenFeign的拦截器


      这个拦截器很简单就写在了同一个配置类下,当然可以新建一个配置类然后实现RequestInterceptor接口

Nacos的扩展 配置共享

在我们的微服务中很多配置(例如mybatispuls,日志,数据库连接池)都是微服务共有的,这需要我们在每个微服务里都写一遍,这是很麻烦的
所以Nacos为我们解决了这个问题

配置共享

在Nacos上添加配置

注意 :dataID不能乱写,因为是yaml格式的文件,所以 dataid的结尾必须是 .yaml

添加依赖

1
2
3
4
5
6
7
8
9
10
<!--nacos配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--读取bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

编写bootstrap.yaml

在此之前我们需要先了解sringboot的启动顺序,springboot启动会先加载application.yml,然后初始化ApplicationContext;
在微服务Springcloud中启动,sringclound会先在bootstrap.yaml中拉取nacos的共享配置,然后初始化ApplicationContext,接着加载springboard的application.yml,进行配置整合,最后初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
application:
name: cart-service # 服务名称
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.217.128 # nacos地址
config:
file-extension: yaml # 文件后缀名
shared-configs: # 共享配置
- dataId: shared-jdbc.yaml # 共享mybatis配置
- dataId: shared-swagger.yaml # 共享日志配置

配置热更新

当我们在idea中修改了项目的配置的时候,一般都需要重启项目才能生效,但利用nacos可以做到热更新
添加配置

然后需要创建一个类用于接收配置

1
2
3
4
5
6
7
8
9
10
11
12
package com.hmall.cart.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {
private Integer maxAmount;
}

这样maxAmount 就可以实时等于配置中的数据了

动态路由

动态路由也一样,防止路由变化然后需要项目重启

添加依赖

1
2
3
4
5
6
7
8
9
10
<!--统一配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--加载bootstrap-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

然后在网关gateway的resources目录创建bootstrap.yaml文件

1
2
3
4
5
6
7
8
9
10
11
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.150.101
config:
file-extension: yaml
shared-configs:
- dataId: shared-log.yaml # 共享日志配置
- dataId: gateway-routes.json #这个需要根据自己定义的dataId

配置路由监听器

因为我们需要先监听路由是否变化,如果路由没有变化,就不用去更新路由;
RouteDefinitionWriter接口是由nacos提供的Bean,导入nacos的依赖就可以注入,作用是更新和删除路由。但不能批量
NacosConfigManager也是由nacos提供的Bean,作用是与nacos建立联系,可以实时获取nacos里的配置,并监听是否发生变化

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
@Slf4j
@Component
@RequiredArgsConstructor
public class RouterLoader {

private final RouteDefinitionWriter writer;
private final NacosConfigManager nacosConfigManager;

// 路由配置文件的id和分组
private final String dataId = "gateway-routes.json";
private final String group = "DEFAULT_GROUP";
// 保存更新过的路由id
private final Set<String> routeIds = new HashSet<>();

@PostConstruct
public void initRouteConfigListener() throws NacosException {
// 1.注册监听器并首次拉取配置
String configInfo = nacosConfigManager.getConfigService()
.getConfigAndSignListener(dataId, group, 5000, new Listener() {
@Override
public Executor getExecutor() {
return null;
}

@Override //路由变化则触发
public void receiveConfigInfo(String configInfo) {
updateConfigInfo(configInfo);
}
});
// 2.首次启动时,更新一次配置
updateConfigInfo(configInfo);
}

private void updateConfigInfo(String configInfo) {
log.debug("监听到路由配置变更,{}", configInfo);
// 1.反序列化
List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
// 2.更新前先清空旧路由
// 2.1.清除旧路由
for (String routeId : routeIds) {
writer.delete(Mono.just(routeId)).subscribe();
}
routeIds.clear();
// 2.2.判断是否有新的路由要更新
if (CollUtils.isEmpty(routeDefinitions)) {
// 无新路由配置,直接结束
return;
}
// 3.更新路由
routeDefinitions.forEach(routeDefinition -> {
// 3.1.更新路由
writer.save(Mono.just(routeDefinition)).subscribe();
// 3.2.记录路由id,方便将来删除
routeIds.add(routeDefinition.getId());
});
}
}

nacos添加路由

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
[
{
"id": "item",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}
}],
"filters": [],
"uri": "lb://item-service"
},
{
"id": "cart",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/carts/**"}
}],
"filters": [],
"uri": "lb://cart-service"
},
{
"id": "user",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/users/**", "_genkey_1":"/addresses/**"}
}],
"filters": [],
"uri": "lb://user-service"
},
{
"id": "trade",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/orders/**"}
}],
"filters": [],
"uri": "lb://trade-service"
},
{
"id": "pay",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/pay-orders/**"}
}],
"filters": [],
"uri": "lb://pay-service"
}
]

微服务的雪崩问题

微服务调用链路中的某个服务故障,引起整个链路中的所有的微服务都不可用。
简单来说就是蝴蝶效应,因为微服务之间是错用复杂的,如果一个微服务A故障,那么由于微服务B调用A,导致B也故障,微服务C调用B,结果导致C也故障了。以此类推就会可能发生微服务大面积雪崩

Sentinel

Sentinel 是一款开源的微服务限流器,可以帮助我们解决微服务的雪崩问题

  • 初识Sentinel
  1. 官网下载Jar,并运行
    端口号冲突,更改了一下端口号
    1
    java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
    运行成功即可访问
  2. 添加依赖和配置
    这些配置可以共享到nacos中以后更加方便
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!--sentinel-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>


    spring:
    cloud:
    sentinel:
    transport:
    dashboard: localhost:8090
  3. 访问8090端口

    sentinel 之线程隔离

    一个微服务A中可能会调用多个微服务 B,C,D等,但是如果B微服务宕机等,会导致A调用B的线程也出问题,会占用微服务A的资源。当异常线程组都多,会影响到微服务A调用其他微服务。
    所以sentinel会我们提供了线程隔离,例如给一个请求规定5个线程,当这个请求的线程达到5个,就不允许这个请求的线程再增加。这样就不会影响到其他线程

    sentinel设置线程隔离很简单,再请求的流控中设置并发线程数即可

    sentinel 之线程熔断

    查询商品的RT较高(模拟的500ms),从而导致查询购物车的RT也变的很长。这样不仅拖慢了购物车服务,消耗了购物车服务的更多资源,而且用户体验也很差。
    对于商品服务这种不太健康的接口,我们应该停止调用,直接走降级逻辑,避免影响到当前服务。也就是将商品查询接口熔断。当商品服务接口恢复正常后,再允许调用。这其实就是断路器的工作模式了。

Sentinel中的断路器不仅可以统计某个接口的慢请求比例,还可以统计异常请求比例。当这些比例超出阈值时,就会熔断该接口,即拦截访问该接口的一切请求,降级处理;当该接口恢复正常时,再放行对于该接口的请求。
断路器的工作状态切换有一个状态机来控制:

状态机包括三个状态:

  • closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
  • open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态持续一段时间后会进入half-open状态
  • half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
    • 请求成功:则切换到closed状态
    • 请求失败:则切换到open状态

      分布式事务

      首先我们以前在单体架构中实现事务的同成功同失败是需要在方法上添加@Transactional注解实现的;
      但是这个方法已经对微服务不适用了,我们先来看一个业务流程

      由于订单、购物车、商品分别在三个不同的微服务,而每个微服务都有自己独立的数据库,因此下单过程中就会跨多个数据库完成业务。而每个微服务都会执行自己的本地事务:
  • 交易服务:下单事务
  • 购物车服务:清理购物车事务
  • 库存服务:扣减库存事务
    这些分开的交易,购物车,库存 被称为分支事务,组合在一起被称为全局事务,分布式事务需要保证全局事务同成功同失败

    认识Seata

    解决分布式事务的思路:分布式事务的主要问题是分支事务都可以保证自己事务的同成功同失败,但是分支事务之间不知道对方是否成功或失败,
    所以就需要找到一个事务协调者,帮忙记录下每个分支事务的结果
  1. Seata的架构图
    在Seata的事务管理中有三个重要的角色:
  • TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
  • TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager) - 资源管理器:管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

    个人理解呢:TC既是事务协调者记录每个分支事务结果,当一个业务中所有分支事务结束后判断是否需要回滚。
        但是TC并不知道这个业务的最后一个分支事务是哪个,中间也由可能有其他业务的分支事务,所以TM出现了,
        TM定义了一个业务的开始和结束,当TM开始的时候,TC会接收信号然后记录分支事务的结果,当TM结束后,TC也会结束这个业务的事务
        RM即是每个分支事务结束后会通过RM向TC传入自己分支事务的状态和结果。
        个人觉得RM中还会带有TM的标识,TC根据标识可以分辨出这个事务是否属于TM,这样可以多个全局事务一起执行
    

    部署Seata

  1. 准备Seata的数据库
    具体sql语句可以查询Seata的官方文档有给出
  2. 在Docker上部署Seata
    • 准备好seata的配置文件application.yml上传到虚拟机中
      配置文件官网中也有,也可以之下在seata的镜像文件,然后修改
    • 启动容器
      1
      2
      3
      4
      5
      6
      7
      8
      9
       docker run --name seata \
      -p 8099:8099 \
      -p 7099:7099 \
      -e SEATA_IP=192.168.150.101 \
      -v ./seata:/seata-server/resources \
      --privileged=true \
      --network hm-net \
      -d \
      seataio/seata-server:1.5.2
      这里我出现了报错,docker ps -a 查看后发现状态码为139,
      查看日志发现报错是

      查询了半天后需要添加一项命令在run命令后
      1
      2
      3
      4
      5
      6
      7
      8
      9
        docker run --name seata \
      -p 8099:8099 \
      -p 7099:7099 \
      -e SEATA_IP=192.168.150.101 \
      -v ./seata:/seata-server/resources \
      --privileged=true \
      --network hm-net \
      -d --ulimit nofile=1024:1024 \
      seataio/seata-server:1.5.2
  3. 启动成功

    微服务整合seata

  4. 添加相关依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!--统一配置管理-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <!--读取bootstrap文件-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
    </dependency>
    <!--seata-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
  5. seata的配置添加到nacos上
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    seata:
    registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
    type: nacos # 注册中心类型 nacos
    nacos:
    server-addr: 192.168.217.128:8848 # nacos地址
    namespace: "" # namespace,默认为空
    group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
    application: seata-server # seata服务名称
    username: nacos
    password: nacos
    tx-service-group: hmall # 事务组名称
    service:
    vgroup-mapping: # 事务组与tc集群的映射关系
    hmall: "default"
  6. 在bootstrap.yaml文件中添加
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    spring:
    application:
    name: trade-service # 服务名称
    profiles:
    active: dev
    cloud:
    nacos:
    server-addr: 192.168.217.128 # nacos地址
    config:
    file-extension: yaml # 文件后缀名
    shared-configs: # 共享配置
    - dataId: shared-jdbc.yaml # 共享mybatis配置
    - dataId: shared-log.yaml # 共享日志配置
    - dataId: shared-swagger.yaml # 共享日志配置
    - dataId: shared-seata.yaml # 共享seata配置
    之后就可以启动服务啦,不过这里记录一下JDK21我启动遇见的问题

    解决方法JVM启动添加—add-opens=java.base/java.lang=ALL-UNNAMED

    百度了一下:—add-opens选项是Java 9引入的一个命令行选项,用于打开模块之间的包,以便其他模块可以访问这些包中的类和成员。通过使用—add-opens选项,我们可以解决由于模块的隔离性而导致的访问限制问题

Seata之XA模式

XA模式比较依赖关系型数据库,XA模式和上面提到的Seata架构的执行顺序基本相同,不过XA模式的特点是当分支事务执行完后并不会提交事务,而是当所有分支事务都执行完后,TC端如果记录的执行状态全部成功,则所有分支事务一起提交事务,如有有一个执行失败,则TC通知所有分支事务全部回滚。以这样的方式保证了数据的强一致性。但是也消耗性能。因为分支事务再执行完后并不会提交事务,所以分支事务会占着数据库该表的锁,mysql数据库的锁又是排他锁,所以如果这个分支事务不提交事务,就无法释放锁,其他业务也就没办法操作数据库

RM一阶段的工作:

  1. 注册分支事务到TC
  2. 执行分支业务sql但不提交
  3. 报告执行状态到TC

TC二阶段的工作:

  1. TC检测各分支事务执行状态
    1. 如果都成功,通知所有RM提交事务
    2. 如果有失败,通知所有RM回滚事务

RM二阶段的工作:

  • 接收TC指令,提交或回滚事务

XA模式的优点是什么?

  • 事务的强一致性,满足ACID原则
  • 常用数据库都支持,实现简单,并且没有代码侵入

XA模式的缺点是什么?

  • 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
  • 依赖关系型数据库实现事务

    XA的实现

  1. 在Seata的yaml文件中指定XA模式(可以在nacos中添加)
    1
    2
    seata:
    data-source-proxy-mode: XA
  2. 添加全局事务注解
    在需要的方法上添加@GlobalTransactional(rollbackFor = Exception.class)即可

Seata之AT模式

AT模式相比与XA模式提升了性能,在AT模式的架构图中可以看出,AT模式提升性能的主要原因是分支事务在执行完后会直接提交事务,而不是等待其他分支执行完。所以AT模式明显提升了性能。但是由于分支事务执行完后就会直接提交事务,如果当另一个分支事务执行失败时,已经提交完事务的分支事务就无法进行回滚,AT模式显然也考虑到这点,所以使用了快照的方法(其实就是备份)快照会记录分支事务执行前的数据,如果有分支事务失败了,那么就会通过快照进行数据还原,如果所有事务都成功了,则会清楚快照

阶段一RM的工作:

  • 注册分支事务
  • 记录undo-log(数据快照)
  • 执行业务sql并提交
  • 报告事务状态
    阶段二提交时RM的工作:
  • 删除undo-log即可
    阶段二回滚时RM的工作:
  • 根据undo-log恢复数据到更新前
    流程图:

    注意!!AT模式会有短暂的数据不一致的情况

AT的实现

  1. 由于需要快照,所以每一个微服务都需要一张单独的表存储快照
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    CREATE TABLE IF NOT EXISTS `undo_log`
    (
    `branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
    `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
    `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
    `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
    ) ENGINE = InnoDB
    AUTO_INCREMENT = 1
    DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
  2. 在Seata的yaml文件中指定XA模式(可以在nacos中添加)
    1
    2
    seata:
    data-source-proxy-mode: AT

    @RequiredArgsConstructor注解

    该注解可以是Lombok所提供的,其主要的作用是简化@Autowired 的书写过程。在编写 Controller 层或 Service 层代码时,常常需要注入众多的 mapper 接口或 service 接口。若每个接口都使用 @Autowired 进行标注,代码会显得繁琐。而 @RequiredArgsConstructor 注解能够替代 @Autowired 注解,但需注意,在类上添加 @RequiredArgsConstructor 时,需要注入的类必须使用 final 进行声明。

其底层原理是为final的字段生成构造参数