typora-root-url: image typora-copy-images-to: image
谷粒商城高级篇
笔记中仍有很多不足,如有错误还请包涵(●ˇ?ˇ●)
1、Elasticsearch - 全文检索
简介
https://www.elastic.co/cn/what-is/elasticsearch/
全文搜索属于最常见的需求,开源的 Elasticsearch 是目前全文搜索引擎的首选。
他可以快速地存储、搜索和分析海量数据。维基百科、Stack Overflow、Github 都采用他
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Oxl2wiN2-1650621164738)(image-20201026084916698.png)]
Elatic 的底层是开源库吧Lucene。但是,你没法直接用,必须自己写代码调用它的接口,Elastic 是 Lunce 的封装,提供了 REST API 的操作接口,开箱即用
REST API:天然的跨平台
官网文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
官网中文:https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html
社区中文:
http://doc.codingdict.com/elasticsearch/
1.1、基本概念
1.1.1 index(索引)
动词,相当于 MySQL 中的 insert;
名词,相当于MySQL 中的 DataBase
1.1.2 Type(类型)
在 Index(索引)中,可以定义一个或多个类型
类似于 MySQL 中的 Table,每一种类型的数据放在一起
1.1.3 Document(文档)
保存在某个索引(index)下,某种类型(Type)的一个数据(Document),文档是 JSON 格式的,Document 就像是 MySQL 中某个 Table 里面的内容
1.1.4 倒排索引机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SFvcNqNZ-1650621164739)(image-20201026092738311.png)]
1.2 Docker 安装 ES
1.2.1 下载镜像
docker pull elasticsearch:7.4.2 存储和检索数据
docker pull kibana:7.4.2 可视化检索数据
1.2.2 创建实例
1、ElasticSearch
配置
mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data
echo "http.host: 0.0.0.0" >/mydata/elasticsearch/config/elasticsearch.yml
chmod -R 777 /mydata/elasticsearch/
启动
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
开机启动 elasticsearch
docker update elasticsearch --restart=always
以后再外面装好插件重启就可
特别注意:
-e ES_JAVA_OPTS=“-Xms64m -Xmx128m” \ 测试环境下,设置 ES 的初始内存和最大内存,否则导致过大启动不了ES
2、Kibana
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.56.10:9200 -p 5601:5601 -d kibana:7.4.2
http://192.168.56.10:9200 改成自己Elasticsearch上的地址
3、安装nginx
随便启动一个 nginx 实例,只是为了复制出配置
docker run -p80:80 --name nginx -d nginx:1.10
将容器内的配置文件拷贝到当前目录 (注意后面有个小点
)
docker container cp nginx:/etc/nginx .
创建nginx文件夹
mkdir -p /mydata/nginx/html
mkdir -p /mydata/nginx/logs
mv /mydata/nginx/conf/nginx/* /mydata/nginx/conf/
rm -rf /mydata/nginx/conf/nginx
别忘了后面的点
修改文件名称:mv nginx.conf 把这个conf 移动到 /mydata/nginx 下
终止原容器, docker stop nginx
执行命令删除容器:docker rm $Containerid
创建新的 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
1.3 初步检索
1.3.1、_cat
GET /_cat/nodes:查看所有节点
GET /_cat/health:查看 es 健康状况
GET /_cat/master:查看主节点
GET /_cat/incices:查看所有索引 show databases;
1.3.2 索引一个文档(保存)
保存一个数据,保存在哪个索引的哪个类型下,指定用哪个唯一标识
PUT customer/external/1; 在 customer 索引下的 external 类型下保存 1号数据为
PUT customer/external/1
1.3.3、查询文档
GET custome/external/1
结果:
{
"_index": "customer",
"_type": "external",
"_id": "1",
"_version": 1, 。
"_seq_no": 0,
"_primary_term": 1,
"found": true,
"_source": {
"name": "John Doe"
}
}
更新携带:?if_seq_no=4&if_primary_term=1
1.3.4 更新文档
POST customer/external/1/_update
{
"doc":{
"name":"John Doew"
}
}
或者
POST customer/external/1
{
"name":"John Doe2"
}
或者
PUT customer/external/1
{
"name":"jack"
}
不同:Post操作会对比源文档数据,如果相同不会有什么操作,文档 version 不增加 PUT操作总会将数据重新保存并增加 version 版本
带 _update 对比元数据如果一样就不进行任何操作
看场景
对于大并发更新,不带update
对于大并发查询并偶尔更新,带update 对比更新,重新计算分配规则
更新同时增加属性
POS customer/external/1/_update
{
"doc":{"name":"Jane Doe","age":20}
}
PUT 和 POST 不带_update也可以
1.3.5 删除文档&索引
DELETE customer/external/1
DELETE customer
1.3.6 bulk 批量 API
POST customer/external/_bulk
{"index":{"_id":"1"}}
{"name":"John Doe"}
{"index":{"_id":"2"}}
{"name":"John Doe"}
语法格式
{action:{metadata}}\n
{requeestBody}\n
{action:{metadata}}\n
{requesetbod }\n
复杂实例:
POST /_bulk
{"delete":{"_index":"website","_type":"blog","_id":"123"}}
{"create":{"_index":"website","_type":"blog","_id":"123"}}
{"title":"my first blog post"}
{"index":{"_index":"website","_type":"blog"}}
{"title":"my second blog post"}
{"update":{"_index":"website","_type":"blog","_id":"123"}}
{"doc":{"title":"my updated blog post"}}
bulk API以此按顺序执行所有的action (动作)。如果一一个单个的动作因任何原因而失败,它将继续处理它后面剩余的动作。当bulkAPI 返回时,它将提供每个动作的状态(与发送的顺序相同),所以您可以检查是否一个指定的动作是不是失败了。
1.3.7 样本测试数据
我准备了一份顾客银行账户信息虚构的 JSON 文档样本,每个用户都有下列的 schema (模式):
{
"account_number": 1,
"balance": 39225,
"firstname": "Amber",
"lastname": "Duke",
"age": 32,
"gender": "M",
"address": "880 Holmes Lane",
"employer": "Pyrami",
"email": "amberduke@pyrami.com",
"city": "Brogan",
"state": "IL"
}
https://github.com/elastic/elasticsearch/edit/master/docs/src/test/resources/accounts.json
导入测试数据
POST bank/account/_bank
测试数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UUWUfTkn-1650621164739)(/image-20201026114903942.png)]
1.4 进阶检索
1.4.1 SearchAPI
ES 支持两种基本方式检索:
- 一个是通过使用 REST request URL,发送搜索参数,(uri + 检索参数)
- 另一个是通过使用 REST request bod 来发送他们,(uri + 请求体)
1、检索信息
一切检索从_search开始
GET /bank/_search 检索 bank 下的所有信息,包括 type 和 docs
GET /bank/_search?q=*&sort=account_number:asc 请求参数方式检索
响应结果解释 took - Elasticearch执行搜索的时间(毫秒)
time_ out - 告诉我们搜索是否超时
_shards - 告诉我们多少个分片被搜索了,以及统计了成功/失败的搜索分片 hit - 搜索结果 hits.total - 搜索结果 hits.hits - 实际的搜索结果数组(默认为前10的文档) sort - 结果的排序key (键) (没有则按 score 排序) score 和 max score - 相关性得分和最高得分(全文检索用)
uri + 请求体进行检查
GET /bank/_search
{
"query": { "match_all": {} },
"sort": [
{ "account_number": "asc" }
]
}
HTTP 客户端工具(POSTMAN),get请求不能携带请求体,我们变为 post也是一样的 我们 POST 一个 JSON风格的查询请求体到 _search API
需要了解,一旦搜索结果被返回,ES 就完成了这次请求的搜索,并且不会维护任何服务端的资源或者结果的 cursor(游标)
1.4.2、QueryDSL
1、基本语法格式
ES 提供了一个可以执行查询的 Json 风格的 DSL (domain-specifig langurage 领域特定语言),这个被成为 Query DSL ,该查询语言非常全面,并且刚开始的时候优点复杂,真正学好对他的方法是从一些基础的示例开始的
一个查询语句 的典型结构
{
QUERY_NAME:{
ARGUMENT:VALUE,
ARGUMENT:VALUE.....
}
}
如果针对某个字段,那么它的结构如下
{
QUERY_NAME:{
FIELD_NAME:{
ARGUMENT:VALUE,
ARGUMENT:VALUE.....
}
}
}
GET /bank/_search
{
"query": { "match_all": {} },
"sort": [
{ "account_number": "asc" }
],
"from": 10,
"size": 10
}
query 定义如何查询
match_all 查询类型【代表查询所有的所有】,es中可以在 query中 组合非常多的查询类型完成复杂查询
除了 query 参数之外,我们也可以传递其他的参数改变查询结构,如 sort,size
from + size 限定,完成分页功能
sort排序,多字段排序,会在前序字段相等时后续字段内部排序,否则以前序为准
2、返回部分字段
GET bank/_search
{
"query":{
"match_all": {}
},
"sort": [
{
"balance": {
"order": "desc"
}
}
],
"from": 5,
"size": 5,
"_source": ["firstname","lastname"]
}
3、match【匹配查询】
基本类型(非字符串),精准匹配
GET bank/_search
{
"query":{
"match": {
"address": "mill lane"
}
}
}
全文检索按照评分进行排序,会对检索条件进行分词匹配
4、match_phrase【短语匹配】
将需要匹配的值当成一个整体单词(不分词)进行检索
GET /bank/_search
{
"query": { "match_phrase": { "address": "mill lane" } }
}
5、multi_match【多字段匹配】
GET bank/_search
{
"query":{
"multi_match": {
"query": "mill",
"fields": ["address","city"]
}
}
}
6、bool 【复合查询】
bool 用来做复合查询
复合语句可以合并 任何 其他嵌套语句,包括复合语句,了解这一点是很重要的,这就意味着,复合语句之间可以互相嵌套,可以表达式非常复杂的逻辑
GET /bank/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"gender": "M"
}
},
{
"match": {
"address": "mill"
}
}
],
"must_not": [
{"match":{
"age":"18"
}}
],
"should": [
{"match": {
"lastname": "Wallace"
}}
]
}
}
}
- should:应该达到 should 列举的条件,如果达到会增加相关文档的评分,并不会改变查询的结果,如果 query 中只有 should 且只有一种匹配规则,那么 should的条件就会被作为默认匹配条件而区改变查询结果
"should": [
{"match": {
"lastname": "Wallace"
}}
]
"must_not": [
{"match":{
"age":"18"
}}
],
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oqZJyaIy-1650621164739)(/image-20201026052225903.png)]
7、filter【结果过滤】
并不是所有的查询都需要产生分数,特别是那些仅仅用于 filtering(过滤)的文档,为了不计算分数 ES 会自动检查场景并且优化查询的执行
GET /bank/_search
{
"query": {
"bool": {
"must": { "match_all": {} },
"filter": {
"range": {
"balance": {
"gte": 20000,
"lte": 30000
}
}
}
}
}
}
8、term
和 match 一样,匹配某个属性的值,全文检索字段用 match,其他非text字段匹配用 term
GET bank/_search
{
"query":{
"match_phrase": {
"address": "789 Madison Street"
}
}
}
9、aggregations(执行聚合)
聚合提供了从数据分组和提取数据的能力,最简单的聚合方法大致等于 SQL GROUP BY 和 SQL 聚合函数,在 ES 中,你有执行搜索返回 hits (命中结果) 并且同时返回聚合结果,把一个响应中的所有 hits(命中结果)分隔开的能力,这是非常强大有效的,你可以执行查询和多个聚合,并且在一个使用中得到各自的(任何一个的)返回结果,使用一次简洁简化的 API 来避免网络往返
搜索address中包含mill的所有人的年龄分布以及平均年龄
GET bank/_search
{
"query": {
"match": {
"address": "mill"
}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 10
}
},
"ageAvg":{
"avg": {
"field": "age"
}
},
"balanceAvg":{
"avg": {
"field": "balance"
}
}
},
"size":0
}
按照年龄聚合,并且请求这些年龄段的这些人的平均薪资
GET bank/_search
{
"query":{
"match_all": {}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 10
},
"aggs": {
"ageAvg": {
"avg": {
"field": "balance"
}
}
}
}
}
}
查出所有年龄分布,并且这些年龄段中M的平均薪资和 F 的平均薪资以及这个年龄段的总体平均薪资
GET /bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"genderAgg": {
"terms": {
"field": "gender.keyword",
"size": 10
},
"aggs": {
"balanceAvg": {
"avg": {
"field": "balance"
}
}
}
}
}
}
}
}
10、nested
嵌套查询
数据类型概览
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R1UhfHok-1650621164740)(/image-20201225063939652.png)]
参考博客:https://elastic.blog.csdn.net/article/details/82950393
1.4.3 Mapping
1、字段类型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aK4iH2du-1650621164740)(/image-20201026074813810.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JInEzeiH-1650621164740)(/image-20201026074841875.png)]
2、映射
Mapping(映射)
Mapping 是用来定义一个文档(document),以及他所包含的属性(field)是如何存储索引的,比如使用 mapping来定义的:
- 哪些字符串属性应该被看做全文本属性(full text fields)
- 那些属性包含数字,日期或者地理位置
- 文档中的所有属性是能被索引(_all 配置)
- 日期的格式
- 自定义映射规则来执行动态添加属性
查看 mapping 信息
GET bank/_mapping
修改 mapping 信息
https://www.elastic.co/guide/en/elasticsearch/reference/7.10/mapping-types.html
自动猜测的映射类型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CrWUPKK2-1650621164740)(/image-20201026075424198.png)]
3、新版本改变
-
关系型数据库中两个数据表示是独立的,即使他们里面有相同名称的列也不影响使用,但ES中不是这样的。elasticsearch 是基于Lucene开发的搜索引擎,而ES中不同type下名称相同的filed 最终在Lucene,中的处理方式是一样的。 -
两个不同 type下的两个user_ name, 在ES同-个索引下其实被认为是同一一个filed,你必须在两个不同的type中定义相同的filed映射。否则,不同typpe中的相同字段称就会在处理中出现神突的情况,导致Lucene处理效率下降。 -
去掉type就是为了提高ES处理数据的效率。
ES 7.x
URL 中的 type 参数 可选,比如索引一个文档不再要求提供文档类型
ES 8.X
不在支持 URL 中的 type 参数
解决:
1、将索引从多类型迁移到单类型,每种类型文档一个独立的索引
2、将已存在的索引下的类型数据,全部迁移到指定位置即可,详见数据迁移
1、创建映射
PUT /my_index
{
"mappings":{
"properties": {
"age":{"type":"integer"},
"email":{"type":"keyword"}
}
}
}
2、添加新的字段映射
PUT /my_index/_mapping
{
"properties":{
"employeeid":{
"type":"keyword",
"index":false
}
}
}
3、更新映射
对于已经存在的映射字段,我们不能更新,更新必须创建新的索引进行数据迁移
4、数据迁移
先创 new_twitter 的正确映射,然乎使用如下方式进行数据迁移
POST _reindex [固定写法]
{
"source":{
"index":"twitter"
},
"dest":{
"index":"new_twitter"
}
}
## 将旧索引的 type 下的数据进行迁移
POST _reindex
{
"source": {
"index":"twitter",
"type":"tweet"
},
"dest":{
"index":"twweets"
}
}
参考官网:https://www.elastic.co/guide/en/elasticsearch/reference/7.10/mapping-types.html
参数映射规则:https://www.elastic.co/guide/en/elasticsearch/reference/7.10/mapping-params.html#mapping-params
1.4.4 分词
一个 tokenizer(分词器)接收一个字符流,将之分割为独立的 tokens(词元,通常是独立的单词),然后输出 token 流
列如,witespace tokenizer 遇到的空白字符时分割文本,它会将文本 “Quick brown fox” 分割为 【Quick brown fox】
该 tokenizer (分词器)还负责记录各个term (词条)的顺序或 position 位置(用于phrase短语和word proximity词近邻查询),以及
term (词条)所代表的原始 word (单词)的start(起始)和end (结束)的 character offsets (字符偏移量) (用于 高亮显示搜索的内容)。
Elasticsearch 提供了很多内置的分词器,可以用来构建custom analyzers(自定义分词器)
1、安装 ik 分词器
注意:不能用默认的 elasticsearch-plugin.install xxx.zip 进行自动安装
https://github.com/medcl/elasticsearch-analysis-ik/releases 下载与 es对应的版本
安装后拷贝到 plugins 目录下
2、测试
分词器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-53WupiTO-1650621164741)(/image-20201026092255250.png)]
3、自定义词库
修改 /usr/share/elasticsearch/plugins/ik/config/中的 IKAnalyzer.cfg.xml
/usr/share/elasticsearch/plugins/ik/config
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict"></entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<entry key="remote_ext_dict">http://192.168.56.10/es/fenci.txt</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
1.5 Elasticsearch - Rest - client
1、9300:TCP
Spring-data-elasticsearch:transport-api.jar
SpringBoot版本不同,transport-api.jar 不同,不能适配 es 版本
7.x 已经不在适合使用,8 以后就要废弃
2、9200:HTTP
JestClient 非官方,更新慢
RestTemplate:默认发送 HTTP 请求,ES很多操作都需要自己封装、麻烦
HttpClient:同上
Elasticsearch - Rest - Client:官方RestClient,封装了 ES 操作,API层次分明
最终选择 Elasticsearch - Rest - Client (elasticsearch - rest - high - level - client)
1.5.1 SpringBoot 整合
1、Pom.xml
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>
为什么要导入这个?这个配置那里来的?
官网:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-getting-started-maven.html
1.5.2 Config配置
@Configuration
public class GulimallElasticsearchConfig {
public static final RequestOptions COMMON_OPTIONS;
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
COMMON_OPTIONS = builder.build();
}
@Bean
public RestHighLevelClient esRestClient() {
RestClientBuilder builder = null;
builder = RestClient.builder(new HttpHost("192.168.56.10", 9200, "http"));
RestHighLevelClient client = new RestHighLevelClient(builder);
return client;
}
}
官网:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-getting-started-initialization.html
1.5.3 使用
测试是否注入成功
@Autowired
private RestHighLevelClient client;
@Test
public void contextLoads() {
System.out.println(client);
}
测试是否能 添加 或更新数据
@Test
public void indexData() throws IOException {
IndexRequest indexRequest = new IndexRequest("users");
User user = new User();
user.setAge(19);
user.setGender("男");
user.setUserName("张三");
String jsonString = JSON.toJSONString(user);
indexRequest.source(jsonString,XContentType.JSON);
IndexResponse index = client.index(indexRequest, GulimallElasticsearchConfig.COMMON_OPTIONS);
System.out.println(index);
}
测试复杂检索
@Test
public void searchTest() throws IOException {
SearchRequest searchRequest = new SearchRequest();
searchRequest.indices("bank");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchQuery("address", "mill"));
TermsAggregationBuilder aggAvg = AggregationBuilders.terms("ageAgg").field("age").size(10);
sourceBuilder.aggregation(aggAvg);
AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");
sourceBuilder.aggregation(balanceAvg);
System.out.println("检索条件" + sourceBuilder.toString());
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, GulimallElasticsearchConfig.COMMON_OPTIONS);
System.out.println(searchResponse.toString());
SearchHits hits = searchResponse.getHits();
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {
String sourceAsString = hit.getSourceAsString();
Accout accout = JSON.parseObject(sourceAsString, Accout.class);
System.out.println("account:" + accout );
}
Aggregations aggregations = searchResponse.getAggregations();
Terms ageAgg1 = aggregations.get("ageAgg");
for (Terms.Bucket bucket : ageAgg1.getBuckets()) {
String keyAsString = bucket.getKeyAsString();
System.out.println("年龄:" + keyAsString);
long docCount = bucket.getDocCount();
System.out.println("个数:" + docCount);
}
Avg balanceAvg1 = aggregations.get("balanceAvg");
System.out.println("平均薪资:" + balanceAvg1.getValue());
System.out.println(searchResponse.toString());
}
结果:
accout:GulimallSearchApplicationTests.Accout(account_number=970, balance=19648, firstname=Forbes, lastname=Wallace, age=28, gender=M, address=990 Mill Road, employer=Pheast, email=forbeswallace@pheast.com, city=Lopezo, state=AK)accout:GulimallSearchApplicationTests.Accout(account_number=136, balance=45801, firstname=Winnie, lastname=Holland, age=38, gender=M, address=198 Mill Lane, employer=Neteria, email=winnieholland@neteria.com, city=Urie, state=IL)accout:GulimallSearchApplicationTests.Accout(account_number=345, balance=9812, firstname=Parker, lastname=Hines, age=38, gender=M, address=715 Mill Avenue, employer=Baluba, email=parkerhines@baluba.com, city=Blackgum, state=KY)accout:GulimallSearchApplicationTests.Accout(account_number=472, balance=25571, firstname=Lee, lastname=Long, age=32, gender=F, address=288 Mill Street, employer=Comverges, email=leelong@comverges.com, city=Movico, state=MT)年龄:38
个数:2
年龄:28
个数:1
年龄:32
个数:1
平均薪水:25208.0
总结:参考官网的API 和对应在 kibana 中发送的请求,在代码中通过调用对应API实现效果
官网:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-search.html#java-rest-high-search-request-optional
ELK
Elasticsearch 用于检索数据
logstach:存储数据
Kiban:视图化查看数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OV7Xy1DU-1650621164741)(/image-20201027120603889.png)]
2、商城业务 & 商品上架
上架的商品才可以在网站展示
上架的商品需要可以检索
2.1 商品Mapping
分析:商品上架在 es 中是存入 sku 还是 spu ?
1、检索的时候输入名字,是需要按照 sku 的 title进行全文检索的
2、检索使用商品规格,规格是 spu 的公共属性,每个 spu 是一样的
3、按照分类 id 进去的 都是直接列出 spu的,还可以切换
4、我们如果将 sku 的 全量信息 保存在 es 中 (包括 spu 属性),就太多量字段了
5、如果我们将 spu 以及他包含的 sku 信息保存到 es 中,也可以方
PUT product
{
"mappings":{
"properties":{
"skuId":{
"type":"long"
},
"spuId":{
"type":"keyword"
},
"skuTitle":{
"type":"text",
"analyzer": "ik_smart"
},
"skuPrice":{
"type":"keyword"
},
"skuImg":{
"type":"text",
"analyzer": "ik_smart"
},
"saleCount":{
"type":"long"
},
"hasStock":{
"type":"boolean"
},
"hotScore":{
"type":"long"
},
"brandId":{
"type":"long"
},
"catelogId":{
"type":"long"
},
"brandName":{
"type":"keyword",
"index": false,
"doc_values": false
},
"brandImg":{
"type":"keyword",
"index": false,
"doc_values": false
},
"catalogName":{
"type":"keyword",
"index": false,
"doc_values": false
},
"attrs":{
"type":"nested",
"properties": {
"attrId":{
"type":"long"
},
"attrName":{
"type":"keyword",
"index":false,
"doc_values":false
},
"attrValue": {
"type":"keyword"
}
}
}
}
}
}
2.2 商品上架接口编写
需求分析:
上架商品、将该商品相关属性上传到 Es中 为搜索服务做铺垫
Controller
@RequestMapping("/{spuId}/up")
public R list(@PathVariable("spuId") Long spuId){
spuInfoService.up(spuId);
return R.ok();
}
Service
@Override
public void up(Long spuId) {
List<SkuInfoEntity> skus = skuInfoService.getSkuBySpuId(spuId);
List<Long> skuIds = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
List<ProductAttrValueEntity> attrValueEntities = attrValueService.baseAttrListforspu(spuId);
List<Long> attrIds = attrValueEntities.stream().map(attr -> {
return attr.getAttrId();
}).collect(Collectors.toList());
List<Long> searchAttrIds = attrService.selectSearchAttrs(attrIds);
Set<Long> idSet = new HashSet<>(searchAttrIds);
List<SkuEsModel.Attrs> attrList = attrValueEntities.stream().filter(item -> {
return idSet.contains(item.getAttrId());
}).map(item -> {
SkuEsModel.Attrs attrs1 = new SkuEsModel.Attrs();
BeanUtils.copyProperties(item, attrs1);
return attrs1;
}).collect(Collectors.toList());
Map<Long, Boolean> stockMap = null;
try {
R r = wareFeignService.getSkuHasStock(skuIds);
TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {
};
stockMap = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
} catch (Exception e) {
log.error("库存服务异常:原因:{}",e);
e.printStackTrace();
}
Map<Long, Boolean> finalStockMap = stockMap;
List<SkuEsModel> upProducts = skus.stream().map(sku -> {
SkuEsModel skuEsModel = new SkuEsModel();
BeanUtils.copyProperties(sku, skuEsModel);
skuEsModel.setSkuPrice(sku.getPrice());
skuEsModel.setSkuImg(sku.getSkuDefaultImg());
skuEsModel.setAttrs(attrList);
if (finalStockMap == null) {
skuEsModel.setHasStock(true);
} else {
skuEsModel.setHasStock(finalStockMap.get(sku.getSkuId()));
}
skuEsModel.setHotScore(0L);
BrandEntity brandEntity = brandService.getById(skuEsModel.getBrandId());
skuEsModel.setBrandName(brandEntity.getName());
skuEsModel.setBrandImg(brandEntity.getLogo());
CategoryEntity categoryEntity = categoryService.getById(skuEsModel.getCatalogId());
skuEsModel.setCatalogName(categoryEntity.getName());
return skuEsModel;
}).collect(Collectors.toList());
R r = esFeignClient.productStatusUp(upProducts);
if (r.getCode() == 0) {
baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
} else {
}
}
2.2.1、发送远程调用,库存系统查询是否有库存
@PostMapping("/hasstock")
public R getSkuHasStock(@RequestBody List<Long> skuIds) {
List<SkuHasStockVo> vos = wareSkuService.getSkusStock(skuIds);
R ok = R.ok();
ok.setData(vos);
return ok;
}
Service
@Override
public List<SkuHasStockVo> getSkusStock(List<Long> skuIds) {
List<SkuHasStockVo> collect = skuIds.stream().map(skuId -> {
SkuHasStockVo vo = new SkuHasStockVo();
Long count = baseMapper.getSkuStock(skuId);
vo.setSkuId(skuId);
vo.setHasStock(count == null?false:count>0);
return vo;
}).collect(Collectors.toList());
return collect;
}
2.2.2 将数据发送给 es保存 ,直接发送给 search服务
controller
@PostMapping("/product")
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModelList){
boolean b = false;
try {
b = productSaveService.productStatusUp(skuEsModelList);
} catch (Exception e) {
log.error("ElasticSaveController商品上架错误:{}",e);
return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
if (!b) {
return R.ok();
} else {
return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
}
service
@Override
public boolean productStatusUp(List<SkuEsModel> skuEsModelList) throws IOException {
BulkRequest bulkRequest = new BulkRequest();
for (SkuEsModel model : skuEsModelList) {
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
indexRequest.id(model.getSkuId().toString());
String s = JSON.toJSONString(model);
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 -> {
return item.getId();
}).collect(Collectors.toList());
log.error("商品上架完成:{}",collect);
return b;
}
业务流程总结:
前端点击上架后,发送请求并带上参数 spuid
-
根据 spuid 查询 pms_sku_info 表得到商品相关属性 -
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BePbeKW2-1650621164741)(/image-20201028153205471.png)] -
根据 spuid 查询 pms_product_attr_value 表得到可以用来检索的规格属性 -
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UelG3QEV-1650621164741)(/image-20201028153038796.png)] -
从 ProductAttrValueEntity 中拿到所有的 attrId,根据 attrId 查询 pms_attr 查询检索的属性 -
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q93mxcTt-1650621164742)(/image-20201028155204710.png)] -
x根据 pms_attr 查询到检索属性后,用检索属性和 原先根据 spuid 查询 pms_sku_info 表得到商品相关属性进行比较,pms_sku_info 包含 从 pms_attr 字段attr_id 则数据保存否则过滤 -
根据 skuIds 去查询远程仓库中是否有库存 SELECT SUM(stock-stock_locked) FROM wms_ware_sku WHERE sku_id = 1 -
组装数据 设置 SkuEsModel -
发送给 es 保存
3、商城业务 & 首页整合
3.1 整合 thymeleaf 渲染首页
需求分析:
开发传统Java WEB工程时,我们可以使用JSP页面模板语言,但是在SpringBoot中已经不推荐使用了。SpringBoot支持如下页面模板语言
- Thymeleaf
- FreeMarker
- Velocity
- Groovy
- JSP
thymeleaf 官网:https://www.thymeleaf.org/
官网文档给出了,语法、相关标签如何使用的步骤,由于官网文档都是英文,英文文档阅读能力好的同学可以选择阅读,英文不好的同学可以选择中文文档进行学习,为此我在网上找到了相关的中文文档:文档:thymeleaf 链接:http://note.youdao.com/noteshare?id=7771a96e9031b30b91ed55c50528e918
3.1.2 SpringBoot 整合 thymeleaf
Pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
application.yml
Spring:
thymeleaf:
cache: false
resource 目录介绍:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x8pIbRdt-1650621164742)(/image-20201029102830164.png)]
index.html中使用
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
相关语法使用
<ul>
<li th:each="category : ${categorys}">
<a href="#" class="header_main_left_a"
th:attr="ctg-data=${category.catId}">
<b th:text="${category.name}">家用电器</b></a>
</li>
默认SpringBoot会直接去找 templates 下的 index.html
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VfxwXECs-1650621164742)(/image-20201029103230550.png)]
3.2 整合dev-tools 渲染分类数据
需求分析:
我们需要在页面的侧边查询出分类的数据,并且选中一级分类数据后显示二级和三级分类数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aLmVdVi6-1650621164742)(/image-20201029103544553.png)]
Controller
@GetMapping({"/","/index.html"})
public String indexPage(Model model){
List<CategoryEntity> categoryEntityList = categoryService.getLevel1Categorys();
model.addAttribute("categorys",categoryEntityList);
return "index";
}
Service
@Override
public List<CategoryEntity> getLevel1Categorys() {
return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid",0));
}
一级分类数据能显示后,着手处理二级分类数据获取
Controller
@ResponseBody
@RequestMapping("/index/catalog.json")
public Map<String, List<Catelog2Vo>> getCatelogJson() {
Map<String, List<Catelog2Vo>> catelogJson = categoryService.getCatelogJson();
return catelogJson;
}
Service
@Override
public Map<String, List<Catelog2Vo>> getCatelogJson() {
List<CategoryEntity> level1Categorys = getLevel1Categorys();
Map<String, List<Catelog2Vo>> categoryList = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
List<Catelog2Vo> catelog2Vos = null;
if (categoryEntities != null) {
catelog2Vos = categoryEntities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
List<CategoryEntity> categoryEntities1 = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", l2.getCatId()));
if (categoryEntities1 != null ){
List<Catelog2Vo.catelog3Vo> catelog3VoList = categoryEntities1.stream().map(l3 -> {
Catelog2Vo.catelog3Vo catelog3Vo = new Catelog2Vo.catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return catelog3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(catelog3VoList);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
return categoryList;
}
上方代码具体业务逻辑:
- 查询到一级分类,根据一级分类查询出二级分类并设置对应Vo对象,以此类推
4、商城业务 & Nginx 域名访问
4.1 Nginx 搭建域名环境一(反向代理配置)
什么是 反向代理?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kUPs8NJ2-1650621164742)(/image-20201029051037570.png)]
vi nginx.conf 文件后在底部有该条语句:
- 引入nginx下的 conf.d 下面的conf文件
- 那么我们开始在该目录下增加关于 谷粒商城的 nginx
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KGhuebcd-1650621164743)(/image-20201029045921936.png)]
拷贝原先默认的 conf
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a6RjHunK-1650621164743)(/image-20201029050207857.png)]
修改
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hvrHwqcS-1650621164743)(/image-20201029050136324.png)]
4.2 Nginx 搭建域名环境二 (负载均衡到网关)
配置 UpStream
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SgDc3iMw-1650621164743)(/image-20201029050256216.png)]
使用nginx实现负载平衡的最简单配置如下,官网地址:https://nginx.org/en/docs/http/load_balancing.html
http {
upstream myapp1 {
server srv1.example.com;
server srv2.example.com;
server srv3.example.com;
}
server {
listen 80;
location / {
proxy_pass http://myapp1;
}
}
}
同时在 本机上 hosts 文件上那个配置 域名映射
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-skBLYNga-1650621164743)(/image-20201029050625740.png)]
将请求转接给网关后,需要在网关配置
- id: gulimall_host_route
uri: lb://gulimall-product
predicates:
- Host=**.gulimall.com
最后放几张图方便理解哈
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZVoeHky7-1650621164743)(/image-20201029050828421.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bwC5fsH7-1650621164744)(/image-20201029050901546.png)]
5、性能压测 & 压力测试
简介
压力测试考察当前软硬件环境下系统所能承受住的最大负荷并帮助找出系统的瓶颈所在,压测都是为了系统
在线上的处理能力和稳定性维持在一个标准范围内,做到心中有数
使用压力测试,我们有希望找到很多种用其他测试方法更难发现的错误,有两种错误类型是:
内存泄漏、并发与同步
有效的压力测试系统将应用以下这些关键条件:重复、并发、量级、随机变化
5.1 性能指标
5.1.1 Jvm 内存模型
1、Jvm内存模型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qFc23QQ2-1650621164744)(/image-20201029112517466.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1wW5yECg-1650621164744)(/image-20201029113956043.png)]
5.1.2 堆
所有的对象实例以及数组都要在堆上分配,堆时垃圾收集器管理的主要区域,也被称为 “GC堆”,也是我们优化最多考虑的地方
堆可以细分为:
-
新生代
- Eden空间
- From Survivor 空间
- To Survivor 空间
-
老年代 -
永久代/原空间
- Java8 以前永久代、受 JVM 管理、Java8 以后原空间,直接使用物理内存,因此默认情况下,原空间的大小仅受本地内存限制
垃圾回收
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qdDtvmbv-1650621164744)(/image-20201029114153244.png)]
从 Java8 开始,HotSpot 已经完全将永久代(Permanent Generation)移除,取而代之的是一个新的区域 - 元空间(MetaSpac)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cn1ebInK-1650621164744)(/image-20201029114218716.png)]
5.1.3 jconsole 与 jvisualvm
jdk 的两个小工具 jconsole、jvisualvm(升级版本的 jconsole)。通过命令行启动、可监控本地和远程应用、远程应用需要配置
1、jvisualvm 能干什么
监控内存泄漏、跟踪垃圾回收、执行时内存、cpu分析、线程分析…
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bKNKEe9x-1650621164744)(/image-20201029120502383.png)]
运行:正在运行的线程
休眠:sleep
等待:wait
驻留:线程池里面的空闲线程
监视:组赛的线程、正在等待锁
2、安装插件方便查看 gc
cmd 启动 jvisualvm
工具->插件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Wo6rtmO-1650621164744)(/image-20201029121108492.png)]
如果503 错误解决
打开网址: https://visualvm.github.io/pluginscenters.html
cmd 查看自己的jdk版本,找到对应的
docker stats 查看相关命令
5.1.4 监控指标
SQL 耗时越小越好、一般情况下微妙级别
命中率越高越好、一般情况下不能低于95%
锁等待次数越低越好、等待时间越短越好
1、中间件指标
毫秒
压测内容 | 压测线程数 | 吞吐量/ms | 90%响应时间/ms | 99%响应时间/ms |
---|
Nginx | 50 | 1834 | 11 | 40 | GateWay | 50b | 16577 | 5 | 19 | 简单服务 | 50 | 17965 | 5 | 10 | 首页一级菜单渲染 | 50 | 400(db,thymeleaf) | 149 | 230 | 首页渲染(开缓存) | 50 | 467 | 128 | 209 | 首页渲染(开缓存、优化数据库,关日志) | 50 | 1300 | 60 | 107 | 三级分类数据获取 | 50 | 4.4(db)/18(优化后) | 16622 | 16832 | 三级分类数据获取(优化业务) | 50 | 163 | 410 | 585 | 三级分类数据获取(redis缓存) | 50 | 553 | 113 | 182 | 首页全量数据获取 | 50 | 10(静态资源)/12 | 6183 | 12358 | Nginx+Gateway | 50 | | | | Gateway+简单服务 | 50 | 2685 | 7 | 10 | 全链路 | 50 | 900 | 73 | 971 |
中间件越多,性能损失越大,大多都损失在了网络交互
2、数据库指标
-
响应时间(Response Time:RT) -
响应时间指用户从客户端发起一个请求开始,到客户端接收到服务器端返回的响应结束,整个过程所耗费的时间 -
HPS(Hits Per Second) :每秒点击次数,单位是次/秒 -
TPS(Transaction per Second):系统每秒处理交易数,单位是笔/秒 -
QPS (Query perSecond) :系统每秒处理查询次数,单位是次/秒。对于互联网业务中,如果某些业务有且仅有一个请求连接,那么TPS=QPS=HPS,一般情况下用TPS来衡量整个业务流程,用QPS来衡量接口查询次数,用HPS来表示对服务器单击请求。 -
无论TPS、QPS、HPS,此指标是衡量系统处理能力非常重要的指标,越大越好,根据经验,一般情况下:
- 金融行业: 1000TPS~50000TPS, 不包括互联网化的活动
- 保险行业: 1007P-00000PS, 不包括互联网化的活动
- 制造行业: 10TPS~5000TPS
- 互联网电子商务: 10000TPS~-100000TPS
- 互联网中型网站: 1000TPS~50000TPS
- 互联网小型网站: 5007PS~10000TPS
-
最大响应时间(Max Response Time) 指用户发出请求或者指令到系统做出反应(响应)的最大时间。 -
最少响应时间 (Mininum ResponseTime)指用户发出请求或者指令到系统做出反应(响应)的最少时间 -
90%响应时间(90% Response Time) 是指所有用户的响应时间进行排序、第90%的响应时间 -
从外部看、性能测试主要关注如下三个指
- 吞吐量:每秒钟系统能够处理的请求数、任务数
- 响应时间:服务处理一个请求或一个任务的耗时
- 错误率:一批请求中结果出错的请求所占比例
吞吐量大:系统支持高并发,
响应时间:越短说明接口性能越好
5.2 JMeter
5.2.1 JMeter 安装
jmeter官网:https://jmeter.apache.org/
5.2.2 JMeter 压测示例
1、添加线程组
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LbPPTco2-1650621164745)(/image-20201029084634498.png)]
2、添加 HTTP 请求
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xo56BasK-1650621164745)(/image-20201029085843220.png)]
3、添加监听器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YxROBPFc-1650621164745)(/image-20201029085942442.png)]
4、启动压测&查看
汇总图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8ZLRz2OK-1650621164745)(/image-20201029092357910.png)]
察看结果树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rF6lmqYq-1650621164745)(/image-20201029092436633.png)]
汇总报告
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-27v2nhlw-1650621164745)(/image-20201029092454376.png)]
聚合报告
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5re4ClGw-1650621164746)(/image-20201029092542876.png)]
5.2.3 JMeter Address Already in use 错误解决
windows本身提供的端口访问机制的问题。 Windows提供给TCP/IP 链接的端口为1024-5000,并且要四分钟来循环回收他们。就导致 我们在短时间内跑大量的请求时将端口占满了。
1.cmd中,用regedit命令打开注册表
2.在HKEY_ LOCAL MACHINE\SYSTEMCurrentControlSet\Services Tcpip\Parameters下,
? 1.右击parameters,添加一个新的DWORD,名字为MaxUserPort ? 2.然后双击 MaxUserPort,输入数值数据为65534,基数选择十进制(如果是分布式运行的话,控制机器和负载机器都需要这样操作哦)
3.修改配置完毕之后记得重启机器才会生效
TCPTimedWaitDelay:30
6、缓存与分布式锁
6.1 缓存
6.1.2 缓存使用
为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问,而 db 承担数据落盘工作
哪些数据适合放入缓存?
- 即时性、数据一致性要求不高的
- 访问量大且更新频率不高的数据(读多、写少)
举例:电商类应用、商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率来定)后台如果发布一个商品、买家需要 5 分钟才能看到新商品一般还是可以接受的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2WNiPFqz-1650621164746)(/image-20201030190425556.png)]
伪代码
data = cche.load(b);
if(data == null) {
data = db.load(id);
cache.put(id,data);
}
return data;
注意:在开发中,凡是放到缓存中的数据我们都应该制定过期时间,使其可以在系统即使没有主动更新数据也能自动触发数据加载的流程,避免业务奔溃导致的数据永久不一致的问题
6.1.3 整合 redis 作为缓存
1、引入依赖
SpringBoot 整合 redis,查看SpringBoot提供的 starts
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ndOQbDFJ-1650621164746)(/image-20201031154148722.png)]
官网:https://docs.spring.io/spring-boot/docs/2.1.18.RELEASE/reference/html/using-boot-build-systems.html#using-boot-starter
pom.xml
<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>
2、配置
application.yaml
Spring:
redis:
host: 192.168.56.10
port: 6379
RedisAutoConfig.java
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dn9NSu40-1650621164746)(/image-20201031154710108.png)]
3、测试
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
public void testStringRedisTemplate() {
stringRedisTemplate.opsForValue().set("hello","world_" + UUID.randomUUID().toString());
String hello = stringRedisTemplate.opsForValue().get("hello");
System.out.println("之前保存的数据是:" + hello);
}
4、优化三级分类数据获取
@Override
public Map<String, List<Catelog2Vo>> getCatelogJson() {
String catelogJSON = redisTemplate.opsForValue().get("catelogJSON");
if (StringUtils.isEmpty(catelogJSON)) {
Map<String, List<Catelog2Vo>> catelogJsonFromDb = getCatelogJsonFromDb();
String s = JSON.toJSONString(catelogJsonFromDb);
redisTemplate.opsForValue().set("catelogJSON",s);
return catelogJsonFromDb;
}
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catelogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {});
return result;
}
6.2 缓存失效
高并发下缓存失效问题
缓存失效
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KjTlw2zI-1650621164746)(/image-20201031163704355.png)]
缓存雪崩
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PTWWCDhk-1650621164746)(/image-20201031163949881.png)]
缓存击穿
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cnOttNFT-1650621164747)(/image-20201031164021131.png)]
分布式下如何加锁
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oVpwIHzF-1650621164747)(/image-20201031112235353.png)]
6.4 分布式锁
分布式锁原理与应用
分布式锁基本原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5d891TCB-1650621164747)(/image-20201031122557660.png)]
**理解:**就先当1000个人去占一个厕所,厕所只能有一个人占到这个坑,占到这个坑其他人就只能在外面等待,等待一段时间后可以再次来占坑,业务执行后,释放锁,那么其他人就可以来占这个坑
分布式锁演进 - 阶段一
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FyYY92SE-1650621164747)(/image-20201031123441336.png)]
代码:
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "0");
if (lock) {
Map<String,List<Catelog2Vo>> dataFromDb = getDataFromDB();
redisTemplate.delete("lock");
return dataFromDb;
} else {
return getCatelogJsonFromDbWithRedisLock();
}
分布式锁演进 - 阶段二
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ulNQcx5o-1650621164747)(/image-20201031123640746.png)]
代码:
Boolean lock = redisTemplate.opsForValue().setIfAbsent()
if (lock) {
redisTemplate.expire("lock",30,TimeUnit.SECONDS);
Map<String,List<Catelog2Vo>> dataFromDb = getDataFromDB();
redisTemplate.delete("lock");
return dataFromDb;
} else {
return getCatelogJsonFromDbWithRedisLock();
}
分布式锁演进 - 阶段三
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wUBRWCNK-1650621164747)(/image-20201031124210112.png)]
代码:
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","111",300,TimeUnit.SECONDS);
if (lock) {
redisTemplate.expire("lock",30,TimeUnit.SECONDS);
Map<String,List<Catelog2Vo>> dataFromDb = getDataFromDB();
redisTemplate.delete("lock");
return dataFromDb;
} else {
return getCatelogJsonFromDbWithRedisLock();
}
分布式锁演进 - 阶段四
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eFDa4fix-1650621164747)(/image-20201031124615670.png)]
图解:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3aVZOffW-1650621164748)(/image-20201031130547173.png)]
代码:
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
if (lock) {
Map<String,List<Catelog2Vo>> dataFromDb = getDataFromDB();
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
return dataFromDb;
} else {
return getCatelogJsonFromDbWithRedisLock();
}
分布式锁演进 - 阶段五 最终模式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QA2wIn97-1650621164748)(/image-20201031130201609.png)]
代码:
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
if (lock) {
System.out.println("获取分布式锁成功");
Map<String,List<Catelog2Vo>> dataFromDb;
try {
dataFromDb = getDataFromDB();
} finally {
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
return dataFromDb;
} else {
System.out.println("获取分布式锁失败,等待重试");
try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
return getCatelogJsonFromDbWithRedisLock();
}
问题:
- 分布式加锁解锁都是这两套代码,可以封装成工具类
- 分布式锁有更专业的框架
分布式锁 - Redisson
1、简介&整合
官网文档上详细说明了 不推荐使用 setnx来实现分布式锁,应该参考 the Redlock algorithm 的实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fLryRbpQ-1650621164748)(/image-20201101050725534.png)]
the Redlock algorithm:https://redis.io/topics/distlock
在Java 语言环境下使用 Redisson
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uNhLEggi-1650621164748)(/image-20201101050924914.png)]
github:https://github.com/redisson/redisson
有对应的 中文文档
在 Maven 仓库中搜索也能搜索出 Redisson
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IHUHXmnK-1650621164748)(/image-20201101051157803.png)]
Pom
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
2、Redisson - Lock 锁测试 & Redisson - Lock 看门狗原理 - Redisson 如何解决死锁
@RequestMapping("/hello")
@ResponseBody
public String hello(){
RLock lock = redission.getLock("my-lock");
lock.lock();
lock.lock(10, TimeUnit.SECONDS);
try {
System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
Thread.sleep(3000);
} catch (Exception e) {
} finally {
System.out.println("释放锁...." + Thread.currentThread().getId());
lock.unlock();
}
return "hello";
}
进入到 Redisson Lock 源码
1、进入 Lock 的实现 发现 他调用的也是 lock 方法参数 时间为 -1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uimirQPq-1650621164748)(/image-20201101051659465.png)]
2、再次进入 lock 方法
发现他调用了 tryAcquire
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qrFxuUIj-1650621164749)(/image-20201101051925487.png)]
3、进入 tryAcquire
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UZQ27Z0K-1650621164749)(/image-20201101052008724.png)]
4、里头调用了 tryAcquireAsync
这里判断 laseTime != -1 就与刚刚的第一步传入的值有关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x8ePZz9B-1650621164749)(/image-20201101052037959.png)]
5、进入到 tryLockInnerAsync 方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7xAGMIzC-1650621164749)(/image-20201101052158592.png)]
6、internalLockLeaseTime 这个变量是锁的默认时间
这个变量在构造的时候就赋初始值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1oi2tiOY-1650621164749)(/image-20201101052346059.png)]
7、最后查看 lockWatchdogTimeout 变量
也就是30秒的时间
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A5KJVv2U-1650621164749)(/image-20201101052428198.png)]
3、Reidsson - 读写锁
二话不说,上代码!!!
@RequestMapping("/write")
@ResponseBody
public String writeValue() {
RReadWriteLock lock = redission.getReadWriteLock("rw_lock");
String s = "";
RLock rLock = lock.writeLock();
try {
rLock.lock();
System.out.println("写锁加锁成功..." + Thread.currentThread().getId());
s = UUID.randomUUID().toString();
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
redisTemplate.opsForValue().set("writeValue",s);
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
System.out.println("写锁释放..." + Thread.currentThread().getId());
}
return s;
}
@RequestMapping("/read")
@ResponseBody
public String readValue() {
RReadWriteLock lock = redission.getReadWriteLock("rw_lock");
RLock rLock = lock.readLock();
String s = "";
rLock.lock();
try {
System.out.println("读锁加锁成功..." + Thread.currentThread().getId());
s = (String) redisTemplate.opsForValue().get("writeValue");
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
System.out.println("读锁释放..." + Thread.currentThread().getId());
}
return s;
}
来看下官网的解释
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t0eev9uG-1650621164750)(/image-20201101053042268.png)]
4、Redisson - 闭锁测试
官网!!!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vBe5IBs3-1650621164750)(/image-20201101053053554.png)]
上代码
@GetMapping("/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redission.getCountDownLatch("door");
door.trySetCount(5);
door.await();
return "放假了....";
}
@GetMapping("/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id) {
RCountDownLatch door = redission.getCountDownLatch("door");
door.countDown();
return id + "班的人走完了.....";
}
和 JUC 的 CountDownLatch 一致
await()等待闭锁完成
countDown() 把计数器减掉后 await就会放行
5、Redisson - 信号量测试
官网!!!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UNCdzOpi-1650621164750)(/image-20201101053450708.png)]
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {
RSemaphore park = redission.getSemaphore("park");
boolean b = park.tryAcquire();
return "ok=" + b;
}
@GetMapping("/go")
@ResponseBody
public String go() {
RSemaphore park = redission.getSemaphore("park");
park.release();
return "ok";
}
类似 JUC 中的 Semaphore
6、Redission - 缓存一致性解决
6.3 缓存数据一致性
缓存数据一致性 - 双写模式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZOybFQ5h-1650621164750)(/image-20201101053613373.png)]
两个线程写 最终只有一个线程写成功,后写成功的会把之前写的数据给覆盖,这就会造成脏数据
缓存数据一致性 - 失效模式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ydKLyjyE-1650621164750)(/image-20201101053834126.png)]
三个连接
一号连接 写数据库 然后删缓存
二号连接 写数据库时网络连接慢,还没有写入成功
三号链接 直接读取数据,读到的是一号连接写入的数据,此时 二号链接写入数据成功并删除了缓存,三号开始更新缓存发现更新的是二号的缓存
缓存数据一致性解决方案
无论是双写模式还是失效模式,都会到这缓存不一致的问题,即多个实力同时更新会出事,怎么办?
- 1、如果是用户纯度数据(订单数据、用户数据),这并发几率很小,几乎不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
- 2、如果是菜单,商品介绍等基础数据,也可以去使用 canal 订阅,binlog 的方式
- 3、缓存数据 + 过期时间也足够解决大部分业务对缓存的要求
- 4、通过加锁保证并发读写,写写的时候按照顺序排好队,读读无所谓,所以适合读写锁,(业务不关心脏数据,允许临时脏数据可忽略)
总结:
- 我们能放入缓存的数据本来就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前的最新值即可
- 我们不应该过度设计,增加系统的复杂性
- 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CbXO9Yk2-1650621164750)(D:\java\gitee\java-learning-note\Evan Guo\项目笔记\谷粒商城项目开发文档\分布式高级篇\image\image-20201101054937769.png)]
最后符上 三级分类数据 加上分布式锁
6.5 Spring Cache
1、简介
- Spring 从3.1开始定义了
org.springframework.cache.Cache 和 org.sprngframework.cache.CacheManager 接口睐统一不同的缓存技术 - 并支持使用
JCache (JSR-107)注解简化我们的开发 - Cache 接口为缓存的组件规范定义,包含缓存的各种操作集合
Cache 接口下 Spring 提供了各种 XXXCache的实现,如 RedisCache 、EhCache ,ConcrrentMapCache 等等, - 每次调用需要缓存功能实现方法的时候,
Spring 会检查检查指定参数的马努表犯法是否已经被嗲用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户,下次直接调用从缓存中获取 - 使用
Sprng 缓存抽象时我们需要关注的点有以下两点
- 1、确定方法需要被缓存以及他们的的缓存策略
- 2、从缓存中读取之前缓存存储的数据
官网地址:https://docs.spring.io/spring-framework/docs/5.2.10.RELEASE/spring-framework-reference/integration.html#cache-strategie
缓存注解配置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6GqtKLpY-1650621164750)(/image-20201228171806703.png)]
2、基础概念
从3.1版本开始,Spring 框架就支持透明地向现有 Spring 应用程序添加缓存。与事务支持类似,缓存抽象允许在对代码影响最小的情况下一致地使用各种缓存解决方案。从 Spring 4.1 开始,缓存抽象在JSR-107注释和更多定制选项的支持下得到了显著扩展。
3、注解
对于缓存声明,Spring的缓存抽象提供了一组Java注解
注解使用
@Cacheable(value = {"category"},key = "#root.method.name")
@Override
public List<CategoryEntity> getLevel1Categorys() {
long l = System.currentTimeMillis();
List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
System.out.println("耗费时间:" + (System.currentTimeMillis() - l));
return categoryEntities;
}
4、表达式语法
配置
package com.atguigu.gulimall.product.config;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@EnableConfigurationProperties(CacheProperties.class)
@EnableCaching
@Configuration
public class MyCacheConfig {
@Bean
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;
}
}
yaml
Spring:
cache:
type: redis
redis:
time-to-live: 3600000
key-prefix: CACHE_
use-key-prefix: true
cache-null-values: true
5、缓存穿透问题解决
@Cacheable(value = {"category"},key = "#root.method.name",sync = true)
@Override
public List<CategoryEntity> getLevel1Categorys() {
long l = System.currentTimeMillis();
List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
System.out.println("耗费时间:" + (System.currentTimeMillis() - l));
return categoryEntities;
}
6、缓存更新
@Caching(evict = {
@CacheEvict(value = {"category"},key = "'getLevel1Categorys'"),
@CacheEvict(value = {"category"},key = "'getCatelogJson'")
})
@Transactional
@Override
public void updateCascate(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
}
总结业务流程:
如果忘了这个技术点看下做的笔记的例子,然后去官网看下文档,温故而知新
流程图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2iGfrrhD-1650621164751)(/image-20201228171552816.png)]
7、商城业务 & 商品检索
7.1 检索业务分析
7.1.1 检索查询参数模型分析抽取
打个比例吧 你肯定上过京东、淘宝买过东西吧? 那么你想要购买什么东西,你需要在搜索框中搜索你想要购买的物品,那么系统就会给你响应
我在京东搜索 Iphone 他会显示出相对应的产品
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tZDZlxRf-1650621164751)(/image-20201104152544243.png)]
那么我们开始对业务条件进行分析,并创建对应的VO类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yPNYLsl7-1650621164751)(/image-20201104153118677.png)]
好的创建出来了…
@Data
public class SearchParam {
private String keyword;
private Long catalog3Id;
private String sort;
private Integer hasStock = 0;
private String skuPrice;
private List<Long> brandId;
private List<String> attrs;
private Integer pageNum = 1;
}
7.1.2 检索返回结果模型分析抽取
那么返回的数据我们是不是也要创建一个 VO 用来返回页面的数据?
借鉴京东的实例来做参考
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I8Xcw2PJ-1650621164751)(/image-20201104153950990.png)]
那么抽取实体类
@Data
public class SearchResult {
private List<SkuEsModel> products;
private Integer pageNum;
private Long total;
private Integer totalPages;
private List<BrandVo> brands;
private List<CatalogVo> catalogs;
private List<AttrVo> attrs;
private List<Integer> pageNavs;
@Data
public static class BrandVo {
private Long brandId;
private String brandName;
private String brandImg;
}
@Data
public static class CatalogVo {
private Long catalogId;
private String CatalogName;
}
@Data
public static class AttrVo {
private Long attrId;
private String attrName;
private List<String> attrValue;
}
}
7.2 检索语句构建
7.2.1 查询部分 & 聚合部分
那么这个 DSL 编写我们就在 Kibana 中测试
GET gulimall_product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "华为"
}
}
],
"filter": [
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"1",
"5",
"9"
]
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "8"
}
}
},
{
"terms": {
"attrs.attrValue": [
"2019"
]
}
}
]
}
}
}
},
{
"term": {
"hasStock": {
"value": "false"
}
}
},
{
"range": {
"skuPrice": {
"gte": 0,
"lte": 7000
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size":4,
"highlight": {
"fields": {"skuTitle": {}},
"pre_tags": "<b style=color:red>",
"post_tags": "</b>"
},
"aggs": {
"brand_agg": {
"terms": {
"field": "brandId",
"size": 10
},
"aggs": {
"brand_name_agg": {
"terms": {
"field": "brandName",
"size": 10
}
},
"brand_img_agg": {
"terms": {
"field": "brandImg",
"size": 10
}
}
}
},
"catalog_agg": {
"terms": {
"field": "catalogId",
"size": 10
},
"aggs": {
"catalog_name_agg": {
"terms": {
"field": "catalogName",
"size": 10
}
}
}
},
"attr_agg":{
"nested": {
"path": "attrs"
},
"aggs": {
"attr_id_agg": {
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attr_name_agg": {
"terms": {
"field": "attrs.attrName",
"size": 10
}
},
"attr_value_agg":{
"terms": {
"field": "attrs.attrValue",
"size": 10
}
}
}
}
}
}
}
}
用代码实现
private SearchRequest buildSearchRequest(SearchParam param) {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
if (!StringUtils.isEmpty(param.getKeyword())) {
boolQuery.must(QueryBuilders.matchQuery("skuTitle", param.getKeyword()));
}
if (param.getCatalog3Id() != null) {
boolQuery.filter(QueryBuilders.termQuery("catalogId", param.getCatalog3Id()));
}
if (param.getBrandId() != null && param.getBrandId().size() > 0) {
boolQuery.filter(QueryBuilders.termsQuery("brandId", param.getBrandId()));
}
if (param.getAttrs() != null && param.getAttrs().size() > 0) {
for (String attr : param.getAttrs()) {
BoolQueryBuilder nestedboolQuery = QueryBuilders.boolQuery();
String[] s = attr.split("_");
String attrId = s[0];
String[] attrValues = s[1].split(":");
nestedboolQuery.must(QueryBuilders.termQuery("attrs.attrId", attrId));
nestedboolQuery.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));
NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", nestedboolQuery, ScoreMode.None);
boolQuery.filter(nestedQuery);
}
}
boolQuery.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1 ? true : false));
if (!StringUtils.isEmpty(param.getSkuPrice())) {
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");
String[] s = param.getSkuPrice().split("_");
if (s.length == 2) {
rangeQuery.gte(s[0]).lte(s[1]);
} else if (s.length == 1) {
if (param.getSkuPrice().startsWith("_")) {
rangeQuery.lte(s[0]);
}
if (param.getSkuPrice().endsWith("_")) {
rangeQuery.gte(s[0]);
}
}
boolQuery.filter(rangeQuery);
}
sourceBuilder.query(boolQuery);
if (!StringUtils.isEmpty(param.getSort())) {
String sort = param.getSort();
String[] s = sort.split("_");
SortOrder order = s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC;
sourceBuilder.sort(s[0], order);
}
sourceBuilder.from((param.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
sourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);
if (!StringUtils.isEmpty(param.getKeyword())) {
HighlightBuilder builder = new HighlightBuilder();
builder.field("skuTitle");
builder.preTags("<b style='color:red'>");
builder.postTags("</b>");
sourceBuilder.highlighter(builder);
}
TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg");
brand_agg.field("brandId").size(50);
brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(2));
brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(2));
sourceBuilder.aggregation(brand_agg);
TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);
catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));
sourceBuilder.aggregation(catalog_agg);
NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");
TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50));
attr_agg.subAggregation(attr_id_agg);
sourceBuilder.aggregation(attr_agg);
String s = sourceBuilder.toString();
System.out.println("构建的DSL:" + s);
SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, sourceBuilder);
return searchRequest;
}
代码实现基本上是 根据 json 来写 调用对应的 API
term 和 terms 不要调用错误
7.3 结果提取封装
Controller
@GetMapping("/list.html")
public String listPage(SearchParam param, Model model){
SearchResult result = mallSearchService.search(param);
model.addAttribute("result",result);
return "list";
}
Service
@Override
public SearchResult search(SearchParam param) {
SearchResult result = null;
SearchRequest searchRequest = buildSearchRequest(param);
try {
SearchResponse response = client.search(searchRequest, GulimallElasticsearchConfig.COMMON_OPTIONS);
result = buildSearchResult(response, param);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
具体业务执行
private SearchResult buildSearchResult(SearchResponse response, SearchParam param) {
SearchResult result = new SearchResult();
SearchHits hits = response.getHits();
List<SkuEsModel> esModels = new ArrayList<>();
if (hits.getHits() != null && hits.getHits().length > 0) {
for (SearchHit hit : hits.getHits()) {
String sourceAsString = hit.getSourceAsString();
SkuEsModel skuEsModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
if (!StringUtils.isEmpty(param.getKeyword())) {
HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
String string = skuTitle.getFragments()[0].string();
skuEsModel.setSkuTitle(string);
}
esModels.add(skuEsModel);
}
}
result.setProducts(esModels);
List<SearchResult.AttrVo> attrVos = new ArrayList<>();
ParsedNested attr_agg = response.getAggregations().get("attr_agg");
ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");
for (Terms.Bucket bucket : attr_id_agg.getBuckets()) {
SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
Long attrId = bucket.getKeyAsNumber().longValue();
String attrName = ((ParsedStringTerms) bucket.getAggregations().get("attr_name_agg")).getBuckets().get(0).getKeyAsString();
List<String> attrValue = ((ParsedStringTerms) bucket.getAggregations().get("attr_value_agg")).getBuckets().stream().map(item -> {
String keyAsString = item.getKeyAsString();
return keyAsString;
}).collect(Collectors.toList());
attrVo.setAttrId(attrId);
attrVo.setAttrName(attrName);
attrVo.setAttrValue(attrValue);
attrVos.add(attrVo);
}
result.setAttrs(attrVos);
ParsedLongTerms Catalog_agg = response.getAggregations().get("catalog_agg");
List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
List<? extends Terms.Bucket> buckets = Catalog_agg.getBuckets();
for (Terms.Bucket bucket : buckets) {
SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
String keyAsString = bucket.getKeyAsString();
catalogVo.setCatalogId(Long.parseLong(keyAsString));
ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
String catalog_name = catalog_name_agg.getBuckets().get(0).getKeyAsString();
catalogVo.setCatalogName(catalog_name);
catalogVos.add(catalogVo);
}
result.setCatalogs(catalogVos);
List<SearchResult.BrandVo> brandVos = new ArrayList<>();
ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");
for (Terms.Bucket bucket : brand_agg.getBuckets()) {
SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
long brandId = bucket.getKeyAsNumber().longValue();
String brandImg = ((ParsedStringTerms) bucket.getAggregations().get("brand_img_agg")).getBuckets().get(0).getKeyAsString();
String brandname = ((ParsedStringTerms) bucket.getAggregations().get("brand_name_agg")).getBuckets().get(0).getKeyAsString();
brandVo.setBrandName(brandname);
brandVo.setBrandId(brandId);
brandVo.setBrandImg(brandImg);
brandVos.add(brandVo);
}
result.setBrands(brandVos);
long total = hits.getTotalHits().value;
result.setTotal(total);
result.setPageNum(param.getPageNum());
int totalPages = (int) total % EsConstant.PRODUCT_PAGESIZE == 0 ? (int) total / EsConstant.PRODUCT_PAGESIZE : ((int) total / EsConstant.PRODUCT_PAGESIZE + 1);
result.setTotalPages(totalPages);
List<Integer> pageNavs = new ArrayList<>();
for(int i = 1; i <= totalPages; i++) {
pageNavs.add(i);
}
result.setPageNavs(pageNavs);
return result;
}
7.4 页面数据渲染
7.4.1基本数据渲染
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b92MSJWV-1650621164751)(/image-20201105124530345.png)]
遍历后显示结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xFYP8WNv-1650621164751)(/image-20201105124552833.png)]
7.4.3 筛选条件渲染
品牌条件筛选
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kv9hlTiG-1650621164752)(/image-20201105125018666.png)]
分类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-POQcbGcx-1650621164752)(/image-20201105125250519.png)]
属性筛选
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xxVCkSg1-1650621164752)(/image-20201105125718289.png)]
function searchProducts(name, value) {
location.href = replaceAndParamVal(location.href,name,value)
}
function replaceAndParamVal(url, paramName, replaceVal,forceAdd) {
var oUrl = url.toString();
if (oUrl.indexOf(paramName) != -1) {
if(forceAdd) {
var nUrl = "";
if (oUrl.indexOf("?") != -1) {
nUrl = oUrl + "&" + paramName + "=" + replaceVal;
} else {
nUrl = oUrl + "?" + paramName + "=" + replaceVal;
}
return nUrl;
} else {
var re = eval('/(' + paramName + '=)([^&]*)/gi');
var nUrl = oUrl.replace(re, paramName + '=' + replaceVal)
return nUrl;
}
} else {
var nUrl = "";
if (oUrl.indexOf("?") != -1) {
nUrl = oUrl + "&" + paramName + "=" + replaceVal;
} else {
nUrl = oUrl + "?" + paramName + "=" + replaceVal;
}
return nUrl;
}
}
7.4.4 分页数据筛选
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fm5Xd8iH-1650621164752)(/image-20201105125925391.png)]
7.4.5 页面排序功能
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KhRPM6Vf-1650621164752)(/image-20201105130422378.png)]
7.4.6 页面价格筛选
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3KKjNTUt-1650621164752)(/image-20201105131105507.png)]
JS
$("#skuPriceSearchBtn").click(function() {
var from = $("#skuPriceFrom").val();
var to = $("#skuPriceTo").val();
var query = from + "_" + to;
location.href = replaceAndParamVal(location.href,"skuPrice",query);
})
7.4.7 面包屑导航(遇到问题!)
前端页面:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HZQwBZJG-1650621164753)(/image-20201105131701322.png)]
在返回Vo类中 新增了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DwZnsxgq-1650621164753)(/image-20201105131841728.png)]
Controller中 的解析方法中
if (param.getAttrs()!=null && param.getAttrs().size() >0){
List<SearchResult.NavVo> collect = param.getAttrs().stream().map(attr -> {
SearchResult.NavVo navo = new SearchResult.NavVo();
String[] s = attr.split("_");
navo.setNavValue(s[1]);
R r = productFeignService.getAttrInfo(Long.parseLong(s[0]));
result.getAttrIds().add(Long.parseLong(s[0]));
if (r.getCode() == 0) {
AttrResponseVo data = r.getData("attr", new TypeReference<AttrResponseVo>() {
});
navo.setNavName(data.getAttrName());
} else {
navo.setNavName(s[0]);
}
String replace = replaceQueryString(param, attr,"attrs");
navo.setLink("http://search.gulimall.com/list.html?" + replace);
return navo;
}).collect(Collectors.toList());
result.setNavs(collect);
}
if(param.getBrandId() != null && param.getBrandId().size() > 0) {
List<SearchResult.NavVo> navs = result.getNavs();
SearchResult.NavVo navVo = new SearchResult.NavVo();
navVo.setNavName("品牌");
R info = productFeignService.info(param.getBrandId());
if (info.getCode() == 0) {
List<BrandVo> brand = info.getData("brand", new TypeReference<List<BrandVo>>() {
});
StringBuffer buffer = new StringBuffer();
String replace = "";
for (BrandVo brandVo : brand) {
buffer.append(brandVo.getBrandName() + ";");
replace = replaceQueryString(param,brandVo.getBrandId() + "","brandId");
}
navVo.setNavValue(buffer.toString());
navVo.setLink("http://search.gulimall.com/list.html?" + replace);
}
navs.add(navVo);
}
return result;
8、异步 & 线程池
8.1 线程回顾
8.1.1 初始化线程的 4 种方式
1、继承 Thread
2、实现 Runnable
3、实现 Callable 接口 + FutureTask(可以拿到返回结果,可以处理异常)
4、线程池
方式一和方式二 主进程无法获取线程的运算结果,不适合当前场景
方式三:主进程可以获取当前线程的运算结果,但是不利于控制服务器种的线程资源,可以导致服务器资源耗尽
方式四:通过如下两种方式初始化线程池
Executors.newFixedThreadPool(3);
new ThreadPollExecutor(corePoolSize,maximumPoolSize,keepAliveTime,TimeUnit,unit,workQueue,threadFactory,handler);
通过线程池性能稳定,也可以获取执行结果,并捕获异常,但是,在业务复杂情况下,一个异步调用可能会依赖另一个异步调用的执行结果
8.1.2 线程池的 7 大参数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jksNMwUi-1650621164753)(/image-20201105154808826.png)]
运行流程:
1、线程池创建,准备好 core 数量 的核心线程,准备接受任务
2、新的任务进来,用 core 准备好的空闲线程执行
core 满了,就将再进来的任务放入阻塞队列中,空闲的 core 就会自己去阻塞队列获取任务执行- 阻塞队列也满了,就直接开新线程去执行,最大只能开到
max 指定的数量 max 都执行好了,Max-core 数量空闲的线程会在 keepAliveTime 指定的时间后自动销毁,终保持到 core 大小- 如果线程数开到了
max 数量,还有新的任务进来,就会使用 reject 指定的拒绝策略进行处理
3、所有的线程创建都是由指定的 factory 创建的
面试;
一个线程池 core 7、max 20 ,queue 50 100 并发进来怎么分配的 ?
先有 7 个能直接得到运行,接下来 50 个进入队列排队,再多开 13 个继续执行,线程70个被安排上了,剩下30个默认拒绝策略
8.1.3 常见的 4 种线程池
newCacheThreadPool
- 创建一个可缓存的线程池,如果线程池长度超过需要,可灵活回收空闲线程,若无可回收,则新建线程
newFixedThreadPool
- 创建一个指定长度的线程池,可控制线程最大并发数,超出的线程会再队列中等待
newScheduleThreadPool
newSingleThreadExecutor
- 创建一个单线程化的线程池,她只会用唯一的工作线程来执行任务,保证所有任务
8.1.4 开发中为什么使用线程池
- 降低资源的消耗
- 通过重复利用已创建好的线程降低线程的创建和销毁带来的损耗
- 提高响应速度
- 因为线程池中的线程没有超过线程池的最大上限时,有的线程处于等待分配任务的状态,当任务来时无需创建新的线程就能执行
- 提高线程的客观理性
- 线程池会根据当前系统的特点对池内的线程进行优化处理,减少创建和销毁线程带来的系统开销,无限的创建和销毁线程不仅消耗系统资源,还降低系统的稳定性,使用线程池进行统一分配
8.2 CompletableFuture 异步编排
业务场景:
查询商品详情页逻辑比较复杂,有些数据还需要远程调用,必然需要花费更多的时间
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9QlGQzjB-1650621164753)(/image-20201105163535757.png)]
假如商品详情页的每个查询,需要如下标注时间才能完成
那么,用户需要5.5s后才能看到商品相详情页的内容,很显然是不能接受的
如果有多个线程同时完成这 6 步操作,也许只需要 1.5s 即可完成响应
8.2.1 创建异步对象
CompletableFuture 提供了四个静态方法来创建一个异步操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LrWwCymm-1650621164753)(/image-20201105185420349.png)]
1、runXxx 都是没有返回结果的,supplyXxxx都是可以获取返回结果的
2、可以传入自定义的线程池,否则就是用默认的线程池
3、根据方法的返回类型来判断是否该方法是否有返回类型
代码实现:
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main....start.....");
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
}, executor);
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}, executor);
Integer integer = future.get();
System.out.println("main....stop....." + integer);
}
8.2.2 计算完成时回调方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jgyh0hQZ-1650621164753)(/image-20201105185821263.png)]
whenComplete 可以处理正常和异常的计算结果,exceptionally 处理异常情况
whenComplete 和 whenCompleteAsync 的区别
? whenComplete :是执行当前任务的线程继续执行 whencomplete 的任务
? whenCompleteAsync: 是执行把 whenCompleteAsync 这个任务继续提交给线程池来进行执行
方法不以 Async 结尾,意味着 Action 使用相同的线程执行,而 Async 可能会使用其他线程执行(如果是使用相同的线程池,也可能会被同一个线程选中执行)
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 0;
System.out.println("运行结果:" + i);
return i;
}, executor).whenComplete((res,exception) ->{
System.out.println("异步任务成功完成了...结果是:" +res + "异常是:" + exception);
}).exceptionally(throwable -> {
return 10;
});
8.2.3 handle 方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O3WrSKUh-1650621164753)(/image-20201105194503175.png)]
和 complete 一样,可以对结果做最后的处理(可处理异常),可改变返回值
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}, executor).handle((res,thr) ->{
if (res != null ) {
return res * 2;
}
if (thr != null) {
return 0;
}
return 0;
});
8.2.4 线程串行方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9N0aKg8s-1650621164754)(/image-20201105195632819.png)]
thenApply 方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前任物的返回值
thenAccept方法:消费处理结果,接受任务处理结果,并消费处理,无返回结果
thenRun 方法:只要上面任务执行完成,就开始执行 thenRun ,只是处理完任务后,执行 thenRun的后续操作
带有 Async 默认是异步执行的,同之前,
以上都要前置任务完成
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}, executor).thenApplyAsync(res -> {
System.out.println("任务2启动了..." + res);
return "Hello " + res;
}, executor);
String s = future.get();
System.out.println("main....stop....." + s);
8.2.5 两任务组合 - 都要完成
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uPj06UXq-1650621164754)(/image-20210102044028142.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d9ITD9ZF-1650621164754)(/image-20210102044044914.png)]
两个任务必须都完成,触发该任务
thenCombine: 组合两个 future,获取两个 future的返回结果,并返回当前任务的返回值
thenAccpetBoth: 组合两个 future,获取两个 future 任务的返回结果,然后处理任务,没有返回值
runAfterBoth:组合 两个 future,不需要获取 future 的结果,只需要两个 future处理完成任务后,处理该任务,
CompletableFuture<Integer> future01 = CompletableFuture.supplyAsync(() -> {
System.out.println("任务1当前线程:" + Thread.currentThread().getId());
int i = 10 / 4;
System.out.println("任务1结束:" + i);
return i;
}, executor);
CompletableFuture<String> future02 = CompletableFuture.supplyAsync(() -> {
System.out.println("任务2当前线程:" + Thread.currentThread().getId());
System.out.println("任务2结束:");
return "Hello";
}, executor);
CompletableFuture<String> future = future01.thenCombineAsync(future02, (f1, f2) -> {
return f1 + ":" + f2 + "-> Haha";
}, executor);
System.out.println("main....end....." + future.get());
8.2.6 两任务组合 - 一个完成
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y5RcCtqJ-1650621164754)(/image-20201106101904880.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PqiUnc1F-1650621164754)(/image-20201106101918013.png)]
当两个任务中,任意一个future 任务完成时,执行任务
applyToEither;两个任务有一个执行完成,获取它的返回值,处理任务并有新的返回值
acceptEither: 两个任务有一个执行完成,获取它的返回值,处理任务,没有新的返回值
runAfterEither:两个任务有一个执行完成,不需要获取 future 的结果,处理任务,也没有返回值
CompletableFuture<String> future = future01.applyToEitherAsync(future02, res -> {
System.out.println("任务3开始...之前的结果:" + res);
return res.toString() + "->哈哈";
}, executor);
8.2.7 多任务组合
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oBAENdUC-1650621164754)(/image-20201106104031315.png)]
allOf:等待所有任务完成
anyOf:只要有一个任务完成
CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
System.out.println("查询商品的图片信息");
return "hello.jpg";
});
CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
System.out.println("查询商品的属性");
return "黑色+256G";
});
CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("查询商品介绍");
return "华为";
});
CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futureImg, futureAttr, futureDesc);
anyOf.get();
System.out.println("main....end....." + anyOf.get());
都是操作 CompletableFuture 类 更多方法还请参考该类
9、商品详情
9.1 详情数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-js7iI1E3-1650621164755)(/image-20201229121729595.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bvAaxbQR-1650621164755)(/image-20201109080935340.png)]
**需求分析:**通过 skuId 查询出商品的相关信息,图片、标题、价格,属性对应版本等等
9.1.1 返回数据模型抽取
@Data
public class SkuItemVo {
SkuInfoEntity skuInfo;
boolean hasStock = true;
List<SkuImagesEntity> images;
List<SkuItemSalAttrVo> saleAttr;
SpuInfoDescEntity desc;
List<SpuItemAttrGroupVo> groupAttrs;
}
@Data
public class SkuItemSalAttrVo {
private Long attrId;
private String attrName;
private List<AttrValueWithSkuIdVo> attrValues;
}
@Data
public class AttrValueWithSkuIdVo {
private String attrValue;
private String skuIds;
}
9.2 查询详情
@Override
public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
SkuItemVo skuItemVo = new SkuItemVo();
CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
SkuInfoEntity info = getById(skuId);
skuItemVo.setSkuInfo(info);
return info;
}, executor);
CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
List<SkuItemSalAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrBySpuId(res.getSpuId());
skuItemVo.setSaleAttr(saleAttrVos);
}, executor);
CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync(res -> {
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
skuItemVo.setDesc(spuInfoDescEntity);
}, executor);
CompletableFuture<Void> baseFuture = infoFuture.thenAcceptAsync(res -> {
List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrBySpuId(res.getSpuId(), res.getCatalogId());
skuItemVo.setGroupAttrs(attrGroupVos);
}, executor);
CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
skuItemVo.setImages(images);
}, executor);
CompletableFuture.allOf(saleAttrFuture, descFuture, baseFuture, imageFuture).get();
return skuItemVo;
}
获取spu的销售属性组合
<select id="getSaleAttrBySpuId" resultMap="SkuItemSaleAttrVo">
SELECT
ssav.`attr_id` attr_id,
ssav.`attr_name` attr_name,
ssav.`attr_value` ,
GROUP_CONCAT(DISTINCT info.`sku_id`) sku_ids
FROM
`pms_sku_info` info
LEFT JOIN `pms_sku_sale_attr_value` ssav
ON ssav.`sku_id` = info.`sku_id`
WHERE info.`spu_id` = #{spuId}
GROUP BY ssav.`attr_id`,ssav.`attr_name`,ssav.`attr_value`
</select>
获取spu的规格参数信息
<select id="getAttrGroupWithAttrsBySpuId"
resultMap="spuItemAttrGroupVo">
SELECT
pav.`spu_id`,
ag.`attr_group_name` ,
ag.`attr_group_id`,
aar.`attr_id`,
attr.`attr_name`,
pav.`attr_value`
FROM
`pms_attr_group` ag
LEFT JOIN `pms_attr_attrgroup_relation` aar
ON aar.`attr_group_id` = ag.`attr_group_id`
LEFT JOIN `pms_attr` attr
ON attr.`attr_id` = aar.`attr_id`
LEFT JOIN `pms_product_attr_value` pav
ON pav.`attr_id` = attr.`attr_id`
WHERE ag.`catelog_id` = #{catalogId}
AND pav.`spu_id` = #{spuId}
</select>
其他都是通过对应关联的 id 进行查询,就上面两个查询销售属性、规格参数属性 比较难,SQL这块比较难以理解
9.3 sku 组合切换
$(".sku_attr_value").click(function () {
var skus = new Array();
$(this).addClass("clicked")
var curr = $(this).attr("skus").split(",");
skus.push(curr);
$(this).parent().parent().find(".sku_attr_value").removeClass("checked")
$("a[class='sku_attr_value checked']").each(function () {
skus.push($(this).attr("skus").split(","))
})
var filterEle = skus[0]
for (var i = 1; i < skus.length; i++) {
filterEle = $(filterEle).filter(skus[i])
}
console.log(filterEle[0])
location.href = "http://item.gulimall.com/" + filterEle[0] + ".html"
})
$(function () {
$(".sku_attr_value").parent().css({"border": "solid 1px #CCC"});
$("a[class='sku_attr_value checked']").parent().css({"border": "solid 1px red"});
})
10、商品业务 & 认证服务
10.1 环境搭建
- 为登录和注册创建一个服务
- 讲提供的前端放到
templates 目录下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FpyD4QDX-1650621164755)(/image-20201110084252039.png)]
10.2 前端验证码倒计时
定义id 使用 Jquery 触发点击事件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vV2UBBXC-1650621164755)(/image-20201110084521166.png)]
Jquery
$(function () {
$("#sendCode").click(function () {
if ($(this).hasClass("disabled")) {
} else {
$.get("/sms/sendCode?phone=" + $("#phoneNum").val(), function (data) {
if (data.code != 0) {
alert(data.msg)
}
})
timeoutChangeStyle();
}
})
})
var num = 60;
function timeoutChangeStyle() {
$("#sendCode").attr("class", "disabled")
if (num == 0) {
$("#sendCode").text("发送验证码")
num = 60;
$("#sendCode").attr("class", "");
} else {
var str = num + "s 后再次发送"
$("#sendCode").text(str);
setTimeout("timeoutChangeStyle()", 1000);
}
num--;
}
对应效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w91JWMqq-1650621164755)(/image-20201110084733372.png)]
10.3 整合短信验证码
1、短信验证我们选择的是阿里云的短信服务
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rKeaFwR6-1650621164755)(/image-20201110084936446.png)]
2、选择对应短信服务进行开通
在云市场就能看到购买的服务
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1um2ZHSu-1650621164755)(/image-20201110085141506.png)]
3、验证短信功能是否能发送
在购买短信的页面,能进行调试短信
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PF7oO9Md-1650621164756)(/image-20201110085315288.png)]
输入对应手机号,appCode 具体功能不做演示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YkHGdg45-1650621164756)(/image-20201110085348103.png)]
4、使用 Java 测试短信是否能进行发送
往下拉找到对应 Java 代码
注意:
? 服务商提供的接口地址,请求参数都不同,请参考服务商提供的测试代码
@Test
public void contextLoads() {
String host = "http://dingxin.market.alicloudapi.com";
String path = "/dx/sendSms";
String method = "POST";
String appcode = "你自己的AppCode";
Map<String, String> headers = new HashMap<String, String>();
headers.put("Authorization", "APPCODE " + appcode);
Map<String, String> querys = new HashMap<String, String>();
querys.put("mobile", "159xxxx9999");
querys.put("param", "code:1234");
querys.put("tpl_id", "TP1711063");
Map<String, String> bodys = new HashMap<String, String>();
try {
HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
System.out.println(response.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
需要导入对应工具类,参照注释就行
10.4 验证码防刷校验
用户要是一直提交验证码
-
前台:限制一分钟后提交 -
后台:存入redis 如果有就返回
@GetMapping("/sms/sendCode")
@ResponseBody
public R sendCode(@RequestParam("phone") String phone) {
String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
if(!StringUtils.isEmpty(redisCode)) {
long l = Long.parseLong(redisCode.split("_")[1]);
if (System.currentTimeMillis() -l < 60000) {
R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
}
}
String code = UUID.randomUUID().toString().substring(0,5).toUpperCase();
String substring = code+"_" + System.currentTimeMillis();
redisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,substring,10, TimeUnit.MINUTES);
thirdPartFeignService.sendCode(phone,code);
return R.ok();
}
10.5 一步一坑注册页环境
1、编写 vo 接收页面提交
@Data
public class UserRegistVo {
@NotEmpty(message = "用户名必须提交")
@Length(min = 6,max = 18,message = "用户名必须是6-18位字符")
private String userName;
@NotEmpty(message = "密码必须填写")
@Length(min = 6,max = 18,message = "密码必须是6-18位字符")
private String password;
@NotEmpty(message = "手机号码必须提交")
@Pattern(regexp = "^[1]([3-9])[0-9]{9}$",message = "手机格式不正确")
private String phone;
@NotEmpty(message = "验证码必须填写")
private String code;
}
2、页面提交数据与Vo一致
设置 name 属性与 Vo 一致,方便将传递过来的数据转换成 JSON
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HHAJDO9v-1650621164756)(/image-20201110100732631.png)]
3、数据校验
@PostMapping("/regist")
public String regist(@Valid UserRegistVo vo, BindingResult result,
RedirectAttributes redirectAttributes) {
if (result.hasErrors()) {
Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
String code = vo.getCode();
String s = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
if (!StringUtils.isEmpty(s)) {
if(code.equals(s.split("_")[0])) {
redisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
R r = memberFeignService.regist(vo);
if (r.getCode() == 0) {
return "redirect:http://auth.gulimall.com/login.html";
} else {
Map<String, String> errors = new HashMap<>();
errors.put("msg",r.getData(new TypeReference<String>(){}));
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
} else {
Map<String, String> errors = new HashMap<>();
errors.put("code", "验证码错误");
redirectAttributes.addFlashAttribute("code", "验证码错误");
return "redirect:http://auth.gulimall.com/reg.html";
}
} else {
Map<String, String> errors = new HashMap<>();
errors.put("code", "验证码错误");
redirectAttributes.addFlashAttribute("code", "验证码错误");
return "redirect:http://auth.gulimall.com/reg.html";
}
}
4、前端页面接收错误信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tPG2DKIy-1650621164756)(/image-20201110101306173.png)]
5、异常机制 & 用户注册
Controller
@PostMapping("/regist")
public R regist(@RequestBody MemberRegistVo registVo) {
try {
memberService.regist(registVo);
} catch (PhoneExsitException e) {
return R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnume.PHONE_EXIST_EXCEPTION.getMsg());
} catch (UserNameExistException e) {
return R.error(BizCodeEnume.USER_EXIST_EXCEPTION.getCode(),BizCodeEnume.USER_EXIST_EXCEPTION.getMsg());
}
return R.ok();
}
@Override
public void regist(MemberRegistVo registVo) {
MemberDao memberDao = this.baseMapper;
MemberEntity entity = new MemberEntity();
MemberLevelEntity memberLevelEntity = memberLevelDao.getDefaultLevel();
entity.setLevelId(memberLevelEntity.getId());
checkPhoneUnique(registVo.getPhone());
checkUserNameUnique(registVo.getUserName());
entity.setMobile(registVo.getPhone());
entity.setUsername(registVo.getUserName());
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode(registVo.getPassword());
entity.setPassword(encode);
memberDao.insert(entity);
}
@Override
public void checkPhoneUnique(String phone) throws PhoneExsitException {
MemberDao memberDao = this.baseMapper;
Integer mobile = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
if (mobile > 0) {
throw new PhoneExsitException();
}
}
@Override
public void checkUserNameUnique(String username) throws UserNameExistException {
MemberDao memberDao = this.baseMapper;
Integer count = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("username", username));
if (count > 0) {
throw new PhoneExsitException();
}
}
此处引入一个问题
- 密码是直接存入数据库吗? 这样子会导致数据的不安全,
- 引出了使用 MD5进行加密,但是MD5加密后,别人任然可以暴力破解
- 可以使用加盐的方式,将密码加密后,得到一串随机字符,
- 随机字符和密码和进行验证相同结果返回true否则false
至此注册相关结束~
10.6 账号密码登录
1、定义 Vo 接收数据提交
@Data
public class UserLoginVo {
private String loginacct;
private String password;
}
同时需要保证前端页面提交字段与 Vo 类中一致
2、在 Member 服务中编写接口
@Override
public MemberEntity login(MemberLoginVo vo) {
String loginacct = vo.getLoginacct();
String password = vo.getPassword();
MemberDao memberDao = this.baseMapper;
MemberEntity memberEntity = memberDao.selectOne(new QueryWrapper<MemberEntity>()
.eq("username", loginacct).or().
eq("mobile", loginacct));
if (memberDao == null) {
return null;
} else {
String passwordDB = memberEntity.getPassword();
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
boolean matches = passwordEncoder.matches(password, passwordDB);
if(matches) {
return memberEntity;
} else {
return null;
}
}
}
11.7 分布式 Session不共享不同步问题
我们在auth.gulimall.com中保存session,但是网址跳转到 gulimall.com中,取不出auth.gulimall.com中保存的session,这就造成了微服务下的session不同步问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U64mq8xd-1650621164756)(/image-20201111103637615.png)]
1、Session同步解决方案-分布式下session共享问题
同一个服务复制多个,但是session还是只能在一个服务上保存,浏览器也是只能读取到一个服务的session
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JtVmCBwN-1650621164756)(/image-20201111104758917.png)]
2、Session共享问题解决-session复制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CXbklFW2-1650621164757)(/image-20201111104851977.png)]
3、Session共享问题解决-客户端存储
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nt0vTtO0-1650621164757)(/image-20201111104913888.png)]
4、Session共享问题解决-hash一致性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kE3r8zwB-1650621164757)(/image-20201111105039741.png)]
5、Session共享问题解决-统一存储
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cMuNTrCS-1650621164757)(/image-20201111105135178.png)]
11.8 SpringSession整合
1、官网文档 阅读
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TyMcGwa9-1650621164757)(/image-20201111144109273.png)]
2、选择Spring Session文档
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nvfw2nIt-1650621164757)(/image-20201111144350506.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LuUk2BkV-1650621164757)(/image-20201111144438592.png)]
3、开始使用Spring Session
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CLg5TXQ5-1650621164758)(/image-20201111144639786.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4YLlBJAH-1650621164758)(/image-20201111144718176.png)]
2、整合SpringBoot
1、添加Pom.xml依赖
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eRMQYSar-1650621164758)(/image-20201111144914600.png)]
2、application.yml 配置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hIMWZ7tS-1650621164758)(/image-20201111145601673.png)]
3、reids配置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RI70EqM1-1650621164758)(/image-20201111150056671.png)]
4、启动类加上 @EnableRedisHttpSession
@EnableRedisHttpSession
3、自定义 SpringSession 完成 Session 子域共享
CookieSerializer
api文档参考:https://docs.spring.io/spring-session/docs/2.4.1/reference/html5/index.html#api-cookieserializer
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xh78hvsJ-1650621164758)(/image-20210101124234037.png)]
指定redis序列化
文档地址:
https://docs.spring.io/spring-session/docs/2.4.1/reference/html5/index.html#api-redisindexedsessionrepository-config
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tovu4dWz-1650621164758)(/image-20210101124513827.png)]
redis中json序列化
官网文档地址:https://docs.spring.io/spring-session/docs/2.4.1/reference/html5/index.html#samples
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mp9lpJwU-1650621164759)(/image-20210101125216426.png)]
提供的实例:
https://github.com/spring-projects/spring-session/blob/2.4.1/spring-session-samples/spring-session-sample-boot-redis-json/src/main/java/sample/config/SessionConfig.java
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XOs9p0DN-1650621164759)(/image-20210101125303807.png)]
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer CookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULIMALLSESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
4、SpringSession 原理
核心源码是:
-
SessionRepositoryFilter 类下面的 doFilterInternal 方法 -
及那个 request 、response 包装成 SessionRepositoryRequestWrapper [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-omSs53XT-1650621164759)(/image-20201111195249024.png)]
11、单点登录和社交登录
11.1 社交登录
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LtkUv2RF-1650621164759)(/image-20201110124933726.png)]
QQ、微博,github等网站的用户量非常大,别的网站为了简化网站的登陆和注册逻辑,引入社交登录功能
步骤
1、用户点击 QQ按钮
2、引导跳转进 QQ 授权页
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fTaJLvfA-1650621164759)(/image-20201110124945111.png)]
3、用户主动点击授权,跳回之前网页
11.1.1 OAuth2.0
-
**OAuth:**OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们的数据的内容 -
**OAuth2.0:**对于用户相关的 OpenAPI(例如获取用户信息,动态同步,照片,日志,分享等),为了保存用户数据的安全和隐私,第三方网站访问用户数据前都需要显示向用户授权 -
官方版流程 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E55GpfUL-1650621164759)(/oAuth2_01.gif)]
文档地址:
相关流程分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xO5RvQKx-1650621164760)(/image-20201110154532752.png)]
11.2 微博登录准备工作
1、进入微博开放平台
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MyjAUxfo-1650621164760)(/image-20201110154702360.png)]
2、登录微博,进入微连接,选择网站接入
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Avv88la8-1650621164760)(/image-20201110160834589.png)]
3、选择立即接入
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Su60s6V-1650621164760)(/image-20201110161001013.png)]
4、创建自己的应用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N4GpY9R2-1650621164760)(/image-20201110161032203.png)]
5、我们可以在开发阶段
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bT2TeLAG-1650621164760)(/image-20201110161152105.png)]
6、进入高级信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Puc3uNIP-1650621164761)(/image-20201110161407018.png)]
7、添加测试账号
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lpg68T1q-1650621164761)(/image-20201110161451881.png)]
8、进入文档
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b3HaPPu2-1650621164761)(/image-20201110161634486.png)]
11.3 微博登录代码实现
登陆注册流程图
看不清,放大一点
微博登录流程[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rg5tEUSK-1650621164761)(/image-20201231084733753.png)]
注册流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WbFyV1DE-1650621164761)(/image-20201231084909415.png)]
账号密码登录流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GnqGsK0u-1650621164761)(/image-20201231012134722.png)]
手机验证码发送流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8j2pPsKa-1650621164762)(/image-20201231012207446.png)]
11.3.1 查看微博开放平台文档
https://open.weibo.com/wiki/%E6%8E%88%E6%9D%83%E6%9C%BA%E5%88%B6%E8%AF%B4%E6%98%8E
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z4C83ne6-1650621164762)(/image-20201111093019560.png)]
11.3.2 点击微博登录后,跳转到微博授权页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j3vOL4M8-1650621164762)(/image-20201111093153199.png)]
11.3.3 用户授权后调用回调接口,并带上参数code换取AccessToken
@GetMapping("/oauth2.0/weibo/success")
public String weibo(@RequestParam("code") String code) throws Exception {
Map<String, String> map = new HashMap<>();
map.put("client_id", "1133714539");
map.put("client_secret", "f22eb330342e7f8797a7dbe173bd9424");
map.put("grant_type", "authorization_code");
map.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
map.put("code", code);
HttpResponse response = HttpUtils.doPost("https://api.weibo.com",
"/oauth2/access_token",
"post",
new HashMap<>(),
map,
new HashMap<>());
if (response.getStatusLine().getStatusCode() == 200 ){
String json = EntityUtils.toString(response.getEntity());
SocialUser socialUser = JSON.parseObject(json, SocialUser.class);
R r = memberFeignService.OAuthlogin(socialUser);
if (r.getCode() == 0) {
MemberRespVo data = r.getData("data", new TypeReference<MemberRespVo>() {
});
log.info("登录成功:用户:{}",data.toString());
return "redirect:http://gulimall.com";
} else {
return "redirect:http://auth.gulimall.com/login.html";
}
} else {
return "redirect:http://auth.gulimall.com/login.html";
}
return "redirect:http://gulimall.com";
}
11.3.4 拿到AccessToken 请求对应接口拿到信息
@Override
public MemberEntity login(SocialUser vo) {
String uid = vo.getUid();
MemberDao memberDao = this.baseMapper;
MemberEntity memberEntity = memberDao.selectOne(new QueryWrapper<MemberEntity>()
.eq("social_uid", uid));
if (memberEntity != null ){
MemberEntity update = new MemberEntity();
update.setId(memberEntity.getId());
update.setAccessToken(vo.getAccess_token());
update.setExpiresIn(vo.getExpires_in());
memberDao.updateById(update);
memberEntity.setAccessToken(vo.getAccess_token());
memberEntity.setExpiresIn(vo.getExpires_in());
return memberEntity;
} else {
MemberEntity regist = new MemberEntity();
try {
Map<String,String> query = new HashMap<>();
query.put("access_token",vo.getAccess_token());
query.put("uid",vo.getUid());
HttpResponse response = HttpUtils.doGet("https://api.weibo.com/",
"2/users/show.json",
"get",
new HashMap<>(),
query);
if (response.getStatusLine().getStatusCode() == 200){
String json = EntityUtils.toString(response.getEntity());
JSONObject jsonObject = JSON.parseObject(json);
String name = jsonObject.getString("name");
String gender = jsonObject.getString("gender");
regist.setNickname(name);
regist.setGender("m".equals(gender) ? 1 : 0);
}
} catch (Exception e) {
e.printStackTrace();
}
regist.setSocialUid(vo.getUid());
regist.setAccessToken(vo.getAccess_token());
regist.setExpiresIn(vo.getExpires_in());
memberDao.insert(regist);
return regist;
}
}
11.2 SSO(单点登录)
1、什么是SSO
单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的 [1]
2、前置知识
https://gitee.com/xuxueli0323/xxl-sso
3、同域下的单点登录
4、不同域下的单点登录
5、单点登录框架 & 原理演示
XXL-SSO 是一个分布式单点登录框架。只需要登录一次就可以访问所有相互信任的应用系统。 拥有"轻量级、分布式、跨域、Cookie+Token均支持、Web+APP均支持"等特性。现已开放源代码,开箱即用。
首先对整个项目进行:mvn clean package -Dmaven.skip.test=true
xxl-sso-server:
-
8080/xxl-sso-server -
编排:
- ssoserver.com 登陆验证服务器
- client1.com 客户端1
- client2.com 客户端2
先启动xxl-sso-server 然后启动client1
只要 client1 登录成功 client2 就不用进行登录直接登录成功
代码测试:
sso-client
@Controller
public class HelloController {
@Value("${sso.server.url}")
private String ssoServerUrl;
@ResponseBody
@RequestMapping("/hello")
public String hello() {
return "hello";
}
@GetMapping("/employees")
public String employees(Model model, HttpSession session,
@RequestParam(value="token",required = false) String token) {
if (!StringUtils.isEmpty(token)) {
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> forEntity = restTemplate.getForEntity("http://ssoserver.com:8080/userInfo?token=" + token, String.class);
String body = forEntity.getBody();
session.setAttribute("loginUser",body);
}
Object loginUser = session.getAttribute("loginUser");
if (loginUser == null ){
return "redirect:" + ssoServerUrl + "?redirect_url=http://client1.com:8081/employees";
} else {
List<String> emps = new ArrayList<>();
emps.add("张三");
emps.add("李四");
model.addAttribute("emps",emps);
return "list";
}
}
}
sso-server
@Controller
public class LoginController {
@Autowired
StringRedisTemplate redisTemplate;
@ResponseBody
@GetMapping("/userInfo")
public String userInfo(@RequestParam("token") String token) {
String s = redisTemplate.opsForValue().get(token);
return s;
}
@GetMapping("login.html")
public String login(@RequestParam("redirect_url") String url, Model model,
@CookieValue(value = "sso_token",required = false)String sso_token) {
if (!StringUtils.isEmpty(sso_token)) {
return "redirect:" + url + "?token=" + sso_token;
}
model.addAttribute("url",url);
return "login";
}
@PostMapping("/doLogin")
public String doLogin(@RequestParam("username") String username,
@RequestParam("password") String password,
@RequestParam("url") String url,
HttpServletResponse response){
if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) {
String uuid = UUID.randomUUID().toString().replace("-","");
redisTemplate.opsForValue().set(uuid,username);
Cookie token = new Cookie("sso_token",uuid);
response.addCookie(token);
return "redirect:" + url + "?token=" + uuid;
}
return "login";
}
}
6、使用 jwt
11.3 JWT
12、购物车
12.1 购物车需求
1、需求描述:
用户可以在登录状态下将商品添加到购物车**【登录购物车/在线购物车】**
放入数据库
mongodb
放入 redis(采用)
用户可以在未登录状态下将商品添加到购物车**【游客购物车/离线购物车】**
放入 localstorage
cookie
WebSQL
放入 redis (采用)
用户可以使用购物车一起结算下单
用户可以查询自己的购物车
用户可以在购物中修改购买商品的数量
用户可以在购物车中删除商品
选中不选中商品
在购物车中展示商品优惠信息
提示购物车商品价格变化
购物车数据结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CklA2z8V-1650621164762)(/image-20201113110938713.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FVrPllUo-1650621164762)(/image-20201115154652394.png)]
每一个购物项信息,都是一个对象,基本字段包括
skuId:123123,
check:true,
title:"apple phone",
defaultImage:'',
price:4999,
count:1,
totalPrice:4999
skuSaleVo:{}
另外,购物车中不止一条数据,因此最终会是对象的数组
{...},{....},{....}
Redis有5种不同数据结构,这里选择哪一种 比较合适呢? Map<String, List> 首先不同用户应该有独立的购物车。
因此购物车应该以用户的作为key来存储,Value是用户的所有购物车信息。这样看来基本的 k-v 结构就可以了。
但是,我们对购物车中的商品进行增、删、改操作,基本都需要根据商品id进行判断,为了方便后期处理,我们的购物车也应该是 k-v 结构,key 是商品 id, value 才是这个商品的购物车信息。
综上所述,我们的购物车结构是一一个双层Map: Map<String,Map<String,String>>,在redis中类型为 hash
外头key就是用户的id,里面的key就是skuid
redis中:
新增商品:hset gulimall:cart:7 (key)334488[商品skuid] (value)商品的信息
增加商品数量:hincrby shopcar:uuid1024 334477 1
商品总数:hlen shopcar:uuid
全部选择:hgetall shopcar:uuid1024
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1HVYUcpE-1650621164762)( /image-20201115155054434.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vf0Telwb-1650621164763)(/image-20210102202926973.png)]
12.2 Vo编写 & ThreadLocal身份验证
12.2.1 Vo 编写
购物车
public class Cart {
List<CartItem> items;
private Integer countNum;
private Integer countType;
private BigDecimal totalAmount;
private BigDecimal reduce = new BigDecimal("0");;
public Integer getCountType() {
int count = 0;
if (items !=null && items.size() > 0) {
for (CartItem item : items) {
count+=1;
}
}
return count;
}
public BigDecimal getTotalAmount() {
BigDecimal amount = new BigDecimal("0");
if (items != null && items.size() > 0) {
for (CartItem item : items) {
BigDecimal totalPrice = item.getTotalPrice();
amount = amount.add(totalPrice);
}
}
BigDecimal subtract = amount.subtract(getReduce());
return subtract;
}
}
购物车中的实体
public class CartItem {
private Long skuId;
private Boolean check = true;
private String title;
private String image;
private List<String> skuAttr;
private BigDecimal price;
private Integer count;
private BigDecimal totalPrice;
public BigDecimal getTotalPrice() {
return this.price.multiply(new BigDecimal("" + this.count));
}
12.2.3 ThreadLocal 身份验证
需求分析:
代码
**
* 在执行目标方法之前,判断用户的登录状态,并封装给Controller目标请求
* @author gcq
* @Create 2020-11-13
*/
public class CartInterceptor implements HandlerInterceptor {
public static ThreadLocal<UserInfo> threadLocal = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
UserInfo userInfo = new UserInfo();
HttpSession session = request.getSession();
MemberRespVo member = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if (member != null) {
userInfo.setUserId(member.getId());
}
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
String name = cookie.getName();
if (name.equals(CartConstant.TEMP_USER_COOKIE_NAME)) {
userInfo.setUserKey(cookie.getValue());
userInfo.setTempUser(true);
}
}
}
if (StringUtils.isEmpty(userInfo.getUserKey())) {
String uuid = UUID.randomUUID().toString();
userInfo.setUserKey(uuid);
}
threadLocal.set(userInfo);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
UserInfo userInfo = threadLocal.get();
if (!userInfo.isTempUser()) {
Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfo.getUserKey());
cookie.setDomain("gulimall.com");
cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
response.addCookie(cookie);
}
}
}
12.3 添加购物车
在页面点击加入购物车后将商品添加进购物车
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pb0J3B7A-1650621164763)(/image-20201115155956396.png)]
需求分析:
首先需要在页面拿到要提交的参数,skuid,购买数量,并提交后台完成购物车数据添加
后端如何处理这个数据?
- 通过
skuid 远程查询这个商品的信息 - 远程查询sku组合的信息
- 如果购物车中已经有该数据如何进行提交?
- 如果有该数据,那么就行更改,根据 skuid 从 reids 中取出数据
- 将数据转换成对象然后加上对应的数量,再次转换为json存入redis
- 前端页面频繁添加购物车如何解决?
- 请求发送过来后我们重定向到其他的页面用来显示数据,这时候用户刷新的话也是在其他页面进行刷新
$("#addToCartA").click(function() {
var num = $("#numInput").val();
var skuid = $(this).attr("skuId");
location.href="http://cart.gulimall.com/addToCart?skuId=" + skuid + "&num=" + num;
return false
})
请求发送到Controller后
@GetMapping("/addToCart")
public String addToCart(@RequestParam("skuId") Long skuId,
@RequestParam("num") Integer num,
RedirectAttributes res) throws ExecutionException, InterruptedException {
cartService.addToCart(skuId,num);
res.addAttribute("skuId",skuId);
return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
}
Service
@Override
public void addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
String result = (String) cartOps.get(skuId.toString());
if (StringUtils.isEmpty(result)) {
CartItem cartItem = new CartItem();
CompletableFuture<Void> completableFutureSkuInfo = CompletableFuture.runAsync(() -> {
R r = productFeignService.getSkuInfo(skuId);
SkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
cartItem.setCheck(true);
cartItem.setSkuId(skuInfo.getSkuId());
cartItem.setImage(skuInfo.getSkuDefaultImg());
cartItem.setPrice(skuInfo.getPrice());
cartItem.setTitle(skuInfo.getSkuTitle());
cartItem.setCount(num);
}, executor);
CompletableFuture<Void> completableFutureSkuAttr = CompletableFuture.runAsync(() -> {
List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);
cartItem.setSkuAttr(skuSaleAttrValues);
}, executor);
CompletableFuture.allOf(completableFutureSkuInfo,completableFutureSkuAttr).get();
String cartItemJson = JSON.toJSONString(cartItem);
cartOps.put(skuId.toString(),cartItemJson);
} else {
CartItem cartItem = JSON.parseObject(result, CartItem.class);
cartItem.setCount(cartItem.getCount() + num);
String cartItemJson = JSON.toJSONString(cartItem);
cartOps.put(skuId.toString(),cartItemJson);
}
}
解决刷新问题
@GetMapping("/addToCartSuccess.html")
public String addToCartSuccessPage(@RequestParam("skuId") Long skuId,Model model) {
CartItem item = cartService.getCartItem(skuId);
model.addAttribute("item",item);
return "success";
}
12.4 获取合并购物车
12.4.1 需求分析
- 如果用户登录,临时购物车的数据如何显示?
- 将临时购物车的数据合并到用户购物车中
- 如何显示购物车的数据?
- 从 redis 取出数据放到对象中并渲染出来
12.4.2 代码编写
Controller
@GetMapping("/cart.html")
public String cartListPage(Model model) throws ExecutionException, InterruptedException {
Cart cart = cartService.getCart();
model.addAttribute("cart",cart);
return "cartList";
}
Service
拼装 Key 从 redis 中查询数据
临时购物车如果有数据,当前状态是登录,那就将临时购物车的数据合并到当前用户的购物车
@Override
public Cart getCart() throws ExecutionException, InterruptedException {
Cart cart = new Cart();
UserInfo userInfo = CartIntercept.threadLocal.get();
if (userInfo.getUserId() != null) {
String cartKey = CART_PREFIX + userInfo.getUserKey();
String tempCartKey = CART_PREFIX + userInfo.getUserKey();
List<CartItem> cartItems = getCartItems(tempCartKey);
if (cartItems != null ) {
for (CartItem item : cartItems) {
addToCart(item.getSkuId(),item.getCount());
}
clearCart(tempCartKey);
}
List<CartItem> cartItemList = getCartItems(cartKey);
cart.setItems(cartItemList);
} else {
String cartKey = CART_PREFIX + userInfo.getUserKey();
List<CartItem> cartItems = getCartItems(cartKey);
cart.setItems(cartItems);
}
return cart;
}
getCaerItems
private List<CartItem> getCartItems(String cartKey) {
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
List<Object> values = operations.values();
if (values != null) {
List<CartItem> cartItemList = values.stream().map((obj) -> {
String s = (String) obj;
CartItem cartItem = JSON.parseObject(s, CartItem.class);
return cartItem;
}).collect(Collectors.toList());
return cartItemList;
}
return null;
}
12.5 选中购物项 & 改变购物项的数量 & 删除购物项
12.5.1 需求分析:
1、选中购物项
- 在页面中选中购物项后,数据应该要和redis中的数据进行同步
- 页面选中,reids中数据就要更改
2、改变购物项数量
- 购物车中,增加了商品的数量,对应的价格,总价也需要进行改变
3、删除购物项
- 在购物项中删除该条数据,从redis中根据skuid删除这条记录
4、将数据修改或删除后,重新跳转到cart.html 重新加载数据
12.5.2 代码实现
前端页面方法
$(".itemCheck").click(function () {
var skuId = $(this).attr("skuId");
var check = $(this).prop("checked");
location.href = "http://cart.gulimall.com/checkItem?skuId=" + skuId + "&check=" + (check?1:0);
})
$(".countOpsBtn").click(function() {
var skuId = $(this).parent().attr("skuId");
var num = $(this).parent().find(".countOpsNum").text();
location.href = "http://cart.gulimall.com/countItem?skuId=" + skuId + "&num=" + num;
})
var deleteId = 0;
function deleteItem() {
location.href = "http://cart.gulimall.com/deleteItem?skuId=" + deleteId;
}
$(".deleteItemBtn").click(function() {
deleteId = $(this).attr("skuId");
})
Controller
@GetMapping("/checkItem")
public String checkItem(@RequestParam("skuId") Long skuId,
@RequestParam("checked") Integer checked) {
cartService.checkItem(skuId,checked);
return "redirect:http://cart.gulimall.com/cart.html";
}
@GetMapping("/updateItem")
public String updateItem(@RequestParam("skuId") Long skuId,
@RequestParam("count") Integer count) {
cartService.updateItem(skuId,count);
return "redirect:http://cart.gulimall.com/cart.html";
}
@GetMapping("/deleteItem")
public String deleteItem(@RequestParam("skuId") Long skuId) {
cartService.deleteItem(skuId);
return "redirect:http://cart.gulimall.com/cart.html";
}
Service
@Override
public void checkItem(Long skuId, Integer checked) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
String cart = (String) cartOps.get(skuId.toString());
CartItem cartItem = JSON.parseObject(cart, CartItem.class);
cartItem.setCheck(checked == 1 ? true : false);
String jsonStirng = JSON.toJSONString(cartItem);
cartOps.put(skuId.toString(),jsonStirng);
}
@Override
public void updateItem(Long skuId, Integer count) {
CartItem cartItem = getCartItem(skuId);
cartItem.setCount(count);
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
}
@Override
public void deleteItem(Long skuId) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
cartOps.delete(skuId.toString());
}
13、消息队列 - MQ
13.1 RabbitMQ
异步处理
消息发送的时间取决于业务执行的最长的时间
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EldEC72h-1650621164763)(/image-20210104205412477.png)]
应用解耦
原本是需要订单系统直接调用库存系统
只需要将请求发送给消息队列,其他的就不需要去处理了,节省了处理业务逻辑的时间
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WPmiBJ4Z-1650621164763)(/image-20210104210738678.png)]
流量消峰
某一时刻如果请求特别的大,那就先把它放入消息队列,从而达到流量消峰的作用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KdvjP4MO-1650621164763)(/image-20210104210725093.png)]
流程图地址:https://www.processon.com/view/link/5fbda8c35653bb1d54f7077b
13.2 概述
- 大多应用中,可通过消息服务中间件来提升系统异步通信,扩展解耦能力
- 消息服务中两个重要概念:
- 消息代理(message broker) 和 目的地(destination)
- 当消息发送者发送消息后,将由消息代理接管,消息代理保证消息传递到指定目的地
- 消息队列主要有两种形式的目的地
- 队列(Queue):点对点消息通信(point - to - point)
- 主题(topic):发布(publish)/订阅(subscribe)消息通信
- 点对点式:
- 消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,消息读取后被移出队列
- 消息只有唯一的发送者和接受者,单并不是说只能有一个接收者
- 发布订阅式:
- 发送者(发布者)发到消息到主题,多个接收者(订阅者)监听(订阅)这个主题,那么就会在消息到达时同时收到消息
- JMS(Java Message Service) Java消息服务:
- 基于JVM消息代理的规范,ActiveMQ、HornetMQ是JMS的实现
- AMQP(Advanced Message Queuing Protocol)
- 高级消息队列协议,也是一个消息代理的规范,兼容JMS
- RabbitMQ是AMQP的实现
- Spring 支持
- spring - jms提供了对JMS的支持
- spring - rabbit提供了对AMQP的支持
- 需要ConnectionFactory的实现来连接消息代理
- 提供 JmsTemplate、RabbitTemplate 来发送消息
- @JmsListener(JMS)、@RabbitListener(AMQP)注解在方法上监听消息代理发布的消息
- @EnableJms、@EnableRabbit开启支持
- Spring Boot 自动配置
- JmsAutoConfiguration
- RabbitAutoConfiguration
- 市面上的MQ产品
- ActiveMQ、RabbitMQ、RocketMQ,kafka
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZIHKgzK5-1650621164763)(/image-20201116091205853.png)]
13.3 RabbitMQ概念
RabbitMQ简介:
RabbitMQ是一由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现
核心概念
Message
消息,消息是不具名的,它是由消息头和消息体组成,消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括 routing - key (路由键),priority(相对于其他消息的优先权),**delivery - mode(指出该消息可能需要持久性存储)**等
Publisher
消息的生产者,也是一个像交换器发布消息的客户端应用程序
Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列
Exchange有4种类型:direct(默认)、fanout、topic,和heades,不同类型的 Exchange 转发消息的策略有所区别
Queue
消息队列,用来保存消息直到发送给消费者,他是消息的容器,也是消息的重点,一个消息可以投入一个或多个队列,消息一直在队列里面,等待消费者连接到这个队列将其取走
Binding
绑定,用于消息队列和交换器之间的关联,一个绑定就是基于路由键将交换器和消息队列连接起来的规则,所有可以将交换器理解成一个由绑定构成的路由表
Connection
网路连接,比如一个TCP连接
Channel
信道,多路复用连接中的一个独立的双向数据流通道,信道是建立在真实的TCP连接的内的虚拟连接,AMQP 命令都是通过信息到发送出去的,不管是发布消息,订阅队列还是接收消息,这些动作都是通过队列完成,因为对应操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP连接
Consumer
消息的消费者,表示一个消息队列中取得消息的客户端应用程序
Virtual Host
虚拟主机,表示交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个Virtual host本质上就是一个 mini 版的RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。Virtual host 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ默认的vhost是/。
Broker
表示消息队列服务器实体
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z22jzHBn-1650621164764)(/image-20201116100431093.png)]
13.4 Docker 安装RabbitMQ
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management
4369, 25672 (Erlang发现&集群端口)
5672, 5671 (AMQP端口)
15672 (web管理后台端口)
61613, 61614 (STOMP协议端口)
1883, 8883 (MQTT协议端口)
docker update rabbitmq --restart=always
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FymVmxKX-1650621164764)(/image-20201116102734767.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n0rNbivR-1650621164764)(/image-20201116103446291.png)]
13.5 RabbitMQ 运行机制
AMQP 中的消息路由
AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP中增加了 Exchange 和 Binding 的角色 生产者把消息发布到 Exchange 上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送给那个队列
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uDUXhRb0-1650621164764)(/image-20201116104235856.png)]
Exchange 类型
Exchange 分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、tanout、topic、headers header匹配AMQP消息的 header 而不是路由键,headers 交换器和 direct 交换器完全一致,但性能差能多,目前几乎用不到了,所以直接看另外三种类型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sQBfdtpN-1650621164764)(/image-20201116104546717.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u5h47jo3-1650621164764)(/image-20201116104918897.png)]
13.6 RabbitMQ 整合
1、引入 Spring-boot-starter-amqp
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2、application.yml配置
spring:
rabbitmq:
host: 192.168.56.10
port: 5672
virtual-host: /
3、测试RabbitMQ
1、AmqpAdmin:管理组件
@Test
public void contextLoads() {
DirectExchange directExchange = new DirectExchange("hello-java.exchange",true,false);
amqpAdmin.declareExchange(directExchange);
log.info("Exchange[{}]创建成功:","hello-java.exchange");
}
@Test
public void createQueue() {
Queue queue = new Queue("hello-java-queue",true,false,false);
amqpAdmin.declareQueue(queue);
log.info("Queue[{}]:","创建成功");
}
@Test
public void createBinding() {
Binding binding = new Binding("hello-java-queue",
Binding.DestinationType.QUEUE,
"hello-java.exchange",
"hello.java",null);
amqpAdmin.declareBinding(binding);
log.info("Binding[{}]创建成功","hello-java-binding");
}
2、RabbitTemplate:消息发送处理组件
@Autowired
@Test
public void sendMessageTest() {
for(int i = 1; i <=5; i++) {
if(i%2==0) {
OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
reasonEntity.setId(1l);
reasonEntity.setCreateTime(new java.util.Date());
reasonEntity.setName("哈哈");
String msg = "Hello World";
rabbitTemplate.convertAndSend("hello-java.exchange","hello.java",reasonEntity);
} else {
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("hello-java.exchange","hello.java",orderEntity);
}
log.info("消息发送完成{}");
}
}
13.7 RabbitMQ消息确认机制 - 可靠到达
- 保证消息不丢失,可靠抵达,可以使用事务消息,性能下降250倍,为此引入确认机制
- publisher confirmCallback 确认模式
- publisher returnCallback 未投递到 queue 退回
- consumer ack 机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZoFt71PA-1650621164765)(/image-20201116163631107.png)]
可靠抵达 - ConfirmCallback
spring.rabbitmq.publisher-confirms=true
在创建 connectionFactory 的时候设置 PublisherConfirms(true) 选项,开启 confirmcallback 。
CorrelationData 用来表示当前消息唯一性
消息只要被 broker 接收到就会执行 confirmCallback,如果 cluster 模式,需要所有 broker 接收到才会调用 confirmCallback
被 broker 接收到只能表示 message 已经到达服务器,并不能保证消息一定会被投递到目标 queue 里,所以需要用到接下来的 returnCallback
可靠抵达 - ReturnCallback
spring.rabbitmq.publisher-retuns=true
spring.rabbitmq.template.mandatory=true
confirm 模式只能保证消息到达 broker,不能保证消息准确投递到目标 queue 里。在有些模式业务场景下,我们需要保证消息一定要投递到目标 queue 里,此时就需要用到 return 退回模式
这样如果未能投递到目标 queue 里将调用 returnCallback,可以记录下详细到投递数据,定期的巡检或者自动纠错都需要这些数据
可靠抵达 - Ack 消息确认机制
- 消费者获取到消息,成功处理,可以回复Ack给Broker
- basic.ack 用于肯定确认:broker 将移除此消息
- basic.nack 用于否定确认:可以指定 beoker 是否丢弃此消息,可以批量
- basic.reject用于否定确认,同上,但不能批量
- 默认,消息被消费者收到,就会从broker的queue中移除
- 消费者收到消息,默认自动ack,但是如果无法确定此消息是否被处理完成,或者成功处理,我们可以开启手动ack模式
- 消息处理成功,ack(),接受下一条消息,此消息broker就会移除
- 消息处理失败,nack()/reject() 重新发送给其他人进行处理,或者容错处理后ack
- 消息一直没有调用ack/nack方法,brocker认为此消息正在被处理,不会投递给别人,此时客户端断开,消息不会被broker移除,会投递给别人
13.8 RabbitMQ 延时队列(实现定时任务)
场景:
比如未付款的订单,超过一定时间后,系统自动取消订单并释放占有物品
常用解决方案:
Spring的schedule 定时任务轮询数据库
缺点:
消耗系统内存,增加了数据库的压力,存在较大的时间误差
**解决:**rabbitmq的消息TTL和死信Exchange结合
使用场景
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r9uR7qgy-1650621164765)(/image-20201120120737525.png)]
时效问题
上一轮扫描刚好扫描,而这个时候刚好下了订单,就没有扫描到,下一轮扫描的时候,订单还没有过期,等到订单过期后30分钟才被扫描到
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UISojsbz-1650621164765)(/image-20201120120914320.png)]
消息的TTL(Time To Live)
- 消息的TTL 就是消息的存活时间,
- RabbitMQ可以对队列还有消息分别设置TTL
- 对队列设置就是没有消费者连着的保持时间,也可以对每一个消息单独的设置,超过了这个时间我们可以认为这个消息他死了,称之为死信
- 如果队列设置了,消息也设置了,那么会取小,所以一个消息如果被路由到不同的队列中,这个消息死亡时间有可能不一样的(不同队列设置),这里讲的是单个TTL 因为他是实现延时任务的关键,可以通过设置消息的 expiration 字段或者 x-message-ttl 来设置时间两者是一样的效果
Dead Letter Exchange(DLX)
- 一个消息在满足如下条件下,会进死信路由,记住这里是路由不是队列,一个路由可以对应很多队列,(什么是死信)
- 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不被再次放在队列里,被其他消费者使用。(basic.reject/ basic.nack)
- requeue= false上面的消息的TTL到了,消息过期了。
- 队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上
- Dead Letter Exchange其实就是一种普通的exchange, 和创建其他exchange没有两样。只是在某一个设置 Dead Letter Exchange的队列中有消息过期了自动触发消息的转发,发送到Dead Letter Exchange中去。
- 我们既可以控制消息在一段时间后变成死信, 又可以控制变成死信的消息被路由到某一个指定的交换机, 结合C者,其实就可以实现一个延时队列
延时队列实现 - 1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o0AwTJRo-1650621164765)(/image-20201120132805292.png)]
延时队列实现 - 2
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MuPJ5Bp0-1650621164765)(/image-20201120132922164.png)]
代码实现:
下单场景
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KmpFHNe9-1650621164765)(/image-20201120133054368.png)]
模式升级
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NzCEcsLc-1650621164765)(/image-20201120133258725.png)]
代码实现:
SpringBoot可以使用@Bean 来初始化Queue、exchange、Biding等
@RabbitListener(queues = "order.release.order.queue")
public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
System.out.println("收到过期的订单信息,准备关闭订单" + orderEntity.getOrderSn());
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
@Bean
public Queue orderDelayQueue(){
Map<String,Object> map = new HashMap<>();
map.put("x-dead-letter-exchange", "order-event-exchange");
map.put("x-dead-letter-routing-key","order.release.order");
map.put("x-message-ttl",60000);
Queue queue = new Queue("order.delay.queue", true, false, false,map);
return queue;
}
@Bean
public Queue orderReleaseOrderQueue() {
Queue queue = new Queue("order.release.order.queue", true, false, false);
return queue;
}
@Bean
public Exchange orderEventExchange() {
return new TopicExchange("order-event-exchange",true,false);
}
@Bean
public Binding orderCreateOrderBingding(){
return new Binding("order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null);
}
@Bean
public Binding orderReleaseOrderBinding(){
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
}
13.9 如何保证消息可靠性 - 消息丢失 & 消息重复
1、消息丢失
- 消息发送出去,由于网络问题没有抵达服务器
- 做好容错方法(try-Catch) ,发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式
- 做好日志记录,每个消息状态是否都被服务器收到都应该记录
- 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发
- 消息抵达Broker, Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机。
- publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。
- 自动ACK的状态下。消费者收到消息,但没来得及消息然后宕机
- 一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队
2、消息重复
- 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者
- 消息消费失败,由于重试机制,自动又将消息发送出去
- 成功消费,ack时宕机,消息由unack变为ready, Broker又重新发送
- 消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志
- 使用防重表(redis/mysq|) ,发送消息每一 个都有业务的唯一 标识,处理过就不用处理
- rabbitMQ的每一个消息都有redelivered字段, 可以获取是否是被重新投递过来的,而不是第一次投递过来的
3、消息积压
- 消费者积压
- 消费者消费能力不足积压
- 发送者发送流量太大
- 上线更多消费者,进行正常消费
- 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理
14、订单
14.1 订单中心
1、订单中心
电商系列涉及到 3 流,分别为信息流、资金流、物流,而订单系统作为中枢将三者有机的集合起来
订单模块是电商系统的枢纽,在订单这个模块上获取多个模块的数据和信息,同时对这些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息疏通
1、订单构成
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jNgGRyxu-1650621164766)(/image-20201117102129127.png)]
1、用户信息
用户信息包括是用户账号、用户等级、用户的收货地址、收货人、收货人电话、用户账号需要绑定手机号码,但是用户绑定的手机号码不一定是收货信息上的电话。用户可以添加多个收货信息,用户等级信息可以用来和促销系统进行匹配,获取商品折扣,同时用户等级还可以获取积分的奖励等
2、订单基础信息
订单基础信息是订单流转的核心,其包括订单类型,父/子订单、订单编号、订单状态、订单流转时间等
- 订单类型包括实体订单和虚拟订单商品等,这个根据商城商品和服务类型进行区分
- 同时订单都需要做父子订单处理,之前在初创公司一直只有一个订单,没有做父子订单处理后期需
- 要进行拆单的时候就比较麻烦,尤其是多商户商场,和不同仓库商品的时候,父子订单就是为后期做拆单准备的。
- 订单编号不多说了,需要强调的一点是父子订单都需要有订单编号,需要完善的时候可以对订单编号的每个字段进行统一定义和诠释。
- 订单状态记录订单每次流转过程,后面会对订单状态进行单独的说明。
- 订单流转时间需要记录下单时间,支付时间,发货时间,结束时间/关闭时间等等
3、商品信息
商品信息从商品库中获取商品的SKU信息、图片、名称、属性规格、商品单价、商户信息等,从用户
下单行为记录的用户下单数量,商品合计价格等
4、优惠信息
优惠信息记录用户参与过的优惠活动,包括优惠促销活动,比如满减、满赠、秒杀等,用户使用的优
惠卷信息,优惠卷满足条件的优惠卷需要展示出来,另外虚拟币抵扣信息等进行记录
为什么把优惠信息单独拿出来而不放在支付信息里面呢?
因为优惠信息只是记录用户使用的条目,而支付信息需要加入数据进行计算,所以做为区分。
5、支付信息
支付流水单号,这个流水单号是在唤起网关支付后支付通道返回给电商业务平台的支付流水号,财务
通过订单号和流水单号与支付通道进行对账使用。
支付方式用户使用的支付方式,比如微信支付、支付宝支付、钱包支付、快捷支付等。支付方式有时候可能有两个一-余额支付+第三方支付。
商品总金额,每个商品加总后的金额:运费,物流产生的费用;优惠总金额,包括促销活动的优惠金额,
优惠券优惠金额,虚拟积分或者虛拟币抵扣的金額,会员折扣的金额等之和;实付金额,用户实际需要
付款的金额。
用户实付金额=商品总金额+运费 - 优惠总金额
6、物流信息
物流信息包括配送方式,物流公司,物流单号,物流状态,物流状态可以通过第三方接口来 获取和向用户展示物流每个状态节点。
2、订单状态
1、待付款
用户提交订单后,订单进行预下单,目前主流电商网站都会唤起支付,便于用户快速完成支 付,需要注意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超 时后将自动取消订单,订单变更关闭状态。
2、已付款/代发货
用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到WMS系统,仓库进行调动、配货、分拣,出库等操作
3、待收货/已发货
仓库将商品出库后,订单进入物流环节,订单系统需要同步物流信息,便于用户实时熟悉商品的物流状态
4、已完成
用户确认收货后吗,订单交易完成,后续支付则进行计算,如果订单存在问题进入售后状态
5、已取消
付款之前取消订单,包括超时未付款或用户商户取消订单都会产生这种订单状态
6、售后中
用户在付款后申请退款,或商家发货后用户申请退货
售后也同样存在各种状态,当发起售后申请后生成售后订单,售后订单状态为待审核,等待
商家审核,商家审核通过后订单状态变更为待退货,等待用户将商品寄回,商家收货后订单
状态更新为待退款状态,退款到用户原账户后订单状态更新为售后成功。
14.2 订单流程
订单流程是指从订单产生到完成整个流转的过程,从而行程了-套标准流程规则。而不同的产品类型或业务类型在系统中的流程会千差万别,比如上面提到的线上实物订单和虚拟订单的流程,线上实物订单与020订单等,所以需要根据不同的类型进行构建订单流程。不管类型如何订单都包括正向流程和逆向流程,对应的场景就是购买商品和退换货流程,正向流程就是一一个正常的网购步骤:订单生成>支付订单->卖家发货一确认收货>交易成功。而每个步骤的背后,订单是如何在多系统之间交互流转的,
可概括如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0i8t3PQD-1650621164766)(/image-20201117104613032.png)]
1、订单创建与支付
- 订单创建前需要预览订单,选择收货信息等
- 订单创建需要锁定库存,库存有才可创建,否则不能创建
- 订单创建后超时未支付需要解锁库存
- 支付成功后,需要进行拆单,根据商品打包方式,所在仓库,物流等进行拆单
- 支付的每笔流水都需要记录,以待查账
- 订单创建,支付成功等状态都需要给MQ发送消息,方便其他系统感知订阅
2、逆向流程
- 修改订单,用户没有提交订单,可以对订单一些信息进行修改,比如配送信息,优惠信息,及其他一些订单可修改范围的内容,此时只需对数据进行变更即可。
- 订单取消**,用户主动取消订单和用户超时未支付**,两种情况下订单都会取消订单,而超时情况是系统自动关闭订单,所以在订单支付的响应机制上面要做支付的
14.3 幂等性处理
14.4 订单业务
1、搭建环境
在订单服务下准备好页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PzrUHwJJ-1650621164766)(/image-20210105095210202.png)]
可以发现订单结算页,包含以下信息:
1.收货人信息:有更多地址,即有多个收货地址,其中有一个默认收货地址
2.支付方式:货到付款下在线支付,不需要后台提供
3.送货清单:配送方式(不做)及商品列表(根据购物车选中的skuld到数据库中查询)
4.发票:不做
5.优惠:查询用户领取的优惠券(不做)及可用积分(京豆)
1.1、整合SpringSession
1、引入pom
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
2、配置文件添加
Spring:
session:
store-type: redis
3、启动类加注解
@EnableRedisHttpSession
4、修改页面中登录
<li style="border: 0;">
<a th:if="${session.loginUser != null }" class="aa">[[${session.loginUser.nickname}]]</a>
<a th:if="${session.loginUser == null }" href="http://auth.gulimall.com/login.html">你好请登录</a>
</li>
<li>
<a th:if="${session.loginUser == null }" style="color: red;" href="http://auth.gulimall.com/reg.html" class="li_2">免费注册</a>
</li>
1.2、订单登录拦截
任何请求都需要先经过拦截器的验证,才能去执行目标方法,这里是用户是否登录,用户登录了则放行,否则跳转到登陆页面
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
boolean match1 = new AntPathMatcher().match("/order/order/status/**", requestURI);
boolean match2 = new AntPathMatcher().match("/payed/notify", requestURI);
if (match1 || match2) {
return true;
}
MemberRespVo memberRespVo= (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (memberRespVo != null) {
loginUser.set(memberRespVo);
return true;
} else {
request.getSession().setAttribute("msg","请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
2、订单确认页
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bomOYgZQ-1650621164766)(/image-20210105142735857.png)]
根据图片中商品信息抽取成Vo
2.1、抽取Vo
订单确认OrderConfirmVo
public class OrderConfirmVo {
@Getter @Setter
List<MemberAddressVo> address;
@Getter @Setter
List<OrderItemVo> item;
@Getter @Setter
Integer integration;
BigDecimal total;
public BigDecimal getTotal() {
BigDecimal sum = new BigDecimal("0");
if(item != null) {
for (OrderItemVo orderItemVo : item) {
BigDecimal multiply = orderItemVo.getPrice().multiply(new BigDecimal(orderItemVo.getCount().toString()));
sum = sum.add(multiply);
}
}
return sum;
}
@Getter @Setter
Map<Long,Boolean> stocks;
BigDecimal payPrice;
public BigDecimal getPayPrice() {
BigDecimal sum = new BigDecimal("0");
if(item != null) {
for (OrderItemVo orderItemVo : item) {
BigDecimal multiply = orderItemVo.getPrice().multiply(new BigDecimal(orderItemVo.getCount().toString()));
sum = sum.add(multiply);
}
}
return sum;
}
@Setter
private Integer count;
public Integer getCount() {
Integer i = 0;
if (item != null) {
for (OrderItemVo orderItemVo : item) {
i+=orderItemVo.getCount();
}
}
return i;
}
@Getter @Setter
private String orderToken;
}
商品项orderItemVo
@Data
public class OrderItemVo {
private Long skuId;
private Boolean check = true;
private String title;
private String image;
private List<String> skuAttr;
private BigDecimal price;
private Integer count;
private BigDecimal totalPrice;
private BigDecimal weight;
}
用户地址MemberAddressVo
@Data
public class MemberAddressVo {
private Long id;
private Long memberId;
private String name;
private String phone;
private String postCode;
private String province;
private String city;
private String region;
private String detailAddress;
private String areacode;
private Integer defaultStatus;
}
2.2、订单确认页数据查询
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = OrderInterceptor.loginUser.get();
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> memberFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
log.error("address:{}",address);
confirmVo.setAddress(address);
}, executor);
CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> currentUserCartItem = cartFeignServicea.getCurrentUserCartItem();
log.error("currentUserCartItem:{}",currentUserCartItem);
confirmVo.setItem(currentUserCartItem);
}, executor).thenRunAsync(() -> {
List<OrderItemVo> items = confirmVo.getItem();
List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
R data = wareFeignService.hasStock(collect);
List<SkuHasStockVo> hasStockVo = data.getData(new TypeReference<List<SkuHasStockVo>>() {
});
if (hasStockVo != null) {
Map<Long, Boolean> stockMap = hasStockVo.stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, SkuHasStockVo::getHasStock));
confirmVo.setStocks(stockMap);
log.error("stockMap:{}",stockMap);
}
},executor);
Integer integration = confirmVo.getIntegration();
confirmVo.setIntegration(integration);
CompletableFuture.allOf(memberFuture, addressFuture).get();
String token = UUID.randomUUID().toString().replace("-","");
redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId(),token,30, TimeUnit.SECONDS);
confirmVo.setOrderToken(token);
return confirmVo;
3、创建订单
1、OrderWebController
@PostMapping("/submitOrder")
public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes redirectAttributes) {
SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
log.error("======================订单创建成功{}:",responseVo);
if (responseVo.getCode() == 0 ) {
model.addAttribute("submitOrderResp",responseVo);
return "pay";
} else {
String msg = "下单失败";
switch (responseVo.getCode()) {
case 1: msg += "订单信息过期,请刷新后再次提交"; break;
case 2: msg += "订单商品价格发生变化,请确认后再次提交"; break;
case 3: msg += "库存锁定失败,商品库存不足"; break;
}
redirectAttributes.addFlashAttribute("msg",msg);
return "redirect:http://order.gulimall.com/toTrade";
}
}
2、Service
具体业务
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
confirmVoThreadLocal.set(vo);
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
response.setCode(0);
MemberRespVo memberRespVo = LoginInterceptor.loginUser.get();
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
String orderToken = vo.getOrderToken();
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
, Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId())
, orderToken);
if (result == 0L) {
response.setCode(1);
return response;
} else {
OrderCreateTo order = createOrder();
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if(Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
saveOrder(order);
WareSkuLockedVo wareSkuLockedVo = new WareSkuLockedVo();
List<OrderItemVo> lock = order.getOrderItem().stream().map(orderItemEntity -> {
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setCount(orderItemEntity.getSkuQuantity());
orderItemVo.setSkuId(orderItemEntity.getSkuId());
orderItemVo.setTitle(orderItemEntity.getSkuName());
return orderItemVo;
}).collect(Collectors.toList());
wareSkuLockedVo.setOrderSn(order.getOrder().getOrderSn());
wareSkuLockedVo.setLocks(lock);
R r = wareFeignService.orderLockStock(wareSkuLockedVo);
if (r.getCode() == 0) {
response.setOrder(order.getOrder());
response.setCode(0);
rabbitTemplate.convertAndSend("order-event-exchange"
,"order.create.order"
,order.getOrder());
return response;
} else {
response.setCode(3);
return response;
}
} else {
response.setCode(2);
return response;
}
}
}
private OrderCreateTo createOrder() {
OrderCreateTo orderCreateTo = new OrderCreateTo();
String orderSn = IdWorker.getTimeId();
OrderEntity orderEntity = buildOrder(orderSn);
List<OrderItemEntity> itemEntities = builderOrderItems(orderSn);
computPrice(orderEntity,itemEntities);
orderCreateTo.setOrderItem(itemEntities);
orderCreateTo.setOrder(orderEntity);
return orderCreateTo;
}
private OrderEntity buildOrder(String orderSn) {
OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();
MemberRespVo memberRespVo = LoginInterceptor.loginUser.get();
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(orderSn);
orderEntity.setMemberId(memberRespVo.getId());
R fare = wareFeignService.getFare(orderSubmitVo.getAddrId());
FareVo data = fare.getData(new TypeReference<FareVo>() {
});
orderEntity.setFreightAmount(data.getFare());
orderEntity.setReceiverCity(data.getMemberAddressVo().getCity());
orderEntity.setReceiverDetailAddress(data.getMemberAddressVo().getDetailAddress());
orderEntity.setReceiverName(data.getMemberAddressVo().getName());
orderEntity.setReceiverPhone(data.getMemberAddressVo().getPhone());
orderEntity.setReceiverRegion(data.getMemberAddressVo().getRegion());
orderEntity.setReceiverProvince(data.getMemberAddressVo().getProvince());
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
orderEntity.setAutoConfirmDay(7);
return orderEntity;
}
private List<OrderItemEntity> builderOrderItems(String orderSn) {
List<OrderItemVo> currentUserCartItem = cartFeignServicea.getCurrentUserCartItem();
if (currentUserCartItem != null && currentUserCartItem.size() > 0) {
List<OrderItemEntity> collect = currentUserCartItem.stream().map(orderItemVo -> {
OrderItemEntity itemEntity = builderOrderItem(orderItemVo);
itemEntity.setOrderSn(orderSn);
return itemEntity;
}).collect(Collectors.toList());
return collect;
}
return null;
}
private OrderItemEntity builderOrderItem(OrderItemVo cartItem) {
OrderItemEntity itemEntity = new OrderItemEntity();
Long skuId = cartItem.getSkuId();
R spuinfo = productFeignService.getSpuInfoBySkuId(skuId);
SpuInfoVo spuInfoVo = spuinfo.getData(new TypeReference<SpuInfoVo>() {
});
itemEntity.setSpuBrand(spuInfoVo.getBrandId().toString());
itemEntity.setCategoryId(spuInfoVo.getCatalogId());
itemEntity.setSpuId(spuInfoVo.getId());
itemEntity.setSpuName(spuInfoVo.getSpuName());
itemEntity.setSkuId(skuId);
itemEntity.setSkuName(cartItem.getTitle());
itemEntity.setSkuPic(cartItem.getImage());
itemEntity.setSkuPrice(cartItem.getPrice());
String skuAttr = StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(), ";");
itemEntity.setSkuAttrsVals(skuAttr);
itemEntity.setSkuQuantity(cartItem.getCount());
itemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
itemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
itemEntity.setPromotionAmount(new BigDecimal("0"));
itemEntity.setCouponAmount(new BigDecimal("0"));
itemEntity.setIntegrationAmount(new BigDecimal("0"));
BigDecimal origin = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString()));
origin.subtract(itemEntity.getPromotionAmount())
.subtract(itemEntity.getCouponAmount())
.subtract(itemEntity.getIntegrationAmount());
itemEntity.setRealAmount(origin);
return itemEntity;
}
private OrderEntity computPrice(OrderEntity orderEntity, List<OrderItemEntity> itemEntities) {
BigDecimal total = new BigDecimal("0");
BigDecimal coupon = new BigDecimal("0");
BigDecimal integration = new BigDecimal("0");
BigDecimal promotion = new BigDecimal("0");
BigDecimal gift = new BigDecimal("0");
BigDecimal growth = new BigDecimal("0");
for (OrderItemEntity itemEntity : itemEntities) {
coupon = coupon.add(itemEntity.getCouponAmount());
integration = integration.add(itemEntity.getIntegrationAmount());
promotion = promotion.add(itemEntity.getPromotionAmount());
gift = gift.add(new BigDecimal(itemEntity.getGiftIntegration().toString()));
growth = growth.add(new BigDecimal(itemEntity.getGiftGrowth()));
total = total.add(itemEntity.getRealAmount());
}
orderEntity.setTotalAmount(total);
orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
orderEntity.setPromotionAmount(promotion);
orderEntity.setCouponAmount(coupon);
orderEntity.setGrowth(growth.intValue());
orderEntity.setIntegrationAmount(integration);
orderEntity.setIntegration(gift.intValue());
orderEntity.setDeleteStatus(0);
return orderEntity;
}
3、库存自动解锁—>MQ
库存解锁、StockReleaseListener
package com.atguigu.gulimall.ware.listener;
import com.atguigu.common.to.mq.OrderTo;
import com.atguigu.common.to.mq.StockLockedTo;
import com.atguigu.gulimall.ware.service.WareSkuService;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
@Service
@RabbitListener(queues = "stock.release.stock.queue")
public class StockReleaseListener {
@Autowired
WareSkuService wareSkuService;
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo lockedTo, Message message, Channel channel) throws IOException {
System.out.println("收到解锁库存的信息");
try {
wareSkuService.unLockStock(lockedTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
e.printStackTrace();
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
@RabbitHandler
public void handlerOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
System.out.println("订单关闭准备解锁库存......");
try {
wareSkuService.unLockStock(orderTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
e.printStackTrace();
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
Service
@Override
public void unLockStock(StockLockedTo lockedTo) {
StockDetailTo detail = lockedTo.getDetail();
Long detailId = detail.getId();
WareOrderTaskDetailEntity taskDetailEntity = wareOrderTaskDetailService.getById(detailId);
if (taskDetailEntity != null) {
Long id = lockedTo.getId();
WareOrderTaskEntity TaskEntity = wareOrderTaskService.getById(id);
String orderSn = TaskEntity.getOrderSn();
R orderStatus = orderFeignService.getOrderStatus(orderSn);
if (orderStatus.getCode() == 0) {
OrderVo data = orderStatus.getData(new TypeReference<OrderVo>() {
});
if (data == null || data.getStatus() == 4) {
if (taskDetailEntity.getLockStatus() == 1) {
unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);
}
}
} else {
throw new RuntimeException("远败");
}
}
}
4、支付
选择的是支付宝支付,根据老师所提供的素材 alipayTemplate、PayVo、PaySyncVo,引入到项目中进行开发
1、Controller
跳转到支付宝支付页面,支付完成后跳转到支付成功的回调页面
@ResponseBody
@GetMapping(value = "/payOrder",produces = "text/html")
public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
PayVo payvo = orderService.payOrder(orderSn);
String pay = alipayTemplate.pay(payvo);
System.out.println(pay);
return pay;
}
2、Service
@Override
public PayVo payOrder(String orderSn) {
PayVo payVo = new PayVo();
OrderEntity orderEntity = this.getOrderByOrderSn(orderSn);
BigDecimal decimal = orderEntity.getPayAmount().setScale(2, BigDecimal.ROUND_UP);
payVo.setTotal_amount(decimal.toString());
payVo.setOut_trade_no(orderSn);
List<OrderItemEntity> itemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>()
.eq("order_sn", orderSn));
OrderItemEntity itemEntity = itemEntities.get(0);
payVo.setSubject(itemEntity.getSkuName());
payVo.setBody(itemEntity.getSkuAttrsVals());
return payVo;
}
3、支付成功后跳转页面
支付成功后跳转到订单页面
@GetMapping("/memberOrder.html")
public String memberList(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum, Model model) {
Map<String,Object> params = new HashMap<>();
params.put("page",pageNum);
R r = orderFeignService.listwithItem(params);
System.out.println(JSON.toJSONString(r));
if (r.getCode() == 0) {
model.addAttribute("orders",r);
}
return "list";
}
Service
@Override
public PageUtils queryPageWithItem(Map<String, Object> params) {
MemberRespVo memberRespVo = LoginInterceptor.loginUser.get();
IPage<OrderEntity> page = this.page(
new Query<OrderEntity>().getPage(params),
new QueryWrapper<OrderEntity>()
.eq("member_id",memberRespVo.getId())
.orderByDesc("id")
);
List<OrderEntity> records = page.getRecords();
List<OrderEntity> orderEntityList = records.stream().map(item -> {
List<OrderItemEntity> itemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>()
.eq("order_sn", item.getOrderSn()));
item.setOrderEntityItem(itemEntities);
return item;
}).collect(Collectors.toList());
page.setRecords(orderEntityList);
return new PageUtils(page);
}
然后页面渲染数据
支付宝文档地址:
https://opendocs.alipay.com/open/270/105900
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EwJ6qb3p-1650621164766)(/image-20210108143131033.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BP5qw3cI-1650621164767)(/image-20210105093259313.png)]
5、收单
订单:OrderListener
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderListener {
@Autowired
OrderService orderService;
@RabbitHandler
public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
System.out.println("收到过期的订单信息,准备关闭订单:" + orderEntity.getOrderSn());
try {
orderService.closeOrder(orderEntity);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
e.printStackTrace();
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
Service
@Override
public void closeOrder(OrderEntity orderEntity) {
OrderEntity entity = this.getById(orderEntity.getId());
if(entity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()) {
OrderEntity updateOrder = new OrderEntity();
updateOrder.setId(entity.getId());
updateOrder.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(updateOrder);
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(entity,orderTo);
try {
rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
} catch (Exception e) {
e.printStackTrace();
}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UbhywSym-1650621164767)(/image-20210105093437013.png)]
- 订单在支付页,不支付,一直刷新,订单过期了才支付,订单状态改为已支付了,但是库存解锁了。
- 使用支付宝自动收单功能解决。只要一段时间不支付,就不能支付了。
- 由于时延等问题。订单解锁完成,正在解锁库存的时候,异步通知才到
- 网络阻塞问题,订单支付成功的异步通知一直不到达
- 查询订单列表时,ajax获取当前未支付的订单状态,查询订单状态时,再获取一下支付宝此订单的状态
- 其他各种问题
15、幂等性
15.1 什么是幂等性
接口幂等性就是用户对同一操作发起的一次请求和多次请求结果是一致的,不会因为多次点击而产生了副作用,比如支付场景,用户购买了商品,支付扣款成功,但是返回结果的时候出现了网络异常,此时钱已经扣了,用户再次点击按钮,此时就会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。。。这就没有保证接口幂等性
15.2 那些情况需要防止
用户多次点击按钮
用户页面回退再次提交
微服务互相调用,由于网络问题,导致请求失败,feign触发重试机制
其他业务情况
15.3 什么情况下需要幂等
以 SQL 为例,有些操作时天然幂等的
SELECT * FROM table WHERE id =? 无论执行多少次都不会改变状态是天然的幂等
UPDATE tab1 SET col1=1 WHERE col2=2 无论执行成功多少状态都是一致的,也是幂等操作
delete from user where userid=1 多次操作,结果一样,具备幂等性
insert into user(userid,name) values(1,’ a’ ) 如userid为唯一主键,即重复上面的业务,只会插入一条用户记录,具备幂等性
UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,不是幂等的。insert into user(userid,name) values(,a")如userid不是主键,可以重复,那上面业务多次操作,数据都会新增多条,不具备幂等性。
15.4 幂等解决方案
1、token 机制
1、服务端提供了发送 token 的接口,我们在分析业务的时候,哪些业务是存在幂等性问题的,就必须在执行业务前,先获取 token ,服务器会把 token 保存到 redis 中
2、然后调用业务接口请求时, 把 token 携带过去,一般放在请求头部
3、服务器判断 token 是否存在 redis ,存在表示第一次请求,然后删除 token ,继续执行业务
4、如果判断 token 不存在 redis 中,就表示重复操作,直接返回重复标记给 client ,这样就保证了业务代码,不被重复执行
危险性:
1、先删除 token 还是后删除 token:
- 先删除可能导致,业务确实没有执行,重试还得带上之前的 token, 由于防重设计导致,请求还是不能执行
- 后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除掉token,别人继续重试,导致业务被执行两次
- 我们最后设计为先删除 token,如果业务调用失败,就重新获取 token 再次请求
2、Token 获取,比较 和删除 必须是原子性
- redis.get(token),token.equals、redis.del(token),如果说这两个操作都不是原子,可能导致,在高并发下,都 get 同样的数据,判断都成功,继续业务并发执行
- 可以在 redis 使用 lua 脚本完成这个操作
"if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"
2、各种锁机制
1、数据库悲观锁
select * from xxx where id = 1 for update;
for update 查询的时候锁定这条记录 别人需要等待
悲观锁使用的时候一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用,另外需要注意的是,id字段一定是主键或唯一索引,不然可能造成锁表的结果,处理起来会非常麻烦
2、数据库的乐观锁
这种方法适合在更新的场景中
update t_goods set count = count - 1,version = version + 1 where good_id = 2 and version = 1
根据 version 版本,也就是在操作数据库存前先获取当前商品的 version 版本号,然后操作的时候带上 version 版本号,我们梳理下,我们第一次操作库存时,得
到 version 为 1,调用库存服务 version = 2,但返回给订单服务出现了问题,订单服务又一次调用了库存服务,当订单服务传的 version 还是 1,再执行上面的
sql 语句 就不会执行,因为 version 已经变成 2 了,where 条件不成立,这样就保证了不管调用几次,只会真正处理一次,乐观锁主要使用于处理读多写少的问题
3、业务层分布锁
如果多个机器可能在同一时间处理相同的数据,比如多台机器定时任务拿到了相同的数据,我们就可以加分布式锁,锁定此数据,处理完成后后释放锁,获取锁必须先判断这个数据是否被处理过
3、各种唯一约束
1、数据库唯一约束
插入数据,应该按照唯一索引进行插入,比如订单号,相同订单就不可能有两条订单插入,我们在数据库层面防止重复
这个机制利用了数据库的主键唯一约束的特性,解决了 insert场 景时幂等问题,但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键
如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关
2、redis set 防重
很多数据需要处理,只能被处理一次,比如我们可以计算数据的 MD5 将其放入 redis 的
set,每次处理数据,先看这个 MD5 是否已经存在,存在就不处理
4、防重表
使用订单表 orderNo 做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一个事务中,这样就保证了重复请求时,因为去重表有唯一
约束,导致请求失败,避免了幂等性等问题,去重表和业务表应该在同一个库中,这样就保证了在同一个事务,即使业务操作失败,也会把去重表的数据回滚,这
个很好的保证了数据的一致性,
redis防重也算
5、全局请求唯一id
调用接口时,生成一个唯一的id,redis 将数据保存到集合中(去重),存在即处理过,可以使用 nginx 设置每一个请求一个唯一id
proxy_set_header X-Request-Id $Request_id
16、本地事务
16.1 事务的基本性质
数据库事物的几个特性:原子性(Atomiccity)、一致性(Consistetcy)、隔离性或独立性(Isolation)和持久性(Durabilily),简称ACID
- **原子性:**一系列操作整体不可拆分,要么同时成功要么同时失败
- 一致性:数据在业务的前后,业务整体一致
- 转账 A:1000 B:1000 转200 事务成功 A:800 B:1200
- **隔离性:**事物之间需要相互隔离
- **持久性:**一旦事务成功,数据一定会落盘在数据库
在以往的单体应用中,我们多个业务操作使用同一条连接操作不同的表,一旦有异常我们很容易整体回滚
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DJlcsdVp-1650621164767)(/image-20201119115615054.png)]
Business:我们具体的业务代码 Storage:库存业务代码;扣库存 Order:订单业务代码;保存订单 Account:账号业务代码:减账户余额 比如买东西业务,扣库存,下订单,账户扣款,是一个整体;必须同时成功或者失败
一个事务开始,代表以下的所有操作都在同一个连接里面
16.2 事物的隔离级别
- READ UNCOMMITEED(读未提交)
- 该隔离级别的事务会读到其他未提交事务的数据,此现象也称为脏读
- READ COMMITTED(读提交)
- 一个事物可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重复读,复读问题,Oracle 和SQL Server 的默认隔离级别
- REPEATABLE READ(可重复读)
- 该隔离级别是 MySQL 默认的隔离级别,在同一个事物中,SELECT 的结果是事务开始时时间点的状态,因此,同样的 SELECT 操作读到的结果会是一致
- 但是会有幻读一致,MySQL的 InnoDB 引擎可以通过 next-key locks 机制 来避免幻读
- SERIALIZABLE(序列化)
- 在该隔离级别下事务都是串行顺序执行的,MySQL 数据库的 InnoDB 引擎会给读操作隐士加一把读共享锁,从而避免了脏读、不可重复读、幻读等问题
越大的隔离级别,并发能力越低
16.3 事务传播行为
**PROPAGATION_REQUIRED:**如果当前没事事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置时最常使用的配置
**PROPAGATION_SUPPORTS:**支持当前事务。如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行
**PROPAGATION_MANDATORY:**支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在该事务,就抛出异常
**PROPAGATION_REQUIRES_NEW:**创建新事务,无论当前存不存在事务,都创建新事务
**PROPAGATION_NOT_SUPPORTED:**以非事务的方式执行操作,如果当前存在事务,就把当前事务挂起
**PROPAGATION_NEVER:**以非事务方式运行,如果当前存在事务,则抛出异常
**PROPAGATION_NESTED:**如果当前存在事务,就嵌套在该事务内执行,如果当前没有事务,则执行PROPAGATION_REQUIRED相关操作
16.4 SpringBoot 事务关键点
在同一个事物内编写两个方法,内部调用的时候,会导致事务失效,原因是没有用到代理对象的缘故
解决
0)、导入spring-boot-starter-aop 1)、@EnableTransactionManagement(proxyTargetClass= true) 2)、@EnableAspectJAutoProxy(lexposeProxy-true) 3)、AopContext.currentProxy() 调用方法
1、事物的自动配置
2、事物的坑
17、分布式事务
17.1 为什么要有分布式事务
分布式系统经常出现的异常
机器宕机、网络异常、消息丢失、消息乱序、不可靠的TCP、存储数据丢失…
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z2fV2J5W-1650621164767)(/image-20201119170602679.png)]
分布式事务是企业中集成的一个难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎是无法避免的
17.2 CAP 定理与 BASE 理论
1、CAP 定理
CAP 原则又称 CAP 定理指的是在一个分布式系统中
- 一致性(Consistency)
- 在分布式系统中所有数据备份,在同一时刻是否是同样的值,(等同于所有节点访问同一份最新数据的副本)
- 可用性(Avaliability)
- 在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求,(对数据更新具备高可用性)
- 分区容错性(Partition tolerance)
- 大多数分布式系统都分布在多个子网络,每个自网络叫做一个区(partition)。分区容错性的意思是,区间通信可能失败,比如,一台服务器放在中国另一台服务器放在美国,这就是两个区,它们之间可能无法通信
CAP 的原则是,这三个要素最多只能满足两个点,不可能三者兼顾
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IBZwYJBK-1650621164767)(/image-20201119194354467.png)]
一般来说,分区容错无法避免,因此我们可以认为 CAP 的 P 总是成立,CAP 定理告诉我们 剩下的 C 和 A 无法同时做到
分布式实现一致性的 raft 算法
http://thesecretlivesofdata.com/raft/
2、面临的问
对于大多数互联网应用的场景、主机众多、部署分散,而且集群规模越来越大,所以节点故障,网络故障是常态,而且要保证服务可用性达到99.999%,即保证P 和 A,舍弃C
3、BASE 理论
是对CAP的延申,思想即是无法做到强一致性(CAP的一致性就是强一致性),但可以采用适当的弱一致性,即最终一致性
BASE 是指
- 基本可用(Basically Avaliable)
- 基本可用是指分布式系统中在出现故障的时候,允许损失部分可用性(列入响应时间,功能上的可用性)允许损失部分可用性。需要注意的是基本可用不等价于系统不可用
- 响应时间上的损失,正常情况下搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房断电断网故障),查询的结果响应时间增加到了1~2秒
- 功能上的损失,购物网站双十一购物高峰,为了保证系统的稳定性,部分消费者会被引入到一个降级页面
- 软状态(Soft State)
- 软状态是指允许 系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据会有 多个副本,允许不同副本同步的延时就是软状态的体现。mysglreplication的异步复制也是一种体现。
- 最终一致性( Eventual Consistency)
- 最终致性是指系统中的所有数据副本经过一定时间后,最终能够达到一 致的状态。弱一致性和强一致性相反,最终-致性是弱一致性的一种特殊情况。
4、强一致性、弱一致性、最终一致性
从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。如果能容忍后续的部分或者全部访问不到,则是弱一致性。如果经过一段时间后要求能访问到更新后的数据,则是最终一致性
17.3 分布式事务的几种方案
1、2PC 模式
数据库支持的 2PC[2 phase commit 二阶提交],又叫 XA Transactions
MySQL 从5.5版本开始支持,Sql Server 2005 开始支持,oracle7 开始支持
其中,XA 是一个两阶段提交协议,该协议分为两个阶段:
第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反应是否可以提交
第二阶段:事务协调器要求每个数据库提交数据
其中,如果有任何一个数据库否认这次提交,那么所有数据库都会要求回滚他们在此事务中的那部分信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7QoBiOtg-1650621164767)(/image-20201120090516086.png)]
- XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。
- XA性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景
- XA目前在商业数据库支持的比较理想,在mysq!数据库中支持的不太理想,mysgl的
- XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。
- 许多nosgI也没有支持XA,这让XA的应用场景变得非常狭隘。
- 也有3PC,引入了超时机制(无论协调者还是参与者,在向对方发送请求后,若长时间未收到回应则做出相应处理)
2、柔性事务 - TCC 事务
刚性事务:遵循ACID原则,强一致性。 柔性事务:遵循BASE理论,最终一致性; 与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B1y35u3J-1650621164768)(/image-20201120090923734.png)]
一阶段 prepare 行为:调用自定义的 prepare 逻辑。 二阶段 commit 行为:调用自定义的 commit 逻辑。 二阶段 rollback行为:调用自定义的 rollback 逻辑。 所谓TCC模式,是指支持把自定义的分支事务纳入到全局事务的管理中。
3、柔性事务 - 最大努力通知型方案
按规律进行通知,不保证数据定能通知成功, 但会提供可查询操作接口进行核对。这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合MQ进行实现,例如:通过MQ发送http请求,设置最大通知次数。达到通知次数后即不再通知。
案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对账文件),支付宝的支付成功异步回调
4、柔性事务 - 可靠信息 + 最终一致性方案(异步通知型)
实现:业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。
18、支付宝支付
1、进入蚂蚁金服开放平台
https://open.alipay.com/platform/home.htm
2、下载支付宝官方Demo,进行配置和测试
文档地址:
https://openhome.alipay.com/docCenter/docCenter.htm 创建应用对应文档
https://opendocs.alipay.com/open/200/105304 网页移动应用文档
https://opendocs.alipay.com/open/54/cyz7do 相关Demo
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EXQO7Vrp-1650621164768)(/image-20201122123349754.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W1yzhRC8-1650621164768)(/image-20201122114005616.png)]密码
创建应用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fLUWqWIv-1650621164768)(/image-20201122113922427.png)]
3、使用沙箱进行测试
https://openhome.alipay.com/platform/appDaily.htm?tab=info
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6fPAtWmI-1650621164768)(/image-20201122125716179.png)]
4、什么是公钥、私钥、加密、签名和验签?
1、公钥私钥 公钥和私钥是一一个相对概念
它们的公私性是相对于生成者来说的。
一对密钥生成后,保存在生成者手里的就是私钥,生成者发布出去大家用的就是公钥
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YMyAjHw1-1650621164768)(/image-20201122174141878.png)]
5、支付宝支付流程
https://opendocs.alipay.com/open/270/105898
1、引导用户进入到支付宝页面
1、pom.xml
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.9.28.ALL</version>
</dependency>
2、将素材提供的文件引入到项目中
AlipayTemplate、PayVo、PaySyncVo
AlipayTemplate
package com.atguigu.gulimall.order.config;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.atguigu.gulimall.order.vo.PayVo;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {
private String app_id = "2016092200568607";
private String merchant_private_key = "XXX";
private String alipay_public_key = "XXX";
private String notify_url = "http://member.gulimall.com/memberOrder.html";
private String return_url = "http://member.gulimall.com/memberOrder.html";
private String sign_type = "RSA2";
private String charset = "utf-8";
private String timeout = "30m";
private String gatewayUrl = "https://openapi.alipaydev.com/gateway.do";
public String pay(PayVo vo) throws AlipayApiException {
AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,
app_id, merchant_private_key, "json",
charset, alipay_public_key, sign_type);
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setReturnUrl(return_url);
alipayRequest.setNotifyUrl(notify_url);
String out_trade_no = vo.getOut_trade_no();
String total_amount = vo.getTotal_amount();
String subject = vo.getSubject();
String body = vo.getBody();
alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
+ "\"total_amount\":\""+ total_amount +"\","
+ "\"subject\":\""+ subject +"\","
+ "\"body\":\""+ body +"\","
+ "\"timeout_express\":\"" + timeout + "\","
+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
String result = alipayClient.pageExecute(alipayRequest).getBody();
System.out.println("支付宝的响应:"+result);
return result;
}
}
PayVo
package com.atguigu.gulimall.order.vo;
import lombok.Data;
@Data
public class PayVo {
private String out_trade_no;
private String subject;
private String total_amount;
private String body;
}
payAsyncVo
package com.atguigu.gulimall.order.vo;
import lombok.Data;
import lombok.ToString;
import java.util.Date;
@ToString
@Data
public class PayAsyncVo {
private String gmt_create;
private String charset;
private String gmt_payment;
private Date notify_time;
private String subject;
private String sign;
private String buyer_id;
private String body;
private String invoice_amount;
private String version;
private String notify_id;
private String fund_bill_list;
private String notify_type;
private String out_trade_no;
private String total_amount;
private String trade_status;
private String trade_no;
private String auth_app_id;
private String receipt_amount;
private String point_amount;
private String app_id;
private String buyer_pay_amount;
private String sign_type;
private String seller_id;
}
3、编写业务跳转到支付页面
@Controller
public class PayWebController {
@Autowired
AlipayTemplate alipayTemplate;
@Autowired
OrderService orderService;
@ResponseBody
@GetMapping(value = "/payOrder",produces = "text/html")
public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
PayVo payvo = orderService.payOrder(orderSn);
String pay = alipayTemplate.pay(payvo);
System.out.println(pay);
return pay;
}
}
Service
@Override
public PayVo payOrder(String orderSn) {
PayVo payVo = new PayVo();
OrderEntity orderEntity = this.getOrderByOrderSn(orderSn);
BigDecimal decimal = orderEntity.getPayAmount().setScale(2, BigDecimal.ROUND_UP);
payVo.setTotal_amount(decimal.toString());
payVo.setOut_trade_no(orderSn);
List<OrderItemEntity> itemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>()
.eq("order_sn", orderSn));
OrderItemEntity itemEntity = itemEntities.get(0);
payVo.setSubject(itemEntity.getSkuName());
payVo.setBody(itemEntity.getSkuAttrsVals());
return payVo;
}
最后生成页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-526czcUh-1650621164769)(/image-20210108162512457.png)]
2、用户点击付款
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p74BaMK7-1650621164769)(/image-20210108162541917.png)]
3、付款成功后跳转到成功页面
跳转的页面是根据AlipayTemplate定义的回调地址来进行跳转
- notify_url:支付成功异步回调,返回支付成功相关的信息
- return_url:同步通知,支付成功后页面跳转到那里
private String notify_url = "http://member.gulimall.com/memberOrder.html";
private String return_url = "http://member.gulimall.com/memberOrder.html";
这里跳转到的会员服务的订单页面,需要自己处理请求,已在14.4.4.3节完成该功能
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QYrQVts9-1650621164769)(/image-20210108162610017.png)]
1、支付成功后异步回调接口处理
需要有服务器或配置了内网穿透才能接收到该方法
@RestController
public class OrderPayedListener {
@Autowired
AlipayTemplate alipayTemplate;
@Autowired
OrderService orderService;
@PostMapping("/payed/notify")
public String handleAlipayed(PayAsyncVo vo, HttpServletRequest request) throws UnsupportedEncodingException, AlipayApiException {
Map<String,String> params = new HashMap<String,String>();
Map<String,String[]> requestParams = request.getParameterMap();
for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
}
boolean signVerified = AlipaySignature.rsaCheckV1(params
, alipayTemplate.getAlipay_public_key()
, alipayTemplate.getCharset()
, alipayTemplate.getSign_type());
if (signVerified) {
String result = orderService.handleAlipayed(vo);
return result;
} else {
return "error";
}
}
}
Service
@Override
public String handleAlipayed(PayAsyncVo vo) {
PaymentInfoEntity infoEntity = new PaymentInfoEntity();
infoEntity.setOrderSn(vo.getOut_trade_no());
infoEntity.setAlipayTradeNo(vo.getTrade_no());
infoEntity.setPaymentStatus(vo.getTrade_status());
infoEntity.setCallbackTime(vo.getNotify_time());
paymentInfoService.save(infoEntity);
if (vo.getTrade_status().equals("TRADE_SUCCESS") || vo.getTrade_status().equals("TRADE_FINISHED")) {
String outTradeNo = vo.getOut_trade_no();
this.baseMapper.updateOrderStatus(outTradeNo,OrderStatusEnum.PAYED.getCode());
}
return "success";
}
6、内网穿透
1、简介
内网穿透功能可以允许我们使用外网的网址来访问主机 正常的外网需要访问我们项目的流程是:
1、买服务器并且有公网固定IP
2、买域名映射到服务器的IP
3、域名需要进行备案和审核
2、使用场景
1、开发测试(微信、支付宝)
2、智慧互联
3、远程控制
4、私有云
3、内网穿透常用的软件
1、natapp:https://natapp.cn/ 2、续断: www.zhexi.tech 3、花生壳: https://www.oray.com/
19、秒杀
19.1 秒杀业务
秒杀具有瞬间高并发的特点,针对这一特点,必须要做到限流 + 异步 + 缓存(页面静态化) + 独立部署
限流方式:
1、前端限流,一些高并发的网站直接在前端页面开始限流,列如:小米的验证码
2、nginx限流,直接负载部分请求到错误的静态页面,令牌算法、漏斗算法
3、网关限流、限流的过滤器
4、代码中使用分布式信号量
5、rabbitmq限流(能者多劳 channel.basicQos(1))保证发挥服务器的所用性能
19.2 秒杀流程
参考京东秒杀流程
见秒杀流程图
完整流程图地址:https://www.processon.com/view/link/5fbda8c35653bb1d54f7077b
秒杀方式一
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GhhcNhqP-1650621164769)(/image-20210108164514905.png)]
秒杀方式2
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-heDyiYZU-1650621164769)(/image-20210108164555957.png)]
19.3 限流
19.4 秒杀核心业务
20、定时任务
20.1 cron表达式
语法:秒 分 时 日 月 周 年(Spring不支持)
http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kZEqA648-1650621164770)(/image-20201123095003586.png)]
特殊字符:
* ( “所有值”)-用于选择字段中的所有值。例如,分钟字段中的“ ***** ”表示*“每分钟”*。? ( “无特定值”)-当您需要在允许使用字符的两个字段之一中指定某个内容而在另一个不允许的字段中指定某些内容时很有用。例如,如果我希望在某个月的某个特定日期(例如,第10天)触发触发器,但不在乎一周中的哪一天发生,则将“ 10”设置为月字段,以及“?” 在“星期几”字段中。请参阅下面的示例以进行澄清。-- 用于指定范围。例如,小时字段中的“ 10-12”表示*“小时10、11和12”*。, -用于指定其他值。例如,“星期几”字段中的“ MON,WED,FRI”表示*“星期一,星期三和星期五的日子”*。/ -用于指定增量。例如,秒字段中的“ 0/15”表示*“秒0、15、30和45”。秒字段中的“ 5/15”表示“秒5、20、35和50”。您还可以在“ ”字符后指定“ /” -在这种情况下,“ ”等效于在“ /”之前使用“ 0”。每月日期字段中的“ 1/3”表示“从该月的第一天开始每三天触发一次”*。L ( “最后一个”)-在允许使用的两个字段中都有不同的含义。例如,“月”字段中的值“ L”表示*“月的最后一天”,即非January年的1月31日,2月28日。如果单独用于星期几字段,则仅表示“ 7”或“ SAT”。但是,如果在星期几字段中使用另一个值,则表示“该月的最后xxx天”,例如“ 6L”表示“该月的最后一个星期五”*。您还可以指定与该月最后一天的偏移量,例如“ L-3”,这表示日历月的倒数第三天。 使用“ L”选项时,不要指定列表或值的范围很重要,因为这样会导致混淆/意外结果。W ( “工作日”)-用于指定最接近给定日期的工作日(星期一至星期五)。例如,如果您要指定“ 15W”作为“月日”字段的值,则含义是: “离月15日最近的工作日”。因此,如果15号是星期六,则触发器将在14号星期五触发。如果15日是星期日,则触发器将在16日星期一触发。如果15号是星期二,那么它将在15号星期二触发。但是,如果您将“ 1W”指定为月份的值,并且第一个是星期六,则触发器将在第3个星期一触发,因为它不会“跳过”一个月日的边界。仅当月份中的某天是一天,而不是范围或天数列表时,才可以指定“ W”字符。
“ L”和“ W”字符也可以在“月日”字段中组合以产生“ LW”,这表示*“每月的最后一个工作日” *。
# -用于指定每月的第“ XXX”天。例如,“星期几”字段中的值“ 6#3”表示*“该月的第三个星期五”(第6天=星期五,“#3” =该月的第三个星期五*)。其他示例:“ 2#1” =该月的第一个星期一,“ 4#5” =该月的第五个星期三。请注意,如果您指定“#5”,并且该月的指定星期几中没有5个,则该月将不会触发。
法定字符以及月份和星期几的名称不区分大小写。MON 与mon 相同。
20.2 cron 示例
阅读技巧:秒 分 时 日 月 周
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ED3NR1OD-1650621164770)(/image-20201123095544841.png)]
使用谷歌翻译后中文意思是这样的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OcpCMLLM-1650621164770)(/image-20201123095718609.png)]
20.3 SpringBoot整合定时任务
定时任务相关注解:
@EnableAsync
@EnableScheduling
@Async 异步
@Scheduled(cron = "* * * * * ?")
代码:
@Slf4j
@Component
@EnableAsync
@EnableScheduling
public class HelloSchedule {
}
20.4 分布式定时任务
20.5定时任务的问题
20.6 扩展 - 分布式调整
21、秒杀(高并发)系统关注的问题
21.1 服务单一职责
秒杀服务即使自己扛不住压力,挂掉,不要影响别人
21.2 秒杀链接加密
防止恶意攻击模拟秒杀请求,1000次/s攻击 防止连接暴露,自己工作人员,提前秒杀商品
21.3 库存预热 + 快速扣减
提前把数据加载缓存中,
21.4 动静分离
nginx做好动静分离,保证秒杀和商品详情页的动态请求才打到后端的服务集群
使用 CDN 网络,分担服务器压力
21.5 恶意请求拦截
识别非法攻击的请求并进行拦截 ,网管层
21.6 流量错峰
使用各种手段,将流量分担到更大宽度的时间点,比如验证码,加入购物车
21.7 限流 & 熔断 & 降级
前端限流 + 后端限流
限制次数,限制总量,快速失败降级运行,熔断隔离防止雪崩
21.8 队列消峰
1万个请求,每个1000件被秒杀,双11所有秒杀成功的请求,进入队列,慢慢创建订单,扣减库存即可
|