微服务架构
微服务
服务架构演变过程
单体应用架构:所有的功能都在一个项目中
集群架构:把一个单体项目部署多个,使用nginx进行负载均衡,根据负载均衡策略调用后端服务
缺点:有的服务访问量大,有的服务访问量小,这样不管访问量大小都会进行多次部署.
垂直架构:将不同的功能模块进行拆分,服务之间可以相互调用,还可以根据访问量大小进行选择性的多次部署.
缺点:服务之间的管理调用比较麻烦
微服务架构:一套完整的,对多个服务进行管理的解决方案.
服务治理(管理这么多服务)
服务调用
服务网关(对外提供一个入口)
服务容错
链路追踪
常见的微服务解决方案: 原生的SpringCloud
本次介绍SpringCloud alibaba,是阿里巴巴开源的一套微服务解决方案
微服务案例
以电商为例,主要服务有:订单服务,商品服务,用户服务.
以下订单为例,在订单服务中调用商品服务和用户服务.
简单演示服务调用
restTemplate.getForObject(url,类.class)
这种方式的问题:
1.一旦服务提供者地址变化,就需要手工修改代码
2.一箪食多个服务提供者,无法实现负载均衡功能
3.一旦服务变得越来越多,人工维护调用关系困难
服务管理
服务注册中心,将微服务中的多个服务管理起来
常见的注册中心有:
ZooKeeper: zookeeper 是一个分布式服务框架,是Apache 的一个子项 目,它主要是用来解决分布式 应用中经常遇到的一些数据管理问题
EureKa: Eureka是Spring Cloud 微服务框架默认的也是推荐的服务注册中 心,由Netflix 公司与2012将其开源出来,主要作用就是做服务注册和发现。但 是现在已经闭源,主要面向分布式,服务化的系统提供服务注册、服务发现 和 配置管理的功能。
nacos:Nacos 是阿里巴巴最新开源的项目,是一个更易于构建云原生应用 的动态服务发现、配置管理和服务管理平台。它是 SpringCloudAlibaba 组 件之一,负责服务注册发现和服务配置
安装nacos
第1步:下载地址: https://github.com/alibaba/nacos/releases
第2步: 启动nacos #切换目录 cd nacos/bin
#命令启动 startup.cmd -m standalone
第3步: 访问nacos 打开浏览器输入http://localhost:8848/nacos,即可访问服务, 默认密码是 nacos/nacos
将商品微服务注册到nacos
接下来开始修改shop-user模块的代码,将其注册到nacos服务上
1在pom.xml中添加nacos的依赖
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2在启动类上添加@EnableDiscoveryClient注解
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.ffyc.springcloudshop.dao")
publicclassShopUserApplication{
publicstaticvoidmain(String[]args){
SpringApplication.run(ShopUserApplication.class);
}
}
3在application.yml中为每个微服务定义服务名,并添加nacos服务的地址
spring:
application:
name: service-user #服务名
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 #nacos地址
4启动服务,观察nacos的控制面板中是否有注册上来的商品微服务
服务调用
1.原始调用
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@LoadBalanced
@Bean
public RestTemplate createRestTempalte(){
return new RestTemplate();
}
}
@Autowired
RestTemplate restTemplate;
@RequestMapping("/create/{pid}/{uid}/{num}")
public Order createOrder(@PathVariable("pid") int pid, @PathVariable("uid")int uid,@PathVariable("num") int num){
Product product =restTemplate.getForObject("http://127.0.0.1:8091/product/get/"+pid,Product.class);
User user=restTemplate.getForObject("http://127.0.0.1:8081/user/get/"+uid,User.class);
//查询商品,查询用户
Order order=null;
if(product!=null&&num<product.getStock()&&user!=null){
order= orderService.saveorder(pid,uid,num);
}
return order;
}
弊端:ip,端口写死,如果有多个服务,使用不方便,没有用到服务中心
2.使用nacos客户端根据服务名动态获取服务地址和端口
使用nacos提供的客户端 DiscoveryClient,动态的从注册中心通过服务名获取服务.
@Autowired
DiscoveryClient discoveryClient;
@RequestMapping("/create/{pid}/{uid}/{num}")
public Order createOrder(@PathVariable("pid") int pid, @PathVariable("uid")int uid,@PathVariable("num") int num){
//通过服务名,从注册中心找到对应服务器名的服务(一个服务下面,可能会有多个服务
List<ServiceInstance> instances=discoveryClient.getInstances("service-product");
//从服务中随机获取一个服务
ServiceInstance productservice=instances.get(new Random().nextInt(instances.size()));
String producturl=productservice.getHost()+":"+productservice.getPort();
Product product=restTemplate.getForObject("http://"+producturl+"/product/get/"+pid,Product.class);
List<ServiceInstance> instances1 = discoveryClient.getInstances("service-user");
ServiceInstance userservice=instances1.get(new Random().nextInt(instances1.size()));
String userurl=userservice.getHost()+":"+userservice.getPort();
User user=restTemplate.getForObject("http://"+userurl+"/user/get/"+uid,User.class);
Order order=null;
if(user!=null&&product!=null&&product.getStock()>num){
order=orderService.saveorder(pid,uid,num);
}
return order ;
}
3.Ribbon组件实现服务调用
负载均衡
什么是负载均衡 ?
通俗的讲,负载均衡就是将负载(工作任务,访问请求)进行分摊到多个 操作单元(服务器,组件)上进行执行。
@RequestMapping("/create/{pid}/{uid}/{num}")
public Order createOrder(@PathVariable("pid") int pid, @PathVariable("uid")int uid,@PathVariable("num") int num){
Product product = restTemplate.getForObject("http://service-product/product/get/" + pid, Product.class);
User user=restTemplate.getForObject("http://service-user/product/get/"+uid,User.class);
Order order=null;
if(product!=null&&product.getStock()<num&&user!=null){
order=orderService.saveorder(pid,uid,num);
}
return order;
}
自定义实现负载均衡
通过修改端口启动两个商品服务
可以将获取服务的方式改为随机获取
//获取服务列表
List<ServiceInstance>instances =
discoveryClient.getInstances("service-product");
//随机生成索引
Integer index = new Random().nextInt(instances.size());
//获取服务
ServiceInstance productService= instances.get(index);
//获取服务地址
String purl = productService.getHost() + ":" +
productService.getPort();
基于Ribbon实现负载均衡
Ribbon是SpringCloud的一个组件,它可以让我们使用一个注解就能轻松的 搞定负载均衡
第1步:在RestTemplate的生成方法上添加@LoadBalanced注解
第2步:修改服务调用的方法 restTemplate.getForObject(“http://服务名/product/get/”+pid,Product.class); Ribbon支持的负载均衡策略Ribbon内置了多种负载均衡策略,内部负载均衡 的顶级接口为com.netflix.loadbalancer.IRule
ribbon:
ConnectTimeout: 2000 # 请求连接的超时时间
ReadTimeout: 5000 # 请求处理的超时时间
service-product: # 调用的提供者的名称
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
@RequestMapping("/create/{pid}/{uid}/{num}")
public Order createOrder(@PathVariable("pid") int pid, @PathVariable("uid")int uid,@PathVariable("num") int num){
//基于ribbon组件实现负载均衡
Product product = restTemplate.getForObject("http://service-product/product/get/" + pid, Product.class);
User user=restTemplate.getForObject("http://service-user/product/get/"+uid,User.class);
Order order=null;
if(product!=null&&product.getStock()<num&&user!=null){
order=orderService.saveorder(pid,uid,num);
}
return order;
}
七种负载均衡策略
1.轮询策略:RoundRobinRule,按照一定的顺序依次调用服务实例。比如一 共有3个服务,第一次调用服务1,第二次调用服务2,第三次调用服务3, 依次类推。
NFLoadBalancerRuleClassName:com.netflix.loadbalancer.RoundRobinRule
2.权重策略:WeightedResponseTimeRule,根据每个服务提供者的响应时 间分配一个权重,响应时间越长,权重越小,被选中的可能性也就越低。它的实 现原理是,刚开始使用轮询策略并开启一个计时器,每一段时间收集一次所有服 务提供者的平均响应时间,然后再给每个服务提供者附上一个权重,权重越高被 选中的概率也越大。
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule
3.随机策略:RandomRule,从服务提供者的列表中随机选择一个服务实例。
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
**4.最小连接数策略:**BestAvailableRule,也叫最小并发数策略,它是遍历服 务提供者列表,选取连接数最小的⼀个服务实例。如果有相同的最小连接数,那 么会调用轮询策略进行选取。
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.BestAvailableRule
5.可用敏感性策略:AvailabilityFilteringRule,先过滤掉非健康的服务实例, 然后再选择连接数较小的服务实例。
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.AvailabilityFilteringRule
6.区域敏感策略:ZoneAvoidanceRule,根据服务所在区域(zone)的性能和 服务的可用性来选择服务实例,在没有区域的环境下,该策略和轮询策略类似。
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.ZoneAvoidanceRule
4.基于Feign实现服务调用
什么是Feign Feign?
是Spring Cloud提供的一个声明式的伪Http客户端, 它使得调用远程服务 就像调用本地服务 一样简单, 只需要创建一个接口并添加一个注解即可。 Nacos 很好的兼容了Feign, Feign默认集成了 Ribbon, 所以在Nacos下使用Fegin默认就实现了负 载均衡的效果。
使用Feign组件,可以将访问地址与接口绑定,这样我们可以像调用本地服务一样,调用其他服务
1.加入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.在启动类上添加Fegin的注解 @EnableFeignClients//开启Fegin
@SpringBootApplication
@MapperScan("com.ffyc.springcloudshop.dao")
@EnableDiscoveryClient
@EnableFeignClients//开启Fegin
public class ShopOrderApplication {
public static void main(String[] args) {
SpringApplication.run(ShopOrderApplication.class);
}
}
3.创建一个ProductService接口,并使用Fegin实现微服务调用
@FeignClient(value = "service-product")
public interface ProductService {
@GetMapping("/product/get/{pid}")
Product findProductByid(@PathVariable("pid") int pid);
}
@FeignClient(value = "service-user")
public interface UserService {
@GetMapping("/user/get/{uid}")
User findUserByid(@PathVariable("uid") int uid);
}
4.修改controller代码,并启动验证
@Autowired
ProductService productService;
@Autowired
UserService userService;
@RequestMapping("/create/{pid}/{uid}/{num}")
public Order createOrder(@PathVariable("pid") int pid, @PathVariable("uid")int uid,@PathVariable("num") int num){
Product product=productService.findProductByid(pid);
User user=userService.findUserByid(uid);
Order order=null;
if(product!=null&&product.getStock()<num&&user!=null){
order=orderService.saveorder(pid,uid,num);
}
return order;
}
Sentinel–服务容错
在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相 互调用,但是由于网络原因或者自身的原因,服务并不能保证服务的100%可 用,如果单个服务出现问题,调用这个服务就会出现网络延迟,此时若有大量 的网络涌入,会形成任务堆积,最终导致服务瘫痪。
高并发场景下,如果访问量过大,不加以控制,大量请求堆积,会击垮整个服务.需要在某些场景下为了保证服务不宕机,使用jmeter测试工具,模拟多线程,向后端服务发送请求
模拟一个高并发的场景
@RequestMapping(path = "/create/{pid}/{uid}/{num}")
public Order createOrder(@PathVariable Integer pid,
@PathVariable Integer uid,@PathVariable Integer num){
//使用feign实现服务调用
Product p = productService.findProductById(pid);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Order order=null;
if(p!=null&&u!=null&&p.getStock()>=num){
order = orderService.createOrder(pid,uid,num);
}
return order;
}
//测试接口
@GetMapping(path = "/message")
public String message(){
return "测试高并发";
}
2.修改配置文件中tomcat的最大连接数量
server:
port: 8071
tomcat:
max-threads: 10 #指定tomcat最大连接数量
3.安装ApacheJmeter用压测工具,对请求进行压力测试
3.1下载解压 3.2修改配置,并启动软件 进入bin目录,修改jmeter.properties文件中的语言支持为language=zh_CN, 然后点击jmeter.bat启动软件。
3.3添加线程组
3.4 添加http取样
添加查看结果树
访问message接口,观察访问结果.
结论: 此时会发现, 由于order方法囤积了大量请求, 导致message方法的访问 出现了问题,这就是服务雪崩的雏形。
对服务请求进行一个限制,需要使用sentinel组件对请求进行各种控制
Sentinel 使用及概念
什么是Sentinel?
Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于服务容错的综 合性解决方案。它以流量 为切入点, 从流量控制、熔断降级、系统负载保护等 多个维度来保护服务的稳定性。
Sentinel 具有以下特征:
丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的 核心场景, 例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰 填谷、集群流量控制、实时熔断下游不可用应用等。
完备的实时监控:Sentinel 提供了实时的监控功能。通过控制台可以看到 接入应用的单台机器秒级数据, 甚至 500 台以下规模的集群的汇总运行情况。
广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块, 例如与 SpringCloud、Dubbo、RPC 的整合。只需要引入相应的依赖并进行 简单的配置即可快速地接入Sentinel。
Sentinel 分为两个部分:
核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时 环境,同时对 Dubbo/SpringCloud 等框架也有较好的支持。
控制台(Dashboard)基于 SpringBoot 开发,打包后可以直接运行, 不需要额外的 Tomcat 等应用容器。
Sentinel 的概念和功能
资源就是Sentinel要保护的东西
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,可 以是一个服务,也可以是一个方法,甚至可以是一段代码。
我们入门案例中的message/test接口就可以认为是一个资源。 规则就是用来定义如何进行保护资源的
作用在资源之上, 定义以什么样的方式保护资源,主要包括流量控制规则、 熔断降级规则以及系统保护规则。
我们入门案例中就是为message1资源设置了一种流控规则, 限制了进入 message/test 的流量。
重要功能
Sentinel 的主要功能就是容错,主要体现为下面这三个:
流量控制:流量控制在网络传输中是一个常用的概念,它用于调整网络包的数据。任意 时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根 据系统的处理能力对流量进行控制。 Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状。
熔断降级: 当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异 常比例升高的时候,则对这个资源的调用进行限制,让请求快速失败,避免影响 到其它的资源而导致级联故障。
1.在 pom.xml 中加入下面依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
2.编写一个controller测试使用
@RestController
@RequestMapping(path = "/message")
public class MessageController {
@GetMapping(path = "/test1")
public String test1(){
return "测试高并发";
}
}
-
application.yml 中配置
sentinel: transport: port: 9966 dashboard: 127.0.0.1:9999
4.下载客户端
https://github.com/alibaba/Sentinel/releases
5.启动控制台
# 直接使用jar命令启动项目(控制台本身是一个SpringBoot项目)
java-Dserver.port=9999 -Dcsp.sentinel.dashboard.server=localhost:9999-Dproject.name=sentinel-dashboard -jar sentinel -dashboard-1.8.5.jar
6.访问控制台:http://ip+端口 默认用户名密码是 sentinel/sentinel
服务雪崩效应
在分布式系统中,由于网络原因或自身的原因,服务一般无法保证 100% 可 用。如果一个服务出现了问题,调用这个服务就会出现线程阻塞的情况,此时若 有大量的请求涌入,就会出现多条线程阻塞等待,进而导致服务瘫痪。 由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难 性的严重后果,这就是服务故障的 “雪崩效应”。
雪崩发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方 法响应变慢,亦或是某台机器的资源耗尽。我们无法完全杜绝雪崩源头的发生, 只有做好足够的容错,保证在一个服务发生问题,不会影响到其它服务的正常运 行。也就是"雪落而不雪崩"。
常见容错方案
要防止雪崩的扩散,我们就要做好服务的容错,容错说白了就是保护自己不 被猪队友拖垮的一些措施, 下面介绍常见的服务容错思路和组件。
常见的容错思路有: 隔离、超时、限流、熔断、降级这几种,下面分别介绍 一下。
隔离
它是指将系统按照一定的原则划分为若干个服务模块,各个模块之间相对独 立,无强依赖。当有故障发生时,能将问题和影响隔离在某个模块内部,而不扩 散风险,不波及其它模块,不影响整体的系统服务。常见的隔离方式有:线程池 隔离和信号量隔离.
超时
在上游服务调用下游服务的时候,设置一个最大响应时间,如果超过这个时 间,下游未作出反应, 就断开请求,释放掉线程。
限流
限流就是限制系统的输入和输出流量已达到保护系统的目的。为了保证系 统的稳固运行,一旦达到的需要限制的阈值,就需要限制流量并采取少量措施以完 成限制流量的目的。
熔断
在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务 为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部, 保全整体的措施就叫做熔断。
降级
降级其实就是为服务提供一个托底方案,一旦服务无法正常调用,就使用托 底方案。
常见的容错组件
Hystrix
Hystrix 是由 Netflflix 开源的一个延迟和容错库,用于隔离访问远程系统、 服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。
Resilience4J
Resilicence4J 一款非常轻量、简单,并且文档非常清晰、丰富的熔断工具, 这也是Hystrix官方推荐的替代产品。不仅如此,Resilicence4j还原生支持 SpringBoot,而且监控也支持和prometheus等多款主流产品进行整合。
Sentinel
Sentinel 是阿里巴巴开源的一款断路器实现,本身在阿里内部已经被大规 模采用,非常稳定。
Gateway–服务网关
大家都都知道在微服务架构中,一个系统会被拆分为很多个微服务。那么作 为客户端要如何去调用 这么多的微服务呢?如果没有网关的存在,我们只能在 客户端记录每个微服务的地址,然后分别去调用。
这样的架构,会存在着诸多的问题: 客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性。 认证复杂,每个服务都需要独立认证。 存在跨域请求,在一定场景下处理相对复杂。 上面的这些问题可以借助API网关来解决。 所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客 户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如 认证、鉴权、监控、路由转发等等。
添加上API网关之后,系统的架构图变成了如下所示:
我们也可以观察下,我们现在的整体架构图:
在业界比较流行的网关,有下面这些:
Ngnix+lua 使用nginx的反向代理和负载均衡可实现对api服务器的负载均衡及高可 用 lua是一种脚本语言,可以来编写一些简单的逻辑,nginx支持lua脚本.
Kong 基于Nginx+Lua开发,性能高,稳定,有多个可用的插件(限流、鉴权等等) 可以开箱即用。 问题: 只支持Http协议;二次开发,自由扩展困难;提供管 理API,缺乏更易用的管控、配置方式。
Zuul Netflix 开源的网关,功能丰富,使用JAVA开发,易于二次开发 问题:缺 乏管控,无法动态配 置;依赖组件较多;处理Http请求依赖的是Web容器, 性能不如Nginx
Spring Cloud Gateway SpringCloud alibaba 技术栈中并没有提供自己的网关,我们可以采用 Spring Cloud Gateway 来做网关,将在下面具体介绍。
Gateway简介
Spring Cloud Gateway 是 Spring公司基于Spring5.0,SpringBoot2.0 和 Project Reactor 等技术 开发的网关,它旨在为微服务架构提供一种简单有 效的统一的 API 路由管理方式。它的目标是替代 NetflixZuul,其不仅提供统 一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全, 监控和限流。
Gateway快速入门
1.导入依赖,不导入web相关的依赖
<!--gateway网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
2.创建主类
@SpringBootApplication
@EnableDiscoveryClient
public class GateWayApplication {
public static void main(String[] args) {
SpringApplication.run(GateWayApplication.class);
}
}
3.加入nacos依赖
<!--nacos服务发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
4.在主类上添加注解 @EnableDiscoveryClient
5.修改配置文件
server:
port: 9001
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 #nacos服务地址
gateway:
discovery:
locator:
enabled: true
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: product_route # 当前路由的标识, 要求唯一
uri: lb://service-order # lb 指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
order: 1 # 路由的优先级,数字越小级别越高
predicates: # 断言(就是路由转发要满足的条件)
- Path=/order-serv/** # 当请求路径满足Path指定的规则时,才进行路由转发
filters: #过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
- StripPrefix=1 # 转发之前去掉1层路径
7.测试访问
http://127.0.0.1:9001/order-serv/order/create/1/1/1
)
全局过滤器
全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一 校验,安全性验证等功能.
自定义全局过滤器
内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功 能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式 自定义一个过滤器,去完成统一的权限校验。 开发中的鉴权逻辑:
当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
以后每次请求,客户端都携带认证的token
服务端对token进行解密,判断是否有效。
自定义一个全局过滤器,去校验所有请求的请求参数中是否包含“token”, 如何不包含请求 参数“token”则不转发路由,否则执行正常的逻辑。
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class TokenFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取请求中的参数部分
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (!"123456".equals(token)) {//模拟验证token
System.out.println("鉴权失败");
exchange.getResponse().setStatusCode(HttpStatus.valueOf(401));
return exchange.getResponse().setComplete();//响应状态码
}
//调用chain.filter 继续向下游执行
return chain.filter(exchange);
}
}
网关限流
网关是所有请求的公共入口,所以可以在网关进行限流,而且限流的方式也很多, 我们本次采用Sentinel组件来实现网关的限流。Sentinel支持对 SpringCloud Gateway、Zuul 等主流网关进行限流。
从1.6.0 版本开始,Sentinel提供了SpringCloudGateway的适配模块,可以 提供两种资源维度的限流:route维度:即在Spring配置文件中配置的路由条 目,资源名为对应的routeId 自定义API维度:用户可以利用Sentinel提供的 API 来自定义一些API分组.
1.导入依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
2.编写配置类
基于Sentinel的Gateway限流是通过其提供的Filter来完成的,使用时只需注 入对应的SentinelGatewayFilter实例以及 SentinelGatewayBlockExceptionHandler实例即可。
@Configuration
publicclass GatewayConfiguration {
private final List<ViewResolver>viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
publicGatewayConfiguration(ObjectProvider<List<ViewResolver>>
viewResolversProvider,ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers =
viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer =serverCodecConfigurer;
}
//初始化一个限流的过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
publicGlobalFilter sentinelGatewayFilter() {
returnnew SentinelGatewayFilter();
}
//配置初始化的限流参数
@PostConstruct
publicvoid initGatewayRules(){
Set<GatewayFlowRule> rules =new HashSet<>();
rules.add(
new GatewayFlowRule("order_route") //资源名称,对应路由id
.setCount(1) //限流阈值
.setIntervalSec(1)//统计时间窗口,单位是秒,默认是1秒
);
GatewayRuleManager.loadRules(rules);
}
//配置限流的异常处理器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
publicSentinelGatewayBlockExceptionHandler
sentinelGatewayBlockExceptionHandler() {
returnnew SentinelGatewayBlockExceptionHandler(viewResolvers,
serverCodecConfigurer);
}
//自定义限流异常页面
@PostConstruct
publicvoid initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
publicMono<ServerResponse> handleRequest(ServerWebExchange
serverWebExchange, Throwable throwable) {
Map map =new HashMap<>();
map.put("code", 0);
map.put("message","接口被限流了");
returnServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON_UTF8).
body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
消息队列
消息队列(Message Queue) 缩写为MQ
消息队列一般称为消息队列中间件(tomcat,redis ,实现两个不同内容之间进行交互的软件)
消息队列的使用场景
异步解耦:将一些不需要即时响应的操作放到消息队列中,例如在项目中发送短信验证码或者邮箱验证码.可以再点击发送后,先将要发送的内容放入到消息队列中,然后给用户作出响应,之后发送验证码的服务从消息队列中取出要发送的信息逐一处理即可.
实现了从同步发送消息转变为异步发送消息.
常见的MQ产品
目前业界有很多MQ产品,比较出名的有下面这些:
ZeroMQ: 号称最快的消息队列系统,尤其针对大吞吐量的需求场景。扩展性好, 开发比较灵活,采用C语言 实现,实际上只是一个socket库的重新封装,如 果做为消息队列使用,需要开发大量的代码。ZeroMQ仅提供非持久性的队列, 也就是说如果down机,数据将会丢失。
RabbitMQ: 使用erlang语言开发,性能较好,适合于企业级的开发。但是不 利于做二次开发和维护。
ActiveMQ: 历史悠久的Apache开源项目。已经在很多产品中得到应用,实现 了JMS1.1规范,可以和springjms轻松融合,实现了多种协议,支持持久化到 数据库,对队列数较多的情况支持不好。
RocketMQ: 阿里巴巴的MQ中间件,由java语言开发,性能非常好,能够撑 住双十一的大流量,而且使用起来 很简单。
Kafka: Kafka 是 Apache 下的一个子项目,是一个高性能跨语言分布式 Publish/Subscribe 消息队列系统, 相对于ActiveMQ是一个非常轻量级的消 息系统,除了性能非常好之外,还是一个工作良好的分布式系统。
RocketMQ入门
RocketMQ 是阿里巴巴开源的分布式消息中间件,现在是Apache的一个 顶级项目。在阿里内部使用非常广泛,已经经过了"双11"这种万亿级的消息流转。
RocketMQ环境搭建
下载地址 https://rocketmq.apache.org/download/
配置环境变量
ROCKETMQ_HOME=D:\ProgramFiles\rocketmq-4.9.3
NAMESRV_ADDR=127.0.0.1:9876
启动NameServer 进入到bin目录输入命令: mqnamesrv.cmd
启动Broker 进入到bin目录输入命令: mqbroker.cmd-n 127.0.0.1:9876 atuoCreateTopicEnable=true
发送和接收消息测试
模拟发送消息 进入到bin目录输入命令: tools.cmd org.apache.rocketmq.example.quickstart.Producer
模拟接收消息 进入到bin目录输入命令: tools.cmd org.apache.rocketmq.example.quickstart.Consumer
启动控制台 启动控制台 java-jar rocketmq-console-ng-1.0.0.jar
访问:http://127.0.0.1:6060
RocketMQ的架构及概念
如上图所示,整体可以分成4个角色,分别是: NameServer,Broker,Producer,Consumer。
Broker(邮递员) Broker 是 RocketMQ的核心,负责消息的接收,存储,投递 等功能.
NameServer(邮局) :消息队列的协调者,Broker 向它注册路由信息,同时 Producer 和 Consumer向其获取路由信息
Producer(寄件人) :消息的生产者,需要从NameServer获取Broker信息,然 后与Broker建立连接,向Broker发送消 息
Consumer(收件人): 消息的消费者,需要从NameServer获取Broker信息, 然后与Broker建立连接,从Broker获取消息
Topic(地区) 用来区分不同类型的消息,发送和接收消息前都需要先创建Topic, 针对Topic来发送和接收消息
Message Queue(邮件) 为了提高性能和吞吐量,引入了MessageQueue, 一个Topic可以设置一个或多个MessageQueue,这样消息就可以并行往各个 Message Queue发送消息,消费者也可以并行的从多个 MessageQueue读 取消息 MessageMessage 是消息的载体。
Producer Group 生产者组,简单来说就是多个发送同一类消息的生产者称之 为一个生产者组。
Consumer Group 消费者组,消费同一类消息的多个 consumer 实例组成一 个消费者组。
java 消息发送和接收演示
1.添加依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
2.发送消息
- 创建消息生产者, 指定生产者所属的组名
- 指定Nameserver地址
- 启动生产者
- 创建消息对象,指定主题、标签和消息体
- 发送消息
- 关闭生产者
生产者创建
public class MQProducerTest {
public static void main(String[] args) throws Exception {
//1. 创建消息生产者, 指定生产者所属的组名
DefaultMQProducer producer = new DefaultMQProducer("myproducer-group");
//2. 指定Nameserver地址
producer.setNamesrvAddr("127.0.0.1:9876");
//3. 启动生产者
producer.start();
//4.创建消息对象,指定主题、标签和消息体
Message msg= new Message("myTopic","myTag",
("RocketMQ Message").getBytes());
//5.发送消息
SendResult sendResult= producer.send(msg,10000);
System.out.println(sendResult);
//6.关闭生产者
producer.shutdown();
}
}
3.接收消息
消息接收步骤:
1.创建消息消费者,指定消费者所属的组名
2.指定Nameserver地址
3.指定消费者订阅的主题和标签
4.设置回调函数,编写处理消息的方法
5.启动消息消费者
消费者
public class MQConsumerTest {
public static void main(String[]args) throws Exception {
//1.创建消息消费者,指定消费者所属的组名
DefaultMQPushConsumer consumer=new DefaultMQPushConsumer("myconsumergroup");
//2.指定Nameserver地址
consumer.setNamesrvAddr("127.0.0.1:9876");
//3.指定消费者订阅的主题和标签
consumer.subscribe("myTopic","*");
//4.设置回调函数,编写处理消息的方法
consumer.registerMessageListener(new MessageListenerConcurrently(){
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt>
msgs,
ConsumeConcurrentlyContext
context) {
System.out.println("Receive NewMessages: "+ msgs);//返回消费状态
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//5.启动消息消费者
consumer.start();
System.out.println("Consumer Started.");
}
}
案例
接下来我们模拟一种场景:下单成功之后,向下单用户发送短信。设计图如下:
订单微服务发送消息
1.添加rocketmq的依赖
<!--rocketmq-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.4.0</version>
</dependency>
2.添加配置
rocketmq:
name-server: 127.0.0.1:9876#rocketMQ服务的地址
producer:
group: shop-order #生产者组
3.编写测试代码
在订单微服务控制器中添加代码
@Autowired
private RocketMQTemplate rocketMQTemplate;
@RequestMapping("/create/{pid}/{uid}/{num}")
public Order createOrder(@PathVariable("pid") int pid, @PathVariable("uid")int uid,@PathVariable("num") int num){
Product product=productService.findProductByid(pid);
User user=userService.findUserByid(uid);
Order order=null;
if(product!=null&&product.getStock()<num&&user!=null){
order=orderService.saveorder(pid,uid,num);
}
rocketMQTemplate.convertAndSend("order-topic", "下单成功");
return order;
}
用户微服务接收消息
1.添加依赖
<!--rocketmq-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.4.0</version>
</dependency>
2.修改配置文件
rocketmq:
name-server: 127.0.0.1:9876
3.编写消息接收服务
@Service
@RocketMQMessageListener(consumerGroup="shop-user",topic="order-topic")
public class SmsService implements RocketMQListener<Order> {
@Override
public void onMessage(Order order){
System.out.println("收到一个订单信息:"+ JSON.toJSONString(order)+",接下来发送短信");
}
}
4.启动服务,执行下单操作,观看后台输出
分布式锁
在微服务系统中,一个项目可以有多个服务(进程),此时java中的锁(synchronized锁)就会失效,使用分布锁来对多个进程中的操作进行控制.
如何实现分布式锁?
基于redis实现分布式锁,在redis中存储一个变量,用来当做锁标志,因为redis是共享的.例如redis中存在共享变量,说明有用户正在操作,持有锁.用完之后删除变量,就是释放了锁.
基于Redis分布式锁的实现方式
方式1:SETNX命令
redis中有一个SETNX命令,在向redis中设置值的时候,会自动判断redis中是否存在指定的key,若不存在就设置初始值为1,若已经存在则返回0.加锁完成后,客户端操作共享资源,当结束操作后,删除key,即释放锁
//此方法会出现
// 1.程序处理业务逻辑异常,没及时释放锁
//2.进程挂了,没机会释放锁
@Autowired
RedisTemplate redisTemplate;
@GetMapping(path = "/substock")
public String substock(){
//借助redis实现加锁
//获取锁,利用setnx命令设置一个键值,若果设置成功,说明获取锁成功,否则获取失败
boolean res=redisTemplate.opsForValue().setIfAbsent("stock_lock","stock_lock",10, TimeUnit.SECONDS);
if(!res){
return "fail";
}
//1.查询库存
Integer stock=(Integer) redisTemplate.opsForValue().get("stock");
//2.判断库存是否大于0
//3.如果大于0,扣除库存
if(stock>0){
redisTemplate.opsForValue().decrement("stock");
}
//4.删除分布式锁
redisTemplate.delete("stock_lock");
return "suc";
}
但是,以上实现存在一个很大的问题,当客户端1拿到锁后,如果发生下面 的场景,就会造成死锁。
- 程序处理业务逻辑异常,没及时释放锁
- 进程挂了,没机会释放锁 以上情况会导致已经获得锁的客户端一直占用锁,其他客户端永远无法获取到锁。
解决方法:在finally 中释放锁,以及设置键失效时间.
/*
* 扣库存时,存在线程安全问题
* 必须要加锁进行控制
* 如果是在单体,单服务项目中,使用synchronized锁可以解决
* 在微服务项目中,一个项目需要启动多个服务,此时java中的单体项目锁就失效了,一个服务就是一个进程,两个进程中的锁没有关系.
*
* */
@Autowired
RedisTemplate redisTemplate;
@GetMapping(path = "/substock")
public String substock(){
try {
//借助redis实现加锁
//获取锁,利用setnx命令设置一个键值,若果设置成功,说明获取锁成功,否则获取失败
boolean res=redisTemplate.opsForValue().setIfAbsent("stock_lock","stock_lock",10, TimeUnit.SECONDS);
if(!res){
return "fail";
}
//1.查询库存
Integer stock=(Integer) redisTemplate.opsForValue().get("stock");
//2.判断库存是否大于0
//3.如果大于0,扣除库存
if(stock>0){
redisTemplate.opsForValue().decrement("stock");
}
}finally {
//4.删除分布式锁
redisTemplate.delete("stock_lock");
}
return "suc";
}
但是设置失效时间10s,有可能业务执行时间大于10s,那么锁会失效,导致其 他线程进入到减库存业务中,这时,第一个线程执行完成,会误删除第二个线程的 锁标志,导致其他线程进入.导致锁失效
解决办法: 为每个线程的添加一个版本号,删除时,判断版本号
@GetMapping(path = "/substock")
public String substock(){
String clientId= UUID.randomUUID().toString();//生成一个32为不重复的字符串
try {
//借助redis实现加锁
//获取锁,利用setnx命令设置一个键值,若果设置成功,说明获取锁成功,否则获取失败
//设置失效时间,一担业务执行时间超过预设时间,此时其他线程就可以进入获取锁
//先进入的线程,执行完业务,就会删除锁,那么先进来的线程会删掉后来的锁,导致其他线程在进入到同步块中
boolean res=redisTemplate.opsForValue().setIfAbsent("stock_lock",clientId,10, TimeUnit.SECONDS);
if(!res){
return "fail";
}
//1.查询库存
Integer stock=(Integer) redisTemplate.opsForValue().get("stock");
//2.判断库存是否大于0
//3.如果大于0,扣除库存
if(stock>0){
redisTemplate.opsForValue().decrement("stock");
}
}finally {
//4.删除分布式锁
String cid =(String)redisTemplate.opsForValue().get("stock-lock");
if(cid.equals(clientId)){
redisTemplate.delete("stock_lock");
}
}
return "suc";
}
方式2: 使用redission
1.导入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
2.创建Redisson对象
@Configuration
public class RedissonConfig {
@Bean
public Redisson getRedisson(){
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
return (Redisson)Redisson.create(config);
}
}
3.使用Redisson实现加锁,释放锁
@Autowired
RedisTemplate redisTemplate;
@Autowired
Redisson redisson;
@GetMapping(path = "/substock")
public String substock1(){
RLock rLock=redisson.getLock("stock_lock");
try {
rLock.lock(30,TimeUnit.SECONDS);
//1.查询库存
Integer stock=(Integer) redisTemplate.opsForValue().get("stock");
//2.判断库存是否大于0
//3.如果大于0,扣除库存
if(stock>0){
redisTemplate.opsForValue().decrement("stock");
}
}finally {
//4.删除分布式锁
rLock.unlock();
}
return "suc";
}
tring)redisTemplate.opsForValue().get(“stock-lock”);
if(cid.equals(clientId)){
redisTemplate.delete(“stock_lock”);
}
}
return “suc”;
}
**方式2: 使用redission**
1.导入依赖
```xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
2.创建Redisson对象
@Configuration
public class RedissonConfig {
@Bean
public Redisson getRedisson(){
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
return (Redisson)Redisson.create(config);
}
}
3.使用Redisson实现加锁,释放锁
@Autowired
RedisTemplate redisTemplate;
@Autowired
Redisson redisson;
@GetMapping(path = "/substock")
public String substock1(){
RLock rLock=redisson.getLock("stock_lock");
try {
rLock.lock(30,TimeUnit.SECONDS);
//1.查询库存
Integer stock=(Integer) redisTemplate.opsForValue().get("stock");
//2.判断库存是否大于0
//3.如果大于0,扣除库存
if(stock>0){
redisTemplate.opsForValue().decrement("stock");
}
}finally {
//4.删除分布式锁
rLock.unlock();
}
return "suc";
}
原文地址:https://blog.csdn.net/2301_78085386/article/details/144790985
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!