谷粒商城高级篇上(未完待续)

谷粒商城高级篇(上)保姆级整理


之前整理了基础篇,Typora提示将近20000词,谷粒商城基础篇保姆级整理

在学高级篇的时候,不知不觉又整理了两万多词,做了一阶段,先发出来,剩余部分整理好了再发。自己也在学习的过程中,能力有限,如果有什么问题欢迎找我讨论。

文章目录

ElasticSearch
image-20200927080424206

image-20200927080641450

image-20200927080755617

image-20200927081054389

索引类比database,类型类比表table,文档(json格式)类比表记录,属性类比列

image-20200927081424108

举例,mysql中保存一个数据可能是正向索引,每条数据都有id存在这,在电影表中检索红海行动,用like,mysql匹配所有的记录,看每一条记录中是否是红海行动,这样非常慢。

es首先把红海行动拆成两个单词,红海,行动,es中保存1号文档,额外又维护一张倒排索引表,存了红海,和行动的单词,在一号记录里面有,所以如图所示

查询红海特工行动,会查到12345,五条记录,3号和5号都命中两个,但是3号3个单词命中两个,5号四个单词命中两个,根据相关性得分,从高到低排列,检索出数据还可以对数据进行复杂分析

image-20200927081751653

把mysql数据es中,然后全局检索

Docker安装es
docker pull elasticsearch:7.4.2
docker pull kibana:7.4.2

//将es中配置文件挂载到外面的目录,通过修改虚拟机外面的文件夹es配置,进而修改docker中es的配置
mkdir -p /mydata/elasticsearch/config

mkdir -p /mydata/elasticsearch/data
//写了一个配置  http.host:0.0.0.0 代表es可以被远程的任何机器访问,注意这里host:后需要有空格 
echo "http.host: 0.0.0.0">> /mydata/elasticsearch/config/elasticsearch.yml

运行elasticsearch命令,
//为容器起一个名字为elasticsearch,-p暴露两个端口 9200 9300, 9200是发送http请求——restapi的端口,9300是es在分布式集群状态下,结点之间的通信端口, \代表换行下一行, 
//-e  single-node 是以单节点方式运行,ES_JAVA_OPTS不指定的话,es一启动,会将内存全部占用,整个虚拟机就卡死了,
//-v 进行挂载,目录中配置,数据等一一关联 -d 后台启动es使用指定的镜像 z
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx128m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2

安装完 elasticsearch 后我们来启动一下,会发现使用docker ps命令查看启动的容器时没有找到我们的 es,这是因为目前 es 的配置文件的权限导致的,因此我们还需要修改一下 es 的配置文件的权限:

chmod -R 777 /mydata/elasticsearch/

image-20200927215212803

ubuntu中vi下删除键和上下左右键的异常解决
  1. 下载完整vim即可解决:

sudo apt-get remove vim-common

sudo apt-get install vim-gtk

http://192.168.218.128:9200/

{
  "name" : "90dbb5181665",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "Vq74sMKCTWGVuPtn36m8TQ",
  "version" : {
    "number" : "7.4.2",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "2f90bbf7b93631e52bafb59b3b049cb44ec25e96",
    "build_date" : "2019-10-28T20:40:44.881551Z",
    "build_snapshot" : false,
    "lucene_version" : "8.2.0",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

_cat 结点相关信息

http://192.168.218.128:9200/_cat/nodes

带*表示主节点

image-20200927215954582

Docker安装Kibana
//访问5601端口,访问到可视化界面kibana,kibana再先发送请求到es9200
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.218.128:9200 -p 5601:5601 -d kibana:7.4.2
ElasticSearch 入门

_cat

检索文档

image-20200927221807777

image-20200927223024402

http://192.168.218.128:9200/customer/external/1

{
    "name":"John Doe"
}

image-20200927223352925

{	
    //带_的都称为元数据,反应基本信息
    "_index": "customer",
    "_type": "external",
    "_id": "1",
    "_version": 1,
    "result": "created",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 0,
    "_primary_term": 1
}
//再发送一遍请求
{
    "_index": "customer",
    "_type": "external",
    "_id": "1",
    "_version": 2,//变化
    "result": "updated", //变化
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 1,
    "_primary_term": 1
}
查询文档

image-20200927222421079

es修改数据是无序的,给es发起请求改数据,为了控制并发修改,

A,B都要修改es中1记录,只要有一个人把这个记录改了,记录的版本号就+1(老版本),新版本用_sql_no,如果A还想改1,就需要加一个判断,

http://192.168.8.201:9200/customer/external/1
{
    "_index": "customer",
    "_type": "external",
    "_id": "1",
    "_version": 4,
    "_seq_no": 7,//乐观锁操作  只要有改动就会往上加
    "_primary_term": 1, //乐观锁操作   只要有改动就会往上加
    "found": true,
    "_source": {
        "name": "John Doe"
    }
}
http://192.168.218.128:9200/customer/external/1?if_seq_no=7&if_primary_term=1

image-20200927230632709

更新文档
//post更新带update会对比原数据,如果这次数据和原来一模一样,版本号就不会往上加,序列号也不变
http://192.168.218.128:9200/customer/external/2/_update
{
    "doc":{
        "name":"John"
    }    
}

第一次发起请求

{
    "_index": "customer",
    "_type": "external",
    "_id": "2",
    "_version": 5,
    "result": "updated",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 12,
    "_primary_term": 3
}

第二次发起请求

{
    "_index": "customer",
    "_type": "external",
    "_id": "2",
    "_version": 5,
    "result": "noop",//no operation
    "_shards": {
        "total": 0,
        "successful": 0,
        "failed": 0
    },
    "_seq_no": 12,
    "_primary_term": 3
}
//不带_update就不会检查原数据,仅仅是put(put没有带update语法)也是一样效果,永远是更新操作,不会对比原来数据
http://192.168.218.128:9200/customer/external/2
{
    "_index": "customer",
    "_type": "external",
    "_id": "2",
    "_version": 10,
    "result": "updated",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 17,
    "_primary_term": 3
}

image-20201001075518746

{
    "doc":{
        "name":"John",
        "age":20
    }    
}
删除文档&索引

image-20201001075835881

http://192.168.218.128:9200/customer/external/1

es中没有提供类型直接删除的操作

bulk批量API

image-20201001080926187

//两个为一行操作,每一条都是独立的,index是一个保存操作,上一条的失败不会影响下一条的记录的成功失败,不像mysql中的事务,一条失败全部回滚
POST /customer/external/_bulk
{"index":{"_id":"1"}}
{"name":"tang"}
{"index":{"_id":"2"}}
{"name":"yao"}

image-20201001081815714

测试数据地址

POST bank/account/_bulk

image-20201001083741362

进阶操作

image-20201001090733198

文档地址https://www.elastic.co/guide/en/elasticsearch/reference/7.5/getting-started-search.html

image-20201001090758621

//?检索条件,q=* 查询所有,sort=account_number:asc排序规则按照该字段升序排列
GET bank/_search?q=*&sort=account_number:asc

image-20201001091253146

image-20201001091851223
image-20201001092021588

GET /bank/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "balance":"desc"
    },
    {
      "account_number": "asc"
    }
  ]
}

查询领域对象

image-20201001092040670

image-20201001092424976

image-20201001092751956

image-20201001093010880

image-20201001093140594

##match 全文检索按照评分进行排序,会对检索条件进行分词匹配
GET /bank/_search
{
  "query": {
    "match": {
      "account_number": "20"
    }
  }
}

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

image-20201001094049712

image-20201001094602864

image-20201001095242658

GET /bank/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "age": "40"
          }
        }
      ],
      "must_not": [
        {
          "match": {
            "state": "ID"
          }
        }
      ],
      "should": [
        {
          "match": {
            "lastname": "Ross"
          }
        }
      ]
    }
  }
}
filter

filter和match或should区别是, 不会计算相关性得分,只起过滤作用

GET /bank/_search
{
  "query": {
    "bool": {
      "must": { "match_all": {} },
      "filter": {
        "range": {
          "balance": {
            "gte": 20000,
            "lte": 30000
          }
        }
      }
    }
  }
}
term

image-20201001110638403

两者区别,前者查询的内容可以包含789 Madison,后者是精确查询,

GET /_search
{
  "query": {
    "match_phrase": {
      "address": "789 Madison"
    }
  }
}

GET /_search
{
  "query": {
    "match": {
      "address.keyword": "789 Madison"
    }
  }
}

image-20201001111549710

规定:非text字段,都用term,文本字段就用match

aggregations(执行聚合)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mfTLUAP3-1603542203904)(https://tangyao.oss-cn-beijing.aliyuncs.com/image-20201001111708818.png)]

image-20201014205425513

GET bank/_search
{
  "query": {
    "match": {
      "address": "mill"
    }
  },
  "aggs": {
    "ageaggs": {
      "terms": {
        "field": "age",
        "size": 10 //假设年龄有100种可能,只取出前10种可能
      }
    }
  }
}

结果为

image-20201014210029508

GET bank/_search
{
  "query": {
    "match": {
      "address": "mill"
    }
  },
  "aggs": {
    "ageAgg": {
      "terms": {
        "field": "age",
        "size": 10
      }
    },
    "aggAvg": {
      "avg": {
        "field": "age"
      }
    },
    "balanceAvg": {
      "avg": {
        "field": "balance"
      }
    }
  },
  "size": 0
}

分页size指定为0,意思为不要任何分页记录,这里只看聚合结果

image-20201014213414472

image-20201014213655084

# 按照年龄聚合,并且请求这些年龄段的这些人的平均薪资
GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "ageAgg": {
      "terms": {
        "field": "age",
        "size": 100
      },
      "aggs": {
        "balanceAvg": {
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  }
}

image-20201014214701158

# 查出所有的年龄分布,并且这些年龄段中性别为M的平均薪资和F的平均薪资以及这个年龄段的总体平均薪资

GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "ageAgg": {
      "terms": {
        "field": "age",
        "size": 100
      },
      "aggs": {
        "gender": {
          "terms": {
            "field": "gender.keyword"
          },
          "aggs": {
            "genderBalance": {
              "avg": {
                "field": "balance"
              }
            }
          }
        },
        "ageBlance":{
         "avg": {
           "field": "balance"
         }
        }
        
      }
      
    }
  },
  "size": 0
}

image-20201014221531540

Mapping

image-20201014222511497

image-20201015081146715

image-20201015081330492

image-20201015081542550

image-20201015082339145

每个属性的映射类型,type为text默认就会就全文检索,检索起来就会分词,想要精确检索address的值,就要用address.keyword

image-20201015081933709

image-20201015082609399

PUT /my_index
{
  "mappings": {
    "properties": {
      "age":    { "type": "integer" },  
      "email":  { "type": "keyword"  }, 
      "name":   { "type": "text"  }  
    }
  }
}

GET my_index/_mapping

添加新的字段映射

image-20201015134003360

PUT /my_index/_mapping
{
  "properties": {
    "employee-id": {
      "type": "keyword",
      "index": false  // 设置不可以被索引
    }
  }
}
更新映射与数据迁移

image-20201015134840749

https://www.elastic.co/guide/en/elasticsearch/reference/7.4/docs-reindex.html

image-20201015141846705

image-20201015141915973

根据bank的属性,复制过来进行修改而生成新的索引和映射规则

PUT /newbank
{
  "mappings": {
     "properties" : {
        "account_number" : {
          "type" : "long"
        },
        "address" : {
          "type" : "text"
        },
        "age" : {
          "type" : "integer"
        },
        "balance" : {
          "type" : "long"
        },
        "city" : {
          "type" : "keyword"
        },
        "email" : {
          "type" : "keyword"
        },
        "employer" : {
          "type" : "keyword"
          
        },
        "firstname" : {
          "type" : "text"
        },
        "gender" : {
          "type" : "keyword"
        },
        "lastname" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "state" : {
          "type" : "keyword"
          
        }
     }
  }
}

想把老银行的数据迁移到新银行,因为老索引,分类型

image-20201015150549965

POST _reindex
{
  "source": {
    "index": "bank",
    "type": "account"
  },
  "dest": {
    "index": "newbank"
  }
}

GET newbank/_search

所有的type变成了默认的_doc

image-20201015171835228

分词

将完成的大段话分词,利用单词的相关性匹配,最终完成全文检索功能。默认使用标准分词器

https://www.elastic.co/guide/en/elasticsearch/reference/7.4/analysis-standard-analyzer.html

image-20201015172021131

