https://spring.io/projects/spring-cloud 网址查看版本变化,版本一定要对应.
File | Settings | Editor | File Encodings
dependencyManagement 标签和dependencies的区别:
dependencyManagement 只是父工程的一个规范,并不实际引入依赖,该规范使得子工程可以沿用父工程的版本,避免因版本号不同而引起的问题.dependencies则实际引入依赖.
注:子工程若想对接父工程的版本,要引入坐标,但不用引入版本号,若想继续引用版本号,则会覆盖父工程的版本号.
<groupId>com.hyb.springCloudgroupId>
<artifactId>hybSpringCloudartifactId>
<version>1.0-SNAPSHOTversion>
<packaging>pompackaging>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
<log4j.version>1.2.17log4j.version>
<lombok.version>1.16.18lombok.version>
<mysql.version>8.0.22mysql.version>
<druid.version>1.2.5druid.version>
<mybatis.spring.boot.version>2.1.4mybatis.spring.boot.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>2.6.1version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>2021.0.0version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.2.7.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>${mysql.version}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>${druid.version}version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>${mybatis.spring.boot.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<version>2.6.2version>
<scope>testscope>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>${log4j.version}version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>${lombok.version}version>
<optional>trueoptional>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<fork>truefork>
<addResources>trueaddResources>
configuration>
plugin>
plugins>
build>
新建完毕后,会发现在父工程模块的pom文件中,会增加一个子模块的信息:
<modules>
<module>provider-paymodule>
modules>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
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别名类所在包
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment implements Serializable {
private Integer id;
private String series;
}
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">
<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>
@Mapper
public interface PaymentDao {
public void insert(Payment payment);
public Payment getPaymentById(@Param("id")Integer id);
}
public interface PaymentService {
public void insert(Payment payment);
public Payment getPaymentById(@Param("id")Integer id);
}
@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);
}
}
@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);
}
}
@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);
}
}
}
自动热部署工具.
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<version>2.6.2version>
dependency>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<fork>truefork>
<addResources>trueaddResources>
configuration>
plugin>
子工程也引入devtools但可以不用引入版本
<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>hybSpringCloudartifactId>
<groupId>com.hyb.springCloudgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>consumer-orderartifactId>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
project>
server:
port: 80 # 该端口号与其他端口号不同的是,该端口号的服务被浏览不用写端口号80
订单要用到支付模块的东西,所以订单要解决的问题是这两个服务之间的通信,而让两个服务之间能通讯,我们就必须使用到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方法如何传递参数:
postForObject方法如何传递参数:
工程重构使得不同的模块的共同代码都能抽取到同一个模块,而省去大量的重复代码.
新建一个Maven工程,然后将两个模块重复的部分提取到该工程中,比如实体包下的所有类.
导入该工程依赖:
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.7.20version>
dependency>
dependencies>
<dependency>
<groupId>com.hyb.springCloudgroupId>
<artifactId>commons-cloud-apiartifactId>
<version>${project.version}version>
dependency>
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:
@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 方法能返回请求体,状态码等等详细信息.
在传统的rpc远程服务调用框架中,管理每个服务与服务之间的依赖关系比较复杂,需要用到服务治理,用来实现服务调用,容错,负载均衡,服务注册和发现等功能.
可以看到,各个模块之间若是进行通信,可以先向注册中心注册自己的信息,然后通信的时候,可以在注册中心进行,从而省去了直接与模块进行通信,减轻被访问的模块负担.
客户端还内置了一个轮询(round-robin)算法为基础的负载均衡器.
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
<version>3.1.0version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
<dependency>
<groupId>com.hyb.springcloudgroupId>
<artifactId>commons-cloud-apiartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
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/
安装了注册中心,我们可以将前面的支付模块注册进Eureka Server中.
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
<version>3.1.0version>
dependency>
然后在支付模块中加入该依赖,不用写版本号.
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka
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:端口号
注意:这两个配置文件表示互相注册,产生关联.
127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com
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中
这里我们的地址必须修改成http://提供者集群名字:
修改完之后还不行,虽然我们指定了访问哪个提供者集群,但是具体上还是要访问某个提供者,所以这个时候,如果我们启动测试这个订单模块,就会报错,说该provider-pay找不到,这是因为,这是一个集群的名字,Eureka 底层不知道要调用哪一个具体的提供者,所以我们该对其进行负载均衡,让其通过轮询的策略进行具体的提供者调用:
我们具体调用提供者是用到了RestTemplate组件,所以要在注册该组件进入IOC容器的时候指定负载均衡策略:
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
instance:
instance-id: 8081 #名字以端口号显示
prefer-ip-address: true # 鼠标放上去有ip地址
在服务可发现且注册中心启动和服务启动的前提下,利用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 Server会跟每个微服务进行心跳连接,如果超过了90秒没有心跳反应,Eureka会注销该微服务实例.但如果出现网络故障的情况,心跳时间肯定会变慢,那上述的默认行为就变得危险了,因为微服务本身没有问题,不能因为外部因素导致的心跳超时而注销该服务.
所以Eureka的自我保护机制便应运而生,一旦因外部因素产生心跳超时问题,Eureka不会立刻注销该微服务,而是进入一种自我保护机制.该机制思想属于CAP里的AP分支.
该自我保护机制是默认设置的.
这里以单机版为例.
如果要将集群版的zk中某个zk更改为单机版的,要在zoo.cfg中修改zk端口号为默认的2181.
启动zk服务和客户端.
在父工程引入一个依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zookeeper-discoveryartifactId>
<version>3.1.0version>
dependency>
新建一个springboot工程,然后引入依赖:
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.hyb.springcloudgroupId>
<artifactId>commons-cloud-apiartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zookeeper-discoveryartifactId>
<exclusions>
<exclusion>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.5.7version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
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这个请求.
这里以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.cloudgroupId>
<artifactId>spring-cloud-starter-consul-discoveryartifactId>
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定理?”在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定理:
在某时刻如果满足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定理证明中的一致性指强一致性,强一致性要求多节点组成的被调要能像单节点一样运作、操作具备原子性,数据在时间、时序上都有要求。如果放宽这些要求,还有其他一致性类型:
可用性在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理论有帮助。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
<version>2.2.10.RELEASEversion>
dependency>
Spring Cloud Ribbon 是基于NetFlix Ribbon 实现的一套客户端负载均衡工具,主要功能是提供负载均衡算法和服务调用.
负载均衡是什么?
简单来说,就是将用户的请求平摊到多个服务上,以减少单个服务的压力,实现系统的高可用.
Ribbon是客户端的负载均衡,Nginx是服务端的负载均衡.
Nginx是集中式的负载均衡(LB),即在消费方与提供方使用独立的LB设施.
Ribbon是进程内的负载均衡,是一个类库,集成于消费方进程,消费方通过他来获取到服务提供方的地址.
报错: No instances available for *** 检查了很多遍,什么都对,但是就是读取不到服务.
先导入:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
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;
}
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);
}
这个请求用到的实例是提供者一端,表示我们服务器有多少个,然后我们消费者进行主要的代码撰写,判断下一个要选择的提供者是哪个.
Feign 是一个声明式的WebService客户端,他让客户端调用的代码更加简洁,只是声明一个接口然后加上相应的注解即可.
<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.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
<version>3.1.0version>
dependency>
@Service
@FeignClient(value = "provider-pay")
public interface OpenFeignService {
@GetMapping("/u")
public String u();
}
该接口表示,我们要调用的客户端是provider-pay,然后调用其/u方法.
@RestController
@Slf4j
public class OpenFeignController {
@Autowired
OpenFeignService openFeignService;
@GetMapping("/uz")
public String uz(){
return openFeignService.u();
}
}
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ReadTimeout: 10000
#指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 10000
feign:
client:
config:
# 填服务名称,具体到某个服务,填default,表示所有服务都按照此设置
provider-pay:
connectTimeout: 10000
readTimeout: 10000
@Configuration
public class OpenFeignConfig {
@Bean
public Request.Options options(){
return new Request.Options(10000, TimeUnit.MILLISECONDS,10000,TimeUnit.MILLISECONDS,true);
}
}
如何测试? 可以尝试在被调用的提供者一方的具体方法,用Thread.sleep的方式暂停几秒.
比如: 我们可以设置沉睡的时间比超时时间还长,就说明远远大于超时时间,就一定超时,这个时候就会报错:
@Bean
public Logger.Level level(){
return Logger.Level.FULL;
}
logging:
level:
# feign日志以什么级别监控哪个接口
com.hyb.springcloud.service.OpenFeignService: debug
此项目已经停更
Hystrix 最主要的功能是服务降级(fallback),服务熔断(break),服务限流(flowlimit)
当服务器出现故障和延迟等状态,不让客户端等待,立即返回一个结果.程序运行异常,超时,服务熔断触发服务降级,线程池/信号量打满都会出现服务降级.
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
<version>2.2.10.RELEASEversion>
dependency>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.hyb.springcloudgroupId>
<artifactId>commons-cloud-apiartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
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地址
public String Normal(){
return "这是一个正常的方法!";
}
public String InNormal(){
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "这是一个不正常的方法!";
}
@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 ,会报算数错误,该错误处理方法也会生效.
@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....";
}
}
@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);
}
可见,服务熔断也是服务降级,只是有些特殊.如果你测试的时候会发现,当我们一直传入负数,传出的请求错误次数超过百分之六十,就会开启熔断机制,这个时候不仅是负数请求都会走回调函数,正数请求也会走回调函数,只有连续多几次的正数请求后,正数的链路才会慢慢恢复.
熔断类型:
断路器在什么时候开始启动?
断路打开之后:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboardartifactId>
<version>2.2.10.RELEASEversion>
dependency>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboardartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
/**
*此配置是为了服务监控而配置,与服务容错本身无关,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;
}
秒杀高并发操作,对某段时间内的请求量进行限制,剩下的进行排队,严禁超过限制的流量峰值.
能做什么?反向代理,鉴权,流量控制,熔断,日志监控…
SpringCloud 和Zuul1.x的区别:
核心概念:
工作流程:
核心逻辑: 路由转发+执行过滤链
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
<version>3.1.0version>
dependency>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
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
@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
在routes里有一个配置项为predicates,该配置项是uri配置项的一个条件:
比如第一次After,表示在某个时间后路由才生效,Before表示在某个时间前路由才生效,Between表示在某个时间段内路由才生效,而Cookie则表示要带上某段Cookie才能完成.
例如:
predicates:
- Cookie=username,zzyy #表示地址必须加上该Cookie才能访问

可以发现,如果我们直接访问,便会报错,但如果我们:
带上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 Server中,让大家都从这里拿.
能干嘛?
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-config-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-autoconfigureartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-autoconfigureartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-autoconfigureartifactId>
dependency>
dependencies>
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错误.
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bootstrapartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-configartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
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
@RestController
public class ConfigClientController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/s")
public String s(){
return serverPort;
}
}
如果你想要覆盖外部化配置文件的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文件的值,我们在客户端这边还要重启一遍机器才能获取最新的值,这样太过于麻烦,我们希望通过不重启服务器的方式就能实时刷新获取最新数据.
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
前面两张图片是两种消息通知的设计模式,第二张设计地比较合理一些,第一张设计地不适合,它将通知分给了客户端,增加了这些客户端的职责,而且,这些客户端可能是一个集群,增加了配置的负担,若是客户端发生了迁移,要修改的地方有很多,如果将消息放在统一配置中心,能简化客户端的职责,更加方便,客户端只负责接收消息即可.
通过前面的分析,我们希望通过刷新Config Server端来实现Config Client端的刷新.这样子,无论我们有多少个Config Client,都可以直接刷新Server端而不用刷新各自Client端.
ConfigServer端:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
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:
# 暴露监控端点
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 Client+其端口号,但本人自测不成功.
本人看到了这句话,于是就萌生了将/busrefresh/ConfigClient:*/** 结合去尝试,结果发现,这样就能成功了.该方式表示将该客户端下所有端口号的服务都刷新.就很离谱.
在开发中,消息处理我们可能使用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便提供了链路跟踪机制,通过链路跟踪,我们可以获取一个请求经过的链路信息,方便我们管理模块和维护.
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
<version>2.2.8.RELEASEversion>
dependency>
spring:
zipkin:
base-url: http://localhost:9411/ #默认不写也行
sleuth:
sampler:
# 采集信息率,一般介于0到1之间,1表示全采集
probability: 1
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.2.7.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
nacos可替代Eureka做服务注册中心
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
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;
}
}
@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();
}
nacos可替代config做服务配置中心
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bootstrapartifactId>
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具有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搭建.
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下的数据库.
单台机器的启动:sh startup.sh -m standalone
使用内置数据源集群的启动:sh startup.sh -p embedded
使用mysql数据源的启动:sh startup.sh
都启动完成后,可登录任意一台机器,查看节点:
yum install -y yum-utils
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
yum install -y openresty
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标志的.
Sentinel主要是用来解决服务雪崩等问题.
官方: 因服务提供者的不可用导致服务调用者的不可用,并将不可用逐渐放大的过程称为服务雪崩.
简单来说:
比如上图,首先是1调用共享,然后调用4,如果共享和4之间挂掉了,造成1开始的链路请求堵塞,在共享中积压,然后这个时候,2,3等微服务进来,因为4挂掉了,所以这个时候也造成请求积压,一旦共享因为积压请求挂掉了,就会造成1,2,3的危险雪崩.
所以,如果没有一个很好的容错机制的话,就会造成雪崩效应,有了容错机制,就能很好提高系统的可用性.
比如在服务熔断期间,必须要有一个兜底的方案,因为这个时候请求是断开的,所以我们不可能返回断开信息给用户,肯定得有一个提示兜底的方案去处理.
服务降级一般发生在弱依赖关系中,就是某个断开的,不会产生太大影响链路请求的微服务.
Sentinel 是面向分布式服务架构的流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统自适应保护等多个维度来帮助您保障微服务的稳定性。
官网:https://sentinelguard.io/zh-cn/docs/introduction.html
这是一个Sentinel监控的管理平台
只需要在官网下载对应版本的jar包,这里本人使用1.7的.
下载jar包,用java -jar运行该jar包,成功后,查看浏览端口,1.7的为localhost:8080
登录账号和密码都是sentinel.
登录后,可以做一个小的测试,新建一个微服务,要导入sentinel-starter的jar.:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
让该服务注册到nacos中,被sentinel监控:
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.188.100:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719 #表示与该微服务交互的端口,默认,可以不写8080是其sentinel控制台的端口
测试: 需要进行一次请求,sentinel的管控平台才会有流量显示,需要请求几次,实时监控才会刷新出趋势图.
这个时候,表示一秒内的最高流量限制为1.
不然会报Blocked by Sentinel (flow limiting) 错误.

