From 2c4fcb7099a6c3a5839992db1b23a7dd620304fb Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 5 Mar 2022 15:11:26 +0800 Subject: [PATCH 01/51] Update Java Note --- Frame.md | 265 ++++++++++++++++++++++++------------------------------- Java.md | 4 + Prog.md | 7 +- 3 files changed, 121 insertions(+), 155 deletions(-) diff --git a/Frame.md b/Frame.md index 5cf9cd6..7d86be6 100644 --- a/Frame.md +++ b/Frame.md @@ -1480,7 +1480,7 @@ Reactor 模式,通过一个或多个输入同时传递给服务处理器的** Reactor 模式关键组成: -- Reactor:在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 I/O 事件做出反应 +- Reactor:在一个单独的线程中运行,负责**监听和分发事件**,分发给适当的处理程序来对 I/O 事件做出反应 - Handler:处理程序执行 I/O 要完成的实际事件,Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行**非阻塞操作** Reactor 模式具有如下的优点: @@ -1510,7 +1510,7 @@ Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 * 如果不是建立连接事件,则 Reactor 会分发给连接对应的 Handler 来响应,Handler 会完成 read、业务处理、send 的完整流程 - 说明:Handler 和 Acceptor 属于同一个线程 + 说明:**Handler 和 Acceptor 属于同一个线程** @@ -1568,7 +1568,7 @@ Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 模型优点 -- 父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理 +- **父线程与子线程**的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理 - 父线程与子线程的数据交互简单,Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据 使用场景:Nginx 主从 Reactor 多进程模型,Memcached 主从多线程,Netty 主从多线程模型的支持 @@ -1621,7 +1621,7 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty 2. BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup,该 Group 相当于一个事件循环组,含有多个事件循环,每一个事件循环是 NioEventLoop,所以可以有多个线程 -3. NioEventLoop 表示一个循环处理任务的线程,每个 NioEventLoop 都有一个 Selector,用于监听绑定在其上的 socket 的网络通讯 +3. NioEventLoop 表示一个**循环处理任务的线程**,每个 NioEventLoop 都有一个 Selector,用于监听绑定在其上的 Socket 的通讯 4. 每个 Boss NioEventLoop 循环执行的步骤: @@ -1761,14 +1761,14 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty #### 基本介绍 -事件循环对象 EventLoop,本质是一个单线程执行器(同时维护了一个 selector),有 run 方法处理 Channel 上源源不断的 IO 事件 +事件循环对象 EventLoop,**本质是一个单线程执行器同时维护了一个 Selector**,有 run 方法处理 Channel 上源源不断的 IO 事件 事件循环组 EventLoopGroup 是一组 EventLoop,Channel 会调用 Boss EventLoopGroup 的 register 方法来绑定其中一个 Worker 的 EventLoop,后续这个 Channel 上的 IO 事件都由此 EventLoop 来处理,保证了事件处理时的线程安全 EventLoopGroup 类 API: * `EventLoop next()`:获取集合中下一个 EventLoop,EventLoopGroup 实现了 Iterable 接口提供遍历 EventLoop 的能力 -* `Future shutdownGracefully()`:优雅关闭的方法,会首先切换 `EventLoopGroup` 到关闭状态从而拒绝新的任务的加入,然后在任务队列的任务都处理完成后,停止线程的运行。从而确保整体应用是在正常有序的状态下退出的 +* `Future shutdownGracefully()`:优雅关闭的方法,会首先切换 EventLoopGroup 到关闭状态从而拒绝新的任务的加入,然后在任务队列的任务都处理完成后,停止线程的运行,从而确保整体应用是在正常有序的状态下退出的 * ` Future submit(Callable task)`:提交任务 * `ScheduledFuture scheduleWithFixedDelay`:提交定时任务 @@ -2395,7 +2395,7 @@ Pipeline 的存在,需要将 ByteBuf 传递给下一个 ChannelHandler,如 ### 现象演示 -在 TCP 传输中,客户端发送消息时,实际上是将数据写入TCP的缓存,此时数据的大小和缓存的大小就会造成粘包和半包 +在 TCP 传输中,客户端发送消息时,实际上是将数据写入 TCP 的缓存,此时数据的大小和缓存的大小就会造成粘包和半包 * 当数据超过 TCP 缓存容量时,就会被拆分成多个包,通过 Socket 多次发送到服务端,服务端每次从缓存中取数据,产生半包问题 @@ -3605,7 +3605,7 @@ RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Produce 消息的发布是指某个生产者向某个 Topic 发送消息,消息的订阅是指某个消费者关注了某个 Topic 中带有某些 Tag 的消息,进而从该 Topic 消费数据 -导入 MQ 客户端依赖 +导入 MQ 客户端依赖: ```xml @@ -3815,8 +3815,8 @@ public class Consumer { 顺序消息分为全局顺序消息与分区顺序消息, -- 全局顺序:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。 适用于性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景 -- 分区顺序:对于指定的一个 Topic,所有消息根据 sharding key 进行分区,同一个分组内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用于性能要求高,以 Sharding key 作为分区字段,在同一个区中严格的按照 FIFO 原则进行消息发布和消费的场景 +- 全局顺序:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费,适用于性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景 +- 分区顺序:对于指定的一个 Topic,所有消息根据 Sharding key 进行分区,同一个分组内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念,适用于性能要求高的场景 在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列),而消费消息是从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当**发送和消费参与的 queue 只有一个**,则是全局有序;如果多个queue 参与,则为分区有序,即相对每个 queue,消息都是有序的 @@ -4000,7 +4000,7 @@ Broker 可以配置 messageDelayLevel,该属性是 Broker 的属性,不属 - 1<=level<=maxLevel:消息延迟特定时间,例如 level==1,延迟 1s - level > maxLevel:则 level== maxLevel,例如 level==20,延迟 2h -定时消息会暂存在名为 SCHEDULE_TOPIC_XXXX 的 Topic 中,并根据 delayTimeLevel 存入特定的 queue,队列的标识 `queueId = delayTimeLevel – 1`,即一个 queue 只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。Broker 会调度地消费 SCHEDULE_TOPIC_XXXX,将消息写入真实的 Topic +定时消息会暂存在名为 SCHEDULE_TOPIC_XXXX 的 Topic 中,并根据 delayTimeLevel 存入特定的 queue,队列的标识 `queueId = delayTimeLevel – 1`,即**一个 queue 只存相同延迟的消息**,保证具有相同发送延迟的消息能够顺序消费。Broker 会调度地消费 SCHEDULE_TOPIC_XXXX,将消息写入真实的 Topic 注意:定时消息在第一次写入和调度写入真实 Topic 时都会计数,因此发送数量、tps 都会变高。 @@ -4219,7 +4219,7 @@ consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC"); #### 原理解析 -RocketMQ 分布式消息队列的消息过滤方式是在 Consumer 端订阅消息时再做消息过滤的,所以是在 Broker 端实现的,优点是减少了对于 Consumer 无用消息的网络传输,缺点是增加了 Broker 的负担、而且实现相对复杂 +RocketMQ 分布式消息队列的消息过滤方式是在 Consumer 端订阅消息时再做消息过滤的,所以是在 Broker 端实现的,优点是减少了对于 Consumer 无用消息的网络传输,缺点是增加了 Broker 的负担,而且实现相对复杂 RocketMQ 在 Producer 端写入消息和在 Consumer 端订阅消息采用**分离存储**的机制实现,Consumer 端订阅消息是需要通过 ConsumeQueue 这个消息消费的逻辑队列拿到一个索引,然后再从 CommitLog 里面读取真正的消息实体内容 @@ -4300,7 +4300,7 @@ public class Consumer { #### 工作流程 -RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交事务消息,同时增加一个补偿逻辑来处理二阶段超时或者失败的消息,如下图所示: +RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交事务消息,同时增加一个**补偿逻辑**来处理二阶段超时或者失败的消息,如下图所示: ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-事务消息.png) @@ -4308,12 +4308,14 @@ RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交 1. 事务消息发送及提交: - * 发送消息(Half 消息) -* 服务端响应消息写入结果 - * 根据发送结果执行本地事务(如果写入失败,此时 Half 消息对业务不可见,本地逻辑不执行) -* 根据本地事务状态执行 Commit 或者 Rollback(Commit 操作生成消息索引,消息对消费者可见) - - ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-事务工作流程.png) + * 发送消息(Half 消息),服务器将消息的主题和队列改为半消息状态,并放入半消息队列 + + * 服务端响应消息写入结果(如果写入失败,此时 Half 消息对业务不可见) + * 根据发送结果执行本地事务 + * 根据本地事务状态执行 Commit 或者 Rollback(Commit 操作生成消息索引,消息对消费者可见) + + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-事务工作流程.png) 2. 补偿流程: @@ -4352,7 +4354,7 @@ RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 T * 如果执行 Commit 操作,则需要让消息对用户可见,构建出 Half 消息的索引。一阶段的 Half 消息写到一个特殊的 Topic,构建索引时需要读取出 Half 消息,然后通过一次普通消息的写入操作将 Topic 和 Queue 替换成真正的目标 Topic 和 Queue,生成一条对用户可见的消息。其实就是利用了一阶段存储的消息的内容,在二阶段时恢复出一条完整的普通消息,然后走一遍消息写入流程 -* 如果是 Rollback 则需要撤销一阶段的消息,因为消息本就不可见,所以并不需要真正撤销消息(实际上 RocketMQ 也无法去删除一条消息,因为是顺序写文件的)。RocketMQ 为了区分这条消息没有确定状态的消息(Pending 状态),采用 Op 消息标识已经确定状态的事务消息(Commit 或者 Rollback) +* 如果是 Rollback 则需要撤销一阶段的消息,因为消息本就不可见,所以并**不需要真正撤销消息**(实际上 RocketMQ 也无法去删除一条消息,因为是顺序写文件的)。RocketMQ 为了区分这条消息没有确定状态的消息(Pending 状态),采用 Op 消息标识已经确定状态的事务消息(Commit 或者 Rollback) 事务消息无论是 Commit 或者 Rollback 都会记录一个 Op 操作,两者的区别是 Commit 相对于 Rollback 在写入 Op 消息前创建 Half 消息的索引。如果一条事务消息没有对应的 Op 消息,说明这个事务的状态还无法确定(可能是二阶段失败了) @@ -4504,7 +4506,7 @@ NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态 NameServer 主要包括两个功能: -* Broker 路由管理,NameServer 接受 Broker 集群的注册信息,保存下来作为路由信息的基本数据,提供**心跳检测机制**检查 Broker 是否还存活,每 10 秒清除一次两小时没有活跃的 Broker +* Broker 管理,NameServer 接受 Broker 集群的注册信息,保存下来作为路由信息的基本数据,提供**心跳检测机制**检查 Broker 是否还存活,每 10 秒清除一次两小时没有活跃的 Broker * 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 NameServer 特点: @@ -4517,7 +4519,7 @@ BrokerServer 主要负责消息的存储、投递和查询以及服务高可用 Broker 包含了以下几个重要子模块: -* Remoting Module:整个 Broker 的实体,负责处理来自 clients 端的请求 +* Remoting Module:整个 Broker 的实体,负责处理来自 Clients 端的请求 * Client Manager:负责管理客户端(Producer/Consumer)和维护 Consumer 的 Topic 订阅信息 @@ -4544,7 +4546,7 @@ RocketMQ 的工作流程: - 收发消息前,先创建 Topic,创建 Topic 时需要指定该 Topic 要存储在哪些 Broker 上,也可以在发送消息时自动创建 Topic - Producer 启动时先跟 NameServer 集群中的**其中一台**建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,同时 Producer 会默认每隔 30s 向 NameServer **定时拉取**一次路由信息 - Producer 发送消息时,根据消息的 Topic 从本地缓存的 TopicPublishInfoTable 获取路由信息,如果没有则会从 NameServer 上重新拉取并更新,轮询队列列表并选择一个队列 MessageQueue,然后与队列所在的 Broker 建立长连接,向 Broker 发消息 -- Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接,定时获取路由信息,根据当前订阅 Topic 存在哪些 Broker 上,直接跟 Broker 建立连接通道,在完成客户端的负载均衡后,选择其中的某一个或者某几个 MessageQueue 来拉取消息并进行消费 +- Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接,**定时获取路由信息**,根据当前订阅 Topic 存在哪些 Broker 上,直接跟 Broker 建立连接通道,在完成客户端的负载均衡后,选择其中的某一个或者某几个 MessageQueue 来拉取消息并进行消费 @@ -4560,7 +4562,7 @@ At least Once:至少一次,指每个消息必须投递一次,Consumer 先 回溯消费:指 Consumer 已经消费成功的消息,由于业务上需求需要重新消费,Broker 在向 Consumer 投递成功消息后,消息仍然需要保留。并且重新消费一般是按照时间维度,例如由于 Consumer 系统故障,恢复后需要重新消费 1 小时前的数据,RocketMQ 支持按照时间回溯消费,时间维度精确到毫秒 -分布式队列因为有高可靠性的要求,所以数据要进行持久化存储 +分布式队列因为有高可靠性的要求,所以数据要进行**持久化存储** 1. 消息生产者发送消息 2. MQ 收到消息,将消息进行持久化,在存储中新增一条记录 @@ -4593,9 +4595,9 @@ RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,Com ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) -* CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是顺序写入日志文件,单个文件大小默认 1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 +* CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是**顺序写入**日志文件,单个文件大小默认 1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 * ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,**保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset**,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M -* IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法不影响发送与消费消息的主流程。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 **hash 索引** +* IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法**不影响发送与消费消息的主流程**。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 **hash 索引** RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)针对 Producer 和 Consumer 分别采用了**数据和索引部分相分离**的存储结构,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 @@ -4619,7 +4621,7 @@ RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所 补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 -通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用**零拷贝技术**,提高消息存盘和网络发送的速度。 +通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用**零拷贝技术**,提高消息存盘和网络发送的速度 RocketMQ 通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 @@ -4633,10 +4635,10 @@ MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文 #### 页面缓存 -页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** +页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。因为 OS 将一部分的内存用作 PageCache,所以程序对文件进行顺序读写的速度几乎接近于内存的读写速度 * 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 -* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理,最大 128K) +* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行**预读取**(局部性原理,最大 128K) 在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 @@ -4655,7 +4657,7 @@ MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文 RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使用**顺序 IO**,因为磁盘的顺序读写要比随机读写快很多 -* 同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 +* 同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ 消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 * 异步刷盘:利用 OS 的 PageCache,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 @@ -4689,7 +4691,7 @@ RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使 - 优点:配置简单,单个 Master 宕机或重启维护对应用无影响,在磁盘配置为 RAID10 时,即使机器宕机不可恢复情况下,由于 RAID10 磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高 - 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响 -* 多 Master 多 Slave 模式(同步):每个 Master 配置一个 Slave,有多对 Master-Slave,HA 采用同步双写方式,即只有主备都写成功,才向应用返回成功 +* 多 Master 多 Slave 模式(同步):每个 Master 配置一个 Slave,有多对 Master-Slave,HA 采用**同步双写**方式,即只有主备都写成功,才向应用返回成功 * 优点:数据与服务都无单点故障,Master 宕机情况下,消息无延迟,服务可用性与数据可用性都非常高 * 缺点:性能比异步复制略低(大约低 10% 左右),发送单个消息的 RT 略高,目前不能实现主节点宕机,备机自动切换为主机 @@ -4710,7 +4712,7 @@ RocketMQ 网络部署特点: - NameServer 是一个几乎**无状态节点**,节点之间相互独立,无任何信息同步 -- Broker 部署相对复杂,Broker 分为 Master 与 Slave,Master 可以部署多个,一个 Master 可以对应多个 Slave,但是一个 Slave 只能对应一个 Master,Master 与 Slave 的对应关系通过指定相同 BrokerName、不同 BrokerId 来定义,BrokerId 为 0 是 Master,非 0 表示 Slave。**每个 Broker 与 NameServer 集群中的所有节点建立长连接**,定时注册 Topic 信息到所有 NameServer +- Broker 部署相对复杂,Broker 分为 Master 与 Slave,Master 可以部署多个,一个 Master 可以对应多个 Slave,但是一个 Slave 只能对应一个 Master,Master 与 Slave 的对应关系通过指定相同 BrokerName、不同 BrokerId 来定义,**BrokerId 为 0 是 Master,非 0 表示 Slave。每个 Broker 与 NameServer 集群中的所有节点建立长连接**,定时注册 Topic 信息到所有 NameServer 注意:部署架构上也支持一 Master 多 Slave,但只有 BrokerId=1 的从服务器才会参与消息的读负载(读写分离) @@ -4738,7 +4740,7 @@ RocketMQ 网络部署特点: 在 Consumer 的配置文件中,并不需要设置是从 Master 读还是从 Slave 读,当 Master 不可用或者繁忙的时候,Consumer 会被自动切换到从 Slave 读。有了自动切换的机制,当一个 Master 机器出现故障后,Consumer 仍然可以从 Slave 读取消息,不影响 Consumer 程序,达到了消费端的高可用性 -在创建 Topic 的时候,把 Topic 的多个 Message Queue 创建在多个 Broker 组上(相同 Broker 名称,不同 brokerId 的机器组成一个 Broker 组),当一个 Broker 组的 Master 不可用后,其他组的 Master 仍然可用,Producer 仍然可以发送消息。 +在创建 Topic 的时候,把 Topic 的多个 Message Queue 创建在多个 Broker 组上(相同 Broker 名称,不同 brokerId 的机器组成一个 Broker 组),当一个 Broker 组的 Master 不可用后,其他组的 Master 仍然可用,Producer 仍然可以发送消息 RocketMQ 目前还不支持把 Slave 自动转成 Master,需要手动停止 Slave 角色的 Broker,更改配置文件,用新的配置文件启动 Broker @@ -4795,7 +4797,7 @@ Producer 端在发送消息时,会先根据 Topic 找到指定的 TopicPublish 容错策略均在 MQFaultStrategy 这个类中定义,有一个 sendLatencyFaultEnable 开关变量: -* 如果开启,会在随机递增取模的基础上,再过滤掉 not available 的 Broker 代理 +* 如果开启,会在随机(只有初始化索引变量时才随机,正常都是递增)递增取模的基础上,再过滤掉 not available 的 Broker 代理 * 如果关闭,采用随机递增取模的方式选择一个队列(MessageQueue)来发送消息 LatencyFaultTolerance 机制是实现消息发送高可用的核心关键所在,对之前失败的,按一定的时间做退避。例如上次请求的 latency 超过 550Lms,就退避 3000Lms;超过 1000L,就退避 60000L @@ -4810,13 +4812,13 @@ LatencyFaultTolerance 机制是实现消息发送高可用的核心关键所在 在 RocketMQ 中,Consumer 端的两种消费模式(Push/Pull)都是基于拉模式来获取消息的,而在 Push 模式只是对 Pull 模式的一种封装,其本质实现为消息拉取线程在从服务器拉取到一批消息,提交到消息消费线程池后,又继续向服务器再次尝试拉取消息,如果未拉取到消息,则延迟一下又继续拉取 -在两种基于拉模式的消费方式(Push/Pull)中,均需要 Consumer 端在知道从 Broker 端的哪一个消息队列—队列中去获取消息,所以在 Consumer 端来做负载均衡,即 Broker 端中多个 MessageQueue 分配给同一个 Consumer Group 中的哪些 Consumer 消费 +在两种基于拉模式的消费方式(Push/Pull)中,均需要 Consumer 端在知道从 Broker 端的哪一个消息队列中去获取消息,所以在 Consumer 端来做负载均衡,即 Broker 端中多个 MessageQueue 分配给同一个 Consumer Group 中的哪些 Consumer 消费 * 广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以不存在负载均衡,在实现上,Consumer 分配 queue 时,所有 Consumer 都分到所有的 queue。 * 在集群消费模式下,每条消息只需要投递到订阅这个 Topic 的 Consumer Group 下的一个实例即可,RocketMQ 采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条 Message Queue -集群模式下,每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照 queue 的数量和实例的数量平均分配 queue 给每个实例。默认的分配算法是 AllocateMessageQueueAveragely: +集群模式下,每当消费者实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照 queue 的数量和实例的数量平均分配 queue 给每个实例。默认的分配算法是 AllocateMessageQueueAveragely: ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-平均队列分配.png) @@ -4824,7 +4826,7 @@ LatencyFaultTolerance 机制是实现消息发送高可用的核心关键所在 ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-平均队列轮流分配.png) -集群模式下,queue 都是只允许分配只一个实例,如果多个实例同时消费一个 queue 的消息,由于拉取哪些消息是 Consumer 主动控制的,会导致同一个消息在不同的实例下被消费多次 +集群模式下,**queue 都是只允许分配只一个实例**,如果多个实例同时消费一个 queue 的消息,由于拉取哪些消息是 Consumer 主动控制的,会导致同一个消息在不同的实例下被消费多次 通过增加 Consumer 实例去分摊 queue 的消费,可以起到水平扩展的消费能力的作用。而当有实例下线时,会重新触发负载均衡,这时候原来分配到的 queue 将分配到其他实例上继续消费。但是如果 Consumer 实例的数量比 Message Queue 的总数量还多的话,多出来的 Consumer 实例将无法分到 queue,也就无法消费到消息,也就无法起到分摊负载的作用了,所以需要**控制让 queue 的总数量大于等于 Consumer 的数量** @@ -4836,11 +4838,11 @@ LatencyFaultTolerance 机制是实现消息发送高可用的核心关键所在 #### 原理解析 -在 Consumer 启动后,会通过定时任务不断地向 RocketMQ 集群中的所有 Broker 实例发送心跳包。Broke r端在收到 Consumer 的心跳消息后,会将它维护 在ConsumerManager 的本地缓存变量 consumerTable,同时并将封装后的客户端网络通道信息保存在本地缓存变量 channelInfoTable 中,为 Consumer 端的负载均衡提供可以依据的元数据信息 +在 Consumer 启动后,会通过定时任务不断地向 RocketMQ 集群中的所有 Broker 实例发送心跳包。Broker 端在收到 Consumer 的心跳消息后,会将它维护在 ConsumerManager 的本地缓存变量 consumerTable,同时并将封装后的客户端网络通道信息保存在本地缓存变量 channelInfoTable 中,为 Consumer 端的负载均衡提供可以依据的元数据信息 Consumer 端实现负载均衡的核心类 **RebalanceImpl** -在 Consumer 实例的启动流程中的启动 MQClientInstance 实例部分,会完成负载均衡服务线程 RebalanceService 的启动(每隔 20s 执行一次),RebalanceService 线程的 run() 方法最终调用的是 RebalanceImpl 类的 rebalanceByTopic() 方法,该方法是实现 Consumer 端负载均衡的核心。rebalanceByTopic() 方法会根据消费者通信类型为广播模式还是集群模式做不同的逻辑处理。这里主要看下集群模式下的处理流程: +在 Consumer 实例的启动流程中的会启动 MQClientInstance 实例,完成负载均衡服务线程 RebalanceService 的启动(每隔 20s 执行一次),RebalanceService 线程的 run() 方法最终调用的是 RebalanceImpl 类的 rebalanceByTopic() 方法,该方法是实现 Consumer 端负载均衡的核心。rebalanceByTopic() 方法会根据消费者通信类型为广播模式还是集群模式做不同的逻辑处理。这里主要看下集群模式下的处理流程: * 从 rebalanceImpl 实例的本地缓存变量 topicSubscribeInfoTable 中,获取该 Topic 主题下的消息消费队列集合 mqSet @@ -4856,7 +4858,7 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** * processQueueTable 的绿色部分,表示与分配到的消息队列集合 mqSet 的交集,判断该 ProcessQueue 是否已经过期了,在 Pull 模式的不用管,如果是 Push 模式的,设置 Dropped 属性为 true,并且调用 removeUnnecessaryMessageQueue() 方法,像上面一样尝试移除 Entry -* 为过滤后的消息队列集合 mqSet 中每个 MessageQueue 创建 ProcessQueue 对象存入 RebalanceImpl 的 processQueueTable 队列中(其中调用 RebalanceImpl 实例的 `computePullFromWhere(MessageQueue mq)` 方法获取该 MessageQueue 对象的下一个进度消费值 offset,随后填充至接下来要创建的 pullRequest 对象属性中),并创建拉取请求对象 pullRequest 添加到拉取列表 pullRequestList 中,最后执行 dispatchPullRequest() 方法,将 Pull 消息的请求对象 PullRequest 依次放入 PullMessageService 服务线程的阻塞队列 pullRequestQueue 中,待该服务线程取出后向 Broker 端发起 Pull 消息的请求。 +* 为过滤后的消息队列集合 mqSet 中每个 MessageQueue 创建 ProcessQueue 对象存入 RebalanceImpl 的 processQueueTable 队列中(其中调用 RebalanceImpl 实例的 `computePullFromWhere(MessageQueue mq)` 方法获取该 MessageQueue 对象的下一个进度消费值 offset,随后填充至接下来要创建的 pullRequest 对象属性中),并**创建拉取请求对象** pullRequest 添加到拉取列表 pullRequestList 中,最后执行 dispatchPullRequest() 方法,将 Pull 消息的请求对象 PullRequest 放入 PullMessageService 服务线程的**阻塞队列** pullRequestQueue 中,待该服务线程取出后向 Broker 端发起 Pull 消息的请求 对比下 RebalancePushImpl 和 RebalancePullImpl 两个实现类的 dispatchPullRequest() 方法,RebalancePullImpl 类里面的该方法为空 @@ -4932,7 +4934,7 @@ IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName 注意点: * 如果同步模式发送失败,则选择到下一个 Broker,如果异步模式发送失败,则**只会在当前 Broker 进行重试** -* 发送消息超时时间默认3000毫秒,就不会再尝试重试 +* 发送消息超时时间默认 3000 毫秒,就不会再尝试重试 @@ -4944,7 +4946,7 @@ IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName Consumer 消费消息失败后,提供了一种重试机制,令消息再消费一次。Consumer 消费消息失败可以认为有以下几种情况: -- 由于消息本身的原因,例如反序列化失败,消息数据本身无法处理等。这种错误通常需要跳过这条消息,再消费其它消息,而这条失败的消息即使立刻重试消费,99% 也不成功,所以需要提供一种定时重试机制,即过 10秒 后再重试 +- 由于消息本身的原因,例如反序列化失败,消息数据本身无法处理等。这种错误通常需要跳过这条消息,再消费其它消息,而这条失败的消息即使立刻重试消费,99% 也不成功,所以需要提供一种定时重试机制,即过 10 秒后再重试 - 由于依赖的下游应用服务不可用,例如 DB 连接不可用,外系统网络不可达等。这种情况即使跳过当前失败的消息,消费其他消息同样也会报错,这种情况建议应用 sleep 30s,再消费下一条消息,这样可以减轻 Broker 重试消息的压力 RocketMQ 会为每个消费组都设置一个 Topic 名称为 `%RETRY%+consumerGroup` 的重试队列(这个 Topic 的重试队列是**针对消费组**,而不是针对每个 Topic 设置的),用于暂时保存因为各种异常而导致 Consumer 端无法消费的消息 @@ -5198,11 +5200,11 @@ NamesrvStartup#createNamesrvController:读取配置信息,初始化 Namesrv * `namesrvConfig = new NamesrvConfig()`:创建 Namesrv 配置对象 * `private String rocketmqHome`:获取 ROCKETMQ_HOME 值 - * `private boolean orderMessageEnable = false`:顺序消息功能是否开启 + * `private boolean orderMessageEnable = false`:**顺序消息**功能是否开启 * `nettyServerConfig = new NettyServerConfig()`:Netty 的服务器配置对象 -* `nettyServerConfig.setListenPort(9876)`:Namesrv 服务器的监听端口设置为 9876 +* `nettyServerConfig.setListenPort(9876)`:Namesrv 服务器的**监听端口设置为 9876** * `if (commandLine.hasOption('c'))`:读取命令行 -c 的参数值 @@ -5247,11 +5249,11 @@ NamesrvController 用来初始化和启动 Namesrv 服务器 private final ScheduledExecutorService scheduledExecutorService; // 调度线程池,用来执行定时任务 private final RouteInfoManager routeInfoManager; // 管理【路由信息】的对象 private RemotingServer remotingServer; // 【网络层】封装对象 - private ExecutorService remotingExecutor; // 业务线程池,用来 work private BrokerHousekeepingService brokerHousekeepingService; // 用于监听 channel 状态 - private ExecutorService remotingExecutor; // 业务线程池 ``` + `private ExecutorService remotingExecutor`:业务线程池,**netty 线程解析报文成 RemotingCommand 对象,然后将该对象交给业务线程池再继续处理** + * 初始化: ```java @@ -5262,13 +5264,12 @@ NamesrvController 用来初始化和启动 Namesrv 服务器 // 监听器监听 channel 状态的改变,会向事件队列发起事件,最后交由 service 处理 this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService); // 【创建业务线程池,默认线程数 8】 - // netty 线程解析报文成 RemotingCommand 对象,然后将该对象交给业务线程池再继续处理。 this.remotingExecutor = Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads().); - // 注册协议处理器(缺省协议处理器),处理器是 DefaultRequestProcessor,线程使用的是刚创建的业务的线程池 + // 注册协议处理器(缺省协议处理器),【处理器是 DefaultRequestProcessor】,线程使用的是刚创建的业务的线程池 this.registerProcessor(); - // 定时任务1:每 10 秒钟检查 broker 存活状态,将 IDLE 状态的 broker 移除【扫描机制】 + // 定时任务1:每 10 秒钟检查 broker 存活状态,将 IDLE 状态的 broker 移除【扫描机制,心跳检测】 this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { @@ -5289,7 +5290,7 @@ NamesrvController 用来初始化和启动 Namesrv 服务器 return true; } ``` - + * 启动方法: ```java @@ -5324,7 +5325,7 @@ RocketMQ 基于 NettyRemotingServer 的 Reactor 多线程模型: * 一个 Reactor 主线程(eventLoopGroupBoss)负责监听 TCP 网络连接请求,建立好连接创建 SocketChannel(RocketMQ 会自动根据 OS 的类型选择 NIO 和 Epoll,也可以通过参数配置),并注册到 Selector 上,然后监听真正的网络数据 * 拿到网络数据交给 Worker 线程池(eventLoopGroupSelector,默认设置为 3),在真正执行业务逻辑之前需要进行 SSL 验证、编解码、空闲检查、网络连接管理,这些工作交给 defaultEventExecutorGroup(默认设置为 8)去做 -* 处理业务操作放在业务线程池中执行,根据 RomotingCommand 的业务请求码 code 去 processorTable 这个本地缓存变量中找到对应的 processor,封装成 task 任务提交给对应的业务 processor 处理线程池来执行(sendMessageExecutor,以发送消息为例) +* 处理业务操作放在业务线程池中执行,根据 RomotingCommand 的**业务请求码 code*** 去 processorTable 这个本地缓存变量中找到对应的 processor,封装成 task 任务提交给对应的 processor 处理线程池来执行(sendMessageExecutor,以发送消息为例) * 从入口到业务逻辑的几个步骤中线程池一直再增加,这跟每一步逻辑复杂性相关,越复杂,需要的并发通道越宽 | 线程数 | 线程名 | 线程具体说明 | @@ -5510,12 +5511,12 @@ NettyRemotingServer 类成员变量: // housekeepingService 不为空,则创建【网络异常事件处理器】 if (this.channelEventListener != null) { - // 线程一直轮询 nettyEventExecutor 状态,根据 CONNECT,CLOSE,IDLE,EXCEPTION 四种事件类型 - // CONNECT 不做操作,其余都是回调 onChannelDestroy 关闭服务器与 Broker 物理节点的 Channel + // 线程一直轮询 nettyEvent 状态,根据 CONNECT,CLOSE,IDLE,EXCEPTION 四种事件类型 + // CONNECT 不做操作,其余都是回调 onChannelDestroy 【关闭服务器与 Broker 物理节点的 Channel】 this.nettyEventExecutor.start(); } - // 提交定时任务,每一秒 执行一次。扫描 responseTable 表,将过期的数据移除。 + // 提交定时任务,每一秒 执行一次。扫描 responseTable 表,将过期的数据移除 this.timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { @@ -5562,16 +5563,16 @@ NettyRemotingServer 类成员变量: 服务器主动向客户端发起请求时,使用三种方法 -* invokeSync(): 同步调用,服务器需要阻塞等待调用的返回结果 +* invokeSync(): 同步调用,**服务器需要阻塞等待调用的返回结果** * `int opaque = request.getOpaque()`:获取请求 ID(与请求码不同) - * `responseFuture = new ResponseFuture(...)`:创建响应对象,将请求 ID、通道、超时时间传入,没有回调函数和 Once + * `responseFuture = new ResponseFuture(...)`:**创建响应对象**,没有回调函数和 Once * `this.responseTable.put(opaque, responseFuture)`:**加入到响应映射表中**,key 为请求 ID * `SocketAddress addr = channel.remoteAddress()`:获取客户端的地址信息 * `channel.writeAndFlush(request).addListener(...)`:将**业务 Command 信息**写入通道,业务线程将数据交给 Netty ,Netty 的 IO 线程接管写刷数据的操作,**监听器由 IO 线程在写刷后回调** * `if (f.isSuccess())`:写入成功会将响应对象设置为成功状态直接 return,写入失败设置为失败状态 * `responseTable.remove(opaque)`:将当前请求的 responseFuture **从映射表移除** * `responseFuture.setCause(f.cause())`:设置错误的信息 - * `responseFuture.putResponse(null)`:请求的业务码设置为 null + * `responseFuture.putResponse(null)`:响应 Command 设置为 null * `responseCommand = responseFuture.waitResponse(timeoutMillis)`:当前线程设置超时时间挂起,**同步等待响应** * `if (null == responseCommand)`:超时或者出现异常,直接报错 * `return responseCommand`:返回响应 Command 信息 @@ -5580,7 +5581,7 @@ NettyRemotingServer 类成员变量: * `if (acquired)`:许可证获取失败说明并发较高,会抛出异常 * `once = new SemaphoreReleaseOnlyOnce(this.semaphoreAsync)`:Once 对象封装了释放信号量的操作 * `costTime = System.currentTimeMillis() - beginStartTime`:计算一下耗费的时间,超时不再发起请求 - * `responseFuture = new ResponseFuture()`:创建响应对象,包装了回调函数和 Once 对象 + * `responseFuture = new ResponseFuture()`:**创建响应对象,包装了回调函数和 Once 对象** * `this.responseTable.put(opaque, responseFuture)`:加入到响应映射表中,key 为请求 ID * `channel.writeAndFlush(request).addListener(...)`:写刷数据 * `if (f.isSuccess())`:写刷成功,设置 responseFuture 发生状态为 true @@ -5629,7 +5630,7 @@ NettyRemotingServer 类成员变量: -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#22-%E5%8D%8F%E8%AE%AE%E8%AE%BE%E8%AE%A1%E4%B8%8E%E7%BC%96%E8%A7%A3%E7%A0%81 +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md @@ -5699,7 +5700,7 @@ NettyRemotingAbstract#processRequestCommand:**处理请求的数据** * `requestTask = new RequestTask(run, ctx.channel(), cmd)`:将任务对象、通道、请求封装成 RequestTask 对象 -* `pair.getObject2().submit(requestTask)`:获取处理器对应的线程池,将 task 提交,**从 IO 线程切换到业务线程** +* `pair.getObject2().submit(requestTask)`:**获取处理器对应的线程池,将 task 提交,从 IO 线程切换到业务线程** NettyRemotingAbstract#processResponseCommand:**处理响应的数据** @@ -5871,7 +5872,7 @@ MappedFile 类成员变量: * 文件相关:CL 是 CommitLog,CQ 是 ConsumeQueue ```java - private String fileName; // 文件名称,CL和CQ文件名是第一条消息的物理偏移量,索引文件是年月日时分秒 + private String fileName; // 文件名称,CL和CQ文件名是【第一条消息的物理偏移量】,索引文件是【年月日时分秒】 private long fileFromOffset;// 文件名转long,代表该对象的【起始偏移量】 private File file; // 文件对象 ``` @@ -5951,19 +5952,19 @@ MappedFile 类核心方法: public boolean destroy(final long intervalForcibly) ``` -* cleanup():释放堆外内存,更新总虚拟内存和总内存映射文件数 +* cleanup():**释放堆外内存**,更新总虚拟内存和总内存映射文件数 ```java public boolean cleanup(final long currentRef) ``` -* warmMappedFile():内存预热,当要新建的 MappedFile 对象大于 1g 时,执行该方法对该 MappedFile 的每个 Page Cache 进行写入一个字节进行分配内存,**将映射文件全部加载到内存** +* warmMappedFile():内存预热,当要新建的 MappedFile 对象大于 1g 时执行该方法。mappedByteBuffer 已经通过mmap映射,此时操作系统中只是记录了该文件和该 Buffer 的映射关系,而并没有映射到物理内存中,对该 MappedFile 的每个 Page Cache 进行写入一个字节分配内存,**将映射文件全部加载到内存** ```java public void warmMappedFile(FlushDiskType type, int pages) ``` -* mlock():锁住指定的内存区域避免被操作系统调到 **swap 空间**,一次性将一段数据读入到映射内存区域,减少了缺页异常的产生 +* mlock():锁住指定的内存区域避免被操作系统调到 swap 空间,减少了缺页异常的产生 ```java public void mlock() @@ -5992,7 +5993,6 @@ ReferenceResource 类核心方法: public void release() ``` - @@ -6217,10 +6217,9 @@ CommitLog 类核心方法: * `if (msg.getDelayTimeLevel() > 0) `:获取消息的延迟级别 * `topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC`:**修改消息的主题为 `SCHEDULE_TOPIC_XXXX`** * `queueId = ScheduleMessageService.delayLevel2QueueId()`:队列 ID 为延迟级别 -1 - * `MessageAccessor.putProperty`:将原来的消息主题和 ID 存入消息的属性 `REAL_TOPIC` 中 - * `msg.setTopic(topic)`:修改主题 + * `MessageAccessor.putProperty`:**将原来的消息主题和 ID 存入消息的属性 `REAL_TOPIC` 中** * `mappedFile = this.mappedFileQueue.getLastMappedFile()`:获取当前顺序写的 MappedFile 对象 - * `putMessageLock.lock()`:获取**写锁** + * `putMessageLock.lock()`:**获取写锁** * `msg.setStoreTimestamp(beginLockTimestamp)`:设置消息的存储时间为获取锁的时间 * `if (null == mappedFile || mappedFile.isFull())`:文件写满了创建新的 MF 对象 * `result = mappedFile.appendMessage(msg, this.appendMessageCallback)`:**消息追加**,核心逻辑在回调器类 @@ -6261,7 +6260,7 @@ CommitLog 类核心方法: * `int index = mappedFiles.size() - 1`:从尾部开始遍历 MFQ,验证 MF 的第一条消息,找到第一个验证通过的文件对象 * `dispatchRequest = this.checkMessageAndReturnSize()`:每次解析出一条 msg 封装成 DispatchRequest 对象 - * `this.defaultMessageStore.doDispatch(dispatchRequest)`:重建 ConsumerQueue 和 Index,避免上次异常停机导致 CQ 和 Index 与 CommitLog 不对齐 + * `this.defaultMessageStore.doDispatch(dispatchRequest)`:**重建 ConsumerQueue 和 Index,避免上次异常停机导致 CQ 和 Index 与 CommitLog 不对齐** * 剩余逻辑与正常关机的恢复方法相似 @@ -6274,14 +6273,14 @@ CommitLog 类核心方法: AppendMessageCallback 消息追加服务实现类为 DefaultAppendMessageCallback -* doAppend() +* doAppend(): ```java public AppendMessageResult doAppend() ``` * `long wroteOffset = fileFromOffset + byteBuffer.position()`:消息写入的位置,物理偏移量 phyOffset - * `String msgId`:消息 ID,规则是客户端 IP + 消息偏移量 phyOffset + * `String msgId`:**消息 ID,规则是客户端 IP + 消息偏移量 phyOffset** * `byte[] topicData`:序列化消息,将消息的字段压入到 msgStoreItemMemory 这个 Buffer 中 * `byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen)`:将 msgStoreItemMemory 中的数据写入 MF 对象的内存映射的 Buffer 中,数据还没落盘 * `AppendMessageResult result`:构造结果对象,包括存储位点、是否成功、队列偏移量等信息 @@ -6301,11 +6300,11 @@ FlushRealTimeService 刷盘 CL 数据,默认是异步刷盘类 FlushRealTimeSe * `int interval`:获取配置中的刷盘时间间隔 - * `int flushPhysicQueueLeastPages`:获取最小刷盘页数,默认是 4 页,脏页达到指定页数才刷盘 + * `int flushPhysicQueueLeastPages`:**获取最小刷盘页数,默认是 4 页**,脏页达到指定页数才刷盘 * `int flushPhysicQueueThoroughInterval`:获取强制刷盘周期,默认是 10 秒,达到周期后强制刷盘,不考虑脏页 - * `if (flushCommitLogTimed)`:休眠逻辑,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 + * `if (flushCommitLogTimed)`:**休眠逻辑**,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 * `CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)`:**刷盘** @@ -6323,7 +6322,7 @@ FlushRealTimeService 刷盘 CL 数据,默认是异步刷盘类 FlushRealTimeSe * `while (!this.isStopped())`:stopped为 true 才跳出循环 - `this.waitForRunning(10)`:线程休眠 10 毫秒,最后调用 `onWaitEnd()` 进行请求的交换 `swapRequests()` + `this.waitForRunning(10)`:线程休眠 10 毫秒,最后调用 `onWaitEnd()` 进行**请求的交换** `swapRequests()` `this.doCommit()`:做提交逻辑 @@ -6342,7 +6341,7 @@ FlushRealTimeService 刷盘 CL 数据,默认是异步刷盘类 FlushRealTimeSe `req.wakeupCustomer(flushOK ? ...)`:设置 Future 结果,在 Future 阻塞的线程在这里会被唤醒 - `this.requestsRead.clear()`:清理 reqeustsRead 列表,方便交换时 成为 requestsWrite 使用 + `this.requestsRead.clear()`:清理 reqeustsRead 列表,方便交换时成为 requestsWrite 使用 * `else`:读请求集合为空 @@ -6377,7 +6376,7 @@ ConsumerQueue 是消息消费队列,存储消息在 CommitLog 的索引,便 ```java private final MappedFileQueue mappedFileQueue; // 文件管理器,管理 CQ 目录下的文件 private final String storePath; // 目录,比如../store/consumequeue/xxx_topic/0 - private final int mappedFileSize; // 每一个 CCQ 存储文件大小,默认 20 * 30w = 600w byte + private final int mappedFileSize; // 每一个 CQ 存储文件大小,默认 20 * 30w = 600w byte ``` * 存储主模块:上层的对象 @@ -6558,7 +6557,8 @@ IndexFile 类方法 * `int slotValue = this.mappedByteBuffer.getInt(absSlotPos)`:获取槽中的值,如果是无效值说明没有哈希冲突 * `timeDiff = timeDiff / 1000`:计算当前 msg 存储时间减去索引文件内第一条消息存储时间的差值,转化为秒进行存储 * `int absIndexPos`:计算当前索引数据存储的位置,开始填充索引数据到对应的位置 - * `this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount())`:在 slot 放入当前索引的索引编号 + * `this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue)`:**hash 桶的原值,头插法** + * `this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader...)`:在 slot 放入当前索引的索引编号 * `if (this.indexHeader.getIndexCount() <= 1)`:索引文件插入的第一条数据,需要设置起始偏移量和存储时间 * `if (invalidIndex == slotValue)`:没有哈希冲突,说明占用了一个新的 hash slot * `this.indexHeader`:设置索引头的相关属性 @@ -6652,11 +6652,11 @@ IndexService 类用来管理 IndexFile 文件 * `indexFile = retryGetAndCreateIndexFile()`:获取或者创建顺序写的索引文件对象 - * `buildKey(topic, req.getUniqKey())`:**构建索引 key**,`topic + # + uniqKey` + * `buildKey(topic, req.getUniqKey())`:**构建索引 key,`topic + # + uniqKey`** * `indexFile = putKey()`:插入索引文件 - * `if (keys != null && keys.length() > 0)`:消息存在自定义索引 + * `if (keys != null && keys.length() > 0)`:消息存在自定义索引 keys `for (int i = 0; i < keyset.length; i++)`:遍历每个索引,为每个 key 调用一次 putKey @@ -6685,7 +6685,7 @@ HAService 类成员变量: ```java // master 节点当前有多少个 slave 节点与其进行数据同步 private final AtomicInteger connectionCount = new AtomicInteger(0); - // master 节点会给每个发起连接的 slave 节点的通道创建一个 HAConnection,控制 master 端向 slave 端传输数据 + // master 节点会给每个发起连接的 slave 节点的通道创建一个 HAConnection,【控制 master 端向 slave 端传输数据】 private final List connectionList = new LinkedList<>(); // master 向 slave 节点推送的最大的 offset,表示数据同步的进度 private final AtomicLong push2SlaveMaxOffset = new AtomicLong(0) @@ -6733,7 +6733,7 @@ HAService 类成员变量: ###### Accept -AcceptSocketService 类用于监听从节点的连接,创建 HAConnection 连接对象 +AcceptSocketService 类用于**监听从节点的连接**,创建 HAConnection 连接对象 成员变量: @@ -6763,13 +6763,6 @@ AcceptSocketService 类用于监听从节点的连接,创建 HAConnection 连 public void beginAccept() ``` - * `this.serverSocketChannel = ServerSocketChannel.open()`:获取服务端 SocketChannel - * `this.selector = RemotingUtil.openSelector()`:获取多路复用器 - * `this.serverSocketChannel.socket().setReuseAddress(true)`:开启通道可重用 - * `this.serverSocketChannel.socket().bind(this.socketAddressListen)`:绑定连接端口 - * `this.serverSocketChannel.configureBlocking(false)`:设置非阻塞 - * `this.serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT)`:将通道注册到多路复用器上,关注 `OP_ACCEPT` 事件 - * run():服务启动 ```java @@ -6804,9 +6797,17 @@ GroupTransferService 用来控制数据同步 ``` * `if (!this.requestsRead.isEmpty())`:读请求不为空 - * `boolean transferOK = HAService.this.push2SlaveMaxOffset.get() >= req.getNextOffset()`:主从同步是否完成 + * `boolean transferOK = HAService.this.push2SlaveMaxOffset... >= req.getNextOffset()`:**主从同步是否完成** * `req.wakeupCustomer(transferOK ? ...)`:唤醒消费者 * `this.requestsRead.clear()`:清空读请求 + +* swapRequests():交换读写请求 + + ```java + private void swapRequests() + ``` + + @@ -6880,7 +6881,7 @@ HAClient 是 slave 端运行的代码,用于和 master 服务器建立长连 * `if (this.isTimeToReportOffset())`:slave 每 5 秒会上报一次 slave 端的同步进度信息给 master - `boolean result = this.reportSlaveMaxOffset()`:上报同步信息,上报失败关闭连接 + `boolean result = this.reportSlaveMaxOffset()`:**上报同步信息**,上报失败关闭连接 * `this.selector.select(1000)`:多路复用器阻塞获取就绪的通道,最多等待 1 秒钟,**获取到就绪事件或者超时后结束** @@ -6920,9 +6921,7 @@ HAClient 是 slave 端运行的代码,用于和 master 服务器建立长连 `boolean result = this.dispatchReadRequest()`:处理数据的核心逻辑 - * `else if (readSize == 0) `:无新数据 - - `if (++readSizeZeroTimes >= 3)`:大于 3 时跳出循环 + * `else if (readSize == 0) `:连续无新数据 3 次,跳出循环 * `else`:readSize = -1 就表示 Socket 处于半关闭状态,对端已经关闭了 @@ -6940,31 +6939,17 @@ HAClient 是 slave 端运行的代码,用于和 master 服务器建立长连 * `if (diff >= msgHeaderSize)`:缓冲区还有完整的协议头 header 数据 - * `long masterPhyOffset, int bodySize`:读取 header 信息 - - * `long slavePhyOffset`:获取 slave 端最大的物理偏移量 + * `if (diff >= (msgHeaderSize + bodySize))`:说明**缓冲区内是包含当前帧的全部数据的**,开始处理帧数据 - * `if (slavePhyOffset != masterPhyOffset)`:正常情况两者是相等的,因为是一帧一帧同步的 - - * `if (diff >= (msgHeaderSize + bodySize))`:说明**缓冲区内是包含当前帧的全部数据的**,开始处理帧数据 - - `byte[] bodyData = new byte[bodySize]`:提取帧内的 body 数据 - - `this.byteBufferRead.position(this.dispatchPosition + msgHeaderSize)`:**设置 pos 为当前帧的 body 起始位置** - - `this.byteBufferRead.get(bodyData)`:读取数据到 bodyData - - `HAService...appendToCommitLog(masterPhyOffset, bodyData)`:存储数据到 CommitLog + `HAService...appendToCommitLog(masterPhyOffset, bodyData)`:**存储数据到 CommitLog** `this.byteBufferRead.position(readSocketPos)`:恢复 byteBufferRead 的 pos 指针 - `this.dispatchPosition += msgHeaderSize + bodySize`:**加一帧数据长度** + `this.dispatchPosition += msgHeaderSize + bodySize`:加一帧数据长度,处理下一条数据使用 `if (!reportSlaveMaxOffsetPlus())`:上报 slave 同步信息 - * `if (!this.byteBufferRead.hasRemaining())`:缓冲区写满了 - - `this.reallocateByteBuffer()`:重新分配缓冲区 + * `if (!this.byteBufferRead.hasRemaining())`:缓冲区写满了,重新分配缓冲区 * reallocateByteBuffer():重新分配缓冲区 @@ -7015,7 +7000,7 @@ HAConnection 类成员变量: private ReadSocketService readSocketService; // 读数据服务 ``` -* 请求位点:在 slave上报本地的进度之后被赋值,该值大于 0 后同步逻辑才会运行,master 如果不知道 slave 节点当前消息的存储进度,就无法给 slave 推送数据 +* 请求位点:在 slave 上报本地的进度之后被赋值,该值大于 0 后同步逻辑才会运行,master 如果不知道 slave 节点当前消息的存储进度,就无法给 slave 推送数据 ```java private volatile long slaveRequestOffset = -1; @@ -7123,20 +7108,16 @@ ReadSocketService 类是一个任务对象,slave 向 master 传输的帧格式 * `if (readSize > 0)`:加载成功,有新数据 - `readSizeZeroTimes = 0`:置为 0 - `if ((byteBufferRead.position() - processPosition) >= 8)`:缓冲区的可读数据最少包含一个数据帧 - * `int pos = ...`:**获取可读帧数据中最后一个完整的帧数据的位点**,后面的数据丢弃 - * `long readOffset = ...byteBufferRead.getLong(pos - 8)`:读取最后一帧数据,slave 端当前的同步进度信息 + * `int pos = ...`:**获取可读帧数据中最后一个完整的帧数据的位点,后面的数据丢弃** + * `long readOffset = ...byteBufferRead.getLong(pos - 8)`:读取最后一帧数据,slave 端当前的同步进度信息 * `this.processPosition = pos`:更新处理位点 * `HAConnection.this.slaveAckOffset = readOffset`:更新应答位点 * `if (HAConnection.this.slaveRequestOffset < 0)`:条件成立**给 slaveRequestOffset 赋值** * `HAConnection...notifyTransferSome(slaveAckOffset)`:**唤醒阻塞的生产者线程** - - * `else if (readSize == 0) `:无新数据 - - `if (++readSizeZeroTimes >= 3)`:大于 3 时跳出循环 + + * `else if (readSize == 0) `:读取 3 次无新数据跳出循环 * `else`:readSize = -1 就表示 Socket 处于半关闭状态,对端已经关闭了 @@ -7218,13 +7199,13 @@ WriteSocketService 类是一个任务对象,master向 slave 传输的数据帧 * `if (this.lastWriteOver)`:上一次待发送数据全部发送完成 - `if (interval > 5)`:超过 5 秒未同步数据,发送一个 header 数据包,维持长连接 + `if (interval > 5)`:**超过 5 秒未同步数据,发送一个 header 心跳数据包,维持长连接** * `else`:上一轮的待发送数据未全部发送,需要同步数据到 slave 节点 - * `SelectMappedBufferResult selectResult`:到 CommitLog 中查询 nextTransferFromWhere 开始位置的数据 + * `SelectMappedBufferResult selectResult`:**到 CommitLog 中查询 nextTransferFromWhere 开始位置的数据** - * `if (size > 32k)`:**一次最多同步 32k 数据** + * `if (size > 32k)`:一次最多同步 32k 数据 * `this.nextTransferFromWhere += size`:增加 size,下一轮传输跳过本帧数据 @@ -7248,18 +7229,8 @@ WriteSocketService 类是一个任务对象,master向 slave 传输的数据帧 * `int writeSize = this.socketChannel.write(this.byteBufferHeader)`:向通道写帧头数据 - * `if (writeSize > 0)`:写数据成功 - - `writeSizeZeroTimes = 0`:控制变量置为 0 - - * `else if (readSize == 0)`:写失败 - - `if (++readSizeZeroTimes >= 3)`:大于 3 时跳出循环 - * `if (null == this.selectMappedBufferResult)`:说明是心跳数据,返回心跳数据是否发送完成 - * `writeSizeZeroTimes = 0`:控制变量置为 0 - * `if (!this.byteBufferHeader.hasRemaining())`:**Header写成功之后,才进行写 Body** * `while (this.selectMappedBufferResult.getByteBuffer().hasRemaining())`:**数据缓冲区有待发送的数据** @@ -7268,11 +7239,7 @@ WriteSocketService 类是一个任务对象,master向 slave 传输的数据帧 * `if (writeSize > 0)`:写数据成功,但是不代表 SMBR 中的数据全部写完成 - `writeSizeZeroTimes = 0`:控制变量置为 0 - - * `else if (readSize == 0)`:写失败,因为 Socket 写缓冲区写满了 - - `if (++readSizeZeroTimes >= 3)`:大于 3 时跳出循环 + * `boolean result`:判断是否发送完成,返回该值 @@ -7299,18 +7266,12 @@ DefaultMessageStore 类核心是整个存储服务的调度类 * `this.allocateMappedFileService.start()`:启动**创建 MappedFile 文件服务** * `this.indexService.start()`:启动索引服务 -* load():加载资源 +* load():先加载 CommitLog,再加载 ConsumeQueue,最后加载 IndexFile,加载完进入恢复阶段,先恢复 CQ,在恢复 CL ```java public boolean load() ``` - * `this.commitLog.load()`:先加载 CommitLog - * `this.loadConsumeQueue()`:再加载 ConsumeQueue - * `this.storeCheckpoint`:检查位点对象 - * `this.indexService.load(lastExitOK)`:加载 IndexFile - * `this.recover(lastExitOK)`:恢复阶段,先恢复 CQ,在恢复 CL - * start():核心启动方法 ```java @@ -7337,18 +7298,18 @@ DefaultMessageStore 类核心是整个存储服务的调度类 * `this.storeStatsService.start()`:启动状态存储服务 - * `this.createTempFile()`:创建 AbortFile,正常关机时 JVM HOOK 会删除该文件,异常宕机时该文件不会删除,开机数据恢复阶段根据是否存在该文件,执行不同的**恢复策略** + * `this.createTempFile()`:创建 AbortFile,正常关机时 JVM HOOK 会删除该文件,**异常宕机时该文件不会删除**,开机数据恢复阶段根据是否存在该文件,执行不同的恢复策略 * `this.addScheduleTask()`:添加定时任务 - * `DefaultMessageStore.this.cleanFilesPeriodically()`:定时**清理过期文件**,周期是 10 秒 + * `DefaultMessageStore.this.cleanFilesPeriodically()`:**定时清理过期文件**,周期是 10 秒 * `this.cleanCommitLogService.run()`:启动清理过期的 CL 文件服务 * `this.cleanConsumeQueueService.run()`:启动清理过期的 CQ 文件服务 * `DefaultMessageStore.this.checkSelf()`:每 10 分种进行健康检查 - * `DefaultMessageStore.this.cleanCommitLogService.isSpaceFull()`:**磁盘预警**定时任务,每 10 秒一次 + * `DefaultMessageStore.this.cleanCommitLogService.isSpaceFull()`:**磁盘预警定时任务**,每 10 秒一次 * `if (physicRatio > this.diskSpaceWarningLevelRatio)`:检查磁盘是否到达 waring 阈值,默认 90% @@ -7424,7 +7385,7 @@ ServiceThread 类被很多服务继承,本身是一个 Runnable 任务对象 ##### 构建服务 -AllocateMappedFileService 创建 MappedFile 服务 +AllocateMappedFileService **创建 MappedFile 服务** * mmapOperation():核心服务 @@ -7432,7 +7393,7 @@ AllocateMappedFileService 创建 MappedFile 服务 private boolean mmapOperation() ``` - * `req = this.requestQueue.take()`: 从 requestQueue 阻塞队列(优先级)中获取 AllocateRequest 任务 + * `req = this.requestQueue.take()`: **从 requestQueue 阻塞队列(优先级)中获取 AllocateRequest 任务** * `if (...isTransientStorePoolEnable())`:条件成立使用直接内存写入数据, 从直接内存中 commit 到 FileChannel 中 * `mappedFile = new MappedFile(req.getFilePath(), req.getFileSize())`:根据请求的路径和大小创建对象 * `mappedFile.warmMappedFile()`:判断 mappedFile 大小,只有 CommitLog 才进行文件预热 @@ -7450,7 +7411,7 @@ AllocateMappedFileService 创建 MappedFile 服务 * `result.getCountDownLatch().await(...)`:**线程挂起**,直到超时或者 nextFilePath 对应的 MF 文件创建完成 * `return result.getMappedFile()`:返回创建好的 MF 文件对象 -ReputMessageService 消息分发服务,用于构建 ConsumerQueue 和 IndexFile 文件 +ReputMessageService 消息分发服务,用于构**建 ConsumerQueue 和 IndexFile 文件** * run():循环执行 doReput 方法,每执行一次线程休眠 1 毫秒 @@ -7573,7 +7534,7 @@ CleanConsumeQueueService 清理过期的 CQ 数据 * `if (minOffset > this.lastPhysicalMinOffset)`:CL 最小的偏移量大于 CQ 最小的,说明有过期数据 * `this.lastPhysicalMinOffset = minOffset`:更新 CQ 的最小偏移量 * `for (ConsumeQueue logic : maps.values())`:遍历所有的 CQ 文件 - * `logic.deleteExpiredFile(minOffset)`:调用 MFQ 对象的删除方法 + * `logic.deleteExpiredFile(minOffset)`:**调用 MFQ 对象的删除方法** * `DefaultMessageStore.this.indexService.deleteExpiredFile(minOffset)`:**删除过期的索引文件** diff --git a/Java.md b/Java.md index cbd5bdf..428fde5 100644 --- a/Java.md +++ b/Java.md @@ -1520,6 +1520,10 @@ public class FinalDemo { +*** + + + ##### 实例变量 final 修饰变量的总规则:有且仅能被赋值一次 diff --git a/Prog.md b/Prog.md index 2b83ba3..f5c8db8 100644 --- a/Prog.md +++ b/Prog.md @@ -7336,7 +7336,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 try { // 在条件队列 available 使用带超时的挂起(堆顶任务.time - now() 纳秒值..) available.awaitNanos(delay); - // 到达阻塞时间时,当前线程会从来 + // 到达阻塞时间时,当前线程会从这里醒来来 } finally { // t堆顶更新,leader 置为 null,offer 方法释放锁后, // 有其它线程通过 take/poll 拿到锁,读到 leader == null,然后将自身更新为leader。 @@ -7348,7 +7348,8 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 } } } finally { - // 没有 leader 线程,头结点不为 null,唤醒阻塞获取头节点的线程 + // 没有 leader 线程,头结点不为 null,唤醒阻塞获取头节点的线程, + // 【如果没有这一步,就会出现有了需要执行的任务,但是没有线程去执行】 if (leader == null && queue[0] != null) available.signal(); lock.unlock(); @@ -13141,7 +13142,7 @@ final void updateHead(Node h, Node p) { 2. IP 地址:互联网协议地址(Internet Protocol Address),用来给一个网络中的计算机设备做唯一的编号 * IPv4:4个字节,32 位组成,192.168.1.1 - * Pv6:可以实现为所有设备分配 IP,128 位 + * IPv6:可以实现为所有设备分配 IP,128 位 * ipconfig:查看本机的 IP * ping 检查本机与某个 IP 指定的机器是否联通,或者说是检测对方是否在线。 From 401ab6f35cf4b0faf6e67e48a308bef9b6d5475e Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 7 Mar 2022 02:26:01 +0800 Subject: [PATCH 02/51] Update Java Note --- DB.md | 2 +- Frame.md | 411 ++++++++++++++++++++++++++----------------------------- Java.md | 4 +- Prog.md | 8 ++ SSM.md | 7 +- 5 files changed, 209 insertions(+), 223 deletions(-) diff --git a/DB.md b/DB.md index 060767d..67e2bdd 100644 --- a/DB.md +++ b/DB.md @@ -6471,7 +6471,7 @@ InnoDB 刷脏页的控制策略: ### 内存结构 -对一条记录加锁的本质就是在内存中创建一个锁结构与之关联,结构包括 +对一条记录加锁的本质就是**在内存中**创建一个锁结构与之关联,结构包括 * 事务信息:锁对应的事务信息,一个锁属于一个事务 * 索引信息:对于行级锁,需要记录加锁的记录属于哪个索引 diff --git a/Frame.md b/Frame.md index 7d86be6..c884974 100644 --- a/Frame.md +++ b/Frame.md @@ -4312,19 +4312,19 @@ RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交 * 服务端响应消息写入结果(如果写入失败,此时 Half 消息对业务不可见) * 根据发送结果执行本地事务 - * 根据本地事务状态执行 Commit 或者 Rollback(Commit 操作生成消息索引,消息对消费者可见) + * 根据本地事务状态执行 Commit 或者 Rollback ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-事务工作流程.png) -2. 补偿流程: +2. 补偿机制:用于解决消息 Commit 或者 Rollback 发生超时或者失败的情况,比如出现网络问题 - * 对没有 Commit/Rollback 的事务消息(pending 状态的消息),服务端根据根据半消息的生产者组,到 ProducerManager 中获取生产者的会话通道,发起一次回查(**单向请求**) + * Broker 服务端通过对比 Half 消息和 Op 消息,对未确定状态的消息推进 CheckPoint(记录哪些事务消息的状态是确定的) + * 没有 Commit/Rollback 的事务消息,服务端根据根据半消息的生产者组,到 ProducerManager 中获取生产者(同一个 Group 的 Producer)的会话通道,发起一次回查(**单向请求**) * Producer 收到回查消息,检查事务消息状态表内对应的本地事务的状态 - * 根据本地事务状态,重新 Commit 或者 Rollback - 补偿阶段用于解决消息 Commit 或者 Rollback 发生超时或者失败的情况 + 注意:RocketMQ 并不会无休止的进行事务状态回查,默认回查 15 次,如果 15 次回查还是无法得知事务状态,则默认回滚该消息 @@ -4332,9 +4332,9 @@ RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交 -#### 原理解析 +#### 两阶段 -##### 不可见性 +##### 一阶段 事务消息相对普通消息最大的特点就是**一阶段发送的消息对用户是不可见的**,因为对于 Half 消息,会备份原消息的主题与消息消费队列,然后改变主题为 RMQ_SYS_TRANS_HALF_TOPIC,由于消费组未订阅该主题,故消费端无法消费 Half 类型的消息 @@ -4344,42 +4344,26 @@ RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 T -**** +*** -##### OP 消息 +##### 二阶段 一阶段写入不可见的消息后,二阶段操作: * 如果执行 Commit 操作,则需要让消息对用户可见,构建出 Half 消息的索引。一阶段的 Half 消息写到一个特殊的 Topic,构建索引时需要读取出 Half 消息,然后通过一次普通消息的写入操作将 Topic 和 Queue 替换成真正的目标 Topic 和 Queue,生成一条对用户可见的消息。其实就是利用了一阶段存储的消息的内容,在二阶段时恢复出一条完整的普通消息,然后走一遍消息写入流程 -* 如果是 Rollback 则需要撤销一阶段的消息,因为消息本就不可见,所以并**不需要真正撤销消息**(实际上 RocketMQ 也无法去删除一条消息,因为是顺序写文件的)。RocketMQ 为了区分这条消息没有确定状态的消息(Pending 状态),采用 Op 消息标识已经确定状态的事务消息(Commit 或者 Rollback) +* 如果是 Rollback 则需要撤销一阶段的消息,因为消息本就不可见,所以并**不需要真正撤销消息**(实际上 RocketMQ 也无法去删除一条消息,因为是顺序写文件的)。RocketMQ 为了区分这条消息没有确定状态的消息,采用 Op 消息标识已经确定状态的事务消息(Commit 或者 Rollback) -事务消息无论是 Commit 或者 Rollback 都会记录一个 Op 操作,两者的区别是 Commit 相对于 Rollback 在写入 Op 消息前创建 Half 消息的索引。如果一条事务消息没有对应的 Op 消息,说明这个事务的状态还无法确定(可能是二阶段失败了) +**事务消息无论是 Commit 或者 Rollback 都会记录一个 Op 操作**,两者的区别是 Commit 相对于 Rollback 在写入 Op 消息前创建 Half 消息的索引。如果一条事务消息没有对应的 Op 消息,说明这个事务的状态还无法确定(可能是二阶段失败了) -RocketMQ 将 Op 消息写入到全局一个特定的 Topic 中,通过源码中的方法 `TransactionalMessageUtil.buildOpTopic()`,这个主题是一个内部的 Topic(像 Half 消息的 Topic 一样),不会被用户消费。Op 消息的内容为对应的 Half 消息的存储的 Offset,这样**通过 Op 消息能索引到 Half 消息**进行后续的回查操作 +RocketMQ 将 Op 消息写入到全局一个特定的 Topic 中,通过源码中的方法 `TransactionalMessageUtil.buildOpTopic()`,这个主题是一个内部的 Topic(像 Half 消息的 Topic 一样),不会被用户消费。Op 消息的内容为对应的 Half 消息的存储的 Offset,这样**通过 Op 消息能索引到 Half 消息** ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-OP消息.png) -**** - - - -##### 补偿机制 - -如果在 RocketMQ 事务消息的二阶段过程中失败了,例如在做 Commit 操作时,出现网络问题导致 Commit 失败,那么需要通过一定的策略使这条消息最终被 Commit,RocketMQ 采用了一种补偿机制,称为回查 - -Broker 服务端通过对比 Half 消息和 Op 消息,对未确定状态的消息发起回查并且推进 CheckPoint(记录哪些事务消息的状态是确定的),将消息发送到对应的 Producer 端(同一个 Group 的 Producer),由 Producer 根据消息来检查本地事务的状态,然后执行提交或回滚 - -注意:RocketMQ 并不会无休止的进行事务状态回查,默认回查 15 次,如果 15 次回查还是无法得知事务状态,则默认回滚该消息 - - - - - **** @@ -4511,7 +4495,7 @@ NameServer 主要包括两个功能: NameServer 特点: -* NameServer 通常是集群的方式部署,各实例间相互不进行信息通讯 +* NameServer 通常是集群的方式部署,**各实例间相互不进行信息通讯** * Broker 向每一台 NameServer 注册自己的路由信息,所以每个 NameServer 实例上面**都保存一份完整的路由信息** * 当某个 NameServer 因某种原因下线了,Broker 仍可以向其它 NameServer 同步其路由信息 @@ -4791,13 +4775,13 @@ RocketMQ 中的负载均衡可以分为 Producer 端发送消息时候的负载 Producer 端在发送消息时,会先根据 Topic 找到指定的 TopicPublishInfo,在获取了 TopicPublishInfo 路由信息后,RocketMQ 的客户端在默认方式调用 `selectOneMessageQueue()` 方法从 TopicPublishInfo 中的 messageQueueList 中选择一个队列 MessageQueue 进行发送消息 -默认会轮询所有的 Message Queue 发送,以让消息平均落在不同的 queue 上,而由于 queue可以散落在不同的 Broker,所以消息就发送到不同的 Broker 下,图中箭头线条上的标号代表顺序,发布方会把第一条消息发送至 Queue 0,然后第二条消息发送至 Queue 1,以此类推: +默认会**轮询所有的 Message Queue 发送**,以让消息平均落在不同的 queue 上,而由于 queue可以散落在不同的 Broker,所以消息就发送到不同的 Broker 下,图中箭头线条上的标号代表顺序,发布方会把第一条消息发送至 Queue 0,然后第二条消息发送至 Queue 1,以此类推: ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-producer负载均衡.png) 容错策略均在 MQFaultStrategy 这个类中定义,有一个 sendLatencyFaultEnable 开关变量: -* 如果开启,会在随机(只有初始化索引变量时才随机,正常都是递增)递增取模的基础上,再过滤掉 not available 的 Broker 代理 +* 如果开启,会在**随机(只有初始化索引变量时才随机,正常都是递增)递增取模**的基础上,再过滤掉 not available 的 Broker * 如果关闭,采用随机递增取模的方式选择一个队列(MessageQueue)来发送消息 LatencyFaultTolerance 机制是实现消息发送高可用的核心关键所在,对之前失败的,按一定的时间做退避。例如上次请求的 latency 超过 550Lms,就退避 3000Lms;超过 1000L,就退避 60000L @@ -4927,8 +4911,8 @@ IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName 如下方法可以设置消息重投策略: -- retryTimesWhenSendFailed:同步发送失败重投次数,默认为 2,因此生产者会最多尝试发送 retryTimesWhenSendFailed + 1 次。不会选择上次失败的 Broker,尝试向其他 Broker 发送,最大程度保证消息不丢。超过重投次数抛出异常,由客户端保证消息不丢。当出现 RemotingException、MQClientException 和部分 MQBrokerException 时会重投 -- retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他 Broker,仅在同一个 Broker 上做重试,不保证消息不丢 +- retryTimesWhenSendFailed:同步发送失败重投次数,默认为 2,因此生产者会最多尝试发送 retryTimesWhenSendFailed + 1 次。不会选择上次失败的 Broker,尝试向其他 Broker 发送,**最大程度保证消息不丢**。超过重投次数抛出异常,由客户端保证消息不丢。当出现 RemotingException、MQClientException 和部分 MQBrokerException 时会重投 +- retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他 Broker,仅在同一个 Broker 上做重试,**不保证消息不丢** - retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或 slave 不可用(返回状态非 SEND_OK),是否尝试发送到其他 Broker,默认 false,十分重要消息可以开启 注意点: @@ -5075,7 +5059,7 @@ public class MessageListenerImpl implements MessageListener { 死信队列具有以下特性: -- 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例 +- **一个死信队列对应一个 Group ID, 而不是对应单个消费者实例** - 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列 - 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic @@ -5164,7 +5148,7 @@ At least Once 机制保证消息不丢失,但是可能会造成消息重复, ## 原理解析 -### 服务端 +### Namesrv #### 服务启动 @@ -5685,7 +5669,7 @@ NettyRemotingAbstract#processRequestCommand:**处理请求的数据** * `doAfterRpcHooks()`:RPC HOOK 后置处理 * `if (!cmd.isOnewayRPC())`:条件成立说明不是单向请求,需要结果 * `response.setOpaque(opaque)`:将请求 ID 设置到 response - * `response.markResponseType()`:设置当前的处理是响应处理 + * `response.markResponseType()`:**设置当前请求是响应** * `ctx.writeAndFlush(response)`: **将响应数据交给 Netty IO 线程,完成数据写和刷** * `if (pair.getObject1() instanceof AsyncNettyRequestProcessor)`:Nameserver 默认使用 DefaultRequestProcessor 处理器,是一个 AsyncNettyRequestProcessor 子类 @@ -5842,7 +5826,7 @@ RouteInfoManager#registerBroker:注册 Broker 的信息 -### 存储端 +### Broker #### MappedFile @@ -6214,9 +6198,9 @@ CommitLog 类核心方法: * `msg.setStoreTimestamp(System.currentTimeMillis())`:设置存储时间,后面获取到写锁后这个事件会重写 * `msg.setBodyCRC(UtilAll.crc32(msg.getBody()))`:获取消息的 CRC 值 * `topic、queueId`:获取主题和队列 ID - * `if (msg.getDelayTimeLevel() > 0) `:获取消息的延迟级别 + * `if (msg.getDelayTimeLevel() > 0) `:**获取消息的延迟级别,这里是延迟消息实现的关键** * `topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC`:**修改消息的主题为 `SCHEDULE_TOPIC_XXXX`** - * `queueId = ScheduleMessageService.delayLevel2QueueId()`:队列 ID 为延迟级别 -1 + * `queueId = ScheduleMessageService.delayLevel2QueueId()`:**队列 ID 为延迟级别 -1** * `MessageAccessor.putProperty`:**将原来的消息主题和 ID 存入消息的属性 `REAL_TOPIC` 中** * `mappedFile = this.mappedFileQueue.getLastMappedFile()`:获取当前顺序写的 MappedFile 对象 * `putMessageLock.lock()`:**获取写锁** @@ -7660,6 +7644,7 @@ BrokerStartup#createBrokerController:构造控制器,并初始化 * `final BrokerController controller()`:创建实例对象 * `boolean initResult = controller.initialize()`:控制器初始化 * `this.registerProcessor()`:**注册了处理器,包括发送消息、拉取消息、查询消息等核心处理器** + * `initialTransaction()`:初始化了事务服务,用于进行**事务回查** BrokerController#start:核心启动方法 @@ -7669,6 +7654,8 @@ BrokerController#start:核心启动方法 * `this.fileWatchService.start()`:启动文件监听服务 +* `startProcessorByHa(messageStoreConfig.getBrokerRole())`:**启动事务回查** + * `this.scheduledExecutorService.scheduleAtFixedRate()`:每隔 30s 向 NameServer 上报 Topic 路由信息,**心跳机制** `BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister())` @@ -7679,7 +7666,7 @@ BrokerController#start:核心启动方法 -### 生产者 +### Producer #### 生产者类 @@ -7884,13 +7871,17 @@ DefaultMQProducerImpl 类是默认的生产者实现类 * start():启动方法,参数默认是 true,代表正常的启动路径 + ```java + public void start(final boolean startFactory) + ``` + * `this.serviceState = ServiceState.START_FAILED`:先修改为启动失败,成功后再修改,这种思想很常见 * `this.checkConfig()`:判断生产者组名不能是空,也不能是 default_PRODUCER * `if (!getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP))`:条件成立说明当前生产者不是内部产生者,内部生产者是**处理消息回退**的这种情况使用的生产者 - `this.defaultMQProducer.changeInstanceNameToPID()`:正常的生产者,修改生产者实例名称为当前进程的 PID + `this.defaultMQProducer.changeInstanceNameToPID()`:修改生产者实例名称为当前进程的 PID * ` this.mQClientFactory = ...`:获取当前进程的 MQ 客户端实例对象,从 factoryTable 中获取 key 为 客户端 ID,格式是`ip@pid`,**一个 JVM 进程只有一个 PID,也只有一个 MQClientInstance** @@ -7898,14 +7889,11 @@ DefaultMQProducerImpl 类是默认的生产者实现类 * `this.topicPublishInfoTable.put(...)`:添加一个主题发布信息,key 是 **TBW102** ,value 是一个空对象 - * `if (startFactory) `:正常启动路径 - - `mQClientFactory.start()`:启动 RocketMQ 客户端实例对象 + * `mQClientFactory.start()`:启动 RocketMQ 客户端实例对象 - * `this.serviceState = ServiceState.RUNNING`:修改生产者实例的状态 + * `this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()`:RocketMQ **客户端实例向已知的 Broker 节点发送一次心跳**(也是定时任务) - * `this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()`:RocketMQ 客户端实例向已知的 Broker 节点发送一次心跳(也是定时任务) - * `this.timer.scheduleAtFixedRate()`: request 发送的回执信息,启动定时任务每秒一次删除超时请求 + * `this.timer.scheduleAtFixedRate()`: request 发送的消息需要消费着回执信息,启动定时任务每秒一次删除超时请求 * 生产者 msg 添加信息关联 ID 发送到 Broker * 消费者从 Broker 拿到消息后会检查 msg 类型是一个需要回执的消息,处理完消息后会根据 msg 关联 ID 和客户端 ID 生成一条响应结果消息发送到 Broker,Broker 判断为回执消息,会根据客户端ID 找到 channel 推送给生产者 @@ -7920,50 +7908,33 @@ DefaultMQProducerImpl 类是默认的生产者实现类 * `this.makeSureStateOK()`:校验生产者状态是运行中,否则抛出异常 - * `Validators.checkMessage(msg, this.defaultMQProducer)`:校验消息规格 - - * `long beginTimestampPrev, endTimestamp`:本轮发送的开始时间和本轮的结束时间 - * `topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic())`:**获取当前消息主题的发布信息** - * `this.topicPublishInfoTable.get(topic)`:尝试从本地主题发布信息映射表获取信息,不空直接返回 - - * `if (null == topicPublishInfo || !topicPublishInfo.ok())`:本地没有需要去 MQ 客户端获取 + * `this.topicPublishInfoTable.get(topic)`:先尝试从本地主题发布信息映射表获取信息,获取不到继续执行 - `this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo())`:保存一份空数据 + * `this.mQClientFactory.update...FromNameServer(topic)`:然后从 Namesrv 更新该 Topic 的路由数据 - `this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic)`:从 Namesrv 更新该 Topic 的路由数据 + * `this.mQClientFactory.update...FromNameServer(...)`:**路由数据是空,获取默认 TBW102 的数据** - `topicPublishInfo = this.topicPublishInfoTable.get(topic)`:重新从本地获取发布信息 - - * `this.mQClientFactory.updateTopicRouteInfoFromNameServer(..)`:**路由数据是空,获取默认 TBW102 的数据** - - * `return topicPublishInfo`:返回 TBW102 主题的发布信息 - - * `int timesTotal, times `:发送的总尝试次数和当前是第几次发送 + `return topicPublishInfo`:返回 TBW102 主题的发布信息 * `String[] brokersSent = new String[timesTotal]`:下标索引代表第几次发送,值代表这次发送选择 Broker name - * `for (; times < timesTotal; times++)`:循环发送,发送成功或者发送尝试次数达到上限,结束循环 + * `for (; times < timesTotal; times++)`:循环发送,**发送成功或者发送尝试次数达到上限,结束循环** * `String lastBrokerName = null == mq ? null : mq.getBrokerName()`:获取上次发送失败的 BrokerName - * `mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName)`:**从发布信息中选择一个队列** + * `mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName)`:从发布信息中选择一个队列,生产者的**负载均衡策略**,参考系统特性章节 - * `if (this.sendLatencyFaultEnable)`:默认不开启,可以通过配置开启 - * `return tpInfo.selectOneMessageQueue(lastBrokerName)`:默认选择队列的方式,就是循环主题全部的队列 - * `brokersSent[times] = mq.getBrokerName()`:将本次选择的 BrokerName 存入数组 - * `msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()))`:**重投的消息需要加上标记** + * `msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()))`:**产生重投,重投消息需要加上标记** * `sendResult = this.sendKernelImpl`:核心发送方法 - * `this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false)`:更新一下时间 - - * `switch (communicationMode)`:异步或者单向消息直接返回 null,同步发送进入逻辑判断 + * `switch (communicationMode)`:异步或者单向消息直接返回 null,异步通过回调函数处理,同步发送进入逻辑判断 - `if (sendResult.getSendStatus() != SendStatus.SEND_OK)`:**服务端 Broker 存储失败**,需要重试其他 Broker + `if (sendResult.getSendStatus() != SendStatus.SEND_OK)`:**服务端 Broker 存储失败,需要重试其他 Broker** * `throw new MQClientException()`:未找到当前主题的路由数据,无法发送消息,抛出异常 @@ -7982,7 +7953,7 @@ DefaultMQProducerImpl 类是默认的生产者实现类 * `if (!(msg instanceof MessageBatch))`:非批量消息,需要重新设置消息 ID - `MessageClientIDSetter.setUniqID(msg)`:msg id 由两部分组成,一部分是 ip 地址、进程号、ClassLoader 的 hashcode,另一部分是时间差(当前时间减去当月一号的时间)和计数器的值 + `MessageClientIDSetter.setUniqID(msg)`:**msg id 由两部分组成**,一部分是 ip 地址、进程号、Classloader 的 hashcode,另一部分是时间差(当前时间减去当月一号的时间)和计数器的值 * `if (this.tryToCompressMessage(msg))`:判断消息是否压缩,压缩需要设置压缩标记 @@ -7998,7 +7969,7 @@ DefaultMQProducerImpl 类是默认的生产者实现类 * `request = RemotingCommand.createRequestCommand()`:创建一个 RequestCommand 对象 * `request.setBody(msg.getBody())`:**将消息放入请求体** - * `switch (communicationMode)`:根据不同的模式 invoke 不同的方法 + * `switch (communicationMode)`:**根据不同的模式 invoke 不同的方法** * request():请求方法,消费者回执消息,这种消息是异步消息 @@ -8017,7 +7988,7 @@ DefaultMQProducerImpl 类是默认的生产者实现类 return this.responseMsg; } - * 当消息被消费后,会获取消息的关联 ID,从映射表中获取消息的 RequestResponseFuture,执行下面的方法唤醒挂起线程 + * 当消息被消费后,客户端处理响应时通过消息的关联 ID,从映射表中获取消息的 RequestResponseFuture,执行下面的方法唤醒挂起线程 ```java public void putResponseMessage(final Message responseMsg) { @@ -8360,7 +8331,7 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 * `if (null == this.clientConfig.getNamesrvAddr())`:Namesrv 地址是空,需要两分钟拉取一次 Namesrv 地址 - * 定时任务 1:从 Namesrv 更新客户端本地的路由数据,周期 30 秒一次 + * 定时任务 1:**从 Namesrv 更新客户端本地的路由数据**,周期 30 秒一次 ```java // 获取生产者和消费者订阅的主题集合,遍历集合,对比从 namesrv 拉取最新的主题路由数据和本地数据,是否需要更新 @@ -8369,8 +8340,8 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 * 定时任务 2:周期 30 秒一次,两个任务 - * 清理下线的 Broker 节点,遍历客户端的 Broker 物理节点映射表,将所有主题数据都不包含的 Broker 物理节点清理掉,如果被清理的 Broker 下所有的物理节点都没有了,就将该 Broker 的映射数据删除掉 - * 向在线的所有的 Broker 发送心跳数据,**同步发送的方式**,返回值是 Broker 物理节点的版本号,更新版本映射表 + * **清理下线的 Broker 节点**,遍历客户端的 Broker 物理节点映射表,将所有主题数据都不包含的 Broker 物理节点清理掉,如果被清理的 Broker 下所有的物理节点都没有了,就将该 Broker 的映射数据删除掉 + * **向在线的所有的 Broker 发送心跳数据**,同步发送的方式,返回值是 Broker 物理节点的版本号,更新版本映射表 ```java MQClientInstance.this.cleanOfflineBroker(); @@ -8407,8 +8378,6 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 `topicRouteData = ...getDefaultTopicRouteInfoFromNameServer()`:从 Namesrv 获取默认的 TBW102 的路由数据 - `int queueNums`:遍历所有队列,为每个读写队列设置较小的队列数 - * `topicRouteData = ...getTopicRouteInfoFromNameServer(topic)`:需要**从 Namesrv 获取**路由数据(同步) * `old = this.topicRouteTable.get(topic)`:获取客户端实例本地的该主题的路由数据 @@ -8417,16 +8386,14 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 * `if (changed)`:不一致进入更新逻辑 - `cloneTopicRouteData = topicRouteData.cloneTopicRouteData()`:克隆一份最新数据 - `Update Pub info`:更新生产者信息 - * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:**将主题路由数据转化为发布数据** + * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:将主题路由数据转化为发布数据,会**创建消息队列 MQ**,放入发布数据对象的集合中 * `impl.updateTopicPublishInfo(topic, publishInfo)`:生产者将主题的发布数据保存到它本地,方便发送消息使用 - - `Update sub info`:更新消费者信息 - - `this.topicRouteTable.put(topic, cloneTopicRouteData)`:将数据放入本地路由表 + + `Update sub info`:更新消费者信息,创建 MQ 队列,更新订阅信息,用于负载均衡 + + `this.topicRouteTable.put(topic, cloneTopicRouteData)`:**将数据放入本地路由表** @@ -8596,7 +8563,7 @@ NettyRemotingClient 类负责客户端的网络通信 ##### 消息处理 -BrokerStartup 初始化 BrokerController 调用 `registerProcessor()` 方法将 SendMessageProcessor 注册到 NettyRemotingServer 中,对应的请求 ID 为 `SEND_MESSAGE = 10`,NettyServerHandler 在处理请求时通过请求 ID 会获取处理器执行 processRequest +BrokerStartup 初始化 BrokerController 调用 `registerProcessor()` 方法将 SendMessageProcessor 注册到 NettyRemotingServer 中,对应的请求 ID 为 `SEND_MESSAGE = 10`,NettyServerHandler 在处理请求时通过 CMD 会获取处理器执行 processRequest ```java // 参数一:处理通道的事件; 参数二:客户端 @@ -8620,9 +8587,9 @@ SendMessageProcessor#asyncConsumerSendMsgBack:异步发送消费者的回调 * `if ()`:鉴权,是否找到订阅组配置、Broker 是否支持写请求、订阅组是否支持消息重试 -* `String newTopic = MixAll.getRetryTopic(...)`:获取**消费者组的重试主题**,规则是 `%RETRY%GroupName` +* `String newTopic = MixAll.getRetryTopic(...)`:**获取消费者组的重试主题**,规则是 `%RETRY%GroupName` -* `int queueIdInt = Math.abs()`:充实主题下的队列 ID 是 0 +* `int queueIdInt = Math.abs()`:**重试主题下的队列 ID 是 0** * `TopicConfig topicConfig`:获取重试主题的配置信息 @@ -8636,7 +8603,7 @@ SendMessageProcessor#asyncConsumerSendMsgBack:异步发送消费者的回调 * `if (msgExt...() >= maxReconsumeTimes || delayLevel < 0)`:消息重试次数超过最大次数,不支持重试 - `newTopic = MixAll.getDLQTopic()`:获取消费者的死信队列,规则是 `%DLQ%GroupName` + `newTopic = MixAll.getDLQTopic()`:**获取消费者的死信队列**,规则是 `%DLQ%GroupName` `queueIdInt, topicConfig`:死信队列 ID 为 0,创建死信队列的配置 @@ -8644,7 +8611,7 @@ SendMessageProcessor#asyncConsumerSendMsgBack:异步发送消费者的回调 `delayLevel = 3 + msgExt.getReconsumeTimes()`:**延迟级别默认从 3 级开始**,每重试一次,延迟级别 +1 -* `msgExt.setDelayTimeLevel(delayLevel)`:**将延迟级别设置进消息属性**,存储时会检查该属性,该属性值 > 0 会将消息的主题和队列再次修改,修改为调度主题和调度队列 ID +* `msgExt.setDelayTimeLevel(delayLevel)`:**将延迟级别设置进消息属性**,存储时会检查该属性,该属性值 > 0 会**将消息的主题和队列修改为调度主题和调度队列 ID** * `MessageExtBrokerInner msgInner`:创建一条空消息,消息属性从 offset 查询出来的 msg 中拷贝 @@ -8702,7 +8669,7 @@ DefaultMessageStore 中有成员属性 ScheduleMessageService,在 start 方法 成员方法: -* load():加载调度消息,初始化 delayLevelTable 和 offsetTable +* load():加载调度消息,**初始化 delayLevelTable 和 offsetTable** ```java public boolean load() @@ -8718,7 +8685,7 @@ DefaultMessageStore 中有成员属性 ScheduleMessageService,在 start 方法 * `this.timer`:创建定时器对象 - * `for (... : this.delayLevelTable.entrySet())`:为**每个延迟级别创建一个延迟任务**提交到 timer ,延迟 1 秒后执行 + * `for (... : this.delayLevelTable.entrySet())`:为**每个延迟级别创建一个延迟任务**提交到 timer ,这样就可以**将延迟消息得到及时的消费** * `this.timer.scheduleAtFixedRate()`:提交周期型任务,延迟 10 秒执行,周期为 10 秒,持久化延迟队列消费进度任务 @@ -8765,7 +8732,7 @@ DeliverDelayedMessageTimerTask 是一个任务类 public void executeOnTimeup() ``` - * `ConsumeQueue cq`:获取出该延迟队列任务处理的延迟队列 ConsumeQueue + * `ConsumeQueue cq`:获取出该延迟队列任务处理的**延迟队列 ConsumeQueue** * `SelectMappedBufferResult bufferCQ`:根据消费进度查询出 SMBR 对象 @@ -8775,15 +8742,11 @@ DeliverDelayedMessageTimerTask 是一个任务类 * `long tagsCode`:延迟消息的交付时间,在 ReputMessageService 转发时根据消息的 DELAY 属性是否 >0 ,会在 tagsCode 字段存储交付时间 - * `long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode)`:**延迟交付时间** - - * `long maxTimestamp`:当前时间 + 延迟级别对应的延迟毫秒值的时间戳 - * `if (deliverTimestamp > maxTimestamp)`:条件成立说明延迟时间过长,调整为当前时间立刻执行 - * `return result`:一般情况 result 就是 deliverTimestamp + * `long deliver... = this.correctDeliverTimestamp(..)`:**校准交付时间**,延迟时间过长会调整为当前时间立刻执行 * `long countdown = deliverTimestamp - now`:计算差值 - - * `if (countdown <= 0)`:消息已经到达交付时间了 + + * `if (countdown <= 0)`:**消息已经到达交付时间了** `MessageExt msgExt`:根据物理偏移量和消息大小获取这条消息 @@ -8791,11 +8754,11 @@ DeliverDelayedMessageTimerTask 是一个任务类 * `long tagsCodeValue`:不再是交付时间了 * `MessageAccessor.clearProperty(msgInner, DELAY..)`:清理新消息的 DELAY 属性,避免存储时重定向到延迟队列 - * `msgInner.setTopic()`:修改主题为原始的主题 `%RETRY%GroupName` + * `msgInner.setTopic()`:**修改主题为原始的主题 `%RETRY%GroupName`** * `String queueIdStr`:修改队列 ID 为原始的 ID - + `PutMessageResult putMessageResult`:**将新消息存储到 CommitLog**,消费者订阅的是目标主题,会再次消费该消息 - + * `else`:消息还未到达交付时间 `ScheduleMessageService.this.timer.schedule()`:创建该延迟级别的任务,延迟 countDown 毫秒之后再执行 @@ -8860,8 +8823,6 @@ TransactionMQProducer 类发送事务消息时使用 * `if (null == localTransactionExecuter && null == transactionListener)`:两者都为 null 抛出异常 - * `Validators.checkMessage(msg, this.defaultMQProducer)`:检查消息 - * `MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true")`:**设置事务标志** * `sendResult = this.send(msg)`:发送消息 @@ -8870,7 +8831,7 @@ TransactionMQProducer 类发送事务消息时使用 * `case SEND_OK`:消息发送成功 - `msg.setTransactionId(transactionId)`:设置事务 ID 为消息的 UNIQ_KEY 属性 + `msg.setTransactionId(transactionId)`:**设置事务 ID 为消息的 UNIQ_KEY 属性** `localTransactionState = ...executeLocalTransactionBranch(msg, arg)`:**执行本地事务** @@ -8882,7 +8843,7 @@ TransactionMQProducer 类发送事务消息时使用 * `EndTransactionRequestHeader requestHeader`:构建事务结束头对象 * `this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway()`:向 Broker 发起事务结束的单向请求 - + *** @@ -8891,7 +8852,7 @@ TransactionMQProducer 类发送事务消息时使用 ##### 回查处理 -ClientRemotingProcessor 用于处理到客户端的请求,创建 MQClientAPIImpl 时将该处理器注册到 Netty 中,`processRequest()` 方法根据请求的命令码,进行不同的处理,事务回查的处理命令码为 `CHECK_TRANSACTION_STATE` +ClientRemotingProcessor 用于处理到服务端的请求,创建 MQClientAPIImpl 时将该处理器注册到 Netty 中,`processRequest()` 方法根据请求的命令码,进行不同的处理,事务回查的处理命令码为 `CHECK_TRANSACTION_STATE` 成员方法: @@ -8971,32 +8932,28 @@ SendMessageProcessor 是服务端处理客户端发送来的消息的处理器 * `RemotingCommand response`:创建响应对象 - * `SendMessageResponseHeader responseHeader`:获取响应头,此时为 null - - * `byte[] body = request.getBody()`:获取请求体 - * `MessageExtBrokerInner msgInner = new MessageExtBrokerInner()`:创建 msgInner 对象,并赋值相关的属性,主题和队列 ID 都是请求头中的 - * `String transFlag`:获取**事务属性** + * `String transFlag`:**获取事务属性** * `if (transFlag != null && Boolean.parseBoolean(transFlag))`:判断事务属性是否是 true,走事务消息的存储流程 - * `putMessageResult = ...asyncPrepareMessage(msgInner)`:事务消息处理流程 + * `putMessageResult = ...asyncPrepareMessage(msgInner)`:**事务消息处理流程** ```java public CompletableFuture asyncPutHalfMessage(MessageExtBrokerInner messageInner) { - // 调用存储模块,将修改后的 msg 存储进 Broker + // 调用存储模块,将修改后的 msg 存储进 Broker(CommitLog) return store.asyncPutMessage(parseHalfMessageInner(messageInner)); } ``` - + TransactionalMessageBridge#parseHalfMessageInner: - - * `MessageAccessor.putProperty(...)`:将消息的原主题和队列 ID 放入消息的属性中 + + * `MessageAccessor.putProperty(...)`:**将消息的原主题和队列 ID 放入消息的属性中** * `msgInner.setSysFlag(...)`:消息设置为非事务状态 * `msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic())`:**消息主题设置为半消息主题** * `msgInner.setQueueId(0)`:**队列 ID 设置为 0** - + * `else`:普通消息存储 @@ -9016,19 +8973,33 @@ EndTransactionProcessor 类用来处理客户端发来的提交或者回滚请 ``` * `EndTransactionRequestHeader requestHeader`:从请求中解析出 EndTransactionRequestHeader - * `result = this.brokerController...commitMessage(requestHeader)`:根据 commitLogOffset 提取出 halfMsg 消息 - * `MessageExtBrokerInner msgInner`:根据 result 克隆出一条新消息 - * `msgInner.setTopic(msgExt.getUserProperty(...))`:**设置回原主题** - * `msgInner.setQueueId(Integer.parseInt(msgExt.getUserProperty(..)))`:**设置回原队列 ID** - * `MessageAccessor.clearProperty()`:清理上面的两个属性 - * `MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED)`:清理事务属性 - * `RemotingCommand sendResult = sendFinalMessage(msgInner)`:调用存储模块存储至 Broker - * `this.brokerController...deletePrepareMessage(result.getPrepareMessage())`:向删除(OP)队列添加消息,消息体的数据是 halfMsg 的 queueOffset,表示半消息队列指定的 offset 的消息已被删除 - * `if (this...putOpMessage(msgExt, TransactionalMessageUtil.REMOVETAG))`:**添加一条 OP 数据** + + * `if (MessageSysFlag.TRANSACTION_COMMIT_TYPE)`:**事务提交** + + `result = this.brokerController...commitMessage(requestHeader)`:根据 commitLogOffset 提取出 halfMsg 消息 + + * `MessageExtBrokerInner msgInner`:根据 result 克隆出一条新消息 + + `msgInner.setTopic(msgExt.getUserProperty(...))`:**设置回原主题** + + * `msgInner.setQueueId(Integer.parseInt(msgExt.getUserProperty(..)))`:**设置回原队列 ID** + * `MessageAccessor.clearProperty()`:清理上面的两个属性 + + `MessageAccessor.clearProperty(msgInner, ...)`:**清理事务属性** + + `RemotingCommand sendResult = sendFinalMessage(msgInner)`:调用存储模块存储至 Broker + + `this.brokerController...deletePrepareMessage(result.getPrepareMessage())`:**向删除(OP)队列添加消息**,消息体的数据是 halfMsg 的 queueOffset,**表示半消息队列指定的 offset 的消息已被删除** + + * `if (this...putOpMessage(msgExt, TransactionalMessageUtil.REMOVETAG))`:添加一条 OP 数据 * `MessageQueue messageQueue`:新建一个消息队列,OP 队列 * `return addRemoveTagInTransactionOp(messageExt, messageQueue)`:添加数据 - * `Message message`:创建消息 - * `writeOp(message, messageQueue)`:写入消息 + * `Message message`:创建 OP 消息 + * `writeOp(message, messageQueue)`:写入 OP 消息 + + * `else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE)`:**事务回滚** + + `this.brokerController...deletePrepareMessage(result.getPrepareMessage())`:**也需要向 OP 队列添加消息** @@ -9036,7 +9007,7 @@ EndTransactionProcessor 类用来处理客户端发来的提交或者回滚请 -### 消费者 +### Consumer #### 消费者类 @@ -9115,7 +9086,7 @@ DefaultMQPushConsumer 类是默认的消费者类 public void registerMessageListener(MessageListener messageListener) ``` -* subscribe():添加订阅信息 +* subscribe():添加订阅信息,**将订阅信息放入负载均衡对象的 subscriptionInner 中** ```java public void subscribe(String topic, String subExpression) @@ -9163,7 +9134,7 @@ DefaultMQPushConsumerImpl 是默认消费者的实现类 private final DefaultMQPushConsumer defaultMQPushConsumer; ``` -* **负载均衡**:分配订阅主题的队列给当前消费者,20秒钟一个周期执行 Rebalance 算法(客户端实例触发) +* **负载均衡**:分配订阅主题的队列给当前消费者,20 秒钟一个周期执行 Rebalance 算法(客户端实例触发) ```java private final RebalanceImpl rebalanceImpl = new RebalancePushImpl(this); @@ -9219,13 +9190,13 @@ DefaultMQPushConsumerImpl 是默认消费者的实现类 * `this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData)`:将订阅信息加入 rbl 的 map 中 * `this.messageListenerInner = ...getMessageListener()`:将消息监听器保存到实例对象 * `switch (this.defaultMQPushConsumer.getMessageModel())`:判断消费模式,广播模式下直接返回 - * `final String retryTopic`:当前**消费者组重试的主题名**,规则 `%RETRY%ConsumerGroup` + * `final String retryTopic`:创建当前**消费者组重试的主题名**,规则 `%RETRY%ConsumerGroup` * `SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData()`:创建重试主题的订阅数据对象 - * `this.rebalanceImpl.getSubscriptionInner().put(retryTopic, subscriptionData)`:将创建的重试主题加入到 rbl 对象的 map 中,消息重试时会再次加入到该主题,消费者订阅这个主题之后,就有机会再次拿到该消息进行消费处理 + * `this.rebalanceImpl.getSubscriptionInner().put(retryTopic, subscriptionData)`:将创建的重试主题加入到 rbl 对象的 map 中,**消息重试时会加入到该主题,消费者订阅这个主题之后,就有机会再次拿到该消息进行消费处理** * `this.mQClientFactory = ...getOrCreateMQClientInstance()`:获取客户端实例对象 * `this.rebalanceImpl.`:初始化负载均衡对象,设置**队列分配策略对象**到属性中 * `this.pullAPIWrapper = new PullAPIWrapper()`:创建拉消息 API 对象,内部封装了查询推荐主机算法 - * `this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList)`:将 过滤 Hook 列表注册到该对象内,消息拉取下来之后会执行该 Hook,再**进行一次自定义的过滤** + * `this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList)`:将过滤 Hook 列表注册到该对象内,消息拉取下来之后会执行该 Hook,**再进行一次自定义的消息过滤** * `this.offsetStore = new RemoteBrokerOffsetStore()`:默认集群模式下创建消息进度存储器 * `this.consumeMessageService = ...`:根据消息监听器的类型创建消费服务 * `this.consumeMessageService.start()`:启动消费服务 @@ -9238,7 +9209,7 @@ DefaultMQPushConsumerImpl 是默认消费者的实现类 * `mQClientFactory.start()`:启动客户端实例 * ` this.updateTopic`:从 nameserver 获取主题路由数据,生成主题集合放入 rbl 对象的 table * `this.mQClientFactory.checkClientInBroker()`:检查服务器是否支持消息过滤模式,一般使用 tag 过滤,服务器默认支持 - * `this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()`:向所有已知的 Broker 节点,发送心跳数据 + * `this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()`:向所有已知的 Broker 节点,**发送心跳数据** * `this.mQClientFactory.rebalanceImmediately()`:唤醒 rbl 线程,触发负载均衡执行 @@ -9251,7 +9222,7 @@ DefaultMQPushConsumerImpl 是默认消费者的实现类 ##### 实现方式 -MQClientInstance#start 中会启动负载均衡服务: +MQClientInstance#start 中会启动负载均衡服务 RebalanceService: ```java public void run() { @@ -9267,7 +9238,7 @@ public void run() { RebalanceImpl 类成员变量: -* 分配给当前消费者的处理队列:处理消息队列集合,ProcessQueue 是 MQ 队列在消费者端的快照 +* 分配给当前消费者的处理队列:处理消息队列集合,**ProcessQueue 是 MQ 队列在消费者端的快照** ```java protected final ConcurrentMap processQueueTable; @@ -9282,7 +9253,7 @@ RebalanceImpl 类成员变量: * 订阅数据: ```java - protected final ConcurrentMap subscriptionInner; + protected final ConcurrentMap subscriptionInner; ``` * 队列分配策略: @@ -9293,7 +9264,7 @@ RebalanceImpl 类成员变量: 成员方法: -* doRebalance():负载均衡方法 +* doRebalance():负载均衡方法,以每个消费者实例为粒度进行负载均衡 ```java public void doRebalance(final boolean isOrder) { @@ -9313,23 +9284,23 @@ RebalanceImpl 类成员变量: } ``` - * `Set mqSet = this.topicSubscribeInfoTable.get(topic)`:获取当前主题的全部队列信息 - - * `cidAll = this...findConsumerIdList(topic, consumerGroup)`:从服务器获取消费者组下的全部消费者 ID + 集群模式下: - * `Collections.sort(mqAll)`:主题 MQ 队列和消费者 ID 都进行排序,保证每个消费者的视图一致性 + * `Set mqSet = this.topicSubscribeInfoTable.get(topic)`:订阅的主题下的全部队列信息 - * `strategy = this.allocateMessageQueueStrategy`:获取队列分配策略对象 + * `cidAll = this...findConsumerIdList(topic, consumerGroup)`:从服务器获取消费者组下的全部消费者 ID - * `allocateResult = strategy.allocate()`: **调用队列分配策略**,给当前消费者进行分配 MessageQueue + * `Collections.sort(mqAll)`:主题 MQ 队列和消费者 ID 都进行排序,**保证每个消费者的视图一致性** - * `boolean changed = this.updateProcessQueueTableInRebalance(...)`:**更新队列处理集合** + * `allocateResult = strategy.allocate()`: **调用队列分配策略**,给当前消费者进行分配 MessageQueue(下一节) - * `boolean changed = false`:当前消费者的消费队列是否有变化 + * `boolean changed = this.updateProcessQueueTableInRebalance(...)`:**更新队列处理集合**,mqSet 是 rbl 算法分配到当前消费者的 MQ 集合 * `while (it.hasNext())`:遍历当前消费者的所有处理队列 - * `if (!mqSet.contains(mq))`:该 MQ 经过 rbl 计算之后,**被分配到其它 consumer 节点** + * `if (mq.getTopic().equals(topic))`:该 MQ 是 本次 rbl 分配算法计算的主题 + + * `if (!mqSet.contains(mq))`:该 MQ 经过 rbl 计算之后,**被分配到其它 Consumer 节点** `pq.setDropped(true)`:将删除状态设置为 true @@ -9343,7 +9314,7 @@ RebalanceImpl 类成员变量: `if (pq.getLockConsume().tryLock(1000, ..))`: 获取锁成功,说明顺序消费任务已经停止消费工作 - `return this.unlockDelay(mq, pq)`:**释放锁 Broker 端的队列锁** + `return this.unlockDelay(mq, pq)`:**释放锁 Broker 端的队列锁,向服务器发起 oneway 的解锁请求** * `if (pq.hasTempMessage())`:队列中有消息,延迟 20 秒释放队列分布式锁,确保全局范围内只有一个消费任务 运行中 * `else`:当前消费者本地该消费任务已经退出,直接释放锁 @@ -9352,11 +9323,11 @@ RebalanceImpl 类成员变量: `it.remove()`:从 processQueueTable 移除该 MQ - * `else if (pq.isPullExpired())`:说明当前 MQ 还是被当前 consumer 消费,此时判断一下是否超过 2 分钟未到服务器 拉消息,如果条件成立进行上述相同的逻辑 + * `else if (pq.isPullExpired())`:说明当前 MQ 还是被当前 Consumer 消费,此时判断一下是否超过 2 分钟未到服务器 拉消息,如果条件成立进行上述相同的逻辑 - * `for (MessageQueue mq : mqSet)`:开始处理当前主题**新分配**到当前节点的队列 + * `for (MessageQueue mq : mqSet)`:开始处理当前主题**新分配到当前节点的队列** - `if (isOrder && !this.lock(mq))`:**顺序消息为了保证有序性,需要获取分布式锁** + `if (isOrder && !this.lock(mq))`:**顺序消息为了保证有序性,需要获取队列锁** `ProcessQueue pq = new ProcessQueue()`:为每个新分配的消息队列创建快照队列 @@ -9364,9 +9335,9 @@ RebalanceImpl 类成员变量: `ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq)`:保存到处理队列集合 - `PullRequest pullRequest = new PullRequest()`:创建拉取请求对象 + `PullRequest pullRequest = new PullRequest()`:**创建拉取请求对象** - * `this.dispatchPullRequest(pullRequestList)`:放入拉消息服务的本地阻塞队列内,**用于拉取消息工作** + * `this.dispatchPullRequest(pullRequestList)`:放入 PullMessageService 的**本地阻塞队列**内,用于拉取消息工作 * lockAll():续约锁,对消费者的所有队列进行续约 @@ -9374,7 +9345,7 @@ RebalanceImpl 类成员变量: public void lockAll() ``` - * `HashMap> brokerMqs`:将分配给当前消费者的全部 MQ,按照 BrokerName 分组 + * `HashMap> brokerMqs`:将分配给当前消费者的全部 MQ 按照 BrokerName 分组 * `while (it.hasNext())`:遍历所有的分组 @@ -9384,11 +9355,11 @@ RebalanceImpl 类成员变量: * `LockBatchRequestBody requestBody`:创建请求对象,填充属性 - * `Set lockOKMQSet`:**向 Broker 发起批量续约锁的同步请求**,返回成功的队列集合 + * `Set lockOKMQSet`:**以组为单位向 Broker 发起批量续约锁的同步请求**,返回成功的队列集合 * `for (MessageQueue mq : lockOKMQSet)`:遍历续约锁成功的 MQ - `processQueue.setLocked(true)`:分布式锁状态设置为 true,**表示允许顺序消费** + `processQueue.setLocked(true)`:**分布式锁状态设置为 true,表示允许顺序消费** `processQueue.setLastLockTimestamp(System.currentTimeMillis())`:设置上次获取锁的时间为当前时间 @@ -9396,7 +9367,7 @@ RebalanceImpl 类成员变量: `if (!lockOKMQSet.contains(mq))`:条件成立说明续约锁失败 - `processQueue.setLocked(false)`:分布式锁状态设置为 false,表示不允许顺序消费 + `processQueue.setLocked(false)`:**分布式锁状态设置为 false,表示不允许顺序消费** @@ -9461,7 +9432,7 @@ MQClientInstance#start 中会启动消息拉取服务:PullMessageService ```java public void run() { - // 检查停止标记 + // 检查停止标记,【循环拉取】 while (!this.isStopped()) { try { // 从阻塞队列中获取拉消息请求 @@ -9491,9 +9462,9 @@ DefaultMQPushConsumerImpl#pullMessage: * `SubscriptionData subscriptionData`:本次拉消息请求订阅的主题数据,如果调用了 `unsubscribe(主题)` 将会获取为 null -* `PullCallback pullCallback = new PullCallback()`:**拉消息处理回调对象**, +* `PullCallback pullCallback = new PullCallback()`:**拉消息处理回调对象** - * `pullResult = ...processPullResult()`:预处理 PullResult 结果 + * `pullResult = ...processPullResult()`:预处理 PullResult 结果,将服务器端指定 MQ 的拉消息**下一次的推荐节点**保存到 pullFromWhichNodeTable 中,**并进行消息过滤** * `case FOUND`:正常拉取到消息 @@ -9503,21 +9474,21 @@ DefaultMQPushConsumerImpl#pullMessage: `boolean .. = processQueue.putMessage()`:将服务器拉取的消息集合**加入到消费者本地**的 processQueue 内 - `DefaultMQPushConsumerImpl...submitConsumeRequest()`:**提交消费任务**,分为顺序消费和并发消费 + `DefaultMQPushConsumerImpl...submitConsumeRequest()`:**提交消费任务,分为顺序消费和并发消费** `Defaul..executePullRequestImmediately(pullRequest)`:将更新过 nextOffset 字段的 PullRequest 对象,再次放到 pullMessageService 的阻塞队列中,**形成闭环** - * `case NO_NEW_MSG ||NO_MATCHED_MSG`:表示本次 pull 没有新的可消费的信息 + * `case NO_NEW_MSG ||NO_MATCHED_MSG`:**表示本次 pull 没有新的可消费的信息** `pullRequest.setNextOffset()`:更新更新 pullRequest 对象下一次拉取消息的位点 `Defaul..executePullRequestImmediately(pullRequest)`:再次拉取请求 - * `case OFFSET_ILLEGAL`:本次 pull 时使用的 offset 是无效的,即 offset > maxOffset || offset < minOffset + * `case OFFSET_ILLEGAL`:**本次 pull 时使用的 offset 是无效的**,即 offset > maxOffset || offset < minOffset - `pullRequest.setNextOffset()`:调整pullRequest nextOffset 为 正确的 offset + `pullRequest.setNextOffset()`:调整 pullRequest.nextOffset 为正确的 offset - `pullRequest.getProcessQueue().setDropped(true)`:设置该 processQueue 为删除状态,如果有该 queue 的消费任务,该消费任务会马上停止任务 + `pullRequest.getProcessQueue().setDropped(true)`:设置该 processQueue 为删除状态,如果有该 queue 的消费任务,消费任务会马上停止 `DefaultMQPushConsumerImpl.this.executeTaskLater()`:提交异步任务,10 秒后去执行 @@ -9531,7 +9502,7 @@ DefaultMQPushConsumerImpl#pullMessage: 负载均衡 rbl 程序会重建该队列的 processQueue,重建完之后会为该队列创建新的 PullRequest 对象 -* `int sysFlag = PullSysFlag.buildSysFlag()`:构建标志对象,sysFlag 高 4 位未使用,低 4 位使用,从左到右为 0000 0011 +* `int sysFlag = PullSysFlag.buildSysFlag()`:**构建标志对象**,sysFlag 高 4 位未使用,低 4 位使用,从左到右 0000 0011 * 第一位:表示是否提交消费者本地该队列的 offset,一般是 1 * 第二位:表示是否允许服务器端进行长轮询,一般是 1 @@ -9576,23 +9547,13 @@ PullAPIWrapper 类封装了拉取消息的 API * `RemotingCommand request`:创建网络层传输对象 RemotingCommand 对象,**请求 ID 为 `PULL_MESSAGE = 11`** - * `return this.pullMessageSync(...)`:此处是异步调用,**处理结果放入 ResponseFuture 中**,参考服务端小节的处理器类 `NettyServerHandler#processMessageReceived` 方法 - - * `RemotingCommand response = responseFuture.getResponseCommand()`:获取服务器端响应数据 response + * `return this.pullMessageSync(...)`:此处是**异步调用,处理结果放入 ResponseFuture 中**,参考服务端小节的处理器类 `NettyServerHandler#processMessageReceived` 方法 +* `RemotingCommand response = responseFuture.getResponseCommand()`:获取服务器端响应数据 response * `PullResult pullResult`:从 response 内提取出来拉消息结果对象,将响应头 PullMessageResponseHeader 对象中信息**填充到 PullResult 中**,列出两个重要的字段: * `private Long suggestWhichBrokerId`:服务端建议客户端下次 Pull 时选择的 BrokerID * `private Long nextBeginOffset`:客户端下次 Pull 时使用的 offset 信息 - - * `pullCallback.onSuccess(pullResult)`:将 PullResult 交给拉消息结果处理回调对象,调用 onSuccess 方法 - -* processPullResult():预处理拉消息结果,**更新推荐 Broker 和过滤消息** - - * `this.updatePullFromWhichNode()`:更新 pullFromWhichNodeTable 内该 MQ 的下次查询推荐 BrokerID - * `if (PullStatus.FOUND == pullResult.getPullStatus())`:条件成立说明拉取成功 - * `List msgList`:**将获取的消息进行解码** - * `if (!subscriptionData... && !subscriptionData.isClassFilterMode())`:客户端按照 tag 值进行过滤 - * `pullResultExt.setMsgFoundList(msgListFilterAgain)`:将再次过滤后的消息集合,保存到 pullResult - * `pullResultExt.setMessageBinary(null)`:设置为 null,帮助 GC + +* `pullCallback.onSuccess(pullResult)`:将 PullResult 交给拉消息结果处理回调对象,调用 onSuccess 方法 @@ -9617,11 +9578,11 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re * `final PullMessageRequestHeader requestHeader`:解析出请求头 PullMessageRequestHeader -* `response.setOpaque(request.getOpaque())`:设置 opaque 属性,客户端需要根据该字段**获取 ResponseFuture** +* `response.setOpaque(request.getOpaque())`:设置 opaque 属性,客户端**根据该字段获取 ResponseFuture** 进行处理 * 进行一些鉴权的逻辑:是否允许长轮询、提交 offset、topicConfig 是否是空、队列 ID 是否合理 -* `ConsumerGroupInfo consumerGroupInfo`:获取消费者组信息,包好全部的消费者和订阅数据 +* `ConsumerGroupInfo consumerGroupInfo`:获取消费者组信息,包含全部的消费者和订阅数据 * `subscriptionData = consumerGroupInfo.findSubscriptionData()`:**获取指定主题的订阅数据** @@ -9629,13 +9590,13 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re * `MessageFilter messageFilter`:创建消息过滤器,一般是通过 tagCode 进行过滤 -* `DefaultMessageStore.getMessage()`:**查询消息的核心逻辑**,在 Broker 端查询消息(存储端笔记详解了该源码) +* `DefaultMessageStore.getMessage()`:**查询消息的核心逻辑,在 Broker 端查询消息**(存储端笔记详解了该源码) * `response.setRemark()`:设置此次响应的状态 * `responseHeader.set..`:设置响应头对象的一些字段 -* `switch (this.brokerController.getMessageStoreConfig().getBrokerRole())`:如果当前主机节点角色为 slave 并且**从节点读**并未开启的话,直接给客户端 一个状态 `PULL_RETRY_IMMEDIATELY` +* `switch (this.brokerController.getMessageStoreConfig().getBrokerRole())`:如果当前主机节点角色为 slave 并且**从节点读**并未开启的话,直接给客户端 一个状态 `PULL_RETRY_IMMEDIATELY`,并设置为下次从主节点读 * `if (this.brokerController.getBrokerConfig().isSlaveReadEnable())`:消费太慢,下次从另一台机器拉取 @@ -9669,12 +9630,12 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re * `this.brokerController...suspendPullRequest(topic, queueId, pullRequest)`:将长轮询请求对象交给长轮询服务 * `String key = this.buildKey(topic, queueId)`:构建一个 `topic@queueId` 的 key * `ManyPullRequest mpr = this.pullRequestTable.get(key)`:从拉请求表中获取对象 - * `mpr.addPullRequest(pullRequest)`:将 PullRequest 对象放入到 ManyPullRequest 的请求集合中 + * `mpr.addPullRequest(pullRequest)`:**将 PullRequest 对象放入到长轮询的请求集合中** * `response = null`:响应设置为 null 内部的 callBack 就不会给客户端发送任何数据,**不进行通信**,否则就又开始重新请求 * `boolean storeOffsetEnable`:允许长轮询、sysFlag 表示提交消费者本地该队列的offset、当前 broker 节点角色为 master 节点三个条件成立,才**在 Broker 端存储消费者组内该主题的指定 queue 的消费进度** -* `return response`:返回 response,不为 null 时外层 requestTask 的 callback 会将数据写给客户端 +* `return response`:返回 response,不为 null 时外层 processRequestCommand 的 callback 会将数据写给客户端 @@ -9715,13 +9676,13 @@ PullRequestHoldService 类负责长轮询,BrokerController#start 方法中调 * `long offset = this...getMaxOffsetInQueue(topic, queueId)`: 到存储模块查询该 ConsumeQueue 的**最大 offset** * `this.notifyMessageArriving(topic, queueId, offset)`:通知消息到达 -* notifyMessageArriving():通知消息到达的逻辑,ReputMessageService 消息分发服务也会调用该方法 +* notifyMessageArriving():**通知消息到达**的逻辑,ReputMessageService 消息分发服务也会调用该方法 - * `ManyPullRequest mpr = this.pullRequestTable.get(key)`:获取对应的的manyPullRequest对象 + * `ManyPullRequest mpr = this.pullRequestTable.get(key)`:获取对应的的 manyPullRequest 对象 * `List requestList`:获取该队列下的所有 PullRequest,并进行遍历 * `List replayList`:当某个 pullRequest 不超时,并且对应的 `CQ.maxOffset <= pullRequest.offset`,就将该 PullRequest 再放入该列表 * `long newestOffset`:该值为 CQ 的 maxOffset - * `if (newestOffset > request.getPullFromThisOffset())`:说明该请求对应的队列内可以 pull 消息了,**结束长轮询** + * `if (newestOffset > request.getPullFromThisOffset())`:**请求对应的队列内可以 pull 消息了,结束长轮询** * `boolean match`:进行过滤匹配 * `this.brokerController...executeRequestWhenWakeup()`:将满足条件的 pullRequest 再次提交到线程池内执行 * `final RemotingCommand response`:执行 processRequest 方法,并且**不会触发长轮询** @@ -9930,7 +9891,7 @@ ConsumeMessageConcurrentlyService 负责并发消费服务 * 线程池: ```java - private final ThreadPoolExecutor consumeExecutor; // 消费任务线程池 + private final ThreadPoolExecutor consumeExecutor; // 消费任务线程池,默认 20 private final ScheduledExecutorService scheduledExecutorService;// 调度线程池,延迟提交消费任务 private final ScheduledExecutorService cleanExpireMsgExecutors; // 清理过期消息任务线程池,15min 一次 ``` @@ -9975,7 +9936,7 @@ ConsumeMessageConcurrentlyService 并发消费核心方法 public void submitConsumeRequest(List msgs, ProcessQueue processQueue, MessageQueue messageQueue, boolean dispatchToConsume) ``` - * `final int consumeBatchSize`:**一个消费任务**可消费的消息数量,默认为 1 + * `final int consumeBatchSize`:**一个消费任务可消费的消息数量**,默认为 1 * `if (msgs.size() <= consumeBatchSize)`:判断一个消费任务是否可以提交 @@ -10008,28 +9969,23 @@ ConsumeMessageConcurrentlyService 并发消费核心方法 * `switch (this.defaultMQPushConsumer.getMessageModel())`:判断消费模式,默认是**集群模式** - * `for (int i = ackIndex + 1; i < msgs.size(); i++)`:当消费失败时 ackIndex 为 -1,i 的起始值为 0,该消费任务内的全部消息都会尝试回退给服务器 + * `for (int i = ackIndex + 1; i < msgs.size(); i++)`:当消费失败时 ackIndex 为 -1,i 的起始值为 0,该消费任务内的**全部消息**都会尝试回退给服务器 * `MessageExt msg`:提取一条消息 - * `boolean result = this.sendMessageBack(msg, context)`:发送**消息回退** - - * `String brokerAddr`:根据 brokerName 获取 master 节点地址 - * `his.mQClientFactory...consumerSendMessageBack()`:发送回退消息 - * `RemotingCommand request`:创建请求对象 - * `RemotingCommand response = this.remotingClient.invokeSync()`:**同步请求** + * `boolean result = this.sendMessageBack(msg, context)`:**发送消息回退,同步发送** * `if (!result)`:回退失败的消息,将**消息的重试属性加 1**,并加入到回退失败的集合 - + * `if (!msgBackFailed.isEmpty())`:回退失败集合不为空 - - `consumeRequest.getMsgs().removeAll(msgBackFailed)`:将回退失败的消息从当前消费任务的 msgs 集合内移除 - - `this.submitConsumeRequestLater()`:回退失败的消息会再次提交消费任务,延迟 5 秒钟后**再次尝试消费** - - * `long offset = ...removeMessage(msgs)`:从 pq 中删除已经消费成功的消息,返回 offset - - * `this...getOffsetStore().updateOffset()`:更新消费者本地该 mq 的**消费进度** + + `consumeRequest.getMsgs().removeAll(msgBackFailed)`:将回退失败的消息从当前消费任务的 msgs 集合内移除 + + `this.submitConsumeRequestLater()`:**回退失败的消息会再次提交消费任务**,延迟 5 秒钟后再次尝试消费 + +* `long offset = ...removeMessage(msgs)`:从 pq 中删除已经消费成功的消息,返回 offset + +* `this...getOffsetStore().updateOffset()`:更新消费者本地该 mq 的**消费进度** @@ -10115,7 +10071,7 @@ ConsumeMessageOrderlyService 负责顺序消费服务 private final ScheduledExecutorService scheduledExecutorService;// 调度线程池,延迟提交消费任务 ``` -* 队列锁:消费者本地 MQ 锁,确保本地对于需要顺序消费的 MQ 同一时间只有一个任务在执行 +* 队列锁:消费者本地 MQ 锁,**确保本地对于需要顺序消费的 MQ 同一时间只有一个任务在执行** ```java private final MessageQueueLock messageQueueLock = new MessageQueueLock(); @@ -10139,9 +10095,9 @@ ConsumeMessageOrderlyService 负责顺序消费服务 } ``` - 已经获取了 Broker 端该 Queue 的独占锁,为什么还要获取本地队列锁对象?(这里我也没太懂,先记录下来) + 已经获取了 Broker 端该 Queue 的独占锁,为什么还要获取本地队列锁对象?(这里我也没太懂,先记录下来,本地多线程?) - * Broker queue 占用锁的角度是 Client 占用,Client 从 Broker 的某个占用了锁的 queue 拉取下来消息以后,将消息存储到消费者本地的 ProcessQueue 中,快照对象的 consuming 属性置为 true,表示本地的队列正在消费处理中。 + * Broker queue 占用锁的角度是 Client 占用,Client 从 Broker 的某个占用了锁的 queue 拉取下来消息以后,将消息存储到消费者本地的 ProcessQueue 中,快照对象的 consuming 属性置为 true,表示本地的队列正在消费处理中 * ProcessQueue 调用 takeMessages 方法时会获取下一批待处理的消息,获取不到会修改 `consuming = false`,本消费任务马上停止。 * 如果此时 Pull 再次拉取一批当前 ProcessQueue 的 msg,会再次向顺序消费服务提交消费任务,此时需要本地队列锁对象同步本地线程 @@ -10195,7 +10151,7 @@ ConsumeMessageOrderlyService 负责顺序消费服务 * `case SUSPEND_CURRENT_QUEUE_A_MOMENT`:挂起当前队列 - `consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs)`:回滚消息 + `consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs)`:**回滚消息** * `for (MessageExt msg : msgs)`:遍历所有的消息 * `this.consumingMsgOrderlyTreeMap.remove(msg.getQueueOffset())`:从顺序消费临时容器中移除 @@ -10263,6 +10219,27 @@ ConsumeRequest 是 ConsumeMessageOrderlyService 的内部类,是一个 Runnabl +*** + + + +### 生产消费 + +生产流程: + +* 首先获取当前消息主题的发布信息,获取不到去 Namesrv 获取(默认有 TBW102),并将获取的到的路由数据转化为发布数据,**创建 MQ 队列**,同样更新订阅数据,创建 MQ 队列,放入负载均衡服务 topicSubscribeInfoTable 中 +* 发送消息后,会创建主题对应的 MQ 放入 SendResult +* Broker 端通过 SendMessageProcessor 对发送的消息进行持久化处理,存储到 CommitLog。将重试次数过多的消息加入死信队列,将延迟级别消息的主题和队列修改为调度主题和调度队列 ID +* ScheduleMessageService 服务会为每个延迟级别创建一个延迟任务,让延迟消息得到有效的处理,将到达交付时间的消息修改为原始主题的原始 ID 存入 CommitLog,消费者就可以进行消费了 + +消费流程: + +* 首先通过负载均衡服务,将分配到当前消费者实例的 MQ 创建 PullRequest,并放入 PullMessageService 的本地阻塞队列内 +* PullMessageService 循环从阻塞队列获取请求对象,发起拉消息请求,并创建 PullCallback 回调对象,将拉取的消息**提交到消费任务线程池**,并设置下一次拉取的位点,重新放入阻塞队列,形成闭环 +* 消费任务对消费失败的消息进行回退,回退失败的消息会再次提交消费任务 +* Broker 端对拉取消息的请求进行处理(processRequestCommand),查询成功将消息放入响应体,通过 Netty 写回客户端,当 `pullRequest.offset == queue.maxOffset` 说明该队列已经没有需要获取的消息,此时进行长轮询等待有新的消息 +* PullRequestHoldService 负责长轮询,每 5 秒检查一次,将满足条件的 PullRequest 再次提交到线程池内执行 + diff --git a/Java.md b/Java.md index 428fde5..f325b35 100644 --- a/Java.md +++ b/Java.md @@ -8098,7 +8098,7 @@ public class UserServiceTest { 反射的缺点: -- 性能开销:反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化,反射操作的效率要比那些非射操作低得多,应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。 +- **性能开销**:反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化,反射操作的效率要比那些非射操作低得多,应该避免在经常被执行的代码或对性能要求很高的程序中使用反射 - 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行,如果一个程序必须在有安全限制的环境中运行 - 内部暴露:由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化 @@ -10342,7 +10342,7 @@ CGLIB 的优缺点 * 优点: * 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用 - * **代理对象可以增强目标对象的功能,内部持有原始的目标对象** + * **代理对象可以增强目标对象的功能,被用来间接访问底层对象,与原始对象具有相同的 hashCode** * 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度 * 缺点:增加了系统的复杂度 diff --git a/Prog.md b/Prog.md index f5c8db8..ca94bb1 100644 --- a/Prog.md +++ b/Prog.md @@ -5843,6 +5843,14 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 // 使用线程工厂创建一个线程,并且【将当前worker指定为Runnable】,所以thread启动时会调用 worker.run() this.thread = getThreadFactory().newThread(this); } + // 【不可重入锁】 + protected boolean tryAcquire(int unused) { + if (compareAndSetState(0, 1)) { + setExclusiveOwnerThread(Thread.currentThread()); + return true; + } + return false; + } } ``` diff --git a/SSM.md b/SSM.md index d85f232..cd3384e 100644 --- a/SSM.md +++ b/SSM.md @@ -8172,9 +8172,9 @@ AbstractAutowireCapableBeanFactory.**doCreateBean**(beanName, RootBeanDefinition `earlySingletonReference = getSingleton(beanName, false)`:**从二级缓存获取实例**,放入一级缓存是在 doGetBean 中的sharedInstance = getSingleton() 逻辑中,此时在 createBean 的逻辑还没有返回,所以一级缓存没有 - `if (earlySingletonReference != null)`:当前 bean 实例从二级缓存中获取到了,说明产生了循环依赖,在属性填充阶段会提前调用三级缓存中的工厂生成 Bean 对象的动态代理,放入二级缓存中,然后使用原始 bean 继续执行初始化 + `if (earlySingletonReference != null)`:当前 bean 实例从二级缓存中获取到了,说明**产生了循环依赖**,在属性填充阶段会提前调用三级缓存中的工厂生成 Bean 的代理对象(或原始实例),放入二级缓存中,然后使用原始 bean 继续执行初始化 - * ` if (exposedObject == bean)`:初始化后的 bean == 创建的原始实例,条件成立的两种情况:当前的真实实例不需要被代理;当前实例已经被代理过了,初始化时的后置处理器直接返回 bean 原实例 + * ` if (exposedObject == bean)`:**初始化后的 bean == 创建的原始实例**,条件成立的两种情况:当前的真实实例不需要被代理;当前实例存在循环依赖已经被提前代理过了,初始化时的后置处理器直接返回 bean 原实例 `exposedObject = earlySingletonReference`:**把代理后的 Bean 传给 exposedObject 用来返回,因为只有代理对象才封装了拦截器链,main 方法中用代理对象调用方法时会进行增强,代理是对原始对象的包装,所以这里返回的代理对象中含有完整的原实例(属性填充和初始化后的),是一个完整的代理对象,返回后外层方法会将当前 Bean 放入一级缓存** @@ -8431,7 +8431,8 @@ private final Map> singletonFactories = new HashMap<>(1 * 为什么需要三级缓存? * 循环依赖解决需要提前引用动态代理对象,AOP 动态代理是在 Bean 初始化后的后置处理中进行,这时的 bean 已经是成品对象。因为需要提前进行动态代理,三级缓存的 ObjectFactory 提前产生需要代理的对象,把提前引用放入二级缓存 - * 如果只有二级缓存,提前引用就直接放入了一级缓存,然后初始化 Bean 完成后会将 Bean 放入一级缓存,这时就发生冲突了 + * 如果只有二级缓存,提前引用就直接放入了一级缓存,然后 Bean 初始化完成后又会放入一级缓存,产生数据覆盖,**导致提前引用的对象和一级缓存中的并不是同一个对象** + * 一级缓存只能存放完整的单实例,**为了保证 Bean 的生命周期不被破坏**,不能将未初始化的 Bean 暴露到一级缓存 * 若存在循环依赖,**后置处理不创建代理对象,真正创建代理对象的过程是在 getBean(B) 的阶段中** * 三级缓存一定会创建提前引用吗? From 3cb0150daddf699fe2a91e1ead16a4f6bb0e8b89 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 9 Mar 2022 01:02:53 +0800 Subject: [PATCH 03/51] Update Java Note --- Frame.md | 16 ++++++++-------- Java.md | 10 +++++----- Web.md | 8 ++++++++ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/Frame.md b/Frame.md index c884974..1916253 100644 --- a/Frame.md +++ b/Frame.md @@ -9552,7 +9552,7 @@ PullAPIWrapper 类封装了拉取消息的 API * `PullResult pullResult`:从 response 内提取出来拉消息结果对象,将响应头 PullMessageResponseHeader 对象中信息**填充到 PullResult 中**,列出两个重要的字段: * `private Long suggestWhichBrokerId`:服务端建议客户端下次 Pull 时选择的 BrokerID * `private Long nextBeginOffset`:客户端下次 Pull 时使用的 offset 信息 - + * `pullCallback.onSuccess(pullResult)`:将 PullResult 交给拉消息结果处理回调对象,调用 onSuccess 方法 @@ -10227,18 +10227,18 @@ ConsumeRequest 是 ConsumeMessageOrderlyService 的内部类,是一个 Runnabl 生产流程: -* 首先获取当前消息主题的发布信息,获取不到去 Namesrv 获取(默认有 TBW102),并将获取的到的路由数据转化为发布数据,**创建 MQ 队列**,同样更新订阅数据,创建 MQ 队列,放入负载均衡服务 topicSubscribeInfoTable 中 -* 发送消息后,会创建主题对应的 MQ 放入 SendResult +* 首先获取当前消息主题的发布信息,获取不到去 Namesrv 获取(默认有 TBW102),并将获取的到的路由数据转化为发布数据,**创建 MQ 队列**,客户端实例同样更新订阅数据,创建 MQ 队列,放入负载均衡服务 topicSubscribeInfoTable 中 +* 然后从发布数据中选择一个 MQ 队列发送消息 * Broker 端通过 SendMessageProcessor 对发送的消息进行持久化处理,存储到 CommitLog。将重试次数过多的消息加入死信队列,将延迟级别消息的主题和队列修改为调度主题和调度队列 ID -* ScheduleMessageService 服务会为每个延迟级别创建一个延迟任务,让延迟消息得到有效的处理,将到达交付时间的消息修改为原始主题的原始 ID 存入 CommitLog,消费者就可以进行消费了 +* Broker 启动 ScheduleMessageService 服务会为每个延迟级别创建一个延迟任务,让延迟消息得到有效的处理,将到达交付时间的消息修改为原始主题的原始 ID 存入 CommitLog,消费者就可以进行消费了 消费流程: * 首先通过负载均衡服务,将分配到当前消费者实例的 MQ 创建 PullRequest,并放入 PullMessageService 的本地阻塞队列内 -* PullMessageService 循环从阻塞队列获取请求对象,发起拉消息请求,并创建 PullCallback 回调对象,将拉取的消息**提交到消费任务线程池**,并设置下一次拉取的位点,重新放入阻塞队列,形成闭环 -* 消费任务对消费失败的消息进行回退,回退失败的消息会再次提交消费任务 -* Broker 端对拉取消息的请求进行处理(processRequestCommand),查询成功将消息放入响应体,通过 Netty 写回客户端,当 `pullRequest.offset == queue.maxOffset` 说明该队列已经没有需要获取的消息,此时进行长轮询等待有新的消息 -* PullRequestHoldService 负责长轮询,每 5 秒检查一次,将满足条件的 PullRequest 再次提交到线程池内执行 +* PullMessageService 循环从阻塞队列获取请求对象,发起拉消息请求,并创建 PullCallback 回调对象,将正常拉取的消息**提交到消费任务线程池**,并设置下一次拉取的位点,重新放入阻塞队列,形成闭环 +* 消费任务服务对消费失败的消息进行回退,回退失败的消息会再次提交消费任务重新消费 +* Broker 端对拉取消息的请求进行处理(processRequestCommand),查询成功将消息放入响应体,通过 Netty 写回客户端,当 `pullRequest.offset == queue.maxOffset` 说明该队列已经没有需要获取的消息,将请求放入长轮询集合等待有新消息 +* PullRequestHoldService 负责长轮询,每 5 秒遍历一次长轮询集合,将满足条件的 PullRequest 再次提交到线程池内处理 diff --git a/Java.md b/Java.md index f325b35..7ce2e5f 100644 --- a/Java.md +++ b/Java.md @@ -11767,7 +11767,7 @@ CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿 - 初始标记:使用 STW 出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 - 并发标记:进行 GC Roots 开始遍历整个对象图,在整个回收过程中耗时最长,不需要 STW,可以与用户线程并发运行 - 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况) -- 并发清除:清除标记为可以回收对象,不需要移动存活对象,所以这个阶段可以与用户线程同时并发的 +- 并发清除:清除标记为可以回收对象,**不需要移动存活对象**,所以这个阶段可以与用户线程同时并发的 Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因: @@ -11922,9 +11922,9 @@ G1 中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 Full GC,在 * 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC * 根区域扫描 (Root Region Scanning):扫描 Survivor 区中指向老年代的,被初始标记标记了的引用及引用的对象,这一个过程是并发进行的,但是必须在 Young GC 之前完成 - * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。会计算每个区域的对象活性,即区域中存活对象的比例,若区域中的所有对象都是垃圾,则这个区域会被立即回收(实时回收),给浮动垃圾准备出更多的空间,把需要收集的 Region 放入 CSet 当中 + * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。会计算每个区域的对象活性,即区域中存活对象的比例,若区域中的所有对象都是垃圾,则这个区域会被立即回收(**实时回收**),给浮动垃圾准备出更多的空间,把需要收集的 Region 放入 CSet 当中 * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行(**防止漏标**) - * 筛选回收:并发清理阶段,首先对 CSet 中各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率 + * 筛选回收:并发清理阶段,首先对 CSet 中各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划,也需要 STW ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1收集器.jpg) @@ -12004,7 +12004,7 @@ ZGC 收集器是一个可伸缩的、低延迟的垃圾收集器,基于 Region * 可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据 * 内存多重映射:多个虚拟地址指向同一个物理地址 -可并发的标记压缩算法:染色指针标识对象是否被标记或移动,读屏障保证在每次应用程序或 GC 程序访问对象时先根据染色指针的标识判断是否被移动,如果被移动就根据转发表访问新的移动对象,并更新引用,不会像 G1 一样必须等待垃圾回收完成才能访问 +可并发的标记压缩算法:染色指针标识对象是否被标记或移动,读屏障保证在每次应用程序或 GC 程序访问对象时先根据染色指针的标识判断是否被移动,如果被移动就根据转发表访问新的移动对象,**并更新引用**,不会像 G1 一样必须等待垃圾回收完成才能访问 ZGC 目标: @@ -13309,7 +13309,7 @@ Java 语言:跨平台的语言(write once ,run anywhere) 编译过程中的编译器: -* 前端编译器: Sun 的全量式编译器 javac、 Eclipse 的增量式编译器 ECJ,把源代码编译为字节码文件 .class +* 前端编译器: Sun 的全量式编译器 javac、 Eclipse 的增量式编译器 ECJ,**把源代码编译为字节码文件 .class** * IntelliJ IDEA 使用 javac 编译器 * Eclipse 中,当开发人员编写完代码后保存时,ECJ 编译器就会把未编译部分的源码逐行进行编译,而非每次都全量编译,因此 ECJ 的编译效率会比 javac 更加迅速和高效 diff --git a/Web.md b/Web.md index 3cc854a..0cf91a7 100644 --- a/Web.md +++ b/Web.md @@ -2272,6 +2272,14 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 * 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存 * 响应报文的状态码是可缓存的,包括:200、203、204、206、300、301、404、405、410、414 and 501 * 响应报文的 Cache-Control 首部字段没有指定不进行缓存 + + * PUT 和 POST 的区别 + + PUT 请求:如果两个请求相同,后一个请求会把第一个请求覆盖掉(幂等),所以 PUT 用来修改资源 + + POST 请求:后一个请求不会把第一个请求覆盖掉(非幂等),所以 POST 用来创建资源 + + PATCH方法 是新引入的,是对 PUT 方法的补充,用来对已知资源进行**局部更新** From 1fa14f5e9fe4226d7b57c989ac2383abfb968c78 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 10 Mar 2022 00:04:39 +0800 Subject: [PATCH 04/51] Update Java Note --- DB.md | 10 +++++----- SSM.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DB.md b/DB.md index 67e2bdd..ef6d250 100644 --- a/DB.md +++ b/DB.md @@ -5698,7 +5698,7 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 * 每当需要为某个事务分配 ID,就会把全局变量的值赋值给事务 ID,然后变量自增 1 * 每当变量值为 256 的倍数时,就将该变量的值刷新到系统表空间的 Max Trx ID 属性中,该属性占 8 字节 -* 系统再次启动后,会读取表空间的 Max Trx ID 属性到内存,加上 256 后赋值给全局变量,因为关机时的事务 ID 可能并不是 256 的倍数,会比 Max Trx ID 大,所以需要加上 256 保持事务 ID 是一个递增的数字 +* 系统再次启动后,会读取表空间的 Max Trx ID 属性到内存,加上 256 后赋值给全局变量,因为关机时的事务 ID 可能并不是 256 的倍数,会比 Max Trx ID 大,所以需要加上 256 保持事务 ID 是一个**递增的数字** **聚簇索引**的行记录除了完整的数据,还会自动添加 trx_id、roll_pointer 隐藏列,如果表中没有主键并且没有非空唯一索引,也会添加一个 row_id 的隐藏列作为聚簇索引 @@ -11635,7 +11635,7 @@ Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 * 服务器运行ID(runid):服务器运行 ID 是每一台服务器每次运行的身份识别码,一台服务器多次运行可以生成多个运行 ID,由 40 位字符组成,是一个随机的十六进制字符 - 作用:用于在服务器间进行传输识别身份,如果想两次操作均对同一台服务器进行,必须每次操作携带对应的运行 ID,用于对方识别 + 作用:服务器间进行传输识别身份,如果想两次操作均对同一台服务器进行,**每次必须操作携带对应的运行 ID**,用于对方识别 实现:运行 ID 在每台服务器启动时自动生成,master 在首次连接 slave 时,将运行 ID 发送给 slave,slave 保存此 ID,通过 info Server 命令,可以查看节点的 runid @@ -11736,13 +11736,13 @@ slave 心跳任务 * master 内部创建 master_replid 变量,使用 runid 相同的策略生成,长度41位,并发送给所有 slave -* 在master关闭时执行命令 `shutdown save`,进行RDB持久化,将 runid 与 offset 保存到RDB文件中 +* 在master关闭时执行命令 `shutdown save`,进行 RDB 持久化,将 runid 与 offset 保存到 RDB 文件中 `redis-check-rdb dump.rdb` 命令可以查看该信息,保存为 repl-id 和 repl-offset * master 重启后加载 RDB 文件,恢复数据 - 重启后,将RDB文件中保存的 repl-id 与 repl-offset 加载到内存中 + 重启后,将 RDB 文件中保存的 repl-id 与 repl-offset 加载到内存中 * master_repl_id = repl-id,master_repl_offset = repl-offset * 通过 info 命令可以查看该信息 @@ -11758,7 +11758,7 @@ slave 心跳任务 master 的 CPU 占用过高或 slave 频繁断开连接 * 出现的原因: - * slave 每1秒发送 REPLCONF ACK 命令到 master + * slave 每 1 秒发送 REPLCONF ACK 命令到 master * 当 slave 接到了慢查询时(keys * ,hgetall等),会大量占用 CPU 性能 * master 每1秒调用复制定时函数 replicationCron(),比对 slave 发现长时间没有进行响应 diff --git a/SSM.md b/SSM.md index cd3384e..b2b7e4c 100644 --- a/SSM.md +++ b/SSM.md @@ -1677,7 +1677,7 @@ public void testFirstLevelCache(){ } ``` -* commit():事务提交,清空一级缓存,二级缓存使用 TransactionalCacheManager(tcm)管理 +* commit():事务提交,**清空一级缓存,放入二级缓存**,二级缓存使用 TransactionalCacheManager(tcm)管理 ```java public void commit(boolean required) throws SQLException { From f4267a6e675aab426e0bd2a1c3f82a5c60b5df6d Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 15 Mar 2022 09:48:22 +0800 Subject: [PATCH 05/51] Update Java Note --- DB.md | 87 +++++++++++++++++++++++++++++++++++++------------------- Frame.md | 6 ++-- Java.md | 3 ++ Prog.md | 15 ++++++++++ 4 files changed, 79 insertions(+), 32 deletions(-) diff --git a/DB.md b/DB.md index ef6d250..24fb3e3 100644 --- a/DB.md +++ b/DB.md @@ -522,7 +522,7 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 查询缓存失效的情况: -* SQL 语句不一致的情况,要想命中查询缓存,查询的 SQL 语句必须一致 +* SQL 语句不一致,要想命中查询缓存,查询的 SQL 语句必须一致,因为**缓存中 key 是查询的语句**,value 是查询结构 ```mysql select count(*) from tb_item; @@ -567,9 +567,13 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 SELECT * FROM t WHERE id = 1; ``` +解析器:处理语法和解析查询,生成一课对应的解析树 + * 先做**词法分析**,输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么代表什么。从输入的 select 这个关键字识别出来这是一个查询语句;把字符串 t 识别成 表名 t,把字符串 id 识别成列 id * 然后做**语法分析**,根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果你的语句不对,就会收到 `You have an error in your SQL syntax` 的错误提醒 +预处理器:进一步检查解析树的合法性,比如数据表和数据列是否存在、别名是否有歧义等 + *** @@ -647,7 +651,7 @@ MySQL 在真正执行语句之前,并不能精确地知道满足条件的记 #### 执行器 -开始执行的时候,要先判断一下当前连接对表有没有执行查询的权限,如果没有就会返回没有权限的错误,在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。如果有权限,就打开表继续执行,执行器就会根据表的引擎定义,去使用这个引擎提供的接口 +开始执行的时候,要先判断一下当前连接对表有没有**执行查询的权限**,如果没有就会返回没有权限的错误,在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。如果有权限,就打开表继续执行,执行器就会根据表的引擎定义,去使用这个引擎提供的接口 @@ -715,7 +719,9 @@ InnoDB 的数据是按页存储的如果删掉了一个数据页上的所有记 #### 重建数据 -重建表就是按照主键 ID 递增的顺序,把数据一行一行地从旧表中读出来再插入到新表中,重建表时 MySQL 会自动完成转存数据、交换表名、删除旧表的操作,重建命令: +重建表就是按照主键 ID 递增的顺序,把数据一行一行地从旧表中读出来再插入到新表中,重建表时 MySQL 会自动完成转存数据、交换表名、删除旧表的操作,线上操作会阻塞大量的线程增删改查的操作 + +重建命令: ```sql ALTER TABLE A ENGINE=InnoDB @@ -3934,7 +3940,7 @@ BTree 又叫多路平衡搜索树,一颗 m 叉的 BTree 特性如下: 插入 C N G A H E K Q M F W L T Z D P R X Y S 数据的工作流程: -* 插入前4个字母 C N G A +* 插入前 4 个字母 C N G A ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程1.png) @@ -4040,12 +4046,16 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 每个索引中每个块存储在磁盘页中,可能会出现以下两种情况: -* 如果所在的数据页已经满了,这时候需要申请一个新的数据页,然后挪动部分数据过去,这个过程称为**页分裂** +* 如果所在的数据页已经满了,这时候需要申请一个新的数据页,然后挪动部分数据过去,这个过程称为**页分裂**,原本放在一个页的数据现在分到两个页中,降低了空间利用率 * 当相邻两个页由于删除了数据,利用率很低之后,会将数据页做**页合并**,合并的过程可以认为是分裂过程的逆过程 * 这两个情况都是由 B+ 树的结构决定的 一般选用数据小的字段做索引,字段长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小 +自增主键的插入数据模式,不涉及到挪动其他记录,也**不会触发叶子节点的分裂** + + + *** @@ -5319,8 +5329,6 @@ MySQL 提供了缓冲页的快速查找方式:**哈希表**,使用表空间 - - *** @@ -5943,7 +5951,7 @@ undo log 是采用段的方式来记录,Rollback Segement 称为回滚段, MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来**解决读写冲突的无锁并发控制**,可以在发生读写请求冲突时不用加锁解决,这个读是指的快照读(也叫一致性读或一致性无锁读),而不是当前读: * 快照读:实现基于 MVCC,因为是多版本并发,所以快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据 -* 当前读:读取数据库记录是当前最新的版本(产生幻读、不可重复读),可以对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读 +* 当前读:读取数据库记录是当前**最新的版本**(产生幻读、不可重复读),可以对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读 数据库并发场景: @@ -6036,7 +6044,7 @@ Read View 是事务进行读数据操作时产生的读视图,该事务执行 注意:这里的快照并不是把所有的数据拷贝一份副本,而是由 undo log 记录的逻辑日志,根据库中的数据进行计算出历史数据 -工作流程:将版本链的头节点的事务 ID(最新数据事务 ID)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比进行可见性分析,不可见就通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足可见性的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 +工作流程:将版本链的头节点的事务 ID(最新数据事务 ID,大概率不是当前线程)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比进行可见性分析,不可见就通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足可见性的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 Read View 几个属性: @@ -6048,7 +6056,7 @@ Read View 几个属性: creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交) * db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以此数据对 creator 是可见的 -* db_trx_id < min_trx_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见 +* db_trx_id < min_trx_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见(因为比已提交的最大事务 ID 小的并不一定已经提交,所以应该先判断是否在活跃事务列表) * db_trx_id >= max_trx_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 * min_trx_id<= db_trx_id < max_trx_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 @@ -6136,13 +6144,13 @@ RC、RR 级别下的 InnoDB 快照读区别 - RR 级别下,某个事务的对某条记录的**第一次快照读**会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个 Read View,所以一个事务的查询结果每次都是相同的 - RR 级别下,通过 `START TRANSACTION WITH CONSISTENT SNAPSHOT` 开启事务,会在执行该语句后立刻生成一个 Read View,不是在执行第一条 SELECT 语句时生成 + RR 级别下,通过 `START TRANSACTION WITH CONSISTENT SNAPSHOT` 开启事务,会在执行该语句后立刻生成一个 Read View,不是在执行第一条 SELECT 语句时生成(所以说 `START TRANSACTION` 并不是事务的起点,执行第一条语句才算起点) 解决幻读问题: - 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** - 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1 去 UPDATE 该行会发现更新成功,因为 **Read View 并不能阻止事务去更新数据**,并且把这条新记录的 trx_id 变为当前的事务 id,所以对当前事务就是可见的 + 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1 去 UPDATE 该行会发现更新成功,并且把这条新记录的 trx_id 变为当前的事务 id,所以对当前事务就是可见的。因为 **Read View 并不能阻止事务去更新数据,更新数据都是先读后写并且是当前读**,读取到的是最新版本的数据 - 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 @@ -6177,6 +6185,8 @@ Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机 + + **** @@ -6221,7 +6231,7 @@ InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到 * 服务器关闭时 * checkpoint 时(下小节详解) -redo 日志在磁盘中以文件组的形式存储,同一组中的每个文件大小一样格式一样, +InnoDB 的 redo log 是**固定大小**的,redo 日志在磁盘中以文件组的形式存储,同一组中的每个文件大小一样格式一样, * `innodb_log_group_home_dir` 代表磁盘存储 redo log 的文件目录,默认是当前数据目录 * `innodb_log_file_size` 代表文件大小,默认 48M,`innodb_log_files_in_group` 代表文件个数,默认 2 最大 100,所以日志的文件大小为 `innodb_log_file_size * innodb_log_files_in_group` @@ -6249,13 +6259,15 @@ MTR 的执行过程中修改过的页对应的控制块会加到 Buffer Pool 的 * oldest_modification:第一次修改 Buffer Pool 中某个缓冲页时,将修改该页的 MTR **开始时**对应的 lsn 值写入这个属性,所以链表页是以该值进行排序的 * newest_modification:每次修改页面,都将 MTR 结束时全局的 lsn 值写入这个属性,所以该值是该页面最后一次修改后的 lsn 值 -全局变量 checkpoint_lsn 表示当前系统可以被覆盖的 redo 日志总量,当 redo 日志对应的脏页已经被刷新到磁盘后,该文件空间就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息,刷脏和执行一次 checkpoint 并不是同一个线程 +全局变量 checkpoint_lsn 表示**当前系统可以被覆盖的 redo 日志总量**,当 redo 日志对应的脏页已经被刷新到磁盘后,该文件空间就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息(刷脏和执行一次 checkpoint 并不是同一个线程),该值的增量就代表磁盘文件中当前位置向后可以被覆盖的文件的量,所以该值是一直增大的 **checkpoint**:从 flush 链表尾部中找出还未刷脏的页面,该页面是当前系统中最早被修改的脏页,该页面之前产生的脏页都已经刷脏,然后将该页 oldest_modification 值赋值给 checkpoint_lsn,因为 lsn 小于该值时产生的 redo 日志都可以被覆盖了 -checkpoint_lsn 是一个总量,随着 lsn 写入的增加,刷脏的继续进行,所以 checkpoint_lsn 值就会一直变大,该值的增量就代表磁盘文件中当前位置向后可以被覆盖的文件的量 +但是在系统忙碌时,后台线程的刷脏操作不能将脏页快速刷出,导致系统无法及时执行 checkpoint ,这时需要用户线程从 flush 链表中把最早修改的脏页刷新到磁盘中,然后执行 checkpoint -在系统忙碌时,后台线程的刷脏操作不能将脏页快速刷出,导致系统无法及时执行 checkpoint,这时需要用户线程从 flush 链表中把最早修改的脏页刷新到磁盘中,然后执行 checkpoint +```java +write pos ------- checkpoint_lsn // 两值之间的部分表示可以写入的日志量,当 pos 追赶上 lsn 时必须执行 checkpoint +``` 使用命令可以查看当前 InnoDB 存储引擎各种 lsn 的值: @@ -6280,6 +6292,8 @@ SHOW ENGINE INNODB STATUS\G * 使用哈希表:根据 redo log 的 space ID 和 page number 属性计算出哈希值,将对同一页面的修改放入同一个槽里,可以一次性完成对某页的恢复,**避免了随机 IO** * 跳过已经刷新到磁盘中的页面:数据页的 File Header 中的 FILE_PAGE_LSN 属性(类似 newest_modification)表示最近一次修改页面时的 lsn 值,如果在 checkpoint 后,数据页被刷新到磁盘中,那么该页 lsn 属性肯定大于 checkpoint_lsn +总结:先写 redo buffer,在写 change buffer,先刷 redo log,再刷脏,在删除完成刷脏 redo log + 参考书籍:https://book.douban.com/subject/35231266/ @@ -6309,7 +6323,7 @@ MySQL 中还存在 binlog(二进制日志)也可以记录写操作并用于 ##### 更新记录 -更新一条记录的过程: +更新一条记录的过程:写之前一定先读 * 在 B+ 树中定位到该记录(这个过程也被称作加锁读),如果该记录所在的页面不在 Buffer Pool 里,先将其加载进内存 @@ -6320,7 +6334,7 @@ MySQL 中还存在 binlog(二进制日志)也可以记录写操作并用于 注意:修改 undo页面也是在**修改页面**,事务凡是修改页面就需要先记录相应的 redo 日志 - * 然后记录更新数据对应的的 redo 日志(等待 MTR 提交后写入 redo log buffer),最后进行真正的更新记录 + * 然后**先记录对应的的 redo 日志**(等待 MTR 提交后写入 redo log buffer),**最后进行真正的更新记录** * 更新其他的二级索引记录,不会再记录 undo log,只记录 redo log 到 buffer 中 @@ -6346,18 +6360,26 @@ update T set c=c+1 where ID=2; -流程说明:执行引擎将这行新数据更新到内存中(Buffer Pool)后,将这个更新操作记录到 redo log buffer 里,然后更新记录。redo log 刷盘后事务处于 prepare 状态,执行器会生成这个操作的 binlog,并**把 binlog 写入磁盘**,完成提交 +流程说明:执行引擎将这行新数据读入到内存中(Buffer Pool)后,先将此次更新操作记录到 redo log buffer 里,然后更新记录。最后将 redo log 刷盘后事务处于 prepare 状态,执行器会生成这个操作的 binlog,并**把 binlog 写入磁盘**,完成提交 两阶段: * Prepare 阶段:存储引擎将该事务的 **redo 日志刷盘**,并且将本事务的状态设置为 PREPARE,代表执行完成随时可以提交事务 -* Commit 阶段:先将事务执行过程中产生的 binlog 刷新到硬盘,再执行存储引擎的提交工作 +* Commit 阶段:先将事务执行过程中产生的 binlog 刷新到硬盘,再执行存储引擎的提交工作,引擎把 redo log 改成提交状态 redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 -系统崩溃前没有提交的事务的 redo log 可能已经刷盘(定时线程或者 checkpoint),如何防止这些数据可能在重启后也会恢复? -解决:通过 undo log 在服务器重启时将未提交的事务回滚掉,首先定位到 128 个回滚段遍历 slot,获取 undo 链表首节点页面的 undo segement header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,事务状态是活跃的就全部回滚,如果是 PREPARE 状态,就需要根据 binlog 的状态进行判断: + +*** + + + +##### 数据恢复 + +系统崩溃前没有提交的事务的 redo log 可能已经刷盘(定时线程或者 checkpoint),怎么处理崩溃恢复? + +工作流程:通过 undo log 在服务器重启时将未提交的事务回滚掉。首先定位到 128 个回滚段遍历 slot,获取 undo 链表首节点页面的 undo segement header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,事务状态是活跃的就全部回滚,如果是 PREPARE 状态,就需要根据 binlog 的状态进行判断: * 如果在时刻 A 发生了崩溃(crash),由于此时 binlog 还没完成,所以需要进行回滚 * 如果在时刻 B 发生了崩溃,redo log 和 binlog 有一个共同的数据字段叫 XID,崩溃恢复的时候,会按顺序扫描 redo log: @@ -6506,19 +6528,19 @@ MDL 叫元数据锁,主要用来保护 MySQL 内部对象的元数据,保证 MDL 锁的特性: -* MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,在事务开始执行时申请,在整个事务提交后释放 +* MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,在事务开始时申请,整个事务提交后释放(执行完单条语句不释放) * MDL 锁是在 Server 中实现,不是 InnoDB 存储引擎层能直接实现的锁 * MDL 锁还能实现其他粒度级别的锁,比如全局锁、库级别的锁、表空间级别的锁 -FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处于只读状态,工作流程: +FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处于只读状态,DDL DML 都被阻塞,工作流程: 1. 上全局读锁(lock_global_read_lock) 2. 清理表缓存(close_cached_tables) 3. 上全局 COMMIT 锁(make_global_read_lock_block_commit) -该命令主要用于备份工具做一致性备份,由于 FTWRL 需要持有两把全局的 MDL 锁,并且还要关闭所有表对象,因此杀伤性很大 +该命令主要用于备份工具做**一致性备份**,由于 FTWRL 需要持有两把全局的 MDL 锁,并且还要关闭所有表对象,因此杀伤性很大 @@ -6707,7 +6729,9 @@ InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用 - 共享锁 (S):又称为读锁,简称 S 锁,多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 - 排他锁 (X):又称为写锁,简称 X 锁,不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 -RR 隔离界别下,对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放(在事务中加的锁,会**在事务中止或提交时自动释放**);对于普通 SELECT 语句,不会加任何锁(只是针对 InnoDB 层来说的,因为在 Server 层会**加 MDL 读锁**),通过 MVCC 防止并发冲突 +RR 隔离界别下,对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 时自动释放;对于普通 SELECT 语句,不会加任何锁(只是针对 InnoDB 层来说的,因为在 Server 层会**加 MDL 读锁**),通过 MVCC 防止并发冲突 + +在事务中加的锁,并不是不需要了就释放,而是**在事务中止或提交时自动释放**,这个就是两阶段锁协议。所以一般将更新共享资源(并发高)的 SQL 放到事务的最后执行,可以让其他线程尽量的减少等待时间 锁的兼容性: @@ -7012,8 +7036,11 @@ InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面 解决策略: -* 直接进入等待直到超时,超时时间可以通过参数 innodb_lock_wait_timeout 来设置,但是时间的设置不好控制,超时可能不是因为死锁,而是因为事务处理比较慢,所以一般不采取该方式 -* 主动死锁检测,发现死锁后**主动回滚死锁链条中较小的一个事务**,让其他事务得以继续执行,将参数 `innodb_deadlock_detect` 设置为 on,表示开启该功能。较小的意思就是事务执行过程中插入、删除、更新的记录条数 +* 直接进入等待直到超时,超时时间可以通过参数 innodb_lock_wait_timeout 来设置,默认 50 秒,但是时间的设置不好控制,超时可能不是因为死锁,而是因为事务处理比较慢,所以一般不采取该方式 + +* 主动死锁检测,发现死锁后**主动回滚死锁链条中较小的一个事务**,让其他事务得以继续执行,将参数 `innodb_deadlock_detect` 设置为 on,表示开启该功能(事务较小的意思就是事务执行过程中插入、删除、更新的记录条数) + + 死锁检测并不是每个语句都要检测,只有在加锁访问的行上已经有锁时,当前事务被阻塞了才会检测,也是从当前事务开始进行检测 通过执行 `SHOW ENGINE INNODB STATUS` 可以查看最近发生的一次死循环,全局系统变量 `innodb_print_all_deadlocks` 设置为 on,就可以将每个死锁信息都记录在 MySQL 错误日志中 @@ -7077,7 +7104,7 @@ lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁 - 对于读的操作远多于写的操作的时候,一个更新操作加锁会阻塞所有的读取操作,降低了吞吐量,最后需要释放锁,锁是需要一些开销的,这时候可以选择乐观锁 - 如果是读写比例差距不是非常大或者系统没有响应不及时,吞吐量瓶颈的问题,那就不要去使用乐观锁,它增加了复杂度,也带来了业务额外的风险,这时候可以选择悲观锁 -乐观锁的现方式: +乐观锁的实现方式:就是 CAS,比较并交换 * 版本号 @@ -7114,7 +7141,9 @@ lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁 - 读取数据时,将时间读取出来,在执行更新的时候,比较时间 - 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 +乐观锁的异常情况:如果 version 被其他事务抢先更新,则在当前事务中更新失败,trx_id 没有变成当前事务的 ID,当前事务再次查询还是旧值,就会出现**值没变但是更新不了**的现象(anomaly) +解决方案:每次 CAS 更新不管成功失败,就结束当前事务;如果失败则重新起一个事务进行查询更新 diff --git a/Frame.md b/Frame.md index 1916253..6f82fd0 100644 --- a/Frame.md +++ b/Frame.md @@ -10229,15 +10229,15 @@ ConsumeRequest 是 ConsumeMessageOrderlyService 的内部类,是一个 Runnabl * 首先获取当前消息主题的发布信息,获取不到去 Namesrv 获取(默认有 TBW102),并将获取的到的路由数据转化为发布数据,**创建 MQ 队列**,客户端实例同样更新订阅数据,创建 MQ 队列,放入负载均衡服务 topicSubscribeInfoTable 中 * 然后从发布数据中选择一个 MQ 队列发送消息 -* Broker 端通过 SendMessageProcessor 对发送的消息进行持久化处理,存储到 CommitLog。将重试次数过多的消息加入死信队列,将延迟级别消息的主题和队列修改为调度主题和调度队列 ID +* Broker 端通过 SendMessageProcessor 对发送的消息进行持久化处理,存储到 CommitLog。将重试次数过多的消息加入死信队列,将延迟消息的主题和队列修改为调度主题和调度队列 ID * Broker 启动 ScheduleMessageService 服务会为每个延迟级别创建一个延迟任务,让延迟消息得到有效的处理,将到达交付时间的消息修改为原始主题的原始 ID 存入 CommitLog,消费者就可以进行消费了 消费流程: * 首先通过负载均衡服务,将分配到当前消费者实例的 MQ 创建 PullRequest,并放入 PullMessageService 的本地阻塞队列内 -* PullMessageService 循环从阻塞队列获取请求对象,发起拉消息请求,并创建 PullCallback 回调对象,将正常拉取的消息**提交到消费任务线程池**,并设置下一次拉取的位点,重新放入阻塞队列,形成闭环 +* PullMessageService 循环从阻塞队列获取请求对象,发起拉消息请求,并创建 PullCallback 回调对象,将正常拉取的消息**提交到消费任务线程池**,并设置请求的下一次拉取位点,重新放入阻塞队列,形成闭环 * 消费任务服务对消费失败的消息进行回退,回退失败的消息会再次提交消费任务重新消费 -* Broker 端对拉取消息的请求进行处理(processRequestCommand),查询成功将消息放入响应体,通过 Netty 写回客户端,当 `pullRequest.offset == queue.maxOffset` 说明该队列已经没有需要获取的消息,将请求放入长轮询集合等待有新消息 +* Broker 端对拉取消息的请求进行处理(processRequestCommand),查询成功将消息放入响应体,通过 Netty 写回客户端,当 `pullRequest.offset == queue.maxOffset` 说明该队列已经没有需要获取的消息,将请求放入长轮询集合等待有新消息 * PullRequestHoldService 负责长轮询,每 5 秒遍历一次长轮询集合,将满足条件的 PullRequest 再次提交到线程池内处理 diff --git a/Java.md b/Java.md index 7ce2e5f..3828bec 100644 --- a/Java.md +++ b/Java.md @@ -10388,6 +10388,9 @@ JVM 结构: JVM、JRE、JDK 对比: +* JDK(Java SE Development Kit):Java 标准开发包,它提供了编译、运行 Java 程序所需的各种工具和资源 +* JRE( Java Runtime Environment):Java 运行环境,用于解释执行 Java 的字节码文件 + diff --git a/Prog.md b/Prog.md index ca94bb1..4800568 100644 --- a/Prog.md +++ b/Prog.md @@ -2320,7 +2320,11 @@ CPU 的基本工作是执行存储的指令序列,即程序,程序的执行 * 单线程环境也存在指令重排,由于存在依赖性,最终执行结果和代码顺序的结果一致 * 多线程环境中线程交替执行,由于编译器优化重排,会获取其他线程处在不同阶段的指令同时执行 +补充知识: +* 指令周期是取出一条指令并执行这条指令的时间,一般由若干个机器周期组成 +* 机器周期也称为 CPU 周期,一条指令的执行过程划分为若干个阶段(如取指、译码、执行等),每一阶段完成一个基本操作,完成一个基本操作所需要的时间称为机器周期 +* 振荡周期指周期性信号作周期性重复变化的时间间隔 @@ -5966,6 +5970,17 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 ##### 添加线程 +* prestartAllCoreThreads():**提前预热**,创建所有的核心线程 + + ```java + public int prestartAllCoreThreads() { + int n = 0; + while (addWorker(null, true)) + ++n; + return n; + } + ``` + * addWorker():**添加线程到线程池**,返回 true 表示创建 Worker 成功,且线程启动。首先判断线程池是否允许添加线程,允许就让线程数量 + 1,然后去创建 Worker 加入线程池 注意:SHUTDOWN 状态也能添加线程,但是要求新加的 Woker 没有 firstTask,而且当前 queue 不为空,所以创建一个线程来帮助执行队列中的任务 From 89def8471ecd442d40b255b85189abe7cbe85d10 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 19 Mar 2022 10:56:57 +0800 Subject: [PATCH 06/51] Update Java Note --- DB.md | 178 ++++++++++++++++++++++++++++++++++--------------------- Frame.md | 113 ++++++++++++++++++----------------- Java.md | 5 +- Prog.md | 22 +++---- Web.md | 5 +- 5 files changed, 188 insertions(+), 135 deletions(-) diff --git a/DB.md b/DB.md index 24fb3e3..0d9b44e 100644 --- a/DB.md +++ b/DB.md @@ -340,7 +340,7 @@ mysqlshow -uroot -p1234 test book --count 体系结构详解: * 第一层:网络连接层 - * 一些客户端和链接服务,包含本地 socket 通信和大多数基于客户端/服务端工具实现的 TCP/IP 通信,主要完成一些类似于连接处理、授权认证、及相关的安全方案 + * 一些客户端和链接服务,包含本地 Socket 通信和大多数基于客户端/服务端工具实现的 TCP/IP 通信,主要完成一些类似于连接处理、授权认证、及相关的安全方案 * 在该层上引入了**连接池** Connection Pool 的概念,管理缓冲用户连接,线程处理等需要缓存的需求 * 在该层上实现基于 SSL 的安全链接,服务器也会为安全接入的每个客户端验证它所具有的操作权限 @@ -605,7 +605,7 @@ MySQL 中保存着两种统计数据: * innodb_table_stats 存储了表的统计数据,每一条记录对应着一个表的统计数据 * innodb_index_stats 存储了索引的统计数据,每一条记录对应着一个索引的一个统计项的数据 -MySQL 在真正执行语句之前,并不能精确地知道满足条件的记录有多少条,只能根据统计信息来估算记录,统计信息就是索引的区分度,一个索引上不同的值的个数(比如性别只能是男女,就是 2 ),称之为基数(cardinality),基数越大说明区分度越好 +MySQL 在真正执行语句之前,并不能精确地知道满足条件的记录有多少条,只能根据统计信息来估算记录,统计信息就是索引的区分度,一个索引上不同的值的个数(比如性别只能是男女,就是 2 ),称之为基数(cardinality),**基数越大说明区分度越好** 通过**采样统计**来获取基数,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数 @@ -614,7 +614,7 @@ MySQL 在真正执行语句之前,并不能精确地知道满足条件的记 * ON:表示统计信息会持久化存储(默认),采样页数 N 默认为 20,可以通过 `innodb_stats_persistent_sample_pages` 指定,页数越多统计的数据越准确,但消耗的资源更大 * OFF:表示统计信息只存储在内存,采样页数 N 默认为 8,也可以通过系统变量设置(不推荐,每次重新计算浪费资源) -数据表是会持续更新的,两种更新方式: +数据表是会持续更新的,两种统计信息的更新方式: * 设置 `innodb_stats_auto_recalc` 为 1,当发生变动的记录数量超过表大小的 10% 时,自动触发重新计算,不过是**异步进行** * 调用 `ANALYZE TABLE t` 手动更新统计信息,只对信息做**重新统计**(不是重建表),没有修改数据,这个过程中加了 MDL 读锁并且是同步进行,所以会暂时阻塞系统 @@ -665,7 +665,7 @@ Server 层和存储引擎层的交互是**以记录为单位的**,存储引擎 工作流程: -* 首先根据二级索引选择扫描范围,获取第一条符合条件的记录,进行回表查询,将聚簇索引的记录返回 +* 首先根据二级索引选择扫描范围,获取第一条符合条件的记录,进行回表查询,将聚簇索引的记录返回 Server 层 * 然后在二级索引上继续扫描下一个符合条件的记录 @@ -703,7 +703,7 @@ Server 层和存储引擎层的交互是**以记录为单位的**,存储引擎 #### 数据删除 -MySQL 的数据删除就是移除掉某个记录后,该位置就被标记为可复用,如果有符合范围条件的数据可以插入到这里。符合范围条件的意思是假设删除记录 R4,之后要再插入一个 ID 在 300 和 600 之间的记录时,就会复用这个位置 +MySQL 的数据删除就是移除掉某个记录后,该位置就被标记为**可复用**,如果有符合范围条件的数据可以插入到这里。符合范围条件的意思是假设删除记录 R4,之后要再插入一个 ID 在 300 和 600 之间的记录时,就会复用这个位置 @@ -719,7 +719,7 @@ InnoDB 的数据是按页存储的如果删掉了一个数据页上的所有记 #### 重建数据 -重建表就是按照主键 ID 递增的顺序,把数据一行一行地从旧表中读出来再插入到新表中,重建表时 MySQL 会自动完成转存数据、交换表名、删除旧表的操作,线上操作会阻塞大量的线程增删改查的操作 +重建表就是按照主键 ID 递增的顺序,把数据一行一行地从旧表中读出来再插入到新表中,让数据更加紧凑。重建表时 MySQL 会自动完成转存数据、交换表名、删除旧表的操作,线上操作会阻塞大量的线程增删改查的操作 重建命令: @@ -731,7 +731,7 @@ ALTER TABLE A ENGINE=InnoDB 重建表的步骤需要 DDL 不是 Online 的,因为在导入数据的过程有新的数据要写入到表 A 的话,就会造成数据丢失 -MySQL 5.6 版本开始引入的 Online DDL,重建表的命令默认执行此步骤: +MySQL 5.6 版本开始引入的 **Online DDL**,重建表的命令默认执行此步骤: * 建立一个临时文件 tmp_file(InnoDB 创建),扫描表 A 主键的所有数据页 * 用数据页中表 A 的记录生成 B+ 树,存储到临时文件中 @@ -741,7 +741,7 @@ MySQL 5.6 版本开始引入的 Online DDL,重建表的命令默认执行此 -Online DDL 操作会先获取 MDL 写锁,再退化成 MDL 读锁。但 MDL 写锁持有时间比较短,所以可以称为 Online; 而 MDL 读锁,不阻止数据增删查改,但会阻止其它线程修改表结构 +Online DDL 操作会先获取 MDL 写锁,再退化成 MDL 读锁。但 MDL 写锁持有时间比较短,所以可以称为 Online; 而 MDL 读锁,不阻止数据增删查改,但会阻止其它线程修改表结构(可以对比 `ANALYZE TABLE t` 命令) 问题:重建表可以收缩表空间,但是执行指令后整体占用空间增大 @@ -3505,11 +3505,11 @@ InnoDB 存储引擎:(MySQL5.5 版本后默认的存储引擎) MEMORY 存储引擎: -- 特点:每个 MEMORY 表实际对应一个磁盘文件 ,该文件中只存储表的结构,表数据保存在内存中,且默认使用 HASH 索引,这样有利于数据的快速处理,在需要快速定位记录可以提供更快的访问,但是服务一旦关闭,表中的数据就会丢失,数据存储不安全 +- 特点:每个 MEMORY 表实际对应一个磁盘文件 ,该文件中只存储表的结构,表数据保存在内存中,且默认**使用 HASH 索引**,这样有利于数据的快速处理,在需要快速定位记录可以提供更快的访问,但是服务一旦关闭,表中的数据就会丢失,数据存储不安全 - 应用场景:通常用于更新不太频繁的小表,用以快速得到访问结果,类似缓存 - 存储方式:表结构保存在 .frm 中 -MERGE存储引擎: +MERGE 存储引擎: * 特点: @@ -3537,23 +3537,23 @@ MERGE存储引擎: ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MERGE.png) -| 特性 | MyISAM | InnoDB | MEMORY | -| ------------ | ---------------------------- | ------------- | ------------------ | -| 存储限制 | 有(平台对文件系统大小的限制) | 64TB | 有(平台的内存限制) | -| **事务安全** | **不支持** | **支持** | **不支持** | -| **锁机制** | **表锁** | **表锁/行锁** | **表锁** | -| B+Tree索引 | 支持 | 支持 | 支持 | -| 哈希索引 | 不支持 | 不支持 | 支持 | -| 全文索引 | 支持 | 支持 | 不支持 | -| 集群索引 | 不支持 | 支持 | 不支持 | -| 数据索引 | 不支持 | 支持 | 支持 | -| 数据缓存 | 不支持 | 支持 | N/A | -| 索引缓存 | 支持 | 支持 | N/A | -| 数据可压缩 | 支持 | 不支持 | 不支持 | -| 空间使用 | 低 | 高 | N/A | -| 内存使用 | 低 | 高 | 中等 | -| 批量插入速度 | 高 | 低 | 高 | -| **外键** | **不支持** | **支持** | **不支持** | +| 特性 | MyISAM | InnoDB | MEMORY | +| ------------ | ------------------------------ | ------------- | -------------------- | +| 存储限制 | 有(平台对文件系统大小的限制) | 64TB | 有(平台的内存限制) | +| **事务安全** | **不支持** | **支持** | **不支持** | +| **锁机制** | **表锁** | **表锁/行锁** | **表锁** | +| B+Tree索引 | 支持 | 支持 | 支持 | +| 哈希索引 | 不支持 | 不支持 | 支持 | +| 全文索引 | 支持 | 支持 | 不支持 | +| 集群索引 | 不支持 | 支持 | 不支持 | +| 数据索引 | 不支持 | 支持 | 支持 | +| 数据缓存 | 不支持 | 支持 | N/A | +| 索引缓存 | 支持 | 支持 | N/A | +| 数据可压缩 | 支持 | 不支持 | 不支持 | +| 空间使用 | 低 | 高 | N/A | +| 内存使用 | 低 | 高 | 中等 | +| 批量插入速度 | 高 | 低 | 高 | +| **外键** | **不支持** | **支持** | **不支持** | 面试问题:MyIsam 和 InnoDB 的区别? @@ -3998,7 +3998,7 @@ B+Tree 为 BTree 的变种,B+Tree 与 BTree 的区别为: - 所有**非叶子节点只存储键值 key** 信息,只进行数据索引,使每个非叶子节点所能保存的关键字大大增加 - 所有**数据都存储在叶子节点**,所以每次数据查询的次数都一样 - **叶子节点按照 key 大小顺序排列,左边结尾数据都会保存右边节点开始数据的指针,形成一个链表** -- 所有节点中的 key 在叶子节点中也存在(比如 5),key 允许重复,B 树不同节点不存在重复的 key +- 所有节点中的 key 在叶子节点中也存在(比如 5),**key 允许重复**,B 树不同节点不存在重复的 key @@ -4150,7 +4150,7 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 索引下推充分利用了索引中的数据,在查询出整行数据之前过滤掉无效的数据,再去主键索引树上查找 -* 不使用索引下推优化时存储引擎通过索引检索到数据,然后回表查询记录返回给 MySQL Server 层,服务器判断数据是否符合条件 +* 不使用索引下推优化时存储引擎通过索引检索到数据,然后回表查询记录返回给 Server 层,**服务器判断数据是否符合条件** ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-不使用索引下推.png) * 使用索引下推优化时,如果**存在某些被索引的列的判断条件**时,由存储引擎在索引遍历的过程中判断数据是否符合传递的条件,将符合条件的数据进行回表,检索出来返回给服务器,由此减少 IO 次数 @@ -4616,7 +4616,7 @@ SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的** #### TRACE -MySQL 提供了对 SQL 的跟踪, 通过 trace 文件可以查看优化器生成执行计划的过程 +MySQL 提供了对 SQL 的跟踪, 通过 trace 文件可以查看优化器**生成执行计划的过程** * 打开 trace 功能,设置格式为 JSON,并设置 trace 的最大使用内存,避免解析过程中因默认内存过小而不能够完整展示 @@ -5050,8 +5050,8 @@ CREATE INDEX idx_emp_age_salary ON emp(age,salary); 对于 Filesort , MySQL 有两种排序算法: -* 两次扫描算法:MySQL4.1 之前,使用该方式排序。首先根据条件取出排序字段和行指针信息,然后在排序区 sort buffer 中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后再根据行指针**回表读取记录**,该操作可能会导致大量随机 I/O 操作 -* 一次扫描算法:一次性取出满足条件的所有数据,需要回表,然后在排序区 sort buffer 中排序后直接输出结果集。排序时内存开销较大,但是排序效率比两次扫描算法高 +* rowid 排序:MySQL4.1 之前,使用该方式排序。首先根据条件(回表)取出排序字段和信息,然后在排序区 sort buffer 中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后再根据行指针**回表读取记录**,该操作可能会导致大量随机 I/O 操作 +* 全字段排序:一次性取出满足条件的所有数据,需要回表,然后在排序区 sort buffer 中排序后直接输出结果集。排序时内存开销较大,但是排序效率比两次扫描算法高 MySQL 通过比较系统变量 max_length_for_sort_data 的大小和 Query 语句取出的字段的大小,来判定使用哪种排序算法。如果前者大,则说明 sort buffer 空间足够,使用第二种优化之后的算法,否则使用第一种。 @@ -5064,6 +5064,8 @@ SHOW VARIABLES LIKE 'max_length_for_sort_data'; -- 默认1024 SHOW VARIABLES LIKE 'sort_buffer_size'; -- 默认262114 ``` +注意:sort buffer 在 Server 层 + *** @@ -5278,8 +5280,8 @@ count 函数的按照效率排序:`count(字段) < count(主键id) < count(1) * count(主键 id):InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来返回给 Server 层,Server 判断 id 不为空就按行累加 * count(1):InnoDB 引擎遍历整张表但不取值,Server 层对于返回的每一行,放一个数字 1 进去,判断不为空就按行累加 - * count(字段):如果这个字段是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;如果这个字段定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加 +* count(*):不取值,按行累加 @@ -5318,8 +5320,6 @@ Buffer Pool 本质上是 InnoDB 向操作系统申请的一段连续的内存空 * 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool * 向数据库写入数据时,会写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 -**唯一索引的更新不能使用 Buffer,只有普通索引可以使用,直接写入 Buffer 就结束,不用校验唯一性** - Buffer Pool 中每个缓冲页都有对应的控制信息,包括表空间编号、页号、偏移量、链表信息等,控制信息存放在占用的内存称为控制块,控制块与缓冲页是一一对应的,但并不是物理上相连的,都在缓冲池中 MySQL 提供了缓冲页的快速查找方式:**哈希表**,使用表空间号和页号作为 Key,缓冲页控制块的地址作为 Value 创建一个哈希表,获取数据页时根据 Key 进行哈希寻址: @@ -5327,6 +5327,8 @@ MySQL 提供了缓冲页的快速查找方式:**哈希表**,使用表空间 * 如果不存在对应的缓存页,就从 free 链表中选一个空闲缓冲页,把磁盘中的对应页加载到该位置 * 如果存在对应的缓存页,直接获取使用,提高查询数据的效率 +当内存数据页跟磁盘数据页内容不一致时,称这个内存页为脏页;内存数据写入磁盘后,内存和磁盘上的数据页一致,称为干净页 + *** @@ -5369,7 +5371,7 @@ Flush 链表是一个用来**存储脏页**的链表,对于已经修改过的 * 从 Flush 链表中刷新一部分页面到磁盘: * **后台线程定时**从 Flush 链表刷脏,根据系统的繁忙程度来决定刷新速率,这种方式称为 BUF_FLUSH_LIST - * 线程刷脏的比较慢,导致用户线程加载一个新的数据页时发现没有空闲缓冲页,此时会尝试从 LRU 链表尾部寻找未修改的缓冲页直接释放,如果没有就会将 LRU 链表尾部的一个脏页**同步刷新**到磁盘,速度较慢,这种方式称为 BUF_FLUSH_SINGLE_PAGE + * 线程刷脏的比较慢,导致用户线程加载一个新的数据页时发现没有空闲缓冲页,此时会尝试从 LRU 链表尾部寻找未修改的缓冲页直接释放,如果没有就将 LRU 链表尾部的一个脏页**同步刷新**到磁盘,速度较慢,这种方式称为 BUF_FLUSH_SINGLE_PAGE * 从 LRU 链表的冷数据中刷新一部分页面到磁盘,即:BUF_FLUSH_LRU * 后台线程会定时从 LRU 链表的尾部开始扫描一些页面,扫描的页面数量可以通过系统变量 `innodb_lru_scan_depth` 指定,如果在 LRU 链表中发现脏页,则把它们刷新到磁盘,这种方式称为 BUF_FLUSH_LRU * 控制块里会存储该缓冲页是否被修改的信息,所以可以很容易的获取到某个缓冲页是否是脏页 @@ -5396,7 +5398,7 @@ MySQL 基于局部性原理提供了预读功能: * 线性预读:系统变量 `innodb_read_ahead_threshold`,如果顺序访问某个区(extent:16 KB 的页,连续 64 个形成一个区,一个区默认 1MB 大小)的页面数超过了该系统变量值,就会触发一次**异步读取**下一个区中全部的页面到 Buffer Pool 中 * 随机预读:如果某个区 13 个连续的页面都被加载到 Buffer Pool,无论这些页面是否是顺序读取,都会触发一次**异步读取**本区所有的其他页面到 Buffer Pool 中 -预读会造成加载太多用不到的数据页,造成那些使用频率很高的数据页被挤到 LRU 链表尾部,所以 InnoDB 将 LRU 链表分成两段: +预读会造成加载太多用不到的数据页,造成那些使用**频率很高的数据页被挤到 LRU 链表尾部**,所以 InnoDB 将 LRU 链表分成两段: * 一部分存储使用频率很高的数据页,这部分链表也叫热数据,young 区 * 一部分存储使用频率不高的冷数据,old 区,默认占 37%,可以通过系统变量 `innodb_old_blocks_pct` 指定 @@ -5414,7 +5416,7 @@ MySQL 基于局部性原理提供了预读功能: #### 参数优化 -Innodb 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 Innodb 的索引块,也用来缓存 Innodb 的数据块,可以通过下面的指令查看 Buffer Pool 的状态信息: +InnoDB 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 InnoDB 的索引块,也用来缓存 InnoDB 的数据块,可以通过下面的指令查看 Buffer Pool 的状态信息: ```mysql SHOW ENGINE INNODB STATUS\G @@ -5461,7 +5463,35 @@ MySQL 5.7.5 之前 `innodb_buffer_pool_size` 只支持在系统启动时修改 #### 其他内存 -InnoDB 管理的 Buffer Pool 中有一块内存叫 Change Buffer 用来对**增删改**操作提供缓存,参数 `innodb_change_buffer_max_size ` 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% +##### Change + +InnoDB 管理的 Buffer Pool 中有一块内存叫 Change Buffer 用来对**增删改操作**提供缓存,参数 `innodb_change_buffer_max_size ` 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% + +* 唯一索引的更新不能使用 Change Buffer,需要将数据页读入内存,判断没有冲突在写入 +* 普通索引可以使用 Change Buffer,**直接写入 Buffer 就结束**,不用校验唯一性 + +Change Buffer 并不是数据页,只是对操作的缓存,所以需要将 Change Buffer 中的操作应用到旧数据页,得到新的数据页(脏页)的过程称为 Merge + +* 触发时机:访问数据页时会触发 Merge、后台有定时线程进行 Merge、在数据库正常关闭(shutdown)的过程中也会触发 +* 工作流程:首先从磁盘读入数据页到内存(因为 Buffer Pool 中不一定存在对应的数据页),从 Change Buffer 中找到对应的操作应用到数据页,得到新的数据页即为脏页,然后写入 redo log,等待刷脏即可 + +说明:Change Buffer 中的记录,在事务提交时也会写入 redo log,所以是可以保证不丢失的 + +业务场景: + +* 对于**写多读少**的业务来说,页面在写完以后马上被访问到的概率比较小,此时 Change Buffer 的使用效果最好,常见的就是账单类、日志类的系统 + +* 一个业务的更新模式是写入后马上做查询,那么即使满足了条件,将更新先记录在 Change Buffer,但之后由于马上要访问这个数据页,会立即触发 Merge 过程,这样随机访问 IO 的次数不会减少,并且增加了 Change Buffer 的维护代价 + +补充:Change Buffer 的前身是 Insert Buffer,只能对 Insert 操作优化,后来增加了 Update/Delete 的支持,改为 Change Buffer + + + +*** + + + +##### Net Server 层针对优化**查询**的内存为 Net Buffer,内存的大小是由参数 `net_buffer_length`定义,默认 16k,实现流程: @@ -5472,6 +5502,14 @@ MySQL 采用的是边算边发的逻辑,因此对于数据量很大的查询 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查询内存优化.png) + + +*** + + + +##### Key + MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的读写速度。对于 MyISAM 表的数据块没有特别的缓存机制,完全依赖于操作系统的 IO 缓存 * key_buffer_size:该变量决定 MyISAM 索引块缓存区的大小,直接影响到 MyISAM 表的存取效率 @@ -5686,8 +5724,8 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 * 自动提交:如果没有 START TRANSACTION 显式地开始一个事务,那么**每条 SQL 语句都会被当做一个事务执行提交操作**;显式开启事务后,会在本次事务结束(提交或回滚)前暂时关闭自动提交 * 手动提交:不需要显式的开启事务,所有的 SQL 语句都在一个事务中,直到执行了提交或回滚,然后进入下一个事务 -* 隐式提交:存在一些特殊的命令,在事务中执行了这些命令会马上强制执行 COMMIT 提交事务 - * DDL 语句 (CREATE/DROP/ALTER)、LOCK TABLES 语句、LOAD DATA 导入数据语句、主从复制语句等 +* 隐式提交:存在一些特殊的命令,在事务中执行了这些命令会马上**强制执行 COMMIT 提交事务** + * **DDL 语句** (CREATE/DROP/ALTER)、LOCK TABLES 语句、LOAD DATA 导入数据语句、主从复制语句等 * 当一个事务还没提交或回滚,显式的开启一个事务会隐式的提交上一个事务 @@ -6179,14 +6217,12 @@ Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机 缓冲池的**刷脏策略**: * redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把对应的更新持久化到磁盘中 -* Buffer Pool 内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘(要避免大事务) +* Buffer Pool 内存不足,需要淘汰部分数据页(LRU 链表尾部),如果淘汰的是脏页,就要先将脏页写到磁盘(要避免大事务) * 系统空闲时,后台线程会自动进行刷脏(Flush 链表部分已经详解) * MySQL 正常关闭时,会把内存的脏页都刷新到磁盘上 - - **** @@ -6283,7 +6319,7 @@ SHOW ENGINE INNODB STATUS\G ##### 崩溃恢复 -恢复的起点:在从 redo 日志文件组的管理信息中获取最近发生 checkpoint 的信息,从 checkpoint_lsn 对应的日志文件开始恢复 +恢复的起点:在从 redo 日志文件组的管理信息中获取最近发生 checkpoint 的信息,**从 checkpoint_lsn 对应的日志文件开始恢复** 恢复的终点:扫描日志文件的 block,block 的头部记录着当前 block 使用了多少字节,填满的 block 总是 512 字节, 如果某个 block 不是 512 字节,说明该 block 就是需要恢复的最后一个 block @@ -6315,6 +6351,11 @@ MySQL 中还存在 binlog(二进制日志)也可以记录写操作并用于 * 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) * 写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元 +binlog 为什么不支持奔溃恢复? + +* binlog 记录的是语句,并不记录数据页级的数据(哪个页改了哪些地方),所以没有能力恢复数据页 +* binlog 是追加写,保存全量的日志,没有标志确定从哪个点开始的数据是已经刷盘了,而 redo log 只要在 checkpoint_lsn 后面的就是没有刷盘的 + *** @@ -6327,8 +6368,6 @@ MySQL 中还存在 binlog(二进制日志)也可以记录写操作并用于 * 在 B+ 树中定位到该记录(这个过程也被称作加锁读),如果该记录所在的页面不在 Buffer Pool 里,先将其加载进内存 -* 读取到记录后判断记录更新前后是否一样,一样的话就跳过该记录,否则进行后续步骤 - * 首先更新该记录对应的聚簇索引,更新聚簇索引记录时: * 更新记录前向 undo 页面写 undo 日志,由于这是更改页面,所以需要记录一下相应的 redo 日志 @@ -6340,6 +6379,14 @@ MySQL 中还存在 binlog(二进制日志)也可以记录写操作并用于 * 在一条更新语句执行完成后(也就是将所有待更新记录都更新完了),就会开始记录该语句对应的 binlog 日志,此时记录的 binlog 并没有刷新到硬盘上,还在内存中,在事务提交时才会统一将该事务运行过程中的所有 binlog 日志刷新到硬盘 +假设表中有字段 id 和 a,存在一条 `id = 1, a = 2` 的记录,此时执行更新语句: + +```sql +update table set a=2 where id=1; +``` + +InnoDB 会真正的去执行把值修改成 (1,2) 这个操作,先加行锁,在去更新,并不会提前判断相同就不修改了 + 参考文章:https://mp.weixin.qq.com/s/wcJ2KisSaMnfP4nH5NYaQA @@ -6382,7 +6429,7 @@ redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段 工作流程:通过 undo log 在服务器重启时将未提交的事务回滚掉。首先定位到 128 个回滚段遍历 slot,获取 undo 链表首节点页面的 undo segement header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,事务状态是活跃的就全部回滚,如果是 PREPARE 状态,就需要根据 binlog 的状态进行判断: * 如果在时刻 A 发生了崩溃(crash),由于此时 binlog 还没完成,所以需要进行回滚 -* 如果在时刻 B 发生了崩溃,redo log 和 binlog 有一个共同的数据字段叫 XID,崩溃恢复的时候,会按顺序扫描 redo log: +* 如果在时刻 B 发生了崩溃,redo log 和 binlog 有一个共**同的数据字段叫 XID**,崩溃恢复的时候,会按顺序扫描 redo log: * 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,说明 binlog 也已经记录完整,直接从 redo log 恢复数据 * 如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以恢复数据,提交事务 @@ -6391,7 +6438,7 @@ redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段 * statement 格式的 binlog,最后会有 COMMIT * row 格式的 binlog,最后会有一个 XID event -* MySQL 5.6.2 版本以后,引入了 binlog-checksum 参数用来验证 binlog 内容的正确性 +* MySQL 5.6.2 版本以后,引入了 binlog-checksum 参数用来验证 binlog 内容的正确性(可能日志中间出错) @@ -6403,9 +6450,9 @@ redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段 -#### 系统优化 +#### 刷脏优化 -系统在进行刷脏时会占用一部分系统资源,会影响系统的性能,产生系统抖动 +系统在进行刷脏时会占用一部分系统资源,会影响系统的性能,**产生系统抖动** * 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长 * 日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的 @@ -6413,12 +6460,10 @@ redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段 InnoDB 刷脏页的控制策略: * `innodb_io_capacity` 参数代表磁盘的读写能力,建议设置成磁盘的 IOPS(每秒的 IO 次数) - * 刷脏速度参考两个因素:脏页比例和 redo log 写盘速度 * 参数 `innodb_max_dirty_pages_pct` 是脏页比例上限,默认值是 75%,InnoDB 会根据当前的脏页比例,算出一个范围在 0 到 100 之间的数字 * InnoDB 每次写入的日志都有一个序号,当前写入的序号跟 checkpoint 对应的序号之间的差值,InnoDB 根据差值算出一个范围在 0 到 100 之间的数字 * 两者较大的值记为 R,执行引擎按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度 - * `innodb_flush_neighbors` 参数置为 1 代表控制刷脏时检查相邻的数据页,如果也是脏页就一起刷脏,并检查邻居的邻居,这个行为会一直蔓延直到不是脏页,在 MySQL 8.0 中该值的默认值是 0,不建议开启此功能 @@ -6731,7 +6776,7 @@ InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用 RR 隔离界别下,对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 时自动释放;对于普通 SELECT 语句,不会加任何锁(只是针对 InnoDB 层来说的,因为在 Server 层会**加 MDL 读锁**),通过 MVCC 防止并发冲突 -在事务中加的锁,并不是不需要了就释放,而是**在事务中止或提交时自动释放**,这个就是两阶段锁协议。所以一般将更新共享资源(并发高)的 SQL 放到事务的最后执行,可以让其他线程尽量的减少等待时间 +在事务中加的锁,并不是不需要了就释放,而是在事务中止或提交时自动释放,这个就是**两阶段锁协议**。所以一般将更新共享资源(并发高)的 SQL 放到事务的最后执行,可以让其他线程尽量的减少等待时间 锁的兼容性: @@ -7169,7 +7214,7 @@ MySQL 复制的优点主要包含以下三个方面: - 可以在从库上执行查询操作,从主库中更新,实现读写分离 -- 可以在从库中执行备份,以避免备份期间影响主库的服务 +- 可以在从库中执行备份,以避免备份期间影响主库的服务(备份时会加全局读锁) @@ -7195,7 +7240,7 @@ MySQL 的主从之间维持了一个长连接。主库内部有一个线程, - binlog thread:在主库事务提交时,负责把数据变更记录在二进制日志文件 binlog 中,并通知 slave 有数据更新 - I/O thread:负责从主服务器上**拉取二进制日志**,并将 binlog 日志内容依次写到 relay log 中转日志的最末端,并将新的 binlog 文件名和 offset 记录到 master-info 文件中,以便下一次读取日志时从指定 binlog 日志文件及位置开始读取新的 binlog 日志内容 -- SQL thread:监测本地 relay log 中新增了日志内容,读取中继日志并重做其中的 SQL 语句,从库在 relay-log.info 中记录当前应用中继日志的文件名和位置点以便下一次执行 +- SQL thread:监测本地 relay log 中新增了日志内容,读取中继日志并重做其中的 SQL 语句,从库在 relay-log.info 中记录当前应用中继日志的文件名和位点以便下一次执行 同步与异步: @@ -7240,8 +7285,8 @@ MySQL 的主从之间维持了一个长连接。主库内部有一个线程, 通过在从库执行 `show slave status` 命令,返回结果会显示 seconds_behind_master 表示当前从库延迟了多少秒 -- 每一个事务的 binlog 都有一个时间字段,用于记录主库上**写入**的时间 -- 从库取出当前正在**执行**的事务的时间字段,跟系统的时间进行相减,得到的就是 seconds_behind_master +- 每一个事务的 binlog 都有一个时间字段,用于记录主库上写入的时间 +- 从库取出当前正在执行的事务的时间字段,跟系统的时间进行相减,得到的就是 seconds_behind_master 主从延迟的原因: @@ -7255,7 +7300,7 @@ MySQL 的主从之间维持了一个长连接。主库内部有一个线程, * 优化 SQL,避免慢 SQL,减少批量操作 * 降低多线程大事务并发的概率,优化业务逻辑 -* 业务中大多数情况查询操作要比更新操作更多,搭建一主多从结构,让这些从库来分担读的压力 +* 业务中大多数情况查询操作要比更新操作更多,搭建**一主多从**结构,让这些从库来分担读的压力 * 尽量采用短的链路,主库和从库服务器的距离尽量要短,提升端口带宽,减少 binlog 传输的网络延时 * 实时性要求高的业务读强制走主库,从库只做备份 @@ -7272,7 +7317,7 @@ MySQL 的主从之间维持了一个长连接。主库内部有一个线程, 高并发情况下,主库的会产生大量的 binlog,在从库中有两个线程 IO Thread 和 SQL Thread 单线程执行,会导致主库延迟变大。为了改善复制延迟问题,MySQL 5.6 版本增加了并行复制功能,以采用多线程机制来促进执行 -coordinator 就是原来的 sql_thread,并行复制中它不再直接更新数据,只**负责读取中转日志和分发事务**: +coordinator 就是原来的 SQL Thread,并行复制中它不再直接更新数据,**只负责读取中转日志和分发事务**: * 线程分配完成并不是立即执行,为了防止造成更新覆盖,更新同一 DB 的两个事务必须被分发到同一个工作线程 * 同一个事务不能被拆开,必须放到同一个工作线程 @@ -7316,9 +7361,10 @@ MySQL 5.7.22 版本里,MySQL 增加了一个新的并行复制策略,基于 * WRITESET:表示的是对于每个事务涉及更新的每一行,计算出这一行的 hash 值,组成该事务的 writeset 集合,如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行(**按行并行**) + 为了唯一标识,这个 hash 表的值是通过 `库名 + 表名 + 索引名 + 值`(表示的是某一行)计算出来的 + * WRITESET_SESSION:是在 WRITESET 的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序 - 为了唯一标识,这个 hash 表的值是通过 `库名 + 表名 + 索引名 + 值`(表示的是某一行)计算出来的 MySQL 5.7.22 按行并发的优势: @@ -7545,8 +7591,6 @@ MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于 - - *** @@ -7907,8 +7951,6 @@ JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行 JDBC 是 Java 官方提供的一套规范(接口),用于帮助开发人员快速实现不同关系型数据库的连接 -使用 JDBC 需要导包 - *** @@ -7921,7 +7963,7 @@ JDBC 是 Java 官方提供的一套规范(接口),用于帮助开发人员 DriverManager:驱动管理对象 -* 注册驱动 +* 注册驱动: * 注册给定的驱动:`public static void registerDriver(Driver driver)` * 代码实现语法:`Class.forName("com.mysql.jdbc.Driver)` @@ -7942,9 +7984,9 @@ DriverManager:驱动管理对象 * jar 包中 META-INF 目录下存在一个 java.sql.Driver 配置文件,文件中指定了 com.mysql.jdbc.Driver -* 获取数据库连接并返回连接对象 +* 获取数据库连接并返回连接对象: - `public static Connection getConnection(String url, String user, String password)` + 方法:`public static Connection getConnection(String url, String user, String password)` * url:指定连接的路径。语法:`jdbc:mysql://ip地址(域名):端口号/数据库名称` * user:用户名 diff --git a/Frame.md b/Frame.md index 6f82fd0..ea98a80 100644 --- a/Frame.md +++ b/Frame.md @@ -4319,12 +4319,14 @@ RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交 2. 补偿机制:用于解决消息 Commit 或者 Rollback 发生超时或者失败的情况,比如出现网络问题 - * Broker 服务端通过对比 Half 消息和 Op 消息,对未确定状态的消息推进 CheckPoint(记录哪些事务消息的状态是确定的) + * Broker 服务端通过**对比 Half 消息和 Op 消息**,对未确定状态的消息推进 CheckPoint * 没有 Commit/Rollback 的事务消息,服务端根据根据半消息的生产者组,到 ProducerManager 中获取生产者(同一个 Group 的 Producer)的会话通道,发起一次回查(**单向请求**) * Producer 收到回查消息,检查事务消息状态表内对应的本地事务的状态 * 根据本地事务状态,重新 Commit 或者 Rollback - 注意:RocketMQ 并不会无休止的进行事务状态回查,默认回查 15 次,如果 15 次回查还是无法得知事务状态,则默认回滚该消息 + RocketMQ 并不会无休止的进行事务状态回查,最大回查 15 次,如果 15 次回查还是无法得知事务状态,则默认回滚该消息, + + 回查服务:`TransactionalMessageCheckService#run` @@ -4338,7 +4340,7 @@ RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交 事务消息相对普通消息最大的特点就是**一阶段发送的消息对用户是不可见的**,因为对于 Half 消息,会备份原消息的主题与消息消费队列,然后改变主题为 RMQ_SYS_TRANS_HALF_TOPIC,由于消费组未订阅该主题,故消费端无法消费 Half 类型的消息 -RocketMQ 会开启一个定时任务,从 Topic 为 RMQ_SYS_TRANS_HALF_TOPIC 中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息 +RocketMQ 会开启一个**定时任务**,从 Topic 为 RMQ_SYS_TRANS_HALF_TOPIC 中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息 RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 Topic 和 Queue 等属性进行替换,同时将原来的 Topic 和 Queue 信息存储到**消息的属性**中,因为消息的主题被替换,所以消息不会转发到该原主题的消息消费队列,消费者无法感知消息的存在,不会消费 @@ -4356,7 +4358,7 @@ RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 T * 如果是 Rollback 则需要撤销一阶段的消息,因为消息本就不可见,所以并**不需要真正撤销消息**(实际上 RocketMQ 也无法去删除一条消息,因为是顺序写文件的)。RocketMQ 为了区分这条消息没有确定状态的消息,采用 Op 消息标识已经确定状态的事务消息(Commit 或者 Rollback) -**事务消息无论是 Commit 或者 Rollback 都会记录一个 Op 操作**,两者的区别是 Commit 相对于 Rollback 在写入 Op 消息前创建 Half 消息的索引。如果一条事务消息没有对应的 Op 消息,说明这个事务的状态还无法确定(可能是二阶段失败了) +**事务消息无论是 Commit 或者 Rollback 都会记录一个 Op 操作**,两者的区别是 Commit 相对于 Rollback 在写入 Op 消息前将原消息的主题和队列恢复。如果一条事务消息没有对应的 Op 消息,说明这个事务的状态还无法确定(可能是二阶段失败了) RocketMQ 将 Op 消息写入到全局一个特定的 Topic 中,通过源码中的方法 `TransactionalMessageUtil.buildOpTopic()`,这个主题是一个内部的 Topic(像 Half 消息的 Topic 一样),不会被用户消费。Op 消息的内容为对应的 Half 消息的存储的 Offset,这样**通过 Op 消息能索引到 Half 消息** @@ -8825,7 +8827,7 @@ TransactionMQProducer 类发送事务消息时使用 * `MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true")`:**设置事务标志** - * `sendResult = this.send(msg)`:发送消息 + * `sendResult = this.send(msg)`:发送消息,同步发送 * `switch (sendResult.getSendStatus())`:**判断发送消息的结果状态** @@ -8850,44 +8852,6 @@ TransactionMQProducer 类发送事务消息时使用 -##### 回查处理 - -ClientRemotingProcessor 用于处理到服务端的请求,创建 MQClientAPIImpl 时将该处理器注册到 Netty 中,`processRequest()` 方法根据请求的命令码,进行不同的处理,事务回查的处理命令码为 `CHECK_TRANSACTION_STATE` - -成员方法: - -* checkTransactionState():检查事务状态 - - ```java - public RemotingCommand checkTransactionState(ChannelHandlerContext ctx, RemotingCommand request) - ``` - - * `final CheckTransactionStateRequestHeader requestHeader`:解析出请求头对象 - * `final MessageExt messageExt`:从请求 body 中解析出服务器回查的事务消息 - * `String transactionId`:提取 UNIQ_KEY 字段属性值赋值给事务 ID - * `final String group`:提取生产者组名 - * `MQProducerInner producer = this...selectProducer(group)`:根据生产者组获取生产者对象 - * `String addr = RemotingHelper.parseChannelRemoteAddr()`:解析出要回查的 Broker 服务器的地址 - * `producer.checkTransactionState(addr, messageExt, requestHeader)`:生产者的事务回查 - * `Runnable request = new Runnable()`:**创建回查事务状态任务对象** - * 获取生产者的 TransactionCheckListener 和 TransactionListener,选择一个不为 null 的监听器进行事务状态回查 - * `this.processTransactionState()`:处理回查状态 - * `EndTransactionRequestHeader thisHeader`:构建 EndTransactionRequestHeader 对象 - * `DefaultMQProducerImpl...endTransactionOneway()`:向 Broker 发起结束事务单向请求,**二阶段提交** - * `this.checkExecutor.submit(request)`:提交到线程池运行 - - - -参考图:https://www.processon.com/view/link/61c8257e0e3e7474fb9dcbc0 - -参考视频:https://space.bilibili.com/457326371 - - - -*** - - - ##### 接受消息 SendMessageProcessor 是服务端处理客户端发送来的消息的处理器,`processRequest()` 方法处理请求 @@ -8946,14 +8910,14 @@ SendMessageProcessor 是服务端处理客户端发送来的消息的处理器 return store.asyncPutMessage(parseHalfMessageInner(messageInner)); } ``` - + TransactionalMessageBridge#parseHalfMessageInner: - + * `MessageAccessor.putProperty(...)`:**将消息的原主题和队列 ID 放入消息的属性中** * `msgInner.setSysFlag(...)`:消息设置为非事务状态 * `msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic())`:**消息主题设置为半消息主题** * `msgInner.setQueueId(0)`:**队列 ID 设置为 0** - + * `else`:普通消息存储 @@ -8962,9 +8926,49 @@ SendMessageProcessor 是服务端处理客户端发送来的消息的处理器 +##### 回查处理 + +ClientRemotingProcessor 是客户端用于处理请求,创建 MQClientAPIImpl 时将该处理器注册到 Netty 中,`processRequest()` 方法根据请求的命令码,进行不同的处理,事务回查的处理命令码为 `CHECK_TRANSACTION_STATE` + +Broker 端有定时任务发送回查请求 + +成员方法: + +* checkTransactionState():检查事务状态 + + ```java + public RemotingCommand checkTransactionState(ChannelHandlerContext ctx, RemotingCommand request) + ``` + + * `final CheckTransactionStateRequestHeader requestHeader`:解析出请求头对象 + * `final MessageExt messageExt`:从请求 body 中解析出服务器回查的事务消息 + * `String transactionId`:提取 UNIQ_KEY 字段属性值赋值给事务 ID + * `final String group`:提取生产者组名 + * `MQProducerInner producer = this...selectProducer(group)`:根据生产者组获取生产者对象 + * `String addr = RemotingHelper.parseChannelRemoteAddr()`:解析出要回查的 Broker 服务器的地址 + * `producer.checkTransactionState(addr, messageExt, requestHeader)`:生产者的事务回查 + * `Runnable request = new Runnable()`:**创建回查事务状态任务对象** + * 获取生产者的 TransactionCheckListener 和 TransactionListener,选择一个不为 null 的监听器进行事务状态回查 + * `this.processTransactionState()`:处理回查状态 + * `EndTransactionRequestHeader thisHeader`:构建 EndTransactionRequestHeader 对象 + * `DefaultMQProducerImpl...endTransactionOneway()`:向 Broker 发起结束事务单向请求,**二阶段提交** + * `this.checkExecutor.submit(request)`:提交到线程池运行 + + + +参考图:https://www.processon.com/view/link/61c8257e0e3e7474fb9dcbc0 + +参考视频:https://space.bilibili.com/457326371 + + + +*** + + + ##### 事务提交 -EndTransactionProcessor 类用来处理客户端发来的提交或者回滚请求 +EndTransactionProcessor 类是服务端用来处理客户端发来的提交或者回滚请求 * processRequest():处理请求 @@ -8978,12 +8982,12 @@ EndTransactionProcessor 类用来处理客户端发来的提交或者回滚请 `result = this.brokerController...commitMessage(requestHeader)`:根据 commitLogOffset 提取出 halfMsg 消息 - * `MessageExtBrokerInner msgInner`:根据 result 克隆出一条新消息 + `MessageExtBrokerInner msgInner`:根据 result 克隆出一条新消息 - `msgInner.setTopic(msgExt.getUserProperty(...))`:**设置回原主题** + * `msgInner.setTopic(msgExt.getUserProperty(...))`:**设置回原主题** - * `msgInner.setQueueId(Integer.parseInt(msgExt.getUserProperty(..)))`:**设置回原队列 ID** - * `MessageAccessor.clearProperty()`:清理上面的两个属性 + * `msgInner.setQueueId(Integer.parseInt(msgExt.getUserProperty(..)))`:**设置回原队列 ID** + * `MessageAccessor.clearProperty()`:清理上面的两个属性 `MessageAccessor.clearProperty(msgInner, ...)`:**清理事务属性** @@ -9549,9 +9553,10 @@ PullAPIWrapper 类封装了拉取消息的 API * `return this.pullMessageSync(...)`:此处是**异步调用,处理结果放入 ResponseFuture 中**,参考服务端小节的处理器类 `NettyServerHandler#processMessageReceived` 方法 * `RemotingCommand response = responseFuture.getResponseCommand()`:获取服务器端响应数据 response - * `PullResult pullResult`:从 response 内提取出来拉消息结果对象,将响应头 PullMessageResponseHeader 对象中信息**填充到 PullResult 中**,列出两个重要的字段: - * `private Long suggestWhichBrokerId`:服务端建议客户端下次 Pull 时选择的 BrokerID - * `private Long nextBeginOffset`:客户端下次 Pull 时使用的 offset 信息 + + * `PullResult pullResult`:从 response 内提取出来拉消息结果对象,将响应头 PullMessageResponseHeader 对象中信息**填充到 PullResult 中**,列出两个重要的字段: + * `private Long suggestWhichBrokerId`:服务端建议客户端下次 Pull 时选择的 BrokerID + * `private Long nextBeginOffset`:客户端下次 Pull 时使用的 offset 信息 * `pullCallback.onSuccess(pullResult)`:将 PullResult 交给拉消息结果处理回调对象,调用 onSuccess 方法 diff --git a/Java.md b/Java.md index 3828bec..c2fa3c0 100644 --- a/Java.md +++ b/Java.md @@ -59,7 +59,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, **int:** - int 数据类型是 32 位 4 字节、有符号的以二进制补码表示的整数 -- 最小值是 **-2,147,483,648(-2^31)** +- 最小值是 **-2,147,483,648(-2^31 -)** - 最大值是 **2,147,483,647(2^31 - 1)** - 一般地整型变量默认为 int 类型 - 默认值是 **`0`** @@ -5000,8 +5000,11 @@ HashMap 继承关系如下图所示: * hash():HashMap 是支持 Key 为空的;HashTable 是直接用 Key 来获取 HashCode,key 为空会抛异常 * &(按位与运算):相同的二进制数位上,都是 1 的时候,结果为 1,否则为零 + * ^(按位异或运算):相同的二进制数位上,数字相同,结果为 0,不同为 1,**不进位加法** + 0 1 相互做 & | ^ 运算,结果出现 0 和 1 的数量分别是 3:1、1:3、1:1,所以异或是最平均的 + ```java static final int hash(Object key) { int h; diff --git a/Prog.md b/Prog.md index 4800568..690de91 100644 --- a/Prog.md +++ b/Prog.md @@ -616,13 +616,15 @@ t.start(); 不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁: -| 方法 | 功能 | -| --------------------------- | ------------------------ | -| public final void stop() | 停止线程运行 | -| public final void suspend() | **挂起(暂停)线程运行** | -| public final void resume() | 恢复线程运行 | +* `public final void stop()`:停止线程运行 + 废弃原因:方法粗暴,除非可能执行 finally 代码块以及释放 synchronized 外,线程将直接被终止,如果线程持有 JUC 的互斥锁可能导致锁来不及释放,造成其他线程永远等待的局面 +* `public final void suspend()`:**挂起(暂停)线程运行** + + 废弃原因:如果目标线程在暂停时对系统资源持有锁,则在目标线程恢复之前没有线程可以访问该资源,如果**恢复目标线程的线程**在调用 resume 之前尝试锁定此共享资源,则会导致死锁 + +* `public final void resume()`:恢复线程运行 @@ -931,7 +933,7 @@ Monitor 被翻译为监视器或管程 * 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner,**obj 对象的 Mark Word 指向 Monitor**,把**对象原有的 MarkWord 存入线程栈中的锁记录**中(轻量级锁部分详解) * 在 Thread-2 上锁的过程,Thread-3、Thread-4、Thread-5 也执行 synchronized(obj),就会进入 EntryList BLOCKED(双向链表) -* Thread-2 执行完同步代码块的内容,根据 obj 对象头中 Monitor 地址寻找,设置 Owner 为空,把线程栈的锁记录中的对象头的值设置到 MarkWord +* Thread-2 执行完同步代码块的内容,根据 obj 对象头中 Monitor 地址寻找,设置 Owner 为空,把线程栈的锁记录中的对象头的值设置回 MarkWord * 唤醒 EntryList 中等待的线程来竞争锁,竞争是**非公平的**,如果这时有新的线程想要获取锁,可能直接就抢占到了,阻塞队列的线程就会继续阻塞 * WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify 机制) @@ -1034,13 +1036,13 @@ LocalVariableTable: 一个对象创建时: -* 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,thread、epoch、age 都为 0 +* 如果开启了偏向锁(默认开启),那么对象创建后,MarkWord 值为 0x05 即最后 3 位为 101,thread、epoch、age 都为 0 * 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 `-XX:BiasedLockingStartupDelay=0` 来禁用延迟 JDK 8 延迟 4s 开启偏向锁原因:在刚开始执行代码时,会有好多线程来抢锁,如果开偏向锁效率反而降低 -* 如果禁用了偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,**第一次用到 hashcode 时才会赋值**,添加 VM 参数 `-XX:-UseBiasedLocking` 禁用偏向锁 +* 如果禁用了偏向锁,那么对象创建后,MarkWord 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,**第一次用到 hashcode 时才会赋值**,添加 VM 参数 `-XX:-UseBiasedLocking` 禁用偏向锁 撤销偏向锁的状态: @@ -1269,7 +1271,7 @@ public class SpinLock { #### 多把锁 -多把不相干的锁:一间大屋子有两个功能:睡觉、学习,互不相干。现在一人要学习,一人要睡觉,如果只用一间屋子(一个对象锁)的话,那么并发度很低 +多把不相干的锁:一间大屋子有两个功能睡觉、学习,互不相干。现在一人要学习,一人要睡觉,如果只用一间屋子(一个对象锁)的话,那么并发度很低 将锁的粒度细分: @@ -1437,7 +1439,7 @@ class HoldLockThread implements Runnable { at thread.TestDeadLock$$Lambda$1/495053715 ``` -* linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 `top -Hp 进程id` 来定位是哪个线程,最后再用 jstack 的输出来看各个线程栈 +* Linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 `top -Hp 进程id` 来定位是哪个线程,最后再用 jstack 的输出来看各个线程栈 * 避免死锁:避免死锁要注意加锁顺序 diff --git a/Web.md b/Web.md index 0cf91a7..59c8d93 100644 --- a/Web.md +++ b/Web.md @@ -2146,7 +2146,8 @@ URL 和 URI * HTTP/0.9 仅支持 GET 请求,不支持请求头 * HTTP/1.0 默认短连接(一次请求建议一次 TCP 连接,请求完就断开),支持 GET、POST、 HEAD 请求 * HTTP/1.1 默认长连接(一次 TCP 连接可以多次请求);支持 PUT、DELETE、PATCH 等六种请求;增加 HOST 头,支持虚拟主机;支持**断点续传**功能 -* HTTP/2.0 多路复用,降低开销(一次 TCP 连接可以处理多个请求);服务器主动推送(相关资源一个请求全部推送);解析基于二进制,解析错误少,更高效(HTTP/1.X 解析基于文本);报头压缩,降低开销。 +* HTTP/2.0 多路复用,降低开销(一次 TCP 连接可以处理多个请求);服务器主动推送(相关资源一个请求全部推送);解析基于二进制,解析错误少,更高效(HTTP/1.X 解析基于文本);报头压缩,降低开销 +* HTTP/3.0 QUIC (Quick UDP Internet Connections),快速 UDP 互联网连接,基于 UDP 协议 HTTP 1.0 和 HTTP 1.1 的主要区别: @@ -2279,7 +2280,7 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 POST 请求:后一个请求不会把第一个请求覆盖掉(非幂等),所以 POST 用来创建资源 - PATCH方法 是新引入的,是对 PUT 方法的补充,用来对已知资源进行**局部更新** + PATCH 方法 是新引入的,是对 PUT 方法的补充,用来对已知资源进行**局部更新** From 9a18249bdb42c14b2379b1854661a8ddd5f564c8 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 22 Mar 2022 00:42:57 +0800 Subject: [PATCH 07/51] Update Java Note --- DB.md | 499 +++++++++++++++++++++++++++++++++++++------------------- Java.md | 59 ++++--- 2 files changed, 368 insertions(+), 190 deletions(-) diff --git a/DB.md b/DB.md index 0d9b44e..514b51e 100644 --- a/DB.md +++ b/DB.md @@ -380,13 +380,6 @@ mysqlshow -uroot -p1234 test book --count 连接建立 TCP 以后需要做**权限验证**,验证成功后可以进行执行 SQL。如果这时管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限,只有再新建的连接才会使用新的权限设置 -客户端如果长时间没有操作,连接器就会自动断开,时间是由参数 wait_timeout 控制的,默认值是 8 小时。如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒:`Lost connection to MySQL server during query` - -数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接;短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。为了减少连接的创建,推荐使用长连接,但是过多的长连接会造成 OOM,解决方案: - -* 定期断开长连接,使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。 -* MySQL 5.7 版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源,这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态 - MySQL 服务器可以同时和多个客户端进行交互,所以要保证每个连接会话的隔离性(事务机制部分详解) 整体的执行流程: @@ -401,7 +394,19 @@ MySQL 服务器可以同时和多个客户端进行交互,所以要保证每 ##### 连接状态 -首先连接到数据库上,这时连接器发挥作用,连接完成后如果没有后续的动作,这个连接就处于空闲状态,通过指令查看连接状态 +客户端如果长时间没有操作,连接器就会自动断开,时间是由参数 wait_timeout 控制的,默认值是 8 小时。如果在连接被断开之后,客户端**再次发送请求**的话,就会收到一个错误提醒:`Lost connection to MySQL server during query` + +数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接;短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个 + +为了减少连接的创建,推荐使用长连接,但是**过多的长连接会造成 OOM**,解决方案: + +* 定期断开长连接,使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连 + + ```mysql + kill connection id + ``` + +* MySQL 5.7 版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源,这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态 SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 SQL 的执行情况,其中的 Command 列显示为 Sleep 的这一行,就表示现在系统里面有一个空闲连接 @@ -674,98 +679,6 @@ Server 层和存储引擎层的交互是**以记录为单位的**,存储引擎 -**** - - - -### 数据空间 - -#### 数据存储 - -系统表空间是用来放系统信息的,比如数据字典什么的,对应的磁盘文件是 ibdata,数据表空间是一个个的表数据文件,对应的磁盘文件就是表名.ibd - -表数据既可以存在共享表空间里,也可以是单独的文件,这个行为是由参数 innodb_file_per_table 控制的: - -* OFF:表示表的数据放在系统共享表空间,也就是跟数据字典放在一起 -* ON :表示每个 InnoDB 表数据存储在一个以 .ibd 为后缀的文件中(默认) - -一个表单独存储为一个文件更容易管理,在不需要这个表时通过 drop table 命令,系统就会直接删除这个文件;如果是放在共享表空间中,即使表删掉了,空间也是不会回收的 - - - -说明:本章节知识是抄录自《MySQL 实战 45 讲》,作者目前没有更深的理解,后续有了新的认知后会更新知识 - - - -*** - - - -#### 数据删除 - -MySQL 的数据删除就是移除掉某个记录后,该位置就被标记为**可复用**,如果有符合范围条件的数据可以插入到这里。符合范围条件的意思是假设删除记录 R4,之后要再插入一个 ID 在 300 和 600 之间的记录时,就会复用这个位置 - - - -InnoDB 的数据是按页存储的如果删掉了一个数据页上的所有记录,整个数据页就可以被复用了,如果相邻的两个数据页利用率都很小,系统就会把这两个页上的数据合到其中一个页上,另外一个数据页就被标记为可复用 - -删除命令其实只是把记录的位置,或者**数据页标记为了可复用,但磁盘文件的大小是不会变的**,这些可以复用还没有被使用的空间,看起来就像是空洞,造成数据库的稀疏,因此需要进行紧凑处理 - - - -*** - - - -#### 重建数据 - -重建表就是按照主键 ID 递增的顺序,把数据一行一行地从旧表中读出来再插入到新表中,让数据更加紧凑。重建表时 MySQL 会自动完成转存数据、交换表名、删除旧表的操作,线上操作会阻塞大量的线程增删改查的操作 - -重建命令: - -```sql -ALTER TABLE A ENGINE=InnoDB -``` - -工作流程:新建临时表 tmp_table B(在 Server 层创建的),把表 A 中的数据导入到表 B 中,操作完成后用表 B 替换表 A,完成重建 - -重建表的步骤需要 DDL 不是 Online 的,因为在导入数据的过程有新的数据要写入到表 A 的话,就会造成数据丢失 - -MySQL 5.6 版本开始引入的 **Online DDL**,重建表的命令默认执行此步骤: - -* 建立一个临时文件 tmp_file(InnoDB 创建),扫描表 A 主键的所有数据页 -* 用数据页中表 A 的记录生成 B+ 树,存储到临时文件中 -* 生成临时文件的过程中,将所有对 A 的操作记录在一个日志文件(row log)中,对应的是图中 state2 的状态 -* 临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表 A 相同的数据文件,对应的就是图中 state3 -* 用临时文件替换表 A 的数据文件 - - - -Online DDL 操作会先获取 MDL 写锁,再退化成 MDL 读锁。但 MDL 写锁持有时间比较短,所以可以称为 Online; 而 MDL 读锁,不阻止数据增删查改,但会阻止其它线程修改表结构(可以对比 `ANALYZE TABLE t` 命令) - -问题:重建表可以收缩表空间,但是执行指令后整体占用空间增大 - -原因:在重建表后 InnoDB 不会把整张表占满,每个页留了 1/16 给后续的更新使用。表在未整理之前页已经占用 15/16 以上,收缩之后需要保持数据占用空间在 15/16,所以文件占用空间更大才能保持 - -注意:临时文件也要占用空间,如果空间不足会重建失败 - - - -**** - - - -#### 原地置换 - -DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临时文件 tmp_file 是 InnoDB 在内部创建出来的,整个 DDL 过程都在 InnoDB 内部完成,对于 Server 层来说,没有把数据挪动到临时表,是一个原地操作,这就是 inplace - -两者的关系: - -* DDL 过程如果是 Online 的,就一定是 inplace 的 -* inplace 的 DDL,有可能不是 Online 的,截止到 MySQL 8.0,全文索引(FULLTEXT)和空间索引(SPATIAL)属于这种情况 - - - *** @@ -2053,7 +1966,7 @@ INSERT INTO stu_course VALUES (NULL,1,1),(NULL,1,2),(NULL,2,1),(NULL,2,2); 连接查询的是两张表有交集的部分数据,如果结果集中的每条记录都是两个表相互匹配的组合,则称这样的结果集为笛卡尔积 -查询原理:两张表分为驱动表和被驱动表,首先查询驱动表得到数据集,然后根据数据集中的每一条记录再**分别**到被驱动表中查找匹配,所以驱动表只需要访问一次,被驱动表要访问多次 +查询原理:两张表分为驱动表和被驱动表,首先查询驱动表得到数据集,然后根据数据集中的每一条记录的**关联字段再分别**到被驱动表中查找匹配,所以驱动表只需要访问一次,被驱动表要访问多次 MySQL 将查询驱动表后得到的记录成为驱动表的扇出,连接查询的成本:单次访问驱动表的成本 + 扇出值 * 单次访问被驱动表的成本,优化器会选择成本最小的表连接顺序(确定谁是驱动表,谁是被驱动表)生成执行计划,进行连接查询,优化方式: @@ -2262,7 +2175,7 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 系统变量 tmp_table_size 或者 max_heap_table_size 为表的最值 * 小于系统变量时,内存中可以保存,会为建立基于内存的 MEMORY 存储引擎的临时表,并建立哈希索引 -* 大于任意一个系统变量时,物化表会使用基于磁盘的存储引擎来保存结果集中的记录,索引类型为 B+ 树 +* 大于任意一个系统变量时,物化表会使用基于磁盘的 InnoDB 存储引擎来保存结果集中的记录,索引类型为 B+ 树 物化后,嵌套查询就相当于外层查询的表和物化表进行内连接查询,然后经过优化器选择成本最小的表连接顺序执行查询 @@ -4161,7 +4074,7 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 * 需要存储引擎将索引中的数据与条件进行判断(所以**条件列必须都在同一个索引中**),所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM * 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 -* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了,索引下推的目的减少 IO 次数也就失去了意义 +* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了,索引下推的目的减少回表的 IO 次数也就失去了意义 工作过程:用户表 user,(name, age) 是联合索引 @@ -4256,7 +4169,7 @@ CREATE INDEX idx_area ON table_name(area(7)); 先将从不同索引中扫描到的记录的主键值进行排序,再按照 Union 索引合并的方式进行查询 - +索引合并算法的效率并不好,通过将其中的一个索引改成联合索引会优化效率 @@ -4728,7 +4641,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引5.png) -* 在索引列上进行**运算操作**, 索引将失效: +* 在索引列上**函数或者运算(+ - 数值)操作**, 索引将失效:会破坏索引值的有序性 ```mysql EXPLAIN SELECT * FROM tb_seller WHERE SUBSTRING(name,3,2) = '科技'; @@ -4736,16 +4649,22 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引6.png) -* **字符串不加单引号**,造成索引失效: +* **字符串不加单引号**,造成索引失效:隐式类型转换,当字符串和数字比较时会**把字符串转化为数字** - 在查询时,没有对字符串加单引号,MySQL 的查询优化器,会自动的进行类型转换,造成索引失效 + 在查询时,没有对字符串加单引号,查询优化器会调用 CAST 函数将 status 转换为 int 进行比较,造成索引失效 ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status=1; + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status = 1; ``` ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引7.png) + 如果 status 是 int 类型,SQL 为 `SELECT * FROM tb_seller WHERE status = '1' ` 并不会造成索引失效,因为会将 `'1'` 转换为 `1`,并**不会对索引列产生操作** + +* 多表连接查询时,如果两张表的**字符集不同**,会造成索引失效,因为会进行类型转换 + + 解决方法:CONVERT 函数是加在输入参数上、修改表的字符集 + * **用 OR 分割条件,索引失效**,导致全表查询: OR 前的条件中的列有索引而后面的列中没有索引或 OR 前后两个列是同一个复合索引,都造成索引失效 @@ -4825,6 +4744,8 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 EXPLAIN SELECT * FROM tb_seller WHERE sellerId NOT IN ('alibaba','huawei'); ``` +* [MySQL 实战 45 讲](https://time.geekbang.org/column/article/74687)该章节最后提出了一种场景,获取到数据以后 Server 层还会做判断 + *** @@ -5045,26 +4966,33 @@ CREATE INDEX idx_emp_age_salary ON emp(age,salary); ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序3.png) 尽量减少额外的排序,通过索引直接返回有序数据。**需要满足 Order by 使用相同的索引、Order By 的顺序和索引顺序相同、Order by 的字段都是升序或都是降序**,否则需要额外的操作,就会出现 FileSort + +* ORDER BY RAND() 命令用来进行随机排序,会使用了临时内存表,临时内存表排序的时使用 rowid 排序方法 + +优化方式:创建合适的索引能够减少 Filesort 的出现,但是某些情况下条件限制不能让 Filesort 消失,就要加快 Filesort 的排序操作 -优化:通过创建合适的索引能够减少 Filesort 的出现,但是某些情况下条件限制不能让 Filesort 消失,就要加快 Filesort 的排序操作 +内存临时表,MySQL 有两种 Filesort 排序算法: -对于 Filesort , MySQL 有两种排序算法: +* rowid 排序:首先根据条件(回表)取出排序字段和信息,然后在**排序区 sort buffer(Server 层)**中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后再根据行指针**回表读取记录**,该操作可能会导致大量随机 I/O 操作 + + 说明:对于临时内存表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,不会导致多访问磁盘,优先选择该方式 -* rowid 排序:MySQL4.1 之前,使用该方式排序。首先根据条件(回表)取出排序字段和信息,然后在排序区 sort buffer 中排序,如果 sort buffer 不够,则在临时表 temporary table 中存储排序结果。完成排序后再根据行指针**回表读取记录**,该操作可能会导致大量随机 I/O 操作 * 全字段排序:一次性取出满足条件的所有数据,需要回表,然后在排序区 sort buffer 中排序后直接输出结果集。排序时内存开销较大,但是排序效率比两次扫描算法高 -MySQL 通过比较系统变量 max_length_for_sort_data 的大小和 Query 语句取出的字段的大小,来判定使用哪种排序算法。如果前者大,则说明 sort buffer 空间足够,使用第二种优化之后的算法,否则使用第一种。 +具体的选择方式: -可以适当提高 sort_buffer_size 和 max_length_for_sort_data 系统变量,来增大排序区的大小,提高排序的效率 +* MySQL 通过比较系统变量 max_length_for_sort_data 的大小和 Query 语句取出的字段的大小,来判定使用哪种排序算法。如果前者大,则说明 sort buffer 空间足够,使用第二种优化之后的算法,否则使用第一种。 -```mysql -SET @@max_length_for_sort_data = 10000; -- 设置全局变量 -SET max_length_for_sort_data = 10240; -- 设置会话变量 -SHOW VARIABLES LIKE 'max_length_for_sort_data'; -- 默认1024 -SHOW VARIABLES LIKE 'sort_buffer_size'; -- 默认262114 -``` +* 可以适当提高 sort_buffer_size 和 max_length_for_sort_data 系统变量,来增大排序区的大小,提高排序的效率 -注意:sort buffer 在 Server 层 + ```mysql + SET @@max_length_for_sort_data = 10000; -- 设置全局变量 + SET max_length_for_sort_data = 10240; -- 设置会话变量 + SHOW VARIABLES LIKE 'max_length_for_sort_data'; -- 默认1024 + SHOW VARIABLES LIKE 'sort_buffer_size'; -- 默认262114 + ``` + +磁盘临时表:排序使用优先队列(堆)的方式 @@ -5085,7 +5013,7 @@ GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序1.png) - Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序和分组查询 + Using temporary:表示 MySQL 需要使用临时表(不是 sort buffer)来存储结果集,常见于排序和分组查询 * 查询包含 GROUP BY 但是用户想要避免排序结果的消耗, 则可以执行 ORDER BY NULL 禁止排序: @@ -5540,6 +5468,102 @@ MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的 +*** + + + + + +### 存储优化 + +#### 数据存储 + +系统表空间是用来放系统信息的,比如数据字典什么的,对应的磁盘文件是 ibdata,数据表空间是一个个的表数据文件,对应的磁盘文件就是表名.ibd + +表数据既可以存在共享表空间里,也可以是单独的文件,这个行为是由参数 innodb_file_per_table 控制的: + +* OFF:表示表的数据放在系统共享表空间,也就是跟数据字典放在一起 +* ON :表示每个 InnoDB 表数据存储在一个以 .ibd 为后缀的文件中(默认) + +一个表单独存储为一个文件更容易管理,在不需要这个表时通过 drop table 命令,系统就会直接删除这个文件;如果是放在共享表空间中,即使表删掉了,空间也是不会回收的 + + + +说明:本章节知识是抄录自《MySQL 实战 45 讲》,作者目前没有更深的理解,后续有了新的认知后会更新知识 + + + +*** + + + +#### 数据删除 + +MySQL 的数据删除就是移除掉某个记录后,该位置就被标记为**可复用**,如果有符合范围条件的数据可以插入到这里。符合范围条件的意思是假设删除记录 R4,之后要再插入一个 ID 在 300 和 600 之间的记录时,就会复用这个位置 + + + +InnoDB 的数据是按页存储的如果删掉了一个数据页上的所有记录,整个数据页就可以被复用了,如果相邻的两个数据页利用率都很小,系统就会把这两个页上的数据合到其中一个页上,另外一个数据页就被标记为可复用 + +删除命令其实只是把记录的位置,或者**数据页标记为了可复用,但磁盘文件的大小是不会变的**,这些可以复用还没有被使用的空间,看起来就像是空洞,造成数据库的稀疏,因此需要进行紧凑处理 + + + +*** + + + +#### 重建数据 + +重建表就是按照主键 ID 递增的顺序,把数据一行一行地从旧表中读出来再插入到新表中,让数据更加紧凑。重建表时 MySQL 会自动完成转存数据、交换表名、删除旧表的操作,线上操作会阻塞大量的线程增删改查的操作 + +重建命令: + +```sql +ALTER TABLE A ENGINE=InnoDB +``` + +工作流程:新建临时表 tmp_table B(在 Server 层创建的),把表 A 中的数据导入到表 B 中,操作完成后用表 B 替换表 A,完成重建 + +重建表的步骤需要 DDL 不是 Online 的,因为在导入数据的过程有新的数据要写入到表 A 的话,就会造成数据丢失 + +MySQL 5.6 版本开始引入的 **Online DDL**,重建表的命令默认执行此步骤: + +* 建立一个临时文件 tmp_file(InnoDB 创建),扫描表 A 主键的所有数据页 +* 用数据页中表 A 的记录生成 B+ 树,存储到临时文件中 +* 生成临时文件的过程中,将所有对 A 的操作记录在一个日志文件(row log)中,对应的是图中 state2 的状态 +* 临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表 A 相同的数据文件,对应的就是图中 state3 +* 用临时文件替换表 A 的数据文件 + + + +Online DDL 操作会先获取 MDL 写锁,再退化成 MDL 读锁。但 MDL 写锁持有时间比较短,所以可以称为 Online; 而 MDL 读锁,不阻止数据增删查改,但会阻止其它线程修改表结构(可以对比 `ANALYZE TABLE t` 命令) + +问题:重建表可以收缩表空间,但是执行指令后整体占用空间增大 + +原因:在重建表后 InnoDB 不会把整张表占满,每个页留了 1/16 给后续的更新使用。表在未整理之前页已经占用 15/16 以上,收缩之后需要保持数据占用空间在 15/16,所以文件占用空间更大才能保持 + +注意:临时文件也要占用空间,如果空间不足会重建失败 + + + +**** + + + +#### 原地置换 + +DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临时文件 tmp_file 是 InnoDB 在内部创建出来的,整个 DDL 过程都在 InnoDB 内部完成,对于 Server 层来说,没有把数据挪动到临时表,是一个原地操作,这就是 inplace + +两者的关系: + +* DDL 过程如果是 Online 的,就一定是 inplace 的 +* inplace 的 DDL,有可能不是 Online 的,截止到 MySQL 8.0,全文索引(FULLTEXT)和空间索引(SPATIAL)属于这种情况 + + + + + *** @@ -5550,9 +5574,9 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 * max_connections:控制允许连接到 MySQL 数据库的最大连接数,默认值是 151 - 如果状态变量 connection_errors_max_connections 不为零,并且一直增长,则说明不断有连接请求因数据库连接数已达到允许最大值而失败,这时可以考虑增大max_connections 的值 + 如果状态变量 connection_errors_max_connections 不为零,并且一直增长,则说明不断有连接请求因数据库连接数已达到允许最大值而失败,这时可以考虑增大 max_connections 的值 - Mysql 最大可支持的连接数取决于很多因素,包括操作系统平台的线程库的质量、内存大小、每个连接的负荷、CPU的处理速度、期望的响应时间等。在 Linux 平台下,性能好的服务器,可以支持 500-1000 个连接,需要根据服务器性能进行评估设定 + MySQL 最大可支持的连接数取决于很多因素,包括操作系统平台的线程库的质量、内存大小、每个连接的负荷、CPU的处理速度、期望的响应时间等。在 Linux 平台下,性能好的服务器,可以支持 500-1000 个连接,需要根据服务器性能进行评估设定 * back_log:控制 MySQL 监听 TCP 端口时的积压请求栈的大小 @@ -5989,7 +6013,7 @@ undo log 是采用段的方式来记录,Rollback Segement 称为回滚段, MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来**解决读写冲突的无锁并发控制**,可以在发生读写请求冲突时不用加锁解决,这个读是指的快照读(也叫一致性读或一致性无锁读),而不是当前读: * 快照读:实现基于 MVCC,因为是多版本并发,所以快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据 -* 当前读:读取数据库记录是当前**最新的版本**(产生幻读、不可重复读),可以对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读 +* 当前读:又叫加锁读,读取数据库记录是当前**最新的版本**(产生幻读、不可重复读),可以对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读 数据库并发场景: @@ -6025,7 +6049,7 @@ MVCC 的优点: 实现原理主要是隐藏字段,undo日志,Read View 来实现的 -数据库中的**聚簇索引**每行数据,除了自定义的字段,还有数据库隐式定义的字段: +InnoDB 存储引擎,数据库中的**聚簇索引**每行数据,除了自定义的字段,还有数据库隐式定义的字段: * DB_TRX_ID:最近修改事务 ID,记录创建该数据或最后一次修改该数据的事务 ID * DB_ROLL_PTR:回滚指针,**指向记录对应的 undo log 日志**,undo log 中又指向上一个旧版本的 undo log @@ -6244,6 +6268,15 @@ MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction( * 所以不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入 log buffer** +InnoDB 的 redo log 是**固定大小**的,redo 日志在磁盘中以文件组的形式存储,同一组中的每个文件大小一样格式一样, + +* `innodb_log_group_home_dir` 代表磁盘存储 redo log 的文件目录,默认是当前数据目录 +* `innodb_log_file_size` 代表文件大小,默认 48M,`innodb_log_files_in_group` 代表文件个数,默认 2 最大 100,所以日志的文件大小为 `innodb_log_file_size * innodb_log_files_in_group` + +redo 日志文件也是由若干个 512 字节的 block 组成,日志文件的前 2048 个字节(前 4 个 block)用来存储一些管理信息,以后的用来存储 log buffer 中的 block 镜像 + +注意:block 并不代表一组 redo log,一组日志可能占用不到一个 block 或者几个 block,依赖于 MTR 的大小 + *** @@ -6254,27 +6287,20 @@ MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction( redo log 需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快,原因: -* 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO -* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,减少无效 IO +* 刷脏是随机 IO,因为每次修改的数据位置随机;redo log 和 binlog 都是**顺序写**,磁盘的顺序 IO 比随机 IO 速度要快 +* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入;redo log 中只包含真正需要写入的部分,减少无效 IO +* **组提交机制**,可以大幅度降低磁盘的 IO 消耗 -InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到磁盘,具体的**刷盘策略**: +InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化(fsync)到磁盘,具体的**刷盘策略**: * 在事务提交时需要进行刷盘,通过修改参数 `innodb_flush_log_at_trx_commit` 设置: * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待**后台线程每秒刷新一次** * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功(默认值) - * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作。已经写入到操作系统的缓存,如果操作系统没有宕机而 MySQL 宕机,也是可以恢复数据的 + * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作。日志已经在操作系统的缓存,如果操作系统没有宕机而 MySQL 宕机,也是可以恢复数据的 * 写入 redo log buffer 的日志超过了总容量的一半,就会将日志刷入到磁盘文件,这会影响执行效率,所以开发中应**避免大事务** * 服务器关闭时 * checkpoint 时(下小节详解) - -InnoDB 的 redo log 是**固定大小**的,redo 日志在磁盘中以文件组的形式存储,同一组中的每个文件大小一样格式一样, - -* `innodb_log_group_home_dir` 代表磁盘存储 redo log 的文件目录,默认是当前数据目录 -* `innodb_log_file_size` 代表文件大小,默认 48M,`innodb_log_files_in_group` 代表文件个数,默认 2 最大 100,所以日志的文件大小为 `innodb_log_file_size * innodb_log_files_in_group` - -redo 日志文件也是由若干个 512 字节的 block 组成,日志文件的前 2048 个字节(前 4 个 block)用来存储一些管理信息,以后的用来存储 log buffer 中的 block 镜像 - -注意:block 并不代表一组 redo log,一组日志可能占用不到一个 block 或者几个 block,依赖于 MTR 的大小 +* 并行的事务提交(组提交)时,会将将其他事务的 redo log 持久化到磁盘。假设事务 A 已经写入 redo log buffer 中,这时另外一个线程的事务 B 提交,如果 innodb_flush_log_at_trx_commit 设置的是 1,那么事务 B 要把 redo log buffer 里的日志全部持久化到磁盘,**因为多个事务共用一个 redo log buffer**,所以一次 fsync 可以刷盘多个事务的 redo log,提升了并发量 服务器启动后 redo 磁盘空间不变,所以 redo 磁盘中的日志文件是被**循环使用**的,采用循环写数据的方式,写完尾部重新写头部,所以要确保头部 log 对应的修改已经持久化到磁盘 @@ -6785,13 +6811,15 @@ RR 隔离界别下,对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自 - 排他锁和排他锁 冲突 - 排他锁和共享锁 冲突 -可以通过以下语句显式给数据集加共享锁或排他锁: +显式给数据集加共享锁或排他锁:**加锁读就是当前读,读取的是最新数据** ```mysql SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE -- 共享锁 SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 ``` +注意:**锁默认会锁聚簇索引(锁就是加在索引上)**,但是当使用覆盖索引时,加共享锁只锁二级索引,不锁聚簇索引 + *** @@ -6897,19 +6925,28 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 ##### 间隙锁 -当使用范围条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据进行加锁,对于键值在条件范围内但并不存在的记录,叫做间隙(GAP), InnoDB 会对间隙进行加锁,就是间隙锁 +InnoDB 会对间隙(GAP)进行加锁,就是间隙锁 (RR 隔离级别下才有该锁)。间隙锁之间不存在冲突关系,**多个事务可以同时对一个间隙加锁**,但是间隙锁会阻止往这个间隙中插入一个记录的操作 -* 唯一索引加锁只有在值存在时才是行锁,值不存在会变成间隙锁,所以范围查询时容易出现间隙锁 -* 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 +InnoDB 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的组合,但是加锁过程是分为间隙锁和行锁两段执行 -加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的组合,可以**保护当前记录和前面的间隙** +* 可以**保护当前记录和前面的间隙**,遵循左开右闭原则,单纯的是间隙锁左开右开 +* 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,正无穷) -* 加锁遵循左开右闭原则 -* 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,正无穷),锁住索引 11 会对 (10,11] 加锁 +几种索引的加锁情况: + +* 唯一索引加锁在值存在时是行锁,next-key lock 会退化为行锁,值不存在会变成间隙锁 +* 普通索引加锁会继续向右遍历到不满足条件的值为止,next-key lock 退化为间隙锁 +* 范围查询无论是否是唯一索引,都需要访问到不满足条件的第一个值为止 +* 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁,只能加锁一部分的间隙。 -间隙锁危害:当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害 +间隙锁危害: + +* 当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害,影响并发度 +* 事务 A B 同时锁住一个间隙后,A 往当前间隙插入数据时会被 B 的间隙锁阻塞,B 也执行插入间隙数据的操作时就会**产生死锁** + +现场演示: * 关闭自动提交功能: @@ -7226,7 +7263,7 @@ MySQL 复制的优点主要包含以下三个方面: #### 主从结构 -MySQL 的主从之间维持了一个长连接。主库内部有一个线程,专门用于服务从库的长连接,连接过程: +MySQL 的主从之间维持了一个**长连接**。主库内部有一个线程,专门用于服务从库的长连接,连接过程: * 从库执行 change master 命令,设置主库的 IP、端口、用户名、密码以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量 * 从库执行 start slave 命令,这时从库会启动两个线程,就是图中的 io_thread 和 sql_thread,其中 io_thread 负责与主库建立连接 @@ -7290,11 +7327,11 @@ MySQL 的主从之间维持了一个长连接。主库内部有一个线程, 主从延迟的原因: -* 从库的查询压力大 +* 从库的机器性能比主库的差,导致从库的复制能力弱 +* 从库的查询压力大,建立一主多从的结构 * 大事务的执行,主库必须要等到事务完成之后才会写入 binlog,导致从节点出现应用 binlog 延迟 * 主库的 DDL,从库与主库的 DDL 同步是串行进行,DDL 在主库执行时间很长,那么从库也会消耗同样的时间 * 锁冲突问题也可能导致从节点的 SQL 线程执行慢 -* 从库的机器性能比主库的差,导致从库的复制能力弱 主从同步问题永远都是**一致性和性能的权衡**,需要根据实际的应用场景,可以采取下面的办法: @@ -7324,7 +7361,7 @@ coordinator 就是原来的 SQL Thread,并行复制中它不再直接更新数 MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库 名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 -每个事务在分发的时候,跟线程的**冲突**(事务操作的是同一个 DB)关系包括以下三种情况: +每个事务在分发的时候,跟线程的**冲突**(事务操作的是同一个库)关系包括以下三种情况: * 如果跟所有线程都不冲突,coordinator 线程就会把这个事务分配给最空闲的线程 * 如果只跟一个线程冲突,coordinator 线程就会把这个事务分配给这个存在冲突关系的线程 @@ -7344,18 +7381,17 @@ MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当 ##### MySQL5.7 -MySQL 5.7 并行复制策略的思想是: - -* 所有处于 commit 状态的事务可以并行执行 -* 同时处于 prepare 状态的事务,在从库执行时是可以并行的 -* 处于 prepare 状态的事务,与处于 commit 状态的事务之间,在从库执行时也是可以并行的 - MySQL 5.7 由参数 slave-parallel-type 来控制并行复制策略: * 配置为 DATABASE,表示使用 MySQL 5.6 版本的**按库(DB)并行策略** * 配置为 LOGICAL_CLOCK,表示的**按提交状态并行**执行 -MySQL 5.7.22 版本里,MySQL 增加了一个新的并行复制策略,基于 WRITESET 的并行复制。新增了一个参数 binlog-transaction-dependency-tracking,用来控制是否启用这个新策略: +按提交状态并行复制策略的思想是: + +* 所有处于 commit 状态的事务可以并行执行;同时处于 prepare 状态的事务,在从库执行时是可以并行的 +* 处于 prepare 状态的事务,与处于 commit 状态的事务之间,在从库执行时也是可以并行的 + +MySQL 5.7.22 版本里,MySQL 增加了一个新的并行复制策略,基于 WRITESET 的并行复制,新增了一个参数 binlog-transaction-dependency-tracking,用来控制是否启用这个新策略: * COMMIT_ORDER:表示根据同时进入 prepare 和 commit 来判断是否可以并行的策略 @@ -7428,9 +7464,7 @@ MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于 ### 主从搭建 -#### 搭建流程 - -##### master +#### master 1. 在master 的配置文件(/etc/mysql/my.cnf)中,配置如下内容: @@ -7490,7 +7524,7 @@ MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于 -##### slave +#### slave 1. 在 slave 端配置文件中,配置如下内容: @@ -7529,7 +7563,7 @@ MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于 -##### 验证 +#### 验证 1. 在主库中创建数据库,创建表并插入数据: @@ -7564,7 +7598,9 @@ MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于 -#### 主从切换 +### 主从切换 + +#### 正常切换 正常切换步骤: @@ -7585,9 +7621,126 @@ MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于 * 将原来 master 变为 slave(参考搭建流程中的 slave 方法) -主库发生故障,从库会进行上位,其他从库指向新的主库 +**可靠性优先策略**: + +* 判断备库 B 现在的 seconds_behind_master,如果小于某个值(比如 5 秒)继续下一步,否则持续重试这一步 +* 把主库 A 改成只读状态,即把 readonly 设置为 true +* 判断备库 B 的 seconds_behind_master 的值,直到这个值变成 0 为止(该步骤比较耗时,所以步骤 1 中要尽量等待该值变小) +* 把备库 B 改成可读写状态,也就是把 readonly 设置为 false +* 把业务请求切到备库 B + +可用性优先策略:先做最后两步,会造成主备数据不一致的问题 + + + +参考文章:https://time.geekbang.org/column/article/76795 + + + +*** + + + +#### 基于位点 + +主库发生故障后从库会上位,**其他从库指向新的主库**,从库(B)执行 CHANGE MASTER TO 命令需要指定 MASTER_LOG_FILE、MASTER_LOG_POS 表示从新主库(A)的哪个文件的哪个位点开始同步,这个位置就是**同步位点**,对应主库的文件名和日志偏移量 + +寻找位点需要找一个稍微往前的,然后再通过判断跳过那些在从库 B 上已经执行过的事务,获取位点方法: + +* 等待新主库 A 把中转日志(relay log)全部同步完成 +* 在 A 上执行 show master status 命令,得到当前 A 上最新的 File 和 Position +* 取原主库故障的时刻 T,用 mysqlbinlog 工具解析新主库 A 的 File,得到 T 时刻的位点 + +通常情况下该值并不准确,在切换的过程中会发生错误,所以要先主动跳过这些错误: + +* 切换过程中,可能会重复执行一个事务,所以需要主动跳过所有重复的事务 + + ```mysql + SET GLOBAL sql_slave_skip_counter=1; + START SLAVE; + ``` + +* 设置 slave_skip_errors 参数,直接设置跳过指定的错误,保证主从切换的正常进行 + + * 1062 错误是插入数据时唯一键冲突 + * 1032 错误是删除数据时找不到行 + + 该方法针对的是主备切换时,由于找不到精确的同步位点,只能采用这种方法来创建从库和新主库的主备关系。等到主备间的同步关系建立完成并稳定执行一段时间后,还需要把这个参数设置为空,以免真的出现了主从数据不一致也跳过了 + + + +**** + + + +#### 基于GITD + +##### GITD + +GTID 的全称是 Global Transaction Identifier,全局事务 ID,是一个事务**在提交时生成**的,是这个事务的唯一标识,组成: + +```mysql +GTID=source_id:transaction_id +``` + +* source_id:是一个实例第一次启动时自动生成的,是一个全局唯一的值 +* transaction_id:初始值是 1,每次提交事务的时候分配给这个事务,并加 1,是连续的(区分事务 ID,事务 ID 是在执行时生成) + +启动 MySQL 实例时,加上参数 `gtid_mode=on` 和 `enforce_gtid_consistency=on` 就可以启动 GTID 模式,每个事务都会和一个 GTID 一一对应,每个 MySQL 实例都维护了一个 **GTID 集合**,用来对应当前实例执行过的所有事务 + +GTID 有两种生成方式,使用哪种方式取决于 session 变量 gtid_next: +* `gtid_next=automatic`:使用默认值,把 source_id:transaction_id (递增)分配给这个事务,然后加入本实例的 GTID 集合 + ```mysql + @@SESSION.GTID_NEXT = 'source_id:transaction_id'; + ``` + +* `gtid_next=GTID`:指定的 GTID 的值,如果该值已经存在于实例的 GTID 集合中,接下来执行的事务会直接被系统忽略;反之就将该值分配给接下来要执行的事务,系统不需要给这个事务生成新的 GTID,也不用加 1 + + 注意:一个 GTID 只能给一个事务使用,所以执行下一个事务,要把 gtid_next 设置成另外一个 GTID 或者 automatic + +业务场景: + +* 主库 X 和从库 Y 执行一条相同的指令后进行事务同步 + + ```mysql + INSERT INTO t VALUES(1,1); + ``` + +* 当 Y 同步 X 时,会出现主键冲突,导致实例 X 的同步线程停止,解决方法: + + ```mysql + SET gtid_next='(这里是主库 X 的 GTID 值)'; + BEGIN; + COMMIT; + SET gtid_next=automatic; + START SLAVE; + ``` + + 前三条语句通过**提交一个空事务**,把这个 GTID 加到实例 Y 的 GTID 集合中,实例 Y 就会直接跳过这个事务 + + + +**** + + + +##### 切换 + +在 GTID 模式下,CHANGE MASTER TO 不需要指定日志名和日志偏移量,指定 `master_auto_position=1` 代表使用 GTID 模式 + +新主库实例 A 的 GTID 集合记为 set_a,从库实例 B 的 GTID 集合记为 set_b,主备切换逻辑: + +* 实例 B 指定主库 A,基于主备协议建立连接,实例 B 并把 set_b 发给主库 A +* 实例 A 算出 set_a 与 set_b 的差集,就是所有存在于 set_a 但不存在于 set_b 的 GTID 的集合,判断 A 本地是否包含了这个**差集**需要的所有 binlog 事务 + * 如果不包含,表示 A 已经把实例 B 需要的 binlog 给删掉了,直接返回错误 + * 如果确认全部包含,A 从自己的 binlog 文件里面,找出第一个不在 set_b 的事务,发给 B +* 实例 A 之后就从这个事务开始,往后读文件,按顺序取 binlog 发给 B 去执行 + + + +参考文章:https://time.geekbang.org/column/article/77427 @@ -7682,6 +7835,24 @@ binlog_format=STATEMENT +#### 日志刷盘 + +事务执行过程中,先将日志写(write)到 binlog cache,事务提交时再把 binlog cache 写(fsync)到 binlog 文件中,一个事务的 binlog 是不能被拆开的,所以不论这个事务多大也要确保一次性写入 + +事务提交时执行器把 binlog cache 里的完整事务写入到 binlog 中,并清空 binlog cache + +write 和 fsync 的时机由参数 sync_binlog 控制的: + +* sync_binlog=0:表示每次提交事务都只 write,不 fsync +* sync_binlog=1:表示每次提交事务都会执行 fsync +* sync_binlog=N(N>1):表示每次提交事务都 write,但累积 N 个事务后才 fsync,但是如果主机发生异常重启,会丢失最近 N 个事务的 binlog 日志 + + + +*** + + + #### 日志读取 日志文件存储位置:/var/lib/mysql diff --git a/Java.md b/Java.md index c2fa3c0..a795778 100644 --- a/Java.md +++ b/Java.md @@ -41,37 +41,37 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, **byte:** - byte 数据类型是 8 位、有符号的,以**二进制补码**表示的整数,**8 位一个字节**,首位是符号位 -- 最小值是 **-128(-2^7)** -- 最大值是 **127(2^7-1)** -- 默认值是 **`0`** +- 最小值是 -128(-2^7) +- 最大值是 127(2^7-1) +- 默认值是 `0` - byte 类型用在大型数组中节约空间,主要代替整数,byte 变量占用的空间只有 int 类型的四分之一 - 例子:`byte a = 100,byte b = -50` **short:** - short 数据类型是 16 位、有符号的以二进制补码表示的整数 -- 最小值是 **-32768(-2^15)** -- 最大值是 **32767(2^15 - 1)** +- 最小值是 -32768(-2^15) +- 最大值是 32767(2^15 - 1) - short 数据类型也可以像 byte 那样节省空间,一个 short 变量是 int 型变量所占空间的二分之一 -- 默认值是 **`0`** +- 默认值是 `0` - 例子:`short s = 1000,short r = -20000` **int:** - int 数据类型是 32 位 4 字节、有符号的以二进制补码表示的整数 -- 最小值是 **-2,147,483,648(-2^31 -)** -- 最大值是 **2,147,483,647(2^31 - 1)** +- 最小值是 -2,147,483,648(-2^31 -) +- 最大值是 2,147,483,647(2^31 - 1) - 一般地整型变量默认为 int 类型 -- 默认值是 **`0`** +- 默认值是 `0` - 例子:`int a = 100000, int b = -200000` **long:** - long 数据类型是 64 位 8 字节、有符号的以二进制补码表示的整数 -- 最小值是 **-9,223,372,036,854,775,808(-2^63)** -- 最大值是 **9,223,372,036,854,775,807(2^63 -1)** +- 最小值是 -9,223,372,036,854,775,808(-2^63) +- 最大值是 9,223,372,036,854,775,807(2^63 -1) - 这种类型主要使用在需要比较大整数的系统上 -- 默认值是 **` 0L`** +- 默认值是 ` 0L` - 例子: `long a = 100000L,Long b = -200000L` L 理论上不分大小写,但是若写成 I 容易与数字 1 混淆,不容易分辩,所以最好大写 @@ -79,7 +79,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, - float 数据类型是单精度、32 位、符合 IEEE 754 标准的浮点数 - float 在储存大型浮点数组的时候可节省内存空间 -- 默认值是 **`0.0f`** +- 默认值是 `0.0f` - 浮点数不能用来表示精确的值,如货币 - 例子:`float f1 = 234.5F` @@ -88,7 +88,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, - double 数据类型是双精度、64 位、符合 IEEE 754 标准的浮点数 - 浮点数的默认类型为 double 类型 - double 类型同样不能表示精确的值,如货币 -- 默认值是 **`0.0d`** +- 默认值是 `0.0d` - 例子:`double d1 = 123.4` **boolean:** @@ -96,18 +96,24 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, - boolean 数据类型表示一位的信息 - 只有两个取值:true 和 false - JVM 规范指出 boolean 当做 int 处理,boolean 数组当做 byte 数组处理,这样可以得出 boolean 类型单独使用占了 4 个字节,在数组中是 1 个字节 -- 默认值是 **`false`** +- 默认值是 `false` - 例子:`boolean one = true` **char:** -- char 类型是一个单一的 16 位两个字节的 Unicode 字符 -- 最小值是 **`\u0000`**(即为 0) -- 最大值是 **`\uffff`**(即为 65535) +- char 类型是一个单一的 16 位**两个字节**的 Unicode 字符 +- 最小值是 `\u0000`(即为 0) +- 最大值是 `\uffff`(即为 65535) - char 数据类型可以存储任何字符 - 例子:`char c = 'A'`,`char c = '张'` -上下转型 + + +**** + + + +##### 上下转型 * float 与 double: @@ -138,16 +144,17 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, * 隐式类型转换: - 字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型向下转型为 short 类型 + 字面量 1 是 int 类型,比 short 类型精度要高,因此不能隐式地将 int 类型向下转型为 short 类型 使用 += 或者 ++ 运算符会执行隐式类型转换: ```java + short s1 = 1; s1 += 1; //s1++; //上面的语句相当于将 s1 + 1 的计算结果进行了向下转型 s1 = (short) (s1 + 1); ``` - + @@ -2425,14 +2432,14 @@ public class Student { #### 基本介绍 -**String 被声明为 final,因此不可被继承 (Integer 等包装类也不能被继承)** +String 被声明为 final,因此不可被继承 **(Integer 等包装类也不能被继承)** ```java public final class String implements java.io.Serializable, Comparable, CharSequence { /** The value is used for character storage. */ - private final byte[] value; - /** The identifier of the encoding used to encode the bytes in {@code value}. */ - private final byte coder; + private final char value[]; + /** Cache the hash code for the string */ + private int hash; // Default to 0 } ``` @@ -2440,7 +2447,7 @@ public final class String implements java.io.Serializable, Comparable, C value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组,并且 String 内部没有改变 value 数组的方法,因此可以**保证 String 不可变,也保证线程安全** -**注意:不能改变的意思是每次更改字符串都会产生新的对象,并不是对原始对象进行改变** +注意:不能改变的意思是**每次更改字符串都会产生新的对象**,并不是对原始对象进行改变 ```java String s = "abc"; From 572a59cb137e7108fa4239a9994ca87f346dd2b4 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 24 Mar 2022 21:52:04 +0800 Subject: [PATCH 08/51] Update Java Note --- DB.md | 144 +++++++++++++++++++++++++++++++++++++++++++++++++------ Frame.md | 48 +++++++------------ Java.md | 26 ---------- Prog.md | 4 +- SSM.md | 14 +++++- 5 files changed, 163 insertions(+), 73 deletions(-) diff --git a/DB.md b/DB.md index 514b51e..e9b0d2c 100644 --- a/DB.md +++ b/DB.md @@ -7259,7 +7259,7 @@ MySQL 复制的优点主要包含以下三个方面: -### 复制原理 +### 主从复制 #### 主从结构 @@ -7420,7 +7420,9 @@ MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于 -#### 读写分离 +### 读写分离 + +#### 读写延迟 读写分离:可以降低主库的访问压力,提高系统的并发能力 @@ -7432,13 +7434,73 @@ MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于 解决方案: * 强制将写之后**立刻读的操作转移到主库**,比如刚注册的用户,直接登录从库查询可能查询不到,先走主库登录 - * **二次查询**,如果从库查不到数据,则再去主库查一遍,由 API 封装,比较简单,但导致主库压力大 +* 更新主库后,读从库之前先 sleep 一下,类似于执行一条 `select sleep(1)` 命令,大多数情况下主备延迟在 1 秒之内 + + + +*** + + + +#### 确保机制 + +##### 无延迟 + +确保主备无延迟的方法: + +* 每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0,如果不等于那就等到参数变为 0 执行查询请求 +* 对比位点,Master_Log_File 和 Read_Master_Log_Pos 表示的是读到的主库的最新位点,Relay_Master_Log_File 和 Exec_Master_Log_Pos 表示的是备库执行的最新位点,这两组值完全相同就说明接收到的日志已经同步完成 +* 对比 GTID 集合,Retrieved_Gtid_Set 是备库收到的所有日志的 GTID 集合,Executed_Gtid_Set 是备库所有已经执行完成的 GTID 集合,如果这两个集合相同也表示备库接收到的日志都已经同步完成 + + + +*** + + + +##### 半同步 + +半同步复制就是 semi-sync replication,适用于一主一备的场景,工作流程: + +* 事务提交的时候,主库把 binlog 发给从库 +* 从库收到 binlog 以后,发回给主库一个 ack,表示收到了 +* 主库收到这个 ack 以后,才能给客户端返回事务完成的确认 + +在一主多从场景中,主库只要等到一个从库的 ack,就开始给客户端返回确认,这时在从库上执行查询请求,有两种情况: + +* 如果查询是落在这个响应了 ack 的从库上,是能够确保读到最新数据 +* 如果查询落到其他从库上,它们可能还没有收到最新的日志,就会产生过期读的问题 + +在业务更新的高峰期,主库的位点或者 GTID 集合更新很快,导致从库来不及处理,那么两个位点等值判断就会一直不成立,很可能出现从库上迟迟无法响应查询请求的情况 + + + +**** + + + +##### 等位点 + +在**从库执行判断位点**的命令,参数 file 和 pos 指的是主库上的文件名和位置,timeout 可选,设置为正整数 N 表示最多等待 N 秒 + +```mysql +SELECT master_pos_wait(file, pos[, timeout]); +``` + +命令正常返回的结果是一个正整数 M,表示从命令开始执行,到应用完 file 和 pos 表示的 binlog 位置,执行了多少事务 + +* 如果执行期间,备库同步线程发生异常,则返回 NULL +* 如果等待超过 N 秒,就返回 -1 +* 如果刚开始执行的时候,就发现已经执行过这个位置了,则返回 0 -* 更新主库后,读从库之前先 sleep 一下,类似于执行一条 `select sleep(1)` 命令 -* 确保主备无延迟的方法,每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0,如果不等于那就等到这个参数变为 0 才能执行查询请求 +工作流程:先执行 trx1,再执行一个查询请求的逻辑,要**保证能够查到正确的数据** +* trx1 事务更新完成后,马上执行 `show master status` 得到当前主库执行到的 File 和 Position +* 选定一个从库执行判断位点语句,如果返回值是 >=0 的正整数,说明从库已经同步完事务,可以在这个从库执行查询语句 +* 如果出现其他情况,需要到主库执行查询语句 +注意:如果所有的从库都延迟超过 timeout 秒,查询压力就都跑到主库上,所以需要进行权衡 @@ -7446,7 +7508,37 @@ MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于 -### 负载均衡 +##### 等GTID + +数据库开启了 GTID 模式,MySQL 提供了判断 GTID 的命令 + +```mysql +SELECT wait_for_executed_gtid_set(gtid_set [, timeout]) +``` + +* 等待直到这个库执行的事务中包含传入的 gtid_set,返回 0 +* 超时返回 1 + +工作流程:先执行 trx1,再执行一个查询请求的逻辑,要保证能够查到正确的数据 + +* trx1 事务更新完成后,从返回包直接获取这个事务的 GTID,记为 gtid1 +* 选定一个从库执行查询语句,如果返回值是 0,则在这个从库执行查询语句,否则到主库执行查询语句 + +对比等待位点方法,减少了一次 `show master status` 的方法,将参数 session_track_gtids 设置为 OWN_GTID,然后通过 API 接口 mysql_session_track_get_first 从返回包解析出 GTID 的值即可 + +总结:所有的等待无延迟的方法,都需要根据具体的业务场景去判断实施 + + + +参考文章:https://time.geekbang.org/column/article/77636 + + + +*** + + + +#### 负载均衡 负载均衡是应用中使用非常普遍的一种优化方法,机制就是利用某种均衡算法,将固定的负载量分布到不同的服务器上,以此来降低单台服务器的负载,达到优化的效果 @@ -7641,9 +7733,33 @@ MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于 +#### 健康检测 + +主库发生故障后从库会上位,**其他从库指向新的主库**,所以需要一个健康检测的机制来判断主库是否宕机 + +* select 1 判断,但是高并发下检测不出线程的锁等待的阻塞问题 + +* 查表判断,在系统库(mysql 库)里创建一个表,比如命名为 health_check,里面只放一行数据,然后定期执行。但是当 binlog 所在磁盘的空间占用率达到 100%,所有的更新和事务提交语句都被阻塞,查询语句可以继续运行 + +* 更新判断,在健康检测表中放一个 timestamp 字段,用来表示最后一次执行检测的时间 + + ```mysql + UPDATE mysql.health_check SET t_modified=now(); + ``` + + 节点可用性的检测都应该包含主库和备库,为了让主备之间的更新不产生冲突,可以在 mysql.health_check 表上存入多行数据,并用主备的 server_id 做主键,保证主、备库各自的检测命令不会发生冲突 + + + +*** + + + + + #### 基于位点 -主库发生故障后从库会上位,**其他从库指向新的主库**,从库(B)执行 CHANGE MASTER TO 命令需要指定 MASTER_LOG_FILE、MASTER_LOG_POS 表示从新主库(A)的哪个文件的哪个位点开始同步,这个位置就是**同步位点**,对应主库的文件名和日志偏移量 +主库上位后,从库(B)执行 CHANGE MASTER TO 命令,指定 MASTER_LOG_FILE、MASTER_LOG_POS 表示从新主库(A)的哪个文件的哪个位点开始同步,这个位置就是**同步位点**,对应主库的文件名和日志偏移量 寻找位点需要找一个稍微往前的,然后再通过判断跳过那些在从库 B 上已经执行过的事务,获取位点方法: @@ -7673,9 +7789,9 @@ MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于 -#### 基于GITD +#### 基于GTID -##### GITD +##### GTID GTID 的全称是 Global Transaction Identifier,全局事务 ID,是一个事务**在提交时生成**的,是这个事务的唯一标识,组成: @@ -7718,7 +7834,7 @@ GTID 有两种生成方式,使用哪种方式取决于 session 变量 gtid_nex START SLAVE; ``` - 前三条语句通过**提交一个空事务**,把这个 GTID 加到实例 Y 的 GTID 集合中,实例 Y 就会直接跳过这个事务 + 前三条语句通过**提交一个空事务**,把 X 的 GTID 加到实例 Y 的 GTID 集合中,实例 Y 就会直接跳过这个事务 @@ -8541,8 +8657,8 @@ SQL 注入攻击演示 PreparedStatement:预编译 sql 语句的执行者对象,继承 `PreparedStatement extends Statement` -* 在执行 sql 语句之前,将 sql 语句进行提前编译。明确 sql 语句的格式,剩余的内容都会认为是参数 -* sql 语句中的参数使用 ? 作为占位符 +* 在执行 sql 语句之前,将 sql 语句进行提前编译,**明确 sql 语句的格式**,剩余的内容都会认为是参数 +* sql 语句中的参数使用 ? 作为**占位符** 为 ? 占位符赋值的方法:`setXxx(int parameterIndex, xxx data)` @@ -9199,7 +9315,7 @@ Redis (REmote DIctionary Server) :用 C 语言开发的一个开源的高性 * 数据间没有必然的关联关系,**不存关系,只存数据** * 数据**存储在内存**,存取速度快,解决了磁盘 IO 速度慢的问题 * 内部采用**单线程**机制进行工作 -* 高性能,官方测试数据,50 个并发执行100000 个请求,读的速度是110000 次/s,写的速度是81000次/s +* 高性能,官方测试数据,50 个并发执行 100000 个请求,读的速度是 110000 次/s,写的速度是 81000次/s * 多数据类型支持 * 字符串类型:string(String) * 列表类型:list(LinkedList) @@ -11456,7 +11572,7 @@ Redis 采用惰性删除和定期删除策略的结合使用 定时删除和惰性删除这两种方案都是走的极端,定期删除就是折中方案 -定期删除是周期性轮询 Redis 库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度 +定期删除是**周期性轮询 Redis 库中的时效性**数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度 定期删除方案: diff --git a/Frame.md b/Frame.md index ea98a80..864c479 100644 --- a/Frame.md +++ b/Frame.md @@ -3469,19 +3469,7 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-数据分发.png) -主要缺点包含以下几点: -* 系统可用性降低:系统引入的外部依赖越多,系统稳定性越差,一旦 MQ 宕机,就会对业务造成影响 - - 引申问题:如何保证 MQ 的高可用? - -* 系统复杂度提高:MQ 的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过 MQ 进行异步调用 - - 引申问题:如何保证消息没有被重复消费?怎么处理消息丢失情况?那么保证消息传递的顺序性? - -* 一致性问题:A 系统处理完业务,通过 MQ 给 B、C、D 三个系统发消息数据,如果 B 系统、C 系统处理成功,D 系统处理失败 - - 引申问题:如何保证消息数据处理的一致性? @@ -4528,7 +4516,7 @@ Broker 包含了以下几个重要子模块: RocketMQ 的工作流程: - 启动 NameServer 监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心 -- Broker 启动,跟所有的 NameServer 保持长连接,每隔 30s 时间向 NameServer 上报 Topic 路由信息(心跳包)。心跳包中包含当前 Broker 信息(IP、端口等)以及存储所有 Topic 信息。注册成功后,NameServer 集群中就有 Topic 跟 Broker 的映射关系 +- Broker 启动,跟**所有的 NameServer 保持长连接**,每隔 30s 时间向 NameServer 上报 Topic 路由信息(心跳包)。心跳包中包含当前 Broker 信息(IP、端口等)以及存储所有 Topic 信息。注册成功后,NameServer 集群中就有 Topic 跟 Broker 的映射关系 - 收发消息前,先创建 Topic,创建 Topic 时需要指定该 Topic 要存储在哪些 Broker 上,也可以在发送消息时自动创建 Topic - Producer 启动时先跟 NameServer 集群中的**其中一台**建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,同时 Producer 会默认每隔 30s 向 NameServer **定时拉取**一次路由信息 - Producer 发送消息时,根据消息的 Topic 从本地缓存的 TopicPublishInfoTable 获取路由信息,如果没有则会从 NameServer 上重新拉取并更新,轮询队列列表并选择一个队列 MessageQueue,然后与队列所在的 Broker 建立长连接,向 Broker 发消息 @@ -4698,9 +4686,9 @@ RocketMQ 网络部署特点: - NameServer 是一个几乎**无状态节点**,节点之间相互独立,无任何信息同步 -- Broker 部署相对复杂,Broker 分为 Master 与 Slave,Master 可以部署多个,一个 Master 可以对应多个 Slave,但是一个 Slave 只能对应一个 Master,Master 与 Slave 的对应关系通过指定相同 BrokerName、不同 BrokerId 来定义,**BrokerId 为 0 是 Master,非 0 表示 Slave。每个 Broker 与 NameServer 集群中的所有节点建立长连接**,定时注册 Topic 信息到所有 NameServer +- Broker 部署相对复杂,Broker 分为 Master 与 Slave,Master 可以部署多个,一个 Master 可以对应多个 Slave,但是一个 Slave 只能对应一个 Master,Master 与 Slave 的对应关系通过指定相同 BrokerName、不同 BrokerId 来定义,BrokerId 为 0 是 Master,非 0 表示 Slave。**每个 Broker 与 NameServer 集群中的所有节点建立长连接**,定时注册 Topic 信息到所有 NameServer - 注意:部署架构上也支持一 Master 多 Slave,但只有 BrokerId=1 的从服务器才会参与消息的读负载(读写分离) + 说明:部署架构上也支持一 Master 多 Slave,但只有 BrokerId=1 的从服务器才会参与消息的读负载(读写分离) - Producer 与 NameServer 集群中的其中**一个节点(随机选择)建立长连接**,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master 建立长连接,且定时向 Master **发送心跳**。Producer 完全无状态,可集群部署 @@ -4710,8 +4698,6 @@ RocketMQ 网络部署特点: ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-集群架构.png) -集群工作流程:参考通信机制 → 工作流程 - 官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/architecture.md @@ -4722,12 +4708,12 @@ RocketMQ 网络部署特点: -#### 高可用 - -在 Consumer 的配置文件中,并不需要设置是从 Master 读还是从 Slave 读,当 Master 不可用或者繁忙的时候,Consumer 会被自动切换到从 Slave 读。有了自动切换的机制,当一个 Master 机器出现故障后,Consumer 仍然可以从 Slave 读取消息,不影响 Consumer 程序,达到了消费端的高可用性 +#### 高可用性 在创建 Topic 的时候,把 Topic 的多个 Message Queue 创建在多个 Broker 组上(相同 Broker 名称,不同 brokerId 的机器组成一个 Broker 组),当一个 Broker 组的 Master 不可用后,其他组的 Master 仍然可用,Producer 仍然可以发送消息 +在 Consumer 的配置文件中,并不需要设置是从 Master 读还是从 Slave 读,当 Master 不可用或者繁忙的时候,Consumer 会被自动切换到从 Slave 读。有了自动切换的机制,当一个 Master 机器出现故障后,Consumer 仍然可以从 Slave 读取消息,不影响 Consumer 程序,达到了消费端的高可用性 + RocketMQ 目前还不支持把 Slave 自动转成 Master,需要手动停止 Slave 角色的 Broker,更改配置文件,用新的配置文件启动 Broker ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-高可用.png) @@ -6132,6 +6118,8 @@ MappedFileQueue 用来管理 MappedFile 文件 private volatile long beginTimeInLock = 0; // 写数据时加锁的开始时间 protected final PutMessageLock putMessageLock; // 写锁,两个实现类:自旋锁和重入锁 ``` + + 因为发送消息是需要持久化的,在 Broker 端持久化时会获取该锁,**保证发送的消息的线程安全** 构造方法: @@ -6805,7 +6793,7 @@ GroupTransferService 用来控制数据同步 ###### 成员属性 -HAClient 是 slave 端运行的代码,用于和 master 服务器建立长连接,上报本地同步进度,消费服务器发来的 msg 数据 +HAClient 是 slave 端运行的代码,用于**和 master 服务器建立长连接**,上报本地同步进度,消费服务器发来的 msg 数据 成员变量: @@ -7225,8 +7213,6 @@ WriteSocketService 类是一个任务对象,master向 slave 传输的数据帧 * `if (writeSize > 0)`:写数据成功,但是不代表 SMBR 中的数据全部写完成 - - * `boolean result`:判断是否发送完成,返回该值 @@ -7947,7 +7933,7 @@ DefaultMQProducerImpl 类是默认的生产者实现类 private SendResult sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout) ``` - * `brokerAddr = this.mQClientFactory(...)`:获取指定 BrokerName 对应的 mater 节点的地址,master 节点的 ID 为 0 + * `brokerAddr = this.mQClientFactory(...)`:**获取指定 BrokerName 对应的 mater 节点的地址**,master 节点的 ID 为 0,集群模式下,**发送消息要发到主节点** * `brokerAddr = MixAll.brokerVIPChannel()`:Broker 启动时会绑定两个服务器端口,一个是普通端口,一个是 VIP 端口,服务器端根据不同端口创建不同的的 NioSocketChannel @@ -8374,7 +8360,7 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 MQClientInstance.this.adjustThreadPool(); ``` -* updateTopicRouteInfoFromNameServer():**更新路由数据** +* updateTopicRouteInfoFromNameServer():**更新路由数据**,通过加锁保证当前实例只有一个线程去更新 * `if (isDefault && defaultMQProducer != null)`:需要默认数据 @@ -8388,13 +8374,15 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 * `if (changed)`:不一致进入更新逻辑 - `Update Pub info`:更新生产者信息 + `this.brokerAddrTable.put(...)`:更新客户端 broker 物理**节点映射表** + `Update Pub info`:更新生产者信息 + * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:将主题路由数据转化为发布数据,会**创建消息队列 MQ**,放入发布数据对象的集合中 * `impl.updateTopicPublishInfo(topic, publishInfo)`:生产者将主题的发布数据保存到它本地,方便发送消息使用 - - `Update sub info`:更新消费者信息,创建 MQ 队列,更新订阅信息,用于负载均衡 + `Update sub info`:更新消费者信息,创建 MQ 队列,更新订阅信息,用于负载均衡 + `this.topicRouteTable.put(topic, cloneTopicRouteData)`:**将数据放入本地路由表** @@ -9537,7 +9525,7 @@ PullAPIWrapper 类封装了拉取消息的 API * pullKernelImpl():拉消息 - * `FindBrokerResult findBrokerResult`:查询指定 BrokerName 的地址信息,主节点或者推荐节点 + * `FindBrokerResult findBrokerResult`:**本地查询指定 BrokerName 的地址信息**,推荐节点或者主节点 * `if (null == findBrokerResult)`:查询不到,就到 Namesrv 获取指定 topic 的路由数据 @@ -9603,7 +9591,7 @@ private RemotingCommand processRequest(final Channel channel, RemotingCommand re * `switch (this.brokerController.getMessageStoreConfig().getBrokerRole())`:如果当前主机节点角色为 slave 并且**从节点读**并未开启的话,直接给客户端 一个状态 `PULL_RETRY_IMMEDIATELY`,并设置为下次从主节点读 -* `if (this.brokerController.getBrokerConfig().isSlaveReadEnable())`:消费太慢,下次从另一台机器拉取 +* `if (this.brokerController.getBrokerConfig().isSlaveReadEnable())`:消费太慢,**下次从另一台机器拉取** * `switch (getMessageResult.getStatus())`:根据 getMessageResult 的状态设置 response 的 code diff --git a/Java.md b/Java.md index a795778..735be75 100644 --- a/Java.md +++ b/Java.md @@ -4743,8 +4743,6 @@ HashMap 继承关系如下图所示: ![](https://gitee.com/seazean/images/raw/master/Java/HashMap继承关系.bmp) - - 说明: * Cloneable 空接口,表示可以克隆, 创建并返回 HashMap 对象的一个副本。 @@ -11601,22 +11599,6 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 -*** - - - -#### 增量收集 - -增量收集算法:通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作,基础仍是标记-清除和复制算法,用于多线程并发环境 - -工作原理:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,影响系统的交互性,所以让垃圾收集线程和应用程序线程**交替执行**,每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复直到垃圾收集完成 - -缺点:线程切换和上下文转换消耗资源,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降 - - - - - *** @@ -12181,14 +12163,6 @@ public class UsingRandom { -*** - - - -##### 监听器 - -监听器和其他回调情况,假如客户端在实现的 API 中注册回调,却没有显式的取消,那么就会一直积聚下去,所以确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,比如保存为 WeakHashMap 中的键 - *** diff --git a/Prog.md b/Prog.md index 690de91..59e5cbc 100644 --- a/Prog.md +++ b/Prog.md @@ -1028,7 +1028,7 @@ LocalVariableTable: 偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程之后重新获取该锁不再需要同步操作: -* 当锁对象第一次被线程获得的时候进入偏向状态,标记为 101,同时使用 CAS 操作将线程 ID 记录到 Mark Word。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作 +* 当锁对象第一次被线程获得的时候进入偏向状态,标记为 101,同时**使用 CAS 操作将线程 ID 记录到 Mark Word**。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作 * 当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定或轻量级锁状态 @@ -1124,7 +1124,7 @@ public static void method2() { ![](https://gitee.com/seazean/images/raw/master/Java/JUC-重量级锁原理1.png) -* Thread-1 加轻量级锁失败,进入锁膨胀流程:为 Object 对象申请 Monitor 锁,通过 Object 对象头获取到持锁线程,将 Monitor 的 Owner 置为 Thread-0,将 Object 的对象头指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED +* Thread-1 加轻量级锁失败,进入锁膨胀流程:为 Object 对象申请 Monitor 锁,**通过 Object 对象头获取到持锁线程**,将 Monitor 的 Owner 置为 Thread-0,将 Object 的对象头指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED ![](https://gitee.com/seazean/images/raw/master/Java/JUC-重量级锁原理2.png) diff --git a/SSM.md b/SSM.md index b2b7e4c..45af664 100644 --- a/SSM.md +++ b/SSM.md @@ -6699,7 +6699,7 @@ MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ( **不支持当前事务**的情况: -- TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。 +- TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起 - 内外层是不同的事务,如果 bMethod 已经提交,如果 aMethod 失败回滚 ,bMethod 不会回滚 - 如果 bMethod 失败回滚,ServiceB 抛出的异常被 ServiceA 捕获,如果 B 抛出的异常是 A 会回滚的异常,aMethod 事务需要回滚,否则仍然可以提交 - TransactionDefinition.PROPAGATION_NOT_SUPPORTED: **以非事务方式运行**,如果当前存在事务,则把当前事务挂起 @@ -9942,6 +9942,8 @@ MVC(Model View Controller),一种用于设计创建Web应用程序表现 ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-MVC功能图示.png) + + 参考视频:https://space.bilibili.com/37974444/ @@ -11436,6 +11438,8 @@ SpringMVC 提供访问原始 Servlet 接口的功能 #### 组件介绍 +核心组件: + * DispatcherServlet:核心控制器, 是 SpringMVC 的核心,整体流程控制的中心,所有的请求第一步都先到达这里,由其调用其它组件处理用户的请求,它就是在 web.xml 配置的核心 Servlet,有效的降低了组件间的耦合性 * HandlerMapping:处理器映射器, 负责根据请求找到对应具体的 Handler 处理器,SpringMVC 中针对配置文件方式、注解方式等提供了不同的映射器来处理 @@ -11450,6 +11454,14 @@ SpringMVC 提供访问原始 Servlet 接口的功能 ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-技术架构.png) +优点: + +* 与 Spring 集成,更好的管理资源 +* 有很多参数解析器和视图解析器,支持的数据类型丰富 +* 将映射器、处理器、视图解析器进行解耦,分工明确 + + + **** From 9cf39ea38dcc99a72968b4a0afb0b178d1070f79 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 29 Mar 2022 22:12:15 +0800 Subject: [PATCH 09/51] Update Java Note --- DB.md | 861 ++++++++++++++++++++++++++++++++++++++----------------- Frame.md | 118 ++++---- Java.md | 222 +++++++------- Prog.md | 202 ++++++------- SSM.md | 106 +++---- Tool.md | 92 +++--- Web.md | 146 +++++----- 7 files changed, 1035 insertions(+), 712 deletions(-) diff --git a/DB.md b/DB.md index e9b0d2c..e62fc47 100644 --- a/DB.md +++ b/DB.md @@ -260,11 +260,11 @@ mysqldump -uroot -p2143 -T /tmp test city * 备份 - ![图形化界面备份](https://gitee.com/seazean/images/raw/master/DB/图形化界面备份.png) + ![图形化界面备份](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/图形化界面备份.png) * 恢复 - ![图形化界面恢复](https://gitee.com/seazean/images/raw/master/DB/图形化界面恢复.png) + ![图形化界面恢复](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/图形化界面恢复.png) @@ -362,7 +362,7 @@ mysqlshow -uroot -p1234 test book --count - 数据存储层,主要是将数据存储在文件系统之上,并完成与存储引擎的交互 - File System:文件系统,保存配置文件、数据文件、日志文件、错误文件、二进制文件等 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-体系结构.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-体系结构.png) @@ -370,12 +370,10 @@ mysqlshow -uroot -p1234 test book --count -### 执行流程 +### 建立连接 #### 连接器 -##### 连接原理 - 池化技术:对于访问数据库来说,建立连接的代价是比较昂贵的,因为每个连接对应一个用来交互的线程,频繁的创建关闭连接比较耗费资源,有必要建立数据库连接池,以提高访问的性能 连接建立 TCP 以后需要做**权限验证**,验证成功后可以进行执行 SQL。如果这时管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限,只有再新建的连接才会使用新的权限设置 @@ -384,7 +382,7 @@ MySQL 服务器可以同时和多个客户端进行交互,所以要保证每 整体的执行流程: - + @@ -392,7 +390,7 @@ MySQL 服务器可以同时和多个客户端进行交互,所以要保证每 -##### 连接状态 +#### 连接状态 客户端如果长时间没有操作,连接器就会自动断开,时间是由参数 wait_timeout 控制的,默认值是 8 小时。如果在连接被断开之后,客户端**再次发送请求**的话,就会收到一个错误提醒:`Lost connection to MySQL server during query` @@ -403,14 +401,14 @@ MySQL 服务器可以同时和多个客户端进行交互,所以要保证每 * 定期断开长连接,使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连 ```mysql - kill connection id + KILL CONNECTION id ``` * MySQL 5.7 版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源,这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态 SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 SQL 的执行情况,其中的 Command 列显示为 Sleep 的这一行,就表示现在系统里面有一个空闲连接 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SHOW_PROCESSLIST命令.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SHOW_PROCESSLIST命令.png) | 参数 | 含义 | | ------- | ------------------------------------------------------------ | @@ -423,12 +421,18 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 | State | 显示使用当前连接的 sql 语句的状态,以查询为例,需要经过 copying to tmp table、sorting result、sending data等状态才可以完成 | | Info | 显示执行的 sql 语句,是判断问题语句的一个重要依据 | +**Sending data 状态**表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅只是返回给客户端,是处于执行器过程中的任意阶段。由于在 Sending data 状态下,MySQL 线程需要做大量磁盘读取操作,所以是整个查询中耗时最长的状态。 + *** + + +### 执行流程 + #### 查询缓存 ##### 工作流程 @@ -493,7 +497,7 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 SHOW STATUS LIKE 'Qcache%'; ``` - + | 参数 | 含义 | | ----------------------- | ------------------------------------------------------------ | @@ -666,11 +670,11 @@ MySQL 在真正执行语句之前,并不能精确地知道满足条件的记 #### 引擎层 -Server 层和存储引擎层的交互是**以记录为单位的**,存储引擎会将符合条件的单条记录返回给 Server 层做进一步处理,并不是直接返回所有的记录 +Server 层和存储引擎层的交互是**以记录为单位的**,存储引擎会将单条记录返回给 Server 层做进一步处理,并不是直接返回所有的记录 工作流程: -* 首先根据二级索引选择扫描范围,获取第一条符合条件的记录,进行回表查询,将聚簇索引的记录返回 Server 层 +* 首先根据二级索引选择扫描范围,获取第一条符合二级索引条件的记录,进行回表查询,将聚簇索引的记录返回 Server 层,由 Server 判断记录是否符合要求 * 然后在二级索引上继续扫描下一个符合条件的记录 @@ -685,6 +689,62 @@ Server 层和存储引擎层的交互是**以记录为单位的**,存储引擎 +### 终止流程 + +#### 终止语句 + +终止线程中正在执行的语句: + +```mysql +KILL QUERY thread_id +``` + +KILL 不是马上终止的意思,而是告诉执行线程这条语句已经不需要继续执行,可以开始执行停止的逻辑(类似于打断)。因为对表做增删改查操作,会在表上加 MDL 读锁,如果线程被 KILL 时就直接终止,那这个 MDL 读锁就没机会被释放了 + +命令 `KILL QUERYthread_id_A` 的执行流程: + +* 把 session A 的运行状态改成 THD::KILL_QUERY(将变量 killed 赋值为 THD::KILL_QUERY) +* 给 session A 的执行线程发一个信号,让 session A 来处理这个 THD::KILL_QUERY 状态 + +会话处于等待状态(锁阻塞),必须满足是一个可以被唤醒的等待,必须有机会去**判断线程的状态**,如果不满足就会造成 KILL 失败 + +典型场景:innodb_thread_concurrency 为 2,代表并发线程上限数设置为 2 + +* session A 执行事务,session B 执行事务,达到线程上限;此时 session C 执行事务会阻塞等待,session D 执行 kill query C 无效 +* C 的逻辑是每 10 毫秒判断是否可以进入 InnoDB 执行,如果不行就调用 nanosleep 函数进入 sleep 状态,没有去判断线程状态 + +补充:执行 Ctrl+C 的时候,是 MySQL 客户端另外启动一个连接,然后发送一个 KILL QUERY 命令 + + + +*** + + + +#### 终止连接 + +断开线程的连接: + +```mysql +KILL CONNECTION id +``` + +断开连接后执行 SHOW PROCESSLIST 命令,如果这条语句的 Command 列显示 Killed,代表线程的状态是 KILL_CONNECTION,说明这个线程有语句正在执行,当前状态是停止语句执行中,终止逻辑耗时较长 + +* 超大事务执行期间被 KILL,这时回滚操作需要对事务执行期间生成的所有新数据版本做回收操作,耗时很长 +* 大查询回滚,如果查询过程中生成了比较大的临时文件,删除临时文件可能需要等待 IO 资源,导致耗时较长 +* DDL 命令执行到最后阶段被 KILL,需要删除中间过程的临时文件,也可能受 IO 资源影响耗时较久 + +总结:KILL CONNECTION 本质上只是把客户端的 SQL 连接断开,后面的终止流程还是要走 KILL QUERY + +一个事务被 KILL 之后,持续处于回滚状态,不应该强行重启整个 MySQL 进程,应该等待事务自己执行完成,因为重启后依然继续做回滚操作的逻辑 + + + +**** + + + ## 单表操作 @@ -723,7 +783,7 @@ Server 层和存储引擎层的交互是**以记录为单位的**,存储引擎 - 用来定义数据库的访问权限和安全级别,及创建用户。关键字:grant, revoke等 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL分类.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL分类.png) @@ -1270,7 +1330,7 @@ LIMIT SELECT * FROM product WHERE NAME LIKE '%电脑%'; ``` - + @@ -1546,7 +1606,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 SELECT * FROM product LIMIT 6,2; -- 第四页 开始索引=(4-1) * 2 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-DQL分页查询图解.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-DQL分页查询图解.png) @@ -1875,7 +1935,7 @@ CREATE TABLE card( INSERT INTO card VALUES (NULL,'12345',1),(NULL,'56789',2); ``` -![](https://gitee.com/seazean/images/raw/master/DB/多表设计一对一.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/多表设计一对一.png) @@ -1909,7 +1969,7 @@ CREATE TABLE orderlist( INSERT INTO orderlist VALUES (NULL,'hm001',1),(NULL,'hm002',1),(NULL,'hm003',2),(NULL,'hm004',2); ``` -![多表设计一对多](https://gitee.com/seazean/images/raw/master/DB/多表设计一对多.png) +![多表设计一对多](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/多表设计一对多.png) @@ -1952,7 +2012,7 @@ CREATE TABLE stu_course( INSERT INTO stu_course VALUES (NULL,1,1),(NULL,1,2),(NULL,2,1),(NULL,2,2); ``` -![](https://gitee.com/seazean/images/raw/master/DB/多表设计多对多.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/多表设计多对多.png) @@ -1962,55 +2022,27 @@ INSERT INTO stu_course VALUES (NULL,1,1),(NULL,1,2),(NULL,2,1),(NULL,2,2); ### 连接查询 -#### 连接原理 - -连接查询的是两张表有交集的部分数据,如果结果集中的每条记录都是两个表相互匹配的组合,则称这样的结果集为笛卡尔积 - -查询原理:两张表分为驱动表和被驱动表,首先查询驱动表得到数据集,然后根据数据集中的每一条记录的**关联字段再分别**到被驱动表中查找匹配,所以驱动表只需要访问一次,被驱动表要访问多次 - -MySQL 将查询驱动表后得到的记录成为驱动表的扇出,连接查询的成本:单次访问驱动表的成本 + 扇出值 * 单次访问被驱动表的成本,优化器会选择成本最小的表连接顺序(确定谁是驱动表,谁是被驱动表)生成执行计划,进行连接查询,优化方式: - -* 减少驱动表的扇出 -* 降低访问被驱动表的成本 - -MySQL 提出了一种**空间换时间**的优化方式,基于块的循环连接,执行连接查询前申请一块固定大小的内存作为连接缓冲区 Join Buffer,先把若干条驱动表中的扇出暂存在缓冲区,每一条被驱动表中的记录一次性的与 Buffer 中多条记录进行匹配(可能是一对多),因为是在内存中完成,所以速度快,并且降低了 I/O 成本。 - -Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 256 KB - -在成本分析时,对于很多张表的连接查询,连接顺序有非常多,MySQL 如果挨着进行遍历计算成本,会消耗很多资源 - -* 提前结束某种连接顺序的成本评估:维护一个全局变量记录当前成本最小的连接方式,如果一种顺序只计算了一部分就已经超过了最小成本,可以提前结束计算 -* 系统变量 optimizer_search_depth:如果连接表的个数小于该变量,就继续穷举分析每一种连接数量,反之只对数量与 depth 值相同的表进行分析,该值越大成本分析的越精确 - -* 系统变量 optimizer_prune_level:控制启发式规则的启用,这些规则就是根据以往经验指定的,不满足规则的连接顺序不分析成本 - - - - - -*** - - - #### 内外连接 ##### 内连接 +连接查询的是两张表有交集的部分数据,两张表分为**驱动表和被驱动表**,如果结果集中的每条记录都是两个表相互匹配的组合,则称这样的结果集为笛卡尔积 + 内连接查询,若驱动表中的记录在被驱动表中找不到匹配的记录时,则该记录不会加到最后的结果集 -* 显式内连接 +* 显式内连接: ```mysql SELECT 列名 FROM 表名1 [INNER] JOIN 表名2 ON 条件; ``` -* 隐式内连接 +* 隐式内连接:内连接中 WHERE 子句和 ON 子句是等价的 ```mysql SELECT 列名 FROM 表名1,表名2 WHERE 条件; ``` -内连接中 WHERE 子句和 ON 子句是等价的 +STRAIGHT_JOIN与 JOIN 类似,只不过左表始终在右表之前读取,只适用于内连接 @@ -2023,7 +2055,7 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 外连接查询,若驱动表中的记录在被驱动表中找不到匹配的记录时,则该记录也会加到最后的结果集,只是对于被驱动表中**不匹配过滤条件**的记录,各个字段使用 NULL 填充 -应用实例:查学生成绩,也想查出缺考的人的成绩 +应用实例:查学生成绩,也想展示出缺考的人的成绩 * 左外连接:选择左侧的表为驱动表,查询左表的全部数据,和左右两张表有交集部分的数据 @@ -2037,7 +2069,7 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 SELECT 列名 FROM 表名1 RIGHT [OUTER] JOIN 表名2 ON 条件; ``` -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-JOIN查询图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-JOIN查询图.png) @@ -2074,7 +2106,7 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 (1009,'宋江',NULL,16000.00); ``` - ![](https://gitee.com/seazean/images/raw/master/DB/自关联查询数据准备.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/自关联查询数据准备.png) * 数据查询 @@ -2117,6 +2149,100 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 +*** + + + +#### 连接原理 + +Index Nested-Loop Join 算法:查询驱动表得到**数据集**,然后根据数据集中的每一条记录的**关联字段再分别**到被驱动表中查找匹配(**走索引**),所以驱动表只需要访问一次,被驱动表要访问多次 + +MySQL 将查询驱动表后得到的记录成为驱动表的扇出,连接查询的成本:单次访问驱动表的成本 + 扇出值 * 单次访问被驱动表的成本,优化器会选择成本最小的表连接顺序(确定谁是驱动表,谁是被驱动表)生成执行计划,进行连接查询,优化方式: + +* 减少驱动表的扇出(让数据量小的表来做驱动表) +* 降低访问被驱动表的成本 + +说明:STRAIGHT_JOIN 是查一条驱动表,然后根据关联字段去查被驱动表,要访问多次驱动表,所以需要优化为 INL 算法 + +Block Nested-Loop Join 算法:一种**空间换时间**的优化方式,基于块的循环连接,执行连接查询前申请一块固定大小的内存作为连接缓冲区 Join Buffer,先把若干条驱动表中的扇出暂存在缓冲区,每一条被驱动表中的记录一次性的与 Buffer 中多条记录进行匹配(扫描全部数据,一条一条的匹配),因为是在内存中完成,所以速度快,并且降低了 I/O 成本 + +Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 256 KB + +在成本分析时,对于很多张表的连接查询,连接顺序有非常多,MySQL 如果挨着进行遍历计算成本,会消耗很多资源 + +* 提前结束某种连接顺序的成本评估:维护一个全局变量记录当前成本最小的连接方式,如果一种顺序只计算了一部分就已经超过了最小成本,可以提前结束计算 +* 系统变量 optimizer_search_depth:如果连接表的个数小于该变量,就继续穷举分析每一种连接数量,反之只对数量与 depth 值相同的表进行分析,该值越大成本分析的越精确 + +* 系统变量 optimizer_prune_level:控制启发式规则的启用,这些规则就是根据以往经验指定的,不满足规则的连接顺序不分析成本 + + + +*** + + + +#### 连接优化 + +##### BKA + +Batched Key Access 算法是对 NLJ 算法的优化,在读取被驱动表的记录时使用顺序 IO,Extra 信息中会有 Batched Key Access 信息 + +使用 BKA 的表的 JOIN 过程如下: + +* 连接驱动表将满足条件的记录放入 Join Buffer,并将两表连接的字段放入一个 DYNAMIC_ARRAY ranges 中 +* 在进行表的过接过程中,会将 ranges 相关的信息传入 Buffer 中,进行被驱动表主建的查找及排序操作 +* 调用步骤 2 中产生的有序主建,**顺序读取被驱动表的数据** +* 当缓冲区的数据被读完后,会重复进行步骤 2、3,直到记录被读取完 + +使用 BKA 优化需要设进行设置: + +```mysql +SET optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on'; +``` + +说明:前两个参数的作用是启用 MRR,因为 BKA 算法的优化要依赖于 MRR(系统优化 → 内存优化 → Read 详解) + + + +*** + + + +##### BNL + +###### 问题 + +BNL 即 Block Nested-Loop Join 算法,由于要访问多次被驱动表,会产生两个问题: + +* Join 语句多次扫描一个冷表,并且语句执行时间小于 1 秒,就会在再次扫描冷表时,把冷表的数据页移到 LRU 链表头部,导致热数据被淘汰,影响业务的正常运行 + + 这种情况冷表的数据量要小于整个 Buffer Pool 的 old 区域,能够完全放入 old 区,才会再次被读时加到 young,否则读取下一段时就已经把上一段淘汰 + +* Join 语句在循环读磁盘和淘汰内存页,进入 old 区域的数据页很可能在 1 秒之内就被淘汰,就会导致 MySQL 实例的 Buffer Pool 在这段时间内 young 区域的数据页没有被合理地淘汰 + +大表 Join 操作虽然对 IO 有影响,但是在语句执行结束后对 IO 的影响随之结束。但是对 Buffer Pool 的影响就是持续性的,需要依靠后续的查询请求慢慢恢复内存命中率 + + + +###### 优化 + +将 BNL 算法转成 BKA 算法,优化方向: + +* 在被驱动表上建索引,这样就可以根据索引进行顺序 IO +* 使用临时表,**在临时表上建立索引**,将被驱动表和临时表进行连接查询 + +驱动表 t1,被驱动表 t2,使用临时表的工作流程: + +* 把表 t1 中满足条件的数据放在临时表 tmp_t 中 +* 给临时表 tmp_t 的关联字段加上索引,使用 BKA 算法 +* 让表 t2 和 tmp_t 做 Join 操作(临时表是被驱动表) + +补充:MySQL 8.0 支持 hash join,join_buffer 维护的不再是一个无序数组,而是一个哈希表,查询效率更高,执行效率比临时表更高 + + + + + *** @@ -2174,8 +2300,8 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 系统变量 tmp_table_size 或者 max_heap_table_size 为表的最值 -* 小于系统变量时,内存中可以保存,会为建立基于内存的 MEMORY 存储引擎的临时表,并建立哈希索引 -* 大于任意一个系统变量时,物化表会使用基于磁盘的 InnoDB 存储引擎来保存结果集中的记录,索引类型为 B+ 树 +* 小于系统变量时,内存中可以保存,会为建立**基于内存**的 MEMORY 存储引擎的临时表,并建立哈希索引 +* 大于任意一个系统变量时,物化表会使用**基于磁盘**的 InnoDB 存储引擎来保存结果集中的记录,索引类型为 B+ 树 物化后,嵌套查询就相当于外层查询的表和物化表进行内连接查询,然后经过优化器选择成本最小的表连接顺序执行查询 @@ -2194,6 +2320,32 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 +#### 联合查询 + +UNION 是取这两个子查询结果的并集,并进行去重,同时进行默认规则的排序(union 是行加起来,join 是列加起来) + +UNION ALL 是对两个结果集进行并集操作不进行去重,不进行排序 + +```mysql +(select 1000 as f) union (select id from t1 order by id desc limit 2); #t1表中包含id 为 1-1000 的数据 +``` + +语句的执行流程: + +* 创建一个内存临时表,这个临时表只有一个整型字段 f,并且 f 是主键字段 +* 执行第一个子查询,得到 1000 这个值,并存入临时表中 +* 执行第二个子查询,拿到第一行 id=1000,试图插入临时表中,但由于 1000 这个值已经存在于临时表了,违反了唯一性约束,所以插入失败,然后继续执行 +* 取到第二行 id=999,插入临时表成功 +* 从临时表中按行取出数据,返回结果并删除临时表,结果中包含两行数据分别是 1000 和 999 + + + + + +**** + + + ### 查询练习 数据准备: @@ -2243,17 +2395,18 @@ CREATE TABLE us_pro( ); ``` -![多表练习架构设计](https://gitee.com/seazean/images/raw/master/DB/多表练习架构设计.png) +![多表练习架构设计](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/多表练习架构设计.png) **数据查询:** -1. 查询用户的编号、姓名、年龄、订单编号。 - 分析: - 数据:用户的编号、姓名、年龄在 user 表,订单编号在 orderlist 表 - 条件:user.id = orderlist.uid - +1. 查询用户的编号、姓名、年龄、订单编号 + + 数据:用户的编号、姓名、年龄在 user 表,订单编号在 orderlist 表 + + 条件:user.id = orderlist.uid + ```mysql SELECT u.*, @@ -2264,7 +2417,7 @@ CREATE TABLE us_pro( WHERE u.id = o.uid; ``` - + 2. 查询所有的用户,显示用户的编号、姓名、年龄、订单编号。 ```mysql @@ -2279,7 +2432,7 @@ CREATE TABLE us_pro( u.id = o.uid; ``` -3. 查询用户年龄大于 23 岁的信息,显示用户的编号、姓名、年龄、订单编号。 +3. 查询用户年龄大于 23 岁的信息,显示用户的编号、姓名、年龄、订单编号 ```mysql SELECT @@ -2320,8 +2473,10 @@ CREATE TABLE us_pro( u.name IN ('张三','李四'); ```` -5. 查询所有的用户和该用户能查看的所有的商品,显示用户的编号、姓名、年龄、商品名称。 - 数据:用户的编号、姓名、年龄在user表,商品名称在product表,中间表 us_pro +5. 查询所有的用户和该用户能查看的所有的商品,显示用户的编号、姓名、年龄、商品名称 + + 数据:用户的编号、姓名、年龄在 user 表,商品名称在 product 表,中间表 us_pro + 条件:us_pro.uid = user.id AND us_pro.pid = product.id ```mysql @@ -3418,7 +3573,7 @@ InnoDB 存储引擎:(MySQL5.5 版本后默认的存储引擎) MEMORY 存储引擎: -- 特点:每个 MEMORY 表实际对应一个磁盘文件 ,该文件中只存储表的结构,表数据保存在内存中,且默认**使用 HASH 索引**,这样有利于数据的快速处理,在需要快速定位记录可以提供更快的访问,但是服务一旦关闭,表中的数据就会丢失,数据存储不安全 +- 特点:每个 MEMORY 表实际对应一个磁盘文件 ,该文件中只存储表的结构,表数据保存在内存中,且默认**使用 HASH 索引**,这样有利于数据的快速处理,在需要快速定位记录可以提供更快的访问,但是**服务一旦关闭,表中的数据就会丢失**,存储不安全 - 应用场景:通常用于更新不太频繁的小表,用以快速得到访问结果,类似缓存 - 存储方式:表结构保存在 .frm 中 @@ -3448,7 +3603,7 @@ MERGE 存储引擎: )ENGINE = MERGE UNION = (order_1,order_2) INSERT_METHOD=LAST DEFAULT CHARSET=utf8; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MERGE.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MERGE.png) | 特性 | MyISAM | InnoDB | MEMORY | | ------------ | ------------------------------ | ------------- | -------------------- | @@ -3468,7 +3623,7 @@ MERGE 存储引擎: | 批量插入速度 | 高 | 低 | 高 | | **外键** | **不支持** | **支持** | **不支持** | -面试问题:MyIsam 和 InnoDB 的区别? +MyISAM 和 InnoDB 的区别? * 事务:InnoDB 支持事务,MyISAM 不支持事务 * 外键:InnoDB 支持外键,MyISAM 不支持外键 @@ -3543,7 +3698,7 @@ MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获 **索引是在存储引擎层实现的**,所以并没有统一的索引标准,即不同存储引擎的索引的工作方式并不一样 索引使用:一张数据表,用于保存数据;一个索引配置文件,用于保存索引;每个索引都指向了某一个数据 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引的介绍.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-索引的介绍.png) 左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的)。为了加快 Col2 的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据的物理地址的指针,这样就可以运用二叉查找快速获取到相应数据 @@ -3592,7 +3747,7 @@ MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获 联合索引图示:根据身高年龄建立的组合索引(height,age) -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-组合索引图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-组合索引图.png) @@ -3758,7 +3913,7 @@ InnoDB 使用 B+Tree 作为索引结构,并且 InnoDB 一定有索引 * InnoDB 表是基于聚簇索引建立的,因此 InnoDB 的索引能提供一种非常快速的主键查找性能。不过辅助索引也会包含主键列,所以不建议使用过长的字段作为主键,**过长的主索引会令辅助索引变得过大** -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB聚簇和辅助索引结构.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB聚簇和辅助索引结构.png) @@ -3775,7 +3930,7 @@ MyISAM 的主键索引使用的是非聚簇索引,索引文件和数据文件 * 主键索引 B+ 树的节点存储了主键,辅助键索引 B+ 树存储了辅助键,表数据存储在独立的地方,这两颗 B+ 树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别 * 由于索引树是独立的,通过辅助索引检索**无需回表查询**访问主键的索引树 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-聚簇索引和辅助索引检锁数据图.jpg) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-聚簇索引和辅助索引检锁数据图.jpg) @@ -3791,7 +3946,7 @@ MyISAM 的索引方式也叫做非聚集的,之所以这么称呼是为了与 辅助索引:MyISAM 中主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求 key 是唯一的,而辅助索引的 key 可以重复 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM主键和辅助索引结构.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM主键和辅助索引结构.png) @@ -3855,40 +4010,40 @@ BTree 又叫多路平衡搜索树,一颗 m 叉的 BTree 特性如下: * 插入前 4 个字母 C N G A - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程1.png) * 插入 H,n>4,中间元素 G 字母向上分裂到新的节点 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程2.png) * 插入 E、K、Q 不需要分裂 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程3.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程3.png) * 插入 M,中间元素 M 字母向上分裂到父节点 G - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程4.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程4.png) * 插入 F,W,L,T 不需要分裂 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程5.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程5.png) * 插入 Z,中间元素 T 向上分裂到父节点中 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程6.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程6.png) * 插入 D,中间元素 D 向上分裂到父节点中,然后插入 P,R,X,Y 不需要分裂 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程7.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程7.png) * 最后插入 S,NPQR 节点 n>5,中间节点 Q 向上分裂,但分裂后父节点 DGMT 的 n>5,中间节点 M 向上分裂 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程8.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-BTree工作流程8.png) BTree 树就已经构建完成了,BTree 树和二叉树相比, 查询数据的效率更高, 因为对于相同的数据量来说,**BTree 的层级结构比二叉树少**,所以搜索速度快 BTree 结构的数据可以让系统高效的找到数据所在的磁盘块,定义一条记录为一个二元组 [key, data] ,key 为记录的键值,对应表中的主键值,data 为一行记录中除主键外的数据。对于不同的记录,key 值互不相同,BTree 中的每个节点根据实际情况可以包含大量的关键字信息和分支 -![](https://gitee.com/seazean/images/raw/master/DB/索引的原理1.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/索引的原理1.png) 缺点:当进行范围查找时会出现回旋查找 @@ -3913,7 +4068,7 @@ B+Tree 为 BTree 的变种,B+Tree 与 BTree 的区别为: - **叶子节点按照 key 大小顺序排列,左边结尾数据都会保存右边节点开始数据的指针,形成一个链表** - 所有节点中的 key 在叶子节点中也存在(比如 5),**key 允许重复**,B 树不同节点不存在重复的 key - + B* 树:是 B+ 树的变体,在 B+ 树的非根和非叶子结点再增加指向兄弟的指针 @@ -3931,7 +4086,7 @@ MySQL 索引数据结构对经典的 B+Tree 进行了优化,在原 B+Tree 的 B+ 树的**叶子节点是数据页**(page),一个页里面可以存多个数据行 -![](https://gitee.com/seazean/images/raw/master/DB/索引的原理2.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/索引的原理2.png) 通常在 B+Tree 上有两个头指针,**一个指向根节点,另一个指向关键字最小的叶子节点**,而且所有叶子节点(即数据节点)之间是一种链式环结构。可以对 B+Tree 进行两种查找运算: @@ -4065,10 +4220,10 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 * 不使用索引下推优化时存储引擎通过索引检索到数据,然后回表查询记录返回给 Server 层,**服务器判断数据是否符合条件** - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-不使用索引下推.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-不使用索引下推.png) * 使用索引下推优化时,如果**存在某些被索引的列的判断条件**时,由存储引擎在索引遍历的过程中判断数据是否符合传递的条件,将符合条件的数据进行回表,检索出来返回给服务器,由此减少 IO 次数 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-使用索引下推.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-使用索引下推.png) **适用条件**: @@ -4084,10 +4239,10 @@ SELECT * FROM user WHERE name LIKE '张%' AND age = 10; -- 头部模糊匹配 * 优化前:在非主键索引树上找到满足第一个条件的行,然后通过叶子节点记录的主键值再回到主键索引树上查找到对应的行数据,再对比 AND 后的条件是否符合,符合返回数据,需要 4 次回表 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引下推优化1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-索引下推优化1.png) * 优化后:检查索引中存储的列信息是否符合索引条件,然后交由存储引擎用剩余的判断条件判断此行数据是否符合要求,**不满足条件的不去读取表中的数据**,满足下推条件的就根据主键值进行回表查询,2 次回表 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引下推优化2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-索引下推优化2.png) 当使用 EXPLAIN 进行分析时,如果使用了索引条件下推,Extra 会显示 Using index condition @@ -4183,6 +4338,124 @@ CREATE INDEX idx_area ON table_name(area(7)); ## 系统优化 +### 表优化 + +#### 临时表 + +临时表分为内部临时表和用户临时表 + +* 内部临时表:系统执行 SQL 语句优化时产生的表,例如 Join 连接查询、去重查询等 + +* 用户临时表:用户主动创建的临时表 + + ```mysql + CREATE TEMPORARY TABLE temp_t like table_1; + ``` + +临时表可以是内存表,也可以是磁盘表(多表操作 → 嵌套查询章节提及) + +* 内存表指的是使用 Memory 引擎的表,建立哈希索引,建表语法是 `create table … engine=memory`,这种表的数据都保存在内存里,系统重启时会被清空,但是表结构还在 +* 磁盘表是使用 InnoDB 引擎或者 MyISAM 引擎的临时表,建立 B+ 树索引,写数据的时候是写到磁盘上的 + +临时表的特点: + +* 一个临时表只能被创建它的 session 访问,对其他线程不可见,所以不同 session 的临时表是**可以重名**的 +* 临时表可以与普通表同名,会话内有同名的临时表和普通表时,执行 show create 语句以及增删改查语句访问的都是临时表 +* show tables 命令不显示临时表 +* 数据库发生异常重启不需要担心数据删除问题,临时表会**自动回收** + + + +*** + + + +#### 重名原理 + +执行创建临时表的 SQL: + +```mysql +create temporary table temp_t(id int primary key)engine=innodb; +``` + +MySQL 给 InnoDB 表创建一个 frm 文件保存表结构定义,在 ibd 保存表数据。frm 文件放在临时文件目录下,文件名的后缀是 .frm,**前缀是** `#sql{进程 id}_{线程 id}_ 序列号`,使用 `select @@tmpdir` 命令,来显示实例的临时文件目录 + +MySQL 维护数据表,除了物理磁盘上的文件外,内存里也有一套机制区别不同的表,每个表都对应一个 table_def_key + +* 一个普通表的 table_def_key 的值是由 `库名 + 表名` 得到的,所以如果在同一个库下创建两个同名的普通表,创建第二个表的过程中就会发现 table_def_key 已经存在了 +* 对于临时表,table_def_key 在 `库名 + 表名` 基础上,又加入了 `server_id + thread_id`,所以不同线程之间,临时表可以重名 + +实现原理:每个线程都维护了自己的临时表链表,每次 session 内操作表时,先遍历链表,检查是否有这个名字的临时表,如果有就**优先操作临时表**,如果没有再操作普通表;在 session 结束时对链表里的每个临时表,执行 `DROP TEMPORARY TABLE + 表名` 操作 + +执行 rename table 语句无法修改临时表,因为会按照 `库名 / 表名.frm` 的规则去磁盘找文件,但是临时表文件名的规则是 `#sql{进程 id}_{线程 id}_ 序列号.frm`,因此会报找不到文件名的错误 + + + +**** + + + +#### 基本应用 + +##### 主备复制 + +创建临时表的语句会传到备库执行,因此备库的同步线程就会创建这个临时表。主库在线程退出时会自动删除临时表,但备库同步线程是持续在运行的并不会退出,所以这时就需要在主库上再写一个 DROP TEMPORARY TABLE 传给备库执行 + +binlog 日志写入规则: + +* binlog_format=row,跟临时表有关的语句就不会记录到 binlog +* binlog_format=statment/mixed,binlog 中才会记录临时表的操作,也就会记录 `DROP TEMPORARY TABLE` 这条命令 + +主库上不同的线程创建同名的临时表是不冲突的,但是备库只有一个执行线程,所以 MySQL 在记录 binlog 时会把主库执行这个语句的线程 id 写到 binlog 中,在备库的应用线程就可以获取执行每个语句的主库线程 id,并利用这个线程 id 来构造临时表的 table_def_key + +* session A 的临时表 t1,在备库的 table_def_key 就是:`库名 + t1 +“M 的 serverid" + "session A 的 thread_id”` +* session B 的临时表 t1,在备库的 table_def_key 就是 :`库名 + t1 +"M 的 serverid" + "session B 的 thread_id"` + +MySQL 在记录 binlog 的时不论是 create table 还是 alter table 语句都是原样记录,但是如果执行 drop table,系统记录 binlog 就会被服务端改写 + +```mysql +DROP TABLE `t_normal` /* generated by server */ +``` + + + +*** + + + +##### 跨库查询 + +分库分表系统的跨库查询使用临时表不用担心线程之间的重名冲突,分库分表就是要把一个逻辑上的大表分散到不同的数据库实例上 + +比如将一个大表 ht,按照字段 f,拆分成 1024 个分表,分布到 32 个数据库实例上,一般情况下都有一个中间层 proxy 解析 SQL 语句,通过分库规则通过分表规则(比如 N%1024)确定将这条语句路由到哪个分表做查询 + +```mysql +select v from ht where f=N; +``` + +如果这个表上还有另外一个索引 k,并且查询语句: + +```mysql +select v from ht where k >= M order by t_modified desc limit 100; +``` + +查询条件里面没有用到分区字段 f,只能**到所有的分区**中去查找满足条件的所有行,然后统一做 order by 操作,两种方式: + +* 在 proxy 层的进程代码中实现排序,拿到分库的数据以后,直接在内存中参与计算,但是对 proxy 端的压力比较大,很容易出现内存不够用和 CPU 瓶颈问题 +* 把各个分库拿到的数据,汇总到一个 MySQL 实例的一个表中,然后在这个汇总实例上做逻辑操作,执行流程: + * 在汇总库上创建一个临时表 temp_ht,表里包含三个字段 v、k、t_modified + * 在各个分库执行:`select v,k,t_modified from ht_x where k >= M order by t_modified desc limit 100` + * 把分库执行的结果插入到 temp_ht 表中 + * 在临时表上执行:`select v from temp_ht order by t_modified desc limit 100` + + + + + +*** + + + ### 优化步骤 #### 执行频率 @@ -4205,7 +4478,7 @@ SHOW [SESSION|GLOBAL] STATUS LIKE ''; Com_xxx 表示每种语句执行的次数 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL语句执行频率.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL语句执行频率.png) * 查询 SQL 语句影响的行数: @@ -4213,7 +4486,7 @@ SHOW [SESSION|GLOBAL] STATUS LIKE ''; SHOW STATUS LIKE 'Innodb_rows_%'; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL语句影响的行数.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL语句影响的行数.png) Com_xxxx:这些参数对于所有存储引擎的表操作都会进行累计 @@ -4278,7 +4551,7 @@ SQL 执行慢有两种情况: * SHOW PROCESSLIST:**实时查看**当前 MySQL 在进行的连接线程,包括线程的状态、是否锁表、SQL 的执行情况,同时对一些锁表操作进行优化 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SHOW_PROCESSLIST命令.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SHOW_PROCESSLIST命令.png) @@ -4301,7 +4574,7 @@ SQL 执行慢有两种情况: EXPLAIN SELECT * FROM table_1 WHERE id = 1; ``` -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain查询SQL语句的执行计划.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-explain查询SQL语句的执行计划.png) | 字段 | 含义 | | ------------- | ------------------------------------------------------------ | @@ -4331,7 +4604,7 @@ SHOW WARINGS:在使用 EXPALIN 命令后执行该语句,可以查询与执 环境准备: -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-执行计划环境准备.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-执行计划环境准备.png) @@ -4351,7 +4624,7 @@ id 代表 SQL 执行的顺序的标识,每个 SELECT 关键字对应一个唯 EXPLAIN SELECT * FROM t_role r, t_user u, user_role ur WHERE r.id = ur.role_id AND u.id = ur.user_id ; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain之id相同.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-explain之id相同.png) * id 不同时,id 值越大优先级越高,越先被执行 @@ -4359,7 +4632,7 @@ id 代表 SQL 执行的顺序的标识,每个 SELECT 关键字对应一个唯 EXPLAIN SELECT * FROM t_role WHERE id = (SELECT role_id FROM user_role WHERE user_id = (SELECT id FROM t_user WHERE username = 'stu1')) ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain之id不同.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-explain之id不同.png) * id 有相同也有不同时,id 相同的可以认为是一组,从上往下顺序执行;在所有的组中,id 的值越大的组,优先级越高,越先执行 @@ -4367,7 +4640,7 @@ id 代表 SQL 执行的顺序的标识,每个 SELECT 关键字对应一个唯 EXPLAIN SELECT * FROM t_role r , (SELECT * FROM user_role ur WHERE ur.`user_id` = '2') a WHERE r.id = a.role_id ; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain之id相同和不同.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-explain之id相同和不同.png) * id 为 NULL 时代表的是临时表 @@ -4484,11 +4757,11 @@ key_len: SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的**资源消耗**情况 * 通过 have_profiling 参数,能够看到当前 MySQL 是否支持 profile: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-have_profiling.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-have_profiling.png) * 默认 profiling 是关闭的,可以通过 set 语句在 Session 级别开启 profiling: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-profiling.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-profiling.png) ```mysql SET profiling=1; #开启profiling 开关; @@ -4500,7 +4773,7 @@ SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的** SHOW PROFILES; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查看SQL语句执行耗时.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-查看SQL语句执行耗时.png) * 查看到该 SQL 执行过程中每个线程的状态和消耗的时间: @@ -4508,13 +4781,11 @@ SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的** SHOW PROFILE FOR QUERY query_id; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL执行每个状态消耗的时间.png) - - **Sending data 状态**表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅只是返回给客户端。由于在 Sending data 状态下,MySQL 线程需要做大量磁盘读取操作,所以是整个查询中耗时最长的状态。 + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL执行每个状态消耗的时间.png) * 在获取到最消耗时间的线程状态后,MySQL 支持选择 all、cpu、block io 、context switch、page faults 等类型查看 MySQL 在使用什么资源上耗费了过高的时间。例如,选择查看 CPU 的耗费时间: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL执行每个状态消耗的CPU.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-SQL执行每个状态消耗的CPU.png) * Status:SQL 语句执行的状态 * Durationsql:执行过程中每一个步骤的耗时 @@ -4560,7 +4831,7 @@ MySQL 提供了对 SQL 的跟踪, 通过 trace 文件可以查看优化器** -### 索引失效 +### 索引优化 #### 创建索引 @@ -4581,7 +4852,7 @@ INSERT INTO `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, ` CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联合索引 ``` -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引环境准备.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引环境准备.png) @@ -4599,7 +4870,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市'; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引1.png) * **最左前缀法则**:联合索引遵守最左前缀法则 @@ -4610,7 +4881,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status='1'; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引2.png) 违法最左前缀法则 , 索引失效: @@ -4619,7 +4890,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 EXPLAIN SELECT * FROM tb_seller WHERE status='1' AND address='西安市'; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引3.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引3.png) 如果符合最左法则,但是出现跳跃某一列,只有最左列索引生效: @@ -4627,7 +4898,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND address='西安市'; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引4.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引4.png) 虽然索引列失效,但是系统会**使用了索引下推进行了优化** @@ -4639,7 +4910,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 根据前面的两个字段 name , status 查询是走索引的, 但是最后一个条件 address 没有用到索引,使用了索引下推 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引5.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引5.png) * 在索引列上**函数或者运算(+ - 数值)操作**, 索引将失效:会破坏索引值的有序性 @@ -4647,7 +4918,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 EXPLAIN SELECT * FROM tb_seller WHERE SUBSTRING(name,3,2) = '科技'; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引6.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引6.png) * **字符串不加单引号**,造成索引失效:隐式类型转换,当字符串和数字比较时会**把字符串转化为数字** @@ -4657,7 +4928,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status = 1; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引7.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引7.png) 如果 status 是 int 类型,SQL 为 `SELECT * FROM tb_seller WHERE status = '1' ` 并不会造成索引失效,因为会将 `'1'` 转换为 `1`,并**不会对索引列产生操作** @@ -4674,7 +4945,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' OR status='1'; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引10.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引10.png) **AND 分割的条件不影响**: @@ -4682,7 +4953,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 EXPLAIN SELECT * FROM tb_seller WHERE name='阿里巴巴' AND createtime = '2088-01-01 12:00:00'; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引11.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引11.png) * **以 % 开头的 LIKE 模糊查询**,索引失效: @@ -4692,7 +4963,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 EXPLAIN SELECT * FROM tb_seller WHERE name like '%科技%'; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引12.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引12.png) 解决方案:通过覆盖索引来解决 @@ -4700,7 +4971,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 EXPLAIN SELECT sellerid,name,status FROM tb_seller WHERE name like '%科技%'; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引13.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引13.png) 原因:在覆盖索引的这棵 B+ 数上只需要进行 like 的匹配,或者是基于覆盖索引查询再进行 WHERE 的判断就可以获得结果 @@ -4724,7 +4995,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 北京市的键值占 9/10(区分度低),所以优化为全表扫描,type = ALL - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引14.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引14.png) * IS NULL、IS NOT NULL **有时**索引失效: @@ -4735,7 +5006,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 NOT NULL 失效的原因是 name 列全部不是 null,优化为全表扫描,当 NULL 过多时,IS NULL 失效 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引15.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引15.png) * IN 肯定会走索引,但是当 IN 的取值范围较大时会导致索引失效,走全表扫描: @@ -4756,16 +5027,16 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联 索引失效一般是针对联合索引,联合索引一般由几个字段组成,排序方式是先按照第一个字段进行排序,然后排序第二个,依此类推,图示(a, b)索引,**a 相等的情况下 b 是有序的** - + * 最左前缀法则:当不匹配前面的字段的时候,后面的字段都是无序的。这种无序不仅体现在叶子节点,也会**导致查询时扫描的非叶子节点也是无序的**,因为索引树相当于忽略的第一个字段,就无法使用二分查找 * 范围查询右边的列,不能使用索引,比如语句: `WHERE a > 1 AND b = 1 `,在 a 大于 1 的时候,b 是无序的,a > 1 是扫描时有序的,但是找到以后进行寻找 b 时,索引树就不是有序的了 - + * 以 % 开头的 LIKE 模糊查询,索引失效,比如语句:`WHERE a LIKE '%d'`,前面的不确定,导致不符合最左匹配,直接去索引中搜索以 d 结尾的节点,所以没有顺序 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引失效底层原理3.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-索引失效底层原理3.png) @@ -4784,7 +5055,7 @@ SHOW STATUS LIKE 'Handler_read%'; SHOW GLOBAL STATUS LIKE 'Handler_read%'; ``` -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL查看索引使用情况.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL查看索引使用情况.png) * Handler_read_first:索引中第一条被读的次数,如果较高,表示服务器正执行大量全索引扫描(这个值越低越好) @@ -4818,7 +5089,7 @@ SHOW GLOBAL STATUS LIKE 'Handler_read%'; EXPLAIN SELECT name,status,address FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市'; ``` -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引8.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引8.png) 如果查询列,超出索引列,也会降低性能: @@ -4826,7 +5097,7 @@ EXPLAIN SELECT name,status,address FROM tb_seller WHERE name='小米科技' AND EXPLAIN SELECT name,status,address,password FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市'; ``` -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引9.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用索引9.png) @@ -4887,7 +5158,7 @@ EXPLAIN SELECT name,status,address,password FROM tb_seller WHERE name='小米科 当使用 load 命令导入数据的时候,适当的设置可以提高导入的效率: -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL load data.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL load data.png) ```mysql LOAD DATA LOCAL INFILE = '/home/seazean/sql1.log' INTO TABLE `tb_user_1` FIELD TERMINATED BY ',' LINES TERMINATED BY '\n'; -- 文件格式如上图 @@ -4901,21 +5172,21 @@ LOAD DATA LOCAL INFILE = '/home/seazean/sql1.log' INTO TABLE `tb_user_1` FIELD T * 插入 ID 顺序排列数据: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入ID顺序排列数据.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL插入ID顺序排列数据.png) * 插入 ID 无序排列数据: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入ID无序排列数据.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL插入ID无序排列数据.png) 2. **关闭唯一性校验**:在导入数据前执行 `SET UNIQUE_CHECKS=0`,关闭唯一性校验;导入结束后执行 `SET UNIQUE_CHECKS=1`,恢复唯一性校验,可以提高导入的效率。 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入数据关闭唯一性校验.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL插入数据关闭唯一性校验.png) 3. **手动提交事务**:如果应用使用自动提交的方式,建议在导入前执行`SET AUTOCOMMIT=0`,关闭自动提交;导入结束后再打开自动提交,可以提高导入的效率。 事务需要控制大小,事务太大可能会影响执行的效率。MySQL 有 innodb_log_buffer_size 配置项,超过这个值的日志会写入磁盘数据,效率会下降,所以在事务大小达到配置项数据级前进行事务提交可以提高效率 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入数据手动提交事务.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL插入数据手动提交事务.png) @@ -4945,7 +5216,7 @@ CREATE INDEX idx_emp_age_salary ON emp(age,salary); EXPLAIN SELECT * FROM emp ORDER BY age DESC; -- 年龄降序 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL ORDER BY排序1.png) * 第二种通过有序索引顺序扫描直接返回**有序数据**,这种情况为 Using index,不需要额外排序,操作效率高 @@ -4953,7 +5224,7 @@ CREATE INDEX idx_emp_age_salary ON emp(age,salary); EXPLAIN SELECT id, age, salary FROM emp ORDER BY age DESC; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL ORDER BY排序2.png) * 多字段排序: @@ -4963,7 +5234,7 @@ CREATE INDEX idx_emp_age_salary ON emp(age,salary); EXPLAIN SELECT id,age,salary FROM emp ORDER BY age DESC, salary ASC; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序3.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL ORDER BY排序3.png) 尽量减少额外的排序,通过索引直接返回有序数据。**需要满足 Order by 使用相同的索引、Order By 的顺序和索引顺序相同、Order by 的字段都是升序或都是降序**,否则需要额外的操作,就会出现 FileSort @@ -5011,7 +5282,7 @@ GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是 EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL GROUP BY排序1.png) Using temporary:表示 MySQL 需要使用临时表(不是 sort buffer)来存储结果集,常见于排序和分组查询 @@ -5021,15 +5292,17 @@ GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是 EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age ORDER BY NULL; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL GROUP BY排序2.png) -* 创建索引: +* 创建索引:索引本身有序,不需要临时表,也不需要再额外排序 ```mysql CREATE INDEX idx_emp_age_salary ON emp(age,salary); ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序3.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL GROUP BY排序3.png) + +* 数据量很大时,使用 SQL_BIG_RESULT 提示优化器直接使用直接用磁盘临时表 @@ -5047,7 +5320,7 @@ GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是 EXPLAIN SELECT * FROM emp WHERE id = 1 OR age = 30; -- 两个索引,并且不是复合索引 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL OR条件查询1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL OR条件查询1.png) ```sh Extra: Using sort_union(idx_emp_age_salary,PRIMARY); Using where @@ -5061,7 +5334,7 @@ GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是 EXPLAIN SELECT * FROM emp WHERE id = 1 UNION SELECT * FROM emp WHERE age = 30; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL OR条件查询2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL OR条件查询2.png) * UNION 要优于 OR 的原因: @@ -5090,7 +5363,7 @@ MySQL 4.1 版本之后,开始支持 SQL 的子查询 EXPLAIN SELECT * FROM t_user WHERE id IN (SELECT user_id FROM user_role); ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL嵌套查询1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL嵌套查询1.png) * 优化后: @@ -5098,7 +5371,7 @@ MySQL 4.1 版本之后,开始支持 SQL 的子查询 EXPLAIN SELECT * FROM t_user u , user_role ur WHERE u.id = ur.user_id; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL嵌套查询2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL嵌套查询2.png) 连接查询之所以效率更高 ,是因为不需要在内存中创建临时表来完成逻辑上需要两个步骤的查询工作 @@ -5122,15 +5395,15 @@ MySQL 4.1 版本之后,开始支持 SQL 的子查询 EXPLAIN SELECT * FROM tb_user_1 LIMIT 200000,10; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL分页查询1.png) -* 优化方式一:子查询,在索引列 id 上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容 +* 优化方式一:内连接查询,在索引列 id 上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容 ```mysql EXPLAIN SELECT * FROM tb_user_1 t,(SELECT id FROM tb_user_1 ORDER BY id LIMIT 200000,10) a WHERE t.id = a.id; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL分页查询2.png) * 优化方式二:方案适用于主键自增的表,可以把 LIMIT 查询转换成某个位置的查询 @@ -5139,7 +5412,7 @@ MySQL 4.1 版本之后,开始支持 SQL 的子查询 EXPLAIN SELECT * FROM tb_user_1 WHERE id BETWEEN 200000 and 200010; -- 写法 2 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询3.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL分页查询3.png) @@ -5158,7 +5431,7 @@ SQL 提示,是优化数据库的一个重要手段,就是在 SQL 语句中 EXPLAIN SELECT * FROM tb_seller USE INDEX(idx_seller_name) WHERE name='小米科技'; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用提示1.png) * IGNORE INDEX:让 MySQL 忽略一个或者多个索引,则可以使用 IGNORE INDEX 作为提示 @@ -5166,7 +5439,7 @@ SQL 提示,是优化数据库的一个重要手段,就是在 SQL 语句中 EXPLAIN SELECT * FROM tb_seller IGNORE INDEX(idx_seller_name) WHERE name = '小米科技'; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用提示2.png) * FORCE INDEX:强制 MySQL 使用一个特定的索引 @@ -5174,7 +5447,7 @@ SQL 提示,是优化数据库的一个重要手段,就是在 SQL 语句中 EXPLAIN SELECT * FROM tb_seller FORCE INDEX(idx_seller_name_sta_addr) WHERE NAME='小米科技'; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示3.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-优化SQL使用提示3.png) @@ -5198,7 +5471,7 @@ SQL 提示,是优化数据库的一个重要手段,就是在 SQL 语句中 * 计数直接放到数据库里单独的一张计数表中,利用事务解决计数精确问题: - + 会话 B 的读操作在 T3 执行的,这时更新事务还没有提交,所以计数值加 1 这个操作对会话 B 还不可见,因此会话 B 查询的计数值和最近 100 条记录,返回的结果逻辑上就是一致的 @@ -5223,7 +5496,7 @@ count 函数的按照效率排序:`count(字段) < count(主键id) < count(1) -### 内存优化 +### 缓冲优化 #### 优化原则 @@ -5269,7 +5542,7 @@ MySQL 提供了缓冲页的快速查找方式:**哈希表**,使用表空间 MySQL 启动时完成对 Buffer Pool 的初始化,先向操作系统申请连续的内存空间,然后将内存划分为若干对控制块和缓冲页。为了区分空闲和已占用的数据页,将所有空闲缓冲页对应的**控制块作为一个节点**放入一个链表中,就是 Free 链表(**空闲链表**) - + 基节点:是一块单独申请的内存空间(占 40 字节),并不在 Buffer Pool 的那一大片连续内存空间里 @@ -5293,7 +5566,7 @@ MySQL 启动时完成对 Buffer Pool 的初始化,先向操作系统申请连 Flush 链表是一个用来**存储脏页**的链表,对于已经修改过的缓冲脏页,第一次修改后加入到**链表头部**,以后每次修改都不会重新加入,只修改部分控制信息,出于性能考虑并不是直接更新到磁盘,而是在未来的某个时间进行刷脏 - + **后台有专门的线程每隔一段时间把脏页刷新到磁盘**: @@ -5328,12 +5601,12 @@ MySQL 基于局部性原理提供了预读功能: 预读会造成加载太多用不到的数据页,造成那些使用**频率很高的数据页被挤到 LRU 链表尾部**,所以 InnoDB 将 LRU 链表分成两段: -* 一部分存储使用频率很高的数据页,这部分链表也叫热数据,young 区 -* 一部分存储使用频率不高的冷数据,old 区,默认占 37%,可以通过系统变量 `innodb_old_blocks_pct` 指定 +* 一部分存储使用频率很高的数据页,这部分链表也叫热数据,young 区,靠近链表头部的区域 +* 一部分存储使用频率不高的冷数据,old 区,靠近链表尾部,默认占 37%,可以通过系统变量 `innodb_old_blocks_pct` 指定 当磁盘上的某数据页被初次加载到 Buffer Pool 中会被放入 old 区,淘汰时优先淘汰 old 区 -* 当对 old 区的数据进行访问时,会在控制块记录下访问时间,等待后续的访问时间与第一次访问的时间是否在某个时间间隔内,通过系统变量 `innodb_old_blocks_time` 指定时间间隔,默认 1000ms,成立就移动到 young 区的链表头部 +* 当对 old 区的数据进行访问时,会在控制块记录下访问时间,等待后续的访问时间与第一次访问的时间是否在某个时间间隔内,通过系统变量 `innodb_old_blocks_time` 指定时间间隔,默认 1000ms,成立就**移动到 young 区的链表头部** * `innodb_old_blocks_time` 为 0 时,每次访问一个页面都会放入 young 区的头部 @@ -5360,7 +5633,7 @@ SHOW ENGINE INNODB STATUS\G SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; ``` - 在保证操作系统及其他程序有足够内存可用的情况下,`innodb_buffer_pool_size` 的值越大,缓存命中率越高 + 在保证操作系统及其他程序有足够内存可用的情况下,`innodb_buffer_pool_size` 的值越大,缓存命中率越高,建议设置成可用物理内存的 60%~80% ```sh innodb_buffer_pool_size=512M @@ -5389,9 +5662,9 @@ MySQL 5.7.5 之前 `innodb_buffer_pool_size` 只支持在系统启动时修改 -#### 其他内存 +### 内存优化 -##### Change +#### Change InnoDB 管理的 Buffer Pool 中有一块内存叫 Change Buffer 用来对**增删改操作**提供缓存,参数 `innodb_change_buffer_max_size ` 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% @@ -5419,16 +5692,26 @@ Change Buffer 并不是数据页,只是对操作的缓存,所以需要将 Ch -##### Net +#### Net Server 层针对优化**查询**的内存为 Net Buffer,内存的大小是由参数 `net_buffer_length`定义,默认 16k,实现流程: * 获取一行数据写入 Net Buffer,重复获取直到 Net Buffer 写满,调用网络接口发出去 -* 若发送成功就清空 Net Buffer,然后继续取下一行;若发送函数返回 `EAGAIN` 或 `WSAEWOULDBLOCK`,表示本地网络栈 `socket send buffer` 写满了,**进入等待**,直到网络栈重新可写再继续发送 +* 若发送成功就清空 Net Buffer,然后继续取下一行;若发送函数返回 EAGAIN 或 WSAEWOULDBLOCK,表示本地网络栈 `socket send buffer` 写满了,**进入等待**,直到网络栈重新可写再继续发送 + +MySQL 采用的是边读边发的逻辑,因此对于数据量很大的查询来说,不会在 Server 端保存完整的结果集,如果客户端读结果不及时,会堵住 MySQL 的查询过程,但是**不会把内存打爆导致 OOM** + + + +SHOW PROCESSLIST 获取线程信息后,处于 Sending to client 状态代表服务器端的网络栈写满,等待客户端接收数据 -MySQL 采用的是边算边发的逻辑,因此对于数据量很大的查询来说,不会在 Server 端保存完整的结果集,如果客户端读结果不及时,会堵住 MySQL 的查询过程,但是**不会把内存打爆导致 OOM** +假设有一个业务的逻辑比较复杂,每读一行数据以后要处理很久的逻辑,就会导致客户端要过很久才会去取下一行数据,导致 MySQL 的阻塞,一直处于 Sending to client 的状态 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查询内存优化.png) +解决方法:如果一个查询的返回结果很是很多,建议使用 mysql_store_result 这个接口,直接把查询结果保存到本地内存 + + + +参考文章:https://blog.csdn.net/qq_33589510/article/details/117673449 @@ -5436,7 +5719,33 @@ MySQL 采用的是边算边发的逻辑,因此对于数据量很大的查询 -##### Key +#### Read + +read_rnd_buffer 是 MySQL 的随机读缓冲区,当按任意顺序读取记录行时将分配一个随机读取缓冲区,进行排序查询时,MySQL 会首先扫描一遍该缓冲,以避免磁盘搜索,提高查询速度,大小是由 read_rnd_buffer_size 参数控制的 + +**Multi-Range Read 优化**,将随机 IO 转化为顺序 IO 以降低查询过程中 IO 开销,因为大多数的数据都是按照主键递增顺序插入得到,所以按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能 + +二级索引为 a,聚簇索引为 id,优化回表流程: + +* 根据索引 a,定位到满足条件的记录,将 id 值放入 read_rnd_buffer 中 +* 将 read_rnd_buffer 中的 id 进行**递增排序** +* 排序后的 id 数组,依次回表到主键 id 索引中查记录,并作为结果返回 + +说明:如果步骤 1 中 read_rnd_buffer 放满了,就会先执行步骤 2 和 3,然后清空 read_rnd_buffer,之后继续找索引 a 的下个记录 + +使用 MRR 优化需要设进行设置: + +```mysql +SET optimizer_switch='mrr_cost_based=off' +``` + + + +*** + + + +#### Key MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的读写速度。对于 MyISAM 表的数据块没有特别的缓存机制,完全依赖于操作系统的 IO 缓存 @@ -5461,13 +5770,6 @@ MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的 - -参考文章:https://blog.csdn.net/qq_33589510/article/details/117673449 - - - - - *** @@ -5501,7 +5803,7 @@ MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的 MySQL 的数据删除就是移除掉某个记录后,该位置就被标记为**可复用**,如果有符合范围条件的数据可以插入到这里。符合范围条件的意思是假设删除记录 R4,之后要再插入一个 ID 在 300 和 600 之间的记录时,就会复用这个位置 - + InnoDB 的数据是按页存储的如果删掉了一个数据页上的所有记录,整个数据页就可以被复用了,如果相邻的两个数据页利用率都很小,系统就会把这两个页上的数据合到其中一个页上,另外一个数据页就被标记为可复用 @@ -5535,7 +5837,7 @@ MySQL 5.6 版本开始引入的 **Online DDL**,重建表的命令默认执行 * 临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表 A 相同的数据文件,对应的就是图中 state3 * 用临时文件替换表 A 的数据文件 - + Online DDL 操作会先获取 MDL 写锁,再退化成 MDL 读锁。但 MDL 写锁持有时间比较短,所以可以称为 Online; 而 MDL 读锁,不阻止数据增删查改,但会阻止其它线程修改表结构(可以对比 `ANALYZE TABLE t` 命令) @@ -5578,6 +5880,8 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 MySQL 最大可支持的连接数取决于很多因素,包括操作系统平台的线程库的质量、内存大小、每个连接的负荷、CPU的处理速度、期望的响应时间等。在 Linux 平台下,性能好的服务器,可以支持 500-1000 个连接,需要根据服务器性能进行评估设定 +* innodb_thread_concurrency:并发线程数,代表系统内同时运行的线程数量(已经被移除) + * back_log:控制 MySQL 监听 TCP 端口时的积压请求栈的大小 如果 Mysql 的连接数达到 max_connections 时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即 back_log。如果等待连接的数量超过 back_log,将不被授予连接资源直接报错 @@ -6055,7 +6359,7 @@ InnoDB 存储引擎,数据库中的**聚簇索引**每行数据,除了自定 * DB_ROLL_PTR:回滚指针,**指向记录对应的 undo log 日志**,undo log 中又指向上一个旧版本的 undo log * DB_ROW_ID:隐含的自增 ID(**隐藏主键**),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 作为聚簇索引 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC版本链隐藏字段.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MVCC版本链隐藏字段.png) @@ -6084,7 +6388,7 @@ undo log 主要分为两种: 说明:因为 DELETE 删除记录,都是移动到垃圾链表中,不是真正的删除,所以才可以通过版本链访问原始数据 - + 注意:undo 是逻辑日志,这里只是直观的展示出来 @@ -6155,7 +6459,7 @@ START TRANSACTION; -- 开启事务 -- 操作表的其他数据 ``` -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC工作流程1.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MVCC工作流程1.png) ID 为 0 的事务创建 Read View: @@ -6164,7 +6468,7 @@ ID 为 0 的事务创建 Read View: * max_trx_id:61 * creator_trx_id:0 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC工作流程2.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MVCC工作流程2.png) 只有红框部分才复合条件,所以只有张三对应的版本的数据可以被看到 @@ -6431,7 +6735,7 @@ InnoDB 会真正的去执行把值修改成 (1,2) 这个操作,先加行锁, update T set c=c+1 where ID=2; ``` - + 流程说明:执行引擎将这行新数据读入到内存中(Buffer Pool)后,先将此次更新操作记录到 redo log buffer 里,然后更新记录。最后将 redo log 刷盘后事务处于 prepare 状态,执行器会生成这个操作的 binlog,并**把 binlog 写入磁盘**,完成提交 @@ -6653,7 +6957,7 @@ MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表 * 对 MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求 * 对 MyISAM 表的写操作,则会阻塞其他用户对同一表的读和写操作 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 锁的兼容性.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 锁的兼容性.png) 锁调度:**MyISAM 的读写锁调度是写优先**,因为写锁后其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞,所以 MyISAM 不适合做写为主的表的存储引擎 @@ -6691,7 +6995,7 @@ MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表 SELECT * FROM tb_book; -- C1、C2 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 读锁1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 读锁1.png) * C1 加读锁,C1、C2 查询未锁定的表,C1 报错,C2 正常查询 @@ -6700,7 +7004,7 @@ MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表 SELECT * FROM tb_user; -- C1、C2 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 读锁2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 读锁2.png) C1、C2 执行插入操作,C1 报错,C2 等待获取 @@ -6708,7 +7012,7 @@ MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表 INSERT INTO tb_book VALUES(NULL,'Spring高级','2088-01-01','1'); -- C1、C2 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 读锁3.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 读锁3.png) 当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 INSERT 语句立即执行 @@ -6729,7 +7033,7 @@ MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表 SELECT * FROM tb_book; -- C1、C2 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 写锁1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 写锁1.png) 当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 SELECT 语句立即执行 @@ -6739,7 +7043,7 @@ MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表 LOCK TABLE tb_book WRITE; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 写锁2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 写锁2.png) * C1 加写锁,C1、C2查询未锁定的表,C1 报错,C2 正常查询 @@ -6757,7 +7061,7 @@ MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表 SHOW OPEN TABLES; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-锁争用情况查看1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-锁争用情况查看1.png) In_user:表当前被查询使用的次数,如果该数为零,则表是打开的,但是当前没有被使用 @@ -6767,7 +7071,7 @@ MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表 LOCK TABLE tb_book READ; -- 执行命令 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-锁争用情况查看2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-锁争用情况查看2.png) * 查看锁状态: @@ -6775,7 +7079,7 @@ MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表 SHOW STATUS LIKE 'Table_locks%'; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 锁状态.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-MyISAM 锁状态.png) Table_locks_immediate:指的是能立即获得表级锁的次数,每立即获取锁,值加 1 @@ -6864,7 +7168,7 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 SELECT * FROM test_innodb_lock WHERE id=3; -- C1、C2 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作1.png) * C1 更新 id 为 3 的数据,但不提交: @@ -6872,7 +7176,7 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 UPDATE test_innodb_lock SET name='300' WHERE id=3; -- C1 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作2.png) C2 查询不到 C1 修改的数据,因为隔离界别为 REPEATABLE READ,C1 提交事务,C2 查询: @@ -6880,7 +7184,7 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 COMMIT; -- C1 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作3.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作3.png) 提交后仍然查询不到 C1 修改的数据,因为隔离级别可以防止脏读、不可重复读,所以 C2 需要提交才可以查询到其他事务对数据的修改: @@ -6889,7 +7193,7 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 SELECT * FROM test_innodb_lock WHERE id=3; -- C2 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作4.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作4.png) * C1 更新 id 为 3 的数据,但不提交,C2 也更新 id 为 3 的数据: @@ -6898,7 +7202,7 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 UPDATE test_innodb_lock SET name='30' WHERE id=3; -- C2 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作5.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作5.png) 当 C1 提交,C2 直接解除阻塞,直接更新 @@ -6909,7 +7213,7 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 UPDATE test_innodb_lock SET name='30' WHERE id=3; -- C2 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作6.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁操作6.png) 由于 C1、C2 操作的不同行,获取不同的行锁,所以都可以正常获取行锁 @@ -6960,7 +7264,7 @@ InnoDB 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的 SELECT * FROM test_innodb_lock; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 间隙锁1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 间隙锁1.png) * C1 根据 id 范围更新数据,C2 插入数据: @@ -6969,7 +7273,7 @@ InnoDB 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的 INSERT INTO test_innodb_lock VALUES(2,'200','2'); -- C2 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 间隙锁2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 间隙锁2.png) 出现间隙锁,C2 被阻塞,等待 C1 提交事务后才能更新 @@ -6995,7 +7299,7 @@ InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支 兼容性如下所示: -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-意向锁兼容性.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-意向锁兼容性.png) **插入意向锁** Insert Intention Lock 是在插入一行记录操作之前设置的一种间隙锁,是行级锁 @@ -7100,7 +7404,7 @@ InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面 UPDATE test_innodb_lock SET sex='2' WHERE id=3; -- C2 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁升级.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB 锁升级.png) 索引失效:执行更新时 name 字段为 varchar 类型,造成索引失效,最终行锁变为表锁 @@ -7142,7 +7446,7 @@ InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面 SHOW STATUS LIKE 'innodb_row_lock%'; ``` - + 参数说明: @@ -7165,7 +7469,7 @@ SELECT * FROM information_schema.innodb_locks; #锁的概况 SHOW ENGINE INNODB STATUS\G; #InnoDB整体状态,其中包括锁的情况 ``` -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB查看锁状态.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-InnoDB查看锁状态.png) lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁(写锁);lock_type 为 RECORD 代表锁为行锁(记录锁) @@ -7271,7 +7575,7 @@ MySQL 的主从之间维持了一个**长连接**。主库内部有一个线程 主从复制原理图: -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制原理图.jpg) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-主从复制原理图.jpg) 主从复制主要依赖的是 binlog,MySQL 默认是异步复制,需要三个线程: @@ -7521,7 +7825,7 @@ SELECT wait_for_executed_gtid_set(gtid_set [, timeout]) 工作流程:先执行 trx1,再执行一个查询请求的逻辑,要保证能够查到正确的数据 -* trx1 事务更新完成后,从返回包直接获取这个事务的 GTID,记为 gtid1 +* trx1 事务更新完成后,从返回包直接获取这个事务的 GTID,记为 gtid * 选定一个从库执行查询语句,如果返回值是 0,则在这个从库执行查询语句,否则到主库执行查询语句 对比等待位点方法,减少了一次 `show master status` 的方法,将参数 session_track_gtids 设置为 OWN_GTID,然后通过 API 接口 mysql_session_track_get_first 从返回包解析出 GTID 的值即可 @@ -7544,7 +7848,7 @@ SELECT wait_for_executed_gtid_set(gtid_set [, timeout]) * 分流查询:通过 MySQL 的主从复制,实现读写分离,使增删改操作走主节点,查询操作走从节点,从而可以降低单台服务器的读写压力 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-负载均衡主从复制.jpg) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-负载均衡主从复制.jpg) * 分布式数据库架构:适合大数据量、负载高的情况,具有良好的拓展性和高可用性。通过在多台服务器之间分布数据,可以实现在多台服务器之间的负载均衡,提高访问效率 @@ -7604,7 +7908,7 @@ SELECT wait_for_executed_gtid_set(gtid_set [, timeout]) SHOW MASTER STATUS; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查看master状态.jpg) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-查看master状态.jpg) * File:从哪个日志文件开始推送日志文件 * Position:从哪个位置开始推送日志 @@ -7678,11 +7982,11 @@ SELECT wait_for_executed_gtid_set(gtid_set [, timeout]) 在从库中,可以查看到刚才创建的数据库: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制验证1.jpg) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-主从复制验证1.jpg) 在该数据库中,查询表中的数据: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制验证2.jpg) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-主从复制验证2.jpg) @@ -8004,7 +8308,7 @@ mysqlbinlog log-file; mysqlbinlog mysqlbing.000001; ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-日志读取1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-日志读取1.png) 日志结尾有 COMMIT @@ -8029,7 +8333,7 @@ mysqlbinlog log-file; mysqlbinlog -vv mysqlbin.000002 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-日志读取2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-日志读取2.png) @@ -8063,6 +8367,29 @@ mysqlbinlog log-file; + + +**** + + + +#### 数据恢复 + +误删库或者表时,需要根据 binlog 进行数据恢复, + +一般情况下数据库有定时的全量备份,假如每天 0 点定时备份,12 点误删了库,恢复流程: + +* 取最近一次全量备份,用备份恢复出一个临时库 +* 从日志文件中取出凌晨 0 点之后的日志 +* 把除了误删除数据的语句外日志,全部应用到临时库 + +跳过误删除语句日志的方法: + +* 如果原实例没有使用 GTID 模式,只能在应用到包含 12 点的 binlog 文件的时候,先用 –stop-position 参数执行到误操作之前的日志,然后再用 –start-position 从误操作之后的日志继续执行 +* 如果实例使用了 GTID 模式,假设误操作命令的 GTID 是 gtid1,那么只需要提交一个空事务先将这个 GTID 加到临时实例的 GTID 集合,之后按顺序执行 binlog 的时就会自动跳过误操作的语句 + + + *** @@ -8091,7 +8418,7 @@ SELECT * FROM tb_book WHERE id < 8 执行完毕之后, 再次来查询日志文件: -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查询日志.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-查询日志.png) @@ -8124,7 +8451,7 @@ long_query_time=10 cat slow_query.log ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-慢日志读取1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-慢日志读取1.png) * 如果慢查询日志内容很多,直接查看文件比较繁琐,可以借助 mysql 自带的 mysqldumpslow 工具对慢查询日志进行分类汇总: @@ -8132,7 +8459,7 @@ long_query_time=10 mysqldumpslow slow_query.log ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-慢日志读取2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/MySQL-慢日志读取2.png) @@ -8152,12 +8479,12 @@ long_query_time=10 基本表: -![](https://gitee.com/seazean/images/raw/master/DB/普通表.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/普通表.png) 第一范式表: -![](https://gitee.com/seazean/images/raw/master/DB/第一范式.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/第一范式.png) @@ -8186,7 +8513,7 @@ long_query_time=10 * 主属性:码属性组中的所有属性 * 非主属性:除码属性组以外的属性 -![](https://gitee.com/seazean/images/raw/master/DB/第二范式.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/第二范式.png) @@ -8202,7 +8529,7 @@ long_query_time=10 作用:可以通过主键 id 区分相同数据,修改数据的时候只需要修改一张表(方便修改),反之需要修改多表。 -![](https://gitee.com/seazean/images/raw/master/DB/第三范式.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/第三范式.png) @@ -8216,7 +8543,7 @@ long_query_time=10 ### 总结 -![](https://gitee.com/seazean/images/raw/master/DB/三大范式.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/三大范式.png) @@ -8638,7 +8965,7 @@ SQL 注入攻击演示 * 在登录界面,输入一个错误的用户名或密码,也可以登录成功 - ![](https://gitee.com/seazean/images/raw/master/DB/SQL注入攻击演示.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/SQL注入攻击演示.png) * 原理:我们在密码处输入的所有内容,都应该认为是密码的组成,但是 Statement 对象在执行 SQL 语句时,将一部分内容当做查询条件来执行 @@ -8696,7 +9023,7 @@ PreparedStatement:预编译 sql 语句的执行者对象,继承 `PreparedSta 数据库连接池原理 -![](https://gitee.com/seazean/images/raw/master/DB/数据库连接池原理图解.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/数据库连接池原理图解.png) @@ -9292,7 +9619,7 @@ MySQL 支持 ACID 特性,保证可靠性和持久性,读取性能不高, 常见的 Nosql:Redis、memcache、HBase、MongoDB -![](https://gitee.com/seazean/images/raw/master/DB/电商场景解决方案.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/电商场景解决方案.png) @@ -9600,7 +9927,7 @@ Redis 并没有直接使用数据结构来实现键值对数据库,而是基 Redis 自身是一个 Map,其中所有的数据都是采用 key : value 的形式存储,**键对象都是字符串对象**,而值对象有五种基本类型和三种高级类型对象 -![](https://gitee.com/seazean/images/raw/master/DB/Redis-对象模型.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-对象模型.png) @@ -9651,7 +9978,7 @@ io-threads-do-reads yesCopy to clipboardErrorCopied io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程 ``` - + @@ -9812,7 +10139,7 @@ Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub) Redis 客户端可以订阅任意数量的频道 -![](https://gitee.com/seazean/images/raw/master/DB/Redis-发布订阅.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-发布订阅.png) 操作命令: @@ -9820,7 +10147,7 @@ Redis 客户端可以订阅任意数量的频道 2. 打开另一个客户端,给 channel1发布消息 hello:`publish channel1 hello` 3. 第一个客户端可以看到发送的消息 - + 注意:发布的消息没有持久化,所以订阅的客户端只能收到订阅后发布的消息 @@ -9834,7 +10161,7 @@ Redis 客户端可以订阅任意数量的频道 Redis ACL 是 Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接 -![](https://gitee.com/seazean/images/raw/master/DB/Redis-ACL指令.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-ACL指令.png) * acl cat:查看添加权限指令类别 * acl whoami:查看当前用户 @@ -9861,7 +10188,7 @@ Redis ACL 是 Access Control List(访问控制列表)的缩写,该功能 存储内容:通常使用字符串,如果字符串以整数的形式展示,可以作为数字操作使用 - + Redis 所有操作都是**原子性**的,采用**单线程**机制,命令是单个顺序执行,无需考虑并发带来影响,原子性就是有一个失败则都失败 @@ -9941,7 +10268,7 @@ Redis 所有操作都是**原子性**的,采用**单线程**机制,命令是 * 单数据执行 3 条指令的过程:3 次发送 + 3 次处理 + 3 次返回 * 多数据执行 1 条指令的过程:1 次发送 + 3 次处理 + 1 次返回(发送和返回的事件略高于单数据) - + @@ -9999,7 +10326,7 @@ struct sdshdr{ } ``` -![](https://gitee.com/seazean/images/raw/master/DB/Redis-string数据结构.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-string数据结构.png) 内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len,当字符串长度小于 1M 时,扩容都是双倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间,需要注意的是字符串最大长度为 512M @@ -10023,7 +10350,7 @@ struct sdshdr{ hash 类型:底层使用**哈希表**结构实现数据存储 - + Redis 中的 hash 类似于 Java 中的 `Map>`,左边是 key,右边是值,中间叫 field 字段,本质上 **hash 存了一个 key-value 的存储空间** @@ -10099,7 +10426,7 @@ user:id:3506728370 → {"name":"春晚","fans":12210862,"blogs":83} 假如现在粉丝数量发生了变化,要把整个值都改变,但是用单条存就不存在这个问题,只需要改其中一个就可以 - + 可以实现购物车的功能,key 对应着每个用户,存储空间存储购物车的信息 @@ -10132,7 +10459,7 @@ ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在 压缩列表(ziplist)是列表和哈希的底层实现之一,压缩列表用来紧凑数据存储,节省内存,有序: - + 压缩列表是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结枃,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值 @@ -10165,7 +10492,7 @@ Redis 字典使用散列表为底层实现,一个散列表里面有多个散 list 类型:保存多个数据,底层使用**双向链表**存储结构实现,类似于 LinkedList - + 如果两端都能存取数据的话,这就是双端队列,如果只能从一端进一端出,这个模型叫栈 @@ -10278,7 +10605,7 @@ typedef struct listNode } listNode; ``` -![](https://gitee.com/seazean/images/raw/master/DB/Redis-链表数据结构.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-链表数据结构.png) - 双向:链表节点带有前驱、后继指针,获取某个节点的前驱、后继节点的时间复杂度为 O(1) - 无环:链表为非循环链表,表头节点的前驱指针和表尾节点的后继指针都指向 NULL,对链表的访问以 NULL 为终点 @@ -10293,7 +10620,7 @@ typedef struct listNode quicklist 实际上是 ziplist 和 linkedlist 的混合体,将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来,既满足了快速的插入删除性能,又不会出现太大的空间冗余 - + @@ -10311,7 +10638,7 @@ quicklist 实际上是 ziplist 和 linkedlist 的混合体,将 linkedlist 按 set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不存储值(nil),所以添加,删除,查找的复杂度都是 O(1),并且**值是不允许重复且无序的** - + @@ -10422,7 +10749,7 @@ set 类型:与 hash 存储结构哈希表完全相同,只是仅存储键不 sorted_set 类型:在 set 的存储结构基础上添加可排序字段,类似于 TreeSet - + @@ -10526,7 +10853,7 @@ Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个 - Redis 每个跳跃表节点的层高都是 1 至 32 之间的随机数(Redis5 之后最大层数为 64) - 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。跳跃表中的节点按照分值大小进行排序,当分值相同时节点按照成员对象的大小进行排序 -![](https://gitee.com/seazean/images/raw/master/DB/Redis-跳跃表数据结构.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-跳跃表数据结构.png) @@ -10586,7 +10913,7 @@ Bitmaps 本身不是一种数据类型, 实际上就是字符串(key-value - 解决 Redis 缓存穿透,判断给定数据是否存在, 防止缓存穿透 - + - 垃圾邮件过滤,对每一个发送邮件的地址进行判断是否在布隆的黑名单中,如果在就判断为垃圾邮件 @@ -10808,7 +11135,7 @@ public JedisPool(GenericObjectPoolConfig poolConfig, String host, int port) { Redis Desktop Manager - + @@ -10827,7 +11154,7 @@ Redis Desktop Manager 作用:持久化用于防止数据的意外丢失,确保数据安全性,因为 Redis 是内存级,所以需要持久化到磁盘 计算机中的数据全部都是二进制,保存一组数据有两种方式 - + 第一种:将当前数据状态进行保存,快照形式,存储数据结果,存储格式简单 @@ -10881,7 +11208,7 @@ rdbchecksum yes|no bgsave 指令工作原理: -![](https://gitee.com/seazean/images/raw/master/DB/Redis-bgsave工作原理.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-bgsave工作原理.png) 流程:当执行 bgsave 的时候,客户端发出 bgsave 指令给到 redis 服务器,服务器返回后台已经开始执行的信息给客户端,同时使用 fork 函数**创建一个子进程**,让子进程去执行 save 相关的操作。持久化过程是先将数据写入到一个临时文件中,持久化操作结束再用这个临时文件**替换**上次持久化的文件,在这个过程中主进程是不进行任何 IO 操作的,这确保了极高的性能 @@ -10987,7 +11314,7 @@ AOF(append only file)持久化:以独立日志的方式记录每次写命 AOF 主要作用是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式 AOF 写数据过程: -![](https://gitee.com/seazean/images/raw/master/DB/Redis-AOF工作原理.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-AOF工作原理.png) @@ -11024,7 +11351,7 @@ AOF 持久化数据的三种策略(appendfsync): **AOF 缓冲区同步文件策略**,系统调用 write 和 fsync: * write 操作会触发延迟写(delayed write)机制,Linux 在内核提供页缓冲区用来提高硬盘 IO 性能,write 操作在写入系统缓冲区后直接返回 -* 同步硬盘操作依赖于系统调度机制,比如缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失 +* 同步硬盘操作(刷脏)依赖于系统调度机制,比如缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失 * fsync 针对单个文件操作(比如 AOF 文件)做强制硬盘同步,fsync 将阻塞到写入硬盘完成后返回,保证了数据持久化 异常恢复:AOF 文件损坏,通过 redis-check-aof--fix appendonly.aof 进行恢复,重启 Redis,然后重新加载 @@ -11082,7 +11409,7 @@ AOF 重写规则: 原理分析: - ![](https://gitee.com/seazean/images/raw/master/DB/Redis-AOF手动重写原理.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-AOF手动重写原理.png) * 自动重写 @@ -11117,11 +11444,11 @@ AOF 重写规则: 持久化流程: -![](https://gitee.com/seazean/images/raw/master/DB/Redis-AOF重写流程1.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-AOF重写流程1.png) 重写流程: -![](https://gitee.com/seazean/images/raw/master/DB/Redis-AOF重写流程2.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-AOF重写流程2.png) 使用**新的 AOF 文件覆盖旧的 AOF 文件**,完成 AOF 重写 @@ -11273,7 +11600,7 @@ int main(void) */ ``` - + 在 p3224 和 p3225 执行完第二个循环后,main 函数退出,进程死亡。所以 p3226,p3227 就没有父进程了,成为孤儿进程,所以 p3226 和 p3227 的父进程就被置为 ID 为 1的 init 进程(笔记 Tool → Linux → 进程管理详解) @@ -11293,7 +11620,7 @@ fork() 调用之后父子进程的内存关系 * 父子进程的代码段是相同的,所以代码段是没必要复制的,只需内核将代码段标记为只读,父子进程就共享此代码段。fork() 之后在进程创建代码段时,子进程的进程级页表项都指向和父进程相同的物理页帧 - + * 对于父进程的数据段,堆段,栈段中的各页,由于父子进程要相互独立,采用**写时复制**的技术,来提高内存以及内核的利用率 @@ -11301,7 +11628,7 @@ fork() 调用之后父子进程的内存关系 fork 之后内核会将子进程放在队列的前面,让子进程先执行,以免父进程执行导致写时复制,而后子进程再执行,因无意义的复制而造成效率的下降 - + 补充知识: @@ -11363,17 +11690,17 @@ Redis 事务的三大特性: 事务机制整体工作流程: -![](https://gitee.com/seazean/images/raw/master/DB/Redis-事务的工作流程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-事务的工作流程.png) 几种常见错误: * 定义事务的过程中,命令格式输入错误,出现语法错误造成,**整体事务中所有命令均不会执行**,包括那些语法正确的命令 - + * 定义事务的过程中,命令执行出现错误,例如对字符串进行 incr 操作,能够正确运行的命令会执行,运行错误的命令不会被执行 - + * 已经执行完毕的命令对应的数据不会自动回滚,需要程序员在代码中实现回滚,应该尽可能避免: @@ -11505,7 +11832,7 @@ TTL 返回的值有三种情况:正数,-1,-2 过期数据是一块独立的存储空间,Hash 结构,field 是内存地址,value 是过期时间,保存了所有 key 的过期描述,在最终进行过期处理的时候,对该空间的数据进行检测, 当时间到期之后通过 field 找到内存该地址处的数据,然后进行相关操作 - + @@ -11594,7 +11921,7 @@ Redis 采用惰性删除和定期删除策略的结合使用 * 参数 current_db 用于记录 activeExpireCycle() 进入哪个expires[*] 执行 * 如果 activeExpireCycle() 执行时间到期,下次从 current_db 继续向下执行 - + 定期删除特点: @@ -11758,7 +12085,7 @@ Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 * 解决方案:为了避免单点 Redis 服务器故障,准备多台服务器,互相连通。将数据复制多个副本保存在不同的服务器上连接在一起,并保证数据是同步的。即使有其中一台服务器宕机,其他服务器依然可以继续提供服务,实现 Redis 高可用,同时实现数据冗余备份 - + @@ -11774,7 +12101,7 @@ Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 * 数据同步阶段 * 命令传播阶段 -![](https://gitee.com/seazean/images/raw/master/DB/Redis-主从复制工作流程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-主从复制工作流程.png) @@ -11805,7 +12132,7 @@ Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 * 主从之间创建了连接的 socket - + @@ -11927,7 +12254,7 @@ Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 * 主从之间完成了数据克隆 - + @@ -12021,7 +12348,7 @@ Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 - master 记录已发送的信息对应的 offset - slave 记录已接收的信息对应的 offset - + @@ -12033,7 +12360,7 @@ Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 全量复制/部分复制 -![](https://gitee.com/seazean/images/raw/master/DB/Redis-主从复制流程更新.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-主从复制流程更新.png) @@ -12181,7 +12508,7 @@ slave 与 master 连接断开 哨兵(sentinel)是一个分布式系统,用于对主从结构中的每台服务器进行**监控**,当出现故障时通过**投票机制选择**新的 master 并将所有 slave 连接到新的 master - + 哨兵的作用: @@ -12298,7 +12625,7 @@ sentinel 1 首先连接 master,建立 cmd 通道,根据主节点访问从节 sentinel 2 首先连接 master,然后通过 master 中的 sentinels 发现其他哨兵,然后寻找哨兵建立连接,哨兵之间同步数据 - + @@ -12310,7 +12637,7 @@ sentinel 2 首先连接 master,然后通过 master 中的 sentinels 发现其 sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各个 sentinel 之间进行共享,流程如下: -![](https://gitee.com/seazean/images/raw/master/DB/Redis-哨兵模式通知工作流程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-哨兵模式通知工作流程.png) @@ -12326,13 +12653,13 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 sentinel1 检测到 master 下线后会做 flag:SRI_S_DOWN 标志,此时 master 的状态是主观下线,并通知其他哨兵,其他哨兵也会尝试与 master 连接,如果大于 (n/2) + 1 个sentinel 检测到 master 下线,就达成共识更改 flag,此时 master 的状态是客观下线 - ![](https://gitee.com/seazean/images/raw/master/DB/Redis-哨兵模式故障转移工作流程1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-哨兵模式故障转移工作流程1.png) * 当 sentinel 认定 master 下线之后,此时需要决定更换 master,选举某个 sentinel 处理事故 在选举的时候每一个 sentinel 都有一票,于是每个 sentinel 都会发出一个指令,在内网广播要做主持人;比如 sentinel1 和 sentinel4 发出这个选举指令了,那么 sentinel2 既能接到 sentinel1 的也能接到 sentinel4 的,sentinel2 会把一票投给其中一方,投给指令最先到达的 sentinel。选举最终得票多的,就成为了处理事故的哨兵,需要注意在这个过程中有可能会存在失败的现象,就是一轮选举完没有选取,那就会接着进行第二轮第三轮直到完成选举。 - ![](https://gitee.com/seazean/images/raw/master/DB/Redis-哨兵模式故障转移工作流程2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-哨兵模式故障转移工作流程2.png) 选择新的 master,在服务器列表中挑选备选 master 的原则: @@ -12362,7 +12689,7 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 集群就是使用网络将若干台计算机联通起来,并提供统一的管理方式,使其对外呈现单机的服务效果 -![](https://gitee.com/seazean/images/raw/master/DB/Redis-集群图示.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-集群图示.png) **集群作用:** @@ -12392,7 +12719,7 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 3. 将 key 按照计算出的结果放到对应的存储空间 - + 查找数据: @@ -12402,7 +12729,7 @@ sentinel 在通知阶段不断的去获取 master/slave 的信息,然后在各 设置数据:系统默认存储到某一个 - + @@ -12928,7 +13255,7 @@ Redis 中的监控指标如下: redis-benchmark -c 100 -n 5000 ``` - ![](https://gitee.com/seazean/images/raw/master/DB/Redis-redis-benchmark指令.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-redis-benchmark指令.png) * redis-cli diff --git a/Frame.md b/Frame.md index 864c479..7efbd38 100644 --- a/Frame.md +++ b/Frame.md @@ -14,7 +14,7 @@ pom.xml:Maven 需要一个 pom.xml 文件,Maven 通过加载这个配置文 管理资源的存储位置:本地仓库,私服,中央仓库 -![](https://gitee.com/seazean/images/raw/master/Frame/Maven介绍.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven介绍.png) 基本作用: @@ -24,7 +24,7 @@ pom.xml:Maven 需要一个 pom.xml 文件,Maven 通过加载这个配置文 * 统一开发结构:提供标准的,统一的项目开发结构 - ![](https://gitee.com/seazean/images/raw/master/Frame/Maven标准结构.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven标准结构.png) 各目录存放资源类型说明: @@ -107,7 +107,7 @@ Maven 的官网:http://maven.apache.org/ 配置 MAVEN_HOME: -![](https://gitee.com/seazean/images/raw/master/Frame/Maven配置环境变量.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven配置环境变量.png) Path 下配置:`%MAVEN_HOME%\bin` @@ -272,7 +272,7 @@ Path 下配置:`%MAVEN_HOME%\bin` ### 插件构建 -![](https://gitee.com/seazean/images/raw/master/Frame/Maven-插件构建.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven-插件构建.png) @@ -285,7 +285,7 @@ Path 下配置:`%MAVEN_HOME%\bin` #### 不用原型 1. 在 IDEA 中配置 Maven,选择 maven3.6.1 防止依赖问题 - IDEA配置Maven + IDEA配置Maven 2. 创建 Maven,New Module → Maven → 不选中 Create from archetype @@ -296,15 +296,15 @@ Path 下配置:`%MAVEN_HOME%\bin` 4. 查看各目录颜色标记是否正确 - ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA创建Maven目录结构.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/IDEA创建Maven目录结构.png) 5. IDEA 右侧侧栏有 Maven Project,打开后有 Lifecycle 生命周期 - ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA-Maven生命周期.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/IDEA-Maven生命周期.png) 6. 自定义 Maven 命令:Run → Edit Configurations → 左上角 + → Maven - ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA配置Maven命令.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/IDEA配置Maven命令.png) @@ -318,7 +318,7 @@ Path 下配置:`%MAVEN_HOME%\bin` 1. 创建 Maven 项目的时候选择使用原型骨架 - ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA创建Maven-quickstart.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/IDEA创建Maven-quickstart.png) 2. 创建完成后发现通过这种方式缺少一些目录,需要手动去补全目录,并且要对补全的目录进行标记 @@ -328,7 +328,7 @@ Web 工程: 1. 选择 Web 对应的原型骨架(选择 Maven 开头的是简化的) - ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA创建Maven-webapp.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/IDEA创建Maven-webapp.png) 2. 通过原型创建 Web 项目得到的目录结构是不全的,因此需要我们自行补全,同时要标记正确 @@ -472,13 +472,13 @@ Web 工程: `scope` 标签的取值有四种:`compile,test,provided,runtime` -![](https://gitee.com/seazean/images/raw/master/Frame/Maven依赖范围.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven依赖范围.png) **依赖范围的传递性:** -![](https://gitee.com/seazean/images/raw/master/Frame/Maven依赖范围的传递性.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven依赖范围的传递性.png) @@ -506,7 +506,7 @@ Maven 的构建生命周期描述的是一次构建过程经历了多少个事 对于 default 生命周期,每个事件在执行之前都会**将之前的所有事件依次执行一遍** - ![](https://gitee.com/seazean/images/raw/master/Frame/Maven-default生命周期.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven-default生命周期.png) * site:产生报告,发布站点等 @@ -569,7 +569,7 @@ Maven 的插件用来执行生命周期中的相关事件 工程模块与模块划分: -![](https://gitee.com/seazean/images/raw/master/Frame/Maven模块划分.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven模块划分.png) * ssm_pojo 拆分 @@ -854,7 +854,7 @@ Maven 的插件用来执行生命周期中的相关事件 * 版本统一的重要性: - ![](https://gitee.com/seazean/images/raw/master/Frame/Maven版本统一的重要性.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven版本统一的重要性.png) * 属性类别: @@ -1097,7 +1097,7 @@ mvn 指令 –D skipTests IEDA 界面: -![](https://gitee.com/seazean/images/raw/master/Frame/IDEA使用界面操作跳过测试.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/IDEA使用界面操作跳过测试.png) @@ -1162,7 +1162,7 @@ http://localhost:8081 ### 资源操作 -![](https://gitee.com/seazean/images/raw/master/Frame/Maven私服资源获取.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Maven私服资源获取.png) @@ -1200,7 +1200,7 @@ http://localhost:8081 #### 上传下载 -![](https://gitee.com/seazean/images/raw/master/Frame/IDEA环境中资源上传与下载.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/IDEA环境中资源上传与下载.png) @@ -1292,7 +1292,7 @@ mvn deploy Log4j 是 Apache 的一个开源项目。使用 Log4j,通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。我们可以控制日志信息输送的目的地是控制台、文件等位置,也可以控制每一条日志的输出格式。 - + @@ -1340,7 +1340,7 @@ Log4j 是 Apache 的一个开源项目。使用 Log4j,通过一个配置文件 + org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等信息) + PatternLayout 常用的选项 - + @@ -1433,7 +1433,7 @@ Netty 的功能特性: * 协议支持:HTTP、Protobuf、二进制、文本、WebSocket 等一系列协议都支持,也支持通过实行编码解码逻辑来实现自定义协议 * Core 核心:可扩展事件模型、通用通信 API、支持零拷贝的 ByteBuf 缓冲对象 - + @@ -1451,7 +1451,7 @@ Netty 的功能特性: 传统阻塞型 I/O 模式,每个连接都需要独立的线程完成数据的输入,业务处理,数据返回 - + 模型缺点: @@ -1476,7 +1476,7 @@ Reactor 模式,通过一个或多个输入同时传递给服务处理器的** **I/O 复用结合线程池**,就是 Reactor 模式基本设计思想: - + Reactor 模式关键组成: @@ -1512,7 +1512,7 @@ Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 说明:**Handler 和 Acceptor 属于同一个线程** - + 模型优点:模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成 @@ -1537,7 +1537,7 @@ Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 * Worker 线程池会分配独立的线程完成真正的业务处理,将响应结果发给 Handler 进行处理,最后由 Handler 收到响应结果后通过 send 将响应结果返回给 Client - + 模型优点:可以充分利用多核 CPU 的处理能力 @@ -1564,7 +1564,7 @@ Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 * Worker 线程池会分配独立的线程完成真正的业务处理,将响应结果发给 Handler 进行处理,最后由 Handler 收到响应结果后通过 send 将响应结果返回给 Client - + 模型优点 @@ -1585,7 +1585,7 @@ Reactor 模式中,Reactor 等待某个事件的操作状态发生变化(文 把 I/O 操作改为异步,交给操作系统来完成就能进一步提升性能,这就是异步网络模型 Proactor(AIO): - + 工作流程: @@ -1613,7 +1613,7 @@ Reactor 模式中,Reactor 等待某个事件的操作状态发生变化(文 Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty 的工作架构图: - + 工作流程: @@ -1637,7 +1637,7 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty 6. 每个 Worker NioEventLoop 处理业务时,会使用 pipeline(管道),pipeline 中包含了 channel,即通过 pipeline 可以获取到对应通道,管道中维护了很多的处理器 Handler - + @@ -2127,7 +2127,7 @@ public static void main(String[] args) { * 入站事件会从链表 head 往后传递到最后一个入站的 handler * 出站事件会从链表 tail 往前传递到最前一个出站的 handler -![](https://gitee.com/seazean/images/raw/master/Frame/Netty-ChannelPipeline.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-ChannelPipeline.png) @@ -2212,7 +2212,7 @@ ByteBuf 是对字节数据的封装,优点: ByteBuf 由四部分组成,最开始读写指针(**双指针**)都在 0 位置 -![](https://gitee.com/seazean/images/raw/master/Frame/Netty-ByteBuf组成.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-ByteBuf组成.png) 写入方法: @@ -2829,7 +2829,7 @@ public class LoginRequestMessage extends Message { } ``` -![](https://gitee.com/seazean/images/raw/master/Frame/Netty-自定义协议.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-自定义协议.png) @@ -3068,7 +3068,7 @@ bootstrap.handler(new ChannelInitializer() { Codec(编解码器)的组成部分有两个:Decoder(解码器)和 Encoder(编码器)。Encoder 负责把业务数据转换成字节码数据,Decoder 负责把字节码数据转换成业务数据 - + @@ -3098,7 +3098,7 @@ Protobuf 是以 message 的方式来管理数据,支持跨平台、跨语言 } ``` - + 编译 `protoc.exe --java_out=.Student.proto`(cmd 窗口输入) 将生成的 StudentPOJO 放入到项目使用 @@ -3416,7 +3416,7 @@ public class ConnectionTimeoutTest { * sync queue:半连接队列,大小通过 `/proc/sys/net/ipv4/tcp_max_syn_backlog` 指定,在 `syncookies` 启用的情况下,逻辑上没有最大值限制 * accept queue:全连接队列,大小通过 `/proc/sys/net/core/somaxconn` 指定,在使用 listen 函数时,内核会根据传入的 backlog 参数与系统参数,取二者的较小值。如果 accpet queue 队列满了,server 将**发送一个拒绝连接的错误信息**到 client -![](https://gitee.com/seazean/images/raw/master/Frame/Netty-TCP三次握手.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Netty-TCP三次握手.png) @@ -3459,15 +3459,15 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 实例:用户创建订单后,耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障都会造成下单异常,影响用户使用体验。使用消息队列解耦合,比如物流系统发生故障,需要几分钟恢复,将物流系统要处理的数据缓存到消息队列中,用户的下单操作正常完成。等待物流系统正常后处理存在消息队列中的订单消息即可,终端系统感知不到物流系统发生过几分钟故障 - ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-解耦.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-解耦.png) * 流量削峰:应用系统如果遇到系统请求流量的瞬间猛增,有可能会将系统压垮,使用消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以提高系统的稳定性和用户体验。 - ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-流量削峰.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-流量削峰.png) * 数据分发:让数据在多个系统更加之间进行流通,数据的产生方不需要关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消息队列中直接获取数据 - ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-数据分发.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-数据分发.png) @@ -4213,7 +4213,7 @@ RocketMQ 在 Producer 端写入消息和在 Consumer 端订阅消息采用**分 ConsumeQueue 的存储结构如下,有 8 个字节存储的 Message Tag 的哈希值,基于 Tag 的消息过滤就是基于这个字段 -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消费队列结构.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-消费队列结构.png) * Tag 过滤:Consumer 端订阅消息时指定 Topic 和 TAG,然后将订阅请求构建成一个 SubscriptionData,发送一个 Pull 消息的请求给 Broker 端。Broker 端用这些数据先构建一个 MessageFilter,然后传给文件存储层 Store。Store 从 ConsumeQueue 读取到一条记录后,会用它记录的消息 tag hash 值去做过滤。因为在服务端只是根据 hashcode 进行判断,无法精确对 tag 原始字符串进行过滤,所以消费端拉取到消息后,还需要对消息的原始 tag 字符串进行比对,如果不同,则丢弃该消息,不进行消息消费 @@ -4290,7 +4290,7 @@ public class Consumer { RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交事务消息,同时增加一个**补偿逻辑**来处理二阶段超时或者失败的消息,如下图所示: -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-事务消息.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-事务消息.png) 事务消息的大致方案分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程 @@ -4303,7 +4303,7 @@ RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交 * 根据本地事务状态执行 Commit 或者 Rollback -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-事务工作流程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-事务工作流程.png) 2. 补偿机制:用于解决消息 Commit 或者 Rollback 发生超时或者失败的情况,比如出现网络问题 @@ -4350,7 +4350,7 @@ RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 T RocketMQ 将 Op 消息写入到全局一个特定的 Topic 中,通过源码中的方法 `TransactionalMessageUtil.buildOpTopic()`,这个主题是一个内部的 Topic(像 Half 消息的 Topic 一样),不会被用户消费。Op 消息的内容为对应的 Half 消息的存储的 Offset,这样**通过 Op 消息能索引到 Half 消息** -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-OP消息.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-OP消息.png) @@ -4503,7 +4503,7 @@ Broker 包含了以下几个重要子模块: * Index Service:根据特定的 Message key 对投递到 Broker 的消息进行索引服务,以提供消息的快速查询 -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Broker工作流程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-Broker工作流程.png) @@ -4545,7 +4545,7 @@ At least Once:至少一次,指每个消息必须投递一次,Consumer 先 5. 如果消息消费者在指定时间内成功返回 ACK,那么 MQ 认为消息消费成功,在存储中删除消息;如果 MQ 在指定时间内没有收到 ACK,则认为消息消费失败,会尝试重新 push 消息,重复执行 4、5、6 步骤 6. MQ 删除消息 -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存取.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-消息存取.png) @@ -4567,7 +4567,7 @@ RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,Com 每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-消息存储结构.png) * CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是**顺序写入**日志文件,单个文件大小默认 1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 * ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,**保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset**,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M @@ -4591,7 +4591,7 @@ RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所 * write:将读取的内容通过网络发送出去 -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-文件与网络操作.png) 补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 @@ -4637,7 +4637,7 @@ RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使 通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-刷盘机制.png) @@ -4696,7 +4696,7 @@ RocketMQ 网络部署特点: Consumer 既可以从 Master 订阅消息,也可以从 Slave 订阅消息,在向 Master 拉取消息时,Master 服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读 I/O),以及从服务器是否可读等因素建议下一次是从 Master 还是 Slave 拉取 -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-集群架构.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-集群架构.png) @@ -4716,7 +4716,7 @@ RocketMQ 网络部署特点: RocketMQ 目前还不支持把 Slave 自动转成 Master,需要手动停止 Slave 角色的 Broker,更改配置文件,用新的配置文件启动 Broker -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-高可用.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-高可用.png) @@ -4765,7 +4765,7 @@ Producer 端在发送消息时,会先根据 Topic 找到指定的 TopicPublish 默认会**轮询所有的 Message Queue 发送**,以让消息平均落在不同的 queue 上,而由于 queue可以散落在不同的 Broker,所以消息就发送到不同的 Broker 下,图中箭头线条上的标号代表顺序,发布方会把第一条消息发送至 Queue 0,然后第二条消息发送至 Queue 1,以此类推: -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-producer负载均衡.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-producer负载均衡.png) 容错策略均在 MQFaultStrategy 这个类中定义,有一个 sendLatencyFaultEnable 开关变量: @@ -4792,11 +4792,11 @@ LatencyFaultTolerance 机制是实现消息发送高可用的核心关键所在 集群模式下,每当消费者实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照 queue 的数量和实例的数量平均分配 queue 给每个实例。默认的分配算法是 AllocateMessageQueueAveragely: -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-平均队列分配.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-平均队列分配.png) 还有一种平均的算法是 AllocateMessageQueueAveragelyByCircle,以环状轮流均分 queue 的形式: -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-平均队列轮流分配.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-平均队列轮流分配.png) 集群模式下,**queue 都是只允许分配只一个实例**,如果多个实例同时消费一个 queue 的消息,由于拉取哪些消息是 Consumer 主动控制的,会导致同一个消息在不同的实例下被消费多次 @@ -4824,7 +4824,7 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** * 调用 updateProcessQueueTableInRebalance() 方法,先将分配到的消息队列集合 mqSet 与 processQueueTable 做一个过滤比对 - ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-负载均衡重新平衡算法.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-负载均衡重新平衡算法.png) * processQueueTable 标注的红色部分,表示与分配到的消息队列集合 mqSet 互不包含,将这些队列设置 Dropped 属性为 true,然后查看这些队列是否可以移除出 processQueueTable 缓存变量。具体执行 removeUnnecessaryMessageQueue() 方法,即每隔 1s 查看是否可以获取当前消费处理队列的锁,拿到的话返回 true;如果等待 1s 后,仍然拿不到当前消费处理队列的锁则返回 false。如果返回 true,则从 processQueueTable 缓存变量中移除对应的 Entry @@ -4868,7 +4868,7 @@ RocketMQ 支持按照两种维度进行消息查询:按照 Message ID 查询 RocketMQ 的索引文件逻辑结构,类似 JDK 中 HashMap 的实现,具体结构如下: -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-IndexFile索引文件.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-IndexFile索引文件.png) IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName 是以创建时的时间戳命名,文件大小是固定的,等于 `40+500W*4+2000W*20= 420000040` 个字节大小。如果消息的 properties 中设置了 UNIQ_KEY 这个属性,就用 `topic + “#” + UNIQ_KEY` 作为 key 来做写入操作;如果消息设置了 KEYS 属性(多个 KEY 以空格分隔),也会用 `topic + “#” + KEY` 来做索引 @@ -5290,7 +5290,7 @@ NamesrvController 用来初始化和启动 Namesrv 服务器 RocketMQ 的 RPC 通信采用 Netty 组件作为底层通信库,同样也遵循了 Reactor 多线程模型,NettyRemotingServer 类负责框架的通信服务,同时又在这之上做了一些扩展和优化 -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Reactor设计.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-Reactor设计.png) RocketMQ 基于 NettyRemotingServer 的 Reactor 多线程模型: @@ -5309,7 +5309,7 @@ RocketMQ 基于 NettyRemotingServer 的 Reactor 多线程模型: RocketMQ 的异步通信流程: -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-异步通信流程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-异步通信流程.png) @@ -5588,7 +5588,7 @@ NettyRemotingServer 类成员变量: | remark | String | 传输自定义文本信息 | 传输自定义文本信息 | | extFields | HashMap | 请求自定义扩展信息 | 响应自定义扩展信息 | -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息协议.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-消息协议.png) 传输内容主要可以分为以下四部分: @@ -9400,11 +9400,11 @@ AllocateMessageQueueStrategy 类是队列的分配策略 队列排序后:Q1 → Q2 → Q3,消费者排序后 C1 → C2 → C3 - ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-平均队列分配.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-平均队列分配.png) * 轮流分配:AllocateMessageQueueAveragelyByCircle - ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-平均队列轮流分配.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/RocketMQ-平均队列轮流分配.png) * 指定机房平均分配:AllocateMessageQueueByMachineRoom,前提是 Broker 的命名规则为 `机房名@BrokerName` diff --git a/Java.md b/Java.md index 735be75..2e2efde 100644 --- a/Java.md +++ b/Java.md @@ -59,7 +59,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, **int:** - int 数据类型是 32 位 4 字节、有符号的以二进制补码表示的整数 -- 最小值是 -2,147,483,648(-2^31 -) +- 最小值是 -2,147,483,648(-2^31) - 最大值是 2,147,483,647(2^31 - 1) - 一般地整型变量默认为 int 类型 - 默认值是 `0` @@ -444,15 +444,15 @@ public static void main(String[] args) { * 一个数组内存图 - ![](https://gitee.com/seazean/images/raw/master/Java/数组内存分配-一个数组内存图.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/数组内存分配-一个数组内存图.png) * 两个数组内存图 - ![](https://gitee.com/seazean/images/raw/master/Java/数组内存分配-两个数组内存图.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/数组内存分配-两个数组内存图.png) * 多个数组指向相同内存图 - ![](https://gitee.com/seazean/images/raw/master/Java/数组内存分配-多个数组指向一个数组内存图.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/数组内存分配-多个数组指向一个数组内存图.png) *** @@ -976,9 +976,9 @@ Debug 是供程序员使用的程序调试工具,它可以用于查看程序 加断点 → Debug 运行 → 单步运行 → 看 Debugger 窗口 → 看 Console 窗口 -![](https://gitee.com/seazean/images/raw/master/Java/Debug按键说明.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Debug按键说明.png) -Debug条件断点 +Debug条件断点 @@ -2356,7 +2356,7 @@ Cloneable 接口是一个标识性接口,即该接口不包含任何方法( * 对基本数据类型和包装类的克隆是没有问题的。String、Integer 等包装类型在内存中是**不可以被改变的对象**,所以在使用克隆时可以视为基本类型,只需浅克隆引用即可 * 如果对一个引用类型进行克隆时只是克隆了它的引用,和原始对象共享对象成员变量 - ![](https://gitee.com/seazean/images/raw/master/Java/Object浅克隆.jpg) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Object浅克隆.jpg) 深克隆:在对整个对象浅克隆后,对其引用变量进行克隆,并将其更新到浅克隆对象中去 @@ -2509,7 +2509,7 @@ s.replace("-","");//12378 * 创建一个对象:字符串池中已经存在 abc 对象,那么直接在创建一个对象放入堆中,返回堆内引用 * 创建两个对象:字符串池中未找到 abc 对象,那么分别在堆中和字符串池中创建一个对象,字符串池中的比较都是采用 equals() - + `new String("a") + new String("b")` 创建字符串对象: @@ -2518,7 +2518,7 @@ s.replace("-","");//12378 * 对象 2:new String("a")、对象 3:常量池中的 a * 对象 4:new String("b")、对象 5:常量池中的 b - + * StringBuilder 的 toString(): @@ -2701,7 +2701,7 @@ Java 7 之前,String Pool 被放在运行时常量池中,属于永久代;J } ``` -![](https://gitee.com/seazean/images/raw/master/Java/JVM-内存图对比.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-内存图对比.png) @@ -3782,7 +3782,7 @@ public class RegexDemo { 各数据结构时间复杂度对比: -![](https://gitee.com/seazean/images/raw/master/Java/数据结构的复杂度对比.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/数据结构的复杂度对比.png) @@ -4263,7 +4263,7 @@ public class ListDemo { LinkedList 是一个实现了 List 接口的**双端链表**,支持高效的插入和删除操作,另外也实现了 Deque 接口,使得 LinkedList 类也具有队列的特性 -![](https://gitee.com/seazean/images/raw/master/Java/LinkedList底层结构.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/LinkedList底层结构.png) 核心方法: @@ -4397,7 +4397,7 @@ Set 集合添加的元素是无序,不重复的。 * 当链表长度超过阈值 8 且当前数组的长度 > 64时,将链表转换为红黑树,减少了查找时间 * 当链表长度超过阈值 8 且当前数组的长度 < 64时,扩容 - ![](https://gitee.com/seazean/images/raw/master/Java/HashSet底层结构哈希表.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/HashSet底层结构哈希表.png) 每个元素的 hashcode() 的值进行响应的算法运算,计算出的值相同的存入一个数组块中,以链表的形式存储,如果链表长度超过8就采取红黑树存储,所以输出的元素是无序的。 @@ -4725,7 +4725,7 @@ JDK7 对比 JDK8: * 当链表长度**超过(大于)阈值**(或者红黑树的边界值,默认为 8)并且当前数组的**长度大于等于 64 时**,此索引位置上的所有数据改为红黑树存储 * 即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,就相当于一个长的单链表,假如单链表有 n 个元素,遍历的**时间复杂度是 O(n)**,所以 JDK1.8 中引入了 红黑树(查找**时间复杂度为 O(logn)**)来优化这个问题,使得查找效率更高 - ![](https://gitee.com/seazean/images/raw/master/Java/HashMap底层结构.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/HashMap底层结构.png) @@ -4741,7 +4741,7 @@ JDK7 对比 JDK8: HashMap 继承关系如下图所示: -![](https://gitee.com/seazean/images/raw/master/Java/HashMap继承关系.bmp) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/HashMap继承关系.bmp) 说明: @@ -5076,7 +5076,7 @@ HashMap 继承关系如下图所示: * `(n - 1) & hash`:计算下标位置 - + * 余数本质是不断做除法,把剩余的数减去,运算效率要比位运算低 @@ -5209,7 +5209,7 @@ HashMap 继承关系如下图所示: 注意:这里要求**数组长度 2 的幂** - ![](https://gitee.com/seazean/images/raw/master/Java/HashMap-resize扩容.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/HashMap-resize扩容.png) 普通节点:把所有节点分成高低位两个链表,转移到数组 @@ -10392,14 +10392,14 @@ Java 代码执行流程:Java 程序 --(编译)--> 字节码文件 --(解 JVM 结构: - + JVM、JRE、JDK 对比: * JDK(Java SE Development Kit):Java 标准开发包,它提供了编译、运行 Java 程序所需的各种工具和资源 * JRE( Java Runtime Environment):Java 运行环境,用于解释执行 Java 的字节码文件 - + @@ -10438,14 +10438,14 @@ Java 编译器输入的指令流是一种基于栈的指令集架构。因为跨 ### 生命周期 -JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡。 +JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡 - **启动**:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点 - **运行**: - main() 方法是一个程序的初始起点,任何线程均可由在此处启动 - 在 JVM 内部有两种线程类型,分别为:用户线程和守护线程,**JVM 使用的是守护线程,main() 和其他线程使用的是用户线程**,守护线程会随着用户线程的结束而结束 - - 执行一个 Java 程序时,真真正正在执行的是一个 Java 虚拟机的进程 + - 执行一个 Java 程序时,真真正正在执行的是一个 **Java 虚拟机的进程** - JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多 Server 模式启动的 JVM 采用的是重量级的虚拟机,对程序采用了更多的优化;Client 模式启动的 JVM 采用的是轻量级的虚拟机 @@ -10474,11 +10474,11 @@ JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡。 JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行 * Java1.8 以前的内存结构图: - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java7内存结构图.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-Java7内存结构图.png) * Java1.8 之后的内存结果图: - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java8内存结构图.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-Java8内存结构图.png) 线程运行诊断: @@ -10521,7 +10521,7 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 * 方法返回地址:方法正常退出或者异常退出的定义 * 操作数栈或表达式栈和其他一些附加信息 - + 设置栈内存大小:`-Xss size` `-Xss 1024k` @@ -10597,13 +10597,13 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 * 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接符号引用.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-动态链接符号引用.png) * 在 Java 源文件被编译成的字节码文件中,所有的变量和方法引用都作为符号引用保存在 class 的常量池中 常量池的作用:提供一些符号和常量,便于指令的识别 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接运行时常量池.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-动态链接运行时常量池.png) @@ -10656,7 +10656,7 @@ JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可 * 直接从本地内存的堆中分配任意数量的内存 * 可以直接使用本地处理器中的寄存器 - + @@ -10795,7 +10795,7 @@ public static void main(String[] args) { 本地内存概述图: - + @@ -10949,7 +10949,7 @@ TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了 问题:堆空间都是共享的么? 不一定,因为还有 TLAB,在堆中划分出一块区域,为每个线程所独占 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配策略.jpg) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-TLAB内存分配策略.jpg) JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在 TLAB 空间分配内存失败时,JVM 就会通过**使用加锁机制确保数据操作的原子性**,从而直接在堆中分配内存 @@ -10962,7 +10962,7 @@ JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都 * `-XX:TLABWasteTargetPercent`:设置 TLAB 空间所占用 Eden 空间的百分比大小,默认情况下 TLAB 空间的内存非常小,仅占有整个 Eden 空间的1% * `-XX:TLABRefillWasteFraction`:指当 TLAB 空间不足,请求分配的对象内存大小超过此阈值时不会进行 TLAB 分配,直接进行堆内存分配,否则还是会优先进行 TLAB 分配 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配过程.jpg) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-TLAB内存分配过程.jpg) @@ -11063,7 +11063,7 @@ Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 Eden 和 Survivor 大小比例默认为 8:1:1 - + @@ -11248,7 +11248,7 @@ public void localvarGC4() { } ``` -![](https://gitee.com/seazean/images/raw/master/Java/JVM-循环引用.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-循环引用.png) @@ -11293,7 +11293,7 @@ GC Roots 对象: - 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象 - + @@ -11321,7 +11321,7 @@ GC Roots 对象: 4. 重复步骤 3,直至灰色集合为空时结束 5. 结束后,仍在白色集合的对象即为 GC Roots 不可达,可以进行回收 - + @@ -11342,7 +11342,7 @@ GC Roots 对象: * 针对并发标记开始后的**新对象**,通常的做法是直接全部当成黑色,也算浮动垃圾 * 浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除 - + **漏标情况:** @@ -11350,7 +11350,7 @@ GC Roots 对象: * 条件二:其他线程中修改了黑色对象,插入了一条或多条对该白色对象的新引用 * 结果:导致该白色对象当作垃圾被 GC,影响到了程序的正确性 - + 代码角度解释漏标: @@ -11542,7 +11542,7 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 - 标记和清除过程效率都不高 - 会产生大量不连续的内存碎片,导致无法给大对象分配内存,需要维护一个空闲链表 - + @@ -11556,7 +11556,7 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 应用场景:如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之则不适合 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-复制算法.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-复制算法.png) 算法优点: @@ -11586,7 +11586,7 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 缺点:需要移动大量对象,处理效率比较低 - + | | Mark-Sweep | Mark-Compact | Copying | | -------- | ------------------ | ---------------- | --------------------------------------- | @@ -11630,7 +11630,7 @@ GC 性能指标: **垃圾收集器的组合关系**: -![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器关系图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-垃圾回收器关系图.png) 新生代收集器:Serial、ParNew、Parallel Scavenge @@ -11667,7 +11667,7 @@ Serial:串行垃圾收集器,作用于新生代,是指使用单线程进 开启参数:`-XX:+UseSerialGC` 等价于新生代用 Serial GC 且老年代用 Serial old GC -![](https://gitee.com/seazean/images/raw/master/Java/JVM-Serial收集器.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-Serial收集器.png) 优点:简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,可以获得最高的单线程收集效率 @@ -11700,7 +11700,7 @@ Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,* 在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge + Parallel Old 收集器,在 Server 模式下的内存回收性能很好,**Java8 默认是此垃圾收集器组合** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParallelScavenge收集器.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-ParallelScavenge收集器.png) 参数配置: @@ -11738,7 +11738,7 @@ Par 是 Parallel 并行的缩写,New 是只能处理的是新生代 * `-XX:ParallelGCThreads`:默认开启和 CPU 数量相同的线程数 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParNew收集器.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-ParNew收集器.png) ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器 @@ -11764,15 +11764,11 @@ CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿 - 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况) - 并发清除:清除标记为可以回收对象,**不需要移动存活对象**,所以这个阶段可以与用户线程同时并发的 -Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因: - -* Mark Compact 算法会整理内存,导致用户线程使用的**对象的地址改变**,影响用户线程继续执行 - -* Mark Compact 更适合 Stop The World 场景 +Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因:Mark Compact 算法会整理内存,导致用户线程使用的**对象的地址改变**,影响用户线程继续执行 在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-CMS收集器.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-CMS收集器.png) 优点:并发收集、低延迟 @@ -11832,7 +11828,7 @@ G1 对比其他处理器的优点: * Region 结构图: -![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1-Region区域.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-G1-Region区域.png) - 空间整合: @@ -11866,7 +11862,7 @@ G1 垃圾收集器的缺点: 记忆集 Remembered Set 在新生代中,每个 Region 都有一个 Remembered Set,用来被哪些其他 Region 里的对象引用(谁引用了我就记录谁) - + * 程序对 Reference 类型数据写操作时,产生一个 Write Barrier 暂时中断操作,检查该对象和 Reference 类型数据是否在不同的 Region(跨代引用),不同就将相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中 * 进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏 @@ -11897,7 +11893,7 @@ G1 中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 Full GC,在 * 当堆内存使用达到一定值(默认 45%)时,开始老年代并发标记过程 * 标记完成马上开始混合回收过程 - + 顺时针:Young GC → Young GC + Concurrent Mark → Mixed GC 顺序,进行垃圾回收 @@ -11921,7 +11917,7 @@ G1 中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 Full GC,在 * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行(**防止漏标**) * 筛选回收:并发清理阶段,首先对 CSet 中各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划,也需要 STW - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1收集器.jpg) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-G1收集器.jpg) * **Mixed GC**:当很多对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,除了回收整个 young region,还会回收一部分的 old region,过程同 YGC @@ -12038,7 +12034,7 @@ Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同: - 最大化应用程序的吞吐量,选 Parallel GC - 最小化 GC 的中断或停顿时间,选 CMS GC -![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器总结.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-垃圾回收器总结.png) @@ -12312,7 +12308,7 @@ private int hash32; 下图显示了一个简单的对象引用关系图,对象 A 引用了 C 和 D,对象 B 引用了 C 和 E。那么对象 A 的浅堆大小只是 A 本身,**A 的实际大小为 A、C、D 三者之和**,A 的深堆大小为 A 与 D 之和,由于对象 C 还可以通过对象 B 访问到 C,因此 C 不在对象 A 的深堆范围内 - + 内存分析工具 MAT 提供了一种叫支配树的对象图,体现了对象实例间的支配关系 @@ -12326,7 +12322,7 @@ private int hash32; 左图表示对象引用图,右图表示左图所对应的支配树: -![](https://gitee.com/seazean/images/raw/master/Java/JVM-支配树.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-支配树.png) 比如:对象 F 与对象 D 相互引用,因为到对象 F 的所有路径必然经过对象 D,因此对象 D 是对象 F 的直接支配者 @@ -12373,7 +12369,7 @@ JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: 优点:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-句柄访问.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-对象访问-句柄访问.png) * 直接指针(HotSpot 采用):Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的对象地址 @@ -12381,7 +12377,7 @@ JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: 缺点:对象被移动时(如进行 GC 后的内存重新排列),对象的 reference 也需要同步更新 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-直接指针.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-对象访问-直接指针.png) @@ -12573,7 +12569,7 @@ Java 对象创建时机: 类是在运行期间**第一次使用时动态加载**的(不使用不加载),而不是一次性加载所有类,因为一次性加载会占用很多的内存,加载的类信息存放于一块成为方法区的内存空间 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-类的生命周期.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-类的生命周期.png) 包括 7 个阶段: @@ -12619,7 +12615,7 @@ Java 对象创建时机: * 加载和链接可能是交替运行的 * Class 对象和 _java_mirror 相互持有对方的地址,堆中对象通过 instanceKlass 和元空间进行交互 - + 创建数组类有些特殊,因为数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建,创建数组类的过程: @@ -13039,7 +13035,7 @@ ClassLoader 类常用方法: 双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但**顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类**(可见性) - + @@ -13135,7 +13131,7 @@ protected Class loadClass(String name, boolean resolve) 热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为,**热替换的关键需求在于服务不能中断**,修改必须立即表现正在运行的系统之中 - + @@ -13154,7 +13150,7 @@ protected Class loadClass(String name, boolean resolve) * JDK1.2:改进了安全机制,增加了代码签名,不论本地代码或是远程代码都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制 * JDK1.6:当前最新的安全机制,引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,不同的保护域对应不一样的权限。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问 - + @@ -13272,7 +13268,7 @@ public static void main(String[] args) { Java 文件编译执行的过程: -![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java文件编译执行的过程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-Java文件编译执行的过程.png) - 类加载器:用于装载字节码文件(.class文件) - 运行时数据区:用于分配存储空间 @@ -13347,7 +13343,7 @@ Java 语言:跨平台的语言(write once ,run anywhere) * 字节码为了实现特定软件运行和软件环境,与硬件环境无关 * 通过编译器和虚拟机器实现,编译器将源码编译成字节码,虚拟机器将字节码转译为可以直接执行的指令 - + @@ -13452,7 +13448,7 @@ Class 文件格式采用一种类似于 C 语言结构体的方式进行数据 | 54 | 0 | 1.10 | | 55 | 0 | 1.11 | -![](https://gitee.com/seazean/images/raw/master/Java/JVM-类结构.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-类结构.png) @@ -14260,7 +14256,7 @@ finally 中的代码被**复制了 3 份**,分别放入 try 流程,catch 流 * montiorenter:进入并获取对象监视器,即为栈顶对象加锁 * monitorexit:释放并退出对象监视器,即为栈顶对象解锁 - + @@ -14304,12 +14300,12 @@ javap -v Demo.class:省略 `istore_1`:将操作数栈顶数据弹出,存入局部变量表的 slot 1 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-字节码执行流程1.png) `ldc #3`:从常量池加载 #3 数据到操作数栈 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算完成 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-字节码执行流程2.png) `istore_2`:将操作数栈顶数据弹出,存入局部变量表的 slot 2 @@ -14319,17 +14315,17 @@ javap -v Demo.class:省略 `iadd`:执行相加操作 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程3.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-字节码执行流程3.png) `istore_3`:将操作数栈顶数据弹出,存入局部变量表的 slot 3 `getstatic #4`:获取静态字段 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程4.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-字节码执行流程4.png) `iload_3`: - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程5.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-字节码执行流程5.png) `invokevirtual #5`: @@ -14340,7 +14336,7 @@ javap -v Demo.class:省略 * 执行完毕,弹出栈帧 * 清除 main 操作数栈内容 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程6.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-字节码执行流程6.png) return:完成 main 方法调用,弹出 main 栈帧,程序结束 @@ -14389,7 +14385,7 @@ HotSpot VM 可以通过 VM 参数设置程序执行方式: - -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行 - -Xmixed:采用解释器 + 即时编译器的混合模式共同执行程序 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-执行引擎工作流程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-执行引擎工作流程.png) @@ -14737,7 +14733,7 @@ Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类 * 其一,子类方法表中包含父类方法表中的**所有方法**,并且在方法表中的索引值与父类方法表种的索引值相同 * 其二,**非重写的方法指向父类的方法表项,与父类共享一个方法表项,重写的方法指向本身自己的实现**。所以这就是为什么多态情况下可以访问父类的方法。 - + Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方法表调换了 toString 方法和 passThroughImmigration 方法的位置,是因为 toString 方法的索引值需要与 Object 类中同名方法的索引值一致,为了保持简洁,这里不考虑 Object 类中的其他方法。 @@ -14772,7 +14768,7 @@ class Girl extends Person { } ``` -![](https://gitee.com/seazean/images/raw/master/Java/JVM-虚方法表指向.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-虚方法表指向.png) @@ -15736,7 +15732,7 @@ jstatd 是一个 RMI 服务端程序,相当于代理服务器,建立本地 远程主机信息收集,前面的指令只涉及到监控本机的 Java 应用程序,而在这些工具中,一些监控工具也支持对远程计算机的监控(如 jps、jstat),为了启用远程监控,则需要配合使用 jstatd 工具。 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-jstatd图解.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JVM-jstatd图解.png) @@ -16317,7 +16313,7 @@ public class BeerDemo{ 1. 确定总共需要冒几轮:数组的长度-1 2. 每轮两两比较几次 - + ```java // 0 1位置比较,大的放后面,然后1 2位置比较,大的继续放后面,一轮循环最后一位是最大值 @@ -16374,7 +16370,7 @@ public class BubbleSort { 1. 控制选择几轮:数组的长度 - 1 2. 控制每轮从当前位置开始比较几次 - + ```java public class SelectSort { @@ -16426,7 +16422,7 @@ public class SelectSort { 3. 交换后新的堆顶 R[1] 可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将 R[1] 与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn),不断重复此过程直到有序区的元素个数为 n-1,则整个排序过程完成 - + floor:向下取整 @@ -16496,7 +16492,7 @@ public class HeapSort { 插入排序(Insertion Sort):在要排序的一组数中,假定前 n-1 个数已经排好序,现在将第 n 个数插到这个有序数列中,使得这 n 个数也是排好顺序的,如此反复循环,直到全部排好顺序 - + ```java public class InsertSort { @@ -16543,7 +16539,7 @@ public class InsertSort { 2. 对分好组的每一组数据完成插入排序 3. 减小增长量,最小减为 1,重复第二步操作 - + 希尔排序的核心在于间隔序列的设定,既可以提前设定好间隔序列,也可以动态的定义间隔序列,希尔排序就是插入排序增加了间隔 @@ -16598,15 +16594,15 @@ public class ShellSort { 2. 将相邻的两个子组进行合并成一个有序的大组 3. 不断的重复步骤2,直到最终只有一个组为止 - + 归并步骤:每次比较两端最小的值,把最小的值放在辅助数组的左边 -![](https://gitee.com/seazean/images/raw/master/Java/Sort-归并步骤1.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-归并步骤1.png) -![](https://gitee.com/seazean/images/raw/master/Java/Sort-归并步骤2.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-归并步骤2.png) -![](https://gitee.com/seazean/images/raw/master/Java/Sort-归并步骤3.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-归并步骤3.png) @@ -16659,7 +16655,7 @@ public class MergeSort { } ``` - + 用树状图来描述归并,假设元素的个数为 n,那么使用归并排序拆分的次数为 `log2(n)`,即层数,每次归并需要做 n 次对比,最终得出的归并排序的时间复杂度为 `log2(n)*n`,根据大O推导法则,忽略底数,最终归并排序的时间复杂度为 O(nlogn) @@ -16683,7 +16679,7 @@ public class MergeSort { 2. 重新排序数列,所有比基准值小的摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边),在这个分区退出之后,该基准就处于数列的中间位置,这个称为分区(partition)操作; 3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序 - + ```java public class QuickSort { @@ -16743,7 +16739,7 @@ public class QuickSort { * 最坏情况:每一次切分选择的基准数字是当前序列中最大数或者最小数,这使得每次切分都会有一个子组,那么总共就得切分n次,所以最坏情况下,快速排序的时间复杂度为 O(n^2) - + * 平均情况:每一次切分选择的基准数字不是最大值和最小值,也不是中值,这种情况用数学归纳法证明,快速排序的时间复杂度为 O(nlogn) @@ -16771,7 +16767,7 @@ public class QuickSort { 解释:先排低位再排高位,可以说明在高位相等的情况下低位是递增的,如果高位也是递增,则数据有序 - + 实现思路: @@ -16849,7 +16845,7 @@ public class BucketSort { 如果一组数据只需要一次排序,则稳定性一般是没有意义的,如果一组数据需要多次排序,稳定性是有意义的。 - + * 冒泡排序:只有当 `arr[i]>arr[i+1]` 的时候,才会交换元素的位置,而相等的时候并不交换位置,所以冒泡排序是一种稳定排序算法 * 选择排序:是给每个位置选择当前元素最小的,例如有数据{5(1),8 ,5(2), 3, 9 },第一遍选择到的最小元素为3,所以5(1)会和3进行交换位置,此时5(1)到了5(2)后面,破坏了稳定性,所以是不稳定的排序算法 @@ -16874,7 +16870,7 @@ public class BucketSort { #### 算法对比 -![](https://gitee.com/seazean/images/raw/master/Java/Sort-排序算法对比.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Sort-排序算法对比.png) @@ -16943,7 +16939,7 @@ public class binarySearch { } ``` -![](https://gitee.com/seazean/images/raw/master/Java/二分查找.gif) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/二分查找.gif) 查找第一个匹配的元素: @@ -17146,7 +17142,7 @@ public class Kmp { + 节点:在树结构中,每一个元素称之为节点 + 度:每一个节点的子节点数量称之为度 -二叉树结构图 +二叉树结构图 @@ -17165,7 +17161,7 @@ public class Kmp { + 右子树上所有节点的值都大于根节点的值 + 不存在重复的节点 -二叉查找树 +二叉查找树 @@ -17261,7 +17257,7 @@ public class Kmp { } ``` -* 删除节点:要删除节点12,先找到节点19,然后移动并替换节点12 +* 删除节点:要删除节点12,先找到节点19,然后移动并替换节点12 代码链接:https://leetcode-cn.com/submissions/detail/190232548/ @@ -17290,15 +17286,15 @@ public class Kmp { + 平衡二叉树和二叉查找树对比结构图 -![平衡二叉树和二叉查找树对比](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树和二叉查找树对比结构图.png) +![平衡二叉树和二叉查找树对比](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/平衡二叉树和二叉查找树对比结构图.png) + 左旋:将根节点的右侧往左拉,原先的右子节点变成新的父节点,并把多余的左子节点出让,给已经降级的根节点当右子节点 - ![平衡二叉树左旋](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树左旋01.png) + ![平衡二叉树左旋](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/平衡二叉树左旋01.png) * 右旋:将根节点的左侧往右拉,左子节点变成了新的父节点,并把多余的右子节点出让,给已经降级根节点当左子节点 - ![平衡二叉树右旋](https://gitee.com/seazean/images/raw/master/Java/平衡二叉树右旋01.png) + ![平衡二叉树右旋](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/平衡二叉树右旋01.png) 推荐文章:https://pdai.tech/md/algorithm/alg-basic-tree-balance.html @@ -17332,12 +17328,12 @@ public class Kmp { - 红黑树整体性能略优于 AVL 树,AVL 树的旋转比红黑树的旋转多,更加难以平衡和调试,插入和删除的效率比红黑树慢 -![红黑树](https://gitee.com/seazean/images/raw/master/Java/红黑树结构图.png) +![红黑树](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/红黑树结构图.png) 红黑树添加节点的默认颜色为红色,效率高 -![](https://gitee.com/seazean/images/raw/master/Java/红黑树添加节点颜色.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/红黑树添加节点颜色.png) @@ -17377,7 +17373,7 @@ public class Kmp { * 一个组中的数据对应的树和另外一个组中的数据对应的树之间没有任何联系 * 元素在树中并没有子父级关系的硬性要求 - + 可以高效地进行如下操作: @@ -17386,11 +17382,11 @@ public class Kmp { 存储结构: - + 合并方式: - + @@ -17489,7 +17485,7 @@ public class Kmp { 让每个索引处的节点都指向它的父节点,当 eleGroup[i] = i 时,说明 i 是根节点 - + ```java //查询p所在的分组的标识符,递归寻找父标识符,直到找到根节点 @@ -17522,7 +17518,7 @@ public void union(int p, int q) { 平均时间复杂度为 O(N),最坏时间复杂度是 O(N^2) - + 继续优化:路径压缩,保证每次把小树合并到大树 @@ -17595,7 +17591,7 @@ public class UF_Tree_Weighted { 畅通工程:某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府畅通工程的目标是使全省任何两个城镇间都可以实现交通,但不一定有直接的道路相连,只要互相间接通过道路可达即可,问最少还需要建设多少条道路? - + 解题思路: @@ -17644,9 +17640,9 @@ Trie 树,也叫字典树,是一种专门处理字符串匹配的树形结构 * 每个节点表示一个字符串中的字符,从**根节点到红色节点的一条路径表示一个字符串** * 红色节点并不都是叶子节点 - + - + 注意:要查找的是字符串“he”,从根节点开始,沿着某条路径来匹配,可以匹配成功。但是路径的最后一个节点“e”并不是红色的,也就是说,“he”是某个字符串的前缀子串,但并不能完全匹配任何字符串 @@ -17660,7 +17656,7 @@ Trie 树,也叫字典树,是一种专门处理字符串匹配的树形结构 通过一个下标与字符一一映射的数组,来存储子节点的指针 - + 时间复杂度是 O(n)(n 表示要查找字符串的长度) @@ -17724,7 +17720,7 @@ public class Trie { Trie 树是非常耗内存,采取空间换时间的思路。Trie 树的变体有很多,可以在一定程度上解决内存消耗的问题。比如缩点优化,对只有一个子节点的节点,而且此节点不是一个串的结束节点,可以将此节点与子节点合并 -![](https://gitee.com/seazean/images/raw/master/Java/Tree-字典树缩点优化.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/Tree-字典树缩点优化.png) @@ -17819,7 +17815,7 @@ public class MGraph { 布隆过滤器:一种数据结构,是一个很长的二进制向量(位数组)和一系列随机映射函数(哈希函数),既然是二进制,每个空间存放的不是 0 就是 1,但是初始默认值都是 0,所以布隆过滤器不存数据只存状态 - + 这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且理论情况下,添加到集合中的元素越多,误报的可能性就越大 @@ -17833,7 +17829,7 @@ public class MGraph { 向布隆过滤器中添加一个元素 key 时,会通过多个 hash 函数得到多个哈希值,在位数组中把对应下标的值置为 1 -![](https://gitee.com/seazean/images/raw/master/DB/Redis-布隆过滤器添加数据.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/DB/Redis-布隆过滤器添加数据.png) 布隆过滤器查询一个数据,是否在二进制的集合中,查询过程如下: diff --git a/Prog.md b/Prog.md index 59e5cbc..73034ba 100644 --- a/Prog.md +++ b/Prog.md @@ -265,7 +265,7 @@ Java Virtual Machine Stacks(Java 虚拟机栈):每个线程启动后,虚 程序计数器(Program Counter Register):记住下一条 JVM 指令的执行地址,是线程私有的 -当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等 +当 Context Switch 发生时,需要由操作系统保存当前线程的状态(PCB 中),并恢复另一个线程的状态,包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等 Java 创建的线程是内核级线程,**线程的调度是在内核态运行的,而线程中的代码是在用户态运行**,所以线程切换(状态改变)会导致用户与内核态转换,这是非常消耗性能 @@ -523,7 +523,7 @@ LockSupport 类在 同步 → park-un 详解 两阶段终止模式图示: - + 打断线程可能在任何时间,所以需要考虑在任何时刻被打断的处理方法: @@ -647,7 +647,7 @@ t.start(); | Timed Waiting (计时等待) | 有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait | | Teminated(被终止) | run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡 | -![](https://gitee.com/seazean/images/raw/master/Java/JUC-线程6种状态.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-线程6种状态.png) * NEW → RUNNABLE:当调用 t.start() 方法时,由 NEW → RUNNABLE @@ -921,23 +921,23 @@ Monitor 被翻译为监视器或管程 * Mark Word 结构: - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-Monitor-MarkWord结构32位.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor-MarkWord结构32位.png) * 64 位虚拟机 Mark Word: - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-Monitor-MarkWord结构64位.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor-MarkWord结构64位.png) 工作流程: * 开始时 Monitor 中 Owner 为 null * 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner,**obj 对象的 Mark Word 指向 Monitor**,把**对象原有的 MarkWord 存入线程栈中的锁记录**中(轻量级锁部分详解) - + * 在 Thread-2 上锁的过程,Thread-3、Thread-4、Thread-5 也执行 synchronized(obj),就会进入 EntryList BLOCKED(双向链表) * Thread-2 执行完同步代码块的内容,根据 obj 对象头中 Monitor 地址寻找,设置 Owner 为空,把线程栈的锁记录中的对象头的值设置回 MarkWord * 唤醒 EntryList 中等待的线程来竞争锁,竞争是**非公平的**,如果这时有新的线程想要获取锁,可能直接就抢占到了,阻塞队列的线程就会继续阻塞 * WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify 机制) -![](https://gitee.com/seazean/images/raw/master/Java/JUC-Monitor工作原理2.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor工作原理2.png) 注意: @@ -1016,7 +1016,7 @@ LocalVariableTable: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 // 随着竞争的增加,只能锁升级,不能降级 ``` -![](https://gitee.com/seazean/images/raw/master/Java/JUC-锁升级过程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-锁升级过程.png) @@ -1032,7 +1032,7 @@ LocalVariableTable: * 当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定或轻量级锁状态 - + 一个对象创建时: @@ -1089,19 +1089,19 @@ public static void method2() { * 创建锁记录(Lock Record)对象,每个线程的**栈帧**都会包含一个锁记录的结构,存储锁定对象的 Mark Word - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-轻量级锁原理1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-轻量级锁原理1.png) * 让锁记录中 Object reference 指向锁住的对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录 * 如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-轻量级锁原理2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-轻量级锁原理2.png) * 如果 CAS 失败,有两种情况: * 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程 * 如果是线程自己执行了 synchronized 锁重入,就添加一条 Lock Record 作为重入的计数 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-轻量级锁原理3.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-轻量级锁原理3.png) * 当退出 synchronized 代码块(解锁时) @@ -1122,11 +1122,11 @@ public static void method2() { * 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-重量级锁原理1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-重量级锁原理1.png) * Thread-1 加轻量级锁失败,进入锁膨胀流程:为 Object 对象申请 Monitor 锁,**通过 Object 对象头获取到持锁线程**,将 Monitor 的 Owner 置为 Thread-0,将 Object 的对象头指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-重量级锁原理2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-重量级锁原理2.png) * 当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头失败,这时进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程 @@ -1156,11 +1156,11 @@ public static void method2() { 自旋锁情况: * 自旋成功的情况: - + * 自旋失败的情况: - + 自旋锁说明: @@ -1528,7 +1528,7 @@ public final native void wait(long timeout):有时限的等待, 到n毫秒后结 * BLOCKED 线程会在 Owner 线程释放锁时唤醒 * WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,唤醒后并不意味者立刻获得锁,**需要进入 EntryList 重新竞争** -![](https://gitee.com/seazean/images/raw/master/Java/JUC-Monitor工作原理2.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Monitor工作原理2.png) @@ -1660,7 +1660,7 @@ LockSupport 出现就是为了增强 wait & notify 的功能: 4. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1 5. 唤醒 _cond 条件变量中的 Thread_0,Thread_0 恢复运行,设置 _counter 为 0 -![](https://gitee.com/seazean/images/raw/master/Java/JUC-park原理1.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-park原理1.png) * 先 unpark: @@ -1668,7 +1668,7 @@ LockSupport 出现就是为了增强 wait & notify 的功能: 2. 当前线程调用 Unsafe.park() 方法 3. 检查 _counter ,本情况为 1,这时线程无需挂起,继续运行,设置 _counter 为 0 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-park原理2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-park原理2.png) @@ -1742,7 +1742,7 @@ Guarded Suspension,用在一个线程等待另一个线程的执行结果 * 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者) * JDK 中,join 的实现、Future 的实现,采用的就是此模式 -![](https://gitee.com/seazean/images/raw/master/Java/JUC-保护性暂停.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-保护性暂停.png) ```java public static void main(String[] args) { @@ -1813,7 +1813,7 @@ class GuardedObject { 多任务版保护性暂停: -![](https://gitee.com/seazean/images/raw/master/Java/JUC-保护性暂停多任务版.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-保护性暂停多任务版.png) ```java public static void main(String[] args) throws InterruptedException { @@ -2065,7 +2065,7 @@ public class TraditionalProducerConsumer { * 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据 * JDK 中各种阻塞队列,采用的就是这种模式 -![](https://gitee.com/seazean/images/raw/master/Java/JUC-生产者消费者模式.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-生产者消费者模式.png) ```java public class demo { @@ -2205,7 +2205,7 @@ JMM 作用: 根据 JMM 的设计,系统存在一个主内存(Main Memory),Java 中所有变量都存储在主存中,对于所有线程都是共享的;每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些**变量的拷贝**,线程对所有变量的操作都是先对变量进行拷贝,然后在工作内存中进行,不能直接操作主内存中的变量;线程之间无法相互直接访问,线程间的通信(传递)必须通过主内存来完成 -![](https://gitee.com/seazean/images/raw/master/Java/JMM内存模型.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM内存模型.png) 主内存和工作内存: @@ -2227,7 +2227,7 @@ JMM 作用: Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作: - + * lock:将一个变量标识为被一个线程**独占状态** * unclock:将一个变量从独占状态释放出来,释放后的变量才可以被其他线程锁定 @@ -2278,7 +2278,7 @@ public static void main(String[] args) throws InterruptedException { * 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率 * 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值 -![](https://gitee.com/seazean/images/raw/master/Java/JMM-可见性例子.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-可见性例子.png) @@ -2344,7 +2344,7 @@ CPU 的基本工作是执行存储的指令序列,即程序,程序的执行 CPU 处理器速度远远大于在主内存中的,为了解决速度差异,在它们之间架设了多级缓存,如 L1、L2、L3 级别的缓存,这些缓存离 CPU 越近就越快,将频繁操作的数据缓存到这里,加快访问速度 - + | 从 CPU 到 | 大约需要的时钟周期 | | --------- | --------------------------------- | @@ -2374,7 +2374,7 @@ CPU 处理器速度远远大于在主内存中的,为了解决速度差异, 缓存会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中,CPU 要保证数据的一致性,需要做到某个 CPU 核心更改了数据,其它 CPU 核心对应的**整个缓存行必须失效**,这就是伪共享 - + 解决方法: @@ -2576,7 +2576,7 @@ lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) } ``` - + * 全能屏障:mfence(modify/mix Barrier),兼具 sfence 和 lfence 的功能 @@ -2605,7 +2605,7 @@ lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence) 2: iinc 1, 1 ``` - + @@ -2697,7 +2697,7 @@ getInstance 方法对应的字节码为: * 关键在于 0:getstatic 这行代码在 monitor 控制之外,可以越过 monitor 读取 INSTANCE 变量的值 * 当其他线程访问 INSTANCE 不为 null 时,由于 INSTANCE 实例未必已初始化,那么 t2 拿到的是将是一个未初始化完毕的单例返回,这就造成了线程安全的问题 -![](https://gitee.com/seazean/images/raw/master/Java/JMM-DCL出现的问题.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JMM-DCL出现的问题.png) @@ -3221,11 +3221,11 @@ Cell 为累加单元:数组访问索引是通过 Thread 里的 threadLocalRand Cell 是数组形式,**在内存中是连续存储的**,64 位系统中,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),每一个 cache line 为 64 字节,因此缓存行可以存下 2 个的 Cell 对象,当 Core-0 要修改 Cell[0]、Core-1 要修改 Cell[1],无论谁修改成功都会导致当前缓存行失效,从而导致对方的数据失效,需要重新去主存获取,影响效率 -![](https://gitee.com/seazean/images/raw/master/Java/JUC-伪共享1.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-伪共享1.png) @sun.misc.Contended:防止缓存行伪共享,在使用此注解的对象或字段的前后各增加 128 字节大小的 padding,使用 2 倍于大多数硬件缓存行让 CPU 将对象预读至缓存时**占用不同的缓存行**,这样就不会造成对方缓存行的失效 -![](https://gitee.com/seazean/images/raw/master/Java/JUC-伪共享2.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-伪共享2.png) @@ -3774,7 +3774,7 @@ public class ThreadLocalDateUtil { JDK8 以前:每个 ThreadLocal 都创建一个 Map,然后用线程作为 Map 的 key,要存储的局部变量作为 Map 的 value,达到各个线程的局部变量隔离的效果。这种结构会造成 Map 结构过大和内存泄露,因为 Thread 停止后无法通过 key 删除对应的数据 -![](https://gitee.com/seazean/images/raw/master/Java/JUC-ThreadLocal数据结构JDK8前.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal数据结构JDK8前.png) JDK8 以后:每个 Thread 维护一个 ThreadLocalMap,这个 Map 的 key 是 ThreadLocal 实例本身,value 是真正要存储的值 @@ -3783,7 +3783,7 @@ JDK8 以后:每个 Thread 维护一个 ThreadLocalMap,这个 Map 的 key 是 * Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值 * 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成副本的隔离,互不干扰 -![](https://gitee.com/seazean/images/raw/master/Java/JUC-ThreadLocal数据结构JDK8后.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ThreadLocal数据结构JDK8后.png) JDK8 前后对比: @@ -4120,7 +4120,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { } ``` - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-replaceStaleEntry流程.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-replaceStaleEntry流程.png) ```java private static int prevIndex(int i, int len) { @@ -4315,9 +4315,9 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { } ``` - + - + * 启发式清理:向后循环扫描过期数据,发现过期数据调用探测式清理方法,如果连续几次的循环都没有发现过期数据,就停止扫描 @@ -4369,11 +4369,11 @@ Memory leak:内存泄漏是指程序中动态分配的堆内存由于某种原 * 如果 key 使用强引用:使用完 ThreadLocal ,threadLocal Ref 被回收,但是 threadLocalMap 的 Entry 强引用了 threadLocal,造成 threadLocal 无法被回收,无法完全避免内存泄漏 - + * 如果 key 使用弱引用:使用完 ThreadLocal ,threadLocal Ref 被回收,ThreadLocalMap 只持有 ThreadLocal 的弱引用,所以threadlocal 也可以被回收,此时 Entry 中的 key = null。但没有手动删除这个 Entry 或者 CurrentThread 依然运行,依然存在强引用链,value 不会被回收,而这块 value 永远不会被访问到,也会导致 value 内存泄漏 - + * 两个主要原因: @@ -4622,7 +4622,7 @@ public class LinkedBlockingQueue extends AbstractQueue } ``` - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-LinkedBlockingQueue入队流程.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-LinkedBlockingQueue入队流程.png) * 再来一个节点入队 `last = last.next = node` @@ -4648,11 +4648,11 @@ public class LinkedBlockingQueue extends AbstractQueue * `h = head` → `first = h.next` - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-LinkedBlockingQueue出队流程1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-LinkedBlockingQueue出队流程1.png) * `h.next = h` → `head = first` - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-LinkedBlockingQueue出队流程2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-LinkedBlockingQueue出队流程2.png) * `first.item = null`:当前节点置为 Dummy 节点 @@ -5462,7 +5462,7 @@ public ThreadPoolExecutor(int corePoolSize, 工作原理: -![](https://gitee.com/seazean/images/raw/master/Java/JUC-线程池工作原理.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-线程池工作原理.png) 1. 创建线程池,这时没有创建线程(**懒惰**),等待提交过来的任务请求,调用 execute 方法才会创建线程 @@ -5539,7 +5539,7 @@ Executors 提供了四种线程池的创建:newCachedThreadPool、newFixedThre * Executors.newFixedThreadPool(1) 初始时为1,可以修改。对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改 -![](https://gitee.com/seazean/images/raw/master/Java/JUC-newSingleThreadExecutor.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-newSingleThreadExecutor.png) @@ -5689,7 +5689,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 private static final int CAPACITY = (1 << COUNT_BITS) - 1; ``` - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-线程池状态转换图.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-线程池状态转换图.png) * 四种状态: @@ -7831,7 +7831,7 @@ AQS 核心思想: CLH 是一种基于单向链表的**高性能、公平的自旋锁**,AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配 - + @@ -7931,7 +7931,7 @@ AbstractQueuedSynchronizer 中 state 设计: } ``` - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-AQS队列设计.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-AQS队列设计.png) * 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet,**条件队列是单向链表** @@ -8181,7 +8181,7 @@ public void lock() { } ``` - + * 进入 tryAcquire 尝试获取锁逻辑,这时 state 已经是1,结果仍然失败(第二次),加锁成功有两种情况: @@ -8275,7 +8275,7 @@ public void lock() { } ``` - + * 线程节点加入阻塞队列成功,进入 AbstractQueuedSynchronizer#acquireQueued 逻辑阻塞线程 @@ -8357,7 +8357,7 @@ public void lock() { * 再有多个线程经历竞争失败后: - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-非公平锁3.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-非公平锁3.png) @@ -8457,14 +8457,14 @@ Thread-0 释放锁,进入 release 流程 * head 指向刚刚 Thread-1 所在的 Node,该 Node 会清空 Thread * 原本的 head 因为从链表断开,而可被垃圾回收(图中有错误,原来的头节点的 waitStatus 被改为 0 了) - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-非公平锁4.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-非公平锁4.png) * 如果这时有其它线程来竞争**(非公平)**,例如这时有 Thread-4 来了并抢占了锁 * Thread-4 被设置为 exclusiveOwnerThread,state = 1 * Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-非公平锁5.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-非公平锁5.png) @@ -9039,7 +9039,7 @@ public static void main(String[] args) throws InterruptedException { private static final int THROW_IE = -1; ``` - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-条件变量1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-条件变量1.png) * **创建新的 Node 状态为 -2(Node.CONDITION)**,关联 Thread-0,加入等待队列尾部 @@ -9130,7 +9130,7 @@ public static void main(String[] args) throws InterruptedException { * fullyRelease 中会 unpark AQS 队列中的下一个节点竞争锁,假设 Thread-1 竞争成功 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-条件变量2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-条件变量2.png) * Thread-0 进入 isOnSyncQueue 逻辑判断节点**是否移动到阻塞队列**,没有就 park 阻塞 Thread-0 @@ -9271,7 +9271,7 @@ public static void main(String[] args) throws InterruptedException { } ``` - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantLock-条件变量3.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantLock-条件变量3.png) * Thread-1 释放锁,进入 unlock 流程 @@ -9388,7 +9388,7 @@ public static void main(String[] args) { * 补充情况:查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询 - + 可以使用读写锁进行操作 @@ -9723,11 +9723,11 @@ Sync 类的属性: 如果没有成功,在 doAcquireShared 内 for (;;) 循环一次,shouldParkAfterFailedAcquire 内把前驱节点的 waitStatus 改为 -1,再 for (;;) 循环一次尝试 tryAcquireShared,不成功在 parkAndCheckInterrupt() 处 park - + * 这种状态下,假设又有 t3 r.lock,t4 w.lock,这期间 t1 仍然持有锁,就变成了下面的样子 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ReentrantReadWriteLock加锁2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ReentrantReadWriteLock加锁2.png) @@ -9819,7 +9819,7 @@ Sync 类的属性: } ``` - + * 下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点 @@ -9854,7 +9854,7 @@ Sync 类的属性: * t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;;) 这次自己是头节点的临节点,并且没有其他节点竞争,tryAcquire(1) 成功,修改头结点,流程结束 - + @@ -10283,7 +10283,7 @@ public static void main(String[] args) { } ``` - + @@ -10577,7 +10577,7 @@ public static void main(String[] args) { } ``` - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-Semaphore工作流程1.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Semaphore工作流程1.png) * 这时 Thread-4 释放了 permits,状态如下 @@ -10611,7 +10611,7 @@ public static void main(String[] args) { } ``` - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-Semaphore工作流程2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-Semaphore工作流程2.png) * 接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,并且 unpark 接下来的共享状态的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态 @@ -10806,7 +10806,7 @@ class ThreadB extends Thread{ 4. ConcurrentHashMap、Hashtable **不允许 null 值**,HashMap 允许 null 值 5. ConcurrentHashMap、HashMap 的初始容量为 16,Hashtable 初始容量为11,填充因子默认都是 0.75,两种 Map 扩容是当前容量翻倍:capacity * 2,Hashtable 扩容时是容量翻倍 + 1:capacity*2 + 1 -![ConcurrentHashMap数据结构](https://gitee.com/seazean/images/raw/master/Java/ConcurrentHashMap数据结构.png) +![ConcurrentHashMap数据结构](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/ConcurrentHashMap数据结构.png) 工作步骤: @@ -11778,7 +11778,7 @@ public V put(K key, V value) { 链表处理的 LastRun 机制,**可以减少节点的创建** - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentHashMap-LastRun机制.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentHashMap-LastRun机制.png) * helpTransfer():帮助扩容机制 @@ -12026,7 +12026,7 @@ ConcurrentHashMap 对锁粒度进行了优化,**分段锁技术**,将整张 * 缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentHashMap 1.7底层结构.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentHashMap 1.7底层结构.png) @@ -12140,7 +12140,7 @@ public CopyOnWriteArraySet() { * 弱一致性:系统并不保证进程或者线程的访问都会返回最新的更新过的值,也不会承诺多久之后可以读到 - + | 时间点 | 操作 | | ------ | ---------------------------- | @@ -12255,7 +12255,7 @@ ConcurrentSkipListMap 提供了一种线程安全的并发访问的排序映射 * 对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整;而对跳表的插入和删除,**只需要对整个结构的局部进行操作** * 在高并发的情况下,保证整个平衡树的线程安全需要一个全局锁;对于跳表则只需要部分锁,拥有更好的性能 -![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentSkipListMap数据结构.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentSkipListMap数据结构.png) BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指向链表最下面的节点** @@ -12424,7 +12424,7 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 } ``` - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentSkipListMap-Put流程.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentSkipListMap-Put流程.png) * put():添加数据 @@ -12786,7 +12786,7 @@ BaseHeader 存储数据,headIndex 存储索引,纵向上**所有索引都指 经过 findPredecessor() 中的 unlink() 后索引已经被删除 - ![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentSkipListMap-remove流程.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentSkipListMap-remove流程.png) * appendMarker():添加删除标记节点 @@ -12971,11 +12971,11 @@ public boolean offer(E e) { 图解入队: -![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentLinkedQueue入队操作1.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentLinkedQueue入队操作1.png) -![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentLinkedQueue入队操作2.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentLinkedQueue入队操作2.png) -![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentLinkedQueue入队操作3.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentLinkedQueue入队操作3.png) 当 tail 节点和尾节点的距离**大于等于 1** 时(每入队两次)更新 tail,可以减少 CAS 更新 tail 节点的次数,提高入队效率 @@ -13038,11 +13038,11 @@ final void updateHead(Node h, Node p) { 在更新完 head 之后,会将旧的头结点 h 的 next 域指向为 h,图中所示的虚线也就表示这个节点的自引用,被移动的节点(item 为 null 的节点)会被 GC 回收 -![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentLinkedQueue出队操作1.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentLinkedQueue出队操作1.png) -![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentLinkedQueue出队操作2.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentLinkedQueue出队操作2.png) -![](https://gitee.com/seazean/images/raw/master/Java/JUC-ConcurrentLinkedQueue出队操作3.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/JUC-ConcurrentLinkedQueue出队操作3.png) 如果这时,有一个线程来添加元素,通过 tail 获取的 next 节点则仍然是它本身,这就出现了p == q 的情况,出现该种情况之后,则会触发执行 head 的更新,将 p 节点重新指向为 head @@ -13303,7 +13303,7 @@ Linux 有五种 I/O 模型: recvfrom() 用于**接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中**,把 recvfrom() 当成系统调用 -![](https://gitee.com/seazean/images/raw/master/Java/IO模型-阻塞式IO.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO模型-阻塞式IO.png) @@ -13317,7 +13317,7 @@ recvfrom() 用于**接收 Socket 传来的数据,并复制到应用进程的 由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率比较低 -![](https://gitee.com/seazean/images/raw/master/Java/IO模型-非阻塞式IO.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO模型-非阻塞式IO.png) @@ -13331,7 +13331,7 @@ recvfrom() 用于**接收 Socket 传来的数据,并复制到应用进程的 相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高 -![](https://gitee.com/seazean/images/raw/master/Java/IO模型-信号驱动IO.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO模型-信号驱动IO.png) @@ -13347,7 +13347,7 @@ IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Ev 如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都要创建一个线程去处理,如果同时有几万个连接,就需要创建相同数量的线程。相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小 -![](https://gitee.com/seazean/images/raw/master/Java/IO模型-IO复用模型.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO模型-IO复用模型.png) @@ -13361,7 +13361,7 @@ IO 复用让单个进程具有处理多个 I/O 事件的能力,又被称为 Ev 异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O -![](https://gitee.com/seazean/images/raw/master/Java/IO模型-异步IO模型.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO模型-异步IO模型.png) @@ -13463,7 +13463,7 @@ while(1) { select 调用流程图: -![](https://gitee.com/seazean/images/raw/master/Java/IO-select调用过程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-select调用过程.png) 1. 使用 copy_from_user 从用户空间拷贝 fd_set 到内核空间,进程阻塞 2. 注册回调函数 _pollwait @@ -13692,7 +13692,7 @@ epoll 的特点: * 线程上下文:用户程序基地址,程序计数器、cpu cache、寄存器等,方便程序切回用户态时恢复现场 * 内核堆栈:**系统调用函数也是要创建变量的,**这些变量在内核堆栈上分配 -![](https://gitee.com/seazean/images/raw/master/Java/IO-用户态和内核态.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-用户态和内核态.png) @@ -13718,7 +13718,7 @@ epoll 的特点: * 执行 80 中断处理程序,找到刚刚存的系统调用号(read),先检查缓存中有没有对应的数据,没有就去磁盘中加载到内核缓冲区,然后从内核缓冲区拷贝到用户空间 * 最后恢复到用户态,通过 thread_info 恢复现场,用户态继续执行 -![](https://gitee.com/seazean/images/raw/master/Java/IO-系统调用的过程.jpg) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-系统调用的过程.jpg) @@ -13745,7 +13745,7 @@ DMA (Direct Memory Access) :直接存储器访问,让外部设备不通过 C 一个完整的 DMA 传输过程必须经历 DMA 请求、DMA 响应、DMA 传输、DMA 结束四个步骤: - + DMA 方式是一种完全由硬件进行信息传送的控制方式,通常系统总线由 CPU 管理,在 DMA 方式中,CPU 的主存控制信号被禁止使用,CPU 把总线(地址总线、数据总线、控制总线)让出来由 DMA 控制器接管,用来控制传送的字节数、判断 DMA 是否结束、以及发出 DMA 结束信号,所以 DMA 控制器必须有以下功能: @@ -13772,11 +13772,11 @@ DMA 方式是一种完全由硬件进行信息传送的控制方式,通常系 流程图中的箭头反过来也成立,可以从网卡获取数据 -![](https://gitee.com/seazean/images/raw/master/Java/IO-BIO工作流程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-BIO工作流程.png) read 调用图示:read、write 都是系统调用指令 - + @@ -13795,7 +13795,7 @@ mmap(Memory Mapped Files)内存映射加 write 实现零拷贝,**零拷贝 * 发出 mmap 系统调用,DMA 拷贝到内核缓冲区,映射到共享缓冲区;mmap 系统调用返回,无需拷贝 * 发出 write 系统调用,将数据从内核缓冲区拷贝到内核 Socket 缓冲区;write系统调用返回,DMA 将内核空间 Socket 缓冲区中的数据传递到协议引擎 -![](https://gitee.com/seazean/images/raw/master/Java/IO-mmap工作流程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-mmap工作流程.png) 原理:利用操作系统的 Page 来实现文件到物理内存的直接映射,完成映射后对物理内存的操作会**被同步**到硬盘上 @@ -13815,7 +13815,7 @@ sendfile 实现零拷贝,打开文件的文件描述符 fd 和 socket 的 fd 原理:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,由于和用户态完全无关,就减少了两次上下文切换 -![](https://gitee.com/seazean/images/raw/master/Java/IO-sendfile工作流程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/IO-sendfile工作流程.png) sendfile2.4 之后,sendfile 实现了更简单的方式,文件到达内核缓冲区后,不必再将数据全部复制到 socket buffer 缓冲区,而是只**将记录数据位置和长度相关等描述符信息**保存到 socket buffer,DMA 根据 Socket 缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上(2 次复制 2 次切换) @@ -14015,9 +14015,9 @@ TCP 协议的使用场景:文件上传和下载、邮件发送和接收、远 注意:**TCP 不会为没有数据的 ACK 超时重传** -三次握手 +三次握手 -四次挥手 +四次挥手 推荐阅读:https://yuanrengu.com/2020/77eef79f.html @@ -14062,7 +14062,7 @@ ServerSocket 类: 三次握手后 TCP 连接建立成功,服务器内核会把连接从 SYN 半连接队列(一次握手时在服务端建立的队列)中移出,移入 accept 全连接队列,等待进程调用 accept 函数时把连接取出。如果进程不能及时调用 accept 函数,就会造成 accept 队列溢出,最终导致建立好的 TCP 连接被丢弃 - + **相当于**客户端和服务器建立一个数据管道(虚连接,不是真正的物理连接),管道一般不用 close @@ -14089,9 +14089,9 @@ ServerSocket 类: 3. 从 Socket 通信管道中得到一个字节输入流 4. 从字节输入流中读取客户端发来的数据 -![](https://gitee.com/seazean/images/raw/master/Java/BIO工作机制.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/BIO工作机制.png) -![](https://gitee.com/seazean/images/raw/master/Java/TCP-工作模型.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/TCP-工作模型.png) * 如果输出缓冲区空间不够存放主机发送的数据,则会被阻塞,输入缓冲区同理 * 缓冲区不属于应用程序,属于内核 @@ -14513,7 +14513,7 @@ NIO 三大核心部分:Channel( 通道) ,Buffer( 缓冲区), Selector( 选 NIO 的实现框架: -![](https://gitee.com/seazean/images/raw/master/Java/NIO框架.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/NIO框架.png) * 每个 Channel 对应一个 Buffer * 一个线程对应 Selector , 一个 Selector 对应多个 Channel(连接) @@ -14536,7 +14536,7 @@ Java NIO 系统的核心在于:通道和缓冲区,通道表示打开的 IO 缓冲区(Buffer):缓冲区本质上是一个**可以读写数据的内存块**,用于特定基本数据类型的容器,用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的 -![](https://gitee.com/seazean/images/raw/master/Java/NIO-Buffer.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/NIO-Buffer.png) **Buffer 底层是一个数组**,可以保存多个相同类型的数据,根据数据类型不同 ,有以下 Buffer 常用子类:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer @@ -14558,7 +14558,7 @@ Java NIO 系统的核心在于:通道和缓冲区,通道表示打开的 IO * 位置、限制、容量遵守以下不变式: **0 <= position <= limit <= capacity** - + @@ -14751,9 +14751,9 @@ Direct Memory 优点: JVM 直接内存图解: - + - + @@ -15084,7 +15084,7 @@ Channel 的方法:**sendfile 实现零拷贝** 1. Buffer 2. 使用上述两种方法 -![](https://gitee.com/seazean/images/raw/master/Java/NIO-复制文件.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/NIO-复制文件.png) ```java public class ChannelTest { @@ -15203,7 +15203,7 @@ public class ChannelTest { 选择器(Selector) 是 SelectableChannle 对象的**多路复用器**,Selector 可以同时监控多个通道的状况,利用 Selector 可使一个单独的线程管理多个 Channel,**Selector 是非阻塞 IO 的核心**。 -![](https://gitee.com/seazean/images/raw/master/Java/NIO-Selector.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Java/NIO-Selector.png) * Selector 能够检测多个注册的通道上是否有事件发生(多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理,就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求 * 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程 diff --git a/SSM.md b/SSM.md index 45af664..b4ad616 100644 --- a/SSM.md +++ b/SSM.md @@ -4,7 +4,7 @@ ORM(Object Relational Mapping): 对象关系映射,指的是持久化数据和实体对象的映射模式,解决面向对象与关系型数据库存在的互不匹配的现象 -![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-ORM介绍.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-ORM介绍.png) **MyBatis**: @@ -542,7 +542,7 @@ SqlSession 常用 API: 调用流程: -![](https://gitee.com/seazean/images/raw/master/Frame/分层思想调用流程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/分层思想调用流程.png) 传统方式实现 DAO 层,需要写接口和实现类。采用 Mybatis 的代理开发方式实现 DAO 层的开发,只需要编写 Mapper 接口(相当于 Dao 接口),由 Mybatis 框架根据接口定义创建接口的**动态代理对象** @@ -561,7 +561,7 @@ Mapper 接口开发需要遵循以下规范: * Mapper.xml 文件中的增删改查标签的 resultType 属性和 DAO 层 Mapper 接口方法的返回值相同 - ![](https://gitee.com/seazean/images/raw/master/Frame/接口代理方式实现DAO层.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/接口代理方式实现DAO层.png) @@ -1505,7 +1505,7 @@ public class Blog { * cacheEnabled:true 表示全局性地开启所有映射器配置文件中已配置的任何缓存,默认 true -![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-缓存的实现原理.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-缓存的实现原理.png) @@ -1521,7 +1521,7 @@ public class Blog { 一级缓存是 SqlSession 级别的缓存 - + 工作流程:第一次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,如果没有,从数据库查询用户信息,得到用户信息,将用户信息存储到一级缓存中;第二次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,缓存中有,直接从缓存中获取用户信息。 @@ -1850,7 +1850,7 @@ OGNL:Object Graphic Navigation Language(对象图导航语言),用于对 表结构: -![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-动态sql用户表.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-动态sql用户表.png) @@ -2437,7 +2437,7 @@ MyBatis 提供了 org.apache.ibatis.jdbc.SQL 功能类,专门用于构建 SQL ### 运行机制 -![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-执行流程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-执行流程.png) MyBatis 运行过程: @@ -2511,7 +2511,7 @@ XMLConfigBuilder.parse():解析核心配置文件每个标签的信息(**XPa * `SqlSource sqlSource = getSqlSourceFromAnnotations()`:获取 SQL 的资源对象 - ![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-SQL资源对象.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-SQL资源对象.png) * `builderAssistant.addMappedStatement(...)`:封装成 MappedStatement 对象加入 Configuration 对象 @@ -2519,7 +2519,7 @@ XMLConfigBuilder.parse():解析核心配置文件每个标签的信息(**XPa return new DefaultSqlSessionFactory(config):返回工厂对象,包含 Configuration 对象 -![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-获取工厂对象.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-获取工厂对象.png) 总结:解析 XML 是对 Configuration 中的属性进行填充,那么可以在一个类中创建 Configuration 对象,自定义其中属性的值来达到配置的效果 @@ -2543,7 +2543,7 @@ DefaultSqlSessionFactory.openSessionFromDataSource(...):ExecutorType 为 Execu return new DefaultSqlSession(configuration, executor, autoCommit):返回 DefaultSqlSession 对象 -![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-获取会话对象.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-获取会话对象.png) @@ -2564,7 +2564,7 @@ MapperRegistry.getMapper(Class, SqlSession):MapperRegistry 是 Configuration * `MapperProxy implements InvocationHandler` 说明 MapperProxy 默认是一个 InvocationHandler 对象 * `Proxy.newProxyInstance()`:**JDK 动态代理**创建 MapperProxy 对象 -![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-获取代理对象.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-获取代理对象.png) @@ -2628,7 +2628,7 @@ Executor#query(): 构造函数中有:`this.parameterObject = parameterObject` - ![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-boundSql对象.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-boundSql对象.png) * `CachingExecutor.createCacheKey()`:创建缓存对象 @@ -2697,7 +2697,7 @@ Executor#query(): `return list.get(0)`:返回结果集的第一个数据 -![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-执行SQL过程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/MyBatis-执行SQL过程.png) @@ -2715,7 +2715,7 @@ Executor#query(): 实现原理:插件是按照插件配置顺序创建层层包装对象,执行目标方法的之后,按照逆向顺序执行(栈) - + 在四大对象创建时: @@ -2790,7 +2790,7 @@ public class MyFirstPlugin implements Interceptor{ ### 分页插件 -![](https://gitee.com/seazean/images/raw/master/Frame/分页介绍.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/分页介绍.png) * 分页可以将很多条结果进行分页显示。如果当前在第一页,则没有上一页。如果当前在最后一页,则没有下一页,需要明确当前是第几页,这一页中显示多少条结果。 * MyBatis 是不带分页功能的,如果想实现分页功能,需要手动编写 LIMIT 语句,不同的数据库实现分页的 SQL 语句也是不同,手写分页 成本较高。 @@ -2913,7 +2913,7 @@ PageInfo相关API: Spring 是分层的 JavaSE/EE 应用 full-stack 轻量级开源框架 -![](https://gitee.com/seazean/images/raw/master/Frame/Spring-框架介绍.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Spring-框架介绍.png) Spring 优点: @@ -2926,7 +2926,7 @@ Spring 优点: 体系结构: -![](https://gitee.com/seazean/images/raw/master/Frame/Spring-体系结构.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Spring-体系结构.png) @@ -2942,7 +2942,7 @@ Spring 优点: - **Spring 控制的资源全部放置在 Spring 容器中,该容器称为 IoC 容器**(存放实例对象) - 官方网站:https://spring.io/ → Projects → spring-framework → LEARN → Reference Doc -![](https://gitee.com/seazean/images/raw/master/Frame/Spring-IOC介绍.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Spring-IOC介绍.png) @@ -3024,7 +3024,7 @@ Spring 优点: } ``` - ![](https://gitee.com/seazean/images/raw/master/Frame/Spring-IOC实现.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Spring-IOC实现.png) @@ -3280,7 +3280,7 @@ ApplicationContext 子类相关API: - DI(Dependency Injection)依赖注入,应用程序运行依赖的资源由 Spring 为其提供,资源进入应用程序的方式称为注入,简单说就是利用反射机制为类的属性赋值的操作 - ![](https://gitee.com/seazean/images/raw/master/Frame/Spring-DI介绍.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Spring-DI介绍.png) IoC 和 DI 的关系:IoC 与 DI 是同一件事站在不同角度看待问题 @@ -3993,7 +3993,7 @@ DAO 接口不需要创建实现类,MyBatis-Spring 提供了一个动态代理 缺点:为了达成注解驱动的目的,可能会将原先很简单的书写,变的更加复杂。XML 中配置第三方开发的资源是很方便的,但使用注解驱动无法在第三方开发的资源中进行编辑,因此会增大开发工作量 -![](https://gitee.com/seazean/images/raw/master/Frame/注解驱动示例.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/注解驱动示例.png) @@ -4700,7 +4700,7 @@ ApplicationContext: FileSystemXmlApplicationContext:加载文件系统中任意位置的配置文件,而 ClassPathXmlAC 只能加载类路径下的配置文件 -![](https://gitee.com/seazean/images/raw/master/Frame/Spring-ApplicationContext层级结构图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Spring-ApplicationContext层级结构图.png) BeanFactory 的成员属性: @@ -5208,11 +5208,11 @@ AOP 作用: - Introduction(引入/引介):就是对原始对象无中生有的添加成员变量或成员方法 -![](https://gitee.com/seazean/images/raw/master/Frame/AOP连接点.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/AOP连接点.png) -![](https://gitee.com/seazean/images/raw/master/Frame/AOP切入点切面通知.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/AOP切入点切面通知.png) -![](https://gitee.com/seazean/images/raw/master/Frame/AOP织入.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/AOP织入.png) @@ -5823,7 +5823,7 @@ AOP 的通知类型共5种:前置通知,后置通知、返回后通知、抛 * 设定切入点表达式为通知方法传递参数(锁定通知变量名) -* 流程图:![](https://gitee.com/seazean/images/raw/master/Frame/AOP通知获取参数方式二.png) +* 流程图:![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/AOP通知获取参数方式二.png) * 解释: * `&` 代表并且 & @@ -5832,7 +5832,7 @@ AOP 的通知类型共5种:前置通知,后置通知、返回后通知、抛 第三种方式: * 设定切入点表达式为通知方法传递参数(改变通知变量名的定义顺序) -* 流程图:![](https://gitee.com/seazean/images/raw/master/Frame/AOP通知获取参数方式三.png) +* 流程图:![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/AOP通知获取参数方式三.png) * 解释:输出结果 a = param2 b = param1 @@ -6166,7 +6166,7 @@ AOP 的通知类型共5种:前置通知,后置通知、返回后通知、抛 AOP 注解简化 XML: -![](https://gitee.com/seazean/images/raw/master/Frame/AOP注解开发.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/AOP注解开发.png) 注意事项: @@ -6500,7 +6500,7 @@ CGLIB 特点: * CGLIB 动态代理无需要原始被代理对象,动态创建出新的代理对象 * CGLIB **继承被代理类**,如果代理类是 final 则不能实现 -![](https://gitee.com/seazean/images/raw/master/Frame/AOP底层原理-cglib.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/AOP底层原理-cglib.png) * CGLIB 类 @@ -6598,7 +6598,7 @@ Spirng 可以通过配置的形式控制使用的代理形式,Spring 会先判 #### 织入时机 -![AOP织入时机](https://gitee.com/seazean/images/raw/master/Frame/AOP织入时机.png) +![AOP织入时机](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/AOP织入时机.png) @@ -7449,7 +7449,7 @@ Spring 模板对象:TransactionTemplate、JdbcTemplate、RedisTemplate、Rabbi } ``` -![](https://gitee.com/seazean/images/raw/master/Frame/Spring-RedisTemplate.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Spring-RedisTemplate.png) @@ -9074,7 +9074,7 @@ retVal = invocation.proceed():**拦截器链驱动方法** } ``` -![](https://gitee.com/seazean/images/raw/master/Frame/Spring-AOP动态代理执行方法.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Spring-AOP动态代理执行方法.png) @@ -9925,7 +9925,7 @@ SpringMVC 优点: - 数据层:负责数据操作 - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-MVC三层架构.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-MVC三层架构.png) MVC(Model View Controller),一种用于设计创建Web应用程序表现层的模式 @@ -9940,7 +9940,7 @@ MVC(Model View Controller),一种用于设计创建Web应用程序表现 * Servlet * SpringMVC - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-MVC功能图示.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-MVC功能图示.png) @@ -10609,7 +10609,7 @@ SpringMVC 对接收的数据进行自动类型转换,该工作通过 Converter ##### 日期 -![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-date数据类型转换.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-date数据类型转换.png) 如果访问 URL:http://localhost/requestParam11?date=1999-09-09 会报错,所以需要日期类型转换 @@ -11452,7 +11452,7 @@ SpringMVC 提供访问原始 Servlet 接口的功能 * View:视图, View 最后对页面进行渲染将结果返回给用户,SpringMVC 框架提供了很多的 View 视图类型,包括:jstlView、freemarkerView、pdfView 等 - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-技术架构.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-技术架构.png) 优点: @@ -11501,7 +11501,7 @@ SpringMVC 提供访问原始 Servlet 接口的功能 请求进入原生的 HttpServlet 的 doGet() 方法处理,调用子类 FrameworkServlet 的 doGet() 方法,最终调用 DispatcherServlet 的 doService() 方法,为请求设置相关属性后调用 doDispatch(),请求和响应的以参数的形式传入 -![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-请求相应的原理.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-请求相应的原理.png) ```java // request 和 response 为 Java 原生的类 @@ -11622,7 +11622,7 @@ protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Ex } ``` -![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-获取Controller处理器.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-获取Controller处理器.png) * `mapping.getHandler(request)`:调用 AbstractHandlerMapping#getHandler @@ -11636,7 +11636,7 @@ protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Ex * `directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath)`:获取当前的映射器与当前**请求的 URI 有关的所有映射规则** - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-HandlerMapping的映射规则.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-HandlerMapping的映射规则.png) * `addMatchingMappings(directPathMatches, matches, request)`:**匹配某个映射规则** @@ -11706,7 +11706,7 @@ public String param(Map map, Model model, HttpServletRequest req } ``` -![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-Model和Map的数据解析.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-Model和Map的数据解析.png) doDispatch() 中调用 `mv = ha.handle(processedRequest, response, mappedHandler.getHandler())` **使用适配器执行方法** @@ -12078,7 +12078,7 @@ RequestResponseBodyMethodProcessor#handleReturnValue:处理返回值,要进 * `MediaType.sortBySpecificityAndQuality(mediaTypes)`:按照相对品质因数 q 降序排序 - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-浏览器支持接收的数据类型.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-浏览器支持接收的数据类型.png) * `producibleTypes = getProducibleMediaTypes(request, valueType, targetType)`:**服务器能生成的媒体类型** @@ -12135,7 +12135,7 @@ RequestResponseBodyMethodProcessor#handleReturnValue:处理返回值,要进 * `addDefaultHeaders(headers, t, contentType)`:**设置响应头中的数据类型** - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-服务器设置数据类型.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-服务器设置数据类型.png) * `writeInternal(t, type, outputMessage)`:**数据写出为 JSON 格式** @@ -12678,7 +12678,7 @@ public User cross(HttpServletRequest request){ 2. 拦截内容不同: Filter 对所有访问进行增强, Interceptor 仅针对 SpringMVC 的访问进行增强 - + @@ -12809,7 +12809,7 @@ public void afterCompletion(HttpServletRequest request, * 链路过长时,处理效率低下 * 可能存在节点上的循环引用现象,造成死循环,导致系统崩溃 - + @@ -12899,7 +12899,7 @@ void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse resp 拦截器的执行流程: - + @@ -13278,7 +13278,7 @@ public class UserController { 上传文件过程: -![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-上传文件过程分析.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-上传文件过程分析.png) @@ -13617,7 +13617,7 @@ public String addEmployee(@Valid Employee employee, Errors errors, Model model){ ``` * 三种判定空校验器的区别 - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-三种判定空检验器的区别.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-三种判定空检验器的区别.png) @@ -13811,7 +13811,7 @@ public class HelloController { * 业务层接口 + 业务层实现类 * 表现层类 - ![](https://gitee.com/seazean/images/raw/master/Frame/SSM-目录结构.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SSM-目录结构.png) @@ -14538,7 +14538,7 @@ public class ProjectExceptionAdivce { 项目整体目录结构 -![](https://gitee.com/seazean/images/raw/master/Frame/SSM-annotation.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SSM-annotation.png) @@ -14596,7 +14596,7 @@ public class ProjectExceptionAdivce { ### applicationContext.xml -![](https://gitee.com/seazean/images/raw/master/Frame/SSM-IoC注解整合MyBatis图解.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SSM-IoC注解整合MyBatis图解.png) * JdbcConfig @@ -14875,7 +14875,7 @@ SpringBoot 功能: 快速构建: -![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-IDEA构建工程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringBoot-IDEA构建工程.png) @@ -15458,7 +15458,7 @@ SpringBoot 定义了一套接口规范,这套规范规定 SpringBoot 在启动 * 从 spring-boot-autoconfigure-2.5.3.jar/META-INF/spring.factories 文件中寻找 EnableAutoConfiguration 字段,获取自动装配类,**进行条件装配,按需装配** - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-自动装配配置文件.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringBoot-自动装配配置文件.png) @@ -15970,7 +15970,7 @@ Profile 的配置: * 虚拟机参数:在VM options 指定:`-Dspring.profiles.active=dev` - ![](https://gitee.com/seazean/images/raw/master/Frame/SpringBoot-profile激活方式虚拟机参数.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringBoot-profile激活方式虚拟机参数.png) * 命令行参数:`java –jar xxx.jar --spring.profiles.active=dev` diff --git a/Tool.md b/Tool.md index 28bffd9..cc67520 100644 --- a/Tool.md +++ b/Tool.md @@ -124,7 +124,7 @@ GitLab(地址: https://about.gitlab.com/ )是一个用于仓库管理系 ### 工作过程 -![](https://gitee.com/seazean/images/raw/master/Tool/Git基本工作流程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Git基本工作流程.png) 版本库:.git 隐藏文件夹就是版本库,版本库中存储了很多配置信息、日志信息和文件版本信息等 @@ -132,7 +132,7 @@ GitLab(地址: https://about.gitlab.com/ )是一个用于仓库管理系 暂存区:.git 文件夹中有很多文件,其中有一个 index 文件就是暂存区,也可以叫做 stage,暂存区是一个临时保存修改文件的地方 -![](https://gitee.com/seazean/images/raw/master/Tool/文件流程图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/文件流程图.png) @@ -224,7 +224,7 @@ pull = fetch + merge fetch 是从远程仓库更新到本地仓库,pull是从远程仓库直接更新到工作空间中 -![](https://gitee.com/seazean/images/raw/master/Tool/图解远程仓库工作流程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/图解远程仓库工作流程.png) @@ -288,7 +288,7 @@ git push :上传本地指定分支到远程仓库 ## 版本管理 -![](https://gitee.com/seazean/images/raw/master/Tool/版本切换.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/版本切换.png) 命令:git reset --hard 版本唯一索引值 @@ -338,7 +338,7 @@ git merge branch-name:合并指定分支到当前分支 有时候合并操作不会如此顺利。 如果你在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,Git 就没办法合并它们,同时会提示文件冲突。此时需要我们打开冲突的文件并修复冲突内容,最后执行 git add 命令来标识冲突已解决 -​ ![](https://gitee.com/seazean/images/raw/master/Tool/合并分支冲突.png) +​ ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/合并分支冲突.png) @@ -431,14 +431,14 @@ File → Settings 打开设置窗口,找到 Version Control 下的 git 选项 ### 版本管理 * 版本对比 - ![](https://gitee.com/seazean/images/raw/master/Tool/版本对比.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/版本对比.png) * 版本切换方式一:控制台 Version Control → Log → 右键 Reset Current Branch → Reset,这种切换会抛弃原来的提交记录 - ![](https://gitee.com/seazean/images/raw/master/Tool/版本切换方式一.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/版本切换方式一.png) * 版本切换方式二:控制台 Version Control → Log → Revert Commit → Merge → 处理代码 → commit,这种切换会当成一个新的提交记录,之前的提交记录也都保留 - ![](https://gitee.com/seazean/images/raw/master/Tool/版本切换方式二.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/版本切换方式二.png) -​ ![](https://gitee.com/seazean/images/raw/master/Tool/版本切换方式二(1).png) +​ ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/版本切换方式二(1).png) @@ -465,7 +465,7 @@ File → Settings 打开设置窗口,找到 Version Control 下的 git 选项 1. VCS → Git → Push → 点击 master Define remote 2. 将远程仓库的 url 路径复制过来 → Push - ![](https://gitee.com/seazean/images/raw/master/Tool/本地仓库推送到远程仓库.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/本地仓库推送到远程仓库.png) @@ -477,7 +477,7 @@ File → Settings 打开设置窗口,找到 Version Control 下的 git 选项 File → Close Project → Checkout from Version Control → Git → 指定远程仓库的路径 → 指定本地存放的路径 → clone -![](https://gitee.com/seazean/images/raw/master/Tool/远程仓库克隆到本地仓库.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/远程仓库克隆到本地仓库.png) @@ -499,11 +499,11 @@ File → Close Project → Checkout from Version Control → Git → 指定远 操作系统作为接口的示意图: - + 移动设备操作系统: -![](https://gitee.com/seazean/images/raw/master/Tool/移动设备操作系统.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/移动设备操作系统.png) @@ -518,7 +518,7 @@ File → Close Project → Checkout from Version Control → Git → 指定远 ### 系统介绍 从内到位依次是硬件 → 内核层 → Shell 层 → 应用层 → 用户 -![Linux](https://gitee.com/seazean/images/raw/master/Tool/Linux系统.png) +![Linux](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Linux系统.png) 内核层:核心和基础,附着在硬件平台上,控制和管理系统内的各种资源,有效的组织进程的运行,扩展硬件的功能,提高资源利用效率,为用户提供安全可靠的应用环境。 @@ -534,7 +534,7 @@ Shell 层:与用户直接交互的界面。用户可以在提示符下输入 Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没有各种盘符的概念。根目录只有一个/,采用层级式的树状目录结构。 -![Linux文件系统](https://gitee.com/seazean/images/raw/master/Tool/Linux文件系统.png) +![Linux文件系统](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Linux文件系统.png) /:根目录,所有的目录、文件、设备都在/之下,/就是 Linux 文件系统的组织者,也是最上级的领导者。 @@ -586,11 +586,11 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 #### NAT 首先设置虚拟机中 NAT 模式的选项,打开 VMware,点击“编辑”下的“虚拟网络编辑器”,设置 NAT 参数 - ![](https://gitee.com/seazean/images/raw/master/Tool/配置NAT.jpg) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/配置NAT.jpg) **注意**:VMware Network Adapter VMnet8 保证是启用状态 -​ ![](https://gitee.com/seazean/images/raw/master/Tool/本地主机网络连接.jpg) +​ ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/本地主机网络连接.jpg) @@ -637,7 +637,7 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 * 宿主机 ping 虚拟机,虚拟机 ping 宿主机 * 在虚拟机中访问网络,需要增加一块 NAT 网卡 * 【虚拟机】--【设置】--【添加】 - * + * @@ -652,7 +652,7 @@ Linux 文件系统目录结构和熟知的 windows 系统有较大区别,没 **服务器维护工作** 都是在 远程 通过 SSH 客户端 来完成的, 并没有图形界面, 所有的维护工作都需要通过命令来完成,Linux 服务器需要安装 SSH 相关服务。 首先执行 sudo apt-get install openssh-server 指令。接下来用 xshell 连接。 -![](https://gitee.com/seazean/images/raw/master/Tool/远程连接Linux.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/远程连接Linux.png) 先用普通用户登录,然后转成 root @@ -957,7 +957,7 @@ top:用于实时显示 process 的动态 `top -Hp 进程 id`:分析该进程内各线程的cpu使用情况 -![](https://gitee.com/seazean/images/raw/master/Tool/top命令.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/top命令.png) **各进程(任务)的状态监控属性解释说明:** PID — 进程 id @@ -1017,17 +1017,17 @@ Linux 系统中查看进程使用情况的命令是 ps 指令 ### kill -Linux kill命令用于删除执行中的程序或工作(可强制中断) +Linux kill 命令用于删除执行中的程序或工作,并不是让进程直接停止,而是给进程发一个信号,可以进入终止逻辑 命令:kill [-s <信息名称或编号>] [程序] 或 kill [-l <信息编号>] -- -l <信息编号>  若不加<信息编号>选项,则-l参数会列出全部的信息名称。 -- -s <信息名称或编号>  指定要送出的信息。 +- -l <信息编号>  若不加<信息编号>选项,则-l参数会列出全部的信息名称 +- -s <信息名称或编号>  指定要送出的信息 - -KILL 强制杀死进程 - **-9 彻底杀死进程(常用)** -- [程序] 程序的 PID、PGID、工作编号。 +- [程序] 程序的 PID、PGID、工作编号 -`kill 15642 `. `kill -KILL 15642`. **`kill -9 15642`** +`kill 15642 `. `kill -KILL 15642`. `kill -9 15642` 杀死指定用户所有进程: @@ -1362,7 +1362,7 @@ mv [options] source... directory Linux系统是一种典型的多用户系统,不同的用户处于不同的地位,拥有不同的权限。为了保护系统的安全性,Linux系统对不同的用户访问同一文件(包括目录文件)的权限做了不同的规定。 -![](https://gitee.com/seazean/images/raw/master/Tool/用户目录下的文件.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/用户目录下的文件.png) 在Linux中第一个字符代表这个文件是目录、文件或链接文件等等。 @@ -1374,7 +1374,7 @@ Linux系统是一种典型的多用户系统,不同的用户处于不同的地 接下来的字符,以三个为一组,均为[rwx] 的三个参数组合。其中,[ r ]代表可读(read)、[ w ]代表可写(write)、[ x ]代表可执行(execute)。 要注意的是,这三个权限的位置不会改变,如果没有权限,就会出现[ - ]。 - + 从左至右用0-9这些数字来表示: 第0位确定文件类型,第1-3位确定属主(该文件的所有者)拥有该文件的权限。 @@ -1391,7 +1391,7 @@ Linux系统是一种典型的多用户系统,不同的用户处于不同的地 > 文件的【属主】有一套【读写执行权限rwx】 > 文件的【属组】有一套【读写执行权限rwx】 -![](https://gitee.com/seazean/images/raw/master/Tool/列出目录文件.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/列出目录文件.png) `ls -l` 可以查看文件夹下文件的详细信息, 从左到右 依次是: @@ -1436,7 +1436,7 @@ mode : 权限设定字串,格式: [ugoa...][[+-=][rwxX]...][,...] 文件的权限字符为:[-rwxrwxrwx], 这九个权限是三三一组的,我们使用数字来代表各个权限。 - + 各权限的数字对照表:[r]:4; [w]:2; [x]:1; [-]:0 @@ -1452,7 +1452,7 @@ mode : 权限设定字串,格式: [ugoa...][[+-=][rwxX]...][,...] ##### 符号权限 -![](https://gitee.com/seazean/images/raw/master/Tool/权限符号表.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/权限符号表.png) - user 属主权限 - group 属组权限 @@ -2128,7 +2128,7 @@ vim 中提供有一个 被复制文本的缓冲区 * 下次再使用 vim 编辑文件时, 会看到以下屏幕信息, - ![](https://gitee.com/seazean/images/raw/master/Tool/vim异常.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/vim异常.png) * ls -a 一下,会看到隐藏的.swp文件 删除了此文件即可。 @@ -2147,7 +2147,7 @@ ln [-sf] source_filename dist_filename * -s:默认是实体链接,加 -s 为符号链接 * -f:如果目标文件存在时,先删除目标文件 - + **实体链接**: @@ -2348,7 +2348,7 @@ ifconfig [网络设备][down up -allmulti -arp -promisc][add<地址>][del<地址 ``` * `ifconfig`:显示激活的网卡信息 ens - + **ens33(有的是eth0)**表示第一块网卡。 表示 ens33 网卡的 IP地址是 192.168.0.137,广播地址,broadcast 192.168.0.255,掩码地址netmask:255.255.255.0 ,inet6 对应的是 ipv6 @@ -2377,7 +2377,7 @@ ping [-dfnqrRv][-c<完成次数>][-i<间隔秒数>][-I<网络界面>][-l<前置 * -c<完成次数>:设置完成要求回应的次数; * `ping -c 2 www.baidu.com` - ![](https://gitee.com/seazean/images/raw/master/Tool/ping百度.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/ping百度.png) icmp_seq:ping 序列,从1开始 @@ -2454,7 +2454,7 @@ lsblk 命令的英文是 list block,即用于列出所有可用块设备的信 命令:lsblk [参数] * `lsblk`:以树状列出所有块设备 - ![](https://gitee.com/seazean/images/raw/master/Tool/可用块设备.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/可用块设备.png) NAME:这是块设备名。 @@ -2473,7 +2473,7 @@ lsblk 命令的英文是 list block,即用于列出所有可用块设备的信 * `lsblk -f`:不会列出所有空设备 - ![](https://gitee.com/seazean/images/raw/master/Tool/不包含空设备.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/不包含空设备.png) NAME表示设备名称 @@ -2500,7 +2500,7 @@ lsblk 命令的英文是 list block,即用于列出所有可用块设备的信 * -h, 使用人类可读的格式(预设值是不加这个选项的...) * --total 计算所有的数据之和 -![](https://gitee.com/seazean/images/raw/master/Tool/磁盘管理.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/磁盘管理.png) 第一列指定文件系统的名称;第二列指定一个特定的文件系统,1K 是 1024 字节为单位的总容量;已用和可用列分别指定的容量;最后一个已用列指定使用的容量的百分比;最后一栏指定的文件系统的挂载点 @@ -2532,7 +2532,7 @@ mount [-fnrsvw] [-t vfstype] [-o options] device dir `mkdir -p /mnt/cdrom `:切换到 root 下创建一个挂载点(其实就是创建一个目录) * 开始挂载 `mount -t auto /dev/cdrom /mnt/cdrom`:通过挂载点的方式查看上面的【ISO文件内容】 - ![挂载成功](https://gitee.com/seazean/images/raw/master/Tool/挂载成功.png) + ![挂载成功](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/挂载成功.png) * 查看挂载内容:`ls -l -a ./mnt/cdrom/` * 卸载 cdrom:`umount /mnt/cdrom/` @@ -2608,7 +2608,7 @@ Shell 脚本(shell script),是一种为 shell 编写的脚本程序。 Shell 编程跟 JavaScript、php 编程一样,只要有一个能编写代码的文本编辑器和一个能解释执行的脚本解释器就可以了。 `cat /etc/shells`:查看解释器 -![](https://gitee.com/seazean/images/raw/master/Tool/shell环境.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/shell环境.png) Linux 的 Shell 种类众多,常见的有: @@ -3339,7 +3339,7 @@ Docker 架构: * **容器(Container)**:镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和对象一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等 * **仓库(Repository)**:仓库可看成一个代码控制中心,用来保存镜像 -![](https://gitee.com/seazean/images/raw/master/Tool/Docker-docker架构.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Docker-docker架构.png) 安装步骤: @@ -3514,7 +3514,7 @@ sudo systemctl restart docker > Docker 容器和外部机器可以直接交换文件吗? > 容器之间想要进行数据交互? - + **数据卷**:数据卷是宿主机中的一个目录或文件,当容器目录和数据卷目录绑定后,对方的修改会立即同步 @@ -3550,7 +3550,7 @@ sudo systemctl restart docker * 多个容器挂载同一个数据卷 * 数据卷容器 - + * 创建启动c3数据卷容器,使用 –v 参数设置数据卷 @@ -3585,7 +3585,7 @@ sudo systemctl restart docker * 当容器中的网络服务需要被外部机器访问时,可以将容器中提供服务的端口映射到宿主机的端口上。外部机器访问宿主机的该端口,从而间接访问容器的服务。这种操作称为:**端口映射** - ![](https://gitee.com/seazean/images/raw/master/Tool/Docker-MySQL部署.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Docker-MySQL部署.png) MySQL部署步骤:搜索mysql镜像,拉取mysql镜像,创建容器,操作容器中的mysql @@ -3845,7 +3845,7 @@ Docker镜像原理: ### 镜像制作 -![](https://gitee.com/seazean/images/raw/master/Tool/Docker-Docker镜像原理.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Docker-Docker镜像原理.png) **** @@ -3950,7 +3950,7 @@ Docker Compose是一个编排多容器分布式部署的工具,提供命令集 3. 运行 docker-compose up 启动应用 -![](https://gitee.com/seazean/images/raw/master/Tool/Docker-Compose原理.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Docker-Compose原理.png) @@ -4094,7 +4094,7 @@ Docker官方的Docker hub(https://hub.docker.com)是一个用于管理公共 * 容器虚拟化的是操作系统,虚拟机虚拟化的是硬件。 * 传统虚拟机可以运行不同的操作系统,容器只能运行同一类型操作系统 - ![](https://gitee.com/seazean/images/raw/master/Tool/Docker-容器和虚拟机对比.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Tool/Docker-容器和虚拟机对比.png) | 特性 | 容器 | 虚拟机 | | ---------- | ------------------ | ---------- | diff --git a/Web.md b/Web.md index 59c8d93..6c993a1 100644 --- a/Web.md +++ b/Web.md @@ -64,7 +64,7 @@ HTML 标签可以拥有属性 ### 结构 -![HTML结构](https://gitee.com/seazean/images/raw/master/Web/HTML结构.png) +![HTML结构](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML结构.png) 文档结构介绍: @@ -286,7 +286,7 @@ HTML 标签可以拥有属性 **效果如下**: -![](https://gitee.com/seazean/images/raw/master/Web/HTML文本标签效果图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML文本标签效果图.png) @@ -364,7 +364,7 @@ target属性取值: 效果图: - + *** @@ -499,7 +499,7 @@ button标签:表示按钮 使用方式:以name属性值作为键,value属性值作为值,构成键值对提交到服务器,多个键值对浏览器使用`&`进行分隔。 -![](https://gitee.com/seazean/images/raw/master/Web/HTML标签input属性-name-value.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML标签input属性-name-value.png) @@ -587,7 +587,7 @@ button标签:表示按钮 ``` -![](https://gitee.com/seazean/images/raw/master/Web/HTML标签input属性-type.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML标签input属性-type.png) @@ -645,7 +645,7 @@ button标签:表示按钮 ``` -![](https://gitee.com/seazean/images/raw/master/Web/HTML标签select和文本域属性.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML标签select和文本域属性.png) *** @@ -690,7 +690,7 @@ button标签:表示按钮 * tr:table row,表示表中单元的行 * td:table data,表示表中一个单元格 * th:table header,表格单元格的表头,通常字体样式加粗居中 -* ![](https://gitee.com/seazean/images/raw/master/Web/HTML表格标签.png) +* ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML表格标签.png) @@ -778,7 +778,7 @@ button标签:表示按钮 效果图: -![](https://gitee.com/seazean/images/raw/master/Web/HTML表格标签跨行跨列效果图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML表格标签跨行跨列效果图.png) @@ -861,7 +861,7 @@ background属性用来设置背景相关的样式。 ``` - ![](https://gitee.com/seazean/images/raw/master/Web/HTML背景图.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML背景图.png) @@ -881,7 +881,7 @@ background属性用来设置背景相关的样式。 } ``` - ![](https://gitee.com/seazean/images/raw/master/Web/HTML背景设计.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML背景设计.png) *** @@ -905,7 +905,7 @@ background属性用来设置背景相关的样式。
right
``` - ![](https://gitee.com/seazean/images/raw/master/Web/HTML-div简单布局.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML-div简单布局.png) @@ -1013,7 +1013,7 @@ background属性用来设置背景相关的样式。 ``` - ![](https://gitee.com/seazean/images/raw/master/Web/HTML-div基本布局.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML-div基本布局.png) @@ -1032,7 +1032,7 @@ background属性用来设置背景相关的样式。 | **article** | 文章元素 | 表示独立内容区域 | 标签定义的内容本身必须是有意义且必须独立于文档的其他部分 | | **footer** | 页脚元素 | 表示页面的底部 | 块元素,文档中可以定义多个 | -![](https://gitee.com/seazean/images/raw/master/Web/语义化标签结构图.jpg) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/语义化标签结构图.jpg) @@ -1109,7 +1109,7 @@ background属性用来设置背景相关的样式。 ``` -![](https://gitee.com/seazean/images/raw/master/Web/HTML标签video.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTML标签video.png) @@ -1194,7 +1194,7 @@ CSS是一门基于规则的语言—你能定义用于你的网页中**特定元 } ``` -![](https://gitee.com/seazean/images/raw/master/Web/CSS的组成.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS的组成.png) @@ -1665,7 +1665,7 @@ h1 { ``` - + @@ -1697,7 +1697,7 @@ h1 { ``` -![](https://gitee.com/seazean/images/raw/master/Web/CSS边框轮廓效果图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS边框轮廓效果图.png) *** @@ -1709,7 +1709,7 @@ h1 { 盒子模型是通过设置**元素框**与**元素内容**和**外部元素**的边距,而进行布局的方式。 -![](https://gitee.com/seazean/images/raw/master/Web/CSS盒子模型.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS盒子模型.png) - element : 元素。 - padding : 内边距,也有资料将其翻译为填充。 @@ -1805,7 +1805,7 @@ h1 { ``` - ![](https://gitee.com/seazean/images/raw/master/Web/CSS盒子模式-效果图2.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS盒子模式-效果图2.png) @@ -1881,7 +1881,7 @@ span{ 微信 ``` -![](https://gitee.com/seazean/images/raw/master/Web/CSS-文本样式效果图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS-文本样式效果图.png) @@ -2061,7 +2061,7 @@ a{ ``` -![](https://gitee.com/seazean/images/raw/master/Web/CSS案例登陆页面.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/CSS案例登陆页面.png) @@ -2201,7 +2201,7 @@ HTTP 和 HTTPS 的区别: HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加密,客户端生成的随机密钥,用来进行对称加密 -![](https://gitee.com/seazean/images/raw/master/Web/HTTP-HTTPS加密过程.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTTP-HTTPS加密过程.png) 1. 客户端向服务器发起 HTTPS 请求,连接到服务器的 443 端口,请求携带了浏览器支持的加密算法和哈希算法,协商加密算法 2. 服务器端会向数字证书认证机构提出公开密钥的申请,认证机构对公开密钥做数字签名后进行分配,会将公钥绑定在数字证书(又叫公钥证书,内容有公钥,网站地址,证书颁发机构,失效日期等) @@ -2236,7 +2236,7 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 * POST - ![](https://gitee.com/seazean/images/raw/master/Web/HTTP请求部分.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTTP请求部分.png) * GET @@ -2343,7 +2343,7 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 响应部分图: -![](https://gitee.com/seazean/images/raw/master/Web/HTTP响应部分.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTTP响应部分.png) @@ -2356,7 +2356,7 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 OK:状态码描述 * 响应状态码: - ![](https://gitee.com/seazean/images/raw/master/Web/HTTP状态响应码.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/HTTP状态响应码.png) | 状态码 | 说明 | | ------- | -------------------------------------------------- | @@ -2469,11 +2469,11 @@ Web,在计算机领域指网络。像我们接触的 `WWW`,它是由 3 个 部署方式划分:一体化架构,垂直拆分架构,分布式架构,流动计算架构,微服务架构。 * C/S结构:客户端—服务器的方式。其中C代表Client,S代表服务器。C/S结构的系统设计图如下: - + * B/S结构是浏览器—服务器的方式。B代表Browser,S代表服务器。B/S结构的系统设计图如下: - + @@ -2529,7 +2529,7 @@ Web,在计算机领域指网络。像我们接触的 `WWW`,它是由 3 个 目录结构详解: -![](https://gitee.com/seazean/images/raw/master/Web/Tomcat目录结构详解.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat目录结构详解.png) @@ -2587,7 +2587,7 @@ Tomcat服务器的停止文件也在二进制文件目录bin中:shutdown.bat * 进程很重要:修改自己的端口号。修改的是 Tomcat 目录下`\conf\server.xml`中的配置。 - ![](https://gitee.com/seazean/images/raw/master/Web/Tomcat-server.xml端口配置.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat-server.xml端口配置.png) @@ -2599,7 +2599,7 @@ Tomcat服务器的停止文件也在二进制文件目录bin中:shutdown.bat Run -> Edit Configurations -> Templates -> Tomcat Server -> Local -![](https://gitee.com/seazean/images/raw/master/Web/Tomcat-IDEA配置Tomcat.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat-IDEA配置Tomcat.png) @@ -2646,10 +2646,10 @@ Run -> Edit Configurations -> Templates -> Tomcat Server -> Local #### IDEA部署 * 新建工程 - + * 发布工程 - ![](https://gitee.com/seazean/images/raw/master/Web/Tomcat-IDEA发布工程.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat-IDEA发布工程.png) * Run @@ -2687,7 +2687,7 @@ Run -> Edit Configurations -> Templates -> Tomcat Server -> Local Tomcat 核心组件架构图如下所示: -![](https://gitee.com/seazean/images/raw/master/Web/Tomcat-核心组件架构图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat-核心组件架构图.png) 组件介绍: @@ -2845,7 +2845,7 @@ Socket 是使用 TCP/IP 或者 UDP 协议在服务器与客户端之间进行传 - **HTTP 协议:是在 TCP/IP 协议之上进一步封装的一层协议,关注数据传输的格式是否规范,底层的数据传输还是运用了 Socket 和 TCP/IP** Tomcat 和 Servlet 的关系:Servlet 的运行环境叫做 Web 容器或 Servlet 服务器,**Tomcat 是 Web 应用服务器,是一个 Servlet/JSP 容器**。Tomcat 作为 Servlet 容器,负责处理客户请求,把请求传送给 Servlet,并将 Servlet 的响应传送回给客户。而 Servlet 是一种运行在支持 Java 语言的服务器上的组件,Servlet 用来扩展 Java Web 服务器功能,提供非常安全的、可移植的、易于使用的 CGI 替代品 -![](https://gitee.com/seazean/images/raw/master/Web/Tomcat与Servlet的关系.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Tomcat与Servlet的关系.png) @@ -2867,7 +2867,7 @@ Servlet是SUN公司提供的一套规范,名称就叫Servlet规范,它也是 4. 支持配置相关功能 - ![](https://gitee.com/seazean/images/raw/master/Web/Servlet类关系总视图.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Servlet类关系总视图.png) @@ -2879,7 +2879,7 @@ Servlet是SUN公司提供的一套规范,名称就叫Servlet规范,它也是 创建 Web 工程 → 编写普通类继承 Servlet 相关类 → 重写方法 -![](https://gitee.com/seazean/images/raw/master/Web/Servlet入门案例执行.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Servlet入门案例执行.png) @@ -2887,7 +2887,7 @@ Servlet执行过程分析: 通过浏览器发送请求,请求首先到达Tomcat服务器,由服务器解析请求URL,然后在部署的应用列表中找到应用。然后找到web.xml配置文件,在web.xml中找到FirstServlet的配置(/),找到后执行service方法,最后由FirstServlet响应客户浏览器。整个过程如下图所示: -![](https://gitee.com/seazean/images/raw/master/Web/Servlet执行过程图.jpg) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Servlet执行过程图.jpg) @@ -2921,7 +2921,7 @@ Servlet执行过程分析: Servlet 3.0 中的异步处理指的是允许Servlet重新发起一条新线程去调用 耗时业务方法,这样就可以避免等待 -![](https://gitee.com/seazean/images/raw/master/Web/Servlet3.0的异步处理.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Servlet3.0的异步处理.png) @@ -3262,7 +3262,7 @@ ServletConfig 是 Servlet 的配置参数对象。在 Servlet 规范中,允许 * 效果: - ![](https://gitee.com/seazean/images/raw/master/Web/ServletConfig演示.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/ServletConfig演示.png) @@ -3290,7 +3290,7 @@ Servlet 规范中,共有4个域对象,ServletContext 是其中一个,web 数据共享: - + 获取ServletContext: @@ -3467,7 +3467,7 @@ Servlet3.0 版本!不需要配置 web.xml Web服务器收到客户端的http请求,会针对每一次请求,分别创建一个用于代表请求的request对象、和代表响应的response对象。 -![](https://gitee.com/seazean/images/raw/master/Web/Servlet请求响应图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Servlet请求响应图.png) @@ -3487,7 +3487,7 @@ Request 作用: * 请求转发 * 作为域对象存数据 -![](https://gitee.com/seazean/images/raw/master/Web/Request请求对象的类视图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Request请求对象的类视图.png) @@ -3940,7 +3940,7 @@ Response 的作用: * 请求重定向 -![](https://gitee.com/seazean/images/raw/master/Web/Response响应类视图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Response响应类视图.png) *** @@ -4138,7 +4138,7 @@ public class ServletDemo04 extends HttpServlet { ``` -![](https://gitee.com/seazean/images/raw/master/Web/Response设置缓存时间.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Response设置缓存时间.png) @@ -4313,7 +4313,7 @@ public class ServletDemo08 extends HttpServlet { 3. 请求转发可以和请求域对象共享数据,数据不会丢失 4. 请求转发浏览器地址栏不变 -![](https://gitee.com/seazean/images/raw/master/Web/重定向和请求转发对比图.jpg) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/重定向和请求转发对比图.jpg) @@ -4412,7 +4412,7 @@ Cookie:客户端会话管理技术,把要共享的数据保存到了客户 作用:保存客户浏览器访问网站的相关内容(需要客户端不禁用 Cookie),从而在每次访问同一个内容时,先从本地缓存获取,使资源共享,提高效率。 -![](https://gitee.com/seazean/images/raw/master/Web/Cookie类讲解.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Cookie类讲解.png) @@ -4589,7 +4589,7 @@ HttpServletRequest类获取Session: | HttpSession getSession() | 获取HttpSession对象 | | HttpSession getSession(boolean creat) | 获取HttpSession对象,未获取到是否自动创建 | - + @@ -4812,7 +4812,7 @@ JSP部署在服务器上,可以处理客户端发送的请求,并根据请 客户端提交请求——Tomcat服务器解析请求地址——找到JSP页面——Tomcat将JSP页面翻译成Servlet的java文件——将翻译好的.java文件编译成.class文件——返回到客户浏览器上 - ![](https://gitee.com/seazean/images/raw/master/Web/JSP执行过程.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/JSP执行过程.png) * 溯源,打开JSP翻译后的Java文件 @@ -4820,7 +4820,7 @@ JSP部署在服务器上,可以处理客户端发送的请求,并根据请 在文件中找到了输出页面的代码,本质都是用out.write()输出的JSP语句 - ![](https://gitee.com/seazean/images/raw/master/Web/Jsp的本质说明.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/Jsp的本质说明.png) @@ -5016,7 +5016,7 @@ jsp中的隐式对象也并不是未声明,它是在翻译成.java文件时声 * PageContext方法如下,页面域操作的方法定义在了PageContext的父类JspContext中 - ![](https://gitee.com/seazean/images/raw/master/Web/PageContext方法详解.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/PageContext方法详解.png) @@ -5047,7 +5047,7 @@ M : model, 通常用于封装数据,封装的是数据模型 V : view,通常用于展示数据。动态展示用jsp页面,静态数据展示用html C : controller,通常用于处理请求和响应,一般指的是Servlet -![](https://gitee.com/seazean/images/raw/master/Web/MVC模型.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/MVC模型.png) @@ -5216,7 +5216,7 @@ str: EL表达式中运算符: -* 关系运算符:![](https://gitee.com/seazean/images/raw/master/Web/EL表达式关系运算符.png) +* 关系运算符:![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/EL表达式关系运算符.png) * 逻辑运算符: @@ -5261,7 +5261,7 @@ EL表达式中运算符: ``` -![](https://gitee.com/seazean/images/raw/master/Web/EL表达式运算符效果图.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/EL表达式运算符效果图.png) @@ -6714,7 +6714,7 @@ DOM(Document Object Model):文档对象模型。 将 HTML 文档的各个组成部分,封装为对象。借助这些对象,可以对 HTML 文档进行增删改查的动态操作。 -![](https://gitee.com/seazean/images/raw/master/Web/DOM介绍.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/DOM介绍.png) @@ -6882,11 +6882,11 @@ Attribute属性的操作: * 常用的事件: - ![](https://gitee.com/seazean/images/raw/master/Web/JS常用的事件.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/JS常用的事件.png) * 更多的事件: - ![](https://gitee.com/seazean/images/raw/master/Web/JS更多的事件.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/JS更多的事件.png) @@ -6947,7 +6947,7 @@ Attribute属性的操作: 在姓名、年龄、性别三个文本框中填写信息后,添加到“学生信息表”列表(表格),点击删除后,删除该行数据,并且不需刷新 -![](https://gitee.com/seazean/images/raw/master/Web/事件案例效果.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/事件案例效果.png) @@ -7366,7 +7366,7 @@ JSON(JavaScript Object Notation):是一种轻量级的数据交换格式。 * 创建格式: **name是字符串类型** - ![](https://gitee.com/seazean/images/raw/master/Web/JSON创建格式.png) + ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/JSON创建格式.png) * json常用方法 @@ -7536,7 +7536,7 @@ RegExp: 使用onsubmit表单提交事件 -![](https://gitee.com/seazean/images/raw/master/Web/表单校验.png) +![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Web/表单校验.png) ```html