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 里面的内容

倒排索引:

image-20240704213208313

1.2Docker安装ES

1.3Docker安装Kibana

1.4入门-cat

查看所有节点

GET /_cat/nodes
http://192.168.188.180:9200/_cat/nodes

image-20240705025952234

查看 es 健康状况

GET /_cat/health
http://192.168.188.180:9200/_cat/health

image-20240705030116243

查看主节点

GET /_cat/master
http://192.168.188.180:9200/_cat/master

image-20240705030153386

查看所有索引 show databases

GET /_cat/indices
http://192.168.188.180:9200/_cat/indices

image-20240705030233244

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"
}

image-20240705030741361

POST新增数据

POST customer/external/2
http://192.168.188.180:9200/customer/external/2
{ 
    "name": "Tom"
}

image-20240705031054038

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"
}

image-20240705032236481

再次带上?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"
}

image-20240705032617517

如果想要修改成功,需要携带最新查询的 _seq_no_primary_term,此时修改成功,这就是乐观锁

http://192.168.188.180:9200/customer/external/1?if_seq_no=3&if_primary_term=1
{ 
    "name": "Peng"
}

image-20240705032906528

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"
    }
}

image-20240705033425644

POST修改数据

POST customer/external/1
http://192.168.188.180:9200/customer/external/1/
{ 
  "name": "Marry"
}

image-20240705033658101

PUT修改数据

PUT customer/external/1
http://192.168.188.180:9200/customer/external/1
{
    "name": "Tony"
}

image-20240705034021913

总结:

  • 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
}

image-20240705034754239

POST_update更新同时增加属性

POST customer/external/1/_update
http://192.168.188.180:9200/customer/external/1
{ 
"doc":{ 
      "name": "John Doew",
      "age": 18
    }
}

image-20240705035105557

PUT更新同时增加属性

PUT customer/external/1
http://192.168.188.180:9200/customer/external/1
{ 
  "name": "Tony",
  "age":1
}

image-20240705035518353

1.8入门-删除数据&bulk批量导入测试数据

删除文档

DELETE customer/external/1
http://192.168.188.180:9200/customer/external/1

image-20240705040318724

删除索引

DELETE customer
http://192.168.188.180:9200/customer

image-20240705040426180

bulk批量API

  • 第一行:定义了批量操作的类型和目标索引,以及文档的 _id

  • 第二行:提供了要插入或更新的文档内容。

POST customer/external/_bulk
{"index":{"_id":"1"}}
{"name": "Peng" }
{"index":{"_id":"2"}}
{"name": "Tom" }

image-20240705041112499

语法格式

{ action: { metadata }}
{ request body }
{ action: { metadata }}
{ request body }

复杂实例

bulk API 以此按顺序执行所有的 action(动作)。如果一个单个的动作因任何原因而失败, 它将继续处理它后面剩余的动作。当 bulk API 返回时,它将提供每个动作的状态(与发送 的顺序相同),所以您可以检查是否一个指定的动作是不是失败了。

  • 第一行:_bulk命令
  • 第二行:这表示删除 _indexwebsite_typeblog_id123 的文档。
  • 第三行:表示在 _indexwebsite_typeblog_id123 的位置创建
  • 第四行:创建一个新文档,文档内容是 { "title": "My first blog post" }
  • 第五行:这表示在 _indexwebsite_typeblog 的位置索引一个新文档
  • 第六行:文档内容是 { "title": "My second blog post" }
  • 第七行:更新 _indexwebsite_typeblog_id123 的文档, _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" }}

image-20240705043222665

样本测试数据

https://github.com/elastic/elasticsearch/blob/7.5/docs/src/test/resources/accounts.json

POST bank/account/_bulk
bank测试数据

image-20240705044513643

查看已经创建的索引

image-20240705044621775

1.9进阶-俩种查询方式

ES 支持两种基本方式检索 :

  • 一个是通过使用 REST request URI 发送搜索参数(uri+检索参数)

  • 另一个是通过使用 REST request body 来发送它们(uri+请求体)

检索 bank 下所有信息,包括 type 和 doc

GET bank/_search

image-20240705045030334

请求参数方式检索

  • GET bank/_search:指定了从 bank 索引中执行搜索操作。

  • q=\*:使用 Query String 方式,表示匹配所有文档。

  • sort=account_number:asc:按照 account_number 字段的升序排序。

GET bank/_search?q=*&sort=account_number:asc

image-20240705045107165

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个文档。

  • 选择字段:只返回 balancefirstname 字段。

GET bank/_search
{
    "query": {
        "match_all": {

        }
    },
    "sort": [
        {
            "account_number": {
                "order": "desc"
            }
        }
    ],
    "from": 0,
    "size": 5
    "_source":["balance","firstname"]
}

image-20240707225410374

1.11进阶-match全文检索

基本类型(非字符串),精确匹配

match 返回 account_number=20 的数据

GET bank/_search
{
    "query": {
        "match": {
           "account_number":20
        }
    }
}