POST _analyze
{
  "analyzer": "standard",
  "text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}

但是默认是英文分词器,如果text为中文的话就会分割成一个个汉字,需要自己的分词器。

image-20201015200528827

https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.4.2

# -it 交互模式 /bin/bash 进入控制台
docker exec -it 658 /bin/bash

image-20201015193241065

复制链接地址https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip

image-20201015193504268

​ Vagrant创建的虚拟机默认是免密连接的,所以需要修改一下配置。

image-20201015193957221

image-20201015194043514

我用的是Ubuntu,先安装ssh

sudo apt-get install openssh-server

image-20201015194800409

安装xshell和xftp并把下载好文件上传

image-20201015200626568

解压到ik文件夹下

创建ik文件夹时,记得要修改文件夹权限否则不能讲zip文件通过 xftp拷贝过来

进入bin目录里面,都是可执行命令,运行这个plugin命令

image-20201015210745872

image-20201015210935692

测试分词器

image-20201015211059073

先退出并重启elasticsearch

修改网络

image-20201015211655527

image-20201015211710352

新增这三项

DNS帮忙解析域名都在哪里,再设置一个备用的DNS

image-20201015211823274

重启网卡

image-20201015212121369

Ubuntu的没有yum,下面是简单介绍,所以每安装yum

image-20201015212404731

yum安装wget

image-20201015212523936

image-20201015212541217

yum再安装unzip等等,这里就略过了。我的Ubuntu里面都有。

自定义扩展词库

指定一个远程 词库,让ik分词器自己向远程发送请求,要到最新的单词,最新的单词就会作为新的单词进行分解

1)自己写一个项目,处理这个请求,返回我们新的单词,让ik分词器给我们项目发送请求

2)安装nginx,将最新词库放到nginx里面,让ik分词器给nginx发送请求,由它(也是个web服务器)返回最新的词库,就能把新词库和原来的词库合并起来

image-20201015213108991

ctrl+l清屏
free -m

如果可用比较小,就关闭虚拟机设置大一点

image-20201015213845180

因为之前设置es的jvm堆内存比较小,最大只有128m

想要设置成512m,最快的方式停掉原来的创建一个新的。

这样做数据并不会丢失,因为之前创建的时候,将es中数据映射到外面的文件data下,即使删掉了容器,可是外面的文件夹还在,再创建新的容器和外面文件夹关联起来,数据也不会丢失。

image-20201015214307093

docker stop elasticsearch
docker rm elasticsearch

运行elasticsearch命令,
//为容器起一个名字为elasticsearch,-p暴露两个端口 9200 9300, 9200是发送http请求——restapi的端口,9300是es在分布式集群状态下,结点之间的通信端口, \代表换行下一行, 
//-e  single-node 是以单节点方式运行,ES_JAVA_OPTS不指定的话,es一启动,会将内存全部占用,整个虚拟机就卡死了,
//-v 进行挂载,目录中配置,数据等一一关联 -d 后台启动es使用指定的镜像 z
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2
安装nginx

image-20201015213627967docker

// 在/mydata 目录下面创建nginx
mkdir nginx

// 本地没有找到镜像,自动去远程下载
docker run -p 80:80 --name nginx -d nginx:1.10

docker container cp nginx:/etc/nginx . 
    
docker stop nginx

docker rm nginx
// 切换到mydata后 把nginx改名字为conf
mv nginx conf

mkdir nginx
// 把整个文件夹移动到 nginx下,以后nginx就在conf下面了
mv conf nginx/
注意,\前面是有空格的
docker run -p 80:80 --name nginx \
-v /mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-v /mydata/nginx/conf:/etc/nginx \
-d nginx:1.10

访问虚拟机80端口,不带端口号默认是80端口

image-20201015235511240

// 在nginx文件夹下
cd /html
// 只要这个文件夹下有index.html,就会默认展示
vi index.html
   

image-20201016000013689

image-20201016000030140

//在html文件夹下,创建es,把es中ik分词器用到的资源放在这里
mkdir es
    
cd es
    
vi fenci.txt // 添加尚硅谷和乔碧萝两个词

    

nginx默认找资源都是在html下面找的,想要访问es下面的资源直接带上路径就可以了

乱码问题先不管,至此nginx就装好了

image-20201016001230392

再配置ik分词器的远程词库地址,来到自定义词库,只需要修改ik分词器的配置

image-20201016001427644

cd /mydata/elasticsearch/plugins/ik/config/

image-20201016001730848

image-20201016001946935

docker restart elasticsearch

分词成功

image-20201016002241387

docker update elasticsearch --restart=always
Elasticsearch-Rest-Client

image-20201016002732895

image-20201016141148947

有这样一个检索场景,当选中一些检索条件的时候,需要给es发送请求,来检索真正的商品,请求过来就需要给页面检索数据,这段请求应该由java程序接受,es进行处理,将处理的结果再返回给前端页面,java操作es有两种方式,

通过操作9300端口,它是一个tcp端口,es集群结点之间通信也都使用9300端口,如果使用9300端口操作es,就要与es建立一个长连接,支持这些操作在springdata项目里面,有transport-api对应es操作,包括官方elasticsearch.jar这些依赖也能支持这些操作,基于上面图片原因不使用9300端口,依然是通过9200操作es更简单,就是给es发送请求。

image-20201016140302879

Elasticsearch Clients
https://www.elastic.co/guide/en/elasticsearch/client/index.html

虽然Elasticsearch Clients 支持在页面通过js给es发送请求在页面展示,就不需要过java这一层了,虽然说这种操作是可以的,但是es属于我们后台集群服务器,这个端口我们一般不对外暴露,如果对外暴露,会被别人恶意破坏(出于安全原因),所以所有请求应该发给我们java项目,由java操作后台集群,第二个原因,js客户端对于es的支持度本身有点低,说白了,想用js操作,完全可以不用es官方提供的js相关的api,直接发送ajax请求就行了,想要进行什么查询,自己写好QueryDSL发送出去,基于这两点,就不需要直接用js操作es。还是把所有请求发给我们的java程序,由后台业务直接操作后台存储集群。

java api是通过9300端口操作es的,别混淆。应该用java REST Client
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.4/index.html

image-20201016141555519

java api的官方提示

image-20201016142458148

为检索单独做一个项目

image-20201016143542660

image-20201016143657958

按照文档操作https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.4/index.html

引入依赖

<dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-high-level-client</artifactId>
            <version>7.4.2</version>
 </dependency>

发现不配套

image-20201016144735964

原因是springboot项目依赖里面搜索es,发现springboot对es的版本也做了管理,当前Springboot默认整合springData,来操作es,他的版本是6.8.5

image-20201016144903946

image-20201016144929989

把版本改掉

image-20201016145202331

       <elasticsearch.version>7.4.2</elasticsearch.version>

maven依赖已经改掉了

image-20201016150046612

如何操作es,就需要配置,如果导入Springdata来操作es,就非常简单,只需要在配置文件里面指定好es地址就可以了。

创建GulimallElasticSearchConfig

package com.atguigu.gulimall.search.config;

import org.springframework.context.annotation.Configuration;

/**
 * @author tangyao
 * @version 1.0.0
 * @Description TODO
 * @createTime 2020年10月16日 15:18:00
 */
@Configuration
public class GulimallElasticSearchConfig {
}

导入common

  <dependency>
            <groupId>com.atguigu.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
spring.application.name=gulimall-search
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

创建实例

https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.4/java-rest-high-getting-started-initialization.html

image-20201016152918778

/**
 * 1、导入依赖
 * 2、编写配置,给容器中注入一个RestHighlevelClient
 * 3、参照API https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.4/java-rest-high-getting-started-initialization.html
 */
@Configuration
public class GulimallElasticSearchConfig {

    @Bean
    public RestHighLevelClient esRestClient() {

        RestClientBuilder builder = null;
        //final String hostname, final int port, final String scheme
        builder = RestClient.builder(new HttpHost("192.168.218.128", 9200, "http"));
        RestHighLevelClient client = new RestHighLevelClient(builder);

//        RestHighLevelClient client = new RestHighLevelClient(
//                //如果es有多个,指定es的地址和端口号以及协议名
//                RestClient.builder(
//                        new HttpHost("192.168.218.128", 9200, "http")));
        return client;
    }
}

进行单元测试,

image-20201016154614768

image-20201016154629103

由于我用的springboot版本是2.2.7.RELEASE,juit的导入的是

import org.junit.jupiter.api.Test;

所以没出现任何问题,但是在这里记录一下

老师项目存在的问题

老师用的是2.1.x,首先都加public,

image-20201016154918487

但是出现的是null,说明autowired注解都没解析成功,如果解析成功,要么解析不到,也不能是null.

image-20201016154956202

增加@RunWith()指定Spring驱动来跑单元测试,这是以前兼容的单元测试,

image-20201016155314116

image-20201016155242947

又出现以下错误,没有指定数据源,

image-20201016155432731

image-20201016155455314

在common里面默认是有数据源的,只要有数据源就要配合数据源有关的配置,比如mysql的驱动,包括mybatis-plus的依赖,但是又不操作数据库,所以去掉

image-20201016155718683

image-20201016155634399

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallSearchApplication {

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

}

老师的项目测试通过了

image-20201016160027424

ElasticSearch-整合-测试保存

首先了解第一个,请求设置项,以后要发所有请求,比如es添加了安全访问规则,

https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.4/java-rest-high-getting-started-request-options.html

image-20201016163505971

image-20201016164120146

   public static final RequestOptions COMMON_OPTIONS;
    static {
        RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
//        builder.addHeader("Authorization", "Bearer " + TOKEN);
//        builder.setHttpAsyncResponseConsumerFactory(
//                new HttpAsyncResponseConsumerFactory
//                        .HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024));
        COMMON_OPTIONS = builder.build();
    }

测试

  /**
     * 测试存储数据到es
     */
    @Test
    void indexData() {
        IndexRequest request = new IndexRequest("users");
        request.id("1");
//        request.source("username","zhangsan","age",18,"gender","男");
        User user = new User();
        String jsonString = JSON.toJSONString(user);
        request.source(jsonString);
    }
        
    @Data
    class User{
        private String userName;
        private String gender;
        private Integer age;
    }

可以执行同步与异步两种方式https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.4/java-rest-high-document-index.html

异步多了一个listener,调用监听器的成功方法或者失败方法,类似于写js的ajax请求的success,error回调函数一样,

image-20201016174645877

image-20201016174733430

 /**
     * 测试存储数据到es
     */
    @Test
    void indexData() throws IOException {
        IndexRequest request = new IndexRequest("users");
        request.id("1");
//        request.source("username","zhangsan","age",18,"gender","男");
        User user = new User();
        String jsonString = JSON.toJSONString(user);
        request.source(jsonString);
        //网络操作都会有异常,
        //执行操作,
        IndexResponse index = client.index(request, GulimallElasticSearchConfig.COMMON_OPTIONS);

        //提取有用的响应数据
        System.out.println(index);
    }

先通过kibana查看要查询的索引,

image-20201016175315144

执行测试用例,这里的even指的是扁平的json数据。本来是有的,说明api调用的不对,没有指定内容类型,添加一个参数

image-20201016175409162

request.source(jsonString,XContentType.JSON);

执行成功,再添加数据

image-20201016175656795

user.setUserName("zhangsan");
user.setAge(22);
user.setGender("男");

再次测试。

更新新增二合一。

image-20201016180339262

测试复杂检索

https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.4/java-rest-high-search.html

​ 一切都以文档为主。

  @Test
    void searchData() throws IOException {
        //1.创建一个检索请求
        SearchRequest searchRequest = new SearchRequest();
        //制定索引
        searchRequest.indices("bank");
        //制定DSL,检索条件
        // SearchSourceBuilder searchSourceBuilder 封装检索条件
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //1.1)构造检索条件
//        searchSourceBuilder.query();
//        searchSourceBuilder.from();
//        searchSourceBuilder.size();
//        searchSourceBuilder.aggregation();

        searchSourceBuilder.query(QueryBuilders.matchQuery("address", "mill"));
        //1.2)按照年龄的值分布进行聚合
        TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
        searchSourceBuilder.aggregation(ageAgg);

        //1.3)计算平均薪资
        AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");
        searchSourceBuilder.aggregation(balanceAvg);

        System.out.println("检索条件" + searchSourceBuilder.toString());


        SearchRequest source = searchRequest.source(searchSourceBuilder);
        //2.执行检索
        SearchResponse searchResponse = client.search(source, GulimallElasticSearchConfig.COMMON_OPTIONS);


        //3、分析结果
        System.out.println(searchResponse.toString());

//        Map map = JSON.parseObject(searchResponse.toString(), Map.class);
        //3.1)、获取所有查到的数据
        SearchHits hits = searchResponse.getHits();
        SearchHit[] searchHits = hits.getHits();
        for (SearchHit hit : searchHits) {
            //在此之前根据json生成java对象Account
            String sourceAsString = hit.getSourceAsString();
            Account account = JSON.parseObject(sourceAsString, Account.class);
            System.out.println("account = " + account);
        }

        //3.2)获取检索到的分析信息
        Aggregations aggregations = searchResponse.getAggregations();
//        for (Aggregation aggregation : aggregations.asList()) {
//            System.out.println("name = " + aggregation.getName());
//        }
        System.out.println("aggregations = " + aggregations.toString());
        Terms ageAgg1 = aggregations.get("ageAgg");
        for (Terms.Bucket bucket : ageAgg1.getBuckets()) {
            String keyAsString = bucket.getKeyAsString();
            System.out.println("年龄 = " + keyAsString + "===>" + bucket.getDocCount());
        }
        Avg balanceAvg1 = aggregations.get("balanceAvg");
        System.out.println("平均薪资" + balanceAvg1.getValue());
        System.out.println(aggregations);
    }
