文章目录
版本选择
https://spring.io/projects/spring-cloud 网址查看版本变化,版本一定要对应.
demo
工程创建
- 新建Maven工程,选择site模板:
- 设置字符编码:
File | Settings | Editor | File Encodings
- 注解生效激活:
- 设置编辑环境:
父工程Maven
dependencyManagement 标签和dependencies的区别:
dependencyManagement 只是父工程的一个规范,并不实际引入依赖,该规范使得子工程可以沿用父工程的版本,避免因版本号不同而引起的问题.dependencies则实际引入依赖.
注:子工程若想对接父工程的版本,要引入坐标,但不用引入版本号,若想继续引用版本号,则会覆盖父工程的版本号.
<groupId>com.hyb.springCloud</groupId>
<artifactId>hybSpringCloud</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
<mysql.version>8.0.22</mysql.version>
<druid.version>1.2.5</druid.version>
<mybatis.spring.boot.version>2.1.4</mybatis.spring.boot.version>
</properties>
<!-- 子模块继承之后,提供作用:锁定版本+子modlue不用写groupId和version -->
<dependencyManagement>
<dependencies>
<!--spring boot 2.2.2-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.6.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud Hoxton.SR1-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba 2.1.0.RELEASE-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.7.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.6.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
支付模块
- 在父工程下,新建一个支付的Maven模块,支付模块选择父工程为刚才新建的父工程:
新建完毕后,会发现在父工程模块的pom文件中,会增加一个子模块的信息:
<modules>
<module>provider-pay</module>
</modules>
- ,然后修改成Springboot工程模块.在pom文件中,导入相关jar
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!--mysql-connector-java-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 编写yaml文件.配置其连接数据库的环境和项目系统环境:
server:
port: 8081
spring:
application:
name: provider-pay
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型
driver-class-name: com.mysql.cj.jdbc.Driver # mysql驱动包
url: jdbc:mysql://localhost:3306/hyb?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 15717747056HYb!
mybatis:
mapperLocations: classpath:mapper/*.xml
type-aliases-package: com.hyb.springcloud.entities # 所有Entity别名类所在包
- 编写代码,从建数据库开始,新建一个表:
- 然后写对应的实体类,注意这里的实体类要放在刚才在yaml指定的实体类路径下:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment implements Serializable {
private Integer id;
private String series;
}
- 编写完实体类,编写对应的sql映射文件,在类路径下新建一个mapper文件,然后新建一个xml文件,编写对应的sql映射:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.hyb.springcloud.dao.PaymentDao">
<!--注意这里的parameterType不写全类名是因为在yaml文件中全局配置好了全类路径-->
<insert id="insert" parameterType="Payment" useGeneratedKeys="true" keyColumn="id">
insert into payment(`series`) values(#{series});
</insert>
<select id="getPaymentById" parameterType="Payment" resultMap="BaseResultMap">
select id,`series` from payment where id=#{id};
</select>
<resultMap id="BaseResultMap" type="com.hyb.springcloud.entities.Payment">
<id column="id" property="id" jdbcType="INTEGER"/>
<id column="series" property="series" jdbcType="VARCHAR"/>
</resultMap>
</mapper>
- 然后写映射的dao接口
@Mapper
public interface PaymentDao {
public void insert(Payment payment);
public Payment getPaymentById(@Param("id")Integer id);
}
- 之后写对应的service接口:
public interface PaymentService {
public void insert(Payment payment);
public Payment getPaymentById(@Param("id")Integer id);
}
- 写对应的service接口实现类:
@Service
public class PaymentServiceImpl implements PaymentService {
@Autowired
PaymentDao paymentDao;
@Override
public void insert(Payment payment){
paymentDao.insert(payment);
}
@Override
public Payment getPaymentById(Integer id) {
return paymentDao.getPaymentById(id);
}
}
- 之后编写controller,但编写controller之前,必须进行返回值的构建,一般我们将返回值以json的形式进行返回,而且要构建在一个类中,该类就相当于处理返回结果的类的,里面包含了返回失败与否的信息:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult <T>{
private Integer code;
private String msg;
private T data;
public CommonResult(Integer code,String msg){
this(code,msg,null);
}
}
- 最后才是编写controller:
@RestController
@Slf4j
public class PaymentController {
@Autowired
PaymentServiceImpl paymentService;
@GetMapping("/i") //注意:真正的业务开发一般是Post
public CommonResult<Payment> create(Payment payment){
paymentService.insert(payment);
log.info("插入成功");
return new CommonResult<Payment>(200,"插入数据库成功",null);
}
@GetMapping("/g/{id}")
public CommonResult<Payment> queryPayment(@PathVariable("id")Integer id){
Payment paymentById = paymentService.getPaymentById(id);
if (paymentById!=null){
log.info("查询成功:{}",paymentById);
return new CommonResult<>(200,"查询成功",paymentById);
}else {
log.info("查询失败:{}","null");
return new CommonResult<>(100,"查询失败",null);
}
}
}
devtools
自动热部署工具.
- 父工程引入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<version>2.6.2</version>
</dependency>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
子工程也引入devtools但可以不用引入版本
- 设置1
- 设置2 (2020版本的idea)
ctrl+shift+alt+/ 进入:
重启
- 设置2 (2021版本idea):
重启
- 3和4 任选一个即可.
订单模块
- 和前面一样新建一个Maven工程,更改为springboot工程.
- 导入依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>hybSpringCloud</artifactId>
<groupId>com.hyb.springCloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>consumer-order</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
- 编写yaml文件:
server:
port: 80 # 该端口号与其他端口号不同的是,该端口号的服务被浏览不用写端口号80
- 将支付模块的实体包类全拿过来.
- 新建一个controller层
订单要用到支付模块的东西,所以订单要解决的问题是这两个服务之间的通信,而让两个服务之间能通讯,我们就必须使用到httpClient,RestTemplate便封装了该httpClient.
package com.hyb.springcloud.controller;
import com.hyb.springcloud.entities.CommonResult;
import com.hyb.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
@RestController
@Slf4j
public class OrderController {
private static final String url="http://localhost:8081";
@Autowired
RestTemplate restTemplate;
@GetMapping("/c")
public CommonResult<Payment> create(Payment payment){
Map<String,Object> map=new HashMap<>();
map.put("series",payment.getSeries());
CommonResult forObject = restTemplate.getForObject(url + "/i?series={series}", CommonResult.class, payment.getSeries());
// CommonResult forObject=restTemplate.postForObject(url+"/i",payment,CommonResult.class);
// CommonResult forObject=restTemplate.getForObject(url+"/i",CommonResult.class,payment);
log.info("远程调用状态码为:{}",forObject.getCode());
return forObject;
}
@GetMapping("/cg/{id}")
public CommonResult<Payment> getPayment(@PathVariable("id")Integer id){
CommonResult forObject = restTemplate.getForObject(url + "/g/" + id, CommonResult.class);
//CommonResult forObject = restTemplate.postForObject(url + "/g/"+id, null,CommonResult.class);
log.info("远程调用状态码为:{}",forObject.getCode());
return forObject;
}
}
- 测试,记得将两个模块全部开启.
getForObject方法如何传递参数:
- 直接将参数以 /参数 的形式拼接给url,其底层会映射成一个 链接,请求成功.(url + “/g/” + id)
- 以url + "/i?series={series}"形式发送请求,利用占位符的方式,从getForObject 第三个参数传入一个Map或字符串.
postForObject方法如何传递参数:
- 若被调用的服务的方法形参是以单个参数接收时:postForObject(_url _+ “/g/”+id, null,CommonResult.class)
- 若被调用的服务的方法形参是以对象的形式接收时:postForObject(url+“/i”,payment,CommonResult.class
- 注意:被调用的服务的方法尽量使用上@RequestBody接收.
工程重构
-
工程重构使得不同的模块的共同代码都能抽取到同一个模块,而省去大量的重复代码.
-
新建一个Maven工程,然后将两个模块重复的部分提取到该工程中,比如实体包下的所有类.
-
导入该工程依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--小型java工具类库-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.20</version>
</dependency>
</dependencies>
- 将该工程进行打包,放弃测试类打包,并将jar放入maven工程:
- 这个时候,该工程就相当于一个jar包了,所以,在需要用到该jar的模块导入即可:
<dependency>
<groupId>com.hyb.springCloud</groupId>
<artifactId>commons-cloud-api</artifactId>
<!--该版本依赖表示依赖项目版本-->
<version>${project.version}</version>
</dependency>
- 可以将引入该依赖的模块原来的共同代码实体类删除,然后重新测试链接.
拓展
run Dashboard
run Dashboard可以使得我们在开发多个微服务模块的时候,快速定位各个模块的启动功能:
idea 2021.2使用run Dashboard:
在父工程的.idea文件中,查找Dashboard的配置,如果没有,假如以下配置:
<component name="RunDashboard">
<option name="configurationTypes">
<set>
<option value="SpringBootApplicationConfigurationType" />
</set>
</option>
<option name="ruleStates">
<list>
<RuleState>
<option name="name" value="ConfigurationTypeDashboardGroupingRule" />
</RuleState>
<RuleState>
<option name="name" value="StatusDashboardGroupingRule" />
</RuleState>
</list>
</option>
</component>
然后重启idea,之后启动一个模块后,会发现下方多了一个service:
RestTemplate 拓展
@GetMapping("/tz")
public CommonResult<Payment> tz(){
ResponseEntity<CommonResult> forEntity = restTemplate.getForEntity(url, CommonResult.class);
if (forEntity.getStatusCode().is2xxSuccessful()){
return forEntity.getBody();
}else {
return new CommonResult<>(forEntity.getStatusCodeValue(),"返回失敗",null);
}
}
getForEntity 方法能返回请求体,状态码等等详细信息.
服务注册中心
Eureka(过时)
什么是服务治理?
在传统的rpc远程服务调用框架中,管理每个服务与服务之间的依赖关系比较复杂,需要用到服务治理,用来实现服务调用,容错,负载均衡,服务注册和发现等功能.
什么是服务注册?
- Eureka Server属于服务注册中心,微服务模块与其维持心跳连接,这样维护人员可以在注册中心中查看各个微服务是否正常.
可以看到,各个模块之间若是进行通信,可以先向注册中心注册自己的信息,然后通信的时候,可以在注册中心进行,从而省去了直接与模块进行通信,减轻被访问的模块负担.
- EurekaClient:是一个客户端,该客户端用于简化Eureka Server之间的交互,Eureka Client 是一个 Java 客户端,用于简化与 Eureka Server 的交互。Eureka Client 会拉取、更新和缓存 Eureka Server 中的信息。因此当所有的 Eureka Server 节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者,但是这个时候当服务有更改的时候会出现信息不一致。
客户端还内置了一个轮询(round-robin)算法为基础的负载均衡器.
单机版的Eureka Server安装
- 在父工程导入Eureka Server 服务包:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>3.1.0</version>
</dependency>
- 新建一个springboot工程,然后导入jar:
<!--eureka-server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.hyb.springcloud</groupId>
<artifactId>commons-cloud-api</artifactId>
<version>${project.version}</version>
</dependency>
<!--boot web actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--一般通用配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
- 编写相关yaml:
server:
port: 7001
eureka:
instance:
hostname: localhost #eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
#
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
- 在启动类上加上@EnableEurekaServer 注解
- 启动,访问localhost:7001 出现以下界面:
支付模块注册
安装了注册中心,我们可以将前面的支付模块注册进Eureka Server中.
- 首先在父工程中引入客户端的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>3.1.0</version>
</dependency>
然后在支付模块中加入该依赖,不用写版本号.
- 编写yaml文件,对Eureka Client进行配置:
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka
- 在启动类上加上@EnableEurekaClient 注解
- 启动Eureka Server和Eureka Client(支付模块),然后会在注册中心看到:
- 同样的方法将订单模块也注册进去.
注册中心集群
- 和单机版的完全一样,就是每个的Eureka Server的yaml需要文件改变:进行互相注册
server:
port: 7002
eureka:
instance:
hostname: eureka7002.com #eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
#
defaultZone: http://eureka7001.com:7001/eureka/
server:
port: 7001
eureka:
instance:
hostname: eureka7001.com #eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
#
defaultZone: http://eureka7002.com:7002/eureka/ # localhost:端口号
注意:这两个配置文件表示互相注册,产生关联.
- 修改host文件: 注意: 这里映射的主要目的是为了不用写localhost,也可以不用映射,直接localhost:端口号就可以了
127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com
- 对于服务的注册,也只是修改该服务的yaml文件即可:
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
# defaultZone: http://localhost:7001/eureka
defaultZone: http://eureka7001.com/eureka,http://eureka7002.com/eureka # 表示注册到两个Server中
- 先启动集群中各个Server,然后启动注册的服务,访问集群的Server后,会发现:
提供者集群
- 前面的订单模块要使用到支付模块,所以这里订单模块就是提供者.而提供者也需要集群,以解决单点故障问题.
- 这里的提供者只需要另外复制一份即可,其他都得一致,比如:spring.application.name必须一致,表示一个集群,但是端口号必须不一样.
- 这里提供者集群以两个为例子,必须都注册进注册中心集群中.
- 当有了提供者集群,消费者消费的时候,就可以选择性地对提供者进行消费,但在这之前,我们必须对调用地址进行修改,也就是前面订单模块的Controller调用支付模块的时候的地址:
这里我们的地址必须修改成http://提供者集群名字:
修改完之后还不行,虽然我们指定了访问哪个提供者集群,但是具体上还是要访问某个提供者,所以这个时候,如果我们启动测试这个订单模块,就会报错,说该provider-pay找不到,这是因为,这是一个集群的名字,Eureka 底层不知道要调用哪一个具体的提供者,所以我们该对其进行负载均衡,让其通过轮询的策略进行具体的提供者调用:
我们具体调用提供者是用到了RestTemplate组件,所以要在注册该组件进入IOC容器的时候指定负载均衡策略:
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
- 测试:为了方便测试,我们可以在两个提供者里面具体被调用的方法里识别出该提供者的端口号,然后启动订单模块,因为这两个提供者为一个集群,我们又指定了负载均衡,所以会进行轮询调用.
服务显示完善
instance:
instance-id: 8081 #名字以端口号显示
prefer-ip-address: true # 鼠标放上去有ip地址
服务发现
- 一个服务在注册中心是可以设置是否对其他服务可见,默认是可见的,相当于在启动类上加上了@EnableDiscoveryClient
- 但是其实默认配置,可以不用配置,而且,就算设置为不可见,也可以在yaml文件中修改spring.cloud.discovery.enabled .
- 如何证明是否不可见了?
查看前台,发现服务不存在了.
获取服务信息
在服务可发现且注册中心启动和服务启动的前提下,利用org.springframework.cloud.client.discovery.DiscoveryClient中的组件DiscoveryClient,可以获取一个或多个的服务信息:
@Test
public void t1(){
List<String> services = discoveryClient.getServices();
for (String s :
services) {
System.out.println(s+"1111");
}
List<ServiceInstance> instances = discoveryClient.getInstances("provider-pay");
for (ServiceInstance s :
instances) {
System.out.println(s);
}
}
Eureka自我保护机制
默认情况下,Eureka Server会跟每个微服务进行心跳连接,如果超过了90秒没有心跳反应,Eureka会注销该微服务实例.但如果出现网络故障的情况,心跳时间肯定会变慢,那上述的默认行为就变得危险了,因为微服务本身没有问题,不能因为外部因素导致的心跳超时而注销该服务.
所以Eureka的自我保护机制便应运而生,一旦因外部因素产生心跳超时问题,Eureka不会立刻注销该微服务,而是进入一种自我保护机制.该机制思想属于CAP里的AP分支.
该自我保护机制是默认设置的.
Zookeeper
初始化
这里以单机版为例.
如果要将集群版的zk中某个zk更改为单机版的,要在zoo.cfg中修改zk端口号为默认的2181.
启动zk服务和客户端.
在父工程引入一个依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<version>3.1.0</version>
</dependency>
新建一个springboot工程,然后引入依赖:
<dependencies>
<!-- SpringBoot整合Web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.hyb.springcloud</groupId>
<artifactId>commons-cloud-api</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot整合zookeeper客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<!--先排除自带的zookeeper3.5.3-->
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--添加zookeeper3.5.7和zk服务器对应版本-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
修改yaml
server:
port: 8083
spring:
application:
name: provider-pay-zk
cloud:
zookeeper:
connect-string: 192.168.188.100:2181
注意要在启动类上加上@EnableDiscoveryClient
启动,若没有报错,就正常,然后查看zk节点,有services节点就可以
services节点是临时节点.
测试
前面新建的springboot工程,相当于支付模块,但这里我们可以随便建立一个访问比如 /g
然后再新建一个springboot工程,同样注册进zk里,和前面的订单模块一样,远程访问/g这个请求.
Consul
安装
这里以windows版为例.
官网https://www.consul.io/downloads下载windows的压缩包
然后解压,会获取到一个解压的exe文件
在exe文件目录下,进入cmd窗口,consul --version 可以查看版本号.
consul agent -dev 可启动consul服务.
启动后,浏览器访问 http://localhost:8500/ 可到达前端控制页面.
测试
这个测试与Zookeeper的测试一样,只是导入的依赖和yam文件有些不一样
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
###consul服务端口号
server:
port: 8084
spring:
application:
name: provider-pay-consul
####consul注册中心地址
cloud:
consul:
host: localhost
port: 8500
discovery:
#hostname: 127.0.0.1
service-name: ${spring.application.name}
CAP理论
引言
CAP是分布式系统、特别是分布式存储领域中被讨论最多的理论,“什么是CAP定理?”在Quora 分布式系统分类下排名 FAQ 的 No.1。CAP在程序员中也有较广的普及,它不仅仅是“C、A、P不能同时满足,最多只能3选2”,以下尝试综合各方观点,从发展历史、工程实践等角度讲述CAP理论。希望大家透过本文对CAP理论有更多地了解和认识。
CAP定理
CAP由Eric Brewer在2000年PODC会议上提出[1][2],是Eric Brewer在Inktomi[3]期间研发搜索引擎、分布式web缓存时得出的关于数据一致性(consistency)、服务可用性(availability)、分区容错性(partition-tolerance)的猜想:
It is impossible for a web service to provide the three following guarantees : Consistency, Availability and Partition-tolerance.
该猜想在提出两年后被证明成立[4],成为我们熟知的CAP定理:
- 数据一致性(consistency):如果系统对一个写操作返回成功,那么之后的读请求都必须读到这个新数据;如果返回失败,那么所有读操作都不能读到这个数据,对调用者而言数据具有强一致性(strong consistency) (又叫原子性 atomic、线性一致性 linearizable consistency)[5]
- 服务可用性(availability):所有读写请求在一定时间内得到响应,可终止、不会一直等待
- 分区容错性(partition-tolerance):在网络分区的情况下,被分隔的节点仍能正常对外服务
在某时刻如果满足AP,分隔的节点同时对外服务但不能相互通信,将导致状态不一致,即不能满足C;如果满足CP,网络分区的情况下为达成C,请求只能一直等待,即不满足A;如果要满足CA,在一定时间内要达到节点状态一致,要求不能出现网络分区,则不能满足P。
C、A、P三者最多只能满足其中两个,和FLP定理一样,CAP定理也指示了一个不可达的结果(impossibility result)。
CAP的工程启示
CAP理论提出7、8年后,NoSql圈将CAP理论当作对抗传统关系型数据库的依据、阐明自己放宽对数据一致性(consistency)要求的正确性[6],随后引起了大范围关于CAP理论的讨论。
CAP理论看似给我们出了一道3选2的选择题,但在工程实践中存在很多现实限制条件,需要我们做更多地考量与权衡,避免进入CAP认识误区[7]。
1、关于 P 的理解
Partition字面意思是网络分区,即因网络因素将系统分隔为多个单独的部分,有人可能会说,网络分区的情况发生概率非常小啊,是不是不用考虑P,保证CA就好[8]。要理解P,我们看回CAP证明[4]中P的定义:
In order to model partition tolerance, the network will be allowed to lose arbitrarily many messages sent from one node to another.
网络分区的情况符合该定义,网络丢包的情况也符合以上定义,另外节点宕机,其他节点发往宕机节点的包也将丢失,这种情况同样符合定义。现实情况下我们面对的是一个不可靠的网络、有一定概率宕机的设备,这两个因素都会导致Partition,因而分布式系统实现中 P 是一个必须项,而不是可选项[9][10]。
对于分布式系统工程实践,CAP理论更合适的描述是:在满足分区容错的前提下,没有算法能同时满足数据一致性和服务可用性[11]:
In a network subject to communication failures, it is impossible for any web service to implement an atomic read/write shared memory that guarantees a response to every request.
2、CA非0/1的选择
P 是必选项,那3选2的选择题不就变成数据一致性(consistency)、服务可用性(availability) 2选1?工程实践中一致性有不同程度,可用性也有不同等级,在保证分区容错性的前提下,放宽约束后可以兼顾一致性和可用性,两者不是非此即彼.
CAP定理证明中的一致性指强一致性,强一致性要求多节点组成的被调要能像单节点一样运作、操作具备原子性,数据在时间、时序上都有要求。如果放宽这些要求,还有其他一致性类型:
- 序列一致性(sequential consistency)[13]:不要求时序一致,A操作先于B操作,在B操作后如果所有调用端读操作得到A操作的结果,满足序列一致性
- 最终一致性(eventual consistency)[14]:放宽对时间的要求,在被调完成操作响应后的某个时间点,被调多个节点的数据最终达成一致
可用性在CAP定理里指所有读写操作必须要能终止,实际应用中从主调、被调两个不同的视角,可用性具有不同的含义。当P(网络分区)出现时,主调可以只支持读操作,通过牺牲部分可用性达成数据一致。
工程实践中,较常见的做法是通过异步拷贝副本(asynchronous replication)、quorum/NRW,实现在调用端看来数据强一致、被调端最终一致,在调用端看来服务可用、被调端允许部分节点不可用(或被网络分隔)的效果[15]。
3、跳出CAP
CAP理论对实现分布式系统具有指导意义,但CAP理论并没有涵盖分布式工程实践中的所有重要因素。
例如延时(latency),它是衡量系统可用性、与用户体验直接相关的一项重要指标[16]。CAP理论中的可用性要求操作能终止、不无休止地进行,除此之外,我们还关心到底需要多长时间能结束操作,这就是延时,它值得我们设计、实现分布式系统时单列出来考虑。
延时与数据一致性也是一对“冤家”,如果要达到强一致性、多个副本数据一致,必然增加延时。加上延时的考量,我们得到一个CAP理论的修改版本PACELC[17]:如果出现P(网络分区),如何在A(服务可用性)、C(数据一致性)之间选择;否则,如何在L(延时)、C(数据一致性)之间选择。
小结
以上介绍了CAP理论的源起和发展,介绍了CAP理论给分布式系统工程实践带来的启示。
CAP理论对分布式系统实现有非常重大的影响,我们可以根据自身的业务特点,在数据一致性和服务可用性之间作出倾向性地选择。通过放松约束条件,我们可以实现在不同时间点满足CAP(此CAP非CAP定理中的CAP,如C替换为最终一致性)[18][19][20]。
有非常非常多文章讨论和研究CAP理论,希望这篇对你认识和了解CAP理论有帮助。
服务调用
Ribbon
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-ribbon -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>
是什么?
Spring Cloud Ribbon 是基于NetFlix Ribbon 实现的一套客户端负载均衡工具,主要功能是提供负载均衡算法和服务调用.
负载均衡是什么?
简单来说,就是将用户的请求平摊到多个服务上,以减少单个服务的压力,实现系统的高可用.
与Nginx的区别
Ribbon是客户端的负载均衡,Nginx是服务端的负载均衡.
Nginx是集中式的负载均衡(LB),即在消费方与提供方使用独立的LB设施.
Ribbon是进程内的负载均衡,是一个类库,集成于消费方进程,消费方通过他来获取到服务提供方的地址.
修改负载均衡策略(这里本人做不出来报错)
报错: No instances available for *** 检查了很多遍,什么都对,但是就是读取不到服务.
先导入:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
在IRul接口中,定义了很多负载均衡策略:
如果我们要修改某个策略,就在消费者一方将某个实现类以@Bean的方式注入到容器中,然后在启动类上:
@RibbonClient(name = "provider-pay-zk",configuration = RandomRuleRibbon.class)
name表示提供者,configuration表示以@Bean注入实现类的配置类.
注意: 这里注入容器的策略bean所在的包根据尚硅谷的周阳老师教学是要放在主启动类所在包外面的包.
轮询算法原理
假如有一个集群,该集群在底层用集合来存储.
这张图片表示集群里有两台机器:
当请求为第一次: 1%2=1 对应下标是1,所以是8001
当请求为第二次: 2%2=0 对应下标是0,所以是8002
当请求为第三次: 3%2=1 对应下标是1,所以是8001
当请求为第四次: 4%2=0 对应下标是0,所以是8002
所以,实际被调用的服务器所在下标=rest接口的第几次请求%服务集群机器总数量
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
} else {
Server server = null;
int count = 0;
while(true) {
if (server == null && count++ < 10) {
//获取注册中心里状态为up的服务,即活跃的服务
List<Server> reachableServers = lb.getReachableServers();
//获取注册中心里的所有服务
List<Server> allServers = lb.getAllServers();
//获取活跃的服务数
int upCount = reachableServers.size();
//获取所有服务的数量,即集群机器总数量
int serverCount = allServers.size();
if (upCount != 0 && serverCount != 0) {
// 计算下一个选择的服务的索引
int nextServerIndex = this.incrementAndGetModulo(serverCount);
// 根据索引获取下一个服务
server = (Server)allServers.get(nextServerIndex);
if (server == null) {
Thread.yield();
} else {
if (server.isAlive() && server.isReadyToServe()) {
return server;
}
server = null;
}
continue;
}
log.warn("No up servers available from load balancer: " + lb);
return null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: " + lb);
}
return server;
}
}
}
private int incrementAndGetModulo(int modulo) {
int current;
int next;
do {
current = this.nextServerCyclicCounter.get();
next = (current + 1) % modulo;
} while(!this.nextServerCyclicCounter.compareAndSet(current, next));
return next;
}
private int incrementAndGetModulo(int modulo) {
//当前服务
int current;
//下一个服务
int next;
do {
//获取当前服务
current = this.nextServerCyclicCounter.get();
//下一个服务=(下一个服务索引)%/服务总数
//下一个服务索引便是当前服务索引+1
next = (current + 1) % modulo;
/*
*compareAndSet 自旋锁,比较和设置,nextServerCyclicCounter 保存下一个位置
*compareAndSet(current, next) 表示如果current的值是当前nextServerCyclicCounter的值
* 就让其更新为next,否则不可能更新,比如,nextServerCyclicCounter初始值为0,当current为0时
* 才会被更新为next这个值.
*/
} while(!this.nextServerCyclicCounter.compareAndSet(current, next));
return next;
}
自实现轮询负载均衡算法
- 我们可以将@Bean 配置RestTemplate的时候,将_@LoadBalanced _注解注释掉,表示将默认的均衡算法省略.
- 然后我们自己手动实现轮询的负载均衡策略,在消费者一端:
public interface LoadBalancer {
ServiceInstance instance(List<ServiceInstance> list);
}
@Component
@Slf4j
public class MyLb implements LoadBalancer{
private AtomicInteger atomicInteger=new AtomicInteger(0);
/*
* next表示第几次访问
* */
public final int getAndIncrement(){
int current;
int next;
do {
current=this.atomicInteger.get();
next=current>=Integer.MAX_VALUE?0:current+1;
}while (!this.atomicInteger.compareAndSet(current,next));
return next;
}
@Override
public ServiceInstance instance(List<ServiceInstance> list) {
// 得到下一次访问的服务
int index=getAndIncrement()%list.size();
ServiceInstance serviceInstance = list.get(index);
int port = serviceInstance.getPort();
log.info("第:{}次访问,端口号为:{}",index,port);
return serviceInstance;
}
}
- 之后我们模拟一个请求:
@GetMapping("/lb")
public String getLb(){
List<ServiceInstance> instances =
discoveryClient.getInstances("provider-pay");
if (instances==null&&instances.size()<=0){
return null;
}
ServiceInstance instance = loadBalancer.instance(instances);
URI uri = instance.getUri();
return restTemplate.getForObject(uri+"/u",String.class);
}
这个请求用到的实例是提供者一端,表示我们服务器有多少个,然后我们消费者进行主要的代码撰写,判断下一个要选择的提供者是哪个.
OpenFeign
简介
Feign 是一个声明式的WebService客户端,他让客户端调用的代码更加简洁,只是声明一个接口然后加上相应的注解即可.
测试
- 新建一个消费者,该消费者和前面的普通消费者类似,但前面用的ribbon调用,而我们现在要用OpenFeign来进行调用.该消费者还是springboot工程,然后yaml文件没有特殊的配置,只是要将其注册到注册中心里.
- 假如pom文件
<dependencies>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.hyb.springcloud</groupId>
<artifactId>commons-cloud-api</artifactId>
<version>${project.version}</version>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--一般基础通用配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
注意: 记得在父工程的pom文件中加上spring-cloud-starter-openfeign的带版本号坐标.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>3.1.0</version>
</dependency>
- 在主启动类上加上@EnableFeignClients 注解.
- 编写一个调用的接口
@Service
@FeignClient(value = "provider-pay")
public interface OpenFeignService {
@GetMapping("/u")
public String u();
}
该接口表示,我们要调用的客户端是provider-pay,然后调用其/u方法.
- 编写一个controller:
@RestController
@Slf4j
public class OpenFeignController {
@Autowired
OpenFeignService openFeignService;
@GetMapping("/uz")
public String uz(){
return openFeignService.u();
}
}
超时设置
- 在和客户端提供者连接或者调用的时候,都可能因为网络超时的原因导致调用失败.
- 在最新版的OpenFeign中,默认超时时间是两秒,如果两秒还未调用成功,便会返回错误.
- 我们要三种方式修改:
- 首先因为feign底层是ribbon,所以可以直接用ribbon进行设置
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ReadTimeout: 10000
#指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 10000
- 其次便是用feign自己的设置:
feign:
client:
config:
# 填服务名称,具体到某个服务,填default,表示所有服务都按照此设置
provider-pay:
connectTimeout: 10000
readTimeout: 10000
- 其次便是直接用@Bean的方式将一个组件注入进去:
@Configuration
public class OpenFeignConfig {
@Bean
public Request.Options options(){
return new Request.Options(10000, TimeUnit.MILLISECONDS,10000,TimeUnit.MILLISECONDS,true);
}
}
如何测试? 可以尝试在被调用的提供者一方的具体方法,用Thread.sleep的方式暂停几秒.
比如: 我们可以设置沉睡的时间比超时时间还长,就说明远远大于超时时间,就一定超时,这个时候就会报错:
日志打印
- 日志打印可以更加详细的看到调用的过程和结果.日志打印一共有四个级别.
- 以@Bean的方式注入一个日志级别:
@Bean
public Logger.Level level(){
return Logger.Level.FULL;
}
- 修改yaml文件:
logging:
level:
# feign日志以什么级别监控哪个接口
com.hyb.springcloud.service.OpenFeignService: debug
- 测试.当请求成功时,控制台输出:
服务降级
HyStrix(过时)
此项目已经停更
Hystrix 最主要的功能是服务降级(fallback),服务熔断(break),服务限流(flowlimit)
服务雪崩
服务降级
当服务器出现故障和延迟等状态,不让客户端等待,立即返回一个结果.程序运行异常,超时,服务熔断触发服务降级,线程池/信号量打满都会出现服务降级.
提供方
- 在父工程导入HyStrix的依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>
- 新建springboot工程,然后在主启动类上加上@EnableEurekaClient注解
- 导入依赖:
<dependencies>
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.hyb.springcloud</groupId>
<artifactId>commons-cloud-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 修改yaml文件:
server:
port: 8085
spring:
application:
name: provider-pay-hystrix
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
# defaultZone: http://localhost:7001/eureka
defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
instance:
instance-id: 8085
prefer-ip-address: true # 显示ip地址
- 编写service组件:
public String Normal(){
return "这是一个正常的方法!";
}
public String InNormal(){
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "这是一个不正常的方法!";
}
- 编写controller组件,模仿请求:
@Autowired
private PaymentService paymentService;
@GetMapping("/n")
public String n(){
return paymentService.Normal();
}
@GetMapping("/i")
public String i(){
return paymentService.InNormal();
}
- 测试请求,可以发现,虽然有一个方法沉睡了十秒,但是最终还是会返回,并且没有沉睡的方法也瞬间返回了,但如果在高并发的情况下,因为本身沉睡了十秒的方法被大量的请求冲击,这个方法肯定变得很缓慢,而且该方法占用了大量线程,导致其他方法被困死,若是再访问其他方法,也会导致其他方法变得缓慢.
这个例子还只是提供方而已,若是有一个消费方消费提供方的沉睡方法,加上中间远程调用的耗时,引起的延迟会更加长.
解决办法
无论是消费方还是提供方出现任何问题导致服务卡死或出错,都需要服务降级,但一般都是放在消费端.
- 写一个错误处理方法:
public String InNormalHandler(){
return "该业务超时!";
}
- 在可能会超时的方法上,指定错误处理方法:
@HystrixCommand(fallbackMethod = "InNormalHandler",commandProperties = {
// 表示线程池设置超时时间为3秒,而我们延迟超过三秒,所以会跳转InNormalHandler方法处理
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
public String InNormal(){
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "这是一个不正常的方法!";
}
execution.isolation.thread.timeoutInMilliseconds 代表超时,其和Thread.sleep方法没有联系若是Thread.sleep换成int 10/0 ,会报算数错误,该错误处理方法也会生效.
- 在主启动类上加上@EnableHystrix注解.
- 测试: 测试超时方法,会跳转到处理方法中.
- 如果在提供方和消费方都使用了同样的错误处理,从消费方这端请求,超时时间便以消费方这端为准,反之以提供方这端为准.
- 而以上只是单独一个方法提供服务降级,若是有多个方法出现相同的降级方法,那么造成了代码的冗杂,所以,可以在类上使用全局降级方式:
@Service
// 全局配置
@DefaultProperties(defaultFallback = "callBack")
public class PaymentService {
public String Normal(){
return "这是一个正常的方法!";
}
//单独配置,以单独为准
@HystrixCommand(fallbackMethod = "InNormalHandler",commandProperties = {
// 表示线程池设置超时时间为3秒,而我们延迟超过三秒,所以会跳转InNormalHandler方法处理
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
public String InNormal(){
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "这是一个不正常的方法!";
}
//没有指明,以全局为准
@HystrixCommand
public String inNormal1(){
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "这是一个不正常的方法1!";
}
public String InNormalHandler(){
return "该业务超时!";
}
public String callBack(){
return "callBack....";
}
}
- 还有一种办法进行处理,直接使用@FeignClient来进行处理:这里以消费者为例
@Service
@FeignClient(value = "provider-pay-hystrix",fallback = HystrixAndFeignHandlerException.class)
public interface HystrixAndFeignService {
@GetMapping("/n")
String n();
@GetMapping("/i")
String i();
}
HystrixAndFeignHandlerException 是我们实现该接口的自定义类,该类里的实现方法对应该接口的方法的回调方法:
@Component
public class HystrixAndFeignHandlerException implements HystrixAndFeignService {
@Override
public String n() {
return "n.......";
}
@Override
public String i() {
return "i.......";
}
}
虽然是重写该方法,但该方法体的内容只有在出现错误或者超时的情况下才会触发.
修改yaml文件:
feign:
circuitbreaker:
enabled: true
注意: 如果我们对同方法使用 @FeignClient处理 和 @HystrixCommand的精确处理 和 @DefaultProperties+ @HystrixCommand的全局处理,返回结果的顺序是:@FeignClient>@HystrixCommand的精确处理>@DefaultProperties 全局.
服务熔断
当访问达到最大量的时候,直接拒绝访问,调用服务降级方法并返回友好提示.
@HystrixCommand(fallbackMethod = "callBack",commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled",value = "true"),// 是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),// 请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), // 时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"),// 失败率达到多少后跳闸
})
public String circuitBreaker(@PathVariable("id")Integer id){
if (id<0){
throw new RuntimeException("id不能小于0");
}
return "id="+id;
}
public String callBack(@PathVariable("id")Integer id ){
return "callBack....";
}
注意:fallbackMethod属性指定的回调方法其除了方法名不一样,其他任何参数都要一样,比如返回值类型,形参等等.
@GetMapping("/i2/{id}")
public String i2(@PathVariable("id")Integer id){
return paymentService.circuitBreaker(id);
}
可见,服务熔断也是服务降级,只是有些特殊.如果你测试的时候会发现,当我们一直传入负数,传出的请求错误次数超过百分之六十,就会开启熔断机制,这个时候不仅是负数请求都会走回调函数,正数请求也会走回调函数,只有连续多几次的正数请求后,正数的链路才会慢慢恢复.
熔断类型:
断路器在什么时候开始启动?
断路打开之后:
HystrixDashboard
- 新建springboot工程,在主启动类上加上@EnableHystrixDashboard.
- 在父工程加入以下依赖:
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-hystrix-dashboard -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>
- 子工程加入:
<dependencies>
<!--下面这两个依赖很重要:hystrix-dashboard,actuator-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 设置端口号,访问http://localhost:端口号/hystrix
- 前面的图片中,我们写上了监控地址,记住,除了地址前缀改变以外,所有的监控地址都得加上/hystrix.stream,这里比如我们要监控端口号为8085的微服务.
- 首先,在启动类上加上@EnableHystrix,这个注解内置了@EnableCircuitBreaker,所以你也可以在yaml文件中修改_feign.circuitbreaker.enabled=true_这个配置项.
- 然后往容器里注入一个bean
/**
*此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
*ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
*只要在自己的项目里配置上下面的servlet就可以了
*/
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
- 在监控工程Dashboard的yaml文件中,hystrix.dashboard.proxy-stream-allow-list="*"的配置项
- 测试的时候注意,要先进行一次服务熔断,然后在刷新监控页面,才会有图形化显示,若还是没有,说明你的配置没有成功.
服务限流
秒杀高并发操作,对某段时间内的请求量进行限制,剩下的进行排队,严禁超过限制的流量峰值.
服务网关
GateWay
简介
能做什么?反向代理,鉴权,流量控制,熔断,日志监控…
SpringCloud 和Zuul1.x的区别:
核心概念:
工作流程:
核心逻辑: 路由转发+执行过滤链
例子
- 下面我们演示,利用网关来掩饰真正的请求路径.
- 在父工程加入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>3.1.0</version>
</dependency>
- 在新的springboot工程加入依赖:
<dependencies>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--一般基础配置类-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 然后主启动类加上@EnableEurekaClient
- 修改yaml文件:
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8085 #匹配后提供服务的路由地址
# uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/n # 断言,路径相匹配的进行路由 /** 表示当前路径下所有
#- After=2020-02-21T15:51:37.485+08:00[Asia/Shanghai]
#- Cookie=username,zzyy
#- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
- 因为这里我们匹配的是http://localhost:8085/n 地址,所以我们可以利用网关访问 http://localhost:9527/n 这样我们便隐藏了真实地址.
- 同样的,我们可以使用@Bean的方式进行匹配:
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder){
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("r1",r->r.path("/n").uri("http://localhost:8085"));
return routes.build();
}
和前面的yaml文件一样,path这个路径会和后面的uri进行拼接得到一个地址,如果该地址存在,便可以跳转.
- 还可以进行动态路由配置:
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8085 #匹配后提供服务的路由地址
uri: lb://provider-pay-hystrix #匹配后提供服务的路由地址 lb为gateway的地址协议
predicates:
- Path=/n
predicates
在routes里有一个配置项为predicates,该配置项是uri配置项的一个条件:
比如第一次After,表示在某个时间后路由才生效,Before表示在某个时间前路由才生效,Between表示在某个时间段内路由才生效,而Cookie则表示要带上某段Cookie才能完成.
例如:
predicates:
- Cookie=username,zzyy #表示地址必须加上该Cookie才能访问
![image.png](https://img-blog.csdnimg.cn/img_convert/24c22a0e43ffce5edbe80eaf5b3f671b.png#clientId=u67999b26-a8fc-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=39&id=u55e48f8c&margin=[object Object]&name=image.png&originHeight=54&originWidth=1008&originalType=binary&ratio=1&rotation=0&showTitle=false&size=12791&status=done&style=none&taskId=u0e8658e9-1c0e-4987-8753-183abfbb6dd&title=&width=733.0909090909091)
可以发现,如果我们直接访问,便会报错,但如果我们:
带上Cookie便能访问.
Header表示要带上一个请求头的请求才有效:
- Header=X-Request-Id, \d+请求头要有X-Request-Id属性并且值为整数的正则表达式
_这个时候_无论我们访问http://localhost:9527/n -H "X-Request-Id:-11"还是http://localhost:9527/n 读不会成功,只有http://localhost:9527/n -H "X-Request-Id:正整数"才会成功.
更多设置请查看官网https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories
过滤器
GateWay内置了很多过滤器,请看官网,下面主要是讲解自定义的过滤器.
@Component
@Slf4j
public class GlobalGateWayFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String username = exchange.getRequest().getQueryParams().getFirst("username");
if (!Objects.equals(username, "123")){
return exchange.getResponse().setComplete();
}
log.info("传过来的用户名是:{}",username);
return chain.filter(exchange);
}
}
服务配置
Config(过时)
简介
简单来说,就是将各个子模块公共的配置提取到配置中心里,比如,多个子模块读连接同一个数据库,这个时候就可以将这相同的数据库连接配置到Config Server中,让大家都从这里拿.
能干嘛?
控制中心搭建
- 在gitee上新建一个仓库,然后在里面新建一个配置yaml文件.
- 只有新建一个springboot工程,在启动类上加上@EnableConfigServer,然后添加依赖:
<dependencies>
<!--下面这个依赖是本次主要依赖,不需要版本号,但是我在父工程引入该带有版本号的依赖时,Maven会报错,不知道为什么-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
</dependencies>
- 编写yaml文件:
server:
port: 3344
spring:
application:
name: config-server-1 #注册进Eureka服务器的微服务名
cloud:
config:
server:
git:
uri: https://gitee.com/hyb182/config-server.git #Gitee上面的git仓库名字
# 搜索目录,如果是地址所在根目录,就不用写这个路径
# 这个路径是按数组来写的,可以匹配多个,当你
search-paths:
- config-server
- a #代表我也要搜索a
default-label: master #如果报错main分支没有,则设置Master分支,反之这里设置为main
# username: # 适当的时候可以写用户名和密码
# password:
#读取分支,可以不配,默认是master
label: master
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
default-label: gitee为master,github为main.不然,如果是gitee,不配置为master,会报No such label: main 反之报No such label: master错误.
- 浏览地址:localhost:3344/搜索路径(search-paths)/文件名字 ,可以查看master(label)分支下的文件内容.如果文件就在地址本身当前目录下,就不用写搜索路径了.
Config客户端测试与配置
- 前面是Config的服务端,但Config也有客户端,从服务端拿取数据,也可以拿到外部yaml文件的值.
- 新建springboot工程,在主启动类加入@EnableEurekaClient
- 引入依赖:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- bootstrap.yml文件,该文件的优先级高于application.yml,会优先后者加载,用于提取公共的配置.
server:
port: 3355
spring:
application:
name: config-client-1
cloud:
#Config客户端配置
config:
label: master #分支名称
name: config #配置文件名称 与test形成config-test.yml文件
profile: test #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://localhost:3344/config-dev.yml
uri: http://localhost:3344 #配置中心地址k
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
- 编写controller测试:
@RestController
public class ConfigClientController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/s")
public String s(){
return serverPort;
}
}
- 测试/s请求,会识别config-test.yaml文件里server.port,然后返回.
- 注意点: 如果bootstrap.yml里和外部化配置yml文件有共同配置,以外部化配置文件为主,比如,这里的bootstrap.yml里配置了server.port,而config-test.yml里也配置了server.port,当启动工程的时候,该Tomcat端口号会以config-test为主.
如果你想要覆盖外部化配置文件的yml的端口号,可以用@Component的方式进行代码编写:
@Component
public class MyWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
@Override
public void customize(ConfigurableServletWebServerFactory server) {
server.setPort(3355);
}
}
增加如上代码,Tomcat启动便会识别3355,但@Value取到的server.port还是外部化配置文件的server.port.
手动实时刷新
前面我们使用客户端成功获取了gitee上的yaml文件值,但还存在一个问题,如果我们在gitee上修改yml文件的值,我们在客户端这边还要重启一遍机器才能获取最新的值,这样太过于麻烦,我们希望通过不重启服务器的方式就能实时刷新获取最新数据.
- 首先,在客户端的bootstrap.yml文件里增加如下配置:
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
- 然后在要刷新的controller类上加上@RefreshScope注解.
- 之后,修改gitee上的yml文件后,不用重启客户端的服务器,只需要再通过curl -X POST "http://localhost:3355/actuator/refresh" 进行刷新即可,虽然每次更新都要手动通过该命令刷新一次,但是在开发中可以通过脚本,使得一个命令可以刷新多台机器.
- 缺点: 虽然可以通过脚本的方式启动多台服务器,但这也是一台台启动而已,能不能通过广播的方式,刷新一处便全部刷新呢?或者指定刷新哪台机器呢?
消息总线(Spring Cloud Bus)
前面两张图片是两种消息通知的设计模式,第二张设计地比较合理一些,第一张设计地不适合,它将通知分给了客户端,增加了这些客户端的职责,而且,这些客户端可能是一个集群,增加了配置的负担,若是客户端发生了迁移,要修改的地方有很多,如果将消息放在统一配置中心,能简化客户端的职责,更加方便,客户端只负责接收消息即可.
整合Rabbitmq
通过前面的分析,我们希望通过刷新Config Server端来实现Config Client端的刷新.这样子,无论我们有多少个Config Client,都可以直接刷新Server端而不用刷新各自Client端.
ConfigServer端:
- 在导入rabbitmq的包:
<!--添加消息总线RabbitMQ支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
- 增加yaml配置,这个很重要:
spring:
rabbitmq:
host: 192.168.188.135
port: 5672
username: admin
password: 123
#rabbitmq相关配置,暴露bus刷新配置的端点
management:
endpoints: #暴露bus刷新配置的端点
web:
exposure:
include: "busrefresh" #这里的写法不同版本会不一样,周阳版本要在bus后加一个"-"
Config Client:
- Config Client 也加入rabbitmq整合的包
- 其次yaml文件也以同样的方式配置rabbitmq的账号密码
- 只不过端点暴露的方式不一样:
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
测试: Config依赖的微服务都要一次启动
启动后,先浏览一次http://localhost:3355/s,发现可查看gitee中yml文件内容,如果我们修改yml内容,再次刷新该网址,发现不会及时获取.前面我们说过,必须重启3355这个服务器,或者是在cmd进行curl -X POST "http://localhost:3355/actuator/refresh"这个命令的执行就可以获取修改后的数据,但是,通过rabbitmq这个配置后,我们只需要在cmd中,以类似的命令,去刷新Config Server来达到统一的进行所有Config Client的刷新:
curl -X POST "http://localhost:3344/actuator/busrefresh" 在cmd窗口测试,注意busrefresh和端点暴露的include值一致,这里因为版本的不同和周阳老师的有些不一样.
如果cmd命令行有问题,可以尝试用postman进行测试,如果postman测试报的错还是一样,那证明配置有问题.
- 我们还可以在Config Server这一端刷新指定Config Client,
在官网上是这么解释:
指定Config Client+其端口号,但本人自测不成功.
本人看到了这句话,于是就萌生了将/busrefresh/ConfigClient:*/** 结合去尝试,结果发现,这样就能成功了.该方式表示将该客户端下所有端口号的服务都刷新.就很离谱.
消息驱动
SpringCloudStream(注解版[过时])
在开发中,消息处理我们可能使用rabbitmq,但数据处理和监控(大数据)我们可能在用Kafka,不同MQ技术可能存在差异,所以在此中间,我们希望有一种技术实现不同技术的连接和维护.
SpringCloudStream 通过绑定器作为中间层,实现了应用程序与消息中间件细节之间的隔离.
生产者
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: 192.168.188.135
port: 5672
username: admin
password: 123
bindings: # 服务的整合处理
output: # 这个名字是一个通道的名称,表示这个是生产者
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
instance-id: 8801 # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
public interface MessageProvider {
public String send();
}
@EnableBinding(Source.class)
public class MessageProviderImpl implements MessageProvider {
@Autowired
MessageChannel output;
@Override
public String send() {
output.send(MessageBuilder.withPayload("这是一条生产者的消息").build());
return null;
}
}
@SpringBootTest
public class MessageProviderTest {
@Autowired
MessageProviderImpl messageProvider;
@Test
public void t1(){
messageProvider.send();
}
}
消费者
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: 192.168.188.135
port: 5672
username: admin
password: 123
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称,表示这个是消费者
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为对象json,如果是文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
instance-id: 8802 # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
@Controller
@EnableBinding(Sink.class)
public class ConsumerController8802 {
@StreamListener(Sink.INPUT)
public void input(Message<String> message){
System.out.println(message.getPayload());
}
}
测试:
启动依赖的Eureka,然后再启动生产者和消费者.
从生产者的测试方法中发送一条消息,消费者的监听器立马能接收到并将消息打印在控制台上.
该版本使用了最新版的3.2,但是在3.x开始,该框架已经放弃了原始注解,要使用响应式编程.
解决重复消费
在rabbitmq中,不同组的可以重复消费,同样组存在竞争关系,只能由一个消费者去消费.
spring.cloud.bindings.input.group 可以指定组别.
默认情况下,一个微服务是一个消费者,也是一个组,所以如果不设置,不同微服务要进行重复消费.
消息的持久化
spring.cloud.bindings.input.group 不仅可以指定组别,还可以让消息持久化,如果指定了组别,若该消费者宕机,下一次启动,可以消费之前没消费掉的消息,不会造成消息丢失.
如果没有指定组别,虽然默认是自己一组,但是该消费者便消费不到之前没消费的消息,造成消息丢失.
分布式请求链路跟踪
SpringCloud Sleuth
在微服务开发中,每一个请求都会经过不同模块的调用,这样就会产生一条请求链路,链路的任何一个环节出了问题都会造成严重性后果.
而且一旦微服务模块增多,就会造成混乱,这样一个请求的维护会变得困难,SpringCloud Sleuth便提供了链路跟踪机制,通过链路跟踪,我们可以获取一个请求经过的链路信息,方便我们管理模块和维护.
- 在官网https://zipkin.io/pages/quickstart.html 下载java的jar包,然后运行该jar包
- 浏览器访问: http://localhost:9411/ ,看到图形界面即可.
- 生产者和消费者都要加入如下jar包:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
<version>2.2.8.RELEASE</version>
</dependency>
- 然后都要在yaml文件中,增加如下配置:
spring:
zipkin:
base-url: http://localhost:9411/ #默认不写也行
sleuth:
sampler:
# 采集信息率,一般介于0到1之间,1表示全采集
probability: 1
- 然后启动各个微服务,执行一条正常的调用,这个时候,图形界面就可以显示依赖信息:
SpringCloud Alibaba***
简介
- 由于SpringCloud Netflix 进入了维护模式,所以Alibaba便出了一套新的进行替代.
- SpringCloud Alibaba能干什么?
- 中文官网: https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/README-zh.md 可查看.
- 英文官网: https://spring-cloud-alibaba-group.github.io/github-pages/hoxton/en-us/index.html#_dependency_management
- 主要功能:
- 服务限流降级:默认支持 WebServlet、WebFlux、OpenFeign、RestTemplate、Spring Cloud Gateway、Zuul、Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
- 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
- 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
- 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
- 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
- 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
- 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
- 学习之前,请现在父工程引入pom
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.7.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Nacos
- Naming Configuration Service
- Nacos 是一个易于使用的动态服务发现、配置和服务管理平台,用于构建云原生应用程序(服务注册与配置中心)
- 可以理解为 Eureka+Config+Bus
- 官网: https://nacos.io/zh-cn/
Windows安装
- 官网下载最新的2.x版本,这里我选择2.0.4,下载zip的,这个windows版本.
- 下载完成,解压该文件得到一个安装包,然后在该安装的bin目录下,进入cmd界面
- 查看官网,发现该版本的启动命令为startup.cmd -m standalone 该启动命令为单机模式的启动命令.
- 启动成功后,会出现success字样,然后会有要你登录网址,端口为8848,账号和密码都为nacos.
服务注册中心
nacos可替代Eureka做服务注册中心
提供者
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
server:
port: 9002
spring:
application:
name: nacos-provider-pay
cloud:
nacos:
discovery:
server-addr: 192.168.216.1:8848
management:
endpoints:
web:
exposure:
include: "*"
@RestController
public class EchoController {
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/echo/{string}")
public String echo(@PathVariable String string) {
return "Hello Nacos Discovery " + string+serverPort;
}
}
消费者
- pom和yml都一样,只不过端口号和服务名不一样.
- controller用restTemplate调用,记得先注入组件
@Autowired
RestTemplate restTemplate;
@GetMapping("/echo/{string}")
public String echo(@PathVariable String string){
String forObject = restTemplate.getForObject(addr+"/echo/"+string, String.class);
return forObject;
}
@Bean
public RestTemplate rest(){
return new RestTemplate();
}
- 调用成功即可.
- 这里主要演示负载均衡,在@Bean后加入注解@LoadBalance
- 然后新建另一个提供者,端口号不一样
- 之后测试消费者调用提供者,发现端口号都会遍历到,说明naos默认轮询的负载均衡策略.
- 这里我们在引入的依赖可以求证:
拓展
nacos支持cp和ap模式的切换.
服务配置中心
nacos可替代config做服务配置中心
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
</dependencies>
bootstrap.yml
spring:
cloud:
nacos:
config:
server-addr: 192.168.216.1:8848 #主要功能配置
file-extension: yml
discovery:
server-addr: 192.168.216.1:8848 # 自己也得注册服务
application:
name: nacos-config
application.yml
spring:
profiles:
active: prod
server:
port: 3377
@RestController
@RefreshScope
public class NacosConfigController {
@Value("${config.info}")
private String info;
@GetMapping("/c")
public String c(){
return info;
}
}
新建文件:
注意: 这个文件命令有讲究,在官网中,其全名是application.name-spring.profiles.active.file-extension,所以起名字要根据配置文件里的某些属性对应.
这里我们在新建的yml文件是:
config:
info: 1
nacos config除了可以读取外部化配置文件外,还可以进行自动刷新,当我们在外部的yml修改配置后,不用手动刷新一次,直接便可以获取到最新值.
nacos 特色
nacos具有Eureka,config,bus等框架实现的功能,更加强大.
从这张图我们可以看出,nacos的核心名词有这三个.命名空间,Data Id 和Group
这三者的关系如下
Data Id :前面说过,Data Id是有一定规则拼凑的,其中就有环境的名称,所以通过spring.profiles.active.file的修改,就可以读取不同环境下的外部化配置文件.
Group: 与file-extension 同级下,加上group配置,指定一个组别,指定只能读取是这个组别的外部化配置文件.
NameSpace: 与group同级,加上namespace配置,指定命令空间,读取时只能读取到该命令空间下的文件.
持久化和集群搭建
nacos有内嵌数据库,但不是mysql,官方建议我们更换为mysql,进行数据的持久化.
同时,我们前面演示的是windows版本的nacos,在开发中,我们肯定要进行集群版本的nacos搭建.
- 下载tar.gz包,然后解压,这里准备三台机器,都要进行相同的动作.
- 在nacos的解压目录nacos/的conf目录下,有配置文件cluster.conf.sample,将其修改成cluster.conf,请每行配置成ip:port。(请配置3个或3个以上节点)
这些配置都要在三台机器上配置.
- 修改持久化数据库(可选)
- 在conf目录下有一个nacos-mysql.sql文件,该文件里有可执行sql语句,将该语句复制到mysql客户端中执行,注意,在此之前,记得先生新建一个数据库再执行.
2)然后在conf目录下修改application.properties文件,将如下配置打开:
spring.datasource.platform=mysql
### Count of DB:
db.num=1
### Connect URL of DB:
# 数据库连接地址,hyb是刚才执行sql语句的数据库名字
db.url.0=jdbc:mysql://127.0.0.1:3306/hyb?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=数据库密码
该数据库的连接可以是windows远程连接的数据库,也可以使linux下的数据库.
- 进入bin下的startup.sh文件,修改启动内存,防止个人机器启动太慢
- 启动机器有多种类型:
单台机器的启动:sh startup.sh -m standalone
使用内置数据源集群的启动:sh startup.sh -p embedded
使用mysql数据源的启动:sh startup.sh
都启动完成后,可登录任意一台机器,查看节点:
- Nginx做反向代理:
- 安装Nginx:(只在一台机器上安装即可)
yum install -y yum-utils
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
yum install -y openresty
- cd /usr/local/openresty 若有Nginx则说明安装Nginx成功
3) 进入Nginx 的conf 的Nginx.conf文件修改.在http里加入以下配置:
upstream nacoscluster{
server 192.168.188.100:8848;
server 192.168.188.130:8848;
server 192.168.188.134:8848;
}
server{
listen 8847;
server_name localhost;
location /nacos/{
proxy_pass http://nacoscluster/nacos/;
}
}
# 这些配置的意思是,由server进行代理,代理的地址式upstream标志的.
- 这个时候,在nginx目录下,sbin/nginx可启动nginx.启动完成后,浏览http://192.168.188.100:8847/nacos/index.html 也可以访问前台,我们从100这个端口进来是因为nginx默认会对我们集群的端口进行轮询代理,而我们代理的第一个地址端口就是100.也可以浏览http://192.168.188.134:8847/nacos/index.html因为我在134这个端口的机器安装的nginx.
5) 如果要注册服务,我们只需注册进nginx代理的地址即可.
6) 注意:linux下和idea的nacos版本必须一致.
Sentinel
简介
Sentinel主要是用来解决服务雪崩等问题.
- 什么是服务雪崩?
官方: 因服务提供者的不可用导致服务调用者的不可用,并将不可用逐渐放大的过程称为服务雪崩.
简单来说:
比如上图,首先是1调用共享,然后调用4,如果共享和4之间挂掉了,造成1开始的链路请求堵塞,在共享中积压,然后这个时候,2,3等微服务进来,因为4挂掉了,所以这个时候也造成请求积压,一旦共享因为积压请求挂掉了,就会造成1,2,3的危险雪崩.
所以,如果没有一个很好的容错机制的话,就会造成雪崩效应,有了容错机制,就能很好提高系统的可用性.
- 容错机制有哪些?
- 超时机制:对请求设置超时时间,一旦超时,进行请求降级.
2) 服务限流: 对每次的请求量进行限流,防止超过线程池的最高流量,一旦超流,进行服务降级
- 什么是服务熔断?
- 前面说过服务雪崩效应是因为请求迟迟没有响应,这个时候我们就可以在调用者一方设置一个熔断机制,进行停止访问,而不是一直等待回应,避免雪崩效应.
- 什么是服务降级?
比如在服务熔断期间,必须要有一个兜底的方案,因为这个时候请求是断开的,所以我们不可能返回断开信息给用户,肯定得有一个提示兜底的方案去处理.
服务降级一般发生在弱依赖关系中,就是某个断开的,不会产生太大影响链路请求的微服务.
- 什么是Sentinel?
Sentinel 是面向分布式服务架构的流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统自适应保护等多个维度来帮助您保障微服务的稳定性。
官网:https://sentinelguard.io/zh-cn/docs/introduction.html
- Sentinel和HyStrix的对比:
- Sentinel的主要特征:
Sentinel Dashboard安装
这是一个Sentinel监控的管理平台
只需要在官网下载对应版本的jar包,这里本人使用1.7的.
下载jar包,用java -jar运行该jar包,成功后,查看浏览端口,1.7的为localhost:8080
登录账号和密码都是sentinel.
登录后,可以做一个小的测试,新建一个微服务,要导入sentinel-starter的jar.:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
让该服务注册到nacos中,被sentinel监控:
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.188.100:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719 #表示与该微服务交互的端口,默认,可以不写8080是其sentinel控制台的端口
测试: 需要进行一次请求,sentinel的管控平台才会有流量显示,需要请求几次,实时监控才会刷新出趋势图.
Sentinel流量控制
- 设置最高访问量
这个时候,表示一秒内的最高流量限制为1.
不然会报Blocked by Sentinel (flow limiting) 错误.
- 对流控设置的一些名词解释:
![image.png](https://img-blog.csdnimg.cn/img_convert/a2d8a2293b357562d89994deb92682c0.png#clientId=ud407b6f3-9f87-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=175&id=uc4de53e5&margin=[object Object]&name=image.png&originHeight=219&originWidth=1076&originalType=binary&ratio=1&rotation=0&showTitle=false&size=64609&status=done&style=none&taskId=ud428c09b-60c8-4712-8079-c853aa95d16&title=&width=860.8)
- 一秒钟只允许一个线程访问:
测试: 在请求的代码返回值之前,延迟几秒钟,然后在浏览器请求的时候,还未返回结果之前,重新发送请求,这个时候,表示还未返回结果,又一个线程进来了,就会报错.
- 高级选项:关联模式.
该模式的意思是,如果关联资源的每秒请求的数量超过1,那么资源/echo/1就会出现错误.
测试: 另开一个浏览器,访问关联资源/echo/2,一秒内再请求/echo/1,会发现1报错了.
- 链路流控:
链路流控代表从入口资源到资源这条链路被限制,如果有与其他共享资源在这条链路中,不会影响其他链路的使用.
指定入口资源为a,限流a和共享资源之间的链路,入口资源b不影响使用.
但值得注意的是: 无论是哪个版本,链路限流不开启的,也就是官方将链路设置为了收敛,我们要将其设置为不收敛.
从1.6.3 版本开始,Sentinel Web fifilter默认收敛所有URL的入口context,因此链路限流不生效。
1.7.0 版本开始(对应SCA的2.1.1.RELEASE),官方在CommonFilter 引入了
WEB_CONTEXT_UNIFY 参数,用于控制是否收敛context。将其配置为 false 即可根据不同的URL 进行链路限流。
SCA 2.2.7.RELEASE之后的版本,可以通过配置spring.cloud.sentinel.web-context-unify=false即可关闭收敛
- 请求预热:
刚开始请求一秒钟的上限值是(单机上限值/默认值[10/3=3]),直到预热时长(5秒)后,单机上限值才慢慢恢复到10.
测试: 上限值虽然设为了10,但是刚开始上限是10/3=3个请求,所以一秒内超过三个请求会报错,但是过了五秒后,上限值到达10,这个时候一秒内超过10个请求才会报错.
- 排队等待效果:
该设置表示,对该请求一秒内只允许一个请求,如果多出,排队等待,等待的请求超过2000毫秒就会请求失败.
测试: 我们可以在请求代码中设置一个count:
这个时候,我们测试请求,不断点击,会发现,控制台不断输出count的值,一直累加,这是因为虽然一秒内只允许一个请求进来,但是其他请求可以等待,只要不超出等待时间,这些请求都是有效的,一秒好便轮到他,但是我们继续狂点,请求多了,有一些请求等待时间则会变长,那么这个时候就会过时,请求就会失败,网页报错.
Sentinel服务降级
降级的类别:
注意: Sentinel断路器没有半开的状态,半开状态是HyStrix的概念,一旦服务熔断,系统会自动监测是否有异常,有异常则不修复断路器,没异常则尝试进行修复请求.
- RT设置:
如果一秒内打进来的请求超过5个,要求每个请求都100毫秒内处理完,如果不行,则进行熔断,熔断时间为1秒,一秒后尝试恢复.(注意: 如果打进来的请求不超过5个,则不会熔断,这是1.7版本的默认值)我们将这些在一个时间内不能完成的请求称为慢调用.
测试: 让被调用的方法沉睡十秒,然后不断点击地址栏左边的圈圈,表示一时间多个请求进来,之后会发现出现了错误,然后等待一秒,又恢复了.
- 异常比例:
一秒请求必须大于等于5个才会触发.
异常比例为,一秒内请求失败的数量占一秒内总请求数量,如果出现超过20%的异常,则熔断,熔断一秒.
测试: 给被调用的方法写入一个算术异常,因为每次都调用这个方法,所以每次都有算术异常,这个时候,异常比例远远超出预设置的,所以在我们不断刷新地址栏旁边圈圈的时候,就会降级.如果你单独访问一次,就是一秒没超过五次请求,肯定不会降级.
- 异常数降级:
没有时间限制,只要异常的请求超过5次,就会降级一秒钟.
热点key限流
热点key限流也是限流的一种,只不过该限流方式通过对参数key进行限流.比如在某个网站中,某个网址的访问带的参数是比较热门的,如果http://www.baidu.com?a=1 ,我们就可以对带有a=1这个key的网址进行限流.
- 撰写请求代码:
@GetMapping("/key")
//value的值不一定要为key,这里只是为了保证代码编程规范
@SentinelResource(value = "key",blockHandler = "handleKey")
public String key(@RequestParam(value = "p1",required = false)String p1){
return "key....";
}
public String handleKey(String p1, BlockException blockException){
return "key..出现错误,进入兜底.";
}
资源名是@SentinelResource的value值,不是请求名/key.
参数索引是参数的位置,这个位置不是url后面参数的位置,而是请求方法里的参数位置.
单机阈值一秒内允许访问量,统计窗口时长为熔断时长.
一定要使用@SentinelResource方式进行热点key测试,不然不通过.blockHandler表示自定义兜底方法,自定义的兜底方法参数要一致,另外加上一个BlockException类型的参数.
测试: 对浏览地址写上第0个参数p1,然后狂点击圈圈,发现会熔断,进入兜底方法.
- 热点key额外参数配置:
在添加key参数的高级选项,有参数例外项,可以为带有某个值的参数添加额外的限流.
这里的设置意思为,对第0个参数设置限制一秒内为1请求量,但当第0个参数值为5时,限流阈值为200.
注意: 如果此刻,我们在上述被调用的方法中写一句算术异常的代码,该错误出现会覆盖热点key带来的错误,因为热点key只能处理平台设置的热点key限流,而不能处理java代码带来的异常,而又因为前者处理在先,后者进入方法才出现算术异常,所以会被覆盖.
系统规则
系统规则是通过一系列的规则来达到对系统进行降级保护.
系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 和线程数四个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则支持以下的阈值类型:
- Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般是 CPU cores * 2.5。
- CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0)。
- RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
@SentinelResource
该注解前面用到过,方便对被限流的方法更好的设置限流规则.
- 首先,我们能value可以指定限流的资源名称,当然这个限流的资源名称可以是方法的请求地址.
- blockHandler 指定兜底的降级方法,去除了系统默认方法,灵活度更高,但如果每个限流方法都要指定一个降级方法,就会产生代码膨胀.
- 可以使用全局的降级方法解决代码膨胀的问题.
- 虽然有全局的降级方法,但每次修改代码都要进入到该降级方法去处理,一个系统又有很多个类,这样使得代码耦合度不高,所以产生了自定义限流处理的方法:
public class MyHandler {
public static String handleException(BlockException blockException){
return "MyHandler...";
}
}
@GetMapping("/k")
@SentinelResource(value = "k",
blockHandlerClass = MyHandler.class,
blockHandler = "handleException")
public String k(){
return "k.....";
}
注意:自定义类里的处理方法必须是静态的,而且参数要和请求方法一致,另外多加一个BlockException类型变量.
- 还可以指定fallback属性,该属性用来指定java代码异常的处理方法,如果和blockHandler一起配置,若同时出现错误,以后者为主.注意: fallback属性也有默认配置和自定义配置.
- exceptionsToIgnore 异常忽略属性,该属性用来指定可以忽略的异常,该忽略的异常是代码层面的.
- exceptionsToTrace 异常追踪属性,该属性用来追踪哪些异常才进入兜底.
Open Feign
Sentinel也可以结合OpenFeign使用,引入OpenFeign的依赖即可.
操作与OpenFeign一样,只不过要操作Sentinel的时候,指定feign.sentinel.enabled=true
Sentinel持久化到Nacos
前面我们进行流量控制和限流的时候,每启动一次服务器都要重新新建一个流控规则,并且这个流控规则是新的流控规则,不能保留原来的.Sentinel提供了将流控规则持久化到Nacos的方式,下一次重启服务器的时候,就可以直接从Nacos拉取流控规则到Sentinel里,且这个规则还是原来的.
- 首先,我们先在nacos新建一个配置:
[
{
"resource": "/echo1",
"limitApp": "default",
"grade": 1,
"count": 5,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
- 在使用Sentinel的微服务中引入以下pom:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
- 然后修改yml,加入以下配置:
spring:
cloud:
sentinel:
datasource:
flow-sentinel: #自定义
nacos: #对应nacos的规则
server-addr: 192.168.216.1:8848
username: nacos
password: nacos
data-id: my-sentinel
group-id: DEFAULT_GROUP
data-type: json
rule-type: flow
namespace: public
- 测试: 首先启动一个微服务,刷新一个请求,会发现该请求的流控端点自动出现在流控规则中,而且规则都是在nacos的json文件中配置好的.其次,我们再关闭微服务,刷新Sentinel控制台,发现流控端点消失,因为微服务关闭了,这个时候我们再次启动微服务,点击刷新Sentinel控制台,发现还是没有流控端点,这不是没有持久化,而是没有刷新一个请求,再刷新一次请求,便会发现与上次一模一样的流控端点又出现了.如果还是不出现,说明持久化不成功.
Seata
分布式事务
在传统的本地事务中,一般都只是一个系统一个数据库.但对于分布式系统中,尤其是在微服务思想架构下的系统,每个模块都有可能有自己的数据库和数据表,俗称分库分表,这个时候,每个数据库只能保证本地的数据统一性,不能做到全局事务,保证整体的统一性,因为各个微服务之间的数据库是隔离的.
简介
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
其采用一加三的模式:一是全局事务ID XID,三是三个重要组件TC TM RM:
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
处理过程
Windows安装
- 网页https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E查看版本说明,版本一定要对应.
这里我学习Seata使用的版本是spring-cloud-alibaba 2.2.7:
- 官网https://seata.io/zh-cn/blog/download.html 点击binary下载,这个下载得很慢,推荐去一个国外的依赖收集网站去下载: https://sourceforge.net/mirror/seata/activity/?page=0&limit=100#60921f4ad74acf009abdb010
- 下载完成后,解压,得到seata-server-1.42文件,需要修改一些配置.
- 进入conf目录的file.conf文件,修改下面这些地方:
store->model 选择db数据库模式.
于是乎,我们就要配置db的大括号内容:
这个就是数据库连接,这些连接按照自己的mysql版本去设定.
2) 进入registry.conf文件,修改以下地方:
选择注册中心类型为nacos或者指定负载均衡策略,后者可以不指定,默认是ribbon
指定了哪个为注册中心,就要配置哪个为注册中心,指定了nacos,就要配置nacos里的大括号内容:
这里面包含了注册中心地址,组,还是集群,用户名和密码.
选择注册中心后,还要选择配置中心,也为nacos,配置几乎一样.
- 下载seata-1.42资源文件,不带有server字样的文件.然后将script包导入seata-server-1.42中,这个不导入也行,只是为了方便.
- 进入\script\config-center包中的config.txt文件:
- 修改service.vgroupMapping.my_test_tx_group分组配置,注意,该配置的默认值是default,但待会我们在yml文件里不是配置default,所以为了方便,我们将default改为my_test_tx_group.这个事务分组主要是为了解决机房停电容错的问题,假如北京组的机房停电,便可以马上切换到另一个组别的机房.其中service.vgroupMapping后为分组名,=号后为分组内容. 其中这个分组内容必须要和注册中心里的cluster的值相同.
2) 然后我们还需要修改一些有关数据库的配置:
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=15717747056HYb!
这些配置和刚才的数据库配置一致.
- 将config.txt文件的内容导入nacos的配置中心里,在\script\config-center\nacos包下有一个nacos-config.sh文件,双击运行,前提要安装git.运行会发现config.txt里的内容一点点转移到了nacos的配置中心里,传输完成后,便可以上nacos查看了,记住,在传输钱,先启动nacos.
- 准备完毕,便可以启动seata,在seata-server的bin目录下,有一个bat文件,双击启动即可.
环境准备
- seata安装成功并启动:
- 修改file.conf文件里的数据库配置
2) 修改registry.conf里的nacos配置
3) 修改config.txt文件里的配置,然后导入到nacos中
4) 点击bin里的bat文件启动,如果不成功,看导入的nacos配置是否和刚刚修改的一致.
- 准备一个记录全局事务的seata数据库,和三个更加细分的事务数据库,这些数据库名字要和官方的一致,如果不想一致,可以修改nacos导入的配置
- 三个更加细分的数据库要准备一个日志表,名字也必须一致,如果不一致,可进入nacos配置里修改.
准备数据库环境
- 创建一个刚才我们在配置文件里写的数据库seata,然后在\script\server\db包下找到mysql.sql文件里,里面装有mysql语句:
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data 存储全局事务信息
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data 存储分支事务信息
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data 存储锁表信息,一旦出现问题,会将锁将该事务隔离出来,然后根据锁表信息回滚,
-- 如何回滚? 通过锁表信息,逆向生成sql语句,比如insert语句遇到问题后,就逆向生成删除语句.
-- 并不是说遇到问题了insert就真的不执行了,只是用delete语句删除以下而已
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
拿着这些语句去seata数据库下执行.这个数据库是系统必要的数据库.
- 其次,通过下面的sql脚本创建出另外三个数据库:
DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `account_tbl`;
CREATE TABLE `account_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 便是我们代码层面的数据表,可以使用官方的,也可以新建我们自己的,但是必须要在官方的三个数据库下,一个代表订单Order,一个代表库存Stroage.
库存的id是订单的外键pro_id.待会我们要做的实验就是,插入一条订单,就减少一个库存,并模拟事务效果.
- 其次,分别在这官方的三个数据库下,做一个必要的数据表,也是官方推荐做的:
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
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;
代码实现
库存模块
<dependencies>
<!-- https://mvnrepository.com/artifact/io.seata/seata-core -->
<!--loadbalancer和openfeign一定要联合其使用,除非其他包包含loadbalancer-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-loadbalancer</artifactId>-->
<!-- <version>2.2.7.RELEASE</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-openfeign</artifactId>-->
<!-- <version>2.2.7.RELEASE</version>-->
<!-- </dependency>-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.1.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<!-- <exclusions>-->
<!-- <!–但加入了loadbalancer这个包,一定要排除nacos里的ribbon–>-->
<!-- <exclusion>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>-->
<!-- </exclusion>-->
<!-- </exclusions>-->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!--mysql-connector-java-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
seata-spring-boot-starter和spring-cloud-starter-alibaba-seata要结合使用,并且使用了这两个,就不能使用OpenFeign,本人也不知道原因,不然就会报错,启动不了,可能是版本的问题.
server:
port: 8888
spring:
main:
allow-circular-references: true
application:
name: storage
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver # mysql驱动包
url: jdbc:mysql://localhost:3306/seata_storage?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: 15717747056HYb!
cloud:
nacos:
discovery:
server-addr: 192.168.216.1:8848
mybatis:
mapperLocations: classpath:mapper/*.xml
type-aliases-package: com.hyb.springcloud.entities
configuration: #指定mybatis全局配置文件
map-underscore-to-camel-case: true # 启动驼峰命名规则
seata:
registry:
type: nacos
nacos:
server-addr: 192.168.216.1:8848
# seata服务名
application: seata-server
username: nacos
password: nacos
config:
type: nacos
nacos:
server-addr: 192.168.216.1:8848
username: nacos
password: nacos
tx-service-group: my_test_tx_group #这个名字不是default
dao:
@Mapper
public interface StorageDao {
@Update("update storage set count=count-1 where `id`=#{proId}")
public int update(Integer proId);
}
service:
@Service
public class StorageService {
@Autowired
StorageDao storageDao;
public int updateService(Integer id){
return storageDao.update(id);
}
}
controller:
@RestController
public class StorageController {
@Autowired
StorageService storageService;
@GetMapping("/update")
public String update() {
storageService.updateService(1);
int i=10/0;
return 1+"";
}
}
controller层我们模拟出错,被订单调用.
entities:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Storage {
private Integer id;
private Integer count;
}
主函数:
@SpringBootApplication
@EnableDiscoveryClient
@EnableTransactionManagement
订单模块
<dependencies>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.1.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!--mysql-connector-java-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
订单模块的依赖一样,yml文件也差不多
server:
port: 8889
spring:
main:
allow-circular-references: true #允许循环依赖
application:
name: order
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver # mysql驱动包
url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: 15717747056HYb!
cloud:
nacos:
discovery:
server-addr: 192.168.216.1:8848
mybatis:
mapperLocations: classpath:mapper/*.xml
type-aliases-package: com.hyb.springcloud.entities
configuration: #指定mybatis全局配置文件
map-underscore-to-camel-case: true # 启动驼峰命名规则
seata:
registry:
type: nacos
nacos:
server-addr: 192.168.216.1:8848
# seata服务名
application: seata-server
username: nacos
password: nacos
config:
type: nacos
nacos:
server-addr: 192.168.216.1:8848
username: nacos
password: nacos
tx-service-group: my_test_tx_group
@Mapper
public interface OrderDao {
@Insert("insert into sorder(`pro_id`) values(#{proId})")
@Options(useGeneratedKeys = true,keyProperty = "id")
int insert(Order order);
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
private Integer id;
private Integer proId;
}
@RestController
public class OrderController {
@Autowired
OrderService orderService;
@Autowired
RestTemplate restTemplate;
// @Transactional
@GlobalTransactional
public String s(){
Order order = new Order(null, 1);
int insert = orderService.insert(order);
// orderOpenfeign.update();
restTemplate.getForObject("http://localhost:8888/update",String.class);
return insert+"";
}
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
注意: 在网上的教程,@Transactional注解标上去,理应上是只能将本方法的错误回滚的,但是这里我尝试了一下,其效果居然和全局的一样.
测试
本人测试了很久,都是因为依赖的问题,各种问题.
首先,没有加任何事务注解,执行,即使发生错误,但因为有代码执行先后的问题,所以两个数据库的表都扣除了信息,这理论上是正常的,但是在实际的操作数据库的过程中,代码是不允许有错误出现的,所以只能回滚.这里本人使用本地事务也能回滚,进行过很多次测试,发现结果都和全局事务注解的效果一样,不知道为什么.