image-20240707225708607

字符串,全文检索

最终查询出 address 中包含 mill 单词的所有记录

match 当搜索字符串类型的时候,会进行全文检索,并且每条记录有相关性得分。

GET bank/_search
{
    "query": {
        "match": {
           "address":"mill"
        }
    }
}

image-20240707225813437

字符串,多个单词(分词+全文检索)

最终查询出 address 中包含 mill 或者 road 或者 mill road 的所有记录,并给出相

GET bank/_search
{
    "query": {
        "match": {
           "address": "mill road"
        }
    }
}

image-20240707225947637

1.12进阶-match_phrase短语匹配

将需要匹配的值当成一个整体单词(不分词)进行检索

查出 address 中包含 mill road 的所有记录,并给出相关性得分

GET bank/_search
{
    "query": {
        "match_phrase": {
           "address": "mill road"
        }
    }
}

image-20240707230150870

1.13进阶-multi_match多字段匹配

state 或者 addresss 包含 mill

GET bank/_search
{
    "query": {
        "multi_match": {
          "query": "mill", 
          "fields": ["state","address"]
        }
    }
}

image-20240707230422328

1.14进阶-bool复合查询

bool 用来做复合查询: 复合语句可以合并 任何 其它查询语句,包括复合语句,了解这一点是很重要的。这就意味 着,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑。

must:必须达到 must 列举的所有条件

GET bank/_search
{
    "query": {
        "bool": {
            "must": [
                { "match": { "address": "mill" }},
                { "match": {"gender": "M" }}
            ]
        }
    }
}

image-20240707230901110

should:应该达到 should 列举的条件,如果达到会增加相关文档的评分,并不会改变 查询的结果。如果 query 中只有 should 且只有一种匹配规则,那么 should 的条件就会 被作为默认匹配条件而去改变查询结果

GET bank/_search
{
    "query": {
        "bool": {
            "must": [
                {"match": { "address": "mill" }},
                {"match": { "gender": "M"}}
            ],
            "should": [
              {"match": { "address": "lane" }}
            ]
        }
    }
}

image-20240707232107384

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" }}
            ]
        }
    }
}

image-20240707231345194

1.15进阶-filter过滤

并不是所有的查询都需要产生分数,特别是那些仅用于 “filtering”(过滤)的文档。

为了不 计算分数 Elasticsearch 会自动检查场景并且优化查询的执行。

GET bank/_search
{
    "query": {
        "bool": {
            "must": [
                { "match": {  "address": "mill" }}
            ],
            "filter": [
              {
                 "range": {
                    "balance": {
                      "gte": 10000,
                      "lte": 20000
                 }
              }}
            ]
        }
    }
}

image-20240707232300647

1.16进阶-term查询

和 match 一样。匹配某个属性的值。全文检索字段用 match,其他非 text 字段匹配用 term。

GET bank/_search
{
    "query": {
        "bool": {
            "must": [
                { "term": { "age": { "value": 28 } } },
                { "match": { "address": "990 Mill Road" } }
            ]
        }
    }
}

image-20240707233015682

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
}

image-20240707235436111

按照年龄聚合,并且请求这些年龄段的这些人的平均薪资

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
}

image-20240708000105878

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:用于表示 truefalse 值。
  • 二进制类型(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:用于存储文本字段的令牌数量。

image-20240708001818458

映射

Mapping(映射) Mapping 是用来定义一个文档(document),以及它所包含的属性(field)是如何存储和 索引的。比如,使用 mapping 来定义:

  • 哪些字符串属性应该被看做全文本属性(full text fields)。

  • 哪些属性包含数字,日期或者地理位置。

  • 文档中的所有属性是否都能被索引(_all 配置)。

  • 日期的格式。

  • 自定义映射规则来执行动态添加属性。

  • 查看 mapping 信息:GET bank/_mapping

  • 修改 mapping 信息:[Mapping | Elasticsearch Guide 8.14] | Elastic

    image-20240708002114632

新版本改变

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" }
        }
    }
}

image-20240708002826743

1.19映射-添加新的字段映射

PUT /my-index/_mapping
{
    "properties": {
        "employee-id": {
            "type": "keyword",
            "index": false
        }
    }
}

image-20240708003016480

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

image-20240708012050936

查看数据卷mall_elasticsearch_plugins

docker volume inspect mall_elasticsearch_plugins

image-20240708012120002

可以看到elasticsearch的插件目录挂载到了/var/lib/docker/volumes/mall_elasticsearch_plugins/_data这个目录。

我们需要把IK分词器上传至这个目录。这里解压完后命名elasticsearch-analysis-ik-7.12.1改为了ik

查看elasticsearch版本

image-20240708013025955

ik下载地址:https://github.com/infinilabs/analysis-ik/releases

搜索对应的版本然后下载

image-20240708013127552

这里解压完后命名elasticsearch-analysis-ik-7.12.1改为了ik,然后上传到/var/lib/docker/volumes/mall_elasticsearch_plugins/_data这个目录。