商城业务-商品上架-sku在es中存储模型分析

es在项目中的使用:

1、作为全文检索引擎,承担所有项目里面的全文检索功能,京东手机首页,可以按照名字全文检索,也可以按照手机不同规格属性,进行全文检索,

2、承担日志的分析检索功能,可能需要对日志进行快速定位,日志也有检索需求,就可以将日志存储到es里面,有一个技术栈ELK logStash负责收集日志存到es里面

image-20201016212817444

腾讯云截图,这部分会在运维部分提及。

image-20201016213326351

检索需要给es存储数据,不用mysql的原因,mysql全文检索功能没有es强大,这么复杂的检索分析数据,mysql性能远不及es,es数据是存在内存中的。商品都存在内存中够吗?es是天然支持分布式的,一个es不够可以多装几个es分布在不同服务器里面。然后就会将数据分片存储。容量不够,数量来凑。

所以我们要做的第一件事,先要将商品数据es里面存一份,方便做检索功能。

商品从数据库里面保存到es里面这一过程,称为商品的上架。

点击上架,首先此商品状态改为上架状态,其次商品的数据要在es中保存,前端的商城项目要检索就在es中检索商品数据。要给es保存,首先分析需要保存哪些数据,虽然是在sku上架,但是要将什么信息保存进来呢?首先达成一个共识,es所有数据都是存在内存中的。虽然原生支持分布式,理论上容量无限。但是内存比硬盘贵得多,尽量能节省就节省。

第一个共识就是只保留页面有用的数据,没用的全部不保存。要用的时候,大不了再检索出来。已经查到skuid了,想要看sku的全部图片,包括整个商品的完整介绍,我们去数据库再查一遍就行了。

其次我们考虑哪些数据要进es,搜索名字的时候搜索的是sku的标题,sku信息得进来,可能还会根据sku价格区间进行检索,sku的销量,也就是说sku一些基本信息都是要用的。还要保存当前sku对象的规格信息

image-20201016214207112

设计存储方式。第二种虽然不冗余,但是存在一个极大的问题。

image-20201016220312811

image-20201016220337054

给定一个场景,检索手机的时候,每选中一个规格,比如选中了一个屏幕,5.49寸,再选中一个高清HD+,剩下又是一些可选规格

image-20201016220647157

但是这些可选规格不断在变,比如又选了个安卓,它有一个最大特点,这些所列举的属性查询出来的商品是一定拥有的,所以上面的规格,动态计算出来的。这么计算出来的呢?

在检索手机的时候,找到所有标题里面包含手机的商品,会把所有的商品聚合起来,分析一下所有商品涉及的所有属性,以及所有的属性值,点进某一个属性值,就保证下面商品都会拥有它,这里是动态计算的。假设要完成这些动态计算,

image-20201016220757800

10000个人搜索,集群中光数据传输,会有320mb数据,百万并发,就是32GB数据,别的不说,关阻塞时间就会非常长,所以说虽然第二种方案也是可以的,但是随着系统不断壮大,未来可能引申这样的问题。 所以一句话,空间与时间总是不能二者兼得,第一种浪费空间但是节省时间,第二种节省空间,分到了两个索引下,一定会造成一些时间的浪费,基于种种原因考虑,商品es存储的设计模型,就如方案一所示

image-20201016222756068

## 一、商品上架

上架的商品才可以在网站展示。

上架的商品需要可以被检索。

1、商品Mapping

分析:商品上架在es中是存sku还是spu?

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的所有关联属性,再做一次查询,
就必须将所有spuid都发出去。假设有1万个数据,数据传输一次就10000*4=4MB;
并发情况下假设1000检索请求,那就是4GB的数据,传输阻寒时间会很长,业务更加无法继续。

所以,我们如下设计,这样才是文档区别于关系型数据库的地方,宽表设计,不能去考虑数据库范式。

1)、PUT product
-

spuid后续会用到一个数据折叠功能,所以把他设计成keyword,以后会涉及再说。

为了防止数据精度问题,将skuPrice 保存成keyword

skuImg保存默认图片,index:false表示不可被检索,但是查询的是时候可以带,相当于冗余存储字段,为了一次查出来就能看到图片,“doc_values”: false 本来这个字段默认是true的,为false表示不能被聚合,排序,脚本。es就不会维护一些额外检索,更能节省空间。

一句话,只要做冗余存储的字段,只是为了拿来看一下,就标上这两个。 “index”: false, “doc_values”: false

原话是这样

All fields which support doc values have them enabled by default. If you are sure that you don’t need to sort or aggregate on a field, or access the field value from a script, you can disable doc values in order to save disk space:
默认情况下,所有支持doc值的字段均已启用它们。如果您确定不需要对字段进行排序或汇总,也不需要通过脚本访问字段值,则可以禁用doc值以节省磁盘空间:

hasStock 只是存储true或false,是否有库存,也就是说无需每天在数据库核查库存,再进行修改,因为数据只要一修改,es就会重新把它索引一次,维护整片索引也是很慢的过程,所有只有商品没库存的时候,才把它改一下,只要上来库存就把它改为true。这样要把实时更新库存要好的多。

attrs 当前这个商品,所有的属性规格,是一个数组,数组里面是对象,而且要按照对象里面某些值进行检索,相当于是内部的属性,标志nested,嵌入式的,如果不标就会出现问题,非常重要。

唯一需要全文匹配的就是skuTitle使用ik_smart分词器

商城业务-商品上架-nested数据类型场景

文档关于nested介绍https://www.elastic.co/guide/en/elasticsearch/reference/7.4/nested.html

数组类型的对象会被扁平化处理

image-20201016230912357

PUT my_index/_doc/1
{
  "group" : "fans",
  "user" : [ 
    {
      "first" : "John",
      "last" :  "Smith"
    },
    {
      "first" : "Alice",
      "last" :  "White"
    }
  ]
}

实际是这么存储的

{
  "group" :        "fans",
  "user.first" : [ "alice", "john" ],
  "user.last" :  [ "smith", "white" ]
}
GET my_index/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "user.first": "Alice" }},
        { "match": { "user.last":  "Smith" }}
      ]
    }
  }
}

存储过后,想要检索alice smith,应该没有一个人存在的,但是却检索出来了。

image-20201016231452307

也就是说检索这个数组里面的user.first,确实包含alice,user.last确实包含smith,所以返回了这个数组,

image-20201016231713505

解决方案

Using nested fields for arrays of objects
If you need to index arrays of objects and to maintain the independence of each object in the array, you should use the nested datatype instead of the object datatype. Internally, nested objects index each object in the array as a separate hidden document, meaning that each nested object can be queried independently of the others, with the nested query:

先删除原来索引

PUT my_index
{
  "mappings": {
    "properties": {
      "user": {
        "type": "nested" //用户是嵌入式的,就不会出现扁平化处理的这些错误
      }
    }
  }
}

PUT my_index/_doc/1
{
  "group" : "fans",
  "user" : [
    {
      "first" : "John",
      "last" :  "Smith"
    },
    {
      "first" : "Alice",
      "last" :  "White"
    }
  ]
}

GET my_index/_search
{
  "query": {
    "nested": {
      "path": "user",
      "query": {
        "bool": {
          "must": [
            { "match": { "user.first": "Alice" }},
            { "match": { "user.last":  "Smith" }} 
          ]
        }
      }
    }
  }
}

GET my_index/_search
{
  "query": {
    "nested": {
      "path": "user",
      "query": {
        "bool": {
          "must": [
            { "match": { "user.first": "Alice" }},
            { "match": { "user.last":  "White" }} 
          ]
        }
      },
      "inner_hits": { 
        "highlight": {
          "fields": {
            "user.first": {}
          }
        }
      }
    }
  }
}

image-20201016232655329

具体含义看文档。https://www.elastic.co/guide/en/elasticsearch/reference/7.4/nested.html

image-20201016233213038

商城业务-商品上架-构造基本数据

为上架的数据模型创建bean,直接在common里面创建bean,在product里面组装好数据,还要传给search服务,search再来进行上架,所以可以写在common里面,实际开发中可能因为权限只能在search里面看不到common里面的修改,如果这样的话,应该在product里面写一份,product会给search发送请求,在search里面再写一份,本次为了方便只在common里面写一份。

在common里面创建传输对象SkuEsModel,sku在es里面保存的数据模型。

package com.atguigu.common.to.es;

import jdk.internal.util.xml.impl.Attrs;
import lombok.Data;

import java.math.BigDecimal;
import java.util.List;

/**
 * @author tangyao
 * @version 1.0.0
 * @Description
 * @createTime 2020年10月17日 08:54:00
 */

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

}
上架接口实现
    @Override
    public void up(Long spuId) {

        //组装需要的数据
        //1、查出当前spuid对应的所有sku信息
        List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId);
        List<Long> skuIds = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());

        //调用远程仓储服务,提前查出来,避免循环查库
        Map<Long, Boolean> stockMap = null;
        // todo 1、发送远程调用,库存系统查询是否有库存
        try {
            //会有8次库存查询,所以希望远程服务有一个接口可以统一帮我们查到所有sku有没有库存
            R<List<SpuHasStockVo>> skuHasStock = wareFeignService.getSkuHasStock(skuIds);
            stockMap = skuHasStock.
                    getData().stream().collect(Collectors.toMap(SpuHasStockVo::getSkuId, SpuHasStockVo::getStock));
        } catch (Exception e) {
            log.error("调用库存服务查询异常,原因为{}" + e);
        }

        //封装attrs
        //todo 4、查询当前sku所有可以被检索的规格属性
//        productAttrValueService.baseAttrListForSpu();
        List<ProductAttrValueEntity> attrsBySpuId = productAttrValueService.getAttrsBySpuId(spuId);
        List<Long> attrIds = attrsBySpuId.stream().map(ProductAttrValueEntity::getAttrId).collect(Collectors.toList());

        //这是可被检索属性的id集合
        List<Long> searchAttrIds = attrService.selectSearchAttrIds(attrIds);
        //为了筛选出可被检索的商品attrs
        Set<Long> setAttrIds = new HashSet<>(searchAttrIds);
        //拿到所有的可以被检索的商品属性关系表中数据,并提取出商品需要的attrs
        List<SkuEsModel.Attrs> attrsList = attrsBySpuId.stream()
                .filter(item -> setAttrIds.contains(item.getAttrId()))
                .map(item -> {
                    SkuEsModel.Attrs attrs = new SkuEsModel.Attrs();
                    BeanUtils.copyProperties(item, attrs);
                    return attrs;
                }).collect(Collectors.toList());

        //封装每个sku信息
        Map<Long, Boolean> finalStockMap = stockMap;
        List<SkuEsModel> upProducts = skus.stream().map(sku -> {
            SkuEsModel skuEsModel = new SkuEsModel();
            BeanUtils.copyProperties(sku, skuEsModel);

            // skuPrice skuImg
            skuEsModel.setSkuPrice(sku.getPrice());
            skuEsModel.setSkuImg(sku.getSkuDefaultImg());

            // hasStock hotScore
            if (finalStockMap == null) {
                skuEsModel.setHasStock(true);
            } else {
                skuEsModel.setHasStock(finalStockMap.get(sku.getSkuId()));
            }
            // todo 2、热度评分。0
            skuEsModel.setHotScore(0L);

            // brandName brandImg
            //todo 3、查询品牌和分类的名字信息
            BrandEntity brand = brandService.getById(sku.getBrandId());
            skuEsModel.setBrandName(brand.getName());
            skuEsModel.setBrandImg(brand.getLogo());
            //  catalogName
            CategoryEntity category = categoryService.getById(sku.getCatalogId());
            skuEsModel.setCatalogName(category.getName());

            //   private Long attrId;
            //   private String attrName;
            //   private String attrValue;
            //设置检索属性
            skuEsModel.setAttrs(attrsList);
            return skuEsModel;
        }).collect(Collectors.toList());
        // 远程调用上架商品
        //todo 5、将数据发送给es保存
        R r = searchFeignService.productStatusUp(upProducts);
        if (r.getCode()==0){
            //远程调用成功
            //todo 更改spuinfo中商品的发布状态为已上架
            //状态应该作为枚举类存在的
            baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
        }else {
            //远程调用失败
            //todo 7、重复调用?接口幂等性;重试机制?xxx
        }


    }
