From 790c6250d49f915e848885d0cc069167d8d8248f Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 5 Sep 2021 21:10:59 +0800 Subject: [PATCH 001/122] Update Java Notes --- DB.md | 50 ++++++++++++++----------------------- Java.md | 28 +++++++++++---------- Prog.md | 16 +++++++----- SSM.md | 32 ++++++++++++------------ Tool.md | 76 ++++++++++++++++++++++++++++----------------------------- 5 files changed, 98 insertions(+), 104 deletions(-) diff --git a/DB.md b/DB.md index 968e2a2..abb9c75 100644 --- a/DB.md +++ b/DB.md @@ -2550,7 +2550,7 @@ Buffer Pool 是一片内存空间,可以通过 innodb_buffer_pool_size 来控 * Change Buffer 是 Buffer Pool 里的内存,不能无限增大,用来对增删改操作提供缓存 * Change Buffer 的大小可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% -* 补充知识:唯一索引的更新不能使用 Buffer,一般只有普通索引可以使用,直接写入 Buffer 就结束 +* 补充知识:**唯一索引的更新不能使用 Buffer**,一般只有普通索引可以使用,直接写入 Buffer 就结束 InnoDB 的数据是按数据页为单位来读写,每个数据页的大小默认是 16KB。数据是存放在磁盘中,每次读写数据都需要磁盘 IO,效率会很低。InnoDB 提供了缓存 Change Buffer,Buffer 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲: @@ -5567,9 +5567,10 @@ MySQL 4.1 版本之后,开始支持 SQL 的子查询 * 优化方式二:方案适用于主键自增的表,可以把 LIMIT 查询转换成某个位置的查询 ```mysql - EXPLAIN SELECT * FROM tb_user_1 WHERE id > 200000 LIMIT 10; + EXPLAIN SELECT * FROM tb_user_1 WHERE id > 200000 LIMIT 10; -- 写法 1 + 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) @@ -9391,6 +9392,8 @@ sorted_set类型:在 set 的存储结构基础上添加可排序字段,类 - 有序集合保存的元素个数要小于 128 个; - 有序集合保存的所有元素大小都小于 64 字节 +当元素比较多时,此时 ziplist 的读写效率会下降,时间复杂度是 O(n),跳表的时间复杂度是 O(logn) + *** @@ -10034,7 +10037,7 @@ AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢 - 对数据**非常敏感**,建议使用默认的 AOF 持久化方案 - AOF 持久化策略使用 everysecond,每秒钟 fsync 一次,该策略 redis 仍可以保持很好的处理性能,当出现问题时,最多丢失 1 秒内的数据。 + AOF 持久化策略使用 everysecond,每秒钟 fsync 一次,该策略 redis 仍可以保持很好的处理性能,当出现问题时,最多丢失 1 秒内的数据 注意:AOF 文件存储体积较大,恢复速度较慢,因为要执行每条指令 @@ -10638,7 +10641,7 @@ Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 * 容量瓶颈:内存不足,放弃使用 redis -* 解决方案:为了避免单点 Redis 服务器故障,准备多台服务器,互相连通。将数据复制多个副本保存在不同的服务器上连接在一起,并保证数据是同步的。即使有其中一台服务器宕机,其他服务器依然可以继续提供服务,实现Redis的高可用,同时实现数据冗余备份 +* 解决方案:为了避免单点 Redis 服务器故障,准备多台服务器,互相连通。将数据复制多个副本保存在不同的服务器上连接在一起,并保证数据是同步的。即使有其中一台服务器宕机,其他服务器依然可以继续提供服务,实现 Redis 高可用,同时实现数据冗余备份 @@ -11580,32 +11583,15 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 解决方案: -* 思路: - - 1. 更多的页面静态化处理 - - 2. 构建**多级缓存**架构:Nginx 缓存 + Redis 缓存 + ehcache 缓存 - - 3. 检测 MySQL 严重耗时业务进行优化:对数据库的瓶颈排查,例如超时查询、耗时较高事务等 - - 4. 灾难预警机制:监控 Redis 服务器性能指标,CPU 使用率、内存容量、平均响应时间、线程数 - - 5. 限流、降级:短时间范围内牺牲一些客户体验,限制一部分请求访问,降低应用服务器压力,待业务低速运转后再逐步放开访问 - -* 实践: - - 1. LRU 与 LFU切换 - - 2. 数据有效期策略调整:根据业务数据有效期进行分类错峰,A 类 90 分钟,B 类 80 分钟,C 类 70 分钟,过期时间使用固定时间 + 随机值的形式,稀释集中到期的 key 的数量 - - 3. 超热数据使用永久 key - - 4. 定期维护:对即将过期数据做访问量分析,确认是否延时,配合访问量统计,做热点数据的延时 - - 5. 加锁:慎用 +1. 加锁,慎用 +2. 设置热点数据永远不过期,如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中 +3. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生 +4. 构建**多级缓存**架构,Nginx 缓存 + Redis 缓存 + ehcache 缓存 +5. 灾难预警机制,监控 Redis 服务器性能指标,CPU 使用率、内存容量、平均响应时间、线程数 +6. 限流、降级:短时间范围内牺牲一些客户体验,限制一部分请求访问,降低应用服务器压力,待业务低速运转后再逐步放开访问 -总的来说:缓存雪崩就是瞬间过期数据量太大,导致对数据库服务器造成压力。如能够有效避免过期时间集中,可以有效解决雪崩现象的出现(约40%),配合其他策略一起使用,并监控服务器的运行数据,根据运行记录做快速调整。 +总的来说:缓存雪崩就是瞬间过期数据量太大,导致对数据库服务器造成压力。如能够有效避免过期时间集中,可以有效解决雪崩现象的出现(约 40%),配合其他策略一起使用,并监控服务器的运行数据,根据运行记录做快速调整。 @@ -11639,7 +11625,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 5. 加锁:分布式锁,防止被击穿,但是要注意也是性能瓶颈,慎重 -总的来说:缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中 redis 后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个 key 的过期监控难度较高,配合雪崩处理策略即可 +总的来说:缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中 Redis 后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个 key 的过期监控难度较高,配合雪崩处理策略即可 @@ -11667,14 +11653,14 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 1. 缓存 null:对查询结果为 null 的数据进行缓存,设定短时限,例如 30-60 秒,最高 5 分钟 -2. 白名单策略:提前预热各种分类数据 id 对应的 **bitmaps**,id 作为 bitmaps 的 offset,相当于设置了数据白名单。当加载正常数据时放行,加载异常数据时直接拦截(效率偏低),也可以使用布隆过滤器(有关布隆过滤器的命中问题对当前状况可以忽略) +2. 白名单策略:提前预热各种分类**数据 id 对应的 bitmaps**,id 作为 bitmaps 的 offset,相当于设置了数据白名单。当加载正常数据时放行,加载异常数据时直接拦截(效率偏低),也可以使用布隆过滤器(有关布隆过滤器的命中问题对当前状况可以忽略) 3. 实时监控:实时监控 Redis 命中率(业务正常范围时,通常会有一个波动值)与 null 数据的占比 * 非活动时段波动:通常检测 3-5 倍,超过 5 倍纳入重点排查对象 * 活动时段波动:通常检测10-50 倍,超过 50 倍纳入重点排查对象 - ​ 根据倍数不同,启动不同的排查流程。然后使用黑名单进行防控(运营) + 根据倍数不同,启动不同的排查流程。然后使用黑名单进行防控 4. key 加密:临时启动防灾业务 key,对 key 进行业务层传输加密服务,设定校验程序,过来的 key 校验;例如每天随机分配 60 个加密串,挑选 2 到 3 个,混淆到页面数据 id 中,发现访问 key 不满足规则,驳回数据访问 diff --git a/Java.md b/Java.md index 0225e4e..ecbed28 100644 --- a/Java.md +++ b/Java.md @@ -4915,7 +4915,7 @@ HashMap继承关系如下图所示: } ``` -* 构造一个具有指定的初始容量和负载因子的HashMap +* 构造一个具有指定的初始容量和负载因子的 HashMap ```java public HashMap(int initialCapacity, float loadFactor) { @@ -4942,7 +4942,7 @@ HashMap继承关系如下图所示: } ``` - putMapEntries源码分析: + putMapEntries 源码分析: ```java final void putMapEntries(Map m, boolean evict) { @@ -4987,8 +4987,8 @@ HashMap继承关系如下图所示: * hash():HashMap 是支持 Key 为空的;HashTable 是直接用 Key 来获取 HashCode,key 为空会抛异常 - * &(按位与运算):相同的二进制数位上,都是1的时候,结果为1,否则为零 - * ^(按位异或运算):相同的二进制数位上,数字相同,结果为0,不同为1,**不进位加法** + * &(按位与运算):相同的二进制数位上,都是 1 的时候,结果为 1,否则为零 + * ^(按位异或运算):相同的二进制数位上,数字相同,结果为 0,不同为 1,**不进位加法** ```java static final int hash(Object key) { @@ -5185,9 +5185,9 @@ HashMap继承关系如下图所示: HashMap 在进行扩容后,节点**要么就在原来的位置,要么就被分配到"原位置+旧容量"的位置** - 判断:e.hash 与 oldCap 对应的有效高位上的值是 1,即当前数组长度 n 为 1 的位为 x,如果 key 的哈希值 x 位也为 1,则扩容后的索引为 now + n + 判断:e.hash 与 oldCap 对应的有效高位上的值是 1,即当前数组长度 n 二进制为 1 的位为 x 位,如果 key 的哈希值 x 位也为 1,则扩容后的索引为 now + n - 注意:这里也要求**数组长度 2 的幂** + 注意:这里要求**数组长度 2 的幂** ![](https://gitee.com/seazean/images/raw/master/Java/HashMap-resize扩容.png) @@ -10614,7 +10614,7 @@ objD.fieldG = G; // 写 * **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 - 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了),重新扫描该对象 + 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了),重新扫描该对象的引用关系 SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 @@ -11046,7 +11046,7 @@ G1(Garbage-First)是一款面向服务端应用的垃圾收集器,**应用 G1 对比其他处理器的优点: -* **并发与并行**: +* 并发与并行: * 并行性:G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力,此时用户线程 STW * 并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此不会在整个回收阶段发生完全阻塞应用程序的情况 * 其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作,而 G1 GC 可以采用应用线程承担后台运行的 GC 工作,JVM 的 GC 线程处理速度慢时,系统会**调用应用程序线程加速垃圾回收**过程 @@ -11062,7 +11062,7 @@ G1 对比其他处理器的优点: ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1-Region区域.png) -- **空间整合**: +- 空间整合: - CMS:标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理 - G1:整体来看是基于标记 - 整理算法实现的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片 @@ -11146,7 +11146,7 @@ G1 中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不 * 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC * 根区域扫描 (Root Region Scanning):扫描 Survivor 区中指向老年代的,被初始标记标记了的引用及引用的对象,这一个过程是并发进行的,但是必须在 Young GC 之前完成 * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。会计算每个区域的对象活性,即区域中存活对象的比例,若区域中的所有对象都是垃圾,则这个区域会被立即回收(实时回收),给浮动垃圾准备出更多的空间,把需要收集的 Region 放入 CSet 当中 - * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行(防止漏标) + * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行(**防止漏标**) * 筛选回收:并发清理阶段,首先对 CSet 中各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率 ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1收集器.jpg) @@ -14875,7 +14875,7 @@ option 参数: #### jhat -jhat(JVM Heap Analysis Tool):Sun JDK 提供的 jhat 命令与 jmap 命令搭配使用,用于分析 jmap 生成的 heap dump 文件(堆转储快照),jhat 内置了一个微型的 HTTP/HTML 服务器,生成 dump 文件的分析结果后,用户可以在浏览器中查看分析结果 +jhat(JVM Heap Analysis Tool):Sun JDK 提供的 jhat 命令与 jmap 命令搭配使用,用于**分析 jmap 生成的 heap dump 文件**(堆转储快照),jhat 内置了一个微型的 HTTP/HTML 服务器,生成 dump 文件的分析结果后,用户可以在浏览器中查看分析结果 使用语法:`jhat ` @@ -14930,7 +14930,7 @@ options 参数: - 对象等待中:Object.wait() 或 TIMED_WAITING -- 停止,:Parked +- 停止:Parked @@ -14986,6 +14986,8 @@ jstatd 是一个 RMI 服务端程序,相当于代理服务器,建立本地 工具的使用此处不再多言,推荐一个写的非常好的文章,JVM 调优部分的笔记全部参考此文章编写 + + 视频链接:https://www.bilibili.com/video/BV1PJ411n7xZ?p=304 文章链接:https://www.yuque.com/u21195183/jvm/lv1zot @@ -16515,7 +16517,7 @@ public class Kmp { 平衡二叉树(AVL)的特点: -+ 二叉树左右两个子树的高度差不超过1 ++ 二叉树左右两个子树的高度差不超过 1 + 任意节点的左右两个子树都是一颗平衡二叉树 平衡二叉树旋转: diff --git a/Prog.md b/Prog.md index 913726c..a8da42c 100644 --- a/Prog.md +++ b/Prog.md @@ -1263,7 +1263,7 @@ public class SpinLock { ##### 锁消除 -锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除 +锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除,这是 JVM **即时编译器的优化** 锁消除主要是通过**逃逸分析**来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除(同步消除:JVM 内存分配) @@ -4086,7 +4086,7 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { // 数量 + 1 int sz = ++size; - // 【做一次启发式清理】,如果没有清除任何 entry 并且当前使用量达到了负载因子所定义,那么进行 rehash + // 【做一次启发式清理】,如果没有清除任何 entry 并且【当前使用量达到了负载因子所定义,那么进行 rehash if (!cleanSomeSlots(i, sz) && sz >= threshold) // 扩容 rehash(); @@ -5898,8 +5898,6 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 } ``` - - * 拒绝策略相关的内部类 @@ -6238,7 +6236,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 // 如果线程数量是否超过最大线程数,直接回收 // 如果当前线程【允许超时回收并且已经超时了】,就应该被回收了,由于【担保机制】还要做判断: // wc > 1 说明线程池还用其他线程,当前线程可以直接回收 - // workQueue.isEmpty() 前置条件是 wc = 1,如果当前任务队列也是空了,最后一个线程就可以安全的退出 + // workQueue.isEmpty() 前置条件是 wc = 1,【如果当前任务队列也是空了,最后一个线程就可以退出】 if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { // 使用 CAS 机制将 ctl 值 -1 ,减 1 成功的线程,返回 null,代表可以退出 if (compareAndDecrementWorkerCount(c)) @@ -6264,7 +6262,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 } ``` -* processWorkerExit():**线程退出线程池** +* processWorkerExit():**线程退出线程池**,也有担保机制,保证队列中的任务被执行 ```java // 正常退出 completedAbruptly = false,异常退出为 true @@ -13813,6 +13811,8 @@ TCP 协议的使用场景:文件上传和下载、邮件发送和接收、远 四次挥手 +推荐阅读:https://yuanrengu.com/2020/77eef79f.html + *** @@ -13850,6 +13850,8 @@ ServerSocket 类: * 构造方法:`public ServerSocket(int port)` * 常用API:`public Socket accept()`,**阻塞等待**接收一个客户端的 Socket 管道连接请求,连接成功返回一个 Socket 对象 + 三次握手后 TCP 连接建立成功,服务器内核会把连接从 SYN 半连接队列中移出,移入 accept (全连接)队列,等待进程调用 accept 函数时把连接取出。如果进程不能及时调用 accept 函数,就会造成 accept 队列溢出,最终导致建立好的 TCP 连接被丢弃 + 相当于客户端和服务器建立一个数据管道,管道一般不用 close @@ -14531,6 +14533,8 @@ NIO 使用的 SocketChannel 也是使用的堆外内存,源码解析: } ``` +* 读操作相同 + *** diff --git a/SSM.md b/SSM.md index 8c1b353..a4a1c20 100644 --- a/SSM.md +++ b/SSM.md @@ -4,7 +4,7 @@ ORM(Object Relational Mapping): 对象关系映射,指的是持久化数据和实体对象的映射模式,解决面向对象与关系型数据库存在的互不匹配的现象 -![](https://gitee.com/seazean/images/raw/master/Frame/ORM介绍.png) +![](https://gitee.com/seazean/images/raw/master/Frame/MyBatis-ORM介绍.png) **MyBatis**: @@ -1470,7 +1470,7 @@ Mapper 接口开发需要遵循以下规范: 一级缓存是 SqlSession 级别的缓存 - + 工作流程:第一次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,如果没有,从数据库查询用户信息,得到用户信息,将用户信息存储到一级缓存中;第二次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,缓存中有,直接从缓存中获取用户信息。 @@ -2722,7 +2722,7 @@ PageInfo相关API: Spring 是分层的 JavaSE/EE 应用 full-stack 轻量级开源框架 -![](https://gitee.com/seazean/images/raw/master/Frame/Spring框架介绍.png) +![](https://gitee.com/seazean/images/raw/master/Frame/Spring-框架介绍.png) Spring 优点: @@ -2735,7 +2735,7 @@ Spring 优点: 体系结构: -![](https://gitee.com/seazean/images/raw/master/Frame/Spring体系结构.png) +![](https://gitee.com/seazean/images/raw/master/Frame/Spring-体系结构.png) @@ -2751,7 +2751,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://gitee.com/seazean/images/raw/master/Frame/Spring-IOC介绍.png) @@ -2835,7 +2835,7 @@ Spring 优点: } ``` - ![](https://gitee.com/seazean/images/raw/master/Frame/Spring_ioc实现.png) + ![](https://gitee.com/seazean/images/raw/master/Frame/Spring-IOC实现.png) @@ -3091,7 +3091,7 @@ ApplicationContext 子类相关API: - DI(Dependency Injection)依赖注入,应用程序运行依赖的资源由 Spring 为其提供,资源进入应用程序的方式称为注入。简单说就是利用反射机制为类的属性赋值的操作 - ![](https://gitee.com/seazean/images/raw/master/Frame/DI介绍.png) + ![](https://gitee.com/seazean/images/raw/master/Frame/Spring-DI介绍.png) IoC 和 DI 的关系:IoC 与 DI 是同一件事站在不同角度看待问题 @@ -4582,7 +4582,7 @@ ApplicationContext: FileSystemXmlApplicationContext:加载文件系统中任意位置的配置文件,而 ClassPathXmlAC 只能加载类路径下的配置文件 -![](https://gitee.com/seazean/images/raw/master/Frame/ApplicationContext层级结构图.png) +![](https://gitee.com/seazean/images/raw/master/Frame/Spring-ApplicationContext层级结构图.png) BeanFactory 的成员属性: @@ -8521,7 +8521,7 @@ protected Object[] getAdvicesAndAdvisorsForBean(Class beanClass, String beanN AbstractAdvisorAutoProxyCreator.findEligibleAdvisors(): -* `candidateAdvisors = findCandidateAdvisors()`:**获取当前容器内可以使用(所有)的 advisor**,调用的是 AnnotationAwareAspectJAutoProxyCreator 类的方法 +* `candidateAdvisors = findCandidateAdvisors()`:**获取当前容器内可以使用(所有)的 advisor**,调用的是 AnnotationAwareAspectJAutoProxyCreator 类的方法,每个方法对应一个 Advisor * `advisors = super.findCandidateAdvisors()`:**查询出 XML 配置的所有 Advisor 类型** @@ -8580,7 +8580,7 @@ AbstractAdvisorAutoProxyCreator.findEligibleAdvisors(): * `if (!Proxy.isProxyClass(targetClass))`:判断当前实例是不是代理类,确保 class 内存储的数据包括目标对象的class 而不是代理类的 class * `for (Class clazz : classes)`:**检查目标 class 和上级接口的所有方法,查看是否会被方法匹配器匹配**,如果有一个方法匹配成功,就说明目标对象 AOP 代理需要增强 * `specificMethod = AopUtils.getMostSpecificMethod(method, targetClass)`:方法可能是接口的,判断当前类有没有该方法 - * `return (specificMethod != method && matchesMethod(specificMethod))`:**类和方法的匹配**,不包括参数,就是静态匹配 + * `return (specificMethod != method && matchesMethod(specificMethod))`:**类和方法的匹配**,不包括参数 * `extendAdvisors(eligibleAdvisors)`:在 eligibleAdvisors 列表的索引 0 的位置添加 DefaultPointcutAdvisor,**封装了 ExposeInvocationInterceptor 拦截器** @@ -9128,7 +9128,7 @@ SpringMVC 优点: - 数据层:负责数据操作 - ![](https://gitee.com/seazean/images/raw/master/Frame/MVC三层架构.png) + ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-MVC三层架构.png) MVC(Model View Controller),一种用于设计创建Web应用程序表现层的模式 @@ -9143,7 +9143,7 @@ MVC(Model View Controller),一种用于设计创建Web应用程序表现 * Servlet * SpringMVC - ![](https://gitee.com/seazean/images/raw/master/Frame/MVC功能图示.png) + ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-MVC功能图示.png) @@ -10651,7 +10651,7 @@ SpringMVC 提供访问原始 Servlet 接口的功能 * View:视图, View 最后对页面进行渲染将结果返回给用户。SpringMVC 框架提供了很多的 View 视图类型,包括:jstlView、freemarkerView、pdfView 等 - ![](https://gitee.com/seazean/images/raw/master/Frame/SpingMVC-技术架构.png) + ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-技术架构.png) @@ -12982,6 +12982,8 @@ public class HelloController { ### 结构搭建 +整合 SSM 三种框架进行项目开发 + * 创建项目,组织项目结构,创建包 * 创建表与实体类 @@ -12993,7 +12995,7 @@ public class HelloController { * 业务层接口 + 业务层实现类 * 表现层类 - ![](https://gitee.com/seazean/images/raw/master/Frame/SSM目录结构.png) + ![](https://gitee.com/seazean/images/raw/master/Frame/SSM-目录结构.png) @@ -13778,7 +13780,7 @@ public class ProjectExceptionAdivce { ### applicationContext.xml -![](https://gitee.com/seazean/images/raw/master/Frame/IoC注解整合MyBatis图解.png) +![](https://gitee.com/seazean/images/raw/master/Frame/SSM-IoC注解整合MyBatis图解.png) * JdbcConfig diff --git a/Tool.md b/Tool.md index 7d53ef2..93c4d62 100644 --- a/Tool.md +++ b/Tool.md @@ -517,14 +517,12 @@ File → Close Project → Checkout from Version Control → Git → 指定远 ### 系统介绍 -从内到位依次是硬件--内核层--Shell层--应用层--用户 +从内到位依次是硬件 → 内核层 → Shell 层 → 应用层 → 用户 ![Linux](https://gitee.com/seazean/images/raw/master/Tool/Linux系统.png) -内核层: - 核心和基础,附着在硬件平台上,控制和管理系统内的各种资源,有效的组织进程的运行,扩展硬件的功能,提高资源利用效率,为用户提供安全可靠的应用环境。 +内核层:核心和基础,附着在硬件平台上,控制和管理系统内的各种资源,有效的组织进程的运行,扩展硬件的功能,提高资源利用效率,为用户提供安全可靠的应用环境。 -Shell层: - Shell 层是与用户直接交互的界面。用户可以在提示符下输入命令行,由 Shell 解释执行并输出相应结果或者有关信息,所以我们也把 Shell 称作命令解释器,利用系统提供的丰富命令可以快捷而简便地完成许多工作。 +Shell 层:与用户直接交互的界面。用户可以在提示符下输入命令行,由 Shell 解释执行并输出相应结果或者有关信息,所以我们也把 Shell 称作命令解释器,利用系统提供的丰富命令可以快捷而简便地完成许多工作。 @@ -2357,7 +2355,7 @@ ifconfig [网络设备][down up -allmulti -arp -promisc][add<地址>][del<地址 **ens33(有的是eth0)**表示第一块网卡。 - 表示 ens33 网卡的 IP地址是 192.168.0.137,广播地址,broadcast 192.168.0.255,掩码地址netmask:255.255.255.0 ,inet6 对应的是ipv6 + 表示 ens33 网卡的 IP地址是 192.168.0.137,广播地址,broadcast 192.168.0.255,掩码地址netmask:255.255.255.0 ,inet6 对应的是 ipv6 **lo** 是表示主机的回坏地址,这个一般是用来测试一个网络程序,但又不想让局域网或外网的用户能够查看,只能在此台主机上运行和查看所用的网络接口 @@ -2405,14 +2403,14 @@ netstat 命令用于显示网络状态 netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip] ``` -- -a 显示所有连线中的Socket,显示详细的连接状况 +- -a 显示所有连线中的 Socket,显示详细的连接状况 - -i 显示网络界面信息表单,显示网卡列表 -- -p 显示正在使用Socket的程序识别码和程序名称 -- -n 显示使用IP地址,而不通过域名服务器 -- -t 显示TCP传输协议的连线状况。 -- -u 显示UDP传输协议的连线状况 -- **-aptn:查看所有TCP开启端口** -- **-apun:查看所有UDP开启端口** +- -p 显示正在使用 Socket 的程序识别码和程序名称 +- -n 显示使用 IP 地址,而不通过域名服务器 +- -t 显示 TCP 传输协议的连线状况。 +- -u 显示 UDP 传输协议的连线状况 +- **-aptn:查看所有 TCP 开启端口** +- **-apun:查看所有 UDP 开启端口** 补充: @@ -2433,46 +2431,48 @@ netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip] ### 挂载概念 -在安装linux系统时设立的各个分区,如根分区、/boot分区等都是自动挂载的,也就是说不需要我们人为操作,开机就会自动挂载。但是光盘、u盘等存储设备如果需要使用,就必须人为的进行挂载。 +在安装 Linux 系统时设立的各个分区,如根分区、/boot 分区等都是自动挂载的,也就是说不需要我们人为操作,开机就会自动挂载。但是光盘、U 盘等存储设备如果需要使用,就必须人为的进行挂载。 -其实我们在windows下插入U盘也是需要挂载(分配盘符)的,只不过windows下分配盘符是自动的。其实挂载可以理解为Windows当中的分配盘符(重要),只不过windows当中是以英文字母ABCD等作为盘符,而linux是拿系统目录作为盘符,当然linux当中也不叫盘符,而是称为挂载点,而把为分区或者光盘等存储设备分配一个挂载点的过程称为挂载 +在 Windows 下插入 U 盘也是需要挂载(分配盘符)的,只不过 Windows 下分配盘符是自动的。其实挂载可以理解为 Windows 当中的分配盘符(重要),只不过 Windows 当中是以英文字母 ABCD 等作为盘符,而 Linux 是拿系统目录作为盘符,当然 Linux 当中也不叫盘符,而是称为挂载点,而把为分区或者光盘等存储设备分配一个挂载点的过程称为挂载 -Linux中的根目录以外的文件要想被访问,需要将其“关联”到根目录下的某个目录来实现,这种关联操作就是“挂载”,这个目录就是“挂载点”,解除次关联关系的过程称之为“卸载”。 +Linux 中的根目录以外的文件要想被访问,需要将其“关联”到根目录下的某个目录来实现,这种关联操作就是挂载,这个目录就是挂载点,解除次关联关系的过程称之为卸载 **注意:“挂载点”的目录需要以下几个要求:** -(1)目录要先存在,可以用mkdir命令新建目录; +* 目录要先存在,可以用 mkdir 命令新建目录 +* 挂载点目录不可被其他进程使用到 +* 挂载点下原有文件将被隐藏 -(2)挂载点目录不可被其他进程使用到; -(3)挂载点下原有文件将被隐藏。 + +*** ### lsblk -lsblk命令的英文是“list block”,即用于列出所有可用块设备的信息,而且还能显示他们之间的依赖关系,但是它不会列出RAM盘的信息。 +lsblk 命令的英文是 list block,即用于列出所有可用块设备的信息,而且还能显示他们之间的依赖关系,但是不会列出 RAM 盘的信息 命令:lsblk [参数] * `lsblk`:以树状列出所有块设备 ![](https://gitee.com/seazean/images/raw/master/Tool/可用块设备.png) - NAME : 这是块设备名。 + NAME:这是块设备名。 - MAJ:MIN : 本栏显示主要和次要设备号。 + MAJ:MIN : 本栏显示主要和次要设备号。 - RM : 本栏显示设备是否可移动设备。注意,在上面设备sr0的RM值等于1,这说明他们是可移动设备。 + RM:本栏显示设备是否可移动设备。注意,在上面设备sr0的RM值等于1,这说明他们是可移动设备。 - SIZE : 本栏列出设备的容量大小信息。 + SIZE:本栏列出设备的容量大小信息。 - RO : 该项表明设备是否为只读。在本案例中,所有设备的RO值为0,表明他们不是只读的。 + RO:该项表明设备是否为只读。在本案例中,所有设备的RO值为0,表明他们不是只读的。 - TYPE :本栏显示块设备是否是磁盘或磁盘上的一个分区。在本例中,sda和sdb是磁盘,而sr0是只读存储(rom)。 + TYPE:本栏显示块设备是否是磁盘或磁盘上的一个分区。在本例中,sda和sdb是磁盘,而sr0是只读存储(rom)。 - MOUNTPOINT : 本栏指出设备挂载的挂载点。 + MOUNTPOINT:本栏指出设备挂载的挂载点。 @@ -2491,9 +2491,13 @@ lsblk命令的英文是“list block”,即用于列出所有可用块设备 +*** + + + ### df - df命令用于显示目前在Linux系统上的文件系统的磁盘使用情况统计。 + df 命令用于显示目前在 Linux 系统上的文件系统的磁盘使用情况统计。 命令:df [options]... [FILE]... @@ -2502,21 +2506,17 @@ lsblk命令的英文是“list block”,即用于列出所有可用块设备 ![](https://gitee.com/seazean/images/raw/master/Tool/磁盘管理.png) -第一列指定文件系统的名称 +第一列指定文件系统的名称;第二列指定一个特定的文件系统,1K 是 1024 字节为单位的总容量;已用和可用列分别指定的容量;最后一个已用列指定使用的容量的百分比;最后一栏指定的文件系统的挂载点 -第二列指定一个特定的文件系统1K-块1K是1024字节为单位的总容量。 -已用和可用列分别指定的容量。 -最后一个已用列指定使用的容量的百分比 - -最后一栏指定的文件系统的挂载点。 +**** ### mount -mount命令是经常会使用到的命令,它用于挂载Linux系统外的文件。 +mount 命令是经常会使用到的命令,它用于挂载 Linux 系统外的文件 使用者权限:所有用户,设置级别的需要管理员 @@ -2531,9 +2531,9 @@ mount [-fnrsvw] [-t vfstype] [-o options] device dir 通过挂载的方式查看Linux CD/DVD光驱,查看 ubuntu-20.04.1-desktop-amd64.iso的文件 -* 进入【虚拟机】--【设置】,设置CD/DVD的内容,ubuntu-20.04.1-desktop-amd64.iso -* 创建挂载点(注意:一般用户无法挂载cdrom,只有root用户才可以操作) - `mkdir -p /mnt/cdrom `:切换到root下创建一个挂载点(其实就是创建一个目录) +* 进入【虚拟机】--【设置】,设置 CD/DVD 的内容,ubuntu-20.04.1-desktop-amd64.iso +* 创建挂载点(注意:一般用户无法挂载 cdrom,只有 root 用户才可以操作) + `mkdir -p /mnt/cdrom `:切换到 root 下创建一个挂载点(其实就是创建一个目录) * 开始挂载 `mount -t auto /dev/cdrom /mnt/cdrom`:通过挂载点的方式查看上面的【ISO文件内容】 ![挂载成功](https://gitee.com/seazean/images/raw/master/Tool/挂载成功.png) From 9b2e0662c9735fbeeafbc204b8efe0054e3e2bb8 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 7 Sep 2021 16:46:14 +0800 Subject: [PATCH 002/122] Update Java Notes --- DB.md | 9 ++++++++- Java.md | 20 +++++++++---------- SSM.md | 14 ++++++------- Web.md | 62 ++++++++++++++++++++++++++------------------------------- 4 files changed, 53 insertions(+), 52 deletions(-) diff --git a/DB.md b/DB.md index abb9c75..1b163ad 100644 --- a/DB.md +++ b/DB.md @@ -4728,7 +4728,14 @@ Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算 #### 定位低效 -慢 SQL 由三种原因造成:索引没有设计好、SQL 语句没写好、MySQL 选错了索引 +慢 SQL 由三种原因造成: + +* 偶尔慢:DB 在刷新脏页 + * redo log 写满了 + * 内存不够用,要从 LRU 链表中淘汰 + * MySQL 认为系统空闲的时候 + * MySQL 关闭时 +* 一直慢的原因:索引没有设计好、SQL 语句没写好、MySQL 选错了索引 通过以下两种方式定位执行效率较低的 SQL 语句 diff --git a/Java.md b/Java.md index ecbed28..2745010 100644 --- a/Java.md +++ b/Java.md @@ -2856,8 +2856,8 @@ public class MyArraysDemo { 1. 导入包:`import java.util.Random` 2. 创建对象:`Random r = new Random()` 3. 随机整数:`int num = r.nextInt(10)` -* 解释:10 代表的是一个范围,如果括号写 10,产生的随机数就是 0-9,括号写 20 的随机数则是 0-19 -* 获取 0-10:`int num = r.nextInt(10 + 1)` + * 解释:10 代表的是一个范围,如果括号写 10,产生的随机数就是 0 - 9,括号写 20 的随机数则是 0 - 19 + * 获取 0 - 10:`int num = r.nextInt(10 + 1)` 4. 随机小数:`public double nextDouble()` 从范围 `0.0d` 至 `1.0d` (左闭右开),伪随机地生成并返回 @@ -12217,7 +12217,7 @@ ClassLoader 类常用方法: * `loadclass(String name)`:加载名为 name 的类,返回结果为 Class 类的实例,**该方法就是双亲委派模式** * `findclass(String name)`:查找二进制名称为 name 的类,返回结果为 Class 类的实例,该方法会在检查完父类加载器之后被 loadClass() 方法调用 * `findLoadedClass(String name)`:查找名称为 name 的已经被加载过的类,final 修饰无法重写 -* `defineClass(String name, byte[] b, int off, int len)`:将字节流解析成 JVM 能够识别的类对象 +* `defineClass(String name, byte[] b, int off, int len)`:将**字节流**解析成 JVM 能够识别的类对象 * `resolveclass(Class c)`:链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 * `InputStream getResourceAsStream(String name)`:指定资源名称获取输入流 @@ -12424,21 +12424,21 @@ public class MyClassLoader extends ClassLoader{ BufferedInputStream bis = null; ByteArrayOutputStream baos = null; try { - //获取字节码文件的完整路径 + // 获取字节码文件的完整路径 String fileName = classPath + className + ".class"; - //获取一个输入流 + // 获取一个输入流 bis = new BufferedInputStream(new FileInputStream(fileName)); - //获取一个输出流 + // 获取一个输出流 baos = new ByteArrayOutputStream(); - //具体读入数据并写出的过程 + // 具体读入数据并写出的过程 int len; byte[] data = new byte[1024]; while ((len = bis.read(data)) != -1) { baos.write(data, 0, len); } - //获取内存中的完整的字节数组的数据 + // 获取内存中的完整的字节数组的数据 byte[] byteCodes = baos.toByteArray(); - //调用 defineClass(),将字节数组的数据转换为 Class 的实例。 + // 调用 defineClass(),将字节数组的数据转换为 Class 的实例。 Class clazz = defineClass(null, byteCodes, 0, byteCodes.length); return clazz; } catch (IOException e) { @@ -12507,7 +12507,7 @@ public static void main(String[] args) { ### 执行过程 - Java文件编译执行的过程: + Java 文件编译执行的过程: ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java文件编译执行的过程.png) diff --git a/SSM.md b/SSM.md index a4a1c20..d1d5307 100644 --- a/SSM.md +++ b/SSM.md @@ -628,7 +628,7 @@ Mapper 接口开发需要遵循以下规范: :返回的是一个集合,要写集合中元素的类型 -:返回一条记录的Map,key 是列名,value 是对应的值,用来配置字段和对象属性的映射关系标签,结果映射(和 resultType 二选一) +:返回一条记录的 Map,key 是列名,value 是对应的值,用来配置字段和对象属性的映射关系标签,结果映射(和 resultType 二选一) * id 属性:唯一标识 * type 属性:实体对象类型 @@ -650,7 +650,7 @@ Mapper 接口开发需要遵循以下规范: * :鉴别器,用来判断某列的值,根据得到某列的不同值做出不同自定义的封装行为 -自定义封装规则可以将数据库中比较复杂的数据类型映射为 javaBean 中的属性 +自定义封装规则可以将数据库中比较复杂的数据类型映射为 JavaBean 中的属性 @@ -1374,14 +1374,14 @@ Mapper 接口开发需要遵循以下规范: 学生和课程 -* SQL查询语句 +* SQL 查询语句 ```mysql SELECT DISTINCT s.id,s.name,s.age FROM student s,stu_cr sc WHERE sc.sid=s.id SELECT c.id,c.name FROM stu_cr sc,course c WHERE sc.cid=c.id AND sc.sid=#{id} ``` -* CourseMapper接口 +* CourseMapper 接口 ```java public interface CourseMapper { @@ -1391,7 +1391,7 @@ Mapper 接口开发需要遵循以下规范: } ``` -* StudentMapper接口 +* StudentMapper 接口 ```java public interface StudentMapper { @@ -2901,7 +2901,7 @@ Spring 容器中 Bean 的**线程安全**问题: * 原型 Bean,每次创建一个新对象,线程之间并不存在 Bean 共享,所以不会有线程安全的问题 -* 单例Bean,所有线程共享一个单例实例 Bean,因此是存在资源的竞争,如果单例 Bean是一个**无状态 Bean**,也就是线程中的操作不会对 Bean 的成员执行查询以外的操作,那么这个单例 Bean 是线程安全的 +* 单例 Bean,所有线程共享一个单例实例 Bean,因此是存在资源的竞争,如果单例 Bean是一个**无状态 Bean**,也就是线程中的操作不会对 Bean 的成员执行查询以外的操作,那么这个单例 Bean 是线程安全的 解决方法:开发人员来进行线程安全的保证,最简单的办法就是把 Bean 的作用域 singleton 改为 protopyte @@ -14089,7 +14089,7 @@ SpringBoot 功能: #### SpringBoot -@SpringBootApplication():启动注解,实现 SpringBoot 的自动部署 +@SpringBootApplication:启动注解,实现 SpringBoot 的自动部署 * 参数 scanBasePackages:可以指定扫描范围 * 默认扫描当前引导类所在包及其子包 diff --git a/Web.md b/Web.md index 5321f0f..631d9d6 100644 --- a/Web.md +++ b/Web.md @@ -2089,8 +2089,8 @@ HTTP 作用:用于定义 WEB 浏览器与 WEB 服务器之间交换数据的 浏览器和服务器交互过程:浏览器请求,服务请求响应 -* 请求(请求行,请求头,请求体) -* 响应(响应行,响应头,响应体) +* 请求(请求行、请求头、请求体) +* 响应(响应行、响应头、响应体) URL 和 URI @@ -2101,8 +2101,7 @@ URL 和 URI * URI:统一资源标志符 格式:/request/servletDemo01 -* 区别:`URL-HOST=URI`,URI是抽象的定义,URL用地址定位,URI 用名称定位。 - **只要能唯一标识资源的是URI,在URI的基础上给出其资源的访问方式的是URL** +* 区别:`URL-HOST=URI`,URI是抽象的定义,URL用地址定位,URI 用名称定位。**只要能唯一标识资源的是 URI,在 URI 的基础上给出其资源的访问方式的是 URL** 短连接和长连接: @@ -2132,20 +2131,18 @@ URL 和 URI ## 版本区别 -87691457 - 版本介绍: -* HTTP/0.9 仅支持 GET 请求,不支持请求头。 +* HTTP/0.9 仅支持 GET 请求,不支持请求头 * HTTP/1.0 默认短连接(一次请求建议一次 TCP 连接,请求完就断开),支持 GET、POST、 HEAD 请求 -* HTTP/1.1 默认长连接(一次 TCP 连接可以多次请求);支持 PUT、DELETE、PATCH 等六种请求;增加 host 头,支持虚拟主机;支持断点续传功能 +* HTTP/1.1 默认长连接(一次 TCP 连接可以多次请求);支持 PUT、DELETE、PATCH 等六种请求;增加 host 头,支持虚拟主机;支持**断点续传**功能 * HTTP/2.0 多路复用,降低开销(一次 TCP 连接可以处理多个请求);服务器主动推送(相关资源一个请求全部推送);解析基于二进制,解析错误少,更高效(HTTP/1.X 解析基于文本);报头压缩,降低开销。 HTTP 1.0 和 HTTP 1.1 的主要区别: * 长短连接: - **在HTTP/1.0中,默认使用的是短连接**,每次请求都要重新建立一次连接。HTTP 基于 TCP/IP 协议的,每一次建立或者断开连接都需要三次握手四次挥手的开销,如果每次请求都要这样的话,开销会比较大。因此最好能维持一个长连接,可以用个长连接来发多个请求 + **在HTTP/1.0中,默认使用的是短连接**,每次请求都要重新建立一次连接。HTTP 基于 TCP/IP 协议的,每一次建立或者断开连接都需要三次握手四次挥手,开销会比较大 **HTTP 1.1起,默认使用长连接** ,默认开启 `Connection: keep-alive`,HTTP/1.1 的持续连接有非流水线方式和流水线方式 ,流水线方式是客户端在收到 HTTP 的响应报文之前就能接着发送新的请求报文,非流水线方式是客户端在收到前一个响应后才能发送下一个请求 @@ -2157,13 +2154,13 @@ HTTP 1.0 和 HTTP 1.1 的主要区别: HTTP 和 HTTPS 的区别: -* 端口 :HTTP 默认使用端口 80,HTTPS 默认使用端口443 -* 安全性: HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份;HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上,所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密 -* 资源消耗:HTTP 安全性没有 HTTPS高,但是 HTTPS 比 HTTP 耗费更多服务器资源 +* 端口 :HTTP 默认使用端口 80,HTTPS 默认使用端口 443 +* 安全性:HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份;HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上,所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密 +* 资源消耗:HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源 **对称加密和非对称加密** -* 对称加密:加密和解密使用同一个秘钥,把密钥转发给需要发送数据的客户机,中途会被拦截(类似于把带锁的箱子和钥匙给别人,对方打开箱子放入数据,上锁后发送),典型的对称加密算法有DES、AES等 +* 对称加密:加密和解密使用同一个秘钥,把密钥转发给需要发送数据的客户机,中途会被拦截(类似于把带锁的箱子和钥匙给别人,对方打开箱子放入数据,上锁后发送),典型的对称加密算法有 DES、AES 等 * 优点:运算速度快 * 缺点:无法安全的将密钥传输给通信方 @@ -2228,12 +2225,12 @@ HTTP 和 HTTPS 的区别: * 安全的方法除了 GET 之外还有:HEAD、OPTIONS * 不安全的方法除了 POST 之外还有 PUT、DELETE - 幂等性:同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。所有的安全方法也都是幂等的。在正确实现条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,POST 方法不是 + 幂等性:同样的请求**被执行一次与连续执行多次的效果是一样的**,服务器的状态也是一样的。所有的安全方法也都是幂等的。在正确实现条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,POST 方法不是 可缓存:如果要对响应进行缓存,需要满足以下条件 - * 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的 - * 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 + * 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存 + * 响应报文的状态码是可缓存的,包括:200、203、204、206、300、301、404、405、410、414 and 501 * 响应报文的 Cache-Control 首部字段没有指定不进行缓存 @@ -2309,24 +2306,23 @@ HTTP 和 HTTPS 的区别: * 响应状态码: ![](https://gitee.com/seazean/images/raw/master/Web/HTTP状态响应码.png) - | 状态码 | 说明 | - | ------- | ------------------------------------------------ | - | 200 | 一切都OK>,与服务器连接成功,发送请求成功 | - | 302/307 | 请求重定向(客户端行为,两次请求,地址栏发生改变) | - | 304 | 请求资源未改变,使用缓存 | - | 400 | 客户端错误,请求错误,最常见的就是请求参数有问题 | - | 403 | 客户端错误,但forbidden权限不够,拒绝处理 | - | 404 | 客户端错误,请求资源未找到 | - | 500 | 服务器错误,服务器运行内部错误 | + | 状态码 | 说明 | + | ------- | -------------------------------------------------- | + | 200 | 一切都 OK,与服务器连接成功,发送请求成功 | + | 302/307 | 请求重定向(客户端行为,两次请求,地址栏发生改变) | + | 304 | 请求资源未改变,使用缓存 | + | 400 | 客户端错误,请求错误,最常见的就是请求参数有问题 | + | 403 | 客户端错误,但 forbidden权 限不够,拒绝处理 | + | 404 | 客户端错误,请求资源未找到 | + | 500 | 服务器错误,服务器运行内部错误 | 面试题: - * 301 redirect: 301 代表永久性转移(Permanently Moved)。 - * 302 redirect: 302 代表暂时性转移(Temporarily Moved ) - -* 响应头 - 响应头以key:vaue存在, 可能多个value情况。 + * 301 redirect: 301 代表永久性转移 (Permanently Moved) + * 302 redirect: 302 代表暂时性转移 (Temporarily Moved ) +* 响应头:以 key:vaue 存在,可能多个 value 情况。 + | 消息头 | 说明 | | ----------------------- | ------------------------------------------------------------ | | Location | 请求重定向的地址,常与302,307配合使用。 | @@ -2345,9 +2341,7 @@ HTTP 和 HTTPS 的区别: -* 响应体 - - 页面展示内容, 类似网页的源码 +* 响应体:页面展示内容, 类似网页的源码 ```html @@ -2360,7 +2354,7 @@ HTTP 和 HTTPS 的区别: ``` - + From 33a57f192651c6f7579a02786f61d5845e526c64 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 9 Sep 2021 20:14:00 +0800 Subject: [PATCH 003/122] Update Java Notes --- DB.md | 34 +++++++++++++++++----------------- Java.md | 29 +++++++++++++++-------------- Prog.md | 24 ++++++++++++------------ SSM.md | 17 +++++++++-------- Tool.md | 20 ++++++++++---------- Web.md | 44 ++++++++++++++++++++++---------------------- 6 files changed, 85 insertions(+), 83 deletions(-) diff --git a/DB.md b/DB.md index 1b163ad..c8a31f5 100644 --- a/DB.md +++ b/DB.md @@ -385,7 +385,7 @@ mysqlshow -uroot -p1234 test book --count SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 SQL 的执行情况,其中的 Command 列显示为 Sleep 的这一行,就表示现在系统里面有一个空闲连接 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SHOW PROCESSLIST命令.png) +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SHOW_PROCESSLIST命令.png) | 参数 | 含义 | | ------- | ------------------------------------------------------------ | @@ -4139,12 +4139,12 @@ MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获 - R-tree 索引(空间索引):空间索引是 MyISAM 引擎的一个特殊索引类型,主要用于地理空间数据类型 - Full-text 索引(全文索引):快速匹配全部文档的方式。MyISAM 支持, InnoDB 不支持 FULLTEXT 类型的索引,但是 InnoDB 可以使用 sphinx 插件支持全文索引,MEMORY 引擎不支持 - | 索引 | InnoDB引擎 | MyISAM引擎 | Memory引擎 | - | --------- | --------------- | ---------- | ---------- | - | BTREE | 支持 | 支持 | 支持 | - | HASH | 不支持 | 不支持 | 支持 | - | R-tree | 不支持 | 支持 | 不支持 | - | Full-text | 5.6版本之后支持 | 支持 | 不支持 | + | 索引 | InnoDB | MyISAM | Memory | + | --------- | ---------------- | ------ | ------ | + | BTREE | 支持 | 支持 | 支持 | + | HASH | 不支持 | 不支持 | 支持 | + | R-tree | 不支持 | 支持 | 不支持 | + | Full-text | 5.6 版本之后支持 | 支持 | 不支持 | 联合索引图示:根据身高年龄建立的组合索引(height,age) @@ -5894,7 +5894,7 @@ MySQL 的主从之间维持了一个长连接。主库内部有一个线程, coordinator 就是原来的 sql_thread,并行复制中它不再直接更新数据,只**负责读取中转日志和分发事务**: -* 线程分配完成并不是立即执行,为了防止造成更新覆盖,更新同一行的两个事务必须被分发到同一个工作线程 +* 线程分配完成并不是立即执行,为了防止造成更新覆盖,更新同一 DB 的两个事务必须被分发到同一个工作线程 * 同一个事务不能被拆开,必须放到同一个工作线程 MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库 名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 @@ -5902,13 +5902,13 @@ MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当 每个事务在分发的时候,跟线程的冲突(事务操作的是同一个 DB)关系包括以下三种情况: * 如果跟所有线程都不冲突,coordinator 线程就会把这个事务分配给最空闲的线程 -* 如果跟多于一个线程冲突,coordinator 线程就进入等待状态,直到和这个事务存在冲突关系的线程只剩下 1 个 * 如果只跟一个线程冲突,coordinator 线程就会把这个事务分配给这个存在冲突关系的线程 +* 如果跟多于一个线程冲突,coordinator 线程就进入等待状态,直到和这个事务存在冲突关系的线程只剩下 1 个 优缺点: * 构造 hash 值的时候很快,只需要库名,而且一个实例上 DB 数也不会很多,不会出现需要构造很多个项的情况 -* 不要求 binlog 的格式,statement 格式的 binlog 也可以很容易拿到库名 +* 不要求 binlog 的格式,statement 格式的 binlog 也可以很容易拿到库名(日志章节详解了 binlog) * 主库上的表都放在同一个 DB 里面,这个策略就没有效果了;或者不同 DB 的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果,需要**把相同热度的表均匀分到这些不同的 DB 中**,才可以使用这个策略 @@ -5965,7 +5965,7 @@ MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于 * 主库不建查询的索引,从库建查询的索引。因为索引需要维护的,比如插入一条数据,不仅要在聚簇索引上面插入,对应的二级索引也得插入 * 将读操作分到从库了之后,可以在主库把查询要用的索引删了,减少写操作对主库的影响 -读写分离产生了读写延迟,造成数据的不一致性。假如客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,可能读到的还是以前的数据,叫过期读, +读写分离产生了读写延迟,造成数据的不一致性。假如客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,可能读到的还是以前的数据,叫过期读 解决方案: @@ -6221,7 +6221,7 @@ FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处 MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL) -MDL 叫元数据锁,主要用来保护 MySQL内部对象的元数据,保证数据读写的正确性,通过 MDL 机制保证 DDL、DML、SELECT 操作的并发,**当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁** +MDL 叫元数据锁,主要用来保护 MySQL内部对象的元数据,保证数据读写的正确性,通过 MDL 机制保证 DDL、DML、DQL 操作的并发,**当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁** * MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,事务中的 MDL 锁,在语句执行开始时申请,在整个事务提交后释放 @@ -6852,7 +6852,7 @@ binlog_format=STATEMENT 缺点:记录的数据比较多,占用很多的存储空间 -* MIXED:这是 MySQL 默认的日志格式,混合了STATEMENT 和 ROW两种格式。MIXED 格式能尽量利用两种模式的优点,而避开它们的缺点 +* MIXED:这是 MySQL 默认的日志格式,混合了STATEMENT 和 ROW 两种格式。MIXED 格式能尽量利用两种模式的优点,而避开它们的缺点 @@ -7035,10 +7035,10 @@ long_query_time=10 ## 范式 -建立科学的,规范的数据库就需要满足一些规则来优化数据的设计和存储,这些规则就称为范式。 - ### 第一范式 +建立科学的,**规范的数据表**就需要满足一些规则来优化数据的设计和存储,这些规则就称为范式 + **1NF:**数据库表的每一列都是不可分割的原子数据项,不能是集合、数组等非原子数据项。即表中的某个列有多个值时,必须拆分为不同的列。简而言之,**第一范式每一列不可再拆分,称为原子性** 基本表: @@ -8513,7 +8513,7 @@ Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被 * 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器 -* 当被监听的套接字准备好执行连接应答 (accept)、读取 (read)、写入 (write)、关闭 (close) 等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器会调用套接字之前关联好的事件处理器来处理事件 +* 当被监听的套接字准备好执行连接应答 (accept)、读取 (read)、写入 (write)、关闭 (close) 等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器会将处理请求放入**单线程的执行队列**中,等待调用套接字关联好的事件处理器来处理事件 **Redis 单线程也能高效的原因**: @@ -11524,7 +11524,7 @@ Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解 缓存不一致的方法: -* 数据库和缓存数据强一致场景 : +* 数据库和缓存数据强一致场景: * 更新 DB 时同样更新 cache,加一个锁来保证更新 cache 时不存在线程安全问题,这样可以增加命中率 * 延迟双删:先淘汰缓存再写数据库,休眠 1 秒再次淘汰缓存,可以将 1 秒内造成的缓存脏数据再次删除 * CDC 同步:通过 canal 订阅 MySQL binlog 的变更上报给 Kafka,系统监听 Kafka 消息触发缓存失效 diff --git a/Java.md b/Java.md index 2745010..14c3b26 100644 --- a/Java.md +++ b/Java.md @@ -1559,6 +1559,7 @@ public class FinalDemo { > 父类知道子类要完成某个功能,但是每个子类实现情况不一样。 抽象方法:没有方法体,只有方法签名,必须用**abstract**修饰的方法就是抽象方法 + 抽象类:拥有抽象方法的类必须定义成抽象类,必须用**abstract**修饰,抽象类是为了被继承 一个类继承抽象类,**必须重写抽象类的全部抽象方法**,否则这个类必须定义成抽象类,因为拥有抽象方法的类必须定义成抽象类 @@ -1618,6 +1619,7 @@ abstract class Animal{ ``` 二、static 与 abstract 能同时使用吗? + 答:不能,被 static 修饰的方法属于类,是类自己的东西,不是给子类来继承的,而抽象方法本身没有实现,就是用来给子类继承 @@ -1833,12 +1835,12 @@ interface InterfaceJDK8{ | **参数** | **抽象类** | **接口** | | ------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -| 默认的方法实现 | 可以有默认的方法实现 | 接口完全是抽象的,jdk8以后有默认的实现 | -| 实现 | 子类使用**extends**关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 | 子类使用关键字**implements**来实现接口。它需要提供接口中所有声明的方法的实现 | +| 默认的方法实现 | 可以有默认的方法实现 | 接口完全是抽象的,jdk8 以后有默认的实现 | +| 实现 | 子类使用 **extends** 关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 | 子类使用关键字 **implements** 来实现接口。它需要提供接口中所有声明的方法的实现 | | 构造器 | 抽象类可以有构造器 | 接口不能有构造器 | -| 与正常Java类的区别 | 除了不能实例化抽象类之外,和普通Java类没有任何区别 | 接口是完全不同的类型 | -| 访问修饰符 | 抽象方法可以有**public**、**protected**和**default**这些修饰符 | 接口方法默认修饰符是**public**,别的修饰符需要有方法体 | -| main方法 | 抽象方法可以有main方法并且我们可以运行它 | jdk8以前接口没有main方法,不能运行;jdk8以后接口可以有default和static方法,可以运行main方法 | +| 与正常Java类的区别 | 除了不能实例化抽象类之外,和普通 Java 类没有任何区别 | 接口是完全不同的类型 | +| 访问修饰符 | 抽象方法有 **public**、**protected** 和 **default** 这些修饰符 | 接口方法默认修饰符是 **public**,别的修饰符需要有方法体 | +| main方法 | 抽象方法可以有 main 方法并且我们可以运行它 | jdk8 以前接口没有 main 方法,不能运行;jdk8 以后接口可以有 default 和 static 方法,可以运行 main 方法 | | 多继承 | 抽象方法可以继承一个类和实现多个接口 | 接口可以继承一个或多个其它接口,接口不可继承类 | | 速度 | 比接口速度要快 | 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法 | | 添加新方法 | 如果往抽象类中添加新的方法,可以给它提供默认的实现,因此不需要改变现在的代码 | 如果往接口中添加方法,那么必须改变实现该接口的类 | @@ -2777,7 +2779,7 @@ StringBuffer sb1 = new StringBuffer();//new byte[16] sb1.append('a'); //value[0] = 'a'; ``` -append 源码: +append 源码:扩容为二倍 ```java public AbstractStringBuilder append(String str) { @@ -5319,8 +5321,8 @@ HashMap继承关系如下图所示: 5. 时间复杂度 O(1) - * 若为树,则在树中通过key.equals(k)查找,**O(logn)** - * 若为链表,则在链表中通过key.equals(k)查找,**O(n)** + * 若为树,则在树中通过 key.equals(k) 查找,**O(logn)** + * 若为链表,则在链表中通过 key.equals(k) 查找,**O(n)** @@ -5402,6 +5404,7 @@ abstract class HashIterator { // 同步expectedModCount expectedModCount = modCount; } +} ``` @@ -16551,13 +16554,13 @@ public class Kmp { * 每一个节点可以是红或者黑 -+ 红黑树不是高度平衡的,它的平衡是通过"自己的红黑规则"进行实现的 ++ 红黑树不是高度平衡的,它的平衡是通过自己的红黑规则进行实现的 红黑树的红黑规则有哪些: 1. 每一个节点或是红色的,或者是黑色的 2. 根节点必须是黑色 -3. 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为 Nil,这些 Nil 视为叶节点,每个叶节点(Nil) 是黑色的 +3. 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为 Nil,这些 Nil 视为叶节点,每个叶节点 (Nil) 是黑色的 4. 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连的情况) 5. 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点 @@ -18680,9 +18683,7 @@ Java 中提供了一个动态代理类 Proxy,Proxy 并不是代理对象的类 `static Object newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h) ` -* 参数一:类加载器,负责加载代理类 - - 传入类加载器,代理和被代理对象要用一个类加载器才是父子关系,不同类加载器加载相同的类在 JVM 中都不是同一个类对象 +* 参数一:类加载器,负责加载代理类。传入类加载器,代理和被代理对象要用一个类加载器才是父子关系,不同类加载器加载相同的类在 JVM 中都不是同一个类对象 * 参数二:被代理业务对象的**全部实现的接口**,代理对象与真实对象实现相同接口,知道为哪些方法做代理 @@ -18912,7 +18913,7 @@ private static final class ProxyClassFactory { // 【生成二进制字节码,这个字节码写入到文件内】,就是编译好的 class 文件 byte[] proxyClassFile = ProxyGenerator.generateProxyClass( - proxyName, interfaces, accessFlags); +proxyName, interfaces, accessFlags); try { // 【使用加载器加载二进制到 jvm】,并且返回 class return defineClass0(loader, proxyName, diff --git a/Prog.md b/Prog.md index a8da42c..22e90ca 100644 --- a/Prog.md +++ b/Prog.md @@ -261,7 +261,7 @@ Java Virtual Machine Stacks(Java 虚拟机栈):每个线程启动后,虚 * 线程的 CPU 时间片用完 * 垃圾回收 * 有更高优先级的线程需要运行 -* 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法 +* 线程自己调用了 sleep、yield、wait、join、park 等方法 程序计数器(Program Counter Register):记住下一条 JVM 指令的执行地址,是线程私有的 @@ -4634,7 +4634,7 @@ public class LinkedBlockingQueue extends AbstractQueue } ``` -入队:尾插法 +入队:**尾插法** * 初始化链表 `last = head = new Node(null)`,**Dummy 节点用来占位**,item 为 null @@ -4660,7 +4660,7 @@ public class LinkedBlockingQueue extends AbstractQueue * 再来一个节点入队 `last = last.next = node` -出队:出队首节点,FIFO +出队:**出队头节点**,FIFO * 出队源码: @@ -5511,7 +5511,7 @@ public ThreadPoolExecutor(int corePoolSize, -参考视频:https://space.bilibili.com/457326371/ +图片来源:https://space.bilibili.com/457326371/ @@ -6810,7 +6810,7 @@ FutureTask 类的成员方法: } ``` -* FutureTask#cancel:任务取消 +* FutureTask#cancel:任务取消,打断正在执行该任务的线程 ```java public boolean cancel(boolean mayInterruptIfRunning) { @@ -6830,7 +6830,7 @@ FutureTask 类的成员方法: // 打断执行的线程 t.interrupt(); } finally { - // 设置任务状态为中断完成 + // 设置任务状态为【中断完成】 UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); } } @@ -7015,7 +7015,7 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { ##### 延迟任务 -ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口,具有延迟执行的特点,覆盖 FutureTask 的 run 方法来实现对**延时执行、周期执行**的支持。对于延时任务调用 FutureTask#run 而对于周期性任务则调用 FutureTask#runAndReset 并且在成功之后根据 fixed-delay/fixed-rate 模式来设置下次执行时间并重新将任务塞到工作队列。 +ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口,具有延迟执行的特点,覆盖 FutureTask 的 run 方法来实现对**延时执行、周期执行**的支持。对于延时任务调用 FutureTask#run,而对于周期性任务则调用 FutureTask#runAndReset 并且在成功之后根据 fixed-delay/fixed-rate 模式来设置下次执行时间并重新将任务塞到工作队列。 在调度线程池中无论是 runnable 还是 callable,无论是否需要延迟和定时,所有的任务都会被封装成 ScheduledFutureTask @@ -7160,7 +7160,7 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, public boolean cancel(boolean mayInterruptIfRunning) { // 调用父类 FutureTask#cancel 来取消任务 boolean cancelled = super.cancel(mayInterruptIfRunning); - // removeOnCancel 用于控制任务取消后是否应该从队列中移除 + // removeOnCancel 用于控制任务取消后是否应该从阻塞队列中移除 if (cancelled && removeOnCancel && heapIndex >= 0) // 从等待队列中删除该任务,并调用 tryTerminate() 判断是否需要停止线程池 remove(this); @@ -7197,7 +7197,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 ```java private final ReentrantLock lock = new ReentrantLock(); // 控制并发 - private final Condition available = lock.newCondition();// + private final Condition available = lock.newCondition();// 条件队列 ``` * 阻塞等待头节点的线程: @@ -7271,7 +7271,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 } ``` -* poll():非阻塞获取头结点,**获取执行时间最近的** +* poll():非阻塞获取头结点,**获取执行时间最近并且可以执行的** ```java // 非阻塞获取 @@ -7514,7 +7514,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 if (isShutdown()) reject(task); else { - // 把当前任务放入阻塞队列,因为需要【获取执行时间最近的】 + // 把当前任务放入阻塞队列,因为需要【获取执行时间最近的】,当前任务需要比较 super.getQueue().add(task); // 线程池状态为 SHUTDOWN 并且不允许执行任务了,就从队列删除该任务,并设置任务的状态为取消状态 if (isShutdown() && !canRunInCurrentRunState(task.isPeriodic()) && remove(task)) @@ -7681,7 +7681,7 @@ class AddTask extends RecursiveTask { ForkJoinPool 实现了**工作窃取算法**来提高 CPU 的利用率: -* 每个线程都维护了一个双端队列,用来存储需要执行的任务 +* 每个线程都维护了一个**双端队列**,用来存储需要执行的任务 * 工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行 * 窃取的必须是**最晚的任务**,避免和队列所属线程发生竞争,但是队列中只有一个任务时还是会发生竞争 diff --git a/SSM.md b/SSM.md index d1d5307..544426d 100644 --- a/SSM.md +++ b/SSM.md @@ -478,7 +478,7 @@ SqlSession:构建者对象接口,用于执行 SQL、管理事务、接口代 defaultExecutorType:配置默认的执行器 * SIMPLE 就是普通的执行器(默认) - * REUSE 执行器会重用预处理语句(PreparedStatement) + * REUSE 执行器会重用预处理语句 * BATCH 执行器不仅重用语句还会执行批量更新 * SqlSession **会话内批量**操作: @@ -2618,9 +2618,9 @@ public class MyFirstPlugin implements Interceptor{ 1. 导入 PageHelper 的 Maven 坐标 -2. 在 mybatis 核心配置文件中配置 PageHelper 插件 +2. 在 MyBatis 核心配置文件中配置 PageHelper 插件 - 注意:分页助手的插件配置在通用 mapper 之前 + 注意:分页助手的插件配置在通用 Mapper 之前 ```xml @@ -2633,8 +2633,9 @@ public class MyFirstPlugin implements Interceptor{ ``` 3. 与 MySQL 分页查询页数计算公式不同 - static Page startPage(int pageNum, int pageSize) : pageNum第几页,pageSize页面大小 - + + `static Page startPage(int pageNum, int pageSize)`:pageNum第几页,pageSize页面大小 + ```java @Test public void selectAll() { @@ -2646,7 +2647,7 @@ public class MyFirstPlugin implements Interceptor{ } } ``` - + **** @@ -6515,7 +6516,7 @@ Spring 在事务开始时,根据当前环境中设置的隔离级别,调整 TransactionDefinition 接口中定义了五个表示隔离级别的常量: -- TransactionDefinition.ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别,Oracle 默认采用的 READ_COMMITTED隔离级别. +- TransactionDefinition.ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ 隔离级别,Oracle 默认采用的 READ_COMMITTED隔离级别. - TransactionDefinition.ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 - TransactionDefinition.ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 - TransactionDefinition.ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。 @@ -7227,7 +7228,7 @@ public void addAccount{} public int update(){} ``` - +* 情况 7:注解在接口上,代理对象是 CGLIB diff --git a/Tool.md b/Tool.md index 93c4d62..9b5642f 100644 --- a/Tool.md +++ b/Tool.md @@ -1802,7 +1802,7 @@ uniq [OPTION]... [INPUT [OUTPUT]] #### tar -tar的主要功能是打包、压缩和解压文件。tar本身不具有压缩功能。他是调用压缩功能实现的 。 +tar 的主要功能是打包、压缩和解压文件,tar 本身不具有压缩功能,是调用压缩功能实现的。 命令:tar [必要参数] [选择参数] [文件] @@ -1814,11 +1814,11 @@ tar的主要功能是打包、压缩和解压文件。tar本身不具有压缩 * -t 列出tar文件中包含的文件的信息 * -r 附加新的文件到tar文件中 -`tar -cvf txt.tar txtfile.txt `:将 txtfile.txt文件打包(仅打包,不压缩) +`tar -cvf txt.tar txtfile.txt `:将 txtfile.txt 文件打包(仅打包,不压缩) -`tar -zcvf combine.tar.gz 1.txt 2.txt 3.txt`:将 123.txt文件打包压缩(打包**压缩**(gzip) +`tar -zcvf combine.tar.gz 1.txt 2.txt 3.txt`:将 123.txt 文件打包压缩(gzip) -`tar -ztvf txt.tar.gz`:查看tar中有哪些文件 +`tar -ztvf txt.tar.gz`:查看 tar 中有哪些文件 `tar -zxvf Filename -C 目标路径`:**解压** @@ -1892,9 +1892,9 @@ bzip2采用新的压缩演算法,压缩效果比传统的LZ77/LZ78压缩演算 bzip2 [-cdfhkLstvVz][--repetitive-best][--repetitive-fast][- 压缩等级][要压缩的文件] ``` -压缩: +压缩:bzip2 a.txt + -bzip2 a.txt #### bunzip2 @@ -2196,7 +2196,7 @@ ll ps 指令:查看某个时间点的进程信息 -top指令:实时显示进程信息 +top 指令:实时显示进程信息 pstree:查看进程树 @@ -2218,11 +2218,11 @@ pstree -A #查看所有进程树 * 进程号为 1 是 init 进程,是一个守护进程,在自举过程结束时由内核调用,init 进程绝不会终止,是一个普通的用户进程,但是它以超级用户特权运行 -父进程 ID 为 0 的进程通常是内核进程,它们作为系统自举过程的一部分而启动,init 进程是个例外,它的父进程是0,但它是用户进程 +父进程 ID 为 0 的进程通常是内核进程,它们作为系统自举过程的一部分而启动,init 进程是个例外,它的父进程是 0,但它是用户进程 主存 = RAM + BIOS 部分的 ROM -自举程序存储在内存中 ROM(BIOS 芯片),用来加载操作系统。CPU 的程序计数器指向 ROM 中自举程序第一条指令所对应的位置,当计算机通电,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入RAM中,这个过程是自举过程。装入完成后 CPU 的程序计数器就被设置为 RAM 中操作系统的第一条指令所对应的位置,接下来 CPU 将开始执行操作系统的指令 +自举程序存储在内存中 ROM(BIOS 芯片),用来加载操作系统。CPU 的程序计数器指向 ROM 中自举程序第一条指令,当计算机**通电**,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入 RAM 中,这个过程是自举过程。装入完成后 CPU 的程序计数器就被设置为 RAM 中操作系统的第一条指令所对应的位置,接下来 CPU 将开始执行操作系统的指令 存储在 ROM 中保留很小的自举装入程序,完整功能的自举程序保存在磁盘的启动块上,启动块位于磁盘的固定位,拥有启动分区的磁盘称为启动磁盘或系统磁盘(C盘) @@ -3336,7 +3336,7 @@ Docker 让开发者打包开发应用以及依赖包到一个轻量级、可移 * 容器性能开销极低。 -Docker架构: +Docker 架构: * **镜像(Image):**Docker 镜像,就相当于一个 root 文件系统。比如官方镜像 ubuntu:16.04 就包含了完整的一套 Ubuntu16.04 最小系统的 root 文件系统 diff --git a/Web.md b/Web.md index 631d9d6..bc52b2c 100644 --- a/Web.md +++ b/Web.md @@ -4,19 +4,19 @@ ### 概述 -HTML(超文本标记语言——HyperText Markup Language)是构成 Web 世界的一砖一瓦。它是一种用来告知浏览器如何组织页面的标记语言。 +HTML(超文本标记语言—HyperText Markup Language)是构成 Web 世界的一砖一瓦。它是一种用来告知浏览器如何组织页面的标记语言。 -* 超文本Hypertext,是指连接单个或者多个网站间的网页的链接。通过链接,就能访问互联网中的内容。 +* 超文本 Hypertext,是指连接单个或者多个网站间的网页的链接。通过链接,就能访问互联网中的内容 -* 标记Markup ,是用来注明文本,图片等内容,以便于在浏览器中显示,例如``,``等。 +* 标记 Markup ,是用来注明文本,图片等内容,以便于在浏览器中显示,例如 ``,`` 等 **网页的构成** -* [HTML](https://developer.mozilla.org/zh-CN/docs/Web/HTML):通常用来定义网页内容的含义和基本结构。 -* [CSS](https://developer.mozilla.org/zh-CN/docs/Web/CSS):通常用来描述网页的表现与展示效果。 -* [JavaScript](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript):通常用来执行网页的功能与行为。 +* [HTML](https://developer.mozilla.org/zh-CN/docs/Web/HTML):通常用来定义网页内容的含义和基本结构 +* [CSS](https://developer.mozilla.org/zh-CN/docs/Web/CSS):通常用来描述网页的表现与展示效果 +* [JavaScript](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript):通常用来执行网页的功能与行为 + -> 扩展资料:w3c是万维网联盟(World Wide Web Consortium,[W3C](https://www.w3school.com.cn/index.html)),又称W3C理事会。1994年10月在麻省理工学院计算机科学实验室成立。建立者是万维网的发明者蒂姆·伯纳斯-李,负责制定web相关标准的制定。 @@ -26,13 +26,13 @@ HTML(超文本标记语言——HyperText Markup Language)是构成 Web 世 ### 组成 -HTML页面由一系列的**元素([elements](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element))** 组成,而元素是使用**标签**创建的。 - #### 标签 -一**对**标签( [tags](https://developer.mozilla.org/en-US/docs/Glossary/Tag))可以设置一段文字样式,添加一张图片或者添加超链接等等 +HTML 页面由一系列的**元素(elements)** 组成,而元素是使用**标签**创建的 + +一对标签(tags)可以设置一段文字样式,添加一张图片或者添加超链接等等 -在HTML中,`

`标签表示**标题**,我们可以使用**开始标签**和**结束标签**包围文本内容,这样其中的内容就以标题的形式显示。 +在 HTML 中,`

` 标签表示**标题**,我们可以使用**开始标签**和**结束标签**包围文本内容,这样其中的内容就以标题的形式显示 ```html

开始学习JavaWeb

@@ -43,18 +43,18 @@ HTML页面由一系列的**元素([elements](https://developer.mozilla.org/zh- #### 属性 -HTML标签可以拥有[属性](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Attributes)。 +HTML 标签可以拥有属性 -* 属性是属于标签的,修饰标签,让标签有更多的效果。 -* 属性一般定义在起始标签里面。 +* 属性是属于标签的,修饰标签,让标签有更多的效果 +* 属性一般定义在起始标签里面 * 属性一般以**属性=属性值**的形式出现 -* 属性值一般用 `''` 或者`""` 括起来。 不加引号也是可以的(不建议使用)。比如:name='value' +* 属性值一般用 `''` 或者 `""` 括起来。 不加引号也是可以的(不建议使用)。比如:name='value' ```html

开始学习JavaWeb

``` -在HTML标签中,`align` 属性表示**水平对齐方式**,我们可以赋值为 `center` 表示 **居中** 。 +在 HTML 标签中,`align` 属性表示**水平对齐方式**,我们可以赋值为 `center` 表示 **居中** 。 @@ -68,12 +68,12 @@ HTML标签可以拥有[属性](https://developer.mozilla.org/zh-CN/docs/Web/HTML 文档结构介绍: -* 文档声明:用于声明当前HTML的版本,这里的``是HTML5的声明 -* html根标签:除文档声明以外,其它内容全部要放在根标签html内部 -* 文档头部配置:head标签,是当前页面的配置信息,外部引入文件, 例如网页标签、字符集等 - * ``:这个标签是页面的元数据信息,设置文档使用utf-8字符集编码 +* 文档声明:用于声明当前 HTML 的版本,这里的``是 HTML5 的声明 +* html 根标签:除文档声明以外,其它内容全部要放在根标签 html 内部 +* 文档头部配置:head 标签,是当前页面的配置信息,外部引入文件, 例如网页标签、字符集等 + * ``:这个标签是页面的元数据信息,设置文档使用 utf-8 字符集编码 * ``:这个标签定义文档标题,位置出现在浏览器标签。在收藏页面时,它可用来描述页面 -* 文档显示内容:body标签,里边的内容会显示到浏览器页面上 +* 文档显示内容:body 标签,里边的内容会显示到浏览器页面上 @@ -87,7 +87,7 @@ HTML标签可以拥有[属性](https://developer.mozilla.org/zh-CN/docs/Web/HTML ### 注释方式 -将一段HTML中的内容置为注释,你需要将其用特殊的记号<!---->包括起来 +将一段 HTML 中的内容置为注释,你需要将其用特殊的记号 <!----> 包括起来 ```html <p>我在注释外!</p> From 8d508481b25853dfcbaf93ea679e04b206cfbba9 Mon Sep 17 00:00:00 2001 From: Seazean <imseazean@gmail.com> Date: Thu, 9 Sep 2021 20:17:38 +0800 Subject: [PATCH 004/122] Update Java Notes --- Java.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Java.md b/Java.md index 14c3b26..635a007 100644 --- a/Java.md +++ b/Java.md @@ -325,7 +325,7 @@ System.out.println(x == y); // true,因为 y 会调用 intValue 自动拆箱 语法:`Scanner sc = new Scanner(System.in)` -* next():遇到了空格,就不再录入数据了,结束标记:空格、tab键 +* next():遇到了空格,就不再录入数据了,结束标记:空格、tab 键 * nextLine():可以将数据完整的接收过来,结束标记:回车换行符 一般使用 `sc.nextInt()` 或者 `sc.nextLine()` 接受整型和字符串,然后转成需要的数据类型 @@ -354,18 +354,18 @@ public static void main(String[] args) { * 有了基本数据类型,为什么还要引用数据类型? - > 1、引用数据类型封装了数据和处理该数据的方法,比如Integer.parseInt(String)就是将String字符类型数据转换为Integer整型数据 + > 1、引用数据类型封装了数据和处理该数据的方法,比如 Integer.parseInt(String) 就是将 String 字符类型数据转换为 Integer 整型数据 > - > 2、Java中大部分类和方法都是针对引用数据类型,包括泛型和集合 + > 2、Java 中大部分类和方法都是针对引用数据类型,包括泛型和集合 * 引用数据类型那么好,为什么还用基本数据类型? > 引用类型的对象要多储存对象头,对基本数据类型来说空间浪费率太高 - > 逻辑上来讲,java只有包装类就够了,为了运行速度,需要用到基本数据类型;优先考虑运行效率的问题,所以二者同时存在是合乎情理的。 + > 逻辑上来讲,java 只有包装类就够了,为了运行速度,需要用到基本数据类型;优先考虑运行效率的问题,所以二者同时存在是合乎情理的 -* Java集合不能存放基本数据类型,只存放对象的引用? +* Java 集合不能存放基本数据类型,只存放对象的引用? - > 不能放基本数据类型是因为不是Object的子类。泛型思想,如果不用泛型要写很多参数类型不同的但功能相同的函数(方法重载) + > 不能放基本数据类型是因为不是 Object 的子类。泛型思想,如果不用泛型要写很多参数类型不同的但功能相同的函数(方法重载) * == @@ -391,21 +391,21 @@ public static void main(String[] args) { 静态初始化: -* 数据类型[] 数组名 = new 数据类型[]{元素1,元素2,...}:`int[] arr = new int[]{11,22,33};` -* 数据类型[] 数组名 = {元素1,元素2,...}:`int[] arr = {44,55,66};` +* 数据类型[] 数组名 = new 数据类型[]{元素1,元素2,...}:`int[] arr = new int[]{11,22,33}` +* 数据类型[] 数组名 = {元素1,元素2,...}:`int[] arr = {44,55,66}` 动态初始化 -* 数据类型[] 数组名 = new 数据类型[数组长度]:`int[] arr = new int[3];` +* 数据类型[] 数组名 = new 数据类型[数组长度]:`int[] arr = new int[3]` #### 元素访问 -* **索引**:每一个存储到数组的元素,都会自动的拥有一个编号,从**0**开始。这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素。 +* **索引**:每一个存储到数组的元素,都会自动的拥有一个编号,从 **0** 开始。这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素。 -* **访问格式:**数组名[索引]:`arr[0]` -* **赋值:**`arr[0] = 10;` +* **访问格式**:数组名[索引] `arr[0]` +* **赋值:**`arr[0] = 10` From 1eeb46f85c0123f72d97d4fc2e6e91638104c950 Mon Sep 17 00:00:00 2001 From: Seazean <imseazean@gmail.com> Date: Sat, 11 Sep 2021 09:50:21 +0800 Subject: [PATCH 005/122] Update Java Notes --- Java.md | 49 +++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/Java.md b/Java.md index 635a007..05fc0fd 100644 --- a/Java.md +++ b/Java.md @@ -2445,21 +2445,24 @@ s = s + "cd"; //s = abccd 新对象 #### 常用方法 -`public boolean equals(String s)` : 比较两个字符串内容是否相同、区分大小写 -`public boolean equalsIgnoreCase(String anotherString)` : 比较字符串的内容,忽略大小写 -`public int length()` : 返回此字符串的长度 -`public String trim()` : 返回一个字符串,其值为此字符串,并删除任何前导和尾随空格 -`public String[] split(String regex)` : 将字符串按给定的正则表达式分割成字符串数组 -`public char charAt(int index)` : 取索引处的值 -`public char[] toCharArray()` : 将字符串拆分为字符数组后返回 -`public boolean startsWith(String prefix)` : 测试此字符串是否以指定的前缀开头 -`public int indexOf(String str)` : 返回指定子字符串第一次出现的字符串内的索引,没有返回-1 -`public int lastIndexOf(String str)` : 返回字符串最后一次出现str的索引,没有返回-1 -`public String substring(int beginIndex)` : 返回子字符串,以原字符串指定索引处到结尾 -`public String substring(int i, int j)` : 指定索引处扩展到 j - 1 的位置,字符串长度为 j - i -`public String toLowerCase()` : 将此 String 所有字符转换为小写,使用默认语言环境的规则 -`public String toUpperCase()` : 使用默认语言环境的规则将此 String 所有字符转换为大写 -`public String replace(CharSequence target, CharSequence replacement)` : 使用新值,将字符串中的旧值替换,得到新的字符串 +常用 API: + +* `public boolean equals(String s)` : 比较两个字符串内容是否相同、区分大小写 + +* `public boolean equalsIgnoreCase(String anotherString)` : 比较字符串的内容,忽略大小写 +* `public int length()` : 返回此字符串的长度 +* `public String trim()` : 返回一个字符串,其值为此字符串,并删除任何前导和尾随空格 +* `public String[] split(String regex)` : 将字符串按给定的正则表达式分割成字符串数组 +* `public char charAt(int index)` : 取索引处的值 +* `public char[] toCharArray()` : 将字符串拆分为字符数组后返回 +* `public boolean startsWith(String prefix)` : 测试此字符串是否以指定的前缀开头 +* `public int indexOf(String str)` : 返回指定子字符串第一次出现的字符串内的索引,没有返回 -1 +* `public int lastIndexOf(String str)` : 返回字符串最后一次出现str的索引,没有返回 -1 +* `public String substring(int beginIndex)` : 返回子字符串,以原字符串指定索引处到结尾 +* `public String substring(int i, int j)` : 指定索引处扩展到 j - 1 的位置,字符串长度为 j - i +* `public String toLowerCase()` : 将此 String 所有字符转换为小写,使用默认语言环境的规则 +* `public String toUpperCase()` : 使用默认语言环境的规则将此 String 所有字符转换为大写 +* `public String replace(CharSequence target, CharSequence replacement)` : 使用新值,将字符串中的旧值替换,得到新的字符串 ```java String s = 123-78; @@ -7070,12 +7073,9 @@ public class FileDemo { #### 遍历目录 -- `public String[] list()`: - 获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回。 -- `public File[] listFiles()(常用)`: - 获取当前目录下所有的"一级文件对象"到一个**文件对象数组**中去返回(**重点**) -- `public long lastModified` : - 返回此抽象路径名表示的文件上次修改的时间。 +- `public String[] list()`:获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回。 +- `public File[] listFiles()(常用)`:获取当前目录下所有的"一级文件对象"到一个**文件对象数组**中去返回(**重点**) +- `public long lastModified`:返回此抽象路径名表示的文件上次修改的时间。 ```java public class FileDemo { @@ -7109,9 +7109,10 @@ public class FileDemo { #### 文件搜索 -递归实现文件搜索(非规律递归) - (1)定义一个方法用于做搜索。 - (2)进入方法中进行业务搜索分析。 +递归实现文件搜索(非规律递归) + +* 定义一个方法用于做搜索 +* 进入方法中进行业务搜索分析 ```java /** From aee7bd9d40e5e04e98672ac14a45113d958b2bfc Mon Sep 17 00:00:00 2001 From: Seazean <imseazean@gmail.com> Date: Mon, 13 Sep 2021 17:45:35 +0800 Subject: [PATCH 006/122] Update Java Notes --- DB.md | 24 +++-- Java.md | 182 ++++++++++++++++++++----------------- Prog.md | 46 +++++----- SSM.md | 275 ++++++++++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 369 insertions(+), 158 deletions(-) diff --git a/DB.md b/DB.md index c8a31f5..50c2150 100644 --- a/DB.md +++ b/DB.md @@ -26,11 +26,9 @@ - 可以指定列名、数据类型、约束等 - 一个表中可以存储多条数据 -- 数据 +- 数据:想要永久化存储的数据 - - 想要永久化存储的数据 - - ![](https://gitee.com/seazean/images/raw/master/DB/数据库、数据表、数据之间的关系.png) + <img src="https://gitee.com/seazean/images/raw/master/DB/数据库、数据表、数据之间的关系.png" style="zoom:50%;" /> @@ -381,7 +379,7 @@ mysqlshow -uroot -p1234 test book --count 池化技术:对于访问数据库来说,建立连接的代价是比较昂贵的,频繁的创建关闭连接比较耗费资源,有必要建立数据库连接池,以提高访问的性能 -首先连接到数据库上,这时连接器发挥作用,连接完成后如果没有后续的动作,这个连接就处于空闲状态,通过指令查看连接状态: +首先连接到数据库上,这时连接器发挥作用,连接完成后如果没有后续的动作,这个连接就处于空闲状态,通过指令查看连接状态 SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 SQL 的执行情况,其中的 Command 列显示为 Sleep 的这一行,就表示现在系统里面有一个空闲连接 @@ -939,12 +937,12 @@ DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临 | TIMESTAMP | 时间戳类型,包含年月日时分秒:yyyy-MM-dd HH:mm:ss<br />如果不给这个字段赋值或赋值为null,则默认使用当前的系统时间 | | VARCHAR | 字符串<br />name varchar(20):姓名最大20个字符:zhangsan8个字符,张三2个字符 | - `INT(n)`:n代表位数 + `INT(n)`:n 代表位数 - * 3:int(9)显示结果为000000010 - * 3:int(3)显示结果为010 + * 3:int(9)显示结果为 000000010 + * 3:int(3)显示结果为 010 - `varchar(n)`:n表示的是字符数 + `varchar(n)`:n 表示的是字符数 - 例如: @@ -1030,19 +1028,19 @@ DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临 * 新增表数据 - * 新增格式1:给指定列添加数据 + * 新增格式 1:给指定列添加数据 ```mysql INSERT INTO 表名(列名1,列名2...) VALUES (值1,值2...); ``` - * 新增格式2:默认给全部列添加数据 + * 新增格式 2:默认给全部列添加数据 ```mysql INSERT INTO 表名 VALUES (值1,值2,值3,...); ``` - * 新增格式3:批量添加数据 + * 新增格式 3:批量添加数据 ```mysql -- 给指定列批量添加数据 @@ -2399,7 +2397,7 @@ CREATE TABLE us_pro( 工作原理: - * 自动提交模式下,如果没有 start transaction 显式地开始一个事务,那么每个 SQL 语句都会被当做一个事务执行提交操作 + * 自动提交模式下,如果没有 start transaction 显式地开始一个事务,那么**每个 SQL 语句都会被当做一个事务执行提交操作** * 手动提交模式下,所有的 SQL 语句都在一个事务中,直到执行了 commit 或 rollback,该事务结束的同时开启另外一个事务 * 存在一些特殊的命令,在事务中执行了这些命令会马上强制执行 COMMIT 提交事务,如 DDL 语句 (create/drop/alter/table)、lock tables 语句等 diff --git a/Java.md b/Java.md index 05fc0fd..05c8fd7 100644 --- a/Java.md +++ b/Java.md @@ -4494,8 +4494,8 @@ PriorityQueue 是优先级队列,底层存储结构为 Object[],默认实现 常用 API: -* `public boolean offer(E e)`:将指定的元素插入到此优先级队列中尾部 -* `public E poll() `:检索并删除此队列的头元素,如果此队列为空,则返回 null +* `public boolean offer(E e)`:将指定的元素插入到此优先级队列中**尾部** +* `public E poll() `:检索并删除此队列的**头元素**,如果此队列为空,则返回 null * `public E peek()`:检索但不删除此队列的头,如果此队列为空,则返回 null * `public boolean remove(Object o)`:从该队列中删除指定元素(如果存在),删除元素 e 使用 o.equals(e) 比较,如果队列包含多个这样的元素,删除第一个 @@ -5858,7 +5858,7 @@ public class GenericDemo { Integer[] num = {10 , 20 , 30 , 40 , 50}; String s1 = arrToString(nums); - String[] name = {"贾乃亮","王宝绿","陈羽凡"}; + String[] name = {"张三","李四","王五"}; String s2 = arrToString(names); } @@ -5873,6 +5873,7 @@ public class GenericDemo { 自定义泛型接口 泛型接口:使用了泛型定义的接口就是泛型接口。 + 泛型接口的格式: ```java @@ -8614,16 +8615,16 @@ public class MyBook { #### 普通属性 -注解可以有属性,**属性名必须带()**,在用注解的时候,属性必须赋值,除非属性有默认值 +注解可以有属性,**属性名必须带 ()**,在用注解的时候,属性必须赋值,除非属性有默认值 属性的格式: -* 格式1:数据类型 属性名(); -* 格式2:数据类型 属性名() default 默认值; +* 格式 1:数据类型 属性名() +* 格式 2:数据类型 属性名() default 默认值 属性适用的数据类型: -* 八种数据数据类型(int,short,long,double,byte,char,boolean,float) 和 String、Class +* 八种数据数据类型(int,short,long,double,byte,char,boolean,float)和 String、Class * 以上类型的数组形式都支持 ```java @@ -8744,7 +8745,7 @@ Class 类 API : * `boolean isAnnotationPresent(Class<Annotation> class)`:判断对象是否使用了指定的注解 * `boolean isAnnotation()`:此 Class 对象是否表示注释类型 -注解原理:注解本质是一个继承了 `Annotation` 的特殊接口,其具体实现类是 Java 运行时生成的**动态代理类**,通过反射获取注解时,返回的是 Java 运行时生成的动态代理对象 `$Proxy1`,通过代理对象调用自定义注解(接口)的方法,会最终调用 `AnnotationInvocationHandler` 的 `invoke` 方法,该方法会从 `memberValues` 这个Map 中找出对应的值,而 `memberValues` 的来源是 Java 常量池 +注解原理:注解本质是一个继承了 `Annotation` 的特殊接口,其具体实现类是 Java 运行时生成的**动态代理类**,通过反射获取注解时,返回的是运行时生成的动态代理对象 `$Proxy1`,通过代理对象调用自定义注解(接口)的方法,回调 `AnnotationInvocationHandler` 的 `invoke` 方法,该方法会从 `memberValues` 这个Map 中找出对应的值,而 `memberValues` 的来源是 Java 常量池 解析注解数据的原理:注解在哪个成分上,就先拿哪个成分对象,比如注解作用在类上,则要该类的Class对象,再来拿上面的注解 @@ -8849,12 +8850,12 @@ XML介绍: - XML 被设计为具有自我描述性,易于阅读 - XML 是 W3C 的推荐标准 -**xml与html的区别**: +**XML 与 HTML 的区别**: -​ XML 不是 HTML 的替代,XML 和 HTML 为不同的目的而设计。 -​ XML 被设计为传输和存储数据,其焦点是数据的内容;XMl标签可自定义,便于阅读。 -​ HTML 被设计用来显示数据,其焦点是数据的外观;HTML标签被预设好,便于浏览器识别。 -​ HTML 旨在显示信息,而 XML 旨在传输信息。 +* XML 不是 HTML 的替代,XML 和 HTML 为不同的目的而设计 +* XML 被设计为传输和存储数据,其焦点是数据的内容;XMl标签可自定义,便于阅读 +* HTML 被设计用来显示数据,其焦点是数据的外观;HTML标签被预设好,便于浏览器识别 +* HTML 旨在显示信息,而 XML 旨在传输信息 @@ -8883,41 +8884,40 @@ person.xml ### 组成 -XML文件中常见的组成元素有:文档声明、元素、属性、注释、转义字符、字符区。文件后缀名为xml +XML 文件中常见的组成元素有:文档声明、元素、属性、注释、转义字符、字符区。文件后缀名为 xml * **文档声明** - ``<?xml version="1.0" encoding="utf-8" standalone="yes" ?>`` - 文档声明必须在第一行,以<?xml开头,以?>结束, - version:指定XML文档版本。必须属性,这里一般选择1.0; - enconding:指定当前文档的编码,可选属性,默认值是utf-8; - standalone: 该属性不是必须的,描述XML文件是否依赖其他的xml文件,取值为yes/no + `<?xml version="1.0" encoding="utf-8" standalone="yes" ?>`,文档声明必须在第一行,以 `<?xml` 开头,以 `?>` 结束, + + * version:指定 XML 文档版本。必须属性,这里一般选择 1.0 + * enconding:指定当前文档的编码,可选属性,默认值是 utf-8 + * standalone:该属性不是必须的,描述 XML 文件是否依赖其他的 xml 文件,取值为 yes/no * **元素** - * 格式1:`<person></person> ` - 格式2:`<person/>` - 普通元素的结构由开始标签、元素体、结束标签组成; - 标签由一对尖括号和合法标识符组成,标签必须成对出现。特殊的标签可以不成对,必须有结束标记</>; + * 格式 1:`<person></person> ` + * 格式 2:`<person/>` + * 普通元素的结构由开始标签、元素体、结束标签组成 + * 标签由一对尖括号和合法标识符组成,标签必须成对出现。特殊的标签可以不成对,必须有结束标记 </> * 元素体:可以是元素,也可以是文本,例如:``<person><name>张三</name></person>`` * 空元素:空元素只有标签,而没有结束标签,但**元素必须自己闭合**,例如:``<sex/>`` - * 元素命名:区分大小写、不能使用空格冒号、不建议用XML xml Xml等开头 + * 元素命名:区分大小写、不能使用空格冒号、不建议用 XML、xml、Xml 等开头 * 必须存在一个根标签,有且只能有一个 -* **属性** - `<name id="1" desc="高富帅">` - 属性是元素的一部分,它必须出现在元素的开始标签中 - 属性的定义格式:`属性名=“属性值”`,其中属性值必须使用单引或双引号括起来 - 一个元素可以有0~N个属性,但一个元素中不能出现同名属性 - 属性名不能使用空格 , 不要使用冒号等特殊字符,且必须以字母开头 - -* **注释** - <!--注释内容--> - XML的注释与HTML相同,既以``<!--``开始,``-->``结束。 +* **属性**:`<name id="1" desc="高富帅">` + + * 属性是元素的一部分,它必须出现在元素的开始标签中 + * 属性的定义格式:`属性名=“属性值”`,其中属性值必须使用单引或双引号括起来 + * 一个元素可以有 0~N 个属性,但一个元素中不能出现同名属性 + * 属性名不能使用空格 , 不要使用冒号等特殊字符,且必须以字母开头 +* **注释**:<!--注释内容--> + XML的注释与HTML相同,既以 `<!--` 开始,`-->` 结束。 + * **转义字符** - XML中的转义字符与HTML一样。因为很多符号已经被文档结构所使用,所以在元素体或属性值中想使用这些符号就必须使用转义字符(也叫实体字符),例如:">"、"<"、"'"、"""、"&"。 - XML 中仅有字符 "<"和"&" 是非法的。省略号、引号和大于号是合法的,把它们替换为实体引用 + XML 中的转义字符与 HTML 一样。因为很多符号已经被文档结构所使用,所以在元素体或属性值中想使用这些符号就必须使用转义字符(也叫实体字符),例如:">"、"<"、"'"、"""、"&" + XML 中仅有字符 < 和 & 是非法的。省略号、引号和大于号是合法的,把它们替换为实体引用 | 字符 | 预定义的转义字符 | 说明 | | :--: | :--------------: | :----: | @@ -8939,9 +8939,9 @@ XML文件中常见的组成元素有:文档声明、元素、属性、注释、 * CDATA 部分由 "<![CDATA[" 开始,由 "]]>" 结束; * 大量的转义字符在xml文档中时,会使XML文档的可读性大幅度降低。这时使用CDATA段就会好一些 - * 规则 - CDATA 部分不能包含字符串 "]]>"。也不允许嵌套的 CDATA 部分。 - 标记 CDATA 部分结尾的 "]]>" 不能包含空格或折行。 + * 规则: + * CDATA 部分不能包含字符串 ]]>,也不允许嵌套的 CDATA 部分 + * 标记 CDATA 部分结尾的 ]]> 不能包含空格或折行 ```xml <?xml version="1.0" encoding="UTF-8" ?> @@ -9079,23 +9079,25 @@ DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XM +**** + ##### DTD引入 -* 引入本地dtd +* 引入本地 dtd ```dtd <!DOCTYPE 根元素名称 SYSTEM ‘DTD文件的路径'> ``` -* 在xml文件内部引入 +* 在 xml 文件内部引入 ```dtd <!DOCTYPE 根元素名称 [ dtd文件内容 ]> ``` -* 引入网络dtd +* 引入网络 dtd ```dtd <!DOCTYPE 根元素的名称 PUBLIC "DTD文件名称" "DTD文档的URL"> @@ -9153,9 +9155,13 @@ DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XM +*** + + + ##### DTD实现 -persondtd.dtd文件 +persondtd.dtd 文件 ```dtd <!ELEMENT persons (person+)> <!--约束人们至少一个人--> @@ -9192,29 +9198,33 @@ persondtd.dtd文件 ##### XSD定义 -1.Schema 语言也可作为 XSD(XML Schema Definition) -2.schema 约束文件本身也是一个 xml 文件,符合 xml 的语法,这个文件的后缀名.xsd -3.一个 xml 中可以引用多个 schema 约束文件,多个 schema 使用名称空间区分(名称空间类似于java包名) -4.dtd 里面元素类型的取值比较单一常见的是 PCDATA 类型,但是在 schema 里面可以支持很多个数据类型 -**5.Schema 文件约束 xml 文件的同时也被别的文件约束着** +1. Schema 语言也可作为 XSD(XML Schema Definition) +2. Schema 约束文件本身也是一个 xml 文件,符合 xml 的语法,这个文件的后缀名 .xsd +3. 一个 xml 中可以引用多个 Schema 约束文件,多个 Schema 使用名称空间区分(名称空间类似于 Java 包名) +4. dtd 里面元素类型的取值比较单一常见的是 PCDATA 类型,但是在 Schema 里面可以支持很多个数据类型 +5. **Schema 文件约束 xml 文件的同时也被别的文件约束着** + + + +*** ##### XSD规则 -1、创建一个文件,这个文件的后缀名为.xsd。 -2、定义文档声明 -3、schema文件的根标签为: <schema> -4、在<schema>中定义属性: - xmlns=http://www.w3.org/2001/XMLSchema - 代表当前文件时约束别人的,同时这个文件也对该Schema进行约束 -5、在<schema>中定义属性 : - targetNamespace = 唯一的url地址,指定当前这个schema文件的名称空间。 - **名称空间**:当其他xml使用该schema文件,需要引入此空间 -6、在<schema>中定义属性 : - elementFormDefault="qualified“,表示当前schema文件是一个质量良好的文件。 -7、通过element定义元素 -8、**判断当前元素是简单元素还是复杂元素** +1. 创建一个文件,这个文件的后缀名为 .xsd +2. 定义文档声明 +3. schema 文件的根标签为: <schema> +4. 在 <schema> 中定义属性: + * xmlns=http://www.w3.org/2001/XMLSchema + * 代表当前文件时约束别人的,同时这个文件也对该 Schema 进行约束 +5. 在<schema>中定义属性 : + * targetNamespace = 唯一的 url 地址,指定当前这个 schema 文件的名称空间。 + * **名称空间**:当其他 xml 使用该 schema 文件,需要引入此空间 +6. 在<schema>中定义属性 : + * elementFormDefault="qualified“,表示当前 schema 文件是一个质量良好的文件。 +7. 通过 element 定义元素 +8. **判断当前元素是简单元素还是复杂元素** person.xsd @@ -9247,14 +9257,16 @@ person.xsd +**** + + + ##### XSD引入 -1、在根标签上定义属性xmlns="http://www.w3.org/2001/XMLSchema-instance" -2、**通过xmlns引入约束文件的名称空间** -3、给某一个xmlns属性添加一个标识,用于区分不同的名称空间 - 格式为: xmlns:标识=“名称空间url” ,标识可以是任意的,但是一般取值都是xsi -4、通过xsi:schemaLocation指定名称空间所对应的约束文件路径 - 格式为: xsi:schemaLocation = "名称空间url 文件路径“ +1. 在根标签上定义属性 xmlns="http://www.w3.org/2001/XMLSchema-instance" +2. **通过 xmlns 引入约束文件的名称空间** +3. 给某一个 xmlns 属性添加一个标识,用于区分不同的名称空间,格式为 `xmlns:标识="名称空间url"` ,标识可以是任意的,但是一般取值都是 xsi +4. 通过 xsi:schemaLocation 指定名称空间所对应的约束文件路径,格式为 `xsi:schemaLocation = "名称空间url 文件路径` ```scheme <?xml version="1.0" encoding="UTF-8" ?> @@ -9274,6 +9286,10 @@ person.xsd +**** + + + ##### XSD属性 ```scheme @@ -9333,21 +9349,21 @@ person.xsd #### 解析 -xml 解析就是从 xml 中获取到数据,DOM 是解析思想 +XML 解析就是从 XML 中获取到数据,DOM 是解析思想 -DOM(Document Object Model):文档对象模型,把文档的各个组成部分看做成对应的对象,把 xml 文件全部加载到内存,在内存中形成一个树形结构,再获取对应的值 +DOM(Document Object Model):文档对象模型,把文档的各个组成部分看做成对应的对象,把 XML 文件全部加载到内存,在内存中形成一个树形结构,再获取对应的值 -dom4j 实现 -* dom4j 解析器构造方法:`SAXReader saxReader = new SAXReader();` +Dom4J 实现: +* Dom4J 解析器构造方法:`SAXReader saxReader = new SAXReader()` * SAXReader 常用API: - `public Document read(File file)` : Reads a Document from the given File - `public Document read(InputStream in)` : Reads a Document from the given stream using SAX + * `public Document read(File file)`:Reads a Document from the given File + * `public Document read(InputStream in)`:Reads a Document from the given stream using SAX * Java Class 类API: - `public InputStream getResourceAsStream(String path)`:加载文件成为一个字节输入流返回 + * `public InputStream getResourceAsStream(String path)`:加载文件成为一个字节输入流返回 @@ -9357,7 +9373,7 @@ dom4j 实现 #### 根元素 -Document 方法:Element getRootElement() 获取根元素。 +Document 方法:`Element getRootElement()` 获取根元素 ```java // 需求:解析books.xml文件成为一个Document文档树对象,得到根元素对象。 @@ -9452,14 +9468,14 @@ public class Dom4JDemo { Element 元素的 API: -* List<Attribute> attributes():获取元素的全部属性对象。 -* Attribute attribute(String name):根据名称获取某个元素的属性对象。 -* String attributeValue(String var):直接获取某个元素的某个属性名称的值。 +* List<Attribute> attributes():获取元素的全部属性对象 +* Attribute attribute(String name):根据名称获取某个元素的属性对象 +* String attributeValue(String var):直接获取某个元素的某个属性名称的值 -Attribute 对象的API: +Attribute 对象的 API: -* String getName():获取属性名称。 -* String getValue():获取属性值。 +* String getName():获取属性名称 +* String getValue():获取属性值 ```java public class Dom4JDemo { @@ -9499,8 +9515,8 @@ Element: * String elementText(String name):可以直接获取当前元素的子元素的文本内容 * String elementTextTrim(String name):去前后空格,直接获取当前元素的子元素的文本内容 -* String getText():直接获取当前元素的文本内容。 -* String getTextTrim():去前后空格,直接获取当前元素的文本内容。 +* String getText():直接获取当前元素的文本内容 +* String getTextTrim():去前后空格,直接获取当前元素的文本内容 ```java public class Dom4JDemo { diff --git a/Prog.md b/Prog.md index 22e90ca..c6408c2 100644 --- a/Prog.md +++ b/Prog.md @@ -13683,14 +13683,14 @@ UDP 协议的使用场景:在线视频、网络语音、电话 #### 实现UDP -UDP 协议相关的两个类 +UDP 协议相关的两个类: * DatagramPacket(数据包对象):用来封装要发送或要接收的数据,比如:集装箱 * DatagramSocket(发送对象):用来发送或接收数据包,比如:码头 **DatagramPacket**: -* DatagramPacket 类 +* DatagramPacket 类: `public new DatagramPacket(byte[] buf, int length, InetAddress address, int port)`:创建发送端数据包对象 @@ -13704,7 +13704,7 @@ UDP 协议相关的两个类 * buf:用来存储接收到内容 * length:能够接收内容的长度 -* DatagramPacket 类常用方法 +* DatagramPacket 类常用方法: * `public int getLength()`:获得实际接收到的字节个数 * `public byte[] getData()`:返回数据缓冲区 @@ -13714,7 +13714,7 @@ UDP 协议相关的两个类 * DatagramSocket 类构造方法: * `protected DatagramSocket()`:创建发送端的 Socket 对象,系统会随机分配一个端口号 * `protected DatagramSocket(int port)`:创建接收端的 Socket 对象并指定端口号 -* DatagramSocket 类成员方法 +* DatagramSocket 类成员方法: * `public void send(DatagramPacket dp)`:发送数据包 * `public void receive(DatagramPacket p)`:接收数据包 * `public void close()`:关闭数据报套接字 @@ -13725,8 +13725,7 @@ public class UDPClientDemo { System.out.println("===启动客户端==="); // 1.创建一个集装箱对象,用于封装需要发送的数据包! byte[] buffer = "我学Java".getBytes(); - DatagramPacket packet = new DatagramPacket(buffer,bubffer.length - ,InetAddress.getLoclHost,8000); + DatagramPacket packet = new DatagramPacket(buffer,bubffer.length,InetAddress.getLoclHost,8000); // 2.创建一个码头对象 DatagramSocket socket = new DatagramSocket(); // 3.开始发送数据包对象 @@ -13747,7 +13746,7 @@ public class UDPServerDemo{ // 4.从集装箱中获取本次读取的数据量 int len = packet.getLength(); // 5.输出数据 - //String rs = new String(socket.getData(), 0, len) + // String rs = new String(socket.getData(), 0, len) String rs = new String(buffer , 0 , len); System.out.println(rs); // 6.服务端还可以获取发来信息的客户端的IP和端口。 @@ -13828,7 +13827,7 @@ TCP 协议相关的类: * Socket:一个该类的对象就代表一个客户端程序。 * ServerSocket:一个该类的对象就代表一个服务器端程序。 -Socket 类 +Socket 类: * 构造方法: @@ -13899,7 +13898,7 @@ ServerSocket 类: public class ClientDemo { public static void main(String[] args) throws Exception { // 1.客户端要请求于服务端的socket管道连接。 - Socket socket = new Socket("127.0.0.1",8080); + Socket socket = new Socket("127.0.0.1", 8080); // 2.从socket通信管道中得到一个字节输出流 OutputStream os = new socket.getOutputStream(); // 3.把低级的字节输出流包装成高级的打印流。 @@ -13932,7 +13931,7 @@ public class ServerDemo{ -需求二:客户2端可以反复发送数据,服务端可以反复数据2 +需求二:客户端可以反复发送数据,服务端可以反复数据 ```java public class ClientDemo { @@ -13974,7 +13973,7 @@ public class ServerDemo{ -需求三:实现一个服务端可以同时接收多个客户端的消息。 +需求三:实现一个服务端可以同时接收多个客户端的消息 ```java public class ClientDemo { @@ -14029,10 +14028,11 @@ class ServerReaderThread extends Thread{ ##### 伪异步 -一个客户端要一个线程,这种模型是不行的,并发越高系统瘫痪的越快,可以在服务端引入线程池,使用线程池来处理与客户端的消息通信 +一个客户端要一个线程,并发越高系统瘫痪的越快,可以在服务端引入线程池,使用线程池来处理与客户端的消息通信 + +* 优势:不会引起系统的死机,可以控制并发线程的数量 -优势:不会引起系统的死机,可以控制并发线程的数量 -劣势:同时可以并发的线程将受到限制 +* 劣势:同时可以并发的线程将受到限制 ```java public class BIOServer { @@ -14953,15 +14953,15 @@ public class ChannelTest { **Selector API**: -| 方法 | 说明 | -| ------------------------------------------------ | ------------------------------------- | -| public static Selector open() | 打开选择器 | -| public abstract void close() | 关闭此选择器 | -| public abstract int select() | 阻塞选择一组通道准备好进行I/O操作的键 | -| public abstract int select(long timeout) | 阻塞等待 timeout 毫秒 | -| public abstract int selectNow() | 获取一下,不阻塞,立刻返回 | -| public abstract Selector wakeup() | 唤醒正在阻塞的 selector | -| public abstract Set<SelectionKey> selectedKeys() | 返回此选择器的选择键集 | +| 方法 | 说明 | +| ------------------------------------------------ | ------------------------------------------- | +| public static Selector open() | 打开选择器 | +| public abstract void close() | 关闭此选择器 | +| public abstract int select() | **阻塞**选择一组通道准备好进行 I/O 操作的键 | +| public abstract int select(long timeout) | **阻塞**等待 timeout 毫秒 | +| public abstract int selectNow() | 获取一下,**不阻塞**,立刻返回 | +| public abstract Selector wakeup() | 唤醒正在阻塞的 selector | +| public abstract Set<SelectionKey> selectedKeys() | 返回此选择器的选择键集 | SelectionKey API: diff --git a/SSM.md b/SSM.md index 544426d..3e40868 100644 --- a/SSM.md +++ b/SSM.md @@ -49,6 +49,8 @@ SqlSession:构建者对象接口,用于执行 SQL、管理事务、接口代 注:**update 数据需要提交事务,或开启默认提交** +SqlSession 常用 API: + | 方法 | 说明 | | ----------------------------------------------------- | ------------------------------ | | List<E> selectList(String statement,Object parameter) | 执行查询语句,返回List集合 | @@ -477,9 +479,9 @@ SqlSession:构建者对象接口,用于执行 SQL、管理事务、接口代 defaultExecutorType:配置默认的执行器 - * SIMPLE 就是普通的执行器(默认) - * REUSE 执行器会重用预处理语句 - * BATCH 执行器不仅重用语句还会执行批量更新 + * SIMPLE 就是普通的执行器(默认,每次执行都要重新设置参数) + * REUSE 执行器会重用预处理语句(只预设置一次参数,多次执行) + * BATCH 执行器不仅重用语句还会执行批量更新(只针对**修改操作**) * SqlSession **会话内批量**操作: @@ -626,12 +628,13 @@ Mapper 接口开发需要遵循以下规范: ### 相关标签 -<resultType>:返回的是一个集合,要写集合中元素的类型 +<resultType>:返回结果映射对象类型,和对应方法的返回值类型保持一致,但是如果返回值是 List 则和其泛型保持一致 -<resultMap>:返回一条记录的 Map,key 是列名,value 是对应的值,用来配置字段和对象属性的映射关系标签,结果映射(和 resultType 二选一) +<resultMap>:返回一条记录的 Map,key 是列名,value 是对应的值,用来配置**字段和对象属性**的映射关系标签,结果映射(和 resultType 二选一) * id 属性:唯一标识 * type 属性:实体对象类型 +* autoMapping 属性:结果自动映射 <resultMap>内的核心配置文件标签: @@ -641,9 +644,10 @@ Mapper 接口开发需要遵循以下规范: * property 属性: 实体对象变量名称 * <association>:配置被包含对象的映射关系标签,嵌套封装结果集(多对一、一对一) - * property 属性:被包含对象的变量名,要进行映射的属性名(Java 中的 Bean 类) - * javaType 属性:被包含对象的数据类型,要进行映射的属性的类型 - + * property 属性:被包含对象的**变量名,**要进行映射的属性名 + * javaType 属性:被包含对象的**数据类型**,要进行映射的属性的类型(Java 中的 Bean 类) + * select 属性:加载复杂类型属性的映射语句的 ID,会从 column 属性指定的列中检索数据,作为参数传递给目标 select 语句 + * <collection>:配置被包含集合对象的映射关系标签,嵌套封装结果集(一对多、多对多) * property 属性:被包含集合对象的变量名 * ofType 属性:集合中保存的对象数据类型 @@ -654,11 +658,59 @@ Mapper 接口开发需要遵循以下规范: +*** + + + +### 嵌套查询 + +子查询: + +```java +public class Blog { + private int id; + private String msg; + private Author author; + // set + get +} +``` + +```xml +<resultMap id="blogResult" type="Blog" autoMapping = "true"> + <association property="author" column="author_id" javaType="Author" select="selectAuthor"/> +</resultMap> + +<select id="selectBlog" resultMap="blogResult"> + SELECT * FROM BLOG WHERE ID = #{id} +</select> + +<select id="selectAuthor" resultType="Author"> + SELECT * FROM AUTHOR WHERE ID = #{id} +</select> +``` + +循环引用:通过缓存解决 + +```xml +<resultMap id="blogResult" type="Blog" autoMapping = "true"> + <id column="id" property="id"/> + <collection property="comment" ofType="Comment"> + <association property="blog" javaType="Blog" resultMap="blogResult"/><!--y--> + </collection> +</resultMap +``` + + + + + **** -### 一对一 +### 多表查询 + +#### 一对一 一对一实现: @@ -712,7 +764,6 @@ Mapper 接口开发需要遵循以下规范: <resultMap id="oneToOne" type="card"> <!--column 表中字段名称,property 实体对象变量名称--> <id column="cid" property="id" /> - <!--column 表中字段名称,property 实体对象变量名称--> <result column="number" property="number" /> <!-- association:配置被包含对象的映射关系 @@ -731,7 +782,7 @@ Mapper 接口开发需要遵循以下规范: </select> </mapper> ``` - + * 核心配置文件 MyBatisConfig.xml ```xml @@ -784,7 +835,7 @@ Mapper 接口开发需要遵循以下规范: -### 一对多 +#### 一对多 一对多实现: @@ -871,7 +922,7 @@ Mapper 接口开发需要遵循以下规范: -### 多对多 +#### 多对多 学生课程例子,中间表不需要 bean 实体类 @@ -1049,13 +1100,10 @@ Mapper 接口开发需要遵循以下规范: ```java public interface PersonMapper { - /** - * 为了演示分步查询的一对多另写的一个方法 - */ User findPersonByid(int id); } ``` - + * 测试文件 ```java @@ -1066,12 +1114,12 @@ Mapper 接口开发需要遵循以下规范: SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(is); SqlSession sqlSession = ssf.openSession(true); OneToOneMapper mapper = sqlSession.getMapper(OneToOneMapper.class); - //调用实现类的方法,接收结果 + // 调用实现类的方法,接收结果 List<Card> list = mapper.selectAll(); - //不能遍历,遍历就是相当于使用了该数据,需要加载,不遍历就是没有使用。 + // 不能遍历,遍历就是相当于使用了该数据,需要加载,不遍历就是没有使用。 - //释放资源 + // 释放资源 sqlSession.close(); is.close(); } @@ -1095,7 +1143,7 @@ Mapper 接口开发需要遵循以下规范: 一对多映射: * column 是用于指定使用哪个字段的值作为条件查询 - * select 是用于指定查询账户的唯一标识(账户的dao全限定类名加上方法名称) + * select 是用于指定查询账户的唯一标识(账户的 dao 全限定类名加上方法名称) ```xml <mapper namespace="OneToManyMapper"> @@ -1478,8 +1526,8 @@ Mapper 接口开发需要遵循以下规范: * SqlSession 不同 * SqlSession 相同,查询条件不同时(还未缓存该数据) -* SqlSession 相同,手动清除了一级缓存,调用 `openSession.clearCache()` -* SqlSession 相同,执行 commit 操作(执行插入、更新、删除),清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,**避免脏读** +* SqlSession 相同,手动清除了一级缓存,调用 `sqlSession.clearCache()` +* SqlSession 相同,执行 commit 操作或者执行插入、更新、删除,清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,**避免脏读** 测试一级缓存存在 @@ -1517,9 +1565,13 @@ public void testFirstLevelCache(){ ### 二级缓存 -二级缓存是 mapper 的缓存,只要是同一个 mapper 的 SqlSession 就共享二级缓存的内容,并且可以操作二级缓存 +#### 基本介绍 + +二级缓存是 mapper 的缓存,只要是同一个命名空间(namespace)的 SqlSession 就共享二级缓存的内容,并且可以操作二级缓存 + +作用:作用范围是整个应用,可以跨线程使用,适合缓存一些修改较少的数据 -工作流程:一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中,如果**会话关闭**一级缓存中的数据会保存到二级缓存 +工作流程:一个会话查询数据,这个数据就会被放在当前会话的一级缓存中,如果**会话关闭或提交**一级缓存中的数据会保存到二级缓存 二级缓存的基本使用: @@ -1568,7 +1620,15 @@ public void testFirstLevelCache(){ public class User implements Serializable{} ``` -相关属性: + + + + +*** + + + +#### 相关属性 1. select 标签的 useCache 属性 @@ -1582,12 +1642,109 @@ public void testFirstLevelCache(){ </select> ``` -2. 每个增删改标签都有 flushCache 属性,默认为 true,代表在**执行增删改之后就会清除一、二级缓存**,而查询标签默认值为 false,所以查询不会清空缓存 +2. 每个增删改标签都有 flushCache 属性,默认为 true,代表在**执行增删改之后就会清除一、二级缓存**,保证缓存的一致性;而查询标签默认值为 false,所以查询不会清空缓存 3. localCacheScope:本地缓存作用域,<settings> 中的配置项,默认值为 SESSION,当前会话的所有数据保存在会话缓存中,设置为 STATEMENT 禁用一级缓存 +*** + + + +#### 源码解析 + +事务提交二级缓存才生效:DefaultSqlSession 调用 commit() 时会回调 `executor.commit()` + +* CachingExecutor#query():执行查询方法,查询出的数据会先放入 entriesToAddOnCommit 集合暂存 + + ```java + // 从二缓存中获取数据,获取不到去一级缓存获取 + List<E> list = (List<E>) tcm.getObject(cache, key); + if (list == null) { + // 回调 BaseExecutor#query + list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); + // 将数据放入 entriesToAddOnCommit 集合暂存,此时还没放入二级缓存 + tcm.putObject(cache, key, list); + } + ``` + +* commit():事务提交,清空一级缓存,二级缓存使用 TransactionalCacheManager(tcm)管理 + + ```java + public void commit(boolean required) throws SQLException { + // 首先调用 BaseExecutor#commit 方法,【清空一级缓存】 + delegate.commit(required); + tcm.commit(); + } + ``` + +* TransactionalCacheManager#commit:查询出的数据放入二级缓存 + + ```java + public void commit() { + // 获取所有的缓存事务,挨着进行提交 + for (TransactionalCache txCache : transactionalCaches.values()) { + txCache.commit(); + } + } + ``` + + ```java + public void commit() { + if (clearOnCommit) { + delegate.clear(); + } + // 将 entriesToAddOnCommit 中的数据放入二级缓存 + flushPendingEntries(); + // 清空相关集合 + reset(); + } + ``` + + ```java + private void flushPendingEntries() { + for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) { + // 将数据放入二级缓存 + delegate.putObject(entry.getKey(), entry.getValue()); + } + } + ``` + +增删改操作会清空缓存: + +* update():CachingExecutor 的更新操作 + + ```java + public int update(MappedStatement ms, Object parameterObject) throws SQLException { + flushCacheIfRequired(ms); + // 回调 BaseExecutor#update 方法,也会清空一级缓存 + return delegate.update(ms, parameterObject); + } + ``` + +* flushCacheIfRequired():判断是否需要清空二级缓存 + + ```java + private void flushCacheIfRequired(MappedStatement ms) { + Cache cache = ms.getCache(); + // 判断二级缓存是否存在 + // 判断标签的 flushCache 的值,增删改操作的 flushCache 属性默认为 true + if (cache != null && ms.isFlushCacheRequired()) { + // 清空二级缓存 + tcm.clear(cache); + } + } + ``` + + + + + + + + + *** @@ -1878,7 +2035,7 @@ trim 标记是一个格式化的标记,可以完成 set 或者是 where 标记 * suffix:给拼串后的整个字符串加一个后缀 * suffixOverrides:去掉整个字符串后面多余的字符 -改写 if+where 语句: +改写 if + where 语句: ```xml <select id="selectUserByUsernameAndSex" resultType="user" parameterType="com.ys.po.User"> @@ -1894,7 +2051,7 @@ trim 标记是一个格式化的标记,可以完成 set 或者是 where 标记 </select> ``` -改写 if+set 语句: +改写 if + set 语句: ```xml <!-- 根据 id 更新 user 表的数据 --> @@ -2129,7 +2286,7 @@ MyBatis 提供了 org.apache.ibatis.jdbc.SQL 功能类,专门用于构建 SQL #### 基本操作 -* MyBatisConfig.xml配置 +* MyBatisConfig.xml 配置 ```xml <!-- mappers引入映射配置文件 --> @@ -2138,7 +2295,7 @@ MyBatis 提供了 org.apache.ibatis.jdbc.SQL 功能类,专门用于构建 SQL </mappers> ``` -* Mapper类 +* Mapper 类 ```java public interface StudentMapper { @@ -2161,7 +2318,7 @@ MyBatis 提供了 org.apache.ibatis.jdbc.SQL 功能类,专门用于构建 SQL } ``` -* ReturnSql类 +* ReturnSQL 类 ```java public class ReturnSql { @@ -2454,6 +2611,12 @@ Executor#query(): * `CachingExecutor.query()`:先执行 CachingExecutor 去二级缓存获取数据 + ```java + public class CachingExecutor implements Executor { + private final Executor delegate; // 包装了 BaseExecutor,二级缓存不存在数据调用 BaseExecutor 查询 + } + ``` + * `MappedStatement.getBoundSql(parameterObject)`:**把 parameterObject 封装成 BoundSql** 构造函数中有:`this.parameterObject = parameterObject` @@ -2466,13 +2629,14 @@ Executor#query(): * `tcm.getObject(cache, key)`:尝试从**二级缓存**中获取数据 -* `BaseExecutor.query()`:获取不到缓存继续执行该方法 +* `BaseExecutor.query()`:二级缓存不存在该数据,调用该方法 * `localCache.getObject(key) `:尝试从**本地缓存(一级缓存**)获取数据 * `BaseExecutor.queryFromDatabase()`:缓存获取数据失败,**开始从数据库获取数据,并放入本地缓存** * `SimpleExecutor.doQuery()`:执行 query + * `configuration.newStatementHandler()`:创建 StatementHandler 对象 * 根据 <select> 标签的 statementType 属性,根据属性选择创建哪种对象 * 判断 BoundSql 是否被创建,没有创建会重新封装参数信息到 BoundSql @@ -2487,20 +2651,40 @@ Executor#query(): * 获取预编译执行者对象:`Connection.prepareStatement()` * `handler.parameterize()`:进行参数的设置 * `ParameterHandler.setParameters()`:**通过 ParameterHandler 设置参数** - * `typeHandler.setParameter()`:底层通过 TypeHandler 实现 + * `typeHandler.setParameter()`:底层通过 TypeHandler 实现,回调 JDBC 的接口进行设置 * `StatementHandler.query()`:**调用 JDBC 原生的 PreparedStatement 执行 SQL** - + ```java public <E> List<E> query(Statement statement, ResultHandler resultHandler) { // 获取 SQL 语句 String sql = boundSql.getSql(); statement.execute(sql); + // 通过 ResultSetHandler 对象封装结果集,映射成 JavaBean return resultSetHandler.handleResultSets(statement); } ``` - - * `resultSetHandler.handleResultSets(ps)`:**通过 ResultSetHandler 对象封装结果集** + + `resultSetHandler.handleResultSets(statement)`:处理结果集 + + * `handleResultSet(rsw, resultMap, multipleResults, null)`:底层回调 + + * `handleRowValues()`:逐行处理数据,根据是否配置了 <resultMap> 属性选择是否使用简单结果集映射 + + * 首先判断数据是否被限制行数,然后进行结果集的映射 + + * 最后将数据存入 ResultHandler 对象,底层就是 List 集合 + + ```java + public class DefaultResultHandler implements ResultHandler<Object> { + private final List<Object> list; + public void handleResult(ResultContext<?> context) { + list.add(context.getResultObject()); + } + } + ``` + + * `return collapseSingleResultList(multipleResults)`:可能存在多个结果集的情况 * `localCache.putObject(key, list)`:放入本地缓存 `return list.get(0)`:返回结果集的第一个数据 @@ -2511,8 +2695,6 @@ Executor#query(): - - **** @@ -4646,7 +4828,22 @@ FactoryBean与 BeanFactory 区别: } ``` +* MapperFactoryBean 继承 SqlSessionDaoSupport,可以获取 SqlSessionTemplate,完成 MyBatis 的整合 + + ```java + public abstract class SqlSessionDaoSupport extends DaoSupport { + private SqlSessionTemplate sqlSessionTemplate; + // 获取 SqlSessionTemplate 对象 + public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { + if (this.sqlSessionTemplate == null || + sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) { + this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory); + } + } + } + ``` +* MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor 接口,重写 postProcessBeanDefinitionRegistry() 方法,可以扫描到 MyBatis 的 Mapper From 8dc393b62f5da309ea51ca80a2e203cb4dc35f29 Mon Sep 17 00:00:00 2001 From: Seazean <imseazean@gmail.com> Date: Wed, 15 Sep 2021 13:57:20 +0800 Subject: [PATCH 007/122] Update Java Notes --- DB.md | 21 +++++++++++++++---- Java.md | 64 +++++++++++++++++++++++++++++++++++---------------------- Prog.md | 6 +++--- SSM.md | 4 +--- 4 files changed, 60 insertions(+), 35 deletions(-) diff --git a/DB.md b/DB.md index 50c2150..8e16c0e 100644 --- a/DB.md +++ b/DB.md @@ -36,6 +36,8 @@ 参考文章:https://time.geekbang.org/column/intro/139 +参考书籍:https://book.douban.com/subject/35231266/ + *** @@ -2416,8 +2418,19 @@ CREATE TABLE us_pro( SET @@AUTOCOMMIT=数字; -- 系统 SET AUTOCOMMIT=数字; -- 会话 ``` + + - 系统变量的操作: - + ```sql + SET [GLOBAL|SESSION] 变量名 = 值; -- 默认是会话 + SET @@[(GLOBAL|SESSION).]变量名 = 值; -- 默认是系统 + ``` + + ```sql + SHOW [GLOBAL|SESSION] VARIABLES [LIKE '变量%']; -- 默认查看会话内系统变量值 + ``` + + * 管理实务演示 @@ -2588,7 +2601,7 @@ InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到 刷脏策略: * redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到磁盘中的数据文件中 -* 系统内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘(大事务) +* Buffer Pool 内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘(大事务) * 系统空闲时,后台线程会自动进行刷脏 * MySQL 正常关闭时,会把内存的脏页都 flush 到磁盘上 @@ -4210,7 +4223,7 @@ InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,叶子 辅助索引叶子节点存储的是主键值,而不是数据的物理地址,所以访问数据需要二次查找,推荐使用覆盖索引,可以减少回表查询 -检索过程:辅助索引找到主键值,再通过聚簇索引找到数据页,最后通过数据页中的 Page Directory 找到数据行 +**检索过程**:辅助索引找到主键值,再通过聚簇索引(二分)找到数据页,最后通过数据页中的 Page Directory(二分)找到对应的数据分组,遍历组内所所有的数据找到数据行 @@ -4391,7 +4404,7 @@ B+ 树的叶子节点是数据页(page),一个页里面可以存多个数 - 有范围:对于主键的范围查找和分页查找 - 有顺序:从根节点开始,进行随机查找,顺序查找 -InnoDB 中每个数据页的大小默认是 16KB,一般表的主键类型为 INT(4字节)或 BIGINT(8字节),指针类型也一般为 4 或 8 个字节,也就是说一个页(B+Tree 中的**一个节点**)中大概存储 16KB/(8B+8B)=1K 个键值(估值)。则一个深度为 3 的 B+Tree 索引可以维护 `10^3 * 10^3 * 10^3 = 10亿` 条记录 +InnoDB 中每个数据页的大小默认是 16KB,一般表的主键类型为 INT(4 字节)或 BIGINT(8 字节),指针类型也一般为 4 或 8 个字节,也就是说一个页(B+Tree 中的**一个节点**)中大概存储 16KB/(8B+8B)=1K 个键值(估值)。则一个深度为 3 的 B+Tree 索引可以维护 `10^3 * 10^3 * 10^3 = 10亿` 条记录 实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree 的高度一般都在 2-4 层。MySQL 的 InnoDB 存储引擎在设计时是**将根节点常驻内存的**,也就是说查找某一键值的行记录时最多只需要 1~3 次磁盘 I/O 操作 diff --git a/Java.md b/Java.md index 05c8fd7..2662eee 100644 --- a/Java.md +++ b/Java.md @@ -19,10 +19,6 @@ -参考视频:https://www.bilibili.com/video/BV1TE41177mP - - - *** @@ -6403,7 +6399,7 @@ public class AgeIllegalRuntimeException extends RuntimeException{ -### 异常规范 +### 处理规范 异常的语法注意: @@ -7180,10 +7176,10 @@ B 66 中国人:中国人有 9 万左右字符,2 个字节编码一个中文字符,1 个字节编码一个英文字符,这套编码叫:GBK 编码,兼容 ASCII 编码表 -美国人:收集全球所有的字符,统一编号,这套编码叫 Unicode编码(万国码),一个英文等于两个字节,一个中文(含繁体)等于两个字节,中文标点占两个字节,英文标点占两个字节 +美国人:收集全球所有的字符,统一编号,这套编码叫 Unicode 编码(万国码),一个英文等于两个字节,一个中文(含繁体)等于两个字节,中文标点占两个字节,英文标点占两个字节 * UTF-8 是变种形式,也必须兼容ASCII编码表 -* UTF-8一个中文一般占 3 个字节,中文标点占 3 个,英文字母和数字 1 个字节 +* UTF-8 一个中文一般占 3 个字节,中文标点占 3 个,英文字母和数字 1 个字节 编码前与编码后的编码集必须一致才不会乱码 @@ -7513,7 +7509,11 @@ fw.close; -##### 字节缓冲输入流 +*** + + + +##### 字节缓冲输入 字节缓冲输入流:BufferedInputStream @@ -7543,7 +7543,11 @@ public class BufferedInputStreamDemo01 { -##### 字节缓冲输出流 +*** + + + +##### 字节缓冲输出 字节缓冲输出流:BufferedOutputStream @@ -7572,7 +7576,7 @@ public class BufferedOutputStreamDemo02 { -##### 字节流的性能分析 +##### 字节流性能 利用字节流的复制统计各种写法形式下缓冲流的性能执行情况。 @@ -7591,7 +7595,7 @@ public class BufferedOutputStreamDemo02 { -##### 字符缓冲输入流 +##### 字符缓冲输入 字符缓冲输入流:BufferedReader @@ -7625,7 +7629,11 @@ public static void main(String[] args) throws Exception { -##### 字符缓冲输出流 +*** + + + +##### 字符缓冲输出 符缓冲输出流:BufferedWriter @@ -7656,19 +7664,19 @@ public static void main(String[] args) throws Exception { ##### 高效原因 -字符型缓冲流高效的原因: +字符型缓冲流高效的原因:(空间换时间) -* BufferedReader:每次调用read方法,只有第一次从磁盘中读取了8192(**8k**)个字符,存储到该类型对象的缓冲区数组中,将其中一个返回给调用者,再次调用read方法时,就不需要访问磁盘,直接从缓冲区中拿出一个数据即可,提升了效率 -* BufferedWriter:每次调用write方法,不会直接将字符刷新到文件中,而是存储到字符数组中,等字符数组写满了,才一次性刷新到文件中,减少了和磁盘交互的次数,提升了效率 +* BufferedReader:每次调用 read 方法,只有第一次从磁盘中读取了 8192(**8k**)个字符,存储到该类型对象的缓冲区数组中,将其中一个返回给调用者,再次调用 read 方法时,就不需要访问磁盘,直接从缓冲区中拿出一个数据即可,提升了效率 +* BufferedWriter:每次调用 write 方法,不会直接将字符刷新到文件中,而是存储到字符数组中,等字符数组写满了,才一次性刷新到文件中,减少了和磁盘交互的次数,提升了效率 字节型缓冲流高效的原因: -* BufferedInputStream:在该类型中准备了一个数组,存储字节信息,当外界调用read()方法想获取一个字节的时候,该对象从文件中一次性读取了8192个字节到数组中,只返回了第一个字节给调用者。将来调用者再次调用read方法时,当前对象就不需要再次访问磁盘,只需要从数组中取出一个字节返回给调用者即可,由于读取的是数组,所以速度非常快。当8192个字节全都读取完成之后,再需要读取一个字节,就得让该对象到文件中读取下一个8192个字节 -* BufferedOutputStream:在该类型中准备了一个数组,存储字节信息,当外界调用write方法想写出一个字节的时候,该对象直接将这个字节存储到了自己的数组中,而不刷新到文件中。一直到该数组所有8192个位置全都占满,该对象才把这个数组中的所有数据一次性写出到目标文件中。如果最后一次循环过程中,没有将数组写满,最终在关闭流对象的时候,也会将该数组中的数据刷新到文件中。 +* BufferedInputStream:在该类型中准备了一个数组,存储字节信息,当外界调用 read() 方法想获取一个字节的时候,该对象从文件中一次性读取了 8192 个字节到数组中,只返回了第一个字节给调用者。将来调用者再次调用 read 方法时,当前对象就不需要再次访问磁盘,只需要从数组中取出一个字节返回给调用者即可,由于读取的是数组,所以速度非常快。当 8192 个字节全都读取完成之后,再需要读取一个字节,就得让该对象到文件中读取下一个 8192 个字节 +* BufferedOutputStream:在该类型中准备了一个数组,存储字节信息,当外界调用 write 方法想写出一个字节的时候,该对象直接将这个字节存储到了自己的数组中,而不刷新到文件中。一直到该数组所有 8192 个位置全都占满,该对象才把这个数组中的所有数据一次性写出到目标文件中。如果最后一次循环没有将数组写满,最终在关闭流对象的时候,也会将该数组中的数据刷新到文件中。 -注意:**字节流和字符流,都是装满时自动写出,或者没满时手动flush写出,或close时刷新写出** +注意:**字节流和字符流,都是装满时自动写出,或者没满时手动 flush 写出,或 close 时刷新写出** @@ -7689,8 +7697,8 @@ GBK GBK 不乱码! UTF-8 GBK 乱码! ``` -如果代码编码和读取的文件编码一致。字符流读取的时候不会乱码。 -如果代码编码和读取的文件编码不一致。字符流读取的时候会乱码。 +如果代码编码和读取的文件编码一致,字符流读取的时候不会乱码。 +如果代码编码和读取的文件编码不一致,字符流读取的时候会乱码。 @@ -7698,7 +7706,7 @@ UTF-8 GBK 乱码! -##### 字符输入转换流 +##### 字符输入 字符输入转换流:InputStreamReader @@ -7707,7 +7715,7 @@ UTF-8 GBK 乱码! 构造器: * `public InputStreamReader(InputStream is)` : 使用当前代码默认编码 UTF-8 转换成字符流 -* `public InputStreamReader(InputStream is,String charset)` : 指定编码把字节流转换成字符流 +* `public InputStreamReader(InputStream is, String charset)` : 指定编码把字节流转换成字符流 ```java public class InputStreamReaderDemo{ @@ -7729,7 +7737,11 @@ public class InputStreamReaderDemo{ -##### 字符输出转换流 +*** + + + +##### 字符输出 字符输出转换流:OutputStreamWriter @@ -7738,7 +7750,7 @@ public class InputStreamReaderDemo{ 构造器: * `public OutputStreamWriter(OutputStream os)` : 用默认编码 UTF-8 把字节输出流转换成字符输出流 -* `public OutputStreamWriter(OutputStream os ,String charset)` : 指定编码把字节输出流转换成 +* `public OutputStreamWriter(OutputStream os, String charset)` : 指定编码把字节输出流转换成 ```Java OutputStream os = new FileOutputStream("Day10Demo/src/dlei07.txt"); @@ -9670,6 +9682,8 @@ JVM、JRE、JDK对比: +参考书籍:https://book.douban.com/subject/34907497/ + 参考视频:https://www.bilibili.com/video/BV1PJ411n7xZ 参考视频:https://www.bilibili.com/video/BV1yE411Z7AP @@ -15370,7 +15384,7 @@ GCViewer 是一款离线的 GC 日志分析器,用于可视化 Java VM 选项 -参考书籍:《数据结构高分笔记》 +参考书籍:https://book.douban.com/subject/35263893/ diff --git a/Prog.md b/Prog.md index c6408c2..0f39a40 100644 --- a/Prog.md +++ b/Prog.md @@ -26,7 +26,7 @@ -参考视频:https://www.bilibili.com/video/BV16J411h7Rd(推荐观看) +参考视频:https://www.bilibili.com/video/BV16J411h7Rd 笔记的整体结构依据视频编写,并随着学习的深入补充了很多知识 @@ -12958,8 +12958,8 @@ final void updateHead(Node<E> h, Node<E> p) { * Pv6:可以实现为所有设备分配 IP 128 位 * ipconfig:查看本机的 IP - ​ ping 检查本机与某个 IP 指定的机器是否联通,或者说是检测对方是否在线。 - ​ ping 空格 IP地址 :ping 220.181.57.216,ping www.baidu.com + * ping 检查本机与某个 IP 指定的机器是否联通,或者说是检测对方是否在线。 + * ping 空格 IP地址 :ping 220.181.57.216,ping www.baidu.com 特殊的IP地址: 本机IP地址,**127.0.0.1 == localhost**,回环测试 diff --git a/SSM.md b/SSM.md index 3e40868..0cd8522 100644 --- a/SSM.md +++ b/SSM.md @@ -2893,7 +2893,7 @@ PageInfo相关API: -笔记的总体架构基于黑马程序员的视频进行制作 +参考视频:https://space.bilibili.com/37974444 @@ -2942,8 +2942,6 @@ Spring 优点: - 内聚(Cohesion):代码编写过程中单个模块内部各组成部分间的联系,用于衡量软件中各个功能模块内部的功能联系 - 代码编写的目标:高内聚,低耦合。同一个模块内的各个元素之间要高度紧密,各个模块之间的相互依存度不紧密 -![](https://gitee.com/seazean/images/raw/master/Frame/Spring发展历程.png) - From cc4cd6c512d8703841be489b425b42afc14649a5 Mon Sep 17 00:00:00 2001 From: Seazean <imseazean@gmail.com> Date: Thu, 16 Sep 2021 22:47:42 +0800 Subject: [PATCH 008/122] Update Java Notes --- DB.md | 4 +++- Prog.md | 2 +- SSM.md | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/DB.md b/DB.md index 8e16c0e..380a105 100644 --- a/DB.md +++ b/DB.md @@ -5112,13 +5112,15 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引4.png) + 虽然索引列失效,但是系统**使用了索引下推进行了优化** + * **范围查询**右边的列,不能使用索引: ```mysql EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status>'1' AND address='西安市'; ``` - 根据前面的两个字段 name , status 查询是走索引的, 但是最后一个条件 address 没有用到索引 + 根据前面的两个字段 name , status 查询是走索引的, 但是最后一个条件 address 没有用到索引,使用了索引下推 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引5.png) diff --git a/Prog.md b/Prog.md index 0f39a40..c908711 100644 --- a/Prog.md +++ b/Prog.md @@ -15072,7 +15072,7 @@ public class Server { Selector selector = Selector.open(); // 5、将通道都注册到选择器上去,并且开始指定监听接收事件 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); - // 6、使用Selector选择器轮询已经就绪好的事件 + // 6、使用Selector选择器阻塞等待轮已经就绪好的事件 while (selector.select() > 0) { System.out.println("----开始新一轮的时间处理----"); // 7、获取选择器中的所有注册的通道中已经就绪好的事件 diff --git a/SSM.md b/SSM.md index 0cd8522..ec3e827 100644 --- a/SSM.md +++ b/SSM.md @@ -7897,7 +7897,7 @@ AbstractBeanFactory.doGetBean():获取 Bean,context.getBean() 追踪到此 原因:先加载 A,把 A 加入集合,A 依赖 B 去加载 B,B 又依赖 A,去加载 A,发现 A 在正在创建集合中,产生循环依赖 -* `markBeanAsCreated(beanName)`:把 bean 标记为已经创建 +* `markBeanAsCreated(beanName)`:把 bean 标记为已经创建,**防止其他线程重新创建 Bean** * `mbd = getMergedLocalBeanDefinition(beanName)`:**获取合并父 BD 后的 BD 对象**,BD 是直接继承的,合并后的 BD 信息是包含父类的 BD 信息 From 975622110ec6c0f2035cc224bd8a3b6501236e08 Mon Sep 17 00:00:00 2001 From: Seazean <imseazean@gmail.com> Date: Sat, 18 Sep 2021 22:41:16 +0800 Subject: [PATCH 009/122] Update Java Notes --- Prog.md | 115 +++++++++++++++++++++++++++++++++++++++++++------------- Web.md | 21 +++++++++++ 2 files changed, 109 insertions(+), 27 deletions(-) diff --git a/Prog.md b/Prog.md index c908711..daebcb3 100644 --- a/Prog.md +++ b/Prog.md @@ -2726,7 +2726,7 @@ getInstance 方法对应的字节码为: * 21 表示利用一个对象引用,调用构造方法初始化对象 * 24 表示利用一个对象引用,赋值给 static INSTANCE -步骤 21 和 24 之间不存在数据依赖关系,而且无论重排前后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的 +**步骤 21 和 24 之间不存在数据依赖关系**,而且无论重排前后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的 * 关键在于 0:getstatic 这行代码在 monitor 控制之外,可以越过 monitor 读取 INSTANCE 变量的值 * 当其他线程访问 instance 不为 null 时,由于 instance 实例未必已初始化,那么 t2 拿到的是将是一个未初始化完毕的单例返回,这就造成了线程安全的问题 @@ -14359,29 +14359,30 @@ Java NIO 系统的核心在于:通道和缓冲区,通道表示打开的 IO Buffer 基本操作: -| 方法 | 说明 | -| ------------------------------------------- | ------------------------------------------------------- | -| public Buffer clear() | 清空缓冲区,不清空内容,将位置设置为零,限制设置为容量 | -| public Buffer flip() | 翻转缓冲区,将缓冲区的界限设置为当前位置,position 置 0 | -| public int capacity() | 返回 Buffer的 capacity 大小 | -| public final int limit() | 返回 Buffer 的界限 limit 的位置 | -| public Buffer limit(int n) | 设置缓冲区界限为 n | -| public Buffer mark() | 在此位置对缓冲区设置标记 | -| public final int position() | 返回缓冲区的当前位置 position | -| public Buffer position(int n) | 设置缓冲区的当前位置为n | -| public Buffer reset() | 将位置 position 重置为先前 mark 标记的位置 | -| public Buffer rewind() | 将位置设为为0,取消设置的 mark | -| public final int remaining() | 返回当前位置 position 和 limit 之间的元素个数 | -| public final boolean hasRemaining() | 判断缓冲区中是否还有元素 | -| public static ByteBuffer wrap(byte[] array) | 将一个字节数组包装到缓冲区中 | -| abstract ByteBuffer asReadOnlyBuffer() | 创建一个新的只读字节缓冲区 | +| 方法 | 说明 | +| ------------------------------------------- | ------------------------------------------------------------ | +| public Buffer clear() | 清空缓冲区,不清空内容,将位置设置为零,限制设置为容量 | +| public Buffer flip() | 翻转缓冲区,将缓冲区的界限设置为当前位置,position 置 0 | +| public int capacity() | 返回 Buffer的 capacity 大小 | +| public final int limit() | 返回 Buffer 的界限 limit 的位置 | +| public Buffer limit(int n) | 设置缓冲区界限为 n | +| public Buffer mark() | 在此位置对缓冲区设置标记 | +| public final int position() | 返回缓冲区的当前位置 position | +| public Buffer position(int n) | 设置缓冲区的当前位置为n | +| public Buffer reset() | 将位置 position 重置为先前 mark 标记的位置 | +| public Buffer rewind() | 将位置设为为 0,取消设置的 mark | +| public final int remaining() | 返回当前位置 position 和 limit 之间的元素个数 | +| public final boolean hasRemaining() | 判断缓冲区中是否还有元素 | +| public static ByteBuffer wrap(byte[] array) | 将一个字节数组包装到缓冲区中 | +| abstract ByteBuffer asReadOnlyBuffer() | 创建一个新的只读字节缓冲区 | +| public abstract ByteBuffer compact() | 缓冲区当前位置与其限制(如果有)之间的字节被复制到缓冲区的开头 | Buffer 数据操作: | 方法 | 说明 | | ------------------------------------------------- | ----------------------------------------------- | -| public abstract byte get() | 读取该缓冲区当前位置的单个字节,然后增加位置 | -| public ByteBuffer get(byte[] dst) | 读取多个字节到字节数组dst中 | +| public abstract byte get() | 读取该缓冲区当前位置的单个字节,然后位置 + 1 | +| public ByteBuffer get(byte[] dst) | 读取多个字节到字节数组 dst 中 | | public abstract byte get(int index) | 读取指定索引位置的字节,不移动 position | | public abstract ByteBuffer put(byte b) | 将给定单个字节写入缓冲区的当前位置,position+1 | | public final ByteBuffer put(byte[] src) | 将 src 字节数组写入缓冲区的当前位置 | @@ -14452,6 +14453,57 @@ public class TestBuffer { +**** + + + +#### 粘包拆包 + +网络上有多条数据发送给服务端,数据之间使用 \n 进行分隔,但这些数据在接收时,被进行了重新组合 + +```java +// Hello,world\n +// I'm zhangsan\n +// How are you?\n +------ > 黏包,半包 +// Hello,world\nI'm zhangsan\nHo +// w are you?\n +``` + +```java +public static void main(String[] args) { + ByteBuffer source = ByteBuffer.allocate(32); + // 11 24 + source.put("Hello,world\nI'm zhangsan\nHo".getBytes()); + split(source); + + source.put("w are you?\nhaha!\n".getBytes()); + split(source); +} + +private static void split(ByteBuffer source) { + source.flip(); + int oldLimit = source.limit(); + for (int i = 0; i < oldLimit; i++) { + if (source.get(i) == '\n') { + // 根据数据的长度设置缓冲区 + ByteBuffer target = ByteBuffer.allocate(i + 1 - source.position()); + // 0 ~ limit + source.limit(i + 1); + target.put(source); // 从source 读,向 target 写 + // debugAll(target); 访问 buffer 的方法 + source.limit(oldLimit); + } + } + // 访问过的数据复制到开头 + source.compact(); +} +``` + + + + + **** @@ -14700,11 +14752,12 @@ public class MappedByteBufferTest { 3. Channel 在 NIO 中是一个接口:`public interface Channel extends Closeable{}` - - Channel 实现类: -* FileChannel:用于读取、写入、映射和操作文件的通道 +* FileChannel:用于读取、写入、映射和操作文件的通道,只能工作在阻塞模式下 + * 通过 FileInputStream 获取的 Channel 只能读 + * 通过 FileOutputStream 获取的 Channel 只能写 + * 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定 * DatagramChannel:通过 UDP 读写网络中的数据通道 * SocketChannel:通过 TCP 读写网络中的数据 * ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个SocketChannel。 @@ -14729,9 +14782,9 @@ Channel 基本操作: | 方法 | 说明 | | ------------------------------------------ | -------------------------------------------------------- | | public abstract int read(ByteBuffer dst) | 从 Channel 中读取数据到 ByteBuffer,从 position 开始储存 | -| public final long read(ByteBuffer[] dsts) | 将Channel中的数据“分散”到ByteBuffer[] | +| public final long read(ByteBuffer[] dsts) | 将 Channel 中的数据分散到 ByteBuffer[] | | public abstract int write(ByteBuffer src) | 将 ByteBuffer 中的数据写入 Channel,从 position 开始写出 | -| public final long write(ByteBuffer[] srcs) | 将ByteBuffer[]到中的数据“聚集”到Channel | +| public final long write(ByteBuffer[] srcs) | 将 ByteBuffer[] 到中的数据聚集到 Channel | | public abstract long position() | 返回此通道的文件位置 | | FileChannel position(long newPosition) | 设置此通道的文件位置 | | public abstract long size() | 返回此通道的文件的当前大小 | @@ -14941,7 +14994,7 @@ public class ChannelTest { 创建 Selector:`Selector selector = Selector.open();` -向选择器注册通道:`SelectableChannel.register(Selector sel, int ops)` +向选择器注册通道:`SelectableChannel.register(Selector sel, int ops, Object att)` 选择器对通道的监听事件,需要通过第二个参数 ops 指定。监听的事件类型用四个常量表示: @@ -14969,6 +15022,7 @@ SelectionKey API: | ------------------------------------------- | -------------------------------------------------- | | public abstract void cancel() | 取消该键的通道与其选择器的注册 | | public abstract SelectableChannel channel() | 返回创建此键的通道,该方法在取消键之后仍将返回通道 | +| public final Object attachment() | 返回当前 key 关联的缓冲 | | public final boolean isAcceptable() | 检测此密钥的通道是否已准备好接受新的套接字连接 | | public final boolean isConnectable() | 检测此密钥的通道是否已完成或未完成其套接字连接操作 | | public final boolean isReadable() | 检测此密钥的频道是否可以阅读 | @@ -15057,7 +15111,7 @@ ssChannel.register(selector, SelectionKey.OP_ACCEPT); 3. 分配指定大小的缓冲区:`ByteBuffer buffer = ByteBuffer.allocate(1024)` 4. 发送数据给服务端 -37 行代码,如果判断条件改为 !=-1,需要客户端 shutdown 一下 +37 行代码,如果判断条件改为 !=-1,需要客户端 close 一下 ```java public class Server { @@ -15087,6 +15141,11 @@ public class Server { SocketChannel socketChannel = serverSocketChannel.accept(); // 11 、切换成非阻塞模式 socketChannel.configureBlocking(false); + /* + ByteBuffer buffer = ByteBuffer.allocate(16); + // 将一个 byteBuffer 作为附件【关联】到 selectionKey 上 + SelectionKey scKey = sc.register(selector, 0, buffer); + */ // 12、将本客户端通道注册到选择器 socketChannel.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { @@ -15095,6 +15154,8 @@ public class Server { SocketChannel socketChannel = (SocketChannel) channel; // 14、读取数据 ByteBuffer buffer = ByteBuffer.allocate(1024); + // 获取关联的附件 + // ByteBuffer buffer = (ByteBuffer) key.attachment(); int len; while ((len = socketChannel.read(buffer)) > 0) { buffer.flip(); @@ -15124,7 +15185,7 @@ public class Client { while (true){ System.out.print("请说:"); String msg = sc.nextLine(); - buffer.put(("波妞:" + msg).getBytes()); + buffer.put(("Client:" + msg).getBytes()); buffer.flip(); socketChannel.write(buffer); buffer.clear(); diff --git a/Web.md b/Web.md index bc52b2c..3bf591f 100644 --- a/Web.md +++ b/Web.md @@ -2175,7 +2175,28 @@ HTTP 和 HTTPS 的区别: 思想:锁上加锁 +HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加密,客户端生成的随机密钥,用来进行对称加密 +![](https://gitee.com/seazean/images/raw/master/Web/HTTP-HTTPS加密过程.png) + +1. 客户端向服务器发起 HTTPS 请求,连接到服务器的 443 端口 + +2. 服务器端有一个密钥对,即公钥和私钥,用来进行非对称加密,服务器端保存着私钥不能泄露,公钥可以发给任何客户端 + +3. 服务器将公钥发送给客户端 + +4. 客户端收到服务器端的数字证书之后,会对证书进行检查,验证其合法性,如果发现发现证书有问题,那么 HTTPS 传输就无法继续。如果公钥合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,将该密钥称之为 client key,即客户端密钥。然后用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文了,HTTPS 中的第一次 HTTP 请求结束 + +5. 客户端会发起 HTTPS 中的第二个 HTTP 请求,将加密之后的客户端密钥发送给服务器 + +6. 服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文 + +7. 服务器将加密后的密文发送给客户端 + +8. 客户端收到服务器发送来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据,这样 HTTPS 中的第二个 HTTP 请求结束,整个 HTTPS 传输完成 + + +参考文章:https://www.jianshu.com/p/14cd2c9d2cd2 From c5b5fb50bd912b67aab310a9cde9bf73634f1e56 Mon Sep 17 00:00:00 2001 From: Seazean <imseazean@gmail.com> Date: Sun, 19 Sep 2021 11:22:39 +0800 Subject: [PATCH 010/122] Update Java Notes --- Frame.md | 1854 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Java.md | 4 +- 2 files changed, 1856 insertions(+), 2 deletions(-) create mode 100644 Frame.md diff --git a/Frame.md b/Frame.md new file mode 100644 index 0000000..ad34333 --- /dev/null +++ b/Frame.md @@ -0,0 +1,1854 @@ +# Maven + +## 基本介绍 + +### Mvn介绍 + +Maven:本质是一个项目管理工具,将项目开发和管理过程抽象成一个项目对象模型(POM) + +POM:Project Object Model 项目对象模型。Maven 是用 Java 语言编写的,管理的东西以面向对象的形式进行设计,最终把一个项目看成一个对象,这个对象叫做 POM + +pom.xml:Maven 需要一个 pom.xml 文件,Maven 通过加载这个配置文件可以知道项目的相关信息,这个文件代表就一个项目。如果做 8 个项目,对应的是 8 个 pom.xml 文件 + +依赖管理:Maven 对项目所有依赖资源的一种管理,它和项目之间是一种双向关系,即做项目时可以管理所需要的其他资源,当其他项目需要依赖我们项目时,Maven 也会把我们的项目当作一种资源去进行管理。 + +管理资源的存储位置:本地仓库,私服,中央仓库 + +![](https://gitee.com/seazean/images/raw/master/Frame/Maven介绍.png) + +基本作用: + +* 项目构建:提供标准的,跨平台的自动化构建项目的方式 + +* 依赖管理:方便快捷的管理项目依赖的资源(jar 包),避免资源间的版本冲突等问题 + +* 统一开发结构:提供标准的,统一的项目开发结构 + + ![](https://gitee.com/seazean/images/raw/master/Frame/Maven标准结构.png) + +各目录存放资源类型说明: + +* src/main/java:项目 java 源码 + +* src/main/resources:项目的相关配置文件(比如 mybatis 配置,xml 映射配置,自定义配置文件等) + +* src/main/webapp:web 资源(比如 html、css、js 等) + +* src/test/java:测试代码 + +* src/test/resources:测试相关配置文件 + +* src/pom.xml:项目 pom 文件 + + + +*** + + + +### 基础概念 + +仓库:用于存储资源,主要是各种 jar 包。有本地仓库,私服,中央仓库,私服和中央仓库都是远程仓库 + +* 中央仓库:Maven 团队自身维护的仓库,属于开源的 + +* 私服:各公司/部门等小范围内存储资源的仓库,私服也可以从中央仓库获取资源,作用: + * 保存具有版权的资源,包含购买或自主研发的 jar + * 一定范围内共享资源,能做到仅对内不对外开放 + +* 本地仓库:开发者自己电脑上存储资源的仓库,也可从远程仓库获取资源 + + + +坐标:Maven 中的坐标用于描述仓库中资源的位置 + +* 作用:使用唯一标识,唯一性定义资源位置,通过该标识可以将资源的识别与下载工作交由机器完成 + * https://mvnrepository.com:查询 maven 某一个资源的坐标,输入资源名称进行检索 + + * 依赖设置: + * groupId:定义当前资源隶属组织名称(通常是域名反写,如:org.mybatis) + * artifactId:定义当前资源的名称(通常是项目或模块名称,如:crm、sms) + * version:定义当前资源的版本号 + +* packaging:定义资源的打包方式,取值一般有如下三种 + + * jar:该资源打成 jar 包,默认是 jar + + * war:该资源打成 war 包 + + * pom:该资源是一个父资源(表明使用 Maven 分模块管理),打包时只生成一个 pom.xml 不生成 jar 或其他包结构 + + + + + +*** + + + +## 环境搭建 + +### 环境配置 + +Maven 的官网:http://maven.apache.org/ + +下载安装:Maven 是一个绿色软件,解压即安装 + +目录结构: + +* bin:可执行程序目录 +* boot:Maven 自身的启动加载器 +* conf:Maven 配置文件的存放目录 +* lib:Maven运行所需库的存放目录 + +配置 MAVEN_HOME: + +![](https://gitee.com/seazean/images/raw/master/Frame/Maven配置环境变量.png) + +Path 下配置:`%MAVEN_HOME%\bin` + +环境变量配置好之后需要测试环境配置结果,在 DOS 命令窗口下输入以下命令查看输出:`mvn -v` + + + +*** + + + +### 仓库配置 + +默认情况 Maven 本地仓库在系统用户目录下的 `.m2/repository`,修改 Maven 的配置文件 `conf/settings.xml` 来修改仓库位置 + +* 修改本地仓库位置:找到 <localRepository> 标签,修改默认值 + + ```xml + <!-- localRepository + | The path to the local repository maven will use to store artifacts. + | Default: ${user.home}/.m2/repository + <localRepository>/path/to/local/repo</localRepository> + --> + <localRepository>E:\Workspace\Java\Project\.m2\repository</localRepository> + ``` + + 注意:在仓库的同级目录即 `.m2` 也应该包含一个 `settings.xml` 配置文件,局部用户配置优先与全局配置 + + * 全局 setting 定义了 Maven 的公共配置 + * 用户 setting 定义了当前用户的配置 + +* 修改远程仓库:在配置文件中找到 `<mirrors>` 标签,在这组标签下添加国内镜像 + + ```xml + <mirror> + <id>nexus-aliyun</id> + <mirrorOf>central</mirrorOf> <!--必须是central--> + <name>Nexus aliyun</name> + <url>http://maven.aliyun.com/nexus/content/groups/public</url> + </mirror> + ``` + +* 修改默认 JDK:在配置文件中找到 `<profiles>` 标签,添加配置 + + ```xml + <profile> + <id>jdk-10</id> + <activation> + <activeByDefault>true</activeByDefault> + <jdk>10</jdk> + </activation> + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <maven.compiler.source>10</maven.compiler.source> + <maven.compiler.target>10</maven.compiler.target> + </properties> + </profile> + ``` + + + + + +*** + + + + + +## 项目搭建 + +### 手动搭建 + +1. 在 E 盘下创建目录 `mvnproject 进入该目录,作为我们的操作目录 + +2. 创建我们的 Maven 项目,创建一个目录 `project-java` 作为我们的项目文件夹,并进入到该目录 + +3. 创建 Java 代码(源代码)所在目录,即创建 `src/main/java` + +4. 创建配置文件所在目录,即创建 `src/main/resources` + +5. 创建测试源代码所在目录,即创建 `src/test/java` + +6. 创建测试存放配置文件存放目录,即 `src/test/resources` + +7. 在 `src/main/java` 中创建一个包(注意在 Windos 文件夹下就是创建目录)`demo`,在该目录下创建 `Demo.java` 文件,作为演示所需 Java 程序,内容如下 + + ```java + package demo; + public class Demo{ + public String say(String name){ + System.out.println("hello "+name); + return "hello "+name; + } + } + ``` + +8. 在 `src/test/java` 中创建一个测试包(目录)`demo`,在该包下创建测试程序 `DemoTest.java` + + ```java + package demo; + import org.junit.*; + public class DemoTest{ + @Test + public void testSay(){ + Demo d = new Demo(); + String ret = d.say("maven"); + Assert.assertEquals("hello maven",ret); + } + } + ``` + +9. **在 `project-java/src` 下创建 `pom.xml` 文件,格式如下:** + + ```xml + <?xml version="1.0" encoding="UTF-8"?> + <project + xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + + <!--指定pom的模型版本--> + <modelVersion>4.0.0</modelVersion> + <!--打包方式,web工程打包为war,java工程打包为jar --> + <packaging>jar</packaging> + + <!--组织id--> + <groupId>demo</groupId> + <!--项目id--> + <artifactId>project-java</artifactId> + <!--版本号:release,snapshot--> + <version>1.0</version> + + <!--设置当前工程的所有依赖--> + <dependencies> + <!--具体的依赖--> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.12</version> + </dependency> + </dependencies> + </project> + ``` + +10. 搭建完成 Maven 的项目结构,通过 Maven 来构建项目。Maven 的构建命令以 `mvn` 开头,后面添加功能参数,可以一次性执行多个命令,用空格分离 + + * `mvn compile`:编译 + * `mvn clean`:清理 + * `mvn test`:测试 + * `mvn package`:打包 + * `mvn install`:安装到本地仓库 + + 注意:执行某一条命令,则会把前面所有的都执行一遍 + + + +*** + + + +### 插件构建 + +![](https://gitee.com/seazean/images/raw/master/Frame/Maven-插件构建.png) + + + +*** + + + +### IDEA搭建 + +#### 不用原型 + +1. 在 IDEA 中配置 Maven,选择 maven3.6.1 防止依赖问题 + <img src="https://gitee.com/seazean/images/raw/master/Frame/IDEA配置Maven.png" alt="IDEA配置Maven" style="zoom:67%;" /> + +2. 创建 Maven,New Module → Maven→ 不选中 Create from archetype + +3. 填写项目的坐标 + + * GroupId:demo + * ArtifactId:project-java + +4. 查看各目录颜色标记是否正确 + + ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA创建Maven目录结构.png) + +5. IDEA 右侧侧栏有 Maven Project,打开后有 Lifecycle 生命周期 + + ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA-Maven生命周期.png) + +6. 自定义 Maven 命令:Run → Edit Configurations → 左上角 + → Maven + + ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA配置Maven命令.png) + + + +*** + + + +#### 使用原型 + +普通工程: + +1. 创建 Maven 项目的时候选择使用原型骨架 + + ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA创建Maven-quickstart.png) + +2. 创建完成后发现通过这种方式缺少一些目录,需要手动去补全目录,并且要对补全的目录进行标记 + + + +web 工程: + +1. 选择 web 对应的原型骨架(选择 Maven 开头的是简化的) + + ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA创建Maven-webapp.png) + +2. 通过原型创建 web 项目得到的目录结构是不全的,因此需要我们自行补全,同时要标记正确 + +3. web 工程创建之后需要启动运行,使用 tomcat 插件来运行项目,在 `pom.xml` 中添加插件的坐标: + + ```xml + <?xml version="1.0" encoding="UTF-8"?> + <project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + <packaging>war</packaging> + + <name>web01</name> + <groupId>demo</groupId> + <artifactId>web01</artifactId> + <version>1.0-SNAPSHOT</version> + + <dependencies> + </dependencies> + + <!--构建--> + <build> + <!--设置插件--> + <plugins> + <!--具体的插件配置--> + <plugin> + <!--https://mvnrepository.com/ 搜索--> + <groupId>org.apache.tomcat.maven</groupId> + <artifactId>tomcat7-maven-plugin</artifactId> + <version>2.1</version> + <configuration> + <port>80</port> <!--80端口默认不显示--> + <path>/</path> + </configuration> + </plugin> + </plugins> + </build> + </project> + ``` + +4. 插件配置以后,在 IDEA 右侧 `maven-project` 操作面板看到该插件,并且可以利用该插件启动项目,web01 → Plugins → tomcat7 → tomcat7:run + + + +*** + + + +## 依赖管理 + +### 依赖配置 + +依赖是指在当前项目中运行所需的 jar,依赖配置的格式如下: + +```xml +<!--设置当前项目所依赖的所有jar--> +<dependencies> + <!--设置具体的依赖--> + <dependency> + <!--依赖所属群组id--> + <groupId>junit</groupId> + <!--依赖所属项目id--> + <artifactId>junit</artifactId> + <!--依赖版本号--> + <version>4.12</version> + </dependency> +</dependencies> +``` + + + +*** + + + +### 依赖传递 + +依赖具有传递性,分两种: + +* 直接依赖:在当前项目中通过依赖配置建立的依赖关系 + +* 间接依赖:被依赖的资源如果依赖其他资源,则表明当前项目间接依赖其他资源 + + 注意:直接依赖和间接依赖其实也是一个相对关系 + + + +依赖传递的冲突问题:在依赖传递过程中产生了冲突,有三种优先法则 + +* 路径优先:当依赖中出现相同资源时,层级越深,优先级越低,反之则越高 + +* 声明优先:当资源在相同层级被依赖时,配置顺序靠前的覆盖靠后的 + +* 特殊优先:当同级配置了相同资源的不同版本时,后配置的覆盖先配置的 + + + +**可选依赖:**对外隐藏当前所依赖的资源,不透明 + +```xml +<dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.11</version> + <optional>true</optional> + <!--默认是false,true以后就变得不透明--> +</dependency> +``` + +**排除依赖:主动**断开依赖的资源,被排除的资源无需指定版本 + +```xml +<dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.12</version> + <exclusions> + <exclusion> + <groupId>org.hamcrest</groupId> <!--排除这个资源--> + <artifactId>hamcrest-core</artifactId> + </exclusion> + </exclusions> +</dependency> +``` + + + +*** + + + +### 依赖范围 + +依赖的 jar 默认情况可以在任何地方可用,可以通过 `scope` 标签设定其作用范围,有三种: + +* 主程序范围有效(src/main 目录范围内) + +* 测试程序范围内有效(src/test 目录范围内) + +* 是否参与打包(package 指令范围内) + +`scope` 标签的取值有四种:`compile,test,provided,runtime` + +![](https://gitee.com/seazean/images/raw/master/Frame/Maven依赖范围.png) + + + +**依赖范围的传递性:** + +![](https://gitee.com/seazean/images/raw/master/Frame/Maven依赖范围的传递性.png) + + + + + +*** + + + +## 生命周期 + +### 相关事件 + +Maven 的构建生命周期描述的是一次构建过程经历了多少个事件 + +最常用的一套流程:compile → test-compile → test → package → install + +* clean:清理工作 + + * pre-clean:执行一些在 clean 之前的工作 + * clean:移除上一次构建产生的所有文件 + * post-clean:执行一些在 clean 之后立刻完成的工作 + +* default:核心工作,例如编译,测试,打包,部署等 + + 对于 default 生命周期,每个事件在执行之前都会**将之前的所有事件依次执行一遍** + + ![](https://gitee.com/seazean/images/raw/master/Frame/Maven-default生命周期.png) + +* site:产生报告,发布站点等 + + * pre-site:执行一些在生成站点文档之前的工作 + * site:生成项目的站点文档 + * post-site:执行一些在生成站点文档之后完成的工作,并为部署做准备 + * site-deploy:将生成的站点文档部署到特定的服务器上 + + + +*** + + + +### 执行事件 + +Maven 的插件用来执行生命周期中的相关事件 + +- 插件与生命周期内的阶段绑定,在执行到对应生命周期时执行对应的插件 + +- Maven 默认在各个生命周期上都绑定了预先设定的插件来完成相应功能 + +- 插件还可以完成一些自定义功能 + + ```xml + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-source-plugin</artifactId> + <version>2.2.1</version> + <!--执行--> + <excutions> + <!--具体执行位置--> + <excution> + <goals> + <!--对源码进行打包,打包放在target目录--> + <goal>jar</goal> + <!--对测试代码进行打包--> + <goal>test-jar</goal> + </goals> + <!--执行的生命周期--> + <phase>generate-test-resources</phase> + </excution> + </excutions> + </plugin> + </plugins> + </build> + ``` + + + +*** + + + +## 模块开发 + +### 拆分 + +工程模块与模块划分: + +![](https://gitee.com/seazean/images/raw/master/Frame/Maven模块划分.png) + +* ssm_pojo 拆分 + + * 新建模块,拷贝原始项目中对应的相关内容到 ssm_pojo 模块中 + * 实体类(User) + * 配置文件(无) + +* ssm_dao 拆分 + + * 新建模块 + + * 拷贝原始项目中对应的相关内容到 ssm_dao 模块中 + + - 数据层接口(UserDao) + + - 配置文件:保留与数据层相关配置文件(3 个) + + - 注意:分页插件在配置中与 SqlSessionFactoryBean 绑定,需要保留 + + - pom.xml:引入数据层相关坐标即可,删除 SpringMVC 相关坐标 + + - Spring + - MyBatis + - Spring 整合 MyBatis + - MySQL + - druid + - pagehelper + - 直接依赖 ssm_pojo(对 ssm_pojo 模块执行 install 指令,将其安装到本地仓库) + + ```xml + <dependencies> <!--导入资源文件pojo--> + <dependency> + <groupId>demo</groupId> + <artifactId>ssm_pojo</artifactId> + <version>1.0-SNAPSHOT</version> + </dependency> + <!--spring环境--> + <!--mybatis环境--> + <!--mysql环境--> + <!--spring整合jdbc--> + <!--spring整合mybatis--> + <!--druid连接池--> + <!--分页插件坐标--> + </dependencies> + ``` + +* ssm_service 拆分 + + * 新建模块 + * 拷贝原始项目中对应的相关内容到 ssm_service 模块中 + + - 业务层接口与实现类(UserService、UserServiceImpl) + - 配置文件:保留与数据层相关配置文件(1 个) + - pom.xml:引入数据层相关坐标即可,删除 SpringMVC 相关坐标 + + - spring + + - junit + + - spring 整合 junit + + - 直接依赖 ssm_dao(对 ssm_dao 模块执行 install 指令,将其安装到本地仓库) + + - 间接依赖 ssm_pojo(由 ssm_dao 模块负责依赖关系的建立) + - 修改 service 模块 Spring 核心配置文件名,添加模块名称,格式:applicationContext-service.xml + - 修改 dao 模块 Spring 核心配置文件名,添加模块名称,格式:applicationContext-dao.xml + - 修改单元测试引入的配置文件名称,由单个文件修改为多个文件 + +* ssm_control 拆分 + + * 新建模块(使用 webapp 模板) + + * 拷贝原始项目中对应的相关内容到 ssm_controller 模块中 + + - 现层控制器类与相关设置类(UserController、异常相关……) + + - 配置文件:保留与表现层相关配置文件(1 个)、服务器相关配置文件(1 个) + + - pom.xml:引入数据层相关坐标即可,删除 SpringMVC 相关坐标 + + - spring + + - springmvc + + - jackson + + - servlet + + - tomcat 服务器插件 + + - 直接依赖 ssm_service(对 ssm_service 模块执行 install 指令,将其安装到本地仓库) + + - 间接依赖 ssm_dao、ssm_pojo + + ```xml + <dependencies> + <!--导入资源文件service--> + <dependency> + <groupId>demo</groupId> + <artifactId>ssm_service</artifactId> + <version>1.0-SNAPSHOT</version> + </dependency> + <!--springmvc环境--> + <!--jackson相关坐标3个--> + <!--servlet环境--> + </dependencies> + <build> + <!--设置插件--> + <plugins> + <!--具体的插件配置--> + <plugin> + </plugin> + </plugins> + </build> + ``` + + - 修改 web.xml 配置文件中加载 Spring 环境的配置文件名称,使用*通配,加载所有 applicationContext- 开始的配置文件: + + ```xml + <!--加载配置文件--> + <context-param> + <param-name>contextConfigLocation</param-name> + <param-value>classpath*:applicationContext-*.xml</param-value> + </context-param> + ``` + + - spring-mvc + + ```xml + <mvc:annotation-driven/><context:component-scan base-package="controller"/> + ``` + + + +*** + + + +### 聚合 + +作用:聚合用于快速构建 Maven 工程,一次性构建多个项目/模块 + +制作方式: + +- 创建一个空模块,打包类型定义为 pom + + ```xml + <packaging>pom</packaging> + ``` + +- 定义当前模块进行构建操作时关联的其他模块名称 + + ```xml + <?xml version="1.0" encoding="UTF-8"?><project xmlns="............"> + <modelVersion>4.0.0</modelVersion> + <groupId>demo</groupId> + <artifactId>ssm</artifactId> + <version>1.0-SNAPSHOT</version> + <!--定义该工程用于构建管理--> + <packaging>pom</packaging> + <!--管理的工程列表--> + <modules> + <!--具体的工程名称--> + <module>../ssm_pojo</module> + <module>../ssm_dao</module> + <module>../ssm_service</module> + <module>../ssm_controller</module> + </modules></project> + ``` + +注意事项:参与聚合操作的模块最终执行顺序与模块间的依赖关系有关,与配置顺序无关 + + + +*** + + + +### 继承 + +作用:通过继承可以实现在子工程中沿用父工程中的配置 + +- Maven 中的继承与 Java 中的继承相似,在子工程中配置继承关系 + +制作方式: + +- 在子工程中声明其父工程坐标与对应的位置 + + ```xml + <!--定义该工程的父工程--> + <parent> + <groupId>com.seazean</groupId> + <artifactId>ssm</artifactId> + <version>1.0-SNAPSHOT</version> + <!--填写父工程的pom文件--> + <relativePath>../ssm/pom.xml</relativePath> + </parent> + ``` + +- 继承依赖的定义:在父工程中定义依赖管理 + + ```xml + <!--声明此处进行依赖管理,版本锁定--> + <dependencyManagement> + <!--具体的依赖--> + <dependencies> + <!--spring环境--> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-context</artifactId> + <version>5.1.9.RELEASE</version> + </dependency> + <!--等等所有--> + </dependencies> + </dependencyManagement> + ``` + +- 继承依赖的使用:在子工程中定义依赖关系,**无需声明依赖版本**,版本参照父工程中依赖的版本 + + ```xml + <dependencies> + <!--spring环境--> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-context</artifactId> + </dependency> + </dependencies> + ``` + +- 继承的资源: + + ```xml + groupId:项目组ID,项目坐标的核心元素 + version:项目版本,项目坐标的核心因素 + description:项目的描述信息 + organization:项目的组织信息 + inceptionYear:项目的创始年份 + url:项目的URL地址 + developers:项目的开发者信息 + contributors:项目的贡献者信息 + distributionManagement:项目的部署配置 + issueManagement:项目的缺陷跟踪系统信息 + ciManagement:项目的持续集成系统信息 + scm:项目的版本控制系统信息 + malilingLists:项目的邮件列表信息 + properties:自定义的Maven属性 + dependencies:项目的依赖配置 + dependencyManagement:项目的依赖管理配置 + repositories:项目的仓库配置 + build:包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等 + reporting:包括项目的报告输出目录配置、报告插件配置等 + ``` + +- 继承与聚合: + + 作用: + + - 聚合用于快速构建项目 + + - 继承用于快速配置 + + 相同点: + + - 聚合与继承的 pom.xml 文件打包方式均为 pom,可以将两种关系制作到同一个 pom 文件中 + + - 聚合与继承均属于设计型模块,并无实际的模块内容 + + 不同点: + + - 聚合是在当前模块中配置关系,聚合可以感知到参与聚合的模块有哪些 + + - 继承是在子模块中配置关系,父模块无法感知哪些子模块继承了自己 + + + +*** + + + +### 属性 + +* 版本统一的重要性: + + ![](https://gitee.com/seazean/images/raw/master/Frame/Maven版本统一的重要性.png) + +* 属性类别: + + 1.自定义属性 2.内置属性 3.Setting属性 4.Java系统属性 5.环境变量属性 + +* 自定义属性: + + 作用:等同于定义变量,方便统一维护 + + 定义格式: + + ```xml + <!--定义自定义属性,放在dependencyManagement上方--> + <properties> + <spring.version>5.1.9.RELEASE</spring.version> + <junit.version>4.12</junit.version> + </properties> + ``` + + - 聚合与继承的 pom.xml 文件打包方式均为 pom,可以将两种关系制作到同一个 pom 文件中 + + - 聚合与继承均属于设计型模块,并无实际的模块内容 + + 调用格式: + + ```xml + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-context</artifactId> + <version>${spring.version}</version> + </dependency> + ``` + +* 内置属性: + + 作用:使用 Maven 内置属性,快速配置 + + 调用格式: + + ```xml + ${project.basedir} or ${project.basedir} <!--../ssm根目录-->${version} or ${project.version} + ``` + + * vresion 是 1.0-SNAPSHOT + + ```xml + <groupId>demo</groupId> + <artifactId>ssm</artifactId> + <version>1.0-SNAPSHOT</version> + ``` + +* Setting 属性 + + - 使用 Maven 配置文件 setting.xml 中的标签属性,用于动态配置 + + 调用格式: + + ```xml + ${settings.localRepository} + ``` + +* Java 系统属性: + + 作用:读取 Java 系统属性 + + 调用格式: + + ``` + ${user.home} + ``` + + 系统属性查询方式 cmd 命令: + + ```sh + mvn help:system + ``` + +* 环境变量属性 + + 作用:使用 Maven 配置文件 setting.xml 中的标签属性,用于动态配置 + + 调用格式: + + ``` + ${env.JAVA_HOME} + ``` + + 环境变量属性查询方式: + + ```sh + mvn help:system + ``` + + + + +*** + + + +### 工程版本 + +SNAPSHOT(快照版本) + +- 项目开发过程中,为方便团队成员合作,解决模块间相互依赖和时时更新的问题,开发者对每个模块进行构建的时候,输出的临时性版本叫快照版本(测试阶段版本) + +- 快照版本会随着开发的进展不断更新 + +RELEASE(发布版本) + +- 项目开发到进入阶段里程碑后,向团队外部发布较为稳定的版本,这种版本所对应的构件文件是稳定的,即便进行功能的后续开发,也不会改变当前发布版本内容,这种版本称为发布版本 + +约定规范: + +- <主版本>.<次版本>.<增量版本>.<里程碑版本> + +- 主版本:表示项目重大架构的变更,如:Spring5 相较于 Spring4 的迭代 + +- 次版本:表示有较大的功能增加和变化,或者全面系统地修复漏洞 + +- 增量版本:表示有重大漏洞的修复 + +- 里程碑版本:表明一个版本的里程碑(版本内部)。这样的版本同下一个正式版本相比,相对来说不是很稳定,有待更多的测试 + +范例: + +- 5.1.9.RELEASE + + + +*** + + + + + +### 资源配置 + +作用:在任意配置文件中加载 pom 文件中定义的属性 + +* 父文件 pom.xml + + ```xml + <properties> + <jdbc.url>jdbc:mysql://192.168.0.137:3306/ssm_db?useSSL=false</jdbc.url></properties> + ``` + +- 开启配置文件加载 pom 属性: + + ```xml + <!--配置资源文件对应的信息--> + <resources> + <resource> + <!--设定配置文件对应的位置目录,支持使用属性动态设定路径--> + <directory>${project.basedir}/src/main/resources</directory> + <!--开启对配置文件的资源加载过滤--> + <filtering>true</filtering> + </resource> + </resources> + ``` + +* properties 文件中调用格式: + + ```properties + jdbc.driver=com.mysql.jdbc.Driverjdbc.url=${jdbc.url} + jdbc.username=rootjdbc.password=123456 + ``` + + + +*** + + + +### 多环境配置 + +* 环境配置 + + ```xml + <!--创建多环境--> + <profiles> + <!--定义具体的环境:生产环境--> + <profile> + <!--定义环境对应的唯一名称--> + <id>pro_env</id> + <!--定义环境中专用的属性值--> + <properties> + <jdbc.url>jdbc:mysql://127.1.1.1:3306/ssm_db</jdbc.url> + </properties> + <!--设置默认启动--> + <activation> + <activeByDefault>true</activeByDefault> + </activation> + </profile> + <!--定义具体的环境:开发环境--> + <profile> + <id>dev_env</id> + …… + </profile> + </profiles> + ``` + +* 加载指定环境 + + 作用:加载指定环境配置 + + 调用格式: + + ```sh + mvn 指令 –P 环境定义id + ``` + + 范例: + + ```sh + mvn install –P pro_env + ``` + + + + +*** + + + +## 跳过测试 + +命令: + +```sh +mvn 指令 –D skipTests +``` + +注意事项:执行的指令生命周期必须包含测试环节 + + + +IEDA 界面: + +![](https://gitee.com/seazean/images/raw/master/Frame/IDEA使用界面操作跳过测试.png) + + + +配置跳过: + +```xml +<plugin> + <!--<groupId>org.apache.maven</groupId>--> + <artifactId>maven-surefire-plugin</artifactId> + <version>2.22.1</version> + <configuration> + <skipTests>true</skipTests><!--设置跳过测试--> + <includes> <!--包含指定的测试用例--> + <include>**/User*Test.java</include> + </includes> + <excludes><!--排除指定的测试用例--> + <exclude>**/User*TestCase.java</exclude> + </excludes> + </configuration> +</plugin> +``` + + + +*** + + + +## 私服 + +### Nexus + +Nexus 是 Sonatype 公司的一款 Maven 私服产品 + +下载地址:https://help.sonatype.com/repomanager3/download + +启动服务器(命令行启动): + +```sh +nexus.exe /run nexus +``` + +访问服务器(默认端口:8081): + +```sh +http://localhost:8081 +``` + +修改基础配置信息 + +- 安装路径下 etc 目录中 nexus-default.properties 文件保存有 nexus 基础配置信息,例如默认访问端口 + +修改服务器运行配置信息 + +- 安装路径下 bin 目录中 nexus.vmoptions 文件保存有 nexus 服务器启动的配置信息,例如默认占用内存空间 + + + +*** + + + +### 资源操作 + +![](https://gitee.com/seazean/images/raw/master/Frame/Maven私服资源获取.png) + + + +仓库分类: + +* 宿主仓库 hosted + * 保存无法从中央仓库获取的资源 + * 自主研发 + * 第三方非开源项目 + +* 代理仓库 proxy + * 代理远程仓库,通过 nexus 访问其他公共仓库,例如中央仓库 + +* 仓库组 group + * 将若干个仓库组成一个群组,简化配置 + * 仓库组不能保存资源,属于设计型仓库 + + + +资源上传,上传资源时提供对应的信息 + +- 保存的位置(宿主仓库) + +- 资源文件 + +- 对应坐标 + + + +*** + + + +### IDEA操作 + +#### 上传下载 + +![](https://gitee.com/seazean/images/raw/master/Frame/IDEA环境中资源上传与下载.png) + + + +*** + + + +#### 访问私服 + +##### 本地访问 + +配置本地仓库访问私服的权限(setting.xml) + +```xml +<servers> + <server> + <id>heima-release</id> + <username>admin</username> + <password>admin</password> + </server> + <server> + <id>heima-snapshots</id> + <username>admin</username> + <password>admin</password> + </server> +</servers> +``` + +配置本地仓库资源来源(setting.xml) + +```xml +<mirrors> + <mirror> + <id>nexus-heima</id> + <mirrorOf>*</mirrorOf> + <url>http://localhost:8081/repository/maven-public/</url> + </mirror> +</mirrors> +``` + + + +*** + + + +##### 工程访问 + +配置当前项目访问私服上传资源的保存位置(pom.xml) + +```xml +<distributionManagement> + <repository> + <id>heima-release</id> + <url>http://localhost:8081/repository/heima-release/</url> + </repository> + <snapshotRepository> + <id>heima-snapshots</id> + <url>http://localhost:8081/repository/heima-snapshots/</url> + </snapshotRepository> +</distributionManagement> +``` + +发布资源到私服命令 + +```sh +mvn deploy +``` + + + + + +*** + + + +## 日志 + +### Log4j + +程序中的日志可以用来记录程序在运行时候的详情,并可以进行永久存储。 + +| | 输出语句 | 日志技术 | +| -------- | -------------------------- | ---------------------------------------- | +| 取消日志 | 需要修改代码,灵活性比较差 | 不需要修改代码,灵活性比较好 | +| 输出位置 | 只能是控制台 | 可以将日志信息写入到文件或者数据库中 | +| 多线程 | 和业务代码处于一个线程中 | 多线程方式记录日志,不影响业务代码的性能 | + +Log4j 是 Apache 的一个开源项目。使用 Log4j,通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。我们可以控制日志信息输送的目的地是控制台、文件等位置,也可以控制每一条日志的输出格式。 + +<img src="https://gitee.com/seazean/images/raw/master/Frame/日志体系结构.png" style="zoom:50%;" /> + + + +*** + + + +### 配置文件 + +配置文件的三个核心: + ++ 配置根 Logger + + + 格式:log4j.rootLogger=日志级别,appenderName1,appenderName2,… + + + 日志级别:常见的五个级别:**DEBUG < INFO < WARN < ERROR < FATAL**(可以自定义) + Log4j规则:只输出级别不低于设定级别的日志信息 + + + appenderName1:指定日志信息要输出地址。可以同时指定多个输出目的地,用逗号隔开: + + 例如:log4j.rootLogger=INFO,ca,fa + ++ Appenders(输出源):日志要输出的地方,如控制台(Console)、文件(Files)等 + + + Appenders 取值: + + org.apache.log4j.ConsoleAppender(控制台) + + org.apache.log4j.FileAppender(文件) + + + ConsoleAppender 常用参数 + + `ImmediateFlush=true`:表示所有消息都会被立即输出,设为 false 则不输出,默认值是 true + + `Target=System.err`:默认值是 System.out + + FileAppender常用的选项 + + `ImmediateFlush=true`:表示所有消息都会被立即输出。设为 false 则不输出,默认值是 true + + + `Append=false`:true 表示将消息添加到指定文件中,原来的消息不覆盖。默认值是 true + + + `File=E:/logs/logging.log4j`:指定消息输出到 logging.log4j 文件中 + ++ Layouts (布局):日志输出的格式,常用的布局管理器: + + + org.apache.log4j.PatternLayout(可以灵活地指定布局模式) + ++ org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串) + ++ org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等信息) + ++ PatternLayout 常用的选项 + <img src="https://gitee.com/seazean/images/raw/master/Frame/日志-PatternLayout常用的选项.png" style="zoom:80%;" /> + + + +*** + + + +### 日志应用 + +* log4j 的配置文件,名字为 log4j.properties, 放在 src 根目录下 + + ```properties + log4j.rootLogger=debug,my,fileAppender + + ### direct log messages to my ### + log4j.appender.my=org.apache.log4j.ConsoleAppender + log4j.appender.my.ImmediateFlush = true + log4j.appender.my.Target=System.out + log4j.appender.my.layout=org.apache.log4j.PatternLayout + log4j.appender.my.layout.ConversionPattern=%d %t %5p %c{1}:%L - %m%n + + # fileAppender演示 + log4j.appender.fileAppender=org.apache.log4j.FileAppender + log4j.appender.fileAppender.ImmediateFlush = true + log4j.appender.fileAppender.Append=true + log4j.appender.fileAppender.File=E:/log4j-log.log + log4j.appender.fileAppender.layout=org.apache.log4j.PatternLayout + log4j.appender.fileAppender.layout.ConversionPattern=%d %5p %c{1}:%L - %m%n + ``` + +* 测试类 + + ```java + // 测试类 + public class Log4JTest01 { + //使用log4j的api来获取日志的对象 + //弊端:如果以后我们更换日志的实现类,那么下面的代码就需要跟着改 + //不推荐使用 + //private static final Logger LOGGER = Logger.getLogger(Log4JTest01.class); + //使用slf4j里面的api来获取日志的对象 + //好处:如果以后我们更换日志的实现类,那么下面的代码不需要跟着修改 + //推荐使用 + private static final Logger LOGGER = LoggerFactory.getLogger(Log4JTest01.class); + public static void main(String[] args) { + //1.导入jar包 + //2.编写配置文件 + //3.在代码中获取日志的对象 + //4.按照日志级别设置日志信息 + LOGGER.debug("debug级别的日志"); + LOGGER.info("info级别的日志"); + LOGGER.warn("warn级别的日志"); + LOGGER.error("error级别的日志"); + } + } + ``` + + + + + +*** + + + +# Netty + +## 基本介绍 + +Netty 是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端 + +Netty 官网:https://netty.io/ + +Netty 的对 JDK 自带的 NIO 的 API 进行封装,解决上述问题,主要特点有: + +- 设计优雅,适用于各种传输类型的统一 API, 阻塞和非阻塞 Socket 基于灵活且可扩展的事件模型 +- 使用方便,详细记录的 Javadoc、用户指南和示例,没有其他依赖项 +- 高性能,吞吐量更高,延迟更低,减少资源消耗,最小化不必要的内存复制 +- 安全,完整的 SSL/TLS 和 StartTLS 支持 + +Netty 的功能特性: + +* 传输服务:支持 BIO 和 NIO +* 容器集成:支持 OSGI、JBossMC、Spring、Guice 容器 +* 协议支持:HTTP、Protobuf、二进制、文本、WebSocket 等一系列协议都支持,也支持通过实行编码解码逻辑来实现自定义协议 +* Core 核心:可扩展事件模型、通用通信 API、支持零拷贝的 ByteBuf 缓冲对象 + +<img src="https://gitee.com/seazean/images/raw/master/Frame/Netty-功能特性.png" style="zoom:50%;" /> + + + + + +*** + + + +## 线程模型 + +### 阻塞模型 + +传统阻塞型 I/O 模式,每个连接都需要独立的线程完成数据的输入,业务处理,数据返回 + +<img src="https://gitee.com/seazean/images/raw/master/Frame/Netty-传统阻塞IO服务模型.png" style="zoom:50%;" /> + +模型缺点: + +- 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大 +- 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 read 操作上,造成线程资源浪费 + + + +*** + + + +### Reactor + +#### 设计思想 + +Reactor 模式,通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。 服务端程序处理传入多路请求,并将它们同步分派给对应的处理线程,Reactor 模式也叫 Dispatcher 模式,即 I/O 多路复用统一监听事件,收到事件后分发(Dispatch给某进程),是编写高性能网络服务器的必备技术之一 + +I/O 复用结合线程池,就是 Reactor 模式基本设计思想: + +<img src="https://gitee.com/seazean/images/raw/master/Frame/Netty-Reactor模型.png" style="zoom: 50%;" /> + +Reactor 模式关键组成: + +- Reactor:在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 I/O 事件做出反应 +- Handlers:处理程序执行 I/O 事件要完成的实际事件,Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作 + +Reactor 模式具有如下的优点: + +- 响应快,不必为单个同步时间所阻塞,虽然 Reactor 本身依然是同步的 +- 编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销 +- 可扩展性,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源 +- 可复用性,Reactor 模型本身与具体事件处理逻辑无关,具有很高的复用性 + +根据Reactor的数量和处理资源池线程的数量不同,有3种典型的实现: + +- 单 Reactor 单线程 +- 单 Reactor 多线程 +- 主从 Reactor 多线程 + + + +参考文章:https://www.jianshu.com/p/2965fca6bb8f + + + +*** + + + +#### 单R单线程 + +Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 dispatch 进行分发: + +* 如果是建立连接请求事件,则由 Acceptor 通过 accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后的后续业务处理 + +* 如果不是建立连接事件,则 Reactor 会分发给连接对应的 Handler 来响应,Handler 会完成 read、业务处理、send 的完整业务流程 + +<img src="https://gitee.com/seazean/images/raw/master/Frame/Netty-单Reactor单线程.png" style="zoom:50%;" /> + +模型优点:模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成 + +模型缺点: + +* 性能问题:只有一个线程,无法发挥多核 CPU 的性能,Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈 +* 可靠性问题:线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障 + +使用场景:客户端的数量有限,业务处理非常快速,比如 Redis,业务处理的时间复杂度 O(1) + + + +*** + + + +#### 单R多线程 + +执行流程通同单 Reactor 单线程,不同的是: + +* Handler 只负责响应事件,不做具体业务处理,通过 read 读取数据后,会分发给后面的 Worker 线程池进行业务处理 + +* Worker 线程池会分配独立的线程完成真正的业务处理,将响应结果发给 Handler 进行处理,最后由 Handler 收到响应结果后通过 send 将响应结果返回给 Client + +<img src="https://gitee.com/seazean/images/raw/master/Frame/Netty-单Reactor多线程.png" style="zoom:50%;" /> + +模型优点:可以充分利用多核CPU的处理能力 + +模型缺点: + +* 多线程数据共享和访问比较复杂 +* Reactor 承担所有事件的监听和响应,在单线程中运行,高并发场景下容易成为性能瓶颈 + + + +*** + + + +#### 主从模型 + +采用多个 Reactor ,执行流程: + +* Reactor 主线程 MainReactor 通过 select 监控建立连接事件,收到事件后通过 Acceptor 接收,处理建立连接事件,处理完成后 MainReactor 会将连接分配给 Reactor 子线程的 SubReactor 处理 + +* SubReactor 将连接加入连接队列进行监听,并创建一个 Handler 用于处理该连接的事件,当有新的事件发生时,SubReactor 会调用连接对应的 Handler 进行响应 + +* Handler 通过 read 读取数据后,会分发给 Worker 线程池进行业务处理 + +* Worker 线程池会分配独立的线程完成真正的业务处理,将响应结果发给 Handler 进行处理,最后由 Handler 收到响应结果后通过 send 将响应结果返回给 Client + +<img src="https://gitee.com/seazean/images/raw/master/Frame/Netty-主从Reactor多线程.png" style="zoom: 50%;" /> + +模型优点 + +- 父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理 +- 父线程与子线程的数据交互简单,Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据 + +使用场景:Nginx 主从 Reactor 多进程模型,Memcached 主从多线程,Netty 主从多线程模型的支持 + + + +*** + + + +### Proactor + +Reactor 模式中,Reactor 等待某个事件的操作状态发生变化(文件描述符可读写,socket 可读写),然后把事件传递给事先注册的 Handler 来做实际的读写操作,其中的读写操作都需要应用程序同步操作,所以 Reactor 是非阻塞同步网络模型(NIO) + +把 I/O操作改为异步,交给操作系统来完成就能进一步提升性能,这就是异步网络模型 Proactor(AIO): + +<img src="https://gitee.com/seazean/images/raw/master/Frame/Netty-Proactor模型.png" style="zoom:50%;" /> + +工作流程: + +* ProactorInitiator 创建 Proactor 和 Handler 对象,并将 Proactor 和 Handler 通过 Asynchronous Operation Processor(AsyOptProcessor)注册到内核 +* AsyOptProcessor 处理注册请求,并处理 I/O 操作,完成I/O后通知 Proactor +* Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理,最后由 Handler 完成业务处理 + +对比 Reactor:Reactor 在事件发生时就通知事先注册的处理器(读写在应用程序线程中处理完成);Proactor是在事件发生时基于异步 I/O 完成读写操作(内核完成),I/O 完成后才回调应用程序的处理器进行业务处理 + +模式优点:异步 I/O 更加充分发挥 DMA(Direct Memory Access 直接内存存取)的优势 + +模式缺点: + +* 编程复杂性,由于异步操作流程的事件的初始化和事件完成在时间和空间上都是相互分离的,因此开发异步应用程序更加复杂,应用程序还可能因为反向的流控而变得更加难以调试 +* 内存使用,缓冲区在读或写操作的时间段内必须保持住,可能造成持续的不确定性,并且每个并发操作都要求有独立的缓存,Reactor 模式在 socket 准备好读或写之前是不要求开辟缓存的 +* 操作系统支持,Windows 下通过 IOCP 实现了真正的异步 I/O,而在 Linux 系统下,Linux2.6 才引入异步 I/O,目前还不完善,所以在 Linux 下实现高并发网络编程都是以 Reactor 模型为主 + + + +**** + + + +### Netty + +Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty 的工作架构图: + +<img src="https://gitee.com/seazean/images/raw/master/Frame/Netty-工作模型.png" style="zoom:50%;" /> + +工作流程: + +1. Netty 抽象出两组线程池 BossGroup 专门负责接收客户端的连接,WorkerGroup 专门负责网络的读写 + +2. BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup,该 Group 相当于一个事件循环组,含有多个事件循环,每一个事件循环是 NioEventLoop,所以可以有多个线程 + +3. NioEventLoop 表示一个不断循环的执行处理任务的线程,每个 NioEventLoop 都有一个 Selector,用于监听绑定在其上的 socket 的网络通讯 + +4. 每个 BossNioEventLoop 循环执行的步骤: + + - 轮询 accept 事件 + - 处理 accept 事件,与 client 建立连接,生成 NioScocketChannel,并将其注册到某个 Worker 中的某个 NioEventLoop 上的 Selector + - 处理任务队列的任务,即 runAllTasks + +5. 每个 Worker NioEventLoop 循环执行的步骤: + + - 轮询 read、write 事件 + - 处理 I/O 事件,即 read,write 事件,在对应 NioScocketChannel 处理 + - 处理任务队列的任务,即 runAllTasks + +6. 每个 Worker NioEventLoop 处理业务时,会使用 pipeline(管道),pipeline 中包含了 channel,即通过 pipeline 可以获取到对应通道,管道中维护了很多的处理器 Handler + + <img src="https://gitee.com/seazean/images/raw/master/Frame/Netty-Channel与Pipeline.png" style="zoom: 50%;" /> + + + +参考文章:https://www.jianshu.com/p/2965fca6bb8f + + + +*** + + + +## 基本操作 + +### 服务端 + +* pom.xml + + ```xml + <dependency> + <groupId>io.netty</groupId> + <artifactId>netty-all</artifactId> + <version>4.1.20.Final</version> + </dependency> + ``` + +* NettyServer.java + + ```java + public class NettyServer { + public static void main(String[] args) { + //创建 BossGroup 和 WorkerGroup,两个都是无限循环 + //bossGroup 只是处理连接请求,真正的和客户端业务处理会交给 workerGroup完成 + //两个 Group 含有的子线程(NioEventLoop)的个数为默认为 cpu核数 * 2 + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); //8 + + try { + //创建服务器端的启动对象,配置参数 + ServerBootstrap bootstrap = new ServerBootstrap(); + //使用链式编程来进行设置,设置两个线程组 + bootstrap.group(bossGroup, workerGroup) + //使用NioSocketChannel 作为服务器的通道实现 + .channel(NioServerSocketChannel.class) + // 设置线程队列得到连接个数 + .option(ChannelOption.SO_BACKLOG, 128) + //设置保持活动连接状态 + .childOption(ChannelOption.SO_KEEPALIVE, true) + // 该 handler对应 bossGroup , childHandler 对应 workerGroup + //.handler(null) + //创建一个通道初始化对象(匿名对象) + .childHandler(new NettyServerInitializer()); + + System.out.println(".....Server is ready..."); + + //绑定一个端口并且同步, 生成了一个ChannelFuture对象,启动服务器 + ChannelFuture channelFuture = bootstrap.bind(6668).sync(); + + //给cf 注册监听器,监控我们关心的事件 + channelFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (channelFuture.isSuccess()) { + System.out.println("listening on port 6668 succeeded"); + } else { + System.out.println("listening on port 6668 failed"); + } + } + }); + + //对关闭通道进行监听 + channelFuture.channel().closeFuture().sync(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } + } + ``` + +* NettyServerInitializer.java + + ```java + public class NettyServerInitializer extends ChannelInitializer<SocketChannel> { + //给pipeline设置处理器 + @Override + protected void initChannel(SocketChannel ch) throws Exception { + // 可以使用一个集合管理所有的SocketChannel + // 推送消息时直接拿到对应的 NIOEventLoop 的 taskQueue + System.out.println("Client socket channel hashcode : " + ch.hashCode()); + // 给 workerGroup 的 EventLoop 对应的管道设置处理器 + ch.pipeline().addLast(new NettyServerHandler()); + } + } + ``` + +* NettyServerHandler.java + + ```java + //自定义一个Handler,需要继承netty,规定好的某个HandlerAdapter + public class NettyServerHandler extends ChannelInboundHandlerAdapter { + /** + * 读取数据的方法 + * @param ctx 上下文对象,含有管道pipeline、通道channel、地址 + * @param msg 客户端发送的数据 + * @throws Exception + */ + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + System.out.println("server read thread " + Thread.currentThread().getName()); + System.out.println("server ctx = " + ctx); + System.out.println("the relationship between channels and pipeline"); + Channel channel = ctx.channel(); + ChannelPipeline pipeline = ctx.pipeline();//本质是一个双向链表 + + //将msg转成ByteBuf,ByteBuf是Netty提供的,不是NIO的ByteBuffer + ByteBuf buf = (ByteBuf) msg; + sout("Client send data is : " + buf.toString(CharsetUtil.UTF_8)); + sout("Client address is : " + ctx.channel().remoteAddress()); + + } + + //数据读取完毕执行该方法 + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + // writeAndFlush 是 write + flush,写入到缓存并刷新,对发送的数据进行编码 + ctx.writeAndFlush(Unpooled.copiedBuffer("Hello client", CharsetUtil.UTF_8)); + } + + //处理异常,一般是需要关闭通道 + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Thro,wable cause) throws Exception { + cause.printStackTrace(); + ctx.close(); + } + } + ``` + + + +*** + + + +### 客户端 + +* NettyClient.java + + ```java + public class NettyClient { + public static void main(String[] args){ + //客户端需要一个事件循环组 + NioEventLoopGroup group = new NioEventLoopGroup(); + try { + //创建客户端启动对象,客户端使用的不是 ServerBootstrap 而是 Bootstrap + Bootstrap bootstrap = new Bootstrap(); + //设置相关参数 + bootstrap.group(group) //设置线程组 + //使用NioSocketChannel客户端通道,反射实现 + .channel(NioSocketChannel.class) + .handler(new NettyClientInitializer()); + + System.out.println("...Client is ok..."); + + //启动客户端连接服务器端 + ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync(); + //对关闭通道进行监听,只是监听不是关闭 + channelFuture.channel().closeFuture().sync(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + group.shutdownGracefully(); + } + } + } + ``` + +* NettyClientInitializer.java + + ```java + public class NettyClientInitializer extends ChannelInitializer<SocketChannel> { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new NettyClientHandler()); + } + } + ``` + +* NettyClientHandler.java + + ```java + public class NettyClientHandler extends ChannelInboundHandlerAdapter { + // 通道就绪就会触发该方法 + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + System.out.println("client : " + ctx); + ctx.writeAndFlush(Unpooled.copiedBuffer("hello server", CharsetUtil.UTF_8)); + } + + // 通道有读取事件时,会触发 + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + ByteBuf buf = (ByteBuf) msg; + System.out.println("Server reply : " + buf.toString(CharsetUtil.UTF_8)); + System.out.println("Server address : " + ctx.channel().remoteAddress()); + } + + //异常处理 + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + cause.printStackTrace(); + ctx.close(); + } + } + ``` + + + + + +*** + + + + + + + diff --git a/Java.md b/Java.md index 2662eee..dc3c5b0 100644 --- a/Java.md +++ b/Java.md @@ -95,8 +95,8 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, - char 类型是一个单一的 16 位两个字节的 Unicode 字符 - 最小值是 **`\u0000`**(即为 0) - 最大值是 **`\uffff`**(即为 65535) -- char 数据类型可以储存任何字符 -- 例子:`char c = 'A';` `char c = '张'` +- char 数据类型可以**存储任何字符** +- 例子:`char c = 'A'`,`char c = '张'` 上下转型 From 2b3818b246c4e85c8eb8789101d1e747369dc46f Mon Sep 17 00:00:00 2001 From: Seazean <imseazean@gmail.com> Date: Sun, 19 Sep 2021 11:26:04 +0800 Subject: [PATCH 011/122] Update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9691b30..9952e97 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ 内容说明: * DB:MySQL、Redis +* Frame:Maven、Netty * Java:JavaSE、JVM、Algorithm、Design Pattern * Prog:Concurrent、Network Programming * SSM:MyBatis、Spring、SpringMVC、SpringBoot From 44e88c64ea0b2fa964dfb831173f0a9e4ebdfe13 Mon Sep 17 00:00:00 2001 From: Seazean <imseazean@gmail.com> Date: Mon, 20 Sep 2021 23:17:51 +0800 Subject: [PATCH 012/122] Update Java Notes --- Frame.md | 862 ++++++++++++++++++++++++++++++++++++++++++++----------- Java.md | 26 +- Prog.md | 2 + 3 files changed, 706 insertions(+), 184 deletions(-) diff --git a/Frame.md b/Frame.md index ad34333..95cfa19 100644 --- a/Frame.md +++ b/Frame.md @@ -1396,15 +1396,21 @@ Log4j 是 Apache 的一个开源项目。使用 Log4j,通过一个配置文件 + + *** + + + + # Netty ## 基本介绍 -Netty 是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端 +Netty 是一个异步事件驱动的网络应用程序框架,用于快速开发可维护、高性能的网络服务器和客户端 Netty 官网:https://netty.io/ @@ -1432,6 +1438,8 @@ Netty 的功能特性: + + ## 线程模型 ### 阻塞模型 @@ -1447,6 +1455,10 @@ Netty 的功能特性: +参考文章:https://www.jianshu.com/p/2965fca6bb8f + + + *** @@ -1455,9 +1467,9 @@ Netty 的功能特性: #### 设计思想 -Reactor 模式,通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。 服务端程序处理传入多路请求,并将它们同步分派给对应的处理线程,Reactor 模式也叫 Dispatcher 模式,即 I/O 多路复用统一监听事件,收到事件后分发(Dispatch给某进程),是编写高性能网络服务器的必备技术之一 +Reactor 模式,通过一个或多个输入同时传递给服务处理器的事件驱动处理模式。 服务端程序处理传入的多路请求,并将它们同步分派给对应的处理线程,Reactor 模式也叫 Dispatcher 模式,即 I/O 多路复用统一监听事件,收到事件后分发(Dispatch 给某线程) -I/O 复用结合线程池,就是 Reactor 模式基本设计思想: +**I/O 复用结合线程池**,就是 Reactor 模式基本设计思想: <img src="https://gitee.com/seazean/images/raw/master/Frame/Netty-Reactor模型.png" style="zoom: 50%;" /> @@ -1481,10 +1493,6 @@ Reactor 模式具有如下的优点: -参考文章:https://www.jianshu.com/p/2965fca6bb8f - - - *** @@ -1495,7 +1503,7 @@ Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 * 如果是建立连接请求事件,则由 Acceptor 通过 accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后的后续业务处理 -* 如果不是建立连接事件,则 Reactor 会分发给连接对应的 Handler 来响应,Handler 会完成 read、业务处理、send 的完整业务流程 +* 如果不是建立连接事件,则 Reactor 会分发给连接对应的 Handler 来响应,Handler 会完成 read、业务处理、send 的完整流程 <img src="https://gitee.com/seazean/images/raw/master/Frame/Netty-单Reactor单线程.png" style="zoom:50%;" /> @@ -1541,7 +1549,7 @@ Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 采用多个 Reactor ,执行流程: -* Reactor 主线程 MainReactor 通过 select 监控建立连接事件,收到事件后通过 Acceptor 接收,处理建立连接事件,处理完成后 MainReactor 会将连接分配给 Reactor 子线程的 SubReactor 处理 +* Reactor 主线程 MainReactor 通过 select 监控建立连接事件,收到事件后通过 Acceptor 接收,处理建立连接事件,处理完成后 MainReactor 会将连接分配给 Reactor 子线程的 SubReactor(有多个)处理 * SubReactor 将连接加入连接队列进行监听,并创建一个 Handler 用于处理该连接的事件,当有新的事件发生时,SubReactor 会调用连接对应的 Handler 进行响应 @@ -1568,7 +1576,7 @@ Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 Reactor 模式中,Reactor 等待某个事件的操作状态发生变化(文件描述符可读写,socket 可读写),然后把事件传递给事先注册的 Handler 来做实际的读写操作,其中的读写操作都需要应用程序同步操作,所以 Reactor 是非阻塞同步网络模型(NIO) -把 I/O操作改为异步,交给操作系统来完成就能进一步提升性能,这就是异步网络模型 Proactor(AIO): +把 I/O 操作改为异步,交给操作系统来完成就能进一步提升性能,这就是异步网络模型 Proactor(AIO): <img src="https://gitee.com/seazean/images/raw/master/Frame/Netty-Proactor模型.png" style="zoom:50%;" /> @@ -1606,18 +1614,18 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty 2. BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup,该 Group 相当于一个事件循环组,含有多个事件循环,每一个事件循环是 NioEventLoop,所以可以有多个线程 -3. NioEventLoop 表示一个不断循环的执行处理任务的线程,每个 NioEventLoop 都有一个 Selector,用于监听绑定在其上的 socket 的网络通讯 +3. NioEventLoop 表示一个循环处理任务的线程,每个 NioEventLoop 都有一个 Selector,用于监听绑定在其上的 socket 的网络通讯 -4. 每个 BossNioEventLoop 循环执行的步骤: +4. 每个 Boss NioEventLoop 循环执行的步骤: - 轮询 accept 事件 - - 处理 accept 事件,与 client 建立连接,生成 NioScocketChannel,并将其注册到某个 Worker 中的某个 NioEventLoop 上的 Selector + - 处理 accept 事件,与 client 建立连接,生成 NioScocketChannel,并将其**注册到某个 Worker 中**的某个 NioEventLoop 上的 Selector,连接就与 NioEventLoop 绑定 - 处理任务队列的任务,即 runAllTasks 5. 每个 Worker NioEventLoop 循环执行的步骤: - 轮询 read、write 事件 - - 处理 I/O 事件,即 read,write 事件,在对应 NioScocketChannel 处理 + - 处理 I/O 事件,即 read,write 事件,在对应 NioSocketChannel 处理 - 处理任务队列的任务,即 runAllTasks 6. 每个 Worker NioEventLoop 处理业务时,会使用 pipeline(管道),pipeline 中包含了 channel,即通过 pipeline 可以获取到对应通道,管道中维护了很多的处理器 Handler @@ -1626,17 +1634,23 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty -参考文章:https://www.jianshu.com/p/2965fca6bb8f - *** -## 基本操作 -### 服务端 + +## 基本实现 + +开发简单的服务器端和客户端,基本介绍: + +* channel 理解为数据的通道,把 msg 理解为流动的数据,最开始输入是 ByteBuf,但经过 pipeline 的加工,会变成其它类型对象,最后输出又变成 ByteBuf +* handler 理解为数据的处理工序,pipeline 负责发布事件传播给每个 handler,handler 对自己感兴趣的事件进行处理(重写了相应事件处理方法),分 Inbound 和 Outbound 两类 +* eventLoop 理解为处理数据的执行者,既可以执行 IO 操作,也可以进行任务处理。每个执行者有任务队列,队列里可以堆放多个 channel 的待处理任务,任务分为普通任务、定时任务。按照 pipeline 顺序,依次按照 handler 的规划(代码)处理数据 + +代码实现: * pom.xml @@ -1648,207 +1662,715 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty </dependency> ``` -* NettyServer.java + +* Server.java ```java - public class NettyServer { + public class HelloServer { public static void main(String[] args) { - //创建 BossGroup 和 WorkerGroup,两个都是无限循环 - //bossGroup 只是处理连接请求,真正的和客户端业务处理会交给 workerGroup完成 - //两个 Group 含有的子线程(NioEventLoop)的个数为默认为 cpu核数 * 2 - EventLoopGroup bossGroup = new NioEventLoopGroup(1); - EventLoopGroup workerGroup = new NioEventLoopGroup(); //8 - - try { - //创建服务器端的启动对象,配置参数 - ServerBootstrap bootstrap = new ServerBootstrap(); - //使用链式编程来进行设置,设置两个线程组 - bootstrap.group(bossGroup, workerGroup) - //使用NioSocketChannel 作为服务器的通道实现 - .channel(NioServerSocketChannel.class) - // 设置线程队列得到连接个数 - .option(ChannelOption.SO_BACKLOG, 128) - //设置保持活动连接状态 - .childOption(ChannelOption.SO_KEEPALIVE, true) - // 该 handler对应 bossGroup , childHandler 对应 workerGroup - //.handler(null) - //创建一个通道初始化对象(匿名对象) - .childHandler(new NettyServerInitializer()); - - System.out.println(".....Server is ready..."); - - //绑定一个端口并且同步, 生成了一个ChannelFuture对象,启动服务器 - ChannelFuture channelFuture = bootstrap.bind(6668).sync(); - - //给cf 注册监听器,监控我们关心的事件 - channelFuture.addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - if (channelFuture.isSuccess()) { - System.out.println("listening on port 6668 succeeded"); - } else { - System.out.println("listening on port 6668 failed"); + EventLoopGroup boss = new NioEventLoopGroup(); + EventLoopGroup worker = new NioEventLoopGroup(2); + // 1. 启动器,负责组装 netty 组件,启动服务器 + new ServerBootstrap() + // 2. 线程组,boss 只负责【处理 accept 事件】, worker 只【负责 channel 上的读写】 + .group(boss, worker) + // 3. 选择服务器的 ServerSocketChannel 实现 + .channel(NioServerSocketChannel.class) + // 4. boss 负责处理连接,worker(child) 负责处理读写,决定了能执行哪些操作(handler) + .childHandler(new ChannelInitializer<NioSocketChannel>() { + // 5. channel 代表和客户端进行数据读写的通道 Initializer 初始化,负责添加别的 handler + // 7. 连接建立后,执行初始化方法 + @Override + protected void initChannel(NioSocketChannel ch) throws Exception { + // 添加具体的 handler + ch.pipeline().addLast(new StringDecoder());// 将 ByteBuf 转成字符串 + ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { // 自定义 handler + // 读事件 + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + // 打印转换好的字符串 + System.out.println(msg); + } + }); } - } - }); - - //对关闭通道进行监听 - channelFuture.channel().closeFuture().sync(); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - bossGroup.shutdownGracefully(); - workerGroup.shutdownGracefully(); - } + }) + // 6. 绑定监听端口 + .bind(8080); } } ``` -* NettyServerInitializer.java +* Client.java ```java - public class NettyServerInitializer extends ChannelInitializer<SocketChannel> { - //给pipeline设置处理器 - @Override - protected void initChannel(SocketChannel ch) throws Exception { - // 可以使用一个集合管理所有的SocketChannel - // 推送消息时直接拿到对应的 NIOEventLoop 的 taskQueue - System.out.println("Client socket channel hashcode : " + ch.hashCode()); - // 给 workerGroup 的 EventLoop 对应的管道设置处理器 - ch.pipeline().addLast(new NettyServerHandler()); + public class HelloClient { + public static void main(String[] args) throws InterruptedException { + // 1. 创建启动器类 + new Bootstrap() + // 2. 添加 EventLoop + .group(new NioEventLoopGroup()) + // 3. 选择客户端 channel 实现 + .channel(NioSocketChannel.class) + // 4. 添加处理器 + .handler(new ChannelInitializer<NioSocketChannel>() { + // 4.1 连接建立后被调用 + @Override + protected void initChannel(NioSocketChannel ch) throws Exception { + // 将 Hello World 转为 ByteBuf + ch.pipeline().addLast(new StringEncoder()); + } + }) + // 5. 连接到服务器,然后调用 4.1 + .connect(new InetSocketAddress("127.0.0.1",8080)) + // 6. 阻塞方法,直到连接建立 + .sync() + // 7. 代表连接对象 + .channel() + // 8. 向服务器发送数据 + .writeAndFlush("Hello World"); } } ``` -* NettyServerHandler.java - ```java - //自定义一个Handler,需要继承netty,规定好的某个HandlerAdapter - public class NettyServerHandler extends ChannelInboundHandlerAdapter { - /** - * 读取数据的方法 - * @param ctx 上下文对象,含有管道pipeline、通道channel、地址 - * @param msg 客户端发送的数据 - * @throws Exception - */ - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - System.out.println("server read thread " + Thread.currentThread().getName()); - System.out.println("server ctx = " + ctx); - System.out.println("the relationship between channels and pipeline"); - Channel channel = ctx.channel(); - ChannelPipeline pipeline = ctx.pipeline();//本质是一个双向链表 - - //将msg转成ByteBuf,ByteBuf是Netty提供的,不是NIO的ByteBuffer - ByteBuf buf = (ByteBuf) msg; - sout("Client send data is : " + buf.toString(CharsetUtil.UTF_8)); - sout("Client address is : " + ctx.channel().remoteAddress()); - - } - - //数据读取完毕执行该方法 - @Override - public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { - // writeAndFlush 是 write + flush,写入到缓存并刷新,对发送的数据进行编码 - ctx.writeAndFlush(Unpooled.copiedBuffer("Hello client", CharsetUtil.UTF_8)); - } - - //处理异常,一般是需要关闭通道 - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Thro,wable cause) throws Exception { - cause.printStackTrace(); - ctx.close(); - } - } - ``` - + + + +**** + + + + + +## 组件介绍 + +### EventLoop + +#### 基本介绍 + +事件循环对象 EventLoop,本质是一个单线程执行器(同时维护了一个 selector),里面有 run 方法处理 Channel 上源源不断的 IO 事件 + +事件循环组 EventLoopGroup 是一组 EventLoop,Channel 会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop,后续这个 Channel 上的 IO 事件都由此 EventLoop 来处理,保证了事件处理时的线程安全 + +EventLoopGroup 类 API: + +* `EventLoop next()`:获取集合中下一个 EventLoop,EventLoopGroup 实现了 Iterable 接口提供遍历 EventLoop 的能力 +* `Future<?> shutdownGracefully()`:优雅关闭的方法,会首先切换 `EventLoopGroup` 到关闭状态从而拒绝新的任务的加入,然后在任务队列的任务都处理完成后,停止线程的运行。从而确保整体应用是在正常有序的状态下退出的 + +* `<T> Future<T> submit(Callable<T> task)`:提交任务 +* `ScheduledFuture<?> scheduleWithFixedDelay`:提交定时任务 + + *** -### 客户端 +#### 任务传递 + +把要调用的代码封装为一个任务对象,由下一个 handler 的线程来调用 + +```java +public class EventLoopServer { + public static void main(String[] args) { + EventLoopGroup group = new DefaultEventLoopGroup(); + new ServerBootstrap() + .group(new NioEventLoopGroup(), new NioEventLoopGroup(2)) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer<NioSocketChannel>() { + @Override + protected void initChannel(NioSocketChannel ch) { + ch.pipeline().addLast("handler1", new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf buf = (ByteBuf) msg; + log.debug(buf.toString(Charset.defaultCharset())); + ctx.fireChannelRead(msg); // 让消息【传递】给下一个 handler + } + }).addLast(group, "handler2", new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf buf = (ByteBuf) msg; + log.debug(buf.toString(Charset.defaultCharset())); + + } + }); + } + }) + .bind(8080); + } +} +``` + +源码分析: + +```java +static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) { + final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next); + EventExecutor executor = next.executor(); + // 下一个 handler 的事件循环是否与当前的事件循环是同一个线程 + if (executor.inEventLoop()) { + // 是,直接调用 + next.invokeChannelRead(m); + } else { + // 不是,将要执行的代码作为任务提交给下一个 handler 处理 + executor.execute(new Runnable() { + @Override + public void run() { + next.invokeChannelRead(m); + } + }); + } +} +``` + + + + + +**** + + + +### Channel + +#### 基本使用 + +Channel 类 API: + +* `ChannelFuture close()`:关闭通道 +* `ChannelPipeline pipeline()`:添加处理器 +* `ChannelFuture write(Object msg)`:数据写入缓冲区 +* `ChannelFuture writeAndFlush(Object msg)`:数据写入缓冲区并且刷出 + +ChannelFuture 类 API: + +* `ChannelFuture sync()`:同步阻塞等待连接成功 +* `ChannelFuture addListener(GenericFutureListener listener)`:异步等待 + +代码实现: + +* connect 方法是异步的,不等连接建立完成就返回,因此 channelFuture 对象中不能立刻获得到正确的 Channel 对象,需要等待 +* 连接未建立 channel 打印为 `[id: 0x2e1884dd]`;建立成功打印为 `[id: 0x2e1884dd, L:/127.0.0.1:57191 - R:/127.0.0.1:8080]` + +```java +public class ChannelClient { + public static void main(String[] args) throws InterruptedException { + ChannelFuture channelFuture = new Bootstrap() + .group(new NioEventLoopGroup()) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer<NioSocketChannel>() { + @Override + protected void initChannel(NioSocketChannel ch) throws Exception { + ch.pipeline().addLast(new StringEncoder()); + } + }) + // 1. 连接服务器,【异步非阻塞】,main 调用 connect 方法,真正执行连接的是 nio 线程 + .connect(new InetSocketAddress("127.0.0.1", 8080)); + // 2.1 使用 sync 方法【同步】处理结果,阻塞当前线程,直到 nio 线程连接建立完毕 + channelFuture.sync(); + Channel channel = channelFuture.channel(); + System.out.println(channel); // 【打印】 + // 向服务器发送数据 + channel.writeAndFlush("hello world"); + +**************************************************************************************二选一 + // 2.2 使用 addListener 方法【异步】处理结果 + channelFuture.addListener(new ChannelFutureListener() { + @Override + // nio 线程连接建立好以后,回调该方法 + public void operationComplete(ChannelFuture future) throws Exception { + Channel channel = future.channel(); + channel.writeAndFlush("hello, world"); + } + }); + } +} +``` + + + +*** + + + +#### 关闭操作 + +关闭 EventLoopGroup 的运行,分为同步关闭和异步关闭 + +```java +public class CloseFutureClient { + public static void main(String[] args) throws InterruptedException { + NioEventLoopGroup group = new NioEventLoopGroup(); + ChannelFuture channelFuture = new Bootstrap() + .group(group) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer<NioSocketChannel>() { + @Override + protected void initChannel(NioSocketChannel ch) throws Exception { + ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); + ch.pipeline().addLast(new StringEncoder()); + } + }) + .connect(new InetSocketAddress("127.0.0.1", 8080)); + Channel channel = channelFuture.sync().channel(); + new Thread(() -> { + Scanner sc = new Scanner(System.in); + while (true) { + String line = sc.nextLine(); + if (line.equals("q")) { + channel.close(); + break; + } + channel.writeAndFlush(line); + } + }, "input").start(); + + // 获取 CloseFuture 对象 + ChannelFuture closeFuture = channel.closeFuture(); + // 1. 同步处理关闭 + System.out.println("waiting close..."); + closeFuture.sync(); + System.out.println("处理关闭后的操作"); +**************************************************** + // 2. 异步处理关闭 + closeFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + System.out.println("处理关闭后的操作"); + group.shutdownGracefully(); + } + }); + } +} +``` + + + +**** + + + +### Future + +#### 基本介绍 + +Netty 中的 Future 与 JDK 中的 Future 同名,但是功能的实现不同 + +```java +package io.netty.util.concurrent; +public interface Future<V> extends java.util.concurrent.Future<V> +``` + +Future 类 API: + +* `V get()`:阻塞等待获取任务执行结果 +* `V getNow()`:非阻塞获取任务结果,还未产生结果时返回 null +* `Throwable cause()`:非阻塞获取失败信息,如果没有失败,返回 null +* `Future<V> sync()`:等待任务结束,如果任务失败,抛出异常 +* `boolean cancel(boolean mayInterruptIfRunning)`:取消任务 +* `Future<V> addListener(GenericFutureListener listener)`:添加回调,异步接收结果 +* `boolean isSuccess()`:判断任务是否成功 +* `boolean isCancellable()`:判断任务是否取消 + +```java +public class NettyFutureDemo { + public static void main(String[] args) throws ExecutionException, InterruptedException { + NioEventLoopGroup group = new NioEventLoopGroup(); + EventLoop eventLoop = group.next(); + Future<Integer> future = eventLoop.submit(new Callable<Integer>() { + @Override + public Integer call() throws Exception { + System.out.println("执行计算"); + Thread.sleep(1000); + return 70; + } + }); + future.getNow(); + System.out.println(new Date() + "等待结果"); + System.out.println(new Date() + "" + future.get()); + } +} +``` + + + +**** + + + +#### 扩展子类 + +Promise 类是 Future 的子类,可以脱离任务独立存在,作为两个线程间传递结果的容器 + +```java +public interface Promise<V> extends Future<V> +``` + +Promise 类 API: + +* `Promise<V> setSuccess(V result)`:设置成功结果 +* `Promise<V> setFailure(Throwable cause)`:设置失败结果 + +```java +public class NettyPromiseDemo { + public static void main(String[] args) throws Exception { + // 1. 准备 EventLoop 对象 + EventLoop eventLoop = new NioEventLoopGroup().next(); + // 2. 主动创建promise + DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop); + // 3. 任意一个线程执行计算,计算完毕后向 promise 填充结果 + new Thread(() -> { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + promise.setSuccess(200); + }).start(); + + // 4. 接受结果的线程 + System.out.println(new Date() + "等待结果"); + System.out.println(new Date() + "" + promise.get()); + } +} +``` + + + +**** + + + +### Pipeline + +ChannelHandler 用来处理 Channel 上的各种事件,分为入站出站两种,所有 ChannelHandler 连接成双向链表就是 Pipeline + +* 入站处理器通常是 ChannelInboundHandlerAdapter 的子类,主要用来读取客户端数据,写回结果 +* 出站处理器通常是 ChannelOutboundHandlerAdapter 的子类,主要对写回结果进行加工 + +```java +public static void main(String[] args) { + new ServerBootstrap() + .group(new NioEventLoopGroup()) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer<NioSocketChannel>() { + @Override + protected void initChannel(NioSocketChannel ch) throws Exception { + // 1. 通过 channel 拿到 pipeline + ChannelPipeline pipeline = ch.pipeline(); + // 2. 添加处理器 head -> h1 -> h2 -> h3 -> h4 -> h5 -> h6 -> tail + pipeline.addLast("h1", new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + log.debug("1"); + ByteBuf buf = (ByteBuf) msg; + String s = buf.toString(Charset.defaultCharset()); + // 将数据传递给下一个【入站】handler,如果不调用该方法则链会断开 + super.channelRead(ctx, s); + } + }); + pipeline.addLast("h2", new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + log.debug("2"); + // 从【尾部开始向前触发】出站处理器 + ch.writeAndFlush(ctx.alloc().buffer().writeBytes("server".getBytes())); + // 该方法会让管道从【当前 handler 向前】寻找出站处理器 + // ctx.writeAndFlush(); + } + }); + pipeline.addLast("h3", new ChannelOutboundHandlerAdapter() { + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + log.debug("3"); + super.write(ctx, msg, promise); + } + }); + pipeline.addLast("h4", new ChannelOutboundHandlerAdapter() { + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + log.debug("4"); + super.write(ctx, msg, promise); + } + }); + } + }) + .bind(8080); +} + +``` + +服务器端依次打印:1 2 4 3 ,所以**入站是按照 addLast 的顺序执行的,出站是按照 addLast 的逆序执行** + +一个 Channel 包含了一个 ChannelPipeline,而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中关联着一个 ChannelHandler + +入站事件和出站事件在一个双向链表中,两种类型的 handler 互不干扰: + +* 入站事件会从链表 head 往后传递到最后一个入站的 handler +* 出站事件会从链表 tail 往前传递到最前一个出站的handler + +![](https://gitee.com/seazean/images/raw/master/Frame/Netty-ChannelPipeline.png) + + + +**** + + + +### ByteBuf -* NettyClient.java +#### 基本介绍 + +ByteBuf 是对字节数据的封装,优点: + +* 池化,可以重用池中 ByteBuf 实例,更节约内存,减少内存溢出的可能 +* 读写指针分离,不需要像 ByteBuffer 一样切换读写模式 +* 可以自动扩容 +* 支持链式调用,使用更流畅 +* 零拷贝思想,例如 slice、duplicate、CompositeByteBuf + + + +**** + + + +#### 创建方法 + +创建方式 + +* `ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10)`:创建了一个默认的 ByteBuf,初始容量是 10 ```java - public class NettyClient { - public static void main(String[] args){ - //客户端需要一个事件循环组 - NioEventLoopGroup group = new NioEventLoopGroup(); - try { - //创建客户端启动对象,客户端使用的不是 ServerBootstrap 而是 Bootstrap - Bootstrap bootstrap = new Bootstrap(); - //设置相关参数 - bootstrap.group(group) //设置线程组 - //使用NioSocketChannel客户端通道,反射实现 - .channel(NioSocketChannel.class) - .handler(new NettyClientInitializer()); - - System.out.println("...Client is ok..."); - - //启动客户端连接服务器端 - ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync(); - //对关闭通道进行监听,只是监听不是关闭 - channelFuture.channel().closeFuture().sync(); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - group.shutdownGracefully(); - } + public ByteBuf buffer() { + if (directByDefault) { + return directBuffer(); } + return heapBuffer(); } ``` -* NettyClientInitializer.java +* `ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10)`:创建池化基于堆的 ByteBuf + +* `ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10)`:创建池化基于直接内存的 ByteBuf + +* **推荐**的创建方式:在添加处理器的方法中 ```java - public class NettyClientInitializer extends ChannelInitializer<SocketChannel> { + pipeline.addLast("h1", new ChannelInboundHandlerAdapter() { @Override - protected void initChannel(SocketChannel ch) throws Exception { - ch.pipeline().addLast(new NettyClientHandler()); + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + ByteBuf buffer = ctx.alloc().buffer(); } + }); + ``` + +直接内存对比堆内存: + +* 直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用 +* 直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放 + +池化的意义在于可以重用 ByteBuf,高并发时池化功能更节约内存,减少内存溢出的可能,与非池化对比: + +* 非池化,每次都要创建新的 ByteBuf 实例,这个操作对直接内存代价昂贵,堆内存会增加 GC 压力 +* 池化,可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率 + +池化功能的开启,可以通过下面的系统环境变量来设置: + +```sh +-Dio.netty.allocator.type={unpooled|pooled} # VM 参数 +``` + +* 4.1 以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现 +* 4.1 之前,池化功能还不成熟,默认是非池化实现 + + + +**** + + + +#### 读写操作 + +ByteBuf 由四部分组成,最开始读写指针(**双指针**)都在 0 位置 + +![](https://gitee.com/seazean/images/raw/master/Frame/Netty-ByteBuf组成.png) + +写入方法: + +| 方法名 | 说明 | 备注 | +| ------------------------------------------------ | ---------------------- | ------------------------------------------- | +| writeBoolean(boolean value) | 写入 boolean 值 | 用一字节 01\|00 代表 true\|false | +| writeByte(int value) | 写入 byte 值 | | +| writeInt(int value) | 写入 int 值 | Big Endian,即 0x250,写入后 00 00 02 50 | +| writeIntLE(int value) | 写入 int 值 | Little Endian,即 0x250,写入后 50 02 00 00 | +| writeBytes(ByteBuf src) | 写入 ByteBuf | | +| writeBytes(byte[] src) | 写入 byte[] | | +| writeBytes(ByteBuffer src) | 写入 NIO 的 ByteBuffer | | +| int writeCharSequence(CharSequence s, Charset c) | 写入字符串 | | + +* 这些方法的未指明返回值的,其返回值都是 ByteBuf,意味着可以链式调用 +* 写入几位写指针后移几位,指向可以写入的位置 +* 网络传输,默认习惯是 Big Endian + +扩容:写入数据时,容量不够了(初始容量是 10),这时会引发扩容 + +* 如果写入后数据大小未超过 512,则选择下一个 16 的整数倍,例如写入后大小为 12 ,则扩容后 capacity 是 16 +* 如果写入后数据大小超过 512,则选择下一个 2^n,例如写入后大小为 513,则扩容后 capacity 是 2^10 = 1024(2^9=512 不够) +* 扩容不能超过 max capacity 会报错 + +读取方法: + +* `byte readByte()`:读取一个字节,读指针后移 +* `byte getByte(int index)`:读取指定索引位置的字节,读指针不动 +* `ByteBuf markReaderIndex()`:标记读数据的位置 +* `ByteBuf resetReaderIndex()`:重置到标记位置,可以重复读取标记位置向后的数据 + + + +**** + + + +#### 内存释放 + +Netty 中三种内存的回收: + +* UnpooledHeapByteBuf 使用的是 JVM 内存,只需等 GC 回收内存 +* UnpooledDirectByteBuf 使用的就是直接内存了,需要特殊的方法来回收内存 +* PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存 + +Netty 采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口,回收的规则: + +* 每个 ByteBuf 对象的初始计数为 1 +* 调用 release 方法计数减 1,如果计数为 0,ByteBuf 内存被回收 +* 调用 retain 方法计数加 1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收 +* 当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用 + +```java +ByteBuf buf = .ByteBufAllocator.DEFAULT.buffer(10) +try { + // 逻辑处理 +} finally { + buf.release(); +} +``` + +Pipeline 的存在,需要将 ByteBuf 传递给下一个 ChannelHandler,如果在 finally 中 release 了,就失去了传递性,处理规则: + +* 创建 ByteBuf 放入 pipeline + +* 入站 ByteBuf 处理原则 + + * 对原始 ByteBuf 不做处理,调用 ctx.fireChannelRead(msg) 向后传递,这时无须 release + + * 将原始 ByteBuf 转换为其它类型的 Java 对象,这时 ByteBuf 就没用了,此时必须 release + + * 如果不调用 ctx.fireChannelRead(msg) 向后传递,那么也必须 release + + * 如果出现异常,ByteBuf 没有成功传递到下一个 ChannelHandler,必须 release + + * 假设消息一直向后传,那么 TailContext 会负责释放未处理消息(原始的 ByteBuf) + + ```java + // io.netty.channel.DefaultChannelPipeline#onUnhandledInboundMessage(java.lang.Object) + protected void onUnhandledInboundMessage(Object msg) { + try { + logger.debug(); + } finally { + ReferenceCountUtil.release(msg); + } + } + // io.netty.util.ReferenceCountUtil#release(java.lang.Object) + public static boolean release(Object msg) { + if (msg instanceof ReferenceCounted) { + return ((ReferenceCounted) msg).release(); + } + return false; + } + ``` + +* 出站 ByteBuf 处理原则 + + * 出站消息最终都会转为 ByteBuf 输出,一直向前传,由 HeadContext flush 后 release + +* 不确定 ByteBuf 被引用了多少次,但又必须彻底释放,可以循环调用 release 直到返回 true + + + +**** + + + +#### 拷贝操作 + +零拷贝方法: + +* `ByteBuf slice(int index, int length)`:对原始 ByteBuf 进行切片成多个 ByteBuf,切片后的 ByteBuf 并没有发生内存复制,**共用原始 ByteBuf 的内存**,切片后的 ByteBuf 维护独立的 read,write 指针 + + ```java + public static void main(String[] args) { + ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(10); + buf.writeBytes(new byte[]{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}); + // 在切片过程中并没有发生数据复制 + ByteBuf f1 = buf.slice(0, 5); + f1.retain(); + ByteBuf f2 = buf.slice(5, 5); + f2.retain(); + // 对 f1 进行相关的操作也会体现在 buf 上 } ``` -* NettyClientHandler.java +* `ByteBuf duplicate()`:截取原始 ByteBuf 所有内容,并且没有 max capacity 的限制,也是与原始 ByteBuf 使用同一块底层内存,只是读写指针是独立的 + +* `CompositeByteBuf addComponents(boolean increaseWriterIndex, ByteBuf... buffers)`:合并多个 ByteBuf ```java - public class NettyClientHandler extends ChannelInboundHandlerAdapter { - // 通道就绪就会触发该方法 - @Override - public void channelActive(ChannelHandlerContext ctx) throws Exception { - System.out.println("client : " + ctx); - ctx.writeAndFlush(Unpooled.copiedBuffer("hello server", CharsetUtil.UTF_8)); - } + public static void main(String[] args) { + ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(); + buf1.writeBytes(new byte[]{1, 2, 3, 4, 5}); - // 通道有读取事件时,会触发 - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - ByteBuf buf = (ByteBuf) msg; - System.out.println("Server reply : " + buf.toString(CharsetUtil.UTF_8)); - System.out.println("Server address : " + ctx.channel().remoteAddress()); - } + ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(); + buf1.writeBytes(new byte[]{6, 7, 8, 9, 10}); - //异常处理 - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - cause.printStackTrace(); - ctx.close(); - } + CompositeByteBuf buf = ByteBufAllocator.DEFAULT.compositeBuffer(); + buf.addComponents(true, buf1, buf2); } ``` - + CompositeByteBuf 是一个组合的 ByteBuf,内部维护了一个 Component 数组,每个 Component 管理一个 ByteBuf,记录了这个 ByteBuf 相对于整体偏移量等信息,代表着整体中某一段的数据 + * 优点:对外是一个虚拟视图,组合这些 ByteBuf 不会产生内存复制 + * 缺点:复杂了很多,多次操作会带来性能的损耗 +深拷贝: -*** +* `ByteBuf copy()`:将底层内存数据进行深拷贝,因此无论读写,都与原始 ByteBuf 无关 + +池化相关: + +* Unpooled 是一个工具类,提供了非池化的 ByteBuf 创建、组合、复制等操作 + + ```java + ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(5); + buf1.writeBytes(new byte[]{1, 2, 3, 4, 5}); + ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(5); + buf2.writeBytes(new byte[]{6, 7, 8, 9, 10}); + + // 当包装 ByteBuf 个数超过一个时, 底层使用了 CompositeByteBuf,零拷贝思想 + ByteBuf buf = Unpooled.wrappedBuffer(buf1, buf2); + ``` + +**** + + +# Tail diff --git a/Java.md b/Java.md index dc3c5b0..9d8f2b2 100644 --- a/Java.md +++ b/Java.md @@ -17520,7 +17520,7 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 * 静态变量的方式: ```java - public class Singleton { + public final class Singleton { // 私有构造方法 private Singleton() {} // 在成员位置创建该类的对象 @@ -17565,23 +17565,23 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 ```java public class Singleton { - //私有构造方法 + // 私有构造方法 private Singleton() {} - //在成员位置创建该类的对象 + // 在成员位置创建该类的对象 private static Singleton instance; static { instance = new Singleton(); } - //对外提供静态方法获取该对象 + // 对外提供静态方法获取该对象 public static Singleton getInstance() { return instance; } } ``` -* 枚举方式:枚举类型是所用单例实现中**唯一一种**不会被破坏的单例实现模式 +* 枚举方式:枚举类型是所用单例实现中**唯一一种不会被破坏**的单例实现模式 ```java public enum Singleton { @@ -17605,7 +17605,7 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 反编译结果: ```java - public final class Singleton extends java.lang.Enum<Singleton> {//Enum实现序列化接口 + public final class Singleton extends java.lang.Enum<Singleton> { // Enum实现序列化接口 public static final Singleton INSTANCE = new Singleton(); } ``` @@ -17624,16 +17624,16 @@ UML 从目标系统的不同角度出发,定义了用例图、类图、对象 ```java public class Singleton { - //私有构造方法 + // 私有构造方法 private Singleton() {} - //在成员位置创建该类的对象 + // 在成员位置创建该类的对象 private static Singleton instance; - //对外提供静态方法获取该对象 + // 对外提供静态方法获取该对象 public static Singleton getInstance() { if(instance == null) { - //多线程环境,会出现线程安全问题,可能多个线程同时进入这里 + // 多线程环境,会出现线程安全问题,可能多个线程同时进入这里 instance = new Singleton(); } return instance; @@ -18943,12 +18943,10 @@ private static final class ProxyClassFactory { String proxyName = proxyPkg + proxyClassNamePrefix + num; // 【生成二进制字节码,这个字节码写入到文件内】,就是编译好的 class 文件 - byte[] proxyClassFile = ProxyGenerator.generateProxyClass( -proxyName, interfaces, accessFlags); + byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags); try { // 【使用加载器加载二进制到 jvm】,并且返回 class - return defineClass0(loader, proxyName, - proxyClassFile, 0, proxyClassFile.length); + return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); } catch (ClassFormatError e) { } } } diff --git a/Prog.md b/Prog.md index daebcb3..9c05c3b 100644 --- a/Prog.md +++ b/Prog.md @@ -13822,6 +13822,8 @@ TCP 协议的使用场景:文件上传和下载、邮件发送和接收、远 TCP 通信也叫 **Socket 网络编程**,只要代码基于 Socket 开发,底层就是基于了可靠传输的 TCP 通信 +双向通信:Java Socket 是全双工的,在任意时刻,线路上存在 `A -> B` 和 `B -> A` 的双向信号传输,即使是阻塞 IO,读和写也是可以同时进行的,只要分别采用读线程和写线程即可,读不会阻塞写、写也不会阻塞读 + TCP 协议相关的类: * Socket:一个该类的对象就代表一个客户端程序。 From 47d73d31a18a7d29788a202197f7de7ca4c1ad52 Mon Sep 17 00:00:00 2001 From: Seazean <imseazean@gmail.com> Date: Wed, 22 Sep 2021 23:18:08 +0800 Subject: [PATCH 013/122] Update Java Notes --- Frame.md | 643 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 631 insertions(+), 12 deletions(-) diff --git a/Frame.md b/Frame.md index 95cfa19..5aba065 100644 --- a/Frame.md +++ b/Frame.md @@ -1674,6 +1674,8 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty new ServerBootstrap() // 2. 线程组,boss 只负责【处理 accept 事件】, worker 只【负责 channel 上的读写】 .group(boss, worker) + //.option() // 给 ServerSocketChannel 配置参数 + //.childOption() // 给 SocketChannel 配置参数 // 3. 选择服务器的 ServerSocketChannel 实现 .channel(NioServerSocketChannel.class) // 4. boss 负责处理连接,worker(child) 负责处理读写,决定了能执行哪些操作(handler) @@ -1709,6 +1711,7 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty new Bootstrap() // 2. 添加 EventLoop .group(new NioEventLoopGroup()) + //.option() //给 SocketChannel 配置参数 // 3. 选择客户端 channel 实现 .channel(NioSocketChannel.class) // 4. 添加处理器 @@ -1734,6 +1737,8 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty +参考视频:https://www.bilibili.com/video/BV1py4y1E7oA + @@ -1751,7 +1756,7 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty 事件循环对象 EventLoop,本质是一个单线程执行器(同时维护了一个 selector),里面有 run 方法处理 Channel 上源源不断的 IO 事件 -事件循环组 EventLoopGroup 是一组 EventLoop,Channel 会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop,后续这个 Channel 上的 IO 事件都由此 EventLoop 来处理,保证了事件处理时的线程安全 +事件循环组 EventLoopGroup 是一组 EventLoop,Channel 会调用 Boss EventLoopGroup 的 register 方法来绑定其中一个 Worker 的 EventLoop,后续这个 Channel 上的 IO 事件都由此 EventLoop 来处理,保证了事件处理时的线程安全 EventLoopGroup 类 API: @@ -1806,6 +1811,10 @@ public class EventLoopServer { 源码分析: ```java +public ChannelHandlerContext fireChannelRead(final Object msg) { + invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg); + return this; +} static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) { final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next); EventExecutor executor = next.executor(); @@ -1835,7 +1844,7 @@ static void invokeChannelRead(final AbstractChannelHandlerContext next, Object m ### Channel -#### 基本使用 +#### 基本介绍 Channel 类 API: @@ -1915,6 +1924,7 @@ public class CloseFutureClient { }) .connect(new InetSocketAddress("127.0.0.1", 8080)); Channel channel = channelFuture.sync().channel(); + // 发送数据 new Thread(() -> { Scanner sc = new Scanner(System.in); while (true) { @@ -1926,9 +1936,9 @@ public class CloseFutureClient { channel.writeAndFlush(line); } }, "input").start(); - // 获取 CloseFuture 对象 ChannelFuture closeFuture = channel.closeFuture(); + // 1. 同步处理关闭 System.out.println("waiting close..."); closeFuture.sync(); @@ -1976,7 +1986,7 @@ Future 类 API: ```java public class NettyFutureDemo { - public static void main(String[] args) throws ExecutionException, InterruptedException { + public static void main(String[] args) throws Exception { NioEventLoopGroup group = new NioEventLoopGroup(); EventLoop eventLoop = group.next(); Future<Integer> future = eventLoop.submit(new Callable<Integer>() { @@ -2018,7 +2028,7 @@ public class NettyPromiseDemo { public static void main(String[] args) throws Exception { // 1. 准备 EventLoop 对象 EventLoop eventLoop = new NioEventLoopGroup().next(); - // 2. 主动创建promise + // 2. 主动创建 promise DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop); // 3. 任意一个线程执行计算,计算完毕后向 promise 填充结果 new Thread(() -> { @@ -2048,7 +2058,7 @@ public class NettyPromiseDemo { ChannelHandler 用来处理 Channel 上的各种事件,分为入站出站两种,所有 ChannelHandler 连接成双向链表就是 Pipeline * 入站处理器通常是 ChannelInboundHandlerAdapter 的子类,主要用来读取客户端数据,写回结果 -* 出站处理器通常是 ChannelOutboundHandlerAdapter 的子类,主要对写回结果进行加工 +* 出站处理器通常是 ChannelOutboundHandlerAdapter 的子类,主要对写回结果进行加工(入站和出站是对于服务端来说的) ```java public static void main(String[] args) { @@ -2109,7 +2119,7 @@ public static void main(String[] args) { 入站事件和出站事件在一个双向链表中,两种类型的 handler 互不干扰: * 入站事件会从链表 head 往后传递到最后一个入站的 handler -* 出站事件会从链表 tail 往前传递到最前一个出站的handler +* 出站事件会从链表 tail 往前传递到最前一个出站的 handler ![](https://gitee.com/seazean/images/raw/master/Frame/Netty-ChannelPipeline.png) @@ -2159,7 +2169,7 @@ ByteBuf 是对字节数据的封装,优点: * **推荐**的创建方式:在添加处理器的方法中 ```java - pipeline.addLast("h1", new ChannelInboundHandlerAdapter() { + pipeline.addLast(new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buffer = ctx.alloc().buffer(); @@ -2172,7 +2182,7 @@ ByteBuf 是对字节数据的封装,优点: * 直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用 * 直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放 -池化的意义在于可以重用 ByteBuf,高并发时池化功能更节约内存,减少内存溢出的可能,与非池化对比: +池化的意义在于可以**重用 ByteBuf**,高并发时池化功能更节约内存,减少内存溢出的可能,与非池化对比: * 非池化,每次都要创建新的 ByteBuf 实例,这个操作对直接内存代价昂贵,堆内存会增加 GC 压力 * 池化,可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率 @@ -2240,7 +2250,7 @@ Netty 中三种内存的回收: * UnpooledHeapByteBuf 使用的是 JVM 内存,只需等 GC 回收内存 * UnpooledDirectByteBuf 使用的就是直接内存了,需要特殊的方法来回收内存 -* PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存 +* PooledByteBuf 和子类使用了池化机制,需要更复杂的规则来回收内存 Netty 采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口,回收的规则: @@ -2336,12 +2346,13 @@ Pipeline 的存在,需要将 ByteBuf 传递给下一个 ChannelHandler,如 buf1.writeBytes(new byte[]{6, 7, 8, 9, 10}); CompositeByteBuf buf = ByteBufAllocator.DEFAULT.compositeBuffer(); + // true 表示增加新的 ByteBuf 自动递增 write index, 否则 write index 会始终为 0 buf.addComponents(true, buf1, buf2); } ``` - + CompositeByteBuf 是一个组合的 ByteBuf,内部维护了一个 Component 数组,每个 Component 管理一个 ByteBuf,记录了这个 ByteBuf 相对于整体偏移量等信息,代表着整体中某一段的数据 - + * 优点:对外是一个虚拟视图,组合这些 ByteBuf 不会产生内存复制 * 缺点:复杂了很多,多次操作会带来性能的损耗 @@ -2363,7 +2374,596 @@ Pipeline 的存在,需要将 ByteBuf 传递给下一个 ChannelHandler,如 ByteBuf buf = Unpooled.wrappedBuffer(buf1, buf2); ``` + + + + + +**** + + + + + +## 粘包半包 + +### 现象演示 + +在 TCP 传输中,客户端发送消息时,实际上是将数据写入TCP的缓存,此时数据的大小和缓存的大小就会造成粘包和半包 + +* 当数据超过 TCP 缓存容量时,就会被拆分成多个包,通过 Socket 多次发送到服务端,服务端每次从缓存中取数据,产生半包问题 + +* 当数据小于 TCP 缓存容量时,缓存中可以存放多个包,客户端和服务端一次通信就可能传递多个包,这时候服务端就可能一次读取多个包,产生粘包的问题 + +代码演示: + +* 客户端代码: + + ```java + public class HelloWorldClient { + public static void main(String[] args) { + send(); + } + private static void send() { + NioEventLoopGroup worker = new NioEventLoopGroup(); + try { + Bootstrap bootstrap = new Bootstrap(); + bootstrap.channel(NioSocketChannel.class); + bootstrap.group(worker); + bootstrap.handler(new ChannelInitializer<SocketChannel>() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { + // 【在连接 channel 建立成功后,会触发 active 方法】 + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + // 发送内容随机的数据包 + Random r = new Random(); + char c = '0'; + ByteBuf buf = ctx.alloc().buffer(); + for (int i = 0; i < 10; i++) { + byte[] bytes = new byte[10]; + for (int j = 0; j < r.nextInt(9) + 1; j++) { + bytes[j] = (byte) c; + } + c++; + buf.writeBytes(bytes); + } + ctx.writeAndFlush(buf); + } + }); + } + }); + ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync(); + channelFuture.channel().closeFuture().sync(); + + } catch (InterruptedException e) { + log.error("client error", e); + } finally { + worker.shutdownGracefully(); + } + } + } + ``` + +* 服务器代码: + + ```java + public class HelloWorldServer { + public static void main(String[] args) { + NioEventLoopGroup boss = new NioEventLoopGroup(1); + NioEventLoopGroup worker = new NioEventLoopGroup(); + try { + ServerBootstrap serverBootstrap = new ServerBootstrap(); + serverBootstrap.channel(NioServerSocketChannel.class); + // 调整系统的接受缓冲区【滑动窗口】 + //serverBootstrap.option(ChannelOption.SO_RCVBUF, 10); + // 调整 netty 的接受缓冲区(ByteBuf) + //serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, + // new AdaptiveRecvByteBufAllocator(16, 16, 16)); + serverBootstrap.group(boss, worker); + serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + // 【这里可以添加解码器】 + // LoggingHandler 用来打印消息 + ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); + } + }); + ChannelFuture channelFuture = serverBootstrap.bind(8080); + channelFuture.sync(); + channelFuture.channel().closeFuture().sync(); + } catch (InterruptedException e) { + log.error("server error", e); + } finally { + boss.shutdownGracefully(); + worker.shutdownGracefully(); + log.debug("stop"); + } + } + } + ``` + +* 粘包效果展示: + + ```java + 09:57:27.140 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xddbaaef6, L:/127.0.0.1:8080 - R:/127.0.0.1:8701] READ: 100B // 读了 100 字节,发生粘包 + +-------------------------------------------------+ + | 0 1 2 3 4 5 6 7 8 9 a b c d e f | + +--------+-------------------------------------------------+----------------+ + |00000000| 30 30 30 30 30 00 00 00 00 00 31 00 00 00 00 00 |00000.....1.....| + |00000010| 00 00 00 00 32 32 32 32 00 00 00 00 00 00 33 00 |....2222......3.| + |00000020| 00 00 00 00 00 00 00 00 34 34 00 00 00 00 00 00 |........44......| + |00000030| 00 00 35 35 35 35 00 00 00 00 00 00 36 36 36 00 |..5555......666.| + |00000040| 00 00 00 00 00 00 37 37 37 37 00 00 00 00 00 00 |......7777......| + |00000050| 38 38 38 38 38 00 00 00 00 00 39 39 00 00 00 00 |88888.....99....| + |00000060| 00 00 00 00 |.... | + +--------+-------------------------------------------------+----------------+ + ``` + +解决方法:通过调整系统的接受缓冲区的滑动窗口和 Netty 的接受缓冲区保证每条包只含有一条数据,滑动窗口大小,仅决定了 Netty 读取的**最小单位**,实际每次读取的一般是它的整数倍 + + + +*** + + + +### 解决方案 + +#### 短连接 + +发一个包建立一次连接,这样连接建立到连接断开之间就是消息的边界,缺点就是效率很低 + +客户端代码改造: + +```java +public class HelloWorldClient { + public static void main(String[] args) { + // 分 10 次发送 + for (int i = 0; i < 10; i++) { + send(); + } + } +} +``` + + + +**** + + + +#### 固定长度 + +服务器端加入定长解码器,每一条消息采用固定长度,缺点浪费空间 + +```java +serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new FixedLengthFrameDecoder(8)); + // LoggingHandler 用来打印消息 + ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); + } +}); +``` + +```java +10:29:06.522 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x38a70fbf, L:/127.0.0.1:8080 - R:/127.0.0.1:10144] READ: 10B + +-------------------------------------------------+ + | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ++--------+-------------------------------------------------+----------------+ +|00000000| 31 31 00 00 00 00 00 00 00 00 |11........ | ++--------+-------------------------------------------------+----------------+ +10:29:06.522 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x38a70fbf, L:/127.0.0.1:8080 - R:/127.0.0.1:10144] READ: 10B + +-------------------------------------------------+ + | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ++--------+-------------------------------------------------+----------------+ +|00000000| 32 32 32 32 32 32 00 00 00 00 |222222.... | ++--------+-------------------------------------------------+----------------+ +``` + + + +**** + + + +#### 分隔符 + +服务端加入行解码器,默认以 `\n` 或 `\r\n` 作为分隔符,如果超出指定长度仍未出现分隔符,则抛出异常: + +```java +serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new FixedLengthFrameDecoder(8)); + ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); + } +}); +``` + +客户端在每条消息之后,加入 `\n` 分隔符: + +```java +public void channelActive(ChannelHandlerContext ctx) throws Exception { + Random r = new Random(); + char c = 'a'; + ByteBuf buffer = ctx.alloc().buffer(); + for (int i = 0; i < 10; i++) { + for (int j = 1; j <= r.nextInt(16)+1; j++) { + buffer.writeByte((byte) c); + } + // 10 代表 '\n' + buffer.writeByte(10); + c++; + } + ctx.writeAndFlush(buffer); +} +``` + + + +**** + + + +#### 预设长度 + +LengthFieldBasedFrameDecoder 解码器自定义长度解决 TCP 粘包黏包问题 + +```java +int maxFrameLength // 数据最大长度 +int lengthFieldOffset // 长度字段偏移量,从第几个字节开始是内容的长度字段 +int lengthFieldLength // 长度字段本身的长度 +int lengthAdjustment // 长度字段为基准,几个字节后才是内容 +int initialBytesToStrip // 从头开始剥离几个字节解码后显示 +``` + +```java +lengthFieldOffset = 1 (= the length of HDR1) +lengthFieldLength = 2 +lengthAdjustment = 1 (= the length of HDR2) +initialBytesToStrip = 3 (= the length of HDR1 + LEN) + +BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes)//解码 ++------+--------+------+----------------+ +------+----------------+ +| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content | +| 0xCA | 0x000C | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" | ++------+--------+------+----------------+ +------+----------------+ +``` + +代码实现: + +```java +public class LengthFieldDecoderDemo { + public static void main(String[] args) { + EmbeddedChannel channel = new EmbeddedChannel( + // int 占 4 字节,版本号一个字节 + new LengthFieldBasedFrameDecoder(1024, 0, 4, 1,5), + new LoggingHandler(LogLevel.DEBUG) + ); + + // 4 个字节的内容长度, 实际内容 + ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); + send(buffer, "Hello, world"); + send(buffer, "Hi!"); + // 写出缓存 + channel.writeInbound(buffer); + } + // 写入缓存 + private static void send(ByteBuf buffer, String content) { + byte[] bytes = content.getBytes(); // 实际内容 + int length = bytes.length; // 实际内容长度 + buffer.writeInt(length); + buffer.writeByte(1); // 表示版本号 + buffer.writeBytes(bytes); + } +} +``` + +```java +10:49:59.344 [main] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ: 12B + +-------------------------------------------------+ + | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ++--------+-------------------------------------------------+----------------+ +|00000000| 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 |Hello, world | ++--------+-------------------------------------------------+----------------+ +10:49:59.344 [main] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ: 3B + +-------------------------------------------------+ + | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ++--------+-------------------------------------------------+----------------+ +|00000000| 48 69 21 |Hi! | ++--------+-------------------------------------------------+----------------+ +``` + + + +**** + + + +### 协议设计 + +#### HTTP协议 + +访问URL:http://localhost:8080/ + +```java +public class HttpDemo { + public static void main(String[] args) { + NioEventLoopGroup boss = new NioEventLoopGroup(); + NioEventLoopGroup worker = new NioEventLoopGroup(); + try { + ServerBootstrap serverBootstrap = new ServerBootstrap(); + serverBootstrap.channel(NioServerSocketChannel.class); + serverBootstrap.group(boss, worker); + serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); + ch.pipeline().addLast(new HttpServerCodec()); + // 只针对某一种类型的请求处理,此处针对 HttpRequest + ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception { + // 获取请求 + log.debug(msg.uri()); + + // 返回响应 + DefaultFullHttpResponse response = new DefaultFullHttpResponse( + msg.protocolVersion(), HttpResponseStatus.OK); + + byte[] bytes = "<h1>Hello, world!</h1>".getBytes(); + + response.headers().setInt(CONTENT_LENGTH, bytes.length); + response.content().writeBytes(bytes); + + // 写回响应 + ctx.writeAndFlush(response); + } + }); + } + }); + ChannelFuture channelFuture = serverBootstrap.bind(8080).sync(); + channelFuture.channel().closeFuture().sync(); + } catch (InterruptedException e) { + log.error("n3.server error", e); + } finally { + boss.shutdownGracefully(); + worker.shutdownGracefully(); + } + } +} +``` + + + + + +*** + + + +#### 自定义协议 + +处理器代码: + +```java +@Slf4j +public class MessageCodec extends ByteToMessageCodec<Message> { + // 编码 + @Override + public void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception { + // 4 字节的魔数 + out.writeBytes(new byte[]{1, 2, 3, 4}); + // 1 字节的版本, + out.writeByte(1); + // 1 字节的序列化方式 jdk 0 , json 1 + out.writeByte(0); + // 1 字节的指令类型 + out.writeByte(msg.getMessageType()); + // 4 个字节 + out.writeInt(msg.getSequenceId()); + // 无意义,对齐填充, 1 字节 + out.writeByte(0xff); + // 获取内容的字节数组,msg 对象序列化 + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + oos.writeObject(msg); + byte[] bytes = bos.toByteArray(); + // 长度 + out.writeInt(bytes.length); + // 写入内容 + out.writeBytes(bytes); + } + + // 解码 + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { + int magicNum = in.readInt(); + byte version = in.readByte(); + byte serializerType = in.readByte(); + byte messageType = in.readByte(); + int sequenceId = in.readInt(); + in.readByte(); + int length = in.readInt(); + byte[] bytes = new byte[length]; + in.readBytes(bytes, 0, length); + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); + Message message = (Message) ois.readObject(); + log.debug("{}, {}, {}, {}, {}, {}", magicNum, version, serializerType, messageType, sequenceId, length); + log.debug("{}", message); + out.add(message); + } +} +``` + +测试代码: + +```java +public static void main(String[] args) throws Exception { + EmbeddedChannel channel = new EmbeddedChannel(new LoggingHandler(), new MessageCodec()); + // encode + LoginRequestMessage message = new LoginRequestMessage("zhangsan", "123"); + channel.writeOutbound(message); + + // decode + ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(); + new MessageCodec().encode(null, message, buf); + // 入站 + channel.writeInbound(buf); +} +public class LoginRequestMessage extends Message { + private String username; + private String password; + // set + get +} +``` + +![](https://gitee.com/seazean/images/raw/master/Frame/Netty-自定义协议.png) + + + +*** + + + +#### Sharable + +@Sharable 注解的添加时机: + +* 当 handler 不保存状态时,就可以安全地在多线程下被共享 + +* 对于编解码器类不能继承 ByteToMessageCodec 或 CombinedChannelDuplexHandler,它们的构造方法对 @Sharable 有限制 + + ```java + protected ByteToMessageCodec(boolean preferDirect) { + ensureNotSharable(); + outboundMsgMatcher = TypeParameterMatcher.find(this, ByteToMessageCodec.class, "I"); + encoder = new Encoder(preferDirect); + } + ``` + + ```java + protected void ensureNotSharable() { + // 如果类上有该注解 + if (isSharable()) { + throw new IllegalStateException(); + } + } + ``` + +* 如果能确保编解码器不会保存状态,可以继承 MessageToMessageCodec 父类 + +```java +@Slf4j +@ChannelHandler.Sharable +// 必须和 LengthFieldBasedFrameDecoder 一起使用,确保接到的 ByteBuf 消息是完整的 +public class MessageCodecSharable extends MessageToMessageCodec<ByteBuf, Message> { + @Override + protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> outList) throws Exception { + ByteBuf out = ctx.alloc().buffer(); + // 4 字节的魔数 + out.writeBytes(new byte[]{1, 2, 3, 4}); + // .... + outList.add(out); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { + //.... + } +} +``` + + + + + +*** + + + + + +## 场景优化 + +### 空闲检测 + +#### 连接假死 + +连接假死就是客户端数据发不出去,服务端也一直收不到数据,保持这种状态,假死的连接占用的资源不能自动释放,而且向假死连接发送数据,得到的反馈是发送超时 + +解决方案:每隔一段时间就检查这段时间内是否接收到客户端数据,没有就可以判定为连接假死 + +IdleStateHandler 是 Netty 提供的处理空闲状态的处理器,用来判断是不是读空闲时间或写空闲时间过长 + +* 参数一 long readerIdleTime:读空闲,表示多长时间没有读 +* 参数二 long writerIdleTime:写空闲,表示多长时间没有写 +* 参数三 long allIdleTime:读写空闲,表示多长时间没有读写 + +```java +serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 12, 4, 0, 0)); + ch.pipeline().addLast(new MessageCodec()); + // 5s 内如果没有收到 channel 的数据,会触发一个 IdleState#READER_IDLE 事件, + ch.pipeline().addLast(new IdleStateHandler(5, 0, 0)); + // ChannelDuplexHandler 【可以同时作为入站和出站】处理器 + ch.pipeline().addLast(new ChannelDuplexHandler() { + // 用来触发特殊事件 + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{ + IdleStateEvent event = (IdleStateEvent) evt; + // 触发了读空闲事件 + if (event.state() == IdleState.READER_IDLE) { + log.debug("已经 5s 没有读到数据了"); + ctx.channel().close(); + } + } + }); + } +} +``` + + + +*** + + + +#### 心跳机制 + +客户端定时向服务器端发送数据,**时间间隔要小于服务器定义的空闲检测的时间间隔**,就能防止误判连接假死,这就是心跳机制 + +```java +bootstrap.handler(new ChannelInitializer<SocketChannel>() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 12, 4, 0, 0)); + ch.pipeline().addLast(new MessageCodec()); + // 3s 内如果没有向服务器写数据,会触发一个 IdleState#WRITER_IDLE 事件 + ch.pipeline().addLast(new IdleStateHandler(0, 3, 0)); + // ChannelDuplexHandler 可以同时作为入站和出站处理器 + ch.pipeline().addLast(new ChannelDuplexHandler() { + // 用来触发特殊事件 + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + IdleStateEvent event = (IdleStateEvent) evt; + // 触发了写空闲事件 + if (event.state() == IdleState.WRITER_IDLE) { + // 3s 没有写数据了,【发送一个心跳包】 + ctx.writeAndFlush(new PingMessage()); + } + } + }); + } +} +``` @@ -2374,3 +2974,22 @@ Pipeline 的存在,需要将 ByteBuf 传递给下一个 ChannelHandler,如 # Tail + +```java +// 发送内容随机的数据包 +Random r = new Random(); +char c = '0'; +ByteBuf buf = ctx.alloc().buffer(); +for (int i = 0; i < 10; i++) { + byte[] bytes = new byte[10]; + for (int j = 0; j < r.nextInt(10); j++) { + bytes[j] = (byte) c; + } + c++; + buf.writeBytes(bytes); +} +ctx.writeAndFlush(buf); +``` + + + From b7608e66d056cef2e1dfc136d9f34bdf1b11a8cc Mon Sep 17 00:00:00 2001 From: Seazean <imseazean@gmail.com> Date: Thu, 23 Sep 2021 18:06:02 +0800 Subject: [PATCH 014/122] Update Java Notes --- Frame.md | 483 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- Prog.md | 23 +-- 2 files changed, 484 insertions(+), 22 deletions(-) diff --git a/Frame.md b/Frame.md index 5aba065..0795ddb 100644 --- a/Frame.md +++ b/Frame.md @@ -2973,23 +2973,482 @@ bootstrap.handler(new ChannelInitializer<SocketChannel>() { -# Tail +### 序列化 + +#### 普通方式 + +序列化,反序列化主要用在消息正文的转换上 + +* 序列化时,需要将 Java 对象变为要传输的数据(可以是 byte[],或 json 等,最终都需要变成 byte[]) +* 反序列化时,需要将传入的正文数据还原成 Java 对象,便于处理 + +代码实现: + +* 抽象一个 Serializer 接口 + + ```java + public interface Serializer { + // 反序列化方法 + <T> T deserialize(Class<T> clazz, byte[] bytes); + // 序列化方法 + <T> byte[] serialize(T object); + } + ``` + +* 提供两个实现 + + ```java + enum SerializerAlgorithm implements Serializer { + // Java 实现 + Java { + @Override + public <T> T deserialize(Class<T> clazz, byte[] bytes) { + try { + ObjectInputStream in = + new ObjectInputStream(new ByteArrayInputStream(bytes)); + Object object = in.readObject(); + return (T) object; + } catch (IOException | ClassNotFoundException e) { + throw new RuntimeException("SerializerAlgorithm.Java 反序列化错误", e); + } + } + + @Override + public <T> byte[] serialize(T object) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + new ObjectOutputStream(out).writeObject(object); + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("SerializerAlgorithm.Java 序列化错误", e); + } + } + }, + // Json 实现(引入了 Gson 依赖) + Json { + @Override + public <T> T deserialize(Class<T> clazz, byte[] bytes) { + return new Gson().fromJson(new String(bytes, StandardCharsets.UTF_8), clazz); + } + + @Override + public <T> byte[] serialize(T object) { + return new Gson().toJson(object).getBytes(StandardCharsets.UTF_8); + } + }; + + // 需要从协议的字节中得到是哪种序列化算法 + public static SerializerAlgorithm getByInt(int type) { + SerializerAlgorithm[] array = SerializerAlgorithm.values(); + if (type < 0 || type > array.length - 1) { + throw new IllegalArgumentException("超过 SerializerAlgorithm 范围"); + } + return array[type]; + } + } + ``` + + + + + +*** + + + +#### ProtoBuf + +##### 基本介绍 + +Codec(编解码器)的组成部分有两个:Decoder(解码器)和 Encoder(编码器)。Encoder 负责把业务数据转换成字节码数据,Decoder 负责把字节码数据转换成业务数据 + +<img src="https://gitee.com/seazean/images/raw/master/Frame/Netty-编码解码.png" style="zoom: 67%;" /> + + + +Protobuf 是 Google 发布的开源项目,全称 Google Protocol Buffers ,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。很适合做数据存储或 RPC(远程过程调用 remote procedure call)数据交换格式。目前很多公司从 HTTP + Json 转向 TCP + Protobuf ,效率会更高 + +Protobuf 是以 message 的方式来管理数据,支持跨平台、跨语言(客户端和服务器端可以是不同的语言编写的),高性能、高可靠性 + +工作过程:使用 Protobuf 编译器自动生成代码,Protobuf 是将类的定义使用 .proto 文件进行描述,然后通过 protoc.exe 编译器根据 .proto 自动生成 .java 文件 + + + +*** + + + +##### 代码实现 + +* 单个 message: + + ```protobuf + syntax = "proto3"; // 版本 + option java_outer_classname = "StudentPOJO"; // 生成的外部类名,同时也是文件名 + // protobuf 使用 message 管理数据 + message Student { // 在 StudentPOJO 外部类种生成一个内部类 Student,是真正发送的 POJO 对象 + int32 id = 1; // Student 类中有一个属性:名字为 id 类型为 int32(protobuf类型) ,1表示属性序号,不是值 + string name = 2; + } + ``` + + <img src="https://gitee.com/seazean/images/raw/master/Frame/Netty-Protobuf编译文件.png" style="zoom:80%;" /> + + 编译 `protoc.exe --java_out=.Student.proto`(cmd 窗口输入) 将生成的 StudentPOJO 放入到项目使用 + + Server 端: + + ```java + new ServerBootstrap() //... + .childHandler(new ChannelInitializer<SocketChannel>() { // 创建一个通道初始化对象 + // 给pipeline 设置处理器 + @Override + protected void initChannel(SocketChannel ch) throws Exception { + // 在pipeline加入ProtoBufDecoder,指定对哪种对象进行解码 + ch.pipeline().addLast("decoder", new ProtobufDecoder( + StudentPOJO.Student.getDefaultInstance())); + ch.pipeline().addLast(new NettyServerHandler()); + } + }); + } + ``` + + Client 端: + + ```java + new Bootstrap().group(group) // 设置线程组 + .channel(NioSocketChannel.class) // 设置客户端通道的实现类(反射) + .handler(new ChannelInitializer<SocketChannel>() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + // 在pipeline中加入 ProtoBufEncoder + ch.pipeline().addLast("encoder", new ProtobufEncoder()); + ch.pipeline().addLast(new NettyClientHandler()); // 加入自定义的业务处理器 + } + }); + ``` + +* 多个 message:Protobuf 可以使用 message 管理其他的 message。假设某个项目需要传输 20 个对象,可以在一个文件里定义 20 个 message,最后再用一个总的 message 来决定在实际传输时真正需要传输哪一个对象 + + ```protobuf + syntax = "proto3"; + option optimize_for = SPEED; // 加快解析 + option java_package="com.atguigu.netty.codec2"; // 指定生成到哪个包下 + option java_outer_classname="MyDataInfo"; // 外部类名, 文件名 + + message MyMessage { + // 定义一个枚举类型,DataType 如果是 0 则表示一个 Student 对象实例,DataType 这个名称自定义 + enum DataType { + StudentType = 0; //在 proto3 要求 enum 的编号从 0 开始 + WorkerType = 1; + } + + // 用 data_type 来标识传的是哪一个枚举类型,这里才真正开始定义 Message 的数据类型 + DataType data_type = 1; // 所有后面的数字都只是编号而已 + + // oneof 关键字,表示每次枚举类型进行传输时,限制最多只能传输一个对象。 + // dataBody名称也是自定义的 + // MyMessage 里出现的类型只有两个 DataType 类型,Student 或者 Worker 类型,在真正传输的时候只会有一个出现 + oneof dataBody { + Student student = 2; //注意这后面的数字也都只是编号而已,上面DataType data_type = 1 占了第一个序号了 + Worker worker = 3; + } + + + } + + message Student { + int32 id = 1; // Student类的属性 + string name = 2; // + } + message Worker { + string name=1; + int32 age=2; + } + ``` + + 编译: + + Server 端: + + ```java + ch.pipeline().addLast("decoder", new ProtobufDecoder(MyDataInfo.MyMessage.getDefaultInstance())); + ``` + + Client 端: + + ```java + pipeline.addLast("encoder", new ProtobufEncoder()); + ``` + + + + + +*** + + + +### 长连接 + +HTTP 协议是无状态的,浏览器和服务器间的请求响应一次,下一次会重新创建连接。实现基于 WebSocket 的长连接的全双工的交互,改变 HTTP 协议多次请求的约束 + +开发需求: + +* 实现长连接,服务器与浏览器相互通信客户端 +* 浏览器和服务器端会相互感知,比如服务器关闭了,浏览器会感知,同样浏览器关闭了,服务器会感知 + +代码实现: + +* WebSocket: + + * WebSocket 的数据是**以帧(frame)形式传递**,WebSocketFrame 下面有六个子类,代表不同的帧格式 + + * 浏览器请求 URL:ws://localhost:8080/xxx + + ```java + public class MyWebSocket { + public static void main(String[] args) throws Exception { + // 创建两个线程组 + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + + ServerBootstrap serverBootstrap = new ServerBootstrap(); + serverBootstrap.group(bossGroup, workerGroup); + serverBootstrap.channel(NioServerSocketChannel.class); + serverBootstrap.handler(new LoggingHandler(LogLevel.INFO)); + serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + + // 基于 http 协议,使用 http 的编码和解码器 + pipeline.addLast(new HttpServerCodec()); + // 是以块方式写,添加 ChunkedWriteHandler 处理器 + pipeline.addLast(new ChunkedWriteHandler()); + + // http 数据在传输过程中是分段, HttpObjectAggregator 就是可以将多个段聚合 + // 这就就是为什么,当浏览器发送大量数据时,就会发出多次 http 请求 + pipeline.addLast(new HttpObjectAggregator(8192)); + + // WebSocketServerProtocolHandler 核心功能是【将 http 协议升级为 ws 协议】,保持长连接 + pipeline.addLast(new WebSocketServerProtocolHandler("/hello")); + + // 自定义的handler ,处理业务逻辑 + pipeline.addLast(new MyTextWebSocketFrameHandler()); + } + }); + + //启动服务器 + ChannelFuture channelFuture = serverBootstrap.bind(7000).sync(); + channelFuture.channel().closeFuture().sync(); + + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } + } + ``` + +* 处理器: + + ```java + public class MyTextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { + // TextWebSocketFrame 类型,表示一个文本帧(frame) + @Override + protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { + System.out.println("服务器收到消息 " + msg.text()); + // 回复消息 + ctx.writeAndFlush(new TextWebSocketFrame("服务器时间" + LocalDateTime.now() + " " + msg.text())); + } + + // 当web客户端连接后, 触发方法 + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + // id 表示唯一的值,LongText 是唯一的 ShortText 不是唯一 + System.out.println("handlerAdded 被调用" + ctx.channel().id().asLongText()); + System.out.println("handlerAdded 被调用" + ctx.channel().id().asShortText()); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + System.out.println("handlerRemoved 被调用" + ctx.channel().id().asLongText()); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + System.out.println("异常发生 " + cause.getMessage()); + ctx.close(); // 关闭连接 + } + } + ``` + +* HTML: + + ```html + <!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <title>Title + + + +
+ + + + +
+ + + ``` + + + + + +*** + + + +### 参数调优 + +#### CONNECT + +参数配置方式: + +* 客户端通过 .option() 方法配置参数,给 SocketChannel 配置参数 +* 服务器端: + * new ServerBootstrap().option(): 给 ServerSocketChannel 配置参数 + * new ServerBootstrap().childOption():给 SocketChannel 配置参数 + +CONNECT_TIMEOUT_MILLIS 参数: + +* 属于 SocketChannal 参数 +* 在客户端建立连接时,如果在指定毫秒内无法连接,会抛出 timeout 异常 + +* SO_TIMEOUT 主要用在阻塞 IO,阻塞 IO 中 accept,read 等都是无限等待的,如果不希望永远阻塞,可以调整超时时间 ```java -// 发送内容随机的数据包 -Random r = new Random(); -char c = '0'; -ByteBuf buf = ctx.alloc().buffer(); -for (int i = 0; i < 10; i++) { - byte[] bytes = new byte[10]; - for (int j = 0; j < r.nextInt(10); j++) { - bytes[j] = (byte) c; +public class ConnectionTimeoutTest { + public static void main(String[] args) { + NioEventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap bootstrap = new Bootstrap() + .group(group) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .channel(NioSocketChannel.class) + .handler(new LoggingHandler()); + ChannelFuture future = bootstrap.connect("127.0.0.1", 8080); + future.sync().channel().closeFuture().sync(); + } catch (Exception e) { + e.printStackTrace(); + log.debug("timeout"); + } finally { + group.shutdownGracefully(); + } } - c++; - buf.writeBytes(bytes); } -ctx.writeAndFlush(buf); ``` +**** + + + +#### SO_BACKLOG + +属于 ServerSocketChannal 参数,通过 `option(ChannelOption.SO_BACKLOG, value)` 来设置大小 + +在 Linux 2.2 之前,backlog 大小包括了两个队列的大小,在 2.2 之后,分别用下面两个参数来控制 + +* 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) + + + +**** + + + +#### 其他参数 + +ALLOCATOR:属于 SocketChannal 参数,用来分配 ByteBuf, ctx.alloc() + +RCVBUF_ALLOCATOR:属于 SocketChannal 参数 + +* 控制 Netty 接收缓冲区大小 +* 负责入站数据的分配,决定入站缓冲区的大小(并可动态调整),统一采用 direct 直接内存,具体池化还是非池化由 allocator 决定 + + + + + +*** + + + + + + + +# Tail + + + + + + + + + + + + + diff --git a/Prog.md b/Prog.md index 9c05c3b..b461c65 100644 --- a/Prog.md +++ b/Prog.md @@ -5946,7 +5946,7 @@ ThreadPoolExecutor 使用 int 的**高 3 位来表示线程池状态,低 29 } ``` -* execute():执行任务,但是没有返回值,没办法获取任务执行结果,出现异常会直接抛出任务执行时的异常。根据线程池中的线程数,选择添加任务时的处理方式 +* execute():执行任务,**但是没有返回值,没办法获取任务执行结果**,出现异常会直接抛出任务执行时的异常。根据线程池中的线程数,选择添加任务时的处理方式 ```java // command 可以是普通的 Runnable 实现类,也可以是 FutureTask,不能是 Callable @@ -13851,7 +13851,9 @@ ServerSocket 类: * 构造方法:`public ServerSocket(int port)` * 常用API:`public Socket accept()`,**阻塞等待**接收一个客户端的 Socket 管道连接请求,连接成功返回一个 Socket 对象 - 三次握手后 TCP 连接建立成功,服务器内核会把连接从 SYN 半连接队列中移出,移入 accept (全连接)队列,等待进程调用 accept 函数时把连接取出。如果进程不能及时调用 accept 函数,就会造成 accept 队列溢出,最终导致建立好的 TCP 连接被丢弃 + 三次握手后 TCP 连接建立成功,服务器内核会把连接从 SYN 半连接队列中移出,移入 accept 全连接队列,等待进程调用 accept 函数时把连接取出。如果进程不能及时调用 accept 函数,就会造成 accept 队列溢出,最终导致建立好的 TCP 连接被丢弃 + + 相当于客户端和服务器建立一个数据管道,管道一般不用 close @@ -14998,13 +15000,14 @@ public class ChannelTest { 向选择器注册通道:`SelectableChannel.register(Selector sel, int ops, Object att)` -选择器对通道的监听事件,需要通过第二个参数 ops 指定。监听的事件类型用四个常量表示: - -* 读 : SelectionKey.OP_READ (1) -* 写 : SelectionKey.OP_WRITE (4) -* 连接 : SelectionKey.OP_CONNECT (8) -* 接收 : SelectionKey.OP_ACCEPT (16) -* 若不止监听一个事件,可以使用位或操作符连接:`int interest = SelectionKey.OP_READ | SelectionKey.OP_WRITE` +* 参数一:选择器,指定当前 Channel 注册到的选择器 +* 参数二:选择器对通道的监听事件,监听的事件类型用四个常量表示 + * 读 : SelectionKey.OP_READ (1) + * 写 : SelectionKey.OP_WRITE (4) + * 连接 : SelectionKey.OP_CONNECT (8) + * 接收 : SelectionKey.OP_ACCEPT (16) + * 若不止监听一个事件,使用位或操作符连接:`int interest = SelectionKey.OP_READ | SelectionKey.OP_WRITE` +* 参数三:**关联一个附件**,可以是任何对象 **Selector API**: @@ -15024,7 +15027,7 @@ SelectionKey API: | ------------------------------------------- | -------------------------------------------------- | | public abstract void cancel() | 取消该键的通道与其选择器的注册 | | public abstract SelectableChannel channel() | 返回创建此键的通道,该方法在取消键之后仍将返回通道 | -| public final Object attachment() | 返回当前 key 关联的缓冲 | +| public final Object attachment() | 返回当前 key 关联的附件 | | public final boolean isAcceptable() | 检测此密钥的通道是否已准备好接受新的套接字连接 | | public final boolean isConnectable() | 检测此密钥的通道是否已完成或未完成其套接字连接操作 | | public final boolean isReadable() | 检测此密钥的频道是否可以阅读 | From 930c577f1b6c67fba915613e2be065641aafd89c Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 28 Sep 2021 23:27:29 +0800 Subject: [PATCH 015/122] Update Java Notes --- Tool.md | 56 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/Tool.md b/Tool.md index 9b5642f..4849926 100644 --- a/Tool.md +++ b/Tool.md @@ -871,7 +871,7 @@ gpasswd 是 Linux 工作组文件 /etc/group 和 /etc/gshadow 管理工具,用 可以看到命令的帮助文档 **man** [指令名称] 查看帮助文档 -比如 man ls。退出方式 q +比如 man ls,退出方式 q @@ -888,11 +888,11 @@ date 可以用来显示或设定系统的日期与时间 * -d<字符串>:显示字符串所指的日期与时间。字符串前后必须加上双引号; * -s<字符串>:根据字符串来设置日期与时间。字符串前后必须加上双引号 -* -u:显示GMT; +* -u:显示 GMT; * --version:显示版本信息 -查看时间:**date** ------->2020年 11月 30日 星期一 17:10:54 CST -查看指定格式时间:**date "+%Y-%m-%d %H:%M:%S"** -----> 2020-11-30 17:11:44 +查看时间:**date** → 2020年 11月 30日 星期一 17:10:54 CST +查看指定格式时间:**date "+%Y-%m-%d %H:%M:%S"** → 2020-11-30 17:11:44 设置日期指令:**date -s “2019-12-23 19:21:00”** @@ -903,17 +903,17 @@ date 可以用来显示或设定系统的日期与时间 ### id -id会显示用户以及所属群组的实际与有效ID。若两个ID相同,则仅显示实际ID。若仅指定用户名称,则显示目前用户的ID。 +id 会显示用户以及所属群组的实际与有效 ID。若两个 ID 相同,则仅显示实际 ID;若仅指定用户名称,则显示目前用户的 ID。 命令:id [-gGnru] [--help] [--version] [用户名称] //参数的顺序 -- -g或--group  显示用户所属群组的ID。 -- -G或--groups  显示用户所属附加群组的ID。 -- -n或--name  显示用户,所属群组或附加群组的名称。 -- -r或--real  显示实际ID。 -- -u或--user  显示用户ID。 +- -g 或--group  显示用户所属群组的 ID +- -G 或--groups  显示用户所属附加群组的 ID +- -n 或--name  显示用户,所属群组或附加群组的名称。 +- -r 或--real  显示实际 ID +- -u 或--user  显示用户 ID -> id命令参数虽然很多,但是常用的是不带参数的id命令,主要看他的uid和组信息 +> id 命令参数虽然很多,但是常用的是不带参数的id命令,主要看他的uid和组信息 @@ -923,7 +923,7 @@ id会显示用户以及所属群组的实际与有效ID。若两个ID相同, ### sudo -sudo:控制用户对系统命令的使用权限,root允许的操作。通过sudo可以提高普通用户的操作权限 +sudo:控制用户对系统命令的使用权限,root 允许的操作,通过 sudo 可以提高普通用户的操作权限 - -V 显示版本编号 - -h 会显示版本编号及指令的使用方式说明 @@ -934,9 +934,9 @@ sudo:控制用户对系统命令的使用权限,root允许的操作。通过su - -p prompt 可以更改问密码的提示语,其中 %u 会代换为使用者的帐号名称, %h 会显示主机名称 - -u username/#uid 不加此参数,代表要以 root 的身份执行指令,而加了此参数,可以以 username 的身份执行指令(#uid 为该 username 的使用者号码) - -s 执行环境变数中的 SHELL 所指定的 shell ,或是 /etc/passwd 里所指定的 shell -- -H 将环境变数中的 HOME 指定为要变更身份的使用者HOME目录(如不加 -u 参数就是系统管理者 root ) +- -H 将环境变数中的 HOME 指定为要变更身份的使用者 HOME 目录(如不加 -u 参数就是系统管理者 root ) - -command 要以系统管理者身份(或以 -u 更改为其他人)执行的指令 - **sudo -u root command -l**:指定root用户执行指令command + **sudo -u root command -l**:指定 root 用户执行指令 command @@ -948,30 +948,30 @@ sudo:控制用户对系统命令的使用权限,root允许的操作。通过su top:用于实时显示 process 的动态 -* -c:command属性进行了命令补全 +* -c:command 属性进行了命令补全 -* -p 进程号:显示指定pid的进程信息 +* -p 进程号:显示指定 pid 的进程信息 * -d 秒数:表示进程界面更新时间(每几秒刷新一次) * -H 表示线程模式 -`top -Hp 进程id`:分析该进程内各线程的cpu使用情况 +`top -Hp 进程 id`:分析该进程内各线程的cpu使用情况 ![](https://gitee.com/seazean/images/raw/master/Tool/top命令.png) **各进程(任务)的状态监控属性解释说明:** - PID — 进程id - TID — 线程id + PID — 进程 id + TID — 线程 id USER — 进程所有者 PR — 进程优先级 - NI — nice值。负值表示高优先级,正值表示低优先级 - VIRT — 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES - RES — 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA - SHR — 共享内存大小,单位kb - S — 进程状态。D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程 - %CPU — 上次更新到现在的CPU时间占用百分比 + NI — nice 值,负值表示高优先级,正值表示低优先级 + VIRT — 进程使用的虚拟内存总量,单位 kb,VIRT=SWAP+RES + RES — 进程使用的、未被换出的物理内存大小,单位 kb,RES=CODE+DATA + SHR — 共享内存大小,单位 kb + S — 进程状态,D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程 + %CPU — 上次更新到现在的 CPU 时间占用百分比 %MEM — 进程使用的物理内存百分比 - TIME+ — 进程使用的CPU时间总计,单位1/100秒 + TIME+ — 进程使用的 CPU 时间总计,单位 1/100 秒 COMMAND — 进程名称(命令名/命令行) @@ -1005,9 +1005,9 @@ Linux 系统中查看进程使用情况的命令是 ps 指令 **ps和top区别:** -* ps命令:可以查看进程的瞬间信息,是系统在过去执行的进程的静态快照 +* ps 命令:可以查看进程的瞬间信息,是系统在过去执行的进程的静态快照 -* top命令:可以持续的监视进程的动态信息 +* top 命令:可以持续的监视进程的动态信息 From 5059e39a06dabb0578f739df43df715c2b4b2080 Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 8 Oct 2021 22:16:48 +0800 Subject: [PATCH 016/122] Update Java Notes --- DB.md | 8 ++++---- Frame.md | 13 +++++-------- Java.md | 7 ++++--- Prog.md | 2 +- SSM.md | 2 +- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/DB.md b/DB.md index 380a105..1c56ae1 100644 --- a/DB.md +++ b/DB.md @@ -4418,7 +4418,7 @@ B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较 ##### 索引维护 -B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护 +B+ 树为了保持索引的有序性,在插入新值的时候需要做相应的维护 每个索引中每个块存储在磁盘页中,可能会出现以下两种情况: @@ -5400,7 +5400,7 @@ INSERT INTO `emp` (`id`, `name`, `age`, `salary`) VALUES('1','Tom','25','2300'); CREATE INDEX idx_emp_age_salary ON emp(age,salary); ``` -* 第一种是通过对返回数据进行排序,所有不是通过索引直接返回排序结果的排序都叫 FileSort 排序,会在内存中重新排序 +* 第一种是通过对返回数据进行排序,所有不通过索引直接返回结果的排序都叫 FileSort 排序,会在内存中重新排序 ```mysql EXPLAIN SELECT * FROM emp ORDER BY age DESC; -- 年龄降序 @@ -5408,7 +5408,7 @@ CREATE INDEX idx_emp_age_salary ON emp(age,salary); ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序1.png) -* 第二种通过有序索引顺序扫描直接返回有序数据,这种情况为 Using index,不需要额外排序,操作效率高 +* 第二种通过有序索引顺序扫描直接返回**有序数据**,这种情况为 Using index,不需要额外排序,操作效率高 ```mysql EXPLAIN SELECT id, age, salary FROM emp ORDER BY age DESC; @@ -5588,7 +5588,7 @@ MySQL 4.1 版本之后,开始支持 SQL 的子查询 ```mysql EXPLAIN SELECT * FROM tb_user_1 WHERE id > 200000 LIMIT 10; -- 写法 1 - EXPLAIN SELECT * FROM tb_user_1 WHERE id BETWEEN 200000 and 200010; -- 写法 2 + 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) diff --git a/Frame.md b/Frame.md index 0795ddb..55fbb3c 100644 --- a/Frame.md +++ b/Frame.md @@ -3241,7 +3241,7 @@ HTTP 协议是无状态的,浏览器和服务器间的请求响应一次,下 }); //启动服务器 - ChannelFuture channelFuture = serverBootstrap.bind(7000).sync(); + ChannelFuture channelFuture = serverBootstrap.bind(8080).sync(); channelFuture.channel().closeFuture().sync(); } finally { @@ -3300,7 +3300,7 @@ HTTP 协议是无状态的,浏览器和服务器间的请求响应一次,下 // 判断当前浏览器是否支持websocket if(window.WebSocket) { //go on - socket = new WebSocket("ws://localhost:7000/hello2"); + socket = new WebSocket("ws://localhost:8080/hello"); //相当于channelReado, ev 收到服务器端回送的消息 socket.onmessage = function (ev) { var rt = document.getElementById("responseText"); @@ -3325,11 +3325,12 @@ HTTP 协议是无状态的,浏览器和服务器间的请求响应一次,下 // 发送消息到服务器 function send(message) { - if(!window.socket) { //先判断socket是否创建好 + // 先判断socket是否创建好 + if(!window.socket) { return; } if(socket.readyState == WebSocket.OPEN) { - //通过socket 发送消息 + // 通过socket 发送消息 socket.send(message) } else { alert("连接没有开启"); @@ -3438,10 +3439,6 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 -# Tail - - - diff --git a/Java.md b/Java.md index 9d8f2b2..0fb3182 100644 --- a/Java.md +++ b/Java.md @@ -6780,7 +6780,7 @@ list.stream().filter(s -> s.startsWith("张")); Collection c = new ArrayList<>(); Stream listStream = c.stream(); -//Map集合获取流 +// Map集合获取流 // 先获取键的Stream流。 Stream keysStream = map.keySet().stream(); // 在获取值的Stream流 @@ -8579,6 +8579,7 @@ public class ReflectDemo { * 注解是 JDK1.5 的新特性 * 注解是给编译器或 JVM 看的,编译器或 JVM 可以根据注解来完成对应的功能 * 注解类似修饰符,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中 +* 父类中的注解是不能被子类继承的 注解作用: @@ -8757,9 +8758,9 @@ Class 类 API : * `boolean isAnnotationPresent(Class class)`:判断对象是否使用了指定的注解 * `boolean isAnnotation()`:此 Class 对象是否表示注释类型 -注解原理:注解本质是一个继承了 `Annotation` 的特殊接口,其具体实现类是 Java 运行时生成的**动态代理类**,通过反射获取注解时,返回的是运行时生成的动态代理对象 `$Proxy1`,通过代理对象调用自定义注解(接口)的方法,回调 `AnnotationInvocationHandler` 的 `invoke` 方法,该方法会从 `memberValues` 这个Map 中找出对应的值,而 `memberValues` 的来源是 Java 常量池 +注解原理:注解本质是**特殊接口**,继承了 `Annotation` ,其具体实现类是 Java 运行时生成的**动态代理类**,通过反射获取注解时,返回的是运行时生成的动态代理对象 `$Proxy1`,通过代理对象调用自定义注解(接口)的方法,回调 `AnnotationInvocationHandler` 的 `invoke` 方法,该方法会从 `memberValues` 这个 Map 中找出对应的值,而 `memberValues` 的来源是 Java 常量池 -解析注解数据的原理:注解在哪个成分上,就先拿哪个成分对象,比如注解作用在类上,则要该类的Class对象,再来拿上面的注解 +解析注解数据的原理:注解在哪个成分上,就先拿哪个成分对象,比如注解作用在类上,则要该类的 Class 对象,再来拿上面的注解 ```java public class AnnotationDemo{ diff --git a/Prog.md b/Prog.md index b461c65..6a1c1d1 100644 --- a/Prog.md +++ b/Prog.md @@ -13422,7 +13422,7 @@ epoll 的特点: * epoll 仅适用于 Linux 系统 * epoll 使用**一个文件描述符管理多个描述符**,将用户关系的文件描述符的事件存放到内核的一个事件表(个人理解成哑元节点) -* 没有最大描述符数量(并发连接)的限制,打开 fd 的上限远大于1024(1G 内存能监听约10万个端口) +* 没有最大描述符数量(并发连接)的限制,打开 fd 的上限远大于1024(1G 内存能监听约 10 万个端口) * epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait **只是轮询就绪链表**。当监听列表有设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中阻塞的进程,所以 epoll 实际上是**事件驱动**(每个事件关联上fd)的,降低了 system call 的时间复杂度 * epoll 内核中根据每个 fd 上的 callback 函数来实现,只有活跃的 socket 才会主动调用 callback,所以使用 epoll 没有前面两者的线性下降的性能问题,效率提高 diff --git a/SSM.md b/SSM.md index ec3e827..b6edc17 100644 --- a/SSM.md +++ b/SSM.md @@ -7412,7 +7412,7 @@ public void addAccount{} * 情况 6:Spring 的事务传播策略在**内部方法**调用时将不起作用,在一个 Service 内部,事务方法之间的嵌套调用,普通方法和事务方法之间的嵌套调用,都不会开启新的事务,事务注解要加到调用方法上才生效 - 原因:Spring 的事务都是使用 AOP 代理的模式,动态代理最终是要调用原始对象,而原始对象在去调用方法时是不会触发拦截器,就是**一个方法调用本对象的另一个方法**,所以事务也就无法生效 + 原因:Spring 的事务都是使用 AOP 代理的模式,动态代理 invoke 后会调用原始对象,而原始对象在去调用方法时是不会触发拦截器,就是**一个方法调用本对象的另一个方法**,所以事务也就无法生效 ```java @Transactional From 52c22bb29296add4fd3be699a70a2345105219d8 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 12 Oct 2021 22:54:15 +0800 Subject: [PATCH 017/122] Update Java Notes --- DB.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DB.md b/DB.md index 1c56ae1..c9b9ea3 100644 --- a/DB.md +++ b/DB.md @@ -2631,9 +2631,9 @@ update T set c=c+1 where ID=2; -流程说明:执行引擎将这行新数据更新到内存中(Buffer Pool)后,然后会将这个更新操作记录到 redo log buffer 里,此时 redo log 处于 prepare 状态,代表执行完成随时可以提交事务,然后执行器生成这个操作的 binlog,并**把 binlog 写入磁盘**,在提交事务后 **redo log 也持久化到磁盘** +流程说明:执行引擎将这行新数据更新到内存中(Buffer Pool)后,然后会将这个更新操作记录到 redo log buffer 里,此时 redo log 处于 prepare 状态,代表执行完成随时可以提交事务,然后执行器生成这个操作的 binlog 并**把 binlog 写入磁盘**,在提交事务后 **redo log 也持久化到磁盘** -redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致,也有利于主从复制,更好的保持主从数据的一致性 +redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 故障恢复数据: @@ -11497,7 +11497,7 @@ Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 D #### 读写穿透 -读写穿透模式 Read/Write Through Pattern:服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中,cache 负责将此数据读取和写入 DB,从而减轻了应用程序的职责 +读写穿透模式 Read/Write Through Pattern:服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中,cache 负责将此数据同步写入 DB,从而减轻了应用程序的职责 * 写操作:先查 cache,cache 中不存在,直接更新 DB;cache 中存在则先更新 cache,然后 cache 服务更新 DB(同步更新 cache 和 DB) From c6affc81fa8faf08c32d076606207a6eeea2f614 Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 15 Oct 2021 00:13:05 +0800 Subject: [PATCH 018/122] Update Java Notes --- Tool.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool.md b/Tool.md index 4849926..d5e0f99 100644 --- a/Tool.md +++ b/Tool.md @@ -328,7 +328,7 @@ git push origin branch-name:推送到远程仓库,origin 是引用名 ### 切换分支 -git checkout branch-name:切换到branch-name分支 +git checkout branch-name:切换到 branch-name 分支 From 2cf310aab9e0f5e65dcf5fe53964b95e3bc60e89 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 18 Oct 2021 22:40:37 +0800 Subject: [PATCH 019/122] Update Java Notes --- Prog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prog.md b/Prog.md index 6a1c1d1..6e602cb 100644 --- a/Prog.md +++ b/Prog.md @@ -8966,7 +8966,7 @@ public static void main(String[] args) throws InterruptedException { ###### await -总体流程是将 await 线程包装成 node 节点放入 ConditionObject 的阻条件塞队列,如果被唤醒就将 node 转移到 AQS 的执行阻塞队列,等待获取锁 +总体流程是将 await 线程包装成 node 节点放入 ConditionObject 的条件队列,如果被唤醒就将 node 转移到 AQS 的执行阻塞队列,等待获取锁 * 开始 Thread-0 持有锁,调用 await,线程进入 ConditionObject 等待,直到被唤醒或打断,调用 await 方法的线程都是持锁状态的,所以说逻辑里**不存在并发** From 198c9ed8d10dbac9a9426b657897f026d5aa9de3 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 1 Nov 2021 23:29:11 +0800 Subject: [PATCH 020/122] Update Java Notes --- DB.md | 14 +++++++----- Frame.md | 44 +++++++++++++++++++----------------- Java.md | 6 ++--- Tool.md | 68 ++++++++++++++++++++++++++++---------------------------- 4 files changed, 69 insertions(+), 63 deletions(-) diff --git a/DB.md b/DB.md index c9b9ea3..3bdd92b 100644 --- a/DB.md +++ b/DB.md @@ -4225,6 +4225,8 @@ InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,叶子 **检索过程**:辅助索引找到主键值,再通过聚簇索引(二分)找到数据页,最后通过数据页中的 Page Directory(二分)找到对应的数据分组,遍历组内所所有的数据找到数据行 +补充:无索引走全表查询,查到数据页后和上述步骤一致 + *** @@ -4233,7 +4235,7 @@ InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,叶子 ##### 索引实现 -InnoDB 使用 B+Tree 作为索引结构 +InnoDB 使用 B+Tree 作为索引结构,并且 InnoDB 一定有索引 主键索引: @@ -4739,7 +4741,7 @@ Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算 #### 定位低效 -慢 SQL 由三种原因造成: +SQL 执行慢有两种情况: * 偶尔慢:DB 在刷新脏页 * redo log 写满了 @@ -4898,12 +4900,12 @@ SQL 执行的顺序的标识,SQL 从大到小的执行 | type | 含义 | | ------ | ------------------------------------------------------------ | -| ALL | Full Table Scan,MySQL将遍历全表以找到匹配的行,全表扫描 | +| ALL | Full Table Scan,MySQL 将遍历全表以找到匹配的行,全表扫描 | | index | Full Index Scan,index 与 ALL 区别为 index 类型只遍历索引树 | -| range | 索引范围扫描,常见于between、<、>等的查询 | -| ref | 非唯一性索引扫描,返回匹配某个单独值的所有,本质上也是一种索引访问 | +| range | 索引范围扫描,常见于 between、<、> 等的查询 | +| ref | 非唯一性索引扫描,返回匹配某个单独值的所有记录,本质上也是一种索引访问 | | eq_ref | 唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描 | -| const | 当 MySQL 对查询某部分进行优化,并转换为一个常量时,使用该类型访问,如将主键置于where列表中,MySQL就能将该查询转换为一个常量 | +| const | 当 MySQL 对查询某部分进行优化,并转换为一个常量时,使用该类型访问,如将主键置于 where 列表中,MySQL 就能将该查询转换为一个常量 | | system | system 是 const 类型的特例,当查询的表只有一行的情况下,使用 system | | NULL | MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引 | diff --git a/Frame.md b/Frame.md index 55fbb3c..af8d30c 100644 --- a/Frame.md +++ b/Frame.md @@ -2858,26 +2858,26 @@ public class LoginRequestMessage extends Message { * 如果能确保编解码器不会保存状态,可以继承 MessageToMessageCodec 父类 -```java -@Slf4j -@ChannelHandler.Sharable -// 必须和 LengthFieldBasedFrameDecoder 一起使用,确保接到的 ByteBuf 消息是完整的 -public class MessageCodecSharable extends MessageToMessageCodec { - @Override - protected void encode(ChannelHandlerContext ctx, Message msg, List outList) throws Exception { - ByteBuf out = ctx.alloc().buffer(); - // 4 字节的魔数 - out.writeBytes(new byte[]{1, 2, 3, 4}); - // .... - outList.add(out); - } - - @Override - protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { - //.... - } -} -``` + ````java + @Slf4j + @ChannelHandler.Sharable + // 必须和 LengthFieldBasedFrameDecoder 一起使用,确保接到的 ByteBuf 消息是完整的 + public class MessageCodecSharable extends MessageToMessageCodec { + @Override + protected void encode(ChannelHandlerContext ctx, Message msg, List outList) throws Exception { + ByteBuf out = ctx.alloc().buffer(); + // 4 字节的魔数 + out.writeBytes(new byte[]{1, 2, 3, 4}); + // .... + outList.add(out); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + //.... + } + } + ```` @@ -3439,6 +3439,10 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 +# RocketMQ + + + diff --git a/Java.md b/Java.md index 0fb3182..ae24482 100644 --- a/Java.md +++ b/Java.md @@ -12975,9 +12975,9 @@ attributes[](属性表):属性表的每个项的值必须是 attribute_inf | Exceptions | 方法表 | 方法抛出的异常 | | EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 | | InnerClass | 类文件 | 内部类列表 | - | LineNumberTable | Code属性 | Java 源码的行号与字节码指令的对应关系 | - | LocalVariableTable | Code属性 | 方法的局部变量描述 | - | StackMapTable | Code属性 | JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 | + | LineNumberTable | Code 属性 | Java 源码的行号与字节码指令的对应关系 | + | LocalVariableTable | Code 属性 | 方法的局部变量描述 | + | StackMapTable | Code 属性 | JDK1.6 中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 | | Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 | | SourceFile | 类文件 | 记录源文件名称 | | SourceDebugExtension | 类文件 | 用于存储额外的调试信息 | diff --git a/Tool.md b/Tool.md index d5e0f99..d81b3ce 100644 --- a/Tool.md +++ b/Tool.md @@ -1003,7 +1003,7 @@ Linux 系统中查看进程使用情况的命令是 ps 指令 -**ps和top区别:** +**ps 和 top 区别:** * ps 命令:可以查看进程的瞬间信息,是系统在过去执行的进程的静态快照 @@ -1576,9 +1576,9 @@ tail 命令可用于查看文件的内容,有一个常用的参数 **-f** 常 `tail -f filename`:动态显示最尾部的内容 -`tail -n +2 txtfile.txt`:显示文件txtfile.txt 的内容,从第 2 行至文件末尾 +`tail -n +2 txtfile.txt`:显示文件 txtfile.txt 的内容,从第 2 行至文件末尾 -`tail -n 2 txtfile.txt`:显示文件txtfile.txt 的内容,最后2行 +`tail -n 2 txtfile.txt`:显示文件 txtfile.txt 的内容,最后 2 行 @@ -1605,28 +1605,28 @@ grep 指令用于查找内容包含指定的范本样式的文件,若不指定 grep [-abcEFGhHilLnqrsvVwxy][-A<显示列数>][-B<显示列数>][-C<显示列数>][-d<进行动作>][-e<范本样式>][-f<范本文件>][--help][范本样式][文件或目录...] ``` -* -c:只输出匹配行的计数。 -* -i:不区分大小写。 -* -h:查询多文件时不显示文件名。 -* -l:查询多文件时只输出包含匹配字符的文件名。 -* -n:显示匹配行及行号。 -* -s:不显示不存在或无匹配文本的错误信息。 -* -v:显示不包含匹配文本的所有行。 -* --color=auto :可以将找到的关键词部分加上颜色的显示。 +* -c:只输出匹配行的计数 +* -i:不区分大小写 +* -h:查询多文件时不显示文件名 +* -l:查询多文件时只输出包含匹配字符的文件名 +* -n:显示匹配行及行号 +* -s:不显示不存在或无匹配文本的错误信息 +* -v:显示不包含匹配文本的所有行 +* --color=auto :可以将找到的关键词部分加上颜色的显示 **管道符 |**:表示将前一个命令处理的结果传递给后面的命令处理。 -`grep aaaa Filename `:显示存在关键字aaaa的行 +`grep aaaa Filename `:显示存在关键字 aaaa 的行 -`grep -n aaaa Filename`:显示存在关键字aaaa的行,且显示行号 +`grep -n aaaa Filename`:显示存在关键字 aaaa 的行,且显示行号 -`grep -i aaaa Filename`:忽略大小写,显示存在关键字aaaa的行 +`grep -i aaaa Filename`:忽略大小写,显示存在关键字 aaaa 的行 `grep -v aaaa Filename`:显示存在关键字aaaa的所有行 -`ps -ef | grep sshd`:查找包含sshd进程的进程信息 +`ps -ef | grep sshd`:查找包含 sshd 进程的进程信息 -` ps -ef|grep -c sshd`:查找sshd相关的进程个数 +` ps -ef | grep -c sshd`:查找 sshd 相关的进程个数 @@ -1642,32 +1642,32 @@ grep [-abcEFGhHilLnqrsvVwxy][-A<显示列数>][-B<显示列数>][-C<显示列数 - 通过 `命令 >> 文件` 将**命令的成功结果** **追加** 指定文件的后面 - 通过 `命令 &>> 文件` 将 **命令的失败结果** **追加** 指定文件的后面 -`echo "程序员" >> a.txt`:将程序员追加到a.txt后面 +`echo "程序员" >> a.txt`:将程序员追加到 a.txt 后面 -`cat 不存在的目录 &>> error.log`:将错误信息追加到error.log文件 +`cat 不存在的目录 &>> error.log`:将错误信息追加到 error.log 文件 #### awk -AWK是一种处理文本文件的语言,是一个强大的文本分析工具。 +AWK 是一种处理文本文件的语言,是一个强大的文本分析工具。 ```shell awk [options] 'script' var=value file(s) awk [options] -f scriptfile var=value file(s) ``` -* -F fs 指定输入文件折分隔符,fs是一个字符串或者是一个正则表达式 -* -v var=value赋值一个用户定义变量 -* -f 从脚本文件中读取awk命令 -* $n(数字) 获取**第几段**内容 -* $0 获取 **当前行** 内容 -* NF 表示当前行共有多少个字段 -* $NF 代表 最后一个字段 +* -F fs:指定输入文件折分隔符,fs 是一个字符串或者是一个正则表达式 +* -v:var=value 赋值一个用户定义变量 +* -f:从脚本文件中读取 awk 命令 +* $n(数字):获取**第几段**内容 +* $0:获取**当前行** 内容 +* NF:表示当前行共有多少个字段 +* $NF:代表最后一个字段 -* $(NF-1) 代表 倒数第二个字段 +* $(NF-1):代表倒数第二个字段 -* NR 代表 处理的是第几行 +* NR:代表处理的是第几行 * ```shell 命令:awk 'BEGIN{初始化操作}{每行都执行} END{结束时操作}' @@ -1728,19 +1728,19 @@ zhouba 98 44 46 #### find -find命令用来在指定目录下查找文件。任何位于参数之前的字符串都将被视为欲查找的目录名。如果使用该命令时,不设置任何参数,则find命令将在当前目录下查找子目录与文件。并且将查找到的子目录和文件全部进行显示 +find 命令用来在指定目录下查找文件。任何位于参数之前的字符串都将被视为查找的目录名。如果使用该命令不设置任何参数,将在当前目录下查找子目录与文件,并且将查找到的子目录和文件全部进行显示 命令:find <指定目录> <指定条件> <指定内容> * `find . -name "*.gz"`:将目前目录及其子目录下所有延伸档名是 gz 的文件查询出来 -* `find . -ctime -1`:将目前目录及其子目录下所有最近 1天内更新过的文件查询出来 -* ` find / -name 'seazean'`:全局搜索seazean +* `find . -ctime -1`:将目前目录及其子目录下所有最近 1 天内更新过的文件查询出来 +* ` find / -name 'seazean'`:全局搜索 seazean #### read -read命令用于从标准输入读取数值。 +read 命令用于从标准输入读取数值。 read 内部命令被用来从标准输入读取单行数据。这个命令可以用来读取键盘输入,当使用重定向的时候,可以读取文件中的一行数据。 @@ -1759,10 +1759,10 @@ sort [-bcdfimMnr][文件] ``` * -n 依照数值的大小排序 -* -r 以相反的顺序来排序(sort默认的排序方式是**升序**,改成降序,加-r) +* -r 以相反的顺序来排序(sort 默认的排序方式是**升序**,改成降序,加 -r) * -u 去掉重复 -面试题:一列数字,输出最大的4个不重复的数 +面试题:一列数字,输出最大的 4 个不重复的数 ```sh sort -ur a.txt | head -n 4 From 1084367d4d843471dac22a0a5b14fdf21e03c9ae Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 3 Nov 2021 01:25:48 +0800 Subject: [PATCH 021/122] Update README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9952e97..6433260 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,5 @@ * 所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 * Java.md 更新后大于 1M,导致网页无法显示,所以划分为 Java 和 Program 两个文档。 +* 笔记的编写是基于 Windows 平台,可能会因为平台的不同而造成空格、制表符的显示效果不同。 + From 3f38376e44437614802b7f3e4e73d3a0b0e875cd Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 3 Nov 2021 23:46:19 +0800 Subject: [PATCH 022/122] Update Java Notes --- DB.md | 8 ++--- Frame.md | 97 ++++++++++++++++++++++++++++---------------------------- 2 files changed, 52 insertions(+), 53 deletions(-) diff --git a/DB.md b/DB.md index 3bdd92b..6c28da1 100644 --- a/DB.md +++ b/DB.md @@ -4606,7 +4606,7 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 **适用条件**: -* 需要存储引擎将索引中的数据与条件进行判断,所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM +* 需要存储引擎将索引中的数据与条件进行判断(所以条件列必须都在同一个索引中),所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM * 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 * 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了,索引下推的目的减少 IO 次数也就失去了意义 @@ -4673,8 +4673,6 @@ CREATE INDEX idx_area ON table_name(area(7)); - - *** @@ -4900,12 +4898,12 @@ SQL 执行的顺序的标识,SQL 从大到小的执行 | type | 含义 | | ------ | ------------------------------------------------------------ | -| ALL | Full Table Scan,MySQL 将遍历全表以找到匹配的行,全表扫描 | +| ALL | Full Table Scan,MySQL 将遍历全表以找到匹配的行,全表扫描,如果是 InnoDB 引擎是扫描聚簇索引 | | index | Full Index Scan,index 与 ALL 区别为 index 类型只遍历索引树 | | range | 索引范围扫描,常见于 between、<、> 等的查询 | | ref | 非唯一性索引扫描,返回匹配某个单独值的所有记录,本质上也是一种索引访问 | | eq_ref | 唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描 | -| const | 当 MySQL 对查询某部分进行优化,并转换为一个常量时,使用该类型访问,如将主键置于 where 列表中,MySQL 就能将该查询转换为一个常量 | +| const | 通过主键或者唯一索引来定位一条记录 | | system | system 是 const 类型的特例,当查询的表只有一行的情况下,使用 system | | NULL | MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引 | diff --git a/Frame.md b/Frame.md index af8d30c..8a731be 100644 --- a/Frame.md +++ b/Frame.md @@ -177,7 +177,7 @@ Path 下配置:`%MAVEN_HOME%\bin` ### 手动搭建 -1. 在 E 盘下创建目录 `mvnproject 进入该目录,作为我们的操作目录 +1. 在 E 盘下创建目录 mvnproject 进入该目录,作为我们的操作目录 2. 创建我们的 Maven 项目,创建一个目录 `project-java` 作为我们的项目文件夹,并进入到该目录 @@ -283,7 +283,7 @@ Path 下配置:`%MAVEN_HOME%\bin` 1. 在 IDEA 中配置 Maven,选择 maven3.6.1 防止依赖问题 IDEA配置Maven -2. 创建 Maven,New Module → Maven→ 不选中 Create from archetype +2. 创建 Maven,New Module → Maven → 不选中 Create from archetype 3. 填写项目的坐标 @@ -320,15 +320,15 @@ Path 下配置:`%MAVEN_HOME%\bin` -web 工程: +Web 工程: -1. 选择 web 对应的原型骨架(选择 Maven 开头的是简化的) +1. 选择 Web 对应的原型骨架(选择 Maven 开头的是简化的) ![](https://gitee.com/seazean/images/raw/master/Frame/IDEA创建Maven-webapp.png) -2. 通过原型创建 web 项目得到的目录结构是不全的,因此需要我们自行补全,同时要标记正确 +2. 通过原型创建 Web 项目得到的目录结构是不全的,因此需要我们自行补全,同时要标记正确 -3. web 工程创建之后需要启动运行,使用 tomcat 插件来运行项目,在 `pom.xml` 中添加插件的坐标: +3. Web 工程创建之后需要启动运行,使用 tomcat 插件来运行项目,在 `pom.xml` 中添加插件的坐标: ```xml @@ -413,7 +413,6 @@ web 工程: 注意:直接依赖和间接依赖其实也是一个相对关系 - 依赖传递的冲突问题:在依赖传递过程中产生了冲突,有三种优先法则 @@ -423,9 +422,7 @@ web 工程: * 特殊优先:当同级配置了相同资源的不同版本时,后配置的覆盖先配置的 - - -**可选依赖:**对外隐藏当前所依赖的资源,不透明 +**可选依赖**:对外隐藏当前所依赖的资源,不透明 ```xml @@ -437,7 +434,7 @@ web 工程: ``` -**排除依赖:主动**断开依赖的资源,被排除的资源无需指定版本 +**排除依赖**:主动断开依赖的资源,被排除的资源无需指定版本 ```xml @@ -663,30 +660,31 @@ Maven 的插件用来执行生命周期中的相关事件 - 间接依赖 ssm_dao、ssm_pojo - ```xml - - - - demo - ssm_service - 1.0-SNAPSHOT - - - - - - - - - - - - - - ``` - + + ```xml + + + + demo + ssm_service + 1.0-SNAPSHOT + + + + + + + + + + + + + + ``` + - 修改 web.xml 配置文件中加载 Spring 环境的配置文件名称,使用*通配,加载所有 applicationContext- 开始的配置文件: - + ```xml @@ -694,13 +692,13 @@ Maven 的插件用来执行生命周期中的相关事件 classpath*:applicationContext-*.xml ``` - + - spring-mvc - + ```xml ``` - + *** @@ -856,7 +854,11 @@ Maven 的插件用来执行生命周期中的相关事件 * 属性类别: - 1.自定义属性 2.内置属性 3.Setting属性 4.Java系统属性 5.环境变量属性 + 1. 自定义属性 + 2. 内置属性 + 3. setting 属性 + 4. Java 系统属性 + 5. 环境变量属性 * 自定义属性: @@ -898,13 +900,14 @@ Maven 的插件用来执行生命周期中的相关事件 * vresion 是 1.0-SNAPSHOT - ```xml - demo - ssm - 1.0-SNAPSHOT - ``` -* Setting 属性 + ```xml + demo + ssm + 1.0-SNAPSHOT + ``` + +* setting 属性 - 使用 Maven 配置文件 setting.xml 中的标签属性,用于动态配置 @@ -920,7 +923,7 @@ Maven 的插件用来执行生命周期中的相关事件 调用格式: - ``` + ```xml ${user.home} ``` @@ -936,7 +939,7 @@ Maven 的插件用来执行生命周期中的相关事件 调用格式: - ``` + ```xml ${env.JAVA_HOME} ``` @@ -977,9 +980,7 @@ RELEASE(发布版本) - 里程碑版本:表明一个版本的里程碑(版本内部)。这样的版本同下一个正式版本相比,相对来说不是很稳定,有待更多的测试 -范例: -- 5.1.9.RELEASE From 4a7852b72fb38999353067289027312ec3f0f40e Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 6 Nov 2021 01:01:13 +0800 Subject: [PATCH 023/122] Update Java Notes --- DB.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/DB.md b/DB.md index 6c28da1..579e96a 100644 --- a/DB.md +++ b/DB.md @@ -573,7 +573,12 @@ SELECT * FROM t WHERE id = 1; ##### 扫描行数 -优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。找到一个最优的执行方案,用最小的代价去执行语句 +优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。 + +* 根据搜索条件找出所有可能的使用的索引 +* 计算全表扫描的代价 +* 计算使用不同索引执行 SQL 的的代价 +* 找到一个最优的执行方案,用最小的代价去执行语句 在数据库里面,扫描行数是影响执行代价的因素之一,扫描的行数越少意味着访问磁盘的次数越少,消耗的 CPU 资源越少,优化器还会结合是否使用临时表、是否排序等因素进行综合判断 @@ -1190,13 +1195,12 @@ LIMIT SELECT 列名1,列名2,... FROM 表名; ``` -* 去除重复查询 - 注意:只有值全部重复的才可以去除 - +* 去除重复查询:只有值全部重复的才可以去除,需要创建临时表辅助查询 + ```mysql SELECT DISTINCT 列名1,列名2,... FROM 表名; ``` - + * 计算列的值(四则运算) ```mysql @@ -2012,7 +2016,7 @@ WHERE #### 内连接 -查询原理:内连接查询的是两张表有交集的部分数据 +查询原理:内连接查询的是两张表有交集的部分数据,分为驱动表和被驱动表,首先查询驱动表得到结果集,然后根据结果集中的每一条记录都分别到被驱动表中查找匹配 * 显式内连接 @@ -4671,6 +4675,42 @@ CREATE INDEX idx_area ON table_name(area(7)); +**** + + + +#### 索引合并 + +使用多个索引来完成一次查询的执行方法叫做索引合并 index merge + +* Intersection 索引合并: + + ```sql + SELECT * FROM table_test WHERE key1 = 'a' AND key3 = 'b'; # key1 和 key3 列都是单列索引、二级索引 + ``` + + 从不同索引中扫描到的记录的 id 值取交集(相同 id),然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 + +* Union 索引合并: + + ```sql + SELECT * FROM table_test WHERE key1 = 'a' OR key3 = 'b'; + ``` + + 从不同索引中扫描到的记录的 id 值取并集,然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 + +* Sort-Union 索引合并 + + ```sql + SELECT * FROM table_test WHERE key1 < 'a' OR key3 > 'b'; + ``` + + 先将从不同索引中扫描到的记录的主键值进行排序,再按照 Union 索引合并的方式进行查询 + + + + + *** From 9cb437d3b2c92d71fd9db1f6f986f939d792c4ee Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 8 Nov 2021 01:33:28 +0800 Subject: [PATCH 024/122] Update Java Notes --- Frame.md | 1037 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ SSM.md | 19 +- 2 files changed, 1053 insertions(+), 3 deletions(-) diff --git a/Frame.md b/Frame.md index 8a731be..587bb0c 100644 --- a/Frame.md +++ b/Frame.md @@ -3432,6 +3432,8 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 + + *** @@ -3442,15 +3444,1050 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 # RocketMQ +## 消息队列 + +消息队列是一种先进先出的数据结构,常见的应用场景: + +* 应用解耦:系统的耦合性越高,容错性就越低 + + 实例:用户创建订单后,耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障都会造成下单异常,影响用户使用体验。使用消息队列解耦合,比如物流系统发生故障,需要几分钟恢复,将物流系统要处理的数据缓存到消息队列中,用户的下单操作正常完成。等待物流系统正常后处理存在消息队列中的订单消息即可,终端系统感知不到物流系统发生过几分钟故障 + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-解耦.png) + +* 流量削峰:应用系统如果遇到系统请求流量的瞬间猛增,有可能会将系统压垮,使用消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以提高系统的稳定性和用户体验。 + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-流量削峰.png) + +* 数据分发:让数据在多个系统更加之间进行流通,数据的产生方不需要关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消息队列中直接获取数据 + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-数据分发.png) + +主要缺点包含以下几点: + +* 系统可用性降低:系统引入的外部依赖越多,系统稳定性越差,一旦 MQ 宕机,就会对业务造成影响 + + 引申问题:如何保证 MQ 的高可用? + +* 系统复杂度提高:MQ 的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过 MQ 进行异步调用 + + 引申问题:如何保证消息没有被重复消费?怎么处理消息丢失情况?那么保证消息传递的顺序性? + +* 一致性问题:A 系统处理完业务,通过 MQ 给 B、C、D 三个系统发消息数据,如果 B 系统、C 系统处理成功,D 系统处理失败 + + 引申问题:如何保证消息数据处理的一致性? + + + + + +**** + + + + + +## 概念模型 + +### 安装测试 + +安装需要 Java 环境,下载解压后进入安装目录,进行启动: + +* 启动 NameServer + + ```sh + # 1.启动 NameServer + nohup sh bin/mqnamesrv & + # 2.查看启动日志 + tail -f ~/logs/rocketmqlogs/namesrv.log + ``` + + RocketMQ 默认的虚拟机内存较大,需要编辑如下两个配置文件,修改 JVM 内存大小 + + ```shell + # 编辑runbroker.sh和runserver.sh修改默认JVM大小 + vi runbroker.sh + vi runserver.sh + ``` + + 参考配置:JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m" + +* 启动 Broker + + ```sh + # 1.启动 Broker + nohup sh bin/mqbroker -n localhost:9876 autoCreateTopicEnable=true & + # 2.查看启动日志 + tail -f ~/logs/rocketmqlogs/broker.log + ``` + +* 发送消息: + + ```sh + # 1.设置环境变量 + export NAMESRV_ADDR=localhost:9876 + # 2.使用安装包的 Demo 发送消息 + sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer + ``` + +* 接受消息: + + ```sh + # 1.设置环境变量 + export NAMESRV_ADDR=localhost:9876 + # 2.接收消息 + sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer + +* 关闭 RocketMQ: + + ```sh + # 1.关闭 NameServer + sh bin/mqshutdown namesrv + # 2.关闭 Broker + sh bin/mqshutdown broker + + + +**** + + + +### 基本概念 + +#### 服务相关 + +RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息,NameServer 负责管理 Broker + +* 代理服务器(Broker Server):消息中转角色,负责**存储消息、转发消息**。在 RocketMQ 系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 + +* 名字服务(Name Server):充当**路由消息**的提供者。生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表 + +* 消息生产者(Producer):负责**生产消息**,把业务应用系统里产生的消息发送到 Broker 服务器。RocketMQ 提供多种发送方式,同步发送、异步发送、顺序发送、单向发送,同步和异步方式均需要 Broker 返回确认信息,单向发送不需要;可以通过 MQ 的负载均衡模块选择相应的 Broker 集群队列进行消息投递,投递的过程支持快速失败并且低延迟 +* 消息消费者(Consumer):负责**消费消息**,一般是后台系统负责异步消费,一个消息消费者会从 Broker 服务器拉取消息、并将其提供给应用程序。从用户应用的角度而提供了两种消费形式: + * 拉取式消费(Pull Consumer):应用通主动调用 Consumer 的拉消息方法从 Broker 服务器拉消息,主动权由应用控制,一旦获取了批量消息,应用就会启动消费过程 + * 推动式消费(Push Consumer):该模式下 Broker 收到数据后会主动推送给消费端,实时性较高 + +* 生产者组(Producer Group):同一类 Producer 的集合,都发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,**则 Broker 服务器会联系同一生产者组的其他生产者实例以提交或回溯消费** + +* 消费者组(Consumer Group):同一类 Consumer 的集合,消费者实例必须订阅完全相同的 Topic,消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面更容易的实现负载均衡和容错。RocketMQ 支持两种消息模式: + * 集群消费(Clustering):相同 Consumer Group 的每个 Consumer 实例平均分摊消息 + * 广播消费(Broadcasting):相同 Consumer Group 的每个 Consumer 实例都接收全量的消息 + + + +*** + + + +#### 消息相关 + +每个 Broker 可以存储多个 Topic 的消息,每个 Topic 的消息也可以分片存储于不同的 Broker,Message Queue(消息队列)是用于存储消息的物理地址,每个 Topic 中的消息地址存储于多个 Message Queue 中 + +* 主题(Topic):表示一类消息的集合,每个主题包含若干条消息,每条消息只属于一个主题,是 RocketMQ 消息订阅的基本单位 + +* 消息(Message):消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ 中每个消息拥有唯一的 Message ID,且可以携带具有业务标识的 Key,系统提供了通过 Message ID 和 Key 查询消息的功能 + +* 标签(Tag):为消息设置的标志,用于同一主题下区分不同类型的消息。标签能够有效地保持代码的清晰度和连贯性,并优化 RocketMQ 提供的查询系统,消费者可以根据 Tag 实现对不同子主题的不同消费逻辑,实现更好的扩展性 + +* 普通顺序消息(Normal Ordered Message):消费者通过同一个消息队列(Topic 分区)收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的 + +* 严格顺序消息(Strictly Ordered Message):消费者收到的所有消息均是有顺序的 + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/concept.md + + + + + +*** + + + + + +### 集群设计 + +#### 集群模式 + +常用的以下几种模式: + +* 单 Master 模式:这种方式风险较大,一旦 Broker 重启或者宕机,会导致整个服务不可用 + +* 多 Master 模式:一个集群无 Slave,全是 Master + + - 优点:配置简单,单个 Master 宕机或重启维护对应用无影响,在磁盘配置为 RAID10 时,即使机器宕机不可恢复情况下,由于 RAID10 磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高 + + - 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响 + +* 多 Master 多 Slave 模式(同步):每个 Master 配置一个 Slave,有多对 Master-Slave,HA 采用同步双写方式,即只有主备都写成功,才向应用返回成功 + + * 优点:数据与服务都无单点故障,Master 宕机情况下,消息无延迟,服务可用性与数据可用性都非常高 + * 缺点:性能比异步复制略低(大约低 10% 左右),发送单个消息的 RT 略高,目前不能实现主节点宕机,备机自动切换为主机 + +* 多 Master 多 Slave 模式(异步):HA 采用异步复制的方式,会造成主备有短暂的消息延迟(毫秒级别) + + - 优点:即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,同时 Master 宕机后,消费者仍然可以从 Slave 消费,而且此过程对应用透明,不需要人工干预,性能同多 Master 模式几乎一样 + + - 缺点:Master 宕机,磁盘损坏情况下会丢失少量消息 + + + + +*** + + + +#### 系统架构 + +NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态注册与发现。NameServer 通常是集群的方式部署,各实例间相互不进行信息通讯。Broker 向每一台 NameServer 注册自己的路由信息,所以每个 NameServer 实例上面**都保存一份完整的路由信息**。当某个 NameServer 因某种原因下线了,Broker 仍可以向其它 NameServer 同步其路由信息 + +NameServer 主要包括两个功能: + +* Broker 管理,NameServer 接受 Broker 集群的注册信息并保存下来作为路由信息的基本数据,提供**心跳检测**检查 Broker 活性 +* 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 + +BrokerServer 主要负责消息的存储、投递和查询以及服务高可用保证,为了实现这些功能,Broker 包含了以下几个重要子模块: +* Remoting Module:整个 Broker 的实体,负责处理来自 clients 端的请求 +* Client Manager:负责管理客户端(Producer/Consumer)和维护 Consumer 的 Topic 订阅信息 +* Store Service:提供方便简单的 API 接口处理消息存储到物理硬盘和查询功能 + +* HA Service:高可用服务,提供 Master Broker 和 Slave Broker 之间的数据同步功能 + +* Index Service:根据特定的 Message key 对投递到 Broker 的消息进行索引服务,以提供消息的快速查询 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Broker工作流程.png) + + + +*** + + + +#### 集群架构 + +RocketMQ 网络部署特点: + +- 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 的从服务器才会参与消息的读负载(读写分离) + +- Producer 与 NameServer 集群中的其中**一个节点(随机选择)建立长连接**,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master 建立长连接,且定时向 Master **发送心跳**。Producer 完全无状态,可集群部署 + +- Consumer 与 NameServer 集群中的其中一个节点(随机选择)建立长连接,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master、Slave 建立长连接,且定时向 Master、Slave 发送心跳 + + Consumer 既可以从 Master 订阅消息,也可以从 Slave 订阅消息,在向 Master 拉取消息时,Master 服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读 I/O),以及从服务器是否可读等因素建议下一次是从 Master 还是 Slave 拉取 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-集群架构.png) + +集群工作流程: + +- 启动 NameServer 监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心 +- Broker 启动,跟所有的 NameServer 保持长连接,定时发送心跳包。心跳包中包含当前 Broker 信息(IP、端口等)以及存储所有 Topic 信息。注册成功后,NameServer 集群中就有 Topic 跟 Broker 的映射关系 +- 收发消息前,先创建 Topic,创建 Topic 时需要指定该 Topic 要存储在哪些 Broker 上,也可以在发送消息时自动创建 Topic +- Producer 发送消息,启动时先跟 NameServer 集群中的其中一台建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,轮询从队列列表中选择一个队列,然后与队列所在的 Broker 建立长连接从而向 Broker 发消息。 +- Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接,获取当前订阅 Topic 存在哪些 Broker 上,然后直接跟 Broker 建立连接通道,开始消费消息 + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/architecture.md + + + + + +**** + + + + + +## 基本操作 + +### 基本样例 + +#### 工作流程 + +导入 MQ 客户端依赖 + +```xml + + org.apache.rocketmq + rocketmq-client + 4.4.0 + +``` + +消息发送者步骤分析: + +1. 创建消息生产者 producer,并制定生产者组名 +2. 指定 Nameserver 地址 +3. 启动 producer +4. 创建消息对象,指定主题 Topic、Tag 和消息体 +5. 发送消息 +6. 关闭生产者 producer + +消息消费者步骤分析: + +1. 创建消费者 Consumer,制定消费者组名 +2. 指定 Nameserver 地址 +3. 订阅主题 Topic 和 Tag +4. 设置回调函数,处理消息 +5. 启动消费者 consumer + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/RocketMQ_Example.md + + + +*** + + + +#### 发送消息 + +##### 同步发送 + +使用 RocketMQ 发送三种类型的消息:同步消息、异步消息和单向消息,其中前两种消息是可靠的,因为会有发送是否成功的应答 + +这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知 + +```java +public class SyncProducer { + public static void main(String[] args) throws Exception { + // 实例化消息生产者Producer + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // 设置NameServer的地址 + producer.setNamesrvAddr("localhost:9876"); + // 启动Producer实例 + producer.start(); + for (int i = 0; i < 100; i++) { + // 创建消息,并指定Topic,Tag和消息体 + Message msg = new Message( + "TopicTest" /* Topic */, + "TagA" /* Tag */, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */); + + // 发送消息到一个Broker + SendResult sendResult = producer.send(msg); + // 通过sendResult返回消息是否成功送达 + System.out.printf("%s%n", sendResult); + } + // 如果不再发送消息,关闭Producer实例。 + producer.shutdown(); + } +} +``` + + + +*** + + + +##### 异步发送 + +异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待 Broker 的响应 + +```java +public class AsyncProducer { + public static void main(String[] args) throws Exception { + // 实例化消息生产者Producer + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // 设置NameServer的地址 + producer.setNamesrvAddr("localhost:9876"); + // 启动Producer实例 + producer.start(); + producer.setRetryTimesWhenSendAsyncFailed(0); + + int messageCount = 100; + // 根据消息数量实例化倒计时计算器 + final CountDownLatch2 countDownLatch = new CountDownLatch2(messageCount); + for (int i = 0; i < messageCount; i++) { + final int index = i; + // 创建消息,并指定Topic,Tag和消息体 + Message msg = new Message("TopicTest", "TagA", "OrderID188", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + + // SendCallback接收异步返回结果的回调 + producer.send(msg, new SendCallback() { + // 发送成功回调函数 + @Override + public void onSuccess(SendResult sendResult) { + countDownLatch.countDown(); + System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId()); + } + + @Override + public void onException(Throwable e) { + countDownLatch.countDown(); + System.out.printf("%-10d Exception %s %n", index, e); + e.printStackTrace(); + } + }); + } + // 等待5s + countDownLatch.await(5, TimeUnit.SECONDS); + // 如果不再发送消息,关闭Producer实例。 + producer.shutdown(); + } +} +``` + + + +*** + + + +##### 单向发送 + +单向发送主要用在不特别关心发送结果的场景,例如日志发送 + +```java +public class OnewayProducer { + public static void main(String[] args) throws Exception{ + // 实例化消息生产者Producer + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // 设置NameServer的地址 + producer.setNamesrvAddr("localhost:9876"); + // 启动Producer实例 + producer.start(); + for (int i = 0; i < 100; i++) { + // 创建消息,并指定Topic,Tag和消息体 + Message msg = new Message("TopicTest","TagA", + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + // 发送单向消息,没有任何返回结果 + producer.sendOneway(msg); + } + // 如果不再发送消息,关闭Producer实例。 + producer.shutdown(); + } +} +``` + + + +**** + + + +#### 消费消息 + +```java +public class Consumer { + public static void main(String[] args) throws InterruptedException, MQClientException { + // 实例化消费者 + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); + // 设置NameServer的地址 + consumer.setNamesrvAddr("localhost:9876"); + + // 订阅一个或者多个Topic,以及Tag来过滤需要消费的消息 + consumer.subscribe("TopicTest", "*"); + // 注册消息监听器,回调实现类来处理从broker拉取回来的消息 + consumer.registerMessageListener(new MessageListenerConcurrently() { + // 接受消息内容 + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + // 标记该消息已经被成功消费 + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + // 启动消费者实例 + consumer.start(); + System.out.printf("Consumer Started.%n"); + } +} +``` + + + + + +**** +### 顺序消息 +#### 原理解析 + +消息有序指的是一类消息消费时,能按照发送的顺序来消费。例如:一个订单产生了三条消息分别是订单创建、订单付款、订单完成。消费时要按照这个顺序消费才能有意义,但是同时订单之间是可以并行消费的,RocketMQ 可以严格的保证消息有序。 + +顺序消息分为全局顺序消息与分区顺序消息, + +- 全局顺序:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。 适用于性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景 +- 分区顺序:对于指定的一个 Topic,所有消息根据 sharding key 进行分区,同一个分组内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用于性能要求高,以 sharding key 作为分区字段,在同一个区中严格的按照 FIFO 原则进行消息发布和消费的场景 + +在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列),而消费消息是从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当发送和消费参与的 queue 只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个 queue,消息都是有序的 + + + +*** + + + +#### 代码实例 + +一个订单的顺序流程是:创建、付款、推送、完成,订单号相同的消息会被先后发送到同一个队列中,消费时同一个 OrderId 获取到的肯定是同一个队列 + +```java +public class Producer { + public static void main(String[] args) throws Exception { + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + producer.setNamesrvAddr("127.0.0.1:9876"); + producer.start(); + // 标签集合 + String[] tags = new String[]{"TagA", "TagC", "TagD"}; + + // 订单列表 + List orderList = new Producer().buildOrders(); + + Date date = new Date(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String dateStr = sdf.format(date); + for (int i = 0; i < 10; i++) { + // 加个时间前缀 + String body = dateStr + " Hello RocketMQ " + orderList.get(i); + Message msg = new Message("OrderTopic", tags[i % tags.length], "KEY" + i, body.getBytes()); + /** + * 参数一:消息对象 + * 参数二:消息队列的选择器 + * 参数三:选择队列的业务标识(订单 ID) + */ + SendResult sendResult = producer.send(msg, new MessageQueueSelector() { + @Override + /** + * mqs:队列集合 + * msg:消息对象 + * arg:业务标识的参数 + */ + public MessageQueue select(List mqs, Message msg, Object arg) { + Long id = (Long) arg; + long index = id % mqs.size(); // 根据订单id选择发送queue + return mqs.get((int) index); + } + }, orderList.get(i).getOrderId());//订单id + + System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s", + sendResult.getSendStatus(), + sendResult.getMessageQueue().getQueueId(), + body)); + } + + producer.shutdown(); + } + + // 订单的步骤 + private static class OrderStep { + private long orderId; + private String desc; + // set + get + } + + // 生成模拟订单数据 + private List buildOrders() { + List orderList = new ArrayList(); + + OrderStep orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("创建"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111065L); + orderDemo.setDesc("创建"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("付款"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103117235L); + orderDemo.setDesc("创建"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111065L); + orderDemo.setDesc("付款"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103117235L); + orderDemo.setDesc("付款"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111065L); + orderDemo.setDesc("完成"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("推送"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103117235L); + orderDemo.setDesc("完成"); + orderList.add(orderDemo); + + orderDemo = new OrderStep(); + orderDemo.setOrderId(15103111039L); + orderDemo.setDesc("完成"); + orderList.add(orderDemo); + + return orderList; + } +} +``` + +```java +// 顺序消息消费,带事务方式(应用可控制Offset什么时候提交) +public class ConsumerInOrder { + public static void main(String[] args) throws Exception { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3"); + consumer.setNamesrvAddr("127.0.0.1:9876"); + // 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费 + // 如果非第一次启动,那么按照上次消费的位置继续消费 + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + // 订阅三个tag + consumer.subscribe("OrderTopic", "TagA || TagC || TagD"); + consumer.registerMessageListener(new MessageListenerOrderly() { + Random random = new Random(); + @Override + public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { + context.setAutoCommit(true); + for (MessageExt msg : msgs) { + // 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序 + System.out.println("consumeThread=" + Thread.currentThread().getName() + "queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody())); + } + return ConsumeOrderlyStatus.SUCCESS; + } + }); + consumer.start(); + System.out.println("Consumer Started."); + } +} +``` + + + + + +***** + + + +### 延时消息 + +提交了一个订单就可以发送一个延时消息,1h 后去检查这个订单的状态,如果还是未付款就取消订单释放库存 + +RocketMQ 并不支持任意时间的延时,需要设置几个固定的延时等级,从 1s 到 2h 分别对应着等级 1 到 18,消息消费失败会进入延时消息队列,消息发送时间与设置的延时等级和重试次数有关,详见代码 `SendMessageProcessor.java` + +```java +private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"; +``` + +```java +public class ScheduledMessageProducer { + public static void main(String[] args) throws Exception { + // 实例化一个生产者来产生延时消息 + DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); + producer.setNamesrvAddr("127.0.0.1:9876"); + // 启动生产者 + producer.start(); + int totalMessagesToSend = 100; + for (int i = 0; i < totalMessagesToSend; i++) { + Message message = new Message("DelayTopic", ("Hello scheduled message " + i).getBytes()); + // 设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel) + message.setDelayTimeLevel(3); + // 发送消息 + producer.send(message); + } + // 关闭生产者 + producer.shutdown(); + } +} +``` + +```java +public class ScheduledMessageConsumer { + public static void main(String[] args) throws Exception { + // 实例化消费者 + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleConsumer"); + consumer.setNamesrvAddr("127.0.0.1:9876"); + // 订阅Topics + consumer.subscribe("DelayTopic", "*"); + // 注册消息监听者 + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List messages, ConsumeConcurrentlyContext context) { + for (MessageExt message : messages) { + // 打印延迟的时间段 + System.out.println("Receive message[msgId=" + message.getMsgId() + "] " + (System.currentTimeMillis() - message.getBornTimestamp()) + "ms later");} + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + // 启动消费者 + consumer.start(); + } +} +``` + + + +**** + + + +### 批量消息 + +批量发送消息能显著提高传递小消息的性能,限制是这些批量消息应该有相同的 topic,相同的 waitStoreMsgOK,而且不能是延时消息,并且这一批消息的总大小不应超过 4MB + +```java +public class Producer { + + public static void main(String[] args) throws Exception { + DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup") + producer.setNamesrvAddr("127.0.0.1:9876"); + //启动producer + producer.start(); + + List msgs = new ArrayList(); + // 创建消息对象,指定主题Topic、Tag和消息体 + Message msg1 = new Message("BatchTopic", "Tag1", ("Hello World" + 1).getBytes()); + Message msg2 = new Message("BatchTopic", "Tag1", ("Hello World" + 2).getBytes()); + Message msg3 = new Message("BatchTopic", "Tag1", ("Hello World" + 3).getBytes()); + + msgs.add(msg1); + msgs.add(msg2); + msgs.add(msg3); + + // 发送消息 + SendResult result = producer.send(msgs); + System.out.println("发送结果:" + result); + // 关闭生产者producer + producer.shutdown(); + } +} +``` + +当发送大批量数据时,可能不确定消息是否超过了大小限制(4MB),所以需要将消息列表分割一下 + +```java +public class ListSplitter implements Iterator> { + private final int SIZE_LIMIT = 1024 * 1024 * 4; + private final List messages; + private int currIndex; + + public ListSplitter(List messages) { + this.messages = messages; + } + + @Override + public boolean hasNext() { + return currIndex < messages.size(); + } + + @Override + public List next() { + int startIndex = getStartIndex(); + int nextIndex = startIndex; + int totalSize = 0; + for (; nextIndex < messages.size(); nextIndex++) { + Message message = messages.get(nextIndex); + int tmpSize = calcMessageSize(message); + // 单个消息超过了最大的限制 + if (tmpSize + totalSize > SIZE_LIMIT) { + break; + } else { + totalSize += tmpSize; + } + } + List subList = messages.subList(startIndex, nextIndex); + currIndex = nextIndex; + return subList; + } + + private int getStartIndex() { + Message currMessage = messages.get(currIndex); + int tmpSize = calcMessageSize(currMessage); + while (tmpSize > SIZE_LIMIT) { + currIndex += 1; + Message message = messages.get(curIndex); + tmpSize = calcMessageSize(message); + } + return currIndex; + } + + private int calcMessageSize(Message message) { + int tmpSize = message.getTopic().length() + message.getBody().length; + Map properties = message.getProperties(); + for (Map.Entry entry : properties.entrySet()) { + tmpSize += entry.getKey().length() + entry.getValue().length(); + } + tmpSize = tmpSize + 20; // 增加⽇日志的开销20字节 + return tmpSize; + } + + public static void main(String[] args) { + //把大的消息分裂成若干个小的消息 + ListSplitter splitter = new ListSplitter(messages); + while (splitter.hasNext()) { + try { + List listItem = splitter.next(); + producer.send(listItem); + } catch (Exception e) { + e.printStackTrace(); + //处理error + } + } + } +} +``` + + + + + +*** + + + +### 过滤消息 + +#### 基本语法 + +RocketMQ 定义了一些基本语法来支持过滤特性,可以很容易地扩展: + +- 数值比较,比如:>,>=,<,<=,BETWEEN,= +- 字符比较,比如:=,<>,IN +- IS NULL 或者 IS NOT NULL +- 逻辑符号 AND,OR,NOT + +常量支持类型为: + +- 数值,比如 123,3.1415 +- 字符,比如 'abc',必须用单引号包裹起来 +- NULL,特殊的常量 +- 布尔值,TRUE 或 FALSE + +只有使用 push 模式的消费者才能用使用SQL92标准的sql语句,接口如下: + +```java +public void subscribe(final String topic, final MessageSelector messageSelector) +``` + +例如:消费者接收包含 TAGA 或 TAGB 或 TAGC 的消息 + +```java +DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE"); +consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC"); +``` + + + +**** + + + +#### 代码实例 + +发送消息时,通过 putUserProperty 来设置消息的属性 + +```java +public class Producer { + public static void main(String[] args) throws Exception { + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + producer.setNamesrvAddr("127.0.0.1:9876"); + producer.start(); + for (int i = 0; i < 10; i++) { + Message msg = new Message("FilterTopic", "tag", + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + // 设置一些属性 + msg.putUserProperty("i", String.valueOf(i)); + SendResult sendResult = producer.send(msg); + } + producer.shutdown(); + } +} +``` + +使用 SQL 筛选过滤消息: + +```java +public class Consumer { + public static void main(String[] args) throws Exception { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); + consumer.setNamesrvAddr("127.0.0.1:9876"); + // 过滤属性大于 5 的消息 + consumer.subscribe("FilterTopic", MessageSelector.bySql("i>5")); + + // 设置回调函数,处理消息 + consumer.registerMessageListener(new MessageListenerConcurrently() { + //接受消息内容 + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + for (MessageExt msg : msgs) { + System.out.println("consumeThread=" + Thread.currentThread().getName() + "," + new String(msg.getBody())); + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + // 启动消费者consumer + consumer.start(); + } +} +``` + + + + + +*** + + + +### 事务消息 + +#### 事务机制 + +事务消息共有三种状态,提交状态、回滚状态、中间状态: + +- TransactionStatus.CommitTransaction:提交事务,允许消费者消费此消息。 +- TransactionStatus.RollbackTransaction:回滚事务,代表该消息将被删除,不允许被消费 +- TransactionStatus.Unknown:中间状态,代表需要检查消息队列来确定状态 + +使用限制: + +1. 事务消息不支持延时消息和批量消息 +2. Broker 配置文件中的参数 `transactionTimeout` 为特定时间,事务消息将在特定时间长度之后被检查。当发送事务消息时,还可以通过设置用户属性 `CHECK_IMMUNITY_TIME_IN_SECONDS` 来改变这个限制,该参数优先于 `transactionTimeout` 参数 +3. 为了避免单个消息被检查太多次而导致半队列消息累积,默认将单个消息的检查次数限制为 15 次,开发者可以通过 Broker 配置文件的 `transactionCheckMax` 参数来修改此限制。如果已经检查某条消息超过 N 次(N = `transactionCheckMax`), 则 Broker 将丢弃此消息,在默认情况下会打印错误日志。可以通过重写 `AbstractTransactionalMessageCheckListener` 类来修改这个行为 +4. 事务性消息可能不止一次被检查或消费 +5. 提交给用户的目标主题消息可能会失败,可以查看日志的记录。事务的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望事务消息不丢失、并且事务完整性得到保证,可以使用同步的双重写入机制 +6. 事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询,MQ 服务器能通过消息的生产者 ID 查询到消费者 + + + +**** + + + +#### 代码实例 + +使用 **TransactionMQProducer** 类创建事务性生产者,并指定唯一的 `ProducerGroup`,就可以设置自定义线程池来处理这些检查请求,执行本地事务后、需要根据执行结果对消息队列进行回复 + +```java +public class Producer { + public static void main(String[] args) throws MQClientException, InterruptedException { + // 创建事务监听器 + TransactionListener transactionListener = new TransactionListenerImpl(); + // 创建消息生产者 + TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); + ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS); + producer.setExecutorService(executorService); + // 生产者的监听器 + producer.setTransactionListener(transactionListener); + producer.start(); + String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"}; + for (int i = 0; i < 10; i++) { + try { + Message msg = new Message("TransactionTopic", tags[i % tags.length], "KEY" + i, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + SendResult sendResult = producer.sendMessageInTransaction(msg, null); + System.out.printf("%s%n", sendResult); + Thread.sleep(10); + } catch (MQClientException | UnsupportedEncodingException e) { + e.printStackTrace(); + } + } + //Thread.sleep(1000000); + //producer.shutdown();暂时不关闭 + } +} +``` + +消费者代码和前面的实例相同的 + +实现事务的监听接口,当发送半消息成功时 + +* `executeLocalTransaction` 方法来执行本地事务,返回三个事务状态之一 +* `checkLocalTransaction` 方法检查本地事务状态,响应消息队列的检查请求,返回三个事务状态之一 + +```java +public class TransactionListenerImpl implements TransactionListener { + private AtomicInteger transactionIndex = new AtomicInteger(0); + private ConcurrentHashMap localTrans = new ConcurrentHashMap<>(); + + @Override + public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { + int value = transactionIndex.getAndIncrement(); + int status = value % 3; + // 将事务ID和状态存入 map 集合 + localTrans.put(msg.getTransactionId(), status); + return LocalTransactionState.UNKNOW; + } + + @Override + public LocalTransactionState checkLocalTransaction(MessageExt msg) { + // 从 map 集合读出当前事务对应的状态 + Integer status = localTrans.get(msg.getTransactionId()); + if (null != status) { + switch (status) { + case 0: + return LocalTransactionState.UNKNOW; + case 1: + return LocalTransactionState.COMMIT_MESSAGE; + case 2: + return LocalTransactionState.ROLLBACK_MESSAGE; + } + } + return LocalTransactionState.COMMIT_MESSAGE; + } +} +``` + + + + + + + + + + + +**** + + + + + +## 高级特性 + +待补充笔记: + +* https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#3-%E6%B6%88%E6%81%AF%E8%BF%87%E6%BB%A4 +* https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#5-%E4%BA%8B%E5%8A%A1%E6%B6%88%E6%81%AF + + + + + +**** +## 源码分析 diff --git a/SSM.md b/SSM.md index b6edc17..740fc8f 100644 --- a/SSM.md +++ b/SSM.md @@ -12869,7 +12869,7 @@ DispatcherServlet#checkMultipart: 校验分类:客户端校验和服务端校验 * 格式校验 - * 客户端:使用Js技术,利用正则表达式校验 + * 客户端:使用 js 技术,利用正则表达式校验 * 服务端:使用校验框架 * 逻辑校验 * 客户端:使用ajax发送要校验的数据,在服务端完成逻辑校验,返回校验结果 @@ -12903,8 +12903,9 @@ DispatcherServlet#checkMultipart: ``` **注意:** -tomcat7:搭配hibernate-validator版本5.*.*.Final -tomcat8.5↑:搭配hibernate-validator版本6.*.*.Final + +* tomcat7:搭配 hibernate-validator 版本 5.*.*.Final +* tomcat8.5:搭配 hibernate-validator 版本 6.*.*.Final @@ -12917,9 +12918,13 @@ tomcat8.5↑:搭配hibernate-validator版本6.*.*.Final ##### 开启校验 名称:@Valid、@Validated + 类型:形参注解 + 位置:处理器类中的实体类类型的方法形参前方 + 作用:设定对当前实体类类型参数进行校验 + 范例: ```java @@ -12934,9 +12939,13 @@ public String addEmployee(@Valid Employee employee) { ##### 设置校验规则 名称:@NotNull + 类型:属性注解等 + 位置:实体类属性上方 + 作用:设定当前属性校验规则 + 范例:每个校验规则所携带的参数不同,根据校验规则进行相应的调整,具体的校验规则查看对应的校验框架进行获取 ```java @@ -13012,9 +13021,13 @@ public String addEmployee(@Valid Employee employee, Errors errors, Model model){ #### 嵌套校验 名称:@Valid + 类型:属性注解 + 位置:实体类中的引用类型属性上方 + 作用:设定当前应用类型属性中的属性开启校验 + 范例: ```java From 83738e501b849e3ed2aac19ddca971cef4c39333 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 9 Nov 2021 01:32:40 +0800 Subject: [PATCH 025/122] Update Java Notes --- Frame.md | 137 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 119 insertions(+), 18 deletions(-) diff --git a/Frame.md b/Frame.md index 587bb0c..0c18f72 100644 --- a/Frame.md +++ b/Frame.md @@ -3594,7 +3594,7 @@ RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Produce -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/concept.md +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/concept.md(基础知识部分的笔记参考官方文档编写) @@ -3934,7 +3934,7 @@ public class Consumer { -#### 代码实例 +#### 代码实现 一个订单的顺序流程是:创建、付款、推送、完成,订单号相同的消息会被先后发送到同一个队列中,消费时同一个 OrderId 获取到的肯定是同一个队列 @@ -4282,7 +4282,7 @@ RocketMQ 定义了一些基本语法来支持过滤特性,可以很容易地 - NULL,特殊的常量 - 布尔值,TRUE 或 FALSE -只有使用 push 模式的消费者才能用使用SQL92标准的sql语句,接口如下: +只有使用 push 模式的消费者才能用使用 SQL92 标准的 sql 语句,接口如下: ```java public void subscribe(final String topic, final MessageSelector messageSelector) @@ -4297,13 +4297,33 @@ consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC"); +*** + + + +#### 原理解析 + +RocketMQ 分布式消息队列的消息过滤方式是在 Consumer 端订阅消息时再做消息过滤的。因为 RocketMQ 在 Producer 端写入消息和在 Consumer 端订阅消息采用**分离存储**的机制实现,Consumer 端订阅消息是需要通过 ConsumeQueue 这个消息消费的逻辑队列拿到一个索引,然后再从 CommitLog 里面读取真正的消息实体内容,所以绕不开其存储结构。 + +ConsumeQueue 的存储结构如下,有 8 个字节存储的 Message Tag 的哈希值,基于 Tag 的消息过滤就是基于这个字段 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消费队列结构.png) + +* Tag 过滤:Consumer 端订阅消息时指定 Topic 和 TAG,然后将订阅请求构建成一个 SubscriptionData,发送一个 Pull 消息的请求给 Broker 端。Broker 端用这些数据先构建一个 MessageFilter,然后传给文件存储层 Store。Store 从 ConsumeQueue 读取到一条记录后,会用它记录的消息 tag hash 值去做过滤。因为在服务端只是根据 hashcode 进行判断,无法精确对 tag 原始字符串进行过滤,所以消费端拉取到消息后,还需要对消息的原始 tag 字符串进行比对,如果不同,则丢弃该消息,不进行消息消费 + +* SQL92 过滤:工作流程和 Tag 过滤大致一样,只是在 Store 层的具体过滤方式不一样。真正的 SQL expression 的构建和执行由 rocketmq-filter 模块负责,每次过滤都去执行 SQL 表达式会影响效率,所以 RocketMQ 使用了 BloomFilter 来避免了每次都去执行 + + + + + **** -#### 代码实例 +#### 代码实现 -发送消息时,通过 putUserProperty 来设置消息的属性 +发送消息时,通过 putUserProperty 来设置消息的属性,SQL92 的表达式上下文为消息的属性 ```java public class Producer { @@ -4360,7 +4380,97 @@ public class Consumer { ### 事务消息 -#### 事务机制 +#### 工作流程 + +RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交事务消息,同时增加一个补偿逻辑来处理二阶段超时或者失败的消息,如下图所示: + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-事务消息.png) + +事务消息的大致方案分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程 + +1. 事务消息发送及提交: + + * 发送消息(Half 消息) + + * 服务端响应消息写入结果 + + * 根据发送结果执行本地事务(如果写入失败,此时 Half 消息对业务不可见,本地逻辑不执行) + * 根据本地事务状态执行 Commit 或者 Rollback(Commit 操作生成消息索引,消息对消费者可见) + +2. 补偿流程: + + * 对没有 Commit/Rollback 的事务消息(pending 状态的消息),从服务端发起一次回查 + * Producer 收到回查消息,检查回查消息对应的本地事务的状态 + + * 根据本地事务状态,重新 Commit 或者 Rollback + + 补偿阶段用于解决消息 Commit 或者 Rollback 发生超时或者失败的情况 + + + +**** + + + +#### 原理解析 + +##### 不可见性 + +事务消息相对普通消息最大的特点就是**一阶段发送的消息对用户是不可见的**,因为对于 Half 消息,会备份原消息的主题与消息消费队列,然后改变主题为 RMQ_SYS_TRANS_HALF_TOPIC,由于消费组未订阅该主题,故消费端无法消费 Half 类型的消息 + +RocketMQ 会开启一个定时任务,从 Topic 为 RMQ_SYS_TRANS_HALF_TOPIC 中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息 + +在 RocketMQ 中,每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个二级索引来读取消息实体内容,如图: + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-事务工作流程.png) + +RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 Topic 和 Queue 等属性进行替换,同时将原来的 Topic 和 Queue 信息存储到消息的属性中,因为消息主题被替换,消息并不会转发到该原主题的消息消费队列,消费者无法感知消息的存在,不会消费 + + + +**** + + + +##### OP消息 + +一阶段写入不可见的消息后,二阶段操作: + +* 如果执行 Commit 操作,则需要让消息对用户可见,构建出 Half 消息的索引。一阶段的 Half 消息写到一个特殊的 Topic,所以构建索引时需要读取出 Half 消息,并将 Topic 和 Queue 替换成真正的目标的 Topic 和 Queue,然后通过一次普通消息的写入操作来生成一条对用户可见的消息 + +* 如果是 Rollback 则需要撤销一阶段的消息,因为消息本就不可见,所以并不需要真正撤销消息(实际上 RocketMQ 也无法去删除一条消息,因为是顺序写文件的),为了区分这条消息没有确定的状态(Pending 状态),RocketMQ 用 Op 消息标识事务消息已经确定的状态(Commit 或者 Rollback) + +事务消息无论是 Commit 或者 Rollback 都会记录一个 Op 操作,两者的区别是 Commit 相对于 Rollback 在写入 Op 消息前创建 Half 消息的索引。如果一条事务消息没有对应的 Op 消息,说明这个事务的状态还无法确定(可能是二阶段失败了) + +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 次回查还是无法得知事务状态,则默认回滚该消息 + + + + + +**** + + + +#### 基本使用 + +##### 使用方式 事务消息共有三种状态,提交状态、回滚状态、中间状态: @@ -4379,11 +4489,11 @@ public class Consumer { -**** +*** -#### 代码实例 +##### 代码实现 使用 **TransactionMQProducer** 类创建事务性生产者,并指定唯一的 `ProducerGroup`,就可以设置自定义线程池来处理这些检查请求,执行本地事务后、需要根据执行结果对消息队列进行回复 @@ -4419,7 +4529,7 @@ public class Producer { 消费者代码和前面的实例相同的 -实现事务的监听接口,当发送半消息成功时 +实现事务的监听接口,当发送半消息成功时: * `executeLocalTransaction` 方法来执行本地事务,返回三个事务状态之一 * `checkLocalTransaction` 方法检查本地事务状态,响应消息队列的检查请求,返回三个事务状态之一 @@ -4461,12 +4571,6 @@ public class TransactionListenerImpl implements TransactionListener { - - - - - - **** @@ -4475,10 +4579,7 @@ public class TransactionListenerImpl implements TransactionListener { ## 高级特性 -待补充笔记: -* https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#3-%E6%B6%88%E6%81%AF%E8%BF%87%E6%BB%A4 -* https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#5-%E4%BA%8B%E5%8A%A1%E6%B6%88%E6%81%AF From 0ab4708b5dd076fbafe35d18588380b329c54027 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 11 Nov 2021 01:30:36 +0800 Subject: [PATCH 026/122] Update Java Notes --- Frame.md | 292 ++++++++++++++++++++++++++----------------------------- Java.md | 4 +- Prog.md | 6 +- 3 files changed, 142 insertions(+), 160 deletions(-) diff --git a/Frame.md b/Frame.md index 0c18f72..fb9ae14 100644 --- a/Frame.md +++ b/Frame.md @@ -3444,7 +3444,9 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 # RocketMQ -## 消息队列 +## 基本介绍 + +### 消息队列 消息队列是一种先进先出的数据结构,常见的应用场景: @@ -3484,10 +3486,6 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 - - -## 概念模型 - ### 安装测试 安装需要 Java 环境,下载解压后进入安装目录,进行启动: @@ -3547,107 +3545,28 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 -**** - - - -### 基本概念 - -#### 服务相关 - -RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息,NameServer 负责管理 Broker - -* 代理服务器(Broker Server):消息中转角色,负责**存储消息、转发消息**。在 RocketMQ 系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 - -* 名字服务(Name Server):充当**路由消息**的提供者。生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表 - -* 消息生产者(Producer):负责**生产消息**,把业务应用系统里产生的消息发送到 Broker 服务器。RocketMQ 提供多种发送方式,同步发送、异步发送、顺序发送、单向发送,同步和异步方式均需要 Broker 返回确认信息,单向发送不需要;可以通过 MQ 的负载均衡模块选择相应的 Broker 集群队列进行消息投递,投递的过程支持快速失败并且低延迟 -* 消息消费者(Consumer):负责**消费消息**,一般是后台系统负责异步消费,一个消息消费者会从 Broker 服务器拉取消息、并将其提供给应用程序。从用户应用的角度而提供了两种消费形式: - * 拉取式消费(Pull Consumer):应用通主动调用 Consumer 的拉消息方法从 Broker 服务器拉消息,主动权由应用控制,一旦获取了批量消息,应用就会启动消费过程 - * 推动式消费(Push Consumer):该模式下 Broker 收到数据后会主动推送给消费端,实时性较高 - -* 生产者组(Producer Group):同一类 Producer 的集合,都发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,**则 Broker 服务器会联系同一生产者组的其他生产者实例以提交或回溯消费** - -* 消费者组(Consumer Group):同一类 Consumer 的集合,消费者实例必须订阅完全相同的 Topic,消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面更容易的实现负载均衡和容错。RocketMQ 支持两种消息模式: - * 集群消费(Clustering):相同 Consumer Group 的每个 Consumer 实例平均分摊消息 - * 广播消费(Broadcasting):相同 Consumer Group 的每个 Consumer 实例都接收全量的消息 - - - -*** - - - -#### 消息相关 - -每个 Broker 可以存储多个 Topic 的消息,每个 Topic 的消息也可以分片存储于不同的 Broker,Message Queue(消息队列)是用于存储消息的物理地址,每个 Topic 中的消息地址存储于多个 Message Queue 中 - -* 主题(Topic):表示一类消息的集合,每个主题包含若干条消息,每条消息只属于一个主题,是 RocketMQ 消息订阅的基本单位 - -* 消息(Message):消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ 中每个消息拥有唯一的 Message ID,且可以携带具有业务标识的 Key,系统提供了通过 Message ID 和 Key 查询消息的功能 - -* 标签(Tag):为消息设置的标志,用于同一主题下区分不同类型的消息。标签能够有效地保持代码的清晰度和连贯性,并优化 RocketMQ 提供的查询系统,消费者可以根据 Tag 实现对不同子主题的不同消费逻辑,实现更好的扩展性 - -* 普通顺序消息(Normal Ordered Message):消费者通过同一个消息队列(Topic 分区)收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的 - -* 严格顺序消息(Strictly Ordered Message):消费者收到的所有消息均是有顺序的 - - - -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/concept.md(基础知识部分的笔记参考官方文档编写) - - - - - -*** - - - - - -### 集群设计 - -#### 集群模式 - -常用的以下几种模式: - -* 单 Master 模式:这种方式风险较大,一旦 Broker 重启或者宕机,会导致整个服务不可用 - -* 多 Master 模式:一个集群无 Slave,全是 Master - - - 优点:配置简单,单个 Master 宕机或重启维护对应用无影响,在磁盘配置为 RAID10 时,即使机器宕机不可恢复情况下,由于 RAID10 磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高 - - - 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响 - -* 多 Master 多 Slave 模式(同步):每个 Master 配置一个 Slave,有多对 Master-Slave,HA 采用同步双写方式,即只有主备都写成功,才向应用返回成功 - - * 优点:数据与服务都无单点故障,Master 宕机情况下,消息无延迟,服务可用性与数据可用性都非常高 - * 缺点:性能比异步复制略低(大约低 10% 左右),发送单个消息的 RT 略高,目前不能实现主节点宕机,备机自动切换为主机 - -* 多 Master 多 Slave 模式(异步):HA 采用异步复制的方式,会造成主备有短暂的消息延迟(毫秒级别) - - - 优点:即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,同时 Master 宕机后,消费者仍然可以从 Slave 消费,而且此过程对应用透明,不需要人工干预,性能同多 Master 模式几乎一样 - - - 缺点:Master 宕机,磁盘损坏情况下会丢失少量消息 - - - - *** -#### 系统架构 +### 工作流程 -NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态注册与发现。NameServer 通常是集群的方式部署,各实例间相互不进行信息通讯。Broker 向每一台 NameServer 注册自己的路由信息,所以每个 NameServer 实例上面**都保存一份完整的路由信息**。当某个 NameServer 因某种原因下线了,Broker 仍可以向其它 NameServer 同步其路由信息 +NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态注册与发现,生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表 NameServer 主要包括两个功能: * Broker 管理,NameServer 接受 Broker 集群的注册信息并保存下来作为路由信息的基本数据,提供**心跳检测**检查 Broker 活性 * 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 -BrokerServer 主要负责消息的存储、投递和查询以及服务高可用保证,为了实现这些功能,Broker 包含了以下几个重要子模块: +NameServer 特点: + +* NameServer 通常是集群的方式部署,各实例间相互不进行信息通讯 +* Broker 向每一台 NameServer 注册自己的路由信息,所以每个 NameServer 实例上面**都保存一份完整的路由信息** +* 当某个 NameServer 因某种原因下线了,Broker 仍可以向其它 NameServer 同步其路由信息 + +BrokerServer 主要负责消息的存储、投递和查询以及服务高可用保证,在 RocketMQ 系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 + +Broker 包含了以下几个重要子模块: * Remoting Module:整个 Broker 的实体,负责处理来自 clients 端的请求 @@ -3667,35 +3586,36 @@ BrokerServer 主要负责消息的存储、投递和查询以及服务高可用 -#### 集群架构 +### 相关概念 -RocketMQ 网络部署特点: +RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息,NameServer 负责管理 Broker -- NameServer 是一个几乎**无状态节点**,节点之间相互独立,无任何信息同步 +* 消息生产者(Producer):负责**生产消息**,把业务应用系统里产生的消息发送到 Broker 服务器。RocketMQ 提供多种发送方式,同步发送、异步发送、顺序发送、单向发送,同步和异步方式均需要 Broker 返回确认信息,单向发送不需要;可以通过 MQ 的负载均衡模块选择相应的 Broker 集群队列进行消息投递,投递的过程支持快速失败并且低延迟 +* 消息消费者(Consumer):负责**消费消息**,一般是后台系统负责异步消费,一个消息消费者会从 Broker 服务器拉取消息、并将其提供给应用程序。从用户应用的角度而提供了两种消费形式: + * 拉取式消费(Pull Consumer):应用通主动调用 Consumer 的拉消息方法从 Broker 服务器拉消息,主动权由应用控制,一旦获取了批量消息,应用就会启动消费过程 + * 推动式消费(Push Consumer):该模式下 Broker 收到数据后会主动推送给消费端,实时性较高 -- Broker 部署相对复杂,Broker 分为 Master 与 Slave,Master 可以部署多个,一个 Master 可以对应多个 Slave,但是一个 Slave 只能对应一个 Master,Master 与 Slave 的对应关系通过指定相同 BrokerName、不同 BrokerId 来定义,BrokerId 为 0 是 Master,非 0 表示 Slave。**每个 Broker 与 NameServer 集群中的所有节点建立长连接**,定时注册 Topic 信息到所有 NameServer +* 生产者组(Producer Group):同一类 Producer 的集合,都发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,**则 Broker 服务器会联系同一生产者组的其他生产者实例以提交或回溯消费** - 注意:部署架构上也支持一 Master 多 Slave,但只有 BrokerId=1 的从服务器才会参与消息的读负载(读写分离) +* 消费者组(Consumer Group):同一类 Consumer 的集合,消费者实例必须订阅完全相同的 Topic,消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面更容易的实现负载均衡和容错。RocketMQ 支持两种消息模式: + * 集群消费(Clustering):相同 Consumer Group 的每个 Consumer 实例平均分摊消息 + * 广播消费(Broadcasting):相同 Consumer Group 的每个 Consumer 实例都接收全量的消息 -- Producer 与 NameServer 集群中的其中**一个节点(随机选择)建立长连接**,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master 建立长连接,且定时向 Master **发送心跳**。Producer 完全无状态,可集群部署 +每个 Broker 可以存储多个 Topic 的消息,每个 Topic 的消息也可以分片存储于不同的 Broker,Message Queue(消息队列)是用于存储消息的物理地址,每个 Topic 中的消息地址存储于多个 Message Queue 中 -- Consumer 与 NameServer 集群中的其中一个节点(随机选择)建立长连接,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master、Slave 建立长连接,且定时向 Master、Slave 发送心跳 +* 主题(Topic):表示一类消息的集合,每个主题包含若干条消息,每条消息只属于一个主题,是 RocketMQ 消息订阅的基本单位 - Consumer 既可以从 Master 订阅消息,也可以从 Slave 订阅消息,在向 Master 拉取消息时,Master 服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读 I/O),以及从服务器是否可读等因素建议下一次是从 Master 还是 Slave 拉取 +* 消息(Message):消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ 中每个消息拥有唯一的 Message ID,且可以携带具有业务标识的 Key,系统提供了通过 Message ID 和 Key 查询消息的功能 -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-集群架构.png) +* 标签(Tag):为消息设置的标志,用于同一主题下区分不同类型的消息。标签能够有效地保持代码的清晰度和连贯性,并优化 RocketMQ 提供的查询系统,消费者可以根据 Tag 实现对不同子主题的不同消费逻辑,实现更好的扩展性 -集群工作流程: +* 普通顺序消息(Normal Ordered Message):消费者通过同一个消息队列(Topic 分区)收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的 -- 启动 NameServer 监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心 -- Broker 启动,跟所有的 NameServer 保持长连接,定时发送心跳包。心跳包中包含当前 Broker 信息(IP、端口等)以及存储所有 Topic 信息。注册成功后,NameServer 集群中就有 Topic 跟 Broker 的映射关系 -- 收发消息前,先创建 Topic,创建 Topic 时需要指定该 Topic 要存储在哪些 Broker 上,也可以在发送消息时自动创建 Topic -- Producer 发送消息,启动时先跟 NameServer 集群中的其中一台建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,轮询从队列列表中选择一个队列,然后与队列所在的 Broker 建立长连接从而向 Broker 发消息。 -- Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接,获取当前订阅 Topic 存在哪些 Broker 上,然后直接跟 Broker 建立连接通道,开始消费消息 +* 严格顺序消息(Strictly Ordered Message):消费者收到的所有消息均是有顺序的 -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/architecture.md +官方文档:https://github.com/apache/rocketmq/tree/master/docs/cn(基础知识部分的笔记参考官方文档编写) @@ -3707,11 +3627,13 @@ RocketMQ 网络部署特点: -## 基本操作 +## 消息操作 ### 基本样例 -#### 工作流程 +#### 订阅发布 + +消息的发布是指某个生产者向某个 Topic 发送消息,消息的订阅是指某个消费者关注了某个 Topic 中带有某些 Tag 的消息,进而从该 Topic 消费数据 导入 MQ 客户端依赖 @@ -4420,11 +4342,11 @@ RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交 RocketMQ 会开启一个定时任务,从 Topic 为 RMQ_SYS_TRANS_HALF_TOPIC 中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息 -在 RocketMQ 中,每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个二级索引来读取消息实体内容,如图: +在 RocketMQ 中,每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue(在 Broker 端)这个类似二级索引的结构来读取消息实体内容 ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-事务工作流程.png) -RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 Topic 和 Queue 等属性进行替换,同时将原来的 Topic 和 Queue 信息存储到消息的属性中,因为消息主题被替换,消息并不会转发到该原主题的消息消费队列,消费者无法感知消息的存在,不会消费 +RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 Topic 和 Queue 等属性进行替换,同时将原来的 Topic 和 Queue 信息存储到**消息的属性**中,因为消息的主题被替换,所以消息不会转发到该原主题的消息消费队列,消费者无法感知消息的存在,不会消费 @@ -4436,13 +4358,13 @@ RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 T 一阶段写入不可见的消息后,二阶段操作: -* 如果执行 Commit 操作,则需要让消息对用户可见,构建出 Half 消息的索引。一阶段的 Half 消息写到一个特殊的 Topic,所以构建索引时需要读取出 Half 消息,并将 Topic 和 Queue 替换成真正的目标的 Topic 和 Queue,然后通过一次普通消息的写入操作来生成一条对用户可见的消息 +* 如果执行 Commit 操作,则需要让消息对用户可见,构建出 Half 消息的索引。一阶段的 Half 消息写到一个特殊的 Topic,构建索引时需要读取出 Half 消息,然后通过一次普通消息的写入操作将 Topic 和 Queue 替换成真正的目标 Topic 和 Queue,生成一条对用户可见的消息。其实就是利用了一阶段存储的消息的内容,在二阶段时恢复出一条完整的普通消息,然后走一遍消息写入流程 -* 如果是 Rollback 则需要撤销一阶段的消息,因为消息本就不可见,所以并不需要真正撤销消息(实际上 RocketMQ 也无法去删除一条消息,因为是顺序写文件的),为了区分这条消息没有确定的状态(Pending 状态),RocketMQ 用 Op 消息标识事务消息已经确定的状态(Commit 或者 Rollback) +* 如果是 Rollback 则需要撤销一阶段的消息,因为消息本就不可见,所以并不需要真正撤销消息(实际上 RocketMQ 也无法去删除一条消息,因为是顺序写文件的)。RocketMQ 为了区分这条消息没有确定状态的消息(Pending 状态),采用 Op 消息标识已经确定状态的事务消息(Commit 或者 Rollback) 事务消息无论是 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) @@ -4454,9 +4376,9 @@ RocketMQ 将 Op 消息写入到全局一个特定的 Topic 中,通过源码中 ##### 补偿机制 -如果在 RocketMQ 事务消息的二阶段过程中失败了,例如在做 Commit 操作时,出现网络问题导致 Commit 失败,那么需要通过一定的策略使这条消息最终被 Commit,RocketMQ采用了一种补偿机制,称为回查 +如果在 RocketMQ 事务消息的二阶段过程中失败了,例如在做 Commit 操作时,出现网络问题导致 Commit 失败,那么需要通过一定的策略使这条消息最终被 Commit,RocketMQ 采用了一种补偿机制,称为回查 -Broker 端通过对比 Half 消息和 Op 消息,对未确定状态的消息发起回查并且推进CheckPoint(记录那些事务消息的状态是确定的),将消息发送到对应的 Producer 端(同一个 Group 的 Producer),由 Producer 根据消息来检查本地事务的状态,然后执行提交或回滚 +Broker 端通过对比 Half 消息和 Op 消息,对未确定状态的消息发起回查并且推进 CheckPoint(记录哪些事务消息的状态是确定的),将消息发送到对应的 Producer 端(同一个 Group 的 Producer),由 Producer 根据消息来检查本地事务的状态,然后执行提交或回滚 注意:RocketMQ 并不会无休止的进行事务状态回查,默认回查 15 次,如果 15 次回查还是无法得知事务状态,则默认回滚该消息 @@ -4495,40 +4417,6 @@ Broker 端通过对比 Half 消息和 Op 消息,对未确定状态的消息发 ##### 代码实现 -使用 **TransactionMQProducer** 类创建事务性生产者,并指定唯一的 `ProducerGroup`,就可以设置自定义线程池来处理这些检查请求,执行本地事务后、需要根据执行结果对消息队列进行回复 - -```java -public class Producer { - public static void main(String[] args) throws MQClientException, InterruptedException { - // 创建事务监听器 - TransactionListener transactionListener = new TransactionListenerImpl(); - // 创建消息生产者 - TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); - ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS); - producer.setExecutorService(executorService); - // 生产者的监听器 - producer.setTransactionListener(transactionListener); - producer.start(); - String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"}; - for (int i = 0; i < 10; i++) { - try { - Message msg = new Message("TransactionTopic", tags[i % tags.length], "KEY" + i, - ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); - SendResult sendResult = producer.sendMessageInTransaction(msg, null); - System.out.printf("%s%n", sendResult); - Thread.sleep(10); - } catch (MQClientException | UnsupportedEncodingException e) { - e.printStackTrace(); - } - } - //Thread.sleep(1000000); - //producer.shutdown();暂时不关闭 - } -} -``` - -消费者代码和前面的实例相同的 - 实现事务的监听接口,当发送半消息成功时: * `executeLocalTransaction` 方法来执行本地事务,返回三个事务状态之一 @@ -4567,6 +4455,43 @@ public class TransactionListenerImpl implements TransactionListener { } ``` +使用 **TransactionMQProducer** 类创建事务性生产者,并指定唯一的 `ProducerGroup`,就可以设置自定义线程池来处理这些检查请求,执行本地事务后、需要根据执行结果对消息队列进行回复 + +```java +public class Producer { + public static void main(String[] args) throws MQClientException, InterruptedException { + // 创建消息生产者 + TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); + ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS); + producer.setExecutorService(executorService); + + // 创建事务监听器 + TransactionListener transactionListener = new TransactionListenerImpl(); + // 生产者的监听器 + producer.setTransactionListener(transactionListener); + // 启动生产者 + producer.start(); + String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"}; + for (int i = 0; i < 10; i++) { + try { + Message msg = new Message("TransactionTopic", tags[i % tags.length], "KEY" + i, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + // 发送消息 + SendResult sendResult = producer.sendMessageInTransaction(msg, null); + System.out.printf("%s%n", sendResult); + Thread.sleep(10); + } catch (MQClientException | UnsupportedEncodingException e) { + e.printStackTrace(); + } + } + //Thread.sleep(1000000); + //producer.shutdown();暂时不关闭 + } +} +``` + +消费者代码和前面的实例相同的 + @@ -4577,7 +4502,64 @@ public class TransactionListenerImpl implements TransactionListener { -## 高级特性 +## 系统机制 + +### 集群设计 + +#### 集群模式 + +常用的以下几种模式: + +* 单 Master 模式:这种方式风险较大,一旦 Broker 重启或者宕机,会导致整个服务不可用 +* 多 Master 模式:一个集群无 Slave,全是 Master + + - 优点:配置简单,单个 Master 宕机或重启维护对应用无影响,在磁盘配置为 RAID10 时,即使机器宕机不可恢复情况下,由于 RAID10 磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高 + + - 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响 +* 多 Master 多 Slave 模式(同步):每个 Master 配置一个 Slave,有多对 Master-Slave,HA 采用同步双写方式,即只有主备都写成功,才向应用返回成功 + + * 优点:数据与服务都无单点故障,Master 宕机情况下,消息无延迟,服务可用性与数据可用性都非常高 + * 缺点:性能比异步复制略低(大约低 10% 左右),发送单个消息的 RT 略高,目前不能实现主节点宕机,备机自动切换为主机 +* 多 Master 多 Slave 模式(异步):HA 采用异步复制的方式,会造成主备有短暂的消息延迟(毫秒级别) + + - 优点:即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,同时 Master 宕机后,消费者仍然可以从 Slave 消费,而且此过程对应用透明,不需要人工干预,性能同多 Master 模式几乎一样 + - 缺点:Master 宕机,磁盘损坏情况下会丢失少量消息 + + + +*** + + + +#### 集群架构 + +RocketMQ 网络部署特点: + +- 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 的从服务器才会参与消息的读负载(读写分离) + +- Producer 与 NameServer 集群中的其中**一个节点(随机选择)建立长连接**,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master 建立长连接,且定时向 Master **发送心跳**。Producer 完全无状态,可集群部署 + +- Consumer 与 NameServer 集群中的其中一个节点(随机选择)建立长连接,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master、Slave 建立长连接,且定时向 Master、Slave 发送心跳 + + Consumer 既可以从 Master 订阅消息,也可以从 Slave 订阅消息,在向 Master 拉取消息时,Master 服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读 I/O),以及从服务器是否可读等因素建议下一次是从 Master 还是 Slave 拉取 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-集群架构.png) + +集群工作流程: + +- 启动 NameServer 监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心 +- Broker 启动,跟所有的 NameServer 保持长连接,定时发送心跳包。心跳包中包含当前 Broker 信息(IP、端口等)以及存储所有 Topic 信息。注册成功后,NameServer 集群中就有 Topic 跟 Broker 的映射关系 +- 收发消息前,先创建 Topic,创建 Topic 时需要指定该 Topic 要存储在哪些 Broker 上,也可以在发送消息时自动创建 Topic +- Producer 发送消息,启动时先跟 NameServer 集群中的其中一台建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,轮询从队列列表中选择一个队列,然后与队列所在的 Broker 建立长连接从而向 Broker 发消息。 +- Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接,获取当前订阅 Topic 存在哪些 Broker 上,然后直接跟 Broker 建立连接通道,开始消费消息 + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/architecture.md diff --git a/Java.md b/Java.md index ae24482..389fc78 100644 --- a/Java.md +++ b/Java.md @@ -9677,7 +9677,7 @@ JVM 结构: -JVM、JRE、JDK对比: +JVM、JRE、JDK 对比: @@ -12568,7 +12568,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/Prog.md b/Prog.md index 6e602cb..e0ff4ea 100644 --- a/Prog.md +++ b/Prog.md @@ -14691,7 +14691,7 @@ MappedByteBuffer,可以让文件在直接内存(堆外内存)中进行修 * **用在进程间的通信,能达到共享内存页的作用**,但在高并发下要对文件内存进行加锁,防止出现读写内容混乱和不一致性,Java 提供了文件锁 FileLock,但在父/子进程中锁定后另一进程会一直等待,效率不高 * 读写那些太大而不能放进内存中的文件,分段映射 -MappedByteBuffer 较之 ByteBuffer新增的三个方法 +MappedByteBuffer 较之 ByteBuffer 新增的三个方法: - `final MappedByteBuffer force()`:缓冲区是 READ_WRITE 模式下,对缓冲区内容的修改强行写入文件 - `final MappedByteBuffer load()`:将缓冲区的内容载入物理内存,并返回该缓冲区的引用 @@ -14752,13 +14752,13 @@ public class MappedByteBufferTest { * 通道可以实现异步读写数据 * 通道可以从缓冲读数据,也可以写数据到缓冲 -2. BIO 中的 stream 是单向的,NIO中的 Channel 是双向的,可以读操作,也可以写操作 +2. BIO 中的 Stream 是单向的,NIO中的 Channel 是双向的,可以读操作,也可以写操作 3. Channel 在 NIO 中是一个接口:`public interface Channel extends Closeable{}` Channel 实现类: -* FileChannel:用于读取、写入、映射和操作文件的通道,只能工作在阻塞模式下 +* FileChannel:用于读取、写入、映射和操作文件的通道,**只能工作在阻塞模式下** * 通过 FileInputStream 获取的 Channel 只能读 * 通过 FileOutputStream 获取的 Channel 只能写 * 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定 From 85b0df459548f0e06e0ea00c0c1c361dcd04156d Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 12 Nov 2021 01:11:46 +0800 Subject: [PATCH 027/122] Update Java Notes --- DB.md | 3 +- Frame.md | 206 +++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 163 insertions(+), 46 deletions(-) diff --git a/DB.md b/DB.md index 579e96a..60ca2e1 100644 --- a/DB.md +++ b/DB.md @@ -576,8 +576,7 @@ SELECT * FROM t WHERE id = 1; 优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。 * 根据搜索条件找出所有可能的使用的索引 -* 计算全表扫描的代价 -* 计算使用不同索引执行 SQL 的的代价 +* 成本分析,执行成本由 I/O 成本和 CPU 成本组成,计算全表扫描和使用不同索引执行 SQL 的代价 * 找到一个最优的执行方案,用最小的代价去执行语句 在数据库里面,扫描行数是影响执行代价的因素之一,扫描的行数越少意味着访问磁盘的次数越少,消耗的 CPU 资源越少,优化器还会结合是否使用临时表、是否排序等因素进行综合判断 diff --git a/Frame.md b/Frame.md index fb9ae14..7152a16 100644 --- a/Frame.md +++ b/Frame.md @@ -3549,54 +3549,17 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 -### 工作流程 - -NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态注册与发现,生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表 - -NameServer 主要包括两个功能: - -* Broker 管理,NameServer 接受 Broker 集群的注册信息并保存下来作为路由信息的基本数据,提供**心跳检测**检查 Broker 活性 -* 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 - -NameServer 特点: - -* NameServer 通常是集群的方式部署,各实例间相互不进行信息通讯 -* Broker 向每一台 NameServer 注册自己的路由信息,所以每个 NameServer 实例上面**都保存一份完整的路由信息** -* 当某个 NameServer 因某种原因下线了,Broker 仍可以向其它 NameServer 同步其路由信息 - -BrokerServer 主要负责消息的存储、投递和查询以及服务高可用保证,在 RocketMQ 系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 - -Broker 包含了以下几个重要子模块: - -* Remoting Module:整个 Broker 的实体,负责处理来自 clients 端的请求 - -* Client Manager:负责管理客户端(Producer/Consumer)和维护 Consumer 的 Topic 订阅信息 - -* Store Service:提供方便简单的 API 接口处理消息存储到物理硬盘和查询功能 - -* HA Service:高可用服务,提供 Master Broker 和 Slave Broker 之间的数据同步功能 - -* Index Service:根据特定的 Message key 对投递到 Broker 的消息进行索引服务,以提供消息的快速查询 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Broker工作流程.png) - - - -*** - - - ### 相关概念 RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息,NameServer 负责管理 Broker +* 代理服务器(Broker Server):消息中转角色,负责**存储消息、转发消息**。在 RocketMQ 系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 +* 名字服务(Name Server):充当**路由消息**的提供者。生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表 * 消息生产者(Producer):负责**生产消息**,把业务应用系统里产生的消息发送到 Broker 服务器。RocketMQ 提供多种发送方式,同步发送、异步发送、顺序发送、单向发送,同步和异步方式均需要 Broker 返回确认信息,单向发送不需要;可以通过 MQ 的负载均衡模块选择相应的 Broker 集群队列进行消息投递,投递的过程支持快速失败并且低延迟 * 消息消费者(Consumer):负责**消费消息**,一般是后台系统负责异步消费,一个消息消费者会从 Broker 服务器拉取消息、并将其提供给应用程序。从用户应用的角度而提供了两种消费形式: * 拉取式消费(Pull Consumer):应用通主动调用 Consumer 的拉消息方法从 Broker 服务器拉消息,主动权由应用控制,一旦获取了批量消息,应用就会启动消费过程 * 推动式消费(Push Consumer):该模式下 Broker 收到数据后会主动推送给消费端,实时性较高 - * 生产者组(Producer Group):同一类 Producer 的集合,都发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,**则 Broker 服务器会联系同一生产者组的其他生产者实例以提交或回溯消费** - * 消费者组(Consumer Group):同一类 Consumer 的集合,消费者实例必须订阅完全相同的 Topic,消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面更容易的实现负载均衡和容错。RocketMQ 支持两种消息模式: * 集群消费(Clustering):相同 Consumer Group 的每个 Consumer 实例平均分摊消息 * 广播消费(Broadcasting):相同 Consumer Group 的每个 Consumer 实例都接收全量的消息 @@ -4225,7 +4188,9 @@ consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC"); #### 原理解析 -RocketMQ 分布式消息队列的消息过滤方式是在 Consumer 端订阅消息时再做消息过滤的。因为 RocketMQ 在 Producer 端写入消息和在 Consumer 端订阅消息采用**分离存储**的机制实现,Consumer 端订阅消息是需要通过 ConsumeQueue 这个消息消费的逻辑队列拿到一个索引,然后再从 CommitLog 里面读取真正的消息实体内容,所以绕不开其存储结构。 +RocketMQ 分布式消息队列的消息过滤方式是在 Consumer 端订阅消息时再做消息过滤的,所以是在 Broker 端实现的,优点是减少了对于 Consumer 无用消息的网络传输,缺点是增加了 Broker 的负担、而且实现相对复杂 + +RocketMQ 在 Producer 端写入消息和在 Consumer 端订阅消息采用**分离存储**的机制实现,Consumer 端订阅消息是需要通过 ConsumeQueue 这个消息消费的逻辑队列拿到一个索引,然后再从 CommitLog 里面读取真正的消息实体内容 ConsumeQueue 的存储结构如下,有 8 个字节存储的 Message Tag 的哈希值,基于 Tag 的消息过滤就是基于这个字段 @@ -4342,10 +4307,6 @@ RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交 RocketMQ 会开启一个定时任务,从 Topic 为 RMQ_SYS_TRANS_HALF_TOPIC 中拉取消息进行消费,根据生产者组获取一个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息 -在 RocketMQ 中,每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue(在 Broker 端)这个类似二级索引的结构来读取消息实体内容 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-事务工作流程.png) - RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 Topic 和 Queue 等属性进行替换,同时将原来的 Topic 和 Queue 信息存储到**消息的属性**中,因为消息的主题被替换,所以消息不会转发到该原主题的消息消费队列,消费者无法感知消息的存在,不会消费 @@ -4504,6 +4465,163 @@ public class Producer { ## 系统机制 +### 消息存储 + +#### 工作流程 + +分布式队列因为有高可靠性的要求,所以数据要进行持久化存储 + +1. 消息生产者发送消息 +2. MQ 收到消息,将消息进行持久化,在存储中新增一条记录 +3. 返回 ACK 给生产者 +4. MQ push 消息给对应的消费者,然后等待消费者返回 ACK +5. 如果消息消费者在指定时间内成功返回 ACK,那么 MQ 认为消息消费成功,在存储中删除消息;如果 MQ 在指定时间内没有收到 ACK,则认为消息消费失败,会尝试重新 push 消息,重复执行 4、5、6 步骤 +6. MQ删除消息 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存取.png) + + + + + +*** + + + +#### 存储结构 + +RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,消息真正的物理存储文件是 CommitLog,ConsumeQueue 是消息的逻辑队列,类似数据库的索引节点,存储的是指向物理存储的地址。每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件 + +每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue(在 Broker 端)这个结构来读取消息实体内容 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) + +* 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 索引 + +RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 + +服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 + + + + + +**** + + + +#### 存储优化 + +##### 存储媒介 + +两种持久化的方案: + +* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 + +* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 + + 注意:磁盘的顺序读写要比随机读写快很多,可以匹配上网络的速度,RocketMQ 的消息采用的顺序写 + +页缓存(PageCache)是 OS 对文件的缓存,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,对读写访问操作进行了性能优化, + +* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 +* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理) + +在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 + + + +*** + + + +##### 内存映射 + +操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: + +* read:读取本地文件内容 + +* write:将读取的内容通过网络发送出去 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) + +补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 + +通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用零拷贝技术,提高消息存盘和网络发送的速度。 + +RocketMQ 主要通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 + +MappedByteBuffer 内存映射的方式限制一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 + + + +*** + + + +#### 刷盘机制 + +同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 + +异步刷盘:利用 OS 的 PageCache 的优势,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 + +通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + + + + + +**** + + + +### 通信机制 + +NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态注册与发现,生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表 + +NameServer 主要包括两个功能: + +* Broker 管理,NameServer 接受 Broker 集群的注册信息并保存下来作为路由信息的基本数据,提供**心跳检测**检查 Broker 活性 +* 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 + +NameServer 特点: + +* NameServer 通常是集群的方式部署,各实例间相互不进行信息通讯 +* Broker 向每一台 NameServer 注册自己的路由信息,所以每个 NameServer 实例上面**都保存一份完整的路由信息** +* 当某个 NameServer 因某种原因下线了,Broker 仍可以向其它 NameServer 同步其路由信息 + +BrokerServer 主要负责消息的存储、投递和查询以及服务高可用保证,在 RocketMQ 系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 + +Broker 包含了以下几个重要子模块: + +* Remoting Module:整个 Broker 的实体,负责处理来自 clients 端的请求 + +* Client Manager:负责管理客户端(Producer/Consumer)和维护 Consumer 的 Topic 订阅信息 + +* Store Service:提供方便简单的 API 接口处理消息存储到物理硬盘和查询功能 + +* HA Service:高可用服务,提供 Master Broker 和 Slave Broker 之间的数据同步功能 + +* Index Service:根据特定的 Message key 对投递到 Broker 的消息进行索引服务,以提供消息的快速查询 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Broker工作流程.png) + + + + + +*** + + + ### 集群设计 #### 集群模式 From e59dcdb9bc3cb19901695dabc3df2dd1628d99ab Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 13 Nov 2021 00:03:17 +0800 Subject: [PATCH 028/122] Update Java Notes --- DB.md | 34 ++++-- Frame.md | 327 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- Java.md | 9 +- 3 files changed, 352 insertions(+), 18 deletions(-) diff --git a/DB.md b/DB.md index 60ca2e1..cd7f505 100644 --- a/DB.md +++ b/DB.md @@ -571,7 +571,7 @@ SELECT * FROM t WHERE id = 1; #### 优化器 -##### 扫描行数 +##### 成本分析 优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。 @@ -581,16 +581,34 @@ SELECT * FROM t WHERE id = 1; 在数据库里面,扫描行数是影响执行代价的因素之一,扫描的行数越少意味着访问磁盘的次数越少,消耗的 CPU 资源越少,优化器还会结合是否使用临时表、是否排序等因素进行综合判断 + + +*** + + + +##### 统计数据 + +MySQL 中保存着两种统计数据: + +* innodb_table_stats 存储了表的统计数据,每一条记录对应着一个表的统计数据 +* innodb_index_stats 存储了索引的统计数据,每一条记录对应着一个索引的一个统计项的数据 + MySQL 在真正执行语句之前,并不能精确地知道满足条件的记录有多少条,只能根据统计信息来估算记录,统计信息就是索引的区分度,一个索引上不同的值的个数(比如性别只能是男女,就是 2 ),称之为基数(cardinality),基数越大说明区分度越好 -* 通过**采样统计**来获取基数,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数 -* 数据表是会持续更新的,索引统计信息也不会固定不变,当变更的数据行数超过 1/ M 的时候,会自动触发重新做一次索引统计 +通过**采样统计**来获取基数,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数 + +在 MySQL 中,有两种存储统计数据的方式,可以通过设置参数 `innodb_stats_persistent` 的值来选择: + +* ON:表示统计信息会持久化存储(默认),采样页数 N 默认为 20,可以通过 `innodb_stats_persistent_sample_pages `指定,页数越多统计的数据越准确,但消耗的资源更大 +* 设置为 off 时,表示统计信息只存储在内存,采样页数 N 默认为 8,也可以通过系统变量设置(不推荐,每次重新计算浪费资源) + +数据表是会持续更新的,两种更新方式: -* 在 MySQL 中,有两种存储索引统计的方式,可以通过设置参数 innodb_stats_persistent 的值来选择: - * 设置为 on 时,表示统计信息会持久化存储,这时默认的 N 是 20,M 是 10 - * 设置为 off 时,表示统计信息只存储在内存,这时默认的 N 是 8,M 是 16 +* 设置 `innodb_stats_auto_recalc` 为 1,当发生变动的记录数量超过表大小的 10% 时,自动触发重新计算,不过是**异步进行** +* 调用 `ANALYZE TABLE t` 手动更新统计信息,只对信息做重新统计(不是重建表),没有修改数据,这个过程中加了 MDL 读锁并且是同步进行,所以会暂时阻塞系统 -EXPLAIN 执行计划在优化器阶段生成,如果发现 explain 的结果预估的 rows 值跟实际情况差距比较大,可以执行 `analyze table t ` 重新修正信息,只是对表的索引信息做重新统计(不是重建表),没有修改数据,这个过程中加了 MDL 读锁 +EXPLAIN 执行计划在优化器阶段生成,如果 explain 的结果预估的 rows 值跟实际情况差距比较大,可以执行 analyze 命令重新修正信息 @@ -600,7 +618,7 @@ EXPLAIN 执行计划在优化器阶段生成,如果发现 explain 的结果预 ##### 错选索引 -扫描行数本身是估算数据,或者 SQL 语句中的字段选择有问题时,可能导致 MySQL 没有选择正确的执行索引 +采样统计本身是估算数据,或者 SQL 语句中的字段选择有问题时,可能导致 MySQL 没有选择正确的执行索引 解决方法: diff --git a/Frame.md b/Frame.md index 7152a16..d1f54c3 100644 --- a/Frame.md +++ b/Frame.md @@ -3975,7 +3975,9 @@ public class ConsumerInOrder { ### 延时消息 -提交了一个订单就可以发送一个延时消息,1h 后去检查这个订单的状态,如果还是未付款就取消订单释放库存 +#### 原理解析 + +定时消息(延迟队列)是指消息发送到 Broker 后,不会立即被消费,等待特定时间投递给真正的 Topic RocketMQ 并不支持任意时间的延时,需要设置几个固定的延时等级,从 1s 到 2h 分别对应着等级 1 到 18,消息消费失败会进入延时消息队列,消息发送时间与设置的延时等级和重试次数有关,详见代码 `SendMessageProcessor.java` @@ -3983,6 +3985,28 @@ RocketMQ 并不支持任意时间的延时,需要设置几个固定的延时 private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"; ``` +Broker 可以配置 messageDelayLevel,该属性是 Broker 的属性,不属于某个 Topic + +发消息时,可以设置延迟等级 `msg.setDelayLevel(level)`,level 有以下三种情况: + +- level == 0:消息为非延迟消息 +- 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 + +注意:定时消息在第一次写入和调度写入真实 Topic 时都会计数,因此发送数量、tps 都会变高。 + + + +*** + + + +#### 代码实现 + +提交了一个订单就可以发送一个延时消息,1h 后去检查这个订单的状态,如果还是未付款就取消订单释放库存 + ```java public class ScheduledMessageProducer { public static void main(String[] args) throws Exception { @@ -4467,7 +4491,11 @@ public class Producer { ### 消息存储 -#### 工作流程 +#### 生产消费 + +At least Once:至少一次,指每个消息必须投递一次,Consumer 先 Pull 消息到本地,消费完成后才向服务器返回 ACK,如果没有消费一定不会 ACK 消息 + +回溯消费:指 Consumer 已经消费成功的消息,由于业务上需求需要重新消费,Broker 在向 Consumer 投递成功消息后,消息仍然需要保留。并且重新消费一般是按照时间维度,例如由于 Consumer 系统故障,恢复后需要重新消费 1 小时前的数据,RocketMQ 支持按照时间回溯消费,时间维度精确到毫秒 分布式队列因为有高可靠性的要求,所以数据要进行持久化存储 @@ -4476,7 +4504,7 @@ public class Producer { 3. 返回 ACK 给生产者 4. MQ push 消息给对应的消费者,然后等待消费者返回 ACK 5. 如果消息消费者在指定时间内成功返回 ACK,那么 MQ 认为消息消费成功,在存储中删除消息;如果 MQ 在指定时间内没有收到 ACK,则认为消息消费失败,会尝试重新 push 消息,重复执行 4、5、6 步骤 -6. MQ删除消息 +6. MQ 删除消息 ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存取.png) @@ -4506,8 +4534,6 @@ RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所 - - **** @@ -4681,7 +4707,21 @@ RocketMQ 网络部署特点: +**** + + + +#### 高可用 + +在 Consumer 的配置文件中,并不需要设置是从 Master 读还是从 Slave 读,当 Master 不可用或者繁忙的时候,Consumer 会被自动切换到从 Slave 读。有了自动切换的机制,当一个 Master 机器出现故障后,Consumer 仍然可以从 Slave 读取消息,不影响 Consumer 程序,达到了消费端的高可用性 + +在创建 Topic 的时候,把 Topic 的多个 Message Queue 创建在多个 Broker 组上(相同 Broker 名称,不同 brokerId 的机器组成一个 Broker 组),当一个 Broker 组的 Master 不可用后,其他组的 Master 仍然可用,Producer 仍然可以发送消息。 +RocketMQ 目前还不支持把 Slave 自动转成 Master,需要手动停止 Slave 角色的 Broker,更改配置文件,用新的配置文件启动 Broker + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-高可用.png) + +5)、6)属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。RocketMQ在这两种情况下,通过异步复制,可保证99%的消息不丢,但是仍然会有极少量的消息可能丢失。通过同步双写技术可以完全避免单点,同步双写势必会影响性能,适合对消息可靠性要求极高的场合,例如与Money相关的应用。注:RocketMQ从3.0版本开始支持同步双写。 @@ -4689,6 +4729,283 @@ RocketMQ 网络部署特点: +#### 主从复制 + +如果一个 Broker 组有 Master 和 Slave,消息需要从 Master 复制到 Slave 上,有同步和异步两种复制方式: + +* 同步复制方式:Master 和 Slave 均写成功后才反馈给客户端写成功状态。在同步复制方式下,如果 Master 出故障, Slave 上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量 + +* 异步复制方式:只要 Master 写成功,即可反馈给客户端写成功状态,系统拥有较低的延迟和较高的吞吐量,但是如果 Master 出了故障,有些数据因为没有被写入 Slave,有可能会丢失 + +同步复制和异步复制是通过 Broker 配置文件里的 brokerRole 参数进行设置的,可以设置成 ASYNC_MASTE、RSYNC_MASTER、SLAVE 三个值中的一个 + +一般把刷盘机制配置成 ASYNC_FLUSH,主从复制为 SYNC_MASTER,这样即使有一台机器出故障,仍然能保证数据不丢 + +RocketMQ 支持消息的高可靠,影响消息可靠性的几种情况: + +1. Broker 非正常关闭 +2. Broker 异常 Crash +3. OS Crash +4. 机器掉电,但是能立即恢复供电情况 +5. 机器无法开机(可能是 CPU、主板、内存等关键设备损坏) +6. 磁盘设备损坏 + +前四种情况都属于硬件资源可立即恢复情况,RocketMQ 在这四种情况下能保证消息不丢,或者丢失少量数据(依赖刷盘方式) + +后两种属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。RocketMQ 在这两种情况下,通过主从异步复制,可保证 99% 的消息不丢,但是仍然会有极少量的消息可能丢失。通过**同步双写技术**可以完全避免单点,但是会影响性能,适合对消息可靠性要求极高的场合,RocketMQ 从 3.0 版本开始支持同步双写 + + + +**** + + + +### 负载均衡 + +#### 生产端 + +Producer 端,每个实例在发消息的时候,默认会轮询所有的 Message Queue 发送,以让消息平均落在不同的 queue 上。而由于 queue可以散落在不同的 Broker,所以消息就发送到不同的 Broker 下 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-producer负载均衡.png) + +图中箭头线条上的标号代表顺序,发布方会把第一条消息发送至 Queue 0,然后第二条消息发送至 Queue 1,以此类推 + + + +*** + + + +#### 消费端 + +广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以不存在负载均衡,在实现上,Consumer 分配 queue 时,所有 Consumer 都分到所有的queue。 + +在集群消费模式下,每条消息只需要投递到订阅这个 Topic 的 Consumer Group 下的一个实例即可,RocketMQ 采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条 Message Queue + +而每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照 queue 的数量和实例的数量平均分配 queue 给每个实例。默认的分配算法是 AllocateMessageQueueAveragely: + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-consumer负载均衡1.png) + + 还有一种平均的算法是 AllocateMessageQueueAveragelyByCircle,以环状轮流均分 queue 的形式: + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-consumer负载均衡2.png) + +集群模式下,queue 都是只允许分配只一个实例,如果多个实例同时消费一个 queue 的消息,由于拉取哪些消息是 Consumer 主动控制的,会导致同一个消息在不同的实例下被消费多次 + +通过增加 Consumer 实例去分摊 queue 的消费,可以起到水平扩展的消费能力的作用。而当有实例下线时,会重新触发负载均衡,这时候原来分配到的 queue 将分配到其他实例上继续消费 + +但是如果 Consumer 实例的数量比 Message Queue 的总数量还多的话,多出来的 Consumer 实例将无法分到 queue,也就无法消费到消息,也就无法起到分摊负载的作用了,所以需要控制让 queue 的总数量大于等于 Consumer 的数量 + + + +**** + + + +### 消息重试 + +todo:以下还需要修改,明日完成 + +顺序消息的重试 + +对于顺序消息,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时,应用会出现消息消费被阻塞的情况。因此,在使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。 + +1.4.2 无序消息的重试 + +对于无序消息(普通、定时、延时、事务消息),当消费者消费消息失败时,您可以通过设置返回状态达到消息重试的结果。 + +无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。 + +1)重试次数 + +消息队列 RocketMQ 默认允许每条消息最多重试 16 次,每次重试的间隔时间如下: + +| 第几次重试 | 与上次重试的间隔时间 | 第几次重试 | 与上次重试的间隔时间 | +| :--------: | :------------------: | :--------: | :------------------: | +| 1 | 10 秒 | 9 | 7 分钟 | +| 2 | 30 秒 | 10 | 8 分钟 | +| 3 | 1 分钟 | 11 | 9 分钟 | +| 4 | 2 分钟 | 12 | 10 分钟 | +| 5 | 3 分钟 | 13 | 20 分钟 | +| 6 | 4 分钟 | 14 | 30 分钟 | +| 7 | 5 分钟 | 15 | 1 小时 | +| 8 | 6 分钟 | 16 | 2 小时 | + +如果消息重试 16 次后仍然失败,消息将不再投递。如果严格按照上述重试时间间隔计算,某条消息在一直消费失败的前提下,将会在接下来的 4 小时 46 分钟之内进行 16 次重试,超过这个时间范围消息将不再重试投递。 + +**注意:** 一条消息无论重试多少次,这些重试消息的 Message ID 不会改变。 + +2)配置方式 + +**消费失败后,重试配置方式** + +集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确进行配置(三种方式任选一种): + +- 返回 Action.ReconsumeLater (推荐) +- 返回 Null +- 抛出异常 + +```java +public class MessageListenerImpl implements MessageListener { + @Override + public Action consume(Message message, ConsumeContext context) { + //处理消息 + doConsumeMessage(message); + //方式1:返回 Action.ReconsumeLater,消息将重试 + return Action.ReconsumeLater; + //方式2:返回 null,消息将重试 + return null; + //方式3:直接抛出异常, 消息将重试 + throw new RuntimeException("Consumer Message exceotion"); + } +} +``` + +**消费失败后,不重试配置方式** + +集群消费方式下,消息失败后期望消息不重试,需要捕获消费逻辑中可能抛出的异常,最终返回 Action.CommitMessage,此后这条消息将不会再重试。 + +```java +public class MessageListenerImpl implements MessageListener { + @Override + public Action consume(Message message, ConsumeContext context) { + try { + doConsumeMessage(message); + } catch (Throwable e) { + //捕获消费逻辑中的所有异常,并返回 Action.CommitMessage; + return Action.CommitMessage; + } + //消息处理正常,直接返回 Action.CommitMessage; + return Action.CommitMessage; + } +} +``` + +**自定义消息最大重试次数** + +消息队列 RocketMQ 允许 Consumer 启动的时候设置最大重试次数,重试时间间隔将按照如下策略: + +- 最大重试次数小于等于 16 次,则重试时间间隔同上表描述。 +- 最大重试次数大于 16 次,超过 16 次的重试时间间隔均为每次 2 小时。 + +```java +Properties properties = new Properties(); +//配置对应 Group ID 的最大消息重试次数为 20 次 +properties.put(PropertyKeyConst.MaxReconsumeTimes,"20"); +Consumer consumer =ONSFactory.createConsumer(properties); +``` + +> 注意: + +- 消息最大重试次数的设置对相同 Group ID 下的所有 Consumer 实例有效。 +- 如果只对相同 Group ID 下两个 Consumer 实例中的其中一个设置了 MaxReconsumeTimes,那么该配置对两个 Consumer 实例均生效。 +- 配置采用覆盖的方式生效,即最后启动的 Consumer 实例会覆盖之前的启动实例的配置 + +**获取消息重试次数** + +消费者收到消息后,可按照如下方式获取消息的重试次数: + +```java +public class MessageListenerImpl implements MessageListener { + @Override + public Action consume(Message message, ConsumeContext context) { + //获取消息的重试次数 + System.out.println(message.getReconsumeTimes()); + return Action.CommitMessage; + } +} +``` + + + + + +*** + + + +### 死信队列 + +当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。 + +在消息队列 RocketMQ 中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。 + +死信消息具有以下特性 + +- 不会再被消费者正常消费。 +- 有效期与正常消息相同,均为 3 天,3 天后会被自动删除。因此,请在死信消息产生后的 3 天内及时处理。 + +死信队列具有以下特性: + +- 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。 +- 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列。 +- 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。 + +一条消息进入死信队列,意味着某些因素导致消费者无法正常消费该消息,因此,通常需要您对其进行特殊处理。排查可疑因素并解决问题后,可以在消息队列 RocketMQ 控制台重新发送该消息,让消费者重新消费一次。 + + + +**** + + + +### 幂等消费 + +消息队列 RocketMQ 消费者在接收到消息以后,有必要根据业务上的唯一 Key 对消息做幂等处理的必要性。 + +1.6.1 消费幂等的必要性 + +在互联网应用中,尤其在网络不稳定的情况下,消息队列 RocketMQ 的消息有可能会出现重复,这个重复简单可以概括为以下情况: + +- 发送时消息重复 + + 当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败。 如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。 + +- 投递时消息重复 + + 消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。 为了保证消息至少被消费一次,消息队列 RocketMQ 的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。 + +- 负载均衡时消息重复(包括但不限于网络抖动、Broker 重启以及订阅方应用重启) + + 当消息队列 RocketMQ 的 Broker 或客户端重启、扩容或缩容时,会触发 Rebalance,此时消费者可能会收到重复消息。 + +1.6.2 处理方式 + +因为 Message ID 有可能出现冲突(重复)的情况,所以真正安全的幂等处理,不建议以 Message ID 作为处理依据。 最好的方式是以业务唯一标识作为幂等处理的关键依据,而业务的唯一标识可以通过消息 Key 进行设置: + +```java +Message message = new Message(); +message.setKey("ORDERID_100"); +SendResult sendResult = producer.send(message); +``` + +订阅方收到消息时可以根据消息的 Key 进行幂等处理: + +```java +consumer.subscribe("ons_test", "*", new MessageListener() { + public Action consume(Message message, ConsumeContext context) { + String key = message.getKey() + // 根据业务唯一标识的 key 做幂等处理 + } +}); +``` + + + + + + + + + + + + + +*** + + + ## 源码分析 diff --git a/Java.md b/Java.md index 389fc78..e918610 100644 --- a/Java.md +++ b/Java.md @@ -350,15 +350,14 @@ public static void main(String[] args) { * 有了基本数据类型,为什么还要引用数据类型? - > 1、引用数据类型封装了数据和处理该数据的方法,比如 Integer.parseInt(String) 就是将 String 字符类型数据转换为 Integer 整型数据 + > 引用数据类型封装了数据和处理该数据的方法,比如 Integer.parseInt(String) 就是将 String 字符类型数据转换为 Integer 整型 > - > 2、Java 中大部分类和方法都是针对引用数据类型,包括泛型和集合 + > Java 中大部分类和方法都是针对引用数据类型,包括泛型和集合 * 引用数据类型那么好,为什么还用基本数据类型? - > 引用类型的对象要多储存对象头,对基本数据类型来说空间浪费率太高 - > 逻辑上来讲,java 只有包装类就够了,为了运行速度,需要用到基本数据类型;优先考虑运行效率的问题,所以二者同时存在是合乎情理的 - + > 引用类型的对象要多储存对象头,对基本数据类型来说空间浪费率太高。逻辑上来讲,Java 只有包装类就够了,为了运行速度,需要用到基本数据类型;优先考虑运行效率的问题,所以二者同时存在是合乎情理的 + * Java 集合不能存放基本数据类型,只存放对象的引用? > 不能放基本数据类型是因为不是 Object 的子类。泛型思想,如果不用泛型要写很多参数类型不同的但功能相同的函数(方法重载) From 95d59a122cc5ad1ae210899c7d36fa579806dfdc Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 14 Nov 2021 20:16:14 +0800 Subject: [PATCH 029/122] Update Java Notes --- Frame.md | 422 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 314 insertions(+), 108 deletions(-) diff --git a/Frame.md b/Frame.md index d1f54c3..3536a79 100644 --- a/Frame.md +++ b/Frame.md @@ -1468,7 +1468,7 @@ Netty 的功能特性: #### 设计思想 -Reactor 模式,通过一个或多个输入同时传递给服务处理器的事件驱动处理模式。 服务端程序处理传入的多路请求,并将它们同步分派给对应的处理线程,Reactor 模式也叫 Dispatcher 模式,即 I/O 多路复用统一监听事件,收到事件后分发(Dispatch 给某线程) +Reactor 模式,通过一个或多个输入同时传递给服务处理器的**事件驱动处理模式**。 服务端程序处理传入的多路请求,并将它们同步分派给对应的处理线程,Reactor 模式也叫 Dispatcher 模式,即 I/O 多路复用统一监听事件,收到事件后分发(Dispatch 给某线程) **I/O 复用结合线程池**,就是 Reactor 模式基本设计思想: @@ -1477,7 +1477,7 @@ Reactor 模式,通过一个或多个输入同时传递给服务处理器的事 Reactor 模式关键组成: - Reactor:在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 I/O 事件做出反应 -- Handlers:处理程序执行 I/O 事件要完成的实际事件,Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作 +- Handler:处理程序执行 I/O 要完成的实际事件,Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行**非阻塞操作** Reactor 模式具有如下的优点: @@ -1486,7 +1486,7 @@ Reactor 模式具有如下的优点: - 可扩展性,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源 - 可复用性,Reactor 模型本身与具体事件处理逻辑无关,具有很高的复用性 -根据Reactor的数量和处理资源池线程的数量不同,有3种典型的实现: +根据 Reactor 的数量和处理资源池线程的数量不同,有三种典型的实现: - 单 Reactor 单线程 - 单 Reactor 多线程 @@ -1506,6 +1506,8 @@ Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 * 如果不是建立连接事件,则 Reactor 会分发给连接对应的 Handler 来响应,Handler 会完成 read、业务处理、send 的完整流程 + 说明:Handler 和 Acceptor 属于同一个线程 + 模型优点:模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成 @@ -1533,7 +1535,7 @@ Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 -模型优点:可以充分利用多核CPU的处理能力 +模型优点:可以充分利用多核 CPU 的处理能力 模型缺点: @@ -1575,7 +1577,7 @@ Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 ### Proactor -Reactor 模式中,Reactor 等待某个事件的操作状态发生变化(文件描述符可读写,socket 可读写),然后把事件传递给事先注册的 Handler 来做实际的读写操作,其中的读写操作都需要应用程序同步操作,所以 Reactor 是非阻塞同步网络模型(NIO) +Reactor 模式中,Reactor 等待某个事件的操作状态发生变化(文件描述符可读写,socket 可读写),然后把事件传递给事先注册的 Handler 来做实际的读写操作,其中的读写操作都需要应用程序同步操作,所以 **Reactor 是非阻塞同步网络模型(NIO)** 把 I/O 操作改为异步,交给操作系统来完成就能进一步提升性能,这就是异步网络模型 Proactor(AIO): @@ -1587,7 +1589,7 @@ Reactor 模式中,Reactor 等待某个事件的操作状态发生变化(文 * AsyOptProcessor 处理注册请求,并处理 I/O 操作,完成I/O后通知 Proactor * Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理,最后由 Handler 完成业务处理 -对比 Reactor:Reactor 在事件发生时就通知事先注册的处理器(读写在应用程序线程中处理完成);Proactor是在事件发生时基于异步 I/O 完成读写操作(内核完成),I/O 完成后才回调应用程序的处理器进行业务处理 +对比:Reactor 在事件发生时就通知事先注册的处理器(读写在应用程序线程中处理完成);Proactor 是在事件发生时基于异步 I/O 完成读写操作(内核完成),I/O 完成后才回调应用程序的处理器进行业务处理 模式优点:异步 I/O 更加充分发挥 DMA(Direct Memory Access 直接内存存取)的优势 @@ -3559,7 +3561,7 @@ RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Produce * 消息消费者(Consumer):负责**消费消息**,一般是后台系统负责异步消费,一个消息消费者会从 Broker 服务器拉取消息、并将其提供给应用程序。从用户应用的角度而提供了两种消费形式: * 拉取式消费(Pull Consumer):应用通主动调用 Consumer 的拉消息方法从 Broker 服务器拉消息,主动权由应用控制,一旦获取了批量消息,应用就会启动消费过程 * 推动式消费(Push Consumer):该模式下 Broker 收到数据后会主动推送给消费端,实时性较高 -* 生产者组(Producer Group):同一类 Producer 的集合,都发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,**则 Broker 服务器会联系同一生产者组的其他生产者实例以提交或回溯消费** +* 生产者组(Producer Group):同一类 Producer 的集合,发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,**则 Broker 服务器会联系同一生产者组的其他生产者实例以提交或回溯消费** * 消费者组(Consumer Group):同一类 Consumer 的集合,消费者实例必须订阅完全相同的 Topic,消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面更容易的实现负载均衡和容错。RocketMQ 支持两种消息模式: * 集群消费(Clustering):相同 Consumer Group 的每个 Consumer 实例平均分摊消息 * 广播消费(Broadcasting):相同 Consumer Group 的每个 Consumer 实例都接收全量的消息 @@ -4487,7 +4489,136 @@ public class Producer { -## 系统机制 +## 系统特性 + +### 工作机制 + +#### 模块介绍 + +NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态注册与发现,生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表 + +NameServer 主要包括两个功能: + +* Broker 管理,NameServer 接受 Broker 集群的注册信息并保存下来作为路由信息的基本数据,提供**心跳检测**检查 Broker 活性 +* 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 + +NameServer 特点: + +* NameServer 通常是集群的方式部署,各实例间相互不进行信息通讯 +* Broker 向每一台 NameServer 注册自己的路由信息,所以每个 NameServer 实例上面**都保存一份完整的路由信息** +* 当某个 NameServer 因某种原因下线了,Broker 仍可以向其它 NameServer 同步其路由信息 + +BrokerServer 主要负责消息的存储、投递和查询以及服务高可用保证,在 RocketMQ 系统中接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 + +Broker 包含了以下几个重要子模块: + +* Remoting Module:整个 Broker 的实体,负责处理来自 clients 端的请求 + +* Client Manager:负责管理客户端(Producer/Consumer)和维护 Consumer 的 Topic 订阅信息 + +* Store Service:提供方便简单的 API 接口处理消息存储到物理硬盘和查询功能 + +* HA Service:高可用服务,提供 Master Broker 和 Slave Broker 之间的数据同步功能 + +* Index Service:根据特定的 Message key 对投递到 Broker 的消息进行索引服务,以提供消息的快速查询 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Broker工作流程.png) + + + +*** + + + +#### 工作流程 + +RocketMQ 的工作流程: + +- 启动 NameServer 监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心 +- 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 发消息 +- Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接获取路由信息,根据当前订阅 Topic 存在哪些 Broker 上,直接跟 Broker 建立连接通道,在完成客户端的负载均衡后,选择其中的某一个或者某几个 MessageQueue 来拉取消息并进行消费 + + + +**** + + + +#### 协议设计 + +在 Client 和 Server 之间完成一次消息发送时,需要对发送的消息进行一个协议约定,所以自定义 RocketMQ 的消息协议。为了高效地在网络中传输消息和对收到的消息读取,就需要对消息进行编解码。在 RocketMQ 中,RemotingCommand 这个类在消息传输过程中对所有数据内容的封装,不但包含了所有的数据结构,还包含了编码解码操作 + +| Header字段 | 类型 | Request 说明 | Response 说明 | +| ---------- | ----------------------- | ------------------------------------------------------------ | ------------------------------------------- | +| code | int | 请求操作码,应答方根据不同的请求码进行不同的处理 | 应答响应码,0 表示成功,非 0 则表示各种错误 | +| language | LanguageCode | 请求方实现的语言 | 应答方实现的语言 | +| version | int | 请求方程序的版本 | 应答方程序的版本 | +| opaque | int | 相当于 requestId,在同一个连接上的不同请求标识码,与响应消息中的相对应 | 应答不做修改直接返回 | +| flag | int | 区分是普通 RPC 还是 onewayRPC 的标志 | 区分是普通 RPC 还是 onewayRPC的标志 | +| remark | String | 传输自定义文本信息 | 传输自定义文本信息 | +| extFields | HashMap | 请求自定义扩展信息 | 响应自定义扩展信息 | + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息协议.png) + +传输内容主要可以分为以下四部分: + +* 消息长度:总长度,四个字节存储,占用一个 int 类型 + +* 序列化类型&消息头长度:同样占用一个 int 类型,第一个字节表示序列化类型,后面三个字节表示消息头长度 + +* 消息头数据:经过序列化后的消息头数据 + +* 消息主体数据:消息主体的二进制字节数据内容 + + + +***** + + + +#### 通信原理 + +==todo:后期学习了源码会进行扩充,现在暂时 copy 官方文档== + +在 RocketMQ 消息队列中支持通信的方式主要有同步(sync)、异步(async)、单向(oneway)三种,其中单向通信模式相对简单,一般用在发送心跳包场景下,无需关注其 Response + +RocketMQ 的异步通信流程: + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-异步通信流程.png) + +RocketMQ 的 RPC 通信采用 Netty 组件作为底层通信库,同样也遵循了 Reactor 多线程模型,同时又在这之上做了一些扩展和优化 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Reactor设计.png) + +RocketMQ 基于 NettyRemotingServer 的 Reactor 多线程模型: + +* 一个 Reactor 主线程(eventLoopGroupBoss)负责监听 TCP 网络连接请求,建立好连接,创建 SocketChannel,并注册到 selector 上。RocketMQ 会自动根据 OS 的类型选择 NIO 和 Epoll,也可以通过参数配置),然后监听真正的网络数据 + +* 拿到网络数据交给 Worker 线程池(eventLoopGroupSelector,默认设置为 3),在真正执行业务逻辑之前需要进行 SSL 验证、编解码、空闲检查、网络连接管理,这些工作交给 defaultEventExecutorGroup(默认设置为 8)去做 +* 处理业务操作放在业务线程池中执行,根据 RomotingCommand 的业务请求码 code 去 processorTable 这个本地缓存变量中找到对应的 processor,封装成 task 任务提交给对应的业务 processor 处理线程池来执行(sendMessageExecutor,以发送消息为例) +* 从入口到业务逻辑的几个步骤中线程池一直再增加,这跟每一步逻辑复杂性相关,越复杂,需要的并发通道越宽 + +| 线程数 | 线程名 | 线程具体说明 | +| ------ | ------------------------------ | ------------------------- | +| 1 | NettyBoss_%d | Reactor 主线程 | +| N | NettyServerEPOLLSelector_%d_%d | Reactor 线程池 | +| M1 | NettyServerCodecThread_%d | Worker 线程池 | +| M2 | RemotingExecutorThread_%d | 业务 processor 处理线程池 | + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#2-%E9%80%9A%E4%BF%A1%E6%9C%BA%E5%88%B6 + + + + + +*** + + ### 消息存储 @@ -4518,9 +4649,11 @@ At least Once:至少一次,指每个消息必须投递一次,Consumer 先 #### 存储结构 -RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,消息真正的物理存储文件是 CommitLog,ConsumeQueue 是消息的逻辑队列,类似数据库的索引节点,存储的是指向物理存储的地址。每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件 +Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的 + +RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,消息真正的物理存储文件是 CommitLog,ConsumeQueue 是消息的逻辑队列,类似数据库的索引节点,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** -每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue(在 Broker 端)这个结构来读取消息实体内容 +每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) @@ -4550,7 +4683,7 @@ RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所 注意:磁盘的顺序读写要比随机读写快很多,可以匹配上网络的速度,RocketMQ 的消息采用的顺序写 -页缓存(PageCache)是 OS 对文件的缓存,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,对读写访问操作进行了性能优化, +页缓存(PageCache)是 OS 对文件的缓存,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,对读写访问操作进行了性能优化 * 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 * 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理) @@ -4609,36 +4742,38 @@ MappedByteBuffer 内存映射的方式限制一次只能映射 1.5~2G 的文件 -### 通信机制 +### 消息查询 -NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态注册与发现,生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表 +#### Message ID -NameServer 主要包括两个功能: +RocketMQ 支持按照两种维度进行消息查询:按照 Message ID 查询消息、按照 Message Key 查询消息 -* Broker 管理,NameServer 接受 Broker 集群的注册信息并保存下来作为路由信息的基本数据,提供**心跳检测**检查 Broker 活性 -* 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 +RocketMQ 中的 MessageId 的长度总共有 16 字节,其中包含了消息存储主机地址(IP 地址和端口),消息 Commit Log offset -NameServer 特点: +实现方式:Client 端从 MessageId 中解析出 Broker 的地址(IP 地址和端口)和 Commit Log 的偏移地址,封装成一个 RPC 请求后通过 Remoting 通信层发送(业务请求码 VIEW_MESSAGE_BY_ID)。Broker 端走的是 QueryMessageProcessor,读取消息的过程用其中的 CommitLog 的 offset 和 size 去 CommitLog 中找到真正的记录并解析成一个完整的消息返回 -* NameServer 通常是集群的方式部署,各实例间相互不进行信息通讯 -* Broker 向每一台 NameServer 注册自己的路由信息,所以每个 NameServer 实例上面**都保存一份完整的路由信息** -* 当某个 NameServer 因某种原因下线了,Broker 仍可以向其它 NameServer 同步其路由信息 -BrokerServer 主要负责消息的存储、投递和查询以及服务高可用保证,在 RocketMQ 系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备,也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等 -Broker 包含了以下几个重要子模块: +*** -* Remoting Module:整个 Broker 的实体,负责处理来自 clients 端的请求 -* Client Manager:负责管理客户端(Producer/Consumer)和维护 Consumer 的 Topic 订阅信息 -* Store Service:提供方便简单的 API 接口处理消息存储到物理硬盘和查询功能 +#### Message Key -* HA Service:高可用服务,提供 Master Broker 和 Slave Broker 之间的数据同步功能 +按照 Message Key 查询消息,主要是基于 RocketMQ 的 IndexFile 索引文件来实现的,RocketMQ 的索引文件逻辑结构,类似 JDK 中 HashMap 的实现,具体结构如下: -* Index Service:根据特定的 Message key 对投递到 Broker 的消息进行索引服务,以提供消息的快速查询 +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-IndexFile索引文件.png) -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Broker工作流程.png) +IndexFile 索引文件为提供了通过 Message Key 查询消息的服务,IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName 是以创建时的时间戳命名,文件大小是固定的,等于 `40+500W*4+2000W*20= 420000040` 个字节大小。如果消息的 properties 中设置了 UNIQ_KEY 这个属性,就用 `topic + “#” + UNIQ_KEY` 作为 key 来做写入操作;如果消息设置了 KEYS 属性(多个 KEY 以空格分隔),也会用 `topic + “#” + KEY` 来做索引 + +整个 Index File 的结构如图,40 Byte 的 Header 用于保存一些总的统计信息,`4*500W` 的 Slot Table 并不保存真正的索引数据,而是保存每个槽位对应的单向链表的头,即一个 Index File 可以保存 2000W 个索引,`20*2000W` 是真正的索引数据 + +索引数据包含了 Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段,一共 20 Byte + +* NextIndex offset 即前面读出来的 slotValue,如果有 hash 冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来 +* Timestamp 记录的是消息 storeTimestamp 之间的差,并不是一个绝对的时间 + +实现方式:通过 Broker 端的 QueryMessageProcessor 业务处理器来查询,读取消息的过程用 Topic 和 Key 找到 IndexFile 索引文件中的一条记录,根据其中的 CommitLog Offset 从 CommitLog 文件中读取消息的实体内容 @@ -4693,13 +4828,7 @@ RocketMQ 网络部署特点: ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-集群架构.png) -集群工作流程: - -- 启动 NameServer 监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心 -- Broker 启动,跟所有的 NameServer 保持长连接,定时发送心跳包。心跳包中包含当前 Broker 信息(IP、端口等)以及存储所有 Topic 信息。注册成功后,NameServer 集群中就有 Topic 跟 Broker 的映射关系 -- 收发消息前,先创建 Topic,创建 Topic 时需要指定该 Topic 要存储在哪些 Broker 上,也可以在发送消息时自动创建 Topic -- Producer 发送消息,启动时先跟 NameServer 集群中的其中一台建立长连接,并从 NameServer 中获取当前发送的 Topic 存在哪些 Broker 上,轮询从队列列表中选择一个队列,然后与队列所在的 Broker 建立长连接从而向 Broker 发消息。 -- Consumer 跟 Producer 类似,跟其中一台 NameServer 建立长连接,获取当前订阅 Topic 存在哪些 Broker 上,然后直接跟 Broker 建立连接通道,开始消费消息 +集群工作流程:参考通信机制 → 工作流程 @@ -4721,8 +4850,6 @@ RocketMQ 目前还不支持把 Slave 自动转成 Master,需要手动停止 Sl ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-高可用.png) -5)、6)属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。RocketMQ在这两种情况下,通过异步复制,可保证99%的消息不丢,但是仍然会有极少量的消息可能丢失。通过同步双写技术可以完全避免单点,同步双写势必会影响性能,适合对消息可靠性要求极高的场合,例如与Money相关的应用。注:RocketMQ从3.0版本开始支持同步双写。 - **** @@ -4764,11 +4891,20 @@ RocketMQ 支持消息的高可靠,影响消息可靠性的几种情况: #### 生产端 -Producer 端,每个实例在发消息的时候,默认会轮询所有的 Message Queue 发送,以让消息平均落在不同的 queue 上。而由于 queue可以散落在不同的 Broker,所以消息就发送到不同的 Broker 下 +RocketMQ 中的负载均衡可以分为 Producer 端发送消息时候的负载均衡和 Consumer 端订阅消息的负载均衡 + +Producer 端在发送消息时,会先根据 Topic 找到指定的 TopicPublishInfo,在获取了 TopicPublishInfo 路由信息后,RocketMQ 的客户端在默认方式调用 selectOneMessageQueue() 方法从 TopicPublishInfo 中的 messageQueueList 中选择一个队列 MessageQueue 进行发送消息 + +默认会轮询所有的 Message Queue 发送,以让消息平均落在不同的 queue 上,而由于 queue可以散落在不同的 Broker,所以消息就发送到不同的 Broker 下,图中箭头线条上的标号代表顺序,发布方会把第一条消息发送至 Queue 0,然后第二条消息发送至 Queue 1,以此类推: ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-producer负载均衡.png) -图中箭头线条上的标号代表顺序,发布方会把第一条消息发送至 Queue 0,然后第二条消息发送至 Queue 1,以此类推 +容错策略均在 MQFaultStrategy 这个类中定义,有一个 sendLatencyFaultEnable 开关变量: + +* 如果开启,会在随机递增取模的基础上,再过滤掉 not available 的 Broker 代理 +* 如果关闭,采用随机递增取模的方式选择一个队列(MessageQueue)来发送消息 + +latencyFaultTolerance 机制是实现消息发送高可用的核心关键所在,对之前失败的,按一定的时间做退避。例如上次请求的 latency 超过 550Lms,就退避 3000Lms;超过 1000L,就退避 60000L @@ -4778,11 +4914,15 @@ Producer 端,每个实例在发消息的时候,默认会轮询所有的 Mess #### 消费端 -广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以不存在负载均衡,在实现上,Consumer 分配 queue 时,所有 Consumer 都分到所有的queue。 +在 RocketMQ 中,Consumer 端的两种消费模式(Push/Pull)都是基于拉模式来获取消息的,而在 Push 模式只是对 Pull 模式的一种封装,其本质实现为消息拉取线程在从服务器拉取到一批消息,提交到消息消费线程池后,又继续向服务器再次尝试拉取消息,如果未拉取到消息,则延迟一下又继续拉取 + +在两种基于拉模式的消费方式(Push/Pull)中,均需要 Consumer 端在知道从 Broker 端的哪一个消息队列—队列中去获取消息,所以在 Consumer 端来做负载均衡,即 Broker 端中多个 MessageQueue 分配给同一个 ConsumerGroup 中的哪些 Consumer 消费 + +* 广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以不存在负载均衡,在实现上,Consumer 分配 queue 时,所有 Consumer 都分到所有的queue。 -在集群消费模式下,每条消息只需要投递到订阅这个 Topic 的 Consumer Group 下的一个实例即可,RocketMQ 采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条 Message Queue +* 在集群消费模式下,每条消息只需要投递到订阅这个 Topic 的 Consumer Group 下的一个实例即可,RocketMQ 采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条 Message Queue -而每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照 queue 的数量和实例的数量平均分配 queue 给每个实例。默认的分配算法是 AllocateMessageQueueAveragely: +集群模式下,每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照 queue 的数量和实例的数量平均分配 queue 给每个实例。默认的分配算法是 AllocateMessageQueueAveragely: ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-consumer负载均衡1.png) @@ -4792,9 +4932,45 @@ Producer 端,每个实例在发消息的时候,默认会轮询所有的 Mess 集群模式下,queue 都是只允许分配只一个实例,如果多个实例同时消费一个 queue 的消息,由于拉取哪些消息是 Consumer 主动控制的,会导致同一个消息在不同的实例下被消费多次 -通过增加 Consumer 实例去分摊 queue 的消费,可以起到水平扩展的消费能力的作用。而当有实例下线时,会重新触发负载均衡,这时候原来分配到的 queue 将分配到其他实例上继续消费 +通过增加 Consumer 实例去分摊 queue 的消费,可以起到水平扩展的消费能力的作用。而当有实例下线时,会重新触发负载均衡,这时候原来分配到的 queue 将分配到其他实例上继续消费。但是如果 Consumer 实例的数量比 Message Queue 的总数量还多的话,多出来的 Consumer 实例将无法分到 queue,也就无法消费到消息,也就无法起到分摊负载的作用了,所以需要控制让 queue 的总数量大于等于 Consumer 的数量 + + + +*** + + + +#### 原理解析 + +==todo:暂时 copy 官方文档,学习源码后更新,建议粗略看一下,真想搞懂过程还需要研究一下源码== + +在 Consumer 启动后,会通过定时任务不断地向 RocketMQ 集群中的所有 Broker 实例发送心跳包。Broke r端在收到 Consumer 的心跳消息后,会将它维护 在ConsumerManager 的本地缓存变量 consumerTable,同时并将封装后的客户端网络通道信息保存在本地缓存变量 channelInfoTable 中,为 Consumer 端的负载均衡提供可以依据的元数据信息 + +Consumer 端实现负载均衡的核心类 **RebalanceImpl** + +在 Consumer 实例的启动流程中的启动 MQClientInstance 实例部分,会完成负载均衡服务线程 RebalanceService 的启动(每隔 20s 执行一次),RebalanceService 线程的 run() 方法最终调用的是 RebalanceImpl 类的 rebalanceByTopic() 方法,该方法是实现 Consumer 端负载均衡的核心。rebalanceByTopic() 方法会根据消费者通信类型为广播模式还是集群模式做不同的逻辑处理。这里主要看下集群模式下的处理流程: + +* 从 rebalanceImpl 实例的本地缓存变量 topicSubscribeInfoTable 中,获取该 Topic 主题下的消息消费队列集合 mqSet + +* 根据 Topic 和 consumerGroup 为参数调用 `mQClientFactory.findConsumerIdList()` 方法向 Broker 端发送获取该消费组下消费者 ID 列表的 RPC 通信请求(Broker 端基于前面 Consumer 端上报的心跳包数据而构建的 consumerTable 做出响应返回,业务请求码 `GET_CONSUMER_LIST_BY_GROUP`) + +* 先对 Topic 下的消息消费队列、消费者 ID 排序,然后用消息队列分配策略算法(默认是消息队列的平均分配算法),计算出待拉取的消息队列。平均分配算法类似于分页的算法,将所有 MessageQueue 排好序类似于记录,将所有消费端 Consumer 排好序类似页数,并求出每一页需要包含的平均 size 和每个页面记录的范围 range,最后遍历整个 range 而计算出当前 Consumer 端应该分配到的记录(这里即为 MessageQueue) + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-负载均衡平均分配算法.png) -但是如果 Consumer 实例的数量比 Message Queue 的总数量还多的话,多出来的 Consumer 实例将无法分到 queue,也就无法消费到消息,也就无法起到分摊负载的作用了,所以需要控制让 queue 的总数量大于等于 Consumer 的数量 +* 调用 updateProcessQueueTableInRebalance() 方法,先将分配到的消息队列集合 mqSet 与 processQueueTable 做一个过滤比对 + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-负载均衡重新平衡算法.png) + +* processQueueTable 标注的红色部分,表示与分配到的消息队列集合 mqSet 互不包含,将这些队列设置 Dropped 属性为 true,然后查看这些队列是否可以移除出 processQueueTable 缓存变量。具体执行 removeUnnecessaryMessageQueue() 方法,即每隔 1s 查看是否可以获取当前消费处理队列的锁,拿到的话返回 true;如果等待 1s 后,仍然拿不到当前消费处理队列的锁则返回 false。如果返回 true,则从 processQueueTable 缓存变量中移除对应的 Entry + +* 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 消息的请求。 + + 对比下 RebalancePushImpl 和 RebalancePullImpl 两个实现类的 dispatchPullRequest() 方法,RebalancePullImpl 类里面的该方法为空 + +消息消费队列在同一消费组不同消费者之间的负载均衡,其核心设计理念是在一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列 @@ -4804,19 +4980,20 @@ Producer 端,每个实例在发消息的时候,默认会轮询所有的 Mess ### 消息重试 -todo:以下还需要修改,明日完成 +#### 重试机制 -顺序消息的重试 +Consumer 消费消息失败后,提供了一种重试机制,令消息再消费一次。Consumer 消费消息失败可以认为有以下几种情况: -对于顺序消息,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时,应用会出现消息消费被阻塞的情况。因此,在使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。 +- 由于消息本身的原因,例如反序列化失败,消息数据本身无法处理等。这种错误通常需要跳过这条消息,再消费其它消息,而这条失败的消息即使立刻重试消费,99% 也不成功,所以需要提供一种定时重试机制,即过 10秒 后再重试 +- 由于依赖的下游应用服务不可用,例如 DB 连接不可用,外系统网络不可达等。这种情况即使跳过当前失败的消息,消费其他消息同样也会报错,这种情况建议应用 sleep 30s,再消费下一条消息,这样可以减轻 Broker 重试消息的压力 -1.4.2 无序消息的重试 +RocketMQ 会为每个消费组都设置一个 Topic 名称为 `%RETRY%+consumerGroup` 的重试队列(这个 Topic 的重试队列是针对消费组,而不是针对每个 Topic 设置的),用于暂时保存因为各种异常而导致 Consumer 端无法消费的消息 -对于无序消息(普通、定时、延时、事务消息),当消费者消费消息失败时,您可以通过设置返回状态达到消息重试的结果。 +* 顺序消息的重试,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时应用会出现消息消费被阻塞的情况。所以在使用顺序消息时,必须保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生 -无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。 +* 无序消息(普通、定时、延时、事务消息)的重试,可以通过设置返回状态达到消息重试的结果。无序消息的重试只针对集群消费方式生效,广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息 -1)重试次数 +**无序消息情况下**,因为异常恢复需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ 对于重试消息的处理是先保存至 Topic 名称为 `SCHEDULE_TOPIC_XXXX` 的延迟队列中,后台定时任务按照对应的时间进行 Delay 后重新保存至 `%RETRY%+consumerGroup` 的重试队列中 消息队列 RocketMQ 默认允许每条消息最多重试 16 次,每次重试的间隔时间如下: @@ -4831,25 +5008,29 @@ todo:以下还需要修改,明日完成 | 7 | 5 分钟 | 15 | 1 小时 | | 8 | 6 分钟 | 16 | 2 小时 | -如果消息重试 16 次后仍然失败,消息将不再投递。如果严格按照上述重试时间间隔计算,某条消息在一直消费失败的前提下,将会在接下来的 4 小时 46 分钟之内进行 16 次重试,超过这个时间范围消息将不再重试投递。 +如果消息重试 16 次后仍然失败,消息将**不再投递**,如果严格按照上述重试时间间隔计算,某条消息在一直消费失败的前提下,将会在接下来的 4 小时 46 分钟之内进行 16 次重试,超过这个时间范围消息将不再重试投递 + +说明:一条消息无论重试多少次,消息的 Message ID 是不会改变的 + + + +*** -**注意:** 一条消息无论重试多少次,这些重试消息的 Message ID 不会改变。 -2)配置方式 -**消费失败后,重试配置方式** +#### 重试操作 集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确进行配置(三种方式任选一种): - 返回 Action.ReconsumeLater (推荐) -- 返回 Null +- 返回 null - 抛出异常 ```java public class MessageListenerImpl implements MessageListener { @Override public Action consume(Message message, ConsumeContext context) { - //处理消息 + // 处理消息 doConsumeMessage(message); //方式1:返回 Action.ReconsumeLater,消息将重试 return Action.ReconsumeLater; @@ -4861,9 +5042,7 @@ public class MessageListenerImpl implements MessageListener { } ``` -**消费失败后,不重试配置方式** - -集群消费方式下,消息失败后期望消息不重试,需要捕获消费逻辑中可能抛出的异常,最终返回 Action.CommitMessage,此后这条消息将不会再重试。 +集群消费方式下,消息失败后期望消息不重试,需要捕获消费逻辑中可能抛出的异常,最终返回 Action.CommitMessage,此后这条消息将不会再重试 ```java public class MessageListenerImpl implements MessageListener { @@ -4872,7 +5051,7 @@ public class MessageListenerImpl implements MessageListener { try { doConsumeMessage(message); } catch (Throwable e) { - //捕获消费逻辑中的所有异常,并返回 Action.CommitMessage; + // 捕获消费逻辑中的所有异常,并返回 Action.CommitMessage; return Action.CommitMessage; } //消息处理正常,直接返回 Action.CommitMessage; @@ -4881,35 +5060,30 @@ public class MessageListenerImpl implements MessageListener { } ``` -**自定义消息最大重试次数** - -消息队列 RocketMQ 允许 Consumer 启动的时候设置最大重试次数,重试时间间隔将按照如下策略: +自定义消息最大重试次数,RocketMQ 允许 Consumer 启动的时候设置最大重试次数,重试时间间隔将按照如下策略: -- 最大重试次数小于等于 16 次,则重试时间间隔同上表描述。 -- 最大重试次数大于 16 次,超过 16 次的重试时间间隔均为每次 2 小时。 +- 最大重试次数小于等于 16 次,则重试时间间隔同上表描述 +- 最大重试次数大于 16 次,超过 16 次的重试时间间隔均为每次 2 小时 ```java Properties properties = new Properties(); -//配置对应 Group ID 的最大消息重试次数为 20 次 +// 配置对应 Group ID 的最大消息重试次数为 20 次 properties.put(PropertyKeyConst.MaxReconsumeTimes,"20"); -Consumer consumer =ONSFactory.createConsumer(properties); +Consumer consumer = ONSFactory.createConsumer(properties); ``` -> 注意: +注意: -- 消息最大重试次数的设置对相同 Group ID 下的所有 Consumer 实例有效。 -- 如果只对相同 Group ID 下两个 Consumer 实例中的其中一个设置了 MaxReconsumeTimes,那么该配置对两个 Consumer 实例均生效。 +- 消息最大重试次数的设置对相同 Group ID 下的所有 Consumer 实例有效。例如只对相同 Group ID 下两个 Consumer 实例中的其中一个设置了 MaxReconsumeTimes,那么该配置对两个 Consumer 实例均生效 - 配置采用覆盖的方式生效,即最后启动的 Consumer 实例会覆盖之前的启动实例的配置 -**获取消息重试次数** - 消费者收到消息后,可按照如下方式获取消息的重试次数: ```java public class MessageListenerImpl implements MessageListener { @Override public Action consume(Message message, ConsumeContext context) { - //获取消息的重试次数 + // 获取消息的重试次数 System.out.println(message.getReconsumeTimes()); return Action.CommitMessage; } @@ -4918,6 +5092,22 @@ public class MessageListenerImpl implements MessageListener { +*** + + + +#### 重投机制 + +生产者在发送消息时,同步消息失败会重投,异步消息有重试,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 + +消息重复在 RocketMQ 中是无法避免的问题,如下方法可以设置消息重试策略: + +- retryTimesWhenSendFailed:同步发送失败重投次数,默认为 2,因此生产者会最多尝试发送 retryTimesWhenSendFailed + 1 次。不会选择上次失败的 Broker,尝试向其他 Broker 发送,最大程度保证消息不丢。超过重投次数抛出异常,由客户端保证消息不丢。当出现 RemotingException、MQClientException 和部分 MQBrokerException 时会重投 +- retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他 Broker,仅在同一个 Broker 上做重试,不保证消息不丢 +- retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或 slave 不可用(返回状态非 SEND_OK),是否尝试发送到其他 Broker,默认 false,十分重要消息可以开启 + + + *** @@ -4926,22 +5116,24 @@ public class MessageListenerImpl implements MessageListener { ### 死信队列 -当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。 +正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue) -在消息队列 RocketMQ 中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。 +当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试,达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的死信队列中 -死信消息具有以下特性 +死信消息具有以下特性: -- 不会再被消费者正常消费。 -- 有效期与正常消息相同,均为 3 天,3 天后会被自动删除。因此,请在死信消息产生后的 3 天内及时处理。 +- 不会再被消费者正常消费 +- 有效期与正常消息相同,均为 3 天,3 天后会被自动删除,所以请在死信消息产生后的 3 天内及时处理 死信队列具有以下特性: -- 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。 -- 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列。 -- 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。 +- 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例 +- 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列 +- 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic + +一条消息进入死信队列,需要排查可疑因素并解决问题后,可以在消息队列 RocketMQ 控制台重新发送该消息,让消费者重新消费一次 + -一条消息进入死信队列,意味着某些因素导致消费者无法正常消费该消息,因此,通常需要您对其进行特殊处理。排查可疑因素并解决问题后,可以在消息队列 RocketMQ 控制台重新发送该消息,让消费者重新消费一次。 @@ -4951,52 +5143,66 @@ public class MessageListenerImpl implements MessageListener { ### 幂等消费 -消息队列 RocketMQ 消费者在接收到消息以后,有必要根据业务上的唯一 Key 对消息做幂等处理的必要性。 +消息队列 RocketMQ 消费者在接收到消息以后,需要根据业务上的唯一 Key 对消息做幂等处理 -1.6.1 消费幂等的必要性 +在互联网应用中,尤其在网络不稳定的情况下,消息队列 RocketMQ 的消息有可能会出现重复,几种情况: -在互联网应用中,尤其在网络不稳定的情况下,消息队列 RocketMQ 的消息有可能会出现重复,这个重复简单可以概括为以下情况: +- 发送时消息重复:当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或客户端宕机,导致服务端对客户端应答失败。此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息 -- 发送时消息重复 +- 投递时消息重复:消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。为了保证消息至少被消费一次,消息队列 RocketMQ 的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息 - 当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败。 如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。 +- 负载均衡时消息重复:当消息队列 RocketMQ 的 Broker 或客户端重启、扩容或缩容时,会触发 Rebalance,此时消费者可能会收到重复消息 -- 投递时消息重复 - 消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。 为了保证消息至少被消费一次,消息队列 RocketMQ 的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。 +处理方式: -- 负载均衡时消息重复(包括但不限于网络抖动、Broker 重启以及订阅方应用重启) +* 因为 Message ID 有可能出现冲突(重复)的情况,所以真正安全的幂等处理,不建议以 Message ID 作为处理依据,最好的方式是以业务唯一标识作为幂等处理的关键依据,而业务的唯一标识可以通过消息 Key 进行设置: - 当消息队列 RocketMQ 的 Broker 或客户端重启、扩容或缩容时,会触发 Rebalance,此时消费者可能会收到重复消息。 + ```java + Message message = new Message(); + message.setKey("ORDERID_100"); + SendResult sendResult = producer.send(message); + ``` -1.6.2 处理方式 +* 订阅方收到消息时可以根据消息的 Key 进行幂等处理: -因为 Message ID 有可能出现冲突(重复)的情况,所以真正安全的幂等处理,不建议以 Message ID 作为处理依据。 最好的方式是以业务唯一标识作为幂等处理的关键依据,而业务的唯一标识可以通过消息 Key 进行设置: + ```java + consumer.subscribe("ons_test", "*", new MessageListener() { + public Action consume(Message message, ConsumeContext context) { + String key = message.getKey() + // 根据业务唯一标识的 key 做幂等处理 + } + }); + ``` -```java -Message message = new Message(); -message.setKey("ORDERID_100"); -SendResult sendResult = producer.send(message); -``` + + + + +*** -订阅方收到消息时可以根据消息的 Key 进行幂等处理: -```java -consumer.subscribe("ons_test", "*", new MessageListener() { - public Action consume(Message message, ConsumeContext context) { - String key = message.getKey() - // 根据业务唯一标识的 key 做幂等处理 - } -}); -``` +### 流量控制 +生产者流控,因为 Broker 处理能力达到瓶颈;消费者流控,因为消费能力达到瓶颈 +生产者流控: +- CommitLog 文件被锁时间超过 osPageCacheBusyTimeOutMills 时,参数默认为 1000ms,返回流控 +- 如果开启 transientStorePoolEnable == true,且 Broker 为异步刷盘的主机,且 transientStorePool 中资源不足,拒绝当前 send 请求,返回流控 +- Broker 每隔 10ms 检查 send 请求队列头部请求的等待时间,如果超过 waitTimeMillsInSendQueue,默认 200ms,拒绝当前 send 请求,返回流控。 +- Broker 通过拒绝 send 请求方式实现流量控制 +注意:生产者流控,不会尝试消息重投 +消费者流控: +- 消费者本地缓存消息数超过 pullThresholdForQueue 时,默认 1000 +- 消费者本地缓存消息大小超过 pullThresholdSizeForQueue 时,默认 100MB +- 消费者本地缓存消息跨度超过 consumeConcurrentlyMaxSpan 时,默认 2000 +消费者流控的结果是降低拉取频率 From df4af1f76c9d889b5470173aac5e25d7ff1c92a0 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 17 Nov 2021 00:37:29 +0800 Subject: [PATCH 030/122] Update Java Notes --- Frame.md | 2 +- Java.md | 66 ++++++++++++++++++++++++++++---------------------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Frame.md b/Frame.md index 3536a79..c8e1908 100644 --- a/Frame.md +++ b/Frame.md @@ -4942,7 +4942,7 @@ latencyFaultTolerance 机制是实现消息发送高可用的核心关键所在 #### 原理解析 -==todo:暂时 copy 官方文档,学习源码后更新,建议粗略看一下,真想搞懂过程还需要研究一下源码== +==todo:暂时 copy 官方文档,学习源码后更新,真想搞懂过程还需要研究一下源码== 在 Consumer 启动后,会通过定时任务不断地向 RocketMQ 集群中的所有 Broker 实例发送心跳包。Broke r端在收到 Consumer 的心跳消息后,会将它维护 在ConsumerManager 的本地缓存变量 consumerTable,同时并将封装后的客户端网络通道信息保存在本地缓存变量 channelInfoTable 中,为 Consumer 端的负载均衡提供可以依据的元数据信息 diff --git a/Java.md b/Java.md index e918610..4f49e7c 100644 --- a/Java.md +++ b/Java.md @@ -2442,22 +2442,22 @@ s = s + "cd"; //s = abccd 新对象 常用 API: -* `public boolean equals(String s)` : 比较两个字符串内容是否相同、区分大小写 - -* `public boolean equalsIgnoreCase(String anotherString)` : 比较字符串的内容,忽略大小写 -* `public int length()` : 返回此字符串的长度 -* `public String trim()` : 返回一个字符串,其值为此字符串,并删除任何前导和尾随空格 -* `public String[] split(String regex)` : 将字符串按给定的正则表达式分割成字符串数组 -* `public char charAt(int index)` : 取索引处的值 -* `public char[] toCharArray()` : 将字符串拆分为字符数组后返回 -* `public boolean startsWith(String prefix)` : 测试此字符串是否以指定的前缀开头 -* `public int indexOf(String str)` : 返回指定子字符串第一次出现的字符串内的索引,没有返回 -1 -* `public int lastIndexOf(String str)` : 返回字符串最后一次出现str的索引,没有返回 -1 -* `public String substring(int beginIndex)` : 返回子字符串,以原字符串指定索引处到结尾 -* `public String substring(int i, int j)` : 指定索引处扩展到 j - 1 的位置,字符串长度为 j - i -* `public String toLowerCase()` : 将此 String 所有字符转换为小写,使用默认语言环境的规则 -* `public String toUpperCase()` : 使用默认语言环境的规则将此 String 所有字符转换为大写 -* `public String replace(CharSequence target, CharSequence replacement)` : 使用新值,将字符串中的旧值替换,得到新的字符串 +* `public boolean equals(String s)`:比较两个字符串内容是否相同、区分大小写 + +* `public boolean equalsIgnoreCase(String anotherString)`:比较字符串的内容,忽略大小写 +* `public int length()`:返回此字符串的长度 +* `public String trim()`:返回一个字符串,其值为此字符串,并删除任何前导和尾随空格 +* `public String[] split(String regex)`:将字符串按给定的正则表达式分割成字符串数组 +* `public char charAt(int index)`:取索引处的值 +* `public char[] toCharArray()`:将字符串拆分为字符数组后返回 +* `public boolean startsWith(String prefix)`:测试此字符串是否以指定的前缀开头 +* `public int indexOf(String str)`:返回指定子字符串第一次出现的字符串内的索引,没有返回 -1 +* `public int lastIndexOf(String str)`:返回字符串最后一次出现str的索引,没有返回 -1 +* `public String substring(int beginIndex)`:返回子字符串,以原字符串指定索引处到结尾 +* `public String substring(int i, int j)`:指定索引处扩展到 j - 1 的位置,字符串长度为 j - i +* `public String toLowerCase()`:将此 String 所有字符转换为小写,使用默认语言环境的规则 +* `public String toUpperCase()`:使用默认语言环境的规则将此 String 所有字符转换为大写 +* `public String replace(CharSequence target, CharSequence replacement)`:使用新值,将字符串中的旧值替换,得到新的字符串 ```java String s = 123-78; @@ -2474,9 +2474,9 @@ s.replace("-","");//12378 构造方法: -* `public String()` : 创建一个空白字符串对象,不含有任何内容 -* `public String(char[] chs)` : 根据字符数组的内容,来创建字符串对象 -* `public String(String original)` : 根据传入的字符串内容,来创建字符串对象 +* `public String()`:创建一个空白字符串对象,不含有任何内容 +* `public String(char[] chs)`:根据字符数组的内容,来创建字符串对象 +* `public String(String original)`:根据传入的字符串内容,来创建字符串对象 直接赋值:`String s = "abc"` 直接赋值的方式创建字符串对象,内容就是 abc @@ -3009,20 +3009,20 @@ JDK1.8 新增,线程安全 常用API: -| 方法名 | 说明 | -| --------------------------------------------------------- | ----------------------------------------------------------- | -| public int getYear() | 获取年 | -| public int getMonthValue() | 获取月份(1-12) | -| public int getDayOfMonth() | 获取月份中的第几天(1-31) | -| public int getDayOfYear() | 获取一年中的第几天(1-366) | -| public DayOfWeek getDayOfWeek() | 获取星期 | -| public int getMinute() | 获取分钟 | -| public int getHour() | 获取小时 | -| public LocalDate toLocalDate() | 转换成为一个LocalDate对象(年月日) | -| public LocalTime toLocalTime() | 转换成为一个LocalTime对象(时分秒) | -| public String format(指定格式) | 把一个LocalDateTime格式化成为一个字符串 | -| public LocalDateTime parse(准备解析的字符串, 解析格式) | 把一个日期字符串解析成为一个LocalDateTime对象 | -| public static DateTimeFormatter ofPattern(String pattern) | 使用指定的日期模板获取一个日期格式化器DateTimeFormatter对象 | +| 方法名 | 说明 | +| --------------------------------------------------------- | ------------------------------------------------------------ | +| public int getYear() | 获取年 | +| public int getMonthValue() | 获取月份(1-12) | +| public int getDayOfMonth() | 获取月份中的第几天(1-31) | +| public int getDayOfYear() | 获取一年中的第几天(1-366) | +| public DayOfWeek getDayOfWeek() | 获取星期 | +| public int getMinute() | 获取分钟 | +| public int getHour() | 获取小时 | +| public LocalDate toLocalDate() | 转换成为一个 LocalDate 对象(年月日) | +| public LocalTime toLocalTime() | 转换成为一个 LocalTime 对象(时分秒) | +| public String format(指定格式) | 把一个 LocalDateTime 格式化成为一个字符串 | +| public LocalDateTime parse(准备解析的字符串, 解析格式) | 把一个日期字符串解析成为一个 LocalDateTime 对象 | +| public static DateTimeFormatter ofPattern(String pattern) | 使用指定的日期模板获取一个日期格式化器 DateTimeFormatter 对象 | ```java public class JDK8DateDemo2 { From 5d4885103b4598a7d5f6056955d9acdd90bff9a2 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 20 Nov 2021 15:05:06 +0800 Subject: [PATCH 031/122] Update Java Notes --- Prog.md | 229 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 206 insertions(+), 23 deletions(-) diff --git a/Prog.md b/Prog.md index e0ff4ea..41bb420 100644 --- a/Prog.md +++ b/Prog.md @@ -622,7 +622,7 @@ t.start(); | public final void suspend() | **挂起(暂停)线程运行** | | public final void resume() | 恢复线程运行 | -所以 Java 中线程的状态是阻塞,很少使用挂起 + @@ -9289,7 +9289,7 @@ ReentrantReadWriteLock 其**读锁是共享锁,写锁是独占锁** * **重入时升级不支持**:持有读锁的情况下去获取写锁会导致获取写锁永久等待,需要先释放读,再去获得写 -* **重入时降级支持**:持有写锁的情况下去获取读锁 +* **重入时降级支持**:持有写锁的情况下去获取读锁,造成只有当前线程会持有读锁,因为写锁会互斥其他的锁 ```java w.lock(); @@ -9357,7 +9357,7 @@ public static void main(String[] args) { 缓存更新时,是先清缓存还是先更新数据库 -* 先清缓存:可能造成刚清理缓存还没有更新数据库,线程直接查询了数据库更新缓存 +* 先清缓存:可能造成刚清理缓存还没有更新数据库,线程直接查询了数据库更新过期数据到缓存 * 先更新据库:可能造成刚更新数据库,还没清空缓存就有线程从缓存拿到了旧数据 @@ -9375,14 +9375,110 @@ public static void main(String[] args) { #### 实现原理 -##### 加锁原理 +##### 成员属性 读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个,原理与 ReentrantLock 加锁相比没有特殊之处,不同是**写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位** -* t1 w.lock(写锁),成功上锁 state = 0_1 +* 读写锁: + + ```java + private final ReentrantReadWriteLock.ReadLock readerLock; + private final ReentrantReadWriteLock.WriteLock writerLock; + ``` + +* 构造方法:默认是非公平锁,可以指定参数创建公平锁 + + ```java + public ReentrantReadWriteLock(boolean fair) { + // true 为公平锁 + sync = fair ? new FairSync() : new NonfairSync(); + // 这两个 lock 共享同一个 sync 实例,都是由 ReentrantReadWriteLock 的 sync 提供同步实现 + readerLock = new ReadLock(this); + writerLock = new WriteLock(this); + } + ``` + +Sync 类的属性: + +* 统计变量: + + ```java + // 用来移位 + static final int SHARED_SHIFT = 16; + // 高16位的1 + static final int SHARED_UNIT = (1 << SHARED_SHIFT); + // 65535,16个1,代表写锁的最大重入次数 + static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; + // 低16位掩码:0b 1111 1111 1111 1111,用来获取写锁重入的次数 + static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; + ``` + +* 获取读写锁的次数: + + ```java + // 获取读写锁的读锁分配的总次数 + static int sharedCount(int c) { return c >>> SHARED_SHIFT; } + // 写锁(独占)锁的重入次数 + static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } + ``` + +* 内部类: + + ```java + // 记录读锁线程自己的持有读锁的数量(重入次数),因为 state 高16位记录的是全局范围内所有的读线程获取读锁的总量 + static final class HoldCounter { + int count = 0; + // Use id, not reference, to avoid garbage retention + final long tid = getThreadId(Thread.currentThread()); + } + // 线程安全的存放线程各自的 HoldCounter 对象 + static final class ThreadLocalHoldCounter extends ThreadLocal { + public HoldCounter initialValue() { + return new HoldCounter(); + } + } + ``` + +* 内部类实例: + + ```java + // 当前线程持有的可重入读锁的数量,计数为 0 时删除 + private transient ThreadLocalHoldCounter readHolds; + // 记录最后一个获取【读锁】线程的 HoldCounter 对象 + private transient HoldCounter cachedHoldCounter; + ``` + +* 首次获取锁: + + ```java + // 第一个获取读锁的线程 + private transient Thread firstReader = null; + // 记录该线程持有的读锁次数(读锁重入次数) + private transient int firstReaderHoldCount; + ``` + +* Sync 构造方法: ```java - //lock() -> sync.acquire(1); + Sync() { + readHolds = new ThreadLocalHoldCounter(); + // 确保其他线程的数据可见性,state 是 volatile 修饰的变量,重写该值会将线程本地缓存数据【同步至主存】 + setState(getState()); + } + ``` + + + +*** + + + +##### 加锁原理 + +* t1 线程:w.lock(**写锁**),成功上锁 state = 0_1 + + ```java + // lock() -> sync.acquire(1); public void lock() { sync.acquire(1); } @@ -9399,20 +9495,22 @@ public static void main(String[] args) { int c = getState(); // 获得低 16 位, 代表写锁的 state 计数 int w = exclusiveCount(c); + // 说明有读锁或者写锁 if (c != 0) { - // c != 0 and w == 0 表示 r != 0,有读锁,读锁不能升级,直接返回false + // c != 0 and w == 0 表示有读锁,【读锁不能升级】,直接返回 false // w != 0 说明有写锁,写锁的拥有者不是自己,获取失败 if (w == 0 || current != getExclusiveOwnerThread()) return false; - // 锁重入计数超过低 16 位, 报异常 + + // 执行到这里只有一种情况:【写锁重入】,所以下面几行代码不存在并发 if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); - // 【写锁重入, 获得锁成功】 + // 写锁重入, 获得锁成功,没有并发,所以不使用 CAS setState(c + acquires); return true; } - // c == 0,没有任何锁,判断写锁是否该阻塞,是 false 就尝试获取锁,失败返回 false + // c == 0,说明没有任何锁,判断写锁是否该阻塞,是 false 就尝试获取锁,失败返回 false if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; // 获得锁成功,设置锁的持有线程为当前线程 @@ -9429,11 +9527,11 @@ public static void main(String[] args) { } ``` -* t2 r.lock(读锁),进入 tryAcquireShared 流程,如果有写锁占据,那么 tryAcquireShared +* t2 r.lock(**读锁**),进入 tryAcquireShared 流程: * 返回 -1 表示失败 * 如果返回 0 表示成功 - * 返回正数表示还有多少后继节点支持共享模式,读写锁返回1 + * 返回正数表示还有多少后继节点支持共享模式,读写锁返回 1 ```java public void lock() { @@ -9451,28 +9549,113 @@ public static void main(String[] args) { protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); - // 低 16 位, 代表写锁的 state - // 如果是其它线程持有写锁, 并且写锁的持有者不是当前线程,获取读锁失败,【写锁允许降级】 + // exclusiveCount(c) 代表低 16 位, 写锁的 state,成立说明有线程持有写锁 + // 写锁的持有者不是当前线程,则获取读锁失败,【写锁允许降级】 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; - // 高 16 位,代表读锁的 state + // 高 16 位,代表读锁的 state,共享锁分配出去的总次数 int r = sharedCount(c); - if (!readerShouldBlock() && // 读锁不该阻塞 - r < MAX_COUNT && // 小于读锁计数 - compareAndSetState(c, c + SHARED_UNIT)) {// 尝试增加计数成功 - // .... + // 读锁是否应该阻塞 + if (!readerShouldBlock() && r < MAX_COUNT && + compareAndSetState(c, c + SHARED_UNIT)) { // 尝试增加读锁计数 + // 加锁成功 + // 加锁之前读锁为 0,说明当前线程是第一个读锁线程 + if (r == 0) { + firstReader = current; + firstReaderHoldCount = 1; + // 第一个读锁线程是自己就发生了读锁重入 + } else if (firstReader == current) { + firstReaderHoldCount++; + } else { + // cachedHoldCounter 设置为当前线程的 holdCounter 对象,即最后一个获取读锁的线程 + HoldCounter rh = cachedHoldCounter; + // 说明还没设置 rh + if (rh == null || rh.tid != getThreadId(current)) + // 获取当前线程的锁重入的对象,赋值给 cachedHoldCounter + cachedHoldCounter = rh = readHolds.get(); + // 还没重入 + else if (rh.count == 0) + readHolds.set(rh); + // 重入 + 1 + rh.count++; + } // 读锁加锁成功 return 1; } - // 与 tryAcquireShared 功能类似, 但会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞 + // 逻辑到这 应该阻塞,或者 cas 加锁失败 + // 会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞 return fullTryAcquireShared(current); } - // 非公平锁 readerShouldBlock 看 AQS 队列中第一个节点是否是写锁,是则阻塞,反之不阻塞 + // 非公平锁 readerShouldBlock 偏向写锁一些,看 AQS 阻塞队列中第一个节点是否是写锁,是则阻塞,反之不阻塞 + // 防止一直有读锁线程,导致写锁线程饥饿 // true 则该阻塞, false 则不阻塞 final boolean readerShouldBlock() { return apparentlyFirstQueuedIsExclusive(); } + final boolean readerShouldBlock() { + return hasQueuedPredecessors(); + } + ``` + + ```java + final int fullTryAcquireShared(Thread current) { + // 当前读锁线程持有的读锁次数对象 + HoldCounter rh = null; + for (;;) { + int c = getState(); + // 说明有线程持有写锁 + if (exclusiveCount(c) != 0) { + // 写锁不是自己则获取锁失败 + if (getExclusiveOwnerThread() != current) + return -1; + } else if (readerShouldBlock()) { + // 条件成立说明当前线程是 firstReader,当前锁是读忙碌状态,而且当前线程也是读锁重入 + if (firstReader == current) { + // assert firstReaderHoldCount > 0; + } else { + if (rh == null) { + // 最后一个读锁的 HoldCounter + rh = cachedHoldCounter; + // 说明当前线程也不是最后一个读锁 + if (rh == null || rh.tid != getThreadId(current)) { + // 获取当前线程的 HoldCounter + rh = readHolds.get(); + // 条件成立说明 HoldCounter 对象是上一步代码新建的 + // 当前线程不是锁重入,在 readerShouldBlock() 返回 true 时需要去排队 + if (rh.count == 0) + // 防止内存泄漏 + readHolds.remove(); + } + } + if (rh.count == 0) + return -1; + } + } + // 越界判断 + if (sharedCount(c) == MAX_COUNT) + throw new Error("Maximum lock count exceeded"); + // 读锁加锁,条件内的逻辑与 tryAcquireShared 相同 + if (compareAndSetState(c, c + SHARED_UNIT)) { + if (sharedCount(c) == 0) { + firstReader = current; + firstReaderHoldCount = 1; + } else if (firstReader == current) { + firstReaderHoldCount++; + } else { + if (rh == null) + rh = cachedHoldCounter; + if (rh == null || rh.tid != getThreadId(current)) + rh = readHolds.get(); + else if (rh.count == 0) + readHolds.set(rh); + rh.count++; + cachedHoldCounter = rh; // cache for release + } + return 1; + } + } + } ``` * 获取读锁失败,进入 sync.doAcquireShared(1) 流程开始阻塞,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态 @@ -9603,7 +9786,7 @@ public static void main(String[] args) { else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; } - // 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的head, + // 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的 head, // 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新的 head 节点的后继节点 if (h == head) break; @@ -9632,7 +9815,7 @@ public static void main(String[] args) { ```java protected final boolean tryReleaseShared(int unused) { - // + for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; From 50c329b304ca222a20f25b00a04a2bf6c01c4845 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 21 Nov 2021 16:56:13 +0800 Subject: [PATCH 032/122] Update Java Notes --- Java.md | 6 ++-- Prog.md | 97 +++++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 69 insertions(+), 34 deletions(-) diff --git a/Java.md b/Java.md index 4f49e7c..2ac7b25 100644 --- a/Java.md +++ b/Java.md @@ -4414,7 +4414,7 @@ TreeSet 集合自排序的方式: * 直接为**对象的类**实现比较器规则接口 Comparable,重写比较方法: - 方法:`public int compareTo(Employee o): this 是比较者, o 是被比较者` + 方法:`public int compareTo(Employee o): this 是比较者, o 是被比较者` * 比较者大于被比较者,返回正数(升序) * 比较者小于被比较者,返回负数 @@ -5948,8 +5948,8 @@ class Dog{} + 这个集合不能添加,不能删除,不能修改 + 但是可以结合集合的带参构造,实现集合的批量添加 -在Map接口中,还有一个ofEntries方法可以提高代码的阅读性 -+ 首先会把键值对封装成一个Entry对象,再把这个Entry对象添加到集合当中 +在 Map 接口中,还有一个 ofEntries 方法可以提高代码的阅读性 ++ 首先会把键值对封装成一个 Entry 对象,再把这个 Entry 对象添加到集合当中 ````java public class MyVariableParameter4 { diff --git a/Prog.md b/Prog.md index 41bb420..82e14e6 100644 --- a/Prog.md +++ b/Prog.md @@ -6845,9 +6845,6 @@ FutureTask 类的成员方法: -参考视频:https://www.bilibili.com/video/BV13E411N7pp - - **** @@ -6912,8 +6909,8 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { 常用 API: * `ScheduledFuture schedule(Runnable/Callable, long delay, TimeUnit u)`:延迟执行任务 -* `ScheduledFuture scheduleAtFixedRate(Runnable/Callable, long initialDelay, long period, TimeUnit unit)`:定时执行任务,参数为初始延迟时间、间隔时间、单位 -* `ScheduledFuture scheduleWithFixedDelay(Runnable/Callable, long initialDelay, long delay, TimeUnit unit)`:定时执行任务,参数为初始延迟时间、间隔时间、单位 +* `ScheduledFuture scheduleAtFixedRate(Runnable/Callable, long initialDelay, long period, TimeUnit unit)`:定时执行周期任务,不考虑执行的耗时,参数为初始延迟时间、间隔时间、单位 +* `ScheduledFuture scheduleWithFixedDelay(Runnable/Callable, long initialDelay, long delay, TimeUnit unit)`:定时执行周期任务,考虑执行的耗时,参数为初始延迟时间、间隔时间、单位 基本使用: @@ -6983,7 +6980,7 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { ##### 成员变量 -* shutdown 后是否继续执行定时任务: +* shutdown 后是否继续执行周期任务: ```java private volatile boolean continueExistingPeriodicTasksAfterShutdown; @@ -6998,10 +6995,11 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { * 取消方法是否将该任务从队列中移除: ```java + // 默认 false,不移除,等到线程拿到任务之后抛弃 private volatile boolean removeOnCancel = false; ``` -* 任务的序列号: +* 任务的序列号,可以用来比较优先级: ```java private static final AtomicLong sequencer = new AtomicLong(); @@ -7015,7 +7013,7 @@ public ScheduledThreadPoolExecutor(int corePoolSize) { ##### 延迟任务 -ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口,具有延迟执行的特点,覆盖 FutureTask 的 run 方法来实现对**延时执行、周期执行**的支持。对于延时任务调用 FutureTask#run,而对于周期性任务则调用 FutureTask#runAndReset 并且在成功之后根据 fixed-delay/fixed-rate 模式来设置下次执行时间并重新将任务塞到工作队列。 +ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口,具有延迟执行的特点,覆盖 FutureTask 的 run 方法来实现对**延时执行、周期执行**的支持。对于延时任务调用 FutureTask#run,而对于周期性任务则调用 FutureTask#runAndReset 并且在成功之后根据 fixed-delay/fixed-rate 模式来设置下次执行时间并重新将任务塞到工作队列 在调度线程池中无论是 runnable 还是 callable,无论是否需要延迟和定时,所有的任务都会被封装成 ScheduledFutureTask @@ -7030,7 +7028,7 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, * 执行时间: ```java - private long time; // 任务可以被执行的时间,以纳秒表示 + private long time; // 任务可以被执行的时间,交付时间,以纳秒表示 private final long period; // 0 表示非周期任务,正数表示 fixed-rate 模式的周期,负数表示 fixed-delay 模式 ``` @@ -7045,7 +7043,8 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, * 任务在队列数组中的索引下标: ```java - int heapIndex; // -1 代表删除 + // DelayedWorkQueue 底层使用的数据结构是最小堆,记录当前任务在堆中的索引,-1 代表删除 + int heapIndex; ``` 成员方法: @@ -7066,13 +7065,40 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, * compareTo():ScheduledFutureTask 根据执行时间 time 正序排列,如果执行时间相同,在按照序列号 sequenceNumber 正序排列,任务需要放入 DelayedWorkQueue,延迟队列中使用该方法按照从小到大进行排序 + ```java + public int compareTo(Delayed other) { + if (other == this) // compare zero if same object + return 0; + if (other instanceof ScheduledFutureTask) { + // 类型强转 + ScheduledFutureTask x = (ScheduledFutureTask)other; + // 比较者 - 被比较者的执行时间 + long diff = time - x.time; + // 比较者先执行 + if (diff < 0) + return -1; + // 被比较者先执行 + else if (diff > 0) + return 1; + // 比较者的序列号小 + else if (sequenceNumber < x.sequenceNumber) + return -1; + else + return 1; + } + // 不是 ScheduledFutureTask 类型时,根据延迟时间排序 + long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS); + return (diff < 0) ? -1 : (diff > 0) ? 1 : 0; + } + ``` + * run():执行任务,非周期任务直接完成直接结束,**周期任务执行完后会设置下一次的执行时间,重新放入线程池的阻塞队列**,如果线程池中的线程数量少于核心线程,就会添加 Worker 开启新线程 ```java public void run() { // 是否周期性,就是判断 period 是否为 0 boolean periodic = isPeriodic(); - // 检查当前状态能否执行任务,不能执行就取消任务 + // 根据是否是周期任务检查当前状态能否执行任务,不能执行就取消任务 if (!canRunInCurrentRunState(periodic)) cancel(false); // 非周期任务,直接调用 FutureTask#run 执行 @@ -7088,7 +7114,7 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, } ``` - 周期任务正常完成后任务的状态不会变化,依旧是 NEW,不会设置 outcome 属性。但是如果本次任务执行出现异常,会进入 setException 方法将任务状态置为异常,把异常保存在 outcome 中,方法返回 false,后续的该任务将不会再周期的执行 + 周期任务正常完成后**任务的状态不会变化**,依旧是 NEW,不会设置 outcome 属性。但是如果本次任务执行出现异常,会进入 setException 方法将任务状态置为异常,把异常保存在 outcome 中,方法返回 false,后续的该任务将不会再周期的执行 ```java protected boolean runAndReset() { @@ -7128,10 +7154,10 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, private void setNextRunTime() { long p = period; if (p > 0) - // fixed-rate 模式,【时间设置为上一次执行任务的时间 +p】,两次任务执行的时间差 + // fixed-rate 模式,【时间设置为上一次执行任务的时间 + p】,两次任务执行的时间差 time += p; else - // fixed-delay 模式,下一次执行时间是当【前这次任务结束的时间(就是现在) +delay 值】 + // fixed-delay 模式,下一次执行时间是【当前这次任务结束的时间(就是现在) + delay 值】 time = triggerTime(-p); } ``` @@ -7144,7 +7170,8 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, if (canRunInCurrentRunState(true)) { // 【放入任务队列】 super.getQueue().add(task); - // 再次检查是否可以执行,如果不能执行且任务还在队列中未被取走,则取消任务 + // 如果提交完任务之后,线程池状态变为了 shutdown 状态,需要再次检查是否可以执行, + // 如果不能执行且任务还在队列中未被取走,则取消任务 if (!canRunInCurrentRunState(true) && remove(task)) task.cancel(false); else @@ -7178,9 +7205,9 @@ ScheduledFutureTask 继承 FutureTask,实现 RunnableScheduledFuture 接口, ##### 延迟队列 -DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先队列 PriorityQueue(小根堆)存储元素 +DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先队列 PriorityQueue(小根堆、满二叉树)存储元素 -其他阻塞队列存储节点的数据结构大都是链表,**延迟队列是数组**,所以延迟队列出队头元素后需要让其他元素(尾)替换到头节点,防止空指针异常 +其他阻塞队列存储节点的数据结构大都是链表,**延迟队列是数组**,所以延迟队列出队头元素后需要**让其他元素(尾)替换到头节点**,防止空指针异常 成员变量: @@ -7203,9 +7230,10 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 * 阻塞等待头节点的线程: ```java - // 通过阻塞方式去获取头结点,那么 leader 线程的等待时间为头结点的延迟时间,其它线程则会陷入阻塞状态 - // leader 线程获取到头结点后需要发送信号唤醒其它线程 available.asignAll() - // 使用了 Leader/Follower 来避免不必要的等待,只让leader来等待需要等待的时间,其余线程无限等待直至被唤醒即可 + // 线程池内的某个线程去 take() 获取任务时,如果延迟队列顶层节点不为null(队列内有任务),但是节点任务还不到触发时间,线程就去检查【队列的 leader】字段是否被占用 + // * 如果未被占用,则当前线程占用该字段,然后当前线程到 available 条件队列指定超时时间(堆顶任务.time - now())挂起 + // * 如果被占用,当前线程直接到 available 条件队列“不指定”超时时间的挂起 + // leader 在 available 条件队列内是首元素,它超时之后会醒过来,然后再次将堆顶元素获取走,获取走之后,take()结束之前,会调用是 available.signal() 唤醒下一个条件队列内的等待者,然后释放 lock,下一个等待者被唤醒后去到 AQS 队列,做 acquireQueue(node) 逻辑 private Thread leader = null; ``` @@ -7219,7 +7247,7 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 if (x == null) throw new NullPointerException(); RunnableScheduledFuture e = (RunnableScheduledFuture)x; - // 队列锁 + // 队列锁,增加删除数据时都要加锁 final ReentrantLock lock = this.lock; lock.lock(); try { @@ -7238,7 +7266,11 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 // 向上调整元素的位置,并更新 heapIndex siftUp(i, e); } - // 【插入的元素是头节点,原先的 leader 等待的是原先的头节点,所以 leader 已经无效】 + // 情况1:当前任务是第一个加入到 queue 内的任务,所以在当前任务加入到 queue 之前,take() 线程会直接 + // 到 available 队列不设置超时的挂起,并不会去占用 leader 字段,这时需会唤醒一个线程 让它去消费 + // 情况2:当前任务优先级最高,原堆顶任务可能还未到触发时间,leader 线程设置超时的在 available 挂起 + // 原先的 leader 等待的是原先的头节点,所以 leader 已经无效,需要将 leader 线程唤醒, + // 唤醒之后它会检查堆顶,如果堆顶任务可以被消费,则直接获取走,否则继续成为 leader 等待新堆顶任务 if (queue[0] == e) { // 将 leader 设置为 null leader = null; @@ -7295,11 +7327,13 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 ```java private RunnableScheduledFuture finishPoll(RunnableScheduledFuture f) { + // 获取尾索引 int s = --size; // 获取尾节点 RunnableScheduledFuture x = queue[s]; - // 置空 + // 将堆结构最后一个节点占用的 slot 设置为 null,因为该节点要尝试升级成堆顶,会根据特性下调 queue[s] = null; + // s == 0 说明 当前堆结构只有堆顶一个节点,此时不需要做任何的事情 if (s != 0) // 从索引处 0 开始向下调整 siftDown(0, x); @@ -7309,11 +7343,12 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 } ``` -* take():阻塞获取头节点,读取当前堆中最小的也就是执行开始时间最近的任务 +* take():阻塞获取头节点,读取当前堆中最小的也就是触发时间最近的任务 ```java public RunnableScheduledFuture take() throws InterruptedException { final ReentrantLock lock = this.lock; + // 保证线程安全 lock.lockInterruptibly(); try { for (;;) { @@ -7323,15 +7358,15 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 // 等待队列不空,直至有任务通过 offer 入队并唤醒 available.await(); else { - // 获取头节点的剩延迟时间是否到时 + // 获取头节点的延迟时间是否到时 long delay = first.getDelay(NANOSECONDS); if (delay <= 0) - // 到时了,获取头节点并调整堆,重新选择延迟时间最小的节点放入头部 + // 到达触发时间,获取头节点并调整堆,重新选择延迟时间最小的节点放入头部 return finishPoll(first); // 逻辑到这说明头节点的延迟时间还没到 first = null; - // 说明有 leader 线程在等待获取头节点,需要阻塞等待 + // 说明有 leader 线程在等待获取头节点,当前线程直接去阻塞等待 if (leader != null) available.await(); else { @@ -7339,12 +7374,12 @@ DelayedWorkQueue 是支持延时获取元素的阻塞队列,内部采用优先 Thread thisThread = Thread.currentThread(); leader = thisThread; try { + // 在条件队列 available 使用带超时的挂起(堆顶任务.time - now() 纳秒值..) available.awaitNanos(delay); + // 到达阻塞时间时,当前线程会从来 } finally { - // 条件成立的情况: - // 1. 原先 thisThread == leader, 然后堆顶更新了,leader 被置为 null - // 2. 堆顶更新,offer 方法释放锁后,有其它线程通过 take/poll 拿到锁, - // 读到 leader == null,然后将自身更新为leader。 + // t堆顶更新,leader 置为 null,offer 方法释放锁后, + // 有其它线程通过 take/poll 拿到锁,读到 leader == null,然后将自身更新为leader。 if (leader == thisThread) // leader 置为 null 用以接下来判断是否需要唤醒后继线程 leader = null; From 84fad7fbef6e6c15ee438bd3bbe9b70f0fb234c0 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 23 Nov 2021 01:04:54 +0800 Subject: [PATCH 033/122] Update Java Notes --- DB.md | 165 +++++++++++++++++++++++++++++++--------------------------- 1 file changed, 88 insertions(+), 77 deletions(-) diff --git a/DB.md b/DB.md index cd7f505..3333bb5 100644 --- a/DB.md +++ b/DB.md @@ -1153,7 +1153,7 @@ SELECT DISTINCT FROM JOIN - ON + ON -- 连接查询在多表查询部分详解 WHERE GROUP BY @@ -1603,18 +1603,18 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 - - *** -## 约束操作 +## 多表操作 ### 约束分类 +#### 约束介绍 + 约束:对表中的数据进行限定,保证数据的正确性、有效性、完整性! 约束的分类: @@ -1635,7 +1635,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -### 主键约束 +#### 主键约束 * 主键约束特点: @@ -1687,7 +1687,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -### 主键自增 +#### 主键自增 主键自增约束可以为空,并自动增长。删除某条数据不影响自增的下一个数值,依然按照前一个值自增。 @@ -1733,7 +1733,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -### 唯一约束 +#### 唯一约束 唯一约束:约束不能有重复的数据 @@ -1765,7 +1765,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -### 非空约束 +#### 非空约束 * 建表时添加非空约束 @@ -1795,7 +1795,7 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -### 外键约束 +#### 外键约束 外键约束:让表和表之间产生关系,从而保证数据的准确性! @@ -1850,9 +1850,14 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 DELETE FROM USER WHERE NAME='王五'; ``` - -### 外键级联 + + +*** + + + +#### 外键级联 级联操作:当把主表中的数据进行删除或更新时,从表中有关联的数据的相应操作,包括 RESTRICT、CASCADE、SET NULL 和 NO ACTION @@ -1892,16 +1897,12 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 -## 多表操作 - ### 多表设计 #### 一对一 多表:有多张数据表,而表与表之间有一定的关联关系,通过外键约束实现,分为一对一、一对多、多对多三类 - - 举例:人和身份证 实现原则:在任意一个表建立外键,去关联另外一个表的主键 @@ -2003,27 +2004,29 @@ INSERT INTO stu_course VALUES (NULL,1,1),(NULL,1,2),(NULL,2,1),(NULL,2,2); -### 多表查询 +### 连接查询 -#### 查询格式 +#### 连接原理 -多表查询分类: +连接查询的是两张表有交集的部分数据,如果结果集中的每条记录都是两个表相互匹配的组合,则称这样的结果集为笛卡尔积 -* 内连接查询 -* 外连接查询 -* 子查询 -* 自关联查询 +查询原理:两张表分为驱动表和被驱动表,首先查询驱动表得到数据集,然后根据数据集中的每一条记录再分别到被驱动表中查找匹配,所以驱动表只需要访问一次,被驱动表要访问多次 -多表查询格式:(笛卡儿积) +MySQL 将查询驱动表后得到的记录成为驱动表的扇出,连接查询的成本:单次访问驱动表的成本 + 扇出值 * 单次访问被驱动表的成本,优化器会选择成本最小的表连接顺序(确定谁是驱动表,谁是被驱动表)生成执行计划,进行连接查询,优化方式: -```mysql -SELECT - 列名列表 -FROM - 表名列表 -WHERE - 条件... -``` +* 减少驱动表的扇出 +* 降低访问被驱动表的成本 + +MySQL 提出了一种空间换时间的优化方式,基于块的循环连接,执行连接查询前申请一块固定大小的内存作为连接缓冲区 Join Buffer,先把若干条驱动表中的扇出暂存在缓冲区,每一条被驱动表中的记录一次性的与 Buffer 中多条记录进行匹配(可能是一对多),因为是在内存中完成,所以速度快,并且降低了 I/O 成本。 + +Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 256 KB + +在成本分析时,对于很多张表的连接查询,连接顺序有非常多,MySQL 如果挨着进行遍历计算成本,会消耗很多资源 + +* 提前结束某种连接顺序的成本评估:维护一个全局变量记录当前成本最小的连接方式,如果一种顺序只计算了一部分就已经超过了最小成本,可以提前结束计算 +* 系统变量 optimizer_search_depth:如果连接表的个数小于该变量,就继续穷举分析每一种连接数量,反之只对数量与 depth 值相同的表进行分析,该值越大成本分析的越精确 + +* 系统变量 optimizer_prune_level:控制启发式规则的启用,这些规则就是根据以往经验指定的,不满足规则的连接顺序不分析成本 @@ -2031,9 +2034,11 @@ WHERE -#### 内连接 +#### 内外连接 + +##### 内连接 -查询原理:内连接查询的是两张表有交集的部分数据,分为驱动表和被驱动表,首先查询驱动表得到结果集,然后根据结果集中的每一条记录都分别到被驱动表中查找匹配 +内连接查询,若驱动表中的记录在被驱动表中找不到匹配的记录时,则该记录不会加到最后的结果集 * 显式内连接 @@ -2054,15 +2059,19 @@ WHERE -#### 外连接 +##### 外连接 -* 左外连接:查询左表的全部数据,和左右两张表有交集部分的数据 +外连接查询,若驱动表中的记录在被驱动表中找不到匹配的记录时,则该记录也会加到最后的结果集,只是对于被驱动表中**不匹配过滤条件**的记录,各个字段使用 NULL 填充 + +应用实例:差学生成绩,也想查出缺考的人的成绩 + +* 左外连接:选择左侧的表为驱动表,查询左表的全部数据,和左右两张表有交集部分的数据 ```mysql SELECT 列名 FROM 表名1 LEFT [OUTER] JOIN 表名2 ON 条件; ``` -* 右外连接:查询右表的全部数据,和左右两张表有交集部分的数据 +* 右外连接:选择右侧的表为驱动表,查询右表的全部数据,和左右两张表有交集部分的数据 ```mysql SELECT 列名 FROM 表名1 RIGHT [OUTER] JOIN 表名2 ON 条件; @@ -2077,45 +2086,9 @@ WHERE -#### 子查询 - -子查询概念:查询语句中嵌套了查询语句,**将嵌套查询称为子查询** - -* 结果是单行单列:可以将查询的结果作为另一条语句的查询条件,使用运算符判断 - - ```mysql - SELECT 列名 FROM 表名 WHERE 列名=(SELECT 列名/聚合函数(列名) FROM 表名 [WHERE 条件]); - ``` - -* 结果是多行单列:可以作为条件,使用运算符in或not in进行判断 - - ```mysql - SELECT 列名 FROM 表名 WHERE 列名 [NOT] IN (SELECT 列名 FROM 表名 [WHERE 条件]); - ``` - -* 结果是多行多列:查询的结果可以作为一张虚拟表参与查询 - - ```mysql - SELECT 列名 FROM 表名 [别名],(SELECT 列名 FROM 表名 [WHERE 条件]) [别名] [WHERE 条件]; - - -- 查询订单表orderlist中id大于4的订单信息和所属用户USER信息 - SELECT - * - FROM - USER u, - (SELECT * FROM orderlist WHERE id>4) o - WHERE - u.id=o.uid; - ``` - - - - -*** - -#### 自关联 +#### 关联查询 自关联查询:同一张表中有数据关联,可以多次查询这同一个表 @@ -2182,15 +2155,53 @@ WHERE 1009 宋江 NULL NULL NULL ``` + + + +*** + + + +### 嵌套查询 + +子查询概念:查询语句中嵌套了查询语句,**将嵌套查询称为子查询** + +* 结果是单行单列:可以将查询的结果作为另一条语句的查询条件,使用运算符判断 + + ```mysql + SELECT 列名 FROM 表名 WHERE 列名=(SELECT 列名/聚合函数(列名) FROM 表名 [WHERE 条件]); + ``` + +* 结果是多行单列:可以作为条件,使用运算符in或not in进行判断 + + ```mysql + SELECT 列名 FROM 表名 WHERE 列名 [NOT] IN (SELECT 列名 FROM 表名 [WHERE 条件]); + ``` + +* 结果是多行多列:查询的结果可以作为一张虚拟表参与查询 + + ```mysql + SELECT 列名 FROM 表名 [别名],(SELECT 列名 FROM 表名 [WHERE 条件]) [别名] [WHERE 条件]; + -- 查询订单表orderlist中id大于4的订单信息和所属用户USER信息 + SELECT + * + FROM + USER u, + (SELECT * FROM orderlist WHERE id>4) o + WHERE + u.id=o.uid; + ``` + + *** -### 多表练习 +### 查询练习 -#### 数据准备 +数据准备: ```mysql -- 创建db4数据库 @@ -2241,11 +2252,11 @@ CREATE TABLE us_pro( -#### 数据查询 +**数据查询:** 1. 查询用户的编号、姓名、年龄、订单编号。 分析: - 数据:用户的编号、姓名、年龄在user表,订单编号在orderlist表 + 数据:用户的编号、姓名、年龄在 user 表,订单编号在 orderlist 表 条件:user.id = orderlist.uid ```mysql From 210c98f4859f7821119fe2982b8479cc32bffb19 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 28 Nov 2021 23:20:58 +0800 Subject: [PATCH 034/122] Update Java Notes --- DB.md | 2 +- Java.md | 8 +++--- Prog.md | 8 +++--- SSM.md | 76 +++++++++++++++------------------------------------------ 4 files changed, 28 insertions(+), 66 deletions(-) diff --git a/DB.md b/DB.md index 3333bb5..7569aaf 100644 --- a/DB.md +++ b/DB.md @@ -2432,7 +2432,7 @@ CREATE TABLE us_pro( 工作原理: * 自动提交模式下,如果没有 start transaction 显式地开始一个事务,那么**每个 SQL 语句都会被当做一个事务执行提交操作** - * 手动提交模式下,所有的 SQL 语句都在一个事务中,直到执行了 commit 或 rollback,该事务结束的同时开启另外一个事务 + * 手动提交模式下,所有的 SQL 语句都在一个事务中,直到执行了 commit 或 rollback * 存在一些特殊的命令,在事务中执行了这些命令会马上强制执行 COMMIT 提交事务,如 DDL 语句 (create/drop/alter/table)、lock tables 语句等 diff --git a/Java.md b/Java.md index 2ac7b25..7d9523a 100644 --- a/Java.md +++ b/Java.md @@ -4415,11 +4415,11 @@ TreeSet 集合自排序的方式: * 直接为**对象的类**实现比较器规则接口 Comparable,重写比较方法: 方法:`public int compareTo(Employee o): this 是比较者, o 是被比较者` - + * 比较者大于被比较者,返回正数(升序) * 比较者小于被比较者,返回负数 * 比较者等于被比较者,返回 0 - + * 直接为**集合**设置比较器 Comparator 对象,重写比较方法: 方法:`public int compare(Employee o1, Employee o2): o1 比较者, o2 被比较者` @@ -4489,7 +4489,7 @@ PriorityQueue 是优先级队列,底层存储结构为 Object[],默认实现 常用 API: -* `public boolean offer(E e)`:将指定的元素插入到此优先级队列中**尾部** +* `public boolean offer(E e)`:将指定的元素插入到此优先级队列的**尾部** * `public E poll() `:检索并删除此队列的**头元素**,如果此队列为空,则返回 null * `public E peek()`:检索但不删除此队列的头,如果此队列为空,则返回 null * `public boolean remove(Object o)`:从该队列中删除指定元素(如果存在),删除元素 e 使用 o.equals(e) 比较,如果队列包含多个这样的元素,删除第一个 @@ -6671,7 +6671,7 @@ public class MethodDemo{ public static void main(String[] args) { String[] strs = new String[]{"James", "AA", "John", "Patricia","Dlei" , "Robert","Boom", "Cao" ,"black" , - "Michael", "Linda","cao","after","sBBB"}; + "Michael", "Linda","cao","after","sa"}; // public static void sort(T[] a, Comparator c) // 需求:按照元素的首字符(忽略大小写)升序排序!!! diff --git a/Prog.md b/Prog.md index 82e14e6..0109c33 100644 --- a/Prog.md +++ b/Prog.md @@ -3812,8 +3812,8 @@ JDK8 以前:每个 ThreadLocal 都创建一个 Map,然后用线程作为 Map JDK8 以后:每个 Thread 维护一个 ThreadLocalMap,这个 Map 的 key 是 ThreadLocal 实例本身,value 是真正要存储的值 -* 每个 Thread 线程内部都有一个 Map (ThreadLocalMap) -* Map 里面存储 ThreadLocal 对象(key)和线程的变量副本(value) +* **每个 Thread 线程内部都有一个 Map (ThreadLocalMap)** +* Map 里面存储 ThreadLocal 对象(key)和线程的私有变量(value) * Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值 * 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成副本的隔离,互不干扰 @@ -4589,8 +4589,8 @@ java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:**FIFO | 方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 | | ---------------- | --------- | -------- | ------ | ------------------ | -| 插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) | -| 移除 | remove() | poll() | take() | poll(time,unit) | +| 插入(尾) | add(e) | offer(e) | put(e) | offer(e,time,unit) | +| 移除(头) | remove() | poll() | take() | poll(time,unit) | | 检查(队首元素) | element() | peek() | 不可用 | 不可用 | * 抛出异常组: diff --git a/SSM.md b/SSM.md index 740fc8f..8702630 100644 --- a/SSM.md +++ b/SSM.md @@ -6755,8 +6755,13 @@ MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ( **支持当前事务**的情况: * TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则**加入该事务**;如果当前没有事务,则创建一个新的事务 - * 内外层是相同的事务 - * 在 aMethod 或者在 bMethod 内的任何地方出现异常,事务都会被回滚 + * 内外层是相同的事务,在 aMethod 或者在 bMethod 内的任何地方出现异常,事务都会被回滚 + * 工作流程: + * 线程执行到 serviceA.aMethod() 时,其实是执行的代理 serviceA 对象的 aMethod + * 首先执行事务增强器逻辑(环绕增强),提取事务标签属性,检查当前线程是否绑定 connection 数据库连接资源,没有就调用 datasource.getConnection(),设置事务提交为手动提交 autocommit(false) + * 执行其他增强器的逻辑,然后调用 target 的目标方法 aMethod() 方法,进入 serviceB 的逻辑 + * serviceB 也是先执行事务增强器的逻辑,提取事务标签属性,但此时会检查到线程绑定了 connection,检查注解的传播属性,所以调用 DataSourceUtils.getConnection(datasource) 共享该连接资源,执行完相关的增强和 SQL 后,发现事务并不是当前方法开启的,可以直接返回上层 + * serviceA.aMethod() 继续执行,执行完增强后进行提交事务或回滚事务 * TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则**加入该事务**;如果当前没有事务,则以非事务的方式继续运行 * TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则**加入该事务**;如果当前没有事务,则抛出异常 @@ -9144,6 +9149,18 @@ retVal = invocation.proceed():**拦截器链驱动方法** +*** + + + +### 事务 + + + + + + + *** @@ -9239,62 +9256,7 @@ AutowiredAnnotationBeanPostProcessor 间接实现 InstantiationAwareBeanPostProc -*** - - - -#### Transaction - -@EnableTransactionManagement 导入 TransactionManagementConfigurationSelector,该类给 Spring 容器中两个组件: - -* AdviceMode 为 PROXY:导入 AutoProxyRegistrar 和 ProxyTransactionManagementConfiguration(默认) -* AdviceMode 为 ASPECTJ:导入 AspectJTransactionManagementConfiguration(与声明式事务无关) - -AutoProxyRegistrar:给容器中注册 InfrastructureAdvisorAutoProxyCreator,**利用后置处理器机制拦截 bean 以后包装并返回一个代理对象**,代理对象中保存所有的拦截器,利用拦截器的链式机制依次进入每一个拦截器中进行拦截执行(就是 AOP 原理) - -ProxyTransactionManagementConfiguration:是一个 Spring 的事务配置类,注册了三个 Bean: -* BeanFactoryTransactionAttributeSourceAdvisor:事务增强器,利用注解 @Bean 把该类注入到容器中,该增强器有两个字段: - -* TransactionAttributeSource:解析事务注解的相关信息,比如 @Transactional 注解,该类的真实类型是 AnnotationTransactionAttributeSource,构造方法中注册了三个**注解解析器**,解析三种类型的事务注解 Spring、JTA、Ejb3 - -* TransactionInterceptor:**事务拦截器**,代理对象执行拦截器方法时,会调用 TransactionInterceptor 的 invoke 方法,底层调用TransactionAspectSupport.invokeWithinTransaction(),通过 PlatformTransactionManager 控制着事务的提交和回滚,所以事务的底层原理就是通过 AOP 动态织入,进行事务开启和提交 - - ```java - // 创建平台事务管理器对象 - final PlatformTransactionManager tm = determineTransactionManager(txAttr); - // 开启事务 - TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); - // 执行目标方法(方法引用方式,invocation::proceed,还是调用 proceed) - retVal = invocation.proceedWithInvocation(); - // 提交或者回滚事务 - commitTransactionAfterReturning(txInfo); - ``` - - `createTransactionIfNecessary(tm, txAttr, joinpointIdentification)`: - - * `status = tm.getTransaction(txAttr)`:获取事务状态,开启事务 - - * `doBegin`: **调用 Connection 的 setAutoCommit(false) 开启事务**,就是 JDBC 原生的方式 - - * `prepareTransactionInfo(tm, txAttr, joinpointIdentification, status)`:准备事务信息 - - * `bindToThread() `:利用 ThreadLocal **把当前事务绑定到当前线程**(一个线程对应一个事务) - - 策略模式(Strategy Pattern):使用不同策略的对象实现不同的行为方式,策略对象的变化导致行为的变化,事务也是这种模式,每个事务对应一个新的 connection 对象 - - `commitTransactionAfterReturning(txInfo)`: - - * `txInfo.getTransactionManager().commit(txInfo.getTransactionStatus())`:通过平台事务管理器操作事务 - - * `processRollback(defStatus, false)`:回滚事务,和提交逻辑一样 - - * `processCommit(defStatus)`:提交事务,调用 doCommit(status) - - * `Connection con = txObject.getConnectionHolder().getConnection()`:获取当前线程的连接对象 - * `con.commit()`:事务提交,JDBC 原生的方式 - - From 8fe98eefc6ea30168c75156a9bf6b69223458375 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 30 Nov 2021 00:38:47 +0800 Subject: [PATCH 035/122] Update Java Notes --- SSM.md | 706 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 706 insertions(+) diff --git a/SSM.md b/SSM.md index 8702630..01da621 100644 --- a/SSM.md +++ b/SSM.md @@ -6778,6 +6778,7 @@ MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ( * TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED * 如果 ServiceB 异常回滚,可以通过 try-catch 机制执行 ServiceC * 如果 ServiceB 提交, ServiceA 可以根据具体的配置决定是 commit 还是 rollback + * **应用场景**:在查询数据的时候要向数据库中存储一些日志,系统不希望存日志的行为影响到主逻辑,可以使用该传播 requied:必须的、supports:支持的、mandatory:强制的、nested:嵌套的 @@ -9155,7 +9156,712 @@ retVal = invocation.proceed():**拦截器链驱动方法** ### 事务 +#### 解析方法 +##### 标签解析 + +```xml + +``` + +容器启动时会根据注解注册对应的解析器: + +```java +public class TxNamespaceHandler extends NamespaceHandlerSupport { + public void init() { + registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser()); + // 注册解析器 + registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser()); + registerBeanDefinitionParser("jta-transaction-manager", new JtaTransactionManagerBeanDefinitionParser()); + } +} +protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) { + this.parsers.put(elementName, parser); +} +``` + +获取对应的解析器 NamespaceHandlerSupport#findParserForElement: + +```java +private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) { + String localName = parserContext.getDelegate().getLocalName(element); + // 获取对应的解析器 + BeanDefinitionParser parser = this.parsers.get(localName); + // ... + return parser; +} +``` + +调用解析器的方法对 XML 文件进行解析: + +```java +public BeanDefinition parse(Element element, ParserContext parserContext) { + // 向Spring容器注册了一个 BD -> TransactionalEventListenerFactory.class + registerTransactionalEventListenerFactory(parserContext); + String mode = element.getAttribute("mode"); + if ("aspectj".equals(mode)) { + // mode="aspectj" + registerTransactionAspect(element, parserContext); + if (ClassUtils.isPresent("javax.transaction.Transactional", getClass().getClassLoader())) { + registerJtaTransactionAspect(element, parserContext); + } + } + else { + // mode="proxy",默认逻辑,不配置 mode 时 + // 用来向容器中注入一些 BeanDefinition,包括事务增强器、事务拦截器、注解解析器 + AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext); + } + return null; +} +``` + + + + + +**** + + + +##### 注解解析 + +@EnableTransactionManagement 导入 TransactionManagementConfigurationSelector,该类给 Spring 容器中两个组件: + +```java +protected String[] selectImports(AdviceMode adviceMode) { + switch (adviceMode) { + // 导入 AutoProxyRegistrar 和 ProxyTransactionManagementConfiguration(默认) + case PROXY: + return new String[] {AutoProxyRegistrar.class.getName(), + ProxyTransactionManagementConfiguration.class.getName()}; + // 导入 AspectJTransactionManagementConfiguration(与声明式事务无关) + case ASPECTJ: + return new String[] {determineTransactionAspectClass()}; + default: + return null; + } +} +``` + +AutoProxyRegistrar:给容器中注册 InfrastructureAdvisorAutoProxyCreator,**利用后置处理器机制拦截 bean 以后包装并返回一个代理对象**,代理对象中保存所有的拦截器,利用拦截器的链式机制依次进入每一个拦截器中进行拦截执行(就是 AOP 原理) + +ProxyTransactionManagementConfiguration:是一个 Spring 的事务配置类,注册了三个 Bean: + +* BeanFactoryTransactionAttributeSourceAdvisor:事务驱动,利用注解 @Bean 把该类注入到容器中,该增强器有两个字段: +* TransactionAttributeSource:解析事务注解的相关信息,真实类型是 AnnotationTransactionAttributeSource,构造方法中注册了三个**注解解析器**,解析 Spring、JTA、Ejb3 三种类型的事务注解 +* TransactionInterceptor:**事务拦截器**,代理对象执行拦截器方法时,调用 TransactionInterceptor 的 invoke 方法,底层调用TransactionAspectSupport.invokeWithinTransaction(),通过 PlatformTransactionManager 控制着事务的提交和回滚,所以事务的底层原理就是通过 AOP 动态织入,进行事务开启和提交 + +注解解析器 SpringTransactionAnnotationParser **解析 @Transactional 注解**: + +```java +protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) { + RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute(); + // 从注解信息中获取传播行为 + Propagation propagation = attributes.getEnum("propagation"); + rbta.setPropagationBehavior(propagation.value()); + // 获取隔离界别 + Isolation isolation = attributes.getEnum("isolation"); + rbta.setIsolationLevel(isolation.value()); + rbta.setTimeout(attributes.getNumber("timeout").intValue()); + // 从注解信息中获取 readOnly 参数 + rbta.setReadOnly(attributes.getBoolean("readOnly")); + // 从注解信息中获取 value 信息并且设置 qualifier,表示当前事务指定使用的【事务管理器】 + rbta.setQualifier(attributes.getString("value")); + // 【存放的是 rollback 条件】,回滚规则放在这个集合 + List rollbackRules = new ArrayList<>(); + // 表示事务碰到哪些指定的异常才进行回滚,不指定的话默认是 RuntimeException/Error 非检查型异常菜回滚 + for (Class rbRule : attributes.getClassArray("rollbackFor")) { + rollbackRules.add(new RollbackRuleAttribute(rbRule)); + } + // 与 rollbackFor 功能相同 + for (String rbRule : attributes.getStringArray("rollbackForClassName")) { + rollbackRules.add(new RollbackRuleAttribute(rbRule)); + } + // 表示事务碰到指定的 exception 实现对象不进行回滚,否则碰到其他的class就进行回滚 + for (Class rbRule : attributes.getClassArray("noRollbackFor")) { + rollbackRules.add(new NoRollbackRuleAttribute(rbRule)); + } + for (String rbRule : attributes.getStringArray("noRollbackForClassName")) { + rollbackRules.add(new NoRollbackRuleAttribute(rbRule)); + } + // 设置回滚规则 + rbta.setRollbackRules(rollbackRules); + + return rbta; +} +``` + + + + + +**** + + + + + +#### 驱动方法 + +TransactionInterceptor 事务拦截器的核心驱动方法: + +```java +public Object invoke(MethodInvocation invocation) throws Throwable { + // targetClass 是需要被事务增强器增强的目标类,invocation.getThis() → 目标对象 → 目标类 + Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); + // 参数一是目标方法,参数二是目标类,参数三是方法引用,用来触发驱动方法 + return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed); +} + +protected Object invokeWithinTransaction(Method method, @Nullable Class targetClass, + final InvocationCallback invocation) throws Throwable { + + // 事务属性源信息 + TransactionAttributeSource tas = getTransactionAttributeSource(); + // 提取 @Transactional 注解信息,txAttr 是注解信息的承载对象 + final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); + // 获取 Spring 配置的事务管理器 + // 首先会检查是否通过XML或注解配置 qualifier,没有就尝试去容器获取,一般情况下为 DatasourceTransactionManager + final PlatformTransactionManager tm = determineTransactionManager(txAttr); + // 权限定类名.方法名,该值用来当做事务名称使用 + final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); + + // 条件成立说明是【声明式事务】 + if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { + // 用来【开启事务】 + TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); + + Object retVal; + try { + // This is an 【around advice】: Invoke the next interceptor in the chain. + // 环绕通知,执行目标方法(方法引用方式,invocation::proceed,还是调用 proceed) + retVal = invocation.proceedWithInvocation(); + } + catch (Throwable ex) { + // 执行业务代码时抛出异常,执行回滚逻辑 + completeTransactionAfterThrowing(txInfo, ex); + throw ex; + } + finally { + // 清理事务的信息 + cleanupTransactionInfo(txInfo); + } + // 提交事务的入口 + commitTransactionAfterReturning(txInfo); + return retVal; + } + else { + // 编程式事务,省略 + } +} +``` + + + +*** + + + +#### 开启事务 + +##### 事务绑定 + +创建事务的方法: + +```java +protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm, + @Nullable TransactionAttribute txAttr, + final String joinpointIdentification) { + + // If no name specified, apply method identification as transaction name. + if (txAttr != null && txAttr.getName() == null) { + // 事务的名称: 类的权限定名.方法名 + txAttr = new DelegatingTransactionAttribute(txAttr) { + @Override + public String getName() { + return joinpointIdentification; + } + }; + } + TransactionStatus status = null; + if (txAttr != null) { + if (tm != null) { + // 通过事务管理器根据事务属性创建事务状态对象,事务状态对象一般情况下包装着 事务对象,当然也有可能是null + // 方法上的注解为 @Transactional(propagation = NOT_SUPPORTED || propagation = NEVER) 时 + // 【下一小节详解】 + status = tm.getTransaction(txAttr); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Skipping transactional joinpoint [" + joinpointIdentification + + "] because no transaction manager has been configured"); + } + } + } + // 包装成一个上层的事务上下文对象 + return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); +} +``` + +TransactionAspectSupport#prepareTransactionInfo:为事务的属性和状态准备一个事务信息对象 + +* `TransactionInfo txInfo = new TransactionInfo(tm, txAttr, joinpointIdentification)`:创建事务信息对象 +* `txInfo.newTransactionStatus(status)`:填充事务的状态信息 +* `txInfo.bindToThread()`:利用 ThreadLocal **把当前事务信息绑定到当前线程**,不同的事务信息会形成一个栈的结构 + * `this.oldTransactionInfo = transactionInfoHolder.get()`:获取其他事务的信息存入 oldTransactionInfo + * `transactionInfoHolder.set(this)`:将当前的事务信息设置到 ThreadLocalMap 中 + + + +*** + + + +##### 事务创建 + +```java +public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException { + // 获取事务的对象 + Object transaction = doGetTransaction(); + boolean debugEnabled = logger.isDebugEnabled(); + + if (definition == null) { + // Use defaults if no transaction definition given. + definition = new DefaultTransactionDefinition(); + } + // 条件成立说明当前是事务重入的情况,事务中有 ConnectionHolder 对象 + if (isExistingTransaction(transaction)) { + // a方法开启事务,a方法内调用b方法,b方法仍然加了 @Transactional 注解,需要检查传播行为 + return handleExistingTransaction(definition, transaction, debugEnabled); + } + + // 逻辑到这说明当前线程没有连接资源,一个连接对应一个事务,没有连接就相当于没有开启事务 + // 检查事务的延迟属性 + if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) { + throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout()); + } + + // 传播行为是 MANDATORY,没有事务就抛出异常 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { + throw new IllegalTransactionStateException(); + } + // 需要开启事务的传播行为 + else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || + definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || + definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + // 什么也没挂起,因为线程并没有绑定事务 + SuspendedResourcesHolder suspendedResources = suspend(null); + try { + // 是否支持同步线程事务,一般是 true + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + // 新建一个事务状态信息 + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); + // 【启动事务】 + doBegin(transaction, definition); + // 设置线程上下文变量,方便程序运行期间获取当前事务的一些核心的属性,initSynchronization() 启动同步 + prepareSynchronization(status, definition); + return status; + } + catch (RuntimeException | Error ex) { + // 恢复现场 + resume(null, suspendedResources); + throw ex; + } + } + // 不支持事务的传播行为 + else { + // Create "empty" transaction: no actual transaction, but potentially synchronization. + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + // 创建事务状态对象 + // 参数2 transaction 是 null 说明当前事务状态是未手动开启事,线程上未绑定任何的连接资源,业务程序执行时需要先去 datasource 获取的 conn,是自动提交事务的,不需要 Spring 再提交事务 + // 参数6 suspendedResources 是 null 说明当前事务状态未挂起任何事,当前这个事务执行到后置处理时不需要恢复现场 + return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null); + } +} +``` + +DataSourceTransactionManager#doGetTransaction:真正获取事务的方法 + +* `DataSourceTransactionObject txObject = new DataSourceTransactionObject()`:**创建事务对象** + +* `txObject.setSavepointAllowed(isNestedAllowed())`:设置事务对象是否支持保存点,由事务管理器控制(默认不支持) + +* `ConnectionHolder conHolder = TransactionSynchronizationManager.getResource(obtainDataSource())`: + + * 从 ThreadLocal 中获取 conHolder 资源,可能拿到 null 或者不是 null + + * 是 null:举例 + + ```java + @Transaction + public void a() {...b.b()....} + ``` + + * 不是 null:执行 b 方法事务增强的前置逻辑时,可以拿到 a 放进去的 conHolder 资源 + + ```java + @Transaction + public void b() {....} + ``` + +* `txObject.setConnectionHolder(conHolder, false)`:将 ConnectionHolder 保存到事务对象内,参数二是 false 代表连接资源是上层事务共享的,不是新建的连接资源 + +* `return txObject`:返回事务的对象 + +DataSourceTransactionManager#doBegin:事务开启的逻辑 + +* `txObject = (DataSourceTransactionObject) transaction`:强转为事务对象 + +* 事务中没有数据库连接资源就要分配: + + `Connection newCon = obtainDataSource().getConnection()`:**获取 JDBC 原生的数据库连接对象** + + `txObject.setConnectionHolder(new ConnectionHolder(newCon), true)`:代表是新开启的事务,新建的连接对象 + +* `previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition)`:修改连接属性 + + * `if (definition != null && definition.isReadOnly())`:注解(或 XML)配置了只读属性,需要设置 + + * `if (..definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT)`:注解配置了隔离级别 + + `int currentIsolation = con.getTransactionIsolation()`:获取连接的隔离界别 + + `previousIsolationLevel = currentIsolation`:保存之前的隔离界别,返回该值 + + ` con.setTransactionIsolation(definition.getIsolationLevel())`:**将当前连接设置为配置的隔离界别** + +* `txObject.setPreviousIsolationLevel(previousIsolationLevel)`:将 Conn 原来的隔离级别保存到事务对象,为了释放 Conn 时重置回原状态 + +* `if (con.getAutoCommit())`:默认会成立,说明还没开启事务 + + `txObject.setMustRestoreAutoCommit(true)`:保存 Conn 原来的事务状态 + + `con.setAutoCommit(false)`:**开启事务,JDBC 原生的方式** + +* `txObject.getConnectionHolder().setTransactionActive(true)`:表示 Holder 持有的 Conn 已经手动开启事务了 + +* `TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder())`:将 ConnectionHolder 对象绑定到 ThreadLocal 内,数据源为 key,为了方便获取手动开启事务的连接对象去执行 SQL + + + +*** + + + +##### 事务重入 + +事务重入的核心处理逻辑: + +```java +private TransactionStatus handleExistingTransaction( TransactionDefinition definition, + Object transaction, boolean debugEnabled){ + // 传播行为是 PROPAGATION_NEVER,需要以非事务方式执行操作,如果当前事务存在则【抛出异常】 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) { + throw new IllegalTransactionStateException(); + } + // 传播行为是 PROPAGATION_NOT_SUPPORTED,以非事务方式运行,如果当前存在事务,则【把当前事务挂起】 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { + // 挂起事务 + Object suspendedResources = suspend(transaction); + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + // 创建一个非事务的事务状态对象返回 + return prepareTransactionStatus(definition, null, false, newSynchronization, debugEnabled, suspendedResources); + } + // 开启新事物的逻辑 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) { + // 【挂起当前事务】 + SuspendedResourcesHolder suspendedResources = suspend(transaction); + // 【开启新事物】 + } + // 传播行为是 PROPAGATION_NESTED,嵌套事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + // Spring 默认不支持内嵌事务 + // 【开启方式】: + if (!isNestedTransactionAllowed()) { + throw new NestedTransactionNotSupportedException(); + } + + if (useSavepointForNestedTransaction()) { + // 为当前方法创建一个 TransactionStatus 对象, + DefaultTransactionStatus status = + prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); + // 创建一个 JDBC 的保存点 + status.createAndHoldSavepoint(); + // 不需要使用同步,直接返回 + return status; + } + else { + // Usually only for JTA transaction,开启一个新事务 + } + } + + // Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED,【使用当前的事务】 + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); +} +``` + + + +*** + + + +##### 挂起恢复 + +AbstractPlatformTransactionManager#suspend:**挂起事务**,并获得一个上下文信息对象 + +```java +protected final SuspendedResourcesHolder suspend(@Nullable Object transaction) { + // 事务是同步状态的 + if (TransactionSynchronizationManager.isSynchronizationActive()) { + List suspendedSynchronizations = doSuspendSynchronization(); + try { + Object suspendedResources = null; + if (transaction != null) { + // do it + suspendedResources = doSuspend(transaction); + } + //将上层事务绑定在线程上下文的变量全部取出来 + //... + // 通过被挂起的资源和上层事务的上下文变量,创建一个【SuspendedResourcesHolder】返回 + return new SuspendedResourcesHolder(suspendedResources, suspendedSynchronizations, + name, readOnly, isolationLevel, wasActive); + } //... +} +protected Object doSuspend(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + // 将当前方法的事务对象 connectionHolder 属性置为 null,不和上层共享资源 + // 当前方法有可能是不开启事务或者要开启一个独立的事务 + txObject.setConnectionHolder(null); + // 解绑在线程上的事务 + return TransactionSynchronizationManager.unbindResource(obtainDataSource()); +} +``` + +AbstractPlatformTransactionManager#resume:**恢复现场**,根据挂起资源去恢复线程上下文信息 + +```java +protected final void resume(Object transaction, SuspendedResourcesHolder resourcesHolder) { + if (resourcesHolder != null) { + // 获取被挂起的事务资源 + Object suspendedResources = resourcesHolder.suspendedResources; + if (suspendedResources != null) { + //绑定上一个事务的 ConnectionHolder 到线程上下文 + doResume(transaction, suspendedResources); + } + List suspendedSynchronizations = resourcesHolder.suspendedSynchronizations; + if (suspendedSynchronizations != null) { + //.... + // 将线程上下文变量恢复为上一个事务的挂起现场 + doResumeSynchronization(suspendedSynchronizations); + } + } +} +protected void doResume(@Nullable Object transaction, Object suspendedResources) { + // doSuspend 的逆动作 + TransactionSynchronizationManager.bindResource(obtainDataSource(), suspendedResources); +} +``` + + + + + +*** + + + +#### 提交回滚 + +##### 回滚方式 + +```java +protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) { + // 事务状态信息不为空进入逻辑 + if (txInfo != null && txInfo.getTransactionStatus() != null) { + // 条件二成立 说明目标方法抛出的异常需要回滚事务 + if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) { + try { + // 事务管理器的回滚方法 + txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); + } + catch (TransactionSystemException ex2) {} + } + else { + // 执行到这里,说明当前事务虽然抛出了异常,但是该异常并不会导致整个事务回滚 + try { + // 提交事务 + txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); + } + catch (TransactionSystemException ex2) {} + } + } +} +public boolean rollbackOn(Throwable ex) { + // 继承自 RuntimeException 或 error 的是【非检查型异常】,才会归滚事务 + // 如果配置了其他回滚错误,会获取到回滚规则 rollbackRules 进行判断 + return (ex instanceof RuntimeException || ex instanceof Error); +} +``` + +```java +public final void rollback(TransactionStatus status) throws TransactionException { + // 事务已经完成不需要回滚 + if (status.isCompleted()) { + throw new IllegalTransactionStateException(); + } + DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status; + // 开始回滚事务 + processRollback(defStatus, false); +} +``` + +AbstractPlatformTransactionManager#processRollback:事务回滚 + +* `triggerBeforeCompletion(status)`:用来做扩展逻辑,回滚前的前置处理 + +* `if (status.hasSavepoint())`:条件成立说明当前事务是一个**内嵌事务**,当前方法只是复用了上层事务的一个内嵌事务 + + `status.rollbackToHeldSavepoint()`:内嵌事务加入事务时会创建一个保存点,此时恢复至保存点 + +* `if (status.isNewTransaction())`:说明事务是当前连接开启的,需要去回滚事务 + + `doRollback(status)`:真正的的回滚函数 + + * `DataSourceTransactionObject txObject = status.getTransaction()`:获取事务对象 + * `Connection con = txObject.getConnectionHolder().getConnection()`:获取连接对象 + * `con.rollback()`:**JDBC 的方式回滚事务** + +* `else`:当前方法是共享的上层的事务,和上层使用同一个 Conn 资源,**共享的事务不能直接回滚,应该交给上层处理** + + `doSetRollbackOnly(status)`:设置 con.rollbackOnly = true,线程回到上层事务 commit 时会检查该字段,然后执行回滚操作 + +* `triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK)`:回滚的后置处理 + +* `cleanupAfterCompletion(status)`:清理和恢复现场 + + + +*** + + + +##### 提交方式 + +```java +protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) { + if (txInfo != null && txInfo.getTransactionStatus() != null) { + // 事务管理器的提交方法 + txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); + } +} +``` + +```java +public final void commit(TransactionStatus status) throws TransactionException { + // 已经完成的事务不需要提交了 + if (status.isCompleted()) { + throw new IllegalTransactionStateException(); + } + DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status; + // 条件成立说明是当前的业务强制回滚 + if (defStatus.isLocalRollbackOnly()) { + // 回滚逻辑, + processRollback(defStatus, false); + return; + } + // 成立说明共享当前事务的【下层事务逻辑出错,需要回滚】 + if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) { + // 如果当前事务还是事务重入,会继续抛给上层,最上层事务会进行真实的事务回滚操作 + processRollback(defStatus, true); + return; + } + // 执行提交 + processCommit(defStatus); +} +``` + +AbstractPlatformTransactionManager#processCommit:事务提交 + +* `prepareForCommit(status)`:前置处理 + +* `if (status.hasSavepoint())`:条件成立说明当前事务是一个**内嵌事务**,只是复用了上层事务 + + `status.releaseHeldSavepoint()`:清理保存点,因为没有发生任何异常,所以保存点没有存在的意义了 + +* `if (status.isNewTransaction())`:说明事务是归属于当前连接的,需要去提交事务 + + `doCommit(status)`:真正的提交函数 + + * `Connection con = txObject.getConnectionHolder().getConnection()`:获取连接对象 + * `con.commit()`:**JDBC 的方式提交事务** + +* `doRollbackOnCommitException(status, ex)`:**提交事务出错后进行回滚** + +* ` cleanupAfterCompletion(status)`:清理和恢复现场 + + + +*** + + + +##### 清理现场 + +恢复上层事务: + +```java +protected void cleanupTransactionInfo(@Nullable TransactionInfo txInfo) { + if (txInfo != null) { + // 从当前线程的 ThreadLocal 获取上层的事务信息,将当前事务出栈,继续执行上层事务 + txInfo.restoreThreadLocalStatus(); + } +} +private void restoreThreadLocalStatus() { + // Use stack to restore old transaction TransactionInfo. + transactionInfoHolder.set(this.oldTransactionInfo); +} +``` + +当前层级事务结束时的清理: + +```java +private void cleanupAfterCompletion(DefaultTransactionStatus status) { + // 设置当前方法的事务状态为完成状态 + status.setCompleted(); + if (status.isNewSynchronization()) { + // 清理线程上下文变量以及扩展点注册的 sync + TransactionSynchronizationManager.clear(); + } + // 事务是当前线程开启的 + if (status.isNewTransaction()) { + // 解绑资源 + doCleanupAfterCompletion(status.getTransaction()); + } + // 条件成立说明当前事务执行的时候,【挂起了一个上层的事务】 + if (status.getSuspendedResources() != null) { + Object transaction = (status.hasTransaction() ? status.getTransaction() : null); + // 恢复上层事务现场 + resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources()); + } +} +``` + +DataSourceTransactionManager#doCleanupAfterCompletion:清理工作 + +* `TransactionSynchronizationManager.unbindResource(obtainDataSource())`:解绑数据库资源 + +* `if (txObject.isMustRestoreAutoCommit())`:是否恢复连接,Conn 归还到 DataSource**,归还前需要恢复到申请时的状态** + + `con.setAutoCommit(true)`:恢复链接为自动提交 + +* `DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel())`:恢复隔离级别 + +* `DataSourceUtils.releaseConnection(con, this.dataSource)`:将连接归还给数据库连接池 + +* `txObject.getConnectionHolder().clear()`:清理 ConnectionHolder 资源 From a1aa654a2be69b9ba8c51edcb12b1aa5ee467753 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 1 Dec 2021 23:43:03 +0800 Subject: [PATCH 036/122] Update Java Notes --- DB.md | 104 +++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 70 insertions(+), 34 deletions(-) diff --git a/DB.md b/DB.md index 7569aaf..df7af85 100644 --- a/DB.md +++ b/DB.md @@ -958,11 +958,12 @@ DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临 | DOUBLE | 小数类型 | | DATE | 日期,只包含年月日:yyyy-MM-dd | | DATETIME | 日期,包含年月日时分秒:yyyy-MM-dd HH:mm:ss | - | TIMESTAMP | 时间戳类型,包含年月日时分秒:yyyy-MM-dd HH:mm:ss
如果不给这个字段赋值或赋值为null,则默认使用当前的系统时间 | - | VARCHAR | 字符串
name varchar(20):姓名最大20个字符:zhangsan8个字符,张三2个字符 | - + | TIMESTAMP | 时间戳类型,包含年月日时分秒:yyyy-MM-dd HH:mm:ss
如果不给这个字段赋值或赋值为 NULL,则默认使用当前的系统时间 | + | CHAR | 字符串,定长类型 | + | VARCHAR | 字符串,**变长类型**
name varchar(20) 代表姓名最大 20 个字符:zhangsan 8 个字符,张三 2 个字符 | + `INT(n)`:n 代表位数 - + * 3:int(9)显示结果为 000000010 * 3:int(3)显示结果为 010 @@ -1286,8 +1287,8 @@ LIMIT | AND 或 && | 并且 | | OR 或 \|\| | 或者 | | NOT 或 ! | 非,不是 | - | UNION | 对两个结果集进行并集操作,不包括重复行,同时进行默认规则的排序 | - | UNION ALL | 对两个结果集进行并集操作,包括重复行,不进行排序 | + | UNION | 对两个结果集进行并集操作并进行去重,同时进行默认规则的排序 | + | UNION ALL | 对两个结果集进行并集操作不进行去重,不进行排序 | * 例如: @@ -2164,7 +2165,11 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 ### 嵌套查询 -子查询概念:查询语句中嵌套了查询语句,**将嵌套查询称为子查询** +#### 查询分类 + +查询语句中嵌套了查询语句,**将嵌套查询称为子查询**,FROM 子句后面的子查询的结果集称为派生表 + +根据结果分类: * 结果是单行单列:可以将查询的结果作为另一条语句的查询条件,使用运算符判断 @@ -2172,7 +2177,7 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 SELECT 列名 FROM 表名 WHERE 列名=(SELECT 列名/聚合函数(列名) FROM 表名 [WHERE 条件]); ``` -* 结果是多行单列:可以作为条件,使用运算符in或not in进行判断 +* 结果是多行单列:可以作为条件,使用运算符 IN 或 NOT IN 进行判断 ```mysql SELECT 列名 FROM 表名 WHERE 列名 [NOT] IN (SELECT 列名 FROM 表名 [WHERE 条件]); @@ -2193,6 +2198,26 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 u.id=o.uid; ``` +相关性分类: + +* 不相关子查询:子查询不依赖外层查询的值,可以单独运行出结果 +* 相关子查询:子查询的执行需要依赖外层查询的值 + + + +**** + + + +#### 查询优化 + +不相关子查询的结果集会被写入一个临时表,并且在写入时去重,该过程称为物化,存储结果集的临时表称为物化表 + +系统变量 tmp_table_size 或者 max_heap_table_size 为表的最值 + +* 小于系统变量时,内存中可以保存,会为建立基于内存的 MEMORY 存储引擎的临时表,并建立哈希索引 +* 大于任意一个系统变量时,物化表会使用基于磁盘的存储引擎来保存结果集中的记录,索引类型为 B+ 树, + *** @@ -4717,7 +4742,7 @@ CREATE INDEX idx_area ON table_name(area(7)); SELECT * FROM table_test WHERE key1 = 'a' AND key3 = 'b'; # key1 和 key3 列都是单列索引、二级索引 ``` - 从不同索引中扫描到的记录的 id 值取交集(相同 id),然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 + 从不同索引中扫描到的记录的 id 值取**交集**(相同 id),然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 * Union 索引合并: @@ -4725,7 +4750,7 @@ CREATE INDEX idx_area ON table_name(area(7)); SELECT * FROM table_test WHERE key1 = 'a' OR key3 = 'b'; ``` - 从不同索引中扫描到的记录的 id 值取并集,然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 + 从不同索引中扫描到的记录的 id 值取**并集**,然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 * Sort-Union 索引合并 @@ -4871,16 +4896,16 @@ EXPLAIN SELECT * FROM table_1 WHERE id = 1; | 字段 | 含义 | | ------------- | ------------------------------------------------------------ | -| id | select查询的序列号,表示查询中执行select子句或操作表的顺序 | +| id | SELECT 的序列号 | | select_type | 表示 SELECT 的类型 | -| table | 输出结果集的表,显示这一步所访问数据库中表名称,有时不是真实的表名字,可能是简称 | +| table | 访问数据库中表名称,有时可能是简称或者临时表名称() | | type | 表示表的连接类型 | | possible_keys | 表示查询时,可能使用的索引 | | key | 表示实际使用的索引 | | key_len | 索引字段的长度 | -| ref | 列与索引的比较,表示表的连接匹配条件,即哪些列或常量被用于查找索引列上的值 | +| ref | 表示与索引列进行等值匹配的对象,常数、某个列、函数等,type 必须在(range, const] 之间,左闭右开 | | rows | 扫描出的行数,表示 MySQL 根据表统计信息及索引选用情况,**估算**的找到所需的记录扫描的行数 | -| filtered | 按表条件过滤的行百分比 | +| filtered | 条件过滤的行百分比,单表查询没意义,用于连接查询中对驱动表的扇出进行过滤,查询优化器预测所有扇出值满足剩余查询条件的百分比,相乘以后表示多表查询中还要对被驱动执行查询的次数 | | extra | 执行情况的说明和描述 | MySQL 执行计划的局限: @@ -4907,9 +4932,9 @@ MySQL 执行计划的局限: ##### id -SQL 执行的顺序的标识,SQL 从大到小的执行 +id 代表 SQL 执行的顺序的标识,每个 SELECT 关键字对应一个唯一 id,所以在同一个 SELECT 关键字中的表的 id 都是相同的。SELECT 后的 FROM 可以跟随多个表,每个表都会对应一条记录,这些记录的 id 都是相同的, -* id 相同时,执行顺序由上至下 +* id 相同时,执行顺序由上至下。连接查询的执行计划,记录的 id 值都是相同的,出现在前面的表为驱动表,后面为被驱动表 ```mysql EXPLAIN SELECT * FROM t_role r, t_user u, user_role ur WHERE r.id = ur.role_id AND u.id = ur.user_id ; @@ -4933,6 +4958,8 @@ SQL 执行的顺序的标识,SQL 从大到小的执行 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain之id相同和不同.png) +* id 为 NULL 时代表的是临时表 + *** @@ -4946,13 +4973,18 @@ SQL 执行的顺序的标识,SQL 从大到小的执行 | select_type | 含义 | | ------------------ | ------------------------------------------------------------ | | SIMPLE | 简单的 SELECT 查询,查询中不包含子查询或者 UNION | -| PRIMARY | 查询中若包含任何复杂的子查询,最外层查询标记为该标识 | -| SUBQUERY | 在 SELECT 或 WHERE 中包含子查询,该子查询被标记为:SUBQUERY | -| DEPENDENT SUBQUERY | 在 SUBQUERY 基础上,子查询中的第一个SELECT,取决于外部的查询 | -| DERIVED | 在 FROM 列表中包含的子查询,被标记为 DERIVED(衍生),MYSQL会递归执行这些子查询,把结果放在临时表中 | -| UNION | UNION 中的第二个或后面的 SELECT 语句,则标记为UNION ; 若 UNION 包含在 FROM 子句的子查询中,外层 SELECT 将被标记为:DERIVED | -| DEPENDENT UNION | UNION 中的第二个或后面的SELECT语句,取决于外面的查询 | -| UNION RESULT | UNION 的结果,UNION 语句中第二个 SELECT 开始后面所有 SELECT | +| PRIMARY | 查询中若包含任何复杂的子查询,最外层(也就是最左侧)查询标记为该标识 | +| UNION | 对于 UNION 或者 UNION ALL 的复杂查询,除了最左侧的查询,其余的小查询都是 UNION | +| UNION RESULT | UNION 需要使用临时表进行去重,临时表的是 UNION RESULT | +| DEPENDENT UNION | 对于 UNION 或者 UNION ALL 的复杂查询,如果各个小查询都依赖外层查询,是相关子查询,除了最左侧的小查询为 DEPENDENT SUBQUERY,其余都是 DEPENDENT UNION | +| SUBQUERY | 子查询不是相关子查询,该子查询第一个 SELECT 代表的查询就是这种类型,会进行物化(该子查询只需要执行一次) | +| DEPENDENT SUBQUERY | 子查询是相关子查询,该子查询第一个 SELECT 代表的查询就是这种类型,不会物化(该子查询需要执行多次) | +| DERIVED | 在 FROM 列表中包含的子查询,被标记为 DERIVED(衍生),也就是生成物化派生表的这个子查询 | +| MATERIALIZED | 将子查询物化后与与外层进行连接查询,生成物化表的子查询 | + +子查询为 DERIVED:`SELECT * FROM (SELECT key1 FROM t1) AS derived_1 WHERE key1 > 10` + +子查询为 MATERIALIZED:`SELECT * FROM t1 WHERE key1 IN (SELECT key1 FROM t2)` @@ -4964,16 +4996,20 @@ SQL 执行的顺序的标识,SQL 从大到小的执行 对表的访问方式,表示 MySQL 在表中找到所需行的方式,又称访问类型 -| type | 含义 | -| ------ | ------------------------------------------------------------ | -| ALL | Full Table Scan,MySQL 将遍历全表以找到匹配的行,全表扫描,如果是 InnoDB 引擎是扫描聚簇索引 | -| index | Full Index Scan,index 与 ALL 区别为 index 类型只遍历索引树 | -| range | 索引范围扫描,常见于 between、<、> 等的查询 | -| ref | 非唯一性索引扫描,返回匹配某个单独值的所有记录,本质上也是一种索引访问 | -| eq_ref | 唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描 | -| const | 通过主键或者唯一索引来定位一条记录 | -| system | system 是 const 类型的特例,当查询的表只有一行的情况下,使用 system | -| NULL | MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引 | +| type | 含义 | +| --------------- | ------------------------------------------------------------ | +| ALL | 全表扫描,如果是 InnoDB 引擎是扫描聚簇索引 | +| index | 可以使用覆盖索引,但需要扫描全部索引 | +| range | 索引范围扫描,常见于 between、<、> 等的查询 | +| index_subquery | 子查询可以普通索引,则子查询的 type 为 index_subquery | +| unique_subquery | 子查询可以使用主键或唯一二级索引,则子查询的 type 为 index_subquery | +| index_merge | 索引合并 | +| ref_or_null | 非唯一性索引(普通二级索引)并且可以存储 NULL,进行等值匹配 | +| ref | 非唯一性索引与常量等值匹配 | +| eq_ref | 唯一性索引(主键或不存储 NULL 的唯一二级索引)进行等值匹配,如果二级索引是联合索引,那么所有联合的列都要进行等值匹配 | +| const | 通过主键或者唯一二级索引与常量进行等值匹配 | +| system | system 是 const 类型的特例,当查询的表只有一条记录的情况下,使用 system | +| NULL | MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引 | 从上到下,性能从差到好,一般来说需要保证查询至少达到 range 级别, 最好达到 ref @@ -4992,7 +5028,7 @@ possible_keys: key: -* 显示MySQL在查询中实际使用的索引,若没有使用索引,显示为 NULL +* 显示 MySQL 在查询中实际使用的索引,若没有使用索引,显示为 NULL * 查询中若使用了**覆盖索引**,则该索引可能出现在 key 列表,不出现在 possible_keys key_len: From c03d5e02383c20de8a997257f4eb293297154c3b Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 2 Dec 2021 23:33:07 +0800 Subject: [PATCH 037/122] Update Java Notes --- Java.md | 32 ++++++++++++++++-- SSM.md | 100 +++++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 115 insertions(+), 17 deletions(-) diff --git a/Java.md b/Java.md index 7d9523a..0ba5164 100644 --- a/Java.md +++ b/Java.md @@ -10400,7 +10400,7 @@ FullGC 同时回收新生代、老年代和方法区,只会存在一个 FullGC * 老年代空间不足: * 为了避免引起的 Full GC,应当尽量不要创建过大的对象以及数组 - * 通过 -Xmn 参数调整新生代的大小,让对象尽量在新生代被回收掉不进入老年代,可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间 + * 通过 -Xmn 参数调整新生代的大小,让对象尽量在新生代被回收掉不进入老年代,可以通过 `-XX:MaxTenuringThreshold` 调大对象进入老年代的年龄,让对象在新生代多存活一段时间 * 空间分配担保失败 @@ -10748,7 +10748,9 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 -#### 无用类 +#### 无用属性 + +##### 无用类 方法区主要回收的是无用的类 @@ -10766,6 +10768,32 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 +##### 废弃常量 + +在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该常量,说明常量 "abc" 是废弃常量,如果这时发生内存回收的话**而且有必要的话**(内存不够用),"abc" 就会被系统清理出常量池 + + + +*** + + + +##### 静态变量 + +类加载时(第一次访问),这个类中所有静态成员就会被加载到静态变量区,该区域的成员一旦创建,直到程序退出才会被回收 + +如果是静态引用类型的变量,静态变量区只存储一份对象的引用地址,真正的对象在堆内,如果要回收该对象可以设置引用为 null + + + +参考文章:https://blog.csdn.net/zhengzhb/article/details/7331354 + + + +*** + + + ### 回收算法 #### 标记清除 diff --git a/SSM.md b/SSM.md index 01da621..8bad561 100644 --- a/SSM.md +++ b/SSM.md @@ -2930,8 +2930,8 @@ Spring 优点: ### 基本概述 -- IoC(Inversion Of Control)控制反转,Spring反向控制应用程序所需要使用的外部资源 -- **Spring 控制的资源全部放置在 Spring 容器中,该容器称为 IoC 容器** +- IoC(Inversion Of Control)控制反转,Spring 反向控制应用程序所需要使用的外部资源 +- **Spring 控制的资源全部放置在 Spring 容器中,该容器称为 IoC 容器**(存放实例对象) - 官方网站:https://spring.io/ → Projects → spring-framework → LEARN → Reference Doc ![](https://gitee.com/seazean/images/raw/master/Frame/Spring-IOC介绍.png) @@ -3311,7 +3311,7 @@ IoC 和 DI 的关系:IoC 与 DI 是同一件事站在不同角度看待问题 代码实现: -* DAO层:要注入的资源 +* DAO 层:要注入的资源 ```java public interface UserDao { @@ -3327,7 +3327,7 @@ IoC 和 DI 的关系:IoC 与 DI 是同一件事站在不同角度看待问题 } ``` -* Service业务层 +* Service 业务层 ```java public interface UserService { @@ -3357,7 +3357,7 @@ IoC 和 DI 的关系:IoC 与 DI 是同一件事站在不同角度看待问题 } ``` -* 配置applicationContext.xml +* 配置 applicationContext.xml ```xml @@ -3418,7 +3418,7 @@ IoC 和 DI 的关系:IoC 与 DI 是同一件事站在不同角度看待问题 代码实现: -* DAO层:要注入的资源 +* DAO 层:要注入的资源 ```java public class UserDaoImpl implements UserDao{ @@ -4315,6 +4315,8 @@ public class UserServiceImpl implements UserService { } ``` +一个对象的执行顺序:Constructor >> @Autowired(注入属性) >> @PostConstruct(初始化逻辑) + *** @@ -4394,7 +4396,9 @@ private String username; -##### 属性填充 +##### 自动装配 + +###### 属性注入 名称:@Autowired、@Qualifier @@ -4426,7 +4430,7 @@ private UserDao userDao; -##### 属性设置 +###### 优先注入 名称:@Primary @@ -4451,7 +4455,7 @@ public class ClassName{} -##### 注解对比 +###### 注解对比 名称:@Inject、@Named、@Resource @@ -4464,11 +4468,77 @@ public class ClassName{} - type:设置注入的 bean 的类型,接收的参数为 Class 类型 -**@Autowired 和 @Resource之间的区别**: +@Autowired 和 @Resource之间的区别: + +* @Autowired 默认是**按照类型装配**注入,默认情况下它要求依赖对象必须存在(可以设置它 required 属性为 false) + +* @Resource 默认**按照名称装配**注入,只有当找不到与名称匹配的 bean 才会按照类型来装配注入 + + + +**** + + + +##### 静态注入 + +Spring 容器管理的都是实例对象,**@Autowired 依赖注入的都是容器内的对象实例**,在 Java 中 static 修饰的静态属性(变量和方法)是属于类的,而非属于实例对象 + +当类加载器加载静态变量时,Spring 上下文尚未加载,所以类加载器不会在 Bean 中正确注入静态类 + +```java +@Component +public class TestClass { + @Autowired + private static Component component; + + // 调用静态组件的方法 + public static void testMethod() { + component.callTestMethod(); + } +} +// 编译正常,但运行时报java.lang.NullPointerException,所以在调用testMethod()方法时,component变量还没被初始化 +``` + +解决方法: + +* @Autowired 注解到类的构造函数上,Spring 扫描到 Component 的 Bean,然后赋给静态变量 component + + ```java + @Component + public class TestClass { + private static Component component; + + @Autowired + public TestClass(Component component) { + TestClass.component = component; + } + + public static void testMethod() { + component.callTestMethod(); + } + } + ``` + +* @Autowired 注解到静态属性的 setter 方法上 + +* 使用 @PostConstruct 注解一个方法,在方法内为 static 静态成员赋值 + +* 使用 Spring 框架工具类获取 bean,定义成局部变量使用 + + ```java + public class TestClass { + // 调用静态组件的方法 + public static void testMethod() { + Component component = SpringApplicationContextUtil.getBean("component"); + component.callTestMethod(); + } + } + ``` + -* @Autowired 默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false) -* @Resource 默认按照名称来装配注入,只有当找不到与名称匹配的bean才会按照类型来装配注入 +参考文章:http://jessehzx.top/2018/03/18/spring-autowired-static-field/ @@ -4482,7 +4552,7 @@ public class ClassName{} 类型:类注解 -作用:加载properties文件中的属性值 +作用:加载 properties 文件中的属性值 格式: @@ -4586,9 +4656,9 @@ public class ClassName { @DependsOn -- 微信订阅号,发布消息和订阅消息的bean的加载顺序控制(先开订阅,再发布) +- 微信订阅号,发布消息和订阅消息的 bean 的加载顺序控制(先开订阅,再发布) -- 双11活动期间,零点前是结算策略A,零点后是结算策略B,策略B操作的数据为促销数据。策略B加载顺序与促销数据的加载顺序 +- 双 11 活动,零点前是结算策略 A,零点后是结算策略 B,策略 B 操作的数据为促销数据,策略 B 加载顺序与促销数据的加载顺序 @Lazy From 006f9552a9a1bebdcf337712bcb9c2cf4bf0ad9d Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 3 Dec 2021 01:10:25 +0800 Subject: [PATCH 038/122] Update Java Notes --- SSM.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/SSM.md b/SSM.md index 8bad561..9c01b44 100644 --- a/SSM.md +++ b/SSM.md @@ -3946,9 +3946,16 @@ Mybatis 核心配置文件消失 - 类型别名交由 spring 处理 -业务发起使用spring上下文对象获取对应的bean +DAO 接口不需要创建实现类,MyBatis-Spring 提供了一个动态代理的实现 **MapperFactoryBean**,这个类可以让直接注入数据映射器接口到 service 层 bean 中,底层将会动态代理创建类 -**原理**:DAO 接口不需要创建实现类,MyBatis-Spring 提供了一个动态代理的实现 **MapperFactoryBean**,这个类可以让直接注入数据映射器接口到 service 层 bean 中,底层将会动态代理创建类 +整合原理:利用 Spring 框架的 SPI 机制,在 META-INF 目录的 spring.handlers 中给 Spring 容器中导入 NamespaceHandler 类 + +* NamespaceHandler 的 init 方法注册 bean 信息的解析器 MapperScannerBeanDefinitionParser +* 解析器在 Spring 容器创建过程中去解析 mapperScanner 标签,解析出的属性填充到 MapperScannerConfigurer 中 + +* MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor 接口,重写 postProcessBeanDefinitionRegistry() 方法,可以扫描到 MyBatis 的 Mapper + +整合代码: * pom.xml,导入坐标 @@ -4911,7 +4918,7 @@ FactoryBean与 BeanFactory 区别: } ``` -* MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor 接口,重写 postProcessBeanDefinitionRegistry() 方法,可以扫描到 MyBatis 的 Mapper + From 784aab5c004ab3deda10bf85ddf3cdfb6c991136 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 6 Dec 2021 00:02:46 +0800 Subject: [PATCH 039/122] Update Java Notes --- Prog.md | 32 +++++--- SSM.md | 9 ++- Web.md | 234 ++++++++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 223 insertions(+), 52 deletions(-) diff --git a/Prog.md b/Prog.md index 0109c33..5fcf710 100644 --- a/Prog.md +++ b/Prog.md @@ -595,7 +595,7 @@ t.start(); 用户线程:平常创建的普通线程 -守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束。守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示 +守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束。守护进程是**脱离于终端并且在后台运行的进程**,脱离终端是为了避免在执行的过程中的信息在终端上显示 说明:当运行的线程都是守护线程,Java 虚拟机将退出,因为普通线程执行完后,JVM 是守护线程,不会继续运行下去 @@ -4842,7 +4842,7 @@ public class LinkedBlockingQueue extends AbstractQueue ##### 成员属性 -与其他 BlockingQueue 不同,SynchronousQueue 是一个不存储元素的 BlockingQueue,每一个生产者必须阻塞匹配到一个消费者 +SynchronousQueue 是一个不存储元素的 BlockingQueue,**每一个生产者必须阻塞匹配到一个消费者** 成员变量: @@ -13644,7 +13644,7 @@ epoll 的特点: * epoll 的时间复杂度 O(1),epoll 理解为 event poll,不同于忙轮询和无差别轮询,调用 epoll_wait **只是轮询就绪链表**。当监听列表有设备就绪时调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中阻塞的进程,所以 epoll 实际上是**事件驱动**(每个事件关联上fd)的,降低了 system call 的时间复杂度 * epoll 内核中根据每个 fd 上的 callback 函数来实现,只有活跃的 socket 才会主动调用 callback,所以使用 epoll 没有前面两者的线性下降的性能问题,效率提高 -* epoll 注册新的事件都是注册到到内核中 epoll 句柄中,不需要每次调用 epoll_wait 时重复拷贝,对比前面两种,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次。 epoll 也可以利用 **mmap() 文件映射内存**加速与内核空间的消息传递,减少复制开销 +* epoll 注册新的事件都是注册到到内核中 epoll 句柄中,不需要每次调用 epoll_wait 时重复拷贝,对比前面两种,epoll 只需要将描述符从进程缓冲区向内核缓冲区**拷贝一次**,epoll 也可以利用 **mmap() 文件映射内存**加速与内核空间的消息传递(只是可以用) * 前面两者要把 current 往设备等待队列中挂一次,epoll 也只把 current 往等待队列上挂一次,但是这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列,这样可以节省开销 * epoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符,也不会产生像 select 和 poll 的不确定情况 @@ -14069,7 +14069,7 @@ ServerSocket 类: * 构造方法:`public ServerSocket(int port)` * 常用API:`public Socket accept()`,**阻塞等待**接收一个客户端的 Socket 管道连接请求,连接成功返回一个 Socket 对象 - 三次握手后 TCP 连接建立成功,服务器内核会把连接从 SYN 半连接队列中移出,移入 accept 全连接队列,等待进程调用 accept 函数时把连接取出。如果进程不能及时调用 accept 函数,就会造成 accept 队列溢出,最终导致建立好的 TCP 连接被丢弃 + 三次握手后 TCP 连接建立成功,服务器内核会把连接从 SYN 半连接队列(一次握手时在服务端建立的队列)中移出,移入 accept 全连接队列,等待进程调用 accept 函数时把连接取出。如果进程不能及时调用 accept 函数,就会造成 accept 队列溢出,最终导致建立好的 TCP 连接被丢弃 @@ -14122,7 +14122,7 @@ public class ClientDemo { // 1.客户端要请求于服务端的socket管道连接。 Socket socket = new Socket("127.0.0.1", 8080); // 2.从socket通信管道中得到一个字节输出流 - OutputStream os = new socket.getOutputStream(); + OutputStream os = socket.getOutputStream(); // 3.把低级的字节输出流包装成高级的打印流。 PrintStream ps = new PrintStream(os); // 4.开始发消息出去 @@ -14980,10 +14980,16 @@ Channel 实现类: * 通过 FileInputStream 获取的 Channel 只能读 * 通过 FileOutputStream 获取的 Channel 只能写 * 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定 + * DatagramChannel:通过 UDP 读写网络中的数据通道 + * SocketChannel:通过 TCP 读写网络中的数据 -* ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个SocketChannel。 - 提示:ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket + +* ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel + + 提示:ServerSocketChanne 类似 ServerSocket、SocketChannel 类似 Socket + + @@ -14999,7 +15005,7 @@ Channel 实现类: * 通过通道的静态方法 `open()` 打开并返回指定通道 * 使用 Files 类的静态方法 `newByteChannel()` 获取字节通道 -Channel 基本操作: +Channel 基本操作:**读写都是相对于内存来看,也就是缓冲区** | 方法 | 说明 | | ------------------------------------------ | -------------------------------------------------------- | @@ -15011,7 +15017,13 @@ Channel 基本操作: | FileChannel position(long newPosition) | 设置此通道的文件位置 | | public abstract long size() | 返回此通道的文件的当前大小 | -**读写都是相对于内存来看,也就是缓冲区** +**SelectableChannel 的操作 API**: + +| 方法 | 说明 | +| -------------------------------------------------------- | ------------------------------------------------------------ | +| SocketChannel accept() | 如果通道处于非阻塞模式,没有请求连接时此方法将立即返回 NULL,否则将阻塞直到有新的连接或发生 I/O 错误,**通过该方法返回的套接字通道将处于阻塞模式** | +| SelectionKey register(Selector sel, int ops) | 将通道注册到选择器上,并指定监听事件 | +| SelectionKey register(Selector sel, int ops, Object att) | 将通道注册到选择器上,并在当前通道绑定一个附件对象,Object 代表可以是任何类型 | @@ -15225,7 +15237,7 @@ public class ChannelTest { * 连接 : SelectionKey.OP_CONNECT (8) * 接收 : SelectionKey.OP_ACCEPT (16) * 若不止监听一个事件,使用位或操作符连接:`int interest = SelectionKey.OP_READ | SelectionKey.OP_WRITE` -* 参数三:**关联一个附件**,可以是任何对象 +* 参数三:可以关联一个附件,可以是任何对象 **Selector API**: diff --git a/SSM.md b/SSM.md index 9c01b44..b045da3 100644 --- a/SSM.md +++ b/SSM.md @@ -44,7 +44,7 @@ SqlSessionFactory:获取 SqlSession 构建者对象的工厂接口 SqlSession:构建者对象接口,用于执行 SQL、管理事务、接口代理 -* SqlSession 代表和数据库的一次会话,用完必须关闭 +* SqlSession **代表和数据库的一次会话**,用完必须关闭 * SqlSession 和 Connection 一样都是非线程安全,每次使用都应该去获取新的对象 注:**update 数据需要提交事务,或开启默认提交** @@ -1529,6 +1529,11 @@ public class Blog { * SqlSession 相同,手动清除了一级缓存,调用 `sqlSession.clearCache()` * SqlSession 相同,执行 commit 操作或者执行插入、更新、删除,清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,**避免脏读** +Spring 整合 MyBatis 后,一级缓存作用: + +* 未开启事务的情况,每次查询 Spring 都会创建新的 SqlSession,因此一级缓存失效 +* 开启事务的情况,Spring 使用 ThreadLocal 获取当前资源绑定同一个 SqlSession,因此此时一级缓存是有效的 + 测试一级缓存存在 ```java @@ -2644,7 +2649,7 @@ Executor#query(): * `interceptorChain.pluginAll(statementHandler)`:拦截器链 * `prepareStatement()`:通过 StatementHandler 创建 JDBC 原生的 Statement 对象 - * `getConnection()`:获取 JDBC 的 Connection 对象 + * `getConnection()`:**获取 JDBC 的 Connection 对象** * `handler.prepare()`:初始化 Statement 对象 * `instantiateStatement(Connection connection)`:Connection 中的方法实例化对象 * 获取普通执行者对象:`Connection.createStatement()` diff --git a/Web.md b/Web.md index 3bf591f..f12c7a5 100644 --- a/Web.md +++ b/Web.md @@ -4,7 +4,7 @@ ### 概述 -HTML(超文本标记语言—HyperText Markup Language)是构成 Web 世界的一砖一瓦。它是一种用来告知浏览器如何组织页面的标记语言。 +HTML(超文本标记语言—HyperText Markup Language)是构成 Web 世界的基础,是一种用来告知浏览器如何组织页面的标记语言 * 超文本 Hypertext,是指连接单个或者多个网站间的网页的链接。通过链接,就能访问互联网中的内容 @@ -2479,10 +2479,10 @@ Web,在计算机领域指网络。像我们接触的`WWW`,它是由3个单 | 服务器名称 | 说明 | | ----------- | ----------------------------------------------------- | -| weblogic | 实现了javaEE规范,重量级服务器,又称为javaEE容器 | -| websphereAS | 实现了javaEE规范,重量级服务器。 | -| JBOSSAS | 实现了JavaEE规范,重量级服务器。免费的。 | -| Tomcat | 实现了jsp/servlet规范,是一个轻量级服务器,开源免费。 | +| weblogic | 实现了 JavaEE 规范,重量级服务器,又称为 JavaEE 容器 | +| websphereAS | 实现了 JavaEE 规范,重量级服务器。 | +| JBOSSAS | 实现了 JavaEE 规范,重量级服务器,免费 | +| Tomcat | 实现了 jsp/servlet 规范,是一个轻量级服务器,开源免费 | @@ -2646,6 +2646,160 @@ Run -> Edit Configurations -> Templates -> Tomcat Server -> Local +**** + + + +### 执行原理 + +#### 整体架构 + +Tomcat 核心组件架构图如下所示: + +![](https://gitee.com/seazean/images/raw/master/Web/Tomcat-核心组件架构图.png) + +组件介绍: + +- GlobalNamingResources:实现 JNDI,指定一些资源的配置信息 +- Server:Tomcat 是一个 Servlet 容器,一个 Tomcat 对应一个 Server,一个 Server 可以包含多个 Service +- Service:核心服务是 Catalina,用来对请求进行处理,一个 Service 包含多个 Connector 和一个 Container +- Connector:连接器,负责处理客户端请求,解析不同协议及 I/O 方式 +- Executor:线程池 +- Container:容易包含 Engine,Host,Context,Wrapper 等组件 +- Engine:服务交给引擎处理请求,Container 容器中顶层的容器对象,一个 Engine 可以包含多个 Host 主机 +- Host:Engine 容器的子容器,一个 Host 对应一个网络域名,一个 Host 包含多个 Context +- Context:Host 容器的子容器,表示一个 Web 应用 +- Wrapper:Tomcat 中的最小容器单元,表示 Web 应用中的 Servlet + +核心类库: + +* Coyote:Tomcat 连接器的名称,封装了底层的网络通信,为 Catalina 容器提供了统一的接口,使容器与具体的协议以及 I/O 解耦 +* EndPoint:Coyote 通信端点,即通信监听的接口,是 Socket 接收和发送处理器,是对传输层的抽象,用来实现 TCP/IP 协议 +* Processor : Coyote 协议处理接口,用来实现 HTTP 协议,Processor 接收来自 EndPoint 的 Socket,读取字节流解析成 Tomcat 的 Request 和 Response 对象,并通过 Adapter 将其提交到容器处理,Processor 是对应用层协议的抽象 +* CoyoteAdapter:适配器,连接器调用 CoyoteAdapter 的 sevice 方法,传入的是 TomcatRequest 对象,CoyoteAdapter 负责将TomcatRequest 转成 ServletRequest,再调用容器的 service 方法 + + + +参考文章:https://www.jianshu.com/p/7c9401b85704 + +参考文章:https://www.yuque.com/yinhuidong/yu877c/ktq82e + + + +*** + + + +#### 启动过程 + +Tomcat 的启动入口是 Bootstrap#main 函数,首先通过调用 `bootstrap.init()` 初始化相关组件: + +* `initClassLoaders()`:初始化三个类加载器,commonLoader 的父类加载器是启动类加载器 +* `Thread.currentThread().setContextClassLoader(catalinaLoader)`:自定义类加载器加载 Catalina 类,**打破双亲委派** +* `Object startupInstance = startupClass.getConstructor().newInstance()`:反射创建 Catalina 对象 +* `method.invoke(startupInstance, paramValues)`:反射调用方法,设置父类加载器是 sharedLoader +* `catalinaDaemon = startupInstance`:引用 Catalina 对象 + +`daemon.load(args)` 方法反射调用 Catalina 对象的 load 方法,对**服务器的组件进行初始化**,并绑定了 ServerSocket 的端口: + +* `parseServerXml(true)`:解析 XML 配置文件 + +* `getServer().init()`:服务器执行初始化,采用责任链的执行方式 + + * `LifecycleBase.init()`:生命周期接口的初始化方法,开始链式调用 + + * `StandardServer.initInternal()`:Server 的初始化,遍历所有的 Service 进行初始化 + + * `StandardService.initInternal()`:Service 的初始化,对 Engine、Executor、listener、Connector 进行初始化 + + * `StandardEngine.initInternal()`:Engine 的初始化 + + * `getRealm()`:创建一个 Realm 对象 + * `ContainerBase.initInternal()`:容器的初始化,设置处理容器内组件的启动和停止事件的线程池 + + * `Connector.initInternal()`:Connector 的初始化 + + ```java + public Connector() { + this("HTTP/1.1"); //默认无参构造方法,会创建出 Http11NioProtocol 的协议处理器 + } + ``` + + * `adapter = new CoyoteAdapter(this)`:实例化 CoyoteAdapter 对象 + + * `protocolHandler.setAdapter(adapter)`:设置到 ProtocolHandler 协议处理器中 + + * `ProtocolHandler.init()`:协议处理器的初始化,底层调用 `AbstractProtocol#init` 方法 + + `endpoint.init()`:端口的初始化,底层调用 `AbstractEndpoint#init` 方法 + + `NioEndpoint.bind()`:绑定方法 + + * `initServerSocket()`:初始化 ServerSocket,以 NIO 的方式监听端口 + * `serverSock = ServerSocketChannel.open()`:**NIO 的方式打开通道** + * `serverSock.bind(addr, getAcceptCount())`:通道绑定连接端口 + * `serverSock.configureBlocking(true)`:切换为阻塞模式(没懂,为什么阻塞) + * `initialiseSsl()`:初始化 SSL 连接 + * `selectorPool.open(getName())`:打开选择器,类似 NIO 的多路复用器 + +初始化完所有的组件,调用 `daemon.start()` 进行**组件的启动**,底层反射调用 Catalina 对象的 start 方法: + +* `getServer().start()`:启动组件,也是责任链的模式 + + * `LifecycleBase.start()`:生命周期接口的初始化方法,开始链式调用 + + * `StandardServer.startInternal()`:Server 服务的启动 + + * `globalNamingResources.start()`:启动 JNDI 服务 + * `for (Service service : services)`:遍历所有的 Service 进行启动 + + * `StandardService.startInternal()`:Service 的启动,对所有 Executor、listener、Connector 进行启 + + * `StandardEngine.startInternal()`:启动引擎,部署项目 + + * `ContainerBase.startInternal()`:容器的启动 + * 启动集群、Realm 组件,并且创建子容器,提交给线程池 + * `((Lifecycle) pipeline).start()`:遍历所有的管道进行启动 + * `Valve current = first`:获取第一个阀门 + * `((Lifecycle) current).start()`:启动阀门,底层 `ValveBase#startInternal` 中设置启动的状态 + * `current = current.getNext()`:获取下一个阀门 + + * `Connector.startInternal()`:Connector 的初始化 + + * `protocolHandler.start()`:协议处理器的启动 + + `endpoint.start()`:端点启动 + + `NioEndpoint.startInternal()`:启动 NIO 的端点 + + * `createExecutor()`:创建 Worker 线程组,10 个线程,用来进行任务处理 + * `initializeConnectionLatch()`:用来进行连接限流,**最大 8*1024 条连接** + * `poller = new Poller()`:**创建 Poller 对象**,开启了一个多路复用器 Selector + * `Thread pollerThread = new Thread(poller, getName() + "-ClientPoller")`:创建并启动 Poller 线程,Poller 实现了 Runnable 接口,是一个任务对象,**线程 start 后进入 Poller#run 方法** + * `pollerThread.setDaemon(true)`:设置为守护线程 + * `startAcceptorThread()`:启动接收者线程 + * `acceptor = new Acceptor<>(this)`:**创建 Acceptor 对象** + * `Thread t = new Thread(acceptor, threadName)`:创建并启动 Acceptor 接受者线程 + + + +*** + + + +#### 处理过程 + +1) Acceptor 监听客户端套接字,每 50ms 调用一次 `serverSocket.accept`,获取 Socket 后把封装成 NioSocketWrapper(是 SocketWrapperBase 的子类),并设置为非阻塞模式,把 NioSocketWrapper 封装成 PollerEvent 放入同步队列中 +2) Poller 循环判断同步队列中是否有就绪的事件,如果有则通过 `selector.selectedKeys()` 获取就绪事件,获取 SocketChannel 中携带的 attachment(NioSocketWrapper),在 processKey 方法中根据事件类型进行 processSocket,将 Wrapper 对象封装成 SocketProcessor 对象,该对象是一个任务对象,提交到 Worker 线程池进行执行 +3) `SocketProcessorBase.run()` 加锁调用 `SocketProcessor#doRun`,保证线程安全,从协议处理器 ProtocolHandler 中获取 AbstractProtocol,然后**创建 Http11Processor 对象处理请求** +4) `Http11Processor#service` 中调用 `CoyoteAdapter#service` ,把生成的 Tomcat 下的 Request 和 Response 对象通过方法 postParseRequest 匹配到对应的 Servlet 的请求响应,将请求传递到对应的 Engine 容器中调用 Pipeline,管道中包含若干个 Valve,执行完所有的 Valve 最后执行 StandardEngineValve,继续调用 Host 容器的 Pipeline,执行 Host 的 Valve,再传递给 Context 的 Pipeline,最后传递到 Wrapper 容器 +5) `StandardWrapperValve#invoke` 中创建了 Servlet 对象并执行初始化,并为当前请求准备一个 FilterChain 过滤器链执行 doFilter 方法,`ApplicationFilterChain#doFilter` 是一个**责任链的驱动方法**,通过调用 internalDoFilter 来获取过滤器链的下一个过滤器执行 doFilter,执行完所有的过滤器后执行 `servlet.service` 的方法 +6) 最后调用 HttpServlet#service(),根据请求的方法来调用 doGet、doPost 等,执行到自定义的业务方法 + + + + + *** @@ -2659,9 +2813,7 @@ Socket 是使用 TCP/IP 或者 UDP 协议在服务器与客户端之间进行传 - **Servlet 是使用 HTTP 协议在服务器与客户端之间通信的技术,是 Socket 的一种应用** - **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 替代品 +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) @@ -5337,11 +5489,11 @@ JSTL:Java Server Pages Standarded Tag Library,JSP中标准标签库。 ### 过滤器 -Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Listener。 +Filter:过滤器,是 JavaWeb 三大组件之一,另外两个是 Servlet 和 Listener -工作流程:在程序访问服务器资源时,当一个请求到来,服务器首先判断是否有过滤器与去请求资源相关联,如果有,过滤器可以将请求拦截下来,完成一些特定的功能,再由过滤器决定是否交给请求资源。如果没有就直接请求资源,响应同理。 +工作流程:在程序访问服务器资源时,当一个请求到来,服务器首先判断是否有过滤器与去请求资源相关联,如果有过滤器可以将请求拦截下来,完成一些特定的功能,再由过滤器决定是否交给请求资源,如果没有就直接请求资源,响应同理 -作用:过滤器一般用于完成通用的操作,例如:登录验证、统一编码处理、敏感字符过滤等。 +作用:过滤器一般用于完成通用的操作,例如:登录验证、统一编码处理、敏感字符过滤等 @@ -5353,7 +5505,7 @@ Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Lis #### Filter -**Filter是一个接口,如果想实现过滤器的功能,必须实现该接口** +Filter是一个接口,如果想实现过滤器的功能,必须实现该接口 * 核心方法 @@ -5365,27 +5517,31 @@ Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Lis * 配置方式 - * 注解方式 + 注解方式 - ```java - @WebFilter("/*") - ()内填拦截路径,/*代表全部路径 - ``` + ```java + @WebFilter("/*") + ()内填拦截路径,/*代表全部路径 + ``` - * 配置文件 + 配置文件 + + ```xml + + filterDemo01 + filter.FilterDemo01 + + + filterDemo01 + /* + + ``` + + + +*** - ```xml - - filterDemo01 - filter.FilterDemo01 - - - filterDemo01 - /* - - ``` - #### FilterChain @@ -5404,16 +5560,14 @@ Filter:过滤器,是JavaWeb三大组件之一,另外两个是Servlet和Lis FilterConfig 是一个接口,代表过滤器的配置对象,可以加载一些初始化参数 -* 核心方法: +| 方法 | 作用 | +| ------------------------------------------- | -------------------------------------------- | +| String getFilterName() | 获取过滤器对象名称 | +| String getInitParameter(String name) | 获取指定名称的初始化参数的值,不存在返回null | +| Enumeration getInitParameterNames() | 获取所有参数的名称 | +| ServletContext getServletContext() | 获取应用上下文对象 | - | 方法 | 作用 | - | ------------------------------------------- | -------------------------------------------- | - | String getFilterName() | 获取过滤器对象名称 | - | String getInitParameter(String name) | 获取指定名称的初始化参数的值,不存在返回null | - | Enumeration getInitParameterNames() | 获取所有参数的名称 | - | ServletContext getServletContext() | 获取应用上下文对象 | - @@ -5429,7 +5583,7 @@ FilterConfig 是一个接口,代表过滤器的配置对象,可以加载一 过滤器放行之后执行完目标资源,仍会回到过滤器中 -* Filter代码: +* Filter 代码: ```java @WebFilter("/*") @@ -5446,7 +5600,7 @@ FilterConfig 是一个接口,代表过滤器的配置对象,可以加载一 } ``` -* Servlet代码: +* Servlet 代码: ```java @WebServlet("/servletDemo01") @@ -5584,7 +5738,7 @@ FilterConfig 是一个接口,代表过滤器的配置对象,可以加载一 ``` -* Servlet代码:`System.out.println("servletDemo03执行了...");` +* Servlet 代码:`System.out.println("servletDemo03执行了...");` * 控制台输出: From 361a77583c9e9645da58d5822a81e0af50f261c6 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 7 Dec 2021 01:34:29 +0800 Subject: [PATCH 040/122] Update Java Notes --- DB.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/DB.md b/DB.md index df7af85..05fc858 100644 --- a/DB.md +++ b/DB.md @@ -1287,7 +1287,7 @@ LIMIT | AND 或 && | 并且 | | OR 或 \|\| | 或者 | | NOT 或 ! | 非,不是 | - | UNION | 对两个结果集进行并集操作并进行去重,同时进行默认规则的排序 | + | UNION | 对两个结果集进行**并集操作并进行去重,同时进行默认规则的排序** | | UNION ALL | 对两个结果集进行并集操作不进行去重,不进行排序 | * 例如: @@ -2211,12 +2211,21 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 #### 查询优化 -不相关子查询的结果集会被写入一个临时表,并且在写入时去重,该过程称为物化,存储结果集的临时表称为物化表 - -系统变量 tmp_table_size 或者 max_heap_table_size 为表的最值 +不相关子查询的结果集会被写入一个临时表,并且在写入时**去重**,该过程称为**物化**,存储结果集的临时表称为物化表。系统变量 tmp_table_size 或者 max_heap_table_size 为表的最值 * 小于系统变量时,内存中可以保存,会为建立基于内存的 MEMORY 存储引擎的临时表,并建立哈希索引 -* 大于任意一个系统变量时,物化表会使用基于磁盘的存储引擎来保存结果集中的记录,索引类型为 B+ 树, +* 大于任意一个系统变量时,物化表会使用基于磁盘的存储引擎来保存结果集中的记录,索引类型为 B+ 树 + +物化后,嵌套查询就相当于外层查询的表和物化表进行内连接查询,然后经过优化器选择成本最小的表连接顺序执行查询 + +将子查询物化会产生建立临时表的成本,但是将子查询转化为连接查询可以充分发挥优化器的作用,所以引入:半连接 + +* t1 和 t2 表进行半连接,对于 t1 表中的某条记录,只需要关心在 s2 表中是否存在,而不需要关心有多少条记录与之匹配,最终结果集只保留 t1 的记录 +* 半连接只是执行子查询的一种方式,MySQL 并没有提供面向用户的半连接语法 + + + +详细内容可以参考:《MySQL 是怎样运行的》 From c3ce60fcd20347579d24c589b9c31065e415d34e Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 8 Dec 2021 00:47:17 +0800 Subject: [PATCH 041/122] Update Java Notes --- DB.md | 487 ++++++++++++++++++++++++++++++---------------------------- 1 file changed, 254 insertions(+), 233 deletions(-) diff --git a/DB.md b/DB.md index 05fc858..4663c01 100644 --- a/DB.md +++ b/DB.md @@ -2018,7 +2018,7 @@ MySQL 将查询驱动表后得到的记录成为驱动表的扇出,连接查 * 减少驱动表的扇出 * 降低访问被驱动表的成本 -MySQL 提出了一种空间换时间的优化方式,基于块的循环连接,执行连接查询前申请一块固定大小的内存作为连接缓冲区 Join Buffer,先把若干条驱动表中的扇出暂存在缓冲区,每一条被驱动表中的记录一次性的与 Buffer 中多条记录进行匹配(可能是一对多),因为是在内存中完成,所以速度快,并且降低了 I/O 成本。 +MySQL 提出了一种**空间换时间**的优化方式,基于块的循环连接,执行连接查询前申请一块固定大小的内存作为连接缓冲区 Join Buffer,先把若干条驱动表中的扇出暂存在缓冲区,每一条被驱动表中的记录一次性的与 Buffer 中多条记录进行匹配(可能是一对多),因为是在内存中完成,所以速度快,并且降低了 I/O 成本。 Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 256 KB @@ -2413,7 +2413,7 @@ CREATE TABLE us_pro( ## 事务机制 -### 事务介绍 +### 管理事务 事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 sql 语句,这些语句要么都执行,要么都不执行。作为一个关系型数据库,MySQL 支持事务。 @@ -2423,14 +2423,6 @@ CREATE TABLE us_pro( * 如果单元中所有的 SQL 语句都执行成功,则事务就顺利执行 - - -*** - - - -### 管理事务 - 管理事务的三个步骤 1. 开启事务:记录回滚点,并通知服务器,将要执行一组操作,要么同时成功、要么同时失败 @@ -2485,7 +2477,7 @@ CREATE TABLE us_pro( SET AUTOCOMMIT=数字; -- 会话 ``` - - 系统变量的操作: + - **系统变量的操作**: ```sql SET [GLOBAL|SESSION] 变量名 = 值; -- 默认是会话 @@ -2496,9 +2488,7 @@ CREATE TABLE us_pro( SHOW [GLOBAL|SESSION] VARIABLES [LIKE '变量%']; -- 默认查看会话内系统变量值 ``` - - -* 管理实务演示 +* 操作演示 ```mysql -- 开启事务 @@ -2525,7 +2515,7 @@ CREATE TABLE us_pro( -### 四大特征 +### 事务特性 #### ACID @@ -2542,7 +2532,7 @@ CREATE TABLE us_pro( -#### 原子性 +#### 原子特性 原子性是指事务是一个不可分割的工作单位,事务的操作如果成功就必须要完全应用到数据库,失败则不能对数据库有任何影响。比如事务中一个 SQL 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态 @@ -2578,7 +2568,7 @@ rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segme -#### 一致性 +#### 一致特性 一致性是指事务执行前后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。 @@ -2596,7 +2586,9 @@ rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segme -#### 隔离性 +### 隔离特性 + +#### 实现方式 隔离性是指,事务内部的操作与其他事务是隔离的,多个并发事务之间要相互隔离,不能互相干扰 @@ -2617,188 +2609,6 @@ rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segme -#### 持久性 - -##### 实现原理 - -持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 - -Buffer Pool 是一片内存空间,可以通过 innodb_buffer_pool_size 来控制 Buffer Pool 的大小(内存优化部分会详解参数) - -* Change Buffer 是 Buffer Pool 里的内存,不能无限增大,用来对增删改操作提供缓存 -* Change Buffer 的大小可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% -* 补充知识:**唯一索引的更新不能使用 Buffer**,一般只有普通索引可以使用,直接写入 Buffer 就结束 - -InnoDB 的数据是按数据页为单位来读写,每个数据页的大小默认是 16KB。数据是存放在磁盘中,每次读写数据都需要磁盘 IO,效率会很低。InnoDB 提供了缓存 Change Buffer,Buffer 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲: - -* 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool -* 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 - - - -*** - - - -##### 数据恢复 - -Buffer Pool 的使用提高了读写数据的效率,但是也带了新的问题:如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入 redo log - -* 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作 -* 如果 MySQL 宕机,InnoDB 判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏(buffer pool 的任务) - -redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置 - -redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 - -redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快: - -* 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO -* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,减少无效 IO - -InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到磁盘,具体的刷盘策略: - -* 通过修改参数 `innodb_flush_log_at_trx_commit` 设置: - * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待主线程每秒刷新一次 - * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功 - * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作 -* 如果写入 redo log buffer 的日志已经占据了 redo log buffer 总容量的一半了,此时就会刷入到磁盘文件,这时会影响执行效率,所以开发中应该**避免大事务** - -刷脏策略: - -* redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到磁盘中的数据文件中 -* Buffer Pool 内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘(大事务) -* 系统空闲时,后台线程会自动进行刷脏 -* MySQL 正常关闭时,会把内存的脏页都 flush 到磁盘上 - - - -*** - - - -##### 工作流程 - -MySQL中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,**保证数据不丢失**,二者的区别是: - -* 作用不同:redo log 是用于 crash recovery (故障恢复),保证 MySQL 宕机也不会影响持久性;binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制 - -* 层次不同:redo log 是 InnoDB 存储引擎实现的,而 binlog 是MySQL的服务器层实现的,同时支持 InnoDB 和其他存储引擎 - -* 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) - -* 写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元 - -两种日志在 update 更新数据的**作用时机**: - -```sql -update T set c=c+1 where ID=2; -``` - - - -流程说明:执行引擎将这行新数据更新到内存中(Buffer Pool)后,然后会将这个更新操作记录到 redo log buffer 里,此时 redo log 处于 prepare 状态,代表执行完成随时可以提交事务,然后执行器生成这个操作的 binlog 并**把 binlog 写入磁盘**,在提交事务后 **redo log 也持久化到磁盘** - -redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 - -故障恢复数据: - -* 如果在时刻 A 发生了崩溃(crash),由于此时 binlog 还没写,redo log 也没提交,所以数据恢复的时候这个事务会回滚 -* 如果在时刻 B 发生了崩溃,redo log 和 binlog 有一个共同的数据字段叫 XID,崩溃恢复的时候,会按顺序扫描 redo log: - * 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,说明 binlog 也已经记录完整,直接从 redo log 恢复数据 - * 如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以从 binlog 恢复 redo log 的信息,进而恢复数据,提交事务 - - -判断一个事务的 binlog 是否完整的方法: - -* statement 格式的 binlog,最后会有 COMMIT -* row 格式的 binlog,最后会有一个 XID event -* MySQL 5.6.2 版本以后,引入了 binlog-checksum 参数用来验证 binlog 内容的正确性 - - - -参考文章:https://time.geekbang.org/column/article/73161 - - - -*** - - - -##### 系统优化 - -系统在进行刷脏时会占用一部分系统资源,会影响系统的性能,产生系统抖动 - -* 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长 -* 日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的 - -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,不建议开启此功能 - - - - - -*** - - - -### 隔离级别 - -事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别,否则就会产生问题。 - -隔离级别分类: - -| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | -| ---------------- | -------- | -------------------------------- | ------------------- | -| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | -| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | -| repeatable read | 可重复读 | 幻读 | MySQL | -| serializable | 串行化 | 无(因为写会加写锁,读会加读锁) | | - -一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 - -* 丢失更新 (Lost Update):当两个或多个事务选择同一行,最初的事务修改的值,被后面事务修改的值覆盖,所有的隔离级别都可以避免丢失更新(行锁) - -* 脏读 (Dirty Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个**未提交**的事务中的数据 - -* 不可重复读 (Non-Repeatable Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 - - > 可重复读的意思是不管读几次,结果都一样,可以重复的读,可以理解为快照读,要读的数据集不会发生变化 - -* 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,后一次查询查到了前一次查询没有查到的行,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入 - -**隔离级别操作语法:** - -* 查询数据库隔离级别 - - ```mysql - SELECT @@TX_ISOLATION; - SHOW VARIABLES LIKE 'tx_isolation'; - ``` - -* 修改数据库隔离级别 - - ```mysql - SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串; - ``` - - - -*** - - - -### 并发控制 - #### MVCC MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来**解决读写冲突的无锁并发控制** @@ -2836,7 +2646,7 @@ MVCC 的优点: -#### 原理 +#### 实现原理 ##### 隐藏字段 @@ -2917,12 +2727,12 @@ Read View 几个属性: creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交) * db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以这种情况下此数据对 creator 是可见的 -* db_trx_id < up_limit_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见 +* db_trx_id < up_limit_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见 -* db_trx_id >= low_limit_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 -* up_limit_id <= db_trx_id < low_limit_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 - * 在列表中,说明该版本对应的事务正在运行,数据不能显示(**不能读到未提交的数据**) - * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示(**可以读到已经提交的数据**) +* db_trx_id >= low_limit_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 +* up_limit_id <= db_trx_id < low_limit_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 + * 在列表中,说明该版本对应的事务正在运行,数据不能显示(**不能读到未提交的数据**) + * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示(**可以读到已经提交的数据**) @@ -2999,12 +2809,197 @@ RC、RR 级别下的 InnoDB 快照读区别 - 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 Read View 并不能阻止事务去更新数据,并且把这条新记录的 trx_id 给变为当前的事务 id,对当前事务就是可见的了 + - 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 + + +*** + + + +### 持久特性 + +#### 实现原理 + +持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 + +Buffer Pool 是一片内存空间,可以通过 innodb_buffer_pool_size 来控制 Buffer Pool 的大小(内存优化部分会详解参数) + +* Change Buffer 是 Buffer Pool 里的内存,不能无限增大,用来对增删改操作提供缓存 +* Change Buffer 的大小可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% +* 补充知识:**唯一索引的更新不能使用 Buffer**,一般只有普通索引可以使用,直接写入 Buffer 就结束 + +InnoDB 的数据是按数据页为单位来读写,每个数据页的大小默认是 16KB。数据是存放在磁盘中,每次读写数据都需要磁盘 IO,效率会很低。InnoDB 提供了缓存 Change Buffer,Buffer 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲: + +* 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool +* 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 + + + +*** + + + +#### 数据恢复 + +Buffer Pool 的使用提高了读写数据的效率,但是也带了新的问题:如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入 redo log + +* 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作 +* 如果 MySQL 宕机,InnoDB 判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏(buffer pool 的任务) + +redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置 + +redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 + +redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快: + +* 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO +* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,减少无效 IO + +InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到磁盘,具体的刷盘策略: + +* 通过修改参数 `innodb_flush_log_at_trx_commit` 设置: + * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待主线程每秒刷新一次 + * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功 + * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作 +* 如果写入 redo log buffer 的日志已经占据了 redo log buffer 总容量的一半了,此时就会刷入到磁盘文件,这时会影响执行效率,所以开发中应该**避免大事务** + +刷脏策略: + +* redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到磁盘中的数据文件中 +* Buffer Pool 内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘(大事务) +* 系统空闲时,后台线程会自动进行刷脏 +* MySQL 正常关闭时,会把内存的脏页都 flush 到磁盘上 + + + +*** + + + +#### 工作流程 + +MySQL中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,**保证数据不丢失**,二者的区别是: + +* 作用不同:redo log 是用于 crash recovery (故障恢复),保证 MySQL 宕机也不会影响持久性;binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制 + +* 层次不同:redo log 是 InnoDB 存储引擎实现的,而 binlog 是MySQL的服务器层实现的,同时支持 InnoDB 和其他存储引擎 + +* 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) + +* 写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元 + +两种日志在 update 更新数据的**作用时机**: + +```sql +update T set c=c+1 where ID=2; +``` + + + +流程说明:执行引擎将这行新数据更新到内存中(Buffer Pool)后,然后会将这个更新操作记录到 redo log buffer 里,此时 redo log 处于 prepare 状态,代表执行完成随时可以提交事务,然后执行器生成这个操作的 binlog 并**把 binlog 写入磁盘**,在提交事务后 **redo log 也持久化到磁盘** + +redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 + +故障恢复数据: + +* 如果在时刻 A 发生了崩溃(crash),由于此时 binlog 还没写,redo log 也没提交,所以数据恢复的时候这个事务会回滚 +* 如果在时刻 B 发生了崩溃,redo log 和 binlog 有一个共同的数据字段叫 XID,崩溃恢复的时候,会按顺序扫描 redo log: + * 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,说明 binlog 也已经记录完整,直接从 redo log 恢复数据 + * 如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以从 binlog 恢复 redo log 的信息,进而恢复数据,提交事务 + + +判断一个事务的 binlog 是否完整的方法: + +* statement 格式的 binlog,最后会有 COMMIT +* row 格式的 binlog,最后会有一个 XID event +* MySQL 5.6.2 版本以后,引入了 binlog-checksum 参数用来验证 binlog 内容的正确性 + + + +参考文章:https://time.geekbang.org/column/article/73161 + + + +*** + + + +#### 系统优化 + +系统在进行刷脏时会占用一部分系统资源,会影响系统的性能,产生系统抖动 + +* 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长 +* 日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的 + +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,不建议开启此功能 + + + + + +*** + + + +### 隔离级别 + +事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别,否则就会产生问题。 + +隔离级别分类: + +| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | +| ---------------- | -------- | -------------------------------- | ------------------- | +| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | +| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | +| repeatable read | 可重复读 | 幻读 | MySQL | +| serializable | 串行化 | 无(因为写会加写锁,读会加读锁) | | + +一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 + +* 丢失更新 (Lost Update):当两个或多个事务选择同一行,最初的事务修改的值,被后面事务修改的值覆盖,所有的隔离级别都可以避免丢失更新(行锁) + +* 脏读 (Dirty Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个**未提交**的事务中的数据 + +* 不可重复读 (Non-Repeatable Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 + + > 可重复读的意思是不管读几次,结果都一样,可以重复的读,可以理解为快照读,要读的数据集不会发生变化 + +* 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,后一次查询查到了前一次查询没有查到的行,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入 + +**隔离级别操作语法:** + +* 查询数据库隔离级别 + + ```mysql + SELECT @@TX_ISOLATION; + SHOW VARIABLES LIKE 'tx_isolation'; + ``` + +* 修改数据库隔离级别 + + ```mysql + SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串; + ``` + + + + + *** @@ -4364,14 +4359,25 @@ MyISAM 的索引方式也叫做非聚集的,之所以这么称呼是为了与 ### 索引结构 -#### BTree +#### 数据页 + +文件系统的最小单元是块(block),一个块的大小是 4K,系统从磁盘读取数据到内存时是以磁盘块为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么 + +InnoDB 存储引擎中有页(Page)的概念,页是 MySQL 磁盘管理的最小单位 + +* **InnoDB 存储引擎中默认每个页的大小为 16KB,索引中一个节点就是一个数据页**,所以会一次性读取 16KB 的数据到内存 +* InnoDB 引擎将若干个地址连接磁盘块,以此来达到页的大小 16KB +* 在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率 + -磁盘存储: -* 系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么 -- InnoDB 存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位,InnoDB 存储引擎中默认每个页的大小为 16KB -- InnoDB 引擎将若干个地址连接磁盘块,以此来达到页的大小 16KB,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率 + +*** + + + +#### BTree BTree 的索引类型是基于 B+Tree 树型数据结构的,B+Tree 又是 BTree 数据结构的变种,用在数据库和操作系统中的文件系统,特点是能够保持数据稳定有序 @@ -4391,31 +4397,31 @@ BTree 又叫多路平衡搜索树,一颗 m 叉的 BTree 特性如下: ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程1.png) -* 插入H,n>4,中间元素G字母向上分裂到新的节点 +* 插入 H,n>4,中间元素 G 字母向上分裂到新的节点 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程2.png) -* 插入E,K,Q不需要分裂 +* 插入 E、K、Q 不需要分裂 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程3.png) -* 插入M,中间元素M字母向上分裂到父节点G +* 插入 M,中间元素 M 字母向上分裂到父节点 G ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程4.png) -* 插入F,W,L,T 不需要分裂 +* 插入 F,W,L,T 不需要分裂 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程5.png) -* 插入Z,中间元素T向上分裂到父节点中 +* 插入 Z,中间元素 T 向上分裂到父节点中 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程6.png) -* 插入D,中间元素D向上分裂到父节点中,然后插入P,R,X,Y不需要分裂 +* 插入 D,中间元素 D 向上分裂到父节点中,然后插入 P,R,X,Y 不需要分裂 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程7.png) -* 最后插入S,NPQR节点n>5,中间节点Q向上分裂,但分裂后父节点DGMT的n>5,中间节点M向上分裂 +* 最后插入 S,NPQR 节点 n>5,中间节点 Q 向上分裂,但分裂后父节点 DGMT 的 n>5,中间节点 M 向上分裂 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程8.png) @@ -4463,7 +4469,7 @@ MySQL 索引数据结构对经典的 B+Tree 进行了优化,在原 B+Tree 的 区间访问的意思是访问索引为 5 - 15 的数据,可以直接根据相邻节点的指针遍历 -B+ 树的叶子节点是数据页(page),一个页里面可以存多个数据行 +B+ 树的**叶子节点是数据页**(page),一个页里面可以存多个数据行 ![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-B+Tree.png) @@ -4472,7 +4478,10 @@ B+ 树的叶子节点是数据页(page),一个页里面可以存多个数 - 有范围:对于主键的范围查找和分页查找 - 有顺序:从根节点开始,进行随机查找,顺序查找 -InnoDB 中每个数据页的大小默认是 16KB,一般表的主键类型为 INT(4 字节)或 BIGINT(8 字节),指针类型也一般为 4 或 8 个字节,也就是说一个页(B+Tree 中的**一个节点**)中大概存储 16KB/(8B+8B)=1K 个键值(估值)。则一个深度为 3 的 B+Tree 索引可以维护 `10^3 * 10^3 * 10^3 = 10亿` 条记录 +InnoDB 中每个数据页的大小默认是 16KB, + +* 索引行:一般表的主键类型为 INT(4 字节)或 BIGINT(8 字节),指针大小在 InnoDB 中设置为 6 字节节,也就是说一个页大概存储 16KB/(8B+6B)=1K 个键值(估值)。则一个深度为 3 的 B+Tree 索引可以维护 `10^3 * 10^3 * 10^3 = 10亿` 条记录 +* 数据行:一行数据的大小可能是 1k,一个数据页可以存储 16 行 实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree 的高度一般都在 2-4 层。MySQL 的 InnoDB 存储引擎在设计时是**将根节点常驻内存的**,也就是说查找某一键值的行记录时最多只需要 1~3 次磁盘 I/O 操作 @@ -4672,7 +4681,7 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 **适用条件**: -* 需要存储引擎将索引中的数据与条件进行判断(所以条件列必须都在同一个索引中),所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM +* 需要存储引擎将索引中的数据与条件进行判断(所以**条件列必须都在同一个索引中**),所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM * 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 * 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了,索引下推的目的减少 IO 次数也就失去了意义 @@ -4917,7 +4926,7 @@ EXPLAIN SELECT * FROM table_1 WHERE id = 1; | filtered | 条件过滤的行百分比,单表查询没意义,用于连接查询中对驱动表的扇出进行过滤,查询优化器预测所有扇出值满足剩余查询条件的百分比,相乘以后表示多表查询中还要对被驱动执行查询的次数 | | extra | 执行情况的说明和描述 | -MySQL 执行计划的局限: +MySQL **执行计划的局限**: * 只是计划,不是执行 SQL 语句,可以随着底层优化器输入的更改而更改 * EXPLAIN 不会告诉显示关于触发器、存储过程的信息对查询的影响情况 @@ -4927,6 +4936,8 @@ MySQL 执行计划的局限: * EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看执行计划 * EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句实际的执行计划不同 +SHOW WARINGS:在使用 EXPALIN 命令后执行该语句,可以查询与执行计划相关的拓展信息,展示出 Level、Code、Message 三个字段,当 Code 为 1003 时,Message 字段展示的信息类似于将查询语句重写后的信息,但是不是等价,不能执行复制过来运行 + 环境准备: ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-执行计划环境准备.png) @@ -5056,15 +5067,15 @@ key_len: 其他的额外的执行计划信息,在该列展示: +* No tables used:查询语句中使用 FROM dual 或者没有 FROM 语句 +* Impossible WHERE:查询语句中的 WHERE 子句条件永远为 FALSE,会导致没有符合条件的行 * Using index:该值表示相应的 SELECT 操作中使用了**覆盖索引**(Covering Index) -* Using index condition:第一种情况是搜索条件中虽然出现了索引列,但是有部分条件无法使用索引,会根据能用索引的条件先搜索一遍再匹配无法使用索引的条件,回表查询数据;第二种是使用了索引下推 -* Using where:表示存储引擎收到记录后进行后过滤(Post-filter),如果查询操作未能使用索引,Using where 的作用是提醒我们 MySQL 将用 where 子句来过滤结果集,即需要回表查询 -* Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序和分组查询 -* Using filesort:对数据使用外部排序算法,将取得的数据在内存中进行排序,这种无法利用索引完成的排序操作称为文件排序 -* Using join buffer:说明在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果 -* Impossible where:说明 where 语句会导致没有符合条件的行,通过收集统计信息不可能存在结果 +* Using index condition:第一种情况是搜索条件中虽然出现了索引列,但是部分条件无法形成扫描区间(**索引失效**),会根据可用索引的条件先搜索一遍再匹配无法使用索引的条件,回表查询数据;第二种是使用了**索引条件下推**优化 +* Using where:搜索条件需要在 Server 层判断,判断后执行回表操作查询,无法使用索引下推 +* Using join buffer:连接查询被驱动表无法利用索引,需要连接缓冲区来存储中间结果 +* Using filesort:无法利用索引完成排序(优化方向),需要对数据使用外部排序算法,将取得的数据在内存或磁盘中进行排序 +* Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序、去重、分组等场景 * Select tables optimized away:说明仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 - * No tables used:Query 语句中使用 from dual 或不含任何 from 子句 @@ -5125,11 +5136,11 @@ SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的** -#### trace +#### TRACE -MySQL 提供了对 SQL 的跟踪, 通过 trace 文件能够进一步了解执行过程。 +MySQL 提供了对 SQL 的跟踪, 通过 trace 文件可以查看优化器生成执行计划的过程 -* 打开 trace,设置格式为 JSON,并设置 trace 最大能够使用的内存大小,避免解析过程中因为默认内存过小而不能够完整展示 +* 打开 trace 功能,设置格式为 JSON,并设置 trace 的最大使用内存,避免解析过程中因默认内存过小而不能够完整展示 ```mysql SET optimizer_trace="enabled=on",end_markers_in_json=ON; -- 会话内有效 @@ -5147,6 +5158,8 @@ MySQL 提供了对 SQL 的跟踪, 通过 trace 文件能够进一步了解执 ```mysql SELECT * FROM information_schema.optimizer_trace \G; -- \G代表竖列展示 ``` + + 执行信息主要有三个阶段:prepare 阶段、optimize 阶段(成本分析)、execute 阶段(执行) @@ -5187,7 +5200,7 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); #### 避免失效 -索引失效的情况: +##### 语句错误 * 全值匹配:对索引中所有列都指定具体值,这种情况索引生效,执行效率高 @@ -5294,6 +5307,14 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); 原因:在覆盖索引的这棵 B+ 数上只需要进行 like 的匹配,或者是基于覆盖索引查询再进行 WHERE 的判断就可以获得结果 + + +*** + + + +##### 系统优化 + 系统优化为全表扫描: * 如果 MySQL 评估使用索引比全表更慢,则不使用索引,索引失效: From a5061ed3f2b7436c2c02e873f55e20a2255b2244 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 11 Dec 2021 01:53:28 +0800 Subject: [PATCH 042/122] Update Java Notes --- DB.md | 5373 ++++++++++++++++++++++++++++--------------------------- Java.md | 29 +- Prog.md | 4 + SSM.md | 4 +- Web.md | 48 +- 5 files changed, 2803 insertions(+), 2655 deletions(-) diff --git a/DB.md b/DB.md index 4663c01..7235c9b 100644 --- a/DB.md +++ b/DB.md @@ -52,7 +52,7 @@ MySQL 数据库是一个最流行的关系型数据库管理系统之一 缺点:数据存储在磁盘中,导致读写性能差,而且数据关系复杂,扩展性差 -MySQL 所使用的SQL语句是用于访问数据库最常用的标准化语言。 +MySQL 所使用的 SQL 语句是用于访问数据库最常用的标准化语言。 MySQL 配置: @@ -450,7 +450,7 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 SHOW VARIABLES LIKE 'have_query_cache'; -- YES ``` -2. 查看当前MySQL是否开启了查询缓存: +2. 查看当前 MySQL 是否开启了查询缓存: ```mysql SHOW VARIABLES LIKE 'query_cache_type'; -- OFF @@ -2405,180 +2405,198 @@ CREATE TABLE us_pro( -**** +*** -## 事务机制 +## 存储结构 -### 管理事务 +### 视图 -事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 sql 语句,这些语句要么都执行,要么都不执行。作为一个关系型数据库,MySQL 支持事务。 +#### 基本介绍 -单元中的每条 SQL 语句都相互依赖,形成一个整体 +视图概念:视图是一种虚拟存在的数据表,这个虚拟的表并不在数据库中实际存在 -* 如果某条 SQL 语句执行失败或者出现错误,那么整个单元就会回滚,撤回到事务最初的状态 +本质:将一条 SELECT 查询语句的结果封装到了一个虚拟表中,所以在创建视图的时候,工作重心要放在这条 SELECT 查询语句上 -* 如果单元中所有的 SQL 语句都执行成功,则事务就顺利执行 +作用:将一些比较复杂的查询语句的结果,封装到一个虚拟表中,再有相同查询需求时,直接查询该虚拟表 -管理事务的三个步骤 +优点: -1. 开启事务:记录回滚点,并通知服务器,将要执行一组操作,要么同时成功、要么同时失败 +* 简单:使用视图的用户不需要关心表的结构、关联条件和筛选条件,因为虚拟表中已经是过滤好的结果集 +* 安全:使用视图的用户只能访问查询的结果集,对表的权限管理并不能限制到某个行某个列 -2. 执行 SQL 语句:执行具体的一条或多条 SQL 语句 +* 数据独立,一旦视图的结构确定,可以屏蔽表结构变化对用户的影响,源表增加列对视图没有影响;源表修改列名,则可以通过修改视图来解决,不会造成对访问者的影响 -3. 结束事务(提交|回滚) - - 提交:没出现问题,数据进行更新 - - 回滚:出现问题,数据恢复到开启事务时的状态 +*** -事务操作: -* 开启事务 - ```mysql - START TRANSACTION; - ``` +#### 视图创建 -* 回滚事务 +* 创建视图 ```mysql - ROLLBACK; + CREATE [OR REPLACE] + VIEW 视图名称 [(列名列表)] + AS 查询语句 + [WITH [CASCADED | LOCAL] CHECK OPTION]; ``` -* 提交事务,显示执行是手动提交,MySQL 默认为自动提交 + `WITH [CASCADED | LOCAL] CHECK OPTION` 决定了是否允许更新数据使记录不再满足视图的条件: + + * LOCAL:只要满足本视图的条件就可以更新 + * CASCADED:必须满足所有针对该视图的所有视图的条件才可以更新, 默认值 + +* 例如 ```mysql - COMMIT; + -- 数据准备 city + id NAME cid + 1 深圳 1 + 2 上海 1 + 3 纽约 2 + 4 莫斯科 3 + + -- 数据准备 country + id NAME + 1 中国 + 2 美国 + 3 俄罗斯 + + -- 创建city_country视图,保存城市和国家的信息(使用指定列名) + CREATE + VIEW + city_country (city_id,city_name,country_name) + AS + SELECT + c1.id, + c1.name, + c2.name + FROM + city c1, + country c2 + WHERE + c1.cid=c2.id; ``` - 工作原理: - - * 自动提交模式下,如果没有 start transaction 显式地开始一个事务,那么**每个 SQL 语句都会被当做一个事务执行提交操作** - * 手动提交模式下,所有的 SQL 语句都在一个事务中,直到执行了 commit 或 rollback + - * 存在一些特殊的命令,在事务中执行了这些命令会马上强制执行 COMMIT 提交事务,如 DDL 语句 (create/drop/alter/table)、lock tables 语句等 +*** - 提交方式语法: - - 查看事务提交方式 - ```mysql - SELECT @@AUTOCOMMIT; -- 1 代表自动提交 0 代表手动提交 - ``` +#### 视图查询 - - 修改事务提交方式 +* 查询所有数据表,视图也会查询出来 - ```mysql - SET @@AUTOCOMMIT=数字; -- 系统 - SET AUTOCOMMIT=数字; -- 会话 - ``` - - - **系统变量的操作**: + ```mysql + SHOW TABLES; + SHOW TABLE STATUS [\G]; + ``` - ```sql - SET [GLOBAL|SESSION] 变量名 = 值; -- 默认是会话 - SET @@[(GLOBAL|SESSION).]变量名 = 值; -- 默认是系统 - ``` +* 查询视图 - ```sql - SHOW [GLOBAL|SESSION] VARIABLES [LIKE '变量%']; -- 默认查看会话内系统变量值 - ``` + ```mysql + SELECT * FROM 视图名称; + ``` -* 操作演示 +* 查询某个视图创建 ```mysql - -- 开启事务 - START TRANSACTION; - - -- 张三给李四转账500元 - -- 1.张三账户-500 - UPDATE account SET money=money-500 WHERE NAME='张三'; - -- 2.李四账户+500 - UPDATE account SET money=money+500 WHERE NAME='李四'; - - -- 回滚事务(出现问题) - ROLLBACK; - - -- 提交事务(没出现问题) - COMMIT; + SHOW CREATE VIEW 视图名称; ``` - - *** -### 事务特性 - -#### ACID - -事务的四大特征:ACID - -- 原子性 (atomicity) -- 一致性 (consistency) -- 隔离性 (isolaction) -- 持久性 (durability) +#### 视图修改 +视图表数据修改,会**自动修改源表中的数据**,因为更新的是视图中的基表中的数据 +* 修改视图表中的数据 -*** + ```mysql + UPDATE 视图名称 SET 列名 = 值 WHERE 条件; + ``` +* 修改视图的结构 + ```mysql + ALTER [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] + VIEW 视图名称 [(列名列表)] + AS 查询语句 + [WITH [CASCADED | LOCAL] CHECK OPTION] + + -- 将视图中的country_name修改为name + ALTER + VIEW + city_country (city_id,city_name,name) + AS + SELECT + c1.id, + c1.name, + c2.name + FROM + city c1, + country c2 + WHERE + c1.cid=c2.id; + ``` -#### 原子特性 -原子性是指事务是一个不可分割的工作单位,事务的操作如果成功就必须要完全应用到数据库,失败则不能对数据库有任何影响。比如事务中一个 SQL 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态 -InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 undo log(回滚日志) +*** -* redo log 用于保证事务持久性 -* undo log 用于保证事务原子性和隔离性 -undo log 属于逻辑日志,根据每行操作进行记录,记录了 SQL 执行相关的信息,用来回滚行记录到某个版本 -当事务对数据库进行修改时,InnoDB 会先记录对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容**做与之前相反的操作**: +#### 视图删除 -* 对于每个 insert,回滚时会执行 delete +* 删除视图 -* 对于每个 delete,回滚时会执行 insert + ```mysql + DROP VIEW 视图名称; + ``` -* 对于每个 update,回滚时会执行一个相反的 update,把数据修改回去 +* 如果存在则删除 -undo log 是采用段(segment)的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment + ```mysql + DROP VIEW IF EXISTS 视图名称; + ``` -rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segment -* 在以前老版本,只支持 1 个 rollback segment,只能记录 1024 个 undo log segment -* MySQL5.5 开始支持 128 个 rollback segment,支持 128*1024 个 undo 操作 -参考文章:https://www.cnblogs.com/kismetv/p/10331633.html +*** -*** +### 存储过程 +#### 基本介绍 -#### 一致特性 +存储过程和函数:存储过程和函数是事先经过编译并存储在数据库中的一段 SQL 语句的集合 -一致性是指事务执行前后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。 +存储过程和函数的好处: -数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变) +* 提高代码的复用性 +* 减少数据在数据库和应用服务器之间的传输,提高传输效率 +* 减少代码层面的业务处理 +* **一次编译永久有效** -实现一致性的措施: +存储过程和函数的区别: -- 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证 -- 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等 -- 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致 +* 存储函数必须有返回值 +* 存储过程可以没有返回值 @@ -2586,86 +2604,189 @@ rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segme -### 隔离特性 +#### 基本操作 -#### 实现方式 +DELIMITER: -隔离性是指,事务内部的操作与其他事务是隔离的,多个并发事务之间要相互隔离,不能互相干扰 +* DELIMITER 关键字用来声明 sql 语句的分隔符,告诉 MySQL 该段命令已经结束 -* 严格的隔离性,对应了事务隔离级别中的 serializable,实际应用中对性能考虑很少使用可串行化 +* MySQL 语句默认的分隔符是分号,但是有时需要一条功能 sql 语句中包含分号,但是并不作为结束标识,这时使用 DELIMITER 来指定分隔符: -* 与原子性、持久性侧重于研究事务本身不同,隔离性研究的是**不同事务**之间的相互影响 + ```mysql + DELIMITER 分隔符 + ``` -隔离性让并发情形下的事务之间互不干扰: +存储过程的创建调用查看和删除: -- 一个事务的写操作对另一个事务的写操作(写写):锁机制保证隔离性 -- 一个事务的写操作对另一个事务的读操作(读写):MVCC 保证隔离性 +* 创建存储过程 -锁机制:事务在修改数据之前,需要先获得相应的锁,获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁(详解见锁机制) + ```mysql + -- 修改分隔符为$ + DELIMITER $ + + -- 标准语法 + CREATE PROCEDURE 存储过程名称(参数...) + BEGIN + sql语句; + END$ + + -- 修改分隔符为分号 + DELIMITER ; + ``` +* 调用存储过程 + ```mysql + CALL 存储过程名称(实际参数); + ``` -*** +* 查看存储过程 + ```mysql + SELECT * FROM mysql.proc WHERE db='数据库名称'; + ``` +* 删除存储过程 -#### MVCC + ```mysql + DROP PROCEDURE [IF EXISTS] 存储过程名称; + ``` -MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来**解决读写冲突的无锁并发控制** +练习: -MVCC 处理读写请求,可以做到在发生读写请求冲突时不用加锁,这个读是指的快照读,而不是当前读 +* 数据准备 -* 快照读:实现基于 MVCC,因为是多版本并发,所以快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据 -* 当前读:读取数据库记录是当前最新的版本(产生幻读、不可重复读),可以对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读 - -数据库并发场景: - -* 读-读:不存在任何问题,也不需要并发控制 + ```mysql + id NAME age gender score + 1 张三 23 男 95 + 2 李四 24 男 98 + 3 王五 25 女 100 + 4 赵六 26 女 90 + ``` -* 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读 +* 创建 stu_group() 存储过程,封装分组查询总成绩,并按照总成绩升序排序的功能 -* 写-写:有线程安全问题,可能会存在丢失更新问题 + ```mysql + DELIMITER $ + + CREATE PROCEDURE stu_group() + BEGIN + SELECT gender,SUM(score) getSum FROM student GROUP BY gender ORDER BY getSum ASC; + END$ + + DELIMITER ; + + -- 调用存储过程 + CALL stu_group(); + -- 删除存储过程 + DROP PROCEDURE IF EXISTS stu_group; + ``` -MVCC 的优点: + -* 在并发读写数据库时,做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了并发读写的性能 -* 可以解决脏读,不可重复读等事务隔离问题(加锁也能解决),但不能解决更新丢失问题 +*** -提高读写和写写的并发性能: -* MVCC + 悲观锁:MVCC 解决读写冲突,悲观锁解决写写冲突 -* MVCC + 乐观锁:MVCC 解决读写冲突,乐观锁解决写写冲突 +#### 存储语法 +##### 变量使用 -参考文章:https://www.jianshu.com/p/8845ddca3b23 +存储过程是可以进行编程的,意味着可以使用变量、表达式、条件控制语句等,来完成比较复杂的功能 +* 定义变量:DECLARE 定义的是局部变量,只能用在 BEGIN END 范围之内 + + ```mysql + DECLARE 变量名 数据类型 [DEFAULT 默认值]; + ``` + +* 变量的赋值 + ```mysql + SET 变量名 = 变量值; + SELECT 列名 INTO 变量名 FROM 表名 [WHERE 条件]; + ``` -*** +* 数据准备:表 student + ```mysql + id NAME age gender score + 1 张三 23 男 95 + 2 李四 24 男 98 + 3 王五 25 女 100 + 4 赵六 26 女 90 + ``` +* 定义两个 int 变量,用于存储男女同学的总分数 -#### 实现原理 + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test3() + BEGIN + -- 定义两个变量 + DECLARE men,women INT; + -- 查询男同学的总分数,为men赋值 + SELECT SUM(score) INTO men FROM student WHERE gender='男'; + -- 查询女同学的总分数,为women赋值 + SELECT SUM(score) INTO women FROM student WHERE gender='女'; + -- 使用变量 + SELECT men,women; + END$ + DELIMITER ; + -- 调用存储过程 + CALL pro_test3(); + ``` -##### 隐藏字段 -实现原理主要是隐藏字段,undo日志,Read View 来实现的 -数据库中的每行数据,除了自定义的字段,还有数据库隐式定义的字段: +*** -* DB_TRX_ID:6byte,最近修改事务ID,记录创建该数据或最后一次修改(修改/插入)该数据的事务ID。当每个事务开启时,都会被分配一个ID,这个 ID 是递增的 -* DB_ROLL_PTR:7byte,回滚指针,配合 undo 日志,指向上一个旧版本(存储在 rollback segment) -* DB_ROW_ID:6byte,隐含的自增ID(**隐藏主键**),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 作为聚簇索引 +##### IF语句 -* DELETED_BIT:删除标志的隐藏字段,记录被更新或删除并不代表真的删除,而是删除位变了 +* if 语句标准语法 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC版本链隐藏字段.png) + ```mysql + IF 判断条件1 THEN 执行的sql语句1; + [ELSEIF 判断条件2 THEN 执行的sql语句2;] + ... + [ELSE 执行的sql语句n;] + END IF; + ``` +* 数据准备:表 student + ```mysql + id NAME age gender score + 1 张三 23 男 95 + 2 李四 24 男 98 + 3 王五 25 女 100 + 4 赵六 26 女 90 + ``` + +* 根据总成绩判断:全班 380 分及以上学习优秀、320 ~ 380 学习良好、320 以下学习一般 + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test4() + BEGIN + DECLARE total INT; -- 定义总分数变量 + DECLARE description VARCHAR(10); -- 定义分数描述变量 + SELECT SUM(score) INTO total FROM student; -- 为总分数变量赋值 + -- 判断总分数 + IF total >= 380 THEN + SET description = '学习优秀'; + ELSEIF total >=320 AND total < 380 THEN + SET description = '学习良好'; + ELSE + SET description = '学习一般'; + END IF; + END$ + DELIMITER ; + -- 调用pro_test4存储过程 + CALL pro_test4(); + ``` @@ -2674,65 +2795,155 @@ MVCC 的优点: -##### undo - -undo log 是逻辑日志,记录的是每个事务对数据执行的操作,而不是记录的全部数据,需要根据 undo log 逆推出以往事务的数据 - -undo log 的作用: +##### 参数传递 -* 保证事务进行 rollback 时的原子性和一致性,当事务进行回滚的时候可以用 undo log 的数据进行恢复 -* 用于 MVCC 快照读,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本 +* 参数传递的语法 -undo log 主要分为两种: + IN:代表输入参数,需要由调用者传递实际数据,默认的 + OUT:代表输出参数,该参数可以作为返回值 + INOUT:代表既可以作为输入参数,也可以作为输出参数 -* insert undo log:事务在 insert 新记录时产生的 undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃 + ```mysql + DELIMITER $ + + -- 标准语法 + CREATE PROCEDURE 存储过程名称([IN|OUT|INOUT] 参数名 数据类型) + BEGIN + 执行的sql语句; + END$ + + DELIMITER ; + ``` -* update undo log:事务在进行 update 或 delete 时产生的 undo log,在事务回滚时需要,在快照读时也需要。不能随意删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除 +* 输入总成绩变量,代表学生总成绩,输出分数描述变量,代表学生总成绩的描述 -每次对数据库记录进行改动,都会将旧值放到一条 undo 日志中,算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为**版本链**,版本链的头节点就是当前记录最新的值,链尾就是最早的旧记录 + ```mysql + DELIMITER $ + + CREATE PROCEDURE pro_test6(IN total INT, OUT description VARCHAR(10)) + BEGIN + -- 判断总分数 + IF total >= 380 THEN + SET description = '学习优秀'; + ELSEIF total >= 320 AND total < 380 THEN + SET description = '学习不错'; + ELSE + SET description = '学习一般'; + END IF; + END$ + + DELIMITER ; + -- 调用pro_test6存储过程 + CALL pro_test6(310,@description); + CALL pro_test6((SELECT SUM(score) FROM student), @description); + -- 查询总成绩描述 + SELECT @description; + ``` - +* 查看参数方法 -* 有个事务插入 persion 表一条新记录,name 为 Jerry,age 为 24 + * @变量名 : **用户会话变量**,代表整个会话过程他都是有作用的,类似于全局变量 + * @@变量名 : **系统变量** -* 事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先记录 undo log,然后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁 -* 以此类推 + -补充知识:purge 线程 +*** -* 为了实现 InnoDB 的 MVCC 机制,更新或者删除操作都只是设置一下老记录的 deleted_bit,并不真正将过时的记录删除,为了节省磁盘空间,InnoDB 有专门的 purge 线程来清理 deleted_bit 为 true 的记录 -* purge 线程维护了一个 Read view(这个 Read view 相当于系统中最老活跃事务的 Read view),如果某个记录的 deleted_bit 为 true,并且 DB_TRX_ID 相对于 purge 线程的 Read view 可见,那么这条记录一定是可以被安全清除的 +##### CASE -*** +* 标准语法 1 + ```mysql + CASE 表达式 + WHEN 值1 THEN 执行sql语句1; + [WHEN 值2 THEN 执行sql语句2;] + ... + [ELSE 执行sql语句n;] + END CASE; + ``` +* 标准语法 2 -##### 读视图 + ```mysql + sCASE + WHEN 判断条件1 THEN 执行sql语句1; + [WHEN 判断条件2 THEN 执行sql语句2;] + ... + [ELSE 执行sql语句n;] + END CASE; + ``` -Read View 是事务进行**快照读**操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 +* 演示 -注意:这里的快照并不是把所有的数据拷贝一份副本,而是由 undo log 记录的逻辑日志,根据库中的数据进行计算出历史数据 + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test7(IN total INT) + BEGIN + -- 定义变量 + DECLARE description VARCHAR(10); + -- 使用case判断 + CASE + WHEN total >= 380 THEN + SET description = '学习优秀'; + WHEN total >= 320 AND total < 380 THEN + SET description = '学习不错'; + ELSE + SET description = '学习一般'; + END CASE; + + -- 查询分数描述信息 + SELECT description; + END$ + DELIMITER ; + -- 调用pro_test7存储过程 + CALL pro_test7(390); + CALL pro_test7((SELECT SUM(score) FROM student)); + ``` -工作流程:将版本链的头节点的事务 ID(最新数据事务 ID)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比,如果 DB_TRX_ID 不符合可见性,通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足特定条件的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 -Read View 几个属性: -- m_ids:生成 Read View 时当前系统中活跃的事务 id 列表(未提交的事务集合,当前事务也在其中) -- up_limit_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值(已提交的事务集合) -- low_limit_id:生成 Read View 时系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1(未开始事务) -- creator_trx_id:生成该 Read View 的事务的事务 id,就是判断该 id 的事务能读到什么数据 +*** -creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交) -* db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以这种情况下此数据对 creator 是可见的 -* db_trx_id < up_limit_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见 -* db_trx_id >= low_limit_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 -* up_limit_id <= db_trx_id < low_limit_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 - * 在列表中,说明该版本对应的事务正在运行,数据不能显示(**不能读到未提交的数据**) - * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示(**可以读到已经提交的数据**) +##### WHILE + +* while 循环语法 + + ```mysql + WHILE 条件判断语句 DO + 循环体语句; + 条件控制语句; + END WHILE; + ``` + +* 计算 1~100 之间的偶数和 + + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test6() + BEGIN + -- 定义求和变量 + DECLARE result INT DEFAULT 0; + -- 定义初始化变量 + DECLARE num INT DEFAULT 1; + -- while循环 + WHILE num <= 100 DO + IF num % 2 = 0 THEN + SET result = result + num; + END IF; + SET num = num + 1; + END WHILE; + -- 查询求和结果 + SELECT result; + END$ + DELIMITER ; + + -- 调用pro_test6存储过程 + CALL pro_test6(); + ``` @@ -2740,46 +2951,97 @@ creator 创建一个 Read View,进行可见性算法分析:(解决了读 -##### 工作流程 +##### REPEAT -表 user 数据 +* repeat 循环标准语法 -```sh -id name age -1 张三 18 -``` + ```mysql + 初始化语句; + REPEAT + 循环体语句; + 条件控制语句; + UNTIL 条件判断语句 + END REPEAT; + ``` -Transaction 20: +* 计算 1~10 之间的和 -```mysql -START TRANSACTION; -- 开启事务 -UPDATE user SET name = '李四' WHERE id = 1; -UPDATE user SET name = '王五' WHERE id = 1; -``` + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test9() + BEGIN + -- 定义求和变量 + DECLARE result INT DEFAULT 0; + -- 定义初始化变量 + DECLARE num INT DEFAULT 1; + -- repeat循环 + REPEAT + -- 累加 + SET result = result + num; + -- 让num+1 + SET num = num + 1; + -- 停止循环 + UNTIL num > 10 + END REPEAT; + -- 查询求和结果 + SELECT result; + END$ + + DELIMITER ; + -- 调用pro_test9存储过程 + CALL pro_test9(); + ``` -Transaction 60: -```mysql -START TRANSACTION; -- 开启事务 --- 操作表的其他数据 -``` -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC工作流程1.png) -ID 为 0 的事务创建 Read View: +*** -* m_ids:20、60 -* up_limit_id:20 -* low_limit_id:61 -* creator_trx_id:0 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC工作流程2.png) -只有红框部分才复合条件,所以只有张三对应的版本的数据可以被看到 +##### LOOP +LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定义,通常可以使用 LEAVE 语句实现,如果不加退出循环的语句,那么就变成了死循环 +* loop 循环标准语法 -参考视频:https://www.bilibili.com/video/BV1t5411u7Fg + ```mysql + [循环名称:] LOOP + 条件判断语句 + [LEAVE 循环名称;] + 循环体语句; + 条件控制语句; + END LOOP 循环名称; + ``` + +* 计算 1~10 之间的和 + + ```mysql + DELIMITER $ + CREATE PROCEDURE pro_test10() + BEGIN + -- 定义求和变量 + DECLARE result INT DEFAULT 0; + -- 定义初始化变量 + DECLARE num INT DEFAULT 1; + -- loop循环 + l:LOOP + -- 条件成立,停止循环 + IF num > 10 THEN + LEAVE l; + END IF; + -- 累加 + SET result = result + num; + -- 让num+1 + SET num = num + 1; + END LOOP l; + -- 查询求和结果 + SELECT result; + END$ + DELIMITER ; + -- 调用pro_test10存储过程 + CALL pro_test10(); + ``` @@ -2787,57 +3049,361 @@ ID 为 0 的事务创建 Read View: -#### RC RR +##### 游标 -Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现 +游标是用来存储查询结果集的数据类型,在存储过程和函数中可以使用光标对结果集进行循环的处理 +* 游标可以遍历返回的多行结果,每次拿到一整行数据 +* 简单来说游标就类似于集合的迭代器遍历 +* MySQL 中的游标只能用在存储过程和函数中 -RR、RC 生成时机: +游标的语法 -- RC 隔离级别下,每个快照读都会生成并获取最新的 Read View -- RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View +* 创建游标 -RC、RR 级别下的 InnoDB 快照读区别 + ```mysql + DECLARE 游标名称 CURSOR FOR 查询sql语句; + ``` -- RC 级别下的,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因 +* 打开游标 -- RR 级别下的某个事务的对某条记录的第一次快照读会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个Read View,所以一个事务的查询结果每次都是相同的 + ```mysql + OPEN 游标名称; + ``` - 当前事务在其他事务提交之前使用过快照读,那么以后其他事务对数据的修改都是不可见的,就算以后其他事务提交了数据也不可见;早于 Read View 创建的事务所做的修改并提交的均是可见的 +* 使用游标获取数据 -解决幻读问题: + ```mysql + FETCH 游标名称 INTO 变量名1,变量名2,...; + ``` -- 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** +* 关闭游标 - 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 Read View 并不能阻止事务去更新数据,并且把这条新记录的 trx_id 给变为当前的事务 id,对当前事务就是可见的了 + ```mysql + CLOSE 游标名称; + ``` -- 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 +* Mysql 通过一个 Error handler 声明来判断指针是否到尾部,并且必须和创建游标的 SQL 语句声明在一起: + + ```mysql + DECLARE EXIT HANDLER FOR NOT FOUND (do some action,一般是设置标志变量) + ``` + + + +游标的基本使用 + +* 数据准备:表 student + ```mysql + id NAME age gender score + 1 张三 23 男 95 + 2 李四 24 男 98 + 3 王五 25 女 100 + 4 赵六 26 女 90 + ``` + +* 创建 stu_score 表 + + ```mysql + CREATE TABLE stu_score( + id INT PRIMARY KEY AUTO_INCREMENT, + score INT + ); + ``` + +* 将student表中所有的成绩保存到stu_score表中 + ```mysql + DELIMITER $ + + CREATE PROCEDURE pro_test12() + BEGIN + -- 定义成绩变量 + DECLARE s_score INT; + -- 定义标记变量 + DECLARE flag INT DEFAULT 0; + + -- 创建游标,查询所有学生成绩数据 + DECLARE stu_result CURSOR FOR SELECT score FROM student; + -- 游标结束后,将标记变量改为1 这两个必须声明在一起 + DECLARE EXIT HANDLER FOR NOT FOUND SET flag = 1; + + -- 开启游标 + OPEN stu_result; + -- 循环使用游标 + REPEAT + -- 使用游标,遍历结果,拿到数据 + FETCH stu_result INTO s_score; + -- 将数据保存到stu_score表中 + INSERT INTO stu_score VALUES (NULL,s_score); + UNTIL flag=1 + END REPEAT; + -- 关闭游标 + CLOSE stu_result; + END$ + + DELIMITER ; + + -- 调用pro_test12存储过程 + CALL pro_test12(); + -- 查询stu_score表 + SELECT * FROM stu_score; + ``` + + + + + +*** + + + +#### 存储函数 + +存储函数和存储过程是非常相似的,存储函数可以做的事情,存储过程也可以做到 + +存储函数有返回值,存储过程没有返回值(参数的 out 其实也相当于是返回数据了) + +* 创建存储函数 + + ```mysql + DELIMITER $ + -- 标准语法 + CREATE FUNCTION 函数名称(参数 数据类型) + RETURNS 返回值类型 + BEGIN + 执行的sql语句; + RETURN 结果; + END$ + + DELIMITER ; + ``` + +* 调用存储函数,因为有返回值,所以使用 SELECT 调用 + + ```mysql + SELECT 函数名称(实际参数); + ``` + +* 删除存储函数 + + ```mysql + DROP FUNCTION 函数名称; + ``` + +* 定义存储函数,获取学生表中成绩大于95分的学生数量 + + ```mysql + DELIMITER $ + CREATE FUNCTION fun_test() + RETURN INT + BEGIN + -- 定义统计变量 + DECLARE result INT; + -- 查询成绩大于95分的学生数量,给统计变量赋值 + SELECT COUNT(score) INTO result FROM student WHERE score > 95; + -- 返回统计结果 + SELECT result; + END + DELIMITER ; + -- 调用fun_test存储函数 + SELECT fun_test(); + ``` + + + + + +*** + + + +### 触发器 + +#### 基本介绍 + +触发器是与表有关的数据库对象,在 insert/update/delete 之前或之后触发并执行触发器中定义的 SQL 语句 + +* 触发器的这种特性可以协助应用在数据库端确保数据的完整性 、日志记录 、数据校验等操作 + +- 使用别名 NEW 和 OLD 来引用触发器中发生变化的记录内容,这与其他的数据库是相似的 +- 现在触发器还只支持行级触发,不支持语句级触发 + +| 触发器类型 | OLD的含义 | NEW的含义 | +| --------------- | ------------------------------ | ------------------------------ | +| INSERT 型触发器 | 无 (因为插入前状态无数据) | NEW 表示将要或者已经新增的数据 | +| UPDATE 型触发器 | OLD 表示修改之前的数据 | NEW 表示将要或已经修改后的数据 | +| DELETE 型触发器 | OLD 表示将要或者已经删除的数据 | 无 (因为删除后状态无数据) | + + + +*** + + + +#### 基本操作 + +* 创建触发器 + + ```mysql + DELIMITER $ + + CREATE TRIGGER 触发器名称 + BEFORE|AFTER INSERT|UPDATE|DELETE + ON 表名 + [FOR EACH ROW] -- 行级触发器 + BEGIN + 触发器要执行的功能; + END$ + + DELIMITER ; + ``` + +* 查看触发器的状态、语法等信息 + + ```mysql + SHOW TRIGGERS; + ``` + +* 删除触发器,如果没有指定 schema_name,默认为当前数据库 + + ```mysql + DROP TRIGGER [schema_name.]trigger_name; + ``` + + + +*** + + + +#### 触发演示 + +通过触发器记录账户表的数据变更日志。包含:增加、修改、删除 + +* 数据准备 + + ```mysql + -- 创建db9数据库 + CREATE DATABASE db9; + -- 使用db9数据库 + USE db9; + ``` + + ```mysql + -- 创建账户表account + CREATE TABLE account( + id INT PRIMARY KEY AUTO_INCREMENT, -- 账户id + NAME VARCHAR(20), -- 姓名 + money DOUBLE -- 余额 + ); + -- 添加数据 + INSERT INTO account VALUES (NULL,'张三',1000),(NULL,'李四',2000); + ``` + + ```mysql + -- 创建日志表account_log + CREATE TABLE account_log( + id INT PRIMARY KEY AUTO_INCREMENT, -- 日志id + operation VARCHAR(20), -- 操作类型 (insert update delete) + operation_time DATETIME, -- 操作时间 + operation_id INT, -- 操作表的id + operation_params VARCHAR(200) -- 操作参数 + ); + ``` + +* 创建 INSERT 型触发器 + ```mysql + DELIMITER $ + + CREATE TRIGGER account_insert + AFTER INSERT + ON account + FOR EACH ROW + BEGIN + INSERT INTO account_log VALUES (NULL,'INSERT',NOW(),new.id,CONCAT('插入后{id=',new.id,',name=',new.name,',money=',new.money,'}')); + END$ + + DELIMITER ; + ``` + ```mysql + -- 向account表添加记录 + INSERT INTO account VALUES (NULL,'王五',3000); + + -- 查询日志表 + SELECT * FROM account_log; + /* + id operation operation_time operation_id operation_params + 1 INSERT 2021-01-26 19:51:11 3 插入后{id=3,name=王五money=2000} + */ + ``` + +* 创建 UPDATE 型触发器 -*** + ```mysql + DELIMITER $ + + CREATE TRIGGER account_update + AFTER UPDATE + ON account + FOR EACH ROW + BEGIN + INSERT INTO account_log VALUES (NULL,'UPDATE',NOW(),new.id,CONCAT('修改前{id=',old.id,',name=',old.name,',money=',old.money,'}','修改后{id=',new.id,',name=',new.name,',money=',new.money,'}')); + END$ + + DELIMITER ; + ``` + ```mysql + -- 修改account表 + UPDATE account SET money=3500 WHERE id=3; + + -- 查询日志表 + SELECT * FROM account_log; + /* + id operation operation_time operation_id operation_params + 2 UPDATE 2021-01-26 19:58:54 2 更新前{id=2,name=李四money=1000} + 更新后{id=2,name=李四money=200} + */ + ``` + -### 持久特性 +* 创建 DELETE 型触发器 -#### 实现原理 + ```mysql + DELIMITER $ + + CREATE TRIGGER account_delete + AFTER DELETE + ON account + FOR EACH ROW + BEGIN + INSERT INTO account_log VALUES (NULL,'DELETE',NOW(),old.id,CONCAT('删除前{id=',old.id,',name=',old.name,',money=',old.money,'}')); + END$ + + DELIMITER ; + ``` -持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 + ```mysql + -- 删除account表数据 + DELETE FROM account WHERE id=3; + + -- 查询日志表 + SELECT * FROM account_log; + /* + id operation operation_time operation_id operation_params + 3 DELETE 2021-01-26 20:02:48 3 删除前{id=3,name=王五money=2000} + */ + ``` -Buffer Pool 是一片内存空间,可以通过 innodb_buffer_pool_size 来控制 Buffer Pool 的大小(内存优化部分会详解参数) -* Change Buffer 是 Buffer Pool 里的内存,不能无限增大,用来对增删改操作提供缓存 -* Change Buffer 的大小可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% -* 补充知识:**唯一索引的更新不能使用 Buffer**,一般只有普通索引可以使用,直接写入 Buffer 就结束 -InnoDB 的数据是按数据页为单位来读写,每个数据页的大小默认是 16KB。数据是存放在磁盘中,每次读写数据都需要磁盘 IO,效率会很低。InnoDB 提供了缓存 Change Buffer,Buffer 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲: -* 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool -* 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 @@ -2845,84 +3411,110 @@ InnoDB 的数据是按数据页为单位来读写,每个数据页的大小默 -#### 数据恢复 - -Buffer Pool 的使用提高了读写数据的效率,但是也带了新的问题:如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入 redo log - -* 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作 -* 如果 MySQL 宕机,InnoDB 判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏(buffer pool 的任务) -redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置 -redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 +## 存储引擎 -redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快: +### 基本介绍 -* 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO -* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,减少无效 IO +对比其他数据库,MySQL 的架构可以在不同场景应用并发挥良好作用,主要体现在存储引擎,插件式的存储引擎架构将查询处理和其他的系统任务以及数据的存储提取分离,可以针对不同的存储需求可以选择最优的存储引擎 -InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到磁盘,具体的刷盘策略: +存储引擎的介绍: -* 通过修改参数 `innodb_flush_log_at_trx_commit` 设置: - * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待主线程每秒刷新一次 - * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功 - * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作 -* 如果写入 redo log buffer 的日志已经占据了 redo log buffer 总容量的一半了,此时就会刷入到磁盘文件,这时会影响执行效率,所以开发中应该**避免大事务** +- MySQL 数据库使用不同的机制存取表文件 , 机制的差别在于不同的存储方式、索引技巧、锁定水平等不同的功能和能力,在 MySQL 中,将这些不同的技术及配套的功能称为存储引擎 +- Oracle、SqlServer 等数据库只有一种存储引擎,MySQL **提供了插件式的存储引擎架构**,所以 MySQL 存在多种存储引擎 , 就会让数据库采取了不同的处理数据的方式和扩展功能 +- 在关系型数据库中数据的存储是以表的形式存进行,所以存储引擎也称为表类型(存储和操作此表的类型) +- 通过选择不同的引擎,能够获取最佳的方案, 也能够获得额外的速度或者功能,提高程序的整体效果。 -刷脏策略: +MySQL 支持的存储引擎: -* redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到磁盘中的数据文件中 -* Buffer Pool 内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘(大事务) -* 系统空闲时,后台线程会自动进行刷脏 -* MySQL 正常关闭时,会把内存的脏页都 flush 到磁盘上 +- MySQL 支持的引擎包括:InnoDB、MyISAM、MEMORY、Archive、Federate、CSV、BLACKHOLE 等 +- MySQL5.5 之前的默认存储引擎是 MyISAM,5.5 之后就改为了 InnoDB -*** +**** -#### 工作流程 +### 引擎对比 -MySQL中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,**保证数据不丢失**,二者的区别是: +MyISAM 存储引擎: -* 作用不同:redo log 是用于 crash recovery (故障恢复),保证 MySQL 宕机也不会影响持久性;binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制 +* 特点:不支持事务和外键,读取速度快,节约资源 +* 应用场景:查询和插入操作为主,只有很少更新和删除操作,并对事务的完整性、并发性要求不高 +* 存储方式: + * 每个 MyISAM 在磁盘上存储成 3 个文件,其文件名都和表名相同,拓展名不同 + * 表的定义保存在 .frm 文件,表数据保存在 .MYD (MYData) 文件中,索引保存在 .MYI (MYIndex) 文件中 -* 层次不同:redo log 是 InnoDB 存储引擎实现的,而 binlog 是MySQL的服务器层实现的,同时支持 InnoDB 和其他存储引擎 +InnoDB 存储引擎:(MySQL5.5 版本后默认的存储引擎) -* 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) +- 特点:**支持事务**和外键操作,支持并发控制。对比 MyISAM 的存储引擎,InnoDB 写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引 +- 应用场景:对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,读写频繁的操作 +- 存储方式: + - 使用共享表空间存储, 这种方式创建的表的表结构保存在 .frm 文件中, 数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path 定义的表空间中,可以是多个文件 + - 使用多表空间存储,创建的表的表结构存在 .frm 文件中,每个表的数据和索引单独保存在 .ibd 中 -* 写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元 +MEMORY 存储引擎: -两种日志在 update 更新数据的**作用时机**: +- 特点:每个 MEMORY 表实际对应一个磁盘文件 ,该文件中只存储表的结构,表数据保存在内存中,且默认使用 HASH 索引,这样有利于数据的快速处理,在需要快速定位记录可以提供更快的访问,但是服务一旦关闭,表中的数据就会丢失,数据存储不安全 +- 应用场景:通常用于更新不太频繁的小表,用以快速得到访问结果,类似缓存 +- 存储方式:表结构保存在 .frm 中 -```sql -update T set c=c+1 where ID=2; -``` +MERGE存储引擎: - +* 特点: -流程说明:执行引擎将这行新数据更新到内存中(Buffer Pool)后,然后会将这个更新操作记录到 redo log buffer 里,此时 redo log 处于 prepare 状态,代表执行完成随时可以提交事务,然后执行器生成这个操作的 binlog 并**把 binlog 写入磁盘**,在提交事务后 **redo log 也持久化到磁盘** + * 是一组 MyISAM 表的组合,这些 MyISAM 表必须结构完全相同,通过将不同的表分布在多个磁盘上 + * MERGE 表本身并没有存储数据,对 MERGE 类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的 MyISAM 表进行的 -redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 +* 应用场景:将一系列等同的 MyISAM 表以逻辑方式组合在一起,并作为一个对象引用他们,适合做数据仓库 -故障恢复数据: +* 操作方式: -* 如果在时刻 A 发生了崩溃(crash),由于此时 binlog 还没写,redo log 也没提交,所以数据恢复的时候这个事务会回滚 -* 如果在时刻 B 发生了崩溃,redo log 和 binlog 有一个共同的数据字段叫 XID,崩溃恢复的时候,会按顺序扫描 redo log: - * 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,说明 binlog 也已经记录完整,直接从 redo log 恢复数据 - * 如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以从 binlog 恢复 redo log 的信息,进而恢复数据,提交事务 + * 插入操作是通过 INSERT_METHOD 子句定义插入的表,使用 FIRST 或 LAST 值使得插入操作被相应地作用在第一或者最后一个表上;不定义这个子句或者定义为 NO,表示不能对 MERGE 表执行插入操作 + * 对 MERGE 表进行 DROP 操作,但是这个操作只是删除 MERGE 表的定义,对内部的表是没有任何影响的 + ```mysql + CREATE TABLE order_1( + )ENGINE = MyISAM DEFAULT CHARSET=utf8; + + CREATE TABLE order_2( + )ENGINE = MyISAM DEFAULT CHARSET=utf8; + + CREATE TABLE order_all( + -- 结构与MyISAM表相同 + )ENGINE = MERGE UNION = (order_1,order_2) INSERT_METHOD=LAST DEFAULT CHARSET=utf8; + ``` -判断一个事务的 binlog 是否完整的方法: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MERGE.png) -* statement 格式的 binlog,最后会有 COMMIT -* row 格式的 binlog,最后会有一个 XID event -* MySQL 5.6.2 版本以后,引入了 binlog-checksum 参数用来验证 binlog 内容的正确性 +| 特性 | MyISAM | InnoDB | MEMORY | +| ------------ | ---------------------------- | ------------- | ------------------ | +| 存储限制 | 有(平台对文件系统大小的限制) | 64TB | 有(平台的内存限制) | +| **事务安全** | **不支持** | **支持** | **不支持** | +| **锁机制** | **表锁** | **表锁/行锁** | **表锁** | +| B+Tree索引 | 支持 | 支持 | 支持 | +| 哈希索引 | 不支持 | 不支持 | 支持 | +| 全文索引 | 支持 | 支持 | 不支持 | +| 集群索引 | 不支持 | 支持 | 不支持 | +| 数据索引 | 不支持 | 支持 | 支持 | +| 数据缓存 | 不支持 | 支持 | N/A | +| 索引缓存 | 支持 | 支持 | N/A | +| 数据可压缩 | 支持 | 不支持 | 不支持 | +| 空间使用 | 低 | 高 | N/A | +| 内存使用 | 低 | 高 | 中等 | +| 批量插入速度 | 高 | 低 | 高 | +| **外键** | **不支持** | **支持** | **不支持** | +面试问题:MyIsam 和 InnoDB 的区别? +* 事务:InnoDB 支持事务,MyISAM 不支持事务 +* 外键:InnoDB 支持外键,MyISAM 不支持外键 +* 索引:InnoDB 是聚集(聚簇)索引,MyISAM 是非聚集(非聚簇)索引 -参考文章:https://time.geekbang.org/column/article/73161 +* 锁粒度:InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁 +* 存储结构:参考本节上半部分 @@ -2930,100 +3522,118 @@ redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段 -#### 系统优化 - -系统在进行刷脏时会占用一部分系统资源,会影响系统的性能,产生系统抖动 +### 引擎操作 -* 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长 -* 日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的 +* 查询数据库支持的存储引擎 -InnoDB 刷脏页的控制策略: + ```mysql + SHOW ENGINES; + SHOW VARIABLES LIKE '%storage_engine%'; -- 查看Mysql数据库默认的存储引擎 + ``` -* `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% 来控制刷脏页的速度 + ```mysql + SHOW TABLE STATUS FROM 数据库名称; + ``` -* `innodb_flush_neighbors` 参数置为 1 代表控制刷脏时检查相邻的数据页,如果也是脏页就一起刷脏,并检查邻居的邻居,这个行为会一直蔓延直到不是脏页,在 MySQL 8.0 中该值的默认值是 0,不建议开启此功能 +* 查询某个数据库中某个数据表的存储引擎 + ```mysql + SHOW TABLE STATUS FROM 数据库名称 WHERE NAME = '数据表名称'; + ``` +* 创建数据表,指定存储引擎 + ```mysql + CREATE TABLE 表名( + 列名,数据类型, + ... + )ENGINE = 引擎名称; + ``` +* 修改数据表的存储引擎 -*** + ```mysql + ALTER TABLE 表名 ENGINE = 引擎名称; + ``` -### 隔离级别 -事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别,否则就会产生问题。 -隔离级别分类: -| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | -| ---------------- | -------- | -------------------------------- | ------------------- | -| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | -| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | -| repeatable read | 可重复读 | 幻读 | MySQL | -| serializable | 串行化 | 无(因为写会加写锁,读会加读锁) | | -一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 -* 丢失更新 (Lost Update):当两个或多个事务选择同一行,最初的事务修改的值,被后面事务修改的值覆盖,所有的隔离级别都可以避免丢失更新(行锁) +*** -* 脏读 (Dirty Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个**未提交**的事务中的数据 -* 不可重复读 (Non-Repeatable Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 - > 可重复读的意思是不管读几次,结果都一样,可以重复的读,可以理解为快照读,要读的数据集不会发生变化 -* 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,后一次查询查到了前一次查询没有查到的行,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入 -**隔离级别操作语法:** +## 索引机制 -* 查询数据库隔离级别 +### 索引介绍 - ```mysql - SELECT @@TX_ISOLATION; - SHOW VARIABLES LIKE 'tx_isolation'; - ``` +#### 基本介绍 -* 修改数据库隔离级别 +MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,**本质是排好序的快速查找数据结构。**在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 - ```mysql - SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串; - ``` +**索引是在存储引擎层实现的**,所以并没有统一的索引标准,即不同存储引擎的索引的工作方式并不一样 +索引使用:一张数据表,用于保存数据;一个索引配置文件,用于保存索引;每个索引都指向了某一个数据 +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引的介绍.png) +左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的)。为了加快 Col2 的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据的物理地址的指针,这样就可以运用二叉查找快速获取到相应数据 +索引的优点: +* 类似于书籍的目录索引,提高数据检索的效率,降低数据库的 IO 成本 +* 通过索引列对数据进行排序,降低数据排序的成本,降低 CPU 的消耗 -*** +索引的缺点: +* 一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式**存储在磁盘**上 +* 虽然索引大大提高了查询效率,同时却也降低更新表的速度。对表进行 INSERT、UPDATE、DELETE 操作,MySQL 不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,还会调整因为更新所带来的键值变化后的索引信息,**但是更新数据也需要先从数据库中获取**,索引加快了获取速度,所以可以相互抵消一下。 +* 索引会影响到 WHERE 的查询条件和排序 ORDER BY 两大功能 +*** -## 存储结构 -### 视图 -#### 基本介绍 +#### 索引分类 -视图概念:视图是一种虚拟存在的数据表,这个虚拟的表并不在数据库中实际存在 +索引一般的分类如下: -本质:将一条 SELECT 查询语句的结果封装到了一个虚拟表中,所以在创建视图的时候,工作重心要放在这条 SELECT 查询语句上 +- 功能分类 + - 主键索引:一种特殊的唯一索引,不允许有空值,一般在建表时同时创建主键索引 + - 单列索引:一个索引只包含单个列,一个表可以有多个单列索引(普通索引) + - 联合索引:顾名思义,就是将单列索引进行组合 + - 唯一索引:索引列的值必须唯一,**允许有空值**,如果是联合索引,则列值组合必须唯一 + * NULL 值必须只出现一次 + * 可以声明不允许存储 NULL 值的非空唯一索引 + - 外键索引:只有 InnoDB 引擎支持外键索引,用来保证数据的一致性、完整性和实现级联操作 + +- 结构分类 + - BTree 索引:MySQL 使用最频繁的一个索引数据结构,是 InnoDB 和 MyISAM 存储引擎默认的索引类型,底层基于 B+Tree + - Hash 索引:MySQL中 Memory 存储引擎默认支持的索引类型 + - R-tree 索引(空间索引):空间索引是 MyISAM 引擎的一个特殊索引类型,主要用于地理空间数据类型 + - Full-text 索引(全文索引):快速匹配全部文档的方式。MyISAM 支持, InnoDB 不支持 FULLTEXT 类型的索引,但是 InnoDB 可以使用 sphinx 插件支持全文索引,MEMORY 引擎不支持 + + | 索引 | InnoDB | MyISAM | Memory | + | --------- | ---------------- | ------ | ------ | + | BTREE | 支持 | 支持 | 支持 | + | HASH | 不支持 | 不支持 | 支持 | + | R-tree | 不支持 | 支持 | 不支持 | + | Full-text | 5.6 版本之后支持 | 支持 | 不支持 | -作用:将一些比较复杂的查询语句的结果,封装到一个虚拟表中,再有相同查询需求时,直接查询该虚拟表 +联合索引图示:根据身高年龄建立的组合索引(height,age) -优点: +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-组合索引图.png) -* 简单:使用视图的用户不需要关心表的结构、关联条件和筛选条件,因为虚拟表中已经是过滤好的结果集 -* 安全:使用视图的用户只能访问查询的结果集,对表的权限管理并不能限制到某个行某个列 -* 数据独立,一旦视图的结构确定,可以屏蔽表结构变化对用户的影响,源表增加列对视图没有影响;源表修改列名,则可以通过修改视图来解决,不会造成对访问者的影响 @@ -3031,80 +3641,75 @@ InnoDB 刷脏页的控制策略: -#### 视图创建 +### 索引操作 -* 创建视图 +索引在创建表的时候可以同时创建, 也可以随时增加新的索引 + +* 创建索引:如果一个表中有一列是主键,那么会**默认为其创建主键索引**(主键列不需要单独创建索引) ```mysql - CREATE [OR REPLACE] - VIEW 视图名称 [(列名列表)] - AS 查询语句 - [WITH [CASCADED | LOCAL] CHECK OPTION]; + CREATE [UNIQUE|FULLTEXT] INDEX 索引名称 [USING 索引类型] ON 表名(列名...); + -- 索引类型默认是 B+TREE ``` - `WITH [CASCADED | LOCAL] CHECK OPTION` 决定了是否允许更新数据使记录不再满足视图的条件: +* 查看索引 - * LOCAL:只要满足本视图的条件就可以更新 - * CASCADED:必须满足所有针对该视图的所有视图的条件才可以更新, 默认值 + ```mysql + SHOW INDEX FROM 表名; + ``` -* 例如 +* 添加索引 ```mysql - -- 数据准备 city - id NAME cid - 1 深圳 1 - 2 上海 1 - 3 纽约 2 - 4 莫斯科 3 + -- 单列索引 + ALTER TABLE 表名 ADD INDEX 索引名称(列名); - -- 数据准备 country - id NAME - 1 中国 - 2 美国 - 3 俄罗斯 + -- 组合索引 + ALTER TABLE 表名 ADD INDEX 索引名称(列名1,列名2,...); - -- 创建city_country视图,保存城市和国家的信息(使用指定列名) - CREATE - VIEW - city_country (city_id,city_name,country_name) - AS - SELECT - c1.id, - c1.name, - c2.name - FROM - city c1, - country c2 - WHERE - c1.cid=c2.id; - ``` - + -- 主键索引 + ALTER TABLE 表名 ADD PRIMARY KEY(主键列名); + -- 外键索引(添加外键约束,就是外键索引) + ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主键列名); + + -- 唯一索引 + ALTER TABLE 表名 ADD UNIQUE 索引名称(列名); + + -- 全文索引(mysql只支持文本类型) + ALTER TABLE 表名 ADD FULLTEXT 索引名称(列名); + ``` -*** - +* 删除索引 + ```mysql + DROP INDEX 索引名称 ON 表名; + ``` -#### 视图查询 +* 案例练习 -* 查询所有数据表,视图也会查询出来 + 数据准备:student ```mysql - SHOW TABLES; - SHOW TABLE STATUS [\G]; + id NAME age score + 1 张三 23 99 + 2 李四 24 95 + 3 王五 25 98 + 4 赵六 26 97 ``` -* 查询视图 + 索引操作: ```mysql - SELECT * FROM 视图名称; + -- 为student表中姓名列创建一个普通索引 + CREATE INDEX idx_name ON student(NAME); + + -- 为student表中年龄列创建一个唯一索引 + CREATE UNIQUE INDEX idx_age ON student(age); ``` -* 查询某个视图创建 - ```mysql - SHOW CREATE VIEW 视图名称; - ``` + @@ -3112,39 +3717,17 @@ InnoDB 刷脏页的控制策略: -#### 视图修改 +### 聚簇索引 -视图表数据修改,会**自动修改源表中的数据**,因为更新的是视图中的基表中的数据 +#### 索引对比 -* 修改视图表中的数据 +聚簇索引是一种数据存储方式,并不是一种单独的索引类型 - ```mysql - UPDATE 视图名称 SET 列名 = 值 WHERE 条件; - ``` +* 聚簇索引的叶子节点存放的是主键值和数据行,支持覆盖索引 -* 修改视图的结构 +* 非聚簇索引的叶子节点存放的是主键值或指向数据行的指针(由存储引擎决定) - ```mysql - ALTER [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] - VIEW 视图名称 [(列名列表)] - AS 查询语句 - [WITH [CASCADED | LOCAL] CHECK OPTION] - - -- 将视图中的country_name修改为name - ALTER - VIEW - city_country (city_id,city_name,name) - AS - SELECT - c1.id, - c1.name, - c2.name - FROM - city c1, - country c2 - WHERE - c1.cid=c2.id; - ``` +在 Innodb 下主键索引是聚簇索引,在 MyISAM 下主键索引是非聚簇索引 @@ -3152,46 +3735,45 @@ InnoDB 刷脏页的控制策略: -#### 视图删除 +#### Innodb -* 删除视图 +##### 聚簇索引 - ```mysql - DROP VIEW 视图名称; - ``` +在 Innodb 存储引擎,B+ 树索引可以分为聚簇索引(也称聚集索引、clustered index)和辅助索引(也称非聚簇索引或二级索引、secondary index、non-clustered index) -* 如果存在则删除 +InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,叶子节点中存放的就是整张表的数据,将聚簇索引的叶子节点称为数据页 - ```mysql - DROP VIEW IF EXISTS 视图名称; - ``` +* 这个特性决定了**数据也是索引的一部分**,所以一张表只能有一个聚簇索引 +* 辅助索引的存在不影响聚簇索引中数据的组织,所以一张表可以有多个辅助索引 +聚簇索引的优点: +* 数据访问更快,聚簇索引将索引和数据保存在同一个 B+ 树中,因此从聚簇索引中获取数据比非聚簇索引更快 +* 聚簇索引对于主键的排序查找和范围查找速度非常快 +聚簇索引的缺点: +* 插入速度严重依赖于插入顺序,按照主键的顺序(递增)插入是最快的方式,否则将会出现页分裂,严重影响性能,所以对于 InnoDB 表,一般都会定义一个自增的 ID 列为主键 +* 更新主键的代价很高,将会导致被更新的行移动,所以对于 InnoDB 表,一般定义主键为不可更新 -*** +* 二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据 -### 存储过程 +*** -#### 基本介绍 -存储过程和函数:存储过程和函数是事先经过编译并存储在数据库中的一段 SQL 语句的集合 -存储过程和函数的好处: +##### 辅助索引 -* 提高代码的复用性 -* 减少数据在数据库和应用服务器之间的传输,提高传输效率 -* 减少代码层面的业务处理 -* **一次编译永久有效** +在聚簇索引之上创建的索引称之为辅助索引,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引等 -存储过程和函数的区别: +辅助索引叶子节点存储的是主键值,而不是数据的物理地址,所以访问数据需要二次查找,推荐使用覆盖索引,可以减少回表查询 -* 存储函数必须有返回值 -* 存储过程可以没有返回值 +**检索过程**:辅助索引找到主键值,再通过聚簇索引(二分)找到数据页,最后通过数据页中的 Page Directory(二分)找到对应的数据分组,遍历组内所所有的数据找到数据行 + +补充:无索引走全表查询,查到数据页后和上述步骤一致 @@ -3199,138 +3781,62 @@ InnoDB 刷脏页的控制策略: -#### 基本操作 +##### 索引实现 -DELIMITER: +InnoDB 使用 B+Tree 作为索引结构,并且 InnoDB 一定有索引 -* DELIMITER 关键字用来声明 sql 语句的分隔符,告诉 MySQL 该段命令已经结束 +主键索引: -* MySQL 语句默认的分隔符是分号,但是有时需要一条功能 sql 语句中包含分号,但是并不作为结束标识,这时使用 DELIMITER 来指定分隔符: +* 在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这个索引的 key 是数据表的主键,叶子节点 data 域保存了完整的数据记录 - ```mysql - DELIMITER 分隔符 - ``` +* InnoDB 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个**隐含字段**作为主键,这个字段长度为 6 个字节,类型为长整形(MVCC 部分的笔记提及) -存储过程的创建调用查看和删除: +辅助索引: -* 创建存储过程 +* InnoDB 的所有辅助索引(二级索引)都引用主键作为 data 域 - ```mysql - -- 修改分隔符为$ - DELIMITER $ - - -- 标准语法 - CREATE PROCEDURE 存储过程名称(参数...) - BEGIN - sql语句; - END$ - - -- 修改分隔符为分号 - DELIMITER ; - ``` +* InnoDB 表是基于聚簇索引建立的,因此 InnoDB 的索引能提供一种非常快速的主键查找性能。不过辅助索引也会包含主键列,所以不建议使用过长的字段作为主键,**过长的主索引会令辅助索引变得过大** -* 调用存储过程 +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB聚簇和辅助索引结构.png) - ```mysql - CALL 存储过程名称(实际参数); - ``` -* 查看存储过程 - ```mysql - SELECT * FROM mysql.proc WHERE db='数据库名称'; - ``` +*** -* 删除存储过程 - ```mysql - DROP PROCEDURE [IF EXISTS] 存储过程名称; - ``` -练习: +#### MyISAM -* 数据准备 +##### 非聚簇 - ```mysql - id NAME age gender score - 1 张三 23 男 95 - 2 李四 24 男 98 - 3 王五 25 女 100 - 4 赵六 26 女 90 - ``` +MyISAM 的主键索引使用的是非聚簇索引,索引文件和数据文件是分离的,**索引文件仅保存数据的地址** -* 创建 stu_group() 存储过程,封装分组查询总成绩,并按照总成绩升序排序的功能 +* 主键索引 B+ 树的节点存储了主键,辅助键索引 B+ 树存储了辅助键,表数据存储在独立的地方,这两颗 B+ 树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别 +* 由于索引树是独立的,通过辅助索引检索无需访问主键的索引树回表查询 + +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-聚簇索引和辅助索引检锁数据图.jpg) - ```mysql - DELIMITER $ - - CREATE PROCEDURE stu_group() - BEGIN - SELECT gender,SUM(score) getSum FROM student GROUP BY gender ORDER BY getSum ASC; - END$ - - DELIMITER ; - - -- 调用存储过程 - CALL stu_group(); - -- 删除存储过程 - DROP PROCEDURE IF EXISTS stu_group; - ``` - *** -#### 存储语法 +##### 索引实现 -##### 变量使用 +MyISAM 的索引方式也叫做非聚集的,之所以这么称呼是为了与 InnoDB 的聚集索引区分 -存储过程是可以进行编程的,意味着可以使用变量、表达式、条件控制语句等,来完成比较复杂的功能 +主键索引:MyISAM 引擎使用 B+Tree 作为索引结构,叶节点的 data 域存放的是数据记录的地址 -* 定义变量:DECLARE 定义的是局部变量,只能用在 BEGIN END 范围之内 - - ```mysql - DECLARE 变量名 数据类型 [DEFAULT 默认值]; - ``` - -* 变量的赋值 +辅助索引:MyISAM 中主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求 key 是唯一的,而辅助索引的 key 可以重复 - ```mysql - SET 变量名 = 变量值; - SELECT 列名 INTO 变量名 FROM 表名 [WHERE 条件]; - ``` +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM主键和辅助索引结构.png) -* 数据准备:表 student - ```mysql - id NAME age gender score - 1 张三 23 男 95 - 2 李四 24 男 98 - 3 王五 25 女 100 - 4 赵六 26 女 90 - ``` -* 定义两个 int 变量,用于存储男女同学的总分数 - ```mysql - DELIMITER $ - CREATE PROCEDURE pro_test3() - BEGIN - -- 定义两个变量 - DECLARE men,women INT; - -- 查询男同学的总分数,为men赋值 - SELECT SUM(score) INTO men FROM student WHERE gender='男'; - -- 查询女同学的总分数,为women赋值 - SELECT SUM(score) INTO women FROM student WHERE gender='女'; - -- 使用变量 - SELECT men,women; - END$ - DELIMITER ; - -- 调用存储过程 - CALL pro_test3(); - ``` + +参考文章:https://blog.csdn.net/lm1060891265/article/details/81482136 @@ -3338,51 +3844,27 @@ DELIMITER: -##### IF语句 +### 索引结构 -* if 语句标准语法 +#### 数据页 - ```mysql - IF 判断条件1 THEN 执行的sql语句1; - [ELSEIF 判断条件2 THEN 执行的sql语句2;] - ... - [ELSE 执行的sql语句n;] - END IF; - ``` +文件系统的最小单元是块(block),一个块的大小是 4K,系统从磁盘读取数据到内存时是以磁盘块为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么 -* 数据准备:表 student +InnoDB 存储引擎中有页(Page)的概念,页是 MySQL 磁盘管理的最小单位 - ```mysql - id NAME age gender score - 1 张三 23 男 95 - 2 李四 24 男 98 - 3 王五 25 女 100 - 4 赵六 26 女 90 - ``` - -* 根据总成绩判断:全班 380 分及以上学习优秀、320 ~ 380 学习良好、320 以下学习一般 +* **InnoDB 存储引擎中默认每个页的大小为 16KB,索引中一个节点就是一个数据页**,所以会一次性读取 16KB 的数据到内存 +* InnoDB 引擎将若干个地址连接磁盘块,以此来达到页的大小 16KB +* 在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率 - ```mysql - DELIMITER $ - CREATE PROCEDURE pro_test4() - BEGIN - DECLARE total INT; -- 定义总分数变量 - DECLARE description VARCHAR(10); -- 定义分数描述变量 - SELECT SUM(score) INTO total FROM student; -- 为总分数变量赋值 - -- 判断总分数 - IF total >= 380 THEN - SET description = '学习优秀'; - ELSEIF total >=320 AND total < 380 THEN - SET description = '学习良好'; - ELSE - SET description = '学习一般'; - END IF; - END$ - DELIMITER ; - -- 调用pro_test4存储过程 - CALL pro_test4(); - ``` +数据页物理结构,从上到下: +* File Header:上一页和下一页的指针、该页的类型(索引页、数据页、日志页等)、**校验和**等信息 +* Page Header:记录状态信息 +* Infimum + Supremum:当前页的最小记录和最大记录(头尾指针),Infimum 所在分组只有一条记录,Supremum 所在分组可以有 1 ~ 8 条记录,剩余的分组可以有 4 ~ 8 条记录 +* User Records:存储数据的记录 +* Free Space:尚未使用的存储空间 +* Page Directory:分组的目录,可以通过目录快速定位(二分法)数据的分组 +* File Trailer:检验和字段,在刷脏过程中,页首和页尾的校验和一致才能说明页面刷新成功,二者不同说明刷新期间发生了错误;LSN 字段,也是用来校验页面的完整性 @@ -3390,155 +3872,60 @@ DELIMITER: -##### 参数传递 - -* 参数传递的语法 - - IN:代表输入参数,需要由调用者传递实际数据,默认的 - OUT:代表输出参数,该参数可以作为返回值 - INOUT:代表既可以作为输入参数,也可以作为输出参数 - - ```mysql - DELIMITER $ - - -- 标准语法 - CREATE PROCEDURE 存储过程名称([IN|OUT|INOUT] 参数名 数据类型) - BEGIN - 执行的sql语句; - END$ - - DELIMITER ; - ``` +#### BTree -* 输入总成绩变量,代表学生总成绩,输出分数描述变量,代表学生总成绩的描述 +BTree 的索引类型是基于 B+Tree 树型数据结构的,B+Tree 又是 BTree 数据结构的变种,用在数据库和操作系统中的文件系统,特点是能够保持数据稳定有序 - ```mysql - DELIMITER $ - - CREATE PROCEDURE pro_test6(IN total INT, OUT description VARCHAR(10)) - BEGIN - -- 判断总分数 - IF total >= 380 THEN - SET description = '学习优秀'; - ELSEIF total >= 320 AND total < 380 THEN - SET description = '学习不错'; - ELSE - SET description = '学习一般'; - END IF; - END$ - - DELIMITER ; - -- 调用pro_test6存储过程 - CALL pro_test6(310,@description); - CALL pro_test6((SELECT SUM(score) FROM student), @description); - -- 查询总成绩描述 - SELECT @description; - ``` +BTree 又叫多路平衡搜索树,一颗 m 叉的 BTree 特性如下: -* 查看参数方法 +- 树中每个节点最多包含 m 个孩子 +- 除根节点与叶子节点外,每个节点至少有 [ceil(m/2)] 个孩子 +- 若根节点不是叶子节点,则至少有两个孩子 +- 所有的叶子节点都在同一层 +- 每个非叶子节点由 n 个key与 n+1 个指针组成,其中 [ceil(m/2)-1] <= n <= m-1 - * @变量名 : **用户会话变量**,代表整个会话过程他都是有作用的,类似于全局变量 - * @@变量名 : **系统变量** +5 叉,key 的数量 [ceil(m/2)-1] <= n <= m-1 为 2 <= n <=4 ,当 n>4 时中间节点分裂到父节点,两边节点分裂 - +插入 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 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程1.png) +* 插入 H,n>4,中间元素 G 字母向上分裂到新的节点 -##### CASE + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程2.png) -* 标准语法 1 +* 插入 E、K、Q 不需要分裂 - ```mysql - CASE 表达式 - WHEN 值1 THEN 执行sql语句1; - [WHEN 值2 THEN 执行sql语句2;] - ... - [ELSE 执行sql语句n;] - END CASE; - ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程3.png) -* 标准语法 2 +* 插入 M,中间元素 M 字母向上分裂到父节点 G - ```mysql - sCASE - WHEN 判断条件1 THEN 执行sql语句1; - [WHEN 判断条件2 THEN 执行sql语句2;] - ... - [ELSE 执行sql语句n;] - END CASE; - ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程4.png) -* 演示 +* 插入 F,W,L,T 不需要分裂 - ```mysql - DELIMITER $ - CREATE PROCEDURE pro_test7(IN total INT) - BEGIN - -- 定义变量 - DECLARE description VARCHAR(10); - -- 使用case判断 - CASE - WHEN total >= 380 THEN - SET description = '学习优秀'; - WHEN total >= 320 AND total < 380 THEN - SET description = '学习不错'; - ELSE - SET description = '学习一般'; - END CASE; - - -- 查询分数描述信息 - SELECT description; - END$ - DELIMITER ; - -- 调用pro_test7存储过程 - CALL pro_test7(390); - CALL pro_test7((SELECT SUM(score) FROM student)); - ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程5.png) +* 插入 Z,中间元素 T 向上分裂到父节点中 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程6.png) -*** +* 插入 D,中间元素 D 向上分裂到父节点中,然后插入 P,R,X,Y 不需要分裂 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程7.png) +* 最后插入 S,NPQR 节点 n>5,中间节点 Q 向上分裂,但分裂后父节点 DGMT 的 n>5,中间节点 M 向上分裂 -##### WHILE + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程8.png) -* while 循环语法 +BTree 树就已经构建完成了,BTree 树和二叉树相比, 查询数据的效率更高, 因为对于相同的数据量来说,**BTree 的层级结构比二叉树小**,所以搜索速度快 - ```mysql - WHILE 条件判断语句 DO - 循环体语句; - 条件控制语句; - END WHILE; - ``` - -* 计算 1~100 之间的偶数和 +BTree 结构的数据可以让系统高效的找到数据所在的磁盘块,定义一条记录为一个二元组 [key, data] ,key 为记录的键值,对应表中的主键值,data 为一行记录中除主键外的数据。对于不同的记录,key 值互不相同,BTree 中的每个节点根据实际情况可以包含大量的关键字信息和分支 +![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-BTree.png) - ```mysql - DELIMITER $ - CREATE PROCEDURE pro_test6() - BEGIN - -- 定义求和变量 - DECLARE result INT DEFAULT 0; - -- 定义初始化变量 - DECLARE num INT DEFAULT 1; - -- while循环 - WHILE num <= 100 DO - IF num % 2 = 0 THEN - SET result = result + num; - END IF; - SET num = num + 1; - END WHILE; - -- 查询求和结果 - SELECT result; - END$ - DELIMITER ; - - -- 调用pro_test6存储过程 - CALL pro_test6(); - ``` +缺点:当进行范围查找时会出现回旋查找 @@ -3546,206 +3933,118 @@ DELIMITER: -##### REPEAT +#### B+Tree -* repeat 循环标准语法 +##### 数据结构 - ```mysql - 初始化语句; - REPEAT - 循环体语句; - 条件控制语句; - UNTIL 条件判断语句 - END REPEAT; - ``` +BTree 数据结构中每个节点中不仅包含数据的 key 值,还有 data 值。磁盘中每一页的存储空间是有限的,如果 data 数据较大时将会导致每个节点(即一个页)能存储的 key 的数量很小,当存储的数据量很大时同样会导致 B-Tree 的深度较大,增大查询时的磁盘 I/O 次数,进而影响查询效率,所以引入 B+Tree -* 计算 1~10 之间的和 +B+Tree 为 BTree 的变种,B+Tree 与 BTree 的区别为: - ```mysql - DELIMITER $ - CREATE PROCEDURE pro_test9() - BEGIN - -- 定义求和变量 - DECLARE result INT DEFAULT 0; - -- 定义初始化变量 - DECLARE num INT DEFAULT 1; - -- repeat循环 - REPEAT - -- 累加 - SET result = result + num; - -- 让num+1 - SET num = num + 1; - -- 停止循环 - UNTIL num > 10 - END REPEAT; - -- 查询求和结果 - SELECT result; - END$ - - DELIMITER ; - -- 调用pro_test9存储过程 - CALL pro_test9(); - ``` +* n 叉 B+Tree 最多含有 n 个 key(哈希值),而 BTree 最多含有 n-1 个 key +- 所有**非叶子节点只存储键值 key** 信息,只进行数据索引,使每个非叶子节点所能保存的关键字大大增加 +- 所有**数据都存储在叶子节点**,所以每次数据查询的次数都一样 +- **叶子节点按照 key 大小顺序排列,左边结尾数据都会保存右边节点开始数据的指针,形成一个链表** +- 所有节点中的 key 在叶子节点中也存在(比如 5),key 允许重复,B 树不同节点不存在重复的 key + +B* 树:是 B+ 树的变体,在 B+ 树的非根和非叶子结点再增加指向兄弟的指针 -*** +*** -##### LOOP -LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定义,通常可以使用 LEAVE 语句实现,如果不加退出循环的语句,那么就变成了死循环 -* loop 循环标准语法 +##### 优化结构 - ```mysql - [循环名称:] LOOP - 条件判断语句 - [LEAVE 循环名称;] - 循环体语句; - 条件控制语句; - END LOOP 循环名称; - ``` - -* 计算 1~10 之间的和 +MySQL 索引数据结构对经典的 B+Tree 进行了优化,在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,**提高区间访问的性能,防止回旋查找** - ```mysql - DELIMITER $ - CREATE PROCEDURE pro_test10() - BEGIN - -- 定义求和变量 - DECLARE result INT DEFAULT 0; - -- 定义初始化变量 - DECLARE num INT DEFAULT 1; - -- loop循环 - l:LOOP - -- 条件成立,停止循环 - IF num > 10 THEN - LEAVE l; - END IF; - -- 累加 - SET result = result + num; - -- 让num+1 - SET num = num + 1; - END LOOP l; - -- 查询求和结果 - SELECT result; - END$ - DELIMITER ; - -- 调用pro_test10存储过程 - CALL pro_test10(); - ``` +区间访问的意思是访问索引为 5 - 15 的数据,可以直接根据相邻节点的指针遍历 +B+ 树的**叶子节点是数据页**(page),一个页里面可以存多个数据行 +![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-B+Tree.png) -*** +通常在 B+Tree 上有两个头指针,**一个指向根节点,另一个指向关键字最小的叶子节点**,而且所有叶子节点(即数据节点)之间是一种链式环结构。可以对 B+Tree 进行两种查找运算: +- 有范围:对于主键的范围查找和分页查找 +- 有顺序:从根节点开始,进行随机查找,顺序查找 +InnoDB 中每个数据页的大小默认是 16KB, -##### 游标 +* 索引行:一般表的主键类型为 INT(4 字节)或 BIGINT(8 字节),指针大小在 InnoDB 中设置为 6 字节节,也就是说一个页大概存储 16KB/(8B+6B)=1K 个键值(估值)。则一个深度为 3 的 B+Tree 索引可以维护 `10^3 * 10^3 * 10^3 = 10亿` 条记录 +* 数据行:一行数据的大小可能是 1k,一个数据页可以存储 16 行 -游标是用来存储查询结果集的数据类型,在存储过程和函数中可以使用光标对结果集进行循环的处理 -* 游标可以遍历返回的多行结果,每次拿到一整行数据 -* 简单来说游标就类似于集合的迭代器遍历 -* MySQL 中的游标只能用在存储过程和函数中 +实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree 的高度一般都在 2-4 层。MySQL 的 InnoDB 存储引擎在设计时是**将根节点常驻内存的**,也就是说查找某一键值的行记录时最多只需要 1~3 次磁盘 I/O 操作 -游标的语法 +B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较小 -* 创建游标 - ```mysql - DECLARE 游标名称 CURSOR FOR 查询sql语句; - ``` -* 打开游标 +*** - ```mysql - OPEN 游标名称; - ``` -* 使用游标获取数据 - ```mysql - FETCH 游标名称 INTO 变量名1,变量名2,...; - ``` +##### 索引维护 -* 关闭游标 +B+ 树为了保持索引的有序性,在插入新值的时候需要做相应的维护 - ```mysql - CLOSE 游标名称; - ``` +每个索引中每个块存储在磁盘页中,可能会出现以下两种情况: -* Mysql 通过一个 Error handler 声明来判断指针是否到尾部,并且必须和创建游标的 SQL 语句声明在一起: +* 如果所在的数据页已经满了,这时候需要申请一个新的数据页,然后挪动部分数据过去,这个过程称为**页分裂** +* 当相邻两个页由于删除了数据,利用率很低之后,会将数据页做**页合并**,合并的过程可以认为是分裂过程的逆过程 +* 这两个情况都是由 B+ 树的结构决定的 - ```mysql - DECLARE EXIT HANDLER FOR NOT FOUND (do some action,一般是设置标志变量) - ``` +一般选用数据小的字段做索引,字段长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小 - -游标的基本使用 -* 数据准备:表 student +*** - ```mysql - id NAME age gender score - 1 张三 23 男 95 - 2 李四 24 男 98 - 3 王五 25 女 100 - 4 赵六 26 女 90 - ``` -* 创建 stu_score 表 - ```mysql - CREATE TABLE stu_score( - id INT PRIMARY KEY AUTO_INCREMENT, - score INT - ); - ``` +### 设计原则 -* 将student表中所有的成绩保存到stu_score表中 +索引的设计可以遵循一些已有的原则,创建索引的时候请尽量考虑符合这些原则,便于提升索引的使用效率 - ```mysql - DELIMITER $ +创建索引时的原则: +- 对查询频次较高,且数据量比较大的表建立索引 +- 使用唯一索引,区分度越高,使用索引的效率越高 +- 索引字段的选择,最佳候选列应当从 where 子句的条件中提取,使用覆盖索引 +- 使用短索引,索引创建之后也是使用硬盘来存储的,因此提升索引访问的 I/O 效率,也可以提升总体的访问效率。假如构成索引的字段总长度比较短,那么在给定大小的存储块内可以存储更多的索引值,相应的可以有效的提升 MySQL 访问索引的 I/O 效率 +- 索引可以有效的提升查询数据的效率,但索引数量不是多多益善,索引越多,维护索引的代价越高。对于插入、更新、删除等 DML 操作比较频繁的表来说,索引过多,会引入相当高的维护代价,降低 DML 操作的效率,增加相应操作的时间消耗;另外索引过多的话,MySQL 也会犯选择困难病,虽然最终仍然会找到一个可用的索引,但提高了选择的代价 + +* MySQL 建立联合索引时会遵守**最左前缀匹配原则**,即最左优先,在检索数据时从联合索引的最左边开始匹配 - CREATE PROCEDURE pro_test12() - BEGIN - -- 定义成绩变量 - DECLARE s_score INT; - -- 定义标记变量 - DECLARE flag INT DEFAULT 0; - - -- 创建游标,查询所有学生成绩数据 - DECLARE stu_result CURSOR FOR SELECT score FROM student; - -- 游标结束后,将标记变量改为1 这两个必须声明在一起 - DECLARE EXIT HANDLER FOR NOT FOUND SET flag = 1; - - -- 开启游标 - OPEN stu_result; - -- 循环使用游标 - REPEAT - -- 使用游标,遍历结果,拿到数据 - FETCH stu_result INTO s_score; - -- 将数据保存到stu_score表中 - INSERT INTO stu_score VALUES (NULL,s_score); - UNTIL flag=1 - END REPEAT; - -- 关闭游标 - CLOSE stu_result; - END$ + N 个列组合而成的组合索引,相当于创建了 N 个索引,如果查询时 where 句中使用了组成该索引的**前**几个字段,那么这条查询 SQL 可以利用组合索引来提升查询效率 - DELIMITER ; + ```mysql + -- 对name、address、phone列建一个联合索引 + ALTER TABLE user ADD INDEX index_three(name,address,phone); + -- 查询语句执行时会依照最左前缀匹配原则,检索时分别会使用索引进行数据匹配。 + (name,address,phone) + (name,address) + (name,phone) -- 只有name字段走了索引 + (name) - -- 调用pro_test12存储过程 - CALL pro_test12(); - -- 查询stu_score表 - SELECT * FROM stu_score; + -- 索引的字段可以是任意顺序的,优化器会帮助我们调整顺序,下面的SQL语句可以命中索引 + SELECT * FROM user WHERE address = '北京' AND phone = '12345' AND name = '张三'; ``` - + ```mysql + -- 如果联合索引中最左边的列不包含在条件查询中,SQL语句就不会命中索引,比如: + SELECT * FROM user WHERE address = '北京' AND phone = '12345'; + ``` + +哪些情况不要建立索引: + +* 记录太少的表 +* 经常增删改的表 +* 频繁更新的字段不适合创建索引 +* where 条件里用不到的字段不创建索引 @@ -3753,59 +4052,35 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 -#### 存储函数 +### 索引优化 -存储函数和存储过程是非常相似的,存储函数可以做的事情,存储过程也可以做到 +#### 覆盖索引 -存储函数有返回值,存储过程没有返回值(参数的 out 其实也相当于是返回数据了) +覆盖索引:包含所有满足查询需要的数据的索引(SELECT 后面的字段刚好是索引字段),可以利用该索引返回 SELECT 列表的字段,而不必根据索引去聚簇索引上读取数据文件 -* 创建存储函数 +回表查询:要查找的字段不在非主键索引树上时,需要通过叶子节点的主键值去主键索引上获取对应的行数据 - ```mysql - DELIMITER $ - -- 标准语法 - CREATE FUNCTION 函数名称(参数 数据类型) - RETURNS 返回值类型 - BEGIN - 执行的sql语句; - RETURN 结果; - END$ - - DELIMITER ; - ``` +使用覆盖索引,防止回表查询: -* 调用存储函数,因为有返回值,所以使用 SELECT 调用 +* 表 user 主键为 id,普通索引为 age,查询语句: ```mysql - SELECT 函数名称(实际参数); + SELECT * FROM user WHERE age = 30; ``` -* 删除存储函数 - - ```mysql - DROP FUNCTION 函数名称; - ``` + 查询过程:先通过普通索引 age=30 定位到主键值 id=1,再通过聚集索引 id=1 定位到行记录数据,需要两次扫描 B+ 树 -* 定义存储函数,获取学生表中成绩大于95分的学生数量 +* 使用覆盖索引: ```mysql - DELIMITER $ - CREATE FUNCTION fun_test() - RETURN INT - BEGIN - -- 定义统计变量 - DECLARE result INT; - -- 查询成绩大于95分的学生数量,给统计变量赋值 - SELECT COUNT(score) INTO result FROM student WHERE score > 95; - -- 返回统计结果 - SELECT result; - END - DELIMITER ; - -- 调用fun_test存储函数 - SELECT fun_test(); + DROP INDEX idx_age ON user; + CREATE INDEX idx_age_name ON user(age,name); + SELECT id,age FROM user WHERE age = 30; ``` + 在一棵索引树上就能获取查询所需的数据,无需回表速度更快 +使用覆盖索引,要注意 SELECT 列表中只取出需要的列,不可用 SELECT *,所有字段一起做索引会导致索引文件过大,查询性能下降 @@ -3813,346 +4088,319 @@ LOOP 实现简单的循环,退出循环的条件需要使用其他的语句定 -### 触发器 +#### 索引下推 -#### 基本介绍 +索引条件下推优化(Index Condition Pushdown)是 MySQL5.6 添加,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数 -触发器是与表有关的数据库对象,在 insert/update/delete 之前或之后触发并执行触发器中定义的 SQL 语句 +索引下推充分利用了索引中的数据,在查询出整行数据之前过滤掉无效的数据,再去主键索引树上查找 -* 触发器的这种特性可以协助应用在数据库端确保数据的完整性 、日志记录 、数据校验等操作 +* 不使用索引下推优化时存储引擎通过索引检索到数据返回给 MySQL 服务器,服务器判断数据是否符合条件,符合条件的数据去聚簇索引回表查询,获取完整的数据 -- 使用别名 NEW 和 OLD 来引用触发器中发生变化的记录内容,这与其他的数据库是相似的 -- 现在触发器还只支持行级触发,不支持语句级触发 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-不使用索引下推.png) +* 使用索引下推优化时,如果**存在某些被索引的列的判断条件**时,MySQL 服务器将这一部分判断条件传递给存储引擎,由存储引擎在索引遍历的过程中判断数据是否符合传递的条件,将符合条件的数据进行回表,检索出来返回给服务器,由此减少 IO 次数 -| 触发器类型 | OLD的含义 | NEW的含义 | -| --------------- | ------------------------------ | ------------------------------ | -| INSERT 型触发器 | 无 (因为插入前状态无数据) | NEW 表示将要或者已经新增的数据 | -| UPDATE 型触发器 | OLD 表示修改之前的数据 | NEW 表示将要或已经修改后的数据 | -| DELETE 型触发器 | OLD 表示将要或者已经删除的数据 | 无 (因为删除后状态无数据) | + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-使用索引下推.png) +**适用条件**: +* 需要存储引擎将索引中的数据与条件进行判断(所以**条件列必须都在同一个索引中**),所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM +* 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 +* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了,索引下推的目的减少 IO 次数也就失去了意义 -*** +工作过程:用户表 user,(name, age) 是联合索引 +```mysql +SELECT * FROM user WHERE name LIKE '张%' AND age = 10; -- 头部模糊匹配会造成索引失效 +``` +* 优化前:在非主键索引树上找到满足第一个条件的行,然后通过叶子节点记录的主键值再回到主键索引树上查找到对应的行数据,再对比 AND 后的条件是否符合,符合返回数据,需要 4 次回表 -#### 基本操作 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引下推优化1.png) -* 创建触发器 +* 优化后:检查索引中存储的列信息是否符合索引条件,然后交由存储引擎用剩余的判断条件判断此行数据是否符合要求,**不满足条件的不去读取表中的数据**,满足下推条件的就根据主键值进行回表查询,2 次回表 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引下推优化2.png) - ```mysql - DELIMITER $ - - CREATE TRIGGER 触发器名称 - BEFORE|AFTER INSERT|UPDATE|DELETE - ON 表名 - [FOR EACH ROW] -- 行级触发器 - BEGIN - 触发器要执行的功能; - END$ - - DELIMITER ; - ``` +当使用 EXPLAIN 进行分析时,如果使用了索引条件下推,Extra 会显示 Using index condition -* 查看触发器的状态、语法等信息 - ```mysql - SHOW TRIGGERS; - ``` -* 删除触发器,如果没有指定 schema_name,默认为当前数据库 +参考文章:https://blog.csdn.net/sinat_29774479/article/details/103470244 + +参考文章:https://time.geekbang.org/column/article/69636 - ```mysql - DROP TRIGGER [schema_name.]trigger_name; - ``` - *** -#### 触发演示 +#### 前缀索引 -通过触发器记录账户表的数据变更日志。包含:增加、修改、删除 +当要索引的列字符很多时,索引会变大变慢,可以只索引列开始的部分字符串,节约索引空间,提高索引效率 -* 数据准备 +注意:使用前缀索引就系统就忽略覆盖索引对查询性能的优化了 - ```mysql - -- 创建db9数据库 - CREATE DATABASE db9; - -- 使用db9数据库 - USE db9; - ``` +优化原则:**降低重复的索引值** - ```mysql - -- 创建账户表account - CREATE TABLE account( - id INT PRIMARY KEY AUTO_INCREMENT, -- 账户id - NAME VARCHAR(20), -- 姓名 - money DOUBLE -- 余额 - ); - -- 添加数据 - INSERT INTO account VALUES (NULL,'张三',1000),(NULL,'李四',2000); - ``` +比如地区表: - ```mysql - -- 创建日志表account_log - CREATE TABLE account_log( - id INT PRIMARY KEY AUTO_INCREMENT, -- 日志id - operation VARCHAR(20), -- 操作类型 (insert update delete) - operation_time DATETIME, -- 操作时间 - operation_id INT, -- 操作表的id - operation_params VARCHAR(200) -- 操作参数 - ); - ``` +```mysql +area gdp code +chinaShanghai 100 aaa +chinaDalian 200 bbb +usaNewYork 300 ccc +chinaFuxin 400 ddd +chinaBeijing 500 eee +``` -* 创建 INSERT 型触发器 +发现 area 字段很多都是以 china 开头的,那么如果以前 1-5 位字符做前缀索引就会出现大量索引值重复的情况,索引值重复性越低,查询效率也就越高,所以需要建立前 6 位字符的索引: - ```mysql - DELIMITER $ - - CREATE TRIGGER account_insert - AFTER INSERT - ON account - FOR EACH ROW - BEGIN - INSERT INTO account_log VALUES (NULL,'INSERT',NOW(),new.id,CONCAT('插入后{id=',new.id,',name=',new.name,',money=',new.money,'}')); - END$ - - DELIMITER ; +```mysql +CREATE INDEX idx_area ON table_name(area(7)); +``` + +场景:存储身份证 + +* 直接创建完整索引,这样可能比较占用空间 +* 创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引 +* 倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题(前 6 位相同的很多) +* 创建 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描 + + + +**** + + + +#### 索引合并 + +使用多个索引来完成一次查询的执行方法叫做索引合并 index merge + +* Intersection 索引合并: + + ```sql + SELECT * FROM table_test WHERE key1 = 'a' AND key3 = 'b'; # key1 和 key3 列都是单列索引、二级索引 ``` - ```mysql - -- 向account表添加记录 - INSERT INTO account VALUES (NULL,'王五',3000); - - -- 查询日志表 - SELECT * FROM account_log; - /* - id operation operation_time operation_id operation_params - 1 INSERT 2021-01-26 19:51:11 3 插入后{id=3,name=王五money=2000} - */ + 从不同索引中扫描到的记录的 id 值取**交集**(相同 id),然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 + +* Union 索引合并: + + ```sql + SELECT * FROM table_test WHERE key1 = 'a' OR key3 = 'b'; ``` - + 从不同索引中扫描到的记录的 id 值取**并集**,然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 -* 创建 UPDATE 型触发器 +* Sort-Union 索引合并 - ```mysql - DELIMITER $ - - CREATE TRIGGER account_update - AFTER UPDATE - ON account - FOR EACH ROW - BEGIN - INSERT INTO account_log VALUES (NULL,'UPDATE',NOW(),new.id,CONCAT('修改前{id=',old.id,',name=',old.name,',money=',old.money,'}','修改后{id=',new.id,',name=',new.name,',money=',new.money,'}')); - END$ - - DELIMITER ; + ```sql + SELECT * FROM table_test WHERE key1 < 'a' OR key3 > 'b'; ``` + 先将从不同索引中扫描到的记录的主键值进行排序,再按照 Union 索引合并的方式进行查询 + + + + + + + +*** + + + + + +## 系统优化 + +### 优化步骤 + +#### 执行频率 + +随着生产数据量的急剧增长,很多 SQL 语句逐渐显露出性能问题,对生产的影响也越来越大,此时有问题的 SQL 语句就成为整个系统性能的瓶颈,因此必须要进行优化 + +MySQL 客户端连接成功后,查询服务器状态信息: + +```mysql +SHOW [SESSION|GLOBAL] STATUS LIKE ''; +-- SESSION: 显示当前会话连接的统计结果,默认参数 +-- GLOBAL: 显示自数据库上次启动至今的统计结果 +``` + +* 查看SQL执行频率: + ```mysql - -- 修改account表 - UPDATE account SET money=3500 WHERE id=3; - - -- 查询日志表 - SELECT * FROM account_log; - /* - id operation operation_time operation_id operation_params - 2 UPDATE 2021-01-26 19:58:54 2 更新前{id=2,name=李四money=1000} - 更新后{id=2,name=李四money=200} - */ + SHOW STATUS LIKE 'Com_____'; ``` - + Com_xxx 表示每种语句执行的次数 -* 创建 DELETE 型触发器 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL语句执行频率.png) - ```mysql - DELIMITER $ - - CREATE TRIGGER account_delete - AFTER DELETE - ON account - FOR EACH ROW - BEGIN - INSERT INTO account_log VALUES (NULL,'DELETE',NOW(),old.id,CONCAT('删除前{id=',old.id,',name=',old.name,',money=',old.money,'}')); - END$ - - DELIMITER ; - ``` +* 查询 SQL 语句影响的行数: ```mysql - -- 删除account表数据 - DELETE FROM account WHERE id=3; - - -- 查询日志表 - SELECT * FROM account_log; - /* - id operation operation_time operation_id operation_params - 3 DELETE 2021-01-26 20:02:48 3 删除前{id=3,name=王五money=2000} - */ + SHOW STATUS LIKE 'Innodb_rows_%'; ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL语句影响的行数.png) +Com_xxxx:这些参数对于所有存储引擎的表操作都会进行累计 +Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算法也略有不同 +| 参数 | 含义 | +| :------------------- | ------------------------------------------------------------ | +| Com_select | 执行 SELECT 操作的次数,一次查询只累加 1 | +| Com_insert | 执行 INSERT 操作的次数,对于批量插入的 INSERT 操作,只累加一次 | +| Com_update | 执行 UPDATE 操作的次数 | +| Com_delete | 执行 DELETE 操作的次数 | +| Innodb_rows_read | 执行 SELECT 查询返回的行数 | +| Innodb_rows_inserted | 执行 INSERT 操作插入的行数 | +| Innodb_rows_updated | 执行 UPDATE 操作更新的行数 | +| Innodb_rows_deleted | 执行 DELETE 操作删除的行数 | +| Connections | 试图连接 MySQL 服务器的次数 | +| Uptime | 服务器工作时间 | +| Slow_queries | 慢查询的次数 | -*** +**** -## 存储引擎 +#### 定位低效 -### 基本介绍 +SQL 执行慢有两种情况: -对比其他数据库,MySQL 的架构可以在不同场景应用并发挥良好作用,主要体现在存储引擎,插件式的存储引擎架构将查询处理和其他的系统任务以及数据的存储提取分离,可以针对不同的存储需求可以选择最优的存储引擎 +* 偶尔慢:DB 在刷新脏页 + * redo log 写满了 + * 内存不够用,要从 LRU 链表中淘汰 + * MySQL 认为系统空闲的时候 + * MySQL 关闭时 +* 一直慢的原因:索引没有设计好、SQL 语句没写好、MySQL 选错了索引 -存储引擎的介绍: +通过以下两种方式定位执行效率较低的 SQL 语句 -- MySQL 数据库使用不同的机制存取表文件 , 机制的差别在于不同的存储方式、索引技巧、锁定水平等不同的功能和能力,在 MySQL 中,将这些不同的技术及配套的功能称为存储引擎 -- Oracle、SqlServer 等数据库只有一种存储引擎,MySQL **提供了插件式的存储引擎架构**,所以 MySQL 存在多种存储引擎 , 就会让数据库采取了不同的处理数据的方式和扩展功能 -- 在关系型数据库中数据的存储是以表的形式存进行,所以存储引擎也称为表类型(存储和操作此表的类型) -- 通过选择不同的引擎,能够获取最佳的方案, 也能够获得额外的速度或者功能,提高程序的整体效果。 +* 慢日志查询: 慢查询日志在查询结束以后才记录,执行效率出现问题时查询日志并不能定位问题 -MySQL 支持的存储引擎: + 配置文件修改:修改 .cnf 文件 `vim /etc/mysql/my.cnf`,重启 MySQL 服务器 -- MySQL 支持的引擎包括:InnoDB、MyISAM、MEMORY、Archive、Federate、CSV、BLACKHOLE 等 -- MySQL5.5 之前的默认存储引擎是 MyISAM,5.5 之后就改为了 InnoDB + ```sh + slow_query_log=ON + slow_query_log_file=/usr/local/mysql/var/localhost-slow.log + long_query_time=1 #记录超过long_query_time秒的SQL语句的日志 + log-queries-not-using-indexes = 1 + ``` + 使用命令配置: + ```mysql + mysql> SET slow_query_log=ON; + mysql> SET GLOBAL slow_query_log=ON; + ``` -**** + 查看是否配置成功: + ```mysql + SHOW VARIABLES LIKE '%query%' + ``` +* SHOW PROCESSLIST:**实时查看**当前 MySQL 在进行的连接线程,包括线程的状态、是否锁表、SQL 的执行情况,同时对一些锁表操作进行优化 -### 引擎对比 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SHOW PROCESSLIST命令.png) -MyISAM 存储引擎: -* 特点:不支持事务和外键,读取速度快,节约资源 -* 应用场景:查询和插入操作为主,只有很少更新和删除操作,并对事务的完整性、并发性要求不高 -* 存储方式: - * 每个 MyISAM 在磁盘上存储成 3 个文件,其文件名都和表名相同,拓展名不同 - * 表的定义保存在 .frm 文件,表数据保存在 .MYD (MYData) 文件中,索引保存在 .MYI (MYIndex) 文件中 -InnoDB 存储引擎:(MySQL5.5 版本后默认的存储引擎) -- 特点:**支持事务**和外键操作,支持并发控制。对比 MyISAM 的存储引擎,InnoDB 写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引 -- 应用场景:对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,读写频繁的操作 -- 存储方式: - - 使用共享表空间存储, 这种方式创建的表的表结构保存在 .frm 文件中, 数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path 定义的表空间中,可以是多个文件 - - 使用多表空间存储,创建的表的表结构存在 .frm 文件中,每个表的数据和索引单独保存在 .ibd 中 -MEMORY 存储引擎: -- 特点:每个 MEMORY 表实际对应一个磁盘文件 ,该文件中只存储表的结构,表数据保存在内存中,且默认使用 HASH 索引,这样有利于数据的快速处理,在需要快速定位记录可以提供更快的访问,但是服务一旦关闭,表中的数据就会丢失,数据存储不安全 -- 应用场景:通常用于更新不太频繁的小表,用以快速得到访问结果,类似缓存 -- 存储方式:表结构保存在 .frm 中 +*** -MERGE存储引擎: -* 特点: - * 是一组 MyISAM 表的组合,这些 MyISAM 表必须结构完全相同,通过将不同的表分布在多个磁盘上 - * MERGE 表本身并没有存储数据,对 MERGE 类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的 MyISAM 表进行的 +#### EXPLAIN -* 应用场景:将一系列等同的 MyISAM 表以逻辑方式组合在一起,并作为一个对象引用他们,适合做数据仓库 +##### 执行计划 -* 操作方式: +通过 EXPLAIN 命令获取执行 SQL 语句的信息,包括在 SELECT 语句执行过程中如何连接和连接的顺序,执行计划在优化器优化完成后、执行器之前生成,然后执行器会调用存储引擎检索数据 - * 插入操作是通过 INSERT_METHOD 子句定义插入的表,使用 FIRST 或 LAST 值使得插入操作被相应地作用在第一或者最后一个表上;不定义这个子句或者定义为 NO,表示不能对 MERGE 表执行插入操作 - * 对 MERGE 表进行 DROP 操作,但是这个操作只是删除 MERGE 表的定义,对内部的表是没有任何影响的 +查询 SQL 语句的执行计划: - ```mysql - CREATE TABLE order_1( - )ENGINE = MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE order_2( - )ENGINE = MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE order_all( - -- 结构与MyISAM表相同 - )ENGINE = MERGE UNION = (order_1,order_2) INSERT_METHOD=LAST DEFAULT CHARSET=utf8; - ``` +```mysql +EXPLAIN SELECT * FROM table_1 WHERE id = 1; +``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MERGE.png) +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain查询SQL语句的执行计划.png) -| 特性 | MyISAM | InnoDB | MEMORY | -| ------------ | ---------------------------- | ------------- | ------------------ | -| 存储限制 | 有(平台对文件系统大小的限制) | 64TB | 有(平台的内存限制) | -| **事务安全** | **不支持** | **支持** | **不支持** | -| **锁机制** | **表锁** | **表锁/行锁** | **表锁** | -| B+Tree索引 | 支持 | 支持 | 支持 | -| 哈希索引 | 不支持 | 不支持 | 支持 | -| 全文索引 | 支持 | 支持 | 不支持 | -| 集群索引 | 不支持 | 支持 | 不支持 | -| 数据索引 | 不支持 | 支持 | 支持 | -| 数据缓存 | 不支持 | 支持 | N/A | -| 索引缓存 | 支持 | 支持 | N/A | -| 数据可压缩 | 支持 | 不支持 | 不支持 | -| 空间使用 | 低 | 高 | N/A | -| 内存使用 | 低 | 高 | 中等 | -| 批量插入速度 | 高 | 低 | 高 | -| **外键** | **不支持** | **支持** | **不支持** | +| 字段 | 含义 | +| ------------- | ------------------------------------------------------------ | +| id | SELECT 的序列号 | +| select_type | 表示 SELECT 的类型 | +| table | 访问数据库中表名称,有时可能是简称或者临时表名称() | +| type | 表示表的连接类型 | +| possible_keys | 表示查询时,可能使用的索引 | +| key | 表示实际使用的索引 | +| key_len | 索引字段的长度 | +| ref | 表示与索引列进行等值匹配的对象,常数、某个列、函数等,type 必须在(range, const] 之间,左闭右开 | +| rows | 扫描出的行数,表示 MySQL 根据表统计信息及索引选用情况,**估算**的找到所需的记录扫描的行数 | +| filtered | 条件过滤的行百分比,单表查询没意义,用于连接查询中对驱动表的扇出进行过滤,查询优化器预测所有扇出值满足剩余查询条件的百分比,相乘以后表示多表查询中还要对被驱动执行查询的次数 | +| extra | 执行情况的说明和描述 | -面试问题:MyIsam 和 InnoDB 的区别? +MySQL **执行计划的局限**: -* 事务:InnoDB 支持事务,MyISAM 不支持事务 -* 外键:InnoDB 支持外键,MyISAM 不支持外键 -* 索引:InnoDB 是聚集(聚簇)索引,MyISAM 是非聚集(非聚簇)索引 +* 只是计划,不是执行 SQL 语句,可以随着底层优化器输入的更改而更改 +* EXPLAIN 不会告诉显示关于触发器、存储过程的信息对查询的影响情况 +* EXPLAIN 不考虑各种 Cache +* EXPLAIN 不能显示 MySQL 在执行查询时的动态,因为执行计划在执行查询之前生成 +* EXPALIN 部分统计信息是估算的,并非精确值 +* EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看执行计划 +* EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句实际的执行计划不同 -* 锁粒度:InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁 -* 存储结构:参考本节上半部分 +SHOW WARINGS:在使用 EXPALIN 命令后执行该语句,可以查询与执行计划相关的拓展信息,展示出 Level、Code、Message 三个字段,当 Code 为 1003 时,Message 字段展示的信息类似于将查询语句重写后的信息,但是不是等价,不能执行复制过来运行 +环境准备: +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-执行计划环境准备.png) -*** -### 引擎操作 -* 查询数据库支持的存储引擎 +*** - ```mysql - SHOW ENGINES; - SHOW VARIABLES LIKE '%storage_engine%'; -- 查看Mysql数据库默认的存储引擎 - ``` -* 查询某个数据库中所有数据表的存储引擎 - ```mysql - SHOW TABLE STATUS FROM 数据库名称; - ``` +##### id -* 查询某个数据库中某个数据表的存储引擎 +id 代表 SQL 执行的顺序的标识,每个 SELECT 关键字对应一个唯一 id,所以在同一个 SELECT 关键字中的表的 id 都是相同的。SELECT 后的 FROM 可以跟随多个表,每个表都会对应一条记录,这些记录的 id 都是相同的, + +* id 相同时,执行顺序由上至下。连接查询的执行计划,记录的 id 值都是相同的,出现在前面的表为驱动表,后面为被驱动表 ```mysql - SHOW TABLE STATUS FROM 数据库名称 WHERE NAME = '数据表名称'; + 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) + +* id 不同时,id 值越大优先级越高,越先被执行 ```mysql - CREATE TABLE 表名( - 列名,数据类型, - ... - )ENGINE = 引擎名称; + 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) + +* id 有相同也有不同时,id 相同的可以认为是一组,从上往下顺序执行;在所有的组中,id 的值越大的组,优先级越高,越先执行 ```mysql - ALTER TABLE 表名 ENGINE = 引擎名称; + 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) - +* id 为 NULL 时代表的是临时表 @@ -4160,69 +4408,76 @@ MERGE存储引擎: +##### select +表示查询中每个 select 子句的类型(简单 OR 复杂) -## 索引机制 +| select_type | 含义 | +| ------------------ | ------------------------------------------------------------ | +| SIMPLE | 简单的 SELECT 查询,查询中不包含子查询或者 UNION | +| PRIMARY | 查询中若包含任何复杂的子查询,最外层(也就是最左侧)查询标记为该标识 | +| UNION | 对于 UNION 或者 UNION ALL 的复杂查询,除了最左侧的查询,其余的小查询都是 UNION | +| UNION RESULT | UNION 需要使用临时表进行去重,临时表的是 UNION RESULT | +| DEPENDENT UNION | 对于 UNION 或者 UNION ALL 的复杂查询,如果各个小查询都依赖外层查询,是相关子查询,除了最左侧的小查询为 DEPENDENT SUBQUERY,其余都是 DEPENDENT UNION | +| SUBQUERY | 子查询不是相关子查询,该子查询第一个 SELECT 代表的查询就是这种类型,会进行物化(该子查询只需要执行一次) | +| DEPENDENT SUBQUERY | 子查询是相关子查询,该子查询第一个 SELECT 代表的查询就是这种类型,不会物化(该子查询需要执行多次) | +| DERIVED | 在 FROM 列表中包含的子查询,被标记为 DERIVED(衍生),也就是生成物化派生表的这个子查询 | +| MATERIALIZED | 将子查询物化后与与外层进行连接查询,生成物化表的子查询 | -### 索引介绍 +子查询为 DERIVED:`SELECT * FROM (SELECT key1 FROM t1) AS derived_1 WHERE key1 > 10` -#### 基本介绍 +子查询为 MATERIALIZED:`SELECT * FROM t1 WHERE key1 IN (SELECT key1 FROM t2)` -MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,**本质是排好序的快速查找数据结构。**在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。 -**索引是在存储引擎层实现的**,所以并没有统一的索引标准,即不同存储引擎的索引的工作方式并不一样 -索引使用:一张数据表,用于保存数据;一个索引配置文件,用于保存索引;每个索引都指向了某一个数据 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引的介绍.png) +**** -左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的)。为了加快 Col2 的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据的物理地址的指针,这样就可以运用二叉查找快速获取到相应数据 -索引的优点: -* 类似于书籍的目录索引,提高数据检索的效率,降低数据库的 IO 成本 -* 通过索引列对数据进行排序,降低数据排序的成本,降低 CPU 的消耗 +##### type -索引的缺点: +对表的访问方式,表示 MySQL 在表中找到所需行的方式,又称访问类型 -* 一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式**存储在磁盘**上 -* 虽然索引大大提高了查询效率,同时却也降低更新表的速度。对表进行 INSERT、UPDATE、DELETE 操作,MySQL 不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,还会调整因为更新所带来的键值变化后的索引信息,**但是更新数据也需要先从数据库中获取**,索引加快了获取速度,所以可以相互抵消一下。 -* 索引会影响到 WHERE 的查询条件和排序 ORDER BY 两大功能 +| type | 含义 | +| --------------- | ------------------------------------------------------------ | +| ALL | 全表扫描,如果是 InnoDB 引擎是扫描聚簇索引 | +| index | 可以使用覆盖索引,但需要扫描全部索引 | +| range | 索引范围扫描,常见于 between、<、> 等的查询 | +| index_subquery | 子查询可以普通索引,则子查询的 type 为 index_subquery | +| unique_subquery | 子查询可以使用主键或唯一二级索引,则子查询的 type 为 index_subquery | +| index_merge | 索引合并 | +| ref_or_null | 非唯一性索引(普通二级索引)并且可以存储 NULL,进行等值匹配 | +| ref | 非唯一性索引与常量等值匹配 | +| eq_ref | 唯一性索引(主键或不存储 NULL 的唯一二级索引)进行等值匹配,如果二级索引是联合索引,那么所有联合的列都要进行等值匹配 | +| const | 通过主键或者唯一二级索引与常量进行等值匹配 | +| system | system 是 const 类型的特例,当查询的表只有一条记录的情况下,使用 system | +| NULL | MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引 | +从上到下,性能从差到好,一般来说需要保证查询至少达到 range 级别, 最好达到 ref -*** +*** -#### 索引分类 -索引一般的分类如下: +##### key -- 功能分类 - - 单列索引:一个索引只包含单个列,一个表可以有多个单列索引(普通索引) - - 联合索引:顾名思义,就是将单列索引进行组合 - - 唯一索引:索引列的值必须唯一,允许有空值。如果是联合索引,则列值组合必须唯一 - - 主键索引:一种特殊的唯一索引,不允许有空值,一般在建表时同时创建主键索引 - - 外键索引:只有 InnoDB 引擎支持外键索引,用来保证数据的一致性、完整性和实现级联操作 +possible_keys: -- 结构分类 - - BTree 索引:MySQL 使用最频繁的一个索引数据结构,是 InnoDB 和 MyISAM 存储引擎默认的索引类型,底层基于 B+Tree - - Hash 索引:MySQL中 Memory 存储引擎默认支持的索引类型 - - R-tree 索引(空间索引):空间索引是 MyISAM 引擎的一个特殊索引类型,主要用于地理空间数据类型 - - Full-text 索引(全文索引):快速匹配全部文档的方式。MyISAM 支持, InnoDB 不支持 FULLTEXT 类型的索引,但是 InnoDB 可以使用 sphinx 插件支持全文索引,MEMORY 引擎不支持 - - | 索引 | InnoDB | MyISAM | Memory | - | --------- | ---------------- | ------ | ------ | - | BTREE | 支持 | 支持 | 支持 | - | HASH | 不支持 | 不支持 | 支持 | - | R-tree | 不支持 | 支持 | 不支持 | - | Full-text | 5.6 版本之后支持 | 支持 | 不支持 | +* 指出 MySQL 能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用 +* 如果该列是 NULL,则没有相关的索引 -联合索引图示:根据身高年龄建立的组合索引(height,age) +key: -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-组合索引图.png) +* 显示 MySQL 在查询中实际使用的索引,若没有使用索引,显示为 NULL +* 查询中若使用了**覆盖索引**,则该索引可能出现在 key 列表,不出现在 possible_keys +key_len: +* 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度 +* key_len 显示的值为索引字段的最大可能长度,并非实际使用长度,即 key_len 是根据表定义计算而得,不是通过表内检索出的 +* 在不损失精确性的前提下,长度越短越好 @@ -4230,207 +4485,289 @@ MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获 -### 聚簇索引 +##### Extra -#### 索引对比 +其他的额外的执行计划信息,在该列展示: -聚簇索引是一种数据存储方式,并不是一种单独的索引类型 +* No tables used:查询语句中使用 FROM dual 或者没有 FROM 语句 +* Impossible WHERE:查询语句中的 WHERE 子句条件永远为 FALSE,会导致没有符合条件的行 +* Using index:该值表示相应的 SELECT 操作中使用了**覆盖索引**(Covering Index) +* Using index condition:第一种情况是搜索条件中虽然出现了索引列,但是部分条件无法形成扫描区间(**索引失效**),会根据可用索引的条件先搜索一遍再匹配无法使用索引的条件,回表查询数据;第二种是使用了**索引条件下推**优化 +* Using where:搜索条件需要在 Server 层判断,判断后执行回表操作查询,无法使用索引下推 +* Using join buffer:连接查询被驱动表无法利用索引,需要连接缓冲区来存储中间结果 +* Using filesort:无法利用索引完成排序(优化方向),需要对数据使用外部排序算法,将取得的数据在内存或磁盘中进行排序 +* Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序、去重、分组等场景 +* Select tables optimized away:说明仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 +* No tables used:Query 语句中使用 from dual 或不含任何 from 子句 -* 聚簇索引的叶子节点存放的是主键值和数据行,支持覆盖索引 -* 非聚簇索引的叶子节点存放的是主键值或指向数据行的指针(由存储引擎决定) -在 Innodb 下主键索引是聚簇索引,在 MyISAM 下主键索引是非聚簇索引 +参考文章:https://www.cnblogs.com/ggjucheng/archive/2012/11/11/2765237.html -*** +**** -#### Innodb +#### PROFILES -##### 聚簇索引 +SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的**资源消耗**情况 -在 Innodb 存储引擎,B+ 树索引可以分为聚簇索引(也称聚集索引、clustered index)和辅助索引(也称非聚簇索引或二级索引、secondary index、non-clustered index) +* 通过 have_profiling 参数,能够看到当前 MySQL 是否支持 profile: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-have_profiling.png) -InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,叶子节点中存放的就是整张表的数据,将聚簇索引的叶子节点称为数据页 +* 默认 profiling 是关闭的,可以通过 set 语句在 Session 级别开启 profiling: -* 这个特性决定了**数据也是索引的一部分**,所以一张表只能有一个聚簇索引 -* 辅助索引的存在不影响聚簇索引中数据的组织,所以一张表可以有多个辅助索引 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-profiling.png) -聚簇索引的优点: + ```mysql + SET profiling=1; #开启profiling 开关; + ``` -* 数据访问更快,聚簇索引将索引和数据保存在同一个 B+ 树中,因此从聚簇索引中获取数据比非聚簇索引更快 -* 聚簇索引对于主键的排序查找和范围查找速度非常快 +* 执行 SHOW PROFILES 指令, 来查看 SQL 语句执行的耗时: -聚簇索引的缺点: + ```mysql + SHOW PROFILES; + ``` -* 插入速度严重依赖于插入顺序,按照主键的顺序(递增)插入是最快的方式,否则将会出现页分裂,严重影响性能,所以对于 InnoDB 表,一般都会定义一个自增的 ID 列为主键 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查看SQL语句执行耗时.png) -* 更新主键的代价很高,将会导致被更新的行移动,所以对于 InnoDB 表,一般定义主键为不可更新 +* 查看到该 SQL 执行过程中每个线程的状态和消耗的时间: -* 二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据 + ```mysql + SHOW PROFILE FOR QUERY query_id; + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL执行每个状态消耗的时间.png) + **Sending data 状态**表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅是返回给客户端。由于在 Sending data 状态下,MySQL 线程需要做大量磁盘读取操作,所以是整个查询中耗时最长的状态。 +* 在获取到最消耗时间的线程状态后,MySQL 支持选择 all、cpu、block io 、context switch、page faults 等类型查看 MySQL 在使用什么资源上耗费了过高的时间。例如,选择查看 CPU 的耗费时间: -*** + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL执行每个状态消耗的CPU.png) + * Status:SQL 语句执行的状态 + * Durationsql:执行过程中每一个步骤的耗时 + * CPU_user:当前用户占有的 CPU + * CPU_system:系统占有的 CPU -##### 辅助索引 -在聚簇索引之上创建的索引称之为辅助索引,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引等 +*** -辅助索引叶子节点存储的是主键值,而不是数据的物理地址,所以访问数据需要二次查找,推荐使用覆盖索引,可以减少回表查询 -**检索过程**:辅助索引找到主键值,再通过聚簇索引(二分)找到数据页,最后通过数据页中的 Page Directory(二分)找到对应的数据分组,遍历组内所所有的数据找到数据行 -补充:无索引走全表查询,查到数据页后和上述步骤一致 +#### TRACE +MySQL 提供了对 SQL 的跟踪, 通过 trace 文件可以查看优化器生成执行计划的过程 +* 打开 trace 功能,设置格式为 JSON,并设置 trace 的最大使用内存,避免解析过程中因默认内存过小而不能够完整展示 -*** + ```mysql + SET optimizer_trace="enabled=on",end_markers_in_json=ON; -- 会话内有效 + SET optimizer_trace_max_mem_size=1000000; + ``` +* 执行 SQL 语句: + ```mysql + SELECT * FROM tb_item WHERE id < 4; + ``` -##### 索引实现 +* 检查 information_schema.optimizer_trace: -InnoDB 使用 B+Tree 作为索引结构,并且 InnoDB 一定有索引 + ```mysql + SELECT * FROM information_schema.optimizer_trace \G; -- \G代表竖列展示 + ``` + + 执行信息主要有三个阶段:prepare 阶段、optimize 阶段(成本分析)、execute 阶段(执行) -主键索引: -* 在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这个索引的 key 是数据表的主键,叶子节点 data 域保存了完整的数据记录 -* InnoDB 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个**隐含字段**作为主键,这个字段长度为 6 个字节,类型为长整形(MVCC 部分的笔记提及) -辅助索引: -* InnoDB 的所有辅助索引(二级索引)都引用主键作为 data 域 +**** -* InnoDB 表是基于聚簇索引建立的,因此 InnoDB 的索引能提供一种非常快速的主键查找性能。不过辅助索引也会包含主键列,所以不建议使用过长的字段作为主键,**过长的主索引会令辅助索引变得过大** -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB聚簇和辅助索引结构.png) +### 索引失效 +#### 创建索引 -*** +索引是数据库优化最重要的手段之一,通过索引通常可以帮助用户解决大多数的 MySQL 的性能优化问题 +```mysql +CREATE TABLE `tb_seller` ( + `sellerid` varchar (100), + `name` varchar (100), + `nickname` varchar (50), + `password` varchar (60), + `status` varchar (1), + `address` varchar (100), + `createtime` datetime, + PRIMARY KEY(`sellerid`) +)ENGINE=INNODB DEFAULT CHARSET=utf8mb4; +INSERT INTO `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('xiaomi','小米科技','小米官方旗舰店','e10adc3949ba59abbe56e057f20f883e','1','西安市','2088-01-01 12:00:00'); +CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); +``` +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引环境准备.png) -#### MyISAM -##### 非聚簇 -MyISAM 的主键索引使用的是非聚簇索引,索引文件和数据文件是分离的,**索引文件仅保存数据的地址** +**** -* 主键索引 B+ 树的节点存储了主键,辅助键索引 B+ 树存储了辅助键,表数据存储在独立的地方,这两颗 B+ 树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别 -* 由于索引树是独立的,通过辅助索引检索无需访问主键的索引树回表查询 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-聚簇索引和辅助索引检锁数据图.jpg) +#### 避免失效 +##### 语句错误 -*** +* 全值匹配:对索引中所有列都指定具体值,这种情况索引生效,执行效率高 + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市'; + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引1.png) -##### 索引实现 +* **最左前缀法则**:联合索引遵守最左前缀法则 -MyISAM 的索引方式也叫做非聚集的,之所以这么称呼是为了与 InnoDB 的聚集索引区分 + 匹配最左前缀法则,走索引: -主键索引:MyISAM 引擎使用 B+Tree 作为索引结构,叶节点的 data 域存放的是数据记录的地址 + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技'; + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status='1'; + ``` -辅助索引:MyISAM 中主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求 key 是唯一的,而辅助索引的 key 可以重复 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引2.png) -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM主键和辅助索引结构.png) + 违法最左前缀法则 , 索引失效: + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE status='1'; + EXPLAIN SELECT * FROM tb_seller WHERE status='1' AND address='西安市'; + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引3.png) + 如果符合最左法则,但是出现跳跃某一列,只有最左列索引生效: + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND address='西安市'; + ``` -参考文章:https://blog.csdn.net/lm1060891265/article/details/81482136 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引4.png) + 虽然索引列失效,但是系统**使用了索引下推进行了优化** +* **范围查询**右边的列,不能使用索引: -*** + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status>'1' AND address='西安市'; + ``` + 根据前面的两个字段 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) = '科技'; + ``` -文件系统的最小单元是块(block),一个块的大小是 4K,系统从磁盘读取数据到内存时是以磁盘块为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引6.png) -InnoDB 存储引擎中有页(Page)的概念,页是 MySQL 磁盘管理的最小单位 +* **字符串不加单引号**,造成索引失效: -* **InnoDB 存储引擎中默认每个页的大小为 16KB,索引中一个节点就是一个数据页**,所以会一次性读取 16KB 的数据到内存 -* InnoDB 引擎将若干个地址连接磁盘块,以此来达到页的大小 16KB -* 在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率 + 在查询时,没有对字符串加单引号,MySQL 的查询优化器,会自动的进行类型转换,造成索引失效 + + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status=1; + ``` + + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引7.png) +* **用 OR 分割条件,索引失效**,导致全表查询: + OR 前的条件中的列有索引而后面的列中没有索引或 OR 前后两个列是同一个复合索引,都造成索引失效 + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name='阿里巴巴' OR createtime = '2088-01-01 12:00:00'; + EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' OR status='1'; + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引10.png) -*** + **AND 分割的条件不影响**: + ```mysql + 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) -#### BTree +* **以 % 开头的 LIKE 模糊查询**,索引失效: -BTree 的索引类型是基于 B+Tree 树型数据结构的,B+Tree 又是 BTree 数据结构的变种,用在数据库和操作系统中的文件系统,特点是能够保持数据稳定有序 + 如果是尾部模糊匹配,索引不会失效;如果是头部模糊匹配,索引失效。 -BTree 又叫多路平衡搜索树,一颗 m 叉的 BTree 特性如下: + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name like '%科技%'; + ``` -- 树中每个节点最多包含 m 个孩子 -- 除根节点与叶子节点外,每个节点至少有 [ceil(m/2)] 个孩子 -- 若根节点不是叶子节点,则至少有两个孩子 -- 所有的叶子节点都在同一层 -- 每个非叶子节点由 n 个key与 n+1 个指针组成,其中 [ceil(m/2)-1] <= n <= m-1 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引12.png) -5 叉,key 的数量 [ceil(m/2)-1] <= n <= m-1 为 2 <= n <=4 ,当 n>4 时中间节点分裂到父节点,两边节点分裂 + 解决方案:通过覆盖索引来解决 -插入 C N G A H E K Q M F W L T Z D P R X Y S 数据的工作流程: + ```mysql + EXPLAIN SELECT sellerid,name,status FROM tb_seller WHERE name like '%科技%'; + ``` -* 插入前4个字母 C N G A + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引13.png) - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程1.png) + 原因:在覆盖索引的这棵 B+ 数上只需要进行 like 的匹配,或者是基于覆盖索引查询再进行 WHERE 的判断就可以获得结果 -* 插入 H,n>4,中间元素 G 字母向上分裂到新的节点 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程2.png) -* 插入 E、K、Q 不需要分裂 +*** - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程3.png) -* 插入 M,中间元素 M 字母向上分裂到父节点 G - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程4.png) +##### 系统优化 -* 插入 F,W,L,T 不需要分裂 +系统优化为全表扫描: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程5.png) +* 如果 MySQL 评估使用索引比全表更慢,则不使用索引,索引失效: -* 插入 Z,中间元素 T 向上分裂到父节点中 + ```mysql + CREATE INDEX idx_address ON tb_seller(address); + EXPLAIN SELECT * FROM tb_seller WHERE address='西安市'; + EXPLAIN SELECT * FROM tb_seller WHERE address='北京市'; + ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程6.png) + 北京市的键值占 9/10(区分度低),所以优化为全表扫描,type = ALL -* 插入 D,中间元素 D 向上分裂到父节点中,然后插入 P,R,X,Y 不需要分裂 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引14.png) - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程7.png) +* IS NULL、IS NOT NULL **有时**索引失效: -* 最后插入 S,NPQR 节点 n>5,中间节点 Q 向上分裂,但分裂后父节点 DGMT 的 n>5,中间节点 M 向上分裂 + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE name IS NULL; + EXPLAIN SELECT * FROM tb_seller WHERE name IS NOT NULL; + ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-BTree工作流程8.png) + NOT NULL 失效的原因是 name 列全部不是 null,优化为全表扫描,当 NULL 过多时,IS NULL 失效 -BTree 树就已经构建完成了,BTree 树和二叉树相比, 查询数据的效率更高, 因为对于相同的数据量来说,**BTree 的层级结构比二叉树小**,所以搜索速度快 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引15.png) -BTree 结构的数据可以让系统高效的找到数据所在的磁盘块,定义一条记录为一个二元组 [key, data] ,key 为记录的键值,对应表中的主键值,data 为一行记录中除主键外的数据。对于不同的记录,key 值互不相同,BTree 中的每个节点根据实际情况可以包含大量的关键字信息和分支 -![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-BTree.png) +* IN 肯定会走索引,但是当 IN 的取值范围较大时会导致索引失效,走全表扫描: -缺点:当进行范围查找时会出现回旋查找 + ```mysql + EXPLAIN SELECT * FROM tb_seller WHERE sellerId IN ('alibaba','huawei');-- 都走索引 + EXPLAIN SELECT * FROM tb_seller WHERE sellerId NOT IN ('alibaba','huawei'); + ``` @@ -4438,24 +4775,20 @@ BTree 结构的数据可以让系统高效的找到数据所在的磁盘块, -#### B+Tree - -##### 数据结构 +#### 底层原理 -BTree 数据结构中每个节点中不仅包含数据的 key 值,还有 data 值。磁盘中每一页的存储空间是有限的,如果 data 数据较大时将会导致每个节点(即一个页)能存储的 key 的数量很小,当存储的数据量很大时同样会导致 B-Tree 的深度较大,增大查询时的磁盘 I/O 次数,进而影响查询效率,所以引入 B+Tree +索引失效一般是针对联合索引,联合索引一般由几个字段组成,排序方式是先按照第一个字段进行排序,然后排序第二个,依此类推,图示(a, b)索引,**a 相等的情况下 b 是有序的** -B+Tree 为 BTree 的变种,B+Tree 与 BTree 的区别为: + -* n 叉 B+Tree 最多含有 n 个 key(哈希值),而 BTree 最多含有 n-1 个 key +* 最左前缀法则:当不匹配前面的字段的时候,后面的字段都是无序的。这种无序不仅体现在叶子节点,也会**导致查询时扫描的非叶子节点也是无序的**,因为索引树相当于忽略的第一个字段,就无法使用二分查找 -- 所有**非叶子节点只存储键值 key** 信息,只进行数据索引,使每个非叶子节点所能保存的关键字大大增加 -- 所有**数据都存储在叶子节点**,所以每次数据查询的次数都一样 -- **叶子节点按照 key 大小顺序排列,左边结尾数据都会保存右边节点开始数据的指针,形成一个链表** -- 所有节点中的 key 在叶子节点中也存在(比如 5),key 允许重复,B 树不同节点不存在重复的 key +* 范围查询右边的列,不能使用索引,比如语句: `WHERE a > 1 AND b = 1 `,在 a 大于 1 的时候,b 是无序的,a > 1 是扫描时有序的,但是找到以后进行寻找 b 时,索引树就不是有序的了 - + -B* 树:是 B+ 树的变体,在 B+ 树的非根和非叶子结点再增加指向兄弟的指针 +* 以 % 开头的 LIKE 模糊查询,索引失效,比如语句:`WHERE a LIKE '%d'`,前面的不确定,导致不符合最左匹配,直接去索引中搜索以 d 结尾的节点,所以没有顺序 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引失效底层原理3.png) @@ -4463,29 +4796,28 @@ B* 树:是 B+ 树的变体,在 B+ 树的非根和非叶子结点再增加指 -##### 优化结构 +#### 查看索引 -MySQL 索引数据结构对经典的 B+Tree 进行了优化,在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,**提高区间访问的性能,防止回旋查找** +```mysql +SHOW STATUS LIKE 'Handler_read%'; +SHOW GLOBAL STATUS LIKE 'Handler_read%'; +``` -区间访问的意思是访问索引为 5 - 15 的数据,可以直接根据相邻节点的指针遍历 +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL查看索引使用情况.png) -B+ 树的**叶子节点是数据页**(page),一个页里面可以存多个数据行 +* Handler_read_first:索引中第一条被读的次数,如果较高,表示服务器正执行大量全索引扫描(这个值越低越好) -![](https://gitee.com/seazean/images/raw/master/DB/索引的原理-B+Tree.png) +* Handler_read_key:如果索引正在工作,这个值代表一个行被索引值读的次数,值越低表示索引不经常使用(这个值越高越好) -通常在 B+Tree 上有两个头指针,**一个指向根节点,另一个指向关键字最小的叶子节点**,而且所有叶子节点(即数据节点)之间是一种链式环结构。可以对 B+Tree 进行两种查找运算: +* Handler_read_next:按照键顺序读下一行的请求数,如果范围约束或执行索引扫描来查询索引列,值增加 -- 有范围:对于主键的范围查找和分页查找 -- 有顺序:从根节点开始,进行随机查找,顺序查找 +* Handler_read_prev:按照键顺序读前一行的请求数,该读方法主要用于优化 ORDER BY ... DESC -InnoDB 中每个数据页的大小默认是 16KB, +* Handler_read_rnd:根据固定位置读一行的请求数,如果执行大量查询并对结果进行排序则该值较高,可能是使用了大量需要 MySQL 扫描整个表的查询或连接,这个值较高意味着运行效率低,应该建立索引来解决 -* 索引行:一般表的主键类型为 INT(4 字节)或 BIGINT(8 字节),指针大小在 InnoDB 中设置为 6 字节节,也就是说一个页大概存储 16KB/(8B+6B)=1K 个键值(估值)。则一个深度为 3 的 B+Tree 索引可以维护 `10^3 * 10^3 * 10^3 = 10亿` 条记录 -* 数据行:一行数据的大小可能是 1k,一个数据页可以存储 16 行 +* Handler_read_rnd_next:在数据文件中读下一行的请求数,如果正进行大量的表扫描,该值较高,说明表索引不正确或写入的查询没有利用索引 -实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree 的高度一般都在 2-4 层。MySQL 的 InnoDB 存储引擎在设计时是**将根节点常驻内存的**,也就是说查找某一键值的行记录时最多只需要 1~3 次磁盘 I/O 操作 -B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较小 @@ -4493,136 +4825,76 @@ B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较 -##### 索引维护 +### SQL 优化 -B+ 树为了保持索引的有序性,在插入新值的时候需要做相应的维护 +#### 覆盖索引 -每个索引中每个块存储在磁盘页中,可能会出现以下两种情况: +复合索引叶子节点不仅保存了复合索引的值,还有主键索引,所以使用覆盖索引的时候,加上主键也会用到索引 -* 如果所在的数据页已经满了,这时候需要申请一个新的数据页,然后挪动部分数据过去,这个过程称为**页分裂** -* 当相邻两个页由于删除了数据,利用率很低之后,会将数据页做**页合并**,合并的过程可以认为是分裂过程的逆过程 -* 这两个情况都是由 B+ 树的结构决定的 +尽量使用覆盖索引,避免 SELECT *: -一般选用数据小的字段做索引,字段长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小 +```mysql +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) +如果查询列,超出索引列,也会降低性能: -*** +```mysql +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) -### 索引操作 -索引在创建表的时候可以同时创建, 也可以随时增加新的索引 +**** -* 创建索引:如果一个表中有一列是主键,那么会**默认为其创建主键索引**(主键列不需要单独创建索引) - - ```mysql - CREATE [UNIQUE|FULLTEXT] INDEX 索引名称 [USING 索引类型] ON 表名(列名...); - -- 索引类型默认是 B+TREE - ``` - -* 查看索引 - ```mysql - SHOW INDEX FROM 表名; - ``` -* 添加索引 +#### 减少访问 - ```mysql - -- 单列索引 - ALTER TABLE 表名 ADD INDEX 索引名称(列名); - - -- 组合索引 - ALTER TABLE 表名 ADD INDEX 索引名称(列名1,列名2,...); - - -- 主键索引 - ALTER TABLE 表名 ADD PRIMARY KEY(主键列名); - - -- 外键索引(添加外键约束,就是外键索引) - ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主键列名); - - -- 唯一索引 - ALTER TABLE 表名 ADD UNIQUE 索引名称(列名); - - -- 全文索引(mysql只支持文本类型) - ALTER TABLE 表名 ADD FULLTEXT 索引名称(列名); - ``` +避免对数据进行重复检索:能够一次连接就获取到结果的,就不用两次连接,这样可以大大减少对数据库无用的重复请求 -* 删除索引 +* 查询数据: ```mysql - DROP INDEX 索引名称 ON 表名; + SELECT id,name FROM tb_book; + SELECT id,status FROM tb_book; -- 向数据库提交两次请求,数据库就要做两次查询操作 + -- > 优化为: + SELECT id,name,statu FROM tb_book; ``` -* 案例练习 - - 数据准备:student - +* 插入数据: + ```mysql - id NAME age score - 1 张三 23 99 - 2 李四 24 95 - 3 王五 25 98 - 4 赵六 26 97 + INSERT INTO tb_test VALUES(1,'Tom'); + INSERT INTO tb_test VALUES(2,'Cat'); + INSERT INTO tb_test VALUES(3,'Jerry'); -- 连接三次数据库 + -- >优化为 + INSERT INTO tb_test VALUES(1,'Tom'),(2,'Cat'),(3,'Jerry'); -- 连接一次 ``` - 索引操作: - +* 在事务中进行数据插入: + ```mysql - -- 为student表中姓名列创建一个普通索引 - CREATE INDEX idx_name ON student(NAME); - - -- 为student表中年龄列创建一个唯一索引 - CREATE UNIQUE INDEX idx_age ON student(age); + start transaction; + INSERT INTO tb_test VALUES(1,'Tom'); + INSERT INTO tb_test VALUES(2,'Cat'); + INSERT INTO tb_test VALUES(3,'Jerry'); + commit; -- 手动提交,分段提交 ``` - - - -*** - - - -### 设计原则 - -索引的设计可以遵循一些已有的原则,创建索引的时候请尽量考虑符合这些原则,便于提升索引的使用效率 -创建索引时的原则: -- 对查询频次较高,且数据量比较大的表建立索引 -- 使用唯一索引,区分度越高,使用索引的效率越高 -- 索引字段的选择,最佳候选列应当从 where 子句的条件中提取,使用覆盖索引 -- 使用短索引,索引创建之后也是使用硬盘来存储的,因此提升索引访问的 I/O 效率,也可以提升总体的访问效率。假如构成索引的字段总长度比较短,那么在给定大小的存储块内可以存储更多的索引值,相应的可以有效的提升 MySQL 访问索引的 I/O 效率 -- 索引可以有效的提升查询数据的效率,但索引数量不是多多益善,索引越多,维护索引的代价越高。对于插入、更新、删除等 DML 操作比较频繁的表来说,索引过多,会引入相当高的维护代价,降低 DML 操作的效率,增加相应操作的时间消耗;另外索引过多的话,MySQL 也会犯选择困难病,虽然最终仍然会找到一个可用的索引,但提高了选择的代价 +* 数据有序插入: -* MySQL 建立联合索引时会遵守**最左前缀匹配原则**,即最左优先,在检索数据时从联合索引的最左边开始匹配 - - N 个列组合而成的组合索引,相当于创建了 N 个索引,如果查询时 where 句中使用了组成该索引的**前**几个字段,那么这条查询 SQL 可以利用组合索引来提升查询效率 - - ```mysql - -- 对name、address、phone列建一个联合索引 - ALTER TABLE user ADD INDEX index_three(name,address,phone); - -- 查询语句执行时会依照最左前缀匹配原则,检索时分别会使用索引进行数据匹配。 - (name,address,phone) - (name,address) - (name,phone) -- 只有name字段走了索引 - (name) - - -- 索引的字段可以是任意顺序的,优化器会帮助我们调整顺序,下面的SQL语句可以命中索引 - SELECT * FROM user WHERE address = '北京' AND phone = '12345' AND name = '张三'; - ``` - ```mysql - -- 如果联合索引中最左边的列不包含在条件查询中,SQL语句就不会命中索引,比如: - SELECT * FROM user WHERE address = '北京' AND phone = '12345'; + INSERT INTO tb_test VALUES(1,'Tom'); + INSERT INTO tb_test VALUES(2,'Cat'); + INSERT INTO tb_test VALUES(3,'Jerry'); ``` -哪些情况不要建立索引: - -* 记录太少的表 -* 经常增删改的表 -* 频繁更新的字段不适合创建索引 -* where 条件里用不到的字段不创建索引 +增加 cache 层:在应用中增加缓存层来达到减轻数据库负担的目的。可以部分数据从数据库中抽取出来放到应用端以文本方式存储,或者使用框架(Mybatis)提供的一级缓存 / 二级缓存,或者使用 Redis 数据库来缓存数据 @@ -4630,355 +4902,330 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 -### 索引优化 - -#### 覆盖索引 - -覆盖索引:包含所有满足查询需要的数据的索引(SELECT 后面的字段刚好是索引字段),可以利用该索引返回 SELECT 列表的字段,而不必根据索引去聚簇索引上读取数据文件 - -回表查询:要查找的字段不在非主键索引树上时,需要通过叶子节点的主键值去主键索引上获取对应的行数据 +#### 数据插入 -使用覆盖索引,防止回表查询: +当使用 load 命令导入数据的时候,适当的设置可以提高导入的效率: -* 表 user 主键为 id,普通索引为 age,查询语句: +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL load data.png) - ```mysql - SELECT * FROM user WHERE age = 30; - ``` +```mysql +LOAD DATA LOCAL INFILE = '/home/seazean/sql1.log' INTO TABLE `tb_user_1` FIELD TERMINATED BY ',' LINES TERMINATED BY '\n'; -- 文件格式如上图 +``` - 查询过程:先通过普通索引 age=30 定位到主键值 id=1,再通过聚集索引 id=1 定位到行记录数据,需要两次扫描 B+ 树 +对于 InnoDB 类型的表,有以下几种方式可以提高导入的效率: -* 使用覆盖索引: +1. **主键顺序插入**:因为 InnoDB 类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率,如果 InnoDB 表没有主键,那么系统会自动默认创建一个内部列作为主键。 - ```mysql - DROP INDEX idx_age ON user; - CREATE INDEX idx_age_name ON user(age,name); - SELECT id,age FROM user WHERE age = 30; - ``` + **主键是否连续对性能影响不大,只要是递增的就可以**,比如雪花算法产生的 ID 不是连续的,但是是递增的 - 在一棵索引树上就能获取查询所需的数据,无需回表速度更快 + * 插入 ID 顺序排列数据: -使用覆盖索引,要注意 SELECT 列表中只取出需要的列,不可用 SELECT *,所有字段一起做索引会导致索引文件过大,查询性能下降 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入ID顺序排列数据.png) + * 插入 ID 无序排列数据: + ![](https://gitee.com/seazean/images/raw/master/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) +3. **手动提交事务**:如果应用使用自动提交的方式,建议在导入前执行`SET AUTOCOMMIT=0`,关闭自动提交;导入结束后再打开自动提交,可以提高导入的效率。 -#### 索引下推 + 事务需要控制大小,事务太大可能会影响执行的效率。MySQL 有 innodb_log_buffer_size 配置项,超过这个值的日志会写入磁盘数据,效率会下降,所以在事务大小达到配置项数据级前进行事务提交可以提高效率 -索引条件下推优化(Index Condition Pushdown)是 MySQL5.6 添加,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入数据手动提交事务.png) -索引下推充分利用了索引中的数据,在查询出整行数据之前过滤掉无效的数据,再去主键索引树上查找 -* 不使用索引下推优化时存储引擎通过索引检索到数据返回给 MySQL 服务器,服务器判断数据是否符合条件,符合条件的数据去聚簇索引回表查询,获取完整的数据 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-不使用索引下推.png) -* 使用索引下推优化时,如果**存在某些被索引的列的判断条件**时,MySQL 服务器将这一部分判断条件传递给存储引擎,由存储引擎在索引遍历的过程中判断数据是否符合传递的条件,将符合条件的数据进行回表,检索出来返回给服务器,由此减少 IO 次数 +**** - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-使用索引下推.png) -**适用条件**: -* 需要存储引擎将索引中的数据与条件进行判断(所以**条件列必须都在同一个索引中**),所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM -* 存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化 -* 对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了,索引下推的目的减少 IO 次数也就失去了意义 +#### ORDER BY -工作过程:用户表 user,(name, age) 是联合索引 +数据准备: ```mysql -SELECT * FROM user WHERE name LIKE '张%' AND age = 10; -- 头部模糊匹配会造成索引失效 +CREATE TABLE `emp` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL, + `age` INT(3) NOT NULL, + `salary` INT(11) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=INNODB DEFAULT CHARSET=utf8mb4; +INSERT INTO `emp` (`id`, `name`, `age`, `salary`) VALUES('1','Tom','25','2300');-- ... +CREATE INDEX idx_emp_age_salary ON emp(age,salary); ``` -* 优化前:在非主键索引树上找到满足第一个条件的行,然后通过叶子节点记录的主键值再回到主键索引树上查找到对应的行数据,再对比 AND 后的条件是否符合,符合返回数据,需要 4 次回表 - - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引下推优化1.png) - -* 优化后:检查索引中存储的列信息是否符合索引条件,然后交由存储引擎用剩余的判断条件判断此行数据是否符合要求,**不满足条件的不去读取表中的数据**,满足下推条件的就根据主键值进行回表查询,2 次回表 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-索引下推优化2.png) - -当使用 EXPLAIN 进行分析时,如果使用了索引条件下推,Extra 会显示 Using index condition +* 第一种是通过对返回数据进行排序,所有不通过索引直接返回结果的排序都叫 FileSort 排序,会在内存中重新排序 + ```mysql + EXPLAIN SELECT * FROM emp ORDER BY age DESC; -- 年龄降序 + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序1.png) -参考文章:https://blog.csdn.net/sinat_29774479/article/details/103470244 +* 第二种通过有序索引顺序扫描直接返回**有序数据**,这种情况为 Using index,不需要额外排序,操作效率高 -参考文章:https://time.geekbang.org/column/article/69636 + ```mysql + 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) +* 多字段排序: -*** + ```mysql + EXPLAIN SELECT id,age,salary FROM emp ORDER BY age DESC, salary DESC; + EXPLAIN SELECT id,age,salary FROM emp ORDER BY salary DESC, age DESC; + 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) + 尽量减少额外的排序,通过索引直接返回有序数据。**需要满足 Order by 使用相同的索引、Order By 的顺序和索引顺序相同、Order by 的字段都是升序或都是降序**,否则需要额外的操作,就会出现 FileSort -#### 前缀索引 +优化:通过创建合适的索引能够减少 Filesort 的出现,但是某些情况下条件限制不能让 Filesort 消失,就要加快 Filesort 的排序操作 -当要索引的列字符很多时,索引会变大变慢,可以只索引列开始的部分字符串,节约索引空间,提高索引效率 +对于 Filesort , MySQL 有两种排序算法: -注意:使用前缀索引就系统就忽略覆盖索引对查询性能的优化了 +* 两次扫描算法: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 -area gdp code -chinaShanghai 100 aaa -chinaDalian 200 bbb -usaNewYork 300 ccc -chinaFuxin 400 ddd -chinaBeijing 500 eee +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 ``` -发现 area 字段很多都是以 china 开头的,那么如果以前 1-5 位字符做前缀索引就会出现大量索引值重复的情况,索引值重复性越低,查询效率也就越高,所以需要建立前 6 位字符的索引: -```mysql -CREATE INDEX idx_area ON table_name(area(7)); -``` -场景:存储身份证 +*** -* 直接创建完整索引,这样可能比较占用空间 -* 创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引 -* 倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题(前 6 位相同的很多) -* 创建 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描 +#### GROUP BY -**** +GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是多了排序之后的分组操作,所以在 GROUP BY 的实现过程中,与 ORDER BY 一样也可以利用到索引 +* 分组查询: + ```mysql + DROP INDEX idx_emp_age_salary ON emp; + EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age; + ``` -#### 索引合并 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序1.png) -使用多个索引来完成一次查询的执行方法叫做索引合并 index merge + Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序和分组查询 -* Intersection 索引合并: +* 查询包含 GROUP BY 但是用户想要避免排序结果的消耗, 则可以执行 ORDER BY NULL 禁止排序: - ```sql - SELECT * FROM table_test WHERE key1 = 'a' AND key3 = 'b'; # key1 和 key3 列都是单列索引、二级索引 + ```mysql + EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age ORDER BY NULL; ``` - 从不同索引中扫描到的记录的 id 值取**交集**(相同 id),然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序2.png) -* Union 索引合并: +* 创建索引: - ```sql - SELECT * FROM table_test WHERE key1 = 'a' OR key3 = 'b'; + ```mysql + CREATE INDEX idx_emp_age_salary ON emp(age,salary); ``` - 从不同索引中扫描到的记录的 id 值取**并集**,然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序3.png) -* Sort-Union 索引合并 - ```sql - SELECT * FROM table_test WHERE key1 < 'a' OR key3 > 'b'; - ``` - 先将从不同索引中扫描到的记录的主键值进行排序,再按照 Union 索引合并的方式进行查询 +*** +#### OR + +对于包含 OR 的查询子句,如果要利用索引,则 OR 之间的**每个条件列都必须用到索引,而且不能使用到条件之间的复合索引**,如果没有索引,则应该考虑增加索引 +* 执行查询语句: + ```mysql + EXPLAIN SELECT * FROM emp WHERE id = 1 OR age = 30; -- 两个索引,并且不是复合索引 + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL OR条件查询1.png) -*** + ```sh + Extra: Using sort_union(idx_emp_age_salary,PRIMARY); Using where + ``` +* 使用 UNION 替换 OR,求并集: + + 注意:该优化只针对多个索引列有效,如果有列没有被索引,查询效率可能会因为没有选择 OR 而降低 + + ```mysql + 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) + +* UNION 要优于 OR 的原因: + * UNION 语句的 type 值为 ref,OR 语句的 type 值为 range + * UNION 语句的 ref 值为 const,OR 语句的 ref 值为 null,const 表示是常量值引用,非常快 -## 系统优化 +**** -### 优化步骤 -#### 执行频率 -随着生产数据量的急剧增长,很多 SQL 语句逐渐显露出性能问题,对生产的影响也越来越大,此时有问题的 SQL 语句就成为整个系统性能的瓶颈,因此必须要进行优化 +#### 嵌套查询 -MySQL 客户端连接成功后,查询服务器状态信息: +MySQL 4.1 版本之后,开始支持 SQL 的子查询 -```mysql -SHOW [SESSION|GLOBAL] STATUS LIKE ''; --- SESSION: 显示当前会话连接的统计结果,默认参数 --- GLOBAL: 显示自数据库上次启动至今的统计结果 -``` +* 可以使用 SELECT 语句来创建一个单列的查询结果,然后把结果作为过滤条件用在另一个查询中 +* 使用子查询可以一次性的完成逻辑上需要多个步骤才能完成的 SQL 操作,同时也可以避免事务或者表锁死 +* 在有些情况下,子查询是可以被更高效的连接(JOIN)替代 -* 查看SQL执行频率: +例如查找有角色的所有的用户信息: + +* 执行计划: ```mysql - SHOW STATUS LIKE 'Com_____'; + EXPLAIN SELECT * FROM t_user WHERE id IN (SELECT user_id FROM user_role); ``` - Com_xxx 表示每种语句执行的次数 - - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL语句执行频率.png) + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL嵌套查询1.png) -* 查询 SQL 语句影响的行数: +* 优化后: ```mysql - SHOW STATUS LIKE 'Innodb_rows_%'; + 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语句影响的行数.png) - -Com_xxxx:这些参数对于所有存储引擎的表操作都会进行累计 - -Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算法也略有不同 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL嵌套查询2.png) -| 参数 | 含义 | -| :------------------- | ------------------------------------------------------------ | -| Com_select | 执行 SELECT 操作的次数,一次查询只累加 1 | -| Com_insert | 执行 INSERT 操作的次数,对于批量插入的 INSERT 操作,只累加一次 | -| Com_update | 执行 UPDATE 操作的次数 | -| Com_delete | 执行 DELETE 操作的次数 | -| Innodb_rows_read | 执行 SELECT 查询返回的行数 | -| Innodb_rows_inserted | 执行 INSERT 操作插入的行数 | -| Innodb_rows_updated | 执行 UPDATE 操作更新的行数 | -| Innodb_rows_deleted | 执行 DELETE 操作删除的行数 | -| Connections | 试图连接 MySQL 服务器的次数 | -| Uptime | 服务器工作时间 | -| Slow_queries | 慢查询的次数 | + 连接查询之所以效率更高 ,是因为不需要在内存中创建临时表来完成逻辑上需要两个步骤的查询工作 -**** +*** -#### 定位低效 -SQL 执行慢有两种情况: -* 偶尔慢:DB 在刷新脏页 - * redo log 写满了 - * 内存不够用,要从 LRU 链表中淘汰 - * MySQL 认为系统空闲的时候 - * MySQL 关闭时 -* 一直慢的原因:索引没有设计好、SQL 语句没写好、MySQL 选错了索引 +#### 分页查询 -通过以下两种方式定位执行效率较低的 SQL 语句 +一般分页查询时,通过创建覆盖索引能够比较好地提高性能 -* 慢日志查询: 慢查询日志在查询结束以后才记录,执行效率出现问题时查询日志并不能定位问题 +一个常见的问题是 `LIMIT 200000,10`,此时需要 MySQL 扫描前 200010 记录,仅仅返回 200000 - 200010 之间的记录,其他记录丢弃,查询排序的代价非常大 - 配置文件修改:修改 .cnf 文件 `vim /etc/mysql/my.cnf`,重启 MySQL 服务器 +* 分页查询: - ```sh - slow_query_log=ON - slow_query_log_file=/usr/local/mysql/var/localhost-slow.log - long_query_time=1 #记录超过long_query_time秒的SQL语句的日志 - log-queries-not-using-indexes = 1 + ```mysql + EXPLAIN SELECT * FROM tb_user_1 LIMIT 200000,10; ``` - 使用命令配置: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询1.png) - ```mysql - mysql> SET slow_query_log=ON; - mysql> SET GLOBAL slow_query_log=ON; +* 优化方式一:子查询,在索引列 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) + +* 优化方式二:方案适用于主键自增的表,可以把 LIMIT 查询转换成某个位置的查询 ```mysql - SHOW VARIABLES LIKE '%query%' + EXPLAIN SELECT * FROM tb_user_1 WHERE id > 200000 LIMIT 10; -- 写法 1 + 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) -* SHOW PROCESSLIST:**实时查看**当前 MySQL 在进行的连接线程,包括线程的状态、是否锁表、SQL 的执行情况,同时对一些锁表操作进行优化 - - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SHOW PROCESSLIST命令.png) +**** +#### 使用提示 -*** +SQL 提示,是优化数据库的一个重要手段,就是在 SQL 语句中加入一些提示来达到优化操作的目的 +* USE INDEX:在查询语句中表名的后面添加 USE INDEX 来提供 MySQL 去参考的索引列表,可以让 MySQL 不再考虑其他可用的索引 + ```mysql + CREATE INDEX idx_seller_name ON tb_seller(name); + EXPLAIN SELECT * FROM tb_seller USE INDEX(idx_seller_name) WHERE name='小米科技'; + ``` -#### EXPLAIN + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示1.png) -##### 执行计划 +* IGNORE INDEX:让 MySQL 忽略一个或者多个索引,则可以使用 IGNORE INDEX 作为提示 -通过 EXPLAIN 命令获取执行 SQL 语句的信息,包括在 SELECT 语句执行过程中如何连接和连接的顺序,执行计划在优化器优化完成后、执行器之前生成,然后执行器会调用存储引擎检索数据 + ```mysql + EXPLAIN SELECT * FROM tb_seller IGNORE INDEX(idx_seller_name) WHERE name = '小米科技'; + ``` -查询 SQL 语句的执行计划: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示2.png) -```mysql -EXPLAIN SELECT * FROM table_1 WHERE id = 1; -``` +* FORCE INDEX:强制 MySQL 使用一个特定的索引 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain查询SQL语句的执行计划.png) + ```mysql + EXPLAIN SELECT * FROM tb_seller FORCE INDEX(idx_seller_name_sta_addr) WHERE NAME='小米科技'; + ``` -| 字段 | 含义 | -| ------------- | ------------------------------------------------------------ | -| id | SELECT 的序列号 | -| select_type | 表示 SELECT 的类型 | -| table | 访问数据库中表名称,有时可能是简称或者临时表名称() | -| type | 表示表的连接类型 | -| possible_keys | 表示查询时,可能使用的索引 | -| key | 表示实际使用的索引 | -| key_len | 索引字段的长度 | -| ref | 表示与索引列进行等值匹配的对象,常数、某个列、函数等,type 必须在(range, const] 之间,左闭右开 | -| rows | 扫描出的行数,表示 MySQL 根据表统计信息及索引选用情况,**估算**的找到所需的记录扫描的行数 | -| filtered | 条件过滤的行百分比,单表查询没意义,用于连接查询中对驱动表的扇出进行过滤,查询优化器预测所有扇出值满足剩余查询条件的百分比,相乘以后表示多表查询中还要对被驱动执行查询的次数 | -| extra | 执行情况的说明和描述 | + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示3.png) -MySQL **执行计划的局限**: -* 只是计划,不是执行 SQL 语句,可以随着底层优化器输入的更改而更改 -* EXPLAIN 不会告诉显示关于触发器、存储过程的信息对查询的影响情况 -* EXPLAIN 不考虑各种 Cache -* EXPLAIN 不能显示 MySQL 在执行查询时的动态,因为执行计划在执行查询之前生成 -* EXPALIN 部分统计信息是估算的,并非精确值 -* EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看执行计划 -* EXPLAIN PLAN 显示的是在解释语句时数据库将如何运行 SQL 语句,由于执行环境和 EXPLAIN PLAN 环境的不同,此计划可能与 SQL 语句实际的执行计划不同 -SHOW WARINGS:在使用 EXPALIN 命令后执行该语句,可以查询与执行计划相关的拓展信息,展示出 Level、Code、Message 三个字段,当 Code 为 1003 时,Message 字段展示的信息类似于将查询语句重写后的信息,但是不是等价,不能执行复制过来运行 -环境准备: -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-执行计划环境准备.png) +*** +#### 统计计数 +在不同的 MySQL 引擎中,count(*) 有不同的实现方式: -*** +* MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高,但不支持事务 +* show table status 命令通过采样估算可以快速获取,但是不准确 +* InnoDB 表执行 count(*) 会遍历全表,虽然结果准确,但会导致性能问题 +解决方案: +* 计数保存在 Redis 中,但是更新 MySQL 和 Redis 的操作不是原子的,会存在数据一致性的问题 -##### id +* 计数直接放到数据库里单独的一张计数表中,利用事务解决计数精确问题: -id 代表 SQL 执行的顺序的标识,每个 SELECT 关键字对应一个唯一 id,所以在同一个 SELECT 关键字中的表的 id 都是相同的。SELECT 后的 FROM 可以跟随多个表,每个表都会对应一条记录,这些记录的 id 都是相同的, + -* id 相同时,执行顺序由上至下。连接查询的执行计划,记录的 id 值都是相同的,出现在前面的表为驱动表,后面为被驱动表 + 会话 B 的读操作在 T3 执行的,这时更新事务还没有提交,所以计数值加 1 这个操作对会话 B 还不可见,因此会话 B 查询的计数值和最近 100 条记录,返回的结果逻辑上就是一致的 - ```mysql - 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) +count 函数的按照效率排序:`count(字段) < count(主键id) < count(1) ≈ count(*)`,所以建议尽量使用 count(*) -* id 不同时,id 值越大优先级越高,越先被执行 +* count(主键 id):InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来返回给 Server 层,Server 判断 id 不为空就按行累加 +* count(1):InnoDB 引擎遍历整张表但不取值,Server 层对于返回的每一行,放一个数字 1 进去,判断不为空就按行累加 - ```mysql - EXPLAIN SELECT * FROM t_role WHERE id = (SELECT role_id FROM user_role WHERE user_id = (SELECT id FROM t_user WHERE username = 'stu1')) - ``` +* count(字段):如果这个字段是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;如果这个字段定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain之id不同.png) -* id 有相同也有不同时,id 相同的可以认为是一组,从上往下顺序执行;在所有的组中,id 的值越大的组,优先级越高,越先执行 - ```mysql - EXPLAIN SELECT * FROM t_role r , (SELECT * FROM user_role ur WHERE ur.`user_id` = '2') a WHERE r.id = a.role_id ; - ``` +参考文章:https://time.geekbang.org/column/article/72775 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-explain之id相同和不同.png) -* id 为 NULL 时代表的是临时表 @@ -4986,101 +5233,82 @@ id 代表 SQL 执行的顺序的标识,每个 SELECT 关键字对应一个唯 -##### select +### 内存优化 -表示查询中每个 select 子句的类型(简单 OR 复杂) +#### 优化原则 -| select_type | 含义 | -| ------------------ | ------------------------------------------------------------ | -| SIMPLE | 简单的 SELECT 查询,查询中不包含子查询或者 UNION | -| PRIMARY | 查询中若包含任何复杂的子查询,最外层(也就是最左侧)查询标记为该标识 | -| UNION | 对于 UNION 或者 UNION ALL 的复杂查询,除了最左侧的查询,其余的小查询都是 UNION | -| UNION RESULT | UNION 需要使用临时表进行去重,临时表的是 UNION RESULT | -| DEPENDENT UNION | 对于 UNION 或者 UNION ALL 的复杂查询,如果各个小查询都依赖外层查询,是相关子查询,除了最左侧的小查询为 DEPENDENT SUBQUERY,其余都是 DEPENDENT UNION | -| SUBQUERY | 子查询不是相关子查询,该子查询第一个 SELECT 代表的查询就是这种类型,会进行物化(该子查询只需要执行一次) | -| DEPENDENT SUBQUERY | 子查询是相关子查询,该子查询第一个 SELECT 代表的查询就是这种类型,不会物化(该子查询需要执行多次) | -| DERIVED | 在 FROM 列表中包含的子查询,被标记为 DERIVED(衍生),也就是生成物化派生表的这个子查询 | -| MATERIALIZED | 将子查询物化后与与外层进行连接查询,生成物化表的子查询 | +三个原则: -子查询为 DERIVED:`SELECT * FROM (SELECT key1 FROM t1) AS derived_1 WHERE key1 > 10` +* 将尽量多的内存分配给 MySQL 做缓存,但也要给操作系统和其他程序预留足够内存 +* MyISAM 存储引擎的数据文件读取依赖于操作系统自身的 IO 缓存,如果有 MyISAM 表,就要预留更多的内存给操作系统做 IO 缓存 +* 排序区、连接区等缓存是分配给每个数据库会话(Session)专用的,值的设置要根据最大连接数合理分配,如果设置太大,不但浪费资源,而且在并发数较高时会导致物理内存耗尽 -子查询为 MATERIALIZED:`SELECT * FROM t1 WHERE key1 IN (SELECT key1 FROM t2)` +MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的读写速度。对于 MyISAM 表的数据块没有特别的缓存机制,完全依赖于操作系统的 IO 缓存 +* key_buffer_size:该变量决定 MyISAM 索引块缓存区的大小,直接影响到 MyISAM 表的存取效率 + + ```mysql + SHOW VARIABLES LIKE 'key_buffer_size'; -- 单位是字节 + ``` + 在 MySQL 配置文件中设置该值,建议至少将1/4可用内存分配给 key_buffer_size: -**** + ```sh + vim /etc/mysql/my.cnf + key_buffer_size=1024M + ``` +* read_buffer_size:如果需要经常顺序扫描 MyISAM 表,可以通过增大 read_buffer_size 的值来改善性能。但 read_buffer_size 是每个 Session 独占的,如果默认值设置太大,并发环境就会造成内存浪费 +* read_rnd_buffer_size:对于需要做排序的 MyISAM 表的查询,如带有 ORDER BY 子句的语句,适当增加该的值,可以改善此类的 SQL 的性能,但是 read_rnd_buffer_size 是每个 Session 独占的,如果默认值设置太大,就会造成内存浪费 -##### type -对表的访问方式,表示 MySQL 在表中找到所需行的方式,又称访问类型 -| type | 含义 | -| --------------- | ------------------------------------------------------------ | -| ALL | 全表扫描,如果是 InnoDB 引擎是扫描聚簇索引 | -| index | 可以使用覆盖索引,但需要扫描全部索引 | -| range | 索引范围扫描,常见于 between、<、> 等的查询 | -| index_subquery | 子查询可以普通索引,则子查询的 type 为 index_subquery | -| unique_subquery | 子查询可以使用主键或唯一二级索引,则子查询的 type 为 index_subquery | -| index_merge | 索引合并 | -| ref_or_null | 非唯一性索引(普通二级索引)并且可以存储 NULL,进行等值匹配 | -| ref | 非唯一性索引与常量等值匹配 | -| eq_ref | 唯一性索引(主键或不存储 NULL 的唯一二级索引)进行等值匹配,如果二级索引是联合索引,那么所有联合的列都要进行等值匹配 | -| const | 通过主键或者唯一二级索引与常量进行等值匹配 | -| system | system 是 const 类型的特例,当查询的表只有一条记录的情况下,使用 system | -| NULL | MySQL 在优化过程中分解语句,执行时甚至不用访问表或索引 | +*** -从上到下,性能从差到好,一般来说需要保证查询至少达到 range 级别, 最好达到 ref +#### 缓冲内存 -*** +Buffer Pool 本质上是 InnoDB 向操作系统申请的一段连续的内存空间。InnoDB 的数据是按数据页为单位来读写,每个数据页的大小默认是 16KB。数据是存放在磁盘中,每次读写数据都需要进行磁盘 IO 将数据读入内存进行操作,效率会很低,所以提供了 Buffer Pool 来暂存这些数据页,缓存中的这些页又叫缓冲页 +工作流程: +* 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool +* 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 -##### key +Buffer Pool 中每个缓冲页都有对应的控制信息,包括表空间编号、页号、偏移量、链表信息等,控制信息存放在占用的内存称为控制块,控制块与缓冲页是一一对应的,但并不是物理上相连的,都在缓冲池中 -possible_keys: +MySQL 提供了缓冲页的快速查找方式:**哈希表**,使用表空间号和页号作为 Key,缓冲页控制块的地址作为 Value 创建一个哈希表,获取数据页时根据 Key 进行哈希寻址: -* 指出 MySQL 能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用 -* 如果该列是 NULL,则没有相关的索引 +* 如果不存在对应的缓存页,就从 free 链表中选一个空闲缓冲页,把磁盘中的对应页加载到该位置 +* 如果存在对应的缓存页,直接获取使用 -key: -* 显示 MySQL 在查询中实际使用的索引,若没有使用索引,显示为 NULL -* 查询中若使用了**覆盖索引**,则该索引可能出现在 key 列表,不出现在 possible_keys -key_len: +*** -* 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度 -* key_len 显示的值为索引字段的最大可能长度,并非实际使用长度,即 key_len 是根据表定义计算而得,不是通过表内检索出的 -* 在不损失精确性的前提下,长度越短越好 +#### 内存管理 -*** +##### Free 链表 +MySQL 启动时完成对 Buffer Pool 的初始化,先向操作系统申请连续的内存空间,然后将内存划分为若干对控制块和缓冲页。为了区分空闲和已占用的数据页,将所有缓冲页对应的控制块作为一个节点放入一个链表中,就是 Free 链表(**空闲链表**) + -##### Extra +基节点:是一块单独申请的内存空间(占 40 字节),并不在Buffer Pool的那一大片连续内存空间里 -其他的额外的执行计划信息,在该列展示: +磁盘加载页的流程: -* No tables used:查询语句中使用 FROM dual 或者没有 FROM 语句 -* Impossible WHERE:查询语句中的 WHERE 子句条件永远为 FALSE,会导致没有符合条件的行 -* Using index:该值表示相应的 SELECT 操作中使用了**覆盖索引**(Covering Index) -* Using index condition:第一种情况是搜索条件中虽然出现了索引列,但是部分条件无法形成扫描区间(**索引失效**),会根据可用索引的条件先搜索一遍再匹配无法使用索引的条件,回表查询数据;第二种是使用了**索引条件下推**优化 -* Using where:搜索条件需要在 Server 层判断,判断后执行回表操作查询,无法使用索引下推 -* Using join buffer:连接查询被驱动表无法利用索引,需要连接缓冲区来存储中间结果 -* Using filesort:无法利用索引完成排序(优化方向),需要对数据使用外部排序算法,将取得的数据在内存或磁盘中进行排序 -* Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序、去重、分组等场景 -* Select tables optimized away:说明仅通过使用索引,优化器可能仅从聚合函数结果中返回一行 -* No tables used:Query 语句中使用 from dual 或不含任何 from 子句 +* 从 Free 链表中取出一个空闲的缓冲页 +* 把缓冲页对应的控制块的信息填上(页所在的表空间、页号之类的信息) +* 把缓冲页对应的 Free 链表节点(控制块)从链表中移除,表示该缓冲页已经被使用 -参考文章:https://www.cnblogs.com/ggjucheng/archive/2012/11/11/2765237.html +参考文章:https://blog.csdn.net/li1325169021/article/details/121124440 @@ -5088,497 +5316,431 @@ key_len: -#### PROFILES +##### Flush 链表 -SHOW PROFILES 能够在做 SQL 优化时分析当前会话中语句执行的**资源消耗**情况 +Flush 链表是一个用来**存储脏页**的链表,对于已经修改过的缓冲脏页,出于性能考虑并不是直接更新到磁盘,而是在未来的某个时间进行刷脏,所以需要暂时存储所有的脏页 -* 通过 have_profiling 参数,能够看到当前 MySQL 是否支持 profile: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-have_profiling.png) + -* 默认 profiling 是关闭的,可以通过 set 语句在 Session 级别开启 profiling: +后台有专门的线程每隔一段时间把脏页刷新到磁盘: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-profiling.png) +* 从 Flush 链表中刷新一部分页面到磁盘: + * 后台线程定时从 Flush 链表刷脏,根据系统的繁忙程度来决定刷新速率,这种方式称为 BUF_FLUSH_LIST + * 线程刷脏的比较慢,导致用户线程加载一个新的数据页时发现没有空闲缓冲页,此时会尝试从 LRU 链表尾部寻找未修改的缓冲页直接释放,如果没有就会将 LRU 链表尾部的一个脏页**同步刷新**到磁盘,速度较慢,这种方式称为 BUF_FLUSH_SINGLE_PAGE +* 从 LRU 链表的冷数据中刷新一部分页面到磁盘,即:BUF_FLUSH_LRU + * 后台线程会定时从 LRU 链表的尾部开始扫描一些页面,扫描的页面数量可以通过系统变量 `innodb_lru_scan_depth` 指定,如果在 LRU 链表中发现脏页,则把它们刷新到磁盘,这种方式称为 BUF_FLUSH_LRU + * 控制块里会存储该缓冲页是否被修改的信息,所以可以很容易的获取到某个缓冲页是否是脏页 - ```mysql - SET profiling=1; #开启profiling 开关; - ``` -* 执行 SHOW PROFILES 指令, 来查看 SQL 语句执行的耗时: - ```mysql - SHOW PROFILES; - ``` +参考文章:https://blog.csdn.net/li1325169021/article/details/121125765 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查看SQL语句执行耗时.png) -* 查看到该 SQL 执行过程中每个线程的状态和消耗的时间: - ```mysql - SHOW PROFILE FOR QUERY query_id; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL执行每个状态消耗的时间.png) - **Sending data 状态**表示 MySQL 线程开始访问数据行并把结果返回给客户端,而不仅仅是返回给客户端。由于在 Sending data 状态下,MySQL 线程需要做大量磁盘读取操作,所以是整个查询中耗时最长的状态。 +*** -* 在获取到最消耗时间的线程状态后,MySQL 支持选择 all、cpu、block io 、context switch、page faults 等类型查看 MySQL 在使用什么资源上耗费了过高的时间。例如,选择查看 CPU 的耗费时间: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SQL执行每个状态消耗的CPU.png) - * Status:SQL 语句执行的状态 - * Durationsql:执行过程中每一个步骤的耗时 - * CPU_user:当前用户占有的 CPU - * CPU_system:系统占有的 CPU +##### LRU 链表 +当 Buffer Pool 中没有空闲缓冲页时就需要淘汰掉最近最少使用的部分缓冲页,为了实现这个功能,MySQL 创建了一个 LRU 链表,当访问某个页时: +* 如果该页不在 Buffer Pool 中,把该页从磁盘加载进来后会将该缓冲页对应的控制块作为节点放入 **LRU 链表的头部** +* 如果该页在 Buffer Pool 中,则直接把该页对应的控制块移动到 LRU 链表的头部 -*** +这样操作后 LRU 链表的尾部就是最近最少使用的缓冲页 +MySQL 基于局部性原理提供了预读功能: +* 线性预读:系统变量 `innodb_read_ahead_threshold`,如果顺序访问某个区(extent:16 KB 的页,连续 64 个形成一个区,一个区默认 1MB 大小)的页面数超过了该系统变量值,就会触发一次**异步读取**下一个区中全部的页面到 Buffer Pool 中 +* 随机预读:如果某个区 13 个连续的页面都被加载到 Buffer Pool,无论这些页面是否是顺序读取,都会触发一次**异步读取**本区所有的其他页面到 Buffer Pool 中 -#### TRACE +预读会造成加载太多用不到的数据页,造成那些使用频率很高的数据页被挤到 LRU 链表尾部,所以 InnoDB 将 LRU 链表分成两段: -MySQL 提供了对 SQL 的跟踪, 通过 trace 文件可以查看优化器生成执行计划的过程 +* 一部分存储使用频率很高的数据页,这部分链表也叫热数据,young 区 +* 一部分存储使用频率不高的冷数据,old 区,默认占 37%,可以通过系统变量 `innodb_old_blocks_pct` 指定 -* 打开 trace 功能,设置格式为 JSON,并设置 trace 的最大使用内存,避免解析过程中因默认内存过小而不能够完整展示 +当磁盘上的某数据页被初次加载到 Buffer Pool 中会被放入 old 区,淘汰时优先淘汰 old 区 - ```mysql - SET optimizer_trace="enabled=on",end_markers_in_json=ON; -- 会话内有效 - SET optimizer_trace_max_mem_size=1000000; - ``` +* 当对 old 区的数据进行访问时,会在控制块记录下访问时间,等待后续的访问时间与第一次访问的时间是否在某个时间间隔内,通过系统变量 `innodb_old_blocks_time` 指定时间间隔,默认 1000ms,成立就移动到 young 区的链表头部 +* `innodb_old_blocks_time` 为 0 时,每次访问一个页面都会放入 young 区的头部 -* 执行 SQL 语句: - ```mysql - SELECT * FROM tb_item WHERE id < 4; - ``` -* 检查 information_schema.optimizer_trace: - ```mysql - SELECT * FROM information_schema.optimizer_trace \G; -- \G代表竖列展示 - ``` - - 执行信息主要有三个阶段:prepare 阶段、optimize 阶段(成本分析)、execute 阶段(执行) +*** +#### 参数优化 -**** +Innodb 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 Innodb 的索引块,也用来缓存 Innodb 的数据块,可以通过下面的指令查看 Buffer Pool 的状态信息: +```mysql +SHOW ENGINE INNODB STATUS\G +``` +核心参数: -### 索引失效 +* `innodb_buffer_pool_size`:该变量决定了 Innodb 存储引擎表数据和索引数据的最大缓存区大小,默认 128M -#### 创建索引 + ```mysql + SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; + ``` -索引是数据库优化最重要的手段之一,通过索引通常可以帮助用户解决大多数的 MySQL 的性能优化问题 + 在保证操作系统及其他程序有足够内存可用的情况下,`innodb_buffer_pool_size` 的值越大,缓存命中率越高 -```mysql -CREATE TABLE `tb_seller` ( - `sellerid` varchar (100), - `name` varchar (100), - `nickname` varchar (50), - `password` varchar (60), - `status` varchar (1), - `address` varchar (100), - `createtime` datetime, - PRIMARY KEY(`sellerid`) -)ENGINE=INNODB DEFAULT CHARSET=utf8mb4; -INSERT INTO `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('xiaomi','小米科技','小米官方旗舰店','e10adc3949ba59abbe56e057f20f883e','1','西安市','2088-01-01 12:00:00'); -CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); -``` + ```sh + innodb_buffer_pool_size=512M + ``` -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引环境准备.png) +* `innodb_log_buffer_size`:该值决定了 Innodb 日志缓冲区的大小,保存要写入磁盘上的日志文件数据 + 对于可能产生大量更新记录的大事务,增加该值的大小,可以避免 Innodb 在事务提交前就执行不必要的日志写入磁盘操作,影响执行效率,通过配置文件修改: + ```sh + innodb_log_buffer_size=10M + ``` -**** +在多线程下,访问 Buffer Pool 中的各种链表都需要加锁,所以将 Buffer Pool 拆成若干个小实例,每个实例独立管理内存空间和各种链表(类似 ThreadLocal),多线程访问各实例互不影响,提高了并发能力 +* 在系统启动时设置系统变量 `innodb_buffer_pool_instance` 可以指定 Buffer Pool 实例的个数,但是当 Buffer Pool 小于 1GB 时,设置多个实例时无效的 +MySQL 5.7.5 之前 `innodb_buffer_pool_size` 只支持在系统启动时修改,现在已经支持运行时修改 Buffer Pool 的大小,但是每次调整参数都会重新向操作系统申请一块连续的内存空间,将旧的缓冲池的内容拷贝到新空间非常耗时,所以 MySQL 开始以一个 chunk 为单位向操作系统申请内存,所以一个 Buffer Pool 实例由多个 chunk 组成 -#### 避免失效 +* 指定系统变量 `innodb_buffer_pool_chunk_size` 来改变 chunk 的大小,只能在启动时修改,运行中不能修改,而且该变量并不包含缓冲页的控制块的内存大小 +* `innodb_buffer_pool_size` 必须是 `innodb_buffer_pool_chunk_size × innodb_buffer_pool_instance` 的倍数,默认值是 `128M × 16 = 2G`,Buffer Pool 必须是 2G 的整数倍,如果指定 5G,会自动调整成 6G -##### 语句错误 +* 如果启动时 `chunk × instances` > `pool_size`,那么 chunk 的值会自动设置为 `pool_size ÷ instances` -* 全值匹配:对索引中所有列都指定具体值,这种情况索引生效,执行效率高 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市'; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引1.png) -* **最左前缀法则**:联合索引遵守最左前缀法则 - 匹配最左前缀法则,走索引: +*** - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技'; - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status='1'; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引2.png) - 违法最左前缀法则 , 索引失效: +### 并发优化 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE status='1'; - EXPLAIN SELECT * FROM tb_seller WHERE status='1' AND address='西安市'; - ``` +MySQL Server 是多线程结构,包括后台线程和客户服务线程。多线程可以有效利用服务器资源,提高数据库的并发性能。在 MySQL 中,控制并发连接和线程的主要参数: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引3.png) +* max_connections:控制允许连接到 MySQL 数据库的最大连接数,默认值是 151 - 如果符合最左法则,但是出现跳跃某一列,只有最左列索引生效: + 如果状态变量 connection_errors_max_connections 不为零,并且一直增长,则说明不断有连接请求因数据库连接数已达到允许最大值而失败,这时可以考虑增大max_connections 的值 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND address='西安市'; - ``` + Mysql 最大可支持的连接数取决于很多因素,包括操作系统平台的线程库的质量、内存大小、每个连接的负荷、CPU的处理速度、期望的响应时间等。在 Linux 平台下,性能好的服务器,可以支持 500-1000 个连接,需要根据服务器性能进行评估设定 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引4.png) +* back_log:控制 MySQL 监听 TCP 端口时的积压请求栈的大小 - 虽然索引列失效,但是系统**使用了索引下推进行了优化** + 如果 Mysql 的连接数达到 max_connections 时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即 back_log。如果等待连接的数量超过 back_log,将不被授予连接资源直接报错 -* **范围查询**右边的列,不能使用索引: + 5.6.6 版本之前默认值为 50,之后的版本默认为 `50 + (max_connections/5)`,但最大不超过900,如果需要数据库在较短的时间内处理大量连接请求, 可以考虑适当增大 back_log 的值 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status>'1' AND address='西安市'; - ``` +* table_open_cache:控制所有 SQL 语句执行线程可打开表缓存的数量 - 根据前面的两个字段 name , status 查询是走索引的, 但是最后一个条件 address 没有用到索引,使用了索引下推 + 在执行 SQL 语句时,每个执行线程至少要打开1个表缓存,该参数的值应该根据设置的最大连接数以及每个连接执行关联查询中涉及的表的最大数量来设定:`max_connections * N` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引5.png) +* thread_cache_size:可控制 MySQL 缓存客户服务线程的数量 -* 在索引列上进行**运算操作**, 索引将失效: + 为了加快连接数据库的速度,MySQL 会缓存一定数量的客户服务线程以备重用,池化思想 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE SUBSTRING(name,3,2) = '科技'; - ``` +* innodb_lock_wait_timeout:设置 InnoDB 事务等待行锁的时间,默认值是 50ms - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引6.png) + 对于需要快速反馈的业务系统,可以将行锁的等待时间调小,以避免事务被长时间挂起; 对于后台运行的批量处理程序来说,可以将行锁的等待时间调大,以避免发生大的回滚操作 -* **字符串不加单引号**,造成索引失效: - 在查询时,没有对字符串加单引号,MySQL 的查询优化器,会自动的进行类型转换,造成索引失效 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status=1; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引7.png) -* **用 OR 分割条件,索引失效**,导致全表查询: - OR 前的条件中的列有索引而后面的列中没有索引或 OR 前后两个列是同一个复合索引,都造成索引失效 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name='阿里巴巴' OR createtime = '2088-01-01 12:00:00'; - EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' OR status='1'; - ``` +*** - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引10.png) - **AND 分割的条件不影响**: - ```mysql - 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) -* **以 % 开头的 LIKE 模糊查询**,索引失效: +## 事务机制 - 如果是尾部模糊匹配,索引不会失效;如果是头部模糊匹配,索引失效。 +### 管理事务 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name like '%科技%'; - ``` +事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 sql 语句,这些语句要么都执行,要么都不执行。作为一个关系型数据库,MySQL 支持事务。 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引12.png) +事务的四大特征:ACID - 解决方案:通过覆盖索引来解决 +- 原子性 (atomicity) +- 一致性 (consistency) +- 隔离性 (isolaction) +- 持久性 (durability) - ```mysql - EXPLAIN SELECT sellerid,name,status FROM tb_seller WHERE name like '%科技%'; - ``` +单元中的每条 SQL 语句都相互依赖,形成一个整体 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引13.png) +* 如果某条 SQL 语句执行失败或者出现错误,那么整个单元就会回滚,撤回到事务最初的状态 - 原因:在覆盖索引的这棵 B+ 数上只需要进行 like 的匹配,或者是基于覆盖索引查询再进行 WHERE 的判断就可以获得结果 +* 如果单元中所有的 SQL 语句都执行成功,则事务就顺利执行 +管理事务的三个步骤 +1. 开启事务:记录回滚点,并通知服务器,将要执行一组操作,要么同时成功、要么同时失败 -*** +2. 执行 SQL 语句:执行具体的一条或多条 SQL 语句 +3. 结束事务(提交|回滚) + - 提交:没出现问题,数据进行更新 + - 回滚:出现问题,数据恢复到开启事务时的状态 -##### 系统优化 -系统优化为全表扫描: +事务操作: -* 如果 MySQL 评估使用索引比全表更慢,则不使用索引,索引失效: +* 开启事务 ```mysql - CREATE INDEX idx_address ON tb_seller(address); - EXPLAIN SELECT * FROM tb_seller WHERE address='西安市'; - EXPLAIN SELECT * FROM tb_seller WHERE address='北京市'; + START TRANSACTION; ``` - 北京市的键值占 9/10(区分度低),所以优化为全表扫描,type = ALL +* 回滚事务 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引14.png) + ```mysql + ROLLBACK; + ``` -* IS NULL、IS NOT NULL **有时**索引失效: +* 提交事务,显示执行是手动提交,MySQL 默认为自动提交 ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE name IS NULL; - EXPLAIN SELECT * FROM tb_seller WHERE name IS NOT NULL; + COMMIT; ``` - NOT NULL 失效的原因是 name 列全部不是 null,优化为全表扫描,当 NULL 过多时,IS NULL 失效 + 工作原理: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引15.png) + * 自动提交模式下,如果没有 start transaction 显式地开始一个事务,那么**每个 SQL 语句都会被当做一个事务执行提交操作** + * 手动提交模式下,所有的 SQL 语句都在一个事务中,直到执行了 commit 或 rollback -* IN 肯定会走索引,但是当 IN 的取值范围较大时会导致索引失效,走全表扫描: + * 存在一些特殊的命令,在事务中执行了这些命令会马上强制执行 COMMIT 提交事务,如 DDL 语句 (create/drop/alter/table)、lock tables 语句等 - ```mysql - EXPLAIN SELECT * FROM tb_seller WHERE sellerId IN ('alibaba','huawei');-- 都走索引 - EXPLAIN SELECT * FROM tb_seller WHERE sellerId NOT IN ('alibaba','huawei'); - ``` + 提交方式语法: + - 查看事务提交方式 + ```mysql + SELECT @@AUTOCOMMIT; -- 1 代表自动提交 0 代表手动提交 + ``` -*** + - 修改事务提交方式 + ```mysql + SET @@AUTOCOMMIT=数字; -- 系统 + SET AUTOCOMMIT=数字; -- 会话 + ``` + - **系统变量的操作**: -#### 底层原理 + ```sql + SET [GLOBAL|SESSION] 变量名 = 值; -- 默认是会话 + SET @@[(GLOBAL|SESSION).]变量名 = 值; -- 默认是系统 + ``` -索引失效一般是针对联合索引,联合索引一般由几个字段组成,排序方式是先按照第一个字段进行排序,然后排序第二个,依此类推,图示(a, b)索引,**a 相等的情况下 b 是有序的** + ```sql + SHOW [GLOBAL|SESSION] VARIABLES [LIKE '变量%']; -- 默认查看会话内系统变量值 + ``` - +* 操作演示 -* 最左前缀法则:当不匹配前面的字段的时候,后面的字段都是无序的。这种无序不仅体现在叶子节点,也会**导致查询时扫描的非叶子节点也是无序的**,因为索引树相当于忽略的第一个字段,就无法使用二分查找 + ```mysql + -- 开启事务 + START TRANSACTION; + + -- 张三给李四转账500元 + -- 1.张三账户-500 + UPDATE account SET money=money-500 WHERE NAME='张三'; + -- 2.李四账户+500 + UPDATE account SET money=money+500 WHERE NAME='李四'; + + -- 回滚事务(出现问题) + ROLLBACK; + + -- 提交事务(没出现问题) + COMMIT; + ``` -* 范围查询右边的列,不能使用索引,比如语句: `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) +*** -*** +### 隔离级别 +事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别,否则就会产生问题。 -#### 查看索引 +隔离级别分类: -```mysql -SHOW STATUS LIKE 'Handler_read%'; -SHOW GLOBAL STATUS LIKE 'Handler_read%'; -``` +| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | +| ---------------- | -------- | -------------------------------- | ------------------- | +| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | +| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | +| repeatable read | 可重复读 | 幻读 | MySQL | +| serializable | 串行化 | 无(因为写会加写锁,读会加读锁) | | -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL查看索引使用情况.png) +一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 -* Handler_read_first:索引中第一条被读的次数,如果较高,表示服务器正执行大量全索引扫描(这个值越低越好) +* 丢失更新 (Lost Update):当两个或多个事务选择同一行,最初的事务修改的值,被后面事务修改的值覆盖,所有的隔离级别都可以避免丢失更新(行锁) -* Handler_read_key:如果索引正在工作,这个值代表一个行被索引值读的次数,值越低表示索引不经常使用(这个值越高越好) +* 脏读 (Dirty Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个**未提交**的事务中的数据 -* Handler_read_next:按照键顺序读下一行的请求数,如果范围约束或执行索引扫描来查询索引列,值增加 +* 不可重复读 (Non-Repeatable Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 -* Handler_read_prev:按照键顺序读前一行的请求数,该读方法主要用于优化 ORDER BY ... DESC + > 可重复读的意思是不管读几次,结果都一样,可以重复的读,可以理解为快照读,要读的数据集不会发生变化 -* Handler_read_rnd:根据固定位置读一行的请求数,如果执行大量查询并对结果进行排序则该值较高,可能是使用了大量需要 MySQL 扫描整个表的查询或连接,这个值较高意味着运行效率低,应该建立索引来解决 +* 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,后一次查询查到了前一次查询没有查到的行,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入 -* Handler_read_rnd_next:在数据文件中读下一行的请求数,如果正进行大量的表扫描,该值较高,说明表索引不正确或写入的查询没有利用索引 +**隔离级别操作语法:** +* 查询数据库隔离级别 + ```mysql + SELECT @@TX_ISOLATION; + SHOW VARIABLES LIKE 'tx_isolation'; + ``` -*** +* 修改数据库隔离级别 + ```mysql + SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串; + ``` -### SQL优化 -#### 覆盖索引 -复合索引叶子节点不仅保存了复合索引的值,还有主键索引,所以使用覆盖索引的时候,加上主键也会用到索引 -尽量使用覆盖索引,避免 SELECT *: +*** -```mysql -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) -如果查询列,超出索引列,也会降低性能: +### 原子特性 -```mysql -EXPLAIN SELECT name,status,address,password FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市'; -``` +原子性是指事务是一个不可分割的工作单位,事务的操作如果成功就必须要完全应用到数据库,失败则不能对数据库有任何影响。比如事务中一个 SQL 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用索引9.png) +InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 undo log(回滚日志) +* redo log 用于保证事务持久性 +* undo log 用于保证事务原子性和隔离性 +undo log 属于逻辑日志,根据每行操作进行记录,记录了 SQL 执行相关的信息,用来回滚行记录到某个版本 -**** +当事务对数据库进行修改时,InnoDB 会先记录对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容**做与之前相反的操作**: +* 对于每个 insert,回滚时会执行 delete +* 对于每个 delete,回滚时会执行 insert -#### 减少访问 +* 对于每个 update,回滚时会执行一个相反的 update,把数据修改回去 -避免对数据进行重复检索:能够一次连接就获取到结果的,就不用两次连接,这样可以大大减少对数据库无用的重复请求 +undo log 是采用段(segment)的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment + +rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segment + +* 在以前老版本,只支持 1 个 rollback segment,只能记录 1024 个 undo log segment +* MySQL5.5 开始支持 128 个 rollback segment,支持 128*1024 个 undo 操作 + + + +参考文章:https://www.cnblogs.com/kismetv/p/10331633.html -* 查询数据: - ```mysql - SELECT id,name FROM tb_book; - SELECT id,status FROM tb_book; -- 向数据库提交两次请求,数据库就要做两次查询操作 - -- > 优化为: - SELECT id,name,statu FROM tb_book; - ``` -* 插入数据: - ```mysql - INSERT INTO tb_test VALUES(1,'Tom'); - INSERT INTO tb_test VALUES(2,'Cat'); - INSERT INTO tb_test VALUES(3,'Jerry'); -- 连接三次数据库 - -- >优化为 - INSERT INTO tb_test VALUES(1,'Tom'),(2,'Cat'),(3,'Jerry'); -- 连接一次 - ``` - -* 在事务中进行数据插入: - ```mysql - start transaction; - INSERT INTO tb_test VALUES(1,'Tom'); - INSERT INTO tb_test VALUES(2,'Cat'); - INSERT INTO tb_test VALUES(3,'Jerry'); - commit; -- 手动提交,分段提交 - ``` +*** -* 数据有序插入: - ```mysql - INSERT INTO tb_test VALUES(1,'Tom'); - INSERT INTO tb_test VALUES(2,'Cat'); - INSERT INTO tb_test VALUES(3,'Jerry'); - ``` -增加 cache 层:在应用中增加缓存层来达到减轻数据库负担的目的。可以部分数据从数据库中抽取出来放到应用端以文本方式存储,或者使用框架(Mybatis)提供的一级缓存 / 二级缓存,或者使用 Redis 数据库来缓存数据 +### 隔离特性 +#### 实现方式 +隔离性是指,事务内部的操作与其他事务是隔离的,多个并发事务之间要相互隔离,不能互相干扰 -*** +* 严格的隔离性,对应了事务隔离级别中的 serializable,实际应用中对性能考虑很少使用可串行化 +* 与原子性、持久性侧重于研究事务本身不同,隔离性研究的是**不同事务**之间的相互影响 +隔离性让并发情形下的事务之间互不干扰: -#### 数据插入 +- 一个事务的写操作对另一个事务的写操作(写写):锁机制保证隔离性 +- 一个事务的写操作对另一个事务的读操作(读写):MVCC 保证隔离性 -当使用 load 命令导入数据的时候,适当的设置可以提高导入的效率: +锁机制:事务在修改数据之前,需要先获得相应的锁,获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁(详解见锁机制) -![](https://gitee.com/seazean/images/raw/master/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'; -- 文件格式如上图 -``` -对于 InnoDB 类型的表,有以下几种方式可以提高导入的效率: +*** -1. **主键顺序插入**:因为 InnoDB 类型的表是按照主键的顺序保存的,所以将导入的数据按照主键的顺序排列,可以有效的提高导入数据的效率,如果 InnoDB 表没有主键,那么系统会自动默认创建一个内部列作为主键。 - **主键是否连续对性能影响不大,只要是递增的就可以**,比如雪花算法产生的 ID 不是连续的,但是是递增的 - * 插入 ID 顺序排列数据: +#### 并发控制 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入ID顺序排列数据.png) +MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来**解决读写冲突的无锁并发控制** - * 插入 ID 无序排列数据: +MVCC 处理读写请求,可以做到在发生读写请求冲突时不用加锁,这个读是指的快照读,而不是当前读 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入ID无序排列数据.png) +* 快照读:实现基于 MVCC,因为是多版本并发,所以快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据 +* 当前读:读取数据库记录是当前最新的版本(产生幻读、不可重复读),可以对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读 -2. **关闭唯一性校验**:在导入数据前执行 `SET UNIQUE_CHECKS=0`,关闭唯一性校验;导入结束后执行 `SET UNIQUE_CHECKS=1`,恢复唯一性校验,可以提高导入的效率。 +数据库并发场景: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入数据关闭唯一性校验.png) +* 读-读:不存在任何问题,也不需要并发控制 -3. **手动提交事务**:如果应用使用自动提交的方式,建议在导入前执行`SET AUTOCOMMIT=0`,关闭自动提交;导入结束后再打开自动提交,可以提高导入的效率。 +* 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读 - 事务需要控制大小,事务太大可能会影响执行的效率。MySQL 有 innodb_log_buffer_size 配置项,超过这个值的日志会写入磁盘数据,效率会下降,所以在事务大小达到配置项数据级前进行事务提交可以提高效率 +* 写-写:有线程安全问题,可能会存在丢失更新问题 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL插入数据手动提交事务.png) +MVCC 的优点: +* 在并发读写数据库时,做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了并发读写的性能 +* 可以解决脏读,不可重复读等事务隔离问题(加锁也能解决),但不能解决更新丢失问题 +提高读写和写写的并发性能: -**** +* MVCC + 悲观锁:MVCC 解决读写冲突,悲观锁解决写写冲突 +* MVCC + 乐观锁:MVCC 解决读写冲突,乐观锁解决写写冲突 -#### ORDER BY +参考文章:https://www.jianshu.com/p/8845ddca3b23 -数据准备: -```mysql -CREATE TABLE `emp` ( - `id` INT(11) NOT NULL AUTO_INCREMENT, - `name` VARCHAR(100) NOT NULL, - `age` INT(3) NOT NULL, - `salary` INT(11) DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=INNODB DEFAULT CHARSET=utf8mb4; -INSERT INTO `emp` (`id`, `name`, `age`, `salary`) VALUES('1','Tom','25','2300');-- ... -CREATE INDEX idx_emp_age_salary ON emp(age,salary); -``` -* 第一种是通过对返回数据进行排序,所有不通过索引直接返回结果的排序都叫 FileSort 排序,会在内存中重新排序 +*** - ```mysql - EXPLAIN SELECT * FROM emp ORDER BY age DESC; -- 年龄降序 - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序1.png) -* 第二种通过有序索引顺序扫描直接返回**有序数据**,这种情况为 Using index,不需要额外排序,操作效率高 +#### 实现原理 - ```mysql - 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) +实现原理主要是隐藏字段,undo日志,Read View 来实现的 -* 多字段排序: +数据库中的每行数据,除了自定义的字段,还有数据库隐式定义的字段: - ```mysql - EXPLAIN SELECT id,age,salary FROM emp ORDER BY age DESC, salary DESC; - EXPLAIN SELECT id,age,salary FROM emp ORDER BY salary DESC, age DESC; - EXPLAIN SELECT id,age,salary FROM emp ORDER BY age DESC, salary ASC; - ``` +* DB_TRX_ID:6byte,最近修改事务ID,记录创建该数据或最后一次修改(修改/插入)该数据的事务ID。当每个事务开启时,都会被分配一个ID,这个 ID 是递增的 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL ORDER BY排序3.png) +* DB_ROLL_PTR:7byte,回滚指针,配合 undo 日志,指向上一个旧版本(存储在 rollback segment) - 尽量减少额外的排序,通过索引直接返回有序数据。**需要满足 Order by 使用相同的索引、Order By 的顺序和索引顺序相同、Order by 的字段都是升序或都是降序**,否则需要额外的操作,就会出现 FileSort +* DB_ROW_ID:6byte,隐含的自增ID(**隐藏主键**),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 作为聚簇索引 -优化:通过创建合适的索引能够减少 Filesort 的出现,但是某些情况下条件限制不能让 Filesort 消失,就要加快 Filesort 的排序操作 +* DELETED_BIT:删除标志的隐藏字段,记录被更新或删除并不代表真的删除,而是删除位变了 -对于 Filesort , MySQL 有两种排序算法: +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC版本链隐藏字段.png) -* 两次扫描算法: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 -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 -``` @@ -5586,36 +5748,34 @@ SHOW VARIABLES LIKE 'sort_buffer_size'; -- 默认262114 -#### GROUP BY +##### undo -GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是多了排序之后的分组操作,所以在 GROUP BY 的实现过程中,与 ORDER BY 一样也可以利用到索引 +undo log 是逻辑日志,记录的是每个事务对数据执行的操作,而不是记录的全部数据,需要根据 undo log 逆推出以往事务的数据 -* 分组查询: +undo log 的作用: - ```mysql - DROP INDEX idx_emp_age_salary ON emp; - EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age; - ``` +* 保证事务进行 rollback 时的原子性和一致性,当事务进行回滚的时候可以用 undo log 的数据进行恢复 +* 用于 MVCC 快照读,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序1.png) +undo log 主要分为两种: - Using temporary:表示 MySQL 需要使用临时表来存储结果集,常见于排序和分组查询 +* insert undo log:事务在 insert 新记录时产生的 undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃 -* 查询包含 GROUP BY 但是用户想要避免排序结果的消耗, 则可以执行 ORDER BY NULL 禁止排序: +* update undo log:事务在进行 update 或 delete 时产生的 undo log,在事务回滚时需要,在快照读时也需要。不能随意删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除 - ```mysql - EXPLAIN SELECT age,COUNT(*) FROM emp GROUP BY age ORDER BY NULL; - ``` +每次对数据库记录进行改动,都会将旧值放到一条 undo 日志中,算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为**版本链**,版本链的头节点就是当前记录最新的值,链尾就是最早的旧记录 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序2.png) + -* 创建索引: +* 有个事务插入 persion 表一条新记录,name 为 Jerry,age 为 24 - ```mysql - CREATE INDEX idx_emp_age_salary ON emp(age,salary); - ``` +* 事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先记录 undo log,然后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁 +* 以此类推 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL GROUP BY排序3.png) +补充知识:purge 线程 + +* 为了实现 InnoDB 的 MVCC 机制,更新或者删除操作都只是设置一下老记录的 deleted_bit,并不真正将过时的记录删除,为了节省磁盘空间,InnoDB 有专门的 purge 线程来清理 deleted_bit 为 true 的记录 +* purge 线程维护了一个 Read view(这个 Read view 相当于系统中最老活跃事务的 Read view),如果某个记录的 deleted_bit 为 true,并且 DB_TRX_ID 相对于 purge 线程的 Read view 可见,那么这条记录一定是可以被安全清除的 @@ -5623,144 +5783,124 @@ GROUP BY 也会进行排序操作,与 ORDER BY 相比,GROUP BY 主要只是 -#### OR +##### 读视图 -对于包含 OR 的查询子句,如果要利用索引,则 OR 之间的**每个条件列都必须用到索引,而且不能使用到条件之间的复合索引**,如果没有索引,则应该考虑增加索引 +Read View 是事务进行**快照读**操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 -* 执行查询语句: +注意:这里的快照并不是把所有的数据拷贝一份副本,而是由 undo log 记录的逻辑日志,根据库中的数据进行计算出历史数据 - ```mysql - EXPLAIN SELECT * FROM emp WHERE id = 1 OR age = 30; -- 两个索引,并且不是复合索引 - ``` +工作流程:将版本链的头节点的事务 ID(最新数据事务 ID)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比,如果 DB_TRX_ID 不符合可见性,通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足特定条件的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL OR条件查询1.png) +Read View 几个属性: - ```sh - Extra: Using sort_union(idx_emp_age_salary,PRIMARY); Using where - ``` +- m_ids:生成 Read View 时当前系统中活跃的事务 id 列表(未提交的事务集合,当前事务也在其中) +- up_limit_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值(已提交的事务集合) +- low_limit_id:生成 Read View 时系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1(未开始事务) +- creator_trx_id:生成该 Read View 的事务的事务 id,就是判断该 id 的事务能读到什么数据 -* 使用 UNION 替换 OR,求并集: - - 注意:该优化只针对多个索引列有效,如果有列没有被索引,查询效率可能会因为没有选择 OR 而降低 - - ```mysql - 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) - -* UNION 要优于 OR 的原因: +creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交) - * UNION 语句的 type 值为 ref,OR 语句的 type 值为 range - * UNION 语句的 ref 值为 const,OR 语句的 ref 值为 null,const 表示是常量值引用,非常快 +* db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以这种情况下此数据对 creator 是可见的 +* db_trx_id < up_limit_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见 +* db_trx_id >= low_limit_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 +* up_limit_id <= db_trx_id < low_limit_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 + * 在列表中,说明该版本对应的事务正在运行,数据不能显示(**不能读到未提交的数据**) + * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示(**可以读到已经提交的数据**) -**** +*** -#### 嵌套查询 -MySQL 4.1 版本之后,开始支持 SQL 的子查询 +##### 工作流程 -* 可以使用 SELECT 语句来创建一个单列的查询结果,然后把结果作为过滤条件用在另一个查询中 -* 使用子查询可以一次性的完成逻辑上需要多个步骤才能完成的 SQL 操作,同时也可以避免事务或者表锁死 -* 在有些情况下,子查询是可以被更高效的连接(JOIN)替代 +表 user 数据 -例如查找有角色的所有的用户信息: +```sh +id name age +1 张三 18 +``` -* 执行计划: +Transaction 20: - ```mysql - EXPLAIN SELECT * FROM t_user WHERE id IN (SELECT user_id FROM user_role); - ``` +```mysql +START TRANSACTION; -- 开启事务 +UPDATE user SET name = '李四' WHERE id = 1; +UPDATE user SET name = '王五' WHERE id = 1; +``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL嵌套查询1.png) +Transaction 60: -* 优化后: +```mysql +START TRANSACTION; -- 开启事务 +-- 操作表的其他数据 +``` - ```mysql - EXPLAIN SELECT * FROM t_user u , user_role ur WHERE u.id = ur.user_id; - ``` +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC工作流程1.png) - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL嵌套查询2.png) +ID 为 0 的事务创建 Read View: - 连接查询之所以效率更高 ,是因为不需要在内存中创建临时表来完成逻辑上需要两个步骤的查询工作 +* m_ids:20、60 +* up_limit_id:20 +* low_limit_id:61 +* creator_trx_id:0 + +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC工作流程2.png) +只有红框部分才复合条件,所以只有张三对应的版本的数据可以被看到 +参考视频:https://www.bilibili.com/video/BV1t5411u7Fg -*** +*** -#### 分页查询 -一般分页查询时,通过创建覆盖索引能够比较好地提高性能 -一个常见的问题是 `LIMIT 200000,10`,此时需要 MySQL 扫描前 200010 记录,仅仅返回 200000 - 200010 之间的记录,其他记录丢弃,查询排序的代价非常大 +#### RC RR -* 分页查询: +Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现 - ```mysql - EXPLAIN SELECT * FROM tb_user_1 LIMIT 200000,10; - ``` +RR、RC 生成时机: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询1.png) +- RC 隔离级别下,每个快照读都会生成并获取最新的 Read View +- RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View -* 优化方式一:子查询,在索引列 id 上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容 +RC、RR 级别下的 InnoDB 快照读区别 - ```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; - ``` +- RC 级别下的,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL分页查询2.png) +- RR 级别下的某个事务的对某条记录的第一次快照读会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个Read View,所以一个事务的查询结果每次都是相同的 -* 优化方式二:方案适用于主键自增的表,可以把 LIMIT 查询转换成某个位置的查询 + 当前事务在其他事务提交之前使用过快照读,那么以后其他事务对数据的修改都是不可见的,就算以后其他事务提交了数据也不可见;早于 Read View 创建的事务所做的修改并提交的均是可见的 - ```mysql - EXPLAIN SELECT * FROM tb_user_1 WHERE id > 200000 LIMIT 10; -- 写法 1 - 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) +解决幻读问题: +- 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** + 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 Read View 并不能阻止事务去更新数据,并且把这条新记录的 trx_id 给变为当前的事务 id,对当前事务就是可见的了 -**** +- 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 -#### 使用提示 -SQL 提示,是优化数据库的一个重要手段,就是在 SQL 语句中加入一些提示来达到优化操作的目的 -* USE INDEX:在查询语句中表名的后面添加 USE INDEX 来提供 MySQL 去参考的索引列表,可以让 MySQL 不再考虑其他可用的索引 - ```mysql - CREATE INDEX idx_seller_name ON tb_seller(name); - EXPLAIN SELECT * FROM tb_seller USE INDEX(idx_seller_name) WHERE name='小米科技'; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示1.png) +*** -* IGNORE INDEX:让 MySQL 忽略一个或者多个索引,则可以使用 IGNORE INDEX 作为提示 - ```mysql - EXPLAIN SELECT * FROM tb_seller IGNORE INDEX(idx_seller_name) WHERE name = '小米科技'; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-优化SQL使用提示2.png) +### 持久特性 -* FORCE INDEX:强制 MySQL 使用一个特定的索引 +#### 重做日志 - ```mysql - 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) @@ -5770,36 +5910,46 @@ SQL 提示,是优化数据库的一个重要手段,就是在 SQL 语句中 -#### 统计计数 +#### 实现原理 -在不同的 MySQL 引擎中,count(*) 有不同的实现方式: +##### 数据恢复 -* MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高,但不支持事务 -* show table status 命令通过采样估算可以快速获取,但是不准确 -* InnoDB 表执行 count(*) 会遍历全表,虽然结果准确,但会导致性能问题 +持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 -解决方案: +Buffer Pool 是一片内存空间,可以通过 innodb_buffer_pool_size 来控制 Buffer Pool 的大小(内存优化部分会详解参数) -* 计数保存在 Redis 中,但是更新 MySQL 和 Redis 的操作不是原子的,会存在数据一致性的问题 +* Change Buffer 是 Buffer Pool 里的内存,不能无限增大,用来对增删改操作提供缓存 +* Change Buffer 的大小可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% +* 补充知识:**唯一索引的更新不能使用 Buffer**,一般只有普通索引可以使用,直接写入 Buffer 就结束 -* 计数直接放到数据库里单独的一张计数表中,利用事务解决计数精确问题: +Buffer Pool 的使用提高了读写数据的效率,但是也带了新的问题:如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入 redo log - +* 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作 +* 如果 MySQL 宕机,InnoDB 判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏(buffer pool 的任务) - 会话 B 的读操作在 T3 执行的,这时更新事务还没有提交,所以计数值加 1 这个操作对会话 B 还不可见,因此会话 B 查询的计数值和最近 100 条记录,返回的结果逻辑上就是一致的 +redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置 - 并发系统性能的角度考虑,应该先插入操作记录再更新计数表,因为更新计数表涉及到行锁的竞争,**先插入再更新能最大程度地减少事务之间的锁等待,提升并发度** +redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 -count 函数的按照效率排序:`count(字段) < count(主键id) < count(1) ≈ count(*)`,所以建议尽量使用 count(*) +redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快: -* count(主键 id):InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来返回给 Server 层,Server 判断 id 不为空就按行累加 -* count(1):InnoDB 引擎遍历整张表但不取值,Server 层对于返回的每一行,放一个数字 1 进去,判断不为空就按行累加 +* 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO +* 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,减少无效 IO -* count(字段):如果这个字段是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;如果这个字段定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加 +InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到磁盘,具体的刷盘策略: +* 通过修改参数 `innodb_flush_log_at_trx_commit` 设置: + * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待主线程每秒刷新一次 + * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功 + * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作 +* 如果写入 redo log buffer 的日志已经占据了 redo log buffer 总容量的一半了,此时就会刷入到磁盘文件,这时会影响执行效率,所以开发中应该**避免大事务** +刷脏策略: -参考文章:https://time.geekbang.org/column/article/72775 +* redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到磁盘中的数据文件中 +* Buffer Pool 内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘(大事务) +* 系统空闲时,后台线程会自动进行刷脏 +* MySQL 正常关闭时,会把内存的脏页都 flush 到磁盘上 @@ -5807,44 +5957,47 @@ count 函数的按照效率排序:`count(字段) < count(主键id) < count(1) +##### 工作流程 +MySQL 中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,**保证数据不丢失**,二者的区别是: -### 内存优化 +* 作用不同:redo log 是用于 crash recovery (故障恢复),保证 MySQL 宕机也不会影响持久性;binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制 -#### 优化原则 +* 层次不同:redo log 是 InnoDB 存储引擎实现的,而 binlog 是MySQL的服务器层实现的,同时支持 InnoDB 和其他存储引擎 -三个原则: +* 内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解) -* 将尽量多的内存分配给 MySQL 做缓存,但也要给操作系统和其他程序预留足够内存 -* MyISAM 存储引擎的数据文件读取依赖于操作系统自身的 IO 缓存,如果有 MyISAM 表,就要预留更多的内存给操作系统做 IO 缓存 -* 排序区、连接区等缓存是分配给每个数据库会话(Session)专用的,值的设置要根据最大连接数合理分配,如果设置太大,不但浪费资源,而且在并发数较高时会导致物理内存耗尽 +* 写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元 +两种日志在 update 更新数据的**作用时机**: +```sql +update T set c=c+1 where ID=2; +``` -*** + +流程说明:执行引擎将这行新数据更新到内存中(Buffer Pool)后,然后会将这个更新操作记录到 redo log buffer 里,此时 redo log 处于 prepare 状态,代表执行完成随时可以提交事务,然后执行器生成这个操作的 binlog 并**把 binlog 写入磁盘**,在提交事务后 **redo log 也持久化到磁盘** +redo log 和 binlog 都可以用于表示事务的提交状态,而**两阶段提交就是让这两个状态保持逻辑上的一致**,也有利于主从复制,更好的保持主从数据的一致性 -#### MyISAM +故障恢复数据: -MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的读写速度。对于 MyISAM 表的数据块没有特别的缓存机制,完全依赖于操作系统的 IO 缓存 +* 如果在时刻 A 发生了崩溃(crash),由于此时 binlog 还没写,redo log 也没提交,所以数据恢复的时候这个事务会回滚 +* 如果在时刻 B 发生了崩溃,redo log 和 binlog 有一个共同的数据字段叫 XID,崩溃恢复的时候,会按顺序扫描 redo log: + * 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,说明 binlog 也已经记录完整,直接从 redo log 恢复数据 + * 如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以从 binlog 恢复 redo log 的信息,进而恢复数据,提交事务 -* key_buffer_size:该变量决定 MyISAM 索引块缓存区的大小,直接影响到 MyISAM 表的存取效率 - ```mysql - SHOW VARIABLES LIKE 'key_buffer_size'; -- 单位是字节 - ``` +判断一个事务的 binlog 是否完整的方法: - 在 MySQL 配置文件中设置该值,建议至少将1/4可用内存分配给 key_buffer_size: +* statement 格式的 binlog,最后会有 COMMIT +* row 格式的 binlog,最后会有一个 XID event +* MySQL 5.6.2 版本以后,引入了 binlog-checksum 参数用来验证 binlog 内容的正确性 - ```sh - vim /etc/mysql/my.cnf - key_buffer_size=1024M - ``` -* read_buffer_size:如果需要经常顺序扫描 MyISAM 表,可以通过增大 read_buffer_size 的值来改善性能。但 read_buffer_size 是每个 Session 独占的,如果默认值设置太大,并发环境就会造成内存浪费 -* read_rnd_buffer_size:对于需要做排序的 MyISAM 表的查询,如带有 ORDER BY 子句的语句,适当增加该的值,可以改善此类的 SQL 的性能,但是 read_rnd_buffer_size 是每个 Session 独占的,如果默认值设置太大,就会造成内存浪费 +参考文章:https://time.geekbang.org/column/article/73161 @@ -5852,141 +6005,159 @@ MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的 -#### InnoDB +#### 系统优化 -Innodb 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 Innodb 的索引块,也用来缓存 Innodb 的数据块 +系统在进行刷脏时会占用一部分系统资源,会影响系统的性能,产生系统抖动 -* innodb_buffer_pool_size:该变量决定了 Innodb 存储引擎表数据和索引数据的最大缓存区大小 +* 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长 +* 日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的 - ```mysql - SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; - ``` +InnoDB 刷脏页的控制策略: - 在保证操作系统及其他程序有足够内存可用的情况下,innodb_buffer_pool_size 的值越大,缓存命中率越高 +* `innodb_io_capacity` 参数代表磁盘的读写能力,建议设置成磁盘的 IOPS(每秒的 IO 次数) - ```sh - innodb_buffer_pool_size=512M - ``` +* 刷脏速度参考两个因素:脏页比例和 redo log 写盘速度 + * 参数 `innodb_max_dirty_pages_pct` 是脏页比例上限,默认值是 75%,InnoDB 会根据当前的脏页比例,算出一个范围在 0 到 100 之间的数字 + * InnoDB 每次写入的日志都有一个序号,当前写入的序号跟 checkpoint 对应的序号之间的差值,InnoDB 根据差值算出一个范围在 0 到 100 之间的数字 + * 两者较大的值记为 R,执行引擎按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度 -* innodb_log_buffer_size:该值决定了 Innodb 日志缓冲区的大小,保存要写入磁盘上的日志文件数据 +* `innodb_flush_neighbors` 参数置为 1 代表控制刷脏时检查相邻的数据页,如果也是脏页就一起刷脏,并检查邻居的邻居,这个行为会一直蔓延直到不是脏页,在 MySQL 8.0 中该值的默认值是 0,不建议开启此功能 - 对于可能产生大量更新记录的大事务,增加 innodb_log_buffer_size 的大小,可以避免 Innodb 在事务提交前就执行不必要的日志写入磁盘操作,影响执行效率。通过配置文件修改: - ```sh - innodb_log_buffer_size=10M - ``` +**** -*** +### 一致特性 +一致性是指事务执行前后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。 -### 并发优化 +数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变) -MySQL Server 是多线程结构,包括后台线程和客户服务线程。多线程可以有效利用服务器资源,提高数据库的并发性能。在 MySQL 中,控制并发连接和线程的主要参数: +实现一致性的措施: -* max_connections:控制允许连接到 MySQL 数据库的最大连接数,默认值是 151 +- 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证 +- 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等 +- 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致 - 如果状态变量 connection_errors_max_connections 不为零,并且一直增长,则说明不断有连接请求因数据库连接数已达到允许最大值而失败,这时可以考虑增大max_connections 的值 - Mysql 最大可支持的连接数取决于很多因素,包括操作系统平台的线程库的质量、内存大小、每个连接的负荷、CPU的处理速度、期望的响应时间等。在 Linux 平台下,性能好的服务器,可以支持 500-1000 个连接,需要根据服务器性能进行评估设定 -* back_log:控制 MySQL 监听 TCP 端口时的积压请求栈的大小 - 如果 Mysql 的连接数达到 max_connections 时,新来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即 back_log。如果等待连接的数量超过 back_log,将不被授予连接资源直接报错 - 5.6.6 版本之前默认值为 50,之后的版本默认为 `50 + (max_connections/5)`,但最大不超过900,如果需要数据库在较短的时间内处理大量连接请求, 可以考虑适当增大 back_log 的值 -* table_open_cache:控制所有 SQL 语句执行线程可打开表缓存的数量 - 在执行 SQL 语句时,每个执行线程至少要打开1个表缓存,该参数的值应该根据设置的最大连接数以及每个连接执行关联查询中涉及的表的最大数量来设定:`max_connections * N` +**** -* thread_cache_size:可控制 MySQL 缓存客户服务线程的数量 - 为了加快连接数据库的速度,MySQL 会缓存一定数量的客户服务线程以备重用,池化思想 -* innodb_lock_wait_timeout:设置 InnoDB 事务等待行锁的时间,默认值是 50ms - 对于需要快速反馈的业务系统,可以将行锁的等待时间调小,以避免事务被长时间挂起; 对于后台运行的批量处理程序来说,可以将行锁的等待时间调大,以避免发生大的回滚操作 +## 锁机制 + +### 基本介绍 +锁机制:数据库为了保证数据的一致性,在共享的资源被并发访问时变得安全有序所设计的一种规则 +作用:锁机制类似于多线程中的同步,可以保证数据的一致性和安全性 +锁的分类: -*** +- 按操作分类: + - 共享锁:也叫读锁。对同一份数据,多个事务读操作可以同时加锁而不互相影响 ,但不能修改数据 + - 排他锁:也叫写锁。当前的操作没有完成前,会阻断其他操作的读取和写入 +- 按粒度分类: + - 表级锁:会锁定整个表,开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低,偏向 MyISAM + - 行级锁:会锁定当前操作行,开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突概率低,并发度高,偏向 InnoDB + - 页级锁:锁的力度、发生冲突的概率和加锁开销介于表锁和行锁之间,会出现死锁,并发性能一般 +- 按使用方式分类: + - 悲观锁:每次查询数据时都认为别人会修改,很悲观,所以查询时加锁 + - 乐观锁:每次查询数据时都认为别人不会修改,很乐观,但是更新时会判断一下在此期间别人有没有去更新这个数据 +* 不同存储引擎支持的锁 + | 存储引擎 | 表级锁 | 行级锁 | 页级锁 | + | -------- | -------- | -------- | ------ | + | MyISAM | 支持 | 不支持 | 不支持 | + | InnoDB | **支持** | **支持** | 不支持 | + | MEMORY | 支持 | 不支持 | 不支持 | + | BDB | 支持 | 不支持 | 支持 | +从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如 Web 应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并查询的应用,如一些在线事务处理系统 -## 主从复制 -### 基本介绍 +*** -复制是指将主数据库的 DDL 和 DML 操作通过二进制日志传到从库服务器中,然后在从库上对这些日志重新执行(也叫重做),从而使得从库和主库的数据保持同步 -MySQL 支持一台主库同时向多台从库进行复制,从库同时也可以作为其他从服务器的主库,实现链状复制 -MySQL 复制的优点主要包含以下三个方面: +### Server -- 主库出现问题,可以快速切换到从库提供服务 +FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处于只读状态,工作流程: -- 可以在从库上执行查询操作,从主库中更新,实现读写分离 +1. 上全局读锁(lock_global_read_lock) +2. 清理表缓存(close_cached_tables) +3. 上全局 COMMIT 锁(make_global_read_lock_block_commit) -- 可以在从库中执行备份,以避免备份期间影响主库的服务 +该命令主要用于备份工具做一致性备份,由于 FTWRL 需要持有两把全局的 MDL 锁,并且还要关闭所有表对象,因此杀伤性很大 +MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL) +MDL 叫元数据锁,主要用来保护 MySQL内部对象的元数据,保证数据读写的正确性,通过 MDL 机制保证 DDL、DML、DQL 操作的并发,**当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁** -*** +* MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,事务中的 MDL 锁,在语句执行开始时申请,在整个事务提交后释放 +* MDL 锁是在 Server 中实现,不是 InnoDB 存储引擎层不能直接实现的锁 +* MDL 锁还能实现其他粒度级别的锁,比如全局锁、库级别的锁、表空间级别的锁 -### 复制原理 -#### 主从结构 -MySQL 的主从之间维持了一个长连接。主库内部有一个线程,专门用于服务从库的长连接,连接过程: -* 从库执行 change master 命令,设置主库的 IP、端口、用户名、密码以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量 -* 从库执行 start slave 命令,这时从库会启动两个线程,就是图中的 io_thread 和 sql_thread,其中 io_thread 负责与主库建立连接 -* 主库校验完用户名、密码后,开始按照从传过来的位置,从本地读取 binlog 发给从库,开始主从复制 -主从复制原理图: +*** -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制原理图.jpg) -主从复制主要依赖的是 binlog,MySQL 默认是异步复制,需要三个线程: -- binlog dump thread:在主库事务提交时,负责把数据变更记录在二进制日志文件 binlog 中,并通知 slave 有数据更新 -- I/O thread:负责从主服务器上拉取二进制日志,并将 binlog 日志内容依次写到 relay log 中转日志的最末端,并将新的 binlog 文件名和 offset 记录到 master-info 文件中,以便下一次读取日志时从指定 binlog 日志文件及位置开始读取新的 binlog 日志内容 -- SQL thread:监测本地 relay log 中新增了日志内容,读取中继日志并重做其中的 SQL 语句,从库在 relay-log.info 中记录当前应用中继日志的文件名和位置点以便下一次执行 +### MyISAM -同步与异步: +#### 表级锁 -* 异步复制有数据丢失风险,例如数据还未同步到从库,主库就给客户端响应,然后主库挂了,此时从库晋升为主库的话数据是缺失的 -* 同步复制,主库需要将 binlog 复制到所有从库,等所有从库响应了之后主库才进行其他逻辑,这样的话性能很差,一般不会选择 -* MySQL 5.7 之出现了半同步复制,有参数可以选择成功同步几个从库就返回响应 +MyISAM 存储引擎只支持表锁,这也是 MySQL 开始几个版本中唯一支持的锁类型 + +MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表加读锁,在执行增删改之前,会**自动**给涉及的表加写锁,这个过程并不需要用户干预,所以用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁 +* 加锁命令: + 读锁:所有连接只能读取数据,不能修改 -**** + 写锁:其他连接不能查询和修改数据 + ```mysql + -- 读锁 + LOCK TABLE table_name READ; + + -- 写锁 + LOCK TABLE table_name WRITE; + ``` +* 解锁命令: -#### 主主结构 + ```mysql + -- 将当前会话所有的表进行解锁 + UNLOCK TABLES; + ``` -主主结构就是两个数据库之间总是互为主从关系,这样在切换的时候就不用再修改主从关系 +锁的兼容性: -循环复制:在库 A 上更新了一条语句,然后把生成的 binlog 发给库 B,库 B 执行完这条更新语句后也会生成 binlog,会再发给 A +* 对 MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求 +* 对 MyISAM 表的写操作,则会阻塞其他用户对同一表的读和写操作 -解决方法: +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 锁的兼容性.png) -* 两个库的 server id 必须不同,如果相同则它们之间不能设定为主主关系 -* 一个库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog -* 每个库在收到从主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志 +锁调度:MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞,所以 MyISAM 不适合做写为主的表的存储引擎 @@ -5994,110 +6165,123 @@ MySQL 的主从之间维持了一个长连接。主库内部有一个线程, -### 主从延迟 +#### 锁操作 -#### 延迟原因 +##### 读锁 -正常情况主库执行更新生成的所有 binlog,都可以传到从库并被正确地执行,从库就能达到跟主库一致的状态,这就是最终一致性 +两个客户端操作 Client 1和 Client 2,简化为 C1、C2 -主从延迟是主从之间是存在一定时间的数据不一致,就是同一个事务在从库执行完成的时间和主库执行完成的时间的差值,即 T2-T1 +* 数据准备: -- 主库 A 执行完成一个事务,写入 binlog,该时刻记为 T1 -- 日志传给从库 B,从库 B 执行完这个事务,该时刻记为 T2 + ```mysql + CREATE TABLE `tb_book` ( + `id` INT(11) AUTO_INCREMENT, + `name` VARCHAR(50) DEFAULT NULL, + `publish_time` DATE DEFAULT NULL, + `status` CHAR(1) DEFAULT NULL, + PRIMARY KEY (`id`) + ) ENGINE=MYISAM DEFAULT CHARSET=utf8 ; + + INSERT INTO tb_book (id, NAME, publish_time, STATUS) VALUES(NULL,'java编程思想','2088-08-01','1'); + INSERT INTO tb_book (id, NAME, publish_time, STATUS) VALUES(NULL,'mysql编程思想','2088-08-08','0'); + ``` -通过在从库执行 `show slave status` 命令,返回结果会显示 seconds_behind_master 表示当前从库延迟了多少秒 +* C1、C2 加读锁,同时查询可以正常查询出数据 -- 每一个事务的 binlog 都有一个时间字段,用于记录主库上**写入**的时间 -- 从库取出当前正在**执行**的事务的时间字段,跟系统的时间进行相减,得到的就是 seconds_behind_master + ```mysql + LOCK TABLE tb_book READ; -- C1、C2 + SELECT * FROM tb_book; -- C1、C2 + ``` -主从延迟的原因: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 读锁1.png) -* 从库的查询压力大 -* 大事务的执行,主库必须要等到事务完成之后才会写入 binlog,导致从节点出现应用 binlog 延迟 -* 主库的 DDL,从库与主库的 DDL 同步是串行进行,DDL 在主库执行时间很长,那么从库也会消耗同样的时间 -* 锁冲突问题也可能导致从节点的 SQL 线程执行慢 -* 从库的机器性能比主库的差,导致从库的复制能力弱 +* C1 加读锁,C1、C2查询未锁定的表,C1 报错,C2 正常查询 -主从同步问题永远都是**一致性和性能的权衡**,需要根据实际的应用场景,可以采取下面的办法: + ```mysql + LOCK TABLE tb_book READ; -- C1 + SELECT * FROM tb_user; -- C1、C2 + ``` -* 优化 SQL,避免慢 SQL,减少批量操作 -* 降低多线程大事务并发的概率,优化业务逻辑 -* 业务中大多数情况查询操作要比更新操作更多,搭建一主多从结构,让这些从库来分担读的压力 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 读锁2.png) -* 尽量采用短的链路,主库和从库服务器的距离尽量要短,提升端口带宽,减少 binlog 传输的网络延时 -* 实时性要求高的业务读强制走主库,从库只做备份 + C1、C2 执行插入操作,C1 报错,C2 等待获取 + ```mysql + 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) -*** + 当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 INSERT 语句立即执行 -#### 并行复制 +*** -##### MySQL5.6 -高并发情况下,主库的会产生大量的 binlog,在从库中有两个线程 IO Thread 和 SQL Thread 单线程执行,会导致主库延迟变大。为了改善复制延迟问题,MySQL 5.6 版本增加了并行复制功能,以采用多线程机制来促进执行 -coordinator 就是原来的 sql_thread,并行复制中它不再直接更新数据,只**负责读取中转日志和分发事务**: +##### 写锁 -* 线程分配完成并不是立即执行,为了防止造成更新覆盖,更新同一 DB 的两个事务必须被分发到同一个工作线程 -* 同一个事务不能被拆开,必须放到同一个工作线程 +两个客户端操作 Client 1和 Client 2,简化为 C1、C2 -MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库 名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 +* C1 加写锁,C1、C2查询表,C1 正常查询,C2 需要等待 -每个事务在分发的时候,跟线程的冲突(事务操作的是同一个 DB)关系包括以下三种情况: + ```mysql + LOCK TABLE tb_book WRITE; -- C1 + SELECT * FROM tb_book; -- C1、C2 + ``` -* 如果跟所有线程都不冲突,coordinator 线程就会把这个事务分配给最空闲的线程 -* 如果只跟一个线程冲突,coordinator 线程就会把这个事务分配给这个存在冲突关系的线程 -* 如果跟多于一个线程冲突,coordinator 线程就进入等待状态,直到和这个事务存在冲突关系的线程只剩下 1 个 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 写锁1.png) -优缺点: + 当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 SELECT 语句立即执行 -* 构造 hash 值的时候很快,只需要库名,而且一个实例上 DB 数也不会很多,不会出现需要构造很多个项的情况 -* 不要求 binlog 的格式,statement 格式的 binlog 也可以很容易拿到库名(日志章节详解了 binlog) -* 主库上的表都放在同一个 DB 里面,这个策略就没有效果了;或者不同 DB 的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果,需要**把相同热度的表均匀分到这些不同的 DB 中**,才可以使用这个策略 +* C1、C2 同时加写锁 + ```mysql + LOCK TABLE tb_book WRITE; + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 写锁2.png) -*** +* C1 加写锁,C1、C2查询未锁定的表,C1 报错,C2 正常查询 -##### 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,用来控制是否启用这个新策略: + ```mysql + SHOW OPEN TABLES; + ``` -* COMMIT_ORDER:表示根据同时进入 prepare 和 commit 来判断是否可以并行的策略 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-锁争用情况查看1.png) -* WRITESET:表示的是对于每个事务涉及更新的每一行,计算出这一行的 hash 值,组成该事务的 writeset 集合,如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行(**按行并行**) + In_user:表当前被查询使用的次数,如果该数为零,则表是打开的,但是当前没有被使用 -* WRITESET_SESSION:是在 WRITESET 的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序 + Name_locked:表名称是否被锁定,名称锁定用于取消表或对表进行重命名等操作 - 为了唯一标识,这个 hash 表的值是通过 `库名 + 表名 + 索引名 + 值` (表示的是某一行)计算出来的 + ```mysql + LOCK TABLE tb_book READ; -- 执行命令 + ``` -MySQL 5.7.22 按行并发的优势: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-锁争用情况查看2.png) -* writeset 是在主库生成后直接写入到 binlog 里面的,这样在备库执行的时候,不需要解析 binlog 内容,节省了计算量 -* 不需要把整个事务的 binlog 都扫一遍才能决定分发到哪个线程,更省内存 -* 从库的分发策略不依赖于 binlog 内容,所以 binlog 是 statement 格式也可以,更节约内存(因为 row 才记录更改的行) +* 查看锁状态: -MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于表上没主键、唯一和外键约束的场景,WRITESET 策略也是没法并行的,也会暂时退化为单线程模型 + ```mysql + SHOW STATUS LIKE 'Table_locks%'; + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 锁状态.png) + Table_locks_immediate:指的是能立即获得表级锁的次数,每立即获取锁,值加 1 -参考文章:https://time.geekbang.org/column/article/77083 + Table_locks_waited:指的是不能立即获取表级锁而需要等待的次数,每等待一次,该值加 1,此值高说明存在着较为严重的表级锁争用情况 @@ -6105,278 +6289,252 @@ MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于 -#### 读写分离 - -读写分离:可以降低主库的访问压力,提高系统的并发能力 - -* 主库不建查询的索引,从库建查询的索引。因为索引需要维护的,比如插入一条数据,不仅要在聚簇索引上面插入,对应的二级索引也得插入 -* 将读操作分到从库了之后,可以在主库把查询要用的索引删了,减少写操作对主库的影响 - -读写分离产生了读写延迟,造成数据的不一致性。假如客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,可能读到的还是以前的数据,叫过期读 - -解决方案: - -* 强制将写之后**立刻读的操作转移到主库**,比如刚注册的用户,直接登录从库查询可能查询不到,先走主库登录 - -* **二次查询**,如果从库查不到数据,则再去主库查一遍,由 API 封装,比较简单,但导致主库压力大 +### InnoDB -* 更新主库后,读从库之前先 sleep 一下,类似于执行一条 `select sleep(1)` 命令 -* 确保主备无延迟的方法,每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0,如果不等于那就等到这个参数变为 0 才能执行查询请求 +#### 行级锁 +InnoDB 与 MyISAM 的**最大不同**有两点:一是支持事务;二是采用了行级锁,InnoDB 同时支持表锁和行锁 +InnoDB 实现了以下两种类型的行锁: +- 共享锁 (S):又称为读锁,简称 S 锁,就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 +- 排他锁 (X):又称为写锁,简称 X 锁,就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 +对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放;对于普通 SELECT 语句,不会加任何锁 -*** +锁的兼容性: +- 共享锁和共享锁 兼容 +- 共享锁和排他锁 冲突 +- 排他锁和排他锁 冲突 +- 排他锁和共享锁 冲突 +可以通过以下语句显式给数据集加共享锁或排他锁: -### 负载均衡 +```mysql +SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE -- 共享锁 +SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 +``` -负载均衡是应用中使用非常普遍的一种优化方法,机制就是利用某种均衡算法,将固定的负载量分布到不同的服务器上,以此来降低单台服务器的负载,达到优化的效果 -* 分流查询:通过 MySQL 的主从复制,实现读写分离,使增删改操作走主节点,查询操作走从节点,从而可以降低单台服务器的读写压力 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-负载均衡主从复制.jpg) -* 分布式数据库架构:适合大数据量、负载高的情况,具有良好的拓展性和高可用性。通过在多台服务器之间分布数据,可以实现在多台服务器之间的负载均衡,提高访问效率 +*** -**** +#### 锁操作 +两个客户端操作 Client 1和 Client 2,简化为 C1、C2 -### 主从搭建 +* 环境准备 -#### 搭建流程 + ```mysql + CREATE TABLE test_innodb_lock( + id INT(11), + name VARCHAR(16), + sex VARCHAR(1) + )ENGINE = INNODB DEFAULT CHARSET=utf8; + + INSERT INTO test_innodb_lock VALUES(1,'100','1'); + -- .......... + + CREATE INDEX idx_test_innodb_lock_id ON test_innodb_lock(id); + CREATE INDEX idx_test_innodb_lock_name ON test_innodb_lock(name); + ``` -##### master +* 关闭自动提交功能: -1. 在master 的配置文件(/etc/mysql/my.cnf)中,配置如下内容: + ```mysql + SET AUTOCOMMIT=0; -- C1、C2 + ``` - ```sh - #mysql 服务ID,保证整个集群环境中唯一 - server-id=1 - - #mysql binlog 日志的存储路径和文件名 - log-bin=/var/lib/mysql/mysqlbin - - #错误日志,默认已经开启 - #log-err - - #mysql的安装目录 - #basedir - - #mysql的临时目录 - #tmpdir - - #mysql的数据存放目录 - #datadir - - #是否只读,1 代表只读, 0 代表读写 - read-only=0 - - #忽略的数据, 指不需要同步的数据库 - binlog-ignore-db=mysql - - #指定同步的数据库 - #binlog-do-db=db01 - ``` + 正常查询数据: -2. 执行完毕之后,需要重启 MySQL + ```mysql + SELECT * FROM test_innodb_lock; -- C1、C2 + ``` -3. 创建同步数据的账户,并且进行授权操作: +* 查询 id 为 3 的数据,正常查询: - ```mysql - GRANT REPLICATION SLAVE ON *.* TO 'seazean'@'192.168.0.137' IDENTIFIED BY '123456'; - FLUSH PRIVILEGES; - ``` + ```mysql + SELECT * FROM test_innodb_lock WHERE id=3; -- C1、C2 + ``` -4. 查看 master 状态: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作1.png) - ```mysql - SHOW MASTER STATUS; - ``` +* C1 更新 id 为 3 的数据,但不提交: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查看master状态.jpg) + ```mysql + UPDATE test_innodb_lock SET name='300' WHERE id=3; -- C1 + ``` - * File:从哪个日志文件开始推送日志文件 - * Position:从哪个位置开始推送日志 - * Binlog_Ignore_DB:指定不需要同步的数据库 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作2.png) + C2 查询不到 C1 修改的数据,因为隔离界别为 REPEATABLE READ,C1 提交事务,C2 查询: + ```mysql + COMMIT; -- C1 + ``` -*** + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作3.png) + 提交后仍然查询不到 C1 修改的数据,因为隔离级别可以防止脏读、不可重复读,所以 C2 需要提交才可以查询到其他事务对数据的修改: + ```mysql + COMMIT; -- C2 + SELECT * FROM test_innodb_lock WHERE id=3; -- C2 + ``` -##### slave + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作4.png) -1. 在 slave 端配置文件中,配置如下内容: +* C1 更新 id 为 3 的数据,但不提交,C2 也更新 id 为 3 的数据: - ```sh - #mysql服务端ID,唯一 - server-id=2 - - #指定binlog日志 - log-bin=/var/lib/mysql/mysqlbin - ``` + ```mysql + UPDATE test_innodb_lock SET name='3' WHERE id=3; -- C1 + UPDATE test_innodb_lock SET name='30' WHERE id=3; -- C2 + ``` -2. 执行完毕之后,需要重启 MySQL + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作5.png) -3. 指定当前从库对应的主库的IP地址、用户名、密码,从哪个日志文件开始的那个位置开始同步推送日志 + 当 C1 提交,C2 直接解除阻塞,直接更新 - ```mysql - CHANGE MASTER TO MASTER_HOST= '192.168.0.138', MASTER_USER='seazean', MASTER_PASSWORD='seazean', MASTER_LOG_FILE='mysqlbin.000001', MASTER_LOG_POS=413; - ``` +* 操作不同行的数据: -4. 开启同步操作: + ```mysql + UPDATE test_innodb_lock SET name='10' WHERE id=1; -- C1 + UPDATE test_innodb_lock SET name='30' WHERE id=3; -- C2 + ``` - ```mysql - START SLAVE; - SHOW SLAVE STATUS; - ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作6.png) -5. 停止同步操作: + 由于C1、C2 操作的不同行,获取不同的行锁,所以都可以正常获取行锁 - ```mysql - STOP SLAVE; - ``` +​ *** -##### 验证 +#### 锁分类 -1. 在主库中创建数据库,创建表并插入数据: +##### 间隙锁 - ```mysql - CREATE DATABASE db01; - USE db01; - CREATE TABLE user( - id INT(11) NOT NULL AUTO_INCREMENT, - name VARCHAR(50) NOT NULL, - sex VARCHAR(1), - PRIMARY KEY (id) - )ENGINE=INNODB DEFAULT CHARSET=utf8; - - INSERT INTO user(id,NAME,sex) VALUES(NULL,'Tom','1'); - INSERT INTO user(id,NAME,sex) VALUES(NULL,'Trigger','0'); - INSERT INTO user(id,NAME,sex) VALUES(NULL,'Dawn','1'); - ``` +当使用范围条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据进行加锁,对于键值在条件范围内但并不存在的记录,叫做间隙(GAP), InnoDB 会对间隙进行加锁,就是间隙锁 -2. 在从库中查询数据,进行验证: +* 唯一索引加锁只有在值存在时才是行锁,值不存在会变成间隙锁,所以范围查询时容易出现间隙锁 +* 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 - 在从库中,可以查看到刚才创建的数据库: +加锁的基本单位是 next-key lock,该锁是行锁和这条记录前面的 gap lock 的组合,就是行锁加间隙锁 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制验证1.jpg) +* 加锁遵循前开后闭原则 +* 假设有索引值 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会同时对间隙 (10,11]、(11,13] 加锁 - 在该数据库中,查询表中的数据: +间隙锁优点:RR 级别下间隙锁可以解决事务的**幻读问题**,通过对间隙加锁,防止读取过程中数据条目发生变化 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制验证2.jpg) +间隙锁危害:当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害 +* 关闭自动提交功能: + ```mysql + SET AUTOCOMMIT=0; -- C1、C2 + ``` -*** +* 查询数据表: + ```mysql + SELECT * FROM test_innodb_lock; + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 间隙锁1.png) -#### 主从切换 +* C1 根据 id 范围更新数据,C2 插入数据: -正常切换步骤: + ```mysql + UPDATE test_innodb_lock SET name='8888' WHERE id < 4; -- C1 + INSERT INTO test_innodb_lock VALUES(2,'200','2'); -- C2 + ``` -* 在开始切换之前先对主库进行锁表 `flush tables with read lock`,然后等待所有语句执行完成,切换完成后可以释放锁 + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 间隙锁2.png) -* 检查 slave 同步状态,在 slave 执行 `show processlist` + 出现间隙锁,C2 被阻塞,等待 C1 提交事务后才能更新 -* 停止 slave io 线程,执行命令 `STOP SLAVE IO_THREAD` -* 提升 slave 为 master - ```sql - Stop slave; - Reset master; - Reset slave all; - set global read_only=off; -- 设置为可更新状态 - ``` +*** -* 将原来 master 变为 slave(参考搭建流程中的 slave 方法) -主库发生故障,从库会进行上位,其他从库指向新的主库 +##### 意向锁 +InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支持在不同粒度上的加锁操作,InnoDB 增加了意向锁(Intention Lock ) +意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁,意向锁分为两种: +* 意向共享锁(IS):事务有意向对表中的某些行加共享锁 -**** +* 意向排他锁(IX):事务有意向对表中的某些行加排他锁 +InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除全表扫描以外的任何请求,表级意向锁与行级锁的兼容性如下所示: +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-意向锁兼容性.png) +插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设某列有索引值2,6,只要两个事务插入位置不同,如事务 A 插入 3,事务 B 插入 4,那么就可以同时插入 -## 锁机制 -### 基本介绍 +*** -锁机制:数据库为了保证数据的一致性,在共享的资源被并发访问时变得安全有序所设计的一种规则 -作用:锁机制类似于多线程中的同步,可以保证数据的一致性和安全性 -锁的分类: +##### 死锁 -- 按操作分类: - - 共享锁:也叫读锁。对同一份数据,多个事务读操作可以同时加锁而不互相影响 ,但不能修改数据 - - 排他锁:也叫写锁。当前的操作没有完成前,会阻断其他操作的读取和写入 -- 按粒度分类: - - 表级锁:会锁定整个表,开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低,偏向 MyISAM - - 行级锁:会锁定当前操作行,开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突概率低,并发度高,偏向 InnoDB - - 页级锁:锁的力度、发生冲突的概率和加锁开销介于表锁和行锁之间,会出现死锁,并发性能一般 -- 按使用方式分类: - - 悲观锁:每次查询数据时都认为别人会修改,很悲观,所以查询时加锁 - - 乐观锁:每次查询数据时都认为别人不会修改,很乐观,但是更新时会判断一下在此期间别人有没有去更新这个数据 +当并发系统中不同线程出现循环资源依赖,线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁 -* 不同存储引擎支持的锁 +死锁情况:线程 A 修改了 id = 1 的数据,请求修改 id = 2 的数据,线程 B 修改了 id = 2 的数据,请求修改 id = 1 的数据,产生死锁 - | 存储引擎 | 表级锁 | 行级锁 | 页级锁 | - | -------- | -------- | -------- | ------ | - | MyISAM | 支持 | 不支持 | 不支持 | - | InnoDB | **支持** | **支持** | 不支持 | - | MEMORY | 支持 | 不支持 | 不支持 | - | BDB | 支持 | 不支持 | 支持 | +解决策略: -从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如 Web 应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并查询的应用,如一些在线事务处理系统 +* 直接进入等待直到超时,超时时间可以通过参数 innodb_lock_wait_timeout 来设置,但是时间的设置不好控制,超时可能不是因为死锁,而是因为事务处理比较慢,所以一般不采取该方式 +* 主动死锁检测,发现死锁后**主动回滚死锁链条中的某一个事务**,让其他事务得以继续执行,将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑 -*** +**** -### Server +#### 锁优化 -FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处于只读状态,工作流程: +##### 锁升级 -1. 上全局读锁(lock_global_read_lock) -2. 清理表缓存(close_cached_tables) -3. 上全局 COMMIT 锁(make_global_read_lock_block_commit) +索引失效造成行锁升级为表锁,不通过索引检索数据,InnoDB 会将对表中的所有记录加锁,实际效果和**表锁**一样实际开发过程应避免出现索引失效的状况 -该命令主要用于备份工具做一致性备份,由于 FTWRL 需要持有两把全局的 MDL 锁,并且还要关闭所有表对象,因此杀伤性很大 +* 查看当前表的索引: -MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL) + ```mysql + SHOW INDEX FROM test_innodb_lock; + ``` -MDL 叫元数据锁,主要用来保护 MySQL内部对象的元数据,保证数据读写的正确性,通过 MDL 机制保证 DDL、DML、DQL 操作的并发,**当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁** +* 关闭自动提交功能: -* MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,事务中的 MDL 锁,在语句执行开始时申请,在整个事务提交后释放 + ```mysql + SET AUTOCOMMIT=0; -- C1、C2 + ``` -* MDL 锁是在 Server 中实现,不是 InnoDB 存储引擎层不能直接实现的锁 +* 执行更新语句: -* MDL 锁还能实现其他粒度级别的锁,比如全局锁、库级别的锁、表空间级别的锁 + ```mysql + UPDATE test_innodb_lock SET sex='2' WHERE name=10; -- C1 + UPDATE test_innodb_lock SET sex='2' WHERE id=3; -- C2 + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁升级.png) + 索引失效:执行更新时 name 字段为 varchar 类型,造成索引失效,最终行锁变为表锁 @@ -6384,129 +6542,116 @@ MDL 叫元数据锁,主要用来保护 MySQL内部对象的元数据,保证 -### MyISAM - -#### 表级锁 +##### 优化锁 -MyISAM 存储引擎只支持表锁,这也是 MySQL 开始几个版本中唯一支持的锁类型 +InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高,但是在整体并发处理能力方面要远远优于 MyISAM 的表锁,当系统并发量较高的时候,InnoDB 的整体性能远远好于 MyISAM -MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表加读锁,在执行增删改之前,会**自动**给涉及的表加写锁,这个过程并不需要用户干预,所以用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁 +但是使用不当可能会让InnoDB 的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差 -* 加锁命令: +优化建议: - 读锁:所有连接只能读取数据,不能修改 +- 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁 +- 合理设计索引,尽量缩小锁的范围 +- 尽可能减少索引条件及索引范围,避免间隙锁 +- 尽量控制事务大小,减少锁定资源量和时间长度 +- 尽可使用低级别事务隔离(需要业务层面满足需求) - 写锁:其他连接不能查询和修改数据 - ```mysql - -- 读锁 - LOCK TABLE table_name READ; - - -- 写锁 - LOCK TABLE table_name WRITE; - ``` -* 解锁命令: - ```mysql - -- 将当前会话所有的表进行解锁 - UNLOCK TABLES; - ``` -锁的兼容性: +*** -* 对 MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求 -* 对 MyISAM 表的写操作,则会阻塞其他用户对同一表的读和写操作 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 锁的兼容性.png) -锁调度:MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞,所以 MyISAM 不适合做写为主的表的存储引擎 +#### 锁状态 +```mysql +SHOW STATUS LIKE 'innodb_row_lock%'; +``` + -*** +参数说明: +* Innodb_row_lock_current_waits:当前正在等待锁定的数量 +* Innodb_row_lock_time:从系统启动到现在锁定总时间长度 -#### 锁操作 +* Innodb_row_lock_time_avg:每次等待所花平均时长 -##### 读锁 +* Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间 -两个客户端操作 Client 1和 Client 2,简化为 C1、C2 +* Innodb_row_lock_waits:系统启动后到现在总共等待的次数 -* 数据准备: +当等待的次数很高,而且每次等待的时长也不短的时候,就需要分析系统中为什么会有如此多的等待,然后根据分析结果制定优化计划 - ```mysql - CREATE TABLE `tb_book` ( - `id` INT(11) AUTO_INCREMENT, - `name` VARCHAR(50) DEFAULT NULL, - `publish_time` DATE DEFAULT NULL, - `status` CHAR(1) DEFAULT NULL, - PRIMARY KEY (`id`) - ) ENGINE=MYISAM DEFAULT CHARSET=utf8 ; - - INSERT INTO tb_book (id, NAME, publish_time, STATUS) VALUES(NULL,'java编程思想','2088-08-01','1'); - INSERT INTO tb_book (id, NAME, publish_time, STATUS) VALUES(NULL,'mysql编程思想','2088-08-08','0'); - ``` +查看锁状态: -* C1、C2 加读锁,同时查询可以正常查询出数据 +```mysql +SELECT * FROM information_schema.innodb_locks; #锁的概况 +SHOW ENGINE INNODB STATUS; #InnoDB整体状态,其中包括锁的情况 +``` - ```mysql - LOCK TABLE tb_book READ; -- C1、C2 - SELECT * FROM tb_book; -- C1、C2 - ``` +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB查看锁状态.png) - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 读锁1.png) +lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁(写锁);lock_type 为 RECORD 代表锁为行锁(记录锁) -* C1 加读锁,C1、C2查询未锁定的表,C1 报错,C2 正常查询 - ```mysql - LOCK TABLE tb_book READ; -- C1 - SELECT * FROM tb_user; -- C1、C2 - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 读锁2.png) - C1、C2 执行插入操作,C1 报错,C2 等待获取 - ```mysql - 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) - 当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 INSERT 语句立即执行 +### 乐观锁 +悲观锁和乐观锁使用前提: -*** +- 对于读的操作远多于写的操作的时候,一个更新操作加锁会阻塞所有的读取操作,降低了吞吐量,最后需要释放锁,锁是需要一些开销的,这时候可以选择乐观锁 +- 如果是读写比例差距不是非常大或者系统没有响应不及时,吞吐量瓶颈的问题,那就不要去使用乐观锁,它增加了复杂度,也带来了业务额外的风险,这时候可以选择悲观锁 +乐观锁的现方式: +* 版本号 -##### 写锁 + 1. 给数据表中添加一个 version 列,每次更新后都将这个列的值加 1 -两个客户端操作 Client 1和 Client 2,简化为 C1、C2 + 2. 读取数据时,将版本号读取出来,在执行更新的时候,比较版本号 -* C1 加写锁,C1、C2查询表,C1 正常查询,C2 需要等待 + 3. 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 - ```mysql - LOCK TABLE tb_book WRITE; -- C1 - SELECT * FROM tb_book; -- C1、C2 - ``` + 4. 用户自行根据这个通知来决定怎么处理,比如重新开始一遍,或者放弃本次更新 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 写锁1.png) + ```mysql + -- 创建city表 + CREATE TABLE city( + id INT PRIMARY KEY AUTO_INCREMENT, -- 城市id + NAME VARCHAR(20), -- 城市名称 + VERSION INT -- 版本号 + ); + + -- 添加数据 + INSERT INTO city VALUES (NULL,'北京',1),(NULL,'上海',1),(NULL,'广州',1),(NULL,'深圳',1); + + -- 修改北京为北京市 + -- 1.查询北京的version + SELECT VERSION FROM city WHERE NAME='北京'; + -- 2.修改北京为北京市,版本号+1。并对比版本号 + UPDATE city SET NAME='北京市',VERSION=VERSION+1 WHERE NAME='北京' AND VERSION=1; + ``` - 当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 SELECT 语句立即执行 +* 时间戳 + + - 和版本号方式基本一样,给数据表中添加一个列,名称无所谓,数据类型需要是 **timestamp** + - 每次更新后都将最新时间插入到此列 + - 读取数据时,将时间读取出来,在执行更新的时候,比较时间 + - 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 -* C1、C2 同时加写锁 - ```mysql - LOCK TABLE tb_book WRITE; - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 写锁2.png) -* C1 加写锁,C1、C2查询未锁定的表,C1 报错,C2 正常查询 @@ -6514,216 +6659,184 @@ MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表 -#### 锁状态 -* 查看锁竞争: - ```mysql - SHOW OPEN TABLES; - ``` +## 主从 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-锁争用情况查看1.png) +### 基本介绍 - In_user:表当前被查询使用的次数,如果该数为零,则表是打开的,但是当前没有被使用 +复制是指将主数据库的 DDL 和 DML 操作通过二进制日志传到从库服务器中,然后在从库上对这些日志重新执行(也叫重做),从而使得从库和主库的数据保持同步 - Name_locked:表名称是否被锁定,名称锁定用于取消表或对表进行重命名等操作 +MySQL 支持一台主库同时向多台从库进行复制,从库同时也可以作为其他从服务器的主库,实现链状复制 - ```mysql - LOCK TABLE tb_book READ; -- 执行命令 - ``` +MySQL 复制的优点主要包含以下三个方面: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-锁争用情况查看2.png) +- 主库出现问题,可以快速切换到从库提供服务 -* 查看锁状态: +- 可以在从库上执行查询操作,从主库中更新,实现读写分离 - ```mysql - SHOW STATUS LIKE 'Table_locks%'; - ``` +- 可以在从库中执行备份,以避免备份期间影响主库的服务 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 锁状态.png) - Table_locks_immediate:指的是能立即获得表级锁的次数,每立即获取锁,值加 1 - Table_locks_waited:指的是不能立即获取表级锁而需要等待的次数,每等待一次,该值加 1,此值高说明存在着较为严重的表级锁争用情况 +*** -*** +### 复制原理 +#### 主从结构 +MySQL 的主从之间维持了一个长连接。主库内部有一个线程,专门用于服务从库的长连接,连接过程: -### InnoDB +* 从库执行 change master 命令,设置主库的 IP、端口、用户名、密码以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量 +* 从库执行 start slave 命令,这时从库会启动两个线程,就是图中的 io_thread 和 sql_thread,其中 io_thread 负责与主库建立连接 +* 主库校验完用户名、密码后,开始按照从传过来的位置,从本地读取 binlog 发给从库,开始主从复制 -#### 行级锁 +主从复制原理图: -InnoDB 与 MyISAM 的**最大不同**有两点:一是支持事务;二是采用了行级锁,InnoDB 同时支持表锁和行锁 +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制原理图.jpg) -InnoDB 实现了以下两种类型的行锁: +主从复制主要依赖的是 binlog,MySQL 默认是异步复制,需要三个线程: -- 共享锁 (S):又称为读锁,简称 S 锁,就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 -- 排他锁 (X):又称为写锁,简称 X 锁,就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 +- binlog dump thread:在主库事务提交时,负责把数据变更记录在二进制日志文件 binlog 中,并通知 slave 有数据更新 +- I/O thread:负责从主服务器上拉取二进制日志,并将 binlog 日志内容依次写到 relay log 中转日志的最末端,并将新的 binlog 文件名和 offset 记录到 master-info 文件中,以便下一次读取日志时从指定 binlog 日志文件及位置开始读取新的 binlog 日志内容 +- SQL thread:监测本地 relay log 中新增了日志内容,读取中继日志并重做其中的 SQL 语句,从库在 relay-log.info 中记录当前应用中继日志的文件名和位置点以便下一次执行 -对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放;对于普通 SELECT 语句,不会加任何锁 +同步与异步: -锁的兼容性: +* 异步复制有数据丢失风险,例如数据还未同步到从库,主库就给客户端响应,然后主库挂了,此时从库晋升为主库的话数据是缺失的 +* 同步复制,主库需要将 binlog 复制到所有从库,等所有从库响应了之后主库才进行其他逻辑,这样的话性能很差,一般不会选择 +* MySQL 5.7 之出现了半同步复制,有参数可以选择成功同步几个从库就返回响应 -- 共享锁和共享锁 兼容 -- 共享锁和排他锁 冲突 -- 排他锁和排他锁 冲突 -- 排他锁和共享锁 冲突 -可以通过以下语句显式给数据集加共享锁或排他锁: -```mysql -SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE -- 共享锁 -SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -``` +**** +#### 主主结构 +主主结构就是两个数据库之间总是互为主从关系,这样在切换的时候就不用再修改主从关系 -*** +循环复制:在库 A 上更新了一条语句,然后把生成的 binlog 发给库 B,库 B 执行完这条更新语句后也会生成 binlog,会再发给 A +解决方法: +* 两个库的 server id 必须不同,如果相同则它们之间不能设定为主主关系 +* 一个库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog +* 每个库在收到从主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志 -#### 锁操作 -两个客户端操作 Client 1和 Client 2,简化为 C1、C2 -* 环境准备 +*** - ```mysql - CREATE TABLE test_innodb_lock( - id INT(11), - name VARCHAR(16), - sex VARCHAR(1) - )ENGINE = INNODB DEFAULT CHARSET=utf8; - - INSERT INTO test_innodb_lock VALUES(1,'100','1'); - -- .......... - - CREATE INDEX idx_test_innodb_lock_id ON test_innodb_lock(id); - CREATE INDEX idx_test_innodb_lock_name ON test_innodb_lock(name); - ``` -* 关闭自动提交功能: - ```mysql - SET AUTOCOMMIT=0; -- C1、C2 - ``` +### 主从延迟 - 正常查询数据: +#### 延迟原因 - ```mysql - SELECT * FROM test_innodb_lock; -- C1、C2 - ``` +正常情况主库执行更新生成的所有 binlog,都可以传到从库并被正确地执行,从库就能达到跟主库一致的状态,这就是最终一致性 -* 查询 id 为 3 的数据,正常查询: +主从延迟是主从之间是存在一定时间的数据不一致,就是同一个事务在从库执行完成的时间和主库执行完成的时间的差值,即 T2-T1 - ```mysql - SELECT * FROM test_innodb_lock WHERE id=3; -- C1、C2 - ``` +- 主库 A 执行完成一个事务,写入 binlog,该时刻记为 T1 +- 日志传给从库 B,从库 B 执行完这个事务,该时刻记为 T2 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作1.png) +通过在从库执行 `show slave status` 命令,返回结果会显示 seconds_behind_master 表示当前从库延迟了多少秒 -* C1 更新 id 为 3 的数据,但不提交: +- 每一个事务的 binlog 都有一个时间字段,用于记录主库上**写入**的时间 +- 从库取出当前正在**执行**的事务的时间字段,跟系统的时间进行相减,得到的就是 seconds_behind_master - ```mysql - UPDATE test_innodb_lock SET name='300' WHERE id=3; -- C1 - ``` +主从延迟的原因: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作2.png) +* 从库的查询压力大 +* 大事务的执行,主库必须要等到事务完成之后才会写入 binlog,导致从节点出现应用 binlog 延迟 +* 主库的 DDL,从库与主库的 DDL 同步是串行进行,DDL 在主库执行时间很长,那么从库也会消耗同样的时间 +* 锁冲突问题也可能导致从节点的 SQL 线程执行慢 +* 从库的机器性能比主库的差,导致从库的复制能力弱 - C2 查询不到 C1 修改的数据,因为隔离界别为 REPEATABLE READ,C1 提交事务,C2 查询: +主从同步问题永远都是**一致性和性能的权衡**,需要根据实际的应用场景,可以采取下面的办法: - ```mysql - COMMIT; -- C1 - ``` +* 优化 SQL,避免慢 SQL,减少批量操作 +* 降低多线程大事务并发的概率,优化业务逻辑 +* 业务中大多数情况查询操作要比更新操作更多,搭建一主多从结构,让这些从库来分担读的压力 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作3.png) +* 尽量采用短的链路,主库和从库服务器的距离尽量要短,提升端口带宽,减少 binlog 传输的网络延时 +* 实时性要求高的业务读强制走主库,从库只做备份 - 提交后仍然查询不到 C1 修改的数据,因为隔离级别可以防止脏读、不可重复读,所以 C2 需要提交才可以查询到其他事务对数据的修改: - ```mysql - COMMIT; -- C2 - SELECT * FROM test_innodb_lock WHERE id=3; -- C2 - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作4.png) +*** -* C1 更新 id 为 3 的数据,但不提交,C2 也更新 id 为 3 的数据: - ```mysql - UPDATE test_innodb_lock SET name='3' WHERE id=3; -- C1 - UPDATE test_innodb_lock SET name='30' WHERE id=3; -- C2 - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作5.png) +#### 并行复制 - 当 C1 提交,C2 直接解除阻塞,直接更新 +##### MySQL5.6 -* 操作不同行的数据: +高并发情况下,主库的会产生大量的 binlog,在从库中有两个线程 IO Thread 和 SQL Thread 单线程执行,会导致主库延迟变大。为了改善复制延迟问题,MySQL 5.6 版本增加了并行复制功能,以采用多线程机制来促进执行 - ```mysql - UPDATE test_innodb_lock SET name='10' WHERE id=1; -- C1 - UPDATE test_innodb_lock SET name='30' WHERE id=3; -- C2 - ``` +coordinator 就是原来的 sql_thread,并行复制中它不再直接更新数据,只**负责读取中转日志和分发事务**: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作6.png) +* 线程分配完成并不是立即执行,为了防止造成更新覆盖,更新同一 DB 的两个事务必须被分发到同一个工作线程 +* 同一个事务不能被拆开,必须放到同一个工作线程 - 由于C1、C2 操作的不同行,获取不同的行锁,所以都可以正常获取行锁 +MySQL 5.6 版本的策略:每个线程对应一个 hash 表,用于保存当前这个线程的执行队列里的事务所涉及的表,hash 表的 key 是数据库 名,value 是一个数字,表示队列中有多少个事务修改这个库,适用于主库上有多个 DB 的情况 +每个事务在分发的时候,跟线程的冲突(事务操作的是同一个 DB)关系包括以下三种情况: +* 如果跟所有线程都不冲突,coordinator 线程就会把这个事务分配给最空闲的线程 +* 如果只跟一个线程冲突,coordinator 线程就会把这个事务分配给这个存在冲突关系的线程 +* 如果跟多于一个线程冲突,coordinator 线程就进入等待状态,直到和这个事务存在冲突关系的线程只剩下 1 个 -​ +优缺点: -*** +* 构造 hash 值的时候很快,只需要库名,而且一个实例上 DB 数也不会很多,不会出现需要构造很多个项的情况 +* 不要求 binlog 的格式,statement 格式的 binlog 也可以很容易拿到库名(日志章节详解了 binlog) +* 主库上的表都放在同一个 DB 里面,这个策略就没有效果了;或者不同 DB 的热点不同,比如一个是业务逻辑库,一个是系统配置库,那也起不到并行的效果,需要**把相同热度的表均匀分到这些不同的 DB 中**,才可以使用这个策略 -#### 锁分类 +*** -##### 间隙锁 -当使用范围条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据进行加锁,对于键值在条件范围内但并不存在的记录,叫做间隙(GAP), InnoDB 会对间隙进行加锁,就是间隙锁 -* 唯一索引加锁只有在值存在时才是行锁,值不存在会变成间隙锁,所以范围查询时容易出现间隙锁 -* 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 +##### MySQL5.7 + +MySQL 5.7 并行复制策略的思想是: + +* 所有处于 commit 状态的事务可以并行执行 +* 同时处于 prepare 状态的事务,在从库执行时是可以并行的 +* 处于 prepare 状态的事务,与处于 commit 状态的事务之间,在从库执行时也是可以并行的 -加锁的基本单位是 next-key lock,该锁是行锁和这条记录前面的 gap lock 的组合,就是行锁加间隙锁 +MySQL 5.7 由参数 slave-parallel-type 来控制并行复制策略: -* 加锁遵循前开后闭原则 -* 假设有索引值 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会同时对间隙 (10,11]、(11,13] 加锁 +* 配置为 DATABASE,表示使用 MySQL 5.6 版本的**按库(DB)并行策略** +* 配置为 LOGICAL_CLOCK,表示的**按提交状态并行**执行 -间隙锁优点:RR 级别下间隙锁可以解决事务的**幻读问题**,通过对间隙加锁,防止读取过程中数据条目发生变化 +MySQL 5.7.22 版本里,MySQL 增加了一个新的并行复制策略,基于 WRITESET 的并行复制。新增了一个参数 binlog-transaction-dependency-tracking,用来控制是否启用这个新策略: -间隙锁危害:当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害 +* COMMIT_ORDER:表示根据同时进入 prepare 和 commit 来判断是否可以并行的策略 -* 关闭自动提交功能: +* WRITESET:表示的是对于每个事务涉及更新的每一行,计算出这一行的 hash 值,组成该事务的 writeset 集合,如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行(**按行并行**) - ```mysql - SET AUTOCOMMIT=0; -- C1、C2 - ``` +* WRITESET_SESSION:是在 WRITESET 的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序 -* 查询数据表: + 为了唯一标识,这个 hash 表的值是通过 `库名 + 表名 + 索引名 + 值`(表示的是某一行)计算出来的 - ```mysql - SELECT * FROM test_innodb_lock; - ``` +MySQL 5.7.22 按行并发的优势: - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 间隙锁1.png) +* writeset 是在主库生成后直接写入到 binlog 里面的,这样在备库执行的时候,不需要解析 binlog 内容,节省了计算量 +* 不需要把整个事务的 binlog 都扫一遍才能决定分发到哪个线程,更省内存 +* 从库的分发策略不依赖于 binlog 内容,所以 binlog 是 statement 格式也可以,更节约内存(因为 row 才记录更改的行) -* C1 根据 id 范围更新数据,C2 插入数据: +MySQL 5.7.22 的并行复制策略在通用性上是有保证的,但是对于表上没主键、唯一和外键约束的场景,WRITESET 策略也是没法并行的,也会暂时退化为单线程模型 - ```mysql - UPDATE test_innodb_lock SET name='8888' WHERE id < 4; -- C1 - INSERT INTO test_innodb_lock VALUES(2,'200','2'); -- C2 - ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 间隙锁2.png) - 出现间隙锁,C2 被阻塞,等待 C1 提交事务后才能更新 +参考文章:https://time.geekbang.org/column/article/77083 @@ -6731,136 +6844,179 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -##### 意向锁 - -InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支持在不同粒度上的加锁操作,InnoDB 增加了意向锁(Intention Lock ) - -意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁,意向锁分为两种: +#### 读写分离 -* 意向共享锁(IS):事务有意向对表中的某些行加共享锁 +读写分离:可以降低主库的访问压力,提高系统的并发能力 -* 意向排他锁(IX):事务有意向对表中的某些行加排他锁 +* 主库不建查询的索引,从库建查询的索引。因为索引需要维护的,比如插入一条数据,不仅要在聚簇索引上面插入,对应的二级索引也得插入 +* 将读操作分到从库了之后,可以在主库把查询要用的索引删了,减少写操作对主库的影响 -InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除全表扫描以外的任何请求,表级意向锁与行级锁的兼容性如下所示: +读写分离产生了读写延迟,造成数据的不一致性。假如客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,可能读到的还是以前的数据,叫过期读 -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-意向锁兼容性.png) +解决方案: -插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设某列有索引值2,6,只要两个事务插入位置不同,如事务 A 插入 3,事务 B 插入 4,那么就可以同时插入 +* 强制将写之后**立刻读的操作转移到主库**,比如刚注册的用户,直接登录从库查询可能查询不到,先走主库登录 +* **二次查询**,如果从库查不到数据,则再去主库查一遍,由 API 封装,比较简单,但导致主库压力大 +* 更新主库后,读从库之前先 sleep 一下,类似于执行一条 `select sleep(1)` 命令 +* 确保主备无延迟的方法,每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0,如果不等于那就等到这个参数变为 0 才能执行查询请求 -*** -##### 死锁 -当并发系统中不同线程出现循环资源依赖,线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁 +*** -死锁情况:线程 A 修改了 id = 1 的数据,请求修改 id = 2 的数据,线程 B 修改了 id = 2 的数据,请求修改 id = 1 的数据,产生死锁 -解决策略: -* 直接进入等待直到超时,超时时间可以通过参数 innodb_lock_wait_timeout 来设置,但是时间的设置不好控制,超时可能不是因为死锁,而是因为事务处理比较慢,所以一般不采取该方式 -* 主动死锁检测,发现死锁后**主动回滚死锁链条中的某一个事务**,让其他事务得以继续执行,将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑 +### 负载均衡 +负载均衡是应用中使用非常普遍的一种优化方法,机制就是利用某种均衡算法,将固定的负载量分布到不同的服务器上,以此来降低单台服务器的负载,达到优化的效果 +* 分流查询:通过 MySQL 的主从复制,实现读写分离,使增删改操作走主节点,查询操作走从节点,从而可以降低单台服务器的读写压力 -**** + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-负载均衡主从复制.jpg) +* 分布式数据库架构:适合大数据量、负载高的情况,具有良好的拓展性和高可用性。通过在多台服务器之间分布数据,可以实现在多台服务器之间的负载均衡,提高访问效率 -#### 锁优化 -##### 锁升级 +**** -索引失效造成行锁升级为表锁,不通过索引检索数据,InnoDB 会将对表中的所有记录加锁,实际效果和**表锁**一样实际开发过程应避免出现索引失效的状况 -* 查看当前表的索引: - ```mysql - SHOW INDEX FROM test_innodb_lock; - ``` +### 主从搭建 -* 关闭自动提交功能: +#### 搭建流程 - ```mysql - SET AUTOCOMMIT=0; -- C1、C2 - ``` +##### master -* 执行更新语句: +1. 在master 的配置文件(/etc/mysql/my.cnf)中,配置如下内容: - ```mysql - UPDATE test_innodb_lock SET sex='2' WHERE name=10; -- C1 - UPDATE test_innodb_lock SET sex='2' WHERE id=3; -- C2 - ``` + ```sh + #mysql 服务ID,保证整个集群环境中唯一 + server-id=1 + + #mysql binlog 日志的存储路径和文件名 + log-bin=/var/lib/mysql/mysqlbin + + #错误日志,默认已经开启 + #log-err + + #mysql的安装目录 + #basedir + + #mysql的临时目录 + #tmpdir + + #mysql的数据存放目录 + #datadir + + #是否只读,1 代表只读, 0 代表读写 + read-only=0 + + #忽略的数据, 指不需要同步的数据库 + binlog-ignore-db=mysql + + #指定同步的数据库 + #binlog-do-db=db01 + ``` - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁升级.png) +2. 执行完毕之后,需要重启 MySQL - 索引失效:执行更新时 name 字段为 varchar 类型,造成索引失效,最终行锁变为表锁 +3. 创建同步数据的账户,并且进行授权操作: + ```mysql + GRANT REPLICATION SLAVE ON *.* TO 'seazean'@'192.168.0.137' IDENTIFIED BY '123456'; + FLUSH PRIVILEGES; + ``` +4. 查看 master 状态: -*** + ```mysql + SHOW MASTER STATUS; + ``` + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查看master状态.jpg) + * File:从哪个日志文件开始推送日志文件 + * Position:从哪个位置开始推送日志 + * Binlog_Ignore_DB:指定不需要同步的数据库 -##### 优化锁 -InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高,但是在整体并发处理能力方面要远远优于 MyISAM 的表锁,当系统并发量较高的时候,InnoDB 的整体性能远远好于 MyISAM -但是使用不当可能会让InnoDB 的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差 +*** -优化建议: -- 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁 -- 合理设计索引,尽量缩小锁的范围 -- 尽可能减少索引条件及索引范围,避免间隙锁 -- 尽量控制事务大小,减少锁定资源量和时间长度 -- 尽可使用低级别事务隔离(需要业务层面满足需求) +##### slave +1. 在 slave 端配置文件中,配置如下内容: + ```sh + #mysql服务端ID,唯一 + server-id=2 + + #指定binlog日志 + log-bin=/var/lib/mysql/mysqlbin + ``` +2. 执行完毕之后,需要重启 MySQL -*** +3. 指定当前从库对应的主库的IP地址、用户名、密码,从哪个日志文件开始的那个位置开始同步推送日志 + ```mysql + CHANGE MASTER TO MASTER_HOST= '192.168.0.138', MASTER_USER='seazean', MASTER_PASSWORD='seazean', MASTER_LOG_FILE='mysqlbin.000001', MASTER_LOG_POS=413; + ``` +4. 开启同步操作: -#### 锁状态 + ```mysql + START SLAVE; + SHOW SLAVE STATUS; + ``` -```mysql -SHOW STATUS LIKE 'innodb_row_lock%'; -``` +5. 停止同步操作: - + ```mysql + STOP SLAVE; + ``` -参数说明: -* Innodb_row_lock_current_waits:当前正在等待锁定的数量 -* Innodb_row_lock_time:从系统启动到现在锁定总时间长度 +*** -* Innodb_row_lock_time_avg:每次等待所花平均时长 -* Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间 -* Innodb_row_lock_waits:系统启动后到现在总共等待的次数 +##### 验证 -当等待的次数很高,而且每次等待的时长也不短的时候,就需要分析系统中为什么会有如此多的等待,然后根据分析结果制定优化计划 +1. 在主库中创建数据库,创建表并插入数据: -查看锁状态: + ```mysql + CREATE DATABASE db01; + USE db01; + CREATE TABLE user( + id INT(11) NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL, + sex VARCHAR(1), + PRIMARY KEY (id) + )ENGINE=INNODB DEFAULT CHARSET=utf8; + + INSERT INTO user(id,NAME,sex) VALUES(NULL,'Tom','1'); + INSERT INTO user(id,NAME,sex) VALUES(NULL,'Trigger','0'); + INSERT INTO user(id,NAME,sex) VALUES(NULL,'Dawn','1'); + ``` -```mysql -SELECT * FROM information_schema.innodb_locks; #锁的概况 -SHOW ENGINE INNODB STATUS; #InnoDB整体状态,其中包括锁的情况 -``` +2. 在从库中查询数据,进行验证: -![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB查看锁状态.png) + 在从库中,可以查看到刚才创建的数据库: -lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁(写锁);lock_type 为 RECORD 代表锁为行锁(记录锁) + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制验证1.jpg) + 在该数据库中,查询表中的数据: + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-主从复制验证2.jpg) @@ -6868,49 +7024,30 @@ lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁 -### 乐观锁 - -悲观锁和乐观锁使用前提: +#### 主从切换 -- 对于读的操作远多于写的操作的时候,一个更新操作加锁会阻塞所有的读取操作,降低了吞吐量,最后需要释放锁,锁是需要一些开销的,这时候可以选择乐观锁 -- 如果是读写比例差距不是非常大或者系统没有响应不及时,吞吐量瓶颈的问题,那就不要去使用乐观锁,它增加了复杂度,也带来了业务额外的风险,这时候可以选择悲观锁 +正常切换步骤: -乐观锁的现方式: +* 在开始切换之前先对主库进行锁表 `flush tables with read lock`,然后等待所有语句执行完成,切换完成后可以释放锁 -* 版本号 +* 检查 slave 同步状态,在 slave 执行 `show processlist` - 1. 给数据表中添加一个 version 列,每次更新后都将这个列的值加 1 +* 停止 slave io 线程,执行命令 `STOP SLAVE IO_THREAD` - 2. 读取数据时,将版本号读取出来,在执行更新的时候,比较版本号 +* 提升 slave 为 master - 3. 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 + ```sql + Stop slave; + Reset master; + Reset slave all; + set global read_only=off; -- 设置为可更新状态 + ``` - 4. 用户自行根据这个通知来决定怎么处理,比如重新开始一遍,或者放弃本次更新 +* 将原来 master 变为 slave(参考搭建流程中的 slave 方法) - ```mysql - -- 创建city表 - CREATE TABLE city( - id INT PRIMARY KEY AUTO_INCREMENT, -- 城市id - NAME VARCHAR(20), -- 城市名称 - VERSION INT -- 版本号 - ); - - -- 添加数据 - INSERT INTO city VALUES (NULL,'北京',1),(NULL,'上海',1),(NULL,'广州',1),(NULL,'深圳',1); - - -- 修改北京为北京市 - -- 1.查询北京的version - SELECT VERSION FROM city WHERE NAME='北京'; - -- 2.修改北京为北京市,版本号+1。并对比版本号 - UPDATE city SET NAME='北京市',VERSION=VERSION+1 WHERE NAME='北京' AND VERSION=1; - ``` +主库发生故障,从库会进行上位,其他从库指向新的主库 -* 时间戳 - - 和版本号方式基本一样,给数据表中添加一个列,名称无所谓,数据类型需要是 **timestamp** - - 每次更新后都将最新时间插入到此列 - - 读取数据时,将时间读取出来,在执行更新的时候,比较时间 - - 如果相同则执行更新,如果不相同,说明此条数据已经发生了变化 @@ -7674,13 +7811,13 @@ public class JDBCDemo01 { ### 攻击演示 -SQL注入攻击演示 +SQL 注入攻击演示 * 在登录界面,输入一个错误的用户名或密码,也可以登录成功 ![](https://gitee.com/seazean/images/raw/master/DB/SQL注入攻击演示.png) -* 原理:我们在密码处输入的所有内容,都应该认为是密码的组成,但是 Statement 对象在执行 sql 语句时,将一部分内容当做查询条件来执行 +* 原理:我们在密码处输入的所有内容,都应该认为是密码的组成,但是 Statement 对象在执行 SQL 语句时,将一部分内容当做查询条件来执行 ```mysql SELECT * FROM user WHERE loginname='aaa' AND password='aaa' OR '1'='1'; @@ -8762,7 +8899,7 @@ io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的 -### key指令 +### key 指令 key 是一个字符串,通过 key 获取 redis 中保存的数据 @@ -8818,7 +8955,7 @@ key 是一个字符串,通过 key 获取 redis 中保存的数据 -### DB指令 +### DB 指令 Redis 在使用过程中,随着操作数据量的增加,会出现大量的数据以及对应的 key,数据不区分种类、类别混在一起,容易引起重复或者冲突,所以 Redis 为每个服务提供 16 个数据库,编码 0-15,每个数据库之间相互独立,**共用 **Redis 内存,不区分大小 @@ -8870,7 +9007,7 @@ Redis 客户端可以订阅任意数量的频道 -### ACL指令 +### ACL 指令 Redis ACL 是 Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接 diff --git a/Java.md b/Java.md index 0ba5164..c1d2f77 100644 --- a/Java.md +++ b/Java.md @@ -3867,11 +3867,12 @@ Collection 集合的遍历方式有三种: 集合可以直接输出内容,因为底层重写了 toString() 方法 1. 迭代器 - `public Iterator iterator()`:获取集合对应的迭代器,用来遍历集合中的元素的 - `E next()`:获取下一个元素值 - `boolean hasNext()`:判断是否有下一个元素,有返回true ,反之 - `default void remove()`:从底层集合中删除此迭代器返回的最后一个元素,这种方法只能在每次调用next() 时调用一次 - + + * `public Iterator iterator()`:获取集合对应的迭代器,用来遍历集合中的元素的 + * `E next()`:获取下一个元素值 + * `boolean hasNext()`:判断是否有下一个元素,有返回 true ,反之返回 false + * `default void remove()`:从底层集合中删除此迭代器返回的最后一个元素,这种方法只能在每次调用 next() 时调用一次 + 2. 增强 for 循环:可以遍历集合或者数组,遍历集合实际上是迭代器遍历的简化写法 ```java @@ -3925,11 +3926,11 @@ Collection 集合的遍历方式有三种: ##### 概述 -List集合继承了Collection集合全部的功能。 +List 集合继承了 Collection 集合全部的功能。 -List系列集合有索引,所以多了很多按照索引操作元素的功能:for循环遍历(4种遍历) +List 系列集合有索引,所以多了很多按照索引操作元素的功能:for 循环遍历(4 种遍历) -List系列集合:添加的元素是有序,可重复,有索引。 +List 系列集合:添加的元素是有序,可重复,有索引。 * ArrayList:添加的元素是有序,可重复,有索引。 @@ -4141,7 +4142,7 @@ public class ArrayList extends AbstractList if (modCount != expectedModCount) throw new ConcurrentModificationException(); } - // 允许删除操作 + // 【允许删除操作】 public void remove() { // ... checkForComodification(); @@ -4162,7 +4163,7 @@ public class ArrayList extends AbstractList ##### Vector -同步:Vector的实现与 ArrayList 类似,但是方法上使用了 synchronized 进行同步 +同步:Vector 的实现与 ArrayList 类似,但是方法上使用了 synchronized 进行同步 构造:默认长度为 10 的数组 @@ -4284,8 +4285,8 @@ LinkedList 是一个实现了 List 接口的**双端链表**,支持高效的 * 获取元素:`get(int index)` 根据指定索引返回数据 - * 获取头节点 (index=0):getFirst()、element()、peek()、peekFirst() 这四个获取头结点方法的区别在于对链表为空时的处理,是抛出异常还是返回null,其中**getFirst() 和element()** 方法将会在链表为空时,抛出异常 - * 获取尾节点 (index=-1):getLast() 方法在链表为空时,会抛出NoSuchElementException,而peekLast() 则不会,只会返回 null + * 获取头节点 (index=0):getFirst()、element()、peek()、peekFirst() 这四个获取头结点方法的区别在于对链表为空时的处理,是抛出异常还是返回null,其中 **getFirst() 和 element()** 方法将会在链表为空时,抛出异常 + * 获取尾节点 (index=-1):getLast() 方法在链表为空时,抛出 NoSuchElementException,而 peekLast() 不会,只会返回 null * 删除元素: @@ -4303,7 +4304,7 @@ LinkedList 是一个实现了 List 接口的**双端链表**,支持高效的 * LinkedList采 用链表存储,所以对于`add(E e)`方法的插入,删除元素不受元素位置的影响 4. 是否支持快速随机访问: * LinkedList 不支持高效的随机元素访问,ArrayList 支持 - * 快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。 + * 快速随机访问就是通过元素的序号快速获取元素对象(对应于 `get(int index)` 方法) 5. 内存空间占用: * ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间 * LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据) @@ -4347,7 +4348,7 @@ Set 系列集合:添加的元素是无序,不重复,无索引的 **HashSet 底层就是基于 HashMap 实现,值是 PRESENT = new Object()** -Set集合添加的元素是无序,不重复的。 +Set 集合添加的元素是无序,不重复的。 * 是如何去重复的? diff --git a/Prog.md b/Prog.md index 5fcf710..36c0ed0 100644 --- a/Prog.md +++ b/Prog.md @@ -8279,7 +8279,9 @@ public void lock() { } else { // 自旋到这,普通入队方式,【尾插法】 node.prev = t; + // 【在设置完尾节点后,才更新的原始尾节点的后继节点,所以此时从前往后遍历会丢失尾节点】 if (compareAndSetTail(t, node)) { + //【此时 t.next = null,并且这里已经 CAS 结束,线程并不是安全的】 t.next = node; return t; // 返回当前 node 的前驱节点 } @@ -8461,6 +8463,8 @@ Thread-0 释放锁,进入 release 流程 } ``` + 从后向前的原因:enq 方法中,节点是尾插法,首先赋值的是尾节点的前驱节点,此时前驱节点的 next 并没有指向尾节点,从前遍历会丢失节点 + * 唤醒的线程会从 park 位置开始执行,如果加锁成功(没有竞争),会设置 * exclusiveOwnerThread 为 Thread-1,state = 1 diff --git a/SSM.md b/SSM.md index b045da3..085ba19 100644 --- a/SSM.md +++ b/SSM.md @@ -16,6 +16,8 @@ ORM(Object Relational Mapping): 对象关系映射,指的是持久化数 MyBatis 官网地址:http://www.mybatis.org/mybatis-3/ +参考视频:https://space.bilibili.com/37974444/ + *** @@ -10091,7 +10093,7 @@ MVC(Model View Controller),一种用于设计创建Web应用程序表现 ![](https://gitee.com/seazean/images/raw/master/Frame/SpringMVC-MVC功能图示.png) - +参考视频:https://space.bilibili.com/37974444/ diff --git a/Web.md b/Web.md index f12c7a5..7c70150 100644 --- a/Web.md +++ b/Web.md @@ -9026,9 +9026,9 @@ Element:网站快速成型工具,是饿了么公司前端开发团队提供 ## 安装软件 -Nginx(engine x) 是一个高性能的HTTP和[反向代理](https://baike.baidu.com/item/反向代理/7793488)web服务器,同时也提供了IMAP/POP3/SMTP服务。 +Nginx 是一个高性能的 HTTP 和[反向代理 ](https://baike.baidu.com/item/反向代理/7793488)Web 服务器,同时也提供了 IMAP/POP3/SMTP 服务 -Nginx两个最核心的功能:高性能的静态web服务器,反向代理 +Nginx 两个最核心的功能:高性能的静态 Web 服务器,反向代理 * 安装指令:sudo apt-get install nginx @@ -9036,6 +9036,7 @@ Nginx两个最核心的功能:高性能的静态web服务器,反向代理 * 系统指令:systemctl / service start/restart/stop/status nginx 配置文件安装目录:/etc/nginx + 日志文件:/var/log/nginx @@ -9046,20 +9047,20 @@ Nginx两个最核心的功能:高性能的静态web服务器,反向代理 ## 配置文件 -nginx.conf 文件时nginx的主配置文件 +nginx.conf 文件时 Nginx 的主配置文件 -* main部分 +* main 部分 -* events部分 +* events 部分 -* server部分 +* server 部分 - root设置的路径会拼接上location的路径,然后去最终路径寻找对应的文件 + root 设置的路径会拼接上 location 的路径,然后去最终路径寻找对应的文件 @@ -9069,15 +9070,18 @@ nginx.conf 文件时nginx的主配置文件 ## 发布项目 -1. 创建一个toutiao目录 - cd /home - mkdir toutiao - -2. 将项目上传到toutiao目录 +1. 创建一个 toutiao 目录 + + ```sh + cd /home + mkdir toutiao + ``` + +2. 将项目上传到 toutiao 目录 3. 解压项目 unzip web.zip -4. 编辑Nginx配置文件nginx.conf +4. 编辑 Nginx 配置文件 nginx.conf ```shell server { @@ -9090,9 +9094,9 @@ nginx.conf 文件时nginx的主配置文件 } ``` -5. 重启nginx服务:systemctl restart nginx +5. 重启 Nginx 服务:systemctl restart nginx -6. 浏览器打开网址 http://127.0.0.1:80 +6. 浏览器打开网址:http://127.0.0.1:80 @@ -9102,15 +9106,15 @@ nginx.conf 文件时nginx的主配置文件 ## 反向代理 -> 无法访问Google,可以配置一个代理服务器,发送请求到代理服务器,代理服务器经过转发,再将请求转发给Google,返回结果之后,再次转发给用户。这个叫做正向代理,正向代理对于用户来说,是有感知的 +> 无法访问 Google,可以配置一个代理服务器,发送请求到代理服务器,代理服务器经过转发,再将请求转发给 Google,返回结果之后,再次转发给用户,这个叫做正向代理,正向代理对于用户来说,是有感知的 -**正向代理(forward proxy)**:是一个位于客户端和目标服务器之间的服务器(代理服务器),为了从目标服务器取得内容,客户端向代理服务器发送一个请求并指定目标,然后代理服务器向目标服务器转交请求并将获得的内容返回给客户端,**正向代理,其实是"代理服务器"代理了"客户端",去和"目标服务器"进行交互** +**正向代理(forward proxy)**:是一个位于客户端和目标服务器之间的代理服务器,为了从目标服务器取得内容,客户端向代理服务器发送一个请求并指定目标,然后代理服务器向目标服务器转交请求并将获得的内容返回给客户端,**正向代理,其实是"代理服务器"代理了当前"客户端",去和"目标服务器"进行交互** 作用: -* 突破访问限制:通过代理服务器,可以突破自身IP访问限制,访问国外网站,教育网等 +* 突破访问限制:通过代理服务器,可以突破自身 IP 访问限制,访问国外网站,教育网等 * 提高访问速度:代理服务器都设置一个较大的硬盘缓冲区,会将部分请求的响应保存到缓冲区中,当其他用户再访问相同的信息时, 则直接由缓冲区中取出信息,传给用户,以提高访问速度 -* 隐藏客户端真实IP:隐藏自己的IP,免受攻击 +* 隐藏客户端真实 IP:隐藏自己的 IP,免受攻击 @@ -9118,14 +9122,14 @@ nginx.conf 文件时nginx的主配置文件 -**反向代理(reverse proxy)**:是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器,**反向代理,其实是"代理服务器"代理了"目标服务器",去和"客户端"进行交互** +**反向代理(reverse proxy)**:是指以代理服务器来接受 Internet 上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 Internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器,**反向代理,其实是"代理服务器"代理了"目标服务器",去和当前"客户端"进行交互** 作用: -* 隐藏服务器真实IP:使用反向代理,可以对客户端隐藏服务器的IP地址 +* 隐藏服务器真实 IP:使用反向代理,可以对客户端隐藏服务器的 IP 地址 * 负载均衡:根据所有真实服务器的负载情况,将客户端请求分发到不同的真实服务器上 * 提高访问速度:反向代理服务器可以对于静态内容及短时间内有大量访问请求的动态内容提供缓存服务 -* 提供安全保障:反向代理服务器可以作为应用层防火墙,为网站提供对基于Web的攻击行为(例如DoS/DDoS)的防护,更容易排查恶意软件等 +* 提供安全保障:反向代理服务器可以作为应用层防火墙,为网站提供对基于 Web 的攻击行为(例如 DoS/DDoS)的防护,更容易排查恶意软件等 From c58a00285ded7dbaa786d038b5d40e6d0e3cdad4 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 12 Dec 2021 20:52:27 +0800 Subject: [PATCH 043/122] Update Java Notes --- DB.md | 251 ++++++++++++++++++++++++++++++++++++++------------------ Java.md | 4 +- 2 files changed, 175 insertions(+), 80 deletions(-) diff --git a/DB.md b/DB.md index 7235c9b..2710a29 100644 --- a/DB.md +++ b/DB.md @@ -730,9 +730,9 @@ DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临 +参考文章:https://time.geekbang.org/column/article/72388 -参考文章:https://time.geekbang.org/column/article/72388 @@ -3858,7 +3858,7 @@ InnoDB 存储引擎中有页(Page)的概念,页是 MySQL 磁盘管理的 数据页物理结构,从上到下: -* File Header:上一页和下一页的指针、该页的类型(索引页、数据页、日志页等)、**校验和**等信息 +* File Header:上一页和下一页的指针、该页的类型(索引页、数据页、日志页等)、**校验和**、LSN(最近一次修改当前页面时的系统 lsn 值,事务持久性部分详解)等信息 * Page Header:记录状态信息 * Infimum + Supremum:当前页的最小记录和最大记录(头尾指针),Infimum 所在分组只有一条记录,Supremum 所在分组可以有 1 ~ 8 条记录,剩余的分组可以有 4 ~ 8 条记录 * User Records:存储数据的记录 @@ -4274,7 +4274,7 @@ Innodb_xxxx:这几个参数只是针对InnoDB 存储引擎的,累加的算 SQL 执行慢有两种情况: -* 偶尔慢:DB 在刷新脏页 +* 偶尔慢:DB 在刷新脏页(学完事务就懂了) * redo log 写满了 * 内存不够用,要从 LRU 链表中淘汰 * MySQL 认为系统空闲的时候 @@ -4309,7 +4309,7 @@ SQL 执行慢有两种情况: * SHOW PROCESSLIST:**实时查看**当前 MySQL 在进行的连接线程,包括线程的状态、是否锁表、SQL 的执行情况,同时对一些锁表操作进行优化 - ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SHOW PROCESSLIST命令.png) + ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-SHOW_PROCESSLIST命令.png) @@ -5272,11 +5272,14 @@ MyISAM 存储引擎使用 key_buffer 缓存索引块,加速 MyISAM 索引的 Buffer Pool 本质上是 InnoDB 向操作系统申请的一段连续的内存空间。InnoDB 的数据是按数据页为单位来读写,每个数据页的大小默认是 16KB。数据是存放在磁盘中,每次读写数据都需要进行磁盘 IO 将数据读入内存进行操作,效率会很低,所以提供了 Buffer Pool 来暂存这些数据页,缓存中的这些页又叫缓冲页 -工作流程: +工作原理: * 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool + * 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 + **唯一索引的更新不能使用 Buffer**,一般只有普通索引可以使用,直接写入 Buffer 就结束 + Buffer Pool 中每个缓冲页都有对应的控制信息,包括表空间编号、页号、偏移量、链表信息等,控制信息存放在占用的内存称为控制块,控制块与缓冲页是一一对应的,但并不是物理上相连的,都在缓冲池中 MySQL 提供了缓冲页的快速查找方式:**哈希表**,使用表空间号和页号作为 Key,缓冲页控制块的地址作为 Value 创建一个哈希表,获取数据页时根据 Key 进行哈希寻址: @@ -5286,6 +5289,8 @@ MySQL 提供了缓冲页的快速查找方式:**哈希表**,使用表空间 + + *** @@ -5298,7 +5303,7 @@ MySQL 启动时完成对 Buffer Pool 的初始化,先向操作系统申请连 -基节点:是一块单独申请的内存空间(占 40 字节),并不在Buffer Pool的那一大片连续内存空间里 +基节点:是一块单独申请的内存空间(占 40 字节),并不在 Buffer Pool的那一大片连续内存空间里 磁盘加载页的流程: @@ -5318,7 +5323,7 @@ MySQL 启动时完成对 Buffer Pool 的初始化,先向操作系统申请连 ##### Flush 链表 -Flush 链表是一个用来**存储脏页**的链表,对于已经修改过的缓冲脏页,出于性能考虑并不是直接更新到磁盘,而是在未来的某个时间进行刷脏,所以需要暂时存储所有的脏页 +Flush 链表是一个用来**存储脏页**的链表,对于已经修改过的缓冲脏页,第一次修改后加入到**链表头部**,以后每次修改都不会重新加入,只修改部分控制信息,出于性能考虑并不是直接更新到磁盘,而是在未来的某个时间进行刷脏 @@ -5348,9 +5353,7 @@ Flush 链表是一个用来**存储脏页**的链表,对于已经修改过的 当 Buffer Pool 中没有空闲缓冲页时就需要淘汰掉最近最少使用的部分缓冲页,为了实现这个功能,MySQL 创建了一个 LRU 链表,当访问某个页时: * 如果该页不在 Buffer Pool 中,把该页从磁盘加载进来后会将该缓冲页对应的控制块作为节点放入 **LRU 链表的头部** -* 如果该页在 Buffer Pool 中,则直接把该页对应的控制块移动到 LRU 链表的头部 - -这样操作后 LRU 链表的尾部就是最近最少使用的缓冲页 +* 如果该页在 Buffer Pool 中,则直接把该页对应的控制块移动到 LRU 链表的头部,所以 LRU 链表尾部就是最近最少使用的缓冲页 MySQL 基于局部性原理提供了预读功能: @@ -5369,8 +5372,6 @@ MySQL 基于局部性原理提供了预读功能: - - *** @@ -5405,11 +5406,13 @@ SHOW ENGINE INNODB STATUS\G innodb_log_buffer_size=10M ``` +Buffer Pool 中有一块内存叫 Change Buffer 用来对增删改操作提供缓存,可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% + 在多线程下,访问 Buffer Pool 中的各种链表都需要加锁,所以将 Buffer Pool 拆成若干个小实例,每个实例独立管理内存空间和各种链表(类似 ThreadLocal),多线程访问各实例互不影响,提高了并发能力 * 在系统启动时设置系统变量 `innodb_buffer_pool_instance` 可以指定 Buffer Pool 实例的个数,但是当 Buffer Pool 小于 1GB 时,设置多个实例时无效的 -MySQL 5.7.5 之前 `innodb_buffer_pool_size` 只支持在系统启动时修改,现在已经支持运行时修改 Buffer Pool 的大小,但是每次调整参数都会重新向操作系统申请一块连续的内存空间,将旧的缓冲池的内容拷贝到新空间非常耗时,所以 MySQL 开始以一个 chunk 为单位向操作系统申请内存,所以一个 Buffer Pool 实例由多个 chunk 组成 +MySQL 5.7.5 之前 `innodb_buffer_pool_size` 只支持在系统启动时修改,现在已经支持运行时修改 Buffer Pool 的大小,但是每次调整参数都会重新向操作系统申请一块连续的内存空间,**将旧的缓冲池的内容拷贝到新空间**非常耗时,所以 MySQL 开始以一个 chunk 为单位向操作系统申请内存,所以一个 Buffer Pool 实例由多个 chunk 组成 * 指定系统变量 `innodb_buffer_pool_chunk_size` 来改变 chunk 的大小,只能在启动时修改,运行中不能修改,而且该变量并不包含缓冲页的控制块的内存大小 * `innodb_buffer_pool_size` 必须是 `innodb_buffer_pool_chunk_size × innodb_buffer_pool_instance` 的倍数,默认值是 `128M × 16 = 2G`,Buffer Pool 必须是 2G 的整数倍,如果指定 5G,会自动调整成 6G @@ -5466,9 +5469,15 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 ## 事务机制 -### 管理事务 +### 基本介绍 + +事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 SQL 语句,这些语句要么都执行,要么都不执行,作为一个关系型数据库,MySQL 支持事务。 + +单元中的每条 SQL 语句都相互依赖,形成一个整体 -事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 sql 语句,这些语句要么都执行,要么都不执行。作为一个关系型数据库,MySQL 支持事务。 +* 如果某条 SQL 语句执行失败或者出现错误,那么整个单元就会回滚,撤回到事务最初的状态 + +* 如果单元中所有的 SQL 语句都执行成功,则事务就顺利执行 事务的四大特征:ACID @@ -5477,13 +5486,27 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 - 隔离性 (isolaction) - 持久性 (durability) -单元中的每条 SQL 语句都相互依赖,形成一个整体 +事务的几种状态: -* 如果某条 SQL 语句执行失败或者出现错误,那么整个单元就会回滚,撤回到事务最初的状态 +* 活动的(active):事务对应的数据库操作正在执行中 +* 部分提交的(partially committed):事务的最后一个操作执行完,但是内存还没刷新至磁盘 +* 失败的(failed):当事务处于活动状态或部分提交状态时,如果数据库遇到了错误或刷脏失败,或者用户主动停止当前的事务 +* 中止的(aborted):失败状态的事务回滚完成后的状态 +* 提交的(committed):当处于部分提交状态的事务刷脏成功,就处于提交状态 -* 如果单元中所有的 SQL 语句都执行成功,则事务就顺利执行 -管理事务的三个步骤 + + + +*** + + + +### 事务管理 + +#### 基本操作 + +事务管理的三个步骤 1. 开启事务:记录回滚点,并通知服务器,将要执行一组操作,要么同时成功、要么同时失败 @@ -5497,13 +5520,14 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 事务操作: -* 开启事务 +* 显式开启事务 ```mysql - START TRANSACTION; + START TRANSACTION [READ ONLY|READ WRITE|WITH CONSISTENT SNAPSHOT]; #可以跟一个或多个状态,最后的是一致性读 + BEGIN [WORK]; ``` -* 回滚事务 +* 回滚事务,用来手动中止事务 ```mysql ROLLBACK; @@ -5515,38 +5539,13 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 COMMIT; ``` - 工作原理: - - * 自动提交模式下,如果没有 start transaction 显式地开始一个事务,那么**每个 SQL 语句都会被当做一个事务执行提交操作** - * 手动提交模式下,所有的 SQL 语句都在一个事务中,直到执行了 commit 或 rollback - - * 存在一些特殊的命令,在事务中执行了这些命令会马上强制执行 COMMIT 提交事务,如 DDL 语句 (create/drop/alter/table)、lock tables 语句等 - - 提交方式语法: - - - 查看事务提交方式 - - ```mysql - SELECT @@AUTOCOMMIT; -- 1 代表自动提交 0 代表手动提交 - ``` - - - 修改事务提交方式 - - ```mysql - SET @@AUTOCOMMIT=数字; -- 系统 - SET AUTOCOMMIT=数字; -- 会话 - ``` - - - **系统变量的操作**: - - ```sql - SET [GLOBAL|SESSION] 变量名 = 值; -- 默认是会话 - SET @@[(GLOBAL|SESSION).]变量名 = 值; -- 默认是系统 - ``` +* 保存点:在事务的执行过程中设置的还原点,调用 ROLLBACK 时可以指定回滚到哪个点 - ```sql - SHOW [GLOBAL|SESSION] VARIABLES [LIKE '变量%']; -- 默认查看会话内系统变量值 - ``` + ```mysql + SAVEPOINT point_name; #设置保存点 + RELEASE point_name #删除保存点 + ROLLBACK [WORK] TO [SAVEPOINT] point_name #回滚至某个保存点,不填默认回滚到事务执行之前的状态 + ``` * 操作演示 @@ -5569,6 +5568,48 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 +*** + + + +#### 提交方式 + +提交方式的相关语法: + +- 查看事务提交方式 + + ```mysql + SELECT @@AUTOCOMMIT; -- 1 代表自动提交 0 代表手动提交 + ``` + +- 修改事务提交方式 + + ```mysql + SET @@AUTOCOMMIT=数字; -- 系统 + SET AUTOCOMMIT=数字; -- 会话 + ``` + +- **系统变量的操作**: + + ```sql + SET [GLOBAL|SESSION] 变量名 = 值; -- 默认是会话 + SET @@[(GLOBAL|SESSION).]变量名 = 值; -- 默认是系统 + ``` + + ```sql + SHOW [GLOBAL|SESSION] VARIABLES [LIKE '变量%']; -- 默认查看会话内系统变量值 + ``` + +工作原理: + +* 自动提交:如果没有 START TRANSACTION 显式地开始一个事务,那么**每条 SQL 语句都会被当做一个事务执行提交操作**;显式开启事务后,会在本次事务结束(提交或回滚)前暂时关闭自动提交 +* 手动提交:不需要显式的开启事务,所有的 SQL 语句都在一个事务中,直到执行了提交或回滚,然后进入下一个事务 +* 隐式提交:存在一些特殊的命令,在事务中执行了这些命令会马上强制执行 COMMIT 提交事务 + * DDL 语句 (CREATE/DROP/ALTER)、LOCK TABLES 语句、LOAD DATA 导入数据语句、主从复制语句等 + * 当一个事务还没提交或回滚,显式的开启一个事务会隐式的提交上一个事务 + + + *** @@ -5898,58 +5939,112 @@ RC、RR 级别下的 InnoDB 快照读区别 ### 持久特性 -#### 重做日志 +#### 持久方式 +持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 +Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入了 redo log 日志: +* redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复**提交后**的数据页,只能恢复到最后一次提交的位置 +* redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 +工作过程:MySQL 发生了宕机,InnoDB 会判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏 +缓冲池的**刷脏策略**: -*** +* redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到磁盘中的数据文件中 +* Buffer Pool 内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘(要避免大事务) +* 系统空闲时,后台线程会自动进行刷脏(Flush 链表部分已经详解) +* MySQL 正常关闭时,会把内存的脏页都刷新到磁盘上 -#### 实现原理 +**** -##### 数据恢复 -持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 -Buffer Pool 是一片内存空间,可以通过 innodb_buffer_pool_size 来控制 Buffer Pool 的大小(内存优化部分会详解参数) +#### 重做日志 -* Change Buffer 是 Buffer Pool 里的内存,不能无限增大,用来对增删改操作提供缓存 -* Change Buffer 的大小可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% -* 补充知识:**唯一索引的更新不能使用 Buffer**,一般只有普通索引可以使用,直接写入 Buffer 就结束 +##### 缓冲区 -Buffer Pool 的使用提高了读写数据的效率,但是也带了新的问题:如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入 redo log +服务器启动时会向操作系统申请一片连续内存空间作为 redo log buffer(重做日志缓冲区),可以通过 `innodb_log_buffer_size` 系统变量指定 log buffer 的大小,默认是 16MB -* 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作 -* 如果 MySQL 宕机,InnoDB 判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏(buffer pool 的任务) +log buffer 被划分为若干 redo log block(块,类似数据页的概念),每个默认大小 512 字节,每个 block 由 12 字节的 log block head、496 字节的 log block body、4 字节的 log block trailer 组成 -redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复提交后的物理数据页,且只能恢复到最后一次提交的位置 +补充知识:MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction(MTR),比如在 B+ 树上插入一条数据就算一个 MTR -redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 +* 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作,写入 log buffer 的过程是顺序写入的(先写入前面的 block,写满后继续写下一个) +* log buffer 中有一个指针 buf_free,来标识该位置之前都是填满的 block,该位置之后都是空闲区域(碰撞指针) +* 一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,所以并不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入 log buffer**,在进行数据恢复时把这一组 redo log 当作一个不可分割的整体处理 redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快: * 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO * 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,减少无效 IO -InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到磁盘,具体的刷盘策略: +InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到磁盘,具体的**刷盘策略**: * 通过修改参数 `innodb_flush_log_at_trx_commit` 设置: - * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待主线程每秒刷新一次 - * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功 - * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作 -* 如果写入 redo log buffer 的日志已经占据了 redo log buffer 总容量的一半了,此时就会刷入到磁盘文件,这时会影响执行效率,所以开发中应该**避免大事务** + * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待后台线程每秒刷新一次 + * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功(默认值) + * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作。已经写入到操作系统的缓存,如果操作系统没有宕机而 MySQL 宕机,也是可以恢复数据的 +* 写入 redo log buffer 的日志超过了总容量的一半,就会将日志刷入到磁盘文件,这会影响执行效率,所以开发中应**避免大事务** +* 服务器关闭时 +* checkpoint 时(下小节详解) + + + +*** -刷脏策略: -* redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把旧记录更新到磁盘中的数据文件中 -* Buffer Pool 内存不足,需要淘汰部分数据页,如果淘汰的是脏页,就要先将脏页写到磁盘(大事务) -* 系统空闲时,后台线程会自动进行刷脏 -* MySQL 正常关闭时,会把内存的脏页都 flush 到磁盘上 + +##### 磁盘文件 + +redo 存储在磁盘中的日志文件是被**循环使用**的,redo 日志文件组中每个文件大小一样格式一样,组成结构:前 2048 个字节(前 4 个 block)用来存储一些管理信息,以后的存储 log buffer 中的 block 镜像 + +注意:block 并不代表一组 redo log,一组日志可能占用不到一个 block 或者几个 block,依赖 MTR 的大小 + +磁盘存储 redo log 的文件目录通过 `innodb_log_group_home_dir` 指定,默认是当前数据目录,文件大小: + +* 通过两个参数调节:`innodb_log_file_size` 文件大小默认 48M,`innodb_log_files_in_group` 文件个数默认 2 最大 100 +* 服务器启动后磁盘空间不变,所以采用循环写数据的方式,写完尾部重新写头部,所以要确保头部 log 对应的修改已经持久化到磁盘 + +lsn (log sequence number) 代表已经写入的 redo 日志量、flushed_to_disk_lsn 指刷新到磁盘中的 redo 日志量,两者都是**全局变量**,如果两者的值相同,说明 log buffer 中所有的 redo 日志都已经持久化到磁盘 + +MTR 的执行过程中修改过的页对应的控制块会加到 Buffer Pool 的 flush 链表中,链表中脏页是按照第一次修改的时间进行排序的(头插),控制块中有两个指针用来记录脏页被修改的时间: + +* oldest_modification:第一次修改 Buffer Pool 中某个缓冲页时,将修改该页的 MTR **开始时**对应的 lsn 值写入这个属性,所以链表页是以该值进行排序的 +* newest_modification:每次修改页面,都将 MTR **结束时**对应的 lsn 值写入这个属性,所以是该页面最后一次修改后对应的 lsn 值 + +全局变量 checkpoint_lsn 表示当前系统中可以被覆盖的 redo 日志量,当 redo 日志对应的脏页已经被刷新到磁盘后就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息,刷脏和执行一次 checkpoint并不是同一个线程 + +使用命令可以查看当前 InnoDB 存储引擎各种 lsn 的值: + +```mysql +SHOW ENGINE INNODB STATUS\G +``` + + + +**** + + + +##### 崩溃恢复 + +恢复的起点:在从 redo 日志文件组的管理信息中获取最近发生 checkpoint 的信息,从 checkpoint_lsn 对应的日志文件开始恢复 + +恢复的终点:扫描日志文件的 block,block 的头部记录着当前 block 使用了多少字节,填满的 block 总是 512 字节, 如果某个 block 不是 512 字节,说明该 block 就是需要恢复的最后一个 block + +恢复的过程:按照 redo log 依次执行恢复数据,优化方式 + +* 使用哈希表:根据 redo log 的 space ID 和 page number 属性计算出哈希值,将对同一页面的修改放入同一个槽里,可以一次性完成对某页的恢复,**避免了随机 IO** +* 跳过已经刷新到磁盘中的页面:数据页的 File Header 中的 FILE_PAGE_LSN 属性(类似 newest_modification)表示最近一次修改页面时的 lsn 值,如果在 checkpoint 后,数据页被刷新到磁盘中,那么该页 lsn 属性肯定大于 checkpoint_lsn + + + +参考书籍:https://book.douban.com/subject/35231266/ @@ -5957,7 +6052,7 @@ InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到 -##### 工作流程 +#### 工作流程 MySQL 中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,**保证数据不丢失**,二者的区别是: diff --git a/Java.md b/Java.md index c1d2f77..dc01f5b 100644 --- a/Java.md +++ b/Java.md @@ -4285,7 +4285,7 @@ LinkedList 是一个实现了 List 接口的**双端链表**,支持高效的 * 获取元素:`get(int index)` 根据指定索引返回数据 - * 获取头节点 (index=0):getFirst()、element()、peek()、peekFirst() 这四个获取头结点方法的区别在于对链表为空时的处理,是抛出异常还是返回null,其中 **getFirst() 和 element()** 方法将会在链表为空时,抛出异常 + * 获取头节点 (index=0):`getFirst()、element()、peek()、peekFirst()` 这四个获取头结点方法的区别在于对链表为空时的处理方式,是抛出异常还是返回NULL,其中 `getFirst() element()` 方法将会在链表为空时,抛出异常 * 获取尾节点 (index=-1):getLast() 方法在链表为空时,抛出 NoSuchElementException,而 peekLast() 不会,只会返回 null * 删除元素: @@ -10209,7 +10209,7 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 为对象分配内存:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 -* 如果内存规整,使用指针碰撞(BumpThePointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 +* 如果内存规整,使用指针碰撞(Bump The Pointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 * 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配。已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 From 635916b60153be1a0418e03eaba44ba303346658 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 12 Dec 2021 23:25:14 +0800 Subject: [PATCH 044/122] Update Java Notes --- DB.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/DB.md b/DB.md index 2710a29..e3ef7c8 100644 --- a/DB.md +++ b/DB.md @@ -650,6 +650,8 @@ EXPLAIN 执行计划在优化器阶段生成,如果 explain 的结果预估的 ### 数据空间 +==TODO:本节知识是抄录自《MySQL 实战 45 讲》不作为重点学习目标,暂时记录方便后续有了新的理解后更新知识== + #### 数据存储 系统表空间是用来放系统信息的,比如数据字典什么的,对应的磁盘文件是 ibdata,数据表空间是一个个的表数据文件,对应的磁盘文件就是表名.ibd @@ -5527,6 +5529,8 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 BEGIN [WORK]; ``` + 说明:只读事务不能对普通的表进行增删改操作,但是可以对临时表增删改 + * 回滚事务,用来手动中止事务 ```mysql @@ -5946,8 +5950,8 @@ RC、RR 级别下的 InnoDB 快照读区别 Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入了 redo log 日志: * redo log **记录数据页的物理修改**,而不是某一行或某几行的修改,用来恢复**提交后**的数据页,只能恢复到最后一次提交的位置 - * redo log 采用的是 WAL(Write-ahead logging,**预写式日志**),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求 +* 简单的 redo log 是纯粹的物理日志,负责的 redo log 会存在物理日志和逻辑日志 工作过程:MySQL 发生了宕机,InnoDB 会判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏 @@ -5966,17 +5970,17 @@ Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机 #### 重做日志 -##### 缓冲区 +##### 日志缓冲 服务器启动时会向操作系统申请一片连续内存空间作为 redo log buffer(重做日志缓冲区),可以通过 `innodb_log_buffer_size` 系统变量指定 log buffer 的大小,默认是 16MB -log buffer 被划分为若干 redo log block(块,类似数据页的概念),每个默认大小 512 字节,每个 block 由 12 字节的 log block head、496 字节的 log block body、4 字节的 log block trailer 组成 - 补充知识:MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction(MTR),比如在 B+ 树上插入一条数据就算一个 MTR +log buffer 被划分为若干 redo log block(块,类似数据页的概念),每个默认大小 512 字节,每个 block 由 12 字节的 log block head、496 字节的 log block body、4 字节的 log block trailer 组成 + * 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作,写入 log buffer 的过程是顺序写入的(先写入前面的 block,写满后继续写下一个) * log buffer 中有一个指针 buf_free,来标识该位置之前都是填满的 block,该位置之后都是空闲区域(碰撞指针) -* 一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,所以并不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入 log buffer**,在进行数据恢复时把这一组 redo log 当作一个不可分割的整体处理 +* 一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,所以并不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入 log buffer**,在进行数据恢复时也把这一组 redo log 当作一个不可分割的整体处理 redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快: @@ -5985,7 +5989,7 @@ redo log 也需要在事务提交时将日志写入磁盘,但是比将内存 InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到磁盘,具体的**刷盘策略**: -* 通过修改参数 `innodb_flush_log_at_trx_commit` 设置: +* 在事务提交时需要进行刷盘,通过修改参数 `innodb_flush_log_at_trx_commit` 设置: * 0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待后台线程每秒刷新一次 * 1:在事务提交时将缓冲区的 redo 日志**同步写入**到磁盘,保证一定会写入成功(默认值) * 2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作。已经写入到操作系统的缓存,如果操作系统没有宕机而 MySQL 宕机,也是可以恢复数据的 From c98186e0d4b99f8d80ebbecd06f05e30af95c8f6 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 15 Dec 2021 00:51:39 +0800 Subject: [PATCH 045/122] Update Java Notes --- DB.md | 147 +++++++++++++++++++++++++++++++++++++++++++++++++------- Java.md | 15 ++++-- 2 files changed, 141 insertions(+), 21 deletions(-) diff --git a/DB.md b/DB.md index e3ef7c8..48cb370 100644 --- a/DB.md +++ b/DB.md @@ -650,8 +650,6 @@ EXPLAIN 执行计划在优化器阶段生成,如果 explain 的结果预估的 ### 数据空间 -==TODO:本节知识是抄录自《MySQL 实战 45 讲》不作为重点学习目标,暂时记录方便后续有了新的理解后更新知识== - #### 数据存储 系统表空间是用来放系统信息的,比如数据字典什么的,对应的磁盘文件是 ibdata,数据表空间是一个个的表数据文件,对应的磁盘文件就是表名.ibd @@ -685,7 +683,7 @@ InnoDB 的数据是按页存储的如果删掉了一个数据页上的所有记 -#### 空间收缩 +#### 重建数据 重建表就是按照主键 ID 递增的顺序,把数据一行一行地从旧表中读出来再插入到新表中,重建表时 MySQL 会自动完成转存数据、交换表名、删除旧表的操作,重建命令: @@ -709,7 +707,7 @@ MySQL 5.6 版本开始引入的 Online DDL,重建表的命令默认执行此 Online DDL 操作会先获取 MDL 写锁,再退化成 MDL 读锁。但 MDL 写锁持有时间比较短,所以可以称为 Online; 而 MDL 读锁,不阻止数据增删查改,但会阻止其它线程修改表结构 -问题:想要收缩表空间,执行指令后整体占用空间增大 +问题:重建表可以收缩表空间,但是执行指令后整体占用空间增大 原因:在重建表后 InnoDB 不会把整张表占满,每个页留了 1/16 给后续的更新使用。表在未整理之前页已经占用 15/16 以上,收缩之后需要保持数据占用空间在 15/16,所以文件占用空间更大才能保持 @@ -732,6 +730,8 @@ DDL 中的临时表 tmp_table 是在 Server 层创建的,Online DDL 中的临 +==本节知识是抄录自《MySQL 实战 45 讲》,作者目前没有更深的理解,暂时记录,后续有了新的认知后会更新知识== + 参考文章:https://time.geekbang.org/column/article/72388 @@ -3791,7 +3791,7 @@ InnoDB 使用 B+Tree 作为索引结构,并且 InnoDB 一定有索引 * 在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这个索引的 key 是数据表的主键,叶子节点 data 域保存了完整的数据记录 -* InnoDB 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个**隐含字段**作为主键,这个字段长度为 6 个字节,类型为长整形(MVCC 部分的笔记提及) +* InnoDB 的表数据文件**通过主键聚集数据**,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个**隐含字段 row_id** 作为主键,这个字段长度为 6 个字节,类型为长整形 辅助索引: @@ -3868,6 +3868,10 @@ InnoDB 存储引擎中有页(Page)的概念,页是 MySQL 磁盘管理的 * Page Directory:分组的目录,可以通过目录快速定位(二分法)数据的分组 * File Trailer:检验和字段,在刷脏过程中,页首和页尾的校验和一致才能说明页面刷新成功,二者不同说明刷新期间发生了错误;LSN 字段,也是用来校验页面的完整性 +数据页中包含数据行,数据的存储是基于数据行的,数据行有 next_record 属性指向下一个行数据,所以是可以遍历的,但是一组数据至多 8 个行,通过 Page Directory 先定位到组,然后遍历获取所需的数据行即可 + +数据行中有三个隐藏字段:trx_id、roll_pointer、row_id(在事务章节会详细介绍它们的作用) + *** @@ -5529,7 +5533,7 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 BEGIN [WORK]; ``` - 说明:只读事务不能对普通的表进行增删改操作,但是可以对临时表增删改 + 说明:不填状态默认是读写事务 * 回滚事务,用来手动中止事务 @@ -5614,6 +5618,26 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 +**** + + + +#### 事务 ID + +只读事务不能对普通的表进行增删改操作,但是可以对临时表增删改,读写事务可以对数据表执行增删改查操作 + +事务在执行过程中对某个表执行了**增删改操作或者创建表**,就会为当前事务分配一个独一无二的事务 ID(对临时表并不会分配 ID),如果当前事务没有被分配 ID,默认是 0 + +事务 ID 本质上就是一个数字,服务器在内存中维护一个全局变量: + +* 每当需要为某个事务分配 ID,就会把全局变量的值赋值给事务 ID,然后变量自增 1 +* 每当变量值为 256 的倍数时,就将该变量的值刷新到系统表空间的 Max Trx ID 属性中,该属性占 8 字节 +* 系统再次启动后,会读取表空间的 Max Trx ID 属性到内存,加上 256 后赋值给全局变量,因为关机时的事务 ID 可能并不是 256 的倍数,会比 Max Trx ID 大,所以需要加上 256 保持事务 ID 是一个递增的数字 + +**聚簇索引**的行记录除了完整的数据,还会自动添加 trx_id、roll_pointer 隐藏列,如果表中没有主键并且没有非空唯一索引,也会添加一个 row_id 的隐藏列作为聚簇索引 + + + *** @@ -5669,6 +5693,8 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 ### 原子特性 +#### 实现方式 + 原子性是指事务是一个不可分割的工作单位,事务的操作如果成功就必须要完全应用到数据库,失败则不能对数据库有任何影响。比如事务中一个 SQL 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态 InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 undo log(回滚日志) @@ -5686,16 +5712,100 @@ undo log 属于逻辑日志,根据每行操作进行记录,记录了 SQL 执 * 对于每个 update,回滚时会执行一个相反的 update,把数据修改回去 -undo log 是采用段(segment)的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment -rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segment -* 在以前老版本,只支持 1 个 rollback segment,只能记录 1024 个 undo log segment -* MySQL5.5 开始支持 128 个 rollback segment,支持 128*1024 个 undo 操作 +参考文章:https://www.cnblogs.com/kismetv/p/10331633.html -参考文章:https://www.cnblogs.com/kismetv/p/10331633.html +*** + + + +#### DML 解析 + +##### INSERT + +乐观插入:当前数据页的剩余空间充足,直接将数据进行插入 + +悲观插入:当前数据页的剩余空间不足,需要进行页分裂,申请一个新的页面来插入数据,会造成更多的 redo log,undo log 影响不大 + +当向某个表插入一条记录,实际上需要向聚簇索引和所有二级索引都插入一条记录,但是 undo log 只需要针对聚簇索引记录,在回滚时会根据聚簇索引去所有的二级索引进行回滚操作 + +roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一条记录就是一个数据行,行格式中的 roll_pointer 就指向 undo log + + + +*** + + + +##### DELETE + +插入到页面中的记录会根据 next_record 属性组成一个单向链表,这个链表称为正常链表,被删除的记录也会通过 next_record 组成一个垃圾链表,该链表中所占用的存储空间可以被重新利用,并不会直接清除数据 + +在页面 Page Header 中,PAGE_FREE 属性指向垃圾链表的头节点,删除的工作过程: + +* 将要删除的记录的 delete_flag 位置为 1,其他不做修改,这个过程叫 **delete mark** +* 在事务提交前,delete_flag = 1 的记录一直都会处于中间状态 +* 事务提交后,有专门的线程将 delete_flag = 1 的记录从正常链表移除并加入垃圾链表,这个过程叫 **purge** + +在对一条记录 delete mark 前会将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到 undo log 对应的属性中,这样就会产生记录的 roll_pointer 指向当前 undo log 记录,当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录,**形成一个版本链** + +当有新插入的记录时,首先判断 PAGE_FREE 指向的头节点是否足够容纳新纪录: + +* 如果可以容纳新纪录,就会直接重用已删除的记录的存储空间,然后让 PAGE_FREE 指向垃圾链表的下一个节点 +* 如果不能容纳新纪录,就直接向页面申请新的空间存储,并不会遍历垃圾链表 + +重用已删除的记录空间,可能会造成空间碎片,当数据页容纳不了一条记录时,会判断将碎片空间加起来是否可以容纳,判断为真就会重新组织页内的记录: + +* 开辟一个临时页面,将页内记录一次插入到临时页面,此时临时页面时没有碎片的 +* 把临时页面的内容复制到本页,这样就解放出了内存碎片,但是会耗费很大的性能资源 + + + +**** + + + +##### UPDATE + +执行 UPDATE 语句,对于更新主键和不更新主键有两种不同的处理方式 + +不更新主键的情况: + +* 就地更新(in-place update),如果更新后的列和更新前的列占用的存储空间一样大,就可以直接在原记录上修改 + +* 先删除旧纪录,再插入新纪录,这里的删除不是 delete mark,而是直接将记录加入垃圾链表,并且修改页面的相应的控制信息,执行删除的线程不是 purge,是执行更新的用户线程 + + 插入新记录时可能造成页空间不足,从而导致页分裂 + +更新主键的情况: + +* 将旧纪录进行 delete mark,在更新语句提交后由 purge 线程移入垃圾链表 +* 根据更新的各列的值创建一条新纪录,插入到聚簇索引中 + + + +*** + + + +#### 回滚日志 + +undo log 是采用段(segment)的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment + +Rollback Segement 称为回滚段,每个回滚段中有 1024 个 undo slot + +* 在以前老版本,只支持 1 个 Rollback Segement,只能记录 1024 个 undo log segment +* MySQL5.5 开始支持 128 个 Rollback Segement,支持 128*1024 个 undo 操作 + +工作流程: + +* 事务执行前需要到系统表空间第 5 号页面中分配一个回滚段(页),获取一个 Rollback Segement Header 页面的地址 +* 回滚段页面有 1024 个 undo slot,每个 slot 存放 undo 链表页面的头节点页号。首先去回滚段的两个 cached 链表看是否有缓存的 slot,缓存中没有就在回滚段中找一个可用的 slot +* 缓存中获取的 slot 对应的 Undo Log Segment 已经分配了,需要重新分配,然后从 Undo Log Segment 中申请一个页面作为日志链表的头节点,并填入对应的 slot 中 +* 开始记录 @@ -5817,11 +5927,6 @@ undo log 主要分为两种: * 事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先记录 undo log,然后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁 * 以此类推 -补充知识:purge 线程 - -* 为了实现 InnoDB 的 MVCC 机制,更新或者删除操作都只是设置一下老记录的 deleted_bit,并不真正将过时的记录删除,为了节省磁盘空间,InnoDB 有专门的 purge 线程来清理 deleted_bit 为 true 的记录 -* purge 线程维护了一个 Read view(这个 Read view 相当于系统中最老活跃事务的 Read view),如果某个记录的 deleted_bit 为 true,并且 DB_TRX_ID 相对于 purge 线程的 Read view 可见,那么这条记录一定是可以被安全清除的 - *** @@ -5943,7 +6048,7 @@ RC、RR 级别下的 InnoDB 快照读区别 ### 持久特性 -#### 持久方式 +#### 实现方式 持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。 @@ -6021,7 +6126,9 @@ 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 链表中把最早修改的脏页刷新到磁盘中,然后执行 checkpoint 使用命令可以查看当前 InnoDB 存储引擎各种 lsn 的值: @@ -6046,6 +6153,10 @@ 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 log 可能已经刷盘,这些数据可能在重启后也会恢复 + +解决:通过 undo log 在服务器重启时将未提交的事务回滚掉,定位到 128 个回滚段,遍历 slot,获取 undo 链表首节点页面的 Undo Segement Header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,如果是活跃的就全部回滚 + 参考书籍:https://book.douban.com/subject/35231266/ diff --git a/Java.md b/Java.md index dc01f5b..e3ddbb2 100644 --- a/Java.md +++ b/Java.md @@ -19,6 +19,15 @@ +初学时笔记内容参考视频:https://www.bilibili.com/video/BV1TE41177mP,后随着学习的深入逐渐增加了很多知识 + +给初学者的一些个人建议: + +* 初学者对编程的认知比较浅显,一些专有词汇和概念难以理解,所以建议观看视频进行入门,大部分公开课视频讲的比较基础 +* 在有了一定的编程基础后,需要看一些经典书籍和技术博客,来扩容自己的知识广度和深度,可以长期保持记录笔记的好习惯 + + + *** @@ -140,8 +149,8 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, ``` - - + + *** @@ -4464,7 +4473,7 @@ public class Student implements Comparable{ } ``` -比较器原理:底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边。(树) +比较器原理:底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边。(红黑树) From 8ad135fd9c29437e0aac3aa128b0b0e293220ec1 Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 15 Dec 2021 23:24:20 +0800 Subject: [PATCH 046/122] Update Java Notes --- DB.md | 147 +++++++++++++++++++++++++++++++++----------------------- Java.md | 8 +-- 2 files changed, 92 insertions(+), 63 deletions(-) diff --git a/DB.md b/DB.md index 48cb370..45d46c7 100644 --- a/DB.md +++ b/DB.md @@ -379,7 +379,32 @@ 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 服务器可以同时和多个客户端进行交互,所以要保证每个连接会话的隔离性(事务机制部分详解) + +整体的执行流程: + + + + + +**** + + + +##### 连接状态 首先连接到数据库上,这时连接器发挥作用,连接完成后如果没有后续的动作,这个连接就处于空闲状态,通过指令查看连接状态 @@ -398,19 +423,6 @@ SHOW PROCESSLIST:查看当前 MySQL 在进行的线程,可以实时地查看 | State | 显示使用当前连接的 sql 语句的状态,以查询为例,需要经过 copying to tmp table、sorting result、sending data等状态才可以完成 | | Info | 显示执行的 sql 语句,是判断问题语句的一个重要依据 | -连接建立 TCP 以后需要做**权限验证**,验证成功后可以进行执行 SQL。如果这时管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限,只有再新建的连接才会使用新的权限设置 - -客户端如果太长时间没动静,连接器就会自动断开,这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒:`Lost connection to MySQL server during query` - -数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接;短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。为了减少连接的创建,推荐使用长连接,但是过多的长连接会造成 OOM,解决方案: - -* 定期断开长连接,使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。 -* MySQL 5.7 版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源,这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态 - - - - - *** @@ -5281,10 +5293,10 @@ Buffer Pool 本质上是 InnoDB 向操作系统申请的一段连续的内存空 工作原理: * 从数据库读取数据时,会首先从缓存中读取,如果缓存中没有,则从磁盘读取后放入 Buffer Pool - * 向数据库写入数据时,会首先写入缓存,缓存中修改的数据会**定期刷新**到磁盘,这一过程称为刷脏 - **唯一索引的更新不能使用 Buffer**,一般只有普通索引可以使用,直接写入 Buffer 就结束 + +**唯一索引的更新不能使用 Buffer,只有普通索引可以使用,直接写入 Buffer 就结束,不用校验唯一性** Buffer Pool 中每个缓冲页都有对应的控制信息,包括表空间编号、页号、偏移量、链表信息等,控制信息存放在占用的内存称为控制块,控制块与缓冲页是一一对应的,但并不是物理上相连的,都在缓冲池中 @@ -5649,26 +5661,29 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 隔离级别分类: -| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | -| ---------------- | -------- | -------------------------------- | ------------------- | -| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | -| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | -| repeatable read | 可重复读 | 幻读 | MySQL | -| serializable | 串行化 | 无(因为写会加写锁,读会加读锁) | | +| 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | +| ---------------- | -------- | ---------------------- | ------------------- | +| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | +| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | +| repeatable read | 可重复读 | 幻读 | MySQL | +| serializable | 可串行化 | 无 | | + +* 串行化:让所有事务按顺序单独执行,写操作会加写锁,读操作会加读锁 +* 可串行化:让所有操作相同数据的事务顺序执行,通过加锁实现 一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 -* 丢失更新 (Lost Update):当两个或多个事务选择同一行,最初的事务修改的值,被后面事务修改的值覆盖,所有的隔离级别都可以避免丢失更新(行锁) +* 脏写 (Dirty Write):当两个或多个事务选择同一行,最初的事务修改的值被后面事务修改的值覆盖,所有的隔离级别都可以避免脏写(又叫丢失更新),因为有行锁 -* 脏读 (Dirty Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个**未提交**的事务中的数据 +* 脏读 (Dirty Reads):在一个事务处理过程中读取了另一个**未提交**的事务中修改过的数据 -* 不可重复读 (Non-Repeatable Reads):在事务中先后两次读取同一个数据,两次读取的结果不一样,原因是在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 +* 不可重复读 (Non-Repeatable Reads):在一个事务处理过程中读取了另一个事务中修改并**已提交**的数据 > 可重复读的意思是不管读几次,结果都一样,可以重复的读,可以理解为快照读,要读的数据集不会发生变化 * 幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,后一次查询查到了前一次查询没有查到的行,**数据条目**发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入 -**隔离级别操作语法:** +隔离级别操作语法: * 查询数据库隔离级别 @@ -5750,7 +5765,7 @@ roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一 * 在事务提交前,delete_flag = 1 的记录一直都会处于中间状态 * 事务提交后,有专门的线程将 delete_flag = 1 的记录从正常链表移除并加入垃圾链表,这个过程叫 **purge** -在对一条记录 delete mark 前会将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到 undo log 对应的属性中,这样就会产生记录的 roll_pointer 指向当前 undo log 记录,当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录,**形成一个版本链** + purge 线程在执行删除操作时会创建一个 ReadView,根据事务的可见性移除数据(隔离特性部分详解) 当有新插入的记录时,首先判断 PAGE_FREE 指向的头节点是否足够容纳新纪录: @@ -5785,6 +5800,10 @@ roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一 * 将旧纪录进行 delete mark,在更新语句提交后由 purge 线程移入垃圾链表 * 根据更新的各列的值创建一条新纪录,插入到聚簇索引中 +在对一条记录修改前会**将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到 undo log 对应的属性中**,这样就记录的 roll_pointer 指向当前 undo log 记录,当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录,**形成一个版本链** + + + *** @@ -5853,7 +5872,7 @@ MVCC 处理读写请求,可以做到在发生读写请求冲突时不用加锁 * 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读 -* 写-写:有线程安全问题,可能会存在丢失更新问题 +* 写-写:有线程安全问题,可能会存在脏写(丢失更新)问题 MVCC 的优点: @@ -5881,15 +5900,11 @@ MVCC 的优点: 实现原理主要是隐藏字段,undo日志,Read View 来实现的 -数据库中的每行数据,除了自定义的字段,还有数据库隐式定义的字段: - -* DB_TRX_ID:6byte,最近修改事务ID,记录创建该数据或最后一次修改(修改/插入)该数据的事务ID。当每个事务开启时,都会被分配一个ID,这个 ID 是递增的 - -* DB_ROLL_PTR:7byte,回滚指针,配合 undo 日志,指向上一个旧版本(存储在 rollback segment) - -* DB_ROW_ID:6byte,隐含的自增ID(**隐藏主键**),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 作为聚簇索引 +数据库中的**聚簇索引**每行数据,除了自定义的字段,还有数据库隐式定义的字段: -* DELETED_BIT:删除标志的隐藏字段,记录被更新或删除并不代表真的删除,而是删除位变了 +* DB_TRX_ID:最近修改事务 ID,记录创建该数据或最后一次修改该数据的事务 ID +* 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) @@ -5897,15 +5912,13 @@ MVCC 的优点: - - *** -##### undo +##### 版本链 -undo log 是逻辑日志,记录的是每个事务对数据执行的操作,而不是记录的全部数据,需要根据 undo log 逆推出以往事务的数据 +undo log 是逻辑日志,记录的是每个事务对数据执行的操作,而不是记录的全部数据,要**根据 undo log 逆推出以往事务的数据** undo log 的作用: @@ -5918,12 +5931,15 @@ undo log 主要分为两种: * update undo log:事务在进行 update 或 delete 时产生的 undo log,在事务回滚时需要,在快照读时也需要。不能随意删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除 -每次对数据库记录进行改动,都会将旧值放到一条 undo 日志中,算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为**版本链**,版本链的头节点就是当前记录最新的值,链尾就是最早的旧记录 +每次对数据库记录进行改动,都会产生的新版本的 undo log,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为**版本链**,版本链的头节点就是当前的最新的 undo log,链尾就是最早的旧 undo log -* 有个事务插入 persion 表一条新记录,name 为 Jerry,age 为 24 +补充:undo 是逻辑日志,这里只是直观的展示出来 +工作流程: + +* 有个事务插入 persion 表一条新记录,name 为 Jerry,age 为 24 * 事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先记录 undo log,然后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁 * 以此类推 @@ -5935,26 +5951,26 @@ undo log 主要分为两种: ##### 读视图 -Read View 是事务进行**快照读**操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 +Read View 是事务进行读数据操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据 注意:这里的快照并不是把所有的数据拷贝一份副本,而是由 undo log 记录的逻辑日志,根据库中的数据进行计算出历史数据 -工作流程:将版本链的头节点的事务 ID(最新数据事务 ID)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比,如果 DB_TRX_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 几个属性: - m_ids:生成 Read View 时当前系统中活跃的事务 id 列表(未提交的事务集合,当前事务也在其中) -- up_limit_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值(已提交的事务集合) -- low_limit_id:生成 Read View 时系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1(未开始事务) +- min_trx_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值(已提交的事务集合) +- max_trx_id:生成 Read View 时当前系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1(未开始事务) - creator_trx_id:生成该 Read View 的事务的事务 id,就是判断该 id 的事务能读到什么数据 creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交) -* db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以这种情况下此数据对 creator 是可见的 -* db_trx_id < up_limit_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见 +* db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以此数据对 creator 是可见的 +* db_trx_id < min_trx_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见 -* db_trx_id >= low_limit_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见 -* up_limit_id <= db_trx_id < low_limit_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中 +* 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 中 * 在列表中,说明该版本对应的事务正在运行,数据不能显示(**不能读到未提交的数据**) * 不在列表中,说明该版本对应的事务已经被提交,数据可以显示(**可以读到已经提交的数据**) @@ -5993,8 +6009,8 @@ START TRANSACTION; -- 开启事务 ID 为 0 的事务创建 Read View: * m_ids:20、60 -* up_limit_id:20 -* low_limit_id:61 +* min_trx_id:20 +* max_trx_id:61 * creator_trx_id:0 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MVCC工作流程2.png) @@ -6011,28 +6027,41 @@ ID 为 0 的事务创建 Read View: +##### 二级索引 + +只有在聚簇索引中才有 trx_id 和 roll_pointer 的隐藏列,对于二级索引判断可见性的方式: + +* 二级索引页面的 Page Header 中有一个 PAGE_MAX_TRX_ID 属性,代表修改当前页面的最大的事务 ID,SELECT 语句访问某个二级索引时会判断 ReadView 的 min_trx_id 是否大于该属性,大于说明该页面的所有属性对 ReadView 可见 +* 如果属性判断不可见,就需要利用二级索引获取主键值,进行回表操作,得到聚簇索引后再按照聚簇索引的可见性判断的方法操作 + + + +*** + + + #### RC RR Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现 RR、RC 生成时机: -- RC 隔离级别下,每个快照读都会生成并获取最新的 Read View -- RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View +- RC 隔离级别下,每次读取数据前都会生成最新的 Read View(当前读) +- RR 隔离级别下,在第一次数据读取时才会创建 Read View(快照读) RC、RR 级别下的 InnoDB 快照读区别 -- RC 级别下的,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因 +- RC 级别下,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因 -- RR 级别下的某个事务的对某条记录的第一次快照读会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个Read View,所以一个事务的查询结果每次都是相同的 +- RR 级别下,某个事务的对某条记录的第一次快照读会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个 Read View,所以一个事务的查询结果每次都是相同的 - 当前事务在其他事务提交之前使用过快照读,那么以后其他事务对数据的修改都是不可见的,就算以后其他事务提交了数据也不可见;早于 Read View 创建的事务所做的修改并提交的均是可见的 + RR 级别下,通过 `START TRANSACTION WITH CONSISTENT SNAPSHOT` 开启事务,会在执行该语句后立刻生成一个 Read View,不是在执行第一条 SELECT 语句时生成 解决幻读问题: - 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** - 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 Read View 并不能阻止事务去更新数据,并且把这条新记录的 trx_id 给变为当前的事务 id,对当前事务就是可见的了 + 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 **Read View 并不能阻止事务去更新数据**,并且把这条新记录的 trx_id 给变为当前的事务 id,所以对当前事务就是可见的 - 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 @@ -6040,8 +6069,6 @@ RC、RR 级别下的 InnoDB 快照读区别 - - *** diff --git a/Java.md b/Java.md index e3ddbb2..e0f0de8 100644 --- a/Java.md +++ b/Java.md @@ -531,11 +531,11 @@ public class Test1 { * || 和 |,&& 和& 的区别,逻辑运算符 - **&和| 称为布尔运算符,位运算符。&&和|| 称为条件布尔运算符,也叫短路运算符**。 + **&和| 称为布尔运算符,位运算符。&&和|| 称为条件布尔运算符,也叫短路运算符**。 - 两种运算符得到的结果完全相同,但得到结果的方式又一个重要区别:条件布尔运算符性能比较好。他检查第一个操作数的值,再根据该操作数的值进行操作,可能根本就不处理第二个操作数。 + 如果 && 运算符的第一个操作数是 false,就不需要考虑第二个操作数的值了,因为无论第二个操作数的值是什么,其结果都是 false;同样,如果第一个操作数是 true,|| 运算符就返回 true,无需考虑第二个操作数的值;但 & 和 | 却不是这样,它们总是要计算两个操作数。为了提高性能,**尽可能使用 && 和 || 运算符** - 结论:如果 && 运算符的第一个操作数是 false,就不需要考虑第二个操作数的值了,因为无论第二个操作数的值是什么,其结果都是 false;同样,如果第一个操作数是 true,|| 运算符就返回 true,无需考虑第二个操作数的值。但 & 和 | 却不是这样,它们总是要计算两个操作数。为了提高性能,**尽可能使用 && 和 || 运算符** +* ^ 异或:两位相异为 1,相同为 0,又叫不进位加法。同或:两位相同为 1,相异为 0 * switch @@ -591,11 +591,13 @@ public class Test1 { 运算规则: * 正数的左移与右移,空位补 0 + * 负数原码的左移与右移,空位补 0 负数反码的左移与右移,空位补 1 负数补码,左移低位补 0(会导致负数变为正数的问题,因为移动了符号位),右移高位补 1 + * 无符号移位,空位补 0 From 744e3006467240d278b6cce5a3cffd337ddffbad Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 18 Dec 2021 00:05:29 +0800 Subject: [PATCH 047/122] Update Java Notes --- DB.md | 14 ++++++++------ Java.md | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/DB.md b/DB.md index 45d46c7..558ea03 100644 --- a/DB.md +++ b/DB.md @@ -3626,7 +3626,7 @@ MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获 - 单列索引:一个索引只包含单个列,一个表可以有多个单列索引(普通索引) - 联合索引:顾名思义,就是将单列索引进行组合 - 唯一索引:索引列的值必须唯一,**允许有空值**,如果是联合索引,则列值组合必须唯一 - * NULL 值必须只出现一次 + * NULL 值可以出现多次,因为两个 NULL 比较的结果既不相等,也不不等,结果仍然是未知 * 可以声明不允许存储 NULL 值的非空唯一索引 - 外键索引:只有 InnoDB 引擎支持外键索引,用来保证数据的一致性、完整性和实现级联操作 @@ -4108,7 +4108,7 @@ B+ 树为了保持索引的有序性,在插入新值的时候需要做相应 #### 索引下推 -索引条件下推优化(Index Condition Pushdown)是 MySQL5.6 添加,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数 +索引条件下推优化(Index Condition Pushdown,ICP)是 MySQL5.6 添加,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数 索引下推充分利用了索引中的数据,在查询出整行数据之前过滤掉无效的数据,再去主键索引树上查找 @@ -5762,7 +5762,9 @@ roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一 在页面 Page Header 中,PAGE_FREE 属性指向垃圾链表的头节点,删除的工作过程: * 将要删除的记录的 delete_flag 位置为 1,其他不做修改,这个过程叫 **delete mark** + * 在事务提交前,delete_flag = 1 的记录一直都会处于中间状态 + * 事务提交后,有专门的线程将 delete_flag = 1 的记录从正常链表移除并加入垃圾链表,这个过程叫 **purge** purge 线程在执行删除操作时会创建一个 ReadView,根据事务的可见性移除数据(隔离特性部分详解) @@ -5859,9 +5861,7 @@ Rollback Segement 称为回滚段,每个回滚段中有 1024 个 undo slot #### 并发控制 -MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来**解决读写冲突的无锁并发控制** - -MVCC 处理读写请求,可以做到在发生读写请求冲突时不用加锁,这个读是指的快照读,而不是当前读 +MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来**解决读写冲突的无锁并发控制**,可以在发生读写请求冲突时不用加锁解决,这个读是指的快照读(也叫一致性读或一致性无锁读),而不是当前读: * 快照读:实现基于 MVCC,因为是多版本并发,所以快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据 * 当前读:读取数据库记录是当前最新的版本(产生幻读、不可重复读),可以对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读 @@ -5933,6 +5933,8 @@ undo log 主要分为两种: 每次对数据库记录进行改动,都会产生的新版本的 undo log,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为**版本链**,版本链的头节点就是当前的最新的 undo log,链尾就是最早的旧 undo log +说明:因为 DELETE 删除记录,都是移动到垃圾链表中,不是真正的删除,所以才可以通过版本链访问原始数据 + 补充:undo 是逻辑日志,这里只是直观的展示出来 @@ -6042,7 +6044,7 @@ ID 为 0 的事务创建 Read View: #### RC RR -Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现 +Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现,所以 SELECT 在 RC 和 RR 隔离级别才使用 MVCC 读取记录 RR、RC 生成时机: diff --git a/Java.md b/Java.md index e0f0de8..35e4ddd 100644 --- a/Java.md +++ b/Java.md @@ -19,7 +19,7 @@ -初学时笔记内容参考视频:https://www.bilibili.com/video/BV1TE41177mP,后随着学习的深入逐渐增加了很多知识 +初学时笔记内容参考视频:https://www.bilibili.com/video/BV1TE41177mP,随着学习的深入又增加了很多知识 给初学者的一些个人建议: From 84186472aa520ef9699b16a28becfb8c7e20d1f6 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 18 Dec 2021 22:32:45 +0800 Subject: [PATCH 048/122] Update Java Notes --- DB.md | 197 +++++++++++++++++++++++++++++++++++++++++--------------- Java.md | 8 +-- 2 files changed, 150 insertions(+), 55 deletions(-) diff --git a/DB.md b/DB.md index 558ea03..894b004 100644 --- a/DB.md +++ b/DB.md @@ -1946,6 +1946,10 @@ INSERT INTO card VALUES (NULL,'12345',1),(NULL,'56789',2); +*** + + + #### 一对多 举例:用户和订单、商品分类和商品 @@ -1976,6 +1980,10 @@ INSERT INTO orderlist VALUES (NULL,'hm001',1),(NULL,'hm002',1),(NULL,'hm003',2), +*** + + + #### 多对多 举例:学生和课程。一个学生可以选择多个课程,一个课程也可以被多个学生选择 @@ -6044,7 +6052,7 @@ ID 为 0 的事务创建 Read View: #### RC RR -Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现,所以 SELECT 在 RC 和 RR 隔离级别才使用 MVCC 读取记录 +Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现,所以 **SELECT 在 RC 和 RR 隔离级别才使用 MVCC 读取记录** RR、RC 生成时机: @@ -6300,7 +6308,7 @@ InnoDB 刷脏页的控制策略: 锁机制:数据库为了保证数据的一致性,在共享的资源被并发访问时变得安全有序所设计的一种规则 -作用:锁机制类似于多线程中的同步,可以保证数据的一致性和安全性 +利用 MVCC 性质进行读取的操作叫**一致性读**,读取数据前加锁的操作叫**锁定读** 锁的分类: @@ -6332,27 +6340,56 @@ InnoDB 刷脏页的控制策略: -### Server +### 内存结构 -FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处于只读状态,工作流程: +对一条记录加锁的本质就是在内存中创建一个锁结构与之关联,结构包括 -1. 上全局读锁(lock_global_read_lock) -2. 清理表缓存(close_cached_tables) -3. 上全局 COMMIT 锁(make_global_read_lock_block_commit) +* 事务信息:锁对应的事务信息,一个锁属于一个事务 +* 索引信息:对于行级锁,需要记录加锁的记录属于哪个索引 +* 表锁和行锁信息:表锁记录着锁定的表,行锁记录了 Space ID 所在表空间、Page Number 所在的页号、n_bits 使用了多少比特 +* type_mode:一个 32 比特的数,被分成 lock_mode、lock_type、rec_lock_type 三个部分 + * lock_mode:锁模式,记录是共享锁、排他锁、意向锁之类 + * lock_type:代表表级锁还是行级锁 + * rec_lock_type:代表行锁的具体类型和 is_waiting 属性,is_waiting = true 时表示当前事务尚未获取到锁,处于等待状态。事务获取锁后的锁结构是 is_waiting 为 false,释放锁时会检查是否与当前记录关联的锁结构,如果有就唤醒对应事务的线程 -该命令主要用于备份工具做一致性备份,由于 FTWRL 需要持有两把全局的 MDL 锁,并且还要关闭所有表对象,因此杀伤性很大 +一个事务可能操作多条记录,为了节省内存,满足下面条件的锁使用同一个锁结构: + +* 在同一个事务中的加锁操作 +* 被加锁的记录在同一个页面中 +* 加锁的类型时一样的 +* 加锁的状态时一样的 + + + + + +**** + + + +### Server MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL) -MDL 叫元数据锁,主要用来保护 MySQL内部对象的元数据,保证数据读写的正确性,通过 MDL 机制保证 DDL、DML、DQL 操作的并发,**当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁** +MDL 叫元数据锁,主要用来保护 MySQL内部对象的元数据,保证数据读写的正确性,**当对一个表做增删改查的时候,加 MDL 读锁;当要对表做结构变更操作 DDL 的时候,加 MDL 写锁**,两种锁不相互兼容,所以可以保证 DDL、DML、DQL 操作的安全 -* MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,事务中的 MDL 锁,在语句执行开始时申请,在整个事务提交后释放 +说明:DDL 操作执行前会隐式提交当前会话的事务,因为 DDL 一般会在若干个特殊事务中完成,开启特殊事务前需要提交到其他事务 -* MDL 锁是在 Server 中实现,不是 InnoDB 存储引擎层不能直接实现的锁 +MDL 锁的特性: + +* MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,在事务开始执行时申请,在整个事务提交后释放 + +* MDL 锁是在 Server 中实现,不是 InnoDB 存储引擎层能直接实现的锁 * MDL 锁还能实现其他粒度级别的锁,比如全局锁、库级别的锁、表空间级别的锁 +FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处于只读状态,工作流程: +1. 上全局读锁(lock_global_read_lock) +2. 清理表缓存(close_cached_tables) +3. 上全局 COMMIT 锁(make_global_read_lock_block_commit) + +该命令主要用于备份工具做一致性备份,由于 FTWRL 需要持有两把全局的 MDL 锁,并且还要关闭所有表对象,因此杀伤性很大 @@ -6368,7 +6405,7 @@ MyISAM 存储引擎只支持表锁,这也是 MySQL 开始几个版本中唯一 MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表加读锁,在执行增删改之前,会**自动**给涉及的表加写锁,这个过程并不需要用户干预,所以用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁 -* 加锁命令: +* 加锁命令:(对 InnoDB 存储引擎也适用) 读锁:所有连接只能读取数据,不能修改 @@ -6396,7 +6433,7 @@ MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-MyISAM 锁的兼容性.png) -锁调度:MyISAM 的读写锁调度是写优先,因为写锁后其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞,所以 MyISAM 不适合做写为主的表的存储引擎 +锁调度:**MyISAM 的读写锁调度是写优先**,因为写锁后其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞,所以 MyISAM 不适合做写为主的表的存储引擎 @@ -6532,14 +6569,16 @@ MyISAM 引擎在执行查询语句之前,会**自动**给涉及到的所有表 #### 行级锁 -InnoDB 与 MyISAM 的**最大不同**有两点:一是支持事务;二是采用了行级锁,InnoDB 同时支持表锁和行锁 +##### 记录锁 + +InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用了行级锁,**InnoDB 同时支持表锁和行锁** -InnoDB 实现了以下两种类型的行锁: +行级锁,也成为记录锁(Record Lock),InnoDB 实现了以下两种类型的行锁: -- 共享锁 (S):又称为读锁,简称 S 锁,就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 -- 排他锁 (X):又称为写锁,简称 X 锁,就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 +- 共享锁 (S):又称为读锁,简称 S 锁,多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 +- 排他锁 (X):又称为写锁,简称 X 锁,不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 -对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放;对于普通 SELECT 语句,不会加任何锁 +对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放(在事务中加的锁,会**在事务中止或提交时自动释放**);对于普通 SELECT 语句,不会加任何锁 锁的兼容性: @@ -6557,13 +6596,11 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 +**** -*** - - -#### 锁操作 +##### 锁操作 两个客户端操作 Client 1和 Client 2,简化为 C1、C2 @@ -6648,13 +6685,13 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB 锁操作6.png) - 由于C1、C2 操作的不同行,获取不同的行锁,所以都可以正常获取行锁 + 由于 C1、C2 操作的不同行,获取不同的行锁,所以都可以正常获取行锁 -​ -*** + +**** @@ -6662,17 +6699,17 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 ##### 间隙锁 -当使用范围条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据进行加锁,对于键值在条件范围内但并不存在的记录,叫做间隙(GAP), InnoDB 会对间隙进行加锁,就是间隙锁 +当使用范围条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据进行加锁,对于键值在条件范围内但并不存在的记录,叫做间隙(GAP), InnoDB 会对间隙进行加锁,就是间隙锁 * 唯一索引加锁只有在值存在时才是行锁,值不存在会变成间隙锁,所以范围查询时容易出现间隙锁 * 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁 -加锁的基本单位是 next-key lock,该锁是行锁和这条记录前面的 gap lock 的组合,就是行锁加间隙锁 +加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的组合,可以保护当前记录和前面的间隙 -* 加锁遵循前开后闭原则 -* 假设有索引值 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会同时对间隙 (10,11]、(11,13] 加锁 +* 加锁遵循左开右闭原则 +* 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会对 (10,11] 加锁 -间隙锁优点:RR 级别下间隙锁可以解决事务的**幻读问题**,通过对间隙加锁,防止读取过程中数据条目发生变化 +间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化 间隙锁危害:当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害 @@ -6703,25 +6740,31 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -*** +**** ##### 意向锁 -InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支持在不同粒度上的加锁操作,InnoDB 增加了意向锁(Intention Lock ) +InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支持在不同粒度上的加锁操作,InnoDB 增加了意向锁(Intention Lock) 意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁,意向锁分为两种: * 意向共享锁(IS):事务有意向对表中的某些行加共享锁 - * 意向排他锁(IX):事务有意向对表中的某些行加排他锁 -InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除全表扫描以外的任何请求,表级意向锁与行级锁的兼容性如下所示: +**IX,IS 是表级锁**,不会和行级的 X,S 锁发生冲突,意向锁是在加表级锁之前添加,为了在加表级锁时可以快速判断表中是否有记录被上锁,比如向一个表添加表级 X 锁的时: + +- 没有意向锁,则需要遍历整个表判断是否有锁定的记录 +- 有了意向锁,首先判断是否存在意向锁,然后判断该意向锁与即将添加的表级锁是否兼容即可,因为意向锁的存在代表有表级锁的存在或者即将有表级锁的存在 + +兼容性如下所示: ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-意向锁兼容性.png) -插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设某列有索引值2,6,只要两个事务插入位置不同,如事务 A 插入 3,事务 B 插入 4,那么就可以同时插入 +**插入意向锁** Insert Intention Lock 是在插入一行记录操作之前设置的一种间隙锁,是行级锁 + +插入意向锁释放了一种插入信号,即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设某列有索引值 2,6,只要两个事务插入位置不同,如事务 A 插入 3,事务 B 插入 4,那么就可以同时插入 @@ -6729,16 +6772,18 @@ InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除 -##### 死锁 +##### 自增锁 -当并发系统中不同线程出现循环资源依赖,线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁 +系统会自动给 AUTO_INCREMENT 修饰的列进行递增赋值,实现方式: -死锁情况:线程 A 修改了 id = 1 的数据,请求修改 id = 2 的数据,线程 B 修改了 id = 2 的数据,请求修改 id = 1 的数据,产生死锁 +* AUTO_INC 锁:表级锁,执行插入语句时会自动添加,在该语句执行完成后释放,并不是事务结束 +* 轻量级锁:为插入语句生成 AUTO_INCREMENT 修饰的列时获取该锁,生成以后释放掉,不需要等到插入语句执行完后释放 -解决策略: +系统变量 `innodb_autoinc_lock_mode` 控制采取哪种方式: -* 直接进入等待直到超时,超时时间可以通过参数 innodb_lock_wait_timeout 来设置,但是时间的设置不好控制,超时可能不是因为死锁,而是因为事务处理比较慢,所以一般不采取该方式 -* 主动死锁检测,发现死锁后**主动回滚死锁链条中的某一个事务**,让其他事务得以继续执行,将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑 +* 0:全部采用 AUTO_INC 锁 +* 1:全部采用轻量级锁 +* 2:混合使用,在插入记录的数量确定是采用轻量级锁,不确定时采用 AUTO_INC 锁 @@ -6746,8 +6791,57 @@ InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除 +##### 隐式锁 + +一般情况下 INSERT 语句是不需要在内存中生成锁结构的,会进行隐式的加锁,保护的是插入后的安全 + +注意:如果插入的间隙被其他事务加了间隙锁,此次插入会被阻塞,并在该间隙插入一个插入意向锁 + +* 聚簇索引:索引记录有 trx_id 隐藏列,表示最后改动该记录的事务 id,插入数据后事务 id 就是当前事务。其他事务想获取该记录的锁时会判断当前记录的事务 id 是否是活跃的,如果不是就可以正常加锁;如果是就创建一个 X 的锁结构,该锁的 is_waiting 是 false,为自己的事务创建一个锁结构,is_waiting 是 true(类似 Java 中的锁升级) +* 二级索引:获取数据页 Page Header 中的 PAGE_MAX_TRX_ID 属性,代表修改当前页面的最大的事务 ID,如果小于当前活跃的最小事务 id,就证明插入该数据的事务已经提交,否则就需要获取到主键值进行回表操作 + +隐式锁起到了延迟生成锁的效果,如果其他事务与隐式锁没有冲突,就可以避免锁结构的生成,节省了内存资源 + +INSERT 在两种情况下会生成锁结构: + +* 重复键:在插入主键或唯一二级索引时遇到重复的键值会报错,在报错前需要对对应的聚簇索引进行加锁 + * 隔离级别 <= Read Uncommitted,加 S 型 Record Lock + * 隔离级别 >= Repeatable Read,加 S 型 next_key 锁 + +* 外键检查:如果待插入的记录在父表中可以找到,会对父表的记录加 S 型 Record Lock。如果待插入的记录在父表中找不到 + * 隔离级别 <= Read Committed,不加锁 + * 隔离级别 >= Repeatable Read,加间隙锁 + + + + + +*** + + + #### 锁优化 +##### 优化锁 + +InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高,但是在整体并发处理能力方面要远远优于 MyISAM 的表锁,当系统并发量较高的时候,InnoDB 的整体性能远远好于 MyISAM + +但是使用不当可能会让 InnoDB 的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差 + +优化建议: + +- 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁 +- 合理设计索引,尽量缩小锁的范围 +- 尽可能减少索引条件及索引范围,避免间隙锁 +- 尽量控制事务大小,减少锁定资源量和时间长度 +- 尽可使用低级别事务隔离(需要业务层面满足需求) + + + +**** + + + ##### 锁升级 索引失效造成行锁升级为表锁,不通过索引检索数据,InnoDB 会将对表中的所有记录加锁,实际效果和**表锁**一样实际开发过程应避免出现索引失效的状况 @@ -6781,21 +6875,20 @@ InnoDB 存储引擎支持的是行级别的锁,因此意向锁不会阻塞除 -##### 优化锁 +##### 死锁 -InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高,但是在整体并发处理能力方面要远远优于 MyISAM 的表锁,当系统并发量较高的时候,InnoDB 的整体性能远远好于 MyISAM +不同事务由于互相持有对方需要的锁而导致事务都无法继续执行的情况称为死锁 -但是使用不当可能会让InnoDB 的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差 +死锁情况:线程 A 修改了 id = 1 的数据,请求修改 id = 2 的数据,线程 B 修改了 id = 2 的数据,请求修改 id = 1 的数据,产生死锁 -优化建议: +解决策略: -- 尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁 -- 合理设计索引,尽量缩小锁的范围 -- 尽可能减少索引条件及索引范围,避免间隙锁 -- 尽量控制事务大小,减少锁定资源量和时间长度 -- 尽可使用低级别事务隔离(需要业务层面满足需求) +* 直接进入等待直到超时,超时时间可以通过参数 innodb_lock_wait_timeout 来设置,但是时间的设置不好控制,超时可能不是因为死锁,而是因为事务处理比较慢,所以一般不采取该方式 +* 主动死锁检测,发现死锁后**主动回滚死锁链条中较小的一个事务**,让其他事务得以继续执行,将参数 `innodb_deadlock_detect` 设置为 on,表示开启该功能。较小的意思就是事务执行过程中插入、删除、更新的记录条数 +通过执行 `SHOW ENGINE INNODB STATUS` 可以查看最近发生的一次死循环,全局系统变量 `innodb_print_all_deadlocks` 设置为 on,就可以将每个死锁信息都记录在 MySQL 错误日志中 +死锁一般是行级锁,当表锁发生死锁时,会在事务中访问其他表时直接报错,破坏了持有并等待的死锁条件 @@ -6805,6 +6898,8 @@ InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面 #### 锁状态 +查看锁信息 + ```mysql SHOW STATUS LIKE 'innodb_row_lock%'; ``` @@ -6829,7 +6924,7 @@ SHOW STATUS LIKE 'innodb_row_lock%'; ```mysql SELECT * FROM information_schema.innodb_locks; #锁的概况 -SHOW ENGINE INNODB STATUS; #InnoDB整体状态,其中包括锁的情况 +SHOW ENGINE INNODB STATUS\G; #InnoDB整体状态,其中包括锁的情况 ``` ![](https://gitee.com/seazean/images/raw/master/DB/MySQL-InnoDB查看锁状态.png) diff --git a/Java.md b/Java.md index 35e4ddd..0d8a455 100644 --- a/Java.md +++ b/Java.md @@ -2324,7 +2324,7 @@ hashCode 的作用: * 浅拷贝 (shallowCopy):**对基本数据类型进行值传递,对引用数据类型只是复制了引用**,被复制对象属性的所有的引用仍然指向原来的对象,简而言之就是增加了一个指针指向原来对象的内存地址 - Java 中的复制方法基本都是浅克隆:Object.clone()、System.arraycopy()、Arrays.copyOf() + **Java 中的复制方法基本都是浅克隆**:Object.clone()、System.arraycopy()、Arrays.copyOf() * 深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误 @@ -2334,9 +2334,9 @@ Cloneable 接口是一个标识性接口,即该接口不包含任何方法( * Clone & Copy:`Student s = new Student` - `Student s1 = s`:只是 copy 了一下 reference,s 和 s1 指向内存中同一个 object,对对象的修改会影响对方 + `Student s1 = s`:只是 copy 了一下 reference,s 和 s1 指向内存中同一个 Object,对对象的修改会影响对方 - `Student s2 = s.clone()`:会生成一个新的Student对象,并且和s具有相同的属性值和方法 + `Student s2 = s.clone()`:会生成一个新的 Student 对象,并且和s具有相同的属性值和方法 * Shallow Clone & Deep Clone: @@ -18365,7 +18365,7 @@ Java中 的 Object 类中提供了 `clone()` 方法来实现浅克隆,实现 C } ``` - stu1 对象和 stu2 对象是同一个对象,将 stu2 对象中 name 属性改为李四,两个Citation对象中都是李四,这就是浅克隆的效果 + stu1 对象和 stu2 对象是同一个对象,将 stu2 对象中 name 属性改为李四,两个 Citation 对象中都是李四,这就是浅克隆的效果 * 序列化实现深克隆,或者重写克隆方法: From e60e7ffdef0f89a3dc85dde902fbefca7bb1ad6f Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 21 Dec 2021 00:26:39 +0800 Subject: [PATCH 049/122] Update Java Notes --- DB.md | 85 +++++++++++++++++++++++++++++++++++--------------------- Frame.md | 14 +++++----- 2 files changed, 60 insertions(+), 39 deletions(-) diff --git a/DB.md b/DB.md index 894b004..c835ac6 100644 --- a/DB.md +++ b/DB.md @@ -5822,9 +5822,9 @@ roll_pointer 是一个指针,**指向记录对应的 undo log 日志**,一 #### 回滚日志 -undo log 是采用段(segment)的方式来记录,每个 undo 操作在记录的时候占用一个 undo log segment +undo log 是采用段的方式来记录,Rollback Segement 称为回滚段,本质上就是一个类型是 Rollback Segement Header 的页面 -Rollback Segement 称为回滚段,每个回滚段中有 1024 个 undo slot +每个回滚段中有 1024 个 undo slot,每个 slot 存放 undo 链表页面的头节点页号,每个链表对应一个叫 undo log segment 的段 * 在以前老版本,只支持 1 个 Rollback Segement,只能记录 1024 个 undo log segment * MySQL5.5 开始支持 128 个 Rollback Segement,支持 128*1024 个 undo 操作 @@ -5832,9 +5832,11 @@ Rollback Segement 称为回滚段,每个回滚段中有 1024 个 undo slot 工作流程: * 事务执行前需要到系统表空间第 5 号页面中分配一个回滚段(页),获取一个 Rollback Segement Header 页面的地址 -* 回滚段页面有 1024 个 undo slot,每个 slot 存放 undo 链表页面的头节点页号。首先去回滚段的两个 cached 链表看是否有缓存的 slot,缓存中没有就在回滚段中找一个可用的 slot -* 缓存中获取的 slot 对应的 Undo Log Segment 已经分配了,需要重新分配,然后从 Undo Log Segment 中申请一个页面作为日志链表的头节点,并填入对应的 slot 中 -* 开始记录 +* 回滚段页面有 1024 个 undo slot,首先去回滚段的两个 cached 链表获取缓存的 slot,缓存中没有就在回滚段页面中找一个可用的 undo slot 分配给当前事务 +* 如果是缓存中获取的 slot,则该 slot 对应的 undo log segment 已经分配了,需要重新分配,然后从 undo log segment 中申请一个页面作为日志链表的头节点,并填入对应的 slot 中 +* 每个事务 undo 日志在记录的时候占用两个 undo 页面的组成链表,分别为 insert undo 链表和 update undo 链表,链表的头节点页面为 first undo page 会包含一些管理信息,其他页面为 normal undo page + + 说明:事务执行过程的临时表也需要两个 undo 链表,不和普通表共用,这些链表并不是事务开始就分配,而是按需分配 @@ -6114,17 +6116,28 @@ Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机 ##### 日志缓冲 -服务器启动时会向操作系统申请一片连续内存空间作为 redo log buffer(重做日志缓冲区),可以通过 `innodb_log_buffer_size` 系统变量指定 log buffer 的大小,默认是 16MB - -补充知识:MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction(MTR),比如在 B+ 树上插入一条数据就算一个 MTR +服务器启动时会向操作系统申请一片连续内存空间作为 redo log buffer(重做日志缓冲区),可以通过 `innodb_log_buffer_size` 系统变量指定 redo log buffer 的大小,默认是 16MB log buffer 被划分为若干 redo log block(块,类似数据页的概念),每个默认大小 512 字节,每个 block 由 12 字节的 log block head、496 字节的 log block body、4 字节的 log block trailer 组成 * 当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作,写入 log buffer 的过程是顺序写入的(先写入前面的 block,写满后继续写下一个) -* log buffer 中有一个指针 buf_free,来标识该位置之前都是填满的 block,该位置之后都是空闲区域(碰撞指针) -* 一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,所以并不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入 log buffer**,在进行数据恢复时也把这一组 redo log 当作一个不可分割的整体处理 +* log buffer 中有一个指针 buf_free,来标识该位置之前都是填满的 block,该位置之后都是空闲区域(**碰撞指针**) + +MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction(MTR),比如在 B+ 树上插入一条数据就算一个 MTR + +* 一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,在进行数据恢复时也把一组 redo log 当作一个不可分割的整体处理 + +* 所以不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后**将一组 redo 日志写入 log buffer** + + + +*** -redo log 也需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快: + + +##### 日志刷盘 + +redo log 需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快,原因: * 刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是尾部追加操作,属于顺序 IO * 刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入,而 redo log 中只包含真正需要写入的部分,减少无效 IO @@ -6139,31 +6152,39 @@ InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化到 * 服务器关闭时 * checkpoint 时(下小节详解) +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 磁盘空间不变,所以 redo 磁盘中的日志文件是被**循环使用**的,采用循环写数据的方式,写完尾部重新写头部,所以要确保头部 log 对应的修改已经持久化到磁盘 -##### 磁盘文件 -redo 存储在磁盘中的日志文件是被**循环使用**的,redo 日志文件组中每个文件大小一样格式一样,组成结构:前 2048 个字节(前 4 个 block)用来存储一些管理信息,以后的存储 log buffer 中的 block 镜像 -注意:block 并不代表一组 redo log,一组日志可能占用不到一个 block 或者几个 block,依赖 MTR 的大小 +*** -磁盘存储 redo log 的文件目录通过 `innodb_log_group_home_dir` 指定,默认是当前数据目录,文件大小: -* 通过两个参数调节:`innodb_log_file_size` 文件大小默认 48M,`innodb_log_files_in_group` 文件个数默认 2 最大 100 -* 服务器启动后磁盘空间不变,所以采用循环写数据的方式,写完尾部重新写头部,所以要确保头部 log 对应的修改已经持久化到磁盘 + +##### 日志序号 lsn (log sequence number) 代表已经写入的 redo 日志量、flushed_to_disk_lsn 指刷新到磁盘中的 redo 日志量,两者都是**全局变量**,如果两者的值相同,说明 log buffer 中所有的 redo 日志都已经持久化到磁盘 +工作过程:写入 log buffer 数据时,buf_free 会进行偏移,偏移量就会加到 lsn 上 + MTR 的执行过程中修改过的页对应的控制块会加到 Buffer Pool 的 flush 链表中,链表中脏页是按照第一次修改的时间进行排序的(头插),控制块中有两个指针用来记录脏页被修改的时间: * 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 @@ -6192,7 +6213,7 @@ SHOW ENGINE INNODB STATUS\G 问题:系统崩溃前没有提交的事务的 redo log 可能已经刷盘,这些数据可能在重启后也会恢复 -解决:通过 undo log 在服务器重启时将未提交的事务回滚掉,定位到 128 个回滚段,遍历 slot,获取 undo 链表首节点页面的 Undo Segement Header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,如果是活跃的就全部回滚 +解决:通过 undo log 在服务器重启时将未提交的事务回滚掉,定位到 128 个回滚段,遍历 slot,获取 undo 链表首节点页面的 undo segement header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,如果是活跃的就全部回滚 @@ -10454,7 +10475,7 @@ RDB三种启动方式对比: #### 总结 -* RDB特殊启动形式的指令(客户端输入) +* RDB 特殊启动形式的指令(客户端输入) * 服务器运行过程中重启 @@ -10472,17 +10493,17 @@ RDB三种启动方式对比: * 全量复制:主从复制部分详解 -* RDB优点: +* RDB 优点: - RDB 是一个紧凑压缩的二进制文件,存储效率较高,但存储数据量较大时,存储效率较低 - RDB 内部存储的是 redis 在某个时间点的数据快照,非常**适合用于数据备份,全量复制**等场景 - RDB 恢复数据的速度要比 AOF 快很多,因为是快照,直接恢复 - - 应用:服务器中每 X 小时执行 bgsave 备份,并将 RDB 文件拷贝到远程机器中,用于灾难恢复 -* RDB缺点: +* RDB 缺点: - bgsave 指令每次运行要执行 fork 操作创建子进程,会牺牲一些性能 - RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有丢失数据的可能性,最后一次持久化后的数据可能丢失 - Redis 的众多版本中未进行 RDB 文件格式的版本统一,可能出现各版本之间数据格式无法兼容 +* 应用:服务器中每 X 小时执行 bgsave 备份,并将 RDB 文件拷贝到远程机器中,用于灾难恢复 @@ -10539,7 +10560,7 @@ AOF 持久化数据的三种策略(appendfsync): * 同步硬盘操作依赖于系统调度机制,比如缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失 * fsync 针对单个文件操作(比如 AOF 文件)做强制硬盘同步,fsync 将阻塞到写入硬盘完成后返回,保证了数据持久化 -异常恢复:AOF 文件损坏,通过 redis-check-aof--fix appendonly.aof 进行恢复,重启 redis,然后重新加载 +异常恢复:AOF 文件损坏,通过 redis-check-aof--fix appendonly.aof 进行恢复,重启 Redis,然后重新加载 @@ -10678,7 +10699,7 @@ AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢 - 如不能承受数分钟以内的数据丢失,对业务数据非常敏感,选用 AOF - 如能承受数分钟以内的数据丢失,且追求大数据集的恢复速度,选用 RDB - 灾难恢复选用 RDB -- 双保险策略,同时开启 RDB和 AOF,重启后 Redis 优先使用 AOF 来恢复数据,降低丢失数据的量 +- 双保险策略,同时开启 RDB 和 AOF,重启后 Redis 优先使用 AOF 来恢复数据,降低丢失数据的量 - 不建议单独用 AOF,因为可能会出现 Bug,如果只是做纯内存缓存,可以都不用 @@ -10731,8 +10752,8 @@ fpid 的值在父子进程中不同:进程形成了链表,父进程的 fpid int main () { pid_t fpid; // fpid表示fork函数返回的值 - int count=0; - fpid=fork(); + int count = 0; + fpid = fork(); if (fpid < 0) printf("error in fork!"); else if (fpid == 0) { @@ -10761,16 +10782,16 @@ int main () #include int main(void) { - int i=0; + int i = 0; // ppid 指当前进程的父进程pid // pid 指当前进程的pid, // fpid 指fork返回给当前进程的值,在这可以表示子进程 - for(i=0; i<2; i++){ + for(i = 0; i < 2; i++){ pid_t fpid = fork(); if(fpid == 0) - printf("%d child %4d %4d %4d/n",i,getppid(),getpid(),fpid); + printf("%d child %4d %4d %4d/n",i, getppid(), getpid(), fpid); else - printf("%d parent %4d %4d %4d/n",i,getppid(),getpid(),fpid); + printf("%d parent %4d %4d %4d/n",i, getppid(), getpid(),fpid); } return 0; } diff --git a/Frame.md b/Frame.md index c8e1908..8947b3a 100644 --- a/Frame.md +++ b/Frame.md @@ -2505,7 +2505,7 @@ Pipeline 的存在,需要将 ByteBuf 传递给下一个 ChannelHandler,如 +--------+-------------------------------------------------+----------------+ ``` -解决方法:通过调整系统的接受缓冲区的滑动窗口和 Netty 的接受缓冲区保证每条包只含有一条数据,滑动窗口大小,仅决定了 Netty 读取的**最小单位**,实际每次读取的一般是它的整数倍 +解决方法:通过调整系统的接受缓冲区的滑动窗口和 Netty 的接受缓冲区保证每条包只含有一条数据,滑动窗口的大小仅决定了 Netty 读取的**最小单位**,实际每次读取的一般是它的整数倍 @@ -2540,13 +2540,13 @@ public class HelloWorldClient { #### 固定长度 -服务器端加入定长解码器,每一条消息采用固定长度,缺点浪费空间 +服务器端加入定长解码器,每一条消息采用固定长度。如果是半包消息,会缓存半包消息并等待下个包到达之后进行拼包合并,直到读取一个完整的消息包;如果是粘包消息,空余的位置会进行补 0,会浪费空间 ```java serverBootstrap.childHandler(new ChannelInitializer() { @Override protected void initChannel(SocketChannel ch) throws Exception { - ch.pipeline().addLast(new FixedLengthFrameDecoder(8)); + ch.pipeline().addLast(new FixedLengthFrameDecoder(10)); // LoggingHandler 用来打印消息 ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG)); } @@ -2692,7 +2692,7 @@ public class LengthFieldDecoderDemo { #### HTTP协议 -访问URL:http://localhost:8080/ +访问 URL:http://localhost:8080/ ```java public class HttpDemo { @@ -4537,9 +4537,9 @@ RocketMQ 的工作流程: - 启动 NameServer 监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心 - 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 启动时先跟 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 来拉取消息并进行消费 @@ -4581,7 +4581,7 @@ RocketMQ 的工作流程: #### 通信原理 -==todo:后期学习了源码会进行扩充,现在暂时 copy 官方文档== +==todo:后期对 Netty 有了更深的认知后会进行扩充,现在暂时 copy 官方文档== 在 RocketMQ 消息队列中支持通信的方式主要有同步(sync)、异步(async)、单向(oneway)三种,其中单向通信模式相对简单,一般用在发送心跳包场景下,无需关注其 Response From 2bfeaa8c14fe64a2c4ca17d4f6cf3bfcd7c982b5 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 25 Dec 2021 01:06:00 +0800 Subject: [PATCH 050/122] Update Java Notes --- DB.md | 5 +- Frame.md | 639 ++++++++++++++++++++++++++++++++++++++++++++++++------- Java.md | 8 +- 3 files changed, 568 insertions(+), 84 deletions(-) diff --git a/DB.md b/DB.md index c835ac6..ebf0128 100644 --- a/DB.md +++ b/DB.md @@ -4,7 +4,7 @@ ### 数据库 -数据库:DataBase,简称 DB,用于存储和管理数据的仓库,它的存储空间很大,可以存放百万上亿条数据。 +数据库:DataBase,简称 DB,存储和管理数据的仓库 数据库的优势: @@ -22,13 +22,12 @@ - 数据表 - 数据库最重要的组成部分之一 - - 它由纵向的列和横向的行组成(类似 excel 表格) + - 由纵向的列和横向的行组成(类似 excel 表格) - 可以指定列名、数据类型、约束等 - 一个表中可以存储多条数据 - 数据:想要永久化存储的数据 - diff --git a/Frame.md b/Frame.md index 8947b3a..4dc7ebc 100644 --- a/Frame.md +++ b/Frame.md @@ -42,6 +42,10 @@ pom.xml:Maven 需要一个 pom.xml 文件,Maven 通过加载这个配置文 +参考视频:https://www.bilibili.com/video/BV1Ah411S7ZE + + + *** @@ -1757,7 +1761,7 @@ Netty 主要基于主从 Reactors 多线程模型做了一定的改进,Netty #### 基本介绍 -事件循环对象 EventLoop,本质是一个单线程执行器(同时维护了一个 selector),里面有 run 方法处理 Channel 上源源不断的 IO 事件 +事件循环对象 EventLoop,本质是一个单线程执行器(同时维护了一个 selector),有 run 方法处理 Channel 上源源不断的 IO 事件 事件循环组 EventLoopGroup 是一组 EventLoop,Channel 会调用 Boss EventLoopGroup 的 register 方法来绑定其中一个 Worker 的 EventLoop,后续这个 Channel 上的 IO 事件都由此 EventLoop 来处理,保证了事件处理时的线程安全 @@ -1847,7 +1851,7 @@ static void invokeChannelRead(final AbstractChannelHandlerContext next, Object m ### Channel -#### 基本介绍 +#### 连接操作 Channel 类 API: @@ -2112,7 +2116,6 @@ public static void main(String[] args) { }) .bind(8080); } - ``` 服务器端依次打印:1 2 4 3 ,所以**入站是按照 addLast 的顺序执行的,出站是按照 addLast 的逆序执行** @@ -2690,7 +2693,7 @@ public class LengthFieldDecoderDemo { ### 协议设计 -#### HTTP协议 +#### HTTP 访问 URL:http://localhost:8080/ @@ -2750,7 +2753,7 @@ public class HttpDemo { -#### 自定义协议 +#### 自定义 处理器代码: @@ -4499,7 +4502,7 @@ NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态 NameServer 主要包括两个功能: -* Broker 管理,NameServer 接受 Broker 集群的注册信息并保存下来作为路由信息的基本数据,提供**心跳检测**检查 Broker 活性 +* Broker 路由管理,NameServer 接受 Broker 集群的注册信息,并保存下来作为路由信息的基本数据,提供**心跳检测机制**检查 Broker 活性(每 10 秒) * 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 NameServer 特点: @@ -4543,77 +4546,6 @@ RocketMQ 的工作流程: -**** - - - -#### 协议设计 - -在 Client 和 Server 之间完成一次消息发送时,需要对发送的消息进行一个协议约定,所以自定义 RocketMQ 的消息协议。为了高效地在网络中传输消息和对收到的消息读取,就需要对消息进行编解码。在 RocketMQ 中,RemotingCommand 这个类在消息传输过程中对所有数据内容的封装,不但包含了所有的数据结构,还包含了编码解码操作 - -| Header字段 | 类型 | Request 说明 | Response 说明 | -| ---------- | ----------------------- | ------------------------------------------------------------ | ------------------------------------------- | -| code | int | 请求操作码,应答方根据不同的请求码进行不同的处理 | 应答响应码,0 表示成功,非 0 则表示各种错误 | -| language | LanguageCode | 请求方实现的语言 | 应答方实现的语言 | -| version | int | 请求方程序的版本 | 应答方程序的版本 | -| opaque | int | 相当于 requestId,在同一个连接上的不同请求标识码,与响应消息中的相对应 | 应答不做修改直接返回 | -| flag | int | 区分是普通 RPC 还是 onewayRPC 的标志 | 区分是普通 RPC 还是 onewayRPC的标志 | -| remark | String | 传输自定义文本信息 | 传输自定义文本信息 | -| extFields | HashMap | 请求自定义扩展信息 | 响应自定义扩展信息 | - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息协议.png) - -传输内容主要可以分为以下四部分: - -* 消息长度:总长度,四个字节存储,占用一个 int 类型 - -* 序列化类型&消息头长度:同样占用一个 int 类型,第一个字节表示序列化类型,后面三个字节表示消息头长度 - -* 消息头数据:经过序列化后的消息头数据 - -* 消息主体数据:消息主体的二进制字节数据内容 - - - -***** - - - -#### 通信原理 - -==todo:后期对 Netty 有了更深的认知后会进行扩充,现在暂时 copy 官方文档== - -在 RocketMQ 消息队列中支持通信的方式主要有同步(sync)、异步(async)、单向(oneway)三种,其中单向通信模式相对简单,一般用在发送心跳包场景下,无需关注其 Response - -RocketMQ 的异步通信流程: - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-异步通信流程.png) - -RocketMQ 的 RPC 通信采用 Netty 组件作为底层通信库,同样也遵循了 Reactor 多线程模型,同时又在这之上做了一些扩展和优化 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Reactor设计.png) - -RocketMQ 基于 NettyRemotingServer 的 Reactor 多线程模型: - -* 一个 Reactor 主线程(eventLoopGroupBoss)负责监听 TCP 网络连接请求,建立好连接,创建 SocketChannel,并注册到 selector 上。RocketMQ 会自动根据 OS 的类型选择 NIO 和 Epoll,也可以通过参数配置),然后监听真正的网络数据 - -* 拿到网络数据交给 Worker 线程池(eventLoopGroupSelector,默认设置为 3),在真正执行业务逻辑之前需要进行 SSL 验证、编解码、空闲检查、网络连接管理,这些工作交给 defaultEventExecutorGroup(默认设置为 8)去做 -* 处理业务操作放在业务线程池中执行,根据 RomotingCommand 的业务请求码 code 去 processorTable 这个本地缓存变量中找到对应的 processor,封装成 task 任务提交给对应的业务 processor 处理线程池来执行(sendMessageExecutor,以发送消息为例) -* 从入口到业务逻辑的几个步骤中线程池一直再增加,这跟每一步逻辑复杂性相关,越复杂,需要的并发通道越宽 - -| 线程数 | 线程名 | 线程具体说明 | -| ------ | ------------------------------ | ------------------------- | -| 1 | NettyBoss_%d | Reactor 主线程 | -| N | NettyServerEPOLLSelector_%d_%d | Reactor 线程池 | -| M1 | NettyServerCodecThread_%d | Worker 线程池 | -| M2 | RemotingExecutorThread_%d | 业务 processor 处理线程池 | - - - -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#2-%E9%80%9A%E4%BF%A1%E6%9C%BA%E5%88%B6 - - - *** @@ -5215,3 +5147,556 @@ public class MessageListenerImpl implements MessageListener { ## 源码分析 + +### 服务启动 + +#### 启动方法 + +NamesrvStartup 类中有 Namesrv 服务的启动方法: + +```java +public static void main(String[] args) { + // 如果启动时 使用 -c -p 设置参数了,这些参数存储在 args 中 + main0(args); +} + +public static NamesrvController main0(String[] args) { + try { + // 创建 namesrv 控制器,用来初始化 namesrv 启动 namesrv 关闭 namesrv + NamesrvController controller = createNamesrvController(args); + // 启动 controller + start(controller); + return controller; + } catch (Throwable e) { + // 出现异常,停止系统 + System.exit(-1); + } + return null; +} +``` + +NamesrvStartup#createNamesrvController:读取配置信息,初始化 Namesrv 控制器 + +* `ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options),..)`:解析启动时的参数信息 + +* `namesrvConfig = new NamesrvConfig()`:创建 Namesrv 配置对象 + + * `private String rocketmqHome`:获取 ROCKETMQ_HOME 值 + * `private boolean orderMessageEnable = false`:顺序消息功能是否开启 + +* `nettyServerConfig = new NettyServerConfig()`:Netty 的服务器配置对象 + + ```java + public class NettyServerConfig implements Cloneable { + // 服务端启动时监听的端口号 + private int listenPort = 8888; + // 【业务线程池】 线程数量 + private int serverWorkerThreads = 8; + // 根据该值创建 remotingServer 内部的一个 publicExecutor + private int serverCallbackExecutorThreads = 0; + // netty 【worker】线程数 + private int serverSelectorThreads = 3; + // 【单向访问】时的并发限制 + private int serverOnewaySemaphoreValue = 256; + // 【异步访问】时的并发限制 + private int serverAsyncSemaphoreValue = 64; + // channel 最大的空闲存活时间 默认是 2min + private int serverChannelMaxIdleTimeSeconds = 120; + // 发送缓冲区大小 65535 + private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize; + // 接收缓冲区大小 65535 + private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 是否启用 netty 内存池 默认开启 + private boolean serverPooledByteBufAllocatorEnable = true; + + // 默认 linux 会启用 【epoll】 + private boolean useEpollNativeSelector = false; + } + ``` + +* `nettyServerConfig.setListenPort(9876)`:Namesrv 服务器的监听端口设置为 9876 + +* `if (commandLine.hasOption('c'))`:读取命令行 -c 的参数值 + + `in = new BufferedInputStream(new FileInputStream(file))`:读取指定目录的配置文件 + + `properties.load(in)`:将配置文件信息加载到 properties 对象,相关属性会复写到 Namesrv 配置和 Netty 配置对象 + + `namesrvConfig.setConfigStorePath(file)`:将配置文件的路径保存到配置保存字段 + +* `if (null == namesrvConfig.getRocketmqHome())`:检查 ROCKETMQ_HOME 配置是否是空,是空就报错 + +* `lc = (LoggerContext) LoggerFactory.getILoggerFactory()`:创建日志对象 + +* `controller = new NamesrvController(namesrvConfig, nettyServerConfig)`:**创建 Namesrv 控制器** + +NamesrvStartup#start:启动 Namesrv 控制器 + +* `boolean initResult = controller.initialize()`:初始化方法 + +* ` Runtime.getRuntime().addShutdownHook(new ShutdownHookThread())`:JVM HOOK 平滑关闭的逻辑, 当 JVM 被关闭时,主动调用 controller.shutdown() 方法,让服务器平滑关机 +* `controller.start()`:启动服务器 + + + +**** + + + + + +#### 控制器类 + +NamesrvController 用来初始化和启动 Namesrv 服务器 + +* 成员变量: + + ```java + private final ScheduledExecutorService scheduledExecutorService; // 调度线程池,用来执行定时任务 + private final RouteInfoManager routeInfoManager; // 管理【路由信息】的对象 + private RemotingServer remotingServer; // 【网络层】封装对象 + private ExecutorService remotingExecutor; // 业务线程池,用来 work + private BrokerHousekeepingService brokerHousekeepingService; // 用于监听 channel 状态 + private ExecutorService remotingExecutor; // 业务线程池 + ``` + +* 初始化: + + ```java + public boolean initialize() { + // 加载本地kv配置(我还不明白 kv 配置是啥) + this.kvConfigManager.load(); + // 创建网络服务器对象,【将 netty 的配置和监听器传入】 + // 监听器监听 channel 状态的改变,会向事件队列发起事件,最后交由 service 处理 + this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService); + // 【创建业务线程池,默认线程数 8】 + // netty 线程解析报文成 RemotingCommand 对象,然后将该对象交给业务线程池再继续处理。 + this.remotingExecutor = Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads().); + + // 注册协议处理器(缺省协议处理器),处理器是 DefaultRequestProcessor,线程使用的是刚创建的业务的线程池 + this.registerProcessor(); + + // 定时任务1:每 10 秒钟检查 broker 存活状态,将 IDLE 状态的 broker 移除。【心跳机制】 + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + // 将两小时没有活动的 broker 关闭,通过 next.getKey() 获取 broker 的地址 + // 然后【关闭服务器与broker物理节点的 channel】 + NamesrvController.this.routeInfoManager.scanNotActiveBroker(); + } + }, 5, 10, TimeUnit.SECONDS); + + // 定时任务2:每 10 分钟打印一遍 kv 配置。 + this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + NamesrvController.this.kvConfigManager.printAllPeriodically(); + } + }, 1, 10, TimeUnit.MINUTES); + + return true; + } + ``` + +* 启动方法: + + ```java + public void start() throws Exception { + // 服务器网络层启动。 + this.remotingServer.start(); + + if (this.fileWatchService != null) { + this.fileWatchService.start(); + } + } + ``` + + + + + +*** + + + +#### 网络服务 + +##### 通信原理 + +RocketMQ 的 RPC 通信采用 Netty 组件作为底层通信库,同样也遵循了 Reactor 多线程模型,NettyRemotingServer 类负责框架的通信服务,同时又在这之上做了一些扩展和优化 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-Reactor设计.png) + +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,以发送消息为例) +* 从入口到业务逻辑的几个步骤中线程池一直再增加,这跟每一步逻辑复杂性相关,越复杂,需要的并发通道越宽 + +| 线程数 | 线程名 | 线程具体说明 | +| ------ | ------------------------------ | ------------------------- | +| 1 | NettyBoss_%d | Reactor 主线程 | +| N | NettyServerEPOLLSelector_%d_%d | Reactor 线程池 | +| M1 | NettyServerCodecThread_%d | Worker 线程池 | +| M2 | RemotingExecutorThread_%d | 业务 processor 处理线程池 | + +RocketMQ 的异步通信流程: + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-异步通信流程.png) + + + +==todo:后期对 Netty 有了更深的认知后会进行扩充,现在暂时 copy 官方文档== + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md#2-%E9%80%9A%E4%BF%A1%E6%9C%BA%E5%88%B6 + + + +*** + + + +##### 成员属性 + +成员变量: + +* 服务器相关属性: + + ```java + private final ServerBootstrap serverBootstrap; // netty 服务端启动对象 + private final EventLoopGroup eventLoopGroupSelector; // netty worker 组线程池,【默认 3 个线程】 + private final EventLoopGroup eventLoopGroupBoss; // netty boss 组线程池,【一般是 1 个线程】 + private final NettyServerConfig nettyServerConfig; // netty 服务端网络配置 + private int port = 0; // 服务器绑定的端口 + ``` + +* 公共线程池:注册处理器时如果未指定线程池,则业务处理使用公共线程池,线程数量默认是 4 + + ```java + private final ExecutorService publicExecutor; + ``` + +* 事件监听器:Nameserver 使用 BrokerHouseKeepingService,Broker 使用 ClientHouseKeepingService + + ```java + private final ChannelEventListener channelEventListener; + ``` + +* 事件处理线程池:默认是 8 + + ```java + private DefaultEventExecutorGroup defaultEventExecutorGroup; + ``` + +* 定时器:执行循环任务,并且将定时器线程设置为守护线程 + + ```java + private final Timer timer = new Timer("ServerHouseKeepingService", true); + ``` + +* 处理器:多个 Channel 共享的处理器 Handler,多个通道使用同一个对象 + + +构造方法: + +* 无监听器构造: + + ```java + public NettyRemotingServer(final NettyServerConfig nettyServerConfig) { + this(nettyServerConfig, null); + } + ``` + +* 有参构造方法: + + ```java + public NettyRemotingServer(final NettyServerConfig nettyServerConfig, + final ChannelEventListener channelEventListener) { + // 服务器对客户端主动发起请求时并发限制。【单向请求和异步请求】的并发限制 + super(nettyServerConfig.getServerOnewaySemaphoreValue(), nettyServerConfig.getServerAsyncSemaphoreValue()); + // Netty 的启动器,负责组装 netty 组件 + this.serverBootstrap = new ServerBootstrap(); + // 成员变量的赋值 + this.nettyServerConfig = nettyServerConfig; + this.channelEventListener = channelEventListener; + + // 公共线程池的线程数量,默认给的0,这里最终修改为4. + int publicThreadNums = nettyServerConfig.getServerCallbackExecutorThreads(); + if (publicThreadNums <= 0) { + publicThreadNums = 4; + } + // 创建公共线程池,指定线程工厂,设置线程名称前缀:NettyServerPublicExecutor_[数字] + this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums, new ThreadFactory(){.}); + + // 创建两个 netty 的线程组,一个是boss组,一个是worker组,【linux 系统默认启用 epoll】 + if (useEpoll()) {...} else {...} + // SSL 相关 + loadSslContext(); + } + ``` + + + + + +*** + + + +##### 启动方法 + +核心方法的解析: + +* start():启动方法 + + ```java + public void start() { + // 向 channel pipeline 添加 handler,网络事件传播到当前 handler 时,【线程分配给 handler 处理事件】 + this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(...); + + // 创建通用共享的处理器 handler,【非常重要的 NettyServerHandler】 + prepareSharableHandlers(); + + ServerBootstrap childHandler = + // 配置工作组 boss(数量1) 和 worker(数量3) 组 + this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector) + // 设置服务端 ServerSocketChannel 类型, Linux 用 epoll + .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class) + // 设置服务端 channel 选项 + .option(ChannelOption.SO_BACKLOG, 1024) + // 客户端 channel 选项 + .childOption(ChannelOption.TCP_NODELAY, true) + // 设置服务器端口 + .localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort())) + // 向 channel pipeline 添加了很多 handler,包括 NettyServerHandler + .childHandler(new ChannelInitializer() {}); + + // 客户端开启 内存池,使用的内存池是 PooledByteBufAllocator.DEFAULT + if (nettyServerConfig.isServerPooledByteBufAllocatorEnable()) { + childHandler.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); + } + + try { + // 同步等待建立连接,并绑定端口。 + ChannelFuture sync = this.serverBootstrap.bind().sync(); + InetSocketAddress addr = (InetSocketAddress) sync.channel().localAddress(); + // 将服务器成功绑定的端口号赋值给字段 port。 + this.port = addr.getPort(); + } catch (InterruptedException e1) {} + + // housekeepingService 不为空,则创建【网络异常事件处理器】 + if (this.channelEventListener != null) { + // 线程一直轮询 nettyEventExecutor 状态,根据 CONNECT,CLOSE,IDLE,EXCEPTION 四种事件类型 + // CONNECT 不做操作,其余都是回调 onChannelDestroy 关闭服务器与 Broker 物理节点的 Channel + this.nettyEventExecutor.start(); + } + + // 提交定时任务,每一秒 执行一次。扫描 responseTable 表,将过期的 请求 移除。 + this.timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + NettyRemotingServer.this.scanResponseTable(); + } + }, 1000 * 3, 1000); + } + ``` + +* registerProcessor():注册业务处理器 + + ```java + public void registerProcessor(int requestCode, NettyRequestProcessor processor, ExecutorService executor) { + ExecutorService executorThis = executor; + if (null == executor) { + // 未指定线程池资源,将公共线程池赋值 + executorThis = this.publicExecutor; + } + // pair 对象,第一个参数代表的是处理器, 第二个参数是线程池,默认是公共的线程池 + Pair pair = new Pair(processor, executorThis); + + // key 是请求码,value 是 Pair 对象 + this.processorTable.put(requestCode, pair); + } + ``` + +* getProcessorPair():**根据请求码获取对应的处理器和线程池资源** + + ```java + public Pair getProcessorPair(int requestCode) { + return processorTable.get(requestCode); + } + ``` + + + +*** + + + +##### 请求方法 + +在 RocketMQ 消息队列中支持通信的方式主要有同步(sync)、异步(async)、单向(oneway)三种,其中单向通信模式相对简单,一般用在发送心跳包场景下,无需关注其 Response + +服务器主动向客户端发起请求时,使用三种方法 + +* invokeSync(): 同步调用,服务器需要阻塞等待调用的返回结果 + * `int opaque = request.getOpaque()`:获取请求 ID(与请求码不同) + * `responseFuture = new ResponseFuture(...)`:创建响应对象,将请求 ID、通道、超时时间传入,没有回调函数和 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 + * `responseCommand = responseFuture.waitResponse(timeoutMillis)`:当前线程设置超时时间挂起,**同步等待响应** + * `if (null == responseCommand)`:超时或者出现异常,直接报错 + * `return responseCommand`:返回响应 Command 信息 +* invokeAsync():异步调用,有回调对象,无返回值 + * `boolean acquired = this.semaphoreAsync.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS)`:获取信号量的许可证,信号量用来**限制异步请求**的数量 + * `if (acquired)`:许可证获取失败说明并发较高,会抛出异常 + * `once = new SemaphoreReleaseOnlyOnce(this.semaphoreAsync)`:Once 对象封装了释放信号量的操作 + * `costTime = System.currentTimeMillis() - beginStartTime`:计算一下耗费的时间,超时不再发起请求 + * `responseFuture = new ResponseFuture()`:创建响应对象,包装了回调函数和 Once 对象 + * `this.responseTable.put(opaque, responseFuture)`:加入到响应映射表中,key 为请求 ID + * `channel.writeAndFlush(request).addListener(...)`:写刷数据 + * `if (f.isSuccess())`:写刷成功,设置 responseFuture 发生状态为 true + * `requestFail(opaque)`:写入失败,使用 publicExecutor **公共线程池异步执行回调对象的函数** + * `responseFuture.release()`:出现异常会释放信号量 + +* invokeOneway():单向调用,不关注响应结果 + * `request.markOnewayRPC()`:设置单向标记,对端检查标记可知该请是单向请求 + * `boolean acquired = this.semaphoreOneway.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS)`:获取信号量的许可证,信号量用来**限制单向请求**的数量 + + + + + +*** + + + +#### 处理器类 + +##### 协议设计 + +在 Client 和 Server 之间完成一次消息发送时,需要对发送的消息进行一个协议约定,所以自定义 RocketMQ 的消息协议。在 RocketMQ 中,为了高效地在网络中传输消息和对收到的消息读取,就需要对消息进行编解码,RemotingCommand 这个类在消息传输过程中对所有数据内容的封装,不但包含了所有的数据结构,还包含了编码解码操作 + +| Header字段 | 类型 | Request 说明 | Response 说明 | +| ---------- | ----------------------- | ------------------------------------------------------------ | ------------------------------------------- | +| code | int | 请求操作码,应答方根据不同的请求码进行不同的处理 | 应答响应码,0 表示成功,非 0 则表示各种错误 | +| language | LanguageCode | 请求方实现的语言 | 应答方实现的语言 | +| version | int | 请求方程序的版本 | 应答方程序的版本 | +| opaque | int | 相当于 requestId,在同一个连接上的不同请求标识码,与响应消息中的相对应 | 应答不做修改直接返回 | +| flag | int | 区分是普通 RPC 还是 onewayRPC 的标志 | 区分是普通 RPC 还是 onewayRPC的标志 | +| remark | String | 传输自定义文本信息 | 传输自定义文本信息 | +| extFields | HashMap | 请求自定义扩展信息 | 响应自定义扩展信息 | + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息协议.png) + +传输内容主要可以分为以下四部分: + +* 消息长度:总长度,四个字节存储,占用一个 int 类型 + +* 序列化类型&消息头长度:同样占用一个 int 类型,第一个字节表示序列化类型,后面三个字节表示消息头长度 + +* 消息头数据:经过序列化后的消息头数据 + +* 消息主体数据:消息主体的二进制字节数据内容 + + + +官方文档: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 + + + +**** + + + +##### 处理方法 + +NettyServerHandler 类用来处理 RemotingCommand 相关的数据,针对某一种类型的**请求处理** + +```java +class NettyServerHandler extends SimpleChannelInboundHandler { + @Override + protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception { + // 服务器处理接受到的请求信息 + processMessageReceived(ctx, msg); + } +} +public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception { + final RemotingCommand cmd = msg; + if (cmd != null) { + // 根据请求的类型进行处理 + switch (cmd.getType()) { + case REQUEST_COMMAND:// 客户端发起的请求,走这里 + processRequestCommand(ctx, cmd); + break; + case RESPONSE_COMMAND:// 客户端响应的数据,走这里【当前类本身是服务器类也是客户端类】 + processResponseCommand(ctx, cmd); + break; + default: + break; + } + } +} +``` + +NettyRemotingAbstract#processRequestCommand:处理请求的数据 + +* `matched = this.processorTable.get(cmd.getCode())`:根据业务请求码获取 Pair 对象,包含**处理器和线程池资源** + +* `pair = null == matched ? this.defaultRequestProcessor : matched`:未找到处理器则使用缺省处理器 + +* `int opaque = cmd.getOpaque()`:获取请求 ID + +* `Runnable run = new Runnable()`:创建任务对象 + + * `doBeforeRpcHooks()`:RPC HOOK 前置处理 + + * `callback = new RemotingResponseCallback()`:封装响应客户端逻辑 + + * `doAfterRpcHooks()`:RPC HOOK 后置处理 + * `if (!cmd.isOnewayRPC())`:条件成立说明不是单向请求,需要结果 + * `response.setOpaque(opaque)`:将请求 ID 设置到 response + * `response.markResponseType()`:设置当前的处理是响应处理 + * `ctx.writeAndFlush(response)`: 将数据交给 Netty IO 线程,完成数据写和刷 + + * `if (pair.getObject1() instanceof AsyncNettyRequestProcessor)`:Nameserver 默认使用 DefaultRequestProcessor 处理器,是一个 AsyncNettyRequestProcessor 子类 + + * `processor = (AsyncNettyRequestProcessor)pair.getObject1()`:获取处理器 + + * `processor.asyncProcessRequest(ctx, cmd, callback)`:异步调用,首先 processRequest,然后 callback 响应客户端 + + DefaultRequestProcessor.processRequest **根据业务码处理请求,执行对应的操作** + +* `requestTask = new RequestTask(run, ctx.channel(), cmd)`:将任务对象、通道、请求封装成 RequestTask 对象 + +* `pair.getObject2().submit(requestTask)`:获取处理器对应的线程池,将 task 提交,**从 IO 线程切换到业务线程** + +NettyRemotingAbstract#processResponseCommand:处理响应的数据 + +* `int opaque = cmd.getOpaque()`:获取请求 ID +* `responseFuture = responseTable.get(opaque)`:**从响应映射表中获取对应的对象** +* `responseFuture.setResponseCommand(cmd)`:设置响应的 Command 对象 +* `responseTable.remove(opaque)`:从映射表中移除对象,代表处理完成 +* `if (responseFuture.getInvokeCallback() != null)`:包含回调对象,异步执行回调对象 +* `responseFuture.putResponse(cmd)`:不好含回调对象,**同步调用时,需要唤醒等待的业务线程** + + + +流程:客户端 invokeSync → 服务器的 processRequestCommand → 客户端的 processResponseCommand → 结束 + + + + + + + + + + + diff --git a/Java.md b/Java.md index 0d8a455..d1f0b60 100644 --- a/Java.md +++ b/Java.md @@ -2320,6 +2320,8 @@ hashCode 的作用: #### 深浅克隆 +Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(),就不能直接去调用该类实例的 clone() 方法 + 深浅拷贝(克隆)的概念: * 浅拷贝 (shallowCopy):**对基本数据类型进行值传递,对引用数据类型只是复制了引用**,被复制对象属性的所有的引用仍然指向原来的对象,简而言之就是增加了一个指针指向原来对象的内存地址 @@ -2328,15 +2330,13 @@ hashCode 的作用: * 深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误 -Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(),就不能直接去调用该类实例的 clone() 方法 - Cloneable 接口是一个标识性接口,即该接口不包含任何方法(包括clone()),但是如果一个类想合法的进行克隆,那么就必须实现这个接口,在使用 clone() 方法时,若该类未实现 Cloneable 接口,则抛出异常 * Clone & Copy:`Student s = new Student` `Student s1 = s`:只是 copy 了一下 reference,s 和 s1 指向内存中同一个 Object,对对象的修改会影响对方 - `Student s2 = s.clone()`:会生成一个新的 Student 对象,并且和s具有相同的属性值和方法 + `Student s2 = s.clone()`:会生成一个新的 Student 对象,并且和 s 具有相同的属性值和方法 * Shallow Clone & Deep Clone: @@ -16085,7 +16085,7 @@ public class QuickSort { 实现思路: -- 获得最大数的位数,可以通过将最大数变为String类型,再求长度 +- 获得最大数的位数,可以通过将最大数变为 String 类型,再求长度 - 将所有待比较数值(正整数)统一为同样的数位长度,**位数较短的数前面补零** - 从最低位开始,依次进行一次排序 - 从最低位排序一直到最高位(个位 → 十位 → 百位 → … →最高位)排序完成以后,数列就变成一个有序序列 From 74f98a667c84532feb5a859d7c9f5d3be5e2451f Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 29 Dec 2021 23:29:46 +0800 Subject: [PATCH 051/122] Update README --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6433260..6f633dd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -**Java** 学习笔记,记录着作者从编程入门到深入学习的所有知识,每次学有所获就会更新笔记,所有的知识都有借鉴其他前辈的博客和文章,排版布局**美观整洁**,如果对各位朋友有所帮助,希望可以给个 star。 +**Java** 学习笔记,记录作者从编程入门到深入学习的所有知识,每次学有所获都会更新笔记,排版布局**美观整洁**,希望对读者朋友有所帮助。 个人邮箱:imseazean@gmail.com @@ -15,8 +15,7 @@ 其他说明: * 推荐使用 Typora 阅读笔记,打开目录栏效果更佳。 -* 所有的知识不保证权威性,如果各位朋友发现错误,非常欢迎与我讨论。 -* Java.md 更新后大于 1M,导致网页无法显示,所以划分为 Java 和 Program 两个文档。 +* 所有的知识不保证权威性,如果各位朋友发现错误,欢迎与我讨论。 +* 笔记的编写基于 Windows 平台,可能会因为平台的不同而造成空格、制表符的显示效果不同。 -* 笔记的编写是基于 Windows 平台,可能会因为平台的不同而造成空格、制表符的显示效果不同。 From 21717ee194664be70adc062ccaa7d5198db3012e Mon Sep 17 00:00:00 2001 From: Seazean Date: Wed, 29 Dec 2021 23:31:29 +0800 Subject: [PATCH 052/122] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f633dd..5b83132 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -**Java** 学习笔记,记录作者从编程入门到深入学习的所有知识,每次学有所获都会更新笔记,排版布局**美观整洁**,希望对读者朋友有所帮助。 +**Java** 学习笔记,记录作者从编程入门到深入学习的所有知识,每次学有所获都会更新笔记,排版布局**美观整洁**,希望对各位读者朋友有所帮助。 个人邮箱:imseazean@gmail.com From a555f81dd9942c557efeac208928b6af0df5f7e5 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 1 Jan 2022 22:54:15 +0800 Subject: [PATCH 053/122] Update Java Notes --- DB.md | 18 +- Frame.md | 1002 +++++++++++++++++++++++++++++++++++++++++++++++++++--- Java.md | 2 +- Web.md | 24 +- 4 files changed, 983 insertions(+), 63 deletions(-) diff --git a/DB.md b/DB.md index ebf0128..2564c46 100644 --- a/DB.md +++ b/DB.md @@ -1226,7 +1226,7 @@ LIMIT SELECT 列名1,列名2,... FROM 表名; ``` -* 去除重复查询:只有值全部重复的才可以去除,需要创建临时表辅助查询 +* **去除重复查询**:只有值全部重复的才可以去除,需要创建临时表辅助查询 ```mysql SELECT DISTINCT 列名1,列名2,... FROM 表名; @@ -1556,6 +1556,8 @@ SELECT * FROM emp WHERE name REGEXP '[uvw]';-- 匹配包含 uvw 的name值 #### 分组查询 +分组查询会进行去重 + * 分组查询语法 ````mysql @@ -2052,6 +2054,8 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 + + *** @@ -2074,6 +2078,8 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 SELECT 列名 FROM 表名1,表名2 WHERE 条件; ``` +内连接中 WHERE 子句和 ON 子句是等价的 + @@ -2085,7 +2091,9 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 外连接查询,若驱动表中的记录在被驱动表中找不到匹配的记录时,则该记录也会加到最后的结果集,只是对于被驱动表中**不匹配过滤条件**的记录,各个字段使用 NULL 填充 -应用实例:差学生成绩,也想查出缺考的人的成绩 +应用实例:查学生成绩,也想查出缺考的人的成绩 + +(内连接快还是外连接快?) * 左外连接:选择左侧的表为驱动表,查询左表的全部数据,和左右两张表有交集部分的数据 @@ -2232,7 +2240,9 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 #### 查询优化 -不相关子查询的结果集会被写入一个临时表,并且在写入时**去重**,该过程称为**物化**,存储结果集的临时表称为物化表。系统变量 tmp_table_size 或者 max_heap_table_size 为表的最值 +不相关子查询的结果集会被写入一个临时表,并且在写入时**去重**,该过程称为**物化**,存储结果集的临时表称为物化表。 + +系统变量 tmp_table_size 或者 max_heap_table_size 为表的最值 * 小于系统变量时,内存中可以保存,会为建立基于内存的 MEMORY 存储引擎的临时表,并建立哈希索引 * 大于任意一个系统变量时,物化表会使用基于磁盘的存储引擎来保存结果集中的记录,索引类型为 B+ 树 @@ -2246,7 +2256,7 @@ Join Buffer 可以通过参数 `join_buffer_size` 进行配置,默认大小是 -详细内容可以参考:《MySQL 是怎样运行的》 +参考书籍:https://book.douban.com/subject/35231266/ diff --git a/Frame.md b/Frame.md index 4dc7ebc..b5c4d88 100644 --- a/Frame.md +++ b/Frame.md @@ -4502,7 +4502,7 @@ NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态 NameServer 主要包括两个功能: -* Broker 路由管理,NameServer 接受 Broker 集群的注册信息,并保存下来作为路由信息的基本数据,提供**心跳检测机制**检查 Broker 活性(每 10 秒) +* Broker 路由管理,NameServer 接受 Broker 集群的注册信息,并保存下来作为路由信息的基本数据,提供**心跳检测机制**检查 Broker 活性,每 10 秒清除一次两小时没有活跃的 Broker * 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 NameServer 特点: @@ -4581,7 +4581,7 @@ At least Once:至少一次,指每个消息必须投递一次,Consumer 先 #### 存储结构 -Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的 +RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,消息真正的物理存储文件是 CommitLog,ConsumeQueue 是消息的逻辑队列,类似数据库的索引节点,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** @@ -5030,15 +5030,19 @@ public class MessageListenerImpl implements MessageListener { #### 重投机制 -生产者在发送消息时,同步消息失败会重投,异步消息有重试,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 +生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 -消息重复在 RocketMQ 中是无法避免的问题,如下方法可以设置消息重试策略: +消息重复在 RocketMQ 中是无法避免的问题,如下方法可以设置消息重投策略: - retryTimesWhenSendFailed:同步发送失败重投次数,默认为 2,因此生产者会最多尝试发送 retryTimesWhenSendFailed + 1 次。不会选择上次失败的 Broker,尝试向其他 Broker 发送,最大程度保证消息不丢。超过重投次数抛出异常,由客户端保证消息不丢。当出现 RemotingException、MQClientException 和部分 MQBrokerException 时会重投 - retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他 Broker,仅在同一个 Broker 上做重试,不保证消息不丢 - retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或 slave 不可用(返回状态非 SEND_OK),是否尝试发送到其他 Broker,默认 false,十分重要消息可以开启 +注意点: +* 如果同步模式发送失败,则选择到下一个 Broker,如果异步模式发送失败,则**只会在当前 Broker 进行重试** + +* 发送消息超时时间默认3000毫秒,就不会再尝试重试 @@ -5148,7 +5152,7 @@ public class MessageListenerImpl implements MessageListener { ## 源码分析 -### 服务启动 +### 服务端 #### 启动方法 @@ -5186,34 +5190,6 @@ NamesrvStartup#createNamesrvController:读取配置信息,初始化 Namesrv * `nettyServerConfig = new NettyServerConfig()`:Netty 的服务器配置对象 - ```java - public class NettyServerConfig implements Cloneable { - // 服务端启动时监听的端口号 - private int listenPort = 8888; - // 【业务线程池】 线程数量 - private int serverWorkerThreads = 8; - // 根据该值创建 remotingServer 内部的一个 publicExecutor - private int serverCallbackExecutorThreads = 0; - // netty 【worker】线程数 - private int serverSelectorThreads = 3; - // 【单向访问】时的并发限制 - private int serverOnewaySemaphoreValue = 256; - // 【异步访问】时的并发限制 - private int serverAsyncSemaphoreValue = 64; - // channel 最大的空闲存活时间 默认是 2min - private int serverChannelMaxIdleTimeSeconds = 120; - // 发送缓冲区大小 65535 - private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize; - // 接收缓冲区大小 65535 - private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; - // 是否启用 netty 内存池 默认开启 - private boolean serverPooledByteBufAllocatorEnable = true; - - // 默认 linux 会启用 【epoll】 - private boolean useEpollNativeSelector = false; - } - ``` - * `nettyServerConfig.setListenPort(9876)`:Namesrv 服务器的监听端口设置为 9876 * `if (commandLine.hasOption('c'))`:读取命令行 -c 的参数值 @@ -5276,12 +5252,12 @@ NamesrvController 用来初始化和启动 Namesrv 服务器 // 注册协议处理器(缺省协议处理器),处理器是 DefaultRequestProcessor,线程使用的是刚创建的业务的线程池 this.registerProcessor(); - // 定时任务1:每 10 秒钟检查 broker 存活状态,将 IDLE 状态的 broker 移除。【心跳机制】 + // 定时任务1:每 10 秒钟检查 broker 存活状态,将 IDLE 状态的 broker 移除【扫描机制】 this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { - // 将两小时没有活动的 broker 关闭,通过 next.getKey() 获取 broker 的地址 - // 然后【关闭服务器与broker物理节点的 channel】 + // 扫描 brokerLiveTable 表,将两小时没有活动的 broker 关闭, + //通过 next.getKey() 获取 broker 的地址,然后【关闭服务器与broker物理节点的 channel】 NamesrvController.this.routeInfoManager.scanNotActiveBroker(); } }, 5, 10, TimeUnit.SECONDS); @@ -5319,7 +5295,7 @@ NamesrvController 用来初始化和启动 Namesrv 服务器 -#### 网络服务 +#### 网络通信 ##### 通信原理 @@ -5360,7 +5336,7 @@ RocketMQ 的异步通信流程: ##### 成员属性 -成员变量: +NettyRemotingServer 类成员变量: * 服务器相关属性: @@ -5398,6 +5374,36 @@ RocketMQ 的异步通信流程: * 处理器:多个 Channel 共享的处理器 Handler,多个通道使用同一个对象 +* Netty 配置对象: + + ```java + public class NettyServerConfig implements Cloneable { + // 服务端启动时监听的端口号 + private int listenPort = 8888; + // 【业务线程池】 线程数量 + private int serverWorkerThreads = 8; + // 根据该值创建 remotingServer 内部的一个 publicExecutor + private int serverCallbackExecutorThreads = 0; + // netty 【worker】线程数 + private int serverSelectorThreads = 3; + // 【单向访问】时的并发限制 + private int serverOnewaySemaphoreValue = 256; + // 【异步访问】时的并发限制 + private int serverAsyncSemaphoreValue = 64; + // channel 最大的空闲存活时间 默认是 2min + private int serverChannelMaxIdleTimeSeconds = 120; + // 发送缓冲区大小 65535 + private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize; + // 接收缓冲区大小 65535 + private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 是否启用 netty 内存池 默认开启 + private boolean serverPooledByteBufAllocatorEnable = true; + + // 默认 linux 会启用 【epoll】 + private boolean useEpollNativeSelector = false; + } + ``` + 构造方法: @@ -5453,7 +5459,7 @@ RocketMQ 的异步通信流程: ```java public void start() { - // 向 channel pipeline 添加 handler,网络事件传播到当前 handler 时,【线程分配给 handler 处理事件】 + // Channel Pipeline 内的 handler 使用的线程资源,【线程分配给 handler 处理事件】 this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(...); // 创建通用共享的处理器 handler,【非常重要的 NettyServerHandler】 @@ -5493,7 +5499,7 @@ RocketMQ 的异步通信流程: this.nettyEventExecutor.start(); } - // 提交定时任务,每一秒 执行一次。扫描 responseTable 表,将过期的 请求 移除。 + // 提交定时任务,每一秒 执行一次。扫描 responseTable 表,将过期的数据移除。 this.timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { @@ -5653,17 +5659,17 @@ NettyRemotingAbstract#processRequestCommand:处理请求的数据 * `int opaque = cmd.getOpaque()`:获取请求 ID -* `Runnable run = new Runnable()`:创建任务对象 +* `Runnable run = new Runnable()`:创建任务对象,任务在提交到线程池后开始执行 * `doBeforeRpcHooks()`:RPC HOOK 前置处理 - * `callback = new RemotingResponseCallback()`:封装响应客户端逻辑 + * `callback = new RemotingResponseCallback()`:**封装响应客户端的逻辑** * `doAfterRpcHooks()`:RPC HOOK 后置处理 * `if (!cmd.isOnewayRPC())`:条件成立说明不是单向请求,需要结果 * `response.setOpaque(opaque)`:将请求 ID 设置到 response * `response.markResponseType()`:设置当前的处理是响应处理 - * `ctx.writeAndFlush(response)`: 将数据交给 Netty IO 线程,完成数据写和刷 + * `ctx.writeAndFlush(response)`: **将响应数据交给 Netty IO 线程,完成数据写和刷** * `if (pair.getObject1() instanceof AsyncNettyRequestProcessor)`:Nameserver 默认使用 DefaultRequestProcessor 处理器,是一个 AsyncNettyRequestProcessor 子类 @@ -5671,7 +5677,9 @@ NettyRemotingAbstract#processRequestCommand:处理请求的数据 * `processor.asyncProcessRequest(ctx, cmd, callback)`:异步调用,首先 processRequest,然后 callback 响应客户端 - DefaultRequestProcessor.processRequest **根据业务码处理请求,执行对应的操作** + `DefaultRequestProcessor.processRequest`:**根据业务码处理请求,执行对应的操作** + + `ClientRemotingProcessor.processRequest`:处理回退消息,需要消费者回执一条消息给生产者 * `requestTask = new RequestTask(run, ctx.channel(), cmd)`:将任务对象、通道、请求封装成 RequestTask 对象 @@ -5684,15 +5692,917 @@ NettyRemotingAbstract#processResponseCommand:处理响应的数据 * `responseFuture.setResponseCommand(cmd)`:设置响应的 Command 对象 * `responseTable.remove(opaque)`:从映射表中移除对象,代表处理完成 * `if (responseFuture.getInvokeCallback() != null)`:包含回调对象,异步执行回调对象 -* `responseFuture.putResponse(cmd)`:不好含回调对象,**同步调用时,需要唤醒等待的业务线程** +* `responseFuture.putResponse(cmd)`:不包含回调对象,**同步调用时,唤醒等待的业务线程** +流程:客户端 invokeSync → 服务器的 processRequestCommand → 客户端的 processResponseCommand → 结束 -流程:客户端 invokeSync → 服务器的 processRequestCommand → 客户端的 processResponseCommand → 结束 +*** + + + +##### 路由注册 + +DefaultRequestProcessor REGISTER_BROKER 方法解析: + +```java +public RemotingCommand registerBroker(ChannelHandlerContext ctx, RemotingCommand request) { + // 创建响应请求的对象,设置为响应类型,【先设置响应的状态码时系统错误码】 + // 反射创建 RegisterBrokerResponseHeader 对象设置到 response.customHeader 属性中 + final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterBrokerResponseHeader.class); + + // 获取出反射创建的 RegisterBrokerResponseHeader 用户自定义header对象。 + final RegisterBrokerResponseHeader responseHeader = (RegisterBrokerResponseHeader) response.readCustomHeader(); + + // 反射创建 RegisterBrokerRequestHeader 对象,并且将 request.extFields 中的数据写入到该对象中 + final RegisterBrokerRequestHeader requestHeader = request.decodeCommandCustomHeader(RegisterBrokerRequestHeader.class); + + // CRC 校验,计算请求中的 CRC 值和请求头中包含的是否一致 + if (!checksum(ctx, request, requestHeader)) { + response.setCode(ResponseCode.SYSTEM_ERROR); + response.setRemark("crc32 not match"); + return response; + } + + TopicConfigSerializeWrapper topicConfigWrapper; + if (request.getBody() != null) { + // 【解析请求体 body】,解码出来的数据就是当前机器的主题信息 + topicConfigWrapper = TopicConfigSerializeWrapper.decode(request.getBody(), TopicConfigSerializeWrapper.class); + } else { + topicConfigWrapper = new TopicConfigSerializeWrapper(); + topicConfigWrapper.getDataVersion().setCounter(new AtomicLong(0)); + topicConfigWrapper.getDataVersion().setTimestamp(0); + } + + // 注册方法 + // 参数1 集群、参数2:节点ip地址、参数3:brokerName、参数4:brokerId 注意brokerId=0的节点为主节点 + // 参数5:ha节点ip地址、参数6当前节点主题信息、参数7:过滤服务器列表、参数8:当前服务器和客户端通信的channel + RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(..); + + // 将结果信息 写到 responseHeader 中 + responseHeader.setHaServerAddr(result.getHaServerAddr()); + responseHeader.setMasterAddr(result.getMasterAddr()); + // 获取 kv配置,写入 response body 中,【kv 配置是顺序消息相关的】 + byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC); + response.setBody(jsonValue); + + // code 设置为 SUCCESS + response.setCode(ResponseCode.SUCCESS); + response.setRemark(null); + // 返回 response ,【返回的 response 由 callback 对象处理】 + return response; +} +``` + +RouteInfoManager#registerBroker:注册 Broker 的信息 + +* `RegisterBrokerResult result = new RegisterBrokerResult()`:返回结果的封装对象 + +* `this.lock.writeLock().lockInterruptibly()`:加写锁后**同步执行** + +* `brokerNames = this.clusterAddrTable.get(clusterName)`:获取当前集群上的 Broker 名称列表,是空就新建列表 + +* `brokerNames.add(brokerName)`:将当前 Broker 名字加入到集群列表 + +* `brokerData = this.brokerAddrTable.get(brokerName)`:获取当前 Broker 的 brokerData,是空就新建放入映射表 + +* `brokerAddrsMap = brokerData.getBrokerAddrs()`:获取当前 Broker 的物理节点 map 表,进行遍历,如果物理节点角色发生变化(slave → master),先将旧数据从物理节点 map 中移除,然后重写放入,**保证节点的唯一性** + +* `if (null != topicConfigWrapper && MixAll.MASTER_ID == brokerId)`:Broker 上的 Topic 不为 null,并且当前物理节点是 Broker 上的 master 节点 + + `tcTable = topicConfigWrapper.getTopicConfigTable()`:获取当前 Broker 信息中的主题映射表 + + `if (tcTable != null)`:映射表不空就加入或者更新到 Namesrv 内 + +* ` prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr)`:添加**当前节点的 BrokerLiveInfo** ,返回上一次心跳时当前 Broker 节点的存活对象数据。**NamesrvController 中的定时任务会扫描映射表 brokerLiveTable** + + ```java + BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr, new BrokerLiveInfo( + System.currentTimeMillis(),topicConfigWrapper.getDataVersion(), channel,haServerAddr)); + ``` + +* `if (MixAll.MASTER_ID != brokerId)`:当前 Broker 不是 master 节点,**获取主节点的信息**设置到结果对象 + +* `this.lock.writeLock().unlock()`:释放写锁 + + + +**** + + + +### 生产者 + +#### 生产者类 + +DefaultMQProducer 是生产者的默认实现类 + +成员变量: + +* 生产者实现类: + + ```java + protected final transient DefaultMQProducerImpl defaultMQProducerImpl + ``` + +* 生产者组:发送事务消息,Broker 端进行事务回查(补偿机制)时,选择当前生产者组的下一个生产者进行事务回查 + + ```java + private String producerGroup; + ``` + +* 默认主题:isAutoCreateTopicEnable 开启时,当发送消息指定的 Topic 在 Namesrv 未找到路由信息,使用该值创建 Topic 信息 + + ```java + private String createTopicKey = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC; + // 值为【TBW102】,Just for testing or demo program + ``` + +* 消息重投:系统特性消息重试部分详解了三个参数的作用 + + ```java + private int retryTimesWhenSendFailed = 2; // 同步发送失败后重试的发送次数,加上第一次发送,一共三次 + private int retryTimesWhenSendAsyncFailed = 2; // 异步 + private boolean retryAnotherBrokerWhenNotStoreOK = false; // 消息未存储成功,选择其他 Broker 重试 + ``` + +* 消息队列: + + ```java + private volatile int defaultTopicQueueNums = 4; // 默认 Broker 创建的队列数 + ``` + +* 消息属性: + + ```java + private int sendMsgTimeout = 3000; // 发送消息的超时限制 + private int compressMsgBodyOverHowmuch = 1024 * 4; // 压缩阈值,当 msg body 超过 4k 后使用压缩 + private int maxMessageSize = 1024 * 1024 * 4; // 消息体的最大限制,默认 4M + private TraceDispatcher traceDispatcher = null; // 消息轨迹 + +构造方法: + +* 构造方法: + + ```java + public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook) { + this.namespace = namespace; + this.producerGroup = producerGroup; + // 创建生产者实现对象 + defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook); + } + ``` + +成员方法: + +* start():启动方法 + + ```java + public void start() throws MQClientException { + // 重置生产者组名,如果传递了命名空间,则 【namespace%group】 + this.setProducerGroup(withNamespace(this.producerGroup)); + // 生产者实现对象启动 + this.defaultMQProducerImpl.start(); + if (null != traceDispatcher) { + // 消息轨迹的逻辑 + traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel()); + } + } + ``` + +* send():**发送消息**: + + ```java + public SendResult send(Message msg){ + // 校验消息 + Validators.checkMessage(msg, this); + // 设置消息 Topic + msg.setTopic(withNamespace(msg.getTopic())); + return this.defaultMQProducerImpl.send(msg); + } + ``` + +* request():请求方法,**需要消费者回执消息**,又叫回退消息 + + ```java + public Message request(final Message msg, final MessageQueue mq, final long timeout) { + msg.setTopic(withNamespace(msg.getTopic())); + return this.defaultMQProducerImpl.request(msg, mq, timeout); + } + ``` + + + + +*** + + + +#### 实现者类 + +##### 成员属性 + +DefaultMQProducerImpl 类是默认的生产者实现类 + +成员变量: + +* 实例对象: + + ```java + private final DefaultMQProducer defaultMQProducer; // 持有默认生产者对象,用来获取对象中的配置信息 + private MQClientInstance mQClientFactory; // 客户端实例对象,生产者启动后需要注册到该客户端对象内 + ``` + +* 主题发布信息映射表:key 是 Topic,value 是发布信息 + + ```java + private final ConcurrentMap topicPublishInfoTable = new ConcurrentHashMap(); + ``` + + ```java + public class TopicPublishInfo { + private boolean orderTopic = false; + private boolean haveTopicRouterInfo = false; + // 主题的全部队列 + private List messageQueueList = new ArrayList(); + // 使用 ThreaLocal 保存发送消息使用的队列 + private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); + // 主题的路由数据 + private TopicRouteData topicRouteData; + } + ``` + +* 异步发送消息:相关信息 + + ```java + private final BlockingQueue asyncSenderThreadPoolQueue;// 异步发送消息,异步线程池使用的队列 + private final ExecutorService defaultAsyncSenderExecutor; // 异步发送消息默认使用的线程池 + private ExecutorService asyncSenderExecutor; // 异步消息发送线程池,指定后就不使用默认线程池了 + ``` + +* 定时器:执行定时任务 + + ```java + private final Timer timer = new Timer("RequestHouseKeepingService", true); // 守护线程 + ``` + +* 状态信息:服务的状态,默认创建状态 + + ```java + private ServiceState serviceState = ServiceState.CREATE_JUST; + ``` + +* 压缩等级:ZIP 压缩算法的等级,默认是 5,越高压缩效果好,但是压缩的更慢 + + ```java + private int zipCompressLevel = Integer.parseInt(System.getProperty..., "5")); + ``` + +* 容错策略:选择队列的容错策略 + + ```java + private MQFaultStrategy mqFaultStrategy = new MQFaultStrategy(); + ``` + +* 钩子:用来进行前置或者后置处理 + + ```java + ArrayList sendMessageHookList; // 发送消息的钩子,留给用户扩展使用 + ArrayList checkForbiddenHookList; // 对比上面的钩子,可以抛异常,控制消息是否可以发送 + private final RPCHook rpcHook; // 传递给 NettyRemotingClient + ``` + +构造方法: + +* 默认构造: + + ```java + public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer) { + // 默认 RPC HOOK 是空 + this(defaultMQProducer, null); + } + ``` + +* 有参构造: + + ```java + public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer, RPCHook rpcHook) { + // 属性赋值 + this.defaultMQProducer = defaultMQProducer; + this.rpcHook = rpcHook; + + // 创建【异步消息线程池任务队列】,长度是 5w + this.asyncSenderThreadPoolQueue = new LinkedBlockingQueue(50000); + // 创建默认的异步消息任务线程池 + this.defaultAsyncSenderExecutor = new ThreadPoolExecutor( + // 核心线程数和最大线程数都是 系统可用的计算资源(8核16线程的系统就是 16)... + } + ``` + + + +**** + + + +##### 成员方法 + +* start():启动方法,参数默认是 true,代表正常的启动路径 + + * `this.serviceState = ServiceState.START_FAILED`:先修改为启动失败,成功后再修改,这种思想很常见 + + * `this.checkConfig()`:判断生产者组名不能是空,也不能是 default_PRODUCER + + * `if (!getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP))`:条件成立说明当前生产者不是内部产生者,内部生产者是**处理消息回退**的这种情况使用的生产者 + + `this.defaultMQProducer.changeInstanceNameToPID()`:正常的生产者,修改生产者实例名称为当前进程的 PID + + * ` this.mQClientFactory = ...`:获取当前进程的 MQ 客户端实例对象,从 factoryTable 中获取 key 为 客户端 ID,格式是`ip@pid`,**一个 JVM 进程只有一个 PID,也只有一个 MQClientInstance** + + * `boolean registerOK = mQClientFactory.registerProducer(...)`:将生产者注册到 RocketMQ 客户端实例内 + + * `this.topicPublishInfoTable.put(...)`:添加一个主题发布信息,key 是 **TBW102** ,value 是一个空对象 + + * `if (startFactory) `:正常启动路径 + + `mQClientFactory.start()`:启动 RocketMQ 客户端实例对象 + + * `this.serviceState = ServiceState.RUNNING`:修改生产者实例的状态 + + * `this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()`:RocketMQ 客户端实例向已知的 Broker 节点发送一次心跳(也是定时任务) + * `this.timer.scheduleAtFixedRate()`: request 发送的回执信息,启动定时任务每秒一次删除超时请求 + + * 生产者 msg 添加信息关联 ID 发送到 Broker + * 消费者从 Broker 拿到消息后会检查 msg 类型是一个需要回执的消息,处理完消息后会根据 msg 关联 ID 和客户端 ID 生成一条响应结果消息发送到 Broker,Broker 判断为回执消息,会根据客户端ID 找到 channel 推送给生产者 + * 生产者拿到回执消息后,读取出来关联 ID 找到对应的 RequestFuture,将阻塞线程唤醒 + +* sendDefaultImpl():发送消息 + + ```java + //参数1:消息;参数2:发送模式(同步异步单向);参数3:回调函数,异步发送时需要;参数4:发送超时时间, 默认 3 秒 + private SendResult sendDefaultImpl(msg, communicationMode, sendCallback,timeout) {} + ``` + + * `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.putIfAbsent(topic, new TopicPublishInfo())`:保存一份空数据 + + `this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic)`:从 Namesrv 更新该 Topic 的路由数据 + + `topicPublishInfo = this.topicPublishInfoTable.get(topic)`:重新从本地获取发布信息 + + * `this.mQClientFactory.updateTopicRouteInfoFromNameServer(..)`:**路由数据是空,获取默认 TBW102 的数据** + + * `return topicPublishInfo`:返回 TBW102 主题的发布信息 + + * `int timesTotal, times `:发送的总尝试次数和当前是第几次发送 + * `String[] brokersSent = new String[timesTotal]`:下标索引代表第几次发送,值代表这次发送选择 Broker name + * `for (; times < timesTotal; times++)`:循环发送,发送成功或者发送尝试次数达到上限,结束循环 + * `String lastBrokerName = null == mq ? null : mq.getBrokerName()`:获取上次发送失败的 BrokerName + + * `mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName)`:**从发布信息中选择一个队列** + + * `if (this.sendLatencyFaultEnable)`:默认不开启,可以通过配置开启 + * `return tpInfo.selectOneMessageQueue(lastBrokerName)`:默认选择队列的方式,就是循环主题全部的队列 + * `int index = this.sendWhichQueue.getAndIncrement()`:选择队列的索引 + * `int pos = Math.abs(index) % this.messageQueueList.size()`:获取该索引对应的队列位置 + * `return this.messageQueueList.get(pos)`:返回消息队列 + + * `brokersSent[times] = mq.getBrokerName()`:将本次选择的 BrokerName 存入数组 + * `msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()))`:**重投的消息需要加上标记** + * `sendResult = this.sendKernelImpl`:核心发送方法 + * `this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false)`:更新一下时间 + + * `switch (communicationMode)`:异步或者单向消息直接返回 null,同步发送进入逻辑判断 + + `if (sendResult.getSendStatus() != SendStatus.SEND_OK)`:**服务端 Broker 存储失败**,需要重试其他 Broker + + * `throw new MQClientException()`:未找到当前主题的路由数据,无法发送消息,抛出异常 + +* sendKernelImpl():**核心发送方法** + + ```java + //参数1:消息;参数2:选择的队列;参数3:发送模式(同步异步单向);参数4:回调函数,异步发送时需要;参数5:主题发布信息;参数6:剩余超时时间限制 + private SendResult sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout) + ``` + + * `brokerAddr = this.mQClientFactory(...)`:获取指定 BrokerName 对应的 mater 节点的地址,master 节点的 ID 为 0 + + * `brokerAddr = MixAll.brokerVIPChannel()`:Broker 启动时会绑定两个服务器端口,一个是普通端口,一个是 VIP 端口,服务器端根据不同端口创建不同的的 NioSocketChannel + + * `byte[] prevBody = msg.getBody()`:获取消息体 + + * `if (!(msg instanceof MessageBatch))`:非批量消息,需要重新设置消息 ID + + `MessageClientIDSetter.setUniqID(msg)`:msg id 由两部分组成,一部分是 ip 地址、进程号、ClassLoader 的 hashcode,另一部分是时间差(当前时间减去当月一号的时间)和计数器的值 + + * `if (this.tryToCompressMessage(msg))`:判断消息是否压缩,压缩需要设置压缩标记 + + * `hasCheckForbiddenHook、hasSendMessageHook`:执行钩子方法 + + * `requestHeader = new SendMessageRequestHeader()`:设置发送消息的消息头 + + * `if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX))`:重投的发送消息 + + * `switch (communicationMode)`:异步发送一种处理方式,单向和同步同样的处理逻辑 + + `sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage()`:**发送消息** + + * `request = RemotingCommand.createRequestCommand()`:创建一个 RequestCommand 对象 + * `request.setBody(msg.getBody())`:**将消息放入请求体** + * `switch (communicationMode)`:根据不同的模式 invoke 不同的方法 + +* request():请求方法,消费者回执消息,这种消息是异步消息 + + * `requestResponseFuture = new RequestResponseFuture(correlationId, timeout, null)`:创建请求响应对象 + + * `getRequestFutureTable().put(correlationId, requestResponseFuture)`:放入RequestFutureTable 映射表中 + + * `this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback())`:**发送异步消息** + + * `return waitResponse(msg, timeout, requestResponseFuture, cost)`:用来挂起请求的方法 + + ```java + public Message waitResponseMessage(final long timeout) throws InterruptedException { + // 请求挂起 + this.countDownLatch.await(timeout, TimeUnit.MILLISECONDS); + return this.responseMsg; + } + + * 当消息被消费后,会获取消息的关联 ID,从映射表中获取消息的 RequestResponseFuture,执行下面的方法唤醒挂起线程 + + ```java + public void putResponseMessage(final Message responseMsg) { + this.responseMsg = responseMsg; + this.countDownLatch.countDown(); + } + ``` + + + +**** + + + +### 客户端 + +#### 实例对象 + +##### 成员属性 + +MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一个客户端实例,**既服务于生产者,也服务于消费者** + +成员变量: + +* 配置信息: + + ```java + private final int instanceIndex; // 索引一般是 0,因为客户端实例一般都是一个进程只有一个 + private final String clientId; // 客户端 ID ip@pid + private final long bootTimestamp; // 客户端的启动时间 + private ServiceState serviceState; // 客户端状态 + ``` + +* 生产者消费者的映射表:key 是组名 + + ```java + private final ConcurrentMap producerTable + private final ConcurrentMap consumerTable + private final ConcurrentMap adminExtTable + ``` + +* 网络层配置: + + ```java + private final NettyClientConfig nettyClientConfig; + ``` + +* 核心功能的实现:负责将 MQ 业务层的数据转换为网络层的 RemotingCommand 对象,使用内部持有的 NettyRemotingClient 对象的 invoke 系列方法,完成网络 IO(同步、异步、单向) + + ```java + private final MQClientAPIImpl mQClientAPIImpl; + ``` + +* 本地路由数据:key 是主题名称,value 路由信息 + + ```java + private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); + +* 锁信息:两把锁,锁不同的数据 + + ```java + private final Lock lockNamesrv = new ReentrantLock(); + private final Lock lockHeartbeat = new ReentrantLock(); + ``` + +* 调度线程池:单线程,执行定时任务 + + ```java + private final ScheduledExecutorService scheduledExecutorService; + ``` + +* Broker 映射表:key 是 BrokerName + + ```java + // 物理节点映射表,value:Long 是 brokerID,【ID=0 的是主节点,其他是从节点】,String 是地址 ip:port + private final ConcurrentMap> brokerAddrTable; + // 物理节点版本映射表,String 是地址 ip:port,Integer 是版本 + ConcurrentMap> brokerVersionTable; + ``` + +* **客户端的协议处理器**:用于处理 IO 事件 + + ```java + private final ClientRemotingProcessor clientRemotingProcessor; + ``` + +* 消息服务: + + ```java + private final PullMessageService pullMessageService; // 拉消息服务 + private final RebalanceService rebalanceService; // 消费者负载均衡服务 + private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 + ``` + +* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 + + ```java + private final DefaultMQProducer defaultMQProducer; + ``` + +* 心跳次数统计: + + ```java + private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) + ``` + +* 公共配置类: + + ```java + public class ClientConfig { + // Namesrv 地址配置 + private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); + // 客户端的 IP 地址 + private String clientIP = RemotingUtil.getLocalAddress(); + // 客户端实例名称 + private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); + // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 命名空间 + protected String namespace; + protected AccessChannel accessChannel = AccessChannel.LOCAL; + + // 获取路由信息的间隔时间 30s + private int pollNameServerInterval = 1000 * 30; + // 客户端与 broker 之间的心跳周期 30s + private int heartbeatBrokerInterval = 1000 * 30; + // 消费者持久化消费的周期 5s + private int persistConsumerOffsetInterval = 1000 * 5; + private long pullTimeDelayMillsWhenException = 1000; + private boolean unitMode = false; + private String unitName; + // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 + private boolean vipChannelEnabled = Boolean.parseBoolean(); + // 语言,默认是 Java + private LanguageCode language = LanguageCode.JAVA; + } + ``` + +构造方法: + +* MQClientInstance 有参构造: + + ```java + public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) { + this.clientConfig = clientConfig; + this.instanceIndex = instanceIndex; + // Netty 相关的配置信息 + this.nettyClientConfig = new NettyClientConfig(); + // 平台核心数 + this.nettyClientConfig.setClientCallbackExecutorThreads(...); + this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS()); + // 【创建客户端协议处理器】 + this.clientRemotingProcessor = new ClientRemotingProcessor(this); + // 创建 API 实现对象 + // 参数一:客户端网络配置 + // 参数二:客户端协议处理器,注册到客户端网络层 + // 参数三:rpcHook,注册到客户端网络层 + // 参数四:客户端配置 + this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig); + + //... + // 内部生产者,指定内部生产者的组 + this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP); + } + ``` + +* MQClientAPIImpl 有参构造: + + ```java + public MQClientAPIImpl(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig) { + this.clientConfig = clientConfig; + topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); + // 创建网络层对象,参数二为 null 说明客户端并不关心 channel event + this.remotingClient = new NettyRemotingClient(nettyClientConfig, null); + // 业务处理器 + this.clientRemotingProcessor = clientRemotingProcessor; + // 注册 RpcHook + this.remotingClient.registerRPCHook(rpcHook); + // ... + // 注册回退消息的请求码 + this.remotingClient.registerProcessor(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, this.clientRemotingProcessor, null); + } + ``` + + + +*** + + + +##### 成员方法 + +* start():启动方法 + + * `synchronized (this)`:加锁保证线程安全,保证只有一个实例对象启动 + * `this.mQClientAPIImpl.start()`:启动客户端网络层,底层调用 RemotingClient 类 + * `this.startScheduledTask()`:启动定时任务 + * `this.pullMessageService.start()`:启动拉取消息服务 + * `this.rebalanceService.start()`:启动负载均衡服务 + * `this.defaultMQProducer.getDefaultMQProducerImpl().start(false)`:启动内部生产者,参数为 false 代表不启动实例 + +* startScheduledTask():**启动定时任务**,调度线程池是单线程 + + * `if (null == this.clientConfig.getNamesrvAddr())`:Namesrv 地址是空,需要两分钟拉取一次 Namesrv 地址 + + * 定时任务 1:从 Namesrv 更新客户端本地的路由数据,周期 30 秒一次 + + ```java + // 获取生产者和消费者订阅的主题集合,遍历集合,对比从 namesrv 拉取最新的主题路由数据和本地数据,是否需要更新 + MQClientInstance.this.updateTopicRouteInfoFromNameServer(); + ``` + + * 定时任务 2:周期 30 秒一次,两个任务 + + * 清理下线的 Broker 节点,遍历客户端的 Broker 物理节点映射表,将所有主题数据都不包含的 Broker 物理节点清理掉,如果被清理的 Broker 下所有的物理节点都没有了,就将该 Broker 的映射数据删除掉 + * 向在线的所有的 Broker 发送心跳数据,**同步发送的方式**,返回值是 Broker 物理节点的版本号,更新版本映射表 + + ```java + MQClientInstance.this.cleanOfflineBroker(); + MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); + ``` + + ```java + // 心跳数据 + public class HeartbeatData extends RemotingSerializable { + // 客户端 ID ip@pid + private String clientID; + // 存储客户端所有生产者数据 + private Set producerDataSet = new HashSet(); + // 存储客户端所有消费者数据 + private Set consumerDataSet = new HashSet(); + } + ``` + + * 定时任务 3:消费者持久化消费数据,周期 5 秒一次 + + ```java + MQClientInstance.this.persistAllConsumerOffset(); + ``` + + * 定时任务 4:动态调整消费者线程池,周期 1 分钟一次 + + ```java + MQClientInstance.this.adjustThreadPool(); + ``` + +* updateTopicRouteInfoFromNameServer():**更新路由数据** + + * `if (isDefault && defaultMQProducer != null)`:需要默认数据 + + `topicRouteData = ...getDefaultTopicRouteInfoFromNameServer()`:从 Namesrv 获取默认的 TBW102 的路由数据 + + `int queueNums`:遍历所有队列,为每个读写队列设置较小的队列数 + + * `topicRouteData = ...getTopicRouteInfoFromNameServer(topic)`:需要**从 Namesrv 获取**路由数据(同步) + + * `old = this.topicRouteTable.get(topic)`:获取客户端实例本地的该主题的路由数据 + + * `boolean changed = topicRouteDataIsChange(old, topicRouteData)`:对比本地和最新下拉的数据是否一致 + + * `if (changed)`:不一致进入更新逻辑 + + `cloneTopicRouteData = topicRouteData.cloneTopicRouteData()`:克隆一份最新数据 + + `Update Pub info`:更新生产者信息 + + * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:**将主题路由数据转化为发布数据** + * `impl.updateTopicPublishInfo(topic, publishInfo)`:生产者将主题的发布数据保存到它本地,方便发送消息使用 + + `Update sub info`:更新消费者信息 + + `this.topicRouteTable.put(topic, cloneTopicRouteData)`:将数据放入本地路由表 + + + +**** + + + +#### 网络通信 + +##### 成员属性 + +NettyRemotingClient 类负责客户端的网络通信 + +成员变量: + +* Netty 服务相关属性: + + ```java + private final NettyClientConfig nettyClientConfig; // 客户端的网络层配置 + private final Bootstrap bootstrap = new Bootstrap(); // 客户端网络层启动对象 + private final EventLoopGroup eventLoopGroupWorker; // 客户端网络层 Netty IO 线程组 + ``` + +* Channel 映射表: + + ```java + private final ConcurrentMap channelTables;// key 是服务器的地址,value 是通道对象 + private final Lock lockChannelTables = new ReentrantLock(); // 锁,控制并发安全 + ``` + +* 定时器:启动定时任务 + + ```java + private final Timer timer = new Timer("ClientHouseKeepingService", true) + ``` + +* 线程池: + + ```java + private ExecutorService publicExecutor; // 公共线程池 + private ExecutorService callbackExecutor; // 回调线程池,客户端发起异步请求,服务器的响应数据由回调线程池处理 + ``` + +* 事件监听器:客户端这里是 null + + ```java + private final ChannelEventListener channelEventListener; + ``` + +* Netty 配置对象: + + ```java + public class NettyClientConfig { + // 客户端工作线程数 + private int clientWorkerThreads = 4; + // 回调处理线程池 线程数:平台核心数 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 单向请求并发数,默认 65535 + private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; + // 异步请求并发数,默认 65535 + private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; + // 客户端连接服务器的超时时间限制 3秒 + private int connectTimeoutMillis = 3000; + // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) + private long channelNotActiveInterval = 1000 * 60; + // 客户端与服务器 ch 最大空闲时间 2分钟 + private int clientChannelMaxIdleTimeSeconds = 120; + + // 底层 Socket 写和收 缓冲区的大小 65535 64k + private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; + private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 客户端 netty 是否启动内存池 + private boolean clientPooledByteBufAllocatorEnable = false; + // 客户端是否超时关闭 Socket 连接 + private boolean clientCloseSocketIfTimeout = false; + } + ``` + +构造方法 + +* 无参构造: + + ```java + public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { + this(nettyClientConfig, null); + } + ``` + +* 有参构造: + + ```java + public NettyRemotingClient(nettyClientConfig, channelEventListener) { + // 父类创建了2个信号量,1、控制单向请求的并发度,2、控制异步请求的并发度 + super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue()); + this.nettyClientConfig = nettyClientConfig; + this.channelEventListener = channelEventListener; + + // 创建公共线程池 + int publicThreadNums = nettyClientConfig.getClientCallbackExecutorThreads(); + if (publicThreadNums <= 0) { + publicThreadNums = 4; + } + this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums,); + + // 创建 Netty IO 线程,1个线程 + this.eventLoopGroupWorker = new NioEventLoopGroup(1, ); + + if (nettyClientConfig.isUseTLS()) { + sslContext = TlsHelper.buildSslContext(true); + } + } + ``` + + + +**** + + + +##### 成员方法 + +* start():启动方法 + + ```java + public void start() { + // channel pipeline 内的 handler 使用的线程资源,默认 4 个 + this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(); + // 配置 netty 客户端启动类对象 + Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class) + //... + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + // 加几个handler + pipeline.addLast( + // 服务端的数据,都会来到这个 + new NettyClientHandler()); + } + }); + // 注意 Bootstrap 只是配置好客户端的元数据了,【在这里并没有创建任何 channel 对象】 + // 定时任务 扫描 responseTable 中超时的 ResponseFuture,避免客户端线程长时间阻塞 + this.timer.scheduleAtFixedRate(() -> { + NettyRemotingClient.this.scanResponseTable(); + }, 1000 * 3, 1000); + // 这里是 null,不启动 + if (this.channelEventListener != null) { + this.nettyEventExecutor.start(); + } + } + ``` + +* 单向通信: + + ```java + public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) { + // 开始时间 + long beginStartTime = System.currentTimeMillis(); + // 获取或者创建客户端与服务端(addr)的通道 channel + final Channel channel = this.getAndCreateChannel(addr); + // 条件成立说明客户端与服务端 channel 通道正常,可以通信 + if (channel != null && channel.isActive()) { + try { + // 执行 rpcHook 拓展点 + doBeforeRpcHooks(addr, request); + // 计算耗时,如果当前耗时已经超过 timeoutMillis 限制,则直接抛出异常,不再进行系统通信 + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeoutMillis < costTime) { + throw new RemotingTimeoutException("invokeSync call timeout"); + } + // 参数1:客户端-服务端通道channel + // 参数二:网络层传输对象,封装着请求数据 + // 参数三:剩余的超时限制 + RemotingCommand response = this.invokeSyncImpl(channel, request, ...); + // 后置处理 + doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response); + // 返回响应数据 + return response; + } catch (RemotingSendRequestException e) {} + } else { + this.closeChannel(addr, channel); + throw new RemotingConnectException(addr); + } + } + ``` + + diff --git a/Java.md b/Java.md index d1f0b60..edc5ccd 100644 --- a/Java.md +++ b/Java.md @@ -4436,7 +4436,7 @@ TreeSet 集合自排序的方式: 方法:`public int compare(Employee o1, Employee o2): o1 比较者, o2 被比较者` - * 比较者大于被比较者,返回正数 + * 比较者大于被比较者,返回正数(升序) * 比较者小于被比较者,返回负数 * 比较者等于被比较者,返回 0 diff --git a/Web.md b/Web.md index 7c70150..5262ce7 100644 --- a/Web.md +++ b/Web.md @@ -2164,7 +2164,7 @@ HTTP 和 HTTPS 的区别: * 优点:运算速度快 * 缺点:无法安全的将密钥传输给通信方 -* 非对称加密:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥,公钥公开给任何人(类似于把锁和箱子给别人,对方打开箱子放入数据,上锁后发送),典型的非对称加密算法有RSA、DSA等 +* 非对称加密:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥,**公钥公开给任何人**(类似于把锁和箱子给别人,对方打开箱子放入数据,上锁后发送),典型的非对称加密算法有 RSA、DSA 等 * 优点:可以更安全地将公开密钥传输给通信发送方 * 缺点:运算速度慢 @@ -2174,28 +2174,28 @@ HTTP 和 HTTPS 的区别: * 获取到 Secret Key 后,再使用对称密钥加密方式进行通信,从而保证效率 思想:锁上加锁 + +* 数字签名:附加在报文上的特殊加密校验码,可以防止报文被篡改,一般是通过哈希算法 + +* 数字证书:由权威机构给某网站颁发的一种认可凭证 HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加密,客户端生成的随机密钥,用来进行对称加密 ![](https://gitee.com/seazean/images/raw/master/Web/HTTP-HTTPS加密过程.png) -1. 客户端向服务器发起 HTTPS 请求,连接到服务器的 443 端口 - -2. 服务器端有一个密钥对,即公钥和私钥,用来进行非对称加密,服务器端保存着私钥不能泄露,公钥可以发给任何客户端 - -3. 服务器将公钥发送给客户端 - -4. 客户端收到服务器端的数字证书之后,会对证书进行检查,验证其合法性,如果发现发现证书有问题,那么 HTTPS 传输就无法继续。如果公钥合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,将该密钥称之为 client key,即客户端密钥。然后用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文了,HTTPS 中的第一次 HTTP 请求结束 - +1. 客户端向服务器发起 HTTPS 请求,连接到服务器的 443 端口,请求携带了浏览器支持的加密算法和哈希算法 +2. 服务器端会向数字证书认证机构提出公开密钥的申请,认证机构对公开密钥做数字签名后进行分配,会将公钥绑定在数字证书(又叫公钥证书,内容有公钥,网站地址,证书颁发机构,失效日期等) +3. 服务器将数字证书发送给客户端,私钥由服务器持有 +4. 客户端收到服务器端的数字证书后对证书进行检查,验证其合法性,如果发现发现证书有问题,那么 HTTPS 传输就无法继续。如果公钥合格,那么客户端会生成一个随机值,**这个随机值就是用于进行对称加密的密钥**,将该密钥称之为 client key(客户端密钥、会话密钥)。用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文,HTTPS 中的第一次 HTTP 请求结束 5. 客户端会发起 HTTPS 中的第二个 HTTP 请求,将加密之后的客户端密钥发送给服务器 - 6. 服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文 - 7. 服务器将加密后的密文发送给客户端 - 8. 客户端收到服务器发送来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据,这样 HTTPS 中的第二个 HTTP 请求结束,整个 HTTPS 传输完成 + +参考文章:https://www.cnblogs.com/linianhui/p/security-https-workflow.html + 参考文章:https://www.jianshu.com/p/14cd2c9d2cd2 From be312e31a807598dc13bd0f33e98ddd676e0e6e4 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 3 Jan 2022 15:04:59 +0800 Subject: [PATCH 054/122] Update README --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 5b83132..4ac1828 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ 内容说明: * DB:MySQL、Redis -* Frame:Maven、Netty +* Frame:Maven、Netty、RocketMQ * Java:JavaSE、JVM、Algorithm、Design Pattern * Prog:Concurrent、Network Programming * SSM:MyBatis、Spring、SpringMVC、SpringBoot @@ -18,4 +18,3 @@ * 所有的知识不保证权威性,如果各位朋友发现错误,欢迎与我讨论。 * 笔记的编写基于 Windows 平台,可能会因为平台的不同而造成空格、制表符的显示效果不同。 - From 4debfde6642506a8e4b4f2e88189efa8038f7745 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 6 Jan 2022 00:48:25 +0800 Subject: [PATCH 055/122] Update Java Notes --- DB.md | 30 ++- Frame.md | 651 +++++++++++++++++++++++++++++++++++++++++++------------ Java.md | 2 +- Prog.md | 16 +- 4 files changed, 544 insertions(+), 155 deletions(-) diff --git a/DB.md b/DB.md index 2564c46..c815155 100644 --- a/DB.md +++ b/DB.md @@ -5320,7 +5320,7 @@ Buffer Pool 中每个缓冲页都有对应的控制信息,包括表空间编 MySQL 提供了缓冲页的快速查找方式:**哈希表**,使用表空间号和页号作为 Key,缓冲页控制块的地址作为 Value 创建一个哈希表,获取数据页时根据 Key 进行哈希寻址: * 如果不存在对应的缓存页,就从 free 链表中选一个空闲缓冲页,把磁盘中的对应页加载到该位置 -* 如果存在对应的缓存页,直接获取使用 +* 如果存在对应的缓存页,直接获取使用,提高查询数据的效率 @@ -5419,6 +5419,8 @@ Innodb 用一块内存区做 IO 缓存池,该缓存池不仅用来缓存 Innod SHOW ENGINE INNODB STATUS\G ``` +`Buffer pool hit rate` 字段代表**内存命中率**,表示 Buffer Pool 对查询的加速效果 + 核心参数: * `innodb_buffer_pool_size`:该变量决定了 Innodb 存储引擎表数据和索引数据的最大缓存区大小,默认 128M @@ -5441,8 +5443,6 @@ SHOW ENGINE INNODB STATUS\G innodb_log_buffer_size=10M ``` -Buffer Pool 中有一块内存叫 Change Buffer 用来对增删改操作提供缓存,可以通过参数 innodb_change_buffer_max_size 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% - 在多线程下,访问 Buffer Pool 中的各种链表都需要加锁,所以将 Buffer Pool 拆成若干个小实例,每个实例独立管理内存空间和各种链表(类似 ThreadLocal),多线程访问各实例互不影响,提高了并发能力 * 在系统启动时设置系统变量 `innodb_buffer_pool_instance` 可以指定 Buffer Pool 实例的个数,但是当 Buffer Pool 小于 1GB 时,设置多个实例时无效的 @@ -5456,6 +5456,30 @@ MySQL 5.7.5 之前 `innodb_buffer_pool_size` 只支持在系统启动时修改 +*** + + + +#### 其他内存 + +InnoDB 管理的 Buffer Pool 中有一块内存叫 Change Buffer 用来对**增删改**操作提供缓存,参数 `innodb_change_buffer_max_size ` 来动态设置,设置为 50 时表示 Change Buffer 的大小最多只能占用 Buffer Pool 的 50% + +Server 层针对优化**查询**的内存为 Net Buffer,内存的大小是由参数 `net_buffer_length`定义,默认 16k,实现流程: + +* 获取一行数据写入 Net Buffer,重复获取直到 Net Buffer 写满,调用网络接口发出去 +* 若发送成功就清空 Net Buffer,然后继续取下一行;若发送函数返回 `EAGAIN` 或 `WSAEWOULDBLOCK`,表示本地网络栈 `socket send buffer` 写满了,进入等待,直到网络栈重新可写再继续发送 + +![](https://gitee.com/seazean/images/raw/master/DB/MySQL-查询内存优化.png) + +MySQL 采用的是边算边发的逻辑,因此对于数据量很大的查询来说,不会在 Server 端保存完整的结果集,如果客户端读结果不及时,会堵住 MySQL 的查询过程,但是不会把内存打爆导致 OOM + + + + +参考文章:https://blog.csdn.net/qq_33589510/article/details/117673449 + + + *** diff --git a/Frame.md b/Frame.md index b5c4d88..b809388 100644 --- a/Frame.md +++ b/Frame.md @@ -3816,7 +3816,7 @@ public class Consumer { - 全局顺序:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。 适用于性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景 - 分区顺序:对于指定的一个 Topic,所有消息根据 sharding key 进行分区,同一个分组内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用于性能要求高,以 sharding key 作为分区字段,在同一个区中严格的按照 FIFO 原则进行消息发布和消费的场景 -在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列),而消费消息是从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当发送和消费参与的 queue 只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个 queue,消息都是有序的 +在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列),而消费消息是从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当发送和消费参与的 queue 只有一个,则是全局有序;如果多个queue 参与,则为分区有序,即相对每个 queue,消息都是有序的 @@ -4552,8 +4552,6 @@ RocketMQ 的工作流程: -### 消息存储 - #### 生产消费 At least Once:至少一次,指每个消息必须投递一次,Consumer 先 Pull 消息到本地,消费完成后才向服务器返回 ACK,如果没有消费一定不会 ACK 消息 @@ -4575,101 +4573,6 @@ At least Once:至少一次,指每个消息必须投递一次,Consumer 先 -*** - - - -#### 存储结构 - -RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 - -RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,消息真正的物理存储文件是 CommitLog,ConsumeQueue 是消息的逻辑队列,类似数据库的索引节点,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** - -每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) - -* 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 索引 - -RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 - -服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 - - - -**** - - - -#### 存储优化 - -##### 存储媒介 - -两种持久化的方案: - -* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 - -* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 - - 注意:磁盘的顺序读写要比随机读写快很多,可以匹配上网络的速度,RocketMQ 的消息采用的顺序写 - -页缓存(PageCache)是 OS 对文件的缓存,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,对读写访问操作进行了性能优化 - -* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 -* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理) - -在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 - - - -*** - - - -##### 内存映射 - -操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: - -* read:读取本地文件内容 - -* write:将读取的内容通过网络发送出去 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) - -补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 - -通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用零拷贝技术,提高消息存盘和网络发送的速度。 - -RocketMQ 主要通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 - -MappedByteBuffer 内存映射的方式限制一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 - - - -*** - - - -#### 刷盘机制 - -同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 - -异步刷盘:利用 OS 的 PageCache 的优势,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 - -通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) - - - -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md - - - - - **** @@ -4912,6 +4815,27 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** ### 消息重试 +#### 重投机制 + +生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 + +如下方法可以设置消息重投策略: + +- retryTimesWhenSendFailed:同步发送失败重投次数,默认为 2,因此生产者会最多尝试发送 retryTimesWhenSendFailed + 1 次。不会选择上次失败的 Broker,尝试向其他 Broker 发送,最大程度保证消息不丢。超过重投次数抛出异常,由客户端保证消息不丢。当出现 RemotingException、MQClientException 和部分 MQBrokerException 时会重投 +- retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他 Broker,仅在同一个 Broker 上做重试,不保证消息不丢 +- retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或 slave 不可用(返回状态非 SEND_OK),是否尝试发送到其他 Broker,默认 false,十分重要消息可以开启 + +注意点: + +* 如果同步模式发送失败,则选择到下一个 Broker,如果异步模式发送失败,则**只会在当前 Broker 进行重试** +* 发送消息超时时间默认3000毫秒,就不会再尝试重试 + + + +*** + + + #### 重试机制 Consumer 消费消息失败后,提供了一种重试机制,令消息再消费一次。Consumer 消费消息失败可以认为有以下几种情况: @@ -4927,7 +4851,7 @@ RocketMQ 会为每个消费组都设置一个 Topic 名称为 `%RETRY%+consumerG **无序消息情况下**,因为异常恢复需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ 对于重试消息的处理是先保存至 Topic 名称为 `SCHEDULE_TOPIC_XXXX` 的延迟队列中,后台定时任务按照对应的时间进行 Delay 后重新保存至 `%RETRY%+consumerGroup` 的重试队列中 -消息队列 RocketMQ 默认允许每条消息最多重试 16 次,每次重试的间隔时间如下: +消息队列 RocketMQ 默认允许每条消息最多重试 16 次,每次重试的间隔时间如下表示: | 第几次重试 | 与上次重试的间隔时间 | 第几次重试 | 与上次重试的间隔时间 | | :--------: | :------------------: | :--------: | :------------------: | @@ -4942,7 +4866,9 @@ RocketMQ 会为每个消费组都设置一个 Topic 名称为 `%RETRY%+consumerG 如果消息重试 16 次后仍然失败,消息将**不再投递**,如果严格按照上述重试时间间隔计算,某条消息在一直消费失败的前提下,将会在接下来的 4 小时 46 分钟之内进行 16 次重试,超过这个时间范围消息将不再重试投递 -说明:一条消息无论重试多少次,消息的 Message ID 是不会改变的 +时间间隔不支持自定义配置,最大重试次数可通过自定义参数 `MaxReconsumeTimes` 取值进行配置,若配置超过 16 次,则超过的间隔时间均为 2 小时 + +说明:一条消息无论重试多少次,**消息的 Message ID 是不会改变的** @@ -5024,26 +4950,6 @@ public class MessageListenerImpl implements MessageListener { -*** - - - -#### 重投机制 - -生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 - -消息重复在 RocketMQ 中是无法避免的问题,如下方法可以设置消息重投策略: - -- retryTimesWhenSendFailed:同步发送失败重投次数,默认为 2,因此生产者会最多尝试发送 retryTimesWhenSendFailed + 1 次。不会选择上次失败的 Broker,尝试向其他 Broker 发送,最大程度保证消息不丢。超过重投次数抛出异常,由客户端保证消息不丢。当出现 RemotingException、MQClientException 和部分 MQBrokerException 时会重投 -- retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他 Broker,仅在同一个 Broker 上做重试,不保证消息不丢 -- retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或 slave 不可用(返回状态非 SEND_OK),是否尝试发送到其他 Broker,默认 false,十分重要消息可以开启 - -注意点: - -* 如果同步模式发送失败,则选择到下一个 Broker,如果异步模式发送失败,则**只会在当前 Broker 进行重试** - -* 发送消息超时时间默认3000毫秒,就不会再尝试重试 - *** @@ -5081,7 +4987,7 @@ public class MessageListenerImpl implements MessageListener { 消息队列 RocketMQ 消费者在接收到消息以后,需要根据业务上的唯一 Key 对消息做幂等处理 -在互联网应用中,尤其在网络不稳定的情况下,消息队列 RocketMQ 的消息有可能会出现重复,几种情况: +At least Once 机制保证消息不丢失,但是可能会造成消息重复,RocketMQ 中无法避免消息重复(Exactly-Once),在互联网应用中,尤其在网络不稳定的情况下,几种情况: - 发送时消息重复:当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或客户端宕机,导致服务端对客户端应答失败。此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息 @@ -5150,11 +5056,13 @@ public class MessageListenerImpl implements MessageListener { -## 源码分析 +## 原理解析 ### 服务端 -#### 启动方法 +#### 服务启动 + +##### 启动方法 NamesrvStartup 类中有 Namesrv 服务的启动方法: @@ -5221,7 +5129,7 @@ NamesrvStartup#start:启动 Namesrv 控制器 -#### 控制器类 +##### 控制器类 NamesrvController 用来初始化和启动 Namesrv 服务器 @@ -5899,7 +5807,7 @@ DefaultMQProducer 是生产者的默认实现类 -#### 实现者类 +#### 默认实现 ##### 成员属性 @@ -5920,19 +5828,6 @@ DefaultMQProducerImpl 类是默认的生产者实现类 private final ConcurrentMap topicPublishInfoTable = new ConcurrentHashMap(); ``` - ```java - public class TopicPublishInfo { - private boolean orderTopic = false; - private boolean haveTopicRouterInfo = false; - // 主题的全部队列 - private List messageQueueList = new ArrayList(); - // 使用 ThreaLocal 保存发送消息使用的队列 - private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); - // 主题的路由数据 - private TopicRouteData topicRouteData; - } - ``` - * 异步发送消息:相关信息 ```java @@ -6078,13 +5973,13 @@ DefaultMQProducerImpl 类是默认的生产者实现类 * `if (this.sendLatencyFaultEnable)`:默认不开启,可以通过配置开启 * `return tpInfo.selectOneMessageQueue(lastBrokerName)`:默认选择队列的方式,就是循环主题全部的队列 - * `int index = this.sendWhichQueue.getAndIncrement()`:选择队列的索引 - * `int pos = Math.abs(index) % this.messageQueueList.size()`:获取该索引对应的队列位置 - * `return this.messageQueueList.get(pos)`:返回消息队列 - + * `brokersSent[times] = mq.getBrokerName()`:将本次选择的 BrokerName 存入数组 + * `msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()))`:**重投的消息需要加上标记** + * `sendResult = this.sendKernelImpl`:核心发送方法 + * `this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false)`:更新一下时间 * `switch (communicationMode)`:异步或者单向消息直接返回 null,同步发送进入逻辑判断 @@ -6152,7 +6047,107 @@ DefaultMQProducerImpl 类是默认的生产者实现类 } ``` - + + + +*** + + + +#### 路由信息 + +TopicPublishInfo 类用来存储路由信息 + +成员变量: + +* 顺序消息: + + ```java + private boolean orderTopic = false; + ``` + +* 消息队列: + + ```java + private List messageQueueList = new ArrayList<>(); // 主题全部的消息队列 + private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); // 消息队列索引 + ``` + + ```java + // 【消息队列类】 + public class MessageQueue implements Comparable, Serializable { + private String topic; + private String brokerName; + private int queueId;// 队列 ID + } + ``` + +* 路由数据:主题对应的路由数据 + + ```java + private TopicRouteData topicRouteData; + ``` + + ```java + public class TopicRouteData extends RemotingSerializable { + private String orderTopicConf; + private List queueDatas; // 队列数据 + private List brokerDatas; // Broker 数据 + private HashMap/* Filter Server */> filterServerTable; + } + ``` + + ```java + public class QueueData implements Comparable { + private String brokerName; // 节点名称 + private int readQueueNums; // 读队列数 + private int writeQueueNums; // 写队列数 + private int perm; // 权限 + private int topicSynFlag; + } + ``` + + ```java + public class BrokerData implements Comparable { + private String cluster; // 集群名 + private String brokerName; // Broker节点名称 + private HashMap brokerAddrs; + } + ``` + +核心方法: + +* selectOneMessageQueue():**选择消息队列**使用 + + ```java + // 参数是上次失败时的 brokerName,可以为 null + public MessageQueue selectOneMessageQueue(final String lastBrokerName) { + if (lastBrokerName == null) { + return selectOneMessageQueue(); + } else { + // 遍历消息队列 + for (int i = 0; i < this.messageQueueList.size(); i++) { + // 获取队列的索引 + int index = this.sendWhichQueue.getAndIncrement(); + // 获取队列的下标位置 + int pos = Math.abs(index) % this.messageQueueList.size(); + if (pos < 0) + pos = 0; + // 获取消息队列 + MessageQueue mq = this.messageQueueList.get(pos); + // 与上次选择的不同就可以返回 + if (!mq.getBrokerName().equals(lastBrokerName)) { + return mq; + } + } + return selectOneMessageQueue(); + } + } + ``` + + + + **** @@ -6604,9 +6599,379 @@ NettyRemotingClient 类负责客户端的网络通信 +*** + + + +### 存储端 + +#### 存储机制 + +##### 存储结构 + +RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 + +RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,CommitLog 是消息真正的**物理存储**文件,ConsumeQueue 是消息的逻辑队列,类似数据库的**索引节点**,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** + +每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) + +* 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 索引 + +RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 + +服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 + + + +**** + + + +##### 存储优化 + +###### 内存映射 + +操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: + +* read:读取本地文件内容 + +* write:将读取的内容通过网络发送出去 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) + +补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 + +通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用**零拷贝技术**,提高消息存盘和网络发送的速度。 + +RocketMQ 通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 + +MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 + + + +*** + + + +###### 页缓存 + +页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** + +* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 +* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理,最大 128K) + +在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 + + + +*** + + + +##### 刷盘机制 + +两种持久化的方案: + +* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 +* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 + +RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使用**顺序 IO**,因为磁盘的顺序读写要比随机读写快很多 + +* 同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 + +* 异步刷盘:利用 OS 的 PageCache,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 + +通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + + + +*** + + + +#### MappedFile + +##### 成员属性 + +MappedFile 类是最基础的存储类,继承自 ReferenceResource 类,用来**保证线程安全** + +MappedFile 类成员变量: + +* 内存相关: + + ```java + public static final int OS_PAGE_SIZE = 1024 * 4;// 内存页大小:默认是 4k + private AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY; // 当前进程下所有的 mappedFile 占用的总虚拟内存大小 + private AtomicInteger TOTAL_MAPPED_FILES; // 当前进程下所有的 mappedFile 个数 + ``` + +* 数据位点: + + ```java + protected final AtomicInteger wrotePosition; // 当前 mappedFile 的数据写入点 + protected final AtomicInteger committedPosition;// 当前 mappedFile 的数据提交点 + private final AtomicInteger flushedPosition; // 数据落盘位点,在这之前的数据是持久化的安全数据 + // flushedPosition-wrotePosition 之间的数据属于脏页 + ``` + +* 文件相关:CL 是 CommitLog,CQ 是 ConsumeQueue + + ```java + private String fileName; // 文件名称,CL和CQ文件名是第一条消息的物理偏移量,索引文件是年月日时分秒 + private long fileFromOffset;// 文件名转long,代表该对象的【起始偏移量】 + private File file; // 文件对象 + ``` + +* 内存映射: + + ```java + protected FileChannel fileChannel; // 文件通道 + private MappedByteBuffer mappedByteBuffer; // 内存映射缓冲区,访问虚拟内存 + ``` + +ReferenceResource 类成员变量: + +* 引用数量:当 `refCount <= 0` 时,表示该资源可以释放了,没有任何其他程序依赖它了,用原子类保证线程安全 + + ```java + protected final AtomicLong refCount = new AtomicLong(1); // 初始值为 1 + ``` + +* 存活状态:表示资源的存活状态 + + ```java + protected volatile boolean available = true; + ``` + +* 是否清理:默认值 false,当执行完子类对象的 cleanup() 清理方法后,该值置为 true ,表示资源已经全部释放 + + ```java + protected volatile boolean cleanupOver = false; + ``` + +* 第一次关闭资源的时间:用来记录超时时间 + + ```java + private volatile long firstShutdownTimestamp = 0; + ``` + + + +*** + + + +##### 成员方法 + +MappedFile 类核心方法: + +* appendMessage():提供上层向内存映射中追加消息的方法,消息如何追加由 AppendMessageCallback 控制 + + ```java + // 参数一:消息 参数二:追加消息回调 + public AppendMessageResult appendMessage(MessageExtBrokerInner msg, AppendMessageCallback cb) + ``` + + ```java + // 将字节数组写入到文件通道 + public boolean appendMessage(final byte[] data) + ``` + +* flush():刷盘接口,参数 flushLeastPages 代表刷盘的最小页数 ,等于 0 时属于强制刷盘;> 0 时需要脏页(计算方法在数据位点)达到该值才进行物理刷盘;文件写满时强制刷盘 + + ```java + public int flush(final int flushLeastPages) + ``` + +* selectMappedBuffer():该方法以 pos 为开始位点 ,到有效数据为止,创建一个切片 ByteBuffer 作为数据副本,供业务访问数据 + + ```java + public SelectMappedBufferResult selectMappedBuffer(int pos) + ``` + +* destroy():销毁映射文件对象,并删除关联的系统文件,参数是强制关闭资源的时间 + + ```java + public boolean destroy(final long intervalForcibly) + ``` + +* cleanup():释放堆外内存,更新总虚拟内存和总内存映射文件数 + + ```java + public boolean cleanup(final long currentRef) + ``` + +* warmMappedFile():内存预热,当要新建的 MappedFile 对象大于 1g 时,执行该方法对该 MappedFile 的每个 Page Cache 进行写入一个字节进行分配内存,**将映射文件都加载到内存** + + ```java + public void warmMappedFile(FlushDiskType type, int pages) + ``` + +ReferenceResource 类核心方法: + +* hold():增加引用记数 refCount,方法加锁 + + ```java + public synchronized boolean hold() + ``` + +* shutdown():关闭资源,参数代表强制关闭资源的时间间隔 + + ```java + // 系统当前时间 - firstShutdownTimestamp 时间 > intervalForcibly 进行【强制关闭】 + public void shutdown(final long intervalForcibly) + ``` + +* release():引用计数减 1,当 refCount 为 0 时,调用子类的 cleanup 方法 + + ```java + public void release() + ``` + + + + + + + +*** + + + +#### MapQueue + +MappedFileQueue 用来管理 MappedFile 文件 + +成员变量: + +* 管理目录:CommitLog 是 `../store/commitlog`, ConsumeQueue 是 `../store/xxx_topic/0` + + ```java + private final String storePath; + ``` + +* 文件属性: + + ```java + private final int mappedFileSize; // 目录下每个文件大小,CL文件默认 1g,CQ文件 默认 600w字节 + private final CopyOnWriteArrayList mappedFiles; //目录下的每个 mappedFile 都加入该集合 + ``` + +* 数据位点: + + ```java + private long flushedWhere = 0; // 目录的刷盘位点,值为 mf.fileName + mf.wrotePosition + private long committedWhere = 0; // 目录的提交位点 + ``` + +* 消息存储: + + ```java + private volatile long storeTimestamp = 0; // 当前目录下最后一条 msg 的存储时间 + ``` + +* 创建服务:新建 MappedFile 实例,继承自 ServiceThread 是一个任务对象,run 方法用来创建实例 + + ```java + private final AllocateMappedFileService allocateMappedFileService; + ``` + +核心方法: + +* load():Broker 启动时,加载本地磁盘数据,该方法读取 storePath 目录下的文件,创建 MappedFile 对象放入集合内 + + ```java + public boolean load() + ``` + +* getLastMappedFile():获取当前正在顺序写入的 MappedFile 对象,如果最后一个 MappedFile 写满了,或者不存在 MappedFile 对象,则创建新的 MappedFile + + ```java + // 参数一:文件起始偏移量;参数二:当list为空时,是否新建 MappedFile + public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) + ``` + +* flush():根据 flushedWhere 属性查找合适的 MappedFile,调用该 MappedFile 的落盘方法,并更新全局的 flushedWhere + + ```java + //参数:0 表示强制刷新, > 0 脏页数据必须达到 flushLeastPages 才刷新 + public boolean flush(final int flushLeastPages) + ``` + +* findMappedFileByOffset():根据偏移量查询对象 + + ```java + public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) + ``` + +* deleteExpiredFileByTime():CL 删除过期文件,根据文件的保留时长决定是否删除 + + ```java + // 参数一:过期时间; 参数二:删除两个文件之间的时间间隔; 参数三:mf.destory传递的参数; 参数四:true 强制删除 + public int deleteExpiredFileByTime(final long expiredTime,final int deleteFilesInterval, final long intervalForcibly, final boolean cleanImmediately) + ``` + +* deleteExpiredFileByOffset():CQ 删除过期文件,遍历每个 MF 文件,获取当前文件最后一个数据单元的物理偏移量,小于 offset 说明当前 MF 文件内都是过期数据 + + ```java + // 参数一:consumeLog 目录下最小物理偏移量,就是第一条消息的 offset; + // 参数二:ConsumerQueue 文件内每个数据单元固定大小 + public int deleteExpiredFileByOffset(long offset, int unitSize) + ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +*** +## TEST diff --git a/Java.md b/Java.md index edc5ccd..c9daa70 100644 --- a/Java.md +++ b/Java.md @@ -10218,7 +10218,7 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 #### 两种方式 -为对象分配内存:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 +不分配内存的对象无法进行其他操作,JVM 为对象分配内存的过程:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 * 如果内存规整,使用指针碰撞(Bump The Pointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 * 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配。已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 diff --git a/Prog.md b/Prog.md index 36c0ed0..dc6f5e3 100644 --- a/Prog.md +++ b/Prog.md @@ -2292,7 +2292,7 @@ Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互 可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值 -存在不可见问题的根本原因是由于缓存的存在,线程持有的是共享变量的副本,无法感知其他线程对于共享变量的更改,导致读取的值不是最新的 +存在不可见问题的根本原因是由于缓存的存在,线程持有的是共享变量的副本,无法感知其他线程对于共享变量的更改,导致读取的值不是最新的。但是 final 修饰的变量是**不可变**的,就算有缓存,也不会存在不可见的问题 main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止: @@ -3613,7 +3613,7 @@ final 变量的赋值通过 putfield 指令来完成,在这条指令之后也 不可变:如果一个对象不能够修改其内部状态(属性),那么就是不可变对象 -不可变对象线程安全的,因为不存在并发修改,是另一种避免竞争的方式 +不可变对象线程安全的,不存在并发修改和可见性问题,是另一种避免竞争的方式 String 类也是不可变的,该类和类中所有属性都是 final 的 @@ -13752,7 +13752,7 @@ DMA (Direct Memory Access) :直接存储器访问,让外部设备不通过 C 把内存数据传输到网卡然后发送: * 没有 DMA:CPU 读内存数据到 CPU 高速缓存,再写到网卡,这样就把 CPU 的速度拉低到和网卡一个速度 -* 使用 DMA:把数据读到 Socket 内核缓存区(CPU 复制),CPU 分配给 DMA 开始**异步**操作,DMA 读取 Socket 缓冲区到 DMA 缓冲区,然后写到网卡。DMA 执行完后中断(就是通知) CPU,这时 Socket 内核缓冲区为空,CPU 从用户态切换到内核态,执行中断处理程序,将需要使用 Socket 缓冲区的阻塞进程移到就绪队列 +* 使用 DMA:把数据读到 Socket 内核缓存区(CPU 复制),CPU 分配给 DMA 开始**异步**操作,DMA 读取 Socket 缓冲区到 DMA 缓冲区,然后写到网卡。DMA 执行完后**中断**(就是通知) CPU,这时 Socket 内核缓冲区为空,CPU 从用户态切换到内核态,执行中断处理程序,将需要使用 Socket 缓冲区的阻塞进程移到就绪队列 一个完整的 DMA 传输过程必须经历 DMA 请求、DMA 响应、DMA 传输、DMA 结束四个步骤: @@ -13797,7 +13797,7 @@ read 调用图示:read、write 都是系统调用指令 #### mmap -mmap(Memory Mapped Files)加 write 实现零拷贝,**零拷贝就是没有数据从内核空间复制到用户空间** +mmap(Memory Mapped Files)内存映射加 write 实现零拷贝,**零拷贝就是没有数据从内核空间复制到用户空间** 用户空间和内核空间都使用内存,所以可以共享同一块物理内存地址,省去用户态和内核态之间的拷贝。写网卡时,共享空间的内容拷贝到 Socket 缓冲区,然后交给 DMA 发送到网卡,只需要 3 次复制 @@ -14902,8 +14902,8 @@ FileChannel 提供 map 方法返回 MappedByteBuffer 对象,把文件映射到 FileChannel 中的成员属性: * MapMode.mode:内存映像文件访问的方式,共三种: - * `MapMode.READ_ONLY`:只读,试图修改得到的缓冲区将导致抛出异常。 - * `MapMode.READ_WRITE`:读/写,对得到的缓冲区的更改最终将写入文件;但该更改对映射到同一文件的其他程序不一定是可见的 + * `MapMode.READ_ONLY`:只读,修改得到的缓冲区将导致抛出异常 + * `MapMode.READ_WRITE`:读/写,对缓冲区的更改最终将写入文件,但此次修改对映射到同一文件的其他程序不一定是可见的 * `MapMode.PRIVATE`:私用,可读可写,但是修改的内容不会写入文件,只是 buffer 自身的改变,称之为写时复制 * `public final FileLock lock()`:获取此文件通道的排他锁 @@ -14915,7 +14915,7 @@ MappedByteBuffer,可以让文件在直接内存(堆外内存)中进行修 MappedByteBuffer 较之 ByteBuffer 新增的三个方法: -- `final MappedByteBuffer force()`:缓冲区是 READ_WRITE 模式下,对缓冲区内容的修改强行写入文件 +- `final MappedByteBuffer force()`:缓冲区是 READ_WRITE 模式下,对缓冲区内容的修改**强制写入文件** - `final MappedByteBuffer load()`:将缓冲区的内容载入物理内存,并返回该缓冲区的引用 - `final boolean isLoaded()`:如果缓冲区的内容在物理内存中,则返回真,否则返回假 @@ -14924,7 +14924,7 @@ public class MappedByteBufferTest { public static void main(String[] args) throws Exception { // 读写模式 RandomAccessFile ra = new RandomAccessFile("1.txt", "rw"); - //获取对应的通道 + // 获取对应的通道 FileChannel channel = ra.getChannel(); /** From 68e76ef7463339f7198bc668aa7b0d017c95e119 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 9 Jan 2022 22:14:49 +0800 Subject: [PATCH 056/122] Update Java Notes --- Frame.md | 2354 ++++++++++++++++++++++++++++++++++++------------------ Java.md | 2 +- 2 files changed, 1589 insertions(+), 767 deletions(-) diff --git a/Frame.md b/Frame.md index b809388..6f84a04 100644 --- a/Frame.md +++ b/Frame.md @@ -3485,6 +3485,8 @@ RCVBUF_ALLOCATOR:属于 SocketChannal 参数 +参考视频:https://www.bilibili.com/video/BV1L4411y7mn + **** @@ -4494,7 +4496,7 @@ public class Producer { ## 系统特性 -### 工作机制 +### 工作流程 #### 模块介绍 @@ -4533,7 +4535,7 @@ Broker 包含了以下几个重要子模块: -#### 工作流程 +#### 总体流程 RocketMQ 的工作流程: @@ -4573,47 +4575,6 @@ At least Once:至少一次,指每个消息必须投递一次,Consumer 先 -**** - - - -### 消息查询 - -#### Message ID - -RocketMQ 支持按照两种维度进行消息查询:按照 Message ID 查询消息、按照 Message Key 查询消息 - -RocketMQ 中的 MessageId 的长度总共有 16 字节,其中包含了消息存储主机地址(IP 地址和端口),消息 Commit Log offset - -实现方式:Client 端从 MessageId 中解析出 Broker 的地址(IP 地址和端口)和 Commit Log 的偏移地址,封装成一个 RPC 请求后通过 Remoting 通信层发送(业务请求码 VIEW_MESSAGE_BY_ID)。Broker 端走的是 QueryMessageProcessor,读取消息的过程用其中的 CommitLog 的 offset 和 size 去 CommitLog 中找到真正的记录并解析成一个完整的消息返回 - - - -*** - - - -#### Message Key - -按照 Message Key 查询消息,主要是基于 RocketMQ 的 IndexFile 索引文件来实现的,RocketMQ 的索引文件逻辑结构,类似 JDK 中 HashMap 的实现,具体结构如下: - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-IndexFile索引文件.png) - -IndexFile 索引文件为提供了通过 Message Key 查询消息的服务,IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName 是以创建时的时间戳命名,文件大小是固定的,等于 `40+500W*4+2000W*20= 420000040` 个字节大小。如果消息的 properties 中设置了 UNIQ_KEY 这个属性,就用 `topic + “#” + UNIQ_KEY` 作为 key 来做写入操作;如果消息设置了 KEYS 属性(多个 KEY 以空格分隔),也会用 `topic + “#” + KEY` 来做索引 - -整个 Index File 的结构如图,40 Byte 的 Header 用于保存一些总的统计信息,`4*500W` 的 Slot Table 并不保存真正的索引数据,而是保存每个槽位对应的单向链表的头,即一个 Index File 可以保存 2000W 个索引,`20*2000W` 是真正的索引数据 - -索引数据包含了 Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段,一共 20 Byte - -* NextIndex offset 即前面读出来的 slotValue,如果有 hash 冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来 -* Timestamp 记录的是消息 storeTimestamp 之间的差,并不是一个绝对的时间 - -实现方式:通过 Broker 端的 QueryMessageProcessor 业务处理器来查询,读取消息的过程用 Topic 和 Key 找到 IndexFile 索引文件中的一条记录,根据其中的 CommitLog Offset 从 CommitLog 文件中读取消息的实体内容 - - - - - *** @@ -4813,9 +4774,27 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** -### 消息重试 +### 消息机制 + +#### 消息查询 + +RocketMQ 支持按照两种维度进行消息查询:按照 Message ID 查询消息、按照 Message Key 查询消息 + +* RocketMQ 中的 MessageID 的长度总共有 16 字节,其中包含了消息存储主机地址(IP 地址和端口),消息 Commit Log offset + + 实现方式:Client 端从 MessageID 中解析出 Broker 的地址(IP 地址和端口)和 Commit Log 的偏移地址,封装成一个 RPC 请求后通过 Remoting 通信层发送(业务请求码 VIEW_MESSAGE_BY_ID)。Broker 端走的是 QueryMessageProcessor,读取消息的过程用其中的 CommitLog 的 offset 和 size 去 CommitLog 中找到真正的记录并解析成一个完整的消息返回 + +* 按照 Message Key 查询消息,IndexFile 索引文件为提供了通过 Message Key 查询消息的服务 -#### 重投机制 + 实现方式:通过 Broker 端的 QueryMessageProcessor 业务处理器来查询,读取消息的过程用 Topic 和 Key 找到 IndexFile 索引文件中的一条记录,根据其中的 CommitLog Offset 从 CommitLog 文件中读取消息的实体内容 + + + +*** + + + +#### 消息重投 生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 @@ -4836,7 +4815,7 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** -#### 重试机制 +#### 消息重试 Consumer 消费消息失败后,提供了一种重试机制,令消息再消费一次。Consumer 消费消息失败可以认为有以下几种情况: @@ -5165,7 +5144,7 @@ NamesrvController 用来初始化和启动 Namesrv 服务器 @Override public void run() { // 扫描 brokerLiveTable 表,将两小时没有活动的 broker 关闭, - //通过 next.getKey() 获取 broker 的地址,然后【关闭服务器与broker物理节点的 channel】 + // 通过 next.getKey() 获取 broker 的地址,然后【关闭服务器与broker物理节点的 channel】 NamesrvController.this.routeInfoManager.scanNotActiveBroker(); } }, 5, 10, TimeUnit.SECONDS); @@ -5696,356 +5675,359 @@ RouteInfoManager#registerBroker:注册 Broker 的信息 + + **** -### 生产者 +### 存储端 -#### 生产者类 +#### 存储机制 -DefaultMQProducer 是生产者的默认实现类 +##### 存储结构 -成员变量: +RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 -* 生产者实现类: +RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,CommitLog 是消息真正的**物理存储**文件,ConsumeQueue 是消息的逻辑队列,类似数据库的**索引节点**,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** - ```java - protected final transient DefaultMQProducerImpl defaultMQProducerImpl - ``` +每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 -* 生产者组:发送事务消息,Broker 端进行事务回查(补偿机制)时,选择当前生产者组的下一个生产者进行事务回查 +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) - ```java - private String producerGroup; - ``` +* 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 索引** -* 默认主题:isAutoCreateTopicEnable 开启时,当发送消息指定的 Topic 在 Namesrv 未找到路由信息,使用该值创建 Topic 信息 +RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 - ```java - private String createTopicKey = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC; - // 值为【TBW102】,Just for testing or demo program - ``` +服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 -* 消息重投:系统特性消息重试部分详解了三个参数的作用 - ```java - private int retryTimesWhenSendFailed = 2; // 同步发送失败后重试的发送次数,加上第一次发送,一共三次 - private int retryTimesWhenSendAsyncFailed = 2; // 异步 - private boolean retryAnotherBrokerWhenNotStoreOK = false; // 消息未存储成功,选择其他 Broker 重试 - ``` -* 消息队列: +**** - ```java - private volatile int defaultTopicQueueNums = 4; // 默认 Broker 创建的队列数 - ``` -* 消息属性: - ```java - private int sendMsgTimeout = 3000; // 发送消息的超时限制 - private int compressMsgBodyOverHowmuch = 1024 * 4; // 压缩阈值,当 msg body 超过 4k 后使用压缩 - private int maxMessageSize = 1024 * 1024 * 4; // 消息体的最大限制,默认 4M - private TraceDispatcher traceDispatcher = null; // 消息轨迹 +##### 存储优化 -构造方法: +###### 内存映射 -* 构造方法: +操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: - ```java - public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook) { - this.namespace = namespace; - this.producerGroup = producerGroup; - // 创建生产者实现对象 - defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook); - } - ``` +* read:读取本地文件内容 -成员方法: +* write:将读取的内容通过网络发送出去 -* start():启动方法 +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) - ```java - public void start() throws MQClientException { - // 重置生产者组名,如果传递了命名空间,则 【namespace%group】 - this.setProducerGroup(withNamespace(this.producerGroup)); - // 生产者实现对象启动 - this.defaultMQProducerImpl.start(); - if (null != traceDispatcher) { - // 消息轨迹的逻辑 - traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel()); - } - } - ``` +补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 -* send():**发送消息**: +通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用**零拷贝技术**,提高消息存盘和网络发送的速度。 - ```java - public SendResult send(Message msg){ - // 校验消息 - Validators.checkMessage(msg, this); - // 设置消息 Topic - msg.setTopic(withNamespace(msg.getTopic())); - return this.defaultMQProducerImpl.send(msg); - } - ``` +RocketMQ 通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 -* request():请求方法,**需要消费者回执消息**,又叫回退消息 +MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 - ```java - public Message request(final Message msg, final MessageQueue mq, final long timeout) { - msg.setTopic(withNamespace(msg.getTopic())); - return this.defaultMQProducerImpl.request(msg, mq, timeout); - } - ``` +*** + + + +###### 页缓存 + +页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** + +* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 +* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理,最大 128K) + +在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 + *** -#### 默认实现 +##### 刷盘机制 + +两种持久化的方案: + +* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 +* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 + +RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使用**顺序 IO**,因为磁盘的顺序读写要比随机读写快很多 + +* 同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 + +* 异步刷盘:利用 OS 的 PageCache,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 + +通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + + + +*** + + + +#### MappedFile ##### 成员属性 -DefaultMQProducerImpl 类是默认的生产者实现类 +MappedFile 类是最基础的存储类,继承自 ReferenceResource 类,用来**保证线程安全** -成员变量: +MappedFile 类成员变量: -* 实例对象: +* 内存相关: ```java - private final DefaultMQProducer defaultMQProducer; // 持有默认生产者对象,用来获取对象中的配置信息 - private MQClientInstance mQClientFactory; // 客户端实例对象,生产者启动后需要注册到该客户端对象内 + public static final int OS_PAGE_SIZE = 1024 * 4;// 内存页大小:默认是 4k + private AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY; // 当前进程下所有的 mappedFile 占用的总虚拟内存大小 + private AtomicInteger TOTAL_MAPPED_FILES; // 当前进程下所有的 mappedFile 个数 ``` -* 主题发布信息映射表:key 是 Topic,value 是发布信息 +* 数据位点: ```java - private final ConcurrentMap topicPublishInfoTable = new ConcurrentHashMap(); + protected final AtomicInteger wrotePosition; // 当前 mappedFile 的数据写入点 + protected final AtomicInteger committedPosition;// 当前 mappedFile 的数据提交点 + private final AtomicInteger flushedPosition; // 数据落盘位点,在这之前的数据是持久化的安全数据 + // flushedPosition-wrotePosition 之间的数据属于脏页 ``` -* 异步发送消息:相关信息 +* 文件相关:CL 是 CommitLog,CQ 是 ConsumeQueue ```java - private final BlockingQueue asyncSenderThreadPoolQueue;// 异步发送消息,异步线程池使用的队列 - private final ExecutorService defaultAsyncSenderExecutor; // 异步发送消息默认使用的线程池 - private ExecutorService asyncSenderExecutor; // 异步消息发送线程池,指定后就不使用默认线程池了 + private String fileName; // 文件名称,CL和CQ文件名是第一条消息的物理偏移量,索引文件是年月日时分秒 + private long fileFromOffset;// 文件名转long,代表该对象的【起始偏移量】 + private File file; // 文件对象 ``` -* 定时器:执行定时任务 + **MF 中以物理偏移量作为文件名,可以更好的寻址和进行判断** + +* 内存映射: ```java - private final Timer timer = new Timer("RequestHouseKeepingService", true); // 守护线程 + protected FileChannel fileChannel; // 文件通道 + private MappedByteBuffer mappedByteBuffer; // 内存映射缓冲区,访问虚拟内存 ``` -* 状态信息:服务的状态,默认创建状态 +ReferenceResource 类成员变量: + +* 引用数量:当 `refCount <= 0` 时,表示该资源可以释放了,没有任何其他程序依赖它了,用原子类保证线程安全 ```java - private ServiceState serviceState = ServiceState.CREATE_JUST; + protected final AtomicLong refCount = new AtomicLong(1); // 初始值为 1 ``` -* 压缩等级:ZIP 压缩算法的等级,默认是 5,越高压缩效果好,但是压缩的更慢 +* 存活状态:表示资源的存活状态 ```java - private int zipCompressLevel = Integer.parseInt(System.getProperty..., "5")); + protected volatile boolean available = true; ``` -* 容错策略:选择队列的容错策略 +* 是否清理:默认值 false,当执行完子类对象的 cleanup() 清理方法后,该值置为 true ,表示资源已经全部释放 ```java - private MQFaultStrategy mqFaultStrategy = new MQFaultStrategy(); + protected volatile boolean cleanupOver = false; ``` -* 钩子:用来进行前置或者后置处理 +* 第一次关闭资源的时间:用来记录超时时间 ```java - ArrayList sendMessageHookList; // 发送消息的钩子,留给用户扩展使用 - ArrayList checkForbiddenHookList; // 对比上面的钩子,可以抛异常,控制消息是否可以发送 - private final RPCHook rpcHook; // 传递给 NettyRemotingClient + private volatile long firstShutdownTimestamp = 0; ``` -构造方法: + -* 默认构造: +*** - ```java - public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer) { - // 默认 RPC HOOK 是空 - this(defaultMQProducer, null); - } - ``` -* 有参构造: + +##### 成员方法 + +MappedFile 类核心方法: + +* appendMessage():提供上层向内存映射中追加消息的方法,消息如何追加由 AppendMessageCallback 控制 ```java - public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer, RPCHook rpcHook) { - // 属性赋值 - this.defaultMQProducer = defaultMQProducer; - this.rpcHook = rpcHook; - - // 创建【异步消息线程池任务队列】,长度是 5w - this.asyncSenderThreadPoolQueue = new LinkedBlockingQueue(50000); - // 创建默认的异步消息任务线程池 - this.defaultAsyncSenderExecutor = new ThreadPoolExecutor( - // 核心线程数和最大线程数都是 系统可用的计算资源(8核16线程的系统就是 16)... - } + // 参数一:消息 参数二:追加消息回调 + public AppendMessageResult appendMessage(MessageExtBrokerInner msg, AppendMessageCallback cb) ``` - + ```java + // 将字节数组写入到文件通道 + public boolean appendMessage(final byte[] data) + ``` -**** +* flush():刷盘接口,参数 flushLeastPages 代表刷盘的最小页数 ,等于 0 时属于强制刷盘;> 0 时需要脏页(计算方法在数据位点)达到该值才进行物理刷盘;文件写满时强制刷盘 + ```java + public int flush(final int flushLeastPages) + ``` +* selectMappedBuffer():该方法以 pos 为开始位点 ,到有效数据为止,创建一个切片 ByteBuffer 作为数据副本,供业务访问数据 -##### 成员方法 + ```java + public SelectMappedBufferResult selectMappedBuffer(int pos) + ``` -* start():启动方法,参数默认是 true,代表正常的启动路径 +* destroy():销毁映射文件对象,并删除关联的系统文件,参数是强制关闭资源的时间 - * `this.serviceState = ServiceState.START_FAILED`:先修改为启动失败,成功后再修改,这种思想很常见 + ```java + public boolean destroy(final long intervalForcibly) + ``` - * `this.checkConfig()`:判断生产者组名不能是空,也不能是 default_PRODUCER +* cleanup():释放堆外内存,更新总虚拟内存和总内存映射文件数 - * `if (!getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP))`:条件成立说明当前生产者不是内部产生者,内部生产者是**处理消息回退**的这种情况使用的生产者 + ```java + public boolean cleanup(final long currentRef) + ``` - `this.defaultMQProducer.changeInstanceNameToPID()`:正常的生产者,修改生产者实例名称为当前进程的 PID +* warmMappedFile():内存预热,当要新建的 MappedFile 对象大于 1g 时,执行该方法对该 MappedFile 的每个 Page Cache 进行写入一个字节进行分配内存,**将映射文件全部加载到内存** - * ` this.mQClientFactory = ...`:获取当前进程的 MQ 客户端实例对象,从 factoryTable 中获取 key 为 客户端 ID,格式是`ip@pid`,**一个 JVM 进程只有一个 PID,也只有一个 MQClientInstance** + ```java + public void warmMappedFile(FlushDiskType type, int pages) + ``` - * `boolean registerOK = mQClientFactory.registerProducer(...)`:将生产者注册到 RocketMQ 客户端实例内 +* mlock():锁住指定的内存区域避免被操作系统调到 **swap 空间**,一次性将一段数据读入到映射内存区域,减少了缺页异常的产生 - * `this.topicPublishInfoTable.put(...)`:添加一个主题发布信息,key 是 **TBW102** ,value 是一个空对象 + ```java + public void mlock() + ``` - * `if (startFactory) `:正常启动路径 + swap space 是磁盘上的一块区域,可以是一个分区或者一个文件或者是组合。当系统物理内存不足时,Linux 会将内存中不常访问的数据保存到 swap 区域上,这样系统就可以有更多的物理内存为各个进程服务,而当系统需要访问 swap 上存储的内容时,需要通过**缺页中断**将 swap 上的数据加载到内存中 - `mQClientFactory.start()`:启动 RocketMQ 客户端实例对象 +ReferenceResource 类核心方法: - * `this.serviceState = ServiceState.RUNNING`:修改生产者实例的状态 +* hold():增加引用记数 refCount,方法加锁 - * `this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()`:RocketMQ 客户端实例向已知的 Broker 节点发送一次心跳(也是定时任务) - * `this.timer.scheduleAtFixedRate()`: request 发送的回执信息,启动定时任务每秒一次删除超时请求 - - * 生产者 msg 添加信息关联 ID 发送到 Broker - * 消费者从 Broker 拿到消息后会检查 msg 类型是一个需要回执的消息,处理完消息后会根据 msg 关联 ID 和客户端 ID 生成一条响应结果消息发送到 Broker,Broker 判断为回执消息,会根据客户端ID 找到 channel 推送给生产者 - * 生产者拿到回执消息后,读取出来关联 ID 找到对应的 RequestFuture,将阻塞线程唤醒 + ```java + public synchronized boolean hold() + ``` -* sendDefaultImpl():发送消息 +* shutdown():关闭资源,参数代表强制关闭资源的时间间隔 ```java - //参数1:消息;参数2:发送模式(同步异步单向);参数3:回调函数,异步发送时需要;参数4:发送超时时间, 默认 3 秒 - private SendResult sendDefaultImpl(msg, communicationMode, sendCallback,timeout) {} + // 系统当前时间 - firstShutdownTimestamp 时间 > intervalForcibly 进行【强制关闭】 + public void shutdown(final long intervalForcibly) ``` - * `this.makeSureStateOK()`:校验生产者状态是运行中,否则抛出异常 - - * `Validators.checkMessage(msg, this.defaultMQProducer)`:校验消息规格 +* release():引用计数减 1,当 refCount 为 0 时,调用子类的 cleanup 方法 - * `long beginTimestampPrev, endTimestamp`:本轮发送的开始时间和本轮的结束时间 + ```java + public void release() + ``` - * `topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic())`:**获取当前消息主题的发布信息** + - * `this.topicPublishInfoTable.get(topic)`:尝试从本地主题发布信息映射表获取信息,不空直接返回 - * `if (null == topicPublishInfo || !topicPublishInfo.ok())`:本地没有需要去 MQ 客户端获取 - `this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo())`:保存一份空数据 - `this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic)`:从 Namesrv 更新该 Topic 的路由数据 - `topicPublishInfo = this.topicPublishInfoTable.get(topic)`:重新从本地获取发布信息 +*** - * `this.mQClientFactory.updateTopicRouteInfoFromNameServer(..)`:**路由数据是空,获取默认 TBW102 的数据** - * `return topicPublishInfo`:返回 TBW102 主题的发布信息 - * `int timesTotal, times `:发送的总尝试次数和当前是第几次发送 +#### MapQueue - * `String[] brokersSent = new String[timesTotal]`:下标索引代表第几次发送,值代表这次发送选择 Broker name +##### 成员属性 - * `for (; times < timesTotal; times++)`:循环发送,发送成功或者发送尝试次数达到上限,结束循环 +MappedFileQueue 用来管理 MappedFile 文件 - * `String lastBrokerName = null == mq ? null : mq.getBrokerName()`:获取上次发送失败的 BrokerName +成员变量: - * `mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName)`:**从发布信息中选择一个队列** +* 管理目录:CommitLog 是 `../store/commitlog`, ConsumeQueue 是 `../store/xxx_topic/0` - * `if (this.sendLatencyFaultEnable)`:默认不开启,可以通过配置开启 - * `return tpInfo.selectOneMessageQueue(lastBrokerName)`:默认选择队列的方式,就是循环主题全部的队列 - - * `brokersSent[times] = mq.getBrokerName()`:将本次选择的 BrokerName 存入数组 + ```java + private final String storePath; + ``` - * `msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()))`:**重投的消息需要加上标记** +* 文件属性: - * `sendResult = this.sendKernelImpl`:核心发送方法 + ```java + private final int mappedFileSize; // 目录下每个文件大小,CL文件默认 1g,CQ文件 默认 600w字节 + private final CopyOnWriteArrayList mappedFiles; //目录下的每个 mappedFile 都加入该集合 + ``` - * `this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false)`:更新一下时间 +* 数据位点: - * `switch (communicationMode)`:异步或者单向消息直接返回 null,同步发送进入逻辑判断 + ```java + private long flushedWhere = 0; // 目录的刷盘位点,值为 mf.fileName + mf.wrotePosition + private long committedWhere = 0; // 目录的提交位点 + ``` - `if (sendResult.getSendStatus() != SendStatus.SEND_OK)`:**服务端 Broker 存储失败**,需要重试其他 Broker +* 消息存储: - * `throw new MQClientException()`:未找到当前主题的路由数据,无法发送消息,抛出异常 + ```java + private volatile long storeTimestamp = 0; // 当前目录下最后一条 msg 的存储时间 + ``` -* sendKernelImpl():**核心发送方法** +* 创建服务:新建 MappedFile 实例,继承自 ServiceThread 是一个任务对象,run 方法用来创建实例 ```java - //参数1:消息;参数2:选择的队列;参数3:发送模式(同步异步单向);参数4:回调函数,异步发送时需要;参数5:主题发布信息;参数6:剩余超时时间限制 - private SendResult sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout) + private final AllocateMappedFileService allocateMappedFileService; ``` - * `brokerAddr = this.mQClientFactory(...)`:获取指定 BrokerName 对应的 mater 节点的地址,master 节点的 ID 为 0 - * `brokerAddr = MixAll.brokerVIPChannel()`:Broker 启动时会绑定两个服务器端口,一个是普通端口,一个是 VIP 端口,服务器端根据不同端口创建不同的的 NioSocketChannel - * `byte[] prevBody = msg.getBody()`:获取消息体 +*** - * `if (!(msg instanceof MessageBatch))`:非批量消息,需要重新设置消息 ID - `MessageClientIDSetter.setUniqID(msg)`:msg id 由两部分组成,一部分是 ip 地址、进程号、ClassLoader 的 hashcode,另一部分是时间差(当前时间减去当月一号的时间)和计数器的值 - * `if (this.tryToCompressMessage(msg))`:判断消息是否压缩,压缩需要设置压缩标记 +##### 成员方法 - * `hasCheckForbiddenHook、hasSendMessageHook`:执行钩子方法 +核心方法: - * `requestHeader = new SendMessageRequestHeader()`:设置发送消息的消息头 +* load():Broker 启动时,加载本地磁盘数据,该方法读取 storePath 目录下的文件,创建 MappedFile 对象放入集合内 - * `if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX))`:重投的发送消息 + ```java + public boolean load() + ``` - * `switch (communicationMode)`:异步发送一种处理方式,单向和同步同样的处理逻辑 +* getLastMappedFile():获取当前正在顺序写入的 MappedFile 对象,如果最后一个 MappedFile 写满了,或者不存在 MappedFile 对象,则创建新的 MappedFile - `sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage()`:**发送消息** + ```java + // 参数一:文件起始偏移量;参数二:当list为空时,是否新建 MappedFile + public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) + ``` - * `request = RemotingCommand.createRequestCommand()`:创建一个 RequestCommand 对象 - * `request.setBody(msg.getBody())`:**将消息放入请求体** - * `switch (communicationMode)`:根据不同的模式 invoke 不同的方法 +* flush():根据 flushedWhere 属性查找合适的 MappedFile,调用该 MappedFile 的落盘方法,并更新全局的 flushedWhere -* request():请求方法,消费者回执消息,这种消息是异步消息 + ```java + //参数:0 表示强制刷新, > 0 脏页数据必须达到 flushLeastPages 才刷新 + public boolean flush(final int flushLeastPages) + ``` - * `requestResponseFuture = new RequestResponseFuture(correlationId, timeout, null)`:创建请求响应对象 +* findMappedFileByOffset():根据偏移量查询对象 - * `getRequestFutureTable().put(correlationId, requestResponseFuture)`:放入RequestFutureTable 映射表中 + ```java + public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) + ``` - * `this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback())`:**发送异步消息** +* deleteExpiredFileByTime():CL 删除过期文件,根据文件的保留时长决定是否删除 - * `return waitResponse(msg, timeout, requestResponseFuture, cost)`:用来挂起请求的方法 + ```java + // 参数一:过期时间; 参数二:删除两个文件之间的时间间隔; 参数三:mf.destory传递的参数; 参数四:true 强制删除 + public int deleteExpiredFileByTime(final long expiredTime,final int deleteFilesInterval, final long intervalForcibly, final boolean cleanImmediately) + ``` - ```java - public Message waitResponseMessage(final long timeout) throws InterruptedException { - // 请求挂起 - this.countDownLatch.await(timeout, TimeUnit.MILLISECONDS); - return this.responseMsg; - } +* deleteExpiredFileByOffset():CQ 删除过期文件,遍历每个 MF 文件,获取当前文件最后一个数据单元的物理偏移量,小于 offset 说明当前 MF 文件内都是过期数据 + + ```java + // 参数一:consumeLog 目录下最小物理偏移量,就是第一条消息的 offset; + // 参数二:ConsumerQueue 文件内每个数据单元固定大小 + public int deleteExpiredFileByOffset(long offset, int unitSize) + ``` - * 当消息被消费后,会获取消息的关联 ID,从映射表中获取消息的 RequestResponseFuture,执行下面的方法唤醒挂起线程 - ```java - public void putResponseMessage(final Message responseMsg) { - this.responseMsg = responseMsg; - this.countDownLatch.countDown(); - } - ``` @@ -6054,276 +6036,229 @@ DefaultMQProducerImpl 类是默认的生产者实现类 -#### 路由信息 +#### CommitLog -TopicPublishInfo 类用来存储路由信息 +##### 成员属性 成员变量: -* 顺序消息: +* 魔数: ```java - private boolean orderTopic = false; + public final static int MESSAGE_MAGIC_CODE = -626843481; // 消息的第一个字段是大小,第二个字段就是魔数 + protected final static int BLANK_MAGIC_CODE = -875286124; // 文件尾消息的魔法值 ``` -* 消息队列: +* MappedFileQueue:用于管理 `../store/commitlog` 目录下的文件 ```java - private List messageQueueList = new ArrayList<>(); // 主题全部的消息队列 - private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); // 消息队列索引 + protected final MappedFileQueue mappedFileQueue; ``` +* 存储服务: + ```java - // 【消息队列类】 - public class MessageQueue implements Comparable, Serializable { - private String topic; - private String brokerName; - private int queueId;// 队列 ID - } + protected final DefaultMessageStore defaultMessageStore; // 存储模块对象,上层服务 + private final FlushCommitLogService flushCommitLogService; // 刷盘服务,默认实现是异步刷盘 ``` -* 路由数据:主题对应的路由数据 +* 回调器:控制消息的哪些字段添加到 MappedFile ```java - private TopicRouteData topicRouteData; + private final AppendMessageCallback appendMessageCallback; ``` +* 队列偏移量字典表:key 是主题队列 id,value 是偏移量 + ```java - public class TopicRouteData extends RemotingSerializable { - private String orderTopicConf; - private List queueDatas; // 队列数据 - private List brokerDatas; // Broker 数据 - private HashMap/* Filter Server */> filterServerTable; - } + protected HashMap topicQueueTable = new HashMap(1024); ``` +* 锁相关: + ```java - public class QueueData implements Comparable { - private String brokerName; // 节点名称 - private int readQueueNums; // 读队列数 - private int writeQueueNums; // 写队列数 - private int perm; // 权限 - private int topicSynFlag; - } + private volatile long beginTimeInLock = 0; // 写数据时加锁的开始时间 + protected final PutMessageLock putMessageLock; // 写锁,两个实现类:自旋锁和重入锁 ``` +构造方法: + +* 有参构造: + ```java - public class BrokerData implements Comparable { - private String cluster; // 集群名 - private String brokerName; // Broker节点名称 - private HashMap brokerAddrs; + public CommitLog(final DefaultMessageStore defaultMessageStore) { + // 创建 MappedFileQueue 对象 + // 参数1:../store/commitlog; 参数2:【1g】; 参数3:allocateMappedFileService + this.mappedFileQueue = new MappedFileQueue(...); + // 默认 异步刷盘,创建这个对象 + this.flushCommitLogService = new FlushRealTimeService(); + // 控制消息哪些字段追加到 mappedFile,【消息最大是 4M】 + this.appendMessageCallback = new DefaultAppendMessageCallback(...); + // 默认使用自旋锁 + this.putMessageLock = ...; } ``` -核心方法: + + +*** + -* selectOneMessageQueue():**选择消息队列**使用 + +##### 成员方法 + +CommitLog 类核心方法: + +* start():会启动刷盘服务 ```java - // 参数是上次失败时的 brokerName,可以为 null - public MessageQueue selectOneMessageQueue(final String lastBrokerName) { - if (lastBrokerName == null) { - return selectOneMessageQueue(); - } else { - // 遍历消息队列 - for (int i = 0; i < this.messageQueueList.size(); i++) { - // 获取队列的索引 - int index = this.sendWhichQueue.getAndIncrement(); - // 获取队列的下标位置 - int pos = Math.abs(index) % this.messageQueueList.size(); - if (pos < 0) - pos = 0; - // 获取消息队列 - MessageQueue mq = this.messageQueueList.get(pos); - // 与上次选择的不同就可以返回 - if (!mq.getBrokerName().equals(lastBrokerName)) { - return mq; - } - } - return selectOneMessageQueue(); - } - } + public void start() ``` - +* shutdown():关闭刷盘服务 + ```java + public void shutdown() + ``` +* load():加载 CommitLog 目录下的文件 -**** + ```java + public boolean load() + ``` +* getMessage():根据 offset 查询单条信息,返回的结果对象内部封装了一个 ByteBuffer,该 Buffer 表示 `[offset, offset + size]` 区间的 MappedFile 的数据 + ```java + public SelectMappedBufferResult getMessage(final long offset, final int size) + ``` -### 客户端 +* deleteExpiredFile():删除过期文件,方法由 DefaultMessageStore 的定时任务调用 -#### 实例对象 + ```java + public int deleteExpiredFile() + ``` -##### 成员属性 +* asyncPutMessage():**存储消息** -MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一个客户端实例,**既服务于生产者,也服务于消费者** + ```java + public CompletableFuture asyncPutMessage(final MessageExtBrokerInner msg) + ``` -成员变量: + * `msg.setStoreTimestamp(System.currentTimeMillis())`:设置存储时间,后面获取到写锁后这个事件会重写 + * `String topic = msg.getTopic()`:获取主题和队列 ID + * `mappedFile = this.mappedFileQueue.getLastMappedFile()`:获取当前顺序写的 MappedFile 对象 -* 配置信息: + * `putMessageLock.lock()`:获取**写锁** + * `msg.setStoreTimestamp(beginLockTimestamp)`:设置消息的存储时间为获取锁的时间 + * `if (null == mappedFile || mappedFile.isFull())`:文件写满了创建新的 MF 对象 + * `result = mappedFile.appendMessage(msg, this.appendMessageCallback)`:**消息追加**,核心逻辑在回调器类 + * `putMessageLock.unlock()`:释放写锁 + * `this.defaultMessageStore.unlockMappedFile(..)`:将 MappedByteBuffer 从 lock 切换为 unlock 状态 + * `putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result)`:结果封装 + * `flushResultFuture = submitFlushRequest(result, msg)`:**唤醒刷盘线程** + * `replicaResultFuture = submitReplicaRequest(result, msg)`:HA 消息同步 + +* recoverNormally():正常关机时的恢复方法,存储模块启动时**先恢复所有的 ConsumeQueue 数据,再恢复 CommitLog 数据** ```java - private final int instanceIndex; // 索引一般是 0,因为客户端实例一般都是一个进程只有一个 - private final String clientId; // 客户端 ID ip@pid - private final long bootTimestamp; // 客户端的启动时间 - private ServiceState serviceState; // 客户端状态 + // 参数表示恢复阶段 ConsumeQueue 中已知的最大的消息 offset + public void recoverNormally(long maxPhyOffsetOfConsumeQueue) ``` -* 生产者消费者的映射表:key 是组名 + * `int index = mappedFiles.size() - 3`:从倒数第三个 file 开始向后恢复 - ```java - private final ConcurrentMap producerTable - private final ConcurrentMap consumerTable - private final ConcurrentMap adminExtTable - ``` + * `dispatchRequest = this.checkMessageAndReturnSize()`:每次从切片内解析出一条 msg 封装成 DispatchRequest 对象 -* 网络层配置: + * `size = dispatchRequest.getMsgSize()`:获取消息的大小,检查 DispatchRequest 对象的状态 - ```java - private final NettyClientConfig nettyClientConfig; - ``` + 情况 1:正常数据,则 `mappedFileOffset += size` -* 核心功能的实现:负责将 MQ 业务层的数据转换为网络层的 RemotingCommand 对象,使用内部持有的 NettyRemotingClient 对象的 invoke 系列方法,完成网络 IO(同步、异步、单向) + 情况 2:文件尾数据,处理下一个文件,mappedFileOffset 置为 0,magic_code 表示文件尾 - ```java - private final MQClientAPIImpl mQClientAPIImpl; - ``` + * `processOffset += mappedFileOffset`:计算出正确的数据存储位点,并设置 MappedFileQueue 的目录刷盘位点 -* 本地路由数据:key 是主题名称,value 路由信息 + * `this.mappedFileQueue.truncateDirtyFiles(processOffset)`:调整 MFQ 中文件的刷盘位点 - ```java - private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); + * `if (maxPhyOffsetOfConsumeQueue >= processOffset)`:删除冗余数据,将超过全局位点的 CQ 下的文件删除,将包含全局位点的 CQ 下的文件重新定位 -* 锁信息:两把锁,锁不同的数据 +* recoverAbnormally():异常关机时的恢复方法 ```java - private final Lock lockNamesrv = new ReentrantLock(); - private final Lock lockHeartbeat = new ReentrantLock(); + public void recoverAbnormally(long maxPhyOffsetOfConsumeQueue) ``` -* 调度线程池:单线程,执行定时任务 + * `int index = mappedFiles.size() - 1`:从尾部开始遍历 MFQ,验证 MF 的第一条消息,找到第一个验证通过的文件对象 + * `dispatchRequest = this.checkMessageAndReturnSize()`:每次解析出一条 msg 封装成 DispatchRequest 对象 + * `this.defaultMessageStore.doDispatch(dispatchRequest)`:重建 ConsumerQueue 和 Index,避免上次异常停机导致 CQ 和 Index 与 CommitLog 不对齐 + * 剩余逻辑与正常关机的恢复方法相似 - ```java - private final ScheduledExecutorService scheduledExecutorService; - ``` +消息追加服务 DefaultAppendMessageCallback -* Broker 映射表:key 是 BrokerName +* doAppend() + * `long wroteOffset = fileFromOffset + byteBuffer.position()`:消息写入的位置,物理偏移量 phyOffset + * `String msgId`:消息 ID,规则是客户端 IP + 消息偏移量 phyOffset + * `byte[] topicData`:序列化消息,将消息的字段压入到 msgStoreItemMemory 这个 Buffer 中 + * `byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen)`:将 msgStoreItemMemory 中的数据写入 MF 对象的内存映射的 Buffer 中,数据还没落盘 + * `AppendMessageResult result`:构造结果对象,包括存储位点、是否成功、队列偏移量等信息 + * `CommitLog.this.topicQueueTable.put(key, ++queueOffset)`:更新队列偏移量 - ```java - // 物理节点映射表,value:Long 是 brokerID,【ID=0 的是主节点,其他是从节点】,String 是地址 ip:port - private final ConcurrentMap> brokerAddrTable; - // 物理节点版本映射表,String 是地址 ip:port,Integer 是版本 - ConcurrentMap> brokerVersionTable; - ``` -* **客户端的协议处理器**:用于处理 IO 事件 - ```java - private final ClientRemotingProcessor clientRemotingProcessor; - ``` +**** -* 消息服务: - ```java - private final PullMessageService pullMessageService; // 拉消息服务 - private final RebalanceService rebalanceService; // 消费者负载均衡服务 - private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 - ``` -* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 +#### ConsQueue + +##### 成员属性 + +ConsumerQueue 是消息消费队列,存储消息在 CommitLog 的索引,便于快速定位消息 + +成员变量: + +* 数据单元:ConsumerQueueData 数据单元的固定大小是 20 字节,默认申请 20 字节的缓冲区 ```java - private final DefaultMQProducer defaultMQProducer; + public static final int CQ_STORE_UNIT_SIZE = 20; ``` -* 心跳次数统计: +* 文件管理: ```java - private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) + private final MappedFileQueue mappedFileQueue; // 文件管理器,管理 CQ 目录下的文件 + private final String storePath; // 目录,比如../store/consumequeue/xxx_topic/0 + private final int mappedFileSize; // 每一个 CCQ 存储文件大小,默认 20 * 30w = 600w byte ``` -* 公共配置类: +* 存储主模块:上层的对象 ```java - public class ClientConfig { - // Namesrv 地址配置 - private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); - // 客户端的 IP 地址 - private String clientIP = RemotingUtil.getLocalAddress(); - // 客户端实例名称 - private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); - // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 - private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); - // 命名空间 - protected String namespace; - protected AccessChannel accessChannel = AccessChannel.LOCAL; - - // 获取路由信息的间隔时间 30s - private int pollNameServerInterval = 1000 * 30; - // 客户端与 broker 之间的心跳周期 30s - private int heartbeatBrokerInterval = 1000 * 30; - // 消费者持久化消费的周期 5s - private int persistConsumerOffsetInterval = 1000 * 5; - private long pullTimeDelayMillsWhenException = 1000; - private boolean unitMode = false; - private String unitName; - // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 - private boolean vipChannelEnabled = Boolean.parseBoolean(); - // 语言,默认是 Java - private LanguageCode language = LanguageCode.JAVA; - } + private final DefaultMessageStore defaultMessageStore; ``` -构造方法: - -* MQClientInstance 有参构造: +* 消息属性: ```java - public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) { - this.clientConfig = clientConfig; - this.instanceIndex = instanceIndex; - // Netty 相关的配置信息 - this.nettyClientConfig = new NettyClientConfig(); - // 平台核心数 - this.nettyClientConfig.setClientCallbackExecutorThreads(...); - this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS()); - // 【创建客户端协议处理器】 - this.clientRemotingProcessor = new ClientRemotingProcessor(this); - // 创建 API 实现对象 - // 参数一:客户端网络配置 - // 参数二:客户端协议处理器,注册到客户端网络层 - // 参数三:rpcHook,注册到客户端网络层 - // 参数四:客户端配置 - this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig); - - //... - // 内部生产者,指定内部生产者的组 - this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP); - } + private final String topic; // CQ 主题 + private final int queueId; // CQ 队列,每一个队列都有一个 ConsumeQueue 对象进行管理 + private final ByteBuffer byteBufferIndex; // 临时缓冲区,插新的 CQData 时使用 + private long maxPhysicOffset = -1; // 当前ConsumeQueue内存储的最大消息物理偏移量 + private volatile long minLogicOffset = 0; // 当前ConsumeQueue内存储的最小消息物理偏移量 ``` -* MQClientAPIImpl 有参构造: +构造方法: + +* 有参构造: ```java - public MQClientAPIImpl(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig) { - this.clientConfig = clientConfig; - topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); - // 创建网络层对象,参数二为 null 说明客户端并不关心 channel event - this.remotingClient = new NettyRemotingClient(nettyClientConfig, null); - // 业务处理器 - this.clientRemotingProcessor = clientRemotingProcessor; - // 注册 RpcHook - this.remotingClient.registerRPCHook(rpcHook); - // ... - // 注册回退消息的请求码 - this.remotingClient.registerProcessor(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, this.clientRemotingProcessor, null); + public ConsumeQueue() { + // 申请了一个 20 字节大小的 临时缓冲区 + this.byteBufferIndex = ByteBuffer.allocate(CQ_STORE_UNIT_SIZE); } ``` - + *** @@ -6331,340 +6266,281 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 ##### 成员方法 -* start():启动方法 +ConsumeQueue 启动阶段方法: - * `synchronized (this)`:加锁保证线程安全,保证只有一个实例对象启动 - * `this.mQClientAPIImpl.start()`:启动客户端网络层,底层调用 RemotingClient 类 - * `this.startScheduledTask()`:启动定时任务 - * `this.pullMessageService.start()`:启动拉取消息服务 - * `this.rebalanceService.start()`:启动负载均衡服务 - * `this.defaultMQProducer.getDefaultMQProducerImpl().start(false)`:启动内部生产者,参数为 false 代表不启动实例 +* load():第一步,加载 storePath 目录下的文件,初始化 MappedFileQueue +* recover():第二步,恢复 ConsumeQueue 数据 + * 从倒数第三个 MF 文件开始向后遍历,依次读取 MF 中 20 个字节的 CQData 数据,检查 offset 和 size 是否是有效数据 + * 找到无效的 CQData 的位点,该位点就是 CQ 的刷盘点和数据顺序写入点 + * 删除无效的 MF 文件,调整当前顺序写的 MF 文件的数据位点 -* startScheduledTask():**启动定时任务**,调度线程池是单线程 +其他方法: - * `if (null == this.clientConfig.getNamesrvAddr())`:Namesrv 地址是空,需要两分钟拉取一次 Namesrv 地址 +* truncateDirtyLogicFiles():CommitLog 恢复阶段调用,将 ConsumeQueue 有效数据文件与 CommitLog 对齐,将超出部分的数据文删除掉,并调整当前文件的数据位点。Broker 启动阶段先恢复 CQ 的数据,再恢复 CL 数据,但是**数据要以 CL 为基准** - * 定时任务 1:从 Namesrv 更新客户端本地的路由数据,周期 30 秒一次 + ```java + // 参数是最大消息物理偏移量 + public void truncateDirtyLogicFiles(long phyOffet) + ``` - ```java - // 获取生产者和消费者订阅的主题集合,遍历集合,对比从 namesrv 拉取最新的主题路由数据和本地数据,是否需要更新 - MQClientInstance.this.updateTopicRouteInfoFromNameServer(); - ``` +* flush():刷盘,调用 MFQ 的刷盘方法 - * 定时任务 2:周期 30 秒一次,两个任务 + ```java + public boolean flush(final int flushLeastPages) + ``` - * 清理下线的 Broker 节点,遍历客户端的 Broker 物理节点映射表,将所有主题数据都不包含的 Broker 物理节点清理掉,如果被清理的 Broker 下所有的物理节点都没有了,就将该 Broker 的映射数据删除掉 - * 向在线的所有的 Broker 发送心跳数据,**同步发送的方式**,返回值是 Broker 物理节点的版本号,更新版本映射表 +* deleteExpiredFile():删除过期文件,将小于 offset 的所有 MF 文件删除,offset 是 CommitLog 目录下最小的物理偏移量,小于该值的 CL 文件已经没有了,所以 CQ 也没有存在的必要 - ```java - MQClientInstance.this.cleanOfflineBroker(); - MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); - ``` + ```java + public int deleteExpiredFile(long offset) + ``` - ```java - // 心跳数据 - public class HeartbeatData extends RemotingSerializable { - // 客户端 ID ip@pid - private String clientID; - // 存储客户端所有生产者数据 - private Set producerDataSet = new HashSet(); - // 存储客户端所有消费者数据 - private Set consumerDataSet = new HashSet(); - } - ``` +* putMessagePositionInfoWrapper():**向 CQ 中追加 CQData 数据**,由存储主模块 DefaultMessageStore 内部的异步线程调用,负责构建 ConsumeQueue 文件和 Index 文件的,该线程会持续关注 CommitLog 文件,当 CommitLog 文件内有新数据写入,就读出来封装成 DispatchRequest 对象,转发给 ConsumeQueue 或者 IndexService - * 定时任务 3:消费者持久化消费数据,周期 5 秒一次 + ```java + public void putMessagePositionInfoWrapper(DispatchRequest request) + ``` - ```java - MQClientInstance.this.persistAllConsumerOffset(); - ``` +* getIndexBuffer():转换 startIndex 为 offset,获取包含该 offset 的 MappedFile 文件,读取 `[offset%maxSize, mfPos]` 范围的数据,包装成结果对象返回 - * 定时任务 4:动态调整消费者线程池,周期 1 分钟一次 + ```java + public SelectMappedBufferResult getIndexBuffer(final long startIndex) + ``` - ```java - MQClientInstance.this.adjustThreadPool(); - ``` -* updateTopicRouteInfoFromNameServer():**更新路由数据** - * `if (isDefault && defaultMQProducer != null)`:需要默认数据 +**** - `topicRouteData = ...getDefaultTopicRouteInfoFromNameServer()`:从 Namesrv 获取默认的 TBW102 的路由数据 - `int queueNums`:遍历所有队列,为每个读写队列设置较小的队列数 - * `topicRouteData = ...getTopicRouteInfoFromNameServer(topic)`:需要**从 Namesrv 获取**路由数据(同步) +#### IndexFile - * `old = this.topicRouteTable.get(topic)`:获取客户端实例本地的该主题的路由数据 +##### 索引机制 - * `boolean changed = topicRouteDataIsChange(old, topicRouteData)`:对比本地和最新下拉的数据是否一致 +RocketMQ 的索引文件逻辑结构,类似 JDK 中 HashMap 的实现,具体结构如下: - * `if (changed)`:不一致进入更新逻辑 +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-IndexFile索引文件.png) - `cloneTopicRouteData = topicRouteData.cloneTopicRouteData()`:克隆一份最新数据 +IndexFile 文件的存储在 `$HOME\store\index${fileName}`,文件名 fileName 是以创建时的时间戳命名,文件大小是固定的,等于 `40+500W*4+2000W*20= 420000040` 个字节大小。如果消息的 properties 中设置了 UNIQ_KEY 这个属性,就用 `topic + “#” + UNIQ_KEY` 作为 key 来做写入操作;如果消息设置了 KEYS 属性(多个 KEY 以空格分隔),也会用 `topic + “#” + KEY` 来做索引 - `Update Pub info`:更新生产者信息 +整个 Index File 的结构如图,40 Byte 的 Header 用于保存一些总的统计信息,`4*500W` 的 Slot Table 并不保存真正的索引数据,而是保存每个槽位对应的单向链表的**头指针**,即一个 Index File 可以保存 2000W 个索引,`20*2000W` 是**真正的索引数据** - * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:**将主题路由数据转化为发布数据** - * `impl.updateTopicPublishInfo(topic, publishInfo)`:生产者将主题的发布数据保存到它本地,方便发送消息使用 +索引数据包含了 Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段,一共 20 Byte - `Update sub info`:更新消费者信息 +* NextIndex offset 即前面读出来的 slotValue,如果有 hash 冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来 +* Timestamp 记录的是消息 storeTimestamp 之间的差,并不是一个绝对的时间 - `this.topicRouteTable.put(topic, cloneTopicRouteData)`:将数据放入本地路由表 +参考文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md -**** +*** -#### 网络通信 -##### 成员属性 -NettyRemotingClient 类负责客户端的网络通信 +##### 成员属性 -成员变量: +IndexFile 类成员属性 -* Netty 服务相关属性: +* 哈希: ```java - private final NettyClientConfig nettyClientConfig; // 客户端的网络层配置 - private final Bootstrap bootstrap = new Bootstrap(); // 客户端网络层启动对象 - private final EventLoopGroup eventLoopGroupWorker; // 客户端网络层 Netty IO 线程组 + private static int hashSlotSize = 4; // 每个 hash 桶的大小是 4 字节,【用来存放索引的编号】 + private final int hashSlotNum; // hash 桶的个数,默认 500 万 ``` -* Channel 映射表: +* 索引: ```java - private final ConcurrentMap channelTables;// key 是服务器的地址,value 是通道对象 - private final Lock lockChannelTables = new ReentrantLock(); // 锁,控制并发安全 + private static int indexSize = 20; // 每个 index 条目的大小是 20 字节 + private static int invalidIndex = 0; // 无效索引编号:0 特殊值 + private final int indexNum; // 默认值:2000w + private final IndexHeader indexHeader; // 索引头 ``` -* 定时器:启动定时任务 +* 映射: ```java - private final Timer timer = new Timer("ClientHouseKeepingService", true) + private final MappedFile mappedFile; // 【索引文件使用的 MF 文件】 + private final FileChannel fileChannel; // 文件通道 + private final MappedByteBuffer mappedByteBuffer;// 从 MF 中获取的内存映射缓冲区 ``` -* 线程池: +构造方法: + +* 有参构造 ```java - private ExecutorService publicExecutor; // 公共线程池 - private ExecutorService callbackExecutor; // 回调线程池,客户端发起异步请求,服务器的响应数据由回调线程池处理 + // endPhyOffset 上个索引文件 最后一条消息的 物理偏移量 + // endTimestamp 上个索引文件 最后一条消息的 存储时间 + public IndexFile(final String fileName, final int hashSlotNum, final int indexNum, + final long endPhyOffset, final long endTimestamp) throws IOException { + // 文件大小 40 + 500w * 4 + 2000w * 20 + int fileTotalSize = + IndexHeader.INDEX_HEADER_SIZE + (hashSlotNum * hashSlotSize) + (indexNum * indexSize); + // 创建 mf 对象,会在disk上创建文件 + this.mappedFile = new MappedFile(fileName, fileTotalSize); + // 创建 索引头对象,传递 索引文件mf 的切片数据 + this.indexHeader = new IndexHeader(byteBuffer); + //... + } ``` -* 事件监听器:客户端这里是 null - ```java - private final ChannelEventListener channelEventListener; - ``` -* Netty 配置对象: +**** - ```java - public class NettyClientConfig { - // 客户端工作线程数 - private int clientWorkerThreads = 4; - // 回调处理线程池 线程数:平台核心数 - private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); - // 单向请求并发数,默认 65535 - private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; - // 异步请求并发数,默认 65535 - private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; - // 客户端连接服务器的超时时间限制 3秒 - private int connectTimeoutMillis = 3000; - // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) - private long channelNotActiveInterval = 1000 * 60; - // 客户端与服务器 ch 最大空闲时间 2分钟 - private int clientChannelMaxIdleTimeSeconds = 120; - - // 底层 Socket 写和收 缓冲区的大小 65535 64k - private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; - private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; - // 客户端 netty 是否启动内存池 - private boolean clientPooledByteBufAllocatorEnable = false; - // 客户端是否超时关闭 Socket 连接 - private boolean clientCloseSocketIfTimeout = false; - } - ``` -构造方法 -* 无参构造: +##### 成员方法 - ```java - public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { - this(nettyClientConfig, null); - } - ``` +IndexFile 类方法 -* 有参构造: +* load():加载 IndexHeader ```java - public NettyRemotingClient(nettyClientConfig, channelEventListener) { - // 父类创建了2个信号量,1、控制单向请求的并发度,2、控制异步请求的并发度 - super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue()); - this.nettyClientConfig = nettyClientConfig; - this.channelEventListener = channelEventListener; - - // 创建公共线程池 - int publicThreadNums = nettyClientConfig.getClientCallbackExecutorThreads(); - if (publicThreadNums <= 0) { - publicThreadNums = 4; - } - this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums,); - - // 创建 Netty IO 线程,1个线程 - this.eventLoopGroupWorker = new NioEventLoopGroup(1, ); - - if (nettyClientConfig.isUseTLS()) { - sslContext = TlsHelper.buildSslContext(true); - } - } + public void load() ``` +* flush():MappedByteBuffer 内的数据强制落盘 + ```java + public void flush() + ``` -**** +* isWriteFull():检查当前的 IndexFile 已写索引数是否 >= indexNum,达到该值则当前 IndexFile 不能继续追加 IndexData 了 + ```java + public boolean isWriteFull() + ``` +* destroy():删除文件时使用的方法 -##### 成员方法 + ```java + public boolean destroy(final long intervalForcibly) + ``` -* start():启动方法 +* putKey():添加索引数据,解决哈希冲突使用**头插法** ```java - public void start() { - // channel pipeline 内的 handler 使用的线程资源,默认 4 个 - this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(); - // 配置 netty 客户端启动类对象 - Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class) - //... - .handler(new ChannelInitializer() { - @Override - public void initChannel(SocketChannel ch) throws Exception { - ChannelPipeline pipeline = ch.pipeline(); - // 加几个handler - pipeline.addLast( - // 服务端的数据,都会来到这个 - new NettyClientHandler()); - } - }); - // 注意 Bootstrap 只是配置好客户端的元数据了,【在这里并没有创建任何 channel 对象】 - // 定时任务 扫描 responseTable 中超时的 ResponseFuture,避免客户端线程长时间阻塞 - this.timer.scheduleAtFixedRate(() -> { - NettyRemotingClient.this.scanResponseTable(); - }, 1000 * 3, 1000); - // 这里是 null,不启动 - if (this.channelEventListener != null) { - this.nettyEventExecutor.start(); - } - } + // 参数一:消息的 key,uniq_key 或者 keys="aaa bbb ccc" 会分别为 aaa bbb ccc 创建索引 + // 参数二:消息的物理偏移量; 参数三:消息存储时间 + public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) ``` -* 单向通信: + * `int slotPos = keyHash % this.hashSlotNum`:对 key 计算哈希后,取模得到对应的哈希槽 slot 下标,然后计算出哈希槽的存储位置 absSlotPos + * `int slotValue = this.mappedByteBuffer.getInt(absSlotPos)`:获取槽中的值,如果是无效值说明没有哈希冲突 + * `timeDiff = timeDiff / 1000`:计算当前 msg 存储时间减去索引文件内第一条消息存储时间的差值,转化为秒进行存储 + * `int absIndexPos`:计算当前索引数据存储的位置,开始填充索引数据到对应的位置 + * `this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount())`:在 slot 放入当前索引的索引编号 + * `if (this.indexHeader.getIndexCount() <= 1)`:索引文件插入的第一条数据,需要设置起始偏移量和存储时间 + * `if (invalidIndex == slotValue)`:没有哈希冲突,说明占用了一个新的 hash slot + * `this.indexHeader`:设置索引头的相关属性 + +* selectPhyOffset():从索引文件查询消息的物理偏移量 ```java - public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) { - // 开始时间 - long beginStartTime = System.currentTimeMillis(); - // 获取或者创建客户端与服务端(addr)的通道 channel - final Channel channel = this.getAndCreateChannel(addr); - // 条件成立说明客户端与服务端 channel 通道正常,可以通信 - if (channel != null && channel.isActive()) { - try { - // 执行 rpcHook 拓展点 - doBeforeRpcHooks(addr, request); - // 计算耗时,如果当前耗时已经超过 timeoutMillis 限制,则直接抛出异常,不再进行系统通信 - long costTime = System.currentTimeMillis() - beginStartTime; - if (timeoutMillis < costTime) { - throw new RemotingTimeoutException("invokeSync call timeout"); - } - // 参数1:客户端-服务端通道channel - // 参数二:网络层传输对象,封装着请求数据 - // 参数三:剩余的超时限制 - RemotingCommand response = this.invokeSyncImpl(channel, request, ...); - // 后置处理 - doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response); - // 返回响应数据 - return response; - } catch (RemotingSendRequestException e) {} - } else { - this.closeChannel(addr, channel); - throw new RemotingConnectException(addr); - } - } + // 参数一:查询结果全部放到该list内; 参数二:查询key; 参数三:结果最大数限制; 参数四五:时间范围 + public void selectPhyOffset(final List phyOffsets, final String key, final int maxNum,final long begin, final long end, boolean lock) ``` - + * `if (this.mappedFile.hold())`: MF 的引用记数 +1,查询期间 MF 资源**不能被释放** + * `int slotValue = this.mappedByteBuffer.getInt(absSlotPos)`:获取槽中的值,可能是无效值或者索引编号,如果是无效值说明查询未命中 + * `int absIndexPos`:计算出索引编号对应索引数据的开始位点 + * `this.mappedByteBuffer`:读取索引数据 + * `long timeRead = this.indexHeader.getBeginTimestamp() + timeDiff`:计算出准确的存储时间 + * `boolean timeMatched = (timeRead >= begin) && (timeRead <= end)`:时间范围的匹配 + * `phyOffsets.add(phyOffsetRead)`:将命中的消息索引的消息偏移量加入到 list 集合中 + * `nextIndexToRead = prevIndexRead`:遍历前驱节点 -*** +**** -### 存储端 -#### 存储机制 -##### 存储结构 +#### IndexServ -RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 +##### 成员属性 -RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,CommitLog 是消息真正的**物理存储**文件,ConsumeQueue 是消息的逻辑队列,类似数据库的**索引节点**,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** +IndexService 类用来管理 IndexFile 文件 -每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 +成员变量: -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) +* 存储主模块: -* 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 索引 + ```java + private final DefaultMessageStore defaultMessageStore; + ``` -RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 +* 索引文件存储目录:`../store/index` -服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 + ```java + private final String storePath; + ``` +* 索引对象集合:目录下的每个文件都有一个 IndexFile 对象 + ```java + private final ArrayList indexFileList = new ArrayList(); + ``` -**** +* 索引文件: + ```java + private final int hashSlotNum; // 每个索引文件包含的 哈希桶数量 :500w + private final int indexNum; // 每个索引文件包含的 索引条目数量 :2000w + ``` -##### 存储优化 -###### 内存映射 +*** -操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: -* read:读取本地文件内容 -* write:将读取的内容通过网络发送出去 +##### 成员方法 -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) +* load():加载 storePath 目录下的文件,为每个文件创建一个 IndexFile 实例对象,并加载 IndexHeader 信息 -补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 + ```java + public boolean load(final boolean lastExitOK) + ``` -通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用**零拷贝技术**,提高消息存盘和网络发送的速度。 +* deleteExpiredFile():删除过期索引文件 -RocketMQ 通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 + ```java + // 参数 offset 表示 CommitLog 内最早的消息的 phyOffset + public void deleteExpiredFile(long offset) + ``` -MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 + * `this.readWriteLock.readLock().lock()`:加锁判断 + * `long endPhyOffset = this.indexFileList.get(0).getEndPhyOffset()`:获取目录中第一个文件的结束偏移量 + * `if (endPhyOffset < offset)`:索引目录内存在过期的索引文件,并且当前的 IndexFile 都是过期的数据 + * `for (int i = 0; i < (files.length - 1); i++)`:遍历文件列表,删除过期的文件 +* buildIndex():存储主模块 DefaultMessageStore 内部的异步线程调用,构建 Index 数据 + ```java + public void buildIndex(DispatchRequest req) + ``` -*** + * `indexFile = retryGetAndCreateIndexFile()`:获取或者创建顺序写的索引文件对象 + * `buildKey(topic, req.getUniqKey())`:**构建索引 key**,`topic + # + uniqKey` + * `indexFile = putKey()`:插入索引文件 -###### 页缓存 + * `if (keys != null && keys.length() > 0)`:消息存在自定义索引 -页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** + `for (int i = 0; i < keyset.length; i++)`:遍历每个索引,为每个 key 调用一次 putKey -* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 -* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理,最大 128K) +* getAndCreateLastIndexFile():获取当前顺序写的 IndexFile,没有就创建 -在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 + ```java + public IndexFile getAndCreateLastIndexFile() + ``` @@ -6672,175 +6548,236 @@ MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文 -##### 刷盘机制 +#### MesStore -两种持久化的方案: +##### 生命周期 -* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 -* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 +DefaultMessageStore 类核心是整个存储服务的调度类 -RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使用**顺序 IO**,因为磁盘的顺序读写要比随机读写快很多 +* 构造方法: -* 同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 + ```java + public DefaultMessageStore() + ``` -* 异步刷盘:利用 OS 的 PageCache,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 + * `this.allocateMappedFileService.start()`:启动**创建 MappedFile 文件服务** + * `this.indexService.start()`:启动索引服务 -通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 +* load():加载资源 -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) + ```java + public boolean load() + ``` + * `this.commitLog.load()`:先加载 CommitLog + * `this.loadConsumeQueue()`:再加载 ConsumeQueue + * `this.storeCheckpoint`:检查位点对象 + * `this.indexService.load(lastExitOK)`:加载 IndexFile + * `this.recover(lastExitOK)`:恢复阶段,先恢复 CQ,在恢复 CL +* start():核心启动方法 -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + ```java + public void start() + ``` + * `lock = lockFile.getChannel().tryLock(0, 1, false)`:获取文件锁,获取失败说明当前目录已经启动过 Broker + * `long maxPhysicalPosInLogicQueue = commitLog.getMinOffset()`:遍历全部的 CQ 对象,获取 CQ 中消息的最大偏移量 -*** + * `this.reputMessageService.start()`:设置分发服务的分发位点,启动**分发服务**,构建 ConsumerQueue 和 IndexFile + * `if (dispatchBehindBytes() <= 0)`:线程等待分发服务将分发数据全部处理完毕 + * `this.recoverTopicQueueTable()`:因为修改了 CQ 数据,所以再次构建队列偏移量字段表 -#### MappedFile + * `this.haService.start()`:启动 **HA 服务** -##### 成员属性 + * `this.handleScheduleMessageService()`:启动**消息调度服务** -MappedFile 类是最基础的存储类,继承自 ReferenceResource 类,用来**保证线程安全** + * `this.flushConsumeQueueService.start()`:启动 CQ **消费队列刷盘服务** -MappedFile 类成员变量: + * `this.commitLog.start()`:启动 **CL 刷盘服务** -* 内存相关: + * `this.storeStatsService.start()`:启动状态存储服务 - ```java - public static final int OS_PAGE_SIZE = 1024 * 4;// 内存页大小:默认是 4k - private AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY; // 当前进程下所有的 mappedFile 占用的总虚拟内存大小 - private AtomicInteger TOTAL_MAPPED_FILES; // 当前进程下所有的 mappedFile 个数 - ``` + * `this.createTempFile()`:创建 AbortFile,正常关机时 JVM HOOK 会删除该文件,异常宕机时该文件不会删除,开机数据恢复阶段根据是否存在该文件,执行不同的**恢复策略** -* 数据位点: + * `this.addScheduleTask()`:添加定时任务 - ```java - protected final AtomicInteger wrotePosition; // 当前 mappedFile 的数据写入点 - protected final AtomicInteger committedPosition;// 当前 mappedFile 的数据提交点 - private final AtomicInteger flushedPosition; // 数据落盘位点,在这之前的数据是持久化的安全数据 - // flushedPosition-wrotePosition 之间的数据属于脏页 - ``` + * `DefaultMessageStore.this.cleanFilesPeriodically()`:定时**清理过期文件**,周期是 10 秒 -* 文件相关:CL 是 CommitLog,CQ 是 ConsumeQueue + * `this.cleanCommitLogService.run()`:启动清理过期的 CL 文件服务 + * `this.cleanConsumeQueueService.run()`:启动清理过期的 CQ 文件服务 - ```java - private String fileName; // 文件名称,CL和CQ文件名是第一条消息的物理偏移量,索引文件是年月日时分秒 - private long fileFromOffset;// 文件名转long,代表该对象的【起始偏移量】 - private File file; // 文件对象 - ``` + * `DefaultMessageStore.this.checkSelf()`:每 10 分种进行健康检查 -* 内存映射: + * `DefaultMessageStore.this.cleanCommitLogService.isSpaceFull()`:**磁盘预警**定时任务,每 10 秒一次 - ```java - protected FileChannel fileChannel; // 文件通道 - private MappedByteBuffer mappedByteBuffer; // 内存映射缓冲区,访问虚拟内存 - ``` + * `if (physicRatio > this.diskSpaceWarningLevelRatio)`:检查磁盘是否到达 waring 阈值,默认 90% -ReferenceResource 类成员变量: + `boolean diskok = ...runningFlags.getAndMakeDiskFull()`:设置磁盘写满标记 -* 引用数量:当 `refCount <= 0` 时,表示该资源可以释放了,没有任何其他程序依赖它了,用原子类保证线程安全 + * `boolean diskok = ...this.runningFlags.getAndMakeDiskOK()`:设置磁盘可写标记 - ```java - protected final AtomicLong refCount = new AtomicLong(1); // 初始值为 1 - ``` + * `this.shutdown = false`:刚启动,设置为 false -* 存活状态:表示资源的存活状态 +* shutdown():关闭各种服务和线程资源,设置存储模块状态为关闭状态 ```java - protected volatile boolean available = true; + public void shutdown() ``` -* 是否清理:默认值 false,当执行完子类对象的 cleanup() 清理方法后,该值置为 true ,表示资源已经全部释放 +* destroy():销毁 Broker 的工作目录 ```java - protected volatile boolean cleanupOver = false; + public void destroy() ``` -* 第一次关闭资源的时间:用来记录超时时间 - ```java - private volatile long firstShutdownTimestamp = 0; - ``` - + *** -##### 成员方法 +##### 服务线程 -MappedFile 类核心方法: +ServiceThread 类被很多服务继承,本身是一个 Runnable 任务对象,继承者通过重写 run 方法来实现服务的逻辑 -* appendMessage():提供上层向内存映射中追加消息的方法,消息如何追加由 AppendMessageCallback 控制 +* run():一般实现方式 ```java - // 参数一:消息 参数二:追加消息回调 - public AppendMessageResult appendMessage(MessageExtBrokerInner msg, AppendMessageCallback cb) + public void run() { + while (!this.isStopped()) { + // 业务逻辑 + } + } ``` + 通过参数 stopped 控制服务的停止,使用 volatile 修饰保证可见性 + ```java - // 将字节数组写入到文件通道 - public boolean appendMessage(final byte[] data) + protected volatile boolean stopped = false ``` -* flush():刷盘接口,参数 flushLeastPages 代表刷盘的最小页数 ,等于 0 时属于强制刷盘;> 0 时需要脏页(计算方法在数据位点)达到该值才进行物理刷盘;文件写满时强制刷盘 +* shutdown():停止线程,首先设置 stopped 为 true,然后进行唤醒,默认不直接打断线程 ```java - public int flush(final int flushLeastPages) + public void shutdown() ``` -* selectMappedBuffer():该方法以 pos 为开始位点 ,到有效数据为止,创建一个切片 ByteBuffer 作为数据副本,供业务访问数据 +* waitForRunning():挂起线程,设置唤醒标记 hasNotified 为 false ```java - public SelectMappedBufferResult selectMappedBuffer(int pos) + protected void waitForRunning(long interval) ``` -* destroy():销毁映射文件对象,并删除关联的系统文件,参数是强制关闭资源的时间 +* wakeup():唤醒线程,设置 hasNotified 为 true ```java - public boolean destroy(final long intervalForcibly) + public void wakeup() ``` -* cleanup():释放堆外内存,更新总虚拟内存和总内存映射文件数 - ```java - public boolean cleanup(final long currentRef) - ``` - -* warmMappedFile():内存预热,当要新建的 MappedFile 对象大于 1g 时,执行该方法对该 MappedFile 的每个 Page Cache 进行写入一个字节进行分配内存,**将映射文件都加载到内存** + +*** + + + +##### 构建服务 + +AllocateMappedFileService 创建 MappedFile 服务 + +* mmapOperation():核心服务 ```java - public void warmMappedFile(FlushDiskType type, int pages) + private boolean mmapOperation() ``` -ReferenceResource 类核心方法: + * `req = this.requestQueue.take()`: 从 requestQueue 阻塞队列(优先级)中获取 AllocateRequest 任务 + * `if (...isTransientStorePoolEnable())`:条件成立使用直接内存写入数据, 从直接内存中 commit 到 FileChannel 中 + * `mappedFile = new MappedFile(req.getFilePath(), req.getFileSize())`:根据请求的路径和大小创建对象 + * `mappedFile.warmMappedFile()`:判断 mappedFile 大小,只有 CommitLog 才进行文件预热 + * `req.setMappedFile(mappedFile)`:将创建好的 MF 对象的赋值给请求对象的成员属性 + * `req.getCountDownLatch().countDown()`:**唤醒请求的阻塞线程** -* hold():增加引用记数 refCount,方法加锁 +* putRequestAndReturnMappedFile():MappedFileQueue 中用来创建 MF 对象的方法 ```java - public synchronized boolean hold() + public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize) ``` -* shutdown():关闭资源,参数代表强制关闭资源的时间间隔 + * `AllocateRequest nextReq = new AllocateRequest(...)`:创建 nextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列,然后创建 nextNextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列 + * `AllocateRequest result = this.requestTable.get(nextFilePath)`:从请求列表获取 nextFilePath 的请求对象 + * `result.getCountDownLatch().await(...)`:**线程挂起**,直到超时或者 nextFilePath 对应的 MF 文件创建完成 + * `return result.getMappedFile()`:返回创建好的 MF 文件对象 + +ReputMessageService 消息分发服务,用于构建 ConsumerQueue 和 IndexFile 文件 + +* run():循环执行 doReput 方法,每执行一次线程休眠 1 毫秒 ```java - // 系统当前时间 - firstShutdownTimestamp 时间 > intervalForcibly 进行【强制关闭】 - public void shutdown(final long intervalForcibly) + public void run() ``` -* release():引用计数减 1,当 refCount 为 0 时,调用子类的 cleanup 方法 +* doReput():实现分发的核心逻辑 ```java - public void release() + private void doReput() ``` - + * `for (boolean doNext = true; this.isCommitLogAvailable() && doNext; )`:循环遍历 + * `SelectMappedBufferResult result`: 从 CommitLog 拉取数据,数据范围 `[reputFromOffset, 包含该偏移量的 MF 的最大 Pos]`,封装成结果对象 + * `DispatchRequest dispatchRequest`:从结果对象读取出一条 DispatchRequest 数据 + * `DefaultMessageStore.this.doDispatch(dispatchRequest)`:将数据交给分发器进行分发,用于**构建 CQ 和索引文件** + * `this.reputFromOffset += size`:更新数据范围 + + + +*** + + + +##### 刷盘服务 + +FlushConsumeQueueService 刷盘 CQ 数据 + +* run():每隔 1 秒执行一次刷盘服务,跳出循环后还会执行一次强制刷盘 + + ```java + public void run() + ``` +* doFlush():刷盘 + ```java + private void doFlush(int retryTimes) + ``` + + * `int flushConsumeQueueLeastPages`:脏页阈值,默认是 2 + + * `if (retryTimes == RETRY_TIMES_OVER)`:**重试次数是 3** 时设置强制刷盘,设置脏页阈值为 0 + * `int flushConsumeQueueThoroughInterval`:两次刷新的**时间间隔超过 60 秒**会强制刷盘 + * `for (ConsumeQueue cq : maps.values())`:遍历所有的 CQ,进行刷盘 + * `DefaultMessageStore.this.getStoreCheckpoint().flush()`:强制刷盘时将 StoreCheckpoint 瞬时数据刷盘 + +FlushCommitLogService 刷盘 CL 数据,默认是异步刷盘 + +* run():运行方法 + + ```java + public void run() + ``` + + * `while (!this.isStopped())`:stopped为 true 才跳出循环 + * `boolean flushCommitLogTimed`:控制线程的休眠方式,默认是 false,使用 `CountDownLatch.await()` 休眠,设置为 true 时使用 `Thread.sleep()` 休眠 + * `int interval`:获取配置中的刷盘时间间隔 + * `int flushPhysicQueueLeastPages`:获取最小刷盘页数,默认是 4 页,脏页达到指定页数才刷盘 + * `int flushPhysicQueueThoroughInterval`:获取强制刷盘周期,默认是 10 秒,达到周期后强制刷盘,不考虑脏页 + * `if (flushCommitLogTimed)`:休眠逻辑,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 + * `CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)`:**刷盘** @@ -6848,120 +6785,1005 @@ ReferenceResource 类核心方法: -#### MapQueue +##### 清理服务 -MappedFileQueue 用来管理 MappedFile 文件 +CleanCommitLogService 清理过期的 CL 数据,定时任务 10 秒调用一次,先清理 CL,再清理 CQ,因为 CQ 依赖于 CL 的数据 -成员变量: +* run():运行方法 -* 管理目录:CommitLog 是 `../store/commitlog`, ConsumeQueue 是 `../store/xxx_topic/0` + ```java + public void run() + ``` + +* deleteExpiredFiles():删除过期 CL 文件 ```java - private final String storePath; + private void deleteExpiredFiles() ``` -* 文件属性: + * `long fileReservedTime`:默认 72,代表文件的保留时间 + * `boolean timeup = this.isTimeToDelete()`:当前时间是否是凌晨 4 点 + * `boolean spacefull = this.isSpaceToDelete()`:CL 或者 CQ 的目录磁盘使用率达到阈值标准 85% + * `boolean manualDelete = this.manualDeleteFileSeveralTimes > 0`:手动删除文件 + * `fileReservedTime *= 60 * 60 * 1000`:默认保留 72 小时 + * `deleteCount = DefaultMessageStore.this.commitLog.deleteExpiredFile()`:**调用 MFQ 对象的删除方法** + +CleanConsumeQueueService 清理过期的 CQ 数据 + +* run():运行方法 ```java - private final int mappedFileSize; // 目录下每个文件大小,CL文件默认 1g,CQ文件 默认 600w字节 - private final CopyOnWriteArrayList mappedFiles; //目录下的每个 mappedFile 都加入该集合 + public void run() ``` -* 数据位点: +* deleteExpiredFiles():删除过期 CQ 文件 ```java - private long flushedWhere = 0; // 目录的刷盘位点,值为 mf.fileName + mf.wrotePosition - private long committedWhere = 0; // 目录的提交位点 + private void deleteExpiredFiles() ``` -* 消息存储: + * `int deleteLogicsFilesInterval`:清理 CQ 的时间间隔,默认 100 毫秒 + * `long minOffset = DefaultMessageStore.this.commitLog.getMinOffset()`:获取 CL 文件中最小的物理偏移量 + * `if (minOffset > this.lastPhysicalMinOffset)`:CL 最小的偏移量大于 CQ 最小的,说明有过期数据 + * `this.lastPhysicalMinOffset = minOffset`:更新 CQ 的最小偏移量 + * `for (ConsumeQueue logic : maps.values())`:遍历所有的 CQ 文件 + * `logic.deleteExpiredFile(minOffset)`:调用 MFQ 对象的删除方法 + * `DefaultMessageStore.this.indexService.deleteExpiredFile(minOffset)`:**删除过期的索引文件** + + + +*** + + + +#### Broker + +BrokerStartup 启动方法 + +```java +public static void main(String[] args) { + start(createBrokerController(args)); +} +public static BrokerController start(BrokerController controller) { + controller.start(); // 启动 +} +``` + +BrokerController#start:核心启动方法 + +* `this.messageStore.start()`:**启动存储服务** + +* `this.remotingServer.start()`:启动 Netty 通信服务 + +* `this.fileWatchService.start()`:启动文件监听服务 + +* `this.scheduledExecutorService.scheduleAtFixedRate()`:每隔 30s 向 NameServer 上报 Topic 路由信息,**心跳机制** + + `BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister())` + + + + + +**** + + + +### 生产者 + +#### 生产者类 + +DefaultMQProducer 是生产者的默认实现类 + +成员变量: + +* 生产者实现类: ```java - private volatile long storeTimestamp = 0; // 当前目录下最后一条 msg 的存储时间 + protected final transient DefaultMQProducerImpl defaultMQProducerImpl ``` -* 创建服务:新建 MappedFile 实例,继承自 ServiceThread 是一个任务对象,run 方法用来创建实例 +* 生产者组:发送事务消息,Broker 端进行事务回查(补偿机制)时,选择当前生产者组的下一个生产者进行事务回查 ```java - private final AllocateMappedFileService allocateMappedFileService; + private String producerGroup; ``` -核心方法: +* 默认主题:isAutoCreateTopicEnable 开启时,当发送消息指定的 Topic 在 Namesrv 未找到路由信息,使用该值创建 Topic 信息 -* load():Broker 启动时,加载本地磁盘数据,该方法读取 storePath 目录下的文件,创建 MappedFile 对象放入集合内 + ```java + private String createTopicKey = TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC; + // 值为【TBW102】,Just for testing or demo program + ``` + +* 消息重投:系统特性消息重试部分详解了三个参数的作用 ```java - public boolean load() + private int retryTimesWhenSendFailed = 2; // 同步发送失败后重试的发送次数,加上第一次发送,一共三次 + private int retryTimesWhenSendAsyncFailed = 2; // 异步 + private boolean retryAnotherBrokerWhenNotStoreOK = false; // 消息未存储成功,选择其他 Broker 重试 ``` -* getLastMappedFile():获取当前正在顺序写入的 MappedFile 对象,如果最后一个 MappedFile 写满了,或者不存在 MappedFile 对象,则创建新的 MappedFile +* 消息队列: ```java - // 参数一:文件起始偏移量;参数二:当list为空时,是否新建 MappedFile - public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) + private volatile int defaultTopicQueueNums = 4; // 默认 Broker 创建的队列数 ``` -* flush():根据 flushedWhere 属性查找合适的 MappedFile,调用该 MappedFile 的落盘方法,并更新全局的 flushedWhere +* 消息属性: ```java - //参数:0 表示强制刷新, > 0 脏页数据必须达到 flushLeastPages 才刷新 - public boolean flush(final int flushLeastPages) + private int sendMsgTimeout = 3000; // 发送消息的超时限制 + private int compressMsgBodyOverHowmuch = 1024 * 4; // 压缩阈值,当 msg body 超过 4k 后使用压缩 + private int maxMessageSize = 1024 * 1024 * 4; // 消息体的最大限制,默认 4M + private TraceDispatcher traceDispatcher = null; // 消息轨迹 + +构造方法: + +* 构造方法: + + ```java + public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook) { + this.namespace = namespace; + this.producerGroup = producerGroup; + // 创建生产者实现对象 + defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook); + } ``` -* findMappedFileByOffset():根据偏移量查询对象 +成员方法: + +* start():启动方法 ```java - public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) + public void start() throws MQClientException { + // 重置生产者组名,如果传递了命名空间,则 【namespace%group】 + this.setProducerGroup(withNamespace(this.producerGroup)); + // 生产者实现对象启动 + this.defaultMQProducerImpl.start(); + if (null != traceDispatcher) { + // 消息轨迹的逻辑 + traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel()); + } + } ``` -* deleteExpiredFileByTime():CL 删除过期文件,根据文件的保留时长决定是否删除 +* send():**发送消息**: ```java - // 参数一:过期时间; 参数二:删除两个文件之间的时间间隔; 参数三:mf.destory传递的参数; 参数四:true 强制删除 - public int deleteExpiredFileByTime(final long expiredTime,final int deleteFilesInterval, final long intervalForcibly, final boolean cleanImmediately) + public SendResult send(Message msg){ + // 校验消息 + Validators.checkMessage(msg, this); + // 设置消息 Topic + msg.setTopic(withNamespace(msg.getTopic())); + return this.defaultMQProducerImpl.send(msg); + } ``` -* deleteExpiredFileByOffset():CQ 删除过期文件,遍历每个 MF 文件,获取当前文件最后一个数据单元的物理偏移量,小于 offset 说明当前 MF 文件内都是过期数据 +* request():请求方法,**需要消费者回执消息**,又叫回退消息 ```java - // 参数一:consumeLog 目录下最小物理偏移量,就是第一条消息的 offset; - // 参数二:ConsumerQueue 文件内每个数据单元固定大小 - public int deleteExpiredFileByOffset(long offset, int unitSize) + public Message request(final Message msg, final MessageQueue mq, final long timeout) { + msg.setTopic(withNamespace(msg.getTopic())); + return this.defaultMQProducerImpl.request(msg, mq, timeout); + } ``` - +*** + + + +#### 默认实现 + +##### 成员属性 + +DefaultMQProducerImpl 类是默认的生产者实现类 + +成员变量: + +* 实例对象: + + ```java + private final DefaultMQProducer defaultMQProducer; // 持有默认生产者对象,用来获取对象中的配置信息 + private MQClientInstance mQClientFactory; // 客户端实例对象,生产者启动后需要注册到该客户端对象内 + ``` +* 主题发布信息映射表:key 是 Topic,value 是发布信息 + ```java + private final ConcurrentMap topicPublishInfoTable = new ConcurrentHashMap(); + ``` +* 异步发送消息:相关信息 + ```java + private final BlockingQueue asyncSenderThreadPoolQueue;// 异步发送消息,异步线程池使用的队列 + private final ExecutorService defaultAsyncSenderExecutor; // 异步发送消息默认使用的线程池 + private ExecutorService asyncSenderExecutor; // 异步消息发送线程池,指定后就不使用默认线程池了 + ``` +* 定时器:执行定时任务 + ```java + private final Timer timer = new Timer("RequestHouseKeepingService", true); // 守护线程 + ``` +* 状态信息:服务的状态,默认创建状态 + ```java + private ServiceState serviceState = ServiceState.CREATE_JUST; + ``` +* 压缩等级:ZIP 压缩算法的等级,默认是 5,越高压缩效果好,但是压缩的更慢 + ```java + private int zipCompressLevel = Integer.parseInt(System.getProperty..., "5")); + ``` +* 容错策略:选择队列的容错策略 + ```java + private MQFaultStrategy mqFaultStrategy = new MQFaultStrategy(); + ``` +* 钩子:用来进行前置或者后置处理 + ```java + ArrayList sendMessageHookList; // 发送消息的钩子,留给用户扩展使用 + ArrayList checkForbiddenHookList; // 对比上面的钩子,可以抛异常,控制消息是否可以发送 + private final RPCHook rpcHook; // 传递给 NettyRemotingClient + ``` +构造方法: +* 默认构造: + ```java + public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer) { + // 默认 RPC HOOK 是空 + this(defaultMQProducer, null); + } + ``` +* 有参构造: + ```java + public DefaultMQProducerImpl(final DefaultMQProducer defaultMQProducer, RPCHook rpcHook) { + // 属性赋值 + this.defaultMQProducer = defaultMQProducer; + this.rpcHook = rpcHook; + + // 创建【异步消息线程池任务队列】,长度是 5w + this.asyncSenderThreadPoolQueue = new LinkedBlockingQueue(50000); + // 创建默认的异步消息任务线程池 + this.defaultAsyncSenderExecutor = new ThreadPoolExecutor( + // 核心线程数和最大线程数都是 系统可用的计算资源(8核16线程的系统就是 16)... + } + ``` + +**** +##### 成员方法 +* start():启动方法,参数默认是 true,代表正常的启动路径 + * `this.serviceState = ServiceState.START_FAILED`:先修改为启动失败,成功后再修改,这种思想很常见 + * `this.checkConfig()`:判断生产者组名不能是空,也不能是 default_PRODUCER + * `if (!getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP))`:条件成立说明当前生产者不是内部产生者,内部生产者是**处理消息回退**的这种情况使用的生产者 + `this.defaultMQProducer.changeInstanceNameToPID()`:正常的生产者,修改生产者实例名称为当前进程的 PID + + * ` this.mQClientFactory = ...`:获取当前进程的 MQ 客户端实例对象,从 factoryTable 中获取 key 为 客户端 ID,格式是`ip@pid`,**一个 JVM 进程只有一个 PID,也只有一个 MQClientInstance** + + * `boolean registerOK = mQClientFactory.registerProducer(...)`:将生产者注册到 RocketMQ 客户端实例内 + + * `this.topicPublishInfoTable.put(...)`:添加一个主题发布信息,key 是 **TBW102** ,value 是一个空对象 + + * `if (startFactory) `:正常启动路径 + + `mQClientFactory.start()`:启动 RocketMQ 客户端实例对象 + + * `this.serviceState = ServiceState.RUNNING`:修改生产者实例的状态 + + * `this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()`:RocketMQ 客户端实例向已知的 Broker 节点发送一次心跳(也是定时任务) + * `this.timer.scheduleAtFixedRate()`: request 发送的回执信息,启动定时任务每秒一次删除超时请求 + + * 生产者 msg 添加信息关联 ID 发送到 Broker + * 消费者从 Broker 拿到消息后会检查 msg 类型是一个需要回执的消息,处理完消息后会根据 msg 关联 ID 和客户端 ID 生成一条响应结果消息发送到 Broker,Broker 判断为回执消息,会根据客户端ID 找到 channel 推送给生产者 + * 生产者拿到回执消息后,读取出来关联 ID 找到对应的 RequestFuture,将阻塞线程唤醒 + +* sendDefaultImpl():发送消息 + + ```java + //参数1:消息;参数2:发送模式(同步异步单向);参数3:回调函数,异步发送时需要;参数4:发送超时时间, 默认 3 秒 + private SendResult sendDefaultImpl(msg, communicationMode, sendCallback,timeout) {} + ``` + + * `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.putIfAbsent(topic, new TopicPublishInfo())`:保存一份空数据 + + `this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic)`:从 Namesrv 更新该 Topic 的路由数据 + + `topicPublishInfo = this.topicPublishInfoTable.get(topic)`:重新从本地获取发布信息 + + * `this.mQClientFactory.updateTopicRouteInfoFromNameServer(..)`:**路由数据是空,获取默认 TBW102 的数据** + + * `return topicPublishInfo`:返回 TBW102 主题的发布信息 + + * `int timesTotal, times `:发送的总尝试次数和当前是第几次发送 + + * `String[] brokersSent = new String[timesTotal]`:下标索引代表第几次发送,值代表这次发送选择 Broker name + + * `for (; times < timesTotal; times++)`:循环发送,发送成功或者发送尝试次数达到上限,结束循环 + + * `String lastBrokerName = null == mq ? null : mq.getBrokerName()`:获取上次发送失败的 BrokerName + + * `mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName)`:**从发布信息中选择一个队列** + + * `if (this.sendLatencyFaultEnable)`:默认不开启,可以通过配置开启 + * `return tpInfo.selectOneMessageQueue(lastBrokerName)`:默认选择队列的方式,就是循环主题全部的队列 + + * `brokersSent[times] = mq.getBrokerName()`:将本次选择的 BrokerName 存入数组 + + * `msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()))`:**重投的消息需要加上标记** + + * `sendResult = this.sendKernelImpl`:核心发送方法 + + * `this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false)`:更新一下时间 + + * `switch (communicationMode)`:异步或者单向消息直接返回 null,同步发送进入逻辑判断 + + `if (sendResult.getSendStatus() != SendStatus.SEND_OK)`:**服务端 Broker 存储失败**,需要重试其他 Broker + + * `throw new MQClientException()`:未找到当前主题的路由数据,无法发送消息,抛出异常 + +* sendKernelImpl():**核心发送方法** + + ```java + //参数1:消息;参数2:选择的队列;参数3:发送模式(同步异步单向);参数4:回调函数,异步发送时需要;参数5:主题发布信息;参数6:剩余超时时间限制 + private SendResult sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout) + ``` + + * `brokerAddr = this.mQClientFactory(...)`:获取指定 BrokerName 对应的 mater 节点的地址,master 节点的 ID 为 0 + + * `brokerAddr = MixAll.brokerVIPChannel()`:Broker 启动时会绑定两个服务器端口,一个是普通端口,一个是 VIP 端口,服务器端根据不同端口创建不同的的 NioSocketChannel + + * `byte[] prevBody = msg.getBody()`:获取消息体 + + * `if (!(msg instanceof MessageBatch))`:非批量消息,需要重新设置消息 ID + + `MessageClientIDSetter.setUniqID(msg)`:msg id 由两部分组成,一部分是 ip 地址、进程号、ClassLoader 的 hashcode,另一部分是时间差(当前时间减去当月一号的时间)和计数器的值 + + * `if (this.tryToCompressMessage(msg))`:判断消息是否压缩,压缩需要设置压缩标记 + + * `hasCheckForbiddenHook、hasSendMessageHook`:执行钩子方法 + + * `requestHeader = new SendMessageRequestHeader()`:设置发送消息的消息头 + + * `if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX))`:重投的发送消息 + + * `switch (communicationMode)`:异步发送一种处理方式,单向和同步同样的处理逻辑 + + `sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage()`:**发送消息** + + * `request = RemotingCommand.createRequestCommand()`:创建一个 RequestCommand 对象 + * `request.setBody(msg.getBody())`:**将消息放入请求体** + * `switch (communicationMode)`:根据不同的模式 invoke 不同的方法 + +* request():请求方法,消费者回执消息,这种消息是异步消息 + + * `requestResponseFuture = new RequestResponseFuture(correlationId, timeout, null)`:创建请求响应对象 + + * `getRequestFutureTable().put(correlationId, requestResponseFuture)`:放入RequestFutureTable 映射表中 + + * `this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback())`:**发送异步消息** + + * `return waitResponse(msg, timeout, requestResponseFuture, cost)`:用来挂起请求的方法 + + ```java + public Message waitResponseMessage(final long timeout) throws InterruptedException { + // 请求挂起 + this.countDownLatch.await(timeout, TimeUnit.MILLISECONDS); + return this.responseMsg; + } + + * 当消息被消费后,会获取消息的关联 ID,从映射表中获取消息的 RequestResponseFuture,执行下面的方法唤醒挂起线程 + + ```java + public void putResponseMessage(final Message responseMsg) { + this.responseMsg = responseMsg; + this.countDownLatch.countDown(); + } + ``` + + + + +*** + + + +#### 路由信息 + +TopicPublishInfo 类用来存储路由信息 + +成员变量: + +* 顺序消息: + + ```java + private boolean orderTopic = false; + ``` + +* 消息队列: + + ```java + private List messageQueueList = new ArrayList<>(); // 主题全部的消息队列 + private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); // 消息队列索引 + ``` + + ```java + // 【消息队列类】 + public class MessageQueue implements Comparable, Serializable { + private String topic; + private String brokerName; + private int queueId;// 队列 ID + } + ``` + +* 路由数据:主题对应的路由数据 + + ```java + private TopicRouteData topicRouteData; + ``` + + ```java + public class TopicRouteData extends RemotingSerializable { + private String orderTopicConf; + private List queueDatas; // 队列数据 + private List brokerDatas; // Broker 数据 + private HashMap/* Filter Server */> filterServerTable; + } + ``` + + ```java + public class QueueData implements Comparable { + private String brokerName; // 节点名称 + private int readQueueNums; // 读队列数 + private int writeQueueNums; // 写队列数 + private int perm; // 权限 + private int topicSynFlag; + } + ``` + + ```java + public class BrokerData implements Comparable { + private String cluster; // 集群名 + private String brokerName; // Broker节点名称 + private HashMap brokerAddrs; + } + ``` + +核心方法: + +* selectOneMessageQueue():**选择消息队列**使用 + + ```java + // 参数是上次失败时的 brokerName,可以为 null + public MessageQueue selectOneMessageQueue(final String lastBrokerName) { + if (lastBrokerName == null) { + return selectOneMessageQueue(); + } else { + // 遍历消息队列 + for (int i = 0; i < this.messageQueueList.size(); i++) { + // 获取队列的索引 + int index = this.sendWhichQueue.getAndIncrement(); + // 获取队列的下标位置 + int pos = Math.abs(index) % this.messageQueueList.size(); + if (pos < 0) + pos = 0; + // 获取消息队列 + MessageQueue mq = this.messageQueueList.get(pos); + // 与上次选择的不同就可以返回 + if (!mq.getBrokerName().equals(lastBrokerName)) { + return mq; + } + } + return selectOneMessageQueue(); + } + } + ``` + + + + + +**** + + + +### 消费者 + + + + + + + +*** + + + +### 客户端 + +#### 实例对象 + +##### 成员属性 + +MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一个客户端实例,**既服务于生产者,也服务于消费者** + +成员变量: + +* 配置信息: + + ```java + private final int instanceIndex; // 索引一般是 0,因为客户端实例一般都是一个进程只有一个 + private final String clientId; // 客户端 ID ip@pid + private final long bootTimestamp; // 客户端的启动时间 + private ServiceState serviceState; // 客户端状态 + ``` + +* 生产者消费者的映射表:key 是组名 + + ```java + private final ConcurrentMap producerTable + private final ConcurrentMap consumerTable + private final ConcurrentMap adminExtTable + ``` + +* 网络层配置: + + ```java + private final NettyClientConfig nettyClientConfig; + ``` + +* 核心功能的实现:负责将 MQ 业务层的数据转换为网络层的 RemotingCommand 对象,使用内部持有的 NettyRemotingClient 对象的 invoke 系列方法,完成网络 IO(同步、异步、单向) + + ```java + private final MQClientAPIImpl mQClientAPIImpl; + ``` + +* 本地路由数据:key 是主题名称,value 路由信息 + + ```java + private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); + +* 锁信息:两把锁,锁不同的数据 + + ```java + private final Lock lockNamesrv = new ReentrantLock(); + private final Lock lockHeartbeat = new ReentrantLock(); + ``` + +* 调度线程池:单线程,执行定时任务 + + ```java + private final ScheduledExecutorService scheduledExecutorService; + ``` + +* Broker 映射表:key 是 BrokerName + + ```java + // 物理节点映射表,value:Long 是 brokerID,【ID=0 的是主节点,其他是从节点】,String 是地址 ip:port + private final ConcurrentMap> brokerAddrTable; + // 物理节点版本映射表,String 是地址 ip:port,Integer 是版本 + ConcurrentMap> brokerVersionTable; + ``` + +* **客户端的协议处理器**:用于处理 IO 事件 + + ```java + private final ClientRemotingProcessor clientRemotingProcessor; + ``` + +* 消息服务: + + ```java + private final PullMessageService pullMessageService; // 拉消息服务 + private final RebalanceService rebalanceService; // 消费者负载均衡服务 + private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 + ``` + +* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 + + ```java + private final DefaultMQProducer defaultMQProducer; + ``` + +* 心跳次数统计: + + ```java + private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) + ``` + +* 公共配置类: + + ```java + public class ClientConfig { + // Namesrv 地址配置 + private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); + // 客户端的 IP 地址 + private String clientIP = RemotingUtil.getLocalAddress(); + // 客户端实例名称 + private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); + // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 命名空间 + protected String namespace; + protected AccessChannel accessChannel = AccessChannel.LOCAL; + + // 获取路由信息的间隔时间 30s + private int pollNameServerInterval = 1000 * 30; + // 客户端与 broker 之间的心跳周期 30s + private int heartbeatBrokerInterval = 1000 * 30; + // 消费者持久化消费的周期 5s + private int persistConsumerOffsetInterval = 1000 * 5; + private long pullTimeDelayMillsWhenException = 1000; + private boolean unitMode = false; + private String unitName; + // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 + private boolean vipChannelEnabled = Boolean.parseBoolean(); + // 语言,默认是 Java + private LanguageCode language = LanguageCode.JAVA; + } + ``` + +构造方法: + +* MQClientInstance 有参构造: + + ```java + public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) { + this.clientConfig = clientConfig; + this.instanceIndex = instanceIndex; + // Netty 相关的配置信息 + this.nettyClientConfig = new NettyClientConfig(); + // 平台核心数 + this.nettyClientConfig.setClientCallbackExecutorThreads(...); + this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS()); + // 【创建客户端协议处理器】 + this.clientRemotingProcessor = new ClientRemotingProcessor(this); + // 创建 API 实现对象 + // 参数一:客户端网络配置 + // 参数二:客户端协议处理器,注册到客户端网络层 + // 参数三:rpcHook,注册到客户端网络层 + // 参数四:客户端配置 + this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig); + + //... + // 内部生产者,指定内部生产者的组 + this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP); + } + ``` + +* MQClientAPIImpl 有参构造: + + ```java + public MQClientAPIImpl(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig) { + this.clientConfig = clientConfig; + topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); + // 创建网络层对象,参数二为 null 说明客户端并不关心 channel event + this.remotingClient = new NettyRemotingClient(nettyClientConfig, null); + // 业务处理器 + this.clientRemotingProcessor = clientRemotingProcessor; + // 注册 RpcHook + this.remotingClient.registerRPCHook(rpcHook); + // ... + // 注册回退消息的请求码 + this.remotingClient.registerProcessor(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, this.clientRemotingProcessor, null); + } + ``` + + + +*** + + + +##### 成员方法 + +* start():启动方法 + + * `synchronized (this)`:加锁保证线程安全,保证只有一个实例对象启动 + * `this.mQClientAPIImpl.start()`:启动客户端网络层,底层调用 RemotingClient 类 + * `this.startScheduledTask()`:启动定时任务 + * `this.pullMessageService.start()`:启动拉取消息服务 + * `this.rebalanceService.start()`:启动负载均衡服务 + * `this.defaultMQProducer.getDefaultMQProducerImpl().start(false)`:启动内部生产者,参数为 false 代表不启动实例 + +* startScheduledTask():**启动定时任务**,调度线程池是单线程 + + * `if (null == this.clientConfig.getNamesrvAddr())`:Namesrv 地址是空,需要两分钟拉取一次 Namesrv 地址 + + * 定时任务 1:从 Namesrv 更新客户端本地的路由数据,周期 30 秒一次 + + ```java + // 获取生产者和消费者订阅的主题集合,遍历集合,对比从 namesrv 拉取最新的主题路由数据和本地数据,是否需要更新 + MQClientInstance.this.updateTopicRouteInfoFromNameServer(); + ``` + + * 定时任务 2:周期 30 秒一次,两个任务 + + * 清理下线的 Broker 节点,遍历客户端的 Broker 物理节点映射表,将所有主题数据都不包含的 Broker 物理节点清理掉,如果被清理的 Broker 下所有的物理节点都没有了,就将该 Broker 的映射数据删除掉 + * 向在线的所有的 Broker 发送心跳数据,**同步发送的方式**,返回值是 Broker 物理节点的版本号,更新版本映射表 + + ```java + MQClientInstance.this.cleanOfflineBroker(); + MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); + ``` + + ```java + // 心跳数据 + public class HeartbeatData extends RemotingSerializable { + // 客户端 ID ip@pid + private String clientID; + // 存储客户端所有生产者数据 + private Set producerDataSet = new HashSet(); + // 存储客户端所有消费者数据 + private Set consumerDataSet = new HashSet(); + } + ``` + + * 定时任务 3:消费者持久化消费数据,周期 5 秒一次 + + ```java + MQClientInstance.this.persistAllConsumerOffset(); + ``` + + * 定时任务 4:动态调整消费者线程池,周期 1 分钟一次 + + ```java + MQClientInstance.this.adjustThreadPool(); + ``` + +* updateTopicRouteInfoFromNameServer():**更新路由数据** + + * `if (isDefault && defaultMQProducer != null)`:需要默认数据 + + `topicRouteData = ...getDefaultTopicRouteInfoFromNameServer()`:从 Namesrv 获取默认的 TBW102 的路由数据 + + `int queueNums`:遍历所有队列,为每个读写队列设置较小的队列数 + + * `topicRouteData = ...getTopicRouteInfoFromNameServer(topic)`:需要**从 Namesrv 获取**路由数据(同步) + + * `old = this.topicRouteTable.get(topic)`:获取客户端实例本地的该主题的路由数据 + + * `boolean changed = topicRouteDataIsChange(old, topicRouteData)`:对比本地和最新下拉的数据是否一致 + + * `if (changed)`:不一致进入更新逻辑 + + `cloneTopicRouteData = topicRouteData.cloneTopicRouteData()`:克隆一份最新数据 + + `Update Pub info`:更新生产者信息 + + * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:**将主题路由数据转化为发布数据** + * `impl.updateTopicPublishInfo(topic, publishInfo)`:生产者将主题的发布数据保存到它本地,方便发送消息使用 + + `Update sub info`:更新消费者信息 + + `this.topicRouteTable.put(topic, cloneTopicRouteData)`:将数据放入本地路由表 + + + +**** + + + +#### 网络通信 + +##### 成员属性 + +NettyRemotingClient 类负责客户端的网络通信 + +成员变量: + +* Netty 服务相关属性: + + ```java + private final NettyClientConfig nettyClientConfig; // 客户端的网络层配置 + private final Bootstrap bootstrap = new Bootstrap(); // 客户端网络层启动对象 + private final EventLoopGroup eventLoopGroupWorker; // 客户端网络层 Netty IO 线程组 + ``` + +* Channel 映射表: + + ```java + private final ConcurrentMap channelTables;// key 是服务器的地址,value 是通道对象 + private final Lock lockChannelTables = new ReentrantLock(); // 锁,控制并发安全 + ``` + +* 定时器:启动定时任务 + + ```java + private final Timer timer = new Timer("ClientHouseKeepingService", true) + ``` + +* 线程池: + + ```java + private ExecutorService publicExecutor; // 公共线程池 + private ExecutorService callbackExecutor; // 回调线程池,客户端发起异步请求,服务器的响应数据由回调线程池处理 + ``` + +* 事件监听器:客户端这里是 null + + ```java + private final ChannelEventListener channelEventListener; + ``` + +* Netty 配置对象: + + ```java + public class NettyClientConfig { + // 客户端工作线程数 + private int clientWorkerThreads = 4; + // 回调处理线程池 线程数:平台核心数 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 单向请求并发数,默认 65535 + private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; + // 异步请求并发数,默认 65535 + private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; + // 客户端连接服务器的超时时间限制 3秒 + private int connectTimeoutMillis = 3000; + // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) + private long channelNotActiveInterval = 1000 * 60; + // 客户端与服务器 ch 最大空闲时间 2分钟 + private int clientChannelMaxIdleTimeSeconds = 120; + + // 底层 Socket 写和收 缓冲区的大小 65535 64k + private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; + private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 客户端 netty 是否启动内存池 + private boolean clientPooledByteBufAllocatorEnable = false; + // 客户端是否超时关闭 Socket 连接 + private boolean clientCloseSocketIfTimeout = false; + } + ``` + +构造方法 + +* 无参构造: + + ```java + public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { + this(nettyClientConfig, null); + } + ``` + +* 有参构造: + + ```java + public NettyRemotingClient(nettyClientConfig, channelEventListener) { + // 父类创建了2个信号量,1、控制单向请求的并发度,2、控制异步请求的并发度 + super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue()); + this.nettyClientConfig = nettyClientConfig; + this.channelEventListener = channelEventListener; + + // 创建公共线程池 + int publicThreadNums = nettyClientConfig.getClientCallbackExecutorThreads(); + if (publicThreadNums <= 0) { + publicThreadNums = 4; + } + this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums,); + + // 创建 Netty IO 线程,1个线程 + this.eventLoopGroupWorker = new NioEventLoopGroup(1, ); + + if (nettyClientConfig.isUseTLS()) { + sslContext = TlsHelper.buildSslContext(true); + } + } + ``` + + + +**** + + + +##### 成员方法 + +* start():启动方法 + + ```java + public void start() { + // channel pipeline 内的 handler 使用的线程资源,默认 4 个 + this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(); + // 配置 netty 客户端启动类对象 + Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class) + //... + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + // 加几个handler + pipeline.addLast( + // 服务端的数据,都会来到这个 + new NettyClientHandler()); + } + }); + // 注意 Bootstrap 只是配置好客户端的元数据了,【在这里并没有创建任何 channel 对象】 + // 定时任务 扫描 responseTable 中超时的 ResponseFuture,避免客户端线程长时间阻塞 + this.timer.scheduleAtFixedRate(() -> { + NettyRemotingClient.this.scanResponseTable(); + }, 1000 * 3, 1000); + // 这里是 null,不启动 + if (this.channelEventListener != null) { + this.nettyEventExecutor.start(); + } + } + ``` + +* 单向通信: + + ```java + public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) { + // 开始时间 + long beginStartTime = System.currentTimeMillis(); + // 获取或者创建客户端与服务端(addr)的通道 channel + final Channel channel = this.getAndCreateChannel(addr); + // 条件成立说明客户端与服务端 channel 通道正常,可以通信 + if (channel != null && channel.isActive()) { + try { + // 执行 rpcHook 拓展点 + doBeforeRpcHooks(addr, request); + // 计算耗时,如果当前耗时已经超过 timeoutMillis 限制,则直接抛出异常,不再进行系统通信 + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeoutMillis < costTime) { + throw new RemotingTimeoutException("invokeSync call timeout"); + } + // 参数1:客户端-服务端通道channel + // 参数二:网络层传输对象,封装着请求数据 + // 参数三:剩余的超时限制 + RemotingCommand response = this.invokeSyncImpl(channel, request, ...); + // 后置处理 + doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response); + // 返回响应数据 + return response; + } catch (RemotingSendRequestException e) {} + } else { + this.closeChannel(addr, channel); + throw new RemotingConnectException(addr); + } + } + ``` + + + + + + +*** diff --git a/Java.md b/Java.md index c9daa70..b0a6075 100644 --- a/Java.md +++ b/Java.md @@ -73,7 +73,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, - 这种类型主要使用在需要比较大整数的系统上 - 默认值是 **` 0L`** - 例子: `long a = 100000L,Long b = -200000L` - "L"理论上不分大小写,但是若写成"l"容易与数字"1"混淆,不容易分辩,所以最好大写 + L 理论上不分大小写,但是若写成 I 容易与数字 1 混淆,不容易分辩,所以最好大写 **float:** From 3bca1287329a4553f1399ed6d2a3282378100f2c Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 9 Jan 2022 22:50:25 +0800 Subject: [PATCH 057/122] Update Java Notes --- Frame.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/Frame.md b/Frame.md index 6f84a04..183cc5f 100644 --- a/Frame.md +++ b/Frame.md @@ -4504,7 +4504,7 @@ NameServer 是一个简单的 Topic 路由注册中心,支持 Broker 的动态 NameServer 主要包括两个功能: -* Broker 路由管理,NameServer 接受 Broker 集群的注册信息,并保存下来作为路由信息的基本数据,提供**心跳检测机制**检查 Broker 活性,每 10 秒清除一次两小时没有活跃的 Broker +* Broker 路由管理,NameServer 接受 Broker 集群的注册信息,保存下来作为路由信息的基本数据,提供**心跳检测机制**检查 Broker 是否还存活,每 10 秒清除一次两小时没有活跃的 Broker * 路由信息管理,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息,然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费 NameServer 特点: @@ -5589,6 +5589,37 @@ NettyRemotingAbstract#processResponseCommand:处理响应的数据 +#### 路由信息 + +##### 信息管理 + +RouteInfoManager 类负责管理路由信息,NamesrvController 的构造方法中创建该类的实例对象,管理服务端的路由数据 + +```java +public class RouteInfoManager { + // Broker 两个小时不活跃,视为离线,被定时任务删除 + private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2; + // 读写锁,保证线程安全 + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + // 主题队列数据,一个主题对应多个队列 + private final HashMap> topicQueueTable; + // Broker 数据列表 + private final HashMap brokerAddrTable; + // 集群 + private final HashMap> clusterAddrTable; + // Broker 存活信息 + private final HashMap brokerLiveTable; + // 服务过滤 + private final HashMap/* Filter Server */> filterServerTable; +} +``` + + + +*** + + + ##### 路由注册 DefaultRequestProcessor REGISTER_BROKER 方法解析: From 411b33b4315e1edc5a5d77749f6028ac442cc01b Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 15 Jan 2022 18:55:18 +0800 Subject: [PATCH 058/122] Update Java Notes --- DB.md | 4 ++++ Frame.md | 8 ++++---- Java.md | 4 ++-- Web.md | 6 +++--- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/DB.md b/DB.md index c815155..41db319 100644 --- a/DB.md +++ b/DB.md @@ -4827,6 +4827,10 @@ CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); +参考文章:https://mp.weixin.qq.com/s/B_M09dzLe9w7cT46rdGIeQ + + + *** diff --git a/Frame.md b/Frame.md index 183cc5f..15ea0fc 100644 --- a/Frame.md +++ b/Frame.md @@ -3617,12 +3617,12 @@ RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Produce 消息发送者步骤分析: -1. 创建消息生产者 producer,并制定生产者组名 +1. 创建消息生产者 Producer,并制定生产者组名 2. 指定 Nameserver 地址 -3. 启动 producer +3. 启动 Producer 4. 创建消息对象,指定主题 Topic、Tag 和消息体 5. 发送消息 -6. 关闭生产者 producer +6. 关闭生产者 Producer 消息消费者步骤分析: @@ -3630,7 +3630,7 @@ RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,其中 Produce 2. 指定 Nameserver 地址 3. 订阅主题 Topic 和 Tag 4. 设置回调函数,处理消息 -5. 启动消费者 consumer +5. 启动消费者 Consumer diff --git a/Java.md b/Java.md index b0a6075..125e703 100644 --- a/Java.md +++ b/Java.md @@ -2551,7 +2551,7 @@ JDK 1.8:当一个字符串调用 intern() 方法时,如果 String Pool 中 * 存在一个字符串和该字符串值相等,就会返回 String Pool 中字符串的引用(需要变量接收) * 不存在,会把对象的**引用地址**复制一份放入串池,并返回串池中的引用地址,前提是堆内存有该对象,因为 Pool 在堆中,为了节省内存不再创建新对象 -JDK 1.6:将这个字符串对象尝试放入串池,如果有就不放入,返回已有的串池中的对象的地址;如果没有会把此对象复制一份,放入串池,把串池中的对象返回 +JDK 1.6:将这个字符串对象尝试放入串池,如果有就不放入,返回已有的串池中的对象的引用;如果没有会把此对象复制一份,放入串池,把串池中的对象返回 ```java public class Demo { @@ -2600,7 +2600,7 @@ String s = new String("ab"); -##### 面试问题 +##### 常见问题 问题一: diff --git a/Web.md b/Web.md index 5262ce7..3236ec1 100644 --- a/Web.md +++ b/Web.md @@ -2235,13 +2235,13 @@ HTTPS 工作流程:服务器端的公钥和私钥,用来进行非对称加 Cookie: Idea-b77ddca6=4bc282fe-febf-4fd1-b6c9-72e9e0f381e8 ``` - * 面试题:**Get 和POST比较** + * **Get 和 POST 比较** 作用:GET 用于获取资源,而 POST 用于传输实体主体 - 参数:GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看 + 参数:GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中(GET 也有请求体,POST 也可以通过 URL 传输参数)。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看 - 安全:安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。GET方法是安全的,而POST不是,因为 POST 的目的是传送实体主体内容 + 安全:安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。GET 方法是安全的,而 POST 不是,因为 POST 的目的是传送实体主体内容 * 安全的方法除了 GET 之外还有:HEAD、OPTIONS * 不安全的方法除了 POST 之外还有 PUT、DELETE From 3bdd11667b7b40fb65d3f531843fa1c2a575b46b Mon Sep 17 00:00:00 2001 From: Seazean Date: Fri, 21 Jan 2022 00:03:53 +0800 Subject: [PATCH 059/122] Update Java Notes --- DB.md | 44 +++- Frame.md | 615 ++++++++++++++++++++++++++++++++++++++----------------- Java.md | 91 ++++---- 3 files changed, 508 insertions(+), 242 deletions(-) diff --git a/DB.md b/DB.md index 41db319..61b9c13 100644 --- a/DB.md +++ b/DB.md @@ -5702,19 +5702,18 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 ### 隔离级别 +#### 四种级别 + 事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,**不同的事务之间不该互相影响**,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别,否则就会产生问题。 隔离级别分类: | 隔离级别 | 名称 | 会引发的问题 | 数据库默认隔离级别 | | ---------------- | -------- | ---------------------- | ------------------- | -| read uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | -| read committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | -| repeatable read | 可重复读 | 幻读 | MySQL | -| serializable | 可串行化 | 无 | | - -* 串行化:让所有事务按顺序单独执行,写操作会加写锁,读操作会加读锁 -* 可串行化:让所有操作相同数据的事务顺序执行,通过加锁实现 +| Read Uncommitted | 读未提交 | 脏读、不可重复读、幻读 | | +| Read Committed | 读已提交 | 不可重复读、幻读 | Oracle / SQL Server | +| Repeatable Read | 可重复读 | 幻读 | MySQL | +| Serializable | 可串行化 | 无 | | 一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差 @@ -5745,6 +5744,31 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 +*** + + + +#### 加锁分析 + +InnoDB 存储引擎支持事务,所以加锁分析是基于该存储引擎 + +* Read Uncommitted 级别,任何操作都不会加锁 + +* Read Committed 级别,增删改操作会加写锁(行锁),读操作不加锁 + + MySQL 做了优化,在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引,需要在Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象,这种情况同样适用于 RR + +* Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,加了写锁后其他事务就无法修改数据,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解 + +* Serializable 级别,读加共享锁,写加排他锁,读写互斥,使用的悲观锁的理论,实现简单,数据更加安全,但是并发能力非常差 + + * 串行化:让所有事务按顺序单独执行,写操作会加写锁,读操作会加读锁 + * 可串行化:让所有操作相同数据的事务顺序执行,通过加锁实现 + + + +参考文章:https://tech.meituan.com/2014/08/20/innodb-lock.html + *** @@ -6636,7 +6660,7 @@ InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用 - 共享锁 (S):又称为读锁,简称 S 锁,多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改 - 排他锁 (X):又称为写锁,简称 X 锁,不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改 -对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放(在事务中加的锁,会**在事务中止或提交时自动释放**);对于普通 SELECT 语句,不会加任何锁 +RR 隔离界别下,对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会**自动给涉及数据集加排他锁**(行锁),在 commit 的时候会自动释放(在事务中加的锁,会**在事务中止或提交时自动释放**);对于普通 SELECT 语句,不会加任何锁(只是针对 InnoDB 层来说的,因为在 Server 层会加 MDL 读锁),通过 MVCC 防止冲突 锁的兼容性: @@ -6654,7 +6678,7 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 -**** +*** @@ -6999,6 +7023,8 @@ lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁 ### 乐观锁 +悲观锁:在整个数据处理过程中,将数据处于锁定状态,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据,修改删除数据时也加锁,其它事务同样无法读取这些数据 + 悲观锁和乐观锁使用前提: - 对于读的操作远多于写的操作的时候,一个更新操作加锁会阻塞所有的读取操作,降低了吞吐量,最后需要释放锁,锁是需要一些开销的,这时候可以选择乐观锁 diff --git a/Frame.md b/Frame.md index 15ea0fc..c126bb8 100644 --- a/Frame.md +++ b/Frame.md @@ -3818,7 +3818,7 @@ public class Consumer { - 全局顺序:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。 适用于性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景 - 分区顺序:对于指定的一个 Topic,所有消息根据 sharding key 进行分区,同一个分组内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用于性能要求高,以 sharding key 作为分区字段,在同一个区中严格的按照 FIFO 原则进行消息发布和消费的场景 -在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列),而消费消息是从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当发送和消费参与的 queue 只有一个,则是全局有序;如果多个queue 参与,则为分区有序,即相对每个 queue,消息都是有序的 +在默认的情况下消息发送会采取 Round Robin 轮询方式把消息发送到不同的 queue(分区队列),而消费消息是从多个 queue 上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个 queue 中,消费的时候只从这个 queue 上依次拉取,则就保证了顺序。当**发送和消费参与的 queue 只有一个**,则是全局有序;如果多个queue 参与,则为分区有序,即相对每个 queue,消息都是有序的 @@ -4346,7 +4346,7 @@ RocketMQ 的具体实现策略:如果写入的是事务消息,对消息的 T -##### OP消息 +##### OP 消息 一阶段写入不可见的消息后,二阶段操作: @@ -4579,6 +4579,106 @@ At least Once:至少一次,指每个消息必须投递一次,Consumer 先 + + +### 存储机制 + +#### 存储结构 + +RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 + +RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,CommitLog 是消息真正的**物理存储**文件,ConsumeQueue 是消息的逻辑队列,类似数据库的**索引节点**,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** + +每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) + +* 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 索引** + +RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 + +服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 + + + +**** + + + +#### 存储优化 + +##### 内存映射 + +操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: + +* read:读取本地文件内容 + +* write:将读取的内容通过网络发送出去 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) + +补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 + +通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用**零拷贝技术**,提高消息存盘和网络发送的速度。 + +RocketMQ 通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 + +MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 + + + +*** + + + +##### 页缓存 + +页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** + +* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 +* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理,最大 128K) + +在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 + + + +*** + + + +#### 刷盘机制 + +两种持久化的方案: + +* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 +* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 + +RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使用**顺序 IO**,因为磁盘的顺序读写要比随机读写快很多 + +* 同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 + +* 异步刷盘:利用 OS 的 PageCache,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 + +通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 + +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) + + + +官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + + + + + +**** + + + + + ### 集群设计 #### 集群模式 @@ -4738,8 +4838,6 @@ latencyFaultTolerance 机制是实现消息发送高可用的核心关键所在 #### 原理解析 -==todo:暂时 copy 官方文档,学习源码后更新,真想搞懂过程还需要研究一下源码== - 在 Consumer 启动后,会通过定时任务不断地向 RocketMQ 集群中的所有 Broker 实例发送心跳包。Broke r端在收到 Consumer 的心跳消息后,会将它维护 在ConsumerManager 的本地缓存变量 consumerTable,同时并将封装后的客户端网络通道信息保存在本地缓存变量 channelInfoTable 中,为 Consumer 端的负载均衡提供可以依据的元数据信息 Consumer 端实现负载均衡的核心类 **RebalanceImpl** @@ -4770,13 +4868,15 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** + + **** -### 消息机制 +### 消息查询 -#### 消息查询 +#### 查询方式 RocketMQ 支持按照两种维度进行消息查询:按照 Message ID 查询消息、按照 Message Key 查询消息 @@ -4794,6 +4894,35 @@ RocketMQ 支持按照两种维度进行消息查询:按照 Message ID 查询 +#### 索引机制 + +RocketMQ 的索引文件逻辑结构,类似 JDK 中 HashMap 的实现,具体结构如下: + +![](https://gitee.com/seazean/images/raw/master/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` 来做索引 + +整个 Index File 的结构如图,40 Byte 的 Header 用于保存一些总的统计信息,`4*500W` 的 Slot Table 并不保存真正的索引数据,而是保存每个槽位对应的单向链表的**头指针**,即一个 Index File 可以保存 2000W 个索引,`20*2000W` 是**真正的索引数据** + +索引数据包含了 Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段,一共 20 Byte + +* NextIndex offset 即前面读出来的 slotValue,如果有 hash 冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来 +* Timestamp 记录的是消息 storeTimestamp 之间的差,并不是一个绝对的时间 + + + +参考文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md + + + + + +*** + + + +### 消息重试 + #### 消息重投 生产者在发送消息时,同步消息和异步消息失败会重投,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但当出现消息量大、网络抖动时,可能会造成消息重复;生产者主动重发、Consumer 负载变化也会导致重复消息。 @@ -4822,7 +4951,7 @@ Consumer 消费消息失败后,提供了一种重试机制,令消息再消 - 由于消息本身的原因,例如反序列化失败,消息数据本身无法处理等。这种错误通常需要跳过这条消息,再消费其它消息,而这条失败的消息即使立刻重试消费,99% 也不成功,所以需要提供一种定时重试机制,即过 10秒 后再重试 - 由于依赖的下游应用服务不可用,例如 DB 连接不可用,外系统网络不可达等。这种情况即使跳过当前失败的消息,消费其他消息同样也会报错,这种情况建议应用 sleep 30s,再消费下一条消息,这样可以减轻 Broker 重试消息的压力 -RocketMQ 会为每个消费组都设置一个 Topic 名称为 `%RETRY%+consumerGroup` 的重试队列(这个 Topic 的重试队列是针对消费组,而不是针对每个 Topic 设置的),用于暂时保存因为各种异常而导致 Consumer 端无法消费的消息 +RocketMQ 会为每个消费组都设置一个 Topic 名称为 `%RETRY%+consumerGroup` 的重试队列(这个 Topic 的重试队列是**针对消费组**,而不是针对每个 Topic 设置的),用于暂时保存因为各种异常而导致 Consumer 端无法消费的消息 * 顺序消息的重试,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时应用会出现消息消费被阻塞的情况。所以在使用顺序消息时,必须保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生 @@ -5714,100 +5843,6 @@ RouteInfoManager#registerBroker:注册 Broker 的信息 ### 存储端 -#### 存储机制 - -##### 存储结构 - -RocketMQ 中 Broker 负责存储消息转发消息,所以以下的结构是存储在 Broker Server 上的,生产者和消费者与 Broker 进行消息的收发是通过主题对应的 Message Queue 完成,类似于通道 - -RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,CommitLog 是消息真正的**物理存储**文件,ConsumeQueue 是消息的逻辑队列,类似数据库的**索引节点**,存储的是指向物理存储的地址。**每个 Topic 下的每个 Message Queue 都有一个对应的 ConsumeQueue 文件** - -每条消息都会有对应的索引信息,Consumer 通过 ConsumeQueue 这个结构来读取消息实体内容 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) - -* 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 索引** - -RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 - -服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 - - - -**** - - - -##### 存储优化 - -###### 内存映射 - -操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: - -* read:读取本地文件内容 - -* write:将读取的内容通过网络发送出去 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-文件与网络操作.png) - -补充:Prog → NET → I/O → 零拷贝部分的笔记详解相关内容 - -通过使用 mmap 的方式,可以省去向用户态的内存复制,RocketMQ 充分利用**零拷贝技术**,提高消息存盘和网络发送的速度。 - -RocketMQ 通过 MappedByteBuffer 对文件进行读写操作,利用了 NIO 中的 FileChannel 模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率 - -MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文件至用户态的虚拟内存,所以 RocketMQ 默认设置单个 CommitLog 日志数据文件为 1G。RocketMQ 的文件存储使用定长结构来存储,方便一次将整个文件映射至内存 - - - -*** - - - -###### 页缓存 - -页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** - -* 对于数据的写入,OS 会先写入至 Cache 内,随后通过异步的方式由 pdflush 内核线程将 Cache 内的数据刷盘至物理磁盘上 -* 对于数据的读取,如果一次读取文件时出现未命中 PageCache 的情况,OS 从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取(局部性原理,最大 128K) - -在 RocketMQ 中,ConsumeQueue 逻辑消费队列存储的数据较少,并且是顺序读取,在 PageCache 机制的预读取作用下,Consume Queue 文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。但是 CommitLog 消息存储的日志数据文件读取内容时会产生较多的随机访问读取,严重影响性能。选择合适的系统 IO 调度算法和固态硬盘,比如设置调度算法为 Deadline,随机读的性能也会有所提升 - - - -*** - - - -##### 刷盘机制 - -两种持久化的方案: - -* 关系型数据库 DB:IO 读写性能比较差,如果 DB 出现故障,则 MQ 的消息就无法落盘存储导致线上故障,可靠性不高 -* 文件系统:消息刷盘至所部署虚拟机/物理机的文件系统来做持久化,分为异步刷盘和同步刷盘两种模式。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式,除非部署 MQ 机器本身或是本地磁盘挂了,一般不会出现无法持久化的问题 - -RocketMQ 采用文件系统的方式,无论同步还是异步刷盘,都使用**顺序 IO**,因为磁盘的顺序读写要比随机读写快很多 - -* 同步刷盘:只有在消息真正持久化至磁盘后 RocketMQ 的 Broker 端才会真正返回给 Producer 端一个成功的 ACK 响应,保障 MQ消息的可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多 - -* 异步刷盘:利用 OS 的 PageCache,只要消息写入内存 PageCache 即可将成功的 ACK 返回给 Producer 端,降低了读写延迟,提高了 MQ 的性能和吞吐量。消息刷盘采用**后台异步线程**提交的方式进行,当内存里的消息量积累到一定程度时,触发写磁盘动作 - -通过 Broker 配置文件里的 flushDiskType 参数设置采用什么方式,可以配置成 SYNC_FLUSH、ASYNC_FLUSH 中的一个 - -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-刷盘机制.png) - - - -官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md - - - -*** - - - #### MappedFile ##### 成员属性 @@ -6346,31 +6381,6 @@ ConsumeQueue 启动阶段方法: #### IndexFile -##### 索引机制 - -RocketMQ 的索引文件逻辑结构,类似 JDK 中 HashMap 的实现,具体结构如下: - -![](https://gitee.com/seazean/images/raw/master/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` 来做索引 - -整个 Index File 的结构如图,40 Byte 的 Header 用于保存一些总的统计信息,`4*500W` 的 Slot Table 并不保存真正的索引数据,而是保存每个槽位对应的单向链表的**头指针**,即一个 Index File 可以保存 2000W 个索引,`20*2000W` 是**真正的索引数据** - -索引数据包含了 Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段,一共 20 Byte - -* NextIndex offset 即前面读出来的 slotValue,如果有 hash 冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来 -* Timestamp 记录的是消息 storeTimestamp 之间的差,并不是一个绝对的时间 - - - -参考文档:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md - - - -*** - - - ##### 成员属性 IndexFile 类成员属性 @@ -7355,6 +7365,223 @@ TopicPublishInfo 类用来存储路由信息 ### 消费者 +#### 消费者类 + +##### 默认消费 + +DefaultMQPushConsumer 类是默认的消费者类 + +成员变量: + +* 消费者实现类: + + ```java + protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; + ``` + +* 消费属性: + + ```java + private String consumerGroup; // 消费者组 + private MessageModel messageModel = MessageModel.CLUSTERING; // 消费模式,默认集群模式 + ``` + +* 订阅信息:key 是主题,value 是过滤表达式,一般是 tag + + ```java + private Map subscription = new HashMap() + ``` + +* 消息监听器:**消息处理逻辑**,并发消费 MessageListenerConcurrently,顺序(分区)消费 MessageListenerOrderly + + ```java + private MessageListener messageListener; + ``` + +* 消费位点:当从 Broker 获取当前组内该 queue 的 offset 不存在时,consumeFromWhere 才有效,默认值代表从队列的最后 offset 开始消费,当队列内再有一条新的 msg 加入时,消费者才会去消费 + + ```java + private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET; + ``` + +* 消费时间戳:当消费位点配置的是 CONSUME_FROM_TIMESTAMP 时,并且服务器 Group 内不存在该 queue 的 offset 时,会使用该时间戳进行消费 + + ```java + private String consumeTimestamp = UtilAll.timeMillisToHumanString3(System.currentTimeMillis() - (1000 * 60 * 30));// 消费者创建时间 - 30秒,转换成 格式: 年月日小时分钟秒,比如 20220203171201 + ``` + +* 队列分配策略:主题下的队列分配策略,RebalanceImpl 对象依赖该算法 + + ```java + private AllocateMessageQueueStrategy allocateMessageQueueStrategy; + ``` + +* 消费进度存储器: + + ```java + private OffsetStore offsetStore; + ``` + +核心方法: + +* start():启动消费者 + + ```java + public void start() + ``` + +* shutdown():关闭消费者 + + ```java + public void shutdown() + ``` + +* registerMessageListener():注册消息监听器 + + ```java + public void registerMessageListener(MessageListener messageListener) + ``` + +* subscribe():添加订阅信息 + + ```java + public void subscribe(String topic, String subExpression) + ``` + +* unsubscribe():删除订阅指定主题的信息 + + ```java + public void unsubscribe(String topic) + ``` + +* suspend():停止消费 + + ```java + public void suspend() + ``` + +* resume():恢复消费 + + ```java + public void resume() + ``` + + + +*** + + + +##### 默认实现 + +DefaultMQPushConsumerImpl 是默认消费者的实现类 + +成员变量: + +* 客户端实例:整个进程内只有一个客户端实例对象 + + ```java + private MQClientInstance mQClientFactory; + ``` + +* 消费者实例:门面对象 + + ```java + private final DefaultMQPushConsumer defaultMQPushConsumer; + ``` + +* **负载均衡**:分配订阅主题的队列给当前消费者,20秒钟一个周期执行 Rebalance 算法(客户端实例触发) + + ```java + private final RebalanceImpl rebalanceImpl = new RebalancePushImpl(this); + ``` + +* 消费者信息: + + ```java + private final long consumerStartTimestamp; // 消费者启动时间 + private volatile ServiceState serviceState; // 消费者状态 + private volatile boolean pause = false; // 是否暂停 + private boolean consumeOrderly = false; // 是否顺序消费 + ``` + +* **拉取消息**:封装拉消息的 API,服务器 Broker 返回结果中包含下次 Pull 时推荐的 BrokerId,根据本次请求数据的冷热程度进行推荐 + + ```java + private PullAPIWrapper pullAPIWrapper; + ``` + +* **消息消费**服务:并发消费和顺序消费 + + ```java + private ConsumeMessageService consumeMessageService; + ``` + +* 流控: + + ```java + private long queueFlowControlTimes = 0; // 队列流控次数,默认每1000次流控,进行一次日志打印 + private long queueMaxSpanFlowControlTimes = 0; // 流控使用,控制打印日志 + ``` + +* HOOK:钩子方法 + + ```java + // 过滤消息 hook + private final ArrayList filterMessageHookList; + // 消息执行hook,在消息处理前和处理后分别执行 hook.before hook.after 系列方法 + private final ArrayList consumeMessageHookList; + ``` + +核心方法: + +* start():加锁保证线程安全 + + ```java + public synchronized void start() + ``` + + * `this.checkConfig()`:检查配置,包括组名、消费模式、订阅信息、消息监听器等 + * `this.copySubscription()`:拷贝订阅信息到 RebalanceImpl 对象 + * `this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData)`:将订阅信息加入 rbl 的 map 中 + * `this.messageListenerInner = ...getMessageListener()`:将消息监听器保存到实例对象 + * `switch (this.defaultMQPushConsumer.getMessageModel())`:判断消费模式,广播模式下直接返回 + * `final String retryTopic`:当前**消费者组重试的主题名**,规则 `%RETRY%ConsumerGroup` + * `SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData()`:创建重试主题的订阅数据对象 + * `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.offsetStore = new RemoteBrokerOffsetStore()`:默认集群模式下创建消息进度存储器 + * `this.consumeMessageService = ...`:根据消息监听器的类型创建消费服务 + * `this.consumeMessageService.start()`:启动消费服务 + * `boolean registerOK = mQClientFactory.registerConsumer()`:**将消费者注册到客户端实例中**,客户端提供的服务: + * 心跳服务:把订阅数据同步到订阅主题的 Broker + * 拉消息服务:内部 PullMessageService 启动线程,基于 PullRequestQueue 工作,消费者负载均衡分配到队列后会向该队列提交 PullRequest + * 队列负载服务:每 20 秒调用一次 `consumer.doRebalance()` 接口 + * 消息进度持久化 + * 动态调整消费者、消费服务线程池 + * `mQClientFactory.start()`:启动客户端实例 + * ` this.updateTopic`:从 nameserver 获取主题路由数据,生成主题集合放入 rbl 对象的 table + * `this.mQClientFactory.checkClientInBroker()`:检查服务器是否支持消息过滤模式,一般使用 tag 过滤,服务器默认支持 + * `this.mQClientFactory.sendHeartbeatToAllBrokerWithLock()`:向所有已知的 Broker 节点,发送心跳数据 + * `this.mQClientFactory.rebalanceImmediately()`:唤醒 rbl 线程,触发负载均衡执行 + + + + + + + + + + + + + + + @@ -7367,6 +7594,77 @@ TopicPublishInfo 类用来存储路由信息 ### 客户端 +#### 公共配置 + +公共的配置信息类 + +* ClientConfig 类 + + ```java + public class ClientConfig { + // Namesrv 地址配置 + private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); + // 客户端的 IP 地址 + private String clientIP = RemotingUtil.getLocalAddress(); + // 客户端实例名称 + private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); + // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 命名空间 + protected String namespace; + protected AccessChannel accessChannel = AccessChannel.LOCAL; + + // 获取路由信息的间隔时间 30s + private int pollNameServerInterval = 1000 * 30; + // 客户端与 broker 之间的心跳周期 30s + private int heartbeatBrokerInterval = 1000 * 30; + // 消费者持久化消费的周期 5s + private int persistConsumerOffsetInterval = 1000 * 5; + private long pullTimeDelayMillsWhenException = 1000; + private boolean unitMode = false; + private String unitName; + // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 + private boolean vipChannelEnabled = Boolean.parseBoolean(); + // 语言,默认是 Java + private LanguageCode language = LanguageCode.JAVA; + } + ``` + +* NettyClientConfig + + ```java + public class NettyClientConfig { + // 客户端工作线程数 + private int clientWorkerThreads = 4; + // 回调处理线程池 线程数:平台核心数 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 单向请求并发数,默认 65535 + private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; + // 异步请求并发数,默认 65535 + private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; + // 客户端连接服务器的超时时间限制 3秒 + private int connectTimeoutMillis = 3000; + // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) + private long channelNotActiveInterval = 1000 * 60; + // 客户端与服务器 ch 最大空闲时间 2分钟 + private int clientChannelMaxIdleTimeSeconds = 120; + + // 底层 Socket 写和收 缓冲区的大小 65535 64k + private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; + private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 客户端 netty 是否启动内存池 + private boolean clientPooledByteBufAllocatorEnable = false; + // 客户端是否超时关闭 Socket 连接 + private boolean clientCloseSocketIfTimeout = false; + } + ``` + + + +*** + + + #### 实例对象 ##### 成员属性 @@ -7442,7 +7740,7 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 ```java private final PullMessageService pullMessageService; // 拉消息服务 private final RebalanceService rebalanceService; // 消费者负载均衡服务 - private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 + private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 ``` * 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 @@ -7457,38 +7755,6 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) ``` -* 公共配置类: - - ```java - public class ClientConfig { - // Namesrv 地址配置 - private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); - // 客户端的 IP 地址 - private String clientIP = RemotingUtil.getLocalAddress(); - // 客户端实例名称 - private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); - // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 - private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); - // 命名空间 - protected String namespace; - protected AccessChannel accessChannel = AccessChannel.LOCAL; - - // 获取路由信息的间隔时间 30s - private int pollNameServerInterval = 1000 * 30; - // 客户端与 broker 之间的心跳周期 30s - private int heartbeatBrokerInterval = 1000 * 30; - // 消费者持久化消费的周期 5s - private int persistConsumerOffsetInterval = 1000 * 5; - private long pullTimeDelayMillsWhenException = 1000; - private boolean unitMode = false; - private String unitName; - // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 - private boolean vipChannelEnabled = Boolean.parseBoolean(); - // 语言,默认是 Java - private LanguageCode language = LanguageCode.JAVA; - } - ``` - 构造方法: * MQClientInstance 有参构造: @@ -7550,7 +7816,7 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 * `this.startScheduledTask()`:启动定时任务 * `this.pullMessageService.start()`:启动拉取消息服务 * `this.rebalanceService.start()`:启动负载均衡服务 - * `this.defaultMQProducer.getDefaultMQProducerImpl().start(false)`:启动内部生产者,参数为 false 代表不启动实例 + * `this.defaultMQProducer...start(false)`:启动内部生产者,参数为 false 代表不启动实例 * startScheduledTask():**启动定时任务**,调度线程池是单线程 @@ -7672,35 +7938,6 @@ NettyRemotingClient 类负责客户端的网络通信 private final ChannelEventListener channelEventListener; ``` -* Netty 配置对象: - - ```java - public class NettyClientConfig { - // 客户端工作线程数 - private int clientWorkerThreads = 4; - // 回调处理线程池 线程数:平台核心数 - private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); - // 单向请求并发数,默认 65535 - private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; - // 异步请求并发数,默认 65535 - private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; - // 客户端连接服务器的超时时间限制 3秒 - private int connectTimeoutMillis = 3000; - // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) - private long channelNotActiveInterval = 1000 * 60; - // 客户端与服务器 ch 最大空闲时间 2分钟 - private int clientChannelMaxIdleTimeSeconds = 120; - - // 底层 Socket 写和收 缓冲区的大小 65535 64k - private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; - private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; - // 客户端 netty 是否启动内存池 - private boolean clientPooledByteBufAllocatorEnable = false; - // 客户端是否超时关闭 Socket 连接 - private boolean clientCloseSocketIfTimeout = false; - } - ``` - 构造方法 * 无参构造: diff --git a/Java.md b/Java.md index 125e703..3004ef2 100644 --- a/Java.md +++ b/Java.md @@ -630,8 +630,9 @@ public class Test1 { 可变参数在方法内部本质上就是一个数组。 可变参数的注意事项: - 1.一个形参列表中可变参数只能有一个! - 2.可变参数必须放在形参列表的**最后面**! + +* 一个形参列表中可变参数只能有一个 +* 可变参数必须放在形参列表的**最后面** ```java public static void main(String[] args) { @@ -929,7 +930,7 @@ Java 的参数是以**值传递**的形式传入方法中 public class EnumDemo { public static void main(String[] args){ // 获取索引 - Season s = Season.SPRING;/ + Season s = Season.SPRING; System.out.println(s); //SPRING System.out.println(s.ordinal()); // 0,代表索引,summer 就是 1 s.s.doSomething(); @@ -3291,7 +3292,7 @@ BigDecimal divide = bd1.divide(参与运算的对象,小数点后精确到多少 正则表达式的作用:是一些特殊字符组成的校验规则,可以校验信息的正确性,校验邮箱、电话号码、金额等。 -比如检验qq号: +比如检验 qq 号: ```java public static boolean checkQQRegex(String qq){ @@ -3333,7 +3334,7 @@ java.util.regex 包主要包括以下三个类: ##### 特殊字符 -\r\n 是Windows中的文本行结束标签,在Unix/Linux则是 \n +\r\n 是 Windows 中的文本行结束标签,在 Unix/Linux 则是 \n | 元字符 | 说明 | | ------ | ------------------------------------------------------------ | @@ -3342,8 +3343,8 @@ java.util.regex 包主要包括以下三个类: | \n | 换行符 | | \r | 回车符 | | \t | 制表符 | -| \\ | 代表\本身 | -| () | 使用( )定义一个子表达式。子表达式的内容可以当成一个独立元素 | +| \\ | 代表 \ 本身 | +| () | 使用 () 定义一个子表达式。子表达式的内容可以当成一个独立元素 | @@ -3353,12 +3354,11 @@ java.util.regex 包主要包括以下三个类: ##### 标准字符 -标准字符集合 -能够与”多种字符“匹配的表达式。注意区分大小写,大写是相反的意思。只能校验**"单"**个字符。 +能够与多种字符匹配的表达式,注意区分大小写,大写是相反的意思,只能校验**单**个字符。 | 元字符 | 说明 | | ------ | ------------------------------------------------------------ | -| . | 匹配任意一个字符(除了换行符),如果要匹配包括“\n”在内的所有字符,一般用[\s\S] | +| . | 匹配任意一个字符(除了换行符),如果要匹配包括 \n 在内的所有字符,一般用 [\s\S] | | \d | 数字字符,0~9 中的任意一个,等价于 [0-9] | | \D | 非数字字符,等价于 [ ^0-9] | | \w | 大小写字母或数字或下划线,等价于[a-zA-Z_0-9_] | @@ -3376,7 +3376,7 @@ java.util.regex 包主要包括以下三个类: ##### 自定义符 -自定义符号集合,[ ]方括号匹配方式,能够匹配方括号中**任意一个**字符 +自定义符号集合,[ ] 方括号匹配方式,能够匹配方括号中**任意一个**字符 | 元字符 | 说明 | | ------------ | ----------------------------------------- | @@ -3388,10 +3388,10 @@ java.util.regex 包主要包括以下三个类: | [a-z&&[m-p]] | 匹配 a 到 z 并且 m 到 p:[a-dm-p](交集) | | [^] | 取反 | -* 正则表达式的特殊符号,被包含到中括号中,则失去特殊意义,除了^,-之外,需要在前面加 \ +* 正则表达式的特殊符号,被包含到中括号中,则失去特殊意义,除了 ^,- 之外,需要在前面加 \ * 标准字符集合,除小数点外,如果被包含于中括号,自定义字符集合将包含该集合。 - 比如:[\d. \ -+]将匹配:数字、小数点、+、- + 比如:[\d. \ -+] 将匹配:数字、小数点、+、- @@ -3403,17 +3403,17 @@ java.util.regex 包主要包括以下三个类: 修饰匹配次数的特殊符号。 -* 匹配次数中的贪婪模式(匹配字符越多越好,默认!),\* 和 + 都是贪婪型元字符。 -* 匹配次数中的非贪婪模式(匹配字符越少越好,修饰匹配次数的特殊符号后再加上一个 "?" 号) +* 匹配次数中的贪婪模式(匹配字符越多越好,默认 !),\* 和 + 都是贪婪型元字符。 +* 匹配次数中的非贪婪模式(匹配字符越少越好,修饰匹配次数的特殊符号后再加上一个 ? 号) -| 元字符 | 说明 | -| ------ | -------------------------------- | -| X? | X一次或一次也没,有相当于 {0,1} | -| X* | X不出现或出现任意次,相当于 {0,} | -| X+ | X至少一次,相当于 {1,} | -| X{n} | X恰好 n 次 | -| {n,} | X至少 n 次 | -| {n,m} | X至少 n 次,但是不超过 m 次 | +| 元字符 | 说明 | +| ------ | --------------------------------- | +| X? | X 一次或一次也没,有相当于 {0,1} | +| X* | X 不出现或出现任意次,相当于 {0,} | +| X+ | X 至少一次,相当于 {1,} | +| X{n} | X 恰好 n 次 | +| {n,} | X 至少 n 次 | +| {n,m} | X 至少 n 次,但是不超过 m 次 | @@ -3443,16 +3443,16 @@ java.util.regex 包主要包括以下三个类: 捕获组是把多个字符当一个单独单元进行处理的方法,它通过对括号内的字符分组来创建。 -在表达式`((A)(B(C)))`,有四个这样的组:((A)(B(C)))、(A)、(B(C))、(C)(按照括号从左到右依次为 group(1)...) +在表达式 `((A)(B(C)))`,有四个这样的组:((A)(B(C)))、(A)、(B(C))、(C)(按照括号从左到右依次为 group(1)...) * 调用 matcher 对象的 groupCount 方法返回一个 int 值,表示 matcher 对象当前有多个捕获组。 -* 特殊的组group(0)、group(),它代表整个表达式,该组不包括在 groupCount 的返回值中。 +* 特殊的组 group(0)、group(),代表整个表达式,该组不包括在 groupCount 的返回值中。 | 表达式 | 说明 | | ------------------------- | ------------------------------------------------------------ | | \| (分支结构) | 左右两边表达式之间 "或" 关系,匹配左边或者右边 | -| () (捕获组) | (1) 在被修饰匹配次数的时候,括号中的表达式可以作为整体被修饰
(2) 取匹配结果的时候,括号中的表达式匹配到的内容可以被单独得到
(3) 每一对括号分配一个编号,()的捕获根据左括号的顺序从1开始自动编号。捕获元素编号为零的第一个捕获是由整个正则表达式模式匹配的文本 | -| (?:Expression) 非捕获组 | 一些表达式中,不得不使用( ),但又不需要保存( )中子表达式匹配的内容,这时可以用非捕获组来抵消使用( )带来的副作用。 | +| () (捕获组) | (1) 在被修饰匹配次数的时候,括号中的表达式可以作为整体被修饰
(2) 取匹配结果的时候,括号中的表达式匹配到的内容可以被单独得到
(3) 每一对括号分配一个编号,()的捕获根据左括号的顺序从 1 开始自动编号。捕获元素编号为零的第一个捕获是由整个正则表达式模式匹配的文本 | +| (?:Expression) 非捕获组 | 一些表达式中,不得不使用( ),但又不需要保存 () 中子表达式匹配的内容,这时可以用非捕获组来抵消使用( )带来的副作用。 | @@ -3470,17 +3470,17 @@ java.util.regex 包主要包括以下三个类: * **把匹配到的字符重复一遍在进行匹配** -* 应用1: +* 应用 1: ```java String regex = "((\d)3)\1[0-9](\w)\2{2}"; ``` - * 首先匹配((\d)3),其次\1匹配((\d)3)已经匹配到的内容,\2匹配(\d), {2}指的是\2的值出现两次 - * 实例:23238n22(匹配到2未来就继续匹配2) + * 首先匹配 ((\d)3),其次 \1 匹配 ((\d)3) 已经匹配到的内容,\2 匹配 (\d), {2} 指的是 \2 的值出现两次 + * 实例:23238n22(匹配到 2 未来就继续匹配 2) * 实例:43438n44 -* 应用2:爬虫 +* 应用 2:爬虫 ```java String regex = "<(h[1-6])>\w*?<\/\1>"; @@ -3506,7 +3506,7 @@ java.util.regex 包主要包括以下三个类: * 只进行子表达式的匹配,匹配内容不计入最终的匹配结果,是零宽度 -* 判断当前位置的前后字符,是否符合指定的条件,但不匹配前后的字符。**是对位置的匹配**。 +* 判断当前位置的前后字符,是否符合指定的条件,但不匹配前后的字符,**是对位置的匹配** * 正则表达式匹配过程中,如果子表达式匹配到的是字符内容,而非位置,并被保存到最终的匹配结果中,那么就认为这个子表达式是占有字符的;如果子表达式匹配的仅仅是位置,或者匹配的内容并不保存到最终的匹配结果中,那么就认为这个子表达式是**零宽度**的。占有字符还是零宽度,是针对匹配的内容是否保存到最终的匹配结果中而言的 @@ -3555,7 +3555,7 @@ Pattern 类: Matcher 类: * `boolean find()`:扫描输入的序列,查找与该模式匹配的下一个子序列 -* `String group()`:返回与上一个匹配的输入子序列。同group(0),匹配整个表达式的子字符串 +* `String group()`:返回与上一个匹配的输入子序列,同 group(0),匹配整个表达式的子字符串 * `String group(int group)`:返回在上一次匹配操作期间由给定组捕获的输入子序列 * `int groupCount()`:返回此匹配器模式中捕获组的数量 @@ -3604,8 +3604,8 @@ public class Demo02 { } ``` -* 正则表达式改为`"(([a-z]+)(?:[0-9]+))"` 没有group(3) 因为是非捕获组 -* 正则表达式改为`"([a-z]+)([0-9]+)"` 没有 group(3) aa232 - aa --232 +* 正则表达式改为 `"(([a-z]+)(?:[0-9]+))"` 没有 group(3) 因为是非捕获组 +* 正则表达式改为 `"([a-z]+)([0-9]+)"` 没有 group(3) aa232 - aa --232 @@ -3695,9 +3695,9 @@ public static void main(String[] args) { -##### 面试问题 +##### 搜索号码 -找出所有189和132开头的手机号 +找出所有 189 和 132 开头的手机号 ```java public class RegexDemo { @@ -5624,15 +5624,15 @@ class LRUCache extends LinkedHashMap { #### TreeMap -TreeMap 实现了 SotredMap 接口,是有序不可重复的键值对集合,基于红黑树(Red-Black tree)实现,每个 key-value 都作为一个红黑树的节点。如果构造 TreeMap 没有指定比较器,则根据 key 执行自然排序(默认升序),如果指定了比较器则按照比较器来进行排序 - -TreeSet 集合的底层是基于TreeMap,只是键的附属值为空对象而已 +TreeMap 实现了 SotredMap 接口,是有序不可重复的键值对集合,基于红黑树(Red-Black tree)实现,每个 key-value 都作为一个红黑树的节点,如果构造 TreeMap 没有指定比较器,则根据 key 执行自然排序(默认升序),如果指定了比较器则按照比较器来进行排序 TreeMap 集合指定大小规则有 2 种方式: -* 直接为对象的类实现比较器规则接口 Comparable,重写比较方法(拓展方式) +* 直接为对象的类实现比较器规则接口 Comparable,重写比较方法 * 直接为集合设置比较器 Comparator 对象,重写比较方法 +说明:TreeSet 集合的底层是基于 TreeMap,只是键的附属值为空对象而已 + 成员属性: * Entry 节点 @@ -5799,8 +5799,7 @@ public class MapDemo{ } ``` -优点:泛型在编译阶段约束了操作的数据类型,从而不会出现类型转换异常 - 体现的是 Java 的严谨性和规范性,数据类型,经常需要进行统一 +优点:泛型在编译阶段约束了操作的数据类型,从而不会出现类型转换异常,体现的是 Java 的严谨性和规范性 @@ -5812,7 +5811,7 @@ public class MapDemo{ ##### 泛型类 -泛型类:使用了泛型定义的类就是泛型类。 +泛型类:使用了泛型定义的类就是泛型类 泛型类格式: @@ -11642,6 +11641,10 @@ private int hash32; +参考文章:https://www.yuque.com/u21195183/jvm/nkq31c + + + *** From 916bc11bd67e092736ee58eb025258c52e45b556 Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 24 Jan 2022 00:23:57 +0800 Subject: [PATCH 060/122] Update Java Notes --- Frame.md | 1475 +++++++++++++++++++++++++++++++++++++++--------------- Java.md | 6 +- 2 files changed, 1086 insertions(+), 395 deletions(-) diff --git a/Frame.md b/Frame.md index c126bb8..bc07592 100644 --- a/Frame.md +++ b/Frame.md @@ -4594,10 +4594,10 @@ RocketMQ 消息的存储是由 ConsumeQueue 和 CommitLog 配合完成 的,Com ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消息存储结构.png) * CommitLog:消息主体以及元数据的存储主体,存储 Producer 端写入的消息内容,消息内容不是定长的。消息主要是顺序写入日志文件,单个文件大小默认 1G,偏移量代表下一次写入的位置,当文件写满了就继续写入下一个文件 -* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M +* ConsumerQueue:消息消费队列,存储消息在 CommitLog 的索引。RocketMQ 消息消费时要遍历 CommitLog 文件,并根据主题 Topic 检索消息,这是非常低效的。引入 ConsumeQueue 作为消费消息的索引,**保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset**,消息大小 size 和消息 Tag 的 HashCode 值,每个 ConsumeQueue 文件大小约 5.72M * IndexFile:为了消息查询提供了一种通过 Key 或时间区间来查询消息的方法,通过 IndexFile 来查找消息的方法不影响发送与消费消息的主流程。IndexFile 的底层存储为在文件系统中实现的 HashMap 结构,故 RocketMQ 的索引文件其底层实现为 **hash 索引** -RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)**针对 Producer 和 Consumer 分别采用了数据和索引部分相分离的存储结构**,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 +RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所有的队列共用一个日志数据文件(CommitLog)来存储。混合型存储结构(多个 Topic 的消息实体内容都存储于一个 CommitLog 中)针对 Producer 和 Consumer 分别采用了**数据和索引部分相分离**的存储结构,Producer 发送消息至 Broker 端,然后 Broker 端使用同步或者异步的方式对消息刷盘持久化,保存至 CommitLog 中。只要消息被持久化至磁盘文件 CommitLog 中,Producer 发送的消息就不会丢失,Consumer 也就肯定有机会去消费这条消息 服务端支持长轮询模式,当消费者无法拉取到消息后,可以等下一次消息拉取,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。RocketMQ 的具体做法是,使用 Broker 端的后台服务线程 ReputMessageService 不停地分发请求并异步构建 ConsumeQueue(逻辑消费队列)和 IndexFile(索引文件)数据 @@ -4607,9 +4607,7 @@ RocketMQ 采用的是混合型的存储结构,即为 Broker 单个实例下所 -#### 存储优化 - -##### 内存映射 +#### 内存映射 操作系统分为用户态和内核态,文件操作、网络操作需要涉及这两种形态的切换,需要进行数据复制。一台服务器把本机磁盘文件的内容发送到客户端,分为两个步骤: @@ -4633,7 +4631,7 @@ MappedByteBuffer 内存映射的方式**限制**一次只能映射 1.5~2G 的文 -##### 页缓存 +#### 页面缓存 页缓存(PageCache)是 OS 对文件的缓存,每一页的大小通常是 4K,用于加速对文件的读写。程序对文件进行顺序读写的速度几乎接近于内存的读写速度,就是因为 OS 将一部分的内存用作 PageCache,**对读写访问操作进行了性能优化** @@ -4812,23 +4810,23 @@ latencyFaultTolerance 机制是实现消息发送高可用的核心关键所在 在 RocketMQ 中,Consumer 端的两种消费模式(Push/Pull)都是基于拉模式来获取消息的,而在 Push 模式只是对 Pull 模式的一种封装,其本质实现为消息拉取线程在从服务器拉取到一批消息,提交到消息消费线程池后,又继续向服务器再次尝试拉取消息,如果未拉取到消息,则延迟一下又继续拉取 -在两种基于拉模式的消费方式(Push/Pull)中,均需要 Consumer 端在知道从 Broker 端的哪一个消息队列—队列中去获取消息,所以在 Consumer 端来做负载均衡,即 Broker 端中多个 MessageQueue 分配给同一个 ConsumerGroup 中的哪些 Consumer 消费 +在两种基于拉模式的消费方式(Push/Pull)中,均需要 Consumer 端在知道从 Broker 端的哪一个消息队列—队列中去获取消息,所以在 Consumer 端来做负载均衡,即 Broker 端中多个 MessageQueue 分配给同一个 Consumer Group 中的哪些 Consumer 消费 -* 广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以不存在负载均衡,在实现上,Consumer 分配 queue 时,所有 Consumer 都分到所有的queue。 +* 广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以不存在负载均衡,在实现上,Consumer 分配 queue 时,所有 Consumer 都分到所有的 queue。 * 在集群消费模式下,每条消息只需要投递到订阅这个 Topic 的 Consumer Group 下的一个实例即可,RocketMQ 采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条 Message Queue 集群模式下,每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照 queue 的数量和实例的数量平均分配 queue 给每个实例。默认的分配算法是 AllocateMessageQueueAveragely: -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-consumer负载均衡1.png) +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-平均队列分配.png) 还有一种平均的算法是 AllocateMessageQueueAveragelyByCircle,以环状轮流均分 queue 的形式: -![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-consumer负载均衡2.png) +![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-平均队列轮流分配.png) 集群模式下,queue 都是只允许分配只一个实例,如果多个实例同时消费一个 queue 的消息,由于拉取哪些消息是 Consumer 主动控制的,会导致同一个消息在不同的实例下被消费多次 -通过增加 Consumer 实例去分摊 queue 的消费,可以起到水平扩展的消费能力的作用。而当有实例下线时,会重新触发负载均衡,这时候原来分配到的 queue 将分配到其他实例上继续消费。但是如果 Consumer 实例的数量比 Message Queue 的总数量还多的话,多出来的 Consumer 实例将无法分到 queue,也就无法消费到消息,也就无法起到分摊负载的作用了,所以需要控制让 queue 的总数量大于等于 Consumer 的数量 +通过增加 Consumer 实例去分摊 queue 的消费,可以起到水平扩展的消费能力的作用。而当有实例下线时,会重新触发负载均衡,这时候原来分配到的 queue 将分配到其他实例上继续消费。但是如果 Consumer 实例的数量比 Message Queue 的总数量还多的话,多出来的 Consumer 实例将无法分到 queue,也就无法消费到消息,也就无法起到分摊负载的作用了,所以需要**控制让 queue 的总数量大于等于 Consumer 的数量** @@ -4850,8 +4848,6 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** * 先对 Topic 下的消息消费队列、消费者 ID 排序,然后用消息队列分配策略算法(默认是消息队列的平均分配算法),计算出待拉取的消息队列。平均分配算法类似于分页的算法,将所有 MessageQueue 排好序类似于记录,将所有消费端 Consumer 排好序类似页数,并求出每一页需要包含的平均 size 和每个页面记录的范围 range,最后遍历整个 range 而计算出当前 Consumer 端应该分配到的记录(这里即为 MessageQueue) - ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-负载均衡平均分配算法.png) - * 调用 updateProcessQueueTableInRebalance() 方法,先将分配到的消息队列集合 mqSet 与 processQueueTable 做一个过滤比对 ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-负载均衡重新平衡算法.png) @@ -4864,7 +4860,7 @@ Consumer 端实现负载均衡的核心类 **RebalanceImpl** 对比下 RebalancePushImpl 和 RebalancePullImpl 两个实现类的 dispatchPullRequest() 方法,RebalancePullImpl 类里面的该方法为空 -消息消费队列在同一消费组不同消费者之间的负载均衡,其核心设计理念是在一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列 +消息消费队列在同一消费组不同消费者之间的负载均衡,其核心设计理念是在**一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列** @@ -5231,6 +5227,10 @@ NamesrvStartup#start:启动 Namesrv 控制器 +源码解析参考视频:https://space.bilibili.com/457326371 + + + **** @@ -5471,7 +5471,7 @@ NettyRemotingServer 类成员变量: 核心方法的解析: -* start():启动方法 +* start():启动方法,**创建 BootStrap,并添加 NettyServerHandler 处理器** ```java public void start() { @@ -5492,7 +5492,7 @@ NettyRemotingServer 类成员变量: .childOption(ChannelOption.TCP_NODELAY, true) // 设置服务器端口 .localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort())) - // 向 channel pipeline 添加了很多 handler,包括 NettyServerHandler + // 向 channel pipeline 添加了很多 handler,【包括 NettyServerHandler】 .childHandler(new ChannelInitializer() {}); // 客户端开启 内存池,使用的内存池是 PooledByteBufAllocator.DEFAULT @@ -5639,7 +5639,7 @@ NettyRemotingServer 类成员变量: ##### 处理方法 -NettyServerHandler 类用来处理 RemotingCommand 相关的数据,针对某一种类型的**请求处理** +NettyServerHandler 类用来处理 Channel 上的事件,在 NettyRemotingServer 启动时注册到 Netty 中,可以处理 RemotingCommand 相关的数据,针对某一种类型的**请求处理** ```java class NettyServerHandler extends SimpleChannelInboundHandler { @@ -5667,7 +5667,7 @@ public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand ms } ``` -NettyRemotingAbstract#processRequestCommand:处理请求的数据 +NettyRemotingAbstract#processRequestCommand:**处理请求的数据** * `matched = this.processorTable.get(cmd.getCode())`:根据业务请求码获取 Pair 对象,包含**处理器和线程池资源** @@ -5701,7 +5701,7 @@ NettyRemotingAbstract#processRequestCommand:处理请求的数据 * `pair.getObject2().submit(requestTask)`:获取处理器对应的线程池,将 task 提交,**从 IO 线程切换到业务线程** -NettyRemotingAbstract#processResponseCommand:处理响应的数据 +NettyRemotingAbstract#processResponseCommand:**处理响应的数据** * `int opaque = cmd.getOpaque()`:获取请求 ID * `responseFuture = responseTable.get(opaque)`:**从响应映射表中获取对应的对象** @@ -6828,7 +6828,7 @@ FlushCommitLogService 刷盘 CL 数据,默认是异步刷盘 ##### 清理服务 -CleanCommitLogService 清理过期的 CL 数据,定时任务 10 秒调用一次,先清理 CL,再清理 CQ,因为 CQ 依赖于 CL 的数据 +CleanCommitLogService 清理过期的 CL 数据,定时任务 10 秒调用一次,**先清理 CL,再清理 CQ**,因为 CQ 依赖于 CL 的数据 * run():运行方法 @@ -6877,6 +6877,105 @@ CleanConsumeQueueService 清理过期的 CQ 数据 +##### 获取消息 + +PullMessageProcessor#processRequest 方法中调用 getMessage 用于获取消息(提示:建议学习消费者源码时再阅读) + +```java +// offset: 客户端拉消息使用位点; maxMsgNums: 32; messageFilter: 一般这里是 tagCode 过滤 +public GetMessageResult getMessage(final String group, final String topic, final int queueId, final long offset, final int maxMsgNums, final MessageFilter messageFilter) +``` + +* `if (this.shutdown)`:检查运行状态 + +* `GetMessageResult getResult`:创建查询结果对象 + +* `final long maxOffsetPy = this.commitLog.getMaxOffset()`:**获取 CommitLog 最大物理偏移量** + +* `ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId)`:根据主题和队列 ID 获取 ConsumeQueue对象 + +* `minOffset, maxOffset`:获取当前 ConsumeQueue 的最小 offset 和 最大 offset,**判断是否满足本次 Pull 的 offset** + + `if (maxOffset == 0)`:说明队列内无数据,设置状态为 NO_MESSAGE_IN_QUEUE,外层进行长轮询 + + `else if (offset < minOffset)`:说明 offset 太小了,设置状态为 OFFSET_TOO_SMALL + + `else if (offset == maxOffset)`:消费进度持平,设置状态为 OFFSET_OVERFLOW_ONE,外层进行长轮询 + + `else if (offset > maxOffset)`:说明 offset 越界了,设置状态为 OFFSET_OVERFLOW_BADLY + +* `SelectMappedBufferResult bufferConsumeQueue`:查询 CQData **获取包含该 offset 的 MappedFile 文件**,如果该文件不是顺序写的文件,就读取 `[offset%maxSize, 文件尾]` 范围的数据,反之读取 `[offset%maxSize, 文件名+wrotePosition尾]` + + 先查 CQ 的原因:因为 CQ 时 CL 的索引,通过 CQ 查询 CL 更加快捷 + +* `if (bufferConsumeQueue != null)`:只有再 CQ 删除过期数据的逻辑执行时,条件才不成立,一般都是成立的 + +* `long nextPhyFileStartOffset = Long.MIN_VALUE`:下一个 commitLog 物理文件名,初始值为最小值 + +* `long maxPhyOffsetPulling = 0`:本次拉消息最后一条消息的物理偏移量 + +* `for ()`:**处理数据**,每次处理 20 字节处理字节数大于 16000 时跳出循环 + +* `offsetPy, sizePy, tagsCode`:读取 20 个字节后,获取消息物理偏移量、消息大小、消息 tagCode + +* `boolean isInDisk = checkInDiskByCommitOffset(...)`:**检查消息是热数据还是冷数据**,false 为热数据 + + * `long memory`:Broker 系统 40% 内存的字节数,写数据时内存不够会使用 LRU 算法淘汰数据,将淘汰数据持久化到磁盘 + * `return (maxOffsetPy - offsetPy) > memory`:返回 true 说明数据已经持久化到磁盘,为冷数据 + +* `if (this.isTheBatchFull())`:**控制是否跳出循环** + + * `if (0 == bufferTotal || 0 == messageTotal)`:本次 pull 消息未拉取到任何东西,需要外层 for 循环继续,返回 false + + * `if (maxMsgNums <= messageTotal)`:结果对象内消息数已经超过了最大消息数量,可以结束循环了 + + * `if (isInDisk)`:冷数据 + + `if ((bufferTotal + sizePy) > ...)`:冷数据一次 pull 请求最大允许获取 64kb 的消息 + + `if (messageTotal > ...)`:冷数据一次 pull 请求最大允许获取8 条消息 + + * `else`:热数据 + + `if ((bufferTotal + sizePy) > ...)`:热数据一次 pull 请求最大允许获取 256kb 的消息 + + `if (messageTotal > ...)`:冷数据一次 pull 请求最大允许获取32 条消息 + +* `if (messageFilter != null)`:按照消息 tagCode 进行过滤 + +* `selectResult = this.commitLog.getMessage(offsetPy, sizePy)`:根据 CQ 消息物理偏移量和消息大小**到 commitLog 中查询这条 msg** + +* `if (null == selectResult)`:条件成立说明 commitLog 执行了删除过期文件的定时任务,因为是先清理的 CL,所以 CQ 还有该索引数据 + +* `nextPhyFileStartOffset = this.commitLog.rollNextFile(offsetPy)`:获取包含该 offsetPy 的下一个数据文件的文件名 + +* `getResult.addMessage(selectResult)`:**将本次循环查询出来的 msg 加入到 getResult 内** + +* `status = GetMessageStatus.FOUND`:查询状态设置为 FOUND + +* `nextPhyFileStartOffset = Long.MIN_VALUE`:设置为最小值,跳过期 CQData 数据的逻辑 + +* `nextBeginOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE)`:计算客户端下一次 pull 时使用的位点信息 + +* `getResult.setSuggestPullingFromSlave(diff > memory)`:**选择主从节点的建议** + + * `diff > memory => true`:表示本轮查询最后一条消息为冷数据,Broker 建议客户端下一次 pull 时到 slave 节点 + * `diff > memory => false`:表示本轮查询最后一条消息为热数据,Broker 建议客户端下一次 pull 时到 master 节点 + +* `getResult.setStatus(status)`:设置结果状态 + +* `getResult.setNextBeginOffset(nextBeginOffset)`:设置客户端下一次 pull 时的 offset + +* `getResult.setMaxOffset(maxOffset)`:设置 queue 的最大 offset 和最小 offset + +* `return getResult`:返回结果对象 + + + +*** + + + #### Broker BrokerStartup 启动方法 @@ -6914,6 +7013,8 @@ BrokerController#start:核心启动方法 #### 生产者类 +##### 生产者类 + DefaultMQProducer 是生产者的默认实现类 成员变量: @@ -7017,9 +7118,7 @@ DefaultMQProducer 是生产者的默认实现类 -#### 默认实现 - -##### 成员属性 +##### 实现者类 DefaultMQProducerImpl 类是默认的生产者实现类 @@ -7111,7 +7210,7 @@ DefaultMQProducerImpl 类是默认的生产者实现类 -##### 成员方法 +##### 实现方法 * start():启动方法,参数默认是 true,代表正常的启动路径 @@ -7337,7 +7436,7 @@ TopicPublishInfo 类用来存储路由信息 } else { // 遍历消息队列 for (int i = 0; i < this.messageQueueList.size(); i++) { - // 获取队列的索引 + // 【获取队列的索引,+1】 int index = this.sendWhichQueue.getAndIncrement(); // 获取队列的下标位置 int pos = Math.abs(index) % this.messageQueueList.size(); @@ -7355,115 +7454,219 @@ TopicPublishInfo 类用来存储路由信息 } ``` - - -**** +*** -### 消费者 -#### 消费者类 +#### 公共配置 -##### 默认消费 +公共的配置信息类 -DefaultMQPushConsumer 类是默认的消费者类 +* ClientConfig 类 -成员变量: + ```java + public class ClientConfig { + // Namesrv 地址配置 + private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); + // 客户端的 IP 地址 + private String clientIP = RemotingUtil.getLocalAddress(); + // 客户端实例名称 + private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); + // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 命名空间 + protected String namespace; + protected AccessChannel accessChannel = AccessChannel.LOCAL; + + // 获取路由信息的间隔时间 30s + private int pollNameServerInterval = 1000 * 30; + // 客户端与 broker 之间的心跳周期 30s + private int heartbeatBrokerInterval = 1000 * 30; + // 消费者持久化消费的周期 5s + private int persistConsumerOffsetInterval = 1000 * 5; + private long pullTimeDelayMillsWhenException = 1000; + private boolean unitMode = false; + private String unitName; + // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 + private boolean vipChannelEnabled = Boolean.parseBoolean(); + // 语言,默认是 Java + private LanguageCode language = LanguageCode.JAVA; + } + ``` -* 消费者实现类: +* NettyClientConfig ```java - protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; + public class NettyClientConfig { + // 客户端工作线程数 + private int clientWorkerThreads = 4; + // 回调处理线程池 线程数:平台核心数 + private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); + // 单向请求并发数,默认 65535 + private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; + // 异步请求并发数,默认 65535 + private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; + // 客户端连接服务器的超时时间限制 3秒 + private int connectTimeoutMillis = 3000; + // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) + private long channelNotActiveInterval = 1000 * 60; + // 客户端与服务器 ch 最大空闲时间 2分钟 + private int clientChannelMaxIdleTimeSeconds = 120; + + // 底层 Socket 写和收 缓冲区的大小 65535 64k + private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; + private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; + // 客户端 netty 是否启动内存池 + private boolean clientPooledByteBufAllocatorEnable = false; + // 客户端是否超时关闭 Socket 连接 + private boolean clientCloseSocketIfTimeout = false; + } ``` -* 消费属性: + + +*** + + + +#### 客户端类 + +##### 成员属性 + +MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一个客户端实例,**既服务于生产者,也服务于消费者** + +成员变量: + +* 配置信息: ```java - private String consumerGroup; // 消费者组 - private MessageModel messageModel = MessageModel.CLUSTERING; // 消费模式,默认集群模式 + private final int instanceIndex; // 索引一般是 0,因为客户端实例一般都是一个进程只有一个 + private final String clientId; // 客户端 ID ip@pid + private final long bootTimestamp; // 客户端的启动时间 + private ServiceState serviceState; // 客户端状态 ``` -* 订阅信息:key 是主题,value 是过滤表达式,一般是 tag +* 生产者消费者的映射表:key 是组名 ```java - private Map subscription = new HashMap() + private final ConcurrentMap producerTable + private final ConcurrentMap consumerTable + private final ConcurrentMap adminExtTable ``` -* 消息监听器:**消息处理逻辑**,并发消费 MessageListenerConcurrently,顺序(分区)消费 MessageListenerOrderly +* 网络层配置: ```java - private MessageListener messageListener; + private final NettyClientConfig nettyClientConfig; ``` -* 消费位点:当从 Broker 获取当前组内该 queue 的 offset 不存在时,consumeFromWhere 才有效,默认值代表从队列的最后 offset 开始消费,当队列内再有一条新的 msg 加入时,消费者才会去消费 +* 核心功能的实现:负责将 MQ 业务层的数据转换为网络层的 RemotingCommand 对象,使用内部持有的 NettyRemotingClient 对象的 invoke 系列方法,完成网络 IO(同步、异步、单向) ```java - private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET; + private final MQClientAPIImpl mQClientAPIImpl; ``` -* 消费时间戳:当消费位点配置的是 CONSUME_FROM_TIMESTAMP 时,并且服务器 Group 内不存在该 queue 的 offset 时,会使用该时间戳进行消费 +* 本地路由数据:key 是主题名称,value 路由信息 ```java - private String consumeTimestamp = UtilAll.timeMillisToHumanString3(System.currentTimeMillis() - (1000 * 60 * 30));// 消费者创建时间 - 30秒,转换成 格式: 年月日小时分钟秒,比如 20220203171201 + private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); ``` -* 队列分配策略:主题下的队列分配策略,RebalanceImpl 对象依赖该算法 +* 锁信息:两把锁,锁不同的数据 ```java - private AllocateMessageQueueStrategy allocateMessageQueueStrategy; + private final Lock lockNamesrv = new ReentrantLock(); + private final Lock lockHeartbeat = new ReentrantLock(); ``` -* 消费进度存储器: +* 调度线程池:单线程,执行定时任务 ```java - private OffsetStore offsetStore; + private final ScheduledExecutorService scheduledExecutorService; ``` -核心方法: - -* start():启动消费者 +* Broker 映射表:key 是 BrokerName ```java - public void start() + // 物理节点映射表,value:Long 是 brokerID,【ID=0 的是主节点,其他是从节点】,String 是地址 ip:port + private final ConcurrentMap> brokerAddrTable; + // 物理节点版本映射表,String 是地址 ip:port,Integer 是版本 + ConcurrentMap> brokerVersionTable; ``` -* shutdown():关闭消费者 +* **客户端的协议处理器**:用于处理 IO 事件 ```java - public void shutdown() + private final ClientRemotingProcessor clientRemotingProcessor; ``` -* registerMessageListener():注册消息监听器 +* 消息服务: ```java - public void registerMessageListener(MessageListener messageListener) + private final PullMessageService pullMessageService; // 拉消息服务 + private final RebalanceService rebalanceService; // 消费者负载均衡服务 + private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 ``` -* subscribe():添加订阅信息 +* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 ```java - public void subscribe(String topic, String subExpression) + private final DefaultMQProducer defaultMQProducer; ``` -* unsubscribe():删除订阅指定主题的信息 +* 心跳次数统计: ```java - public void unsubscribe(String topic) + private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) ``` -* suspend():停止消费 +构造方法: + +* MQClientInstance 有参构造: ```java - public void suspend() + public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) { + this.clientConfig = clientConfig; + this.instanceIndex = instanceIndex; + // Netty 相关的配置信息 + this.nettyClientConfig = new NettyClientConfig(); + // 平台核心数 + this.nettyClientConfig.setClientCallbackExecutorThreads(...); + this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS()); + // 【创建客户端协议处理器】 + this.clientRemotingProcessor = new ClientRemotingProcessor(this); + // 创建 API 实现对象 + // 参数一:客户端网络配置 + // 参数二:客户端协议处理器,注册到客户端网络层 + // 参数三:rpcHook,注册到客户端网络层 + // 参数四:客户端配置 + this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig); + + //... + // 内部生产者,指定内部生产者的组 + this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP); + } ``` -* resume():恢复消费 +* MQClientAPIImpl 有参构造: ```java - public void resume() + public MQClientAPIImpl(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig) { + this.clientConfig = clientConfig; + topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); + // 创建网络层对象,参数二为 null 说明客户端并不关心 channel event + this.remotingClient = new NettyRemotingClient(nettyClientConfig, null); + // 业务处理器 + this.clientRemotingProcessor = clientRemotingProcessor; + // 注册 RpcHook + this.remotingClient.registerRPCHook(rpcHook); + // ... + // 注册回退消息的请求码 + this.remotingClient.registerProcessor(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, this.clientRemotingProcessor, null); + } ``` @@ -7472,15 +7675,371 @@ DefaultMQPushConsumer 类是默认的消费者类 -##### 默认实现 - -DefaultMQPushConsumerImpl 是默认消费者的实现类 - -成员变量: +##### 成员方法 -* 客户端实例:整个进程内只有一个客户端实例对象 +* start():启动方法 - ```java + * `synchronized (this)`:加锁保证线程安全,保证只有一个实例对象启动 + * `this.mQClientAPIImpl.start()`:启动客户端网络层,底层调用 RemotingClient 类 + * `this.startScheduledTask()`:启动定时任务 + * `this.pullMessageService.start()`:启动拉取消息服务 + * `this.rebalanceService.start()`:启动负载均衡服务 + * `this.defaultMQProducer...start(false)`:启动内部生产者,参数为 false 代表不启动实例 + +* startScheduledTask():**启动定时任务**,调度线程池是单线程 + + * `if (null == this.clientConfig.getNamesrvAddr())`:Namesrv 地址是空,需要两分钟拉取一次 Namesrv 地址 + + * 定时任务 1:从 Namesrv 更新客户端本地的路由数据,周期 30 秒一次 + + ```java + // 获取生产者和消费者订阅的主题集合,遍历集合,对比从 namesrv 拉取最新的主题路由数据和本地数据,是否需要更新 + MQClientInstance.this.updateTopicRouteInfoFromNameServer(); + ``` + + * 定时任务 2:周期 30 秒一次,两个任务 + + * 清理下线的 Broker 节点,遍历客户端的 Broker 物理节点映射表,将所有主题数据都不包含的 Broker 物理节点清理掉,如果被清理的 Broker 下所有的物理节点都没有了,就将该 Broker 的映射数据删除掉 + * 向在线的所有的 Broker 发送心跳数据,**同步发送的方式**,返回值是 Broker 物理节点的版本号,更新版本映射表 + + ```java + MQClientInstance.this.cleanOfflineBroker(); + MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); + ``` + + ```java + // 心跳数据 + public class HeartbeatData extends RemotingSerializable { + // 客户端 ID ip@pid + private String clientID; + // 存储客户端所有生产者数据 + private Set producerDataSet = new HashSet(); + // 存储客户端所有消费者数据 + private Set consumerDataSet = new HashSet(); + } + ``` + + * 定时任务 3:消费者持久化消费数据,周期 5 秒一次 + + ```java + MQClientInstance.this.persistAllConsumerOffset(); + ``` + + * 定时任务 4:动态调整消费者线程池,周期 1 分钟一次 + + ```java + MQClientInstance.this.adjustThreadPool(); + ``` + +* updateTopicRouteInfoFromNameServer():**更新路由数据** + + * `if (isDefault && defaultMQProducer != null)`:需要默认数据 + + `topicRouteData = ...getDefaultTopicRouteInfoFromNameServer()`:从 Namesrv 获取默认的 TBW102 的路由数据 + + `int queueNums`:遍历所有队列,为每个读写队列设置较小的队列数 + + * `topicRouteData = ...getTopicRouteInfoFromNameServer(topic)`:需要**从 Namesrv 获取**路由数据(同步) + + * `old = this.topicRouteTable.get(topic)`:获取客户端实例本地的该主题的路由数据 + + * `boolean changed = topicRouteDataIsChange(old, topicRouteData)`:对比本地和最新下拉的数据是否一致 + + * `if (changed)`:不一致进入更新逻辑 + + `cloneTopicRouteData = topicRouteData.cloneTopicRouteData()`:克隆一份最新数据 + + `Update Pub info`:更新生产者信息 + + * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:**将主题路由数据转化为发布数据** + * `impl.updateTopicPublishInfo(topic, publishInfo)`:生产者将主题的发布数据保存到它本地,方便发送消息使用 + + `Update sub info`:更新消费者信息 + + `this.topicRouteTable.put(topic, cloneTopicRouteData)`:将数据放入本地路由表 + + + +**** + + + +#### 网络通信 + +##### 成员属性 + +NettyRemotingClient 类负责客户端的网络通信 + +成员变量: + +* Netty 服务相关属性: + + ```java + private final NettyClientConfig nettyClientConfig; // 客户端的网络层配置 + private final Bootstrap bootstrap = new Bootstrap(); // 客户端网络层启动对象 + private final EventLoopGroup eventLoopGroupWorker; // 客户端网络层 Netty IO 线程组 + ``` + +* Channel 映射表: + + ```java + private final ConcurrentMap channelTables;// key 是服务器的地址,value 是通道对象 + private final Lock lockChannelTables = new ReentrantLock(); // 锁,控制并发安全 + ``` + +* 定时器:启动定时任务 + + ```java + private final Timer timer = new Timer("ClientHouseKeepingService", true) + ``` + +* 线程池: + + ```java + private ExecutorService publicExecutor; // 公共线程池 + private ExecutorService callbackExecutor; // 回调线程池,客户端发起异步请求,服务器的响应数据由回调线程池处理 + ``` + +* 事件监听器:客户端这里是 null + + ```java + private final ChannelEventListener channelEventListener; + ``` + +构造方法 + +* 无参构造: + + ```java + public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { + this(nettyClientConfig, null); + } + ``` + +* 有参构造: + + ```java + public NettyRemotingClient(nettyClientConfig, channelEventListener) { + // 父类创建了2个信号量,1、控制单向请求的并发度,2、控制异步请求的并发度 + super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue()); + this.nettyClientConfig = nettyClientConfig; + this.channelEventListener = channelEventListener; + + // 创建公共线程池 + int publicThreadNums = nettyClientConfig.getClientCallbackExecutorThreads(); + if (publicThreadNums <= 0) { + publicThreadNums = 4; + } + this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums,); + + // 创建 Netty IO 线程,1个线程 + this.eventLoopGroupWorker = new NioEventLoopGroup(1, ); + + if (nettyClientConfig.isUseTLS()) { + sslContext = TlsHelper.buildSslContext(true); + } + } + ``` + + + +**** + + + +##### 成员方法 + +* start():启动方法 + + ```java + public void start() { + // channel pipeline 内的 handler 使用的线程资源,默认 4 个 + this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(); + // 配置 netty 客户端启动类对象 + Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class) + //... + .handler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + // 加几个handler + pipeline.addLast( + // 服务端的数据,都会来到这个 + new NettyClientHandler()); + } + }); + // 注意 Bootstrap 只是配置好客户端的元数据了,【在这里并没有创建任何 channel 对象】 + // 定时任务 扫描 responseTable 中超时的 ResponseFuture,避免客户端线程长时间阻塞 + this.timer.scheduleAtFixedRate(() -> { + NettyRemotingClient.this.scanResponseTable(); + }, 1000 * 3, 1000); + // 这里是 null,不启动 + if (this.channelEventListener != null) { + this.nettyEventExecutor.start(); + } + } + ``` + +* 单向通信: + + ```java + public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) { + // 开始时间 + long beginStartTime = System.currentTimeMillis(); + // 获取或者创建客户端与服务端(addr)的通道 channel + final Channel channel = this.getAndCreateChannel(addr); + // 条件成立说明客户端与服务端 channel 通道正常,可以通信 + if (channel != null && channel.isActive()) { + try { + // 执行 rpcHook 拓展点 + doBeforeRpcHooks(addr, request); + // 计算耗时,如果当前耗时已经超过 timeoutMillis 限制,则直接抛出异常,不再进行系统通信 + long costTime = System.currentTimeMillis() - beginStartTime; + if (timeoutMillis < costTime) { + throw new RemotingTimeoutException("invokeSync call timeout"); + } + // 参数1:客户端-服务端通道channel + // 参数二:网络层传输对象,封装着请求数据 + // 参数三:剩余的超时限制 + RemotingCommand response = this.invokeSyncImpl(channel, request, ...); + // 后置处理 + doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response); + // 返回响应数据 + return response; + } catch (RemotingSendRequestException e) {} + } else { + this.closeChannel(addr, channel); + throw new RemotingConnectException(addr); + } + } + ``` + + + + + +**** + + + +### 消费者 + +#### 消费者类 + +##### 默认消费 + +DefaultMQPushConsumer 类是默认的消费者类 + +成员变量: + +* 消费者实现类: + + ```java + protected final transient DefaultMQPushConsumerImpl defaultMQPushConsumerImpl; + ``` + +* 消费属性: + + ```java + private String consumerGroup; // 消费者组 + private MessageModel messageModel = MessageModel.CLUSTERING; // 消费模式,默认集群模式 + ``` + +* 订阅信息:key 是主题,value 是过滤表达式,一般是 tag + + ```java + private Map subscription = new HashMap() + ``` + +* 消息监听器:**消息处理逻辑**,并发消费 MessageListenerConcurrently,顺序(分区)消费 MessageListenerOrderly + + ```java + private MessageListener messageListener; + ``` + +* 消费位点:当从 Broker 获取当前组内该 queue 的 offset 不存在时,consumeFromWhere 才有效,默认值代表从队列的最后 offset 开始消费,当队列内再有一条新的 msg 加入时,消费者才会去消费 + + ```java + private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET; + ``` + +* 消费时间戳:当消费位点配置的是 CONSUME_FROM_TIMESTAMP 时,并且服务器 Group 内不存在该 queue 的 offset 时,会使用该时间戳进行消费 + + ```java + private String consumeTimestamp = UtilAll.timeMillisToHumanString3(System.currentTimeMillis() - (1000 * 60 * 30));// 消费者创建时间 - 30秒,转换成 格式: 年月日小时分钟秒,比如 20220203171201 + ``` + +* 队列分配策略:主题下的队列分配策略,RebalanceImpl 对象依赖该算法 + + ```java + private AllocateMessageQueueStrategy allocateMessageQueueStrategy; + ``` + +* 消费进度存储器: + + ```java + private OffsetStore offsetStore; + ``` + +核心方法: + +* start():启动消费者 + + ```java + public void start() + ``` + +* shutdown():关闭消费者 + + ```java + public void shutdown() + ``` + +* registerMessageListener():注册消息监听器 + + ```java + public void registerMessageListener(MessageListener messageListener) + ``` + +* subscribe():添加订阅信息 + + ```java + public void subscribe(String topic, String subExpression) + ``` + +* unsubscribe():删除订阅指定主题的信息 + + ```java + public void unsubscribe(String topic) + ``` + +* suspend():停止消费 + + ```java + public void suspend() + ``` + +* resume():恢复消费 + + ```java + public void resume() + ``` + + + +*** + + + +##### 默认实现 + +DefaultMQPushConsumerImpl 是默认消费者的实现类 + +成员变量: + +* 客户端实例:整个进程内只有一个客户端实例对象 + + ```java private MQClientInstance mQClientFactory; ``` @@ -7570,488 +8129,618 @@ DefaultMQPushConsumerImpl 是默认消费者的实现类 +*** + + + +#### 负载均衡 + +##### 实现方式 + +MQClientInstance#start 中会启动负载均衡服务: + +```java +public void run() { + // 检查停止标记 + while (!this.isStopped()) { + // 休眠 20 秒,防止其他线程饥饿,所以【每 20 秒负载均衡一次】 + this.waitForRunning(waitInterval); + // 调用客户端实例的负载均衡方法,底层【会遍历所有消费者,调用消费者的负载均衡】 + this.mqClientFactory.doRebalance(); + } +} +``` + +RebalanceImpl 类成员变量: + +* 分配给当前消费者的处理队列:处理消息队列集合,ProcessQueue 是 MQ 队列在消费者端的快照 + ```java + protected final ConcurrentMap processQueueTable; + ``` +* 消费者订阅主题的队列信息: + ```java + protected final ConcurrentMap> topicSubscribeInfoTable; + ``` +* 订阅数据: + ```java + protected final ConcurrentMap subscriptionInner; + ``` +* 队列分配策略: + ```java + protected AllocateMessageQueueStrategy allocateMessageQueueStrategy; + ``` +成员方法: +* doRebalance():负载均衡方法 + ```java + public void doRebalance(final boolean isOrder) { + // 获取当前消费者的订阅数据 + Map subTable = this.getSubscriptionInner(); + if (subTable != null) { + // 遍历所有的订阅主题 + for (final Entry entry : subTable.entrySet()) { + // 获取订阅的主题 + final String topic = entry.getKey(); + // 按照主题进行负载均衡 + this.rebalanceByTopic(topic, isOrder); + } + } + // 将分配到当前消费者的队列进行过滤,不属于当前消费者订阅主题的直接移除 + this.truncateMessageQueueNotMyTopic(); + } + ``` + * `Set mqSet = this.topicSubscribeInfoTable.get(topic)`:获取当前主题的全部队列信息 + * `cidAll = this...findConsumerIdList(topic, consumerGroup)`:从服务器获取消费者组下的全部消费者 ID + * `Collections.sort(mqAll)`:主题 MQ 队列和消费者 ID 都进行排序,保证每个消费者的视图一致性 + * `strategy = this.allocateMessageQueueStrategy`:获取队列分配策略对象 + * `allocateResult = strategy.allocate()`: **调用队列分配策略**,给当前消费者进行分配 MessageQueue + * `boolean changed = this.updateProcessQueueTableInRebalance(...)`:负载均衡,更新队列处理集合 + * `boolean changed = false`:当前消费者的消费队列是否有变化 -*** + * `while (it.hasNext())`:遍历当前消费者的所有处理队列 + * `if (!mqSet.contains(mq))`:该 MQ 经过 rbl 计算之后,**被分配到其它 consumer 节点** + `pq.setDropped(true)`:将删除状态设置为 true -### 客户端 + `if (this.removeUnnecessaryMessageQueue(mq, pq))`:在 MQ 归属的 broker 节点持久化消费进度,并删除该 MQ 在本地的消费进度 -#### 公共配置 + `it.remove()`:从 processQueueTable 移除该 MQ -公共的配置信息类 + * `else if (pq.isPullExpired())`:说明当前 MQ 还是被当前 consumer 消费,此时判断一下是否超过 2 分钟未到服务器 拉消息,如果条件成立进行上述相同的逻辑、 -* ClientConfig 类 + * `for (MessageQueue mq : mqSet)`:开始处理当前主题**新分配**到当前节点的队列 - ```java - public class ClientConfig { - // Namesrv 地址配置 - private String namesrvAddr = NameServerAddressUtils.getNameServerAddresses(); - // 客户端的 IP 地址 - private String clientIP = RemotingUtil.getLocalAddress(); - // 客户端实例名称 - private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT"); - // 客户端回调线程池的数量,平台核心数,8核16线程的电脑返回16 - private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); - // 命名空间 - protected String namespace; - protected AccessChannel accessChannel = AccessChannel.LOCAL; - - // 获取路由信息的间隔时间 30s - private int pollNameServerInterval = 1000 * 30; - // 客户端与 broker 之间的心跳周期 30s - private int heartbeatBrokerInterval = 1000 * 30; - // 消费者持久化消费的周期 5s - private int persistConsumerOffsetInterval = 1000 * 5; - private long pullTimeDelayMillsWhenException = 1000; - private boolean unitMode = false; - private String unitName; - // vip 通道,broker 启动时绑定两个端口,其中一个是 vip 通道 - private boolean vipChannelEnabled = Boolean.parseBoolean(); - // 语言,默认是 Java - private LanguageCode language = LanguageCode.JAVA; - } - ``` + * `if (isOrder && !this.lock(mq))`:**顺序消息为了保证有序性,需要获取分布式锁** -* NettyClientConfig + * `ProcessQueue pq = new ProcessQueue()`:为每个新分配的消息队列创建快照队列 + + * `long nextOffset = this.computePullFromWhere(mq)`:**从服务端获取新分配的 MQ 的消费进度** + + * `ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq)`:保存到处理队列集合 + + * `PullRequest pullRequest = new PullRequest()`:创建拉取请求对象 + + * `this.dispatchPullRequest(pullRequestList)`:放入拉消息服务的本地阻塞队列内,**用于拉取消息工作** + + + + +*** + + + +##### 队列分配 + +AllocateMessageQueueStrategy 类是队列的分配策略 + +* 平均分配:AllocateMessageQueueAveragely 类 ```java - public class NettyClientConfig { - // 客户端工作线程数 - private int clientWorkerThreads = 4; - // 回调处理线程池 线程数:平台核心数 - private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors(); - // 单向请求并发数,默认 65535 - private int clientOnewaySemaphoreValue = NettySystemConfig.CLIENT_ONEWAY_SEMAPHORE_VALUE; - // 异步请求并发数,默认 65535 - private int clientAsyncSemaphoreValue = NettySystemConfig.CLIENT_ASYNC_SEMAPHORE_VALUE; - // 客户端连接服务器的超时时间限制 3秒 - private int connectTimeoutMillis = 3000; - // 客户端未激活周期,60s(指定时间内 ch 未激活,需要关闭) - private long channelNotActiveInterval = 1000 * 60; - // 客户端与服务器 ch 最大空闲时间 2分钟 - private int clientChannelMaxIdleTimeSeconds = 120; - - // 底层 Socket 写和收 缓冲区的大小 65535 64k - private int clientSocketSndBufSize = NettySystemConfig.socketSndbufSize; - private int clientSocketRcvBufSize = NettySystemConfig.socketRcvbufSize; - // 客户端 netty 是否启动内存池 - private boolean clientPooledByteBufAllocatorEnable = false; - // 客户端是否超时关闭 Socket 连接 - private boolean clientCloseSocketIfTimeout = false; + // 参数一:消费者组 参数二:当前消费者id + // 参数三:主题的全部队列,包括所有 broker 上该主题的 mq 参数四:全部消费者id集合 + public List allocate(String consumerGroup, String currentCID, List mqAll, List cidAll) { + // 获取当前消费者在全部消费者中的位置,【全部消费者是已经排序好的,排在前面的优先分配更多的队列】 + int index = cidAll.indexOf(currentCID); + // 平均分配完以后,还剩余的待分配的 mq 的数量 + int mod = mqAll.size() % cidAll.size(); + // 首先判断整体的 mq 的数量是否小于消费者的数量,小于消费者的数量就说明不够分的,先分一个 + int averageSize = mqAll.size() <= cidAll.size() ? 1 : + // 成立需要多分配一个队列,因为更靠前 + (mod > 0 && index < mod ? mqAll.size() / cidAll.size() + 1 : mqAll.size() / cidAll.size()); + // 获取起始的分配位置 + int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod; + // 防止索引越界 + int range = Math.min(averageSize, mqAll.size() - startIndex); + // 开始分配,【挨着分配,是直接就把当前的 消费者分配完成】 + for (int i = 0; i < range; i++) { + result.add(mqAll.get((startIndex + i) % mqAll.size())); + } + return result; } ``` + 队列排序后:Q1 → Q2 → Q3,消费者排序后 C1 → C2 → C3 + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-平均队列分配.png) + +* 轮流分配:AllocateMessageQueueAveragelyByCircle + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-平均队列轮流分配.png) + +* 指定机房平均分配:AllocateMessageQueueByMachineRoom,前提是 Broker 的命名规则为 `机房名@BrokerName` + + + *** -#### 实例对象 +#### 消息拉取 -##### 成员属性 +##### 实现方式 -MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一个客户端实例,**既服务于生产者,也服务于消费者** +MQClientInstance#start 中会启动消息拉取服务:PullMessageService + +```java +public void run() { + // 检查停止标记 + while (!this.isStopped()) { + try { + // 从阻塞队列中获取拉消息请求 + PullRequest pullRequest = this.pullRequestQueue.take(); + // 拉取消息,获取请求对应的使用当前消费者组中的哪个消费者,调用消费者的 pullMessage 方法 + this.pullMessage(pullRequest); + } catch (Exception e) { + log.error("Pull Message Service Run Method exception", e); + } + } +} +``` + +DefaultMQPushConsumerImpl#pullMessage: + +* `ProcessQueue processQueue = pullRequest.getProcessQueue()`:获取请求对应的快照队列,并判断是否是删除状态 + +* `this.executePullRequestLater()`:如果当前消费者不是运行状态,则拉消息任务延迟 3 秒后执行,如果是暂停状态延迟 1 秒 + +* **流控的逻辑**: + + `long cachedMessageCount = processQueue.getMsgCount().get()`:获取消费者本地该 queue 快照内缓存的消息数量,如果大于 1000 条,进行流控,延迟 50 毫秒 + + `long cachedMessageSizeInMiB`: 消费者本地该 queue 快照内缓存的消息容量 size,超过 100m 消息未被消费进行流控 + + `if(processQueue.getMaxSpan() > 2000)`:消费者本地缓存消息第一条消息最后一条消息跨度超过 2000 进行流控 + +* `SubscriptionData subscriptionData`:本次拉消息请求订阅的主题数据,如果调用了 `unsubscribe(主题)` 将会获取为 null + +* `PullCallback pullCallback = new PullCallback()`:**拉消息处理回调对象**, + + * `pullResult = ...processPullResult()`:预处理 PullResult 结果 + + * `case FOUND`:正常拉取到消息 + + `pullRequest.setNextOffset(pullResult.getNextBeginOffset())`:更新 pullRequest 对象下一次拉取消息的位点 + + `if (pullResult.getMsgFoundList() == null...)`:消息过滤导致消息全部被过滤掉,需要立马发起下一次拉消息 + + `boolean .. = processQueue.putMessage()`:将服务器拉取的消息集合**加入到消费者本地**的 processQueue 内 + + `DefaultMQPushConsumerImpl...submitConsumeRequest()`:**提交消费任务**,分为顺序消费和并发消费 + + `Defaul..executePullRequestImmediately(pullRequest)`:将更新过 nextOffset 字段的 PullRequest 对象,再次放到 pullMessageService 的阻塞队列中,**形成闭环** + + * `case NO_NEW_MSG ||NO_MATCHED_MSG`:表示本次 pull 没有新的可消费的信息 + + `pullRequest.setNextOffset()`:更新更新 pullRequest 对象下一次拉取消息的位点 + + `Defaul..executePullRequestImmediately(pullRequest)`:再次拉取请求 + + * `case OFFSET_ILLEGAL`:本次 pull 时使用的 offset 是无效的,即 offset > maxOffset || offset < minOffset + + `pullRequest.setNextOffset()`:调整pullRequest nextOffset 为 正确的 offset + + `pullRequest.getProcessQueue().setDropped(true)`:设置该 processQueue 为删除状态,如果有该 queue 的消费任务,该消费任务会马上停止任务 + + `DefaultMQPushConsumerImpl.this.executeTaskLater()`:提交异步任务,10 秒后去执行 + + * `DefaultMQPushConsumerImpl...updateOffset()`:更新 offsetStore 该 MQ 的 offset 为正确值,内部直接替换 + + * `DefaultMQPushConsumerImpl...persist()`:持久化该 messageQueue 的 offset 到 Broker 端 + + * `DefaultMQPushConsumerImpl...removeProcessQueue()`: 删除该消费者该 messageQueue 对应的 processQueue + + * 这里没有再次提交 pullRequest 到 pullMessageService 的队列,那该队列不再拉消息了吗? + + 负载均衡 rbl 程序会重建该队列的 processQueue,重建完之后会为该队列创建新的 PullRequest 对象 + +* `int sysFlag = PullSysFlag.buildSysFlag()`:构建标志对象,sysFlag 高 4 位未使用,低 4 位使用,从左到右为 0000 0011 + + * 第一位:表示是否提交消费者本地该队列的 offset,一般是 1 + * 第二位:表示是否允许服务器端进行长轮询,一般是 1 + * 第三位:表示是否提交消费者本地该主题的订阅数据,一般是 0 + * 第四位:表示是否为类过滤,一般是 0 + +* `this.pullAPIWrapper.pullKernelImpl()`:拉取消息的核心方法 + + + +*** + + + +##### 封装对象 + +PullAPIWrapper 类封装了拉取消息的 API 成员变量: -* 配置信息: +* 推荐拉消息使用的主机 ID: ```java - private final int instanceIndex; // 索引一般是 0,因为客户端实例一般都是一个进程只有一个 - private final String clientId; // 客户端 ID ip@pid - private final long bootTimestamp; // 客户端的启动时间 - private ServiceState serviceState; // 客户端状态 + private ConcurrentMap pullFromWhichNodeTable ``` -* 生产者消费者的映射表:key 是组名 +成员方法: - ```java - private final ConcurrentMap producerTable - private final ConcurrentMap consumerTable - private final ConcurrentMap adminExtTable - ``` +* pullKernelImpl():拉消息 -* 网络层配置: + * `FindBrokerResult findBrokerResult`:查询指定 BrokerName 的地址信息,主节点或者推荐节点 - ```java - private final NettyClientConfig nettyClientConfig; - ``` + * `if (null == findBrokerResult)`:查询不到,就到 Namesrv 获取指定 topic 的路由数据 -* 核心功能的实现:负责将 MQ 业务层的数据转换为网络层的 RemotingCommand 对象,使用内部持有的 NettyRemotingClient 对象的 invoke 系列方法,完成网络 IO(同步、异步、单向) + * `if (findBrokerResult.isSlave())`:成立说明 findBrokerResult 表示的主机为 slave 节点,**slave 不存储 offset 信息** - ```java - private final MQClientAPIImpl mQClientAPIImpl; - ``` + `sysFlagInner = PullSysFlag.clearCommitOffsetFlag(sysFlagInner)`:将 sysFlag 标记位中 CommitOffset 的位置为 0 -* 本地路由数据:key 是主题名称,value 路由信息 + * `PullMessageRequestHeader requestHeader`:创建请求头对象,封装所有的参数 - ```java - private final ConcurrentMap topicRouteTable = new ConcurrentHashMap<>(); + * `PullResult pullResult = this.mQClientFactory.getMQClientAPIImpl().pullMessage()`:调用客户端实例的方法,核心逻辑就是**将业务数据转化为 RemotingCommand 通过 NettyRemotingClient 的 IO 进行通信** -* 锁信息:两把锁,锁不同的数据 + * `RemotingCommand request`:创建网络层传输对象 RemotingCommand 对象,**请求 ID 为 `PULL_MESSAGE = 11`** - ```java - private final Lock lockNamesrv = new ReentrantLock(); - private final Lock lockHeartbeat = new ReentrantLock(); - ``` + * `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 信息 - ```java - private final ScheduledExecutorService scheduledExecutorService; - ``` + * `pullCallback.onSuccess(pullResult)`:将 PullResult 交给拉消息结果处理回调对象,调用 onSuccess 方法 -* Broker 映射表:key 是 BrokerName +* processPullResult():预处理拉消息结果,**更新推荐 Broker 和过滤消息** - ```java - // 物理节点映射表,value:Long 是 brokerID,【ID=0 的是主节点,其他是从节点】,String 是地址 ip:port - private final ConcurrentMap> brokerAddrTable; - // 物理节点版本映射表,String 是地址 ip:port,Integer 是版本 - ConcurrentMap> brokerVersionTable; - ``` + * `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 + + + +*** + + + +#### 通信处理 + +##### 处理器 -* **客户端的协议处理器**:用于处理 IO 事件 +BrokerStartup#createBrokerController 方法中创建了 BrokerController 并进行初始化,调用 `registerProcessor()` 方法将处理器 PullMessageProcessor 注册到 NettyRemotingServer 中,对应的请求 ID 为 `PULL_MESSAGE = 11`,NettyServerHandler 在处理请求时通过请求 ID 会获取处理器执行 processRequest 方法 - ```java - private final ClientRemotingProcessor clientRemotingProcessor; - ``` +```java +// 参数一:服务器与客户端 netty 通道; 参数二:客户端请求; 参数三:是否允许服务器端长轮询,默认 true +private RemotingCommand processRequest(final Channel channel, RemotingCommand request, boolean brokerAllowSuspend) +``` -* 消息服务: +* `RemotingCommand response`:创建响应对象,设置为响应类型的请求,响应头是 PullMessageResponseHeader - ```java - private final PullMessageService pullMessageService; // 拉消息服务 - private final RebalanceService rebalanceService; // 消费者负载均衡服务 - private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 - ``` +* `final PullMessageResponseHeader responseHeader`:获取响应对象的 header -* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 +* `final PullMessageRequestHeader requestHeader`:解析出请求头 PullMessageRequestHeader - ```java - private final DefaultMQProducer defaultMQProducer; - ``` +* `response.setOpaque(request.getOpaque())`:设置 opaque 属性,客户端需要根据该字段**获取 ResponseFuture** -* 心跳次数统计: +* 进行一些鉴权的逻辑:是否允许长轮询、提交 offset、topicConfig 是否是空、队列 ID 是否合理 - ```java - private final AtomicLong sendHeartbeatTimesTotal = new AtomicLong(0) - ``` +* `ConsumerGroupInfo consumerGroupInfo`:获取消费者组信息,包好全部的消费者和订阅数据 -构造方法: +* `subscriptionData = consumerGroupInfo.findSubscriptionData()`:**获取指定主题的订阅数据** -* MQClientInstance 有参构造: +* `if (!ExpressionType.isTagType()`:表达式匹配 - ```java - public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) { - this.clientConfig = clientConfig; - this.instanceIndex = instanceIndex; - // Netty 相关的配置信息 - this.nettyClientConfig = new NettyClientConfig(); - // 平台核心数 - this.nettyClientConfig.setClientCallbackExecutorThreads(...); - this.nettyClientConfig.setUseTLS(clientConfig.isUseTLS()); - // 【创建客户端协议处理器】 - this.clientRemotingProcessor = new ClientRemotingProcessor(this); - // 创建 API 实现对象 - // 参数一:客户端网络配置 - // 参数二:客户端协议处理器,注册到客户端网络层 - // 参数三:rpcHook,注册到客户端网络层 - // 参数四:客户端配置 - this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig); - - //... - // 内部生产者,指定内部生产者的组 - this.defaultMQProducer = new DefaultMQProducer(MixAll.CLIENT_INNER_PRODUCER_GROUP); - } - ``` +* `MessageFilter messageFilter`:创建消息过滤器,一般是通过 tagCode 进行过滤 -* MQClientAPIImpl 有参构造: +* `DefaultMessageStore.getMessage()`:**查询消息的核心逻辑**,在 Broker 端查询消息(存储端笔记详解了该源码) - ```java - public MQClientAPIImpl(nettyClientConfig, clientRemotingProcessor, rpcHook, clientConfig) { - this.clientConfig = clientConfig; - topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName()); - // 创建网络层对象,参数二为 null 说明客户端并不关心 channel event - this.remotingClient = new NettyRemotingClient(nettyClientConfig, null); - // 业务处理器 - this.clientRemotingProcessor = clientRemotingProcessor; - // 注册 RpcHook - this.remotingClient.registerRPCHook(rpcHook); - // ... - // 注册回退消息的请求码 - this.remotingClient.registerProcessor(RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT, this.clientRemotingProcessor, null); - } - ``` +* `response.setRemark()`:设置此次响应的状态 +* `responseHeader.set..`:设置响应头对象的一些字段 +* `switch (this.brokerController.getMessageStoreConfig().getBrokerRole())`:如果当前主机节点角色为 slave 并且**从节点读**并未开启的话,直接给客户端 一个状态 `PULL_RETRY_IMMEDIATELY` -*** +* `if (this.brokerController.getBrokerConfig().isSlaveReadEnable())`:消费太慢,下次从另一台机器拉取 +* `switch (getMessageResult.getStatus())`:根据 getMessageResult 的状态设置 response 的 code + ```java + public enum GetMessageStatus { + FOUND, // 查询成功 + NO_MATCHED_MESSAGE, // 未查询到到消息,服务端过滤 tagCode + MESSAGE_WAS_REMOVING, // 查询时赶上 CommitLog 清理过期文件,导致查询失败,立刻尝试 + OFFSET_FOUND_NULL, // 查询时赶上 ConsumerQueue 清理过期文件,导致查询失败,【进行长轮询】 + OFFSET_OVERFLOW_BADLY, // pullRequest.offset 越界 maxOffset + OFFSET_OVERFLOW_ONE, // pullRequest.offset == CQ.maxOffset,【进行长轮询】 + OFFSET_TOO_SMALL, // pullRequest.offset 越界 minOffset + NO_MATCHED_LOGIC_QUEUE, // 没有匹配到逻辑队列 + NO_MESSAGE_IN_QUEUE, // 空队列,创建队列也是因为查询导致,【进行长轮询】 + } + ``` -##### 成员方法 +* `switch (response.getCode())`:根据 response 状态做对应的业务处理 -* start():启动方法 + `case ResponseCode.SUCCESS`:查询成功 - * `synchronized (this)`:加锁保证线程安全,保证只有一个实例对象启动 - * `this.mQClientAPIImpl.start()`:启动客户端网络层,底层调用 RemotingClient 类 - * `this.startScheduledTask()`:启动定时任务 - * `this.pullMessageService.start()`:启动拉取消息服务 - * `this.rebalanceService.start()`:启动负载均衡服务 - * `this.defaultMQProducer...start(false)`:启动内部生产者,参数为 false 代表不启动实例 + * `final byte[] r = this.readGetMessageResult()`:本次 pull 出来的全部消息导入 byte 数组 + * `response.setBody(r)`:将消息的 byte 数组保存到 response body 字段 -* startScheduledTask():**启动定时任务**,调度线程池是单线程 + `case ResponseCode.PULL_NOT_FOUND`:产生这种情况大部分原因是 `pullRequest.offset == queue.maxOffset`,说明已经没有需要获取的消息,此时如果直接返回给客户端,客户端会立刻重新请求,还是继续返回该状态,频繁拉取服务器导致服务器压力大,所以此处**需要长轮询** - * `if (null == this.clientConfig.getNamesrvAddr())`:Namesrv 地址是空,需要两分钟拉取一次 Namesrv 地址 + * `if (brokerAllowSuspend && hasSuspendFlag)`:brokerAllowSuspend = true,当长轮询结束再次执行 processRequest 时该参数为 false,所以**每次 Pull 请求至多在服务器端长轮询控制一次** + * `PullRequest pullRequest = new PullRequest()`:创建长轮询 PullRequest 对象 + * `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 的请求集合中 + * `response = null`:响应设置为 null 内部的 callBack 就不会给客户端发送任何数据,**不进行通信**,否则就又开始重新请求 - * 定时任务 1:从 Namesrv 更新客户端本地的路由数据,周期 30 秒一次 +* `boolean storeOffsetEnable`:允许长轮询、sysFlag 表示提交消费者本地该队列的offset、当前 broker 节点角色为 master 节点三个条件成立,才**在 Broker 端存储消费者组内该主题的指定 queue 的消费进度** - ```java - // 获取生产者和消费者订阅的主题集合,遍历集合,对比从 namesrv 拉取最新的主题路由数据和本地数据,是否需要更新 - MQClientInstance.this.updateTopicRouteInfoFromNameServer(); - ``` +* `return response`:返回 response,不为 null 时外层 requestTask 的 callback 会将数据写给客户端 - * 定时任务 2:周期 30 秒一次,两个任务 - * 清理下线的 Broker 节点,遍历客户端的 Broker 物理节点映射表,将所有主题数据都不包含的 Broker 物理节点清理掉,如果被清理的 Broker 下所有的物理节点都没有了,就将该 Broker 的映射数据删除掉 - * 向在线的所有的 Broker 发送心跳数据,**同步发送的方式**,返回值是 Broker 物理节点的版本号,更新版本映射表 - ```java - MQClientInstance.this.cleanOfflineBroker(); - MQClientInstance.this.sendHeartbeatToAllBrokerWithLock(); - ``` +*** - ```java - // 心跳数据 - public class HeartbeatData extends RemotingSerializable { - // 客户端 ID ip@pid - private String clientID; - // 存储客户端所有生产者数据 - private Set producerDataSet = new HashSet(); - // 存储客户端所有消费者数据 - private Set consumerDataSet = new HashSet(); - } - ``` - * 定时任务 3:消费者持久化消费数据,周期 5 秒一次 - ```java - MQClientInstance.this.persistAllConsumerOffset(); - ``` +##### 长轮询 - * 定时任务 4:动态调整消费者线程池,周期 1 分钟一次 +PullRequestHoldService 类负责长轮询,BrokerController#start 方法中调用了 `this.pullRequestHoldService.start()` 启动该服务 - ```java - MQClientInstance.this.adjustThreadPool(); - ``` +核心方法: -* updateTopicRouteInfoFromNameServer():**更新路由数据** +* run():核心运行方法 - * `if (isDefault && defaultMQProducer != null)`:需要默认数据 + ```java + public void run() { + // 循环运行 + while (!this.isStopped()) { + if (this.brokerController.getBrokerConfig().isLongPollingEnable()) { + // 服务器开启长轮询开关:每次循环休眠5秒 + this.waitForRunning(5 * 1000); + } else { + // 服务器关闭长轮询开关:每次循环休眠1秒 + this.waitForRunning(...); + } + // 检查持有的请求 + this.checkHoldRequest(); + // ..... + } + } + ``` - `topicRouteData = ...getDefaultTopicRouteInfoFromNameServer()`:从 Namesrv 获取默认的 TBW102 的路由数据 +* checkHoldRequest():检查所有的请求 - `int queueNums`:遍历所有队列,为每个读写队列设置较小的队列数 + * `for (String key : this.pullRequestTable.keySet())`:**处理所有的 topic@queueId 的逻辑** + * `String[] kArray = key.split(TOPIC_QUEUEID_SEPARATOR)`:key 按照 @ 拆分,得到 topic 和 queueId + * `long offset = this...getMaxOffsetInQueue(topic, queueId)`: 到存储模块查询该 ConsumeQueue 的**最大 offset** + * `this.notifyMessageArriving(topic, queueId, offset)`:通知消息到达 - * `topicRouteData = ...getTopicRouteInfoFromNameServer(topic)`:需要**从 Namesrv 获取**路由数据(同步) +* notifyMessageArriving():通知消息到达的逻辑,ReputMessageService 消息分发服务也会调用该方法 - * `old = this.topicRouteTable.get(topic)`:获取客户端实例本地的该主题的路由数据 + * `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 消息了,**结束长轮询** + * `boolean match`:进行过滤匹配 + * `this.brokerController...executeRequestWhenWakeup()`:将满足条件的 pullRequest 再次提交到线程池内执行 + * `final RemotingCommand response`:执行 processRequest 方法,并且**不会触发长轮询** + * `channel.writeAndFlush(response).addListene()`:**将结果数据发送给客户端** + * `if (System.currentTimeMillis() >= ...)`:判断该 pullRequest 是否超时,超时后的也是重新提交到线程池,并且不进行长轮询 + * `mpr.addPullRequest(replayList)`:将未满足条件的 PullRequest 对象再次添加到 ManyPullRequest 属性中 - * `boolean changed = topicRouteDataIsChange(old, topicRouteData)`:对比本地和最新下拉的数据是否一致 - * `if (changed)`:不一致进入更新逻辑 - `cloneTopicRouteData = topicRouteData.cloneTopicRouteData()`:克隆一份最新数据 +*** - `Update Pub info`:更新生产者信息 - * `publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData)`:**将主题路由数据转化为发布数据** - * `impl.updateTopicPublishInfo(topic, publishInfo)`:生产者将主题的发布数据保存到它本地,方便发送消息使用 - `Update sub info`:更新消费者信息 +##### 结果类 - `this.topicRouteTable.put(topic, cloneTopicRouteData)`:将数据放入本地路由表 +GetMessageResult 类成员信息: + +```java +public class GetMessageResult { + // 查询消息时,最底层都是 mappedFile 支持的查询,查询时返回给外层一个 SelectMappedBufferResult, + // mappedFile 每查询一次都会 refCount++ ,通过SelectMappedBufferResult持有mappedFile,完成资源释放的句柄 + private final List messageMapedList = + new ArrayList(100); + + // 该List内存储消息,每一条消息都被转成 ByteBuffer 表示了 + private final List messageBufferList = new ArrayList(100); + // 查询结果状态 + private GetMessageStatus status; + // 客户端下次再向当前Queue拉消息时,使用的 offset + private long nextBeginOffset; + // 当前queue最小offset + private long minOffset; + // 当前queue最大offset + private long maxOffset; + // 消息总byte大小 + private int bufferTotalSize = 0; + // 服务器建议客户端下次到该 queue 拉消息时是否使用 【从节点】 + private boolean suggestPullingFromSlave = false; +} +``` -**** +*** -#### 网络通信 +#### 队列快照 ##### 成员属性 -NettyRemotingClient 类负责客户端的网络通信 +ProcessQueue 类是消费队列的快照 成员变量: -* Netty 服务相关属性: +* 属性字段: ```java - private final NettyClientConfig nettyClientConfig; // 客户端的网络层配置 - private final Bootstrap bootstrap = new Bootstrap(); // 客户端网络层启动对象 - private final EventLoopGroup eventLoopGroupWorker; // 客户端网络层 Netty IO 线程组 + private final AtomicLong msgCount = new AtomicLong(); // 队列中消息数量 + private final AtomicLong msgSize = new AtomicLong(); // 消息总大小 + private volatile long queueOffsetMax = 0L; // 快照中最大 offset + private volatile boolean dropped = false; // 快照是否移除 + private volatile long lastPullTimestamp = current; // 上一次拉消息的时间 + private volatile long lastConsumeTimestamp = current; // 上一次消费消息的时间 + private volatile long lastLockTimestamp = current; // 上一次获取锁的时间 ``` -* Channel 映射表: +* **消息容器**:key 是消息偏移量,val 是消息 ```java - private final ConcurrentMap channelTables;// key 是服务器的地址,value 是通道对象 - private final Lock lockChannelTables = new ReentrantLock(); // 锁,控制并发安全 + private final TreeMap msgTreeMap = new TreeMap(); ``` -* 定时器:启动定时任务 +* **顺序消费临时容器**: ```java - private final Timer timer = new Timer("ClientHouseKeepingService", true) + private final TreeMap consumingMsgOrderlyTreeMap = new TreeMap(); ``` -* 线程池: +* 锁: ```java - private ExecutorService publicExecutor; // 公共线程池 - private ExecutorService callbackExecutor; // 回调线程池,客户端发起异步请求,服务器的响应数据由回调线程池处理 + private final ReadWriteLock lockTreeMap; // 读写锁 + private final Lock lockConsume; // 重入锁,【顺序消费使用】 ``` -* 事件监听器:客户端这里是 null +* 顺序消费状态: ```java - private final ChannelEventListener channelEventListener; + private volatile boolean locked = false; // 是否是锁定状态 + private volatile boolean consuming = false; // 是否是消费中 ``` -构造方法 + -* 无参构造: +**** - ```java - public NettyRemotingClient(final NettyClientConfig nettyClientConfig) { - this(nettyClientConfig, null); - } - ``` -* 有参构造: + +##### 成员方法 + +核心成员方法 + +* putMessage():将 Broker 拉取下来的 msgs 存储到快照队列内,返回为 true 表示提交顺序消费任务,false 表示不提交 ```java - public NettyRemotingClient(nettyClientConfig, channelEventListener) { - // 父类创建了2个信号量,1、控制单向请求的并发度,2、控制异步请求的并发度 - super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue()); - this.nettyClientConfig = nettyClientConfig; - this.channelEventListener = channelEventListener; - - // 创建公共线程池 - int publicThreadNums = nettyClientConfig.getClientCallbackExecutorThreads(); - if (publicThreadNums <= 0) { - publicThreadNums = 4; - } - this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums,); - - // 创建 Netty IO 线程,1个线程 - this.eventLoopGroupWorker = new NioEventLoopGroup(1, ); - - if (nettyClientConfig.isUseTLS()) { - sslContext = TlsHelper.buildSslContext(true); - } - } + public boolean putMessage(final List msgs) ``` + * `this.lockTreeMap.writeLock().lockInterruptibly()`:获取写锁 + * `for (MessageExt msg : msgs)`:遍历 msgs 全部加入 msgTreeMap,key 是消息的 queueOffset -**** + * `if (!msgTreeMap.isEmpty() && !this.consuming)`:**消息容器中存在未处理的消息,并且不是消费中的状态** + `dispatchToConsume = true`:代表需要提交顺序消费任务 + `this.consuming = true`:设置为顺序消费执行中的状态 -##### 成员方法 + * `this.lockTreeMap.writeLock().unlock()`:释放写锁 -* start():启动方法 +* removeMessage():移除已经消费的消息,参数是已经消费的消息集合 ```java - public void start() { - // channel pipeline 内的 handler 使用的线程资源,默认 4 个 - this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(); - // 配置 netty 客户端启动类对象 - Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class) - //... - .handler(new ChannelInitializer() { - @Override - public void initChannel(SocketChannel ch) throws Exception { - ChannelPipeline pipeline = ch.pipeline(); - // 加几个handler - pipeline.addLast( - // 服务端的数据,都会来到这个 - new NettyClientHandler()); - } - }); - // 注意 Bootstrap 只是配置好客户端的元数据了,【在这里并没有创建任何 channel 对象】 - // 定时任务 扫描 responseTable 中超时的 ResponseFuture,避免客户端线程长时间阻塞 - this.timer.scheduleAtFixedRate(() -> { - NettyRemotingClient.this.scanResponseTable(); - }, 1000 * 3, 1000); - // 这里是 null,不启动 - if (this.channelEventListener != null) { - this.nettyEventExecutor.start(); - } - } + public long removeMessage(final List msgs) ``` -* 单向通信: + * `long result = -1`:结果初始化为 -1 + * `this.lockTreeMap.writeLock().lockInterruptibly()`:获取写锁 + * `this.lastConsumeTimestamp = now`:更新上一次消费消息的时间为现在 + * `if (!msgTreeMap.isEmpty())`:判断消息容器是否是空,**是空直接返回 -1** + * `result = this.queueOffsetMax + 1`:设置结果,**删除完后消息容器为空时返回** + * `for (MessageExt msg : msgs)`:将已经消费的消息全部从 msgTreeMap 移除 + * `if (!msgTreeMap.isEmpty())`:移除后容器内还有待消费的消息,**获取第一条消息 offset 返回** + * `this.lockTreeMap.writeLock().unlock()`:释放写锁 + +* takeMessages():获取一批消息 ```java - public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) { - // 开始时间 - long beginStartTime = System.currentTimeMillis(); - // 获取或者创建客户端与服务端(addr)的通道 channel - final Channel channel = this.getAndCreateChannel(addr); - // 条件成立说明客户端与服务端 channel 通道正常,可以通信 - if (channel != null && channel.isActive()) { - try { - // 执行 rpcHook 拓展点 - doBeforeRpcHooks(addr, request); - // 计算耗时,如果当前耗时已经超过 timeoutMillis 限制,则直接抛出异常,不再进行系统通信 - long costTime = System.currentTimeMillis() - beginStartTime; - if (timeoutMillis < costTime) { - throw new RemotingTimeoutException("invokeSync call timeout"); - } - // 参数1:客户端-服务端通道channel - // 参数二:网络层传输对象,封装着请求数据 - // 参数三:剩余的超时限制 - RemotingCommand response = this.invokeSyncImpl(channel, request, ...); - // 后置处理 - doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response); - // 返回响应数据 - return response; - } catch (RemotingSendRequestException e) {} - } else { - this.closeChannel(addr, channel); - throw new RemotingConnectException(addr); - } - } + public List takeMessages(final int batchSize) ``` + * `this.lockTreeMap.writeLock().lockInterruptibly()`:获取写锁 + * `this.lastConsumeTimestamp = now`:更新上一次消费消息的时间为现在 + * `for (int i = 0; i < batchSize; i++)`:从头节点开始获取消息 + * `result.add(entry.getValue())`:将消息放入结果集合 + * `consumingMsgOrderlyTreeMap.put()`:将消息加入顺序消费容器中 + * `if (result.isEmpty())`:条件成立说明顺序消费容器本地快照内的消息全部处理完了,**当前顺序消费任务需要停止** + * `consuming = false`:消费状态置为 false + * `this.lockTreeMap.writeLock().unlock()`:释放写锁 + +* commit():处理完一批消息后调用 + ```java + public long commit() + ``` + * `this.lockTreeMap.writeLock().lockInterruptibly()`:获取写锁 + * `Long offset = this.consumingMsgOrderlyTreeMap.lastKey()`:获取顺序消费临时容器最后一条数据的 key + * `msgCount, msgSize`:更新顺序消费相关的字段 + * `this.consumingMsgOrderlyTreeMap.clear()`:清空顺序消费容器的数据 + * `return offset + 1`:**消费者下一条消费的位点** + * `this.lockTreeMap.writeLock().unlock()`:释放写锁 +* cleanExpiredMsg():清除过期消息 + + ```java + public void cleanExpiredMsg(DefaultMQPushConsumer pushConsumer) + ``` + * `if (pushConsumer.getDefaultMQPushConsumerImpl().isConsumeOrderly()) `:顺序消费不执行过期清理逻辑 + * `int loop = msgTreeMap.size() < 16 ? msgTreeMap.size() : 16`:最多循环 16 次 + * `if (!msgTreeMap.isEmpty() &&)`:如果容器中第一条消息的消费开始时间与当前系统时间差值 > 15min,则取出该消息 + * `else`:直接跳出循环,因为**快照队列内的消息是有顺序的**,第一条消息不过期,其他消息都不过期 + * `pushConsumer.sendMessageBack(msg, 3)`:**消息回退**到服务器,设置该消息的延迟级别为 3 + * `if (!msgTreeMap.isEmpty() && msg.getQueueOffset() == msgTreeMap.firstKey())`:条件成立说明消息回退期间,该目标消息并没有被消费任务成功消费 + * `removeMessage(Collections.singletonList(msg))`:从 treeMap 将该回退成功的 msg 删除 -*** @@ -8061,6 +8750,8 @@ NettyRemotingClient 类负责客户端的网络通信 + + ## TEST diff --git a/Java.md b/Java.md index 3004ef2..b932420 100644 --- a/Java.md +++ b/Java.md @@ -5013,7 +5013,7 @@ HashMap继承关系如下图所示: 计算 hash 的方法:将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,扰动运算 - 原因:当数组长度很小,假设是 16,那么 n-1即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 + 原因:当数组长度很小,假设是 16,那么 n-1 即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 哈希冲突的处理方式: @@ -5076,7 +5076,7 @@ HashMap继承关系如下图所示: * treeifyBin() - 节点添加完成之后判断此时节点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树,转换红黑树的方法 treeifyBin,整体代码如下: + 节点添加完成之后判断此时节点个数是否大于 TREEIFY_THRESHOLD 临界值 8,如果大于则将链表转换为红黑树,转换红黑树的方法 treeifyBin,整体代码如下: ```java if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st @@ -5084,7 +5084,7 @@ HashMap继承关系如下图所示: treeifyBin(tab, hash); ``` - 1. 如果当前数组为空或者数组的长度小于进行树形化的阈值(MIN_TREEIFY_CAPACITY = 64)就去扩容,而不是将节点变为红黑树 + 1. 如果当前数组为空或者数组的长度小于进行树形化的阈 MIN_TREEIFY_CAPACITY = 64 就去扩容,而不是将节点变为红黑树 2. 如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系,类似单向链表转换为双向链表 3. 让桶中的第一个元素即数组中的元素指向新建的红黑树的节点,以后这个桶里的元素就是红黑树而不是链表数据结构了 From 159ce03e152acfd33a068eebd59f64a3a9614b80 Mon Sep 17 00:00:00 2001 From: Seazean Date: Thu, 27 Jan 2022 22:35:25 +0800 Subject: [PATCH 061/122] Update Java Notes --- Frame.md | 713 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 691 insertions(+), 22 deletions(-) diff --git a/Frame.md b/Frame.md index bc07592..ee3ce2c 100644 --- a/Frame.md +++ b/Frame.md @@ -3816,7 +3816,7 @@ public class Consumer { 顺序消息分为全局顺序消息与分区顺序消息, - 全局顺序:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。 适用于性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景 -- 分区顺序:对于指定的一个 Topic,所有消息根据 sharding key 进行分区,同一个分组内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用于性能要求高,以 sharding key 作为分区字段,在同一个区中严格的按照 FIFO 原则进行消息发布和消费的场景 +- 分区顺序:对于指定的一个 Topic,所有消息根据 sharding key 进行分区,同一个分组内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用于性能要求高,以 Sharding key 作为分区字段,在同一个区中严格的按照 FIFO 原则进行消息发布和消费的场景 在默认的情况下消息发送会采取 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 都会变高。 @@ -6212,9 +6212,14 @@ CommitLog 类核心方法: ``` * `msg.setStoreTimestamp(System.currentTimeMillis())`:设置存储时间,后面获取到写锁后这个事件会重写 - * `String topic = msg.getTopic()`:获取主题和队列 ID + * `msg.setBodyCRC(UtilAll.crc32(msg.getBody()))`:获取消息的 CRC 值 + * `topic、queueId`:获取主题和队列 ID + * `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)`:修改主题 * `mappedFile = this.mappedFileQueue.getLastMappedFile()`:获取当前顺序写的 MappedFile 对象 - * `putMessageLock.lock()`:获取**写锁** * `msg.setStoreTimestamp(beginLockTimestamp)`:设置消息的存储时间为获取锁的时间 * `if (null == mappedFile || mappedFile.isFull())`:文件写满了创建新的 MF 对象 @@ -6813,12 +6818,22 @@ FlushCommitLogService 刷盘 CL 数据,默认是异步刷盘 ``` * `while (!this.isStopped())`:stopped为 true 才跳出循环 + * `boolean flushCommitLogTimed`:控制线程的休眠方式,默认是 false,使用 `CountDownLatch.await()` 休眠,设置为 true 时使用 `Thread.sleep()` 休眠 + * `int interval`:获取配置中的刷盘时间间隔 + * `int flushPhysicQueueLeastPages`:获取最小刷盘页数,默认是 4 页,脏页达到指定页数才刷盘 + * `int flushPhysicQueueThoroughInterval`:获取强制刷盘周期,默认是 10 秒,达到周期后强制刷盘,不考虑脏页 + * `if (flushCommitLogTimed)`:休眠逻辑,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 + * `CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)`:**刷盘** + + * `for (int i = 0; i < RETRY_TIMES_OVER && !result; i++)`:stopped 停止标记为 true 时,需要确保所有的数据都已经刷盘,所以此处尝试 10 次强制刷盘, + + `result = CommitLog.this.mappedFileQueue.flush(0)`:**强制刷盘** @@ -6989,6 +7004,12 @@ public static BrokerController start(BrokerController controller) { } ``` +BrokerStartup#createBrokerController:构造控制器,并初始化 + +* `final BrokerController controller()`:创建实例对象 +* `boolean initResult = controller.initialize()`:控制器初始化 + * `this.registerProcessor()`:**注册了处理器,包括发送消息、拉取消息、查询消息等核心处理器** + BrokerController#start:核心启动方法 * `this.messageStore.start()`:**启动存储服务** @@ -7102,7 +7123,7 @@ DefaultMQProducer 是生产者的默认实现类 } ``` -* request():请求方法,**需要消费者回执消息**,又叫回退消息 +* request():请求方法,**需要消费者回执消息** ```java public Message request(final Message msg, final MessageQueue mq, final long timeout) { @@ -7336,7 +7357,7 @@ DefaultMQProducerImpl 类是默认的生产者实现类 * `getRequestFutureTable().put(correlationId, requestResponseFuture)`:放入RequestFutureTable 映射表中 - * `this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback())`:**发送异步消息** + * `this.sendDefaultImpl(msg, CommunicationMode.ASYNC, new SendCallback())`:**发送异步消息,有回调函数** * `return waitResponse(msg, timeout, requestResponseFuture, cost)`:用来挂起请求的方法 @@ -7611,7 +7632,7 @@ MQClientInstance 是 RocketMQ 客户端实例,在一个 JVM 进程中只有一 private final ConsumerStatsManager consumerStatsManager; // 消费者状态管理 ``` -* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回执消息 +* 内部生产者实例:处理消费端**消息回退**,用该生产者发送回退消息 ```java private final DefaultMQProducer defaultMQProducer; @@ -7918,6 +7939,236 @@ NettyRemotingClient 类负责客户端的网络通信 +*** + + + +#### 延迟消息 + +##### 消息处理 + +BrokerStartup 初始化 BrokerController 调用 `registerProcessor()` 方法将 SendMessageProcessor 注册到 NettyRemotingServer 中,对应的请求 ID 为 `SEND_MESSAGE = 10`,NettyServerHandler 在处理请求时通过请求 ID 会获取处理器执行 processRequest + +```java +// 参数一:处理通道的事件; 参数二:客户端 +public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) { + RemotingCommand response = null; + response = asyncProcessRequest(ctx, request).get(); + return response; +} +``` + +SendMessageProcessor#asyncConsumerSendMsgBack:异步发送消费者的回调消息 + +* `final RemotingCommand response`:创建一个服务器响应对象 + +* `final ConsumerSendMsgBackRequestHeader requestHeader`:解析出客户端请求头信息,几个**核心字段**: + + * `private Long offset`:回退消息的 CommitLog offset + * `private Integer delayLevel`:延迟级别,一般是 0 + * `private String originMsgId, originTopic`:原始的消息 ID,主题 + * `private Integer maxReconsumeTimes`:最大重试次数,默认是 16 次 + +* `if ()`:鉴权,是否找到订阅组配置、Broker 是否支持写请求、订阅组是否支持消息重试 + +* `String newTopic = MixAll.getRetryTopic(...)`:获取**消费者组的重试主题**,规则是 `%RETRY%GroupName` + +* `int queueIdInt = Math.abs()`:充实主题下的队列 ID 是 0 + +* `TopicConfig topicConfig`:获取重试主题的配置信息 + +* `MessageExt msgExt`:根据消息的物理 offset 到存储模块查询,内部先查询出这条消息的 size,然后再根据 offset 和 size 查询出整条 msg + +* `final String retryTopic`:获取消息的原始主题 + +* `if (null == retryTopic)`:条件成立说明**当前消息是第一次被回退**, 添加 `RETRY_TOPIC` 属性 + +* `msgExt.setWaitStoreMsgOK(false)`:异步刷盘 + +* `if (msgExt...() >= maxReconsumeTimes || delayLevel < 0)`:消息重试次数超过最大次数,不支持重试 + + `newTopic = MixAll.getDLQTopic()`:获取消费者的死信队列,规则是 `%DLQ%GroupName` + + `queueIdInt, topicConfig`:死信队列 ID 为 0,创建死信队列的配置 + +* `if (0 == delayLevel)`:说明延迟级别由 Broker 控制 + + `delayLevel = 3 + msgExt.getReconsumeTimes()`:**延迟级别默认从 3 级开始**,每重试一次,延迟级别 +1 + +* `msgExt.setDelayTimeLevel(delayLevel)`:**将延迟级别设置进消息属性**,存储时会检查该属性,该属性值 > 0 会将消息的主题和队列再次修改,修改为调度主题和调度队列 ID + +* `MessageExtBrokerInner msgInner`:创建一条空消息,消息属性从 offset 查询出来的 msg 中拷贝 + +* `msgInner.setReconsumeTimes)`:重试次数设置为原 msg 的次数 +1 + +* `UtilAll.isBlank(originMsgId)`:判断消息是否是初次返回到服务器 + + * true:说明 msgExt 消息是第一次被返回到服务器,此时使用该 msg 的 id 作为 originMessageId + * false:说明原始消息已经被重试不止 1 次,此时使用 offset 查询出来的 msg 中的 originMessageId + +* `CompletableFuture putMessageResult = ..asyncPutMessage(msgInner)`:调用存储模块存储消息 + + `DefaultMessageStore#asyncPutMessage`: + + * `PutMessageResult result = this.commitLog.asyncPutMessage(msg)`:**将新消息存储到 CommitLog 中** + + + +*** + + + +##### 调度服务 + +DefaultMessageStore 中有成员属性 ScheduleMessageService,在 start 方法中会启动该调度服务 + +成员变量: + +* 延迟级别属性表: + + ```java + // 存储延迟级别对应的 延迟时间长度 (单位:毫秒) + private final ConcurrentMap delayLevelTable; + // 存储延迟级别 queue 的消费进度 offset,该 table 每 10 秒钟,会持久化一次,持久化到本地磁盘 + private final ConcurrentMap offsetTable; + ``` + +* 最大延迟级别: + + ```java + private int maxDelayLevel; + ``` + +* 模块启动状态: + + ```java + private final AtomicBoolean started = new AtomicBoolean(false); + ``` + +* 定时器:内部有线程资源,可执行调度任务 + + ```java + private Timer timer; + ``` + +成员方法: + +* load():加载调度消息,初始化 delayLevelTable 和 offsetTable + + ```java + public boolean load() + ``` + +* start():启动消息调度服务 + + ```java + public void start() + ``` + + * `if (started.compareAndSet(false, true))`:将启动状态设为 true + + * `this.timer`:创建定时器对象 + + * `for (... : this.delayLevelTable.entrySet())`:为**每个延迟级别创建一个延迟任务**提交到 timer ,延迟 1 秒后执行 + + * `this.timer.scheduleAtFixedRate()`:提交周期型任务,延迟 10 秒执行,周期为 10 秒,持久化延迟队列消费进度任务 + + `ScheduleMessageService.this.persist()`:持久化消费进度 + + + +*** + + + +##### 调度任务 + +DeliverDelayedMessageTimerTask 是一个任务类 + +成员变量: + +* 延迟级别:延迟队列任务处理的延迟级别 + + ```java + private final int delayLevel; + ``` + +* 消费进度:延迟队列任务处理的延迟队列的消费进度 + + ```java + private final long offset; + ``` + +成员方法: + +* run():执行任务 + + ```java + public void run() { + if (isStarted()) { + this.executeOnTimeup(); + } + ``` + +* executeOnTimeup():执行任务 + + ```java + public void executeOnTimeup() + ``` + + * `ConsumeQueue cq`:获取出该延迟队列任务处理的延迟队列 ConsumeQueue + + * `SelectMappedBufferResult bufferCQ`:根据消费进度查询出 SMBR 对象 + + * `for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE)`:每次读取 20 各字节的数据 + + * `offsetPy, sizePy`:延迟消息的物理偏移量和消息大小 + + * `long tagsCode`:延迟消息的交付时间,在 ReputMessageService 转发时根据消息的 DELAY 属性是否 >0 ,会在 tagsCode 字段存储交付时间 + + * `long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode)`:**延迟交付时间** + + * `long maxTimestamp`:当前时间 + 延迟级别对应的延迟毫秒值的时间戳 + * `if (deliverTimestamp > maxTimestamp)`:条件成立说明延迟时间过长,调整为当前时间立刻执行 + * `return result`:一般情况 result 就是 deliverTimestamp + + * `long countdown = deliverTimestamp - now`:计算差值 + + * `if (countdown <= 0)`:消息已经到达交付时间了 + + `MessageExt msgExt`:根据物理偏移量和消息大小获取这条消息 + + `MessageExtBrokerInner msgInner`:**构建一条新消息**,将原消息的属性拷贝过来 + + * `long tagsCodeValue`:不再是交付时间了 + * `MessageAccessor.clearProperty(msgInner, DELAY..)`:清理新消息的 DELAY 属性,避免存储时重定向到延迟队列 + * `msgInner.setTopic()`:修改主题为原始的主题 `%RETRY%GroupName` + * `String queueIdStr`:修改队列 ID 为原始的 ID + + `PutMessageResult putMessageResult`:**将新消息存储到 CommitLog**,消费者订阅的是目标主题,会再次消费该消息 + + * `else`:消息还未到达交付时间 + + `ScheduleMessageService.this.timer.schedule()`:创建该延迟级别的任务,延迟 countDown 毫秒之后再执行 + + `ScheduleMessageService.this.updateOffset()`:更新延迟级别队列的消费进度 + + * `PutMessageResult putMessageResult` + + * `bufferCQ == null`:说明通过消费进度没有获取到数据 + + `if (offset < cqMinOffset)`:如果消费进度比最小位点都小,说明是过期数据,重置为最小位点 + + * `ScheduleMessageService.this.timer.schedule()`:重新提交该延迟级别对应的延迟队列任务,延迟 100 毫秒后执行 + + + + + + + + + **** @@ -8209,7 +8460,7 @@ RebalanceImpl 类成员变量: * `allocateResult = strategy.allocate()`: **调用队列分配策略**,给当前消费者进行分配 MessageQueue - * `boolean changed = this.updateProcessQueueTableInRebalance(...)`:负载均衡,更新队列处理集合 + * `boolean changed = this.updateProcessQueueTableInRebalance(...)`:**更新队列处理集合** * `boolean changed = false`:当前消费者的消费队列是否有变化 @@ -8219,25 +8470,70 @@ RebalanceImpl 类成员变量: `pq.setDropped(true)`:将删除状态设置为 true - `if (this.removeUnnecessaryMessageQueue(mq, pq))`:在 MQ 归属的 broker 节点持久化消费进度,并删除该 MQ 在本地的消费进度 + `if (this.removeUnnecessaryMessageQueue(mq, pq))`:删除不需要的 MQ 队列 - `it.remove()`:从 processQueueTable 移除该 MQ + * `this...getOffsetStore().persist(mq)`:在 MQ 归属的 Broker 节点持久化消费进度 - * `else if (pq.isPullExpired())`:说明当前 MQ 还是被当前 consumer 消费,此时判断一下是否超过 2 分钟未到服务器 拉消息,如果条件成立进行上述相同的逻辑、 + * `this...getOffsetStore().removeOffset(mq)`:删除该 MQ 在本地的消费进度 + * `if (this.defaultMQPushConsumerImpl.isConsumeOrderly() &&)`:是否是**顺序消费**和集群模式 + + `if (pq.getLockConsume().tryLock(1000, ..))`: 获取锁成功,说明顺序消费任务已经停止消费工作 + + `return this.unlockDelay(mq, pq)`:**释放锁 Broker 端的队列锁** + + * `if (pq.hasTempMessage())`:队列中有消息,延迟 20 秒释放队列分布式锁,确保全局范围内只有一个消费任务 运行中 + * `else`:当前消费者本地该消费任务已经退出,直接释放锁 + + `else`:顺序消费任务正在消费一批消息,不可打断,增加尝试获取锁的次数 + + `it.remove()`:从 processQueueTable 移除该 MQ + + * `else if (pq.isPullExpired())`:说明当前 MQ 还是被当前 consumer 消费,此时判断一下是否超过 2 分钟未到服务器 拉消息,如果条件成立进行上述相同的逻辑 + * `for (MessageQueue mq : mqSet)`:开始处理当前主题**新分配**到当前节点的队列 + + `if (isOrder && !this.lock(mq))`:**顺序消息为了保证有序性,需要获取分布式锁** + + `ProcessQueue pq = new ProcessQueue()`:为每个新分配的消息队列创建快照队列 + + `long nextOffset = this.computePullFromWhere(mq)`:**从服务端获取新分配的 MQ 的消费进度** + + `ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq)`:保存到处理队列集合 + + `PullRequest pullRequest = new PullRequest()`:创建拉取请求对象 + + * `this.dispatchPullRequest(pullRequestList)`:放入拉消息服务的本地阻塞队列内,**用于拉取消息工作** + +* lockAll():续约锁,对消费者的所有队列进行续约 - * `if (isOrder && !this.lock(mq))`:**顺序消息为了保证有序性,需要获取分布式锁** + ```java + public void lockAll() + ``` - * `ProcessQueue pq = new ProcessQueue()`:为每个新分配的消息队列创建快照队列 + * `HashMap> brokerMqs`:将分配给当前消费者的全部 MQ,按照 BrokerName 分组 - * `long nextOffset = this.computePullFromWhere(mq)`:**从服务端获取新分配的 MQ 的消费进度** + * `while (it.hasNext())`:遍历所有的分组 - * `ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq)`:保存到处理队列集合 + * `final Set mqs`:获取该 Broker 上分配给当前消费者的 queue 集合 - * `PullRequest pullRequest = new PullRequest()`:创建拉取请求对象 + * `FindBrokerResult findBrokerResult`:查询 Broker 主节点信息 - * `this.dispatchPullRequest(pullRequestList)`:放入拉消息服务的本地阻塞队列内,**用于拉取消息工作** + * `LockBatchRequestBody requestBody`:创建请求对象,填充属性 + + * `Set lockOKMQSet`:**向 Broker 发起批量续约锁的同步请求**,返回成功的队列集合 + + * `for (MessageQueue mq : lockOKMQSet)`:遍历续约锁成功的 MQ + + `processQueue.setLocked(true)`:分布式锁状态设置为 true,**表示允许顺序消费** + + `processQueue.setLastLockTimestamp(System.currentTimeMillis())`:设置上次获取锁的时间为当前时间 + + * `for (MessageQueue mq : mqs)`:遍历当前 Broker 上的所有队列集合 + + `if (!lockOKMQSet.contains(mq))`:条件成立说明续约锁失败 + + `processQueue.setLocked(false)`:分布式锁状态设置为 false,表示不允许顺序消费 @@ -8294,7 +8590,7 @@ AllocateMessageQueueStrategy 类是队列的分配策略 -#### 消息拉取 +#### 拉取服务 ##### 实现方式 @@ -8441,7 +8737,7 @@ PullAPIWrapper 类封装了拉取消息的 API -#### 通信处理 +#### 拉取处理 ##### 处理器 @@ -8684,7 +8980,7 @@ ProcessQueue 类是消费队列的快照 * `this.lockTreeMap.writeLock().unlock()`:释放写锁 -* removeMessage():移除已经消费的消息,参数是已经消费的消息集合 +* removeMessage():移除已经消费的消息,参数是已经消费的消息集合,并发消费使用 ```java public long removeMessage(final List msgs) @@ -8699,7 +8995,7 @@ ProcessQueue 类是消费队列的快照 * `if (!msgTreeMap.isEmpty())`:移除后容器内还有待消费的消息,**获取第一条消息 offset 返回** * `this.lockTreeMap.writeLock().unlock()`:释放写锁 -* takeMessages():获取一批消息 +* takeMessages():获取一批消息,顺序消费使用 ```java public List takeMessages(final int batchSize) @@ -8714,7 +9010,7 @@ ProcessQueue 类是消费队列的快照 * `consuming = false`:消费状态置为 false * `this.lockTreeMap.writeLock().unlock()`:释放写锁 -* commit():处理完一批消息后调用 +* commit():处理完一批消息后调用,顺序消费使用 ```java public long commit() @@ -8743,6 +9039,379 @@ ProcessQueue 类是消费队列的快照 +**** + + + +#### 并发消费 + +##### 成员属性 + +ConsumeMessageConcurrentlyService 负责并发消费服务 + +成员变量: + +* 消息监听器:封装处理消息的逻辑,该监听器由开发者实现,并注册到 defaultMQPushConsumer + + ```java + private final MessageListenerConcurrently messageListener; + ``` + +* 消费属性: + + ```java + private final BlockingQueue consumeRequestQueue; // 消费任务队列 + private final String consumerGroup; // 消费者组 + ``` + +* 线程池: + + ```java + private final ThreadPoolExecutor consumeExecutor; // 消费任务线程池 + private final ScheduledExecutorService scheduledExecutorService;// 调度线程池,延迟提交消费任务 + private final ScheduledExecutorService cleanExpireMsgExecutors; // 清理过期消息任务线程池,15min 一次 + ``` + + + +*** + + + +##### 成员方法 + +ConsumeMessageConcurrentlyService 并发消费核心方法 + +* start():启动消费服务,DefaultMQPushConsumerImpl 启动时会调用该方法 + + ```java + public void start() { + // 提交“清理过期消息任务”任务,延迟15min之后执行,之后每15min执行一次 + this.cleanExpireMsgExecutors.scheduleAtFixedRate(() -> cleanExpireMsg()}, + 15, 15, TimeUnit.MINUTES); + } + ``` + +* cleanExpireMsg():清理过期消息任务 + + ```java + private void cleanExpireMsg() + ``` + + * `Iterator> it `:获取分配给当前消费者的队列 + * `while (it.hasNext())`:遍历所有的队列 + * `pq.cleanExpiredMsg(this.defaultMQPushConsumer)`:调用队列快照 ProcessQueue 清理过期消息的方法 + +* submitConsumeRequest():提交消费请求 + + ```java + // 参数一:从服务器 pull 下来的这批消息 + // 参数二:消息归属 mq 在消费者端的 processQueue,提交消费任务之前,msgs已经加入到该pq内了 + // 参数三:消息归属队列 + // 参数四:并发消息此参数无效 + public void submitConsumeRequest(List msgs, ProcessQueue processQueue, MessageQueue messageQueue, boolean dispatchToConsume) + ``` + + * `final int consumeBatchSize`:**一个消费任务**可消费的消息数量,默认为 1 + + * `if (msgs.size() <= consumeBatchSize)`:判断一个消费任务是否可以提交 + + `ConsumeRequest consumeRequest`:封装为消费请求 + + `this.consumeExecutor.submit(consumeRequest)`:提交消费任务,异步执行消息的处理 + + * `else`:说明消息较多,需要多个消费任务 + + `for (int total = 0; total < msgs.size(); )`:将消息拆分成多个消费任务 + +* processConsumeResult():处理消费结果 + + ```java + // 参数一:消费结果状态; 参数二:消费上下文; 参数三:当前消费任务 + public void processConsumeResult(status, context, consumeRequest) + ``` + + * `switch (status)`:根据消费结果状态进行处理 + + * `case CONSUME_SUCCESS`:消费成功 + + `if (ackIndex >= consumeRequest.getMsgs().size())`:消费成功的话,ackIndex 设置成 `消费消息数 - 1` 的值,比如有 5 条消息,这里就设置为 4 + + `ok, failed`:ok 设置为消息数量,failed 设置为 0 + + * `case RECONSUME_LATER`:消费失败 + + `ackIndex = -1`:设置为 -1 + + * `switch (this.defaultMQPushConsumer.getMessageModel())`:判断消费模式,默认是**集群模式** + + * `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()`:**同步请求** + + * `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 是 ConsumeMessageConcurrentlyService 的内部类,是一个 Runnable 任务对象 + +成员变量: + +* 分配到该消费任务的消息: + + ```java + private final List msgs; + ``` + +* 消息队列: + + ```java + private final ProcessQueue processQueue; // 消息处理队列 + private final MessageQueue messageQueue; // 消息队列 + ``` + +核心方法: + +* run():执行任务 + + ```java + public void run() + ``` + + * `if (this.processQueue.isDropped())`:条件成立说明该 queue 经过 rbl 算法分配到其他的 consumer + * `MessageListenerConcurrently listener`:获取消息监听器 + * `ConsumeConcurrentlyContext context`:创建消费上下文对象 + * `defaultMQPushConsumerImpl.resetRetryAndNamespace()`:重置重试标记 + * `final String groupTopic`:获取当前消费者组的重试主题 `%RETRY%GroupName` + * `for (MessageExt msg : msgs)`:遍历所有的消息 + * `String retryTopic = msg.getProperty(...)`:原主题,一般消息没有该属性,只有被重复消费的消息才有 + * `if (retryTopic != null && groupTopic.equals(...))`:条件成立说明该消息是被重复消费的消息 + * `msg.setTopic(retryTopic)`:将被**重复消费的消息主题修改回原主题** + * `if (ConsumeMessageConcurrentlyService...hasHook())`:前置处理 + * `boolean hasException = false`:消费过程中,是否向外抛出异常 + * `MessageAccessor.setConsumeStartTimeStamp()`:给每条消息设置消费开始时间 + * `status = listener.consumeMessage(Collections.unmodifiableList(msgs), context)`:**消费消息** + * `if (ConsumeMessageConcurrentlyService...hasHook())`:后置处理 + * `...processConsumeResult(status, context, this)`:**处理消费结果** + + + +**** + + + +#### 顺序消费 + +##### 成员属性 + +ConsumeMessageOrderlyService 负责顺序消费服务 + +成员变量: + +* 消息监听器:封装处理消息的逻辑,该监听器由开发者实现,并注册到 defaultMQPushConsumer + + ```java + private final MessageListenerOrderly messageListener; + ``` + +* 消费属性: + + ```java + private final BlockingQueue consumeRequestQueue; // 消费任务队列 + private final String consumerGroup; // 消费者组 + private volatile boolean stopped = false; // 消费停止状态 + ``` + +* 线程池: + + ```java + private final ThreadPoolExecutor consumeExecutor; // 消费任务线程池 + private final ScheduledExecutorService scheduledExecutorService;// 调度线程池,延迟提交消费任务 + ``` + +* 队列锁:消费者本地 MQ 锁,确保本地对于需要顺序消费的 MQ 同一时间只有一个任务在执行 + + ```java + private final MessageQueueLock messageQueueLock = new MessageQueueLock(); + ``` + + ```java + public class MessageQueueLock { + private ConcurrentMap mqLockTable = new ConcurrentHashMap(); + // 获取本地队列锁对象 + public Object fetchLockObject(final MessageQueue mq) { + Object objLock = this.mqLockTable.get(mq); + if (null == objLock) { + objLock = new Object(); + Object prevLock = this.mqLockTable.putIfAbsent(mq, objLock); + if (prevLock != null) { + objLock = prevLock; + } + } + return objLock; + } + } + ``` + + 已经获取了 Broker 端该 Queue 的独占锁,为什么还要获取本地队列锁对象?(这里我也没太懂,先记录下来) + + * Broker queue 占用锁的角度是 Client 占用,Client 从 Broker 的某个占用了锁的 queue 拉取下来消息以后,将消息存储到消费者本地的 ProcessQueue 中,快照对象的 consuming 属性置为 true,表示本地的队列正在消费处理中。 + * ProcessQueue 调用 takeMessages 方法时会获取下一批待处理的消息,获取不到会修改 `consuming = false`,本消费任务马上停止。 + * 如果此时 Pull 再次拉取一批当前 ProcessQueue 的 msg,会再次向顺序消费服务提交消费任务,此时需要本地队列锁对象同步本地线程 + + + +*** + + + +##### 成员方法 + +* start():启动消费服务,DefaultMQPushConsumerImpl 启动时会调用该方法 + + ```java + public void start() + ``` + + * `this.scheduledExecutorService.scheduleAtFixedRate()`:提交锁续约任务,延迟 1 秒执行,周期为 20 秒钟 + * `ConsumeMessageOrderlyService.this.lockMQPeriodically()`:**锁续约任务** + * `this.defaultMQPushConsumerImpl.getRebalanceImpl().lockAll()`:对消费者的所有队列进行续约 + +* submitConsumeRequest():**提交消费任务请求** + + ```java + // 参数:true 表示创建消费任务并提交,false不创建消费任务,说明消费者本地已经有消费任务在执行了 + public void submitConsumeRequest(...., final boolean dispathToConsume) { + if (dispathToConsume) { + // 当前进程内不存在 顺序消费任务,创建新的消费任务,【提交到消费任务线程池】 + ConsumeRequest consumeRequest = new ConsumeRequest(processQueue, messageQueue); + this.consumeExecutor.submit(consumeRequest); + } + } + ``` + +* processConsumeResult():消费结果处理 + + ```java + // 参数1:msgs 本轮循环消费的消息集合 参数2:status 消费状态 + // 参数3:context 消费上下文 参数4:消费任务 + // 返回值:boolean 决定是否继续循环处理pq内的消息 + public boolean processConsumeResult(final List msgs, final ConsumeOrderlyStatus status, final ConsumeOrderlyContext context, final ConsumeRequest consumeRequest) + ``` + + * `if (context.isAutoCommit()) `:默认自动提交 + + * `switch (status)`:根据消费状态进行不同的处理 + + * `case SUCCESS`:消费成功 + + `commitOffset = ...commit()`:调用 pq 提交方法,会将本次循环处理的消息从顺序消费 map 删除,并且返回消息进度 + + * `case SUSPEND_CURRENT_QUEUE_A_MOMENT`:挂起当前队列 + + `consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs)`:回滚消息 + + * `for (MessageExt msg : msgs)`:遍历所有的消息 + * `this.consumingMsgOrderlyTreeMap.remove(msg.getQueueOffset())`:从顺序消费临时容器中移除 + * `this.msgTreeMap.put(msg.getQueueOffset(), msg)`:添加到消息容器 + + * `this.submitConsumeRequestLater()`:再次提交消费任务,1 秒后执行 + + * `continueConsume = false`:设置为 false,**外层会退出本次的消费任务** + + * `this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(...)`:更新本地消费进度 + + + +**** + + + +##### 消费请求 + +ConsumeRequest 是 ConsumeMessageOrderlyService 的内部类,是一个 Runnable 任务对象 + +核心方法: + +* run():执行任务 + + ```java + public void run() + ``` + + * `final Object objLock`:获取本地锁对象 + + * `synchronized (objLock)`:本地队列锁,确保每个 MQ 的消费任务只有一个在执行,**确保顺序消费** + + * `if(.. || (this.processQueue.isLocked() && !this.processQueue.isLockExpired())))`:当前队列持有分布式锁,并且锁未过期,持锁时间超过 30 秒算过期 + + * `final long beginTime`:消费开始时间 + + * `for (boolean continueConsume = true; continueConsume; )`:根据是否继续消费的标记判断是否继续 + + * `final int consumeBatchSize`:获取每次循环处理的消息数量,一般是 1 + + * `List msgs = this...takeMessages(consumeBatchSize)`:到**处理队列获取一批消息** + + * `if (!msgs.isEmpty())`:获取到了待消费的消息 + + `final ConsumeOrderlyContext context`:创建消费上下文对象 + + `this.processQueue.getLockConsume().lock()`:**获取 lockConsume 锁**,与 RBL 线程同步使用 + + `status = messageListener.consumeMessage(...)`:监听器处理消息 + + `this.processQueue.getLockConsume().unlock()`:**释放 lockConsume 锁** + + `if (null == status)`:处理消息状态返回 null,设置状态为挂起当前队列 + + `continueConsume = ...processConsumeResult()`:消费结果处理 + + * `else`:获取到的消息是空 + + `continueConsume = false`:结束任务循环 + + * `else`:当前队列未持有分布式锁,或者锁过期 + + `ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume()`:重新提交任务,根据是否获取到队列锁,选择延迟 10 毫秒或者 300 毫秒 + + + + + + + + + + + + + + + From cbfb2fb2c4eea14ac2443a2fbd2a926fdf565062 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 30 Jan 2022 22:34:01 +0800 Subject: [PATCH 062/122] Update Java Notes --- Frame.md | 1236 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 1069 insertions(+), 167 deletions(-) diff --git a/Frame.md b/Frame.md index ee3ce2c..cec0168 100644 --- a/Frame.md +++ b/Frame.md @@ -4227,7 +4227,7 @@ ConsumeQueue 的存储结构如下,有 8 个字节存储的 Message Tag 的哈 ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-消费队列结构.png) -* Tag 过滤:Consumer 端订阅消息时指定 Topic 和 TAG,然后将订阅请求构建成一个 SubscriptionData,发送一个 Pull 消息的请求给 Broker 端。Broker 端用这些数据先构建一个 MessageFilter,然后传给文件存储层 Store。Store 从 ConsumeQueue 读取到一条记录后,会用它记录的消息 tag hash 值去做过滤。因为在服务端只是根据 hashcode 进行判断,无法精确对 tag 原始字符串进行过滤,所以消费端拉取到消息后,还需要对消息的原始 tag 字符串进行比对,如果不同,则丢弃该消息,不进行消息消费 +* Tag 过滤:Consumer 端订阅消息时指定 Topic 和 TAG,然后将订阅请求构建成一个 SubscriptionData,发送一个 Pull 消息的请求给 Broker 端。Broker 端用这些数据先构建一个 MessageFilter,然后传给文件存储层 Store。Store 从 ConsumeQueue 读取到一条记录后,会用它记录的消息 tag hash 值去做过滤。因为在服务端只是根据 hashcode 进行判断,无法精确对 tag 原始字符串进行过滤,所以消费端拉取到消息后,还需要对消息的原始 tag 字符串进行比对,如果不同,则丢弃该消息,不进行消息消费 * SQL92 过滤:工作流程和 Tag 过滤大致一样,只是在 Store 层的具体过滤方式不一样。真正的 SQL expression 的构建和执行由 rocketmq-filter 模块负责,每次过滤都去执行 SQL 表达式会影响效率,所以 RocketMQ 使用了 BloomFilter 来避免了每次都去执行 @@ -4309,16 +4309,16 @@ RocketMQ 支持分布式事务消息,采用了 2PC 的思想来实现了提交 1. 事务消息发送及提交: * 发送消息(Half 消息) - - * 服务端响应消息写入结果 - +* 服务端响应消息写入结果 * 根据发送结果执行本地事务(如果写入失败,此时 Half 消息对业务不可见,本地逻辑不执行) - * 根据本地事务状态执行 Commit 或者 Rollback(Commit 操作生成消息索引,消息对消费者可见) +* 根据本地事务状态执行 Commit 或者 Rollback(Commit 操作生成消息索引,消息对消费者可见) + + ![](https://gitee.com/seazean/images/raw/master/Frame/RocketMQ-事务工作流程.png) 2. 补偿流程: - * 对没有 Commit/Rollback 的事务消息(pending 状态的消息),从服务端发起一次回查 - * Producer 收到回查消息,检查回查消息对应的本地事务的状态 + * 对没有 Commit/Rollback 的事务消息(pending 状态的消息),服务端根据根据半消息的生产者组,到 ProducerManager 中获取生产者的会话通道,发起一次回查(**单向请求**) + * Producer 收到回查消息,检查事务消息状态表内对应的本地事务的状态 * 根据本地事务状态,重新 Commit 或者 Rollback @@ -4370,7 +4370,7 @@ RocketMQ 将 Op 消息写入到全局一个特定的 Topic 中,通过源码中 如果在 RocketMQ 事务消息的二阶段过程中失败了,例如在做 Commit 操作时,出现网络问题导致 Commit 失败,那么需要通过一定的策略使这条消息最终被 Commit,RocketMQ 采用了一种补偿机制,称为回查 -Broker 端通过对比 Half 消息和 Op 消息,对未确定状态的消息发起回查并且推进 CheckPoint(记录哪些事务消息的状态是确定的),将消息发送到对应的 Producer 端(同一个 Group 的 Producer),由 Producer 根据消息来检查本地事务的状态,然后执行提交或回滚 +Broker 服务端通过对比 Half 消息和 Op 消息,对未确定状态的消息发起回查并且推进 CheckPoint(记录哪些事务消息的状态是确定的),将消息发送到对应的 Producer 端(同一个 Group 的 Producer),由 Producer 根据消息来检查本地事务的状态,然后执行提交或回滚 注意:RocketMQ 并不会无休止的进行事务状态回查,默认回查 15 次,如果 15 次回查还是无法得知事务状态,则默认回滚该消息 @@ -4447,7 +4447,7 @@ public class TransactionListenerImpl implements TransactionListener { } ``` -使用 **TransactionMQProducer** 类创建事务性生产者,并指定唯一的 `ProducerGroup`,就可以设置自定义线程池来处理这些检查请求,执行本地事务后、需要根据执行结果对消息队列进行回复 +使用 **TransactionMQProducer** 类创建事务性生产者,并指定唯一的 `ProducerGroup`,就可以设置自定义线程池来处理这些检查请求,执行本地事务后,需要根据执行结果对消息队列进行回复 ```java public class Producer { @@ -4787,7 +4787,7 @@ RocketMQ 支持消息的高可靠,影响消息可靠性的几种情况: RocketMQ 中的负载均衡可以分为 Producer 端发送消息时候的负载均衡和 Consumer 端订阅消息的负载均衡 -Producer 端在发送消息时,会先根据 Topic 找到指定的 TopicPublishInfo,在获取了 TopicPublishInfo 路由信息后,RocketMQ 的客户端在默认方式调用 selectOneMessageQueue() 方法从 TopicPublishInfo 中的 messageQueueList 中选择一个队列 MessageQueue 进行发送消息 +Producer 端在发送消息时,会先根据 Topic 找到指定的 TopicPublishInfo,在获取了 TopicPublishInfo 路由信息后,RocketMQ 的客户端在默认方式调用 `selectOneMessageQueue()` 方法从 TopicPublishInfo 中的 messageQueueList 中选择一个队列 MessageQueue 进行发送消息 默认会轮询所有的 Message Queue 发送,以让消息平均落在不同的 queue 上,而由于 queue可以散落在不同的 Broker,所以消息就发送到不同的 Broker 下,图中箭头线条上的标号代表顺序,发布方会把第一条消息发送至 Queue 0,然后第二条消息发送至 Queue 1,以此类推: @@ -4798,7 +4798,7 @@ Producer 端在发送消息时,会先根据 Topic 找到指定的 TopicPublish * 如果开启,会在随机递增取模的基础上,再过滤掉 not available 的 Broker 代理 * 如果关闭,采用随机递增取模的方式选择一个队列(MessageQueue)来发送消息 -latencyFaultTolerance 机制是实现消息发送高可用的核心关键所在,对之前失败的,按一定的时间做退避。例如上次请求的 latency 超过 550Lms,就退避 3000Lms;超过 1000L,就退避 60000L +LatencyFaultTolerance 机制是实现消息发送高可用的核心关键所在,对之前失败的,按一定的时间做退避。例如上次请求的 latency 超过 550Lms,就退避 3000Lms;超过 1000L,就退避 60000L @@ -5695,7 +5695,7 @@ NettyRemotingAbstract#processRequestCommand:**处理请求的数据** `DefaultRequestProcessor.processRequest`:**根据业务码处理请求,执行对应的操作** - `ClientRemotingProcessor.processRequest`:处理回退消息,需要消费者回执一条消息给生产者 + `ClientRemotingProcessor.processRequest`:处理事务回查消息,或者回执消息,需要消费者回执一条消息给生产者 * `requestTask = new RequestTask(run, ctx.channel(), cmd)`:将任务对象、通道、请求封装成 RequestTask 对象 @@ -6264,9 +6264,22 @@ CommitLog 类核心方法: * `this.defaultMessageStore.doDispatch(dispatchRequest)`:重建 ConsumerQueue 和 Index,避免上次异常停机导致 CQ 和 Index 与 CommitLog 不对齐 * 剩余逻辑与正常关机的恢复方法相似 -消息追加服务 DefaultAppendMessageCallback + + +*** + + + +##### 服务线程 + +AppendMessageCallback 消息追加服务实现类为 DefaultAppendMessageCallback * doAppend() + + ```java + public AppendMessageResult doAppend() + ``` + * `long wroteOffset = fileFromOffset + byteBuffer.position()`:消息写入的位置,物理偏移量 phyOffset * `String msgId`:消息 ID,规则是客户端 IP + 消息偏移量 phyOffset * `byte[] topicData`:序列化消息,将消息的字段压入到 msgStoreItemMemory 这个 Buffer 中 @@ -6274,6 +6287,71 @@ CommitLog 类核心方法: * `AppendMessageResult result`:构造结果对象,包括存储位点、是否成功、队列偏移量等信息 * `CommitLog.this.topicQueueTable.put(key, ++queueOffset)`:更新队列偏移量 +FlushRealTimeService 刷盘 CL 数据,默认是异步刷盘类 FlushRealTimeService + +* run():运行方法 + + ```java + public void run() + ``` + + * `while (!this.isStopped())`:stopped为 true 才跳出循环 + + * `boolean flushCommitLogTimed`:控制线程的休眠方式,默认是 false,使用 `CountDownLatch.await()` 休眠,设置为 true 时使用 `Thread.sleep()` 休眠 + + * `int interval`:获取配置中的刷盘时间间隔 + + * `int flushPhysicQueueLeastPages`:获取最小刷盘页数,默认是 4 页,脏页达到指定页数才刷盘 + + * `int flushPhysicQueueThoroughInterval`:获取强制刷盘周期,默认是 10 秒,达到周期后强制刷盘,不考虑脏页 + + * `if (flushCommitLogTimed)`:休眠逻辑,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 + + * `CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)`:**刷盘** + + * `for (int i = 0; i < RETRY_TIMES_OVER && !result; i++)`:stopped 停止标记为 true 时,需要确保所有的数据都已经刷盘,所以此处尝试 10 次强制刷盘, + + `result = CommitLog.this.mappedFileQueue.flush(0)`:**强制刷盘** + +同步刷盘类 GroupCommitService + +* run():运行方法 + + ```java + public void run() + ``` + + * `while (!this.isStopped())`:stopped为 true 才跳出循环 + + `this.waitForRunning(10)`:线程休眠 10 毫秒,最后调用 `onWaitEnd()` 进行请求的交换 `swapRequests()` + + `this.doCommit()`:做提交逻辑 + + * `if (!this.requestsRead.isEmpty()) `:读请求集合不为空 + + `for (GroupCommitRequest req : this.requestsRead)`:遍历所有的读请求,请求中的属性: + + * `private final long nextOffset`:本条消息存储之后,下一条消息开始的 offset + * `private CompletableFuture flushOKFuture`:Future 对象 + + `boolean flushOK = ...`:当前请求关注的数据是否全部落盘,**落盘成功唤醒消费者线程** + + `for (int i = 0; i < 2 && !flushOK; i++)`:尝试进行两次强制刷盘,保证刷盘成功 + + `CommitLog.this.mappedFileQueue.flush(0)`:强制刷盘 + + `req.wakeupCustomer(flushOK ? ...)`:设置 Future 结果,在 Future 阻塞的线程在这里会被唤醒 + + `this.requestsRead.clear()`:清理 reqeustsRead 列表,方便交换时 成为 requestsWrite 使用 + + * `else`:读请求集合为空 + + `CommitLog.this.mappedFileQueue.flush(0)`:强制刷盘 + + * `this.swapRequests()`:交换读写请求 + + * `this.doCommit()`:交换后做一次提交 + **** @@ -6594,136 +6672,189 @@ IndexService 类用来管理 IndexFile 文件 -#### MesStore +#### HAService -##### 生命周期 +##### HAService -DefaultMessageStore 类核心是整个存储服务的调度类 +###### Service -* 构造方法: +HAService 类成员变量: + +* 主节点属性: ```java - public DefaultMessageStore() + // master 节点当前有多少个 slave 节点与其进行数据同步 + private final AtomicInteger connectionCount = new AtomicInteger(0); + // master 节点会给每个发起连接的 slave 节点的通道创建一个 HAConnection,控制 master 端向 slave 端传输数据 + private final List connectionList = new LinkedList<>(); + // master 向 slave 节点推送的最大的 offset,表示数据同步的进度 + private final AtomicLong push2SlaveMaxOffset = new AtomicLong(0) ``` - * `this.allocateMappedFileService.start()`:启动**创建 MappedFile 文件服务** - * `this.indexService.start()`:启动索引服务 - -* load():加载资源 +* 内部类属性: ```java - public boolean load() + // 封装了绑定服务器指定端口,监听 slave 的连接的逻辑,没有使用 Netty,使用了原生态的 NIO 去做 + private final AcceptSocketService acceptSocketService; + // 控制生产者线程阻塞等待的逻辑 + private final GroupTransferService groupTransferService; + // slave 节点的客户端对象,【slave 端才会正常运行该实例】 + private final HAClient haClient; ``` - * `this.commitLog.load()`:先加载 CommitLog - * `this.loadConsumeQueue()`:再加载 ConsumeQueue - * `this.storeCheckpoint`:检查位点对象 - * `this.indexService.load(lastExitOK)`:加载 IndexFile - * `this.recover(lastExitOK)`:恢复阶段,先恢复 CQ,在恢复 CL - -* start():核心启动方法 +* 线程通信对象: ```java - public void start() + private final WaitNotifyObject waitNotifyObject = new WaitNotifyObject() ``` - * `lock = lockFile.getChannel().tryLock(0, 1, false)`:获取文件锁,获取失败说明当前目录已经启动过 Broker - - * `long maxPhysicalPosInLogicQueue = commitLog.getMinOffset()`:遍历全部的 CQ 对象,获取 CQ 中消息的最大偏移量 - - * `this.reputMessageService.start()`:设置分发服务的分发位点,启动**分发服务**,构建 ConsumerQueue 和 IndexFile +成员方法: - * `if (dispatchBehindBytes() <= 0)`:线程等待分发服务将分发数据全部处理完毕 +* start():启动高可用服务 - * `this.recoverTopicQueueTable()`:因为修改了 CQ 数据,所以再次构建队列偏移量字段表 + ```java + public void start() throws Exception { + // 监听从节点 + this.acceptSocketService.beginAccept(); + // 启动监听服务 + this.acceptSocketService.start(); + // 启动转移服务 + this.groupTransferService.start(); + // 启动从节点客户端实例 + this.haClient.start(); + } + ``` - * `this.haService.start()`:启动 **HA 服务** - * `this.handleScheduleMessageService()`:启动**消息调度服务** - * `this.flushConsumeQueueService.start()`:启动 CQ **消费队列刷盘服务** +**** - * `this.commitLog.start()`:启动 **CL 刷盘服务** - * `this.storeStatsService.start()`:启动状态存储服务 - * `this.createTempFile()`:创建 AbortFile,正常关机时 JVM HOOK 会删除该文件,异常宕机时该文件不会删除,开机数据恢复阶段根据是否存在该文件,执行不同的**恢复策略** +###### Accept - * `this.addScheduleTask()`:添加定时任务 +AcceptSocketService 类用于监听从节点的连接,创建 HAConnection 连接对象 - * `DefaultMessageStore.this.cleanFilesPeriodically()`:定时**清理过期文件**,周期是 10 秒 +成员变量: - * `this.cleanCommitLogService.run()`:启动清理过期的 CL 文件服务 - * `this.cleanConsumeQueueService.run()`:启动清理过期的 CQ 文件服务 +* 端口信息:Master 绑定监听的端口信息 - * `DefaultMessageStore.this.checkSelf()`:每 10 分种进行健康检查 + ```java + private final SocketAddress socketAddressListen; + ``` - * `DefaultMessageStore.this.cleanCommitLogService.isSpaceFull()`:**磁盘预警**定时任务,每 10 秒一次 +* 服务端通道: - * `if (physicRatio > this.diskSpaceWarningLevelRatio)`:检查磁盘是否到达 waring 阈值,默认 90% + ```java + private ServerSocketChannel serverSocketChannel; + ``` - `boolean diskok = ...runningFlags.getAndMakeDiskFull()`:设置磁盘写满标记 +* 多路复用器: - * `boolean diskok = ...this.runningFlags.getAndMakeDiskOK()`:设置磁盘可写标记 + ```java + private Selector selector; + ``` - * `this.shutdown = false`:刚启动,设置为 false +成员方法: -* shutdown():关闭各种服务和线程资源,设置存储模块状态为关闭状态 +* beginAccept():开始监听连接,**NIO** 标准模板 ```java - public void shutdown() + public void beginAccept() ``` -* destroy():销毁 Broker 的工作目录 + * `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 - public void destroy() + public void run() ``` + * `this.selector.select(1000)`:多路复用器阻塞获取就绪的通道,最多等待 1 秒钟 + * `Set selected = this.selector.selectedKeys()`:获取选择器中所有注册的通道中已经就绪好的事件 + * `for (SelectionKey k : selected)`:遍历所有就绪的事件 + * `if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0)`:说明 `OP_ACCEPT` 事件就绪 + * `SocketChannel sc = ((ServerSocketChannel) k.channel()).accept()`:**获取到客户端连接的通道** + * `HAConnection conn = new HAConnection(HAService.this, sc)`:**为每个连接 master 服务器的 slave 创建连接对象** + * `conn.start()`:**启动 HAConnection 对象**,内部启动两个服务为读数据服务、写数据服务 + * `HAService.this.addConnection(conn)`:加入到 HAConnection 集合内 +**** -*** +###### Group -##### 服务线程 +GroupTransferService 用来控制数据同步 -ServiceThread 类被很多服务继承,本身是一个 Runnable 任务对象,继承者通过重写 run 方法来实现服务的逻辑 +成员方法: -* run():一般实现方式 +* doWaitTransfer():等待主从数据同步 ```java - public void run() { - while (!this.isStopped()) { - // 业务逻辑 - } - } + private void doWaitTransfer() ``` - 通过参数 stopped 控制服务的停止,使用 volatile 修饰保证可见性 + * `if (!this.requestsRead.isEmpty())`:读请求不为空 + * `boolean transferOK = HAService.this.push2SlaveMaxOffset.get() >= req.getNextOffset()`:主从同步是否完成 + * `req.wakeupCustomer(transferOK ? ...)`:唤醒消费者 + * `this.requestsRead.clear()`:清空读请求 + + + +**** + + + +##### HAClient + +###### 成员属性 + +HAClient 是 slave 端运行的代码,用于和 master 服务器建立长连接,上报本地同步进度,消费服务器发来的 msg 数据 + +成员变量: + +* 缓冲区: ```java - protected volatile boolean stopped = false + private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024 * 4; // 默认大小:4 MB + private ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); + private ByteBuffer byteBufferBackup = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); ``` -* shutdown():停止线程,首先设置 stopped 为 true,然后进行唤醒,默认不直接打断线程 +* 主节点地址:格式为 `ip:port` ```java - public void shutdown() + private final AtomicReference masterAddress = new AtomicReference<>() ``` -* waitForRunning():挂起线程,设置唤醒标记 hasNotified 为 false +* NIO 属性: ```java - protected void waitForRunning(long interval) + private final ByteBuffer reportOffset; // 通信使用NIO,所以消息使用块传输,上报 slave offset 使用 + private SocketChannel socketChannel; // 客户端与 master 的会话通道 + private Selector selector; // 多路复用器 ``` -* wakeup():唤醒线程,设置 hasNotified 为 true +* 通信时间:上次会话通信时间,用于控制 socketChannel 是否关闭的 + + ````java + private long lastWriteTimestamp = System.currentTimeMillis(); + ```` + +* 进度信息: ```java - public void wakeup() + private long currentReportedOffset = 0; // slave 当前的进度信息 + private int dispatchPosition = 0; // 控制 byteBufferRead position 指针 ``` @@ -6732,151 +6863,710 @@ ServiceThread 类被很多服务继承,本身是一个 Runnable 任务对象 -##### 构建服务 - -AllocateMappedFileService 创建 MappedFile 服务 +###### 成员方法 -* mmapOperation():核心服务 +* run():启动方法 ```java - private boolean mmapOperation() + public void run() ``` - * `req = this.requestQueue.take()`: 从 requestQueue 阻塞队列(优先级)中获取 AllocateRequest 任务 - * `if (...isTransientStorePoolEnable())`:条件成立使用直接内存写入数据, 从直接内存中 commit 到 FileChannel 中 - * `mappedFile = new MappedFile(req.getFilePath(), req.getFileSize())`:根据请求的路径和大小创建对象 - * `mappedFile.warmMappedFile()`:判断 mappedFile 大小,只有 CommitLog 才进行文件预热 - * `req.setMappedFile(mappedFile)`:将创建好的 MF 对象的赋值给请求对象的成员属性 - * `req.getCountDownLatch().countDown()`:**唤醒请求的阻塞线程** + * `if (this.connectMaster())`:连接主节点,连接失败会休眠 5 秒 -* putRequestAndReturnMappedFile():MappedFileQueue 中用来创建 MF 对象的方法 + * `String addr = this.masterAddress.get()`:获取 master 暴露的 HA 地址端口信息 + * `this.socketChannel = RemotingUtil.connect(socketAddress)`:建立连接 + * `this.socketChannel.register(this.selector, SelectionKey.OP_READ)`:注册到多路复用器,**关注读事件** + * `this.currentReportedOffset`: 初始化上报进度字段为 slave 的 maxPhyOffset - ```java - public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize) - ``` + * `if (this.isTimeToReportOffset())`:slave 每 5 秒会上报一次 slave 端的同步进度信息给 master - * `AllocateRequest nextReq = new AllocateRequest(...)`:创建 nextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列,然后创建 nextNextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列 - * `AllocateRequest result = this.requestTable.get(nextFilePath)`:从请求列表获取 nextFilePath 的请求对象 - * `result.getCountDownLatch().await(...)`:**线程挂起**,直到超时或者 nextFilePath 对应的 MF 文件创建完成 - * `return result.getMappedFile()`:返回创建好的 MF 文件对象 + `boolean result = this.reportSlaveMaxOffset()`:上报同步信息,上报失败关闭连接 -ReputMessageService 消息分发服务,用于构建 ConsumerQueue 和 IndexFile 文件 + * `this.selector.select(1000)`:多路复用器阻塞获取就绪的通道,最多等待 1 秒钟,**获取到就绪事件或者超时后结束** -* run():循环执行 doReput 方法,每执行一次线程休眠 1 毫秒 + * `boolean ok = this.processReadEvent()`:处理读事件 + + * `if (!reportSlaveMaxOffsetPlus())`:检查是否重新上报同步进度 + +* reportSlaveMaxOffset():上报 slave 同步进度 ```java - public void run() + private boolean reportSlaveMaxOffset(final long maxOffset) ``` -* doReput():实现分发的核心逻辑 + * 首先向缓冲区写入 slave 端最大偏移量,写完以后切换为指定置为初始状态 + + * `for (int i = 0; i < 3 && this.reportOffset.hasRemaining(); i++)`:尝试三次写数据 + + `this.socketChannel.write(this.reportOffset)`:**写数据** + + * `return !this.reportOffset.hasRemaining()`:写成功之后 pos = limit + +* processReadEvent():处理 master 发送给 slave 数据,返回 true 表示处理成功 false 表示 Socket 处于半关闭状态,需要上层重建 haClient ```java - private void doReput() + private boolean processReadEvent() ``` - * `for (boolean doNext = true; this.isCommitLogAvailable() && doNext; )`:循环遍历 - * `SelectMappedBufferResult result`: 从 CommitLog 拉取数据,数据范围 `[reputFromOffset, 包含该偏移量的 MF 的最大 Pos]`,封装成结果对象 - * `DispatchRequest dispatchRequest`:从结果对象读取出一条 DispatchRequest 数据 - * `DefaultMessageStore.this.doDispatch(dispatchRequest)`:将数据交给分发器进行分发,用于**构建 CQ 和索引文件** - * `this.reputFromOffset += size`:更新数据范围 + * `int readSizeZeroTimes = 0`:控制 while 循环的一个条件变量,当值为 3 时跳出循环 + * `while (this.byteBufferRead.hasRemaining())`:byteBufferRead 有空间可以去 Socket 读缓冲区加载数据 + * `int readSize = this.socketChannel.read(this.byteBufferRead)`:**从通道读数据** -*** + * `if (readSize > 0)`:加载成功,有新数据 + `readSizeZeroTimes = 0`:置为 0 + `boolean result = this.dispatchReadRequest()`:处理数据的核心逻辑 -##### 刷盘服务 + * `else if (readSize == 0) `:无新数据 -FlushConsumeQueueService 刷盘 CQ 数据 + `if (++readSizeZeroTimes >= 3)`:大于 3 时跳出循环 -* run():每隔 1 秒执行一次刷盘服务,跳出循环后还会执行一次强制刷盘 + * `else`:readSize = -1 就表示 Socket 处于半关闭状态,对端已经关闭了 + +* dispatchReadRequest():**处理数据的核心逻辑**,master 与 slave 传输的数据格式 `{[phyOffset][size][data...]}`,phyOffset 表示数据区间的开始偏移量,data 代表数据块,最大 32kb,可能包含多条消息的数据 ```java - public void run() + private boolean dispatchReadRequest() ``` -* doFlush():刷盘 + * `final int msgHeaderSize = 8 + 4`:协议头大小 12 - ```java - private void doFlush(int retryTimes) - ``` + * `int readSocketPos = this.byteBufferRead.position()`:记录缓冲区处理数据前的 pos 位点,用于恢复指针 - * `int flushConsumeQueueLeastPages`:脏页阈值,默认是 2 + * `int diff = ...`:当前 byteBufferRead 还剩多少 byte 未处理,每处理一条帧数据都会更新 dispatchPosition - * `if (retryTimes == RETRY_TIMES_OVER)`:**重试次数是 3** 时设置强制刷盘,设置脏页阈值为 0 - * `int flushConsumeQueueThoroughInterval`:两次刷新的**时间间隔超过 60 秒**会强制刷盘 - * `for (ConsumeQueue cq : maps.values())`:遍历所有的 CQ,进行刷盘 - * `DefaultMessageStore.this.getStoreCheckpoint().flush()`:强制刷盘时将 StoreCheckpoint 瞬时数据刷盘 + * `if (diff >= msgHeaderSize)`:缓冲区还有完整的协议头 header 数据 -FlushCommitLogService 刷盘 CL 数据,默认是异步刷盘 + * `long masterPhyOffset, int bodySize`:读取 header 信息 -* run():运行方法 + * `long slavePhyOffset`:获取 slave 端最大的物理偏移量 - ```java - public void run() - ``` + * `if (slavePhyOffset != masterPhyOffset)`:正常情况两者是相等的,因为是一帧一帧同步的 - * `while (!this.isStopped())`:stopped为 true 才跳出循环 - - * `boolean flushCommitLogTimed`:控制线程的休眠方式,默认是 false,使用 `CountDownLatch.await()` 休眠,设置为 true 时使用 `Thread.sleep()` 休眠 - - * `int interval`:获取配置中的刷盘时间间隔 - - * `int flushPhysicQueueLeastPages`:获取最小刷盘页数,默认是 4 页,脏页达到指定页数才刷盘 - - * `int flushPhysicQueueThoroughInterval`:获取强制刷盘周期,默认是 10 秒,达到周期后强制刷盘,不考虑脏页 - - * `if (flushCommitLogTimed)`:休眠逻辑,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 - - * `CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)`:**刷盘** - - * `for (int i = 0; i < RETRY_TIMES_OVER && !result; i++)`:stopped 停止标记为 true 时,需要确保所有的数据都已经刷盘,所以此处尝试 10 次强制刷盘, - - `result = CommitLog.this.mappedFileQueue.flush(0)`:**强制刷盘** + * `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 + `this.byteBufferRead.position(readSocketPos)`:恢复 byteBufferRead 的 pos 指针 -##### 清理服务 + `this.dispatchPosition += msgHeaderSize + bodySize`:**加一帧数据长度** -CleanCommitLogService 清理过期的 CL 数据,定时任务 10 秒调用一次,**先清理 CL,再清理 CQ**,因为 CQ 依赖于 CL 的数据 + `if (!reportSlaveMaxOffsetPlus())`:上报 slave 同步信息 -* run():运行方法 + * `if (!this.byteBufferRead.hasRemaining())`:缓冲区写满了 - ```java - public void run() - ``` + `this.reallocateByteBuffer()`:重新分配缓冲区 -* deleteExpiredFiles():删除过期 CL 文件 +* reallocateByteBuffer():重新分配缓冲区 ```java - private void deleteExpiredFiles() + private void reallocateByteBuffer() ``` - * `long fileReservedTime`:默认 72,代表文件的保留时间 - * `boolean timeup = this.isTimeToDelete()`:当前时间是否是凌晨 4 点 - * `boolean spacefull = this.isSpaceToDelete()`:CL 或者 CQ 的目录磁盘使用率达到阈值标准 85% - * `boolean manualDelete = this.manualDeleteFileSeveralTimes > 0`:手动删除文件 - * `fileReservedTime *= 60 * 60 * 1000`:默认保留 72 小时 - * `deleteCount = DefaultMessageStore.this.commitLog.deleteExpiredFile()`:**调用 MFQ 对象的删除方法** + * `int remain = READ_MAX_BUFFER_SIZE - this.dispatchPosition`:表示缓冲区尚未处理过的字节数量 -CleanConsumeQueueService 清理过期的 CQ 数据 + * `if (remain > 0)`:条件成立,说明缓冲区**最后一帧数据是半包数据**,但是不能丢失数据 -* run():运行方法 + `this.byteBufferBackup.put(this.byteBufferRead)`:**将半包数据拷贝到 backup 缓冲区** - ```java - public void run() - ``` + * `this.swapByteBuffer()`:交换 backup 成为 read -* deleteExpiredFiles():删除过期 CQ 文件 + * `this.byteBufferRead.position(remain)`:设置 pos 为 remain ,后续加载数据 pos 从remain 开始向后移动 - ```java - private void deleteExpiredFiles() - ``` + * `this.dispatchPosition = 0`:当前缓冲区交换之后,相当于是一个全新的 byteBuffer,所以分配指针归零 + + + +*** + + + +##### HAConn + +###### Connection + +HAConnection 类成员变量: + +* 会话通道:master 和 slave 之间通信的 SocketChannel + + ```java + private final SocketChannel socketChannel; + ``` + +* 客户端地址: + + ```java + private final String clientAddr; + ``` + +* 服务类: + + ```java + private WriteSocketService writeSocketService; // 写数据服务 + private ReadSocketService readSocketService; // 读数据服务 + ``` + +* 请求位点:在 slave上报本地的进度之后被赋值,该值大于 0 后同步逻辑才会运行,master 如果不知道 slave 节点当前消息的存储进度,就无法给 slave 推送数据 + + ```java + private volatile long slaveRequestOffset = -1; + ``` + +* 应答位点: 保存最新的 slave 上报的 offset 信息,slaveAckOffset 之前的数据都可以认为 slave 已经同步完成 + + ```java + private volatile long slaveAckOffset = -1; + ``` + +核心方法: + +* 构造方法: + + ```java + public HAConnection(final HAService haService, final SocketChannel socketChannel) { + // 初始化一些东西 + // 设置 socket 读写缓冲区为 64kb 大小 + this.socketChannel.socket().setReceiveBufferSize(1024 * 64); + this.socketChannel.socket().setSendBufferSize(1024 * 64); + // 创建读写服务 + this.writeSocketService = new WriteSocketService(this.socketChannel); + this.readSocketService = new ReadSocketService(this.socketChannel); + // 自增 + this.haService.getConnectionCount().incrementAndGet(); + } + ``` + +* 启动方法: + + ```java + public void start() { + this.readSocketService.start(); + this.writeSocketService.start(); + } + ``` + + + +*** + + + +###### ReadSocket + +ReadSocketService 类是一个任务对象,slave 向 master 传输的帧格式为 `[long][long][long]`,上报的是 slave 本地的同步进度,同步进度是一个 long 值 + +成员变量: + +* 读缓冲: + + ```java + private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024; // 默认大小 1MB + private final ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE); + ``` + +* NIO 属性: + + ```java + private final Selector selector; // 多路复用器 + private final SocketChannel socketChannel; // master 与 slave 之间的会话 SocketChannel + ``` + +* 处理位点:缓冲区处理位点 + + ```java + private int processPosition = 0; + ``` + +* 上次读操作的时间: + + ```java + private volatile long lastReadTimestamp = System.currentTimeMillis(); + ``` + +核心方法: + +* 构造方法: + + ```java + public ReadSocketService(final SocketChannel socketChannel) + ``` + + * `this.socketChannel.register(this.selector, SelectionKey.OP_READ)`:通道注册到多路复用器,关注读事件 + * `this.setDaemon(true)`:设置为守护线程 + +* 运行方法: + + ```java + public void run() + ``` + + * `this.selector.select(1000)`:多路复用器阻塞获取就绪的通道,最多等待 1 秒钟,获取到就绪事件或者超时后结束 + + * `boolean ok = this.processReadEvent()`:**读数据的核心方法**,返回 true 表示处理成功 false 表示 Socket 处于半关闭状态,需要上层重建 HAConnection 对象 + + * `int readSizeZeroTimes = 0`:控制 while 循环,当连续从 Socket 读取失败 3 次(未加载到数据)跳出循环 + + * `if (!this.byteBufferRead.hasRemaining())`:byteBufferRead 已经全部使用完,需要清理数据并更新位点 + + * `while (this.byteBufferRead.hasRemaining())`:byteBufferRead 有空间可以去 Socket 读缓冲区加载数据 + + * `int readSize = this.socketChannel.read(this.byteBufferRead)`:**从通道读数据** + + * `if (readSize > 0)`:加载成功,有新数据 + + `readSizeZeroTimes = 0`:置为 0 + + `if ((byteBufferRead.position() - processPosition) >= 8)`:缓冲区的可读数据最少包含一个数据帧 + + * `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`:readSize = -1 就表示 Socket 处于半关闭状态,对端已经关闭了 + + * `if (interval > 20)`:超过 20 秒未发生通信,直接结束循环 + + + +*** + + + +###### WriteSocket + +WriteSocketService 类是一个任务对象,master向 slave 传输的数据帧格式为 `{[phyOffset][size][data...]}{[phyOffset][size][data...]}`,上报的是 slave 本地的同步进度,同步进度是一个 long 值 + +* phyOffset:数据区间的开始偏移量,并不表示某一条具体的消息,表示的数据块开始的偏移量位置 +* size:同步的数据块的大小 +* data:数据块,最大 32kb,可能包含多条消息的数据 + +成员变量: + +* 协议头: + + ```java + private final int headerSize = 8 + 4; // 协议头大小:12 + private final ByteBuffer byteBufferHeader; // 帧头缓冲区 + ``` + +* NIO 属性: + + ```java + private final Selector selector; // 多路复用器 + private final SocketChannel socketChannel; // master 与 slave 之间的会话 SocketChannel + ``` + +* 处理位点:下一次传输同步数据的位置信息,master 给当前 slave 同步的位点 + + ```java + private long nextTransferFromWhere = -1; + ``` + +* 上次写操作: + + ```java + private boolean lastWriteOver = true; // 上一轮数据是否传输完毕 + private long lastWriteTimestamp = System.currentTimeMillis(); // 上次写操作的时间 + ``` + +核心方法: + +* 构造方法: + + ```java + public WriteSocketService(final SocketChannel socketChannel) + ``` + + * `this.socketChannel.register(this.selector, SelectionKey.OP_WRITE)`:通道注册到多路复用器,关注写事件 + * `this.setDaemon(true)`:设置为守护线程 + +* 运行方法: + + ```java + public void run() + ``` + + * `this.selector.select(1000)`:多路复用器阻塞获取就绪的通道,最多等待 1 秒钟,获取到就绪事件或者超时后结束 + + * `if (-1 == HAConnection.this.slaveRequestOffset)`:**等待 slave 同步完数据** + + * `if (-1 == this.nextTransferFromWhere)`:条件成立,需要初始化该变量 + + `if (0 == HAConnection.this.slaveRequestOffset)`:slave 是一个全新节点,从正在顺序写的 MF 开始同步数据 + + `long masterOffset = ...`:获取 master 最大的 offset,并计算归属的 mappedFile 文件的开始 offset + + `this.nextTransferFromWhere = masterOffset`:**赋值给下一次传输同步数据的位置信息** + + `this.nextTransferFromWhere = HAConnection.this.slaveRequestOffset`:大部分情况走这个赋值逻辑 + + * `if (this.lastWriteOver)`:上一次待发送数据全部发送完成 + + `if (interval > 5)`:超过 5 秒未同步数据,发送一个 header 数据包,维持长连接 + + * `else`:上一轮的待发送数据未全部发送,需要同步数据到 slave 节点 + + * `SelectMappedBufferResult selectResult`:到 CommitLog 中查询 nextTransferFromWhere 开始位置的数据 + + * `if (size > 32k)`:**一次最多同步 32k 数据** + + * `this.nextTransferFromWhere += size`:增加 size,下一轮传输跳过本帧数据 + + * `selectResult.getByteBuffer().limit(size)`:设置 byteBuffer 可访问数据区间为 [pos, size] + + * `this.selectMappedBufferResult = selectResult`:**待发送的数据** + + * `this.byteBufferHeader.put`:**构建帧头数据** + + * `this.lastWriteOver = this.transferData()`:处理数据,返回是否处理完成 + +* 同步方法:**同步数据到 slave 节点**,返回 true 表示本轮数据全部同步完成,false 表示本轮同步未完成(Header 和 Body 其中一个未同步完成都会返回 false) + + ```java + private boolean transferData() + ``` + + * `int writeSizeZeroTimes= 0`:控制 while 循环,当写失败连续 3 次时,跳出循环)跳出循环 + + * `while (this.byteBufferHeader.hasRemaining())`:**帧头数据缓冲区有待发送的数据** + + * `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())`:**数据缓冲区有待发送的数据** + + * `int writeSize = this.socketChannel.write(this.selectMappedBufferResult...)`:向通道写帧头数据 + + * `if (writeSize > 0)`:写数据成功,但是不代表 SMBR 中的数据全部写完成 + + `writeSizeZeroTimes = 0`:控制变量置为 0 + + * `else if (readSize == 0)`:写失败,因为 Socket 写缓冲区写满了 + + `if (++readSizeZeroTimes >= 3)`:大于 3 时跳出循环 + + * `boolean result`:判断是否发送完成,返回该值 + + + + + +**** + + + +#### MesStore + +##### 生命周期 + +DefaultMessageStore 类核心是整个存储服务的调度类 + +* 构造方法: + + ```java + public DefaultMessageStore() + ``` + + * `this.allocateMappedFileService.start()`:启动**创建 MappedFile 文件服务** + * `this.indexService.start()`:启动索引服务 + +* load():加载资源 + + ```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 + public void start() + ``` + + * `lock = lockFile.getChannel().tryLock(0, 1, false)`:获取文件锁,获取失败说明当前目录已经启动过 Broker + + * `long maxPhysicalPosInLogicQueue = commitLog.getMinOffset()`:遍历全部的 CQ 对象,获取 CQ 中消息的最大偏移量 + + * `this.reputMessageService.start()`:设置分发服务的分发位点,启动**分发服务**,构建 ConsumerQueue 和 IndexFile + + * `if (dispatchBehindBytes() <= 0)`:线程等待分发服务将分发数据全部处理完毕 + + * `this.recoverTopicQueueTable()`:因为修改了 CQ 数据,所以再次构建队列偏移量字段表 + + * `this.haService.start()`:启动 **HA 服务** + + * `this.handleScheduleMessageService()`:启动**消息调度服务** + + * `this.flushConsumeQueueService.start()`:启动 CQ **消费队列刷盘服务** + + * `this.commitLog.start()`:启动 **CL 刷盘服务** + + * `this.storeStatsService.start()`:启动状态存储服务 + + * `this.createTempFile()`:创建 AbortFile,正常关机时 JVM HOOK 会删除该文件,异常宕机时该文件不会删除,开机数据恢复阶段根据是否存在该文件,执行不同的**恢复策略** + + * `this.addScheduleTask()`:添加定时任务 + + * `DefaultMessageStore.this.cleanFilesPeriodically()`:定时**清理过期文件**,周期是 10 秒 + + * `this.cleanCommitLogService.run()`:启动清理过期的 CL 文件服务 + * `this.cleanConsumeQueueService.run()`:启动清理过期的 CQ 文件服务 + + * `DefaultMessageStore.this.checkSelf()`:每 10 分种进行健康检查 + + * `DefaultMessageStore.this.cleanCommitLogService.isSpaceFull()`:**磁盘预警**定时任务,每 10 秒一次 + + * `if (physicRatio > this.diskSpaceWarningLevelRatio)`:检查磁盘是否到达 waring 阈值,默认 90% + + `boolean diskok = ...runningFlags.getAndMakeDiskFull()`:设置磁盘写满标记 + + * `boolean diskok = ...this.runningFlags.getAndMakeDiskOK()`:设置磁盘可写标记 + + * `this.shutdown = false`:刚启动,设置为 false + +* shutdown():关闭各种服务和线程资源,设置存储模块状态为关闭状态 + + ```java + public void shutdown() + ``` + +* destroy():销毁 Broker 的工作目录 + + ```java + public void destroy() + ``` + + + + + +*** + + + +##### 服务线程 + +ServiceThread 类被很多服务继承,本身是一个 Runnable 任务对象,继承者通过重写 run 方法来实现服务的逻辑 + +* run():一般实现方式 + + ```java + public void run() { + while (!this.isStopped()) { + // 业务逻辑 + } + } + ``` + + 通过参数 stopped 控制服务的停止,使用 volatile 修饰保证可见性 + + ```java + protected volatile boolean stopped = false + ``` + +* shutdown():停止线程,首先设置 stopped 为 true,然后进行唤醒,默认不直接打断线程 + + ```java + public void shutdown() + ``` + +* waitForRunning():挂起线程,设置唤醒标记 hasNotified 为 false + + ```java + protected void waitForRunning(long interval) + ``` + +* wakeup():唤醒线程,设置 hasNotified 为 true + + ```java + public void wakeup() + ``` + + + +*** + + + +##### 构建服务 + +AllocateMappedFileService 创建 MappedFile 服务 + +* mmapOperation():核心服务 + + ```java + private boolean mmapOperation() + ``` + + * `req = this.requestQueue.take()`: 从 requestQueue 阻塞队列(优先级)中获取 AllocateRequest 任务 + * `if (...isTransientStorePoolEnable())`:条件成立使用直接内存写入数据, 从直接内存中 commit 到 FileChannel 中 + * `mappedFile = new MappedFile(req.getFilePath(), req.getFileSize())`:根据请求的路径和大小创建对象 + * `mappedFile.warmMappedFile()`:判断 mappedFile 大小,只有 CommitLog 才进行文件预热 + * `req.setMappedFile(mappedFile)`:将创建好的 MF 对象的赋值给请求对象的成员属性 + * `req.getCountDownLatch().countDown()`:**唤醒请求的阻塞线程** + +* putRequestAndReturnMappedFile():MappedFileQueue 中用来创建 MF 对象的方法 + + ```java + public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize) + ``` + + * `AllocateRequest nextReq = new AllocateRequest(...)`:创建 nextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列,然后创建 nextNextFilePath 的 AllocateRequest 对象,放入请求列表和阻塞队列 + * `AllocateRequest result = this.requestTable.get(nextFilePath)`:从请求列表获取 nextFilePath 的请求对象 + * `result.getCountDownLatch().await(...)`:**线程挂起**,直到超时或者 nextFilePath 对应的 MF 文件创建完成 + * `return result.getMappedFile()`:返回创建好的 MF 文件对象 + +ReputMessageService 消息分发服务,用于构建 ConsumerQueue 和 IndexFile 文件 + +* run():循环执行 doReput 方法,每执行一次线程休眠 1 毫秒 + + ```java + public void run() + ``` + +* doReput():实现分发的核心逻辑 + + ```java + private void doReput() + ``` + + * `for (boolean doNext = true; this.isCommitLogAvailable() && doNext; )`:循环遍历 + * `SelectMappedBufferResult result`: 从 CommitLog 拉取数据,数据范围 `[reputFromOffset, 包含该偏移量的 MF 的最大 Pos]`,封装成结果对象 + * `DispatchRequest dispatchRequest`:从结果对象读取出一条 DispatchRequest 数据 + * `DefaultMessageStore.this.doDispatch(dispatchRequest)`:将数据交给分发器进行分发,用于**构建 CQ 和索引文件** + * `this.reputFromOffset += size`:更新数据范围 + + + +*** + + + +##### 刷盘服务 + +FlushConsumeQueueService 刷盘 CQ 数据 + +* run():每隔 1 秒执行一次刷盘服务,跳出循环后还会执行一次强制刷盘 + + ```java + public void run() + ``` + +* doFlush():刷盘 + + ```java + private void doFlush(int retryTimes) + ``` + + * `int flushConsumeQueueLeastPages`:脏页阈值,默认是 2 + + * `if (retryTimes == RETRY_TIMES_OVER)`:**重试次数是 3** 时设置强制刷盘,设置脏页阈值为 0 + * `int flushConsumeQueueThoroughInterval`:两次刷新的**时间间隔超过 60 秒**会强制刷盘 + * `for (ConsumeQueue cq : maps.values())`:遍历所有的 CQ,进行刷盘 + * `DefaultMessageStore.this.getStoreCheckpoint().flush()`:强制刷盘时将 StoreCheckpoint 瞬时数据刷盘 + +FlushCommitLogService 刷盘 CL 数据,默认是异步刷盘 + +* run():运行方法 + + ```java + public void run() + ``` + + * `while (!this.isStopped())`:stopped为 true 才跳出循环 + + * `boolean flushCommitLogTimed`:控制线程的休眠方式,默认是 false,使用 `CountDownLatch.await()` 休眠,设置为 true 时使用 `Thread.sleep()` 休眠 + + * `int interval`:获取配置中的刷盘时间间隔 + + * `int flushPhysicQueueLeastPages`:获取最小刷盘页数,默认是 4 页,脏页达到指定页数才刷盘 + + * `int flushPhysicQueueThoroughInterval`:获取强制刷盘周期,默认是 10 秒,达到周期后强制刷盘,不考虑脏页 + + * `if (flushCommitLogTimed)`:休眠逻辑,避免 CPU 占用太长时间,导致无法执行其他更紧急的任务 + + * `CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages)`:**刷盘** + + * `for (int i = 0; i < RETRY_TIMES_OVER && !result; i++)`:stopped 停止标记为 true 时,需要确保所有的数据都已经刷盘,所以此处尝试 10 次强制刷盘, + + `result = CommitLog.this.mappedFileQueue.flush(0)`:**强制刷盘** + + + +*** + + + +##### 清理服务 + +CleanCommitLogService 清理过期的 CL 数据,定时任务 10 秒调用一次,**先清理 CL,再清理 CQ**,因为 CQ 依赖于 CL 的数据 + +* run():运行方法 + + ```java + public void run() + ``` + +* deleteExpiredFiles():删除过期 CL 文件 + + ```java + private void deleteExpiredFiles() + ``` + + * `long fileReservedTime`:默认 72,代表文件的保留时间 + * `boolean timeup = this.isTimeToDelete()`:当前时间是否是凌晨 4 点 + * `boolean spacefull = this.isSpaceToDelete()`:CL 或者 CQ 的目录磁盘使用率达到阈值标准 85% + * `boolean manualDelete = this.manualDeleteFileSeveralTimes > 0`:手动删除文件 + * `fileReservedTime *= 60 * 60 * 1000`:默认保留 72 小时 + * `deleteCount = DefaultMessageStore.this.commitLog.deleteExpiredFile()`:**调用 MFQ 对象的删除方法** + +CleanConsumeQueueService 清理过期的 CQ 数据 + +* run():运行方法 + + ```java + public void run() + ``` + +* deleteExpiredFiles():删除过期 CQ 文件 + + ```java + private void deleteExpiredFiles() + ``` * `int deleteLogicsFilesInterval`:清理 CQ 的时间间隔,默认 100 毫秒 * `long minOffset = DefaultMessageStore.this.commitLog.getMinOffset()`:获取 CL 文件中最小的物理偏移量 @@ -6894,7 +7584,7 @@ CleanConsumeQueueService 清理过期的 CQ 数据 ##### 获取消息 -PullMessageProcessor#processRequest 方法中调用 getMessage 用于获取消息(提示:建议学习消费者源码时再阅读) +DefaultMessageStore#getMessage 用于获取消息,在 PullMessageProcessor#processRequest 方法中被调用 (提示:建议学习消费者源码时再阅读) ```java // offset: 客户端拉消息使用位点; maxMsgNums: 32; messageFilter: 一般这里是 tagCode 过滤 @@ -7024,8 +7714,6 @@ BrokerController#start:核心启动方法 - - **** @@ -8163,9 +8851,223 @@ DeliverDelayedMessageTimerTask 是一个任务类 +**** + + + +#### 事务消息 + +##### 生产者类 + +TransactionMQProducer 类发送事务消息时使用 + +成员变量: + +* 事务回查线程池资源: + + ```java + private ExecutorService executorService; + +* 事务监听器: + + ```java + private TransactionListener transactionListener; + ``` + +核心方法: + +* start():启动方法 + + ```java + public void start() + ``` + + * `this.defaultMQProducerImpl.initTransactionEnv()`:初始化生产者实例和回查线程池资源 + * `super.start()`:启动生产者实例 + +* sendMessageInTransaction():发送事务消息 + + ```java + public TransactionSendResult sendMessageInTransaction(final Message msg, final Object arg) { + msg.setTopic(NamespaceUtil.wrapNamespace(this.getNamespace(), msg.getTopic())); + // 调用实现类的发送方法 + return this.defaultMQProducerImpl.sendMessageInTransaction(msg, null, arg); + } + ``` + + * `TransactionListener transactionListener = getCheckListener()`:获取监听器 + + * `if (null == localTransactionExecuter && null == transactionListener)`:两者都为 null 抛出异常 + + * `Validators.checkMessage(msg, this.defaultMQProducer)`:检查消息 + + * `MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true")`:**设置事务标志** + + * `sendResult = this.send(msg)`:发送消息 + + * `switch (sendResult.getSendStatus())`:**判断发送消息的结果状态** + + * `case SEND_OK`:消息发送成功 + + `msg.setTransactionId(transactionId)`:设置事务 ID 为消息的 UNIQ_KEY 属性 + + `localTransactionState = ...executeLocalTransactionBranch(msg, arg)`:**执行本地事务** + + * `case SLAVE_NOT_AVAILABLE`:其他情况都需要回滚事务 + + `localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE`:**事务状态设置为回滚** + * `this.endTransaction(sendResult, ...)`:结束事务 + * `EndTransactionRequestHeader requestHeader`:构建事务结束头对象 + * `this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway()`:向 Broker 发起事务结束的单向请求 + + + +*** + + + +##### 回查处理 + +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()` 方法处理请求 + +核心方法: + +* `asyncProcessRequest()`:处理请求 + + ```java + public CompletableFuture asyncProcessRequest(ChannelHandlerContext ctx, + RemotingCommand request) { + final SendMessageContext mqtraceContext; + switch (request.getCode()) { + // 回调消息回退 + case RequestCode.CONSUMER_SEND_MSG_BACK: + return this.asyncConsumerSendMsgBack(ctx, request); + default: + // 解析出请求头对象 + SendMessageRequestHeader requestHeader = parseRequestHeader(request); + if (requestHeader == null) { + return CompletableFuture.completedFuture(null); + } + // 创建上下文对象 + mqtraceContext = buildMsgContext(ctx, requestHeader); + // 前置处理器 + this.executeSendMessageHookBefore(ctx, request, mqtraceContext); + // 判断是否是批量消息 + if (requestHeader.isBatch()) { + return this.asyncSendBatchMessage(ctx, request, mqtraceContext, requestHeader); + } else { + return this.asyncSendMessage(ctx, request, mqtraceContext, requestHeader); + } + } + } + ``` + +* asyncSendMessage():异步处理发送消息 + + ```java + private CompletableFuture asyncSendMessage(ChannelHandlerContext ctx, RemotingCommand request, SendMessageContext mqtraceContext, SendMessageRequestHeader requestHeader) + ``` + + * `RemotingCommand response`:创建响应对象 + + * `SendMessageResponseHeader responseHeader`:获取响应头,此时为 null + + * `byte[] body = request.getBody()`:获取请求体 + + * `MessageExtBrokerInner msgInner = new MessageExtBrokerInner()`:创建 msgInner 对象,并赋值相关的属性,主题和队列 ID 都是请求头中的 + + * `String transFlag`:获取**事务属性** + + * `if (transFlag != null && Boolean.parseBoolean(transFlag))`:判断事务属性是否是 true,走事务消息的存储流程 + + * `putMessageResult = ...asyncPrepareMessage(msgInner)`:事务消息处理流程 + + ```java + public CompletableFuture asyncPutHalfMessage(MessageExtBrokerInner messageInner) { + // 调用存储模块,将修改后的 msg 存储进 Broker + return store.asyncPutMessage(parseHalfMessageInner(messageInner)); + } + ``` + + TransactionalMessageBridge#parseHalfMessageInner: + + * `MessageAccessor.putProperty(...)`:将消息的原主题和队列 ID 放入消息的属性中 + * `msgInner.setSysFlag(...)`:消息设置为非事务状态 + * `msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic())`:**消息主题设置为半消息主题** + * `msgInner.setQueueId(0)`:**队列 ID 设置为 0** + + * `else`:普通消息存储 + + + +*** + + + +##### 事务提交 + +EndTransactionProcessor 类用来处理客户端发来的提交或者回滚请求 + +* processRequest():处理请求 + + ```java + public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) + ``` + * `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 数据** + * `MessageQueue messageQueue`:新建一个消息队列,OP 队列 + * `return addRemoveTagInTransactionOp(messageExt, messageQueue)`:添加数据 + * `Message message`:创建消息 + * `writeOp(message, messageQueue)`:写入消息 From 29d649175caf40b0ec337619219737713ce46cf4 Mon Sep 17 00:00:00 2001 From: Seazean Date: Tue, 1 Feb 2022 22:07:40 +0800 Subject: [PATCH 063/122] Update Java Note --- Frame.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Frame.md b/Frame.md index cec0168..5cf9cd6 100644 --- a/Frame.md +++ b/Frame.md @@ -10314,16 +10314,3 @@ ConsumeRequest 是 ConsumeMessageOrderlyService 的内部类,是一个 Runnabl - - - -*** - - - - - -## TEST - - - From 83dba99c2261bc88b2a549fe9f90a58976b516f6 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sat, 12 Feb 2022 14:23:24 +0800 Subject: [PATCH 064/122] Update Java Note --- Tool.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Tool.md b/Tool.md index d81b3ce..77b4fef 100644 --- a/Tool.md +++ b/Tool.md @@ -2210,7 +2210,7 @@ pstree -A #查看所有进程树 -### 进程ID +### 进程 ID 进程号: @@ -2218,11 +2218,14 @@ pstree -A #查看所有进程树 * 进程号为 1 是 init 进程,是一个守护进程,在自举过程结束时由内核调用,init 进程绝不会终止,是一个普通的用户进程,但是它以超级用户特权运行 -父进程 ID 为 0 的进程通常是内核进程,它们作为系统自举过程的一部分而启动,init 进程是个例外,它的父进程是 0,但它是用户进程 +父进程 ID 为 0 的进程通常是内核进程,作为系统**自举过程**的一部分而启动,init 进程是个例外,它的父进程是 0,但它是用户进程 -主存 = RAM + BIOS 部分的 ROM +* 主存 = RAM + BIOS 部分的 ROM +* DISK:存放 OS 和 Bootloader +* BIOS:基于 I/O 处理系统 +* Bootloader:加载 OS,将 OS 放入内存 -自举程序存储在内存中 ROM(BIOS 芯片),用来加载操作系统。CPU 的程序计数器指向 ROM 中自举程序第一条指令,当计算机**通电**,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入 RAM 中,这个过程是自举过程。装入完成后 CPU 的程序计数器就被设置为 RAM 中操作系统的第一条指令所对应的位置,接下来 CPU 将开始执行操作系统的指令 +自举程序存储在内存中 ROM(BIOS 芯片),用来加载操作系统。CPU 的程序计数器指向 ROM 中自举程序第一条指令,当计算机**通电**,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入 RAM 中,这个过程是自举过程。装入完成后 CPU 的程序计数器就被设置为 RAM 中操作系统的**第一条指令**所对应的位置,接下来 CPU 将开始执行操作系统的指令 存储在 ROM 中保留很小的自举装入程序,完整功能的自举程序保存在磁盘的启动块上,启动块位于磁盘的固定位,拥有启动分区的磁盘称为启动磁盘或系统磁盘(C盘) @@ -2277,7 +2280,7 @@ pstree -A #查看所有进程树 - 得到 SIGCHLD 信号 - waitpid() 或者 wait() 调用会返回 -子进程发送的 SIGCHLD 信号包含了子进程的信息,比如进程 ID、进程状态、进程使用 CPU 的时间等;在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息,父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息 +子进程发送的 SIGCHLD 信号包含了子进程的信息,比如进程 ID、进程状态、进程使用 CPU 的时间等;在子进程退出时进程描述符不会立即释放,父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息,释放子进程的 PCB From cac33c723439189b778f4a61e81ecce5491e116f Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 13 Feb 2022 18:27:23 +0800 Subject: [PATCH 065/122] Update Java Note --- DB.md | 11 ++++++----- Java.md | 4 ++-- Prog.md | 8 ++++---- Tool.md | 21 ++++++++------------- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/DB.md b/DB.md index 61b9c13..f51e5c2 100644 --- a/DB.md +++ b/DB.md @@ -5644,7 +5644,8 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 - 查看事务提交方式 ```mysql - SELECT @@AUTOCOMMIT; -- 1 代表自动提交 0 代表手动提交 + SELECT @@AUTOCOMMIT; -- 会话,1 代表自动提交 0 代表手动提交 + SELECT @@GLOBAL.AUTOCOMMIT; -- 系统 ``` - 修改事务提交方式 @@ -5732,8 +5733,8 @@ MySQL Server 是多线程结构,包括后台线程和客户服务线程。多 * 查询数据库隔离级别 ```mysql - SELECT @@TX_ISOLATION; - SHOW VARIABLES LIKE 'tx_isolation'; + SELECT @@TX_ISOLATION; -- 会话 + SELECT @@GLOBAL.TX_ISOLATION; -- 系统 ``` * 修改数据库隔离级别 @@ -5758,7 +5759,7 @@ InnoDB 存储引擎支持事务,所以加锁分析是基于该存储引擎 MySQL 做了优化,在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引,需要在Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象,这种情况同样适用于 RR -* Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,加了写锁后其他事务就无法修改数据,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解 +* Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,**加了读锁后其他事务就无法修改数据**,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解 * Serializable 级别,读加共享锁,写加排他锁,读写互斥,使用的悲观锁的理论,实现简单,数据更加安全,但是并发能力非常差 @@ -6660,7 +6661,7 @@ 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 防止并发冲突 锁的兼容性: diff --git a/Java.md b/Java.md index b932420..91ff8a8 100644 --- a/Java.md +++ b/Java.md @@ -12677,7 +12677,7 @@ Java 语言:跨平台的语言(write once ,run anywhere) 字节码是一种二进制的类文件,是编译之后供虚拟机解释执行的二进制字节码文件,**一个 class 文件对应一个 public 类型的类或接口** -字节码内容是 **JVM 的字节码指令**,不是机器码,C、C++ 经由编译器直接生成机器码,所以 C 执行效率比 Java 高 +字节码内容是 **JVM 的字节码指令**,不是机器码,C、C++ 经由编译器直接生成机器码,所以执行效率比 Java 高 JVM 官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html @@ -13860,7 +13860,7 @@ public static int invoke(Object... args) { - 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息 -- **Java 是静态类型语言**(尽管 lambda 表达式为其增加了动态特性),js,python是动态类型语言 +- **Java 是静态类型语言**(尽管 Lambda 表达式为其增加了动态特性),JS,Python 是动态类型语言 ```java String s = "abc"; //Java diff --git a/Prog.md b/Prog.md index dc6f5e3..c09c693 100644 --- a/Prog.md +++ b/Prog.md @@ -50,8 +50,8 @@ * 信号量:信号量是一个计数器,用于多进程对共享数据的访问,解决同步相关的问题并避免竞争条件 * 共享存储:多个进程可以访问同一块内存空间,需要使用信号量用来同步对共享存储的访问 - * 管道通信:管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,pipe 文件 - * 匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信,只支持**半双工通信** + * 管道通信:管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件 pipe 文件,该文件同一时间只允许一个进程访问,所以只支持**半双工通信** + * 匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信 * 命名管道(Names Pipes):以磁盘文件的方式存在,可以实现本机任意两个进程通信,遵循 FIFO * 消息队列:内核中存储消息的链表,由消息队列标识符标识,能在不同进程之间提供**全双工通信**,对比管道: * 匿名管道存在于内存中的文件;命名管道存在于实际的磁盘介质或者文件系统;消息队列存放在内核中,只有在内核重启(操作系统重启)或者显示地删除一个消息队列时,该消息队列才被真正删除 @@ -59,9 +59,9 @@ 不同计算机之间的**进程通信**,需要通过网络,并遵守共同的协议,例如 HTTP - * 套接字:与其它通信机制不同的是,它可用于不同机器间的互相通信 + * 套接字:与其它通信机制不同的是,可用于不同机器间的互相通信 -* 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 +* 线程通信相对简单,因为线程之间共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 **Java 中的通信机制**:volatile、等待/通知机制、join 方式、InheritableThreadLocal、MappedByteBuffer diff --git a/Tool.md b/Tool.md index 77b4fef..bf5c965 100644 --- a/Tool.md +++ b/Tool.md @@ -1614,19 +1614,14 @@ grep [-abcEFGhHilLnqrsvVwxy][-A<显示列数>][-B<显示列数>][-C<显示列数 * -v:显示不包含匹配文本的所有行 * --color=auto :可以将找到的关键词部分加上颜色的显示 -**管道符 |**:表示将前一个命令处理的结果传递给后面的命令处理。 +**管道符 |**:表示将前一个命令处理的结果传递给后面的命令处理 -`grep aaaa Filename `:显示存在关键字 aaaa 的行 - -`grep -n aaaa Filename`:显示存在关键字 aaaa 的行,且显示行号 - -`grep -i aaaa Filename`:忽略大小写,显示存在关键字 aaaa 的行 - -`grep -v aaaa Filename`:显示存在关键字aaaa的所有行 - -`ps -ef | grep sshd`:查找包含 sshd 进程的进程信息 - -` ps -ef | grep -c sshd`:查找 sshd 相关的进程个数 +* `grep aaaa Filename `:显示存在关键字 aaaa 的行 +* `grep -n aaaa Filename`:显示存在关键字 aaaa 的行,且显示行号 +* `grep -i aaaa Filename`:忽略大小写,显示存在关键字 aaaa 的行 +* `grep -v aaaa Filename`:显示存在关键字aaaa的所有行 +* `ps -ef | grep sshd`:查找包含 sshd 进程的进程信息 +* ` ps -ef | grep -c sshd`:查找 sshd 相关的进程个数 @@ -2225,7 +2220,7 @@ pstree -A #查看所有进程树 * BIOS:基于 I/O 处理系统 * Bootloader:加载 OS,将 OS 放入内存 -自举程序存储在内存中 ROM(BIOS 芯片),用来加载操作系统。CPU 的程序计数器指向 ROM 中自举程序第一条指令,当计算机**通电**,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入 RAM 中,这个过程是自举过程。装入完成后 CPU 的程序计数器就被设置为 RAM 中操作系统的**第一条指令**所对应的位置,接下来 CPU 将开始执行操作系统的指令 +自举程序存储在内存中 ROM(BIOS 芯片),**用来加载操作系统**。CPU 的程序计数器指向 ROM 中自举程序第一条指令,当计算机**通电**,CPU 开始读取并执行自举程序,将操作系统(不是全部,只是启动计算机的那部分程序)装入 RAM 中,这个过程是自举过程。装入完成后 CPU 的程序计数器就被设置为 RAM 中操作系统的**第一条指令**所对应的位置,接下来 CPU 将开始执行操作系统的指令 存储在 ROM 中保留很小的自举装入程序,完整功能的自举程序保存在磁盘的启动块上,启动块位于磁盘的固定位,拥有启动分区的磁盘称为启动磁盘或系统磁盘(C盘) From e8ee01e5d103823e10507f8aa1f91aec42c1f10d Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 20 Feb 2022 22:35:52 +0800 Subject: [PATCH 066/122] Update Java Note --- DB.md | 8 +- Java.md | 468 ++++++++++++++++++++++++++------------------------------ Prog.md | 2 +- Web.md | 4 +- 4 files changed, 222 insertions(+), 260 deletions(-) diff --git a/DB.md b/DB.md index f51e5c2..bdd5d85 100644 --- a/DB.md +++ b/DB.md @@ -5757,7 +5757,7 @@ InnoDB 存储引擎支持事务,所以加锁分析是基于该存储引擎 * Read Committed 级别,增删改操作会加写锁(行锁),读操作不加锁 - MySQL 做了优化,在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引,需要在Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象,这种情况同样适用于 RR + MySQL 做了优化,在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的**加锁操作不能省略**。所以对数据量很大的表做批量修改时,如果无法使用相应的索引(全表扫描),在Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象,这种情况同样适用于 RR * Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,**加了读锁后其他事务就无法修改数据**,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解 @@ -6135,7 +6135,7 @@ RC、RR 级别下的 InnoDB 快照读区别 - 快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是**并不能完全避免幻读** - 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1去 UPDATE 该行会发现更新成功,因为 **Read View 并不能阻止事务去更新数据**,并且把这条新记录的 trx_id 给变为当前的事务 id,所以对当前事务就是可见的 + 场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1 去 UPDATE 该行会发现更新成功,因为 **Read View 并不能阻止事务去更新数据**,并且把这条新记录的 trx_id 变为当前的事务 id,所以对当前事务就是可见的 - 当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题 @@ -6790,9 +6790,9 @@ SELECT * FROM table_name WHERE ... FOR UPDATE -- 排他锁 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的组合,可以保护当前记录和前面的间隙 * 加锁遵循左开右闭原则 -* 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,20,正无穷),锁住索引 11 会对 (10,11] 加锁 +* 假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,正无穷),锁住索引 11 会对 (10,11] 加锁 -间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化 +间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的**幻读问题**,通过对间隙加锁,可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁,只能加锁一部分的间隙。 间隙锁危害:当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害 diff --git a/Java.md b/Java.md index 91ff8a8..be48f40 100644 --- a/Java.md +++ b/Java.md @@ -104,7 +104,7 @@ Java 语言提供了八种基本类型。六种数字类型(四个整数型, - char 类型是一个单一的 16 位两个字节的 Unicode 字符 - 最小值是 **`\u0000`**(即为 0) - 最大值是 **`\uffff`**(即为 65535) -- char 数据类型可以**存储任何字符** +- char 数据类型可以存储任何字符 - 例子:`char c = 'A'`,`char c = '张'` 上下转型 @@ -187,8 +187,8 @@ Java 为包装类做了一些特殊功能,具体来看特殊功能主要有: * 把字符串类型的数值转换成对应的基本数据类型的值(**重要**) - 1. Xxx.parseXxx("字符串类型的数值") → Integer.parseInt(numStr) - 2. Xxx.valueOf("字符串类型的数值") → Integer.valueOf(numStr) (推荐使用) + 1. Xxx.parseXxx("字符串类型的数值") → `Integer.parseInt(numStr)` + 2. Xxx.valueOf("字符串类型的数值") → `Integer.valueOf(numStr)` (推荐使用) ```java public class PackageClass02 { @@ -219,7 +219,35 @@ Java 为包装类做了一些特殊功能,具体来看特殊功能主要有: } ``` - + + + +*** + + + +##### 类型对比 + +* 有了基本数据类型,为什么还要引用数据类型? + + > 引用数据类型封装了数据和处理该数据的方法,比如 Integer.parseInt(String) 就是将 String 字符类型数据转换为 Integer 整型 + > + > Java 中大部分类和方法都是针对引用数据类型,包括泛型和集合 + +* 引用数据类型那么好,为什么还用基本数据类型? + + > 引用类型的对象要多储存对象头,对基本数据类型来说空间浪费率太高。逻辑上来讲,Java 只有包装类就够了,为了运行速度,需要用到基本数据类型;优先考虑运行效率的问题,所以二者同时存在是合乎情理的 + +* Java 集合不能存放基本数据类型,只存放对象的引用? + + > 不能放基本数据类型是因为不是 Object 的子类。泛型思想,如果不用泛型要写很多参数类型不同的但功能相同的函数(方法重载) + +* == + + > == 比较基本数据类型:比较的是具体的值 + > == 比较引用数据类型:比较的是对象地址值 + + *** @@ -242,8 +270,8 @@ public class PackegeClass { Integer c = 100 ; int c1 = c ; // 自动拆箱 - Integer it = Integer.valueOf(12); // 手工装箱! - // Integer it1 = new Integer(12); // 手工装箱! + Integer it = Integer.valueOf(12); // 手工装箱! + // Integer it1 = new Integer(12); // 手工装箱! Integer it2 = 12; Integer it3 = 111 ; @@ -257,6 +285,7 @@ public class PackegeClass { ```java public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) + // 【缓存池】,本质上是一个数组 return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } @@ -304,10 +333,10 @@ valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中 - Integer values between -128 and 127 - Character in the range \u0000 to \u007F (0 and 127) -在 jdk 1.8 所有的数值类缓冲池中,**Integer 的缓存池 IntegerCache 很特殊,这个缓冲池的下界是 -128,上界默认是 127**,但是上界是可调的,在启动 jvm 的时候,通过 AutoBoxCacheMax= 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界 +在 jdk 1.8 所有的数值类缓冲池中,**Integer 的缓存池 IntegerCache 很特殊,这个缓冲池的下界是 -128,上界默认是 127**,但是上界是可调的,在启动 JVM 时通过 `AutoBoxCacheMax=` 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界 ```java -Integer x = 100; //自动装箱,底层调用 Integer.valueOf(1) +Integer x = 100; // 自动装箱,底层调用 Integer.valueOf(1) Integer y = 100; System.out.println(x == y); // true @@ -335,8 +364,8 @@ System.out.println(x == y); // true,因为 y 会调用 intValue 自动拆箱 一般使用 `sc.nextInt()` 或者 `sc.nextLine()` 接受整型和字符串,然后转成需要的数据类型 -Scanner:`BufferedReader br = new BufferedReader(new InputStreamReader(System.in))` -print:`PrintStream.write()` +* Scanner:`BufferedReader br = new BufferedReader(new InputStreamReader(System.in))` +* print:`PrintStream.write()` > 使用引用数据类型的API @@ -351,31 +380,6 @@ public static void main(String[] args) { -*** - - - -#### 面试题 - -* 有了基本数据类型,为什么还要引用数据类型? - - > 引用数据类型封装了数据和处理该数据的方法,比如 Integer.parseInt(String) 就是将 String 字符类型数据转换为 Integer 整型 - > - > Java 中大部分类和方法都是针对引用数据类型,包括泛型和集合 - -* 引用数据类型那么好,为什么还用基本数据类型? - - > 引用类型的对象要多储存对象头,对基本数据类型来说空间浪费率太高。逻辑上来讲,Java 只有包装类就够了,为了运行速度,需要用到基本数据类型;优先考虑运行效率的问题,所以二者同时存在是合乎情理的 - -* Java 集合不能存放基本数据类型,只存放对象的引用? - - > 不能放基本数据类型是因为不是 Object 的子类。泛型思想,如果不用泛型要写很多参数类型不同的但功能相同的函数(方法重载) - -* == - - > == 比较基本数据类型:比较的是具体的值 - > == 比较引用数据类型:比较的是对象地址值 - **** @@ -386,12 +390,12 @@ public static void main(String[] args) { #### 初始化 -数组就是存储数据长度固定的容器,存储多个数据的**数据类型要一致**,数组也是一个对象 +数组就是存储数据长度固定的容器,存储多个数据的数据类型要一致,**数组也是一个对象** 创建数组: -* 数据类型[] 数组名:`int[] arr;` (常用) -* 数据类型 数组名[]:`int arr[];` +* 数据类型[] 数组名:`int[] arr` (常用) +* 数据类型 数组名[]:`int arr[]` 静态初始化: @@ -406,9 +410,9 @@ public static void main(String[] args) { #### 元素访问 -* **索引**:每一个存储到数组的元素,都会自动的拥有一个编号,从 **0** 开始。这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素。 +* **索引**:每一个存储到数组的元素,都会自动的拥有一个编号,从 **0** 开始。这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素。 -* **访问格式**:数组名[索引] `arr[0]` +* **访问格式**:数组名[索引],`arr[0]` * **赋值:**`arr[0] = 10` @@ -419,19 +423,19 @@ public static void main(String[] args) { #### 内存分配 -内存是计算机中的重要原件,临时存储区域,作用是运行程序。我们编写的程序是存放在硬盘中的,在硬盘中的程序是不会运行的。必须放进内存中才能运行,运行完毕后会清空内存。 Java虚拟机要运行程序,必须要对内存进行空间的分配和管理。 +内存是计算机中的重要器件,临时存储区域,作用是运行程序。我们编写的程序是存放在硬盘中,在硬盘中的程序是不会运行的,必须放进内存中才能运行,运行完毕后会清空内存。 Java 虚拟机要运行程序,必须要对内存进行空间的分配和管理。 -| 区域名称 | 作用 | -| ---------- | -------------------------------------------------------- | -| 寄存器 | 给CPU使用,和我们开发无关 | -| 本地方法栈 | JVM在使用操作系统功能的时候使用,和我们开发无关 | -| 方法区 | 存储可以运行的class文件 | -| 堆内存 | 存储对象或者数组,new来创建的,都存储在堆内存 | -| 方法栈 | 方法运行时使用的内存,比如main方法运行,进入方法栈中执行 | +| 区域名称 | 作用 | +| ---------- | ---------------------------------------------------------- | +| 寄存器 | 给 CPU 使用 | +| 本地方法栈 | JVM 在使用操作系统功能的时候使用 | +| 方法区 | 存储可以运行的 class 文件 | +| 堆内存 | 存储对象或者数组,new 来创建的,都存储在堆内存 | +| 方法栈 | 方法运行时使用的内存,比如 main 方法运行,进入方法栈中执行 | -**内存分配图**: +**内存分配图**:Java 内存分配 -* Java内存分配-一个数组内存图 +* 一个数组内存图 ![](https://gitee.com/seazean/images/raw/master/Java/数组内存分配-一个数组内存图.png) @@ -464,7 +468,7 @@ public static void main(String[] args) { } ``` - arr = null,表示变量arr将不再保存数组的内存地址,也就不允许再操作数组,因此运行的时候会抛出空指针异常。在开发中,空指针异常是不能出现的,一旦出现了,就必须要修改我们编写的代码。 + arr = null,表示变量 arr 将不再保存数组的内存地址,也就不允许再操作数组,因此运行的时候会抛出空指针异常。在开发中,空指针异常是不能出现的,一旦出现了,就必须要修改编写的代码。 解决方案:给数组一个真正的堆内存空间引用即可! @@ -482,14 +486,14 @@ public static void main(String[] args) { * 动态初始化: - 数据类型[][] 变量名 = new 数据类型[m] [n] : `int[][] arr = new int[3][3];` + 数据类型[][] 变量名 = new 数据类型[m] [n] : `int[][] arr = new int[3][3]` * m 表示这个二维数组,可以存放多少个一维数组,行 * n 表示每一个一维数组,可以存放多少个元素,列 * 静态初始化 * 数据类型[][] 变量名 = new 数据类型[][]{ {元素1, 元素2...} , {元素1, 元素2...} - * 数据类型[][] 变量名 = { {元素1, 元素2...} , {元素1, 元素2...} ...} - * `int[][] arr = {{11,22,33}, {44,55,66}};` + * 数据类型[][] 变量名 = {{元素1, 元素2...}, {元素1, 元素2...}...} + * `int[][] arr = {{11,22,33}, {44,55,66}}` 遍历: @@ -525,13 +529,13 @@ public class Test1 { ### 运算 -* i++ 与++i 的区别? +* i++ 与 ++i 的区别? i++ 表示先将 i 放在表达式中运算,然后再加 1 ++i 表示先将 i 加 1,然后再放在表达式中运算 * || 和 |,&& 和& 的区别,逻辑运算符 - **&和| 称为布尔运算符,位运算符。&&和|| 称为条件布尔运算符,也叫短路运算符**。 + **& 和| 称为布尔运算符,位运算符。&& 和 || 称为条件布尔运算符,也叫短路运算符**。 如果 && 运算符的第一个操作数是 false,就不需要考虑第二个操作数的值了,因为无论第二个操作数的值是什么,其结果都是 false;同样,如果第一个操作数是 true,|| 运算符就返回 true,无需考虑第二个操作数的值;但 & 和 | 却不是这样,它们总是要计算两个操作数。为了提高性能,**尽可能使用 && 和 || 运算符** @@ -570,14 +574,14 @@ public class Test1 { ``` * 负数: - 原码:最高位为1,其余位置和正数相同 + 原码:最高位为 1,其余位置和正数相同 反码:保证符号位不变,其余位置取反 补码:保证符号位不变,其余位置取反加 1,即反码 +1 ```java - -100原码: 10000000 00000000 00000000 01100100 //32位 - -100反码: 11111111 11111111 11111111 10011011 - -100补码: 11111111 11111111 11111111 10011100 + -100 原码: 10000000 00000000 00000000 01100100 //32位 + -100 反码: 11111111 11111111 11111111 10011011 + -100 补码: 11111111 11111111 11111111 10011100 ``` 补码 → 原码:符号位不变,其余位置取反加 1 @@ -621,13 +625,11 @@ public class Test1 { #### 可变参数 -可变参数用在形参中可以接收多个数据。 +可变参数用在形参中可以接收多个数据,在方法内部**本质上就是一个数组** -可变参数的格式:数据类型... 参数名称 +格式:数据类型... 参数名称 -可变参数的作用:传输参数非常灵活,方便。可以不传输参数、传输一个参数、或者传输一个数组。 - -可变参数在方法内部本质上就是一个数组。 +作用:传输参数非常灵活,方便,可以不传输参数、传输一个参数、或者传输一个数组。 可变参数的注意事项: @@ -667,7 +669,7 @@ public static void sum(int... nums){ 在方法内部定义的叫局部变量,局部变量不能加 static,包括 protected、private、public 这些也不能加 -原因:局部变量是保存在栈中的,而静态变量保存于方法区(JDK8 在堆中),局部变量出了方法就被栈回收了,而静态变量不会,所以在局部变量前不能加 static 关键字,静态变量是定义在类中,又叫类变量 +原因:局部变量是保存在栈中的,而静态变量保存于方法区(JDK8 在堆中),局部变量出了方法就被栈回收了,而静态变量不会,所以**在局部变量前不能加 static 关键字**,静态变量是定义在类中,又叫类变量 @@ -677,7 +679,7 @@ public static void sum(int... nums){ #### 定义调用 -定义格式 +定义格式: ```java public static 返回值类型 方法名(参数) { @@ -686,11 +688,10 @@ public static 返回值类型 方法名(参数) { } ``` -调用格式 +调用格式: ```java -数据类型 变量名 = 方法名 ( 参数 ) ; -//注意:方法的返回值通常会使用变量接收,否则该返回值将无意义 +数据类型 变量名 = 方法名 (参数) ; ``` * 方法名:调用方法时候使用的标识 @@ -700,10 +701,10 @@ public static 返回值类型 方法名(参数) { 如果方法操作完毕 -* void 类型的方法,直接调用即可,而且方法体中一般不写return +* void 类型的方法,直接调用即可,而且方法体中一般不写 return * 非 void 类型的方法,推荐用变量接收调用 -原理:每个方法在被调用执行的时候,都会进入栈内存,并且拥有自己独立的内存空间,方法内部代码调用完毕之后,会从栈内存中弹栈消失。 +原理:每个方法在被调用执行的时候,都会进入栈内存,并且拥有自己独立的内存空间,方法内部代码调用完毕之后,会从栈内存中弹栈消失 @@ -785,9 +786,9 @@ public class MethodDemo { 重载的方法在编译过程中即可完成识别,方法调用时 Java 编译器会根据所传入参数的声明类型(注意与实际类型区分)来选取重载方法。选取的过程共分为三个阶段: -* 在不考虑对基本类型自动装拆箱 (auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法 -* 如果第 1 个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法 -* 如果第 2 个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法 +* 一阶段:在不考虑对基本类型自动装拆箱 (auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法 +* 二阶段:如果第一阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法 +* 三阶段:如果第二阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法 如果 Java 编译器在同一个阶段中找到了多个适配的方法,那么会选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系,**一般会选择形参为参数类型的子类的方法,因为子类时更具体的实现**: @@ -796,7 +797,7 @@ public class MethodDemo { void invoke(Object obj, Object... args) { ... } void invoke(String s, Object obj, Object... args) { ... } - invoke(null, 1); // 调用第二个invoke方法,选取的第二阶段 + invoke(null, 1); // 调用第二个invoke方法,选取的第二阶段 invoke(null, 1, 2); // 调用第二个invoke方法,匹配第一个和第二个,但String是Object的子类 invoke(null, new Object[]{1}); // 只有手动绕开可变长参数的语法糖,才能调用第一个invoke方法 @@ -932,7 +933,7 @@ Java 的参数是以**值传递**的形式传入方法中 // 获取索引 Season s = Season.SPRING; System.out.println(s); //SPRING - System.out.println(s.ordinal()); // 0,代表索引,summer 就是 1 + System.out.println(s.ordinal()); // 0,该值代表索引,summer 就是 1 s.s.doSomething(); // 获取全部枚举 Season[] ss = Season.values(); @@ -988,15 +989,15 @@ Debug 是供程序员使用的程序调试工具,它可以用于查看程序 ### 概述 -**Java是一种面向对象的高级编程语言。** +**Java 是一种面向对象的高级编程语言。** **三大特征:封装,继承,多态** 面向对象最重要的两个概念:类和对象 -* 类:相同事物共同特征的描述。类只是学术上的一个概念并非真实存在的,只能描述一类事物 -* 对象:是真实存在的实例, 实例==对象,**对象是类的实例化** -* 结论:有了类和对象就可以描述万千世界所有的事物。 必须先有类才能有对象 +* 类:相同事物共同特征的描述,类只是学术上的一个概念并非真实存在的,只能描述一类事物 +* 对象:是真实存在的实例, 实例 == 对象,**对象是类的实例化** +* 结论:有了类和对象就可以描述万千世界所有的事物,必须先有类才能有对象 @@ -1017,7 +1018,7 @@ Debug 是供程序员使用的程序调试工具,它可以用于查看程序 1. 类名的首字母建议大写,满足驼峰模式,比如 StudentNameCode 2. 一个 Java 代码中可以定义多个类,按照规范一个 Java 文件一个类 -3. 一个 Java 代码文件中,只能有一个类是 public 修饰,**public修饰的类名必须成为当前Java代码的文件名称** +3. 一个 Java 代码文件中,只能有一个类是 public 修饰,**public 修饰的类名必须成为当前 Java 代码的文件名称** ```java 类中的成分:有且仅有五大成分 @@ -1086,6 +1087,7 @@ public class ClassDemo { ### 封装 封装的哲学思维:合理隐藏,合理暴露 + 封装最初的目的:提高代码的安全性和复用性,组件化 封装的步骤: @@ -1120,9 +1122,7 @@ this 关键字的作用: #### 基本介绍 -Java 是通过成员变量是否有 static 修饰来区分是类的还是属于对象的。 - -static 静态修饰的成员(方法和成员变量)属于类本身的。 +Java 是通过成员变量是否有 static 修饰来区分是类的还是属于对象的 按照有无 static 修饰,成员变量和方法可以分为: @@ -1140,7 +1140,7 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 -#### static用法 +#### static 用法 成员变量的访问语法: @@ -1175,8 +1175,8 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 inAddr(); // b.对象.实例方法 // Student.eat(); // 报错了! - Student zbj = new Student(); - zbj.eat(); + Student sea = new Student(); + sea.eat(); } } ``` @@ -1199,14 +1199,14 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 访问问题: -* 实例方法是否可以直接访问实例成员变量?可以的,因为它们都属于对象 -* 实例方法是否可以直接访问静态成员变量?可以的,静态成员变量可以被共享访问 -* 实例方法是否可以直接访问实例方法? 可以的,实例方法和实例方法都属于对象 -* 实例方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问 -* 静态方法是否可以直接访问实例变量? 不可以的,实例变量必须用对象访问!! -* 静态方法是否可以直接访问静态变量? 可以的,静态成员变量可以被共享访问。 -* 静态方法是否可以直接访问实例方法? 不可以的,实例方法必须用对象访问!! -* 静态方法是否可以直接访问静态方法?可以的,静态方法可以被共享访问!! +* 实例方法是否可以直接访问实例成员变量?可以,因为它们都属于对象 +* 实例方法是否可以直接访问静态成员变量?可以,静态成员变量可以被共享访问 +* 实例方法是否可以直接访问实例方法? 可以,实例方法和实例方法都属于对象 +* 实例方法是否可以直接访问静态方法?可以,静态方法可以被共享访问 +* 静态方法是否可以直接访问实例变量? 不可以,实例变量必须用对象访问!! +* 静态方法是否可以直接访问静态变量? 可以,静态成员变量可以被共享访问。 +* 静态方法是否可以直接访问实例方法? 不可以,实例方法必须用对象访问!! +* 静态方法是否可以直接访问静态方法?可以,静态方法可以被共享访问!! @@ -1220,8 +1220,8 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 继承是 Java 中一般到特殊的关系,是一种子类到父类的关系 -* 被继承的类称为:父类/超类。 -* 继承父类的类称为:子类。 +* 被继承的类称为:父类/超类 +* 继承父类的类称为:子类 继承的作用: @@ -1235,7 +1235,7 @@ static 静态修饰的成员(方法和成员变量)属于类本身的。 2. **单继承**:一个类只能继承一个直接父类 3. 多层继承:一个类可以间接继承多个父类(家谱) 4. 一个类可以有多个子类 -5. 一个类要么默认继承了 Object 类,要么间接继承了 Object 类,Object 类是 Java 中的祖宗类 +5. 一个类要么默认继承了 Object 类,要么间接继承了 Object 类,**Object 类是 Java 中的祖宗类** 继承的格式: @@ -1356,7 +1356,7 @@ class Animal{ -#### 面试问题 +#### 常见问题 * 为什么子类构造器会先调用父类构造器? @@ -1365,17 +1365,18 @@ class Animal{ 3. 参考 JVM → 类加载 → 对象创建 ```java - class Animal{ - public Animal(){ + class Animal { + public Animal() { System.out.println("==父类Animal的无参数构造器=="); } } - class Tiger extends Animal{ - public Tiger(){ + + class Tiger extends Animal { + public Tiger() { super(); // 默认存在的,根据参数去匹配调用父类的构造器。 System.out.println("==子类Tiger的无参数构造器=="); } - public Tiger(String name){ + public Tiger(String name) { //super(); 默认存在的,根据参数去匹配调用父类的构造器。 System.out.println("==子类Tiger的有参数构造器=="); } @@ -1421,8 +1422,8 @@ class Animal{ 总结与拓展: -* this 代表了当前对象的引用(继承中指代子类对象):this.子类成员变量、this.子类成员方法、**this(...)**可以根据参数匹配访问本类其他构造器。 -* super 代表了父类对象的引用(继承中指代了父类对象空间):super.父类成员变量、super.父类的成员方法、super(...)可以根据参数匹配访问父类的构造器 +* this 代表了当前对象的引用(继承中指代子类对象):this.子类成员变量、this.子类成员方法、**this(...)** 可以根据参数匹配访问本类其他构造器 +* super 代表了父类对象的引用(继承中指代了父类对象空间):super.父类成员变量、super.父类的成员方法、super(...) 可以根据参数匹配访问父类的构造器 **注意:** @@ -1461,7 +1462,7 @@ class Student{ this.age = age; this.schoolName = schoolName; } -// .......get + set + // .......get + set } ``` @@ -1481,7 +1482,7 @@ final 用于修饰:类,方法,变量 * final 可以修饰方法,方法就不能被重写 * final 修饰变量总规则:变量有且仅能被赋值一次 -**面试题**:final 和 abstract 的关系是互斥关系,不能同时修饰类或者同时修饰方法! +final 和 abstract 的关系是**互斥关系**,不能同时修饰类或者同时修饰方法 @@ -1505,7 +1506,7 @@ final 修饰静态成员变量可以在哪些地方赋值: ```java public class FinalDemo { -//常量:public static final修饰,名称字母全部大写,下划线连接。 + //常量:public static final修饰,名称字母全部大写,下划线连接。 public static final String SCHOOL_NAME = "张三" ; public static final String SCHOOL_NAME1; @@ -1565,9 +1566,9 @@ public class FinalDemo { > 父类知道子类要完成某个功能,但是每个子类实现情况不一样。 -抽象方法:没有方法体,只有方法签名,必须用**abstract**修饰的方法就是抽象方法 +抽象方法:没有方法体,只有方法签名,必须用 abstract 修饰的方法就是抽象方法 -抽象类:拥有抽象方法的类必须定义成抽象类,必须用**abstract**修饰,抽象类是为了被继承 +抽象类:拥有抽象方法的类必须定义成抽象类,必须用 abstract 修饰,**抽象类是为了被继承** 一个类继承抽象类,**必须重写抽象类的全部抽象方法**,否则这个类必须定义成抽象类,因为拥有抽象方法的类必须定义成抽象类 @@ -1597,12 +1598,11 @@ abstract class Animal{ -#### 面试问题 +#### 常见问题 一、抽象类是否有构造器,是否可以创建对象? -答:抽象类作为类一定有构造器,而且必须有构造器,提供给子类继承后调用父类构造器使用的 -* 抽象类有构造器,但是抽象类不能创建对象,类的其他成分它都具备 +* 抽象类有构造器,但是抽象类不能创建对象,类的其他成分它都具备,构造器提供给子类继承后调用父类构造器使用 * 抽象类中存在抽象方法,但不能执行,**抽象类中也可没有抽象方法** > 抽象在学术上本身意味着不能实例化 @@ -1678,9 +1678,9 @@ abstract class Template{ #### 基本介绍 -接口,是 Java 语言中一种引用类型,是方法的集合。 +接口是 Java 语言中一种引用类型,是方法的集合。 -接口是更加彻底的抽象,接口中只有抽象方法和常量,没有其他成分,jdk1.8 前 +接口是更加彻底的抽象,接口中只有抽象方法和常量,没有其他成分 ```java 修饰符 interface 接口名称{ @@ -1691,11 +1691,11 @@ abstract class Template{ } ``` -* 抽象方法:接口中的抽象方法默认会加上public abstract修饰,所以可以省略不写 +* 抽象方法:接口中的抽象方法默认会加上 public abstract 修饰,所以可以省略不写 * 静态方法:静态方法必须有方法体 -* 常量:常量是public static final修饰的成员变量,仅能被赋值一次,值不能改变。常量的名称规范上要求全部大写,多个单词下划线连接。public static final可以省略不写。 +* 常量:是 public static final 修饰的成员变量,仅能被赋值一次,值不能改变。常量的名称规范上要求全部大写,多个单词下划线连接,public static final 可以省略不写 ```java public interface InterfaceDemo{ @@ -1717,14 +1717,11 @@ abstract class Template{ #### 实现接口 -作用:**接口是用来被类实现的。** +**接口是用来被类实现的。** -类与类是继承关系:一个类只能直接继承一个父类,单继承 -类与接口是实现关系:一个类可以实现多个接口,多实现,接口不能继承类 -接口与接口继承关系:**多继承** - ->子类 继承 父类 ->实现类 实现 接口 +* 类与类是继承关系:一个类只能直接继承一个父类,单继承 +* 类与接口是实现关系:一个类可以实现多个接口,多实现,接口不能继承类 +* 接口与接口继承关系:**多继承** ```java 修饰符 class 实现类名称 implements 接口1,接口2,接口3,....{ @@ -1741,9 +1738,9 @@ abstract class Template{ 2. 当一个类实现多个接口时,多个接口中存在同名的默认方法,实现类必须重写这个方法 -3. 当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类**就近选择执行父类**的成员方法 +3. 当一个类既继承一个父类,又实现若干个接口时,父类中成员方法与接口中默认方法重名,子类**就近选择执行父类**的成员方法 -4. 接口中,没有构造器,**不能创建对象**,接口是更彻底的抽象,连构造器都没有,自然不能创建对象!! +4. 接口中,没有构造器,**不能创建对象**,接口是更彻底的抽象,连构造器都没有,自然不能创建对象 ```java public class InterfaceDemo { @@ -1783,10 +1780,12 @@ jdk1.8 以后新增的功能: * 默认方法(就是普通实例方法) * 必须用 default 修饰,默认会 public 修饰 * 必须用接口的实现类的对象来调用 + * 必须有默认实现 * 静态方法 * 默认会 public 修饰 * 接口的静态方法必须用接口的类名本身来调用 * 调用格式:ClassName.method() + * 必须有默认实现 * 私有方法:JDK 1.9 才开始有的,只能在**本类中**被其他的默认方法或者私有方法访问 ```java @@ -1846,7 +1845,7 @@ interface InterfaceJDK8{ | 实现 | 子类使用 **extends** 关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 | 子类使用关键字 **implements** 来实现接口。它需要提供接口中所有声明的方法的实现 | | 构造器 | 抽象类可以有构造器 | 接口不能有构造器 | | 与正常Java类的区别 | 除了不能实例化抽象类之外,和普通 Java 类没有任何区别 | 接口是完全不同的类型 | -| 访问修饰符 | 抽象方法有 **public**、**protected** 和 **default** 这些修饰符 | 接口方法默认修饰符是 **public**,别的修饰符需要有方法体 | +| 访问修饰符 | 抽象方法有 **public**、**protected** 和 **default** 这些修饰符 | 接口默认修饰符是 **public**,别的修饰符需要有方法体 | | main方法 | 抽象方法可以有 main 方法并且我们可以运行它 | jdk8 以前接口没有 main 方法,不能运行;jdk8 以后接口可以有 default 和 static 方法,可以运行 main 方法 | | 多继承 | 抽象方法可以继承一个类和实现多个接口 | 接口可以继承一个或多个其它接口,接口不可继承类 | | 速度 | 比接口速度要快 | 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法 | @@ -1928,21 +1927,22 @@ class Animal{ #### 上下转型 >基本数据类型的转换: -> 1.小范围类型的变量或者值可以直接赋值给大范围类型的变量。 -> 2.大范围类型的变量或者值必须强制类型转换给小范围类型的变量。 +> +>1. 小范围类型的变量或者值可以直接赋值给大范围类型的变量 +>2. 大范围类型的变量或者值必须强制类型转换给小范围类型的变量 -引用数据类型的**自动**类型转换语法:子类类型的对象或者变量可以自动类型转换赋值给父类类型的变量。 +引用数据类型的**自动**类型转换语法:子类类型的对象或者变量可以自动类型转换赋值给父类类型的变量 **父类引用指向子类对象** -- **向上转型(upcasting)**:通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换 -- **向下转型(downcasting)**:通过父类对象(大范围)实例化子类对象(小范围),这种属于强制转换 +- **向上转型 (upcasting)**:通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换 +- **向下转型 (downcasting)**:通过父类对象(大范围)实例化子类对象(小范围),这种属于强制转换 ```java public class PolymorphicDemo { public static void main(String[] args){ - Animal a = new Cat();//向上转型 - Cat c = (Cat)a;//向下转型 + Animal a = new Cat(); // 向上转型 + Cat c = (Cat)a; // 向下转型 } } class Animal{} @@ -2007,7 +2007,7 @@ class Animal{} #### 静态内部类 -定义:有static修饰,属于外部类本身,会加载一次 +定义:有 static 修饰,属于外部类本身,会加载一次 静态内部类中的成分研究: @@ -2017,7 +2017,7 @@ class Animal{} 静态内部类的访问格式:外部类名称.内部类名称 -静态内部类创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类名称.内部类构造器; +静态内部类创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类名称.内部类构造器 静态内部类的访问拓展: @@ -2050,22 +2050,20 @@ static class Outter{ #### 实例内部类 -定义:无static修饰的内部类,属于外部类的每个对象,跟着外部类对象一起加载。 +定义:无 static 修饰的内部类,属于外部类的每个对象,跟着外部类对象一起加载。 实例内部类的成分特点:实例内部类中不能定义静态成员,其他都可以定义 实例内部类的访问格式:外部类名称.内部类名称 -创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类构造器.new 内部构造器; +创建对象的格式:外部类名称.内部类名称 对象名称 = new 外部类构造器.new 内部构造器 -* `Outter.Inner in = new Outter().new Inner();` +* `Outter.Inner in = new Outter().new Inner()` -拓展:**实例内部类可以访问外部类的全部成员** +**实例内部类可以访问外部类的全部成员** -> * 实例内部类中是否可以直接访问外部类的静态成员? -> 可以,外部类的静态成员可以被共享访问! -> * 实例内部类中是否可以访问外部类的实例成员? -> 可以,实例内部类属于外部类对象,可以直接访问外部类对象的实例成员! +* 实例内部类中可以直接访问外部类的静态成员,外部类的静态成员可以被共享访问 +* 实例内部类中可以访问外部类的实例成员,实例内部类属于外部类对象,可以直接访问外部类对象的实例成员 @@ -2075,7 +2073,7 @@ static class Outter{ #### 局部内部类 -局部内部类:定义在方法中,在构造器中,代码块中,for循环中定义的内部类。 +局部内部类:定义在方法中,在构造器中,代码块中,for 循环中定义的内部类 局部内部类中的成分特点:只能定义实例成员,不能定义静态成员 @@ -2101,7 +2099,6 @@ public class InnerClass{ #### 匿名内部类 匿名内部类:没有名字的局部内部类 -作用:简化代码,是开发中常用的形式 匿名内部类的格式: @@ -2114,7 +2111,7 @@ new 类名|抽象类|接口(形参){ * 匿名内部类不能定义静态成员 * 匿名内部类一旦写出来,就会立即创建一个匿名内部类的对象返回 -* **匿名内部类的对象的类型相当于是当前new的那个的类型的子类类型** +* **匿名内部类的对象的类型相当于是当前 new 的那个的类型的子类类型** * 匿名内部类引用局部变量必须是**常量**,底层创建为内部类的成员变量(原因:JVM → 运行机制 → 代码优化) ```java @@ -2149,7 +2146,7 @@ abstract class Animal{ ### 权限符 权限修饰符:有四种**(private -> 缺省 -> protected - > public )** -可以修饰成员变量,修饰方法,修饰构造器,内部类,不同修饰符修饰的成员能够被访问的权限将受到限制! +可以修饰成员变量,修饰方法,修饰构造器,内部类,不同修饰符修饰的成员能够被访问的权限将受到限制 | 四种修饰符访问权限 | private | 缺省 | protected | public | | ------------------ | :-----: | :--: | :-------: | :----: | @@ -2184,12 +2181,12 @@ static { ``` * 静态代码块特点: - * 必须有static修饰 + * 必须有 static 修饰 * 会与类一起优先加载,且自动触发执行一次 * 只能访问静态资源 * 静态代码块作用: * 可以在执行类的方法等操作之前先在静态代码块中进行静态资源的初始化 - * **先执行静态代码块,在执行main函数里的操作** + * **先执行静态代码块,在执行 main 函数里的操作** ```java public class CodeDemo { @@ -2234,7 +2231,7 @@ main方法被执行 ``` * 实例代码块的特点: - * 无static修饰,属于对象 + * 无 static 修饰,属于对象 * 会与类的对象一起加载,每次创建类的对象的时候,实例代码块都会被加载且自动触发执行一次 * 实例代码块的代码在底层实际上是提取到每个构造器中去执行的 @@ -2305,7 +2302,7 @@ public boolean equals(Object o) { **面试题**:== 和 equals 的区别 * == 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的**地址**是否相同,即是否是指相同一个对象,比较的是真正意义上的指针操作 -* Object 类中的方法,默认比较两个对象的引用,重写 equals 方法比较的是两个对象的**内容**是否相等,所有的类都是继承自 java.lang.Object 类,所以适用于所有对象 +* Object 类中的方法,**默认比较两个对象的引用**,重写 equals 方法比较的是两个对象的**内容**是否相等,所有的类都是继承自 java.lang.Object 类,所以适用于所有对象 hashCode 的作用: @@ -2331,7 +2328,7 @@ Object 的 clone() 是 protected 方法,一个类不显式去重写 clone(), * 深拷贝 (deepCopy):对基本数据类型进行值传递,对引用数据类型是一个整个独立的对象拷贝,会拷贝所有的属性并指向的动态分配的内存,简而言之就是把所有属性复制到一个新的内存,增加一个指针指向新内存。所以使用深拷贝的情况下,释放内存的时候不会出现使用浅拷贝时释放同一块内存的错误 -Cloneable 接口是一个标识性接口,即该接口不包含任何方法(包括clone()),但是如果一个类想合法的进行克隆,那么就必须实现这个接口,在使用 clone() 方法时,若该类未实现 Cloneable 接口,则抛出异常 +Cloneable 接口是一个标识性接口,即该接口不包含任何方法(包括 clone),但是如果一个类想合法的进行克隆,那么就必须实现这个接口,在使用 clone() 方法时,若该类未实现 Cloneable 接口,则抛出异常 * Clone & Copy:`Student s = new Student` @@ -2343,7 +2340,7 @@ Cloneable 接口是一个标识性接口,即该接口不包含任何方法( 浅克隆:Object 中的 clone() 方法在对某个对象克隆时对其仅仅是简单地执行域对域的 copy - * 对基本数据类型和包装类的克隆是没有问题的。String、Integer 等包装类型在内存中是不可以被改变的对象,所以在使用克隆时可以视为基本类型,只需浅克隆引用即可 + * 对基本数据类型和包装类的克隆是没有问题的。String、Integer 等包装类型在内存中是**不可以被改变的对象**,所以在使用克隆时可以视为基本类型,只需浅克隆引用即可 * 如果对一个引用类型进行克隆时只是克隆了它的引用,和原始对象共享对象成员变量 ![](https://gitee.com/seazean/images/raw/master/Java/Object浅克隆.jpg) @@ -2376,12 +2373,12 @@ SDP → 创建型 → 原型模式 ### Objects -Objects 类与 Object 是继承关系。 +Objects 类与 Object 是继承关系 -Objects的方法: +Objects 的方法: -* `public static boolean equals(Object a, Object b)` : 比较两个对象是否相同。 - 底层进行非空判断,从而可以避免空指针异常,更安全!!推荐使用!! +* `public static boolean equals(Object a, Object b)`:比较两个对象是否相同。 + 底层进行非空判断,从而可以避免空指针异常,更安全,推荐使用! ```java public static boolean equals(Object a, Object b) { @@ -2389,11 +2386,11 @@ Objects的方法: } ``` -* `public static boolean isNull(Object obj)` : 判断变量是否为null ,为null返回true ,反之! +* `public static boolean isNull(Object obj)`:判断变量是否为 null ,为 null 返回 true -* `public static String toString(对象)` : 返回参数中对象的字符串表示形式 +* `public static String toString(对象)`:返回参数中对象的字符串表示形式 -* `public static String toString(对象, 默认字符串)` : 返回对象的字符串表示形式。 +* `public static String toString(对象, 默认字符串)`:返回对象的字符串表示形式 ```java public class ObjectsDemo { @@ -2464,7 +2461,7 @@ s = s + "cd"; //s = abccd 新对象 * `public char[] toCharArray()`:将字符串拆分为字符数组后返回 * `public boolean startsWith(String prefix)`:测试此字符串是否以指定的前缀开头 * `public int indexOf(String str)`:返回指定子字符串第一次出现的字符串内的索引,没有返回 -1 -* `public int lastIndexOf(String str)`:返回字符串最后一次出现str的索引,没有返回 -1 +* `public int lastIndexOf(String str)`:返回字符串最后一次出现 str 的索引,没有返回 -1 * `public String substring(int beginIndex)`:返回子字符串,以原字符串指定索引处到结尾 * `public String substring(int i, int j)`:指定索引处扩展到 j - 1 的位置,字符串长度为 j - i * `public String toLowerCase()`:将此 String 所有字符转换为小写,使用默认语言环境的规则 @@ -2493,7 +2490,7 @@ s.replace("-","");//12378 直接赋值:`String s = "abc"` 直接赋值的方式创建字符串对象,内容就是 abc - 通过构造方法创建:通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同,**返回堆内存中对象的引用** -- 直接赋值方式创建:以“ ”方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会**在 String Pool 中创建一个字符串对象**,并在字符串池中维护 +- 直接赋值方式创建:以 `" "` 方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会**在 String Pool 中创建一个字符串对象**,并在字符串池中维护 `String str = new String("abc")` 创建字符串对象: @@ -2574,7 +2571,7 @@ public class Demo { System.out.println(s3 == s5); // true String x2 = new String("c") + new String("d"); // new String("cd") - // 虽然 new,但是在字符串常量池没有 cd 对象,toString() 方法 + // 虽然 new,但是在字符串常量池没有 cd 对象,因为 toString() 方法 x2.intern(); String x1 = "cd"; @@ -2612,7 +2609,7 @@ public static void main(String[] args) { String s2 = s.intern(); //jdk6:串池中创建一个字符串"ab" - //jdk8:串池中没有创建字符串"ab",而是创建一个引用指向new String("ab"),将此引用返回 + //jdk8:串池中没有创建字符串"ab",而是创建一个引用指向 new String("ab"),将此引用返回 System.out.println(s2 == "ab");//jdk6:true jdk8:true System.out.println(s == "ab");//jdk6:false jdk8:true @@ -2641,7 +2638,7 @@ public static void main(String[] args) { } ``` -* Version类初始化时需要对静态常量字段初始化,被 launcher_name 静态常量字段所引用的"java"字符串字面量就被放入的字符串常量池: +* Version 类初始化时需要对静态常量字段初始化,被 launcher_name 静态常量字段所引用的 `"java"` 字符串字面量就被放入的字符串常量池: ```java package sun.misc; @@ -2664,15 +2661,15 @@ public static void main(String[] args) { ##### 内存位置 -Java 7之前,String Pool 被放在运行时常量池中,它属于永久代;Java 7以后,String Pool 被移到堆中,这是因为永久代的空间有限,在大量使用字符串的场景下会导致OutOfMemoryError 错误 +Java 7 之前,String Pool 被放在运行时常量池中,属于永久代;Java 7以后,String Pool 被移到堆中,这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误 演示 StringTable 位置: -* `-Xmx10m`设置堆内存10m +* `-Xmx10m` 设置堆内存 10m -* 在jdk8下设置: `-Xmx10m -XX:-UseGCOverheadLimit`(运行参数在Run Configurations VM options) +* 在 JDK8 下设置: `-Xmx10m -XX:-UseGCOverheadLimit`(运行参数在 Run Configurations VM options) -* 在jdk6下设置: `-XX:MaxPermSize=10m` +* 在 JDK6 下设置: `-XX:MaxPermSize=10m` ```java public static void main(String[] args) throws InterruptedException { @@ -2824,7 +2821,7 @@ public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { ### Arrays -Array 的工具类 +Array 的工具类 Arrays 常用API: @@ -3755,7 +3752,7 @@ public class RegexDemo { 压栈 == 入栈、弹栈 == 出栈 场景:手枪的弹夹 -* 数组:数组是内存中的连续存储区域,分成若干等分的小区域(每个区域大小是一样的)。元素存在索引 +* 数组:数组是内存中的连续存储区域,分成若干等分的小区域(每个区域大小是一样的)元素存在索引 特点:**查询元素快**(根据索引快速计算出元素的地址,然后立即去定位) **增删元素慢**(创建新数组,迁移元素) @@ -3765,12 +3762,11 @@ public class RegexDemo { * 树: * 二叉树:binary tree 永远只有一个根节点,是每个结点不超过2个节点的树(tree) - 特点:二叉排序树:小的左边,大的右边,但是可能树很高,性能变差 - 为了做排序和搜索会进行左旋和右旋实现平衡查找二叉树,让树的高度差不大于1 - - * 红黑树(基于红黑规则实现自平衡的排序二叉树):树保证到了很矮小,但是又排好序,性能最高的树 - - 特点:**红黑树的增删查改性能都好** + 特点:二叉排序树:小的左边,大的右边,但是可能树很高,性能变差,为了做排序和搜索会进行左旋和右旋实现平衡查找二叉树,让树的高度差不大于1 + +* 红黑树(基于红黑规则实现自平衡的排序二叉树):树保证到了很矮小,但是又排好序,性能最高的树 + + 特点:**红黑树的增删查改性能都好** 各数据结构时间复杂度对比: @@ -3790,7 +3786,7 @@ public class RegexDemo { #### 概述 -Java 中集合的代表是Collection,Collection 集合是 Java 中集合的祖宗类 +Java 中集合的代表是 Collection,Collection 集合是 Java 中集合的祖宗类 Collection 集合底层为数组:`[value1, value2, ....]` @@ -3979,8 +3975,6 @@ public static void main(String[] args){ } ``` -![ArrayList源码分析](https://gitee.com/seazean/images/raw/master/Java/ArrayList添加元素源码解析.png) - *** @@ -3997,8 +3991,8 @@ public class ArrayList extends AbstractList ``` - `RandomAccess` 是一个标志接口,表明实现这个这个接口的 List 集合是支持**快速随机访问**的。在 `ArrayList` 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。 -- `ArrayList` 实现了 `Cloneable` 接口 ,即覆盖了函数`clone()`,能被克隆 -- `ArrayList` 实现了 `Serializable `接口,这意味着`ArrayList`支持序列化,能通过序列化去传输 +- `ArrayList` 实现了 `Cloneable` 接口 ,即覆盖了函数 `clone()`,能被克隆 +- `ArrayList` 实现了 `Serializable ` 接口,这意味着 `ArrayList` 支持序列化,能通过序列化去传输 核心方法: @@ -4082,8 +4076,8 @@ public class ArrayList extends AbstractList MAX_ARRAY_SIZE:要分配的数组的最大大小,分配更大的**可能**会导致 - * OutOfMemoryError:Requested array size exceeds VM limit(请求的数组大小超出VM限制) - * OutOfMemoryError: Java heap space(堆区内存不足,可以通过设置JVM参数 -Xmx 来调节) + * OutOfMemoryError:Requested array size exceeds VM limit(请求的数组大小超出 VM 限制) + * OutOfMemoryError: Java heap space(堆区内存不足,可以通过设置 JVM 参数 -Xmx 来调节) * 删除元素:需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,在旧数组上操作,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的, @@ -4121,7 +4115,7 @@ public class ArrayList extends AbstractList } ``` -* **Fail-Fast**:快速失败,modCount 用来记录 ArrayList **结构发生变化**的次数,结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化 +* **Fail-Fast**:快速失败,modCount 用来记录 ArrayList **结构发生变化**的次数,结构发生变化是指添加或者删除至少一个元素的操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化 在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,改变了抛出 ConcurrentModificationException 异常 @@ -4186,7 +4180,7 @@ public class ArrayList extends AbstractList 2. Vector 每次扩容请求其大小的 2 倍(也可以通过构造函数设置增长的容量),而 ArrayList 是 1.5 倍 -3. 底层都是 `Object[]`数组存储 +3. 底层都是 `Object[]` 数组存储 @@ -4214,7 +4208,7 @@ LinkedList 除了拥有 List 集合的全部功能还多了很多操作首尾元 * `public void push(E e)`:将元素推入此列表所表示的堆栈 * `public int indexOf(Object o)`:返回此列表中指定元素的第一次出现的索引,如果不包含返回 -1 * `public int lastIndexOf(Object o)`:从尾遍历找 -* ` public boolean remove(Object o)`:一次只删除一个匹配的对象,如果删除了匹配对象返回true +* ` public boolean remove(Object o)`:一次只删除一个匹配的对象,如果删除了匹配对象返回 true * `public E remove(int index)`:删除指定位置的元素 ```java @@ -4247,9 +4241,9 @@ public class ListDemo { } ``` -![](https://gitee.com/seazean/images/raw/master/Java/LinkedList添加元素源码解析.png) +*** @@ -4336,7 +4330,7 @@ Set 系列集合:添加的元素是无序,不重复,无索引的 * LinkedHashSet:添加的元素是有序,不重复,无索引的 * TreeSet:不重复,无索引,按照大小默认升序排序 -**面试问题**:没有索引,不能使用普通 for 循环遍历 +**注意**:没有索引,不能使用普通 for 循环遍历 @@ -4355,7 +4349,7 @@ Set 系列集合:添加的元素是无序,不重复,无索引的 - 哈希值的特点 - 同一个对象多次调用 hashCode() 方法返回的哈希值是相同的 - - 默认情况下,不同对象的哈希值是不同的。而重写 hashCode() 方法,可以实现让不同对象的哈希值相同 + - 默认情况下,不同对象的哈希值是不同的,而重写 hashCode() 方法,可以实现让不同对象的哈希值相同 **HashSet 底层就是基于 HashMap 实现,值是 PRESENT = new Object()** @@ -4382,16 +4376,17 @@ Set 集合添加的元素是无序,不重复的。 不重复 重复了 ``` -* Set系列集合元素无序的根本原因 +* Set 系列集合元素无序的根本原因 - Set系列集合添加元素无序的根本原因是因为**底层采用了哈希表存储元素**。 - JDK 1.8 之前:哈希表 = 数组(初始容量16) + 链表 + (哈希算法) - JDK 1.8 之后:哈希表 = 数组(初始容量16) + 链表 + 红黑树 + (哈希算法) - 当链表长度超过阈值8且当前数组的长度 > 64时,将链表转换为红黑树,减少了查找时间 - 当链表长度超过阈值8且当前数组的长度 < 64时,扩容 + Set 系列集合添加元素无序的根本原因是因为**底层采用了哈希表存储元素**。 + + * JDK 1.8 之前:哈希表 = 数组(初始容量16) + 链表 + (哈希算法) + * JDK 1.8 之后:哈希表 = 数组(初始容量16) + 链表 + 红黑树 + (哈希算法) + * 当链表长度超过阈值 8 且当前数组的长度 > 64时,将链表转换为红黑树,减少了查找时间 + * 当链表长度超过阈值 8 且当前数组的长度 < 64时,扩容 ![](https://gitee.com/seazean/images/raw/master/Java/HashSet底层结构哈希表.png) - + 每个元素的 hashcode() 的值进行响应的算法运算,计算出的值相同的存入一个数组块中,以链表的形式存储,如果链表长度超过8就采取红黑树存储,所以输出的元素是无序的。 * 如何设置只要对象内容一样,就希望集合认为它们重复了:**重写 hashCode 和 equals 方法** @@ -4426,9 +4421,9 @@ TreeSet 集合自排序的方式: * 直接为**对象的类**实现比较器规则接口 Comparable,重写比较方法: - 方法:`public int compareTo(Employee o): this 是比较者, o 是被比较者` + 方法:`public int compareTo(Employee o): this 是比较者, o 是被比较者` - * 比较者大于被比较者,返回正数(升序) + * 比较者大于被比较者,返回正数 * 比较者小于被比较者,返回负数 * 比较者等于被比较者,返回 0 @@ -4436,7 +4431,7 @@ TreeSet 集合自排序的方式: 方法:`public int compare(Employee o1, Employee o2): o1 比较者, o2 被比较者` - * 比较者大于被比较者,返回正数(升序) + * 比较者大于被比较者,返回正数 * 比较者小于被比较者,返回负数 * 比较者等于被比较者,返回 0 @@ -4475,7 +4470,7 @@ public class Student implements Comparable{ } ``` -比较器原理:底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边。(红黑树) +比较器原理:底层是以第一个元素为基准,加一个新元素,就会和第一个元素比,如果大于,就继续和大于的元素进行比较,直到遇到比新元素大的元素为止,放在该位置的左边(红黑树) @@ -4642,7 +4637,7 @@ Map集合的遍历方式有:3种。 1. “键找值”的方式遍历:先获取 Map 集合全部的键,再根据遍历键找值。 2. “键值对”的方式遍历:难度较大,采用增强 for 或者迭代器 -3. JDK 1.8 开始之后的新技术:foreach,采用 Lambda表 达式 +3. JDK 1.8 开始之后的新技术:foreach,采用 Lambda 表达式 集合可以直接输出内容,因为底层重写了 toString() 方法 @@ -4732,7 +4727,7 @@ JDK7 对比 JDK8: ##### 继承关系 -HashMap继承关系如下图所示: +HashMap 继承关系如下图所示: ![](https://gitee.com/seazean/images/raw/master/Java/HashMap继承关系.bmp) @@ -4740,9 +4735,9 @@ HashMap继承关系如下图所示: 说明: -* Cloneable 空接口,表示可以克隆, 创建并返回HashMap对象的一个副本。 -* Serializable 序列化接口,属于标记性接口,HashMap对象可以被序列化和反序列化。 -* AbstractMap 父类提供了Map实现接口,以最大限度地减少实现此接口所需的工作 +* Cloneable 空接口,表示可以克隆, 创建并返回 HashMap 对象的一个副本。 +* Serializable 序列化接口,属于标记性接口,HashMap 对象可以被序列化和反序列化。 +* AbstractMap 父类提供了 Map 实现接口,以最大限度地减少实现此接口所需的工作 @@ -4773,7 +4768,7 @@ HashMap继承关系如下图所示: * 为什么必须是 2 的 n 次幂? - HashMap 中添加元素时,需要根据 key 的 hash 值,确定在数组中的具体位置。为了存取高效,要尽量较少碰撞,把数据尽可能分配均匀,每个链表长度大致相同,实现该方法的算法就是取模,hash%length,计算机中直接求余效率不如位移运算,所以源码中使用 hash&(length-1),实际上**hash % length == hash & (length-1)的前提是 length 是 2 的n次幂** + HashMap 中添加元素时,需要根据 key 的 hash 值,确定在数组中的具体位置。为了存取高效,要尽量较少碰撞,把数据分配均匀,每个链表长度大致相同,实现该方法的算法就是取模,hash%length,计算机中直接求余效率不如位移运算,所以源码中使用 hash&(length-1),实际上**hash % length == hash & (length-1)的前提是 length 是 2 的n次幂** 散列平均分布:2 的 n 次方是 1 后面 n 个 0,2 的 n 次方 -1 是 n 个 1,可以**保证散列的均匀性**,减少碰撞 @@ -4833,7 +4828,7 @@ HashMap继承关系如下图所示: * 其他说法 红黑树的平均查找长度是 log(n),如果长度为 8,平均查找长度为 log(8)=3,链表的平均查找长度为 n/2,当长度为 8 时,平均查找长度为 8/2=4,这才有转换成树的必要;链表长度如果是小于等于 6,6/2=3,而 log(6)=2.6,虽然速度也很快的,但转化为树结构和生成树的时间并不短 -6. 当链表的值小 于 6 则会从红黑树转回链表 +6. 当链表的值小于 6 则会从红黑树转回链表 ```java // 当桶(bucket)上的结点数小于这个值时树转链表 @@ -4847,7 +4842,7 @@ HashMap继承关系如下图所示: static final int MIN_TREEIFY_CAPACITY = 64; ``` - 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于 64 时,搜索时间相对快些,所以为了提高性能和减少搜索时间,底层在阈值大于 8 并且数组长度大于 64 时,链表才转换为红黑树,效率也变的更高效 + 原因:数组比较小的情况下变为红黑树结构,反而会降低效率,红黑树需要进行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于 64 时,搜索时间相对快些,所以为了提高性能和减少搜索时间,底层在阈值大于 8 并且数组长度大于等于 64 时,链表才转换为红黑树,效率也变的更高效 8. table 用来初始化(必须是二的 n 次幂) @@ -4858,7 +4853,7 @@ HashMap继承关系如下图所示: jdk8 之前数组类型是 Entry类型,之后是 Node 类型。只是换了个名字,都实现了一样的接口 Map.Entry,负责存储键值对数据的 - 9. HashMap 中**存放元素的个数**(**重点**) + 9. HashMap 中**存放元素的个数** ```java // 存放元素的个数,HashMap中K-V的实时数量,不是table数组的长度 @@ -4899,7 +4894,7 @@ HashMap继承关系如下图所示: loadFactor 太大导致查找元素效率低,存放的数据拥挤,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 **0.75f 是官方给出的一个比较好的临界值** - * threshold 计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认 0.75)。这个值是当前已占用数组长度的最大值。**当 size>=threshold** 的时候,那么就要考虑对数组的 resize(扩容),这就是衡量数组是否需要扩增的一个标准, 扩容后的 HashMap 容量是之前容量的**两倍**. + * threshold 计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认 0.75)。这个值是当前已占用数组长度的最大值。**当 size>=threshold** 的时候,那么就要考虑对数组的 resize(扩容),这就是衡量数组是否需要扩增的一个标准, 扩容后的 HashMap 容量是之前容量的**两倍** @@ -5013,7 +5008,7 @@ HashMap继承关系如下图所示: 计算 hash 的方法:将 hashCode 无符号右移 16 位,高 16bit 和低 16bit 做异或,扰动运算 - 原因:当数组长度很小,假设是 16,那么 n-1 即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 + 原因:当数组长度很小,假设是 16,那么 n-1 即为 1111 ,这样的值和 hashCode() 直接做按位与操作,实际上只使用了哈希值的后 4 位。如果当哈希值的高位变化很大,低位变化很小,就很容易造成哈希冲突了,所以这里**把高低位都利用起来,让高16 位也参与运算**,从而解决了这个问题 哈希冲突的处理方式: @@ -5320,7 +5315,7 @@ HashMap继承关系如下图所示: 3. 桶上的 key 不是要找的 key,则查看后续的节点: - * 如果后续节点是红黑树节点,通过调用红黑树的方法根据 key 获取v alue + * 如果后续节点是红黑树节点,通过调用红黑树的方法根据 key 获取 value * 如果后续节点是链表节点,则通过循环遍历链表根据 key 获取 value @@ -5443,7 +5438,7 @@ LinkedHashMap 是 HashMap 的子类 源码解析: -* 内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序 +* **内部维护了一个双向链表**,用来维护插入顺序或者 LRU 顺序 ```java transient LinkedHashMap.Entry head; @@ -5672,7 +5667,7 @@ TreeMap 集合指定大小规则有 2 种方式: WeakHashMap 是基于弱引用的 -内部的 Entry 继承 WeakReference,被弱引用关联的对象在下一次垃圾回收时会被回收,并且构造方法传入引用队列,用来在清理对象完成以后清理引用 +内部的 Entry 继承 WeakReference,被弱引用关联的对象在**下一次垃圾回收时会被回收**,并且构造方法传入引用队列,用来在清理对象完成以后清理引用 ```java private static class Entry extends WeakReference implements Map.Entry { @@ -5734,41 +5729,6 @@ Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能,Conc -*** - - - -#### 面试题 - -输出一个字符串中每个字符出现的次数。 - -```java -/* - (1)键盘录入一个字符串。aabbccddaa123。 - (2)定义一个Map集合,键是每个字符,值是其出现的次数。 {a=4 , b=2 ,...} - (3)遍历字符串中的每一个字符。 - (4)拿着这个字符去Map集合中看是否有这个字符键,有说明之前统计过,其值+1 - 没有这个字符键,说明该字符是第一次统计,直接存入“该字符=1” -*/ -public class MapDemo{ - public static void main(String[] args){ - String s = "aabbccddaa123"; - Map infos = new HashMap<>(); - for (int i = 0; i < s.length(); i++){ - char ch = datas.charAt(i); - if(infos.containsKey(ch)){ - infos.put(ch,infos.get(ch) + 1); - } else { - infos.put(ch,1); - } - } - System.out.println("结果:"+infos); - } -} -``` - - - *** @@ -14234,7 +14194,7 @@ public static void main(String[] args) { } ``` -注意:如果调用了 foo() 则等价代码为 `foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 +注意:如果调用了 `foo()` 则等价代码为 `foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 diff --git a/Prog.md b/Prog.md index c09c693..8d5634a 100644 --- a/Prog.md +++ b/Prog.md @@ -14077,7 +14077,7 @@ ServerSocket 类: -相当于客户端和服务器建立一个数据管道,管道一般不用 close +**相当于**客户端和服务器建立一个数据管道(虚连接,不是真正的物理连接),管道一般不用 close diff --git a/Web.md b/Web.md index 3236ec1..ea0ed9d 100644 --- a/Web.md +++ b/Web.md @@ -2119,8 +2119,10 @@ URL 和 URI * 进行 URL 解析,进行编码 * DNS 解析,顺序是先查 hosts 文件是否有记录,有的话就会把相对应映射的 IP 返回,然后去本地 DNS 缓存中寻找,然后去找计算机上配置的 DNS 服务器上有或者有缓存,最后去找全球的根 DNS 服务器,直到查到为止 -* 查找到 IP 之后,进行 TCP 协议的三次握手,发出 HTTP 请求 +* 查找到 IP 之后,进行 TCP 协议的三次握手建立连接 +* 发出 HTTP 请求,取文件指令 * 服务器处理请求,返回响应 +* 释放 TCP 连接 * 浏览器解析渲染页面 From c6e1467b4466acb9b6686b8008dc844443b8de01 Mon Sep 17 00:00:00 2001 From: Seazean Date: Sun, 20 Feb 2022 23:20:36 +0800 Subject: [PATCH 067/122] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ac1828..211ff15 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ * DB:MySQL、Redis * Frame:Maven、Netty、RocketMQ -* Java:JavaSE、JVM、Algorithm、Design Pattern +* Java:JavaSE、JVM、Algorithm * Prog:Concurrent、Network Programming * SSM:MyBatis、Spring、SpringMVC、SpringBoot * Tool:Git、Linux、Docker From 83f54324a1b289a542e899a5b0520710481e806e Mon Sep 17 00:00:00 2001 From: Seazean Date: Mon, 21 Feb 2022 00:43:50 +0800 Subject: [PATCH 068/122] Update Java Note --- Java.md | 15131 ++++++++++++++++++++---------------------------------- 1 file changed, 5599 insertions(+), 9532 deletions(-) diff --git a/Java.md b/Java.md index be48f40..77f9179 100644 --- a/Java.md +++ b/Java.md @@ -2260,6 +2260,8 @@ public class CodeDemo { + + *** @@ -2872,6 +2874,48 @@ public class MyArraysDemo { +*** + + + +### System + +System 代表当前系统 + +静态方法: + +* `public static void exit(int status)`:终止 JVM 虚拟机,**非 0 是异常终止** + +* `public static long currentTimeMillis()`:获取当前系统此刻时间毫秒值 + +* `static void arraycopy(Object var0, int var1, Object var2, int var3, int var4)`:数组拷贝 + 参数一:原数组 + 参数二:从原数组的哪个位置开始赋值 + 参数三:目标数组 + 参数四:从目标数组的哪个位置开始赋值 + 参数五:赋值几个 + +```java +public class SystemDemo { + public static void main(String[] args) { + //System.exit(0); // 0代表正常终止!! + long startTime = System.currentTimeMillis();//定义sdf 按照格式输出 + for(int i = 0; i < 10000; i++){输出i} + long endTime = new Date().getTime(); + System.out.println( (endTime - startTime)/1000.0 +"s");//程序用时 + + int[] arr1 = new int[]{10 ,20 ,30 ,40 ,50 ,60 ,70}; + int[] arr2 = new int[6]; // [ 0 , 0 , 0 , 0 , 0 , 0] + // 变成arrs2 = [0 , 30 , 40 , 50 , 0 , 0 ] + System.arraycopy(arr1, 2, arr2, 1, 3); + } +} +``` + + + + + *** @@ -2889,8 +2933,8 @@ public class MyArraysDemo { 时间记录的两种方式: -1. Date日期对象 -2. 时间毫秒值:从1970-01-01 00:00:00开始走到此刻的总的毫秒值。 1s = 1000ms +1. Date 日期对象 +2. 时间毫秒值:从 `1970-01-01 00:00:00` 开始走到此刻的总的毫秒值,1s = 1000ms ```java public class DateDemo { @@ -2934,10 +2978,10 @@ DateFormat 是一个抽象类,不能直接使用,使用它的子类:Simple SimpleDateFormat 简单日期格式化类: -* `public SimpleDateFormat(String pattern)` : 指定时间的格式创建简单日期对象 -* `public String format(Date date) ` : 把日期对象格式化成我们喜欢的时间形式,返回字符串 -* `public String format(Object time)` : 把时间毫秒值格式化成设定的时间形式,返回字符串! -* `public Date parse(String date)` : 把字符串的时间解析成日期对象 +* `public SimpleDateFormat(String pattern)`:指定时间的格式创建简单日期对象 +* `public String format(Date date) `:把日期对象格式化成我们喜欢的时间形式,返回字符串 +* `public String format(Object time)`:把时间毫秒值格式化成设定的时间形式,返回字符串! +* `public Date parse(String date)`:把字符串的时间解析成日期对象 >yyyy年MM月dd日 HH:mm:ss EEE a" 周几 上午下午 @@ -2972,12 +3016,12 @@ Calendar 日历类创建日历对象:`Calendar rightNow = Calendar.getInstance Calendar 的方法: -* `public static Calendar getInstance()`: 返回一个日历类的对象 +* `public static Calendar getInstance()`:返回一个日历类的对象 * `public int get(int field)`:取日期中的某个字段信息 * `public void set(int field,int value)`:修改日历的某个字段信息 * `public void add(int field,int amount)`:为某个字段增加/减少指定的值 -* `public final Date getTime()`: 拿到此刻日期对象 -* `public long getTimeInMillis()`: 拿到此刻时间毫秒值 +* `public final Date getTime()`:拿到此刻日期对象 +* `public long getTimeInMillis()`:拿到此刻时间毫秒值 ```java public static void main(String[] args){ @@ -3048,23 +3092,10 @@ public class JDK8DateDemo2 { } ``` -| 方法名 | 说明 | -| --------------------------------------------------- | ------------------------------ | -| public LocalDateTime plusYears (long years) | 添加或者减去年 | -| public LocalDateTime plusMonths(long months) | 添加或者减去月 | -| public LocalDateTime plusDays(long days) | 添加或者减去日 | -| public LocalDateTime plusHours(long hours) | 添加或者减去时 | -| public LocalDateTime plusMinutes(long minutes) | 添加或者减去分 | -| public LocalDateTime plusSeconds(long seconds) | 添加或者减去秒 | -| public LocalDateTime plusWeeks(long weeks) | 添加或者减去周 | -| public LocalDateTime minusYears (long years) | 减去或者添加年 | -| public LocalDateTime withYear(int year) | 直接修改年 | -| public LocalDateTime withMonth(int month) | 直接修改月 | -| public LocalDateTime withDayOfMonth(int dayofmonth) | 直接修改日期(一个月中的第几天) | -| public LocalDateTime withDayOfYear(int dayOfYear) | 直接修改日期(一年中的第几天) | -| public LocalDateTime withHour(int hour) | 直接修改小时 | -| public LocalDateTime withMinute(int minute) | 直接修改分钟 | -| public LocalDateTime withSecond(int second) | 直接修改秒 | +| 方法名 | 说明 | +| ------------------------------------------- | -------------- | +| public LocalDateTime plusYears (long years) | 添加或者减去年 | +| public LocalDateTime withYear(int year) | 直接修改年 | @@ -3142,6 +3173,10 @@ public class MathDemo { +**** + + + ### DecimalFormat 使任何形式的数字解析和格式化 @@ -3175,42 +3210,6 @@ public static void main(String[]args){ -*** - - - -### System - -System代表当前系统。 - -静态方法: - -1. `public static void exit(int status)` : 终止JVM虚拟机,非0是异常终止 -2. `public static long currentTimeMillis()` : 获取当前系统此刻时间毫秒值 -3. `static void arraycopy(Object var0, int var1, Object var2, int var3, int var4)` : 数组拷贝 - 参数一:原数组 - 参数二:从原数组的哪个位置开始赋值。 - 参数三:目标数组 - 参数四:从目标数组的哪个位置开始赋值 - 参数五:赋值几个。 - -```java -public class SystemDemo { - public static void main(String[] args) { - //System.exit(0); // 0代表正常终止!! - long startTime = System.currentTimeMillis();//定义sdf 按照格式输出 - for(int i = 0; i < 10000; i++){输出i} - long endTime = new Date().getTime(); - System.out.println( (endTime - startTime)/1000.0 +"s");//程序用时 - - int[] arr1 = new int[]{10 ,20 ,30 ,40 ,50 ,60 ,70}; - int[] arr2 = new int[6]; // [ 0 , 0 , 0 , 0 , 0 , 0] - // 变成arrs2 = [0 , 30 , 40 , 50 , 0 , 0 ] - System.arraycopy(arr1, 2, arr2, 1, 3); - } -} -``` - *** @@ -3233,8 +3232,8 @@ Java 在 java.math 包中提供的 API 类,用来对超过16位有效位的数 * `public BigDecimal subtract(BigDecimal value)`:减法运算 * `public BigDecimal multiply(BigDecimal value)`:乘法运算 * `public BigDecimal divide(BigDecimal value)`:除法运算 -* `public double doubleValue()`:把BigDecimal转换成double类型。 -* `public int intValue()`:转为int 其他类型相同 +* `public double doubleValue()`:把 BigDecimal 转换成 double 类型 +* `public int intValue()`:转为 int 其他类型相同 * `public BigDecimal divide (BigDecimal value,精确几位,舍入模式)`:除法 ```java @@ -3261,20 +3260,20 @@ public class BigDecimalDemo { } ``` -总结 +总结: 1. BigDecimal 是用来进行精确计算的 -2. 创建 BigDecimal 的对象,构造方法使用参数类型为字符串的。 -3. 四则运算中的除法,如果除不尽请使用 divide 的三个参数的方法。 +2. 创建 BigDecimal 的对象,构造方法使用参数类型为字符串的 +3. 四则运算中的除法,如果除不尽请使用 divide 的三个参数的方法 ```java BigDecimal divide = bd1.divide(参与运算的对象,小数点后精确到多少位,舍入模式); -参数1:表示参与运算的BigDecimal 对象。 -参数2:表示小数点后面精确到多少位 -参数3:舍入模式 - BigDecimal.ROUND_UP 进一法 - BigDecimal.ROUND_FLOOR 去尾法 - BigDecimal.ROUND_HALF_UP 四舍五入 +//参数1:表示参与运算的BigDecimal 对象。 +//参数2:表示小数点后面精确到多少位 +//参数3:舍入模式 +// BigDecimal.ROUND_UP 进一法 +// BigDecimal.ROUND_FLOOR 去尾法 +// BigDecimal.ROUND_HALF_UP 四舍五入 ``` @@ -3718,6 +3717,8 @@ public class RegexDemo { + + ## 集合 ### 集合概述 @@ -3763,7 +3764,7 @@ public class RegexDemo { * 二叉树:binary tree 永远只有一个根节点,是每个结点不超过2个节点的树(tree) 特点:二叉排序树:小的左边,大的右边,但是可能树很高,性能变差,为了做排序和搜索会进行左旋和右旋实现平衡查找二叉树,让树的高度差不大于1 - + * 红黑树(基于红黑规则实现自平衡的排序二叉树):树保证到了很矮小,但是又排好序,性能最高的树 特点:**红黑树的增删查改性能都好** @@ -5748,15 +5749,13 @@ Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能,Conc * **泛型和集合都只能支持引用数据类型,不支持基本数据类型。** ```java -{ - ArrayList lists = new ArrayList<>(); - lists.add(99.9); - lists.add('a'); - lists.add("Java"); - ArrayList list = new ArrayList<>(); - lists1.add(10); - lists1.add(20); -} +ArrayList lists = new ArrayList<>(); +lists.add(99.9); +lists.add('a'); +lists.add("Java"); +ArrayList list = new ArrayList<>(); +lists1.add(10); +lists1.add(20); ``` 优点:泛型在编译阶段约束了操作的数据类型,从而不会出现类型转换异常,体现的是 Java 的严谨性和规范性 @@ -5913,73 +5912,13 @@ class Dog{} -### 不可变 - -在 List、Set、Map 接口中都存在 of 方法,可以创建一个不可变的集合 -+ 这个集合不能添加,不能删除,不能修改 -+ 但是可以结合集合的带参构造,实现集合的批量添加 - -在 Map 接口中,还有一个 ofEntries 方法可以提高代码的阅读性 -+ 首先会把键值对封装成一个 Entry 对象,再把这个 Entry 对象添加到集合当中 - -````java -public class MyVariableParameter4 { - public static void main(String[] args) { - // static List of(E…elements) 创建一个具有指定元素的List集合对象 - //static Set of(E…elements) 创建一个具有指定元素的Set集合对象 - //static Map of(E…elements) 创建一个具有指定元素的Map集合对象 - - //method1(); - //method2(); - //method3(); - //method4(); - - } - - private static void method4() { - Map map = Map.ofEntries( - Map.entry("zhangsan", "江苏"), - Map.entry("lisi", "北京")); - System.out.println(map); - } - - private static void method3() { - Map map = Map.of("zhangsan", "江苏", "lisi", "北京"); - System.out.println(map); - } - - private static void method2() { - //传递的参数当中,不能存在重复的元素。 - Set set = Set.of("a", "b", "c", "d","a"); - System.out.println(set); - } - - private static void method1() { - List list = List.of("a", "b", "c", "d"); - System.out.println(list); - - //集合的批量添加。 - //首先是通过调用List.of方法来创建一个不可变的集合,of方法的形参就是一个可变参数。 - //再创建一个ArrayList集合,并把这个不可变的集合中所有的数据,都添加到ArrayList中。 - ArrayList list3 = new ArrayList<>(List.of("a", "b", "c", "d")); - System.out.println(list3); - } -} -```` - - - - - -*** - ## 异常 ### 基本介绍 -异常:程序在"编译"或者"执行"的过程中可能出现的问题,Java 为常见的代码异常都设计一个类来代表。 +异常:程序在编译或者执行的过程中可能出现的问题,Java 为常见的代码异常都设计一个类来代表。 错误:Error ,程序员无法处理的错误,只能重启系统,比如内存奔溃,JVM 本身的奔溃 @@ -5998,7 +5937,7 @@ Java 中异常继承的根类是:Throwable Exception 异常的分类: * 编译时异常:继承自 Exception 的异常或者其子类,编译阶段就会报错 -* 运行时异常: 继承自 RuntimeException 的异常或者其子类,编译阶段是不会出错的,在运行时阶段可能出现,编译阶段是不会出错的,但是运行阶段可能出现,建议提前处理 +* 运行时异常:继承自 RuntimeException 的异常或者其子类,编译阶段是不会出错的,在运行阶段出错 @@ -6011,9 +5950,9 @@ Exception 异常的分类: 异常的产生默认的处理过程解析:(自动处理的过程) 1. 默认会在出现异常的代码那里自动的创建一个异常对象:ArithmeticException(算术异常) -2. 异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给JVM虚拟机 +2. 异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给 JVM 虚拟机 3. 虚拟机接收到异常对象后,先在控制台直接输出**异常栈**信息数据 -4. 直接从当前执行的异常点干掉当前程序 +4. 直接从当前执行的异常点终止当前程序 5. 后续代码没有机会执行了,因为程序已经死亡 ```java @@ -6040,12 +5979,9 @@ public class ExceptionDemo { #### 基本介绍 -编译时异常:继承自Exception的异常或者其子类,没有继承 RuntimeException,编译时异常是编译阶段就会报错,必须程序员编译阶段就处理的。否则代码编译就报错 - -编译时异常的作用是什么: +编译时异常:继承自 Exception 的异常或者其子类,没有继承 RuntimeException,编译时异常是编译阶段就会报错 -* 是担心程序员的技术不行,在编译阶段就爆出一个错误, 目的在于提醒 -* 提醒程序员这里很可能出错,请检查并注意不要出bug +编译时异常的作用是什么:在编译阶段就爆出一个错误,目的在于提醒,请检查并注意不要出 BUG ```java public static void main(String[] args) throws ParseException { @@ -6066,12 +6002,9 @@ public static void main(String[] args) throws ParseException { ##### throws -在出现编译时异常的地方层层把异常抛出去给调用者,调用者最终抛出给JVM虚拟机,JVM 虚拟机输出异常信息,直接干掉程序,这种方式与默认方式是一样的。 +在出现编译时异常的地方层层把异常抛出去给调用者,调用者最终抛出给 JVM 虚拟机,JVM 虚拟机输出异常信息,直接终止掉程序,这种方式与默认方式是一样的 -* 优点:可以解决代码编译时的错误 -* 运行时出现异常,程序还是会立即死亡! - -**Exception是异常最高类型可以抛出一切异常!** +**Exception是异常最高类型可以抛出一切异常** ```java public static void main(String[] args) throws Exception { @@ -6091,7 +6024,7 @@ public static void main(String[] args) throws Exception { ##### try/catch -可以处理异常,并且出现异常后代码也不会死亡。 +可以处理异常,并且出现异常后代码也不会死亡 * 自己捕获异常和处理异常的格式:**捕获处理** @@ -6107,9 +6040,8 @@ public static void main(String[] args) throws Exception { } ``` -* 监视捕获处理异常企业级写法: - Exception可以捕获处理一切异常类型! - +* 监视捕获处理异常写法:Exception 可以捕获处理一切异常类型 + ```java try{ // 可能出现异常的代码! @@ -6119,9 +6051,10 @@ public static void main(String[] args) throws Exception { ``` **Throwable成员方法:** - `public String getMessage()` : 返回此 throwable 的详细消息字符串 - `public String toString()` : 返回此可抛出的简短描述 - `public void printStackTrace()` : 把异常的错误信息输出在控制台 + +* `public String getMessage()`:返回此 throwable 的详细消息字符串 +* `public String toString()`:返回此可抛出的简短描述 +* `public void printStackTrace()`:把异常的错误信息输出在控制台 ```java public static void main(String[] args) { @@ -6146,8 +6079,7 @@ public static void main(String[] args) { ##### 规范做法 -在出现异常的地方把异常一层一层的抛出给最外层调用者,最外层调用者集中捕获处理!(**规范做法**) -这种方案最外层调用者可以知道底层执行的情况,同时程序在出现异常后也不会立即死亡(最好的方案) +在出现异常的地方把异常一层一层的抛出给最外层调用者,最外层调用者集中捕获处理 ```java public class ExceptionDemo{ @@ -6174,16 +6106,16 @@ public class ExceptionDemo{ #### 基本介绍 -继承自RuntimeException的异常或者其子类,编译阶段是不会出错的,它是在运行时阶段可能出现的错误,运行时异常编译阶段可以处理也可以不处理,代码编译都能通过!! +继承自 RuntimeException 的异常或者其子类,编译阶段是不会出错的,是在运行时阶段可能出现的错误,运行时异常编译阶段可以处理也可以不处理,代码编译都能通过 **常见的运行时异常**: -1. 数组索引越界异常: ArrayIndexOutOfBoundsException -2. 空指针异常 : NullPointerException,直接输出没问题,调用空指针的变量的功能就会报错! +1. 数组索引越界异常:ArrayIndexOutOfBoundsException +2. 空指针异常:NullPointerException,直接输出没问题,调用空指针的变量的功能就会报错 3. 类型转换异常:ClassCastException 4. 迭代器遍历没有此元素异常:NoSuchElementException 5. 算术异常(数学操作异常):ArithmeticException -6. 数字转换异常: NumberFormatException +6. 数字转换异常:NumberFormatException ```java public class ExceptionDemo { @@ -6267,11 +6199,9 @@ catch:0-N次 (如果有finally那么catch可以没有!!) finally: 0-1次 ``` +**finally 的作用**:可以在代码执行完毕以后进行资源的释放操作 - -**finally的作用**:可以在代码执行完毕以后进行资源的释放操作 - -资源:资源都是实现了 Closeable 接口的,都自带 close() 关闭方法! +资源:资源都是实现了 Closeable 接口的,都自带 close() 关闭方法 注意:如果在 finally 中出现了 return,会吞掉异常 @@ -6322,10 +6252,10 @@ public class FinallyDemo { 自定义异常: -* 自定义编译时异常:定义一个异常类继承 Exception,重写构造器,在出现异常的地方用throw new 自定义对象抛出 -* 自定义运行时异常:定义一个异常类继承 RuntimeException,重写构造器,在出现异常的地方用 throw new 自定义对象抛出! +* 自定义编译时异常:定义一个异常类继承 Exception,重写构造器,在出现异常的地方用 throw new 自定义对象抛出 +* 自定义运行时异常:定义一个异常类继承 RuntimeException,重写构造器,在出现异常的地方用 throw new 自定义对象抛出 -**throws: 用在方法上,用于抛出方法中的异常** +**throws:用在方法上,用于抛出方法中的异常** **throw: 用在出现异常的地方,创建异常对象且立即从此处抛出** @@ -6412,17 +6342,19 @@ public class Demo{ + + ## λ ### lambda #### 基本介绍 -Lambda表达式是JDK1.8开始之后的新技术,是一种代码的新语法,一种特殊写法 +Lambda 表达式是 JDK1.8 开始之后的新技术,是一种代码的新语法,一种特殊写法 作用:为了简化匿名内部类的代码写法 -Lambda表达式的格式: +Lambda 表达式的格式: ```java (匿名内部类被重写方法的形参列表) -> { @@ -6430,11 +6362,11 @@ Lambda表达式的格式: } ``` -Lambda表达式并不能简化所有匿名内部类的写法,只能简化**函数式接口的匿名内部类** +Lambda 表达式并不能简化所有匿名内部类的写法,只能简化**函数式接口的匿名内部类** 简化条件:首先必须是接口,接口中只能有一个抽象方法 -@FunctionalInterface函数式接口注解:一旦某个接口加上了这个注解,这个接口只能有且仅有一个抽象方法 +@FunctionalInterface 函数式接口注解:一旦某个接口加上了这个注解,这个接口只能有且仅有一个抽象方法 @@ -6444,9 +6376,9 @@ Lambda表达式并不能简化所有匿名内部类的写法,只能简化**函 #### 简化方法 -Lambda表达式的省略写法(进一步在Lambda表达式的基础上继续简化) +Lambda 表达式的省略写法(进一步在 Lambda 表达式的基础上继续简化) -* 如果Lambda表达式的方法体代码只有一行代码,可以省略大括号不写,同时要省略分号;如果这行代码是return语句,必须省略return不写 +* 如果 Lambda 表达式的方法体代码只有一行代码,可以省略大括号不写,同时要省略分号;如果这行代码是 return 语句,必须省略 return 不写 * 参数类型可以省略不写 * 如果只有一个参数,参数类型可以省略,同时()也可以省略 @@ -6486,35 +6418,7 @@ names.forEach(s -> System.out.println(s) ); #### 常用简化 -##### Runnable - -```java -//1. -Thread t = new Thread(new Runnable() { - @Override - public void run() { - System.out.println(Thread.currentThread().getName()+":执行~~~"); - } -}); -t.start(); - -//2. -Thread t1 = new Thread(() -> { - System.out.println(Thread.currentThread().getName()+":执行~~~"); -}); -t1.start(); -//3. -new Thread(() -> { - System.out.println(Thread.currentThread().getName()+":执行~~~"); -}).start(); - -//4.一行代码 -new Thread(() -> System.out.println(Thread.currentThread().getName()+":执行~~~")).start(); -``` - - - -##### Comparator +Comparator ```java public class CollectionsDemo { @@ -6550,7 +6454,7 @@ public class CollectionsDemo { #### 基本介绍 -方法引用:方法引用是为了进一步简化Lambda表达式的写法 +方法引用:方法引用是为了进一步简化 Lambda 表达式的写法 方法引用的格式:类型或者对象::引用的方法 @@ -6708,6 +6612,8 @@ public class ConstructorDemo { + + ## I/O ### Stream @@ -6895,9 +6801,9 @@ public static void main(String[] args) { File 类:代表操作系统的文件对象,是用来操作操作系统的文件对象的,删除文件,获取文件信息,创建文件(文件夹),广义来说操作系统认为文件包含(文件和文件夹) File 类构造器: - `public File(String pathname)`:根据路径获取文件对象 - `public File(String parent , String child)`:根据父路径和文件名称获取文件对象! - `public File(File parent , String child)` + +* `public File(String pathname)`:根据路径获取文件对象 +* `public File(String parent , String child)`:根据父路径和文件名称获取文件对象 File 类创建文件对象的格式: @@ -6945,17 +6851,19 @@ public class FileDemo{ ##### 常用方法 -`public String getAbsolutePath()` : 返回此File的绝对路径名字符串。 -`public String getPath()` : 获取创建文件对象的时候用的路径 -`public String getName()` : 返回由此File表示的文件或目录的名称。 -`public long length()` : 返回由此File表示的文件的长度(大小)。 -`public long length(FileFilter filter)` : 文件过滤器。 +| 方法 | 说明 | +| ------------------------------ | -------------------------------------- | +| String getAbsolutePath() | 返回此 File 的绝对路径名字符串 | +| String getPath() | 获取创建文件对象的时候用的路径 | +| String getName() | 返回由此 File 表示的文件或目录的名称 | +| long length() | 返回由此 File 表示的文件的长度(大小) | +| long length(FileFilter filter) | 文件过滤器 | ```java public class FileDemo { public static void main(String[] args) { // 1.绝对路径创建一个文件对象 - File f1 = new File("E:/图片/meinv.jpg"); + File f1 = new File("E:/图片/test.jpg"); // a.获取它的绝对路径。 System.out.println(f1.getAbsolutePath()); // b.获取文件定义的时候使用的路径。 @@ -6967,7 +6875,7 @@ public class FileDemo { System.out.println("------------------------"); // 2.相对路径 - File f2 = new File("Day09Demo/src/dlei01.txt"); + File f2 = new File("Demo/src/test.txt"); // a.获取它的绝对路径。 System.out.println(f2.getAbsolutePath()); // b.获取文件定义的时候使用的路径。 @@ -6983,14 +6891,20 @@ public class FileDemo { +*** + + + ##### 判断方法 -`public boolean exists()` : 此File表示的文件或目录是否实际存在。 -`public boolean isDirectory()` : 此File表示的是否为目录。 -`public boolean isFile()` : 此File表示的是否为文件 +方法列表: + +* `boolean exists()`:此 File 表示的文件或目录是否实际存在 +* `boolean isDirectory()`:此 File 表示的是否为目录 +* `boolean isFile()`:此 File 表示的是否为文件 ```java -File f = new File("Day09Demo/src/dlei01.txt"); +File f = new File("Demo/src/test.txt"); // a.判断文件路径是否存在 System.out.println(f.exists()); // true // b.判断文件对象是否是文件,是文件返回true ,反之 @@ -7001,17 +6915,23 @@ System.out.println(f.isDirectory()); // false +**** + + + ##### 创建删除 -`public boolean createNewFile()` : 当且仅当具有该名称的文件尚不存在时, 创建一个新的空文件。 -`public boolean delete()` : 删除由此File表示的文件或目录。 (只能删除空目录) -`public boolean mkdir()` : 创建由此File表示的目录。(只能创建一级目录) -`public boolean mkdirs()` : 可以创建多级目录(建议使用的) +方法列表: + +* `boolean createNewFile()`:当且仅当具有该名称的文件尚不存在时, 创建一个新的空文件 +* `boolean delete()`:删除由此 File 表示的文件或目录(只能删除空目录) +* `boolean mkdir()`:创建由此 File 表示的目录(只能创建一级目录) +* `boolean mkdirs()`:可以创建多级目录(建议使用) ```java public class FileDemo { public static void main(String[] args) throws IOException { - File f = new File("Day09Demo/src/dlei02.txt"); + File f = new File("Demo/src/test.txt"); // a.创建新文件,创建成功返回true ,反之 System.out.println(f.createNewFile()); @@ -7041,7 +6961,7 @@ public class FileDemo { #### 遍历目录 - `public String[] list()`:获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回。 -- `public File[] listFiles()(常用)`:获取当前目录下所有的"一级文件对象"到一个**文件对象数组**中去返回(**重点**) +- `public File[] listFiles()`:获取当前目录下所有的"一级文件对象"到一个**文件对象数组**中去返回(**重点**) - `public long lastModified`:返回此抽象路径名表示的文件上次修改的时间。 ```java @@ -7060,7 +6980,7 @@ public class FileDemo { } // c - File f1 = new File("D:\\it\\图片资源\\beautiful.jpg"); + File f1 = new File("D:\\图片资源\\beautiful.jpg"); long time = f1.lastModified(); // 最后修改时间! SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println(sdf.format(time)); @@ -7168,30 +7088,23 @@ IO 输入输出流:输入/输出流 * Input:输入 * Output:输出 -引入:File类只能操作文件对象本身,不能读写文件对象的内容,读写数据内容,应该使用IO流 +引入:File 类只能操作文件对象本身,不能读写文件对象的内容,读写数据内容,应该使用 IO 流 IO 流是一个水流模型:IO 理解成水管,把数据理解成水流 IO 流的分类: * 按照流的方向分为:输入流,输出流。 - * 输出流:以内存为基准,把内存中的数据写出到磁盘文件或者网络介质中去的流称为输出流 - 输出流的作用:写数据到文件,或者写数据发送给别人 - * 输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据读入到内存中的流称为输入流 - 输入流的作用:读取数据到内存 + * 输出流:以内存为基准,把内存中的数据**写出到磁盘文件**或者网络介质中去的流称为输出流 + * 输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据**读入到内存**中的流称为输入流 * 按照流的内容分为:字节流,字符流 * 字节流:流中的数据的最小单位是一个一个的字节,这个流就是字节流 * 字符流:流中的数据的最小单位是一个一个的字符,这个流就是字符流(**针对于文本内容**) -流大体分为四大类: - -* 字节输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据以一个一个的字节的形式读入到内存中去的流称为字节输入流 -* 字节输出流:以内存为基准,把内存中的数据以一个一个的字节写出到磁盘文件或者网络介质中去的流称为字节输出流 -* 字符输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据以一个一个的字符的形式读入到内存中去的流称为字符输入流 -* 字符输出流:以内存为基准,把内存中的数据以一个一个的字符写出到磁盘文件或者网络介质中去的流称为字符输出流 +流大体分为四大类:字节输入流、字节输出流、字符输入流、字符输出流 ```java -IO流的体系: +IO 流的体系: 字节流 字符流 字节输入流 字节输出流 字符输入流 字符输出流 InputStream OutputStream Reader Writer (抽象类) @@ -7211,25 +7124,25 @@ ObjectInputStream ObjectOutputStream ##### 字节输入 -FileInputStream 文件字节输入流: +FileInputStream 文件字节输入流:以内存为基准,把磁盘文件中的数据按照字节的形式读入到内存中的流 + +构造方法: -* 作用:以内存为基准,把磁盘文件中的数据按照字节的形式读入到内存中的流 +* `public FileInputStream(File path)`:创建一个字节输入流管道与源文件对象接通 +* `public FileInputStream(String pathName)`:创建一个字节输入流管道与文件路径对接,底层实质上创建 File 对象 -* 构造器: - `public FileInputStream(File path)` : 创建一个字节输入流管道与源文件对象接通 - `public FileInputStream(String pathName)` : 创建一个字节输入流管道与文件路径对接,底层实质上创建了File对象 - -* 方法: - `public int read()` : 每次读取一个字节返回,读取完毕会返回-1 - `public int read(byte[] buffer)` : 从字节输入流中读取字节到字节数组中去,返回读取的字节数量,没有字节可读返回-1,**byte中新读取的数据默认是覆盖原数据**,构造String需要设定长度 - `public String(byte[] bytes,int offset,int length)` : 构造新的String - `public long transferTo(OutputStream out) ` : 从输入流中读取所有字节,并按读取的顺序,将字节写入给定的输出流`is.transferTo(os)` +方法: + +* `public int read()`:每次读取一个字节返回,读取完毕会返回-1 +* `public int read(byte[] buffer)`:从字节输入流中读取字节到字节数组中去,返回读取的字节数量,没有字节可读返回 -1,**byte 中新读取的数据默认是覆盖原数据**,构造 String 需要设定长度 +* `public String(byte[] bytes,int offset,int length)`:构造新的 String +* `public long transferTo(OutputStream out) `:从输入流中读取所有字节,并按读取的顺序,将字节写入给定的输出流 ```java public class FileInputStreamDemo01 { public static void main(String[] args) throws Exception { // 1.创建文件对象定位dlei01.txt - File file = new File("Day09Demo/src/dlei01.txt"); + File file = new File("Demo/src/dlei01.txt"); // 2.创建一个字节输入流管道与源文件接通 InputStream is = new FileInputStream(file); // 3.读取一个字节的编号返回,读取完毕返回-1 @@ -7246,14 +7159,14 @@ public class FileInputStreamDemo01 { } ``` -一个一个字节读取英文和数字没有问题。但是一旦读取中文输出无法避免乱码,因为会截断中文的字节。一个一个字节的读取数据,性能也较差,所以**禁止使用上面的方案** +一个一个字节读取英文和数字没有问题,但是读取中文输出无法避免乱码,因为会截断中文的字节。一个一个字节的读取数据,性能也较差,所以**禁止使用上面的方案** 采取下面的方案: ```java public static void main(String[] args) throws Exception { //简化写法,底层实质上创建了File对象 - InputStream is = new FileInputStream("Day09Demo/src/dlei01.txt"); + InputStream is = new FileInputStream("Demo/src/test.txt"); byte[] buffer = new byte[3];//开发中使用byte[1024] int len; while((len = is.read(buffer)) !=-1){ @@ -7265,17 +7178,9 @@ public static void main(String[] args) throws Exception { ``` ```java -//定义一个字节数组与文件的大小刚刚一样大,然后一桶水读取全部字节数据再输出! -//可以避免中文读取输出乱码,但是如果读取的文件过大,会出现内存溢出!! -//字节流并不适合读取文本文件内容输出,读写文件内容建议使用字符流。 -/* - byte[] buffer = new byte[(int) f.length()]; - int len = is.read(buffer); - String rs = new String(buffer); -*/ - -File f = new File("Day09Demo/src/dlei03.txt"); +File f = new File("Demo/src/test.txt"); InputStream is = new FileInputStream(f); +// 读取全部的 byte[] buffer = is.readAllBytes(); String rs = new String(buffer); System.out.println(rs); @@ -7283,35 +7188,39 @@ System.out.println(rs); +**** + + + ##### 字节输出 -FileOutputStream 文件字节输出流: +FileOutputStream 文件字节输出流:以内存为基准,把内存中的数据,按照字节的形式写出到磁盘文件中去 + +构造方法: + +* `public FileOutputStream(File file)`:创建一个字节输出流管道通向目标文件对象 +* `public FileOutputStream(String file) `:创建一个字节输出流管道通向目标文件路径 +* `public FileOutputStream(File file, boolean append)` : 创建一个追加数据的字节输出流管道到目标文件对象 +* `public FileOutputStream(String file, boolean append)` : 创建一个追加数据的字节输出流管道通向目标文件路径 -* 作用:以内存为基准,把内存中的数据,按照字节的形式写出到磁盘文件中去 +API: -* 构造器: - `public FileOutputStream(File file)` : 创建一个字节输出流管道通向目标文件对象 - `public FileOutputStream(String file) ` : 创建一个字节输出流管道通向目标文件路径 - `public FileOutputStream(File file , boolean append)` : 追加数据的字节输出流管道到目标文件对象 - `public FileOutputStream(String file , boolean append)` : 创建一个追加数据的字节输出流管道通向目标文件路径 -* API: - `public void write(int a)` : 写一个字节出去 - `public void write(byte[] buffer)` :写一个字节数组出去 - `public void write(byte[] buffer , int pos , int len)` : 写一个字节数组的一部分出去 - 参数一,字节数组;参数二:起始字节索引位置,参数三:写多少个字节数出去。 +* `public void write(int a)`:写一个字节出去 +* `public void write(byte[] buffer)`:写一个字节数组出去 +* `public void write(byte[] buffer , int pos , int len)`:写一个字节数组的一部分出去,从 pos 位置,写出 len 长度 -* FileOutputStream字节输出流每次启动写数据的时候都会先清空之前的全部数据,重新写入: - `OutputStream os = new FileOutputStream("Day09Demo/out05")` : 覆盖数据管道 - `OutputStream os = new FileOutputStream("Day09Demo/out05" , true)` : 追加数据的管道 +* FileOutputStream 字节输出流每次启动写数据的时候都会先清空之前的全部数据,重新写入: + * `OutputStream os = new FileOutputStream("Demo/out05")`:覆盖数据管道 + * `OutputStream os = new FileOutputStream("Demo/out05" , true)`:追加数据的管道 说明: -* 字节输出流只能写字节出去,字节输出流默认是**覆盖**数据管道。 -* 换行用: **os.write("\r\n".getBytes());** -* 关闭和刷新:刷新流可以继续使用,关闭包含刷新数据但是流就不能使用了! +* 字节输出流只能写字节出去,字节输出流默认是**覆盖**数据管道 +* 换行用:**os.write("\r\n".getBytes())** +* 关闭和刷新:刷新流可以继续使用,关闭包含刷新数据但是流就不能使用了 ```java -OutputStream os = new FileOutputStream("Day09Demo/out05"); +OutputStream os = new FileOutputStream("Demo/out05"); os.write(97);//a os.write('b'); os.write("\r\n".getBytes()); @@ -7323,14 +7232,7 @@ os.close(); ##### 文件复制 -思想:字节是计算机中一切文件的组成,所以字节流适合做一切文件的复制 - -分析步骤: - (1)创建一个字节输入流管道与源文件接通。 - (2)创建一个字节输出流与目标文件接通。 - (3)创建一个字节数组作为桶 - (4)从字节输入流管道中读取数据,写出到字节输出流管道即可。 - (5)关闭资源! +字节是计算机中一切文件的组成,所以字节流适合做一切文件的复制 ```java public class CopyDemo01 { @@ -7339,9 +7241,9 @@ public class CopyDemo01 { OutputStream os = null ; try{ //(1)创建一个字节输入流管道与源文件接通。 - is = new FileInputStream("D:\\seazean\\图片资源\\meinv.jpg"); + is = new FileInputStream("D:\\seazean\\图片资源\\test.jpg"); //(2)创建一个字节输出流与目标文件接通。 - os = new FileOutputStream("D:\\seazean\\meimei.jpg"); + os = new FileOutputStream("D:\\seazean\\test.jpg"); //(3)创建一个字节数组作为桶 byte buffer = new byte[1024]; //(4)从字节输入流管道中读取数据,写出到字节输出流管道即可 @@ -7375,32 +7277,30 @@ public class CopyDemo01 { ##### 字符输入 -FileReader:文件字符输入流 +FileReader:文件字符输入流,以内存为基准,把磁盘文件的数据以字符的形式读入到内存,读取文本文件内容到内存中去 + +构造器: + +* `public FileReader(File file)`:创建一个字符输入流与源文件对象接通。 +* `public FileReader(String filePath)`:创建一个字符输入流与源文件路径接通。 + +方法: + +* `public int read()`:读取一个字符的编号返回,读取完毕返回 -1 +* `public int read(char[] buffer)`:读取一个字符数组,读取多少个就返回多少个,读取完毕返回 -1 + +结论: + +* 字符流一个一个字符的读取文本内容输出,可以解决中文读取输出乱码的问题,适合操作文本文件,但是一个一个字符的读取文本内容性能较差 +* 字符流按照**字符数组循环读取数据**,可以解决中文读取输出乱码的问题,而且性能也较好 - * 作用:以内存为基准,把磁盘文件的数据以字符的形式读入到内存,读取文本文件内容到内存中去。 - * 构造器: - `public FileReader(File file)` : 创建一个字符输入流与源文件对象接通。 - `public FileReader(String filePath)` : 创建一个字符输入流与源文件路径接通。 - * 方法: - `public int read()` : 读取一个字符的编号返回! 读取完毕返回 -1 - `public int read(char[] buffer)` : 读取一个字符数组,读取多少个就返回多少个,读取完毕返回 -1 - * 结论: - 字符流一个一个字符的读取文本内容输出,可以解决中文读取输出乱码的问题,适合操作文本文件。 - 但是:一个一个字符的读取文本内容性能较差!! - 字符流按照**字符数组循环读取数据**,可以解决中文读取输出乱码的问题,而且性能也较好!! - * **字符流不能复制图片,视频等类型的文件**。字符流在读取完了字节数据后并没有直接往目的地写,而是先查编码表,查到对应的数据就将该数据写入目的地。如果查不到,则码表会将一些未知区域中的数据去map这些字节数据,然后写到目的地,这样的话就造成了源数据和目的数据的不一致。 +**字符流不能复制图片,视频等类型的文件**。字符流在读取完了字节数据后并没有直接往目的地写,而是先查编码表,查到对应的数据就将该数据写入目的地。如果查不到,则码表会将一些未知区域中的数据去 map 这些字节数据,然后写到目的地,这样的话就造成了源数据和目的数据的不一致。 ```java public class FileReaderDemo01{//字符 public static void main(String[] args) throws Exception { - // 1.创建一个文件对象定位源文件 - // File f = new File("Day10Demo/src/dlei01.txt"); - // 2.创建一个字符输入流管道与源文件接通 - // Reader fr = new FileReader(f); - // 3.简化写法:创建一个字符输入流管道与源文件路径接通 - Reader fr = new FileReader("Day10Demo/src/dlei01.txt"); - //int code1 = fr.read(); - //System.out.print((char)code1); + // 创建一个字符输入流管道与源文件路径接通 + Reader fr = new FileReader("Demo/src/test.txt"); int ch; while((ch = fr.read()) != -1){ System.out.print((char)ch); @@ -7409,13 +7309,8 @@ public class FileReaderDemo01{//字符 } public class FileReaderDemo02 {//字符数组 public static void main(String[] args) throws Exception { - Reader fr = new FileReader("Day10Demo/src/dlei01.txt"); + Reader fr = new FileReader("Demo/src/test.txt"); - //char[] buffer = new char[3]; - //int len = fr.read(buffer); - //System.out.println("字符数:"+len); - //String rs = new String(buffer,0,len); - //System.out.println(rs); char[] buffer = new char[1024]; int len; while((len = fr.read(buffer)) != -1) { @@ -7427,30 +7322,33 @@ public class FileReaderDemo02 {//字符数组 +*** + + + ##### 字符输出 -FileWriter:文件字符输出流 - -* 作用:以内存为基准,把内存中的数据按照字符的形式写出到磁盘文件中去 -* 构造器: - `public FileWriter(File file)` : 创建一个字符输出流管道通向目标文件对象 - `public FileWriter(String filePath)` : 创建一个字符输出流管道通向目标文件路径 - `public FileWriter(File file,boolean append)` : 创建一个追加数据的字符输出流管道通向文件对象 - `public FileWriter(String filePath,boolean append)` : 创建一个追加数据的字符输出流管道通向目标文件路径 -* 方法: - `public void write(int c)` : 写一个字符出去 - `public void write(String c)` : 写一个字符串出去 - `public void write(char[] buffer)` : 写一个字符数组出去 - `public void write(String c ,int pos ,int len)` : 写字符串的一部分出去 - `public void write(char[] buffer ,int pos ,int len)` : 写字符数组的一部分出去 -* 说明: - 覆盖数据管道:`Writer fw = new FileWriter("Day10Demo/src/dlei03.txt")` - 追加数据管道:`Writer fw = new FileWriter("Day10Demo/src/dlei03.txt",true)` - 换行:fw.write("\r\n"); // 换行 - 读写字符文件数据建议使用字符流 - -```java -Writer fw = new FileWriter("Day10Demo/src/dlei03.txt"); +FileWriter:文件字符输出流,以内存为基准,把内存中的数据按照字符的形式写出到磁盘文件中去 + +构造器: + +* `public FileWriter(File file)`:创建一个字符输出流管道通向目标文件对象(覆盖数据管道) +* `public FileWriter(String filePath)`:创建一个字符输出流管道通向目标文件路径 +* `public FileWriter(File file, boolean append)`:创建一个追加数据的字符输出流管道通向文件对象(追加数据管道) +* `public FileWriter(String filePath, boolean append)`:创建一个追加数据的字符输出流管道通向目标文件路径 + +方法: + +* `public void write(int c)`:写一个字符出去 +* `public void write(char[] buffer)`:写一个字符数组出去 +* `public void write(String c, int pos, int len)`:写字符串的一部分出去 +* `public void write(char[] buffer, int pos, int len)`:写字符数组的一部分出去 +* `fw.write("\r\n")`:换行 + +读写字符文件数据建议使用字符流 + +```java +Writer fw = new FileWriter("Demo/src/test.txt"); fw.write(97); // 字符a fw.write('b'); // 字符b fw.write("Java是最优美的语言!"); @@ -7468,14 +7366,14 @@ fw.close; ##### 基本介绍 -作用:缓冲流可以提高字节流和字符流的读写数据的性能。 +缓冲流可以提高字节流和字符流的读写数据的性能 缓冲流分为四类: -* BufferedInputStream:字节缓冲输入流,可以提高字节输入流读数据的性能。 -* BufferedOutStream: 字节缓冲输出流,可以提高字节输出流写数据的性能。 -* BufferedReader: 字符缓冲输入流,可以提高字符输入流读数据的性能。 -* BufferedWriter: 字符缓冲输出流,可以提高字符输出流写数据的性能。 +* BufferedInputStream:字节缓冲输入流,可以提高字节输入流读数据的性能 +* BufferedOutStream:字节缓冲输出流,可以提高字节输出流写数据的性能 +* BufferedReader:字符缓冲输入流,可以提高字符输入流读数据的性能 +* BufferedWriter:字符缓冲输出流,可以提高字符输出流写数据的性能 @@ -7487,7 +7385,7 @@ fw.close; 字节缓冲输入流:BufferedInputStream -作用:可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道, 提高字节输入流读数据的性能 +作用:可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道,提高字节输入流读数据的性能 构造器:`public BufferedInputStream(InputStream in)` @@ -7497,7 +7395,7 @@ fw.close; public class BufferedInputStreamDemo01 { public static void main(String[] args) throws Exception { // 1.定义一个低级的字节输入流与源文件接通 - InputStream is = new FileInputStream("Day10Demo/src/dlei04.txt"); + InputStream is = new FileInputStream("Demo/src/test.txt"); // 2.把低级的字节输入流包装成一个高级的缓冲字节输入流。 BufferInputStream bis = new BufferInputStream(is); // 3.定义一个字节数组按照循环读取。 @@ -7525,13 +7423,13 @@ public class BufferedInputStreamDemo01 { 构造器:`public BufferedOutputStream(OutputStream os)` -原理:缓冲字节输出流自带了8KB缓冲池,数据就直接写入到缓冲池中去,性能极高了 +原理:缓冲字节输出流自带了 8KB 缓冲池,数据就直接写入到缓冲池中去,性能提高了 ```java public class BufferedOutputStreamDemo02 { public static void main(String[] args) throws Exception { // 1.写一个原始的字节输出流 - OutputStream os = new FileOutputStream("Day10Demo/src/dlei05.txt"); + OutputStream os = new FileOutputStream("Demo/src/test.txt"); // 2.把低级的字节输出流包装成一个高级的缓冲字节输出流 BufferedOutputStream bos = new BufferedOutputStream(os); // 3.写数据出去 @@ -7548,14 +7446,14 @@ public class BufferedOutputStreamDemo02 { ##### 字节流性能 -利用字节流的复制统计各种写法形式下缓冲流的性能执行情况。 +利用字节流的复制统计各种写法形式下缓冲流的性能执行情况 复制流: -* 使用低级的字节流按照一个一个字节的形式复制文件。 -* 使用低级的字节流按照一个一个字节数组的形式复制文件。 -* 使用高级的缓冲字节流按照一个一个字节的形式复制文件。 -* 使用高级的缓冲字节流按照一个一个字节数组的形式复制文件。 +* 使用低级的字节流按照一个一个字节的形式复制文件 +* 使用低级的字节流按照一个一个字节数组的形式复制文件 +* 使用高级的缓冲字节流按照一个一个字节的形式复制文件 +* 使用高级的缓冲字节流按照一个一个字节数组的形式复制文件 高级的缓冲字节流按照一个一个字节数组的形式复制文件,性能最高,建议使用 @@ -7573,14 +7471,14 @@ public class BufferedOutputStreamDemo02 { 构造器:`public BufferedReader(Reader reader)` -原理:缓冲字符输入流默认会有一个8K的字符缓冲池,可以提高读字符的性能 +原理:缓冲字符输入流默认会有一个 8K 的字符缓冲池,可以提高读字符的性能 -按照行读取数据的功能:`public String readLine()` 读取一行数据返回,读取完毕返回null +按照行读取数据的功能:`public String readLine()` 读取一行数据返回,读取完毕返回 null ```java public static void main(String[] args) throws Exception { // 1.定义一个原始的字符输入流读取源文件 - Reader fr = new FileReader("Day10Demo/src/dlei06.txt"); + Reader fr = new FileReader("Demo/src/test.txt"); // 2.把低级的字符输入流管道包装成一个高级的缓冲字符输入流管道 BufferedReader br = new BufferedReader(fr); // 定义一个字符串变量存储每行数据 @@ -7611,13 +7509,13 @@ public static void main(String[] args) throws Exception { 构造器:`public BufferedWriter(Writer writer)` - 原理:高级的字符缓冲输出流多了一个8k的字符缓冲池,写数据性能极大提高了 + 原理:高级的字符缓冲输出流多了一个 8K 的字符缓冲池,写数据性能极大提高了 字符缓冲输出流多了一个换行的特有功能:`public void newLine()` **新建一行** ```java public static void main(String[] args) throws Exception { - Writer fw = new FileWriter("Day10Demo/src/dlei07.txt",true);//追加 + Writer fw = new FileWriter("Demo/src/test.txt",true);//追加 BufferedWriter bw = new BufferedWriter(fw); bw.write("我爱学习Java"); @@ -7667,8 +7565,8 @@ GBK GBK 不乱码! UTF-8 GBK 乱码! ``` -如果代码编码和读取的文件编码一致,字符流读取的时候不会乱码。 -如果代码编码和读取的文件编码不一致,字符流读取的时候会乱码。 +* 如果代码编码和读取的文件编码一致,字符流读取的时候不会乱码 +* 如果代码编码和读取的文件编码不一致,字符流读取的时候会乱码 @@ -7684,8 +7582,8 @@ UTF-8 GBK 乱码! 构造器: -* `public InputStreamReader(InputStream is)` : 使用当前代码默认编码 UTF-8 转换成字符流 -* `public InputStreamReader(InputStream is, String charset)` : 指定编码把字节流转换成字符流 +* `public InputStreamReader(InputStream is)`:使用当前代码默认编码 UTF-8 转换成字符流 +* `public InputStreamReader(InputStream is, String charset)`:指定编码把字节流转换成字符流 ```java public class InputStreamReaderDemo{ @@ -7719,11 +7617,11 @@ public class InputStreamReaderDemo{ 构造器: -* `public OutputStreamWriter(OutputStream os)` : 用默认编码 UTF-8 把字节输出流转换成字符输出流 -* `public OutputStreamWriter(OutputStream os, String charset)` : 指定编码把字节输出流转换成 +* `public OutputStreamWriter(OutputStream os)`:用默认编码 UTF-8 把字节输出流转换成字符输出流 +* `public OutputStreamWriter(OutputStream os, String charset)`:指定编码把字节输出流转换成 ```Java -OutputStream os = new FileOutputStream("Day10Demo/src/dlei07.txt"); +OutputStream os = new FileOutputStream("Demo/src/test.txt"); OutputStreamWriter osw = new OutputStreamWriter(os,"GBK"); osw.write("我在学习Java"); osw.close(); @@ -7739,11 +7637,11 @@ osw.close(); ##### 基本介绍 -对象序列化:把Java对象转换成字节序列的过程,将对象写入到IO流中。 对象 => 文件中 +对象序列化:把 Java 对象转换成字节序列的过程,将对象写入到 IO 流中。对象 => 文件中 -对象反序列化:把字节序列恢复为Java对象的过程,从IO流中恢复对象。 文件中 => 对象 +对象反序列化:把字节序列恢复为 Java 对象的过程,从 IO 流中恢复对象,文件中 => 对象 -transient 关键字修饰的成员变量,将不参与序列化! +transient 关键字修饰的成员变量,将不参与序列化 @@ -7755,7 +7653,7 @@ transient 关键字修饰的成员变量,将不参与序列化! 对象序列化流(对象字节输出流):ObjectOutputStream -作用:把内存中的Java对象数据保存到文件中去 +作用:把内存中的 Java 对象数据保存到文件中去 构造器:`public ObjectOutputStream(OutputStream out)` @@ -7769,7 +7667,7 @@ public class SerializeDemo01 { // 1.创建User用户对象 User user = new User("seazean","980823","七十一"); // 2.创建低级的字节输出流通向目标文件 - OutputStream os = new FileOutputStream("Day10Demo/src/obj.dat"); + OutputStream os = new FileOutputStream("Demo/src/obj.dat"); // 3.把低级的字节输出流包装成高级的对象字节输出流 ObjectOutputStream ObjectOutputStream oos = new ObjectOutputStream(os); // 4.通过对象字节输出流序列化对象: @@ -7787,7 +7685,7 @@ class User implements Serializable { private String loginName; private transient String passWord; private String userName; - ///get+set + // get+set } ``` @@ -7812,19 +7710,20 @@ byte[] bytes = bos.toByteArray(); 对象反序列化(对象字节输入流):ObjectInputStream -作用:读取序列化的对象文件恢复到Java对象中 +作用:读取序列化的对象文件恢复到 Java 对象中 构造器:`public ObjectInputStream(InputStream is)` 方法:`public final Object readObject()` 序列化版本号:`private static final long serialVersionUID = 2L` -说明:序列化使用的版本号和反序列化使用的版本号一致才可以正常反序列化,否则报错 + +注意:序列化使用的版本号和反序列化使用的版本号一致才可以正常反序列化,否则报错 ```java public class SerializeDemo02 { public static void main(String[] args) throws Exception { - InputStream is = new FileInputStream("Day10Demo/src/obj.dat"); + InputStream is = new FileInputStream("Demo/src/obj.dat"); ObjectInputStream ois = new ObjectInputStream(is); User user = (User)ois.readObject();//反序列化 System.out.println(user); @@ -7859,15 +7758,14 @@ class User implements Serializable { * `public PrintStream(OutputStream os)` * `public PrintStream(String filepath)` -System类: +System 类: * `public static void setOut(PrintStream out)`:让系统的输出流向打印流 ```java public class PrintStreamDemo01 { public static void main(String[] args) throws Exception { - PrintStream ps = new PrintStream("Day10Demo/src/dlei.txt"); - //PrintWriter pw = new PrintWriter("Day10Demo/src/dlei08.txt"); + PrintStream ps = new PrintStream("Demo/src/test.txt"); ps.println(任何类型的数据); ps.print(不换行); ps.write("我爱你".getBytes()); @@ -7877,7 +7775,7 @@ public class PrintStreamDemo01 { public class PrintStreamDemo02 { public static void main(String[] args) throws Exception { System.out.println("==seazean0=="); - PrintStream ps = new PrintStream("Day10Demo/src/log.txt"); + PrintStream ps = new PrintStream("Demo/src/log.txt"); System.setOut(ps); // 让系统的输出流向打印流 //不输出在控制台,输出到文件里 System.out.println("==seazean1=="); @@ -7937,23 +7835,23 @@ try( ### Properties -Properties:属性集对象。就是一个Map集合,一个键值对集合 +Properties:属性集对象。就是一个 Map 集合,一个键值对集合 -核心作用:Properties代表的是一个属性文件,可以把键值对数据存入到一个属性文件 +核心作用:Properties 代表的是一个属性文件,可以把键值对数据存入到一个属性文件 -属性文件:后缀是.properties结尾的文件,里面的内容都是 key=value +属性文件:后缀是 `.properties` 结尾的文件,里面的内容都是 key=value -Properties方法: +Properties 方法: -| 方法名 | 说明 | -| --------------------------------------------------- | ------------------------------------------- | -| public Object setProperty(String key, String value) | 设置集合的键和值,底层调用Hashtable方法 put | -| public String getProperty(String key) | 使用此属性列表中指定的键搜索属性 | -| public Set stringPropertyNames() | 所有键的名称的集合 | -| public synchronized void load(Reader r) | 从输入字符流读取属性列表(键和元素对) | -| public synchronized void load(InputStream inStream) | 加载属性文件的数据到属性集对象中去 | -| public void store(Writer w, String comments) | 将此属性列表(键和元素对)写入 Properties表 | -| public void store(OutputStream os, String comments) | 保存数据到属性文件中去 | +| 方法名 | 说明 | +| -------------------------------------------- | --------------------------------------------- | +| Object setProperty(String key, String value) | 设置集合的键和值,底层调用 Hashtable 方法 put | +| String getProperty(String key) | 使用此属性列表中指定的键搜索属性 | +| Set stringPropertyNames() | 所有键的名称的集合 | +| synchronized void load(Reader r) | 从输入字符流读取属性列表(键和元素对) | +| synchronized void load(InputStream inStream) | 加载属性文件的数据到属性集对象中去 | +| void store(Writer w, String comments) | 将此属性列表(键和元素对)写入 Properties 表 | +| void store(OutputStream os, String comments) | 保存数据到属性文件中去 | ````java public class PropertiesDemo01 { @@ -7962,7 +7860,7 @@ public class PropertiesDemo01 { Properties properties = new Properties();//{} properties.setProperty("admin" , "123456"); // b.把属性集对象的数据存入到属性文件中去(重点) - OutputStream os = new FileOutputStream("Day10Demo/src/users.properties"); + OutputStream os = new FileOutputStream("Demo/src/users.properties"); properties.store(os,"i am very happy!!我保存了用户数据!"); //参数一:被保存数据的输出管道 //参数二:保存心得。就是对象保存的数据进行解释说明! @@ -7974,7 +7872,7 @@ public class PropertiesDemo01 { public class PropertiesDemo02 { public static void main(String[] args) throws Exception { Properties properties = new Properties();//底层基于map集合 - properties.load(new FileInputStream("Day10Demo/src/users.properties")); + properties.load(new FileInputStream("Demo/src/users.properties")); System.out.println(properties); System.out.println(properties.getProperty("admin")); @@ -7998,13 +7896,15 @@ public class PropertiesDemo02 { RandomAccessFile 类:该类的实例支持读取和写入随机访问文件 构造器: -RandomAccessFile(File file, String mode):创建随机访问文件流,从File参数指定的文件读取,可选择写入 -RandomAccessFile(String name, String mode):创建随机访问文件流,从指定名称文件读取,可选择写入文件 + +* `RandomAccessFile(File file, String mode)`:创建随机访问文件流,从 File 参数指定的文件读取,可选择写入 +* `RandomAccessFile(String name, String mode)`:创建随机访问文件流,从指定名称文件读取,可选择写入文件 常用方法: -`public void seek(long pos)` : 设置文件指针偏移,从该文件开头测量,发生下一次读取或写入(插入+覆盖) -`public void write(byte[] b)` : 从指定的字节数组写入 b.length个字节到该文件 -`public int read(byte[] b)` : 从该文件读取最多b.length个字节的数据到字节数组 + +* `public void seek(long pos)`:设置文件指针偏移,从该文件开头测量,发生下一次读取或写入(插入+覆盖) +* `public void write(byte[] b)`:从指定的字节数组写入 b.length 个字节到该文件 +* `public int read(byte[] b)`:从该文件读取最多 b.length 个字节的数据到字节数组 ```java public static void main(String[] args) throws Exception { @@ -8024,16 +7924,16 @@ public static void main(String[] args) throws Exception { ### Commons -commons-io 是apache开源基金组织提供的一组有关IO操作的类库,可以挺提高IO功能开发的效率。 +commons-io 是 apache 提供的一组有关 IO 操作的类库,可以挺提高 IO 功能开发的效率。 commons-io 工具包提供了很多有关 IO 操作的类: -| 包 | 功能描述 | -| ----------------------------------- | :------------------------------------------- | -| org.apache.commons.io | 有关Streams、Readers、Writers、Files的工具类 | -| org.apache.commons.io.input | 输入流相关的实现类,包含Reader和InputStream | -| org.apache.commons.io.output | 输出流相关的实现类,包含Writer和OutputStream | -| org.apache.commons.io.serialization | 序列化相关的类 | +| 包 | 功能描述 | +| ----------------------------------- | :---------------------------------------------- | +| org.apache.commons.io | 有关 Streams、Readers、Writers、Files 的工具类 | +| org.apache.commons.io.input | 输入流相关的实现类,包含 Reader 和 InputStream | +| org.apache.commons.io.output | 输出流相关的实现类,包含 Writer 和 OutputStream | +| org.apache.commons.io.serialization | 序列化相关的类 | IOUtils 和 FileUtils 可以方便的复制文件和文件夹 @@ -8041,18 +7941,18 @@ IOUtils 和 FileUtils 可以方便的复制文件和文件夹 public class CommonsIODemo01 { public static void main(String[] args) throws Exception { // 1.完成文件复制! - IOUtils.copy(new FileInputStream("Day13Demo/src/books.xml"), - new FileOutputStream("Day13Demo/new.xml")); + IOUtils.copy(new FileInputStream("Demo/src/books.xml"), + new FileOutputStream("Demo/new.xml")); // 2.完成文件复制到某个文件夹下! - FileUtils.copyFileToDirectory(new File("Day13Demo/src/books.xml"), + FileUtils.copyFileToDirectory(new File("Demo/src/books.xml"), new File("D:/it")); // 3.完成文件夹复制到某个文件夹下! FileUtils.copyDirectoryToDirectory(new File("D:\\it\\图片服务器") , new File("D:\\")); // Java从1.7开始提供了一些nio, 自己也有一行代码完成复制的技术。 - Files.copy(Paths.get("Day13Demo/src/books.xml") - , new FileOutputStream("Day13Demo/new11.txt")); + Files.copy(Paths.get("Demo/src/books.xml") + , new FileOutputStream("Demo/new11.txt")); } } ``` @@ -8065,19 +7965,17 @@ public class CommonsIODemo01 { + + ## 反射 ### 测试框架 -> 单元测试是指程序员写的测试代码给自己的类中的方法进行预期正确性的验证。 -> 单元测试一旦写好了这些测试代码,就可以一直使用,可以实现一定程度上的自动化测试。 +单元测试的经典框架:Junit,是 Java 语言编写的第三方单元测试框架 -单元测试的经典框架:Junit - -* Junit : 是 Java 语言编写的第三方单元测试框架,可以帮助我们方便快速的测试我们代码的正确性。 -* 单元测试: - * 单元:在 Java 中,一个类就是一个单元 - * 单元测试:Junit 编写的一小段代码,用来对某个类中的某个方法进行功能测试或业务逻辑测试 +单元测试: +* 单元:在 Java 中,一个类就是一个单元 +* 单元测试:Junit 编写的一小段代码,用来对某个类中的某个方法进行功能测试或业务逻辑测试 Junit 单元测试框架的作用: @@ -8086,14 +7984,14 @@ Junit 单元测试框架的作用: 测试方法注意事项:**必须是 public 修饰的,没有返回值,没有参数,使用注解@Test修饰** -Junit常用注解(Junit 4.xxxx版本),@Test 测试方法: +Junit常用注解(Junit 4.xxxx 版本),@Test 测试方法: * @Before:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次 * @After:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次 * @BeforeClass:用来静态修饰方法,该方法会在所有测试方法之前**只**执行一次 * @AfterClass:用来静态修饰方法,该方法会在所有测试方法之后**只**执行一次 -Junit常用注解(Junit5.xxxx版本),@Test 测试方法: +Junit 常用注解(Junit5.xxxx 版本),@Test 测试方法: * @BeforeEach:用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次 * @AfterEach:用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次 @@ -8180,7 +8078,7 @@ public class UserServiceTest { 核心思想:在运行时获取类编译后的字节码文件对象,然后解析类中的全部成分 -反射提供了一个Class类型:HelloWorld.java → javac → HelloWorld.class +反射提供了一个 Class 类型:HelloWorld.java → javac → HelloWorld.class * `Class c = HelloWorld.class` @@ -8188,16 +8086,16 @@ public class UserServiceTest { 作用:可以在运行时得到一个类的全部成分然后操作,破坏封装性,也可以破坏泛型的约束性。 -**反射的优点:** +反射的优点: - 可扩展性:应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类 - 类浏览器和可视化开发环境:一个类浏览器需要可以枚举类的成员,可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码 -- 调试器和测试工具: 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率 +- 调试器和测试工具: 调试器需要能够检查一个类里的私有成员,测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率 -**反射的缺点:** +反射的缺点: - 性能开销:反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化,反射操作的效率要比那些非射操作低得多,应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。 -- 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行,如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了 +- 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行,如果一个程序必须在有安全限制的环境中运行 - 内部暴露:由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化 @@ -8260,9 +8158,9 @@ class Student{} 获取构造器的 API: * Constructor getConstructor(Class... parameterTypes):根据参数匹配获取某个构造器,只能拿 public 修饰的构造器 -* **Constructor getDeclaredConstructor(Class... parameterTypes)**:根据参数匹配获取某个构造器,只要申明就可以定位,不关心权限修饰符 +* Constructor getDeclaredConstructor(Class... parameterTypes):根据参数匹配获取某个构造器,只要申明就可以定位,不关心权限修饰符 * Constructor[] getConstructors():获取所有的构造器,只能拿 public 修饰的构造器 -* **Constructor[] getDeclaredConstructors()**:获取所有构造器,只要申明就可以定位,不关心权限修饰符 +* Constructor[] getDeclaredConstructors():获取所有构造器,只要申明就可以定位,不关心权限修饰符 Constructor 的常用 API: @@ -8356,12 +8254,12 @@ public class TestStudent02 { #### 获取变量 -获取Field成员变量API: +获取 Field 成员变量 API: -* Field getField(String name) : 根据成员变量名获得对应 Field 对象,只能获得 public 修饰 -* Field getDeclaredField(String name) : 根据成员变量名获得对应 Field 对象,所有申明的变量 -* Field[] getFields() : 获得所有的成员变量对应的Field对象,只能获得 public 的 -* Field[] getDeclaredFields() : 获得所有的成员变量对应的 Field 对象,只要申明了就可以得到 +* Field getField(String name):根据成员变量名获得对应 Field 对象,只能获得 public 修饰 +* Field getDeclaredField(String name):根据成员变量名获得对应 Field 对象,所有申明的变量 +* Field[] getFields():获得所有的成员变量对应的 Field 对象,只能获得 public 的 +* Field[] getDeclaredFields():获得所有的成员变量对应的 Field 对象,只要申明了就可以得到 Field 的方法:给成员变量赋值和取值 @@ -8440,6 +8338,10 @@ public class FieldDemo02 { +*** + + + #### 获取方法 获取 Method 方法 API: @@ -8546,10 +8448,9 @@ public class ReflectDemo { 注解:类的组成部分,可以给类携带一些额外的信息,提供一种安全的类似注释标记的机制,用来将任何信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联 -* 注解是 JDK1.5 的新特性 * 注解是给编译器或 JVM 看的,编译器或 JVM 可以根据注解来完成对应的功能 * 注解类似修饰符,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中 -* 父类中的注解是不能被子类继承的 +* **父类中的注解是不能被子类继承的** 注解作用: @@ -8665,29 +8566,25 @@ public class AnnotationDemo01{ 元注解有四个: -* @Target:约束自定义注解可以标记的范围,默认值为任何元素,表示该注解用于什么地方 - - 可使用的值定义在ElementType枚举类中: +* @Target:约束自定义注解可以标记的范围,默认值为任何元素,表示该注解用于什么地方,可用值定义在 ElementType 类中: - `ElementType.CONSTRUCTOR`:用于描述构造器 - - `ElementType.FIELD`:成员变量、对象、属性(包括enum实例) + - `ElementType.FIELD`:成员变量、对象、属性(包括 enum 实例) - `ElementType.LOCAL_VARIABLE`:用于描述局部变量 - `ElementType.METHOD`:用于描述方法 - `ElementType.PACKAGE`:用于描述包 - `ElementType.PARAMETER`:用于描述参数 - - `ElementType.TYPE`:用于描述类、接口(包括注解类型) 或enum声明 - -* @Retention:定义该注解的生命周期,申明注解的作用范围:编译时,运行时 - - 可使用的值定义在RetentionPolicy枚举类中: + - `ElementType.TYPE`:用于描述类、接口(包括注解类型)或 enum 声明 + +* @Retention:定义该注解的生命周期,申明注解的作用范围:编译时,运行时,可使用的值定义在 RetentionPolicy 枚举类中: - - `RetentionPolicy.SOURCE`:在编译阶段丢弃,这些注解在编译结束之后就不再有任何意义,只作用在源码阶段,生成的字节码文件中不存在。`@Override`, `@SuppressWarnings`都属于这类注解 + - `RetentionPolicy.SOURCE`:在编译阶段丢弃,这些注解在编译结束之后就不再有任何意义,只作用在源码阶段,生成的字节码文件中不存在,`@Override`、`@SuppressWarnings` 都属于这类注解 - `RetentionPolicy.CLASS`:在类加载时丢弃,在字节码文件的处理中有用,运行阶段不存在,默认值 - `RetentionPolicy.RUNTIME` : 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息,自定义的注解通常使用这种方式 - + * @Inherited:表示修饰的自定义注解可以被子类继承 -* @Documented:表示是否将自定义的注解信息添加在 java 文档中 +* @Documented:表示是否将自定义的注解信息添加在 Java 文档中 ```java public class AnnotationDemo01{ @@ -8816,10 +8713,14 @@ public class TestDemo{ + + **** + + ## XML ### 概述 @@ -8966,11 +8867,11 @@ XML 文件中常见的组成元素有:文档声明、元素、属性、注释、 #### DTD -##### DTD定义 +##### DTD 定义 -DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XML 文档中出现的元素、这些元素出现的次序、它们如何相互嵌套以及XML文档结构的其它详细信息。 +DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XML 文档中出现的元素、这些元素出现的次序、它们如何相互嵌套以及 XML 文档结构的其它详细信息。 -##### DTD规则 +DTD 规则: * 约束元素的嵌套层级 @@ -9066,7 +8967,7 @@ DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XM -##### DTD引入 +##### DTD 引入 * 引入本地 dtd @@ -9142,7 +9043,7 @@ DTD 是文档类型定义(Document Type Definition)。DTD 可以定义在 XM -##### DTD实现 +##### DTD 实现 persondtd.dtd 文件 @@ -9179,13 +9080,13 @@ persondtd.dtd 文件 #### Schema -##### XSD定义 +##### XSD 定义 1. Schema 语言也可作为 XSD(XML Schema Definition) -2. Schema 约束文件本身也是一个 xml 文件,符合 xml 的语法,这个文件的后缀名 .xsd -3. 一个 xml 中可以引用多个 Schema 约束文件,多个 Schema 使用名称空间区分(名称空间类似于 Java 包名) +2. Schema 约束文件本身也是一个 XML 文件,符合 XML 的语法,这个文件的后缀名 .xsd +3. 一个 XML 中可以引用多个 Schema 约束文件,多个 Schema 使用名称空间区分(名称空间类似于 Java 包名) 4. dtd 里面元素类型的取值比较单一常见的是 PCDATA 类型,但是在 Schema 里面可以支持很多个数据类型 -5. **Schema 文件约束 xml 文件的同时也被别的文件约束着** +5. **Schema 文件约束 XML 文件的同时也被别的文件约束着** @@ -9193,7 +9094,7 @@ persondtd.dtd 文件 -##### XSD规则 +##### XSD 规则 1. 创建一个文件,这个文件的后缀名为 .xsd 2. 定义文档声明 @@ -9244,7 +9145,7 @@ person.xsd -##### XSD引入 +##### XSD 引入 1. 在根标签上定义属性 xmlns="http://www.w3.org/2001/XMLSchema-instance" 2. **通过 xmlns 引入约束文件的名称空间** @@ -9273,7 +9174,7 @@ person.xsd -##### XSD属性 +##### XSD 属性 ```scheme @@ -9339,12 +9240,12 @@ DOM(Document Object Model):文档对象模型,把文档的各个组成 Dom4J 实现: * Dom4J 解析器构造方法:`SAXReader saxReader = new SAXReader()` -* SAXReader 常用API: +* SAXReader 常用 API: * `public Document read(File file)`:Reads a Document from the given File * `public Document read(InputStream in)`:Reads a Document from the given stream using SAX -* Java Class 类API: +* Java Class 类 API: * `public InputStream getResourceAsStream(String path)`:加载文件成为一个字节输入流返回 @@ -9401,6 +9302,10 @@ public class Dom4JDemo { +**** + + + #### 子元素 Element 元素的 API: @@ -9620,92 +9525,126 @@ public class XPathDemo { +**** -*** +## SDP +### 单例模式 -# JVM +#### 基本介绍 -## JVM概述 +创建型模式的主要关注点是怎样创建对象,将对象的创建与使用分离,降低系统的耦合度,使用者不需要关注对象的创建细节 -### 基本介绍 +创建型模式分为:单例模式、工厂方法模式、抽象工程模式、原型模式、建造者模式 -JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身是一个虚拟计算机,直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作 +单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,提供了一种创建对象的最佳方式 -特点: +单例设计模式分类两种: -* Java 虚拟机基于**二进制字节码**执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成 -* JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的**跨平台性** +* 饿汉式:类加载就会导致该单实例对象被创建 -Java 代码执行流程:java程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux) +* 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建 -JVM 结构: - -JVM、JRE、JDK 对比: +*** - +#### 饿汉式 -参考书籍:https://book.douban.com/subject/34907497/ +饿汉式在类加载的过程导致该单实例对象被创建,**虚拟机会保证类加载的线程安全**,但是如果只是为了加载该类不需要实例,则会造成内存的浪费 -参考视频:https://www.bilibili.com/video/BV1PJ411n7xZ +* 静态变量的方式: -参考视频:https://www.bilibili.com/video/BV1yE411Z7AP + ```java + public final class Singleton { + // 私有构造方法 + private Singleton() {} + // 在成员位置创建该类的对象 + private static final Singleton instance = new Singleton(); + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return instance; + } + + // 解决序列化问题 + protected Object readResolve() { + return INSTANCE; + } + } + ``` + * 加 final 修饰,所以不会被子类继承,防止子类中不适当的行为覆盖父类的方法,破坏了单例 + * 防止反序列化破坏单例的方式: -*** + * 对单例声明 transient,然后实现 readObject(ObjectInputStream in) 方法,复用原来的单例 + 条件:访问权限为 private/protected、返回值必须是 Object、异常可以不抛 + * 实现 readResolve() 方法,当 JVM 从内存中反序列化地组装一个新对象,就会自动调用 readResolve 方法返回原来单例 -### 架构模型 + * 构造方法设置为私有,防止其他类无限创建对象,但是不能防止反射破坏 -Java 编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计,Java 的指令都是根据栈来设计的,不同平台 CPU 架构不同,所以不能设计为基于寄存器架构 + * 静态变量初始化在类加载时完成,由 JVM 保证线程安全,能保证单例对象创建时的安全 -* 基于栈式架构的特点: - * 设计和实现简单,适用于资源受限的系统 - * 使用零地址指令方式分配,执行过程依赖操作栈,指令集更小,编译器容易实现 - * 零地址指令:机器指令的一种,是指令系统中的一种不设地址字段的指令,只有操作码而没有地址码。这种指令有两种情况:一是无需操作数,另一种是操作数为默认的(隐含的),默认为操作数在寄存器(ACC)中,指令可直接访问寄存器 - * 一地址指令:一个操作码对应一个地址码,通过地址码寻找操作数 - * 不需要硬件的支持,可移植性更好,更好实现跨平台 -* 基于寄存器架构的特点: - * 需要硬件的支持,可移植性差 - * 性能更好,执行更高效,寄存器比内存快 - * 以一地址指令、二地址指令、三地址指令为主 + * 提供静态方法而不是直接将 INSTANCE 设置为 public,体现了更好的封装性、提供泛型支持、可以改进成懒汉单例设计 +* 静态代码块的方式: + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + + // 在成员位置创建该类的对象 + private static Singleton instance; + static { + instance = new Singleton(); + } + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return instance; + } + } + ``` -*** +* 枚举方式:枚举类型是所用单例实现中**唯一一种不会被破坏**的单例实现模式 + ```java + public enum Singleton { + INSTANCE; + public void doSomething() { + System.out.println("doSomething"); + } + } + public static void main(String[] args) { + Singleton.INSTANCE.doSomething(); + } + ``` + * 问题1:枚举单例是如何限制实例个数的?每个枚举项都是一个实例,是一个静态成员变量 + * 问题2:枚举单例在创建时是否有并发问题?否 + * 问题3:枚举单例能否被反射破坏单例?否,反射创建对象时判断是枚举类型就直接抛出异常 + * 问题4:枚举单例能否被反序列化破坏单例?否 + * 问题5:枚举单例属于懒汉式还是饿汉式?**饿汉式** + * 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做?添加构造方法 -### 生命周期 + 反编译结果: -JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡。 + ```java + public final class Singleton extends java.lang.Enum { // Enum实现序列化接口 + public static final Singleton INSTANCE = new Singleton(); + } + ``` -- **启动**:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点 -- **运行**: - - - main() 方法是一个程序的初始起点,任何线程均可由在此处启动 - - 在 JVM 内部有两种线程类型,分别为:用户线程和守护线程,**JVM 使用的是守护线程,main() 和其他线程使用的是用户线程**,守护线程会随着用户线程的结束而结束 - - 执行一个 Java 程序时,真真正正在执行的是一个 Java 虚拟机的进程 - - JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多 - - Server 模式启动的 JVM 采用的是重量级的虚拟机,对程序采用了更多的优化;Client 模式启动的 JVM 采用的是轻量级的虚拟机 -- **死亡**: - - 当程序中的用户线程都中止,JVM 才会退出 - - 程序正常执行结束、程序异常或错误而异常终止、操作系统错误导致终止 - - 线程调用 Runtime 类 halt 方法或 System 类 exit 方法,并且 java 安全管理器允许这次 exit 或 halt 操作 - - @@ -9713,36 +9652,78 @@ JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡。 +#### 懒汉式 +* 线程不安全 -## 内存结构 - -### 内存概述 - -内存结构是 JVM 中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区 + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + + // 在成员位置创建该类的对象 + private static Singleton instance; + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + if(instance == null) { + // 多线程环境,会出现线程安全问题,可能多个线程同时进入这里 + instance = new Singleton(); + } + return instance; + } + } + ``` -JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行 +* 双端检锁机制 -* Java1.8 以前的内存结构图: - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java7内存结构图.png) + 在多线程的情况下,可能会出现空指针问题,出现问题的原因是 JVM 在实例化对象的时候会进行优化和指令重排序操作,所以需要使用 `volatile` 关键字 -* Java1.8 之后的内存结果图: + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + private static volatile Singleton instance; + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + // 第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例 + if(instance == null) { + synchronized (Singleton.class) { + // 抢到锁之后再次判断是否为null + if(instance == null) { + instance = new Singleton(); + } + } + } + return instance; + } + } + ``` - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java8内存结构图.png) +* 静态内部类方式 -线程运行诊断: + ```java + public class Singleton { + // 私有构造方法 + private Singleton() {} + + private static class SingletonHolder { + private static final Singleton INSTANCE = new Singleton(); + } + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return SingletonHolder.INSTANCE; + } + } + ``` -* 定位:jps 定位进程 id -* jstack 进程 id:用于打印出给定的 java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息 + * 内部类属于懒汉式,类加载本身就是懒惰的,首次调用时加载,然后对单例进行初始化 -常见OOM错误: + 类加载的时候方法不会被调用,所以不会触发 getInstance 方法调用 invokestatic 指令对内部类进行加载;加载的时候字节码常量池会被加入类的运行时常量池,解析工作是将常量池中的符号引用解析成直接引用,但是解析过程不一定非得在类加载时完成,可以延迟到运行时进行,所以静态内部类实现单例会**延迟加载** -* java.lang.StackOverflowError -* java.lang.OutOfMemoryError:java heap space -* java.lang.OutOfMemoryError:GC overhead limit exceeded -* java.lang.OutOfMemoryError:Direct buffer memory -* java.lang.OutOfMemoryError:unable to create new native thread -* java.lang.OutOfMemoryError:Metaspace + * 没有线程安全问题,静态变量初始化在类加载时完成,由 JVM 保证线程安全 @@ -9750,47 +9731,100 @@ JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理 -### JVM内存 - -#### 虚拟机栈 - -##### Java栈 - -Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所需要的内存 - -* 每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(**一个方法一个栈帧**) +#### 破坏单例 -* Java 虚拟机规范允许 **Java 栈的大小是动态的或者是固定不变的** +##### 反序列化 -* 虚拟机栈是**每个线程私有的**,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程 +将单例对象序列化再反序列化,对象从内存反序列化到程序中会重新创建一个对象,通过反序列化得到的对象是不同的对象,而且得到的对象不是通过构造器得到的,**反序列化得到的对象不执行构造器** -* 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,每个栈帧中存储着: +* Singleton - * 局部变量表:存储方法里的java基本数据类型以及对象的引用 - * 动态链接:也叫指向运行时常量池的方法引用 - * 方法返回地址:方法正常退出或者异常退出的定义 - * 操作数栈或表达式栈和其他一些附加信息 + ```java + public class Singleton implements Serializable { //实现序列化接口 + // 私有构造方法 + private Singleton() {} + private static class SingletonHolder { + private static final Singleton INSTANCE = new Singleton(); + } - - -设置栈内存大小:`-Xss size` `-Xss 1024k` + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + return SingletonHolder.INSTANCE; + } + } + ``` -* 在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M +* 序列化 -虚拟机栈特点: + ```java + public class Test { + public static void main(String[] args) throws Exception { + //往文件中写对象 + //writeObject2File(); + //从文件中读取对象 + Singleton s1 = readObjectFromFile(); + Singleton s2 = readObjectFromFile(); + //判断两个反序列化后的对象是否是同一个对象 + System.out.println(s1 == s2); + } + + private static Singleton readObjectFromFile() throws Exception { + //创建对象输入流对象 + ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C://a.txt")); + //第一个读取Singleton对象 + Singleton instance = (Singleton) ois.readObject(); + return instance; + } + + public static void writeObject2File() throws Exception { + //获取Singleton类的对象 + Singleton instance = Singleton.getInstance(); + //创建对象输出流 + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C://a.txt")); + //将instance对象写出到文件中 + oos.writeObject(instance); + } + } + ``` -* 栈内存**不需要进行GC**,方法开始执行的时候会进栈,方法调用后自动弹栈,相当于清空了数据 +* 解决方法: -* 栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大) + 在 Singleton 类中添加 `readResolve()` 方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新创建的对象 -* 方法内的局部变量是否**线程安全**: - * 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的(逃逸分析) - * 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全 + ```java + private Object readResolve() { + return SingletonHolder.INSTANCE; + } + ``` -异常: + ObjectInputStream 类源码分析: -* 栈帧过多导致栈内存溢出 (超过了栈的容量),会抛出 OutOfMemoryError 异常 -* 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常 + ```java + public final Object readObject() throws IOException, ClassNotFoundException{ + //... + Object obj = readObject0(false);//重点查看readObject0方法 + } + + private Object readObject0(boolean unshared) throws IOException { + try { + switch (tc) { + case TC_OBJECT: + return checkResolve(readOrdinaryObject(unshared)); + } + } + } + private Object readOrdinaryObject(boolean unshared) throws IOException { + // isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类 + obj = desc.isInstantiable() ? desc.newInstance() : null; + // 添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true + if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { + // 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量 + // 多次调用ObjectInputStream类中的readObject方法,本质调用定义的readResolve方法,返回的是同一个对象。 + Object rep = desc.invokeReadResolve(obj); + } + return obj; + } + ``` @@ -9798,21 +9832,62 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 -##### 局部变量 +##### 反射破解 -局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量 +* 反射 -* 表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题 -* 表的容量大小是在编译期确定的,保存在方法的 Code 属性的 maximum local variables 数据项中 -* 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁 -* 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收 + ```java + public class Test { + public static void main(String[] args) throws Exception { + //获取Singleton类的字节码对象 + Class clazz = Singleton.class; + //获取Singleton类的私有无参构造方法对象 + Constructor constructor = clazz.getDeclaredConstructor(); + //取消访问检查 + constructor.setAccessible(true); + + //创建Singleton类的对象s1 + Singleton s1 = (Singleton) constructor.newInstance(); + //创建Singleton类的对象s2 + Singleton s2 = (Singleton) constructor.newInstance(); + + //判断通过反射创建的两个Singleton对象是否是同一个对象 + System.out.println(s1 == s2); //false + } + } + ``` + +* 反射方式破解单例的解决方法: + + ```java + public class Singleton { + private static volatile Singleton instance; + + // 私有构造方法 + private Singleton() { + // 反射破解单例模式需要添加的代码 + if(instance != null) { + throw new RuntimeException(); + } + } + + // 对外提供静态方法获取该对象 + public static Singleton getInstance() { + if(instance != null) { + return instance; + } + synchronized (Singleton.class) { + if(instance != null) { + return instance; + } + instance = new Singleton(); + return instance; + } + } + } + ``` -局部变量表最基本的存储单元是 **slot(变量槽)**: -* 参数值的存放总是在局部变量数组的 index0 开始,到数组长度 -1 的索引结束,JVM 为每一个 slot 都分配一个访问索引,通过索引即可访问到槽中的数据 -* 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量 -* 32 位以内的类型只占一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占两个 slot -* 局部变量表中的槽位是可以**重复利用**的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的 @@ -9820,97 +9895,172 @@ Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所 -##### 操作数栈 +#### Runtime -栈:可以使用数组或者链表来实现 +Runtime 类就是使用的单例设计模式中的饿汉式 -操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop) +```java +public class Runtime { + private static Runtime currentRuntime = new Runtime(); + public static Runtime getRuntime() { + return currentRuntime; + } + private Runtime() {} + ... +} +``` -* 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,是执行引擎的一个工作区 +使用 Runtime -* Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈 -* 如果被调用的方法带有返回值的话,其**返回值将会被压入当前栈帧的操作数栈中** +```java +public class RuntimeDemo { + public static void main(String[] args) throws IOException { + //获取Runtime类对象 + Runtime runtime = Runtime.getRuntime(); -栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率 + //返回 Java 虚拟机中的内存总量。 + System.out.println(runtime.totalMemory()); + //返回 Java 虚拟机试图使用的最大内存量。 + System.out.println(runtime.maxMemory()); -基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术 + //创建一个新的进程执行指定的字符串命令,返回进程对象 + Process process = runtime.exec("ipconfig"); + //获取命令执行后的结果,通过输入流获取 + InputStream inputStream = process.getInputStream(); + byte[] arr = new byte[1024 * 1024* 100]; + int b = inputStream.read(arr); + System.out.println(new String(arr,0,b,"gbk")); + } +} +``` -*** +**** -##### 动态链接 -动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是**动态绑定** -* 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用 +### 代理模式 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接符号引用.png) +#### 静态代理 -* 在 Java 源文件被编译成字节码文件中,所有的变量和方法引用都作为符号引用保存在 class 的常量池中 - - 常量池的作用:提供一些符号和常量,便于指令的识别 - - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接运行时常量池.png) +代理模式:由于某些原因需要给某对象提供一个代理以控制对该对象的访问,访问对象不适合或者不能直接引用为目标对象,代理对象作为访问对象和目标对象之间的中介 +Java 中的代理按照代理类生成时机不同又分为静态代理和动态代理,静态代理代理类在编译期就生成,而动态代理代理类则是在 Java 运行时动态生成,动态代理又有 JDK 代理和 CGLib 代理两种 +代理(Proxy)模式分为三种角色: -*** +* 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法 +* 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象 +* 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,可以访问、控制或扩展真实主题的功能 +买票案例,火车站是目标对象,代售点是代理对象 +* 卖票接口: -##### 返回地址 + ```java + public interface SellTickets { + void sell(); + } + ``` -Return Address:存放调用该方法的 PC 寄存器的值 +* 火车站,具有卖票功能,需要实现SellTickets接口 -方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置 + ```java + public class TrainStation implements SellTickets { + public void sell() { + System.out.println("火车站卖票"); + } + } + ``` -* 正常:调用者的 pc 计数器的值作为返回地址,即调用该方法的指令的**下一条指令的地址** -* 异常:返回地址是要通过异常表来确定 +* 代售点: -正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者 + ```java + public class ProxyPoint implements SellTickets { + private TrainStation station = new TrainStation(); + + public void sell() { + System.out.println("代理点收取一些服务费用"); + station.sell(); + } + } + ``` -异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出 +* 测试类: -两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值 + ```java + public class Client { + public static void main(String[] args) { + ProxyPoint pp = new ProxyPoint(); + pp.sell(); + } + } + ``` + 测试类直接访问的是 ProxyPoint 类对象,也就是 ProxyPoint 作为访问对象和目标对象的中介 -##### 附加信息 -栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息 +**** -*** +#### JDK +##### 使用方式 +Java 中提供了一个动态代理类 Proxy,Proxy 并不是代理对象的类,而是提供了一个创建代理对象的静态方法 newProxyInstance() 来获取代理对象 -#### 本地方法栈 +`static Object newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h) ` -本地方法栈是为虚拟机执行本地方法时提供服务的 +* 参数一:类加载器,负责加载代理类。传入类加载器,代理和被代理对象要用一个类加载器才是父子关系,不同类加载器加载相同的类在 JVM 中都不是同一个类对象 -JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植 +* 参数二:被代理业务对象的**全部实现的接口**,代理对象与真实对象实现相同接口,知道为哪些方法做代理 -* 不需要进行GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常 +* 参数三:代理真正的执行方法,也就是代理的处理逻辑 -* 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一 +代码实现: -* 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序 +* 代理工厂:创建代理对象 -* 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限 + ```java + public class ProxyFactory { + private TrainStation station = new TrainStation(); + //也可以在参数中提供 getProxyObject(TrainStation station) + public SellTickets getProxyObject() { + //使用 Proxy 获取代理对象 + SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance( + station.getClass().getClassLoader(), + station.getClass().getInterfaces(), + new InvocationHandler() { + public Object invoke(Object proxy, Method method, Object[] args) { + System.out.println("代理点(JDK动态代理方式)"); + //执行真实对象 + Object result = method.invoke(station, args); + return result; + } + }); + return sellTickets; + } + } + ``` - * 本地方法可以通过本地方法接口来**访问虚拟机内部的运行时数据区** - * 直接从本地内存的堆中分配任意数量的内存 - * 可以直接使用本地处理器中的寄存器 - - - - +* 测试类: -图片来源:https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md + ```java + public class Client { + public static void main(String[] args) { + //获取代理对象 + ProxyFactory factory = new ProxyFactory(); + //必须时代理ji + SellTickets proxyObject = factory.getProxyObject(); + proxyObject.sell(); + } + } + ``` @@ -9918,185 +10068,292 @@ JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可 -#### 程序计数器 - -Program Counter Register 程序计数器(寄存器) +##### 实现原理 -作用:内部保存字节码的行号,用于记录正在执行的字节码指令地址(如果正在执行的是本地方法则为空) +JDK 动态代理方式的优缺点: -原理: +- 优点:可以为任意的接口实现类对象做代理,也可以为被代理对象的所有接口的所有方法做代理,动态代理可以在不改变方法源码的情况下,实现对方法功能的增强,提高了软件的可扩展性,Java 反射机制可以生成任意类型的动态代理类 +- 缺点:**只能针对接口或者接口的实现类对象做代理对象**,普通类是不能做代理对象的 +- 原因:**生成的代理类继承了 Proxy**,Java 是单继承的,所以 JDK 动态代理只能代理接口 -* JVM 对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程 -* 切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号 +ProxyFactory 不是代理模式中的代理类,而代理类是程序在运行过程中动态的在内存中生成的类,可以通过 Arthas 工具查看代理类结构: -特点: +* 代理类($Proxy0)实现了 SellTickets 接口,真实类和代理类实现同样的接口 +* 代理类($Proxy0)将提供了的匿名内部类对象传递给了父类 +* 代理类($Proxy0)的修饰符是 public final -* 是线程私有的 -* **不会存在内存溢出**,是 JVM 规范中唯一一个不出现 OOM 的区域,所以这个空间不会进行 GC +```java +// 程序运行过程中动态生成的代理类 +public final class $Proxy0 extends Proxy implements SellTickets { + private static Method m3; -Java 反编译指令:`javap -v Test.class` + public $Proxy0(InvocationHandler invocationHandler) { + super(invocationHandler);//InvocationHandler对象传递给父类 + } -#20:代表去 Constant pool 查看该地址的指令 + static { + m3 = Class.forName("proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]); + } -```java -0: getstatic #20 // PrintStream out = System.out; -3: astore_1 // -- -4: aload_1 // out.println(1); -5: iconst_1 // -- -6: invokevirtual #26 // -- -9: aload_1 // out.println(2); -10: iconst_2 // -- -11: invokevirtual #26 // -- + public final void sell() { + // 调用InvocationHandler的invoke方法 + this.h.invoke(this, m3, null); + } +} + +// Java提供的动态代理相关类 +public class Proxy implements java.io.Serializable { + protected InvocationHandler h; + + protected Proxy(InvocationHandler h) { + this.h = h; + } +} ``` +执行流程如下: + +1. 在测试类中通过代理对象调用 sell() 方法 +2. 根据多态的特性,执行的是代理类($Proxy0)中的 sell() 方法 +3. 代理类($Proxy0)中的 sell() 方法中又调用了 InvocationHandler 接口的子实现类对象的 invoke 方法 +4. invoke 方法通过反射执行了真实对象所属类(TrainStation)中的 sell() 方法 + **** -#### 堆 +##### 源码解析 -Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题 +```java +public static Object newProxyInstance(ClassLoader loader, + Class[] interfaces, + InvocationHandler h){ + // InvocationHandler 为空则抛出异常 + Objects.requireNonNull(h); -存放哪些资源: + // 复制一份 interfaces + final Class[] intfs = interfaces.clone(); + final SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + checkProxyAccess(Reflection.getCallerClass(), loader, intfs); + } -* 对象实例:类初始化生成的对象,**基本数据类型的数组也是对象实例**,new 创建对象都使用堆内存 -* 字符串常量池: - * 字符串常量池原本存放于方法区,jdk7 开始放置于堆中 - * 字符串常量池**存储的是 String 对象的直接引用或者对象**,是一张 string table -* 静态变量:静态变量是有 static 修饰的变量,jdk7 时从方法区迁移至堆中 -* 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率 + // 从缓存中查找 class 类型的代理对象,会调用 ProxyClassFactory#apply 方法 + Class cl = getProxyClass0(loader, intfs); + //proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory()) + + try { + if (sm != null) { + checkNewProxyPermission(Reflection.getCallerClass(), cl); + } -设置堆内存指令:`-Xmx Size` + // 获取代理类的构造方法,根据参数 InvocationHandler 匹配获取某个构造器 + final Constructor cons = cl.getConstructor(constructorParams); + final InvocationHandler ih = h; + // 构造方法不是 pubic 的需要启用权限,暴力p + if (!Modifier.isPublic(cl.getModifiers())) { + AccessController.doPrivileged(new PrivilegedAction() { + public Void run() { + // 设置可访问的权限 + cons.setAccessible(true); + return null; + } + }); + } + // cons 是构造方法,并且内部持有 InvocationHandler,在 InvocationHandler 中持有 target 目标对象 + return cons.newInstance(new Object[]{h}); + } catch (IllegalAccessException|InstantiationException e) {} +} +``` -内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常 +Proxy 的静态内部类: -堆内存诊断工具:(控制台命令) +```java +private static final class ProxyClassFactory { + // 代理类型的名称前缀 + private static final String proxyClassNamePrefix = "$Proxy"; -1. jps:查看当前系统中有哪些 java 进程 -2. jmap:查看堆内存占用情况 `jhsdb jmap --heap --pid 进程id` -3. jconsole:图形界面的,多功能的监测工具,可以连续监测 + // 生成唯一数字使用,结合上面的代理类型名称前缀一起生成 + private static final AtomicLong nextUniqueNumber = new AtomicLong(); -在 Java7 中堆内会存在**年轻代、老年代和方法区(永久代)**: + //参数一:Proxy.newInstance 时传递的 + //参数二:Proxy.newInstance 时传递的接口集合 + @Override + public Class apply(ClassLoader loader, Class[] interfaces) { + + Map, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length); + // 遍历接口集合 + for (Class intf : interfaces) { + Class interfaceClass = null; + try { + // 加载接口类到 JVM + interfaceClass = Class.forName(intf.getName(), false, loader); + } catch (ClassNotFoundException e) { + } + if (interfaceClass != intf) { + throw new IllegalArgumentException( + intf + " is not visible from class loader"); + } + // 如果 interfaceClass 不是接口 直接报错,保证集合内都是接口 + if (!interfaceClass.isInterface()) { + throw new IllegalArgumentException( + interfaceClass.getName() + " is not an interface"); + } + // 保证接口 interfaces 集合中没有重复的接口 + if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) { + throw new IllegalArgumentException( + "repeated interface: " + interfaceClass.getName()); + } + } -* Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候, GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间 -* Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区 -* Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理 + // 生成的代理类的包名 + String proxyPkg = null; + // 【生成的代理类访问修饰符 public final】 + int accessFlags = Modifier.PUBLIC | Modifier.FINAL; -分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能 + // 检查接口集合内的接口,看看有没有某个接口的访问修饰符不是 public 的 如果不是 public 的接口, + // 生成的代理类 class 就必须和它在一个包下,否则访问出现问题 + for (Class intf : interfaces) { + // 获取访问修饰符 + int flags = intf.getModifiers(); + if (!Modifier.isPublic(flags)) { + accessFlags = Modifier.FINAL; + // 获取当前接口的全限定名 包名.类名 + String name = intf.getName(); + int n = name.lastIndexOf('.'); + // 获取包名 + String pkg = ((n == -1) ? "" : name.substring(0, n + 1)); + if (proxyPkg == null) { + proxyPkg = pkg; + } else if (!pkg.equals(proxyPkg)) { + throw new IllegalArgumentException( + "non-public interfaces from different packages"); + } + } + } -```java -public static void main(String[] args) { - //返回Java虚拟机中的堆内存总量 - long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024; - //返回Java虚拟机使用的最大堆内存量 - long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024; - - System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M - System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M + if (proxyPkg == null) { + // if no non-public proxy interfaces, use com.sun.proxy package + proxyPkg = ReflectUtil.PROXY_PACKAGE + "."; + } + + // 获取唯一的编号 + long num = nextUniqueNumber.getAndIncrement(); + // 包名+ $proxy + 数字,比如 $proxy1 + String proxyName = proxyPkg + proxyClassNamePrefix + num; + + // 【生成二进制字节码,这个字节码写入到文件内】,就是编译好的 class 文件 + byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags); + try { + // 【使用加载器加载二进制到 jvm】,并且返回 class + return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); + } catch (ClassFormatError e) { } + } } ``` -*** +*** -#### 方法区 -方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆) -方法区是一个 JVM 规范,**永久代与元空间都是其一种实现方式** +#### CGLIB -方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError) +CGLIB 是一个功能强大,高性能的代码生成包,为没有实现接口的类提供代理,为 JDK 动态代理提供了补充($$Proxy) -方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现 +* CGLIB 是第三方提供的包,所以需要引入 jar 包的坐标: -为了**避免方法区出现 OOM**,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,**静态变量和字符串常量池等放入堆中** + ```xml + + cglib + cglib + 2.2.2 + + ``` -类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表 +* 代理工厂类: -常量池表(Constant Pool Table)是 Class 文件的一部分,存储了**类在编译期间生成的字面量、符号引用**,JVM 为每个已加载的类维护一个常量池 + ```java + public class ProxyFactory implements MethodInterceptor { + private TrainStation target = new TrainStation(); + + public TrainStation getProxyObject() { + //创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数 + Enhancer enhancer = new Enhancer(); + //设置父类的字节码对象 + enhancer.setSuperclass(target.getClass()); + //设置回调函数 + enhancer.setCallback(new MethodInterceptor() { + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + System.out.println("代理点收取一些服务费用(CGLIB动态代理方式)"); + Object o = methodProxy.invokeSuper(obj, args); + return null;//因为返回值为void + } + }); + //创建代理对象 + TrainStation obj = (TrainStation) enhancer.create(); + return obj; + } + } + ``` -- 字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等 -- 符号引用:类、字段、方法、接口等的符号引用 +CGLIB 的优缺点 -运行时常量池是方法区的一部分 +* 优点: + * CGLIB 动态代理**不限定**是否具有接口,可以对任意操作进行增强 + * CGLIB 动态代理无需要原始被代理对象,动态创建出新的代理对象 + * **JDKProxy 仅对接口方法做增强,CGLIB 对所有方法做增强**,包括 Object 类中的方法,toString、hashCode 等 +* 缺点:CGLIB 不能对声明为 final 的类或者方法进行代理,因为 CGLIB 原理是**动态生成被代理类的子类,继承被代理类** -* 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池 -* 类在解析阶段将这些符号引用替换成直接引用 -* 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern() -*** +**** -### 本地内存 -#### 基本介绍 +#### 方式对比 -虚拟机内存:Java 虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报 OOM +三种方式对比: -本地内存:又叫做**堆外内存**,线程共享的区域,本地内存这块区域是不会受到 JVM 的控制的,不会发生 GC;因此对于整个 java 的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报 OOM +* 动态代理和静态代理: -本地内存概述图: + * 动态代理将接口中声明的所有方法都被转移到一个集中的方法中处理(InvocationHandler.invoke),在接口方法数量比较多的时候,可以进行灵活处理,不需要像静态代理那样每一个方法进行中转 - + * 静态代理是在编译时就已经将接口、代理类、被代理类的字节码文件确定下来 + * 动态代理是程序**在运行后通过反射创建字节码文件**交由 JVM 加载 +* JDK 代理和 CGLIB 代理: + JDK 动态代理采用 ProxyGenerator.generateProxyClass() 方法在运行时生成字节码;CGLIB 底层采用 ASM 字节码生成框架,使用字节码技术生成代理类。在 JDK1.6之前比使用 Java 反射效率要高,到 JDK1.8 的时候,JDK 代理效率高于 CGLIB 代理。所以如果有接口或者当前类就是接口使用 JDK 动态代理,如果没有接口使用 CGLIB 代理 -*** +代理模式的优缺点: +* 优点: + * 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用 + * **代理对象可以增强目标对象的功能,内部持有原始的目标对象** + * 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度 +* 缺点:增加了系统的复杂度 -#### 元空间 +代理模式的使用场景: -PermGen 被元空间代替,永久代的**类信息、方法、常量池**等都移动到元空间区 - -元空间与永久代区别:元空间不在虚拟机中,使用的本地内存,默认情况下,元空间的大小仅受本地内存限制 +* 远程(Remote)代理:本地服务通过网络请求远程服务,需要实现网络通信,处理其中可能的异常。为了良好的代码设计和可维护性,将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能 -方法区内存溢出: +* 防火墙(Firewall)代理:当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网,当互联网返回响应时,代理服务器再把它转给你的浏览器 -* JDK1.8 以前会导致永久代内存溢出:java.lang.OutOfMemoryError: PerGen space +* 保护(Protect or Access)代理:控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限 - ```sh - -XX:MaxPermSize=8m #参数设置 - ``` - -* JDK1.8 以后会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace - ```sh - -XX:MaxMetaspaceSize=8m #参数设置 - ``` -元空间内存溢出演示: -```java -public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码 - public static void main(String[] args) { - int j = 0; - try { - Demo1_8 test = new Demo1_8(); - for (int i = 0; i < 10000; i++, j++) { - // ClassWriter 作用是生成类的二进制字节码 - ClassWriter cw = new ClassWriter(0); - // 版本号, public, 类名, 包名, 父类, 接口 - cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); - // 返回 byte[] - byte[] code = cw.toByteArray(); - // 执行了类的加载 - test.defineClass("Class" + i, code, 0, code.length); // Class 对象 - } - } finally { - System.out.println(j); - } - } -} -``` @@ -10104,172 +10361,193 @@ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制 -#### 直接内存 -直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域 +# JVM +## JVM概述 -直接内存详解参考:NET → NIO → 直接内存 +### 基本介绍 +JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身是一个虚拟计算机,直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作 +特点: -*** +* Java 虚拟机基于**二进制字节码**执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成 +* JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的**跨平台性** +Java 代码执行流程:java程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux) +JVM 结构: -### 变量位置 + -变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的**声明位置** +JVM、JRE、JDK 对比: -静态内部类和其他内部类: + -* **一个 class 文件只能对应一个 public 类型的类**,这个类可以有内部类,但不会生成新的 class 文件 -* 静态内部类属于类本身,加载到方法区,其他内部类属于内部类的属性,加载到堆(待考证) -类变量: +参考书籍:https://book.douban.com/subject/34907497/ -* 类变量是用 static 修饰符修饰,定义在方法外的变量,随着 java 进程产生和销毁 -* 在 java8 之前把静态变量存放于方法区,在 java8 时存放在堆中的静态变量区 +参考视频:https://www.bilibili.com/video/BV1PJ411n7xZ +参考视频:https://www.bilibili.com/video/BV1yE411Z7AP -实例变量: -* 实例(成员)变量是定义在类中,没有 static 修饰的变量,随着类的实例产生和销毁,是类实例的一部分 -* 在类初始化的时候,从运行时常量池取出直接引用或者值,**与初始化的对象一起放入堆中** -局部变量: +*** -* 局部变量是定义在类的方法中的变量 -* 在所在方法被调用时**放入虚拟机栈的栈帧**中,方法执行结束后从虚拟机栈中弹出, -类常量池、运行时常量池、字符串常量池有什么关系?有什么区别? -* 类常量池与运行时常量池都存储在方法区,而字符串常量池在 jdk7 时就已经从方法区迁移到了 java 堆中 -* 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符 -* **在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池** -* 对于文本字符,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池 +### 架构模型 -什么是字面量?什么是符号引用? +Java 编译器输入的指令流是一种基于栈的指令集架构。因为跨平台的设计,Java 的指令都是根据栈来设计的,不同平台 CPU 架构不同,所以不能设计为基于寄存器架构 -* 字面量:java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示 +* 基于栈式架构的特点: + * 设计和实现简单,适用于资源受限的系统 + * 使用零地址指令方式分配,执行过程依赖操作栈,指令集更小,编译器容易实现 + * 零地址指令:机器指令的一种,是指令系统中的一种不设地址字段的指令,只有操作码而没有地址码。这种指令有两种情况:一是无需操作数,另一种是操作数为默认的(隐含的),默认为操作数在寄存器(ACC)中,指令可直接访问寄存器 + * 一地址指令:一个操作码对应一个地址码,通过地址码寻找操作数 + * 不需要硬件的支持,可移植性更好,更好实现跨平台 +* 基于寄存器架构的特点: + * 需要硬件的支持,可移植性差 + * 性能更好,执行更高效,寄存器比内存快 + * 以一地址指令、二地址指令、三地址指令为主 - ```java - int a = 1; //这个1便是字面量 - String b = "iloveu"; //iloveu便是字面量 - ``` -* 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址 +*** -*** +### 生命周期 +JVM 的生命周期分为三个阶段,分别为:启动、运行、死亡。 +- **启动**:当启动一个 Java 程序时,通过引导类加载器(bootstrap class loader)创建一个初始类(initial class),对于拥有 main 函数的类就是 JVM 实例运行的起点 +- **运行**: + + - main() 方法是一个程序的初始起点,任何线程均可由在此处启动 + - 在 JVM 内部有两种线程类型,分别为:用户线程和守护线程,**JVM 使用的是守护线程,main() 和其他线程使用的是用户线程**,守护线程会随着用户线程的结束而结束 + - 执行一个 Java 程序时,真真正正在执行的是一个 Java 虚拟机的进程 + - JVM 有两种运行模式 Server 与 Client,两种模式的区别在于:Client 模式启动速度较快,Server 模式启动较慢;但是启动进入稳定期长期运行之后 Server 模式的程序运行速度比 Client 要快很多 + + Server 模式启动的 JVM 采用的是重量级的虚拟机,对程序采用了更多的优化;Client 模式启动的 JVM 采用的是轻量级的虚拟机 +- **死亡**: + + - 当程序中的用户线程都中止,JVM 才会退出 + - 程序正常执行结束、程序异常或错误而异常终止、操作系统错误导致终止 + - 线程调用 Runtime 类 halt 方法或 System 类 exit 方法,并且 java 安全管理器允许这次 exit 或 halt 操作 -## 内存管理 -### 内存分配 -#### 两种方式 +*** -不分配内存的对象无法进行其他操作,JVM 为对象分配内存的过程:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 -* 如果内存规整,使用指针碰撞(Bump The Pointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 -* 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配。已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 -*** +## 内存结构 +### 内存概述 +内存结构是 JVM 中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区 -#### 分代思想 +JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行 -##### 分代介绍 +* Java1.8 以前的内存结构图: + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java7内存结构图.png) -Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代 +* Java1.8 之后的内存结果图: -- 新生代使用:复制算法 -- 老年代使用:标记 - 清除 或者 标记 - 整理 算法 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java8内存结构图.png) -**Minor GC 和 Full GC**: +线程运行诊断: -- Minor GC:回收新生代,新生代对象存活时间很短,所以 Minor GC 会频繁执行,执行的速度比较快 -- Full GC:回收老年代和新生代,老年代对象其存活时间长,所以 Full GC 很少执行,执行速度会比 Minor GC 慢很多 +* 定位:jps 定位进程 id +* jstack 进程 id:用于打印出给定的 java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息 - Eden 和 Survivor 大小比例默认为 8:1:1 +常见OOM错误: - +* java.lang.StackOverflowError +* java.lang.OutOfMemoryError:java heap space +* java.lang.OutOfMemoryError:GC overhead limit exceeded +* java.lang.OutOfMemoryError:Direct buffer memory +* java.lang.OutOfMemoryError:unable to create new native thread +* java.lang.OutOfMemoryError:Metaspace +*** -*** +### JVM内存 +#### 虚拟机栈 -##### 分代分配 +##### Java栈 -工作机制: +Java 虚拟机栈:Java Virtual Machine Stacks,**每个线程**运行时所需要的内存 -* **对象优先在 Eden 分配**:当创建一个对象的时候,对象会被分配在新生代的 Eden 区,当Eden 区要满了时候,触发 YoungGC -* 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到 to 区,并且当前对象的年龄会加 1,清空 Eden 区 -* 当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 to 中的对象,移动到 from 区中,这些对象的年龄会加 1,清空 Eden 区和 to 区 -* To 区永远是空 Survivor 区,From 区是有数据的,每次 MinorGC 后两个区域互换 -* From 区和 To 区 也可以叫做 S0 区和 S1 区 +* 每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(**一个方法一个栈帧**) -晋升到老年代: +* Java 虚拟机规范允许 **Java 栈的大小是动态的或者是固定不变的** -* **长期存活的对象进入老年代**:为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中 - - `-XX:MaxTenuringThreshold`:定义年龄的阈值,对象头中用 4 个 bit 存储,所以最大值是 15,默认也是 15 -* **大对象直接进入老年代**:需要连续内存空间的对象,最典型的大对象是很长的字符串以及数组;避免在 Eden 和 Survivor 之间的大量复制;经常出现大对象会提前触发 GC 以获取足够的连续空间分配给大对象 - - `-XX:PretenureSizeThreshold`:大于此值的对象直接在老年代分配 -* **动态对象年龄判定**:如果在 Survivor 区中相同年龄的对象的所有大小之和超过 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代 +* 虚拟机栈是**每个线程私有的**,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程 -空间分配担保: +* 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,每个栈帧中存储着: -* 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的 -* 如果不成立,虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试着进行一次 Minor GC;如果小于或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。 + * 局部变量表:存储方法里的java基本数据类型以及对象的引用 + * 动态链接:也叫指向运行时常量池的方法引用 + * 方法返回地址:方法正常退出或者异常退出的定义 + * 操作数栈或表达式栈和其他一些附加信息 + + +设置栈内存大小:`-Xss size` `-Xss 1024k` +* 在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M -*** +虚拟机栈特点: +* 栈内存**不需要进行GC**,方法开始执行的时候会进栈,方法调用后自动弹栈,相当于清空了数据 +* 栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大) -#### TLAB +* 方法内的局部变量是否**线程安全**: + * 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的(逃逸分析) + * 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全 -TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用 TLAB 可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略** +异常: -- 栈上分配使用的是栈来进行对象内存的分配 -- TLAB 分配使用的是 Eden 区域进行内存分配,属于堆内存 +* 栈帧过多导致栈内存溢出 (超过了栈的容量),会抛出 OutOfMemoryError 异常 +* 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常 -堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度 -问题:堆空间都是共享的么? 不一定,因为还有 TLAB,在堆中划分出一块区域,为每个线程所独占 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配策略.jpg) +*** -JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在 TLAB 空间分配内存失败时,JVM 就会通过**使用加锁机制确保数据操作的原子性**,从而直接在堆中分配内存 -栈上分配优先于 TLAB 分配进行,逃逸分析中若可进行栈上分配优化,会优先进行对象栈上直接分配内存 -参数设置: +##### 局部变量 -* `-XX:UseTLAB`:设置是否开启 TLAB 空间 +局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量 -* `-XX:TLABWasteTargetPercent`:设置 TLAB 空间所占用 Eden 空间的百分比大小,默认情况下 TLAB 空间的内存非常小,仅占有整个 Eden 空间的1% -* `-XX:TLABRefillWasteFraction`:指当 TLAB 空间不足,请求分配的对象内存大小超过此阈值时不会进行 TLAB 分配,直接进行堆内存分配,否则还是会优先进行 TLAB 分配 +* 表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题 +* 表的容量大小是在编译期确定的,保存在方法的 Code 属性的 maximum local variables 数据项中 +* 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁 +* 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配过程.jpg) +局部变量表最基本的存储单元是 **slot(变量槽)**: + +* 参数值的存放总是在局部变量数组的 index0 开始,到数组长度 -1 的索引结束,JVM 为每一个 slot 都分配一个访问索引,通过索引即可访问到槽中的数据 +* 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量 +* 32 位以内的类型只占一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占两个 slot +* 局部变量表中的槽位是可以**重复利用**的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的 @@ -10277,223 +10555,183 @@ JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都 -#### 逃逸分析 +##### 操作数栈 -即时编译(Just-in-time Compilation,JIT)是一种通过在运行时将字节码翻译为机器码,从而改善性能的技术,在 HotSpot 实现中有多种选择:C1、C2 和 C1+C2,分别对应 client、server 和分层编译 +栈:可以使用数组或者链表来实现 -* C1 编译速度快,优化方式比较保守;C2 编译速度慢,优化方式比较激进 -* C1+C2 在开始阶段采用 C1 编译,当代码运行到一定热度之后采用 C2 重新编译 +操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop) -逃逸分析并不是直接的优化手段,而是一个代码分析方式,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸 +* 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,是执行引擎的一个工作区 -* 方法逃逸:当一个对象在方法中定义之后,被外部方法引用 - * 全局逃逸:一个对象的作用范围逃出了当前方法或者当前线程,比如对象是一个静态变量、全局变量赋值、已经发生逃逸的对象、作为当前方法的返回值 - * 参数逃逸:一个对象被作为方法参数传递或者被参数引用 -* 线程逃逸:如类变量或实例变量,可能被其它线程访问到 +* Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈 +* 如果被调用的方法带有返回值的话,其**返回值将会被压入当前栈帧的操作数栈中** -如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配 +栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率 -* 同步消除 +基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术 - 线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,不被其它线程访问到,那对象的读写就不会存在竞争,则可以消除对该对象的**同步锁**,通过 `-XX:+EliminateLocks` 可以开启同步消除 ( - 号关闭) -* 标量替换 - * 标量替换:如果把一个对象拆散,将其成员变量恢复到基本类型来访问 - * 标量 (scalar) :不可分割的量,如基本数据类型和 reference 类型 - - 聚合量 (Aggregate):一个数据可以继续分解,对象一般是聚合量 - * 如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替 - * 参数设置: - - * `-XX:+EliminateAllocations`:开启标量替换 - * `-XX:+PrintEliminateAllocations`:查看标量替换情况 +*** -* 栈上分配 - JIT 编译器在编译期间根据逃逸分析的结果,如果一个对象没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需 GC - User 对象的作用域局限在方法 fn 中,可以使用标量替换的优化手段在栈上分配对象的成员变量,这样就不会生成 User 对象,大大减轻 GC 的压力 +##### 动态链接 - ```java - public class JVM { - public static void main(String[] args) throws Exception { - int sum = 0; - int count = 1000000; - //warm up - for (int i = 0; i < count ; i++) { - sum += fn(i); - } - System.out.println(sum); - System.in.read(); - } - private static int fn(int age) { - User user = new User(age); - int i = user.getAge(); - return i; - } - } - - class User { - private final int age; +动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是**动态绑定** + +* 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用 + + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接符号引用.png) + +* 在 Java 源文件被编译成字节码文件中,所有的变量和方法引用都作为符号引用保存在 class 的常量池中 - public User(int age) { - this.age = age; - } + 常量池的作用:提供一些符号和常量,便于指令的识别 - public int getAge() { - return age; - } - } - ``` + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-动态链接运行时常量池.png) - -*** +*** -### 回收策略 -#### 触发条件 +##### 返回地址 -内存垃圾回收机制主要集中的区域就是线程共享区域:**堆和方法区** +Return Address:存放调用该方法的 PC 寄存器的值 -Minor GC 触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC +方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置 -FullGC 同时回收新生代、老年代和方法区,只会存在一个 FullGC 的线程进行执行,其他的线程全部会被**挂起**,有以下触发条件: +* 正常:调用者的 pc 计数器的值作为返回地址,即调用该方法的指令的**下一条指令的地址** +* 异常:返回地址是要通过异常表来确定 -* 调用 System.gc(): +正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者 - * 在默认情况下,通过 System.gc() 或 Runtime.getRuntime().gc() 的调用,会显式触发 FullGC,同时对老年代和新生代进行回收,但是虚拟机不一定真正去执行,无法保证对垃圾收集器的调用 - * 不建议使用这种方式,应该让虚拟机管理内存。一般情况下,垃圾回收应该是自动进行的,无须手动触发;在一些特殊情况下,如正在编写一个性能基准,可以在运行之间调用 System.gc() +异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出 -* 老年代空间不足: +两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值 - * 为了避免引起的 Full GC,应当尽量不要创建过大的对象以及数组 - * 通过 -Xmn 参数调整新生代的大小,让对象尽量在新生代被回收掉不进入老年代,可以通过 `-XX:MaxTenuringThreshold` 调大对象进入老年代的年龄,让对象在新生代多存活一段时间 -* 空间分配担保失败 -* JDK 1.7 及以前的永久代(方法区)空间不足 +##### 附加信息 -* Concurrent Mode Failure:执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC +栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息 -手动 GC 测试,VM参数:`-XX:+PrintGcDetails` -```java -public void localvarGC1() { - byte[] buffer = new byte[10 * 1024 * 1024];//10MB - System.gc(); //输出: 不会被回收, FullGC时被放入老年代 -} +*** -public void localvarGC2() { - byte[] buffer = new byte[10 * 1024 * 1024]; - buffer = null; - System.gc(); //输出: 正常被回收 -} - public void localvarGC3() { - { - byte[] buffer = new byte[10 * 1024 * 1024]; - } - System.gc(); //输出: 不会被回收, FullGC时被放入老年代 - } -public void localvarGC4() { - { - byte[] buffer = new byte[10 * 1024 * 1024]; - } - int value = 10; - System.gc(); //输出: 正常被回收,slot复用,局部变量过了其作用域 buffer置空 -} -``` +#### 本地方法栈 +本地方法栈是为虚拟机执行本地方法时提供服务的 -*** +JNI:Java Native Interface,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植 +* 不需要进行GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常 +* 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一 -#### 安全区域 +* 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序 -安全点 (Safepoint):程序执行时并非在所有地方都能停顿下来开始 GC,只有在安全点才能停下 +* 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限 -- Safe Point 的选择很重要,如果太少可能导致 GC 等待的时间太长,如果太多可能导致运行时的性能问题 -- 大部分指令的执行时间都非常短,通常会根据是否具有让程序长时间执行的特征为标准,选择些执行时间较长的指令作为 Safe Point, 如方法调用、循环跳转和异常跳转等 + * 本地方法可以通过本地方法接口来**访问虚拟机内部的运行时数据区** + * 直接从本地内存的堆中分配任意数量的内存 + * 可以直接使用本地处理器中的寄存器 + + + + -在 GC 发生时,让所有线程都在最近的安全点停顿下来的方法: +图片来源:https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md -- 抢先式中断:没有虚拟机采用,首先中断所有线程,如果有线程不在安全点,就恢复线程让线程运行到安全点 -- 主动式中断:设置一个中断标志,各个线程运行到各个 Safe Point 时就轮询这个标志,如果中断标志为真,则将自己进行中断挂起 -问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应 JVM 的中断请求,运行到安全点去中断挂起,JVM 也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 -安全区域 (Safe Region):指在一段代码片段中,**对象的引用关系不会发生变化**,在这个区域中的任何位置开始 GC 都是安全的 +*** -运行流程: -- 当线程运行到 Safe Region 的代码时,首先标识已经进入了 Safe Region,如果这段时间内发生GC,JVM 会忽略标识为 Safe Region 状态的线程 -- 当线程即将离开 Safe Region 时,会检查 JVM 是否已经完成 GC,如果完成了则继续运行,否则线程必须等待 GC 完成,收到可以安全离开 SafeRegion 的信号 +#### 程序计数器 +Program Counter Register 程序计数器(寄存器) +作用:内部保存字节码的行号,用于记录正在执行的字节码指令地址(如果正在执行的是本地方法则为空) -*** +原理: +* JVM 对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程 +* 切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号 +特点: -### 垃圾判断 +* 是线程私有的 +* **不会存在内存溢出**,是 JVM 规范中唯一一个不出现 OOM 的区域,所以这个空间不会进行 GC -#### 垃圾介绍 +Java 反编译指令:`javap -v Test.class` -垃圾:**如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾** +#20:代表去 Constant pool 查看该地址的指令 -作用:释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象 +```java +0: getstatic #20 // PrintStream out = System.out; +3: astore_1 // -- +4: aload_1 // out.println(1); +5: iconst_1 // -- +6: invokevirtual #26 // -- +9: aload_1 // out.println(2); +10: iconst_2 // -- +11: invokevirtual #26 // -- +``` -垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收 -在堆里存放着几乎所有的 Java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程可以称为垃圾标记阶段,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法** +**** -*** +#### 堆 +Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题 -#### 引用计数法 +存放哪些资源: -引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1;当引用失效时,引用计数器就减 1;当对象 A 的引用计数器的值为 0,即表示对象A不可能再被使用,可进行回收(Java 没有采用) +* 对象实例:类初始化生成的对象,**基本数据类型的数组也是对象实例**,new 创建对象都使用堆内存 +* 字符串常量池: + * 字符串常量池原本存放于方法区,jdk7 开始放置于堆中 + * 字符串常量池**存储的是 String 对象的直接引用或者对象**,是一张 string table +* 静态变量:静态变量是有 static 修饰的变量,jdk7 时从方法区迁移至堆中 +* 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率 -优点: +设置堆内存指令:`-Xmx Size` -- 回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为 0,可以直接回收 -- 在垃圾回收过程中,应用无需挂起;如果申请内存时,内存不足,则立刻报 OOM 错误 -- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象 +内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常 -缺点: +堆内存诊断工具:(控制台命令) -- 每次对象被引用时,都需要去更新计数器,有一点时间开销 +1. jps:查看当前系统中有哪些 java 进程 +2. jmap:查看堆内存占用情况 `jhsdb jmap --heap --pid 进程id` +3. jconsole:图形界面的,多功能的监测工具,可以连续监测 -- 浪费 CPU 资源,即使内存够用,仍然在运行时进行计数器的统计。 +在 Java7 中堆内会存在**年轻代、老年代和方法区(永久代)**: -- **无法解决循环引用问题,会引发内存泄露**(最大的缺点) +* Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候, GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间 +* Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区 +* Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理 - ```java - public class Test { - public Object instance = null; - public static void main(String[] args) { - Test a = new Test();// a = 1 - Test b = new Test();// b = 1 - a.instance = b; // b = 2 - b.instance = a; // a = 2 - a = null; // a = 1 - b = null; // b = 1 - } - } - ``` +分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-循环引用.png) +```java +public static void main(String[] args) { + //返回Java虚拟机中的堆内存总量 + long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024; + //返回Java虚拟机使用的最大堆内存量 + long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024; + + System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M + System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M +} +``` @@ -10501,159 +10739,164 @@ public void localvarGC4() { -#### 可达性分析 +#### 方法区 -##### GC Roots +方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆) -可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集 +方法区是一个 JVM 规范,**永久代与元空间都是其一种实现方式** -GC Roots 对象: +方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError) -- 虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等 -- 本地方法栈中引用的对象 -- 堆中类静态属性引用的对象 -- 方法区中的常量引用的对象 -- 字符串常量池(string Table)里的引用 -- 同步锁 synchronized 持有的对象 +方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现 -**GC Roots 是一组活跃的引用,不是对象**,放在 GC Roots Set 集合 +为了**避免方法区出现 OOM**,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,**静态变量和字符串常量池等放入堆中** +类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表 +常量池表(Constant Pool Table)是 Class 文件的一部分,存储了**类在编译期间生成的字面量、符号引用**,JVM 为每个已加载的类维护一个常量池 -*** +- 字面量:基本数据类型、字符串类型常量、声明为 final 的常量值等 +- 符号引用:类、字段、方法、接口等的符号引用 +运行时常量池是方法区的一部分 +* 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池 +* 类在解析阶段将这些符号引用替换成直接引用 +* 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern() -##### 工作原理 -可达性分析算法以根对象集合(GCRoots)为起始点,从上至下的方式搜索被根对象集合所连接的目标对象 -分析工作必须在一个保障**一致性的快照**中进行,否则结果的准确性无法保证,这也是导致 GC 进行时必须 Stop The World 的一个原因 +*** -基本原理: -- 可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链 -- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象 +### 本地内存 -- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象 +#### 基本介绍 - +虚拟机内存:Java 虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报 OOM +本地内存:又叫做**堆外内存**,线程共享的区域,本地内存这块区域是不会受到 JVM 的控制的,不会发生 GC;因此对于整个 java 的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报 OOM +本地内存概述图: -*** + -##### 三色标记 +*** -###### 标记算法 -三色标记法把遍历对象图过程中遇到的对象,标记成以下三种颜色: -- 白色:尚未访问过 -- 灰色:本对象已访问过,但是本对象引用到的其他对象尚未全部访问 -- 黑色:本对象已访问过,而且本对象引用到的其他对象也全部访问完成 - -当 Stop The World (STW) 时,对象间的引用是不会发生变化的,可以轻松完成标记,遍历访问过程为: - -1. 初始时,所有对象都在白色集合 -2. 将 GC Roots 直接引用到的对象挪到灰色集合 -3. 从灰色集合中获取对象: - * 将本对象引用到的其他对象全部挪到灰色集合中 - * 将本对象挪到黑色集合里面 -4. 重复步骤 3,直至灰色集合为空时结束 -5. 结束后,仍在白色集合的对象即为 GC Roots 不可达,可以进行回收 - - +#### 元空间 +PermGen 被元空间代替,永久代的**类信息、方法、常量池**等都移动到元空间区 +元空间与永久代区别:元空间不在虚拟机中,使用的本地内存,默认情况下,元空间的大小仅受本地内存限制 -参考文章:https://www.jianshu.com/p/12544c0ad5c1 +方法区内存溢出: +* JDK1.8 以前会导致永久代内存溢出:java.lang.OutOfMemoryError: PerGen space + ```sh + -XX:MaxPermSize=8m #参数设置 + ``` + +* JDK1.8 以后会导致元空间内存溢出:java.lang.OutOfMemoryError: Metaspace -**** + ```sh + -XX:MaxMetaspaceSize=8m #参数设置 + ``` +元空间内存溢出演示: +```java +public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码 + public static void main(String[] args) { + int j = 0; + try { + Demo1_8 test = new Demo1_8(); + for (int i = 0; i < 10000; i++, j++) { + // ClassWriter 作用是生成类的二进制字节码 + ClassWriter cw = new ClassWriter(0); + // 版本号, public, 类名, 包名, 父类, 接口 + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); + // 返回 byte[] + byte[] code = cw.toByteArray(); + // 执行了类的加载 + test.defineClass("Class" + i, code, 0, code.length); // Class 对象 + } + } finally { + System.out.println(j); + } + } +} +``` -###### 并发标记 -并发标记时,对象间的引用可能发生变化**,**多标和漏标的情况就有可能发生 -**多标情况:**当 E 变为灰色或黑色时,其他线程断开的 D 对 E 的引用,导致这部分对象仍会被标记为存活,本轮 GC 不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为**浮动垃圾** +*** -* 针对并发标记开始后的**新对象**,通常的做法是直接全部当成黑色,也算浮动垃圾 -* 浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除 - -**漏标情况:** +#### 直接内存 -* 条件一:灰色对象断开了对一个白色对象的引用(直接或间接),即灰色对象原成员变量的引用发生了变化 -* 条件二:其他线程中修改了黑色对象,插入了一条或多条对该白色对象的新引用 -* 结果:导致该白色对象当作垃圾被 GC,影响到了应用程序的正确性 +直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域 - -代码角度解释漏标: -```java -Object G = objE.fieldG; // 读 -objE.fieldG = null; // 写 -objD.fieldG = G; // 写 -``` +直接内存详解参考:NET → NIO → 直接内存 -为了解决问题,可以操作上面三步,**将对象 G 记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的 GC Roots 遍历完(并发标记),再遍历该集合(重新标记) -> 所以**重新标记需要 STW**,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完 -解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理: +*** -* **写屏障 + 增量更新**:黑色对象新增引用,会将黑色对象变成灰色对象,最后对该节节点重新扫描 - 增量更新 (Incremental Update) 破坏了条件二,从而保证了不会漏标 - 缺点:对黑色变灰的对象重新扫描所有引用,比较耗费时间 +### 变量位置 -* **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 +变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的**声明位置** - 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了),重新扫描该对象的引用关系 +静态内部类和其他内部类: - SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 +* **一个 class 文件只能对应一个 public 类型的类**,这个类可以有内部类,但不会生成新的 class 文件 -* **读屏障 (Load Barrier)**:破坏条件二,黑色对象引用白色对象的前提是获取到该对象,此时读屏障发挥作用 +* 静态内部类属于类本身,加载到方法区,其他内部类属于内部类的属性,加载到堆(待考证) -以 Java HotSpot VM 为例,其并发标记时对漏标的处理方案如下: +类变量: -- CMS:写屏障 + 增量更新 -- G1:写屏障 + SATB -- ZGC:读屏障 +* 类变量是用 static 修饰符修饰,定义在方法外的变量,随着 java 进程产生和销毁 +* 在 java8 之前把静态变量存放于方法区,在 java8 时存放在堆中的静态变量区 +实例变量: -*** +* 实例(成员)变量是定义在类中,没有 static 修饰的变量,随着类的实例产生和销毁,是类实例的一部分 +* 在类初始化的时候,从运行时常量池取出直接引用或者值,**与初始化的对象一起放入堆中** +局部变量: +* 局部变量是定义在类的方法中的变量 +* 在所在方法被调用时**放入虚拟机栈的栈帧**中,方法执行结束后从虚拟机栈中弹出, -#### finalization +类常量池、运行时常量池、字符串常量池有什么关系?有什么区别? -Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑 +* 类常量池与运行时常量池都存储在方法区,而字符串常量池在 jdk7 时就已经从方法区迁移到了 java 堆中 +* 在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是类常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符 +* **在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池** +* 对于文本字符,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池 -垃圾回收此对象之前,会先调用这个对象的 finalize() 方法,finalize() 方法允许在子类中被重写,用于在对象被回收时进行后置处理,通常在这个方法中进行一些资源释放和清理,比如关闭文件、套接字和数据库连接等 +什么是字面量?什么是符号引用? -生存 OR 死亡:如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用,此对象需要被回收。但事实上这时候它们暂时处于缓刑阶段。**一个无法触及的对象有可能在某个条件下复活自己**,这样对它的回收就是不合理的,所以虚拟机中的对象可能的三种状态: +* 字面量:java 代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示 -- 可触及的:从根节点开始,可以到达这个对象。 -- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize() 中复活 -- 不可触及的:对象的 finalize() 被调用并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,因为 **finalize() 只会被调用一次**,等到这个对象再被标记为可回收时就必须回收 + ```java + int a = 1; //这个1便是字面量 + String b = "iloveu"; //iloveu便是字面量 + ``` -永远不要主动调用某个对象的 finalize() 方法,应该交给垃圾回收机制调用,原因: +* 符号引用:在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,如果在一个类中引用了另一个类,无法知道它的内存地址,只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取内存地址 -* finalize() 时可能会导致对象复活 -* finalize() 方法的执行时间是没有保障的,完全由 GC 线程决定,极端情况下,若不发生 GC,则 finalize() 方法将没有执行机会,因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收 -* 一个糟糕的 finalize() 会严重影响 GC 的性能 @@ -10661,87 +10904,75 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 -#### 引用分析 -无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关,Java 提供了四种强度不同的引用类型 -1. 强引用:被强引用关联的对象不会被回收,只有所有 GCRoots 都不通过强引用引用该对象,才能被垃圾回收 +## 内存管理 - * 强引用可以直接访问目标对象 - * 虚拟机宁愿抛出 OOM 异常,也不会回收强引用所指向对象 - * 强引用可能导致**内存泄漏** +### 内存分配 - ```java - Object obj = new Object();//使用 new 一个新对象的方式来创建强引用 - ``` +#### 两种方式 -2. 软引用(SoftReference):被软引用关联的对象只有在内存不够的情况下才会被回收 +不分配内存的对象无法进行其他操作,JVM 为对象分配内存的过程:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象 - * **仅(可能有强引用,一个对象可以被多个引用)**有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象 - * 配合**引用队列来释放软引用自身**,在构造软引用时,可以指定一个引用队列,当软引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况 - * 软引用通常用来实现内存敏感的缓存,比如高速缓存就有用到软引用;如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时不会耗尽内存 +* 如果内存规整,使用指针碰撞(Bump The Pointer)。所有用过的内存在一边,空闲的内存在另外一边,中间有一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离 +* 如果内存不规整,虚拟机需要维护一个空闲列表(Free List)分配。已使用的内存和未使用的内存相互交错,虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 - ```java - Object obj = new Object(); - SoftReference sf = new SoftReference(obj); - obj = null; // 使对象只被软引用关联 - ``` -3. 弱引用(WeakReference):被弱引用关联的对象一定会被回收,只能存活到下一次垃圾回收发生之前 - * 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 - * 配合引用队列来释放弱引用自身 - * WeakHashMap 用来存储图片信息,可以在内存不足的时候及时回收,避免了 OOM +*** - ```java - Object obj = new Object(); - WeakReference wf = new WeakReference(obj); - obj = null; - ``` -4. 虚引用(PhantomReference):也称为幽灵引用或者幻影引用,是所有引用类型中最弱的一个 - * 一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象 - * 为对象设置虚引用的唯一目的是在于跟踪垃圾回收过程,能在这个对象被回收时收到一个系统通知 - * 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存 +#### 分代思想 - ```java - Object obj = new Object(); - PhantomReference pf = new PhantomReference(obj, null); - obj = null; - ``` +##### 分代介绍 -5. 终结器引用(finalization) +Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代 +- 新生代使用:复制算法 +- 老年代使用:标记 - 清除 或者 标记 - 整理 算法 +**Minor GC 和 Full GC**: -*** +- Minor GC:回收新生代,新生代对象存活时间很短,所以 Minor GC 会频繁执行,执行的速度比较快 +- Full GC:回收老年代和新生代,老年代对象其存活时间长,所以 Full GC 很少执行,执行速度会比 Minor GC 慢很多 + Eden 和 Survivor 大小比例默认为 8:1:1 + -#### 无用属性 -##### 无用类 -方法区主要回收的是无用的类 -判定一个类是否是无用的类,需要同时满足下面 3 个条件: -- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例 -- 加载该类的 `ClassLoader` 已经被回收 -- 该类对应的 `java.lang.Class` 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 +*** -虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的**仅仅是可以**,而并不是和对象一样不使用了就会必然被回收 +##### 分代分配 -*** +工作机制: +* **对象优先在 Eden 分配**:当创建一个对象的时候,对象会被分配在新生代的 Eden 区,当Eden 区要满了时候,触发 YoungGC +* 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到 to 区,并且当前对象的年龄会加 1,清空 Eden 区 +* 当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 to 中的对象,移动到 from 区中,这些对象的年龄会加 1,清空 Eden 区和 to 区 +* To 区永远是空 Survivor 区,From 区是有数据的,每次 MinorGC 后两个区域互换 +* From 区和 To 区 也可以叫做 S0 区和 S1 区 +晋升到老年代: -##### 废弃常量 +* **长期存活的对象进入老年代**:为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中 + + `-XX:MaxTenuringThreshold`:定义年龄的阈值,对象头中用 4 个 bit 存储,所以最大值是 15,默认也是 15 +* **大对象直接进入老年代**:需要连续内存空间的对象,最典型的大对象是很长的字符串以及数组;避免在 Eden 和 Survivor 之间的大量复制;经常出现大对象会提前触发 GC 以获取足够的连续空间分配给大对象 + + `-XX:PretenureSizeThreshold`:大于此值的对象直接在老年代分配 +* **动态对象年龄判定**:如果在 Survivor 区中相同年龄的对象的所有大小之和超过 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代 -在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该常量,说明常量 "abc" 是废弃常量,如果这时发生内存回收的话**而且有必要的话**(内存不够用),"abc" 就会被系统清理出常量池 +空间分配担保: + +* 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的 +* 如果不成立,虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试着进行一次 Minor GC;如果小于或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。 @@ -10749,163 +10980,215 @@ Java 语言提供了对象终止(finalization)机制来允许开发人员提 -##### 静态变量 +#### TLAB -类加载时(第一次访问),这个类中所有静态成员就会被加载到静态变量区,该区域的成员一旦创建,直到程序退出才会被回收 +TLAB:Thread Local Allocation Buffer,为每个线程在堆内单独分配了一个缓冲区,多线程分配内存时,使用 TLAB 可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做**快速分配策略** -如果是静态引用类型的变量,静态变量区只存储一份对象的引用地址,真正的对象在堆内,如果要回收该对象可以设置引用为 null +- 栈上分配使用的是栈来进行对象内存的分配 +- TLAB 分配使用的是 Eden 区域进行内存分配,属于堆内存 +堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度 +问题:堆空间都是共享的么? 不一定,因为还有 TLAB,在堆中划分出一块区域,为每个线程所独占 -参考文章:https://blog.csdn.net/zhengzhb/article/details/7331354 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配策略.jpg) +JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在 TLAB 空间分配内存失败时,JVM 就会通过**使用加锁机制确保数据操作的原子性**,从而直接在堆中分配内存 +栈上分配优先于 TLAB 分配进行,逃逸分析中若可进行栈上分配优化,会优先进行对象栈上直接分配内存 -*** +参数设置: +* `-XX:UseTLAB`:设置是否开启 TLAB 空间 +* `-XX:TLABWasteTargetPercent`:设置 TLAB 空间所占用 Eden 空间的百分比大小,默认情况下 TLAB 空间的内存非常小,仅占有整个 Eden 空间的1% +* `-XX:TLABRefillWasteFraction`:指当 TLAB 空间不足,请求分配的对象内存大小超过此阈值时不会进行 TLAB 分配,直接进行堆内存分配,否则还是会优先进行 TLAB 分配 -### 回收算法 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-TLAB内存分配过程.jpg) -#### 标记清除 -当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在 JVM 中比较常见的三种垃圾收集算法是 -- 标记清除算法(Mark-Sweep) -- 复制算法(copying) -- 标记压缩算法(Mark-Compact) +*** -标记清除算法,是将垃圾回收分为两个个阶段,分别是**标记和清除** -- **标记**:Collector 从引用根节点开始遍历,标记所有被引用的对象,一般是在对象的 Header 中记录为可达对象,**标记的是引用的对象,不是垃圾** -- **清除**:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收,把分块连接到 **空闲列表**”的单向链表,判断回收后的分块与前一个空闲分块是否连续,若连续会合并这两个分块,之后进行分配时只需要遍历这个空闲列表,就可以找到分块 -- **分配阶段**:程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block,如果找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 block - size 的两部分,返回大小为 size 的分块,并把大小为 block - size 的块返回给空闲列表 +#### 逃逸分析 -算法缺点: +即时编译(Just-in-time Compilation,JIT)是一种通过在运行时将字节码翻译为机器码,从而改善性能的技术,在 HotSpot 实现中有多种选择:C1、C2 和 C1+C2,分别对应 client、server 和分层编译 -- 标记和清除过程效率都不高 -- 会产生大量不连续的内存碎片,导致无法给大对象分配内存,需要维护一个空闲链表 +* C1 编译速度快,优化方式比较保守;C2 编译速度慢,优化方式比较激进 +* C1+C2 在开始阶段采用 C1 编译,当代码运行到一定热度之后采用 C2 重新编译 - +逃逸分析并不是直接的优化手段,而是一个代码分析方式,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸 +* 方法逃逸:当一个对象在方法中定义之后,被外部方法引用 + * 全局逃逸:一个对象的作用范围逃出了当前方法或者当前线程,比如对象是一个静态变量、全局变量赋值、已经发生逃逸的对象、作为当前方法的返回值 + * 参数逃逸:一个对象被作为方法参数传递或者被参数引用 +* 线程逃逸:如类变量或实例变量,可能被其它线程访问到 +如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配 -*** - +* 同步消除 + 线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,不被其它线程访问到,那对象的读写就不会存在竞争,则可以消除对该对象的**同步锁**,通过 `-XX:+EliminateLocks` 可以开启同步消除 ( - 号关闭) -#### 复制算法 +* 标量替换 -复制算法的核心就是,**将原有的内存空间一分为二,每次只用其中的一块**,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清理,交换两个内存的角色,完成垃圾的回收 + * 标量替换:如果把一个对象拆散,将其成员变量恢复到基本类型来访问 + * 标量 (scalar) :不可分割的量,如基本数据类型和 reference 类型 + + 聚合量 (Aggregate):一个数据可以继续分解,对象一般是聚合量 + * 如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替 + * 参数设置: + + * `-XX:+EliminateAllocations`:开启标量替换 + * `-XX:+PrintEliminateAllocations`:查看标量替换情况 -应用场景:如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之则不适合 +* 栈上分配 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-复制算法.png) + JIT 编译器在编译期间根据逃逸分析的结果,如果一个对象没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需 GC -算法优点: + User 对象的作用域局限在方法 fn 中,可以使用标量替换的优化手段在栈上分配对象的成员变量,这样就不会生成 User 对象,大大减轻 GC 的压力 -- 没有标记和清除过程,实现简单,运行高效 -- 复制过去以后保证空间的连续性,不会出现“碎片”问题。 + ```java + public class JVM { + public static void main(String[] args) throws Exception { + int sum = 0; + int count = 1000000; + //warm up + for (int i = 0; i < count ; i++) { + sum += fn(i); + } + System.out.println(sum); + System.in.read(); + } + private static int fn(int age) { + User user = new User(age); + int i = user.getAge(); + return i; + } + } + + class User { + private final int age; + + public User(int age) { + this.age = age; + } + + public int getAge() { + return age; + } + } + ``` -算法缺点: + -- 主要不足是**只使用了内存的一半** -- 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销都不小 +*** -现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 +### 回收策略 -*** +#### 触发条件 +内存垃圾回收机制主要集中的区域就是线程共享区域:**堆和方法区** +Minor GC 触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC -#### 标记整理 +FullGC 同时回收新生代、老年代和方法区,只会存在一个 FullGC 的线程进行执行,其他的线程全部会被**挂起**,有以下触发条件: -标记整理(压缩)算法是在标记清除算法的基础之上,做了优化改进的算法 +* 调用 System.gc(): -标记阶段和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而**解决了碎片化**的问题 + * 在默认情况下,通过 System.gc() 或 Runtime.getRuntime().gc() 的调用,会显式触发 FullGC,同时对老年代和新生代进行回收,但是虚拟机不一定真正去执行,无法保证对垃圾收集器的调用 + * 不建议使用这种方式,应该让虚拟机管理内存。一般情况下,垃圾回收应该是自动进行的,无须手动触发;在一些特殊情况下,如正在编写一个性能基准,可以在运行之间调用 System.gc() -优点:不会产生内存碎片 +* 老年代空间不足: -缺点:需要移动大量对象,处理效率比较低 + * 为了避免引起的 Full GC,应当尽量不要创建过大的对象以及数组 + * 通过 -Xmn 参数调整新生代的大小,让对象尽量在新生代被回收掉不进入老年代,可以通过 `-XX:MaxTenuringThreshold` 调大对象进入老年代的年龄,让对象在新生代多存活一段时间 - +* 空间分配担保失败 -| | Mark-Sweep | Mark-Compact | Copying | -| -------- | ---------------- | -------------- | ----------------------------------- | -| 速度 | 中等 | 最慢 | 最快 | -| 空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍大小(不堆积碎片) | -| 移动对象 | 否 | 是 | 是 | +* JDK 1.7 及以前的永久代(方法区)空间不足 -- 效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。 -- 为了尽量兼顾三个指标,标记一整理算法相对来说更平滑一些 +* Concurrent Mode Failure:执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC +手动 GC 测试,VM参数:`-XX:+PrintGcDetails` -*** +```java +public void localvarGC1() { + byte[] buffer = new byte[10 * 1024 * 1024];//10MB + System.gc(); //输出: 不会被回收, FullGC时被放入老年代 +} +public void localvarGC2() { + byte[] buffer = new byte[10 * 1024 * 1024]; + buffer = null; + System.gc(); //输出: 正常被回收 +} + public void localvarGC3() { + { + byte[] buffer = new byte[10 * 1024 * 1024]; + } + System.gc(); //输出: 不会被回收, FullGC时被放入老年代 + } +public void localvarGC4() { + { + byte[] buffer = new byte[10 * 1024 * 1024]; + } + int value = 10; + System.gc(); //输出: 正常被回收,slot复用,局部变量过了其作用域 buffer置空 +} +``` -#### 增量收集 -增量收集算法:通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作,基础仍是标记-清除和复制算法,用于多线程并发环境 -工作原理:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,影响系统的交互性,所以让垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复直到垃圾收集完成 +*** -缺点:线程切换和上下文转换消耗资源,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降 +#### 安全区域 +安全点 (Safepoint):程序执行时并非在所有地方都能停顿下来开始 GC,只有在安全点才能停下 +- Safe Point 的选择很重要,如果太少可能导致 GC 等待的时间太长,如果太多可能导致运行时的性能问题 +- 大部分指令的执行时间都非常短,通常会根据是否具有让程序长时间执行的特征为标准,选择些执行时间较长的指令作为 Safe Point, 如方法调用、循环跳转和异常跳转等 -*** +在 GC 发生时,让所有线程都在最近的安全点停顿下来的方法: +- 抢先式中断:没有虚拟机采用,首先中断所有线程,如果有线程不在安全点,就恢复线程让线程运行到安全点 +- 主动式中断:设置一个中断标志,各个线程运行到各个 Safe Point 时就轮询这个标志,如果中断标志为真,则将自己进行中断挂起 +问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应 JVM 的中断请求,运行到安全点去中断挂起,JVM 也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决 -### 垃圾回收器 +安全区域 (Safe Region):指在一段代码片段中,**对象的引用关系不会发生变化**,在这个区域中的任何位置开始 GC 都是安全的 -#### 概述 +运行流程: -垃圾收集器分类: +- 当线程运行到 Safe Region 的代码时,首先标识已经进入了 Safe Region,如果这段时间内发生GC,JVM 会忽略标识为 Safe Region 状态的线程 -* 按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器 - * 除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行 -* 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器 - * 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间 - * 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束 -* 按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器 - * 压缩式垃圾回收器在回收完成后进行压缩整理,消除回收后的碎片,再分配对象空间使用指针碰撞 - * 非压缩式的垃圾回收器不进行这步操作,再分配对象空间使用空闲列表 -* 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器 +- 当线程即将离开 Safe Region 时,会检查 JVM 是否已经完成 GC,如果完成了则继续运行,否则线程必须等待 GC 完成,收到可以安全离开 SafeRegion 的信号 -GC 性能指标: -- **吞吐量**:程序的运行时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间) -- 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例 -- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间 -- 收集频率:相对于应用程序的执行,收集操作发生的频率 -- 内存占用:Java 堆区所占的内存大小 -- 快速:一个对象从诞生到被回收所经历的时间 -**垃圾收集器的组合关系**: +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器关系图.png) -新生代收集器:Serial、ParNew、Paralle1 Scavenge; -老年代收集器:Serial old、Parallel old、CMS; +### 垃圾判断 -整堆收集器:G1 +#### 垃圾介绍 -* 红色虚线在 JDK9 移除、绿色虚线在 JDK14 弃用该组合、青色虚线在 JDK14 删除 CMS 垃圾回收器 +垃圾:**如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾** -查看默认的垃圾收回收器: +作用:释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象 -* `-XX:+PrintcommandLineFlags`:查看命令行相关参数(包含使用的垃圾收集器) +垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收 -* 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程 ID +在堆里存放着几乎所有的 Java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程可以称为垃圾标记阶段,判断对象存活一般有两种方式:**引用计数算法**和**可达性分析算法** @@ -10913,209 +11196,199 @@ GC 性能指标: -#### Serial +#### 引用计数法 -Serial:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法** +引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1;当引用失效时,引用计数器就减 1;当对象 A 的引用计数器的值为 0,即表示对象A不可能再被使用,可进行回收(Java 没有采用) -**STW(Stop-The-World)**:垃圾回收时,只有一个线程在工作,并且 java 应用中的所有线程都要暂停,等待垃圾回收的完成 +优点: -**Serial old**:执行老年代垃圾回收的串行收集器,内存回收算法使用的是**标记-整理算法**,同样也采用了串行回收和 STW 机制 +- 回收没有延迟性,无需等到内存不够的时候才开始回收,运行时根据对象计数器是否为 0,可以直接回收 +- 在垃圾回收过程中,应用无需挂起;如果申请内存时,内存不足,则立刻报 OOM 错误 +- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象 -- Serial old 是 Client 模式下默认的老年代的垃圾回收器 -- Serial old 在 Server 模式下主要有两个用途: - - 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用 - - 作为老年代 CMS 收集器的**后备垃圾回收方案**,在并发收集发生 Concurrent Mode Failure 时使用 +缺点: -开启参数:`-XX:+UseSerialGC` 等价于新生代用 Serial GC 且老年代用 Serial old GC +- 每次对象被引用时,都需要去更新计数器,有一点时间开销 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-Serial收集器.png) +- 浪费 CPU 资源,即使内存够用,仍然在运行时进行计数器的统计。 -优点:简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,可以获得最高的单线程收集效率 +- **无法解决循环引用问题,会引发内存泄露**(最大的缺点) -缺点:对于交互性较强的应用而言,这种垃圾收集器是不能够接受的,比如 JavaWeb 应用 + ```java + public class Test { + public Object instance = null; + public static void main(String[] args) { + Test a = new Test();// a = 1 + Test b = new Test();// b = 1 + a.instance = b; // b = 2 + b.instance = a; // a = 2 + a = null; // a = 1 + b = null; // b = 1 + } + } + ``` +![](https://gitee.com/seazean/images/raw/master/Java/JVM-循环引用.png) -**** +*** -#### Parallel -Parallel Scavenge 收集器是应用于新生代的并行垃圾回收器,**采用复制算法**、并行回收和 Stop the World 机制 +#### 可达性分析 -Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,**采用标记-整理算法** +##### GC Roots -对比其他回收器: +可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集 -* 其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间 -* Parallel 目标是达到一个可控制的吞吐量,被称为**吞吐量优先**收集器 -* Parallel Scavenge 对比 ParNew 拥有**自适应调节策略**,可以通过一个开关参数打开 GC Ergonomics +GC Roots 对象: -应用场景: +- 虚拟机栈中局部变量表中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等 +- 本地方法栈中引用的对象 +- 堆中类静态属性引用的对象 +- 方法区中的常量引用的对象 +- 字符串常量池(string Table)里的引用 +- 同步锁 synchronized 持有的对象 -* 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验 -* 高吞吐量可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互 +**GC Roots 是一组活跃的引用,不是对象**,放在 GC Roots Set 集合 -停顿时间和吞吐量的关系:新生代空间变小 → 缩短停顿时间 → 垃圾回收变得频繁 → 导致吞吐量下降 -在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge + Parallel Old 收集器,在 Server 模式下的内存回收性能很好,**Java8 默认是此垃圾收集器组合** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParallelScavenge收集器.png) +*** -参数配置: -* `-XX:+UseParallelGC`:手动指定年轻代使用Paralle并行收集器执行内存回收任务 -* `-XX:+UseParalleloldcc`:手动指定老年代使用并行回收收集器执行内存回收任务 - * 上面两个参数,默认开启一个,另一个也会被开启(互相激活),默认 jdk8 是开启的 -* `-XX:+UseAdaptivesizepplicy`:设置 Parallel scavenge 收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量 -* `-XX:ParallelGcrhreads`:设置年轻代并行收集器的线程数,一般最好与 CPU 数量相等,以避免过多的线程数影响垃圾收集性能 - * 在默认情况下,当 CPU 数量小于 8 个,ParallelGcThreads 的值等于 CPU 数量 - * 当 CPU 数量大于 8 个,ParallelGCThreads 的值等于 3+[5*CPU Count]/8] -* `-XX:MaxGCPauseMillis`:设置垃圾收集器最大停顿时间(即 STW 的时间),单位是毫秒 - * 对于用户来讲,停顿时间越短体验越好;在服务器端,注重高并发,整体的吞吐量 - * 为了把停顿时间控制在 MaxGCPauseMillis 以内,收集器在工作时会调整 Java 堆大小或其他一些参数 -* `-XX:GCTimeRatio`:垃圾收集时间占总时间的比例 =1/(N+1),用于衡量吞吐量的大小 - * 取值范围(0,100)。默认值 99,也就是垃圾回收时间不超过1 - * 与 `-xx:MaxGCPauseMillis` 参数有一定矛盾性,暂停时间越长,Radio 参数就容易超过设定的比例 +##### 工作原理 +可达性分析算法以根对象集合(GCRoots)为起始点,从上至下的方式搜索被根对象集合所连接的目标对象 -*** +分析工作必须在一个保障**一致性的快照**中进行,否则结果的准确性无法保证,这也是导致 GC 进行时必须 Stop The World 的一个原因 +基本原理: +- 可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链 -#### ParNew +- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象 -Par 是 Parallel 并行的缩写,New 是只能处理的是新生代 +- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象 -并行垃圾收集器在串行垃圾收集器的基础之上做了改进,**采用复制算法**,将单线程改为了多线程进行垃圾回收,可以缩短垃圾回收的时间 + -对于其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同 Serial 收集器一样,应用在年轻代,除 Serial 外,只有**ParNew GC 能与 CMS 收集器配合工作** - -相关参数: -* `-XX:+UseParNewGC`:表示年轻代使用并行收集器,不影响老年代 -* `-XX:ParallelGCThreads`:默认开启和 CPU 数量相同的线程数 +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParNew收集器.png) -ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器 -- 对于新生代,回收次数频繁,使用并行方式高效 -- 对于老年代,回收次数少,使用串行方式节省资源(CPU 并行需要切换线程,串行可以省去切换线程的资源) +##### 三色标记 +###### 标记算法 +三色标记法把遍历对象图过程中遇到的对象,标记成以下三种颜色: -**** +- 白色:尚未访问过 +- 灰色:本对象已访问过,但是本对象引用到的其他对象尚未全部访问 +- 黑色:本对象已访问过,而且本对象引用到的其他对象也全部访问完成 +当 Stop The World (STW) 时,对象间的引用是不会发生变化的,可以轻松完成标记,遍历访问过程为: +1. 初始时,所有对象都在白色集合 +2. 将 GC Roots 直接引用到的对象挪到灰色集合 +3. 从灰色集合中获取对象: + * 将本对象引用到的其他对象全部挪到灰色集合中 + * 将本对象挪到黑色集合里面 +4. 重复步骤 3,直至灰色集合为空时结束 +5. 结束后,仍在白色集合的对象即为 GC Roots 不可达,可以进行回收 -#### CMS + -CMS 全称 Concurrent Mark Sweep,是一款**并发的、使用标记-清除**算法、针对老年代的垃圾回收器,其最大特点是**让垃圾收集线程与用户线程同时工作** -CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(**低延迟**)越适合与用户交互的程序,良好的响应速度能提升用户体验 -分为以下四个流程: +参考文章:https://www.jianshu.com/p/12544c0ad5c1 -- 初始标记:使用 STW 出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 -- 并发标记:进行 GC Roots 开始遍历整个对象图,在整个回收过程中耗时最长,不需要 STW,可以与用户线程并发运行 -- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况) -- 并发清除:清除标记为可以回收对象,不需要移动存活对象,所以这个阶段可以与用户线程同时并发的 -Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因: -* Mark Compact 算法会整理内存,导致用户线程使用的对象的地址改变,影响用户线程继续执行 +**** -* Mark Compact 更适合 Stop The World 场景 -在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-CMS收集器.png) +###### 并发标记 -优点:并发收集、低延迟 +并发标记时,对象间的引用可能发生变化**,**多标和漏标的情况就有可能发生 -缺点: +**多标情况:**当 E 变为灰色或黑色时,其他线程断开的 D 对 E 的引用,导致这部分对象仍会被标记为存活,本轮 GC 不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为**浮动垃圾** -- 吞吐量降低:在并发阶段虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,CPU 利用率不够高 -- CMS 收集器**无法处理浮动垃圾**,可能出现 Concurrent Mode Failure 导致另一次 Full GC 的产生 - - 浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾(产生了新对象),这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,CMS 收集需要预留出一部分内存,不能等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS,导致很长的停顿时间 -- 标记 - 清除算法导致的空间碎片,往往出现老年代空间无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC;为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配 +* 针对并发标记开始后的**新对象**,通常的做法是直接全部当成黑色,也算浮动垃圾 +* 浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除 -参数设置: + -* `-XX:+UseConcMarkSweepGC`:手动指定使用 CMS 收集器执行内存回收任务 +**漏标情况:** - 开启该参数后会自动将 `-XX:+UseParNewGC` 打开,即:ParNew + CMS + Serial old的组合 +* 条件一:灰色对象断开了对一个白色对象的引用(直接或间接),即灰色对象原成员变量的引用发生了变化 +* 条件二:其他线程中修改了黑色对象,插入了一条或多条对该白色对象的新引用 +* 结果:导致该白色对象当作垃圾被 GC,影响到了应用程序的正确性 -* `-XX:CMSInitiatingoccupanyFraction`:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收 + - * JDK5 及以前版本的默认值为 68,即当老年代的空间使用率达到 68% 时,会执行一次CMS回收 - * JDK6 及以上版本默认值为 92% +代码角度解释漏标: -* `-XX:+UseCMSCompactAtFullCollection`:用于指定在执行完 Full GC 后对内存空间进行压缩整理,以此避免内存碎片的产生,由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长 +```java +Object G = objE.fieldG; // 读 +objE.fieldG = null; // 写 +objD.fieldG = G; // 写 +``` -* `-XX:CMSFullGCsBeforecompaction`:**设置在执行多少次 Full GC 后对内存空间进行压缩整理** +为了解决问题,可以操作上面三步,**将对象 G 记录起来,然后作为灰色对象再进行遍历**,比如放到一个特定的集合,等初始的 GC Roots 遍历完(并发标记),再遍历该集合(重新标记) -* `-XX:ParallelCMSThreads`:**设置CMS的线程数量** +> 所以**重新标记需要 STW**,应用程序一直在运行,该集合可能会一直增加新的对象,导致永远都运行不完 - * CMS 默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数 - * 收集线程占用的 CPU 资源多于25%,对用户程序影响可能较大;当 CPU 资源比较紧张时,受到 CMS 收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕 +解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理: +* **写屏障 + 增量更新**:黑色对象新增引用,会将黑色对象变成灰色对象,最后对该节节点重新扫描 + 增量更新 (Incremental Update) 破坏了条件二,从而保证了不会漏标 -*** + 缺点:对黑色变灰的对象重新扫描所有引用,比较耗费时间 +* **写屏障 (Store Barrier) + SATB**:当原来成员变量的引用发生变化之前,记录下原来的引用对象 + 保留 GC 开始时的对象图,即原始快照 SATB,当 GC Roots 确定后,对象图就已经确定,那后续的标记也应该是按照这个时刻的对象图走,如果期间对白色对象有了新的引用会记录下来,并且将白色对象变灰(说明可达了),重新扫描该对象的引用关系 -#### G1 + SATB (Snapshot At The Beginning) 破坏了条件一,从而保证了不会漏标 -##### G1特点 +* **读屏障 (Load Barrier)**:破坏条件二,黑色对象引用白色对象的前提是获取到该对象,此时读屏障发挥作用 -G1(Garbage-First)是一款面向服务端应用的垃圾收集器,**应用于新生代和老年代**、采用标记-整理算法、软实时、低延迟、可设定目标(最大STW停顿时间)的垃圾回收器,用于代替 CMS,适用于较大的堆(>4 ~ 6G),在 JDK9 之后默认使用 G1 +以 Java HotSpot VM 为例,其并发标记时对漏标的处理方案如下: -G1 对比其他处理器的优点: +- CMS:写屏障 + 增量更新 +- G1:写屏障 + SATB +- ZGC:读屏障 -* 并发与并行: - * 并行性:G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力,此时用户线程 STW - * 并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此不会在整个回收阶段发生完全阻塞应用程序的情况 - * 其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作,而 G1 GC 可以采用应用线程承担后台运行的 GC 工作,JVM 的 GC 线程处理速度慢时,系统会**调用应用程序线程加速垃圾回收**过程 -* **分区算法**: - * 从分代上看,G1 属于分代型垃圾回收器,区分年轻代和老年代,年轻代依然有 Eden 区和 Survivor 区。从堆结构上看,**新生代和老年代不再物理隔离**,不用担心每个代内存是否足够,这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC - * 将整个堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32 MB之间且为 2 的 N 次幂,所有 Region 大小相同,在 JVM 生命周期内不会被改变。G1 把堆划分成多个大小相等的独立区域,使得每个小空间可以单独进行垃圾回收 - * **新的区域 Humongous**:本身属于老年代区,当出现了一个巨型对象超出了分区容量的一半,该对象就会进入到该区域。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储,为了能找到连续的H区,有时候不得不启动 Full GC - * G1 不会对巨型对象进行拷贝,回收时被优先考虑,G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为 0 的巨型对象就可以在新生代垃圾回收时处理掉 - - * Region 结构图: - -![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1-Region区域.png) +*** -- 空间整合: - - CMS:标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理 - - G1:整体来看是基于标记 - 整理算法实现的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片 -- **可预测的停顿时间模型(软实时 soft real-time)**:可以指定在 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 +#### finalization - - 由于分块的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,对于全局停顿情况也能得到较好的控制 - - G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个**优先列表**,每次根据允许的收集时间优先回收价值最大的 Region,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率 +Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑 - * 相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多 +垃圾回收此对象之前,会先调用这个对象的 finalize() 方法,finalize() 方法允许在子类中被重写,用于在对象被回收时进行后置处理,通常在这个方法中进行一些资源释放和清理,比如关闭文件、套接字和数据库连接等 -G1垃圾收集器的缺点: +生存 OR 死亡:如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用,此对象需要被回收。但事实上这时候它们暂时处于缓刑阶段。**一个无法触及的对象有可能在某个条件下复活自己**,这样对它的回收就是不合理的,所以虚拟机中的对象可能的三种状态: -* 相较于 CMS,G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 要高 -* 从经验上来说,在小内存应用上 CMS 的表现大概率会优于 G1,而 G1 在大内存应用上则发挥其优势。平衡点在 6-8GB 之间 +- 可触及的:从根节点开始,可以到达这个对象。 +- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize() 中复活 +- 不可触及的:对象的 finalize() 被调用并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,因为 **finalize() 只会被调用一次**,等到这个对象再被标记为可回收时就必须回收 -应用场景: +永远不要主动调用某个对象的 finalize() 方法,应该交给垃圾回收机制调用,原因: -* 面向服务端应用,针对具有大内存、多处理器的机器 -* 需要低 GC 延迟,并具有大堆的应用程序提供解决方案 +* finalize() 时可能会导致对象复活 +* finalize() 方法的执行时间是没有保障的,完全由 GC 线程决定,极端情况下,若不发生 GC,则 finalize() 方法将没有执行机会,因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收 +* 一个糟糕的 finalize() 会严重影响 GC 的性能 @@ -11123,282 +11396,278 @@ G1垃圾收集器的缺点: -##### 记忆集 +#### 引用分析 -记忆集 Remembered Set 在新生代中,每个 Region 都有一个 Remembered Set,用来被哪些其他 Region 里的对象引用(谁引用了我就记录谁) +无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关,Java 提供了四种强度不同的引用类型 - +1. 强引用:被强引用关联的对象不会被回收,只有所有 GCRoots 都不通过强引用引用该对象,才能被垃圾回收 -* 程序对 Reference 类型数据写操作时,产生一个 Write Barrier 暂时中断操作,检查该对象和 Reference 类型数据是否在不同的 Region(跨代引用),不同就将相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中 -* 进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏 + * 强引用可以直接访问目标对象 + * 虚拟机宁愿抛出 OOM 异常,也不会回收强引用所指向对象 + * 强引用可能导致**内存泄漏** -垃圾收集器在新生代中建立了记忆集这样的数据结构,可以理解为它是一个抽象类,具体实现记忆集的三种方式: + ```java + Object obj = new Object();//使用 new 一个新对象的方式来创建强引用 + ``` -* 字长精度 -* 对象精度 -* 卡精度(卡表) +2. 软引用(SoftReference):被软引用关联的对象只有在内存不够的情况下才会被回收 -卡表(Card Table)在老年代中,是一种对记忆集的具体实现,主要定义了记忆集的记录精度、与堆内存的映射关系等,卡表中的每一个元素都对应着一块特定大小的内存块,这个内存块称之为卡页(card page),当存在跨代引用时,会将卡页标记为 dirty,JVM 对于卡页的维护也是通过写屏障的方式 + * **仅(可能有强引用,一个对象可以被多个引用)**有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象 + * 配合**引用队列来释放软引用自身**,在构造软引用时,可以指定一个引用队列,当软引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况 + * 软引用通常用来实现内存敏感的缓存,比如高速缓存就有用到软引用;如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时不会耗尽内存 -收集集合 CSet 代表每次 GC 暂停时回收的一系列目标分区,在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。年轻代收集 CSet 只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中 + ```java + Object obj = new Object(); + SoftReference sf = new SoftReference(obj); + obj = null; // 使对象只被软引用关联 + ``` -* CSet of Young Collection -* CSet of Mix Collection +3. 弱引用(WeakReference):被弱引用关联的对象一定会被回收,只能存活到下一次垃圾回收发生之前 + * 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 + * 配合引用队列来释放弱引用自身 + * WeakHashMap 用来存储图片信息,可以在内存不足的时候及时回收,避免了 OOM + ```java + Object obj = new Object(); + WeakReference wf = new WeakReference(obj); + obj = null; + ``` -*** +4. 虚引用(PhantomReference):也称为幽灵引用或者幻影引用,是所有引用类型中最弱的一个 + * 一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象 + * 为对象设置虚引用的唯一目的是在于跟踪垃圾回收过程,能在这个对象被回收时收到一个系统通知 + * 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存 + ```java + Object obj = new Object(); + PhantomReference pf = new PhantomReference(obj, null); + obj = null; + ``` -##### 工作原理 +5. 终结器引用(finalization) -G1 中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不同的条件下被触发 -* 当堆内存使用达到一定值(默认 45%)时,开始老年代并发标记过程 -* 标记完成马上开始混合回收过程 - +*** -顺时针:Young GC → Young GC + Concurrent Mark → Mixed GC 顺序,进行垃圾回收 -* **Young GC**:发生在年轻代的 GC 算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 young gc,G1 停止应用程序的执行 STW,把活跃对象放入老年代,垃圾对象回收 - **回收过程**: +#### 无用属性 - 1. 扫描根:根引用连同 RSet 记录的外部引用作为扫描存活对象的入口 - 2. 更新 RSet:处理 dirty card queue 更新 RS,此后 RSet 准确的反映对象的引用关系 - * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到 RSet - * 作用:产生引用直接更新 RSet 需要线程同步开销很大,使用队列性能好 - 3. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象,把需要回收的分区放入 Young CSet 中进行回收 - 4. 复制对象:Eden 区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 - 5. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作 +##### 无用类 -* **Concurrent Mark **: +方法区主要回收的是无用的类 - * 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC - * 根区域扫描 (Root Region Scanning):扫描 Survivor 区中指向老年代的,被初始标记标记了的引用及引用的对象,这一个过程是并发进行的,但是必须在 Young GC 之前完成 - * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。会计算每个区域的对象活性,即区域中存活对象的比例,若区域中的所有对象都是垃圾,则这个区域会被立即回收(实时回收),给浮动垃圾准备出更多的空间,把需要收集的 Region 放入 CSet 当中 - * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行(**防止漏标**) - * 筛选回收:并发清理阶段,首先对 CSet 中各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率 +判定一个类是否是无用的类,需要同时满足下面 3 个条件: - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1收集器.jpg) +- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例 +- 加载该类的 `ClassLoader` 已经被回收 +- 该类对应的 `java.lang.Class` 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 -* **Mixed GC**:当很多对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,除了回收整个 young region,还会**回收一部分**的 old region,过程同 YGC +虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的**仅仅是可以**,而并不是和对象一样不使用了就会必然被回收 - 注意:**是一部分老年代,而不是全部老年代**,可以选择哪些老年代 region 收集,对垃圾回收的时间进行控制 - 在 G1 中,Mixed GC 可以通过 `-XX:InitiatingHeapOccupancyPercent` 设置阈值 -* **Full GC**:对象内存分配速度过快,Mixed GC 来不及回收,导致老年代被填满,就会触发一次 Full GC,G1 的 Full GC 算法就是单线程执行的垃圾回收,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免 Full GC +*** - 产生 Full GC 的原因: - * 晋升时没有足够的空间存放晋升的对象 - * 并发处理过程完成之前空间耗尽,浮动垃圾 +##### 废弃常量 +在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该常量,说明常量 "abc" 是废弃常量,如果这时发生内存回收的话**而且有必要的话**(内存不够用),"abc" 就会被系统清理出常量池 -*** +*** -##### 相关参数 -- `-XX:+UseG1GC`:手动指定使用 G1 垃圾收集器执行内存回收任务 -- `-XX:G1HeapRegionSize`:设置每个 Region 的大小。值是 2 的幂,范围是 1MB 到 32MB 之间,目标是根据最小的 Java 堆大小划分出约 2048 个区域,默认是堆内存的 1/2000 -- `-XX:MaxGCPauseMillis`:设置期望达到的最大 GC 停顿时间指标,JVM会尽力实现,但不保证达到,默认值是 200ms -- `-XX:+ParallelGcThread`:设置 STW 时 GC 线程数的值,最多设置为 8 -- `-XX:ConcGCThreads`:设置并发标记线程数,设置为并行垃圾回收线程数 ParallelGcThreads 的1/4左右 -- `-XX:InitiatingHeapoccupancyPercent`:设置触发并发 Mixed GC 周期的 Java 堆占用率阈值,超过此值,就触发 GC,默认值是 45 -- `-XX:+ClassUnloadingWithConcurrentMark`:并发标记类卸载,默认启用,所有对象都经过并发标记后,就可以知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 -- `-XX:G1NewSizePercent`:新生代占用整个堆内存的最小百分比(默认5%) -- `-XX:G1MaxNewSizePercent`:新生代占用整个堆内存的最大百分比(默认60%) -- `-XX:G1ReservePercent=10`:保留内存区域,防止 to space(Survivor中的 to 区)溢出 +##### 静态变量 +类加载时(第一次访问),这个类中所有静态成员就会被加载到静态变量区,该区域的成员一旦创建,直到程序退出才会被回收 -*** +如果是静态引用类型的变量,静态变量区只存储一份对象的引用地址,真正的对象在堆内,如果要回收该对象可以设置引用为 null -##### 调优 +参考文章:https://blog.csdn.net/zhengzhb/article/details/7331354 -G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可完成调优: -1. 开启 G1 垃圾收集器 -2. 设置堆的最大内存 -3. 设置最大的停顿时间(STW) -不断调优暂停时间指标: +*** -* `XX:MaxGCPauseMillis=x` 可以设置启动应用程序暂停的时间,G1会根据这个参数选择 CSet 来满足响应时间的设置 -* 设置到 100ms 或者 200ms 都可以(不同情况下会不一样),但设置成50ms就不太合理 -* 暂停时间设置的太短,就会导致出现 G1 跟不上垃圾产生的速度,最终退化成 Full GC -* 对这个参数的调优是一个持续的过程,逐步调整到最佳状态 -不要设置新生代和老年代的大小: - -- 避免使用 -Xmn 或 -XX:NewRatio 等相关选项显式设置年轻代大小,G1 收集器在运行的时候会调整新生代和老年代的大小,从而达到我们为收集器设置的暂停时间目标 -- 设置了新生代大小相当于放弃了 G1 为我们做的自动调优,我们需要做的只是设置整个堆内存的大小,剩下的交给 G1 自己去分配各个代的大小 +### 回收算法 +#### 标记清除 -*** +当成功区分出内存中存活对象和死亡对象后,GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在 JVM 中比较常见的三种垃圾收集算法是 +- 标记清除算法(Mark-Sweep) +- 复制算法(copying) +- 标记压缩算法(Mark-Compact) +标记清除算法,是将垃圾回收分为两个个阶段,分别是**标记和清除** -#### ZGC +- **标记**:Collector 从引用根节点开始遍历,标记所有被引用的对象,一般是在对象的 Header 中记录为可达对象,**标记的是引用的对象,不是垃圾** +- **清除**:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收,把分块连接到 **空闲列表**”的单向链表,判断回收后的分块与前一个空闲分块是否连续,若连续会合并这两个分块,之后进行分配时只需要遍历这个空闲列表,就可以找到分块 -ZGC 收集器是一个可伸缩的、低延迟的垃圾收集器,基于 Region 内存布局的,不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现**可并发的标记压缩算法** +- **分配阶段**:程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block,如果找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 block - size 的两部分,返回大小为 size 的分块,并把大小为 block - size 的块返回给空闲列表 -* 在 CMS 和 G1 中都用到了写屏障,而 ZGC 用到了读屏障 -* 染色指针:直接**将少量额外的信息存储在指针上的技术**,从 64 位的指针中拿高 4 位来标识对象此时的状态 - * 染色指针可以使某个 Region 的存活对象被移走之后,这个 Region 立即就能够被释放和重用 - * 可以直接从指针中看到引用对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集、是否被移动过(Remapped)、是否只能通过 finalize() 方法才能被访问到(Finalizable) - * 可以大幅减少在垃圾收集过程中内存屏障的使用数量,写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作 - * 可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据 -* 内存多重映射:多个虚拟地址指向同一个物理地址 +算法缺点: -可并发的标记压缩算法:染色指针标识对象是否被标记或移动,读屏障保证在每次应用程序或 GC 程序访问对象时先根据染色指针的标识判断是否被移动,如果被移动就根据转发表访问新的移动对象,并更新引用,不会像 G1 一样必须等待垃圾回收完成才能访问 +- 标记和清除过程效率都不高 +- 会产生大量不连续的内存碎片,导致无法给大对象分配内存,需要维护一个空闲链表 -ZGC 目标: + -- 停顿时间不会超过 10ms -- 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在 10ms 以下) -- 可支持几百 M,甚至几 T 的堆大小(最大支持4T) -ZGC 的工作过程可以分为 4 个阶段: -* 并发标记(Concurrent Mark): 遍历对象图做可达性分析的阶段,也要经过初始标记和最终标记,需要短暂停顿 -* 并发预备重分配( Concurrent Prepare for Relocate):根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) -* 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的**每个 Region 维护一个转发表**(Forward Table),记录从旧地址到新地址的转向关系 -* 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象,这样合并节省了一次遍历的开销 +*** -ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的,但这部分的实际时间是非常少的,所以响应速度快,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟 -优点:高吞吐量、低延迟 -缺点:浮动垃圾,当 ZGC 准备要对一个很大的堆做一次完整的并发收集,其全过程要持续十分钟以上,由于应用的对象分配速率很高,将创造大量的新对象产生浮动垃圾 +#### 复制算法 +复制算法的核心就是,**将原有的内存空间一分为二,每次只用其中的一块**,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清理,交换两个内存的角色,完成垃圾的回收 +应用场景:如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之则不适合 -参考文章:https://www.cnblogs.com/jimoer/p/13170249.html +![](https://gitee.com/seazean/images/raw/master/Java/JVM-复制算法.png) +算法优点: +- 没有标记和清除过程,实现简单,运行高效 +- 复制过去以后保证空间的连续性,不会出现“碎片”问题。 -*** +算法缺点: +- 主要不足是**只使用了内存的一半** +- 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销都不小 +现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间 -#### 总结 -Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同: -- 最小化地使用内存和并行开销,选 Serial GC -- 最大化应用程序的吞吐量,选 Parallel GC -- 最小化 GC 的中断或停顿时间,选 CMS GC +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器总结.png) +#### 标记整理 +标记整理(压缩)算法是在标记清除算法的基础之上,做了优化改进的算法 +标记阶段和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而**解决了碎片化**的问题 -*** +优点:不会产生内存碎片 +缺点:需要移动大量对象,处理效率比较低 + -### 内存泄漏 +| | Mark-Sweep | Mark-Compact | Copying | +| -------- | ---------------- | -------------- | ----------------------------------- | +| 速度 | 中等 | 最慢 | 最快 | +| 空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍大小(不堆积碎片) | +| 移动对象 | 否 | 是 | 是 | -#### 泄露溢出 +- 效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。 +- 为了尽量兼顾三个指标,标记一整理算法相对来说更平滑一些 -内存泄漏(Memory Leak):是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果 -可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。由于代码的实现不同就会出现很多种内存泄漏问题,让 JVM 误以为此对象还在引用中,无法回收,造成内存泄漏 -内存溢出(out of memory)指的是申请内存时,没有足够的内存可以使用 +*** -内存泄漏和内存溢出的关系:内存泄漏的越来越多,最终会导致内存溢出 +#### 增量收集 -*** +增量收集算法:通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作,基础仍是标记-清除和复制算法,用于多线程并发环境 +工作原理:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,影响系统的交互性,所以让垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复直到垃圾收集完成 +缺点:线程切换和上下文转换消耗资源,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降 -#### 几种情况 -##### 静态集合 -静态集合类的生命周期与 JVM 程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。原因是**长生命周期的对象持有短生命周期对象的引用**,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收 -```java -public class MemoryLeak { - static List list = new ArrayList(); - public void oomTest(){ - Object obj = new Object();//局部变量 - list.add(obj); - } -} -``` +*** -*** +### 垃圾回收器 +#### 概述 -##### 单例模式 +垃圾收集器分类: -单例模式和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏 +* 按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器 + * 除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行 +* 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器 + * 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间 + * 独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束 +* 按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器 + * 压缩式垃圾回收器在回收完成后进行压缩整理,消除回收后的碎片,再分配对象空间使用指针碰撞 + * 非压缩式的垃圾回收器不进行这步操作,再分配对象空间使用空闲列表 +* 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器 +GC 性能指标: +- **吞吐量**:程序的运行时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间) +- 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例 +- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间 +- 收集频率:相对于应用程序的执行,收集操作发生的频率 +- 内存占用:Java 堆区所占的内存大小 +- 快速:一个对象从诞生到被回收所经历的时间 -**** +**垃圾收集器的组合关系**: +![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器关系图.png) +新生代收集器:Serial、ParNew、Paralle1 Scavenge; -##### 内部类 +老年代收集器:Serial old、Parallel old、CMS; -内部类持有外部类的情况,如果一个外部类的实例对象调用方法返回了一个内部类的实例对象,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象也不会被回收,造成内存泄漏 +整堆收集器:G1 +* 红色虚线在 JDK9 移除、绿色虚线在 JDK14 弃用该组合、青色虚线在 JDK14 删除 CMS 垃圾回收器 +查看默认的垃圾收回收器: -*** +* `-XX:+PrintcommandLineFlags`:查看命令行相关参数(包含使用的垃圾收集器) +* 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程 ID -##### 连接相关 -数据库连接、网络连接和 IO 连接等,当不再使用时,需要显式调用 close 方法来释放与连接,垃圾回收器才会回收对应的对象,否则将会造成大量的对象无法被回收,从而引起内存泄漏 +*** -**** +#### Serial +Serial:串行垃圾收集器,作用于新生代,是指使用单线程进行垃圾回收,采用**复制算法** +**STW(Stop-The-World)**:垃圾回收时,只有一个线程在工作,并且 java 应用中的所有线程都要暂停,等待垃圾回收的完成 -##### 不合理域 +**Serial old**:执行老年代垃圾回收的串行收集器,内存回收算法使用的是**标记-整理算法**,同样也采用了串行回收和 STW 机制 -变量不合理的作用域,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏;如果没有及时地把对象设置为 null,也有可能导致内存泄漏的发生 +- Serial old 是 Client 模式下默认的老年代的垃圾回收器 +- Serial old 在 Server 模式下主要有两个用途: + - 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用 + - 作为老年代 CMS 收集器的**后备垃圾回收方案**,在并发收集发生 Concurrent Mode Failure 时使用 -```java -public class UsingRandom { - private String msg; - public void receiveMsg(){ - msg = readFromNet();// 从网络中接受数据保存到 msg 中 - saveDB(msg); // 把 msg 保存到数据库中 - } -} -``` +开启参数:`-XX:+UseSerialGC` 等价于新生代用 Serial GC 且老年代用 Serial old GC -通过 readFromNet 方法把接收消息保存在 msg 中,然后调用 saveDB 方法把内容保存到数据库中,此时 msg 已经可以被回收,但是 msg 的生命周期与对象的生命周期相同,造成 msg 不能回收,产生内存泄漏 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-Serial收集器.png) -解决: +优点:简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,可以获得最高的单线程收集效率 -* msg 变量可以放在 receiveMsg 方法内部,当方法使用完,msg 的生命周期也就结束,就可以被回收了 -* 在使用完 msg 后,把 msg 设置为 null,这样垃圾回收器也会回收 msg 的内存空间。 +缺点:对于交互性较强的应用而言,这种垃圾收集器是不能够接受的,比如 JavaWeb 应用 @@ -11406,255 +11675,262 @@ public class UsingRandom { -##### 改变哈希 - -当一个对象被存储进 HashSet 集合中以后,就**不能修改这个对象中的那些参与计算哈希值的字段**,否则对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值不同,这种情况下使用该对象的当前引用作为的参数去 HashSet 集合中检索对象返回 false,导致无法从 HashSet 集合中单独删除当前对象,造成内存泄漏 +#### Parallel +Parallel Scavenge 收集器是应用于新生代的并行垃圾回收器,**采用复制算法**、并行回收和 Stop the World 机制 +Parallel Old 收集器:是一个应用于老年代的并行垃圾回收器,**采用标记-整理算法** -*** +对比其他回收器: +* 其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间 +* Parallel 目标是达到一个可控制的吞吐量,被称为**吞吐量优先**收集器 +* Parallel Scavenge 对比 ParNew 拥有**自适应调节策略**,可以通过一个开关参数打开 GC Ergonomics +应用场景: -##### 缓存泄露 +* 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验 +* 高吞吐量可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互 -内存泄漏的一个常见来源是缓存,一旦把对象引用放入到缓存中,就会很容易被遗忘 +停顿时间和吞吐量的关系:新生代空间变小 → 缩短停顿时间 → 垃圾回收变得频繁 → 导致吞吐量下降 -使用 WeakHashMap 代表缓存,当除了自身有对 key 的引用外没有其他引用,map 会自动丢弃此值 +在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge + Parallel Old 收集器,在 Server 模式下的内存回收性能很好,**Java8 默认是此垃圾收集器组合** +![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParallelScavenge收集器.png) +参数配置: -*** +* `-XX:+UseParallelGC`:手动指定年轻代使用Paralle并行收集器执行内存回收任务 +* `-XX:+UseParalleloldcc`:手动指定老年代使用并行回收收集器执行内存回收任务 + * 上面两个参数,默认开启一个,另一个也会被开启(互相激活),默认 jdk8 是开启的 +* `-XX:+UseAdaptivesizepplicy`:设置 Parallel scavenge 收集器具有**自适应调节策略**,在这种模式下,年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量 +* `-XX:ParallelGcrhreads`:设置年轻代并行收集器的线程数,一般最好与 CPU 数量相等,以避免过多的线程数影响垃圾收集性能 + * 在默认情况下,当 CPU 数量小于 8 个,ParallelGcThreads 的值等于 CPU 数量 + * 当 CPU 数量大于 8 个,ParallelGCThreads 的值等于 3+[5*CPU Count]/8] +* `-XX:MaxGCPauseMillis`:设置垃圾收集器最大停顿时间(即 STW 的时间),单位是毫秒 + * 对于用户来讲,停顿时间越短体验越好;在服务器端,注重高并发,整体的吞吐量 + * 为了把停顿时间控制在 MaxGCPauseMillis 以内,收集器在工作时会调整 Java 堆大小或其他一些参数 +* `-XX:GCTimeRatio`:垃圾收集时间占总时间的比例 =1/(N+1),用于衡量吞吐量的大小 + * 取值范围(0,100)。默认值 99,也就是垃圾回收时间不超过1 + * 与 `-xx:MaxGCPauseMillis` 参数有一定矛盾性,暂停时间越长,Radio 参数就容易超过设定的比例 -##### 监听器 +*** -监听器和其他回调情况,假如客户端在实现的 API 中注册回调,却没有显式的取消,那么就会一直积聚下去,所以确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,比如保存为 WeakHashMap 中的键 +#### ParNew -*** +Par 是 Parallel 并行的缩写,New 是只能处理的是新生代 +并行垃圾收集器在串行垃圾收集器的基础之上做了改进,**采用复制算法**,将单线程改为了多线程进行垃圾回收,可以缩短垃圾回收的时间 +对于其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同 Serial 收集器一样,应用在年轻代,除 Serial 外,只有**ParNew GC 能与 CMS 收集器配合工作** -#### 案例分析 +相关参数: -```java -public class Stack { - private Object[] elements; - private int size = 0; - private static final int DEFAULT_INITIAL_CAPACITY = 16; +* `-XX:+UseParNewGC`:表示年轻代使用并行收集器,不影响老年代 - public Stack() { - elements = new Object[DEFAULT_INITIAL_CAPACITY]; - } +* `-XX:ParallelGCThreads`:默认开启和 CPU 数量相同的线程数 - public void push(Object e) { //入栈 - ensureCapacity(); - elements[size++] = e; - } +![](https://gitee.com/seazean/images/raw/master/Java/JVM-ParNew收集器.png) - public Object pop() { //出栈 - if (size == 0) - throw new EmptyStackException(); - return elements[--size]; - } +ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器 - private void ensureCapacity() { - if (elements.length == size) - elements = Arrays.copyOf(elements, 2 * size + 1); - } -} -``` +- 对于新生代,回收次数频繁,使用并行方式高效 +- 对于老年代,回收次数少,使用串行方式节省资源(CPU 并行需要切换线程,串行可以省去切换线程的资源) -程序并没有明显错误,但 pop 函数存在内存泄漏问题,因为 pop 函数只是把栈顶索引下移一位,并没有把上一个出栈索引处的引用置空,导致栈数组一直强引用着已经出栈的对象 -解决方法: -```java -public Object pop() { - if (size == 0) - throw new EmptyStackException(); - Object result = elements[--size]; - elements[size] = null; - return result; -} -``` +**** +#### CMS +CMS 全称 Concurrent Mark Sweep,是一款**并发的、使用标记-清除**算法、针对老年代的垃圾回收器,其最大特点是**让垃圾收集线程与用户线程同时工作** -*** +CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(**低延迟**)越适合与用户交互的程序,良好的响应速度能提升用户体验 +分为以下四个流程: +- 初始标记:使用 STW 出现短暂停顿,仅标记一下 GC Roots 能直接关联到的对象,速度很快 +- 并发标记:进行 GC Roots 开始遍历整个对象图,在整个回收过程中耗时最长,不需要 STW,可以与用户线程并发运行 +- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要 STW(不停顿就会一直变化,采用写屏障 + 增量更新来避免漏标情况) +- 并发清除:清除标记为可以回收对象,不需要移动存活对象,所以这个阶段可以与用户线程同时并发的 +Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因: +* Mark Compact 算法会整理内存,导致用户线程使用的对象的地址改变,影响用户线程继续执行 -## 类加载 +* Mark Compact 更适合 Stop The World 场景 -### 对象访存 +在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿 -#### 存储结构 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-CMS收集器.png) -一个 Java 对象内存中存储为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding) +优点:并发收集、低延迟 -对象头: +缺点: -* 普通对象:分为两部分 +- 吞吐量降低:在并发阶段虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,CPU 利用率不够高 +- CMS 收集器**无法处理浮动垃圾**,可能出现 Concurrent Mode Failure 导致另一次 Full GC 的产生 + + 浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾(产生了新对象),这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,CMS 收集需要预留出一部分内存,不能等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS,导致很长的停顿时间 +- 标记 - 清除算法导致的空间碎片,往往出现老年代空间无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC;为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配 - * **Mark Word**:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等 +参数设置: - ```ruby - hash(25) + age(4) + lock(3) = 32bit #32位系统 - unused(25+1) + hash(31) + age(4) + lock(3) = 64bit #64位系统 - ``` +* `-XX:+UseConcMarkSweepGC`:手动指定使用 CMS 收集器执行内存回收任务 - * **Klass Word**:类型指针,**指向该对象的 Class 类对象的指针**,虚拟机通过这个指针来确定这个对象是哪个类的实例;在 64 位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 **Java 中的一个引用的大小**) + 开启该参数后会自动将 `-XX:+UseParNewGC` 打开,即:ParNew + CMS + Serial old的组合 - ```ruby - |-----------------------------------------------------| - | Object Header (64 bits) | - |---------------------------|-------------------------| - | Mark Word (32 bits) | Klass Word (32 bits) | - |---------------------------|-------------------------| - ``` +* `-XX:CMSInitiatingoccupanyFraction`:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收 -* 数组对象:如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度(12 字节) + * JDK5 及以前版本的默认值为 68,即当老年代的空间使用率达到 68% 时,会执行一次CMS回收 + * JDK6 及以上版本默认值为 92% - ```ruby - |-------------------------------------------------------------------------------| - | Object Header (96 bits) | - |-----------------------|-----------------------------|-------------------------| - | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | - |-----------------------|-----------------------------|-------------------------| - ``` +* `-XX:+UseCMSCompactAtFullCollection`:用于指定在执行完 Full GC 后对内存空间进行压缩整理,以此避免内存碎片的产生,由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长 -实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 +* `-XX:CMSFullGCsBeforecompaction`:**设置在执行多少次 Full GC 后对内存空间进行压缩整理** -对齐填充:Padding 起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求**对象起始地址必须是 8 字节的整数倍**,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 +* `-XX:ParallelCMSThreads`:**设置CMS的线程数量** -32 位系统: + * CMS 默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数 + * 收集线程占用的 CPU 资源多于25%,对用户程序影响可能较大;当 CPU 资源比较紧张时,受到 CMS 收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕 -* 一个 int 在 java 中占据 4byte,所以 Integer 的大小为: - ```java - private final int value; - ``` - ```ruby - # 需要补位4byte - 4(Mark Word) + 4(Klass Word) + 4(data) + 4(Padding) = 16byte - ``` +*** -* `int[] arr = new int[10]` - ```ruby - # 由于需要8位对齐,所以最终大小为`56byte`。 - 4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte - ``` +#### G1 +##### G1特点 -*** +G1(Garbage-First)是一款面向服务端应用的垃圾收集器,**应用于新生代和老年代**、采用标记-整理算法、软实时、低延迟、可设定目标(最大STW停顿时间)的垃圾回收器,用于代替 CMS,适用于较大的堆(>4 ~ 6G),在 JDK9 之后默认使用 G1 +G1 对比其他处理器的优点: +* 并发与并行: + * 并行性:G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力,此时用户线程 STW + * 并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此不会在整个回收阶段发生完全阻塞应用程序的情况 + * 其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作,而 G1 GC 可以采用应用线程承担后台运行的 GC 工作,JVM 的 GC 线程处理速度慢时,系统会**调用应用程序线程加速垃圾回收**过程 -#### 实际大小 +* **分区算法**: + * 从分代上看,G1 属于分代型垃圾回收器,区分年轻代和老年代,年轻代依然有 Eden 区和 Survivor 区。从堆结构上看,**新生代和老年代不再物理隔离**,不用担心每个代内存是否足够,这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC + * 将整个堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32 MB之间且为 2 的 N 次幂,所有 Region 大小相同,在 JVM 生命周期内不会被改变。G1 把堆划分成多个大小相等的独立区域,使得每个小空间可以单独进行垃圾回收 + * **新的区域 Humongous**:本身属于老年代区,当出现了一个巨型对象超出了分区容量的一半,该对象就会进入到该区域。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储,为了能找到连续的H区,有时候不得不启动 Full GC + * G1 不会对巨型对象进行拷贝,回收时被优先考虑,G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为 0 的巨型对象就可以在新生代垃圾回收时处理掉 + + * Region 结构图: + -浅堆(Shallow Heap):**对象本身占用的内存,不包括内部引用对象的大小**,32 位系统中一个对象引用占 4 个字节,每个对象头占用 8 个字节,根据堆快照格式不同,对象的大小会同 8 字节进行对齐 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1-Region区域.png) -JDK7 中的 String:2个 int 值共占 8 字节,value 对象引用占用 4 字节,对象头 8 字节,对齐后占 24 字节,为 String 对象的浅堆大小,与 value 实际取值无关,无论字符串长度如何,浅堆大小始终是 24 字节 +- 空间整合: -```java -private final char value[]; -private int hash; -private int hash32; -``` + - CMS:标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理 + - G1:整体来看是基于标记 - 整理算法实现的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片 -保留集(Retained Set):对象 A 的保留集指当对象 A 被垃圾回收后,可以被释放的所有的对象集合(包括 A 本身),所以对象 A 的保留集就是只能通过对象 A 被直接或间接访问到的所有对象的集合,就是仅被对象 A 所持有的对象的集合 +- **可预测的停顿时间模型(软实时 soft real-time)**:可以指定在 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒 -深堆(Retained Heap):指对象的保留集中所有的对象的浅堆大小之和,一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间 + - 由于分块的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,对于全局停顿情况也能得到较好的控制 + - G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个**优先列表**,每次根据允许的收集时间优先回收价值最大的 Region,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率 -对象的实际大小:一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小 + * 相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多 -下图显示了一个简单的对象引用关系图,对象 A 引用了 C 和 D,对象 B 引用了 C 和 E。那么对象 A 的浅堆大小只是 A 本身,**A 的实际大小为 A、C、D 三者之和**,A 的深堆大小为 A 与 D 之和,由于对象 C 还可以通过对象 B 访问到 C,因此 C 不在对象 A 的深堆范围内 +G1垃圾收集器的缺点: - +* 相较于 CMS,G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 要高 +* 从经验上来说,在小内存应用上 CMS 的表现大概率会优于 G1,而 G1 在大内存应用上则发挥其优势。平衡点在 6-8GB 之间 -内存分析工具 MAT 提供了一种叫支配树的对象图,体现了对象实例间的支配关系 +应用场景: -基本性质: +* 面向服务端应用,针对具有大内存、多处理器的机器 +* 需要低 GC 延迟,并具有大堆的应用程序提供解决方案 -- 对象 A 的子树(所有被对象 A 支配的对象集合)表示对象 A 的保留集(retained set),即深堆 -- 如果对象 A 支配对象 B,那么对象 A 的直接支配者也支配对象 B -- 支配树的边与对象引用图的边不直接对应 +*** -左图表示对象引用图,右图表示左图所对应的支配树: -![](https://gitee.com/seazean/images/raw/master/Java/JVM-支配树.png) -比如:对象 F 与对象 D 相互引用,因为到对象 F 的所有路径必然经过对象 D,因此对象 D 是对象 F 的直接支配者 +##### 记忆集 +记忆集 Remembered Set 在新生代中,每个 Region 都有一个 Remembered Set,用来被哪些其他 Region 里的对象引用(谁引用了我就记录谁) + -参考文章:https://www.yuque.com/u21195183/jvm/nkq31c +* 程序对 Reference 类型数据写操作时,产生一个 Write Barrier 暂时中断操作,检查该对象和 Reference 类型数据是否在不同的 Region(跨代引用),不同就将相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中 +* 进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏 +垃圾收集器在新生代中建立了记忆集这样的数据结构,可以理解为它是一个抽象类,具体实现记忆集的三种方式: +* 字长精度 +* 对象精度 +* 卡精度(卡表) -*** +卡表(Card Table)在老年代中,是一种对记忆集的具体实现,主要定义了记忆集的记录精度、与堆内存的映射关系等,卡表中的每一个元素都对应着一块特定大小的内存块,这个内存块称之为卡页(card page),当存在跨代引用时,会将卡页标记为 dirty,JVM 对于卡页的维护也是通过写屏障的方式 +收集集合 CSet 代表每次 GC 暂停时回收的一系列目标分区,在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。年轻代收集 CSet 只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中 +* CSet of Young Collection +* CSet of Mix Collection -#### 节约内存 -* 尽量使用基本数据类型 -* 满足容量前提下,尽量用小字段 +*** -* 尽量用数组,少用集合,数组中是可以使用基本类型的,但是集合中只能放包装类型,如果需要使用集合,推荐比较节约内存的集合工具:fastutil - 一个 ArrayList 集合,如果里面放了 10 个数字,占用多少内存: - ```java - private transient Object[] elementData; - private int size; - ``` +##### 工作原理 - Mark Word 占 4byte,Klass Word 占 4byte,一个 int 字段占 4byte,elementData 数组占 12byte,数组中 10 个 Integer 对象占 10×16,所以整个集合空间大小为 184byte(深堆) +G1 中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不同的条件下被触发 -* 时间用 long/int 表示,不用 Date 或者 String +* 当堆内存使用达到一定值(默认 45%)时,开始老年代并发标记过程 +* 标记完成马上开始混合回收过程 + +顺时针:Young GC → Young GC + Concurrent Mark → Mixed GC 顺序,进行垃圾回收 -*** +* **Young GC**:发生在年轻代的 GC 算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 young gc,G1 停止应用程序的执行 STW,把活跃对象放入老年代,垃圾对象回收 + **回收过程**: + 1. 扫描根:根引用连同 RSet 记录的外部引用作为扫描存活对象的入口 + 2. 更新 RSet:处理 dirty card queue 更新 RS,此后 RSet 准确的反映对象的引用关系 + * dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到 RSet + * 作用:产生引用直接更新 RSet 需要线程同步开销很大,使用队列性能好 + 3. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象,把需要回收的分区放入 Young CSet 中进行回收 + 4. 复制对象:Eden 区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 + 5. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作 -#### 对象访问 +* **Concurrent Mark **: -JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: + * 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC + * 根区域扫描 (Root Region Scanning):扫描 Survivor 区中指向老年代的,被初始标记标记了的引用及引用的对象,这一个过程是并发进行的,但是必须在 Young GC 之前完成 + * 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。会计算每个区域的对象活性,即区域中存活对象的比例,若区域中的所有对象都是垃圾,则这个区域会被立即回收(实时回收),给浮动垃圾准备出更多的空间,把需要收集的 Region 放入 CSet 当中 + * 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行(**防止漏标**) + * 筛选回收:并发清理阶段,首先对 CSet 中各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率 -* 句柄访问:Java 堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 - - 优点:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-G1收集器.jpg) - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-句柄访问.png) +* **Mixed GC**:当很多对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,除了回收整个 young region,还会**回收一部分**的 old region,过程同 YGC -* 直接指针(HotSpot 采用):Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的对象地址 - - 优点:速度更快,**节省了一次指针定位的时间开销** + 注意:**是一部分老年代,而不是全部老年代**,可以选择哪些老年代 region 收集,对垃圾回收的时间进行控制 - 缺点:对象被移动时(如进行GC后的内存重新排列),对象的 reference 也需要同步更新 - - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-直接指针.png) + 在 G1 中,Mixed GC 可以通过 `-XX:InitiatingHeapOccupancyPercent` 设置阈值 +* **Full GC**:对象内存分配速度过快,Mixed GC 来不及回收,导致老年代被填满,就会触发一次 Full GC,G1 的 Full GC 算法就是单线程执行的垃圾回收,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免 Full GC + 产生 Full GC 的原因: -参考文章:https://www.cnblogs.com/afraidToForget/p/12584866.html + * 晋升时没有足够的空间存放晋升的对象 + * 并发处理过程完成之前空间耗尽,浮动垃圾 @@ -11662,178 +11938,123 @@ JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: -### 对象创建 - -#### 生命周期 +##### 相关参数 -在 Java 中,对象的生命周期包括以下几个阶段: +- `-XX:+UseG1GC`:手动指定使用 G1 垃圾收集器执行内存回收任务 +- `-XX:G1HeapRegionSize`:设置每个 Region 的大小。值是 2 的幂,范围是 1MB 到 32MB 之间,目标是根据最小的 Java 堆大小划分出约 2048 个区域,默认是堆内存的 1/2000 +- `-XX:MaxGCPauseMillis`:设置期望达到的最大 GC 停顿时间指标,JVM会尽力实现,但不保证达到,默认值是 200ms +- `-XX:+ParallelGcThread`:设置 STW 时 GC 线程数的值,最多设置为 8 +- `-XX:ConcGCThreads`:设置并发标记线程数,设置为并行垃圾回收线程数 ParallelGcThreads 的1/4左右 +- `-XX:InitiatingHeapoccupancyPercent`:设置触发并发 Mixed GC 周期的 Java 堆占用率阈值,超过此值,就触发 GC,默认值是 45 +- `-XX:+ClassUnloadingWithConcurrentMark`:并发标记类卸载,默认启用,所有对象都经过并发标记后,就可以知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 +- `-XX:G1NewSizePercent`:新生代占用整个堆内存的最小百分比(默认5%) +- `-XX:G1MaxNewSizePercent`:新生代占用整个堆内存的最大百分比(默认60%) +- `-XX:G1ReservePercent=10`:保留内存区域,防止 to space(Survivor中的 to 区)溢出 -1. 创建阶段 (Created): -2. 应用阶段 (In Use):对象至少被一个强引用持有着 -3. 不可见阶段 (Invisible):程序的执行已经超出了该对象的作用域,不再持有该对象的任何强引用 -4. 不可达阶段 (Unreachable):该对象不再被任何强引用所持有,包括 GC Root 的强引用 -5. 收集阶段 (Collected):垃圾回收器对该对象的内存空间重新分配做好准备,该对象如果重写了 finalize() 方法,则会去执行该方法 -6. 终结阶段 (Finalized):等待垃圾回收器对该对象空间进行回收,当对象执行完 finalize()方 法后仍然处于不可达状态时进入该阶段 -7. 对象空间重分配阶段 (De-allocated):垃圾回收器对该对象的所占用的内存空间进行回收或者再分配 +*** -参考文章:https://blog.csdn.net/sodino/article/details/38387049 +##### 调优 -*** +G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可完成调优: +1. 开启 G1 垃圾收集器 +2. 设置堆的最大内存 +3. 设置最大的停顿时间(STW) +不断调优暂停时间指标: -#### 创建时机 +* `XX:MaxGCPauseMillis=x` 可以设置启动应用程序暂停的时间,G1会根据这个参数选择 CSet 来满足响应时间的设置 +* 设置到 100ms 或者 200ms 都可以(不同情况下会不一样),但设置成50ms就不太合理 +* 暂停时间设置的太短,就会导致出现 G1 跟不上垃圾产生的速度,最终退化成 Full GC +* 对这个参数的调优是一个持续的过程,逐步调整到最佳状态 -类在第一次实例化加载一次,后续实例化不再加载,引用第一次加载的类 +不要设置新生代和老年代的大小: -Java 对象创建时机: +- 避免使用 -Xmn 或 -XX:NewRatio 等相关选项显式设置年轻代大小,G1 收集器在运行的时候会调整新生代和老年代的大小,从而达到我们为收集器设置的暂停时间目标 +- 设置了新生代大小相当于放弃了 G1 为我们做的自动调优,我们需要做的只是设置整个堆内存的大小,剩下的交给 G1 自己去分配各个代的大小 -1. 使用 new 关键字创建对象:由执行类实例创建表达式而引起的对象创建 -2. 使用 Class 类的 newInstance 方法(反射机制) -3. 使用 Constructor 类的 newInstance 方法(反射机制) +*** - ```java - public class Student { - private int id; - public Student(Integer id) { - this.id = id; - } - public static void main(String[] args) throws Exception { - Constructor c = Student.class.getConstructor(Integer.class); - Student stu = c.newInstance(123); - } - } - ``` - 使用 newInstance 方法的这两种方式创建对象使用的就是 Java 的反射机制,事实上 Class 的 newInstance 方法内部调用的也是 Constructor 的 newInstance 方法 -4. 使用 Clone 方法创建对象:用 clone 方法创建对象的过程中并不会调用任何构造函数,要想使用 clone 方法,我们就必须先实现 Cloneable 接口并实现其定义的 clone 方法 +#### ZGC -5. 使用(反)序列化机制创建对象:当反序列化一个对象时,JVM 会创建一个**单独的对象**,在此过程中,JVM 并不会调用任何构造函数,为了反序列化一个对象,需要让类实现 Serializable 接口 +ZGC 收集器是一个可伸缩的、低延迟的垃圾收集器,基于 Region 内存布局的,不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现**可并发的标记压缩算法** -从 Java 虚拟机层面看,除了使用 new 关键字创建对象的方式外,其他方式全部都是通过转变为 invokevirtual 指令直接创建对象的 - - - -*** +* 在 CMS 和 G1 中都用到了写屏障,而 ZGC 用到了读屏障 +* 染色指针:直接**将少量额外的信息存储在指针上的技术**,从 64 位的指针中拿高 4 位来标识对象此时的状态 + * 染色指针可以使某个 Region 的存活对象被移走之后,这个 Region 立即就能够被释放和重用 + * 可以直接从指针中看到引用对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集、是否被移动过(Remapped)、是否只能通过 finalize() 方法才能被访问到(Finalizable) + * 可以大幅减少在垃圾收集过程中内存屏障的使用数量,写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作 + * 可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据 +* 内存多重映射:多个虚拟地址指向同一个物理地址 +可并发的标记压缩算法:染色指针标识对象是否被标记或移动,读屏障保证在每次应用程序或 GC 程序访问对象时先根据染色指针的标识判断是否被移动,如果被移动就根据转发表访问新的移动对象,并更新引用,不会像 G1 一样必须等待垃圾回收完成才能访问 +ZGC 目标: -#### 创建过程 +- 停顿时间不会超过 10ms +- 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在 10ms 以下) +- 可支持几百 M,甚至几 T 的堆大小(最大支持4T) -创建对象的过程: +ZGC 的工作过程可以分为 4 个阶段: -1. 判断对象对应的类是否加载、链接、初始化 +* 并发标记(Concurrent Mark): 遍历对象图做可达性分析的阶段,也要经过初始标记和最终标记,需要短暂停顿 +* 并发预备重分配( Concurrent Prepare for Relocate):根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set) +* 并发重分配(Concurrent Relocate): 重分配是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的**每个 Region 维护一个转发表**(Forward Table),记录从旧地址到新地址的转向关系 +* 并发重映射(Concurrent Remap):修正整个堆中指向重分配集中旧对象的所有引用,ZGC 的并发映射并不是一个必须要立即完成的任务,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,因为都是要遍历所有对象,这样合并节省了一次遍历的开销 -2. 为对象分配内存:指针碰撞、空闲链表。当一个对象被创建时,虚拟机就会为其分配内存来存放对象的实例变量及其从父类继承过来的实例变量,即使从**隐藏变量**也会被分配空间(继承部分解释了为什么会隐藏) +ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的,但这部分的实际时间是非常少的,所以响应速度快,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟 -3. 处理并发安全问题: +优点:高吞吐量、低延迟 - * 采用 CAS 配上自旋保证更新的原子性 - * 每个线程预先分配一块 TLAB +缺点:浮动垃圾,当 ZGC 准备要对一个很大的堆做一次完整的并发收集,其全过程要持续十分钟以上,由于应用的对象分配速率很高,将创造大量的新对象产生浮动垃圾 -4. 初始化分配的空间:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),保证对象实例字段在不赋值时可以直接使用,程序能访问到这些字段的数据类型所对应的零值 -5. 设置对象的对象头:将对象的所属类(类的元数据信息)、对象的 HashCode、对象的 GC 信息、锁信息等数据存储在对象头中 -6. 执行 init 方法进行实例化:实例变量初始化、实例代码块初始化 、构造函数初始化 +参考文章:https://www.cnblogs.com/jimoer/p/13170249.html - * 实例变量初始化与实例代码块初始化: - 对实例变量直接赋值或者使用实例代码块赋值,**编译器会将其中的代码放到类的构造函数中去**,并且这些代码会被放在对超类构造函数的调用语句之后 (Java 要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前 - * 构造函数初始化: +*** - **Java 要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性**,在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到 Object 类。然后从 Object 类依次对以下各类进行实例化,初始化父类中的变量和执行构造函数 +#### 总结 -*** +Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同: +- 最小化地使用内存和并行开销,选 Serial GC +- 最大化应用程序的吞吐量,选 Parallel GC +- 最小化 GC 的中断或停顿时间,选 CMS GC +![](https://gitee.com/seazean/images/raw/master/Java/JVM-垃圾回收器总结.png) -#### 承上启下 -1. 一个实例变量在对象初始化的过程中会被赋值几次? - JVM 在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个实例变量被第一次赋值 - 在声明实例变量的同时对其进行了赋值操作,那么这个实例变量就被第二次赋值 - 在实例代码块中又对变量做了初始化操作,那么这个实例变量就被第三次赋值 - 在构造函数中也对变量做了初始化操作,那么这个实例变量就被第四次赋值 - 在 Java 的对象初始化过程中,一个实例变量最多可以被初始化 4 次 -2. 类的初始化过程与类的实例化过程的异同? - 类的初始化是指类加载过程中的初始化阶段对类变量按照代码进行赋值的过程 - 类的实例化是指在类完全加载到内存中后创建对象的过程(类的实例化触发了类的初始化,先初始化才能实例化) +*** -3. 假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?(**经典案例**) - ```java - public class StaticTest { - public static void main(String[] args) { - staticFunction();//调用静态方法,触发初始化 - } - - static StaticTest st = new StaticTest(); - - static { //静态代码块 - System.out.println("1"); - } - - { // 实例代码块 - System.out.println("2"); - } - - StaticTest() { // 实例构造器 - System.out.println("3"); - System.out.println("a=" + a + ",b=" + b); - } - - public static void staticFunction() { // 静态方法 - System.out.println("4"); - } - - int a = 110; // 实例变量 - static int b = 112; // 静态变量 - }/* Output: - 2 - 3 - a=110,b=0 - 1 - 4 - *///:~ - ``` - `static StaticTest st = new StaticTest();`: +### 内存泄漏 - * 实例实例化不一定要在类初始化结束之后才开始 +#### 泄露溢出 - * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形)。因此在实例化上述程序中的 st 变量时,**实际上是把实例化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致 a 为 110,b 为 0 的原因 +内存泄漏(Memory Leak):是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果 - 代码等价于: +可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。由于代码的实现不同就会出现很多种内存泄漏问题,让 JVM 误以为此对象还在引用中,无法回收,造成内存泄漏 - ```java - public class StaticTest { - (){ - a = 110; // 实例变量 - System.out.println("2"); // 实例代码块 - System.out.println("3"); // 实例构造器中代码的执行 - System.out.println("a=" + a + ",b=" + b); // 实例构造器中代码的执行 - 类变量st被初始化 - System.out.println("1"); //静态代码块 - 类变量b被初始化为112 - } - } - ``` +内存溢出(out of memory)指的是申请内存时,没有足够的内存可以使用 - +内存泄漏和内存溢出的关系:内存泄漏的越来越多,最终会导致内存溢出 @@ -11841,21 +12062,21 @@ Java 对象创建时机: -### 加载过程 - -#### 生命周期 - -类是在运行期间**第一次使用时动态加载**的(不使用不加载),而不是一次性加载所有类,因为一次性加载会占用很多的内存,加载的类信息存放于一块成为方法区的内存空间 +#### 几种情况 -![](https://gitee.com/seazean/images/raw/master/Java/JVM-类的生命周期.png) +##### 静态集合 -包括 7 个阶段: +静态集合类的生命周期与 JVM 程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。原因是**长生命周期的对象持有短生命周期对象的引用**,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收 -* 加载(Loading) -* 链接:验证(Verification)、准备(Preparation)、解析(Resolution) -* 初始化(Initialization) -* 使用(Using) -* 卸载(Unloading) +```java +public class MemoryLeak { + static List list = new ArrayList(); + public void oomTest(){ + Object obj = new Object();//局部变量 + list.add(obj); + } +} +``` @@ -11863,115 +12084,88 @@ Java 对象创建时机: -#### 加载阶段 +##### 单例模式 -加载是类加载的其中一个阶段,注意不要混淆 +单例模式和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏 -加载过程完成以下三件事: -- 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码) -- 将该字节流表示的**静态存储结构转换为方法区的运行时存储结构**(Java 类模型) -- 在内存中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口 -其中二进制字节流可以从以下方式中获取: +**** -- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础 -- 从网络中获取,最典型的应用是 Applet -- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类 -- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 生成字节码 -将字节码文件加载至方法区后,会**在堆中**创建一个 java.lang.Class 对象,用来引用位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象 -方法区内部采用 C++ 的 instanceKlass 描述 Java 类的数据结构: +##### 内部类 -* `_java_mirror` 即 java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 java 使用 -* `_super` 即父类、`_fields` 即成员变量、`_methods` 即方法、`_constants` 即常量池、`_class_loader` 即类加载器、`_vtable` **虚方法表**、`_itable` 接口方法表 +内部类持有外部类的情况,如果一个外部类的实例对象调用方法返回了一个内部类的实例对象,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象也不会被回收,造成内存泄漏 -加载过程: -* 如果这个类还有父类没有加载,先加载父类 -* 加载和链接可能是交替运行的 -* Class 对象和 _java_mirror 相互持有对方的地址,堆中对象通过 instanceKlass 和元空间进行交互 - +*** -创建数组类有些特殊,因为数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建,创建数组类的过程: -- 如果数组的元素类型是引用类型,那么遵循定义的加载过程递归加载和创建数组的元素类型 -- JVM 使用指定的元素类型和数组维度来创建新的数组类 -- 基本数据类型由启动类加载器加载 +##### 连接相关 +数据库连接、网络连接和 IO 连接等,当不再使用时,需要显式调用 close 方法来释放与连接,垃圾回收器才会回收对应的对象,否则将会造成大量的对象无法被回收,从而引起内存泄漏 -*** +**** -#### 链接阶段 -##### 验证 -确保 Class 文件的字节流中包含的信息是否符合 JVM 规范,保证被加载类的正确性,不会危害虚拟机自身的安全 +##### 不合理域 -主要包括**四种验证**: +变量不合理的作用域,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏;如果没有及时地把对象设置为 null,也有可能导致内存泄漏的发生 -* 文件格式验证 +```java +public class UsingRandom { + private String msg; + public void receiveMsg(){ + msg = readFromNet();// 从网络中接受数据保存到 msg 中 + saveDB(msg); // 把 msg 保存到数据库中 + } +} +``` -* 语义检查,但凡在语义上不符合规范的,虚拟机不会给予验证通过 +通过 readFromNet 方法把接收消息保存在 msg 中,然后调用 saveDB 方法把内容保存到数据库中,此时 msg 已经可以被回收,但是 msg 的生命周期与对象的生命周期相同,造成 msg 不能回收,产生内存泄漏 - * 是否所有的类都有父类的存在(除了 Object 外,其他类都应该有父类) +解决: - * 是否一些被定义为 final 的方法或者类被重写或继承了 +* msg 变量可以放在 receiveMsg 方法内部,当方法使用完,msg 的生命周期也就结束,就可以被回收了 +* 在使用完 msg 后,把 msg 设置为 null,这样垃圾回收器也会回收 msg 的内存空间。 - * 非抽象类是否实现了所有抽象方法或者接口方法 - * 是否存在不兼容的方法 -* 字节码验证,试图通过对字节码流的分析,判断字节码是否可以被正确地执行 +**** - * 在字节码的执行过程中,是否会跳转到一条不存在的指令 - * 函数的调用是否传递了正确类型的参数 - * 变量的赋值是不是给了正确的数据类型 - * 栈映射帧(StackMapTable)在这个阶段用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型 -* 符号引用验证,Class 文件在其常量池会通过字符串记录将要使用的其他类或者方法 +##### 改变哈希 +当一个对象被存储进 HashSet 集合中以后,就**不能修改这个对象中的那些参与计算哈希值的字段**,否则对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值不同,这种情况下使用该对象的当前引用作为的参数去 HashSet 集合中检索对象返回 false,导致无法从 HashSet 集合中单独删除当前对象,造成内存泄漏 -*** +*** -##### 准备 -准备阶段为**静态变量分配内存并设置初始值**,使用的是方法区的内存: -* 类变量也叫静态变量,就是是被 static 修饰的变量 -* 实例变量也叫对象变量,即没加 static 的变量 +##### 缓存泄露 -说明:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 +内存泄漏的一个常见来源是缓存,一旦把对象引用放入到缓存中,就会很容易被遗忘 -类变量初始化: +使用 WeakHashMap 代表缓存,当除了自身有对 key 的引用外没有其他引用,map 会自动丢弃此值 -* static 变量分配空间和赋值是两个步骤:**分配空间在准备阶段完成,赋值在初始化阶段完成** -* 如果 static 变量是 final 的基本类型以及字符串常量,那么编译阶段值(方法区)就确定了,准备阶段会显式初始化 -* 如果 static 变量是 final 的,但属于引用类型或者构造器方法的字符串,赋值在初始化阶段完成 -实例: -* 初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123: +*** - ```java - public static int value = 123; - ``` -* 常量 value 被初始化为 123 而不是 0: - ```java - public static final int value = 123; - ``` +##### 监听器 -* Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是0,故 boolean 的默认值就是 false +监听器和其他回调情况,假如客户端在实现的 API 中注册回调,却没有显式的取消,那么就会一直积聚下去,所以确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,比如保存为 WeakHashMap 中的键 @@ -11979,194 +12173,195 @@ Java 对象创建时机: -##### 解析 +#### 案例分析 -将常量池中类、接口、字段、方法的**符号引用替换为直接引用**(内存地址)的过程: +```java +public class Stack { + private Object[] elements; + private int size = 0; + private static final int DEFAULT_INITIAL_CAPACITY = 16; -* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念,如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符** -* 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,如果有了直接引用,那说明引用的目标必定已经存在于内存之中 + public Stack() { + elements = new Object[DEFAULT_INITIAL_CAPACITY]; + } -例如:在 `com.demo.Solution` 类中引用了 `com.test.Quest`,把 `com.test.Quest` 作为符号引用存进类常量池,在类加载完后,**用这个符号引用去方法区找这个类的内存地址** + public void push(Object e) { //入栈 + ensureCapacity(); + elements[size++] = e; + } -解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等 - -* 在类加载阶段解析的是非虚方法,静态绑定 -* 也可以在初始化阶段之后再开始解析,这是为了支持 Java 的**动态绑定** -* 通过解析操作,符号引用就可以转变为目标方法在类的虚方法表中的位置,从而使得方法被成功调用 + public Object pop() { //出栈 + if (size == 0) + throw new EmptyStackException(); + return elements[--size]; + } -```java -public class Load2 { - public static void main(String[] args) throws Exception{ - ClassLoader classloader = Load2.class.getClassLoader(); - // cloadClass 加载类方法不会导致类的解析和初始化,也不会加载D - Class c = classloader.loadClass("cn.jvm.t3.load.C"); - - // new C();会导致类的解析和初始化,从而解析初始化D - System.in.read(); + private void ensureCapacity() { + if (elements.length == size) + elements = Arrays.copyOf(elements, 2 * size + 1); } } -class C { - D d = new D(); -} -class D { -} ``` +程序并没有明显错误,但 pop 函数存在内存泄漏问题,因为 pop 函数只是把栈顶索引下移一位,并没有把上一个出栈索引处的引用置空,导致栈数组一直强引用着已经出栈的对象 +解决方法: -**** - +```java +public Object pop() { + if (size == 0) + throw new EmptyStackException(); + Object result = elements[--size]; + elements[size] = null; + return result; +} +``` -#### 初始化 -##### 介绍 -初始化阶段才真正开始执行类中定义的 Java 程序代码,在准备阶段,类变量已经赋过一次系统要求的初始值;在初始化阶段,通过程序制定的计划去初始化类变量和其它资源,执行 -在编译生成 class 文件时,编译器会产生两个方法加于 class 文件中,一个是类的初始化方法 clinit,另一个是实例的初始化方法 init +*** -类构造器 () 与实例构造器 () 不同,它不需要程序员进行显式调用,在一个类的生命周期中,类构造器最多被虚拟机**调用一次**,而实例构造器则会被虚拟机调用多次,只要程序员创建对象 -类在第一次实例化加载一次,把 class 读入内存,后续实例化不再加载,引用第一次加载的类 -*** +## 类加载 +### 对象访存 +#### 存储结构 -##### clinit +一个 Java 对象内存中存储为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding) -():类构造器,由编译器自动收集类中**所有类变量的赋值动作和静态语句块**中的语句合并产生的 +对象头: -作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块 +* 普通对象:分为两部分 -* 如果类中没有静态变量或静态代码块,那么 clinit 方法将不会被生成 -* clinit 方法只执行一次,在执行 clinit 方法时,必须先执行父类的clinit方法 -* static 变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定 -* static 不加 final 的变量都在初始化环节赋值 + * **Mark Word**:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等 -**线程安全**问题: + ```ruby + hash(25) + age(4) + lock(3) = 32bit #32位系统 + unused(25+1) + hash(31) + age(4) + lock(3) = 64bit #64位系统 + ``` -* 虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都阻塞等待,直到活动线程执行 () 方法完毕 -* 如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽 + * **Klass Word**:类型指针,**指向该对象的 Class 类对象的指针**,虚拟机通过这个指针来确定这个对象是哪个类的实例;在 64 位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 **Java 中的一个引用的大小**) -特别注意:静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问 + ```ruby + |-----------------------------------------------------| + | Object Header (64 bits) | + |---------------------------|-------------------------| + | Mark Word (32 bits) | Klass Word (32 bits) | + |---------------------------|-------------------------| + ``` -```java -public class Test { - static { - //i = 0; // 给变量赋值可以正常编译通过 - System.out.print(i); // 这句编译器会提示“非法向前引用” - } - static int i = 1; -} -``` +* 数组对象:如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度(12 字节) -接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法,两者不同的是: + ```ruby + |-------------------------------------------------------------------------------| + | Object Header (96 bits) | + |-----------------------|-----------------------------|-------------------------| + | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | + |-----------------------|-----------------------------|-------------------------| + ``` -* 在初始化一个接口时,并不会先初始化它的父接口,所以执行接口的 () 方法不需要先执行父接口的 () 方法 -* 在初始化一个类时,不会先初始化所实现的接口,所以接口的实现类在初始化时不会执行接口的 () 方法 -* 只有当父接口中定义的变量使用时,父接口才会初始化 +实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来 +对齐填充:Padding 起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求**对象起始地址必须是 8 字节的整数倍**,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全 +32 位系统: -**** +* 一个 int 在 java 中占据 4byte,所以 Integer 的大小为: + ```java + private final int value; + ``` + ```ruby + # 需要补位4byte + 4(Mark Word) + 4(Klass Word) + 4(data) + 4(Padding) = 16byte + ``` -##### 时机 +* `int[] arr = new int[10]` -类的初始化是懒惰的,只有在首次使用时才会被装载,JVM 不会无条件地装载 Class 类型,Java 虚拟机规定,一个类或接口在初次使用前,必须要进行初始化 + ```ruby + # 由于需要8位对齐,所以最终大小为`56byte`。 + 4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte + ``` -**主动引用**:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列情况必须对类进行初始化(加载、验证、准备都会发生): -* 当创建一个类的实例时,使用 new 关键字,或者通过反射、克隆、反序列化(前文讲述的对象的创建时机) -* 当调用类的静态方法或访问静态字段时,遇到 getstatic、putstatic、invokestatic 这三条字节码指令,如果类没有进行过初始化,则必须先触发其初始化 - * getstatic:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池) - * putstatic:程序给类的静态变量赋值 - * invokestatic :调用一个类的静态方法 -* 使用 java.lang.reflect 包的方法对类进行反射调用时,如果类没有进行初始化,则需要先触发其初始化 -* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化,但这条规则并**不适用于接口** -* 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类 -* MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这两个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类 -* 补充:当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 -**被动引用**:所有引用类的方式都不会触发初始化,称为被动引用 +*** -* 通过子类引用父类的静态字段,不会导致子类初始化,只会触发父类的初始化 -* 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法 -* 常量(final 修饰)在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化 -* 调用 ClassLoader 类的 loadClass() 方法加载一个类,并不是对类的主动使用,不会导致类的初始化 +#### 实际大小 -*** +浅堆(Shallow Heap):**对象本身占用的内存,不包括内部引用对象的大小**,32 位系统中一个对象引用占 4 个字节,每个对象头占用 8 个字节,根据堆快照格式不同,对象的大小会同 8 字节进行对齐 +JDK7 中的 String:2个 int 值共占 8 字节,value 对象引用占用 4 字节,对象头 8 字节,对齐后占 24 字节,为 String 对象的浅堆大小,与 value 实际取值无关,无论字符串长度如何,浅堆大小始终是 24 字节 +```java +private final char value[]; +private int hash; +private int hash32; +``` -##### init +保留集(Retained Set):对象 A 的保留集指当对象 A 被垃圾回收后,可以被释放的所有的对象集合(包括 A 本身),所以对象 A 的保留集就是只能通过对象 A 被直接或间接访问到的所有对象的集合,就是仅被对象 A 所持有的对象的集合 -init 指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行 +深堆(Retained Heap):指对象的保留集中所有的对象的浅堆大小之和,一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间 -实例化即调用 ()V ,虚拟机会保证这个类的构造方法的线程安全,先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块,没有成员变量初始化和代码块则不会执行 +对象的实际大小:一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小 -类实例化过程:**父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数** +下图显示了一个简单的对象引用关系图,对象 A 引用了 C 和 D,对象 B 引用了 C 和 E。那么对象 A 的浅堆大小只是 A 本身,**A 的实际大小为 A、C、D 三者之和**,A 的深堆大小为 A 与 D 之和,由于对象 C 还可以通过对象 B 访问到 C,因此 C 不在对象 A 的深堆范围内 + +内存分析工具 MAT 提供了一种叫支配树的对象图,体现了对象实例间的支配关系 -*** +基本性质: +- 对象 A 的子树(所有被对象 A 支配的对象集合)表示对象 A 的保留集(retained set),即深堆 +- 如果对象 A 支配对象 B,那么对象 A 的直接支配者也支配对象 B -#### 卸载阶段 +- 支配树的边与对象引用图的边不直接对应 -时机:执行了 System.exit() 方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java虚拟机进程终止 +左图表示对象引用图,右图表示左图所对应的支配树: -卸载类即该类的 **Class 对象被 GC**,卸载类需要满足3个要求: +![](https://gitee.com/seazean/images/raw/master/Java/JVM-支配树.png) -1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象 -2. 该类没有在其他任何地方被引用 -3. 该类的类加载器的实例已被 GC,一般是可替换类加载器的场景,如 OSGi、JSP 的重加载等,很难达成 +比如:对象 F 与对象 D 相互引用,因为到对象 F 的所有路径必然经过对象 D,因此对象 D 是对象 F 的直接支配者 -在 JVM 生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,这些类始终是可及的 +参考文章:https://www.yuque.com/u21195183/jvm/nkq31c -**** +*** -### 类加载器 -#### 类加载 -类加载方式: +#### 节约内存 -* 隐式加载:不直接在代码中调用 ClassLoader 的方法加载类对象 - * 创建类对象、使用类的静态域、创建子类对象、使用子类的静态域 - * 在 JVM 启动时,通过三大类加载器加载 class -* 显式加载: - * ClassLoader.loadClass(className):只加载和连接,**不会进行初始化** - * Class.forName(String name, boolean initialize, ClassLoader loader):使用 loader 进行加载和连接,根据参数 initialize 决定是否初始化 +* 尽量使用基本数据类型 -类的唯一性: +* 满足容量前提下,尽量用小字段 -* 在 JVM 中表示两个 class 对象判断为同一个类存在的两个必要条件: - - 类的完整类名必须一致,包括包名 - - 加载这个类的 ClassLoader(指 ClassLoader 实例对象)必须相同 -* 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true +* 尽量用数组,少用集合,数组中是可以使用基本类型的,但是集合中只能放包装类型,如果需要使用集合,推荐比较节约内存的集合工具:fastutil -命名空间: + 一个 ArrayList 集合,如果里面放了 10 个数字,占用多少内存: -- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成 -- 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类 + ```java + private transient Object[] elementData; + private int size; + ``` -基本特征: + Mark Word 占 4byte,Klass Word 占 4byte,一个 int 字段占 4byte,elementData 数组占 12byte,数组中 10 个 Integer 对象占 10×16,所以整个集合空间大小为 184byte(深堆) -* **可见性**,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的 -* **单一性**,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,不会在子加载器中重复加载 +* 时间用 long/int 表示,不用 Date 或者 String @@ -12174,110 +12369,90 @@ init 指的是实例构造器,主要作用是在类实例化过程中执行, -#### 加载器 +#### 对象访问 -类加载器是 Java 的核心组件,用于加载字节码到 JVM 内存,得到 Class 类的对象 +JVM 是通过**栈帧中的对象引用**访问到其内部的对象实例: -从 Java 虚拟机规范来讲,只存在以下两种不同的类加载器: +* 句柄访问:Java 堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 + + 优点:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改 -- 启动类加载器(Bootstrap ClassLoader):使用 C++ 实现,是虚拟机自身的一部分 -- 自定义类加载器(User-Defined ClassLoader):Java 虚拟机规范**将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器**,使用 Java 语言实现,独立于虚拟机 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-句柄访问.png) -从 Java 开发人员的角度看: +* 直接指针(HotSpot 采用):Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的对象地址 + + 优点:速度更快,**节省了一次指针定位的时间开销** -* 启动类加载器(Bootstrap ClassLoader): - * 处于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类 - * 类加载器负责加载在 `JAVA_HOME/jre/lib `或 `sun.boot.class.path` 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的类,并且是虚拟机识别的类库加载到虚拟机内存中 - * 仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在 lib 目录中也不会被加载 - * 启动类加载器无法被 Java 程序直接引用,编写自定义类加载器时,如果要把加载请求委派给启动类加载器,直接使用 null 代替 -* 扩展类加载器(Extension ClassLoader): - * 由 ExtClassLoader (sun.misc.Launcher$ExtClassLoader) 实现,上级为 Bootstrap,显示为 null - * 将 `JAVA_HOME/jre/lib/ext `或者被 `java.ext.dir` 系统变量所指定路径中的所有类库加载到内存中 - * 开发者可以使用扩展类加载器,创建的 JAR 放在此目录下,会由扩展类加载器自动加载 -* 应用程序类加载器(Application ClassLoader): - * 由 AppClassLoader(sun.misc.Launcher$AppClassLoader) 实现,上级为 Extension - * 负责加载环境变量 classpath 或系统属性 `java.class.path` 指定路径下的类库 - * 这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此称为系统类加载器 - * 可以直接使用这个类加载器,如果应用程序中没有自定义类加载器,这个就是程序中默认的类加载器 -* 自定义类加载器:由开发人员自定义的类加载器,上级是 Application + 缺点:对象被移动时(如进行GC后的内存重新排列),对象的 reference 也需要同步更新 + + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-对象访问-直接指针.png) -```java -public static void main(String[] args) { - //获取系统类加载器 - ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); - System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 - //获取其上层 扩展类加载器 - ClassLoader extClassLoader = systemClassLoader.getParent(); - System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6 - //获取其上层 获取不到引导类加载器 - ClassLoader bootStrapClassLoader = extClassLoader.getParent(); - System.out.println(bootStrapClassLoader);//null +参考文章:https://www.cnblogs.com/afraidToForget/p/12584866.html - //对于用户自定义类来说:使用系统类加载器进行加载 - ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); - System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 - //String 类使用引导类加载器进行加载的 --> java核心类库都是使用启动类加载器加载的 - ClassLoader classLoader1 = String.class.getClassLoader(); - System.out.println(classLoader1);//null -} -``` - -补充两个类加载器: +*** -* SecureClassLoader 扩展了 ClassLoader,新增了几个与使用相关的代码源和权限定义类验证(对 class 源码的访问权限)的方法,一般不会直接跟这个类打交道,更多是与它的子类 URLClassLoader 有所关联 -* ClassLoader 是一个抽象类,很多方法是空的没有实现,而 URLClassLoader 这个实现类为这些方法提供了具体的实现,并新增了 URLClassPath 类协助取得 Class 字节流等功能。在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承 URLClassLoader 类,这样就可以避免去编写 findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁 +### 对象创建 -*** +#### 生命周期 +在 Java 中,对象的生命周期包括以下几个阶段: +1. 创建阶段 (Created): +2. 应用阶段 (In Use):对象至少被一个强引用持有着 +3. 不可见阶段 (Invisible):程序的执行已经超出了该对象的作用域,不再持有该对象的任何强引用 +4. 不可达阶段 (Unreachable):该对象不再被任何强引用所持有,包括 GC Root 的强引用 +5. 收集阶段 (Collected):垃圾回收器对该对象的内存空间重新分配做好准备,该对象如果重写了 finalize() 方法,则会去执行该方法 +6. 终结阶段 (Finalized):等待垃圾回收器对该对象空间进行回收,当对象执行完 finalize()方 法后仍然处于不可达状态时进入该阶段 +7. 对象空间重分配阶段 (De-allocated):垃圾回收器对该对象的所占用的内存空间进行回收或者再分配 -#### 常用API -ClassLoader 类,是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器) -获取 ClassLoader 的途径: +参考文章:https://blog.csdn.net/sodino/article/details/38387049 -* 获取当前类的 ClassLoader:`clazz.getClassLoader()` -* 获取当前线程上下文的 ClassLoader:`Thread.currentThread.getContextClassLoader()` -* 获取系统的 ClassLoader:`ClassLoader.getSystemClassLoader()` -* 获取调用者的 ClassLoader:`DriverManager.getCallerClassLoader()` -ClassLoader 类常用方法: -* `getParent()`:返回该类加载器的超类加载器 -* `loadclass(String name)`:加载名为 name 的类,返回结果为 Class 类的实例,**该方法就是双亲委派模式** -* `findclass(String name)`:查找二进制名称为 name 的类,返回结果为 Class 类的实例,该方法会在检查完父类加载器之后被 loadClass() 方法调用 -* `findLoadedClass(String name)`:查找名称为 name 的已经被加载过的类,final 修饰无法重写 -* `defineClass(String name, byte[] b, int off, int len)`:将**字节流**解析成 JVM 能够识别的类对象 -* `resolveclass(Class c)`:链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 -* `InputStream getResourceAsStream(String name)`:指定资源名称获取输入流 +*** -*** +#### 创建时机 +类在第一次实例化加载一次,后续实例化不再加载,引用第一次加载的类 +Java 对象创建时机: -#### 加载模型 +1. 使用 new 关键字创建对象:由执行类实例创建表达式而引起的对象创建 -##### 加载机制 +2. 使用 Class 类的 newInstance 方法(反射机制) -在 JVM 中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制 +3. 使用 Constructor 类的 newInstance 方法(反射机制) -- **全盘加载:**当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入 + ```java + public class Student { + private int id; + public Student(Integer id) { + this.id = id; + } + public static void main(String[] args) throws Exception { + Constructor c = Student.class.getConstructor(Integer.class); + Student stu = c.newInstance(123); + } + } + ``` -- **双亲委派:**先让父类加载器加载该 Class,在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。简单来说就是,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载 + 使用 newInstance 方法的这两种方式创建对象使用的就是 Java 的反射机制,事实上 Class 的 newInstance 方法内部调用的也是 Constructor 的 newInstance 方法 -- **缓存机制:**会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象存入缓冲区中 - - 这就是修改了 Class 后,必须重新启动 JVM,程序所做的修改才会生效的原因 +4. 使用 Clone 方法创建对象:用 clone 方法创建对象的过程中并不会调用任何构造函数,要想使用 clone 方法,我们就必须先实现 Cloneable 接口并实现其定义的 clone 方法 +5. 使用(反)序列化机制创建对象:当反序列化一个对象时,JVM 会创建一个**单独的对象**,在此过程中,JVM 并不会调用任何构造函数,为了反序列化一个对象,需要让类实现 Serializable 接口 +从 Java 虚拟机层面看,除了使用 new 关键字创建对象的方式外,其他方式全部都是通过转变为 invokevirtual 指令直接创建对象的 @@ -12285,343 +12460,310 @@ ClassLoader 类常用方法: -##### 双亲委派 - -双亲委派模型(Parents Delegation Model):该模型要求除了顶层的启动类加载器外,其它类加载器都要有父类加载器,这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance) +#### 创建过程 -工作过程:一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载 +创建对象的过程: -双亲委派机制的优点: +1. 判断对象对应的类是否加载、链接、初始化 -* 可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证全局唯一性 +2. 为对象分配内存:指针碰撞、空闲链表。当一个对象被创建时,虚拟机就会为其分配内存来存放对象的实例变量及其从父类继承过来的实例变量,即使从**隐藏变量**也会被分配空间(继承部分解释了为什么会隐藏) -* Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一 +3. 处理并发安全问题: -* 保护程序安全,防止类库的核心 API 被随意篡改 + * 采用 CAS 配上自旋保证更新的原子性 + * 每个线程预先分配一块 TLAB - 例如:在工程中新建 java.lang 包,接着在该包下新建 String 类,并定义 main 函数 +4. 初始化分配的空间:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),保证对象实例字段在不赋值时可以直接使用,程序能访问到这些字段的数据类型所对应的零值 - ```java - public class String { - public static void main(String[] args) { - System.out.println("demo info"); - } - } - ``` - - 此时执行 main 函数,会出现异常,在类 java.lang.String 中找不到 main 方法,防止恶意篡改核心 API 库。出现该信息是因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 +5. 设置对象的对象头:将对象的所属类(类的元数据信息)、对象的 HashCode、对象的 GC 信息、锁信息等数据存储在对象头中 -双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但**顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类**(可见性) +6. 执行 init 方法进行实例化:实例变量初始化、实例代码块初始化 、构造函数初始化 - + * 实例变量初始化与实例代码块初始化: + 对实例变量直接赋值或者使用实例代码块赋值,**编译器会将其中的代码放到类的构造函数中去**,并且这些代码会被放在对超类构造函数的调用语句之后 (Java 要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前 + * 构造函数初始化: -*** + **Java 要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性**,在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到 Object 类。然后从 Object 类依次对以下各类进行实例化,初始化父类中的变量和执行构造函数 -##### 源码分析 +*** -```java -protected Class loadClass(String name, boolean resolve) - throws ClassNotFoundException { - synchronized (getClassLoadingLock(name)) { - // 调用当前类加载器的 findLoadedClass(name),检查当前类加载器是否已加载过指定 name 的类 - Class c = findLoadedClass(name); - - // 当前类加载器如果没有加载过 - if (c == null) { - long t0 = System.nanoTime(); - try { - // 判断当前类加载器是否有父类加载器 - if (parent != null) { - // 如果当前类加载器有父类加载器,则调用父类加载器的 loadClass(name,false) -          // 父类加载器的 loadClass 方法,又会检查自己是否已经加载过 - c = parent.loadClass(name, false); - } else { - // 当前类加载器没有父类加载器,说明当前类加载器是 BootStrapClassLoader -           // 则调用 BootStrap ClassLoader 的方法加载类 - c = findBootstrapClassOrNull(name); - } - } catch (ClassNotFoundException e) { } - if (c == null) { - // 如果调用父类的类加载器无法对类进行加载,则用自己的 findClass() 方法进行加载 - // 可以自定义 findClass() 方法 - long t1 = System.nanoTime(); - c = findClass(name); - // this is the defining class loader; record the stats - sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); - sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); - sun.misc.PerfCounter.getFindClasses().increment(); - } - } - if (resolve) { - // 链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 - resolveClass(c); - } - return c; - } -} -``` +#### 承上启下 +1. 一个实例变量在对象初始化的过程中会被赋值几次? + JVM 在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个实例变量被第一次赋值 + 在声明实例变量的同时对其进行了赋值操作,那么这个实例变量就被第二次赋值 + 在实例代码块中又对变量做了初始化操作,那么这个实例变量就被第三次赋值 + 在构造函数中也对变量做了初始化操作,那么这个实例变量就被第四次赋值 + 在 Java 的对象初始化过程中,一个实例变量最多可以被初始化 4 次 -**** +2. 类的初始化过程与类的实例化过程的异同? + 类的初始化是指类加载过程中的初始化阶段对类变量按照代码进行赋值的过程 + 类的实例化是指在类完全加载到内存中后创建对象的过程(类的实例化触发了类的初始化,先初始化才能实例化) +3. 假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?(**经典案例**) -##### 破坏委派 + ```java + public class StaticTest { + public static void main(String[] args) { + staticFunction();//调用静态方法,触发初始化 + } + + static StaticTest st = new StaticTest(); + + static { //静态代码块 + System.out.println("1"); + } + + { // 实例代码块 + System.out.println("2"); + } + + StaticTest() { // 实例构造器 + System.out.println("3"); + System.out.println("a=" + a + ",b=" + b); + } + + public static void staticFunction() { // 静态方法 + System.out.println("4"); + } + + int a = 110; // 实例变量 + static int b = 112; // 静态变量 + }/* Output: + 2 + 3 + a=110,b=0 + 1 + 4 + *///:~ + ``` -双亲委派模型并不是一个具有强制性约束的模型,而是 Java 设计者推荐给开发者的类加载器实现方式 + `static StaticTest st = new StaticTest();`: -破坏双亲委派模型的方式: + * 实例实例化不一定要在类初始化结束之后才开始 -* 自定义 ClassLoader + * 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形)。因此在实例化上述程序中的 st 变量时,**实际上是把实例化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置**,这就导致了实例初始化完全发生在静态初始化之前,这也是导致 a 为 110,b 为 0 的原因 - * 如果不想破坏双亲委派模型,只需要重写 findClass 方法 - * 如果想要去破坏双亲委派模型,需要去**重写 loadClass **方法 + 代码等价于: -* 引入线程**上下文类加载器** + ```java + public class StaticTest { + (){ + a = 110; // 实例变量 + System.out.println("2"); // 实例代码块 + System.out.println("3"); // 实例构造器中代码的执行 + System.out.println("a=" + a + ",b=" + b); // 实例构造器中代码的执行 + 类变量st被初始化 + System.out.println("1"); //静态代码块 + 类变量b被初始化为112 + } + } + ``` - Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供,而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里,SPI 接口中的代码需要加载具体的实现类: + - * SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的 - * SPI 的实现类是由系统类加载器来加载的,引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader 无法委派 AppClassLoader 来加载类 - JDK 开发人员引入了线程上下文类加载器(Thread Context ClassLoader),这种类加载器可以通过 Thread 类的 setContextClassLoader 方法进行设置线程上下文类加载器,在执行线程中抛弃双亲委派加载模式,使程序可以逆向使用类加载器,使 Bootstrap 加载器拿到了 Application 加载器加载的类,破坏了双亲委派模型 - -* 实现程序的动态性,如代码热替换(Hot Swap)、模块热部署(Hot Deployment) - IBM 公司主导的 JSR一291(OSGiR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi 中称为 Bundle)都有一个自己的类加载器,当更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换,在 OSGi 环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构 +*** - 当收到类加载请求时,OSGi 将按照下面的顺序进行类搜索: - 1. 将以 java.* 开头的类,委派给父类加载器加载 - 2. 否则,将委派列表名单内的类,委派给父类加载器加载 - 3. 否则,将 Import 列表中的类,委派给 Export 这个类的 Bundle 的类加载器加载 - 4. 否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载 - 5. 否则,查找类是否在自己的 Fragment Bundle 中,如果在就委派给 Fragment Bundle 类加载器加载 - 6. 否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载 - 7. 否则,类查找失败 - - 热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为,**热替换的关键需求在于服务不能中断**,修改必须立即表现正在运行的系统之中 - +### 加载过程 +#### 生命周期 +类是在运行期间**第一次使用时动态加载**的(不使用不加载),而不是一次性加载所有类,因为一次性加载会占用很多的内存,加载的类信息存放于一块成为方法区的内存空间 -*** +![](https://gitee.com/seazean/images/raw/master/Java/JVM-类的生命周期.png) +包括 7 个阶段: +* 加载(Loading) +* 链接:验证(Verification)、准备(Preparation)、解析(Resolution) +* 初始化(Initialization) +* 使用(Using) +* 卸载(Unloading) -#### 沙箱机制 -沙箱机制(Sandbox):将 Java 代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,来保证对代码的有效隔离,防止对本地系统造成破坏 -沙箱**限制系统资源访问**,包括 CPU、内存、文件系统、网络,不同级别的沙箱对资源访问的限制也不一样 +*** -* JDK1.0:Java 中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码被看作是不受信的。对于授信的本地代码,可以访问一切本地资源,而对于非授信的远程代码不可以访问本地资源,其实依赖于沙箱机制。如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现 -* JDK1.1:针对安全机制做了改进,增加了安全策略。允许用户指定代码对本地资源的访问权限 -* JDK1.2:改进了安全机制,增加了代码签名,不论本地代码或是远程代码都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制 -* JDK1.6:当前最新的安全机制,引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,不同的保护域对应不一样的权限。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问 - +#### 加载阶段 +加载是类加载的其中一个阶段,注意不要混淆 -*** +加载过程完成以下三件事: +- 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码) +- 将该字节流表示的**静态存储结构转换为方法区的运行时存储结构**(Java 类模型) +- 在内存中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口 +其中二进制字节流可以从以下方式中获取: -#### 自定义 +- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础 +- 从网络中获取,最典型的应用是 Applet +- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类 +- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 生成字节码 -对于自定义类加载器的实现,只需要继承 ClassLoader 类,覆写 findClass 方法即可 +将字节码文件加载至方法区后,会**在堆中**创建一个 java.lang.Class 对象,用来引用位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象 -作用:隔离加载类、修改类加载的方式、拓展加载源、防止源码泄漏 +方法区内部采用 C++ 的 instanceKlass 描述 Java 类的数据结构: -```java -//自定义类加载器,读取指定的类路径classPath下的class文件 -public class MyClassLoader extends ClassLoader{ - private String classPath; +* `_java_mirror` 即 java 的类镜像,例如对 String 来说就是 String.class,作用是把 class 暴露给 java 使用 +* `_super` 即父类、`_fields` 即成员变量、`_methods` 即方法、`_constants` 即常量池、`_class_loader` 即类加载器、`_vtable` **虚方法表**、`_itable` 接口方法表 - public MyClassLoader(String classPath) { - this.classPath = classPath; - } - - public MyClassLoader(ClassLoader parent, String byteCodePath) { - super(parent); - this.classPath = classPath; - } +加载过程: - @Override - protected Class findClass(String name) throws ClassNotFoundException { - BufferedInputStream bis = null; - ByteArrayOutputStream baos = null; - try { - // 获取字节码文件的完整路径 - String fileName = classPath + className + ".class"; - // 获取一个输入流 - bis = new BufferedInputStream(new FileInputStream(fileName)); - // 获取一个输出流 - baos = new ByteArrayOutputStream(); - // 具体读入数据并写出的过程 - int len; - byte[] data = new byte[1024]; - while ((len = bis.read(data)) != -1) { - baos.write(data, 0, len); - } - // 获取内存中的完整的字节数组的数据 - byte[] byteCodes = baos.toByteArray(); - // 调用 defineClass(),将字节数组的数据转换为 Class 的实例。 - Class clazz = defineClass(null, byteCodes, 0, byteCodes.length); - return clazz; - } catch (IOException e) { - e.printStackTrace(); - } finally { - try { - if (baos != null) - baos.close(); - } catch (IOException e) { - e.printStackTrace(); - } - try { - if (bis != null) - bis.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - return null; - } -} -``` +* 如果这个类还有父类没有加载,先加载父类 +* 加载和链接可能是交替运行的 +* Class 对象和 _java_mirror 相互持有对方的地址,堆中对象通过 instanceKlass 和元空间进行交互 -```java -public static void main(String[] args) { - MyClassLoader loader = new MyClassLoader("D:\Workspace\Project\JVM_study\src\java1\"); + - try { - Class clazz = loader.loadClass("Demo1"); - System.out.println("加载此类的类的加载器为:" + clazz.getClassLoader().getClass().getName());//MyClassLoader +创建数组类有些特殊,因为数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建,创建数组类的过程: - System.out.println("加载当前类的类的加载器的父类加载器为:" + clazz.getClassLoader().getParent().getClass().getName());//sun.misc.Launcher$AppClassLoader - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } -} -``` +- 如果数组的元素类型是引用类型,那么遵循定义的加载过程递归加载和创建数组的元素类型 +- JVM 使用指定的元素类型和数组维度来创建新的数组类 +- 基本数据类型由启动类加载器加载 -**** +*** -#### JDK9 +#### 链接阶段 -为了保证兼容性,JDK9 没有改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行做了一些变动: +##### 验证 -* 扩展机制被移除,扩展类加载器由于**向后兼容性**的原因被保留,不过被重命名为平台类加载器(platform classloader),可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取 +确保 Class 文件的字节流中包含的信息是否符合 JVM 规范,保证被加载类的正确性,不会危害虚拟机自身的安全 -* JDK9 基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数个 JMOD 文件),其中 Java 类库就满足了可扩展的需求,那就无须再保留 `\lib\ext` 目录,此前使用这个目录或者 `java.ext.dirs` 系统变量来扩展 JDK 功能的机制就不需要再存在 +主要包括**四种验证**: -* 启动类加载器、平台类加载器、应用程序类加载器全都继承于 `jdk.internal.loader.BuiltinClassLoader` +* 文件格式验证 - +* 语义检查,但凡在语义上不符合规范的,虚拟机不会给予验证通过 + * 是否所有的类都有父类的存在(除了 Object 外,其他类都应该有父类) + * 是否一些被定义为 final 的方法或者类被重写或继承了 -*** + * 非抽象类是否实现了所有抽象方法或者接口方法 + * 是否存在不兼容的方法 +* 字节码验证,试图通过对字节码流的分析,判断字节码是否可以被正确地执行 + * 在字节码的执行过程中,是否会跳转到一条不存在的指令 + * 函数的调用是否传递了正确类型的参数 + * 变量的赋值是不是给了正确的数据类型 + * 栈映射帧(StackMapTable)在这个阶段用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型 +* 符号引用验证,Class 文件在其常量池会通过字符串记录将要使用的其他类或者方法 -## 运行机制 -### 执行过程 - Java 文件编译执行的过程: +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java文件编译执行的过程.png) -- 类加载器:用于装载字节码文件(.class文件) -- 运行时数据区:用于分配存储空间 -- 执行引擎:执行字节码文件或本地方法 -- 垃圾回收器:用于对 JVM 中的垃圾内容进行回收 +##### 准备 +准备阶段为**静态变量分配内存并设置初始值**,使用的是方法区的内存: -**** +* 类变量也叫静态变量,就是是被 static 修饰的变量 +* 实例变量也叫对象变量,即没加 static 的变量 +说明:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 +类变量初始化: -### 字节码 +* static 变量分配空间和赋值是两个步骤:**分配空间在准备阶段完成,赋值在初始化阶段完成** +* 如果 static 变量是 final 的基本类型以及字符串常量,那么编译阶段值(方法区)就确定了,准备阶段会显式初始化 +* 如果 static 变量是 final 的,但属于引用类型或者构造器方法的字符串,赋值在初始化阶段完成 -#### 跨平台性 +实例: -Java 语言:跨平台的语言(write once ,run anywhere) +* 初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123: -* 当 Java 源代码成功编译成字节码后,在不同的平台上面运行**无须再次编译** -* 让一个 Java 程序正确地运行在 JVM 中,Java 源码就必须要被编译为符合 JVM 规范的字节码 + ```java + public static int value = 123; + ``` -编译过程中的编译器: +* 常量 value 被初始化为 123 而不是 0: -* 前端编译器: Sun 的全量式编译器 javac、 Eclipse 的增量式编译器 ECJ,把源代码编译为字节码文件 .class + ```java + public static final int value = 123; + ``` - * IntelliJ IDEA 使用 javac 编译器 - * Eclipse 中,当开发人员编写完代码后保存时,ECJ 编译器就会把未编译部分的源码逐行进行编译,而非每次都全量编译,因此 ECJ 的编译效率会比 javac 更加迅速和高效 - * 前端编译器并不会直接涉及编译优化等方面的技术,具体优化细节移交给 HotSpot 的 JIT 编译器负责 +* Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是0,故 boolean 的默认值就是 false -* 后端运行期编译器:HotSpot VM 的 C1、C2 编译器,也就是 JIT 编译器,Graal 编译器 - * JIT编译器:执行引擎部分详解 - * Graal 编译器:JDK10 HotSpot 加入的一个全新的即时编译器,编译效果短短几年时间就追平了 C2 -* 静态提前编译器:AOT (Ahead Of Time Compiler)编译器,直接把源代码编译成本地机器代码, +*** - * JDK 9 引入,是与即时编译相对立的一个概念,即时编译指的是在程序的运行过程中将字节码转换为机器码,AOT 是程序运行之前便将字节码转换为机器码 - * 优点:JVM 加载已经预编译成二进制库,可以直接执行,不必等待即时编译器的预热,减少 Java 应用第一次运行慢的现象 - * 缺点: - * 破坏了 Java **一次编译,到处运行**,必须为每个不同硬件编译对应的发行包 - * 降低了 Java 链接过程的动态性,加载的代码在编译期就必须全部已知 +##### 解析 +将常量池中类、接口、字段、方法的**符号引用替换为直接引用**(内存地址)的过程: +* 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念,如:包括类和接口的全限名、字段的名称和描述符、方法的名称和**方法描述符** +* 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,如果有了直接引用,那说明引用的目标必定已经存在于内存之中 +例如:在 `com.demo.Solution` 类中引用了 `com.test.Quest`,把 `com.test.Quest` 作为符号引用存进类常量池,在类加载完后,**用这个符号引用去方法区找这个类的内存地址** -*** +解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等 +* 在类加载阶段解析的是非虚方法,静态绑定 +* 也可以在初始化阶段之后再开始解析,这是为了支持 Java 的**动态绑定** +* 通过解析操作,符号引用就可以转变为目标方法在类的虚方法表中的位置,从而使得方法被成功调用 +```java +public class Load2 { + public static void main(String[] args) throws Exception{ + ClassLoader classloader = Load2.class.getClassLoader(); + // cloadClass 加载类方法不会导致类的解析和初始化,也不会加载D + Class c = classloader.loadClass("cn.jvm.t3.load.C"); + + // new C();会导致类的解析和初始化,从而解析初始化D + System.in.read(); + } +} +class C { + D d = new D(); +} +class D { +} +``` -#### 语言发展 -机器码:各种用二进制编码方式表示的指令,与 CPU 紧密相关,所以不同种类的 CPU 对应的机器指令不同 -指令:指令就是把机器码中特定的 0 和 1 序列,简化成对应的指令,例如 mov,inc 等,可读性稍好,但是不同的硬件平台的同一种指令(比如 mov),对应的机器码也可能不同 +**** -指令集:不同的硬件平台支持的指令是有区别的,每个平台所支持的指令,称之为对应平台的指令集 -- x86 指令集,对应的是 x86 架构的平台 -- ARM 指令集,对应的是 ARM 架构的平台 -汇编语言:用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址 +#### 初始化 -* 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令 -* 计算机只认识指令码,汇编语言编写的程序也必须翻译成机器指令码,计算机才能识别和执行 +##### 介绍 -高级语言:为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言 +初始化阶段才真正开始执行类中定义的 Java 程序代码,在准备阶段,类变量已经赋过一次系统要求的初始值;在初始化阶段,通过程序制定的计划去初始化类变量和其它资源,执行 -字节码:是一种中间状态(中间码)的二进制代码,比机器码更抽象,需要直译器转译后才能成为机器码 +在编译生成 class 文件时,编译器会产生两个方法加于 class 文件中,一个是类的初始化方法 clinit,另一个是实例的初始化方法 init -* 字节码为了实现特定软件运行和软件环境,与硬件环境无关 -* 通过编译器和虚拟机器实现,编译器将源码编译成字节码,虚拟机器将字节码转译为可以直接执行的指令 +类构造器 () 与实例构造器 () 不同,它不需要程序员进行显式调用,在一个类的生命周期中,类构造器最多被虚拟机**调用一次**,而实例构造器则会被虚拟机调用多次,只要程序员创建对象 - +类在第一次实例化加载一次,把 class 读入内存,后续实例化不再加载,引用第一次加载的类 @@ -12629,108 +12771,83 @@ Java 语言:跨平台的语言(write once ,run anywhere) +##### clinit +():类构造器,由编译器自动收集类中**所有类变量的赋值动作和静态语句块**中的语句合并产生的 -#### 类结构 - -##### 文件结构 +作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块 -字节码是一种二进制的类文件,是编译之后供虚拟机解释执行的二进制字节码文件,**一个 class 文件对应一个 public 类型的类或接口** +* 如果类中没有静态变量或静态代码块,那么 clinit 方法将不会被生成 +* clinit 方法只执行一次,在执行 clinit 方法时,必须先执行父类的clinit方法 +* static 变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定 +* static 不加 final 的变量都在初始化环节赋值 -字节码内容是 **JVM 的字节码指令**,不是机器码,C、C++ 经由编译器直接生成机器码,所以执行效率比 Java 高 +**线程安全**问题: -JVM 官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html +* 虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都阻塞等待,直到活动线程执行 () 方法完毕 +* 如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽 -根据 JVM 规范,类文件结构如下: +特别注意:静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问 ```java -ClassFile { - u4 magic; - u2 minor_version; - u2 major_version; - u2 constant_pool_count; - cp_info constant_pool[constant_pool_count-1]; - u2 access_flags; - u2 this_class; - u2 super_class; - u2 interfaces_count; - u2 interfaces[interfaces_count]; - u2 fields_count; - field_info fields[fields_count]; - u2 methods_count; - method_info methods[methods_count]; - u2 attributes_count; - attribute_info attributes[attributes_count]; +public class Test { + static { + //i = 0; // 给变量赋值可以正常编译通过 + System.out.print(i); // 这句编译器会提示“非法向前引用” + } + static int i = 1; } ``` -| 类型 | 名称 | 说明 | 长度 | 数量 | -| -------------- | ------------------- | -------------------- | ------- | --------------------- | -| u4 | magic | 魔数,识别类文件格式 | 4个字节 | 1 | -| u2 | minor_version | 副版本号(小版本) | 2个字节 | 1 | -| u2 | major_version | 主版本号(大版本) | 2个字节 | 1 | -| u2 | constant_pool_count | 常量池计数器 | 2个字节 | 1 | -| cp_info | constant_pool | 常量池表 | n个字节 | constant_pool_count-1 | -| u2 | access_flags | 访问标识 | 2个字节 | 1 | -| u2 | this_class | 类索引 | 2个字节 | 1 | -| u2 | super_class | 父类索引 | 2个字节 | 1 | -| u2 | interfaces_count | 接口计数 | 2个字节 | 1 | -| u2 | interfaces | 接口索引集合 | 2个字节 | interfaces_count | -| u2 | fields_count | 字段计数器 | 2个字节 | 1 | -| field_info | fields | 字段表 | n个字节 | fields_count | -| u2 | methods_count | 方法计数器 | 2个字节 | 1 | -| method_info | methods | 方法表 | n个字节 | methods_count | -| u2 | attributes_count | 属性计数器 | 2个字节 | 1 | -| attribute_info | attributes | 属性表 | n个字节 | attributes_count | +接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法,两者不同的是: -Class 文件格式采用一种类似于 C 语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表 +* 在初始化一个接口时,并不会先初始化它的父接口,所以执行接口的 () 方法不需要先执行父接口的 () 方法 +* 在初始化一个类时,不会先初始化所实现的接口,所以接口的实现类在初始化时不会执行接口的 () 方法 +* 只有当父接口中定义的变量使用时,父接口才会初始化 -* 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串 -* 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,表都以 `_info` 结尾,用于描述有层次关系的数据,整个 Class 文件本质上就是一张表,由于表没有固定长度,所以通常会在其前面加上个数说明 -获取方式: -* HelloWorld.java 执行 `javac -parameters -d . HellowWorld.java`指令 -* 写入文件指令 `javap -v xxx.class >xxx.txt` -* IDEA 插件 jclasslib +**** -*** +##### 时机 +类的初始化是懒惰的,只有在首次使用时才会被装载,JVM 不会无条件地装载 Class 类型,Java 虚拟机规定,一个类或接口在初次使用前,必须要进行初始化 +**主动引用**:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列情况必须对类进行初始化(加载、验证、准备都会发生): -##### 魔数版本 +* 当创建一个类的实例时,使用 new 关键字,或者通过反射、克隆、反序列化(前文讲述的对象的创建时机) +* 当调用类的静态方法或访问静态字段时,遇到 getstatic、putstatic、invokestatic 这三条字节码指令,如果类没有进行过初始化,则必须先触发其初始化 + * getstatic:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池) + * putstatic:程序给类的静态变量赋值 + * invokestatic :调用一个类的静态方法 +* 使用 java.lang.reflect 包的方法对类进行反射调用时,如果类没有进行初始化,则需要先触发其初始化 +* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化,但这条规则并**不适用于接口** +* 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类 +* MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这两个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类 +* 补充:当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化 -魔数:每个 Class 文件开头的 4 个字节的无符号整数称为魔数(Magic Number),是 Class 文件的标识符,代表这是一个能被虚拟机接受的有效合法的 Class 文件, +**被动引用**:所有引用类的方式都不会触发初始化,称为被动引用 -* 魔数值固定为 0xCAFEBABE,不符合则会抛出错误 +* 通过子类引用父类的静态字段,不会导致子类初始化,只会触发父类的初始化 +* 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法 +* 常量(final 修饰)在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化 +* 调用 ClassLoader 类的 loadClass() 方法加载一个类,并不是对类的主动使用,不会导致类的初始化 -* 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动 -版本:4 个 字节,5 6两个字节代表的是编译的副版本号 minor_version,而 7 8 两个字节是编译的主版本号 major_version -* 不同版本的 Java 编译器编译的 Class 文件对应的版本是不一样的,高版本的 Java 虚拟机可以执行由低版本编译器生成的 Class 文件,反之 JVM 会抛出异常 `java.lang.UnsupportedClassVersionError` +*** -| 主版本(十进制) | 副版本(十进制) | 编译器版本 | -| ---------------- | ---------------- | ---------- | -| 45 | 3 | 1.1 | -| 46 | 0 | 1.2 | -| 47 | 0 | 1.3 | -| 48 | 0 | 1.4 | -| 49 | 0 | 1.5 | -| 50 | 0 | 1.6 | -| 51 | 0 | 1.7 | -| 52 | 0 | 1.8 | -| 53 | 0 | 1.9 | -| 54 | 0 | 1.10 | -| 55 | 0 | 1.11 | -![](https://gitee.com/seazean/images/raw/master/Java/JVM-类结构.png) +##### init + +init 指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行 +实例化即调用 ()V ,虚拟机会保证这个类的构造方法的线程安全,先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块,没有成员变量初始化和代码块则不会执行 -图片来源:https://www.bilibili.com/video/BV1PJ411n7xZ +类实例化过程:**父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数** @@ -12738,81 +12855,53 @@ Class 文件格式采用一种类似于 C 语言结构体的方式进行数据 -##### 常量池 - -常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的无符号数,代表常量池计数器(constant_pool_count),这个容量计数是从 1 而不是 0 开始,是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达不引用任何一个常量池项目,这种情况可用索引值 0 来表示 +#### 卸载阶段 -constant_pool 是一种表结构,以1 ~ constant_pool_count - 1为索引,表明有多少个常量池表项。表项中存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池 +时机:执行了 System.exit() 方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java虚拟机进程终止 -* 字面量(Literal) :基本数据类型、字符串类型常量、声明为 final 的常量值等 +卸载类即该类的 **Class 对象被 GC**,卸载类需要满足3个要求: -* 符号引用(Symbolic References):类和接口的全限定名、字段的名称和描述符、方法的名称和描述符 +1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象 +2. 该类没有在其他任何地方被引用 +3. 该类的类加载器的实例已被 GC,一般是可替换类加载器的场景,如 OSGi、JSP 的重加载等,很难达成 - * 全限定名:com/test/Demo 这个就是类的全限定名,仅仅是把包名的 `.` 替换成 `/`,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个 `;` 表示全限定名结束 +在 JVM 生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,这些类始终是可及的 - * 简单名称:指没有类型和参数修饰的方法或者字段名称,比如字段 x 的简单名称就是 x - * 描述符:用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值 - | 标志符 | 含义 | - | ------ | --------------------------------------------------------- | - | B | 基本数据类型 byte | - | C | 基本数据类型 char | - | D | 基本数据类型 double | - | F | 基本数据类型 float | - | I | 基本数据类型 int | - | J | 基本数据类型 long | - | S | 基本数据类型 short | - | Z | 基本数据类型 boolean | - | V | 代表 void 类型 | - | L | 对象类型,比如:`Ljava/lang/Object;`,不同方法间用`;`隔开 | - | [ | 数组类型,代表一维数组。比如:`double[][][] is [[[D` | +**** -常量类型和结构: -| 类型 | 标志(或标识) | 描述 | -| -------------------------------- | ------------ | ---------------------- | -| CONSTANT_utf8_info | 1 | UTF-8编码的字符串 | -| CONSTANT_Integer_info | 3 | 整型字面量 | -| CONSTANT_Float_info | 4 | 浮点型字面量 | -| CONSTANT_Long_info | 5 | 长整型字面量 | -| CONSTANT_Double_info | 6 | 双精度浮点型字面量 | -| CONSTANT_Class_info | 7 | 类或接口的符号引用 | -| CONSTANT_String_info | 8 | 字符串类型字面量 | -| CONSTANT_Fieldref_info | 9 | 字段的符号引用 | -| CONSTANT_Methodref_info | 10 | 类中方法的符号引用 | -| CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 | -| CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 | -| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 | -| CONSTANT_MethodType_info | 16 | 标志方法类型 | -| CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 | -18 种常量没有出现 byte、short、char,boolean 的原因:编译之后都可以理解为 Integer +### 类加载器 +#### 类加载 +类加载方式: -**** +* 隐式加载:不直接在代码中调用 ClassLoader 的方法加载类对象 + * 创建类对象、使用类的静态域、创建子类对象、使用子类的静态域 + * 在 JVM 启动时,通过三大类加载器加载 class +* 显式加载: + * ClassLoader.loadClass(className):只加载和连接,**不会进行初始化** + * Class.forName(String name, boolean initialize, ClassLoader loader):使用 loader 进行加载和连接,根据参数 initialize 决定是否初始化 +类的唯一性: +* 在 JVM 中表示两个 class 对象判断为同一个类存在的两个必要条件: + - 类的完整类名必须一致,包括包名 + - 加载这个类的 ClassLoader(指 ClassLoader 实例对象)必须相同 +* 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true -##### 访问标识 +命名空间: -访问标识(access_flag),又叫访问标志、访问标记,该标识用两个字节表示,用于识别一些类或者接口层次的访问信息,包括这个 Class 是类还是接口,是否定义为 public类型,是否定义为 abstract类型等 +- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成 +- 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类 -* 类的访问权限通常为 ACC_ 开头的常量 -* 每一种类型的表示都是通过设置访问标记的 32 位中的特定位来实现的,比如若是 public final 的类,则该标记为 `ACC_PUBLIC | ACC_FINAL` -* 使用 `ACC_SUPER` 可以让类更准确地定位到父类的方法,确定类或接口里面的 invokespecial 指令使用的是哪一种执行语义,现代编译器都会设置并且使用这个标记 +基本特征: -| 标志名称 | 标志值 | 含义 | -| -------------- | ------ | ------------------------------------------------------------ | -| ACC_PUBLIC | 0x0001 | 标志为 public 类型 | -| ACC_FINAL | 0x0010 | 标志被声明为 final,只有类可以设置 | -| ACC_SUPER | 0x0020 | 标志允许使用 invokespecial 字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真,使用增强的方法调用父类方法 | -| ACC_INTERFACE | 0x0200 | 标志这是一个接口 | -| ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 | -| ACC_SYNTHETIC | 0x1000 | 标志此类并非由用户代码产生(由编译器产生的类,没有源码对应) | -| ACC_ANNOTATION | 0x2000 | 标志这是一个注解 | -| ACC_ENUM | 0x4000 | 标志这是一个枚举 | +* **可见性**,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的 +* **单一性**,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,不会在子加载器中重复加载 @@ -12820,84 +12909,89 @@ constant_pool 是一种表结构,以1 ~ constant_pool_count - 1为索引,表 -##### 索引集合 +#### 加载器 -类索引、父类索引、接口索引集合 +类加载器是 Java 的核心组件,用于加载字节码到 JVM 内存,得到 Class 类的对象 -* 类索引用于确定这个类的全限定名 +从 Java 虚拟机规范来讲,只存在以下两种不同的类加载器: -* 父类索引用于确定这个类的父类的全限定名,Java 语言不允许多重继承,所以父类索引只有一个,除了Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为0 +- 启动类加载器(Bootstrap ClassLoader):使用 C++ 实现,是虚拟机自身的一部分 +- 自定义类加载器(User-Defined ClassLoader):Java 虚拟机规范**将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器**,使用 Java 语言实现,独立于虚拟机 -* 接口索引集合就用来描述这个类实现了哪些接口 - * interfaces_count 项的值表示当前类或接口的直接超接口数量 - * interfaces[] 接口索引集合,被实现的接口将按 implements 语句后的接口顺序从左到右排列在接口索引集合中 +从 Java 开发人员的角度看: -| 长度 | 含义 | -| ---- | ---------------------------- | -| u2 | this_class | -| u2 | super_class | -| u2 | interfaces_count | -| u2 | interfaces[interfaces_count] | +* 启动类加载器(Bootstrap ClassLoader): + * 处于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类 + * 类加载器负责加载在 `JAVA_HOME/jre/lib `或 `sun.boot.class.path` 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的类,并且是虚拟机识别的类库加载到虚拟机内存中 + * 仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在 lib 目录中也不会被加载 + * 启动类加载器无法被 Java 程序直接引用,编写自定义类加载器时,如果要把加载请求委派给启动类加载器,直接使用 null 代替 +* 扩展类加载器(Extension ClassLoader): + * 由 ExtClassLoader (sun.misc.Launcher$ExtClassLoader) 实现,上级为 Bootstrap,显示为 null + * 将 `JAVA_HOME/jre/lib/ext `或者被 `java.ext.dir` 系统变量所指定路径中的所有类库加载到内存中 + * 开发者可以使用扩展类加载器,创建的 JAR 放在此目录下,会由扩展类加载器自动加载 +* 应用程序类加载器(Application ClassLoader): + * 由 AppClassLoader(sun.misc.Launcher$AppClassLoader) 实现,上级为 Extension + * 负责加载环境变量 classpath 或系统属性 `java.class.path` 指定路径下的类库 + * 这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此称为系统类加载器 + * 可以直接使用这个类加载器,如果应用程序中没有自定义类加载器,这个就是程序中默认的类加载器 +* 自定义类加载器:由开发人员自定义的类加载器,上级是 Application +```java +public static void main(String[] args) { + //获取系统类加载器 + ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); + System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 + //获取其上层 扩展类加载器 + ClassLoader extClassLoader = systemClassLoader.getParent(); + System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6 -*** + //获取其上层 获取不到引导类加载器 + ClassLoader bootStrapClassLoader = extClassLoader.getParent(); + System.out.println(bootStrapClassLoader);//null + //对于用户自定义类来说:使用系统类加载器进行加载 + ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); + System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 + //String 类使用引导类加载器进行加载的 --> java核心类库都是使用启动类加载器加载的 + ClassLoader classLoader1 = String.class.getClassLoader(); + System.out.println(classLoader1);//null -##### 字段表 +} +``` -字段 fields 用于描述接口或类中声明的变量,包括类变量以及实例变量,但不包括方法内部、代码块内部声明的局部变量以及从父类或父接口继承。字段叫什么名字、被定义为什么数据类型,都是无法固定的,只能引用常量池中的常量来描述 +补充两个类加载器: -fields_count(字段计数器),表示当前 class 文件 fields 表的成员个数,用两个字节来表示 +* SecureClassLoader 扩展了 ClassLoader,新增了几个与使用相关的代码源和权限定义类验证(对 class 源码的访问权限)的方法,一般不会直接跟这个类打交道,更多是与它的子类 URLClassLoader 有所关联 +* ClassLoader 是一个抽象类,很多方法是空的没有实现,而 URLClassLoader 这个实现类为这些方法提供了具体的实现,并新增了 URLClassPath 类协助取得 Class 字节流等功能。在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承 URLClassLoader 类,这样就可以避免去编写 findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁 -fields[](字段表): -* 表中的每个成员都是一个 fields_info 结构的数据项,用于表示当前类或接口中某个字段的完整描述 -* 字段访问标识: +*** - | 标志名称 | 标志值 | 含义 | - | ------------- | ------ | -------------------------- | - | ACC_PUBLIC | 0x0001 | 字段是否为public | - | ACC_PRIVATE | 0x0002 | 字段是否为private | - | ACC_PROTECTED | 0x0004 | 字段是否为protected | - | ACC_STATIC | 0x0008 | 字段是否为static | - | ACC_FINAL | 0x0010 | 字段是否为final | - | ACC_VOLATILE | 0x0040 | 字段是否为volatile | - | ACC_TRANSTENT | 0x0080 | 字段是否为transient | - | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | - | ACC_ENUM | 0x4000 | 字段是否为enum | -* 字段名索引:根据该值查询常量池中的指定索引项即可 -* 描述符索引:用来描述字段的数据类型、方法的参数列表和返回值 +#### 常用API - | 字符 | 类型 | 含义 | - | ----------- | --------- | ----------------------- | - | B | byte | 有符号字节型树 | - | C | char | Unicode字符,UTF-16编码 | - | D | double | 双精度浮点数 | - | F | float | 单精度浮点数 | - | I | int | 整型数 | - | J | long | 长整数 | - | S | short | 有符号短整数 | - | Z | boolean | 布尔值true/false | - | V | void | 代表void类型 | - | L Classname | reference | 一个名为Classname的实例 | - | [ | reference | 一个一维数组 | +ClassLoader 类,是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器) -* 属性表集合:属性个数存放在 attribute_count 中,属性具体内容存放在 attribute 数组中,一个字段还可能拥有一些属性,用于存储更多的额外信息,比如初始化值、一些注释信息等 +获取 ClassLoader 的途径: - ```java - ConstantValue_attribute{ - u2 attribute_name_index; - u4 attribute_length; - u2 constantvalue_index; - } - ``` +* 获取当前类的 ClassLoader:`clazz.getClassLoader()` +* 获取当前线程上下文的 ClassLoader:`Thread.currentThread.getContextClassLoader()` +* 获取系统的 ClassLoader:`ClassLoader.getSystemClassLoader()` +* 获取调用者的 ClassLoader:`DriverManager.getCallerClassLoader()` - 对于常量属性而言,attribute_length 值恒为2 +ClassLoader 类常用方法: + +* `getParent()`:返回该类加载器的超类加载器 +* `loadclass(String name)`:加载名为 name 的类,返回结果为 Class 类的实例,**该方法就是双亲委派模式** +* `findclass(String name)`:查找二进制名称为 name 的类,返回结果为 Class 类的实例,该方法会在检查完父类加载器之后被 loadClass() 方法调用 +* `findLoadedClass(String name)`:查找名称为 name 的已经被加载过的类,final 修饰无法重写 +* `defineClass(String name, byte[] b, int off, int len)`:将**字节流**解析成 JVM 能够识别的类对象 +* `resolveclass(Class c)`:链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 +* `InputStream getResourceAsStream(String name)`:指定资源名称获取输入流 @@ -12905,108 +12999,108 @@ fields[](字段表): -##### 方法表 +#### 加载模型 -方法表是 methods 指向常量池索引集合,其中每一个 method_info 项都对应着一个类或者接口中的方法信息,完整描述了每个方法的签名 +##### 加载机制 -* 如果这个方法不是抽象的或者不是 native 的,字节码中就会体现出来 -* methods 表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法 -* methods 表可能会出现由编译器自动添加的方法,比如初始化方法 和实例化方法 +在 JVM 中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制 -**重载(Overload)**一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,因为返回值不会包含在特征签名之中,因此 Java 语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在 Class 文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存 +- **全盘加载:**当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入 -methods_count(方法计数器):表示 class 文件 methods 表的成员个数,使用两个字节来表示 +- **双亲委派:**先让父类加载器加载该 Class,在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。简单来说就是,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,**依次递归**,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载 -methods[](方法表):每个表项都是一个 method_info 结构,表示当前类或接口中某个方法的完整描述 +- **缓存机制:**会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象存入缓冲区中 + - 这就是修改了 Class 后,必须重新启动 JVM,程序所做的修改才会生效的原因 -* 方法表结构如下: - | 类型 | 名称 | 含义 | 数量 | - | -------------- | ---------------- | ---------- | ---------------- | - | u2 | access_flags | 访问标志 | 1 | - | u2 | name_index | 字段名索引 | 1 | - | u2 | descriptor_index | 描述符索引 | 1 | - | u2 | attrubutes_count | 属性计数器 | 1 | - | attribute_info | attributes | 属性集合 | attributes_count | -* 方法表访问标志: - | 标志名称 | 标志值 | 含义 | - | ------------- | ------ | -------------------------- | - | ACC_PUBLIC | 0x0001 | 字段是否为 public | - | ACC_PRIVATE | 0x0002 | 字段是否为 private | - | ACC_PROTECTED | 0x0004 | 字段是否为 protected | - | ACC_STATIC | 0x0008 | 字段是否为 static | - | ACC_FINAL | 0x0010 | 字段是否为 final | - | ACC_VOLATILE | 0x0040 | 字段是否为 volatile | - | ACC_TRANSTENT | 0x0080 | 字段是否为 transient | - | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | - | ACC_ENUM | 0x4000 | 字段是否为 enum | +*** -*** +##### 双亲委派 +双亲委派模型(Parents Delegation Model):该模型要求除了顶层的启动类加载器外,其它类加载器都要有父类加载器,这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance) -##### 属性表 +工作过程:一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载 -属性表集合,指的是 Class 文件所携带的辅助信息,比如该 Class 文件的源文件的名称,以及任何带有 `RetentionPolicy.CLASS` 或者 `RetentionPolicy.RUNTIME` 的注解,这类信息通常被用于 Java 虚拟机的验证和运行,以及 Java 程序的调试。字段表、方法表都可以有自己的属性表,用于描述某些场景专有的信息 +双亲委派机制的优点: -attributes_ count(属性计数器):表示当前文件属性表的成员个数 +* 可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证全局唯一性 -attributes[](属性表):属性表的每个项的值必须是 attribute_info 结构 +* Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一 -* 属性的通用格式: +* 保护程序安全,防止类库的核心 API 被随意篡改 + + 例如:在工程中新建 java.lang 包,接着在该包下新建 String 类,并定义 main 函数 ```java - ConstantValue_attribute{ - u2 attribute_name_index; //属性名索引 - u4 attribute_length; //属性长度 - u2 attribute_info; //属性表 + public class String { + public static void main(String[] args) { + System.out.println("demo info"); + } } ``` + + 此时执行 main 函数,会出现异常,在类 java.lang.String 中找不到 main 方法,防止恶意篡改核心 API 库。出现该信息是因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法 -* 属性类型: - - | 属性名称 | 使用位置 | 含义 | - | ------------------------------------- | ------------------ | ------------------------------------------------------------ | - | Code | 方法表 | Java 代码编译成的字节码指令 | - | ConstantValue | 字段表 | final 关键字定义的常量池 | - | Deprecated | 类、方法、字段表 | 被声明为 deprecated 的方法和字段 | - | Exceptions | 方法表 | 方法抛出的异常 | - | EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 | - | InnerClass | 类文件 | 内部类列表 | - | LineNumberTable | Code 属性 | Java 源码的行号与字节码指令的对应关系 | - | LocalVariableTable | Code 属性 | 方法的局部变量描述 | - | StackMapTable | Code 属性 | JDK1.6 中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 | - | Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 | - | SourceFile | 类文件 | 记录源文件名称 | - | SourceDebugExtension | 类文件 | 用于存储额外的调试信息 | - | Syothetic | 类,方法表,字段表 | 标志方法或字段为编泽器自动生成的 | - | LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 | - | RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 | - | RuntimelnvisibleAnnotations | 类,方法表,字段表 | 用于指明哪些注解是运行时不可见的 | - | RuntimeVisibleParameterAnnotation | 方法表 | 作用与 RuntimeVisibleAnnotations 属性类似,只不过作用对象为方法 | - | RuntirmelnvisibleParameterAnniotation | 方法表 | 作用与 RuntimelnvisibleAnnotations 属性类似,作用对象哪个为方法参数 | - | AnnotationDefauit | 方法表 | 用于记录注解类元素的默认值 | - | BootstrapMethods | 类文件 | 用于保存 invokeddynanic 指令引用的引导方式限定符 | - +双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但**顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类**(可见性) + -**** +*** -#### 编译指令 +##### 源码分析 -##### javac +```java +protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + // 调用当前类加载器的 findLoadedClass(name),检查当前类加载器是否已加载过指定 name 的类 + Class c = findLoadedClass(name); + + // 当前类加载器如果没有加载过 + if (c == null) { + long t0 = System.nanoTime(); + try { + // 判断当前类加载器是否有父类加载器 + if (parent != null) { + // 如果当前类加载器有父类加载器,则调用父类加载器的 loadClass(name,false) +          // 父类加载器的 loadClass 方法,又会检查自己是否已经加载过 + c = parent.loadClass(name, false); + } else { + // 当前类加载器没有父类加载器,说明当前类加载器是 BootStrapClassLoader +           // 则调用 BootStrap ClassLoader 的方法加载类 + c = findBootstrapClassOrNull(name); + } + } catch (ClassNotFoundException e) { } -javac:编译命令,将 java 源文件编译成 class 字节码文件 + if (c == null) { + // 如果调用父类的类加载器无法对类进行加载,则用自己的 findClass() 方法进行加载 + // 可以自定义 findClass() 方法 + long t1 = System.nanoTime(); + c = findClass(name); -`javac xx.java` 不会在生成对应的局部变量表等信息,使用 `javac -g xx.java` 可以生成所有相关信息 + // this is the defining class loader; record the stats + sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); + sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); + sun.misc.PerfCounter.getFindClasses().increment(); + } + } + if (resolve) { + // 链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析 + resolveClass(c); + } + return c; + } +} +``` @@ -13014,62 +13108,43 @@ javac:编译命令,将 java 源文件编译成 class 字节码文件 -##### javap - -javap 反编译生成的字节码文件,根据 class 字节码文件,反解析出当前类对应的 code 区 (字节码指令)、局部变量表、异常表和代码行偏移量映射表、常量池等信息 - -用法:javap - -```sh --help --help -? 输出此用法消息 --version 版本信息 --public 仅显示公共类和成员 --protected 显示受保护的/公共类和成员 --package 显示程序包/受保护的/公共类和成员 (默认) --p -private 显示所有类和成员 - #常用的以下三个 --v -verbose 输出附加信息 --l 输出行号和本地变量表 --c 对代码进行反汇编 #反编译 - --s 输出内部类型签名 --sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列) --constants 显示最终常量 --classpath 指定查找用户类文件的位置 --cp 指定查找用户类文件的位置 --bootclasspath 覆盖引导类文件的位置 -``` - - +##### 破坏委派 -*** +双亲委派模型并不是一个具有强制性约束的模型,而是 Java 设计者推荐给开发者的类加载器实现方式 +破坏双亲委派模型的方式: +* 自定义 ClassLoader -#### 指令集 + * 如果不想破坏双亲委派模型,只需要重写 findClass 方法 + * 如果想要去破坏双亲委派模型,需要去**重写 loadClass **方法 -##### 执行指令 +* 引入线程**上下文类加载器** -Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某种操作的操作码(opcode)以及零至多个代表此操作所需参数的操作数(operand)所构成,虚拟机中许多指令并不包含操作数,只有一个操作码(零地址指令) + Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供,而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里,SPI 接口中的代码需要加载具体的实现类: -由于限制了 Java 虚拟机操作码的长度为一个字节(0~255),所以指令集的操作码总数不可能超过 256 条 + * SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的 + * SPI 的实现类是由系统类加载器来加载的,引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader 无法委派 AppClassLoader 来加载类 -在 JVM 的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如 iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中,而 fload 指令加载的则是 float 类型的数据 + JDK 开发人员引入了线程上下文类加载器(Thread Context ClassLoader),这种类加载器可以通过 Thread 类的 setContextClassLoader 方法进行设置线程上下文类加载器,在执行线程中抛弃双亲委派加载模式,使程序可以逆向使用类加载器,使 Bootstrap 加载器拿到了 Application 加载器加载的类,破坏了双亲委派模型 + +* 实现程序的动态性,如代码热替换(Hot Swap)、模块热部署(Hot Deployment) -* i 代表对 int 类型的数据操作 -* l 代表 long -* s 代表 short -* b 代表 byte -* c 代表 char -* f 代表 float -* d 代表 double + IBM 公司主导的 JSR一291(OSGiR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi 中称为 Bundle)都有一个自己的类加载器,当更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换,在 OSGi 环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构 -大部分的指令都没有支持 byte、char、short、boolean 类型,编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展(Sign-Extend-)为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展(Zero-Extend-)为相应的 int 类型数据 + 当收到类加载请求时,OSGi 将按照下面的顺序进行类搜索: -在做值相关操作时: + 1. 将以 java.* 开头的类,委派给父类加载器加载 + 2. 否则,将委派列表名单内的类,委派给父类加载器加载 + 3. 否则,将 Import 列表中的类,委派给 Export 这个类的 Bundle 的类加载器加载 + 4. 否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载 + 5. 否则,查找类是否在自己的 Fragment Bundle 中,如果在就委派给 Fragment Bundle 类加载器加载 + 6. 否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载 + 7. 否则,类查找失败 + + 热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为,**热替换的关键需求在于服务不能中断**,修改必须立即表现正在运行的系统之中 -- 一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是值,也可能是对象的引用)被压入操作数栈 -- 一个指令,也可以从操作数栈中取出一到多个值(pop 多次),完成赋值、加减乘除、方法传参、系统调用等等操作 + @@ -13077,195 +13152,180 @@ Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某 -##### 加载存储 - -加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递 - -局部变量压栈指令:将给定的局部变量表中的数据压入操作数栈 +#### 沙箱机制 -* xload、xload_n,x 表示取值数据类型,为 i、l、f、d、a, n 为 0 到 3 -* 指令 xload_n 表示将第 n 个局部变量压入操作数栈,aload_n 表示将一个对象引用压栈 -* 指令 xload n 通过指定参数的形式,把局部变量压入操作数栈,局部变量数量超过 4 个时使用这个命令 +沙箱机制(Sandbox):将 Java 代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,来保证对代码的有效隔离,防止对本地系统造成破坏 -常量入栈指令:将常数压入操作数栈,根据数据类型和入栈内容的不同,又分为 const、push、ldc指令 +沙箱**限制系统资源访问**,包括 CPU、内存、文件系统、网络,不同级别的沙箱对资源访问的限制也不一样 -* push:包括 bipush 和 sipush,区别在于接收数据类型的不同,bipush 接收 8 位整数作为参数,sipush 接收 16 位整数 -* ldc:如果以上指令不能满足需求,可以使用 ldc 指令,接收一个 8 位的参数,该参数指向常量池中的 int、 float 或者 String 的索引,将指定的内容压入堆栈。ldc_w 接收两个 8 位参数,能支持的索引范围更大,如果要压入的元素是 long 或 double 类型的,则使用 ldc2_w 指令 -* aconst_null 将 null 对象引用压入栈,iconst_m1 将 int 类型常量 -1 压入栈,iconst_0 将 int 类型常量 0 压入栈 +* JDK1.0:Java 中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码被看作是不受信的。对于授信的本地代码,可以访问一切本地资源,而对于非授信的远程代码不可以访问本地资源,其实依赖于沙箱机制。如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现 +* JDK1.1:针对安全机制做了改进,增加了安全策略。允许用户指定代码对本地资源的访问权限 +* JDK1.2:改进了安全机制,增加了代码签名,不论本地代码或是远程代码都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制 +* JDK1.6:当前最新的安全机制,引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,不同的保护域对应不一样的权限。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问 -出栈装入局部变量表指令:将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值 + -* xstore、xstore_n,x 表示取值类型为 i、l、f、d、a, n 为 0 到 3 -* xastore 表示存入数组,x 取值为 i、l、f、d、a、b、c、s -扩充局部变量表的访问索引的指令:wide +*** -**** +#### 自定义 +对于自定义类加载器的实现,只需要继承 ClassLoader 类,覆写 findClass 方法即可 -##### 算术指令 +作用:隔离加载类、修改类加载的方式、拓展加载源、防止源码泄漏 -算术指令用于对两个操作数栈上的值进行某种特定运算,并把计算结果重新压入操作数栈 +```java +//自定义类加载器,读取指定的类路径classPath下的class文件 +public class MyClassLoader extends ClassLoader{ + private String classPath; -没有直接支持 byte、 short、 char 和 boolean类型的算术指令,对于这些数据的运算,都使用 int 类型的指令来处理,数组类型也是转换成 int 数组 - -* 加法指令:iadd、ladd、fadd、dadd -* 减法指令:isub、lsub、fsub、dsub -* 乘法指令:imu、lmu、fmul、dmul -* 除法指令:idiv、ldiv、fdiv、ddiv -* 求余指令:irem、lrem、frem、drem(remainder 余数) -* 取反指令:ineg、lneg、fneg、dneg (negation 取反) -* 自增指令:iinc(直接**在局部变量 slot 上进行运算**,不用放入操作数栈) -* 位运算指令,又可分为: - - 位移指令:ishl、ishr、 iushr、lshl、lshr、 lushr - - 按位或指令:ior、lor - - 按位与指令:iand、land - - 按位异或指令:ixor、lxor - -* 比较指令:dcmpg、dcmpl、 fcmpg、fcmpl、lcmp - -运算模式: - -* 向最接近数舍入模式,JVM 在进行浮点数计算时,所有的运算结果都必须舍入到适当的精度,非精确结果必须舍入为可被表示的最接近的精确值,如果有两种可表示形式与该值一样接近,将优先选择最低有效位为零的 -* 向零舍入模式:将浮点数转换为整数时,该模式将在目标数值类型中选择一个最接近但是不大于原值的数字作为最精确的舍入结果 - -NaN 值:当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义,将使用 NaN 值来表示 - -```java -double j = i / 0.0; -System.out.println(j);//无穷大,NaN: not a number -``` - -**分析 i++**:从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc - -```java - 4 iload_1 //存入操作数栈 - 5 iinc 1 by 1 //自增i++ - 8 istore_3 //把操作数栈没有自增的数据的存入局部变量表 - 9 iinc 2 by 1 //++i -12 iload_2 //加载到操作数栈 -13 istore 4 //存入局部变量表,这个存入没有 _ 符号,_只能到3 -``` + public MyClassLoader(String classPath) { + this.classPath = classPath; + } + + public MyClassLoader(ClassLoader parent, String byteCodePath) { + super(parent); + this.classPath = classPath; + } -```java -public class Demo { - public static void main(String[] args) { - int a = 10; - int b = a++ + ++a + a--; - System.out.println(a); //11 - System.out.println(b); //34 + @Override + protected Class findClass(String name) throws ClassNotFoundException { + BufferedInputStream bis = null; + ByteArrayOutputStream baos = null; + try { + // 获取字节码文件的完整路径 + String fileName = classPath + className + ".class"; + // 获取一个输入流 + bis = new BufferedInputStream(new FileInputStream(fileName)); + // 获取一个输出流 + baos = new ByteArrayOutputStream(); + // 具体读入数据并写出的过程 + int len; + byte[] data = new byte[1024]; + while ((len = bis.read(data)) != -1) { + baos.write(data, 0, len); + } + // 获取内存中的完整的字节数组的数据 + byte[] byteCodes = baos.toByteArray(); + // 调用 defineClass(),将字节数组的数据转换为 Class 的实例。 + Class clazz = defineClass(null, byteCodes, 0, byteCodes.length); + return clazz; + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (baos != null) + baos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + try { + if (bis != null) + bis.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return null; } } ``` -判断结果: - ```java -public class Demo { - public static void main(String[] args) { - int i = 0; - int x = 0; - while (i < 10) { - x = x++; - i++; - } - System.out.println(x); // 结果是 0 +public static void main(String[] args) { + MyClassLoader loader = new MyClassLoader("D:\Workspace\Project\JVM_study\src\java1\"); + + try { + Class clazz = loader.loadClass("Demo1"); + System.out.println("加载此类的类的加载器为:" + clazz.getClassLoader().getClass().getName());//MyClassLoader + + System.out.println("加载当前类的类的加载器的父类加载器为:" + clazz.getClassLoader().getParent().getClass().getName());//sun.misc.Launcher$AppClassLoader + } catch (ClassNotFoundException e) { + e.printStackTrace(); } } ``` -*** +**** -##### 类型转换 +#### JDK9 -类型转换指令可以将两种不同的数值类型进行相互转换,除了 boolean 之外的七种类型 +为了保证兼容性,JDK9 没有改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行做了一些变动: -宽化类型转换: +* 扩展机制被移除,扩展类加载器由于**向后兼容性**的原因被保留,不过被重命名为平台类加载器(platform classloader),可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取 -* JVM 支持以下数值的宽化类型转换(widening numeric conversion),小范围类型到大范围类型的安全转换 - * 从 int 类型到 long、float 或者 double 类型,对应的指令为 i2l、i2f、i2d - * 从 long 类型到 float、 double 类型,对应的指令为 l2f、l2d - * 从 float 类型到 double 类型,对应的指令为 f2d +* JDK9 基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数个 JMOD 文件),其中 Java 类库就满足了可扩展的需求,那就无须再保留 `\lib\ext` 目录,此前使用这个目录或者 `java.ext.dirs` 系统变量来扩展 JDK 功能的机制就不需要再存在 -* 精度损失问题 - * 宽化类型转换是不会因为超过目标类型最大值而丢失信息 - * 从 int 转换到 float 或者 long 类型转换到 double 时,将可能发生精度丢失 +* 启动类加载器、平台类加载器、应用程序类加载器全都继承于 `jdk.internal.loader.BuiltinClassLoader` -* 从 byte、char 和 short 类型到 int 类型的宽化类型转换实际上是不存在的,JVM 把它们当作 int 处理 + -窄化类型转换: -* Java 虚拟机直接支持以下窄化类型转换: - * 从 int 类型至 byte、 short 或者 char 类型,对应的指令有 i2b、i2c、i2s - * 从 long 类型到 int 类型,对应的指令有 l2i - * 从 float 类型到 int 或者 long 类型,对应的指令有:f2i、f2l - * 从 double 类型到 int、long 或 float 者类型,对应的指令有 d2i、d2、d2f -* 精度损失问题: - * 窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,转换过程可能会导致数值丢失精度 - * 将一个浮点值窄化转换为整数类型 T(T 限于 int 或 long 类型之一)时,将遵循以下转换规则: - - 如果浮点值是 NaN,那转换结果就是 int 或 long 类型的 0 - - 如果浮点值不是无穷大的话,浮点值使用 IEEE 754 的向零舍入模式取整,获得整数值 v,如果 v 在目标类型 T 的表示范围之内,那转换结果就是 v,否则将根据 v 的符号,转换为 T 所能表示的最大或者最小正数 +*** -*** +## 运行机制 -##### 创建访问 +### 执行过程 -创建指令: + Java 文件编译执行的过程: -* 创建类实例指令:new,接收一个操作数指向常量池的索引,表示要创建的类型,执行完成后将对象的引用压入栈 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-Java文件编译执行的过程.png) - ```java - 0: new #2 // class com/jvm/bytecode/Demo - 3: dup - 4: invokespecial #3 // Method "":()V - ``` +- 类加载器:用于装载字节码文件(.class文件) +- 运行时数据区:用于分配存储空间 +- 执行引擎:执行字节码文件或本地方法 +- 垃圾回收器:用于对 JVM 中的垃圾内容进行回收 - **dup 是复制操作数栈栈顶的内容**,需要两份引用原因: - - 一个要配合 invokespecial 调用该对象的构造方法 :()V (会消耗掉栈顶一个引用) - - 一个要配合 astore_1 赋值给局部变量 -* 创建数组的指令:newarray、anewarray、multianewarray +**** - * newarray:创建基本类型数组 - * anewarray:创建引用类型数组 - * multianewarray:创建多维数组 -字段访问指令:对象创建后可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素 -* 访问类字段(static字段,或者称为类变量)的指令:getstatic、putstatic -* 访问类实例字段(非static字段,或者称为实例变量)的指令:getfield、 putfield +### 字节码 -类型检查指令:检查类实例或数组类型的指令 +#### 跨平台性 -* checkcast:用于检查类型强制转换是否可以进行,如果可以进行 checkcast 指令不会改变操作数栈,否则它会抛出 ClassCastException 异常 +Java 语言:跨平台的语言(write once ,run anywhere) -* instanceof:判断给定对象是否是某一个类的实例,会将判断结果压入操作数栈 +* 当 Java 源代码成功编译成字节码后,在不同的平台上面运行**无须再次编译** +* 让一个 Java 程序正确地运行在 JVM 中,Java 源码就必须要被编译为符合 JVM 规范的字节码 +编译过程中的编译器: +* 前端编译器: Sun 的全量式编译器 javac、 Eclipse 的增量式编译器 ECJ,把源代码编译为字节码文件 .class + * IntelliJ IDEA 使用 javac 编译器 + * Eclipse 中,当开发人员编写完代码后保存时,ECJ 编译器就会把未编译部分的源码逐行进行编译,而非每次都全量编译,因此 ECJ 的编译效率会比 javac 更加迅速和高效 + * 前端编译器并不会直接涉及编译优化等方面的技术,具体优化细节移交给 HotSpot 的 JIT 编译器负责 -**** +* 后端运行期编译器:HotSpot VM 的 C1、C2 编译器,也就是 JIT 编译器,Graal 编译器 + * JIT编译器:执行引擎部分详解 + * Graal 编译器:JDK10 HotSpot 加入的一个全新的即时编译器,编译效果短短几年时间就追平了 C2 +* 静态提前编译器:AOT (Ahead Of Time Compiler)编译器,直接把源代码编译成本地机器代码, -##### 方法指令 + * JDK 9 引入,是与即时编译相对立的一个概念,即时编译指的是在程序的运行过程中将字节码转换为机器码,AOT 是程序运行之前便将字节码转换为机器码 + + * 优点:JVM 加载已经预编译成二进制库,可以直接执行,不必等待即时编译器的预热,减少 Java 应用第一次运行慢的现象 + * 缺点: + * 破坏了 Java **一次编译,到处运行**,必须为每个不同硬件编译对应的发行包 + * 降低了 Java 链接过程的动态性,加载的代码在编译期就必须全部已知 -方法调用指令:invokevirtual、 invokeinterface、invokespecial、invokestatic、invokedynamic -**方法调用章节详解** @@ -13273,140 +13333,101 @@ public class Demo { -##### 操作数栈 +#### 语言发展 -JVM 提供的操作数栈管理指令,可以用于直接操作操作数栈的指令 +机器码:各种用二进制编码方式表示的指令,与 CPU 紧密相关,所以不同种类的 CPU 对应的机器指令不同 -* pop、pop2:将一个或两个元素从栈顶弹出,并且直接废弃 -* dup、dup2,dup_x1、dup2_x1,dup_x2、dup2_x2:复制栈顶一个或两个数值并重新压入栈顶 +指令:指令就是把机器码中特定的 0 和 1 序列,简化成对应的指令,例如 mov,inc 等,可读性稍好,但是不同的硬件平台的同一种指令(比如 mov),对应的机器码也可能不同 -* swap:将栈最顶端的两个 slot 数值位置交换,JVM 没有提供交换两个 64 位数据类型数值的指令 +指令集:不同的硬件平台支持的指令是有区别的,每个平台所支持的指令,称之为对应平台的指令集 -* nop:一个非常特殊的指令,字节码为 0x00,和汇编语言中的 nop 一样,表示什么都不做,一般可用于调试、占位等 +- x86 指令集,对应的是 x86 架构的平台 +- ARM 指令集,对应的是 ARM 架构的平台 +汇编语言:用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址 +* 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令 +* 计算机只认识指令码,汇编语言编写的程序也必须翻译成机器指令码,计算机才能识别和执行 -*** +高级语言:为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言 +字节码:是一种中间状态(中间码)的二进制代码,比机器码更抽象,需要直译器转译后才能成为机器码 +* 字节码为了实现特定软件运行和软件环境,与硬件环境无关 +* 通过编译器和虚拟机器实现,编译器将源码编译成字节码,虚拟机器将字节码转译为可以直接执行的指令 -##### 控制转移 + -比较指令:比较栈顶两个元素的大小,并将比较结果入栈 -* lcmp:比较两个 long 类型值 -* fcmpl:比较两个 float 类型值(当遇到NaN时,返回-1) -* fcmpg:比较两个 float 类型值(当遇到NaN时,返回1) -* dcmpl:比较两个 double 类型值(当遇到NaN时,返回-1) -* dcmpg:比较两个 double 类型值(当遇到NaN时,返回1) +*** -条件跳转指令: -| 指令 | 说明 | -| --------- | -------------------------------------------------- | -| ifeq | equals,当栈顶int类型数值等于0时跳转 | -| ifne | not equals,当栈顶in类型数值不等于0时跳转 | -| iflt | lower than,当栈顶in类型数值小于0时跳转 | -| ifle | lower or equals,当栈顶in类型数值小于等于0时跳转 | -| ifgt | greater than,当栈顶int类型数组大于0时跳转 | -| ifge | greater or equals,当栈顶in类型数值大于等于0时跳转 | -| ifnull | 为 null 时跳转 | -| ifnonnull | 不为 null 时跳转 | -比较条件跳转指令: -| 指令 | 说明 | -| --------- | --------------------------------------------------------- | -| if_icmpeq | 比较栈顶两 int 类型数值大小(下同),当前者等于后者时跳转 | -| if_icmpne | 当前者不等于后者时跳转 | -| if_icmplt | 当前者小于后者时跳转 | -| if_icmple | 当前者小于等于后者时跳转 | -| if_icmpgt | 当前者大于后者时跳转 | -| if_icmpge | 当前者大于等于后者时跳转 | -| if_acmpeq | 当结果相等时跳转 | -| if_acmpne | 当结果不相等时跳转 | -多条件分支跳转指令: +#### 类结构 -* tableswitch:用于 switch 条件跳转,case 值连续 -* lookupswitch:用于 switch 条件跳转,case 值不连续 - -无条件跳转指令: - -* goto:用来进行跳转到指定行号的字节码 - -* goto_w:无条件跳转(宽索引) - - - - - -*** +##### 文件结构 +字节码是一种二进制的类文件,是编译之后供虚拟机解释执行的二进制字节码文件,**一个 class 文件对应一个 public 类型的类或接口** +字节码内容是 **JVM 的字节码指令**,不是机器码,C、C++ 经由编译器直接生成机器码,所以执行效率比 Java 高 -##### 异常处理 +JVM 官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html -###### 处理机制 +根据 JVM 规范,类文件结构如下: -抛出异常指令:athrow 指令 +```java +ClassFile { + u4 magic; + u2 minor_version; + u2 major_version; + u2 constant_pool_count; + cp_info constant_pool[constant_pool_count-1]; + u2 access_flags; + u2 this_class; + u2 super_class; + u2 interfaces_count; + u2 interfaces[interfaces_count]; + u2 fields_count; + field_info fields[fields_count]; + u2 methods_count; + method_info methods[methods_count]; + u2 attributes_count; + attribute_info attributes[attributes_count]; +} +``` -JVM 处理异常(catch 语句)不是由字节码指令来实现的,而是**采用异常表来完成**的 +| 类型 | 名称 | 说明 | 长度 | 数量 | +| -------------- | ------------------- | -------------------- | ------- | --------------------- | +| u4 | magic | 魔数,识别类文件格式 | 4个字节 | 1 | +| u2 | minor_version | 副版本号(小版本) | 2个字节 | 1 | +| u2 | major_version | 主版本号(大版本) | 2个字节 | 1 | +| u2 | constant_pool_count | 常量池计数器 | 2个字节 | 1 | +| cp_info | constant_pool | 常量池表 | n个字节 | constant_pool_count-1 | +| u2 | access_flags | 访问标识 | 2个字节 | 1 | +| u2 | this_class | 类索引 | 2个字节 | 1 | +| u2 | super_class | 父类索引 | 2个字节 | 1 | +| u2 | interfaces_count | 接口计数 | 2个字节 | 1 | +| u2 | interfaces | 接口索引集合 | 2个字节 | interfaces_count | +| u2 | fields_count | 字段计数器 | 2个字节 | 1 | +| field_info | fields | 字段表 | n个字节 | fields_count | +| u2 | methods_count | 方法计数器 | 2个字节 | 1 | +| method_info | methods | 方法表 | n个字节 | methods_count | +| u2 | attributes_count | 属性计数器 | 2个字节 | 1 | +| attribute_info | attributes | 属性表 | n个字节 | attributes_count | -* 代码: +Class 文件格式采用一种类似于 C 语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表 - ```java - public static void main(String[] args) { - int i = 0; - try { - i = 10; - } catch (Exception e) { - i = 20; - } finally { - i = 30; - } - } - ``` - -* 字节码: +* 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串 +* 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,表都以 `_info` 结尾,用于描述有层次关系的数据,整个 Class 文件本质上就是一张表,由于表没有固定长度,所以通常会在其前面加上个数说明 - * 多出一个 **Exception table** 的结构,**[from, to) 是前闭后开的检测范围**,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号 - * 11 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置,因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用 +获取方式: - ```java - 0: iconst_0 - 1: istore_1 // 0 -> i ->赋值 - 2: bipush 10 // try 10 放入操作数栈顶 - 4: istore_1 // 10 -> i 将操作数栈顶数据弹出,存入局部变量表的 slot1 - 5: bipush 30 // 【finally】 - 7: istore_1 // 30 -> i - 8: goto 27 // return ----------------------------------- - 11: astore_2 // catch Exceptin -> e ---------------------- - 12: bipush 20 // - 14: istore_1 // 20 -> i - 15: bipush 30 // 【finally】 - 17: istore_1 // 30 -> i - 18: goto 27 // return ----------------------------------- - 21: astore_3 // catch any -> slot 3 ---------------------- - 22: bipush 30 // 【finally】 - 24: istore_1 // 30 -> i - 25: aload_3 // 将局部变量表的slot 3数据弹出,放入操作数栈栈顶 - 26: athrow // throw 抛出异常 - 27: return - Exception table: - // 任何阶段出现任务异常都会执行 finally - from to target type - 2 5 11 Class java/lang/Exception - 2 5 21 any // 剩余的异常类型,比如 Error - 11 15 21 any // 剩余的异常类型,比如 Error - LineNumberTable: ... - LocalVariableTable: - Start Length Slot Name Signature - 12 3 2 e Ljava/lang/Exception; - 0 28 0 args [Ljava/lang/String; - 2 26 1 i I - ``` +* HelloWorld.java 执行 `javac -parameters -d . HellowWorld.java`指令 +* 写入文件指令 `javap -v xxx.class >xxx.txt` +* IDEA 插件 jclasslib @@ -13414,418 +13435,482 @@ JVM 处理异常(catch 语句)不是由字节码指令来实现的,而是* -###### finally - -finally 中的代码被**复制了 3 份**,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程(上节案例) - -* 代码: - - ```java - public static int test() { - try { - return 10; - } finally { - return 20; - } - } - ``` +##### 魔数版本 -* 字节码: +魔数:每个 Class 文件开头的 4 个字节的无符号整数称为魔数(Magic Number),是 Class 文件的标识符,代表这是一个能被虚拟机接受的有效合法的 Class 文件, - ```java - 0: bipush 10 // 10 放入栈顶 - 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 - 3: bipush 20 // 20 放入栈顶 - 5: ireturn // 返回栈顶 int(20) - 6: astore_1 // catch any 存入局部变量表的 slot1 - 7: bipush 20 // 20 放入栈顶 - 9: ireturn // 返回栈顶 int(20) - Exception table: - from to target type - 0 3 6 any - ``` +* 魔数值固定为 0xCAFEBABE,不符合则会抛出错误 +* 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动 +版本:4 个 字节,5 6两个字节代表的是编译的副版本号 minor_version,而 7 8 两个字节是编译的主版本号 major_version -*** +* 不同版本的 Java 编译器编译的 Class 文件对应的版本是不一样的,高版本的 Java 虚拟机可以执行由低版本编译器生成的 Class 文件,反之 JVM 会抛出异常 `java.lang.UnsupportedClassVersionError` +| 主版本(十进制) | 副版本(十进制) | 编译器版本 | +| ---------------- | ---------------- | ---------- | +| 45 | 3 | 1.1 | +| 46 | 0 | 1.2 | +| 47 | 0 | 1.3 | +| 48 | 0 | 1.4 | +| 49 | 0 | 1.5 | +| 50 | 0 | 1.6 | +| 51 | 0 | 1.7 | +| 52 | 0 | 1.8 | +| 53 | 0 | 1.9 | +| 54 | 0 | 1.10 | +| 55 | 0 | 1.11 | +![](https://gitee.com/seazean/images/raw/master/Java/JVM-类结构.png) -###### return -* 吞异常 - ```java - public static int test() { - try { - return 10; - } finally { - return 20; - } - } - ``` +图片来源:https://www.bilibili.com/video/BV1PJ411n7xZ - ```java - 0: bipush 10 // 10 放入栈顶 - 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 - 3: bipush 20 // 20 放入栈顶 - 5: ireturn // 返回栈顶 int(20) - 6: astore_1 // catch any 存入局部变量表的 slot1 - 7: bipush 20 // 20 放入栈顶 - 9: ireturn // 返回栈顶 int(20) - Exception table: - from to target type - 0 3 6 any - ``` - * 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果以 finally 的为准 - * 字节码中没有 **athrow** ,表明如果在 finally 中出现了 return,会**吞掉异常** -* 不吞异常 +*** - ```java - public class Demo { - public static void main(String[] args) { - int result = test(); - System.out.println(result);//10 - } - public static int test() { - int i = 10; - try { - return i;//返回10 - } finally { - i = 20; - } - } - } - ``` - - ```java - 0: bipush 10 // 10 放入栈顶 - 2: istore_0 // 10 赋值给i,放入slot 0 - 3: iload_0 // i(10)加载至操作数栈 - 4: istore_1 // 10 -> slot 1,【暂存至 slot 1,目的是为了固定返回值】 - 5: bipush 20 // 20 放入栈顶 - 7: istore_0 // 20 slot 0 - 8: iload_1 // slot 1(10) 载入 slot 1 暂存的值 - 9: ireturn // 返回栈顶的 int(10) - 10: astore_2 // catch any -> slot 2 存入局部变量表的 slot2 - 11: bipush 20 - 13: istore_0 - 14: aload_2 - 15: athrow // 不会吞掉异常 - Exception table: - from to target type - 3 5 10 any - ``` +##### 常量池 -*** +常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的无符号数,代表常量池计数器(constant_pool_count),这个容量计数是从 1 而不是 0 开始,是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达不引用任何一个常量池项目,这种情况可用索引值 0 来表示 +constant_pool 是一种表结构,以1 ~ constant_pool_count - 1为索引,表明有多少个常量池表项。表项中存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池 +* 字面量(Literal) :基本数据类型、字符串类型常量、声明为 final 的常量值等 -##### 同步控制 +* 符号引用(Symbolic References):类和接口的全限定名、字段的名称和描述符、方法的名称和描述符 -方法级的同步:是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中,虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法 + * 全限定名:com/test/Demo 这个就是类的全限定名,仅仅是把包名的 `.` 替换成 `/`,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个 `;` 表示全限定名结束 -方法内指定指令序列的同步:有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义 + * 简单名称:指没有类型和参数修饰的方法或者字段名称,比如字段 x 的简单名称就是 x -* montiorenter:进入并获取对象监视器,即为栈顶对象加锁 -* monitorexit:释放并退出对象监视器,即为栈顶对象解锁 + * 描述符:用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值 - + | 标志符 | 含义 | + | ------ | --------------------------------------------------------- | + | B | 基本数据类型 byte | + | C | 基本数据类型 char | + | D | 基本数据类型 double | + | F | 基本数据类型 float | + | I | 基本数据类型 int | + | J | 基本数据类型 long | + | S | 基本数据类型 short | + | Z | 基本数据类型 boolean | + | V | 代表 void 类型 | + | L | 对象类型,比如:`Ljava/lang/Object;`,不同方法间用`;`隔开 | + | [ | 数组类型,代表一维数组。比如:`double[][][] is [[[D` | +常量类型和结构: +| 类型 | 标志(或标识) | 描述 | +| -------------------------------- | ------------ | ---------------------- | +| CONSTANT_utf8_info | 1 | UTF-8编码的字符串 | +| CONSTANT_Integer_info | 3 | 整型字面量 | +| CONSTANT_Float_info | 4 | 浮点型字面量 | +| CONSTANT_Long_info | 5 | 长整型字面量 | +| CONSTANT_Double_info | 6 | 双精度浮点型字面量 | +| CONSTANT_Class_info | 7 | 类或接口的符号引用 | +| CONSTANT_String_info | 8 | 字符串类型字面量 | +| CONSTANT_Fieldref_info | 9 | 字段的符号引用 | +| CONSTANT_Methodref_info | 10 | 类中方法的符号引用 | +| CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 | +| CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 | +| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 | +| CONSTANT_MethodType_info | 16 | 标志方法类型 | +| CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 | +18 种常量没有出现 byte、short、char,boolean 的原因:编译之后都可以理解为 Integer -*** +**** -#### 执行流程 -原始 Java 代码: +##### 访问标识 -```java -public class Demo { - public static void main(String[] args) { - int a = 10; - int b = Short.MAX_VALUE + 1; - int c = a + b; - System.out.println(c); - } -} -``` +访问标识(access_flag),又叫访问标志、访问标记,该标识用两个字节表示,用于识别一些类或者接口层次的访问信息,包括这个 Class 是类还是接口,是否定义为 public类型,是否定义为 abstract类型等 -javap -v Demo.class:省略 +* 类的访问权限通常为 ACC_ 开头的常量 +* 每一种类型的表示都是通过设置访问标记的 32 位中的特定位来实现的,比如若是 public final 的类,则该标记为 `ACC_PUBLIC | ACC_FINAL` +* 使用 `ACC_SUPER` 可以让类更准确地定位到父类的方法,确定类或接口里面的 invokespecial 指令使用的是哪一种执行语义,现代编译器都会设置并且使用这个标记 -* 常量池载入运行时常量池 +| 标志名称 | 标志值 | 含义 | +| -------------- | ------ | ------------------------------------------------------------ | +| ACC_PUBLIC | 0x0001 | 标志为 public 类型 | +| ACC_FINAL | 0x0010 | 标志被声明为 final,只有类可以设置 | +| ACC_SUPER | 0x0020 | 标志允许使用 invokespecial 字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真,使用增强的方法调用父类方法 | +| ACC_INTERFACE | 0x0200 | 标志这是一个接口 | +| ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 | +| ACC_SYNTHETIC | 0x1000 | 标志此类并非由用户代码产生(由编译器产生的类,没有源码对应) | +| ACC_ANNOTATION | 0x2000 | 标志这是一个注解 | +| ACC_ENUM | 0x4000 | 标志这是一个枚举 | -* 方法区字节码载入方法区 -* main 线程开始运行,分配栈帧内存:(操作数栈stack=2,局部变量表locals=4) -* **执行引擎**开始执行字节码 +*** - `bipush 10`:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令 - * sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节) - * ldc 将一个 int 压入操作数栈 - * ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节) - * 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池 - `istore_1`:将操作数栈顶数据弹出,存入局部变量表的 slot 1 +##### 索引集合 - ![](https://gitee.com/seazean/images/raw/master/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) +* 父类索引用于确定这个类的父类的全限定名,Java 语言不允许多重继承,所以父类索引只有一个,除了Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为0 - `istore_2`:将操作数栈顶数据弹出,存入局部变量表的 slot 2 +* 接口索引集合就用来描述这个类实现了哪些接口 + * interfaces_count 项的值表示当前类或接口的直接超接口数量 + * interfaces[] 接口索引集合,被实现的接口将按 implements 语句后的接口顺序从左到右排列在接口索引集合中 - `iload_1`:将局部变量表的 slot 1数据弹出,放入操作数栈栈顶 +| 长度 | 含义 | +| ---- | ---------------------------- | +| u2 | this_class | +| u2 | super_class | +| u2 | interfaces_count | +| u2 | interfaces[interfaces_count] | - `iload_2`:将局部变量表的 slot 2数据弹出,放入操作数栈栈顶 - `iadd`:执行相加操作 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程3.png) +*** - `istore_3`:将操作数栈顶数据弹出,存入局部变量表的 slot 3 - `getstatic #4`:获取静态字段 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程4.png) +##### 字段表 - `iload_3`: +字段 fields 用于描述接口或类中声明的变量,包括类变量以及实例变量,但不包括方法内部、代码块内部声明的局部变量以及从父类或父接口继承。字段叫什么名字、被定义为什么数据类型,都是无法固定的,只能引用常量池中的常量来描述 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程5.png) +fields_count(字段计数器),表示当前 class 文件 fields 表的成员个数,用两个字节来表示 - `invokevirtual #5`: +fields[](字段表): - * 找到常量池 #5 项 - * 定位到方法区 java/io/PrintStream.println:(I)V 方法 - * **生成新的栈帧**(分配 locals、stack等) - * 传递参数,执行新栈帧中的字节码 - * 执行完毕,弹出栈帧 - * 清除 main 操作数栈内容 +* 表中的每个成员都是一个 fields_info 结构的数据项,用于表示当前类或接口中某个字段的完整描述 - ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程6.png) +* 字段访问标识: - return:完成 main 方法调用,弹出 main 栈帧,程序结束 + | 标志名称 | 标志值 | 含义 | + | ------------- | ------ | -------------------------- | + | ACC_PUBLIC | 0x0001 | 字段是否为public | + | ACC_PRIVATE | 0x0002 | 字段是否为private | + | ACC_PROTECTED | 0x0004 | 字段是否为protected | + | ACC_STATIC | 0x0008 | 字段是否为static | + | ACC_FINAL | 0x0010 | 字段是否为final | + | ACC_VOLATILE | 0x0040 | 字段是否为volatile | + | ACC_TRANSTENT | 0x0080 | 字段是否为transient | + | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | + | ACC_ENUM | 0x4000 | 字段是否为enum | - +* 字段名索引:根据该值查询常量池中的指定索引项即可 +* 描述符索引:用来描述字段的数据类型、方法的参数列表和返回值 + | 字符 | 类型 | 含义 | + | ----------- | --------- | ----------------------- | + | B | byte | 有符号字节型树 | + | C | char | Unicode字符,UTF-16编码 | + | D | double | 双精度浮点数 | + | F | float | 单精度浮点数 | + | I | int | 整型数 | + | J | long | 长整数 | + | S | short | 有符号短整数 | + | Z | boolean | 布尔值true/false | + | V | void | 代表void类型 | + | L Classname | reference | 一个名为Classname的实例 | + | [ | reference | 一个一维数组 | -*** +* 属性表集合:属性个数存放在 attribute_count 中,属性具体内容存放在 attribute 数组中,一个字段还可能拥有一些属性,用于存储更多的额外信息,比如初始化值、一些注释信息等 + ```java + ConstantValue_attribute{ + u2 attribute_name_index; + u4 attribute_length; + u2 constantvalue_index; + } + ``` + 对于常量属性而言,attribute_length 值恒为2 -### 执行引擎 -#### 基本介绍 -执行引擎:Java 虚拟机的核心组成部分之一,类加载主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,需要执行引擎将字节码指令解释/编译为对应平台上的本地机器指令 +*** -虚拟机是一个相对于物理机的概念,这两种机器都有代码执行能力: -* 物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上 -* 虚拟机的执行引擎是由软件自行实现的,可以不受物理条件制约地定制指令集与执行引擎的结构体系 -Java 是**半编译半解释型语言**,将解释执行与编译执行二者结合起来进行: +##### 方法表 -* 解释器:根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令执行 -* 即时编译器(JIT : Just In Time Compiler):虚拟机运行时将源代码直接编译成**和本地机器平台相关的机器码**后再执行,并存入 Code Cache,下次遇到相同的代码直接执行,效率高 +方法表是 methods 指向常量池索引集合,其中每一个 method_info 项都对应着一个类或者接口中的方法信息,完整描述了每个方法的签名 +* 如果这个方法不是抽象的或者不是 native 的,字节码中就会体现出来 +* methods 表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法 +* methods 表可能会出现由编译器自动添加的方法,比如初始化方法 和实例化方法 +**重载(Overload)**一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,因为返回值不会包含在特征签名之中,因此 Java 语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在 Class 文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存 -*** +methods_count(方法计数器):表示 class 文件 methods 表的成员个数,使用两个字节来表示 +methods[](方法表):每个表项都是一个 method_info 结构,表示当前类或接口中某个方法的完整描述 +* 方法表结构如下: -#### 执行方式 + | 类型 | 名称 | 含义 | 数量 | + | -------------- | ---------------- | ---------- | ---------------- | + | u2 | access_flags | 访问标志 | 1 | + | u2 | name_index | 字段名索引 | 1 | + | u2 | descriptor_index | 描述符索引 | 1 | + | u2 | attrubutes_count | 属性计数器 | 1 | + | attribute_info | attributes | 属性集合 | attributes_count | -HotSpot VM 采用**解释器与即时编译器并存的架构**,解释器和即时编译器能够相互协作,去选择最合适的方式来权衡编译本地代码和直接解释执行代码的时间 +* 方法表访问标志: -HostSpot JVM 的默认执行方式: + | 标志名称 | 标志值 | 含义 | + | ------------- | ------ | -------------------------- | + | ACC_PUBLIC | 0x0001 | 字段是否为 public | + | ACC_PRIVATE | 0x0002 | 字段是否为 private | + | ACC_PROTECTED | 0x0004 | 字段是否为 protected | + | ACC_STATIC | 0x0008 | 字段是否为 static | + | ACC_FINAL | 0x0010 | 字段是否为 final | + | ACC_VOLATILE | 0x0040 | 字段是否为 volatile | + | ACC_TRANSTENT | 0x0080 | 字段是否为 transient | + | ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 | + | ACC_ENUM | 0x4000 | 字段是否为 enum | -* 当程序启动后,解释器可以马上发挥作用立即执行,省去编译器编译的时间(解释器存在的**必要性**) -* 随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率 -HotSpot VM 可以通过 VM 参数设置程序执行方式: -- -Xint:完全采用解释器模式执行程序 -- -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行 -- -Xmixed:采用解释器 + 即时编译器的混合模式共同执行程序 +*** -![](https://gitee.com/seazean/images/raw/master/Java/JVM-执行引擎工作流程.png) +##### 属性表 -*** +属性表集合,指的是 Class 文件所携带的辅助信息,比如该 Class 文件的源文件的名称,以及任何带有 `RetentionPolicy.CLASS` 或者 `RetentionPolicy.RUNTIME` 的注解,这类信息通常被用于 Java 虚拟机的验证和运行,以及 Java 程序的调试。字段表、方法表都可以有自己的属性表,用于描述某些场景专有的信息 +attributes_ count(属性计数器):表示当前文件属性表的成员个数 +attributes[](属性表):属性表的每个项的值必须是 attribute_info 结构 -#### 热点探测 +* 属性的通用格式: -热点代码:被 JIT 编译器编译的字节码,根据代码被调用执行的频率而定,一个被多次调用的方法或者一个循环次数较多的循环体都可以被称之为热点代码 + ```java + ConstantValue_attribute{ + u2 attribute_name_index; //属性名索引 + u4 attribute_length; //属性长度 + u2 attribute_info; //属性表 + } + ``` -热点探测:JIT 编译器在运行时会针热点代码做出深度优化,将其直接编译为对应平台的本地机器指令进行缓存,以提升程序执行性能 +* 属性类型: -JIT 编译在默认情况是异步进行的,当触发某方法或某代码块的优化时,先将其放入编译队列,然后由编译线程进行编译,编译之后的代码放在 CodeCache 中,通过 `-XX:-BackgroundCompilation` 参数可以关闭异步编译 + | 属性名称 | 使用位置 | 含义 | + | ------------------------------------- | ------------------ | ------------------------------------------------------------ | + | Code | 方法表 | Java 代码编译成的字节码指令 | + | ConstantValue | 字段表 | final 关键字定义的常量池 | + | Deprecated | 类、方法、字段表 | 被声明为 deprecated 的方法和字段 | + | Exceptions | 方法表 | 方法抛出的异常 | + | EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 | + | InnerClass | 类文件 | 内部类列表 | + | LineNumberTable | Code 属性 | Java 源码的行号与字节码指令的对应关系 | + | LocalVariableTable | Code 属性 | 方法的局部变量描述 | + | StackMapTable | Code 属性 | JDK1.6 中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 | + | Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 | + | SourceFile | 类文件 | 记录源文件名称 | + | SourceDebugExtension | 类文件 | 用于存储额外的调试信息 | + | Syothetic | 类,方法表,字段表 | 标志方法或字段为编泽器自动生成的 | + | LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 | + | RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 | + | RuntimelnvisibleAnnotations | 类,方法表,字段表 | 用于指明哪些注解是运行时不可见的 | + | RuntimeVisibleParameterAnnotation | 方法表 | 作用与 RuntimeVisibleAnnotations 属性类似,只不过作用对象为方法 | + | RuntirmelnvisibleParameterAnniotation | 方法表 | 作用与 RuntimelnvisibleAnnotations 属性类似,作用对象哪个为方法参数 | + | AnnotationDefauit | 方法表 | 用于记录注解类元素的默认值 | + | BootstrapMethods | 类文件 | 用于保存 invokeddynanic 指令引用的引导方式限定符 | -* CodeCache 用于缓存编译后的机器码、动态生成的代码和本地方法代码 JNI -* 如果 CodeCache 区域被占满,编译器被停用,字节码将不会编译为机器码,应用程序继续运行,但运行性能会降低很多 -HotSpot VM 采用的热点探测方式是基于计数器的热点探测,为每一个方法都建立 2 个不同类型的计数器:方法调用计数器(Invocation Counter)和回边计数器(BackEdge Counter) -* 方法调用计数器:用于统计方法被调用的次数,默认阈值在 Client 模式 下是 1500 次,在 Server 模式下是 10000 次(需要进行激进的优化),超过这个阈值,就会触发 JIT 编译,阈值可以通过虚拟机参数 `-XX:CompileThreshold` 设置 - 工作流程:当一个方法被调用时, 会先检查该方法是否存在被 JIT 编译过的版本,存在则使用编译后的本地代码来执行;如果不存在则将此方法的调用计数器值加 1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值,如果超过阈值会向即时编译器**提交一个该方法的代码编译请求** -* 回边计数器:统计一个方法中循环体代码执行的次数,在字节码中控制流向后跳转的指令称为回边 +**** - 如果一个方法中的循环体需要执行多次,可以优化为为栈上替换,简称 OSR (On StackReplacement) 编译,**OSR 替换循环代码体的入口,C1、C2 替换的是方法调用的入口**,OSR 编译后会出现方法的整段代码被编译了,但是只有循环体部分才执行编译后的机器码,其他部分仍是解释执行 +#### 编译指令 -*** +##### javac +javac:编译命令,将 java 源文件编译成 class 字节码文件 +`javac xx.java` 不会在生成对应的局部变量表等信息,使用 `javac -g xx.java` 可以生成所有相关信息 -#### 分层编译 -HotSpot VM 内嵌两个 JIT 编译器,分别为 Client Compiler 和 Server Compiler,简称 C1 编译器和 C2 编译器 -C1 编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度,C1 编译器的优化方法: +**** -* 方法内联:**将调用的函数代码编译到调用点处**,这样可以减少栈帧的生成,减少参数传递以及跳转过程 - 方法内联能够消除方法调用的固定开销,任何方法除非被内联,否则调用都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。 - ```java - private static int square(final int i) { - return i * i; - } - System.out.println(square(9)); - ``` +##### javap - square 是热点方法,会进行内联,把方法内代码拷贝粘贴到调用者的位置: +javap 反编译生成的字节码文件,根据 class 字节码文件,反解析出当前类对应的 code 区 (字节码指令)、局部变量表、异常表和代码行偏移量映射表、常量池等信息 - ```java - System.out.println(9 * 9); - ``` +用法:javap - 还能够进行常量折叠(constant folding)的优化: +```sh +-help --help -? 输出此用法消息 +-version 版本信息 +-public 仅显示公共类和成员 +-protected 显示受保护的/公共类和成员 +-package 显示程序包/受保护的/公共类和成员 (默认) +-p -private 显示所有类和成员 + #常用的以下三个 +-v -verbose 输出附加信息 +-l 输出行号和本地变量表 +-c 对代码进行反汇编 #反编译 - ```java - System.out.println(81); - ``` +-s 输出内部类型签名 +-sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列) +-constants 显示最终常量 +-classpath 指定查找用户类文件的位置 +-cp 指定查找用户类文件的位置 +-bootclasspath 覆盖引导类文件的位置 +``` -* 冗余消除:根据运行时状况进行代码折叠或削除 -* 内联缓存:是一种加快动态绑定的优化技术(方法调用部分详解) -C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,当激进优化的假设不成立时,再退回使用 C1 编译,这也是使用分层编译的原因 +*** -C2 的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 -VM 参数设置: -- -client:指定 Java 虚拟机运行在 Client 模式下,并使用 C1 编译器 -- -server:指定 Java 虚拟机运行在 Server 模式下,并使用 C2 编译器 -- `-server -XX:+TieredCompilation`:在 1.8 之前,分层编译默认是关闭的,可以添加该参数开启 +#### 指令集 -分层编译策略 (Tiered Compilation):程序解释执行可以触发 C1 编译,将字节码编译成机器码,加上性能监控,C2 编译会根据性能监控信息进行激进优化,JVM 将执行状态分成了 5 个层次: +##### 执行指令 -* 0 层,解释执行(Interpreter) +Java 字节码属于 JVM 基本执行指令。由一个字节长度的代表某种操作的操作码(opcode)以及零至多个代表此操作所需参数的操作数(operand)所构成,虚拟机中许多指令并不包含操作数,只有一个操作码(零地址指令) -* 1 层,使用 C1 即时编译器编译执行(不带 profiling) +由于限制了 Java 虚拟机操作码的长度为一个字节(0~255),所以指令集的操作码总数不可能超过 256 条 -* 2 层,使用 C1 即时编译器编译执行(带基本的 profiling) +在 JVM 的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如 iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中,而 fload 指令加载的则是 float 类型的数据 -* 3 层,使用 C1 即时编译器编译执行(带完全的 profiling) +* i 代表对 int 类型的数据操作 +* l 代表 long +* s 代表 short +* b 代表 byte +* c 代表 char +* f 代表 float +* d 代表 double -* 4 层,使用 C2 即时编译器编译执行(C1 和 C2 协作运行) +大部分的指令都没有支持 byte、char、short、boolean 类型,编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展(Sign-Extend-)为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展(Zero-Extend-)为相应的 int 类型数据 - 说明:profiling 是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等 +在做值相关操作时: +- 一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是值,也可能是对象的引用)被压入操作数栈 +- 一个指令,也可以从操作数栈中取出一到多个值(pop 多次),完成赋值、加减乘除、方法传参、系统调用等等操作 -参考文章:https://www.jianshu.com/p/20bd2e9b1f03 +*** -*** +##### 加载存储 +加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递 -### 方法调用 +局部变量压栈指令:将给定的局部变量表中的数据压入操作数栈 -#### 方法识别 +* xload、xload_n,x 表示取值数据类型,为 i、l、f、d、a, n 为 0 到 3 +* 指令 xload_n 表示将第 n 个局部变量压入操作数栈,aload_n 表示将一个对象引用压栈 +* 指令 xload n 通过指定参数的形式,把局部变量压入操作数栈,局部变量数量超过 4 个时使用这个命令 -Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descriptor) +常量入栈指令:将常数压入操作数栈,根据数据类型和入栈内容的不同,又分为 const、push、ldc指令 -* **方法描述符是由方法的参数类型以及返回类型所构成**,Java 层面叫方法特征签名 -* 在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么 Java 虚拟机会在类的验证阶段报错 +* push:包括 bipush 和 sipush,区别在于接收数据类型的不同,bipush 接收 8 位整数作为参数,sipush 接收 16 位整数 +* ldc:如果以上指令不能满足需求,可以使用 ldc 指令,接收一个 8 位的参数,该参数指向常量池中的 int、 float 或者 String 的索引,将指定的内容压入堆栈。ldc_w 接收两个 8 位参数,能支持的索引范围更大,如果要压入的元素是 long 或 double 类型的,则使用 ldc2_w 指令 +* aconst_null 将 null 对象引用压入栈,iconst_m1 将 int 类型常量 -1 压入栈,iconst_0 将 int 类型常量 0 压入栈 -JVM 根据名字和描述符来判断的,只要返回值不一样(方法描述符不一样),其它完全一样,在 JVM 中是允许的,但 Java 语言不允许 +出栈装入局部变量表指令:将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值 -```java -// 返回值类型不同,编译阶段直接报错 -public static Integer invoke(Object... args) { - return 1; -} -public static int invoke(Object... args) { - return 2; -} -``` +* xstore、xstore_n,x 表示取值类型为 i、l、f、d、a, n 为 0 到 3 +* xastore 表示存入数组,x 取值为 i、l、f、d、a、b、c、s +扩充局部变量表的访问索引的指令:wide -*** +**** -#### 调用机制 -方法调用并不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,不是方法的具体运行过程 +##### 算术指令 -在 JVM 中,将符号引用转换为直接引用有两种机制: +算术指令用于对两个操作数栈上的值进行某种特定运算,并把计算结果重新压入操作数栈 -- 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接(类加载的解析阶段) -- 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) +没有直接支持 byte、 short、 char 和 boolean类型的算术指令,对于这些数据的运算,都使用 int 类型的指令来处理,数组类型也是转换成 int 数组 -对应方法的绑定(分配)机制:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次: +* 加法指令:iadd、ladd、fadd、dadd +* 减法指令:isub、lsub、fsub、dsub +* 乘法指令:imu、lmu、fmul、dmul +* 除法指令:idiv、ldiv、fdiv、ddiv +* 求余指令:irem、lrem、frem、drem(remainder 余数) +* 取反指令:ineg、lneg、fneg、dneg (negation 取反) +* 自增指令:iinc(直接**在局部变量 slot 上进行运算**,不用放入操作数栈) +* 位运算指令,又可分为: + - 位移指令:ishl、ishr、 iushr、lshl、lshr、 lushr + - 按位或指令:ior、lor + - 按位与指令:iand、land + - 按位异或指令:ixor、lxor -- 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,将这个方法与所属的类型进行绑定 -- 动态绑定:被调用的目标方法在编译期无法确定,只能在程序运行期根据实际的类型绑定相关的方法 +* 比较指令:dcmpg、dcmpl、 fcmpg、fcmpl、lcmp -* Java 编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载 +运算模式: -非虚方法: +* 向最接近数舍入模式,JVM 在进行浮点数计算时,所有的运算结果都必须舍入到适当的精度,非精确结果必须舍入为可被表示的最接近的精确值,如果有两种可表示形式与该值一样接近,将优先选择最低有效位为零的 +* 向零舍入模式:将浮点数转换为整数时,该模式将在目标数值类型中选择一个最接近但是不大于原值的数字作为最精确的舍入结果 -- 非虚方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的 -- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法 -- 所有普通成员方法、实例方法、被重写的方法都是虚方法 +NaN 值:当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义,将使用 NaN 值来表示 -动态类型语言和静态类型语言: +```java +double j = i / 0.0; +System.out.println(j);//无穷大,NaN: not a number +``` -- 在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言 +**分析 i++**:从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc -- 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息 +```java + 4 iload_1 //存入操作数栈 + 5 iinc 1 by 1 //自增i++ + 8 istore_3 //把操作数栈没有自增的数据的存入局部变量表 + 9 iinc 2 by 1 //++i +12 iload_2 //加载到操作数栈 +13 istore 4 //存入局部变量表,这个存入没有 _ 符号,_只能到3 +``` -- **Java 是静态类型语言**(尽管 Lambda 表达式为其增加了动态特性),JS,Python 是动态类型语言 +```java +public class Demo { + public static void main(String[] args) { + int a = 10; + int b = a++ + ++a + a--; + System.out.println(a); //11 + System.out.println(b); //34 + } +} +``` - ```java - String s = "abc"; //Java - info = "abc"; //Python - ``` +判断结果: + +```java +public class Demo { + public static void main(String[] args) { + int i = 0; + int x = 0; + while (i < 10) { + x = x++; + i++; + } + System.out.println(x); // 结果是 0 + } +} +``` @@ -13833,34 +13918,36 @@ public static int invoke(Object... args) { -#### 调用指令 - -##### 五种指令 +##### 类型转换 -普通调用指令: +类型转换指令可以将两种不同的数值类型进行相互转换,除了 boolean 之外的七种类型 -- invokestatic:调用静态方法 -- invokespecial:调用私有实例方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法 -- invokevirtual:调用所有虚方法(虚方法分派) -- invokeinterface:调用接口方法 +宽化类型转换: -动态调用指令: +* JVM 支持以下数值的宽化类型转换(widening numeric conversion),小范围类型到大范围类型的安全转换 + * 从 int 类型到 long、float 或者 double 类型,对应的指令为 i2l、i2f、i2d + * 从 long 类型到 float、 double 类型,对应的指令为 l2f、l2d + * 从 float 类型到 double 类型,对应的指令为 f2d -- invokedynamic:动态解析出需要调用的方法 - - Java7 为了实现动态类型语言支持而引入了该指令,但是并没有提供直接生成 invokedynamic 指令的方法,需要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令 - - Java8 的 lambda 表达式的出现,invokedynamic 指令在 Java 中才有了直接生成方式 +* 精度损失问题 + * 宽化类型转换是不会因为超过目标类型最大值而丢失信息 + * 从 int 转换到 float 或者 long 类型转换到 double 时,将可能发生精度丢失 -指令对比: +* 从 byte、char 和 short 类型到 int 类型的宽化类型转换实际上是不存在的,JVM 把它们当作 int 处理 -- 普通调用指令固化在虚拟机内部,方法的调用执行不可干预,根据方法的符号引用链接到具体的目标方法 -- 动态调用指令支持用户确定方法 -- invokestatic 和 invokespecial 指令调用的方法称为非虚方法,虚拟机能够直接识别具体的目标方法 -- invokevirtual 和 invokeinterface 指令调用的方法称为虚方法,虚拟机需要在执行过程中根据调用者的动态类型来确定目标方法 +窄化类型转换: -指令说明: +* Java 虚拟机直接支持以下窄化类型转换: + * 从 int 类型至 byte、 short 或者 char 类型,对应的指令有 i2b、i2c、i2s + * 从 long 类型到 int 类型,对应的指令有 l2i + * 从 float 类型到 int 或者 long 类型,对应的指令有:f2i、f2l + * 从 double 类型到 int、long 或 float 者类型,对应的指令有 d2i、d2、d2f -- 如果虚拟机能够确定目标方法有且仅有一个,比如说目标方法被标记为 final,那么可以不通过动态绑定,直接确定目标方法 -- 普通成员方法是由 invokevirtual 调用,属于**动态绑定**,即支持多态 +* 精度损失问题: + * 窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,转换过程可能会导致数值丢失精度 + * 将一个浮点值窄化转换为整数类型 T(T 限于 int 或 long 类型之一)时,将遵循以下转换规则: + - 如果浮点值是 NaN,那转换结果就是 int 或 long 类型的 0 + - 如果浮点值不是无穷大的话,浮点值使用 IEEE 754 的向零舍入模式取整,获得整数值 v,如果 v 在目标类型 T 的表示范围之内,那转换结果就是 v,否则将根据 v 的符号,转换为 T 所能表示的最大或者最小正数 @@ -13868,89 +13955,52 @@ public static int invoke(Object... args) { -##### 符号引用 +##### 创建访问 -在编译过程中,虚拟机并不知道目标方法的具体内存地址,Java 编译器会暂时用符号引用来表示该目标方法,这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符 +创建指令: -* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针 -* 对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引 +* 创建类实例指令:new,接收一个操作数指向常量池的索引,表示要创建的类型,执行完成后将对象的引用压入栈 -符号引用存储在方法区常量池中,根据目标方法是否为接口方法,分为接口符号引用和非接口符号引用: + ```java + 0: new #2 // class com/jvm/bytecode/Demo + 3: dup + 4: invokespecial #3 // Method "":()V + ``` -```java -Constant pool: -... - #16 = InterfaceMethodref #27.#29 // 接口 -... - #22 = Methodref #1.#33 // 非接口 -... -``` + **dup 是复制操作数栈栈顶的内容**,需要两份引用原因: -对于非接口符号引用,假定该符号引用所指向的类为 C,则 Java 虚拟机会按照如下步骤进行查找: + - 一个要配合 invokespecial 调用该对象的构造方法 :()V (会消耗掉栈顶一个引用) + - 一个要配合 astore_1 赋值给局部变量 -1. 在 C 中查找符合名字及描述符的方法 -2. 如果没有找到,在 C 的父类中继续搜索,直至 Object 类 -3. 如果没有找到,在 C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。如果有多个符合条件的目标方法,则任意返回其中一个 +* 创建数组的指令:newarray、anewarray、multianewarray -对于接口符号引用,假定该符号引用所指向的接口为 I,则 Java 虚拟机会按照如下步骤进行查找: + * newarray:创建基本类型数组 + * anewarray:创建引用类型数组 + * multianewarray:创建多维数组 -1. 在 I 中查找符合名字及描述符的方法 -2. 如果没有找到,在 Object 类中的公有实例方法中搜索 -3. 如果没有找到,则在 I 的超接口中搜索,这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致 +字段访问指令:对象创建后可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素 +* 访问类字段(static字段,或者称为类变量)的指令:getstatic、putstatic +* 访问类实例字段(非static字段,或者称为实例变量)的指令:getfield、 putfield +类型检查指令:检查类实例或数组类型的指令 -*** +* checkcast:用于检查类型强制转换是否可以进行,如果可以进行 checkcast 指令不会改变操作数栈,否则它会抛出 ClassCastException 异常 +* instanceof:判断给定对象是否是某一个类的实例,会将判断结果压入操作数栈 -##### 执行流程 -```java -public class Demo { - public Demo() { } - private void test1() { } - private final void test2() { } - public void test3() { } - public static void test4() { } +**** - public static void main(String[] args) { - Demo3_9 d = new Demo3_9(); - d.test1(); - d.test2(); - d.test3(); - d.test4(); - Demo.test4(); - } -} -``` -几种不同的方法调用对应的字节码指令: -```java -0: new #2 // class cn/jvm/t3/bytecode/Demo -3: dup -4: invokespecial #3 // Method "":()V -7: astore_1 -8: aload_1 -9: invokespecial #4 // Method test1:()V -12: aload_1 -13: invokespecial #5 // Method test2:()V -16: aload_1 -17: invokevirtual #6 // Method test3:()V -20: aload_1 -21: pop -22: invokestatic #7 // Method test4:()V -25: invokestatic #7 // Method test4:()V -28: return -``` +##### 方法指令 -- invokespecial 调用该对象的构造方法 :()V +方法调用指令:invokevirtual、 invokeinterface、invokespecial、invokestatic、invokedynamic -- `d.test4()` 是通过**对象引用**调用一个静态方法,在调用 invokestatic 之前执行了 pop 指令,把对象引用从操作数栈弹掉 - - 不建议使用 `对象.静态方法()` 的方式调用静态方法,多了aload 和 pop 指令 - - 成员方法与静态方法调用的区别是:执行方法前是否需要对象引用 +**方法调用章节详解** @@ -13958,129 +14008,177 @@ public class Demo { -#### 多态原理 - -##### 执行原理 - -Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类定义了与父类中非私有、非静态方法同名的方法,只有当这两个方法的参数类型以及返回类型一致,Java 虚拟机才会判定为重写。 - -理解多态: +##### 操作数栈 -- 多态有编译时多态和运行时多态,即静态绑定和动态绑定 -- 前者是通过方法重载实现,后者是通过重写实现(子类覆盖父类方法,虚方法表) -- 虚方法:运行时动态绑定的方法,对比静态绑定的非虚方法调用来说,虚方法调用更加耗时 +JVM 提供的操作数栈管理指令,可以用于直接操作操作数栈的指令 -方法重写的本质: +* pop、pop2:将一个或两个元素从栈顶弹出,并且直接废弃 +* dup、dup2,dup_x1、dup2_x1,dup_x2、dup2_x2:复制栈顶一个或两个数值并重新压入栈顶 -1. 找到操作数栈的第一个元素**所执行的对象的实际类型**,记作 C +* swap:将栈最顶端的两个 slot 数值位置交换,JVM 没有提供交换两个 64 位数据类型数值的指令 -2. 如果在类型 C 中找到与描述符和名称都相符的方法,则进行访问**权限校验**(私有的),如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常 +* nop:一个非常特殊的指令,字节码为 0x00,和汇编语言中的 nop 一样,表示什么都不做,一般可用于调试、占位等 - IllegalAccessError:表示程序试图访问或修改一个属性或调用一个方法,这个属性或方法没有权限访问,一般会引起编译器异常。如果这个错误发生在运行时,就说明一个类发生了不兼容的改变 -3. 找不到,就会按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证过程 -4. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常 +*** -*** +##### 控制转移 +比较指令:比较栈顶两个元素的大小,并将比较结果入栈 -##### 虚方法表 +* lcmp:比较两个 long 类型值 +* fcmpl:比较两个 float 类型值(当遇到NaN时,返回-1) +* fcmpg:比较两个 float 类型值(当遇到NaN时,返回1) +* dcmpl:比较两个 double 类型值(当遇到NaN时,返回-1) +* dcmpg:比较两个 double 类型值(当遇到NaN时,返回1) -在虚拟机工作过程中会频繁使用到动态绑定,每次动态绑定的过程中都要重新在类的元数据中搜索合适目标,影响到执行效率。为了提高性能,JVM 采取了一种用**空间换取时间**的策略来实现动态绑定,在每个类的方法区建立一个虚方法表(virtual method table),实现使用索引表来代替查找,可以快速定位目标方法 +条件跳转指令: -* invokevirtual 所使用的虚方法表(virtual method table,vtable),执行流程 - 1. 先通过栈帧中的对象引用找到对象,分析对象头,找到对象的实际 Class - 2. Class 结构中有 vtable,查表得到方法的具体地址,执行方法的字节码 -* invokeinterface 所使用的接口方法表(interface method table,itable) +| 指令 | 说明 | +| --------- | -------------------------------------------------- | +| ifeq | equals,当栈顶int类型数值等于0时跳转 | +| ifne | not equals,当栈顶in类型数值不等于0时跳转 | +| iflt | lower than,当栈顶in类型数值小于0时跳转 | +| ifle | lower or equals,当栈顶in类型数值小于等于0时跳转 | +| ifgt | greater than,当栈顶int类型数组大于0时跳转 | +| ifge | greater or equals,当栈顶in类型数值大于等于0时跳转 | +| ifnull | 为 null 时跳转 | +| ifnonnull | 不为 null 时跳转 | -虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM 会把该类的方法表也初始化完毕 +比较条件跳转指令: -虚方法表的执行过程: +| 指令 | 说明 | +| --------- | --------------------------------------------------------- | +| if_icmpeq | 比较栈顶两 int 类型数值大小(下同),当前者等于后者时跳转 | +| if_icmpne | 当前者不等于后者时跳转 | +| if_icmplt | 当前者小于后者时跳转 | +| if_icmple | 当前者小于等于后者时跳转 | +| if_icmpgt | 当前者大于后者时跳转 | +| if_icmpge | 当前者大于等于后者时跳转 | +| if_acmpeq | 当结果相等时跳转 | +| if_acmpne | 当结果不相等时跳转 | -* 对于静态绑定的方法调用而言,实际引用将指向具体的目标方法 -* 对于动态绑定的方法调用而言,实际引用则是方法表的索引值,也就是方法的间接地址。Java 虚拟机获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法内存偏移量(指针) +多条件分支跳转指令: -为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向一个记录该类方法的方法表。每个类中都有一个虚方法表,本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法 +* tableswitch:用于 switch 条件跳转,case 值连续 +* lookupswitch:用于 switch 条件跳转,case 值不连续 -方法表满足以下的特质: +无条件跳转指令: -* 其一,子类方法表中包含父类方法表中的**所有方法**,并且在方法表中的索引值与父类方法表种的索引值相同 -* 其二,**非重写的方法指向父类的方法表项,与父类共享一个方法表项,重写的方法指向本身自己的实现**。所以这就是为什么多态情况下可以访问父类的方法。 +* goto:用来进行跳转到指定行号的字节码 - +* goto_w:无条件跳转(宽索引) -Passenger 类的方法表包括两个方法,分别对应 0 号和 1 号。方法表调换了 toString 方法和 passThroughImmigration 方法的位置,是因为 toString 方法的索引值需要与 Object 类中同名方法的索引值一致,为了保持简洁,这里不考虑 Object 类中的其他方法。 -虚方法表对性能的影响: -* 使用了方法表的动态绑定与静态绑定相比,仅仅多出几个内存解引用操作:访问栈上的调用者、读取调用者的动态类型、读取该类型的方法表、读取方法表中某个索引值所对应的目标方法,但是相对于创建并初始化 Java 栈帧这操作的开销可以忽略不计 -* 上述优化的效果看上去不错,但实际上**仅存在于解释执行**中,或者即时编译代码的最坏情况。因为即时编译还拥有另外两种性能更好的优化手段:内联缓存(inlining cache)和方法内联(method inlining) -```java -class Person { - public String toString() { - return "I'm a person."; - } - public void eat() {} - public void speak() {} -} -class Boy extends Person { - public String toString() { - return "I'm a boy"; - } - public void speak() {} - public void fight() {} -} +*** -class Girl extends Person { - public String toString() { - return "I'm a girl"; - } - public void speak() {} - public void sing() {} -} -``` -![](https://gitee.com/seazean/images/raw/master/Java/JVM-虚方法表指向.png) +##### 异常处理 +###### 处理机制 -参考文档:https://www.cnblogs.com/kaleidoscope/p/9790766.html +抛出异常指令:athrow 指令 +JVM 处理异常(catch 语句)不是由字节码指令来实现的,而是**采用异常表来完成**的 +* 代码: -*** + ```java + public static void main(String[] args) { + int i = 0; + try { + i = 10; + } catch (Exception e) { + i = 20; + } finally { + i = 30; + } + } + ``` + +* 字节码: + * 多出一个 **Exception table** 的结构,**[from, to) 是前闭后开的检测范围**,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号 + * 11 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置,因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用 + ```java + 0: iconst_0 + 1: istore_1 // 0 -> i ->赋值 + 2: bipush 10 // try 10 放入操作数栈顶 + 4: istore_1 // 10 -> i 将操作数栈顶数据弹出,存入局部变量表的 slot1 + 5: bipush 30 // 【finally】 + 7: istore_1 // 30 -> i + 8: goto 27 // return ----------------------------------- + 11: astore_2 // catch Exceptin -> e ---------------------- + 12: bipush 20 // + 14: istore_1 // 20 -> i + 15: bipush 30 // 【finally】 + 17: istore_1 // 30 -> i + 18: goto 27 // return ----------------------------------- + 21: astore_3 // catch any -> slot 3 ---------------------- + 22: bipush 30 // 【finally】 + 24: istore_1 // 30 -> i + 25: aload_3 // 将局部变量表的slot 3数据弹出,放入操作数栈栈顶 + 26: athrow // throw 抛出异常 + 27: return + Exception table: + // 任何阶段出现任务异常都会执行 finally + from to target type + 2 5 11 Class java/lang/Exception + 2 5 21 any // 剩余的异常类型,比如 Error + 11 15 21 any // 剩余的异常类型,比如 Error + LineNumberTable: ... + LocalVariableTable: + Start Length Slot Name Signature + 12 3 2 e Ljava/lang/Exception; + 0 28 0 args [Ljava/lang/String; + 2 26 1 i I + ``` -##### 内联缓存 -内联缓存:是一种加快动态绑定的优化技术,能够缓存虚方法调用中**调用者的动态类型以及该类型所对应的目标方法**。在之后的执行过程中,如果碰到已缓存的类型,便会直接调用该类型所对应的目标方法;反之内联缓存则会退化至使用基于方法表的动态绑定 -多态的三个术语: +*** -* 单态 (monomorphic):指的是仅有一种状态的情况 -* 多态 (polymorphic):指的是有限数量种状态的情况,二态(bimorphic)是多态的其中一种 -* 超多态 (megamorphic):指的是更多种状态的情况,通常用一个具体数值来区分多态和超多态,在这个数值之下,称之为多态,否则称之为超多态 -对于内联缓存来说,有对应的单态内联缓存、多态内联缓存: -* 单态内联缓存:只缓存了一种动态类型以及所对应的目标方法,实现简单,比较所缓存的动态类型,如果命中,则直接调用对应的目标方法。 -* 多态内联缓存:缓存了多个动态类型及其目标方法,需要逐个将所缓存的动态类型与当前动态类型进行比较,如果命中,则调用对应的目标方法 +###### finally -为了节省内存空间,**Java 虚拟机只采用单态内联缓存**,没有命中的处理方法: +finally 中的代码被**复制了 3 份**,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程(上节案例) -* 替换单态内联缓存中的纪录,类似于 CPU 中的数据缓存,对数据的局部性有要求,即在替换内联缓存之后的一段时间内,方法调用的调用者的动态类型应当保持一致,从而能够有效地利用内联缓存 -* 劣化为超多态状态,这也是 Java 虚拟机的具体实现方式,这种状态实际上放弃了优化的机会,将直接访问方法表来动态绑定目标方法,但是与替换内联缓存纪录相比节省了写缓存的额外开销 +* 代码: -虽然内联缓存附带内联二字,但是并没有内联目标方法 + ```java + public static int test() { + try { + return 10; + } finally { + return 20; + } + } + ``` +* 字节码: + ```java + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 + 3: bipush 20 // 20 放入栈顶 + 5: ireturn // 返回栈顶 int(20) + 6: astore_1 // catch any 存入局部变量表的 slot1 + 7: bipush 20 // 20 放入栈顶 + 9: ireturn // 返回栈顶 int(20) + Exception table: + from to target type + 0 3 6 any + ``` @@ -14088,30 +14186,73 @@ class Girl extends Person { -### 代码优化 +###### return -#### 语法糖 - -语法糖:指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担 +* 吞异常 + ```java + public static int test() { + try { + return 10; + } finally { + return 20; + } + } + ``` + ```java + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 -> slot 0 【从栈顶移除了】 + 3: bipush 20 // 20 放入栈顶 + 5: ireturn // 返回栈顶 int(20) + 6: astore_1 // catch any 存入局部变量表的 slot1 + 7: bipush 20 // 20 放入栈顶 + 9: ireturn // 返回栈顶 int(20) + Exception table: + from to target type + 0 3 6 any + ``` -#### 构造器 + * 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果以 finally 的为准 + * 字节码中没有 **athrow** ,表明如果在 finally 中出现了 return,会**吞掉异常** -```java -public class Candy1 { -} -``` +* 不吞异常 -```java -public class Candy1 { - // 这个无参构造是编译器帮助我们加上的 - public Candy1() { - super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object." - ":()V - } -} -``` + ```java + public class Demo { + public static void main(String[] args) { + int result = test(); + System.out.println(result);//10 + } + public static int test() { + int i = 10; + try { + return i;//返回10 + } finally { + i = 20; + } + } + } + ``` + + ```java + 0: bipush 10 // 10 放入栈顶 + 2: istore_0 // 10 赋值给i,放入slot 0 + 3: iload_0 // i(10)加载至操作数栈 + 4: istore_1 // 10 -> slot 1,【暂存至 slot 1,目的是为了固定返回值】 + 5: bipush 20 // 20 放入栈顶 + 7: istore_0 // 20 slot 0 + 8: iload_1 // slot 1(10) 载入 slot 1 暂存的值 + 9: ireturn // 返回栈顶的 int(10) + 10: astore_2 // catch any -> slot 2 存入局部变量表的 slot2 + 11: bipush 20 + 13: istore_0 + 14: aload_2 + 15: athrow // 不会吞掉异常 + Exception table: + from to target type + 3 5 10 any + ``` @@ -14119,130 +14260,100 @@ public class Candy1 { -#### 拆装箱 +##### 同步控制 -```java -Integer x = 1; -int y = x; -``` +方法级的同步:是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中,虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法 -这段代码在 JDK 5 之前是无法编译通过的,必须改写为代码片段2: +方法内指定指令序列的同步:有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义 -```java -Integer x = Integer.valueOf(1); -int y = x.intValue(); -``` +* montiorenter:进入并获取对象监视器,即为栈顶对象加锁 +* monitorexit:释放并退出对象监视器,即为栈顶对象解锁 -JDK5 以后编译阶段自动转换成上述片段 + -*** +*** -#### 泛型擦除 -泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行**泛型擦除**的动作,即泛型信息 -在编译为字节码之后就丢失了,实际的类型都**当做了 Object 类型**来处理: -```java -List list = new ArrayList<>(); -list.add(10); // 实际调用的是 List.add(Object e) -Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index); -``` +#### 执行流程 -编译器真正生成的字节码中,还要额外做一个类型转换的操作: +原始 Java 代码: ```java -// 需要将 Object 转为 Integer -Integer x = (Integer)list.get(0); +public class Demo { + public static void main(String[] args) { + int a = 10; + int b = Short.MAX_VALUE + 1; + int c = a + b; + System.out.println(c); + } +} ``` -如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是: - -```java -// 需要将 Object 转为 Integer, 并执行拆箱操作 -int x = ((Integer)list.get(0)).intValue(); -``` +javap -v Demo.class:省略 +* 常量池载入运行时常量池 +* 方法区字节码载入方法区 -*** +* main 线程开始运行,分配栈帧内存:(操作数栈stack=2,局部变量表locals=4) +* **执行引擎**开始执行字节码 + `bipush 10`:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令 -#### 可变参数 + * sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节) + * ldc 将一个 int 压入操作数栈 + * ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节) + * 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池 -```java -public class Candy4 { - public static void foo(String... args) { - String[] array = args; // 直接赋值 - System.out.println(array); - } - public static void main(String[] args) { - foo("hello", "world"); - } -} -``` + `istore_1`:将操作数栈顶数据弹出,存入局部变量表的 slot 1 -可变参数 `String... args` 其实是 `String[] args` , java 编译器会在编译期间将上述代码变换为: + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程1.png) -```java -public static void main(String[] args) { - foo(new String[]{"hello", "world"}); -} -``` + `ldc #3`:从常量池加载 #3 数据到操作数栈 + Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算完成 -注意:如果调用了 `foo()` 则等价代码为 `foo(new String[]{})` ,创建了一个空的数组,而不会传递 null 进去 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程2.png) + `istore_2`:将操作数栈顶数据弹出,存入局部变量表的 slot 2 + `iload_1`:将局部变量表的 slot 1数据弹出,放入操作数栈栈顶 -**** + `iload_2`:将局部变量表的 slot 2数据弹出,放入操作数栈栈顶 + `iadd`:执行相加操作 + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程3.png) -#### foreach + `istore_3`:将操作数栈顶数据弹出,存入局部变量表的 slot 3 -**数组的循环:** + `getstatic #4`:获取静态字段 -```java -int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖 -for (int e : array) { - System.out.println(e); -} -``` + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程4.png) -编译后为循环取数: + `iload_3`: -```java -for(int i = 0; i < array.length; ++i) { - int e = array[i]; - System.out.println(e); -} -``` + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程5.png) -**集合的循环:** + `invokevirtual #5`: -```java -List list = Arrays.asList(1,2,3,4,5); -for (Integer i : list) { - System.out.println(i); -} -``` + * 找到常量池 #5 项 + * 定位到方法区 java/io/PrintStream.println:(I)V 方法 + * **生成新的栈帧**(分配 locals、stack等) + * 传递参数,执行新栈帧中的字节码 + * 执行完毕,弹出栈帧 + * 清除 main 操作数栈内容 -编译后转换为对迭代器的调用: + ![](https://gitee.com/seazean/images/raw/master/Java/JVM-字节码执行流程6.png) -```java -List list = Arrays.asList(1, 2, 3, 4, 5); -Iterator iter = list.iterator(); -while(iter.hasNext()) { - Integer e = (Integer)iter.next(); - System.out.println(e); -} -``` + return:完成 main 方法调用,弹出 main 栈帧,程序结束 -注意:foreach 循环写法,能够配合数组以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器 + @@ -14250,56 +14361,21 @@ while(iter.hasNext()) { -#### switch - -##### 字符串 - -从 JDK 开始,switch 可以作用于字符串和枚举类: +### 执行引擎 -```java -switch (str) { - case "hello": { - System.out.println("h"); - break; - } - case "world": { - System.out.println("w"); - break; - } -} -``` +#### 基本介绍 -注意:**switch 配合 String 和枚举使用时,变量不能为null** +执行引擎:Java 虚拟机的核心组成部分之一,类加载主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,需要执行引擎将字节码指令解释/编译为对应平台上的本地机器指令 -会被编译器转换为: +虚拟机是一个相对于物理机的概念,这两种机器都有代码执行能力: -```java -byte x = -1; -switch(str.hashCode()) { - case 99162322: // hello 的 hashCode - if (str.equals("hello")) { - x = 0; - } - break; - case 113318802: // world 的 hashCode - if (str.equals("world")) { - x = 1; - } -} -switch(x) { - case 0: - System.out.println("h"); - break; - case 1: - System.out.println("w"); - break; -} -``` +* 物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上 +* 虚拟机的执行引擎是由软件自行实现的,可以不受物理条件制约地定制指令集与执行引擎的结构体系 -总结: +Java 是**半编译半解释型语言**,将解释执行与编译执行二者结合起来进行: -* 执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较 -* hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突 +* 解释器:根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容翻译为对应平台的本地机器指令执行 +* 即时编译器(JIT : Just In Time Compiler):虚拟机运行时将源代码直接编译成**和本地机器平台相关的机器码**后再执行,并存入 Code Cache,下次遇到相同的代码直接执行,效率高 @@ -14307,249 +14383,116 @@ switch(x) { -##### 枚举 +#### 执行方式 -switch 枚举的例子,原始代码: +HotSpot VM 采用**解释器与即时编译器并存的架构**,解释器和即时编译器能够相互协作,去选择最合适的方式来权衡编译本地代码和直接解释执行代码的时间 -```java -enum Sex { - MALE, FEMALE -} -public class Candy7 { - public static void foo(Sex sex) { - switch (sex) { - case MALE: - System.out.println("男"); - break; - case FEMALE: - System.out.println("女"); - break; - } - } -} -``` +HostSpot JVM 的默认执行方式: -编译转换后的代码: +* 当程序启动后,解释器可以马上发挥作用立即执行,省去编译器编译的时间(解释器存在的**必要性**) +* 随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率 -```java -/** -* 定义一个合成类(仅 jvm 使用,对我们不可见) -* 用来映射枚举的 ordinal 与数组元素的关系 -* 枚举的 ordinal 表示枚举对象的序号,从 0 开始 -* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1 -*/ -static class $MAP { - // 数组大小即为枚举元素个数,里面存储 case 用来对比的数字 - static int[] map = new int[2]; - static { - map[Sex.MALE.ordinal()] = 1; - map[Sex.FEMALE.ordinal()] = 2; - } -} -public static void foo(Sex sex) { - int x = $MAP.map[sex.ordinal()]; - switch (x) { - case 1: - System.out.println("男"); - break; - case 2: - System.out.println("女"); - break; - } -} -``` +HotSpot VM 可以通过 VM 参数设置程序执行方式: +- -Xint:完全采用解释器模式执行程序 +- -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行 +- -Xmixed:采用解释器 + 即时编译器的混合模式共同执行程序 +![](https://gitee.com/seazean/images/raw/master/Java/JVM-执行引擎工作流程.png) -*** +*** -#### 枚举类 -JDK 7 新增了枚举类: -```java -enum Sex { - MALE, FEMALE -} -``` +#### 热点探测 -编译转换后: +热点代码:被 JIT 编译器编译的字节码,根据代码被调用执行的频率而定,一个被多次调用的方法或者一个循环次数较多的循环体都可以被称之为热点代码 -```java -public final class Sex extends Enum { - public static final Sex MALE; - public static final Sex FEMALE; - private static final Sex[] $VALUES; - static { - MALE = new Sex("MALE", 0); - FEMALE = new Sex("FEMALE", 1); - $VALUES = new Sex[]{MALE, FEMALE}; - } - private Sex(String name, int ordinal) { - super(name, ordinal); - } - public static Sex[] values() { - return $VALUES.clone(); - } - public static Sex valueOf(String name) { - return Enum.valueOf(Sex.class, name); - } -} -``` +热点探测:JIT 编译器在运行时会针热点代码做出深度优化,将其直接编译为对应平台的本地机器指令进行缓存,以提升程序执行性能 +JIT 编译在默认情况是异步进行的,当触发某方法或某代码块的优化时,先将其放入编译队列,然后由编译线程进行编译,编译之后的代码放在 CodeCache 中,通过 `-XX:-BackgroundCompilation` 参数可以关闭异步编译 +* CodeCache 用于缓存编译后的机器码、动态生成的代码和本地方法代码 JNI +* 如果 CodeCache 区域被占满,编译器被停用,字节码将不会编译为机器码,应用程序继续运行,但运行性能会降低很多 +HotSpot VM 采用的热点探测方式是基于计数器的热点探测,为每一个方法都建立 2 个不同类型的计数器:方法调用计数器(Invocation Counter)和回边计数器(BackEdge Counter) +* 方法调用计数器:用于统计方法被调用的次数,默认阈值在 Client 模式 下是 1500 次,在 Server 模式下是 10000 次(需要进行激进的优化),超过这个阈值,就会触发 JIT 编译,阈值可以通过虚拟机参数 `-XX:CompileThreshold` 设置 -*** + 工作流程:当一个方法被调用时, 会先检查该方法是否存在被 JIT 编译过的版本,存在则使用编译后的本地代码来执行;如果不存在则将此方法的调用计数器值加 1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值,如果超过阈值会向即时编译器**提交一个该方法的代码编译请求** +* 回边计数器:统计一个方法中循环体代码执行的次数,在字节码中控制流向后跳转的指令称为回边 + 如果一个方法中的循环体需要执行多次,可以优化为为栈上替换,简称 OSR (On StackReplacement) 编译,**OSR 替换循环代码体的入口,C1、C2 替换的是方法调用的入口**,OSR 编译后会出现方法的整段代码被编译了,但是只有循环体部分才执行编译后的机器码,其他部分仍是解释执行 -#### try-w-r -JDK 7 开始新增了对需要关闭的资源处理的特殊语法 `try-with-resources`,格式: -```java -try(资源变量 = 创建资源对象){ -} catch( ) { -} -``` +*** -其中资源对象需要实现 **AutoCloseable**接口,例如 InputStream、OutputStream、Connection、Statement、ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources可以不用写 finally 语句块,编译器会帮助生成关闭资源代码: -```java -try(InputStream is = new FileInputStream("d:\\1.txt")) { - System.out.println(is); -} catch (IOException e) { - e.printStackTrace(); -} -``` -转换成: +#### 分层编译 -`addSuppressed(Throwable e)`:添加被压制异常,是为了防止异常信息的丢失(**fianlly 中如果抛出了异常**) +HotSpot VM 内嵌两个 JIT 编译器,分别为 Client Compiler 和 Server Compiler,简称 C1 编译器和 C2 编译器 -```java -try { - InputStream is = new FileInputStream("d:\\1.txt"); - Throwable t = null; - try { - System.out.println(is); - } catch (Throwable e1) { - // t 是我们代码出现的异常 - t = e1; - throw e1; - } finally { - // 判断了资源不为空 - if (is != null) { - // 如果我们代码有异常 - if (t != null) { - try { - is.close(); - } catch (Throwable e2) { - // 如果 close 出现异常,作为被压制异常添加 - t.addSuppressed(e2); - } - } else { - // 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e - is.close(); - } - } - } -} catch (IOException e) { - e.printStackTrace(); -} -``` +C1 编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度,C1 编译器的优化方法: +* 方法内联:**将调用的函数代码编译到调用点处**,这样可以减少栈帧的生成,减少参数传递以及跳转过程 + 方法内联能够消除方法调用的固定开销,任何方法除非被内联,否则调用都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。 -*** + ```java + private static int square(final int i) { + return i * i; + } + System.out.println(square(9)); + ``` + square 是热点方法,会进行内联,把方法内代码拷贝粘贴到调用者的位置: + ```java + System.out.println(9 * 9); + ``` -#### 方法重写 + 还能够进行常量折叠(constant folding)的优化: -方法重写时对返回值分两种情况: + ```java + System.out.println(81); + ``` -* 父子类的返回值完全一致 -* 子类返回值可以是父类返回值的子类 +* 冗余消除:根据运行时状况进行代码折叠或削除 -```java -class A { - public Number m() { - return 1; - } -} -class B extends A { - @Override - // 子类m方法的返回值是Integer是父类m方法返回值Number的子类 - public Integer m() { - return 2; - } -} -``` +* 内联缓存:是一种加快动态绑定的优化技术(方法调用部分详解) -对于子类,java 编译器会做如下处理: +C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,当激进优化的假设不成立时,再退回使用 C1 编译,这也是使用分层编译的原因 -```java -class B extends A { - public Integer m() { - return 2; - } - // 此方法才是真正重写了父类 public Number m() 方法 - public synthetic bridge Number m() { - // 调用 public Integer m() - return m(); - } -} -``` +C2 的优化主要是在全局层面,逃逸分析是优化的基础:标量替换、栈上分配、同步消除 -其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突 +VM 参数设置: +- -client:指定 Java 虚拟机运行在 Client 模式下,并使用 C1 编译器 +- -server:指定 Java 虚拟机运行在 Server 模式下,并使用 C2 编译器 +- `-server -XX:+TieredCompilation`:在 1.8 之前,分层编译默认是关闭的,可以添加该参数开启 +分层编译策略 (Tiered Compilation):程序解释执行可以触发 C1 编译,将字节码编译成机器码,加上性能监控,C2 编译会根据性能监控信息进行激进优化,JVM 将执行状态分成了 5 个层次: -*** +* 0 层,解释执行(Interpreter) +* 1 层,使用 C1 即时编译器编译执行(不带 profiling) +* 2 层,使用 C1 即时编译器编译执行(带基本的 profiling) -#### 匿名内部类 +* 3 层,使用 C1 即时编译器编译执行(带完全的 profiling) -##### 无参优化 +* 4 层,使用 C2 即时编译器编译执行(C1 和 C2 协作运行) -源代码: + 说明:profiling 是指在运行过程中收集一些程序执行状态的数据,例如方法的调用次数,循环的回边次数等 -```java -public class Candy11 { - public static void main(String[] args) { - Runnable runnable = new Runnable() { - @Override - public void run() { - System.out.println("ok"); - } - }; - } -} -``` -转化后代码: -```java -// 额外生成的类 -final class Candy11$1 implements Runnable { - Candy11$1() { - } - public void run() { - System.out.println("ok"); - } -} -public class Candy11 { - public static void main(String[] args) { - Runnable runnable = new Candy11$1(); - } -} -``` +参考文章:https://www.jianshu.com/p/20bd2e9b1f03 @@ -14557,181 +14500,138 @@ public class Candy11 { -##### 带参优化 +### 方法调用 -引用局部变量的匿名内部类,源代码: +#### 方法识别 + +Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descriptor) + +* **方法描述符是由方法的参数类型以及返回类型所构成**,Java 层面叫方法特征签名 +* 在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么 Java 虚拟机会在类的验证阶段报错 + +JVM 根据名字和描述符来判断的,只要返回值不一样(方法描述符不一样),其它完全一样,在 JVM 中是允许的,但 Java 语言不允许 ```java -public class Candy11 { - public static void test(final int x) { - Runnable runnable = new Runnable() { - @Override - public void run() { - System.out.println("ok:" + x); - } - }; - } -} -``` - -转换后代码: - -```java -final class Candy11$1 implements Runnable { - int val$x; - Candy11$1(int x) { - this.val$x = x; - } - public void run() { - System.out.println("ok:" + this.val$x); - } +// 返回值类型不同,编译阶段直接报错 +public static Integer invoke(Object... args) { + return 1; } -public class Candy11 { - public static void test(final int x) { - Runnable runnable = new Candy11$1(x); - } +public static int invoke(Object... args) { + return 2; } ``` -局部变量在底层创建为内部类的成员变量,必须是 final 的原因: - -* 在 Java 中方法调用是值传递的,在匿名内部类中对变量的操作都是基于原变量的副本,不会影响到原变量的值,所以原变量的值的改变也无法同步到副本中 - -* 外部变量为 final 是在编译期以强制手段确保用户不会在内部类中做修改原变量值的操作,也是**防止外部操作修改了变量而内部类无法随之变化**出现的影响 - - 在创建 `Candy11$1 ` 对象时,将 x 的值赋值给了 `Candy11$1` 对象的 val 属性,x 不应该再发生变化了,因为发生变化,this.val$x 属性没有机会再跟着变化 - *** -#### 反射优化 +#### 调用机制 -```java -public class Reflect1 { - public static void foo() { - System.out.println("foo..."); - } - public static void main(String[] args) throws Exception { - Method foo = Reflect1.class.getMethod("foo"); - for (int i = 0; i <= 16; i++) { - System.out.printf("%d\t", i); - foo.invoke(null); - } - System.in.read(); - } -} -``` +方法调用并不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,不是方法的具体运行过程 -foo.invoke 0 ~ 15次调用的是 MethodAccessor 的实现类 `NativeMethodAccessorImpl.invoke0()`,本地方法执行速度慢;当调用到第 16 次时,会采用运行时生成的类 `sun.reflect.GeneratedMethodAccessor1` 代替 +在 JVM 中,将符号引用转换为直接引用有两种机制: -```java -public Object invoke(Object obj, Object[] args)throws Exception { - // inflationThreshold 膨胀阈值,默认 15 - if (++numInvocations > ReflectionFactory.inflationThreshold() - && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) { - MethodAccessorImpl acc = (MethodAccessorImpl) - new MethodAccessorGenerator(). - generateMethod(method.getDeclaringClass(), - method.getName(), - method.getParameterTypes(), - method.getReturnType(), - method.getExceptionTypes(), - method.getModifiers()); - parent.setDelegate(acc); - } - // 【调用本地方法实现】 - return invoke0(method, obj, args); -} -private static native Object invoke0(Method m, Object obj, Object[] args); -``` +- 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,将调用方法的符号引用转换为直接引用的过程称之为静态链接(类加载的解析阶段) +- 动态链接:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称为动态链接(初始化后的解析阶段) -```java -public class GeneratedMethodAccessor1 extends MethodAccessorImpl { - // 如果有参数,那么抛非法参数异常 - block4 : { - if (arrobject == null || arrobject.length == 0) break block4; - throw new IllegalArgumentException(); - } - try { - // 【可以看到,已经是直接调用方法】 - Reflect1.foo(); - // 因为没有返回值 - return null; - } - //.... -} -``` +对应方法的绑定(分配)机制:静态绑定和动态绑定。绑定是一个字段、方法或者类从符号引用被替换为直接引用的过程,仅发生一次: -通过查看 ReflectionFactory 源码可知: +- 静态绑定:被调用的目标方法在编译期可知,且运行期保持不变,将这个方法与所属的类型进行绑定 +- 动态绑定:被调用的目标方法在编译期无法确定,只能在程序运行期根据实际的类型绑定相关的方法 -* sun.reflect.noInflation 可以用来禁用膨胀,直接生成 GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算 -* sun.reflect.inflationThreshold 可以修改膨胀阈值 +* Java 编译器已经区分了重载的方法(静态绑定和动态绑定),因此可以认为虚拟机中不存在重载 +非虚方法: +- 非虚方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的 +- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法 +- 所有普通成员方法、实例方法、被重写的方法都是虚方法 +动态类型语言和静态类型语言: +- 在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言 -*** +- 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息 +- **Java 是静态类型语言**(尽管 Lambda 表达式为其增加了动态特性),JS,Python 是动态类型语言 + ```java + String s = "abc"; //Java + info = "abc"; //Python + ``` -## 系统优化 +*** -### 性能调优 -#### 性能指标 -性能指标主要是吞吐量、响应时间、QPS、TPS 等、并发用户数等,而这些性能指标又依赖于系统服务器的资源,如 CPU、内存、磁盘 IO、网络 IO 等,对于这些指标数据的收集,通常可以根据Java本身的工具或指令进行查询 +#### 调用指令 -几个重要的指标: +##### 五种指令 -1. 停顿时间(响应时间):提交请求和返回该请求的响应之间使用的时间,比如垃圾回收中 STW 的时间 -2. 吞吐量:对单位时间内完成的工作量(请求)的量度(可以对比 GC 的性能指标) -3. 并发数:同一时刻,对服务器有实际交互的请求数 -4. QPS:Queries Per Second,每秒处理的查询量 -5. TPS:Transactions Per Second,每秒产生的事务数 -6. 内存占用:Java 堆区所占的内存大小 +普通调用指令: +- invokestatic:调用静态方法 +- invokespecial:调用私有实例方法、构造器,和父类的实例方法或构造器,以及所实现接口的默认方法 +- invokevirtual:调用所有虚方法(虚方法分派) +- invokeinterface:调用接口方法 +动态调用指令: -*** +- invokedynamic:动态解析出需要调用的方法 + - Java7 为了实现动态类型语言支持而引入了该指令,但是并没有提供直接生成 invokedynamic 指令的方法,需要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令 + - Java8 的 lambda 表达式的出现,invokedynamic 指令在 Java 中才有了直接生成方式 +指令对比: +- 普通调用指令固化在虚拟机内部,方法的调用执行不可干预,根据方法的符号引用链接到具体的目标方法 +- 动态调用指令支持用户确定方法 +- invokestatic 和 invokespecial 指令调用的方法称为非虚方法,虚拟机能够直接识别具体的目标方法 +- invokevirtual 和 invokeinterface 指令调用的方法称为虚方法,虚拟机需要在执行过程中根据调用者的动态类型来确定目标方法 -#### 优化步骤 +指令说明: -对于一个系统要部署上线时,则一定会对 JVM 进行调整,不经过任何调整直接上线,容易出现线上系统频繁 FullGC 造成系统卡顿、CPU 使用频率过高、系统无反应等问题 +- 如果虚拟机能够确定目标方法有且仅有一个,比如说目标方法被标记为 final,那么可以不通过动态绑定,直接确定目标方法 +- 普通成员方法是由 invokevirtual 调用,属于**动态绑定**,即支持多态 -1. 性能监控:通过运行日志、堆栈信息、线程快照等信息监控是否出现 GC 频繁、OOM、内存泄漏、死锁、响应时间过长等情况 -2. 性能分析: - * 打印 GC 日志,通过 GCviewe r或者 http://gceasy.io 来分析异常信息 +*** - - 运用命令行工具、jstack、jmap、jinfo 等 - - dump 出堆文件,使用内存分析工具分析文件 - - 使用阿里 Arthas、jconsole、JVisualVM 来**实时查看 JVM 状态** +##### 符号引用 - - jstack 查看堆栈信息 +在编译过程中,虚拟机并不知道目标方法的具体内存地址,Java 编译器会暂时用符号引用来表示该目标方法,这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符 -3. 性能调优: +* 对于静态绑定的方法调用而言,实际引用是一个指向方法的指针 +* 对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引 - * 适当增加内存,根据业务背景选择垃圾回收器 +符号引用存储在方法区常量池中,根据目标方法是否为接口方法,分为接口符号引用和非接口符号引用: - - 优化代码,控制内存使用 +```java +Constant pool: +... + #16 = InterfaceMethodref #27.#29 // 接口 +... + #22 = Methodref #1.#33 // 非接口 +... +``` - - 增加机器,分散节点压力 +对于非接口符号引用,假定该符号引用所指向的类为 C,则 Java 虚拟机会按照如下步骤进行查找: - - 合理设置线程池线程数量 +1. 在 C 中查找符合名字及描述符的方法 +2. 如果没有找到,在 C 的父类中继续搜索,直至 Object 类 +3. 如果没有找到,在 C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。如果有多个符合条件的目标方法,则任意返回其中一个 - - 使用中间件提高程序效率,比如缓存、消息队列等 +对于接口符号引用,假定该符号引用所指向的接口为 I,则 Java 虚拟机会按照如下步骤进行查找: + +1. 在 I 中查找符合名字及描述符的方法 +2. 如果没有找到,在 Object 类中的公有实例方法中搜索 +3. 如果没有找到,则在 I 的超接口中搜索,这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致 @@ -14739,3061 +14639,83 @@ public class GeneratedMethodAccessor1 extends MethodAccessorImpl { -#### 参数调优 +##### 执行流程 -对于 JVM 调优,主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型 +```java +public class Demo { + public Demo() { } + private void test1() { } + private final void test2() { } -* 设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值 + public void test3() { } + public static void test4() { } - ```sh - -Xms:设置堆的初始化大小 - -Xmx:设置堆的最大大小 - ``` + public static void main(String[] args) { + Demo3_9 d = new Demo3_9(); + d.test1(); + d.test2(); + d.test3(); + d.test4(); + Demo.test4(); + } +} +``` -* 设置年轻代中 Eden 区和两个 Survivor 区的大小比例。该值如果不设置,则默认比例为 8:1:1。Java 官方通过增大 Eden 区的大小,来减少 YGC 发生的次数,虽然次数减少了,但 Eden 区满的时候,由于占用的空间较大,导致释放缓慢,此时 STW 的时间较长,因此需要按照程序情况去调优 +几种不同的方法调用对应的字节码指令: - ```sh - -XX:SurvivorRatio - ``` +```java +0: new #2 // class cn/jvm/t3/bytecode/Demo +3: dup +4: invokespecial #3 // Method "":()V +7: astore_1 +8: aload_1 +9: invokespecial #4 // Method test1:()V +12: aload_1 +13: invokespecial #5 // Method test2:()V +16: aload_1 +17: invokevirtual #6 // Method test3:()V +20: aload_1 +21: pop +22: invokestatic #7 // Method test4:()V +25: invokestatic #7 // Method test4:()V +28: return +``` -* 年轻代和老年代默认比例为 1:2,可以通过调整二者空间大小比率来设置两者的大小。 +- invokespecial 调用该对象的构造方法 :()V - ```sh - -XX:newSize 设置年轻代的初始大小 - -XX:MaxNewSize 设置年轻代的最大大小, 初始大小和最大大小两个值通常相同 - ``` +- `d.test4()` 是通过**对象引用**调用一个静态方法,在调用 invokestatic 之前执行了 pop 指令,把对象引用从操作数栈弹掉 + - 不建议使用 `对象.静态方法()` 的方式调用静态方法,多了aload 和 pop 指令 + - 成员方法与静态方法调用的区别是:执行方法前是否需要对象引用 -* 线程堆栈的设置:**每个线程默认会开启 1M 的堆栈**,用于存放栈帧、调用参数、局部变量等,但一般 256K 就够用,通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统 - ```sh - -Xss 对每个线程stack大小的调整,-Xss128k - ``` -* 一般一天超过一次 FullGC 就是有问题,首先通过工具查看是否出现内存泄露,如果出现内存泄露则调整代码,没有的话则调整 JVM 参数 +*** -* 系统 CPU 持续飙高的话,首先先排查代码问题,如果代码没问题,则咨询运维或者云服务器供应商,通常服务器重启或者服务器迁移即可解决 -* 如果数据查询性能很低下的话,如果系统并发量并没有多少,则应更加关注数据库的相关问题 -* 如果服务器配置还不错,JDK8 开始尽量使用 G1 或者新生代和老年代组合使用并行垃圾回收器 +#### 多态原理 +##### 执行原理 +Java 虚拟机中关于方法重写的判定基于方法描述符,如果子类定义了与父类中非私有、非静态方法同名的方法,只有当这两个方法的参数类型以及返回类型一致,Java 虚拟机才会判定为重写。 +理解多态: +- 多态有编译时多态和运行时多态,即静态绑定和动态绑定 +- 前者是通过方法重载实现,后者是通过重写实现(子类覆盖父类方法,虚方法表) +- 虚方法:运行时动态绑定的方法,对比静态绑定的非虚方法调用来说,虚方法调用更加耗时 -**** - - - - - -### 命令行篇 - -#### jps - -jps(Java Process Statu):显示指定系统内所有的 HotSpot 虚拟机进程(查看虚拟机进程信息),可用于查询正在运行的虚拟机进程,进程的本地虚拟机 ID 与操作系统的进程 ID 是一致的,是唯一的 - -使用语法:`jps [options] [hostid]` - -options 参数: - -- -q:仅仅显示 LVMID(local virtual machine id),即本地虚拟机唯一 id,不显示主类的名称等 - -- -l:输出应用程序主类的全类名或如果进程执行的是 jar 包,则输出 jar 完整路径 - -- -m:输出虚拟机进程启动时传递给主类 main()的参数 - -- -v:列出虚拟机进程启动时的JVM参数,比如 -Xms20m -Xmx50m是启动程序指定的 jvm 参数 - -ostid 参数:RMI注册表中注册的主机名,如果想要远程监控主机上的 java 程序,需要安装 jstatd - - - -**** - - - -#### jstat - -jstat(JVM Statistics Monitoring Tool):用于监视 JVM 各种运行状态信息的命令行工具,可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有 GUI 的图形界面,只提供了纯文本控制台环境的服务器上,它是运行期定位虚拟机性能问题的首选工具,常用于检测垃圾回收问题以及内存泄漏问题 - -使用语法:`jstat -