image-20240708013450400

检查自己的文件

image-20240708013602552

重启es

 docker restart es

image-20240708013801403

进入es容器,查看es是否安装ik

 docker exec -it elasticsearch bash
 bin/elasticsearch-plugin list

image-20240708013750268

测试分词器

# 使用默认
POST _analyze
{ 
  "text": "我是中国人"
}

# 使用分词器
POST _analyze
{
  "analyzer": "ik_smart",
  "text":"我是中国人"
}

# ik_max_word
POST _analyze
{ 
  "analyzer": "ik_max_word", "text": "我是中国人"
}

image-20240708013956567

1.22补充-修改Linux网络设置&卡其root密码访问

image-20240708022247472

image-20240708022234565

image-20240708022259413

安装nginx

随便启动一个 nginx 实例,只是为了复制出配置

docker run -p 80:80 --name nginx -d nginx:latest

image-20240708030721355

将容器内的配置文件拷贝到当前目录

docker container cp nginx:/etc/nginx .

image-20240708030815606

修改文件名称

mv nginx conf 

image-20240708031227386

把这个 conf 移动到/mall/nginx 下

mkdir nginx
mv conf nginx

image-20240708031240788

终止原容器

docker stop nginx

image-20240708031338706

执行命令删除原容器

docker rm nginx

image-20240708031404804

创建htmllogs

cd nginx
mkdir html
mkdir logs

image-20240718023428629

创建新的 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 

image-20240708034932997

因为我们修改了端口号,所以要进入容器修改端口号配置

我们把容器下的文件拷贝出来

docker cp nginx:/etc/nginx/conf.d/default.conf /root/mall/nginx/default.conf

image-20240708040353696

配置8001端口号

image-20240708040433070

再把本地修改好的覆盖到容器内

docker cp /root/mall/nginx/default.conf nginx:/etc/nginx/conf.d/default.conf

image-20240708040502378

重启

docker restart nginx

image-20240708040512359

我们在./nginx/html目录下新建index.htmlnginx默认访问index.html

image-20240708040700997

访问:http://192.168.188.180:8001/index.html

image-20240708040748006

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

上传ikelasticsearch映射plugins目录

image-20240708041159265

重启elasticsearch

image-20240708041322309

使用nginx自定义扩展库

保证nginx安装完成

找到/root/mall/elasticsearch/plugins/ik/config/IKAnalyzer.cfg.xml

image-20240708041527611

配置分词地址

http://192.168.188.180:8001/ik/myword.txt

image-20240708042717913

我们在/root/mall/nginx/html创建ik目录,并且创建myword.txt,配置几个自定义分词image-20240708042041381

访问http://192.168.188.180:8001/ik/myword.txt,可以忽略乱码

image-20240708042754506

配置完成后,重启elasticsearch

docker restart elasticsearch

image-20240708042848548

测试自定分词

image-20240708042641515

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>

image-20240709004825726

编写配置

image-20240709004849935

测试

@RunWith(SpringRunner.class)
@SpringBootTest
class GulimallSearchApplicationTest {
    @Autowired
    private RestHighLevelClient client;

    @Test
    public void contextLoads(){
        System.out.println(client);
    }
}

image-20240709005040622

我的这个版本不知道为什么配置@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)还是需要mybatis-plus配置

image-20240709004927597

所以我还是添加了ProductCRUD

image-20240709004720710

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

image-20240709010932220

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;
    }

image-20240709015849827

2.商品业务

2.1商品上架-sku在es中存储模型分析

第一种方式:

  • 方便检索,skuspu放在一起
  • 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属性聚合

image-20240709021052570

  • 搜索小米:手机,电器,粮食
    • 搜索小米有10000个结果,4000个spuId
    • 4000个spuId对应的所有可能属性
    • esClient查询:spuId:[4000个spuId] 因为spuIdlong型数据,4000 * 8byte = 32000byte = 32KB
    • 10000个人检索数据:10000 * 32KB = 320MB,如果是百万并发就是1000000 * 32KB = 32000MB = 32G,光网络阻塞时间就会很长
    • 空间和时间不可能同时获得,用时间换空间,或者空间换时间
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"
                    }
                }
            }
        }
    }
}

image-20240709022219122

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不应该查询出数据image-20240709023729678

修改usersnested类型

# 删除现有的索引,如果你确认不需要其中的数据
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" }}
      ]
    }
  }
}

查询不出来了

image-20240709024548382

2.3商品上架-构造基本数据

主要步骤:

  • 1.查询当前spuId对应的所有sku信息,品牌的名称
  • 2.封装每个sku信息

image-20240712230057722

common-util模块创建SkuEsModel

image-20240712230315924


@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());

image-20240712232843341

根据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>

image-20240712232301500

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>

image-20240713003006522

创建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);

}

image-20240713003733137

泛型结果封装

    /**
     * 1.R设计的时候可以加上泛型
     * 2.直接返回我们想要的结果
     * 3.自己封装解析结果
     * */
