赞
踩
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。 Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:
Sentinel 提供一个轻量级的开源控制台,它提供机器发现以及健康情况管理、监控(单机和集群),规则管理和推送的功能。本节将详细记录何如通过 Sentinel 控制台控制 Sentinel 客户端的各种行为。Sentinel 控制台的功能主要包括:流量控制、降级控制、热点配置、系统规则和授权规则等。
访问:https://github.com/alibaba/Sentinel/releases
找到:1.7.1 版本:
点击 sentinel-dashboard-1.7.1.jar 完成下载:
将下载好的 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 项目结构:
选择Maven项目:
点击 Next:
Parent:选择 spring-cloud-alibaba-examples
Name:命名为 sentinel-example-client
其他的项保持默认值即可。
点击 Finish 完成创建。
修改 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 个依赖:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
这样,我们的项目打包好了后,可以使用java -jar
来直接运行了。
<?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>
命名为:
修改该配置文件,添加以下的配置:
server:
port: 8085
spring:
application:
name: sentinel-client
cloud:
sentinel:
transport:
dashboard: localhost:8080
port: 8719 #sentinel-client 监听,因为sentinel-dashboard将从这个端口来采集数据,若端口被占用+1
其中:
名称为:com.bjsxt.SentinelClientApplication
添加如下的代码:
@SpringBootApplication
public class SentinelClientApplication {
public static void main(String[] args) {
SpringApplication.run(SentinelClientApplication.class ,args) ;
}
}
名称为:controller.TestController
在 TestContrller 里面添加如下接口:
@RestController
public class TestController {
@GetMapping("/hello")
public ResponseEntity<String> hello(){
return ResponseEntity.ok("hello,sentinel") ;
}
}
在浏览器访问:
http://localhost:8080/#/dashboard/home
出现:
发现并没有任何的功能。
此时,我们访问一下我们写的 hello 接口:
http://localhost:8085/hello
多访问几次。
再次访问:
http://localhost:8080/#/dashboard/home
控制台已经显示正常了。
并且,在簇点链路中可以看到刚刚那笔请求,我们可以对它进行流控、降级、授权、热点等
配置(控制台是懒加载的,如果没有任何请求,那么控制台也不会有任何内容)。
流量的控制规则。
在簇点链路列表中,点击/hello
后面的流控按钮:
出现:
演示下 QPS 直接失败设置及效果。点击簇点链路列表中/hello 请求后面的流控按钮:
上面设置的效果是,1 秒钟内请求/hello 资源的次数达到 2 次以上的时候,进行限流。
点击新增完成该规则的设置。
现在,在浏览器访问:
http://localhost:8085/hello
当手速快点的时候(1 秒超过 2 次),页面返回 Blocked by Sentinel (flow limiting)。并且响应码为 429。
在 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 的效果。
点击新增,完成创建。
浏览器快速的访问:
http://localhost:8085/thread
访问某个接口达到一定的流控规则后,开始限制本接口。
@GetMapping("/test1")
public ResponseEntity<String> test1(){
return ResponseEntity.ok("hello,test1") ;
}
@GetMapping("/test2")
public ResponseEntity<String> test2(){
return ResponseEntity.ok("hello,test2") ;
}
重启项目,正常的访问 test1,test2 测试:
我们想让 test1 关联 test2,也就是说,访问 test2 接口达到某种规则后,开始限流 test1 。
上述流控规则表示:当 1 秒内访问 /test2 的次数大于 2 的时候,限流 /test1。
我们使用打开 2 个网页,密集访问/test2,然后我们手动浏览器请求/test1,看看效果。
访问 test1:
发现已经开始限流了。
程序的调用可以看成为一条链路。当触发链路的某条规则后,该链路被限流。
上图从 API 出发,可以发现有 2 条链路,分别为:
2 条链路在调用上,限流规则互补影响。
名称为:(service.TestService)
代码为:
@Service
public class TestService {
public String hello(){
return "hello" ;
}
}
@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())) ;
}
我们现在把 TestService 里面的 hello 方法变成一个资源:
注意:
@SentinelResource(“hello”)将该方法标识为一个 sentinel 资源,名称为 hello。
然后重启测试:
点击簇点链路:
此时我们给 hello 该资源限流:
在入口资源,我们使用的是 link1,点击新增,完成对规则的添加。
上述配置的意思是,当通过/link1
访问 hello 的时候,QPS 大于 2 则进行限流;言外之意就是/link
访问 hello 请求并不受影响。
打开 link1,link2接口。
快速访问 link1,再访问 link2:
访问测试 n 次,发现还是不能成功。难受!
具体的错误:
https://github.com/alibaba/Sentinel/issues/1213
流控效果除了直接失败外,我们也可以选择预热 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 下)。
排队等待方式不会拒绝请求,而是严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过。
在 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 种:
-Dcsp.sentinel.statistic.max.rt=xxx
来配置。当 1s 内持续进入 5 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以 ms为单位),那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。
@GetMapping("/rt")
public ResponseEntity<String> rt() throws InterruptedException {
TimeUnit.SECONDS.sleep(1);
return ResponseEntity.ok("ok") ;
}
在 1s 内,进入 5 个请求,若相应时间都超过 500ms ,则在接下来的 3s ,都执行熔断的机制。
打开 Apache jmeter 。
添加一个线程组:
1 s 10 个线程同时发请求。
添加一个取样器:
添加我们要测试的接口:
启动测试:
查看结果:
当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值之后,资源进入降级状态,在接下来的时间窗口内,程序都会快速失败。
@GetMapping("/exception")
public ResponseEntity<String> exception() throws InterruptedException {
throw new RuntimeException("就是不想成功!") ;
}
上面的配置含义是:如果/exception
的 QPS 大于 5,并且每秒钟的请求异常比例大于 0.5的话,那么在未来的 3 秒钟(时间窗口)内,sentinel 断路器打开,该 api 接口不可用。
也就是说,如果一秒内有 10 个请求进来,超过 5 个以上都出错,那么会触发熔断,1秒钟内这个接口不可用。
打开 Jmeter,修改请求的地址:
开始测试。
在浏览器打开:
http://localhost:8086/exception
直接被熔断,停止 jemter,等待 3s,在此访问:
当策略为异常数时表示:当指定时间窗口内,请求异常数大于等于某个值时,触发降级。继续使用上面的接口测试。
上面的规则表示:在 60 秒内,访问/exception
请求异常的次数大于等于 5,则触发降级。
可以看到,当第 5 次访问的时候成功触发了降级。
热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的数据,并对其访问进行限制。
@GetMapping("/buy")
@SentinelResource("buy")
public ResponseEntity<String> buy(String prodName,Integer prodCount){
return ResponseEntity.ok("买" + prodCount + "份" + prodName );
}
对这个资源添加热点规则:
上面的配置含义是:对 buy 资源添加热点规则,当第 0 个参数的值为华为的时候 QPS阈值为 3,否则为 1。此外,如果第 0 个参数不传,那么这笔请求不受该热点规则限制。
不是华为:
买 1 次后,里面限流。
是华为:同时买 3 次,才限流
系统规则则是针对整个系统设置限流规则,并不针对某个资源,设置页面如下:
阈值类型包含以下五种:
授权规则用于配置资源的黑白名单:
述配置表示:只有 appA 和 appB 才能访问 test1 资源。
Sentinel 提供了@SentinelResource 注解用于定义资源,并提供可选的异常回退和 Block 回退。
异常回退指的是@SentinelResource 注解标注的方法发生 Java 异常时的回退处理;
Block 回退指的是当@SentinelResource 资源访问不符合 Sentinel 控制台定义的规则时的回退(默认返回Blocked by Sentinel (flow limiting))。这里简单记录下该注解的用法。
我们将搭建如图所示的测试框架:
我们将在 sentinel-example 里面演示所有@SentinelResource 的的功能。
<?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>
选择 Maven 项目:
点击 Next,填写以下的内容:
Parent:选择 spring-cloud-alibaba-example
Name:sentinel-example
点击 Finish,完成创建
<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>
<packaging>pom</packaging>
Provide 是一个普通的服务的提供者。
选择Maven项目:
点击 Next,填写以下的内容:
Parent:选择 sentinel-example
Name:sentinel-provider
点击 Finish,完成创建。
我们修改项目的打包方式,以后我们可以使用 jar 来发布项目。
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<?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>
Provide 是一个普通的服务的消费者。
选择 Maven 项目:
点击 Next,填写以下的内容:
Parent:选择 sentinel-example
Name:sentinel-consumer
点击 Finish,完成创建。
我们将在该项目里面演示@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>
<?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>
我们在 provider 里面添加一个模拟数据接口。
代码如下:
@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));
}
}
server:
port: 8081
spring:
application:
name: sentinel-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848
仅仅是为了让服务能注册到注册中心而已。
代码如下:
@SpringBootApplication
@EnableDiscoveryClient
public class SentinelProviderApplication {
public static void main(String[] args) {
SpringApplication.run(SentinelProviderApplication.class ,args) ;
}
}
在启动之前,我们必须保证 Nacos 已经启动成功。
测试接口:
http://localhost:8081/goods/buy/huawei/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)); } }
新建配置文件:
内容如下:
server:
port: 8083
spring:
application:
name: sentinel-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719
@SpringBootApplication
@EnableDiscoveryClient
public class SentinelConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(SentinelConsumerApplication.class ,args) ;
}
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate() ;
}
}
在启动之前,必须保证这些软件已经启动:
当访问该资源,QPS 超过 2 时,抛出异常。
测试:
http://192.168.1.11:8083/buy/huawei/1
在当前类中编写回退方法会使得代码变得冗余耦合度高,我们可以将回退方法抽取出来到一个指定类中。
代码如下:
public class BuyBlockHandler {
// sentinel 回退
public static String buyBlock(@PathVariable String name, @PathVariable
Integer count, BlockException e) {
return String.format("【进入 blockHandler 方法】购买%d 份%s 失败,当前购买人数过多,请稍后再试", count, name);
}
}
代码如下:
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));
}
}
@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()); } }
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双 11,对各 BU 业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。
在使用 Seata 之前,我们首先要安装 Seata-Server 服务器。
由于我们使用的 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 该版本。
点击该文件下载。
将 seata-server 复制到软件的目录里面,使用解压工具解压该文件。
Bin
:可执行文件目录
Conf
:配置文件目录
lib
:依赖的 jar
LICENSE
:授权文件
进入{seata}/bin
目录里面,双击:
代表 seata-server 已经启动成功。
在本示例中,我们模拟了一个用户购买货物的场景:
seata-examples 用来控制所有项目的依赖版本号,以及去除公共的依赖 seata。
选择Maven项目:
点击 Next,添加以下的信息:
Parent:选择 spring-cloud-alibaba-examples
Name:seata-examples
其他的信息保持默认即可。然后点击 Finish 即可完成创建。
打开项目的 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>
<?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。
Account-service 项目将负责扣减用户账户余额
选择 Maven 项目:
点击 Next 后,填写以下的信息:
Parent:seata-examples
Name:account-service
其他的值保持默认即可。
我们需要使用 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>
<?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>
项目的依赖关系如下:
在 business 将主要完成下单逻辑,包含库存的扣减,订单的创建。
选择 Maven 项目:
点击 Next 后,填写以下的信息:
Parent:seata-examples
Name:business-service
其他的值保持默认即可。
为了以后我们打包发布我们的项目,在此我们添加 boot-maven 的打包插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<?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>
项目的依赖关系如下:
order-service 项目将负责保存用户订单。
选择 Maven 项目:
点击 Next 后,填写以下的信息:
Parent:seata-examples
Name:order-service
其他的值保持默认即可。
我们需要使用 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>
<?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>
项目的依赖关系如下:
storage-service 将负责扣除商品的库存。
选择Maven项目:
点击 Next 后,填写以下的信息:
Parent:seata-examples
Name:storage-service
其他的值保持默认即可。
我们需要使用 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>
<?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>
项目的依赖关系如下:
在测试分布式事务之前,我们需要先设计数据库,以及准备测试数据。
新建数据库,命名为:seata
导入 Sql:
导入该 sql:
点击开始,进行导入。
成功后,发现成功的条数为:
表有如下:
account :用户的账号表
Order:订单表;
Stoage:商品的库存表;
undo_log:回滚事务表,SEATA AT 模式需要 UNDO_LOG 表。
使用 IDEA 连接数据库:
设置时区:
选择默认的即可,单击ok
成功后,如图所示:
执行代码的生成:
Account_tbl:
Order_tbl:
Storage_tbl:
代码生成完毕后:
在 storage-service 里面,主要完成对库存的扣减。
新建一个接口:
命名为:StorageService,代码如下:
代码如下:
public interface StorageService {
/**
* 扣减商品的库存
* @param commodityCode
* 商品的编码
* @param count
* 扣减商品的数量
*/
void deduct(String commodityCode, int count);
}
名称为: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); } }
添加一个 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() ; } }
在 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
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.bjsxt.mapper")
public class StorageServiceApplication {
public static void main(String[] args) {
SpringApplication.run(StorageServiceApplication.class ,args) ;
}
}
启动项目后,打印该日志,说明连接 seata-server 成功。
在 account-service 里面,主要完成对用户余扣减。
新建一个接口:
命名为:AccountService,代码如下:
代码如下:
public interface AccountService {
/**
* 从用户的账号扣减金额
* @param userId
* 用户的 Id
* @param money
* 金额
*/
void debit(String userId, int money);
}
名称为: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); } }
添加一个 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() ; } }
在 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
命名为 AccountServiceApplication ,代码如下:
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.bjsxt.mapper")
public class AccoutServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AccoutServiceApplication.class ,args) ;
}
}
启动项目后,打印该日志,说明连接 seata-server 成功。
在 order-service 里面,主要完成保存用户订单的操作。
新建一个接口:
命名为:OrderService,代码如下:
代码如下:
public interface OrderService {
/**
* 创建一个订单
* @param userId 用户 id
* @param commodityCode 商品的编号
* @param orderCount 商品的数量
* @return OrderTbl
*/
OrderTbl create(String userId, String commodityCode, int orderCount) ;
}
创建一个配置类:
代码如下:
@Configuration
public class HttpUtilConfig {
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate() ;
}
}
名称为: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 ; } }
创建一个 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 。
添加一个 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() ; } }
在 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
命名为:OrderServiceApplication
代码如下:
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.bjsxt.mapper")
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class ,args) ;
}
}
启动项目后,打印该日志,说明连接 seata-server 成功。
在 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) ;
}
名称为: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("下单完成"); } }
创建一个 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 。
新建一个类:
代码如下:
@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("订单创建失败") ; } }
添加一个 HttpUtilConfig 的配置类:
代码如下:
@Configuration
public class HttpUtilConfig {
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate() ;
}
}
在 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
命名为:BusinessServiceApplication
代码如下:
@SpringBootApplication
@EnableDiscoveryClient
public class BusinessServiceApplication {
public static void main(String[] args) {
SpringApplication.run(BusinessServiceApplication.class ,args) ;
}
}
继续改造启动类:
@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() ; } }
启动项目后,打印该日志,说明连接 seata-server 成功。
都启动完成后:
Nacos-Server:
在浏览器里面访问:
http://localhost:18087/purchase/SXT_USER_1/HUAWEI_0001/1
代表 SXT_USER_1 购买 HUAWEI_0001 产品 1 件。
数据库里面:
我们演示如图的异常:
我们可以发现,远程调用共有 3 处。
Account_Tbl:
Storage_Tbl:
http://localhost:18087/purchase/SXT_USER_1/HUAWEI_0001/1
数据库的数据:
Account_Tbl:
Storage_Tbl:
我们发现,分布式事务产生了,accout-service 内部的异常,导致 accout_tbl 表数据回滚了。
但是,在 storage_tbl :位于 stoage-service 的事务却没有回滚。
当用户的 ID 为:SXT_USER_2 时,我们抛出异常,当为其他用户时,我们正常的下单。
添加一个注解,看他是否能解决分布式事务的问题
重启 accout-service,business-service 测试
使用 SXT_USER_1 正常的下单测试:
Stoage_tbl:库存正常
Accout_Tbl:余额正常
使用 SXT_USER_2 下单测试:
发现发生异常后,
stoage_tbl 里面的没有发生改变,数据正常
Accout_tbl 里面的数据也没有发生改变,数据正常
分布式事务测试成功了
在上面的章节中,我们使用的时 Ribbon + RestTemplate 的形式做的远程调用。下面我们来演
示 Feign 的调用方式。
修改 business-service 项目里面的 pom.xml 文件,在里面添加依赖。
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
里面的代码如下:
@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);
}
@FeignClient("storage-service")
public interface StorageServiceFeign {
@GetMapping("/deduct/{commodityCode}/{orderCount}")
ResponseEntity<Void> deduct(
@PathVariable("commodityCode") String commodityCode,
@PathVariable("orderCount") Integer orderCount
) ;
}
@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("订单创建失败") ; } }
代码如下:
@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("扣减库存失败"); } }
在 dependencies 添加:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
里面的代码如下:
@FeignClient("account-service")
public interface AccountServiceFeign {
@GetMapping("/debit/{userId}/{orderMoney}")
ResponseEntity<Void> debit(
@PathVariable("userId") String userId,
@PathVariable("orderMoney") Integer orderMoney
) ;
}
/** * 实现对账号服务的远程调用 */ @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("扣减金额失败") ; } }
重启 order-service ,business-service
还原数据库数据,开始测试。
正常下单测试:
使用 SXT_USER_2 下单:
出错了,但是数据库的各个表都正常。
Seata 测试成功了。
Spring Cloud Alibaba技术栈【下】
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。