@FeignClient("gulimall-ware")
public interface WareFeignService {

    /**
     * 1、R 在设计的时候加上泛型
     * 2、直接返回我们想要的结果
     * 3、自己封装解析结果
     * @param ids
     * @return
     */
    @PostMapping("ware/waresku/hasstock")
    R<List<SpuHasStockVo>> getSkuHasStock(@RequestBody List<Long> ids);




}
/**
 * 查询sku是否有库存
 *
 * @param ids
 * @return
 */
@PostMapping("/hasstock")
public R<List<SpuHasStockVo>> getSkuHasStock(@RequestBody List<Long> ids) {
    List<SpuHasStockVo> wareSkuVos = wareSkuService.getSkuHasStock(ids);
    R<List<SpuHasStockVo>>ok = R.ok();
    ok.setData(wareSkuVos);
    return ok;
}
   @Override
    public List<SpuHasStockVo> getSkuHasStock(List<Long> ids) {
        List<SpuHasStockVo> collect = ids.stream().map(skuId -> {
            SpuHasStockVo spuHasStockVo = new SpuHasStockVo();
            Long count = baseMapper.getSkuStock(skuId);
            spuHasStockVo.setSkuId(skuId);
            spuHasStockVo.setStock(count == null ? false : count > 0);
            return spuHasStockVo;
        }).collect(Collectors.toList());

        return collect;
    }

}
@FeignClient("gulimall-search")
public interface SearchFeignService {

    @PostMapping("/search/save/product")
    R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels);
}
@Slf4j
@RestController()
@RequestMapping("/search/save")
public class ElasticSearchSaveController {
    @Autowired
    ProductSaveService productSaveService;

    /**
     * 上架商品
     *
     * @param skuEsModels
     * @return
     */
    @PostMapping("/product")
    public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels) {
        Boolean b = false;
        try {
            //出现这种错误某一个sku数据真的有问题了
            b = productSaveService.productStatusUp(skuEsModels);
        } catch (IOException e) {
            //出现这种错误可能es客户端连不上了
            log.error("ElasticSearchSaveController商品上架错误,错误原因为{}", e);
            return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMessage());
        }
        if (!b) {
            return R.ok();
        }
        return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMessage());
    }

}
@Override
public Boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {

    // 保存到es
    //1、给es建立索引 product,由于索引经常用,所以也应该抽取为一个常量
    //建立好映射关系

    //2、给es中保存这些数据,
    BulkRequest bulkRequest = new BulkRequest();
    skuEsModels.forEach(skuEsModel -> {
        //1、构造保存请求
        IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
        //每一条数据都有一条唯一id
        indexRequest.id(skuEsModel.getSkuId().toString());
        //数据内容
        String s = JSON.toJSONString(skuEsModel);
        indexRequest.source(s, XContentType.JSON);
        bulkRequest.add(indexRequest);
    });

    //批量数据的返回每一条都是独立统计的
    BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);

    boolean b = bulk.hasFailures();
    List<String> collect = Arrays.stream(bulk.getItems()).map(item->item.getId()).collect(Collectors.toList());
    log.info("商品上架成功:{}", collect);
    return b;

}
<select id="getSkuHasStock" resultType="com.atguigu.gulimall.ware.vo.SpuHasStockVo">
    SELECT sku_id,SUM(stock-stock_locked) from `wms_ware_sku` WHERE sku_id in
    <foreach collection="ids" item="id" separator="," open="(" close=")">
        #{id}
    </foreach>
</select>
Feign源码分析

image-20201018014226075

来到feign的调用,先判断是即将调用的方法是否是equals,hashcode,toString 这样的基本方法,如果不是就进入dispatch进入真正的调用,这里就是远程调用的功能。

image-20201018014340112

进入了同步的方法处理器,用我们传过来的参数(SkuEsModel list类型数据)构造了一个RequestTemplate模板

image-20201018015006441

内容已经用utf-8 编码成data 数据,用view as String 可以看见,用json编码成的数据,说明feign在底层将对象转换为json,因为在对象传的时候,包括配置远程接口的时候,本身就说是@RequestBody,feign会给我们编码成json数据,

image-20201018020846765

接下来获取到retryer重试器,

image-20201018021217433

$1是一个内部类,executeAndDecode 执行和解码,相当于远程执行我们请求,再将响应拿过来,进行解码,解码完成后返回Object对象,进来继续看

image-20201018021324571

先来请求目标请求,用我们之前构造的template,里面有给哪里发请求,请求方式,用什么请求地址,包括整个数据json。通过这些来构造出这个请求,然后将这个请求发送出去,而且有日志的话会有日志记录,client.execute是真正的执行,

image-20201018021554344

LoadBalancerFeignClient,是一个负载均衡的客户端,相当于会真正的负载均衡的去执行这个请求,改调哪个服务就调用哪个服务,执行请求就是发送post请求的过程,就可以不看了,放行,

image-20201018021907026

一放行就来到了ElasticSearchSaveController,说明上一步放行,就会执行到远程接口,

image-20201018024421748

准备好的数据已经收集过来了,已经逆转好了,得益于SpringMVC的@RequestBody,自动将json封装好为对象,发请求是得益于,SynchronousMethodHandler,会将数据编码成json发给我们。

image-20201018024601105

image-20201018025415979

image-20201018025458167

直接抛异常

image-20201018025847739

尝试五次

image-20201018025934342

重试机制没有触发,默认是关闭状态

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

服务发送的第一个请求经常超时,服务的一些线程池,数据库连接池都还没有创建好,第一次请求还要初始化这些,

库存服务的Data里面没有set进去数据,

image-20201018120019701

因为R是hashmap,所以写的所有私有属性都没用了,只能存key value了

image-20201018120202895

可以将返回值改为List 但是还是推荐统一返回R。

还是返回R.ok(),ok的时候还是把数据放进去,给R写一个方法,因为R是一个map,所以放数据应该都往map里面放,而不是写自己的私有属性,

为了链式调用新增一个方法

public R setData(Object data){
    put("data",data);
    return this;
}

但是setData的时候,key是data,但是值是list,我们还想转成list,我们希望R有一个getData(),说给它转成什么对象就转成什么对象,

//利用fastjson进行逆转,
public <T> T getData(TypeReference<T> typeReference){
    //默认是map
    Object data = get("data");
    String s = JSON.toJSONString(data);
    T t = JSON.parseObject(s, typeReference);
    return t;
}

R在封装的时候,会默认将数据转成map。先把他转成json,再逆转成想要得的SpuHasStock。

image-20201018125401398

 try {
            //会有8次库存查询,所以希望远程服务有一个接口可以统一帮我们查到所有sku有没有库存
            R r = wareFeignService.getSkuHasStock(skuIds);
            TypeReference<List<SpuHasStockVo>> typeReference = new TypeReference<List<SpuHasStockVo>>() {
            };
            stockMap = r.getData(typeReference).stream().collect(Collectors.toMap(SpuHasStockVo::getSkuId,
                    SpuHasStockVo::getStock));
        } catch (Exception e) {
            log.error("调用库存服务查询异常,原因为{}" + e);
        }
首页-整合thymeleaf渲染首页

前后分离项目会屏蔽很多细节,所以进行服务端的页面渲染式开发。

首先用户访问所有请求,全部先访问nginx,nginx作为反向代理,将数据全部转发给网关,网关再路由到各个服务,有网关的好处可以做限流认证鉴权等工作,而加上nginx,部署的时候,可以将每一个微服务自己里面的页面(页面可以写在微服务里面)引用的静态资源,把他们搬家,全部部署到nginx里面,这样就做到了部署期间的动静分离,静指的就是静态资源,让nginx返回,动指的是动态请求,所有要经过服务器要处理的这个业务动态请求,就称为动态资源,这样的好处就为了分担微服务的压力,要不然将静态资源也放在微服务里面,请求一个图片也都要访问微服务,微服务的tomcat都要建立连接处理再返回,tomcat本来并发度就不高,假设有三千的并发,结果2000个都是处理图片的,只有1000个是进行业务调用处理的这样就会让项目,没有支持高并发功能。

image-20201018145801525

导入模板引擎

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

Springboot的static文件夹是放静态资源的,template放index.html

关闭缓存,这样就能看见实时效果,

image-20201018151119257

新创建web包和将controller改为app

image-20201018151419665

http://localhost:10000/可以直接访问到页面,原因是,

OrderedHiddenHttpMethodFilter来解决页面发送的请求,比如表单提交的时候提交put或者delete请求,将请求方式转换成对应的put或者delete,

image-20201018153455292

image-20201018153705278

默认访问webjars下的所有东西

image-20201018153844108

欢迎页的映射规则,

image-20201018154147397

当前路径下的所有请求,都可以去静态资源路径下去寻找,

image-20201018154133415

静态资源路径在这里配置的

image-20201018154218907

image-20201018154244841

image-20201018154549168

整合dev-tools渲染一级分类数据

不用加前后缀

image-20201018155537516

@Controller
public class IndexController {

    @Autowired
    CategoryService categoryService;
    @GetMapping({"/", "index.html"})
    public String indexPage(Model model) {
        //todo 1、查出所有的一级分类
        List<CategoryEntity> categoryEntities=categoryService.getLevel1Category();
        model.addAttribute("categorys",categoryEntities);
        return "index";
    }
}
<html lang="en" xmlns:th="http://www.thymeleaf.org">

image-20201018164918145

image-20201018164938150

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

ctrl+f9 就修改了,但是前提必须关闭thymeleaf的缓存

image-20201018165823323

image-20201018170134582

image-20201018170658983

image-20201018170825607

效果,一级分类已经出来,(二级和三级分类是写死的,和下面的显示问题)

image-20201018171013147

商城业务-首页-渲染二级三级分类数据

image-20201018204036522

json的数据模型,一共有21的1级分类,每一个一级分类下面有多个Object,每个对象包括一个catalog1Id,表示自己的一级节点属于哪个一级节点,

catalog3List,表示自己的三级分类有哪些,id表示当前的二级分类id,name表示当前二级分类名字。

image-20201018204629926

修改发送请求,获取真实的json数据

image-20201018222722748

@ResponseBody
@RequestMapping("/index/catalog.json")
public Map<String, List<Catalog2Vo>> getCatalogJson() {
    //最大的返回对象是一个json对象,说明他是一个map,map就是一个json对象,所以返回类型为一个map
    //不能写Vo,因为他的key都不确定,
    Map<String, List<Catalog2Vo>> catalogJson = categoryService.getCatalogJson();
    return catalogJson;
}
@Override
public Map<String, List<Catalog2Vo>> getCatalogJson() {
    //1、查出所有一级分类,
    List<CategoryEntity> category = getLevel1Categorys();
    //2、封装数据
    Map<String, List<Catalog2Vo>> parent_cid = category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
        //1、查到这个一级分类下的所有二级分类
        List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq(
                "parent_cid", v.getCatId()));

        //2、封装上面的结果
        List<Catalog2Vo> catalog2Vos = new ArrayList<>();
        if (categoryEntities != null && categoryEntities.size() != 0) {
            catalog2Vos = categoryEntities.stream().map(l2 -> {
                Catalog2Vo catalog2Vo = new Catalog2Vo(
                        l2.getParentCid().toString(), null, l2.getCatId().toString(), l2.getName());
                //1、找当前二级分类的三级分类封装成vo
                List<CategoryEntity> level3Catalog = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq(
                        "parent_cid", l2.getCatId()));
                List<Catalog2Vo.Catalog3Vo> collectlevel3 = new ArrayList<>();
                if (level3Catalog != null && !level3Catalog.isEmpty()) {
                    //2、封装成指定格式
                    collectlevel3 = level3Catalog.stream().map(l3 -> {
                        Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l3.getParentCid().toString(),
                                l3.getCatId().toString(), l3.getName());
                        return catalog3Vo;
                    }).collect(Collectors.toList());
                }
                catalog2Vo.setCatalog3List(collectlevel3);
                return catalog2Vo;
            }).collect(Collectors.toList());
        }
        return catalog2Vos;
    }));
    return parent_cid;
}
商城业务-nginx-搭建域名访问环境一(反向代理配置)

结合以前的nginx搭建出域名访问环境,nginx现在安装在虚拟机里面,讲es的时候安装了nginx做了分词器,将分词器的内容放在了里面,远程请求nginx返回分词器的数据,nginx,在以后的项目中经常使用它的反向代理,和负载均衡

image-20201018223149470

正向与方向相对于自己这台电脑来说,帮我们去上网的就是正向代理,帮助对方服务器了就是反向代理。

**正向代理:**我们想要访问谷歌,搭建一台代理服务器,为电脑配置上代理服务器的地址,电脑想访问任何网址,都由代理服务器帮我们去访问,访问拿到内容后帮我们返回,所以看到的是搭建的这台服务器是帮我们进行上网。

