sleuth提供了一套完整的服务追踪解决方案 在分布式系统中提供追踪解决方案并且兼容支持zipkin
#springcloud F版本起就不需要自己maven构建zipkin了。只需要调用jar包即可
把jar包下载下来放到一个盘 运行java -jar jar包名
修改服务提供者cloud-provider-payment8001 修改服务消费者cloud-consumer-order80
8001: pom新增
<!--包含了sleuth+zipkin--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency>yml
zipkin: base-url: http://localhost:9411 sleuth: sampler: probability: 1controller
@GetMapping("/payment/zipkin") public String paymentZipkin(){ return "hi, i'am paymentzipkin server fall back,welcome to atguigu, O(∩_∩)O哈哈~"; }80 pom和yml一样
controller
//============> zipkin + sleuth @GetMapping("/consumer/payment/zipkin") public String paymentZipkin(){ String result = restTemplate.getForObject("http://localhost:8001" + "/payment/zipkin", String.class); return result; }启动7001,8001,80,调用80的controller接口形成链路。。。。然后刷新9411的web界面。。。可以看到链路追踪详细信息
springcloud alibaba做了一件包饺子的事情
dubbo被springcloud包饺子 cloud又被alibaba cloud包饺子
springcloud alibaba官网 https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
springalibaba 支持的功能 服务限流降级 服务注册与发现 分布式配置管理 消息驱动能力:rocketMq 阿里云对象存储 分布式任务调度
如果需要使用springcloud alibaba,则在父工程的root的pom里引入
<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>springcloud alibaba的各大组件:
sentinel ,nacos,Rocket,Dubbo,seata,oss,schedulerX
nacos:Naming configuration service
服务注册发现与配置中心服务组件
下载地址: https://github.com/alibaba/Nacos
官方学习文档也从这进 https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
nacos和eureka都是保证AP,高可用和分区容错性。Nacos还保证了cp
1,nacos下载后直接找到bin下的startup.cmd,启动即可
2,访问http://localhost:8848/nacos 用户名,密码都是nacos
nacos服务注册与发现#编码 1,新建项目cloudalibaba-provider-payment9001 2,pom新增
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>3,yml
server: port: 9001 spring: application: name: nacos-payment-provider cloud: nacos: discovery: server-addr: localhost:8848 #配置Nacos地址 management: endpoints: web: exposure: include: '*' #监控4,主启动类
package com.atguigu.springcloud.alibaba; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * @author wsk * @date 2020/3/23 10:21 */ @SpringBootApplication @EnableDiscoveryClient public class PaymentMain9001 { public static void main(String[] args) { SpringApplication.run(PaymentMain9001.class,args); } }5,主业务类
package com.atguigu.springcloud.alibaba.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; /** * @author wsk * @date 2020/3/23 10:27 */ @RestController public class PaymentController { @Value("${server.port}") private String serverPort; @GetMapping("/payment/nacos/{id}") public String getPayment(@PathVariable("id") Integer id){ return "nacos registry,serverPort: "+ serverPort+"\t id"+id; } }测试启动9001,访问localhost:9001/payment/nacos/1
nacos负载均衡1,参照9001新建9002 2,新建微服务消费者83 cloudalibaba-consumer-nacos-order83 pom跟提供者一样
yml
server: port: 83 spring: application: name: nacos-order-consumer cloud: nacos: discovery: server-addr: localhost:8848#消费者将要去访问的微服务名称(成功注册进nacos的微服务提供者),在这配置了访问的服务,业务类就不用在定义常量了
service-url: nacos-user-service: http://nacos-payment-provider主启动类一样 配置类
package com.atguigu.springcloud.alibaba.config; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; /** * @author wsk * @date 2020/3/23 11:07 */ @Configuration public class ApplicationContextConfig { @Bean @LoadBalanced //RestTemplate结合Ribbon做负载均衡一定要加@LoadBalanced public RestTemplate getRestTemplate(){ return new RestTemplate(); } }业务类
package com.atguigu.springcloud.alibaba.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; /** * @author wsk * @date 2020/3/23 11:09 */ @RestController public class OrderNacosController { /* 因为在yml中配置了service-url.nacos-user-service, 这里不需要再定义要访问微服务名常量,而是通过boot直接读出来 */ @Value("${service-url.nacos-user-service}") private String serverURL; @Resource private RestTemplate restTemplate; @GetMapping("/consumer/payment/nacos/{id}") public String paymentInfo(@PathVariable("id") Long id){ return restTemplate.getForObject(serverURL+"/payment/nacos/"+id,String.class); } }启动9002 启动83 然后看nacos
再访问80的接口localhost:83/consumer/
nacos是cp+ap的支持HTTP/DNS/UDP协议的各种支持的强大的组件
#nacos ap和cp的切换 AP模式支持是springcloud,保证了高可用和分区容错性。只支持注册临时实例 CP模式是k8s,保证强一致性和分区容错性
AP和cp可以互相切换 切换命令
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP' nacos配置中心#编码 1,新建cloudalibaba-config-nacos-client3377 2,pom新增
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>3,yml 两个 bootstrap.yml
server: port: 3377 spring: application: name: nacos-config-client cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 config: server-addr: localhost:8848 #Nacos作为配置中心地址 file-extension: ymlapplication.yml
spring: profiles: active: dev #4,主启动类
package com.atguigu.springcloud.alibaba; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * @author wsk * @date 2020/3/23 12:28 */ @SpringBootApplication @EnableDiscoveryClient public class NacosConfigClientMain3377 { public static void main(String[] args) { SpringApplication.run(NacosConfigClientMain3377.class,args); } }5,业务类
package com.atguigu.springcloud.alibaba.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * @author wsk * @date 2020/3/23 12:30 */ @RestController @RefreshScope //SpringCloud原生注解 支持Nacos的动态刷新功能 public class ConfigClientController { @Value("${config.info}") private String configInfo; @GetMapping("/config/info") public String getConfigInfo(){ return configInfo; } }6,在nacos里添加规则 规则是
${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}所以根据以上配置
Data Id :nacos-config-client-dev.yml
启动3377,访问localhost:3377/config/info
动态刷新直接修改nacos配置中心的yaml文件,version改成20,在访问接口
nacos高级篇nacos对于多环境多项目的管理十分强大,不仅对生产,开发,测试环境都能管理,而且对其他的公司的外包项目的环境也能管理
由nacos的namespace+Group+DataID三者构成 namespace:用于区分部署环境的 Group和DataID用于逻辑上区分两个目标对象
默认情况下:namespace:public Group:DEFAULT_GROUP 默认的集群cluster也是DEFAULT
Group可以把不同的微服务划分到同一个分组里面去
#######三种配置方式 1,DataId级别的配置 在原基础上再新建一个nacos-config-client-test.yaml 配置内容
config: info: from nacos config center,nacos-config-client-test.yaml ,version=2application.yml里改active改成test就可以访问后面这个新加的配置了
2,group级别的配置 (1)新建配置
DataId: nacos-config-client-info.yamlGroup:DEV_GROUP 配置内容:
config: info: nacos-config-client-info.yaml,DEV_GROUP(2)新建配置
DataId: nacos-config-client-info.yaml Group:TEST_GROUP配置内容:
config: info: nacos-config-client-info.yaml,TEST_GROUP(3)修改bootstrap.yml和application.yml 1,bootstrap config下增加
group: TEST_GROUP2,application
active: info
重启后访问controller
3,namespace命名空间方案 (1)新建dev/test的namespace 点击命名空间,新建命名空间 dev和test
(2)选择dev 点+号
Dataid :nacos-config-client-dev.yaml配置内容:
config: info: xxxxxxxxx DEFAULT_GROUP回到bootstrap group下面配置一行
namespace: 命名空间id(3)dev下再新建配置
nacos-config-client-dev.yaml Group: DEV_GROUP配置内容:
config: info: xxxxxxxxx,DEV-GROUP(4)dev下再新建配置
nacos-config-client-dev.yaml Group:TEST_GROUP配置内容:
config: info: xxxxxxxxx,TEST_GROUP到此为止dev下有三个配置文件。相同命名空间不同组配置 重启服务,访问成功。test命名空间下道理一样不在赘述
nocas集群和持久化8848挂掉了,所有的玩完? 至少得有3台nacos
nocas为什么关闭服务以后。数据还存在。关闭电脑再启动还存在。因为它本身有一个嵌入式数据库,那么有了数据库为什么还要持久化集群mysql?因为多个nacos嵌入式数据库造成数据一致性问题得不到解决
nacos底层源码: https://github.com/alibaba/nacos/blob/develop/config/pom.xml
默认自带derby嵌入式数据库坐标
derby----mysql的配置 1,nacos-server-1.1.4\nacos\conf目录下找到nacos-mysql.sql 2,到sqlyog执行sql脚本 3,nacos-server-1.1.4\nacos\conf目录下找到application.properties 4,mysql版本在5.6.5以上 application下粘贴
spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config? characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true db.user=root db.password=ljs5,重启nacos会发现有一个空的页面
6,新建配置
7,发现数据库
linux下搭建生产环境下的nacos集群+mysql+nginx1,下载linux版本的nacos https://github.com/alibaba/nacos/releases/tag/1.1.4 跟之前windows的一样也是找到sql文件。然后复制到linux的数据库下。然后修改配置文件。。
直接参考这篇文章。。。https://www.cnblogs.com/larscheng/p/11427733.html
代码实例
修改9002 nacos的服务提供者yml文件
注册成功。nacos
alibaba sentinel 实现熔断和限流
#官方文档 github.com/alibaba/sentinel
之前的hystrix 1,需要自己手工搭建监控平台 2,没有web界面可给我们进行更加细粒化的配置 以及流控,速率控制。服务熔断,服务降级
sentinel: 1,单独一个组件,可独立出来 2,直接姐棉花细粒度统一配置
下载地址:https://github.com/alibaba/sentinel/releases 下载1.7的sentinel-dashboard.jar
sentinal分为两个部分: 核心库:(java客户端)不依赖任何框架/库。能够运行于所有java运行时环境。对dubbo/springcloud框架也有很好的支持
控制台:基于springboot开发。打包后可直接运行不需要额外的tomcat等应用容器
运行jar包以后访问localhost:8080
用户名密码都是sentinel
sentinel的初始化监控#编码 1,启动nacos8848
2,新建cloud-alibaba-sentinel-service 8401
3,pom
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--springcloud alibaba sentinel-datasource-nacos 后续做持久化用到--> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency> <!--springcloud alibaba sentinel--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>4,yml
server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: # Nacos服务注册中心地址 server-addr: localhost:8848 sentinel: transport: # sentinel dashboard 地址 dashboard: localhost:8080 # 默认为8719,如果被占用会自动+1,直到找到为止 port: 8719 management: endpoints: web: exposure: include: "*"5,主启动类
package com.atguigu.springcloud.alibaba; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * @author wsk * @date 2020/3/24 13:59 */ @SpringBootApplication @EnableDiscoveryClient public class MainApp8401 { public static void main(String[] args) { SpringApplication.run(MainApp8401.class,args); } }6,controller
package com.atguigu.springcloud.alibaba.controller; import com.alibaba.csp.sentinel.annotation.SentinelResource; import com.alibaba.csp.sentinel.slots.block.BlockException; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * @author wsk * @date 2020/3/24 14:00 */ @RestController @Slf4j public class FlowLimitController { @GetMapping("/testA") public String testA(){ return "--------testA"; } @GetMapping("/testB") public String testB(){ return "--------testB"; } }7,启动8401 访问接口http://localhost:8401/testA
sentinel 流控规则流量控制: 资源名就是我们的application -name 唯一的。或者默认请求路径
针对来源就是:sentinel可以针对调用者进行限流。填写微服务名。默认default
阈值类型/单机阈值 qps:当调用该api的qps达到阈值的时候,进行限流 线程数:当调用该api的线程数达到阈值的时候进行限流 是否集群:sentinel不需要
流控模式 直接:api达到限流条件时,直接限流 关联:当关联资源达到阈值,限流自己 链路:只记录指定链路上的流量
流控效果: 快速失败:直接失败,抛异常 warm up:根据codeFactor(冷加载因子。默认3)的值,从阈值codeFactor,经过预热时长,才达到设置的qps值 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须为qps,否则无效
流控模式1,qps: 设置单机阈值 1 流控模式:直接 流控效果:快速失败
快速点击
2,线程数 在sentinel设置
然后回到testA修改testA方法
@GetMapping("/testA") public String testA(){ try{ TimeUnit.MILLISECONDS.sleep(800); } catch (InterruptedException e) { e.printStackTrace(); } return "--------testA"; }然后开两个浏览器模拟多线程
流控模式:1,关联 相当于我生病,你吃药?这有什么用? #通常用于支付模块达到阈值,限流订单模块。
配置规则 testA 阈值类型qps 单机阈值1 流控模式:关联 关联资源:/testB 流控效果:快速失败 打开postman 模拟多线程访问testB
找到collections
模拟20个线程,每延迟300毫秒访问testB
这期间访问testA
这之后访问
流控效果:default 直接失败:抛出异常
Blocked by sentinel(flow limiting)
流控效果:预热:warm up设置 资源名:/testB 针对来源:default 阈值类型:qps 单机阈值10 预热时长 5 流程模式:直接
在5秒钟阈值慢慢从3-10 访问浏览器很快刷新页面。刚开始不行。慢慢过了5秒的样子能抗住了
如秒杀系统在开启的瞬间,会有很多流量上来。很有可能把系统打死。预热方式就是为了保护系统,慢慢将流量放进来。慢慢把阈值增长到设置的阈值
冷加载因子=单机阈值/3 根据warm up 设置时间慢慢qps由单机阈值/3-----单机阈值
流程效果:排队等待让请求以均匀的速度通过,阈值类型必须设置成qps否则无效 设置:资源名:/testA 阈值类型:qps 单机阈值:1 流控模式:直接 流控效果:排队等待 超时时间:20000
意思是:/testA每秒1次请求。超过就排队等待,等待的超时时间为20000毫秒
相当于在某一秒有大量请求。接下来空闲。一会又有大量请求。我们可以在空闲时间处理剩下的请求。而不是在第一秒就直接拒绝了
回到idea在testB方法里加
log.info(Thread.currentThread().getName()+"\t"+"...testB"); sentinel降级点击降级规则,右上角–+新增降级规则 有三种策略:RT,异常比例,异常数
RT:平均响应时间:秒级 平均响应时间超出阈值,且时间窗口内通过的请求>=5两个条件同时满足触发降级。窗口期过后关闭断路器。RT最大为4900
异常比例:秒级 qps>=5且异常比例(秒级)超过阈值。触发降级。时间窗口结束后。关闭降级
异常数(分钟级) 异常数(分钟统计)超过阈值时。触发降级,时间窗口结束后,关闭降级
sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时候(例如调用超时或异常比例升高),对这个资源的调用进行限制。让请求快速失败。避免影响到其他资源导致级联错误
当资源被降级后,在接下来降级时间窗口内。对该资源的调用都自动熔断。默认抛出DegradeException
Hystrix有open closed halfopen状态 sentinel没有半开状态
1,RT 平均响应时间:1秒持续5个请求。且平均响应时间大于阈值,触发降级。时间窗口期结束。关闭降级
回到controller
@GetMapping("/testD") public String testD(){ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.info("testD 测试 RT"); return "--------testD"; }回到sentinel dashboard —簇点链路–选择降级–选RT–RT200–时间窗口1
使用jmeter压测
线程数设置10 Ramp-up perviod(in seconds):1 循环次数:永远
启动项目,再启动jmeter
再访问接口localhost:8401/testD 会出现降级
#为什么会这样呢? 根据jmeter的设置。永远一秒打进10个线程。大于5个。调用testD。我们希望200ms处理完本次任务。如果超过200ms没有处理完。在未来一秒钟内的时间窗口内。断路器打开。微服务不可用
2,异常比例 异常比例:当资源的每次请求量>=5,并且每秒异常总数占通过量比值超过阈值之后。资源进入降级状态,即在接下的时间窗口内,对这个方法调用都会自动地返回。异常比例的阈值范围在[0.0,1.0]。0%-100%
qps>=5&&异常比例超过阈值,触发降级,时间窗口期结束,关闭降级
将/testD代码改为。。
@GetMapping("/testD") public String testD(){ log.info("testD 测试 RT"); int age=10/0; return "--------testD"; }sentinel的设置 异常比例-0.2–时间窗口1
jmeter依旧和刚才一样设置跑起来
停掉jmeter 3,异常数 异常数:当资源近一分钟的异常数目超过阈值之后会进行熔断:时间窗口一定大于60秒。因为它是分钟级的
异常数超过阈值—降级—时间窗口结束----关闭降级
controller
@GetMapping("/testE") public String testE(){ log.info("testE测试异常数"); int age = 10/0; return "--------testE 测试异常数"; }sentinel设置—异常数–5--时间窗口61
访问localhost:8401/testE 访问5次以后才触发降级
sentinel热点key限流官网:github.com/alibaba/sentinel/wiki/热点参数限流
像hystrix某个方法出问题了,去找对应兜底方法降级。这是我们自定义的
像sentinel不配置的话。默认提示Blocked by sentinel(flow limiting)
可以配置Sentinel的@SentinelResource
热点限流key的原理 官网源码:com.alibaba.csp.sentinel.slots.block.BlockException
#编码 controller
@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 exception){ return "---------------deal_testHotKey,o(╥﹏╥)o"; }sentinel设置热点限流规则 资源名:testHotKey 参数索引:0 单机阈值:1 窗口时长:1
正常访问 点击速度加快
#如果用了@SentinelResource一定要指定value的同时,指定blockhandler否则会有异常页面。用户看到不太好
热点key参数例外项在编辑热点规则下面有个高级选项
当我们设定了p1这个参数为0的索引。一旦qps>1则熔断限流。可不可以p1的值如果等于一个特定的值。则让他不熔断限流呢
设置参数例外项。记得不要忘记添加按钮
参数类型只能支持String加8种基本数据类型 参数值:5 限流阈值:200 添加
然后访问接口
如果在testHotKey方法添加一行代码: int age=10/0; 所以直接的@SentinelResource不管运行时异常
系统自适应限流sentinel 点击系统规则—新增系统规则 load自适应 只支持linux,unix系统。
如果设置了入口qps则掌握全局qps阈值设置1
##### SentinelResource配置1,按资源名称限流+后续处理 #编码 (1)修改pom 增加一个
<dependency> <groupId>com.atguigu.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency>(2)新建controller @RestController
public class RateLimitController { @GetMapping("/byResource") @SentinelResource(value = "byResource",blockHandler = "handlerException") public CommonResult byResource(){ return new CommonResult(200,"按资源名称限流测试OK",new Payment(2020L,"serial001")); } public CommonResult handlerException(BlockException exception){ return new CommonResult(444,exception.getClass().getCanonicalName()+"\t 服务不可用"); } }(3)sentinel–流控规则–新建资源名byResource-阈值类型qps-单机阈值1
启动nacos,sentinel,8401项目 访问localhost:8401/byResource狂点会进入兜底
正常点。正常访问
平时关闭8401微服务。流控规则会消失。所以它是临时节点
2,按照url地址限流
在controller增加一个方法 @GetMapping("/rateLimit/byUrl") @SentinelResource(value=“byUrl”) public CommonResult byurl(){ return new CommonResult(200,“按url限流ok”,new Payment(2020L,“Serial002”)); }
这个sentinel设置直接加一个左斜杠
不好用就去掉斜杠,好用了 ,但是没有指定blockHandler肯定刷快了会爆异常页面
3,兜底方法存在问题 每个业务都有一个兜底方法,像hystrix一样,可以解耦。写一个fallback类?可以!
客户自定义限流 (1)创建CustomerBlockHandler类
package com.atguigu.springcloud.alibaba.handler; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.atguigu.springcloud.entities.CommonResult; import com.atguigu.springcloud.entities.Payment; /** * @author wsk * @date 2020/3/24 22:04 */ 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"); } }(2)RatelimitController添加方法
@GetMapping("/rateLimit/customerBlockHandler") @SentinelResource(value = "customerBlockHandler", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handlerException2") public CommonResult customerBlockHandler(){ return new CommonResult(200,"按客户自定义",new Payment(2020L,"serial002")); }启动8401
访问这个接口
然后设置sentinel
平时访问 兜底访问 sentinel熔断 sentinel注解不支持private只支持public
服务熔断采取整合ribbon或openFeign+fallback
1,修改cloudalibaba-provider-payment9003/9004
package com.atguigu.springcloud.controller; import com.atguigu.springcloud.entities.CommonResult; import com.atguigu.springcloud.entities.Payment; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; /** * @author wsk * @date 2020/3/24 22:36 */ @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,"28adsasadsafas5d1as5fad5as31d3as0")); hashMap.put(2L,new Payment(2L,"4das5d5asda4dad5asda5s3dasd53das0")); hashMap.put(3L,new Payment(3L,"6ads65d351as3d1as53d1a53d3as13as0")); } @GetMapping("/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); return result; } }2,84作为消费者端调9003/9004实现负载均衡 修改84 第一种情况。没有任何配置的
package com.atguigu.springcloud.controller; import com.alibaba.csp.sentinel.annotation.SentinelResource; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.atguigu.springcloud.entities.CommonResult; import com.atguigu.springcloud.entities.Payment; import com.atguigu.springcloud.service.PaymentService; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; /** * @author wsk * @date 2020/3/24 23:06 */ @RestController @Slf4j public class CircleBreakerController { private static final String SERVICE_URL = "http://nacos-payment-provider"; @Resource private RestTemplate restTemplate; @RequestMapping("/consumer/fallback/{id}") @SentinelResource(value = "fallback") //没有配置 // @SentinelResource(value = "fallback",fallback = "handlerFallback") //配置了fallback的,fallback只负责业务异常 // @SentinelResource(value = "fallback",blockHandler = "blockHandler") // 配置了blockHandler,只负责sentinel控制台配置违规 //@SentinelResource(value = "fallback",fallback = "handlerFallback", blockHandler = "blockHandler", // exceptionsToIgnore = {IllegalArgumentException.class}) // 配置了blockHandler和fallback public CommonResult<Payment> fallback(@PathVariable("id") Long id){ CommonResult<Payment> commonResult = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class); if(id == 4){ throw new IllegalArgumentException("IllegalArgumentException,非法参数异常"); }else if(commonResult.getData() == null){ throw new NullPointerException("NullPointerException,该ID没有记录,空指针异常"); } return commonResult; } // 本例是fallback public CommonResult handlerFallback(Long id, Throwable e){ Payment payment = new Payment(id, null); return new CommonResult(444, "兜底异常handler,exception内容"+e.getMessage(), payment); } public CommonResult blockHandler(Long id, BlockException exception){ Payment payment = new Payment(id, null); return new CommonResult<>(445, "blockHandler-sentinel 限流,无此流水号:blockException" + exception.getMessage(), payment); } // --------------- open feign--------- @Resource private PaymentService paymentService; @GetMapping("/consumer/paymentSQL/{id}") public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id){ return paymentService.paymentSQL(id); } }3,启动nacos,sentinel,9003,9004,84 测试http://localhost:84/consumer/fallback
如果是4,报页面的非法参数异常。。。。 4以外的报空指针异常
第二种情况 @SentinelResource(value = “fallback”,fallback = “handlerFallback”)
加代码。方法
// 本例是fallback public CommonResult handlerFallback(Long id, Throwable e){ Payment payment = new Payment(id, null); return new CommonResult(444, "兜底异常handler,exception内容"+e.getMessage(), payment); }第三种情况
@SentinelResource(value = "fallback",blockHandler = "blockHandler")加代码
public CommonResult blockHandler(Long id, BlockException exception){ Payment payment = new Payment(id, null); return new CommonResult<>(445, "blockHandler-sentinel 限流,无此流水号:blockException" + exception.getMessage(), payment); }设置sentinel
然后访问接口方法
多访问几次进入兜底
第四种情况
@SentinelResource(value = "fallback",fallback = "handlerFallback", blockHandler = "blockHandler")放开刚才两个方法 sentinel设置流控规则 qps:1 访问接口。
快速点击进入限流
普通点击走兜底
快速点击fallback4,是走兜底还是限流。是走限流的
异常忽略属性
@SentinelResource(value = "fallback",fallback = "handlerFallback", blockHandler = "blockHandler", exceptionsToIgnore = {IllegalArgumentException.class})然后访问localhost:84/consumer/fallback/4
走的是error page
localhost:84/consumer/fallback/5
走的兜底
sentinel服务熔断+openfeign修改84. 1,pom 新增
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>2,yml #激活sentinel对feign的支持
feign: sentinel: enabled: true3,主启动类
@SpringBootApplication @EnableDiscoveryClient @EnableFeignClients4,service
package com.atguigu.springcloud.service; import com.atguigu.springcloud.entities.CommonResult; import com.atguigu.springcloud.entities.Payment; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; /** * @author wsk * @date 2020/3/25 9:02 */ @FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackService.class) public interface PaymentService { @GetMapping("/paymentSQL/{id}") public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id); } package com.atguigu.springcloud.service; import com.atguigu.springcloud.entities.CommonResult; import com.atguigu.springcloud.entities.Payment; import org.springframework.stereotype.Component; /** * @author wsk * @date 2020/3/25 9:06 */ @Component public class PaymentFallbackService implements PaymentService{ @Override public CommonResult<Payment> paymentSQL(Long id) { return new CommonResult<>(444,"服务降级返回,------------PaymentFallbackService",new Payment(id,"errorSerial")); } }5,controller
// --------------- open feign--------- @Resource private PaymentService paymentService; @GetMapping("/consumer/paymentSQL/{id}") public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id){ return paymentService.paymentSQL(id); }故意关闭9003
触发降级
sentinel持久化当我们配置规则以后,关闭服务。配置规则自动消失。 将限流,熔断这些配置规则持久化进nacos。只要刷新8401某个rest地址。sentinel控制台的流控规则就可以看到。只要nacos里面配置不删除。针对8401上sentinel上的流控规则持续有效
1,修改cloudalibaba-sentinel-service8401 pom新增
<!--springcloud alibaba sentinel-datasource-nacos 后续做持久化用到--> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>2,yml
server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: # Nacos服务注册中心地址 server-addr: localhost:8848 sentinel: transport: # sentinel dashboard 地址 dashboard: localhost:8080 # 默认为8719,如果被占用会自动+1,直到找到为止 port: 8719 # 流控规则持久化到nacos datasource: dsl: nacos: server-addr: localhost:8848 data-id: ${spring.application.name} group-id: DEFAULT_GROUP data-type: json rule-type: flow management: endpoints: web: exposure: include: "*" feign: sentinel: enabled: true3,添加nacos业务规则配置(必须是做了持久化的nacos) 可以用本地的8848 nacos配置
新建配置 DataID:cloudalibaba-sentinel-service 配置格式:json 配置内容:
[ { "resource":"/rateLimit/byUrl", "limitApp":"default", "grade":1, "count":1, "strategy":0, "controllBehavior":0, "clusterMode":false } ]其中各项含义: resource:资源名称 limitApp:来源应用 grade:阈值类型 0,线程数 1,qps count:单机阈值 strategy:流控模式,0直接 1关联 2链路 controlBehavior:流程效果 0快速失败 1warm up 2排队等待 clusterMode 是否集群
4,启动8401,访问接口。刷新sentinel发现流控规则有了 停止8401发现 没了
不怕,再重启8401
快速访问刚才那个localhost:8401/rateLimit/byUrl
我并没有重新配置流控。他快速访问就已经生效。说明。sentinel持久化成功
seata处理分布式事务
以前的单体应用被拆分成多个应用,分别使用多个独立的数据源(数据库),每个服务内部数据的一致性由本地事务来保证,但全局无法保证,所以出现了分布式事务
一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题
seata是alibaba开源的分布式事务解决方案。致力于在微服务架构下提供高性能和简单易用的分布式事务服务
官网http://seata.io/zh-cn/
全局唯一事务id+三组件(TC,TM,RM)
TC:事务协调器,维护全局事务运行状态,负责协调并驱动全局事务提交或回滚 TM:控制全局事务的边界。负责开启一个全局事务,并最终发起全局提交式全局回滚的决议 RM:控制分支事务,负责分支注册,状态汇报,并接受事务协调器指令,驱动本地事务提交和回滚
seata分布式事务执行原理 1,TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID 2,XID在微服务调用链路的上下文中传播 3,RM向TC注册分支事务,将其纳入XID对应全局事务的管辖 4,TM向TC发起针对XID的全局提交或回滚决议 5,TC调度XID下管辖的全部后支事务完成提交或回滚请求
seata安装配置1,地址:https://github.com/seata/seata/releases
2,解压到固定的路径 3,修改:自定义事务组名称+事务日志存储模式db+数据库连接信息 4,修改conf下的file.conf (1)修改service里的
vgroup_mapping.my_test_tx_group = "fsp_tx_group"(2)修改store模块的
mode="db"然后下面改db的配置
url="jdbc:mysql://127.0.0.1:3306/seata" user="root" password="ljs"5,在数据库新建库seata 6,在seata库里建表 建表sql在\seata-server-1.0.0\seata\conf里 db_store.sql
drop table if exists `global_table`; create table `global_table` ( `xid` varchar(128) not null, `transaction_id` bigint, `status` tinyint not null, `application_id` varchar(32), `transaction_service_group` varchar(32), `transaction_name` varchar(128), `timeout` int, `begin_time` bigint, `application_data` varchar(2000), `gmt_create` datetime, `gmt_modified` datetime, primary key (`xid`), key `idx_gmt_modified_status` (`gmt_modified`, `status`), key `idx_transaction_id` (`transaction_id`) ); drop table if exists `branch_table`; create table `branch_table` ( `branch_id` bigint not null, `xid` varchar(128) not null, `transaction_id` bigint , `resource_group_id` varchar(32), `resource_id` varchar(256) , `lock_key` varchar(128) , `branch_type` varchar(8) , `status` tinyint, `client_id` varchar(64), `application_data` varchar(2000), `gmt_create` datetime, `gmt_modified` datetime, primary key (`branch_id`), key `idx_xid` (`xid`) ); drop table if exists `lock_table`; create table `lock_table` ( `row_key` varchar(128) not null, `xid` varchar(96), `transaction_id` long , `branch_id` long, `resource_id` varchar(256) , `table_name` varchar(32) , `pk` varchar(36) , `gmt_create` datetime , `gmt_modified` datetime, primary key(`row_key`) );分支,全局,锁表生成成功
7,修改seata-server-1.0.0\seata\conf目录下的registry.conf配置文件 第三行
type="nacos" 然后找到nacos{ serverAddr="localhost:8848"8,启动nacos,再启动seata-server.bat。如果dos下出现
表示内存不足。要找到 seata-server.bat里修改
改成这样,改小
-load Registry Provider表示执行成功
订单/库存/账户数据库准备都需要先启动nacos,再启动seata 业务:当用户下单时,会在订单服务中创建一个订单。然后通过远程调用库存服务扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额。最后订单服务中修改订单状态为已完成
创建业务数据库 seata-order:存储订单的数据库 seata-storage:存储库存的数据库 seata-account:存储账户信息的数据库 库对应表: order库对应t_order表 storage库对应t_storage表 account库对应t_account表
订单,库存,账户3个库下都需要建各自的回滚日志表 在\seata-server-1.0.0\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;在三个库下分别粘贴执行
最终的数据库准备结果
订单。库存。账户微服务编码 下订单–减库存—减余额–改订单状态
订单模块。创建seata-order-service2001 1,pom
<?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>cloud2020</artifactId> <groupId>com.atguigu.springcloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>seata-order-service2001</artifactId> <dependencies> <!--nacos--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <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> <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> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--jdbc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </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> <!--hutool 测试雪花算法--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-captcha</artifactId> <version>5.2.0</version> </dependency> </dependencies> </project>2,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: 127.0.0.1:8848 datasource: # 当前数据源操作类型 type: com.alibaba.druid.pool.DruidDataSource # mysql驱动类 driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://47.115.93.213:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: ljs feign: hystrix: enabled: false logging: level: io: seata: info #ribbon的超时时间,这里测试用 ribbon: ReadTimeout: 60000 ConnectTimeout: 60000 mybatis: mapper-locations: classpath*:mapper/*.xml3,file.conf
transport { # tcp udt unix-domain-socket type = "TCP" #NIO NATIVE server = "NIO" #enable heartbeat heartbeat = true #thread factory for netty thread-factory { boss-thread-prefix = "NettyBoss" worker-thread-prefix = "NettyServerNIOWorker" server-executor-thread-prefix = "NettyServerBizHandler" share-boss-worker = false client-selector-thread-prefix = "NettyClientSelector" client-selector-thread-size = 1 client-worker-thread-prefix = "NettyClientWorkerThread" # netty boss thread size,will not be used for UDT boss-thread-size = 1 #auto default pin or 8 worker-thread-size = 8 } shutdown { # when destroy server, wait seconds wait = 3 } serialization = "seata" compressor = "none" } service { #vgroup->rgroup # 事务组名称 vgroup_mapping.fsp_tx_group = "default" #only support single node default.grouplist = "127.0.0.1:8091" #degrade current not support enableDegrade = false #disable disable = false #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent max.commit.retry.timeout = "-1" max.rollback.retry.timeout = "-1" } client { async.commit.buffer.limit = 10000 lock { retry.internal = 10 retry.times = 30 } report.retry.count = 5 tm.commit.retry.count = 1 tm.rollback.retry.count = 1 } ## transaction log store store { ## store mode: file、db #mode = "file" mode = "db" ## file store file { dir = "sessionStore" # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions max-branch-session-size = 16384 # globe session size , if exceeded throws exceptions max-global-session-size = 512 # file buffer size , if exceeded allocate new buffer file-write-buffer-cache-size = 16384 # when recover batch read size session.reload.read_size = 100 # async, sync flush-disk-mode = async } ## database store db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. datasource = "dbcp" ## mysql/oracle/h2/oceanbase etc. db-type = "mysql" driver-class-name = "com.mysql.jdbc.Driver" url = "jdbc:mysql://47.115.93.213:3306/seata" user = "root" password = "ljs" min-conn = 1 max-conn = 3 global.table = "global_table" branch.table = "branch_table" lock-table = "lock_table" query-limit = 100 } } lock { ## the lock store mode: local、remote mode = "remote" local { ## store locks in user's database } remote { ## store locks in the seata's server } } recovery { #schedule committing retry period in milliseconds committing-retry-period = 1000 #schedule asyn committing retry period in milliseconds asyn-committing-retry-period = 1000 #schedule rollbacking retry period in milliseconds rollbacking-retry-period = 1000 #schedule timeout retry period in milliseconds timeout-retry-period = 1000 } transaction { undo.data.validation = true undo.log.serialization = "jackson" undo.log.save.days = 7 #schedule delete expired undo_log in milliseconds undo.log.delete.period = 86400000 undo.log.table = "undo_log" } ## metrics settings metrics { enabled = false registry-type = "compact" # multi exporters use comma divided exporter-list = "prometheus" exporter-prometheus-port = 9898 } support { ## spring spring { # auto proxy the DataSource bean datasource.autoproxy = false } }4,regustry.conf
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" nacos { #serverAddr = "localhost" serverAddr = "localhost:8848" namespace = "" cluster = "default" } eureka { serviceUrl = "http://localhost:8761/eureka" application = "default" weight = "1" } redis { serverAddr = "localhost:6379" db = "0" } zk { cluster = "default" serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } consul { cluster = "default" serverAddr = "127.0.0.1:8500" } etcd3 { cluster = "default" serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" application = "default" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" cluster = "default" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" nacos { serverAddr = "localhost" namespace = "" } consul { serverAddr = "127.0.0.1:8500" } apollo { app.id = "seata-server" apollo.meta = "http://192.168.1.204:8801" } zk { serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } }5,domain CommonResult
package com.atguigu.springcloud.alibaba.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * @author wsk * @date 2020/3/25 20:37 */ @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
package com.atguigu.springcloud.alibaba.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.math.BigDecimal; /** * @author wsk * @date 2020/3/25 20:25 */ @Data @AllArgsConstructor @NoArgsConstructor public class Order { private Long id; private Long userId; private Long productId; private Integer count; private BigDecimal money; private Integer status; }6,dao
package com.atguigu.springcloud.alibaba.dao; import com.atguigu.springcloud.alibaba.domain.Order; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; /** * @author wsk * @date 2020/3/25 20:41 */ @Mapper public interface OrderDao { //1 新建订单 void createOrder(Order order); //2 修改订单状态 从0改为1 void update(@Param("userId") Long userId,@Param("status") Integer status); }7,xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.atguigu.springcloud.alibaba.dao.OrderDao"> <resultMap id="BaseResultMap" type="com.atguigu.springcloud.alibaba.domain.Order"> <id column="id" property="id" jdbcType="BIGINT"></id> <result column="user_id" property="userId" jdbcType="BIGINT"></result> <result column="product_id" property="productId" jdbcType="BIGINT"></result> <result column="count" property="count" jdbcType="INTEGER"></result> <result column="money" property="money" jdbcType="DECIMAL"></result> <result column="status" property="status" jdbcType="INTEGER"></result> </resultMap> <insert id="createOrder" parameterType="com.atguigu.springcloud.alibaba.domain.Order" useGeneratedKeys="true" keyProperty="id"> insert into t_order(user_id,product_id,count,money,status) values (#{userId},#{productId},#{count},#{money},0); </insert> <update id="update"> update t_order set status =1 where user_id =#{userId} and status=#{status}; </update> </mapper>8,service orderService
package com.atguigu.springcloud.alibaba.service; import com.atguigu.springcloud.alibaba.domain.Order; /** * @author wsk * @date 2020/3/25 20:58 */ public interface OrderService { void create(Order order); }StorageService
package com.atguigu.springcloud.alibaba.service; import com.atguigu.springcloud.alibaba.domain.CommonResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; /** * @author wsk * @date 2020/3/25 21:00 */ @FeignClient(value = "seata-storage-service") public interface StorageService { @PostMapping(value = "/storage/decrease") CommonResult decrease(@RequestParam("productId") Long productId,@RequestParam("count") Integer count); }AccountService
package com.atguigu.springcloud.alibaba.service; import com.atguigu.springcloud.alibaba.domain.CommonResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import java.math.BigDecimal; /** * @author wsk * @date 2020/3/25 21:00 */ @FeignClient(value = "seata-account-service") public interface AccountService { @PostMapping(value = "/account/decrease") CommonResult decrease(@RequestParam("userId") Long userId,@RequestParam("money") BigDecimal money); }9,serviceImpl
package com.atguigu.springcloud.alibaba.service.impl; import com.atguigu.springcloud.alibaba.dao.OrderDao; import com.atguigu.springcloud.alibaba.domain.Order; import com.atguigu.springcloud.alibaba.service.AccountService; import com.atguigu.springcloud.alibaba.service.OrderService; import com.atguigu.springcloud.alibaba.service.StorageService; import io.seata.spring.annotation.GlobalTransactional; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; /** * @author wsk * @date 2020/3/25 20:59 */ @Service @Slf4j public class OrderServiceImpl implements OrderService { @Resource private OrderDao orderDao; @Resource private AccountService accountService; @Resource private StorageService storageService; /** * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态 * 简单说: * 下订单->减库存->减余额->改状态 * GlobalTransactional seata开启分布式事务,异常时回滚,name保证唯一即可 */ @Override @GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class) public void create(Order order) { log.info("----->开始创建新订单"); //1 新建订单 orderDao.createOrder(order); log.info("----->订单微服务开始调用库存,做扣减Count"); //2 扣减库存 storageService.decrease(order.getProductId(),order.getCount()); log.info("----->订单微服务开始调用库存,做扣减end"); log.info("----->订单微服务开始调用账户,做扣减Money"); //3 扣减账户 accountService.decrease(order.getUserId(),order.getMoney()); log.info("----->订单微服务开始调用库存,做扣减end"); //修改订单状态,从0到1,1代表以及完成 log.info("----->修改订单状态开始"); orderDao.update(order.getUserId(),0); log.info("----->修改订单状态结束"); log.info("----->下订单结束了,O(∩_∩)O哈哈~"); } }10.OrderController
package com.atguigu.springcloud.alibaba.controller; import com.atguigu.springcloud.alibaba.domain.CommonResult; import com.atguigu.springcloud.alibaba.domain.Order; import com.atguigu.springcloud.alibaba.service.OrderService; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; /** * @author wsk * @date 2020/3/25 21:24 */ @RestController public class OrderController { @Resource private OrderService orderService; @GetMapping("/order/create") public CommonResult create(Order order){ orderService.create(order); return new CommonResult(200,"订单创建成功"); } }11,config
package com.atguigu.springcloud.alibaba.config; import com.alibaba.druid.pool.DruidDataSource; import io.seata.rm.datasource.DataSourceProxy; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver; import javax.sql.DataSource; /** * @author wsk * @date 2020/3/25 21:26 */ @Configuration @MapperScan({"com.atguigu.springcloud.alibaba.dao"}) public class MyBatisConfig { @Value("${mybatis.mapper-locations}") private String mapperLocations; /** * @param sqlSessionFactory SqlSessionFactory * @return SqlSessionTemplate */ @Bean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } /** * 从配置文件获取属性构造datasource,注意前缀,这里用的是druid,根据自己情况配置, * 原生datasource前缀取"spring.datasource" * * @return */ @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource() { return new DruidDataSource(); } /** * 构造datasource代理对象,替换原来的datasource * * @param druidDataSource * @return */ @Primary @Bean("dataSource") public DataSourceProxy dataSourceProxy(DataSource druidDataSource) { return new DataSourceProxy(druidDataSource); } @Bean(name = "sqlSessionFactory") public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSourceProxy); ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); bean.setMapperLocations(resolver.getResources(mapperLocations)); SqlSessionFactory factory; try { factory = bean.getObject(); } catch (Exception e) { throw new RuntimeException(e); } return factory; } }12.主启动类
package com.atguigu.springcloud.alibaba; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; /** * @author wsk * @date 2020/3/25 20:40 */ @EnableFeignClients @EnableDiscoveryClient @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源的自带创建 public class SeataOrderMainApp2001 { public static void main(String[] args) { SpringApplication.run(SeataOrderMainApp2001.class,args); } }库存模块 seataStorageService2002 1,pom跟之前一样 2,xml
server: port: 2002 spring: application: name: seata-storage-service cloud: alibaba: seata: # 自定义事务组名称需要与seata-server中的对应 tx-service-group: fsp_tx_group nacos: discovery: server-addr: 127.0.0.1:8848 datasource: # 当前数据源操作类型 type: com.alibaba.druid.pool.DruidDataSource # mysql驱动类 driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://47.115.93.213:3306/seata_storage?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: ljs feign: hystrix: enabled: false logging: level: io: seata: info #ribbon的超时时间 ribbon: ReadTimeout: 60000 ConnectTimeout: 60000 mybatis: mapper-locations: classpath*:mapper/*.xml3,file.conf 跟上一个一样
4,registry.conf 跟上一个一样
5,domain CommonResult跟之前一样 storage
package com.atguigu.springcloud.alibaba.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * @author wsk * @date 2020/3/25 21:45 */ @Data @AllArgsConstructor @NoArgsConstructor public class Storage { private Long id; /** * 产品id */ private Long productId; /** * 总库存 */ private Integer total; /** * 已用库存 */ private Integer used; /** * 剩余库存 */ private Integer residue; }6,dao
package com.atguigu.springcloud.alibaba.dao; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; /** * @author wsk * @date 2020/3/25 21:47 */ @Mapper public interface StorageDao { /** * 减库存 * @param productId * @param count * @return */ int decrease(@Param("productId") Long productId, @Param("count") Integer count); }7,dao.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.atguigu.springcloud.alibaba.dao.StorageDao"> <resultMap id="BaseResultMap" type="com.atguigu.springcloud.alibaba.domain.Storage"> <id column="id" property="id" jdbcType="BIGINT"></id> <result column="product_id" property="productId" jdbcType="BIGINT"></result> <result column="total" property="total" jdbcType="INTEGER"></result> <result column="used" property="used" jdbcType="INTEGER"></result> <result column="residue" property="residue" jdbcType="INTEGER"></result> </resultMap> <!--减库存--> <update id="decrease"> update t_storage set used =used + #{count},residue= residue - #{count} where product_id=#{productId}; </update> </mapper>8,service
package com.atguigu.springcloud.alibaba.service; /** * @author wsk * @date 2020/3/25 21:51 */ public interface StorageService { /** * 扣减库存 * @param productId * @param count */ void decrease(Long productId,Integer count); }9,serviceImpl
package com.atguigu.springcloud.alibaba.service; import com.atguigu.springcloud.alibaba.dao.StorageDao; import com.atguigu.springcloud.alibaba.service.StorageService; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class StorageServiceImpl implements StorageService { @Resource private StorageDao storageDao; /** * 减库存 * * @param productId * @param count * @return */ @Override public void decrease(Long productId, Integer count) { storageDao.decrease(productId, count); } } 10,controller package com.atguigu.springcloud.alibaba.controller; import com.atguigu.springcloud.alibaba.domain.CommonResult; import com.atguigu.springcloud.alibaba.service.StorageService; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; /** * @author wsk * @date 2020/3/25 21:55 */ @RestController public class StorageController { @Resource private StorageService storageService; @RequestMapping("/storage/decrease") public CommonResult decrease(Long productId, Integer count){ storageService.decrease(productId,count); return new CommonResult(200,"扣减库存成功"); } }11,config 和之前一样
12,主启动类和之前一样
seata-account-service2003 1,pom一样 2。xml
server: port: 2003 spring: application: name: seata-account-service cloud: alibaba: seata: # 自定义事务组名称需要与seata-server中的对应 tx-service-group: fsp_tx_group nacos: discovery: server-addr: 127.0.0.1:8848 datasource: # 当前数据源操作类型 type: com.alibaba.druid.pool.DruidDataSource # mysql驱动类 driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://47.115.93.213:3306/seata_account?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: ljs feign: hystrix: enabled: false logging: level: io: seata: info #ribbon的超时时间 ribbon: ReadTimeout: 60000 ConnectTimeout: 60000 mybatis: mapper-locations: classpath*:mapper/*.xml3,file.conf 一样 4,registry.conf 一样 5,domain CommonResult一样 Account
package com.atguigu.springcloud.alibaba.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * 账户实体类 * * @author zzyy * @date 2020/3/8 12:28 **/ @Data @AllArgsConstructor @NoArgsConstructor public class Account { private Long id; /** * 用户id */ private Long userId; /** * 总额度 */ private Integer total; /** * 已用额度 */ private Integer used; /** * 剩余额度 */ private Integer residue; }6,dao
package com.atguigu.springcloud.alibaba.dao; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.math.BigDecimal; /** * @author wsk * @date 2020/3/25 22:05 */ @Mapper public interface AccountDao { /** * 扣减账户余额 * @param userId * @param money */ void decrease(@Param("userId") Long userId, @Param("money")BigDecimal money); }7,dao.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.atguigu.springcloud.alibaba.dao.AccountDao"> <update id="decrease"> update t_account set residue = residue- #{money},used = used + #{money} where user_id =#{userId}; </update> </mapper>8,service
package com.atguigu.springcloud.alibaba.service; import org.springframework.web.bind.annotation.RequestParam; import java.math.BigDecimal; /** * @author wsk * @date 2020/3/25 22:09 */ public interface AccountService { /** * 扣减账户余额 */ void decrease(@RequestParam("userId") Long userId, @RequestParam("money")BigDecimal money); }9,serviceImpl
package com.atguigu.springcloud.alibaba.service.impl; import com.atguigu.springcloud.alibaba.dao.AccountDao; import com.atguigu.springcloud.alibaba.service.AccountService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; /** * @author wsk * @date 2020/3/25 22:10 */ @Service public class AccountServiceImpl implements AccountService { private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class); @Resource AccountDao accountDao; @Override public void decrease(Long userId, BigDecimal money) { LOGGER.info("------>account-service中扣减账户余额开始"); //模拟异常,全局事务回滚 // int age=10/0; accountDao.decrease(userId,money); LOGGER.info("------>account-service中扣减账户余额结束"); } }10,controller
package com.atguigu.springcloud.alibaba.controller; import com.atguigu.springcloud.alibaba.domain.CommonResult; import com.atguigu.springcloud.alibaba.service.AccountService; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.math.BigDecimal; /** * @author wsk * @date 2020/3/25 22:14 */ @RestController public class AccountController { @Resource AccountService accountService; @RequestMapping("/account/decrease") public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money")BigDecimal money){ accountService.decrease(userId,money); return new CommonResult(200,"扣减账户余额成功"); } }11,config 一样
12,主启动类 一样
然后访问localhost:2001/order/create?userId=1&productId=1&count=10&money=100
将会完成数据库操作
这时候在AccountServiceImpl里模拟异常 int age=10/0;
将会导致数据库。。。。。数据不一致的问题。。。
如何让全局事务回滚?
在事务发起者OrderServiceImpl方法前上写 @GlobalTransactional(name = “fsp-create-order”,rollbackFor = Exception.class)
seata原理尽量使用1.0版本,支持集群,1.0版本支持大规模商用 TC:seata服务器 TM:事务的发起方 RM:数据库
AT模式:两阶段提交 一阶段:加载。seata拦截业务SQL在我们执行sql前seata保存到 before image 执行业务sql后保存成 after image。再加一个行锁。保证了一个阶段的原子性
二阶段:业务sql在一阶段已经提交至数据库。seata框架只需将一阶段保存的快照数据(after和before和行锁)删掉。完成数据的清除即可
二阶段回滚: 利用反向补偿机制。seata回滚。阶段已经执行的业务sql,还原业务数据。回滚则用before image还原业务数据。还原前校验脏写。对比数据库当前业务数据库和 after image的数据完全一致,则没有脏写。出现脏写。则转人工处理