微服务框架

Spring Cloud

  • SpringCloud是由Spring提供的一套能够快速搭建微服务架构程序的框架集,框架集表示SpringCloud不是一个框架,而是很多框架的统称

  • Spring Cloud NetFix

  • api网关:zuul组件

  • Feign

  • 服务注册:Eureka

  • 熔断机制:Hystrix

  • Spring Cloud Alibaba:本文基于Spring Cloud Alibaba

Nacos

  • 主要具有注册中心和配置中心

  • 注册:微服务中所有项目都必须注册到注册中心才能成为微服务的一部分,所谓注册,就是将自己的信息,提交到Nacos来保存

安装启动

  • Nacos是java开发的,我们要启动Nacos必须保证当前系统配置了java环境变量

  • Windows系统进入bin目录,打开终端输入:startup.cmd -m standalone

  • startup.cmd:windows启动nacos的命令文件

  • -m 表示要设置启动参数

  • standalone:翻译为标准的孤独的,意思是正常的使用单机模式启动

  • 用户名:nacos

  • 密码:nacos

注册中心

  • 一个项目要想成为微服务项目体系的一部分,必须将当前项目的信息注册到Nacos

  • 项目需要注册到Nacos中需要在pom文件中添加的依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  • 项目需要注册到Nacos中需要在application文件中添加的信息

spring:
  application:
    # 为当前项目起名,这个名字会被Nacos获取并记录,由于之后的微服务调用
    name: nacos-xxxxx
  cloud:
    nacos:
      discovery:
        # 配置Nacos的位置,用于提交当前项目的信息
        server-addr: localhost:8848
  • 项目注册成功后登录Nacos的页面在服务管理的服务列表中可以查询到

配置中心

  • 在微服务的环境下,将项目需要的配置信息保存在配置中心,需要读取时直接从配置中心读取,方便配置管理的微服务工具,我们可以将部分yml文件的内容保存在配置中心

  • 一个微服务项目有很多子模块,这些子模块可能在不同的服务器上,如果有一些统一的修改,我们要逐一修改这些子模块的配置,由于它们是不同的服务器,所以修改起来很麻烦,如果将这些子模块的配置集中在一个服务器上,我们修改这个服务器的配置信息,就相当于修改了所有子模块的信息,这个服务器就是配置中心,使用配置中心的原因就是能够达到高效的修改各模块配置的目的

  • Nacos做配置中心,支持各种格式\类型的配置文,例如properties\yaml(yml)\txt\json\xml等

  • 数据结构,实际在Nacos中定位一个配置的结构为Namespace>Group>DataId

  • namespace:命名空间

  • group:分组

  • Service/DataId:具体数据

  • 添加命名空间.namespace是Nacos提供的最大的数据结构,一个Nacos可以创建多个命名空间,一个命名空间能够包含多个group,每一个group中又可以包含多条配置信息,在nacos中新建命名空间

  • Nacos有默认的命名空间public不能删除和修改,添加命名空间后,我们在Nacos中注册的服务或添加的配置就可以指定命名空间了,因为多个命名空间可以隔离项目,每个项目使用自己的命名空间,互不干扰

  • 添加分组.一个命名空间中可以有多个分组,进行进一步分离,我们使用时,如果不需要进一步分组,推荐使用group名称:DEFAULT_GROUP

  • 服务或配置,确定了命名空间和分组之后,我们就可以添加服务或配置了,之前我们启动的各种模块都是服务,这些服务都是默认保存在public命名空间中,下面我们主要使用配置中心的功能,在命名空间中添加配置,添加配置就是设置DataId

  • 在项目的pom文件中添加依赖

<!--   Nacos配置中心依赖   -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--     支持SpringCloud项目加载\读取系统配置的依赖     -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
  • 配置bootstrap.yml文件

spring:
  cloud:
    nacos:
      config:       
        server-addr: localhost:8848
        group: DEFAULT_GROUP       
        file-extension: yaml
  • server-addr:设置配置中心的ip和端口

  • namespace:默认public可以省略

  • group:默认DEFAULT_GROUP也可以省略的

  • file-extension:指定要读取配置文件的后缀名

  • 配置中心约定,当确定命名空间,分组名称和后缀名称后, 配置中心会自动读取当前文件名为[服务器名称].[后缀名称]的配置信息,具体到本项目 服务器名称为 nacos-test,后缀名为yaml,所以会读取配置文件名为"nacos-test.yaml"的信息

  • 在添加上面的pom文件依赖之后,SpringCloud项目就又多了一组配置文件,它们是bootstrap.yml和bootstrap.properties,这组配置文件是SpringCloud项目才能使用的,它的作用是实际开发时,主要配置系统内容,一般都是不轻易修改的,所以这组配置文件的加载时机,整体早于application这一组,如果有多个配置文件同时设置了同一个属性,后加载的覆盖掉先加载的,加载顺序是:

心跳机制

  • 心跳:周期性的操作,来表示自己是健康可用的机制,注册到Nacos的微服务项目(模块)都是会遵循这个心跳机制的

  • 默认情况下,服务启动开始每隔5秒会向Nacos发送一个"心跳包",这个心跳包中包含了当前服务的基本信息,Nacos接收到这个心跳包,首先检查当前服务在不在注册列表中,如果不在按新服务的业务进行注册,如果在,表示当前这个服务是健康状态

  • 如果一个服务连续3次心跳(默认15秒)没有和Nacos进行信息的交互,就会将当前服务标记为不健康的状态

  • 如果一个服务连续6次心跳(默认30秒)没有和Nacos进行信息的交互,Nacos会将这个服务从注册列表中剔除

  • 心跳机制的目的

  • 是表示当前微服务模块运行状态正常的手段

  • 是表示当前微服务模块和Nacos保持沟通和交换信息的机制

实例类型分类

  • 临时实例(默认)

  • 持久化实例(永久实例),心跳包的规则和临时实例一致,只是不会将该服务从列表中剔除,只有项目的主干业务才会设置为永久实例,通过配置可以更改为持久

cloud:
  nacos:
    discovery:
      # ephemeral设置当前项目启动时注册到nacos的类型 true(默认):临时实例 false:永久实例
      ephemeral: false 

Dubbo

  • Dubbo是一套RPC框架。既然是框架,我们可以在框架结构高度定义Dubbo中使用的通信协议,使用的序列化框架技术等,而数据格式由Dubbo定义,我们配置完成后,就可以直接通过客户端调用服务端代码。可以说Dubbo就是RPC概念的实现.目前主流使用阿里巴巴的Dubbo,Dubbo调用全部是同步的操作

RPC

  • RPC是Remote Procedure Call的缩写 翻译为:远程过程调用,目标是为了实现两台(多台)计算机\服务器,相互调用方法\通信的解决方案

  • RPC只是实现远程调用的一套标准,该标准主要规定了两部分内容

  • 通信协议:指的就是远程调用的通信方式,实际上这个通知的方式可以有多种,例如:写信,飞鸽传书,发电报.在程序中,通信方法实际上也是有多种的,每种通信方式会有不同的优缺点

  • dubbo协议(默认)

  • rmi协议

  • hessian协议

  • http协议

  • webservice

  • 序列化协议:指通信内容的格式,双方都要理解这个格式.发送信息是序列化过程,接收信息需要反序列化.程序中,序列化的方式也是多种的,每种序列化方式也会有不同的优缺点

  • hessian2(默认)

  • java序列化

  • compactedjava

  • nativejava

  • fastjson

  • dubbo

  • fst

  • kryo

注册与发现

  • 在Dubbo的调用过程中,必须包含注册中心的支持,项目调用服务的模块必须在同一个注册中心中.推荐阿里自己的Nacos,兼容性好,能够发挥最大性能,但是Dubbo也支持其它软件作为注册中心(例如Redis,zookeeper等)

  • 服务发现,即消费端自动发现服务地址列表的能力,是微服务框架需要具备的关键能力,借助于自动化的服务发现,微服务之间可以在无需感知对端部署位置与 IP 地址的情况下实现通信。

  • 各模块职责

  • consumer:服务的消费者,指服务的调用者(使用者)

  • provider:服务的提供者,指服务的拥有者(生产者)

  • Nacos:远程调用依据是服务的提供者在Nacos中注册的服务名称.一个服务名称,可能有多个运行的实例,任何一个空闲的实例都可以提供服务

  • 流程

  • 首先服务的提供者启动服务时,将自己的具备的服务注册到注册中心,其中包括当前提供者的ip地址和端口号等信息,Dubbo会同时注册该项目提供的远程调用的方法

  • 消费者(使用者)启动项目,也注册到注册中心,同时从注册中心中获得当前项目具备的所有服务列表

  • 当注册中心中有新的服务出现时,(在心跳时)会通知已经订阅发现的消费者,消费者会更新所有服务列表

  • RPC调用,消费者需要调用远程方法时,根据注册中心服务列表的信息,只需服务名称,不需要ip地址和端口号等信息,就可以利用Dubbo调用远程方法了

负载均衡

  • Loadbalance:就是负载均衡的意思

  • Dubbo框架内部支持负载均衡算法,能够尽可能的让请求在相对空闲的服务器上运行,在不同的项目中,可能选用不同的负载均衡策略,以达到最好效果

  • 在实际项目中,一个服务基本都是集群模式的,也就是多个功能相同的项目在运行,这样才能承受更高的并发,这时一个请求到这个服务,就需要确定访问哪一个服务器

负载均衡算法
  • random loadbalance:随机分配策略(默认)

  • 假设我们当前3台服务器,经过测试它们的性能权重比值为5:3:1,下面可以生成一个权重模型,5:3:1,随机生成随机数,在哪个范围内让哪个服务器运行

  • 优点:算法简单,效率高,长时间运行下,任务分配比例准确

  • 缺点:偶然性高,如果连续的几个随机请求发送到性能弱的服务器,会导致异常甚至宕机

  • round Robin Loadbalance:权重平均分配

  • 连续请求一个服务器肯定是不好的,我们希望所有的服务器都能够穿插在一起运行,一个优秀的权重分配算法,应该是让每个服务器都有机会运行的

  • 如果一个集群服务器性能比为5:3:1服务为A,B,C,每一次请求过来在权重最大的服务器上执行

  • 执行完一次后开始减权,意思就是把执行过这个服务器的权重减去总权重

  • 再下一次请求来之前进行加权,按照5:3:1相加到3个服务器上

  • 请求来的时候在权重最大的服务器上执行,以此循环每次请求

  • 优点:能够尽可能的在权重要求的情况下,实现请求的穿插运行(交替运行),不会发生随机策略中的偶发情况

  • 缺点:服务器较多时,可能需要减权和复权的计算,需要消耗系统资源

  • leastactive Loadbalance:活跃度自动感知分配

  • 记录每个服务器处理一次请求的时间,按照时间比例来分配任务数,运行一次需要时间多的分配的请求数较少

  • consistanthash Loadbalance:一致性hash算法分配

  • 根据请求的参数进行hash运算,以后每次相同参数的请求都会访问固定服务器,因为根据参数选择服务器,不能平均分配到每台服务器上,使用的也不多

