SpringCloudAlibaba相关总结

639 阅读20分钟

Spring Cloud Alibaba 微服工具集

版本: 2.2.1

1.简介

Spring Cloud Alibaba provides a one-stop solution for distributed application development. It contains all the components required to develop distributed applications, making it easy for you to develop your applications using Spring Cloud.

With Spring Cloud Alibaba, you only need to add some annotations and a small amount of configurations to connect Spring Cloud applications to the distributed solutions of Alibaba, and build a distributed application system with Alibaba middleware.

# 0.原文翻译
- https://spring.io/projects/spring-cloud-alibaba
- 阿里云为分布式应用开发提供了一站式解决方案。它包含了开发分布式应用程序所需的所有组件,使您可以轻松地使用springcloud开发应用程序。
- 有了阿里云,你只需要添加一些注解和少量的配置,就可以将Spring云应用连接到阿里的分布式解决方案上,用阿里中间件搭建一个分布式应用系统。

2.环境搭建

# 0.构建项目并引入依赖
<!--定义springcloud版本-->
<properties>
  <spring.cloud.alibaba.version>2.2.1.RELEASE</spring.cloud.alibaba.version>
</properties>

<!--全局引入springcloudalibaba下载依赖地址,并不会引入依赖-->
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-alibaba-dependencies</artifactId>
      <version>${spring.cloud.alibaba.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

3.Nacos

什么是Nacos Name Service & Configurations Services

- https://nacos.io/zh-cn/index.html
- Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
  • 总结:Nacos就是微服务架构中服务注册中心以及统一配置中心,用来替换原来的(eureka,consul)以及config组件

安装Nacos

# 0.准备环境
- 1.64 bit OS,支持 Linux/Unix/Mac/Windows,推荐选用 Linux/Unix/Mac。
- 2.64 bit JDK 1.8+;下载 & 配置。
- 3.Maven 3.2.x+;下载 & 配置。

# 1.下载nacos [本次课程版本:][1.3.0版本]
- https://github.com/alibaba/nacos/releases 

# 2.解压缩安装包到指定位置
- bin  			启动nacos服务的脚本目录
- conf 			nacos的配置文件目录
- target 		nacos的启动依赖存放目录
- data		        nacos启动成功后保存数据的目录

# 3.启动安装服务
- linux/unix/mac启动
	打开终端进入nacos的bin目录执行如下命令 
	./startup.sh -m standalone

- windows启动
	在 cmd中 
	执行 startup.cmd -m standalone 或者双击startup.cmd运行文件。

# 4.访问nacos的web服务管理界面
- http://localhost:8848/nacos/
- 用户名 和 密码都是nacos

开发服务注册到nacos

# 0.创建项目并引入依赖
<!--引入nacos client的依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

# 1.配置注册地址
server.port=8789 																												#指定当前服务端口
spring.application.name=nacosclient																			                    #指定服务名称
spring.cloud.nacos.server-addr=localhost:8848														                            #指定nacos服务地址
spring.cloud.nacos.discovery.server-addr=${spring.cloud.nacos.server-addr} #指定注册中心地址							
management.endpoints.web.exposure.include=*														                                #暴露所有web端点
# 2.加入启动服务注册注解 [注意:][新版本之后这步可以省略不写]

# 3.查看nacos的服务列表

使用nacos作为配置中心

1.从nacos获取配置

# 1.创建项目并引入nacons配置中心依赖
<!--引入nacos client依赖-->
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<!--引入nacos config 依赖-->
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

# 2.配置配置中心地址
spring.cloud.nacos.server-addr=localhost:8848						    # 远程配置中心的地址
spring.cloud.nacos.config.server-addr=${spring.cloud.nacos.server-addr}	                    # 远程配置中心的地址
spring.cloud.nacos.config.group=DEFAULT_GROUP						    # 读取配置的分组
spring.cloud.nacos.config.file-extension=properties					    # 指定读取文件后缀
spring.application.name=config								    # 指定读取文件的前缀
spring.profiles.active=prod								    # 指定读取文件的具体环境

# 3.在nacos中创建配置

# 4.编写控制器测试配置读取情况
@RestController
@Slf4j
public class HelloController {
    //注入配置
    @Value("${user.name}")
    private String username;
    @GetMapping("/hello/config")
    public String config(){
        log.info("用户名: [{}]",username);
        return username;
    }
}

# 5.启动项目方式测试配置读取

2. DataId

# 1.DataId
- 用来读取远程配置中心的中具体配置文件其完整格式如下:
- ${prefix}-${spring.profile.active}.${file-extension}
	a. prefix 默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix来配置。
	
	b. spring.profile.active 即为当前环境对应的 profile,详情可以参考 Spring Boot文档。 注意:当 spring.profile.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension}
	
	c. file-exetension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 properties 和 yaml 类型。

3.实现自动配置刷新

# 1.自动刷新
- 默认情况下nacos已经实现了自动配置刷新功能,如果需要刷新配置直接在控制器中加入@RefreshScope注解即可
@RestController
@Slf4j
@RefreshScope
public class HelloController {
    //注入配置
    @Value("${user.name}")
    private String username;
    @GetMapping("/hello/config")
    public String config(){
        log.info("用户名: [{}]",username);
        return username;
    }
}

4.命名空间