**反向代理:**搭建集群环境的时候非常需要,比如任何人去访问谷粒商城,谷粒商城有我们的后台服务集群,这些服务集群,每一个服务器,都可能都要在内网部署,这是一个内网ip,不可能把服务器的外网ip暴露给外界,这样容易引起攻击。这样做的话,整个服务器内网服务器集群,为了能找到他们,在他们前面前置一个服务器,把这个服务器叫做反向代理,比如前置一个nginx,nginx是拥有公网ip,大家都可以进行访问的。但是访问公网服务器,真正的项目是在内网集群部署的,所以由nginx代转给我们的服务集群,而这个nginx也是和我们的服务集群搭建在一个服务环境里面的。nginx可以帮我们找到服务集群在哪里。nginx相当于对外界屏蔽了我们整个内网服务集群的信息。比如商品服务在哪个ip地址,订单服务在哪个ip地址,我们现在都不知道。

正向代理就不是屏蔽了互联网信息,google的ip地址大家都知道,就是访问不了,所以反向代理是代价项目环境的时候,是一定要用到的。

利用这个功能,用nginx作为反向代理,请求gulimall的时候,先来到nginx,由nginx转交给我们的后台服务集群,这台nginx就拥有一个外网ip,这个外网ip就是大家公众都能访问的,每一个内网服务集群都只有内网ip地址,192.168这个地址仅限于局域网内部,出了这个局域网,大家都访问不到,所以就用nginx作为反向代理服务器,来完成整个的域名功能,但是想完成整个域名环境,先分析一下流程。

首先机器来访问gulimall,以本机环境为例,本机想访问gulimall.com 默认的访问流程,比如我们想访问www.baidu.com. 这个请求先会被网络的dns进行解析,解析出百度的ip地址到底在哪里,然后我们的浏览器就会访问到ip地址对应的内容,正是这个域名解析,没有购买gulimall的这个域名,但是可以在windows的host文件里面配置对应域名对应哪个ip地址,比如在浏览器敲gulimall.com,windows怎么知道,对应哪个ip地址?第一个先查看自己系统内部的域名映射规则,如果这个域名已经有映射了,浏览器就可以直接去这个地址,这是网卡带我们直接转过去的,接下来第二个,系统内部没有说gulimall在哪个地址,想要访问,先去网络上的dns,之前配linux系统的时候,配的备用dns 114.114.114.114还有8.8.8.8,解析出我们的域名,dns保存了哪一个域名对应哪一个ip地址,这只不过是在公网保存的。解析到ip地址后,然后转到对应的ip地址,所以基于这个原理,可以直接配置,gulimall.com域名在哪,直接来到指定的ip地址。那指定的ip地址在哪里呢,由于我们把nginx安装在了虚拟机上,所以可以让域名指定虚拟机的ip地址,

image-20201018223841397

C:\Windows\System32\drivers\etc下面的hosts

也可以用这个软件

image-20201019000106897

es和kibana都可以访问成功

image-20201019000213905

image-20201019000231134

以后每个系统都有对应的域名,都是访问虚拟机的ip地址,

直接访问gulimall.com,nginx正常启动

没有的话设置自动启动

sudo docker update nginx  --restart=always

image-20201019000734942

现在已经根据域名访问到nginx,接下来还要访问到项目,可以让nginx把所有的请求转给我们的网关,由网关再代转给每一个项目。当然也可以让nginx直接转给指定的项目,比如一看是访问的是gulimall.com,想要展示首页,相当于gulimall.com来到的所有请求,都给我转发到localhost:10000端口,也就是商品项目,拥有页面首页的内容,但是这样做不好,未来product项目部署多台的时候,有可能端口不一样,ip地址也不一样,每次都要修改nginx,让gulimall.com来的请求都转到10000端口。

我们先来这个最快的配置但是有局限性的配置-将gulimall请求直接转到10000端口

image-20201019002130728

配置方式:

nginx的总配置有一个细节

image-20201019010743309

nginx配置文件的内容

image-20201019010913723

conf.d 里面的所有配置文件都会合并放进nginx.conf,拆开一个文件就不会很大,比较清晰,

image-20201019011259371

查看默认配置

image-20201019011546110

server name相当于域名配置的虚拟主机,监听这个域名下的东西,

image-20201019011804455

这个域名下的所有请求,都可以在root,也就是根文件下找。

image-20201019011900867

cp default.conf gulimall.conf
    
vi gulimall.conf 

修改为,效果为,nginx是来监听gulimall这个域名下的,为什么能做到这个事?

image-20201019012330879

发这个请求的时候,是从哪个域名下发的请求,请求头里面都有一个host地址是来源于这个地名,由于他的ip映射的是nginx,这个请求的信息交给nginx, 而上面server name的配置是gulimall,相当于nginx就会拿请求里面的host进行匹配,看是不是gulimall,就相当于网关在转发的时候,之前是根据前缀进行匹配,现在是也可以按照来源于哪个主机地址进行匹配,相当于nginx来监听来源于gulimall.com的主机地址,监听到以后,

esc 退出插入模式,dd删除一行,

image-20201019012531510

image-20201019013913372

跟虚拟机中间的网卡是56.1也可以访问到本机

image-20201019013927203

实测这两个也可以 格式为:http://192.168.217.1:10000/

image-20201019014109208

:set number 以行号显示 //可不输这条命令

proxy_pass http://192.168.8.229:10000; //nginx每一个配置一定以;结尾

docker restart nginx

image-20201019014711666

可以访问到了,但是应该让nginx代理给网关,来解决分布式的问题

image-20201019015235299

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

https://nginx.org/en/docs/http/load_balancing.html

监听上游服务器

image-20201019143653219

监听gulimall.com的所有请求,直接代理给网关,网关整个上游服务器的名字就叫gulimall,会动态找到上游服务器组然后动态的转出去,相当于负载均衡的配置。

image-20201019144945253

重启nginx

docker restart nginx

配置网关路由规则

https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.3.RELEASE/reference/html/#the-host-route-predicate-factory

image-20201019150834973

 - id: gulimall_host_root
   uri: lb://gulimall-product
   predicates:
     - Host=**.gulimall.com 
# ** 代表子域名

nginx确实路由到网关了,映射api路径都行,但是直接访问gulimall.com却不行,

image-20201019151422858

image-20201019151230527

原因是nginx在代理给网关的时候会丢失host信息,设置请求头的host信息,只有给gulimall转发的时候配置加上head,相当于路由到网关会加头,其他没设置的路径默认都不加

image-20201019152304056

注意gateway配置的时候一定要写在最后面,网关一进来,就算是发的api请求,由于优先匹配到域名,所以直接路由给商品服务,就会去商品服务里面找api全路径,商品服务真正的是要把api商品服务截串的,相当于就把下面的配置禁用掉了,导致没有截串 所有的api请求,发生404

image-20201019153126414

image-20201019152631267

性能压测-压力测试-基本介绍

image-20201019161123714

image-20201019161347419

image-20201019161722707

安装JMeter
https://jmeter.apache.org/download_jmeter.cgi

image-20201019162019344

image-20201019162150172

image-20201019190910219

image-20201019192739508

image-20201019193533807

加大服务占用内存测试

image-20201019192702331

server.tomcat.accept-count:等待队列长度,默认100(队列也做缓冲池用,但也不能无限长, 不但消耗内存,而且出队入队也消耗CPU)
server.tomcat.max-connections:最大可被连接数,默认10000
server.tomcat.max-threads:最大工作线程数,默认200,线程数不是越多越好,要考虑操作系统上下文切换的开销
server.tomcat.min-spare-threads:最小工作线程数,默认10(用来解决突发的容量问题,需要有一些在工作的线程),操作系统可以有充足的时间反应,先用这10个,不够的再开启就可以
————————————————
版权声明:本文为CSDN博主「bob_man」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/bob_man/article/details/104655349

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

按照老师操作我并没有任何问题,出现问题我再改

image-20201019193649181

#### JMeter Address Already in use 错误解决

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

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

1.`cmd` 中,用 `regedit` 命令打开`注册表`

2.在 `HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters` 下

> 1,右击 `parameters`,添加一个新的 `DWORD`,名字为 `MaxUserPor`
>
> 2.然后双击 `MaxUserPort`,输入数值数据为 `65534`,基数选择`十进制`(如果是分布式运行的话,控制机器和负载机器都需要这样操作哦)

3,修改配置完毕之后记得重启机器才会生效

https://support.microsoft.com/zh-cn/help/196271/when-you-try-to-connect-from-tcp-ports-greater-than-5000-you-receive-t

`TCPTimedWaitDelay`: 30

image-20201019195315634

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

image-20201019200007356

image-20201019200350414

image-20201019200636929

image-20201019200730207

image-20201019200804272

image-20201019200813628

性能压测-性能监控-jvisualvm使用

image-20201019201748735

-Dcom.sun.management.jmxremote

image-20201019202717992

image-20201019203307070

image-20201019203358910

启动jvisualvm

image-20201019203658970

如果开启金山词霸的话,会出现这样的错误 process finished with exit code 0xc0000005

建议关闭 https://blog.csdn.net/m0_37616916/article/details/88358195

image-20201019205232310

image-20201019205725558

image-20201019205837534

image-20201019210042439

image-20201019210644644

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

测试nginx,nginx监听虚拟机的80端口,之前装nginx的时候在root目录下放了一个首页,所以给nginx端口发送请求,默认返回首页,

docker stats 查看cpu使用率内存等等

image-20201019213239935

进行压力测试,发现,比较浪费cpu内存占用很低, 因为他主要需要更多的线程去处理请求,cpu要来回在线程之间切换计算,

image-20201019213854690

image-20201019220959162

image-20201019221024736

开始压网关

image-20201019222058915

网关比较浪费cpu,网关功能和nginx基本是差不多的

image-20201019222023827

如果eden space的大小改变,gc时间减少,就又会将吞吐量提升,

image-20201019222344592

查看简单服务写一个简单服务,没有页面渲染,也不操作数据库。

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

image-20201019223026512

image-20201019223329510

现在想看Gateway加简单服务的压测,gateway除了映射/api/product 以外,还来映射/hello请求,因为不是api请求,也不用截串

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

image-20201019224427142

压测全链路

image-20201019234847127

加粗字体为单压

由于压力测试和真正服务在同一台机器,同时来压的话会线程竞争,所以真正的压力测试不应该是机器部署在服务器之后真正要承受的压力,用另外一台机器来压就是相对标准的数据,

压力测试表
压测内容压测线程数吞吐量/s90%响应时间99%响应时间
Nginx5095013149
Gateway502436636
简单服务504057426
首页一级菜单渲染501294(db,thymeleaf)63114
首页渲染(开缓存)5019973058
首页渲染(开缓存,优化数据库,关日志)5026172337
三级分类数据获取5022(db)/31(开缓存,优化数据库关日志后)23552848
三级分类(优化业务)50316269435
三级分类(使用redis作为缓存)5019423452
首页全部数据获取5034(静态资源)
Nginx+Gateway50
Gateway+简单服务5010313920
全链路502129920

结论:中间件越多,性能损失越大,大多都损失到网络交互了;

业务:db

模板的渲染速度(cpu 内存,最重要缓存),

静态资源(tomcat还要分一些线程来处理静态资源,吞吐量下降很多)

性能压测-优化-简单优化吞吐量测试

压测首页的时候,响应得数据是页面模板,但是页面模板引了非常多的图片,这些图片实际上还是要渲染的,相当于要给服务器发请求,再拿过来。所以一个完整的请求,应该是整个页面,给我们返回来的这个请求,所以在压测的时候压测整个页面的这个返回,

image-20201020010116483

首页全部数据获取

image-20201020011114403

首页渲染(开缓存,优化数据库,关日志)

image-20201020015814096

记录建索引前的消耗时间

image-20201020015716912

建立索引

image-20201020015949748

image-20201020020031093

性能压测-优化-nginx动静分离

image-20201020124622241

mkdir /mydata/nginx/html/static

chmod 777 static

将index静态资源复制过来

image-20201020125858232

image-20201020130937612

image-20201020131034208

image-20201020131107805

image-20201020131425693

  thymeleaf:
    cache: false
cd /mydata/nginx/conf/conf.d/

default配置以前用过,defalut配置location/ ,localhost访问所有请求,root就是这些请求去哪些文件夹下进行资源匹配,之前正好用了/做了es的资源分词器,

http://192.168.218.128/es/fenci.txt,所以后来配置都加一个root,代表这些路径都到哪个地方来找,这里就是都去这个 xxxxx/html路径下去找

image-20201020132053888

修改gulimall.conf

location /static/ {
        root  /usr/share/nginx/html;
    }