导入框架

  • 在pom文件中添加依赖

<!--   Dubbo 在SpringCloud中使用的依赖   -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>
  • 在application文件中配置dubbo信息

dubbo:
  protocol:
    # port设置为-1 表示当前dubbo框架使用的端口自动寻找
    # 使用端口的规则是从20880开始寻找可用端口,如果当前端口号占用,就继续加1来使用,直到找到可用的为止
    port: -1
    # 设置连接的名称,一般固定为dubbo即可
    name: dubbo
  registry:
    # 指定当前Dubbo服务注册中心的类型和位置
    address: nacos://localhost:8848
  consumer:
    # 当前项目启动时,是否检查当前项目需要的所有Dubbo服务是否可用
    # 我们设置它为false,表示不检查,以减少启动时出错的情况
    check: false

使用

  • 生产者:服务的提供者

  • @DubboService注解,标记的业务逻辑层实现类,其中所有的方法都会注册到Nacos,在其他服务启动并"订阅"后,就会"发现"当前类中的所有服务,随时可以调用

@DubboService
@Service
@Slf4j
public class ProviderServiceImpl implements IProviderService {
    
}
  • 还需要在SpringBoot启动类上添加@EnableDubbo的注解,才能真正让Dubbo功能生效

@SpringBootApplication
@EnableDubbo
public class TestApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }

}
  • 消费者:服务的消费者

  • 在pom文件中添加要消费的模块的业务逻辑层接口项目的依赖

<dependency>
    <groupId>xx.xxx</groupId>
    <artifactId>xxx-xxxx-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
  • 在需要Dubbo调用远程服务的地方加上@DubboReference注解,表示当前业务逻辑层中需要消费其他模块的服务

@Service
@Slf4j
public class ConsumerServiceImpl implements IConsumerService {

    @DubboReference
    private IProviderService dubboProviderService;


    @Override
    public void test() {
        dubboProviderService.test();
    }
}

Seata

  • Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务,也是Spring Cloud Alibaba提供的组件

  • 单体项目中的事务,使用的技术叫Spring声明式事务,能够保证一个业务中所有对数据库的操作要么都成功,要么都失败,来保证数据库的数据完整性,但是在微服务的项目中,业务逻辑层涉及远程调用,当前模块发生异常,无法操作远程服务器回滚,这时要想让远程调用也支持事务功能,就需要使用分布式事务组件Seata

原理

  • Seata构成部分包含

  • 事务协调器TC

  • 事务管理器TM

  • 资源管理器RM

事务模式

  • AT(默认的模式),事务分支都必须是操作关系型数据库(Mysql\MariaDB\Oracle),因为关系型数据库才支持提交和回滚,所以如果我们在业务过程中有一个节点操作的是Redis或其它非关系型数据库时,就无法使用AT模式

  • 事务的发起方(TM)会向事务协调器(TC)申请一个全局事务id,并保存

  • Seata会管理事务中所有相关的参与方的数据源,将数据操作之前和之后的镜像都保存在undo_log表中,这个表是seata组件规定的表,没有它就不能实现效果,依靠它来实现提交(commit)或回滚(roll back)的操作

  • 事务的发起方(TM)会连同全局id一起通过远程调用,运行资源管理器(RM)中的方法

  • RM接收到全局id,去运行指定方法,并将运行结果的状态发送给TC

  • 如果所有分支运行都正常,TC会通知所有分支进行提交,真正的影响数据库内容,反之如果所有分支中有任何一个分支发生异常,TC会通知所有分支进行回滚,数据库数据恢复为运行之前的内容

  • TCC,就是自己编写代码完成事务的提交和回滚,在TCC模式下,我们需要为参与事务的业务逻辑编写一组共3个方法

  • prepare:准备.prepare方法是每个模块都会运行的方法

  • commit:提交.当所有模块的prepare方法运行都正常时,运行commit

  • rollback:回滚.当任意模块运行的prepare方法有异常时,运行rollback

  • 优点:虽然代码是自己写的,但是事务整体提交或回滚的机制仍然可用(仍然由TC来调度)

  • 缺点:每个业务都要编写3个方法来对应,代码冗余,而且业务入侵量大

  • SAGA ,是对应每个业务逻辑层编写一个新的类,可以设置指定的业务逻辑层方法发生异常时,运行当新编写的类中的代码,相当于将TCC模式中的rollback方法定义在了一个新的类中

  • 优点:这样编写代码不影响已经编写好的业务逻辑代码,一般用于修改已经编写完成的老代码

  • 缺点:是每个事务分支都要编写一个类来回滚业务,会造成类的数量较多,开发量比较大

  • XA,没使用过

安装启动

  • 下载地址

  • 解压后路径不要用中文,不要用空格

  • 启动

  • 进入到bin目录下,在终端CMD中输入seata-server.bat -h 127.0.0.1 -m file

  • 输入后,最后出现8091端口的提示即可

使用

  • 项目的pom文件中添加依赖

<!--   Seata和SpringBoot整合依赖     -->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
</dependency>
<!--  Seata 完成分布式事务的两个相关依赖(Seata会自动使用其中的资源)  -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
</dependency>
  • 在application文件中添加配置

seata:
  tx-service-group: xxxxx_group  # 定义事务的分组,一般是以项目为单位的,方便与其它项目区分
  service:
    vgroup-mapping:
      xxxxx_group: default     # xxxxx_group分组的配置,default会默认配置Seata
    grouplist:
      default: localhost:8091   # 配置Seata服务器的地址和端口号
  • 同一个事务必须在同一个tx-service-group中,同时指定相同的seata地址和端口

在事务

  • 只要发起业务的业务逻辑方法上添加@GlobalTransactional注解,添加这个注解的模块就是模型中的TM,他调用的所有远程模块都是RM,最终效果就是当前方法开始之后,所有的远程调用操作数据库的功能都在同一个事务中

@Service
@Slf4j
public class ConsumerServiceImpl implements IConsumerService {

    @DubboReference
    private IProviderService dubboProviderService;

    @GlobalTransactional
    @Override
    public void test() {
         dubboProviderService.add();
    }
}

问题解决方案

  • Seata在开始工作时,会将方法相关对象序列化后保存在对应数据库的undo_log表中,但是Seata我们序列化的方式支持很多中,常见的jackson格式序列化的情况下,不支持java对象LocalDataTime类型的序列化,序列化运行时会发送错误,要想解决,两方面思路

  • 将序列化过程中LocalDataTime类型转换为Date

  • 将Seata序列化转换为kryo类型,但是需要在pom文件中添加依赖

<!--解决seata序列化问题-->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-serializer-kryo</artifactId>
</dependency>
  • yml文件使用kryo序列化对象的配置

#seata服务端
seata:  
  client:
    undo:
      log-serialization: kryo

Sentinel

  • Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。为了保证服务器运行的稳定性,在请求数到达设计最高值时,将过剩的请求限流,保证在设计的请求数内的请求能够稳定完成处理

安装启动

  • 下载地址

  • 双击start-sentinel.bat文件启动

使用

  • 在pom文件中添加依赖

<!--  sentinel依赖  -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
  • 在application文件中进行配置

spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080 # 配置Sentinel提供的运行状态仪表台的位置
        # 执行限流的端口号,每个项目需要设置不同的端口号
        port: 8721

限流方法

  • @SentinelResource注解需要标记在控制层方法上,在该方法运行后,会被Sentinel监测。该方法运行前,Sentinel监测不到,必须至少运行一次后才可以开始监测信息,

  • value就是"限流测试"这个名称就是显示在Sentinel上该方法的名称

  • blockHandlerClass,当执行限流的方法不在本体类中的时候可以通过这个属性指定那个类,如果方法在本体类中则无需这个属性

  • blockHandler就是指定被限流时,要运行自定义方法的属性,"blockError"就是方法名

  • 访问修饰符必须是public

  • 返回值类型必须和控制器方法一致

  • 方法名必须是控制器方法注解中blockHandler定义的名称

  • 方法参数必须包含控制器的所有参数,而且可以额外添加BlockException的异常类型参数

  • fallbackClass当执行降级的方法不在本体类中的时候可以通过这个属性指定那个类,如果方法在本体类中则无需这个属性

  • fallback就是指被降级是,要运行自定义方法的属性,"fallbackError"就是方法名

  • 方法定义规则和限流方法基本一致,方法的参数中,最后的参数可以额外添加Throwable类型

  • 当控制器方法发生异常时,Sentinel会调用这个方法,优先级比统一异常处理类高而且针对单一控制器方法编写

  • 实际开发中,可能包含一些业务例如:运行老版本代码,或使用户获得一些补偿

@RestController
@RequestMapping("/test")
public class TestController {
    @Autowired
    privage TestService testService;

    @RequestMapping("/add")
    @SentinelResource(value = "限流测试",
                    blockHandler = "blockError",
                    fallback = "fallbackError")
    public String test() {
        return "测试";
    }

    public String blockError(BlockException e){
    // 运行这个方法表示当前请求被Sentinel限流了,需要给出被限流的提示
    return "服务器忙,请稍后再试";
    }
    
    public String fallbackError(Throwable throwable){
    // 我们的业务仅仅是输出异常提示
    return "运行发生异常,服务降级!";
    }
}
  • 在第一次运行了减少库存方法之后,sentinel的仪表盘才会出现nacos-stock的信息,选中这个信息点击"簇点链路"

  • 找到我们编写的"限流测试"方法,点 "+流控",设置流控规则

  • QPS为每秒请求数,单纯的限制在一秒内有多少个请求访问控制器方法

  • 并发线程数是当前正在使用服务器资源请求线程的数量,限制的是使用当前服务器的线程数

  • 降级是当服务器高峰期的时候对某些服务进行降级把资源让给核心服务

  • 熔断是当某个连锁服务中的某个节点处理时间过长或者不可用的时候对这个节点直接返回错误,等这个节点回复正常的时候再使用

SpringGateway

  • 网关就是当前微服务项目的"统一入口",程序中的网关就是当前微服务项目对外界开放的统一入口,所有外界的请求都需要先经过网关才能访问到我们的程序,提供了统一入口之后,方便对所有请求进行统一的检查和管理

  • 网关的主要功能有

  • 将所有请求统一经过网关

  • 网关可以对这些请求进行检查

  • 网关方便记录所有请求的日志

  • 网关可以统一将所有请求路由到正确的模块\服务上