测试: 在请求的代码返回值之前,延迟几秒钟,然后在浏览器请求的时候,还未返回结果之前,重新发送请求,这个时候,表示还未返回结果,又一个线程进来了,就会报错.
该模式的意思是,如果关联资源的每秒请求的数量超过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断路器没有半开的状态,半开状态是HyStrix的概念,一旦服务熔断,系统会自动监测是否有异常,有异常则不修复断路器,没异常则尝试进行修复请求.
如果一秒内打进来的请求超过5个,要求每个请求都100毫秒内处理完,如果不行,则进行熔断,熔断时间为1秒,一秒后尝试恢复.(注意: 如果打进来的请求不超过5个,则不会熔断,这是1.7版本的默认值)我们将这些在一个时间内不能完成的请求称为慢调用.
测试: 让被调用的方法沉睡十秒,然后不断点击地址栏左边的圈圈,表示一时间多个请求进来,之后会发现出现了错误,然后等待一秒,又恢复了.
一秒请求必须大于等于5个才会触发.
异常比例为,一秒内请求失败的数量占一秒内总请求数量,如果出现超过20%的异常,则熔断,熔断一秒.
测试: 给被调用的方法写入一个算术异常,因为每次都调用这个方法,所以每次都有算术异常,这个时候,异常比例远远超出预设置的,所以在我们不断刷新地址栏旁边圈圈的时候,就会降级.如果你单独访问一次,就是一秒没超过五次请求,肯定不会降级.
热点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参数的高级选项,有参数例外项,可以为带有某个值的参数添加额外的限流.
这里的设置意思为,对第0个参数设置限制一秒内为1请求量,但当第0个参数值为5时,限流阈值为200.
注意: 如果此刻,我们在上述被调用的方法中写一句算术异常的代码,该错误出现会覆盖热点key带来的错误,因为热点key只能处理平台设置的热点key限流,而不能处理java代码带来的异常,而又因为前者处理在先,后者进入方法才出现算术异常,所以会被覆盖.
系统规则是通过一系列的规则来达到对系统进行降级保护.
系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 和线程数四个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则支持以下的阈值类型:
该注解前面用到过,方便对被限流的方法更好的设置限流规则.
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类型变量.
Sentinel也可以结合OpenFeign使用,引入OpenFeign的依赖即可.
操作与OpenFeign一样,只不过要操作Sentinel的时候,指定feign.sentinel.enabled=true
前面我们进行流量控制和限流的时候,每启动一次服务器都要重新新建一个流控规则,并且这个流控规则是新的流控规则,不能保留原来的.Sentinel提供了将流控规则持久化到Nacos的方式,下一次重启服务器的时候,就可以直接从Nacos拉取流控规则到Sentinel里,且这个规则还是原来的.
[
{
"resource": "/echo1",
"limitApp": "default",
"grade": 1,
"count": 5,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
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
在传统的本地事务中,一般都只是一个系统一个数据库.但对于分布式系统中,尤其是在微服务思想架构下的系统,每个模块都有可能有自己的数据库和数据表,俗称分库分表,这个时候,每个数据库只能保证本地的数据统一性,不能做到全局事务,保证整体的统一性,因为各个微服务之间的数据库是隔离的.
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
其采用一加三的模式:一是全局事务ID XID,三是三个重要组件TC TM RM:
维护全局和分支事务的状态,驱动全局事务提交或回滚。
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
这里我学习Seata使用的版本是spring-cloud-alibaba 2.2.7:
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!
这些配置和刚才的数据库配置一致.
-- -------------------------------- 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数据库下执行.这个数据库是系统必要的数据库.
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;
库存的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>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
<version>1.4.2version>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<version>2.2.1.RELEASEversion>
<exclusions>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
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.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
<version>1.4.2version>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<version>2.2.1.RELEASEversion>
<exclusions>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
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注解标上去,理应上是只能将本方法的错误回滚的,但是这里我尝试了一下,其效果居然和全局的一样.
本人测试了很久,都是因为依赖的问题,各种问题.
首先,没有加任何事务注解,执行,即使发生错误,但因为有代码执行先后的问题,所以两个数据库的表都扣除了信息,这理论上是正常的,但是在实际的操作数据库的过程中,代码是不允许有错误出现的,所以只能回滚.这里本人使用本地事务也能回滚,进行过很多次测试,发现结果都和全局事务注解的效果一样,不知道为什么.