# 1.命名空间(namespace)
- https://github.com/alibaba/spring-cloud-alibaba/wiki/Nacos-config
- namespace命名空间是nacos针对于企业级开发设计用来针对于不同环境的区分,比如正在企业开发时有测试环境,生产环境,等其他环境,因此为了保证不同环境配置实现隔离,提出了namespace的概念,默认在nacos中存在一个public命名空间所有配置在没有指定命名空间时都在这个命名空间中获取配置,在实际开发时可以针对于不能环境创建不同的namespace空间。默认空间不能删除!

# 2.创建其他命名空间
- 每个命名空间都有一个唯一id,这个id是读取配置时指定空间的唯一标识

# 3.在配置列表查看空间

# 4.在指定空间下载创建配置文件

# 5.项目中使用命名空间指定配置

# 6.测试配置

5.配置分组

# 1.配置分组(group)
- 配置分组是对配置集进行分组,通过一个有意义的字符串(如 Buy 或 Trade )来表示,不同的配置分组下可以有相同的配置集(Data ID)。当您在 Nacos 上创建一个配置时,如果未填写配置分组的名称,则配置分组的名称默认采用 DEFAULT_GROUP 。配置分组的常见场景:可用于区分不同的项目或应用,例如:学生管理系统的配置集可以定义一个group为:STUDENT_GROUP。

# 2.创建分组

# 3.读取不同分组的配置

4.sentinel 流量卫兵

什么是sentinel

As microservices become popular, the stability of service calls is becoming increasingly important. Sentinel takes "flow" as the breakthrough point, and works on multiple fields including flow control, circuit breaking and load protection to protect service reliability. ---[摘自官网]

# 0.说明
- https://spring-cloud-alibaba-group.github.io/github-pages/hoxton/en-us/index.html#_how_to_use_sentinel
- https://github.com/alibaba/Sentinel/wiki
- 翻译:随着微服务的普及,服务调用的稳定性变得越来越重要。Sentinel以“流量”为突破口,在流量控制、断路、负载保护等多个领域进行工作,保障服务可靠性。
- 通俗:用来在微服务系统中保护微服务对的作用 如何 服务雪崩 服务熔断  服务降级 就是用来替换hystrix

# 1.特性
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。

- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。

- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。

sentinel使用

- sentinel提供了两个服务组件:
	一个是 sentinel 用来实现微服务系统中服务熔断、降级等功能。这点和hystrix 类似
	一个是 sentinel dashboard 用来监控微服务系统中流量调用等情况。这点和hystrix 类似

1. sentinel dashboard的安装

# 1.下载
- https://github.com/alibaba/Sentinel/releases

# 2.启动
- 仪表盘是个jar包可以直接通过java命令启动 如: java -jar 方式运行 默认端口为 8080
- java -Dserver.port=9191 -jar  sentinel-dashboard-1.7.2.jar

# 3.访问web界面
- http://localhost:9191/#/login

# 4.登录
- 用户名&密码: sentinel

2.sentinel 实时监控服务

# 1.创建项目引入依赖
<!--引入nacos client依赖-->
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<!--引入sentinel依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

# 2.配置
server.port=8789
spring.application.name=nacosclient
spring.cloud.nacos.server-addr=localhost:8848
spring.cloud.nacos.discovery.server-addr=${spring.cloud.nacos.server-addr}

spring.cloud.sentinel.enabled=true		         # 开启sentinel 默认开启
spring.cloud.sentinel.transport.dashboard=localhost:9191 # 连接dashboard
spring.cloud.sentinel.transport.port=8719		 # 与dashboard通信的端口

# 3.启动服务

# 4.访问dashboard界面查看服务监控
- 发现界面什么都没有? 
- 默认情况下sentiel为延迟加载,不会在启动之后立即创建服务监控,需要对服务进行调用时才会初始化

# 5.开发服务
@RestController
@Slf4j
public class SentinelController {
    @GetMapping("/sentinel/test")
    public String test(){
        log.info("sentinel test");
        return "sentinel test ";
    }

    @GetMapping("/sentinel/test1")
    public String test1(){
        log.info("sentinel test1");
        return "sentinel test1 ";
    }
}

# 6.启动进行调用
- http://localhost:8789/sentinel/test

# 7.查看监控界面

3.sentinel 流量控制

# 0.说明
- 流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。

- 同一个资源可以创建多条限流规则。FlowSlot 会对该资源的所有限流规则依次遍历,直到有规则触发限流或者所有规则遍历完毕。

- 一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:
	resource:资源名,即限流规则的作用对象
	count: 限流阈值
	grade: 限流阈值类型(QPS 或并发线程数)
	limitApp: 流控针对的调用来源,若为 default 则不区分调用来源
	strategy: 调用关系限流策略
	controlBehavior: 流量控制效果(直接拒绝、Warm Up、匀速排队)

- 流量控制主要有两种统计类型,一种是统计并发线程数,另外一种则是统计 QPS
- 更多细节参见官网:https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6
QPS限流
# 1.配置QPS流量控制

# 2.测试
- 每秒只能最大接收1个请求,超过1个报错

线程数限流
# 1.配置线程数限流

# 2.访问测试

