# spring-cloud-alibab-study **Repository Path**: ilovemo/spring-cloud-alibab-study ## Basic Information - **Project Name**: spring-cloud-alibab-study - **Description**: spring-cloud-alibab学习代码和笔记 - **Primary Language**: Java - **License**: GPL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-12-01 - **Last Updated**: 2020-12-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 准备工作 Spring Boot ---》Spring Cloud ---》Spring Cloud Alibaba 组件对应版本 | Spring Cloud Alibaba Version | Sentinel Version | Nacos Version | RocketMQ Version | Dubbo Version | Seata Version | | ----------------------------------------------- | ---------------- | ------------- | ---------------- | ------------- | ------------- | | 2.2.3.RELEASE or 2.1.3.RELEASE or 2.0.3.RELEASE | 1.8.0 | 1.3.3 | 4.4.0 | 2.7.8 | 1.3.0 | | 2.2.1.RELEASE or 2.1.2.RELEASE or 2.0.2.RELEASE | 1.7.1 | 1.2.1 | 4.4.0 | 2.7.6 | 1.2.0 | | 2.2.0.RELEASE | 1.7.1 | 1.1.4 | 4.4.0 | 2.7.4.1 | 1.0.0 | | 2.1.1.RELEASE or 2.0.1.RELEASE or 1.5.1.RELEASE | 1.7.0 | 1.1.4 | 4.4.0 | 2.7.3 | 0.9.0 | | 2.1.0.RELEASE or 2.0.0.RELEASE or 1.5.0.RELEASE | 1.6.3 | 1.1.1 | 4.4.0 | 2.7.3 | 0.7.1 | Spring Cloud -Spring Cloud Alibab-Spring Boot 对应版本 | Spring Cloud Version | Spring Cloud Alibaba Version | Spring Boot Version | | --------------------------- | --------------------------------- | ------------------- | | Spring Cloud Hoxton.SR8 | 2.2.3.RELEASE | 2.3.2.RELEASE | | Spring Cloud Greenwich.SR6 | 2.1.3.RELEASE | 2.1.13.RELEASE | | Spring Cloud Hoxton.SR3 | 2.2.1.RELEASE | 2.2.5.RELEASE | | Spring Cloud Hoxton.RELEASE | 2.2.0.RELEASE | 2.2.X.RELEASE | | Spring Cloud Greenwich | 2.1.2.RELEASE | 2.1.X.RELEASE | | Spring Cloud Finchley | 2.0.3.RELEASE | 2.0.X.RELEASE | | Spring Cloud Edgware | 1.5.1.RELEASE(停止维护,建议升级) | 1.5.X.RELEASE | 创建父工程 Spring Cloud Alibaba 的环境在父工程中创建,微服务的各个组件作为子工程,继承父工程的环境。 其中pom修改打包方式为pom,完整pom.xml ```xml 4.0.0 org.springframework.boot spring-boot-starter-parent 2.3.2.RELEASE com.godfrey spring-cloud-alibaba 0.0.1-SNAPSHOT spring-cloud-alibaba spring-cloud-alibaba for Spring Boot pom 1.8 2.2.3.RELEASE Hoxton.SR8 com.alibaba.cloud spring-cloud-alibaba-dependencies ${spring.cloud.alibaba.version} pom import org.springframework.cloud spring-cloud-dependencies ${spring.cloud.version} pom import org.springframework.boot spring-boot-maven-plugin ``` # 1、Nacos 服务治理 ## 1.1 Nacos 服务注册 解压,通过cmd`startup.cmd -m standalone`启动单机(非集群)服务或者修改startup.cmd `set MODE="cluster"`=》`set MODE="standalone"`。账号、密码都为nacos Nacos 搭建成功,接下来注册服务。 在父工程路径下创建provide子模块,,让子工程继承父工程的环境依赖,pom.xml 中添加 nacos 组件。 ```xml org.springframework.boot spring-boot-starter-web com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` application.yml 中配置 ```yaml spring: application: name: provider cloud: nacos: discovery: # 指定nacos server地址 server-addr: localhost:8848 ``` 启动类ProviderApplication ```java @SpringBootApplication public class ProviderApplication { public static void main(String[] args) { SpringApplication.run(ProviderApplication.class, args) } } ``` ==注意==:在Spring Cloud官方文档中,从Edgware开始,已经不再强制在启动类上添加@EnableDiscoveryClient注解了,如果不用Nacos作为服务注册的组件,可以添加**autoRegister = false**在@EnableDiscoveryClient中![](http://imgcloud.duiyi.xyz//data20201202171326.png)![](http://imgcloud.duiyi.xyz//data20201202104949.png) ![](http://imgcloud.duiyi.xyz//data20201202105031.png) ## 1.2 Nacos 服务发现与调用 ### 1.2.1 服务发现 在父工程路径下创建consumer子模块,,让子工程继承父工程的环境依赖,pom.xml 中添加 nacos 发现组件。 ```xml org.springframework.boot spring-boot-starter-web com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` application.yml中配置 ```yml server: port: 9090 ``` 启动类ConsumerApplication ```java @SpringBootApplication public class ConsumerApplication { public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args); } } ``` 通过 discoveryClient 发现注册到 nacos 中的 provider 服务。 ```java @RestController public class ConsumerController { private final DiscoveryClient discoveryClient; @Autowired ConsumerController(DiscoveryClient discoveryClient){ this.discoveryClient = discoveryClient; } @GetMapping("/instances") public List getInstances() { return discoveryClient.getInstances("provider"); } } ``` 1、启动Nacos 2、设置允许启动多个provider服务,通过修改端口号(8080~8082)启动三个provider服务 3、启动consumer服务 ![](http://imgcloud.duiyi.xyz//data20201202144006.png) ![](http://imgcloud.duiyi.xyz//data20201202144052.png) ### 1.2.2 服务调用: consumer通过RestTemplate调用provider提供的服务 先去provider controller向外提供调用方法 ```java @RestController public class ProviderController { @Value("${server.port}") private String port; @GetMapping("/index") public String index() { return this.port; } } ``` 去consumer模块配置RestTemplate的Bean ```java @Configuration public class ConsumerConfig { @Bean public RestTemplate getRestTemplate() { return new RestTemplate(); } } ``` controller添加index方法 ```java @RestController public class ConsumerController { private final DiscoveryClient discoveryClient; private final RestTemplate restTemplate; @Autowired public ConsumerController(DiscoveryClient discoveryClient, RestTemplate restTemplate) { this.discoveryClient = discoveryClient; this.restTemplate = restTemplate; } @GetMapping("/instances") public List getInstances() { return discoveryClient.getInstances("provider"); } @GetMapping("/index") public String index() { List instances = discoveryClient.getInstances("provider"); //随机访问服务 int index = ThreadLocalRandom.current().nextInt(instances.size()); String url = instances.get(index).getUri() + "/index"; return "consumer随机远程调用provier:" + this.restTemplate.getForObject(url, String.class); } } ``` 1、启动Nacos 2、设置允许启动多个provider服务,通过修改端口号(8080~8082)启动三个provider服务 3、启动consumer服务 ![](http://imgcloud.duiyi.xyz//data20201202144006.png) ![](http://imgcloud.duiyi.xyz//data20201202151305.png) # 2、Ribbon 负载均衡 新增@LoadBalanced注解 ```java @Configuration public class ConsumerConfig { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } } ``` ConsumerController改造 ```java @RestController public class ConsumerController { private final DiscoveryClient discoveryClient; private final RestTemplate restTemplate; //访问服务提供者服务的前缀,http://服务名 private static final String REST_URL_PREFIX = "http://provider"; @Autowired public ConsumerController(DiscoveryClient discoveryClient, RestTemplate restTemplate) { this.discoveryClient = discoveryClient; this.restTemplate = restTemplate; } @GetMapping("/instances") public List getInstances() { return discoveryClient.getInstances("provider"); } @GetMapping("/index") public String index() { return "consumer远程调用provier:" + this.restTemplate.getForObject(REST_URL_PREFIX + "/index", String.class); } } ``` > 随机 ```yaml server: port: 9090 # 负载均衡策略 服务名+ribbon+策略(不配置默认使用轮询策略) provider: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule ``` > Nacos 权重 ```java public class NacosWeightedRule extends AbstractLoadBalancerRule { private static final Logger log = LoggerFactory.getLogger(NacosWeightedRule.class); @Autowired private NacosDiscoveryProperties nacosDiscoveryProperties; @Override public void initWithNiwsConfig(IClientConfig iClientConfig) { //读取配置文件 } @Override public Server choose(Object o) { ILoadBalancer loadBalancer = this.getLoadBalancer(); BaseLoadBalancer baseLoadBalancer = (BaseLoadBalancer) loadBalancer; //获取要请求的微服务名称 String name = baseLoadBalancer.getName(); //获取服务发现的相关API NamingService namingService = nacosDiscoveryProperties.namingServiceInstance(); try { Instance instance = namingService.selectOneHealthyInstance(name); log.info("选择的实例是port={},instance={}", instance.getPort(), instance); return new NacosServer(instance); } catch (NacosException e) { e.printStackTrace(); return null; } } } ``` ```yaml server: port: 9090 # 负载均衡策略 服务名+ribbon+策略(不配置默认使用轮询策略) provider: ribbon: NFLoadBalancerRuleClassName: com.godfrey.configuration.NacosWeightedRule ``` 默认权重都是1,修改权重再进行测试 ![](http://imgcloud.duiyi.xyz//data20201202162833.png) # 3、Sentinel 服务限流降级 雪崩效应 解决方案 1、设置线程超时 2、设置限流 3、熔断器 Sentinel、Hystrix 1、pom.xml 引入依赖 ```xml com.alibaba.cloud spring-cloud-starter-alibaba-sentinel org.springframework.boot spring-boot-starter-actuator ``` 2、application 配置 ```yml # actuator暴露所有端点 management: endpoints: web: exposure: include: '*' # 与Sentinel DashBoard交互地址 spring: cloud: sentinel: transport: dashboard: localhost:8080 ``` 3、下载 Sentinel 控制台:https://github.com/alibaba/Sentinel/releases,启动:`java -jar sentinel-dashboard-1.8.0.jar` 账号密码皆为**sentinel**,启动nacos,启动provider模块和consumer模块,访问http://localhost:9090/index ![](http://imgcloud.duiyi.xyz//data20201203094223.png) ## 3.1 流控规则 > 直接限流:直接对关联的url资源限流 开启sentinel,开启provider服务 对/index资源做流控,设置QPS为1(表示一秒钟只允许访问一次) ![](http://imgcloud.duiyi.xyz//data20201207094116.png) 当访问一秒钟访问http://localhost:8001/index超过一次时,会限制显示被流控限制阻塞 ![](http://imgcloud.duiyi.xyz//data20201207093635.png) > 关联限流:当被访问的url资源超过设定的阈值,限流关联的资源 controller新增list方法: ```java @GetMapping("/list") public String list() { return "list"; } ``` 添加测试依赖 ```xml org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine ``` 需要同时范文资源才能看到效果,测试类对http://localhost:8001/index访问 ```java @Test @DisplayName("测试关联流控模式") void test1() throws InterruptedException { RestTemplate restTemplate = new RestTemplate(); for (int i = 0; i < 100; ++i) { restTemplate.getForObject("http://localhost:8081/index", String.class); System.out.println("provider==>/index=======>" + i); //休眠200毫秒 TimeUnit.MILLISECONDS.sleep(200); } } ``` 启动provider程序,设置流控规则如下: ![](http://imgcloud.duiyi.xyz//data20201207134856.png) 设置完流控规则后启动测试程序,浏览器访问http://localhost:8001/list则出现以下情况,表示对index访问超过阈值,则关联资源list限流 ![](http://imgcloud.duiyi.xyz//data20201207144427.png) > 链路限流:对更深层次资源限流(不仅仅局限于controller) 1、pom.xml 添加依赖 ```xml com.alibaba.csp sentinel-web-servlet ``` 2、application.yml ```yml spring: cloud: sentinel: filter: enabled: false ``` 3、写配置类 ```java package com.godfrey.configuration; import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; /** * @author godfrey * @since 2020-12-07 */ @Configuration public class FilterConfiguration { @Bean public FilterRegistrationBean registrationBean() { FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new CommonFilter()); registrationBean.addUrlPatterns("/*"); registrationBean.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY, "false"); registrationBean.setName("sentinelFilter"); return registrationBean; } } ``` 4、Service ```java @Service public class ProviderService { @SentinelResource("test") //test 保护资源名 public void test() { System.out.println("test"); } } ``` 5、Controller ```java private final ProviderService providerService; @Autowired public ProviderController(ProviderService providerService) { this.providerService = providerService; } @GetMapping("/test1") public String test1() { this.providerService.test(); return "test1"; } @GetMapping("/test2") public String test2() { this.providerService.test(); return "test2"; } ``` 为了对比,test1做链路限流,对test2不做限流,设置如下 ![](http://imgcloud.duiyi.xyz//data20201207152858.png) 一秒钟访问http://localhost:8081/test1超过一次时,会对service绑定资源限流 ![](http://imgcloud.duiyi.xyz//data20201207153144.png) 访问http://localhost:8081/test2则不会 ![](http://imgcloud.duiyi.xyz//data20201207153338.png) ## 3.2 流控效果 > 快速失败 直接抛出异常 > Warm UP 给系统一个预热的时间,预热时间段内单机阈值较低,预热时间过后单机阈值增加,预热时间内当前的单机阈值是设置的阈值的三分之一,预热时间过后单机阈值恢复设置的值。 ![](http://imgcloud.duiyi.xyz//data20201207154128.png) ![](http://imgcloud.duiyi.xyz//data20201207154307.png) > 排队等待 当请求调用失败之后,不会立即抛出异常,等待下一次调用,时间范围是超时时间,在时间范围内如果请求则抛出异常。**阀值类型必须设成QPS,否则无效** ## 3.3 熔断降级规则 > RT(慢调用比例) 选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(`statIntervalMs`)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(半熔断)(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。 ![](http://imgcloud.duiyi.xyz//data20201208112324.png) > 异常比例 每秒请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(半熔断)(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。 ![](http://imgcloud.duiyi.xyz//data20201208114356.png) > 异常数 异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(半熔断)(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。 ![](http://imgcloud.duiyi.xyz//data20201208114615.png) ## 3.4 热点规则 热点规则是流控规则的更细粒度操作,可以具体到对某个热点参数的限流,设置限流之后,如果带着限流参数的请求量超过阈值,则进行限流,时间为统计窗口时长。 必须要添加 @SentinelResource,即对资源进行流控。 ```java @GetMapping("/hot") @SentinelResource("hot") public String hot( @RequestParam(value = "num1", required = false) Integer num1, @RequestParam(value = "num2", required = false) Integer num2) { return num1 + "-" + num2; } ``` 对参数num1进行限流![](http://imgcloud.duiyi.xyz//data20201226155128.png) 效果: ![](http://imgcloud.duiyi.xyz//data20201226155434.png) ![](http://imgcloud.duiyi.xyz//data20201226155500.png) 可以添加例外值:当传的对应参数值等于例外值的时候,读取的阈值为例外设定阈值 ![](http://imgcloud.duiyi.xyz//data20201226155722.png) ## 3.5、授权规则 给指定的资源设置流控应用(追加参数),可以对流控应用进行访问权限的设置,具体就是添加白名单和黑名单。 如何给请求指定流控应用,通过实现 RequestOriginParser 接口来完成,代码如下所示。 ```java package com.godfrey.configuration; import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; /** * @author godfrey * @since 2020-12-26 */ public class RequestOriginParserDefinition implements RequestOriginParser { @Override public String parseOrigin(HttpServletRequest httpServletRequest) { String name = httpServletRequest.getParameter("name"); if (StringUtils.isEmpty(name)) { throw new RuntimeException("name is null"); } return name; } } ``` 要让 RequestOriginParserDefinition 生效,需要在配置类中进行配置。 ```java package com.godfrey.configuration; import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager; import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; /** * @author godfrey * @since 2020-12-26 */ @Configuration public class SentinelConfiguration { @PostConstruct public void init() { WebCallbackManager.setRequestOriginParser(new RequestOriginParserDefinition()); } } ``` ![](http://imgcloud.duiyi.xyz//data20201226161143.png) ![](http://imgcloud.duiyi.xyz//data20201226161309.png) ![](http://imgcloud.duiyi.xyz//data20201226161325.png) ## 3.6 自定义规则异常返回 创建异常处理类 ```java package com.godfrey.execption; import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlBlockHandler; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException; import com.alibaba.csp.sentinel.slots.block.flow.FlowException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author godfrey * @since 2020-12-26 */ public class ExceptionHandler implements UrlBlockHandler { @Override public void blocked(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws IOException { httpServletResponse.setContentType("text/html;charset=utf-8"); String msg = null; if (e instanceof FlowException) { msg = "限流"; } else if (e instanceof DegradeException) { msg = "降级"; } httpServletResponse.getWriter().write(msg); } } ``` 进行配置。 ```java @Configuration public class SentinelConfiguration { @PostConstruct public void init2() { WebCallbackManager.setUrlBlockHandler(new ExceptionHandler()); } } ``` # 4、整合 RocketMQ ## 4.1 安装 RocketMQ 下载:[rocketmq-all-4.8.0-bin-release.zip](https://apache.claz.org/rocketmq/4.8.0/rocketmq-all-4.8.0-bin-release.zip) 1、传入 Linux 服务器 2、解压缩 ```sh unzip rocketmq-all-4.8.0-bin-release.zip ``` 3、调整启动参数, ```sh cd rocketmq-all-4.8.0-bin-release/bin ``` 修改默认启动参数,默认启动的最大内存为4G,比较大,修改小一点,否则如果服务器内存不够会启动失败 调整namesrv ```sh vim runserver.sh ``` 调整如下 ![](http://imgcloud.duiyi.xyz//data20201226171833.png) 调整broker ```sh vim runbroker.sh ``` 调整如下 ![](http://imgcloud.duiyi.xyz//data20201226172039.png) 4、启动namesrv和启动broker 启动navmesrv ```sh nohup sh mqnamesrv & ``` 启动broker,注意ip为公网ip,端口为navmesrv的默认端口9876 ```sh nohup ./mqbroker -n localhost:9876 & ``` 5、检查是否启动成功 ```sh jps -l ``` ![](http://imgcloud.duiyi.xyz//data20201226202258.png) 也可以查看日志 ```sh tail -f ~/logs/rocketmqlogs/broker.log ``` **启动成功** 6、测试 RocketMQ 消息发送 ```sh export NAMESRV_ADDR=localhost: ./tools.sh org.apache.rocketmq.example.quickstart.Producer ``` 消息接收 ```sh ./tools.sh org.apache.rocketmq.example.quickstart.Consumer ``` 7、关闭 RocketMQ ```sh ./mqshutdown broker ./mqshutdown namesrv ``` ## 4.2 安装 RocketMQ 控制台 ```sh git clone https://github.com/apache/rocketmq-externals.git ``` 1、进入到rocketmq-console的配置文件,修改如下: ![](http://imgcloud.duiyi.xyz//data20201226205834.png) 2、打包 ```sh mvn clean package -Dmaven.test.skip=true ``` 3、进入 target 启动 jar ```sh java -jar rocketmq-console-ng-2.0.0.jar ``` 打开浏览器访问 localhost:9877,如果报错 ![](http://imgcloud.duiyi.xyz//data20201226211136.png) 这是因为我们的 RocketMQ 安装在 Linux 中,控制台在 windows,Linux 需要开放端口才能访问,开放 10909 和 9876 端口 ```shell firewall-cmd --zone=public --add-port=10909/tcp --permanent firewall-cmd --zone=public --add-port=10911/tcp --permanent firewall-cmd --zone=public --add-port=9876/tcp --permanent systemctl restart firewalld.service firewall-cmd --reload ``` 重新启动控制台项目 ## 4.3 Java 实现消息发送 1、pom.xml 中引入依赖 ```xml org.apache.rocketmq rocketmq-spring-boot-starter 2.1.1 ``` 2、生产消息 ```java package com.godfrey; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.common.message.Message; @SpringBootTest class ProviderApplicationTests { @Test @DisplayName("测试RocketMQ消息发送") void test3() throws Exception { //创建消息生产者 DefaultMQProducer producer = new DefaultMQProducer("myproducer-group"); //设置NameServer producer.setNamesrvAddr("39.106.41.184:9876"); //启动生产者 producer.start(); //构建消息对象 Message message = new Message("myTopic", "myTag", ("Test MQ").getBytes()); //发送消息 SendResult result = producer.send(message, 1000); System.out.println(result); //关闭生产者 producer.shutdown(); } } ``` 3、直接运行,如果报错 sendDefaultImpl call timeout,可以开放 10911 端口 ```shell firewall-cmd --zone=public --add-port=10911/tcp --permanent systemctl restart firewalld.service firewall-cmd --reload ``` 打开 RocketMQ 控制台,可查看消息。 ## 4.4 Java 实现消息消费 ```java package com.godfrey; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt; import java.util.List; @SpringBootTest class ProviderApplicationTests { @Test @DisplayName("测试RocketMQ消息接收") void test4() throws MQClientException { //创建消息消费者 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("myconsumer-group"); //设置NameServer consumer.setNamesrvAddr("39.106.41.184:9876"); //指定订阅的主题和标签 consumer.subscribe("myTopic", "*"); //回调函数 consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List list, ConsumeConcurrentlyContext consumeConcurrentlyContext) { return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); //启动消费者 consumer.start(); } } ``` ## 4.5 Spring Boot 整合 RocketMQ > provider 1、pom.xml ```xml org.apache.rocketmq rocketmq-spring-boot-starter 2.1.1 org.apache.rocketmq rocketmq-client 4.8.0 ``` 2、application.yml ```yaml rocketmq: name-server: 39.106.41.184:9876 producer: group: myprovider ``` 3、Order ```java package com.godfrey.entity; import java.io.Serializable; import java.util.Date; /** * @author godfrey * @since 2020-12-27 */ public class Order implements Serializable { private static final long serialVersionUID = -5397628182599822017L; private Integer id; private String buyerName; private String buyerTel; private String address; private Date createDate; public Order() { } public Order(Integer id, String buyerName, String buyerTel, String address, Date createDate) { this.id = id; this.buyerName = buyerName; this.buyerTel = buyerTel; this.address = address; this.createDate = createDate; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getBuyerName() { return buyerName; } public void setBuyerName(String buyerName) { this.buyerName = buyerName; } public String getBuyerTel() { return buyerTel; } public void setBuyerTel(String buyerTel) { this.buyerTel = buyerTel; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public Date getCreateDate() { return createDate; } public void setCreateDate(Date createDate) { this.createDate = createDate; } @Override public String toString() { return "Order{" + "id=" + id + ", buyerName='" + buyerName + '\'' + ", buyerTel='" + buyerTel + '\'' + ", address='" + address + '\'' + ", createDate=" + createDate + '}'; } } ``` 4、Controller ```java private RocketMQTemplate rocketMQTemplate; @Autowired public ProviderController(RocketMQTemplate rocketMQTemplate) { this.rocketMQTemplate = rocketMQTemplate; } @GetMapping("/create") public Order create(){ Order order = new Order( 1, "张三", "123123", "软件园", new Date() ); this.rocketMQTemplate.convertAndSend("myTopic",order); return order; } ``` > consumer 1、pom.xml ```xml org.apache.rocketmq rocketmq-spring-boot-starter 2.1.1 org.apache.rocketmq rocketmq-client 4.8.0 ``` 2、application.yml ```yaml rocketmq: name-server: 39.106.41.184:9876 ``` 3、Order ```java package com.godfrey.entity; import java.io.Serializable; import java.util.Date; /** * @author godfrey * @since 2020-12-27 */ public class Order implements Serializable { private static final long serialVersionUID = -5397628182599822017L; private Integer id; private String buyerName; private String buyerTel; private String address; private Date createDate; public Order() { } public Order(Integer id, String buyerName, String buyerTel, String address, Date createDate) { this.id = id; this.buyerName = buyerName; this.buyerTel = buyerTel; this.address = address; this.createDate = createDate; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getBuyerName() { return buyerName; } public void setBuyerName(String buyerName) { this.buyerName = buyerName; } public String getBuyerTel() { return buyerTel; } public void setBuyerTel(String buyerTel) { this.buyerTel = buyerTel; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public Date getCreateDate() { return createDate; } public void setCreateDate(Date createDate) { this.createDate = createDate; } @Override public String toString() { return "Order{" + "id=" + id + ", buyerName='" + buyerName + '\'' + ", buyerTel='" + buyerTel + '\'' + ", address='" + address + '\'' + ", createDate=" + createDate + '}'; } } ``` 4、Service ```java package com.godfrey.service; import com.godfrey.entity.Order; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; /** * @author godfrey * @since 2020-12-27 */ @Service @RocketMQMessageListener(consumerGroup = "myConsumer", topic = "myTopic") public class SmsService implements RocketMQListener { private static final Logger log = LoggerFactory.getLogger(SmsService.class); @Override public void onMessage(Order order) { log.info("新订单{},发短信通知用户", order); } } ``` # 5、服务网关 Spring Cloud Gateway 是基于 Netty,跟 Servlet 不兼容,所以你的工程中不能出现 Servlet 的组件 。 1、pom.xml 注意,一定不能出现 spring web 的依赖,因为 Gateway 与 Servlet 不兼容。 ```xml org.springframework.cloud spring-cloud-starter-gateway ``` 2、application.yml ```yaml server: port: 8010 spring: application: name: gateway cloud: gateway: discovery: locator: enabled: true routes: - id: provider_route uri: http://localhost:8081 predicates: - Path=/provider/** filters: - StripPrefix=1 ``` 上面这种做法其实没有用到 nacos ,现在我们让 gateway 直接去 nacos 中发现服务,配置更加简单了。 1、pom.xml 引入 nacos ```xml org.springframework.cloud spring-cloud-starter-gateway com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` 2、application.yml ```yaml server: port: 8010 spring: application: name: gateway cloud: gateway: discovery: locator: enabled: true ``` **Gateway 限流** 基于路由限流 1、pom.xml ```xml org.springframework.cloud spring-cloud-starter-gateway com.alibaba.csp sentinel-spring-cloud-gateway-adapter ``` 2、配置类 ```java package com.godfrey.configuration; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager; import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler; import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.annotation.PostConstruct; import java.util.*; /** * @author godfrey * @since 2020-12-27 */ @Configuration public class GatewayConfiguration { private final List viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; public GatewayConfiguration(ObjectProvider> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) { this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); this.serverCodecConfigurer = serverCodecConfigurer; } //配置限流的异常处理 @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() { return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); } //配置初始化的限流参数 @PostConstruct public void initGatewayRules(){ Set rules = new HashSet<>(); rules.add( new GatewayFlowRule("provider_route") .setCount(1) .setIntervalSec(1) ); GatewayRuleManager.loadRules(rules); } //初始化限流过滤器 @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public GlobalFilter sentinelGatewayFilter() { return new SentinelGatewayFilter(); } //自定义限流异常页面 @PostConstruct public void initBlockHandlers(){ BlockRequestHandler blockRequestHandler = new BlockRequestHandler() { @Override public Mono handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) { Map map = new HashMap(); map.put("code",0); map.put("msg","被限流了"); return ServerResponse.status(HttpStatus.OK) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromObject(map)); } }; GatewayCallbackManager.setBlockHandler(blockRequestHandler); } } ``` 3、application.yml ```yaml server: port: 8010 spring: application: name: gateway cloud: gateway: discovery: locator: enabled: true routes: - id: provider_route uri: http://localhost:8081 predicates: - Path=/provider/** filters: - StripPrefix=1 ``` 基于 API 分组限流 1、修改配置类,添加基于 API 分组限流的方法,修改初始化的限流参数 ```java package com.godfrey.configuration; import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem; import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem; import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager; import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler; import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.annotation.PostConstruct; import java.util.*; /** * @author godfrey * @since 2020-12-27 */ @Configuration public class GatewayConfiguration { private final List viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; public GatewayConfiguration(ObjectProvider> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) { this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); this.serverCodecConfigurer = serverCodecConfigurer; } //配置限流的异常处理 @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() { return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); } //配置初始化的限流参数 @PostConstruct public void initGatewayRules(){ Set rules = new HashSet<>(); rules.add(new GatewayFlowRule("provider_api1").setCount(1).setIntervalSec(1)); rules.add(new GatewayFlowRule("provider_api2").setCount(1).setIntervalSec(1)); GatewayRuleManager.loadRules(rules); } //初始化限流过滤器 @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public GlobalFilter sentinelGatewayFilter() { return new SentinelGatewayFilter(); } //自定义限流异常页面 @PostConstruct public void initBlockHandlers(){ BlockRequestHandler blockRequestHandler = new BlockRequestHandler() { @Override public Mono handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) { Map map = new HashMap(); map.put("code",0); map.put("msg","被限流了"); return ServerResponse.status(HttpStatus.OK) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromObject(map)); } }; GatewayCallbackManager.setBlockHandler(blockRequestHandler); } //自定义API分组 @PostConstruct private void initCustomizedApis(){ Set definitions = new HashSet<>(); ApiDefinition api1 = new ApiDefinition("provider_api1") .setPredicateItems(new HashSet(){{ add(new ApiPathPredicateItem().setPattern("/provider/api1/**") .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX)); }}); ApiDefinition api2 = new ApiDefinition("provider_api2") .setPredicateItems(new HashSet(){{ add(new ApiPathPredicateItem().setPattern("/provider/api2/demo1")); }}); definitions.add(api1); definitions.add(api2); GatewayApiDefinitionManager.loadApiDefinitions(definitions); } } ``` 2、Controller 添加方法 ```java @GetMapping("/api1/demo1") public String demo1() { return "demo"; } @GetMapping("/api1/demo2") public String demo2() { return "demo"; } @GetMapping("/api2/demo1") public String demo3() { return "demo"; } @GetMapping("/api2/demo2") public String demo4() { return "demo"; } ``` 也可以基于 Nacos 服务发现组件进行限流 ```yaml server: port: 8010 spring: application: name: gateway cloud: gateway: discovery: locator: enabled: true ``` API 分组代码修改,改为 discovery 中的服务名。 ```java ApiDefinition api2 = new ApiDefinition("provider_api2") .setPredicateItems(new HashSet(){{ add(new ApiPathPredicateItem().setPattern("/p1/api2/demo1")); }}); ``` # 6、分布式事务 ## 模拟分布式事务异常 1、创建两个工程 order、pay,pom.xml ```xml org.springframework.boot spring-boot-starter-jdbc org.springframework.boot spring-boot-starter-web mysql mysql-connector-java runtime org.projectlombok lombok true ``` 2、建两个数据库 order、pay,两个微服务分别访问。 3、分别写两个服务的 application.yml ```yaml server: port: 8010 spring: application: name: order datasource: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123456 url: jdbc:mysql://localhost:3306/order ``` ```yaml server: port: 8020 spring: application: name: pay datasource: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123456 url: jdbc:mysql://localhost:3306/pay ``` 4、分别写两个 Service ```java package com.southwind.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; @Service public class OrderService { @Autowired private JdbcTemplate jdbcTemplate; public void save(){ this.jdbcTemplate.update("insert into orders(username) values ('张三')"); } } ``` ```java package com.southwind.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; @Service public class PayService { @Autowired private JdbcTemplate jdbcTemplate; public void save(){ this.jdbcTemplate.update("insert into pay(username) values ('张三')"); } } ``` 5、控制器 Order 通过 RestTemplate 调用 Pay 的服务 ```java package com.southwind.controller; import com.southwind.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController public class OrderController { @Autowired private OrderService orderService; @Autowired private RestTemplate restTemplate; @GetMapping("/save") public String save(){ //订单 this.orderService.save(); int i = 10/0; //支付 this.restTemplate.getForObject("http://localhost:8020/save",String.class); return "success"; } } ``` ```java package com.southwind.controller; import com.southwind.service.PayService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class PayController { @Autowired private PayService payService; @GetMapping("/save") public String save(){ this.payService.save(); return "success"; } } ``` 6、启动类 ```java package com.southwind; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } } ``` ```java package com.southwind; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class PayApplication { public static void main(String[] args) { SpringApplication.run(PayApplication.class, args); } } ``` 分布式异常模拟结束,Order 存储完成之后,出现异常,会导致 Pay 无法存储,但是 Order 数据库不会进行回滚。 ## Seata 解决 1、下载 2、解压,修改两个文件 ![image-20200624165841578](C:\Users\ningn\AppData\Roaming\Typora\typora-user-images\image-20200624165841578.png) regisry.conf ```conf registry { type = "nacos" nacos { serverAddr = "localhost" namespace = "public" cluster = "default" } } config { type = "nacos" nacos { serverAddr = "localhost" namespace = "public" cluster = "default" } } ``` nacos-config.txt ![image-20200624170027580](C:\Users\ningn\AppData\Roaming\Typora\typora-user-images\image-20200624170027580.png) 3、启动 Nacos,运行 nacos-config.sh 将 Seata 配置导入 Nacos 进入 conf,右键 Git Bash Here ``` cd conf sh nacos-config.sh 127.0.0.1 ``` 执行成功,刷新 Nacos,配置加入 ![image-20200624170411851](C:\Users\ningn\AppData\Roaming\Typora\typora-user-images\image-20200624170411851.png) nacos-config.txt 配置已生效 ![image-20200624170446667](C:\Users\ningn\AppData\Roaming\Typora\typora-user-images\image-20200624170446667.png) 4、启动 Seata Server, **JDK 8 以上环境无法启动** ``` cd bin seata-server.bat -p 8090 -m file ``` ![image-20200624170701118](C:\Users\ningn\AppData\Roaming\Typora\typora-user-images\image-20200624170701118.png) 启动成功,Nacos 注册成功。 ![image-20200624171016755](C:\Users\ningn\AppData\Roaming\Typora\typora-user-images\image-20200624171016755.png) Seata 服务环境搭建完毕,接下来去应用中添加。 1、初始化数据库,在两个数据库中添加事务日志记录表,SQL Seata 已经提供。 ![image-20200624171211591](C:\Users\ningn\AppData\Roaming\Typora\typora-user-images\image-20200624171211591.png) 2、直接在两个数据库运行脚本。 ```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; ``` 3、两个工程的 pom.xml 添加 Seata 组件和 Nacos Config 组件。 ```xml com.alibaba.cloud spring-cloud-starter-alibaba-seata 2.1.1.RELEASE com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config ``` 4、给 JDBCTemplate 添加代理数据源 ```java package com.southwind; import io.seata.rm.datasource.DataSourceProxy; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.client.RestTemplate; import javax.sql.DataSource; @SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource){ return new JdbcTemplate(new DataSourceProxy(dataSource)); } } ``` ```java package com.southwind; import io.seata.rm.datasource.DataSourceProxy; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.jdbc.core.JdbcTemplate; import javax.sql.DataSource; @SpringBootApplication public class PayApplication { public static void main(String[] args) { SpringApplication.run(PayApplication.class, args); } @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource){ return new JdbcTemplate(new DataSourceProxy(dataSource)); } } ``` 5、将 registry.conf 复制到两个工程的 resources 下。 6、给两个工程添加 bootstrap.yml 读取 Nacos 配置。 ```yaml spring: application: name: order cloud: nacos: config: server-addr: localhost:8848 namespace: public group: SEATA_GROUP alibaba: seata: tx-service-group: ${spring.application.name} ``` ```yaml spring: application: name: pay cloud: nacos: config: server-addr: localhost:8848 namespace: public group: SEATA_GROUP alibaba: seata: tx-service-group: ${spring.application.name} ``` tx-service-group 需要和 Nacos 配置中的名称一致。 ![image-20200624172215657](C:\Users\ningn\AppData\Roaming\Typora\typora-user-images\image-20200624172215657.png) 7、在 Order 调用 Pay 处添加注解 @GlobalTransactional ```java package com.southwind.controller; import com.southwind.service.OrderService; import io.seata.spring.annotation.GlobalTransactional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController public class OrderController { @Autowired private OrderService orderService; @Autowired private RestTemplate restTemplate; @GetMapping("/save") @GlobalTransactional public String save(){ //订单 this.orderService.save(); int i = 10/0; //支付 this.restTemplate.getForObject("http://localhost:8020/save",String.class); return "success"; } } ```