static的所有都去哪里找,都去root对应后面的目录去找,因为整个路径是完整的,不仅有static ,还有static文件夹,接下来有什么,就按照层级目录在文件夹下写了什么。除了static外,剩下的转给gulimall(网关的整个集群,而且以负载均衡的方式)

image-20201020172655519

现在首页的静态资源,全部都由nginx返回,首页的数据,全部都是由tomcat返回,这就是nginx的动静分离配置

最起码现在的tomcat只处理动态请求,占用的资源就会很小了。

性能压测-优化-模拟线上应用内存崩溃宕机情况

我用1000个线程来压

image-20201020182707316

访问首页已经不能提供服务了,提示找不到实例了,这就是线上实例的整个过程,持续在运行期间,cpu,内存爆满卡死,将应用挤下线,

image-20201020182751416

image-20201020181232513

性能压测-优化-优化三级分类数据获取

修改位置

 List<CategoryEntity> category = getParent_cid(selectList, 0L);

List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());

List<CategoryEntity> level3Catalog = getParent_cid(selectList, l2.getCatId());
 private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList, Long Parent_cid) {
        List<CategoryEntity> collect =
                selectList.stream().filter(item -> item.getParentCid().equals(Parent_cid)).collect(Collectors.toList());

        return collect;
//        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq(
//                "parent_cid", v.getCatId()));
    }

再将内存改回去 -xms100m

压测 http://localhost:10000/index/catalog.json 吞吐量现在是316

image-20201020185652609

性能提升大神器缓存要来了!!!!

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

image-20201020190033390

**即时性:**物流状态信息适合放入缓存,

image-20201020190310204

image-20201020190650840

image-20201020190828875

可以使用map来做本地缓存,但是会有一些问题,

image-20201020192533517

本地缓存:缓存的组件名字假设为cache,和我们这个代码属于同一个进程,他们运行在同一个项目里面,在同一个jvm里面,只相当于在本地保存一个副本。

如果这个应用是单体应用,永远只部署在一台机器上,什么问题都没有,而且很快,

1.分布式下缓存是分开的,各顾各的,当负载均衡到不同微服务时,只要没有缓存都要重新查一份,

2.如果数据修改,假如三级分类数据修改,为了能读取到正确的数据,一般性还要改一下缓存里面的数据,假设第一次修改请求来到了一号服务器,修改分类数据并修改缓存,之前二,三号服务器里面缓存没法改,因为负载均衡是在一号的,所以以后所有请求,负载均衡到二号三号拿到的数据和一号拿到的数据是不一样的,这就产生了数据一致性问题。

image-20201020191711802

解决方式:在分布式情况下,不应该使用本地缓存,共享一个集中式的缓存中间件

image-20201020192226274

缓存-缓存使用-整合redis测试

https://docs.spring.io/spring-boot/docs/2.2.10.RELEASE/reference/html/using-spring-boot.html#using-boot-starter

image-20201020193245646

<!--     引入redis    -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

接下来就要配置redis

image-20201020193847254

泛型是String,String,key使用String类型的序列化机制来做的,value也是String类型的序列化做的,

image-20201020194641160

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

hello = world_5e324086-d00c-4ac2-9390-3850018b970a

缓存-缓存使用-改造三级分类业务
@Override
public Map<String, List<Catalog2Vo>> getCatalogJson() {
    //给缓存中放json字符串,拿出json字符串,还能逆转为能用的对象类型,【序列化与反序列化】

    //1、加入缓存逻辑,缓存中存储的是json字符串。
    //JSON跨语言,跨平台兼容
    String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
    if (StringUtils.isEmpty(catalogJSON)) {
        //2、缓存中没有,查询数据库
        Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();

        //3、将查到的数据放到缓存,将对象转为json放到缓存中
        String s = JSON.toJSONString(catalogJsonFromDb);
        stringRedisTemplate.opsForValue().set("catalogJSON", s);
        return catalogJsonFromDb;
    }
    //转为我们指定的对象
    Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
    });

    return result;
}
	
	更改以前查询方法的名字
    //从数据库查询并封装分类数据
    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDb() {}
缓存-缓存使用-压力测试出的内存泄露及解决

压测出现大量异常,

老师的机器出现堆外异常

netty是直接操作堆外内存的

image-20201020224725615

用jvisualvm监控也没问题

image-20201020234605190

image-20201020235225517

     <!--     引入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>

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

netty底层自己在计数,数字超过默认的容量限制,就会抛异常,这个就是堆外溢出异常。netty统计内存使用量,操作完了就会减内存使用量,一定是lettcure客户端,在哪一块操作的时候,没有及时调用掉减内存,导致堆外内存溢出,除了升级就是更换

image-20201021000321374

image-20201021000403380

image-20201021001058973

image-20201021000819812

无论这两个谁连接都会放一个连接工厂,就是连接要用的RedisConnectFactory

image-20201021000917670

缓存-缓存使用-缓存击穿、穿透、雪崩

image-20201021001231403

image-20201021001611812

image-20201021002159443

image-20201021002818245

缓存-缓存使用-加锁解决缓存击穿问题

第一种解决方式在单体引用下,一个tomcat一台服务器,这样锁没问题,缺点: 在分布式情况下,锁不住所有服务

 //从数据库查询并封装分类数据
    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDb() {


        //只要是同一把锁,就能锁住需要这个锁的所有线程
        //1、synchronized (this):Springboot所有的组件在容器中都是单例的,所以即使有100万并发进来,
        // 调CategoryServiceImpl的这个方法,这个service只有一个实例对象,this是单例的,相当于100个请求用的是同一个this,就能锁住了
        synchronized (this) {

            //得到锁以后应该再去缓存中确定一次,如果没有才需要继续查询
            String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
            if (StringUtils.isNotEmpty(catalogJSON)) {
                //缓存不为null,直接返回,
                Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
                });

                return result;
            }
            /**
             * 优化 1、将数据库的多次查询,变为一次
             */
            List<CategoryEntity> selectList = baseMapper.selectList(null);

            //1、查出所有一级分类,
//        List<CategoryEntity> category = getLevel1Categorys();
            List<CategoryEntity> category = getParent_cid(selectList, 0L);

            //2、封装数据
            Map<String, List<Catalog2Vo>> parent_cid = category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
                //1、查到这个一级分类下的所有二级分类
                List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());

                //2、封装上面的结果
                List<Catalog2Vo> catalog2Vos = new ArrayList<>();
                if (categoryEntities != null && categoryEntities.size() != 0) {
                    catalog2Vos = categoryEntities.stream().map(l2 -> {
                        Catalog2Vo catalog2Vo = new Catalog2Vo(
                                l2.getParentCid().toString(), null, l2.getCatId().toString(), l2.getName());
                        //1、找当前二级分类的三级分类封装成vo
                        List<CategoryEntity> level3Catalog = getParent_cid(selectList, l2.getCatId());
                        List<Catalog2Vo.Catalog3Vo> collectlevel3 = new ArrayList<>();
                        if (level3Catalog != null && !level3Catalog.isEmpty()) {
                            //2、封装成指定格式
                            collectlevel3 = level3Catalog.stream().map(l3 -> {
                                Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l3.getParentCid().toString(),
                                        l3.getCatId().toString(), l3.getName());
                                return catalog3Vo;
                            }).collect(Collectors.toList());
                        }
                        catalog2Vo.setCatalog3List(collectlevel3);
                        return catalog2Vo;
                    }).collect(Collectors.toList());
                }
                return catalog2Vos;
            }));
            return parent_cid;
        }
    }

image-20201021143317693

image-20201021144534849

image-20201021144554608

100线程进行压力测试,结果发发现查询了两次数据库,

image-20201021144622845

image-20201021144609806

过程分析:100万并发进来,看我们的缓存,大家都进来看缓存,缓存没有,假设都走到StringUtils.isEmpty(catalogJSON),缓存中没有,都打印缓存没命中,准备去查数据库,查数据库的时候,上来就锁了一把锁,只有一个线程进来了查数据库,只要线程查完数据库,就释放锁,锁住的其他线程,进进来了。2号进来,就先确认缓存有没有,1号线程释放锁以后,要给redis里面放数据,单给redis放数据是一次网络交互,可能很慢,包括刚启动起来,还要给redis建立连接,还要整线程池一堆操作,线程池等等都还没有初始化,第一次来做是一次很慢的过程,所以,就导致,1号线程还没有把数据放进去,2号线程从redis中获取,确实没有缓存,就又查了一遍数据库,

image-20201021145738672

改造方法

image-20201021145837648

将放入缓存的步骤放在同步代码块的下,保证查完数据库立刻将结果放入缓存。是一个原子操作,在同一把锁内进行的。否则就会导致释放锁的时序问题,查了两边数据库

image-20201021150157759

image-20201021150556534

缓存-缓存使用-本地锁在分布式下的问题

copyConfiguration

image-20201021151117492

image-20201021151344160

几个服务都都跑起来,压测直接从nginx到网关再负载均衡到各个服务,删掉缓存中数据

image-20201021151526112

image-20201021151733987

发现每一个服务都查询了一次数据库,本地锁的this只能锁住当前服务,其他人进入其他服务,都会进行一次查询

缓存-分布式锁-分布式锁原理与使用

image-20201021153538426

打开多个客户端

image-20201021182113020

docker exec -it redis redis-cli
    
set lock haha nx

124客户端都为nil

image-20201021182441992

3为ok

image-20201021182448984

image-20201021182611807

image-20201021183241885

image-20201021185202285

image-20201021184959965

image-20201021185144289

image-20201021185447032

del lock

//必须保证获取到锁和设置过期时间是一个原子操作
set lock 111 EX 300 NX

ttl lock 

image-20201021190012773

image-20201021191235516

​ 核心:加锁保证原子性,解锁保证原子性

image-20201021191403506

redis设置分布式锁文档

http://www.redis.cn/commands/set.html

image-20201021202355001

public Map<String, List<Catalog2Vo>> getCatalogJson() {
        //给缓存中放json字符串,拿出json字符串,还能逆转为能用的对象类型,【序列化与反序列化】

        /**
         * 1、空结果缓存,解决缓存穿透
         * 2、设置过期时间(加随机值),解决缓存雪崩
         * 3、加锁,解决缓存击穿
         *
         */

        //1、加入缓存逻辑,缓存中存储的是json字符串。
        //JSON跨语言,跨平台兼容
        String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)) {
            //2、缓存中没有,查询数据库
            System.out.println("缓存不命中。。。。将要查询数据库。。。。");
            Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedisLock();


            return catalogJsonFromDb;
        }
        System.out.println("缓存命中。。。。直接返回。。。。");
        //转为我们指定的对象
        Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
        });

        return result;

    }

    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {

        String token = UUID.randomUUID().toString();

        //1、占分布式锁。去redis占坑,
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", token, 100, TimeUnit.SECONDS);


        if (lock) {
            System.out.println("获取分布式锁成功");

            //为了让锁自动续期,不至于执行途中因为时间过短而失效,可以设置时间长一些,然后finally保证业务操作完成之后,就执行删除锁的操作
            //不管怎样,哪怕崩溃也直接解锁,不关心业务异常
            Map<String, List<Catalog2Vo>> dataFromDB;
            try {
                //加锁成功
                dataFromDB = getDataFromDB();
            } finally {

                //存在网络时延问题,比如在redis获取到lock返回时,lock过期被自动删除,
                // 此时其他线程抢占了锁,创建了lock,但是会被这个线程删掉的情况
//            String lock1 = stringRedisTemplate.opsForValue().get("lock");
//            if (token.equals(lock1)) {
//                stringRedisTemplate.delete("lock");
//            }
                String lua = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                //RedisScript<T> script, List<K> keys, Object... args
                RedisScript<Long> luaScript = RedisScript.of(lua, Long.class);
                //删除锁
                Long lock1 = stringRedisTemplate.execute(luaScript, Arrays.asList("lock"), token);
            }

            return dataFromDB;
        } else {
            System.out.println("获取分布式锁失败,等待重试");
            //加锁失败。。。重试      synchronized
            //自旋的方式
            //休眠100ms重试
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getCatalogJsonFromDbWithRedisLock();
        }

    }