public R setData(Object data) {
	put("data",data);
	return this;
}

image-20240713003849140

2.6商品上架-远程上架

主要步骤:

  • gulimall-search中添加上架商品接口,使用BulkRequest批量添加
  • 如果商品上架成功,更新商品状态为已上架

com.peng.search.controller.ElasticSaveController创建productStatusUp商品上架接口,使用BulkRequest批量添加skuEsModels

image-20240713005233784

如果商品上架成功,更新商品状态为已上架

  void updaSpuStatus(Long spuId, int code);
  
  <update id="updaSpuStatus">
        UPDATE pms_spu_info SET publish_status = #{code} ,update_time = NOW() WHERE id = #{spuId}
    </update>

image-20240713010154306

完整代码

// @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 ):skuIdspuIdskuTitleskuPriceskuImgsaleCountskuPriceskuPriceskuPrice
    • 库存信息(远程调用gulimall-ware查询是否有库存):HasStock
    • 热度评分:HotScore(默认为0)
    • 品牌和分类信息:brandIdbrandNamebrandImgcatalogIdcatalogName
    • 检索属性(pms_attr:search_type = 1):Attrs
  • 7.远程调用gulimall-searchsku保存到es,使用BulkRequest批量保存
    • 成功:修改当前spu的状态为已上架
    • 失败:重复调用?接口幂等性:重试机制

image-20240713020443435

调试完源码,成功后使用kibana发现数据全部保存成功

image-20240714025438460

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.fastjsonJSON.toJSONString()会出现$ref循环依赖,导致序列化失败,这里使用了cn.hutool工具包的序列化方法

JSONUtil.toJsonStr()

导入包cn.hutool

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.11</version>
        </dependency>

image-20240714021610234

2.8商品上架-抽取响应结果&上架测试完成

es保存

boolean hasFailures = bulk.hasFailures();返回true代表有错误

image-20240714031711873

抽取响应结果

image-20240714031528026

上架测试完成

image-20240714031604716

3.商城业务

3.1首页-thymeleaf渲染首页

导入模板引擎

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

image-20240714033346258

导入静态资源

  • 创建static.indextemplates目录
  • index目录拷贝到static.index目录
  • index.html拷贝到templates目录

image-20240714034354822

配置thymeleaf

spring:
  thymeleaf:
    cache: false

image-20240714034644964

访问http://localhost:8204/、http://localhost:8204/index/css/GL.css成功代表配置成功

image-20240714034557946

shift点击2下搜索WebMvcAutoConfigurationAdapter

image-20240714035209127

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;
    }

image-20240714213327567

导入devtools

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-devtools</artifactId>
     <optional>true</optional>
 </dependency>

image-20240714213459047

动态遍历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>          

image-20240714213657106

ctrol + shift + f9刷新页面

image-20240714213843734

3.3首页-整合dev-tools渲染二级三级分类数据

主要步骤:

  • 1.删除index/json/catalog.json
  • 2.修改catalogLoader.js请求地址
  • 3.创建getCatalogJson方法,获取二级三级分类

.删除index/json/catalog.json

image-20240714222522033

修改catalogLoader.js请求地址

image-20240714222549814

创建getCatalogJson方法,获取二级三级分类

image-20240714222623436

3.4nginx-搭建域名访问环境一(方向代理配置)

SwitchHosts

下载地址:https://github.com/oldj/SwitchHosts/releases

下载自己对应的版本,我是win64

image-20240714233257305

配置自己的虚拟机的地址和域名

192.168.188.180 gulimall

image-20240714233125614

配置nginx

docker启动时启动nginx

docker update nginx --restart=always

启动nginx

docker start nginx

查看nginx配置文件发现我们的配置都在conf.d目录下

image-20240714233436343

找到conf.d/default.conf

cd conf.d/
ls
vim default.conf

image-20240714234037525

查看本机ip地址

ipconfig

image-20240715000725468

创建gulimall.conf

cd conf.d/
ls
cp default.conf gulimall.conf
ls
vim gulimall.conf

image-20240715003328412

配置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;
    }
}

image-20240715004444775

我之前是使用的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

image-20240715000520516

访问http://gulimall.com/,页面出来配置成功

image-20240715000853400

错误:如果开启了vpn,可能会导致访问失败

可以暂时关掉vpn

image-20240715001201840

3.5nginx-搭建域名访问环境二(负载均衡到网关)

找到nginx.conf

image-20240715012626817

配置上游网关地址,192.168.188.1:8200是你本地gulimall-gateway网关服务的地址

upstream gulimall{
     server 192.168.188.1:8200;
}

image-20240715012702276

配置服务地址,添加请求头

server {
    listen       80;
    listen  [::]:80;
    server_name gulimall.com;

    location / {
        proxy_set_header Host $host;
        proxy_pass http://gulimall;
    }
}

image-20240715012818397

