# 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中

## 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服务


### 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服务


# 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,修改权重再进行测试

# 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

## 3.1 流控规则
> 直接限流:直接对关联的url资源限流
开启sentinel,开启provider服务
对/index资源做流控,设置QPS为1(表示一秒钟只允许访问一次)

当访问一秒钟访问http://localhost:8001/index超过一次时,会限制显示被流控限制阻塞

> 关联限流:当被访问的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://localhost:8001/list则出现以下情况,表示对index访问超过阈值,则关联资源list限流

> 链路限流:对更深层次资源限流(不仅仅局限于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://localhost:8081/test1超过一次时,会对service绑定资源限流

访问http://localhost:8081/test2则不会

## 3.2 流控效果
> 快速失败
直接抛出异常
> Warm UP
给系统一个预热的时间,预热时间段内单机阈值较低,预热时间过后单机阈值增加,预热时间内当前的单机阈值是设置的阈值的三分之一,预热时间过后单机阈值恢复设置的值。


> 排队等待
当请求调用失败之后,不会立即抛出异常,等待下一次调用,时间范围是超时时间,在时间范围内如果请求则抛出异常。**阀值类型必须设成QPS,否则无效**
## 3.3 熔断降级规则
> RT(慢调用比例)
选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(`statIntervalMs`)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(半熔断)(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。

> 异常比例
每秒请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(半熔断)(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

> 异常数
异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(半熔断)(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

## 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进行限流
效果:


可以添加例外值:当传的对应参数值等于例外值的时候,读取的阈值为例外设定阈值

## 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());
}
}
```



## 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
```
调整如下

调整broker
```sh
vim runbroker.sh
```
调整如下

4、启动namesrv和启动broker
启动navmesrv
```sh
nohup sh mqnamesrv &
```
启动broker,注意ip为公网ip,端口为navmesrv的默认端口9876
```sh
nohup ./mqbroker -n localhost:9876 &
```
5、检查是否启动成功
```sh
jps -l
```

也可以查看日志
```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的配置文件,修改如下:

2、打包
```sh
mvn clean package -Dmaven.test.skip=true
```
3、进入 target 启动 jar
```sh
java -jar rocketmq-console-ng-2.0.0.jar
```
打开浏览器访问 localhost:9877,如果报错

这是因为我们的 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、解压,修改两个文件

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

3、启动 Nacos,运行 nacos-config.sh 将 Seata 配置导入 Nacos
进入 conf,右键 Git Bash Here
```
cd conf
sh nacos-config.sh 127.0.0.1
```
执行成功,刷新 Nacos,配置加入

nacos-config.txt 配置已生效

4、启动 Seata Server, **JDK 8 以上环境无法启动**
```
cd bin
seata-server.bat -p 8090 -m file
```

启动成功,Nacos 注册成功。

Seata 服务环境搭建完毕,接下来去应用中添加。
1、初始化数据库,在两个数据库中添加事务日志记录表,SQL Seata 已经提供。

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 配置中的名称一致。

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";
}
}
```