Elasticsearch

0.使用场景

数据库的模糊搜索功能单一,匹配条件非常苛刻,必须恰好包含用户搜索的关键字。

而在搜索引擎中,用户输入出现个别错字,或者用拼音搜索、同义词搜索都能正确匹配到数据。

综上,在面临海量数据的搜索,或者有一些复杂搜索需求的时候,推荐使用专门的搜索引擎来实现搜索功能。

image-20240517164744539

0.1 全文搜索

Elasticsearch 凭借其强大、可扩展和快速的搜索功能,在全文搜索场景中表现出色。

它通常用于支持大型网站和应用程序的搜索功能,允许用户执行复杂的查询并获得近乎实时的响应

0.2 实时分析

Elasticsearch 能够实时执行分析,因此适用于跟踪实时数据(如用户活动、交易或传感器输出)的仪表盘。这种能力使企业能够根据最新信息及时做出决策。

0.3 机器学习

通过在 X-Pack 中添加机器学习功能,Elasticsearch 可以自动检测数据中的异常、模式和趋势。这对于预测分析、个性化和提高运营效率非常有用。

0.4 地理数据应用

Elasticsearch 通过地理空间索引和搜索功能支持地理数据。这对于需要管理和可视化地理信息(如地图和基于位置的服务)的应用非常有用,使执行邻近搜索和基于位置的数据可视化成为可能。

0.5 日志和事件数据分析

企业使用 Elasticsearch 聚合、监控和分析各种来源的日志和事件数据。它是 ELK 堆栈(Elasticsearch、Logstash、Kibana)的关键组件,该堆栈常用于管理系统和应用程序日志,以发现问题并监控系统健康状况。

0.6 安全信息和事件管理(SIEM)

Elasticsearch 可用作 SIEM 的工具,帮助企业实时分析安全事件。这对于检测、分析和响应安全事件和漏洞至关重要。

上述每个用例都利用了 Elasticsearch 的优势(如可扩展性、速度和灵活性)来处理不同的数据类型和复杂的查询,为数据驱动型应用提供了重要价值。

1.Elasticsearch(ES)

ES是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容。

Elasticsearch结合Kibana,Logstash,beats是一整套技术栈,被叫做==ELK==。经常用来做日志收集、系统监控和状态分析等等:

image-20240430171212212

1.1 安装

1.1.1 安装elasticsearch

通过下面的Docker命令即可安装单机版本的elasticsearch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#先在tar所在目录下打开cmd
docker load -i es.tar

#创建一个网络【不然kibana不能连接es,踩坑了!!】
docker network create elastic

#黑马安装:
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \ #配置jvm的内存
-e "discovery.type=single-node" \ #配置运行模式【单点模式/集群模式】
-v es-data:/usr/share/elasticsearch/data \ #挂载
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network hm-net \
-p 9200:9200 \ #访问http端口
-p 9300:9300 \ #集群使用
elasticsearch:7.12.1

#csdn安装:
docker run -d --name es -e ES_JAVA_OPTS="-Xms512m -Xmx512m" -e "discovery.type=single-node" --privileged --network elastic -p 9200:9200 -p 9300:9300 elasticsearch:7.12.1

启动之后访问http://localhost:9200/就可以看到elasticsearch信息:

image-20240507204417602

1.1.2 安装Kibana

通过下面的Docker命令,即可部署Kibana:

1
2
3
4
5
6
7
8
9
10
11
12
13
#先在tar所在目录下打开cmd
docker load -i kibana.tar

#黑马安装:
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \ #es的地址,这里的es要和es配置docker的时候--name一致
--network=hm-net \ #网络和es一个网络
-p 5601:5601 \
kibana:7.12.1 #要保证和es版本一致!!!

#csdn安装:
docker run -d --name kibana -e ELASTICSEARCH_HOSTS=http://es:9200 --network elastic -p 5601:5601 kibana:7.12.1

启动之后访问http://localhost:5601/就可以通过kibana数据化访问elasticsearch:

image-20240507204635028

可以点击右上角Dev tools,进入开发工具页面:

image-20240507204914788

点击之后:

image-20240507205135009

2.倒排索引

elasticsearch的高性能搜索表现,因为底层的倒排索引技术解决的就是根据==部分词条模糊匹配==的问题。【Innodb底层就是用倒排索引做的全文索引】

2.1 正向索引(精准匹配)

我们有一张名为tb_goods的表:

image-20240507210036497

其中,id字段已经创建了索引(底层使用b+树)所以根据id搜索的速度会非常快。但是其他字段例如title只在叶子结点上存在。

比如用户的sql语句为:

1
2
3
select *
from tb_goods
where title like '%手机%';

搜索大概流程如图:

image-20240507212400849

说明:

  • 1)检查到搜索条件为like '%手机%',需要找到title中包含手机的数据
  • 2)逐条遍历每行数据(每个叶子节点),比如第1次拿到id为1的数据
  • 3)判断数据中的title字段值是否符合条件
  • 4)如果符合则放入结果集,不符合则丢弃
  • 5)回到步骤1

综上,根据id搜索条件为精确匹配时,可以走索引,查询效率较高。而当搜索条件为模糊匹配时,由于索引无法生效,导致从索引查询退化为全表扫描,效率很差。

因此,正向索引适合于根据索引字段的精确搜索,不适合基于部分词条的模糊匹配。

而倒排索引恰好解决的就是根据部分词条模糊匹配的问题。

2.2 倒排索引(模糊匹配)

2.2.1 基本概念

倒排索引中两个重要的概念:

  • 文档(Document):用来搜索的数据,每一条数据就是一个文档【一个网页,一个商品信息】

  • 词条(Term):对文档数据/用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条【我是中国人,就可以分为:我,是,中国人,国人,人这几个词条】

2.2.2 创建流程

创建倒排索引是对正向索引的一种特殊处理和应用,流程如下:

  • 将每个文档的数据利用分词算法根据语义拆分得到一个个词条

  • 创建表,表中每行数据:{词条,词条所在文档id,词条位置}

  • 因为词条唯一性,可以给词条创建正向索引(唯一索引)

此时形成的这张以词条为索引的表就是倒排索引表:

image-20240507214322180

2.2.3 搜索流程

以搜索”华为手机”为例,如图:

image-20240507215033610

流程描述:

1)用户输入条件"华为手机"进行搜索。

2)对用户输入条件分词,得到词条:华为手机

3)拿着词条在倒排索引中查找(由于词条有唯一索引,查询效率很高),即可得到包含词条的文档id:1、2、3

4)拿着文档id到正向索引中查找具体文档即可(由于id也有索引,查询效率也很高)

==根据条件先分词,每个词条去倒排索引查询【词条有唯一索引】找到对应文档id,根据文档id到正向索引【id有索引】查询具体文档(一条数据)==

2.2.4 两者对比

  • 正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程
  • 倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程

两者优缺点:

正向索引 倒排索引
优点 1.可以给多个字段创建索引
2.根据索引字段搜索和排序速度非常快
部分词条查询效率高【创建唯一索引】
缺点 部分词条查询效率不高,只能全表扫描 1.只能给词条创建索引,而不是字段
2.无法根据字段做排序

3.基础概念

3.1 文档(一行数据)和字段(一个列)

elasticsearch是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式存储在elasticsearch中:

image-20240507230531294

因此, 数据库中一行数据 <==> ES中一个JSON文档;

而数据库中每行数据都包含很多列,这些列就转换为JSON文档中的字段(Field)

3.2 索引(数据库的表)和映射(数据库表结构约束)

随着业务发展,需要在es中存储的文档也会越来越多,比如有商品的文档,用户的文档,订单的文档等等;

image-20240508223520776

所有文档都散乱存放显然非常混乱,也不方便管理。

因此,我们要将==类型相同的文档==(一行数据)集中在一起管理,称为索引(Index)

image-20240508223847616

因此,==索引(类型相同的很多行文档) <—->数据库中的表==

数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。

因此,索引库中就有==映射(mapping),是索引中文档的字段约束信息,类似表的结构约束==

3.3 Mysql和Elasticsearch对比

image-20240508225648423

注意:mysql的语法就是sql,而es的语法是dsl【提供json风格的请求语句,用来操作es进行crud】

  • Mysql:擅长事务类型操作,可以确保数据的安全和一致性
  • Elasticsearch:擅长海量数据的搜索、分析、计算

因此在企业中,往往是两者结合使用:

  • 对安全性要求较高的写操作,使用mysql实现
  • 对查询性能要求较高的搜索需求,使用elasticsearch实现
  • 两者再基于某种方式,实现数据的同步,保证一致性

image-20240508231244479

3.4 数据一致性

1
2
3
4
5
方法一:同步双写,课程上架的时候数据写入Mysql,同步也写入ES
方法二:异步双写,课程上架的时候数据写入Mysql,发送消息给MQ,MQ通知ES更新 【项目使用】
方法三:定时同步,对于数据库新增的时候,定时批量/全量同步到ES
方法四:基于Logstash输入输出插件
方法五:基于cancal数据库增量日志解析工具,伪装主从数据库进行同步
策略 优点 缺点
同步双写 - 简单易实现
- 实时性高
- 代码侵入性强
- 存在不一致的风险
- 可能影响系统性能
异步双写(MQ方式) - 解耦数据写入操作
- 通过消息队列提升性能和扩展性
- 系统复杂度增加
- 可能存在消息丢失的风险
- 引入了消息中间件的依赖
定期同步 - 实现简单
- 无需改变现有业务逻辑
- 实时性差
- 可能给数据库带来额外压力
基于Binlog实时同步 - 无代码侵入
- 实时性较好
- 业务逻辑与数据同步解耦
- 构建Binlog系统复杂
- 可能存在MQ延时风险
使用Canal监听Binlog同步数据到ES - 基于MySQL的Binlog,实现数据的实时同步
- 减少系统耦合
- 需要维护额外的Canal服务

4.IK分词器(ikun)

Elasticsearch的关键就是倒排索引,而倒排索引依赖于对文档内容的分词情况(分词好那就效率高),而分词则需要高效、精准的分词算法,IK分词器就是这样一个中文分词算法

4.1 安装IK分词器

方案一:在线安装

1
docker exec -it es ./bin/elasticsearch-plugin  install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip

方案二:离线安装

首先,查看之前安装的elasticsearch容器的plugins数据卷目录:

1
docker volume inspect es-plugins

image-20240507221107758

可以看到elasticsearch的插件挂载到了/var/lib/docker/volumes/es-plugins/_data这个目录。我们需要把IK分词器上传至这个目录

image-20240507221628934

4.2 使用IK分词器

4.2.1 官方标准分词器(standard)

我们在Kibana的DevTools上来测试分词器,首先测试Elasticsearch官方提供的标准分词器:

image-20240507221852165

我们可以看到,标准分词器只能1个字作为一个1个词条,无法正取对中文做分词

4.2.2 IK分词器(ik_smart智能语义切分)

这种情况下,可以智能的将词语切分。但是像程序员这种词可以拆分为程序员,程序,员。这个分词器无法实现

image-20240507222246345

4.2.3 IK分词器(ik_max_word最细粒度切分)

这种情况下,可以在4.2.2的前提下继续细分【程序员这种词可以拆分为程序员,程序,员】

image-20240507222545813

4.3 扩展词典

随着互联网的发展,“造词运动”也越发的频繁。出现了很多新的词语,在原有的词汇列表中并不存在。比如:“泰裤辣”,“传智播客” 等

image-20240507222659451

所以想要正确分词,IK分词器的词库也需要不断地更新,IK分词器提供了扩展词汇的功能:

我们在ik-config文件夹下的IkAnalyzer.cfg.xml文件添加拓展词典和停用词典,这样我们再调用的时候,宋亚翔和传智播客就可以被认为是一个词语作为词条

image-20240507224408412

==基础操作(对索引库和文档基础操作)==

==方式一:通过ES手动创建–很繁琐==

1.索引库操作(数据库表)

index类似数据库表,映射类似表的结构。我们要向es中存储数据,必须先创建索引(数据库表)和映射(数据库定义)

1.1 Mapping映射属性

Mapping是对索引库的文档设置约束,常见的mapping属性包括:

image-20240520150601809

1
2
3
4
5
6
7
8
9
10
11
12
{
"age": 21,
"weight": 52.1,
"isMarried": false,
"info": "黑马程序员Java讲师",
"email": "zy@itcast.cn",
"score": [99.1, 99.5, 98.9],
"name": {
"firstName": "云",
"lastName": "赵"
}
}

对应的每个字段映射(Mapping):

image-20240520151047898

1.2 索引库的CRUD

由于Elasticsearch采用的是Restful风格的API,因此其请求方式和路径相对都比较规范,而且请求参数也都采用JSON风格。

我们直接基于Kibana的DevTools来编写请求做测试,由于有语法提示,会非常方便。

1.2.1 创建索引库和映射

基本语法

  • 请求方式:PUT
  • 请求路径:/索引库名,可以自定义
  • 请求参数:mapping映射

格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
},
// ...略
}
}
}

image-20240520151047898

1.2.2 查询索引库

基本语法

  • 请求方式:GET
  • 请求路径:/索引库名
  • 请求参数:无

image-20240520151340133

1.2.3 修改索引库(只能修改新字段)

倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping

虽然==无法修改mapping中已有字段,却允许添加新的字段到mapping==,因为不会对倒排索引产生影响。因此修改索引库能做的就是向索引库中添加新字段,或者更新索引库的基础属性。