项目创建

  • SpringGateway网关是一个依赖,不是一个软件,所以我们要使用它的话,必须先创建一个SpringBoot项目,这个项目也要注册到Nacos注册中心,因为网关项目也是微服务项目的一个组成部分

  • 项目添加依赖

<!--   SpringGateway的依赖   -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--   Nacos依赖   -->
<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-loadbalancer</artifactId>
</dependency>
  • 配置application文件

server:
  port: 9000
spring:
  application:
    name: gateway
   # 网关也是微服务项目的一部分,所以也要注册到Nacos
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      routes:       
        - id: gateway-test
          uri: lb://test       
          predicates:               
            - Path=/test/**
  • routes:就是路由的意思,这属性是一个数组类型,其中的值都是数组元素,-开头表示一个数组元素的开始,后面所有内容都是这个元素的内容

  • id:表示当前路由的名称,和任何之前出现过的名字没有任何关联,唯一的要求就是不要后之后的id重复

  • uri:路由的目标。lb是LoadBalance的缩写,test是目标服务在Nacos中的名称

  • predicates:断言的意思,就是满足下列条件时,就路由到uri定义的目标。predicates也是一个数组,配置断言的内容。

  • 根据上述案例,当浏览器访问localhost:9000/test/add的时候,经过网关会路由到localhost:9001/test/add(9001是经过负载均衡后得到的test服务组中的其中一个子模块的端口,add是test模块的一个方法)

动态路由

  • 网关项目的配置会随着微服务模块数量增多而变得复杂,维护的工作量也会越来越大,所以我们希望gateway能够设计一套默认情况下自动路由到每个模块的路由规则,这样的话,不管当前项目有多少个路由目标,都不需要维护yml文件了

  • 在application文件中把自定义路由改为动态路由

spring: 
    gateway:
      discovery:
        locator: 
          enabled: true
  • 当浏览器访问localhost:9000/test/add的时候,经过网关会路由到localhost:9001/add(9001是经过负载均衡后得到的test服务组中的其中一个子模块的端口,add是test模块的一个方法)

内置断言

  • 断言的意思就是判断某个条件是否满足

  • after,before,between,cookie,header,host,method,path,query,remoteaddr

时间相关
  • after,before,between

  • 判断当前时间在指定时间之前,之后或之间的操作,如果条件满足可以执行路由操作,否则拒绝访问

  • before:必须在指定时间之间访问,否则发生404错误拒绝访问。after和between同理

routes:
  - id: gateway-test
    uri: lb://test
    predicates:
      - Path=/sh/**
      # After时间断言,判断当前时间是否晚于after指定的时间
      # 如果不晚于,则不允许访问,如果晚于才允许访问,它和上面的Path是"与"的关系
      - Between=2022-10-27T10:31:40.712+08:00[Asia/Shanghai],2022-10-27T10:32:10.712+08:00[Asia/Shanghai]

内置过滤器

  • 内置过滤器允许我们在路由请求到目标资源的同时,对这个请求进行一些加工或处理

AddRequestParameter过滤器
  • 在请求参数中添加额外参数

routes:
  - id: gateway-test
    uri: lb://test
    filters:
      - AddRequestParameter=age,18
    predicates:
      - Path=/test/**
  • 当浏览器访问localhost:9000/test/add的时候,经过网关会路由到localhost:9001/add(9001是经过负载均衡后得到的test服务组中的其中一个子模块的端口,add是test模块的一个方法),Controller层中接受到的参数包含了age=18这个参数。

简化knife4j配置

  • 因为现在每个模块进入到knife4j都需要根据每个模块的端口不一样而输入不同的端口号才能进入对应模块的knife4j,现在可以通过gateway做一个统一的入口,这样测试每个模块的时候就不用更改端口号了

  • 复制SwaggerProvider到gateway项目中

@Component
public class SwaggerProvider implements SwaggerResourcesProvider {
    /**
     * 接口地址
     */
    public static final String API_URI = "/v2/api-docs";
    /**
     * 路由加载器
     */
    @Autowired
    private RouteLocator routeLocator;
    /**
     * 网关应用名称
     */
    @Value("${spring.application.name}")
    private String applicationName;

    @Override
    public List<SwaggerResource> get() {
        //接口资源列表
        List<SwaggerResource> resources = new ArrayList<>();
        //服务名称列表
        List<String> routeHosts = new ArrayList<>();
        // 获取所有可用的应用名称
        routeLocator.getRoutes().filter(route -> route.getUri().getHost() != null)
                .filter(route -> !applicationName.equals(route.getUri().getHost()))
                .subscribe(route -> routeHosts.add(route.getUri().getHost()));
        // 去重,多负载服务只添加一次
        Set<String> existsServer = new HashSet<>();
        routeHosts.forEach(host -> {
            // 拼接url
            String url = "/" + host + API_URI;
            //不存在则添加
            if (!existsServer.contains(url)) {
                existsServer.add(url);
                SwaggerResource swaggerResource = new SwaggerResource();
                swaggerResource.setUrl(url);
                swaggerResource.setName(host);
                resources.add(swaggerResource);
            }
        });
        return resources;
    }
}
  • 复制SwaggerController到gateway项目中

@RestController
@RequestMapping("/swagger-resources")
public class SwaggerController {
    @Autowired(required = false)
    private SecurityConfiguration securityConfiguration;
    @Autowired(required = false)
    private UiConfiguration uiConfiguration;
    private final SwaggerResourcesProvider swaggerResources;
    @Autowired
    public SwaggerController(SwaggerResourcesProvider swaggerResources) {
        this.swaggerResources = swaggerResources;
    }
    @GetMapping("/configuration/security")
    public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
        return Mono.just(new ResponseEntity<>(
                Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
    }
    @GetMapping("/configuration/ui")
    public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
        return Mono.just(new ResponseEntity<>(
                Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
    }
    @GetMapping("")
    public Mono<ResponseEntity> swaggerResources() {
        return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
    }
}
  • 复制SwaggerHeaderFilter到gateway项目中

@Component
public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory {
    private static final String HEADER_NAME = "X-Forwarded-Prefix";

    private static final String URI = "/v2/api-docs";

    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            String path = request.getURI().getPath();
            if (!StringUtils.endsWithIgnoreCase(path,URI )) {
                return chain.filter(exchange);
            }
            String basePath = path.substring(0, path.lastIndexOf(URI));
            ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
            ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
            return chain.filter(newExchange);
        };
    }
}

SpringMVC冲突问题

  • 在gateway项目中我们添加了网关依赖,同时因为使用knife4j添加了SpringMVC依赖,这两个依赖在同一个项目中时,默认情况下启动会报错

  • 原因在于SpringMvc框架中自带一个Tomcat服务器,而SpringGateway框架中自带一个Netty的服务器,在启动项目时,两个框架中包含的服务器都想占用相同端口,因为争夺端口号的主动权而发生冲突,导致启动服务时报错

  • 解决办法在application文件中修改配置,添加这个配置之后,会Tomcat服务器会变成非阻塞的运行

spring:
  main:
    web-application-type: reactive

Elasticsearch

  • 它的功能也类似一个数据库,能高效的从大量数据中搜索匹配指定关键字的内容,它也将数据保存在硬盘中,这样的软件有一个名称全文搜索引擎,它本质就是一个java项目,使用它进行数据的增删改查就是访问这个项目的控制器方法(url路径)

  • 这个API提供了全文搜索引擎核心操作的接口,相当于搜索引擎的核心支持,ES是在Lucene的基础上进行了完善,实现了开箱即用的搜索引擎软件

  • 数据库进行模糊查询效率严重低下,所有关系型数据库都有这个缺点(mysql\mariaDB\oracle\DB2等),测试证明一张千万级别的数据表进行模糊查询需要20秒以上,当前互联网项目要求"三高"的需求下,这样的效率肯定不能接受,Elasticsearch主要是为了解决数据库模糊查询性能低下问题的,ES进行优化之后,从同样数据量的ES中查询相同条件数据,效率能够提高100倍以上

运行原理

  • 首先要将数据库中的数据复制到ES中,在新增数据到ES的过程中,ES可以对指定的列进行分词索引保存在索引库中,形成倒排索引结构

  • 正排索引是从文档到关键字的映射(已知文档求关键字)

  • 倒排索引是从关键字到文档的映射(已知关键字求文档)

数据结构

  • ES启动后,ES服务可以创建多个index(索引),index可以理解为数据库中表的概念

  • 一个index可以创建多个保存数据的document(文档),一个document理解为数据库中的一行数据

  • 一个document中可以保存多个属性和属性值,对应数据库中的字段(列)和字段值

安装运行

  • 复制到没有中文,没有空格的目录下解压,双击bin\elasticsearch.bat运行

  • 浏览器输入地址:localhost:9200看到内容即可

测试功能

  • 在IDEA中找到文件类型叫HTTP Request的文件,这类型文件可以直接发出http请求

POST http://localhost:9200/_analyze
Content-Type: application/json

{
  "text": "my name is hanmeimei",
  "analyzer": "standard"
}
  • analyzer:分析者(分词器)

  • text:分词内容

  • standard这个分词器只能对英文等西文字符(有空格的),进行正确分词,但是中文分词不能按空格分,这个是默认的分词器

  • 要实现对中文的分词可以使用免费的简单中文分词器IK,安装插件之后要重启ES才能生效,然后修改analyzer

POST http://localhost:9200/_analyze
Content-Type: application/json

{
  "text": "女士连衣裙",
  "analyzer": "ik_smart"或者"ik_max_word"
}
  • ik_smart

  • 优点:特征是粗略快速的将文字进行分词,占用空间小,查询速度快

  • 缺点:分词的颗粒度大,可能跳过一些重要分词,导致查询结果不全面,查全率低

  • ik_max_word

  • 优点:特征是详细的文字片段进行分词,查询时查全率高,不容易遗漏数据

  • 缺点:因为分词太过详细,导致有一些无用分词,占用空间较大,查询速度慢

使用

  • 在项目pom文件中添加依赖

<!--  Spring Data Elasticsearch 整合依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
  • 在项目application文件中添加配置

# 配置ES所在的ip地址和端口号信息
spring.elasticsearch.rest.uris=http://localhost:9200

# SpringDataElasticsearch框架中有一个转换输出信息的类,根据需要的环境更改日志级别
logging.level.org.elasticsearch.client.RestClient=debug
  • 创建ES实体类,操作ES时也需要一个类似实体类的数据类,作为操作ES的数据载体

@Document(indexName = "items")
public class Item implements Serializable {

