Spring Cloud Alibaba Sentinel
一、Sentinel 简介
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。 Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:
- 丰富的应用场景: Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、实时熔断下游不可用应用等。
- 完备的实时监控: Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态: Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
- 完善的 SPI 扩展点: Sentinel 提供简单易用、完善的 SPI 扩展点。您可以通过实现扩展点,快速的定制逻辑。例如定制规则管理、适配数据源等。
二、Sentinel 控制台安装
Sentinel 提供一个轻量级的开源控制台,它提供机器发现以及健康情况管理、监控(单机和集群),规则管理和推送的功能。本节将详细记录何如通过 Sentinel 控制台控制 Sentinel 客户端的各种行为。Sentinel 控制台的功能主要包括:流量控制、降级控制、热点配置、系统规则和授权规则等。
2.1 下载 Sentinel
访问:https://github.com/alibaba/Sentinel/releases
找到:1.7.1 版本:
点击 sentinel-dashboard-1.7.1.jar 完成下载:
2.2 启动 sentinel-dashboard
将下载好的 sentinel-dashboard-1.7.1.jar 复制到安装软件的目录里面。
使用:
java -jar sentinel-dashboard-1.7.1.jar
来启动一个 sentinel-dashboard 的实例。
启动成功后:
我们可以通过浏览器访问:
http://localhost:8080/
其中,用户名:sentinel
密码: sentinel
更多可用的启动参数配置:
java -D 参数名=参数值 -jar xx.jar
java -jar xx.jar --参数名=参数值
-Dsentinel.dashboard.auth.username=sentinel 用于指定控制台的登录用户名为 sentinel;
-Dsentinel.dashboard.auth.password=123456 用于指定控制台的登录密码为 123456,如果省略这两个参数,默认用户和密码均为 sentinel;
-Dserver.servlet.session.timeout=7200 用于指定 Spring Boot 服务端 session 的过期时间,如 7200 表示 7200 秒;60m 表示 60 分钟,默认为 30 分钟;
-Dcsp.sentinel.dashboard.server=consoleIp:port 指定控制台地址和端口
三、搭建客户端
刚才我们搭建了 sentinel-dashboard,我们还需要搭建一个客户端,用于测试 sentinel 的各种功能。
我们将搭建如图所示的 Maven 项目结构:
3.1 使用 IDEA 创建子模块
选择Maven项目:
点击 Next:
Parent:选择 spring-cloud-alibaba-examples
Name:命名为 sentinel-example-client
其他的项保持默认值即可。
点击 Finish 完成创建。
3.2 添加依赖
修改 sentinel-example-client 里面的 pom.xml 文件:
添加以下的内容:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
</dependencies>
有 2 个依赖:
- spring-cloud-starter-alibaba-sentinel 这是 spring cloud 和 sentinel 集成的项目
- spring-boot-starter-web 开启 web 最基础的依赖添加 spring boot 的打包插
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
这样,我们的项目打包好了后,可以使用java -jar
来直接运行了。
3.3 完整的 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-alibaba-examples</artifactId>
<groupId>com.bjsxt</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sentinel-example-client</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.4 添加一个配置文件
命名为:
修改该配置文件,添加以下的配置:
server:
port: 8085
spring:
application:
name: sentinel-client
cloud:
sentinel:
transport:
dashboard: localhost:8080
port: 8719 #sentinel-client 监听,因为sentinel-dashboard将从这个端口来采集数据,若端口被占用+1
其中:
- spring.cloud.sentinel.transport.dashboard 指定了 sentinel 控制台的 ip 和端口地址;
- spring.cloud.sentinel.transport.port 代表 sentinel 客户端和控制台通信的端口,默认为8719,如果这个端口已经被占用,那么 sentinel 会自动从 8719 开始依次+1 扫描,直到找到未被占用的端口。
3.5 添加一个启动类
名称为:com.bjsxt.SentinelClientApplication
添加如下的代码:
@SpringBootApplication
public class SentinelClientApplication {
public static void main(String[] args) {
SpringApplication.run(SentinelClientApplication.class ,args) ;
}
}
3.6 添加一个 Controller
名称为:controller.TestController
在 TestContrller 里面添加如下接口:
@RestController
public class TestController {
@GetMapping("/hello")
public ResponseEntity<String> hello(){
return ResponseEntity.ok("hello,sentinel") ;
}
}
3.7 启动项目
在浏览器访问:
http://localhost:8080/#/dashboard/home
出现:
发现并没有任何的功能。
此时,我们访问一下我们写的 hello 接口:
http://localhost:8085/hello
多访问几次。
再次访问:
http://localhost:8080/#/dashboard/home
控制台已经显示正常了。
并且,在簇点链路中可以看到刚刚那笔请求,我们可以对它进行流控、降级、授权、热点等
配置(控制台是懒加载的,如果没有任何请求,那么控制台也不会有任何内容)。
四、流控规则
流量的控制规则。
在簇点链路列表中,点击/hello
后面的流控按钮:
出现:
- 资 源 名 : 标 识 资 源 的 唯 一 名 称 , 默 认 为 请 求 路 径 , 也 可 以 在 客 户 端 中 使 用@SentinelResource 配置;
- 针 对 来 源 : Sentinel 可 以 针 对 服 务 调 用 者 进 行 限 流 , 填 写 微 服 务 名 称 即 spring.application.name,默认为 default,不区分来源;
- 阈值类型、单机阈值:
- QPS(Queries-per-second,每秒钟的请求数量):当调用该 api 的 QPS 达到阈值的时候,进行限流;
- 线程数:当调用该 api 的线程数达到阈值的时候,进行限流。
- 是否集群:默认不集群;
- 流控模式:
- 直接:当 api 调用达到限流条件的时,直接限流;
- 关联:当关联的资源请求达到阈值的时候,限流自己;
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,则进行限流)。
- 流控效果:
- 快速失败:直接失败;
- Warm Up:根据 codeFactor(冷加载因子,默认值为 3)的值,从阈值/codeFactor,经过预热时长,才达到设置的 QPS 阈值;
- 排队等待:匀速排队,让请求匀速通过,阈值类型必须设置为 QPS,否则无效。
4.1 QPS 直接失败
演示下 QPS 直接失败设置及效果。点击簇点链路列表中/hello 请求后面的流控按钮:
上面设置的效果是,1 秒钟内请求/hello 资源的次数达到 2 次以上的时候,进行限流。
点击新增完成该规则的设置。
现在,在浏览器访问:
http://localhost:8085/hello
当手速快点的时候(1 秒超过 2 次),页面返回 Blocked by Sentinel (flow limiting)。并且响应码为 429。
4.2 线程数直接失败
4.2.1 添加接口
在 TestController 里面新建一个接口。
/**
* 线程直接失败
* @return
* @throws InterruptedException
*/
@GetMapping("/thread")
public ResponseEntity<String> threadMode() throws InterruptedException {
TimeUnit.SECONDS.sleep(1);
return ResponseEntity.ok("hello,sentinel!") ;
}
其中,我们添加了:
TimeUnit.SECONDS.sleep(1);
让线程睡 1s ,这样,更容易触发规则。
重启项目。
访问我们添加的 thread 的接口:
http://localhost:8085/thread
发现,页面需要等待 1s 左右才会响应,这是线程 sleep 的效果。
4.2.2 新增流控规则
点击新增,完成创建。
4.2.3 测试该规则
浏览器快速的访问:
http://localhost:8085/thread
4.3 关联
访问某个接口达到一定的流控规则后,开始限制本接口。
4.3.1 在 TestController 里面添加 api 接口
@GetMapping("/test1")
public ResponseEntity<String> test1(){
return ResponseEntity.ok("hello,test1") ;
}
@GetMapping("/test2")
public ResponseEntity<String> test2(){
return ResponseEntity.ok("hello,test2") ;
}
重启项目,正常的访问 test1,test2 测试:
4.3.2 添加规则
我们想让 test1 关联 test2,也就是说,访问 test2 接口达到某种规则后,开始限流 test1 。
上述流控规则表示:当 1 秒内访问 /test2 的次数大于 2 的时候,限流 /test1。
4.3.3 测试规则
我们使用打开 2 个网页,密集访问/test2,然后我们手动浏览器请求/test1,看看效果。
访问 test1:
发现已经开始限流了。
4.4 链路
程序的调用可以看成为一条链路。当触发链路的某条规则后,该链路被限流。
上图从 API 出发,可以发现有 2 条链路,分别为:
2 条链路在调用上,限流规则互补影响。
4.4.1 添加一个 Service
名称为:(service.TestService)
代码为:
@Service
public class TestService {
public String hello(){
return "hello" ;
}
}
4.4.2 添加接口
@Autowired
private TestService testService ;
@GetMapping("/link1")
public ResponseEntity<String> link1(){
return ResponseEntity.ok(String.format("link1,调用 test,结果为%s",testService.hello())) ;
}
@GetMapping("/link2")
public ResponseEntity<String> lin2(){
return ResponseEntity.ok(String.format("link2,调用 test,结果为%s",testService.hello())) ;
}
4.4.3 声明资源
我们现在把 TestService 里面的 hello 方法变成一个资源:
注意:
@SentinelResource(“hello”)将该方法标识为一个 sentinel 资源,名称为 hello。
然后重启测试:
4.4.4 添加链路规则
点击簇点链路:
此时我们给 hello 该资源限流:
在入口资源,我们使用的是 link1,点击新增,完成对规则的添加。
上述配置的意思是,当通过/link1
访问 hello 的时候,QPS 大于 2 则进行限流;言外之意就是/link
访问 hello 请求并不受影响。
4.4.5 测试该规则
打开 link1,link2接口。
快速访问 link1,再访问 link2:
访问测试 n 次,发现还是不能成功。难受!
具体的错误:
https://github.com/alibaba/Sentinel/issues/1213
4.5 预热 Warm Up
流控效果除了直接失败外,我们也可以选择预热 Warm Up。
sentinel 客户端的默认冷加载因子 coldFactor 为 3,即请求 QPS 从 threshold / 3 开始,经预热时长逐渐升至设定的 QPS 阈值。
比如:
我们给 hello 资源设置该规则。
上面的配置意思是:对于/hello 资源,一开始的 QPS 阈值为 3,经过 10 秒后,QPS 阈值达到 10。
新增完成后:
快速访问/hello
http://localhost:8085/hello
查看:
过程类似于下图:
前期在预热环境,突然的高 QPS 会导致系统直接拒绝访问,慢慢地,开始大量的介绍新的请求。
最快的手速点刷新,一开始会常看到 Blocked by Sentinel (flow limiting)的提示,10 秒后几乎不再出现(因为你的手速很难达到 1 秒 10 下)。
4.6 排队等待
排队等待方式不会拒绝请求,而是严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过。
在 TestController 里面添加接口:
private static Logger logger = LoggerFactory.getLogger(TestController.class) ;
@GetMapping("/queue")
public ResponseEntity<String> queue(){
logger.info("开始处理请求");
return ResponseEntity.ok("ok") ;
}
重启项目并且访问 queue 接口:
给 queue 添加一个限流规则:
点击新增完成创建。
快速访问 queue 接口,观察后台的打印:
上述配置的含义是,访问/queue 请求每秒钟最多只能 1 次,超过的请求排队等待,等待超过 2000 毫秒则超时。新增该规则后,多次快速访问 localhost:8081/test1,sentinel 客户端控
制台日志打印如下:
五、降级规则
Sentinel 除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积。Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。
当访问系统失败超过一定的次数后,对该接口进行熔断的操作。
我们可以发现,降级策略分为 3 种:
- RT,平均响应时间 (DEGRADE_GRADE_RT):当 1s 内持续进入 5 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以 ms 为单位),那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出 此 阈 值 的 都 会 算 作 4900 ms , 若 需 要 变 更 此 上 限 可 以 通 过 启 动 配 置 项
-Dcsp.sentinel.statistic.max.rt=xxx
来配置。 - 异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
- 异常数 (DEGRADE_GRADE_EXCEPTION_COUNT):当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态。
5.1 RT
当 1s 内持续进入 5 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以 ms为单位),那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。
5.1.1 添加测试接口
@GetMapping("/rt")
public ResponseEntity<String> rt() throws InterruptedException {
TimeUnit.SECONDS.sleep(1);
return ResponseEntity.ok("ok") ;
}
5.1.2 添加降级的规则
在 1s 内,进入 5 个请求,若相应时间都超过 500ms ,则在接下来的 3s ,都执行熔断的机制。
5.1.3 测试
打开 Apache jmeter 。
添加一个线程组:
1 s 10 个线程同时发请求。
添加一个取样器:
添加我们要测试的接口:
启动测试:
查看结果:
5.2 异常比例
当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值之后,资源进入降级状态,在接下来的时间窗口内,程序都会快速失败。
5.2.1 添加接口
@GetMapping("/exception")
public ResponseEntity<String> exception() throws InterruptedException {
throw new RuntimeException("就是不想成功!") ;
}
5.2.2 添加降级规则
上面的配置含义是:如果/exception
的 QPS 大于 5,并且每秒钟的请求异常比例大于 0.5的话,那么在未来的 3 秒钟(时间窗口)内,sentinel 断路器打开,该 api 接口不可用。
也就是说,如果一秒内有 10 个请求进来,超过 5 个以上都出错,那么会触发熔断,1秒钟内这个接口不可用。
5.2.3 测试
打开 Jmeter,修改请求的地址:
开始测试。
在浏览器打开:
http://localhost:8086/exception
直接被熔断,停止 jemter,等待 3s,在此访问:
5.3 异常数
当策略为异常数时表示:当指定时间窗口内,请求异常数大于等于某个值时,触发降级。继续使用上面的接口测试。
5.3.1 添加规则
上面的规则表示:在 60 秒内,访问/exception
请求异常的次数大于等于 5,则触发降级。
5.3.2 测试该规则
可以看到,当第 5 次访问的时候成功触发了降级。
六、热点规则
热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的数据,并对其访问进行限制。
6.1 添加一个接口
@GetMapping("/buy")
@SentinelResource("buy")
public ResponseEntity<String> buy(String prodName,Integer prodCount){
return ResponseEntity.ok("买" + prodCount + "份" + prodName );
}
6.2 添加热点的规则
对这个资源添加热点规则:
上面的配置含义是:对 buy 资源添加热点规则,当第 0 个参数的值为华为的时候 QPS阈值为 3,否则为 1。此外,如果第 0 个参数不传,那么这笔请求不受该热点规则限制。
6.3 测试效果
不是华为:
买 1 次后,里面限流。
是华为:同时买 3 次,才限流
七、系统规则
系统规则则是针对整个系统设置限流规则,并不针对某个资源,设置页面如下:
阈值类型包含以下五种:
- 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 达到阈值即触发系统保护
八、授权规则
授权规则用于配置资源的黑白名单:
述配置表示:只有 appA 和 appB 才能访问 test1 资源。
SentinelResource远程调用测试案例搭建
一、@SentinelResource 简介
Sentinel 提供了@SentinelResource 注解用于定义资源,并提供可选的异常回退和 Block 回退。
异常回退指的是@SentinelResource 注解标注的方法发生 Java 异常时的回退处理;
Block 回退指的是当@SentinelResource 资源访问不符合 Sentinel 控制台定义的规则时的回退(默认返回Blocked by Sentinel (flow limiting))。这里简单记录下该注解的用法。
二、框架的搭建
我们将搭建如图所示的测试框架:
2.1 搭建 sentinel-example
我们将在 sentinel-example 里面演示所有@SentinelResource 的的功能。
2.1.4 完整的 pom.xml 文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-alibaba-examples</artifactId>
<groupId>com.bjsxt</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<artifactId>sentinel-examples</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
</project>
2.1.1 使用 IDEA 创建一个 Maven项目
选择 Maven 项目:
点击 Next,填写以下的内容:
Parent:选择 spring-cloud-alibaba-example
Name:sentinel-example
点击 Finish,完成创建
2.1.2 添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
- Spring-boot-stater-web 是开发 web 最基础的依赖;
- spring-cloud-alibaba-nacos-discovery 是服务的发现组件
2.1.3 修改项目的打包方式
<packaging>pom</packaging>
2.2 搭建 sentinel-provider
Provide 是一个普通的服务的提供者。
2.2.1 使用 IDEA 创建一个 Maven 项目
选择Maven项目:
点击 Next,填写以下的内容:
Parent:选择 sentinel-example
Name:sentinel-provider
点击 Finish,完成创建。
2.2.2 修改项目的打包方式
我们修改项目的打包方式,以后我们可以使用 jar 来发布项目。
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
2.2.3 完整的 pom.xml 文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>sentinel-examples</artifactId>
<groupId>com.bjsxt</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sentinel-provider</artifactId>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.3 搭建 sentinel-consumer
Provide 是一个普通的服务的消费者。
2.3.1 使用 IDEA 创建一个 Maven 项目
选择 Maven 项目:
点击 Next,填写以下的内容:
Parent:选择 sentinel-example
Name:sentinel-consumer
点击 Finish,完成创建。
2.3.2 修改 pom.xml 文件
我们将在该项目里面演示@SentinelResource 的功能。
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
</dependencies>
我们修改项目的打包方式,以后我们可以使用 jar 来发布项目。
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
2.3.3 完整的 pom.xml 文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>sentinel-examples</artifactId>
<groupId>com.bjsxt</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sentinel-consumer</artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
三、完善 sentinel-provider
我们在 provider 里面添加一个模拟数据接口。
3.1 添加一个数据接口
代码如下:
@RestController
public class GoodsController {
@GetMapping("/goods/buy/{name}/{count}")
public ResponseEntity<String> buy(
@PathVariable("name") String name,
@PathVariable("count") Integer count) {
return ResponseEntity.ok(String.format("购买%d 份%s", count, name));
}
}
3.2 添加配置文件
server:
port: 8081
spring:
application:
name: sentinel-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848
仅仅是为了让服务能注册到注册中心而已。
3.3 添加一个启动类
代码如下:
@SpringBootApplication
@EnableDiscoveryClient
public class SentinelProviderApplication {
public static void main(String[] args) {
SpringApplication.run(SentinelProviderApplication.class ,args) ;
}
}
3.4 启动测试
在启动之前,我们必须保证 Nacos 已经启动成功。
测试接口:
http://localhost:8081/goods/buy/huawei/1
四、完善 sentinel-consumer
4.1 调用服务提供者
代码如下:
@RestController
public class BuyController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("buy/{name}/{count}")
@SentinelResource(value = "buy", fallback = "buyFallback", blockHandler =
"buyBlock")
public ResponseEntity<String> buy(@PathVariable String name, @PathVariable
Integer count) {
if (count >= 20) {
throw new IllegalArgumentException("购买数量过多");
}
if ("miband".equalsIgnoreCase(name)) {
throw new NullPointerException("已售罄");
}
Map<String, Object> params = new HashMap<>(2);
params.put("name", name);
params.put("count", count);
return ResponseEntity.ok(
this.restTemplate.getForEntity("http://sentinel-provider/goods/buy/{name}/{count}", String.class, params).getBody());
}
// 异常回退
public ResponseEntity<String> buyFallback(@PathVariable String name,
@PathVariable Integer count, Throwable throwable) {
return ResponseEntity.ok(
String.format("【进入 fallback 方法】购买%d 份%s 失败,%s", count,name, throwable.getMessage()));
}
// sentinel 回退
public ResponseEntity<String> buyBlock(@PathVariable String name,
@PathVariable Integer count, BlockException e) {
return ResponseEntity.ok(String.format("【进入 blockHandler 方法】购买%d份%s 失败,当前购买人数过多,请稍后再试", count, name));
}
}
4.2 添加配置文件
新建配置文件:
内容如下:
server:
port: 8083
spring:
application:
name: sentinel-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719
4.3 添加启动类
@SpringBootApplication
@EnableDiscoveryClient
public class SentinelConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(SentinelConsumerApplication.class ,args) ;
}
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate() ;
}
}
4.4 启动测试
4.4.1 启动软件
在启动之前,必须保证这些软件已经启动:
- Nacos-Server
- Sentinel-Dashboard
- Sentinel-provider
准备就绪后,启动 sentine-consumer:
访问接口测试:
http://192.168.1.11:8083/buy/huawei/1
4.4.2 添加流控的规则
当访问该资源,QPS 超过 2 时,抛出异常。
测试:
http://192.168.1.11:8083/buy/huawei/1
五、其他用法
5.1 说明
在当前类中编写回退方法会使得代码变得冗余耦合度高,我们可以将回退方法抽取出来到一个指定类中。
5.2 新增 BuyBlockHandler
代码如下:
public class BuyBlockHandler {
// sentinel 回退
public static String buyBlock(@PathVariable String name, @PathVariable
Integer count, BlockException e) {
return String.format("【进入 blockHandler 方法】购买%d 份%s 失败,当前购买人数过多,请稍后再试", count, name);
}
}
5.3 新增 BuyFallBack
代码如下:
public class BuyBlockHandler {
// sentinel 回退
public static ResponseEntity<String> buyBlock(@PathVariable String name,
@PathVariable Integer count, BlockException e) {
return ResponseEntity.ok(String.format("【进入 blockHandler 方法】购买%d份%s 失败,当前购买人数过多,请稍后再试", count, name));
}
}
5.4 改造 BuyController
@RestController
public class BuyController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("buy/{name}/{count}")
@SentinelResource(
value = "buy",
fallback = "buyFallback",
fallbackClass = BuyFallBack.class ,
blockHandler = "buyBlock",
blockHandlerClass = BuyBlockHandler.class ,
)
public ResponseEntity<String> buy(@PathVariable String name, @PathVariable
Integer count) {
if (count >= 20) {
throw new IllegalArgumentException("购买数量过多");
}
if ("miband".equalsIgnoreCase(name)) {
throw new NullPointerException("已售罄");
}
Map<String, Object> params = new HashMap<>(2);
params.put("name", name);
params.put("count", count);
return ResponseEntity.ok(
this.restTemplate.getForEntity("http://sentinel-provider/goods/buy/{name}/{
count}", String.class, params).getBody());
}
}
Spring Cloud Alibaba Seata
一、Seata 简介
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双 11,对各 BU 业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。
二、Seata-Server 的安装
在使用 Seata 之前,我们首先要安装 Seata-Server 服务器。
2.1 下载 Seata
由于我们使用的 spring cloud alibaba 版本为 2.2.0.RELEASE,他里面控制了 seata 的版本为1.0.0,故我们在此下载 1.0.0 版本的 seata。
访问:
https://github.com/seata/seata/releases/tag/v1.0.0
由于我使用的是 windows 的电脑,故选择 seata-server-1.0.0.zip 该版本。
点击该文件下载。
2.2 Seata-Server 目录分析
将 seata-server 复制到软件的目录里面,使用解压工具解压该文件。Bin
:可执行文件目录Conf
:配置文件目录lib
:依赖的 jarLICENSE
:授权文件
2.3 Seata 启动
进入{seata}/bin
目录里面,双击:
代表 seata-server 已经启动成功。
三、框架的搭建
在本示例中,我们模拟了一个用户购买货物的场景:
- StorageService 负责扣减库存数量;
- OrderService 负责保存订单;
- AccountService 负责扣减用户账户余额;
- Business 负责用户下单的整个流程处理;
3.1 搭建 seata-examples 项目
seata-examples 用来控制所有项目的依赖版本号,以及去除公共的依赖 seata。
3.1.1 使用 IDEA 创建 Maven 项目
选择Maven项目:
点击 Next,添加以下的信息:
Parent:选择 spring-cloud-alibaba-examples
Name:seata-examples
其他的信息保持默认即可。然后点击 Finish 即可完成创建。
3.1.2 添加依赖
打开项目的 pom.xml 文件,添加以下的依赖。
<dependencies>
<!-- 服务注册-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- web 项目的基础依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
3.1.3 完整的 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-alibaba-examples</artifactId>
<groupId>com.bjsxt</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-examples</artifactId>
<dependencies>
<!-- 服务注册-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- web 项目的基础依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
最后,我们的项目的依赖关系如下:
可以看见,我们的 seata 版本为 1.0.0。
3.2 搭建 account-service 项目
Account-service 项目将负责扣减用户账户余额
3.2.1 使用 IDEA 创建一个 Maven 项目
选择 Maven 项目:
点击 Next 后,填写以下的信息:
Parent:seata-examples
Name:account-service
其他的值保持默认即可。
3.2.2 添加依赖
我们需要使用 ORM 框架来完成对数据库的操作。在次我们需要使用 Mybatis-Plus 来操作数据库。
打开 pom.xml ,在里面添加如下内容:
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>
<!--MySQL 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
为了以后我们打包发布我们的项目,在此我们添加 boot-maven 的打包插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
为了以后我们打包发布我们的项目,在此我们添加 boot-maven 的打包插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
3.2.3 完整的 pom.xml 文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>seata-examples</artifactId>
<groupId>com.bjsxt</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>account-service</artifactId>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>2.3</version>
</dependency>
<!--MySQL 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
项目的依赖关系如下:
3.3 搭建 business-service 项目
在 business 将主要完成下单逻辑,包含库存的扣减,订单的创建。
3.3.1 使用 IDEA 创建一个 Maven 项目
选择 Maven 项目:
点击 Next 后,填写以下的信息:
Parent:seata-examples
Name:business-service
其他的值保持默认即可。
3.3.2 添加依赖
为了以后我们打包发布我们的项目,在此我们添加 boot-maven 的打包插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
3.2.3 完整的 pom.xml 文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>seata-examples</artifactId>
<groupId>com.bjsxt</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>account-service</artifactId>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>2.3</version>
</dependency>
<!--MySQL 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
项目的依赖关系如下:
3.4 搭建 order-service 项目
order-service 项目将负责保存用户订单。
3.4.1 使用 IDEA 创建一个 Maven 项目
选择 Maven 项目:
点击 Next 后,填写以下的信息:
Parent:seata-examples
Name:order-service
其他的值保持默认即可。
3.4.2 添加依赖
我们需要使用 ORM 框架来完成对数据库的操作。在次我们需要使用 Mybatis-Plus 来操作数据库。
打开 pom.xml ,在里面添加如下内容:
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>
<!--MySQL 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
为了以后我们打包发布我们的项目,在此我们添加 boot-maven 的打包插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
3.4.3 完整的 pom.xml 文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>seata-examples</artifactId>
<groupId>com.bjsxt</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>order-service</artifactId>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>2.3</version>
</dependency>
<!--MySQL 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
项目的依赖关系如下:
3.5 搭建 storage-service 项目
storage-service 将负责扣除商品的库存。
3.5.1 使用 IDEA 创建一个 Maven 项目
选择Maven项目:
点击 Next 后,填写以下的信息:
Parent:seata-examples
Name:storage-service
其他的值保持默认即可。
3.5.2 添加依赖
我们需要使用 ORM 框架来完成对数据库的操作。在次我们需要使用 Mybatis-Plus 来操作数据库。
打开 pom.xml ,在里面添加如下内容:
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>
<!--MySQL 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
为了以后我们打包发布我们的项目,在此我们添加 boot-maven 的打包插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
3.5.3 完整的 pom.xml 文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>seata-examples</artifactId>
<groupId>com.bjsxt</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>order-service</artifactId>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>2.3</version>
</dependency>
<!--MySQL 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
项目的依赖关系如下:
3.6 完整的项目的案例为
四、代码的完善
4.1 数据库表导入
在测试分布式事务之前,我们需要先设计数据库,以及准备测试数据。
新建数据库,命名为:seata
导入 Sql:
导入该 sql:
点击开始,进行导入。
成功后,发现成功的条数为:
表有如下:
account :用户的账号表
Order:订单表;
Stoage:商品的库存表;
undo_log:回滚事务表,SEATA AT 模式需要 UNDO_LOG 表。
4.2 模型对象和 Mapper 对象生成
使用 IDEA 连接数据库:
设置时区:
选择默认的即可,单击ok
成功后,如图所示:
执行代码的生成:
-
Account_tbl:
-
Order_tbl:
-
Storage_tbl:
代码生成完毕后:
4.3 storage-service 代码的完善
4.3.1 接口设计
在 storage-service 里面,主要完成对库存的扣减。
新建一个接口:
命名为:StorageService,代码如下:
代码如下:
public interface StorageService {
/**
* 扣减商品的库存
* @param commodityCode
* 商品的编码
* @param count
* 扣减商品的数量
*/
void deduct(String commodityCode, int count);
}
4.3.2 实现该接口
名称为:impl.StorageService
,代码的实现如下:
@Service
public class StorageServiceImpl implements StorageService {
private static Logger logger = LoggerFactory.getLogger(StorageServiceImpl.class);
@Autowired
private StorageTblMapper storageTblMapper;
@Override
public void deduct(String commodityCode, int count) {
logger.info("开始扣减库存,商品编码:{},数量:{}", commodityCode, count);
StorageTbl storageTbl = storageTblMapper.selectOne(
new LambdaQueryWrapper<StorageTbl>()
.eq(StorageTbl::getCommodityCode, commodityCode));
int idleCount = storageTbl.getCount() - count;
if (idleCount < 0) {
throw new RuntimeException("库存不足");
}
storageTbl.setCount(idleCount);
storageTblMapper.updateById(storageTbl);
logger.info("库存扣减成功,商品编码:{},剩余数量:{}", commodityCode, idleCount);
}
}
4.3.3 使用 Restful 暴露此接口
添加一个 Controller
代码如下:
@RestController
public class StorageController {
private static Logger logger = LoggerFactory.getLogger(StorageController.class) ;
@Autowired
private StorageService storageService ;
/**
* 扣减商品的库存
* @param commodityCode 商品的编码
* @param count 商品的数量
* @return
*/
@GetMapping("/deduct/{commodityCode}/{count}")
public ResponseEntity<Void> deduct(
@PathVariable("commodityCode") String commodityCode,
@PathVariable("count") Integer count){
logger.info("Account Service ... xid: " + RootContext.getXID());
// 开始扣减库存
storageService.deduct(commodityCode , count);
return ResponseEntity.ok().build() ;
}
}
4.3.4 添加配置文件
在 resource 目录里面新建配置文件:
内容如下:
server:
port: 18084
spring:
application:
name: storage-service
cloud:
alibaba:
seata:
tx-service-group: storage-service
nacos:
discovery:
server-addr: localhost:8848
datasource:
name: storageDataSource
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url:
jdbc:mysql://localhost:3306/seata?useSSL=false&serverTimezone=UTC
druid:
max-active: 20
min-idle: 2
initial-size: 2
seata:
service:
vgroup-mapping:
account-service: default
grouplist:
default: 127.0.0.1:8091
disable-global-transaction: false
enabled: true
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
4.3.5 添加启动类
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.bjsxt.mapper")
public class StorageServiceApplication {
public static void main(String[] args) {
SpringApplication.run(StorageServiceApplication.class ,args) ;
}
}
4.3.6 启动项目测试
启动项目后,打印该日志,说明连接 seata-server 成功。
4.4 account-service 代码的完善
4.4.1 接口设计
在 account-service 里面,主要完成对用户余扣减。
新建一个接口:
命名为:AccountService,代码如下:
代码如下:
public interface AccountService {
/**
* 从用户的账号扣减金额
* @param userId
* 用户的 Id
* @param money
* 金额
*/
void debit(String userId, int money);
}
4.4.2 实现该接口
名称为:impl.StorageService,代码的实现如下:
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountTblMapper accountTblMapper;
private static Logger logger = LoggerFactory.getLogger(AccountServiceImpl.class);
@Override
public void debit(String userId, int money) {
logger.info("准备扣减用户:{} 余额,扣减的数目为:{}", userId, money);
AccountTbl accountTbl = accountTblMapper.selectOne(
new LambdaQueryWrapper<AccountTbl>()
.eq(AccountTbl::getUserId, userId));
int idleMoney = accountTbl.getMoney() - money;
if (idleMoney < 0) {
throw new RuntimeException("用户余额不足");
}
accountTbl.setMoney(idleMoney);
accountTblMapper.updateById(accountTbl);
logger.info("扣减用户{}金额成功,剩余金额为{}", userId, money);
}
}
4.4.3 使用 Restful 暴露此接口
添加一个 Controller
名称为:
代码如下:
@RestController
public class AccountController {
@Autowired
private AccountService accountService ;
private static Logger logger =
LoggerFactory.getLogger(AccountController.class) ;
@GetMapping("/debit/{userId}/{money}")
public ResponseEntity<Void> debit(
@PathVariable("userId") String userId,
@PathVariable("money") Integer money){
logger.info("Account Service ... xid: " + RootContext.getXID());
// 开始扣减余额
accountService.debit(userId , money);
return ResponseEntity.ok().build() ;
}
}
4.4.4 添加配置文件
在 resource 目录里面新建配置文件:
内容如下:
server:
port: 18085
spring:
application:
name: account-service
cloud:
alibaba:
seata:
tx-service-group: account-service
nacos:
discovery:
server-addr: localhost:8848
datasource:
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata?useSSL=false&serverTimezone=UTC
druid:
max-active: 20
min-idle: 2
initial-size: 2
seata:
service:
vgroup-mapping:
account-service: default
grouplist:
default: 127.0.0.1:8091
disable-global-transaction: false
enabled: true
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
4.4.5 添加启动类
命名为 AccountServiceApplication ,代码如下:
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.bjsxt.mapper")
public class AccoutServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AccoutServiceApplication.class ,args) ;
}
}
4.4.6 启动项目测试
启动项目后,打印该日志,说明连接 seata-server 成功。
4.5 order-service 代码的完善
4.5.1 接口设计
在 order-service 里面,主要完成保存用户订单的操作。
新建一个接口:
命名为:OrderService,代码如下:
代码如下:
public interface OrderService {
/**
* 创建一个订单
* @param userId 用户 id
* @param commodityCode 商品的编号
* @param orderCount 商品的数量
* @return OrderTbl
*/
OrderTbl create(String userId, String commodityCode, int orderCount) ;
}
4.5.4 Ribbon 集成
创建一个配置类:
代码如下:
@Configuration
public class HttpUtilConfig {
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate() ;
}
}
4.5.2 实现该接口
名称为:impl.OrderService,代码的实现如下:
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderTblMapper orderTblMapper;
@Autowired
private AccountService accountService;
private static Logger logger =
LoggerFactory.getLogger(OrderServiceImpl.class);
@Override
public OrderTbl create(String userId, String commodityCode, int orderCount)
{
logger.info("准备为{}创建一个订单,商品编号为{},数量为{}", userId,
commodityCode, orderCount);
// 1 计算总金额
int orderMoney = calculate(commodityCode, orderCount);
accountService.debit(userId, orderMoney);
OrderTbl order = new OrderTbl();
order.setUserId(userId);
order.setCommodityCode(commodityCode);
order.setCount(orderCount);
order.setMoney(orderMoney);
orderTblMapper.insert(order);
// INSERT INTO orders ...
return order;
}
private int calculate(String commodityCode, int orderCount) {
// 我们现在没有商品的表,在此我们把商品的价格定死
int prodPrice = 0 ;
if("HUAWEI_0001".equals(commodityCode)){ // 华为时 100
prodPrice = 100;
}else if ("XIAOMI_002".equals(commodityCode)){ // 小米时 200
prodPrice = 200 ;
}else {
prodPrice = 1000 ; // 其他为 1000
}
return orderCount * prodPrice ;
}
}
4.5.3 远程调用 account-service 的实现
创建一个 AccountService 的类,该类里面主要完成对 accout-servic 的远程调用。
名称为:
/**
* 实现对账号服务的远程调用
*/
@Service
public class AccountService {
private static Logger logger = LoggerFactory.getLogger(AccountService.class) ;
/**
* 1 ribbon 的方式
*/
@Autowired
private RestTemplate restTemplate ;
/**
* 2 feign 的方式
*/
public void debit(String userId, int orderMoney) {
ResponseEntity<Void> entity = restTemplate.
getForEntity(
"http://accout-service/debit/{userId}/{orderMoney}",
Void.class,
userId,
orderMoney
);
if(entity.getStatusCode()== HttpStatus.OK){
logger.info("扣减用户{}金额成功,本次扣减的数目为{}",userId,orderMoney);
return ;
}
logger.info("扣减用户{}金额失败",userId);
throw new RuntimeException("扣减金额失败") ;
}
}
我们在此使用的时 Ribbon 做远程调用,下面的章节我们也会测试 Feign 。
4.5.5 使用 Restful 暴露此接口
添加一个 Controller
命名为:
代码如下:
@RestController
public class OrderController {
private static Logger logger = LoggerFactory.getLogger(OrderController.class) ;
@Autowired
private OrderService orderService ;
/**
* 创建订单
* @param userId
* 用户 Id
* @param commodityCode
* 商品的编号
* @param orderCount
* 商品的数量
* @return
*/
@GetMapping("/create/{userId}/{commodityCode}/{orderCount}")
public ResponseEntity<Void> create(
@PathVariable("userId") String userId,
@PathVariable("commodityCode") String commodityCode,
@PathVariable("orderCount") int orderCount){
logger.info("Order Service ... xid: " + RootContext.getXID());
orderService.create(userId, commodityCode, orderCount) ;
return ResponseEntity.ok().build() ;
}
}
4.5.6 添加配置文件
在 resource 目录里面新建配置文件:
命名为:application.yml
内容如下:
server:
port: 18086
spring:
application:
name: order-service
cloud:
alibaba:
seata:
tx-service-group: order-service
nacos:
discovery:
server-addr: localhost:8848
datasource:
name: orderDataSource
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url:
jdbc:mysql://localhost:3306/seata?useSSL=false&serverTimezone=UTC
druid:
max-active: 20
min-idle: 2
initial-size: 2
seata:
service:
vgroup-mapping:
order-service: default
grouplist:
default: 127.0.0.1:8091
disable-global-transaction: false
enabled: true
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
4.5.7 添加启动类
命名为:OrderServiceApplication
代码如下:
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.bjsxt.mapper")
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class ,args) ;
}
}
4.58 启动项目测试
启动项目后,打印该日志,说明连接 seata-server 成功。
4.6 business-service 代码的完善
4.6.1 接口设计
在 business-service 里面,主要完成下单的逻辑,包含 2 个主要的步骤,就是对库存服务和订单服务的远程调用。
新建一个接口:
命名为:com.bjsxt.service.BusinessService
代码如下:
public interface BusinessService {
/**
* 采购/下单的过程
* @param userId
* 用户的 Id
* @param commodityCode
* 商品的编码
* @param orderCount
* 商品的数量
*/
void purchase(String userId, String commodityCode, int orderCount) ;
}
4.6.2 实现该接口
名称为:impl.BusinessServiceImpl,代码的实现如下:
@Service
public class BusinessServiceImpl implements BusinessService {
private static Logger logger = LoggerFactory.getLogger(BusinessServiceImpl.class) ;
@Autowired
private StorageService storageService;
@Autowired
private OrderService orderService;
@Override
public void purchase(String userId, String commodityCode, int orderCount) {
logger.info("准备下单,用户:{},商品:{},数量:{}",userId,commodityCode,orderCount);
storageService.deduct(commodityCode, orderCount);
orderService.create(userId, commodityCode, orderCount) ;
logger.info("下单完成");
}
}
4.6.3 远程调用 storage-service 的实现
创建一个 StorageService 的类,该类里面主要完成对 storage-servic 的远程调用。
名称为:
@Service
public class StorageService {
private static Logger logger = LoggerFactory.getLogger(StorageService.class) ;
/**
* 1 采用 Ribbon 的形式
*/
@Autowired
private RestTemplate restTemplate ;
/**
* 2 采用 Feign 的形式
*/
public void deduct(String commodityCode, int orderCount) {
ResponseEntity<Void> entity = restTemplate.
getForEntity(
"http://storage-service/debut/{commodityCode}/{orderCount}",
Void.class,
commodityCode,
orderCount
);
if (entity.getStatusCode()== HttpStatus.OK){
logger.info("扣减库存成功,商品编号为{},本次扣减的数量为{}",commodityCode,orderCount);
return;
}
throw new RuntimeException("扣减库存失败") ;
}
}
我们在此使用的时 Ribbon 做远程调用,下面的章节我们也会测试 Feign 。
4.6.4 远程调用 order-service 的实现
新建一个类:
代码如下:
@Service
public class OrderService {
private static Logger logger = LoggerFactory.getLogger(StorageService.class) ;
/**
* 1 采用 Ribbon 的形式
*/
@Autowired
private RestTemplate restTemplate ;
/**
* 2 采用 Feign 的形式
*/
public void create(String userId, String commodityCode, int orderCount) {
ResponseEntity<Void> entity = restTemplate.
getForEntity(
"http://order-service/create/{userId}/{commodityCode}/{orderCount}",
Void.class,
userId ,
commodityCode,
orderCount
);
if (entity.getStatusCode()== HttpStatus.OK){
logger.info("订单创建成功,用户为{} ,商品编号为{},本次扣减的数量为{}",userId ,
commodityCode,orderCount);
return;
}
throw new RuntimeException("订单创建失败") ;
}
}
4.6.5 集成 Ribbon
添加一个 HttpUtilConfig 的配置类:
代码如下:
@Configuration
public class HttpUtilConfig {
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate() ;
}
}
4.6.6 添加配置文件
在 resource 目录里面新建配置文件:
命名为:application.yml
内容如下:
server:
port: 18087
spring:
application:
name: business-service
cloud:
alibaba:
seata:
tx-service-group: business-service
nacos:
discovery:
server-addr: localhost:8848
seata:
service:
vgroup-mapping:
business-service: default
grouplist:
default: 127.0.0.1:8091
disable-global-transaction: false
enabled: tru
4.6.7 添加启动类
命名为:BusinessServiceApplication
代码如下:
@SpringBootApplication
@EnableDiscoveryClient
public class BusinessServiceApplication {
public static void main(String[] args) {
SpringApplication.run(BusinessServiceApplication.class ,args) ;
}
}
4.6.8 暴露下单接口
继续改造启动类:
@SpringBootApplication
@EnableDiscoveryClient
@RestController
public class BusinessServiceApplication {
@Autowired
private BusinessService businessService ;
public static void main(String[] args) {
SpringApplication.run(BusinessServiceApplication.class ,args) ;
}
/**
* 开始下单
* @param userId
* 用户的 Id
* @param commodityCode
* 商品的编号
* @param orderCount
* 商品的数量
* @return
*/
@GetMapping("/purchase/{userId}/{commodityCode}/{orderCount}")
public ResponseEntity<Void> purchase(
@PathVariable("userId") String userId,
@PathVariable("commodityCode")String commodityCode,
@PathVariable("orderCount")Integer orderCount){
businessService.purchase(userId,commodityCode,orderCount);
return ResponseEntity.ok().build() ;
}
}
4.6.9 启动项目测试
启动项目后,打印该日志,说明连接 seata-server 成功。
4.7 总体的调用流程如下
都启动完成后:
Nacos-Server:
4.8 正常下单测试
在浏览器里面访问:
http://localhost:18087/purchase/SXT_USER_1/HUAWEI_0001/1
代表 SXT_USER_1 购买 HUAWEI_0001 产品 1 件。
数据库里面:
- Accout_tbl 里面,SXT_USER_1 用户的金额减少 100;
- Storage_tbl 里面,HUAWEI_0001 的库存减少了1;
- Order_Tbl 里面,创建了一条订单记录;
说明,此时远程调用时正常的。
4.9 分布式事务的演示
我们演示如图的异常:
我们可以发现,远程调用共有 3 处。
4.9.1 在 accout-service 服务扣减余额触发异常
4.9.2 重启 accout-service
4.9.3 还原数据库里面的数据
Account_Tbl:
Storage_Tbl:
4.9.4 重新下单测试
http://localhost:18087/purchase/SXT_USER_1/HUAWEI_0001/1
数据库的数据:
Account_Tbl:
Storage_Tbl:
我们发现,分布式事务产生了,accout-service 内部的异常,导致 accout_tbl 表数据回滚了。
但是,在 storage_tbl :位于 stoage-service 的事务却没有回滚。
4.10 使用 Seata 解决分布式问题
4.10.1 改造 accout-service 里面的 AccountServiceImpl
当用户的 ID 为:SXT_USER_2 时,我们抛出异常,当为其他用户时,我们正常的下单。
4.10.2 改造 BusinessServiceImpl
添加一个注解,看他是否能解决分布式事务的问题
4.10.3 重启测试
重启 accout-service,business-service 测试
使用 SXT_USER_1 正常的下单测试:
Stoage_tbl:库存正常
Accout_Tbl:余额正常
使用 SXT_USER_2 下单测试:
发现发生异常后,
stoage_tbl 里面的没有发生改变,数据正常
Accout_tbl 里面的数据也没有发生改变,数据正常
分布式事务测试成功了
五、集成 Feign 测试 Seata
在上面的章节中,我们使用的时 Ribbon + RestTemplate 的形式做的远程调用。下面我们来演
示 Feign 的调用方式。
5.1 改造 business-service
5.1.1 添加依赖
修改 business-service 项目里面的 pom.xml 文件,在里面添加依赖。
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
5.1.2 添加 OrderServiceFeign
里面的代码如下:
@FeignClient("order-service")
public interface OrderServiceFeign {
@GetMapping("/create/{userId}/{commodityCode}/{orderCount}")
ResponseEntity<Void> create(
@PathVariable("userId") String userId,
@PathVariable("commodityCode") String commodityCode,
@PathVariable("orderCount") Integer orderCount);
}
5.1.3 添加 StorageServiceFeign
@FeignClient("storage-service")
public interface StorageServiceFeign {
@GetMapping("/deduct/{commodityCode}/{orderCount}")
ResponseEntity<Void> deduct(
@PathVariable("commodityCode") String commodityCode,
@PathVariable("orderCount") Integer orderCount
) ;
}
5.1.5 改造 OrderService
@Service
public class OrderService {
private static Logger logger = LoggerFactory.getLogger(StorageService.class) ;
/**
* 1 采用 Ribbon 的形式
*/
@Autowired
private RestTemplate restTemplate ;
@Autowired
private OrderServiceFeign orderServiceFeign ;
/**
* 2 采用 Feign 的形式
*/
public void create(String userId, String commodityCode, int orderCount){
// Ribbon
// ResponseEntity<Void> entity = restTemplate.
// getForEntity(
// "http://order-service/create/{userId}/{commodityCode}/{orderCount}",
// Void.class,
// userId ,
// commodityCode,
// orderCount
// );
//Feign
ResponseEntity<Void> entity = orderServiceFeign.create(userId, commodityCode,
orderCount);
if (entity.getStatusCode()== HttpStatus.OK){
logger.info("订单创建成功,用户为{} ,商品编号为{},本次扣减的数量为{}",userId ,commodityCode,orderCount);
return;
}
throw new RuntimeException("订单创建失败") ;
}
}
5.1.7 改造 StorageService
代码如下:
@Service
public class StorageService {
private static Logger logger = LoggerFactory.getLogger(StorageService.class);
/**
* 1 采用 Ribbon 的形式
*/
@Autowired
private RestTemplate restTemplate;
@Autowired
private StorageServiceFeign storageServiceFeign;
/**
* 2 采用 Feign 的形式
*/
public void deduct(String commodityCode, int orderCount) {
// Ribbon
// ResponseEntity<Void> entity = restTemplate.
// getForEntity(
// "http://storage-service/deduct/{commodityCode}/{orderCount}",
// Void.class,
// commodityCode,
// orderCount
// );
//Feign
ResponseEntity<Void> entity = storageServiceFeign.deduct(commodityCode,
orderCount);
if (entity.getStatusCode() == HttpStatus.OK) {
logger.info("扣减库存成功,商品编号为{},本次扣减的数量为{}", commodityCode,
orderCount);
return;
}
throw new RuntimeException("扣减库存失败");
}
}
5.1.6 在启动类里面开启对 Feign 的支持
5.2 改造 order-service
5.2.1 添加依赖
在 dependencies 添加:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
5.2.2 添加接口
里面的代码如下:
@FeignClient("account-service")
public interface AccountServiceFeign {
@GetMapping("/debit/{userId}/{orderMoney}")
ResponseEntity<Void> debit(
@PathVariable("userId") String userId,
@PathVariable("orderMoney") Integer orderMoney
) ;
}
5.2.3 修改 AccoutService
/**
* 实现对账号服务的远程调用
*/
@Service
public class AccountService {
private static Logger logger = LoggerFactory.getLogger(AccountService.class) ;
/**
* 1 ribbon 的方式
*/
@Autowired
private RestTemplate restTemplate ;
@Autowired
private AccountServiceFeign accountServiceFeign ;
/**
* 2 feign 的方式
*/
public void debit(String userId, int orderMoney) {
//Ribbon
// ResponseEntity<Void> entity = restTemplate.
// getForEntity(
// "http://accout-service/debit/{userId}/{orderMoney}",
// Void.class,
// userId,
// orderMoney
// );
ResponseEntity<Void> entity = accountServiceFeign.debit(userId, orderMoney);
if(entity.getStatusCode()== HttpStatus.OK){
logger.info("扣减用户{}金额成功,本次扣减的数目为{}",userId,orderMoney);
return ;
}
logger.info("扣减用户{}金额失败",userId);
throw new RuntimeException("扣减金额失败") ;
}
}
5.2.4 在启动类里面添加对 Feign 的支持
5.3 重启测试
重启 order-service ,business-service
还原数据库数据,开始测试。
正常下单测试:
使用 SXT_USER_2 下单:
出错了,但是数据库的各个表都正常。
Seata 测试成功了。