流控模式
# 1.说明
- 直接:标识流量控制规则到达阈值直接触发流量控制
- 关联: 当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢,举例来说,read_db 和 write_db 这两个资源分别代表数据库读写,我们可以给 read_db 设置限流规则来达到写优先的目的:设置 strategy 为 RuleConstant.STRATEGY_RELATE 同时设置 refResource 为 write_db。这样当写库操作过于频繁时,读数据的请求会被限流。

- 链路限流: https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6
流控效果
- 直接拒绝:(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。

- Warm Up:(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
	更多:https://github.com/alibaba/Sentinel/wiki/%E9%99%90%E6%B5%81---%E5%86%B7%E5%90%AF%E5%8A%A8
	
	
- 匀速排队:(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。 只能对请求进行排队等待
	更多:https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6-%E5%8C%80%E9%80%9F%E6%8E%92%E9%98%9F%E6%A8%A1%E5%BC%8F

4.熔断降级

# 0.说明
- https://github.com/alibaba/Sentinel/wiki/%E7%86%94%E6%96%AD%E9%99%8D%E7%BA%A7
- 除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积。Sentinel **熔断降级**会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 `DegradeException`)。
降级策略
  • 平均响应时间 (DEGRADE_GRADE_RT):当 1s 内持续进入 N 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以 ms 为单位),那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。

  • 异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒请求量 >= N(可配置),并且每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。

  • 异常数 (DEGRADE_GRADE_EXCEPTION_COUNT):当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态。

5.SentinelResource注解

# 0.说明
- https://github.com/alibaba/Sentinel/wiki/%E6%B3%A8%E8%A7%A3%E6%94%AF%E6%8C%81

 @GetMapping("/sentinel/test1")
    @SentinelResource(value = "aa",blockHandler = "fallBack",fallback = "fall")
    public String test1(int id){
        log.info("sentinel test1");
        if(id<0)		
            throw new RuntimeException("非法参数!!!");
        }
        return "sentinel test1 :"+id;
    }
		//降级异常处理
    public String fallBack(int id,BlockException e){
            if(e instanceof FlowException){
                return "当前服务已被流控! "+e.getClass().getCanonicalName();
            }
            return "当前服务已被降级处理! "+e.getClass().getCanonicalName();
    }
		//异常处理
    public String fall(int id){
        return "当前服务已不可用!";
    }

5.整合环境公共依赖

spring boot 2.2+

springcloud Hoxton

springcloud alibaba 2.2.1+

# 0.构建项目并引入依赖

<properties>
  <java.version>1.8</java.version>
  <spring-cloud.version>Hoxton.SR6</spring-cloud.version>
  <spring.cloud.alibaba.version>2.2.1.RELEASE</spring.cloud.alibaba.version>
</properties>

<dependencyManagement>
  <dependencies>
    <!--引入springcloud alibaba-->
    <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-alibaba-dependencies</artifactId>
      <version>${spring.cloud.alibaba.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
    <!--引入springcloud-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>${spring-cloud.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

---------------------------------------我是分割线------------------------------------------------

Nacos

官方文档地址:nacos.io/zh-cn/docs/…

1、Nacos服务注册与发现

1.1、启动Nacos服务

下载nacos服务端,有多种方式,这里以windows平台1.2.0.beta.1编译后的压缩包单机模式为例,更多方式参考:nacos.io/zh-cn/docs/…

下载解压之后到nacos->bin->startup.cmd,直接双击运行即可。

浏览器访问localhost:8848可以看到以下界面

1.2 创建Maven父工程

pom.xml

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

1.3 服务提供者

@SpringBootApplication
@EnableDiscoveryClient // 新版本加不加都可以了
public class NacosProviderApplication {

    public static void main(String[] args) {
        SpringApplication.run(NacosProviderApplication.class, args);
    }

    @RestController
    class EchoController {
        @RequestMapping(value = "/echo/{string}", method = RequestMethod.GET)
        public String echo(@PathVariable String string) {
            return "Hello Nacos Discovery " + string;
        }
    }
}

application.yml

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848 # 注意了前面不要加http:// 
  application:
    name: service-provider  # 注意名称尽量不要带_和特殊字符 
server:
  port: 8090

1.4 消费者

@SpringBootApplication
@EnableDiscoveryClient
public class NacosConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(NacosConsumerApplication.class, args);
    }

    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
@RestController
public class TestController {
    @Autowired
    private  RestTemplate restTemplate;

    @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
    public String echo(@PathVariable String str) {

        return restTemplate.getForObject("http://service-provider/echo/" + str, String.class);
    }
}

2、Nacos领域模型划分

NameSpace:命名空间,默认的NameSpace是public。比如,我们开发,测试环境共用一个nacos,必定我们的接口地址也是不同,而且你在开发过程中,也是不建议随意配置测试环境的,这时我们就应该用namespace来隔离我们的空间。 group:分组。也是用来隔离的,打个比方啊,加入我们的用户服务,订单服务,仓储服务和物流服务四个服务,订单服务中有一个接口叫getData,仓储服务中也有一个接口叫getData,我们的用户服务只想调用到我们的订单服务的getData,不想调用到仓储服务的getData,这时我们可以用group分组来隔离。
cluster:集群。打个比方,我们现在有两组集群,一组是北京的订单服务集群,北京的商品服务集群,还有一组是南京的订单服务集群,南京的商品服务集群。 我们希望北京的订单集群,优先去调用北京的商品系统,南京的优先调用南京的集群服务。并不希望我们跨地区远程调用(如果组内实在没有服务了,也可以调用,但是优先考虑同一集群的)
metadata: 只要用于版本控制。比如,我们在开发中可能是多个版本共存的,订单的v1版本只能调商品的v1版本,订单的v2版本只能调商品的v2版本。

配置:

spring:
  application:
    name: test-demo
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: 0610f97e-c25d-4f49-bfb8-b340e3584b82
        group: test
        cluster-name: BJ-cluster
        metadata:
          current-version: V1
server:
  port: 8888

Ribbon

是开源的客户端负载均衡器。
ribbon会从nacos上获取要调用的服务地址列表,通过自有算法算出一个实例,交给restTemplate去请求。
不需要加依赖,因为nacos-discovery包中已经包含ribbon
相关概念前面的博客以做相关介绍,这里对相关配置做出介绍。

//注解需要写在restTemplate上,为restTemplate整合ribbon
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
    return new RestTemplate();
}