    @Id
    private Long id;
    @Field(type = FieldType.Text,
            analyzer = "ik_max_word",
            searchAnalyzer = "ik_max_word")
    private String title;
    @Field(type = FieldType.Keyword,index = false)
    private String name;
}
  • @Document注解标记表示当前类是对应ES框架的一个实体类,属性indexName指定ES中的索引名称,运行时如果这个索引不存在SpringData会自动创建这个索引

  • @Id注解标记当前实体类主键

  • @Field注解

  • type:数据类型

  • analyzer:保存时候用什么分词器

  • searchAnalyzer:搜索时候用什么分词器

  • index:不创建索引,但ES会保存这个数据

  • 创建ES的持久层

@Repository
public interface ItemRepository extends ElasticsearchRepository<Item,Long> {
    
}
  • 持久层规范名称为repository(仓库)

  • ItemRepository接口要继承SpringData框架提供的父接口ElasticsearchRepository,继承了父接口后,SpringData会根据我们泛型中编写的Item找到对应的索引,会对这个索引自动生成基本的增删改查方法,我们自己无需再编写

  • ElasticsearchRepository<[要操作的\关联的实体类名称],[实体类主键的类型]>

  • ES的基本使用

@SpringBootTest
@Slf4j
class Test {

    @Autowired
    private ItemRepository itemRepository;

    // 单增
    @Test
    void addOne() {
        Item item=new Item()
                .setId(1L)
                .setTitle("女士连衣裙")               
                .setName("name1");
        itemRepository.save(item);
    }

    // 单查
    @Test
    void getOne(){
        Optional<Item> optional=itemRepository.findById(1L);
        Item item=optional.get();
    }

    // 批量增
    @Test
    void addList(){      
        List<Item> list=new ArrayList<>();
        list.add(new Item(2L,"男士牛仔库","name2"));
        list.add(new Item(3L,"女士上衣","name3"));
        itemRepository.saveAll(list);
    }

    // 批量查
    @Test
    void getAll(){
        Iterable<Item> items=itemRepository.findAll();        
        items.forEach(item -> System.out.println(item));
    }
}
  • ES的进阶使用

  • SpringData框架提供的基本增删改查方法并不能完全满足我们的业务需要,如果是针对当前Es数据,进行个性化的自定义查询,那还是需要自己编写查询代码

  • SpringData实现自定义查询,我们要编写遵循SpringData给定格式的方法名,SpringData会根据方法名称自动推断出查询目的,生成查询语句完成操作

  • query(查询):表达当前方法是一个查询方法,等价于sql中的select

  • Item/Items:确定要查询的实体类(对应的索引),不带s是查询单个对象的,带s的查集合

  • By(通过/根据):标识开始设置查询条件的单词,等价于sql中的where

  • Title:要查询的字段,可以是Item实体类中声明的任何字段

  • Matches(匹配):执行查询条件的操作,Matches是匹配字符串类型的关键字,支持分词等价sql中的like

  • OrderBy:排序

  • 多个条件之间要使用and或or来分隔,表示多个条件间的逻辑关系

  • 多个条件时,方法名就要按规则编写多个条件,参数也要对应条件的个数来变化,声明的参数会按照顺序赋值给方法名中的条件,和参数名称无关

  • 分页查询时返回值类型需要修改为Page类型,这个类型既可以保存从ES中查询到的数据又可以保存,当前分页查询的分页信息:当前页,总页数,总条数,每页条数,有没有上一页,有没有下一页等

  • 分页查询方法参数要添加一个Pageable,必须放在现有所有参数的后面, 它可以设置要查询的页码和每页的条数

  • SpringData也支持我们在代码中编写查询语句,以避免过长的方法名

@Repository
public interface ItemRepository extends ElasticsearchRepository<Item,Long> {
    //单条件查询
    Iterable<Item> queryItemsByTitleMatches(String title);
    //多条件查询
    Iterable<Item> queryItemsByTitleMatchesAndNameMatches(
                                    String title,String name);
    // 排序查询
    Iterable<Item> queryItemsByTitleMatchesOrNameMatchesOrderById(
                                    String title,String name);
    //分页查询
    Page<Item> queryItemsByTitleMatchesOrNameMatchesOrderById(
        String title, String brand, Pageable pageable);

    @Query("{\n" +
        "    \"bool\": {\n" +
        "      \"should\": [\n" +
        "        { \"match\": { \"name\": \"?0\"}},\n" +
        "        { \"match\": { \"title\": \"?0\"}},\n" +
        "        ]\n" +
        "     }\n" +
        "}")
    // 上面指定查询语句的情况下,方法的方法名就可以随意起名了,参数对应查询语句中的"?0"
    Iterable<SpuForElastic> querySearch(String keyword);
}

问题解决

  • 为了保持数据库中的数据和Elasticsearch中的数据一致,我们可以使用下面的办法

  • 在所有对spu表进行增删改的操作代码运行后,也对ES中的数据进行相同的操作,但是会有比较多的代码要编写,而且有比较明显的事务处理问题

  • 实际上业界使用Elasticsearch有一个组合叫ELK,其中L(logstash)可以实现自动同步数据库和ES的信息

Spring Data

  • Spring Data是Spring提供的一套连接各种第三方数据源的框架集。例如我们java代码需要使用socket访问ES,但是过于繁琐,我们可以使用SpringData框架简化

PageHelper

  • 我们可以使用sql语句中添加limit关键字的方法实现分页查询,但是查询分页内容时,我们要自己计算相关的分页信息和参数,PageHelper框架可以实现我们提供页码和每页条数,自动实现分页效果,收集分页信息

  • PageHelper的分页原理就是在程序运行时,在sql语句尾部添加limit关键字,并按照分页信息向limit后追加分页数据

  • PageHepler中虽然使用了静态方法设置了参数,但是最后用ThreadLocal把参数和当前线程绑定在了一起,所以是线程安全的,并且只生效于下一次的查询

引入框架

  • 在pom文件中添加依赖

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>

使用



public PageInfo<Test> test(Integer page,Integer pageSize){
    // Pagehepler框架实现分页功能最核心的代码,是要编写在执行查询数据库代码之前的
    // PageHelper.startPage方法就是在设置分页的查询条件
    // page是查询的页码(从1开始),pageSize是每页条数
    PageHelper.startPage(page,pageSize);
    // 上面设置好分页查询条件,下面的查询在执行时,sql语句会自动被追加limit关键字
    List<Order>  list= orderMapper.findAllOrders();

    // list变量并不是全查结果,而是只包含指定页码内容的数据
    // 我们分页业务功能不能只返回分页查询结果,还需要提供分页信息
    // PageHelper框架提供了PageInfo类,既能保存分页结果,也能保存分页信息
    // 分页信息无需我们计算,直接实例化PageInfo对象,它会自动根据上面的查询生成
    return new PageInfo<>(list);
}

Leaf

  • 源的一个分布式序列号(id)生成系统

  • 一个实际开发中常见的读写分离的数据库部署格式,专门进行数据更新(写)的有两个数据库节点,它们同时新增数据可能产生相同的自增列id,一旦生成相同的id,数据同步就会有问题,会产生id冲突,甚至引发异常,我们为了在这种多数据库节点的环境下能够产生唯一id,可以使用Leaf来生成

工作原理

  • Leaf底层支持通过"雪花算法"生成不同id,我们使用的是单纯的序列,要想使用,需要事先设置好leaf的起始值和缓存id数,举例,从1000开始缓存500,也就是从id1000~1499这些值,都会保存在Leaf的内存中,当有服务需要时,直接取出下一个值,取出过的值不会再次生成,同时因为是单线程获取,所以安全

  • leaf要想设置起始值和缓存数,需要给leaf创建一个指定格式的数据库表,运行过程中会从数据库表获取信息

使用

  • 在gitee中下载对应的Leaf的模块

静态资源服务器

  • 我们无论做什么项目,都会有一些页面中需要显示的静态资源,例如图片,视频文档等,我们一般会创建一个单独的项目,这个项目中保存静态资源,其他项目可以通过我们保存资源的路径访问

  • 使用静态资源服务器的原因是静态资源服务器可以将项目需要的所有图片统一管理起来,当其他模块需要图片时,可以从数据库中直接获得访问静态资源的路径即可,方便管理所有静态资源

  • Nginx就是比较好的静态资源服务器

Quartz

  • 是一个当今市面上流行的高效的任务调度管理工具,所谓"调度"就是制定好的什么时间做什么事情的计划

核心组件

  • job(工作\任务):Quartz 实现过程中是一个接口,接口中有一个方法execute(执行的意思),我们创建一个类,实现这个接口,在方法中编写要进行的操作(执行具体任务),我们还需要一个JobDetail的类型的对象,Quartz每次执行job时,会实例化job类型对象,去调用这个方法,JobDetail是用来描述Job实现类的静态信息,比如任务运行时在Quartz中的名称

  • Trigger(触发器):能够描述触发指定job的规则,分为简单触发和复杂触发

  • 简单触发可以使用SimplTrigger实现类.功能类似timer

  • 复杂触发可以使用CronTrigger实现类,内部利用cron表达式描述各种复杂的时间调度计划

  • Scheduler(调度器):一个可以规定哪个触发器绑定哪个job的容器,在调度器中保存全部的Quartz 保存的任务,SpringBoot框架下,添加Quartz依赖后,调度器由SpringBoot管理,我们不需要编写

Cron表达式

  • * 表示任何值,如果在分的字段上编写*,表示每分钟都会触发

  • , 是个分割符如果秒字段我想20秒和40秒时触发两次就写 20,40

  • - 表示一个区间 秒字段5-10 表示 5,6,7,8,9,10

  • / 表示递增触发 秒字段 5/10表示5秒开始每隔10秒触发一次,日字段编写1/3表示从每月1日起每隔3天触发一次

  • ? 表示不确定值, 因为我们在定日期时,一般确定日期就不确定是周几,相反确定周几时就不确定日期

  • L 表示last最后的意思,我们可以设置当月的最后一天,就会在日字段用L表示,周字段使用L表示本月的最后一个周几,一般会和1-7的数字组合,例如6L表示本月的最后一个周五

  • W (work)表示最近的工作日(单纯的周一到周五) 如果日字段编写15W表示,每月15日最近的工作日触发,如果15日是周六就14日触发,如果15日是周日就16日触发,LW通常一起使用,表示本月的最后一个工作日

  • # 表示第几个,只能使用在周字段上 6#3表示每月的第三个周五,如果#后面数字写大了,是一个不存在的日期,那就不运行了,适合设计在母亲节或父亲节这样的日期运行

使用

  • 在项目pom文件中导入依赖

<!--   SpringBoot 整合 Quartz依赖   -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
  • 创建一个QuartzJob的类,实现Job接口

@Slf4j
public class QuartzJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
       
    }
}
  • 创建一个QuartzConfig类,其中编写Trigger和JobDetail的调度绑定关系

@Configuration
public class QuartzConfig {