基本语法

  • 请求方式:PUT
  • 请求路径:/索引库名
  • 请求参数:/_mapping

image-20240520151647929

1.2.4 删除索引库

基本语法:

  • 请求方式:DELETE
  • 请求路径:/索引库名
  • 请求参数:无

image-20240520151416751

==1.2.5 索引库操作总结==

索引库操作:

  • 创建索引库:PUT /索引库名{“mappings”:{“properties”:{部分新字段信息}}}}
  • 查询索引库:GET /索引库名
  • 删除索引库:DELETE /索引库名
  • 修改索引库【添加字段】:PUT /索引库名/_mapping{“properties”:{部分新字段信息}}

可以看到,对索引库的操作基本遵循的Restful的风格,因此API接口非常统一,方便记忆。

2.文档操作(一行数据)

有了索引库,接下来就可以向索引库中添加数据。而ElasticSearch数据就是JSON风格的文档。

2.1 新增文档

语法:

1
2
3
4
5
6
7
8
9
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
},
}

例如,目前要新增id=1的文档:

image-20240520153641479

2.2 查询文档

语法:

1
GET /{索引库名称}/_doc/{id}

例如,查询id=1的文档:

image-20240520153813733

2.3 删除文档

语法:

1
DELETE /{索引库名}/_doc/id值

例如,删除id=1的文档:

image-20240520153911052

2.4 修改文档

修改有两种方式:

  • 全量修改:直接覆盖原来的文档【会删除旧文档,添加新文档(如果没有就直接删除)】
  • 局部修改:修改文档中的部分字段

2.4.1 全量修改

全量修改是覆盖原来的文档,其本质是两步操作:

  • 根据指定的id删除文档
  • 新增一个相同id的文档

注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。

语法:

1
2
3
4
5
6
PUT /{索引库名}/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
// ... 略
}

image-20240520154216989

2.4.2 局部修改

局部修改是只修改指定id匹配的文档中的部分字段。【注意:局部修改是POST

语法:

1
2
3
4
5
6
POST /{索引库名}/_update/文档id
{
"doc": {
"字段名": "新的值",
}
}

image-20240520154237114

2.5 批处理

类似于Mysql数据库,可以进行多条数据一次性操作【感觉很麻烦,主要是可读性很差】

批处理采用==POST请求==,基本语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST _bulk
# 1.修改 --如果文档id存在就覆盖,不存在就创建
# index代表新增操作 _index表示索引库名 _id表示要操作的文档id
{ "index" : { "_index" : "test", "_id" : "1" } }
# 代表新增的文档内容
{ "field1" : "value1" }

# 2.删除
{ "delete" : { "_index" : "test", "_id" : "2" } }

# 3.新增 --如果文档id存在就报错
{ "create" : { "_index" : "test", "_id" : "3" } }
{ "field1" : "value3" }

# 4.更新
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }

==2.6 文档操作总结==

相对于索引库创建,大致就是中间多了一个_doc路径,修改文档类似于修改索引库比较特殊。

  • 创建文档:POST /{索引库名}/_doc/文档id { json文档 }
  • 查询文档:GET /{索引库名}/_doc/文档id
  • 删除文档:DELETE /{索引库名}/_doc/文档id
  • 修改文档:
    • 全量修改:PUT /{索引库名}/_doc/文档id { json文档 }
    • 局部修改:POST /{索引库名}/_update/文档id { "doc": {字段}}

==索引库操作和文档操作对比:==

image-20240521150017482

5和6步骤主要是在网页端进行设置,因此提出了一个Java的客户端—==JavaRestClient==

==方式二:通过Java实现—不用繁琐的手动创建==

1.JavaRestClient

提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是==组装DSL语句==,通过http请求发送给ES。

由于ES目前最新版本是8.8,提供了全新版本的客户端,老版本的客户端已经被标记为过时。而我们采用的是7.12版本,因此只能使用老版本客户端:

image-20240520155549341

然后选择7.12版本,HighLevelRestClient版本:

image-20240520155613826

==1.1 初始化RestClient==

在Elasticsearch提供的API中,与Elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中,必须先完成这个对象的初始化,建立与elasticsearch的连接。

1.1.1 引入RestHighLevelClient依赖

item-service模块中引入esRestHighLevelClient依赖:

1
2
3
4
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

1.1.2 覆盖ES版本

因为SpringBoot默认的ES版本是7.17.10,所以我们需要覆盖默认的ES版本:

1
2
3
4
5
6
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<!--覆盖成7.12.1-->
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>

1.1.3 初始化RestHighLevelClient

初始化的代码如下:

1
2
3
4
5
6
RestHighLevelClient client = new RestHighLevelClient(
//使用RestClient的builder方法创建
RestClient.builder(
HttpHost.create("http://192.168.xxx.xxx:9200")
)
);

==1.2 商品Mapping映射==

我们针对购物车数据库进行分析:

image-20240520172813812

我们可以对购物车的所有字段进行分析,判断哪些字段必须添加到ElasticSearch中,判断哪些字段必须添加搜索功能。从而进行新建索引库和映射:

image-20240520171754450

在网页上的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
PUT /items
{
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"name":{
"type": "text",
"analyzer": "ik_max_word"
},
"price":{
"type": "integer"
},
"stock":{
"type": "integer"
},
"image":{
"type": "keyword",
"index": false
},
"category":{
"type": "keyword"
},
"brand":{
"type": "keyword"
},
"sold":{
"type": "integer"
},
"commentCount":{
"type": "integer",
"index": false
},
"isAD":{
"type": "boolean"
},
"updateTime":{
"type": "date"
}
}
}
}

1.3 索引库操作

创建索引库的JavaAPI和Restful接口API对比:

1.3.1 创建索引库

image-20240520173351287

具体代码如下:

image-20240521135017531

创建索引库:

image-20240521135105629

1.3.2 删除索引库

具体代码如下:

image-20240521135115905

1.3.3 查询索引库

具体代码如下:

==1.3.4 索引库操作总结==

JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()方法来获取索引库的操作对象。

索引库操作的基本步骤:

  • 1.初始化RestHighLevelClient类对象client【创建客户端】

  • 2.创建XxxIndexRequest对象request【XXX是CreateGetDelete

  • 3.准备请求参数request.source()方法【Create时需要参数,其他情况不需要】

  • 4.发送请求client.indices().xxx()方法【xxx是createexistsdelete

1.4 文档操作

1.4.1 新增文档

  • 1.创建Request对象,这里是IndexRequest,因为添加文档就是创建倒排索引的过程
  • 2.准备请求参数,本例中就是Json文档
  • 3.发送请求【client.index()方法就好了】

image-20240521142712455

1.4.2 查询文档

与之前的流程类似,代码大概分2步:

  • 创建Request对象
  • 准备请求参数,这里是无参,【直接省略】
  • 发送请求
  • 解析结果【因为结果在_source部分内】

image-20240521142844007

可以看到,响应结果是一个JSON,其中文档放在一个_source属性中,因此解析就是拿到_source,反序列化为Java对象即可

1.4.3 删除文档

与查询相比,仅仅是请求方式从DELETE变成GET,可以想象Java代码应该依然是2步走:

  • 1)准备Request对象,因为是删除,这次是DeleteRequest对象。要指定索引库名和id
  • 2)准备参数,无参,直接省略
  • 3)发送请求。因为是删除,所以是client.delete()方法

image-20240521143043972

1.4.4 修改文档

修改我们讲过两种方式:

  • 全量修改:本质是先根据id删除,再新增【与1.4.1一致】
  • 局部修改:修改文档中的指定字段值

在RestClient的API中,全量修改与新增的API完全一致,判断依据是ID:

  • 如果新增时,ID已经存在,则修改
  • 如果新增时,ID不存在,则新增

这里不再赘述,我们主要关注局部修改的API即可

image-20240521143147541

与之前类似,也是三步走:

  • 1)准备Request对象。【这次是修改,所以是UpdateRequest
  • 2)准备参数。【也就是JSON文档,里面包含要修改的字段】
  • 3)更新文档。【这里调用client.update()方法】

1.4.5 批量导入文档

7.4.1-7.4.4的单条处理通过BulkRequest解决。因此BulkRequest中提供了add方法,用以添加其它CRUD的请求:

image-20240521144140401

具体代码:

image-20240521143955532

==1.4.6 文档操作总结==

文档操作的基本步骤:

  • 1.初始化RestHighLevelClient类对象client【创建客户端】
  • 2.创建XxxRequest对象request【Xxx是IndexUpdateDeleteBulk
  • 3.准备请求参数request.source()方法(IndexUpdateBulk时需要)
  • 4.发送请求client.Xxx()方法【Xxx是indexgetupdatedeletebulk
  • 5.解析结果(Get查询时需要,数据在_source内部)

上述的操作都是围绕id来进行的,只能进行简单查询不符合我们所预期的搜索

==进阶操作(DSL查询,更高级的查询)==

Elasticsearch提供基于JSON的DSL(Domain Specific Language)语句来定义查询条件,其JavaAPI就是在组织DSL条件。

Elasticsearch的查询可以分为两大类:

  • 叶子查询(Leaf query clauses):一般是在特定的字段里查询特定值,属于简单查询,很少单独使用。
  • 复合查询(Compound query clauses):以逻辑方式组合多个叶子查询/更改叶子查询的行为方式。

在查询以后,还可以对查询的结果做处理,包括:

排序:按照1个或多个字段值做排序

分页:根据from和size做分页,类似MySQL

高亮:对搜索结果中的关键字添加特殊样式,使其更加醒目

聚合:对搜索结果做数据统计以形成报表

==后续内容总结图:==

image-20240522164945976

==方式一:通过手动创建–DSL查询==

1.快速入门

查询的语法结构:

1
2
3
4
5
6
7
8
GET /{索引库名}/_search   #_search是固定路径,不能修改
{
"query": {
"查询类型": {
// .. 查询条件
}
}
}

例如,我们以最简单的无条件查询为例【查询类型=match_all】:

1
2
3
4
5
6
7
8
GET /items/_search  #_search是固定路径,不能修改
{
"query": {
"match_all": { #查询类型=match_all
#match_all无条件,所以条件位置不写即可
}
}
}

image-20240521154302865

虽然是match_all,但是响应结果中并不会包含索引库中的所有文档,而是仅有10条。这是因为处于安全考虑,elasticsearch设置了默认的查询页数

2.查询—-①叶子查询

image-20240521154435190

2.1 全文检索–(分词)

全文检索的种类也很多,详情可以参考官方文档

2.1.1 match–检索一个字段

1
2
3
4
5
6
7
8
GET /{索引库名}/_search
{
"query": {
"match": {
"字段名": "搜索条件"
}
}
}

举例:

image-20240521162042810

2.1.2 multi_match–检索多个字段

1
2
3
4
5
6
7
8
9
GET /{索引库名}/_search
{
"query": {
"multi_match": {
"query": "搜索条件",
"fields": ["字段1", "字段2"]
}
}
}

举例:

image-20240521162042810

2.2 精确查询–(不分词)

精确查询,英文是Term-level query,顾名思义,词条级别的查询。也就是说不会对用户输入的搜索条件再分词,而是作为一个词条,与搜索的字段内容精确值匹配。因此推荐查找keyword、数值、日期、boolean类型的字段。例如:

  • id
  • price
  • 城市
  • 地名
  • 人名

等等,作为一个整体才有含义的字段。

详情可以查看官方文档

2.2.1 term–根据词条精确匹配

1
2
3
4
5
6
7
8
9
10
GET /{索引库名}/_search
{
"query": {
"term": {
"字段名": {
"value": "搜索条件"
}
}
}
}

举例:

image-20240521163826141

2.2.2 range–根据数值范围查询

1
2
3
4
5
6
7
8
9
10
11
GET /{索引库名}/_search
{
"query": {
"range": {
"字段名": {
"gte": {最小值},
"lte": {最大值}
}
}
}
}

举例:

image-20240521163921559

2.3 地理(geo)查询

未涉及,用到了补充

3.查询—-②复合查询

image-20240521164256532

其他符合查询和相关语法可以参考官方文档

3.1 布尔查询– 与/或/非

bool查询,即布尔查询。就是利用逻辑运算来组合一个或多个查询子句的组合。bool查询支持的逻辑运算有:

  • must:必须匹配每个子查询,类似“与” 【输入框的搜索条件肯定要参与相关性算分】
  • should:选择性匹配子查询,类似“或”
  • must_not:必须不匹配,不参与算分,类似“非”
  • filter:必须匹配,不参与算分 【其他的过滤条件就可以不参与算分】

bool查询的语法如下:

image-20240521170512412

出于性能考虑,与搜索关键字无关的查询尽量采用must_not或filter逻辑运算,避免参与相关性算分

3.2 function_score查询–人为修改相关性算分

3.2.1 相关性算分介绍

当我们利用match进行叶子查询,文档结果会根据与搜索词条的关联度打分_score),返回结果时按照分值降序排列

例如,我们搜索”手机”,结果如下:

image-20240521171322014

从Elasticsearch5.1开始,采用的相关性打分算法是BM25算法,其公式如下:

image-20240521171415660

基于这套公式,就可以判断出某个文档用户搜索的关键字之间的关联度,还是比较准确的。但是,在实际业务需求中,常常会有竞价排名的功能。不是相关度越高排名越靠前,而是掏的钱多的排名靠前。