当restTemplate请求时,ribbon会自动把server在nacos中的url的http地址转化成server自己的服务名称,
例如 http://localhost:8080/users/{id}会变为http://user-center/users/{id}

ribbon的组成

在Spring Cloud中,Ribbon默认配置如下:

IClientConfig:Ribbon 的客户端配置,默认采用 com.netflix.client.config.DefaultClientConfigImpl 实现。
IRule:Ribbon 的负载均衡策略,默认采用 com.netflix.loadbalancer.ZoneAvoidanceRule 实现,该策略能够在多区域环境下选择最佳区域的实例进行访问
IPing:Ribbon 的实例检查策略,默认采用 com.netflix.loadbalancer.NoOpPing 实现,该检查策略是一个特殊的实现,实际上他并不会检查实例是否可用,而是始终返回 true ,默认认为所有服务实例都是可以使用
ServerList<Server>:服务实例清单的维护机制,默认采用 com.netflix.loadbalancer.ConfigurationBasedServerList 实现。
ServerListFilter<Server>:服务实例清单过滤机制,默认采用 org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter 实现,该策略能够优先过滤出与请求调用方处理同区域的服务实现
ILoadBalancer:负载均衡器,默认采用 com.netflix.loadbalancer.ZoneAwareLoadBalancer 实现,他具备了区域感知的能力

IRule配置(ribbon的负载均衡规则)

Java代码配置实现方式

// 注意这个类要放在启动类扫描不到的外部位置
@Configuration
public class RibbonConfiguration {
    @Bean
    public IRule ribbonRule(){
        return new RandomRule();
    }
}
/*------------------------------------------------------------*/
/**
 * UserCenterRibbonConfiguration for 自定义实现User-center service ribbon client
 *
 * @author <a href="mailto:magicianisaac@gmail.com">Isaac.Zhang | 若初</a>
 * @since 2019/7/13
 */
@Configuration
@RibbonClient(name = "user-center", configuration = RibbonConfiguration.class)
public class UserCenterRibbonConfiguration {
    
}
/*------------------------------------------------------------*/
@Configuration
//@RibbonClient(name = "user-center", configuration = RibbonConfiguration.class) //作用域为 user-center
@RibbonClients(defaultConfiguration = RibbonConfiguration.class) //作用域为全局
public class UserCenterRibbonConfiguration {

}

使用配置文件

user-center: # service name
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 规则类的全路径名称

最佳实践

  • 尽量使用属性配置
  • 在同一个微服务中尽量保持单一配置,不要混合使用,增加定位复杂性

Ribbon其他配置项

例如修改IPing规则

代码方式
@Configuration
public class RibbonConfiguration {
    @Bean
    public IPing ping(){
        return new PingUrl();
    }
}
配置文件

Ribbon 饥饿加载

默认情况下,Ribbon是懒加载,在第一次请求的时候才会创建客户端。

ribbon:
  eager-load:
    enabled: true # 饥饿加载激活
    clients: user-center,xxx,xxx # 为哪些clients开启

扩展Ribbon - 支持Nacos权重

  • 接口com.netflix.loadbalancer.IRule
  • 抽象类 com.netflix.loadbalancer.AbstractLoadBalancerRule
    以上两种方式都行。
@Slf4j
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class NacosWeightRule extends AbstractLoadBalancerRule {

    private final NacosDiscoveryProperties nacosDiscoveryProperties;

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
        // 读取配置文件,并初始化 NacosWeightRule4Ribbon
    }

    @Override
    public Server choose(Object key) {

        try {
            // ILoadBalancer 是Ribbon的入口,基本上我们想要的元素都可以在这个对象中找到
            BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
            log.info("NacosWeightRule4Ribbon lb = {}", loadBalancer);
            // 想要请求的微服务名称
            String name = loadBalancer.getName();

            // 实现负载均衡算法
            // 可得到服务发现相关的API(nacos内部实现)
            NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();

            // nacos client 通过基于权重的负载均衡算法,选择一个实例(内置了)
            Instance instance = namingService.selectOneHealthyInstance(name);
            log.info("port = {}, weight = {}, instance = {}", instance.getPort(), instance.getWeight(), instance);
            return new NacosServer(instance);
        } catch (NacosException e) {
            log.error("NacosWeightRule4Ribbon {}", e.getMessage());
        }
        return null;
    }
}