    @Bean
    public JobDetail showTime(){
        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!");
        // 利用JobBuilder类的newJob方法指定要运行的Job实现类的反射,我们编写的就是QuartzJob
        return JobBuilder.newJob(QuartzJob.class)
                // 需要为当前任务起个名字,方便Quartz调用
                .withIdentity("dateTime")
                // 默认情况下,JobDetail对象生成后,如果没有触发器绑定会被立即移除
                // 设置storeDurably方法后,当前JobDetail对象生成后,不会被移除了
                .storeDurably()
                .build();
    }

    // 它来设置上面JobDetail指定的任务的运行时机
    @Bean
    public Trigger showTimeTrigger(){
        System.out.println("+++++++++++++++++++");
        // 声明cron表达式,定义触发时间
        CronScheduleBuilder cron=
                CronScheduleBuilder.cronSchedule("0 40 10 * * ?");
        return TriggerBuilder.newTrigger()
                // 设置要绑定的jobDetail对象
                .forJob(showTime())
                // 也要给当前触发器对象起名字
                .withIdentity("dateTrigger")
                // 设置绑定cron表达式
                .withSchedule(cron)
                .bu0ild();
    }

}

消息队列

  • 消息队列(Message Queue)简称MQ,也称:"消息中间件",消息队列是采用"异步(两个微服务项目并不需要同时完成请求)"的方式来传递数据完成业务操作流程的业务处理方式.你可以把消息队列理解为一个使用队列来通信的组件。它的本质,就是个转发器,包含发消息、存消息、消费消息的过程

  • 消息队列的特点

  • 利用异步的特性,提高服务器的运行效率,减少因为远程调用出现的线程等待\阻塞时间

  • 削峰填谷:在并发峰值超过当前系统处理能力时,我们将没处理的信息保存在消息队列中,在后面出现的较闲的时间中去处理,直到所有数据依次处理完成,能够防止在并发峰值时短时间大量请求而导致的系统不稳定

  • 消息队列的延时:因为是异步执行,请求的发起者并不知道消息何时能处理完,如果业务不能接受这种延迟,就不要使用消息队列

  • 事务处理:当消息队列中(stock)发生异常时,在异常处理的代码中,我们可以向消息的发送者(order)发送消息,然后通知发送者(order)处理,消息的发送者(order)接收到消息后,一般要手写代码回滚,如果回滚代码过程中再发生异常,就又要思考回滚方式,如果一直用消息队列传递消息的话,可能发生异常的情况是无止境的,所以我们在处理消息队列异常时,经常会设置一个"死信队列",将无法处理的异常信息发送到这个队列中,死信队列没有任何处理者,通常情况下会有专人周期性的处理死信队列的消息

  • 生产者产生消息,发送带MQ服务器

  • MQ收到消息后,将消息持久化到存储系统。

  • MQ服务器返回ACK到生产者。

  • MQ服务器把消息push给消费者

  • 消费者消费完消息,响应ACK

  • MQ服务器收到ACK,认为消息消费成功,即在存储中删除消息。

使用场景

应用解耦
  • 举个常见业务场景:下单扣库存,用户下单后,订单系统去通知库存系统扣减。传统的做法就是订单系统直接调用库存系统

  • 如果库存系统无法访问,下单就会失败,订单和库存系统存在耦合关系

  • 如果业务又接入一个营销积分服务,那订单下游系统要扩充,如果未来接入越来越多的下游系统,那订单系统代码需要经常修改

  • 使用消息队列进行解耦

  • 订单系统:用户下单后,消息写入到消息队列,返回下单成功

  • 库存系统:订阅下单消息,获取下单信息,进行库存扣减操作

流量削峰
  • 流量削峰也是消息队列的常用场景。我们做秒杀实现的时候,需要避免流量暴涨,打垮应用系统的风险。可以在应用前面加入消息队列

  • 假设秒杀系统每秒最多可以处理2k个请求,每秒却有5k的请求过来,可以引入消息队列,秒杀系统每秒从消息队列拉2k请求处理得了。有些伙伴担心这样会出现消息积压的问题,

  • 首先秒杀活动不会每时每刻都那么多请求过来,高峰期过去后,积压的请求可以慢慢处理;

  • 其次,如果消息队列长度超过最大数量,可以直接抛弃用户请求或跳转到错误页面;

异步处理
  • 我们经常会遇到这样的业务场景:用户注册成功后,给它发个短信和发个邮件。如果注册信息入库是30ms,发短信、邮件也是30ms,三个动作串行执行的话,会比较耗时,响应90ms

  • 如果采用并行执行的方式,可以减少响应时间。注册信息入库后,同时异步发短信和邮件。如何实现异步呢,用消息队列即可,就是说,注册信息入库成功后,写入到消息队列(这个一般比较快,如只需要3ms),然后异步读取发邮件和短信。

消息通讯
  • 消息队列内置了高效的通信机制,可用于消息通讯。如实现点对点消息队列、聊天室等。

远程调用
  • 我们公司基于MQ,自研了远程调用框架

消息丢失

  • 生产者不丢消息

  • 存储端不丢消息

  • 消费者不丢消息

生产者保证不丢消息
  • 采用同步方式发送,send消息方法返回成功状态,就表示消息正常到达了存储端Broker。

  • 如果send消息异常或者返回非成功状态,可以重试。

  • 可以使用事务消息,RocketMQ的事务消息机制就是为了保证零丢失来设计的

存储端不丢消息
  • 需要把信息持久化,常用的是刷盘机制

  • 生产者消息发过来时,只有持久化到磁盘,RocketMQ的存储端Broker才返回一个成功的ACK响应,这就是同步刷盘。它保证消息不丢失,但是影响了性能。

  • 异步刷盘的话,只要消息写入PageCache缓存,就返回一个成功的ACK响应。这样提高了MQ的性能,但是如果这时候机器断电了,就会丢失消息。

  • Broker一般是集群部署的,有master主节点和slave从节点。消息到Broker存储端,只有主节点和从节点都写入成功,才反馈成功的ack给生产者。这就是同步复制,它保证了消息不丢失,但是降低了系统的吞吐量。与之对应的就是异步复制,只要消息写入主节点成功,就返回成功的ack,它速度快,但是会有性能问题。

消费者不丢消息
  • 消费者执行完业务逻辑,再反馈会Broker说消费成功,这样才可以保证消费阶段不丢消息

消息顺序

  • 消息的有序性,就是指可以按照消息的发送顺序来消费。主要是存在集群中会出现的现象,有些业务对消息的顺序是有要求的,比如先下单再付款,最后再完成订单。假设生产者先后产生了两条消息,分别是下单消息(M1),付款消息(M2),M1比M2先产生,需要保证M1比M2先被消费

  • 将M1和M2发往同一个消费者,且发送M1后,等到消费端ACK成功后,才发送M2就得了

  • 消息队列保证顺序性整体思路就是这样啦。比如Kafka的全局有序消息,就是这种思想的体现

重复消费

  • 出现原因

  • 生产端为了保证消息的可靠性,它可能往MQ服务器重复发送消息,直到拿到成功的ACK。

  • 再然后就是消费端,消费端消费消息一般是这个流程:拉取消息、业务逻辑处理、提交消费位移。假设业务逻辑处理完,事务提交了,但是需要更新消费位移时,消费者却挂了,这时候另一个消费者就会拉到重复消息了。

  • 幂等处理

  • 幂等处理重复消息,简单来说,就是搞个本地表,带唯一业务标记的,利用主键或者唯一性索引,每次处理业务,先校验一下就好啦。又或者用redis缓存下业务标记,每次看下是否处理过了。

消息积压

  • 消息积压是因为生产者的生产速度,大于消费者的消费速度。遇到消息积压问题时,我们需要先排查,是不是有bug产生了。如果不是bug,我们可以优化一下消费的逻辑,比如之前是一条一条消息消费处理的话,我们可以确认是不是可以优为批量处理消息。如果还是慢,我们可以考虑水平扩容,增加Topic的队列数,和消费组机器的数量,提升整体消费能力。

  • 如果是bug导致几百万消息持续积压几小时。有如何处理呢?需要解决bug,临时紧急扩容,大概思路如下:

  • 先修复consumer消费者的问题,以确保其恢复消费速度,然后将现有consumer 都停掉。

  • 新建一个 topic,partition 是原来的 10 倍,临时建立好原先10倍的queue 数量。

  • 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。

  • 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。

  • 等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。

技术选型

  • RabbitMQ是开源的,比较稳定的支持,活跃度也高,但是不是Java语言开发的。

  • 很多公司用RocketMQ,比较成熟,是阿里出品的。

  • 如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的。

高可用

  • 单机是没有高可用可言的,高可用都是对集群来说的,Kafka 的基础集群架构,由多个broker组成,每个broker都是一个节点

  • Kafka 0.8 之后,提供了复制品副本机制来保证高可用,即每个 partition 的数据都会同步到其它机器上,形成多个副本。然后所有的副本会选举一个 leader 出来,让leader去跟生产和消费者打交道,其他副本都是follower。写数据时,leader 负责把数据同步给所有的follower,读消息时, 直接读 leader 上的数据即可。如何保证高可用的?就是假设某个 broker 宕机,这个broker上的partition 在其他机器上都有副本的。如果挂的是leader的broker呢?其他follower会重新选一个leader出来。

数据一致性

  • 我们举个下订单的例子吧。订单系统创建完订单后,再发送消息给下游系统。如果订单创建成功,然后消息没有成功发送出去,下游系统就无法感知这个事情,出导致数据不一致。可以使用事务消息来保证数据的一致性

  • 生产者产生消息,发送一条半事务消息到MQ服务器

  • MQ收到消息后,将消息持久化到存储系统,这条消息的状态是待发送状态。

  • MQ服务器返回ACK确认到生产者,此时MQ不会触发消息推送事件

  • 生产者执行本地事务

  • 如果本地事务执行成功,即commit执行结果到MQ服务器;如果执行失败,发送rollback。

  • 如果是正常的commit,MQ服务器更新消息状态为可发送;如果是rollback,即删除消息。

  • 如果消息状态更新为可发送,则MQ服务器会push消息给消费者。消费者消费完就回ACK。

  • 如果MQ服务器长时间没有收到生产者的commit或者rollback,它会反查生产者,然后根据查询到的结果执行最终状态

Kafka

软件结构
  • Kafka是一个结构相对简单的消息队列(MQ)软件

  • Producer:消息的发送方,也就是消息的来源,Kafka中的生产者

  • Consumer:消息的接收方,也是消息的目标,Kafka中的消费者

  • Topic:话题或主题的意思,消息的收发双方要依据同一个话题名称,才不会将信息错发给别人

  • Record:消息记录,就是生产者和消费者传递的信息内容,保存在指定的Topic中

