diff --git a/README.md b/README.md index 64b61b9..6024c6f 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,10 @@ -> 1. 文章会优先发布在[Github](https://github.com/youthlql/JavaYouth),其它平台会晚一段时间,文章纠错与更新内容只在Github。如果Github很卡,可以在去[Gitee](https://gitee.com/youthlql/JavaYouth)克隆。 -> 2. 提供在线阅读方式:[个人博客](https://imlql.cn/)。原先的电子书已关闭 +> 1. 提供在线阅读方式:[个人自建博客](https://imlql.cn/),[CSDN博客](https://blog.csdn.net/Youth_lql)。目前图床开了防盗链,只可以在CSDN,自建博客,Github上在线看文章,下载到本地是无法看到图片的。 > 3. 转载须知:转载请注明GitHub出处,让我们一起维护一个良好的技术创作环境。 > 4. 如果你要提交 issue 或者 pr 的话建议到 [Github](https://github.com/youthlql/JavaYouth) 提交。笔者会持续更新,如果对你有所帮助,不妨[Github](https://github.com/youthlql/JavaYouth)点个**Star~**,你的**Star**是我创作的动力。 -> 5. 提供所有文章的本地版本【Typora可直接打开】 +> 5. 提供所有文章的本地版本【Typora可直接打开,可以看到图片】 > 1. 阿里云盘:https://www.aliyundrive.com/s/ZNAPANQg54A > 2. 百度云盘:https://pan.baidu.com/s/1V20nKp4TQai-SVwMAfSZtQ 提取码:4ozs @@ -88,16 +87,6 @@ -# 计算机网络 - -**总结篇** - -[计算机网络-总结-秋招篇](docs/Computer_NetWork/计算机网络-总结.md) - - - -[计算机网络-概述](docs/Computer_NetWork/计算机网络-概述.md) - # 操作系统 @@ -189,17 +178,3 @@ 6. [Dubbo服务调用源码解析](docs/dubbo-sourcecode-v1/07.Dubbo源码系列V1-Dubbo第七节-服务调用源码解析.md) - - - -# Apollo - -[Apollo简单入门](docs/Apollo/Apollo简单入门.md) - -# ElasticSearch - -## 用法 - -1、[ElasticSearch-入门](docs/ElasticSearch/usage/ElasticSearch-入门.md) - -2、[ElasticSearch-进阶](docs/ElasticSearch/usage/ElasticSearch-进阶.md) \ No newline at end of file diff --git a/docs/Apollo/Apollo简单入门.md b/docs/Apollo/Apollo简单入门.md deleted file mode 100644 index 0ba15e7..0000000 --- a/docs/Apollo/Apollo简单入门.md +++ /dev/null @@ -1,1718 +0,0 @@ ---- -title: Apollo简单入门 -tags: - - Apollo - - 分布式配置中心 -categories: - - 分布式配置中心 - - Apollo -keywords: Apollo,配置中心。 -description: Apollo简单入门及和SpringBoot集成。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/apollo.png' -abbrlink: 10d32fba -date: 2020-12-29 11:31:58 ---- - - - -# Apollo简单入门 - -目标 ----- - -1)理解配置中心的概念以及使用场景 - -2)了解主流配置中心 - -3)理解Apollo的功能特性 - -4)掌握Apollo的快速入门方法 - -5) 理解Apollo的工作原理 - -6)能够安装Apollo - -8)理解Apollo的核心概念 - -9)掌握Apollo的项目管理方法 - -9)掌握Apollo的配置管理方法 - -10)掌握Apollo集群管理方法 - -11)理解Apollo的配置发布原理 - -12)掌握SpringBoot 集成Apollo的使用方法 - -13)掌握Apollo生产环境的部署方法 - -14)掌握Apollo灰度发布的方法 - - - -概览 ----- - -### 什么是配置 - -应用程序在启动和运行的时候往往需要读取一些配置信息,配置基本上伴随着应用程序的整个生命周期,比如:数据库连接参数、启动参数等。 - -配置主要有以下几个特点: - -* **配置是独立于程序的只读变量** - * 配置首先是独立于程序的,同一份程序在不同的配置下会有不同的行为 - * 其次,配置对于程序是只读的,程序通过读取配置来改变自己的行为,但是程序不应该去改变配置 -* **配置伴随应用的整个生命周期** - * 配置贯穿于应用的整个生命周期,应用在启动时通过读取配置来初始化,在运行时根据配置调整行为。比如:启动时需要读取服务的端口号、系统在运行过程中需要读取定时策略执行定时任务等。 -* **配置可以有多种加载方式** - * 常见的有程序内部硬编码,配置文件,环境变量,启动参数,基于数据库等 -* **配置需要治理** - * 权限控制:由于配置能改变程序的行为,不正确的配置甚至能引起灾难,所以对配置的修改必须有比较完善的权限控制 - * 不同环境、集群配置管理:同一份程序在不同的环境(开发,测试,生产)、不同的集群(如不同的数据中心)经常需要有不同的配置,所以需要有完善的环境、集群配置管理 - -### 什么是配置中心 - - 传统单体应用存在一些潜在缺陷,如随着规模的扩大,部署效率降低,团队协作效率差,系统可靠性变差,维护困难,新功能上线周期长等,所以迫切需要一种新的架构去解决这些问题,而微服务( microservices )架构正是当下一种流行的解法。 - - 不过,解决一个问题的同时,往往会诞生出很多新的问题,所以微服务化的过程中伴随着很多的挑战,其中一个挑战就是有关服务(应用)配置的。当系统从一个单体应用,被拆分成分布式系统上一个个服务节点后,配置文件也必须跟着迁移(分割),这样配置就分散了,不仅如此,分散中还包含着冗余,如下图: - - - -配置中心将配置从应用中剥离出来,统一管理,优雅的解决了配置的动态变更、持久化、运维成本等问题。 - -应用自身既不需要去添加管理配置接口,也不需要自己去实现配置的持久化,更不需要引入“定时任务”以便降低运维成本。 - - **总得来说,配置中心就是一种统一管理各种应用配置的基础服务组件。** - - 在系统架构中,配置中心是整个微服务基础架构体系中的一个组件,如下图,它的功能看上去并不起眼,无非就是配置的管理和存取,但它是整个微服务架构中不可或缺的一环。 - - - - - - 集中管理配置,那么就要将应用的配置作为一个单独的服务抽离出来了,同理也需要解决新的问题,比如:版本管理(为了支持回滚),权限管理等。 - - 总结一下,在传统巨型单体应用纷纷转向细粒度微服务架构的历史进程中,配置中心是微服务化不可缺少的一个系统组件,在这种背景下中心化的配置服务即配置中心应运而生,一个合格的配置中心需要满足: - -* 配置项容易读取和修改 - -* 添加新配置简单直接 - -* 支持对配置的修改的检视以把控风险 - -* 可以查看配置修改的历史记录 - -* 不同部署环境支持隔离 - - -Apollo简介 ----------- - -### 主流配置中心 - -目前市面上用的比较多的配置中心有:(按开源时间排序) - -1. Disconf - - 2014年7月百度开源的配置管理中心,专注于各种「分布式系统配置管理」的「通用组件」和「通用平台」, 提供统一的「配置管理服务」。目前已经不再维护更新。 - - [https://github.com/knightliao/disconf](https://github.com/knightliao/disconf) - -2. Spring Cloud Config - - 2014年9月开源,Spring Cloud 生态组件,可以和Spring Cloud体系无缝整合。 - - [https://github.com/spring-cloud/spring-cloud-config](https://github.com/spring-cloud/spring-cloud-config) - -3. Apollo - - 2016年5月,携程开源的配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。 - - [https://github.com/ctripcorp/apollo](https://github.com/ctripcorp/apollo) - -4. Nacos - - 2018年6月,阿里开源的配置中心,也可以做DNS和RPC的服务发现。 - - [https://github.com/alibaba/nacos](https://github.com/alibaba/nacos) - - - -#### 功能特性对比 - -由于Disconf不再维护,下面主要对比一下Spring Cloud Config、Apollo和Nacos。 - - - -| 功能点 | Spring Cloud Config | Apollo | Nacos | -| ------------ | ---------------------- | ------------------------ | ------------------------ | -| 配置实时推送 | 支持(Spring Cloud Bus) | 支持(HTTP长轮询1s内) | 支持(HTTP长轮询1s内) | -| 版本管理 | 支持(Git) | 支持 | 支持 | -| 配置回滚 | 支持(Git) | 支持 | 支持 | -| 灰度发布 | 支持 | 支持 | 不支持 | -| 权限管理 | 支持(依赖Git) | 支持 | 不支持 | -| 多集群 | 支持 | 支持 | 支持 | -| 多环境 | 支持 | 支持 | 支持 | -| 监听查询 | 支持 | 支持 | 支持 | -| 多语言 | 只支持Java | 主流语言,提供了Open API | 主流语言,提供了Open API | -| 配置格式校验 | 不支持 | 支持 | 支持 | -| 单机读(QPS) | 7(限流所致) | 9000 | 15000 | -| 单击写(QPS) | 5(限流所致) | 1100 | 1800 | -| 3节点读(QPS) | 21(限流所致) | 27000 | 45000 | -| 3节点写(QPS) | 5限流所致() | 3300 | 5600 | - - - -#### 总结 - -总的来看,Apollo和Nacos相对于Spring Cloud Config的生态支持更广,在配置管理流程上做的更好。Apollo相对于Nacos在配置管理做的更加全面,Nacos则使用起来相对比较简洁,在对性能要求比较高的大规模场景更适合。但对于一个开源项目的选型,项目上的人力投入(迭代进度、文档的完整性)、社区的活跃度(issue的数量和解决速度、Contributor数量、社群的交流频次等),这些因素也比较关键,考虑到Nacos开源时间不长和社区活跃度,所以从目前来看Apollo应该是最合适的配置中心选型。 - - - -### Apollo简介 - - - -**Apollo - A reliable configuration management system** - -[https://github.com/ctripcorp/apollo](https://github.com/ctripcorp/apollo) - -Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用的不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。 - -Apollo包括服务端和客户端两部分: - -服务端基于Spring Boot和Spring Cloud开发,打包后可以直接运行,不需要额外安装Tomcat等应用容器。 - -Java客户端不依赖任何框架,能够运行于所有Java运行时环境,同时对Spring/Spring Boot环境也有较好的支持。 - - - -### Apollo特性 - -基于配置的特殊性,所以Apollo从设计之初就立志于成为一个有治理能力的配置发布平台,目前提供了以下的特性: - -* **统一管理不同环境、不同集群的配置** - * Apollo提供了一个统一界面集中式管理不同环境(environment)、不同集群(cluster)、不同命名空间(namespace)的配置。 - * 同一份代码部署在不同的集群,可以有不同的配置,比如zookeeper的地址等 - * 通过命名空间(namespace)可以很方便地支持多个不同应用共享同一份配置,同时还允许应用对共享的配置进行覆盖 -* **配置修改实时生效(热发布)** - * 用户在Apollo修改完配置并发布后,客户端能实时(1秒)接收到最新的配置,并通知到应用程序 -* **版本发布管理** - * 所有的配置发布都有版本概念,从而可以方便地支持配置的回滚 -* **灰度发布** - * 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例 -* **权限管理、发布审核、操作审计** - * 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。 - * 所有的操作都有审计日志,可以方便地追踪问题 -* **客户端配置信息监控** - * 可以在界面上方便地看到配置在被哪些实例使用 -* **提供Java和.Net原生客户端** - * 提供了Java和.Net的原生客户端,方便应用集成 - * 支持Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便应用使用(需要Spring 3.1.1+) - * 同时提供了Http接口,非Java和.Net应用也可以方便地使用 -* **提供开放平台API** - * Apollo自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、流程治理等特性。不过Apollo出于通用性考虑,不会对配置的修改做过多限制,只要符合基本的格式就能保存,不会针对不同的配置值进行针对性的校验,如数据库用户名、密码,Redis服务地址等 - * 对于这类应用配置,Apollo支持应用方通过开放平台API在Apollo进行配置的修改和发布,并且具备完善的授权和权限控制 - - - -Apollo快速入门 ------------- - -### 执行流程 - - - -操作流程如下: - -1、在Apollo配置中心修改配置 - -2、应用程序通过Apollo客户端从配置中心拉取配置信息 - - 用户通过Apollo配置中心修改或发布配置后,会有两种机制来保证应用程序来获取最新配置:一种是Apollo配置中心会向客户端推送最新的配置;另外一种是Apollo客户端会定时从Apollo配置中心拉取最新的配置,通过以上两种机制共同来保证应用程序能及时获取到配置。 - - - -### 安装Apollo - -#### 运行时环境 - -Java - -* Apollo服务端:1.8+ -* Apollo客户端:1.7+ - -由于需要同时运行服务端和客户端,所以建议安装Java 1.8+。 - -MySQL - -* 版本要求:5.6.5+ - -Apollo的表结构对`timestamp`使用了多个default声明,所以需要5.6.5以上版本。 - - - -#### 下载配置 - -1. 访问Apollo的官方主页获取安装包(我自己学习使用的是1.3版本): - - [https://github.com/ctripcorp/apollo/tags](https://github.com/ctripcorp/apollo/tags) - - - -2. 打开1.3发布链接,下载必须的安装包:[https://github.com/ctripcorp/apollo/releases/tag/v1.3.0](https://github.com/ctripcorp/apollo/releases/tag/v1.3.0)。三个都要下 - - - -解压安装包后将apollo-configservice-1.3.0.jar, apollo-adminservice-1.3.0.jar, apollo-portal-1.3.0.jar放置于apollo目录下 - - - -#### 创建数据库 - -Apollo服务端共需要两个数据库:`ApolloPortalDB`和`ApolloConfigDB`,ApolloPortalDB只需要在生产环境部署一个即可,**而ApolloConfigDB需要在每个环境部署一套。** - -1. 创建ApolloPortalDB,sql脚本下载地址:[https://github.com/ctripcorp/apollo/blob/v1.3.0/scripts/db/migration/configdb/V1.0.0__initialization.sql](https://github.com/ctripcorp/apollo/blob/v1.3.0/scripts/db/migration/configdb/V1.0.0__initialization.sql) - - 以MySQL原生客户端为例:推荐用Navicat直接导入此sql即可 - - source apollo/ApolloPortalDB__initialization.sql - -2. 验证ApolloPortalDB - - 导入成功后,可以通过执行以下sql语句来验证: - -```sql - select `Id`, `Key`, `Value`, `Comment` from `ApolloPortalDB`.`ServerConfig` limit 1; -``` - - -> 注:ApolloPortalDB只需要在生产环境部署一个即可 - -3. 创建ApolloConfigDB,sql脚本下载地址:[https://github.com/ctripcorp/apollo/blob/v1.3.0/scripts/db/migration/configdb/V1.0.0__initialization.sql](https://github.com/ctripcorp/apollo/blob/v1.3.0/scripts/db/migration/configdb/V1.0.0__initialization.sql) - - 以MySQL原生客户端为例: - - source apollo/ApolloConfigDB__initialization.sql - -4. 验证ApolloConfigDB - - 导入成功后,可以通过执行以下sql语句来验证: - -```sql - select `Id`, `Key`, `Value`, `Comment` from `ApolloConfigDB`.`ServerConfig` limit 1; -``` - - - -#### 启动Apollo - -**方法一** - -1. 确保端口未被占用 - - **Apollo默认会启动3个服务,分别使用8070, 8080, 8090端口,请确保这3个端口当前没有被使用** - -2. 启动apollo-configservice,在apollo目录下执行如下命令 - - 可通过-Dserver.port=8080修改默认端口 - -```shell - java -Xms256m -Xmx256m -Dspring.datasource.url=jdbc:mysql://localhost:3306/ApolloConfigDB?characterEncoding=utf8 -Dspring.datasource.username=root -Dspring.datasource.password=pbteach0430 -jar apollo-configservice-1.3.0.jar -``` - - - - -3. 启动apollo-adminservice - - 可通过-Dserver.port=8090修改默认端口 - -```shell - java -Xms256m -Xmx256m -Dspring.datasource.url=jdbc:mysql://localhost:3306/ApolloConfigDB?characterEncoding=utf8 -Dspring.datasource.username=root -Dspring.datasource.password=pbteach0430 -jar apollo-adminservice-1.3.0.jar -``` - - - - -4. 启动apollo-portal - - 可通过-Dserver.port=8070修改默认端口 - -```shell - java -Xms256m -Xmx256m -Ddev_meta=http://localhost:8080/ -Dserver.port=8070 -Dspring.datasource.url=jdbc:mysql://localhost:3306/ApolloPortalDB?characterEncoding=utf8 -Dspring.datasource.username=root -Dspring.datasource.password=pbteach0430 -jar apollo-portal-1.3.0.jar -``` - - - -**方法2** - -1. 也可以使用提供的runApollo.bat快速启动三个服务(修改数据库连接地址,数据库以及密码) - - - - 这里面是一个很简单的脚本 - - - - - -具体代码: - -```bash -echo - -set url="localhost:3306" -set username="root" -set password="123456" - -start "configService" java -Xms256m -Xmx256m -Dapollo_profile=github -Dspring.datasource.url=jdbc:mysql://%url%/ApolloConfigDB?characterEncoding=utf8 -Dspring.datasource.username=%username% -Dspring.datasource.password=%password% -Dlogging.file=.\logs\apollo-configservice.log -jar .\apollo-configservice-1.3.0.jar -start "adminService" java -Xms256m -Xmx256m -Dapollo_profile=github -Dspring.datasource.url=jdbc:mysql://%url%/ApolloConfigDB?characterEncoding=utf8 -Dspring.datasource.username=%username% -Dspring.datasource.password=%password% -Dlogging.file=.\logs\apollo-adminservice.log -jar .\apollo-adminservice-1.3.0.jar -start "ApolloPortal" java -Xms256m -Xmx256m -Dapollo_profile=github,auth -Ddev_meta=http://localhost:8080/ -Dserver.port=8070 -Dspring.datasource.url=jdbc:mysql://%url%/ApolloPortalDB?characterEncoding=utf8 -Dspring.datasource.username=%username% -Dspring.datasource.password=%password% -Dlogging.file=.\logs\apollo-portal.log -jar .\apollo-portal-1.3.0.jar -``` - - - -1. 运行runApollo.bat即可启动Apollo -2. 待启动成功后,访问[管理页面](http://localhost:8070/),初始用户名: apollo,初始密码:admin - - - - -### 代码实现 - -#### 发布配置 - -1. 打开[apollo](http://localhost:8070/) :新建项目apollo-quickstart - - - -2. 新建配置项sms.enable - - - - - - - -确认提交配置项 - - - - - -![image-20201228102339176](https://npm.elemecdn.com/youthlql@1.0.9/Apollo/Simple_Introduction/0012.png) - - - -3. 发布配置项 - - - -#### 应用读取配置 - -1、新建Maven工程 - - - -```java - - - 4.0.0 - - com.pbteach - apollo-quickstart - 1.0-SNAPSHOT - - - 1.8 - - - - - com.ctrip.framework.apollo - apollo-client - 1.1.0 - - - - org.slf4j - slf4j-simple - 1.7.28 - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 8 - 8 - - - - - -``` - - -2、编写测试类GetConfigTest - - - -```java - -public class GetConfigTest { - - // VM options: - // -Dapp.id=apollo-quickstart -Denv=DEV -Ddev_meta=http://localhost:8080 - public static void main(String[] args) { - Config config = ConfigService.getAppConfig(); - String someKey = "sms.enable"; - String value = config.getProperty(someKey, null); - System.out.println("sms.enable: " + value); - } -} -``` - - -3、测试 - -配置VM options,设置系统属性: - - -Dapp.id=apollo-quickstart -Denv=DEV -Ddev_meta=http://localhost:8080 - - - -运行GetConfigTest,打开控制台,观察输出结果 - -``` -sma.enable: true -``` - - - -#### 修改配置 - -1. 到管理页面修改sms.enable的值为false,再运行GetConfigTest,可以看到输出结果已为false - - - -#### 热发布 - -1. 修改代码为每3秒获取一次 - -```java - public class GetConfigTest { - public static void main(String[] args) throws InterruptedException { - Config config = ConfigService.getAppConfig(); - String someKey = "sms.enable"; - while (true) { - String value = config.getProperty(someKey, null); - System.out.printf("now: %s, sms.enable: %s%n", LocalDateTime.now().toString(), value); - Thread.sleep(3000L); - } - } - } -``` - - - -2. 运行GetConfigTest观察输出结果 - - - -3. 在Apollo管理界面修改配置项 - - - -4. 发布配置 - - - -5. 在控制台查看详细情况:可以看到程序获取的sms.enable的值已由false变成了修改后的true - - - - - -Apollo应用 ----------- - -### Apollo工作原理 - -下图是Apollo架构模块的概览 - - - -#### 各模块职责 - -上图简要描述了Apollo的总体设计,我们可以从下往上看: - -* Config Service提供配置的读取、推送等功能,服务对象是Apollo客户端 -* Admin Service提供配置的修改、发布等功能,服务对象是Apollo Portal(管理界面) -* Eureka提供服务注册和发现,为了简单起见,目前Eureka在部署时和Config Service是在一个JVM进程中的 -* Config Service和Admin Service都是多实例、无状态部署,所以需要将自己注册到Eureka中并保持心跳 -* 在Eureka之上架了一层Meta Server用于封装Eureka的服务发现接口 -* Client通过域名访问Meta Server获取Config Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Client侧会做load balance、错误重试 -* Portal通过域名访问Meta Server获取Admin Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Portal侧会做load balance、错误重试 -* 为了简化部署,我们实际上会把Config Service、Eureka和Meta Server三个逻辑角色部署在同一个JVM进程中 - - - -#### 分步执行流程 - -1. Apollo启动后,Config/Admin Service会自动注册到Eureka服务注册中心,并定期发送保活心跳。 -2. Apollo Client和Portal管理端通过配置的Meta Server的域名地址经由Software Load Balancer(软件负载均衡器)进行负载均衡后分配到某一个Meta Server -3. Meta Server从Eureka获取Config Service和Admin Service的服务信息,相当于是一个Eureka Client -4. Meta Server获取Config Service和Admin Service(IP+Port)失败后会进行重试 -5. 获取到正确的Config Service和Admin Service的服务信息后,Apollo Client通过Config Service为应用提供配置获取、实时更新等功能;Apollo Portal管理端通过Admin Service提供配置新增、修改、发布等功能 - - - -### 核心概念 - -1. **application (应用)** - -这个很好理解,就是实际使用配置的应用,Apollo客户端在运行时需要知道当前应用是谁,从而可以去获取对应的配置 - -**关键字:appId** - -2. **environment (环境)** - -配置对应的环境,Apollo客户端在运行时需要知道当前应用处于哪个环境,从而可以去获取应用的配置 - -**关键字:env** - -3. **cluster (集群)** - -一个应用下不同实例的分组,比如典型的可以**按照数据中心分**,把上海机房的应用实例分为一个集群,把北京机房的应用实例分为另一个集群。 - -**关键字:cluster** - -4. **namespace (命名空间)** - -一个应用下不同配置的分组,可以简单地把namespace类比为文件,不同类型的配置存放在不同的文件中,如数据库配置文件,RPC配置文件,应用自身的配置文件等 - -**关键字:namespaces** - -它们的关系如下图所示: - - - - - -### 项目管理 - -#### 基础设置 - -1. 部门管理 - -apollo 默认部门有两个。要增加自己的部门,可在系统参数中修改: - -* 进入系统参数设置 - - - - - -![image-20201228103939300](https://npm.elemecdn.com/youthlql@1.0.9/Apollo/Simple_Introduction/0022.png) - -* 输入key查询已存在的部门设置:organizations - - - -* 修改value值来添加新部门,下面添加一个微服务部门: - - ```json - [{"orgId":"TEST1","orgName":"样例部门1"},{"orgId":"TEST2","orgName":"样例部门2"},{"orgId":"micro_service","orgName":"微服务部门"}] - ``` - - - -2. 添加用户 - -apollo默认提供一个超级管理员: apollo,可以自行添加用户 - -* 新建用户张三 - - - - - - - -#### 创建项目 - -1. 打开apollo-portal主页:[http://localhost:8070/](http://localhost:8070/) - -2. 点击“创建项目”:account-service - - - - -3. 输入项目信息 - - * 部门:选择应用所在的部门 - * 应用AppId:用来标识应用身份的唯一id,格式为string,需要和项目配置文件applications.properties中配置的app.id对应 - * 应用名称:应用名,仅用于界面展示 - * 应用负责人:选择的人默认会成为该项目的管理员,具备项目权限管理、集群创建、Namespace创建等权限 - - - -4. 点击提交,创建成功后,会自动跳转到项目首页 - - - -1. 赋予之前添加的用户张三管理account-service服务的权限 - - * 使用管理员apollo将指定项目授权给用户张三 - - - - * 将修改和发布权限都授权给张三 - - * 使用zhangsan登录,查看项目配置 - - * 点击account-service即可管理配置 - - - -#### 删除项目 - -如果要删除整个项目,点击右上角的“管理员工具–》删除应用、集群…” - -首先查询出要删除的项目,点击“删除应用” - - - - - -### 配置管理 - -下边在account-service项目中进行配置。 - -#### 添加发布配置项 - -1. 通过表格模式添加配置,点击新增配置,输入配置项:sms.enable,点击提交即可。 - -2. 通过文本模式编辑,Apollo除了支持表格模式,逐个添加、修改配置外,还提供文本模式批量添加、修改。 这个对于从已有的properties文件迁移尤其有用 - - - 切换到文本编辑模式 - - 输入配置项,并点击提交修改 - -3. 发布配置 - - - -#### 修改配置 - -1. 找到对应的配置项,点击修改 -2. 修改为需要的值,点击提交 -3. 发布配置 - -#### 删除配置 - -1. 找到需要删除的配置项,点击删除 -2. 确认删除后,点击发布 - - - -#### 添加Namespace - -Namespace作为配置的分类,可当成一个配置文件。 - -以添加rocketmq配置为例,添加“spring-rocketmq” Namespace配置rocketmq相关信息。 - -1. 添加项目私有Namespace:spring-rocketmq - -进入项目首页,点击左下脚的“添加Namespace”,共包括两项:关联公共Namespace和创建Namespace,这里选择“创建Namespace” - - - -2. 添加配置项 - -```properties - rocketmq.name-server = 127.0.0.1:9876 - rocketmq.producer.group = PID_ACCOUNT -``` - -3. 发布配置 - - - -#### 公共配置 - -##### 添加公共Namespace - -在项目开发中,有一些配置可能是通用的,我们可以通过把这些通用的配置放到公共的Namespace中,这样其他项目要使用时可以直接添加需要的Namespace - -1. 新建common-template项目 - - - -2. 添加公共Namespace:spring-boot-http - -进入common-template项目管理页面:[http://localhost:8070/config.html?#/appid=common-template](http://localhost:8070/config.html?#/appid=common-template) - - - - - - - -1. 添加配置项并发布 - - ```properties - spring.http.encoding.enabled = true - spring.http.encoding.charset = UTF-8 - spring.http.encoding.force = true - server.tomcat.remote_ip_header = x-forwarded-for - server.tomcat.protocol_header = x-forwarded-proto - server.use-forward-headers = true - server.servlet.context-path = / - ``` - - - - - -##### 关联公共Namespace - -1. 打开之前创建的account-service项目 -2. 点击左侧的添加Namespace -3. 添加Namespace - - - -4. 根据需求可以覆盖引入公共Namespace中的配置,下面以覆盖server.servlet.context-path为例 - - - -5. 修改server.servlet.context-path为:/account-service -6. 发布修改的配置项 - - - -### 集群管理 - -在有些情况下,应用有需求对不同的集群做不同的配置,比如部署在A机房的应用连接的RocketMQ服务器地址和部署在B机房的应用连接的RocketMQ服务器地址不一样。另外在项目开发过程中,也可为不同的开发人员创建不同的集群来满足开发人员的自定义配置。 - -#### 创建集群 - -1. 点击页面左侧的“添加集群”按钮 -2. 输入集群名称SHAJQ,选择环境并提交:添加上海金桥数据中心为例 - - - - - -![image-20201228112150602](https://npm.elemecdn.com/youthlql@1.0.9/Apollo/Simple_Introduction/0034.png) - -3. 切换到对应的集群,修改配置并发布即可 - - - -#### 同步集群配置 - -同步集群的配置是指在同一个应用中拷贝某个环境下的集群的配置到目标环境下的目标集群。 - -1. 从其他集群同步已有配置到新集群 - - * 切换到原有集群 - - * 展开要同步的Namespace,点击同步配置 - - - - - - - ![image-20201228112603903](https://npm.elemecdn.com/youthlql@1.0.9/Apollo/Simple_Introduction/0037.png) - - * 选择同步到的新集群,再选择要同步的配置 - - - - * 同步完成后,切换到SHAJQ集群,发布配置 - - - -#### 读取配置 - -读取某个集群的配置,需要启动应用时指定具体的应用、环境和集群。 - --Dapp.id=应用名称 - --Denv=环境名称 - --Dapollo.cluster=集群名称 - --D环境_meta=meta地址 - -```bash --Dapp.id=account-service -Denv=DEV -Dapollo.cluster=SHAJQ -Ddev_meta=http://localhost:8080 -``` - - - -### 配置发布原理 - -在配置中心中,一个重要的功能就是配置发布后实时推送到客户端。下面我们简要看一下这块是怎么设计实现的。 - - - -上图简要描述了配置发布的主要过程: - -1. 用户在Portal操作配置发布 -2. Portal调用Admin Service的接口操作发布 -3. Admin Service发布配置后,发送ReleaseMessage给各个Config Service -4. Config Service收到ReleaseMessage后,通知对应的客户端 - - - -#### 发送ReleaseMessage - -Admin Service在配置发布后,需要通知所有的Config Service有配置发布,从而Config Service可以通知对应的客户端来拉取最新的配置。 - -从概念上来看,这是一个典型的消息使用场景,Admin Service作为producer(生产者)发出消息,各个Config Service作为consumer(消费者)消费消息。通过一个消息队列组件(Message Queue)就能很好的实现Admin Service和Config Service的解耦。 - -在实现上,考虑到Apollo的实际使用场景,以及为了尽可能减少外部依赖,我们没有采用外部的消息中间件,而是通过数据库实现了一个简单的消息队列。 - -具体实现方式如下: - -1. Admin Service在配置发布后会往ReleaseMessage表插入一条消息记录,消息内容就是配置发布的AppId+Cluster+Namespace - - ```sql - SELECT * FROM ApolloConfigDB.ReleaseMessage - ``` - - - - - -消息发送类:[DatabaseMessageSende](https://github.com/ctripcorp/apollo/blob/master/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/message/DatabaseMessageSender.java) - -```java -@Override - @Transactional - public void sendMessage(String message, String channel) { - logger.info("Sending message {} to channel {}", message, channel); - if (!Objects.equals(channel, Topics.APOLLO_RELEASE_TOPIC)) { - logger.warn("Channel {} not supported by DatabaseMessageSender!", channel); - return; - } - - Tracer.logEvent("Apollo.AdminService.ReleaseMessage", message); - Transaction transaction = Tracer.newTransaction("Apollo.AdminService", "sendMessage"); - try { - //这里发送消息 - ReleaseMessage newMessage = releaseMessageRepository.save(new ReleaseMessage(message)); - toClean.offer(newMessage.getId()); - transaction.setStatus(Transaction.SUCCESS); - } catch (Throwable ex) { - logger.error("Sending message to database failed", ex); - transaction.setStatus(ex); - throw ex; - } finally { - transaction.complete(); - } - } -``` - - - -2. Config Service有一个线程会每秒扫描一次ReleaseMessage表,看看是否有新的消息记录 - -消息扫描类:[ReleaseMessageScanner](https://github.com/ctripcorp/apollo/blob/master/apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/message/ReleaseMessageScanner.java) - -```java - /** - * scan messages and send - * - * @return whether there are more messages - */ - private boolean scanAndSendMessages() { - //current batch is 500 - List releaseMessages = - releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(maxIdScanned); - if (CollectionUtils.isEmpty(releaseMessages)) { - return false; - } - fireMessageScanned(releaseMessages); - int messageScanned = releaseMessages.size(); - maxIdScanned = releaseMessages.get(messageScanned - 1).getId(); - return messageScanned == 500; - } -``` - - - -3. Config Service如果发现有新的消息记录,那么就会通知到所有的消息监听器 - -```java - /** - * Notify listeners with messages loaded - * @param messages - */ - private void fireMessageScanned(List messages) { - for (ReleaseMessage message : messages) { - for (ReleaseMessageListener listener : listeners) { - try { - listener.handleMessage(message, Topics.APOLLO_RELEASE_TOPIC); - } catch (Throwable ex) { - Tracer.logError(ex); - logger.error("Failed to invoke message listener {}", listener.getClass(), ex); - } - } - } - } -``` - - - -然后调用消息监听类的handleMessage方法:[NotificationControllerV2](https://github.com/ctripcorp/apollo/blob/master/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/NotificationControllerV2.java) - -```java -@Override - public void handleMessage(ReleaseMessage message, String channel) { - logger.info("message received - channel: {}, message: {}", channel, message); - - String content = message.getMessage(); - Tracer.logEvent("Apollo.LongPoll.Messages", content); - if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(content)) { - return; - } - - String changedNamespace = retrieveNamespaceFromReleaseMessage.apply(content); - - if (Strings.isNullOrEmpty(changedNamespace)) { - logger.error("message format invalid - {}", content); - return; - } - - if (!deferredResults.containsKey(content)) { - return; - } - - //create a new list to avoid ConcurrentModificationException - List results = Lists.newArrayList(deferredResults.get(content)); - - ApolloConfigNotification configNotification = new ApolloConfigNotification(changedNamespace, message.getId()); - configNotification.addMessage(content, message.getId()); - ... - ... - ... -``` - - - -4. NotificationControllerV2得到配置发布的AppId+Cluster+Namespace后,会通知对应的客户端 - - - -#### Config Service通知客户端 - -上一节中简要描述了NotificationControllerV2是如何得知有配置发布的,那NotificationControllerV2在得知有配置发布后是如何通知到客户端的呢? - -实现方式如下: - -1. 客户端会发起一个Http请求到Config Service的`notifications/v2`接口[NotificationControllerV2](https://github.com/ctripcorp/apollo/blob/master/apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/NotificationControllerV2.java) - -```java -/** - * @author Jason Song(song_s@ctrip.com) - */ -@RestController -@RequestMapping("/notifications/v2") -public class NotificationControllerV2 implements ReleaseMessageListener { - private static final Logger logger = LoggerFactory.getLogger(NotificationControllerV2.class); - private final Multimap deferredResults = - Multimaps.synchronizedSetMultimap(TreeMultimap.create(String.CASE_INSENSITIVE_ORDER, Ordering.natural())); - private static final Splitter STRING_SPLITTER = - Splitter.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).omitEmptyStrings(); - private static final Type notificationsTypeReference = - new TypeToken>() { - }.getType(); -``` - - - -客户端发送请求类:[RemoteConfigLongPollService](https://github.com/ctripcorp/apollo/blob/master/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/RemoteConfigLongPollService.java) - -```java -private void doLongPollingRefresh(String appId, String cluster, String dataCenter, String secret) { - final Random random = new Random(); - ServiceDTO lastServiceDto = null; - while (!m_longPollingStopped.get() && !Thread.currentThread().isInterrupted()) { - if (!m_longPollRateLimiter.tryAcquire(5, TimeUnit.SECONDS)) { - //wait at most 5 seconds - try { - TimeUnit.SECONDS.sleep(5); - } catch (InterruptedException e) { - } - } - Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "pollNotification"); - String url = null; - try { - if (lastServiceDto == null) { - List configServices = getConfigServices(); - lastServiceDto = configServices.get(random.nextInt(configServices.size())); - } - - url = - assembleLongPollRefreshUrl(lastServiceDto.getHomepageUrl(), appId, cluster, dataCenter, - m_notifications); - - logger.debug("Long polling from {}", url); - - HttpRequest request = new HttpRequest(url); - request.setReadTimeout(LONG_POLLING_READ_TIMEOUT); -``` - - - -2. NotificationControllerV2不会立即返回结果,而是把请求挂起。考虑到会有数万客户端向服务端发起长连,因此在服务端使用了async servlet([Spring DeferredResult](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/context/request/async/DeferredResult.html))来服务Http Long Polling请求。 -3. 如果在60秒内没有该客户端关心的配置发布,那么会返回Http状态码304给客户端。 -4. 如果有该客户端关心的配置发布,NotificationControllerV2会调用DeferredResult的[setResult](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/context/request/async/DeferredResult.html#setResult-T-)方法,传入有配置变化的namespace信息,同时该请求会立即返回。客户端从返回的结果中获取到配置变化的namespace后,会立即请求Config Service获取该namespace的最新配置。 - -#### 客户端读取设计 - -除了之前介绍的客户端和服务端保持一个长连接,从而能第一时间获得配置更新的推送外,**客户端还会定时从Apollo配置中心服务端拉取应用的最新配置**。 - -* 这是一个备用机制,为了防止推送机制失效导致配置不更新 - -* 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified - -* 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: `apollo.refreshInterval`来覆盖,单位为分钟 - - - - -Apollo应用于分布式系统 ----------------- - -在微服务架构模式下,项目往往会切分成多个微服务,下面将以一个金融P2P项目为例演示如何在项目中使用。 - -### 项目场景介绍 - -#### 各微服务介绍 - -本章节仅仅是为了演示配置中心,所以摘取了部分微服务,如下: - -用户中心服务(consumer-service):为借款人和投资人提供用户账户管理服务,包括:注册、开户、充值、提现等 - -UAA认证服务(uaa-service):为用户中心的用户提供认证服务 - -统一账户服务(account-service):对借款人和投资人的登录平台账号进行管理,包括:注册账号、账号权限管理等 - -交易中心(transaction-service):负责P2P平台用户发标和投标功能 - -### Spring Boot应用集成 - -下面以集成统一账户服务(account-service)为例 - -#### 工程依赖 - - - -每个工程必须添加依赖: - -```xml - - com.ctrip.framework.apollo - apollo-client - 1.1.0 - -``` - - -下边是account-service依赖,。 - -```xml - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 2.1.3.RELEASE - - - com.pbteach - account-service - 0.0.1-SNAPSHOT - - - 1.8 - - - - - org.springframework.boot - spring-boot-starter - - - org.springframework.boot - spring-boot-starter-logging - - - - - - org.springframework.boot - spring-boot-starter-log4j2 - - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - com.ctrip.framework.apollo - apollo-client - 1.1.0 - - - - - -``` - - -​ - -#### 必选配置说明 - -1. **AppId**:在Spring Boot application.properties或application.yml中配置 - -application.properties - -```properties - app.id=account-service -``` - - -application.yml - -```yaml - app: - id: account-service -``` - -2. **apollo.bootstrap** - -集成springboot,开启apollo.bootstrap,指定namespace - -例子: - -```properties - apollo.bootstrap.enabled = true - apollo.bootstrap.namespaces = application,micro_service.spring-boot-http,spring-rocketmq,micro_service.spring-boot-druid -``` - - -​ - -3. **Apollo Meta Server** - -Apollo支持应用在不同的环境有不同的配置,常用的指定方式有如下两种: - -* 第一种:通过Java System Property的apollo.meta:`-Dapollo.meta=http://localhost:8080` - -* 第二种:在resources目录下新建apollo-env.properties文件 - - ```properties - dev.meta=http://localhost:8080 - pro.meta=http://localhost:8081 - ``` - - - -4. **本地缓存路径** - -Apollo客户端会把从服务端获取到的配置在本地文件系统缓存一份,用于在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置,不影响应用正常运行。本地配置文件会以下面的文件名格式放置于配置的本地缓存路径下:{appId}+{cluster}+{namespace}.properties - - - -可以通过如下方式指定缓存路径,通过Java System Property的apollo.cacheDir: - -```bash - -Dapollo.cacheDir=/opt/data/apollo-config -``` - -5. **Environment** - -通过Java System Property的env来指定环境:`-Denv=DEV` - -6. **Cluster(集群)** - -通过Java System Property的apollo.cluste来指定集群:`-Dapollo.cluster=DEFAULT` - -也可以选择使用之前新建的SHAJQ集群:`-Dapollo.cluster=SHAJQ` - -**最终完整的VM Options如下**: - -```bash - -Denv=DEV -Dapollo.cacheDir=/opt/data/apollo-config -Dapollo.cluster=DEFAULTbash -``` - - - - - -**完整的主配置文件为:** - -application.properties - -```properties -# 指定读哪个应用的配置(必须写在配置文件里,无法写在apollo里,因为配置文件里如果没有写,都不知道读apollo里的哪个应用) -app.id=account-service -# 下面这个配置是开启Apollo的客户端,使其生效 -apollo.bootstrap.enabled = true - -# namespace以逗号分隔 -apollo.bootstrap.namespaces = application,micro_service.spring-boot-http,spring-rocketmq,micro_service.spring-boot-druid - -server.port=63000 -``` - - - -#### 启用配置 - -在咱们应用的启动类添加`@EnableApolloConfig`注解即可: - -```java -@SpringBootApplication(scanBasePackages = "com.itcast.account") -@EnableApolloConfig -public class AccountApplication { - - public static void main(String[] args) { - SpringApplication.run(AccountApplication.class, args); - } -} -``` - - -#### 应用配置 - -1. 将local-config/account.properties中的配置添加到apollo中 - - ```properties - swagger.enable=true - sms.enable=true - - spring.http.encoding.charset=UTF-8 - spring.http.encoding.force=true - spring.http.encoding.enabled=true - server.use-forward-headers=true - server.tomcat.protocol_header=x-forwarded-proto - server.servlet.context-path=/account-service - server.tomcat.remote_ip_header=x-forwarded-for - - spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver - spring.datasource.druid.stat-view-servlet.allow=127.0.0.1,192.168.163.1 - spring.datasource.druid.web-stat-filter.session-stat-enable=false - spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20 - spring.datasource.druid.max-active=20 - spring.datasource.druid.stat-view-servlet.reset-enable=false - spring.datasource.druid.validation-query=SELECT 1 FROM DUAL - spring.datasource.druid.stat-view-servlet.enabled=true - spring.datasource.druid.web-stat-filter.enabled=true - spring.datasource.druid.stat-view-servlet.url-pattern=/druid/* - spring.datasource.druid.stat-view-servlet.deny=192.168.1.73 - spring.datasource.url=jdbc\:mysql\://127.0.0.1\:3306/p2p_account?useUnicode\=true - spring.datasource.druid.filters=config,stat,wall,log4j2 - spring.datasource.druid.test-on-return=false - spring.datasource.druid.web-stat-filter.profile-enable=true - spring.datasource.druid.initial-size=5 - spring.datasource.druid.min-idle=5 - spring.datasource.druid.max-wait=60000 - spring.datasource.druid.web-stat-filter.session-stat-max-count=1000 - spring.datasource.druid.pool-prepared-statements=true - spring.datasource.druid.test-while-idle=true - spring.datasource.password=pbteach0430 - spring.datasource.username=root - spring.datasource.druid.stat-view-servlet.login-password=admin - spring.datasource.druid.stat-view-servlet.login-username=admin - spring.datasource.druid.web-stat-filter.url-pattern=/* - spring.datasource.druid.time-between-eviction-runs-millis=60000 - spring.datasource.druid.min-evictable-idle-time-millis=300000 - spring.datasource.druid.test-on-borrow=false - spring.datasource.druid.web-stat-filter.principal-session-name=admin - spring.datasource.druid.filter.stat.log-slow-sql=true - spring.datasource.druid.web-stat-filter.principal-cookie-name=admin - spring.datasource.type=com.alibaba.druid.pool.DruidDataSource - spring.datasource.druid.aop-patterns=com.pbteach.wanxinp2p.*.service.* - spring.datasource.druid.filter.stat.slow-sql-millis=1 - spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/* - ``` - - - - - -2. spring-http命名空间在之前已通过关联公共命名空间添加好了,现在来添加spring-boot-druid命名空间 - - - -3. 添加本地文件中的配置到对应的命名空间,然后发布配置 - - - -4. 在account-service/src/main/resources/application.properties中配置apollo.bootstrap.namespaces需要引入的命名空间(上面写过) - - ```properties - # 指定读哪个应用的配置(必须写在配置文件里,无法写在apollo里,因为配置文件里如果没有写,都不知道读apollo里的哪个应用) - app.id=account-service - # 下面这个配置是开启Apollo的客户端,使其生效 - apollo.bootstrap.enabled = true - - # namespace以逗号分隔 - apollo.bootstrap.namespaces = application,micro_service.spring-boot-http,spring-rocketmq,micro_service.spring-boot-druid - - server.port=63000 - ``` - -5.写一个测试Controller - - ```java - -package cn.itcast.account; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class AccountController { - - @Value("${sms.enable}") - private Boolean smsEnable; - - @GetMapping("/hi") - public String hi() { - return "hi"; - } - - @GetMapping("/sms") - public String getSmsConfig() { - return "smsEnable: " + smsEnable; - } - - @Value("${rocketmq.name-server}") - private String mqNameServer; - - @Value("${rocketmq.producer.group}") - private String mqProducerGroup; - - @GetMapping("/mq") - public String getRocketMQConf() { - return mqNameServer + ": " + mqProducerGroup; - } - - @Value("${timeout}") - private Long timeout; - - @GetMapping("/timeout") - public Long getTimeout() { - return timeout; - } - - @GetMapping("/db-url") - public String getDBConfig(@Value("${spring.datasource.url}") String url) { - return url; - } - -} - - ``` - - - -#### 读取配置 - -1. 启动应用 - -2. 访问:[http://127.0.0.1:63000/account-service/hi](http://127.0.0.1:63000/account-service/hi),确认Spring Boot中配置的context-path是否生效 - -通过/account-service能正常访问,说明apollo的配置已生效 - - - -3. 确认spring-boot-druid配置 - -* 为了快速确认可以在AccountController中通过@Value获取来验证 - -* 访问[http://127.0.0.1:63000/account-service/db-url](http://127.0.0.1:63000/account-service/db-url),显示结果 - - - -#### 创建其它项目 - -参考account-service将其它项目也创建完成。 - -### 生产环境部署 - -当一个项目要上线部署到生产环境时,项目的配置比如数据库连接、RocketMQ地址等都会发生变化,这时候就需要通过Apollo为生产环境添加自己的配置。 - -#### 企业部署方案 - -在企业中常用的部署方案为:Apollo-adminservice和Apollo-configservice两个服务分别在线上环境(pro),仿真环境(uat)和开发环境(dev)各部署一套,Apollo-portal做为管理端只部署一套,统一管理上述三套环境。 - -具体如下图所示: - - - -下面以添加生产环境部署为例 - -#### 创建数据库 - -创建生产环境的ApolloConfigDB:**每添加一套环境就需要部署一套ApolloConfgService和ApolloAdminService** - -source apollo/ApolloConfigDB\_PRO\_\_initialization.sql - -#### 配置启动参数 - -1. 设置生产环境数据库连接 - -2. 设置ApolloConfigService端口为:8081,ApolloAdminService端口为8091 - - ```bash - echo - - set url="localhost:3306" - set username="root" - set password="123456" - - start "configService-PRO" java -Dserver.port=8081 -Xms256m -Xmx256m -Dapollo_profile=github -Dspring.datasource.url=jdbc:mysql://%url%/ApolloConfigDBPRO?characterEncoding=utf8 -Dspring.datasource.username=%username% -Dspring.datasource.password=%password% -Dlogging.file=.\logs\apollo-configservice.log -jar .\apollo-configservice-1.3.0.jar - start "adminService-PRO" java -Dserver.port=8091 -Xms256m -Xmx256m -Dapollo_profile=github -Dspring.datasource.url=jdbc:mysql://%url%/ApolloConfigDBPRO?characterEncoding=utf8 -Dspring.datasource.username=%username% -Dspring.datasource.password=%password% -Dlogging.file=.\logs\apollo-adminservice.log -jar .\apollo-adminservice-1.3.0.jar - ``` - - - -1. 运行runApollo-PRO.bat【关于这个bat的说明在上面已经说过】 - -#### 修改Eureka地址 - -因为上一套**ApolloConfgService和ApolloAdminService**里的Eureka占用了8080端口,所以这里第二套需要改一下端口 - -更新生产环境Apollo的Eureka地址: - -```sql -USE ApolloConfigDBPRO; - -UPDATE ServerConfig SET `Value` = "http://localhost:8081/eureka/" WHERE `key` = "eureka.service.url"; -``` - - - -#### 调整ApolloPortal服务配置 - -服务配置项统一存储在ApolloPortalDB.ServerConfig表中,可以通过`管理员工具 - 系统参数`页面进行配置:apollo.portal.envs - 可支持的环境列表 - - - -默认值是dev,如果portal需要管理多个环境的话,以逗号分隔即可(大小写不敏感),如: - - dev,pro - - - -#### 启动ApolloPortal - -Apollo Portal需要在不同的环境访问不同的meta service(apollo-configservice)地址,所以我们需要在配置中提供这些信息。 - -```bash --Ddev_meta=http://localhost:8080/ -Dpro_meta=http://localhost:8081/ -``` - - -1. 关闭之前启动的ApolloPortal服务,使用runApolloPortal.bat启动多环境配置 - - ```bash - echo - - set url="localhost:3306" - set username="root" - set password="mysql" - - start "ApolloPortal" java -Xms256m -Xmx256m -Dapollo_profile=github,auth -Ddev_meta=http://localhost:8080/ -Dpro_meta=http://localhost:8081/ -Dserver.port=8070 -Dspring.datasource.url=jdbc:mysql://%url%/ApolloPortalDB?characterEncoding=utf8 -Dspring.datasource.username=%username% -Dspring.datasource.password=%password% -Dlogging.file=.\logs\apollo-portal.log -jar .\apollo-portal-1.3.0.jar - ``` - - - -1. 启动之后,点击account-service服务配置后会提示环境缺失,此时需要补全上边新增生产环境的配置 - - - -3. 点击左下方的补缺环境 - - - -4. 补缺过生产环境后,切换到PRO环境后会提示有Namespace缺失,点击补缺 - - - -5. 从dev环境同步配置到pro - - - -#### 验证配置 - -1. 同步完成后,切换到pro环境,修改生产环境rocketmq地址后发布配置 - - - -2. 配置项目使用pro环境,测试配置是否生效 - -* 在apollo-env.properties中增加pro.meta=[http://localhost:8081](http://localhost:8081) - -* 修改account-service启动参数为:-Denv=pro - -```bash - -Denv=pro -Dapollo.cacheDir=/opt/data/apollo-config -Dapollo.cluster=DEFAULT -``` - -* 访问[http://127.0.0.1:63000/account-service/mq](http://127.0.0.1:63000/account-service/mq) 验证RocketMQ地址是否为上边设置的PRO环境的值 - - - -### 灰度发布 - -#### 定义 - - 灰度发布是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。 - - - -#### Apollo实现的功能 - -1. 对于一些对程序有比较大影响的配置,可以先在一个或者多个实例生效,观察一段时间没问题后再全量发布配置。 -2. 对于一些需要调优的配置参数,可以通过灰度发布功能来实现A/B测试。可以在不同的机器上应用不同的配置,不断调整、测评一段时间后找出较优的配置再全量发布配置。 - -#### 场景介绍 - -apollo-quickstart项目有两个客户端: - -1. 172.16.0.160 -2. 172.16.0.170 - - - -**灰度目标** - -当前有一个配置timeout=2000,我们希望对172.16.0.160灰度发布timeout=3000,对172.16.0.170仍然是timeout=2000。 - - - -#### 创建灰度 - -1. 点击application namespace右上角的`创建灰度`按钮 - - - -2. 点击确定后,灰度版本就创建成功了,页面会自动切换到`灰度版本`Tab - - - -#### 灰度配置 - -1. 点击`主版本的配置`中,timeout配置最右侧的`对此配置灰度`按钮 - -2. 在弹出框中填入要灰度的值:3000,点击提交 - - - -#### 配置灰度规则 - -1. 切换到`灰度规则`Tab,点击`新增规则`按钮 - - - -2. 在弹出框中`灰度的IP`下拉框会默认展示当前使用配置的机器列表,选择我们要灰度的IP,点击完成 - - - -如果下拉框中没找到需要的IP,说明机器还没从Apollo取过配置,可以点击手动输入IP来输入,输入完后点击添加按钮 - - - -#### 灰度发布 - -1. 启动apollo-quickstart项目的GrayTest类输出timeout的值 - -vm options: `-Dapp.id=apollo-quickstart -Denv=DEV -Ddev_meta=http://localhost:8080` - -```java - public class GrayTest { - - // VM options: - // -Dapp.id=apollo-quickstart -Denv=DEV -Ddev_meta=http://localhost:8080 - public static void main(String[] args) throws InterruptedException { - Config config = ConfigService.getAppConfig(); - String someKey = "timeout"; - - while (true) { - String value = config.getProperty(someKey, null); - System.out.printf("now: %s, timeout: %s%n", LocalDateTime.now().toString(), value); - Thread.sleep(3000L); - } - } - } -``` - - - -2. 切换到`配置`Tab,再次检查灰度的配置部分,如果没有问题,点击`灰度发布` - - - -3. 在弹出框中可以看到主版本的值是2000,灰度版本即将发布的值是3000。填入其它信息后,点击发布 - - - -4. 发布后,切换到`灰度实例列表`Tab,就能看到172.16.0.160已经使用了灰度发布的值 - - - - - - - -#### 全量发布 - -如果灰度的配置测试下来比较理想,符合预期,那么就可以操作`全量发布`。 - -全量发布的效果是: - -1. 灰度版本的配置会合并回主版本,在这个例子中,就是主版本的timeout会被更新成3000 -2. 主版本的配置会自动进行一次发布 -3. 在全量发布页面,可以选择是否保留当前灰度版本,默认为不保留。 - - - -#### 放弃灰度 - -如果灰度版本不理想或者不需要了,可以点击`放弃灰度` - - - -#### 发布历史 - -点击主版本的`发布历史`按钮,可以看到当前namespace的主版本以及灰度版本的发布历史 - - - diff --git a/docs/Computer_NetWork/计算机网络-总结.md b/docs/Computer_NetWork/计算机网络-总结.md deleted file mode 100644 index 8de79f6..0000000 --- a/docs/Computer_NetWork/计算机网络-总结.md +++ /dev/null @@ -1,829 +0,0 @@ ---- -title: 计算机网络-总结篇 -tags: - - 计算机网络 - - 面试 -categories: - - 计算机网络 -keywords: 计算机网络,计网,面试 -description: 计算机网络-总结篇,可以用来期末复习,校招面试等。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/network.jpg' -abbrlink: 3905e6f8 -date: 2020-04-16 17:21:58 ---- - - - -# 备注 - -1、打【】的是有印象,能大致说出来即可,但是没打【】的也不要背,理解性记忆。 - -2、如果有知识点,有的博客讲的很清楚,我就直接贴链接了。 - -3、《计算机网络7》是一本很好的书,讲的很详细,而且不难懂,读者有时间的话,建议看一下。 - -4、如果有错误,欢迎在评论区指正 - -> 1、本人正在准备秋招,秋招完之后会持续更新博客。这些总结的部分,也是准备面试期间看了很多很多博客写下来的。 -> 2、本人博客:https://youthlql.gitee.io/ -> 3、等忙完秋招,会陆续更新一些内容。喜欢的朋友可以收藏一下博客 -> - -# 补充 - -这里面的就是讲的比较详细的博客 - -## 关于计网比较好的博客 - -https://blog.csdn.net/ThinkWon/article/details/104903925 - -https://www.cnblogs.com/xjtu-lyh/p/12439036.html - - - -## 三次挥手,四次握手 - -https://blog.csdn.net/qzcsu/article/details/72861891 - - - -## DNS解析 - -https://blog.csdn.net/weixin_40470303/article/details/80642190 - - - -## http1.0,http1.1,http2.0介绍 - -https://segmentfault.com/a/1190000016656529 - - - - - -## https建立链接过程 - -https://blog.csdn.net/iispring/article/details/51615631 - - - - - - - - - - - -# OSI七层模型与TCP/IP 五层模型 - -## 物理层 - -​ 物理层考虑的是怎样才能在连接各种计算机的传输介质上传输数据比特流。现有的计算机网络中的硬件设备和传输媒体(介质)的种类非常多,而通信手段也有许多不同方式。物理层的作用正是要尽可能地屏蔽掉这些传输媒体和硬件设备的差异,使物理层上面的数据链路层感觉不到这些差异,这样就可使数据链路层只考虑如何完成本层的协议和服务,而不必考虑网络具体的传输媒体和通信手段是什么。 - -> 参考《计算机网络7》P51 - - - -## 数据链路层 - - - -### 概念 - -​ 数据链路层研究的是分组怎样从一台主机传送到另一台主机,但并不经过路由器转发。从整个互联网来看,局域网仍属于数据链路层的范围。数据传送单位是帧。 - -**简单的过程:** - -1、在两个相邻节点之间传送数据时,数据链路层将网络层交下来的**IP 数据报**添加首部和尾部组装成帧**,**在两个相邻节点间的链路上传送帧**。**每一帧包括数据和必要的控制信息【如同步信息,地址信息,差错控制等】。 - -2、数据链路层在收到一个帧后,通过控制信息检测收到的帧中是否有差错,如果没有就可从中提出**IP数据报**部分,上交给网络层**。**如果发现差错,数据链路层就简单地丢弃这个出了差错的帧,以避免继续在网络中传送下去白白浪费网络资源 【在接收数据时,控制信息使接收端能够知道一个帧从哪个比特开始和到哪个比特结束。控制信息还使接收端能够检测到所收到的帧中有误差错。如果发现差错,数据链路层就简单地丢弃这个出了差错的帧,以避免继续在网络中传送下去白白浪费网络资源。如果需要改正数据在链路层传输时出现差错(这就是说,数据链路层不仅要检错,而且还要纠错),那么就要采用可靠性传输协议来纠正出现的差错。这种方法会使链路层的协议复杂些】 - - - -### 一些小细节 - - - -**数据链路层使用的信道主要有两种:** - -①点对点信道 - -②广播信道 - - **三个基本问题:** - -封装成帧、透明传输和差错检测。 - -> 更细致的看《计算机网络7》 P82 - - - - - -## 网络层 - -1、在计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换结点,确保数据及时传送。网络层向上只提供简单灵活的、无连接的、尽最大努力交付的IP数据报服务。【其实数据报或IP数据报就是我们经常使用的“分组”】 - -2、网络层在发送分组时不需要先建立连接,没有给分组进行上编号,所传送的分组可能出错、丢失、重复和失序。如果主机(即端系统)中的进程之间的通信需要是可靠的,那么就由网络的主机中的运输层负责(包括差错处理、流量控制等) - - - -> 更多细节-->《计算机网络7》 P124 - - - - - -## 运输层 - - - -1、运输层的主要任务就是负责向两台主机的进程之间提供通用的数据传输服务。 - -2、运输层有一个很重要的功能 复用和分用。这里的“复用"是指在发送方不同的应用进程都可以使用同一个运输层协议传送数据(当然需要加上适当的首部),而“分用”是指接收方的运输层在剥去报文的首部后能够把这些数据正确交付目的应用进程 - -3、运输层向高层用户屏蔽了下面网络核心的细节【如网络拓扑、所采用的路由选择协议等】,**它使应用进程看见的就是好像在两个运输层实体之间有一条端到端的逻辑通信信道**。但这条逻辑通信信道对上层的表现却因运输层使用的不同协议而有很大的差别。当运输层釆用面向连接的TCP协议时,尽管下面的网络层是不可靠的(只提供尽最大努力服务),但这种逻辑通信信道就相当于一条全双工的可靠信道。但当运输层采用无连接的udp协议时,这种逻辑通信信道仍然是一条不可靠信道。 - - - - - -## 应用层 - -(在上一章,我们巳学习了运输层为应用进程提供了端到端的通信服务)。 - -**1.** **不同的网络应用的应用进程之间,还需要有不同的通信规则。因此在运输层协议之上,还需要有应用层协议****。** - -**2.** **每个应用层协议都是为了解决某一类应用问题,**(而问题的解决又必须通过位于不同主机中的多个应用进程之间的通信和协同工作来完成)**。应用进程之间的这种通信必须遵循严格的规则。应用层的具体内容就是精确定义这些通信规则。** - -3.**运输层是两台主机间进程的交互。应用层是为了更加细化不同网络应用的交互规则。** - - - - - -# 常见应用层协议和运输层、网络层协议 - -## 各层协议 - -**应用层:**HTTP(超文本传输协议) ,DNS(域名系统) ,FTP(文件传输协议) ,SMTP(简单邮件传送协议) - -**运输层:**TCP ,UDP - -**网络层:** IP, ARP(地址解析协议)--> 见《计网7》P134 - - - -## 硬件如路由器之类在哪一层 - -- 路由器在网络层,用来进行路由选择 - - - -# TCP与UDP区别和应用场景,基于TCP的协议有哪些,基于UDP的有哪些 - -## 区别+应用场景 - - - -**总结:** - -1、UDP的主要特点是 - -①无连接②尽最大努力交付③面向报文④无拥塞控制⑤支持一对一,一对多,多对一和多对多的交互通信⑥首部开销小(只有四个字段:源端口,目的端口,长度和检验和) - -2、TCP的主要特点是 - -①面向连接②每一条TCP连接只能是一对一的③提供可靠交付④提供全双工通信⑤面向字节流 - - - -## 基于TCP的协议有哪些,基于UDP的有哪些 - -**TCP:** - -HTTP, 超文本传输协议 - -FTP, 文件传输协议 - -SMTP,简单邮件传输协议,用来发送电子邮件 - -SSH 安全外壳协议,用于加密安全登陆 - -**UDP:** - -DHCP协议:动态主机配置协议,动态配置IP地址 - -NTP协议:网络时间协议,用于网络时间同步 - -RIP(路由选择协议) - -DNS - - - -# TCP可靠传输的保证,拥塞控制目的和过程 - -## 如何保证可靠传输 - -TCP通过三次握手建立可靠连接 - -①数据被分割成 TCP 认为最适合发送的数据包。TCP 给发送的每一个包进行编号,接收方对数据包进行排序,将有序数据传送给应用层。**TCP通过序列号和确认应答提高可靠性** - -②校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到端的校验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。 - -③流量控制: TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。 - -④拥塞控制: 当网络拥塞时,减少数据的发送。 - -⑤ARQ协议:分为停止等待ARQ协议和连续ARQ协议 - -5.1 它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。 - -5.2 超时重传::当 TCP 发出一个报文段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。 - -5.3 TCP 的接收端会丢弃重复的数据 - - - -## ARQ - -- 停止等待ARQ:它的基本原理就是每发完一个分组就停止发送,等待对方确认(回复ACK)。如果过了一段时间(超时时间后),还是没有收到 ACK 确认,说明没有发送成功,需要重新发送,直到收到确认后再发下一个分组。停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重转时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为自动重传请求ARQ。另外在停止等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。 - -- 连续ARQ协议:(流水线的传输方式)可提高信道利用率。发送维持一个发送窗口,凡位于发送窗口内的分组可连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组位置的所有分组都已经正确收到了。 - - - - - -## 拥塞控制 - -**目的:** - -拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率,以便使接收端来得及接收。 - - - -**过程:** - -为了进行拥塞控制,TCP 发送方要维持一个 拥塞窗口(cwnd) 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。**慢开始和拥塞避免都是基于窗口的拥塞控制**。 - - - -**区别:** - -> https://blog.csdn.net/ligupeng7929/article/details/79597423 - -* **慢开始:** 慢开始算法的思路是当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么可能会引起网络阻塞,因为现在还不知道网络的符合情况。经验表明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是由小到大逐渐增大拥塞窗口数值。cwnd初始值为1,每经过一个传播轮次,cwnd加倍。 -* **拥塞避免:** 拥塞避免算法的思路是让拥塞窗口cwnd缓慢增大,即每经过一个往返时间RTT就把发送放的cwnd加1. - -- 为了防止拥塞窗口cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限ssthresh状态变量(如何设置ssthresh)。慢开始门限ssthresh的用法如下: - -​ 当 cwnd < ssthresh 时,使用上述的慢开始算法。 - -​ 当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。 - -​ 当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法。 - -- 拥塞避免算法:让拥塞窗口cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口cwnd按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。 - -**快重传和快恢复:** - -1、**快重传**算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时才进行捎带确认。 - -2、发送方知道现在只是丢失了个别的报文段。于是不启动慢开始,而是执行**快恢复算法**。这时,发送方调整门限值ssthresh= (cwnd/2=8,,同时设置拥塞窗口cwnd = ssthresh=8 ,并开始执行拥塞避免算法。 - -3、请注意,也有的快恢复实现是把快恢复开始时的拥塞窗口cwnd值再增大一一些(增大3个报文段的长度),即等于新的ssthresh + 3 x MSS。这样做的理由是:既然发送方收到3个重复的确认,就表明有3个分组已经离开了网络。这3个分组不再消耗网络的资源而是停留在接收方的缓存中(接收方发送出3个重复的确认就证明了这个事实)。可见现在网络中并不是堆积了分组而是减少了3个分组。因此可以适当把拥塞窗口扩大些。 - - - -在采用快恢复算法时,慢开始算法只是在TCP连接建立时和网络出现超时时才使用。 - - - - - - - - - -## 为什么要进行流量控制 - -一般来说,我们总是希望数据传输更快一些。但如果发送方把数据发送的过快,接收方就可能来不及接收,这就会造成数据的丢失(丢包)。 - - - - - -# TCP粘包现象原因和解决方法 - - - -## 原因 - -1.UDP协议的保护消息边界使得每一个消息都是独立的 - -2.而tcp是基于流的传输,流传输却把数据当作一串数据流,他不认为数据是一个一个的消息 - -3.发送端需要等缓冲区满才发送出去,造成粘包 - -4.接收方不及时接收缓冲区的包,造成多个包粘包 - -具体点: - -(1)发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。 - -(2)接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。 - - - -## 解决方法 - -(1)对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP程序收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满; - -(2)对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象; - -(3)由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。 - - - -## 为什么粘包需要处理? - -不是所有的粘包现象都需要处理,若传输的数据为不带结构的连续流数据(如文件传输),则不必把粘连的包分开(简称分包)。但在实际工程应用中,传输的数据一般为带结构的数据,这时就需要做分包处理。分包一般难度较大,所以尽量避免粘包 - - - - - -# 三次握手相关问题 - - - -## 过程+状态改变 - -把**补充**里面的**第二个博客**的过程背下来。(有的地方需要参考第一个博客) - - - -## 为什么三次,两次为什么不行? - -### 第一种答案 - -两次握手只能保证单向连接是畅通的。只有经过第三次握手,才能确保双向都可以接收到对方的发送的 数据。两次握手接收方这里不能确定自己的的发送是正常的,发送方的接收是正常的。 - -**具体点:** - -**三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。** - -第一次握手:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常 - -第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:对方发送正常,自己接收正常 - -第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常 - -所以三次握手就能确认双发收发功能都正常,缺一不可。 - - - -### 第二种答案 - -​ 一句话,主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。 - - - - - - - -## 如果已经建立了连接,但是客户端突然出现故障了怎么办? - - - - - - - -# 四次挥手相关问题 - - - -## 过程及状态改变 - -就是把**补充**里面的**第二个博客**的过程背下来。(有的地方需要参考第一个博客) - - - -## 为什么四次挥手 - -​ 因为只有在客户端和服务端都没有数据要发送的时候才能断开TCP。而客户端发出FIN报文时只能保证客户端没有数据发了,服务端还有没有数据发客户端是不知道的。而服务端收到客户端的FIN报文后只能先回复客户端一个确认报文来告诉客户端我服务端已经收到你的FIN报文了,但我服务端还有一些数据没发完,等这些数据发完了服务端才能给客户端发FIN报文(所以不能一次性将确认报文和FIN报文发给客户端,就是这里多出来了一次)。 - - - - -## CLOSE-WAIT和TIME-WAIT存在的意义 - -- Close-wait存在的意义: 就是服务端还有数据要发送,这个时间内就是服务端发送完最后的数据 - -- Time-wait存在的意义: - - - 第一,这里同样是要考虑丢包的问题,如果第四次挥手的报文丢失,服务端没收到确认ack报文就会重发第三次挥手的报文,这样报文一去一回最长时间就是2MSL,所以需要等这么长时间来确认服务端确实已经收到了 - - 第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。 - - - -## 如何查看TIME-WAIT状态的链接数量?(TODO) - - - -## 为什么会TIME-WAIT过多?解决方法是怎样的? (TODO) - - - - - -## 为什么客户端最后还要等待2MSL? - -补充里的第二个博客 - -https://blog.csdn.net/qzcsu/article/details/72861891 - - - -# TCP - -## 报文首部 - - - -**重要字段:** - -(1)序号:seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。 - -(2)确认序号:ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,ack=seq+1。 - -(3)标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下: - -ACK:确认序号有效。 -FIN:释放一个连接。 -PSH:接收方应该尽快将这个报文交给应用层。 -RST:重置连接。 -SYN:发起一个新连接。 -URG:紧急指针(urgent pointer)有效。 - -**需要注意的是:** - -不要将确认序号ack与标志位中的ACK搞混了。 -确认方ack=发起方seq+1,两端配对。 - -# 浏览器输入URL并回车的过程以及相关协议,DNS查询过程。 - -## 过程 - -1、DNS域名解析,得到IP地址 - -DNS解析流程: - -> 参考:https://blog.csdn.net/yanshuanche3765/article/details/82589210 -> -> https://blog.csdn.net/u014465934/article/details/83241097 - -(1)1. 浏览器先检查自身缓存中有没有被解析过的这个域名对应的ip地址,(如果有,解析结束。同时域名被缓存的时间也可通过TTL属性来设置。) - -(2)在主机查询操作系统DNS缓存,也就是hosts文件里配置的。 - -(3)如果浏览器和系统缓存都没有,系统的 gethostname 函数就会像本地 DNS 服务器发送请求。而网络服务一般都会先经过路由器以及网络服务商(电信),所以会先查询路由器缓存,然后再查询 ISP 的 DNS 缓存。 - -(4)如果至此还没有命中域名,才会真正的请求本地域名服务器(LDNS)来解析这个域名,这台服务器一般在你的城市的某个角落,距离你不会很远,并且这台服务器的性能都很好,一般都会缓存域名解析结果,大约80%的域名解析到这里就完成了。 - -(5)如果LDNS仍然没有命中,本地的DNS服务器向根域名服务器发送查询请求,根域名服务器返回该域名的顶级级域名服务器。依次类推:根域名服务器-顶级域名服务器-主域名服务器 - - - -2、解析出IP地址后,根据IP地址和默认端口80和服务器建立连接,发送http请求 - -3、服务器对浏览器的请求作出响应,并把对应的html文本发送给浏览器 - -4、释放TCP连接(四次挥手断开连接) - -6、浏览器解析该HTML文本并显示内容 - - - -## 用到的协议 - -- TCP:与服务器建立TCP连接 - -- IP: 建立TCP协议时,需要发送数据,发送数据在网络层使用IP协议 - -- OPSF: IP数据包在路由器之间,路由选择使用OPSF协议 - -- ARP: 路由器在与服务器通信时,需要将ip地址转换为MAC地址,需要使用ARP协议 - -- HTTP:在TCP建立完成后,使用HTTP协议访问网页 - - - - - -# HTTP1.0、1.1、2.0之间的区别 - -## TCP队头阻塞和HTTP队头阻塞 - -> http://www.mamicode.com/info-detail-2983775.html - -**管道化请求的致命弱点**: - -1、会造成队头阻塞,前一个响应未及时返回,后面的响应被阻塞 - -2、请求必须是幂等请求,不能修改资源。因为,意外中断时候,客户端需要把未收到响应的请求重发,非幂等请求,会造成资源破坏。 - -3、由于这个原因,目前大部分浏览器和Web服务器,都关闭了管道化,采用非管道化模式。无论是非管道化还是管道化,都会造成队头阻塞(请求阻塞)。 - -**解决http队头阻塞的方法:** - -**1. 并发TCP连接**(浏览器一个域名采用6-8个TCP连接,并发HTTP请求) -**2. 域名分片**(多个域名,可以建立更多的TCP连接,从而提高HTTP请求的并发) - -**2. HTTP2方式** - -http2使用一个域名单一TCP连接发送请求,请求包被二进制分帧,不同请求可以互相穿插,避免了http层面的请求队头阻塞。 -但是不能避免TCP层面的队头阻塞。 - - - -## http1.0 - -1.在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。 - -2.http队列头阻塞 - - 请求队列的第一个请求因为服务器正忙(或请求格式问题等其他原因),导致后面的请求被阻塞 - - - -## http1.1 - -1、支持长链接 - -(在request和response中的header中的connection是close或者Keep-Alive进行控制) - - 一个TCP链接可以传送多个http请求和响应,减少了TCP建立链接和关闭链接的消耗。 - - - -2、支持http管道(也就是流水线方式) - -​ 不使用管道的http请求,在使用持久链接时,必须严格满足先进先出的队列顺序(FIFO),即发送请求,等待响应完成,再发送客户端队列中的下一个请求。管道可以让我们把 FIFO 队列从客户端(请求队列)迁移到服务器(响应队列),即客户端可以并行,服务端串行。客户端可以不用等待前一个请求返回,发送请求,但服务器端必须顺序的返回客户端的请求响应结果。 - -​ 但是http1.1任然无法解决http队头阻塞 - - - -**还有更细的就看补充里的第四个博客** - - - -## http2.0 - - - - - -1.HTTP2.0最主要的特点是:在不改动HTTP语义、方法、状态码、URI及首部字段的情况下,大幅度提高了web性能。 - -**2.** **二进制传输**:大幅提高性能的一个原因就是: HTTP2.0中所有加强性能的核心是二进制传输,在HTTP1.x中,我们是通过文本的方式传输数据。基于文本的方式传输数据存在很多缺陷,文本的表现形式有多样性,因此要做到健壮性考虑的场景必然有很多,但是二进制则不同,只有0和1的组合,因此选择了二进制传输,实现方便且健壮。在HTTP2.0中引入了新的编码机制,所有传输的数据都会被分割,并采用二进制格式编码。 - -**3.** **多路复用**: HTTP2.0中,有两个概念非常重要:帧(frame)和流(stream)。帧是最小的数据单位,每个帧会标识出该帧属于哪个流,流是多个帧组成的数据流。所谓多路复用,即在一个TCP连接中存在多个流,即可以同时发送多个请求 - -对端可以通过帧中的表示知道该帧属于哪个请求。在客户端,这些帧乱序发送,到对端后再根据每个帧首部的流标识符重新组装【请求包被二进制分帧,不同请求可以互相穿插】。通过该技术,可以避免HTTP旧版本的队头阻塞问题,极大提高传输性能 - -**4.** **Header压缩** - -**5.** **服务器Push**: 在HTTP2.0中,服务端可以在客户端某个请求后,主动推送其他资源 - -**6.** **更安全**: HTTP2.0对tls的安全性做了近一步加强 - - - - - -# HTTP与HTTPS - -## 区别 - -**1、端口:**HTTP的URL由“http://”起始且默认使用端口80,而HTTPS的URL由“https://”起始且默认使用端口443。 - -**2、安全性和资源消耗:** HTTP协议运行在TCP之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS是运行在SSL/TLS之上的HTTP协议,SSL/TLS 运行在TCP之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS高,但是 HTTPS 比HTTP耗费更多服务器资源 - - - -## HTTPS链接建立的过程 - -HTTPS在传输的过程中会涉及到三个密钥: - -- 服务器端的公钥和私钥,用来进行非对称加密 - -- 客户端生成的随机密钥,用来进行对称加密 - -一个HTTPS请求实际上包含了两次HTTP传输,可以细分为8步。 - -1. 客户端向服务器发起HTTPS请求,连接到服务器的443端口。 - -2. 服务器端有一个密钥对,即公钥和私钥,是用来进行非对称加密使用的,服务器端保存着私钥,不能将其泄露,公钥可以发送给任何人。 - -3. 服务器将自己的公钥发送给客户端。 - -4. 客户端收到服务器端的公钥之后,会对公钥进行检查,验证其合法性,如果发现发现公钥有问题,那么HTTPS传输就无法继续。【严格的说,这里应该是验证服务器发送的数字证书的合法性,关于客户端如何验证数字证书的合法性,下文会进行说明。】如果公钥合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,我们将该密钥称之为client key,即客户端密钥,这样在概念上和服务器端的密钥容易进行区分。然后用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文了,至此,HTTPS中的第一次HTTP请求结束。 - -5. 客户端会发起HTTPS中的第二个HTTP请求,将加密之后的客户端密钥发送给服务器。 - -6. 服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文。 - -7. 然后服务器将加密后的密文发送给客户端。 - -8. 客户端收到服务器发送来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据。这样HTTPS中的第二个HTTP请求结束,整个HTTPS传输完成。 - - - - -**详细就看补充-5博客** - - - -## 了解对称加密算法和非对称加密算法不? - -1.**对称加密**:密钥只有一个,加密解密为同一个密码,且加解密速度快,典型的对称加密算法有DES、AES等; - -**存在的问题:** 这种方式存在的最大问题就是密钥发送问题,即如何安全地将密钥发给对方,对称加密所使用的密钥我们可以通过非对称加密的方式发送出去。 - -**2.** **非对称加密**:密钥成对出现(且根据公钥无法推知私钥,根据私钥也无法推知公钥),加密解密使用不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度较慢,典型的非对称加密算法有RSA、DSA等。 - - - -# HTTP请求有哪些。Post和get区别。 - -## 有哪些http请求 - - - - - -## post和get的区别 - -1.GET提交的数据放在URL中,POST数据放在Request body。(但这种情况仅限于浏览器发请求的场景)。这点意味着GET更不安全(POST也不安全,因为HTTP是明文传输抓包就能获取数据内容,要想安全还得加密,使用https) - -2.GET回退浏览器无害,POST会再次提交请求(GET方法回退后浏览器再缓存中拿结果,POST每次都会创建新资源) - -3.GET提交的数据大小有限制(是因为浏览器对URL的长度有限制,GET本身没有限制),POST没有 - -4.GET可以被保存为书签,POST不可以。这一点也能感受到。(这点可能没啥用) - -5.GET请求会被浏览器主动cache,而POST不可缓存 - -6.对参数的数据类型,GET只接受ASCII字符,而POST没有限制。 - -7.GET会保存再浏览器历史记录中,POST不会。这点也能感受到。 - - - -下面的就是深层次的 - -8.可以把 get 和 post 当作两个不同的行为,两者并没有什么本质区别,底层都是 TCP 连接 - - - -对于第1点的补充(如果需要举例子可以这样举) - -> 其中的“"可以是GET也可以是POST,或者其他的HTTP Method,如PUT、DELETE、OPTION……。从协议本身看,并没有什么限制说GET一定不能没有body,POST就一定不能把参放到的querystring上。因此其实可以更加自由的去利用格式。比如Elastic Search的_search api就用了带body的GET;也可以自己开发接口让POST一半的参数放在url的querystring里,另外一半放body里;你甚至还可以让所有的参数都放Header里——可以做各种各样的定制,只要请求的客户端和服务器端能够约定好 -> -> 摘自---https://www.zhihu.com/question/28586791 - - - - - -# HTTP常见响应状态码,从1xx到5xx都要说 - - - -需要记住的(下面的应该足够了) - -100, 101 - -200, 201, 202, 204 - -301, 302 , 304, 305 - -400, 401, 403, 404, 408 - -500, 502 ,504 - - - - - -# 重定向和转发区别 - -1、重定向是两次请求,转发是一次请求。因此转发的速度要快于重定向 - -**重定向过程**:第一次,客户端request一个网址,服务器响应,并response回来,告诉浏览器,你应该去别一个网址。 - -2、重定向之后地址栏上的地址会发生变化,变化成第二次请求的地址,转发之后地址栏上的地址不会变化,还是第一次请求的地址 - -3、转发是服务器行为,重定向是客户端行为。 - -4、重定向时的网址可以是任何网址,转发的网址必须是本站点的网址 - - - - - -# Session、Cookie和Token的主要区别 - -HTTP协议本身是无状态的。什么是无状态呢,即服务器无法判断用户身份。 - -## 什么是cookie - -cookie是由Web服务器保存在用户浏览器上的小文件(key-value格式),包含用户相关的信息。客户端向服务器发起请求,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户身份。 - -## 什么是session - -session是依赖Cookie实现的。session是服务器端对象 - -session 是浏览器和服务器会话过程中,服务器分配的一块储存空间。服务器默认为浏览器在cookie中设置 sessionid,浏览器在向服务器请求过程中传输 cookie 包含 sessionid ,服务器根据 sessionid 获取出会话中存储的信息,然后确定会话的身份信息。 - -## cookie与session区别 - -存储位置与安全性:cookie数据存放在客户端上,安全性较差,session数据放在服务器上,安全性相对更高; -存储空间:单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie,session无此限制 -占用服务器资源:session一定时间内保存在服务器上,当访问增多,占用服务器性能,考虑到服务器性能方面,应当使用cookie。 - -## 什么是Token - -- Token的引入:Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token便应运而生。 - -- Token的定义:Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。 - -- 使用Token的目的:Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。 - -- Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位 - -- Token常用JWT实现,原来需要客户端需要请求服务端验证session合法性,现在只需要通过算法检验Token是否合法,减少了查数据库或者访问服务器比对用户密码,用户身份的消耗。也就是通过算法和密钥代替了访问数据库或服务器比对的过程 - -## session与token区别 - -- session机制存在服务器压力增大,CSRF跨站伪造请求攻击,扩展性不强等问题; -- session存储在服务器端,token存储在客户端 -- token提供认证和授权功能,作为身份认证,token安全性比session好; -- session这种会话存储方式方式只适用于客户端代码和服务端代码运行在同一台服务器上,token适用于项目级的前后端分离(前后端代码运行在不同的服务器下) - - - - -# Reactor和Proactor区别 - -1.Reactor被动的等待指示事件的到来并做出反应;它有一个等待的过程,做什么都要先注册到监听事件集合中等待socket可读时再进行操作;。 - -2.Proactor直接调用异步读写操作,调用完后放到到用户线程指定的缓存区,接着通知用户线程直接使用即可。 - -3.Proactor是真正意义上的用于异步IO,但是依赖操作系统对异步的支持。而Reactor用于同步IO - - - -通俗语言: - -reactor:能收数据了你跟我说一声。 - -proactor: 这有十个字节数据,收好了跟我说一声。 - - - -> 想要理解这两设计模式,还是要真正的学习设计模式。 - -# select、poll、epoll的区别? - -> select, poll, epoll 都是I/O多路复用的具体的实现,之所以有这三个存在,其实是他们出现是有先后顺序的。 -> -> * https://blog.csdn.net/nanxiaotao/article/details/90612404 -> * https://www.cnblogs.com/aspirant/p/9166944.html -> * https://www.zhihu.com/question/32163005 - -## select - -1. 它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以**select具有O(n)的无差别轮询复杂度**,同时处理的流越多,无差别轮询时间就越长。 -2. 单个进程可监视的fd_set(监听的端口个数)数量被限制:32位机默认是1024个,64位机默认是2048。 - -## poll - -poll本质上和select没有区别,采用**链表**的方式替换原有fd_set数据结构,而使其**没有连接数的限制**。 - -## epoll - -1. epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1)) -2. 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数。即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。 -3. epoll通过内核和用户空间共享一块内存来实现的。select和poll都是内核需要将消息传递到用户空间,都需要内核拷贝动作 -4. epoll有EPOLLLT和EPOLLET两种触发模式。(**暂时不去记,有个印象,大致是什么样就可以**) - - - diff --git a/docs/Computer_NetWork/计算机网络-概述.md b/docs/Computer_NetWork/计算机网络-概述.md deleted file mode 100644 index acc1d87..0000000 --- a/docs/Computer_NetWork/计算机网络-概述.md +++ /dev/null @@ -1,688 +0,0 @@ ---- -title: 计算机网络-概述 -tags: - - 计算机网络 -categories: - - 计算机网络 -keywords: 计算机网络,计网 -description: 计算机网络-概述篇。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/network.jpg' -abbrlink: be54bfd5 -date: 2021-04-03 14:21:58 ---- - - - -# 概述 - -## 计算机网络在信息时代的作用 - -* 计算机网络已由一种**通信基础设施**发展成为一种重要的**信息服务基础设施** -* 计算机网络已经像水,电,煤气这些基础设施一样,成为我们生活中不可或缺的一部分 - - - -> 我国互联网发展状况:http://www.cnnic.net.cn - -## 因特网概述 - -### 网络、互连网(互联网)和因特网 - -网络:网络(Network)由若干**结点(Node)**和连接这些结点的**链路(Link)**组成。 - - - -互联网(或互连网):多个网络通过路由器互连起来,这样就构成了一个覆盖范围更大的网络,即互连网(互联网)。因此,互联网又称为“网络的网络(Network of Networks)”。 - - - -因特网:因特网(Internet)是世界上最大的互连网络(用户数以亿计,互连的网络数以百万计)。 - - - - - -> **internet与Internet的区别** -> -> * internet(互联网或互连网)是一个通用名词,它泛指多个计算机网络互连而成的网络。在这些网络之间的通信协议可以是任意的。 -> * Internet(因特网)则是一个专用名词,它指当前全球最大的、开放的、由众多网络互连而成的特定计算机网络,它采用TCP/IP协议族作为通信的规则,其前身是美国的ARPANET。 -> - - - -### 因特网发展的三个阶段 - - - - - -**因特网服务提供者`ISP`(`I`nternet `S`ervice `P`rovider)** - - - -> Q:普通用户是如何接入到因特网的呢? -> -> A:通过ISP接入因特网 -> -> ISP可以从因特网管理机构申请到成块的IP地址,同时拥有通信线路以及路由器等联网设备。任何机构和个人只需缴纳费用,就可从ISP的得到所需要的IP地址。这一点很重要。因为因特网上的主机都必须有IP地址才能进行通信,这样就可以通过该ISP接入到因特网 -> - - - -**中国的三大`ISP`:中国电信,中国联通,中国移动** - - - - - -**基于ISP的三层结构的因特网** - - - -1、图中红线部分是两个私人用户进行通信的大致链路,可以看到需要经过多个ISP层级 - -2、一旦某个用户能够接入到因特网,那么他也可以成为一个ISP,所需要做的就是购买一些如调制解调器或路由器这样的设备,让其他用户可以和他相连。 - -3、第一层ISP可以直接互联,第二层ISP和一些大公司都是第一层ISP的用户。 - - - - - -### 因特网的标准化工作 - -* 因特网的标准化工作对因特网的发展起到了非常重要的作用。 -* 因特网在指定其标准上的一个很大的特点是**面向公众。** - * 因特网所有的**RFC**(Request For Comments)技术文档都可从因特网上免费下载; - * 任何人都可以随时用电子邮件发表对某个文档的意见或建议。 -* **因特网协会ISOC**是一个国际性组织,它负责对因特网进行全面管理,以及在世界范围内促进其发展和使用。 - * 因特网体系结构委员会IAB,负责管理因特网有关协议的开发; - * 因特网工程部IETF,负责研究中短期工程问题,主要针对协议的开发和标准化; - * 因特网研究部IRTF,从事理论方面的研究和开发一些需要长期考虑的问题。 - - - -* 制订因特网的正式标准要经过一下**4个阶段**: - - 1、因特网草案(在这个阶段还不是RFC文档) - - 2、建议标准(从这个阶段开始就成为RFC文档) - - 3、草案标准 - - 4、因特网标准 - - - -### 因特网的组成 - -* 边缘部分 - - 由所有连接在因特网上的**主机**组成(台式电脑,大型服务器,笔记本电脑,平板,智能手机,智能手表,以及物联网智能硬件等)。这部分是**用户直接使用**的,用来进行**通信**(传送数据、音频或视频)和**资源共享**。 - -* 核心部分 - - 由**大量网络**和连接这些网络的**路由器**组成。这部分是**为边缘部分提供服务**的(提供连通性和交换)。 - - - -1. 路由器是一种专用计算机,但我们不称它为主机,路由器是实现分组交换的关键构建,其任务是转发收到的分组,这是网络核心最重要的部分。 - - - -## 三种交换方式 - -网络核心部分是互联网中最复杂的部分。 - -网络中的核心部分要向网络边缘中的大量主机提供连通性,使边缘部分中的任何一个主机都能够向其他主机通信(即传送或接收各种形式的数据)。 - -在网络核心部分起特殊作用的是**路由器**(router)。 - -**路由器**是实现**分组交换** (packet switching) 的关键构件,其任务是转发收到的分组,这是网络核心部分最重要的功能。 - -### 电路交换 - - - -1. 传统两两相连的方式,当电话数量很多时,电话线也很多,就很不方便 -2. 所以要使得每一部电话能够很方便地和另一部电话进行通信,就应该使用一个**中间设备**将这些电话连接起来,这个中间设备就是**电话交换机** - - - -* 电话交换机接通电话线的方式称为电路交换; - -* 从通信资源的分配角度来看,交换(Switching)就是按照某种方式动态地分配传输线路的资源; - -* 电路交换的三个步骤: - - 1、建立连接(分配通信资源) - - 2、通话(一直占用通信资源) - - 3、释放连接(归还通信资源) - - - -1. 当使用电路交换来传送计算机数据时,其线路的传输效率往往很低。这是因为计算机数据是突发式地出现在传输线路上的,而不是像打电话一样一直占用着通信资源。 -2. 试想一下这种情况,当用户正在输入和编辑一份待传输的文件时,用户所占用的通信资源暂时未被利用,该通信资源也不能被其它用户利用,宝贵的通信线路资源白白被浪费了 -3. 因此计算机通常采用的是**分组交换**,而不是线路交换 - -### 分组交换 - - - -1. 通常我们把表示该消息的整块数据成为一个**报文**。 -2. 在发送报文之前,先把较长的报文划分成一个个更小的等长数据段,在每一个数据段前面。加上一些由必要的控制信息组成的首部后,就构成一个分组,也可简称为“包”,相应地,首部也可称为“包头”。首部起到了很大的作用,首先首部中肯定包含了分组的目的地址,否则分组传输路径中的各分组交换机(也就是个路由器)就不知道如何转发分组了 -3. 分组从源主机到目的主机,可走不同的路径(也就是不同的路由)。 -4. 分组乱序:也就是分组到达目的站的顺序不一定与分组在源站的发送顺序相同。 -5. 分组也可能出现丢失,误码,重复等问题(后面介绍) - - - -**分组交换过程中各角色的功能** - -1、发送方 - -* 构造分组 -* 发送分组 - -2、路由器 - -* 缓存分组 -* 转发分组 - - - -路由器处理分组的过程是:分组交换机收到一个分组后,先将分组暂存下来,按照首部中的目的地址进行查表转发,找到合适的转发接口,通过该接口将分组转发给下一个分组交换机 - -3、接收方 - -* 接收分组 -* 还原报文 - - - -### 报文交换 - -报文交换中的交换结点也采用存储转发方式,但报文交换对报文的大小没有限制,这就要求交换结点需要较大的缓存空间。报文交换主要用于早期的电报通信网,现在较少使用,通常被较先进的分组交换方式所取代。因此,不再详细介绍报文交换。 - - - -### 三种交换方式的对比 - -假设A,B,C,D是分组传输路径所要经过的4个结点交换机,纵坐标为时间 - - - - - -电路交换: - -* 通信之前首先要建立连接;连接建立好之后,就可以使用已建立好的连接进行数据传送;数据传送后,需释放连接,以归还之前建立连接所占用的通信线路资源。 - -* 一旦建立连接,中间的各结点交换机就是直通形式的,比特流可以直达终点; - -报文交换: - -* 可以随时发送报文,而不需要事先建立连接;整个报文先传送到相邻结点交换机,**全部存储**下来后进行查表转发,转发到下一个结点交换机。 -* **整个报文**需要在各结点交换机上进行存储转发,由于不限制报文大小,因此需要各结点交换机都具有较大的缓存空间。 - -分组交换: - -* 可以随时发送分组,而不需要事先建立连接。构成原始报文的**一个个分组**,**依次**在各结点交换机上存储转发。各结点交换机在发送分组的同时,还缓存接收到的分组。 -* **构成原始报文的一个个分组**,在各结点交换机上进行存储转发,相比报文交换,减少了转发时延,还可以避免过长的报文长时间占用链路,同时也有利于进行差错控制。 - - - - - -## 计算机网络的定义和分类 - -### 定义 - -* 计算机网络的精确定义并未统一,随着网络的发展,给出了不同的定义,这些定义反映了当时网络技术发展的水平 -* 计算机网络的最简单的定义是:一些互相连接的、自治的计算机的集合。 - * 互连:是指计算机之间可以通过有线或无线的方式进行数据通信; - * 自治:是指独立的计算机,他有自己的硬件和软件,可以单独运行使用; - * 集合:是指至少需要两台计算机; -* 计算机网络的较好的定义是:计算机网络主要是由一些通用的,可编程的硬件(一定包含有中央处理机CPU)互连而成的,而这些硬件并非专门用来实现某一特定目的(例如,传送数据或视频信号)。这些可编程的硬件能够用来传送多种不同类型的数据,并能支持广泛的和日益增长的应用。 - * 计算机网络所连接的硬件,并不限于一般的计算机,而是包括了智能手机等智能硬件。 - * 计算机网络并非专门用来传送数据,而是能够支持很多种的应用(包括今后可能出现的各种应用)。 - - - -上图所示的各终端机只是具有显示和输入设备的终端,而并不是自治的计算机,所以上图并不是计算机网络,只是一个运行分时系统的大型机系统。 - -### 分类 - -**按交换技术分类:** - -* 电路交换网络 -* 报文交换网络 -* 分组交换网络 - -**按使用者分类:** - -* 公用网,公用是指电信公司出资建造的大型网络,公用的意思是所有愿意按电信公司规定交纳费用的人都可以使用这种网络 -* 专用网,是指某个单位为单位内部建立的网络,不对本单位意外提供服务,例如军队,铁路,电力等系统均有本系统的专用网 - -**按传输介质分类:** - -* 有线网络,包括双绞线网络,光纤网络等 -* 无线网络,比如wifi - -**按覆盖范围分类:** - -* 广域网WAN(Wide Area Network) - -作用范围通常为几十到几千公里,因而有时也称为远程网。广域网是互联网的核心部分,其任务是通过长距离(例如,跨越不同的国家)运送主机所发送的数据。 - -* 城域网MAN - -作用范围一般是一个城市,可跨越几个街区甚至整个城市,作用距离通常为5-50公里 - -* 局域网LAN - -一般用微型计算机或工作站通过高速通信线路相连(速率通常在 10 Mbit/s 以上),但地理上范围较小(1 km 左右),比如一栋实验楼,一个校园等 - -* 个域网PAN - -就是在个人工作的地方把个人使用的电子设备用无线技术连接起来的网络。(比如笔记本,耳机,键盘等),覆盖范围大约为10米 - -**按拓扑结构分类:** - -* 总线型网络 - - - -优点:建网容易,增减节点方便,节省线路 - -缺点:重负载是通信效率不高,总线任意一处出现故障,则全网瘫痪。 - -* 星型网络 - - - -星型网络图中的中央设备现在一般是交换机或路由器, - -优点:这种网络拓扑便于网络的集中控制和管理,因为端用户都要通过中央设备 - -缺点:成本高,中央设备对故障敏感 - - - -* 环形网络 - - - -环中信号是单向传输的 - -* 网状型网络 - - - -一般情况下,每个节点至少由两条路径与其他节点相连,多用在广域网中 - -优点:可靠性高 - -缺点:控制复杂,线路成本高 - - - -## 计算机网络的性能指标 - -### 速率 - - - -### 带宽 - - - -### 吞吐量 - - - -带宽1 Gb/s的以太网,代表其额定速率是1 Gb/s,这个数值也是该以太网的吞吐量的上限值。实际上,对于带宽1 Gb/s的以太网,可能实际吞吐量只有 700 Mb/s,甚至更低。 - -### 时延 - -时延时指数据(一个报文或分组,甚至比特)从网络(或链路)的一端传送到另一端所需的时间。 - -我们来看看分组从源主机传送给目的主机的过程中,都会在哪些地方产生时延 - -* 发送时延:源主机将分组发往传输线路所需的时间。 - -* 传播时延:代表分组的电信号在链路上传输所需的时间。 - -* 处理时延:路由器在收到分组后,对其进行存储转发所花费的时间 - -> 有的教材中还有一个排队时延,本课程将排队时延与处理时延合并称为处理时延 - -网卡的发送速率,信道带宽,交换机的接口速率,它们共同决定着主机的发送速率。 - - - -当处理时延忽略不计时,发送时延和传播时延谁占主导,要具体情况具体分析 - - - -### 时延带宽积 - - - - - -### 往返时间 - - - - - -### 利用率 - - - - - -### 丢包率 - - - - - - - -## 计算机网络体系结构 - -### 常见的计算机网络体系结构 - - - -1、如今用的最多的是TCP/IP体系结构,现今规模最大的、覆盖全球的、基于TCP/IP的互联网并未使用OSI标准。TCP/IP体系结构相当于将OSI体系结构的物理层和数据链路层合并为了网络接口层,并去掉了会话层和表示层。 - -2、TCP/IP在网络层使用的协议是IP协议,IP协议的意思是网际协议,因此TCP/IP体系结构的网络层称为网际层 - - - - - -1、在用户主机的操作系统中,通常都带有符合TCP/IP体系结构标准的TCP/IP协议族。而用于网络互连的路由器中,也带有符合TCP/IP体系结构标准的TCP/IP协议族。只不过路由器一般只包含网络接口层和网际层。 - -2、 - -**网络接口层**:并没有规定具体内容,这样做的目的是可以互连全世界各种不同的网络接口,例如:有线的以太网接口,无线局域网的WIFI接口等。因此本质上TCP/IP协议体系结构只有上面的三层。 - -**网际层**:核心协议是IP协议。 - -**运输层**:TCP和UDP是这层的两个重要协议。 - -**应用层**: 包含了大量的应用层协议,如 HTTP , DNS 等。 - - - -3、IP协议可以将不同的网络接口(网络接口层)进行互连,并向其上的TCP协议和UDP协议提供网络互连服务。 - -- 而TCP协议在享受IP协议提供的网络互连服务的基础上,可向应用层的相应协议提供可靠的传输服务。 - -- UDP协议在享受IP协议提供的网络互连服务的基础上,可向应用层的相应协议提供不可靠的传输服务。 - - - -4、TCP/IP体系结构中最重要的是**IP协议**和**TCP协议**,因此用TCP和IP来表示整个协议大家族。 - - - - - - - - - -由于TCP/IP体系结构为了将不同的网络接口进行互连,因此它的网络接口层并没有规定什么具体的内容。然而,这对于我们学习计算机网络的完整体系而言就会缺少一部分内容,因此学习计算机体系结构时往往采取折中的办法。也就是综合OSI和TCP/IP的优点,采用一种5层体系的原理结构。 - -### 分层的必要性 - - - -**物理层问题** - - - - - -严格来说,传输媒体并不属于物理层。计算机传输的信号,并不是图示的方波信号,这样举例只是让初学者容易理解 - -**数据链路层问题** - - - -1、如图所示,主机A要给主机C发送数据。 - -2、但是表示数据的信号会通过总线传播到总线上的每一个主机 - -3、那么问题来了,主机C是如何知道该数据是发送给自己的,自己要接受?而主机B,D,E又如何知道该数据不删发送给自己的,自己应该拒绝呢? - -4、这就很自然的引出了下面几个问题 - -Q:如何标识网络中的各主机(也就是主机编址问题,例如MAC地址)? - -A:主机在发送数据时,应该给数据附加上目的地址,当其他主机收到后,根据目的地址和自身地址来决定是否接受数据。 - - - -Q:目的主机如何从信号所表示的一连串比特流中区分出地址和数据? - -A:也就是需要解决分组的封装格式问题 - - - -Q:另外对于总线型的网络,还会出现下面这种典型的问题 - - - - - -某个时刻,总线是空闲的。 片刻之后,主机B和D同时向总线发送数据,这必然造成信号碰撞。不过这种总线型的网络早已淘汰,现在常用的是使用以太网交换机将多台主机互连形成的交换式以太网。那么,以太网交换机又是如何实现的呢? - - - -**网络层问题** - -> 到这里可能会发现,只要解决了物理层和数据链路层各自所面临的问题,我们就可以实现分组在**一个网络**上传输了。但是,我们每天都会使用的因特网是由**非常多的网络**和路由器互连起来的,仅解决物理层和数据链路层的问题还是不能正常工作。 - - - - - - - -**运输层问题** - -1、假设这台主机中运行着两个与网络通信有关的应用进程(QQ和谷歌浏览器),这台服务器中运行着与网络通信相关的服务器进程。某个时刻,主机收到了来自服务器的分组,那么这些分组应该交给浏览器进程处理呢,还是应该交给QQ进程处理? - -- 引出了我们如何标识与网络通信相关的引用进程,进而解决进程之间基于网络通信的问题。 - - - - - -**应用层问题** - - - - - -**总结** - - - - - -### 分层思想举例 - -> 此例子只是简单的例子,后续可能会写一篇详细的。 - -假设网络拓扑如下所示 - - - - - -**解析:** - -主机和Web服务器之间基于网络的通信,实际上是主机中的浏览器应用进程与Web服务器中的Web服务器应用进程之间基于网络的通信 - - - - - -**各层在整个过程中起到怎样的作用?** - -**1、应用层** - -- 应用层按照HTTP协议的规定构建一个HTTP请求报文 - -- 应用层将HTTP请求报文交付给运输层处理 - - - - - - - -**2、运输层** - -- 运输层给HTTP请求报文添加一个TCP首部,使之成为TCP报文段 - -- TCP报文段的首部格式作用是区分应用进程以及实现可靠传输 - -- 运输层将TCP报文段交付给网络层处理 - - - - - -**3、网络层** - -- 网络层给TCP报文段添加一个IP首部,使之成为IP数据报 - -- IP数据报的首部格式作用是使IP数据报可以在互联网传输,也就是被路由器转发 - -- 网络层将IP数据报交付给数据链路层处理 - - - - - -**4、数据链路层** - - - -- 数据链路层给IP数据报添加一个首部和一个尾部,使之成为帧 (图示右边为首部,左边为尾部) - -- 该首部的作用主要是为了让帧能够在一段链路上或一个网络上传输,能够被相应的目的主机接收 - -- 该尾部的作用是让目的主机检查所接收到的帧是否有误码 - -- 数据链路层将帧交付给物理层 - - - - - - - -**5、物理层** - -* 物理层先将帧看做是比特流,这里的网络N1假设是以太网,所以物理层还会给该比特流前面添加前导码 -* 前导码的作用是为了让目的主机做好接收帧的准备 -* 物理层将装有前导码的比特流变换成相应的信号发送给传输媒体 - - - - - -**6、路由器** - -- 信号通过传输媒体到达路由器 - - - - - -7、**路由器转发** - - - -在路由器中 - -* 物理层将信号变为比特流,然后去掉前导码后,将其交付给数据链路层 -* 数据链路层将帧的首部和尾部去掉后,将其交付给网络层,这实际交付的是IP数据报 -* 网络层解析IP数据报的首部,从中提取目的网络地址 - -* 提取目的网络地址后查找自身路由表。确定转发端口, 以便进行转发 -* 网络层将IP数据报交付给数据链路层 -* 数据链路层给IP数据报添加一个首部和一个尾部,使之成为帧 -* 数据链路层将帧交付给物理层 -* 物理层先将帧看成比特流,这里的网络N2假设是以太网,所以物理层还会给该比特流前面添加前导码 -* 物理层将装有前导码的比特流变换成相应的信号发送给传输媒体,信号通过传输媒体到达Web服务器 - - - - - - - -**8、接收方接收** - -在Web 服务器上 - -* 物理层将信号变换为比特流,然后去掉前导码后成为帧,交付给数据链路层 -* 数据链路层将帧的首部和尾部去掉后成为IP数据报,将其交付给网络层 -* 网络层将IP数据报的首部去掉后成为TCP报文段,将其交付给运输层 -* 运输层将TCP报文段的首部去掉后成为HTTP请求报文,将其交付给应用层 -* 应用层对HTTP请求报文进行解析,然后给主机发回响应报文 - -**发回响应报文的步骤和之前过程类似** - - - -### 专用术语 - -以下介绍的专用术语来源于OSI的七层协议体系结构,但也适用于TCP/IP的四层体系结构和五层协议体系结构 - -**实体** - - - - - -**协议** - - - -1、协议:控制两个对等实体进行逻辑通信的规则的集合 - -2、之所以称为逻辑通信,是因为这种通信其实并不存在,它只是我们假设出来的一种通信,目的在于方便我们单独研究体系结构某一层时而不用考虑其它层 - -3、协议三要素: - -* 语法:定义所交换信息的格式 -* 语义:定义收发双方所要完成的操作 -* 同步:定义收发双发的时序关系 - - - -**服务** - - - - - - - - - - - diff --git a/docs/ElasticSearch/ElasticSearch-入门.md b/docs/ElasticSearch/ElasticSearch-入门.md deleted file mode 100644 index f7e2553..0000000 --- a/docs/ElasticSearch/ElasticSearch-入门.md +++ /dev/null @@ -1,651 +0,0 @@ ---- -title: ElasticSearch-入门篇 -tags: - - ElasticSearch - - ELK - - 全文检索 -categories: - - ElasticSearch - - 用法 -keywords: ElasticSearch,全文检索 -description: ElasticSearch-入门篇,适合做入门,或者知识回顾。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/es.jpg' -abbrlink: 7f60dde9 -date: 2020-02-03 13:11:45 ---- - - - -> 文章很长,喜欢的话,可以关注下博客。这段时间秋招忙完之后,会持续更新新内容 - -# ElasticSearch介绍 - -## 介绍 - -1、elasticsearch是一个基于Lucene的高扩展的分布式搜索服务器,支持开箱即用。 - -2、elasticsearch隐藏了Lucene的复杂性,对外提供Restful 接口来操作索引、搜索。 - - - -**突出优点:** - -1. 扩展性好,可部署上百台服务器集群,处理PB级数据。 - -2. 近实时的去索引数据、搜索数据。 - -**es和solr选择哪个?** - -1. 如果你公司现在用的solr可以满足需求就不要换了。 - -2. 如果你公司准备进行全文检索项目的开发,建议优先考虑elasticsearch,因为像Github这样大规模的搜索都在用它。 - - - -## 倒排索引 - -下图是ElasticSearch的索引结构,下边黑色部分是物理结构,上边黄色部分是逻辑结构,逻辑结构也是为了更好的 去描述ElasticSearch的工作原理及去使用物理结构中的索引文件。 - - - -逻辑结构部分是一个倒排索引表: - -1、将要搜索的文档内容分词,所有不重复的词组成分词列表。 - -2、将搜索的文档最终以Document方式存储起来。 - -3、每个词和docment都有关联。 - -如下: - - - -现在,如果我们想搜到`quick brown`我们只需要查找包含每个词条的文档: - - - -两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单 相似性算法 , 那么,我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳 - -# 基本概念 - - - -1.创建索引库 --------------------->类似于:数据库的建表 - -2.创建映射 --------------------->类似于:数据库的添加表中字段 - -3.创建(添加)文档 --------------------->类似于:数据库的往表中添加数据。术语称这个过程为:创建索引 - -5.搜索文档 --------------------->类似于:从数据库里查数据 - -6.文档 --------------------->类似于:数据库中的一行记录(数据) - -7.Field(域) --------------------->类似于:数据库中的字段 - - - - - - - -## 创建索引库 - -### 概念: - -ES的索引库是一个逻辑概念,它包括了分词列表及文档列表,同一个索引库中存储了相同类型的文档。它就相当于MySQL中的表,或相当于Mongodb中的集合。 - -索引(index) - -```shell -# 索引是 ES 对逻辑数据的逻辑存储,所以可以被分为更小的部分 - -# 可以将索引看成 MySQL 的 Table,索引的结构是为快速有效的全文索引准备的,特别是它不存储原始值 - -# 可以将索引存放在一台机器,或分散在多台机器上 - -# 每个索引有一或多个分片(shard),每个分片可以有多个副本(replica) -``` - -### 操作: - -使用postman这样的工具创建: put http://localhost:9200/索引库名称 - - -```shell -# ES 中提供非结构化索引,实际上在底层 ES 会进行结构化操作,对用户透明 - -PUT http://localhost:9200/索引库名称 -{ - "settings":{ - "index":{ - "number_of_shards":"1", # 分片数 - "number_of_replicas":"0" # 副本数 - } - } -} -``` - -- number_of_shards:设置分片的数量,在集群中通常设置多个分片,表示一个索引库将拆分成多片分别存储不同 的结点,提高了ES的处理能力和高可用性,入门程序使用单机环境,这里设置为1。 - -- number_of_replicas:设置副本的数量,设置副本是为了提高ES的高可靠性,单机环境设置为0. - - - -## 创建映射 - -### 概念 - -在索引中每个文档都包括了一个或多个field,创建映射就是向索引库中创建field的过程,下边是document和field 与关系数据库的概念的类比: - -文档(Document)----- Row记录 - -字段(Field)----- Columns 列 - -注意:6.0之前的版本有type(类型)概念,type相当于关系数据库的表,ES官方将在ES9.0版本中彻底删除type。 上边讲的创建索引库相当于关系数据库中的数据库还是表? - -1、如果相当于数据库就表示一个索引库可以创建很多不同类型的文档,这在ES中也是允许的。 - -2、如果相当于表就表示一个索引库只能存储相同类型的文档,ES官方建议在一个索引库中只存储相同类型的文档。 - -3、所以索引库相当于数句酷的一个表 - - - -### 操作 - -1、我们要把课程信息存储到ES中,这里我们创建课程信息的映射,先来一个简单的映射,如下: - -发送:post http://localhost:9200/索引库名称/类型名称/_mapping - -2、创建类型为xc_course的映射,共包括三个字段:name、description、studymondel 由于ES6.0版本还没有将type彻底删除,所以暂时把type起一个没有特殊意义的名字doc。post 请求:http://localhost:9200/xc_course/doc/_mapping - -表示:在xc_course索引库下的doc类型下创建映射。doc是类型名,可以自定义,在ES6.0中要弱化类型的概念, 给它起一个没有具体业务意义的名称。 - -```json - { - "properties": { - "name": { - "type": "text" - }, - - "description":{ - "type": "text" - }, - - "studymodel":{ - "type":"keyword" - } - } -} -``` - - -## 创建文档 - -### 概念 - -ES中的文档相当于MySQL数据库表中的记录。 - -```shell -# 存储在 ES 中的主要实体叫文档,可以看成 MySQL 的一条记录 - -# ES 与 Mongo 的 document 类似,都可以有不同的结构,但 ES 相同字段必须有相同类型 - -# document 由多个字段组成,每个字段可能多次出现在一个文档里,这样的字段叫多值字段(multivalued) - -# 每个字段的类型,可以使文本、数值、日期等。 - -# 字段类型也可以是复杂类型,一个字段包含其他子文档或者数组 - -# 在 ES 中,一个索引对象可以存储很多不同用途的 document,例如一个博客App中,可以保存文章和评论 - -# 每个 document 可以有不同的结构 - -# 不同的 document 不能为相同的属性设置不同的类型,例 : title 在同一索引中所有 Document 都应该相同数据类型 -``` - - - -### 操作 - -发送:put 或Post http://localhost:9200/xc_course/doc/id值 - -(如果不指定id值ES会自动生成ID) - -http://localhost:9200/xc_course/doc/4028e58161bcf7f40161bcf8b77c0000 - -```json - -{ - "name":”Bootstrap开发框架", - - "description" : "Bootstrap是由Twitter推出的一个前台页面开发框架,在行业之中使用较为广泛。此开发框架包含 了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现个不受浏览器限制的精美界面 效果。”, - - "studymodel": "201001" - -} -``` - - - -## 搜索文档 - -1、根据课程id查询文档 - -发送:get http://localhost:9200/xc_course/doc/4028e58161bcf7f40161bcf8b77c0000 - -使用postman测试: - - - - - -2、查询所有记录 - -发送 get http://localhost:9200/xc_course/doc/_search - - - - - -3、查询名称中包括spring 关键字的的记录 - -发送:get http://localhost:9200/xc_course/doc/_search?q=name:bootstrap - - - - - -4、查询学习模式为201001的记录 - -发送 get http://localhost:9200/xc_course/doc/_search?q=studymodel:201001 - - - -**查询结果分析:** - -```json -{ - "took": 1, - "timed_out": false, - "_shards": { - "total": 1, - "successful": 1, - "skipped": 0, - "failed": 0 - }, - "hits": { - "total": 1, - "max_score": 0.2876821, - "hits": [ - { - "_index": "xc_course", - "_type": "doc", - "_id": "4028e58161bcf7f40161bcf8b77c0000", - "_score": 0.2876821, - "_source": { - "name": "Bootstrap开发框架", - "description": "Bootstrap是由Twitter推出的一个前台页面开发框架,在行业之中使用较 为广泛。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现 一个不受浏览器限制的精美界面效果。", - "studymodel": "201001" - } - } - ] - } -} -``` - -**结果说明:** - -took:本次操作花费的时间,单位为毫秒。timed_out:请求是否超时 - -_shards:说明本次操作共搜索了哪些分片hits:搜索命中的记录 - -hits.total : 符合条件的文档总数 hits.hits :匹配度较高的前N个文档 - -hits.max_score:文档匹配得分,这里为最高分 - -_score:每个文档都有一个匹配度得分,按照降序排列。 - -_source:显示了文档的原始内容。 - - - - - -# 分词 - -## 内置分词 - -### 分词API - -分词是将一个文本转换成一系列单词的过程,也叫文本分析,在 ES 中称之为 Analysis - -例如 : 我是中国人 -> 我 | 是 | 中国人 - -```json -# 指定分词器进行分词 -POST http://['自己的ip 加 port']/_analyze -{ - "analyzer":"standard", - "text":"hello world" -} - -# 结果中不仅可以看出分词的结果,还返回了该词在文本中的位置 - -# 指定索引分词 -POST http://['自己的ip 加 port']/beluga/_analyze -{ - "analyzer":"standard", - "field":"hobby", - "text":"听音乐" -} -``` - - - -### Standard - -```shell -# Standard 标准分词,按单词切分,并且会转换成小写 -POST http://['自己的ip 加 port']/_analyze -{ - "analyzer":"standard", - "text": "A man becomes learned by asking questions." -} -``` - -### Simple - -```shell -# Simple 分词器,按照非单词切分,并且做小写处理 -POST http://['自己的ip 加 port']/_analyze -{ - "analyzer":"simple", - "text":"If the document does't already exist" -} -``` - -### Whitespace - -```shell -# Whitespace 是按照空格切分 -POST http://['自己的ip 加 port']/_analyze -{ - "analyzer":"whitespace", - "text":"If the document does't already exist" -} -``` - -### Stop - -```shell -# Stop 去除 Stop Word 语气助词,如 the、an 等 -POST http://['自己的ip 加 port']/_analyze -{ - "analyzer":"stop", - "text":"If the document does't already exist" -} -``` - -### Keyword - -```shell -# keyword 分词器,意思是传入就是关键词,不做分词处理 -POST http://['自己的ip 加 port']/_analyze -{ - "analyzer":"keyword", - "text":"If the document does't already exist" -} -``` - -### 中文分词 - -```shell -# 中文分词的难点在于,汉语中没有明显的词汇分界点 - -# 常用中文分词器,IK jieba THULAC 等,推荐 IK - -# IK Github 站点<自定义词典扩展,禁用词典扩展等> -https://github.com/medcl/elasticsearch-analysis-ik -``` - - - -## IK分词器 - -安装过程这里不介绍,主要是解决常见中文分词的问题 - -Github地址:https://github.com/medcl/elasticsearch-analysis-ik - -### 两种分词模式 - -ik分词器有两种分词模式:ik_max_word和ik_smart模式。 - - 1、ik_max_word - -会将文本做最细粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为“中华人民共和国、中华人民、中华、 华人、人民共和国、人民、共和国、大会堂、大会、会堂等词语。 - -2、ik_smart - -会做最粗粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为中华人民共和国、人民大会堂。 测试两种分词模式: - - - - - -# 映射 - -上边章节安装了ik分词器,如果在索引和搜索时去使用ik分词器呢?如何指定其它类型的field,比如日期类型、数 值类型等。本章节学习各种映射类型及映射维护方法。 - -## 映射维护方法 - -1、查询所有索引的映射: - -GET: http://localhost:9200/_mapping - -2、创建映射 - -post 请求:http://localhost:9200/xc_course/doc/_mapping - -在上面提到过 - -``` - { - "properties": { - "name": { - "type": "text" - }, - - "description":{ - "type": "text" - }, - - "studymodel":{ - "type":"keyword" - } - } -} -``` - - - -3、更新映射 - -映射创建成功可以添加新字段,已有字段不允许更新。 - -4、删除映射 - -通过删除索引来删除映射。 - - - -## 常用映射类型 - -### text文本字段 - -**1)text** - -字符串包括text和keyword两种类型: 通过analyzer属性指定分词器。 - -下边指定name的字段类型为text,使用ik分词器的ik_max_word分词模式。 - -```json -{ - "name": { - "type": "text", - "analyzer": "ik_max_word" - } -} -``` - -上边指定了analyzer是指在索引和搜索都使用ik_max_word,如果单独想定义搜索时使用的分词器则可以通过search_analyzer属性。 - -对于ik分词器建议是索引时使用ik_max_word将搜索内容进行细粒度分词,搜索时使用ik_smart提高搜索精确性。 - -```json -{ - "name": { - "type": "text", - "analyzer": "ik_max_word", - "search_analyzer": "ik_smart" - } -} -``` - -**2) index** - -通过index属性指定是否索引。 - -默认为index=true,即要进行索引,只有进行索引才可以从索引库搜索到。 - -但是也有一些内容不需要索引,比如:商品图片地址只被用来展示图片,不进行搜索图片,此时可以将index设置 为false。 - -删除索引,重新创建映射,将pic的index设置为false,尝试根据pic去搜索,结果搜索不到数据 - -```json -{ - "pic": { - "type": "text", - "index": false - } -} -``` - - - -**3)store** - -是否在source之外存储,每个文档索引后会在 ES中保存一份原始文档,存放在"_source"中,一般情况下不需要设置 store为true,因为在_source中已经有一份原始文档了。 - - - -### keyword关键字字段 - -上边介绍的text文本字段在映射时要设置分词器,keyword字段为关键字字段,通常搜索keyword是按照整体搜 索,所以创建keyword字段的索引时是不进行分词的,比如:邮政编码、手机号码、身份证等。keyword字段通常 用于过虑、排序、聚合等。 - -**测试:** - -更改映射: - -```json -{ - "properties": { - "studymodel": { - "type": "keyword" - }, - "name": { - "type": "keyword" - } - } -} -``` - -添加文档: - -```json -{ - "name": "java编程基础", - "description": "java语言是世界第一编程语言,在软件开发领域使用人数最多。", - "pic": "group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg", - "studymodel": "201001" -} -``` - -根据name查询文档。搜索:http://localhost:9200/xc_course/_search?q=name:java name是keyword类型,所以查询方式是精确查询。 - - - -### 日期类型 - -日期类型不用设置分词器。 - -通常日期类型的字段用于排序。 - -1)format - -通过format设置日期格式例子: - -下边的设置允许date字段存储年月日时分秒、年月日及毫秒三种格式 - -```json -{ - "properties": { - "timestamp": { - "type": "date", - "format": "yyyy‐MM‐dd HH:mm:ss||yyyy‐MM‐dd" - } - } -} -``` - -插入文档: - -Post :http://localhost:9200/xc_course/doc/3 - -```json -{ - "name": "spring开发基础", - "description": "spring 在java领域非常流行,java程序员都在用。", - "studymodel": "201001", - "pic": "group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg", - "timestamp": "2018‐07‐04 18:28:58" -} -``` - - - -### 综合例子 - -post:http://localhost:9200/xc_course/doc/_mapping - -```json -{ - "properties": { - "description": { - "type": "text", - "analyzer": "ik_max_word", - "search_analyzer": "ik_smart" - }, - "name": { - "type": "text", - "analyzer": "ik_max_word", - "search_analyzer": "ik_smart" - }, - "pic": { - "type": "text", - "index": false - }, - "price": { - "type": "float" - }, - "studymodel": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "yyyy‐MM‐dd HH:mm:ss||yyyy‐MM‐dd||epoch_millis" - } - } -} -``` - diff --git a/docs/ElasticSearch/ElasticSearch-进阶.md b/docs/ElasticSearch/ElasticSearch-进阶.md deleted file mode 100644 index 4cf7978..0000000 --- a/docs/ElasticSearch/ElasticSearch-进阶.md +++ /dev/null @@ -1,1732 +0,0 @@ ---- -title: ElasticSearch-进阶篇 -tags: - - ElasticSearch - - ELK - - 全文检索 -categories: - - ElasticSearch - - 用法 -keywords: ElasticSearch,全文检索 -description: ElasticSearch-进阶篇,ElasticSearch的一些实战用法,集成SpringBoot。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/es.jpg' -abbrlink: 50e81c79 -date: 2020-02-08 18:06:23 ---- - - - -# 搭建工程 - -ES提供多种不同的客户端: - -1、TransportClient - -ES提供的传统客户端,官方计划8.0版本删除此客户端。 - -2、RestClient - -RestClient是官方推荐使用的,它包括两种:Java Low Level REST Client和 Java High Level REST Client。 - -ES在6.0之后提供 Java High Level REST Client, 两种客户端官方更推荐使用 Java High Level REST Client,不过当 - -前它还处于完善中,有些功能还没有。 - - - -我们采用SpringBoot2.x与ElasticSearch集成 - -## Maven依赖 - -部分依赖 - -```maven - - UTF-8 - UTF-8 - 1.8 - 6.3.2 - - - - - - - org.elasticsearch.client - transport - ${elasticsearch.version} - - - org.elasticsearch.client - elasticsearch-rest-high-level-client - ${elasticsearch.version} - - - org.elasticsearch - elasticsearch - ${elasticsearch.version} - - - -``` - -## application.properties - -```properties -#elasticsearch配置 -anshe.elasticsearch.hostlist=${eshostlist:你的IP地址:9200} -``` - - - -## 配置类 - -```java -package com.anshe.common.config.es; - -import com.anshe.web.service.ISearchService; -import org.apache.http.HttpHost; -import org.elasticsearch.client.RestClient; -import org.elasticsearch.client.RestHighLevelClient; -import org.elasticsearch.client.transport.TransportClient; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.transport.TransportAddress; -import org.elasticsearch.transport.client.PreBuiltTransportClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.net.InetAddress; - -/** - * @author Administrator - * @version 1.0 - **/ -@Configuration -public class ElasticsearchConfig { - private static final Logger logger = LoggerFactory.getLogger(ISearchService.class); - - @Value("${anshe.elasticsearch.hostlist}") - private String hostlist; - - @Bean - public RestHighLevelClient restHighLevelClient(){ - //解析hostlist配置信息 - String[] split = hostlist.split(","); - //创建HttpHost数组,其中存放es主机和端口的配置信息 - HttpHost[] httpHostArray = new HttpHost[split.length]; - for(int i=0;i jsonMap = new HashMap<>(); - jsonMap.put("name", "spring cloud实战"); - jsonMap.put("description", "本课程主要从四个章节进行讲解: 1.微服务架构入门 2.spring cloud 基础入门 3.实战Spring Boot 4.注册中心eureka。"); - jsonMap.put("studymodel", "201001"); - SimpleDateFormat dateFormat =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - jsonMap.put("timestamp", dateFormat.format(new Date())); - jsonMap.put("price", 5.6f); - - //创建索引创建对象 - IndexRequest indexRequest = new IndexRequest("xc_course","doc"); - //文档内容 - indexRequest.source(jsonMap); - //通过client进行http的请求 - IndexResponse indexResponse = client.index(indexRequest); - DocWriteResponse.Result result = indexResponse.getResult(); - System.out.println(result); - -} -``` - - - -## 查询文档 - -### API - -格式如下: GET /{index}/{type}/{id} - -### Java客户端 - -```java -//查询文档 -@Test -public void testGetDoc() throws IOException { - //查询请求对象 - GetRequest getRequest = new GetRequest("xc_course","doc","0fOCF2sBEYTsNRZ43I8b"); - GetResponse getResponse = client.get(getRequest); - //得到文档的内容 - Map sourceAsMap = getResponse.getSourceAsMap(); - System.out.println(sourceAsMap); -} -``` - - - -## 更新文档 - -### API - -ES更新文档的顺序是:先检索到文档、将原来的文档标记为删除、创建新文档、删除旧文档,创建新文档就会重建 - -索引。 - -通过请求Url有两种方法: - -**1、完全替换** - -Post:http://localhost:9200/xc_test/doc/3 - -```json -{ - "name": "spring cloud实战", - "description": "本课程主要从四个章节进行讲解: 1.微服务架构入门 2.spring cloud 基础入门 3.实战SpringBoot 4.注册中心eureka。", - "studymodel": "201001", - "price": 5.6 -} -``` - -**2、局部更新** - -下边的例子是只更新price字段。 - -post: http://localhost:9200/xc_test/doc/3/_update - -```json -{ - "doc": { - "price": 66.6 - } -} -``` - - - -### Java客户端 - -使用 Client Api更新文档的方法同上边第二种局部更新方法。 - -可以指定文档的部分字段也可以指定完整的文档内容。 - -``` -//更新文档 -@Test public void updateDoc() throws IOException { - UpdateRequest updateRequest = new UpdateRequest("xc_course", "doc", "4028e581617f945f01617f9dabc40000"); - Map map = new HashMap<>(); - map.put("name", "spring cloud实战"); - updateRequest.doc(map); - UpdateResponse update = client.update(updateRequest); - RestStatus status = update.status(); - System.out.println(status); -} -``` - - - - - -## 删除文档 - -### API - -1、根据id删除,格式如下: - -DELETE /{index}/{type}/{id} - -2、搜索匹配删除,将搜索出来的记录删除,格式如下: - -POST /{index}/{type}/_delete_by_query - -下边是搜索条件例子: - -```json -{ - "query": { - "term": { - "studymodel": "201001" - } - } -} -``` - -上边例子的搜索匹配删除会将studymodel为201001的记录全部删除 - -### Java客户端 - -```java -//根据id删除文档 -@Test -public void testDelDoc() throws IOException { - //删除文档id - String id = "eqP_amQBKsGOdwJ4fHiC"; - //删除索引请求对象 - DeleteRequest deleteRequest = new DeleteRequest("xc_course","doc",id); - //响应对象 - DeleteResponse deleteResponse = client.delete(deleteRequest); - //获取响应结果 - DocWriteResponse.Result result = deleteResponse.getResult(); - System.out.println(result); -} -``` - -搜索匹配删除还没有具体的api,可以采用先搜索出文档id,根据文档id删除。 - - - -# -----下面是DSL搜索的内容----- - -# DSL搜索环境准备 - -## 创建映射 - -创建xc_course索引库。 - -创建如下映射 - -post:http://localhost:9200/xc_course/doc/_mapping - -```json -{ - "properties": { - "description": { - "type": "text", - "analyzer": "ik_max_word", - "search_analyzer": "ik_smart" - }, - "name": { - "type": "text", - "analyzer": "ik_max_word", - "search_analyzer": "ik_smart" - }, - "pic": { - "type": "text", - "index": false - }, - "price": { - "type": "float" - }, - "studymodel": { - "type": "keyword" - }, - "timestamp": { - "type": "date", - "format": "yyyy‐MM‐dd HH:mm:ss||yyyy‐MM‐dd||epoch_millis" - } - } -} -``` - - - - - -## 插入原始数据 - -向xc_course/doc中插入以下数据: - -```json -http://localhost:9200/xc_course/doc/1 -{ - "name": "Bootstrap开发", - "description": "Bootstrap是由Twitter推出的一个前台页面开发框架,是一个非常流行的开发框架,此框架集成了 多种页面效果。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松 的实现一个不受浏览器限制的精美界面效果。", - "studymodel": "201002", - "price": 38.6, - "timestamp": "2018‐04‐25 19:11:35", - "pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg" -} - - -http://localhost:9200/xc_course/doc/2 -{ - "name": "java编程基础", - "description": "java语言是世界第一编程语言,在软件开发领域使用人数最多。", - "studymodel": "201001", - "price": 68.6, - "timestamp": "2018‐03‐25 19:11:35", - "pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg" -} - - -http://localhost:9200/xc_course/doc/3 -{ - "name": "spring开发基础", - "description": "spring 在java领域非常流行,java程序员都在用。", - "studymodel": "201001", - "price": 88.6, - "timestamp": "2018‐02‐24 19:11:35", - "pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg" -} - -``` - - - - - -> DSL(Domain Specifific Language)是ES提出的基于json的搜索方式,在搜索时传入特定的json格式的数据来完成不 同的搜索需求。 DSL比URI搜索方式功能强大,在项目中建议使用DSL方式来完成搜索。 - - - - - -# 查询所有文档 - -## API - -查询所有索引库的文档。 - -发送:post http://localhost:9200/_search - -查询指定索引库指定类型下的文档。(通过使用此方法) - -发送:post http://localhost:9200/xc_course/doc/_search - -```json -{ - "query": { - "match_all": {} - }, - "_source": [ - "name", - "studymodel" - ] -} -``` - -_source:source源过虑设置,指定结果中所包括的字段有哪些。 - -**结果说明:** - -took:本次操作花费的时间,单位为毫秒。 - -timed_out:请求是否超时 - -_shards:说明本次操作共搜索了哪些分片 - -hits:搜索命中的记录 - -hits.total : 符合条件的文档总数 hits.hits :匹配度较高的前N个文档 - -hits.max_score:文档匹配得分,这里为最高分 - -_score:每个文档都有一个匹配度得分,按照降序排列。 - -_source:显示了文档的原始内容。 - -## Java客户端 - -```java - @Autowired - RestHighLevelClient client; - - @Autowired - RestClient restClient; - -//搜索全部记录 - @Test - public void testSearchAll() throws IOException, ParseException { - //搜索请求对象 - SearchRequest searchRequest = new SearchRequest("xc_course"); - //指定类型 - searchRequest.types("doc"); - //搜索源构建对象 - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - //搜索方式 - //matchAllQuery搜索全部 - searchSourceBuilder.query(QueryBuilders.matchAllQuery()); - //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 - searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); - //向搜索请求对象中设置搜索源 - searchRequest.source(searchSourceBuilder); - //执行搜索,向ES发起http请求 - SearchResponse searchResponse = client.search(searchRequest); - //搜索结果 - SearchHits hits = searchResponse.getHits(); - //匹配到的总记录数 - long totalHits = hits.getTotalHits(); - //得到匹配度高的文档 - SearchHit[] searchHits = hits.getHits(); - //日期格式化对象 -// SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); - for(SearchHit hit:searchHits){ - //文档的主键 - String id = hit.getId(); - //源文档内容 - Map sourceAsMap = hit.getSourceAsMap(); - String name = (String) sourceAsMap.get("name"); - //由于前边设置了源文档字段过虑,这时description是取不到的 - String description = (String) sourceAsMap.get("description"); - //学习模式 - String studymodel = (String) sourceAsMap.get("studymodel"); - //价格 - Double price = (Double) sourceAsMap.get("price"); - //日期 - Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); - System.out.println(name); - System.out.println(studymodel); - System.out.println(description); - } - - } -``` - - - -# 分页查询 - -## API - -ES支持分页查询,传入两个参数:from和size。 - -form:表示起始文档的下标,从0开始。 - -size:查询的文档数量。 - -发送:post http://localhost:9200/xc_course/doc/_search - -```json -{ - "from": 0, - "size": 1, - "query": { - "match_all": {} - }, - "_source": [ - "name", - "studymodel" - ] -} -``` - - - -## Java客户端 - -```java -//分页查询 -@Test -public void testSearchPage() throws IOException, ParseException { - //搜索请求对象 - SearchRequest searchRequest = new SearchRequest("xc_course"); - //指定类型 - searchRequest.types("doc"); - //搜索源构建对象 - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - //设置分页参数 - //页码 - int page = 1; - //每页记录数 - int size = 1; - //计算出记录起始下标 - int from = (page-1)*size; - searchSourceBuilder.from(from);//起始记录下标,从0开始 - searchSourceBuilder.size(size);//每页显示的记录数 - //搜索方式 - //matchAllQuery搜索全部 - searchSourceBuilder.query(QueryBuilders.matchAllQuery()); - //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 - searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); - //向搜索请求对象中设置搜索源 - searchRequest.source(searchSourceBuilder); - //执行搜索,向ES发起http请求 - SearchResponse searchResponse = client.search(searchRequest); - //搜索结果 - SearchHits hits = searchResponse.getHits(); - //匹配到的总记录数 - long totalHits = hits.getTotalHits(); - //得到匹配度高的文档 - SearchHit[] searchHits = hits.getHits(); - //日期格式化对象 - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - for(SearchHit hit:searchHits){ - //文档的主键 - String id = hit.getId(); - //源文档内容 - Map sourceAsMap = hit.getSourceAsMap(); - String name = (String) sourceAsMap.get("name"); - //由于前边设置了源文档字段过虑,这时description是取不到的 - String description = (String) sourceAsMap.get("description"); - //学习模式 - String studymodel = (String) sourceAsMap.get("studymodel"); - //价格 - Double price = (Double) sourceAsMap.get("price"); - //日期 - Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); - System.out.println(name); - System.out.println(studymodel); - System.out.println(description); - } - -} -``` - - - -# Term Query - -## API - -Term Query为精确查询,在搜索时会整体匹配关键字,不再将关键字分词。 - -发送:post http://localhost:9200/xc_course/doc/_search - -```java -{ - "query": { - "term": { - "name": "spring" - } - }, - "_source": [ - "name", - "studymodel" - ] -} -``` - -上边的搜索会查询name包括“spring”这个词的文档。 - - - -## Java客户端 - -```java -//TermQuery -@Test -public void testTermQuery() throws IOException, ParseException { - //搜索请求对象 - SearchRequest searchRequest = new SearchRequest("xc_course"); - //指定类型 - searchRequest.types("doc"); - //搜索源构建对象 - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - //设置分页参数 - //页码 - int page = 1; - //每页记录数 - int size = 1; - //计算出记录起始下标 - int from = (page-1)*size; - searchSourceBuilder.from(from);//起始记录下标,从0开始 - searchSourceBuilder.size(size);//每页显示的记录数 - //搜索方式 - //termQuery - searchSourceBuilder.query(QueryBuilders.termQuery("name","spring")); - //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 - searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); - //向搜索请求对象中设置搜索源 - searchRequest.source(searchSourceBuilder); - //执行搜索,向ES发起http请求 - SearchResponse searchResponse = client.search(searchRequest); - //搜索结果 - SearchHits hits = searchResponse.getHits(); - //匹配到的总记录数 - long totalHits = hits.getTotalHits(); - //得到匹配度高的文档 - SearchHit[] searchHits = hits.getHits(); - //日期格式化对象 - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - for(SearchHit hit:searchHits){ - //文档的主键 - String id = hit.getId(); - //源文档内容 - Map sourceAsMap = hit.getSourceAsMap(); - String name = (String) sourceAsMap.get("name"); - //由于前边设置了源文档字段过虑,这时description是取不到的 - String description = (String) sourceAsMap.get("description"); - //学习模式 - String studymodel = (String) sourceAsMap.get("studymodel"); - //价格 - Double price = (Double) sourceAsMap.get("price"); - //日期 - Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); - System.out.println(name); - System.out.println(studymodel); - System.out.println(description); - } - -} -``` - - - -# 根据id精确匹配 - -## API - -ES提供根据多个id值匹配的方法: - -测试: - -post: http://127.0.0.1:9200/xc_course/doc/_search - -```json -{ - "query": { - "ids": { - "type": "doc", - "values": [ - "3", - "4", - "100" - ] - } - } -} -``` - - - -## Java客户端 - -```java -//根据id查询 -@Test -public void testTermQueryByIds() throws IOException, ParseException { - //搜索请求对象 - SearchRequest searchRequest = new SearchRequest("xc_course"); - //指定类型 - searchRequest.types("doc"); - //搜索源构建对象 - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - //搜索方式 - //根据id查询 - //定义id - String[] ids = new String[]{"1","2"}; - searchSourceBuilder.query(QueryBuilders.termsQuery("_id",ids)); - //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 - searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); - //向搜索请求对象中设置搜索源 - searchRequest.source(searchSourceBuilder); - //执行搜索,向ES发起http请求 - SearchResponse searchResponse = client.search(searchRequest); - //搜索结果 - SearchHits hits = searchResponse.getHits(); - //匹配到的总记录数 - long totalHits = hits.getTotalHits(); - //得到匹配度高的文档 - SearchHit[] searchHits = hits.getHits(); - //日期格式化对象 - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - for(SearchHit hit:searchHits){ - //文档的主键 - String id = hit.getId(); - //源文档内容 - Map sourceAsMap = hit.getSourceAsMap(); - String name = (String) sourceAsMap.get("name"); - //由于前边设置了源文档字段过虑,这时description是取不到的 - String description = (String) sourceAsMap.get("description"); - //学习模式 - String studymodel = (String) sourceAsMap.get("studymodel"); - //价格 - Double price = (Double) sourceAsMap.get("price"); - //日期 - Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); - System.out.println(name); - System.out.println(studymodel); - System.out.println(description); - } - -} -``` - - - - - -# match Query - -## API - -match Query即全文检索,它的搜索方式是先将搜索字符串分词,再使用各各词条从索引中搜索。 - -match query与Term query区别是match query在搜索前先将搜索关键字分词,再拿各各词语去索引中搜索。 - -发送:post http://localhost:9200/xc_course/doc/_search - -```json -{ - "query": { - "match": { - "description": { - "query": "spring开发", - "operator": "or" - } - } - } -} -``` - -query:搜索的关键字,对于英文关键字如果有多个单词则中间要用半角逗号分隔,而对于中文关键字中间可以用 - -逗号分隔也可以不用。 - -operator:or 表示 只要有一个词在文档中出现则就符合条件,and表示每个词都在文档中出现则才符合条件。 - -上边的搜索的执行过程是: - -1、将“spring开发”分词,分为spring、开发两个词 - -2、再使用spring和开发两个词去匹配索引中搜索。 - -3、由于设置了operator为or,只要有一个词匹配成功则就返回该文档。 - - - -## Java客户端 - -```java -//MatchQuery -@Test -public void testMatchQuery() throws IOException, ParseException { - //搜索请求对象 - SearchRequest searchRequest = new SearchRequest("xc_course"); - //指定类型 - searchRequest.types("doc"); - //搜索源构建对象 - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - - //搜索方式 - //MatchQuery - searchSourceBuilder.query(QueryBuilders.matchQuery("description","spring开发框架") - .minimumShouldMatch("80%")); - //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 - searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); - //向搜索请求对象中设置搜索源 - searchRequest.source(searchSourceBuilder); - //执行搜索,向ES发起http请求 - SearchResponse searchResponse = client.search(searchRequest); - //搜索结果 - SearchHits hits = searchResponse.getHits(); - //匹配到的总记录数 - long totalHits = hits.getTotalHits(); - //得到匹配度高的文档 - SearchHit[] searchHits = hits.getHits(); - //日期格式化对象 - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - for(SearchHit hit:searchHits){ - //文档的主键 - String id = hit.getId(); - //源文档内容 - Map sourceAsMap = hit.getSourceAsMap(); - String name = (String) sourceAsMap.get("name"); - //由于前边设置了源文档字段过虑,这时description是取不到的 - String description = (String) sourceAsMap.get("description"); - //学习模式 - String studymodel = (String) sourceAsMap.get("studymodel"); - //价格 - Double price = (Double) sourceAsMap.get("price"); - //日期 - Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); - System.out.println(name); - System.out.println(studymodel); - System.out.println(description); - } - -} -``` - - - -# multi Query - -## API - -**1、基本使用** - -上边学习的termQuery和matchQuery一次只能匹配一个Field,本节学习multiQuery,一次可以匹配多个字段。 - -单项匹配是在一个fifield中去匹配,多项匹配是拿关键字去多个Field中匹配。 - -例子: - -发送:post http://localhost:9200/xc_course/doc/_search - -拿关键字 “spring css”去匹配name 和description字段。 - -```json -{ - "query": { - "multi_match": { - "query": "spring css", - "minimum_should_match": "50%", - "fields": [ - "name", - "description" - ] - } - } -} -``` - - - -**2、提升boost** - -匹配多个字段时可以提升字段的boost(权重)来提高得分 - -例子: - -提升boost之前,执行下边的查询: - -```json -{ - "query": { - "multi_match": { - "query": "spring框架", - "minimum_should_match": "50%", - "fields": [ - "name", - "description" - ] - } - } -} -``` - -通过查询发现Bootstrap排在前边。 - -提升boost,通常关键字匹配上name的权重要比匹配上description的权重高,这里可以对name的权重提升 - -```json -{ - "query": { - "multi_match": { - "query": "spring框架", - "minimum_should_match": "50%", - "fields": [ - "name^10", - "description" - ] - } - } -} -``` - -“name^10” 表示权重提升10倍,执行上边的查询,发现name中包括spring关键字的文档排在前边。 - - - -## Java客户端 - -```java -//MultiMatchQuery -@Test -public void testMultiMatchQuery() throws IOException, ParseException { - //搜索请求对象 - SearchRequest searchRequest = new SearchRequest("xc_course"); - //指定类型 - searchRequest.types("doc"); - //搜索源构建对象 - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - - //搜索方式 - //MultiMatchQuery - searchSourceBuilder.query(QueryBuilders.multiMatchQuery("spring css","name","description") - .minimumShouldMatch("50%") - .field("name",10)); - //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 - searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); - //向搜索请求对象中设置搜索源 - searchRequest.source(searchSourceBuilder); - //执行搜索,向ES发起http请求 - SearchResponse searchResponse = client.search(searchRequest); - //搜索结果 - SearchHits hits = searchResponse.getHits(); - //匹配到的总记录数 - long totalHits = hits.getTotalHits(); - //得到匹配度高的文档 - SearchHit[] searchHits = hits.getHits(); - //日期格式化对象 - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - for(SearchHit hit:searchHits){ - //文档的主键 - String id = hit.getId(); - //源文档内容 - Map sourceAsMap = hit.getSourceAsMap(); - String name = (String) sourceAsMap.get("name"); - //由于前边设置了源文档字段过虑,这时description是取不到的 - String description = (String) sourceAsMap.get("description"); - //学习模式 - String studymodel = (String) sourceAsMap.get("studymodel"); - //价格 - Double price = (Double) sourceAsMap.get("price"); - //日期 - Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); - System.out.println(name); - System.out.println(studymodel); - System.out.println(description); - } - -} -``` - - - -# 布尔查询 - -## API - -布尔查询对应于Lucene的BooleanQuery查询,实现将多个查询组合起来。 - -- 三个参数: - - - must:文档必须匹配must所包括的查询条件,相当于 “AND” - - - should:文档应该匹配should所包括的查询条件其中的一个或多个,相当于 "OR" - - - must_not:文档不能匹配must_not所包括的该查询条件,相当于“NOT” - -分别使用must、should、must_not测试下边的查询: - -发送:POST http://localhost:9200/xc_course/doc/_search - -```json -{ - "_source": [ - "name", - "studymodel", - "description" - ], - "from": 0, - "size": 1, - "query": { - "bool": { - "must": [ - { - "multi_match": { - "query": "spring框架", - "minimum_should_match": "50%", - "fields": [ - "name^10", - "description" - ] - } - }, - { - "term": { - "studymodel": "201001" - } - } - ] - } - } -} -``` - -must:表示必须,多个查询条件必须都满足。(通常使用must) - -should:表示或者,多个查询条件只要有一个满足即可。 - -must_not:表示非。 - - - -## Java客户端 - -```java -//BoolQuery其实是一个过滤搜索 -@Test -public void testBoolQuery() throws IOException, ParseException { - //搜索请求对象 - SearchRequest searchRequest = new SearchRequest("xc_course"); - //指定类型 - searchRequest.types("doc"); - //搜索源构建对象 - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - - //boolQuery搜索方式 - //先定义一个MultiMatchQuery - MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("spring css", "name", "description") - .minimumShouldMatch("50%") - .field("name", 10); - //再定义一个termQuery - TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("studymodel", "201001"); - - //定义一个boolQuery - BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); - boolQueryBuilder.must(multiMatchQueryBuilder); - boolQueryBuilder.must(termQueryBuilder); - - searchSourceBuilder.query(boolQueryBuilder); - //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 - searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); - //向搜索请求对象中设置搜索源 - searchRequest.source(searchSourceBuilder); - //执行搜索,向ES发起http请求 - SearchResponse searchResponse = client.search(searchRequest); - //搜索结果 - SearchHits hits = searchResponse.getHits(); - //匹配到的总记录数 - long totalHits = hits.getTotalHits(); - //得到匹配度高的文档 - SearchHit[] searchHits = hits.getHits(); - //日期格式化对象 - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - for(SearchHit hit:searchHits){ - //文档的主键 - String id = hit.getId(); - //源文档内容 - Map sourceAsMap = hit.getSourceAsMap(); - String name = (String) sourceAsMap.get("name"); - //由于前边设置了源文档字段过虑,这时description是取不到的 - String description = (String) sourceAsMap.get("description"); - //学习模式 - String studymodel = (String) sourceAsMap.get("studymodel"); - //价格 - Double price = (Double) sourceAsMap.get("price"); - //日期 - Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); - System.out.println(name); - System.out.println(studymodel); - System.out.println(description); - } - -} -``` - - - - - -# 过虑器 - -## API - -​ 过虑是针对搜索的结果进行过虑,过虑器主要判断的是文档是否匹配,不去计算和判断文档的匹配度得分,所以过 虑器性能比查询要高,且方便缓存,推荐尽量使用过虑器去实现查询或者过虑器和查询共同使用。 过虑器在布尔查询中使用,下边是在搜索结果的基础上进行过虑: - -```json -{ - "_source": [ - "name", - "studymodel", - "description", - "price" - ], - "query": { - "bool": { - "must": [ - { - "multi_match": { - "query": "spring框架", - "minimum_should_match": "50%", - "fields": [ - "name^10", - "description" - ] - } - } - ], - "filter": [ - { - "term": { - "studymodel": "201001" - } - }, - { - "range": { - "price": { - "gte": 60, - "lte": 100 - } - } - } - ] - } - } -} -``` - -range:范围过虑,保留大于等于60 并且小于等于100的记录。 - -term:项匹配过虑,保留studymodel等于"201001"的记录。 - -注意:range和term一次只能对一个Field设置范围过虑。 - - - -## Java客户端 - -```java -//filter -@Test -public void testFilter() throws IOException, ParseException { - //搜索请求对象 - SearchRequest searchRequest = new SearchRequest("xc_course"); - //指定类型 - searchRequest.types("doc"); - //搜索源构建对象 - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - - //boolQuery搜索方式 - //先定义一个MultiMatchQuery - MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("spring css", "name", "description") - .minimumShouldMatch("50%") - .field("name", 10); - - //定义一个boolQuery - BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); - boolQueryBuilder.must(multiMatchQueryBuilder); - //定义过虑器 - boolQueryBuilder.filter(QueryBuilders.termQuery("studymodel","201001")); - boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(90).lte(100)); - - searchSourceBuilder.query(boolQueryBuilder); - //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 - searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); - //向搜索请求对象中设置搜索源 - searchRequest.source(searchSourceBuilder); - //执行搜索,向ES发起http请求 - SearchResponse searchResponse = client.search(searchRequest); - //搜索结果 - SearchHits hits = searchResponse.getHits(); - //匹配到的总记录数 - long totalHits = hits.getTotalHits(); - //得到匹配度高的文档 - SearchHit[] searchHits = hits.getHits(); - //日期格式化对象 - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - for(SearchHit hit:searchHits){ - //文档的主键 - String id = hit.getId(); - //源文档内容 - Map sourceAsMap = hit.getSourceAsMap(); - String name = (String) sourceAsMap.get("name"); - //由于前边设置了源文档字段过虑,这时description是取不到的 - String description = (String) sourceAsMap.get("description"); - //学习模式 - String studymodel = (String) sourceAsMap.get("studymodel"); - //价格 - Double price = (Double) sourceAsMap.get("price"); - //日期 - Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); - System.out.println(name); - System.out.println(studymodel); - System.out.println(description); - } - -} -``` - - - - - - - -# 排序 - -## API - -可以在字段上添加一个或多个排序,支持在keyword、date、flfloat等类型上添加,text类型的字段上不允许添加排 - -序。 - -发送 POST http://localhost:9200/xc_course/doc/_search - -过虑0--10元价格范围的文档,并且对结果进行排序,先按studymodel降序,再按价格升序 - -```json -{ - "_source": [ - "name", - "studymodel", - "description", - "price" - ], - "query": { - "bool": { - "filter": [ - { - "range": { - "price": { - "gte": 0, - "lte": 100 - } - } - } - ] - } - }, - "sort": [ - { - "studymodel": "desc" - }, - { - "price": "asc" - } - ] -} -``` - - - -## Java客户端 - -```java -//Sort -@Test -public void testSort() throws IOException, ParseException { - //搜索请求对象 - SearchRequest searchRequest = new SearchRequest("xc_course"); - //指定类型 - searchRequest.types("doc"); - //搜索源构建对象 - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - - //boolQuery搜索方式 - //定义一个boolQuery - BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); - //定义过虑器 - boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(0).lte(100)); - - searchSourceBuilder.query(boolQueryBuilder); - //添加排序 - searchSourceBuilder.sort("studymodel", SortOrder.DESC); - searchSourceBuilder.sort("price", SortOrder.ASC); - //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 - searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); - //向搜索请求对象中设置搜索源 - searchRequest.source(searchSourceBuilder); - //执行搜索,向ES发起http请求 - SearchResponse searchResponse = client.search(searchRequest); - //搜索结果 - SearchHits hits = searchResponse.getHits(); - //匹配到的总记录数 - long totalHits = hits.getTotalHits(); - //得到匹配度高的文档 - SearchHit[] searchHits = hits.getHits(); - //日期格式化对象 - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - for(SearchHit hit:searchHits){ - //文档的主键 - String id = hit.getId(); - //源文档内容 - Map sourceAsMap = hit.getSourceAsMap(); - String name = (String) sourceAsMap.get("name"); - //由于前边设置了源文档字段过虑,这时description是取不到的 - String description = (String) sourceAsMap.get("description"); - //学习模式 - String studymodel = (String) sourceAsMap.get("studymodel"); - //价格 - Double price = (Double) sourceAsMap.get("price"); - //日期 - Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); - System.out.println(name); - System.out.println(studymodel); - System.out.println(description); - } - -} -``` - - - - - -# 高亮显示 - -## API - -高亮显示可以将搜索结果一个或多个字突出显示,以便向用户展示匹配关键字的位置。 - -在搜索语句中添加highlight即可实现,如下: - -Post: http://127.0.0.1:9200/xc_course/doc/_search - -```json -{ - "_source": [ - "name", - "studymodel", - "description", - "price" - ], - "query": { - "bool": { - "must": [ - { - "multi_match": { - "query": "开发框架", - "minimum_should_match": "50%", - "fields": [ - "name^10", - "description" - ], - "type": "best_fields" - } - } - ], - "filter": [ - { - "range": { - "price": { - "gte": 0, - "lte": 100 - } - } - } - ] - } - }, - "sort": [ - { - "price": "asc" - } - ], - "highlight": { - "pre_tags": [ - "" - ], - "post_tags": [ - "" - ], - "fields": { - "name": {}, - "description": {} - } - } -} -``` - - - -## Java客户端 - -```java - //Highlight - @Test - public void testHighlight() throws IOException, ParseException { - //搜索请求对象 - SearchRequest searchRequest = new SearchRequest("xc_course"); - //指定类型 - searchRequest.types("doc"); - //搜索源构建对象 - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - - //boolQuery搜索方式 - //先定义一个MultiMatchQuery - MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("开发框架", "name", "description") - .minimumShouldMatch("50%") - .field("name", 10); - - //定义一个boolQuery - BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); - boolQueryBuilder.must(multiMatchQueryBuilder); - //定义过虑器 - boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(0).lte(100)); - - searchSourceBuilder.query(boolQueryBuilder); - //设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段 - searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{}); - - //设置高亮 - HighlightBuilder highlightBuilder = new HighlightBuilder(); - highlightBuilder.preTags(""); - highlightBuilder.postTags(""); - highlightBuilder.fields().add(new HighlightBuilder.Field("name")); - highlightBuilder.fields().add(new HighlightBuilder.Field("description")); - searchSourceBuilder.highlighter(highlightBuilder); - - //向搜索请求对象中设置搜索源 - searchRequest.source(searchSourceBuilder); - //执行搜索,向ES发起http请求 - SearchResponse searchResponse = client.search(searchRequest); - //搜索结果 - SearchHits hits = searchResponse.getHits(); - //匹配到的总记录数 - long totalHits = hits.getTotalHits(); - //得到匹配度高的文档 - SearchHit[] searchHits = hits.getHits(); - //日期格式化对象 -// SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS Z"); - for(SearchHit hit:searchHits){ - //文档的主键 - String id = hit.getId(); - //源文档内容 - Map sourceAsMap = hit.getSourceAsMap(); - //源文档的name字段内容 - String name = (String) sourceAsMap.get("name"); - //取出高亮字段 - Map highlightFields = hit.getHighlightFields(); - if(highlightFields!=null){ - //取出name高亮字段 - HighlightField nameHighlightField = highlightFields.get("name"); - if(nameHighlightField!=null){ - Text[] fragments = nameHighlightField.getFragments(); - StringBuffer stringBuffer = new StringBuffer(); - for(Text text:fragments){ - stringBuffer.append(text); - } - name = stringBuffer.toString(); - } - } - - //由于前边设置了源文档字段过虑,这时description是取不到的 - String description = (String) sourceAsMap.get("description"); - //学习模式 - String studymodel = (String) sourceAsMap.get("studymodel"); - //价格 - Double price = (Double) sourceAsMap.get("price"); - //日期 - Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp")); - System.out.println(name); - System.out.println(studymodel); - System.out.println(description); - } - - } -``` - diff --git a/docs/JVM/JVM系列-第10章-垃圾回收概述和相关算法.md b/docs/JVM/JVM系列-第10章-垃圾回收概述和相关算法.md index afc82bd..4d208a0 100644 --- a/docs/JVM/JVM系列-第10章-垃圾回收概述和相关算法.md +++ b/docs/JVM/JVM系列-第10章-垃圾回收概述和相关算法.md @@ -8,7 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第10章-垃圾回收概述和相关算法。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/jvm.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/jvm.png' abbrlink: d54daa0f date: 2020-11-16 18:14:02 --- @@ -20,7 +20,7 @@ date: 2020-11-16 18:14:02 - + 1. Java 和 C++语言的区别,就在于垃圾收集技术和内存动态分配上,C++语言没有垃圾收集技术,需要程序员手动的收集。 @@ -102,7 +102,7 @@ date: 2020-11-16 18:14:02 **十几年前磁盘碎片整理的日子** - + @@ -174,7 +174,7 @@ date: 2020-11-16 18:14:02 ### 应该关心哪些区域的回收? - + 1. 垃圾收集器可以对年轻代回收,也可以对老年代回收,甚至是全栈和方法区的回收, 1. 其中,**Java堆是垃圾收集器的工作重点** @@ -216,7 +216,7 @@ date: 2020-11-16 18:14:02 ### 循环引用 - + 当p的指针断开的时候,内部的引用形成一个循环,计数器都还算1,无法被回收,这就是循环引用,从而造成内存泄漏 @@ -255,7 +255,7 @@ public class RefCountGC { - + * 如果不小心直接把`obj1.reference`和`obj2.reference`置为null。则在Java堆中的两块内存依然保持着互相引用,无法被回收 @@ -353,7 +353,7 @@ Process finished with exit code 0 3. 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。 4. 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。 - + @@ -371,7 +371,7 @@ Process finished with exit code 0 - 基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、OutofMemoryError),系统类加载器。 7. 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。 - + @@ -453,7 +453,7 @@ Object 类中 finalize() 源码 **通过 JVisual VM 查看 Finalizer 线程** - + @@ -579,7 +579,7 @@ MAT与JProfiler的GC Roots溯源 **方式一:命令行使用 jmap** - + @@ -631,23 +631,23 @@ public class GCRootsTest { 1、先执行第一步,然后停下来,去生成此步骤dump文件 - + 2、 点击【堆 Dump】 - + 3、右键 --\> 另存为即可 - + 4、输入命令,继续执行程序 - + 5、我们接着捕获第二张堆内存快照 - + @@ -657,19 +657,19 @@ public class GCRootsTest { > 点击Open Heap Dump也行 - + 2、选择Java Basics --> GC Roots - + 3、第一次捕捉堆内存快照时,GC Roots 中包含我们定义的两个局部变量,类型分别为 ArrayList 和 Date,Total:21 - + 4、打开第二个dump文件,第二次捕获内存快照时,由于两个局部变量引用的对象被释放,所以这两个局部变量不再作为 GC Roots ,从 Total Entries = 19 也可以看出(少了两个 GC Roots) - + @@ -716,13 +716,13 @@ public class GCRootsTest { 1、 - + 2、 - + - + 可以发现颜色变绿了,可以动态的看变化 @@ -730,11 +730,11 @@ public class GCRootsTest { 3、右击对象,选择 Show Selection In Heap Walker,单独的查看某个对象 - + - + @@ -742,13 +742,13 @@ public class GCRootsTest { 点击Show Paths To GC Roots,在弹出界面中选择默认设置即可 - + - + - + ### JProfiler 分析 OOM @@ -798,11 +798,11 @@ count = 6 1、看这个超大对象 - + 2、揪出 main() 线程中出问题的代码 - + @@ -837,7 +837,7 @@ count = 6 * 注意:标记的是被引用的对象,也就是可达对象,并非标记的是即将被清除的垃圾对象 2. 清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收 - + @@ -876,7 +876,7 @@ count = 6 将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收 - + 新生代里面就用到了复制算法,Eden区和S0区存活对象整体复制到S1区 @@ -906,7 +906,7 @@ count = 6 2. 老年代大量的对象存活,那么复制的对象将会有很多,效率会很低 3. 在新生代,对常规应用的垃圾回收,一次通常可以回收70% - 99% 的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。 - + @@ -935,7 +935,7 @@ count = 6 2. 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。 - + @@ -1066,7 +1066,7 @@ A:无,没有最好的算法,只有最合适的算法 1. 一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。 3. 分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。 - + diff --git a/docs/JVM/JVM系列-第11章-垃圾回收相关概念.md b/docs/JVM/JVM系列-第11章-垃圾回收相关概念.md index 5ba6c4f..33daa7a 100644 --- a/docs/JVM/JVM系列-第11章-垃圾回收相关概念.md +++ b/docs/JVM/JVM系列-第11章-垃圾回收相关概念.md @@ -8,7 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第11章-垃圾回收相关概念。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/jvm.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/jvm.png' abbrlink: 4d401a8b date: 2020-11-17 12:33:24 --- @@ -117,7 +117,7 @@ JVM参数: 2、我也查过了大对象阈值的默认值 - + 我不太懂这个默认值为啥是0,我猜测可能是代表什么比例,目前也没有搜到相关的东西。这个不太重要,暂时就没有太深究,希望读者有知道的可以告知我一声。 @@ -185,11 +185,11 @@ Heap 1、来看看字节码:实例方法局部变量表第一个变量肯定是 this - + 2、你有没有看到,局部变量表的大小是 2。但是局部变量表里只有一个索引为0的啊?那索引为1的是哪个局部变量呢?实际上索引为1的位置是buffer在占用着,执行 System.gc() 时,栈中还有 buffer 变量指向堆中的字节数组,所以没有进行GC - + 3、那么这种代码块的情况,什么时候会被GC呢?我们来看第四个方法 @@ -217,11 +217,11 @@ A:局部变量表长度为 2 ,这说明了出了代码块时,buffer 就出 > 这点看不懂的可以看我前面的文章:虚拟机栈 --> Slot的重复利用 - + - + @@ -302,7 +302,7 @@ Heap 右边的图:后期有一些对象不用了,按道理应该断开引用,但是存在一些链没有断开(图示中的Forgotten Reference Memory Leak),从而导致没有办法被回收。 - + @@ -446,7 +446,7 @@ Process finished with exit code -1 2. 并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换。由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行 - + @@ -461,7 +461,7 @@ Process finished with exit code -1 3. 适合科学计算,后台处理等弱交互场景 - + > **并发与并行的对比** @@ -482,7 +482,7 @@ Process finished with exit code -1 * 相较于并行的概念,单线程执行。 * 如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收(单线程) - + @@ -492,7 +492,7 @@ Process finished with exit code -1 - 比如用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上; 2. 典型垃圾回收器:CMS、G1 - + @@ -551,7 +551,7 @@ Process finished with exit code -1 1、一般的垃圾回收算法至少会划分出两个年代,年轻代和老年代。但是单纯的分代理论在垃圾回收的时候存在一个巨大的缺陷:为了找到年轻代中的存活对象,却不得不遍历整个老年代,反过来也是一样的。 - + 2、如果我们从年轻代开始遍历,那么可以断定N, S, P, Q都是存活对象。但是,V却不会被认为是存活对象,其占据的内存会被回收了。这就是一个惊天的大漏洞!因为U本身是老年代对象,而且有外部引用指向它,也就是说U是存活对象,而U指向了V,也就是说V也应该是存活对象才是!而这都是因为我们只遍历年轻代对象! @@ -599,7 +599,7 @@ Process finished with exit code -1 4. 这4种引用强度依次逐渐减弱。除强引用外,其他3种引用均可以在java.lang.ref包中找到它们的身影。如下图,显示了这3种引用类型对应的类,开发人员可以在应用程序中直接使用它们。 - + @@ -661,7 +661,7 @@ Hello,尚硅谷 `StringBuffer str = new StringBuffer("hello,尚硅谷");` - + diff --git a/docs/JVM/JVM系列-第12章-垃圾回收器.md b/docs/JVM/JVM系列-第12章-垃圾回收器.md index 8f94f44..06fae9f 100644 --- a/docs/JVM/JVM系列-第12章-垃圾回收器.md +++ b/docs/JVM/JVM系列-第12章-垃圾回收器.md @@ -8,7 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第12章-垃圾回收器。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/jvm.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/jvm.png' abbrlink: 7706d61d date: 2020-11-19 18:33:24 --- @@ -44,7 +44,7 @@ GC 分类与性能指标 **按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器。** - + 1. 串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。 1. 在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以,串行回收默认被应用在客户端的Client模式下的JVM中 @@ -60,7 +60,7 @@ GC 分类与性能指标 1. 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。 2. 独占式垃圾回收器(Stop the World)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。 - + @@ -105,7 +105,7 @@ GC 分类与性能指标 2. 这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的 3. 吞吐量优先,意味着在单位时间内,STW的时间最短:0.2+0.2=0.4 - + @@ -115,7 +115,7 @@ GC 分类与性能指标 - 例如,GC期间100毫秒的暂停时间意味着在这100毫秒期间内没有应用程序线程是活动的 2. 暂停时间优先,意味着尽可能让单次STW的时间最短:0.1+0.1 + 0.1+ 0.1+ 0.1=0.5,但是总的GC时间可能会长 - + @@ -169,17 +169,17 @@ GC 分类与性能指标 2. 并行回收器:ParNew、Parallel Scavenge、Parallel old 3. 并发回收器:CMS、G1 - + **官方文档** - + **7款经典回收器与垃圾分代之间的关系** - + 1. 新生代收集器:Serial、ParNew、Parallel Scavenge; @@ -192,7 +192,7 @@ GC 分类与性能指标 ### 垃圾收集器的组合关系 - + @@ -251,11 +251,11 @@ jinfo -flag UseParallelOldGC 进程id JDK 8 中默认使用 ParallelGC 和 ParallelOldGC 的组合 - + #### JDK9 - + @@ -281,7 +281,7 @@ Serial 回收器:串行回收 这个收集器是一个单线程的收集器,“单线程”的意义:它只会使用一个CPU(串行)或一条收集线程去完成垃圾收集工作。更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World) - + @@ -313,7 +313,7 @@ ParNew 回收器:并行回收 2. ParNew 收集器除了采用**并行回收**的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代中同样也是采用复制算法、"Stop-the-World"机制。 3. ParNew 是很多JVM运行在Server模式下新生代的默认垃圾收集器。 - + 1. 对于新生代,回收次数频繁,使用并行方式高效。 2. 对于老年代,回收次数少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源) @@ -361,7 +361,7 @@ Parallel 回收器:吞吐量优先 5. Parallel Old收集器采用了标记-压缩算法,但同样也是基于并行回收和"Stop-the-World"机制。 - + 1. 在程序吞吐量优先的应用场景中,Parallel收集器和Parallel Old收集器的组合,在server模式下的内存回收性能很不错。 2. **在Java8中,默认是此垃圾收集器。** @@ -418,7 +418,7 @@ CMS 回收器:低延迟 ### CMS 工作原理(过程) - + CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。(涉及STW的阶段主要是:初始标记 和 重新标记) @@ -438,7 +438,7 @@ CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶 3. 另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,**而是当堆内存使用率达到某一阈值时,便开始进行回收**,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次**“Concurrent Mode Failure”** 失败,这时虚拟机将启动后备预案:临时启用Serial old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。 4. CMS收集器的垃圾收集算法采用的是**标记清除算法**,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,**不可避免地将会产生一些内存碎片**。那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配。 - + @@ -558,11 +558,11 @@ G1 回收器:区域化分代式 G1的分代,已经不是下面这样的了 - + G1的分区是这样的一个区域 - + **空间整合** @@ -655,7 +655,7 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC和Full GC,在不 > > 如图所示,可以将区域分配到Eden,幸存者和旧时代区域。 此外,还有第四种类型的物体被称为巨大区域。 这些区域旨在容纳标准区域大小的50%或更大的对象。 它们存储为一组连续区域。 最后,最后一种区域类型是堆的未使用区域。 - + @@ -667,7 +667,7 @@ G1中提供了三种垃圾回收模式:YoungGC、Mixed GC和Full GC,在不 **Regio的细节** - + 1. 每个Region都是通过指针碰撞来分配空间 2. G1为每一个Region设 计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。 @@ -686,7 +686,7 @@ G1 GC的垃圾回收过程主要包括如下三个环节: * 混合回收(Mixed GC) * (如果需要,单线程、独占式、高强度的Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收。) - + 顺时针,Young GC --> Young GC+Concurrent Marking --> Mixed GC顺序,进行垃圾回收 @@ -731,7 +731,7 @@ G1 GC的垃圾回收过程主要包括如下三个环节: - + 1. 在回收 Region 时,为了不进行全堆的扫描,引入了 Remembered Set 2. Remembered Set 记录了当前 Region 中的对象被哪个对象引用了 @@ -748,7 +748,7 @@ G1 GC的垃圾回收过程主要包括如下三个环节: 2. 年轻代回收只回收Eden区和Survivor区 3. YGC时,首先G1停止应用程序的执行(Stop-The-World),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。 - + 图的大致意思就是: @@ -804,7 +804,7 @@ G1 GC的垃圾回收过程主要包括如下三个环节: 当越来越多的对象晋升到老年代Old Region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region。这里需要注意:是一部分老年代,而不是全部老年代。可以选择哪些Old Region进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是Mixed GC并不是Full GC。 - + @@ -854,11 +854,11 @@ G1 GC的垃圾回收过程主要包括如下三个环节: 截止JDK1.8,一共有7款不同的垃圾收集器。每一款的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器。 - + - + @@ -922,11 +922,11 @@ GC 日志分析 2、这个只会显示总的GC堆的变化,如下: - + 3、参数解析 - + @@ -938,11 +938,11 @@ GC 日志分析 2、输入信息如下 - + 3、参数解析 - + @@ -954,7 +954,7 @@ GC 日志分析 2、输出信息如下 - + 3、说明:日志带上了日期和时间 @@ -989,13 +989,13 @@ GC 日志分析 #### Young GC - + #### Full GC - + @@ -1029,11 +1029,11 @@ public class GCLogTest1 { 1、首先我们会将3个2M的数组存放到Eden区,然后后面4M的数组来了后,将无法存储,因为Eden区只剩下2M的剩余空间了,那么将会进行一次Young GC操作,将原来Eden区的内容,存放到Survivor区,但是Survivor区也存放不下,那么就会直接晋级存入Old 区 - + 2、然后我们将4M对象存入到Eden区中 - + 老年代图画的有问题,free应该是4M @@ -1056,7 +1056,7 @@ Process finished with exit code 0 ``` - + 与 JDK7 不同的是,JDK8 直接判定 4M 的数组为大对象,直接怼到老年区去了 @@ -1082,15 +1082,15 @@ GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat等 在线分析网址:gceasy.io - + - + - + @@ -1126,7 +1126,7 @@ GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat等 1. 停顿时间比其他几款收集器确实有了质的飞跃,但也未实现最大停顿时间控制在十毫秒以内的目标。 2. 而吞吐量方面出现了明显的下降,总运行时间是所有测试收集器里最长的。 - + @@ -1156,7 +1156,7 @@ GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat等 **吞吐量** - + max-JOPS:以低延迟为首要前提下的数据 @@ -1166,13 +1166,13 @@ critical-JOPS:不考虑低延迟下的数据 **低延迟** - + 在ZGC的强项停顿时间测试上,它毫不留情的将Parallel、G1拉开了两个数量级的差距。无论平均停顿、95%停顿、998停顿、99. 98停顿,还是最大停顿时间,ZGC都能毫不费劲控制在10毫秒以内。 虽然ZGC还在试验状态,没有完成所有特性,但此时性能已经相当亮眼,用“令人震惊、革命性”来形容,不为过。未来将在服务端、大内存、低延迟应用的首选垃圾收集器。 - + @@ -1192,4 +1192,4 @@ critical-JOPS:不考虑低延迟下的数据 AliGC是阿里巴巴JVM团队基于G1算法,面向大堆(LargeHeap)应用场景。指定场景下的对比: - \ No newline at end of file + \ No newline at end of file diff --git a/docs/JVM/JVM系列-第1章-JVM与Java体系结构.md b/docs/JVM/JVM系列-第1章-JVM与Java体系结构.md index 5fef9cd..e7129f3 100644 --- a/docs/JVM/JVM系列-第1章-JVM与Java体系结构.md +++ b/docs/JVM/JVM系列-第1章-JVM与Java体系结构.md @@ -8,7 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第1章-JVM与Java体系结构。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/jvm.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/jvm.png' abbrlink: 8c954c6 date: 2020-11-02 11:51:56 --- @@ -38,7 +38,7 @@ date: 2020-11-02 11:51:56 3. 新项目上线,对各种JVM参数设置一脸茫然,直接默认吧然后就JJ了。 4. 每次面试之前都要重新背一遍JVM的一些原理概念性的东西,然而面试官却经常问你在实际项目中如何调优VM参数,如何解决GC、OOM等问题,一脸懵逼。 - + 大部分Java开发人员,除了会在项目中使用到与Java平台相关的各种高精尖技术,对于Java技术的核心Java虚拟机了解甚少。 @@ -50,7 +50,7 @@ date: 2020-11-02 11:51:56 1. 一些有一定工作经验的开发人员,打心眼儿里觉得SSM、微服务等上层技术才是重点,基础技术并不重要,这其实是一种本末倒置的“病态”。 2. 如果我们把核心类库的API比做数学公式的话,那么Java虚拟机的知识就好比公式的推导过程。 - + - 计算机系统体系对我们来说越来越远,在不了解底层实现方式的前提下,通过高级语言很容易编写程序代码。但事实上计算机并不认识高级语言。 @@ -87,7 +87,7 @@ Java VS C++ 1. 垃圾收集机制为我们打理了很多繁琐的工作,大大提高了开发的效率,但是,垃圾收集也不是万能的,懂得JVM内部的内存结构、工作机制,是设计高扩展性应用和诊断运行时问题的基础,也是Java工程师进阶的必备能力。 2. C++语言需要程序员自己来分配内存和回收内存,对于高手来说可能更加舒服,但是对于普通开发者,如果技术实力不够,很容易造成内存泄漏。而Java全部交给JVM进行内存分配和回收,这也是一种趋势,减少程序员的工作量。 - + ## 什么人需要学JVM? @@ -103,26 +103,26 @@ Java VS C++ **英文文档规范**:https://docs.oracle.com/javase/specs/index.html - + **中文书籍:** - + > 周志明老师的这本书**非常推荐看**,不过只推荐看第三版,第三版较第二版更新了很多,个人觉得没必要再看第二版。 - + - + TIOBE排行榜 ----------- **TIOBE 排行榜**:https://www.tiobe.com/tiobe-index/ - + - 世界上没有最好的编程语言,只有最适用于具体应用场景的编程语言。 - 目前网上一直流传Java被python,go撼动Java第一的地位。学习者不需要太担心,Java强大的生态圈,也不是说是朝夕之间可以被撼动的。 @@ -148,14 +148,14 @@ Java-跨平台的语言 - + JVM-跨语言的平台 ------ - + @@ -187,7 +187,7 @@ JVM-跨语言的平台 2. 自己动手写一个Java虚拟机,难吗? 3. 天下事有难易乎?为之,则难者亦易矣;不为,则易者亦难矣 - + Java发展重大事件 ------------ @@ -215,7 +215,7 @@ Java发展重大事件 ## Open JDK和Oracle JDK - + - 在JDK11之前,Oracle JDK中还会存在一些Open JDK中没有的,闭源的功能。但在JDK11中,我们可以认为Open JDK和Oracle JDK代码实质上已经达到完全一致的程度了。 - 主要的区别就是两者更新周期不一样 @@ -258,11 +258,11 @@ JVM的位置 JVM是运行在操作系统之上的,它与硬件没有直接的交互 - + - + @@ -275,7 +275,7 @@ JVM的整体结构 - + @@ -284,7 +284,7 @@ Java代码执行流程 凡是能生成被Java虚拟机所能解释、运行的字节码文件,那么理论上我们就可以自己设计一套语言了 - + diff --git a/docs/JVM/JVM系列-第2章-类加载子系统.md b/docs/JVM/JVM系列-第2章-类加载子系统.md index 2cc7087..7df2aa3 100644 --- a/docs/JVM/JVM系列-第2章-类加载子系统.md +++ b/docs/JVM/JVM系列-第2章-类加载子系统.md @@ -8,7 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第2章-类加载子系统。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/jvm.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/jvm.png' abbrlink: 2e0079af date: 2020-11-02 21:31:58 --- @@ -25,19 +25,19 @@ date: 2020-11-02 21:31:58 ### 简图 - + ### 详细图 英文版 - + 中文版 - + 注意:方法区只有HotSpot虚拟机有,J9,JRockit都没有 @@ -59,7 +59,7 @@ date: 2020-11-02 21:31:58 3. **加载的类信息存放于一块称为方法区的内存空间**。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射) - + @@ -71,7 +71,7 @@ date: 2020-11-02 21:31:58 - + 类加载过程 ------- @@ -96,11 +96,11 @@ public class HelloLoader { * 加载成功,则进行链接、初始化等操作。完成后调用 HelloLoader 类中的静态方法 main * 加载失败则抛出异常 - + 完整的流程图如下所示: - + @@ -142,7 +142,7 @@ public class HelloLoader { 使用 BinaryViewer软件查看字节码文件,其开头均为 CAFE BABE ,如果出现不合法的字节码文件,那么将会验证不通过。 - + #### 准备(Prepare) @@ -185,7 +185,7 @@ public class HelloApp { * 反编译 class 文件后可以查看符号引用,下面带# 的就是符号引用 - + ### 初始化阶段 @@ -225,7 +225,7 @@ public class HelloApp { 查看下面这个代码的字节码,可以发现有一个`()`方法。 - + ```java public class ClassInitTest { @@ -277,15 +277,15 @@ public class ClassInitTest { **举例2:无 static 变量** - + 加上之后就有了 - + #### 4说明 - + 在构造器中: @@ -296,7 +296,7 @@ public class ClassInitTest { 若该类具有父类,JVM会保证子类的`()`执行前,父类的`()`已经执行完毕 - + 如上代码,加载流程如下: @@ -374,17 +374,17 @@ class DeadThread{ - + **ExtClassLoader** - + **AppClassLoader** - + @@ -579,15 +579,15 @@ public class CustomClassLoader extends ClassLoader { ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器) - + sun.misc.Launcher 它是一个java虚拟机的入口应用 - + #### 获取ClassLoader途径 - + @@ -640,7 +640,7 @@ Java虚拟机对class文件采用的是**按需加载**的方式,也就是说 3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。 4. 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常 - + ### 双亲委派机制代码演示 @@ -707,7 +707,7 @@ public class String { } ``` - + 由于双亲委派机制一直找父类,所以最后找到了Bootstrap ClassLoader,Bootstrap ClassLoader找到的是 JDK 自带的 String 类,在那个String类中并没有 main() 方法,所以就报了上面的错误。 @@ -765,7 +765,7 @@ Process finished with exit code 1 - + diff --git a/docs/JVM/JVM系列-第3章-运行时数据区.md b/docs/JVM/JVM系列-第3章-运行时数据区.md index f23a9ba..420e87c 100644 --- a/docs/JVM/JVM系列-第3章-运行时数据区.md +++ b/docs/JVM/JVM系列-第3章-运行时数据区.md @@ -8,7 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第3章-运行时数据区。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/jvm.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/jvm.png' abbrlink: a7ad3cab date: 2020-11-09 15:38:42 --- @@ -27,15 +27,15 @@ date: 2020-11-09 15:38:42 本节主要讲的是运行时数据区,也就是下图这部分,它是在类加载完成后的阶段 - + 当我们通过前面的:类的加载 --> 验证 --> 准备 --> 解析 --\> 初始化,这几个阶段完成后,就会用到执行引擎对我们的类进行使用,同时执行引擎将会使用到我们运行时数据区 - + 类比一下也就是大厨做饭,我们把大厨后面的东西(切好的菜,刀,调料),比作是运行时数据区。而厨师可以类比于执行引擎,将通过准备的东西进行制作成精美的菜品。 - + @@ -50,7 +50,7 @@ date: 2020-11-09 15:38:42 > 下图来自阿里巴巴手册JDK8 - + @@ -62,7 +62,7 @@ date: 2020-11-09 15:38:42 - 线程独有:独立包括程序计数器、栈、本地方法栈 - 线程间共享:堆、堆外内存(永久代或元空间、代码缓存) - + @@ -70,7 +70,7 @@ date: 2020-11-09 15:38:42 **每个JVM只有一个Runtime实例**。即为运行时环境,相当于内存结构的中间的那个框框:运行时环境。 - + @@ -114,7 +114,7 @@ PC寄存器介绍 > 官方文档网址:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html - + 1. JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,**寄存器存储指令相关的现场信息**。CPU只有把数据装载到寄存器才能够运行。 2. 这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。**JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟**。 @@ -132,7 +132,7 @@ PC寄存器介绍 PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令,并执行该指令。 - + @@ -268,7 +268,7 @@ SourceFile: "PCRegisterTest.java" * 左边的数字代表**指令地址(指令偏移)**,即 PC 寄存器中可能存储的值,然后执行引擎读取 PC 寄存器中的值,并执行该指令 - + @@ -282,7 +282,7 @@ SourceFile: "PCRegisterTest.java" 2. JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令 - + @@ -306,7 +306,7 @@ CPU 时间片 3. 但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,**每个程序轮流执行**。 - + @@ -314,7 +314,7 @@ CPU 时间片 ## 本地方法 - + @@ -393,7 +393,7 @@ Java使用起来非常方便,然而有些层次的任务用Java实现起来不 4. 本地方法一般是使用C语言或C++语言实现的。 5. 它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。 - + diff --git a/docs/JVM/JVM系列-第4章-虚拟机栈.md b/docs/JVM/JVM系列-第4章-虚拟机栈.md index 2a9e4cc..ec3d516 100644 --- a/docs/JVM/JVM系列-第4章-虚拟机栈.md +++ b/docs/JVM/JVM系列-第4章-虚拟机栈.md @@ -8,7 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第4章-虚拟机栈。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/jvm.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/jvm.png' abbrlink: 5b1b6560 date: 2020-11-10 10:38:42 --- @@ -31,7 +31,7 @@ date: 2020-11-10 10:38:42 1. 首先栈是运行时的单位,而堆是存储的单位。 2. 即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放,放哪里 - + @@ -66,7 +66,7 @@ date: 2020-11-10 10:38:42 - + - 虚拟机栈的生命周期 - 生命周期和线程一致,也就是线程结束了,该虚拟机栈也销毁了 @@ -90,7 +90,7 @@ date: 2020-11-10 10:38:42 - 对于栈来说不存在垃圾回收问题 - 栈不需要GC,但是可能存在OOM - + ### 虚拟机栈的异常 @@ -164,7 +164,7 @@ Exception in thread "main" java.lang.StackOverflowError **设置栈参数之后** - + 部分输出结果 @@ -202,7 +202,7 @@ Exception in thread "main" java.lang.StackOverflowError 4. 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。 - + 1. **不同线程中所包含的栈帧是不允许存在相互引用的**,即不可能在一个栈帧之中引用另外一个线程的栈帧。 2. 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。 @@ -230,11 +230,11 @@ Exception in thread "main" java.lang.StackOverflowError - 一些附加信息 - + 并行每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要由局部变量表 和 操作数栈决定的 - + 局部变量表 ------- @@ -312,7 +312,7 @@ public class LocalVariablesTest { } ``` - + 看完字节码后,可得结论:所以局部变量表所需的容量大小是在编译期确定下来的。 @@ -324,29 +324,29 @@ public class LocalVariablesTest { 1、0-15 也就是有16行字节码 - + 2、方法异常信息表 - + 3、Misc - + 4、行号表 Java代码的行号和字节码指令行号的对应关系 - + 5、注意:生效行数和剩余有效行数都是针对于字节码文件的行数 - + 1、图中圈的东西表示该局部变量的作用域 @@ -370,7 +370,7 @@ Java代码的行号和字节码指令行号的对应关系 6. 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或double类型变量) 7. 如果当前帧是由构造方法或者实例方法创建的,那么**该对象引用this将会存放在index为0的slot处**,其余的参数按照参数表顺序继续排列。(this也相当于一个变量) - + ### Slot代码示例 @@ -388,7 +388,7 @@ Java代码的行号和字节码指令行号的对应关系 局部变量表:this 存放在 index = 0 的位置 - + @@ -408,7 +408,7 @@ Java代码的行号和字节码指令行号的对应关系 weight 为 double 类型,index 直接从 3 蹦到了 5 - + @@ -451,7 +451,7 @@ this 不存在与 static 方法的局部变量表中,所以无法调用 局部变量 c 重用了局部变量 b 的 slot 位置 - + @@ -499,13 +499,13 @@ this 不存在与 static 方法的局部变量表中,所以无法调用 - 比如:执行复制、交换、求和等操作 - + - + @@ -536,7 +536,7 @@ this 不存在与 static 方法的局部变量表中,所以无法调用 - + 局部变量表就相当于食材 @@ -571,23 +571,23 @@ this 不存在与 static 方法的局部变量表中,所以无法调用 10 return ``` - + ### 一步一步看流程 1、首先执行第一条语句,PC寄存器指向的是0,也就是指令地址为0,然后使用bipush让操作数15入操作数栈。 - + 2、执行完后,PC寄存器往下移,指向下一行代码,下一行代码就是将操作数栈的元素存储到局部变量表1的位置(istore_1),我们可以看到局部变量表的已经增加了一个元素。并且操作数栈为空了 * 解释为什么局部变量表索引从 1 开始,因为该方法为实例方法,局部变量表索引为 0 的位置存放的是 this - + 3、然后PC下移,指向的是下一行。让操作数8也入栈,同时执行store操作,存入局部变量表中 - + @@ -595,11 +595,11 @@ this 不存在与 static 方法的局部变量表中,所以无法调用 iload_1:取出局部变量表中索引为1的数据入操作数栈 - + 5、然后将操作数栈中的两个元素执行相加操作,并存储在局部变量表3的位置 - + @@ -607,7 +607,7 @@ iload_1:取出局部变量表中索引为1的数据入操作数栈 **关于类型转换的说明** - + @@ -616,7 +616,7 @@ iload_1:取出局部变量表中索引为1的数据入操作数栈 - + - m改成800之后,byte存储不了,就成了short型,sipush 800 @@ -645,11 +645,11 @@ iload_1:取出局部变量表中索引为1的数据入操作数栈 getSum() 方法字节码指令:最后带着个 ireturn - + testGetSum() 方法字节码指令:一上来就加载 getSum() 方法的返回值() - + @@ -841,7 +841,7 @@ SourceFile: "DynamicLinkingTest.java" - + @@ -1136,7 +1136,7 @@ interface MethodInterface { Son 类中 show() 方法的字节码指令如下 - + @@ -1176,7 +1176,7 @@ public class Lambda { } ``` - + @@ -1233,7 +1233,7 @@ Java:String info = "mogu blog"; (Java是静态类型语言的,会先 如图所示:如果类中重写了方法,那么调用的时候,就会直接在该类的虚方法表中查找 - + 1、比如说son在调用toString的时候,Son没有重写过,Son的父类Father也没有重写过,那就直接调用Object类的toString。那么就直接在虚方法表里指明toString直接指向Object类。 @@ -1243,24 +1243,24 @@ Java:String info = "mogu blog"; (Java是静态类型语言的,会先 **例子2** - + - + - + - + 方法返回地址 -------- - + > 在一些帖子里,方法返回地址、动态链接、一些附加信息 也叫做帧数据区 @@ -1309,7 +1309,7 @@ Java:String info = "mogu blog"; (Java是静态类型语言的,会先 2. 方法执行过程中,抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码 - + @@ -1321,7 +1321,7 @@ Java:String info = "mogu blog"; (Java是静态类型语言的,会先 * target :出现异常跳转至地址为 11 的指令执行 * type :捕获异常的类型 - + diff --git a/docs/JVM/JVM系列-第5章-堆.md b/docs/JVM/JVM系列-第5章-堆.md index 59aa5bc..c0438a1 100644 --- a/docs/JVM/JVM系列-第5章-堆.md +++ b/docs/JVM/JVM系列-第5章-堆.md @@ -8,7 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第5章-堆。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/jvm.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/jvm.png' abbrlink: 50ac3a1c date: 2020-11-11 20:38:42 --- @@ -27,7 +27,7 @@ date: 2020-11-11 20:38:42 1. 堆针对一个JVM进程来说是唯一的。也就是**一个进程只有一个JVM实例**,一个JVM实例中就有一个运行时数据区,一个运行时数据区只有一个堆和一个方法区。 2. 但是**进程包含多个线程,他们是共享同一堆空间的**。 - + @@ -68,7 +68,7 @@ public class SimpleHeap { } ``` - + @@ -88,13 +88,13 @@ public class SimpleHeap { 约定:新生区 <–> 新生代 <–> 年轻代 、 养老区 <–> 老年区 <–> 老年代、 永久区 <–\> 永久代 - + 2. 堆空间内部结构,JDK1.8之前从永久代 替换成 元空间 - + @@ -122,17 +122,17 @@ public class HeapDemo { 1、双击jdk目录下的这个文件 - + 2、工具 -> 插件 -> 安装Visual GC插件 - + 3、运行上面的代码 - + @@ -215,7 +215,7 @@ public class HeapSpaceInitial { 设置下参数再看 - + ```java public class HeapSpaceInitial { @@ -250,7 +250,7 @@ public class HeapSpaceInitial { **方式一: jps / jstat -gc 进程id** - + > jps:查看java进程 > @@ -285,7 +285,7 @@ OU: 老年代使用的量 **方式二:-XX:+PrintGCDetails** - + @@ -333,11 +333,11 @@ Process finished with exit code 1 2、堆内存变化图 - + 3、原因:大对象导致堆内存溢出 - + @@ -357,9 +357,9 @@ Process finished with exit code 1 3、其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区、to区) - + - + - 配置新生代与老年代在堆结构的占比 @@ -383,7 +383,7 @@ Process finished with exit code 1 - + @@ -439,7 +439,7 @@ public class EdenSurvivorTest { 1、我们创建的对象,一般都是存放在Eden区的,**当我们Eden区满了后,就会触发GC操作**,一般被称为 YGC / Minor GC操作 - + 2、当我们进行一次垃圾收集后,红色的对象将会被回收,而绿色的独享还被占用着,存放在S0(Survivor From)区。同时我们给每个对象设置了一个年龄计数器,经过一次回收后还存在的对象,将其年龄加 1。 @@ -453,11 +453,11 @@ public class EdenSurvivorTest { > > 3、也就是说s0区和s1区在互相转换。 - + 4、我们继续不断的进行对象生成和垃圾回收,当Survivor中的对象的年龄达到15的时候,将会触发一次 Promotion 晋升的操作,也就是将年轻代中的对象晋升到老年代中 - + 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。 @@ -475,7 +475,7 @@ public class EdenSurvivorTest { * 那万一老年代都放不下,则先触发FullGC ,再看看能不能放下,放得下最好,但如果还是放不下,那只能报 OOM 3. 如果 Eden 区满了,将对象往幸存区拷贝时,发现幸存区放不下啦,那只能便宜了某些新对象,让他们直接晋升至老年区 - + ### 常用调优工具 @@ -528,7 +528,7 @@ GC分类 3. Minor GC会引发STW(Stop The World),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行 - + @@ -649,7 +649,7 @@ Heap * 新生代:有Eden、两块大小相同的survivor(又称为from/to或s0/s1)构成,to总为空。 * 老年代:存放新生代中经历多次GC仍然存活的对象。 - + @@ -661,7 +661,7 @@ Heap - + @@ -711,7 +711,7 @@ TLAB(Thread Local Allocation Buffer) 2. 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为**快速分配策略**。 3. 据我所知所有OpenJDK衍生出来的JVM都提供了TLAB的设计。 - + 1、每个线程都有一个TLAB空间 @@ -741,7 +741,7 @@ TLAB(Thread Local Allocation Buffer) **TLAB 分配过程** - + diff --git a/docs/JVM/JVM系列-第6章-方法区.md b/docs/JVM/JVM系列-第6章-方法区.md index 2d6c3cd..e5d83ce 100644 --- a/docs/JVM/JVM系列-第6章-方法区.md +++ b/docs/JVM/JVM系列-第6章-方法区.md @@ -8,7 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第6章-方法区。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/jvm.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/jvm.png' abbrlink: 136cd965 date: 2020-11-13 19:38:42 --- @@ -24,7 +24,7 @@ date: 2020-11-13 19:38:42 ThreadLocal:如何保证多个线程在并发环境下的安全性?典型场景就是数据库连接管理,以及会话管理。 - + **栈、堆、方法区的交互关系** @@ -35,7 +35,7 @@ ThreadLocal:如何保证多个线程在并发环境下的安全性?典型场 3. 真正的 person 对象存放在 Java 堆中 4. 在 person 对象中,有个指针指向方法区中的 person 类型数据,表明这个 person 对象是用方法区中的 Person 类 new 出来的 - + 方法区的理解 -------- @@ -47,7 +47,7 @@ ThreadLocal:如何保证多个线程在并发环境下的安全性?典型场 1. 《Java虚拟机规范》中明确说明:尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。 3. 所以,**方法区可以看作是一块独立于Java堆的内存空间**。 - + @@ -87,7 +87,7 @@ public class MethodAreaDemo { 简单的程序,加载了1600多个类 - + @@ -103,7 +103,7 @@ public class MethodAreaDemo { 5. 永久代、元空间二者并不只是名字变了,内部结构也调整了 6. 根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常 - + @@ -120,7 +120,7 @@ public class MethodAreaDemo { 2. -XX:MaxPermsize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M 3. 当JVM加载的类信息容量超过了这个值,会报异常OutofMemoryError:PermGen space。 - + ### JDK8及以后(元空间) @@ -228,11 +228,11 @@ Exception in thread "main" java.lang.OutOfMemoryError: Metaspace #### 概念 - + 《深入理解Java虚拟机》书中对方法区(Method Area)存储内容描述如下:它用于存储已被虚拟机加载的**类型信息、常量、静态变量、即时编译器编译后的代码缓存**等。 - + @@ -714,7 +714,7 @@ public static int count; - + 1. 方法区,内部包含了运行时常量池 2. 字节码文件,内部包含了常量池。(之前的字节码文件中已经看到了很多Constant pool的东西,这个就是常量池) @@ -728,7 +728,7 @@ public static int count; 1. 一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述符信息外。还包含一项信息就是**常量池表**(**Constant Pool Table**),包括各种字面量和对类型、域和方法的符号引用。 2. 字面量: 10 , “我是某某”这种数字和字符串都是字面量 - + **为什么需要常量池?** @@ -749,7 +749,7 @@ public static int count; 2. 比如说我们这个文件中有6个地方用到了"hello"这个字符串,如果不用常量池,就需要在6个地方全写一遍,造成臃肿。我们可以将"hello"等所需用到的结构信息记录在常量池中,并通过**引用的方式**,来加载、调用所需的结构 4. 这里的代码量其实很少了,如果代码多的话,引用的结构将会更多,这里就需要用到常量池了。 - + **常量池中有啥?** @@ -924,69 +924,69 @@ SourceFile: "MethodAreaDemo.java" 1、初始状态 - + 2、首先将操作数500压入操作数栈中 - + 3、然后操作数 500 从操作数栈中取出,存储到局部变量表中索引为 1 的位置 - + 4、 - + 5、 - + 6、 - + 7、 - + 8、 - + 9、 - + 10、 - + 11、图片写错了是#25和#26(获得System类) - + 12、 - + 13、 - + 15、执行加法运算后,将计算结果放在操作数栈顶 - + 16、就是真正的打印 - + 17、 - + @@ -1021,7 +1021,7 @@ SourceFile: "MethodAreaDemo.java" 方法区由永久代实现,使用 JVM 虚拟机内存(虚拟的内存) - + @@ -1029,7 +1029,7 @@ SourceFile: "MethodAreaDemo.java" 方法区由永久代实现,使用 JVM 虚拟机内存 - + @@ -1037,7 +1037,7 @@ SourceFile: "MethodAreaDemo.java" 方法区由元空间实现,使用物理机本地内存 - + @@ -1095,15 +1095,15 @@ public class StaticFieldTest { JDK6环境下 -image-20201113224231761 +image-20201113224231761 JDK7环境下 - + JDK8环境 - + @@ -1150,7 +1150,7 @@ public class StaticObjTest { 4、测试发现:三个对象的数据在内存中的地址都落在Eden区范围内,所以结论:**只要是对象实例必然会在Java堆中分配**。 - + > 1、0x00007f32c7800000(Eden区的起始地址) ---- 0x00007f32c7b50000(Eden区的终止地址) > @@ -1162,7 +1162,7 @@ public class StaticObjTest { 5、接着,找到了一个引用该staticObj对象的地方,是在一个java.lang.Class的实例里,并且给出了这个实例的地址,通过Inspector查看该对象实例,可以清楚看到这确实是一个java.lang.Class类型的对象实例,里面有一个名为staticobj的实例字段: - + 从《Java虚拟机规范》所定义的概念模型来看,所有Class相关的信息都应该存放在方法区之中,但方法区该如何实现,《Java虚拟机规范》并未做出规定,这就成了一件允许不同虚拟机自己灵活把握的事情。JDK7及其以后版本的HotSpot虚拟机选择把静态变量与类型在Java语言一端的映射Class对象存放在一起,**存储于Java堆之中**,从我们的实验中也明确验证了这一点 @@ -1217,7 +1217,7 @@ public class StaticObjTest { - + @@ -1271,7 +1271,7 @@ public class BufferTest { 直接占用了 1G 的本地内存 - + @@ -1281,13 +1281,13 @@ public class BufferTest { 原来采用BIO的架构,在读写本地文件时,我们需要从用户态切换成内核态 - + **直接缓冲区(NIO)** NIO 直接操作物理磁盘,省去了中间过程 - + ### 直接内存与 OOM @@ -1351,7 +1351,7 @@ Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory - + diff --git a/docs/JVM/JVM系列-第7章-对象的实例化内存布局与访问定位.md b/docs/JVM/JVM系列-第7章-对象的实例化内存布局与访问定位.md index 110e55a..7a9d2d9 100644 --- a/docs/JVM/JVM系列-第7章-对象的实例化内存布局与访问定位.md +++ b/docs/JVM/JVM系列-第7章-对象的实例化内存布局与访问定位.md @@ -8,7 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第7章-对象的实例化内存布局与访问定位。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/jvm.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/jvm.png' abbrlink: debff71a date: 2020-11-14 19:38:42 --- @@ -36,7 +36,7 @@ date: 2020-11-14 19:38:42 - + ### 对象创建的方式 @@ -211,7 +211,7 @@ class Account{ 对象的内存布局 --------- - + @@ -242,7 +242,7 @@ class Account{ 图解内存布局 - + @@ -251,7 +251,7 @@ class Account{ **JVM是如何通过栈帧中的对象引用访问到其内部的对象实例呢?** - + 定位,通过栈上reference访问 @@ -262,7 +262,7 @@ class Account{ 1. 缺点:在堆空间中开辟了一块空间作为句柄池,句柄池本身也会占用空间;通过两次指针访问才能访问到堆中的对象,效率低 2. 优点:reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference本身不需要被修改 - + @@ -271,4 +271,4 @@ class Account{ 1. 优点:直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据 2. 缺点:对象被移动(垃圾收集时移动对象很普遍)时需要修改 reference 的值 - \ No newline at end of file + \ No newline at end of file diff --git a/docs/JVM/JVM系列-第8章-执行引擎.md b/docs/JVM/JVM系列-第8章-执行引擎.md index e6fbdef..33224ec 100644 --- a/docs/JVM/JVM系列-第8章-执行引擎.md +++ b/docs/JVM/JVM系列-第8章-执行引擎.md @@ -8,7 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第8章-执行引擎。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/jvm.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/jvm.png' abbrlink: 408712f4 date: 2020-11-15 19:48:42 --- @@ -23,7 +23,7 @@ date: 2020-11-15 19:48:42 - + ### 执行引擎概述 @@ -34,7 +34,7 @@ date: 2020-11-15 19:48:42 3. JVM的主要任务是负责**装载字节码到其内部**,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息。 4. 那么,如果想要让一个Java程序运行起来,执行引擎(Execution Engine)的任务就是**将字节码指令解释/编译为对应平台上的本地机器指令才可以**。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。 - + 1、前端编译:从Java程序员-字节码文件的这个过程叫前端编译 @@ -51,7 +51,7 @@ date: 2020-11-15 19:48:42 3. 当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。 4. 从外观上来看,所有的Java虚拟机的执行引擎输入、处理、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行、即时编译的等效过程,输出的是执行过程。 - + @@ -71,18 +71,18 @@ Java代码编译和执行过程 - + 3. javac编译器(前端编译器)流程图如下所示: - + 4. Java字节码的执行是由JVM执行引擎来完成,流程图如下所示 - + @@ -105,7 +105,7 @@ Java代码编译和执行过程 **用图总结一下** - + 机器码 指令 汇编语言 ------------- @@ -164,7 +164,7 @@ Java代码编译和执行过程 - + @@ -193,7 +193,7 @@ Java代码编译和执行过程 2. 汇编过程:实际上指把汇编语言代码翻译成目标机器指令的过程。 - + @@ -211,7 +211,7 @@ Java代码编译和执行过程 3. 当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。 - + @@ -288,7 +288,7 @@ Java代码编译和执行过程 2. 在生产环境发布过程中,以分批的方式进行发布,根据机器数量划分成多个批次,每个批次的机器数至多占到整个集群的1/8。曾经有这样的故障案例:某程序员在发布平台进行分批发布,在输入发布总批数时,误填写成分为两批发布。如果是热机状态,在正常情况下一半的机器可以勉强承载流量,但由于刚启动的JVM均是解释执行,还没有进行热点代码统计和JIT动态编译,导致机器启动之后,当前1/2发布成功的服务器马上全部宕机,此故障说明了JIT的存在。—**阿里团队** - + @@ -317,7 +317,7 @@ public class JITTest { 通过 JVisualVM 查看 JIT 编译器执行的编译次数 - + @@ -369,7 +369,7 @@ public class JITTest { * 如果已超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。 * 如果未超过阈值,则使用解释器对字节码文件解释执行 - + @@ -387,7 +387,7 @@ public class JITTest { 它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge)。显然,建立回边计数器统计的目的就是为了触发OSR编译。 - + @@ -401,7 +401,7 @@ public class JITTest { - + diff --git a/docs/JVM/JVM系列-第9章-StringTable(字符串常量池).md b/docs/JVM/JVM系列-第9章-StringTable(字符串常量池).md index b00b39a..d1bef75 100644 --- a/docs/JVM/JVM系列-第9章-StringTable(字符串常量池).md +++ b/docs/JVM/JVM系列-第9章-StringTable(字符串常量池).md @@ -8,7 +8,7 @@ categories: - 1.内存与垃圾回收篇 keywords: JVM,虚拟机。 description: JVM系列-第9章-StringTable(字符串常量池)。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/jvm.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/jvm.png' abbrlink: ee2ba71e date: 2020-11-16 12:38:02 --- @@ -166,11 +166,11 @@ str 的内容并没有变:“test ok” 位于字符串常量池中的另一 4. 在JDK7中,StringTable的长度默认值是60013,StringTablesize设置没有要求 5. 在JDK8中,StringTable的长度默认值是60013,StringTable可以设置的最小值为1009 - + - + @@ -276,11 +276,11 @@ String 的内存分配 - + - + @@ -382,23 +382,23 @@ public class StringTest4 { 1、程序启动时已经加载了 2293 个字符串常量 - + 2、加载了一个换行符(println),所以多了一个 - + 3、加载了字符串常量 “1”~“9” - + 4、加载字符串常量 “10” - + 5、之后的字符串"1" 到 "10"不会再次加载 - + @@ -425,7 +425,7 @@ class Memory { 分析运行时内存(foo() 方法是实例方法,其实图中少了一个 this 局部变量) - + @@ -493,7 +493,7 @@ class Memory { IDEA 反编译 class 文件后,来看这个问题 - + @@ -929,7 +929,7 @@ public class StringNewTest { 5. `23 ldc #8 ` :在字符串常量池中放入 “b”(如果之前字符串常量池中没有 “b” 的话) 6. `31 invokevirtual #9 ` :调用 StringBuilder 的 toString() 方法,会生成一个 String 对象 - + @@ -988,13 +988,13 @@ JDK6 :正常眼光判断即可 * new String() 即在堆中 * str.intern() 则把字符串放入常量池中 - + JDK7及后续版本,**注意大坑** - + @@ -1053,11 +1053,11 @@ public class StringExer1 { **JDK6** -![image-20201116113423492](https://npm.elemecdn.com/youthlql@1.0.8/JVM/chapter_009/0015.png) +![image-20201116113423492](https://upyunimg.imlql.cn/youthlql@1.0.8/JVM/chapter_009/0015.png) **JDK7/8** - + @@ -1080,7 +1080,7 @@ public class StringExer1 { } ``` - + **练习3** @@ -1169,11 +1169,11 @@ public class StringIntern2 { arr[i] = new String(String.valueOf(data[i % data.length])); ``` - + - + 2、使用 intern() 方法:由于数组中字符串的引用都指向字符串常量池中的字符串,所以程序需要维护的 String 对象更少,内存占用也更低 @@ -1182,11 +1182,11 @@ arr[i] = new String(String.valueOf(data[i % data.length])); arr[i] = new String(String.valueOf(data[i % data.length])).intern(); ``` - + - + @@ -1218,11 +1218,11 @@ public class StringGCTest { * Number of entries 和 Number of literals 明显没有 100000 * 以上两点均说明 StringTable 区发生了垃圾回收 - + - + diff --git a/docs/Java/Basis/Java8新特性.md b/docs/Java/Basis/Java8新特性.md index fdfb545..eb9eb86 100644 --- a/docs/Java/Basis/Java8新特性.md +++ b/docs/Java/Basis/Java8新特性.md @@ -9,7 +9,7 @@ categories: - 新特性 keywords: Java8,新特性,JDK8 description: 详解JDK8出现的新特性。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/java.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/java.png' abbrlink: de3879ae date: 2020-10-19 22:15:58 --- @@ -20,7 +20,7 @@ date: 2020-10-19 22:15:58 > 本篇文章只讲解比较重要的 - + @@ -308,13 +308,13 @@ public class LambdaTest1 { **核心函数式接口** - + **其它函数式接口** - + @@ -902,7 +902,7 @@ Stream到底是什么呢? - + @@ -1219,7 +1219,7 @@ public class StreamAPITest2 { ## 常用API - + diff --git a/docs/Java/Basis/泛型.md b/docs/Java/Basis/泛型.md index 9156e82..26d5f7f 100644 --- a/docs/Java/Basis/泛型.md +++ b/docs/Java/Basis/泛型.md @@ -9,7 +9,7 @@ categories: - 重难点 keywords: Java基础,泛型 description: 万字长文详解Java泛型。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/java.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/java.png' abbrlink: adb2faf0 date: 2020-10-19 22:21:58 --- @@ -1038,7 +1038,7 @@ class Dog extends Animal { `test1()`在编译时就会飘红 - + @@ -1290,7 +1290,7 @@ public class Test_difference { } ``` - + ### 区别3:?通配符可以使用超类限定而T不行 diff --git a/docs/Java/collection/HashMap-JDK7源码讲解.md b/docs/Java/collection/HashMap-JDK7源码讲解.md index 516b55d..451966c 100644 --- a/docs/Java/collection/HashMap-JDK7源码讲解.md +++ b/docs/Java/collection/HashMap-JDK7源码讲解.md @@ -8,7 +8,7 @@ categories: - HashMap keywords: Java集合,HashMap。 description: HashMap-JDK7源码讲解。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/java.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/java.png' abbrlink: f1f58db2 date: 2020-11-01 10:21:58 --- @@ -202,7 +202,7 @@ hadoop2 大致是这样的一个结构 - + - 每个链表就算哈希表的桶(bucket) - 链表的节点值就算一个键值对 @@ -398,7 +398,7 @@ static class Entry implements Map.Entry { `HashMap`中的数组元素 & 链表节点 采用 `Entry`类实现 - + 1、一个正方形代表一个Entry对象,同时也代表一个键值对。 @@ -769,15 +769,15 @@ void transfer(Entry[] newTable, boolean rehash) { 大概画了一下图: - + - + - + - + @@ -871,7 +871,7 @@ void transfer(Entry[] newTable, boolean rehash) { **hashmap初始状态** - + @@ -897,7 +897,7 @@ void transfer(Entry[] newTable, boolean rehash) { **两个线程调用完毕之后,hashmap目前是这样的。** - + @@ -918,9 +918,9 @@ void transfer(Entry[] newTable, boolean rehash) { 3、来看下此时内存里的状态 - + - + ## 步骤4 @@ -957,7 +957,7 @@ void transfer(Entry[] newTable, boolean rehash) { 2、线程2直接**扩容完毕**,那么完成后的状态是这样【假设e2和e3还是hash到同一个位置】 - + 3、线程1还是原来的状态 @@ -967,11 +967,11 @@ void transfer(Entry[] newTable, boolean rehash) { 目前两个线程里的新数组是这样的 - + 为了方便后面观看,我画成这样。 - + @@ -1015,7 +1015,7 @@ void transfer(Entry[] newTable, boolean rehash) { 也就变成了下面这个样子。 - + @@ -1061,7 +1061,7 @@ void transfer(Entry[] newTable, boolean rehash) { 执行完,变成这样。 - + @@ -1077,7 +1077,7 @@ void transfer(Entry[] newTable, boolean rehash) { 3、执行pos_3: newTable[i] = e得到 newTable1[3] == e2 - + 这样就形成了循环链表,再get()数据就会陷入死循环。 diff --git a/docs/Java/collection/HashMap-JDK8源码讲解及常见面试题.md b/docs/Java/collection/HashMap-JDK8源码讲解及常见面试题.md index fdebb79..91d8ea2 100644 --- a/docs/Java/collection/HashMap-JDK8源码讲解及常见面试题.md +++ b/docs/Java/collection/HashMap-JDK8源码讲解及常见面试题.md @@ -8,7 +8,7 @@ categories: - HashMap keywords: Java集合,HashMap。 description: HashMap-JDK8源码讲解及常见面试题。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/java.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/java.png' abbrlink: cbc5672a date: 2020-11-01 10:22:05 --- @@ -27,7 +27,7 @@ date: 2020-11-01 10:22:05 在JDK8中,优化了HashMap的数据结构,引入了红黑树。即HashMap的数据结构:数组+链表+红黑树。HashMap变成了这样。 - + ### 为什么要引入红黑树 @@ -456,7 +456,7 @@ Process finished with exit code 0 JDK8 hash的运算原理:高位参与低位运算,使得hash更加均匀。 - + @@ -569,7 +569,7 @@ JDK8 hash的运算原理:高位参与低位运算,使得hash更加均匀。 JDK8扩容时,数据在数组下标的计算方式 - + * `JDK8`根据此结论作出的新元素存储位置计算规则非常简单,提高了扩容效率。 diff --git a/docs/design_patterns/设计模式-01.设计思想.md b/docs/design_patterns/设计模式-01.设计思想.md index 995d542..0ed9f7e 100644 --- a/docs/design_patterns/设计模式-01.设计思想.md +++ b/docs/design_patterns/设计模式-01.设计思想.md @@ -8,7 +8,7 @@ categories: - 01.设计思想 keywords: 设计模式,设计思想 description: 设计模式第一部分-常用设计思想。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/design_patterns.jpg' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/design_patterns.jpg' abbrlink: c3dcce5d date: 2021-06-07 17:21:58 --- @@ -198,7 +198,7 @@ public class Ostrich extends AbstractBird { //鸵鸟 1. 这种设计思路虽然可以解决问题,但不够优美。因为除了鸵鸟之外,不会飞的鸟还有很多,比如企鹅。对于这些不会飞的鸟来说,我们都需要重写 fly() 方法,抛出异常。这样的设计,一方面,徒增了编码的工作量;另一方面,也违背了我们之后要讲的最小知识原则(Least Knowledge Principle,也叫最少知识原则或者迪米特法则),暴露不该暴露的接口给外部,增加了类使用过程中被误用的概率。 2. 你可能又会说,那我们再通过 AbstractBird 类派生出两个更加细分的抽象类:会飞的鸟类 AbstractFlyableBird 和不会飞的鸟类 AbstractUnFlyableBird,让麻雀、乌鸦这些会飞的鸟都继承 AbstractFlyableBird,让鸵鸟、企鹅这些不会飞的鸟,都继承 AbstractUnFlyableBird 类,不就可以了吗?具体的继承关系如下图所示: - + @@ -206,7 +206,7 @@ public class Ostrich extends AbstractBird { //鸵鸟 2. 是否会飞?是否会叫?两个行为搭配起来会产生四种情况:会飞会叫、不会飞会叫、会飞不会叫、不会飞不会叫。如果我们继续沿用刚才的设计思路,那就需要再定义四个抽象类(AbstractFlyableTweetableBird、AbstractFlyableUnTweetableBird、AbstractUnFlyableTweetableBird、AbstractUnFlyableUnTweetableBird)。 - + @@ -376,7 +376,7 @@ demofunction(client); - + diff --git a/docs/design_patterns/设计模式-02.经典设计原则-第一节[必读].md b/docs/design_patterns/设计模式-02.经典设计原则-第一节[必读].md index c8d9629..215f33c 100644 --- a/docs/design_patterns/设计模式-02.经典设计原则-第一节[必读].md +++ b/docs/design_patterns/设计模式-02.经典设计原则-第一节[必读].md @@ -8,7 +8,7 @@ categories: - 02.经典设计原则 keywords: 设计模式,经典设计原则 description: 设计模式-经典设计原则,例如:单一职责原则,开闭原则,接口隔离原则等等。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/design_patterns.jpg' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/design_patterns.jpg' abbrlink: fc1c7619 date: 2021-06-13 19:21:58 --- diff --git a/docs/design_patterns/设计模式-02.经典设计原则-第二节[必读].md b/docs/design_patterns/设计模式-02.经典设计原则-第二节[必读].md index af94ca4..1dad219 100644 --- a/docs/design_patterns/设计模式-02.经典设计原则-第二节[必读].md +++ b/docs/design_patterns/设计模式-02.经典设计原则-第二节[必读].md @@ -8,7 +8,7 @@ categories: - 02.经典设计原则 keywords: 设计模式,经典设计原则 description: 设计模式-经典设计原则,例如:迪米特法则,依赖反转原则,KISS等等。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/design_patterns.jpg' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/design_patterns.jpg' abbrlink: 994a8ed3 date: 2021-06-20 19:21:58 --- @@ -724,7 +724,7 @@ public class UserRepo { 前面也提到,“高内聚”有助于“松耦合”,同理,“低内聚”也会导致“紧耦合”。关于这一点,我画了一张对比图来解释。图中左边部分的代码结构是“高内聚、松耦合”;右边部分正好相反,是“低内聚、紧耦合”。 - + diff --git a/docs/design_patterns/设计模式-03.01-创建型-单例.md b/docs/design_patterns/设计模式-03.01-创建型-单例.md index 53ce080..d131268 100644 --- a/docs/design_patterns/设计模式-03.01-创建型-单例.md +++ b/docs/design_patterns/设计模式-03.01-创建型-单例.md @@ -8,7 +8,7 @@ categories: - 03.创建型 keywords: 设计模式,单例 description: 详解了单例设计模式。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/design_patterns.jpg' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/design_patterns.jpg' abbrlink: b5a1ed4a date: 2021-06-26 21:51:58 --- diff --git a/docs/design_patterns/设计模式-03.02-创建型-工厂&建造者&原型.md b/docs/design_patterns/设计模式-03.02-创建型-工厂&建造者&原型.md index 938a825..ff04ec4 100644 --- a/docs/design_patterns/设计模式-03.02-创建型-工厂&建造者&原型.md +++ b/docs/design_patterns/设计模式-03.02-创建型-工厂&建造者&原型.md @@ -10,7 +10,7 @@ categories: - 03.创建型 keywords: 设计模式,工厂,建造者,原型 description: 详解常用的工厂模式和建造者模式,以及不常用的原型模式 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/design_patterns.jpg' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/design_patterns.jpg' abbrlink: ba432704 date: 2021-06-27 00:51:58 --- @@ -727,7 +727,7 @@ public class BeansFactory { 1. 在平时的开发中,创建一个对象最常用的方式是,使用 new 关键字调用类的构造函数来完成。我的问题是,什么情况下这种方式就不适用了,就需要采用建造者模式来创建对象呢?你可以先思考一下,下面我通过一个例子来带你看一下。 2. 假设有这样一道设计面试题:我们需要定义一个资源池配置类 ResourcePoolConfig。这里的资源池,你可以简单理解为线程池、连接池、对象池等。在这个资源池配置类中,有以下几个成员变量,也就是可配置项。现在,请你编写代码实现这个 ResourcePoolConfig 类。 - + @@ -1003,7 +1003,7 @@ r.setHeight(3); // r is valid 2. 如果你熟悉的是 Java 语言,可以直接使用语言中提供的 HashMap 容器来实现。其中,HashMap 的 key 为搜索关键词,value 为关键词详细信息(比如搜索次数)。我们只需要将数据从数据库中读取出来,放入 HashMap 就可以了。 3. 不过,我们还有另外一个系统 B,专门用来分析搜索日志,定期(比如间隔 10 分钟)批量地更新数据库中的数据,并且标记为新的数据版本。比如,在下面的示例图中,我们对 v2 版本的数据进行更新,得到 v3 版本的数据。这里我们假设只有更新和新添关键词,没有删除关键词的行为。 - + @@ -1150,15 +1150,15 @@ public class Demo { 我们来看,在内存中,用散列表组织的搜索关键词信息是如何存储的。我画了一张示意图,大致结构如下所示。从图中我们可以发现,散列表索引中,每个结点存储的 key 是搜索关键词,value 是 SearchWord 对象的内存地址。SearchWord 对象本身存储在散列表之外的内存空间中。 - + 浅拷贝和深拷贝的区别在于,浅拷贝只会复制图中的索引(散列表),不会复制数据(SearchWord 对象)本身。相反,深拷贝不仅仅会复制索引,还会复制数据本身。浅拷贝得到的对象(newKeywords)跟原始对象(currentKeywords)共享数据(SearchWord 对象),而深拷贝得到的是一份完完全全独立的对象。具体的对比如下图所示: - + - + diff --git a/docs/design_patterns/设计模式-04.01-结构型-代理&桥接&装饰器&适配器.md b/docs/design_patterns/设计模式-04.01-结构型-代理&桥接&装饰器&适配器.md index aaf201d..e49f269 100644 --- a/docs/design_patterns/设计模式-04.01-结构型-代理&桥接&装饰器&适配器.md +++ b/docs/design_patterns/设计模式-04.01-结构型-代理&桥接&装饰器&适配器.md @@ -11,7 +11,7 @@ categories: - 04.结构型 keywords: 设计模式,代理模式,桥接模式,装饰器模式,适配器模式 description: 对代理模式,桥接模式,装饰器模式,适配器模式这4个模式进行了比较详细的讲述。其实学习设计模式主要是为了后序看源码 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/design_patterns.jpg' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/design_patterns.jpg' abbrlink: 926a065c date: 2021-07-04 00:51:58 --- @@ -608,7 +608,7 @@ IUserController userController = (IUserController) proxy.createProxy(new UserCon 现在对不同手机类型的不同品牌实现操作编程(比如:开机、关机、上网,打电话等),如图: - + @@ -616,7 +616,7 @@ IUserController userController = (IUserController) proxy.createProxy(new UserCon 传统方法对应的类图 - + 1. 扩展性问题(**类爆炸**),如果我们再增加手机的样式(旋转式),就需要增加各个品牌手机的类,同样如果我们增加一个手机品牌,也要在各个手机样式类下增加。 2. 违反了单一职责原则,当我们增加手机样式时,要同时增加所有品牌的手机,这样增加了代码维护成本. @@ -933,7 +933,7 @@ public class DriverManager { - + @@ -1112,7 +1112,7 @@ public class TrivialNotification extends Notification { ### 方案一 - + @@ -1127,7 +1127,7 @@ public class TrivialNotification extends Notification { 前面分析到方案 1 因为咖啡单品+调料组合会造成类的倍增,因此可以做改进,将调料内置到 Drink 类,这样就不会造成类数量过多。从而提高项目的维护性(如图) - + @@ -1142,7 +1142,7 @@ public class TrivialNotification extends Notification { ### 装饰器模式代码 - + #### Drink【抽象类-主体Component】 @@ -1390,7 +1390,7 @@ Java IO 类库非常庞大和复杂,有几十个类,负责 IO 数据的读 针对不同的读取和写入场景,Java IO 又在这四个父类基础之上,扩展出了很多子类。具体如下所示: - + diff --git a/docs/design_patterns/设计模式-04.02-结构型-门面&组合&享元.md b/docs/design_patterns/设计模式-04.02-结构型-门面&组合&享元.md index 111e448..9d89373 100644 --- a/docs/design_patterns/设计模式-04.02-结构型-门面&组合&享元.md +++ b/docs/design_patterns/设计模式-04.02-结构型-门面&组合&享元.md @@ -10,7 +10,7 @@ categories: - 04.结构型 keywords: 设计模式,门面模式,组合模式,享元模式 description: 对代理模式,门面模式,组合模式,享元模式这3个设计模式进行了比较详细的讲述。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/design_patterns.jpg' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/design_patterns.jpg' abbrlink: 5ea604e0 date: 2021-07-05 16:51:58 --- @@ -75,7 +75,7 @@ date: 2021-07-05 16:51:58 ### 传统方案 - + 1. 在 ClientTest 的 main 方法中,创建各个子系统的对象,并直接去调用子系统(对象)相关方法,会造成调用过程 混乱,没有清晰的过程 不利于在 ClientTest 中,去维护对子系统的操作 @@ -623,7 +623,7 @@ public class Demo { - + diff --git a/docs/design_patterns/设计模式-05.01-行为型-观察者&模板.md b/docs/design_patterns/设计模式-05.01-行为型-观察者&模板.md index a5d9c48..e314ad5 100644 --- a/docs/design_patterns/设计模式-05.01-行为型-观察者&模板.md +++ b/docs/design_patterns/设计模式-05.01-行为型-观察者&模板.md @@ -8,7 +8,7 @@ categories: - 05.行为型 keywords: 观察者模式,模板模式 description: 观察者模式,模板模式。很常用的两个模式 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/design_patterns.jpg' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/design_patterns.jpg' abbrlink: dd09051e date: 2021-07-14 16:51:58 --- @@ -34,7 +34,7 @@ date: 2021-07-14 16:51:58 ### 方案一 - + @@ -721,11 +721,11 @@ public DObserver{ 1. Guava EventBus 的功能我们已经讲清楚了,总体上来说,还是比较简单的。接下来,我们就重复造轮子,“山寨”一个 EventBus 出来。 2. 我们重点来看,EventBus 中两个核心函数 register() 和 post() 的实现原理。弄懂了它们,基本上就弄懂了整个 EventBus 框架。下面两张图是这两个函数的实现原理图。 - + - + diff --git a/docs/design_patterns/设计模式-05.02-行为型-策略&职责链.md b/docs/design_patterns/设计模式-05.02-行为型-策略&职责链.md index a8b7e10..39ec29b 100644 --- a/docs/design_patterns/设计模式-05.02-行为型-策略&职责链.md +++ b/docs/design_patterns/设计模式-05.02-行为型-策略&职责链.md @@ -8,7 +8,7 @@ categories: - 05.行为型 keywords: 策略模式,职责链模式 description: 不多说,看文章 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/design_patterns.jpg' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/design_patterns.jpg' abbrlink: 2c3cc5fd date: 2021-08-01 15:51:58 --- @@ -1171,7 +1171,7 @@ public class SensitiveWordFilter { Servlet Filter 是 Java Servlet 规范中定义的组件,翻译成中文就是过滤器,它可以实现对 HTTP 请求的过滤功能,比如鉴权、限流、记录日志、验证参数等等。因为它是 Servlet 规范的一部分,所以,只要是支持 Servlet 的 Web 容器(比如,Tomcat、Jetty 等),都支持过滤器功能。为了帮助你理解,我画了一张示意图阐述它的工作原理,如下所示。 - + 在实际项目中,我们该如何使用 Servlet Filter 呢?我写了一个简单的示例代码,如下所示。添加一个过滤器,我们只需要定义一个实现 javax.servlet.Filter 接口的过滤器类,并且将它配置在 web.xml 配置文件中。Web 容器启动的时候,会读取 web.xml 中的配置,创建过滤器对象。当有请求到来的时候,会先经过过滤器,然后才由 Servlet 来处理。 @@ -1279,7 +1279,7 @@ ApplicationFilterChain 中的 doFilter() 函数的代码实现比较有技巧, 1. 刚刚讲了 Servlet Filter,现在我们来讲一个功能上跟它非常类似的东西,Spring Interceptor,翻译成中文就是拦截器。尽管英文单词和中文翻译都不同,但这两者基本上可以看作一个概念,都用来实现对 HTTP 请求进行拦截处理。 2. 它们不同之处在于,Servlet Filter 是 Servlet 规范的一部分,实现依赖于 Web 容器。Spring Interceptor 是 Spring MVC 框架的一部分,由 Spring MVC 框架来提供实现。客户端发送的请求,会先经过 Servlet Filter,然后再经过 Spring Interceptor,最后到达具体的业务代码中。我画了一张图来阐述一个请求的处理流程,具体如下所示。 - + diff --git a/docs/design_patterns/设计模式-05.03-行为型-状态&迭代器.md b/docs/design_patterns/设计模式-05.03-行为型-状态&迭代器.md index 0d9480b..4d1baf8 100644 --- a/docs/design_patterns/设计模式-05.03-行为型-状态&迭代器.md +++ b/docs/design_patterns/设计模式-05.03-行为型-状态&迭代器.md @@ -8,7 +8,7 @@ categories: - 05.行为型 keywords: 状态模式,迭代器模式 description: 看文章 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/design_patterns.jpg' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/design_patterns.jpg' abbrlink: 877f4ef2 date: 2021-08-02 15:51:58 --- @@ -30,7 +30,7 @@ date: 2021-08-02 15:51:58 4. 实际上,马里奥形态的转变就是一个状态机。其中,马里奥的不同形态就是状态机中的“状态”,游戏情节(比如吃了蘑菇)就是状态机中的“事件”,加减积分就是状态机中的“动作”。比如,吃蘑菇这个事件,会触发状态的转移:从小马里奥转移到超级马里奥,以及触发动作的执行(增加 100 积分)。 5. 为了方便接下来的讲解,我对游戏背景做了简化,只保留了部分状态和事件。简化之后的状态转移如下图所示: - + @@ -180,7 +180,7 @@ public class MarioStateMachine { 1. 实际上,上面这种实现方法有点类似 hard code,对于复杂的状态机来说不适用,而状态机的第二种实现方式查表法,就更加合适了。接下来,我们就一块儿来看下,如何利用查表法来补全骨架代码。 2. 实际上,除了用状态转移图来表示之外,状态机还可以用二维表来表示,如下所示。在这个二维表中,第一维表示当前状态,第二维表示事件,值表示当前状态经过事件之后,转移到的新状态及其执行的动作。 - + 3. 相对于分支逻辑的实现方式,查表法的代码实现更加清晰,可读性和可维护性更好。当修改状态机时,我们只需要修改 transitionTable 和 actionTable 两个二维数组即可。实际上,如果我们把这两个二维数组存储在配置文件中,当需要修改状态机时,我们甚至可以不修改任何代码,只需要修改配置文件就可以了。具体的代码如下所示: @@ -511,7 +511,7 @@ public class MarioStateMachine { 2. 在开篇中我们讲到,它用来遍历集合对象。这里说的“集合对象”也可以叫“容器”“聚合对象”,实际上就是包含一组对象的对象,比如数组、链表、树、图、跳表。迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一。 3. 迭代器是用来遍历容器的,所以,一个完整的迭代器模式一般会涉及**容器和容器迭代器**部分两部分内容。为了达到基于接口而非实现编程的目的,容器又包含容器接口、容器实现类,迭代器又包含迭代器接口、迭代器实现类。对于迭代器模式,我画了一张简单的类图,你可以看一看,先有个大致的印象。 - + @@ -629,7 +629,7 @@ public class Demo { 2. 结合刚刚的例子,我们来总结一下迭代器的设计思路。总结下来就三句话:迭代器中需要定义 hasNext()、currentItem()、next() 三个最基本的方法。待遍历的容器对象通过依赖注入传递到迭代器类中。容器通过 iterator() 方法来创建迭代器。 3. 这里我画了一张类图,如下所示。实际上就是对上面那张类图的细化,你可以结合着一块看。 - + @@ -752,7 +752,7 @@ public class Demo { 4. 为了保持数组存储数据的连续性,数组的删除操作会涉及元素的搬移。当执行到第 57 行代码的时候,我们从数组中将元素 a 删除掉,b、c、d 三个元素会依次往前搬移一位,这就会导致游标本来指向元素 b,现在变成了指向元素 c。原本在执行完第 56 行代码之后,我们还可以遍历到 b、c、d 三个元素,但在执行完第 57 行代码之后,我们只能遍历到 c、d 两个元素,b 遍历不到了。 5. 对于上面的描述,我画了一张图,你可以对照着理解。 - + 6. 不过,如果第 57 行代码删除的不是游标前面的元素(元素 a)以及游标所在位置的元素(元素 b),而是游标后面的元素(元素 c 和 d),这样就不会存在任何问题了,不会存在某个元素遍历不到的情况了。 7. 所以,我们前面说,在遍历的过程中删除集合元素,结果是不可预期的,有时候没问题(删除元素 c 或 d),有时候就有问题(删除元素 a 或 b),这个要视情况而定(到底删除的是哪个位置的元素),就是这个意思。 @@ -778,7 +778,7 @@ public class Demo { 10. 跟删除情况类似,如果我们在游标的后面添加元素,就不会存在任何问题。所以,在遍历的同时添加集合元素也是一种不可预期行为。 11. 同样,对于上面的添加元素的情况,我们也画了一张图,如下所示,你可以对照着理解。 - + diff --git a/docs/dubbo-sourcecode-v1/01&02.Dubbo源码系列V1-Dubbo第一二节-基本应用与高级应用.md b/docs/dubbo-sourcecode-v1/01&02.Dubbo源码系列V1-Dubbo第一二节-基本应用与高级应用.md index 1e0bcb7..16a8d20 100644 --- a/docs/dubbo-sourcecode-v1/01&02.Dubbo源码系列V1-Dubbo第一二节-基本应用与高级应用.md +++ b/docs/dubbo-sourcecode-v1/01&02.Dubbo源码系列V1-Dubbo第一二节-基本应用与高级应用.md @@ -8,7 +8,7 @@ categories: - Dubbo源码系列v1 keywords: Dubbo,rpc description: 前两节合成一节 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/dubbo.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/dubbo.png' abbrlink: d3c530c4 date: 2021-09-11 15:21:58 --- @@ -97,7 +97,7 @@ Dubbo网关参考:[https://github.com/apache/dubbo-proxy](https://github.com/a ### 基本原理 - + @@ -914,11 +914,11 @@ dubbo.config-center.address= 2. 管理台的**配置管理**作用就是可以实时更改dubbo相关的配置,在这里面写了和在appliaction.properties里面写是一样的效果,这个还不用重启服务。如果appliaction.properties里和管理台写了相同的配置,以管理台的为主。 - + 3. **动态配置**这里,也可以很方便的替代服务提供者@service注解上标注的那些配置。管理台是实时生效的,如果改代码里的@service还需要重启服务。 - + 很多配置都可以在管理台上配。管理台上写的配置会持久化在**你配置的配置中心**里。只有注册中心里的服务提供者信息不持久化,如果注册中心是zookeeper,那么服务提供者在zk上就是临时节点。 diff --git a/docs/dubbo-sourcecode-v1/03.Dubbo源码系列V1-Dubbo第三节-可扩展机制SPI源码解析.md b/docs/dubbo-sourcecode-v1/03.Dubbo源码系列V1-Dubbo第三节-可扩展机制SPI源码解析.md index 8a91313..289f90d 100644 --- a/docs/dubbo-sourcecode-v1/03.Dubbo源码系列V1-Dubbo第三节-可扩展机制SPI源码解析.md +++ b/docs/dubbo-sourcecode-v1/03.Dubbo源码系列V1-Dubbo第三节-可扩展机制SPI源码解析.md @@ -8,7 +8,7 @@ categories: - Dubbo源码系列v1 keywords: Dubbo,rpc description: Dubbo里面SPI是基础,大量用到了SPI -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/dubbo.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/dubbo.png' abbrlink: dbcfef47 date: 2021-09-12 15:21:58 --- diff --git a/docs/dubbo-sourcecode-v1/04.Dubbo源码系列V1-Dubbo第四节-Spring与Dubbo整合原理与源码分析.md b/docs/dubbo-sourcecode-v1/04.Dubbo源码系列V1-Dubbo第四节-Spring与Dubbo整合原理与源码分析.md index c34d278..9787476 100644 --- a/docs/dubbo-sourcecode-v1/04.Dubbo源码系列V1-Dubbo第四节-Spring与Dubbo整合原理与源码分析.md +++ b/docs/dubbo-sourcecode-v1/04.Dubbo源码系列V1-Dubbo第四节-Spring与Dubbo整合原理与源码分析.md @@ -8,7 +8,7 @@ categories: - Dubbo源码系列v1 keywords: Dubbo,rpc description: Spring与Dubbo整合原理与源码分析 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/dubbo.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/dubbo.png' abbrlink: 796f395d date: 2021-10-06 13:21:58 --- diff --git a/docs/dubbo-sourcecode-v1/05.Dubbo源码系列V1-Dubbo第五节-服务导出源码解析.md b/docs/dubbo-sourcecode-v1/05.Dubbo源码系列V1-Dubbo第五节-服务导出源码解析.md index ad652f9..710520c 100644 --- a/docs/dubbo-sourcecode-v1/05.Dubbo源码系列V1-Dubbo第五节-服务导出源码解析.md +++ b/docs/dubbo-sourcecode-v1/05.Dubbo源码系列V1-Dubbo第五节-服务导出源码解析.md @@ -8,7 +8,7 @@ categories: - Dubbo源码系列v1 keywords: Dubbo,rpc description: Dubbo服务导出源码解析 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/dubbo.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/dubbo.png' abbrlink: '48141866' date: 2021-10-06 14:11:58 --- diff --git a/docs/dubbo-sourcecode-v1/06.Dubbo源码系列V1-Dubbo第六节-服务引入源码解析.md b/docs/dubbo-sourcecode-v1/06.Dubbo源码系列V1-Dubbo第六节-服务引入源码解析.md index 02e2073..6feaae4 100644 --- a/docs/dubbo-sourcecode-v1/06.Dubbo源码系列V1-Dubbo第六节-服务引入源码解析.md +++ b/docs/dubbo-sourcecode-v1/06.Dubbo源码系列V1-Dubbo第六节-服务引入源码解析.md @@ -8,7 +8,7 @@ categories: - Dubbo源码系列v1 keywords: Dubbo,rpc description: 服务引入源码解析 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/dubbo.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/dubbo.png' abbrlink: bda15919 date: 2021-11-08 14:11:58 --- @@ -243,7 +243,7 @@ public class Application { - + ### 源码分析-解析@Reference注解上的配置 @@ -438,19 +438,19 @@ public class Application { ``` - + Dubbo官方给的Demo没有配置URL,所以这里就是NULL - + - + - + @@ -622,7 +622,7 @@ Dubbo官方给的Demo没有配置URL,所以这里就是NULL } ``` - + #### RegistryDirectory @@ -641,7 +641,7 @@ Dubbo官方给的Demo没有配置URL,所以这里就是NULL 3. 看下面的截图,registry属性是zookeeper的URL,所以应该是要调用ZookeeperRegistry的subscribe()方法,但是ZookeeperRegistry没有这个方法,所以我们就要找它的父类了,也就是FailbackRegistry, 4. 然后再调用doSubscribe(),ZookeeperRegistry重写了此方法,很明显这是个模板模式。 - + #### FailbackRegistry @@ -843,7 +843,7 @@ Dubbo官方给的Demo没有配置URL,所以这里就是NULL 最终走到了这一步 - + #### RegistryDirectory @@ -1097,7 +1097,7 @@ consumer://192.168.0.100/org.apache.dubbo.demo.DemoService?application=dubbo-dem 到此,路由链构造完毕。 - + diff --git a/docs/dubbo-sourcecode-v1/07.Dubbo源码系列V1-Dubbo第七节-服务调用源码解析.md b/docs/dubbo-sourcecode-v1/07.Dubbo源码系列V1-Dubbo第七节-服务调用源码解析.md index f5215f1..cbda226 100644 --- a/docs/dubbo-sourcecode-v1/07.Dubbo源码系列V1-Dubbo第七节-服务调用源码解析.md +++ b/docs/dubbo-sourcecode-v1/07.Dubbo源码系列V1-Dubbo第七节-服务调用源码解析.md @@ -8,7 +8,7 @@ categories: - Dubbo源码系列v1 keywords: Dubbo,rpc description: 服务调用源码解析 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/dubbo.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/dubbo.png' abbrlink: 84653c9d date: 2021-11-09 14:11:58 --- diff --git a/docs/java_concurrency/Java并发体系-第一阶段-多线程基础知识.md b/docs/java_concurrency/Java并发体系-第一阶段-多线程基础知识.md index 9fd98ca..4d977d1 100644 --- a/docs/java_concurrency/Java并发体系-第一阶段-多线程基础知识.md +++ b/docs/java_concurrency/Java并发体系-第一阶段-多线程基础知识.md @@ -9,7 +9,7 @@ categories: - 原理 keywords: Java并发,原理,源码 description: 万字系列长文讲解Java并发-第一阶段-多线程基础知识。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/Java_concurrency.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/Java_concurrency.png' abbrlink: efc79183 date: 2020-10-05 22:40:58 --- @@ -31,13 +31,13 @@ date: 2020-10-05 22:40:58 概念:进程可进一步细化为线程,是一个程序内部的一条执行路径。 说明:线程作为CPU调度和执行的单位,每个线程拥独立的运行栈和程序计数器(pc),线程切换的开销小。 - + 补充: - + 进程可以细化为多个线程。 每个线程,拥有自己独立的:栈、程序计数器 @@ -281,7 +281,7 @@ private void init(ThreadGroup g, Runnable target, String name, 1、多线程的设计之中,使用了代理设计模式的结构,用户自定义的线程主体只是负责项目核心功能的实现,而所有的辅助实现全部交由Thread类来处理。 2、在进行Thread启动多线程的时候调用的是start()方法,而后找到的是run()方法,但通过Thread类的构造方法传递了一个Runnable接口对象的时候,那么该接口对象将被Thread类中的target属性所保存,在start()方法执行的时候会调用Thread类中的run()方法。而这个run()方法去调用实现了Runnable接口的那个类所重写过run()方法,进而执行相应的逻辑。多线程开发的本质实质上是在于多个线程可以进行同一资源的抢占,那么Thread主要描述的是线程,而资源的描述是通过Runnable完成的。如下图所示: - + @@ -644,7 +644,7 @@ public void run() { 1、如果直接调用run()方法,相当于就是简单的调用一个普通方法。 - + 2、run()的调用是在start0()这个Native C++方法里调用的 @@ -654,11 +654,11 @@ public void run() { Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态,这几个状态在Java源码中用枚举来表示。 - + 线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示。 - + > 图中 wait到 runnable状态的转换中,`join`实际上是`Thread`类的方法,但这里写成了`Object`。 @@ -699,7 +699,7 @@ public static void main(String[] args) { 2、当JVM启动后,实际有多个线程,但是至少有一个非守护线程(比如main线程)。 - + - Finalizer:GC守护线程 @@ -1574,7 +1574,7 @@ volatile自己虽然不能保证原子性,但是和CAS结合起来就可以保 - + @@ -1782,7 +1782,7 @@ public Unsafe getUnsafe() throws IllegalAccessException { Unsafe的功能如下图: - + ## CAS相关 diff --git a/docs/java_concurrency/Java并发体系-第三阶段-JUC并发包-[1].md b/docs/java_concurrency/Java并发体系-第三阶段-JUC并发包-[1].md index cab8af9..5a8c137 100644 --- a/docs/java_concurrency/Java并发体系-第三阶段-JUC并发包-[1].md +++ b/docs/java_concurrency/Java并发体系-第三阶段-JUC并发包-[1].md @@ -9,7 +9,7 @@ categories: - 原理 keywords: Java并发,原理,源码 description: 万字系列长文讲解-Java并发体系-第三阶段-JUC并发包。JUC在高并发编程中使用频率非常高,这里会详细介绍其用法。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/Java_concurrency.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/Java_concurrency.png' abbrlink: 5be45d9e date: 2020-10-09 22:13:58 --- @@ -1869,7 +1869,7 @@ public class ForkJoinRecursiveAction { ForkJoinTask就是ForkJoinPool里面的每一个任务。他主要有两个子类:`RecursiveAction`和`RecursiveTask`。然后通过fork()方法去分配任务执行任务,通过join()方法汇总任务结果。 - + ## 小总结 diff --git a/docs/java_concurrency/Java并发体系-第三阶段-JUC并发包-[2].md b/docs/java_concurrency/Java并发体系-第三阶段-JUC并发包-[2].md index 68943bf..0a5884c 100644 --- a/docs/java_concurrency/Java并发体系-第三阶段-JUC并发包-[2].md +++ b/docs/java_concurrency/Java并发体系-第三阶段-JUC并发包-[2].md @@ -9,7 +9,7 @@ categories: - 原理 keywords: Java并发,原理,源码 description: 万字系列长文讲解-Java并发体系-第三阶段-JUC并发包。JUC在高并发编程中使用频率非常高,这里会详细介绍其用法。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/Java_concurrency.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/Java_concurrency.png' abbrlink: 70c90e5d date: 2020-10-10 22:13:58 --- @@ -563,7 +563,7 @@ public int getUnarrivedParties() 根据上面的代码,我们可以画出下面这个很简单的图: - + 这棵树上有 7 个 phaser 实例,每个 phaser 实例在构造的时候,都指定了 parties 为 5,但是,对于每个拥有子节点的节点来说,每个子节点都是它的一个 party,我们可以通过 phaser.getRegisteredParties() 得到每个节点的 parties 数量: @@ -952,7 +952,7 @@ public class ThreadPoolDemo { ## 线程池的底层工作流程 - + 1、创建线程池后,等待请求任务 @@ -1646,7 +1646,7 @@ public class ExecutorCompletionService implements CompletionService { **执行流程:** - + diff --git a/docs/java_concurrency/Java并发体系-第二阶段-锁与同步-[1].md b/docs/java_concurrency/Java并发体系-第二阶段-锁与同步-[1].md index 6c57573..ce0eaae 100644 --- a/docs/java_concurrency/Java并发体系-第二阶段-锁与同步-[1].md +++ b/docs/java_concurrency/Java并发体系-第二阶段-锁与同步-[1].md @@ -9,7 +9,7 @@ categories: - 原理 keywords: Java并发,原理,源码 description: '万字系列长文讲解-Java并发体系-第二阶段,从C++和硬件方面讲解。' -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/Java_concurrency.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/Java_concurrency.png' abbrlink: 230c5bb3 date: 2020-10-06 22:09:58 --- @@ -291,7 +291,7 @@ d = e - f ; 由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。 - + @@ -343,7 +343,7 @@ int c = a + b; 冯诺依曼,提出计算机由五大组成部分,输入设备,输出设备存储器,控制器,运算器。 - + 输入设备:鼠标,键盘等等 @@ -367,7 +367,7 @@ int c = a + b; CPU的运算速度和内存的访问速度相差比较大。这就导致CPU每次操作内存都要耗费很多等待时间。内 存的读写速度成为了计算机运行的瓶颈。于是就有了在CPU和主内存之间增加缓存的设计。靠近CPU 的缓存称为L1,然后依次是 L2,L3和主内存,CPU缓存模型如图下图所示。 - + CPU Cache分成了三个级别: L1, L2, L3。级别越小越接近CPU,速度也更快,同时也代表着容量越小。速度越快的价格越贵。 @@ -377,7 +377,7 @@ CPU Cache分成了三个级别: L1, L2, L3。级别越小越接近CPU,速 3、L3 Cache是三级缓存中大的一级,例如12MB,同时也是缓存中慢的一级,在同一个CPU插槽 之间的核共享一个L3 Cache。 - + 上面的图中有一个Latency指标。比如Memory这个指标为59.4ns,表示CPU在操作内存的时候有59.4ns的延迟,一级缓存最快只有1.2ns。 @@ -409,7 +409,7 @@ Cache的出现是为了解决CPU直接访问内存效率低下问题的。 每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操 作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接 访问对方工作内存中的变量。 - + Java的线程不能直接在主内存中操作共享变量。而是首先将主内存中的共享变量赋值到自己的工作内存中,再进行操作,操作完成之后,刷回主内存。 @@ -424,7 +424,7 @@ Java内存模型是一套在多线程读写共享数据时,对共享数据的 JMM内存模型与CPU硬件内存架构的关系: - + 工作内存:可能对应CPU寄存器,也可能对应CPU缓存,也可能对应内存。 @@ -434,9 +434,9 @@ JMM内存模型与CPU硬件内存架构的关系: ## 再谈可见性 - + - + 1、图中所示是 个双核 CPU 系统架构 ,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。每个核都有自己的1级缓存,在有些架构里面还有1个所有 CPU 共享的2级缓存。 那么 Java 内存模型里面的工作内存,就对应这里的 Ll 或者 L2 存或者 CPU 寄存器。 @@ -452,7 +452,7 @@ JMM内存模型与CPU硬件内存架构的关系: 为了保证数据交互时数据的正确性,Java内存模型中定义了8种操作来完成这个交互过程,这8种操作本身都是原子性的。虚拟机实现时必须保证下面 提及的每一种操作都是原子的、不可再分的。 - + > (1)lock:作用于主内存的变量,它把一个变量标识为一条线程独占的状态。 > @@ -478,7 +478,7 @@ JMM内存模型与CPU硬件内存架构的关系: 如果没有synchronized,那就是下面这样的 - + @@ -589,7 +589,7 @@ volatile不保证原子性,只保证可见性和禁止指令重排 ## CPU术语介绍 - + @@ -717,7 +717,7 @@ public class VolatileExample { **1、下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图** - + > 图中的StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任 意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。这里比较有意思的是,volatile写后面的StoreLoad屏障。此屏障的作用是避免volatile写与 后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面 是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)。为了保证能正确 实现volatile的内存语义,JMM在采取了保守策略:在每个volatile写的后面,或者在每个volatile 读的前面插入一个StoreLoad屏障。从整体执行效率的角度考虑,JMM最终选择了在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见使用模式是:一个 写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时, 选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里可以看到JMM在实现上的一个特点:首先确保正确性,然后再去追求执行效率 @@ -725,7 +725,7 @@ public class VolatileExample { **2、下面是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图** - + > 图中的LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。 LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。 上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。 @@ -752,7 +752,7 @@ class VolatileBarrierExample { 针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化 - + ​ 注意,最后的StoreLoad屏障不能省略。因为第二个volatile写之后,方法立即return。此时编译器可能无法准确断定后面是否会有volatile读或写,为了安全起见,编译器通常会在这里插入一个StoreLoad屏障。 @@ -766,7 +766,7 @@ class VolatileBarrierExample { X86处理器仅会对写-读操作做重排序。X86不会对读-读、读-写和写-写操作 做重排序,因此在X86处理器中会省略掉这3种操作类型对应的内存屏障。在X86中,JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义。这意味着在X86处理器中,volatile写的开销比volatile读的开销会大很多(因为执行StoreLoad屏障开销会比较大)。 - + diff --git a/docs/java_concurrency/Java并发体系-第二阶段-锁与同步-[2].md b/docs/java_concurrency/Java并发体系-第二阶段-锁与同步-[2].md index 8a81ccc..e5921e9 100644 --- a/docs/java_concurrency/Java并发体系-第二阶段-锁与同步-[2].md +++ b/docs/java_concurrency/Java并发体系-第二阶段-锁与同步-[2].md @@ -9,7 +9,7 @@ categories: - 原理 keywords: Java并发,原理,源码 description: '万字系列长文讲解-Java并发体系-第二阶段,从C++和硬件方面讲解。' -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/Java_concurrency.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/Java_concurrency.png' abbrlink: '8210870' date: 2020-10-07 22:10:58 --- @@ -18,7 +18,7 @@ date: 2020-10-07 22:10:58 # 可见性设计的硬件 - + 从硬件的级别来考虑一下可见性的问题 @@ -305,13 +305,13 @@ MESI协议规定了一组消息,就说各个处理器在操作内存数据的 - + ## MESI-优化 - + ​ MESI协议如果每次写数据的时候都要发送invalidate消息等待所有处理器返回ack,然后获取独占锁后才能写数据,那可能就会导致性能很差了,因为这个对共享变量的写操作,实际上在硬件级别变成串行的了。所以为了解决这个问题,硬件层面引入了写缓冲器和无效队列 @@ -449,7 +449,7 @@ int b = c; //load ## 相关术语 - + @@ -461,7 +461,7 @@ int b = c; //load **第一个机制是通过总线锁保证原子性。**如果多个处理器同时对共享变量进行读改写操作 (i++就是经典的读改写操作),那么共享变量就会被多个处理器同时进行操作,这样读改写操 作就不是原子的,操作完之后共享变量的值会和期望的不一致。举个例子,如果i=1,我们进行 两次i++操作,我们期望的结果是3,但是有可能结果是2,如图所示。 - + ​ 原因可能是多个处理器同时从各自的缓存中读取变量i,分别进行加1操作,然后分别写入 系统内存中。那么,想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享 变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。 diff --git a/docs/java_concurrency/Java并发体系-第二阶段-锁与同步-[3].md b/docs/java_concurrency/Java并发体系-第二阶段-锁与同步-[3].md index 83ab3fc..f829930 100644 --- a/docs/java_concurrency/Java并发体系-第二阶段-锁与同步-[3].md +++ b/docs/java_concurrency/Java并发体系-第二阶段-锁与同步-[3].md @@ -9,7 +9,7 @@ categories: - 原理 keywords: Java并发,原理,源码 description: '万字系列长文讲解-Java并发体系-第二阶段,从C++和硬件方面讲解。' -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/Java_concurrency.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/Java_concurrency.png' abbrlink: 113a3931 date: 2020-10-08 22:10:58 --- @@ -357,17 +357,17 @@ monitorexit释放锁。 monitorexit插入在方法结束处和异常处,JVM保 术语参考: http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html 在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示: - + ## 对象头 当一个线程尝试访问synchronized修饰的代码块时,它首先要获得锁,那么这个锁到底存在哪里呢?是 存在锁对象的对象头中的。 HotSpot采用instanceOopDesc和arrayOopDesc来描述对象头,arrayOopDesc对象用来描述数组类型。instanceOopDesc的定义的在Hotspot源码的 instanceOop.hpp 文件中,另外,arrayOopDesc 的定义对应 arrayOop.hpp - + 从instanceOopDesc代码中可以看到 instanceOopDesc继承自oopDesc,oopDesc的定义载Hotspot 源码中的 oop.hpp 文件中。 - + - 在普通实例对象中,oopDesc的定义包含两个成员,分别是 _mark 和 _metadata @@ -383,21 +383,21 @@ monitorexit释放锁。 monitorexit插入在方法结束处和异常处,JVM保 Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、 线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。Mark Word对应的类 型是 markOop 。源码位于 markOop.hpp 中。 - + - + 在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下: - + 在32位虚拟机下,Mark Word是32bit大小的,其存储结构如下: - + 再加一个图对比一下,有一丁点的补充 - + @@ -447,7 +447,7 @@ Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode) 线程在执行同步块之前,JVM会先在当前的线程的栈帧中创建一个`Lock Record`,其包括一个用于存储对象头中的 `mark word`(官方称之为`Displaced Mark Word`)以及一个指向对象的指针。下图右边的部分就是一个`Lock Record`。 - + @@ -571,7 +571,7 @@ synchronized(obj){ } ``` - + ## 轻量级锁什么时候升级为重量级锁? @@ -608,7 +608,7 @@ synchronized(obj){ - 重量级锁的状态下,对象的`mark word`为指向一个堆中monitor对象的指针。一个monitor对象包括这么几个关键字段:cxq(下图中的ContentionList),EntryList ,WaitSet,owner。其中cxq ,EntryList ,WaitSet都是由ObjectWaiter的链表结构,owner指向持有锁的线程。 - + 在HotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c++来实现的,位于HotSpot虚 拟机源码ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp)。ObjectMonitor主 要数据结构如下: @@ -661,7 +661,7 @@ ObjectMonitor() { 1、执行monitorenter时,会调用InterpreterRuntime.cpp (位于:src/share/vm/interpreter/interpreterRuntime.cpp) 的 InterpreterRuntime::monitorenter函 数。具体代码可参见HotSpot源码。 - + @@ -1147,7 +1147,7 @@ if (TryLock(Self) > 0) break ; 可以看到ObjectMonitor的函数调用中会涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数, 执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这个时候就 会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。所以synchronized是Java语 言中是一个重量级(Heavyweight)的操作。 用户态和和内核态是什么东西呢?要想了解用户态和内核态还需要先了解一下Linux系统的体系架构: - + 从上图可以看出,Linux操作系统的体系架构分为:用户空间(应用程序的活动空间)和内核。 内核:本质上可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境。 用户空间:上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源,包括CPU资源、存 储资源、I/O资源等。 系统调用:为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。 diff --git a/docs/java_concurrency/Java并发体系-第四阶段-AQS源码解读-[1].md b/docs/java_concurrency/Java并发体系-第四阶段-AQS源码解读-[1].md index 50b6211..7aaaa85 100644 --- a/docs/java_concurrency/Java并发体系-第四阶段-AQS源码解读-[1].md +++ b/docs/java_concurrency/Java并发体系-第四阶段-AQS源码解读-[1].md @@ -8,7 +8,7 @@ categories: - 原理 keywords: Java并发,AQS源码 description: '万字系列长文讲解-Java并发体系-第四阶段-AQS源码解读-[1]。' -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/Java_concurrency.png' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/Java_concurrency.png' abbrlink: 92c4503d date: 2020-10-26 17:59:42 --- @@ -410,11 +410,11 @@ Process finished with exit code 0 **技术翻译:**是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量`state`表示持有锁的状态。 - + - + AbstractOwnableSynchronizer AbstractQueuedLongSynchronizer @@ -426,7 +426,7 @@ AbstractQueuedSynchronizer AQS是一个抽象的父类,可以将其理解为一个框架。基于AQS这个框架,我们可以实现多种同步器,比如下方图中的几个Java内置的同步器。同时我们也可以基于AQS框架实现我们自己的同步器以满足不同的业务场景需求。 - + @@ -434,7 +434,7 @@ AQS是一个抽象的父类,可以将其理解为一个框架。基于AQS这 加锁会导致阻塞:有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理 - + 1、抢到资源的线程直接使用办理业务,抢占不到资源的线程的必然涉及一种**排队等候机制**,抢占资源失败的线程继续去等待(类似办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。 @@ -515,7 +515,7 @@ Node 的数据结构其实也挺简单的,就是 thread + waitStatus + pre + n ## AQS队列基本结构 - + 注意排队队列,不包括head(也就是后文要说的哨兵节点)。 @@ -583,7 +583,7 @@ public class AQSDemo { 以这样的一个实际例子说明。 - + @@ -765,7 +765,7 @@ public class AQSDemo { 2、C在if逻辑里准备入队,进行相应设置后,变成下面这样。 - + @@ -831,7 +831,7 @@ public class AQSDemo { 此时队列变成了下面的样子: - + 3、然后if结束之后,继续空的for循环,B线程开始了第二轮循环。 @@ -843,11 +843,11 @@ public class AQSDemo { 2、`node.prev = t`,进入if之后,让B节点的prev指针指向t,然后`compareAndSetTail(t, node)`设置尾节点 - + 3、CAS设置尾节点成功之后,执行if里的逻辑 - + @@ -1197,7 +1197,7 @@ protected final void setExclusiveOwnerThread(Thread thread) { - + diff --git a/docs/netty/Netty入门-第一话.md b/docs/netty/Netty入门-第一话.md index ff64bb3..2db351a 100644 --- a/docs/netty/Netty入门-第一话.md +++ b/docs/netty/Netty入门-第一话.md @@ -7,7 +7,7 @@ categories: - 入门 keywords: Netty description: 第一话对BIO和NIO进行了讲解,为后续做准备。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/netty_logo.jpg' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/netty_logo.jpg' abbrlink: 3f9283e7 date: 2021-04-08 14:21:58 --- @@ -33,7 +33,7 @@ date: 2021-04-08 14:21:58 相对简单的一个体系图 - + ## Netty 的应用场景 @@ -67,7 +67,7 @@ date: 2021-04-08 14:21:58 ## Netty 的学习资料参考 - + @@ -97,11 +97,11 @@ date: 2021-04-08 14:21:58 2. `Java` 共支持 `3` 种网络编程模型 `I/O` 模式:`BIO`、`NIO`、`AIO`。 3. `Java BIO`:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。【简单示意图】 - + 4. `Java NIO`:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 `I/O` 请求就进行处理。【简单示意图】 - + 5. `Java AIO(NIO.2)`:异步非阻塞,`AIO` 引入异步通道的概念,采用了 `Proactor` 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。 6. 我们依次展开讲解。 @@ -120,7 +120,7 @@ date: 2021-04-08 14:21:58 ## Java BIO 工作机制 - + 对 `BIO` 编程流程的梳理 @@ -207,7 +207,7 @@ public class BIOServer { } ``` - + @@ -277,7 +277,7 @@ public class BasicBuffer { 3. `BIO` 基于字节流和字符流进行操作,而 `NIO` 基于 `Channel`(通道)和 `Buffer`(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。`Selector`(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。 4. Buffer和Channel之间的数据流向是双向的 - + ## NIO 三大核心原理示意图 @@ -287,7 +287,7 @@ public class BasicBuffer { 关系图的说明: - + 1. 每个 `Channel` 都会对应一个 `Buffer`。 2. `Selector` 对应一个线程,一个线程对应多个 `Channel`(连接)。 @@ -303,17 +303,17 @@ public class BasicBuffer { 缓冲区(`Buffer`):缓冲区本质上是一个**可以读写数据的内存块**,可以理解成是一个**容器对象(含数组)**,该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。`Channel` 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 `Buffer`,如图:【后面举例说明】 - + ### Buffer 类及其子类 1. 在 `NIO` 中,`Buffer` 是一个顶层父类,它是一个抽象类,类的层级关系图: - + 2. `Buffer` 类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息: - + @@ -321,13 +321,13 @@ public class BasicBuffer { 3. `Buffer` 类相关方法一览 - + ### ByteBuffer 从前面可以看出对于 `Java` 中的基本数据类型(`boolean` 除外),都有一个 `Buffer` 类型与之相对应,最常用的自然是 `ByteBuffer` 类(二进制数据),该类的主要方法如下: - + ## 通道(Channel) @@ -343,7 +343,7 @@ public class BasicBuffer { 5. `FileChannel` 用于文件的数据读写,`DatagramChannel` 用于 `UDP` 的数据读写,`ServerSocketChannel` 和 `SocketChannel` 用于 `TCP` 的数据读写。 6. 图示 - + ### FileChannel 类 @@ -444,7 +444,7 @@ public class NIOFileChannel02 { 2. 拷贝一个文本文件 `1.txt`,放在项目下即可 3. 代码演示 - + ```java package com.atguigu.nio; @@ -721,7 +721,7 @@ public class ScatteringAndGatheringTest { ### Selector 示意图和特点说明 - + 说明如下: @@ -733,7 +733,7 @@ public class ScatteringAndGatheringTest { ### Selector 类相关方法 - + ### 注意事项 @@ -748,7 +748,7 @@ public class ScatteringAndGatheringTest { `NIO` 非阻塞网络编程相关的(`Selector`、`SelectionKey`、`ServerScoketChannel` 和 `SocketChannel`)关系梳理图 - + 对上图的说明: @@ -938,21 +938,21 @@ public static final int OP_ACCEPT = 1 << 4; 2. `SelectionKey` 相关方法 - + ### ServerSocketChannel 1. `ServerSocketChannel` 在服务器端监听新的客户端 `Socket` 连接,负责监听,不负责实际的读写操作 2. 相关方法如下 - + ### SocketChannel 1. `SocketChannel`,网络 `IO` 通道,**具体负责进行读写操作**。`NIO` 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。 2. 相关方法如下 - + ## NIO网络编程应用实例 - 群聊系统 @@ -965,7 +965,7 @@ public static final int OP_ACCEPT = 1 << 4; 5. 目的:进一步理解 `NIO` 非阻塞网络编程机制 6. 示意图分析和代码 - + 代码: @@ -1243,7 +1243,7 @@ socket.getOutputStream().write(arr); ### 传统 IO 模型 - + **DMA**:`direct memory access` 直接内存拷贝(不使用 `CPU`) @@ -1252,19 +1252,19 @@ socket.getOutputStream().write(arr); 1. `mmap` 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。如下图 2. `mmap` 示意图 - + ### sendFile 优化 1. `Linux2.1` 版本提供了 `sendFile` 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 `SocketBuffer`,同时,由于和用户态完全无关,就减少了一次上下文切换 2. 示意图和小结 - + 3. 提示:零拷贝从操作系统角度,是没有 `cpu` 拷贝 4. `Linux在2.4` 版本中,做了一些修改,避免了从内核缓冲区拷贝到 `Socketbuffer` 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。具体如下图和小结: - + 5. 这里其实有一次 `cpu` 拷贝 `kernel buffer` -> `socket buffer` 但是,拷贝的信息很少,比如 `lenght`、`offset` 消耗低,可以忽略 diff --git a/docs/netty/Netty入门-第三话.md b/docs/netty/Netty入门-第三话.md index 0bfca27..6aa80f8 100644 --- a/docs/netty/Netty入门-第三话.md +++ b/docs/netty/Netty入门-第三话.md @@ -7,7 +7,7 @@ categories: - 入门 keywords: Netty description: 对前面两话一些迷惑的点进行细说,讲解handler调用机制,TCP粘包,以及用netty写一个十分简单的RPC。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/netty_logo.jpg' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/netty_logo.jpg' abbrlink: 429acc6d date: 2021-04-21 17:38:58 --- @@ -25,7 +25,7 @@ date: 2021-04-21 17:38:58 1. 编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码[示意图] 2. `codec`(编解码器)的组成部分有两个:`decoder`(解码器)和 `encoder`(编码器)。`encoder` 负责把业务数据转换成字节码数据,`decoder` 负责把字节码数据转换成业务数据 - + ## Netty 本身的编码解码的机制和问题分析 @@ -54,7 +54,7 @@ date: 2021-04-21 17:38:58 8. 然后通过 `protoc.exe` 编译器根据 `.proto` 自动生成 `.java` 文件 9. `protobuf` 使用示意图 - + ## Protobuf 快速入门实例 @@ -91,7 +91,7 @@ message Student { //会在 StudentPOJO 外部类生成一个内部类 Student, protoc.exe --java_out=.Student.proto 将生成的 StudentPOJO 放入到项目使用 - + 生成的StudentPOJO代码太长就不贴在这里了 @@ -676,7 +676,7 @@ public class NettyClientHandler extends ChannelInboundHandlerAdapter { 2. `ChannelHandler` 充当了处理入站和出站数据的应用程序逻辑的容器。例如,实现 `ChannelInboundHandler` 接口(或 `ChannelInboundHandlerAdapter`),你就可以接收入站事件和数据,这些数据会被业务逻辑处理。当要给客户端发送响应时,也可以从 `ChannelInboundHandler` 冲刷数据。业务逻辑通常写在一个或者多个 `ChannelInboundHandler` 中。`ChannelOutboundHandler` 原理一样,只不过它是用来处理出站数据的 3. `ChannelPipeline` 提供了 `ChannelHandler` 链的容器。以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过 `pipeline` 中的一系列 `ChannelOutboundHandler`,并被这些 `Handler` 处理,反之则称为入站的 - + > 出站,入站如果搞不清楚,看下面的**Netty的handler链的调用机制**,通过一个例子和图讲清楚 @@ -689,12 +689,12 @@ public class NettyClientHandler extends ChannelInboundHandlerAdapter { 1. 关系继承图 - + 2. 由于不可能知道远程节点是否会一次性发送一个完整的信息,`tcp` 有可能出现粘包拆包的问题,这个类会对入站数据进行缓冲,直到它准备好被处理.【后面有说TCP的粘包和拆包问题】 3. 一个关于 `ByteToMessageDecoder` 实例分析 - + @@ -710,7 +710,7 @@ public class NettyClientHandler extends ChannelInboundHandlerAdapter { > 读者可以看下这个图,带着这个图去看下面的例子。 - + @@ -978,11 +978,11 @@ public class MyLongToByteEncoder extends MessageToByteEncoder { ### 效果 - + - + @@ -1002,14 +1002,10 @@ public class MyLongToByteEncoder extends MessageToByteEncoder { ​ - + -> 下面是Netty官方源码给的图,我个人觉的不是太好理解,上面的图好理解一些 - -![](image/chapter08_05.png) - ## ByteToMessageDecoder的小细节 @@ -1090,13 +1086,13 @@ public class MyByteToLongDecoder extends ByteToMessageDecoder { 如下图验证结果: - + 2. 同时又引出了一个小问题 - + 当我们`MyClientHandler`传一个Long时,会调用我们的`MyLongToByteEncoder`的编码器。那么控制台就会打印这样一句话:**MyLongToByteEncoder encode 被调用**。但是这里并没有调用编码器,这是为什么呢? @@ -1149,7 +1145,7 @@ public class MyByteToLongDecoder extends ByteToMessageDecoder { ctx.writeAndFlush(Unpooled.copiedBuffer("abcdabcdabcdabcd",CharsetUtil.UTF_8)); ``` - + @@ -1198,7 +1194,7 @@ public class MyByteToLongDecoder2 extends ReplayingDecoder { ## 其它编解码器 - + @@ -1249,7 +1245,7 @@ log4j.appender.stdout.layout.ConversionPattern=[%p]%C{1}-%m%n 3. 演示整合 - + @@ -1261,7 +1257,7 @@ log4j.appender.stdout.layout.ConversionPattern=[%p]%C{1}-%m%n 2. 由于 `TCP` 无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题,看一张图 3. `TCP` 粘包、拆包图解 - + 假设客户端分别发送了两个数据包 `D1` 和 `D2` 给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况: @@ -1502,11 +1498,11 @@ public class MyClientHandler extends SimpleChannelInboundHandler { **Client** - + **Server** - + @@ -1514,13 +1510,13 @@ public class MyClientHandler extends SimpleChannelInboundHandler { **Client** - + **Server** - + @@ -1538,7 +1534,7 @@ public class MyClientHandler extends SimpleChannelInboundHandler { 1. 要求客户端发送 `5` 个 `Message` 对象,客户端每次发送一个 `Message` 对象 2. 服务器端每次接收一个 `Message`,分 `5` 次进行解码,每读取到一个 `Message`,会回复一个 `Message` 对象给客户端。 - + @@ -1996,7 +1992,7 @@ MyMessageEncoder encode 方法被调用 1. `RPC(Remote Procedure Call)`—远程过程调用,是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程 2. 两个或多个应用程序都分布在不同的服务器上,它们之间的调用都像是本地方法调用一样(如图) - + 过程: @@ -2018,11 +2014,11 @@ MyMessageEncoder encode 方法被调用 3. 常见的 `RPC` 框架有:比较知名的如阿里的 `Dubbo`、`Google` 的 `gRPC`、`Go` 语言的 `rpcx`、`Apache` 的 `thrift`,`Spring` 旗下的 `SpringCloud`。 - + ## 我们的RPC 调用流程图 - + **RPC 调用流程说明** @@ -2052,7 +2048,7 @@ MyMessageEncoder encode 方法被调用 3. 创建一个消费者,该类需要透明的调用自己不存在的方法,内部需要使用 `Netty` 请求提供者返回数据 4. 开发的分析图 - + diff --git a/docs/netty/Netty入门-第二话.md b/docs/netty/Netty入门-第二话.md index b2add4f..d5a88db 100644 --- a/docs/netty/Netty入门-第二话.md +++ b/docs/netty/Netty入门-第二话.md @@ -7,7 +7,7 @@ categories: - 入门 keywords: Netty description: 对Netty的架构进行了解析,主要是Reactor设计模式的多种解决方案。同时讲解了Netty的核心模块组件。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/netty_logo.jpg' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/netty_logo.jpg' abbrlink: f846f3f date: 2021-04-15 14:21:58 --- @@ -29,7 +29,7 @@ date: 2021-04-15 14:21:58 Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. - + ## Netty 的优点 @@ -80,7 +80,7 @@ Netty is an asynchronous event-driven network application framework for rapid de 1. 当并发数很大,就会创建大量的线程,占用很大系统资源 2. 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在 Handler对象中的`read` 操作,导致上面的处理线程资源浪费 - + ## Reactor 模式 @@ -97,7 +97,7 @@ Netty is an asynchronous event-driven network application framework for rapid de 1. 基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务。(解决了当并发数很大时,会创建大量线程,占用很大系统资源) 2. 基于 `I/O` 复用模型:多个客户端进行连接,先把连接请求给`ServiceHandler`。多个连接共用一个阻塞对象`ServiceHandler`。假设,当C1连接没有数据要处理时,C1客户端只需要阻塞于`ServiceHandler`,C1之前的处理线程便可以处理其他有数据的连接,不会造成线程资源的浪费。当C1连接再次有数据时,`ServiceHandler`根据线程池的空闲状态,将请求分发给空闲的线程来处理C1连接的任务。(解决了线程资源浪费的那个问题) - + @@ -105,7 +105,7 @@ Netty is an asynchronous event-driven network application framework for rapid de ### I/O 复用结合线程池,就是 Reactor 模式基本设计思想,如图 - + 对上图说明: @@ -132,7 +132,7 @@ Netty is an asynchronous event-driven network application framework for rapid de 原理图,并使用 `NIO` 群聊系统验证 - + ### 方案说明 @@ -155,7 +155,7 @@ Netty is an asynchronous event-driven network application framework for rapid de ### 方案说明 - + @@ -178,11 +178,11 @@ Netty is an asynchronous event-driven network application framework for rapid de 针对单 `Reactor` 多线程模型中,`Reactor` 在单线程中运行,高并发场景下容易成为性能瓶颈,可以让 `Reactor` 在多线程中运行 - + - + > SubReactor是可以有多个的,如果只有一个SubReactor的话那和`单 Reactor 多线程`就没什么区别了。 @@ -197,7 +197,7 @@ Netty is an asynchronous event-driven network application framework for rapid de ### Scalable IO in Java 对 Multiple Reactors 的原理图解 - + ### 方案优缺点说明 @@ -230,7 +230,7 @@ Netty is an asynchronous event-driven network application framework for rapid de `Netty` 主要基于主从 `Reactors` 多线程模型(如图)做了一定的改进,其中主从 `Reactor` 多线程模型有多个 `Reactor` - + **对上图说明** @@ -240,7 +240,7 @@ Netty is an asynchronous event-driven network application framework for rapid de ### 工作原理示意图2 - 进阶版 - + @@ -248,7 +248,7 @@ Netty is an asynchronous event-driven network application framework for rapid de ### 工作原理示意图3 - 详细版 - + @@ -669,9 +669,9 @@ public class NettyServerHandler extends ChannelInboundHandlerAdapter { 下面第一张图就是管道,中间会经过多个handler - + - + 说明: @@ -917,11 +917,11 @@ public class TestHttpServerHandler extends SimpleChannelInboundHandler + 4. 我们经常需要自定义一个 `Handler` 类去继承 `ChannelInboundHandlerAdapter`,然后通过重写相应方法实现业务逻辑,我们接下来看看一般都需要重写哪些方法 - + ## Pipeline 和 ChannelPipeline @@ -931,7 +931,7 @@ public class TestHttpServerHandler extends SimpleChannelInboundHandler + 4. 常用方法 `ChannelPipeline addFirst(ChannelHandler... handlers)`,把一个业务处理类(`handler`)添加到链中的第一个位置`ChannelPipeline addLast(ChannelHandler... handlers)`,把一个业务处理类(`handler`)添加到链中的最后一个位置 @@ -940,7 +940,7 @@ public class TestHttpServerHandler extends SimpleChannelInboundHandler + - `TestServerInitializer`和`HttpServerCodec`这些东西本身也是`handler` - 一般来说事件从客户端往服务器走我们称为出站,反之则是入站。 @@ -950,7 +950,7 @@ public class TestHttpServerHandler extends SimpleChannelInboundHandler + 3. 常用方法 @@ -959,14 +959,14 @@ public class TestHttpServerHandler extends SimpleChannelInboundHandler + ## ChannelOption 1. `Netty` 在创建 `Channel` 实例后,一般都需要设置 `ChannelOption` 参数。 2. `ChannelOption` 参数如下: - + ## EventLoopGroup 和其实现类 NioEventLoopGroup @@ -974,7 +974,7 @@ public class TestHttpServerHandler extends SimpleChannelInboundHandler + 4. 常用方法 `public NioEventLoopGroup()`,构造方法 @@ -985,11 +985,11 @@ public class TestHttpServerHandler extends SimpleChannelInboundHandler + 3. 举例说明 `Unpooled` 获取 `Netty` 的数据容器 `ByteBuf` 的基本使用 - + 案例 1 @@ -1096,7 +1096,7 @@ public class NettyByteBuf02 { - + 代码如下: @@ -1523,7 +1523,7 @@ public class MyServerHandler extends ChannelInboundHandlerAdapter { 4. 客户端浏览器和服务器端会相互感知,比如服务器关闭了,浏览器会感知,同样浏览器关闭了,服务器会感知 5. 运行界面 - + @@ -1724,7 +1724,7 @@ public class MyTextWebSocketFrameHandler extends SimpleChannelInboundHandler + diff --git a/docs/os/操作系统-IO与零拷贝.md b/docs/os/操作系统-IO与零拷贝.md index a64bcda..a5b4d27 100644 --- a/docs/os/操作系统-IO与零拷贝.md +++ b/docs/os/操作系统-IO与零拷贝.md @@ -9,7 +9,7 @@ categories: - 操作系统 keywords: 操作系统,IO,零拷贝 description: 基本面试会问到的IO进行了详解,同时本篇文章也对面试以及平时工作中会看到的零拷贝进行了充分的解析。万字长文系列,读到就是赚到。 -cover: 'https://npm.elemecdn.com/lql_static@latest/logo/os_logo.jpg' +cover: 'https://upyunimg.imlql.cn/lql_static@latest/logo/os_logo.jpg' abbrlink: e959db2e date: 2021-04-08 15:21:58 --- @@ -47,7 +47,7 @@ date: 2021-04-08 15:21:58 3. 而在用户进程这边,整 个进程会被阻塞。当**内核**一直等到数据准备好了,它就会将数据从**内核**中拷贝到用户内存,然后**内核**返回果,用户进程才解除 block的状态,重新运行起来。 4. **所以,blocking IO的特点就是在IO执行的两个阶段都被block了。** - + @@ -58,7 +58,7 @@ date: 2021-04-08 15:21:58 3. 虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。 4. **所以,用户进程第一个阶段不是阻塞的,需要不断的主动询问内核数据好了没有;第二个阶段依然总是阻塞的。** - + @@ -76,7 +76,7 @@ date: 2021-04-08 15:21:58 2. 它的基本原理就是select /epoll这个函数会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程,正式发起read请求。 3. 从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket(也就是数据准备好了的socket),即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。 - + **select函数** @@ -90,7 +90,7 @@ date: 2021-04-08 15:21:58 4. 通过Reactor的方式,可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时(就是数据准备好的时候),则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。 5. 由于select函数是阻塞的,因此多路IO复用模型也被称为异步阻塞IO模型。注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。(一般在使用IO多路复用模型时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起IO请求时,数据已经到达了,用户线程一定不会被阻塞。) - + @@ -106,7 +106,7 @@ date: 2021-04-08 15:21:58 3. 而另一方面,从**内核**的角度,当它受到一个异步读之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都 完成之后,**内核**会给用户进程发送一个信号,告诉它read操作完成了,用户线程直接使用即可。 在这整个过程中,进程完全没有被阻塞。 4. 异步IO模型使用了Proactor设计模式实现了这一机制。**(具体怎么搞得,看上面的文章链接)** - + @@ -196,7 +196,7 @@ date: 2021-04-08 15:21:58 1. 来看一个标准设备(不是真实存在的,相当于一个逻辑上抽象的东西),通过它来帮助我们更好地理解设备交互的机制。可以看到一个包含两部分重要组件的设备。第一部分是向系统其他部分展现的硬件接口(interface)。同软件一样,硬件也需要一些接口,让系统软件来控制它的操作。因此,所有设备都有自己的特定接口以及典型交互的协议。 - + 2. 第2部分是它的内部结构(internal structure)。这部分包含设备相关的特定实现,负责具体实现设备展示给系统的抽象接口。 @@ -231,11 +231,11 @@ While (STATUS == BUSY);//wait until device is done with your request 1. 没有中断时:进程1在CPU上运行一段时间(对应CPU那一行上重复的1),然后发出一个读取数据的I/O请求给磁盘。如果没有中断,那么操作系统就会简单自旋,不断轮询设备状态,直到设备完成I/O操作(对应其中的p)。当设备完成请求的操作后,进程1又可以继续运行。 - + 2. 有了中断后:中断允许计算与I/O重叠(overlap),这是提高CPU利用率的关键。我们利用中断并允许重叠,操作系统就可以在等待磁盘操作时做其他事情。 - + - 在这个例子中,在磁盘处理进程1的请求时,操作系统在CPU上运行进程2。磁盘处理完成后,触发一个中断,然后操作系统唤醒进程1继续运行。这样,在这段时间,无论CPU还是磁盘都可以有效地利用。 @@ -243,7 +243,7 @@ While (STATUS == BUSY);//wait until device is done with your request 中断仍旧存在的缺点: - + IO过程简述: @@ -268,7 +268,7 @@ IO过程简述: > > 标准协议还有一点需要我们注意。具体来说,如果使用编程的I/O将一大块数据传给设备,CPU又会因为琐碎的任务而变得负载很重,浪费了时间和算力,本来更好是用于运行其他进程。下面的时间线展示了这个问题: > -> +> > > 进程1在运行过程中需要向磁盘写一些数据,所以它开始进行I/O操作,将数据从内存拷贝到磁盘(其中标示c的过程)。**拷贝结束后,磁盘上的I/O操作开始执行,此时CPU才可以处理其他请求。** @@ -278,13 +278,13 @@ IO过程简述: > > DMA工作过程如下。为了能够将数据传送给设备,操作系统会通过编程告诉DMA引擎数据在内存的位置,要拷贝的大小以及要拷贝到哪个设备。在此之后,操作系统就可以处理其他请求了。当DMA的任务完成后,DMA控制器会抛出一个中断来告诉操作系统自己已经完成数据传输。修改后的时间线如下: > -> +> > > 从时间线中可以看到,数据的拷贝工作都是由DMA控制器来完成的。因为CPU在此时是空闲的,所以操作系统可以让它做一些其他事情,比如此处调度进程2到CPU来运行。因此进程2在进程1再次运行之前可以使用更多的CPU。 为了更好理解,看图: - + 过程: @@ -302,7 +302,7 @@ IO过程简述: 场景:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。 - + 1. 很明显发生了4次拷贝 @@ -326,7 +326,7 @@ IO过程简述: `read()` 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,为了减少这一步开销,我们可以用 `mmap()` 替换 `read()` 系统调用函数。`mmap()` 系统调用函数会直接把内核缓冲区里的数据映射到用户空间,这样,操作系统内核与用户空间共享缓冲区,就不需要再进行任何的数据拷贝操作。 - + 总的来说mmap减少了一次数据拷贝,总共4次上下文切换,3次数据拷贝 @@ -336,7 +336,7 @@ IO过程简述: `Linux2.1` 版本提供了 `sendFile` 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 `SocketBuffer` - + 总的来说有2次上下文切换,3次数据拷贝。 @@ -344,7 +344,7 @@ IO过程简述: `Linux在2.4` 版本中,做了一些修改,避免了从内核缓冲区拷贝到 `Socketbuffer` 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝 - + diff --git a/docs/spring-sourcecode-v1/01.第1章-Spring源码纵览.md b/docs/spring-sourcecode-v1/01.第1章-Spring源码纵览.md index f5f30f0..f732dd4 100644 --- a/docs/spring-sourcecode-v1/01.第1章-Spring源码纵览.md +++ b/docs/spring-sourcecode-v1/01.第1章-Spring源码纵览.md @@ -38,7 +38,7 @@ Spring源码纵览这一节,主要是先了解下Spring的一些核心东西 - + - **蓝色实线箭头**是指继承关系 - **绿色虚线箭头**是指接口实现关系 @@ -91,13 +91,13 @@ BeanPostProcessor 快捷键:ctrl+F12 看类的方法 - + #### 实现类 快捷键:ctrl+h 查看接口实现类 - + @@ -189,7 +189,7 @@ public interface ResourceLoader { #### 实现类 - + @@ -232,7 +232,7 @@ public interface BeanFactory { > 源码分析小技巧:看源码时,我们可以先看一个类的接口继承关系,因为接口就是规范,大部分开源框架源码都是遵守这一规范的。 - + @@ -248,7 +248,7 @@ public interface BeanFactory { #### AbstractApplicationContext - + 环境类的意思就是谁持有这个策略;这里就解释了上文说ResourceLoader是环境类接口 @@ -265,7 +265,7 @@ public interface BeanFactory { #### GenericApplicationContext - + 这里组合了DefaultListableBeanFactory @@ -310,7 +310,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto - + 1. BeanDefinitionRegistry:Bean定义信息注册中心 2. SimpleAliasRegistry:别名注册中心 @@ -336,7 +336,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto #### 流程图-BeanDefinition注册流程 - + - 我们要看BeanDefinition是何时被放入到beanDefinitionMap,只需要在DefaultListableBeanFactory用到`beanDefinitionMap.put()`的地方打个断点。 - 我们在DefaultListableBeanFactory里搜索,发现了registerBeanDefinition(注册Bean定义信息)这个方法名很像我们要找的东西,再看里面的代码,果然有`beanDefinitionMap.put()`这串代码,我们试着在这里打个断点 @@ -361,7 +361,7 @@ public class MainTest { #### Debug调用栈 - + > 调用栈的调用顺序已经非常清楚了,可以把图放大一点看,下面只说一些必要的信息。 @@ -780,7 +780,7 @@ public class MainTest { - 还有一个debug的猜测方向,想要注册BeanDefinition肯定要new,我们可以直接在AbstractBeanDefinition这个抽象父类的构造函数打断点,我们不知道会走哪个构造函数,所以给三个构造函数都打断点。 - 下面就是打完断点之后,运行MainTest测试类后的调用栈 - + @@ -936,7 +936,7 @@ public class MainTest { ### ApplicationContext接口功能 - + 1. ioc事件派发器 2. 国际化解析 @@ -969,7 +969,7 @@ public interface Aware { 2. 注释的大致意思是:Aware是一个标记性的超接口(顶级接口),指示了一个Bean有资格通过回调方法的形式获取Spring容器底层组件。实际回调方法被定义在每一个子接口中,而且通常一个子接口只包含一个接口一个参数并且返回值为void的方法。 3. 说白了:只要实现了Aware子接口的Bean都能获取到一个Spring底层组件。 - + 比如实现了ApplicationContextAware接口,实现它的方法,就能通过回调机制拿到ApplicationContext @@ -979,7 +979,7 @@ public interface Aware { ##### 流程图-Bean对象创建流程 - + ##### Debug调用栈 @@ -1055,7 +1055,7 @@ public class MainConfig { - + 1. 有一些一样的东西不再赘述 2. 在`AbstractApplicationContext#refresh()`里会调用 @@ -1267,7 +1267,7 @@ public class MainConfig { ##### Debug调用栈 - + @@ -1434,7 +1434,7 @@ public class MainTest { - + @@ -1475,7 +1475,7 @@ public class MainTest { 我们看到此时,Person的name属性还是null - + @@ -1497,7 +1497,7 @@ public class MainTest { 这里拿到属性值 - + @@ -1512,13 +1512,13 @@ public class MainTest { } ``` - + bw就是上面说的 => 里面封装好了真正的Person对象 - + @@ -1528,7 +1528,7 @@ bw就是上面说的 => 里面封装好了真正的Person对象 这里就是一层一层调,不重要跳过。 - + @@ -1549,7 +1549,7 @@ private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) - + @@ -1588,7 +1588,7 @@ private class BeanPropertyHandler extends PropertyHandler { - + 最后就是反射走到了我们的`Person#setName(String name)` @@ -1598,7 +1598,7 @@ private class BeanPropertyHandler extends PropertyHandler { ### 再来看看messageSource何时赋值 - + @@ -1742,7 +1742,7 @@ public class Person implements ApplicationContextAware, MessageSourceAware { 老样子,只看不一样的调用栈 - + ### AbstractAutowireCapableBeanFactory#populateBean() @@ -1785,7 +1785,7 @@ public class Person implements ApplicationContextAware, MessageSourceAware { 这里有一个非常著名的后置处理器,`AutowiredAnnotationBeanPostProcessor`自动装配注解后置处理器,顾名思义就是用来处理@Autowired注解自动装配的。 - + diff --git a/docs/spring-sourcecode-v1/02.第2章-后置工厂处理器和Bean生命周期.md b/docs/spring-sourcecode-v1/02.第2章-后置工厂处理器和Bean生命周期.md index d7b9194..3775b8b 100644 --- a/docs/spring-sourcecode-v1/02.第2章-后置工厂处理器和Bean生命周期.md +++ b/docs/spring-sourcecode-v1/02.第2章-后置工厂处理器和Bean生命周期.md @@ -97,7 +97,7 @@ public interface BeanFactoryPostProcessor { } ``` - + 核心就是我们对于传进来的参数,可以**修改,覆盖,添加**它的东西。对于BeanPostProcessor来说,传进来的参数是`(Object bean, String beanName)` ,它都已经把bean传给你了,这意味着我们可以修改传进来的Bean的任何东西。不管你是事务也好,AOP也好,都是通过这些个后置处理器来添加这些额外功能的。 @@ -106,11 +106,11 @@ BeanFactoryPostProcessor:后置增强BeanFactory,也就是增强Bean工厂 ### BeanFactoryPostProcessor的接口关系 - + ### BeanPostProcessor接口关系 - + DestructionAwareBeanPostProcessor接口是跟销毁有关的,我们这里不分析 @@ -391,7 +391,7 @@ public class MainTest { ### 流程图-Bean生命周期与后置工厂处理器 - + ### BeanDefinitionRegistryPostProcessor @@ -399,7 +399,7 @@ public class MainTest { ##### Debug调用栈 - + ##### AbstractApplicationContext#refresh() @@ -615,9 +615,9 @@ public class MainTest { > Spring中所有组件的获取都是通过getBean(),容器中有就拿,没有就创建。 - + - + 下面那个是Spring默认提供的后置处理器,我们后面再讲。 @@ -677,11 +677,11 @@ public interface Ordered { ##### Debug调用栈 - + 从PostProcessorRegistrationDelegate 142行开始走不同的调用,代码在上面有注释 - + ##### PostProcessorRegistrationDelegate#invokeBeanDefinitionRegistryPostProcessors() @@ -706,7 +706,7 @@ private static void invokeBeanDefinitionRegistryPostProcessors( ##### Debug调用栈 - + @@ -733,7 +733,7 @@ private static void invokeBeanFactoryPostProcessors( ##### Debug调用栈 - + @@ -741,7 +741,7 @@ private static void invokeBeanFactoryPostProcessors( ##### Debug调用栈 - + 代码注释也是上面那个,BeanDefinitionRegistryPostProcessor和BeanFactoryPostProcessor执行逻辑基本一样 @@ -779,11 +779,11 @@ public class MainConfig { - + 从这一步进来 - + @@ -791,7 +791,7 @@ public class MainConfig { #### PostProcessorRegistrationDelegate#invokeBeanDefinitionRegistryPostProcessors() - + F7进入 @@ -799,7 +799,7 @@ F7进入 ConfigurationClassPostProcessor配置类的后置处理 - + @@ -914,7 +914,7 @@ ConfigurationClassPostProcessor配置类的后置处理 这几个怎么来的我们后面说 - + diff --git a/docs/spring-sourcecode-v1/03.第3章-后置处理器和Bean生命周期.md b/docs/spring-sourcecode-v1/03.第3章-后置处理器和Bean生命周期.md index c58dc9f..19c41fa 100644 --- a/docs/spring-sourcecode-v1/03.第3章-后置处理器和Bean生命周期.md +++ b/docs/spring-sourcecode-v1/03.第3章-后置处理器和Bean生命周期.md @@ -250,7 +250,7 @@ public class MainTest { ### 流程图-Bean生命周期与后置处理器 - + ### BeanPostProcessor-执行无参构造 @@ -258,7 +258,7 @@ public class MainTest { #### Debug调用栈 - + #### AbstractApplicationContext#registerBeanPostProcessors()注册Bean后置处理器 @@ -345,7 +345,7 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa - Bean工厂后置处理器调用的是`invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory , List )` - Bean后置处理器调用的是`registerBeanPostProcessors(ConfigurableListableBeanFactory , AbstractApplicationContext )` - + @@ -355,7 +355,7 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa > 2. BeanFactoryPostProcessor是先执行完每一个的无参构造和实现的几个方法,再去执行下一个BeanFactoryPostProcessor > 3. BeanPostProcessor是先执行所有BeanPostProcessor的无参构造,再执行所有BeanPostProcessor实现的方法。 - + ### MergedBeanDefinitionPostProcessor-执行无参构造 @@ -365,7 +365,7 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa 同上 - + @@ -375,7 +375,7 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa #### Debug调用栈 - + @@ -542,7 +542,7 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa 3. 判断是不是想要的类型。 3. 这里有没有优化空间,再存一个BeanType=>BeanName的对应关系?但是这样的关系是一对多的,同一个BeanType下可能有多个beanName,Spring可能是考虑到空间成本,没有这样弄。 - + @@ -554,7 +554,7 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa 2. 我们来看看此时单例池里有哪些对象 - + ```java protected boolean isTypeMatch(String name, ResolvableType typeToMatch, boolean allowFactoryBeanInit) @@ -687,13 +687,13 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa 在此方法里为什么Cat会进来两次呢?往后面看 - + ### InstantiationAwareBeanPostProcessor-执行postProcessBeforeInstantiation方法 #### Debug调用栈 - + #### AbstractApplicationContext#finishBeanFactoryInitialization()完成BeanFactory初始化 @@ -746,7 +746,7 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa 2. 上面我们也看了只有Cat的对象还没创建,还没初始化,所以下面就开始创建对象了。 - + @@ -860,7 +860,7 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa #### Debug调用栈 - + > 前面还是一样的执行逻辑,直接来到下面 @@ -1003,7 +1003,7 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa 1. 实例提供者: - + @@ -1025,7 +1025,7 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa ### Cat-执行无参构造方法 - + @@ -1039,7 +1039,7 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa #### Debug调用栈 - + @@ -1061,7 +1061,7 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa #### Debug调用栈 - + #### AbstractAutowireCapableBeanFactory#populateBean()属性赋值 @@ -1141,7 +1141,7 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa } ``` - + @@ -1193,7 +1193,7 @@ public void setValue(@Nullable Object value) throws Exception { //name setName #### Debug调用栈 - + @@ -1201,7 +1201,7 @@ public void setValue(@Nullable Object value) throws Exception { //name setName #### AutowiredAnnotationBeanPostProcessor#postProcessProperties() - + ```java public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) { @@ -1322,7 +1322,7 @@ public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, Str #### Debug调用栈 - + 这也说明了,@Autowire,@Value赋值的时候会去找setXXX,这也是@Autowire的原理 @@ -1350,7 +1350,7 @@ public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, Str #### Debug调用栈 - + @@ -1461,7 +1461,7 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) thro #### Debug调用栈 - + @@ -1513,7 +1513,7 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) thro #### Debug调用栈 - + diff --git a/docs/spring-sourcecode-v1/04.第4章-Bean初始化流程.md b/docs/spring-sourcecode-v1/04.第4章-Bean初始化流程.md index aee5f6c..2a2bb4a 100644 --- a/docs/spring-sourcecode-v1/04.第4章-Bean初始化流程.md +++ b/docs/spring-sourcecode-v1/04.第4章-Bean初始化流程.md @@ -16,7 +16,7 @@ date: 2022-01-27 21:01:02 ## 流程图-bean初始化流程 - + ## AbstractApplicationContext#refresh() @@ -206,7 +206,7 @@ date: 2022-01-27 21:01:02 ## 工厂Bean的初始化方式 - + @@ -313,13 +313,13 @@ public class Hello { 从这里开始看调用链 - + - + @@ -475,13 +475,13 @@ String FACTORY_BEAN_PREFIX = "&"; 最后返回 - + - + @@ -532,7 +532,7 @@ public class Cat implements InitializingBean, SmartInitializingSingleton { ### Debug调用栈 - + diff --git a/docs/spring-sourcecode-v1/05.第5章-容器刷新流程.md b/docs/spring-sourcecode-v1/05.第5章-容器刷新流程.md index 85abcf7..ed20860 100644 --- a/docs/spring-sourcecode-v1/05.第5章-容器刷新流程.md +++ b/docs/spring-sourcecode-v1/05.第5章-容器刷新流程.md @@ -18,7 +18,7 @@ date: 2022-02-13 18:01:02 ## 流程图-容器刷新 - + ## 容器创建 @@ -209,7 +209,7 @@ public class AnnotationMainTest { 上面的方法走完,我们可以看看到主要是下面4个后置处理器 - + ##### RootBeanDefinition @@ -360,7 +360,7 @@ public class AnnotationMainTest { 走完之后,注册中心肯定多了咱们的配置类 - + @@ -584,7 +584,7 @@ protected void initPropertySources() { } ``` - + @@ -607,11 +607,11 @@ protected void initPropertySources() { 这一步有个很关键的后置处理器 - + - + @@ -746,7 +746,7 @@ protected void initPropertySources() { - + @@ -1043,7 +1043,7 @@ public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateCo 拿到所有类(资源),不管你有没有标@Component注解。然后挨个遍历每一个资源是不是候选的组件(根据前面准备的一些条件,在这里进行判断) - + @@ -1051,7 +1051,7 @@ public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateCo 最后我们看一下执行完之后的BeanDefinition信息 - + @@ -1309,7 +1309,7 @@ public class B { 这就是循环引用的场景,这种写法由于Spring内部获取Bean都是通过getBean方法来获取,就造成了下面的死循环。我们来看看Spring是怎么解决的。 - + @@ -1568,16 +1568,16 @@ public class B { 1. pos_1位置先进入pos_3位置的`getSingleton(beanName, true)`,查看缓存中有没有A组件 - + 2. 然后走到pos_2调用pos_5的`getSingleton()`开始创建A的流程 3. 在pos_5的`getSingleton()`中走到pos_6的`beforeSingletonCreation()`,就变成下面这样 - + 4. 接着pos_7的会调用pos_2的lamda表达式里的`createbean()`,里面再调用`doCreateBean()`。前面讲过不多说,最终调用A的无参构造(pos_8),创建完之后发现A的B属性是null。 - + 5.在pos_9处的`addSingletonFactory()`来准备解决循环引用 @@ -1594,7 +1594,7 @@ public class B { } ``` - + @@ -1662,11 +1662,11 @@ public class B { 通过前面讲过的AutowiredAnnotationBeanPostProcessor来注入B,最后发现要调用setB方法给B赋值 - + 7. 继续走,发现要想获得B还是要调用getBean - + ```java public Object resolveCandidate(String beanName, Class requiredType, BeanFactory beanFactory) @@ -1680,13 +1680,13 @@ public class B { 然后图就是这样子 - + 8. B也是走这一套 - + 9. B为了获取A,还要再走一次getBean()流程,最终还是走到 @@ -1720,13 +1720,13 @@ public class B { } ``` - + 10. - + @@ -1739,7 +1739,7 @@ public class B { 12. B初始化完之后,回到getSingleton,把自己放到单例池里 - + ```java protected void addSingleton(String beanName, Object singletonObject) { @@ -1752,15 +1752,15 @@ protected void addSingleton(String beanName, Object singletonObject) { } ``` - + - + 13. B全部结束之后回到A的流程,A赋值工作结束了,然后就开始A的初始化。初始化的过程中 - + ```java protected void addSingleton(String beanName, Object singletonObject) { @@ -1775,7 +1775,7 @@ protected void addSingleton(String beanName, Object singletonObject) { - + @@ -1787,7 +1787,7 @@ protected void addSingleton(String beanName, Object singletonObject) { - + diff --git a/docs/spring-sourcecode-v1/06.第6章-AOP的后置处理器和代理对象的创建.md b/docs/spring-sourcecode-v1/06.第6章-AOP的后置处理器和代理对象的创建.md index 354d990..704e0ff 100644 --- a/docs/spring-sourcecode-v1/06.第6章-AOP的后置处理器和代理对象的创建.md +++ b/docs/spring-sourcecode-v1/06.第6章-AOP的后置处理器和代理对象的创建.md @@ -222,7 +222,7 @@ class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar { ### Debug调用栈 - + ### ImportBeanDefinitionRegistrar @@ -290,7 +290,7 @@ pos_1 Debug进去是下面的方法 ### AnnotationAwareAspectJAutoProxyCreator后置处理器 - + 我们发现这就是一个后置处理器,印证了我们之前说过的几乎所有功能增强或功能附加都是由后置处理器来完成 @@ -318,13 +318,13 @@ pos_1 Debug进去是下面的方法 ##### 作用 - + 1. 由于它是后置处理器所以肯定是在refresh方法里的registerBeanPostProcessors这一步开始干活的 2. 果然是从getBean调用到了我们的重写方法断点处,也就验证了我们上面说怎么分析。绿色包含的前面已经讲过,不再赘述。 3. 也确实是AnnotationAwareAspectJAutoProxyCreator,走到这里说明前面已经创建出了AnnotationAwareAspectJAutoProxyCreator对象(前面怎么创建的,getbean的流程之前已经讲的很清楚了),后面只是对AnnotationAwareAspectJAutoProxyCreator进行初始化 - + ##### AbstractAutowireCapableBeanFactory#initializeBean()进行初始化 @@ -410,7 +410,7 @@ pos_1 Debug进去是下面的方法 } ``` - + ```java public ReflectiveAspectJAdvisorFactory(@Nullable BeanFactory beanFactory) { @@ -483,7 +483,7 @@ public ReflectiveAspectJAdvisorFactory(@Nullable BeanFactory beanFactory) { } ``` - + 1. 我们来看一下是什么时候触发的,看图中的beanName==myBeanPostProcessor时触发的,也就是创建完AnnotationAwareAspectJAutoProxyCreator,第一次创建别的后置处理器时触发的。 2. 这里虽然开始参与其它Bean的创建过程,但也可能是什么都没做。 @@ -610,7 +610,7 @@ protected boolean isInfrastructureClass(Class beanClass) { #### Debug调用栈-调用aspectJAdvisorsBuilder的findCandidateAdvisors()方法 - + AbstractAutoProxyCreator#postProcessBeforeInstantiation()这个方法,上面刚分析过,这次来分析shouldskip() @@ -633,7 +633,7 @@ AbstractAutoProxyCreator#postProcessBeforeInstantiation()这个方法,上面 - + 它进去的时候就是beanName==LogAspect,我们看看它是怎么判断的 @@ -653,7 +653,7 @@ AbstractAutoProxyCreator#postProcessBeforeInstantiation()这个方法,上面 我们这里先放行,看构造了哪些增强器 - + ##### BeanFactoryAspectJAdvisorsBuilder#buildAspectJAdvisors()找到切面,并创建增强器advisors @@ -738,11 +738,11 @@ AbstractAutoProxyCreator#postProcessBeforeInstantiation()这个方法,上面 - + for循环所有组件之后,有切面的话,就赋值,然后aspectNames - + #### 结论 @@ -939,7 +939,7 @@ AbstractAutoProxyCreator - + @@ -981,21 +981,21 @@ AbstractAutoProxyCreator - + 获取父类的方法 - + 然后回到这里 - + 最后回到这个for循环,循环所有的方法 - + #### ReflectiveAspectJAdvisorFactory#getAdvisor() @@ -1126,7 +1126,7 @@ AbstractAutoProxyCreator 到了初始化这里了,说明前面无参构造创建myBeanPostProcessor对象已经完成了 - + #### AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors() @@ -1148,7 +1148,7 @@ protected List findCandidateAdvisors() { 我们这里先放行,看构造了哪些增强器 - + #### BeanFactoryAspectJAdvisorsBuilder#buildAspectJAdvisors() @@ -1246,7 +1246,7 @@ protected List findCandidateAdvisors() { finishBeanFactoryInitialization(beanFactory); ``` - + ### 创建HelloService代理对象之前的工作 @@ -1254,25 +1254,25 @@ finishBeanFactoryInitialization(beanFactory); 我们在诸如下面的地方打上条件断点`beanName.equals("helloService") || beanName.equals("logAspect")` - + debug放行 - + 往下走进入getBean() - + - + 走到熟悉的createbean - + 我们以前讲过resolveBeforeInstantiation是返回代理对象的机会,我们现在来看一下AOP有没有在这里给helloService返回代理对象。F7进入此方法 @@ -1302,25 +1302,25 @@ debug放行 } ``` - + 这是有一个负责AOP功能的后置处理器,就是我们前面说的那个 当循环到这个后置处理器的时候,我们进入方法 - + - + 我debug完之后发现前置过程啥也没干,返回了个NULL。什么都没干的原因就是**负责AOP功能的后置处理器第一次运行准备好数据**和**构建增强器**这两步已经干过了 - + @@ -1329,11 +1329,11 @@ debug放行 1. 最后我们惊奇的发现,AOP的后置处理器在resolveBeforeInstantiation这一步竟然没有返回代理对象,这可能跟大部分人想的有出入。 2. 想一下为什么没有返回代理对象?走到这一步的时候,咱们的HelloService对象都还没有创建,更没有赋值,初始化。你如果在这里直接返回代理对象,那假设HelloService还注入了其它组件,那你返回的代理对象不就没有这些组件了嘛?直接跳过了。 - + 往下走,创建一个最原始的对象 - + #### 赋值 @@ -1341,15 +1341,15 @@ debug放行 往下走进入赋值环节 - + 我们来看下AOP的后置处理器在这一步有没有做事 - + 啥事没干,直接返回 - + @@ -1359,15 +1359,15 @@ debug放行 #### 初始化 - + 进入之后发现,AOP的后置处理器在此介入了,我们再进去看下到底做了啥 - + 啥也没做,直接返回原生Bean - + @@ -1376,11 +1376,11 @@ debug放行 ##### applyBeanPostProcessorsAfterInitialization后置初始化方法 - + - + F7进入方法 @@ -1471,7 +1471,7 @@ F7进入方法 我发现他调用了咱们之前讲过的方法 - + 因为之前调用过,缓存中有,直接返回 @@ -1520,7 +1520,7 @@ F7进入方法 } ``` - + @@ -1568,7 +1568,7 @@ F7进入方法 接着我们返回到这一步,看到了这几个增强器 - + 这里又有一个重要方法 @@ -1605,11 +1605,11 @@ F7进入方法 } ``` - + 回到AbstractAutoProxyCreator#wrapIfNecessary,调用下面的方法真正开始创建代理对象 - + ##### AbstractAutoProxyCreator#createProxy()开始创建代理对象 @@ -1761,27 +1761,27 @@ F7进入方法 最后返回 - + 继续返回 - + - + 最终单例池里就有代理对象了 - + ##### logAspect创建原生对象,而不是代理对象 logAspect切面对象最后创建的是原生对象,如下图,因为他不需要代理 - + diff --git a/docs/spring-sourcecode-v1/07.第7章-AOP的执行流程原理和监听器原理.md b/docs/spring-sourcecode-v1/07.第7章-AOP的执行流程原理和监听器原理.md index 39a9dbf..efb735e 100644 --- a/docs/spring-sourcecode-v1/07.第7章-AOP的执行流程原理和监听器原理.md +++ b/docs/spring-sourcecode-v1/07.第7章-AOP的执行流程原理和监听器原理.md @@ -20,7 +20,7 @@ date: 2022-04-17 12:01:02 ## 流程图-AOP运行流程原理 - + ## 由Aop的执行流程引出方法拦截器 @@ -28,11 +28,11 @@ date: 2022-04-17 12:01:02 断点打到这里,F7进入方法 - + 自然而然的跳到了cglib这里 - + #### CglibAopProxy#intercept() @@ -80,7 +80,7 @@ date: 2022-04-17 12:01:02 #### CglibAopProxy#getInterceptorsAndDynamicInterceptionAdvice() - + 1. 把5个增强器变成了方法拦截器,增强器只是保存信息的,真正执行还得靠方法拦截器。 2. 我们再给上面的470行打上断点,看下之前是如何生成方法拦截器的。因为第一次生成的时候没有缓存,肯定能进去470行。 @@ -91,9 +91,9 @@ date: 2022-04-17 12:01:02 #### Debug调用栈 - + - + 可以看到就是在之前创建代理对象的时候增强器转成的拦截器 @@ -240,7 +240,7 @@ public class MethodBeforeAdviceInterceptor implements MethodInterceptor, BeforeA 然后debug回到accept()方法 - + @@ -248,12 +248,12 @@ public class MethodBeforeAdviceInterceptor implements MethodInterceptor, BeforeA ## 正式开始分析AOP运行流程-链式执行 - + 1. F7进入方法,上面讲过会调用CglibAopProxy内部类的DynamicAdvisedInterceptor#intercept()。这次我们来说下为什么会跳到DynamicAdvisedInterceptor#intercept()方法 2. HelloService是一个代理对象,它的AOP代理是一个DynamicAdvisedInterceptor对象 - + 3. 而DynamicAdvisedInterceptor实现了MethodInterceptor接口 @@ -347,27 +347,27 @@ public interface MethodInterceptor extends Interceptor { } ``` - + 在前面创建HelloService代理对象时创建好的方法拦截器,然后调用proceed() - + ### CglibMethodInvocation#proceed() - + 顾名思义,就是用来执行Cglib生成的代理对象的方法 - + #### CglibAopProxy#proceed() - + ```java public Object proceed() throws Throwable { @@ -429,7 +429,7 @@ public interface MethodInterceptor extends Interceptor { } ``` - + 上面5个拦截器都继承了Spring的org.aopalliance.intercept.MethodInterceptor,和Cglib的MethodInterceptor没关系了 @@ -437,19 +437,19 @@ public interface MethodInterceptor extends Interceptor { 获取第一个拦截器ExposeInvocationInterceptor - + 往下走,直接走到了这里,准备调用ExposeInvocationInterceptor的invoke() - + - + ##### CglibAopProxy#proceed() - + ```java public Object proceed() throws Throwable { @@ -481,35 +481,35 @@ public Object proceed() throws Throwable { 结果又调回了,很明显这是个递归调用 - + #### MethodBeforeAdviceInterceptor-前置通知 然后后面的调用逻辑和前面就一样了,如下 - + - + - + 这个时候就先执行切面的before方法,前置通知就执行了 - + 继续调父类的方法 - + #### AspectJAfterAdvice-后置通知 - + 后置通知这里先不执行,先继续执行下面的方法拦截器链路,最后finally再执行后置通知 @@ -517,7 +517,7 @@ public Object proceed() throws Throwable { #### AfterReturningAdviceInterceptor-返回通知 - + 1. 先往下走,我们就继续 @@ -527,7 +527,7 @@ public Object proceed() throws Throwable { #### AspectJAfterThrowingAdvice-异常通知 - + 继续往下发现索引要超了 @@ -535,17 +535,17 @@ public Object proceed() throws Throwable { #### 真正执行sayhello() - + - + 放行之后看控制台,sayhello方法就打印了。并且到目前为止,只有前面说的前置通知执行了。 - + 然后咱们就往后返, @@ -553,7 +553,7 @@ public Object proceed() throws Throwable { #### AspectJAfterThrowingAdvice-异常通知 - + 咱们这里没异常,就继续返回了 @@ -561,13 +561,13 @@ public Object proceed() throws Throwable { 返回到这里,准备执行返回通知 - + 放行之后的控制台 - + 继续返回 @@ -575,11 +575,11 @@ public Object proceed() throws Throwable { 准备执行后置通知 - + 放行finally之后的控制台 - + @@ -591,13 +591,13 @@ public Object proceed() throws Throwable { 继续返回 - + #### CglibAopProxy$DynamicAdvisedInterceptor - + @@ -760,7 +760,7 @@ public class AnnotationMainTest { 在还未进行refresh()十二大步刷新时,容器就已经有了这两事件相关的Bean定义信息了。 - + @@ -768,7 +768,7 @@ public class AnnotationMainTest { #### 继承树 - + 1. 我们看到实现了SmartInitializingSingleton和BeanFactoryPostProcessor 2. BeanFactoryPostProcessor我们反复在说,就是工厂后置处理环节。EventListenerMethodProcessor实现了BeanFactoryPostProcessor,说明他在工厂后置处理环节会做事 @@ -776,7 +776,7 @@ public class AnnotationMainTest { > AbstractApplicationContext#refresh() ==> AbstractApplicationContext#finishBeanFactoryInitialization() ==> DefaultListableBeanFactory#preInstantiateSingletons() - + @@ -786,7 +786,7 @@ public class AnnotationMainTest { Debug启动 - + 果然是从工厂后置处理那里过来的 @@ -794,7 +794,7 @@ Debug启动 放行,继续往下走。果然SmartInitializingSingleton这里开始做事了 - + @@ -850,7 +850,7 @@ Debug启动 我们在这里打上条件断点 - + @@ -858,15 +858,15 @@ Debug启动 - + F7进入下面的方法 - + 咱们发现是创建了一个适配器 - + ```java public ApplicationListenerMethodAdapter(String beanName, Class targetClass, Method method) { @@ -885,17 +885,17 @@ public ApplicationListenerMethodAdapter(String beanName, Class targetClass, M 为啥要创建这样一个适配器呢?虽然我们的AppEventListener不是监听器,它只是在方法里标注了监听注解,我们自己没有写监听器。但是咱们解析@EventListener注解之后,在这里生成的适配器却实现了EventListener,也就说明这个适配器就是个监听器。 - + 继续往下放行 - + - + 把适配器放到了事件多播器里 @@ -907,7 +907,7 @@ public ApplicationListenerMethodAdapter(String beanName, Class targetClass, M 最后就是三个方法,三个适配器 - + @@ -915,11 +915,11 @@ public ApplicationListenerMethodAdapter(String beanName, Class targetClass, M 给下面的位置打上断点 - + - + ##### AbstractApplicationContext#publishEvent() diff --git a/docs/spring-sourcecode-v1/08.第8章-SpringMVC子容器和Spring父容器的启动原理.md b/docs/spring-sourcecode-v1/08.第8章-SpringMVC子容器和Spring父容器的启动原理.md index c19b434..8181a88 100644 --- a/docs/spring-sourcecode-v1/08.第8章-SpringMVC子容器和Spring父容器的启动原理.md +++ b/docs/spring-sourcecode-v1/08.第8章-SpringMVC子容器和Spring父容器的启动原理.md @@ -92,11 +92,11 @@ public class AppConfig { - + 根路径是在这里配的,tomcat的配置自己百度下,很简单 - + ## Java的SPI机制 @@ -321,7 +321,7 @@ cn.imlql.mysql.MySQLSaveService ### WebApplicationInitializer - + @@ -342,13 +342,13 @@ public interface ServletContainerInitializer { - + - + @@ -469,15 +469,15 @@ ServiceLoader load = ServiceLoader.load(WebApplicationInitializ 下面就是所有实现了WebApplicationInitializer的类 - + 接着在最底下的for循环执行所有实现了WebApplicationInitializer的类的onStartup(),然后就走到了我们的AppStarter - + 到这一步,ioc容器都没有创建,我们给refresh()打个断点,看什么时候启动的ioc - + @@ -485,7 +485,7 @@ ServiceLoader load = ServiceLoader.load(WebApplicationInitializ 我看的时候debug断点没有看到从AppStarter的哪一步跳到refresh()的。然后我一步一步走的时候发现不是在这个方法里调用的,注意看下面的图,DispatcherServlet已经new完了,但是debug依然没有跳到refresh(),说明不是在new DispatcherServlet()的时候创建的容器 - + 不过我凭经验猜测Springmvc里最重要的是DispatcherServlet,会不会是DispatcherServlet的那一步启动了IOC,我们开始进行下面的尝试 @@ -502,7 +502,7 @@ ServiceLoader load = ServiceLoader.load(WebApplicationInitializ 3. 每次请求调用service处理 4. tomcat停止的时候调用destroy进行销毁 - + 4. Serlvet是被谁调用开始初始化的属于tomcat的源码,我们这里不研究,我们这里只需要知道,每一个Servlet都会被初始化就可以了。 @@ -512,7 +512,7 @@ spring-web中有一个叫DispatcherServlet的类,很明显他是一个Servlet - + 1. 想要启动IOC容器,只可能是创建DispatcherServlet对象或者调用init()的时候来搞。上面我们也看到了,创建DispatcherServlet对象的时候debug调用栈并没有显示跳到了refresh方法,所以显然不是创建对象的时候 2. 那就只有可能是调用init()的时候开始启动的IOC容器 @@ -652,13 +652,13 @@ DispatcherServlet没有重写initFrameworkServlet() webloc.setParent(springloc)。类似于双亲委派,容器隔离。先看当前容器有没有这个组件,当前容器没有再去父容器找有没有这个组件 - + ### AbstractAnnotationConfigDispatcherServletInitializer能更快的整合Spring和SpringMVC -AbstractAnnotationConfigDispatcherServletInitializer能更快的整合Spring和SpringMVC +AbstractAnnotationConfigDispatcherServletInitializer能更快的整合Spring和SpringMVC > 后面的讲解都用这个测试类 @@ -811,7 +811,7 @@ public interface ServletContextListener extends EventListener { #### SpringServletContainerInitializer#onStartup() - + @@ -858,7 +858,7 @@ public interface ServletContextListener extends EventListener { #### AbstractDispatcherServletInitializer#onStartup() - + 因为咱们的QuickAppStarter没有onStarup()所以就调用了父类AbstractDispatcherServletInitializer的,没想到AbstractDispatcherServletInitializer也是继续调用父类的 @@ -875,7 +875,7 @@ public interface ServletContextListener extends EventListener { #### AbstractContextLoaderInitializer#onStartup() - + @@ -907,7 +907,7 @@ public abstract class AbstractContextLoaderInitializer implements WebApplication } ``` - + @@ -930,7 +930,7 @@ public abstract class AbstractContextLoaderInitializer implements WebApplication } ``` - + getRootConfigClasses()正好是咱们QuickAppStarter这个子类重写的,debug F7进入 @@ -938,19 +938,19 @@ getRootConfigClasses()正好是咱们QuickAppStarter这个子类重写的,debu 果不其然,调用了QuickAppStarter#getRootConfigClasses() - + 继续往下走创建Web容器,这是Spring父容器,因为你看它getRootConfigClasses()获取的是父容器配置 - + 然后返回 - + @@ -983,7 +983,7 @@ public class ContextLoaderListener extends ContextLoader implements ServletConte 接着就继续返回 - + @@ -1022,7 +1022,7 @@ public class ContextLoaderListener extends ContextLoader implements ServletConte } ``` - + @@ -1040,25 +1040,25 @@ public class ContextLoaderListener extends ContextLoader implements ServletConte } ``` - + 这里又new了一个容器,和上面那个容器一样都没有初始化。这里也是调用咱们QuickAppStarter重写的方法,因为这里调用的是getServletConfigClasses(),所以很明显这里的容器是Web子容器 - + 然后就一路往回返,走到这里 - + 继续F7进入 - + 这里就是保存咱们上面刚创建的Web子容器,然后再返回 #### 返回到SpringServletContainerInitializer#onStartup() - + 1. 这里应用就加载完了,接下来干嘛呢? 2. 你往前看看,咱们的Spring容器和Web子容器都是只是创建完了,都还没有初始化,甚至都没有webloc.setParent(springloc)这样产生父子容器的关系 @@ -1072,7 +1072,7 @@ public class ContextLoaderListener extends ContextLoader implements ServletConte #### ContextLoaderListener#contextInitialized() - + @@ -1080,23 +1080,23 @@ public class ContextLoaderListener extends ContextLoader implements ServletConte 然后真的走到了这里。tomcat里的代码位置是乱的,乱的意思就是比如说上面写的是4766行的调用,但实际上那里是个`}`大括号。也不知道是什么问题,所以我们就大致看下tomcat的代码,不细究。 - + 这里的调用还是对的 - + 应该就是类似这样的调用 - + 走的应该是第一个if - + @@ -1104,21 +1104,21 @@ public class ContextLoaderListener extends ContextLoader implements ServletConte #### ContextLoader#initWebApplicationContext() - + F7进入,这里因为我重新启动了一次,所以你看到根容器是@3661 - + 终于要调用refresh了 - + 这里直接放行到容器refresh完毕看下父容器 - + 父容器只扫描了,springconfig和helloService,我们继续放行看下Web子容器. @@ -1130,7 +1130,7 @@ F7进入,这里因为我重新启动了一次,所以你看到根容器是@36 跳到了这里,为什么会跳到这里呢?记不记得之前我们用DispatcherServlet保存了Web子容器,这里就要调用DispatcherServlet的相关初始化方法 - + @@ -1140,7 +1140,7 @@ F7进入,这里因为我重新启动了一次,所以你看到根容器是@36 - + 上面父子容器关系形成了,并且父容器已经refresh完毕 @@ -1202,11 +1202,11 @@ F7进入,这里因为我重新启动了一次,所以你看到根容器是@36 #### FrameworkServlet#createWebApplicationContext() - + 再次来到Web子容器的刷新 - + 1. 然后我们看到子容器只有它自己的东西 2. 虽然子容器只有controller,但是因为它保存了父容器。所以它是可以拿到HelloService的,也就是我们可以在HelloController里装配HelloService diff --git a/docs/spring-sourcecode-v1/09.第9章-SpringMVC请求处理源码和HandlerMapping原理.md b/docs/spring-sourcecode-v1/09.第9章-SpringMVC请求处理源码和HandlerMapping原理.md index d9cd564..878f614 100644 --- a/docs/spring-sourcecode-v1/09.第9章-SpringMVC请求处理源码和HandlerMapping原理.md +++ b/docs/spring-sourcecode-v1/09.第9章-SpringMVC请求处理源码和HandlerMapping原理.md @@ -16,7 +16,7 @@ date: 2022-06-21 12:01:02 ## 请求的处理链路 - + 1. tomcat里面可以部署多个项目应用。/abc_test和mvc_test这种就是项目路径,用于区分多个项目 2. 在以前的Servlet开发中,每一个路径都需要有一个Servlet来处理。比如上图所画 @@ -26,7 +26,7 @@ date: 2022-06-21 12:01:02 ### Servlet继承树 - + @@ -41,7 +41,7 @@ date: 2022-06-21 12:01:02 ### Debug调用栈 - + ### DispatcherServlet#doService() @@ -191,7 +191,7 @@ date: 2022-06-21 12:01:02 ### doDispatch处理大流程图 - + @@ -210,7 +210,7 @@ protected HttpServletRequest checkMultipart(HttpServletRequest request) throws M } ``` - + 咱们这里目前连解析器都没有,所以就直接返回了 @@ -268,7 +268,7 @@ public boolean isMultipart(HttpServletRequest request) { [官网介绍](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-servlet-special-bean-types) - + @@ -328,7 +328,7 @@ public boolean isMultipart(HttpServletRequest request) { ### Debug调用栈 - + @@ -381,7 +381,7 @@ public boolean isMultipart(HttpServletRequest request) { - + @@ -491,7 +491,7 @@ org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet. ### DispatcherServlet#getHandler()根据请求拿Controller - + @@ -506,13 +506,13 @@ org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet. - + BeanNameUrlHandlerMapping里找不到映射关系,就直接下一个循环了。咱们主要看RequestMappingHandlerMapping怎么处理的 ## RequestMappingHandlerMapping处理流程 - + @@ -569,7 +569,7 @@ F7进入`mapping.getHandler(request)` } ``` - + F7进入`getHandlerInternal(request)` @@ -609,7 +609,7 @@ F7进入`getHandlerInternal(request)` - + F7进入`lookupHandlerMethod(lookupPath, request)` @@ -617,7 +617,7 @@ F7进入`lookupHandlerMethod(lookupPath, request)` ### AbstractHandlerMethodMapping#lookupHandlerMethod()真正根据URL查Controller - + @@ -627,7 +627,7 @@ F7进入`lookupHandlerMethod(lookupPath, request)` ### 返回到AbstractHandlerMethodMapping#getHandlerInternal() - + 这个时候是已经找到了由哪个处理器处理,接着返回 @@ -635,7 +635,7 @@ F7进入`lookupHandlerMethod(lookupPath, request)` 返回到这一步,准备执行`getHandlerExecutionChain(handler, request)` - + @@ -661,20 +661,20 @@ F7进入`lookupHandlerMethod(lookupPath, request)` } ``` - + - 咱们没写拦截器,就没有。 - 继续往回返 ### 返回到DispatcherServlet#getHandler() - + 这里就是责任链模式,有能处理的handler就直接返回 ### 返回到DispatcherServlet#doDispatch() - + 自此RequestMappingHandlerMapping处理结束 @@ -754,9 +754,9 @@ class MappingRegistry { ### Debug调用栈 - + - + 意料之中,启动应用的时候从init初始化那里调用过来了 @@ -808,7 +808,7 @@ public void afterPropertiesSet() { ### AbstractHandlerMethodMapping#initHandlerMethods()初始化HandlerMethods - + @@ -890,7 +890,7 @@ public void afterPropertiesSet() { - + diff --git a/docs/suibi/我的校招-不完全知识点整理.md b/docs/suibi/我的校招-不完全知识点整理.md deleted file mode 100644 index d983bdb..0000000 --- a/docs/suibi/我的校招-不完全知识点整理.md +++ /dev/null @@ -1,4374 +0,0 @@ ---- -title: 我的校招-不完全知识点整理 -tags: - - 校招 - - 面试 -categories: - - 随笔 -keywords: 校招,面试 -description: 如题,一部分的整理【三万字】,具体的看文章 -cover: 'https://npm.elemecdn.com/lql_static@latest/bg/00009.webp' -abbrlink: 5df2d017 -date: 2021-04-22 14:21:58 ---- - - - - - - - -# 必看说明 - - -> 1. 这篇文章是笔者校招时整理的一部分,有的是直接给出了url链接,也有很多是自己看书的总结(所以直接看答案可能会看不懂),都是相当简洁的版本(反正就是很水很乱,哈哈哈)。这篇文章所包含的知识点,我后面大部分都会重新写详细版,目前放出这篇文章的意义只是给需要看的人一个参考。 -> 2. 比如说 -> - Java集合的源码,我会写详细版【TODO,这些东西大部分已经看过了,在计划中】 -> - Java并发我已经写了详细版。不过还有一些的东西没写比如:阻塞队列,ConcurrentHashMap,CopyOnWriteArrayList等这些并发容器,也是常考点,还有一些常非面试的内容比如手写线程池等等,手写阻塞队列等等我觉得很有意思的点。【TODO,看了一部分内容,在计划中】 -> - jvm也已经写了详细版。还有jvm实战还没写【暂时没计划,因为我还不会,嘿嘿】 -> - Mysql部分很重要,下面给出的是极简版【TODO,看了一部分内容,在计划中】 -> - Redis【暂时没计划】 -> - Dubbo源码【TODO,正在学习,在计划中】 -> - 等等 -> 3. 希望大家多看些书和视频,背答案只是为了应试(无奈),多看书和视频以及一些讲的比较好的博客,才能理解的更深刻。 -> 4. 此篇文章发表于2021年4月,不再更新,后续可能删除 - - - -# Java基础 - -## 为什么重写 equals 方法就必须重写 hashcode 方法? - -### hashCode()介绍 - -- hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。 - -- hashCode() 在散列表中才有用,在其它情况下没用。散列表存储的是键值对(key-value),在散列表中hashCode() 的作用是获取对象的散列码,进而快速确定该对象在散列表中的位置。(可以快速找到所需要的对象) - -### 为什么要有 hashCode - -**问题:**假设,HashSet中已经有1000个元素。当插入第1001个元素时,需要怎么处理? - -- 因为HashSet是Set集合,它不允许有重复元素。“将第1001个元素逐个的和前面1000个元素进行比较”?显然,这个效率是相等低下的。 - -- 散列表很好的解决了这个问题,它根据元素的散列码计算出元素在散列表中的位置,然后将元素插入该位置即可。对于相同的元素,自然是只保存了一个。 - - - -### 为什么重写equals方法就必须重写hashcode方法? - -- 在散列表中, - 1、如果两个对象相等,那么它们的hashCode()值一定要相同; 这里的相等是指,通过equals()比较两个对象 时返回true - - 2、如果两个对象hashCode()相等,它们并不一定相等。(不相等时就是哈希冲突) - - 注意:这是在散列表中的情况。在非散列表中一定如此! - - - -- 考虑只重写equals而不重写 hashcode 时,虽然两个属性值完全相同的对象通过equals方法判断为true,但是当把这两个对象加入到 HashSet 时。会发现HashSet中有重复元素,这就是因为HashSet 使用 hashcode 判断对象是否已存在时造成了歧义,结果会导 致HashSet 的不正常运行。所以重写 equals 方法必须重写 hashcode 方法。 - - - -## 深拷贝和浅拷贝 - -> https://blog.csdn.net/baiye_xing/article/details/71788741 - -1. **浅拷贝**:对一个对象进行拷贝时,这个对象对应的类里的成员变量。 - * 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值拷贝,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据 - * 对于数据类型是引用数据类型的成员变量(也就是子对象,或者数组啥的),也就是只是将该成员变量的引用值(引用拷贝【并发引用传递,Java本质还是值传递】)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。 -2. **深拷贝**:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。 -3. 也就是说浅拷贝对于子对象只是拷贝了引用值,并没有真正的拷贝整个对象。 - -深拷贝实现思路: - -1、对于每个子对象都实现Cloneable 接口,并重写clone方法。最后在最顶层的类的重写的 clone 方法中调用所有子对象的 clone 方法即可实现深拷贝。【简单的说就是:每一层的每个子对象都进行浅拷贝=深拷贝】 - -2、利用序列化。【先对对象进行序列化,紧接着马上反序列化出 】 - - - - - -## 八中数据类型及其范围 - -> 为什么是-128-127 :https://zhidao.baidu.com/question/588564780479617005.html - -* byte:1字节,范围为-128-127 -2^7——2^7-1 -* short:2字节,范围为-32768-32767 -2^15——2^15-1 -* int:4字节, -2^31——2^31-1 -* long:8字节 -* float:4字节 -* double:8字节 -* booolean:比较特殊 - - 官方文档:boolean 值只有 true 和 false 两种,这个数据类型只代表 1 bit 的信息,但是它的“大小”没有严格的定义。也就是说,不管它占多大的空间,只有一个bit的信息是有意义的。 - - 单个boolean 类型变量被编译成 int 类型来使用,占 4 个 byte 。 - - boolean 数组被编译成 byte 数组类型,每个 boolean 数组成员占 1 个 byte。 - - 在 Java 虚拟机里,1 表示 true ,0 表示 false 。 - - 这只是 Java 虚拟机的建议。 - - 可以肯定的是,不会是 1 个 bit 。 -* char:2字节 - -从高精度转到低精度有可能损失精度,所以不会自动强转。比如int(高精度)就不会被自动强转为short,如果需要强转就只能强制转换。 - - - -### Java中int类型的最小值是怎么表示的? - -首先计算机中保存的都是补码,都是二进制的补码 - -**int型能表示的最大正数** - -int型的32bit位中,**第一位是符号为,正数位0**。因此,int型能表示的最大的正数的二进制码是0111 1111 1111 1111 1111 1111 1111 1111,也就是2^31-1。 - -**int型能表示的最小负数** - -最小的负数的二进制码是1000 0000 0000 0000 0000 0000 0000 0000,其补码还是1000 0000 0000 0000 0000 0000 0000 0000,值是2^31。用2^31来代替-0 - - - - - -## 反射的作用及机制 - -**名词解释** - -Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键 - -**用途** - -- 常用在通用框架里,比如Spring。为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射,**运行时动态加载需要加载的对象**。 - -原理 - -```java -public class NewInstanceTest { - - @Test - public void test1() throws IllegalAccessException, InstantiationException { - - Class clazz = Person.class; - /* - newInstance():调用此方法,创建对应的运行时类的对象。内部调用了运行时类的空参的构造器。 - - 要想此方法正常的创建运行时类的对象,要求: - 1.运行时类必须提供空参的构造器 - 2.空参的构造器的访问权限得够。通常,设置为public。 - - - 在javabean中要求提供一个public的空参构造器。原因: - 1.便于通过反射,创建运行时类的对象 - 2.便于子类继承此运行时类时,默认调用super()时,保证父类有此构造器 - - */ - Person obj = clazz.newInstance(); - System.out.println(obj); - - } - - //体会反射的动态性 - @Test - public void test2(){ - - for(int i = 0;i < 100;i++){ - int num = new Random().nextInt(3);//0,1,2 - String classPath = ""; - switch(num){ - case 0: - classPath = "java.util.Date"; - break; - case 1: - classPath = "java.lang.Object"; - break; - case 2: - classPath = "com.atguigu.java.Person"; - break; - } - - try { - Object obj = getInstance(classPath); - System.out.println(obj); - } catch (Exception e) { - e.printStackTrace(); - } - } - - - - } - - /* - 创建一个指定类的对象。 - classPath:指定类的全类名 - */ - public Object getInstance(String classPath) throws Exception { - Class clazz = Class.forName(classPath); - return clazz.newInstance(); - } -``` - - - - - -## 说一下序列化,网络传输使用什么序列化?序列化有哪些方式 - -https://www.jianshu.com/p/7298f0c559dc - - - -## 代理 - -https://www.cnblogs.com/cC-Zhou/p/9525638.html - - - -**静态代理** - -代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。值得注意的是,代理类和被代理类应该共同实现一个接口,或者是共同继承某个类。上面介绍的是静态代理的内容,为什么叫做静态呢?因为它的代理类是事先写好的。而动态代理是动态生成的代理类 - - - -**动态代理和静态代理区别** - -1. 静态代理,代理类需要自己编写代码写成。 -2. 动态代理,代理类通过 newProxyInstance方法生成。 -3. 不管是静态代理还是动态代理,代理与被代理者都要实现两样接口,它们的实质是面向接口编程。 -4. 静态代理和动态代理的区别是在于要不要开发者自己定义 代理 类。 -5. 动态代理通过 Proxy 动态生成 proxy class,但是它也指定了一个 InvocationHandler 的实现类。 -6. 代理模式本质上的目的是为了增强现有代码的功能。 - -```java -/** - * - * 动态代理: - * 特点:字节码随用随创建,随用随加载 - * 作用:不修改源码的基础上对方法增强 - * 分类: - * 基于接口的动态代理 - * 基于子类的动态代理 - * 基于接口的动态代理: - * 涉及的类:Proxy - * 提供者:JDK官方 - * 如何创建代理对象: - * 使用Proxy类中的newProxyInstance方法 - * 创建代理对象的要求: - * 被代理类最少实现一个接口,如果没有则不能使用 - * newProxyInstance方法的参数: - * ClassLoader:类加载器 - * 它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法。 - * Class[]:字节码数组 - * 它是用于让代理对象和被代理对象有相同方法。固定写法。固定写接口 - * InvocationHandler:用于提供增强的代码 - * 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。 - * 此接口的实现类都是谁用谁写。 - */ -IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), - producer.getClass().getInterfaces(), - new InvocationHandler() { - /** - * 作用:执行被代理对象的任何接口方法都会经过该方法 - * 方法参数的含义 - * @param proxy 代理对象的引用 - * @param method 当前执行的方法 - * @param args 当前执行方法所需的参数 - * @return 和被代理对象方法有相同的返回值 - * @throws Throwable - */ - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - //提供增强的代码 - Object returnValue = null; - - //1.获取方法执行的参数 - Float money = (Float)args[0]; - //2.判断当前方法是不是销售 - if("saleProduct".equals(method.getName())) { - returnValue = method.invoke(producer, money*0.8f); - } - return returnValue; - } - }); - proxyProducer.saleProduct(10000f); - } - -来源:黑马Spring讲的动态代理部分 -``` - - - -Cglib动态代理 - -```java -public static void main(String[] args) { - final Producer producer = new Producer(); - - /** - * 动态代理: - * 特点:字节码随用随创建,随用随加载 - * 作用:不修改源码的基础上对方法增强 - * 分类: - * 基于接口的动态代理 - * 基于子类的动态代理 - * 基于子类的动态代理: - * 涉及的类:Enhancer - * 提供者:第三方cglib库 - * 如何创建代理对象: - * 使用Enhancer类中的create方法 - * 创建代理对象的要求: - * 被代理类不能是最终类 - * create方法的参数: - * Class:字节码 - * 它是用于指定被代理对象的字节码。 - * - * Callback:用于提供增强的代码 - * 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。 - * 此接口的实现类都是谁用谁写。 - * 我们一般写的都是该接口的子接口实现类:MethodInterceptor - */ - Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() { - /** - * 执行被代理对象的任何方法都会经过该方法 - * @param proxy - * @param method - * @param args - * 以上三个参数和基于接口的动态代理中invoke方法的参数是一样的 - * @param methodProxy :当前执行方法的代理对象 - * @return - * @throws Throwable - */ - @Override - public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { - //提供增强的代码 - Object returnValue = null; - - //1.获取方法执行的参数 - Float money = (Float)args[0]; - //2.判断当前方法是不是销售 - if("saleProduct".equals(method.getName())) { - returnValue = method.invoke(producer, money*0.8f); - } - return returnValue; - } - }); - cglibProducer.saleProduct(12000f); -} -``` - - - -## Comparable和Comparator有什么区别 - -> https://www.cnblogs.com/starry-skys/p/12157141.html - -1. 它们出自不同的包,Comparator在 java.util 包下,Comparable在 java.lang 包下。 -2. Comparator 使用比较灵活,不需要修改实体类源码,但是需要实现一个比较器。 -3. Comparable 使用简单,但是对代码有侵入性,需要修改实体类源码。 -4. 都是接口 - - - -## 引用传递和值传递 - -> 1、JavaGuide-Java基础知识:https://snailclimb.gitee.io/javaguide/#/docs/java/Java基础知识1.4.2 -> -> 2、https://www.zhihu.com/question/31203609 - -- 值传递( pass by value )是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。 - -- 引用传递( pass by reference )是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。 - -JavaGuide里的第三个例子。如果是引用传递的话,交换数据时(不是第二个例子里的赋值语句),应该是真正的交换内存地址,而不是交换引用。 - - - -## 装箱和拆箱 - -> JavaGuide-Java基础知识:https://snailclimb.gitee.io/javaguide/#/docs/java/Java基础知识 1.3.2 - -在装箱的时候自动调用的是Integer的valueOf(int)方法。而在拆箱的时候自动调用的是Integer的intValue方法。 - - - - - - - -## static变量存储位置 - -> 笔者的JVM篇说的很详细 - -JDK7以上,静态变量存储在其对应的Class对象中。而Class对象作为对象,和其他普通对象一样,都是存在java堆中的。 - - - -## super()和this()不能同时在一个构造函数中出现 - -this()和this. 不一样 - -```java -public class JZ_056 { - private int num; - - private String str; - - public JZ_056() { - System.out.println("调用无参构造"); - } - - public JZ_056(int num) { - this(); //调用无参构造 - System.out.println("调用有参构造"); - this.num = num; - } - - public JZ_056(int num, String str) { - this(1); //这个会调用只有一个参数的有参构造 - this.num = num; - this.str = str; - } - - public static void main(String[] args) { - JZ_056 jz_056 = new JZ_056(2, "哈哈"); - } - -} -``` - - - -由上面代码可以看出来,this()方法可以调用本类的无参构造函数。如果本类继承的有父类,那么无参构造函数会有一个隐式的super()的存在。所以在同一个构造函数里面如果this()之后再调用super(),那么就相当于调用了两次super(),也就是调用了两次父类的无参构造,就失去了语句的意义,编译器也不会通过。 - - - -## 面向对象三大特征 - -> https://www.cnblogs.com/wujing-hubei/p/6012105.html - -1、**多态**是建立在继承的基础上的,是指子类类型的对象可以赋值给父类类型的引用变量,但运行时仍表现子类的行为特征。也就是说,同一种类型的对象执行同一个方法时可以表现出不同的行为特征。 - - - -## 泛型 - -> 笔者有篇泛型文章讲的很详细,包括泛型擦除这些都讲了 - - - -## 父子类相关的问题 - -https://www.nowcoder.com/questionTerminal/d31ea6176417421b9152d19e8bd1b689 - -https://www.nowcoder.com/profile/2164604/test/35226827/365890 - -http://www.mamicode.com/info-detail-2252140.html - -https://developer.aliyun.com/article/653204 - - - - - -## new出来的对象都是在堆上分配的吗? - -https://imlql.cn/post/50ac3a1c.html:我在此篇文章的 `堆是分配对象的唯一选择么?`有讲解。 - - - -# Java集合 - -> 这部分我会重新写新文章 - -## ArrayList和LinkedList时间复杂度 - -**头部插入**:由于ArrayList头部插入需要移动后面所有元素,所以必然导致效率低。LinkedList不用移动后面元素,自然会快一些。 -**中间插入**:查看源码会注意到LinkedList的中间插入其实是先判断插入位置距离头尾哪边更接近,然后从近的一端遍历找到对应位置,而ArrayList是需要将后半部分的数据复制重排,所以两种方式其实都逃不过遍历的操作,相对效率都很低,但是从实验结果还是ArrayList更胜一筹,我猜测这与数组在内存中是连续存储有关。 -**尾部插入**:ArrayList并不需要复制重排数据,所以效率很高,这也应该是我们日常写代码时的首选操作,而LinkedList由于还需要new对象和变换指针,所以效率反而低于ArrayList。 - -删除操作和添加操作没有什么区别。所以笼统的说LinkedList插入删除效率比ArrayList高是不对的,有时候反而还低。之所以笼统的说LinkedList插入删除效率比ArrayList高,我猜测是ArrayList复制数组需要时间,也占一定的时间复杂度。而因为数据量太少,这种效果就体现不出来。 - -https://blog.csdn.net/hollis_chuang/article/details/102480657 - -## fail-fast和fail-safe - -https://blog.csdn.net/zwwhnly/article/details/104987143 - -https://blog.csdn.net/Kato_op/article/details/80356618 - -https://blog.csdn.net/striner/article/details/86375684 - - - -## LinkedHashMap - -> https://www.cnblogs.com/xiaowangbangzhu/p/10445574.html -> -> https://blog.csdn.net/qq_28051453/article/details/71169801 - -### 如何保证插入数据的有序性? - -1、在实现上,LinkedHashMap 很多方法直接继承自 HashMap(比如put remove方法就是直接用的父类的),仅为维护双向链表覆写了部分方法(get()方法是重写的)。 - -2、重新定义了数组中保存的元素Entry(继承于HashMap.node),该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而在哈希表的基础上又构成了双向链接列表。仍然保留hash拉链法的next属性,所以既可像HashMap一样快速查找,用next获取该链表下一个Entry。也可以通过双向链接,通过after完成所有数据的有序迭代. - -3、在这个构造方法中,有个`accessOrder`,它不同的值有不同的意义: 默认为false,即按插入时候的顺序进行迭代。设置为true后,按访问时候的顺序进行迭代输出,即链表的最后一个元素总是最近才访问的。 - -**访问的顺序:**如果有1 2 3这3个Entry,那么访问了1,就把1移到尾部去,即2 3 1。每次访问都把访问的那个数据移到双向队列的尾部去,那么每次要淘汰数据的时候,双向队列最头的那个数据不就是最不常访问的那个数据了吗?换句话说,双向链表最头的那个数据就是要淘汰的数据。 - -4、链表节点的删除过程 - -与插入操作一样,LinkedHashMap 删除操作相关的代码也是直接用父类的实现,但是LinkHashMap 重写了removeNode()方法 afterNodeRemoval()方法,该removeNode方法在hashMap 删除的基础上有调用了afterNodeRemoval 回调方法。完成删除。 - -删除的过程并不复杂,上面这么多代码其实就做了三件事: - -1. 根据 hash 定位到桶位置 -2. 遍历链表或调用红黑树相关的删除方法 -3. 从 LinkedHashMap 维护的双链表中移除要删除的节点 - - - - - - - -## 红黑树 - -https://blog.csdn.net/qq_36610462/article/details/83277524 - -## 集合继承结构 - - - - - -# Java并发 - -> 下面只补充遗漏的,剩余的所有在我的Java并发系列文章都有 - -## 手写DCL,并解释为什么是这样写 - -外层的if是为了让线程尽量少的进入synchronized块 - -内层的if则看下面的解释 - -```java -public static Object getInstance() { - if(instance == null) {//线程1,2到达这里 - synchronized(b) {//线程1到这里开始继续往下执行,线程2等待 - if(instance == null) {//线程1到这里发现instance为空,继续执行if代码块, - //执行完成后退出同步区域,然后线程2进入同步代码块,如果在这里不再加一次判断, - //就会造成instance再次实例化,由于增加了判断, - //线程2到这里发现instance已被实例化于是就跳过了if代码块 - instance = new Object(); - } - } - } - } -``` - - - -## 我们知道ArrayList是线程不安全,请编写一个不安全的案例并给出解决方案 - -```java -/* 笔记 - * 1.只有一边写一边读的时候才会报java.util.ConcurrentModificationException - * 单独测写的时候都没有报这个异常 - * */ -public class Video20_01 { - - public static void main(String[] args) { - List list = new ArrayList<>(); - - for (int i = 0; i < 20; i++) { - new Thread(() ->{ - list.add(UUID.randomUUID().toString().substring(0,8)); //这个是写 - System.out.println(list);//这个是读 - },"线程" + String.valueOf(i)).start(); - } - } -} -``` - - - -```java -/** - * @Author: - * @Date: 2019/9/25 8:45 - *

- * 功能描述: 集合不安全问题的解决(写时复制) - */ - -public class Video20_02 { - - public static void main(String[] args) { -// List list = new Vector<>();//不推荐 -// List list = Collections.synchronizedList(new ArrayList<>());//不推荐 - List list = new CopyOnWriteArrayList<>(); - for (int i = 0; i < 20; i++) { - new Thread(() ->{ - list.add(UUID.randomUUID().toString().substring(0,8)); //这个是写 - System.out.println(list);//这个是读 - },"线程" + String.valueOf(i)).start(); - } - - ConcurrentHashMap test = new ConcurrentHashMap<>(); - test.put(1,1); - } -} -``` - -## ThreadLocal - -### 简单原理 - -```java - /* public class ThreadLocal {}源码 */ - public T get() { - Thread t = Thread.currentThread(); - ThreadLocalMap map = getMap(t); - if (map != null) { - ThreadLocalMap.Entry e = map.getEntry(this); - if (e != null) { - @SuppressWarnings("unchecked") - T result = (T)e.value; - return result; - } - } - return setInitialValue(); - } - - public void set(T value) { - Thread t = Thread.currentThread(); - ThreadLocalMap map = getMap(t); //① - if (map != null) - map.set(this, value); - else - createMap(t, value); - } - - ThreadLocalMap getMap(Thread t) { - return t.threadLocals; //② - } -/* public class ThreadLocal {}源码 */ - - - -/* public class Thread implements Runnable {}*/ - ThreadLocal.ThreadLocalMap threadLocals = null; ③ -``` - - - -1、由源码可以看到,ThreadLocal是在set的时候,才和线程建立关系。并且在get的时候通过获取线程,来判断是哪一个线程。 - -2、同时每个Thread类都有一个```ThreadLocal.ThreadLocalMap```类的变量,set方法里通过getMap获取这个变量,然后在这个map里设置值,所以才有了每一个线程都可以通过ThreadLocal设置专属变量的说法。 - -### 为什么Key要使用弱引用及内存泄漏原因 - -https://blog.csdn.net/puppylpg/article/details/80433271 - - - -# JVM - -这里我只截图下我准备的面试题目录,因为所有答案你都能在笔者的JVM文章里找到答案 - - - - - - - - - -# Redis - -## 为什么使用Redis - -用缓存,主要有两个用途:**高性能**、**高并发**。 - -### 高性能 - -假设这么个场景,你有个操作,一个请求过来,吭哧吭哧你各种乱七八糟操作 mysql,半天查出来一个结果,耗时 600ms。但是这个结果可能接下来几个小时都不会变了,或者变了也可以不用立即反馈给用户。那么此时咋办? - -缓存啊,折腾 600ms 查出来的结果,扔缓存里,一个 key 对应一个 value,下次再有人查,别走 mysql 折腾 600ms 了,直接从缓存里,通过一个 key 查出来一个 value,2ms 搞定。性能提升 300 倍。 - -就是说对于一些需要复杂操作耗时查出来的结果,且确定后面不怎么变化,但是有很多读请求,那么直接将查询出来的结果放在缓存中,后面直接读缓存就好。 - -### 高并发 - -mysql 这么重的数据库,压根儿设计不是让你玩儿高并发的,虽然也可以玩儿,但是天然支持不好。mysql 单机支撑到 `2000QPS` 也开始容易报警了。 - -所以要是你有个系统,高峰期一秒钟过来的请求有 1 万,那一个 mysql 单机绝对会死掉。你这个时候就只能上缓存,把很多数据放缓存,别放 mysql。缓存功能简单,说白了就是 `key-value` 式操作,单机支撑的并发量轻松一秒几万十几万,支撑高并发 so easy。单机承载并发量是 mysql 单机的几十倍。 - -> 缓存是走内存的,内存天然就支撑高并发。 - - - -## 分布式缓存和本地缓存有啥区别 - - - -1. 缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。 -2. 使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。 - - - - - -## redis和Memecache的区别 - -1. redis 支持更丰富的数据类型(支持更复杂的应用场景),Memcached 只支持最简单的 k/v 数据类型 -2. Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memecache 把数据全部存在内存之中。 -3. redis 目前是原生支持 cluster 模式的,而Memecache原生不支持集群。 -4. Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 -5. Redis 支持发布订阅模型、Lua脚本、事务等功能,而Memcached不支持。并且,Redis支持更多的编程语言。 - -## redis常用数据结构和使用场景 - -### 字符串String - -**使用场景:** - -- 缓存,用于支持高并发 -- 计数器,视频播放数 -- 限速,处于安全考虑,每次进行登录时让用户输入手机验证码,为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率。就是设置过期时间 - -### 列表list - -**使用场景:** - -- 文章列表,每个用户都有属于自己的文章列表,现在需要分页展示文章列表,此时可以考虑使用列表,列表不但有序,同时支持按照索引范围获取元素。(lrange命令) -- 消息队列,使用列表技巧: - * lpush+lpop=Stack(栈) - * lpush+rpop=Queue(队列) - * lpush+ltrim=Capped Collection(有限集合) - * lpush+brpop=Message Queue(消息队列) -- 时间轴 - -### 字典hash - -**使用场景:** - -- 记录帖子的点赞数、评论数和点击数。帖子的标题、摘要、作者和封面信息,用于列表页展示 -- 用户信息管理,key是用户标识,value是用户信息。hash 特别适合用于存储对象 - -### 集合set - -**使用场景:** - -- 标签(tag),集合类型比较典型的使用场景,如一个用户对娱乐、体育比较感兴趣,另一个可能对新闻感兴 趣,这些兴趣就是标签,有了这些数据就可以得到同一标签的人,以及用户的共同爱好的标签。 - -### 有序集合zset - -- 排行榜,记录热榜帖子 ID 列表,总热榜和分类热榜 - - - -### HyperLogLog - -统计网页的uv(**独立访客,每个用户每天只记录一次**),如果用SET的话空间耗费会很大 - -pv(**浏览量,用户每点一次记录一次**)可以用String来统计 - -## Zset底层实现?跳表搜索插入删除过程? - -> - 读的这篇文章:https://mp.weixin.qq.com/s/NOsXdrMrWwq4NTm180a6vw -> -> - 总结的时候参考了这些文章: -> -> - https://juejin.im/book/5afc2e5f6fb9a07a9b362527/section/5b5ac63d5188256255299d9c -> - http://zhangtielei.com/posts/blog-redis-skiplist.html -> -> - 什么是跨度:https://www.cnblogs.com/handwrit2000/p/12626570.html -> -> - 下面是总结了一下 - -### Zset底层实现? - -跳跃表 - -### 跳表搜索插入删除过程? - -#### 搜索过程: - - 假设我们需要查找的值为k(score) - -1. 需要从 header 的当前最高层maxLevel开始遍历,直到找到 最后一个比k小的节点 -2. 然后从这个节点开始降一层再遍历找到第二个节点 (最后一个比k小的节点), -3. 然后以此类推一直降到最底层进行遍历就找到了期望的节点 。 - -**注意:** - -1. 不是和当前元素比较,是和当前元素的下一元素比较,所以才能知道(最后一个比k小的元素) -2. redis跳跃表的排序是首先比较score,如果score相等,还需要比较value - -#### 插入过程: - -1. 首先是有一个搜索确定位置的过程,逐步降级寻找目标节点,得到「搜索路径」 - -2. 然后才开始插入 - - 2-1. 为每个节点随机出一个层数(level) - - 2-2. 创建新节点,再将搜索路径上的节点和这个新节点通过前向后向指针串起来 - - 2-3. 如果分配的新节点的高度高于当前跳跃列表的最大高度,更新一下跳跃列表的最大高度,并且填充跨度 - - - -#### 删除过程: - -删除过程和插入过程类似,都需先把这个「搜索路径」找出来。然后对于每个层的相关节点都重排一下前向后向指针就可以了。同时还要注意更新一下最高层数`maxLevel`。 - - - -#### 更新过程 - -1. 当我们调用 `ZADD` 方法时,如果对应的 value 不存在,那就是插入过程,如果这个 value 已经存在,只是调整一下 score 的值,那就需要走一个更新流程。 -2. 假设这个新的 score 值并不会带来排序上的变化,那么就不需要调整位置,直接修改元素的 score 值就可以了, -3. 但是如果排序位置改变了,那就需要调整位置。Redis 采用了一个非常简单的策略,**把这个元素删除再插入这个**,需要经过两次路径搜索,从这一点上来看,Redis 的 `ZADD` 代码似乎还有进一步优化的空间。 - - - -#### 元素排名的实现 - -1. 跳跃表本身是有序的,Redis 在 skiplist 的 forward 指针上进行了优化,给每一个 forward 指针都增加了 跨度`span` 属性,用来 **记录了前进指针所指向节点和当前节点的距离(也就是跨过了几个节点)**。在源码中我们也可以看到 Redis 在插入、删除操作时都会小心翼翼地更新 `span` 值的大小。 -2. 所以,沿着 **"搜索路径"**,把所有经过节点的跨度 `span` 值进行累加就可以算出当前元素的最终 rank 值了。 - - - -## redis过期淘汰策略 - -> 这个讲得好:https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/redis-expiration-policies-and-lru - -**补充:** - -1. **volatile-lfu**:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰 -2. **allkeys-lfu**:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key - -### lru和lfu区别 - - - -- LRU,即:最近最少使用淘汰算法(Least Recently Used)。LRU是淘汰一段时间内,没有被使用的页面。 - -- LFU,即:最不经常使用淘汰算法(Least Frequently Used)。LFU是淘汰一段时间内,使用次数最少的页面。 - -## redis持久化机制?都有什么优缺点?持久化的时候还能接受请求吗? - -> 参考: -> -> - JavaGuide-Redis常见问题总结 -> - https://mp.weixin.qq.com/s/O_qDco6-Dasu3RomWIK_Ig -> - https://juejin.im/book/5afc2e5f6fb9a07a9b362527/section/5afc364c6fb9a07aaf3567c8 - -**Redis** 的数据 **全部存储** 在 **内存** 中,如果 **突然宕机**,数据就会全部丢失,因此必须有一套机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的 **持久化机制**,它会将内存中的数据库状态 **保存到磁盘** 中。 - - - -下面是总结 - -### RDB快照 - -1. **Redis 快照** 是最简单的 Redis 持久性模式。当满足特定条件时,它将生成数据集的时间点快照,例如,如果先前的快照是在2分钟前创建的,并且现在已经至少有 *100* 次新写入,则将创建一个新的快照。此条件可以由用户配置 Redis 实例来控制【JavaGuide里有如何配置】。快照是一次全量备份 - -2. 但我们知道,Redis 是一个 **单线程** 的程序,这意味着,我们不仅仅要响应用户的请求,还需要进行内存快照。而后者要求 Redis 必须进行 IO 操作,这会严重拖累服务器的性能。 - -3. 还有一个重要的问题是,我们在 **持久化的同时**,**内存数据结构** 还可能在 **变化**,比如一个大型的 hash 字典正在持久化,结果一个请求过来把它删除了,还没持久化完呢。怎么办呢 - -4. 操作系统多进程 **COW(Copy On Write) 机制**可以解决上述问题 - - 4-1.**Redis** 在持久化时会调用 `glibc` 的函数 `fork` 产生一个子进程,简单理解也就是基于当前进程 **复制** 了一个进程,主进程和子进程会共享内存里面的代码块和数据段 - - 4-2.所以 **快照持久化** 可以完全交给 **子进程** 来处理,**父进程** 则继续 **处理客户端请求**。**子进程** 做数据持久化,它 **不会修改现有的内存数据结构**,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是 **父进程** 不一样,它必须持续服务客户端请求,然后对 **内存数据结构进行不间断的修改** - - 4-3.这个时候就会使用操作系统的 COW 机制来进行 **数据段页面** 的分离。数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复 制一份分离出来,然后 **对这个复制的页面进行修改**。这时 **子进程** 相应的页面是 **没有变化的**,还是进程产生时那一瞬间的数据。 - - 4-4.子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 **Redis** 的持久化 **叫「快照」的原因**。接下来子进程就可以非常安心的遍历数据了进行序列化写磁盘了。 - - - -### AOF - - - -#### AOF原理 - -1. **AOF(Append Only File - 仅追加文件)** 它的工作方式非常简单:每次执行 **修改内存** 中数据集的写操作时,都会 **记录** 该操作。假设 AOF 日志记录了自 Redis 实例创建以来 **所有的修改性指令序列**,那么就可以通过对一个空的 Redis 实例 **顺序执行所有的指令**,也就是 **「重放」**,来恢复 Redis 当前实例的内存数据结构的状态。 -2. 当 Redis 收到客户端修改指令后,会先进行参数校验、然后直接执行指令,如果没问题,就 **立即** 将该指令文本 **存储** 到 AOF 日志中,也就是说,**先执行指令再将日志存盘**。这一点不同于 `MySQL`、`LevelDB`、`HBase` 等存储引擎,如果我们先存储日志再做逻辑处理,这样就可以保证即使宕机了,我们仍然可以通过之前保存的日志恢复到之前的数据状态,但是 **Redis 为什么没有这么做呢?** -3. 引用一条来自知乎上的回答:我甚至觉得没有什么特别的原因。仅仅是因为,由于AOF文件会比较大,为了避免写入无效指令(错误指令),必须先做指令检查?如何检查,只能先执行了。因为语法级别检查并不能保证指令的有效性,比如删除一个不存在的key。而MySQL这种是因为它本身就维护了所有的表的信息,所以可以语法检查后过滤掉大部分无效指令直接记录日志,然后再执行。 - - - -#### AOF重写 - -1. Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 AOF 日志瘦身。 -2. AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小。 -3. Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。其原理就是开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。 - -#### fsync - -1. AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓存中,然后内核会异步将这些数据刷回到磁盘的。这就意味着如果机器突然宕机,AOF 日志内容可能还没有来得及完全刷到磁盘中,这个时候就会出现日志丢失。那该怎么办? -2. Linux 的`glibc`提供了`fsync(int fd)`函数可以将指定文件的内容强制从内核缓存刷到磁盘。只要 Redis 进程实时调用 fsync 函数就可以保证 aof 日志不丢失。但是 fsync 是一个磁盘 IO 操作,它很慢! -3. 所以在生产环境的服务器中,Redis 通常是每隔 1s 左右执行一次 fsync 操作,周期 1s 是可以配置的。这是在数据安全性和性能之间做了一个折中,在保持高性能的同时,尽可能使得数据少丢失。 - - - -### Redis4.0混合持久化 - -1. 重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。 -2. Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。 -3. 于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。 - - - -## redis事务 - -> https://juejin.im/book/5afc2e5f6fb9a07a9b362527/section/5afc3747f265da0b71567686 - -### 事务简介 - -1. 每个事务的操作都有 begin、commit 和 rollback,begin 指示事务的开始,commit 指示事务的提交,rollback 指示事务的回滚。Redis 在形式上看起来也差不多,分别是 multi/exec/discard。multi 指示事务的开始,exec 指示事务的执行,discard 指示事务的丢弃。 -2. 所有的指令在 exec 之前不执行,而是缓存在服务器的一个事务队列中,服务器一旦收到 exec 指令,才开执行整个事务队列,执行完毕后一次性返回所有指令的运行结果。因为 Redis 的单线程特性,它不用担心自己在执行队列的时候被其它指令打搅,可以保证他们能得到的「原子性」执行。但是 Redis 的事务根本不能算「原子性」,而仅仅是满足了事务的「隔离性」,隔离性中的串行化——当前执行的事务有着不被其它事务打断的权利 - -### 为什么 Redis 的事务不能支持回滚? - -1. redis是先执行指令,然后记录日志,如果执行失败,日志也不会记录,也就不能回滚了 - -### Redis 是单线程,命令是按顺序执行无并发,已经有Multi 和 Exec了为什么还需要Watch? - -1. redis事务在执行时是单线程运行的。但是在执行前有可能别的客户端已经修改了事务里执行的key。所以在multi事务开始之前用watch检测这个key避免被其他客户端改变的。如果这个key被改变 了 exec的时候就会报错 不执行这个事务。 - -2. 这里的“other client” 并不一定是另外一个客户端,watch操作执行之后,multi之外任何操作都可以认为是other clinet在操作(即使仍然是在同一个客户端上操作),exec该事务也仍旧会失败。 - - > redis使用watch实现cas具体示例:https://www.jianshu.com/p/0244a875aa26 - -3. 分布式锁是悲观锁,redis的watch机制是乐观锁。悲观锁的意思就是我不允许你修改。乐观锁的意思就是你修改了之后要告诉我,我让我的操作失败。 - - - -## redis是单线程还是多线程?为什么那么快? - - - -### 为啥 redis 单线程模型也能效率这么高?? - -* 纯内存操作。 -* 核心是基于非阻塞的 IO 多路复用机制。 -* C 语言实现,一般来说,C 语言实现的程序“距离”操作系统更近,执行速度相对会更快。 -* 单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。 - -### redis的线程模型简介,及一次通信过程图解(问的概率小) - -https://doocs.github.io/advanced-java/#/./docs/high-concurrency/redis-single-thread-model - - - -## 几种IO模型 - -我写过这篇文章,可以翻一下 - -## select、poll、epoll的区别? - -> select, poll, epoll 都是I/O多路复用的具体的实现,之所以有这三个存在,其实是他们出现是有先后顺序的。 -> -> - https://blog.csdn.net/nanxiaotao/article/details/90612404 -> - https://www.cnblogs.com/aspirant/p/9166944.html -> - https://www.zhihu.com/question/32163005 - -### 1.select - -1. 它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以**select具有O(n)的无差别轮询复杂度**,同时处理的流越多,无差别轮询时间就越长。 -2. 单个进程可监视的fd_set(监听的端口个数)数量被限制:32位机默认是1024个,64位机默认是2048。但可通过修改宏定义或编译内核修改句柄数量 - -### 2.poll - -poll本质上和select没有区别,采用**链表**的方式替换原有fd_set数据结构,而使其**没有连接数的限制**。 - -### 3.epoll - -1. epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1)) -2. 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数。即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。 -3. epoll通过内核和用户空间共享一块内存来实现的。select和poll都是内核需要将消息传递到用户空间,都需要内核拷贝动作 -4. epoll有EPOLLLT和EPOLLET两种触发模式,也就是水平触发和边沿触发两种模式。(**暂时不去记,有个印象,大致是什么样就可以**) - - - - - -## redis集群数据分布方式?有什么优点?一致性hash呢? - -> 详细看这里:https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/redis-cluster - -### 分布式寻址有哪几种? - -* hash 算法(大量缓存重建) -* 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡) -* redis cluster 的 hash slot 算法 - - - -### redis集群数据分布方式 - -redis cluster采用hash slot。(中文就是hash槽) - -**优点:** - -1. 任何一台机器宕机,其它节点,不影响的。因为 key 找的是 hash slot,不是机器。 - - - -### 一致性hash - -思想上就是一个环,key取hash,然后顺时针找第一个节点 - -**优点:** - -1. 如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点之间的数据,其它不受影响。增加一个节点也同理。 - -**缺点:** - -1. 一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成**缓存热点**的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。 - - - -## 为什么使用跳跃表,不用平衡树,hash表 - - - -* **skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找**。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。 -* **在做范围查找的时候,平衡树比skiplist操作要复杂**。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。 -* **平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。** -* 从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。 -* 查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。 -* **从算法实现难度上来比较,skiplist比平衡树要简单得多**。 - - - -## HyperLogLog(问的概率小) - -> 参考: -> -> - https://mp.weixin.qq.com/s/9dtGe3d_mbbxW5FpVPDNow -> - https://juejin.im/book/5afc2e5f6fb9a07a9b362527/section/5b336548e51d4558a426ff56 -> -> - https://www.jianshu.com/p/55defda6dcd2 -> - https://www.baidu.com/s?wd=HyperLogLog%E5%8E%9F%E7%90%86&ie=UTF-8 - - - - - -## 布隆过滤器 - -> 整体参考: -> -> - https://juejin.im/book/5afc2e5f6fb9a07a9b362527/section/5b33657cf265da597b0f99ab -> - https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/#toc-heading-1 - -### 原理 - -> 原理:https://juejin.im/book/5afc2e5f6fb9a07a9b362527/section/5b33657cf265da597b0f99ab -> -> 看上面那篇文章的-**布隆过滤器的原理**-这部分 - - - -### 使用场景 - -- **大数据判断是否存在**:这就可以实现出上述的去重功能,如果你的服务器内存足够大的话,那么使用 HashMap 可能是一个不错的解决方案,理论上时间复杂度可以达到 O(1 的级别,但是当数据量起来之后,还是只能考虑布隆过滤器。 -- **解决缓存穿透**:我们经常会把一些热点数据放在 Redis 中当作缓存,例如产品详情。 通常一个请求过来之后我们会先查询缓存,而不用直接读取数据库,这是提升性能最简单也是最普遍的做法,但是 **如果一直请求一个不存在的缓存**,那么此时一定不存在缓存,那就会有大量请求直接打到数据库 上,造成 **缓存穿透**,布隆过滤器也可以用来解决此类问题。 -- **爬虫/ 邮箱等系统的过滤**:平时不知道你有没有注意到有一些正常的邮件也会被放进垃圾邮件目录中,这就是使用布隆过滤器误判 导致的。 -- **推荐系统去重**:比如抖音的推荐系统去重,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。通过布隆过滤器判断是否已经看过的重复内容 - - - -### 注意事项 - -1. 当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。 -2. 使用时不要让实际元素远大于初始化大小,当实际元素开始超出初始化大小时,应该对布隆过滤器进行重建,重新分配一个 size 更大的过滤器,再将所有的历史元素批量 add 进去 (这就要求我们在其它的存储器中记录所有的历史元素)。因为 error_rate 不会因为数量超出就急剧增加,这就给我们重建过滤器提供了较为宽松的时间。 - - - - - -### 大致空间占用 - -1. 当一个元素平均需要 1 个字节 (8bit) 的指纹空间时 (l/n=8),错误率大约为 2% -2. 错误率为 10%,一个元素需要的平均指纹空间为 4.792 个 bit,大约为 5bit -3. 错误率为 1%,一个元素需要的平均指纹空间为 9.585 个 bit,大约为 10bit -4. 错误率为 0.1%,一个元素需要的平均指纹空间为 14.377 个 bit,大约为 15bi - - - - - -## 如何保证 redis 的高并发和高可用?(问的概率小) - -1. redis 实现高并发主要依靠**主从架构**,一主多从,一般来说,很多项目其实就足够了,单主用来写入数据,单机几万 QPS,多从用来查询数据,多个从实例可以提供每秒 10w 的 QPS。如果想要在实现高并发的同时,容纳大量的数据,那么就需要 redis 集群,使用 redis 集群之后,可以提供每秒几十万的读写并发。 -2. redis 高可用,如果是做主从架构部署,那么加上哨兵就可以了,就可以实现,任何一个实例宕机,可以进行主备切换。 - - - -## redis主从架构(问的概率小) - -> https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/redis-master-slave -> -> 如果问到的话,答个大概其实就可以了 - - - -## redis哨兵机制(问的概率小) - -> https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/redis-sentinel -> -> 如果问到的话,答个大概其实就可以了 - - - -## redis集群,也就是redis cluster(问的概率小) - -> https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/redis-cluster - -**注意redis cluster和redis replication ,redis哨兵的关系** - -redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。 - - - - - -## 高并发环境使用缓冲会出现什么问题? - - - -### 缓存穿透 - -#### 是什么: - -**缓存穿透** - -1. 是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。 -2. 举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“**视缓存于无物**”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。 - -#### 解决: - -1. 会在接口层增加校验,比如用户鉴权校验,参数做校验,不合法的参数直接代码Return,比如:id 做基础校验,id <=0的直接拦截等。 - -2. 缓存空,但它的过期时间会很短,最长不超过五分钟。为了防止缓存穿透将,null或者空字符串值设置给redis。比如 - - set -999 UNKNOWN ,set -1 null - -3. 布隆过滤器 - - - -#### 比较新的解决办法:布隆过滤器 - - - -1、Redis还有一个高级用法**布隆过滤器(Bloom Filter)**这个也能很好的防止缓存穿透的发生,他的原理也很简单就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了DB刷新KV再return。 - -2、请注意,用 redis 也可以做到判断 key 对应的value 在数据库中存不在,那就是把数据库里的所有value对应的key都储存在redis 中,而value可以为空,然后判断下`key.IsExists()`就可以了,但是这无疑会浪费大量空间,因为存储了数据库中所有的key。而且这也不符合缓存的初衷:咱不能暴力的把所有key都存下来,而是查询了啥key,我们缓存啥key。而布隆过滤器是一种非常高效的数据结构,把所有数据库的value对应的key 存储到布隆过滤器里,几乎不消耗什么空间,而且查询也是相当的快!但是请注意,它只能判断 key 是否存在(而且会有一定的误差)。所以一个查询先通过布隆顾虑器判断key是否存在(key 对应的value是否存在数据库中),如果不存在直接返回空就好了。 - - - -### 缓存雪崩 - -#### 是什么: - -**缓存雪崩** - -1. 是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。 -2. 所有缓存机器意外发生了全盘宕机。缓存挂了,此时 请求全部落数据库,数据库必然扛不住。 - - - -#### 解决: - -1. 原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。 -2. 事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。 -3. 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。 -4. 事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。 - - - -### 缓存击穿 - -#### 是什么: - -对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。 - - - -#### 解决: - -不同场景下的解决方式可如下: - -* 若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期。 -* 若缓存的数据更新不频繁,且缓存更新的整个流程耗时较少的情况下,则可以采用基于 redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。 -* 若缓存的数据更新频繁或者缓存更新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动的重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。 - - - -## 如何保证缓存与数据库的双写一致性? - -> https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/redis-consistence -> -> 讲的蛮好 - - - -## LRU和LFU - -> https://www.cnblogs.com/sddai/p/9739900.html - -1、LRU和LFU都是内存管理的页面置换算法。 - -2、LRU,即:最近最少使用淘汰算法(Least Recently Used)。LRU是淘汰最长时间没有被使用的页面。 - -3、LFU,即:最不经常使用淘汰算法(Least Frequently Used)。LFU是淘汰一段时间内,使用次数最少的页面。 - -4、LRU的意思是只要在最近用了一次这个页面,这个页面就可以不用被淘汰。LFU的意思是即使我最近用了某个页面,但是这个页面在一段时间内使用次数还是最少的话,我还是要淘汰它,相当于LFU说的是使用频率。 - - - -## 参考 - -> 下面是参考的比较多的,参考的比较少的直接在文中写明了 - -- https://github.com/doocs/advanced-java -- https://github.com/Snailclimb/JavaGuide -- [Redis 深度历险:核心原理与应用实践](https://juejin.cn/book/6844733724618129422)【不推荐】 - - - -# Mysql - -> Mysql这里特别强调一下,下面的内容其实我看了一些书,所以下面的参考链接是不完全的,只是答案版本。Mysql的部分详细文章也在我的计划中 - -## 事务4大特性,一致性具体指什么?这4个特性mysql如何保证实现的? - -事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。 - -> https://www.zhihu.com/question/31346392 - -### ACID(四大特性) - -1. **原子性(Atomicity):** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; -2. **一致性(Consistency):** 是指事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。保证*数据库一致性*是指当事务完成时,必须使所有数据都具有一致的状态;【Mysql是怎样运行的:如果数据库中的数据全部符合现实世界中的约束(all defined rules),我们说这些数据就是一致的,或者说符合`一致性`的。】 -3. **隔离性(Isolation):** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间是独立的; -4. **持久性(Durability):** 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失 - -### 这4个特性mysql如何保证实现的 - - - -**总结一下:** - -- 保证一致性: - - - 从数据库层面,数据库通过原子性、隔离性、持久性来保证一致性。也就是说ACID四大特性之中,C(一致性)是目的,A(原子性)、I(隔离性)、D(持久性)是手段,是为了保证一致性,数据库提供的手段 - - 从应用层面,通过代码判断数据库数据是否有效,然后决定回滚还是提交数据! - -- 保证原子性:是利用Innodb的undo log,undo log名为回滚日志,是实现原子性的关键。undo log记录了这些回滚需要的信息,当事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。 - - - -- 保证持久性:是利用Innodb的redo log。当做数据修改的时候,不仅在内存中操作,还会在redo log中记录这次操作。当事务提交的时候,会将redo log日志进行刷盘。当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo log和binlog内容决定回滚数据还是提交数据 - - - 采用redo log的好处?其实好处就是将redo log进行刷盘比对数据页刷盘效率高,具体表现如下 - * redo log体积小,毕竟只记录了哪一页修改了啥,因此体积小,刷盘快。 - * redo log是一直往末尾进行追加,属于顺序IO。效率显然比随机IO来的快。 - -- 保证隔离性:利用的是锁和MVCC机制 - - - - - -## 事务隔离级别,4个隔离级别分别有什么并发问题? - -- READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。 -- READ-COMMITTED(读取已提交): 只允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。 -- REPEATABLE-READ(可重复读): 在同一个事务内对同一数据的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。 -- SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。 - - - -**脏写(`Dirty Write`)**:如果一个事务修改了另一个未提交事务修改过的数据,那就意味着发生了`脏写`, - -**脏读(Drity Read)**:如果一个事务读到了另一个未提交事务修改过的数据,那就意味着发生了`脏读`【某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。】 -**不可重复读(Non-repeatable read)**:如果一个事务能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值。【强调的是每次都能读到最新数据】 -**幻读(Phantom Read)**:如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来,那就意味着发生了`幻读`【那对于先前已经读到的记录,之后又读取不到这种情况,算啥呢?其实这相当于对每一条记录都发生了不可重复读的现象。幻读只是重点强调了读取到了之前读取没有获取到的记录。】 - - - - - -具体讲解:https://blog.csdn.net/qq_35433593/article/details/86094028 - -## Mysql默认隔离级别?如何保证并发安全? - -- Mysql 默认采用的 REPEATABLE_READ隔离级别 -- 并发安全的实现基于锁机制和并发调度。其中并发调度使用的是MVVC(多版本并发控制),通过保存修改的旧版本信息来支持并发一致性读和回滚等特性。 - - - - - -## 隔离级别的单位是数据表还是数据行?如串行化级别,两个事务访问不同的数据行,能并发? - -* READ-UNCOMMITTED(读取未提交):**不加锁** -* READ-COMMITTED(读取已提交): **行锁** -* REPEATABLE-READ(可重复读): **行锁** -* SERIALIZABLE(可串行化): **表锁** - -不能,串行化就直接把表锁住了,无法并发。 - - - - - -## 存储引擎Innodb和Myisam的区别以及使用场景 - - - - - - - -## 介绍Inodb锁机制,行锁,表锁,意向锁 - -https://blog.csdn.net/Waves___/article/details/105295060 - -## 介绍MVCC. - -1. 多版本控制(MVCC): 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,InnoDB通过undo log保存每条数据的多个版本,并且能够找回数据历史版本提供给用户读,每个事务读到的数据版本可能是不一样的。在同一个事务中,用户只能看到该事务创建快照之前已经提交的修改和该事务本身做的修改。 - -2. MVCC只在 Read Committed 和 Repeatable Read两个隔离级别下工作。其他两个隔离级别和MVCC不兼容,Read Uncommitted总是读取最新的记录行,而不是符合当前事务版本的记录行;Serializable 则会对所有读取的记录行都加锁。 - -3. MySQL的InnoDB存储引擎默认事务隔离级别是RR(可重复读),是通过 "行级锁+MVCC"一起实现的,正常读的时候不加锁,写的时候加锁。而 MCVV 的实现依赖:隐藏字段、Read View、Undo log - - - -具体看这里:https://blog.csdn.net/Waves___/article/details/105295060 - - - -**小总结一下**: - -①每次对记录进行改动,都会记录一条`undo日志`,每条`undo日志`也都有一个`roll_pointer`属性(`INSERT`操作对应的`undo日志`没有该属性,因为该记录并没有更早的版本),可以将这些`undo日志`都连起来,串成一个链表。所有的版本都会被`roll_pointer`属性连接成一个链表,我们把这个链表称之为`版本链`,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的`事务id` - -②ReadView:需要判断一下版本链中的哪个版本是当前事务可见的。 - -* `m_ids`:表示在生成`ReadView`时当前系统中活跃的读写事务的`事务id`列表。 -* `min_trx_id`:表示在生成`ReadView`时当前系统中活跃的读写事务中最小的`事务id`,也就是`m_ids`中的最小值。 -* `max_trx_id`:表示生成`ReadView`时系统中应该分配给下一个事务的`id`值。 -* `creator_trx_id`:表示生成该`ReadView`的事务的`事务id`。 - -③有了这个`ReadView`,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见: - -* 如果被访问版本的`trx_id`属性值与`ReadView`中的`creator_trx_id`值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。 -* 如果被访问版本的`trx_id`属性值小于`ReadView`中的`min_trx_id`值,表明生成该版本的事务在当前事务生成`ReadView`前已经提交,所以该版本可以被当前事务访问。 -* 如果被访问版本的`trx_id`属性值大于或等于`ReadView`中的`max_trx_id`值,表明生成该版本的事务在当前事务生成`ReadView`后才开启,所以该版本不可以被当前事务访问。 -* 如果被访问版本的`trx_id`属性值在`ReadView`的`min_trx_id`和`max_trx_id`之间,那就需要判断一下`trx_id`属性值是不是在`m_ids`列表中,如果在,说明创建`ReadView`时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建`ReadView`时生成该版本的事务已经被提交,该版本可以被访问。 - -如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。 - -## 哈希索引是如何实现的? - -对于哈希索引来说,底层的数据结构就是哈希表。对于哈希索引,**InnoDB是自适应哈希索引**的(hash索引的创建由InnoDB存储引擎引擎自动优化创建,我们干预不了 - - - -* 哈希索引也没办法利用索引完成**排序** -* 不支持**最左匹配原则** -* 在有大量重复键值情况下,哈希索引的效率也是极低的---->**哈希碰撞**问题。 -* **不支持范围查询** - - - -## B树索引为什么使用B+树,相对于B树有什么优点?为什么不能红黑树?要提到磁盘预读 - - - -### B+树比B树有什么优点? - -1、B+树只有叶节点存放数据,其余节点用来存索引,而B-树是每个索引节点都会有Data域。B+树单一节点存储更多的数据,使得查询的IO次数更少。 - -2、所有查询都要查找到叶子节点,查询性能稳定。 - -3、B+树所有数据都在叶子节点,所有叶子节点形成有序链表,便于范围查询 - - - -### 为什么不能用红黑树 - -1.在大规模数据存储的时候,平衡二叉树往往出现由于树的深度过大而造成磁盘IO读写过于频繁,进而导致效率低下的情况 - -2.数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。 - -这个网址有答案: - - - -### 什么是磁盘预读 - -1、由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,磁盘的存取速度往往是主存的几百分分之一,因此为了提高效率,要尽量减少磁 盘I/O。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放 入内存。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。 - -https://www.cnblogs.com/leezhxing/p/4420988.html - - - -## 聚簇索引和非聚簇索引区别 - -### 1.聚簇索引 - -1. 使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义: - - * 页内的记录是按照主键的大小顺序排成一个单向链表。 - * 各个存放用户记录的页也是根据页中用户记录的主键大小顺序排成一个双向链表。 - * 存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表。 - -2. `B+`树的叶子节点存储的是完整的用户记录。 - - 所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。 - -### 2.非聚簇索引 - -也叫二级索引(使用唯一索引或普通索引) - -* 使用记录`c2`列的大小进行记录和页的排序,这包括三个方面的含义: - * 页内的记录是按照`c2`列的大小顺序排成一个单向链表。 - * 各个存放用户记录的页也是根据页中记录的`c2`列大小顺序排成一个双向链表。 - * 存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的`c2`列大小顺序排成一个双向链表。 -* `B+`树的叶子节点存储的并不是完整的用户记录,而只是`c2列+主键`这两个列的值。 -* 目录项记录中不再是`主键+页号`的搭配,而变成了`c2列+页号`的搭配。 - -如果用C2和C3列共同建二级索引,这个二级索引也叫做联合索引。 - -### 3.区别 - -* 聚集索引在叶子节点存储的是**表中的数据** -* 非聚集索引在叶子节点存储的是**主键和索引列** -* 使用非聚集索引查询出数据时,**拿到叶子上的主键再去查到想要查找的数据**。(拿到主键再查找这个过程叫做**回表**) - - - -## 回表查询和覆盖索引 - -### 1.回表查询 - -使用非聚集索引查询出数据时,**拿到叶子上的主键再去查到想要查找的数据**。(拿到主键再查找这个过程叫做**回表**) - -### 2.覆盖索引 - -* 我们前面知道了,如果不是聚集索引,叶子节点存储的是主键+索引列值 -* 最终还是要“回表”,也就是要通过主键**再**查找一次。这样就会比较慢 -* 覆盖索引就是把要**查询出的列和索引是对应的**,不做回表操作! - -13、如何创建索引? - -14、如何使用索引避免全表扫描? - -15、Explain语句各字段的意义 - -## 最左前缀!! - -### 1.最左前缀 - -联合索引B+树是如何建立的?是如何查询的?当where子句中出现>时,联合索引命中是如何的? 如 where a > 10 and b = “111”时,联合索引如何创建?mysql优化器会针对得做出优化吗? - -如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始,并且不跳过索引中的列。遇到范围查询`(>、<、between、like`左匹配)等就**不能进一步匹配**了,后续退化为线性查找。 - - - - - -## MySQL中一条SQL语句的执行过程(未总结) - -> 剩余相关知识看JavaGuide: -> -> https://snailclimb.gitee.io/javaguide/#/docs/database/一条sql语句在mysql中如何执行的?id=%e4%b8%89-%e6%80%bb%e7%bb%93 - -### 查询语句 - -```sql -select * from tb_student A where A.age='18' and A.name=' 张三 '; -``` - -* 先用**连接器**检查该语句是否有权限,如果没有权限,直接返回错误信息,如果有权限,在 MySQL8.0 版本以前,会先查询缓存,以这条 sql 语句为 key 在内存中查询是否有结果,如果有直接缓存,如果没有,执行下一步。 - -* 通过**分析器**进行词法分析,提取 sql 语句的关键元素,比如提取上面这个语句是查询 select,提取需要查询的表名为 tb_student,需要查询所有的列,查询条件是这个表的 id='1'。然后判断这个 sql 语句是否有语法错误,比如关键词是否正确等等,如果检查没问题就执行下一步。 - -* 接下来就是**优化器**进行确定执行方案,上面的 sql 语句,可以有两种执行方案: - - ``` - a.先查询学生表中姓名为“张三”的学生,然后判断是否年龄是 18。 - b.先找出学生中年龄 18 岁的学生,然后再查询姓名为“张三”的学生。Copy to clipboardErrorCopied - ``` - - 那么优化器根据自己的优化算法进行选择执行效率最好的一个方案(优化器认为,有时候不一定最好)。那么确认了执行计划后就准备开始执行了。 - -* 执行器执行并返回引擎的执行结果【进行权限校验,如果没有权限就会返回错误信息,如果有权限就会调用数据库引擎接口,返回引擎的执行结果。】 - -SQL Select语句完整的执行顺序[从DBMS使用者角度] : - -1. from子句组装来自不同数据源的数据; - -2. where子句基于指定的条件对记录行进行筛选; - -3. group by子句将数据划分为多个分组; -4. 使用聚集函数进行计算; - -5. 使用having子句筛选分组; -6. 计算所有的表达式; - -7. 使用order by对结果集进行排序。 - - - - - -SQL Select语句的执行步骤[从DBMS实现者角度,这个对我们用户意义不大] : - -1)语法分析,分析语句的语法是否符合规范,衡量语句中各表达式的意义。 - -2)语义分析, 检查语句中涉及的所有数据库对象是否存在,且用户有相应的权限。 - -3)视图转换,将涉及视图的查询语句转换为相应的对基表查询语句。 - -4)表达式转换,将复杂的SQL表达式转换为较简单的等效连接表达式。 - -5)选择优化器,不同的优化器一般产生不同的“ 执行计划" - -6)选择连接方式,ORACLE 有三种连接方式,对多表连接ORACLE可选择适当的连接方式。 - -7)选择连接顺序,对多表连接ORACLE选择哪-对表先连接,选择这两表中哪个表做为源数据表。 - -8)选择数据的搜索路径,根据以上条件选择合适的数据搜索路径,如是选用全表搜索还是利用索引或是其他的方式。9)运行"执行计划”。 - - - -### 更新语句 - -```sql -update tb_student A set A.age='19' where A.name=' 张三 '; -``` - -其实更新语句也基本上会沿着上一个查询的流程走,只不过执行更新的时候肯定要记录日志啦,这就会引入日志模块了,MySQL 自带的日志模块式 **binlog(归档日志)** ,所有的存储引擎都可以使用,我们常用的 InnoDB 引擎还自带了一个日志模块 **redo log(重做日志)**,我们就以 InnoDB 模式下来探讨这个语句的执行流程。流程如下: - -* 先查询到张三这一条数据,如果有缓存,也是会用到缓存。 -* 然后拿到查询的语句,把 age 改为 19,然后调用引擎 API 接口,写入这一行数据,InnoDB 引擎把数据保存在内存中,同时记录 redo log,此时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,随时可以提交。 -* 执行器收到通知后记录 binlog,然后调用引擎接口,提交 redo log 为提交状态。 -* 更新完成。 - - - -## 既然增加树的路数可以降低树的高度,那么无限增加树的路数是不是可以有最优的查找效率? - -这样会形成一个有序数组,文件系统和数据库的索引都是存在硬盘上的,并且如果数据量大的话,不一定能一次性加载到内存中。有序数组没法一次性加载进内存,会进行很多次IO,效率又会降下来。这时候B+树的多路存储威力就出来了,可以每次加载B+树的一个结点,然后一步步往下找 - - - -## 当前读和快照读 - -https://www.jianshu.com/p/785cef383ed6 - - - - - - - -## MySQL中IS NULL、IS NOT NULL、!=可以用索引吗? - - - -我们分别分析了拥有`IS NULL`、`IS NOT NULL`、`!=`这三个条件的查询是在什么情况下使用二级索引来执行的,核心结论就是:成本决定执行计划,跟使用什么查询条件并没有什么关系。优化器会首先针对可能使用到的二级索引划分几个范围区间,然后分别调查这些区间内有多少条记录,在这些范围区间内的二级索引记录的总和占总共的记录数量的比例达到某个值时,优化器将放弃使用二级索引执行查询,转而采用全表扫描。 - - - -## 为什么MyISAM会比Innodb的查询速度快? - -INNODB在做SELECT的时候,要维护的东西比MYISAM引擎多很多: -1)数据块,INNODB要缓存,MYISAM只缓存索引块, 这中间还有换进换出的减少; - - -2)innodb寻址要映射到块,再到行,MYISAM记录的直接是文件的OFFSET,定位比INNODB要快 - -3)INNODB还需要维护MVCC一致;虽然你的场景没有,但他还是需要去检查和维护 -MVCC (Multi-Version Concurrency Control)多版本并发控制 - - - -## limit分页查询相关问题 - -### limit的用法 - -https://www.cnblogs.com/cai170221/p/7122289.html - -### limit会对前面的数据进行IO吗 - -会 https://blog.csdn.net/qq_34208844/article/details/104043486 - - - -### 分页查找,如果要查找很靠后的页面如何,比如100万之后查10条怎么优化() - -https://blog.csdn.net/weixin_43066287/article/details/90024600 - - - - - -## EXPLAIN - - - -这里我们只关注三种,分别是type,key,rows - -### type - -代表着`MySQL`对某个表的执行查询时的访问类型 - -常见的几种: - -all EXPLAIN SELECT * FROM s1 WHERE key1 > 'z' AND key3 = 'a'; -+----+-------------+-------+------------+------+-------------------+----------+---------+-------+------+----------+-------------+ -| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | -+----+-------------+-------+------------+------+-------------------+----------+---------+-------+------+----------+-------------+ -| 1 | SIMPLE | s1 | NULL | ref | idx_key1,idx_key3 | idx_key3 | 303 | const | 6 | 2.75 | Using where | -+----+-------------+-------+------------+------+-------------------+----------+---------+-------+------+----------+-------------+ -1 row in set, 1 warning (0.01 sec) -``` - -上述执行计划的`possible_keys`列的值是`idx_key1,idx_key3`,表示该查询可能使用到`idx_key1,idx_key3`两个索引,然后`key`列的值是`idx_key3`,表示经过查询优化器计算使用不同索引的成本后,最后决定使用`idx_key3`来执行查询比较划算。 - -### rows - -如果查询优化器决定使用全表扫描的方式对某个表执行查询时,执行计划的`rows`列就代表预计需要扫描的行数,如果使用索引来执行查询时,执行计划的`rows`列就代表预计扫描的索引记录行数。比如下边这个查询: - -## 怎么抓取慢sql - -**正确方式:** - -说明开启慢查询日志,可以让MySQL记录下查询超过指定时间的语句,通过定位分析性能的瓶颈,才能更好的优化数据库系统的性能。 - -首先打开慢查询 -`show variables like 'slow_query%';` - -``` -show variables like 'long_query_time'; -``` - -方法一:全局变量设置 -将 slow_query_log 全局变量设置为“ON”状态 - -`mysql> set global slow_query_log='ON';` -设置慢查询日志存放的位置 - -`mysql> set global slow_query_log_file='/usr/local/mysql/data/slow.log';` -查询超过1秒就记录 - -``` -mysql> set global long_query_time=1; -``` - -方法二:配置文件设置 -修改配置文件my.cnf,在[mysqld]下的下方加入 - -``` -[mysqld] -slow_query_log = ON -slow_query_log_file = /usr/local/mysql/data/slow.log -long_query_time = 1 -``` - -3.重启MySQL服务 - -``` -service mysqld restart -``` - -1.执行一条慢查询SQL语句 - -``` -mysql> select sleep(2); -``` - -2.查看是否生成慢查询日志 - -``` -ls /usr/local/mysql/data/slow.log -``` - -如果日志存在,MySQL开启慢查询设置成功! - - - -### explain分析sql - -### show profile分析sql - -## SQL执行慢的原因 - -### 偶尔比较慢 - - 1.数据库刷新脏页 - -- redolog写满:更新数据或者插入数据时,会先在内存中将相应的数据更新,并不会立刻持久化到磁盘中去,而是把更新记录存到redolog日志中去,待到空闲时,再通过redolog把最新数据同步到磁盘中去。所以当redolog写满的时候,就不会等到空闲时,而是暂停手中的活,去把数据同步到磁盘中,所以这个时候SQL就会执行的比较慢 -- 内存写满:如果一次查询的数据过多,查询的数据页并不在内存中,这时候就需要申请新的内存空间,而如果此时内存已满,就需要淘汰一部分内存数据页,如果是干净页就直接释放,如果是脏页就需要flush -- 数据库认为空闲的时候:这时候系统不忙 -- 数据库正常关闭:内存脏页flush到磁盘上 - -2.无法获取锁 - -### 一直很慢 - -1. 字段没有索引 -2. 有索引没用 -3. 索引没用上 -4. 数据库选错索引:通过区分度判断走索引的话反而扫描的行数很大而且索引要走两边,选择全表扫描 - -## redo日志,undo日志,binlog日志 - -1、设计数据库的大叔把这些为了回滚而记录的这些东东称之为撤销日志,英文名为`undo log`,我们也可以土洋结合,称之为`undo日志`。 - -2、我们只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来。所以我们其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把修改了哪些东西记录一下就好,所以上述内容也被称之为`重做日志`,英文名为`redo log` - -3、binlog其实就是记录了数据库执行更改的所有操作,因此很显然,它可以用来做数据归档和数据恢复 - -4、binlog主要用来做数据归档,但是它并不具备崩溃恢复的能力,也就是说如果你的系统突然崩溃,重启后可能会有部分数据丢失,而redo log的存在则可以完美解决这个问题。 - -5、redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用。 - -6、redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1 ”。 - -7、redo log是循环写的,空间固定会用完;binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。 - - - -## count(1),count(*),count(filed) - -- count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为NULL - -- count(列名)在统计结果的时候,会忽略列值为NULL - -- count(1)包括了所有列,用1代表代码行,在统计结果的时候,不会忽略列值为NULL - -对于COUNT(1)和COUNT(*)执行优化器的优化是完全一样的 - - - - - -## 数据库连接池 - -> https://www.cnblogs.com/whb11/p/11315463.html - -### 池化技术 - -1、池是一种广义上的池,比如数据库连接池、线程池、内存池、对象池等。以数据库连接池为例:数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现得尤为突出。 一个数据库连接对象均对应一个物理数据库连接,每次操作都打开一个物理连接,使用完都关闭连接,这样会造成系统的性能低下。数据库连接池的解决方案是在应用程序启动时建立足够的数据库连接,并讲这些连接组成一个连接池(简单说:在一个“池”里放了好多半成品的数据库联接对象),由应用程序动态地对池中的连接进行申请、使用和释放,但并不销毁。对于多于连接池中连接数的并发请求,应该在请求队列中排队等待。 - -2、池技术的优势是: - -- 可以消除对象创建所带来的延迟,从而提高系统的性能。 -- 也可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。 -- 连接复用,降低资源消耗。避免因创建过多连接而导致内存不够用了。 - - - -### 常见的连接池参数 - -| maxActive | 连接池同一时间可分配的最大活跃连接数 | 100 | -| -------------------------- | ------------------------------------------------------------ | ------------------------- | -| maxIdle | 始终保留在池中的最大连接数,如果启用,将定期检查限制连接,超出此属性设定的值且空闲时间超过minEvictableIdleTimeMillis的连接则释放 | 与maxActive设定的值相同 | -| minIdle | 始终保留在池中的最小连接数,池中的连接数量若低于此值则创建新的连接,如果连接验证失败将缩小至此值 | 与initialSize设定的值相同 | -| initialSize | 连接池启动时创建的初始连接数量 | 10 | -| maxWait | 最大等待时间(毫秒),如果在没有连接可用的情况下等待超过此时间,则抛出异常 | 30000(30秒) | -| minEvictableIdleTimeMillis | 连接在池中保持空闲而不被回收的最小时间(毫秒) | 60000(60秒) | - - - -## 数据库范式 - -> https://blog.csdn.net/weixin_43433032/article/details/89293663 - -- 属于第一范式关系的所有属性都不可再分,即数据项不可分 -- 第二范式是指每个表必须有一个(有且仅有一个)数据项作为关键字或主键(primary key),其他数据项与关键字或者主键一一对应,即其他数据项完全依赖于关键字或主键。由此可知单主属性的关系均属于第二范式 - - **候选码:** 若关系中的某一属性组的值能唯一地标识一个元组,而其子集不能,则称该属性组为候选码。若一个关系中有多个候选码,则选定其中一个为主码。 - - 以上面的学生表为例,表中的码为学号(码可以为学号或者姓名,此处假定码为学号),非主属性为性别、年龄(其余都为主属性),当学号确定时,性别、年龄也都惟一的被确定为,故学生表的设计满足第二范式(学生表为单主属性的关系)。 -- 第三范式:非主属性既不传递依赖于码,也不部分依赖于码 - - 人话:非主码字段既不传递依赖于主码,也不部分依赖于主码。这里的主码常见的就是主键 - - - -## 索引失效的场景 - -1、索引列类型是字符串,查询条件未加引号。 - -2、使用like时通配符在前 - -3、在查询条件中使用OR - -4、对索引列进行函数运算 - -5、虽然使用了索引列,但是对索引进行了诸如加减乘除的运算 - -6、联合索引不符合最左前缀法则 - -7、使用索引需要扫描的行数过多,这时mysql优化器会直接使用全表扫描 - -## 三大引擎 - -### MyISAM - - 特性: - -  ①不支持事务。 - -  ②表级锁定,并发性能大大降低。 - -  ③读写互相阻塞。 - - 适用场景: - -  ①不支持事务。 - -  ②并发相对较低,表锁定。 - -  ③执行大量select语句操作的表。 - -  ④count(*)操作较快。 - -  ⑤不支持外键。 - - 注:查询速度快的原因:a.MyISAM存储的直接是文件的offset。b.不用维护mvcc。 - -### InnoDB - - 特征: - -  ①良好的事务支持:支持事务隔离的四个级别。 - -  ②行级锁定:使用间隙锁?????? - -  ③外键约束。 - -  ④支持丢失数据的自动恢复。 - -### Memory - -  在内存中,默认使用hash索引,等值条件查找快速快,范围查找慢,断电后数据丢失,但表结构存在 - - - - - -## Mysql主从复制 - -> https://www.jianshu.com/p/faf0127f1cb2 -> -> https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/mysql-read-write-separation.md - -> 前提是作为主服务器角色的数据库服务器必须开启二进制日志,且复制是异步的 - -1、主服务器上面的任何修改都会通过自己的 I/O tread(I/O 线程)保存在二进制日志 `Binary log` 里面。 - -2、从服务器上面也启动一个 I/O thread,通过配置好的用户名和密码, 连接到主服务器上面请求读取二进制日志,然后把读取到的二进制日志写到本地的一个`Realy log`(中继日志)里面。 - -3、从服务器上面同时开启一个 SQL thread 定时检查 `Realy log`(这个文件也是二进制的),如果发现有更新立即把更新的内容在本机的数据库上面执行一遍。 - -- 这里有一个非常重要的一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行 SQL 的特点,在高并发场景下,从库的数据一定会比主库慢一些,是**有延时**的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。 - -- 而且这里还有另外一个问题,就是如果主库突然宕机,然后恰好数据还没同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。 - - 所以 MySQL 实际上在这一块有两个机制,一个是**半同步复制**,用来解决主库数据丢失问题;一个是**并行复制**,用来解决主从同步延时问题。 - - 这个所谓**半同步复制**,也叫 `semi-sync` 复制,指的就是主库写入 binlog 日志之后,就会将**强制**此时立即将数据同步到从库,从库将日志写入自己本地的 relay log 之后,接着会返回一个 ack 给主库,主库接收到**至少一个从库**的 ack 之后才会认为写操作完成了。 - - 所谓**并行复制**,指的是从库开启多个线程,并行读取 relay log 中不同库的日志,然后**并行重放不同库的日志**,这是库级别的并行。 - -### MySQL 主从同步延时问题(精华) - -一般来说,如果主从延迟较为严重,有以下解决方案: - -* 分库,将一个主库拆分为多个主库,每个主库的写并发就减少了几倍,此时主从延迟可以忽略不计。 -* 打开 MySQL 支持的并行复制,多个库并行复制。如果说某个库的写入并发就是特别高,单库写并发达到了 2000/s,并行复制还是没意义。 -* 重写代码,写代码的同学,要慎重,插入数据时立马查询可能查不到。 -* 如果确实是存在必须先插入,立马要求就查询到,然后立马就要反过来执行一些操作,对这个查询**设置直连主库**。**不推荐**这种方法,你要是这么搞,读写分离的意义就丧失了。 - - - -## Mysql分库分表 - -> https://gitee.com/youthlql/advanced-java/blob/master/docs/high-concurrency/database-shard.md - -### 分表 - -比如你单表都几千万数据了,,**单表数据量太大**,会极大影响你的 sql **执行的性能**,到了后面你的 sql 可能就跑的很慢了。一般来说,单表到几百万的时候,性能就会相对差一些了,就得分表了。 - -### 分库 - -分库就是你一个库一般而言,最多支撑到并发 2000,一定要扩容了,而且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大。那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。 - -### 分库分表中间件 - -#### Sharding-jdbc - -当当开源的,属于 client 层方案,目前已经更名为 [`ShardingSphere`](https://github.com/apache/incubator-shardingsphere)(后文所提到的 `Sharding-jdbc`,等同于 `ShardingSphere`)。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且截至 2019.4,已经推出到了 `4.0.0-RC1` 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。 - -Sharding-jdbc 这种 client 层方案的**优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高**,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要**耦合** Sharding-jdbc 的依赖; - -#### Mycat - -基于 Cobar 改造的,属于 proxy 层方案,支持的功能非常完善。 - -Mycat 这种 proxy 层方案的**缺点在于需要部署**,自己运维一套中间件,运维成本高,但是**好处在于对于各个项目是透明的**,如果遇到升级之类的都是自己中间件那里搞就行了。 - -### 拆分维度 - -- 水平拆分的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来扛更高的并发,还有就是用多个库的存储容量来进行扩容。 - -- 垂直拆分**的意思,就是**把一个有很多字段的表给拆分成多个表**,**或者是多个库上去**。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会**将较少的访问频率很高的字段放到一个表里去**,然后**将较多的访问频率很低的字段放到另外一个表里去**。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。 - - - - - -## 如何让系统从未分库分表动态切换到分库分表上? - -### 停机迁移方案 - -### 双写迁移方案 - -1、简单来说,就是在线上系统里面,之前所有写库的地方,增删改操作,**除了对老库增删改,都加上对新库的增删改**,这就是所谓的**双写**,同时写俩库,老库和新库。 - -2、然后**系统部署**之后,新库数据差太远,用导数据工具,跑起来读老库数据写新库,写的时候要根据 modified 这类字段判断这条数据最后修改的时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写。简单来说,就是不允许用老数据覆盖新数据。 - -3、导完一轮之后,有可能数据还是存在不一致,那么就程序自动做一轮校验,比对新老库每个表的每条数据,接着如果有不一样的,就针对那些不一样的,从老库读数据再次写。反复循环,直到两个库每个表的数据都完全一致为止。 - -4、接着当数据完全一致了,就 ok 了,基于仅仅使用分库分表的最新代码,重新部署一次,不就仅仅基于分库分表在操作了么,还没有几个小时的停机时间,很稳。所以现在基本玩儿数据迁移之类的,都是这么干的。 - - - - - -## 如何设计可以动态扩容缩容的分库分表方案? - -**1、第一次分库分表,就一次性给他分个够**,一开始上来就是 32 个库,每个库 32 个表,那么总共是 1024 张表。这个分法,第一,基本上国内的互联网肯定都是够用了,第二,无论是并发支撑还是数据量支撑都没问题。 - -2、每个库正常承载的写入并发量是 1000,那么 32 个库就可以承载 32 * 1000 = 32000 的写并发,如果每个库承载 1500 的写并发,32 * 1500 = 48000 的写并发,接近 5 万每秒的写入并发,前面再加一个MQ,削峰,每秒写入 MQ 8 万条数据,每秒消费 5 万条数据。 - -3、一个实践是利用 `32 * 32` 来分库分表,即分为 32 个库,每个库里一个表分为 32 张表。一共就是 1024 张表。根据某个 id 先根据 32 取模路由到库,再根据 32 取模路由到库里的表。 - -### 分库分表之后,id 主键如何处理? - - snowflake 算法 - -snowflake 算法是 twitter 开源的分布式 id 生成算法,采用 Scala 语言实现,是把一个 64 位的 long 型的 id,1 个 bit 是不用的,用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id,12 bit 作为序列号。 - -* 1 bit:不用,为啥呢?因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。 -* 41 bit:表示的是时间戳,单位是毫秒。41 bit 可以表示的数字多达 `2^41 - 1`,也就是可以标识 `2^41 - 1` 个毫秒值,换算成年就是表示69年的时间。 -* 10 bit:记录工作机器 id,代表的是这个服务最多可以部署在 2^10台机器上哪,也就是1024台机器。但是 10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 `2^5`个机房(32个机房),每个机房里可以代表 `2^5` 个机器(32台机器)。 -* 12 bit:这个是用来记录同一个毫秒内产生的不同 id,12 bit 可以代表的最大正整数是 `2^12 - 1 = 4096`,也就是说可以用这个 12 bit 代表的数字来区分**同一个毫秒内**的 4096 个不同的 id。 - -```java -0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000000 -``` - - - - - -# Spring - -> ThinkWon博客:https://blog.csdn.net/ThinkWon/article/details/104397516 - - - -## Spring IOC - -### 什么是依赖注入 - -控制反转IoC是一个很大的概念,可以用不同的方式来实现。其主要实现方式有两种:依赖注入和依赖查找 - -依赖注入:相对于IoC而言,依赖注入(DI)更加准确地描述了IoC的设计理念。所谓依赖注入(Dependency Injection),即组件之间的依赖关系由容器在应用系统运行期来决定,也就是由容器动态地将某种依赖关系的目标对象实例注入到应用系统中的各个关联的组件之中。组件不做定位查询,只提供普通的Java方法让容器去决定依赖关系。 - -#### 依赖注入的基本原则 - -依赖注入的基本原则是:应用组件不应该负责查找资源或者其他依赖的协作对象。配置对象的工作应该由IoC容器负责,“查找资源”的逻辑应该从应用组件的代码中抽取出来,交给IoC容器负责。容器全权负责组件的装配,它会把符合依赖关系的对象通过属性(JavaBean中的setter)或者是构造器传递给需要的对象。 - -#### 依赖注入有什么优势 - -依赖注入之所以更流行是因为它是一种更可取的方式:让容器全权负责依赖查询,受管组件只需要暴露JavaBean的setter方法或者带参数的构造器或者接口,使容器可以在初始化时组装对象的依赖关系。其与依赖查找方式相比,主要优势为: - -* 查找定位操作与应用代码完全无关。 -* 不依赖于容器的API,可以很容易地在任何容器以外使用应用对象。 -* 不需要特殊的接口,绝大多数对象可以做到完全不必依赖容器。 - -#### 有哪些不同类型的依赖注入实现方式? - -依赖注入是时下最流行的IoC实现方式,依赖注入分为接口注入(Interface Injection),Setter方法注入(Setter Injection)和构造器注入(Constructor Injection)三种方式。其中接口注入由于在灵活性和易用性比较差,现在从Spring4开始已被废弃。 - -**构造器依赖注入**:构造器依赖注入通过容器触发一个类的构造器来实现的,该类有一系列参数,每个参数代表一个对其他类的依赖。 - -**Setter方法注入**:Setter方法注入是容器通过调用无参构造器或无参static工厂 方法实例化bean之后,调用该bean的setter方法,即实现了基于setter的依赖注入。 - -## Spring AOP,动态 - -**看ThinkWon博客** - -## Bean生命周期 - - - - - -* Bean 容器找到配置文件中 Spring Bean 的定义,Bean 容器利用 Java Reflection API 创建一个Bean的实例。 -* 如果涉及到一些属性值 利用set方法设置一些属性值。 -* 如果 Bean 实现了 `BeanNameAware` 接口,调用 `setBeanName()`方法,传入Bean的名字。 -* (如果 Bean 实现了 `BeanClassLoaderAware` 接口,调用 `setBeanClassLoader()`方法,传入 `ClassLoader`对象的实例。) -* 与上面的类似,如果实现了其他 `*.Aware`接口,就调用相应的方法。比如: - - 1. 如果bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入; - - 2. 如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext()方法,将bean所在的应用上下文的引用传入进来; -* **如果bean实现了BeanPostProcessor接口**(如果有和加载这个 Bean 的 Spring 容器相关的 `BeanPostProcessor` 对象),执行`postProcessBeforeInitialization()` 方法 -* 如果Bean实现了`InitializingBean`接口,执行`afterPropertiesSet()`方法。 -* 如果bean使用initmethod声明了初始化方法,该方法也会被调用; -* **如果bean实现了BeanPostProcessor接口**(如果有和加载这个 Bean的 Spring 容器相关的 `BeanPostProcessor` 对象),执行`postProcessAfterInitialization()` 方法 -* 此时,bean已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁; -* 当要销毁 Bean 的时候,如果 Bean 实现了 `DisposableBean` 接口,执行 `destroy()` 方法。 -* 当要销毁 Bean 的时候,同样,如果bean使用destroy-method声明了销毁方法,该方法也会被调用。 - - - -BeanPostProcessor作用:https://www.jianshu.com/p/369a54201943 - -## Bean作用域?默认什么级别?是否线程安全?Spring如何保障线程安全的? - -### Bean作用域 - -* singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。 -* prototype : 每次请求(将其注入到另一个bean中,或者以程序的方式调用容器的 getBean()方法)都会创建一个新的 bean 实例。 -* request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。 -* session : 在一个HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。。 -* global-session: 在一个全局的HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。 - - - -### 默认级别 - -singleton(单例) - -### 是否线程安全 - -不是,Spring框架中的单例bean不是线程安全的 - -### Spring如何解决线程安全 - -在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域,因为Spring对一些Bean中非线程安全状态采用ThreadLocal进行处理,解决线程安全问题。 - -* 有状态就是有数据存储功能。 -* 无状态就是不会保存数据。 - -(实际上大部分时候 spring bean 无状态的(比如 dao 类),所有某种程度上来说 bean 也是安全的,但如果 bean 有状态的话(比如 view model 对象),那就要开发者自己去保证线程安全了,最简单的就是改变 bean 的作用域,把“singleton”变更为“prototype”,这样请求 bean 相当于 new Bean()了,所以就可以保证线程安全了。) - -## Spring事务隔离级别和事务传播属性 - -**支持当前事务的情况:** - -* **TransactionDefinition.PROPAGATION_REQUIRED:** 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。 -* **TransactionDefinition.PROPAGATION_SUPPORTS:** 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 -* **TransactionDefinition.PROPAGATION_MANDATORY:** 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性) - -**不支持当前事务的情况:** - -* **TransactionDefinition.PROPAGATION_REQUIRES_NEW:** 创建一个新的事务,如果当前存在事务,则把当前事务挂起。 -* **TransactionDefinition.PROPAGATION_NOT_SUPPORTED:** 以非事务方式运行,如果当前存在事务,则把当前事务挂起。 -* **TransactionDefinition.PROPAGATION_NEVER:** 以非事务方式运行,如果当前存在事务,则抛出异常。 - -**其他情况:** - -* **TransactionDefinition.PROPAGATION_NESTED:** 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则创建一个新的事务。 - - - -### 补充 - -脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。 -不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。 -幻读(Phantom Read):同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录,但是第二次同等条件下查询却有 n+1 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了 - - - -具体讲解:https://blog.csdn.net/qq_35433593/article/details/86094028 - - - -## Spring以及Spring MVC常见注解 - -https://blog.csdn.net/hsf15768615284/article/details/81623881 - -## @autowired和@resource的区别,当UserDao存在不止一个bean或没有存在时,会怎样?怎么解决? - -### 区别 - -@Autowired可用于:构造函数、成员变量、Setter方法 -@Autowired和@Resource之间的区别 - -- @Autowired默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。 - -- @Resource默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入。 - -### 会怎么样 - -@Autowired是根据类型进行自动装配的。如果当Spring上下文中存在不止一个UserDao类型的bean时,就会抛出BeanCreationException异常;如果Spring上下文中不存在UserDao类型的bean,也会抛出BeanCreationException异常。我们可以使用@Qualifier配合@Autowired来解决这些问题。如果我们想使用名称装配可以结合@Qualifier注解进行使用,如下: - - - -再不懂的话看这里:https://www.cnblogs.com/aspirant/p/10431029.html - - - - - - - -## SpringBoot自动配置的原理是什么?介绍SpringBootApplication注解. - -### 原理是什么 - -> 1.Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到所有jar包中META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载。 -> -> 2.而这些自动配置类都是以AutoConfiguration结尾来命名的,xxxAutoConfiguration配置类中,通过@EnableConfigurationProperties注解,取得xxxProperties类在全局配置文件中配置的属性(如:server.port) -> -> 3.而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。 - -具体讲解:https://blog.csdn.net/u014745069/article/details/83820511 - -https://github.com/Snailclimb/springboot-guide/blob/master/docs/interview/springboot-questions.md - -### SpringBootApplication注解 - -1. @SpringBootApplication是一个复合注解或派生注解,在@SpringBootApplication中有一个注解@EnableAutoConfiguration,字面意思就是**开启自动配置** -2. `@EnableAutoConfiguration` 注解通过Spring 提供的 `@Import` 注解导入了`AutoConfigurationImportSelector`类 -3. AutoConfigurationImportSelector的selectImports()方法通过SpringFactoriesLoader.loadFactoryNames()扫描所有具有META-INF/spring.factories的jar包。spring-boot-autoconfigure-x.x.x.x.jar里就有一个这样的spring.factories文件。 -4. 这个spring.factories文件也是一组一组的key=value的形式,其中一个key是EnableAutoConfiguration类的全类名,而它的value是一个xxxxAutoConfiguration的类名的列表,这些类名以逗号分隔。将所有自动配置类加载到Spring容器中,用他们来做自动配置。 - - - -## @Transactional注解加载和运行机制? - - - -### 事务的原理 - -1、Spring事务 的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。 - -2、@Transactional注解可以帮助我们把事务开启、提交或者回滚的操作,,免去了重复的事务管理逻辑。是通过aop的方式进行管理. - -3、Spring AOP中对一个方法进行代理的话,肯定需要定义切点。在@Transactional的实现中,同样如此,spring为我们定义了以 @Transactional 注解为植入点的切点,这样才能知道@Transactional注解标注的方法需要被代理。@Transactional的作用一个就是标识方法需要被代理,一个就是携带事务管理需要的一些属性信息。 - -4、bean在进行实例化的时候会判断适配了BeanFactoryTransactionAttributeSourceAdvisor(也就是调用的方法是否适配了切面) - -5、如果没有适配的话,就返回原对象给IOC容器 - -6、如果适配了的话就会创建代理对象返回给IOC容器。AOP动态代理时,开始执行的方法是DynamicAdvisedInterceptor#intercept【这个方法只要是aop都会执行,下面的TransactionInterceptor相当于其具体的实现】。最终执行的方法是TransactionInterceptor#invoke方法。并且把CglibMethodInvocation注入到invoke方法中,CglibMethodInvocation就是包装了目标对象的方法调用的所有必须信息,因此,在TransactionInterceptor#invoke里面也是可以调用目标方法的,并且还可以实现类似@Around的逻辑,在目标方法调用前后继续注入一些其他逻辑,比如事务管理逻辑。 - -7、TransactionInterceptor内部依赖于TransactionManager,TransactionManager是实际的事务管理对象 - - - - - - - -## Spring中用到了哪些设计模式?单例、***、工厂、适配、观察者之类的说一说就行 - -- 工厂设计模式:Spring使用工厂模式可以通过 `BeanFactory` 或 `ApplicationContext` 创建 bean 对象。 -- 单例设计模式:Spring 中 bean 的默认作用域就是 singleton(单例)的。 -- 代理设计模式:Spring AOP 就是基于代理的,AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度。 -- 观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。常用的地方是listener的实现,如ApplicationListener。 -- 适配器模式:适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。 - - Spring AOP 的增强或通知(Advice)使用到了适配器模式,Springmvc也用到了 - -- 模板方法模式:JdbcTemplate中的execute方法 -- 包装器模式:转换数据源 - - - -讲解的地方:https://blog.csdn.net/qq_34337272/article/details/90487768 - -https://www.jianshu.com/p/ace7de971a57 - - - - - -## Spring由哪些模块组成? - -看ThinkWon博客 - - - - - -## Spring中的代理 - -### Spring AOP and AspectJ AOP 有什么区别?AOP 有哪些实现方式? - -AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。 - -(1)AspectJ是静态代理的增强,所谓静态代理,就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强,他会在编译阶段将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。 - -(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。 - -### JDK动态代理和CGLIB动态代理的区别 - -Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理: - -- JDK动态代理只提供接口的代理,不支持类的代理。核心InvocationHandler接口和Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。 - -- 如果代理类没有实现 InvocationHandler 接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。(CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。)CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。 - -静态代理与动态代理区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。 - - - - -## IOC循环依赖及其解决方案 - -### 1.什么是循环依赖 - -https://www.cnblogs.com/java-chen-hao/p/11139887.html - -https://blog.csdn.net/qq_36381855/article/details/79752689 - - - - - -### 2.如何检测循环依赖 - -可以 Bean在创建的时候给其打个标记,如果递归调用回来发现正在创建中的话--->即可说明循环依赖。 - - - -### 3.Spring如何解决循环依赖 - -> https://blog.csdn.net/f641385712/article/details/92801300 - -#### 总结 - -```java -/** Cache of singleton objects: bean name --> bean instance */ -private final Map singletonObjects = new ConcurrentHashMap(256); - -/** Cache of singleton factories: bean name --> ObjectFactory */ -private final Map> singletonFactories = new HashMap>(16); - -/** Cache of early singleton objects: bean name --> bean instance */ -private final Map earlySingletonObjects = new HashMap(16); -``` - - - -| 缓存 | 用途 | -| --------------------- | ------------------------------------------------------------ | -| singletonObjects | 用于存放完全初始化好的 bean,从该缓存中取出的 bean 可以直接使用 | -| earlySingletonObjects | 存放原始的 bean 对象(尚未填充属性),用于解决循环依赖 | -| singletonFactories | 存放 bean 工厂对象,用于解决循环依赖 | - -1.他们就是 Spring 解决 singleton bean 的关键因素所在,我称他们为三级缓存,第一级为 singletonObjects,第二级为 earlySingletonObjects,第三级为 singletonFactories。 - -2.Spring 在创建 bean 的时候并不是等它完全完成,而是在创建过程中将创建中的 bean 的 ObjectFactory 提前曝光(即加入到 singletonFactories 缓存中),这样一旦下一个 bean 创建的时候需要依赖 bean ,从三级缓存中拿到ObjectFactory,然后直接使用 ObjectFactory 的 `getObject()` 获取bean,也就是 `getSingleton()`中的代码片段了。 - - - - - -## Springmvc原理流程 - -> 参考:https://blog.nowcoder.net/n/31217f1bdf644371842a1bc85f1f4987 - -**流程说明(重要):** - -1、用户请求发送至 DispatcherServlet 类进行处理 - -2、DispatcherServlet 类调用HandlerMapping 类请求查找 Handler - -3、HandlerMapping 类根据 request 请求的 URL 等信息,以及相关拦截器 interceptor ,查找能够进行处理的 Handler最后给DispatcherServlet(前端控制器) 返回一个执行链,也就是一个HandlerExecutionChain - -4、前端控制器请求(适配器)HandlerAdapter 执行 Handler(也就是常说的controller控制器) - -5-7、HandlerAdapter 执行相关 Handler ,并获取 ModelAndView 类的对象。最后将ModelAndView 类的对象返回给前端控制器 - -8、DispatcherServlet 请求 ViewResolver 类进行视图解析。 - -9、ViewResolver 类进行视图解析获取 View 对象,最后向前端控制器返回 View 对象 - -10、DispatcherServlet 类进行视图 View 渲染,填充Model - -11、DispatcherServlet 类向用户返回响应。 - - - - - -## 关于interceptor与Filter区别 - - - -| 属性 | 拦截器Interceptor | 过滤器Filter | -| ------------ | ------------------------------------------------------------ | -------------------------------------------- | -| 原理 | 基于java的反射机制 | 基于函数回调 | -| 创建 | (在context.xml中配置)由Spring容器初始化。 | (在web.xml中配置filter基本属性)由web容器创建 | -| servlet 容器 | 拦截器不直接依赖于servlet容器 | 过滤器依赖于servlet容器 | -| 作用对象 | 拦截器只能对action请求起作用 | 过滤器则可以对几乎所有的请求起作用 | -| 访问范围 | 拦截器可以访问action上下文、值栈里的对象,可以获取IOC容器中的各个bean。 | 不能 | -| 使用场景 | 即可用于Web,也可以用于其他Application | 基于Servlet规范,只能用于Web | -| 使用选择 | 可以深入到方法执行前后,使用场景更广 | 只能在Servlet前后起作用 | -| | 在Action的生命周期中,拦截器可以多次调用 | 而过滤器只能在容器初始化时被调用一次。 | - - - - - -### 什么是action请求 - -就是经过controller的请求,直接输入页面路径不拦截? http://localhost:8080/admin/index/index.html这样的话,就不走拦截器,直接跳转到页面上了。 - - - -## IOC容器初始化流程 - -> 简单的总结:https://cxis.me/2020/03/22/Spring%E4%B8%ADIOC%E5%AE%B9%E5%99%A8%E7%9A%84%E5%88%9D%E5%A7%8B%E5%8C%96%E8%BF%87%E7%A8%8B%E6%80%BB%E7%BB%93/ - -1. Spring启动。 -2. 加载配置文件,xml、JavaConfig、注解、其他形式等等,将描述我们自己定义的和Spring内置的定义的Bean加载进来。 -3. 加载完配置文件后将配置文件转化成统一的Resource来处理。 -4. 使用Resource解析将我们定义的一些配置都转化成Spring内部的标识形式:BeanDefinition。 -5. 在低级的容器BeanFactory中,到这里就可以宣告Spring容器初始化完成了,Bean的初始化是在我们使用Bean的时候触发的;在高级的容器ApplicationContext中,会自动触发那些lazy-init=false的单例Bean,让Bean以及依赖的Bean进行初始化的流程,初始化完成Bean之后高级容器也初始化完成了。 -6. 在我们的应用中使用Bean。 -7. Spring容器关闭,销毁各个Bean。 - - - -> 基本没碰到过问的 - - - - - -# Mybatis - -## SQL注入 - -https://blog.csdn.net/qq_41246635/article/details/81392818 - -## Mybatis中#与$的区别 - -https://www.cnblogs.com/PoetryAndYou/p/11622334.html - -https://blog.csdn.net/j04110414/article/details/78914787 - - - -# 场景题 - -## 如何设计一个秒杀系统 - -### 秒杀系统的难点 - -首先我们先看下秒杀场景的难点到底在哪?在秒杀场景中最大的问题在于容易产生大并发请求、产生超卖现象和性能问题,下面我们分别分析下下面这三个问题: - -1)瞬时大并发:一提到秒杀系统给人最深刻的印象是超大的瞬时并发,这时你可以联想到小米手机的抢购场景,在小米手机抢购的场景一般都会有10w+的用户同时访问一个商品页面去抢购手机,这就是一个典型的瞬时大并发,如果系统没有经过限流或者熔断处理,那么系统瞬间就会崩掉,就好像被DDos攻击一样; - -2)超卖:秒杀除了大并发这样的难点,还有一个所有电商都会遇到的痛,那就是超卖,电商搞大促最怕什么?最怕的就是超卖,产生超卖了以后会影响到用户体验,会导致订单系统、库存系统、供应链等等,产生的问题是一系列的连锁反应,所以电商都不希望超卖发生,但是在大并发的场景最容易发生的就是超卖,不同线程读取到的当前库存数据可能下个毫秒就被其他线程修改了,如果没有一定的锁库存机制那么库存数据必然出错,都不用上万并发,几十并发就可以导致商品超卖; - -3)性能:当遇到大并发和超卖问题后,必然会引出另一个问题,那就是性能问题,如何保证在大并发请求下,系统能够有好的性能,让用户能够有更好的体验,不然每个用户都等几十秒才能知道结果,那体验必然是很糟糕的; - -4)黄牛:你这么低的价格,假如我抢到了,我转手卖掉我不是**血赚**?就算我不卖我也不亏啊,那用户知道,你知道,别的别有用心的人(黑客、黄牛…)肯定也知道的。 - -那简单啊,我知道你什么时候抢,我搞个几十台机器搞点脚本,我也模拟出来十几万个人左右的请求,那我是不是意味着我基本上有80%的成功率了。 - -5)F12链接提前暴露 - - - - - -从整个秒杀系统的架构其实和一般的互联网系统架构本身没有太多的不同,核心理念还是通过缓存、异步、限流来保证系统的高并发和高可用。下面从一笔秒杀交易的流程来描述下秒杀系统架构设计的要点: - -1)对于大秒杀活动,一般运营会配置静态的活动页面,配置静态活动页面主要有两个目的一方面是为了便于在各种社交媒体分发,另一方面是因为秒杀活动页的流量是大促期间最大的,通过配置成静态页面可以将页面发布在公有云上动态的横向扩展; - -2)将秒杀活动的静态页面提前刷新到CDN节点,通过CDN节点的页面缓存来缓解访问压力和公司网络带宽,CDN上缓存js、css和图片;或者Nginx服务器提供的静态资源功能。 - -3)将秒杀服务部署在公有云的web server上,使用公有云最大的好处就是能够根据活动的火爆程度动态扩容而且成本较低,同时将访问压力隔离在公司系统外部; - -4)在提供真正商品秒杀业务功能的app server上,需要进行交易限流、熔断控制,防止因为秒杀交易影响到其他正常服务的提供。 - -5)服务降级处理,除了上面讲到的限流和熔断控制,我们还设定了降级开关,对于首页、购物车、订单查询、大数据等功能都会进行一定程度的服务降级。比如进行秒杀的时候,将订单查询系统进行降级,减少秒杀压力 - -6)如何防止超卖现象的发生,我们日常的下单过程中防止超卖一般是通过在数据库上加锁来实现。但是还是无法满足秒杀的上万并发需求,我们的方案其实也很简单实时库存的扣减在缓存中进行,异步扣减数据库中的库存,保证缓存中和数据库中库存的最终一致性。 - -7)库存预热:那不简单了,我们要开始秒杀前你通过定时任务或者运维同学**提前把商品的库存加载到Redis中**去,让整个流程都在Redis里面去做,然后等秒杀介绍了,再异步的去修改库存就好了。 - -8)MQ削峰填谷:你买东西少了你直接100个请求改库我觉得没问题,但是万一秒杀一万个,10万个呢? - -你可以把它放消息队列,然后一点点消费去改库存就好了嘛,不过单个商品其实一次修改就够了,我这里说的是**某个点多个商品**一起秒杀的场景,像极了双十一零点。 - - - -1. 使用缓存。 -2. 页面静态化技术。 -3. 数据库优化。 -4. 分类数据库中活跃的数据。 -5. 批量读取和延迟修改。 -6. 读写分离。 - -### 如何解决高并发秒杀的超卖问题 - - - -**由秒杀引发的一个问题** - -* 秒杀最大的一个问题就是解决超卖的问题。**其中一种解决超卖如下方式:** - -``` -1 update goods set num = num - 1 WHERE id = 1001 and num > 0 -``` - - - -我们假设现在商品只剩下一件了,此时数据库中 **num = 1;** - - - -但有100个线程同时读取到了这个 **num = 1**,所以100个线程都开始减库存了。 - - - -但你会最终会发觉,**其实只有一个线程减库存成功,其他99个线程全部失败。** - - - -为何? - - - -**这就是MySQL中的排他锁起了作用。** - - - -排他锁又称为写锁,简称X锁,顾名思义,排他锁就是不能与其他所并存,**如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁**,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。 - -就是类似于我在执行update操作的时候,这一行是一个事务**(默认加了排他锁**)。**这一行不能被任何其他线程修改和读写** - - - - - -* **第二种解决超卖的方式如下** - -``` -1 select version from goods WHERE id= 1001 -2 update goods set num = num - 1, version = version + 1 WHERE id= 1001 AND num > 0 AND version = @version(上面查到的version); -``` - - - - - -这种方式采用了**版本号**的方式,其实也就是**CAS**的原理。 - - - -假设此时version = 100, num = 1; 100个线程进入到了这里,同时他们select出来版本号都是version = 100。 - - - -然后直接update的时候,只有其中一个先update了,同时更新了版本号。 - - - -那么其他99个在更新的时候,会发觉version并不等于上次select的version,就说明version被其他线程修改过了。那么我就放弃这次update - - - -* **第三种解决超卖的方式如下** - - 利用redis的单线程预减库存。比如商品有100件。那么我在redis存储一个k,v。例如 - - - -每一个用户线程进来,key值就减1,等减到0的时候,全部拒绝剩下的请求。 - - - -那么也就是只有100个线程会进入到后续操作。所以一定不会出现超卖的现象 - - - -* 总结 - - - -可见第二种CAS是失败重试,并无加锁。应该比第一种加锁效率要高很多。**类似于Java中的Synchronize和CAS**。 - - - - -# 电商项目面试点 - -## 说明 - -> 2019年5月开始做的电商,糅合了2020的几家电商项目,总结一下面试点。 -> -> 1. 关于redis,rocketmq的知识点。还需要看过专门的知识才能整理的比较全 - -**参考:** - -https://blog.csdn.net/qq_41618510/article/details/83280653 - -https://blog.51cto.com/13517854/2073947 - - - - - -## 1.架构图 - -![架构图](https://npm.elemecdn.com/youthlql@1.0.12/image/001.png) - - - -## 2.OMS,PMS,UMS,WMS什么意思 - -OMS:订单系统,也就是订单相关的表 - -PMS:商品数据结构 - -UMS:用户数据结构 - -WMS:仓库,库存数据结构 - - - -## 3.SKU和SPU相关 - -> 参考:2019版-04 谷粒商品pms.docx - -### 1.什么是SPU、SKU? - -1. 简单的说: SPU就是一个iPhone6s, SKU就是银色iPhone6s、粉色iPhone6s - -2. 通俗点讲,属性值、特性相同的商品就可以称为一个SPU - -3. SKU是物理上不可分割的最小存货单元 - - - -### 2.为什么将SKU抽取出来? - -比如,咱们购买一台iPhoneX手机,iPhoneX手机就是一个SPU,但是你购买的时候,不可能是以iPhoneX手机为单位买的,商家也不可能以iPhoneX为单位记录库存。必须要以什么颜色什么版本的iPhoneX为单位。比如,你购买的是一台银色、128G内存的、支持联通网络的iPhoneX ,商家也会以这个单位来记录库存数。那这个更细致的单位就叫库存单元(SKU)。 - - - -### 3.SPU解决的是什么问题? - -1. 一般的电商系统你点击进去以后,都能看到这个商品关联了其他好几个类似的商品,而且这些商品很多的信息都是共用的,比如商品图片,海报、销售属性等。 -2. 那么系统是靠什么把这些sku识别为一组的呢,那是这些sku都有一个公用的spu信息。而它们公共的信息,都放在spu信息下,更方便管理。类似于面向对象的封装,把公共的信息封装起来。 - - - -## 4.PMS基本属性 - -> 参考:2019版-04 谷粒商品pms.docx - -### 1.什么是平台属性? - - - -![数据结构之平台属性](https://npm.elemecdn.com/youthlql@1.0.12/image/002.png) - -- 比如电脑整机的一级分类下,有笔记本、游戏本、台式机、一体机的二级分类。笔记本这个二级分类又包含了处理器、屏幕尺寸、内存容量、硬盘容量、显卡类别这些属性。那么针对联想某个型号的笔记本,它作为笔记本这种分类,每个分类属性都有对应的值,cpu(属性)是i7(属性值)的,内存(属性)是8G(属性值)的,屏幕尺寸(属性)是14寸(属性值)的。 - -- 所以注意一点,平台属性的外键是三级分类id,在使用平台属性功能之前必须选择三级分类。 - - - -### 2.什么是销售属性? - - - -![数据结构之销售属性](https://npm.elemecdn.com/youthlql@1.0.12/image/003.png) - - - -### 3.销售属性与平台属性的关系? - -![数据结构](https://npm.elemecdn.com/youthlql@1.0.12/image/004.png) - - - -## 5.申请对接支付宝的流程? - - - -### 1.打开支付宝网站开放平台(open.alipay.com),并点击下方的网络/移动应用列表 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/094.png) - -### 2.点击支付接入,并填写相关信息 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/095.png) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/096.png) - -### 3.接入电脑网站支付 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/097.png) - -### 4.进行签约 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/098.png) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/099.png) - -### 5.代码流程 - -①引入SDK - -②进行支付宝的相关配置 - -``` java -package com.atguigu.atcrowdfunding.app.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.front.vo.pay.PayVo; -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@ConfigurationProperties(prefix = "alipay") -@Component -@Data -public class AlipayTemplate { - - //在支付宝创建的应用的id - private String app_id = "2016092200568607"; - - // 商户私钥,您的PKCS8格式RSA2私钥 - private String merchant_private_key = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCSgX/nTQ0lD+S8ObaM5LGZ1hiz18GXnNpqPLhJCym4xOpn35FNPHrPkDGEoMKrZ5LJeA4cZulckD8AtpvBCpeyIkrj/i1WVmSg10hVX67MlVets4UecCHZv2hKAN0/iId76kozdqrd7Csp/YgXPquN9Np0NFotggTrmiBANk+vcpTF9SCGrDq/isOoCvClfbvVJjApfLLOel3yECe5K/SZ8puiWILVm1NxEXAqJ8z0ipPZVGrXsT6Bo0pEyCPcEL0SqaC9WT0zdWQzdUknCzZV9W2wKjEXBJG9hqxay5kPaKm9leBatSkDAaDxH/N5g36HRfY7BmklwRZsp17lHinxAgMBAAECggEAfnnfck35WBKFc90a9D0F+Xlzr+ZGEV3uzKIIsb46UXFlrzC5HoVkvEWOCiJCjHiIpvbGr8xED43TZgk/IwLC/JxQLM0kVJGWo6fWoSVOIP2YSLNe620APBvaq3BdkFiMJfSYBB+g2J7mkIR39SE8Nvu3j3QWmYzSNJbE2spINnwTzNBL1OPaB5h3hSjyI07KaUcOjhTBF0EZl83NlBDsxmQvy0NmuOIWAcIXXvGoIbwkA774J3LhwL+VS4W2FpQj4FlxvDlPu24GeNWN7oO66T3Jp9bweO120ObhuKwZQosDGkJq0975zVSJX5QtUWHMM/QDPO8Pk24n2AoPcACQcQKBgQDS6kqD+sK8dDBpkmxYopA1gJJATnur0RHFZJb5webOhnEZnePhB1hhhGvKFcrdY2hcYeQiUZkHMsnWItNUe9E9ccp4++m6KKG0iV/BQda7zx1zMTTZUMvSbO282Q31YnQu7Yz6BSk4f/U5Qbu61AK53Tv1ejSAgQhXt1Pwq8KD7QKBgQCx0pkqW4+53tY2o4iPqFGjKYI2yk5bAH5etmOvW51OZ4Slsq/aUJKBVG6fOpRVKkiXulHhrp5csZH0/C7kaj4Hy7TjgUKSWvwlv7i7jgN0dq/bhVJz82y+N9pENWvy5J0I8Kt67XH+6JDEGWjlV58auifMRSx5mRJNn5pM6qrFlQKBgFyZWm/JV1fv1xVyoLjlXlTvBsbO7kMH/jpgqFwtAk1n/x3VEShJ1kayIbTOjotWSopMvCFJG9tqM+0cyxWLatkELXWifAIsNpqRuYWah1FbZD2fu+kxLNtM0a+YyCUUvZeg2cUnIOraWupxbp9e13eMpvdmWMiWXfhM18CRWEwdAoGAUwT0l076EhgUQJwm1JML0jY94eCfpmLbnNJgRe1qysEPr+B1s2IslA7cOqC5we0kyRmmwsuoibQpZYwbRG7JmRAk2pZtgzDRSbpxv7a0rDoBLmbXMOU0Hraqw2+Bf3v2SMc79/9FWnIvrC4EyBYZZPwGOpsNAZRSdEUQX9qrceUCgYB99OOtFFt1ixzyTCyUj3Fuiw7BsPhdI3nuMSoNTPIDNpzRBp/KFXyv/FNJ2CjTAsX3OR3D6KmEYihqUfrYeb0P5zoybcQLMxbXxK+ec6F2o6U2iqFIq0MKwHUqsb9X3pj4qE0ZHbFgRtIHnL2/QGV5PFJdmIZIBKZcvB8fW6ztDA=="; - // 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。 - private String alipay_public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyQQceVUChTJGtF/a8SXufhSxDTKporieTq9NO7yDZSpDlAX1zVPT/nf0KWAlxq1TYappWMIYtyrOABhJyn6flNP6vuSBiM5lYsepHvYrtRHqlFiJruEkiaCgEZBKL5aCfBHYj0oqgQn9MpNV/PEH4cBYAVaiI4+VX8CBUQfeEGjgN6OkpLULZ3X0JUkmSnVvCNJ1m3PD68IIlbOfEZXJUKCqmZhzprGR5VWswjxA+g87cMwvijL4gdkSy/daG62Bz5vApcmmMkuX1k1fMWP4ajZCASVw8HD+MSLRhd8We9F97gd8CW0TavzbdR+mTS5H4yEgO8F9HRAsbkhV9yu0yQIDAQAB"; - // 服务器[异步通知]页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 - // 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息 - private String notify_url; - - // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 - //同步通知,支付成功,一般跳转到成功页 - private String return_url; - - // 签名方式 - private String sign_type = "RSA2"; - - // 字符编码格式 - private String charset = "utf-8"; - - // 支付宝网关; https://openapi.alipaydev.com/gateway.do - private String gatewayUrl = "https://openapi.alipaydev.com/gateway.do"; - - public String pay(PayVo vo) throws AlipayApiException { - - //AlipayClient alipayClient = new DefaultAlipayClient(AlipayTemplate.gatewayUrl, AlipayTemplate.app_id, AlipayTemplate.merchant_private_key, "json", AlipayTemplate.charset, AlipayTemplate.alipay_public_key, AlipayTemplate.sign_type); - //1、根据支付宝的配置生成一个支付客户端 - AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl, - app_id, merchant_private_key, "json", - charset, alipay_public_key, sign_type); - - //2、创建一个支付请求 //设置请求参数 - 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 +"\"," - + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}"); - - String result = alipayClient.pageExecute(alipayRequest).getBody(); - - //会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面 - System.out.println("支付宝的响应:"+result); - - return result; - - } -} - -``` - - - -## 6.用户下单到完成支付的执行流程? - -1.用户在购物车页点击**去结算**,然后后台跳转到结算页,此结算页用于给用户确认收货地址,联系人,商品等信息。 - -- 下面这个就是结算页,也可称作订单确认页 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/114.png) - -2.后台同时会生成一个交易码(也可称作原子令牌),放在结算页(订单确认页) - -3.用户点击提交订单,首先会验证交易码(原子令牌) - -- 如果验证不成功(说明用户通过浏览器回滚等原因重复提交),则直接提示错误。 -- 如果验证成功,后台创建订单项,填充订单信息(收货地址,金额等) - -4.接着进行**验价操作**,从数据查出购买商品的实际价格与页面提交的价格进行对比。如果失败,则提示商品价格出现变化,请重新确认,重定向到结算页(订单确认页)。 - -5.如果验价通过则保存订单到数据库,并远程锁定库存。(锁库存这里会用到基于MQ的最终一致性分布式事务,细问的话看**40.RokcetMQ解决订单-库存场景的分布式事务**) - -- 如下图所示,**TODO 4.远程锁库存**。如果锁定成功,就往RocketMQ里发送一条延时消息(假定的是40min的**延时消息A**,在后面解锁库存会用到) -- 接着如果订单创建成功,库存也锁成功了,之后也没有发生异常。则往RocketMQ里也发送一条延时消息(假定的是30min的**延时消息B**,在后面关闭订单的时候会用到) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/089.png) - -6.一切操作成功的话,就跳转到支付页。(支付更细的流程看**42.支付相关问题**) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/100.png) - -7.点击支付宝,进行一系类的支付信息的处理,跳转到支付宝支付页面。 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/102.png) - -8.如果支付成功,则会通过支付宝的异步回调接口,保存交易流水,修改订单支付状态等数据。接着就可以等物流了。注意我们的库存在用户没有确认收货的时候只是处于锁定状态,用户确认收货的时候才是真正的减去。 - -![1589973740556](https://npm.elemecdn.com/youthlql@1.0.12/image/109.png) - -9.如果中间出现任何的问题支付不成功,比如30min未支付,或者库存等各种异常。就会根据延迟之前的MQ延迟消息进行相应的分布式事务控制。延迟消息B先关闭订单,延迟消息A再解锁库存。(更多场景见**40-42**)。 - -## 7.用户下单成功但是没有接受到信息怎么处理?(未总结) - -## 8.订单生成的过程? - -下面图只是个参考,具体文字逻辑看**6.用户下单到完成支付的执行流程?** - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/070.png) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/071.png) - - - -## 9.添加购物车的过程? - -1.传递参数(商品skuid,添加数量) - -2.根据skuid调用skuService查询商品的详细信息 - -3.将商品详细信息封装成购物车信息 - -4.判断用户是否登录 - -5.根据用户登录决定走cookie的分支还是db - - - 用户没登录 - - - 若cookie为空,则直接加入购物车列表,然后添加cookie。 - - - 若cookie不为空,则判断此商品是否添加过。如果添加过则更新数量,没添加过则直接更新。最后更新Cookie - -- 用户登录了,则根据用户ID和商品spuid,检查此用户购物车是否有此商品 - - 若有此商品,则更新数据库数量。最后更新redis缓存 - - 若无此商品,则直接添加到数据库。最后更新redis缓存 - -## 10.Item商品详情页 - -> 参考:2019版-05 谷粒商品详情页.docx,2019版-day07-Item商品详情.docx - -### 1.从外部点进这个商品详情页后 - -**问题**: - -1. 查出该商品的spu的所有销售属性和属性值 - -2. 标识出本商品(当前sku)对应的销售属性,也就是标红 - -**解决办法:** - -1. 双重for循环,效率很低 - - 循环当前skuInfo对象的销售属性值和对应spu的销售属性值,如果相等就把返回的spu销售属性值对应的isCheck字段置为1,否则设为0。前端页面展示的就是is_check为1,框就变红。 - - 首先双重for循环,效率就低。其次,通过skuInfo对象还要再到数据库里查出此sku的所有销售属性值,多了一次数据库操作,很费时。还有一次数据库操作就是查出spu的所有属性和属性值 - -2. 巧妙的用sql,**算是一次代码优化** - - 通过当前sku对应spu的id,查出所有该spu的销售属性和属性值,并关联某skuid如果能关联上is_check设为1,否则设为0。前端页面展示的就是is_check为1,框就变红。 **这一步的sql有点复杂,但是只需要需要查一次数据库** - - 这样的话通过一次sql,在查出spu的所有属性和属性值的同时关联skuid。 - -```mysql - SELECT sa.id ,sa.spu_id, sa.sale_attr_name,sa.sale_attr_id, - sv.id sale_attr_value_id, - sv.sale_attr_value_name, - skv.sku_id, - IF(skv.sku_id IS NOT NULL,1,0) is_check - FROM spu_sale_attr sa - INNER JOIN spu_sale_attr_value sv ON sa.spu_id=sv.spu_id AND sa.sale_attr_id=sv.sale_attr_id - LEFT JOIN sku_sale_attr_value skv ON skv.sale_attr_id= sa.sale_attr_id AND skv.sale_attr_value_id=sv.id AND skv.sku_id=10 - WHERE sa.spu_id=24 -ORDER BY sv.sale_attr_id,sv.id -``` - - - -### 2.sku根据销售属性的动态切换 - -**要解决的问题:**根据销售属性切换一个sku的其他兄弟姐妹 - -**1.传统的步骤:** - -1. 页面根据销售属性的选择的组合,定位到关联的sku。具体就是通过页面被选择属性值id,得到skuId。**这一步要查一次数据库** -2. 根据skuId查询到sku对象返回到页面。 **这一步要查一次数据库** - -- 缺点:每换一次属性值,就要去数据库查skuid,很费时。 - - - -**2.一个小优化:** - -1. 在用户进入某一个spu领域后,将该spu所包含的sku们和这些sku对应的销售属性值,生成一个k是销售属性值组合,v是skuId的hash表格,放到页面上。 - - - - | key(就是对应的销售属性值) | value(类似239\|243这种组合对应的skuid) | - | :-----------------------: | :------------------------------------: | - | k:239\|243 | v:106 | - | k:239\|244 | v:107 | - | k:240\|245 | v:108 | - -**流程:**也就是在一进入任意一个sku的item页时,就把这个sku对应的spu属性和属性值全查出来。同时也利用查出来的spuid,查出此spu下的所有skuinfo,制作上述hash表。 - -**优点:**每换一个属性,不用再去数据库查skuid。直接在页面上根据hash的key找出skuid,,节省了很多次查询数据库操作。 - -## 11.Item详情页的性能优化 - -> 参考:2019版-05 谷粒商品详情页.docx,2019版-day07-Item商品详情.docx - -### 1.为什么Item详情页要进行性能优化? - -虽然咱们实现了页面需要的功能,但是考虑到该页面是被用户高频访问的,所以性能必须进行尽可能的优化。 - -- 一般一个系统最大的性能瓶颈,就是数据库的io操作。从数据库入手也是调优性价比最高的切入点。 - -- 一般分为两个层面,一是提高数据库sql本身的性能,二是尽量避免直接查询数据库。 - -- 提高数据库本身的性能首先是优化sql,包括:使用索引,减少不必要的大表关联次数,控制查询字段的行数和列数。另外当数据量巨大是可以考虑分库分表,以减轻单点压力。 - -- 重点要讲的是另外一个层面:尽量避免直接查询数据库。解决办法就是:**缓存**。缓存可以理解是数据库的一道保护伞,任何请求只要能在缓存中命中,都不会直接访问数据库。而缓存的处理性能是数据库10-100倍。 - - - -### 2.解决方案(redis) - -- 由于Redis不像数据库表那样有结构,其所有的数据全靠key进行索引,所以redis数据的可读性,全依靠key。 - -- 企业中最常用的方式就是:object:id:filed,比如:sku:1314:info。 - - - -## 12.压测和性能优化 - -> 参考:尚硅谷官方-2020谷粒电商 - -### 1. Jmeter性能压测 - -下面你这个是百度的,这个是Jmeter大致的界面 - -![这个测的是百度的](https://npm.elemecdn.com/youthlql@1.0.12/image/006.png) - - - -### 2.Jconsole和jvisualvm性能监控 - -#### jvisualvm截图 - -![jvisualvm](https://npm.elemecdn.com/youthlql@1.0.12/image/007.png) - - - -#### GC可视化: - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/008.png) - - - -### 3.性能指标 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/009.png) - - - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/010.png) - -### 4.简单压测结果 - -2020谷粒电商-视频147的压测结果。具体的硬件还不清楚 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/011.png) - -### 5.静态资源优化 - -1. 前端用的tymleaf。之前的静态资源加载都是交给了tomcat,可以用nginx进行动静分离。将静态文件交给nginx,提高微服务的性能。 - - - -## 13.首页优化获取三级分类数据 - -> 参考:尚硅谷官方-2020谷粒电商 - -### 1.优化前的业务代码 - -之前的3级分类数据获取,都是在循环里不断的查数据库。查了N多次数据库,导致性能,吞吐量很差(具体数据可以看**12.压测和性能优化-4.简单压测结果**) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/012.png) - - - -### 2.如何优化 - -将N次数据库查询,改为一次把所有分类查出来,然后再在这个基础上区分1,2,3级分类。 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/013.png) - -### 3.优化后结果 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/014.png) - - - -## 14.在你的电商系统中,什么数据适合放入缓存? - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/015.png) - -具体点的就比如: - -- 首页目录的一级,二级,三级分类:**属于**即时性和数据一致性要求不高。 -- 商品列表:**属于**访问量大,且卖家更新商品的频率不高,可以放入缓存。 -- 商品详情:**属于**访问量大,且卖家更新商品的频率不高,可以放入缓存。 - - - -## 15.说说你缓存是怎么考虑的?本地缓存和分布式缓存 - -### 1.本地缓存 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/016.png) - -最简单的本地缓存就是一个Map,单体架构下使用本地缓存没有问题。但是如果是分布式的项目,就会有很多问题。比如我们的商品服务,可能会部署10台服务器。 - -- 第一个问题就是:如果某次请求经过负载均衡进入了1号服务器,这时候一号服务器本地有缓存了。但是其他9个服务器里的服务是没有缓存的,下次经过负载均衡的请求如果到了其他9个服务上,还是要查一遍数据库。 -- 第二个问题就是:本地缓存在分布式环境下的缓存不一致很严重,道理很简单,某个更新操作只能更新这一次所请求的服务上的本地缓存,其他服务上还是旧的缓存。造成严重的数据不一致问题。 - -### 2.分布式缓存 - -常见的就是用redis做分布式缓存。 - - - -## 16.使用缓存优化目录的三级分类业务 - -### 1.代码 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/017.png) - - - -### 2.优化之后压测产生异常 - -#### 出现的问题及原因 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/018.png) - -#### 解决办法 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/019.png) - -### 3.性能比较 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/020.png) - - - -## 17.缓存出现的穿透,雪崩,击穿等问题 - -> 怎么解决看总结的redis部分 - - - -## 18.分布式锁前言问答 - -### 1.锁的时序问题 - -刚开始的时候,把放入缓存这行代码,写在了synchronized代码块之外。导致可能会有:锁释放了,但是缓存还没添加完。其它线程确认了一遍缓存还没有,就继续查数据库了。 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/021.png) - - - -解决办法就是把**结果放入缓存**这一步也锁在synchronized代码块里 - -![1589102152078](https://npm.elemecdn.com/youthlql@1.0.12/image/022.png) - -### 2.为什么不能用本地锁? - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/023.png) - -本地锁的synchronized(this)只能锁住当前服务,而在分布式环境下,同一种功能肯定是部署了多个服务,所以出现了锁不住的问题。 - - - - - -## 19.原生redis的分布式锁 - -### 1.分布式锁演化阶段 - -#### 阶段一 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/024.png) - - - -#### 阶段二 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/025.png) - - - -#### 阶段三 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/026.png) - - - -#### 阶段四 - -这个可能有点不太好理解。如果没有lua脚本提供原子性的**判断+删除**,假设我们设置的锁过期时间是10s。 - -1. a线程9.5s的时候我们redis发请求获取到锁的随机值,假设网络传输了0.3s,9.8s时redis收到请求,redis把值传到我们的服务假设网络传输用了0.5s。 -2. 锁的随机值到我们的服务时,redis里的a线程的锁已经释放了。此时b线程(其它线程)加上了锁,我们对比成功之后,会把b线程(其它线程)的锁给删了。 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/027.png) - - - -#### 阶段五 - -主要意思就是 原子加锁(set nx ex命令)和原子解锁(lua脚本) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/028.png) - - - -### 2.实际分布式锁代码 - -```Java -@Override -public PmsSkuInfo getSkuById(String skuId,String ip) { - System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"进入的商品详情的请求"); - PmsSkuInfo pmsSkuInfo = new PmsSkuInfo(); - // 链接缓存 - Jedis jedis = redisUtil.getJedis(); - // 查询缓存 - String skuKey = "sku:"+skuId+":info"; - String skuJson = jedis.get(skuKey); - - if(StringUtils.isNotBlank(skuJson)){//if(skuJson!=null&&!skuJson.equals("")) - System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"从缓存中获取商品详情"); - - pmsSkuInfo = JSON.parseObject(skuJson, PmsSkuInfo.class); - }else{ - // 如果缓存中没有,查询mysql - System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"发现缓存中没有,申请缓存的分布式锁:"+"sku:" + skuId + ":lock"); - - // 设置分布式锁 - String token = UUID.randomUUID().toString(); - String OK = jedis.set("sku:" + skuId + ":lock", token, "nx", "px", 10*1000);// 拿到锁的线程有10秒的过期时间 - if(StringUtils.isNotBlank(OK)&&OK.equals("OK")){ - // 设置成功,有权在10秒的过期时间内访问数据库 - System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"有权在10秒的过期时间内访问数据库:"+"sku:" + skuId + ":lock"); - - pmsSkuInfo = getSkuByIdFromDb(skuId); - - if(pmsSkuInfo!=null){ - // mysql查询结果存入redis - jedis.set("sku:"+skuId+":info",JSON.toJSONString(pmsSkuInfo)); - }else{ - // 数据库中不存在该sku - // 为了防止缓存穿透将,null或者空字符串值设置给redis - jedis.setex("sku:"+skuId+":info",60*3,JSON.toJSONString("")); - } - - // 在访问mysql后,将mysql的分布锁释放 - System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"使用完毕,将锁归还:"+"sku:" + skuId + ":lock"); - String lockToken = jedis.get("sku:" + skuId + ":lock"); - if(StringUtils.isNotBlank(lockToken)&&lockToken.equals(token)){ - //jedis.eval("lua");可与用lua脚本,在查询到key的同时删除该key,防止高并发下的意外的发生 - jedis.del("sku:" + skuId + ":lock");// 用token确认删除的是自己的sku的锁 - } - }else{ - // 设置失败,自旋(该线程在睡眠几秒后,重新尝试访问本方法) - System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"没有拿到锁,开始自旋"); - - return getSkuById(skuId,ip); - } - } - jedis.close(); - return pmsSkuInfo; -} -``` - - - -> - 上面的分布式锁代码是2019版的谷粒电商代码,lua脚本没有写。 -> - 下面补充了lua脚本你的方式,来自2020官方谷粒电商 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/029.png) - - - -### 3.redission框架分布式锁最终代码 - -```java -private Map> getCatalogJsonFromDbWithRedissonLock() { - - // 1.锁的名字。锁的粒度,越细越快 - // 锁的粒度:具体缓存的是某个数据,11-号商品:product-11-lock - RLock lock = redisson.getLock("CatalogJson-lock"); - lock.lock(); - Map> dataFromDb; - try { - dataFromDb = getDataFromDb(); - } finally { - lock.unlock(); - } - return dataFromDb; -} -``` - - - -## 20.redission分布式锁框架 - -### 1.为什么要使用redission? - -1. Java的JUC包,有各种类型的锁,可重入锁,读写锁,Semaphore/CountDownLatch等很多种特性的锁。但是这些统统属于本地锁,在分布式环境下基本用不了。那我们想用他们的这些特性怎么办呢?redission提供了这些各种各样的分布式同步机制,分布式同步锁。 -2. redission锁有异常自动释放,以及业务时间过长自动续期的功能。 - - - -### 2.常用的redission锁 - -#### 普通的Lock可重入锁 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/030.png) - - - -> 上面是视频截图,下面是github上别人敲的源码。 - -```Java -@ResponseBody - @GetMapping("/hello") - public String hello() { - // 1. 获取一把锁,只要锁的名字一样,就是同一把锁 - RLock lock = redisson.getLock("my-lock"); - - // 2.加锁 - lock.lock(); // 不指定时间,默认30s过期,如果代码业务没有执行完会自动续期,如果执行过程中系统崩溃,则30s后锁在redis中自动被删除 -// lock.lock(10, TimeUnit.SECONDS); // 指定时间,10s之后自动解锁。解锁时间必须大于业务时间,因为不会自动续期,否则会出现系统异常。 - // 1.如果传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间。 - // 2.如果我们未指定锁的超时时间,就使用30 * 1000【lockWatchdogTimeout看门狗的默认时间】 - // 2-1.只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】 - // internalLockLeaseTime【看门狗时间】 / 3 -> 10s续一次期; - - // 最佳实战: - // 1). lock.lock(10, TimeUnit.SECONDS)最好使用指定时间的方法,没有续期的操作,时间设置大一点。 - try { - System.out.println("加锁成功,执行业务" + Thread.currentThread().getId()); - Thread.sleep(30000); - } catch (Exception e) { - e.printStackTrace(); - } finally { - System.out.println("释放锁......." + Thread.currentThread().getId()); - lock.unlock(); - } - return "hello"; - } -``` - -常规的分布式lock锁是以hash的数据结构实现的 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/032.png) - -#### 读写锁 - -```java -@ResponseBody -@GetMapping("/write") -public String writeValue() { - String s = ""; - RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock"); - RLock rLock = readWriteLock.writeLock(); - try { - rLock.lock(); - s = UUID.randomUUID().toString(); - Thread.sleep(30000); - redisTemplate.opsForValue().set("writeValue", s); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - rLock.unlock(); - } - return s; -} - -// 保证一定能读到最新的数据,修改期间,写锁是一个排他锁(互斥锁)。读锁是一个共享锁 -// 写锁没释放读就必须等待 -// 写 + 读:等待写锁释放 -// 写 + 写:阻塞方式 -// 读 + 写:有读锁,写锁也会等待读锁释放后进行。 -// 读 + 读:相当于无锁,并发读,只会在redis中记录好,所有当前的读锁,他们都在同时加锁成功。 -@ResponseBody -@GetMapping("/read") -public String readValue() { - String s = ""; - RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock"); - RLock rLock = readWriteLock.readLock(); - rLock.lock(); - try { - s = redisTemplate.opsForValue().get("writeValue"); - } catch (Exception e) { - e.printStackTrace(); - } finally { - rLock.unlock(); - } - return s; -} -``` - -读写锁,可以看到是以hash数据结构实现的 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/031.png) - - - -#### CountDownLatch - -```java -/** - * 放假锁门 - * 5个班的全部走完,才可以锁大门 - */ -@GetMapping("/lockDoor") -@ResponseBody -public String lockDoor() throws InterruptedException { - RCountDownLatch countDownLatch = redisson.getCountDownLatch("door"); - countDownLatch.trySetCount(5); - countDownLatch.await(); - return "ok"; -} - -@GetMapping("/gogogo/{id}") -@ResponseBody -public String gogogo(@PathVariable("id") Integer id) { - RCountDownLatch countDownLatch = redisson.getCountDownLatch("door"); - countDownLatch.countDown(); - return id + "班的人都走了!"; -} -``` - - - - - -## 21.你的电商项目是怎样解决缓存数据一致性? - -### 1.一致性的两个模式以及会出现的问题? - - - - - -### 2.解决缓存一致性问题 - - - - - -## 22.缓存层SpringCache - -缺点很突出,分布式环境下不建议使用。老老实实使用原生的redis操作 - -```java -* 4. Spring-Cache的不足: -* 1). 读模式: -* 缓存穿透:查询一个null的数据。解决: cache-null-values = true -* 缓存击穿: 大量并发进来同事查询一个正好过期的数据。解决: 加锁?默认是无锁的. @Cacheable(value = {"category"}, key = "#root.method.name", sync = true)加一个本地锁,而且是读的本地锁 -* 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间. spring.cache.redis.time-to-live=xxx ms -* 2). 写模式: (缓存与数据库一致) -* 1). 读写加锁(适用于读多写少的情况) -* 2). 引入canal, 感知到mysql的更新去更新数据库 -* 3). 读多写多,直接查询数据库 -* 总结: -* 常规数据(读多写少, 即时性,一致性要求不高的数据): 完全可以使用spring-Cache -* 特殊数据: 特殊设计 -``` - - - -## 23.项目亮点:线程池与CompletableFuture异步编排 - -### 1.为什么要使用异步编排 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/035.png) - -- 异步的意思是可以用多个线程来完成这六步操作 -- 然而4,5,6的执行都依赖于1的数据。也就是说1执行完,拿到sku的基本信息,才能获得4,5,6的数据。我们在异步的同时需要保证多个线程的顺序。简称**编排**,就是把顺序编排一下。 - - - -## 24.Item商品详情页优化 - -### 1.异步编排优化 - -> 参考:官方2020谷粒电商 - -#### 没有优化前的代码 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/036.png) - - - -#### 优化后的代码 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/037.png) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/038.png) - -这样写之后,会大大提升item详情页的速度。 - - - -### 2.商品详情页放入redis缓存 - -> 参考:微服务谷粒电商_2019 - -**代码**: - -```java -@Override -public PmsSkuInfo getSkuById(String skuId,String ip) { - System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"进入的商品详情的请求"); - PmsSkuInfo pmsSkuInfo = new PmsSkuInfo(); - // 链接缓存 - Jedis jedis = redisUtil.getJedis(); - // 查询缓存 - String skuKey = "sku:"+skuId+":info"; - String skuJson = jedis.get(skuKey); - - if(StringUtils.isNotBlank(skuJson)){//if(skuJson!=null&&!skuJson.equals("")) - System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"从缓存中获取商品详情"); - - pmsSkuInfo = JSON.parseObject(skuJson, PmsSkuInfo.class); - }else{ - // 如果缓存中没有,查询mysql - System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"发现缓存中没有,申请缓存的分布式锁:"+"sku:" + skuId + ":lock"); - - // 设置分布式锁 - String token = UUID.randomUUID().toString(); - String OK = jedis.set("sku:" + skuId + ":lock", token, "nx", "px", 10*1000);// 拿到锁的线程有10秒的过期时间 - if(StringUtils.isNotBlank(OK)&&OK.equals("OK")){ - // 设置成功,有权在10秒的过期时间内访问数据库 - System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"有权在10秒的过期时间内访问数据库:"+"sku:" + skuId + ":lock"); - - pmsSkuInfo = getSkuByIdFromDb(skuId); - - if(pmsSkuInfo!=null){ - // mysql查询结果存入redis - jedis.set("sku:"+skuId+":info",JSON.toJSONString(pmsSkuInfo)); - }else{ - // 数据库中不存在该sku - // 为了防止缓存穿透将,null或者空字符串值设置给redis - jedis.setex("sku:"+skuId+":info",60*3,JSON.toJSONString("")); - } - - // 在访问mysql后,将mysql的分布锁释放 - System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"使用完毕,将锁归还:"+"sku:" + skuId + ":lock"); - String lockToken = jedis.get("sku:" + skuId + ":lock"); - if(StringUtils.isNotBlank(lockToken)&&lockToken.equals(token)){ - //jedis.eval("lua");可与用lua脚本,在查询到key的同时删除该key,防止高并发下的意外的发生 - jedis.del("sku:" + skuId + ":lock");// 用token确认删除的是自己的sku的锁 - } - }else{ - // 设置失败,自旋(该线程在睡眠几秒后,重新尝试访问本方法) - System.out.println("ip为"+ip+"的同学:"+Thread.currentThread().getName()+"没有拿到锁,开始自旋"); - - return getSkuById(skuId,ip); - } - } - jedis.close(); - return pmsSkuInfo; -} -``` - -商品详情页属于用户访问量大,且卖家更新少,即时性要求不高的数据 - - - -## 25.缓存总结 - -### 1.在你的电商系统中实际哪些数据放入到了缓存中? - -三级分类目录,商品详情页。商品列表(可说可不说,能圆话就说,觉得圆不了就不说) - - - -### 2.分布式锁其它的实现方式?(待完善) - -我们用的是redis,还有zookeeper也可以。 - - - - - -##+.注意点(待完善) - -- 分布式锁是为了锁数据库,是为了防止数据库被击穿。 - - - -## 26.分布式session - -### 1.有什么问题? - - - -- Session不同步:是指同一个服务的多个实例(部署在多个服务器上的多个实例)之间,session无法同步 - -- Session不共享:是指不同服务之间,域名不同,cookie存的JsessionID无法共享。 - - - 注意一个问题:即使cookie的domain都搞成了相同的域名。用户A在会员服务登录了,session也只会存在会员服务的服务器上(具体就是存在tomcat里)。订单服务拿到了cookie里的jsessionID,由于订单服务的服务器上没有存A的session,订单服务拿到了jsessionID也没用。 - - - 所以要先解决同步问题,也就是保证所有服务都能访问到已存在的session(不管你是存在哪里)。然后再来解决cookie跨域问题。 - - - -### 2.常见的解决办法 - - - -#### 服务器之间Session复制 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/040.png) - - - -#### Session存到客户端 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/041.png) - - - -#### Session进行hash - -大致意思就是,根据用户的ip或者某个特殊字段,通过nginx将其hash到固定的服务器上。 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/042.png) - - - -#### Session统一存储 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/043.png) - - - -## 27.SpringSession+redis解决分布式session - -> 这个方法采用的就是Session统一存储 - -- 首先将Session全部存到redis,这样所有的服务都可以访问到,解决了session同步的问题 - -- 其次用SpringSession设置domain,解决session跨域问题。 - -### 1.用法 - -#### 主要配置 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/044.png) - -domain的域名设置成了最大的域名gulimall.com。这样可以看到下面的效果,在auth.gulimall.com域名下登录后,cookie的domain是.gulimall.com。 - -#### 效果 - -![1589355305838](https://npm.elemecdn.com/youthlql@1.0.12/image/045.png) - -其实代码什么都必须要改。我们配置了一些东西,直接调用session.setAttribute(),就会自动把session存到redis - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/046.png) - - - -### 2.原理 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/047.png) - - - -## 28.单点登录 - -> baidu.com是一级域名, xxx.baidu.com格式的都是二级域名;xxx.xxx.baidu.com格式的都是三级域名 - -### - - - -### 1.Spring Session+redis实现的跨二级域名的单点登录 - -> - 分布式session只能解决类似a.jd.com,b.jd.com这样二级域名的分布式系统登录。domain不能设置为.com -> -> - 或者video.qq.com,news.qq.com这种的跨二级域名 - -步骤和原理在上面讲了 - - - -### 2.基于Cookie接入的Web端跨一级域名的单点登录 - -> 以新浪公司为例。新浪公司旗下的**微博**域名是weibo.com,新浪官方网站域名是sina.com.cn。经过测试如果在weibo.com登录之后,sina.com.cn就自动登录。单点登录可以多系统跨域名登录,把认证中心单独成一个系统。 - -#### xxl-sso基于cookie的原理 - -client1访问 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/048.png) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/049.png) - -client2客户端登录 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/050.png) - - - -#### 我们的sso-demo代码流程 - - - - - -#### 使用jwt优化一个东西 - -> 具体代码见雷丰阳的-2019电商单点登录部分 - -![1589442017455](https://npm.elemecdn.com/youthlql@1.0.12/image/052.png) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/053.png) - -如果使用这样的优化,倒不如全部都用jwt来实现 - -### 3.基于Token接入的多端跨一级域名的单点登录 - -#### 大致架构和流程图 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/054.png) - - - -下面的两张流程图来自:[使用JWT实现完全跨域](https://blog.csdn.net/weixin_42873937/article/details/82460997) - - - - - - - - - -#### xxl-sso的基于token的文档说明 - -```java -正常情况下,登录流程如下: -1、获取用户输入的账号密码后,请求SSO Server的登录接口,获取用户 sso sessionid ;(参考代码:TokenClientTest.loginTest) -2、登陆成功后,获取到 sso sessionid ,前端(浏览器)需要主动存储,后续请求时需要设置在 Header参数 中 -3、此时,使用 sso sessionid 访问受保护的 "Client01应用" 和 "Client02应用" 提供的接口,接口均正常返回(参考代码:TokenClientTest.clientApiRequestTest) -``` - -## 29.我们的电商系统采用的就是第三种基于token的方式 - -### 1.我们的流程图 - -上面的流程图说的很完善,和我们的电商略有区别。我们没有实现跨一级域名,我们只实现了跨二级域名。 - -- 把认证中心独立成一个服务,只负责token的颁发和校验 -- 每个客户端加一个拦截器,用来校验请求头中是否有jwt,以及jwt的合法性。 - -![1589444375916](https://npm.elemecdn.com/youthlql@1.0.12/image/055.png) - -## 30.认证相关的问题 - -### 1.由认证中心签发的token如何保存? - -1.保存到浏览器的cookie或localstorage中,其它客户端通过iframe+postMeassgae的方式跨域获取 - -2.sso-server认证完成之后,将Token以url参数的形式返回给对应的客户端,并让其储存在本域名下。 - - - -### 2.如何校验JWT - -- 拦截器统一实现,我们的电商系统因为包名都是com.atguigu.gmall。所以我们只在web-util的模块下写了拦截器,但其实也可以在所有客户端都加拦截器。如**28-3的图** -- 通过密钥和签名算法 - - - -### 3.为什么在order.gmall.com域名下登录,其它二级域名(cart.gmall.com)也有cookie? - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/056.png) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/057.png) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/058.png) - -从代码中可以看出,我们刻意把domain设置成了gmall.com这种格式。所以我们的单点登录只是实现了跨二级域名的单点登录,并没有跨一级域名。 - - - -### 4.如何判断哪些操作需要不需要登录? - -自定义注解。 - - - -### 5.jwt生成的token能跨域是什么意思? - -> https://blog.csdn.net/weixin_42873937/article/details/82460997 - -- 这是客户端存取JWT的跨域问题只能这样解决 - -1、前端页面将JWT令牌从response响应头中取出,然后存入localstorage或cookie中。但是遇到跨域场景,处理起来就会比较复杂因为一旦在浏览器中跨域将获取不到localstorage中的JWT令牌。例如www.a.com域下的JWT,在www.b.com域下是获取不到的,所以我选择了一种页面跨域的方式进行处理,使用iframe+H5的postMessage。 - -2、或者sso-server认证完成之后,将Token以url参数的形式返回给对应的客户端,并让其储存在本域名a.com下。并且ssoserver.com域名下也存一个 - -- 如果在Authorization header中发送令牌,则跨域资源共享(CORS)将不会成为问题,因为它不使用cookie。 - -``` -在基于 Token 进行身份验证的的应用程序中,服务器通过Payload、Header和一个密钥(secret)创建令牌(Token)并将 Token发送给客户端,客户端将 Token 保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP Header 的 Authorization字段中:Authorization: Bearer Token。 -``` - -**关于cookie的问题** - -1.比如一个cookie(假设我们称为cookieA)的domain为a.weibo.com,那么所有客户端访问a.weibo.com的网页时,都会带上cookieA。但是如果访问b.weibo.com,那么cookie就带不过去。因为domain不一样 - -2.比如一个cookie(假设我们称为cookieA)的domain为weibo.com,那么所有客户端访问a.weibo.com的网页时,都会带上cookieA。访问b.weibo.com,也会带上cookieA。因为domain是最上层的cookie,但是最多只能到weibo.com这种一级域名。 weibo.com和sina.comc.n这种无论如何都共享不了。 - -3.上面的第二个问题可以用:把jwt放在请求头中带过去,这个才是token能跨域的意思。 - - - -## 31.购物车相关的其它问题 - -### 1.存到redis的购物车的结构? - -- 存储的是购物车集合 - -- 购物车缓存中的某一个购物车数据的更新 - - - 如果用set kv 取出json,转化成集合,从集合中取出对象,修改对象,放回集合,集合放回缓存。很麻烦 - - 所以直接使用hash进行存储(方便查询和修改用户购物车集合中的某一个单独的购物车对象) -- 企业中最常用的方式就是:object:id:filed - - -**redis的hash结构:** - -hashkey - -​ Key value - -​ Key value - - **我们存的:** - -"user:"+memberId+":cart" - -​ skuId cart - - ​ skuId cart - -```java -// 同步到redis缓存中 -Jedis jedis = redisUtil.getJedis(); - -Map map = new HashMap<>(); -for (OmsCartItem cartItem : omsCartItems) { - cartItem.setTotalPrice(cartItem.getPrice().multiply(cartItem.getQuantity())); - map.put(cartItem.getProductSkuId(), JSON.toJSONString(cartItem)); -} - -jedis.del("user:"+memberId+":cart"); -jedis.hmset("user:"+memberId+":cart",map); -``` - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/059.png) - -jedis直接存个map就行 - - - -```java -@RequestMapping("cartList") -@LoginRequired(loginSuccess = false) -public String cartList(HttpServletRequest request, HttpServletResponse response, HttpSession session, ModelMap modelMap) { - - List omsCartItems = new ArrayList<>(); - String memberId = (String)request.getAttribute("memberId"); - String nickname = (String)request.getAttribute("nickname"); - - if(StringUtils.isNotBlank(memberId)){ - // 已经登录查询db - omsCartItems = cartService.cartList(memberId); - }else{ - // 没有登录查询cookie - String cartListCookie = CookieUtil.getCookieValue(request, "cartListCookie", true); - if(StringUtils.isNotBlank(cartListCookie)){ - omsCartItems = JSON.parseArray(cartListCookie,OmsCartItem.class); - } - } - - for (OmsCartItem omsCartItem : omsCartItems) { - omsCartItem.setTotalPrice(omsCartItem.getPrice().multiply(omsCartItem.getQuantity())); - } - - modelMap.put("cartList",omsCartItems); - // 被勾选商品的总额 - BigDecimal totalAmount =getTotalAmount(omsCartItems); - modelMap.put("totalAmount",totalAmount); - return "cartList"; -} -``` - - - -### 2.查询购物车列表有没有什么事情需要注意? - -1.如果redis中没有要从数据库中查询,要连带把最新的价格也取出来,默认要显示最新价格而不是当时放入购物车的价格,如果考虑用户体验可以把两者的差价提示给用户。 - -2.如果用户突然登录了,还要注意合并cookie的购物车数据 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/060.png) - - - -### 3.购物车的另一种实现思路 - -拦截器+ThreadLocal实现用户身份鉴别 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/062.png) - - - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/061.png) - -添加购物车操作 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/063.png) - -```java -@Override -public Cart getCart() throws ExecutionException, InterruptedException { - Cart cart = new Cart(); - - UserInfoTo userInfoTo = CartInterceptor.threadLocal.get(); - if (userInfoTo.getUserId() != null) { - // 1.已经登录 - String cartKey = CART_PREFIX + userInfoTo.getUserId(); - // 2. 如果临时购物车的数据还没有进行合并【合并购物车】 - String tempCartKey = CART_PREFIX + userInfoTo.getUserKey(); - List tempCartItems = getCartItems(tempCartKey); - if (tempCartItems != null) { - // 临时购物车有数据,需要合并 - for (CartItem tempCartItem : tempCartItems) { - addToCart(tempCartItem.getSkuId(), tempCartItem.getCount()); - } - // 清除临时购物车的数据 - clearCart(tempCartKey); - } - // 3. 获取登录后的购物车数据【包含合并过来的临时购物车的数据,和登陆后的购物车的数据】 - List cartItems = getCartItems(cartKey); - cart.setItems(cartItems); - } else { - // 未登录 - String cartKey = CART_PREFIX + userInfoTo.getUserKey(); - List cartItems = getCartItems(cartKey); - cart.setItems(cartItems); - } - return cart; -} -``` - -## 32.订单的相关问题 - -### 1.同一个电商网站的用户账号,可以不可以在不同的机器上登录 - -可以,web网站的账号时可以在不同的客户端同时登录的 - - - -### 2.在我们点击结算按钮时,后台的购物车数据结构是否被删除,订单数据结构是否生成 - -没有生成,结算按钮不调用后台的service数据库服务,结算页面只是用来用户确认送货清单和选择收获地址信息的页面 - - - -### 3.点击提交订单按钮时, 后台的购物车数据结构是否被删除,订单数据结构是否生成 - -生成了,购物车数据转化为订单数据,购物车表删除数据,订单表新增数据 - -提交订单时,是对服务器的写操作,一般不用表单提交,而是直接从缓存或者数据库中查询用户所要购买的商品,转化成订单 - - - -### 4.如何保证订单不被重复提交 - -**①场景:** - -如何防止用户通过页面回退的方式重复提交同一个订单 - -**②从购物车到下订单有两个主要步骤** - -1.购物车页点击去结算,会生成一个结算页。这个结算页时供用户看的,后台生成订单,并不完全按照上面的来。(比如突然有东西涨价了,后台是按实际情况价格来的。不过可以做友好提示,如果价格变了,给用户提示) - -2.用户确认完结算页后,点击生成订单。自此结束 - -**③如何防止重复提交** - -1.在点击结算的时候,根据memberId生成随机交易码,并存在redis中,还要把交易码(或者说令牌)传给前端。 - -```java -public String genTradeCode(String memberId) { - - Jedis jedis = redisUtil.getJedis(); - - String tradeKey = "user:"+memberId+":tradeCode"; - - String tradeCode = UUID.randomUUID().toString(); - - jedis.setex(tradeKey,60*15,tradeCode); - - jedis.close(); - - return tradeCode; -} -``` - -2.在真正点击**生成订单**时,前端不仅要传用户id等相关信息,还要把交易码传过来。首先检验交易码是否正确,如果正确那么直接删除redis中的交易码。这样用户通过浏览器回退结算页的时候,即使再次点击生成订单,因为交易码检验不过,无法生成订单。 - - - -### 5.删除交易码时会出现什么问题? - -这样的代码如果并发很高,有可能会造成交易码校验全部通过if语句,造成交易码重复使用的问题。 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/064.png) - - - -所以删除交易码的时候采用lua脚本删除,就是“原子性的对比+删除”。 - -``` -public String checkTradeCode(String memberId, String tradeCode) { - Jedis jedis = null ; - - try { - jedis = redisUtil.getJedis(); - String tradeKey = "user:" + memberId + ":tradeCode"; - - //String tradeCodeFromCache = jedis.get(tradeKey);// 使用lua脚本在发现key的同时将key删除,防止并发订单攻击 - //对比防重删令牌 - String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; - Long eval = (Long) jedis.eval(script, Collections.singletonList(tradeKey), Collections.singletonList(tradeCode)); - - if (eval!=null&&eval!=0) { - jedis.del(tradeKey); - return "success"; - } else { - return "fail"; - } - }finally { - jedis.close(); - } -} -``` - - - -## 33.你在实际使用Feign的时候,有没有遇到什么问题,怎么解决的? - -### 1.Feign远程调用丢失请求头问题 - -1.问题的原因就是,Feign远程调用的时候会创建一个新的request。而Feign调用的时候,会经历一系列拦截器。所以我们只需要实现某个拦截器,手动的加入请求头即可。 - -2.大致流程,就是先构造新的request,然后经历一系列拦截器,最后发送请求。需要注意的是下面代码的RequestContextHolder是原请求的上下文环境,而不是新的request的上下文。 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/066.png) - - - -**解决办法:** - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/065.png) - - - -### 2.Feign异步调用丢失上下文问题 - -RequestContextHolder内部实现就是用的ThreadLocal。现在主线程和异步线程不是用一个线程,拦截器里的RequestContextHolder当然拿不到主线程的上下文。 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/067.png) - -所以需要我们手动在Service方法里给每个异步线程添加主线程的上下文Attributes。 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/068.png) - - - -## 34.哪些业务是需要保证幂等性的? - -- 库存,不可能让同一个操作,库存扣减多次 - -- 订单,不可能让同一个操作,生成多个订单。上面的订单交易码,就是为了保证幂等性。 - -- 支付,支付就更不用说了 - -- 更多场景见**2020谷粒电商视频274** - - - -**大致的解决方案** - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/069.png) - -- 刚开始有点疑惑数据库悲观锁(行锁,表锁,gap锁,next-key锁等)和分布式锁为什么能防止幂等性?下面讲一下 - -1.1号订单和2号订单失败了,库存需要被解锁(可能是把数量改回去)。此时库存有三台机器部署了三个一样的服务,A,B,C服务。 - -2.后台写了一个定时异步线程,自动去解锁库存。这时候A,B,C三个服务都同时启动了定时任务,如果没有锁的话就会同时解锁,打破了幂等性。如果有悲观锁或者分布式锁的话,配合(表里加一个是否解锁字段,或者版本啥的)。保证数据的正确性(也就是保证幂等性)。 - -3.这些幂等性方案是需要配合来使用的,并不是只用一个就可以的。并发(**锁来保证**)和保证幂等性(**唯一约束啥的来保证**)是同时存在的。 - -4.如果你没有锁,只有唯一约束。并发量高的情况下,就有可能同时存在两个主键相同的的记录(打破了唯一约束)。 - -> 高并发场景下使用锁来防止唯一约束被打破: -> -> https://blog.csdn.net/ruixing222/article/details/103034392 -> -> https://blog.csdn.net/qq_28018283/article/details/80241090 - - - -## 35.如何锁定库存(未总结) - -这块等github上那位大哥的代码,再总结。没代码不好看具体逻辑 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/072.png) - - - -## 36.库存出现的事务问题 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/075.png) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/073.png) - -```java -@Transactional - @Override - public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) { - confirmVoThreadLocal.set(vo); - SubmitOrderResponseVo response = new SubmitOrderResponseVo(); - MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get(); - response.setCode(0); - // 1. 验证令牌【令牌的对比和删除必须保证原子性】 - // 0令牌失败 1 删除成功 - String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS) else return 0 end"; - String orderToken = vo.getOrderToken(); - // 原子验证令牌和删除令牌 - Long result = (Long) redisTemplate.execute(new DefaultRedisScript(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(); - //2. 验价 - BigDecimal payAmount = order.getOrder().getPayAmount(); - BigDecimal payPrice = vo.getPayPrice(); - if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) { - // 验价通过 - // 3.保存订单 - saveOrder(order); - // 4. 库存锁定,只要有异常回滚订单数据 - // 订单号,所有订单项(skuId, skuName, num) - WareSkuLockVo lockVo = new WareSkuLockVo(); - lockVo.setOrderSn(order.getOrder().getOrderSn()); - List locks = order.getOrderItems().stream().map(item -> { - OrderItemVo itemVo = new OrderItemVo(); - itemVo.setSkuId(item.getSkuId()); - itemVo.setCount(item.getSkuQuantity()); - itemVo.setTitle(item.getSkuName()); - return itemVo; - }).collect(Collectors.toList()); - lockVo.setLocks(locks); - R r = wmsFeignService.orderLockStock(lockVo); - if (r.getCode() == 0) { - // 锁成功了 - response.setOrder(order.getOrder()); - rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", order.getOrder()); - return response; - } else { - // 锁失败了 -// throw new NoStockException(1L); - response.setCode(3); - return response; - } - } else { - // 验价没通过 - } - } - - return null; - } -``` - -### 1.本地事务会出现的问题 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/074.png) - -### 2.CAP定理 - -> https://www.zhihu.com/question/54105974 - -#### 为什么CAP只能三选二? - -> 尚硅谷2020谷粒电商-285、商城业务-分布式事务-分布式CAP&Raft原理-从8min开始 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/115.png) - -###为什么P[分区容错]总是成立? - -* 分区容错表明当消息从一个节点向另一个节点发送消息的过程中,消息可能会丢失。以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在 一致性 和 可用性 之间做出选择。**所以如果不保证分区容错性的话,所有消息都无法发送的话,分布式系统的各个节点将无法同步消息。** -* 分区容错无法避免,因此可以认为 CAP定理 的 分区容错性 总是成立。 CAP 定理告诉我们,剩下的 一致性 和 可用性 无法同时做到。通常我们为了分区容错,我们的系统必须保证能够在任意网络分区下正常运行。 - - - -### 3.了解RAFT算法吗? - -raft是保证一致性的算法。 - -**领导选举:** - -- 关键点是两个超时时间, - - - 一个是选举超时。follower节点(从节点)--candidate(候选者的)自旋超时时间,一般是150ms-300ms。超过这个时间。没有收到之前的领导命令,就开始自己成为候选者。给其他节点发信息,进行领导选举。 - - - 一个是消息发送的心跳超时时间 - -**保证一致性的过程:**日志复制 - -1.客户端给leader节点发送一个保存值为5的操作 set 5 - -2.leader节点首先把set 5保存在自己的节点日志里,此时并未把这个日志提交到leader节点里。leader节点首先把这个日志随着心跳发送给各个从节点,命令他们保存这个日志。大部分从节点只要响应日志写好了,leader节点就会commit了。leader节点commit之后,通知其他从节点提交5。然后给客户端响应保存成功。 - - - -### 4.当出现网络分区中断的时候,raft算法如何保证一致性(了解,应该不会问) - -> 尚硅谷2020谷粒电商---285、商城业务-分布式事务-分布式CAP&Raft原理 从30min开始讲的 - -有个点可能会有点误解。 - -- 下面的两个节点选不出leader节点,因为得不到3个以上(大部分节点)的同意。所以如果有客户端请求过来,就会告诉下面的两个节点组成的分区不可用。这就是所谓的牺牲了可用性来保证一致性。 -- 但是视频里讲的是下面的2个节点由于本来就有一个leader节点,所以还可以接受set 数据。但是由于得不到大多数节点的确认,还是保存不了数据,所以依旧给客户端返回是不可用的。但是这里我有个疑问,那如果只是读下面两个节点的数据呢?是否就可以读到呢?如果读到了岂不是就不可用了。所以视频讲的可能有一点点问题。以上面的第一个为准。 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/076.png) - - - - - -## 37.Base理论 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/077.png) - -实际的分布式系统都是想保证AP,一致性采用最终一致性 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/078.png) - - - -## 38.分布式事务解决方案 - -> https://gitee.com/youthlql/advanced-java/blob/master/docs/distributed-system/distributed-transaction.md - -https://www.cnblogs.com/jiangyu666/p/8522547.html - -https://blog.csdn.net/john1337/article/details/97551499 - -## 39.Seata分布式事务(未总结完) - -Seata分布式事务不适合高并发场景,中间用了全局锁之类的东西,并发能力不高。可以用于并发不高的场景,比如电商的后台管理系统。 - -http://seata.io/zh-cn/docs/overview/what-is-seata.html - - - -## 40.RokcetMQ解决订单-库存场景的分布式事务 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/086.png) - -### 1.为什么不用定时任务? - -我们采用的方式是隔一段时间后,检查库存,同时根据各种逻辑判断是否需要解锁库存。按理来说定时任务也是可以的,但是存在时效性问题,以及下方说的轮询数据库带来压力。 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/085.png) - -### 2.Rocketmq延时队列 - -> - 由于rabbitmq不支持定时发送消息,所以需要较为麻烦的方法来实现延时队列,可以看到视频中讲的比较麻烦。 -> -> - 而RocektMQ天生支持延迟消息,也就天生支持延时队列。 - -下面的这种属于补偿性的 - -1.订单发起远程调用锁定库存 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/081.png) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/083.png) - -2.库存锁定成功的话,发送延时消息(假设延迟消息是40min)。 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/082.png) - - - -3.40min中后,MQ检测到消息。并根据情况决定解锁库存或不解锁 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/079.png) - - - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/080.png) - -解锁的sql - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/084.png) - -### 3.延迟队列关闭订单 - -创建订单之后30min之内,若用户没支付,则需要将订单关闭,也就是将订单的状态改为已取消。40min后,则需要执行上面的延时队列解锁库存的操作。 - -**代码流程** - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/089.png) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/090.png) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/091.png) - - - -## 41.上面的关单和解锁库存会出现什么问题? - -### 1.问题 - -如果发送订单创建成功的那个消息,因为网络,机器卡顿等原因发送慢了。导致解锁库存的消息先被消费,结果发现订单是**新建状态**,于是是库存就不解锁了,消息也被消费完了。那么一旦上面的那个订单未被支付,库存将永远无法被解锁。 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/087.png) - -### 2.解决办法 - -关闭订单后,给库存队列发送一个订单已关闭的消息。 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/088.png) - -- 下面代码的第一个监听是为了解决锁定库存远程调用网络故障(实际已锁,但是网络原因返回错误,导致库存锁了,订单回滚了等这些问题)。同时也可以解决正常情况下用户30min未支付,关单并解锁库存的逻辑 - -- 第二个方法的监听就是为了防止上面说的不正常情况。 - -![1589944066987](https://npm.elemecdn.com/youthlql@1.0.12/image/092.png) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/093.png) - - - -## 42.支付相关问题 - -### 1.你是用什么测试的支付? - -支付宝官方提供的沙箱环境 - - - -### 2.支付的大致代码和页面流程 - -#### ①订单页点击支付宝进行付款 - -页面图片: - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/100.png) - -代码: - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/101.png) - -#### ②跳转到支付宝支付页面 - -上面点击**支付宝**就会向这个Controller发请求,带上必要的数据之后,直接跳转到支付页面 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/103.png) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/102.png) - -#### ③跳转到我的订单页(同步回调页面) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/106.png) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/104.png) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/105.png) - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/107.png) - - - -### 3.异步结果通知,来修改订单支付状态 - -支付宝的异步通知,也算一种分布式事务,属于最大努力通知的分布式事务。就是支付宝通过异步地址不断的给我们发送消息,告诉我们支付成功了。 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/108.png) - -![1589973740556](https://npm.elemecdn.com/youthlql@1.0.12/image/109.png) - -![1589973769235](https://npm.elemecdn.com/youthlql@1.0.12/image/110.png) - - - -### 4.支付的最后一步:收单 - -如果我们的订单过期了(根据代码逻辑,订单过期的时候,库存也会马上解锁),支付宝才支付。就有可能造成通过异步回调将订单状态改为已支付,但是库存已经解锁。用户白支付了。所以我们要在支付宝上加上付款时间倒计时。 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/111.png) - -只需要加个参数就可以调用支付宝的自动收单 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/112.png) - -加了1min支付宝自动收单后的效果 - -![](https://npm.elemecdn.com/youthlql@1.0.12/image/113.png) - - - - - -## 电商模式 - -谷粒电商采用的是B2C模式 - - - - - -## 为什么要进行服务拆分,即为什么要用分布式系统(未总结) - -https://gitee.com/youthlql/Java-Interview-Advanced/blob/master/docs/distributed-system/why-dubbo.md - - - -## 什么是RPC - - - - - - - - - - - - - - - - - - - -## #.遇到的难题 - -### 1.sku根据销售属性的动态切换 - -记录一下:视频里当时说的是可以用for循环,也可以通过一个sql语句来写。sql语句的写法在 "2019版-05 谷粒商品详情页.docx" - - - -## 2.Feign调用被认证中心拦截器拦截?没明白为什么 - -> 视频296 - - - - - -## 3.分布式系统为什么要拆分 - -![image-20200913085803325](https://npm.elemecdn.com/youthlql@1.0.12/image/image-20200913085803325.png) -