@Configuration
public class RibbonConfiguration {
    @Bean
    public IRule ribbionRule(){
        return new NacosWeightRule();
    }
}

@Configuration
@RibbonClients(defaultConfiguration = RibbonConfiguration.class) //全局配置
public class UserCenterRibbonConfiguration {
}

上述是采用全局配置的方式,也可以采用配置文件的方式进行相应的配置
配置参照上面即可。
修改权重大小在nacos控制台,针对实例设置权重的数字即可,。

user-center: # service name
  ribbon:
    NFLoadBalancerRuleClassName: com.**.**.NacosWeightRule  

Ribbon同一集群优先调用的负载均衡规则

spring:
  cloud:
    nacos: 
      discovery:
        #设置集群名称,例如:HAOZI
        cluster-name: HAOZI
public class NacosSameClusterWeightedRule extends AbstractLoadBalancerRule {
    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;
 
    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
    }
 
    @Override
    public Server choose(Object key) {
        try {
            //拿到配置文件中的集群名称 HAOZI
            String clusterName = nacosDiscoveryProperties.getClusterName();
            //loadBalancer是ribbon的入口,想要的基本都有
            BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
            //想要请求的微服务的名称
            String name = loadBalancer.getName();
            //可拿到服务发现相关api
            NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
            //1.找到指定服务的所有实例 A
            //true代表只拿健康的实例
            List<Instance> instances = namingService.selectInstances(name , true);
            //2.过滤出相同集群下的所有实例 B
            List<Instance> sameClusterInstances = instances.stream()
                .filter(instance -> Objects.equals(instance.getClusterName(), clusterName))
                .collect(Collectors.toList());
            //3.如果B是空,就用A
            List<Instance> instancesToBeChosen = new ArrayList<>();
            if(CollectionUtils.isEmpty(sameClusterInstances)){
                instancesToBeChosen = instances;
            }else{
                instancesToBeChosen = sameClusterInstances;
            }
            //4.基于权重的负载均衡算法,返回一个实例
            Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToBeChosen);
            return new NacosServer(instance);
        } catch (NacosException e) {
            log.error("异常",e);
            return null;
        }
    }
}
//由于没有基于权重的负载均衡方法调用,所以通过源码找到,但源码的getHostByRandomWeight是protected的,
//所以写类继承该类并通过子类调用该方法并返回
class ExtendBalancer extends Balancer {
    public static Instance getHostByRandomWeight2(List<Instance> hosts){
        return getHostByRandomWeight(hosts);
    }
}

ribbon基于元数据的版本控制的负载均衡规则

写配置

spring:
  cloud:
    nacos:
        metadata: 
          # 自己这个实例的版本
          version: v1
          # 允许调用的提供者版本
          target-version: v1

写规则

public class NacosFinalRule extends AbstractLoadBalancerRule {
    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;
 
    @Override
    public Server choose(Object key) {
        // 负载均衡规则:优先选择同集群下,符合metadata的实例
        // 如果没有,就选择所有集群下,符合metadata的实例
        try {
            //通过配置文件拿到集群名称
            String clusterName = this.nacosDiscoveryProperties.getClusterName();
            //通过元数据拿到可以调用的版本信息
            String targetVersion = this.nacosDiscoveryProperties.getMetadata().get("target-version");
 
            DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
            //微服务名称
            String name = loadBalancer.getName();
            //拿到服务相关api
            NamingService namingService = this.nacosDiscoveryProperties.namingServiceInstance();
 
            // 1. 查询所有实例 A
            List<Instance> instances = namingService.selectInstances(name, true);
 
            List<Instance> metadataMatchInstances = instances;
            // 2. 筛选元数据匹配的实例 B
            // 如果配置了版本映射,那么只调用元数据匹配的实例
            if (StringUtils.isNotBlank(targetVersion)) {
                metadataMatchInstances = instances.stream()
                        .filter(instance -> Objects.equals(targetVersion, instance.getMetadata().get("version")))
                        .collect(Collectors.toList());
                if (CollectionUtils.isEmpty(metadataMatchInstances)) {
                    log.warn("未找到元数据匹配的目标实例!请检查配置。targetVersion = {}, instance = {}", targetVersion, instances);
                    return null;
                }
            }
 
            List<Instance> clusterMetadataMatchInstances = metadataMatchInstances;
            // 3. 筛选出同cluster下元数据匹配的实例 C
            // 如果配置了集群名称,需筛选同集群下元数据匹配的实例
            if (StringUtils.isNotBlank(clusterName)) {
                clusterMetadataMatchInstances = metadataMatchInstances.stream()
                        .filter(instance -> Objects.equals(clusterName, instance.getClusterName()))
                        .collect(Collectors.toList());
                // 4. 如果C为空,就用B
                if (CollectionUtils.isEmpty(clusterMetadataMatchInstances)) {
                    clusterMetadataMatchInstances = metadataMatchInstances;
                    log.warn("发生跨集群调用。clusterName = {}, targetVersion = {}, clusterMetadataMatchInstances = {}", clusterName, targetVersion, clusterMetadataMatchInstances);
                }
            }
            // 5. 随机选择实例
            Instance instance = ExtendBalancer.getHostByRandomWeight2(clusterMetadataMatchInstances);
            return new NacosServer(instance);
        } catch (Exception e) {
            log.warn("发生异常", e);
            return null;
        }
    }
 
    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
    }
}
 