例如在百度中搜索Java培训,排名靠前的就是广告推广

image-20240521171538540

要想人为控制相关性算法【添加一个过滤条件,增加一个算分函数得到一个值,然后和原始相关性算分运算一下得到新的】,就需要利用Elasticsearch的function_score查询

3.2.2 function_score介绍

function score 查询中包含四部分内容:

  • 1.原始查询条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
  • 2.过滤条件:filter部分,符合该条件的文档才会重新算分
  • 3.算分函数:符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
    • weight:函数结果=常量
    • field_value_factor:函数结果=文档中的某个字段值
    • random_score:函数结果=随机数
    • script_score:自定义算分函数算法
  • 4.运算模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:
    • multiply:相乘
    • replace:用function score原始算分替换query score
    • 其它,例如:sum、avg、max、min

==【大致就是在原有BM25算法得到相关性算分,然后根据符合filter条件的文档根据算分函数得到一个值,最后两者进行一些运算方式得到最终值】==

function score的运行流程如下:

  • 1)根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分(query score)
  • 2)根据过滤条件,过滤文档
  • 3)符合过滤条件的文档,基于算分函数,得到函数算分(function score)–算分函数=①常量②文档中某字段值③随机数④自定义
  • 4)将原始算分(query score)和函数算分(function score)基于运算模式[各种]做运算,得到最终结果,作为相关性算分。

举例:给IPhone这个品牌的手机算分提高十倍,分析如下:

  • 过滤条件:品牌必须为IPhone
  • 算分函数:常量weight,值为10
  • 算分模式:相乘multiply

对应代码:

image-20240521172952514

4.排序和分页

1.默认排序:Elasticsearch支持对搜索结果根据相关度算分(_score)进行排序,按照分值降序排列。

2.指定字段排序:Elasticsearch支持对keyword类型,数值类型,地理坐标类型,日期类型等。

4.1 指定排序

基本语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"排序字段": {
"order": "排序方式asc和desc"
}
}
]
}

image-20240522160121221

4.2 分页

elasticsearch 【默认】只返回==top10数据==。而如果要查询更多数据就需要修改分页参数了

elasticsearch中通过修改fromsize参数来控制要返回的分页结果:

  • from:从第几个文档开始
  • size:总共查询几个文档

类似于mysql中的limit ?, ?

基本语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET /items/_search
{
"query": {
"match_all": {}
},
"from": 0, // 分页开始的位置,默认为0
"size": 10, // 每页文档数量,默认10
"sort": [
{
"price": {
"order": "desc"
}
}
]
}

image-20240522160315919

elasticsearch的数据一般会采用分片存储,也就是把一个索引中的数据分成N份,存储到不同节点上。这种存储方式比较有利于数据扩展,但给分页带来了一些麻烦。

举例如下:

如果一个索引库有100000条数据,分别存储到4个分片中,每个分片有25000条数据。现在每页查询10条,查询第99页。那分页查询条件如下:

1
2
3
4
5
6
7
8
9
10
GET /items/_search
{
"from": 990, // 从第990条开始查询
"size": 10, // 每页查询10条
"sort": [
{
"price": "asc"
}
]
}

从语句分析来讲:要查询的是第990-1000名数据。

从实现思路分析来讲:①将所有数据排序,找出前1000名②取出其中990-1000的部分

这样来看操作很复杂,因为每个片的数据不是顺序存储的,只能所有拿到一起再重新排序,才能找到最终前1000名截取出990-1000数据

image-20240522160808390

因此,Elasticsearch对普通分页会有一个设置:(from+size)<10000

4.3 深度分页(解决普通分页)

针对深度分页,elasticsearch提供了两种解决方案:

  • search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
  • scroll:原理将排序后的文档id形成快照,保存下来,基于快照做分页。官方已经不推荐使用。

以search after为例:

image-20240522161437320

优点:没有查询上限,支持深度分页【更智能,无上限】

缺点:只能向后逐页查询,不能随机翻页【一页一页查询】

场景:数据大规模顺序迁移、手机滚动查询【一页一页】

适用建议:

  • 大多数情况下,我们采用普通分页就可以了。查看百度、京东等网站,会发现其分页都有限制。例如百度最多支持77页,每页不足20条。京东最多100页,每页最多60条。

  • 因此,一般我们采用限制分页深度的方式即可,无需实现深度分页。

5.高亮显示

5.1 高亮原理

我们在百度,京东搜索时,关键字会变成红色,比较醒目,这叫高亮显示:

image-20240522162022778

css样式肯定是前端实现页面的时候写好的,但是前端编写页面的时候是不知道页面要展示什么数据的,不可能给数据加标签。而服务端实现搜索功能,要是有elasticsearch做分词搜索,是知道哪些词条需要高亮的。

因此词条的高亮标签肯定是由服务端提供数据的时候已经加上的

5.2 高亮操作

高亮的思路就是:

  • 用户输入搜索关键字搜索数据
  • 服务端根据搜索关键字到Elasticsearch搜索,并给搜索结果中的关键字词条添加html标签
  • 前端提前给约定好的html标签添加CSS样式

基本语法:

image-20240522162432699

注意:

  • 搜索必须有查询条件,而且是叶子查询的全文检索类型的查询条件(有分词),例如match
  • 参与高亮的字段必须是text类型的字段
  • 默认情况下参与高亮的字段要与搜索字段一致,除非添加:required_field_match=false

6.聚合

聚合(aggregations)可以让我们极其方便的实现对数据的统计、分析、运算。例如:

  • 什么品牌的手机最受欢迎?
  • 这些手机的平均价格、最高价格、最低价格?
  • 这些手机每月的销售情况如何?

实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现≈实时搜索效果。

聚合分类

image-20240523143246188

==【注意】:参与聚合的字段必须是Keyword、数值、日期、布尔的类型的字段(这些字段一般不分词)==

举例:

具体位置解释:

image-20240523145507262

==DSL手写规则总结==

image-20240522164945976

==方式二:通过Java实现—RestClient查询==

==0.总体对照分析==

查询数据

我们可以分三步拼凑DSL语句和发起请求获取相应结果:

image-20240522172046658

其中2.组织DSL参数的步骤中source()方法下面对应的查询/高亮/分页/排序/聚合:
image-20240522172832347

在查询方面我们直接可以通过QueryBuilders类调用对应的叶子查询/复杂查询

image-20240522172921305

解析数据

我们可以通过响应结果和Elasticsearch页面返回结果获取具体细节: 【可以扩展很多,但其实就是对照DSL查询结果写

image-20240522173851593

黑马的图:

image-20240522173920457

整体步骤

文档搜索的基本步骤是:

  1. 创建SearchRequest对象实例request
  2. 准备request.source(),也就是DSL语句【这个位置可以创建查询,分页,排序,聚合,高亮等操作】
    1. QueryBuilders来构建查询条件
    2. 传入request.source()query()方法
  3. 发送请求,得到结果
  4. 解析结果(参考DSL查询得到的JSON结果,从外到内,逐层解析)

1.查询

上述手动创建DSL查询的时候讲过查询的分类:
image-20240523135626365

1.1 叶子查询

1.1.1 全文检索

image-20240523135752868

1.1.2 精确查询

image-20240523135825592

1.2 复合查询

image-20240523135928266

1.3 举例

举例:

image-20240523141521261

具体代码如下:

image-20240523141510018

2.分页和排序

2.1 基础API

image-20240523142727445

3.高亮

  • 条件同样是在request.source()中指定,只不过高亮条件要基于HighlightBuilder来构造
  • 高亮响应结果与搜索的文档结果不在一起,需要单独解析

3.1 基础API

image-20240523142500909

3.2 获取高亮值

每一条hits信息原始数据在_source部分,而高亮部分在同级highlight内部:

image-20240523142428052

在整体代码的位置:

image-20240523142651360

代码解读:

  • 从结果中获取_sourcehit.getSourceAsString(),这部分是非高亮结果,json字符串。还需要反序列为ItemDoc对象
  • 获取高亮结果。hit.getHighlightFields(),返回值是一个Map,key是高亮字段名称,值是HighlightField对象,代表高亮值
  • Map中根据高亮字段名称,获取高亮字段值对象HighlightField
  • HighlightField中获取Fragments,并且转为字符串。这部分就是真正的高亮字符串了
  • 最后:用高亮的结果替换ItemDoc中的非高亮结果

4.聚合

我们以品牌聚合为例:

4.1 基础API

image-20240523150059473

在Java代码中位置:

image-20240523150224095

4.2 获取桶结果

image-20240523150857573

在Java代码中位置:

image-20240523150938582

==Elasticsearch学习总结==

==1.基本使用思路:==

1.创建索引库和映射 –有了类似于数据库的表和表定义

2.对文档进行CRUD –有了类似于数据库的一行行数据

3.在对应位置进行复杂的DSL查询 –我们可以进行高级的查询,分页,排序,高亮,聚合操作

==2.如果写Java代码的话:==

1.pom.xml导入RestHighLevelClient依赖,然后在父工程覆盖ES版本,初始化RestHighLevelClient依赖

image-20240523155157771

2.分析Mysql哪些字段需要搜索和必须存在,然后Elasticsearch在网页上进行手动设定索引库映射

image-20240523155251252

3.对索引库进行操作【不过我认为网页上更方便】 —到这一步类似于完成了数据库的表和表定义

image-20240523155356214

4.对文档操作【不过我认为网页上更方便】 —到这一步类似于有了数据库的数据

image-20240523155503652

5.在具体位置就可以进行复杂的DSL查询【可以进行查询,分页,排序,高亮,聚合等操作】

image-20240523155632499

RabbitMQ

1.同步调用和异步调用

1.1 同步调用

image-20240319100914998

综上,同步调用的方式存在下列问题:

  • 拓展性差(新增业务和逻辑就要修改,不符合开闭原则)
  • 性能下降
  • 级联失败

而要解决这些问题,我们就必须用异步调用的方式来代替同步调用

1.2 异步调用

异步调用方式其实就是==基于消息通知的方式==,一般包含三个角色:

  • 消息发送者:投递消息的人,就是原来的调用方
  • 消息Broker:管理、暂存、转发消息,你可以把它理解成微信服务器
  • 消息接收者:接收和处理消息的人,就是原来的服务提供方

image-20240319143703635

异步调用中,发送者不再直接同步调用接收者的业务接口,而是发送一条消息投递给消息Broker(消息代理)。然后接收者根据自己的需求从消息Broker那里订阅消息。每当发送方发送消息后,接受者都能获取消息并处理 —> 发送消息的人和接收消息的人就完全解耦了

如图所示:

image-20240319144436498

综上,异步调用的优势包括:

  • 耦合度更低
  • 性能更好
  • 业务拓展性强
  • 故障隔离,避免级联失败

当然,异步通信也并非完美无缺,它存在下列缺点:

  • 完全依赖于Broker的可靠性、安全性和性能

  • 架构复杂,后期维护和调试麻烦

1.3 MQ技术选型

消息Broker,目前常见的实现方案就是消息队列(MessageQueue),简称为MQ.
目比较常见的MQ实现:

  • ActiveMQ
  • RabbitMQ
  • RocketMQ
  • Kafka

几种常见MQ的对比:

RabbitMQ ActiveMQ RocketMQ Kafka
公司/社区 Rabbit Apache 阿里 Apache
开发语言 Erlang Java Java Scala&Java
协议支持 AMQP XMPP SMTP STOMP OpenWire STOMP REST XMPP AMQP 自定义协议 自定义协议
可用性 一般
单机吞吐量 一般 非常高
消息延迟 微秒级 毫秒级 毫秒级 毫秒以内
消息可靠性 一般 一般

追求可用性:Kafka、 RocketMQ 、RabbitMQ
追求可靠性:RabbitMQ、RocketMQ
追求吞吐能力:RocketMQ、Kafka
追求消息低延迟:RabbitMQ、Kafka

据统计,目前国内消息队列使用最多的还是RabbitMQ,再加上其各方面都比较均衡,稳定性也好

2.RabbitMQ

image-20240319093425166

RabbitMQ是基于Erlang语言开发的开源消息通信中间件,官网地址:Messaging that just works — RabbitMQ

2.1 RabbitMQ安装

基于Docker来安装RabbitMQ,命令如下:

1
2
3
4
5
6
7
8
9
10
11
docker run 
-e RABBITMQ_DEFAULT_USER=larkkkkkkk #设置默认用户名
-e RABBITMQ_DEFAULT_PASS=123456 #设置默认密码
-v mq-plugins:/plugins #将本地主机上的mq-plugins目录挂载到容器内部的/plugins目录,可以存放插件
--name mq #指定容器名
--hostname mq #指定容器的主机名
-p 15672:15672 #RabbitMQ管理页面登录的端口号 [浏览器输入http://localhost:15672/即可进入]
-p 5672:5672 #RabbitMQ用于AMQP协议通信 [SpringAMQP配置时候用]
--network heima #将容器连接到名字为heima的网络中 [如果没有就使用命令创建hmall网络 docker network create heima]
-d #在后台运行容器
rabbitmq:3.8-management #使用RabbitMQ 3.8版本带有管理界面的镜像来创建容器

可以看到在安装命令中有两个映射的端口:

  • 15672:RabbitMQ提供的管理控制台的端口
  • 5672:RabbitMQ的消息发送处理接口

