Spring-Cloud-Alibaba
Nacos(服务注册和配置中心)
一、安装
官网:https://nacos.io/zh-cn/
刚下完是集群模式,所以直接启动会显示"nacos is starting with cluster"
可以在bin/startup.cmd文件中修改:
set MODE="standalone"
修改完成后即可启动成功,在浏览器输入:
http://localhost:8848/nacos
即可进入nacos管理界面:
二、作为服务注册中心
以下配置均可在官网文档找到
1.创建两个module(端口:4001,4002)作为provider集群
-
pom
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
-
yml
# 基本配置 server: port: 4001 # 集群时,注意端口号不一致 spring: application: name: payment-alibaba-provider cloud: nacos: discovery: server-addr: localhost:8848 management: endpoints: web: exposure: include: "*"
-
主启动类:添加注解:
@EnableDiscoveryClient
-
controller
@RestController public class PaymentController { @Value("${spring.application.name}") private String name; @Value("${server.port}") private String port; // 返回服务名称和当前实例的端口号,便于测试负载均衡 @GetMapping("/get/nacos/{id}") public String getNacos(@PathVariable(value = "id")int id){ return "get id from nacos: " + name + "----port:" + port; } }
完成后即可在管理界面看到(注意需要先启动nacos,在启动微服务):
2.创建一个module(端口:81)作为consumer
-
pom,同上
-
yml,同上(注意微服务名称和端口)
-
主启动类,同上
-
controller
- 支持Restful,所以需要注入RestTemplate(代码略);因为nacos包含了ribbon,所以支持负载均衡,使用时与ribbon一样添加``@LoadBalanced`注解即可
@RestController public class ConsumerController { // 此对象为官网使用方法,下面注释的方式配合该对象,可以不用添加@LoadBalanced注解也能开启负载均衡 // @Autowired // private LoadBalancerClient loadBalancerClient; @Autowired private RestTemplate restTemplate; @Value("${provider.service-url}") private String providerName; // @GetMapping("consumer/get/nacos/{id}") // public String getNacos(@PathVariable(value = "id")int id){ // ServiceInstance serviceInstance = loadBalancerClient.choose(providerName); // return restTemplate.getForObject("http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + // " /get/nacos/" + id , String.class); // } @GetMapping("consumer/get/nacos/{id}") public String getNacos(@PathVariable(value = "id")int id){ return restTemplate.getForObject("http://"+providerName+"/get/nacos/"+id,String.class); } }
Nacos支持AP和CP的切换,Eureka只支持AP
三、作为配置中心
1.快速入门
-
新建module(端口2001)
-
pom
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
-
yml(application、bootstrap), springCloud也是这样
bootstrap:
server: port: 2001 spring: application: name: config-center-alibaba cloud: nacos: discovery: server-addr: localhost:8848 # 服务注册 config: server-addr: localhost:8848 # nacos作为配置中地址 file-extension: yaml # 指定配置文件类型
application:
spring: profiles: active: dev # 环境,与bootstrap中的配置可以结合起来
-
主启动类,注意添加EableDiscoverClient注解
-
nacos配置中心创建配置文件:
Data ID需要注意规范:
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
即:微服务名称-环境.配置文件类型====》对应案例:config-center-alibaba-dev.yaml
-
测试:
@RestController @RefreshScope public class ConfigController { @Value("${config.info}") private String info; @GetMapping("/get/config") public String getInfo(){ return info; } }
访问:http://localhost:2001/get/info 即可得到配置信息
Nacos具有自动的动态更新配置的功能,在controller上添加
@RefreshScope
注解即可,不需要和springcloud一样通过bus发送广播更新
tips:docker 安装nacos
-
安装docker(不作解释)
-
docker search nacos
-
docker pull nacos/nacos-server # 默认最新版
-
docker images # 查看是否安装成功
-
docker run -d -p 8848:8848 --env MODE=standalone --name nacos nacos/nacos-server
-
http://IP+8848/nacos 登录
-
注意修改微服务中的配置文件的ip
2.分类配置
NameSpace、Group、Data Id
相当于java中包和类的关系
新建命名空间和配置文件:
微服务服务配置:
server:
port: 2001
spring:
application:
name: config-center-alibaba
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 服务注册
config:
server-addr: localhost:8848 # nacos作为配置中地址
file-extension: yaml # 指定配置文件类型
group: DEFAULT_GROUP # 指定分组
namespace: ca6f1c91-fb7e-4a46-bf91-7f210bcef489 # 指定命名空间
四、Nacos集群和持久化
1.准备:
Nacos集群需要使用mysql:因为Nacos自带有小型的嵌入式数据库,所以在在集群的时候每个nacos都会有自己的数据库,这样就会导致数据的不一致,所以要切换到mysql将其统一持久到一个mysql数据库中。(目前只支持mysql)
在windows中配置mysql为数据源:
在ncaos中的conf文件夹下有:
nacos-mysql.sql文件:里面是数据库建表的语句,直接cv创建即可(数据库名称自定义)
application.properties文件修改如下(只需要将注解打开即可,注意修改数据库名称和密码,docker环境下就不需要修改这个文件了
):
spring.datasource.platform=mysql
### Count of DB:
db.num=1
### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=root
配置完成后,启动naco并登录管理界面,就会看到原来的配置文件不见了,再新建的配置文件信息可以再MySQL的自建的数据库的config_info表中看到,即配置成功。
在mysql版本过高可能会出现建表失败(时间的错误)
2.naocs集群配置(这里使用docker)
在上面已经下载nacos的docker版,只需在linux中配置mysql即可(如上,这里也是使用docker的mysql)。
创建容器命令(需要三份分别为:8846、8847、8848):
docker run -d \
-e PREFER_HOST_MODE=hostname \
-e MODE=cluster \
-e NACOS_APPLICATION_PORT=8847 \
-e NACOS_SERVERS="192.168.1.118:8846 192.168.1.118:8847 192.168.1.118:8848" \
-e SPRING_DATASOURCE_PLATFORM=mysql \
-e MYSQL_SERVICE_HOST=182.168.1.118 \
-e MYSQL_SERVICE_PORT=3307 \
-e MYSQL_SERVICE_USER=root \
-e MYSQL_SERVICE_PASSWORD=123456 \
-e MYSQL_SERVICE_DB_NAME=nacos_config \
-e NACOS_SERVER_IP=192.168.1.118 \
-p 8847:8847 \
--name my-nacos8847 \
nacos/nacos-server:1.4.2
简单解释:
- 配置模式为:集群
- nacos服务端口:8846
- 集群的IP:3个
- 使用数据库为mysql,
在这里配置后,不需要修改application.properties文件了,将会自动绑定数据库
- 数据库信息
- 宿主机端口映射
**此时可以作个小测试:**
浏览器分别访问这三个nacos,在其中一个里面添加配置文件,刷新其他两个nacos管理界面,会出现同样的配置文件,即完成集群,且配置文件已经持久到mysql数据库中,可在数据库中查看。
注意:在登录管理界面时,可能出现默认账户密码错误,可以通过查看数据库中user表中是否有数据,大概率时没有导致的,只需要添加即可(密码是加密后的密码)
3.nginx配置(使用docker)
这里没有开启nginx的高可用
创建nginx容器:
docker run \
--name=nginx-MASTER \
-d -p 8000:80 \
-v $PWD/nginx.conf:/etc/nginx/nginx.conf:ro \
-v $PWD/conf.d:/etc/nginx/conf.d \
nginx
修改配置文件:
upstream cluster{ # nacos集群列表
server 192.168.1.118:8846;
server 192.168.1.118:8847;
server 192.168.1.118:8848;
}
# server下
listen 80; # nginx访问端口,docker映射为81:80
location / {
proxy_pass http://cluster;
}
如上配置启动nginx,nacos;
访问:
http://192.168.1.118:81/nacos/#/lodin/
即可到nacos登录界面,且已经集群;在实际开发中,只能暴露nginx的虚拟ip,而nacos的IP都是不公开的。
遇到的坑:完成上述集群后,最后一步,需要注册微服务。但是始终无法注册进入nacos。调用服务注册api:
/nacos/v1/ns/instance
总会出现:server is DOWN now, please try again later!等字样,在java控制台中报错500或503.
查阅资料、官方文档、github上的issues,尝试过以下方法:
- 修改docker中的nacos版本:latest、2.0.0、1.4.1
- 设置linux的hostname即IP
- 删除nacos里面的data中的protocol文件夹
- 修改alibaba的依赖版本等
最终使用1.4.1版本使用/nacos/v1/ns/instance访问成功,并注册成功。具体原因还是没找到。个人认为是版本问题。在github上,官方说是由于(1)前置Filter拦截。(2)旧的jRaft的data数据会干扰本次jRaft的选举。总之还是使用RELEASE版本比较好
官网问题描述:https://github.com/alibaba/nacos/issues/5346
Sentinel(熔断与限流)
sentinal官网:
https://sentinelguard.io/zh-cn/
https://github.com/alibaba/Sentinel
功能:
下载:https://github.com/alibaba/Sentinel/releases/tag/1.8.1
下载的是一个jar包,所以世直接使用java -jar xxxx 直接运行即可,在浏览器访问http://localhost:8080可以看到dashboard(用户名密码默认为sentinel)
一、入门:对服务的监控
-
新建module
-
添加pom
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
-
配置yml
server: port: 4003 spring: application: name: alibaba-sentinel-service cloud: nacos: discovery: server-addr: localhost:8848 # sentinel监控该服务 sentinel: transport: dashboard: localhost:8080 port: 8719 management: endpoints: web: exposure: include: "*"
-
创建主启动类
-
创建controller(随意创建mapping)
-
测试:在nacos中注册成功;sentinel中没有:因为sentinel使用的是懒加载,当进行一次访问该服务时就会加载
二、流控规则
流控规则添加:
解释:
-
资源名:微服务中的Rest地址
-
针对来源:
-
阈值类型:QPS:访问的数量;线程数:服务端的线程数量
-
单机阈值:QPS的请求阈值或线程数的数量
-
是否集群:使用集群
-
流控模式:
-
直接:达到限流条件时,直接限流自己
-
关联:A关联B,B达到设定阈值后,限流A自己
如下:test1关联test2,当test2的请求QPS大于2时,test1就会被限流
-
链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就可以限流)
-
-
流控效果
- 快速关闭:超过阈值直接返回错误
- Warm up:预热,一个系统平时访问量很低,突然之间访问量暴增,超过阈值会使系统崩溃;使用预热留空会在设定时间内,将阈值提到上限;正常情况的阈值为:阈值上限/3 (3为预热因子,默认为3)
- 排队等待:匀速排队(
RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER
)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
三、降级规则
1.慢调用比例策略
慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例
大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
如图:对于/get/test3这个请求资源,满足如下规则触发熔断:
- 在1000ms的时间内,最少需要有5次请求
- 在1000ms内,超出RT的请求(即慢调用请求)的比例为1(100%)
就会触发2s的熔断。
**测试:**使用JMETER测试,给定每秒钟10个请求,controller中的处理时间为1s(超过RT,所以阈值为100%),此时使用浏览器访问该资源,会出现错误 Blocked by Sentinel (flow limiting)
@GetMapping("/get/test3")
public String test3(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "-----test3----";
}
2.异常比例
异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例
大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
如图,对于test4资源:
设置的比例阈值为0.5(50%),即:
- 当1s内,最少有5次请求
- 且,异常的比例为50%以上
则触发熔断。这里需要注意:这里的异常
指的是业务异常
,对 Sentinel 限流降级本身的异常(BlockException
)不生效
@GetMapping("/get/test4")
public String test4(){
int a=1/0; // 错误率100%
return "-----test4----";
}
3.异常数
异常数 (ERROR_COUNT):当单位统计时长内的异常数目
超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
异常数是按分钟为单位计数
如图:当窗口其内请求异常数超过10个就会触发熔断。
四、热点规则
**官网解释:**何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。
1.初步使用
业务代码:
这里使用到了新的标签@SentinelResource
,该注解只适用于sentinel配置的异常,如服务限流、熔断、热点限流等;后台的程序异常使用,如:RuntimeException等
@GetMapping("/hotKeyTest")
@SentinelResource(value = "hotKeyTest", blockHandler = "deal_hotKeyTest")
public String hotKeyTest(
@RequestParam(value = "p1", required = false)String p1,
@RequestParam(value = "p2", required = false)String p2){
return "-----hotKeyTest----";
}
// blockHandler异常处理的方法
public String deal_hotKeyTest(String p1, String p2, BlockException blockException){
return "deal_hotKeyTest, request error";
}
普通Sentinel配置:
解释:资源名为hotKeyTest,而不是"/hotKeyTest";仅支持QPS的限流方式;参数索引为0,即第一个参数为热点参数;阈值:即统计时长内的请求。
所以,对于上面的热点规则,当在1s内超过一次请求带有参数p1则会触发异常,调用blockHandler
方法;如果不带参数p1则不会触发。
参数例外项配置:
当某个热点参数的值为某个特定的值的时候,给与使用特定的规则,如下:
热点参数为p1,当p1的值为java.String类型的"zhangsan"的时候,允许其阈值为5,即可以在1s内访问5次,而其他带有p1参数的请求只能1s访问1次。
五、系统规则
Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
- Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的
maxQps * minRt
估算得出。设定参考值一般是CPU cores * 2.5
。 - CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
- 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
如图:
当设置系统规则为入口QPS后,系统下的任何一个资源的访问大于1,都会被限流。
六、@SentinelResource
注解包含以下属性:
value
:资源名称,必需项(不能为空)entryType
:entry 类型,可选项(默认为 EntryType.OUT)blockHandler
/blockHandlerClass
: blockHandler 对应处理BlockException的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class`对象,注意对应的函数必需为 static 函数,否则无法解析。fallback/fallbackClass
:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable`类型的参数用于接收对应的异常。
- fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
defaultFallback
(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要为空,或者可以额外多一个 Throwable`类型的参数用于接收对应的异常。
- defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass为对应的类的 Class对象,注意对应的函数必需为 static 函数,否则无法解析。
exceptionsToIgnore
(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException
时只会进入 blockHandler
处理逻辑。若未配置 blockHandler
、fallback
和 defaultFallback
,则被限流降级时会将 BlockException
直接抛出(若方法本身未定义 throws BlockException 则会被 JVM 包装一层 UndeclaredThrowableException
)。
1.blockHandler / blockHandlerClass的使用
该属性用于BlockException异常的处理,即服务限流、降级相关的异常。
当出现这些异常的时候会使用指定的异常处理方法进行处理。配置如下(为了防止代码膨胀,将处理方法置于其他专门的类中):
controller:
@GetMapping("/byResource/globalHandler")
@SentinelResource(
value = "globalHandler" , // 资源名称
blockHandlerClass = CustomerBlockHandler.class, // 指定异常处理所在类的class
blockHandler = "globalHandler1") // 异常处理方法名称
public CommonResult globalHandler(){
return new CommonResult(200,"request /byResource/globalHandler success");
}
异常处理类:
// 异常处理类
public class CustomerBlockHandler {
// 异常处理方法1
public static CommonResult globalHandler1(BlockException e){
return new CommonResult(444, "error by globalHandler1");
}
// 异常处理方法2
public static CommonResult globalHandler2(BlockException e){
return new CommonResult(444, "error by globalHandler2");
}
// 异常处理方法3
public static CommonResult globalHandler3(BlockException e){
return new CommonResult(444, "error by globalHandler3");
}
}
注意:异常处理方法必须是public static修饰的,否则无法解析
因此,当对资源"globalHandler"限流的时候。超过阈值时就会进入"globalHandler1"方法并返回。
2.fallback / fallbackClass的使用
fallback用于处理程序运行时抛出的异常,测试如下:
@RestController
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Value("${provider.service-url}")
private String providerName;
// 调用微服务查询结果
@GetMapping("/consumer/get/payment/{id}")
// fallback可以处理运行(业务)的异常
// @SentinelResource(value = "fallback",fallback = "fallback_method")
// blockHandler不能处理运行异常,只能处理sentinel的配置异常
// @SentinelResource(value = "fallback",blockHandler = "blockHandle_method")
// 同时可以处理sentinel配置异常,也可以处理业务异常
@SentinelResource(value = "fallback",fallback = "fallback_method",blockHandler = "blockHandle_method")
public CommonResult getPayment(@PathVariable(value = "id")Long id){
CommonResult res = restTemplate.getForObject("http://" + providerName + "/get/payment/" + id, CommonResult.class);
Object data = res.getData();
if(data != null){
return res;
}else{
throw new NullPointerException("用户不存在");
}
}
// 再次强调:fallback函数和blockHandle函数的格式必须严格遵守官网要求
public static CommonResult fallback_method(@PathVariable(value = "id")Long id,Throwable e){
return new CommonResult(444,"请求失败:id为:" + id + " 的用户不存在 ");
}
// 再次强调:fallback函数和blockHandle函数的格式必须严格遵守官网要求
public static CommonResult blockHandle_method(@PathVariable(value = "id")Long id, BlockException e){
return new CommonResult(444,"请求失败:被限流限流了");
}
}
上述代码对单独使用"fallback",单独使用"blockHandler",同时使用二者进行了测试,结果为:
- 单独使用fallback,只能处理查询运行时抛出的异常
- 单独使用blockHandler,只能处理sentinel配置后违规的异常
- 同时使用,则所有异常都能处理,且当同时又两种异常发生时,会优先使用blockHandler的异常处理。
对于fallback,也可以将异常处理方法置于一个单独的类中,操作与1.一样。
3.defaultFallback
即:使用了@SentinelResource注解后,不添加fallback,则使用默认的方法;操作同上。
4.exceptionsToIgnore
忽略异常,即:当抛出的异常为指定的以场数时,不再做处理,将按照原样抛出;指定的参数为.class
七、服务熔断
Sentinel+OpenFeign实现服务熔断
-
POM添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
修改yml配置
# 开启sentinel与feign的结合 feign: sentinel: enabled: true
-
主启动类添加注解
@EnableFeignClients
-
添加OpenFeign接口和实现
接口:
@Component @FeignClient(value = "payment-alibaba-provider", fallback = FeignServiceImpl.class) public interface FeignService { @GetMapping("/get/payment/{id}") public CommonResult getNacos(@PathVariable(value = "id")int id); }
实现:
@Component public class FeignServiceImpl implements FeignService { @Override public CommonResult getNacos(int id) { return new CommonResult(444,"请求失败:id为:" + id + " 的用户不存在 "); } }
-
Controller
@Autowired private FeignService feignService; @GetMapping("/consumer/get/byFeign/{id}") public CommonResult getByFeign(@PathVariable(value = "id")int id){ return feignService.getNacos(id); }
-
测试:在断开服务后,consumer访问服务将会被openFeign的实现类中的方法处理。
八、Sentinel规则持久化
…
Seata(处理分布式事务)
分布式事务问题:
微服务的开发需要用到多个数据库保存不同的数据信息,也有许多不同的微服务调用,所以就需要事务来控制数据的一致性,所有需要使用到分布式事务。
一、Seata的基本概念
官网:https://seata.io/zh-cn/
Seata有3个基本组成部分:
- **事务协调器(TC):**维护全局事务和分支事务的状态,驱动全局提交或回滚。
- **事务管理器TM:**定义全局事务的范围:开始全局事务,提交或回滚全局事务。
- **资源管理器(RM):**管理分支事务正在处理的资源,与TC进行对话以注册分支事务并报告分支事务的状态,并驱动分支事务的提交或回滚。
seata由一个XID+三个管理器组成
Seata管理的分布式事务的典型生命周期:
- TM要求TC开始一项新的全局事务。TC生成代表全局事务的XID。
- XID通过微服务的调用链传播。
- RM将本地事务注册为XID到TC的相应全局事务的分支。
- TM要求TC提交或回退相应的XID全局事务。
- TC驱动XID对应的全局事务下的所有分支事务,以完成分支的提交或回滚。
二、seata的配置与启动
下载地址:https://github.com/seata/seata/releases/tag/v1.4.2
1.配置文件修改
-
seata\seata-server-1.4.2\conf\file.conf
1. 修改数据库 mode = "db" 2. 设置数据库连接 driverClassName = "com.mysql.cj.jdbc.Driver" url = "jdbc:mysql://localhost:3306/seata?rewriteBatchedStatements=true&serverTimezone=UTC&useSSL=false" user = "root" password = "root" 3. 添加设置,以下代码原来的文件中没有,需添加,指定事务组;重点关注"admin-tx_group"的值 service { #transaction service group mapping vgroupMapping.my_test_tx_group = "admin-tx_group" #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 }
这里需要注意驱动的使用:本次使用的版本是1.4.2,在lib目录下由jdbc目录,将里面的8.0的驱动拖到lib目录下,并按照上述配置修改即可(不这样操作会导致无法连接数据库而启动失败)
-
seata\seata-server-1.4.2\conf\registry.conf
1.registry.type=nacos 2.registry.nacos.serverAddr="localhost:8848" #此处为nacos地址端口
-
创建数据库
创建数据库,名称为上述配置中的名称(任意)。
创建表:sql脚本地址:https://github.com/seata/seata/tree/develop/script/server/db
创建完成后:
-
测试:
1.启动nacos
2.启动seata
3.注册成功
三、案例准备
-
如图:当用户下单时,会在订单服务(order)创建一个订单,然后通过远程调用库存服务器来扣除库存;
-
再通过远程调用账户服务来扣除用户账户余额;
-
最后再订单服务中修改订单的状态为已完成。
1.创建数据库
-
order
create table t_order( id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, user_id BIGINT(11) DEFAULT NULL, product_id BIGINT(11) DEFAULT NULL, count INT(11) DEFAULT NULL, money DECIMAL(11,0) DEFAULT NULL, status INT(1) DEFAULT NULL COMMENT '0:订单创建中 1:已完结' )ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8; -- 日志表 CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-
storage
create table t_storage( id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, product_id BIGINT(11) DEFAULT NULL, total INT(11) DEFAULT NULL, used INT(11) DEFAULT NULL COMMENT '已用库存' , residue INT(11) DEFAULT NULL COMMENT '剩余' )ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8; insert into seata_storage.t_storage(id,product_id,total,used,residue) values (1,1,100,0,100); select * from seata_storage.t_storage -- 日志表 CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-
account
create table t_account( id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, user_id BIGINT(11) DEFAULT NULL, total DECIMAL(11) DEFAULT NULL COMMENT '总额度度', used DECIMAL(11) DEFAULT NULL COMMENT '已用额度' , residue DECIMAL(11) DEFAULT NULL COMMENT '剩余可用额度' )ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8; insert into t_account(id,user_id,total,used,residue) values (1,1,10000,0,10000); select * from t_account -- 日志表 CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
日志表创建sql脚本:https://github.com/seata/seata-samples/blob/master/ha/src/main/resources/sql/undo_log.sql
2.order-Module创建
1.添加pom
<dependencies>
<!--feign组件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--nacos组件-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata组件-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<!--需要排除该依赖,再另外添加-->
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.4.2</version>
</dependency>
<!--spring boot 组件-->
<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>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2.创建yml
server:
port: 8011
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
tx-service-group: admin-tx_group # 需要与seata的服务端设置的事务组名称一致
service:
vgroup-mapping:
admin-tx_group: default #key与上面的tx-service-group的值对应
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order
password: root
username: root
feign:
hystrix:
enabled: true
mybatis: # 整合mybatis
type-aliases-package: com.zjj.cloud01.domian # 别名所在包
mapperLocations: classpath:mapper/*.xml
3.添加seata配置文件的conf
-
直接将seata的file.conf和registry.conf放入resources目录下即可
-
cong.file
# 添加如下配置信息, service { #transaction service group mapping vgroupMapping.admin-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 }
tips:这里需要注意,当前使用的1.4.2本版seata。在下载下来后,所提供的file.config文件中不存在”service“这个配置项,这个配置项主要用于定义事务组的名称。在使用时,服务端的seata定义了事务组的名称(
详见本章"二"配置的详细内容
),而客户端需要配置与其一致的事务组名称(如上添加的配置项)。如不配置,可能会出现提示:can not get cluster name in registry config ‘…’ please make sure registry config correct 的错误 -
registry.conf
主要修改注册类型为nacos,修改地址和ip
4.创建domain包
- 创建order的实体类:Order
- 创建结果返回类:CommonResult
5.创建mapper
@Component
@Mapper
public interface OrderMapper {
// 添加订单
void createOrder(Order order);
// 修改订单状态
void updateOrderStatus(@Param(value = "userId")Long userId, @Param(value = "status")Integer status);
}
<mapper namespace="com.zjj.cloud01.mapper.OrderMapper">
<insert id="createOrder" parameterType="com.zjj.cloud01.domain.Order">
insert into t_order values (null,#{userId},#{productId},#{count},#{money},0)
</insert>
<update id="updateOrderStatus">
update t_order set status = 1 where user_id = #{userId} and status = #{status}
</update>
</mapper>
6.创建service
-
OrderServiceImpl
public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private AccountService accountService; @Autowired private StorageService storageService; @Override public void createOrder(Order order) { log.info("开始添加订单================"); orderMapper.createOrder(order); log.info("开始扣减库存================="); storageService.decrease(order.getProductId(),order.getCount()); log.info("扣减库存结束================"); log.info("开始扣减余额=================="); accountService.decrease(order.getUserId(),order.getMoney()); log.info("扣减余额结束================"); log.info("开始修改订单的状态============="); orderMapper.updateOrderStatus(order.getUserId(), 0); log.info("修改订单的状态结束================"); log.info("添加订单结束================"); } }
-
AccountService
@FeignClient(value = "seata-account-service") public interface AccountService { @PostMapping("/account/decrease") CommonResult decrease(@RequestParam(value = "userId")Long userId, @RequestParam(value = "money")BigDecimal money); }
-
StorageService
@FeignClient(value = "seata-storage-service") public interface StorageService { @PostMapping("/storage/decrease") CommonResult decrease(@RequestParam(value = "productId")Long productId, @RequestParam(value = "count")Integer count); }
7.创建controller
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("order/create")
public CommonResult createOrder(Order order){
orderService.createOrder(order);
return new CommonResult(200,"添加订单成功~");
}
}
8.创建主启动类
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 取消数据源的自动创建
@EnableDiscoveryClient
@EnableFeignClients
public class SeataOrderMain8011 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMain8011.class,args);
}
}
9.创建config
-
MybatisConfig
@Configuration @MapperScan({"com.zjj.cloud01.mapper.OrderMapper"}) public class MyBatisConfig { }
-
DateSourceProxyConfig
// 使用Seata对数据源管理 @Configuration public class DateSourceProxyConfig { @Value("${mybatis.mapperLocations}") private String mapperLocations; @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource(){ return new DruidDataSource(); } @Bean public DataSourceProxy dataSourceProxy(DataSource dataSource){ return new DataSourceProxy(dataSource); } @Bean public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations)); sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } }
3.storage-Module创建
步骤同上,只需要修改业务逻辑即可(修改库存操作)
4.account-创建
步骤同上,只需要修改业务逻辑即可(修改用户余额操作)
以上步骤全部完成后,测试:访问order,传入订单信息,将会相继调用微服务并修改数据库。
四、案例实践
当正常访问时,一切安好。
1.异常:
情况:当订单创建后(order表新建记录,状态为0),后面的任何一个环节出现故障,都会导致数据的不一致。
实例:修改AccountServiceImpl,为其设置超时(睡眠20s)
@Override
public void updateAccount(Long userId, BigDecimal money) {
log.info("扣款开始===========");
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
accountMapper.updateAccount(userId,money);
log.info("扣款完成===========");
}
结果:当执行到account时,由于时间超时,所以在order端就会报错而不会执行下面的修改状态操作,最终导致用户余额减少,而订单状态处于未处理状态(数据不一致)。
2.解决
在OrderServiceImpl方法上添加@GlobalTransactional
注解即可(在入口添加)
name属性值任取,rollbackFor表示何种情况回滚
@Override
@GlobalTransactional(name = "order-transaction", rollbackFor = Exception.class)
public void createOrder(Order order) {
log.info("开始添加订单================");
orderMapper.createOrder(order);
log.info("开始扣减库存=================");
storageService.decrease(order.getProductId(),order.getCount());
log.info("扣减库存结束================");
log.info("开始扣减余额==================");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("扣减余额结束================");
log.info("开始修改订单的状态=============");
orderMapper.updateOrderStatus(order.getUserId(), 0);
log.info("修改订单的状态结束================");
log.info("添加订单结束================");
}
五、seata原理
ccountServiceImpl,为其设置超时(睡眠20s)
@Override
public void updateAccount(Long userId, BigDecimal money) {
log.info("扣款开始===========");
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
accountMapper.updateAccount(userId,money);
log.info("扣款完成===========");
}
结果:当执行到account时,由于时间超时,所以在order端就会报错而不会执行下面的修改状态操作,最终导致用户余额减少,而订单状态处于未处理状态(数据不一致)。
[外链图片转存中…(img-FZGm2hHG-1620911972626)]
2.解决
在OrderServiceImpl方法上添加@GlobalTransactional
注解即可(在入口添加)
name属性值任取,rollbackFor表示何种情况回滚
@Override
@GlobalTransactional(name = "order-transaction", rollbackFor = Exception.class)
public void createOrder(Order order) {
log.info("开始添加订单================");
orderMapper.createOrder(order);
log.info("开始扣减库存=================");
storageService.decrease(order.getProductId(),order.getCount());
log.info("扣减库存结束================");
log.info("开始扣减余额==================");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("扣减余额结束================");
log.info("开始修改订单的状态=============");
orderMapper.updateOrderStatus(order.getUserId(), 0);
log.info("修改订单的状态结束================");
log.info("添加订单结束================");
}