public class ExtendBalancer extends Balancer {
    /**
     * 根据权重,随机选择实例
     *
     * @param instances 实例列表
     * @return 选择的实例
     */
    public static Instance getHostByRandomWeight2(List<Instance> instances) {
        return getHostByRandomWeight(instances);
    }
}

注:启动类别忘了注入RestTemplate

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    RestTemplate template = new RestTemplate();
    return template;
}

Feign

Feign的组成

Feign的日记级别

首先如何整合Feign

遵循SpringBoot的三板斧

第一步:加依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

第二步:写注解

@EnableFeignClients //在启动类上加

第三步:写配置,无需配置

如何给Feign添加日志级别

方式一:代码实现
第一步:添加Feign配置类,可以添加在主类下,但是不用添加@Configuration。如果添加了@Configuration而且又放在了主类之下,那么就会所有Feign客户端实例共享,同Ribbon配置类一样父子上下文加载冲突;如果一定添加@Configuration,就放在主类加载之外的包。建议还是不用加@Configuration。

public class FeignConfig {
    @Bean
    public Logger.Level Logger() {
        return Logger.Level.FULL;
    }
}

第二步:给@FeignClient添加配置类

@FeignClient(name = "goods", configuration = FeignConfig.class)

第三步:写配置

logging:
  level:
    com.xxx.xxx.FeignAPI: DEBUG #需要将FeignClient接口全路径写上# 开启日志 格式为logging.level.+Feign客户端路径

方式二:配置属性实现

feign:
  client:
    config:
      #想要调用的微服务名称
      server-1:
        loggerLevel: FULL

全局配置
方式一:代码实现添加了@Configuration放在了主类之下,那么就会所有Feign客户端实例共享,同Ribbon配置类一样父子上下文加载冲突;让父子上下文ComponentScan重叠(强烈不建议)唯一正确方式

//在启动类上为@EnableFeignClients注解添加defaultConfiguration配置
@EnableFeignClients(defaultConfiguration = FeignConfig.class)

方式二:配置属性实现

feign:
  client:
    config:
      #将调用的微服务名称改成default就配置成全局的了
      default:
        loggerLevel: FULL

Ribbon配置 VS Feign配置

Feign 代码方式 VS 配置属性方式

如何使用Feign构造多参数的请求

GET请求多参数的URL

假设需请求的URL包含多个参数,例如http://microservice-provider-user/get?id=1&username=张三 ,该如何使用Feign构造呢?

我们知道,Spring Cloud为Feign添加了Spring MVC的注解支持,那么我们不妨按照Spring MVC的写法尝试一下:

@FeignClient("microservice-provider-user")
public interface UserFeignClient {
  @RequestMapping(value = "/get", method = RequestMethod.GET)
  public User get0(User user);
}

然而,这种写法并不正确,控制台会输出类似如下的异常。

feign.FeignException: status 405 reading UserFeignClient#get0(User); content:
{"timestamp":1482676142940,"status":405,"error":"Method Not Allowed","exception":"org.springframework.web.HttpRequestMethodNotSupportedException","message":"Request method 'POST' not supported","path":"/get"}

由异常可知,尽管我们指定了GET方法,Feign依然会使用POST方法发送请求。于是导致了异常。正确写法如下

方法一[推荐]

@FeignClient("microservice-provider-user")
public interface UserFeignClient {
  @GetMapping("/get")
  public User get0(@SpringQueryMap User user);
}

方法二[推荐]

@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient {
  @RequestMapping(value = "/get", method = RequestMethod.GET)
  public User get1(@RequestParam("id") Long id, @RequestParam("username") String username);
}

这是最为直观的方式,URL有几个参数,Feign接口中的方法就有几个参数。使用@RequestParam注解指定请求的参数是什么。

POST请求包含多个参数

下面来讨论如何使用Feign构造包含多个参数的POST请求。假设服务提供者的Controller是这样编写的:

@RestController
public class UserController {
  @PostMapping("/post")
  public User post(@RequestBody User user) {
    ...
  }
}

我们要如何使用Feign去请求呢?答案非常简单,示例:

@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient {
  @RequestMapping(value = "/post", method = RequestMethod.POST)
  public User post(@RequestBody User user);
}

Sentinel

Alibaba Sentinel 规则参数总结

基于Sentinel 1.6.2编写.

一、流控规则

1.1 配置 1.2 参数 1.3 代码配置示例

private void initFlowQpsRule() {
    List<FlowRule> rules = new ArrayList<>();
    FlowRule rule = new FlowRule(resourceName);
    // set limit qps to 20
    rule.setCount(20);
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    rule.setLimitApp("default");
    rules.add(rule);
    FlowRuleManager.loadRules(rules);
}

1.4 参考:github.com/alibaba/Sen…
1.5 参考 github.com/alibaba/Sen…

二、降级规则

2.1 配置 2.2 参数 2.3 代码配置示例

private void initDegradeRule() {
    List<DegradeRule> rules = new ArrayList<>();
    DegradeRule rule = new DegradeRule();
    rule.setResource(KEY);
    // set threshold RT, 10 ms
    rule.setCount(10);
    rule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
    rule.setTimeWindow(10);
    rules.add(rule);
    DegradeRuleManager.loadRules(rules);
}