配置网关地址,一定要配置到最后,要不然无法访问接口,gulimall-product服务接口地址也匹配该规则

 - id: gulimall_host_route
   uri: lb://gulimall-product
   predicates:
     - Host=**.gulimall.com,gulimall.com

image-20240715013529099

重启网关服务和nginx

访问http://gulimall.com/、http://gulimall.com/product/attr/list显示界面和结果配置完成

image-20240715013618374

总结

image-20240715013346121

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

下载

image-20240715030258054

解压后,运行jmeter.bat

image-20240715030150511

使用

添加线程组

image-20240715030412625

线程组参数详解:

  • 线程数:虚拟用户数。一个虚拟用户占用一个进程或线程。设置多少虚拟用户数在这里 也就是设置多少个线程数。
  • Ramp-Up Period(in seconds)准备时长:设置的虚拟用户数需要多长时间全部启动。如果 线程数为 10,准备时长为 2,那么需要 2 秒钟启动 10 个线程,也就是每秒钟启动 5 个 线程。
  • 循环次数:每个线程发送请求的次数。如果线程数为 10,循环次数为 100,那么每个线 程发送 100 次请求。总请求数为 10*100=1000 。如果勾选了“永远”,那么所有线程会 一直发送请求,一到选择停止运行脚本。
  • Delay Thread creation until needed:直到需要时延迟线程的创建。
  • 调度器:设置线程组启动的开始时间和结束时间(配置调度器时,需要勾选循环次数为 永远)
  • 持续时间(秒):测试持续时间,会覆盖结束时间
  • 启动延迟(秒):测试延迟启动时间,会覆盖启动时间
  • 启动时间:测试启动时间,启动延迟会覆盖它。当启动时间已过,手动只需测试时当前 时间也会覆盖它。
  • 结束时间:测试结束时间,持续时间会覆盖它。

2000个线程10s创建完,不循环执行

image-20240715032013246

添加HTTP请求

image-20240715030839417

查看结果树

image-20240715031050969

汇总报告

image-20240715031137583

聚合报告

image-20240715031212842

汇总图

image-20240715031308192

测试百度,执行完查看结果

image-20240715032114658

测试gulimall.com

image-20240715032244272

4.3压力测试-JMeter在windows下地址占用bug解决

windows 本身提供的端口访问机制的问题。

Windows 提供给 TCP/IP 链接的端口为 1024-5000,并且要四分钟来循环回收他们。就导致 我们在短时间内跑大量的请求时将端口占满了。

主要步骤:

image-20240717224949493

4.4性能监控-堆内存与垃圾回收

jvm 内存模型

image-20240717230008903

  • 程序计数器 Program Counter Register:
    • 记录的是正在执行的虚拟机字节码指令的地址
    • 此内存区域是唯一一个在JAVA虚拟机规范中没有规定任何OutOfMemoryError的区 域
  • 虚拟机:VM Stack
    • 描述的是 JAVA 方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧, 用于存储局部变量表,操作数栈,动态链接,方法接口等信息
    • 局部变量表存储了编译期可知的各种基本数据类型、对象引用
    • 线程请求的栈深度不够会报 StackOverflowError 异常
    • 栈动态扩展的容量不够会报 OutOfMemoryError 异常
    • 虚拟机栈是线程隔离的,即每个线程都有自己独立的虚拟机栈
  • 本地方法:Native Stack
    • 本地方法栈类似于虚拟机栈,只不过本地方法栈使用的是本地方法
  • 堆:Heap
    • 几乎所有的对象实例都在堆上分配内存

所有的对象实例以及数组都要在堆上分配。堆是垃圾收集器管理的主要区域,也被称为“GC 堆”;也是我们优化最多考虑的地方。 堆可以细分为:

  • 新生代
    • Eden 空间
    • From Survivor 空间
    • To Survivor 空间
  • 老年代
  • 永久代/元空间
    • Java8 以前永久代,受 jvm 管理,java8 以后元空间,直接使用物理内存。因此, 默认情况下,元空间的大小仅受本地内存限制

image-20240717230510703

从 Java8 开始,HotSpot 已经完全将永久代(Permanent Generation)移除,取而代之的是一 个新的区域—元空间(MetaSpace)

image-20240717230552536

image-20240717230637973

4.4性能监控-jvisualvm使用

jconsole

在cmd中输入jconsole

image-20240717231346816

查看堆

image-20240717231440751

jvisualvm

在cmd中输入jvisualvm

image-20240717231700352

监控内存泄露,跟踪垃圾回收,执行时内存、cpu 分析,线程分析...

  • 运行:正在运行的
  • 休眠:sleep
  • 等待:wait
  • 驻留:线程池里面的空闲线程
  • 监视:阻塞的线程,正在等待锁

image-20240717231840996

安装插件Visual GC

image-20240717232116729

解决503

  • 打开网址https://visualvm.github.io/pluginscenters.html
  • cmd 查看自己的 jdk 版本,找到对应的
  • 复制下面查询出来的链接。并重新设置上即可

我是202版本,所以JDK 8 Update 131 - 351131 - 351包含202,可以使用该版本