private Map<String, List<Catalog2Vo>> getDataFromDB() {
        //得到锁以后应该再去缓存中确定一次,如果没有才需要继续查询
        String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isNotEmpty(catalogJSON)) {
            //缓存不为null,直接返回,
            Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
            });

            return result;
        }
        System.out.println(Thread.currentThread().getName() + "查询了数据库");
        List<CategoryEntity> selectList = baseMapper.selectList(null);

        List<CategoryEntity> category = getParent_cid(selectList, 0L);

        //2、封装数据
        Map<String, List<Catalog2Vo>> parent_cid = category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //1、查到这个一级分类下的所有二级分类
            List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());

            //2、封装上面的结果
            List<Catalog2Vo> catalog2Vos = new ArrayList<>();
            if (categoryEntities != null && categoryEntities.size() != 0) {
                catalog2Vos = categoryEntities.stream().map(l2 -> {
                    Catalog2Vo catalog2Vo = new Catalog2Vo(
                            l2.getParentCid().toString(), null, l2.getCatId().toString(), l2.getName());
                    //1、找当前二级分类的三级分类封装成vo
                    List<CategoryEntity> level3Catalog = getParent_cid(selectList, l2.getCatId());
                    List<Catalog2Vo.Catalog3Vo> collectlevel3 = new ArrayList<>();
                    if (level3Catalog != null && !level3Catalog.isEmpty()) {
                        //2、封装成指定格式
                        collectlevel3 = level3Catalog.stream().map(l3 -> {
                            Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l3.getParentCid().toString(),
                                    l3.getCatId().toString(), l3.getName());
                            return catalog3Vo;
                        }).collect(Collectors.toList());
                    }
                    catalog2Vo.setCatalog3List(collectlevel3);
                    return catalog2Vo;
                }).collect(Collectors.toList());
            }
            return catalog2Vos;
        }));

        //3、将查到的数据放到缓存,将对象转为json放到缓存中
        String s = JSON.toJSONString(parent_cid);
        stringRedisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);


        return parent_cid;
    }
private Map<String, List<Catalog2Vo>> getDataFromDB() {
    //得到锁以后应该再去缓存中确定一次,如果没有才需要继续查询
    String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
    if (StringUtils.isNotEmpty(catalogJSON)) {
        //缓存不为null,直接返回,
        Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
        });

        return result;
    }
    System.out.println(Thread.currentThread().getName() + "查询了数据库");
    List<CategoryEntity> selectList = baseMapper.selectList(null);

    List<CategoryEntity> category = getParent_cid(selectList, 0L);

    //2、封装数据
    Map<String, List<Catalog2Vo>> parent_cid = category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
        //1、查到这个一级分类下的所有二级分类
        List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());

        //2、封装上面的结果
        List<Catalog2Vo> catalog2Vos = new ArrayList<>();
        if (categoryEntities != null && categoryEntities.size() != 0) {
            catalog2Vos = categoryEntities.stream().map(l2 -> {
                Catalog2Vo catalog2Vo = new Catalog2Vo(
                        l2.getParentCid().toString(), null, l2.getCatId().toString(), l2.getName());
                //1、找当前二级分类的三级分类封装成vo
                List<CategoryEntity> level3Catalog = getParent_cid(selectList, l2.getCatId());
                List<Catalog2Vo.Catalog3Vo> collectlevel3 = new ArrayList<>();
                if (level3Catalog != null && !level3Catalog.isEmpty()) {
                    //2、封装成指定格式
                    collectlevel3 = level3Catalog.stream().map(l3 -> {
                        Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l3.getParentCid().toString(),
                                l3.getCatId().toString(), l3.getName());
                        return catalog3Vo;
                    }).collect(Collectors.toList());
                }
                catalog2Vo.setCatalog3List(collectlevel3);
                return catalog2Vo;
            }).collect(Collectors.toList());
        }
        return catalog2Vos;
    }));

    //3、将查到的数据放到缓存,将对象转为json放到缓存中
    String s = JSON.toJSONString(parent_cid);
    stringRedisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);


    return parent_cid;
}
private Map<String, List<Catalog2Vo>> getDataFromDB() {
    //得到锁以后应该再去缓存中确定一次,如果没有才需要继续查询
    String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
    if (StringUtils.isNotEmpty(catalogJSON)) {
        //缓存不为null,直接返回,
        Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
        });

        return result;
    }
    System.out.println(Thread.currentThread().getName() + "查询了数据库");
    List<CategoryEntity> selectList = baseMapper.selectList(null);

    List<CategoryEntity> category = getParent_cid(selectList, 0L);

    //2、封装数据
    Map<String, List<Catalog2Vo>> parent_cid = category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
        //1、查到这个一级分类下的所有二级分类
        List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());

        //2、封装上面的结果
        List<Catalog2Vo> catalog2Vos = new ArrayList<>();
        if (categoryEntities != null && categoryEntities.size() != 0) {
            catalog2Vos = categoryEntities.stream().map(l2 -> {
                Catalog2Vo catalog2Vo = new Catalog2Vo(
                        l2.getParentCid().toString(), null, l2.getCatId().toString(), l2.getName());
                //1、找当前二级分类的三级分类封装成vo
                List<CategoryEntity> level3Catalog = getParent_cid(selectList, l2.getCatId());
                List<Catalog2Vo.Catalog3Vo> collectlevel3 = new ArrayList<>();
                if (level3Catalog != null && !level3Catalog.isEmpty()) {
                    //2、封装成指定格式
                    collectlevel3 = level3Catalog.stream().map(l3 -> {
                        Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l3.getParentCid().toString(),
                                l3.getCatId().toString(), l3.getName());
                        return catalog3Vo;
                    }).collect(Collectors.toList());
                }
                catalog2Vo.setCatalog3List(collectlevel3);
                return catalog2Vo;
            }).collect(Collectors.toList());
        }
        return catalog2Vos;
    }));

    //3、将查到的数据放到缓存,将对象转为json放到缓存中
    String s = JSON.toJSONString(parent_cid);
    stringRedisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);


    return parent_cid;
}
private Map<String, List<Catalog2Vo>> getDataFromDB() {
    //得到锁以后应该再去缓存中确定一次,如果没有才需要继续查询
    String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
    if (StringUtils.isNotEmpty(catalogJSON)) {
        //缓存不为null,直接返回,
        Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
        });

        return result;
    }
    System.out.println(Thread.currentThread().getName() + "查询了数据库");
    List<CategoryEntity> selectList = baseMapper.selectList(null);

    List<CategoryEntity> category = getParent_cid(selectList, 0L);

    //2、封装数据
    Map<String, List<Catalog2Vo>> parent_cid = category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
        //1、查到这个一级分类下的所有二级分类
        List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());

        //2、封装上面的结果
        List<Catalog2Vo> catalog2Vos = new ArrayList<>();
        if (categoryEntities != null && categoryEntities.size() != 0) {
            catalog2Vos = categoryEntities.stream().map(l2 -> {
                Catalog2Vo catalog2Vo = new Catalog2Vo(
                        l2.getParentCid().toString(), null, l2.getCatId().toString(), l2.getName());
                //1、找当前二级分类的三级分类封装成vo
                List<CategoryEntity> level3Catalog = getParent_cid(selectList, l2.getCatId());
                List<Catalog2Vo.Catalog3Vo> collectlevel3 = new ArrayList<>();
                if (level3Catalog != null && !level3Catalog.isEmpty()) {
                    //2、封装成指定格式
                    collectlevel3 = level3Catalog.stream().map(l3 -> {
                        Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l3.getParentCid().toString(),
                                l3.getCatId().toString(), l3.getName());
                        return catalog3Vo;
                    }).collect(Collectors.toList());
                }
                catalog2Vo.setCatalog3List(collectlevel3);
                return catalog2Vo;
            }).collect(Collectors.toList());
        }
        return catalog2Vos;
    }));

    //3、将查到的数据放到缓存,将对象转为json放到缓存中
    String s = JSON.toJSONString(parent_cid);
    stringRedisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);


    return parent_cid;
}
缓存-分布式锁-Redisson简介&整合

https://github.com/redisson/redisson/wiki/Table-of-Content

<!-- 以后要使用redission作为所有分布式锁,分布式对象等功能 -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
        </dependency>

https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95

image-20201021225359714

image-20201021225516453

https://github.com/redisson/redisson/wiki/14.-%E7%AC%AC%E4%B8%89%E6%96%B9%E6%A1%86%E6%9E%B6%E6%95%B4%E5%90%88

image-20201021225116178

@Configuration
public class MyRedissonConfig {