2.4 参考github.com/alibaba/Sen…

三、热点规则

3.1 配置 3.2 参数 3.3 代码配置示例

ParamFlowRule rule = new ParamFlowRule(resourceName)
    .setParamIdx(0)
    .setCount(5);
// 针对 int 类型的参数 PARAM_B,单独设置限流 QPS 阈值为 10,而不是全局的阈值 5.
ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf(PARAM_B))
    .setClassType(int.class.getName())
    .setCount(10);
rule.setParamFlowItemList(Collections.singletonList(item));

ParamFlowRuleManager.loadRules(Collections.singletonList(rule));

3.4 参考github.com/alibaba/Sen…

四、系统规则

4.1 配置 4.2 参数 4.3 代码配置示例

private void initSystemRule() {
    List<SystemRule> rules = new ArrayList<>();
    SystemRule rule = new SystemRule();
    rule.setHighestSystemLoad(10);
    rules.add(rule);
    SystemRuleManager.loadRules(rules);
}

4.4 参考github.com/alibaba/Sen…

五、授权规则

5.1 配置 5.2 参数 5.3 代码配置示例

AuthorityRule rule = new AuthorityRule();
rule.setResource("test");
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
rule.setLimitApp("appA,appB");
AuthorityRuleManager.loadRules(Collections.singletonList(rule));

5.4 参考github.com/alibaba/Sen…

SentinelResource注解 属性总结

1.6.0 之前的版本 fallback 函数只针对降级异常(DegradeException)进行处理,不能针对业务异常进行处理。
若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。若未配置 blockHandler、fallback 和 defaultFallback,则被限流降级时会将 BlockException 直接抛出。
从 1.4.0 版本开始,注解方式定义资源支持自动统计业务异常,无需手动调用 Tracer.trace(ex) 来记录业务异常。Sentinel 1.4.0 以前的版本需要自行调用 Tracer.trace(ex) 来记录业务异常。

Spring Cloud Stream

原理图:

Destination Binder

与外部消息系统通信的组件,为构造 Binding提供了 2 个方法,分别是 bindConsumerbindProducer ,它们分别用于构造生产者和消费者。Binder使Spring Cloud Stream应用程序可以灵活地连接到中间件,目前springkafka、rabbitmq提供binder

Destination Binding

Binding 是连接应用程序跟消息中间件的桥梁,用于消息的消费和生产,由binder创建。

SpringCloudAlibaba—Stream+Rocketmq消息微服务集成

加依赖

生产者消费者都需要添加

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
</dependency>

加注解

生产者: 消费者将注解换成@EnableBinding(Sink.class)

写配置

生产者 消费者

生产者代码

消费者代码

SpringCloudAlibaba—Stream自定义生产者接口

(1)创建接口:

创建一个自定义生产者的binding对应的接口,在接口中写一个out()方法,返回值为MessageChannel,并在方法上添加注解Output,指定一个自定义的binding常量值,这个常量值是与配置文件中写的那个自定义binding值保持一致。

(2)修改启动类:

在启动类上,绑定刚刚创建的自定义binding对应的那个接口。

(3)修改配置文件:

在配置文件中,添加一个自定义的binding的信息,注意binding绑定的名字要与对应接口中定义的常量字符串保持一致。

(4)编写代码:

首先通过Autowired创建自定义的接口对象,然后在方法体中,通过自定义接口对象调用output()方法发送消息。

(5)启动测试:

启动消息生产者,访问接口,然后就会显示发送消息成功。在rocketmq的控制台中可以看到该topic下有刚刚发送的消息。这就表明stream自定义生产者接口成功了,按照这种模式,可以根据自己实际业务需要,配置若干个不同的自定义接口来完成各种业务数据的发送了。

(6)常见异常:

ibatis.binding异常
如果项目中使用了mybatis,那么可能会在使用消息队列的时候,应该会报一个异常:org.apache.ibatis.binding.BindingException,它会因为写mq自定义的接口为mybatis的接口,结果因为找不到对应的xml而报错。

解决方法:我们只需要精确mybatis的接口扫描路径,不让它扫描到我们写的mq的接口即可。

SpringCloudAlibaba—Stream自定义消费者接口

(1)创建接口:

创建一个自定义消费者的binding对应的接口,在接口中写一个input()方法,返回值为SubscribableChannel,并在方法上添加注解input,指定一个自定义的binding常量值,这个常量值是与配置文件中写的那个自定义binding值保持一致。

(2)修改启动类:

在启动类上,绑定刚刚创建的自定义binding对应的那个接口。

(3)修改配置文件:

在配置文件中,添加一个自定义的binding的信息,注意binding绑定的名字要与对应接口中定义的常量字符串保持一致。

(4)编写代码:

创建一个实体类,类上添加service注解,在类中创建一个方法,方法有一个string类型的参数(该参数就是消息体),方法上添加StreamListener,并指定刚才创建自定义接口中的自定义常量。

(5)启动测试:

启动消息消费者,就会打印对应rocketmq中已存在的未消费的消息内省,并且对应topic的生产者发送一条消息,消费者也会马上打印该消息内容。这就表明stream自定义生产者接口成功了,按照这种模式,可以根据自己实际业务需要,配置若干个不同的自定义接口来完成各种业务数据的消费了。