通过访问 http://localhost:15672即可看到管理控制台。首次访问登录,需要配置文件中设定的用户名和密码

image-20240319192803935

2.2 RabbitMQ架构

image-20240319193424489

其中包含几个概念:

  • **publisher**:生产者,也就是发送消息的一方
  • **consumer**:消费者,也就是消费消息的一方
  • **queue**:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理
  • **exchange**:交换机,负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。
  • **virtual host**:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue

上述这些东西都可以在RabbitMQ的管理控制台来管理

3.SpringAMQP

RabbitMQ采用AMQP协议,因此具有跨语言的特性。任何语言只要遵循AMQP协议收发消息,都可以与RabbitMQ交互【RabbitMQ官方提供了各种不同语言的客户端】

但是RabbitMQ官方提供的Java客户端编码复杂,一般生产环境下我们更多会结合Spring来使用,Spring提供模板工具和SpringBoot自动装配 –> ==SpringAMQP==

提供三个功能:

  • 自动声明队列、交换机及其绑定关系
  • 基于注解的监听器模式,异步接收消息
  • 封装了RabbitTemplate工具,用于发送消息 -rabbitTemplate.convertAndSend(队列名,发送信息);

3.1 生产者-消费者(1-1)

3.1.1 导入Demo工程

image-20240319195614981

3.1.2 导入maven坐标

image-20240319195708883

3.1.3 新建队列

image-20240319200226308

3.1.4 每个子工程配置RabbitMQ信息

image-20240319200405081

3.1.5 生产者发送消息

1
2
1.注入RabbitTemplate对象
2.对象调用convertAndSend(队列名,信息)方法
image-20240319200946446

3.1.6 消费者接收消息

1
2
3
1.使用注解RabbitListener(队列名=“xxx”)
2.启动当前消费者子工程(SpringBoot工程)
3.生产者发送一次消息,消费者就会接收到一次消息

image-20240319201418364

3.2 生产者-消费者(1-n) -WorkQueues任务模型

Work queues任务模型 –> 让多个消费者绑定到一个队列,共同消费队列中的消息 –> ==解决消息堆积太多==

image-20240319202054225

3.2.1 新建队列

image-20240320091336394

3.2.2 生产者发送消息

一个发送者,循环发送50次消息

image-20240320091703762

3.2.3 消费者接收消息

两个消费者接收消息,一个休眠20ms(每秒钟处理50个消息),一个休眠200ms(每秒钟处理5个消息)

image-20240320092251196

3.2.4 均匀分配

启动消费者子工程项目,再发送消息就可以接受消息:

image-20240320092647584

可以看到消费者1和消费者2竟然每人消费了25条消息:

  • 消费者1很快完成了自己的25条消息
  • 消费者2却在缓慢的处理自己的25条消息。

也就是说消息是==平均分配==给每个消费者,并没有考虑到消费者的处理能力。导致1个消费者空闲,另一个消费者忙的不可开交。没有充分利用每一个消费者的能力,最终消息处理的耗时远远超过了1秒。这样显然是有问题的

3.2.5 能者多劳(yml配置prefetch)

修改==listener.simple.prefetch:1==可以保证==能者多劳==,每个消费者每次只能获取一条消息,处理完成才能获取下一条消息

image-20240320093106144

更改之后重新发送消息:

image-20240320093225443

可以发现,由于消费者1处理速度较快,所以处理了更多的消息;消费者2处理速度较慢,只处理了6条消息。而最终总的执行耗时也在1秒左右,大大提升。
正所谓能者多劳,这样充分利用了每一个消费者的处理能力,可以有效避免消息积压问题

4.交换机(Exchange)

在3.1和3.2部分没有添加交换机,生产者直接发送消息到队列。但是引入交换机之后消息发送的模式会有很大的变化:

image-20240320093715426

可以看到,在订阅模型中,多了一个exchange角色,而且过程略有变化:

  • Publisher:生产者,不再发送消息到队列中,而是发给交换机
  • Exchange:交换机,一方面,接收生产者发送的消息。另一方面,知道如何处理消息(递交给某个特别队列、递交给所有队列、或是将消息丢弃)
  • Queue:消息队列也与以前一样,接收消息、缓存消息。不过队列一定要与交换机绑定。
  • Consumer:消费者,与以前一样,订阅队列,【没有变化】

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!

Exchange(交换机)的类型有四种:

  • Fanout:广播,将消息交给所有绑定到交换机的队列。【最早在控制台使用】
  • Direct:订阅,基于RoutingKey(写死的路由key)发送给订阅了消息的队列
  • Topic:通配符订阅,基于RoutingKey(符合通配符的路由key)发送给订阅了消息的队列
  • Headers:头匹配,基于MQ的消息头匹配,【用的较少】

4.1 Fanout交换机(广播)

在广播(Fanout)模式下,消息发送流程是这样的

image-20240320094830771

4.1.1 声明交换机和队列

image-20240320095902006

4.1.2 消息发送

image-20240320095942536

4.1.3 消息接收

image-20240320100056347

启动消费者子工程之后发送消息

image-20240320100134844

4.2 Direct交换机(订阅)

4.2.1 声明交换机和队列

image-20240320200427090

官网在线创建:

image-20240320200530874

4.2.2 消息发送

image-20240320200725027

4.2.3 消息接收

image-20240320200746903

4.3 Topic交换机(通配符订阅)

4.3.1 声明交换机和队列

image-20240320200956679

官网在线创建:

image-20240622214037809

4.3.2 消息发送

image-20240320200802564

4.3.3 消息接收

image-20240320200816573

4.3.4 总结

Topic类型的ExchangeDirect相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定BindingKey 的时候使用==通配符==!

BindingKey一般是一个/多个单词组成,多个单词之间用.分割

通配符规则:

通配符规则:

  • #:匹配0个或者多个词
  • *:匹配1个词

举例:

  • item.#:能够匹配item.spu.insert 或者 item.spu 或者 item (#可以是0个词到多个词)
  • item.*:只能匹配item.spu (*只能是一个词)

5. API-队列和交换机(替换网页手动创建)

SpringAMQP提供了声明队列,交换机和绑定关系的API:

  • Queue:队列
  • Exchange:交换机
  • Binding:绑定关系

image-20240320203603754

5.1 @Bean方式声明(不推荐)

image-20240320215605011

这样创建很繁琐,因此提供了基于注解的方式

5.2 注解方式声明(推荐)

image-20240320220328866

其实就是@RabbitListener注解里面配置关系(@QueueBinding),然后里面具体的就是交换机(@Exchange),队列(@Queue)以及路由key(key)

image-20240320221056179

6.消息转换器[解决发送消息的JDK序列化]

Spring的convertAndSend()方法接收的是一个Object类型:

image-20240320221749744

而在数据传输时,可能会因为默认的==JDK序列化==导致数据体积过大(乱码一样的序列化结果),安全漏洞,可读性差等问题。

image-20240320222347892

因此可以考虑使用==Json序列化和反序列化==:

6.1 配置JSON转换器

  • 1.在生产者和消费者两个服务中都要引入依赖

image-20240321092511719

注意:如果项目中引入了Spring-boot-starter-web依赖,则无需再次引入Jackson依赖

  • 2.在生产者和消费者两个服务的启动类中添加一个Bean:配置消息转换器

    image-20240321092839447

==7.RabbitMQ使用总结==【直接看这里写代码】

==可以参考<RabbitMQ-黑马商城为例>这篇文章,有详细的操作介绍和步骤==

7.1 maven引入maven坐标

在生产者和消费者的pom.xml文件中配置:

1
2
3
4
5
<!--消息发送-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

7.2 子工程配置RabbitMQ信息

在生产者和消费者的application.yml文件中配置:

1
2
3
4
5
6
7
8
9
spring:
rabbitmq:
host: 192.168.92.129 # 你的虚拟机IP
port: 5672 # 端口
virtual-host: /hmall # 虚拟主机
username: hmall # 用户名
password: 123456 # 密码

//消费者和生产者会在对应位置添加配置 【例如:生产者消费者的确认机制,重试机制等】

7.3 配置消息转换器[解决发送消息的JDK序列化]

可以在公共模块添加bean

image-20240321095355698

7.4 生产者-发送消息

将原始的同步修改订单信息更改为异步修改

image-20240321095847784

7.5 消费者-接收消息

消费者可以添加消息监听,添加好交换机和路由key和队列

image-20240321100301473

==总结如下:==

image-20240403110457790

大致就是导入maven,子工程配置一些属性,然后生产者调用rabbitMQ的rabbitTemplate.convertAndSend()方法发送消息【里面可以添加各种】,在消费者方面可以①使用bean进行声明交换机,队列和关系②使用@RabbitListener注解进行声明【里面可以添加一些属性,例如持久化的,lazyqueue的,延迟消息的】


==高级进阶 –保证消息可靠性(三个方面)==

异步结构可能会在发送者,MQ,消费者三个地方出现问题!!!因此要考虑这三个位置的可靠性和兜底方案(延迟消息)

消息从发送者发送消息,到消费者处理消息,需要经过的流程是这样的:

image-20240321120455090

消息从生产者到消费者的每一步都可能导致消息丢失:

  • 发送消息时丢失:
    • 生产者发送消息时连接MQ失败
    • 生产者发送消息到达MQ后未找到Exchange
    • 生产者发送消息到达MQ的Exchange后,未找到合适的Queue
    • 消息到达MQ后,处理消息的进程发生异常
  • MQ导致消息丢失:
    • 消息到达MQ,保存到队列后,尚未消费就突然宕机
  • 消费者处理消息时:
    • 消息接收后尚未处理突然宕机
    • 消息接收后处理过程中抛出异常

综上,我们要解决消息丢失问题,保证MQ的可靠性,就必须从3个方面入手:

  • 确保生产者一定把消息发送到MQ —> ==生产者的可靠性(生产者重试机制,生产者确认机制)==

  • 确保MQ不会将消息弄丢 —> ==MQ的可靠性(数据持久化,lazy queue)==

  • 确保消费者一定要处理消息 —> ==消费者的可靠性(消费者确认机制,失败重传机制,失败处理策略,业务幂等性)==

    ==总汇总(复习图)==

image-20240325153049616

8.可靠性——-发送者

8.1 生产者重试机制(建议禁用)

生产者发送消息时,出现网络故障,导致与MQ连接中断 ———-> SpringAMQP提供的消息发送时的==重试机制==

image-20240321110716739

注意:
当网络不稳定的时候,利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是==阻塞式重试==(也就是说多次重试等待的过程中,当前线程是被阻塞的)

如果对业务性能有要求的,建议禁用重试机制。如果一定要使用,请合理配置等待时长和重试次数,当然也可以考虑使用异步线程来执行发送消息的代码。

8.2 生产者确认机制(默认不开启)

一般情况下,只要生产者与MQ之间的网络连接顺畅,基本不会出现发送消息丢失的情况,因此大多数情况下我们无需考虑这种问题。

少数情况下,也会出现消息发送到MQ之后丢失的现象,比如:

  • MQ内部处理消息的进程发生了异常
  • 生产者发送消息到达MQ后未找到交换机
  • 生产者发送消息到达MQ的交换机之后,未找到合适的队列,因此无法路由

针对上述三种情况,RabbitMQ提供了==生产者消息确认机制==,包括了Publisher ConfirmPublisher Return两种方式。

在开启确认机制的情况下,当生产者发送消息给MQ之后,MQ会根据消息处理的情况返回不同的回执:

image-20240321160450585

总结如下:

  • 1.只要消息投递到MQ,就返回ACK,告知投递成功(基本上这三种ack我们可以考虑不处理,直接只关注nack的情况)

​ 1.1 当消息投递到MQ,但是路由失败时,通过Publisher Return返回异常信息,同时返回ack的确认信息,代表投递成功

​ 1.2 临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功

​ 1.3 持久消息投递到了MQ,并且入队完成持久化,返回ACK ,告知投递成功

  • 2.其它情况都会返回NACK,告知投递失败

【其中ack(投递成功)和nack(投递失败)都属于Publisher Confirm机制;return是属于Publisher Return机制】

【(默认)两种机制都是关闭状态,需要通过配置文件来开启,因为是需要额外的网络和系统资源开销】

【一定要使用的话,无需开启Publisher-Return机制(一般路由失败是自己业务问题)】

8.2.1 配置文件添加

image-20240321161135899

这里publisher-confirm-type有三种模式可选:

  • none:关闭confirm机制
  • simple:同步阻塞等待MQ的回执
  • correlated:MQ异步回调返回回执

一般我们推荐使用correlated,回调机制。

8.2.2 定义ReturnCallback(返回信息)

每个RabbitTemplate只能配置一个ReturnCallback,因此我们可以在配置类中统一设置

我们在publisher模块定义一个配置类:rabbitTemplate对象调用setReturnsCallback()方法,方法参数是一个匿名内部类(重写returnedMessage方法)

image-20240321161749402

8.2.3 定义ConfirmCallback(确定ack/nack)

由于每个消息发送时的处理逻辑不一定相同,因此ConfirmCallback需要在每次发送消息时定义。

就是在发送消息时,调用RabbitTemplate.convertAndSend()时多传递一个参数:

image-20240321162745583

这里的CorrelationData中包含两个核心的东西:

  • id:消息的唯一标示,MQ对不同的消息的回执以此做判断,避免混淆
  • SettableListenableFuture:回执结果的Future对象