特点
  • Kafka作为消息队列,它和其他同类产品相比,突出的特点就是性能强大

  • Kafka将消息队列中的信息保存在硬盘中,Kafka对硬盘的读取规则进行优化后,效率能够接近内存,硬盘的优化规则主要依靠"顺序读写,零拷贝,日志压缩等技术"

  • Kafka处理队列中数据的默认设置:

  • Kafka队列信息能够一直向硬盘中保存(理论上没有大小限制)

  • Kafka默认队列中的信息保存7天,可以配置这个时间,缩短这个时间可以减少Kafka的磁盘消耗

安装配置启动
  • 必须将我们kafka软件的解压位置设置在一个根目录,文件夹名称尽量短(例如:kafka),然后路径不要有空格和中文

  • 修改zookeeper.properties配置

//kafaka数据保存的路径
dataDir=E:/data
  • 修改server.properties配置

//日志保存的路径
log.dirs=E:/data
  • 要想启动Kafka必须先启动Zookeeper,然后进入kafka-server-start.bat所在的文件路径启动终端cmd,输入指令

E:\kafka\bin\windows>kafka-server-start.bat ..\..\config\server.properties

使用
  • 项目中pom文件中添加依赖

<!-- Kafka 整合SpringBoot的依赖 -->
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>
<!--   这是google提供的java对象和json格式字符串相互转换的工具类依赖   -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
</dependency>
  • 修改application配置文件

spring:
  kafka:
    # 定义kafka的位置
    bootstrap-servers: localhost:9092
    consumer:
      group-id: test
  • consumer.group-id是kafka框架要求必须配置的内容,不配置启动会报错,意思是话题分组,目的是区分kafka服务器上不通项目消息的,本质上,这个分组名称会在消息发送时,自动前缀在话题名称前,例如当前项目发送了名为一个message的话题,真正发送到kafka的名称可能是test.message

  • 在项目的启动类application类中添加注解@EnableKafka

@SpringBootApplication
// 启动SpringBoot对kafka的支持
@EnableKafka
public class TestApplication {
    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }
}
  • 信息的发送

public class Producer {

    @Autowired
    private KafkaTemplate<String,String> kafkaTemplate;

    public void sendMessage(){   
        kafkaTemplate.send("message","发送了一个信息");
    }
}
  • KafkaTemplate<[话题类型],[消息类型]>

  • 信息的接收

public class Consumer {

    @KafkaListener(topics = "message")
    public void received(ConsumerRecord<String,String> record){
        String string=record.value();
    }
}
  • @KafkaListener(topics = "话题名称")

  • 方法的参数和返回值是指定的,不能修改

  • 方法参数类型必须是ConsumerRecord,泛型<[话题类型],[消息内容的类型]>,这个record就是消息发给者发来的消息,由监听器自动赋值,从消息对象中获得消息内容

RabbitMQ

  • RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。 AMQP :Advanced Message Queue,高级消息队列协议

特征
  • 可靠性(Reliability) RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。

  • 灵活的路由(Flexible Routing) 在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功能,RabbitMQ已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。

  • 消息集群(Clustering) 多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker

  • 高可用(Highly Available Queues) 队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。

  • 多种协议(Multi-protocol) RabbitMQ 支持多种消息队列协议,比如 STOMP、MQTT 等等。

  • 多语言客户端(Many Clients) RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、Ruby 等等。

  • 管理界面(Management UI) RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker 的许多方面。

  • 跟踪机制(Tracing) 如果消息异常,RabbitMQ 提供了消息跟踪机制,使用者可以找出发生了什么。

  • 插件机制(Plugin System) RabbitMQ 提供了许多插件,来从多方面进行扩展,也可以编写自己的插件。

结构
  • RabbitMQ是使用交换机\路由key指定要发送消息的队列,消息的发送者发送消息时,需要指定交换机和路由key名称,消息的接收方接收消息时,只需要指定队列的名称

安装启动
  • RabbitMQ是Erlang语言开发的,所以要先安装Erlang语言的运行环境,不要安装在中文路径和有空格的路径下,下载Erlang的官方路径

  • 下载RabbitMQ的官方网址,不要安装在中文路径和有空格的路径下

  • 要想运行RabbitMQ必须保证系统有Erlang的环境变量,配置Erlang环境变量,把安装Erlang的bin目录配置在环境变量Path的属性中

  • 进入到RabbitMQ的sbin路径下打开终端输入指令

  • E:\tools\rabbit\rabbitmq_server-3.10.1\sbin>rabbitmq-plugins enable rabbitmq_management

使用
  • 在pom文件中添加依赖

<!--  RabbitMQ的依赖   -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  • 在application文件中添加配置信息

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    # 设置虚拟host 单机模式下固定编写"/"即可
    virtual-host: /
  • 在Cinfig文件中配置交换机和路由key

@Configuration
public class RabbitMQConfig {
    
    public static final String STOCK_EX="stock_ex";
    public static final String STOCK_ROUT="stock_rout";
    public static final String STOCK_QUEUE="stock_queue";

    @Bean
    public DirectExchange stockDirectExchange(){
        return new DirectExchange(STOCK_EX);
    }
    @Bean
    public Queue stockQueue(){
        return new Queue(STOCK_QUEUE);
    }
    
    @Bean
    public Binding stockBinding(){
        return BindingBuilder.bind(stockQueue()).
                        to(stockDirectExchange()).with(STOCK_ROUT);
    }
}
  • 交换机和队列是实际对象,而路由key是绑定它俩关系的对象,他们都需要保存到Spring容器中管理

  • 一般在使用前,会将所有需要使用到的交换机,路由key和队列的名称声明为常量

  • DirectExchange:交换机对象

  • Queue:队列对象

  • Binding:交换机,路由Key和队列绑定

  • 发送信息

public class RabbitMQProducer{

    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public void test(){             
        rabbitTemplate.convertAndSend(
                RabbitMQConfig.STOCK_EX,
                RabbitMQConfig.STOCK_ROUT,
                "test RabbitMQ");
    }
}
  • RabbitTemplate:RabbitMQ的操作对象

  • convertAndSend([交换机名称],[路由key名称],[要发送的对象])

  • 接收信息

@Component
@RabbitListener(queues = RabbitMQConfig.STOCK_QUEUE)
@Slf4j
public class RabbitMQConsumer {

    @RabbitHandler
    public void process(String test){        
        log.info("消息接收完成!!!:{}",test);
    }


}
  • @RabbitListener(queues = RabbitMQConfig.STOCK_QUEUE):和Kafka不同,RabbitMQ监听器的注解要写在类上

  • @RabbitHandler:在类上标记的监听器注解,不能明确接收到消息时运行哪个方法,所以我们要在类中定义一个专门处理消息的方法,并使用@RabbitHandler注解标记,每个类只允许一个方法标记这个注解

Zookeeper

  • 在微服务的模块中,如果这些模块在Linux中需要修改配置信息的话,就需要进入这个模块,去修改配置,每个模块都需要单独修改配置的话,工作量很大,我们使用Zookeeper之后,可以创建一个新的管理各种软件配置的文件管理系统.

  • Linux系统中各个软件的配置文件集中到Zookeeper中,实现在Zookeeper中,可以修改服务器系统中的各个软件配置信息,长此以往,很多软件就删除了自己写配置文件的功能,而直接从Zookeeper中获取

启动

  • 进入zookeeper-server-start.bat所在的文件路径,启动终端cmd,输入指令

E:\kafka\bin\windows>zookeeper-server-start.bat ..\..\config\zookeeper.properties

虚拟机

  • 所谓的虚拟机,就是在当前计算机系统中,又开启了一个虚拟系统,这个虚拟系统,我们要安装Linux系统,我们开发的java项目最终也都会运行在Linux系统上,开发使用windows

安装配置

  • 首先检查自己计算机的虚拟化状态是否已经启动

  • 安装虚拟机:VMware/VirtualBox

  • 配置虚拟机网络,如果共享中出现下拉框,一定要选择Virtualbox的网卡选项

  • 加载虚拟镜像,双击蓝色图标,会自动开启virtualbox虚拟机,并加载当前镜像,必须保证当前镜像文件所在全部路径都没有中文

  • 配置镜像参数,桥接的网卡必须是具备网络连接的网卡

  • 启动,开机后如果鼠标被虚拟机捕获,使用右侧Ctrl键解除,尝试连接网络指令:ping www.baidu.com,如果有周期响应,证明网络畅通,虚拟机可以使用当前计算机的网络功能

  • 为了方便操作虚拟机,可以下载一个客户端软件链接Linux,例如Bitvise SSH Client软件

Linux指令

  • sudo su -:切换到root用户

  • passwd:设置当前用户密码

  • ifconfig:查看ip地址

  • ifconfig | more :查看ip地址,逐行显示

  • yum install -y yum-utils:实现更方便的安装"应用商店"中提供的程序

  • systemctl stop firewalld:关闭防火墙。如果当前windows系统要连接Linux中的资源,一般都要关闭Linux的防火墙,实际开发中,不会彻底关闭防火墙,而是开放指定的端口号

Linux部署java项目

  • 安装java环境,yum安装好的java会自动配置环境变量:yum install java

  • 验证是否安装java成功:java -version

  • 打包项目成jar包,打包成功后在项目的target目录下

  • 通过工具把jar包复制到Linux系统中,同时执行运行命令:java -jar [jar包名称]

Docker

  • Docker是一个用来开发、运输和运行应用程序的开放平台。使用Docker可以将应用程序与基础结构分离,以便快速交付软件。使用Docker,您可以以管理应用程序的方式管理基础架构。通过利用Docker的方法快速传送、测试和部署代码,可以显著减少编写代码和在生产中运行代码之间的延迟

  • 更快速的应用交付和部署:

  • 传统的应用开发完成后,需要提供一堆安装程序和配置说明文档,安装部署后需根据配置文档进行繁杂的配置才能正常运行。Docker化之后只需要交付少量容器镜像文件,在正式生产环境加载镜像并运行即可,应用安装配置在镜像里已经内置好,大大节省部署配置和测试验证时间。

  • 更便捷的升级和扩缩容:

  • 随着微服务架构和Docker的发展,大量的应用会通过微服务方式架构,应用的开发构建将变成搭乐高积木一样,每个Docker容器将变成一块“积木”,应用的升级将变得非常容易。当现有的容器不足以支撑业务处理时,可通过镜像运行新的容器进行快速扩容,使应用系统的扩容从原先的天级变成分钟级甚至秒级。

  • 更简单的系统运维:

  • 应用容器化运行后,生产环境运行的应用可与开发、测试环境的应用高度一致,容器会将应用程序相关的环境和状态完全封装起来,不会因为底层基础架构和操作系统的不一致性给应用带来影响,产生新的BUG。当出现程序异常时,也可以通过测试环境的相同容器进行快速定位和修复。

  • 更高效的计算资源利用:

  • Docker是内核级虚拟化,其不像传统的虚拟化技术一样需要额外的Hypervisor [管理程序] 支持,所以在一台物理机上可以运行很多个容器实例,可大大提升物理服务器的CPU和内存的利用率。