image-20240717232447164

设置该插件地址

image-20240717232633955

4.5优化-中间件对性能的影响

中间件指标

image-20240718000623338

  • 当前正在运行的线程数不能超过设定的最大值。一般情况下系统性能较好的情况下,线 程数最小值设置 50 和最大值设置 200 比较合适。
  • 当前运行的 JDBC 连接数不能超过设定的最大值。一般情况下系统性能较好的情况下, JDBC 最小值设置 50 和最大值设置 200 比较合适。
  • GC频率不能频繁,特别是 FULL GC 更不能频繁,一般情况下系统性能较好的情况下, JVM 最小堆大小和最大堆大小分别设置 1024M 比较合适

数据库指标

image-20240718000751231

  • SQL 耗时越小越好,一般情况下微秒级别。
  • 命中率越高越好,一般情况下不能低于 95%。
  • 锁等待次数越低越好,等待时间越短越好。

测试线程

image-20240718001656496

nginx测试

image-20240718002011915

Gateway测试

image-20240718002228311

gulimall-product添加接口

    @ResponseBody
    @GetMapping(value = "/hello")
    public String hello() {
        return "hello";
    }

image-20240718002732568

简单服务测试

image-20240718002934882

配置网关服务

        - id: product_route
          uri: lb://gulimall-product
          predicates:
            - Path=/api/product/**,/hello
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}

image-20240718003201399

Gateway + 简单服务测试

image-20240718003501859

压测内容

压测内容 压测线程数 吞吐量/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 测试结果

image-20240718003834923

4.6优化-简单优化吞吐量测试

修改gulimall-product配置

image-20240718012739854

pms_category创建索引

create index idx_parentcid on mall_pms.pms_category(parent_cid);

image-20240718013121132

全量页面(js、静态页面),我这里程序崩了没测成功

image-20240718013441970

压测内容 压测线程数 吞吐量/s 90%响应时间 99%响应时间
首页 localhost:8204 50 900 84 140
首页(缓存、日志、数据库)localhost:8204 50 1983 35 64
首页(缓存、日志、数据库、全量页面)localhost:8204 50 3964 0 1

首页 localhost:8204测试结果

image-20240718012336609

首页(缓存、日志、数据库)localhost:8204 测试结果

image-20240718013315624

首页(缓存、日志、数据库、全量页面)localhost:8204 测试结果

image-20240718013945125

4.7优化-nginx动静分离

上传首页静态资源到/root/mall/nginx/html目录下

/root/mall/nginx/html创建目录static,并且上传首页资源index

image-20240718042502135

配置/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;
    }
}

image-20240718020203077

重启nginx

docker restart nginx

image-20240718020245829

修改gulimall-product的配置文件,关闭thymeleaf缓存

image-20240718020314326

修改src/main/resources/templates/index.html,所有的静态资源路径都加上/static/

image-20240718020447938

访问http://gulimall.com/

image-20240718042624385

4.8优化-模拟线上应用内存崩溃宕机情况

压测首页(缓存、日志、数据库、全量页面、动静分离)gulimall.com

开启thymeleaf缓存

image-20240718043442030

设置gulimall-product

  • -Xmx1024m :最大内存1024m
  • -Xms1024m :初始1024m
  • -Xmn512m :伊甸园区和幸存者区设置为512m
-Xmx1024m -Xms1024m -Xmn512m

image-20240718043631769

测试gulimall-product

image-20240718045111103

设置全量页面

image-20240718045125974

结果

image-20240718045213228

4.9优化-优化三级分类数据获取

主要步骤

  • 优化业务

  • 多次查询优化为查询一次

image-20240718045923276

修改gulimall-product的运行内存为100m

-Xmx100m

image-20240718050159645

压测

http://localhost:8204/index/catalog.json

image-20240718050604900

结果

image-20240718050642000

5.缓存

5.1缓存使用-本地缓存与分布式缓存

哪些数据适合放入缓存?

  • 即时性、数据一致性要求不高的

  • 访问量大且更新频率不高的数据(读多,写少)

image-20240718223545990

data = cache.load(id);//从缓存加载数据
If(data == null){
    data = db.load(id);//从数据库加载数据
    cache.put(id,data);//保存到 cache 中
}
return data;

本地缓存

image-20240718223850215

分布式缓存

分布式系统下一般不使用本地缓存

image-20240718223911758

缓存中间件Redis

image-20240718223959364

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>

image-20240718230036808

配置Redis

spring:
  redis:
    host: 192.168.188.180

image-20240718225955485

使用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);
    }
}

image-20240718230140017

查看RedisAutoConfiguration,自动帮忙注入了RedisTemplate<Object, Object>StringRedisTemplate

image-20240718230223799

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

除了第一次需要查询数据库,后面方位的速度都是几毫秒

image-20240718231915938

5.4缓存使用-压力测试出的内存泄漏及解决

产生堆外内存溢出:OutOfDirectMemoryError

  • 1.springboot2.0以后默认使用lettuce作为操作redis的客户端。它能使用netty进行网络通信。
  • 2.lettuce的bug导致netty堆外内存溢出-Xmx100m;netty如果没有指定对外内存,默认使用设置的-Xmx100m

解决方案:

  • 升级lettuce客户端
  • 切换使用jedis

redisTemplate:lettuce、jedis操作redis的底层客户端。Spring再次封装redisTemplate

压测

image-20240718235033848

结果,我这里没有报错,可是我使用的Springboot2.7.12版本已经修复了

image-20240718235204497

如果出现了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.我们在锁里即要查询数据库,也要更新缓存,要不然第二次请求可能在第一次请求查询完成未更新缓存的时候再次查询缓存

删除缓存,压测接口

image-20240719023456602

在查询数据库的时候加上日志,我们发现加了本地锁synchronized,依然查询了2次

image-20240719023344021

因为首次请求在查询到数据库时就释放了锁,还没来得及更新到缓存,第二次请求进来时因为第一次没有及时更新缓存所以又查询了一次

image-20240719023647152

所以在首次查询完数据库时,也应该在锁里把数据更新到缓存

image-20240719023827965

再次清空缓存,进行压测,控制台输出就查询了一次

image-20240719024143380

代码

@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缓存使用-本地锁在分布式下的问题

本地锁,只能锁住当前进程,所以我们需要分布式锁

image-20240719024520919

我们在添加2个gulimall-product服务 ,把3个gulimall-product服务全部启动

--server.port=8214
--server.port=8224

image-20240719025707808

因为启动了3个gulimall-product服务,我们需要通过请求nginx转发网关服务gulimall-gateway负载均衡到3个gulimall-product服务

image-20240719025855996

清除redis缓存,设置压测参数,启动测试

image-20240719025815825

我们发现3个gulimall-product都查询了一次数据库

image-20240719030232270

5.1分布式锁-分布式锁原理与使用

主要步骤

  • 1.占分布式锁,去redis占坑
  • 2.设置过期时间,必须和加锁是同步的,保证原子性(避免死锁)
  • 3.lua脚本解锁,保证原子性,删除锁只删除自己当前进程的(uuid)

分布式锁基本原理

image-20240719210812322

Redis分布式锁

https://redis.ac.cn/docs/manual/patterns/distributed-locks/

image-20240719211332713

启动3个gulimall-product服务

image-20240719213859126

压测

image-20240719213930769

我们发现只有8224的gulimall-product服务查询了一次数据库

image-20240719214113050

代码

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依赖

image-20240719232835767

配置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;
    }
}

image-20240719232926135

测试

@Autowired
RedissonClient redissonClient;

@Test
public void testTedissonClient() {
        System.out.println(redissonClient);
}

image-20240719233016753

5.9分布式锁-Redisson-lock锁测试

地址:https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#81-可重入锁reentrant-lock

image-20240721002827894

主要步骤:

  • 1.获取一把锁,只要锁的名字一样,就是同一把锁
  • 2.加锁,阻塞式等待,默认加的锁都是30s
    • 锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s。不用担心业务时间长,锁自动过期被删掉。
    • 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认30s以后自动删除。

启动gulimall-product,2次访问http://localhost:8204/hello

image-20240721004415005

我们看到redis存入了锁的名称,当业务执行完成,锁会被删掉

image-20240721004345414

我们再次启动一个gulimall-product,访问http://localhost:8204/hello,http://localhost:8214/hello

image-20240721004759733

在8204服务执行业务的时候停止掉服务,看看会不会造成死锁

image-20240721005047910

我们看到8214依然执行成功并释放了锁

image-20240721005109636

完整代码

@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

image-20240721233319766

程序会报错,不能删除当前线程的锁,所以自动解锁时间需要大于业务时间,而且myLock.lock(10,TimeUnit.SECONDS)不会自动续期

image-20240721233356322

5.11分布式锁-Redisson-读写锁测试

地址:https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#85-读写锁readwritelock

image-20240722002331284

主要步骤:

  • 修改的时候加写锁
  • 读数据加读锁
  • 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁)。读锁是一个共享锁

修改的时候加写锁

image-20240721235631465

读数据加读锁

image-20240721235651601

/write的时候/read只能等待,等待/write完成,/read才能读取

image-20240722000226474

image-20240722000318061

完整代码

@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

image-20240722001106637

我们发现需要在/read完成之后,才会执行/write

image-20240722001200608

读 + 读,先调用/write,在3次调用/read

image-20240722001746484

读锁执行完成,3次写锁也进行了等待

第1次读锁等待了,后2次没有等待,后2次同时加锁同时释放

image-20240722001957013

5.13分布式锁-Redisson-闭锁测试

地址:https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器#88-闭锁countdownlatch

image-20240722002407393

请求lockDoor,然后请求/door/{id}5次

image-20240722003228265

测试

image-20240722003442366

完整代码

@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

image-20240722003953679

测试

image-20240722004932465

完整代码


    @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;
    }

image-20240722020025536

双写模式

更新数据库的时候更新缓存

image-20240722020432371

失效模式

更新数据库的时候删除缓存

image-20240722020530728

缓存数据一致性-解决方案

image-20240722020644741

缓存数据一致性-解决-Canal

image-20240722020740129

5.16SpringCache-简介

地址:https://docs.spring.io/spring-framework/reference/integration/cache.html

image-20240722022603948

简介:

  • 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、从缓存中读取之前缓存存储的数据

image-20240722022720178

image-20240722022736053

点击2次shift,搜索CacheManager

image-20240722022914610

CacheManagercontrl + H搜索实现

image-20240722023007814

5.18SpringCache-整合&体验@Cacheable

主要步骤:

  • 1.引入依赖

    • spring-boot-starter-data-redis
    • spring-boot-starter-cache
  • 2.配置

    • spring:
        cache:
          type: redis
      
    • CacheAutoConfigurationRedisCacheConfiguration

  • 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

image-20240722025711896

GulimallProductApplication上启用缓存支持@EnableCaching

image-20240722030051908

使用@Cacheable测试

image-20240722030137175

第一次访问后不会在进入getLevel1Categorys方法,说明缓存启用成功

image-20240722030415826

5.18SpringCache-@Cacheable细节设置

地址:https://docs.spring.io/spring-framework/reference/integration/cache/annotations.html

image-20240722032455353

主要步驟:

  • 每一个需要缓存的数据我们都来指定要放到那个名字的缓存。【缓存的分区(按照业务类型分)】
  • 代表当前方法的结果需要缓存,如果缓存中有,方法都不用调用,如果缓存中没有,会调用方法。最后将方法的结果放入缓存
  • 默认行为
    • 如果缓存中有,方法不再调用
    • key是默认生成的:缓存的名字::SimpleKey::
    • 缓存的value值,默认使用jdk序列化机制,将序列化的数据存到redis中
    • 默认时间是 -1:永不过期

image-20240722031840448

  • 自定义操作:key的生成
    • 指定生成缓存的key:key属性指定,接收一个Spel
    • 指定缓存的数据的存活时间:配置文档中修改存活时间
    • 将数据保存为json格式

配置缓存名称和过期时间

image-20240722032543840

redis里已配置成功

image-20240722032642139

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值、禁用缓存前缀

image-20240722034622086

启动项目,删除缓存,访问http://localhost:8204/,所有配置都已生效

image-20240722035439704

5.20SpringCache-@CacheEvict

主要步骤:

  • 1.同时进行多种缓存操作:@Caching
  • 2.指定删除某个分区下的所有数据 @CacheEvict(value = "category",allEntries = true)
  • 3.存储同一类型的数据,都可以指定为同一分区

@CacheEvict

updateCascCade方法上添加注解@CacheEvict(value = {"category"},key = "'getLevel1Categorys'"),并且访问首页后随便更新一个分类,我们发现CACHE_getLevel1Categorys缓存会被清除

image-20240722050503989

对获取二级菜单接口进行优化

把之前手动添加缓存的代码改为特性

 @Cacheable(value = "category",key = "#root.methodName")

image-20240722050928490

访问首页http://gulimall.com/#,会发现自动生成一级和二级菜单接口的缓存

image-20240722052017283

@Caching

updateCascCade添加注解

@Caching(evict = {
            @CacheEvict(value = {"category"},key = "'getLevel1Categorys'"),
            @CacheEvict(value = {"category"},key = "'getCatalogJson'")
    })

image-20240722052306884

然后我们更新商品分类,发现一级和二级菜单接口的缓存已经被删掉

image-20240722052243326

@CacheEvict(value = "category",allEntries = true)

updateCascCade方法上添加注解@CacheEvict(value = "category",allEntries = true)

image-20240722052727640

然后访问首页后随便更新一个分类,我们发现生成CACHE_getCatalogJsonCACHE_getLevel1Categorys缓存

image-20240722053659936

然后随便更新一个商品分类,我们发现更新成功CACHE_getCatalogJsonCACHE_getLevel1Categorys缓存也被删除了

image-20240722053812788

建议分区名默认就是缓存的前缀

image-20240722054028399

业务相同的缓存在一个分区,方便管理

image-20240722054208554

5.21SpringCache-原理与不足

Spring-Cache的不足之处:

  • 1)、读模式
    • 缓存穿透:查询一个null数据。解决方案:缓存空数据
    • 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;使用sync = true来解决击穿问题
    • 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间
  • 2)、写模式:(缓存与数据库一致)
    • 1)、读写加锁。
    • 2)、引入Canal,感知到MySQL的更新去更新Redis
    • 3)、读多写多,直接去数据库查询就行

总结:

  • 常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了)
  • 特殊数据:特殊设计

RedisCache使用sync = true加进程锁解决缓存击穿

image-20240722212819604

posted @ 2024-10-14 00:58  peng_boke  阅读(32)  评论(0编辑  收藏  举报