将来MQ的回执就会通过这个Future来返回,我们可以提前给CorrelationData中的Future添加回调函数来处理消息回执:

image-20240321194022895

发送者位置发送消息(新增字段为了获取MQ给的结果):

image-20240321194656371

注意
开启生产者确认比较消耗MQ性能,一般不建议开启。而且大家思考一下触发确认的几种情况:

  • 路由失败:一般是因为RoutingKey错误导致,往往是编程导致
  • 交换机名称错误:同样是编程错误导致
  • MQ内部故障:这种需要处理,但概率往往较低。因此只有对消息可靠性要求非常高的业务才需要开启,而且仅仅需要开启ConfirmCallback处理nack就可以

9.可靠性——–RabbitMQ

在默认情况下,RabbitMQ会将接收到的信息保存在==内存==中(降低消息收发延迟)。这样会导致两个问题:

  • 一旦RabbitMQ宕机,内存中的消息会丢失 –> ==交换机持久化,队列持久化,消息持久化==
  • 内存空间有限,当消费者故障或者处理过慢,会导致消息积压,引发RabbitMQ阻塞 –> ==Lazy Queue==

image-20240321200321687

9.1 三种持久化

为了提升性能,默认情况下MQ的数据都是在内存存储的临时数据,重启后就会消失。为了保证数据的可靠性,必须配置数据持久化,包括:

  • 1.交换机持久化
  • 2.队列持久化
  • 3.消息持久化

其中以控制台界面为例:

  • 1.交换机持久化:

image-20240321202630567

  • 2.队列持久化:

image-20240321202726506

  • 3.消息持久化:

image-20240321202757851

说明:

在开启持久化机制以后,如果同时还开启了生产者确认机制,那么MQ会在消息持久化以后才发送ACK回执,进一步确保消息的可靠性

不过出于性能考虑,为了减少IO次数,发送到RabbitMQ的消息是每隔一段时间(100ms左右)批量持久化,这会导致后续的ACK回执有一定的延迟,因此建议生产者确认全部采用异步方式

9.2 LazyQueue惰性队列

在默认情况下,RabbitMQ会将接收到的信息保存在内存中以降低消息收发的延迟。但在某些特殊情况下,这会导致消息积压,比如:

  • 消费者宕机或出现网络故障(后续崩了)
  • 消息发送量激增,超过了消费者处理速度(前面发的太快了,后面接不住)
  • 消费者处理业务发生阻塞(后续阻塞)

一旦出现消息堆积问题,RabbitMQ的内存占用会越来越高 —> 触发内存预警上限,此时RabbitMQ会将内存消息 –刷–> 磁盘,这个行为叫==PageOut==,PageOut会耗费一段时间,并且会阻塞队列进程。因此在这个过程中RabbitMQ不会再处理新的消息,生产者的所有请求都会被阻塞

为了解决这个问题,从3.6.0版本开始,增加了Lazy Queues(惰性队列)。惰性队列的特征如下:

  • 接收到消息后直接存入磁盘而非内存 (直接存磁盘,就不会刷盘造成阻塞队列进程)
  • 消费者要消费消息时才会从磁盘中读取并加载到内存(也就是懒加载,需要了我才加载内存)
  • 支持数百万条的消息存储

而在3.12版本之后,LazyQueue已经成为了所有队列的默认格式。因此官方推荐升级RabbitMQ为3.12版本/所有队列都设置为LazyQueue模式

9.2.1方式一— 控制台配置Lazy模式

image-20240321204855176

9.2.2 方式二—代码配置Lazy模式

基本原理都是设置属性:==x-queue-mode=lazy==

  • 基于@Bean注解(配置类)

image-20240321204954868

QueueBuilder底层源码为:

image-20240321205034258

  • 基于@RabbitListener注解(消费者子工程)

image-20240321205132950

9.2.3 更新已有队列为Lazy模式

  • 基于控制台:
image-20240321205402456
  • 基于命令行:

可以基于命令行设置policy:

1
rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues

命令解读:

  • rabbitmqctl :RabbitMQ的命令行工具
  • set_policy :添加一个策略
  • Lazy :策略名称,可以自定义
  • "^lazy-queue$" :用正则表达式匹配队列的名字
  • '{"queue-mode":"lazy"}' :设置队列模式为lazy模式
  • --apply-to queues:策略的作用对象,是所有的队列

10.可靠性——-消费者

10.1 消费者确认机制

为了确定消费者是否成功处理消息,RabbitMQ提供了消费者确认机制(Consumer Acknowledgement)

就是说当消费者处理消息结束后,应该向+RabbitMQ发送一个回执,告知RabbitMQ自己消息处理状态。这时候回执有三种可选值:

  • ack成功处理消息,RabbitMQ从队列中删除该消息
  • nack消息处理失败,RabbitMQ再次投递消息
  • reject(很少使用)消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息

一般第三种reject方式比较少,除非是消息格式问题(那就是开发问题),因此大多数情况下我们需要将消息处理的代码通过try-catch机制捕获,消息处理成功就返回ack,处理失败就返回nack。

由于消息回执的处理代码比较统一,因此SpringAMQP帮我们实现了消息确认。并且允许我们通过配置文件(yml)设置ACK处理方式,有三种模式:

  • none:不处理。就是将消息投递给消费者后立刻回调ack,消息会立刻从MQ中删除。【非常不安全,不建议使用】
  • manual:手动模式。需要自己在业务代码中调用api,回调发送ack/reject【存在业务入侵,但更灵活】
  • auto:自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强。
    • 当业务正常执行时则自动返回ack(RabbitMQ删除消息)
    • 当业务出现异常时根据异常判断返回不同的结果

​ - 如果是业务异常,自动返回nack(RabbitMQ再次投递消息) —> 可能会出现不停重复投递(导致消息堆积)

​ - 如果是消息处理/校验异常,自动返回reject(RabbitMQ删除消息)

配置消费者的xml文件可以修改SpringAMQP的ack处理方式:

image-20240401141236210

但是,如果auto模式下是业务异常回执给nack,那就会不断从MQ中投递消息,会导致MQ消息处理飙升带来不必要的压力(这种极端情况就是消费者一直无法执行成功,发生概率很低,但是不怕万一就怕一万)

10.2 消费者失败重试机制

因为10.1如果是收到nack回执,那么就会不断从MQ中投递消息,可能会导致消息堆积,导致mq的消息处理飙升,带来不必要的压力

我们可以利用Spring的retry机制—>==当消费者异常就利用本地重试(×无限制重试)==

配置消费者的xml文件可以修改SpringAMQP的本地重试机制:

image-20240401154838741

在开启重试机制后,重试次数耗尽之后,如果消息依然失效,则会默认直接丢弃消息!!!!!!!!!!!!!

image-20240401155907410

可以发现:

  • 消费者在失败后消息并没有重新回到MQ无限重新投递,而是重试3次

  • 本地重试3次之后,抛出了AmqpRejectAndDontRequeueException异常(说明直接reject丢弃了)

10.3 失败处理策略[解决10.2重试后reject丢弃消息情况]

image-20240401163030885

因为10.2本地重试之后如果消息失效就直接丢弃,因此我们可以考虑==加上自定义重试次数之后的策略==。只需要==MessageRecoverer接口==来处理,它包含了三种不同的实现:

  • RejectAndDontRequeueRecoverer(默认):重试耗尽后,直接reject,丢弃消息
  • *ImmediateRequeueMessageRecoverer *:重试耗尽后,返回nack,消息重新入队 【减缓重试的速度,就还是要重新投递到前一步】
  • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机(最后人工校验/特殊校验)

10.3.1 第三种策略为例

①定义接受失败消息的交换机,队列和绑定关系

②定义RepublishMessageRecoverer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.itheima.consumer.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.retry.MessageRecoverer;
import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
//当消费者重试机制属性enabled=true的时候生效
@ConditionalOnProperty(name = "spring.rabbitmq.listener.simple.retry.enabled", havingValue = "true")
public class ErrorMessageConfig {
//交换机
@Bean
public DirectExchange errorMessageExchange(){
return new DirectExchange("error.direct");
}
//队列
@Bean
public Queue errorQueue(){
return new Queue("error.queue", true);
}
//交换机和队列绑定
@Bean
public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){
return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");
}
//第三种!!!! 返回一个MessageRecoverer类型
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
}

③在10.2基础上会将消息直接传递到对应交换机上最后进行人工处理

image-20240401163012833

  • 总结:

    自定义的三种方式,第一种默认直接拒绝(不好),第二种相当于往前重试了一个环节(减缓了重试速度,但是也不好),第三种相当于交给专门队列和交换机,最后交给人工处理(也不是很好)

10.4 业务幂等性[解决10.2多次重新投递消息情况]

  • 幂等性

    用函数表达式来描述是这样的:f(x) = f(f(x)),例如求绝对值函数。

    在程序开发中,则是指==同一个业务,执行一次或多次对业务状态的影响是一致的==。例如:

    • 根据id删除数据
    • 查询数据
    • 新增数据

    但数据的更新往往不是幂等的,如果重复执行可能造成不一样的后果。比如:

    • 取消订单,恢复库存的业务。如果多次恢复就会出现库存重复增加的情况
    • 退款业务。重复退款对商家而言会有经济损失。

    所以,我们要尽可能避免业务被重复执行。然而在实际业务场景中,由于意外经常会出现业务被重复执行的情况,例如:

    • 页面卡顿时频繁刷新导致表单重复提交
    • 服务间调用的重试
    • MQ消息的重复投递

    我们在用户支付成功后会发送MQ消息到交易服务,修改订单状态为已支付,就可能出现消息回

    复投递的情况。如果消费者不做判断,很有可能导致消息被消费多次,出现业务故障。

    举例:

    1. 假如用户刚刚支付完成,并且投递消息到交易服务,交易服务更改订单为已支付状态。
    2. 由于某种原因,例如网络故障导致生产者没有得到确认,隔了一段时间后重新投递给交易服务。
    3. 但是,在新投递的消息被消费之前,用户选择了退款,将订单状态改为了已退款状态。
    4. 退款完成后,新投递的消息才被消费,那么订单状态会被再次改为已支付。业务异常。

    因此,我们必须想办法保证消息处理的幂等性。这里给出两种方案:

    • 唯一消息ID
    • 业务状态判断

10.4.1 唯一消息ID(存在业务侵入)

这个思路非常简单:

  1. 每一条消息都生成一个唯一的id,与消息一起投递给消费者。
  2. 消费者接收到消息后处理自己的业务,业务处理成功后将消息ID保存到数据库
  3. 如果下次又收到相同消息,去数据库查询判断是否存在,存在则为重复消息放弃处理。
  • 1.SpringAMQP的MeesageConverter自带MessageID的功能
1
2
3
4
5
6
7
8
9
10
11
//以Jackson的消息转换器为例:
//在消费者和生产者的config配置里面添加
@Bean
public MessageConverter messageConverter(){
// 1.定义消息转换器
Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
// 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
jjmc.setCreateMessageIds(true);
// 3.返回jjmc
return jjmc;
}

我们在生产者位置添加:

image-20240402102220873

打开源码可以看到,生成随机id的底层源码:

image-20240402095327684

最终发送一条消息如下:

image-20240402102055345

  • 2.使用Redis缓存(tk使用)

    在调用接口的时候+调用生成随机数的接口生成id(全局唯一)两者合二为一,然后判断是否第一次调用,第一次调用的话业务处理完成之后将{key:id,value:操作结果}+过期时间存入redis数据库;之后每次进行的时候判断是否key存在,存在的话说明重复提交返回错误

10.4.2 业务状态判断

业务判断就是基于业务本身的逻辑或状态来判断是否是重复的请求或消息,不同的业务场景判断的思路也不一样。

相比较而言,消息ID的方案需要改造原有的数据库(会存在业务侵入问题),所以我更推荐使用业务判断的方案。

以支付修改订单的业务为例,我们需要修改OrderServiceImpl中的markOrderPaySuccess方法:处理消息的业务逻辑是把订单状态从未支付修改为已支付。因此我们就可以在执行业务时判断订单状态是否是未支付,如果不是则证明订单已经被处理过,无需重复处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//在原有基础上添加判断订单状态 ---如果不符合直接消息reject了!!!!!!!

@Override
public void markOrderPaySuccess(Long orderId) {
// 1.查询订单
Order old = getById(orderId);
// 2.判断订单状态
if (old == null || old.getStatus() != 1) {
// 订单不存在或者订单状态不是1,放弃处理
return;
}
// 3.尝试更新订单
Order order = new Order();
order.setId(orderId);
order.setStatus(2);
order.setPayTime(LocalDateTime.now());
updateById(order);
}

根据上述代码逻辑可以完成幂等判断需求,但是由于判断和更新是两步动作,可能会在极小概率下可能存在线程安全问题 –> 可以考虑使用==乐观锁(CAS机制)==

1
2
3
4
5
6
7
8
9
10
11
12
//可以将上述三步直接合并为一条sql语句
@Override
public void markOrderPaySuccess(Long orderId) {
// UPDATE `order` SET status = ? , pay_time = ? WHERE id = ? AND status = 1
//mybatisplus的方式
lambdaUpdate()
.set(Order::getStatus, 2)
.set(Order::getPayTime, LocalDateTime.now())
.eq(Order::getId, orderId)
.eq(Order::getStatus, 1)
.update();
}

注意看,上述代码等同于这样的SQL语句:

1
UPDATE `order` SET status = ? , pay_time = ? WHERE id = ? AND status = 1

我们在where条件中除了判断id以外,还加上了status必须为1的条件。如果条件不符(说明订单已支付),则SQL匹配不到数据,根本不会执行。

10.5 兜底方案[消费者定时主动询问]

上述机制可能增加了消息的可靠性,但是也不好说能保证消息100%的可靠。

其实思想很简单:既然MQ通知不一定发送到交易服务(消费者),那么交易服务(消费者)就必须自己主动去查询支付状态。这样即便支付服务的MQ通知失败,我们依然能通过主动查询来保证订单状态的一致

image-20240623151453867

图中黄色线圈起来的部分就是MQ通知失败后的兜底处理方案,由交易服务自己主动去查询支付状态。

什么时候去查询是无法确定的,因此我们通常采用的措施是利用定时任务(例如:SpringTask框架)定期查询。

==可靠性总结图==

综上,支付服务与交易服务之间的订单状态一致性是如何保证的?

  • 首先,支付服务会正在用户支付成功以后利用MQ消息通知交易服务,完成订单状态同步。
  • 其次,为了保证MQ消息的可靠性,我们采用了生产者确认机制、消费者确认、消费者失败重试等策略,确保消息投递的可靠性
  • 最后,我们还在交易服务设置了定时任务,定期查询订单支付状态。这样即便MQ通知失败,还可以利用定时任务作为兜底方案,确保订单支付状态的最终一致性。

image-20240402164059822

==高级进阶 –延迟消息(两种方式)==

在电商的支付业务中,对于一些库存有限的商品,为了更好的用户体验,通常都会在用户下单时立刻扣减商品库存。例如电影院购票、高铁购票,下单后就会锁定座位资源,其他人无法重复购买。

但是这样就存在一个问题,假如用户下单后一直不付款,就会一直占有库存资源,导致其他客户无法正常交易,最终导致商户利益受损!

因此,电商中通常的做法就是:对于超过一定时间未支付的订单,应该立刻取消订单并释放占用的库存

例如,订单支付超时时间为30分钟,则我们应该在用户下单后的第30分钟检查订单支付状态,如果发现未支付,应该立刻取消订单,释放库存。

但问题来了:如何才能准确的实现在下单后第30分钟去检查支付状态呢?

像这种在一段时间以后才执行的任务,我们称之为延迟任务,而要实现延迟任务,最简单的方案就是利用MQ的延迟消息了。

11.延迟消息

延迟消息:生产者发送消息时指定一个时间,消费者不会立刻收到消息,而在指定时间之后才收到消息

延迟任务:设置一定时间之后才执行的任务,(最简单的方案就是利用MQ的延迟消息)

在RabbitMQ中实现延迟消息也有两种方案:

  • 死信交换机+TTL
  • 延迟消息插件

11.1 死信交换机和延迟消息

11.1.1 死信交换机

死信(dead letter)

  • 消费者使用basic.rejectbasic.nack声明消费失败,并且消息的requeue参数设置为false
  • 消息是一个过期消息,超时无人消费
  • 要投递的队列消息满了,无法投递

如果队列设置属性dead-letter-exchange指定交换机 –>该队列的死信就会投递到这个交换机。

这个交换机就叫做死信交换机(Dead letter Exchange,简称DLX)

死信交换机的作用

  1. 收集那些因处理失败而被拒绝的消息

  2. 收集那些因队列满了而被拒绝的消息

  3. 收集因TTL(有效期)到期的消息

11.1.2 延迟消息

总结来说:宏观上看到就是内部做了一个延迟一样

image-20240402113915144

进一步说:变相的让发送消息到消费多了5s

image-20240402114329589

11.2 延迟消息插件(DelayExchange)

RabbitMQ官方提供一款插件,==原生支持延迟消息功能==。

插件原理就是设计了一种支持延迟消息功能的交换机,当消息投递到交换机后可以存放一定时间,到期后再投递到队列。

11.2.1 下载

插件下载地址:
GitHub - rabbitmq/rabbitmq-delayed-message-exchange: Delayed Messaging for RabbitMQ

由于我们安装的MQ是3.8版本,因此这里下载3.8.17版本:

image-20240402133333466

11.2.2 安装

因为我们是基于Docker安装,所以需要先查看RabbitMQ的插件目录对应的数据卷

1
docker volume inspect mq-plugins

结果如下:

image-20240402133421439

插件目录被挂载到了/var/lib/docker/volumes/mq-plugins/_data这个目录,我们上传插件到该目录下。

接下来执行命令,安装插件:

1
docker exec -it mq rabbitmq-plugins enable rabbitmq_delayed_message_exchange

结果如下:

image-20240402134224233

11.2.3 声明延迟交换机

  • 方式一:基于注解方式

image-20240402134800279

  • 方式二:基于@Bean方式

    image-20240402135010167

11.2.4 发送延迟消息

发送消息时:只需要通过设定x-delay属性设定延迟时间:

image-20240402135204953

延迟消息插件内部会维护一个本地数据库表,同时使用Elang Timers功能实现计时。如果消息的延迟时间设置较长,可能会导致堆积的延迟消息非常多,会带来较大的CPU开销,同时延迟消息的时间会存在误差。
因此,不建议设置延迟时间过长的延迟消息

12. 实际操作(日后补充)

==可以参考<RabbitMQ-黑马商城为例>这篇文章,有详细的操作介绍和步骤==

SpringCloud

1.微服务的定义

随着互联网行业的发展对于服务的要求也越来越高,服务架构也从单体架构逐渐演变为微服务架构

1.1 三种架构的对比

  • 单体架构

image-20240316220448340

  • 分布式架构
image-20240316220536632
  • 微服务(优化版分布式架构)

微服务的架构特征:==可以认为微服务是一种经过良好架构设计的分布式架构方案==

  • 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责
  • 自治:团队独立、技术独立、数据独立,独立部署和交付
  • 面向服务:服务提供统一标准的接口,与语言和技术无关
  • 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题

image-20240316220844109

1.2 SpringCloud

SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud。

SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。

其中常见的组件包括:

image-20240316221239275

此外,要考虑很多版本兼容问题。主要是因为SpringCloud底层是依赖于Springboot实现的:

image-20240316221342911

2.服务拆分和远程调用

2.1 服务拆分原则

  • ==什么时候拆分?==

image-20240528141616591

  • ==怎么拆分?==

image-20240528141659955

  • ==服务拆分原则==

​ 不同微服务,不要重复开发相同业务

​ 微服务数据独立,不要访问其它微服务的数据库

​ 微服务可以将自己的业务暴露为接口,供其它微服务调用

image-20240316222315019

2.2 远程调用原则

  • ==远程调用原则==

    image-20240317152606026

3.提供者与消费者

在服务调用关系中,会有两个不同的角色:

服务提供者:一次业务中,被其它微服务调用的服务。(提供接口给其它微服务)

服务消费者:一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口)

image-20240317152829239

但是,服务提供者与服务消费者的角色并不是绝对的,而是相对于业务而言【==相对的==】

如果服务A调用了服务B,而服务B又调用了服务C,服务B的角色是什么?

  • 对于A调用B的业务而言:A是服务消费者,B是服务提供者
  • 对于B调用C的业务而言:B是服务消费者,C是服务提供者

因此,服务B既可以是服务提供者,也可以是服务消费者。

可能会出现三种情况:

1.消费者该如何获取服务提供者具体信息?

2.如果有多个服务提供者,消费者该如何选择?

3.消费者如何感知服务提供者健康状态?

4.Eureka注册中心

4.1 Eureka结构

image-20240417162755947

组成部分:分为eureka-servereureka-client[服务消费者和服务提供者]两个部分

基本步骤:

1.client向注册中心server注册服务信息(告知有哪些服务和端口信息) –每隔30s就重新注册

2.服务消费者从service拉取服务提供者信息

3.服务消费者自己负载均衡配置

4.服务消费者远程调用

==解决上述可能出现的三种情况==

问题1:order-service如何得知user-service实例地址?

获取地址信息的流程如下:

  • user-service服务实例启动后,将自己的信息注册到eureka-server(Eureka服务端)。这个叫服务注册
  • eureka-server保存服务名称到服务实例地址列表的映射关系
  • order-service根据服务名称,拉取实例地址列表。这个叫服务发现/服务拉取

问题2:order-service如何从多个user-service实例中选择具体的实例?

  • order-service从实例列表中利用负载均衡算法选中一个实例地址
  • 向该实例地址发起远程调用

问题3:order-service如何得知某个user-service实例是否依然健康,是不是已经宕机?

  • user-service会每隔一段时间(默认30秒)向eureka-server发起请求,报告自己状态,称为心跳
  • 当超过一定时间没有发送心跳时,eureka-server会认为微服务实例故障,将该实例从服务列表中剔除
  • order-service拉取服务时,就能将故障实例排除了

注意:一个微服务,既可以是服务提供者,又可以是服务消费者,因此eureka将服务注册、服务发现等功能统一封装到了eureka-client端

==分三步进行==

image-20240417164603638

4.2 搭建三步

==总体图==

image-20240421162352729

4.2.1 搭建注册中心

image-20240421161828967

4.2.2 服务注册

image-20240421161959360

4.2.3 服务发现

分为四个步骤:

1和2步骤类似于服务注册,只有spring.application.name= orderservice

image-20240421163002614

3和4步骤更新具体操作

image-20240421162629385

spring会自动帮助我们从eureka-server端,根据userservice这个服务名称,获取实例列表,而后完成负载均衡

5.Ribbon负载均衡

4部分添加@LoadBalanced注解即可实现负载均衡功能

==补充:以前SpringMVC默认是Ribbon负载均衡,后来默认是loadbalancer负载均衡==

5.1 负载均衡原理

其实就是我们发出的请求是http://userservice/user/1,根据负载均衡变成了http://localhost:8081

SpringCloud底层其实是利用了一个名字为==Ribbon组件==来实现负载均衡

image-20240417205259950

5.2 源码跟踪

image-20240417205452975

基本流程如下:

  • 拦截我们的RestTemplate请求http://userservice/user/1
  • RibbonLoadBalancerClient会从请求url中获取服务名称,也就是user-service
  • DynamicServerListLoadBalancer根据user-service到eureka拉取服务列表
  • eureka返回列表,localhost:8081、localhost:8082
  • IRule利用内置负载均衡规则,从列表中选择一个,例如localhost:8081
  • RibbonLoadBalancerClient修改请求地址,用localhost:8081替代userservice,得到http://localhost:8081/user/1,发起真实请求

5.3 负载均衡策略(IRule接口)

5.3.1 负载均衡策略

负载均衡的规则都定义在==IRule接口==中,而IRule有很多不同的实现类:

image-20240417213754364

不同规则的含义如下:

内置负载均衡规则类 规则描述
RoundRobinRule 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。
AvailabilityFilteringRule 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的..ActiveConnectionsLimit属性进行配置。
WeightedResponseTimeRule 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。
ZoneAvoidanceRule[默认] 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。[集群内优先]
BestAvailableRule 忽略那些短路的服务器,并选择并发数较低的服务器。
RandomRule 随机选择一个可用的服务器。
RetryRule 重试机制的选择逻辑

5.3.2 自定义负载均衡策略(消费者)

通过定义IRule实现可以修改负载均衡规则,有两种方式:

代码方式:在order-service中的OrderApplication启动类上定义一个新的IRule:

1
2
3
4
5
//启动类定义一个bean,放入ioc容器
@Bean
public IRule randomRule(){
return new RandomRule();
}

配置文件方式:在order-service的application.yml文件中,添加新的配置:

1
2
3
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡
  • 注意,一般用默认的负载均衡规则,不做修改。

5.4 饥饿加载

Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长

而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:

1
2
3
4
ribbon:
eager-load:
enabled: true
clients: userservice

6.Nacos

SpringCloudAlibaba推出了一个名为Nacos的注册中心,现在是SpringCloud中的一个组件,相比Eureka功能更加丰富,在国内受欢迎程度较高。

主要涉及nacos注册中心和nacos配置管理

6.1 Nacos结构

image-20240421180436014

6.2 搭建三步

6.2.1 搭建注册中心

  • 1.下载

在Nacos的GitHub页面,提供有下载链接,可以下载编译好的Nacos服务端或者源代码:

GitHub主页:https://github.com/alibaba/nacos

GitHub的Release下载页:https://github.com/alibaba/nacos/releases

  • 2.解压

image-20240421164955642

  • 3.端口配置

Nacos的默认端口是8848,如果你电脑上的其它进程占用了8848端口,请先尝试关闭该进程。

如果无法关闭占用8848端口的进程,也可以进入nacos的conf目录,修改配置文件中的端口:

image-20240421165044792

  • 4.启动nacos

image-20240421165146961

6.2.2 服务注册

image-20240421164336864

6.2.3 服务发现

和服务注册类似,导入nacos依赖

image-20240421165732561

6.3 服务分级存储模型

一个服务可以有多个实例,例如我们的user-service,可以有:

  • user-service:127.0.0.1:8081
  • user-service:127.0.0.1:8082
  • user-service:127.0.0.1:8083

假如这些实例分布于全国各地的不同机房,例如:

  • 127.0.0.1:8081,在上海机房
  • 127.0.0.1:8082,在上海机房
  • 127.0.0.1:8083,在杭州机房

Nacos就将同一机房内的实例 划分为一个集群