名词解释

  • 容器(container)

  • 首先需要了解什么是容器,容器就是一个进程,内部是独立运行的一个或者是一组应用。它可以被启动、开始、停止、删除。每个容器都是相互隔离的,保证安全的平台。

  • 镜像(image)

  • 镜像(Image)就是一个只读的模板文件。镜像可以用来创建 Docker 容器,一个镜像可以创建很多容器。 就好似 Java 中的 类和对象,类就是镜像,对象就是容器!也可以把镜像看成是模具,而镜像创建出来的容器就是通过这个模具创建的一个一个的实际产品。

  • 宿主机(host)

  • 宿主机就是我们调用命令使用镜像创建容器的服务器(linux)。

  • 镜像仓库(repository)

  • 一个用来容纳多个镜像的仓库,可以链接仓库获取你想要的内部镜像,一般一个镜像仓库中包含多个不同tag的镜像。

  • 镜像服务器(registry)

  • 镜像仓库占用的服务器,这里注意一个镜像服务器未必只有一个仓库,可以有很多仓库,每个仓库又保管的是不同镜像。

  • 客户端(docker-client)

  • 调用docker命令,操作镜像,容器的进程。只要能链接宿主机,操作docker的进程都是docker-client。

安装

  • 设置docker仓库,并且从仓库安装所需内容。

  • 先安装yum-utils包,实现更方便的安装"应用商店"中提供的程序:yum install -y yum-utils

  • 指定docker仓库路径,这里使用的是阿里仓库地址

yum-config-manager \
    --add-repo \
    http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
  • 执行安装Docker:yum -y install docker-ce docker-ce-cli containerd.io

  • 启动命令:systemctl start docker

  • 测试是否安装成功,Docker提供了一个专门测试Docker功能的镜像命令:docker run hello-world

命令

  • systemctl start docker:启动docker

  • docker 子命令 [选项]:Docker命令的语法结构

  • docker --help:docker都有哪些子命令呢,我们可以使用docker的helper子命令查看

  • docker 子命令 --help:如果想查询具体的子命令的使用方式

  • docker run --help:启动docker容器的run的相关帮助可以

  • docker images:主要能够完成查看当前本地镜像仓库的功能

  • 这个命令的返回结果显示:

  • REPOSITORY:镜像仓库名,也叫作镜像名。

  • TAG:标签,常用版本号标识仓库,如果是latest就是最新版本。

  • IMAGE ID:镜像id。

  • CREATED:创建镜像时间。

  • SIZE:大小。

  • docker images命令的常用选项如下

  • -a: 显示所有信息

  • -q: 只显示镜像id,在镜像较多的时候比较常用

  • docker search [镜像名称关键字]:先明确正确的镜像名称,我们可以输入查询关键字,对镜像仓库进行搜索

  • 这个命令的返回结果显示:

  • NAME:镜像名称。

  • DESCRIPTION:镜像描述。

  • STARS:镜像星级,越高表示越热,使用人越多。

  • OFFICIAL:是否官方镜像。

  • AUTOMATED:是否支持自动化部署。

  • docker pull [镜像名称]:[版本号]:我们需要的软件拉取到本地仓库了,默认下载最新版本,如果要指定版本号可以在名称后指定,不需要指定版本号的可以省略

  • docker rmi [镜像名]:删除没有运行的景象

  • -f:强制删除这个镜像,无论是否正在使用。

  • docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -v /root/data:/var/lib/mysql mysql:5.7.35:启动景象

  • --name mysql:该容器启动后的名字:(自定义命名)如果没有设置,系统会自动设置一个,毕竟如果开启太多的容器,记不住就很尴尬,建议加上见名知意。

  • -d 代表后台启动该服务

  • -p 3306(这是liunx的端口号,我习惯说成宿主机,如果我们想要远程服务的话,访问的端口就是这个端口):3306(docker容器的端口,每一个容器都是独立的,可理解成操作系统层面的系统),访问这个端口就是先通过远程访问宿主机的端口,再映射到docker容器的端口访问mysql。

  • -e MYSQL_ROOT_PASSWORD=123456 这是说mysql启动需要的开机密码,默认的账号是root ,密码就是上面设置的:123456

  • -v /root/data:/var/lib/mysql /root/data/:这是宿主机的数据存放路径(你也可以自定义), /var/lib/mysql:这是mysql容器存放数据的地方。也是为了同步数据,防止,容器被删除以后,数据就不存在了。

  • 启动成功后就返回一个容器ID

  • docker ps:可以查看当前docker中运行的所有容器的状态

  • 参数

  • -a:显示所有容器,如果不加只显示正在启动运行的容器,停止的不会显示。

  • -l:显示最近的启动创建的容器。

  • -n=[数字]:显示最近n个容器。

  • -q:只显示容器id。经常和-a一起使用,获得当前宿主机所有容器id参数集合。

  • 属性

  • container id:容器id,很多操作容器命令都需要用到的参数。

  • image:容器创建使用的镜像。

  • command:容器中在运行的进程或者命令。

  • created:创建时间。

  • status:容器状态。

  • ports:容器的端口映射情况,这里没有用到端口。

  • names:容器的名字,启动没有指定--name选项,会默认使用一个名字。

  • docker stop [容器id]:stop只是停止容器.并不会删除容器

  • docker rm [容器id]:这里rm删除的是容器,不是本地镜像,和rmi命令要区分

  • -f:强制删除容器,无论是否运行都可以删除该容器,如果不加,运行的容器无法删除。

布隆过滤器

  • 布隆过滤器能够实现使用较少的空间来判断一个指定的元素是否包含在一个集合中

  • 布隆过滤器并不保存这些数据,所以只能判断是否存在,而并不能取出该元素

  • 检查一个元素是否在一个集合中的思路是遍历集合,判断元素是否相等,这样的查询效率非常低下,Hash散列或类似算法可以保证高效判断元素是否存在,但是消耗内存较多,所以我们使用布隆过滤器实现,高效判断是否存在的同时,还能节省内存的效果,但是布隆过滤器的算法天生会有误判情况,需要能够容忍,才能使用

原理

  • 我们向布隆过滤器中保存一个数据的时候,我们使用3个hash算法,找到布隆过滤器的位置,让这三个位置从原来的0变成1,例如我们存入下列两个单词

  • 当再存入第三个数据的时候,有可能跟全面的重叠,误判就是这样导致的

  • 过短的布隆过滤器如果保存了很多的数据,可能造成二进制位置值都是1的情况,一旦发送这种情况,布隆过滤器就会判断任何元素都在当前集合中,布隆过滤器也就失效了,布隆过滤器误判的效果:

  • 布隆过滤器判断不存在的,一定不在集合中

  • 布隆过滤器判断存在的,有可能不在集合中

  • 优点

  • 空间效率和查询效率⾼

  • 缺点

  • 有⼀定误判率即可(可以控制在可接受范围内)。

  • 删除元素困难(不能将该元素hash算法结果位置修改为0,因为可能会影响其他元素)

  • 极端情况下,如果布隆过滤器所有位置都是1,那么任何元素都会被判断为存在于集合中

设计布隆过滤器

  • 我们在启动布隆过滤器时,需要给它分配一个合理大小的内存,这个大小应该满足

  • 内存占用在一个可接受范围

  • 不能有太高的误判率(<1%)

  • 内存约节省,误判率越高,内存越大,误判率越低

使用

  • 如果在Linux下已经安装了Redis,这个redis是一个特殊版本的Redis,这个版本内置了操作布隆过滤器的lua脚本,支持布隆过滤的方法,我们可以直接使用,实现布隆过滤器

  • 项目的pom文件添加依赖

<!--   redis依赖   -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 项目的application文件添加配置

spring:
  redis:
    host: 192.168.137.150
    port: 6379
    password:
  • 把布隆过滤器封装成一个工具类

@Component
public class RedisBloomUtils {
    @Autowired
    private StringRedisTemplate redisTemplate;
    private static RedisScript<Boolean> bfreserveScript = new DefaultRedisScript<>("return redis.call('bf.reserve', KEYS[1], ARGV[1], ARGV[2])", Boolean.class);

    private static RedisScript<Boolean> bfaddScript = new DefaultRedisScript<>("return redis.call('bf.add', KEYS[1], ARGV[1])", Boolean.class);

    private static RedisScript<Boolean> bfexistsScript = new DefaultRedisScript<>("return redis.call('bf.exists', KEYS[1], ARGV[1])", Boolean.class);

    private static String bfmaddScript = "return redis.call('bf.madd', KEYS[1], %s)";

    private static String bfmexistsScript = "return redis.call('bf.mexists', KEYS[1], %s)";

    public Boolean hasBloomFilter(String key){
        return redisTemplate.hasKey(key);
    }
    /**
     * 设置错误率和大小(需要在添加元素前调用,若已存在元素,则会报错)
     * 错误率越低,需要的空间越大
     * @param key
     * @param errorRate 错误率,默认0.01
     * @param initialSize 默认100,预计放入的元素数量,当实际数量超出这个数值时,误判率会上升,尽量估计一个准确数值再加上一定的冗余空间
     * @return
     */
    public Boolean bfreserve(String key, double errorRate, int initialSize){
        return redisTemplate.execute(bfreserveScript, Arrays.asList(key), String.valueOf(errorRate), String.valueOf(initialSize));
    }

    /**
     * 添加元素
     * @param key
     * @param value
     * @return true表示添加成功,false表示添加失败(存在时会返回false)
     */
    public Boolean bfadd(String key, String value){
        return redisTemplate.execute(bfaddScript, Arrays.asList(key), value);
    }

    /**
     * 查看元素是否存在(判断为存在时有可能是误判,不存在是一定不存在)
     * @param key
     * @param value
     * @return true表示存在,false表示不存在
     */
    public Boolean bfexists(String key, String value){
        return redisTemplate.execute(bfexistsScript, Arrays.asList(key), value);
    }

    /**
     * 批量添加元素
     * @param key
     * @param values
     * @return 按序 1表示添加成功,0表示添加失败
     */
    public List<Integer> bfmadd(String key, String... values){
        return (List<Integer>)redisTemplate.execute(this.generateScript(bfmaddScript, values), Arrays.asList(key), values);
    }

    /**
     * 批量检查元素是否存在(判断为存在时有可能是误判,不存在是一定不存在)
     * @param key
     * @param values
     * @return 按序 1表示存在,0表示不存在
     */
    public List<Integer> bfmexists(String key, String... values){
        return (List<Integer>)redisTemplate.execute(this.generateScript(bfmexistsScript, values), Arrays.asList(key), values);
    }