SpringCloudStream与RabbitMQ及KafKa集成

参考: blog.csdn.net/LSY_CSDN_/a…

Spring Cloud Stream实现消息过滤消费

Condition

生产者
生产者设置一下header,比如my-header,值根据你的需要填写:

@Autowired
private Source source;

public String testStream() {
  this.source.output()
    .send(
    MessageBuilder
    .withPayload("消息体")
    .setHeader("my-header","你的header")
    .build()
  );
  return "success";
}

消费者

@Service
@Slf4j
public class TestStreamConsumer {
    @StreamListener(value = Sink.INPUT,condition = "headers['my-header']=='你的header'")
    public void receive(String messageBody) {
        log.info("通过stream收到了消息:messageBody ={}", messageBody);
    }
}

如代码所示,使用 StreamListener 注解的 condition 属性。当 headers['my-header']=='你的header' 条件满足,才会进入到方法体。

Tags

该方式只支持RoketMQ,不支持Kafka/RabbitMQ

生产者

@Autowired
private Source source;

public String testStream() {
  this.source.output()
    .send(
    MessageBuilder
    .withPayload("消息体")
    // 注意:只能设置1个tag
    .setHeader(RocketMQHeaders.TAGS, "tag1")
    .build()
  );
  return "success";
}

消费者

  • 接口
public interface MySink {
    String INPUT1 = "input1";
    String INPUT2 = "input2";

    @Input(INPUT1)
    SubscribableChannel input();

    @Input(INPUT2)
    SubscribableChannel input2();
}
  • 注解
@EnableBinding({MySink.class})
  • 配置
spring:
  cloud:
    stream:
      rocketmq:
        binder:
          name-server: 127.0.0.1:9876
        bindings:
          input1:
            consumer:
              # 表示input2消费带有tag1的消息
              tags: tag1
          input2:
            consumer:
              # 表示input2消费带有tag2或者tag3的消息
              tags: tag2||tag3
      bindings:
        input1:
          destination: test-topic
          group: test-group1
        input2:
          destination: test-topic
          group: test-group2

消费代码

@Service
@Slf4j
public class MyTestStreamConsumer {
    /**
     * 我消费带有tag1的消息
     *
     * @param messageBody 消息体
     */
    @StreamListener(MySink.INPUT1)
    public void receive1(String messageBody) {
        log.info("带有tag1的消息被消费了:messageBody ={}", messageBody);
    }

    /**
     * 我消费带有tag1或者tag2的消息
     *
     * @param messageBody 消息体
     */
    @StreamListener(MySink.INPUT2)
    public void receive2(String messageBody) {
        log.info("带有tag2/tag3的消息被消费了:messageBody ={}", messageBody);
    }
}

参考: www.itmuch.com/spring-clou…

Spring Cloud Stream + RocketMQ实现分布式事务

发送消息

在Spring消息编程模型下,使用RocketMQ收发消息 一文中,发送消息使用的是RocketMQTemplate类. 在集成了Spring Cloud Stream之后,我们可以使用Source实现消息的发送,代码如下

private final Source source;
......
source.output().send(
                MessageBuilder
                        .withPayload(Demo.builder().demoId(1).remark("哈哈哈").build())
                        .setHeader(RocketMQHeaders.TRANSACTION_ID, UUID.randomUUID().toString())
                        .setHeader("comment", JSON.toJSONString(forObject))
                        .build()
        );

在使用rocketMQTemplate类时,sendMessageInTransaction方法的第四个参数可以帮助我们传递对象,source接口的send方法没有多余参数,所以我们利用MessageBuilder将对象信息放在消息头里面.因为setHeader只能传递字符串,所以我们将对象转换为Json字符串,然后在处理本地事务从消息头中取出来,转换回来就可以了.

修改配置

在使用rocketMQTemplate类时,我们使用sendMessageInTransactiontxProducerGroup参数设置txProducerGroup信息,在引入了Spring Cloud Stream之后,我们在配置文件中配置该信息.配置如下

spring:
  cloud:
    stream:
      rocketmq:
        binder:
          name-server: 127.0.0.1:9876
        bindings:
          output:
            producer:
              transactional: true
              # txProducerGroup
              group: test-stream-rocketmq-transactional
      bindings:
        # 生产者
        output:
          # 指定topic
          destination: test-topic

本地业务处理

import com.alibaba.fastjson.JSON;
import com.example.study01.domain.dto.DemoComment;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;

@Slf4j
@RocketMQTransactionListener(txProducerGroup = "test-stream-rocketmq-transactional")
public class demoTransactionalListener implements RocketMQLocalTransactionListener {
    /**
     * 处理本地事务
     */
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object arg) {
        // 消息头
        MessageHeaders headers = message.getHeaders();
        String transactionalId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);
        DemoComment comment = JSON.parseObject(headers.get("comment").toString(), DemoComment.class);

        try {
            log.info("1111111");
            // 本地业务
            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    /**
     * 若在本地事务执行过程中缺少二次确认消息或生产者处于等待状态
     * MQ Server将向同一组中的每个生产者发送检查消息
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        log.info("222222");
        try {
            // 检查业务
            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }
}