也就是说,user-service是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如图:

image-20240421170021358

微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。当本集群内不可用时,才访问其它集群。例如:

image-20240421170136721

6.3.1user-service服务提供者配置集群

image-20240421170739795

6.3.2 order-service服务消费者配置集群

image-20240421171305421

6.4 Nacos负载均衡(NacosRule实现)

Ribbon默认的ZoneAvoidanceRule并不能实现根据同集群优先来实现负载均衡。[Eureka默认是同一个zone区域轮询]

6.4.1 修改负载均衡规则

注意:==原来默认是集群间轮询,后来设定nacosrule之后是本地集群优先(本地集群又是随机的 –>可以设置权重提高访问频率),如果本地的都挂了那就会跨集群==

因此Nacos中提供了一个NacosRule的实现,可以优先从==同集群中挑选实例==

修改order-service服务消费者的application.yml文件,修改负载均衡规则:

image-20240421171140243

6.4.2 修改权重

服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。

但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。

因此,Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高。

image-20240421171620311

  • 注意:如果权重修改为0,则该实例永远不会被访问

==如果设备更新/版本升级,那我就可以将权重设置低一点然后少数用户测试,慢慢增大权重==

6.5 环境隔离

类似于rabbitMQ消息队列的virtueHost虚拟主机空间可以造成环境隔离

  • nacos中可以有多个namespace
  • namespace下可以有group、service等
  • 不同namespace之间相互隔离,例如不同namespace的服务互相不可见

image-20240421172511733

6.5.1 网页创建namespace

默认情况下,所有service、data、group都在同一个namespace,名为public:

image-20240421172717407

我们可以点击页面新增按钮,添加一个namespace:

image-20240421172854688

然后,填写表单:

image-20240421172907671

就能在页面看到一个新的namespace:

image-20240421172925184

6.5.2 服务消费者配置

给==微服务配置namespace==只能通过修改配置来实现

image-20240421173139937

6.6 Nacos服务实例

Nacos的服务实例分为两种类型:

  • 临时实例:【默认类型】如果实例宕机超过一定时间,会从服务列表剔除。

  • 非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例。

配置一个服务实例为永久实例:

image-20240421173358174

6.7 Nacos和Eureka对比

  • Nacos与eureka的共同点

    • 都支持服务注册和服务拉取

    • 都支持服务提供者心跳方式做健康检测

  • Nacos与Eureka的区别

    Eureka Nacos
    客户端 ==搭建新的EurekaServer项目==<br 1.pom.xml引入xxx-client依赖
    2.启动类添加@EnableEurekaServer注解
    3.application.yml配置eureka地址
    ==启动app==
    1.父工程引入依赖
    2.pom.xml引入依赖
    服务提供者 1.pom.xml引入xxx-server依赖
    2.application.yml配置eureka地址
    在application.yml配置nacos信息[server-addr地址,cluster-name集群名]
    服务消费者 1.pom.xml引入xxx-server依赖
    2.application.yml配置eureka地址
    3.RestTemlate引入@LoadBalanced注解
    4.修改具体业务
    在application.yml配置nacos信息[server-addr地址和cluster-name集群名,namespace空间名,ephemeral实例类型]
    临时实例 心跳模式【默认每30s进行检测,不正常的会被剔除】 心跳模式【默认每30s进行检测,不正常的会被剔除】
    非临时实例 × 主动监测模式【不会被剔除】
    集群方式 AP方式 【默认】AP方式
    如果集群中存在非临时实例会变成CP模式
    服务列表变更的消息推送模式 定时拉取服务pull 主动推送变更消息push
    负载均衡策略[服务消费者] 【默认】随机
    【IRule】同Zone轮询
    【默认】集群内轮询
    【NacosRule】优先本地集群内 –> 修改权重还可以提高访问概率

6.8 Nacos配置管理

当微服务部署的实例越来越多[达到数百以上],逐步修改微服务配置就会让你抓狂。 –> 所以我们需要一种==统一配置管理方案==,可以集中管理所有实例配置

image-20240421230353712

Nacos一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新

6.8.1 统一配置管理

6.8.1.1 网站添加配置

image-20240421230927123

注意:项目的核心配置,需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。

6.8.1.2 微服务拉取配置

微服务要拉取nacos中管理的配置,并且与本地的application.yml配置合并,才能完成项目启动

但是如果未读取application.yml文件就不能得到nacos地址。【属于是卡bug呢】

–> 因此Spring引入一种新的配置文件:==bootstrap.yaml==文件【会在application.yml之前被读取】

流程如下:

image-20240421231210518

进行步骤:

image-20240423174007971

②引入nacos-config依赖
首先,在user-service服务中,引入nacos-config的客户端依赖:

1
2
3
4
5
<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

③添加bootstrap.yaml

然后,在user-service中添加一个bootstrap.yaml文件,内容如下:

image-20240423174156884

④尝试读取nacos配置

在user-service中的UserController中添加业务逻辑,读取pattern.dateformat配置:

image-20240423174209359

6.8.2 配置热更新

最终目的是修改nacos中的配置后,微服务无需重启即可让配置生效,也就是配置热更新

  • 方式一:在@Value注入变量所在类上添加@RefreshScope注解

image-20240423173252661

  • 方式二:使用@ConfigurationProperties注解

image-20240423173338466

6.8.3 多环境配置共享

可能不同环境下有不同的yaml文件[像单体架构的时候properties,yml,yaml等情况],因此当出现相同属性时就有优先级:==名字越长越牛逼==

image-20240423173524235

6.9 搭建Nacos集群

==日后学习到了补充==

7.HTTP客户端Feign

之前使用的RestTemplate发起远程调用的代码:

image-20240423202621703

存在下面的问题:

•代码可读性差,编程体验不统一

•参数复杂URL难以维护

==Feign==是一个声明式的http客户端。其作用是帮助我们优雅地实现http请求发送,解决了上述的问题

7.1 使用操作

image-20240423211551585

7.1.1 导入依赖

image-20240423211732453

7.1.2 启动类添加注解

启动类上添加注解@EnableFeignClients

image-20240423211805626

7.1.3 编写子服务接口

这样可以相当于http://userservice/user/id

image-20240423211942883

7.1.4 实际操作

底层也有ribbon负载均衡,可以避免了访问ip地址的麻烦。Feign可以将调用步骤放在接口里面,这样使得我们看起来都是直接调用方法统一了。

image-20240423212526274

7.2 自定义配置

Feign运行自定义配置来覆盖默认配置,可以修改的配置如下:

image-20240423213315647

【注意:一般我们需要配置的就是日志级别】

==下面以日志为例来演示如何自定义配置==

而日志的级别分为四种:

  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据

7.2.1 配置文件yml方式–日志

image-20240423213829442

7.2.2 Java代码方式–日志

image-20240423214701673

7.3 Feign使用优化

==Feign底层发起http请求,依赖于其它的框架==。其底层客户端实现包括:

  • URLConnection:[默认]不支持连接池

  • Apache HttpClient :支持连接池

  • OKHttp:支持连接池

7.3.1 连接池优化

==由此可见,对于默认的URLConnection可以更改为Apache HttpClient或者OKHttp方式==

以HttpClient为例:

①pom.xml文件引入依赖

1
2
3
4
5
<!--httpClient的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>

②yml配置文件

1
2
3
4
5
6
feign:
httpclient:
enabled: true # 开启feign对HttpClient的支持
#线程池的核心值需要压测和实际情况调整!!!!!!!!!!!1
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数

7.3.2 日志级别优化

==日志级别最好是用basic或者none==

7.4 最佳实践方案

像7.1使用操作里面用feign替换掉RestTemplate

image-20240423233327633

这种情况下会发现usercontroller和feign客户端代码相似,因此可以提出继承方式和抽取方式

7.4.1 继承方式

一样的代码可以通过继承来共享:

1)定义一个API接口,利用定义方法,并基于SpringMVC注解做声明。

2)Feign客户端和Controller都集成该接口

image-20240423233410310

优点:

  • 简单
  • 实现了代码共享

缺点:

  • 服务提供方、服务消费方紧耦合

  • 参数列表中的注解映射并不会继承,因此Controller中必须再次声明方法、参数列表、注解

7.4.2 抽取方式

将Feign的Client抽取为独立模块,并且把接口有关的POJO、默认的Feign日志配置都放到这个模块中,提供给所有消费者使用。

例如,将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用。

image-20240423233624757

举例:

image-20240423234056492

8.Gateway服务网关

Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的API 路由管理方式

Gateway网关是我们服务的守门神,==所有微服务的统一入口==

网关的核心功能特性:

  • 1.请求路由和负载均衡:一切请求都必须先经过gateway,只根据某种规则把请求转发到某个微服务。如果转发的目标服务有多个时就需要负载均衡
  • 2.权限控制:通过拦截器校验用户是否有请求资格,没有就进行拦截
  • 3.限流:当请求流量过高时,网关中按照下流的微服务能够接受的速度放行请求,避免服务压力过大

image-20240424170653509

在SpringCloud中网关的实现包括两种:

  • gateway【基于Spring5提供的WebFlux,属于响应式编程】
  • zuul【基于Servlet实现,属于阻塞式编程】

Zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能

8.1 Gateway快速入门

演示下网关的基本路由功能。基本步骤如下:

  1. pom.xml导入依赖
  2. application.yml配置基础配置和路由规则
  3. 启动测试

8.3.1 导入依赖

1
2
3
4
5
6
7
8
9
10
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务发现依赖,也是一个微服务需要注册到Nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