    private RedisScript<List> generateScript(String script, String[] values) {
        StringBuilder sb = new StringBuilder();
        for(int i = 1; i <= values.length; i ++){
            if(i != 1){
                sb.append(",");
            }
            sb.append("ARGV[").append(i).append("]");
        }
        return new DefaultRedisScript<>(String.format(script, sb.toString()), List.class);
    }

}
  • 要把数据添加到布隆过滤器中和查询元素是否存在,只需要调用上面的工具类里面的方法就可以

ELK

  • ELK是当今业界非常流行的日志采集保存和查询的系统,我们编写的程序,会有很多日志信息,但是日志信息的保存和查询是一个问题,所以我们使用ELK来保存

  • Elasticsearch负责将日志信息保存,查询时可以按关键字快速查询,利用logstash这个软件可以监听一个文件,将这个文件中出现的内容经过处理发送到指定端口,我们就可以监听我们程序输出的日志文件,然后将新增的日志信息保存到ES中,Kibana来负责进行查询和查看结果

  • E:Elasticsearch 全文搜索引擎

  • L:logstash 日志采集工具

  • K:Kibana ES的可视化工具

Logstash

  • Logstash是一款开源的日志采集,处理,输出的软件,每秒可以处理数以万计条数据,可以同时从多个来源采集数例如数据库,redis,java的日志文件均可据,转换数据,然后将数据输出至自己喜欢的存储库中(官方推荐的存储库为Elasticsearch)

  • LogStash内部有3个处理数据的步骤

  • input 将数据源的数据采集到Logstash

  • filter (非必要)如果需要可以对采集到的数据进行处理

  • output 将处理好的数据保存到目标(一般就是ES)

  • logstash还有一个非常常见的用法,就是能够自动完成数据库数据和ES中数据的同步问题,实现原理,我们可以配置logstash监听数据库中的某个表,一般设计为监听表中数据的变化,在规范的数据表结构中,logstash可能监听gmt_modified列,只要gmt_modified列数据有变化,就收集变化的数据行,将这行数据的信息更新到ES,实现步骤:

  • 配置Logstash监听数据库表的gmt_modified和gmt_create,当新增数据和数据修改的时候,Logstash会自动收集信息更新到ES中

  • 编写用于跟ES链接的实体类,类中没有任何编写分词的属性,原因是为了更高效的实现分词,logstash将所有需要分词的列拼接组合成了一个新列search_text,当需要查询时只需要查询search_text字段即可,并且Logstash在把search_text存入ES的时候会自动对这一列进行分词

@Data
@Document(indexName = "test")
public class Test implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 记录id
     */
    @Id
    @ApiModelProperty(value="id")
    private Long id;

    /**
     * 标题
     */
    @Field(name="title")
    @ApiModelProperty(value="标题")
    private String title;

    /**
     * 数据创建时间
     */
    @Field(name="gmt_create")
    @ApiModelProperty(value="数据创建时间")
    private LocalDateTime gmtCreate;

    /**
     * 数据最后修改时间
     */
    @Field(name="gmt_modified")
    @ApiModelProperty(value="数据最后修改时间")
    private LocalDateTime gmtModified;
}
  • 编写持久层的代码,根据用户输入的关键字,查询ES中匹配的数据,因为Logstash将所有数据的信息中需要分词的字段,拼接成了一个search_text字段然后Test实体类中没有创建searchText字段,所有只能通过查询语句完成

@Repository
public interface TestRepository extends
                                ElasticsearchRepository<Test,Long> {
    @Query("{\"match\":{\"search_text\":{\"query\":\"?0\"}}}")
    Page<SpuEntity> querySearchByText(String keyword, Pageable pageable);

}
  • 当我们插入新数据或者更新数据的时候Logstash会自动更新到ES中,但是不是事实更新的,可能需要几分钟才会同步一次,这样我们在程序中,就无需编写任何同步ES和数据库的代码

RestTemplate

  • Dubbo功能也有限制,如果我们想调用的方法不是我们当前项目的组件或功能,甚至想调用的方法不是java编写的,那么Dubbo就无能为力了,我们可以使用RestTemplate来调用任何语言编写的公开的Rest路径,也就是只要能够使用浏览器访问的路径,我们都可以使用RestTemplate发送请求,接收响应

使用

  • 生成RestTemplate的Bean对象,需要启动负载均衡的注解,因为Dubbo自带负载均衡,但是RestTemplate是代替Dubbo的,需要单独设置。使用了@LoadBalanced注解底层会通过Nacos的api获取到服务列表,这个列表每30秒回更新一次,然后在请求发送前经过拦截器的时候对用户请求做负载均衡

@SpringBootApplication
public class WebapiApplication {

    public static void main(String[] args) {
        SpringApplication.run(WebapiApplication.class, args);
    }

    @Bean   
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
  • RestTemplate调用时请求以get方法居多,post方法调用代码比较繁琐

public class Test{

    @Autowired
    private RestTemplate restTemplate;

    public void test(){
    
    String url="http://localhost:20003/?commodityCode={1}";

    String result = restTemplate.getForObject(url,String.class,commodityCode, 5);
    }
}
  • getForObject方法参数和返回值的解释

  • 返回值:根据调用的控制器方法的实际返回值给定返回类型即可

  • 参数分为3部分

  • 第一个参数:就是请求的url,指定路径即可

  • 第二个参数:就是返回值类型的反射,根据要求编写在参数位置即可

  • 从第三个参数开始:往后的每个参数都是在给url路径中的{x}占位符赋值

  • 第三个参数赋值给{1} 第四个参数赋值给{2},....以此类推

Nginx

  • Nginx ("engine x") 是一个高性能的 HTTP 和 反向代理 服务器,也是一个IMAP/POP3/SMTP 代理服务器。

  • 高并发响应性能非常好,官方 Nginx 处理静态文件 5万/秒

  • 反向代理性能非常强。(可用于负载均衡)

  • 内存和 cpu 占用率低。(为 Apache(也是一个服务器) 的 1/5-1/10)

原理

  • Nginx使用NIO来实现,是它能快速的主要原因之一.从Nginx内部的结构上和运行流程上,它内部是一个主进程(Master)多个工作进程(Worker),Master负责统筹管理配置和Worker的分工,Worker来负责处理请求,作出响应,而且使用NIO既非阻塞式的,异步的来完成工作

  • 简单来说,就是一个Worker接到从Master分配来的一个请求后,会立即对请求进行处理,但是在请求发送完成后,还没有返回响应前,Worker会继续处理别的请求,直到返回响应时,这个Worker才会去处理响应,最终每条Worker进程全程无阻塞

  • 正向代理,当我们访问的目标服务器无法连通时,可以借助代理服务器,间接访问该目标服务器

  • 反向代理,请求反向代理服务器的特点是,我们请求的是代理服务器的地址,真正提供服务的服务器地址我们不需要知道,这样做的好处是反向代理服务器后可能是一个服务器集群,方便负载均衡

使用场景

  • 因为Nginx优秀的静态内容并发性能,我们常常使用它做静态资源服务器,在Nginx中保存图片,文件视频等静态资源,经常和FastDFS组合使用

  • FastDFS是一个开源的轻量级分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。特别适合以文件为载体的在线服务,如相册网站、视频网站等等。

启动

  • 在Docker下容器的创建和启动

sudo docker run -p 80:80 --restart always --name nginx \
-v /usr/local/docker/nginx/:/etc/nginx/ \
-v /usr/local/docker/nginx/conf.d:/etc/nginx/conf.d \
-d nginx
  • Nginx的启动

docker start nginx
docker restart nginx
docker stop nginx
docker exec -it nginx bash
nginx -v # 查看 nginx 版本 (docker 中需要在容器内部执行)
service nginx reload 重新加载配置文件(docker 中需要在容器内部执行)
  • 核心配置文件

user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
 worker_connections 1024;
}
http {
 include /etc/nginx/mime.types;
 default_type application/octet-stream;
 log_format main '$remote_addr - $remote_user [$time_local] 
"$request" '
 '$status $body_bytes_sent "$http_referer" '
 '"$http_user_agent" "$http_x_forwarded_for"';
 access_log /var/log/nginx/access.log main;
 sendfile on;
 #tcp_nopush on;
 keepalive_timeout 65;
 #gzip on;
 include /etc/nginx/conf.d/*.conf; #嵌套
}
  • 其中,nginx 的配置有三部分构成,在 docker 环境中 nginx 采用了嵌套加载方式,即主 配 置 在 /etc/nginx/nginx.conf 中 , 然 而 平 时 用 到 的 server 配置在/etc/nginx/conf.d 中,在主配置中见 include 指令部分,在 conf.d 目录下默认会有一个 default.conf 文件,这部分配置文件就是基本的 server 配置。无论采用怎样的配置方式,nginx.conf 都只有这三部分构成,例如:

  • 全局块:配置文件开始到 events 中间的部分内容,主要是结合硬件资源进行配置

  • events 块:这块主要是网络配置相关内容,硬件性能好,连接数可以配置更多

  • http 块:nginx 配置中最核心部分,可以配置请求转发,负载均衡等。

负载均衡

  • 轮询策略,默认的方式.打开 conf.d 目录下 default.conf 文件,进行配置

upstream gateways{
server 192.168.174.130:8901;
server 192.168.174.130:8902;
server 192.168.174.130:8903;
}
  • 权重策略.打开 conf.d 目录下 default.conf 文件,进行配置

upstream gateways{
server 192.168.174.130:8901 weight=1;
server 192.168.174.130:8902 weight=2;
server 192.168.174.130:8903 weight=6;
}

常用属性

  • BACKUP属性.备用机设置,正常情况下该服务器不会被访问.当主机全部宕机或者主机遇忙时,该服务器才会访问

  • max_fails属性: 最大的失败次数,fail_timeout属性:设定周期为 60 秒.当服务器宕机时,如果访问时的失败次数达到最大失败次数,则标识为down.自动完成.在一定的周期之内,如果服务器恢复正常,则还会尝试访问故障机.

  • Down 属性.如果服务器宕机,可以在配置文件中标识为 down.这样以后不会再访问故障机

upstream geteways {
server 192.168.227.131:10001 down;
server 192.168.227.131:10002 max_fails=1 fail_timeout=60s;
server 192.168.227.131:10003 backup;
}

Nginx和Gateway的区别

  • 首先明确Nginx和Gateway并不冲突,他们都是统一入口的概念,它们可以同时开启,也可以开启其中一个,只不过Nginx不属于java程序(不属于微服务模块),而Gateway是java程序,而且是微服务的一部分Nginx是服务器程序我们不可编辑,Gateway是我们自己创建的项目,依赖和配置都由我们自己完成,最终如果想做反向代理服务器,就使用Nginx,如果是微服务项目的网关就是Gateway

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值