    /**
     * 所有对Redisson的使用都是对RedissionClient对象的操作
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod="shutdown")
    public RedissonClient redisson() throws IOException {
        //1、创建配置
        Config config = new Config();
        config.useSingleServer().setAddress("192.168.218.128:6379");
        //2、根据Config创建出RedisClient实例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}
@Autowired
private RedissonClient redissonClient;


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

image-20201021225912528

image-20201021225936259

config.useSingleServer().setAddress("redis://192.168.218.128:6379");
打印结果
org.redisson.Redisson@210c1b9d
缓存-分布式锁-Redisson-lock锁测试

https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8

@ResponseBody
@GetMapping("/hello")
public String hello() {
    //1、获取同一把锁,只要锁的名字一样,就是同一把锁,
    RLock lock = redisson.getLock("my-lock");
    //2、加锁
    //阻塞式等待
    lock.lock();
    try{
        System.out.println("加锁成功,执行业务"+Thread.currentThread().getId());
        Thread.sleep(30000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        //3、解锁
        System.out.println(Thread.currentThread().getId()+"释放锁");
        lock.unlock();
    }
    return "hello";
}

84为线程号

image-20201021232250064

跑两个商品服务10000端口和10002,分别发请求,然后中断10000端口,没有手动释放锁,看看redisson会不会死锁

image-20201021233030551

image-20201021233012703

手动没有解锁,也会为我解锁

不断获取锁,只要能获取锁就继续执行我们的业务

image-20201021233935718

缓存-分布式锁-Redisson-lock看门狗原理-redisson如何解决死锁

image-20201022090136596

image-20201022090145313

image-20201022090928072

 @ResponseBody
    @GetMapping("/hello")
    public String hello() {
        //1、获取同一把锁,只要锁的名字一样,就是同一把锁,
        RLock lock = redisson.getLock("my-lock");
        //2、加锁
        //阻塞式等待,默认加的锁都是30秒
        //lock.lock();
        //1)、锁的自动续期,如果业务超长,运行期间自动给锁续上30s,不用担心业务时间长,锁自动过期被删掉
        //2)、加锁的业务只要运行完成,就不会给当前续期,即使不手动删除解锁,锁默认在30s以后自动删除。

        //10秒自动解锁,自动解锁时间一定要大于业务的执行的时间
        lock.lock(10, TimeUnit.SECONDS);
        // 问题:在锁时间到了以后,不会自动续期
        //1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间
        //2、如果我们未指定锁的超时时间,就使用 30 * 1000 【看门狗lockWatchdogTimeout的默认时间】
        // 只要占锁成功,就会启动一个定时任务【重新给锁设定过期时间,新的过期时间就是看门狗的默认时间】,每隔10s自动续期,续成30s
        // internalLockLeaseTime / 3【看门狗时间】/3,10s

        //最佳实战
        //1) lock.lock(10, TimeUnit.SECONDS);省掉了整个续期操作,手动解锁  
        try {
            System.out.println("加锁成功,执行业务" + Thread.currentThread().getId());
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //3、解锁 假设解锁代码没有运行,redis会不会出现死锁
            System.out.println(Thread.currentThread().getId() + "释放锁");
            lock.unlock();
        }
        return "hello";
    }
}
缓存-分布式锁-Redisson-读写锁测试

https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8

/**
 * 保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁)。读锁是一个共享锁
 * 写锁没释放,读就必须等待
 *
 * @return
 */
@GetMapping("/read")
@ResponseBody
public String readValue() {

    RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
    String s = "";
    RLock rLock = readWriteLock.readLock();
    try {
        rLock.lock();
        s = stringRedisTemplate.opsForValue().get("writeValue");
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        rLock.unlock();
    }
    return s;
}


@GetMapping("/write")
@ResponseBody
public String writeValue() {

    RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
    String s = "";
    RLock rLock = readWriteLock.writeLock();
    try {
        //1、改数据加写锁,读数据加读锁
        rLock.lock();
        TimeUnit.SECONDS.sleep(10);
        s = UUID.randomUUID().toString();
        stringRedisTemplate.opsForValue().set("writeValue", s);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        rLock.unlock();
    }
    return s;
}
补充细节
/**
 * 保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁,共享锁)。读锁是一个共享锁
 * 写锁没释放,读就必须等待
 *
 * 读 + 读 :相当于无锁,并发读,只会在redis中记录好,所有当前的读锁,他们都会同时加锁成功
 * 写 + 读 :等待写锁释放
 * 写 + 写:阻塞方式
 * 读 + 写:有读锁,写也需要等待
 * //只要有写的存在,都必须等待
 * @return
 */
@GetMapping("/read")
@ResponseBody
public String readValue() {

    RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
    String s = "";
    RLock rLock = readWriteLock.readLock();
    rLock.lock();
    try {
        System.out.println("读锁加锁成功。。。。"+Thread.currentThread().getId());
        s = stringRedisTemplate.opsForValue().get("writeValue");
        Thread.sleep(30000);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        rLock.unlock();
        System.out.println("读锁释放"+Thread.currentThread().getId());
    }
    return s;
}


@GetMapping("/write")
@ResponseBody
public String writeValue() {

    RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
    String s = "";
    RLock rLock = readWriteLock.writeLock();
    try {
        //1、改数据加写锁,读数据加读锁
        rLock.lock();
        System.out.println("写锁加锁成功。。。。"+Thread.currentThread().getId());
        s = UUID.randomUUID().toString();
        Thread.sleep(10000);
        stringRedisTemplate.opsForValue().set("writeValue", s);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        rLock.unlock();
        System.out.println("写锁释放"+Thread.currentThread().getId());
    }
    return s;
}
/**
 * 信号量可以做分布式限流
 *
 * @return
 * @throws InterruptedException
 */
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {

    RSemaphore park = redisson.getSemaphore("park");
    boolean b = park.tryAcquire();
    if (b) {
        //执行业务
    } else {
        return "error";
    }
    return "ok" + b;

}

@GetMapping("/go")
@ResponseBody
public String go() throws InterruptedException {
    RSemaphore park = redisson.getSemaphore("park");
    park.release();
    return "走了";

}
缓存-分布式锁-Redisson-信号量测试
/**
 * 信号量可以做分布式限流
 *
 * @return
 * @throws InterruptedException
 */
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {

    RSemaphore park = redisson.getSemaphore("park");
    boolean b = park.tryAcquire();
    if (b) {
        //执行业务
    } else {
        return "error";
    }
    return "ok" + b;

}

@GetMapping("/go")
@ResponseBody
public String go() throws InterruptedException {
    RSemaphore park = redisson.getSemaphore("park");
    park.release();
    return "走了";

}
缓存-分布式锁-Redisson-闭锁测试
@GetMapping("/lockdoor")
@ResponseBody
public String lockDoor() throws InterruptedException {

    RCountDownLatch door = redisson.getCountDownLatch("door");
    door.trySetCount(5);
    door.await();
    return "放假了";
}

@GetMapping("/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable int id){

    RCountDownLatch door = redisson.getCountDownLatch("door");
    door.countDown();
    return id+"号走了";
}
缓存-分布式锁-缓存一致性解决

写和写的并发问题

image-20201023183903766

写和读的并发问题

image-20201023184003496

image-20201023184931971

image-20201023195421828

image-20201023200030991

缓存-SpringCache-简介

https://spring.io/projects/spring-framework#learn

image-20201023200332697

image-20201023200950890

缓存管理器是市政府只是定义规则的,造出这些缓存组件,这些缓存组件才是真正帮我们crud的

image-20201023212904199

image-20201023212947799

image-20201023213131952

缓存-SpringCache-整合&体验@Cacheable

想用redis作为缓存还要引用spring-boot-starter-data-redis,已经引用过了

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

xml配置的属性是在这里封装着

image-20201023214741686

除了在容器内@Bean放了一堆组件,又拿选择器导入了好多缓存的配置,

image-20201023214952731

按照每一种缓存的类型在这里映射,

image-20201023215203544

image-20201023215335183

image-20201023215525850

缓存管理器在自动配的时候,会根据缓存配置的,所有配置的缓存的名字

image-20201023215805221

那相当于在配置文件中配了如果配了spring.cache.cacheNames,将把缓存的区域划分成哪些业务,配上了这些缓存的名字以后

image-20201023215902700

image-20201023220255403

初始化缓存

image-20201023221116635

把每一个缓存拿来遍历,把缓存配置和当前缓存名放在一起,配置都是用默认配置,再用默认配置初始化所有缓存

image-20201023221157457

image-20201023221304362

整个初始化逻辑又在下面,将所有缓存的配置拿来,在initialCaches里面一放相当于哪些缓存都是哪些规则,最终都是在这保存好的

image-20201023221333472

RedisCacheConfiguration在配置缓存规则的时候,比如有序列化机制,是jdk默认的序列化,ttl过期时间,都是从properties里面得到的,每一个缓存件有没有前缀,要不要缓存空数据,以及是不是使用缓存的前缀

image-20201023221525829

缓存使用redis作为缓存,为了好看,新创建一个配置文件application.properties

spring.cache.type=redis

如果配置了缓存名字,名字全部按照你配置的来写,如果没配,用到哪些缓存了,系统自动帮你创建出来,先不配,最简化配置

image-20201023222034068

image-20201023222455784

@EnableCaching

image-20201023223328080

/**
 * 每一个缓存的数据我们都来制定来放到哪个名字的缓存【缓存的分区(安装业务类型分)】
 *
 * 代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。
 * 如果缓存中没有,会调用方法,最后将方法的结果放入缓存
 * @return
 */

@Cacheable({"catagory"})
@Override
public List<CategoryEntity> getLevel1Categorys() {
    System.out.println("getLevel1Categorys......");
    long l = System.currentTimeMillis();
    return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
}

直接访问10000端口

第一次

image-20201023224452293

第二次,不会再调用这个方法了

image-20201023224714548

消耗时间是在这里定义的

image-20201023224914452

simplekey为自动生成的key

image-20201023224347302

缓存-SpringCache-@Cacheable细节设置
* 3、默认行为
*  1)、如果缓存中有,方法不用调用
*  2)、key默认自动生成,缓存名字::SimpleKey [](自动生成的key)
*  3)、缓存的value值,默认使用的是jdk的序列化机制,将序列化后的值存在redis中
*  4)、默认时间ttl=-1
*
*  自定义属性:
*      1)、指定生成的缓存使用的key:key属性指定,使用spel表达式
*          SPEL表达式:https://docs.spring.io/spring/docs/5.2.7.RELEASE/spring-framework-reference/integration.html#cache-spel-context
*      2)、指定缓存的数据的存活时间:配置文件中修改ttl,spring.cache.redis.time-to-live=3600000
*      3)、将数据保存为json格式(异构系统比如php可能不兼容)
因为spel动态取值,所有需要额外加''表示字符串
@Cacheable(value = {"catagory"},key = "'Level1Categorys'")
//一小时,这里单位是毫秒
spring.cache.redis.time-to-live=3600000

重启商品服务并访问10000端口 ttl 剩余多少秒

image-20201023231402884

@Cacheable(value = {"catagory"},key = "#root.method.name")

image-20201023232043211

缓存-SpringCache-自定义缓存配置

image-20201024125856460

在redis配置里面,如果是从人家默认的配置的,会从redcacheProperties中拿到redis配置的相关东西,给这里配置上,但是用自己的就不走下面这些步骤了

image-20201024122553799

spring.cache.type=redis

#spring.cache.cache-names=

spring.cache.redis.time-to-live=3600000
#如果使用前缀,就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀
spring.cache.redis.key-prefix=CACHE_
spring.cache.redis.use-key-prefix=true
# 是否缓存空值。防止缓存穿透
spring.cache.redis.cache-null-values=true
/开启属性配置绑定
@EnableConfigurationProperties({CacheProperties.class})
@EnableCaching
@Configuration
public class MyCacheConfig {


    /**
     * 配置文件中的东西没有用上
     *1、原来和配置文件绑定的配置类,是这样子的
     * @ConfigurationProperties(prefix = "spring.cache")
     * public class CacheProperties
     *2、要让他生效,要用这个注解,
     * @return
     */
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        config = config.serializeValuesWith
                (RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }
        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }
        return config;
    }
}

看看空值返不返回

image-20201024125035447

image-20201024125421555

缓存-SpringCache-@CacheEvict
/**
 * 级联数据的更新,
 * @CacheEvict:失效模式
 * @param category
 */
@CacheEvict(value = {"catagory"}, key="'getLevel1Categorys'")
@Transactional(rollbackFor = Exception.class)
@Override
public void updateCascade(CategoryEntity category) {
    this.updateById(category);
    categoryBrandRelationDao.updateCategory(category.getCatId(), category.getName());
    //同时修改缓存中的数据,
    //redis.del('catalogJSON'),等待下次查询时更新
}

满足修改菜单后,自动删除缓存,但是想像这样一个场景,按照常规,getCataLogJson也要删,所以改造一下方法

@Cacheable(value = {"catagory"},key = "#root.method.name")
@Override
public Map<String, List<Catalog2Vo>> getCatalogJson() {

    System.out.println(Thread.currentThread().getName() + "查询了数据库");
    List<CategoryEntity> selectList = baseMapper.selectList(null);

    List<CategoryEntity> category = getParent_cid(selectList, 0L);

    //2、封装数据
    Map<String, List<Catalog2Vo>> parent_cid = category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
        //1、查到这个一级分类下的所有二级分类
        List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());

        //2、封装上面的结果
        List<Catalog2Vo> catalog2Vos = new ArrayList<>();
        if (categoryEntities != null && categoryEntities.size() != 0) {
            catalog2Vos = categoryEntities.stream().map(l2 -> {
                Catalog2Vo catalog2Vo = new Catalog2Vo(
                        l2.getParentCid().toString(), null, l2.getCatId().toString(), l2.getName());
                //1、找当前二级分类的三级分类封装成vo
                List<CategoryEntity> level3Catalog = getParent_cid(selectList, l2.getCatId());
                List<Catalog2Vo.Catalog3Vo> collectlevel3 = new ArrayList<>();
                if (level3Catalog != null && !level3Catalog.isEmpty()) {
                    //2、封装成指定格式
                    collectlevel3 = level3Catalog.stream().map(l3 -> {
                        Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l3.getParentCid().toString(),
                                l3.getCatId().toString(), l3.getName());
                        return catalog3Vo;
                    }).collect(Collectors.toList());
                }
                catalog2Vo.setCatalog3List(collectlevel3);
                return catalog2Vo;
            }).collect(Collectors.toList());
        }
        return catalog2Vos;
    }));

    return parent_cid;

}

image-20201024141404304

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

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateCascade(CategoryEntity category) {xxxx}

或者

@CacheEvict(value = {"catagory"}, allEntries = true)

修改回来

spring.cache.type=redis

#spring.cache.cache-names=

spring.cache.redis.time-to-live=3600000
#如果使用前缀,就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀
#spring.cache.redis.key-prefix=CACHE_
spring.cache.redis.use-key-prefix=true
# 是否缓存空值。防止缓存穿透
spring.cache.redis.cache-null-values=true

image-20201024142553863

image-20201024142644536

缓存-SpringCache-原理与不足

image-20201024143631043

image-20201024144607577

image-20201024144742186

image-20201024144813015

主要给缓存的增删改查操作加上断点

image-20201024174925799

image-20201024174944611

image-20201024175050843

image-20201024175105241

image-20201024175138616

发送请求http://localhost:10000/

发现调用的是lookup方法,没有调用get方法,一直往下走,发现缓存命中失败

image-20201024175538262

image-20201024175701118

执行到目标方法(如果缓存没有命中,就去得到真正的数据,放行这个方法,就去执行真正的业务逻辑)。

image-20201024175955786

执行完毕后将目标方法里面的返回值,封装成缓存里面要放的值。

image-20201024180231485

接下来就会给缓存里卖弄放这些值。放行之后就会调用往缓存里面放数据的put方法。这块整个方法都是在缓存切面支持器

image-20201024180551071

放行之后,确实调了RedisCache的put方法,跟之前编写的业务逻辑代码都是一样的,但是从整个流程里面,没有发现任何加了锁的操作在。

因为任何地方都没有加锁,缓存击穿问题没法解决,要想解决,

1.不用SpringCache,自己手写那一堆之前的缓存代码,

2.加上sync

@Cacheable(value = {"catagory"}, key = "#root.method.name", sync = true)
@Override
public List<CategoryEntity> getLevel1Categorys() {
    xxxx
}

image-20201024183939679

这个配置是加的是本地锁,但是即使是本地锁,也足够了,就算有100个服务也就放100的请求进来,都可以不用分布式锁,

image-20201024185147609

清空缓存再进行测试,

直接进到加锁的get方法里面了

image-20201024192224432

第一个get就是调用lookup方法的,返回为空

image-20201024192310552

缓存中有,就直接返回,没有,从valueFromLoader里面读值,将读到的值,读到以后再放到缓存里面,和缓存的读模式的代码一模一样,

读取值的方式:这里参数传入一个Callable,相当于有返回结果的异步线程的方式,最终读到值

image-20201024192649727

这里就是放行来执行目标方法的

image-20201024192848613

image-20201024192948274

之前加了那个属性,是同步代码的话,就调用缓存的被锁定的同步方法,这里会掉cache.get 两个同步的方法,

image-20201024193241447

image-20201024193832539

用cacheWriter,将key转换,再将value转换,给里面存数据,但是我们发现,缓存的整个方法也是没有锁的,除了get方法,都没有锁,而且就是以前的模式

缓存中有,就返回,缓存中没有调用目标方法,查到以后,放到缓存,再返回,

image-20201024194154178

读模式考虑到了加锁,虽然只是本地锁也够用了,写模式,是根据自己的业务代码,不同情况要去不同执行了,写模式SpringCache没有管

  • 31
    点赞
  • 106
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值