01谷粒商城-高级篇一
前言
自省 自行 自醒
102~171
1.EleasticSearch全文检索
1.1简介
https://www.elastic.co/cn/what-is/elasticsearch
全文搜索属于最常见的需求,开源的 Elasticsearch 是目前全文搜索引擎的首选。 它可以快速地储存、搜索和分析海量数据。维基百科、Stack Overflow、Github 都采用它
Elastic 的底层是开源库 Lucene。但是,你没法直接用 Lucene,必须自己写代码去调用它的 接口。Elastic 是 Lucene 的封装,提供了 REST API 的操作接口,开箱即用。
REST API:天然的跨平台。
官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
官方中文:https://www.elastic.co/guide/cn/elasticsearch/guide/current/foreword_id.html
社区中文:https://doc.codingdict.com/elasticsearch/0/
Index(索引):
动词,相当于 MySQL 中的 insert
名词,相当于 MySQL 中的 Database
Type(类型):
在 Index(索引)中,可以定义一个或多个类型。
类似于 MySQL 中的 Table;每一种类型的数据放在一起
Document(文档):
保存在某个索引(Index)下,某种类型(Type)的一个数据(Document),文档是 JSON 格 式的,Document 就像是 MySQL 中的某个 Table 里面的内容
倒排索引:
1.2Docker安装ES
1.3Docker安装Kibana
1.4入门-cat
查看所有节点
GET /_cat/nodes
http://192.168.188.180:9200/_cat/nodes
查看 es 健康状况
GET /_cat/health
http://192.168.188.180:9200/_cat/health
查看主节点
GET /_cat/master
http://192.168.188.180:9200/_cat/master
查看所有索引 show databases
GET /_cat/indices
http://192.168.188.180:9200/_cat/indices
1.5入门-put&post新增数据
PUT
新增数据
PUT 可以新增可以修改。PUT 必须指定 id;由于 PUT 需要指定 id,我们一般都用来做修改 操作,不指定 id 会报
PUT customer/external/1
http://192.168.188.180:9200/customer/external/1
{
"name": "Peng"
}
POST
新增数据
POST customer/external/2
http://192.168.188.180:9200/customer/external/2
{
"name": "Tom"
}
1.6入门-get&乐观锁
GET查询文档
GET customer/external/1
http://192.168.188.180:9200/customer/external/1
结果:
{
"_index": "customer", //索引
"_type": "external", //类型
"_id": "1", //记录Id
"_version": 1, //版本号
"_seq_no": 0, //并发控制字段,每次更新就会+1,用来做乐观锁
"_primary_term": 1, //同上,主分片重新分配,如重启,就会变化
"found": true, //查询到数据
"_source": { //真正的内容
"name": "Peng"
}
}
乐观锁修改:
我们查询customer/external/1
发现:
_seq_no
: 0_primary_term
: 1
然后带上?if_seq_no=0&if_primary_term=1
查询出来的参数进行第一次PUT
修改,修改成功
http://192.168.188.180:9200/customer/external/1?if_seq_no=0&if_primary_term=1
{
"name": "Jack"
}
再次带上?if_seq_no=0&if_primary_term=1
进行PUT
修改发现修改失败了,查询出来的customer/external/1
值发生了改变:
_seq_no
: 3_primary_term
: 1
http://192.168.188.180:9200/customer/external/1?if_seq_no=0&if_primary_term=1
{
"name": "Peng"
}
如果想要修改成功,需要携带最新查询的 _seq_no
和_primary_term
,此时修改成功,这就是乐观锁
http://192.168.188.180:9200/customer/external/1?if_seq_no=3&if_primary_term=1
{
"name": "Peng"
}
1.7入门-put&post修改数据
POST
修改数据_update
方式
POST customer/external/1/_update
http://192.168.188.180:9200/customer/external/1/_update
{
"doc":{
"name": "John Doew"
}
}
POST
修改数据
POST customer/external/1
http://192.168.188.180:9200/customer/external/1/
{
"name": "Marry"
}
PUT
修改数据
PUT customer/external/1
http://192.168.188.180:9200/customer/external/1
{
"name": "Tony"
}
总结:
POST
操作会对比源文档数据,如果相同不会有什么操作,文档version
不增加PUT
操作总会将数据重新保存并增加version
版本- 带
_update
对比元数据如果一样就不进行任何操作。
场景:
- 对于大并发更新,不带
update
; - 对于大并发查询偶尔更新,带
update
; - 对比更新,重新计算分配规则
更新同时增加属性
POST
更新同时增加属性
POST customer/external/1
http://192.168.188.180:9200/customer/external/1
{
"name": "Peng",
"age":1
}
POST
带_update
更新同时增加属性
POST customer/external/1/_update
http://192.168.188.180:9200/customer/external/1
{
"doc":{
"name": "John Doew",
"age": 18
}
}
PUT
更新同时增加属性
PUT customer/external/1
http://192.168.188.180:9200/customer/external/1
{
"name": "Tony",
"age":1
}
1.8入门-删除数据&bulk批量导入测试数据
删除文档
DELETE customer/external/1
http://192.168.188.180:9200/customer/external/1
删除索引
DELETE customer
http://192.168.188.180:9200/customer
bulk批量API
-
第一行:定义了批量操作的类型和目标索引,以及文档的
_id
。 -
第二行:提供了要插入或更新的文档内容。
POST customer/external/_bulk
{"index":{"_id":"1"}}
{"name": "Peng" }
{"index":{"_id":"2"}}
{"name": "Tom" }
语法格式
{ action: { metadata }}
{ request body }
{ action: { metadata }}
{ request body }
复杂实例
bulk API
以此按顺序执行所有的 action(动作)。如果一个单个的动作因任何原因而失败, 它将继续处理它后面剩余的动作。当 bulk API
返回时,它将提供每个动作的状态(与发送 的顺序相同),所以您可以检查是否一个指定的动作是不是失败了。
- 第一行:
_bulk
命令 - 第二行:这表示删除
_index
为website
,_type
为blog
,_id
为123
的文档。 - 第三行:表示在
_index
为website
,_type
为blog
,_id
为123
的位置创建 - 第四行:创建一个新文档,文档内容是
{ "title": "My first blog post" }
。 - 第五行:这表示在
_index
为website
,_type
为blog
的位置索引一个新文档 - 第六行:文档内容是
{ "title": "My second blog post" }
。 - 第七行:更新
_index
为website
,_type
为blog
,_id
为123
的文档,_retry_on_conflict
,表示在冲突时最多重试 3 次。 - 第七行:更新文档的
title
字段为"My updated"
。
我这里可能是版本问题没使用_retry_on_conflict
POST /_bulk
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "My first blog post" }
{ "index": { "_index": "website", "_type": "blog" }}
{ "title": "My second blog post" }
{ "update": { "_index": "website", "_type": "blog", "_id": "123", "_retry_on_conflict": 3 } }
{ "doc" : {"title" : "My updated blog post" }}
样本测试数据
https://github.com/elastic/elasticsearch/blob/7.5/docs/src/test/resources/accounts.json
POST bank/account/_bulk
bank测试数据
查看已经创建的索引
1.9进阶-俩种查询方式
ES 支持两种基本方式检索 :
-
一个是通过使用 REST request URI 发送搜索参数(uri+检索参数)
-
另一个是通过使用 REST request body 来发送它们(uri+请求体)
检索 bank 下所有信息,包括 type 和 doc
GET bank/_search
请求参数方式检索
-
GET bank/_search
:指定了从bank
索引中执行搜索操作。 -
q=\*
:使用 Query String 方式,表示匹配所有文档。 -
sort=account_number:asc
:按照account_number
字段的升序排序。
GET bank/_search?q=*&sort=account_number:asc
uri+请求体进行检索
-
GET bank/_search
:指定了从bank
索引中执行搜索操作。 -
query
部分:使用了match_all
查询,该查询匹配索引中的所有文档,因此返回所有文档。 -
sort
部分:指定了排序规则,按照account_number
字段的值降序排序。
GET bank/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"account_number":
{
"order": "desc"
}
}]
}
1.10进阶-QueryDSL&match_all
基本语法格式
Elasticsearch 提供了一个可以执行查询的 Json 风格的 DSL(domain-specific language 领域特 定语言)。这个被称为 Query DSL。该查询语言非常全面,并且刚开始的时候感觉有点复杂, 真正学好它的方法是从一些基础的示例开始的
典型结构
{
QUERY_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,...
}
如果是针对某个字段,那么它的结构如下
{
QUERY_NAME: {
FIELD_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,...
}
}
例子
-
查询所有文档:使用
match_all
查询。 -
排序:按
account_number
字段降序排序。 -
分页:从第0个文档开始,取前5个文档。
-
选择字段:只返回
balance
和firstname
字段。
GET bank/_search
{
"query": {
"match_all": {
}
},
"sort": [
{
"account_number": {
"order": "desc"
}
}
],
"from": 0,
"size": 5
"_source":["balance","firstname"]
}
1.11进阶-match全文检索
基本类型(非字符串),精确匹配
match 返回 account_number=20 的数据
GET bank/_search
{
"query": {
"match": {
"account_number":20
}
}
}
字符串,全文检索
最终查询出 address 中包含 mill 单词的所有记录
match 当搜索字符串类型的时候,会进行全文检索,并且每条记录有相关性得分。
GET bank/_search
{
"query": {
"match": {
"address":"mill"
}
}
}
字符串,多个单词(分词+全文检索)
最终查询出 address 中包含 mill 或者 road 或者 mill road 的所有记录,并给出相
GET bank/_search
{
"query": {
"match": {
"address": "mill road"
}
}
}
1.12进阶-match_phrase短语匹配
将需要匹配的值当成一个整体单词(不分词)进行检索
查出 address 中包含 mill road 的所有记录,并给出相关性得分
GET bank/_search
{
"query": {
"match_phrase": {
"address": "mill road"
}
}
}
1.13进阶-multi_match多字段匹配
state 或者 addresss 包含 mill
GET bank/_search
{
"query": {
"multi_match": {
"query": "mill",
"fields": ["state","address"]
}
}
}
1.14进阶-bool复合查询
bool 用来做复合查询: 复合语句可以合并 任何 其它查询语句,包括复合语句,了解这一点是很重要的。这就意味 着,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑。
must:必须达到 must 列举的所有条件
GET bank/_search
{
"query": {
"bool": {
"must": [
{ "match": { "address": "mill" }},
{ "match": {"gender": "M" }}
]
}
}
}
should:应该达到 should 列举的条件,如果达到会增加相关文档的评分,并不会改变 查询的结果。如果 query 中只有 should 且只有一种匹配规则,那么 should 的条件就会 被作为默认匹配条件而去改变查询结果
GET bank/_search
{
"query": {
"bool": {
"must": [
{"match": { "address": "mill" }},
{"match": { "gender": "M"}}
],
"should": [
{"match": { "address": "lane" }}
]
}
}
}
must_not 必须不是指定的情况
-
address 包含 mill,并且 gender 是 M
-
如果 address 里面有 lane 最好不过
-
但是 email 必须不包含 baiuba.com
GET bank/_search
{
"query": {
"bool": {
"must": [
{ "match": { "address": "mill" }},
{ "match": { "gender": "M" } }
],
"should": [
{"match": { "address": "lane" }}
],
"must_not": [
{"match": { "email": "baiuba.com" }}
]
}
}
}
1.15进阶-filter过滤
并不是所有的查询都需要产生分数,特别是那些仅用于 “filtering”(过滤)的文档。
为了不 计算分数 Elasticsearch 会自动检查场景并且优化查询的执行。
GET bank/_search
{
"query": {
"bool": {
"must": [
{ "match": { "address": "mill" }}
],
"filter": [
{
"range": {
"balance": {
"gte": 10000,
"lte": 20000
}
}}
]
}
}
}
1.16进阶-term查询
和 match 一样。匹配某个属性的值。全文检索字段用 match,其他非 text 字段匹配用 term。
GET bank/_search
{
"query": {
"bool": {
"must": [
{ "term": { "age": { "value": 28 } } },
{ "match": { "address": "990 Mill Road" } }
]
}
}
}
1.17进阶-aggregation聚合分析
聚合提供了从数据中分组和提取数据的能力。最简单的聚合方法大致等于 SQL GROUP BY 和 SQL 聚合函数。在 Elasticsearch 中,您有执行搜索返回 hits(命中结果),并且同时返 回聚合结果,把一个响应中的所有 hits(命中结果)分隔开的能力。这是非常强大且有效的, 您可以执行查询和多个聚合,并且在一次使用中得到各自的(任何一个的)返回结果,使用 一次简洁和简化的 API 来避免网络往返。
搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄,但不显示这些人的详情。
GET bank/_search
{
"query": { "match": { "address": "mill" } },
"aggs": { "group_by_state": { "terms": { "field": "age" } },
"avg_age": { "avg": { "field": "age" } }
},
"size": 0
}
按照年龄聚合,并且请求这些年龄段的这些人的平均薪资
GET bank/account/_search
{
"query": { "match_all": { } },
"aggs": {
"age_avg": {
"terms": {
"field": "age",
"size": 1000
},
"aggs": {
"banlances_avg": {
"avg": {
"field": "balance"
}
}
}
}
},
"size": 1000
}
复杂:查出所有年龄分布,并且这些年龄段中 M 的平均薪资和 F 的平均薪资以及这个年龄 段的总体平均薪资
GET bank/account/_search
{
"query": { "match_all": { }},
"aggs": {
"age_agg": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"gender_agg": {
"terms": {
"field": "gender.keyword",
"size": 100
},
"aggs": {
"balance_avg": {
"avg": {
"field": "balance"
}
}
}
},
"balance_avg": {
"avg": {
"field": "balance"
}
}
}
}
},
"size": 1000
}
1.18映射-Mapping创建
字段类型
- 文本类型(Text Types):
text
:适用于全文搜索的字段,例如文章内容。keyword
:适用于精确值的字段,例如标签、分类等。
- 数值类型(Numeric Types):
integer
:32 位有符号整数。`long
:64 位有符号整数。、float
:32 位 IEEE 754 浮点数。double
:64 位 IEEE 754 浮点数。half_float
:16 位 IEEE 754 浮点数。scaled_float
:用于高精度浮点数存储,存储时会乘以一个缩放因子。
- 日期类型(Date Types):
date
:日期类型,支持多种日期格式和时间戳。
- 布尔类型(Boolean Type):
boolean
:用于表示true
或false
值。
- 二进制类型(Binary Type):
binary
:用于存储二进制数据。
- 范围类型(Range Types):
integer_range
:用于存储整数范围。float_range
:用于存储浮点数范围。long_range
:用于存储长整数范围。double_range
:用于存储双精度浮点数范围。date_range
:用于存储日期范围。
- 地理类型(Geo Types):
geo_point
:用于存储地理位置(经纬度)。geo_shape
:用于存储复杂地理形状。
- 特定用途类型(Specialized Types):
ip
:用于存储 IP 地址。completion
:用于自动补全建议。token_count
:用于存储文本字段的令牌数量。
映射
Mapping(映射) Mapping 是用来定义一个文档(document),以及它所包含的属性(field)是如何存储和 索引的。比如,使用 mapping 来定义:
-
哪些字符串属性应该被看做全文本属性(full text fields)。
-
哪些属性包含数字,日期或者地理位置。
-
文档中的所有属性是否都能被索引(_all 配置)。
-
日期的格式。
-
自定义映射规则来执行动态添加属性。
-
查看 mapping 信息:GET bank/_mapping
-
修改 mapping 信息:[Mapping | Elasticsearch Guide 8.14] | Elastic
新版本改变
Es7 及以上移除了 type 的概念。
- 关系型数据库中两个数据表示是独立的,即使他们里面有相同名称的列也不影响使用, 但 ES 中不是这样的。elasticsearch 是基于 Lucene 开发的搜索引擎,而 ES 中不同 type 下名称相同的 filed 最终在 Lucene 中的处理方式是一样的。
- 两个不同 type 下的两个 user_name,在 ES 同一个索引下其实被认为是同一个 filed, 你必须在两个不同的 type 中定义相同的 filed 映射。否则,不同 type 中的相同字段 名称就会在处理中出现冲突的情况,导致 Lucene 处理效率下降。
- 去掉 type 就是为了提高 ES 处理数据的效率。
Elasticsearch 7.x
- URL 中的 type 参数为可选。比如,索引一个文档不再要求提供文档类型
Elasticsearch 8.x
- 不再支持 URL
解决:
-
1)、将索引从多类型迁移到单类型,每种类型文档一个独立索引
-
2)、将已存在的索引下的类型数据,全部迁移到指定位置即可。详见数据迁移
创建映射
PUT /my-index
{
"mappings": {
"properties": {
"age": { "type": "integer" },
"email": { "type": "keyword" },
"name": { "type": "text" }
}
}
}
1.19映射-添加新的字段映射
PUT /my-index/_mapping
{
"properties": {
"employee-id": {
"type": "keyword",
"index": false
}
}
}
1.20映射-修改映射&数据迁移
对于已经存在的映射字段,我们不能更新。更新必须创建新的索引进行数据迁移
先创建出 new_twitter 的正确映射。然后使用如下方式进行数据迁移
#
POST _reindex
{
"source": { "index": "twitter" },
"dest": { "index": "new_twitter" }
}
将旧索引的 type 下的数据进行迁移
# [固定写法]
POST _reindex
{
"source": { "index": "twitter","type": "tweet" },
"dest": { "index": "new_twitter" }
}
例子
# 查看my-index
GET /my-index
# 创建my-index-source
PUT /my-index-source
{
"mappings": {
"properties": {
"age": { "type": "integer" },
"email": { "type": "keyword" },
"name": { "type": "text" }
}
}
}
# 将my-index数据迁移到my-index-source
POST _reindex
{
"source": { "index": "my-index" },
"dest": { "index": "/my-index-source" }
}
1.21分词-分词&安装ik分词
分词
一个 tokenizer(分词器)接收一个字符流,将之分割为独立的 tokens(词元,通常是独立 的单词),然后输出 tokens 流。 例如,whitespace tokenizer 遇到空白字符时分割文本。它会将文本 "Quick brown fox!" 分割 为 [Quick, brown, fox!]。 该 tokenizer(分词器)还负责记录各个 term(词条)的顺序或 position 位置(用于 phrase 短 语和 word proximity 词近邻查询),以及 term(词条)所代表的原始 word(单词)的 start (起始)和 end(结束)的 character offsets(字符偏移量)(用于高亮显示搜索的内容)。 Elasticsearch 提供了很多内置的分词器,可以用来构建 custom
安装ik分词
查看数据卷
docker volume ls
查看数据卷mall_elasticsearch_plugins
docker volume inspect mall_elasticsearch_plugins
可以看到elasticsearch
的插件目录挂载到了/var/lib/docker/volumes/mall_elasticsearch_plugins/_data
这个目录。
我们需要把IK
分词器上传至这个目录。这里解压完后命名elasticsearch-analysis-ik-7.12.1
改为了ik
查看elasticsearch
版本
ik
下载地址:https://github.com/infinilabs/analysis-ik/releases
搜索对应的版本然后下载
这里解压完后命名elasticsearch-analysis-ik-7.12.1
改为了ik
,然后上传到/var/lib/docker/volumes/mall_elasticsearch_plugins/_data
这个目录。
检查自己的文件
重启es
docker restart es
进入es
容器,查看es是否安装ik
docker exec -it elasticsearch bash
bin/elasticsearch-plugin list
测试分词器
# 使用默认
POST _analyze
{
"text": "我是中国人"
}
# 使用分词器
POST _analyze
{
"analyzer": "ik_smart",
"text":"我是中国人"
}
# ik_max_word
POST _analyze
{
"analyzer": "ik_max_word", "text": "我是中国人"
}
1.22补充-修改Linux网络设置&卡其root密码访问
安装nginx
随便启动一个 nginx 实例,只是为了复制出配置
docker run -p 80:80 --name nginx -d nginx:latest
将容器内的配置文件拷贝到当前目录
docker container cp nginx:/etc/nginx .
修改文件名称
mv nginx conf
把这个 conf 移动到/mall/nginx 下
mkdir nginx
mv conf nginx
终止原容器
docker stop nginx
执行命令删除原容器
docker rm nginx
创建html
、logs
cd nginx
mkdir html
mkdir logs
创建新的 nginx
注意这里的目录挂在是相对路径,在配置的mall
目录下执行的
docker run -p 80:80 --name nginx \
-v ./nginx/html:/usr/share/nginx/html \
-v ./nginx/logs:/var/log/nginx \
-v ./nginx/conf:/etc/nginx \
-d nginx:latest
因为我们修改了端口号,所以要进入容器修改端口号配置
我们把容器下的文件拷贝出来
docker cp nginx:/etc/nginx/conf.d/default.conf /root/mall/nginx/default.conf
配置8001
端口号
再把本地修改好的覆盖到容器内
docker cp /root/mall/nginx/default.conf nginx:/etc/nginx/conf.d/default.conf
重启
docker restart nginx
我们在./nginx/html
目录下新建index.html
,nginx
默认访问index.html
访问:http://192.168.188.180:8001/index.html
1.23分词-自定已扩展库
配置elasticsearch
mkdir elasticsearch
cd elasticsearch
mkdir config
mkdir data
mkdir plugins
chmod -R 777 /mall/elasticsearch
# 暂不执行
# echo "http.host: 0.0.0.0" >> # #/mall/elasticsearch/config/elasticsearch.yml
上传ik
到elasticsearch
映射plugins
目录
重启elasticsearch
使用nginx自定义扩展库
保证nginx
安装完成
找到/root/mall/elasticsearch/plugins/ik/config/IKAnalyzer.cfg.xml
配置分词地址
http://192.168.188.180:8001/ik/myword.txt
我们在/root/mall/nginx/html
创建ik
目录,并且创建myword.txt
,配置几个自定义分词
访问http://192.168.188.180:8001/ik/myword.txt,可以忽略乱码
配置完成后,重启elasticsearch
docker restart elasticsearch
测试自定分词
1.24整合-SpringBoot整合hign-level-client
主要步骤:
- 导入依赖
- 编写配置
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.0</version>
<scope>compile</scope>
</dependency>
编写配置
测试
@RunWith(SpringRunner.class)
@SpringBootTest
class GulimallSearchApplicationTest {
@Autowired
private RestHighLevelClient client;
@Test
public void contextLoads(){
System.out.println(client);
}
}
我的这个版本不知道为什么配置@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
还是需要mybatis-plus
配置
所以我还是添加了Product
的CRUD
1.25整合-测试保存
代码
/**
* 测试ES数据
* 更新也可以
*/
@Test
public void saveIndexData() throws IOException {
IndexRequest indexRequest = new IndexRequest("users");
indexRequest.id("1");
User user = new User();
user.setUserName("Peng");
user.setAge("18");
user.setGender("男");
String jsonStr = JSON.toJSONString(user);
indexRequest.source(jsonStr, XContentType.JSON);
//执行操作
IndexResponse index = client.index(indexRequest, com.peng.search.config.GulimallElasticSearchConfig.COMMON_OPTIONS);
//提取有用的响应数据
System.out.println(index);
}
测试
# 查看所有索引
GET /_cat/indices?v
# 查询users
GET users/_search
1.26整合-测试复杂检索
kibana
查询
GET bank/_search
{
"query": {
"match": {
"address": "mill"
}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 10
}
},
"ageAvg":{
"avg": {
"field": "age"
}
},
"balanceAvg":{
"avg": {
"field": "balance"
}
}
},
"size": 0
}
代码查询
@Test
public void searchData() throws IOException {
// 创建检索请求
SearchRequest searchRequest = new SearchRequest();
// 指定索引
searchRequest.indices("bank");
// 构造检索条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchQuery("address","Mill"));
// 按照年龄分布进行聚合
TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
sourceBuilder.aggregation(ageAgg);
// 计算平均年龄
AvgAggregationBuilder ageAvg = AggregationBuilders.avg("ageAvg").field("age");
sourceBuilder.aggregation(ageAvg);
// 计算平均薪资
AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");
sourceBuilder.aggregation(balanceAvg);
System.out.println("检索条件:" + sourceBuilder);
searchRequest.source(sourceBuilder);
// 执行检索
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
System.out.println("检索结果:" + searchResponse);
//3. 将检索结果封装为Bean
SearchHits hits = searchResponse.getHits();
SearchHit[] searchHits = hits.getHits();
for (SearchHit searchHit : searchHits) {
String sourceAsString = searchHit.getSourceAsString();
Account account = JSON.parseObject(sourceAsString, Account.class);
System.out.println(account);
}
//4. 获取聚合信息
Aggregations aggregations = searchResponse.getAggregations();
Terms ageAgg1 = aggregations.get("ageAgg");
for (Terms.Bucket bucket : ageAgg1.getBuckets()) {
String keyAsString = bucket.getKeyAsString();
System.out.println("年龄:" + keyAsString + " ==> " + bucket.getDocCount());
}
Avg ageAvg1 = aggregations.get("ageAvg");
System.out.println("平均年龄:" + ageAvg1.getValue());
Avg balanceAvg1 = aggregations.get("balanceAvg");
System.out.println("平均薪资:" + balanceAvg1.getValue());
}
@ToString
@Data
static class Account {
private int account_number;
private int balance;
private String firstname;
private String lastname;
private int age;
private String gender;
private String address;
private String employer;
private String email;
private String city;
private String state;
}
2.商品业务
2.1商品上架-sku在es中存储模型分析
第一种方式:
- 方便检索,
sku
和spu
放在一起 attr
(SPU
基础属性)冗余:100万 * 2KB = 1000000 * 2KB = 2000MB = 2G内存- 如果有百万并发,每次
attr
占用2KB,那就需要2G内存
- 如果有百万并发,每次
{
skuId:1,
spuId:11,
skuTitle:华为xxx,
price:6999,
saleCount:999
...,
attrs:[ //SPU基础属性
{尺寸:6.7},
{CPU:麒麟980},
{屏幕:华星光电}
]
}
第二种方式:
- 商品的
SPU
规格是动态计算出来的,搜索手机会把所有手机结果的attr
属性聚合
- 搜索小米:手机,电器,粮食
- 搜索小米有10000个结果,4000个
spuId
- 4000个
spuId
对应的所有可能属性 - esClient查询:
spuId
:[4000个spuId] 因为spuId
是long
型数据,4000 * 8byte = 32000byte = 32KB - 10000个人检索数据:10000 * 32KB = 320MB,如果是百万并发就是1000000 * 32KB = 32000MB = 32G,光网络阻塞时间就会很长
- 空间和时间不可能同时获得,用时间换空间,或者空间换时间
- 搜索小米有10000个结果,4000个
SKU索引
{
skuId:1,
spuId:11,
skuTitle:华为xxx,
price:6999,
saleCount:999
...,
}
attr索引
{
spuId:11,
attrs:[ //SPU基础属性
{尺寸:6.7},
{CPU:麒麟980},
{屏幕:华星光电}
]
}
商品Mapping
-
1)、检索的时候输入名字,是需要按照 sku 的 title 进行全文检索的
-
2)、检索使用商品规格,规格是 spu 的公共属性,每个 spu 是一样的
-
3)、按照分类 id 进去的都是直接列出 spu 的,还可以切换。
-
4)、我们如果将 sku 的全量信息保存到 es 中(包括 spu 属性)就太多量字段了。
-
5)、我们如果将 spu 以及他包含的 sku 信息保存到 es 中,也可以方便检索。但是 sku 属于 spu 的级联对象,在 es 中需要 nested 模型,这种性能差点。
-
6)、但是存储与检索我们必须性能折中。
-
7)、如果我们分拆存储,spu 和 attr 一个索引,sku 单独一个索引可能涉及的问题。 检索商品的名字,如“手机”,对应的 spu 有很多,我们要分析出这些 spu 的所有关联属性, 再做一次查询,就必须将所有 spu_id 都发出去。假设有 1 万个数据,数据传输一次就 10000*4=4MB;并发情况下假设 1000 检索请求,那就是 4GB 的数据,,传输阻塞时间会很 长,业务更加无法继续。 所以,我们如下设计,这样才是文档区别于关系型数据库的地方,宽表设计,不能去考虑数 据库范式
# product 的 mapping
PUT product
{
"mappings": {
"properties": {
"skuId": {
"type": "long"
},
"spuId": {
"type": "keyword"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"skuPrice": {
"type": "keyword"
},
"skuImg": {
"type": "keyword",
"index": false,
"doc_values": false
},
"saleCount": {
"type": "long"
},
"hasStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"brandId": {
"type": "long"
},
"catalogId": {
"type": "long"
},
"brandName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"brandImg": {
"type": "keyword",
"index": false,
"doc_values": false
},
"catalogName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"attrValue": {
"type": "keyword"
}
}
}
}
}
}
2.2商品上架-nested数据类型场景
地址:https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html
PUT my-index-000001/_doc/1
{
"group" : "fans",
"user" : [
{
"first" : "John",
"last" : "Smith"
},
{
"first" : "Alice",
"last" : "White"
}
]
}
GET my-index-000001/_search
{
"query": {
"bool": {
"must": [
{ "match": { "user.first": "Alice" }},
{ "match": { "user.last": "Smith" }}
]
}
}
}
查询Alice Smith
不应该查询出数据
修改users
为nested
类型
# 删除现有的索引,如果你确认不需要其中的数据
DELETE /my-index-000001
PUT /my-index-000001
{
"mappings": {
"properties": {
"user": {
"type": "nested"
}
}
}
}
PUT my-index-000001/_doc/1
{
"group" : "fans",
"user" : [
{
"first" : "John",
"last" : "Smith"
},
{
"first" : "Alice",
"last" : "White"
}
]
}
GET my-index-000001/_search
{
"query": {
"bool": {
"must": [
{ "match": { "user.first": "Alice" }},
{ "match": { "user.last": "Smith" }}
]
}
}
}
查询不出来了
2.3商品上架-构造基本数据
主要步骤:
- 1.查询当前
spuId
对应的所有sku
信息,品牌的名称 - 2.封装每个
sku
信息
在common-util
模块创建SkuEsModel
@Data
public class SkuEsModel {
private Long skuId;
private Long spuId;
private String skuTitle;
private BigDecimal skuPrice;
private String skuImg;
private Long saleCount;
private Boolean hasStock;
private Long hotScore;
private Long brandId;
private Long catalogId;
private String brandName;
private String brandImg;
private String catalogName;
private List<Attrs> attrs;
@Data
public static class Attrs {
private Long attrId;
private String attrName;
private String attrValue;
}
}
2.4商品上架-构造sku检索属性
主要步骤:
-
1.查出当前sku的所有可以被用来检索的规格属性
- 根据
spuId
查询所有的基础属性
- 根据
-
2.在指定的所有属性集合里面,挑出检索属性
//TODO 4、查出当前sku的所有可以被用来检索的规格属性
List<ProductAttrValueEntity> baseAttrs = productAttrValueService.baseAttrListforspu(spuId);
List<Long> attrIds = baseAttrs.stream().map(attr -> {
return attr.getAttrId();
}).collect(Collectors.toList());
List<Long> searchAttrIds = attrService.selectSearchAttrs(attrIds);
//转换为Set集合
Set<Long> idSet = searchAttrIds.stream().collect(Collectors.toSet());
根据spuId
查询所有可以检索的基础属性
<select id="selectSearchAttrIds" resultType="java.lang.Long">
SELECT attr_id FROM pms_attr WHERE attr_id IN
<foreach collection="attrIds" item="id" separator="," open="(" close=")">
#{id}
</foreach>
AND search_type = 1
</select>
2.5商品上架-远程查询库存&泛型结果封装
主要步骤:
- 远程查询库存
- 获取当前skuId的库存总和减去锁定库存总和
- 泛型结果封装
根据sku_id
查询商品所有库存,然后减去锁定库存
Long getSkuStock(@Param("skuId") Long skuId);
<select id="getSkuStock" resultType="java.lang.Long">
SELECT SUM(stock - stock_locked) FROM wms_ware_sku WHERE sku_id = #{skuId}
</select>
创建WareFeignService
,远程调用获取验证商品是否有库存接口
@FeignClient("gulimall-ware")
public interface WareFeignService {
/**
* 1、CouponFeignService.saveSpuBounds(spuBoundTo);
* 1)、@RequestBody将这个对象转为json。
* 2)、找到gulimall-coupon服务,给/coupon/spubounds/save发送请求。
* 将上一步转的json放在请求体位置,发送请求;
* 3)、对方服务收到请求。请求体里有json数据。
* (@RequestBody SpuBoundsEntity spuBounds);将请求体的json转为SpuBoundsEntity;
* 只要json数据模型是兼容的。双方服务无需使用同一个to
* @param spuBoundTo
* @return
*/
@PostMapping(value = "/ware/waresku/hasStock")
R getSkuHasStock(@RequestBody List<Long> skuIds);
}
泛型结果封装
/**
* 1.R设计的时候可以加上泛型
* 2.直接返回我们想要的结果
* 3.自己封装解析结果
* */
public R setData(Object data) {
put("data",data);
return this;
}
2.6商品上架-远程上架
主要步骤:
- 在
gulimall-search
中添加上架商品接口,使用BulkRequest
批量添加 - 如果商品上架成功,更新商品状态为已上架
在com.peng.search.controller.ElasticSaveController
创建productStatusUp
商品上架接口,使用BulkRequest
批量添加skuEsModels
如果商品上架成功,更新商品状态为已上架
void updaSpuStatus(Long spuId, int code);
<update id="updaSpuStatus">
UPDATE pms_spu_info SET publish_status = #{code} ,update_time = NOW() WHERE id = #{spuId}
</update>
完整代码
// @Transactional(rollbackFor = Exception.class)
@Override
public void up(Long spuId) {
//1、查出当前spuId对应的所有sku信息,品牌的名字
List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);
//TODO 4、查出当前sku的所有可以被用来检索的规格属性
List<ProductAttrValueEntity> baseAttrs = productAttrValueService.baseAttrListforspu(spuId);
List<Long> attrIds = baseAttrs.stream().map(attr -> {
return attr.getAttrId();
}).collect(Collectors.toList());
List<Long> searchAttrIds = attrService.selectSearchAttrs(attrIds);
//转换为Set集合
Set<Long> idSet = searchAttrIds.stream().collect(Collectors.toSet());
List<SkuEsModel.Attrs> attrsList = baseAttrs.stream().filter(item -> {
return idSet.contains(item.getAttrId());
}).map(item -> {
SkuEsModel.Attrs attrs = new SkuEsModel.Attrs();
BeanUtils.copyProperties(item, attrs);
return attrs;
}).collect(Collectors.toList());
List<Long> skuIdList = skuInfoEntities.stream()
.map(SkuInfoEntity::getSkuId)
.collect(Collectors.toList());
//TODO 1、发送远程调用,库存系统查询是否有库存
Map<Long, Boolean> stockMap = null;
try {
R skuHasStock = wareFeignService.getSkuHasStock(skuIdList);
//
TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {};
stockMap = skuHasStock.getData(typeReference).stream()
.collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
} catch (Exception e) {
log.error("库存服务查询异常:原因{}",e);
}
//2、封装每个sku的信息
Map<Long, Boolean> finalStockMap = stockMap;
List<SkuEsModel> collect = skuInfoEntities.stream().map(sku -> {
//组装需要的数据
SkuEsModel esModel = new SkuEsModel();
esModel.setSkuPrice(sku.getPrice());
esModel.setSkuImg(sku.getSkuDefaultImg());
//设置库存信息
if (finalStockMap == null) {
esModel.setHasStock(true);
} else {
esModel.setHasStock(finalStockMap.get(sku.getSkuId()));
}
//TODO 2、热度评分。0
esModel.setHotScore(0L);
//TODO 3、查询品牌和分类的名字信息
BrandEntity brandEntity = brandService.getById(sku.getBrandId());
esModel.setBrandName(brandEntity.getName());
esModel.setBrandId(brandEntity.getBrandId());
esModel.setBrandImg(brandEntity.getLogo());
CategoryEntity categoryEntity = categoryService.getById(sku.getCatalogId());
esModel.setCatalogId(categoryEntity.getCatId());
esModel.setCatalogName(categoryEntity.getName());
//设置检索属性
esModel.setAttrs(attrsList);
BeanUtils.copyProperties(sku,esModel);
return esModel;
}).collect(Collectors.toList());
//TODO 5、将数据发给es进行保存:gulimall-search
R r = searchFeignService.productStatusUp(collect);
if (r.getCode() == 0) {
//远程调用成功
//TODO 6、修改当前spu的状态
this.baseMapper.updaSpuStatus(spuId, ProductConstant.ProductStatusEnum.SPU_UP.getCode());
} else {
//远程调用失败
//TODO 7、重复调用?接口幂等性:重试机制
}
}
2.7商品上架-上架结果调试&feign源码
商品上架主要步骤:
- 1.查出当前
spuId
对应的所有sku
信息,品牌的名字(pms_sku_info
) - 2.查出当前
spuId
的所有的规格属性(pms_product_attr_value
) - 3.查出当前
spuId
的所有可以检索的规格属性(pms_attr:search_type = 1
) - 4.查询当前商品可以检索的的所有规格属性
- 5.根据获取的所有的
sku
,远程调用gulimall-ware
查询是否有库存- 遍历所有的
skuId
集合,获取当前skuId
的库存信息,库存总和 - 锁定库存总和 > 0
就是有库存
- 遍历所有的
- 6.封装每个
sku
的信息- 基本的信息(
pms_sku_info
):skuId
、spuId
、skuTitle
、skuPrice
、skuImg
、saleCount
、skuPrice
、skuPrice
、skuPrice
- 库存信息(远程调用
gulimall-ware
查询是否有库存):HasStock
- 热度评分:
HotScore
(默认为0) - 品牌和分类信息:
brandId
、brandName
、brandImg
、catalogId
、catalogName
- 检索属性(
pms_attr:search_type = 1
):Attrs
- 基本的信息(
- 7.远程调用
gulimall-search
将sku
保存到es
,使用BulkRequest
批量保存- 成功:修改当前
spu
的状态为已上架 - 失败:重复调用?接口幂等性:重试机制
- 成功:修改当前
调试完源码,成功后使用kibana
发现数据全部保存成功
feign源码
调用流程:
- 1.构造请求数据,将对象转换为json
RequestTemplate template = buildTemplateFromArgs.create(argv)
- 2.发送请求进行执行(执行成功会解码相应数据)
executeAndDecode(template)
- 3.执行请求会有重试机制
while(true){
try{
executeAndDecode(template);
}catch(){
try{retryer.continueOrPropagate(e);}catch(){throw ex;}
continue;
}
}
错误
使用com.alibaba.fastjson
的JSON.toJSONString()
会出现$ref
循环依赖,导致序列化失败,这里使用了cn.hutool
工具包的序列化方法
JSONUtil.toJsonStr()
导入包cn.hutool
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.11</version>
</dependency>
2.8商品上架-抽取响应结果&上架测试完成
es保存
boolean hasFailures = bulk.hasFailures();
返回true
代表有错误
抽取响应结果
上架测试完成
3.商城业务
3.1首页-thymeleaf渲染首页
导入模板引擎
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
导入静态资源
- 创建
static.index
、templates
目录 index
目录拷贝到static.index目录
index.html
拷贝到templates
目录
配置thymeleaf
spring:
thymeleaf:
cache: false
访问http://localhost:8204/、http://localhost:8204/index/css/GL.css成功代表配置成功
shift
点击2下搜索WebMvcAutoConfigurationAdapter
3.2首页-整合dev-tools渲染一级分类数据
主要步骤:
- 1.创建
IndexController
,并且获取一级分类数据,返回给页面 - 2.导入
devtools
,修改完页面ctrl + shift + F9
重新构建
创建indexPage
方法
@GetMapping(value = {"/","index.html"})
private String indexPage(Model model) {
//1、查出所有的一级分类
List<CategoryEntity> categoryEntities = categoryService.getLevel1Categorys();
model.addAttribute("categories",categoryEntities);
return "index";
}
实现查询一级分类
@Override
public List<CategoryEntity> getLevel1Categorys() {
System.out.println("getLevel1Categorys........");
long l = System.currentTimeMillis();
List<CategoryEntity> categoryEntities = this.baseMapper.selectList(
new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
System.out.println("消耗时间:"+ (System.currentTimeMillis() - l));
return categoryEntities;
}
导入devtools
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
动态遍历categories
<ul>
<li th:each="category : ${categories}">
<a href="#" class="header_main_left_a" th:attr="ctg-data = ${category.catId}"><b th:text="${category.name}">家用电器</b></a>
</li>
</ul>
ctrol + shift + f9
刷新页面
3.3首页-整合dev-tools渲染二级三级分类数据
主要步骤:
- 1.删除
index/json/catalog.json
- 2.修改
catalogLoader.js
请求地址 - 3.创建
getCatalogJson
方法,获取二级三级分类
.删除index/json/catalog.json
修改catalogLoader.js
请求地址
创建getCatalogJson
方法,获取二级三级分类
3.4nginx-搭建域名访问环境一(方向代理配置)
SwitchHosts
下载地址:https://github.com/oldj/SwitchHosts/releases
下载自己对应的版本,我是win64
配置自己的虚拟机的地址和域名
192.168.188.180 gulimall
配置nginx
docker
启动时启动nginx
docker update nginx --restart=always
启动nginx
docker start nginx
查看nginx
配置文件发现我们的配置都在conf.d
目录下
找到conf.d/default.conf
cd conf.d/
ls
vim default.conf
查看本机ip地址
ipconfig
创建gulimall.conf
cd conf.d/
ls
cp default.conf gulimall.conf
ls
vim gulimall.conf
配置conf.d/default.conf
,配置ip为上一步查看的,指向地址为本机的gulimall-product
server {
listen 80;
listen [::]:80;
server_name gulimall.com;
location / {
proxy_pass http://192.168.188.1:8204;
}
}
我之前是使用的8001,访问域名需要加上端口,很别扭,还是改回80端口
# 停止nginx
docker stop nginx
# 删除nginx
docker rm nginx
# 启动nginx
docker run -p 80:80 --name nginx \
-v ./nginx/html:/usr/share/nginx/html \
-v ./nginx/logs:/var/log/nginx \
-v ./nginx/conf:/etc/nginx \
-d nginx:latest
然后修改conf.d/default.conf
访问http://gulimall.com/,页面出来配置成功
错误:如果开启了vpn,可能会导致访问失败
可以暂时关掉vpn
3.5nginx-搭建域名访问环境二(负载均衡到网关)
找到nginx.conf
配置上游网关地址,192.168.188.1:8200
是你本地gulimall-gateway
网关服务的地址
upstream gulimall{
server 192.168.188.1:8200;
}
配置服务地址,添加请求头
server {
listen 80;
listen [::]:80;
server_name gulimall.com;
location / {
proxy_set_header Host $host;
proxy_pass http://gulimall;
}
}
配置网关地址,一定要配置到最后,要不然无法访问接口,gulimall-product
服务接口地址也匹配该规则
- id: gulimall_host_route
uri: lb://gulimall-product
predicates:
- Host=**.gulimall.com,gulimall.com
重启网关服务和nginx
访问http://gulimall.com/、http://gulimall.com/product/attr/list显示界面和结果配置完成
总结
4.性能压测
4.1压力测试-基本介绍
压力测试考察当前软硬件环境下系统所能承受的最大负荷并帮助找出系统瓶颈所在。压测都 是为了系统在线上的处理能力和稳定性维持在一个标准范围内,做到心中有数。
使用压力测试,我们有希望找到很多种用其他测试方法更难发现的错误。有两种错误类型是: 内存泄漏,并发与同步。
有效的压力测试系统将应用以下这些关键条件:重复,并发,量级,随机变化。
性能指标
- 响应时间(Response Time: RT) 响应时间指用户从客户端发起一个请求开始,到客户端接收到从服务器端返回的响 应结束,整个过程所耗费的时间。
- HPS(Hits Per Second) :每秒点击次数,单位是次/秒。
- TPS(Transaction per Second):系统每秒处理交易数,单位是笔/秒。
- QPS(Query per Second):系统每秒处理查询次数,单位是次/秒。 对于互联网业务中,如果某些业务有且仅有一个请求连接,那么 TPS=QPS=HPS,一 般情况下用 TPS 来衡量整个业务流程,用 QPS 来衡量接口查询次数,用 HPS 来表 示对服务器单击请求。
- 无论 TPS、QPS、HPS,此指标是衡量系统处理能力非常重要的指标,越大越好,根据经 验,一般情况下:
- 金融行业:1000TPS~50000TPS,不包括互联网化的活动
- 保险行业:100TPS~100000TPS,不包括互联网化的活动
- 制造行业:10TPS~5000TPS
- 互联网电子商务:10000TPS~1000000TPS
- 互联网中型网站:1000TPS~50000TPS
- 互联网小型网站:500TPS~10000TPS
- 最大响应时间(Max Response Time) 指用户发出请求或者指令到系统做出反应(响应) 的最大时间。
- 最少响应时间(Mininum ResponseTime) 指用户发出请求或者指令到系统做出反应(响 应)的最少时间。
- 90%响应时间(90% Response Time) 是指所有用户的响应时间进行排序,第 90%的响应时间。
- 从外部看,性能测试主要关注如下三个指标
- 吞吐量:每秒钟系统能够处理的请求数、任务数。
- 响应时间:服务处理一个请求或一个任务的耗时。
- 错误率:一批请求中结果出错的请求所占比例。
4.2压力测试-Apache JMeter安装使用
安装
地址:https://jmeter.apache.org/download_jmeter.cgi
下载
解压后,运行jmeter.bat
使用
添加线程组
线程组参数详解:
- 线程数:虚拟用户数。一个虚拟用户占用一个进程或线程。设置多少虚拟用户数在这里 也就是设置多少个线程数。
- Ramp-Up Period(in seconds)准备时长:设置的虚拟用户数需要多长时间全部启动。如果 线程数为 10,准备时长为 2,那么需要 2 秒钟启动 10 个线程,也就是每秒钟启动 5 个 线程。
- 循环次数:每个线程发送请求的次数。如果线程数为 10,循环次数为 100,那么每个线 程发送 100 次请求。总请求数为 10*100=1000 。如果勾选了“永远”,那么所有线程会 一直发送请求,一到选择停止运行脚本。
- Delay Thread creation until needed:直到需要时延迟线程的创建。
- 调度器:设置线程组启动的开始时间和结束时间(配置调度器时,需要勾选循环次数为 永远)
- 持续时间(秒):测试持续时间,会覆盖结束时间
- 启动延迟(秒):测试延迟启动时间,会覆盖启动时间
- 启动时间:测试启动时间,启动延迟会覆盖它。当启动时间已过,手动只需测试时当前 时间也会覆盖它。
- 结束时间:测试结束时间,持续时间会覆盖它。
2000个线程10s创建完,不循环执行
添加HTTP请求
查看结果树
汇总报告
聚合报告
汇总图
测试百度,执行完查看结果
测试gulimall.com
4.3压力测试-JMeter在windows下地址占用bug解决
windows 本身提供的端口访问机制的问题。
Windows 提供给 TCP/IP 链接的端口为 1024-5000,并且要四分钟来循环回收他们。就导致 我们在短时间内跑大量的请求时将端口占满了。
主要步骤:
-
1.cmd 中,用 regedit 命令打开注册表
-
2.在
计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters
-
右击 parameters,添加一个新的 DWORD,名字为 MaxUserPort
-
然后双击 MaxUserPort,输入数值数据为 65534,基数选择十进制(如果是分布式运 行的话,控制机器和负载机器都需要这样操作哦)
-
TCPTimedWaitDelay:30
-
-
修改配置完毕之后记得重启机器才会生效
4.4性能监控-堆内存与垃圾回收
jvm 内存模型
- 程序计数器 Program Counter Register:
- 记录的是正在执行的虚拟机字节码指令的地址
- 此内存区域是唯一一个在JAVA虚拟机规范中没有规定任何OutOfMemoryError的区 域
- 虚拟机:VM Stack
- 描述的是 JAVA 方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧, 用于存储局部变量表,操作数栈,动态链接,方法接口等信息
- 局部变量表存储了编译期可知的各种基本数据类型、对象引用
- 线程请求的栈深度不够会报 StackOverflowError 异常
- 栈动态扩展的容量不够会报 OutOfMemoryError 异常
- 虚拟机栈是线程隔离的,即每个线程都有自己独立的虚拟机栈
- 本地方法:Native Stack
- 本地方法栈类似于虚拟机栈,只不过本地方法栈使用的是本地方法
- 堆:Heap
- 几乎所有的对象实例都在堆上分配内存
堆
所有的对象实例以及数组都要在堆上分配。堆是垃圾收集器管理的主要区域,也被称为“GC 堆”;也是我们优化最多考虑的地方。 堆可以细分为:
- 新生代
- Eden 空间
- From Survivor 空间
- To Survivor 空间
- 老年代
- 永久代/元空间
- Java8 以前永久代,受 jvm 管理,java8 以后元空间,直接使用物理内存。因此, 默认情况下,元空间的大小仅受本地内存限制
从 Java8 开始,HotSpot 已经完全将永久代(Permanent Generation)移除,取而代之的是一 个新的区域—元空间(MetaSpace)
4.4性能监控-jvisualvm使用
jconsole
在cmd中输入jconsole
查看堆
jvisualvm
在cmd中输入jvisualvm
监控内存泄露,跟踪垃圾回收,执行时内存、cpu 分析,线程分析...
- 运行:正在运行的
- 休眠:sleep
- 等待:wait
- 驻留:线程池里面的空闲线程
- 监视:阻塞的线程,正在等待锁
安装插件Visual GC
解决503
- 打开网址https://visualvm.github.io/pluginscenters.html
- cmd 查看自己的 jdk 版本,找到对应的
- 复制下面查询出来的链接。并重新设置上即可
我是202
版本,所以JDK 8 Update 131 - 351
中131 - 351
包含202
,可以使用该版本
设置该插件地址
4.5优化-中间件对性能的影响
中间件指标
- 当前正在运行的线程数不能超过设定的最大值。一般情况下系统性能较好的情况下,线 程数最小值设置 50 和最大值设置 200 比较合适。
- 当前运行的 JDBC 连接数不能超过设定的最大值。一般情况下系统性能较好的情况下, JDBC 最小值设置 50 和最大值设置 200 比较合适。
- GC频率不能频繁,特别是 FULL GC 更不能频繁,一般情况下系统性能较好的情况下, JVM 最小堆大小和最大堆大小分别设置 1024M 比较合适
数据库指标
- SQL 耗时越小越好,一般情况下微秒级别。
- 命中率越高越好,一般情况下不能低于 95%。
- 锁等待次数越低越好,等待时间越短越好。
测试线程
nginx测试
Gateway测试
在gulimall-product
添加接口
@ResponseBody
@GetMapping(value = "/hello")
public String hello() {
return "hello";
}
简单服务测试
配置网关服务
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**,/hello
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
Gateway + 简单服务测试
压测内容
压测内容 | 压测线程数 | 吞吐量/s | 90%响应时间 | 99%响应时间 |
---|---|---|---|---|
Nginx 192.168.188.180:80 | 50 | 29981 | 2 | 8 |
Gateway localhost:8200 | 50 | 21112 | 3 | 6 |
简单服务 localhost:8204/hello | 50 | 27481 | 3 | 7 |
Gateway + 简单服务 localhost:8200/hello | 50 | 8152 | 11 | 20 |
nginx + Gateway + 简单服务 gulimall.com/hello | 50 | 3740 | 17 | 29 |
nginx + Gateway + 简单服务 gulimall.com/hello 测试结果
4.6优化-简单优化吞吐量测试
修改gulimall-product
配置
pms_category
的创建索引
create index idx_parentcid on mall_pms.pms_category(parent_cid);
全量页面(js、静态页面),我这里程序崩了没测成功
压测内容 | 压测线程数 | 吞吐量/s | 90%响应时间 | 99%响应时间 |
---|---|---|---|---|
首页 localhost:8204 | 50 | 900 | 84 | 140 |
首页(缓存、日志、数据库)localhost:8204 | 50 | 1983 | 35 | 64 |
首页(缓存、日志、数据库、全量页面)localhost:8204 | 50 | 3964 | 0 | 1 |
首页 localhost:8204测试结果
首页(缓存、日志、数据库)localhost:8204 测试结果
首页(缓存、日志、数据库、全量页面)localhost:8204 测试结果
4.7优化-nginx动静分离
上传首页静态资源到/root/mall/nginx/html
目录下
在/root/mall/nginx/html
创建目录static
,并且上传首页资源index
配置/root/mall/nginx/conf/conf.d/gulimall.conf
server {
listen 80;
listen [::]:80;
server_name gulimall.com;
location /static/ {
root /usr/share/nginx/html;
}
location / {
proxy_set_header Host $host;
proxy_pass http://gulimall;
}
}
重启nginx
docker restart nginx
修改gulimall-product
的配置文件,关闭thymeleaf
缓存
修改src/main/resources/templates/index.html
,所有的静态资源路径都加上/static/
访问http://gulimall.com/
4.8优化-模拟线上应用内存崩溃宕机情况
压测首页(缓存、日志、数据库、全量页面、动静分离)gulimall.com
开启thymeleaf
缓存
设置gulimall-product
的
-Xmx1024m
:最大内存1024m-Xms1024m
:初始1024m-Xmn512m
:伊甸园区和幸存者区设置为512m
-Xmx1024m -Xms1024m -Xmn512m
测试gulimall-product
设置全量页面
结果
4.9优化-优化三级分类数据获取
主要步骤:
-
优化业务
-
多次查询优化为查询一次
修改gulimall-product
的运行内存为100m
-Xmx100m
压测
http://localhost:8204/index/catalog.json
结果
5.缓存
5.1缓存使用-本地缓存与分布式缓存
哪些数据适合放入缓存?
-
即时性、数据一致性要求不高的
-
访问量大且更新频率不高的数据(读多,写少)
data = cache.load(id);//从缓存加载数据
If(data == null){
data = db.load(id);//从数据库加载数据
cache.put(id,data);//保存到 cache 中
}
return data;
本地缓存
分布式缓存
分布式系统下一般不使用本地缓存
缓存中间件Redis
5.2缓存使用-整合Redis测试
主要步骤:
- 1.导入
spring-boot-starter-data-redis
- 2.配置
Redis
- 3.使用
SpringBoot
自动配置好的RedisTemplate
或者StringRedisTemplate
导入spring-boot-starter-data-redis
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置Redis
spring:
redis:
host: 192.168.188.180
使用StringRedisTemplate
@SpringBootTest
class GulimallProductApplicationTest {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
public void testStringRedisTemplate() {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
// 保存
ops.set("hello", "world_" + UUID.randomUUID().toString());
// 查询
String hello = ops.get("hello");
System.out.println("hello:" + hello);
}
}
查看RedisAutoConfiguration
,自动帮忙注入了RedisTemplate<Object, Object>
和StringRedisTemplate
5.3缓存使用-改造三级分类业务
主要步骤:
- 1.加入缓存逻辑,缓存中的数据是json字符串(JSON跨语言,跨平台兼容)
- 2.如果缓存中没有,查询数据库
- 3.查询的数据放入缓存,将对象转换为json放在缓存中,然后查询的新数据直接返回
- 4.缓存中存在直接从缓存获取返回
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
// 1.加入缓存逻辑,缓存中的数据是json字符串
// JSON跨语言,跨平台兼容
String catalogJson = (String)redisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJson)) {
// 2.如果缓存中没有,查询数据库
Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
String s= JSON.toJSONString(catalogJsonFromDb);
// 3.查询的数据放入缓存,将对象转换为json放在缓存中,然后查询的新数据直接返回
redisTemplate.opsForValue().set("catalogJSON", s);
return catalogJsonFromDb;
}
// 4.缓存中存在直接从缓存获取返回
Map<String, List<Catelog2Vo>> res = JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
return res;
}
重启gulimall-product
,访问http://localhost:8204/index/catalog.json
除了第一次需要查询数据库,后面方位的速度都是几毫秒
5.4缓存使用-压力测试出的内存泄漏及解决
产生堆外内存溢出:OutOfDirectMemoryError
- 1.springboot2.0以后默认使用lettuce作为操作redis的客户端。它能使用netty进行网络通信。
- 2.lettuce的bug导致netty堆外内存溢出
-Xmx100m
;netty如果没有指定对外内存,默认使用设置的-Xmx100m
解决方案:
- 升级lettuce客户端
- 切换使用jedis
redisTemplate:lettuce、jedis操作redis的底层客户端。Spring再次封装redisTemplate
压测
结果,我这里没有报错,可是我使用的Springboot2.7.12版本已经修复了
如果出现了OutOfDirectMemoryError,导入spring-boot-starter-data-redis
排除lettuce-core
,导入jedis
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
5.5缓存使用-缓存击穿、穿透、雪崩
缓存穿透
缓存穿透:指查询一个不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
风险:利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
解决:null结果缓存,并加入短暂过期时间。
缓存雪崩
缓存雪崩:缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩
解决:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效事件。
缓存击穿
缓存击穿:
- 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据
- 如果这个key在大量请求的同时进来正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿
解决:加锁,大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db
5.6缓存使用-加锁解决缓存击穿问题
主要步骤:
- 1.请求进来了先查询缓存,缓存不存在查询数据库
- 2.查询数据库的时候使用本地锁
synchronized
,保证只有一个请求查询数据然后更新缓存 - 3.查询数据库的时候需要先判断缓存时候存在,因为可能首次请求已经查询了数据库并更新了缓存
- 4.我们在锁里即要查询数据库,也要更新缓存,要不然第二次请求可能在第一次请求查询完成未更新缓存的时候再次查询缓存
删除缓存,压测接口
在查询数据库的时候加上日志,我们发现加了本地锁synchronized
,依然查询了2次
因为首次请求在查询到数据库时就释放了锁,还没来得及更新到缓存,第二次请求进来时因为第一次没有及时更新缓存所以又查询了一次
所以在首次查询完数据库时,也应该在锁里把数据更新到缓存
再次清空缓存,进行压测,控制台输出就查询了一次
代码
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
// 1.加入缓存逻辑,缓存中的数据是json字符串
// JSON跨语言,跨平台兼容
String catalogJson = (String)redisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJson)) {
// 2.如果缓存中没有,查询数据库
System.out.println("缓存不命中...查询数据库...");
Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJson2();//getCatalogJsonFromDb();
return catalogJsonFromDb;
}
System.out.println("缓存命中...直接返回...");
// 4.缓存中存在直接从缓存获取返回
Map<String, List<Catelog2Vo>> res = JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
return res;
}
public Map<String, List<Catelog2Vo>> getCatalogJson2() {
synchronized (this){
// 第一个请求进来 得到锁 查询db并更新更新缓存
// 第二个请求进来 如果第一个请求此时并未更新缓存,会再次查询数据库,所以需要再次获取缓存
// 得到锁以后,我们应该再去缓存中确定一次,如果没有才继续查询
String catalogJson = (String)redisTemplate.opsForValue().get("catalogJSON");
if (!StringUtils.isEmpty(catalogJson)) {
// 2.如果缓存中没有,查询数据库
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
return result;
}
System.out.println("查询了数据库111");
//将数据库的多次查询变为一次
List<CategoryEntity> selectList = this.baseMapper.selectList(null);
//1、查出所有分类
//1、1)查出所有一级分类
List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
//封装数据
Map<String, List<Catelog2Vo>> parentCid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//1、每一个的一级分类,查到这个一级分类的二级分类
List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
//2、封装上面的结果
List<Catelog2Vo> catelog2Vos = null;
if (categoryEntities != null) {
catelog2Vos = categoryEntities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName().toString());
//1、找当前二级分类的三级分类封装成vo
List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId());
if (level3Catelog != null) {
List<Catelog2Vo.Category3Vo> category3Vos = level3Catelog.stream().map(l3 -> {
//2、封装成指定格式
Catelog2Vo.Category3Vo category3Vo = new Catelog2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return category3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(category3Vos);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
String s= JSON.toJSONString(parentCid);
// 3.查询的数据放入缓存,将对象转换为json放在缓存中,然后查询的新数据直接返回
redisTemplate.opsForValue().set("catalogJSON", s);
return parentCid;
}
}
5.7缓存使用-本地锁在分布式下的问题
本地锁,只能锁住当前进程,所以我们需要分布式锁
我们在添加2个gulimall-product
服务 ,把3个gulimall-product
服务全部启动
--server.port=8214
--server.port=8224
因为启动了3个gulimall-product
服务,我们需要通过请求nginx
转发网关服务gulimall-gateway
负载均衡到3个gulimall-product
服务
清除redis缓存,设置压测参数,启动测试
我们发现3个gulimall-product
都查询了一次数据库
5.1分布式锁-分布式锁原理与使用
主要步骤:
- 1.占分布式锁,去redis占坑
- 2.设置过期时间,必须和加锁是同步的,保证原子性(避免死锁)
- 3.
lua
脚本解锁,保证原子性,删除锁只删除自己当前进程的(uuid
)
分布式锁基本原理
Redis分布式锁
https://redis.ac.cn/docs/manual/patterns/distributed-locks/
启动3个gulimall-product服务
压测
我们发现只有8224的gulimall-product服务
查询了一次数据库
代码
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1、占分布式锁。去redis占坑 设置过期时间必须和加锁是同步的,保证原子性(避免死锁)
String uuid = UUID.randomUUID().toString();
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if (lock) {
System.out.println("获取分布式锁成功...");
Map<String, List<Catelog2Vo>> dataFromDb = null;
try {
//加锁成功...执行业务
dataFromDb = getDataFromDb();
} finally {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//删除锁
stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
//先去redis查询下保证当前的锁是自己的
//获取值对比,对比成功删除=原子性 lua脚本解锁
// String lockValue = stringRedisTemplate.opsForValue().get("lock");
// if (uuid.equals(lockValue)) {
// //删除我自己的锁
// stringRedisTemplate.delete("lock");
// }
return dataFromDb;
} else {
System.out.println("获取分布式锁失败...等待重试...");
//加锁失败...重试机制
//休眠一百毫秒
try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
return getCatalogJsonFromDbWithRedisLock(); //自旋的方式
}
}
5.8分布式锁-Redisson简介&整合
主要步骤:
- 导入依赖
- 配置
Redisson
导入Redisson
依赖
配置Redisson
@Configuration
public class MyRedissonConfig {
/**
* 所有对Redisson的使用都是通过RedissonClient
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
//1、创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.188.180:6379");
//2、根据Config创建出RedissonClient实例
//Redis url should start with redis:// or rediss://
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
测试
@Autowired
RedissonClient redissonClient;
@Test
public void testTedissonClient() {
System.out.println(redissonClient);
}
5.9分布式锁-Redisson-lock锁测试
地址:https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#81-可重入锁reentrant-lock
主要步骤:
- 1.获取一把锁,只要锁的名字一样,就是同一把锁
- 2.加锁,阻塞式等待,默认加的锁都是30s
- 锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s。不用担心业务时间长,锁自动过期被删掉。
- 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认30s以后自动删除。
启动gulimall-product
,2次访问http://localhost:8204/hello
我们看到redis存入了锁的名称,当业务执行完成,锁会被删掉
我们再次启动一个gulimall-product
,访问http://localhost:8204/hello,http://localhost:8214/hello
在8204服务执行业务的时候停止掉服务,看看会不会造成死锁
我们看到8214依然执行成功并释放了锁
完整代码
@ResponseBody
@GetMapping(value = "/hello")
public String hello() {
// 1、获取一把锁,只要锁的名字一样,就是同一把锁
RLock myLock = redisson.getLock("my-lock");
// 2、加锁
myLock.lock(); // 阻塞式等待。默认加的锁都是30s
// 1)、锁的自动续期,如果业务超长,运行期间自动锁上新的30s。不用担心业务时间长,锁自动过期被删掉
// 2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题
try {
System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
try {
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
// 3、解锁 假设解锁代码没有运行,Redisson会不会出现死锁
System.out.println("释放锁..." + Thread.currentThread().getId());
myLock.unlock();
}
return "hello";
}
5.10分布式锁-Redisson-lock看门狗原理-Redisson解决死锁
主要步骤:
- 1.如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是 我们指定的时间
- 2.如果我们未指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】
- 只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒
- 只要占锁成功,续期internalLockLeaseTime 【看门狗时间 / 3 】, 10s
最佳实践:
- myLock.lock(10,TimeUnit.SECONDS); 最好指定锁的超时时间,省掉了整个续期操作
看门狗原理
myLock.lock(10,TimeUnit.SECONDS); //10秒钟自动解锁,自动解锁时间一定要大于业务执行时间
运行项目,锁10s过期,业务执行30s
程序会报错,不能删除当前线程的锁,所以自动解锁时间需要大于业务时间,而且myLock.lock(10,TimeUnit.SECONDS)
不会自动续期
5.11分布式锁-Redisson-读写锁测试
地址:https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#85-读写锁readwritelock
主要步骤:
- 修改的时候加写锁
- 读数据加读锁
- 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁)。读锁是一个共享锁
修改的时候加写锁
读数据加读锁
/write
的时候/read
只能等待,等待/write
完成,/read
才能读取
完整代码
@GetMapping(value = "/write")
@ResponseBody
public String writeValue() {
String s = "";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
RLock rLock = readWriteLock.writeLock();
try {
//1、改数据加写锁,读数据加读锁
rLock.lock();
System.out.println("写锁加锁成功..."+Thread.currentThread().getId());
s = UUID.randomUUID().toString();
ValueOperations<String, String> ops = redisTemplate.opsForValue();
ops.set("writeValue",s);
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
System.out.println("写锁释放成功..."+Thread.currentThread().getId());
}
return s;
}
@GetMapping(value = "/read")
@ResponseBody
public String readValue() {
String s = "";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
//加读锁
RLock rLock = readWriteLock.readLock();
try {
rLock.lock();
System.out.println("读锁加锁成功..."+Thread.currentThread().getId());
ValueOperations<String, String> ops = redisTemplate.opsForValue();
s = ops.get("writeValue");
try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
System.out.println("读锁释放成功..."+Thread.currentThread().getId());
}
return s;
}
5.12分布式锁-Redisson-读写锁补充
主要步骤:
- 写 + 读 :必须等待写锁释放
- 写 + 写 :阻塞方式
- 读 + 写 :有读锁。写也需要等待
- 读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功(有问题,我测试的是读锁也会等待,这里记录一下)
读 + 写 ,先调用/read
,在调用/write
我们发现需要在/read
完成之后,才会执行/write
读 + 读,先调用/write
,在3次调用/read
读锁执行完成,3次写锁也进行了等待
第1次读锁等待了,后2次没有等待,后2次同时加锁同时释放
5.13分布式锁-Redisson-闭锁测试
地址:https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#88-闭锁countdownlatch
请求lockDoor
,然后请求/door/{id}
5次
测试
完整代码
@GetMapping(value = "/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.trySetCount(5);
door.await(); //等待闭锁完成
return "闭锁了...";
}
@GetMapping(value = "/door/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id) {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown(); //计数-1
return id + "已完成";
}
5.14分布式锁-Redisson-信号量测试
地址:https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#86-信号量semaphore
测试
完整代码
@GetMapping(value = "/park")
@ResponseBody
public String park() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
park.acquire(); //获取一个信号、获取一个值,占一个车位
boolean flag = park.tryAcquire();
if (flag) {
//执行业务
} else {
return "error";
}
return "ok=>" + flag;
}
@GetMapping(value = "/go")
@ResponseBody
public String go() {
RSemaphore park = redisson.getSemaphore("park");
park.release(); //释放一个车位
return "ok";
}
5.15分布式锁-缓存一致性解决
我们系统的一致性解决方案:
- 1.缓存的所有数据都有过期时间,数据过期下一次查询主动触发主动更新
- 2.读写数据的时候,加上分布式读写锁。不适合经常写,经常读,适合读多写少
缓存一致性:
- 1.双写模式
- 2.失效模式
Redisson
使用分布式读写锁优化二级分类缓存,避免缓存不一致
// redissonClient
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
//创建读锁
RReadWriteLock readWriteLock = redisson.getReadWriteLock("catalogJson-lock");
RLock rLock = readWriteLock.readLock();
Map<String, List<Catelog2Vo>> dataFromDb = null;
try {
rLock.lock();
//加锁成功...执行业务
dataFromDb = getDataFromDb();
} finally {
rLock.unlock();
}
return dataFromDb;
}
双写模式
更新数据库的时候更新缓存
失效模式
更新数据库的时候删除缓存
缓存数据一致性-解决方案
缓存数据一致性-解决-Canal
5.16SpringCache-简介
地址:https://docs.spring.io/spring-framework/reference/integration/cache.html
简介:
- Spring 从 3.1 开始定义了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术; 并支持使用 JCache(JSR-107)注解简化我们开发
- Cache 接口为缓存的组件规范定义,包含缓存的各种操作集合; Cache 接 口 下 Spring 提 供 了 各 种 xxxCache 的 实 现 ; 如 RedisCache , EhCacheCache , ConcurrentMapCache 等;
- 每次调用需要缓存功能的方法时,Spring 会检查检查指定参数的指定的目标方法是否已 经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓 存结果后返回给用户。下次调用直接从缓存中获取。
- 使用 Spring 缓存抽象时我们需要关注以下两点;
- 1、确定方法需要被缓存以及他们的缓存策略
- 2、从缓存中读取之前缓存存储的数据
点击2次shift
,搜索CacheManager
在CacheManager
上 contrl + H
搜索实现
5.18SpringCache-整合&体验@Cacheable
主要步骤:
-
1.引入依赖
spring-boot-starter-data-redis
spring-boot-starter-cache
-
2.配置
-
spring: cache: type: redis
-
CacheAutoConfiguration
、RedisCacheConfiguration
-
-
3.在应用程序类或任意配置类
GulimallProductApplication
上启用缓存支持@EnableCaching
-
4.测试缓存使用
@Cacheable
: 触发将数据保存到缓存的操作@CacheEvict
: 触发将数据从缓存中删除的操作@CachePut
: 不影响方法执行更新缓存@Caching
: 组合以上多个操作@CacheConfig
: 在类级别共享缓存的相同配置
引入依赖spring-boot-starter-cache
,我的父工程引入了spring-boot-starter-data-redis
,这里不需要引入
然后配置application.yaml
spring:
redis:
host: 192.168.188.180
cache:
type: redis
在GulimallProductApplication
上启用缓存支持@EnableCaching
使用@Cacheable
测试
第一次访问后不会在进入getLevel1Categorys
方法,说明缓存启用成功
5.18SpringCache-@Cacheable细节设置
地址:https://docs.spring.io/spring-framework/reference/integration/cache/annotations.html
主要步驟:
- 每一个需要缓存的数据我们都来指定要放到那个名字的缓存。【缓存的分区(按照业务类型分)】
- 代表当前方法的结果需要缓存,如果缓存中有,方法都不用调用,如果缓存中没有,会调用方法。最后将方法的结果放入缓存
- 默认行为
- 自定义操作:key的生成
- 指定生成缓存的key:key属性指定,接收一个Spel
- 指定缓存的数据的存活时间:配置文档中修改存活时间
- 将数据保存为json格式
配置缓存名称和过期时间
redis里已配置成功
5.19SpringCache-自定义缓存配置
主要步骤:
- 1.原理:
CacheAutoConfiguration
->RedisCacheConfiguration
->
自动配置了RediscacheManager
->初始化所有的缓存->每个缓存决定使用什么配置
->如果rediscacheconfiguration
有就用已有的,没有就用默认配置
->想改缓存的配置,只需要给容器中放-个Rediscacheconfiguration
即可
->就会应用到当前RediscacheManager
管理的所有缓存分区中 - 2.自定义
MyCacheConfig
- 3.
SpringCache
其他配置
自定义MyCacheConfig
@EnableConfigurationProperties(CacheProperties.class)
获取SpringCache
里的配置@EnableCaching
:开启SpringCache
注解,可以从GulimallProductApplication
移动这里MyCacheConfig
主要配置了:- 缓存
key
值 json
序列化- 从配置文件读取配置,配置超时时间、前缀、禁用缓存null值、禁用缓存前缀
- 缓存
启动项目,删除缓存,访问http://localhost:8204/,所有配置都已生效
5.20SpringCache-@CacheEvict
主要步骤:
- 1.同时进行多种缓存操作:
@Caching
- 2.指定删除某个分区下的所有数据
@CacheEvict(value = "category",allEntries = true)
- 3.存储同一类型的数据,都可以指定为同一分区
@CacheEvict
在updateCascCade
方法上添加注解@CacheEvict(value = {"category"},key = "'getLevel1Categorys'")
,并且访问首页后随便更新一个分类,我们发现CACHE_getLevel1Categorys
缓存会被清除
对获取二级菜单接口进行优化
把之前手动添加缓存的代码改为特性
@Cacheable(value = "category",key = "#root.methodName")
访问首页http://gulimall.com/#,会发现自动生成一级和二级菜单接口的缓存
@Caching
在updateCascCade
添加注解
@Caching(evict = {
@CacheEvict(value = {"category"},key = "'getLevel1Categorys'"),
@CacheEvict(value = {"category"},key = "'getCatalogJson'")
})
然后我们更新商品分类,发现一级和二级菜单接口的缓存已经被删掉
@CacheEvict(value = "category",allEntries = true)
在updateCascCade
方法上添加注解@CacheEvict(value = "category",allEntries = true)
,
然后访问首页后随便更新一个分类,我们发现生成CACHE_getCatalogJson
和CACHE_getLevel1Categorys
缓存
然后随便更新一个商品分类,我们发现更新成功CACHE_getCatalogJson
和CACHE_getLevel1Categorys
缓存也被删除了
建议分区名默认就是缓存的前缀
业务相同的缓存在一个分区,方便管理
5.21SpringCache-原理与不足
Spring-Cache的不足之处:
- 1)、读模式
- 缓存穿透:查询一个null数据。解决方案:缓存空数据
- 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;使用sync = true来解决击穿问题
- 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间
- 2)、写模式:(缓存与数据库一致)
- 1)、读写加锁。
- 2)、引入Canal,感知到MySQL的更新去更新Redis
- 3)、读多写多,直接去数据库查询就行
总结:
- 常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了)
- 特殊数据:特殊设计
RedisCache
使用sync = true
加进程锁解决缓存击穿