Spring Cloud Netflix项目进入到维护模式html
什么是维护模式?=> 将模块置于维护模式,意味着Spring Cloud团队将不会再向模块添加新功能(咱们将修复block级别的bug以及安全问题,咱们也会考虑并审查社区的小型pull request)。java
进入维护模式意味着?=> SpringCloud Netflix将再也不开发新的组件。新组件功能将以其余替代品代替的方法实现。mysql
官网 诞生于2018.10.31,Spring Clooud Alibaba正式入驻Spring Cloud官方孵化器,并在Maven中央库发布了第一个版本。linux
能干什么?nginx
功能 | 具体说明 |
---|---|
服务限流与降级 | 默认支持Servlet,Feign,RestTemplate,Dubbo和RocketMQ限流降级功能的接入,能够在运行时经过控制台实时修改限流降级规则,还支持查看限流降级Metrics监控。 |
服务注册与发现 | 适配Spring Cloud服务注册与发现标准,默认集成了Ribbon的支持。 |
分布式配置管理 | 支持分布式系统中的外部化配置,配置更改时自动刷新。 |
消息驱动能力 | 基于SpringCloud Stream 为微服务应用构建消息驱动能力。 |
阿里云对象储存 | 阿里云提供的海量,安全,底成本,高可靠的云存储服务。支持在任什么时候间,任何地点储存和访问任意类型的数据。 |
分布式任务的调度 | 提供秒级,精准,高可靠,高可用的定时(基于Cron表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到全部Worker(schedulerx-client)上执行。 |
使用SpringCloud Alibaba须要在POM文件中引入其依赖:git
<dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.1.0.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
官方文档github
“Nacos”,前四个字母分别问Naming和Configuration的前两个字母,最后的s为Service,它是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台,说白了就是 注册中心 + 配置中心 的组合,等价于SpringCloud以前的 Eureka + Config + Bus,因此Nacos能够替代Eureka作服务中心,能够替代Config作配置中心。web
在Nacos官网下载地址中下载Nacos,在安装Nacos前须要本地Java8和Maven的环境已经准备好,而后下载其压缩包:算法
解压该压缩文件后直接运行在bin目录下的startup.cmd启动Nacos:spring
而后访问 http://localhost:8848/nacos/ 进入Nacos,其默认的帐号密码都是nacos,登陆成功后便可看到以下界面:
在以前学习服务注册中心的时候讲过CAP理论,即
C | A | P |
---|---|---|
Consistency | Available | Partition tolerance |
强一致性 | 可用性 | 分区容错性 |
CAP原则又称CAP定理,指的是在一个分布式系统中,一致性、可用性、分区容错性。
CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。在分布式架构中,P永远要求被保证,因此当前的分布式架构只有AP和CP两种。Nacos属于AP模型,再次对比这些服务注册中心:
服务注册与发现框架 | CAP模型 | 控制台管理 | 社区活跃度 |
---|---|---|---|
Eureka | AP | 支持 | 低(2.x版本历史问题) |
Zookeeper | CP | 不支持 | 中 |
Consul | CP | 支持 | 高 |
Nacos | AP(事实上也能够支持CP) | 支持 | 高 |
听说Nacos在阿里巴巴内部有超过10万的实例运行,已通过了相似双十一等各类大型流量的考验。
基于Nacos的服务提供者
新建Module:cloudalibaba-provider-payment9002做为服务提供方微服务。固然为了实现Nacos的负载均衡,使nacos-payment-provider服务集群化,咱们同事也仿照9002搭建一个9003微服务。
而后在工程的父POM中引入依赖(Spring Cloud Alibaba简介中引入的依赖)的前提下,在该模块的POM引入依赖Nacos服务注册发现的依赖:
<!--SpringCloud ailibaba nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
而后添加其配置文件application.yml:
server: port: 9002 spring: application: name: nacos-payment-provider # 微服务名称 cloud: nacos: discovery: server-addr: localhost:8848 # 配置Nacos地址 management: endpoints: web: exposure: include: '*' # 监控端点所有打开
编写其主启动类并在主启动类上添加 @EnableDiscoveryClient
注解,使9002微服务可以被注册中心发现:
@EnableDiscoveryClient @SpringBootApplication public class PaymentMain9002 { public static void main(String[] args) { SpringApplication.run(PaymentMain9002.class); } }
而后编写一个简单的业务类:
@RestController public class PaymentController { @Value("${server.port}") private String serverPort; @GetMapping("/payment/nacos/{id}") public String getPayment(@PathVariable("id") Integer id) { return "Nacos服务注册,端口:" + serverPort + ";id:" + id; } }
启动900二、9003微服务模块,在Nacos的服务列表中咱们能够看到这两个微服务已经入驻服务注册中心:
点开nacos-payment-provider服务的详情页面,能够看到服务的实例详情:
基于Nacos的服务消费者
新建Module:cloudalibaba-consumer-nacos-order83做为服务消费方微服务,在该模块的POM中一样引入令Nacos服务注册中心发现本身的依赖,配置其配置文件:
server: port: 83 spring: application: name: nacos-order-cosumer cloud: nacos: discovery: server-addr: localhost:8848 # 消费者将要去访问的微服务名称(注册成功进Nacos的微服务提供者) service-url: nacos-user-service: http://nacos-payment-provider
而后编写服务消费方的主启动类,Nacos自己就具备负载均衡功能,由于引入Nacos依赖的同时,Nacos内部集成了Ribbon,如图所示:
而咱们在学习Ribbon时知道,用了Ribbon就须要使用 RestTemplate
,全部咱们编写配置类,向Spring容器中注入 RestTemplate
:
@Configuration public class ApplicationContextConfig { @Bean @LoadBalanced //负载均衡 public RestTemplate getRestTemplate() { return new RestTemplate(); } }
注意 注入 RestTemplate
时必定要添加 @LoadBalanced
注解,不然不会开启负载均衡,在服务提供方集群的状况下,因为没有开启负载均衡,消费方会没法选择具体调用哪一个微服务实例,也就是服务提供方实例有不少个,而消费方服务不知道要调用哪一个具体的服务提供方实例,不加该注解会产生 UnknownHostException
的异常。
而后编写其业务类:
@RestController @Slf4j public class OrderNacosController { @Resource private RestTemplate restTemplate; //直接读取配置文件中的值,减小代码冗余 @Value("${service-url.nacos-user-service}") private String serverURL; @GetMapping("/consumer/payment/nacos/{id}") public String paymentInfo(@PathVariable("id") Long id) { return restTemplate.getForObject(serverURL + "/payment/nacos/" + id, String.class); } }
启动83服务消费方,在Nacos服务注册中心中咱们能够看到入驻了两个服务提供方实例和一个服务消费方实例。
测试
屡次访问 http://localhost:83/consumer/payment/nacos/1 ,咱们发现服务消费方微服务83能够完成对服务提供方微服务9002/9003的轮询负载均衡调用,也就是 Nacos内部就整合了Ribbon,实现了负载均衡,默认负载均衡算法采用轮询。
事实上,Nacos能够在AP模式和CP模式中进行切换,也就是说Nacos不只仅支持AP(可用性和分区容错性),它一样支持CP(一致性和分区容错性)。当 采起入驻到Nacos服务注册中心的微服务对本身的健康状态进行上报时,也就是对入驻到注册中心的微服务进行非持久化的保存,一旦客户端上报不健康信息,就将不健康的实例摘除掉,这相似于Eureka(固然Eureka能够开启自我保护模式);当 采起由Nacos服务注册中心本身探测入驻到中心的微服务是否健康时,也就是对入驻到注册中心的微服务进行持久化保存,即便服务注册中心发现微服务已经不健康了,也不会删除到微服务,这相似于Consul。以下图(CoreDNS也是一种服务注册中心):
Nacos与其余服务注册中心特性的对比:
Nacos | Eureka | Consul | CoreDNS | Zookeeper | |
---|---|---|---|---|---|
一致性协议 | CP/AP | AP | CP | / | CP |
健康检查 | TCP/HTTP/MySQL/Client Beat(客户端心跳) | Clent Beat | TCP/HTTP/gRPC/cmd | / | Client Beat |
负载均衡 | 权重/DSL/元数据/CMDB | Ribbon | Fabio | RR | / |
雪崩保护 | 支持 | 支持 | 不支持 | 不支持 | 不支持 |
自动注销实例 | 支持 | 支持 | 不支持 | 不支持 | 支持 |
访问协议 | HTTP/DNS/UDP | HTTP | HTTP/DNS | DNS | TCP |
监听支持 | 支持 | 支持 | 支持 | 不支持 | 不支持 |
多数据中心 | 支持 | 支持 | 支持 | 不支持 | 不支持 |
跨注册中心 | 支持 | 不支持 | 支持 | 不支持 | 不支持 |
SpringCloud集成 | 支持 | 支持 | 支持 | 不支持 | 不支持 |
Dubbo集成 | 支持 | 不支持 | 不支持 | 不支持 | 支持 |
K8s集成 | 支持 | 不支持 | 支持 | 支持 | 不支持 |
从理论知识中咱们知道Nacos服务注册中心能够在AP和CP模式中进行切换,C是全部节点在同一时间看到的数据是一致的,而A是全部的请求都会受到响应(A能够近似理解为高可用)。
通常来讲,若是不须要存储服务级别的信息且服务实例是经过Nacos-Client注册的,而且服务能保持心跳上报,那么就能够选择AP模式,当前主流的微服务框架如SpringCloud和Dubbo,都适用于AP模式,AP模式为了服务的可能性而减弱了一致性,所以 AP模式下只支持注册临时实例。
而若是须要在服务级别编辑或存储配置信息,那么CP是必须的,K8s服务和DNS服务使用于CP模式,CP模式下则支持注册持久化实例,此时以Raft协议为集群运行模式,该模式下注册实例以前必须先注册服务,若是服务不存在,则会返回错误。
若是须要让Nacos服务从AP模式切换到CP模式的话,只须要向服务注册中心发送POST请求便可:
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'
在用SpringCloud Config结合SpringCloud Bus时,咱们能够把配置信息托管到远端如GitHub上,而如今咱们应用了Nacos后,能够直接在Nacos上托管配置信息。
新建Module:cloudalibaba-config-nacos-client3377做为服务配置中心微服务,在其POM文件中引入必要的依赖(如Nachos服务注册中心的依赖)为,引入配置中心的依赖:
<!--nacos-config--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!--SpringCloud ailibaba nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
Nacos同SpringCloud Config同样,在项目初始化时,要保证先从配置中心(直接以Nacos做为配置中心)进行配置拉取,拉取配置后,才能保证项目的正常启动,而咱们知道SpringBoot配置文件的加载顺序是存在优先级的,bootstrap的优先级要高于application,因此咱们建立两个配置文件 bootstrap.yml
(从配置中心拉取配置)和 application.yml
(写本身的配置),咱们配置 bootstrap.yml
以使3377服务从Nacos上拉取配置信息:
# nacos配置 server: port: 3377 spring: application: name: nacos-config-client cloud: nacos: discovery: #Nacos服务注册中心地址 server-addr: localhost:8848 config: #Nacos做为配置中心地址 server-addr: localhost:8848 # 指定yaml格式的配置,也就是说从Nacos上读yaml格式的配置文件 file-extension: yaml # 配置匹配规则 # ${spring.application.name}-${spring.profile.active}.${file-extension}
而后用 application.yml
定制本身的配置信息,将环境定义为开发环境:
spring: profiles: active: dev # 表示开发环境
编写其主启动类后,而后编写其业务类,在业务来上添加SpringCloud的原生注解@RefreshScope
以使服务能够支持从Nacos配置中心动态刷新配置信息:
@RestController @RefreshScope //支持Nacos的动态刷新功能 public class ConfigClientController { @Value("${config.info}") private String configInfo; @GetMapping("/config/info") public String getConfigInfo() { return configInfo; } }
在上面的业务层中咱们从配置中心读取了配置信息
而咱们在Nacos配置中心中添加配置信息,在Nacos配置中心中添加配置文件要遵循必定的匹配规则。 ↓
Nacos中的 dataId
的组成格式及与SpringBoot配置文件中的匹配规则,更详细的信息能够参考Nacos官方文档。
在Nacos SpringCloud中,dataId
的完整格式为:
${prefix}-${spring.profile.active}.${file-extension}
prefix
默认为 spring.application.name
(服务名)的值,也能够经过配置项 spring.cloud.nacos.config.prefix
来配置。spring.profile.active
即为当前环境对应的 profile(上面咱们在 application.yml
中配置的属性),详情能够参考 Spring Boot文档。 注意:当 spring.profile.active
为空时,对应的链接符 -
也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension}
, 建议 不要使 spring.profile.active
为空。file-exetension
为配置内容的数据格式,能够经过配置项 spring.cloud.nacos.config.file-extension
来配置。目前只支持 properties
和 yaml
类型。因为prefix
默认为 spring.application.name
(服务名)的值,因此 dataId
的完整格式能够替换为:
${spring.application.name}-${spring.profile.active}.${file-extension}
结合咱们在 boostrap.yml
配置文件和 application.yml
配置文件的配置,咱们能够获得在当前实例中的 dataId
应该为:
nacos-config-client-dev.yaml
根据上述公式获得 dataId
应为的值后,咱们就能够在Nacos配置中心的配置列表中新建配置文件。在新建配置文件时,Data ID
中填入咱们获得的 dataId
,组名先选择默认的便可,配置格式选择咱们在 file-extension
中设置的YAML格式,而后编写配置文件,编写后点击 发布便可:
注意,在Nacos中的
dataId
中的后缀名必须用yaml
而不能用yml
。
添加配置文件后再点开Nacos配置中心的配置列表,就能发现已经存在刚才建立的配置文件:
而后咱们启动3377微服务,访问 http://localhost:3377/config/info 查看可否访问都Nacos配置中心的配置信息:
Nacos配置中心直接就 支持动态刷新,再更改了Nacos配置中心的配置信息后,再经过3377微服务访问,就能够获得更新后的配置信息。目前Nacos已经实现了Eureka + Config + Bus的功能,可是Nacos之因此如此优秀是由于这些框架有的功能它都有,而这些框架没有的功能它还有!下面看Nacos的一些高级功能。
问题:多环境多项目管理
在多环境多项目管理时,也就是实际开发中,一般一个系统会准备不一样的开发环境,如dev开发环境、test测试环境、prod生产环境,如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?一个大型分布式微服务系统会有不少微服务子项目,每一个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境等,那对这些微服务配置该如何管理呢?=> 在Nacos配置中心中就能够进行分类配置,一个配置文件的具体所属由 namespace
+ Group
+ dataId
所构成:
为何如此设计?
这种设计就相似于Java中的包名和类名,最外层的 namespace
是能够用于区分部署环境的,主要用来实现隔离,好比有三个环境:开发、测试、生产,那就能够建立三个 namespace
。
Group
和 dataId
逻辑上区分两个目标对象。Group能够把不一样的微服务划分到同一个分组里面去。Service就是微服务,一个Service能够包含多个Cluster(集群),Nacos默认Cluster是DEFALUT,Cluster是对指定微服务的一个虚拟划分。
举例:好比说为了容灾,将Service微服务分别部署在了杭州机房和广州机房,这时就能够给杭州机房的Service微服务起一个集群名称HZ,给广州机房的Service微服务起一个集群名字GZ,还能够尽可能让同一个机房的微服务互相调用,以提高性能。
最后就是Instance
,就是微服务的实例。
默认状况下 namespace
的值为 public
, Group
的值为 DEFAULT_GROUP
,默认Cluster是DEFAULT。
下面对这三个属性进行详细展开。
dataId
方案
指定 spring.profile.active
配置属性和配置文件的 dataId
来使不一样环境下读取不一样的配置,也就是说咱们如今用默认的命名空间 namespace
(即 public
),默认的分组 Group
(即 DEFAULT_GROUP
),而后在Nacos配置中心中创建两个 dataId
分别为 dev
环境和test
环境。此时在同一命名空间同一组中就有了两个应用于开发、测试不一样环境的配置文件:
而后咱们经过修改微服务本身定制配置信息 application.yml
配置文件中的spring.profile.active
属性便可进行多环境下的配置文件的读取,配置什么就加载什么,在以前咱们的3377微服务获取的是配置中心中 nacos-config-client-dev.yaml
配置文件的配置信息,如今咱们将 application.yml
修改成:
spring: profiles: # active: dev # 表示开发环境 active: test # 表示开发环境
而后重启3377微服务,再次进行测试发现访问到的配置信息为测试环境的配置信息:
Group
方案
不只经过 dataId
能够实现环境分区,应用 Group
也能够实现环境分区,首先咱们在Nacos配置中心新建两个相同 dataId
,不一样 Group
的配置文件,一个用于开发环境,一个用于测试环境:
因为这两个配置文件的 dataId
均为 nacos-config-client-info.yaml
,根据以前解释的 dataId
的命名规则,咱们应该将3377微服务的自定义配置文件 application.yml
中的 spring.profile.active
属性更改成 info
:
spring: profiles: active: info
在确认了读取的配置文件的 dataId
后,咱们在其 bootstrap.yml
配置文件中添加 spring.cloud.nacos.config.group
属性,设置为相应的组名便可,好比咱们如今设置为测试环境组:
spring: cloud: nacos: config: group: TEST_GROUP
重启3377微服务,再次访问能够发现获取到的配置信息即为TEST_GROUP
组的配置信息。
namespace
方案
根据namespace一样能够进行环境区分。
新建两个命名空间
服务列表查看
在 bootstrap.yml
配置文件中添加 spring.cloud.nacos.config.namespace
属性,设置为相应的命名空间流水号便可,好比咱们如今设置为开发环境命名空间:
spring: cloud: nacos: config: namespace: 5e4fbf07-10f3-4216-86c5-e40391667310
在dev命名空间下新建不一样group的配置文件
重启3377微服务,再次访问能够发现获取到的配置信息即为 dev
命名空间下TEST_GROUP
组的配置信息。
官网集群部署架构图
开源的时候推荐用户把全部服务列表放到一个vip下面,而后挂到一个域名下面
http://ip1:port/openAPI 直连ip模式,机器挂则须要修改ip才可使用。
http://SLB:port/openAPI 挂载SLB模式(内网SLB,不可暴露到公网,以避免带来安全风险),直连SLB便可,下面挂server真实ip,可读性很差。
http://nacos.com:port/openAPI 域名 + SLB模式(内网SLB,不可暴露到公网,以避免带来安全风险),可读性好,并且换ip方便,推荐模式
VIP:即虚拟IP,即Nginx
上图翻译一下就是
默认Nacos使用嵌入式数据库实现数据的存储,因此,若是启动多个默认配置下的Nacos节点,数据储存是存在一致性问题的。Nacos默认自带的嵌入式数据库为derby。
为了解决这个问题,Nacos采用了 集中式存储的方式来支持集群化部署,目前只支持MySQL的存储。
单机模式支持MySQL,在0.7版本以前,在单机模式时Nacos使用嵌入式数据库实现数据的储存,不方便观察数据的基本状况。0.7版本增长了支持MySQL数据源能力,具体的操做步骤以下。
derby到mysql切换配置步骤:
# 若是是Mysql5.+ spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://mpolaris.top:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&serverTimezone=UTC db.user=root db.password=123456 # 若是是Mysql8.+ spring.datasource.platform=mysql jdbc.DriverClassName=com.mysql.cj.jdbc.Driver db.num=1 db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?serverTimezone=GMT%2B8&characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true db.user= root db.password= 123456
启动Nacos,能够看到是个全新的空记录界面,之前是记录进derby。
须要一个Nginx,三个Nacos注册中心,一个Mysql
Nacos下载Linux版,下载地址,nacos-server-1.1.4.tar.gz,解压后安装
集群配置步骤
Linux服务器上MySQL数据库配置(按照4.3配置)
application.properties配置(按照4.3配置)
Linux服务器上Nacos的集群配置cluster.conf
cd /opt/nacos/conf cp cluster.conf.example cluster.conf
注意:这个IP不能写127.0.0.1,必须是Linux命令hostname -i 可以识别的IP
# it is ip # example # 这里我填的是本身的私网ip 172.20.xxx.xxx:3333 172.20.xxx.xxx:4444 172.20.xxx.xxx:5555
编辑Nacos的启动脚本startup.sh(先备份),使它可以接受不一样的启动端口
# 平时单机版的启动,都是./startup.sh便可 # 可是集群启动,咱们但愿能够相似其余软件的shell命令,传递不一样的端口号启动不一样的nacos实例。 # 例如:命令 ./startup.sh -p 3333 表示启动端口号为3333的nacos服务器实例 vim ./startup.sh # 修改第57行开始增长p export SERVER="nacos-server" export MODE="cluster" export FUNCTION_MODE="all" while getopts ":m:f:s:p:" opt do case $opt in m) MODE=$OPTARG;; f) FUNCTION_MODE=$OPTARG;; s) SERVER=$OPTARG;; p) PORT=$OPTARG;; ?) echo "Unknown parameter" exit 1;; esac done # 修改第134行,增长 - Dserver.port=${PORT} nohup $JAVA -Dserver.port=${PORT} ${JAVA_OPT} nacos.nacos >> ${BASE_DIR}/logs/start.out 2>&1
./startup.sh -p 3333 ./startup.sh -p 4444 ./startup.sh -p 5555 #注意:提示服务器内存不够,能够把nacos启动脚本里-Xms和-Xmx jvm内存调整小一点 # 同时经过window浏览器访问: http://192.168.42.82:3333/nacos/#/login http://192.168.42.82:4444/nacos/#/login http://192.168.42.82:5555/nacos/#/login
Nginx的配置,由它做为负载均衡器
#user nobody; worker_processes 1; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; upstream cluster { server 127.0.0.1:3333; server 127.0.0.1:4444; server 127.0.0.1:5555; } server { listen 1111; server_name localhost; location / { # root html; # index index.html index.htm; proxy_pass http://cluster; } } }
cd /usr/local/nginx/sbin ./nginx -c /opt/nginx/conf/nginx.conf
开始测试
微服务测试
微服务springalibaba-provider-payment9002启动注册进nacos集群
server: port: 9002 spring: application: name: nacos-payment-provider # 微服务名称 cloud: nacos: discovery: # server-addr: localhost:8848 # 配置Nacos地址 # 换成Nginx的1111端口,作集群 server-addr: 192.168.42.1:1111 management: endpoints: web: exposure: include: '*' # 监控端点所有打开
高可用总结
主要特性
与Hystrix对比
Sentiel能够解决的问题
Sentinel分为两部分:
运行命令
# 前提:Java8环境,8080端口不被占用 java -jar sentinel-dashboard-1.7.0.jar
访问Sentinel管理界面
启动Nacos8848,http://localhost:8848/nacos/#/login
新建Moudle:cloudalibaba-sentinel-service8401
pom.xml
<!--SpringCloud ailibaba nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--SpringCloud ailibaba sentinel-datasource-nacos 后续作持久化用到--> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency> <!--SpringCloud ailibaba sentinel --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <!--openfeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--其余基础包-->
yml配置文件
server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: server-addr: localhost:8848 # Nacos服务注册中心地址 sentinel: transport: dashboard: localhost:8080 # 配置Sentinel dashboard地址 # 默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口 port: 8719 datasource: ds1: nacos: server-addr: localhost:8848 dataId: cloudalibaba-sentinel-service groupId: DEFAULT_GROUP data-type: json rule-type: flow management: endpoints: web: exposure: include: '*' feign: sentinel: enabled: true # 激活Sentinel对Feign的支持
主启动
@EnableDiscoveryClient @SpringBootApplication public class MainApp8401 { public static void main(String[] args) { SpringApplication.run(MainApp8401.class, args); } }
业务类FlowLimitController
@RestController @Slf4j public class FlowLimitController { @GetMapping("/testA") public String testA() { return "==> testA"; } @GetMapping("/testB") public String testB() { log.info(Thread.currentThread().getName() + "\t" + "==> testB"); return "==> testB"; } }
启动Sentinel8080,启动微服务8401
启动8401微服务后台查看sentinel控制,啥也没有?=> Sentinel采用懒加载,执行两次访问http:localhost8401/testA,http:localhost8401/testB,效果以下:
名词说明:
流控模式
直接(默认)
测试:快速点击访问 http://localhost:8401/testA => Blocked by Sentinel(flow limiting)
思考:直接调用了默认报错信息,技术方面是没问题的,可是 是否因该有咱们本身的后续处理呢?(相似有一个fallback的兜底方法),后面会讲到如何配置
关联
链路
流控效果
快速失败
Warm Up(预热)
排队等待
名词说明:
RT(平均响应时间,秒级)
-Dcsp.sentinel.statistic.max.rt=XXXX
才能生效)异常比例(秒级)
注意:Sentinel的断路器是没有半开状态的
半开状态:半开的状态,系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用,有异常则继续打开断路器不可用,具体能够参考Hystrix。
Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其余的资源而致使级联错误。
当资源被降级后,在接下来的降级时间窗口以内,对该资源的调用都自动熔断(默认行为是抛出DegradException )。
降级策略演示
RT(平均响应时间(DEGRADE_GRADE_RT
))
count
,以ms为单位),那么在接下来的时间窗口(DegradeRule
中的timeWindow
,以 s 为单位)以内,对这个方法的调用都会自动的熔断(抛出DegradeException
)。注意 Sentinel 莫仍统计的 RT 上限是 4900 ms,超出次阈值的都会算做 4900 ms,若须要变动此上限能够经过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx
来配置。测试
@GetMapping("/testD") public String testD() { //暂停几秒线程 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.info("testD 测试RT"); return "==> testD"; }
异常比例(DEGRADE_GRADE_EXCEPTION_RATIO)
DegradeRule
中的count
)以后,资源进入降级状态,即在接下来的时间窗口(DegradeRule
中的timeWindow
,以 s 为单位)以内,对这个方法的调用都会自动的返回。异常比率的阈值范围是 [0.0,1.0]
,表明 0% - 100%。测试
@GetMapping("/testD") public String testD() { log.info("testD 测试RT"); int age = 10 / 0; return "==> testD"; }
异常数(DEGRADE_GRADE_EXCEPTION_COUNT):
timeWindow
小于 60s,则结束熔断状态后仍可能再进入熔断状态。即 时间窗口必定要 >= 60 秒。测试
源码
com.alibaba.csp.sentinel.slots.block.BlockException
名词说明
何为热点?热点即常常访问的数据。不少时候咱们但愿统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。好比
热点参数限流会统计传入的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流能够看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。
回顾 - 兜底方法
兜底方法分为 系统默认 和 客户自定义 两种,以前限流出现问题后,都是用sentinel 系统默认的提示:Blocked by Sentinel (flow limiting)。
咱们能不能自定义?相似hystrix,某个方法出问题了,就找对应的兜底降级方法!
=> 在sentinel中经过 @SentinelResource
注解可以实现 (相似于hystrix的HystrixCommand
)。
/** * value: 惟一标识,对应热点规则资源名 * blockHandler:违背了热点规则后指向的兜底方法,若是不配置,异常就会直接打到前台去(不友好) */ @GetMapping("/testHotKey") @SentinelResource(value = "testHotKey",blockHandler="deal_testHotKey") public String testHotKey(@RequestParam(value="p1",required=false) String p1, @RequestParam(value="p2",required=false) String p2){ return "==> testHotKey"; } public String deal_testHotKey(String p1, String p2, BlockException e) { return "==> deal_testHotKey"; }
配置
方法testHotKey里面第一个参数只要QPS超过每秒1次,立刻降级处理。且用了咱们自定义的兜底方法。
测试
# QPS超过每秒1次会限流 http://localhost:8401/testHotKey?p1=0 # QPS超过每秒1次会限流 http://localhost:8401/testHotKey?p1=0&p2=1 # 无限制,不会限流 http://localhost:8401/testHotKey?p2=1
参数例外项
上述案例演示了第一个参数p1,当QPS超过每秒1次马上就会被限流。
特殊状况:咱们指望 p1 参数当它是某个特殊值时,它的限流和平时不同,例如当 p1 的值等于 5 时,它的阈值能够达到 200。怎样实现呢?=> 配置参数例外项。
前提条件:热点参数必须是 基本类型 或者 String
其余注意点
@SentineResource 处理的是Sentinel控制台配置的违规状况,有blockHandler方法配置的兜底方法处理。假如程序代码出错如 int age = 10/0,这个是Java运行时爆出的运行时异常RunTimeException,@SentineResource 无论。
总结:@SentineResource 主管配置出错,运行出错该走异常仍是会走异常!
各项配置说明
系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load,CPU使用率,平均RT,入口QPS 和并发线程数等几个维度监控应用指标,让系统尽量跑在最大吞吐量的同时保证系统总体的稳定性。
系统保护规则是 应用总体维度的,而不是资源维度的,而且 仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN
),好比 Web 服务器 或Dubbo 服务端接受的请求,都属于入口流量。
系统规则支持如下的模式:
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为例:
系统规则通常少用,使用危险,一竹竿打死一船人!
按资源名称限流 + 后续处理
启动nacos,sentinel
修改8401pom.xml,将公共模块也引入
<!-- 公共模块 --> <dependency> <groupId>com.polaris</groupId> <artifactId>cloud-api-common</artifactId> <version>${project.version}</version> </dependency>
新增业务类RateLimitController,便于演示
@RestController public class RateLimitController { @GetMapping("/byResource") @SentinelResource(value = "byResource", blockHandler = "handleException") public CommonResult byResource() { return new CommonResult(200, "按资源名称限流测试OK", new Payment(2020L, "serial001")); } public CommonResult handleException(BlockException exception) { return new CommonResult(444, exception.getClass().getCanonicalName() + "\t 服务不可用"); } }
配置流控规则:表示1秒钟内查询次数大于1,就跑到咱们自定义的限流处进行限流
测试:出现了咱们自定义的限流兜底方案!
发现问题:此时若是关闭服务8401会怎样?=> Sentinel控制台流控规则消失了?临时存储?
按照Url地址限流 + 后续处理
经过访问URL来限流,会返回Sentinel自带默认的限流处理信息
业务类增长
@GetMapping("/rateLimit/byUrl") @SentinelResource(value = "byUrl") public CommonResult byUrl() { return new CommonResult(200, "按url限流测试OK", new Payment(2020L, "serial002")); }
配置流控规则
测试:会返回Seninel自带的限流处理结果:Blocked by Sentinel (flow limiting)
上述兜底方案面临的问题
系统默认的方案,没有体现咱们本身的业务需求
依照现有条件,咱们自定义的处理方法又和业务代码耦合在一块儿,不直观
每一个业务方法都添加一个兜底方法,会致使代码膨胀加重
全局统一的处理方法没有体现
客户自定义限流处理逻辑
建立CustomerBlockHandler类用于自定义限流处理逻辑
public class CustomerBlockHandler { public static CommonResult handlerException(BlockException exception) { return new CommonResult(4444, "按客戶自定义,global handlerException => 1"); } public static CommonResult handlerException2(BlockException exception) { return new CommonResult(4444, "按客戶自定义,global handlerException => 2"); } }
RateLimitController增长方法
@GetMapping("/rateLimit/customerBlockHandler") @SentinelResource(value = "customerBlockHandler", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handlerException2") public CommonResult customerBlockHandler() { return new CommonResult(200, "按客戶自定义", new Payment(2020L, "serial003")); }
测试:出现了咱们自定义的限流兜底方案!
@SentinelResource的其余属性
属性 | 做用 | 是否必须 |
---|---|---|
value | 资源名称 | 是 |
entryType | entry类型,标记流量的方向,取值IN/OUT,默认是OUT | 否 |
blockHandler | 处理BlockException的函数名称。函数要求: 1. 必须是 public 2.返回类型与原方法一致 3. 参数类型须要和原方法相匹配,并在最后加 BlockException 类型的参数。4. 默认需和原方法在同一个类中。若但愿使用其余类的函数,可配置 blockHandlerClass ,并指定blockHandlerClass里面的方法。 |
否 |
blockHanderClass | 存放blockHandler的类。对应的处理函数必须static修饰,不然没法解析,其余要求:同blockHandler。 | 否 |
fallback | 用于在抛出异常的时候提供fallback处理逻辑。fallback函数能够针对全部类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。函数要求:1. 返回类型与原方法一致 2. 参数类型须要和原方法相匹配,Sentinel 1.6开始,也可在方法最后加 Throwable 类型的参数。3.默认需和原方法在同一个类中。若但愿使用其余类的函数,可配置 fallbackClass ,并指定fallbackClass里面的方法。 |
否 |
fallbackClass (1.6) | 存放fallback的类。对应的处理函数必须static修饰,不然没法解析,其余要求:同fallback。 | 否 |
defaultFallback | 用于通用的 fallback 逻辑。默认fallback函数能够针对全部类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,以fallback为准。函数要求:\1. 返回类型与原方法一致 \2. 方法参数列表为空,或者有一个 Throwable 类型的参数。\3. 默认须要和原方法在同一个类中。若但愿使用其余类的函数,可配置 fallbackClass ,并指定 fallbackClass 里面的方法。 |
否 |
exceptionTolgnore (1.6) | 指定排除掉哪些异常。排除的异常不会计入异常统计,也不会进入fallback逻辑,而是原样抛出。 | 否 |
exceptionsToTrace | 须要trace的异常 | Throwable |
注意:
1.6.0 以前的版本 fallback 函数只针对降级异常(
DegradeException
)进行处理,不能针对业务异常进行处理。若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出
BlockException
时只会进入blockHandler
处理逻辑。若未配置blockHandler
、fallback
和defaultFallback
,则被限流降级时会将BlockException
直接抛出。从 1.4.0 版本开始,注解方式定义资源支持自动统计业务异常,无需手动调用
Tracer.trace(ex)
来记录业务异常。Sentinel 1.4.0 之前的版本须要自行调用Tracer.trace(ex)
来记录业务异常。
sentinel整合Ribbon + openFeign + fallback
启动nacos和sentinel
新建服务提供者9004/9005
<!--SpringCloud ailibaba nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
server: port: 9004 spring: application: name: nacos-payment-provider cloud: nacos: discovery: server-addr: localhost:8848 # 配置Nacos地址 management: endpoints: web: exposure: include: '*'
业务类
@RestController public class PaymentController { @Value("${server.port}") private String serverPort; public static HashMap<Long, Payment> hashMap = new HashMap<>(); static { hashMap.put(1L, new Payment(1L, "28a8c1e3bc2742d8848569891fb42181")); hashMap.put(2L, new Payment(2L, "bba8c1e3bc2742d8848569891ac32182")); hashMap.put(3L, new Payment(3L, "6ua8c1e3bc2742d8848569891xt92183")); } @GetMapping(value = "/paymentSQL/{id}") public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) { Payment payment = hashMap.get(id); CommonResult<Payment> result = new CommonResult(200, "from mysql,serverPort: " + serverPort, payment); return result; } }
新建服务消费者84
<!--SpringCloud openfeign --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--SpringCloud ailibaba nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--SpringCloud ailibaba sentinel --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
server: port: 84 spring: application: name: nacos-order-consumer cloud: nacos: discovery: server-addr: localhost:8848 sentinel: transport: #配置Sentinel dashboard地址 dashboard: localhost:8080 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口 port: 8719 #消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者) service-url: nacos-user-service: http://nacos-payment-provider # 激活Sentinel对Feign的支持 feign: sentinel: enabled: true
@EnableDiscoveryClient @SpringBootApplication @EnableFeignClients public class OrderNacosMain84 { public static void main(String[] args) { SpringApplication.run(OrderNacosMain84.class, args); } }
@Configuration public class ApplicationContextConfig { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } }
@Component public class PaymentFallbackService implements PaymentService { @Override public CommonResult<Payment> paymentSQL(Long id) { return new CommonResult<>(44444, "服务降级返回,=> PaymentFallbackService", new Payment(id, "errorSerial")); } }
@RestController @Slf4j public class CircleBreakerController { public static final String SERVICE_URL = "http://nacos-payment-provider"; @Resource private RestTemplate restTemplate; //① 没有配置 // @SentinelResource(value = "fallback") //② fallback只负责业务异常 // @SentinelResource(value = "fallback",fallback = "handlerFallback") //③ blockHandler只负责sentinel控制台配置违规 // @SentinelResource(value = "fallback",blockHandler = "blockHandler") @RequestMapping("/consumer/fallback/{id}") @SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler", exceptionsToIgnore = {IllegalArgumentException.class}) public CommonResult<Payment> fallback(@PathVariable Long id) { CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id); if (id == 4) { throw new IllegalArgumentException( "IllegalArgumentException,非法参数异常!"); } else if (result.getData() == null) { throw new NullPointerException( "NullPointerException,该ID没有对应记录,空指针异常"); } return result; } //本例是fallback public CommonResult handlerFallback(@PathVariable Long id, Throwable e) { Payment payment = new Payment(id, "null"); return new CommonResult<>(444, "兜底异常handlerFallback,exception内容 " + e.getMessage(), payment); } //本例是blockHandler public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) { Payment payment = new Payment(id, "null"); return new CommonResult<>(445, "blockHandler-sentinel限流,无此流水: blockException " + blockException.getMessage(), payment); } //==> OpenFeign @Resource private PaymentService paymentService; @GetMapping(value = "/consumer/paymentSQL/{id}") public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) { return paymentService.paymentSQL(id); } }
测试
是什么
一旦咱们重启应用,Sentinel规则消失,生产环境须要将配置规则进行持久化
怎么作
将限流规则持久进Nacos保存,只要刷新8401某个rest地址,Sentinel控制台的流控规则就能看的到,只要Nacos里面的配置不删除,针对8401上的流控规则持续有效。
步骤演示
现象:咱们先在Sentinel配置一条流控规则,当重启8401后,该流控规则就会失效消失了!
修改8401
<!--SpringCloud ailibaba sentinel-datasource-nacos 作持久化用 --> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>
添加Nacos业务规则配置(规则便可以保存在Sentinel中,也能够保存在Nacos中等,只要是一个持久化媒介就行)
[ { "resource":"/rateLimit/byUrl", //资源名称 "limitApp":"default", //来源应用 "grade":1, //阈值类型,0表示线程数,1表示QPS "count":1, //单机阈值 "strategy":0, //流控模式,0表示直接,1表示关联,2表示链路 "controlBehavior":0, //流控效果,0表示快速失败,1表示Warm Up,2表示排队等候 "clusterMode":false //是否集群 } ]
启动8401 刷新Sentinel发现业务规则有了
中止8401,再看sentinel,发现停机后流控规则消失了!
重启8401,再看sentinel,屡次调用接口,屡次刷新sentinel后,发现流控规则从新出现了,持久化验证成功!
分布式以前
单机库存没有这个问题
从1:1 => 1:N => N:N,开始出现分布式事务问题
分布式以后
单机应用被拆分红微服务应用,原来的三个模块被拆分红 三个独立的应用,分别使用 三个独立的数据源 ,业务操做须要调用 三个服务来完成。
此时 每一个服务内部的数据一致性由 本地 事务来保证,可是 全局 的数据一致性问题无法保证。
一句话:一次业务操做须要跨多个数据源或须要跨多个系统进行远程调用,就会产生分布式事务问题。
是什么
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
做用
一个典型的分布式事务过程:ID + 三组件模型
处理过程(咱们能够用这种方式帮助理解:RM-学生,TM-班主任,TC-授课老师)
怎样用
本地 @Transational
全局 @GlobalTransational
下载版本
修改conf目录下的file.conf配置文件
先备份原始file.conf文件
主要修改:自定义事务组名称 + 事务日志存储模式为db + 数据库链接
mysql5.7数据库新建库seata,建表db_store.sql在seata-server-0.9.0\seata\conf目录里面
修改seata-server-0.9.0\seata\conf目录下的registry.conf目录下的registry.conf配置文件
目的是指明注册中心为nacos,及修改nacos链接信息。
先启动Nacos,端口号为8848
再启动seata-server:bin目录下seata-server.bat
分布式事务业务说明
这里咱们建立三个服务,一个订单服务,一个库存服务,一个帐户服务。
当用户下单时,会在订单服务中建立一个订单,而后经过远程调用库存服务来扣减下单商品的库存,再经过远程调用帐户服务来扣减用户帐户里面的余额,最后在订单服务中修改订单状态为已完成。
该操做跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
建立业务数据库
create database seata_order; create database seata_storage; create database seata_account;
# seata_order库下新建t_order表 DROP TABLE IF EXISTS `t_order`; CREATE TABLE `t_order` ( `int` bigint(11) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) DEFAULT NULL COMMENT '用户id', `product_id` bigint(11) DEFAULT NULL COMMENT '产品id', `count` int(11) DEFAULT NULL COMMENT '数量', `money` decimal(11, 0) DEFAULT NULL COMMENT '金额', `status` int(1) DEFAULT NULL COMMENT '订单状态: 0:建立中 1:已完结', PRIMARY KEY (`int`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '订单表' ROW_FORMAT = Dynamic; # seata_storage库下新建t_storage表 DROP TABLE IF EXISTS `t_storage`; CREATE TABLE `t_storage` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `product_id` bigint(11) DEFAULT NULL COMMENT '产品id', `total` int(11) DEFAULT NULL COMMENT '总库存', `used` int(11) DEFAULT NULL COMMENT '已用库存', `residue` int(11) DEFAULT NULL COMMENT '剩余库存', PRIMARY KEY (`int`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '库存' ROW_FORMAT = Dynamic; INSERT INTO `t_storage` VALUES (1, 1, 100, 0, 100); # seata_account库下新建t_account表 CREATE TABLE `t_account` ( `id` bigint(11) NOT NULL COMMENT 'id', `user_id` bigint(11) DEFAULT NULL COMMENT '用户id', `total` decimal(10, 0) DEFAULT NULL COMMENT '总额度', `used` decimal(10, 0) DEFAULT NULL COMMENT '已用余额', `residue` decimal(10, 0) DEFAULT NULL COMMENT '剩余可用额度', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '帐户表' ROW_FORMAT = Dynamic; INSERT INTO `t_account` VALUES (1, 1, 1000, 0, 1000);
# 该sql在seata\conf目录下的db_undo_log.sql中 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, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
业务需求
下订单 => 减库存 => 扣余额 => 改(订单)状态
新建订单Order-Module seata-order-service2001
pom
<dependencies> <!--SpringCloud ailibaba 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>0.9.0</version> </dependency> <!--feign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!-- SpringBoot整合Web组件 --> <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> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <!--mysql-connector-java--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--jdbc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!--平常通用jar包配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </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>
yml
server: port: 2001 spring: application: name: seata-order-service cloud: alibaba: seata: #自定义事务组名称须要与seata-server中的对应 tx-service-group: fsp_tx_group nacos: discovery: server-addr: localhost:8848 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://mpolaris.top:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: 1234321 feign: hystrix: enabled: false logging: level: io: seata: info mybatis: mapperLocations: classpath:mapper/*.xml
domain
//CommonResult @Data @AllArgsConstructor @NoArgsConstructor public class CommonResult<T>{ private Integer code; private String message; private T data; public CommonResult(Integer code, String message) { this(code,message,null); } }
//order @Data @AllArgsConstructor @NoArgsConstructor public class Order{ private Long id; private Long userId; private Long productId; private Integer count; private BigDecimal money; private Integer status; //订单状态:0:建立中;1:已完结 }
Dao
@Mapper public interface OrderDao{ //1 新建订单 void create(Order order); //2 修改订单状态,从零改成1 void update(@Param("userId") Long userId, @Param("status") Integer status); }
service
public interface OrderService{ void create(Order order); }
@FeignClient(value = "seata-storage-service") public interface StorageService{ @PostMapping(value = "/storage/decrease") CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count); }
@FeignClient(value = "seata-account-service") public interface AccountService{ @PostMapping(value = "/account/decrease") CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money); }
@Service @Slf4j public class OrderServiceImpl implements OrderService{ @Resource private OrderDao orderDao; @Resource private StorageService storageService; @Resource private AccountService accountService; /** * 建立订单->调用库存服务扣减库存->调用帐户服务扣减帐户余额->修改订单状态 * 简单说:下订单->扣库存->减余额->改状态 */ @Override @GlobalTransactional(name="fsp-create-order",rollbackFor=Exception.class) public void create(Order order) { log.info("==> 开始新建订单"); //1 新建订单 orderDao.create(order); //2 扣减库存 log.info("==> 订单微服务开始调用库存,作扣减Count"); storageService.decrease(order.getProductId(),order.getCount()); log.info("==> 订单微服务开始调用库存,作扣减end"); //3 扣减帐户 log.info("==> 订单微服务开始调用帐户,作扣减Money"); accountService.decrease(order.getUserId(),order.getMoney()); log.info("==> 订单微服务开始调用帐户,作扣减end"); //4 修改订单状态,从零到1,1表明已经完成 log.info("==> 修改订单状态开始"); orderDao.update(order.getUserId(),0); log.info("==> 修改订单状态结束"); log.info("==> 下订单结束了,O(∩_∩)O哈哈~"); } }
controller
@RestController public class OrderController{ @Resource private OrderService orderService; @GetMapping("/order/create") public CommonResult create(Order order) { orderService.create(order); return new CommonResult(200,"订单建立成功"); } }
config
@Configuration public class DataSourceProxyConfig { @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 sqlSessionFactoryBean( DataSourceProxy dataSourceProxy)throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); sqlSessionFactoryBean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources(mapperLocations)); sqlSessionFactoryBean.setTransactionFactory( new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } }
@Configuration @MapperScan({"com.polaris.springcloud.dao"}) public class MyBatisConfig { }
主启动
@EnableDiscoveryClient @EnableFeignClients @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源的自动建立 public class SeataOrderMainApp2001 { public static void main(String[] args) { SpringApplication.run(SeataOrderMainApp2001.class, args); } }
mapper
<mapper namespace="com.polaris.springcloud.dao.OrderDao"> <resultMap id="BaseResultMap" type="com.polaris.springcloud.domain.Order"> <id column="id" property="id" jdbcType="BIGINT"/> <result column="user_id" property="userId" jdbcType="BIGINT"/> <result column="product_id" property="productId" jdbcType="BIGINT"/> <result column="count" property="count" jdbcType="INTEGER"/> <result column="money" property="money" jdbcType="DECIMAL"/> <result column="status" property="status" jdbcType="INTEGER"/> </resultMap> <insert id="create"> insert into t_order (id,user_id,product_id,count,money,status) values (null,#{userId},#{productId},#{count},#{money},0); </insert> <update id="update"> update t_order set status = 1 where user_id=#{userId} and status = #{status}; </update> </mapper>
将seata-server/conf目录下的file.conf和registry.conf拷贝到项目模块下的resource中(seata-server/conf下的是总控)
新建库存Storage-Module seata-storage-service2002
新建帐户Account-Module seata-account-service2003
参考订单Module,源码参考我的GitHub
数据库初始状况
正常下单
超时异常,没加@GlobalTransactional
@Override public void decrease(Long userId, BigDecimal money) { LOGGER.info("==> account-service中扣减帐户余额开始"); //模拟超时异常,全局事务回滚 //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } accountDao.decrease(userId, money); LOGGER.info("==> account-service中扣减帐户余额结束"); }
超时异常,添加了@GlobalTransactional
再次理解TC/TM/RM三个组件 - 分布式事务的执行流程:
AT模式如何作到对业务的无侵入: