个人学习进度:视频跟敲+笔记(12天)
学习视频:尚硅谷微服务速通(7小时左右课程)
资源:
1.pdf:微服务pdf(课程):https://pan.baidu.com/s/1g_TAuBjQ9Dw6WU7b7lwvCA?pwd=yyds
2.语雀笔记(尚硅谷速通):尚硅谷 · 语雀
3.nacos面板地址:
- http://192.168.43.71:8848/nacos/index.html
alt+enter:快捷键的使用(智能提示!!!)
测试接口:http://localhost:8000/create?userId=1&productId=100
一.微服务概念(名词了解)
SpringCloud微服务架构中,常见的概念和名词通过具体的技术栈得以实现和解决实际问题。
微服务架构风格,就像是把一个单独的应用程序开发为一套小服务,每个小服务运行在自己的进程中,并使用轻量级机制通信,通常是 HTTP API。
简而言之:拒绝大型单体应用,基于业务边界进行服务微化拆分,各个服务独立部署运行。
1.微服务架构
微服务架构是将应用程序分解为一组小型、独立服务的开发方法。在SpringCloud中,每个服务可以独立开发、部署和扩展,它们通过轻量级通信协议(如HTTP RESTful API)进行交互。
场景理解:微服务架构是一种将应用程序分解为一组小型、独立服务的开发方法。每个服务专注于特定的业务功能,并且可以独立开发、部署和扩展。就像一家大型餐厅,有负责烹饪的厨师团队(后端服务)、负责接待客人的服务员团队(前端服务)、负责食材采购的采购团队(数据访问服务)等,每个团队独立运作但又相互协作,共同为顾客(用户)提供完整的用餐体验(应用程序功能)。
实现与解决:通过SpringBoot可以快速创建独立运行的服务,SpringCloud则提供了服务之间的通信、发现等基础设施支持,使得服务能够相互协作完成复杂的业务功能。
Spring Cloud 系列:
- 官网:Spring Cloud
- 远程调用:OpenFeign
- 网关:Gateway
Spring Cloud Alibaba 系列:
- 官网:https://sca.aliyun.com/
- 注册中心/配置中心:Nacos
- 服务保护:Sentinel
- 分布式事务:Seata
2.服务注册与发现
是微服务架构中用于服务实例注册、存储及查询定位,以便服务间通信协作的关键机制。
3.配置中心
集中管理应用配置信息,支持动态调整配置,实现配置与代码分离,便于统一管理。
4.API网关
作为微服务架构的统一入口,负责路由、鉴权、限流等任务,简化服务调用。
5.服务熔断器
当服务调用失败率高时,熔断依赖服务调用链路,避免故障扩散,保障系统稳定。
6.服务降级
在系统故障或资源不足时,优先保障核心业务,降低非核心服务的质量或可用性。
7.分布式事务
确保分布式系统中多个服务的一系列操作要么全部成功,要么全部回滚,维持数据一致性。
8.服务监控与追踪
实时监控微服务运行状态和性能指标,追踪服务调用链路,便于及时发现和解决问题。
9. 实际示例(步骤)
假设我们要构建一个电商系统,包含用户服务、订单服务和库存服务:
1. 服务注册与发现:每个服务在启动时向Nacos注册,其他服务通过Nacos发现并调用。
2. 配置管理:所有服务的配置信息(如数据库连接)存储在Nacos中,服务启动时从Nacos获取配置。
3. API网关:使用SpringCloud Gateway作为系统入口,处理用户请求的路由、认证和限流。
4. 服务熔断与降级:在订单服务调用支付服务时,若支付服务出现故障,使用Resilience4j熔断调用,并返回降级提示给用户。
5. 分布式事务:在下单流程中,涉及订单服务、库存服务和支付服务的数据库操作,使用Seata保证分布式事务的一致性。
6. 服务监控与追踪:通过SpringCloud Sleuth和Zipkin记录服务调用链路和性能指标,便于后续分析和优化。
10.微服务配图
二.Nacos配置(注册中心)
1.项目搭建
第一步:下载nacos
- 下载软件包:📎nacos-server-2.4.3.zip
- 启动控制面板:
startup.cmd -m standalone
- 打开地址:http://localhost:8848/nacos/index.html
第二步:创建SpringBoot项目
SpringBoot版本:3.3.4
项目名称:cloud,根pom如下:
<?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><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.3.4</version><relativePath/> <!-- lookup parent from repository --></parent><modules><module>services</module></modules><modelVersion>4.0.0</modelVersion><groupId>com.weiyan</groupId><artifactId>spring-cloud-demo</artifactId><version>1.0-SNAPSHOT</version><packaging>pom</packaging><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><spring-cloud.version>2023.0.3</spring-cloud.version><spring-cloud-alibaba.version>2023.0.3.2</spring-cloud-alibaba.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>${spring-cloud-alibaba.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement></project>
创建services:
下面有两个子模块:service-order(订单服务)和service-product(商品服务)
第三步:引入公共依赖
services的pom中引入公共依赖,刷新:
<!-- 导入公共依赖--><dependencies>
<!-- 服务发现--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>
<!-- 远程调用--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency></dependencies>
services中的两个子模块分别引入web和test依赖:
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency></dependencies>
maven如下所示,按组查看:
第四步:编写配置文件和主类
配置文件名:application.properties
# 服务名称
spring.application.name=service-order
# 服务端口
server.port=8000
# nacos注册中心地址
spring.cloud.nacos.server-addr=127.0.0.1:8848
主类:@SpringBootApplication注解
@SpringBootApplication
public class OrderMainApplication {public static void main(String[] args) {SpringApplication.run(OrderMainApplication.class, args);}
}
第五步:查看服务启动
启动其中一个服务后,查看nacos的控制面板:
- http://192.168.43.71:8848/nacos/index.html
先打开nacos的目录文件,bin目录下,cmd命令弹窗,启动:startup.cmd -m standalone
如下所示:
OK,项目搭建完成
2.demo1(集群服务)
第一步:打开services
右击鼠标,赋值一个新的服务,需要修改配置
第二步:修改配置和端口
项目名称修改,可以在后面加上数字,如:-2
需要修改选项(Modify Option):Program argument
输入新的端口号:(自定义)
--server.port=8001
第三步:全选执行
ctrl+鼠标全选服务,run,运行:
运行成功,打开nacos面板(期间弹窗不关闭):http://localhost:8848/nacos/index.html
第四步:添加主类服务注解(必须的)
@EnableDiscoveryClient // 开启服务注册发现功能,需要重新启动服务
@SpringBootApplication
public class ProductMainApplication {public static void main(String[] args) {SpringApplication.run(ProductMainApplication.class, args);}
}
开启服务注册发现功能后,可以运行下面测试类:ServiceInstance为服务实例
@SpringBootTest
public class DiscoveryTest {//标准注册中心api(通用)@AutowiredDiscoveryClient discoveryClient;//nacos注册中心api@AutowiredNacosServiceDiscovery nacosServiceDiscovery;@Testvoid nacosServiceDiscoveryTest() throws NacosException {for (String service : nacosServiceDiscovery.getServices()) {//微服务名称System.out.println("service = " + service);//获取ip+port(每个微服务名称下有几个端口)List<ServiceInstance> instances = nacosServiceDiscovery.getInstances(service);for (ServiceInstance instance : instances) {//主机地址+端口号System.out.println("ip:"+instance.getHost()+";"+"port = " + instance.getPort());}}}@Testvoid discoveryClientTest(){for (String service : discoveryClient.getServices()) {//微服务名称System.out.println("service = " + service);//获取ip+port(每个微服务名称下有几个端口)List<ServiceInstance> instances = discoveryClient.getInstances(service);for (ServiceInstance instance : instances) {//主机地址+端口号System.out.println("ip:"+instance.getHost()+";"+"port = " + instance.getPort());}}}
}
运行结果如下:
3.demo2(远程调用)
功能实现:(简单示例)
1.一个订单下有多个商品,这里以一个商品为例
2.订单远程调用商品模块数据,计算的出总金额
3.远程查询订单商品列表
数据公共模块(model)
所有微服务的数据模型要公共使用,创建一个公共模块model,数据在bean目录下:
//可以使用@Data注解
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>annotationProcessor</scope>
</dependency>
@Data
public class Order {private Long id;private BigDecimal totalAmount;private Long userId;private String nickName;private String address;private List<Object> productList;
}
@Data
public class Product {private Long id;private BigDecimal price;private String productName;private int num;
}
在services的模块下的pom文件中;导入model模块的使用:
<dependency><groupId>com.weiyan</groupId><artifactId>model</artifactId><version>1.0-SNAPSHOT</version>
</dependency>
业务代码实现:
配置文件config:
RestTemplate
是 Spring 框架中提供的一个用于简化 HTTP 请求和处理响应的工具类。[远程调用]
@Configuration
public class ProductServiceConfig {@Beanpublic RestTemplate restTemplate() {return new RestTemplate();}
}
@Configuration
public class OrderServiceConfig {@Beanpublic RestTemplate restTemplate() {return new RestTemplate();}
}
service接口层
public interface ProductService{Product getProductById(Long productId);
}
public interface OrderService {// 根据用户id和商品id,创建购物清单Order cresteOrder(Long productId,Long userId);
}
sevice的实现层(impl)
@Service
public class ProductServiceImpl implements ProductService {@Overridepublic Product getProductById(Long productId){Product product = new Product();product.setId(productId);//设置默认值product.setProductName("苹果-"+productId);product.setPrice(new BigDecimal("99"));product.setNum(2);return product;}
}
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {@AutowiredDiscoveryClient discoveryClient;@AutowiredRestTemplate restTemplate;@Overridepublic Order cresteOrder(Long productId, Long userId) {//从远程获取一个商品数据(示例:模拟只有一个商品)Product product = getProductFromRemote(productId);Order order = new Order();order.setId(1L);//总金额:商品的金额*商品购买的数量order.setTotalAmount( product.getPrice().multiply(new BigDecimal(product.getNum())));order.setAddress("北京");order.setNickName("张三");order.setUserId(userId);//远程查询商品列表order.setProductList(Arrays.asList(product));return order;}//获取远程调用的商品数据private Product getProductFromRemote(Long productId){//1.获取所有商品服务所在的所有机器IP+port(获取实例)List<ServiceInstance> instances = discoveryClient.getInstances("service-product");//2.获取实例,远程调用一个url地址ServiceInstance instance = instances.get(0);String url = "http://"+instance.getHost() + ":" + instance.getPort()+"/product/"+productId;log.info("远程请求:{}",url);//3.给远程发送请求RestTemplate(),线程安全的。url返回的json数据自动转换为class对象Product product = restTemplate.getForObject(url, Product.class);return product;}
}
Controller层
@RestController
public class ProductController {@AutowiredProductService productService;//根据商品id,查询商品@GetMapping("/product/{id}")public Product getProduct(@PathVariable("id") Long productId){Product product =productService.getProductById(productId);return product;}
}
@RestController
public class OrderController {@AutowiredOrderService orderService;//创建订单,商品id+用户id@GetMapping("/create")public Order createOrder(@RequestParam("productId") Long productId,@RequestParam("userId") Long userId){Order order =orderService.cresteOrder(productId,userId);return order;}
}
测试结果:
这里默认设置商品数量为2,价格99,id和商品名称动态变化
商品服务:http://localhost:9002/product/4
订单服务:http://localhost:8000/create?userId=2&productId=100
{"id": 4,"price": 99,"productName": "苹果-4","num": 2
}
{"id": 1,"totalAmount": 198,"userId": 2,"nickName": "张三","address": "北京","productList": [{"id": 100,"price": 99,"productName": "苹果-100","num": 2}]
}
日志信息:
@Slf4j注释,使用{}占位符(使用了):log.info("远程请求:{}", url)
示例:log.info("用户 {} 访问了 {} 页面", userId, pageUrl);
结果如下:
停掉其中一个服务后,依旧可以运行:(远程请求端口变了)
缺陷:每次固定发给第一个服务端口,除非宕机了
改进:实现负载均衡,每次请求分别发给不同的服务端口(均衡)
4.demo3(负载均衡)
方法一:choose方法
依赖导入:
<!-- 负载均衡 -->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--测试 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
</dependency>
测试类:
@SpringBootTest
public class LoadBalancerTest {@AutowiredLoadBalancerClient loadBalancerClient;@Testvoid test() {ServiceInstance instance = loadBalancerClient.choose("service-product");System.out.println("choose="+instance.getHost() + ":" + instance.getPort());instance = loadBalancerClient.choose("service-product");System.out.println("choose="+instance.getHost() + ":" + instance.getPort());instance = loadBalancerClient.choose("service-product");System.out.println("choose="+instance.getHost() + ":" + instance.getPort());instance = loadBalancerClient.choose("service-product");System.out.println("choose="+instance.getHost() + ":" + instance.getPort());}
}
改进方法:
//上面引入@AutowiredLoadBalancerClient loadBalancerClient;
//负载均衡获取远程调用的商品数据private Product getProductFromRemoteWithLoadBalance(Long productId){ServiceInstance instance = loadBalancerClient.choose("service-product");String url = "http://"+instance.getHost() + ":" + instance.getPort()+"/product/"+productId;log.info("远程请求:{}",url);//3.给远程发送请求RestTemplate(),线程安全的。url返回的json数据自动转换为class对象Product product = restTemplate.getForObject(url, Product.class);return product;}
多次请求,刷新界面后,日志的远程请求地址变了:
方法二:注解实现
RestTemplate
是 Spring 框架中提供的一个用于简化 HTTP 请求和处理响应的工具类。
添加注解实现负载均衡: @LoadBalanced
@Configuration
public class OrderServiceConfig {@LoadBalanced //注解式实现负载均衡@Beanpublic RestTemplate restTemplate() {return new RestTemplate();}
}
private Product getProductFromRemoteWithLoadBalanceAnnotation(Long productId){//这里ip地址直接写成对方微服务的名字,会动态替换String url = "http://service-product/product/"+productId;log.info("远程请求:{}",url);//restTemplate在请求发送前:动态将service-product负载均衡的替换成一个IP地址Product product = restTemplate.getForObject(url, Product.class);return product;}
因为restTemplate动态替换了,也不知道想谁发送了请求,在商品控制层输出,hello打招呼:
这里8000日志输出,ip地址被隐藏了,动态替换。不知道向谁发出的远程请求。
@RestController
public class ProductController {@AutowiredProductService productService;//根据商品id,查询商品@GetMapping("/product/{id}")public Product getProduct(@PathVariable("id") Long productId){System.out.println("hello");Product product =productService.getProductById(productId);return product;}
}
一共远程请求了5次,9000和9001分别两次,9002请求一次。(如下所示)
5.配置中心
一.基本使用
依赖引入:
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
配置文件:
# 指定配置中心地址
spring.cloud.nacos.server-addr=localhost:8848
spring.config.import=nacos:service-order.properties
Naco控制面板,添加配置:
重启项目:controller层添加:
@Value("${order.timeout}")
String orderTimeout;
@Value("${order.auto-confirm}")
String orderAutoConfirm;//接口
@GetMapping("/config")
public String config(){return "order.timeout="+orderTimeout+" order.auto-confirm="+orderAutoConfirm;
}
运行项目,调用接口:
如果其他服务模块没有使用配置中心,添加配置:
# 禁用导入检查(配置中心)
spring.cloud.nacos.config.import-check.enabled=false
二.动态刷新
第一种方法:
controller层添加注解:@RefreshScope
修改配置:
接口数据页自动刷新:
第二种方法(推荐):
ConfigurationProperties
创建包:Properties,存放采用配置,统一管理
@Component
//配置批量绑定在nacos下,可以无需@RefreshScope就能实现自动刷新(前缀)
@ConfigurationProperties(prefix = "order")
@Data
public class OrderProperties {String timeout;String autoConfirm;
// String dbUrl;
}
修改控制层:
//@RefreshScope //自动刷新
@RestController
public class OrderController {@AutowiredOrderService orderService;// @Value("${order.timeout}")
// String orderTimeout;
// @Value("${order.auto-confirm}")
// String orderAutoConfirm;@AutowiredOrderProperties orderProperties;@GetMapping("/config")public String config(){return "order.timeout="+orderProperties.getTimeout()+"; " +"order.auto-confirm="+orderProperties.getAutoConfirm() +";";}//创建订单,商品id+用户id@GetMapping("/create")public Order createOrder(@RequestParam("productId") Long productId,@RequestParam("userId") Long userId){Order order =orderService.cresteOrder(productId,userId);return order;}
}
三.配置监听
启动类添加:
@BeanApplicationRunner applicationRunner(NacosConfigManager manager){return args -> {ConfigService configService = manager.getConfigService();configService.addListener("service-order.properties", "DEFAULT_GROUP", new Listener() {@Overridepublic Executor getExecutor() {return Executors.newFixedThreadPool(4);}@Overridepublic void receiveConfigInfo(String configInfo) {System.out.println("变化的配置:configInfo = " + configInfo);}});};}
四.数据隔离
用名称空间来区分多套环境:
首先,创建不同命名空间:(四个)
在不同命名空间下,设置配置,其中分组来区分微服务(每个组就是一个微服务):
order.timeout=1min
order.auto-confirm=1h
依次完成配置和操作,开始克隆:
server:port: 8000
spring:
# 激活环境profiles:active: devapplication:name: service-ordercloud:nacos:server-addr: 127.0.0.1:8848config:
# 导入检查禁用import-check:enabled: false# 动态取值,看当前激活环境,默认为devnamespace: ${spring.profiles.active:dev}#同一个文件中定义多个独立的配置块,使用---
---
# 导入不同空间下的order组的配置文件
spring:config:import:- nacos:common.properties?group=order- nacos:database.properties?group=orderactivate:on-profile: test---
spring:config:import:- nacos:common.properties?group=order- nacos:database.properties?group=orderactivate:on-profile: prod---
spring:config:import:- nacos:common.properties?group=order- nacos:database.properties?group=orderactivate:on-profile: dev
6.Nacos经典面试题
一.注册中心宕机
二.配置项优先级
以配置中心的数据为准,在项目中优先级低(规则如下:)
小结:
三.OpenFeign(远程调用)
1.基本介绍
OpenFeign 是一个声明式远程调用客户端;通过注解方式更加便捷。
上面使用的编程式远程调用请求:RestTemplate(自己手动);这里声明式:OpenFeign(注解)
2.基本使用
其中mvc注解两套使用逻辑:
1.标注在controller上,是接受这样的请求
2.标注在feignClient上,是发生这样的请求
实现功能:
1.引入依赖:
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.主类中添加注解:@EnableFeignClients //开启Feign远程调用功能
3.创建feign包,创建接口:自动连接注册中心,负载均衡的挑选一个
@FeignClient(value = "service-product") //feign客户端+远程微服务名
public interface ProductFeignClient {// 其中mvc注解两套使用逻辑:
// 1.标注在controller上,是接受这样的请求
// 2.标注在feignClient上,是发生这样的请求@GetMapping("/product/{id}")Product getProductById(@PathVariable("id") Long id);
}
4.修改OrderServiceimpl中代码:
//引入OpenFeignClient@AutowiredProductFeignClient productFeignClient;@Overridepublic Order cresteOrder(Long productId, Long userId) {//从远程获取一个商品数据(示例:模拟只有一个商品)// Product product = getProductFromRemoteWithLoadBalanceAnnotation(productId);// 改用openfeign注解远程调用Product product = productFeignClient.getProductById(productId);Order order = new Order();order.setId(1L);//总金额:商品的金额*商品购买的数量order.setTotalAmount( product.getPrice().multiply(new BigDecimal(product.getNum())));order.setAddress("北京");order.setNickName("张三");order.setUserId(userId);//远程查询商品列表order.setProductList(Arrays.asList(product));return order;}
5.测试结果:同样实现远程调用,而且实现了负载均衡使用
两次
一次
3.远程调用技巧
1.远程使用第三方api:指定url地址[墨迹天气为例]
2.调用业务api(自己 编写):直接复制对方的controller签名
4.进阶配置
一.开启日志
yml文件配置:
logging:level:# 监控日志包名(整个包),级别debug,记录详细记录com.atguigu.order.feign: debug
config容器中配置:feign下的logger
@Configuration
public class OrderServiceConfig {// 创建RestTemplate实例, 放到容器中并返回(线程安全)@LoadBalanced //注解式实现负载均衡@Beanpublic RestTemplate restTemplate() {return new RestTemplate();}//日志注解@BeanLogger.Level feignLoggerLevel() {return Logger.Level.FULL;}
}
二.超时控制
connectTimeout:连接超时
readTimeout:读取超时{业务处理时间}
默认配置如下所示:读取60s,连接10s
订单impl设置睡眠时间:目的,延迟创建订单的业务处理时间100s
//设置连接时间:100秒try{TimeUnit.SECONDS.sleep(100);}catch (InterruptedException e){throw new RuntimeException(e);}
调用接口,创建订单:http://localhost:8000/create?userId=1&productId=100
在超时前已一直加载转圈:
时间到了之后,返回500错误:读取超时
控制台输出:超时信息
三.超时配置
订单order的配置文件,添加一个application-feign.yml;并在yml中调用激活:
# 激活环境profiles:active: test
# 激活环境标识include: feign
spring:cloud:openfeign:client:config:# 客户端名称(其中default默认配置)default:logger-level: fullconnect-timeout: 1000read-timeout: 2000service-product:logger-level: fullconnect-timeout: 3000read-timeout: 5000
添加这个配置后,5s后就有返回结果:500
四.重试机制
@Bean
Retryer retryer(){return new Retryer.Default();
}
可以查看重试默认配置:5次,请求失败了也会重新请求,最多5次,每次间隔100ms
时间间隔=超时+延迟等待(100ms*1.5)
public Default() {this(100L, TimeUnit.SECONDS.toMillis(1L), 5);}
使用结果测试:http://localhost:8000/create?userId=1&productId=100
五.拦截器
应用:上游放共享数据,下游可以收到(全局有效)
@Component
public class XTokenRequestInterceptor implements RequestInterceptor {/*** 请求拦截器* @param requestTemplate 请求模版**/@Overridepublic void apply(RequestTemplate requestTemplate) {requestTemplate.header("X-Token", UUID.randomUUID().toString());}
}
控制层:测试输出
@RestController
public class ProductController {@AutowiredProductService productService;//根据商品id,查询商品@GetMapping("/product/{id}")public Product getProduct(@PathVariable("id") Long productId, HttpServletRequest request){String header = request.getHeader("X-Token");System.out.println("hello...token="+header);Product product =productService.getProductById(productId);return product;}
}
结果(先注释之前的超时控制):http://localhost:8000/create?userId=1&productId=100
六.Fallback兜底返回
引入依赖:sentinel
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency>
@FeignClient(value = "service-product",fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient {@GetMapping("/product/{id}")Product getProductById(@PathVariable("id") Long id);
}
@Component
public class ProductFeignClientFallback implements ProductFeignClient {@Overridepublic Product getProductById(Long id) {System.out.println("兜底回调....");Product product = new Product();product.setId(id);product.setPrice(new BigDecimal("0"));product.setProductName("未知商品");product.setNum(0);return product;}
}
注意:
1.注释掉上面的重传机制和超时设置
2.停掉商品的服务
3.开启熔断:
feign:sentinel:enabled: true
返回结果:
当商品服务开启时:
5.经典面试题
客服端负载均衡与服务端负载均衡的区别:
四.Sentinel(服务保护)
1.基本使用
常用功能:服务保护,限流,熔断降级
官网:home | Sentinel
wiki:https://github.com/alibaba/Sentinel/wiki
下载控制台:📎sentinel-dashboard-1.8.8.jar
启动命令:java -jar sentinel-dashboard-1.8.8.jar
访问面板:http://localhost:8080/#/login
1.1依赖和配置:
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
其中order的feign的yml配置
spring:cloud:sentinel:transport:dashboard: localhost:8080eager: true # 默认懒加载,这里改为一开始就启动
product的配置:
spring.cloud.sentinel.transport.dashboard=localhost:8080
spring.cloud.sentinel.eager=true
1.2面板和流控测试
测试接口:http://localhost:8000/create?userId=1&productId=100
添加注解:OrderServiceImpl下的创建方法添加:@SentinelResource(value = "createOrder")
测试:每秒创建一个,多了报错。
快速刷新请求,出现默认错误:
2.异常处理
2.1自定义BlockException异常
自定义返回的json对象:R
@Data
public class R {private Integer code;private String msg;private Object data;public static R ok() {R r = new R();r.setCode(200);return r;}public static R ok(String msg,Object data) {R r = new R();r.setCode(200);r.setMsg(msg);r.setData(data);return r;}public static R error() {R r = new R();r.setCode(500);return r;}public static R error(Integer code,String msg) {R r = new R();r.setCode(code);r.setMsg(msg);return r;}
}
编写自定义异常:
@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {private ObjectMapper objectMapper = new ObjectMapper();@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response,String resourceName, BlockException e) throws Exception {// response.setStatus(429); //too many requests,这里//这里把对象转换为json格式的范湖类型response.setContentType("application/json;charset=utf-8");PrintWriter writer = response.getWriter();R error = R.error(500, resourceName + " 被Sentinel限制了,原因:" + e.getClass());String json = objectMapper.writeValueAsString(error);writer.write(json);writer.flush();writer.close();}
}
重启项目,重新配置sentinel的限流,多次刷新后:设置create限流
若如果添加的createOrder限流,多次刷新,出现:
原因:只给资源起了名,没有标注blockhander和fallback,异常没人管,直接回调Springboot的异常
(设置全局异常可以处理)
2.2blockHandler(指定兜底回调)
对createOrder限流,多次刷新
@SentinelResource(value = "createOrder",blockHandler = "createOrderFallback")@Overridepublic Order createOrder(Long productId, Long userId) {
// Product product = getProductFromRemoteWithLoadBalanceAnnotation(productId);//使用Feign完成远程调用Product product = productFeignClient.getProductById(productId);Order order = new Order();order.setId(1L);// 总金额order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));order.setUserId(userId);order.setNickName("zhangsan");order.setAddress("尚硅谷");//远程查询商品列表order.setProductList(Arrays.asList(product));return order;}//兜底回调public Order createOrderFallback(Long productId, Long userId, BlockException e){Order order = new Order();order.setId(0L);order.setTotalAmount(new BigDecimal("0"));order.setUserId(userId);order.setNickName("未知用户");order.setAddress("异常信息:"+e.getClass());return order;}
显示流控异常:
2.3OpenFeign - 兜底回调
对GET:http://service-product/product/{id}添加流量控制
默认会调用之前写好的兜底回调:
3.流控规则
3.1 开启集群
示例:均摊阈值设置为5,每个集群有三个微服务。
单击均摊:表示每个微服务可以每秒5次请求
总体阈值:表示三个微服务1秒一共5次请求
3.2流控模式
(1)默认:直接+快速失败(如上)
(2)链路:控流B,限制C,示例:秒杀场景
spring:cloud:sentinel:transport:dashboard: localhost:8080eager: trueweb-context-unify: false #先关闭统一的上下文
秒杀接口:
@GetMapping("/kill")public Order killOrder(@RequestParam("productId") Long productId,@RequestParam("userId") Long userId){Order order =orderService.cresteOrder(productId,userId);order.setId(Long.MAX_VALUE);return order;}
这种方式,断开链路,仅对某一个链路生效
(3)关联模式:写优先策略
接口:
@GetMapping("/writeDb")public String writeDb(){return "writeDb";}@GetMapping("/readDb")public String readDb(){return "readDb";}
正常读接口没有问题,但当写接口1s内请求过多时,读接口被限制住了
开两个窗口测试:(系统内出现资源竞争时,使用关联策略)
3.3流控效果[直接模式]
3.3.1快速失败
orderController添加日志注解@SLf4j
@GetMapping("/writeDb")public String writeDb(){return "writeDb";}@GetMapping("/readDb")public String readDb(){log.info("readDb"); //日志打印return "readDb";}
// 异常处理首先设置状态码response.setStatus(429); //too many requests
重启项目,添加链路规则,限流1
使用apipost进行压力测试:并发数10次,进行5秒
设置后置断言,成功为200,失败设置了状态码为429
开始测试:
3.3.2 预热/冷启动
慢慢启动,直到到达峰值
这个设置表示,3秒内,逐渐达到峰值10,一开始默认为三分之一处,3个请求。
3.3.3均速排队
其其中QPS数量不能大于1000,默认单位为ms
队排不上,会被抛弃;底层使用漏桶算法。
4.熔断规则
熔断时间到了之后,半开状态,有探测数据进行探测
1.测试:修改商品服务接口,故意休眠2秒
//根据商品id,查询商品@GetMapping("/product/{id}")public Product getProduct(@PathVariable("id") Long productId, HttpServletRequest request){String header = request.getHeader("X-Token");System.out.println("hello...token="+header);Product product =productService.getProductById(productId);//休眠2秒try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);}return product;}
2.太慢了,会使用兜底回调,30s内熔断了。5s内先进行熔断检测,统计5s内的前五个请求的异常比例
3.可知,30s后会处于半开状态,当商品服务正常使用,不慢时,又可以正常使用了。(超时注释掉)
5.热点参数
可以限流对象,不再是资源级别,而是精细到参数级别,对于请求方法的某一个参数进行限制。
应用:
1.商城中的秒杀下单,用户id需要限制。[防止机器人,疯狂下单]
2.用户id为vip,不限流QPS
3商品666是下架商品,需要限流
需要,先自定义资源,不可以和请求方法重名:@SentinelResource注解
资源名称为seckill-order,对它进行热点规则限流
@GetMapping("/seckill")@SentinelResource(value = "seckill-order",fallback = "seckillFallback")public Order seckill(@RequestParam(value = "userId",required = false) Long userId,@RequestParam(value = "productId",defaultValue = "1000") Long productId){Order order = orderService.createOrder(productId, userId);order.setId(Long.MAX_VALUE);return order;}public Order seckillFallback(Long userId,Long productId, BlockException exception){System.out.println("seckillFallback....");Order order = new Order();order.setId(productId);order.setUserId(userId);order.setAddress("异常信息:"+exception.getClass());return order;}
测试:http://localhost:8000/seckill?userId=1&productId=100
1.设置热点规则,每个参数0[用户ID],无论id是几,每秒用户id只能创建一个订单
携带用户id限流空,不带用户id不限流
2.用户6为VIP,不限制QPS请求
3.商品666号为下架商品,不允许访问(限制第二个参数)
4.将上面的 BlockException改为Throwable
可以处理所有异常,使用兜底回调,返回兜底回调,而不是500报错界面
项目重启后,sentinel设置的规则都失效了。sentinel没有对这些规则实现持久化,需要结合nacos,配置好后,然后连接数据库中,实现规则的持久化。
五.GateWay(网关)
官网:Spring Cloud Gateway
1.基本配置(路由)
创建对应模块,引入依赖:
<dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>annotationProcessor</scope></dependency></dependencies>
配置文件application.yml:
spring:profiles:include: routeapplication:name: gatewaycloud:nacos:server-addr: 127.0.0.1:8848# localhost/api/order
server:port: 80
路由规则:
spring:cloud:gateway:routes:- id: order-routeuri: lb://service-orderpredicates:- Path=/api/order/**- id: order-routeuri: lb://service-productpredicates:- Path=/api/product/**
启动服务:
http://localhost/api/order/readDb
2.断言
名 | 参数(个数/类型) | 作用 |
After | 1/datetime | 在指定时间之后 |
Before | 1/datetime | 在指定时间之前 |
Between | 2/datetime | 在指定时间区间内 |
Cookie | 2/string,regexp | 包含cookie名且必须匹配指定值 |
Header | 2/string,regexp | 包含请求头且必须匹配指定值 |
Host | N/string | 请求host必须是指定枚举值 |
Method | N/string | 请求方式必须是指定枚举值 |
Path | 2/List<String>,bool | 请求路径满足规则,是否匹配最后的/ |
Query | 2/string,regexp | 包含指定请求参数 |
RemoteAddr | 1/List<String> | 请求来源于指定网络域(CIDR写法) |
Weight | 2/string,int | 按指定权重负载均衡 |
XForwardedRemoteAddr | 1/List<string> | 从X-Forwarded-For请求头中解析请求来源,并判断是否来源于指定网络域 |
测试:
spring:cloud:gateway:routes:- id: bing-routeuri: https://www.baidu.com/predicates:- name: Pathargs:patterns: /s- name: Queryargs:param: wdregexp: haha- name: Vipargs:param: uservalue: leifengyangorder: 10metadata:hello: world- id: order-routeuri: lb://service-orderpredicates:- name: Pathargs:patterns: /api/order/**matchTrailingSlash: trueorder: 1- id: product-routeuri: lb://service-productpredicates:- name: Pathargs:patterns: /api/product/**order: 1
编写一个自定义的路由断言工厂:
@Component
public class VipRoutePredicateFactory extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {public VipRoutePredicateFactory() {super(Config.class);}@Overridepublic Predicate<ServerWebExchange> apply(Config config) {return new GatewayPredicate() {@Overridepublic boolean test(ServerWebExchange serverWebExchange) {// localhost/s?wd=haha&user=leifengyangServerHttpRequest request = serverWebExchange.getRequest();String first = request.getQueryParams().getFirst(config.param);return StringUtils.hasText(first) && first.equals(config.value);}};}@Overridepublic List<String> shortcutFieldOrder() {return Arrays.asList("param", "value");}/*** 可以配置的参数*/@Validatedpublic static class Config {@NotEmptyprivate String param;@NotEmptyprivate String value;public @NotEmpty String getParam() {return param;}public void setParam(@NotEmpty String param) {this.param = param;}public @NotEmpty String getValue() {return value;}public void setValue(@NotEmpty String value) {this.value = value;}}
}
必须要指定参数和用户才可以!
3.过滤器
名 | 参数(个数/类型) | 作用 |
AddRequestHeader | 2/string | 添加请求头 |
AddRequestHeadersIfNotPresent | 1/List<string> | 如果没有则添加请求头,key:value方式 |
AddRequestParameter | 2/string、string | 添加请求参数 |
AddResponseHeader | 2/string、string | 添加响应头 |
CircuitBreaker | 1/string | 仅支持forward:/inCaseOfFailureUseThis方式进行熔断 |
CacheRequestBody | 1/string | 缓存请求体 |
DedupeResponseHeader | 1/string | 移除重复响应头,多个用空格分割 |
FallbackHeaders | 1/string | 设置Fallback头 |
JsonToGrpc | 请求体Json转为gRPC | |
LocalResponseCache | 2/string | 响应数据本地缓存 |
MapRequestHeader | 2/string | 把某个请求头名字变为另一个名字 |
ModifyRequestBody | 仅 Java 代码方式 | 修改请求体 |
ModifyResponseBody | 仅 Java 代码方式 | 修改响应体 |
PrefixPath | 1/string | 自动添加请求前缀路径 |
PreserveHostHeader | 0 | 保护Host头 |
RedirectTo | 3/string | 重定向到指定位置 |
RemoveJsonAttributesResponseBody | 1/string | 移除响应体中的某些Json字段,多个用,分割 |
RemoveRequestHeader | 1/string | 移除请求头 |
RemoveRequestParameter | 1/string | 移除请求参数 |
RemoveResponseHeader | 1/string | 移除响应头 |
RequestHeaderSize | 2/string | 设置请求大小,超出则响应431状态码 |
RequestRateLimiter | 1/string | 请求限流 |
RewriteLocationResponseHeader | 4/string | 重写Location响应头 |
RewritePath | 2/string | 路径重写 |
RewriteRequestParameter | 2/string | 请求参数重写 |
RewriteResponseHeader | 3/string | 响应头重写 |
SaveSession | 0 | session保存,配合spring-session框架 |
SecureHeaders | 0 | 安全头设置 |
SetPath | 1/string | 路径修改 |
SetRequestHeader | 2/string | 请求头修改 |
SetResponseHeader | 2/string | 响应头修改 |
SetStatus | 1/int | 设置响应状态码 |
StripPrefix | 1/int | 路径层级拆除 |
Retry | 7/string | 请求重试设置 |
RequestSize | 1/string | 请求大小限定 |
SetRequestHostHeader | 1/string | 设置Host请求头 |
TokenRelay | 1/string | OAuth2的token转发 |
1.网关会将原路径原封不动的转过去,在网关向下转的时候,进行路径重写。
2.使用过滤器,路径重写:(使用通义灵码解析,会有就行,正则表达式)
3.注释掉之前的路径,重启项目
spring:cloud:gateway:routes:- id: bing-routeuri: https://www.baidu.com/predicates:- name: Pathargs:patterns: /s- name: Queryargs:param: wdregexp: haha- name: Vipargs:param: uservalue: leifengyangorder: 10metadata:hello: world- id: order-routeuri: lb://service-orderpredicates:- name: Pathargs:patterns: /api/order/**matchTrailingSlash: truefilters: #将/api/order/ab/c 转换为/ab/c- RewritePath=/api/order/?(?<segment>.*), /$\{segment}- OnceToken=X-Response-Token, jwtorder: 1- id: product-routeuri: lb://service-productpredicates:- name: Pathargs:patterns: /api/product/**filters:- RewritePath=/api/product/?(?<segment>.*), /$\{segment}order: 1
4.配置默认过滤器
default-filters:- AddResponseHeader=X-Response-Abc, 123
5.全局过滤器
@Component
@Slf4j
public class RtGlobalFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest request = exchange.getRequest();ServerHttpResponse response = exchange.getResponse();String uri = request.getURI().toString();long start = System.currentTimeMillis();log.info("请求【{}】开始:时间:{}",uri,start);//========================以上是前置逻辑=========================Mono<Void> filter = chain.filter(exchange).doFinally((result)->{//=======================以下是后置逻辑=========================long end = System.currentTimeMillis();log.info("请求【{}】结束:时间:{},耗时:{}ms",uri,end,end-start);}); //放行 10sreturn filter;}@Overridepublic int getOrder() {return 0;}
}
4.微服务跨域(允许跨域)
spring:cloud:gateway:globalcors:cors-configurations:'[/**]':allowed-origin-patterns: '*'allowed-headers: '*'allowed-methods: '*'
六.Seata(分布式事务)---了解使用
1.基础环境
建表:
CREATE DATABASE IF NOT EXISTS `storage_db`;
USE `storage_db`;
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;
INSERT INTO storage_tbl (commodity_code, count) VALUES ('P0001', 100);
INSERT INTO storage_tbl (commodity_code, count) VALUES ('B1234', 10);-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
DROP TABLE IF EXISTS `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;CREATE DATABASE IF NOT EXISTS `order_db`;
USE `order_db`;
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;
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
DROP TABLE IF EXISTS `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;CREATE DATABASE IF NOT EXISTS `account_db`;
USE `account_db`;
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;
INSERT INTO account_tbl (user_id, money) VALUES ('1', 10000);
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
DROP TABLE IF EXISTS `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;
接口测试:
2.远程链路
- 下载seata
📎apache-seata-2.1.0-incubating-bin.tar.gz
- 解压并启动:
seata-server.bat
http://127.0.0.1:7091
导入依赖:
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
编写file.conf文件:
service {#transaction service group mappingvgroupMapping.default_tx_group = "default"#only support when registry.type=file, please don't set multiple addressesdefault.grouplist = "127.0.0.1:8091"#degrade, current not supportenableDegrade = false#disable seatadisableGlobalTransaction = false
}
添加全局事务注解:@GlobalTransactional