8.3.2 编写规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
server:
port: 10010 #网关端口号
spring:
application:
name: gateway #gateway名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
gateway:
routes:
#第一个
- id: user-service #路由表示,必须唯一
uri: lb://userservice #路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: #路由断言,判断是否符合规则
- Path=/user/** #路径断言,判断路径是否以/user开头 --> /user/**转为lb://userservice

#请求头过滤器
#添加一个头结点 --> {Truth,Itcast is freaking awesome!}
filters:
- AddRequestHeader=Truth,Itcast is freaking awesome!
#第二个
- id: order-service #路由表示,必须唯一
uri: lb://orderservice #路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: #路由断言,判断是否符合规则
- Path=/order/** #路径断言,判断路径是否以/order开头 --> /order/**转为lb://orderservice

#默认过滤器
default-filters: # 默认过滤项
- AddRequestHeader=Truth,Itcast is freaking awesome!

#跨域问题
globalcors:
add-to-simple-url-handler-mapping: true #解决options请求被拦截问题
cors-configurations:
'[/**]': #拦截一切请求
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期

总结:

1
2
3
4
路由id:路由唯一标示
uri:路由目的地,支持lb和http两种
predicates:路由断言,判断请求是否符合要求,符合则转发到路由目的地
filters:路由过滤器,处理请求或响应

8.3.3 启动测试

image-20240424171458156

8.2 路由参数3-断言工厂—–==判断是否符合规则==

我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件

例:Path=/user/**是按照路径匹配,这个规则由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理

==要符合条件才能访问转换匹配==

8.2.1 断言工厂分类

从这张图可以看出来可以有很多判断情况来匹配转换,可以通过访问时间,访问ip,访问范围,访问参数等等

image-20240424172200496

8.3 路由参数4-过滤器工厂—–==请求时添加信息==

GatewayFilter是网关中提供的一种过滤器,可以对进入==网关的请求==和==微服务返回的响应==做处理:

image-20240424172659481

8.3.1 路由过滤器种类

image-20240424172758297

8.3.2 请求头过滤器

以AddRequestHeader 为例来讲解:

需求:给所有进入userservice的请求添加一个请求头:Truth=itcast is freaking awesome!

image-20240424173001746

8.3.3 默认过滤器

对所有的路由都生效,需要写到default下面

image-20240424173035471

8.3.4 全局过滤器

需求:自定义拦截请求,前端访问时必须有一个authorization=admin才可以

  • 实现GlobalFilter接口

image-20240424173746934

在filter中编写自定义逻辑,可以实现下列功能:

  • 登录状态判断
  • 权限校验
  • 请求限流等

8.3.5 执行顺序总结

请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter

请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)[==适配器模式]==中,排序后依次执行每个过滤器:

==原因:可以合并是因为路由过滤器和默认过滤器在yml配置的是范围不同,但是底层都是GatewayFilter同一类。而全局过滤器实现GatewayFilter,内部是适配成GatewayFilter==

==总结图:==

image-20240424174011858

  • 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前
  • GlobalFilter过滤器的顺序: 实现Ordered接口/@Order注解,由我们自己指定[@order(数字)]
  • 路由过滤器和defaultFilter的顺序: 由Spring指定,默认是按照声明顺序从1递增[yml文件的书写顺序]。
  • 当三大类过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行

详细内容,可以查看源码:

org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()方法是先加载defaultFilters,然后再加载某个route的filters,然后合并。

org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()方法会加载全局过滤器,与前面的过滤器合并后根据order排序,组织过滤器链

8.4 跨域问题

==跨域==:域名不一致就是跨域,主要包括:

跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题

解决方案:

8.4.1 Gateway配置

在gateway服务的application.yml文件中,添加下面的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
cloud:
gateway:
# 解决跨域问题
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期

8.5 Gateway流程图

image-20240424172300811

发送请求后,可以根据过滤器加一些修改,可以增加头拦截[过滤器]。然后http://localhost:10010/user/1根据断言工厂的断言路径判断/user/**就找到lb:/userservice 就去负载均衡发送给http://localhost:xxxx/user/1

如果是访问orderservice的话在上述过程之后还会使用feign发送请求,feign里面配置的去找userservice/user/id

8.6 yml配置文件详解

image-20240607111353328

9.Springcloud总结

image-20240424175219756

1.服务注册:将网关,userservice,orderservice这些微服务要注册到nacos注册中心

2.服务配置:nacos进行配置管理

3.网关配置和Feign配置:

4.访问转换路径:我们现在访问一个http://localhost:10010/order/101?authorization=songyaxiang,网关这时候分为两个步骤:①经过路由要对符合路由规则的order/**转换为配置的lb或者http:/orderservice微服务去,由于lb还要考虑负载均衡,②要对输入和输出的信息进行修改【可能是新增/删除一些信息】,这时候要考虑过滤器的优先级;之后我们进入orderservice微服务访问使用RestTemplate【会很死板,出现Ip地址信息】/Feign【不会出现IP地址等,选择这个】发送http请求给userservice微服务;最终将结果返回

JUC

1.并发和并行

目前CPU运算速度已经达到百亿次每秒,甚至更高的量级,家用电脑维持操作系统正常运行的进程会有数十个,线程更是数以百计。

所以在现实常见场景中,为了提高生产率和高效地完成任务,处处均采用==多线程==和==并发==的运行方式。

  • ==并发(Concurrency)== 某个时间段内,多任务交替处理的能力

每个CPU将 –(可执行时间)均匀分成若干份–> 每个进程执行一段时间后,记录当前工作状态,释放相关执行资源进入等待状态,让其他进程抢占CPU资源

  • ==并行(Parallelism)== 同时处理多任务的能力

目前CPU已经是多核(可以同时执行多个互不依赖的指令及执行块)

以KTV唱歌为例: 核心在进程是否同时执行

并发指的是同一个话筒被多人轮流使用;

并行指的是有多少人可以使用话筒同时唱歌;

以医生坐诊为例: 并发和并行的

2.线程安全

3.

Apache-Echarts

1.介绍

Apache ECharts 是一款基于 Javascript 的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表

官网地址:https://echarts.apache.org/zh/index.html

image-20240109171236231

2.入门案例

1.在 https://www.jsdelivr.com/package/npm/echarts 选择 dist/echarts.js,点击保存为 echarts.js 文件

image-20240109172602826

2.在保存echarts.js文件的目录下创建一个index.html文件,内容如下:

1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<!-- 引入刚刚下载的 ECharts 文件 -->
<script src="echarts.js"></script>
</head>
</html>

3.绘制一个简单图标 –body标签里面添加div块

1
2
3
4
<body>
<!-- 为 ECharts 准备一个定义了宽高的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
</body>

4.通过script标签里面初始化echarts实例并且通过setOption方法生成xxx图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<body>
<!-- 为 ECharts 准备一个定义了宽高的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'));

// 指定图表的配置项和数据
var option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data: ['销量']
},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
};

// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
</script>
</body>

结果如下:

image-20240109173358917

5.完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ECharts</title>
<!-- 引入刚刚下载的 ECharts 文件 -->
<script src="echarts.js"></script>
</head>
<body>
<!-- 为 ECharts 准备一个定义了宽高的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'));

// 指定图表的配置项和数据
var option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data: ['销量']
},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
};

// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
</script>
</body>
</html>

6.具体解析

image-20240109173227149

前端option的大致对比:

image-20240109173517633

==后端只需要看看前端要什么==

微信小程序开发

1.小程序介绍

小程序是一种新的开放能力,开发者可以快速地开发一个小程序。可以在微信内被便捷地获取和传播,同时具有出色的使用体验

官方网址:https://mp.weixin.qq.com/cgi-bin/wx?token=&lang=zh_CN

image-20240106154758794

小程序主要运行微信内部,可通过上述网站来整体了解微信小程序的开发

2.准备工作

开发微信小程序之前需要做如下准备工作:

  • 注册小程序
  • 完善小程序信息
  • 下载开发者工具

1). 注册小程序

注册地址:https://mp.weixin.qq.com/wxopen/waregister?action=step1

image-20240106154849701

2). 完善小程序信息

登录小程序后台:https://mp.weixin.qq.com/

两种登录方式选其一即可

image-20240106154908699

完善小程序信息、小程序类目

image-20240106154921416

查看小程序的 AppID

image-20240106154938792

3). 下载开发者工具

下载地址: https://developers.weixin.qq.com/miniprogram/dev/devtools/stable.html

3.入门案例

3.1 创建小程序项目

image-20240106155417633

3.2 小程序目录结构

3.2.1 开发界面(五个部分)

3.2.2 小程序整体目录结构(三个部分)

image-20240106163632628

3.2.3 每个页面结构(四个部分)

image-20240106163848831

3.3 具体代码

  • 进入到index.wxml,编写页面布局
image-20240106164109283
  • 进入到index.js,编写业务逻辑代码
image-20240106164345127

具体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// index.js
Page({
data:{
msg:'hello world',
nickname: '',
url:''
},

//获取微信用户头像和昵称
getUserInfo(){
//调用weixin内置方法
wx.getUserProfile({
desc: '获取用户信息',
success: (res) =>{
//调试器输出
console.log(res.userInfo)
//为数据赋值
this.setData({
nickname:res.userInfo.nickName,
url:res.userInfo.avatarUrl
})
}
})
},

//获取微信用户的授权码
wxLogin(){
//调用weixin内置方法
wx.login({
desc: '获取微信用户授权码',
success: (res) =>{
//调试器输出
console.log(res.code) //每次登录的授权码不一样(只能使用一次)
//为数据赋值
this.setData({
code:res.code
})
}
})
},

//发送请求
sendRequest(){
//调用weixin内置方法
wx.request({
desc: '获取用户端查询店铺运营状态',
url: 'http://localhost:8080/user/shop/status', //调用后端的接口!!!!!!!!
method: 'GET',
success: (res) =>{
//调试器输出
console.log(res.data) //相应过来的整体json数据

}
})
}

})
  • 进行调试 -点击编译按钮

    image-20240106164444992

  • 一定要先打开后台程序,然后在模拟器中点击

    image-20240106164648572

3.4 发布小程序

image-20240106164811098

八股文整理

1.Java基础

1.1 Java语言有哪些特点

  • 1.==简单易学==(简化版C++)

  • 2.==面向对象==【封装(就是可以通过权限修饰符将属性设置权限,这样只能通过get/set方法获取),继承(父类和子类之间具有继承关系,子类可以继承父类的方法和属性,并且在此基础上进行修改),多态(编译看左边,运行看右边)】

  • 3.==平台无关性==(Java虚拟机实现平台无关性)

  • 4.==支持多线程==(Java支持多线程)

  • 5.==可靠性==(Java具备异常处理和自动内存管理机制)

  • 6.==安全性==(Java提供权限修饰符,限制程序直接访问操作系统资源)

  • 7.支持==网络编程==

  • 8.==跨平台性==:只要操作系统安装jvm就可以运行java程序 (一次编写,随处运行)

1.2 Java三个版本的比较

image-20240105211658267

  • ==Java SE(Java Platform,Standard Edition)==: Java 平台==标准版==,Java 编程语言的基础,它包含了支持 Java 应用程序开发和运行的核心类库以及虚拟机等核心组件。Java SE 可以用于构建==桌面应用程序== / ==简单的服务器应用程序==

  • ==Java EE(Java Platform, Enterprise Edition )==:Java 平台==企业版==,建立在 Java SE 的基础上,包含了支持==企业级应用程序开发==和部署的标准和规范(比如 Servlet、JSP、EJB、JDBC、JPA、JTA、JavaMail、JMS)。 Java EE 可以用于构建分布式、可移植、健壮、可伸缩和安全的服务端 Java 应用程序,例如 Web 应用程序

  • ==Java ME== 是 Java 的微型版本,主要用于开发==嵌入式消费电子设备的应用程序==,例如手机、PDA、机顶盒、冰箱、空调等。Java ME 无需重点关注,知道有这个东西就好了,现在已经用不上了。

简单来说,Java SE 是 Java 的基础版本,Java EE 是 Java 的高级版本。Java SE 更适合开发桌面应用程序或简单的服务器应用程序,Java EE 更适合开发复杂的企业级应用程序或 Web 应用程序

1.3 JVM & JDK & JRE 三者对比

image-20240105212044287
  • ==JDK==: Java程序开发工具包=jre+开发人员使用的工具(例如javac编译工具)
  • ==JRE==: Java程序运行时环境=jvm+运行时需要的核心类库 [有了jre其实就可以运行代码]
  • ==JVM==: 运行Java字节码的虚拟机,JVM针对不同系统有特定实现(使用相同的字节码.class,得出相同结果) –> 一次编译,随处可以运行

    image-20240105213023670

1.4 字节码(.class文件)

==字节码==:JVM可以理解的代码,不面向任何特定的处理器,只面向虚拟机JVM。

  • Java程序执行过程:
image-20240105213658490

引入==JIT编译器(运行时编译)==,完成第一次编译之后就会将字节码对应的机器码保存下来,下次可以直接使用。机器码效率 > java解释器,所以Java是编译和解释共存的语言

image-20240105213914539

HotSpot 采用了==惰性评估(Lazy Evaluation)==的做法,根据==二八定律==,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。

JDK、JRE、JVM、JIT 这四者的关系如下图所示

image-20240105214120303

1.5 Java语言是”编译和解释并存”

这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。

因为 Java 程序要经过==先编译,后解释==两个步骤,Java 编写的程序需要先经过编译步骤,生成字节码(.class 文件),这种字节码必须由 Java 解释器来解释执行

1.6 Java和C++区别

Java 不提供指针来直接访问内存,程序内存更加安全

Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。

Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。

C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。

1.7 Java三种注释

  • ==单行注释== 通常用于解释方法内某单行代码的作用
  • ==多行注释== 通常用于解释一段代码的作用
  • ==文档注释(Java特有)== 通常用于生成 Java 开发文档
image-20240105215518448

注释并不会执行(编译器在编译代码之前会把代码中的所有注释抹掉,字节码(.class)中不保留注释)

代码的注释不是越详细越好。实际上好的代码本身就是注释,我们要尽量规范和美化自己的代码来减少不必要的注释。

若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述

1.8 Java结构和格式

  • Java结构和语法格式
1
2
3
4
5
6
7
8
9
10
权限修饰符 class 类名{
权限修饰符 方法1(){ //每一级都需要缩进 每一级都需要{}配对
语法1;
语法2;
}
权限修饰符 方法2(){
语法1;
语法2;
}
}
  • Java程序入口 –main方法
1
2
3
4
5
6
7
8
9
public static void main(String[] args){
//解释说明:
public:公共的,用它修饰的类或成员在任意位置可见 [权限修饰符]
static:静态的,用它修饰的方法,可以不用创建对象就可以调用 [标识了静态就可以调用静态和非静态的,但是非静态的只能调用非静态的]
void:表示该方法没有返回值 [return;即可]
main:Java的主方法名,JavaSE的程序入口
String[]:字符串数组,这是main方法的形参类型,可以通过命令行参数传值
args:这是main方法的形参名,如果要在main中使用命令行参数,可以遍历该args数组。
}
  • Java程序输出
1
2
3
4
5
6
//有两种情况
//1.换行 --输出内容(可以忽略)
System.out.println(输出内容);

//2.不换行 --输出内容(不可以忽略)
System.out.print(输出内容);
  • 源文件名和类名

    • ==一致性==:

      如果一个类写了很多类,==源文件名(.java)必须和public的class类名一致==(不一致会编译报错),可以和其他类不一致(不便于代码维护)

    • ==多样性==:

      一个源文件可以有多个类,但是一个源文件只有一个public类

1.9 标识符和关键字

  • ==标识符==:只要你自己要定义的都可以叫做标识符
    • 区别大小写
    • 长度无限制
    • 只能由这四种组成:
      • ①26个英文字母 (可以当做开头)
      • ②数字0-9 【如果可做开头的话就不知道指代数字本身还是变量所对应的值 int 123L=12; long l=123L; 这样的话l最后是123还是12】
      • ③ 下划线_ (可以当做开头)
      • ④ 美元符号$ (可以当做开头)
    • 可以包含关键字和保留字,但是不可以使用
    • 四种情况的命名规范:
      • ①包名 —所有的字母都小写 — java.lang com.at.bean
      • ②类名/接口名 —所有的单词首字母大写 —ShopController
      • ③变量名/方法名 —第一个单词的首字母小写,后面的单词首字母都大写 —getById()
      • ④常量名 —所有的字母大写,中间用下划线连接 — MAX_SHOP_VALUES
  • ==关键字==: 专门用途的字符串/单词
    • 全部为小写,一共有50个(const和goto是保留字,true和false和null是字面量)
image-20240105221810897

其中,default可以在程序控制,可以当做修饰符,还可以设置为访问权限

在程序控制中,当在 switch 中匹配不到任何情况时,可以使用 default 来编写默认匹配的情况。

在类,方法和变量修饰符中,从 JDK8 开始引入了默认方法,可以使用 default 关键字来定义一个方法的默认实现。

在访问控制中,如果一个方法前没有任何修饰符,则默认会有一个修饰符 default,但是这个修饰符加上了就会报错。

1.10 自增自减运算符

1.11 移位运算符

1.12 阿松大

,