2.4、OpenFeign
请求需要的controller层代码实现跨项目的数据联调
OpenFeign是一个声明式的http客户端,是SpringCloud在Eureka公司开源的Feign基础上改造而来。官方地址:
https:/lgithub.com/OpenFeign/feign
其作用就是基于SpringMVC的常见注解,帮我们优雅的实现http请求的发送。
引入依赖
<!--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>
随后在Application启动类上加上注释@EnableFeignClients
定义接口
import java.util.List;
@FeignClient("item-service")
public interface itemClient {
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") List<Long> ids);
}
直接在serviceImpl实现类直接引用即可
连接池
OpenFeign对Http请求做了优雅的伪装,不过其底层发起http请求,依赖于其它的框架。这些框架可以自己选择,包括以下三种:
HttpURLConnection
:默认实现,不支持连接池Apache Httpclient
:支持连接池OKHttp
:支持连接池
具体源码可以参考FeignBlockingLoadBalancerClient类中的delegate成员变量。
连接池依赖
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
在yaml文件中配置
feign:
okhttp:
enabled: true # 开启OKHttp功能
最佳实践
- 思路1:抽取到微服务之外的公共module
当我们写项目的时候,每个模块可能都要相互调用互相的实体类,那么这个时候我们就应该把他建立在一个公共的模块类暴露给别人使用
- 思路2:每个微服务自己抽取一个module
方案1抽取更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。
方案2抽取相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。
当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。有两种方式解决:
-
方式1:声明扫描包:
-
方式2:声明要用的FeignClient:
日志
OpenFeign只会在FeignClient所在包的日志级别为DEBUG
时,才会输出日志。而且其日志级别有4级:
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
由于Feign默认的日志级别就是NONE,所以默认我们看不到请求日志。
要自定义日志级别需要声明一个类型为Logger.Level的Bean,在其中定义日志级别:
package com.hmall.api.config;
import feign.Logger;
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
然后在需要的Application
上加上全局,就可以在debug的时候看到报错信息了。
2.5、网关
网关简介
什么是网关?
顾明思议,网关就是网络的关口。数据在网络间传输,从一个网络传输到另一网络时就需要经过网关来做数据的路由和转发以及数据安全的校验。
现在,微服务网关就起到同样的作用。前端请求不能直接访问微服务,而是要请求网关:
- 网关可以做安全控制,也就是登录身份校验,校验通过才放行
- 通过认证后,网关再根据请求判断应该访问哪个微服务,将请求转发过去
SpringCloudGateway
基于Spring的WebFlux技术,完全支持响应式编程,吞吐能力更强
快速入门
- 创建项目
- 引入依赖
<dependencies>
<!--common-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-common</artifactId>
<version>1.0.0</version>
</dependency>
<!--网关-->
<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>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- 创建启动项
package com.hmall.geteway;
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);
}
}
- application.yaml(路由过滤)
server:
port: 8080 #服务端口
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.150.101:8848
gateway:
routes:
- id: item # 路由规则id,自定义,唯一 建议看你对应的服务名的application中设置的名字
uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/items/**,/search/** # 这里是以请求路径作为判断规则 如果有多个就可以参考search这样写
- id: cart
uri: lb://cart-service
predicates:
- Path=/carts/**
- id: user
uri: lb://user-service
predicates:
- Path=/users/**,/addresses/**
- id: trade
uri: lb://trade-service
predicates:
- Path=/orders/**
- id: pay
uri: lb://pay-service
predicates:
- Path=/pay-orders/**
启动网关服务即可,我们就可以不再访问商品服务的8081端口,而是通过8080端口由网关自动转发到需要的端口上
路由属性
- predicates(路由断言)
名称 | 说明 | 示例 |
---|---|---|
After | 是某个时间点后的请求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before | 是某个时间点之前的请求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between | 是某两个时间点之前的请求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie | 请求必须包含某些cookie | - Cookie=chocolate, ch.p |
Header | 请求必须包含某些header | - Header=X-Request-Id, \d+ |
Host | 请求必须是访问某个host(域名) | - Host=.somehost.org,.anotherhost.org |
Method | 请求方式必须是指定方式 | - Method=GET,POST |
Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/blue/** |
Query | 请求参数必须包含指定参数 | - Query=name, Jack或者- Query=name |
RemoteAddr | 请求者的ip必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
weight | 权重处理 |
- 路由过滤器
俩种写法:
全局过滤
局部过滤
这里我们可以看一下添加的请求头是否生效,
网关登录校验
单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。而微服务拆分后,每个微服务都独立部署,不再共享数据。也就意味着每个微服务都需要做登录校验,这显然不可取。
鉴权思路分析
我们的登录是基于JWT来实现的,校验JWT的算法复杂,而且需要用到秘钥。如果每个微服务都去做登录校验,这就存在着两大问题:
- 每个微服务都需要知道JWT的秘钥,不安全
- 每个微服务重复编写登录校验代码、权限校验代码,麻烦
既然网关是所有微服务的入口,一切请求都需要先经过网关。我们完全可以把登录校验的工作放到网关去做,这样之前说的问题就解决了:
- 只需要在网关和用户服务保存秘钥
- 只需要在网关开发登录校验功能
此时,登录校验的流程如图:
网关过滤器
- 网关请求处理流程
如图所示:
- 客户端请求进入网关后由
HandlerMapping
对请求做判断,找到与当前请求匹配的路由规则(Route
),然后将请求交给WebHandler
去处理。 WebHandler
则会加载当前路由下需要执行的过滤器链(Filter chain
),然后按照顺序逐一执行过滤器(后面称为**Filter
**)。- 图中
Filter
被虚线分为左右两部分,是因为Filter
内部的逻辑分为pre
和post
两部分,分别会在请求路由到微服务之前和之后被执行。 - 只有所有
Filter
的pre
逻辑都依次顺序执行通过后,请求才会被路由到微服务。 - 微服务返回结果后,再倒序执行
Filter
的post
逻辑。 - 最终把响应结果返回。
网关过滤器链中的过滤器有两种:
GatewayFilter
:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route
.GlobalFilter
:全局过滤器,作用范围是所有路由,不可配置。
GlobalFilter
package com.hmall.geteway.fillter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取请求
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
//业务处理
System.out.println("headers = " + headers);
//放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
// 过滤器执行顺序,值越小,优先级越高
return 0;
}
}
网关传递用户
现在,网关已经可以完成登录校验并获取登录用户身份信息。但是当网关将请求转发到微服务时,微服务又该如何获取用户身份呢?
由于网关发送请求到微服务依然采用的是Http
请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。考虑到微服务内部可能很多地方都需要用到登录用户信息,因此我们可以利用SpringMVC的拦截器来实现登录用户信息获取,并存入ThreadLocal,方便后续使用。
- 修改网关的传递,按照前后端的约定,将userid放在请求头中
- 然后为了让每个服务都能拿到,我们把拦截器写到公共模块中,方便获取
package com.hmall.common.interceptors;
import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取登录信息
String header = request.getHeader("user-info");
//2.是否获取了用户 有就存入Threadlocal中
if (StrUtil.isNotBlank(header)){
UserContext.setUser(Long.valueOf(header));
}
//3.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清理用户
UserContext.removeUser();
}
}
- 设置拦截器的配置项,由于我们这个拦截器只用于传值,没有拦截就默认全部拦截就可以,不必单独设置拦截路径
但是这里会出现一个问题,就是它会被微服务引用又会被网关引用,而网关用的不是springmvc这一套 而是响应式编程,那么我们这里就利用条件注解把springmvc当成条件即可
@ConditionalOnClass(DispatcherServlet.class)
package com.hmall.common.config;
import com.hmall.common.interceptors.UserInfoInterceptor;
import org.apache.catalina.User;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
- 这里我们也要保证包被扫描到,所以这里我们还需要修改
OpenFeign传递用户
微服务项目中的很多业务要多个微服务共同合作完成,而这个过程中也需要传递登录用户信息,例如:
由于FeignClient
全部都是在hm-api
模块,因此我们在hm-api
模块的com.hmall.api.config.DefaultFeignConfig
中编写这个拦截器
在com.hmall.api.config.DefaultFeignConfig
中添加一个Bean:
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 获取登录用户
Long userId = UserContext.getUser();
if(userId == null) {
// 如果为空则直接跳过
return;
}
// 如果不为空则放入请求头中,传递给下游微服务
template.header("user-info", userId.toString());
}
};
}
然后我们就可以在我们需要的地方,从请求头中拿id即可
2.6、服务保护
雪崩问题
微服务调用链路中的某个服务故障,引起整个链路中的所有微服务都不可用,这就是雪崩。
服务保护方案
请求限流
服务故障最重要原因,就是并发太高!解决了这个问题,就能避免大部分故障。当然,接口的并发不是一直很高,而是突发的。因此请求限流,就是限制或控制接口访问的并发流量,避免服务因流量激增而出现故障。
线程隔离
线程隔离
∶也叫做舱壁模式,模拟船舱隔板的防水原理。通过限定每个业务能使用的线程数量而将故障业务隔离,避免故障扩散。
服务熔断
服务熔断
∶由断路器统计请求的异常比例或慢调用比例,如果超出阈值则会熔断该业务,则拦截该接口的请求。
线程隔离虽然避免了雪崩问题,但故障服务(商品服务)依然会拖慢购物车服务(服务调用方)的接口响应速度。而且商品查询的故障依然会导致查询购物车功能出现故障,购物车业务也变的不可用了。
所以,我们要做两件事情:
- 编写服务降级逻辑:就是服务调用失败后的处理逻辑,根据业务场景,可以抛出异常,也可以返回友好提示或默认数据。
- 异常统计和熔断:统计服务提供方的异常比例,当比例过高表明该接口会影响到其它服务,应该拒绝调用该接口,而是直接走降级逻辑。
服务保护技术
Sentinel
Sentinel是阿里巴巴开源的一款服务保护框架,目前已经加入SpringCloudAlibaba中。官方网站:https://sentinelguard.io/zh-cn/
Sentinel 的使用可以分为两个部分:
- 核心库(Jar包):不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。在项目中引入依赖即可实现服务限流、隔离、熔断等功能。
- 控制台(Dashboard):Dashboard 主要负责管理推送规则、监控、管理机器信息等。
为了方便监控微服务,我们先把Sentinel的控制台搭建出来。
- 下载jar包
下载地址:https://github.com/alibaba/Sentinel/releases
运行(注意最后加上版本号你们自己的)
java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
访问http://localhost:8090页面,就可以看到sentinel的控制台了:
账号密码均为sentine
微服务整合
引入sentinel依赖
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
配置控制台
修改application.yaml文件,添加下面内容:
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090
然后重启项目,调用一个服务接口,再看sentinel控制台
点击簇点链路菜单,会看到下面的页面:
所谓簇点链路
,就是单机调用链路,是一次请求进入服务后经过的每一个被Sentinel
监控的资源。默认情况下,Sentinel
会监控SpringMVC
的每一个Endpoint
(http接口)。
因此,我们看到/carts
这个接口路径就是其中一个簇点,我们可以对其进行限流、熔断、隔离等保护措施。
默认情况下Sentinel会把路径作为簇点资源的名称,无法区分路径相同但请求方式不同的接口,查询、删除、修改等都被识别为一个簇点资源,这显然是不合适的。
所以我们可以选择打开Sentinel的请求方式前缀,把请求方式 + 请求路径
作为簇点资源名:
首先,application.yml
中添加下面的配置:
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090
http-method-specify: true # 开启请求方式前缀
然后,重启服务,通过页面访问购物车的相关接口,可以看到sentinel控制台的簇点链路发生了变化:
请求限流
在簇点链路后面点击流控按钮,即可对其做限流配置:
这里就是最多每次6次,然后我们用jmeter进行一次压力测试
线程隔离
限流可以降低服务器压力,尽量减少因并发流量引起的服务故障的概率,但并不能完全避免服务故障。一旦某个服务出现故障,我们必须隔离对这个服务的调用,避免发生雪崩。
OpenFeign整合Sentine
修改application.yml文件,开启Feign的sentinel功能:
feign:
sentinel:
enabled: true # 开启feign对sentinel的支持
需要注意的是,默认情况下SpringBoot项目的tomcat最大线程数是200,允许的最大连接是8492,单机测试很难打满。
所以我们需要配置一下cart-service模块的application.yml文件,修改tomcat连接:
server:
port: 8082
tomcat:
threads:
max: 50 # 允许的最大线程数
accept-count: 50 # 最大排队等待数量
max-connections: 100 # 允许的最大连接
然后重启cart-service服务,可以看到查询商品的FeignClient自动变成了一个簇点资源:
配置线程隔离
接下来,点击查询商品的FeignClient对应的簇点资源后面的流控按钮:
同样我们基于jmeter进行一次压力测试
服务熔断
编写降级逻辑
触发限流或熔断后的请求不一定要直接报错,也可以返回一些默认数据或者友好提示,用户体验会更好。
给FeignClient编写失败后的降级逻辑有两种方式:
- 方式一:FallbackClass,无法对远程调用的异常做处理
- 方式二:FallbackFactory,可以对远程调用的异常做处理,我们一般选择这种方式。
这里我们演示方式二的失败降级处理
步骤一:在hm-api模块中给ItemClient
定义降级处理类,实现FallbackFactory
:
package com.hmall.api.fallback;
import com.hmall.api.client.ItemClient;
import com.hmall.api.dto.ItemDTO;
import com.hmall.api.dto.OrderDetailDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@Slf4j
public class ItemClientFallBackFactory implements FallbackFactory<ItemClient> {
@Override
public ItemClient create(Throwable cause) {
return new ItemClient() {
//查询商品业务
@Override
public List<ItemDTO> queryItemByIds(Collection<Long> ids) {
//查询出现异常,返回空集合
log.error("查询商品异常,异常原因:",cause);
return Collections.emptyList();
}
//扣库存业务
@Override
public void deductStock(List<OrderDetailDTO> items) {
throw new RuntimeException(cause);
}
};
}
}
步骤二:在hm-api
模块中的com.hmall.api.config.DefaultFeignConfig
类中将ItemClientFallback
注册为一个Bean
:
@Bean
public ItemClientFallBackFactory itemClientFallBackFactory(){
return new ItemClientFallBackFactory();
}
步骤三:在hm-api
模块中的ItemClient
接口中使用ItemClientFallbackFactory
:
重启项目,然后再次进行压力测试
熔断降级是解决雪崩问题的重要手段。思路是由断路器
统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切 请求;而当服务恢复时,断路器会放行访问该服务的请求。
- 配置熔断规则
持久化存储
通过控制台设置规则后将规则推送到统一的规则中心,客户端实现 ReadableDataSource
接口端监听规则中心实时获取变更,流程如下:
这里利用的nacos
- 添加配置文件(这里版本与sentinel保持一致)
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>x.y.z</version>
</dependency>
- 在nacos上添加一个.json文件
[
{
"resource": "GET:http://item-service/items",
"count": 200.0,
"grade": 0,
"slowRationThresshold": 0.5,
"timeWindow": 10
}
]
在yaml文件中配置
2.7、分布式事务
在分布式系统中,如果一个业务需要多个服务合作完成,而且每一个服务都有事务,多个事务必须同时成功或失败,这样的事务就是分布式事务。其中的每个服务的事务就是一个分支事务。整个业务称为全局事务。
Seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
https://seata.apache.org/zh-cn/docs/overview/what-is-seata/
Seata也不例外,在Seata的事务管理中有三个重要的角色:
- TC (Transaction Coordinator) - **事务协调者:**维护全局和分支事务的状态,协调全局事务提交或回滚。
- TM (Transaction Manager) - **事务管理器:**定义全局事务的范围、开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - **资源管理器:**管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
部署TC服务
Seata支持多种存储模式,但考虑到持久化的需要,我们一般选择基于数据库存储。
global_table
存储全局事务信息,包括事务的唯一标识xid
、事务状态status
、应用程序 IDapplication_id
等。branch_table
存储分支事务信息,包括分支事务的唯一标识branch_id
、关联的全局事务 IDxid
、资源组 IDresource_group_id
等。lock_table
存储分布式锁信息,用于控制事务的并发访问,包括锁的唯一标识row_key
、关联的全局事务 IDxid
、分支事务 IDbranch_id
等。distributed_lock
存储分布式锁的键值对信息,包括锁的键lock_key
、锁的值lock_value
和过期时间expire
。
准备好Seate文件,修改配置文件(配置文件中都是基于容器名访问,要修改可以修改,同时分布式锁如果是基于redis部署,需要修改host)
server:
port: 7099
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: ${user.home}/logs/seata
# extend:
# logstash-appender:
# destination: 127.0.0.1:4560
# kafka-appender:
# bootstrap-servers: 127.0.0.1:9092
# topic: logback_to_logstash
console:
user:
username: admin
password: admin
seata:
config:
# support: nacos, consul, apollo, zk, etcd3
type: nacos
nacos:
server-addr: nacos:8848
group : "DEFAULT_GROUP"
namespace: ""
dataId: "seataServer.properties"
username: "nacos"
password: "nacos"
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa namespace命名空间不填为默认
type: nacos
nacos:
application: seata-server
server-addr: nacos:8848
group : "DEFAULT_GROUP"
namespace: ""
username: "nacos"
password: "nacos"
# server:
# service-port: 8091 #If not configured, the default is '${server.port} + 1000'
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
server:
# service-port: 8091 #If not configured, the default is '${server.port} + 1000'
max-commit-retry-timeout: -1
max-rollback-retry-timeout: -1
rollback-retry-timeout-unlock-enable: false
enable-check-auth: true
enable-parallel-request-handle: true
retry-dead-threshold: 130000
xaer-nota-retry-timeout: 60000
enableParallelRequestHandle: true
recovery:
committing-retry-period: 1000
async-committing-retry-period: 1000
rollbacking-retry-period: 1000
timeout-retry-period: 1000
undo:
log-save-days: 7
log-delete-period: 86400000
session:
branch-async-queue-size: 5000 #branch async remove queue size
enable-branch-async-remove: false #enable to asynchronous remove branchSession
store:
# support: file 、 db 、 redis
mode: db
session:
mode: db
lock:
mode: db
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://mysql:3306/seata?rewriteBatchedStatements=true
user: root
password: 123
min-conn: 10
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
query-limit: 1000
max-wait: 5000
redis:
mode: single
database: 0
min-conn: 10
max-conn: 100
password:
max-total: 100
query-limit: 1000
single:
host: 192.168.150.101
port: 6379
metrics:
enabled: false
registry-type: compact
exporter-list: prometheus
exporter-prometheus-port: 9898
transport:
rpc-tc-request-timeout: 15000
enable-tc-server-batch-send-response: false
shutdown:
wait: 3
thread-factory:
boss-thread-prefix: NettyBoss
worker-thread-prefix: NettyServerNIOWorker
boss-thread-size: 1
将文件上传到服务器/root下
- docker部署
需要注意,要确保nacos、mysql都在hm-net网络中
。如果某个容器不再hm-net网络,可以参考下面的命令将某容器加入指定网络:
docker network connect [网络名] [容器名]
然后把seata镜像上传服务器
docker加载文件
docker load -i seata.tar
在虚拟机的/root
目录执行下面的命令:
ip改成自己的
docker run --name seata \
-p 8099:8099 \
-p 7099:7099 \
-e SEATA_IP= \
-v ./seata:/seata-server/resources \
--privileged=true \
--network hm-net \
-d \
seataio/seata-server:1.5.2
这里一定要特别注意网段是否在一个网段中,最后启动服务看nacos控制台
微服务集成Seata
- 添加依赖
为了方便各个微服务集成seata,我们需要把seata配置共享到nacos,因此trade-service
模块不仅仅要引入seata依赖,还要引入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>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
- 共享seata配置
首先在nacos上添加一个共享的seata配置,命名为shared-seata.yaml
:
seata:
registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
type: nacos # 注册中心类型 nacos
nacos:
server-addr: 192.168.150.101: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"
然后在bootstrap.yaml中加载配置
重启服务,我们可以在服务器seata日志中看到注册信息
测试
我们找到trade-service
模块下的com.hmall.trade.service.impl.OrderServiceImpl
类中的createOrder
方法,也就是下单业务方法。
将其上的@Transactional
注解改为Seata提供的@GlobalTransactional
@GlobalTransactional
注解就是在标记事务的起点,将来TM就会基于这个方法判断全局事务范围,初始化全局事务。
那么,Seata是如何解决分布式事务的呢?
两阶段提交
- 正常情况
- 异常情况
一阶段:
- 事务协调者通知每个事务参与者执行本地事务
- 本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁
二阶段:
- 事务协调者基于一阶段的报告来判断下一步操作
- 如果一阶段都成功,则通知所有事务参与者,提交事务
- 如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务
XA模式
Seata支持四种不同的分布式事务解决方案:
- XA
- TCC
- AT
- SAGA
XA
规范 是 X/Open
组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM
与局部的RM
之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。
Seata的XA模型
Seata对原始的XA模式做了简单的封装和改造,以适应自己的事务模型,基本架构如图:
RM
一阶段的工作:
- 注册分支事务到
TC
- 执行分支业务sql但不提交
- 报告执行状态到
TC
TC
二阶段的工作:
TC
检测各分支事务执行状态- 如果都成功,通知所有RM提交事务
- 如果有失败,通知所有RM回滚事务
RM
二阶段的工作:
- 接收
TC
指令,提交或回滚事务
优缺点
XA
模式的优点是什么?
- 事务的强一致性,满足ACID原则
- 常用数据库都支持,实现简单,并且没有代码侵入
XA
模式的缺点是什么?
- 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
- 依赖关系型数据库实现事务
实现步骤
首先,我们要在配置文件中指定要采用的分布式事务模式。我们可以在Nacos中的共享shared-seata.yaml配置文件中设置:
seata:
data-source-proxy-mode: XA
其次,我们要利用@GlobalTransactional
标记分布式事务的入口方法:
AT模式
AT
模式同样是分阶段提交的事务模型,不过缺弥补了XA
模型中资源锁定周期过长的缺陷。
AT模型
最简单的理解就是用空间换时间,利用快照进行恢复数据
阶段一RM
的工作:
- 注册分支事务
- 记录undo-log(数据快照)
- 执行业务sql并提交
- 报告事务状态
阶段二提交时RM
的工作:
- 删除undo-log即可
阶段二回滚时RM
的工作:
- 根据undo-log恢复数据到更新前
模拟实现
我们用一个真实的业务来梳理下AT模式的原理。
比如,现在有一个数据库表,记录用户余额:
id | money |
---|---|
1 | 100 |
其中一个分支业务要执行的SQL为:
update tb_account set money = money - 10 where id = 1
AT模式下,当前分支事务执行流程如下:
一阶段:
TM
发起并注册全局事务到TC
TM
调用分支事务- 分支事务准备执行业务SQL
RM
拦截业务SQL,根据where条件查询原始数据,形成快照。
{
"id": 1, "money": 100
}
RM
执行业务SQL,提交本地事务,释放数据库锁。此时 money = 90RM
报告本地事务状态给TC
二阶段:
TM
通知TC
事务结束TC
检查分支事务状态- 如果都成功,则立即删除快照
- 如果有分支事务失败,需要回滚。读取快照数据({“id”: 1, “money”: 100}),将快照恢复到数据库。此时数据库再次恢复为100
注意真实场景下每个数据库中的表都需要对照建立一个快照表,用于存储
AT与XA的区别
简述AT
模式与XA
模式最大的区别是什么?
XA
模式一阶段不提交事务,锁定资源;AT
模式一阶段直接提交,不锁定资源。XA
模式依赖数据库机制实现回滚;AT
模式利用数据快照实现数据回滚。XA
模式强一致;AT
模式最终一致
可见,AT模式使用起来更加简单,无业务侵入,性能更好。因此企业90%的分布式事务都可以用AT模式来解决。