之前整理了基础篇,Typora提示将近20000词,谷粒商城基础篇保姆级整理
在学高级篇的时候,不知不觉又整理了两万多词,做了一阶段,先发出来,剩余部分整理好了再发。自己也在学习的过程中,能力有限,如果有什么问题欢迎找我讨论。
索引类比database,类型类比表table,文档(json格式)类比表记录,属性类比列
举例,mysql中保存一个数据可能是正向索引,每条数据都有id存在这,在电影表中检索红海行动,用like,mysql匹配所有的记录,看每一条记录中是否是红海行动,这样非常慢。
es首先把红海行动拆成两个单词,红海,行动,es中保存1号文档,额外又维护一张倒排索引表,存了红海,和行动的单词,在一号记录里面有,所以如图所示
查询红海特工行动,会查到12345,五条记录,3号和5号都命中两个,但是3号3个单词命中两个,5号四个单词命中两个,根据相关性得分,从高到低排列,检索出数据还可以对数据进行复杂分析
把mysql数据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/
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
带*表示主节点
//访问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
_cat
http://192.168.218.128:9200/customer/external/1
{
"name":"John Doe"
}
{
//带_的都称为元数据,反应基本信息
"_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
}
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
//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
}
{
"doc":{
"name":"John",
"age":20
}
}
http://192.168.218.128:9200/customer/external/1
es中没有提供类型直接删除的操作
//两个为一行操作,每一条都是独立的,index是一个保存操作,上一条的失败不会影响下一条的记录的成功失败,不像mysql中的事务,一条失败全部回滚
POST /customer/external/_bulk
{
"index":{
"_id":"1"}}
{
"name":"tang"}
{
"index":{
"_id":"2"}}
{
"name":"yao"}
测试数据地址
POST bank/account/_bulk
文档地址https://www.elastic.co/guide/en/elasticsearch/reference/7.5/getting-started-search.html
//?检索条件,q=* 查询所有,sort=account_number:asc排序规则按照该字段升序排列
GET bank/_search?q=*&sort=account_number:asc
GET /bank/_search
{
"query": {
"match_all": {
}
},
"sort": [
{
"balance":"desc"
},
{
"account_number": "asc"
}
]
}
查询领域对象
##match 全文检索按照评分进行排序,会对检索条件进行分词匹配
GET /bank/_search
{
"query": {
"match": {
"account_number": "20"
}
}
}
GET /bank/_search
{
"query": {
"match": {
"address": "mill lane"
}
}
}
GET /bank/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"age": "40"
}
}
],
"must_not": [
{
"match": {
"state": "ID"
}
}
],
"should": [
{
"match": {
"lastname": "Ross"
}
}
]
}
}
}
filter和match或should区别是, 不会计算相关性得分,只起过滤作用
GET /bank/_search
{
"query": {
"bool": {
"must": {
"match_all": {
} },
"filter": {
"range": {
"balance": {
"gte": 20000,
"lte": 30000
}
}
}
}
}
}
两者区别,前者查询的内容可以包含789 Madison,后者是精确查询,
GET /_search
{
"query": {
"match_phrase": {
"address": "789 Madison"
}
}
}
GET /_search
{
"query": {
"match": {
"address.keyword": "789 Madison"
}
}
}
规定:非text字段,都用term,文本字段就用match
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mfTLUAP3-1603542203904)(https://tangyao.oss-cn-beijing.aliyuncs.com/image-20201001111708818.png)]
GET bank/_search
{
"query": {
"match": {
"address": "mill"
}
},
"aggs": {
"ageaggs": {
"terms": {
"field": "age",
"size": 10 //假设年龄有100种可能,只取出前10种可能
}
}
}
}
结果为
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,意思为不要任何分页记录,这里只看聚合结果
# 按照年龄聚合,并且请求这些年龄段的这些人的平均薪资
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"balanceAvg": {
"avg": {
"field": "balance"
}
}
}
}
}
}
# 查出所有的年龄分布,并且这些年龄段中性别为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
}
每个属性的映射类型,type为text默认就会就全文检索,检索起来就会分词,想要精确检索address的值,就要用address.keyword
PUT /my_index
{
"mappings": {
"properties": {
"age": {
"type": "integer" },
"email": {
"type": "keyword" },
"name": {
"type": "text" }
}
}
}
GET my_index/_mapping
PUT /my_index/_mapping
{
"properties": {
"employee-id": {
"type": "keyword",
"index": false // 设置不可以被索引
}
}
}
https://www.elastic.co/guide/en/elasticsearch/reference/7.4/docs-reindex.html
根据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"
}
}
}
}
想把老银行的数据迁移到新银行,因为老索引,分类型
POST _reindex
{
"source": {
"index": "bank",
"type": "account"
},
"dest": {
"index": "newbank"
}
}
GET newbank/_search
所有的type变成了默认的_doc
将完成的大段话分词,利用单词的相关性匹配,最终完成全文检索功能。默认使用标准分词器
https://www.elastic.co/guide/en/elasticsearch/reference/7.4/analysis-standard-analyzer.html
POST _analyze
{
"analyzer": "standard",
"text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}
但是默认是英文分词器,如果text为中文的话就会分割成一个个汉字,需要自己的分词器。
https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.4.2
# -it 交互模式 /bin/bash 进入控制台
docker exec -it 658 /bin/bash
复制链接地址https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
Vagrant创建的虚拟机默认是免密连接的,所以需要修改一下配置。
我用的是Ubuntu,先安装ssh
sudo apt-get install openssh-server
安装xshell和xftp并把下载好文件上传
解压到ik文件夹下
创建ik文件夹时,记得要修改文件夹权限否则不能讲zip文件通过 xftp拷贝过来
进入bin目录里面,都是可执行命令,运行这个plugin命令
先退出并重启elasticsearch
新增这三项
DNS帮忙解析域名都在哪里,再设置一个备用的DNS
重启网卡
Ubuntu的没有yum,下面是简单介绍,所以每安装yum
yum安装wget
yum再安装unzip等等,这里就略过了。我的Ubuntu里面都有。
指定一个远程 词库,让ik分词器自己向远程发送请求,要到最新的单词,最新的单词就会作为新的单词进行分解
1)自己写一个项目,处理这个请求,返回我们新的单词,让ik分词器给我们项目发送请求
2)安装nginx,将最新词库放到nginx里面,让ik分词器给nginx发送请求,由它(也是个web服务器)返回最新的词库,就能把新词库和原来的词库合并起来
ctrl+l清屏
free -m
如果可用比较小,就关闭虚拟机设置大一点
因为之前设置es的jvm堆内存比较小,最大只有128m
想要设置成512m,最快的方式停掉原来的创建一个新的。
这样做数据并不会丢失,因为之前创建的时候,将es中数据映射到外面的文件data下,即使删掉了容器,可是外面的文件夹还在,再创建新的容器和外面文件夹关联起来,数据也不会丢失。
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
// 在/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端口
// 在nginx文件夹下
cd /html
// 只要这个文件夹下有index.html,就会默认展示
vi index.html
//在html文件夹下,创建es,把es中ik分词器用到的资源放在这里
mkdir es
cd es
vi fenci.txt // 添加尚硅谷和乔碧萝两个词
nginx默认找资源都是在html下面找的,想要访问es下面的资源直接带上路径就可以了
乱码问题先不管,至此nginx就装好了
再配置ik分词器的远程词库地址,来到自定义词库,只需要修改ik分词器的配置
cd /mydata/elasticsearch/plugins/ik/config/
docker restart elasticsearch
分词成功
docker update elasticsearch --restart=always
有这样一个检索场景,当选中一些检索条件的时候,需要给es发送请求,来检索真正的商品,请求过来就需要给页面检索数据,这段请求应该由java程序接受,es进行处理,将处理的结果再返回给前端页面,java操作es有两种方式,
通过操作9300端口,它是一个tcp端口,es集群结点之间通信也都使用9300端口,如果使用9300端口操作es,就要与es建立一个长连接,支持这些操作在springdata项目里面,有transport-api对应es操作,包括官方elasticsearch.jar这些依赖也能支持这些操作,基于上面图片原因不使用9300端口,依然是通过9200操作es更简单,就是给es发送请求。
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
java api的官方提示
按照文档操作https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.4/index.html
引入依赖
<dependency>
<groupId>org.elasticsearch.clientgroupId>
<artifactId>elasticsearch-rest-high-level-clientartifactId>
<version>7.4.2version>
dependency>
发现不配套
原因是springboot项目依赖里面搜索es,发现springboot对es的版本也做了管理,当前Springboot默认整合springData,来操作es,他的版本是6.8.5
把版本改掉
<elasticsearch.version>7.4.2elasticsearch.version>
maven依赖已经改掉了
如何操作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.gulimallgroupId>
<artifactId>gulimall-commonartifactId>
<version>0.0.1-SNAPSHOTversion>
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
/**
* 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;
}
}
进行单元测试,
由于我用的springboot版本是2.2.7.RELEASE,juit的导入的是
import org.junit.jupiter.api.Test;
所以没出现任何问题,但是在这里记录一下
老师用的是2.1.x,首先都加public,
但是出现的是null,说明autowired注解都没解析成功,如果解析成功,要么解析不到,也不能是null.
增加@RunWith()指定Spring驱动来跑单元测试,这是以前兼容的单元测试,
又出现以下错误,没有指定数据源,
在common里面默认是有数据源的,只要有数据源就要配合数据源有关的配置,比如mysql的驱动,包括mybatis-plus的依赖,但是又不操作数据库,所以去掉
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallSearchApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallSearchApplication.class, args);
}
}
老师的项目测试通过了
首先了解第一个,请求设置项,以后要发所有请求,比如es添加了安全访问规则,
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.4/java-rest-high-getting-started-request-options.html
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回调函数一样,
/**
* 测试存储数据到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查看要查询的索引,
执行测试用例,这里的even指的是扁平的json数据。本来是有的,说明api调用的不对,没有指定内容类型,添加一个参数
request.source(jsonString,XContentType.JSON);
执行成功,再添加数据
user.setUserName("zhangsan");
user.setAge(22);
user.setGender("男");
再次测试。
更新新增二合一。
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);
}
es在项目中的使用:
1、作为全文检索引擎,承担所有项目里面的全文检索功能,京东手机首页,可以按照名字全文检索,也可以按照手机不同规格属性,进行全文检索,
2、承担日志的分析检索功能,可能需要对日志进行快速定位,日志也有检索需求,就可以将日志存储到es里面,有一个技术栈ELK logStash负责收集日志存到es里面
腾讯云截图,这部分会在运维部分提及。
检索需要给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对象的规格信息
设计存储方式。第二种虽然不冗余,但是存在一个极大的问题。
给定一个场景,检索手机的时候,每选中一个规格,比如选中了一个屏幕,5.49寸,再选中一个高清HD+,剩下又是一些可选规格
但是这些可选规格不断在变,比如又选了个安卓,它有一个最大特点,这些所列举的属性查询出来的商品是一定拥有的,所以上面的规格,动态计算出来的。这么计算出来的呢?
在检索手机的时候,找到所有标题里面包含手机的商品,会把所有的商品聚合起来,分析一下所有商品涉及的所有属性,以及所有的属性值,点进某一个属性值,就保证下面商品都会拥有它,这里是动态计算的。假设要完成这些动态计算,
10000个人搜索,集群中光数据传输,会有320mb数据,百万并发,就是32GB数据,别的不说,关阻塞时间就会非常长,所以说虽然第二种方案也是可以的,但是随着系统不断壮大,未来可能引申这样的问题。 所以一句话,空间与时间总是不能二者兼得,第一种浪费空间但是节省时间,第二种节省空间,分到了两个索引下,一定会造成一些时间的浪费,基于种种原因考虑,商品es存储的设计模型,就如方案一所示
## 一、商品上架
上架的商品才可以在网站展示。
上架的商品需要可以被检索。
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介绍https://www.elastic.co/guide/en/elasticsearch/reference/7.4/nested.html
数组类型的对象会被扁平化处理
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,应该没有一个人存在的,但是却检索出来了。
也就是说检索这个数组里面的user.first,确实包含alice,user.last确实包含smith,所以返回了这个数组,
解决方案
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": {
}
}
}
}
}
}
}
具体含义看文档。https://www.elastic.co/guide/en/elasticsearch/reference/7.4/nested.html
为上架的数据模型创建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> getSkuHasStock(@RequestBody List 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的调用,先判断是即将调用的方法是否是equals,hashcode,toString 这样的基本方法,如果不是就进入dispatch进入真正的调用,这里就是远程调用的功能。
进入了同步的方法处理器,用我们传过来的参数(SkuEsModel list类型数据)构造了一个RequestTemplate模板
内容已经用utf-8 编码成data 数据,用view as String 可以看见,用json编码成的数据,说明feign在底层将对象转换为json,因为在对象传的时候,包括配置远程接口的时候,本身就说是@RequestBody,feign会给我们编码成json数据,
接下来获取到retryer重试器,
$1是一个内部类,executeAndDecode 执行和解码,相当于远程执行我们请求,再将响应拿过来,进行解码,解码完成后返回Object对象,进来继续看
先来请求目标请求,用我们之前构造的template,里面有给哪里发请求,请求方式,用什么请求地址,包括整个数据json。通过这些来构造出这个请求,然后将这个请求发送出去,而且有日志的话会有日志记录,client.execute是真正的执行,
LoadBalancerFeignClient,是一个负载均衡的客户端,相当于会真正的负载均衡的去执行这个请求,改调哪个服务就调用哪个服务,执行请求就是发送post请求的过程,就可以不看了,放行,
一放行就来到了ElasticSearchSaveController,说明上一步放行,就会执行到远程接口,
准备好的数据已经收集过来了,已经逆转好了,得益于SpringMVC的@RequestBody,自动将json封装好为对象,发请求是得益于,SynchronousMethodHandler,会将数据编码成json发给我们。
直接抛异常
尝试五次
重试机制没有触发,默认是关闭状态
服务发送的第一个请求经常超时,服务的一些线程池,数据库连接池都还没有创建好,第一次请求还要初始化这些,
库存服务的Data里面没有set进去数据,
因为R是hashmap,所以写的所有私有属性都没用了,只能存key value了
可以将返回值改为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。
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);
}
前后分离项目会屏蔽很多细节,所以进行服务端的页面渲染式开发。
首先用户访问所有请求,全部先访问nginx,nginx作为反向代理,将数据全部转发给网关,网关再路由到各个服务,有网关的好处可以做限流认证鉴权等工作,而加上nginx,部署的时候,可以将每一个微服务自己里面的页面(页面可以写在微服务里面)引用的静态资源,把他们搬家,全部部署到nginx里面,这样就做到了部署期间的动静分离,静指的就是静态资源,让nginx返回,动指的是动态请求,所有要经过服务器要处理的这个业务动态请求,就称为动态资源,这样的好处就为了分担微服务的压力,要不然将静态资源也放在微服务里面,请求一个图片也都要访问微服务,微服务的tomcat都要建立连接处理再返回,tomcat本来并发度就不高,假设有三千的并发,结果2000个都是处理图片的,只有1000个是进行业务调用处理的这样就会让项目,没有支持高并发功能。
导入模板引擎
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
Springboot的static文件夹是放静态资源的,template放index.html
关闭缓存,这样就能看见实时效果,
新创建web包和将controller改为app
http://localhost:10000/可以直接访问到页面,原因是,
OrderedHiddenHttpMethodFilter来解决页面发送的请求,比如表单提交的时候提交put或者delete请求,将请求方式转换成对应的put或者delete,
默认访问webjars下的所有东西
欢迎页的映射规则,
当前路径下的所有请求,都可以去静态资源路径下去寻找,
静态资源路径在这里配置的
不用加前后缀
@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";
}
}
org.springframework.boot
spring-boot-devtools
true
ctrl+f9 就修改了,但是前提必须关闭thymeleaf的缓存
效果,一级分类已经出来,(二级和三级分类是写死的,和下面的显示问题)
json的数据模型,一共有21的1级分类,每一个一级分类下面有多个Object,每个对象包括一个catalog1Id,表示自己的一级节点属于哪个一级节点,
catalog3List,表示自己的三级分类有哪些,id表示当前的二级分类id,name表示当前二级分类名字。
修改发送请求,获取真实的json数据
@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现在安装在虚拟机里面,讲es的时候安装了nginx做了分词器,将分词器的内容放在了里面,远程请求nginx返回分词器的数据,nginx,在以后的项目中经常使用它的反向代理,和负载均衡
正向与方向相对于自己这台电脑来说,帮我们去上网的就是正向代理,帮助对方服务器了就是反向代理。
**正向代理:**我们想要访问谷歌,搭建一台代理服务器,为电脑配置上代理服务器的地址,电脑想访问任何网址,都由代理服务器帮我们去访问,访问拿到内容后帮我们返回,所以看到的是搭建的这台服务器是帮我们进行上网。
**反向代理:**搭建集群环境的时候非常需要,比如任何人去访问谷粒商城,谷粒商城有我们的后台服务集群,这些服务集群,每一个服务器,都可能都要在内网部署,这是一个内网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地址,
C:\Windows\System32\drivers\etc下面的hosts
也可以用这个软件
es和kibana都可以访问成功
以后每个系统都有对应的域名,都是访问虚拟机的ip地址,
直接访问gulimall.com,nginx正常启动
没有的话设置自动启动
sudo docker update nginx --restart=always
现在已经根据域名访问到nginx,接下来还要访问到项目,可以让nginx把所有的请求转给我们的网关,由网关再代转给每一个项目。当然也可以让nginx直接转给指定的项目,比如一看是访问的是gulimall.com,想要展示首页,相当于gulimall.com来到的所有请求,都给我转发到localhost:10000端口,也就是商品项目,拥有页面首页的内容,但是这样做不好,未来product项目部署多台的时候,有可能端口不一样,ip地址也不一样,每次都要修改nginx,让gulimall.com来的请求都转到10000端口。
我们先来这个最快的配置但是有局限性的配置-将gulimall请求直接转到10000端口
配置方式:
nginx的总配置有一个细节
nginx配置文件的内容
conf.d 里面的所有配置文件都会合并放进nginx.conf,拆开一个文件就不会很大,比较清晰,
查看默认配置
server name相当于域名配置的虚拟主机,监听这个域名下的东西,
这个域名下的所有请求,都可以在root,也就是根文件下找。
cp default.conf gulimall.conf
vi gulimall.conf
修改为,效果为,nginx是来监听gulimall这个域名下的,为什么能做到这个事?
发这个请求的时候,是从哪个域名下发的请求,请求头里面都有一个host地址是来源于这个地名,由于他的ip映射的是nginx,这个请求的信息交给nginx, 而上面server name的配置是gulimall,相当于nginx就会拿请求里面的host进行匹配,看是不是gulimall,就相当于网关在转发的时候,之前是根据前缀进行匹配,现在是也可以按照来源于哪个主机地址进行匹配,相当于nginx来监听来源于gulimall.com的主机地址,监听到以后,
esc 退出插入模式,dd删除一行,
跟虚拟机中间的网卡是56.1也可以访问到本机
实测这两个也可以 格式为:http://192.168.217.1:10000/
:set number 以行号显示 //可不输这条命令
proxy_pass http://192.168.8.229:10000; //nginx每一个配置一定以;结尾
docker restart nginx
可以访问到了,但是应该让nginx代理给网关,来解决分布式的问题
https://nginx.org/en/docs/http/load_balancing.html
监听上游服务器
监听gulimall.com的所有请求,直接代理给网关,网关整个上游服务器的名字就叫gulimall,会动态找到上游服务器组然后动态的转出去,相当于负载均衡的配置。
重启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
- id: gulimall_host_root
uri: lb://gulimall-product
predicates:
- Host=**.gulimall.com
# ** 代表子域名
nginx确实路由到网关了,映射api路径都行,但是直接访问gulimall.com却不行,
原因是nginx在代理给网关的时候会丢失host信息,设置请求头的host信息,只有给gulimall转发的时候配置加上head,相当于路由到网关会加头,其他没设置的路径默认都不加
注意gateway配置的时候一定要写在最后面,网关一进来,就算是发的api请求,由于优先匹配到域名,所以直接路由给商品服务,就会去商品服务里面找api全路径,商品服务真正的是要把api商品服务截串的,相当于就把下面的配置禁用掉了,导致没有截串 所有的api请求,发生404
加大服务占用内存测试
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 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
-Dcom.sun.management.jmxremote
启动jvisualvm
如果开启金山词霸的话,会出现这样的错误 process finished with exit code 0xc0000005
建议关闭 https://blog.csdn.net/m0_37616916/article/details/88358195
测试nginx,nginx监听虚拟机的80端口,之前装nginx的时候在root目录下放了一个首页,所以给nginx端口发送请求,默认返回首页,
docker stats 查看cpu使用率内存等等
进行压力测试,发现,比较浪费cpu内存占用很低, 因为他主要需要更多的线程去处理请求,cpu要来回在线程之间切换计算,
开始压网关
网关比较浪费cpu,网关功能和nginx基本是差不多的
如果eden space的大小改变,gc时间减少,就又会将吞吐量提升,
查看简单服务写一个简单服务,没有页面渲染,也不操作数据库。
@ResponseBody
@GetMapping("/hello")
public String hello(){
return "hello";
}
现在想看Gateway加简单服务的压测,gateway除了映射/api/product 以外,还来映射/hello请求,因为不是api请求,也不用截串
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**,/hello
filters:
- RewritePath=/api/(?/?.*), /$\{segment}
压测全链路
加粗字体为单压
由于压力测试和真正服务在同一台机器,同时来压的话会线程竞争,所以真正的压力测试不应该是机器部署在服务器之后真正要承受的压力,用另外一台机器来压就是相对标准的数据,
压测内容 | 压测线程数 | 吞吐量/s | 90%响应时间 | 99%响应时间 |
---|---|---|---|---|
Nginx | 50 | 9501 | 3 | 149 |
Gateway | 50 | 24366 | 3 | 6 |
简单服务 | 50 | 40574 | 2 | 6 |
首页一级菜单渲染 | 50 | 1294(db,thymeleaf) | 63 | 114 |
首页渲染(开缓存) | 50 | 1997 | 30 | 58 |
首页渲染(开缓存,优化数据库,关日志) | 50 | 2617 | 23 | 37 |
三级分类数据获取 | 50 | 22(db)/31(开缓存,优化数据库关日志后) | 2355 | 2848 |
三级分类(优化业务) | 50 | 316 | 269 | 435 |
三级分类(使用redis作为缓存) | 50 | 1942 | 34 | 52 |
首页全部数据获取 | 50 | 34(静态资源) | ||
Nginx+Gateway | 50 | |||
Gateway+简单服务 | 50 | 10313 | 9 | 20 |
全链路 | 50 | 2129 | 9 | 20 |
结论:中间件越多,性能损失越大,大多都损失到网络交互了;
业务:db
模板的渲染速度(cpu 内存,最重要缓存),
静态资源(tomcat还要分一些线程来处理静态资源,吞吐量下降很多)
压测首页的时候,响应得数据是页面模板,但是页面模板引了非常多的图片,这些图片实际上还是要渲染的,相当于要给服务器发请求,再拿过来。所以一个完整的请求,应该是整个页面,给我们返回来的这个请求,所以在压测的时候压测整个页面的这个返回,
首页全部数据获取
首页渲染(开缓存,优化数据库,关日志)
记录建索引前的消耗时间
建立索引
mkdir /mydata/nginx/html/static
chmod 777 static
将index静态资源复制过来
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路径下去找
修改gulimall.conf
location /static/ {
root /usr/share/nginx/html;
}
static的所有都去哪里找,都去root对应后面的目录去找,因为整个路径是完整的,不仅有static ,还有static文件夹,接下来有什么,就按照层级目录在文件夹下写了什么。除了static外,剩下的转给gulimall(网关的整个集群,而且以负载均衡的方式)
现在首页的静态资源,全部都由nginx返回,首页的数据,全部都是由tomcat返回,这就是nginx的动静分离配置
最起码现在的tomcat只处理动态请求,占用的资源就会很小了。
我用1000个线程来压
访问首页已经不能提供服务了,提示找不到实例了,这就是线上实例的整个过程,持续在运行期间,cpu,内存爆满卡死,将应用挤下线,
修改位置
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().eq(
// "parent_cid", v.getCatId()));
}
再将内存改回去 -xms100m
压测 http://localhost:10000/index/catalog.json 吞吐量现在是316
性能提升大神器缓存要来了!!!!
**即时性:**物流状态信息适合放入缓存,
可以使用map来做本地缓存,但是会有一些问题,
本地缓存:缓存的组件名字假设为cache,和我们这个代码属于同一个进程,他们运行在同一个项目里面,在同一个jvm里面,只相当于在本地保存一个副本。
如果这个应用是单体应用,永远只部署在一台机器上,什么问题都没有,而且很快,
1.分布式下缓存是分开的,各顾各的,当负载均衡到不同微服务时,只要没有缓存都要重新查一份,
2.如果数据修改,假如三级分类数据修改,为了能读取到正确的数据,一般性还要改一下缓存里面的数据,假设第一次修改请求来到了一号服务器,修改分类数据并修改缓存,之前二,三号服务器里面缓存没法改,因为负载均衡是在一号的,所以以后所有请求,负载均衡到二号三号拿到的数据和一号拿到的数据是不一样的,这就产生了数据一致性问题。
解决方式:在分布式情况下,不应该使用本地缓存,共享一个集中式的缓存中间件
https://docs.spring.io/spring-boot/docs/2.2.10.RELEASE/reference/html/using-spring-boot.html#using-boot-starter
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
接下来就要配置redis
泛型是String,String,key使用String类型的序列化机制来做的,value也是String类型的序列化做的,
@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是直接操作堆外内存的
用jvisualvm监控也没问题
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<exclusions>
<exclusion>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
netty底层自己在计数,数字超过默认的容量限制,就会抛异常,这个就是堆外溢出异常。netty统计内存使用量,操作完了就会减内存使用量,一定是lettcure客户端,在哪一块操作的时候,没有及时调用掉减内存,导致堆外内存溢出,除了升级就是更换
无论这两个谁连接都会放一个连接工厂,就是连接要用的RedisConnectFactory
第一种解决方式在单体引用下,一个tomcat一台服务器,这样锁没问题,缺点: 在分布式情况下,锁不住所有服务
//从数据库查询并封装分类数据
public Map> 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> result = JSON.parseObject(catalogJSON, new TypeReference
100线程进行压力测试,结果发发现查询了两次数据库,
过程分析:100万并发进来,看我们的缓存,大家都进来看缓存,缓存没有,假设都走到StringUtils.isEmpty(catalogJSON),缓存中没有,都打印缓存没命中,准备去查数据库,查数据库的时候,上来就锁了一把锁,只有一个线程进来了查数据库,只要线程查完数据库,就释放锁,锁住的其他线程,进进来了。2号进来,就先确认缓存有没有,1号线程释放锁以后,要给redis里面放数据,单给redis放数据是一次网络交互,可能很慢,包括刚启动起来,还要给redis建立连接,还要整线程池一堆操作,线程池等等都还没有初始化,第一次来做是一次很慢的过程,所以,就导致,1号线程还没有把数据放进去,2号线程从redis中获取,确实没有缓存,就又查了一遍数据库,
改造方法
将放入缓存的步骤放在同步代码块的下,保证查完数据库立刻将结果放入缓存。是一个原子操作,在同一把锁内进行的。否则就会导致释放锁的时序问题,查了两边数据库
copyConfiguration
几个服务都都跑起来,压测直接从nginx到网关再负载均衡到各个服务,删掉缓存中数据
发现每一个服务都查询了一次数据库,本地锁的this只能锁住当前服务,其他人进入其他服务,都会进行一次查询
打开多个客户端
docker exec -it redis redis-cli
set lock haha nx
124客户端都为nil
3为ok
del lock
//必须保证获取到锁和设置过期时间是一个原子操作
set lock 111 EX 300 NX
ttl lock
核心:加锁保证原子性,解锁保证原子性
redis设置分布式锁文档
http://www.redis.cn/commands/set.html
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 script, List 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> getDataFromDB() {
//得到锁以后应该再去缓存中确定一次,如果没有才需要继续查询
String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isNotEmpty(catalogJSON)) {
//缓存不为null,直接返回,
Map> result = JSON.parseObject(catalogJSON, new TypeReference
private Map> getDataFromDB() {
//得到锁以后应该再去缓存中确定一次,如果没有才需要继续查询
String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isNotEmpty(catalogJSON)) {
//缓存不为null,直接返回,
Map> result = JSON.parseObject(catalogJSON, new TypeReference
private Map> getDataFromDB() {
//得到锁以后应该再去缓存中确定一次,如果没有才需要继续查询
String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isNotEmpty(catalogJSON)) {
//缓存不为null,直接返回,
Map> result = JSON.parseObject(catalogJSON, new TypeReference
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
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
@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);
}
config.useSingleServer().setAddress("redis://192.168.218.128:6379");
打印结果
org.redisson.Redisson@210c1b9d
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为线程号
跑两个商品服务10000端口和10002,分别发请求,然后中断10000端口,没有手动释放锁,看看redisson会不会死锁
手动没有解锁,也会为我解锁
不断获取锁,只要能获取锁就继续执行我们的业务
@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";
}
}
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 "走了";
}
/**
* 信号量可以做分布式限流
*
* @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 "走了";
}
@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+"号走了";
}
写和写的并发问题
写和读的并发问题
https://spring.io/projects/spring-framework#learn
缓存管理器是市政府只是定义规则的,造出这些缓存组件,这些缓存组件才是真正帮我们crud的
想用redis作为缓存还要引用spring-boot-starter-data-redis,已经引用过了
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
xml配置的属性是在这里封装着
除了在容器内@Bean放了一堆组件,又拿选择器导入了好多缓存的配置,
按照每一种缓存的类型在这里映射,
缓存管理器在自动配的时候,会根据缓存配置的,所有配置的缓存的名字
那相当于在配置文件中配了如果配了spring.cache.cacheNames,将把缓存的区域划分成哪些业务,配上了这些缓存的名字以后
初始化缓存
把每一个缓存拿来遍历,把缓存配置和当前缓存名放在一起,配置都是用默认配置,再用默认配置初始化所有缓存
整个初始化逻辑又在下面,将所有缓存的配置拿来,在initialCaches里面一放相当于哪些缓存都是哪些规则,最终都是在这保存好的
RedisCacheConfiguration在配置缓存规则的时候,比如有序列化机制,是jdk默认的序列化,ttl过期时间,都是从properties里面得到的,每一个缓存件有没有前缀,要不要缓存空数据,以及是不是使用缓存的前缀
缓存使用redis作为缓存,为了好看,新创建一个配置文件application.properties
spring.cache.type=redis
如果配置了缓存名字,名字全部按照你配置的来写,如果没配,用到哪些缓存了,系统自动帮你创建出来,先不配,最简化配置
@EnableCaching
/**
* 每一个缓存的数据我们都来制定来放到哪个名字的缓存【缓存的分区(安装业务类型分)】
*
* 代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。
* 如果缓存中没有,会调用方法,最后将方法的结果放入缓存
* @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端口
第一次
第二次,不会再调用这个方法了
消耗时间是在这里定义的
simplekey为自动生成的key
* 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 剩余多少秒
@Cacheable(value = {"catagory"},key = "#root.method.name")
在redis配置里面,如果是从人家默认的配置的,会从redcacheProperties中拿到redis配置的相关东西,给这里配置上,但是用自己的就不走下面这些步骤了
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;
}
}
看看空值返不返回
/**
* 级联数据的更新,
* @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;
}
@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
主要给缓存的增删改查操作加上断点
发送请求http://localhost:10000/
发现调用的是lookup方法,没有调用get方法,一直往下走,发现缓存命中失败
执行到目标方法(如果缓存没有命中,就去得到真正的数据,放行这个方法,就去执行真正的业务逻辑)。
执行完毕后将目标方法里面的返回值,封装成缓存里面要放的值。
接下来就会给缓存里卖弄放这些值。放行之后就会调用往缓存里面放数据的put方法。这块整个方法都是在缓存切面支持器
放行之后,确实调了RedisCache的put方法,跟之前编写的业务逻辑代码都是一样的,但是从整个流程里面,没有发现任何加了锁的操作在。
因为任何地方都没有加锁,缓存击穿问题没法解决,要想解决,
1.不用SpringCache,自己手写那一堆之前的缓存代码,
2.加上sync
@Cacheable(value = {
"catagory"}, key = "#root.method.name", sync = true)
@Override
public List<CategoryEntity> getLevel1Categorys() {
xxxx
}
这个配置是加的是本地锁,但是即使是本地锁,也足够了,就算有100个服务也就放100的请求进来,都可以不用分布式锁,
清空缓存再进行测试,
直接进到加锁的get方法里面了
第一个get就是调用lookup方法的,返回为空
缓存中有,就直接返回,没有,从valueFromLoader里面读值,将读到的值,读到以后再放到缓存里面,和缓存的读模式的代码一模一样,
读取值的方式:这里参数传入一个Callable,相当于有返回结果的异步线程的方式,最终读到值
这里就是放行来执行目标方法的
之前加了那个属性,是同步代码的话,就调用缓存的被锁定的同步方法,这里会掉cache.get 两个同步的方法,
用cacheWriter,将key转换,再将value转换,给里面存数据,但是我们发现,缓存的整个方法也是没有锁的,除了get方法,都没有锁,而且就是以前的模式
缓存中有,就返回,缓存中没有调用目标方法,查到以后,放到缓存,再返回,
读模式考虑到了加锁,虽然只是本地锁也够用了,写模式,是根据自己的业务代码,不同情况要去不同执行了,写模式SpringCache没有管