diff --git "a/Git/Git\347\211\210\346\234\254\345\233\236\351\200\200\346\226\271\346\263\225\350\256\272(\345\217\257\350\203\275\350\247\243\345\206\263\344\275\240101%\351\201\207\345\210\260\347\232\204Git\347\211\210\346\234\254\351\227\256\351\242\230).md" "b/Git/Git\347\211\210\346\234\254\345\233\236\351\200\200\346\226\271\346\263\225\350\256\272(\345\217\257\350\203\275\350\247\243\345\206\263\344\275\240101%\351\201\207\345\210\260\347\232\204Git\347\211\210\346\234\254\351\227\256\351\242\230).md" deleted file mode 100644 index 8d5f27d5c5..0000000000 --- "a/Git/Git\347\211\210\346\234\254\345\233\236\351\200\200\346\226\271\346\263\225\350\256\272(\345\217\257\350\203\275\350\247\243\345\206\263\344\275\240101%\351\201\207\345\210\260\347\232\204Git\347\211\210\346\234\254\351\227\256\351\242\230).md" +++ /dev/null @@ -1,120 +0,0 @@ -# 1 本地回退 -你在本地做了错误的 commit,先找到要回退的版本的`commit id`: - -```bash -git reflog -``` -![](https://img-blog.csdnimg.cn/20200414142250436.png) -接着回退版本: - -```bash -git reset --hard cac0 -``` -> cac0就是你要回退的版本的`commit id`的前面几位 - -回退到某次提交。回退到的指定提交以后的提交都会从提交日志上消失。 - -> 工作区和暂存区的内容都会被重置到指定提交的时候,如果不加`--hard`则只移动`HEAD`指针,不影响工作区和暂存区的内容。 - -结合`git reflog`找回提交日志上看不到的版本历史,撤回某次操作前的状态 -这个方法可以对你的回退操作进行回退,因为这时候`git log`已经找不到历史提交的hash值了。 - -# 2 远程回退 -## 2.1 回退自己的远程分支 -你的错误commit已经推送到远程分支,就需要回滚远程分支。 -- 首先要回退本地分支: - -```bash -git reflog -git reset --hard cac0 -``` -![](https://img-blog.csdnimg.cn/20200414142459436.png) -- 由于本地分支回滚后,版本将落后远程分支,必须使用强制推送覆盖远程分支,否则后面将无法推送到远程分支。 - -```bash -git push -f -``` -![](https://img-blog.csdnimg.cn/20200414142539953.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -- 注意修正为`git push -f origin branch_name` -![](https://img-blog.csdnimg.cn/20200414142624784.png) - -## 2.2 回退公共远程分支 -如果你回退公共远程分支,把别人的提交给丢掉了怎么办? -> 本人毕业时在前东家 hw 经常干的蠢事。 - -### 分析 -假如你的远程master分支情况是这样的: - -```bash -A1–A2–B1 -``` -> A、B分别代表两个人 -> A1、A2、B1代表各自的提交 -> 所有人的本地分支都已经更新到最新版本,和远程分支一致 - -这时发现A2这次commit有误,你用reset回滚远程分支master到A1,那么理想状态是你的同事一拉代码git pull,他们的master分支也回滚了 -然而现实却是,你的同事会看到下面的提示: - -```bash -$ git status -On branch master -Your branch is ahead of 'origin/master' by 2 commits. - (use "git push" to publish your local commits) -nothing to commit, working directory clean -``` -也就是说,你的同事的分支并没有主动回退,而是比远程分支超前了两次提交,因为远程分支回退了。 -不幸的是,现实中,我们经常遇到的都是猪一样的队友,他们一看到下面提示: - -```bash -$ git status -On branch master -Your branch is ahead of 'origin/master' by 2 commits. - (use "git push" to publish your local commits) -nothing to commit, working directory clean -``` -就习惯性的git push一下,或者他们直接用的SourceTree这样的图形界面工具,一看到界面上显示的是推送的提示就直接点了推送按钮,卧槽,辛辛苦苦回滚的版本就这样轻松的被你猪一样的队友给还原了,所以,只要有一个队友push之后,远程master又变成了: - -```bash -A1 – A2 – B1 -``` - -这就是分布式,每个人都有副本。 - -用另外一种方法来回退版本。 - -# 3 公共远程回退 -使用git reset回退公共远程分支的版本后,需要其他所有人手动用远程master分支覆盖本地master分支,显然,这不是优雅的回退方法。 - -```bash -git revert HEAD //撤销最近一次提交 -git revert HEAD~1 //撤销上上次的提交,注意:数字从0开始 -git revert 0ffaacc //撤销0ffaacc这次提交 -``` -git revert 命令意思是撤销某次提交。它会产生一个新的提交,虽然代码回退了,但是版本依然是向前的,所以,当你用revert回退之后,所有人pull之后,他们的代码也自动的回退了。但是,要注意以下几点: - -- revert 是撤销一次提交,所以后面的commit id是你需要回滚到的版本的前一次提交 -- 使用revert HEAD是撤销最近的一次提交,如果你最近一次提交是用revert命令产生的,那么你再执行一次,就相当于撤销了上次的撤销操作,换句话说,你连续执行两次revert HEAD命令,就跟没执行是一样的 -- 使用revert HEAD~1 表示撤销最近2次提交,这个数字是从0开始的,如果你之前撤销过产生了commi id,那么也会计算在内的 -- 如果使用 revert 撤销的不是最近一次提交,那么一定会有代码冲突,需要你合并代码,合并代码只需要把当前的代码全部去掉,保留之前版本的代码就可以了 -- git revert 命令的好处就是不会丢掉别人的提交,即使你撤销后覆盖了别人的提交,他更新代码后,可以在本地用 reset 向前回滚,找到自己的代码,然后拉一下分支,再回来合并上去就可以找回被你覆盖的提交了。 - -# 4 revert 合并代码,解决冲突 -使用revert命令,如果不是撤销的最近一次提交,那么一定会有冲突,如下所示: - -```bash -<<<<<<< HEAD -全部清空 -第一次提交 -======= -全部清空 ->>>>>>> parent of c24cde7... 全部清空 -``` -解决冲突很简单,因为我们只想回到某次提交,因此需要把当前最新的代码去掉即可,也就是HEAD标记的代码: - -```bash -<<<<<<< HEAD -全部清空 -第一次提交 -======= -``` -把上面部分代码去掉就可以了,然后再提交一次代码就可以解决冲突了。 \ No newline at end of file diff --git "a/JDK/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213\346\267\261\345\205\245\350\257\246\350\247\243(JMM).md" "b/JDK/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213\346\267\261\345\205\245\350\257\246\350\247\243(JMM).md" deleted file mode 100644 index 1dbb544b41..0000000000 --- "a/JDK/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213\346\267\261\345\205\245\350\257\246\350\247\243(JMM).md" +++ /dev/null @@ -1,128 +0,0 @@ -# 前言 -定义俩共享变量及俩方法: -- 第一个方法, -- 第二个方法 -- (r1,r2)的可能值有哪些? -![](https://img-blog.csdnimg.cn/05139ccfbb40447a869632ff35959841.png) - -在单线程环境下,可先调用第一个方法,最终(r1,r2)为(1,0) -也可以先调用第二个方法,最终为(0,2)。 - -![](https://img-blog.csdnimg.cn/20200404214401993.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -# 1 Java内存模型的意义 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTkyYTFmZGY0OGJlMTllMDYucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTIzZTVlOWE0OWFkZWI1YTEucG5n?x-oss-process=image/format,png) -JMM 与硬件内存架构对应关系![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTVlMTM3NGEwYWJmOWM5MjkucG5n?x-oss-process=image/format,png) -JMM抽象结构图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQ0ZWE4ODQzYTg4YTk0MGQucG5n?x-oss-process=image/format,png) -内存模型描述程序的可能行为。 - -Java虚拟机规范中试图定义一种Java内存模型,`来屏蔽掉各种硬件和os的内存访问差异`,规定: -- 线程如何、何时能看到其他线程修改过的共享变量的值 -- 必要时,如何同步地访问共享变量 - -以实现让Java程序在各种平台下都能达到一致性的内存访问效果。 - -JMM通过检查执行跟踪中的每个读操作,并根据某些规则检查该读操作观察到的写操作是否有效来工作。 - -只要程序的所有执行产生的结果都可由JMM预测。具体实现者任意实现,包括操作的重新排序和删除不必要的同步。 - -JMM决定了在程序的每个点上可以读取什么值。 -## 1.1 共享变量(Shared Variables) -可在线程之间共享的内存称为`共享内存或堆内存`。所有实例字段、静态字段和数组元素都存储在堆内存。 -不包括局部变量与方法参数,因为这些是线程私有的,不存在共享。 - -对同一变量的两次访问(读或写),若有一个是写请求,则是冲突的! -# 2 主内存与工作内存 -工作内存缓存 -![](https://img-blog.csdnimg.cn/20191014024209488.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -JMM的主要是定义了`各个变量的访问规则`,在JVM中的如下底层细节: -- 将变量存储到内存 -- 从内存中取出变量值 - -为获得较好执行效率,JMM并未限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器调整代码执行顺序这类权限。 - -JMM规定: -- 所有变量都存储在主内存(Main Memory) -- 每条线程有自己的工作内存(Working Memory) -保存了该线程使用到的`变量的主内存副本拷贝`(线程所访问对象的引用或者对象中某个在线程访问到的字段,不会是整个对象的拷贝) -线程对变量的所有操作(读,赋值等)都必须在工作内存进行,不能直接读写主内存中的变量 -volatile变量依然有工作内存的拷贝,,是他特殊的操作顺序性规定,看起来如同直接在主内存读写 -不同线程间,无法直接访问对方工作内存中的变量,线程间变量值的传递均要通过主内存 - -线程、主内存、工作内存三者的交互关系: -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTEyMjA5YjEyZDU3OGEyZWQucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWJiM2QzN2MxNTVjZDgyZDgucG5n?x-oss-process=image/format,png) - -JVM模型与JMM不是同一层次的内存划分,基本毫无关系的,硬要对应起来,从变量,内存,工作内存的定义来看 -- 主内存 《=》Java堆中的对象实例数据部分 -- 工作内存 《=》虚拟机栈中的部分区域 - -从更底层的层次来看: -- 主内存直接对应物理硬件的内存 -- 为更好的运行速度,虚拟机(甚至硬件系统的本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存器,因为程序运行时主要访问读写的是工作内存 -# 3 内存间同步操作 -## 3.1 线程操作的定义 -### 操作定义 -write要写的变量以及要写的值。 -read要读的变量以及可见的写入值(由此,我们可以确定可见的值)。 -lock要锁定的管程(监视器monitor)。 -unlock要解锁的管程。 -外部操作(socket等等..) -启动和终止 -### 程序顺序 -如果一个程序没有数据竞争,那么程序的所有执行看起来都是顺序一致的 - -本规范只涉及线程间的操作; -一个变量如何从主内存拷贝到工作内存,从工作内存同步回主内存的实现细节 - -JMM 本身已经定义实现了以下8种操作来完成,且都具备`原子性` -- lock(锁定) -作用于主内存变量,把一个变量标识为一条线程独占的状态 -- unlock(解锁) -作用于主内存变量,把一个处于锁定状态的变量释放,释放后的变量才可以被其它线程锁定 -unlock之前必须将变量值同步回主内存 -- read(读取) -作用于主内存变量,把一个变量的值从主内存传输到工作内存,以便随后的load -- load(载入) -作用于工作内存变量,把read从主内存中得到的变量值放入工作内存的变量副本 -- use(使用) -作用于工作内存变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到的变量的值得字节码指令时将会执行这个操作 -- assign(赋值) -作用于工作内存变量,把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作 -- store(存储) -作用于工作内存变量,把工作内存中一个变量的值传送到主内存,以便随后的write操作使用 -- write(写入) -作用于主内存变量,把store操作从工作内存中得到的值放入主内存的变量中 - -- 把一个变量从主内存`复制`到工作内存 -就要顺序执行read和load - -- 把变量从工作内存`同步`回主内存 -就要顺序地执行store和write操作 - -JMM只要求上述两个操作必须`按序执行`,而没有保证连续执行 -也就是说read/load之间、store/write之间可以插入其它指令 -如对主内存中的变量a,b访问时,一种可能出现的顺序是read a->readb->loadb->load a - -JMM规定执行上述八种基础操作时必须满足如下 -## 3.1 同步规则 -◆ 对于监视器 m 的解锁与所有后续操作对于 m 的加锁 `同步`(之前的操作保持可见) -◆对 volatile变量v的写入,与所有其他线程后续对v的读同步 - -◆ `启动` 线程的操作与线程中的第一个操作同步 -◆ 对于每个属性写入默认值(0, false, null)与每个线程对其进行的操作同步 -◆ 线程 T1的最后操作与线程T2发现线程T1已经结束同步。( isAlive ,join可以判断线程是否终结) -◆ 如果线程 T1中断了T2,那么线程T1的中断操作与其他所有线程发现T2被中断了同步通过抛出*InterruptedException*异常,或者调用*Thread.interrupted*或*Thread.isInterrupted* - -- 不允许read/load、store/write操作之一单独出现 -不允许一个变量从主内存读取了但工作内存不接收,或从工作内存发起回写但主内存不接收 -- 不允许一个线程丢弃它的最近的assign -即变量在工作内存中改变(为工作内存变量赋值)后必须把该变化同步回主内存 -- 新变量只能在主内存“诞生”,不允许在工作内存直接使用一个未被初始化(load或assign)的变量 -换话说就是一个变量在实施use,store之前,必须先执行过assign和load -- 如果一个变量事先没有被load锁定,则不允许对它执行unlock,也不允许去unlock一个被其它线程锁定的变量 -- 对一个变量执行unloack前,必须把此变量同步回主内存中(执行store,write) - -> 参考 -> - https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.1 \ No newline at end of file diff --git "a/JDK/JVM/\345\244\215\344\271\240\357\274\232GC\345\210\206\347\261\273.md" "b/JDK/JVM/\345\244\215\344\271\240\357\274\232GC\345\210\206\347\261\273.md" deleted file mode 100644 index fe5b5ef69a..0000000000 --- "a/JDK/JVM/\345\244\215\344\271\240\357\274\232GC\345\210\206\347\261\273.md" +++ /dev/null @@ -1,340 +0,0 @@ - -## 复习:GC分类 - -针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC) - -- 部分收集(Partial GC):不是完整收集整个 Java 堆的垃圾收集。其中又分为: - - - 新生代收集(Minor GC / Young GC):只是新生代(Eden / S0, S1)的垃圾收集 - - 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。目前,只有 CMS GC 会有单独收集老年代的行为。注意,很多时候 Major GC 会和 Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收。 -- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前,只有 G1 GC 会有这种行为 -- 整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集。 - -1. 新生代收集:只有当Eden区满的时候就会进行新生代收集,所以新生代收集和S0区域和S1区域情况无关 - -2. 老年代收集和新生代收集的关系:进行老年代收集之前会先进行一次年轻代的垃圾收集,原因如下:一个比较大的对象无法放入新生代,那它自然会往老年代去放,如果老年代也放不下,那会先进行一次新生代的垃圾收集,之后尝试往新生代放,如果还是放不下,才会进行老年代的垃圾收集,之后在往老年代去放,这是一个过程,我来说明一下为什么需要往老年代放,但是放不下,而进行新生代垃圾收集的原因,这是因为新生代垃圾收集比老年代垃圾收集更加简单,这样做可以节省性能 - -3. 进行垃圾收集的时候,堆包含新生代、老年代、元空间/永久代:可以看出Heap后面包含着新生代、老年代、元空间,但是我们设置堆空间大小的时候设置的只是新生代、老年代而已,元空间是分开设置的 - -4. 哪些情况会触发Full GC: -- 老年代空间不足 -- 方法区空间不足 -- 显示调用System.gc() -- Minior GC进入老年代的数据的平均大小 大于 老年代的可用内存 -- 大对象直接进入老年代,而老年代的可用空间不足 - - - -## 不同GC分类的GC细节 - -用例代码: - -```Java -/** - * -XX:+PrintCommandLineFlags - * - * -XX:+UseSerialGC:表明新生代使用Serial GC ,同时老年代使用Serial Old GC - * - * -XX:+UseParNewGC:标明新生代使用ParNew GC - * - * -XX:+UseParallelGC:表明新生代使用Parallel GC - * -XX:+UseParallelOldGC : 表明老年代使用 Parallel Old GC - * 说明:二者可以相互激活 - * - * -XX:+UseConcMarkSweepGC:表明老年代使用CMS GC。同时,年轻代会触发对ParNew 的使用 - * @author shkstart - * @create 17:19 - */ -public class GCUseTest { - public static void main(String[] args) { - ArrayList list = new ArrayList<>(); - - while(true){ - byte[] arr = new byte[1024 * 10];//10kb - list.add(arr); -// try { -// Thread.sleep(5); -// } catch (InterruptedException e) { -// e.printStackTrace(); -// } - } - } -} -``` - -### 老年代使用CMS GC - -**GC设置方法**:参数中使用-XX:+UseConcMarkSweepGC,说明老年代使用CMS GC,同时年轻代也会触发对ParNew的使用,因此添加该参数之后,新生代使用ParNew GC,而老年代使用CMS GC,整体是并发垃圾收集,主打低延迟 - -![image-20220419202643](http://pan.icebule.top/%E6%9C%89%E9%81%93%E4%BA%91%E7%AC%94%E8%AE%B0%E5%9B%BE%E5%BA%8A/%E4%B8%AA%E4%BA%BA%E7%AC%94%E8%AE%B0/JAVA/JVM/20220419202551.png) - -打印出来的GC细节: - -![image-20220419211943](http://pan.icebule.top/%E6%9C%89%E9%81%93%E4%BA%91%E7%AC%94%E8%AE%B0%E5%9B%BE%E5%BA%8A/%E4%B8%AA%E4%BA%BA%E7%AC%94%E8%AE%B0/JAVA/JVM/20220419211943.png) - - - -### 新生代使用Serial GC - - **GC设置方法**:参数中使用-XX:+UseSerialGC,说明新生代使用Serial GC,同时老年代也会触发对Serial Old GC的使用,因此添加该参数之后,新生代使用Serial GC,而老年代使用Serial Old GC,整体是串行垃圾收集 - -![image-20220419212907](http://pan.icebule.top/%E6%9C%89%E9%81%93%E4%BA%91%E7%AC%94%E8%AE%B0%E5%9B%BE%E5%BA%8A/%E4%B8%AA%E4%BA%BA%E7%AC%94%E8%AE%B0/JAVA/JVM/20220419212907.png) - - 打印出来的GC细节: - -![image-20220419212940](http://pan.icebule.top/%E6%9C%89%E9%81%93%E4%BA%91%E7%AC%94%E8%AE%B0%E5%9B%BE%E5%BA%8A/%E4%B8%AA%E4%BA%BA%E7%AC%94%E8%AE%B0/JAVA/JVM/20220419212940.png) - -DefNew代表新生代使用Serial GC,然后Tenured代表老年代使用Serial Old GC - -## GC 日志分类 - -### MinorGC - -MinorGC(或 young GC 或 YGC)日志: - -```java -[GC (Allocation Failure) [PSYoungGen: 31744K->2192K (36864K) ] 31744K->2200K (121856K), 0.0139308 secs] [Times: user=0.05 sys=0.01, real=0.01 secs] -``` - -![image-20220419202643](http://pan.icebule.top/%E6%9C%89%E9%81%93%E4%BA%91%E7%AC%94%E8%AE%B0%E5%9B%BE%E5%BA%8A/%E4%B8%AA%E4%BA%BA%E7%AC%94%E8%AE%B0/JAVA/JVM/20220419202643.png) - -![image-20220419202718](http://pan.icebule.top/%E6%9C%89%E9%81%93%E4%BA%91%E7%AC%94%E8%AE%B0%E5%9B%BE%E5%BA%8A/%E4%B8%AA%E4%BA%BA%E7%AC%94%E8%AE%B0/JAVA/JVM/20220419202718.png) - -### FullGC - -```java -[Full GC (Metadata GC Threshold) [PSYoungGen: 5104K->0K (132096K) ] [Par01dGen: 416K->5453K (50176K) ]5520K->5453K (182272K), [Metaspace: 20637K->20637K (1067008K) ], 0.0245883 secs] [Times: user=0.06 sys=0.00, real=0.02 secs] -``` - -![image-20220419202740](http://pan.icebule.top/%E6%9C%89%E9%81%93%E4%BA%91%E7%AC%94%E8%AE%B0%E5%9B%BE%E5%BA%8A/%E4%B8%AA%E4%BA%BA%E7%AC%94%E8%AE%B0/JAVA/JVM/20220419202740.png) - -![image-20220419202804](http://pan.icebule.top/%E6%9C%89%E9%81%93%E4%BA%91%E7%AC%94%E8%AE%B0%E5%9B%BE%E5%BA%8A/%E4%B8%AA%E4%BA%BA%E7%AC%94%E8%AE%B0/JAVA/JVM/20220419202804.png) - -## GC 日志结构剖析 - -### 透过日志看垃圾收集器 - -- Serial 收集器:新生代显示 "[DefNew",即 Default New Generation - -- ParNew 收集器:新生代显示 "[ParNew",即 Parallel New Generation - -- Parallel Scavenge 收集器:新生代显示"[PSYoungGen",JDK1.7 使用的即 PSYoungGen - -- Parallel Old 收集器:老年代显示"[ParoldGen" - -- G1 收集器:显示”garbage-first heap“ - -### 透过日志看 GC 原因 - -- Allocation Failure:表明本次引起 GC 的原因是因为新生代中没有足够的区域存放需要分配的数据 -- Metadata GCThreshold:Metaspace 区不够用了 -- FErgonomics:JVM 自适应调整导致的 GC -- System:调用了 System.gc()方法 - -### 透过日志看 GC 前后情况 - -通过图示,我们可以发现 GC 日志格式的规律一般都是:GC 前内存占用-> GC 后内存占用(该区域内存总大小) - -```java -[PSYoungGen: 5986K->696K (8704K) ] 5986K->704K (9216K) -``` - -- 中括号内:GC 回收前年轻代堆大小,回收后大小,(年轻代堆总大小) - -- 括号外:GC 回收前年轻代和老年代大小,回收后大小,(年轻代和老年代总大小) - -注意:Minor GC 堆内存总容量 = 9/10 年轻代 + 老年代。原因是 Survivor 区只计算 from 部分,而 JVM 默认年轻代中 Eden 区和 Survivor 区的比例关系,Eden:S0:S1=8:1:1。 - -### 透过日志看 GC 时间 - -GC 日志中有三个时间:user,sys 和 real - -- user:进程执行用户态代码(核心之外)所使用的时间。这是执行此进程所使用的实际 CPU 时间,其他进程和此进程阻塞的时间并不包括在内。在垃圾收集的情况下,表示 GC 线程执行所使用的 CPU 总时间。 -- sys:进程在内核态消耗的 CPU 时间,即在内核执行系统调用或等待系统事件所使用的 CPU 时间 -- real:程序从开始到结束所用的时钟时间。这个时间包括其他进程使用的时间片和进程阻塞的时间(比如等待 I/O 完成)。对于并行 gc,这个数字应该接近(用户时间+系统时间)除以垃圾收集器使用的线程数。 - -由于多核的原因,一般的 GC 事件中,real time 是小于 sys time + user time 的,因为一般是多个线程并发的去做 GC,所以 real time 是要小于 sys + user time 的。如果 real > sys + user 的话,则你的应用可能存在下列问题:IO 负载非常重或 CPU 不够用。 - -## Minor GC 日志解析 - -### 日志格式 - -```Java -2021-09-06T08:44:49.453+0800: 4.396: [GC (Allocation Failure) [PSYoungGen: 76800K->8433K(89600K)] 76800K->8449K(294400K), 0.0060231 secs] [Times: user=0.02 sys=0.01, real=0.01 secs] -``` - -### 日志解析 - -#### 2021-09-06T08:44:49.453+0800 - -日志打印时间 日期格式 如 2013-05-04T21:53:59.234+0800 - -添加-XX:+PrintGCDateStamps参数 - -#### 4.396 - -gc 发生时,Java 虚拟机启动以来经过的秒数 - -添加-XX:+PrintGCTimeStamps该参数 - -#### [GC (Allocation Failure) - -发生了一次垃圾回收,这是一次 Minor GC。它不区分新生代 GC 还是老年代 GC,括号里的内容是 gc 发生的原因,这里 Allocation Failure 的原因是新生代中没有足够区域能够存放需要分配的数据而失败。 - -#### [PSYoungGen: 76800K->8433K(89600K)] - -**PSYoungGen**:表示GC发生的区域,区域名称与使用的GC收集器是密切相关的 - -- **Serial收集器**:Default New Generation 显示Defnew -- **ParNew收集器**:ParNew -- **Parallel Scanvenge收集器**:PSYoung -- 老年代和新生代同理,也是和收集器名称相关 - -**76800K->8433K(89600K)**:GC前该内存区域已使用容量->GC后盖区域容量(该区域总容量) - -- 如果是新生代,总容量则会显示整个新生代内存的9/10,即eden+from/to区 -- 如果是老年代,总容量则是全身内存大小,无变化 - -#### 76800K->8449K(294400K) - -虽然本次是Minor GC,只会进行新生代的垃圾收集,但是也肯定会打印堆中总容量相关信息 - -在显示完区域容量GC的情况之后,会接着显示整个堆内存区域的GC情况:GC前堆内存已使用容量->GC后堆内存容量(堆内存总容量),并且堆内存总容量 = 9/10 新生代 + 老年代,然后堆内存总容量肯定小于初始化的内存大小 - -#### ,0.0088371 - -整个GC所花费的时间,单位是秒 - -#### [Times:user=0.02 sys=0.01,real=0.01 secs] - -- **user**:指CPU工作在用户态所花费的时间 -- **sys**:指CPU工作在内核态所花费的时间 -- **real**:指在此次事件中所花费的总时间 - -## Full GC 日志解析 - -### 日志格式 - -```Java -2021-09-06T08:44:49.453+0800: 4.396: [Full GC (Metadata GC Threshold) [PSYoungGen: 10082K->0K(89600K)] [ParOldGen: 32K->9638K(204800K)] 10114K->9638K(294400K), [Metaspace: 20158K->20156K(1067008K)], 0.0149928 secs] [Times: user=0.06 sys=0.02, real=0.02 secs] -``` - -### 日志解析 - -#### 2020-11-20T17:19:43.794-0800 - -日志打印时间 日期格式 如 2013-05-04T21:53:59.234+0800 - -添加-XX:+PrintGCDateStamps参数 - -#### 1.351 - -gc 发生时,Java 虚拟机启动以来经过的秒数 - -添加-XX:+PrintGCTimeStamps该参数 - -#### Full GC(Metadata GCThreshold) - -括号中是gc发生的原因,原因:Metaspace区不够用了。 -除此之外,还有另外两种情况会引起Full GC,如下: - -1. Full GC(FErgonomics) - 原因:JVM自适应调整导致的GC -2. Full GC(System) - 原因:调用了System.gc()方法 - -#### [PSYoungGen: 100082K->0K(89600K)] - -**PSYoungGen**:表示GC发生的区域,区域名称与使用的GC收集器是密切相关的 - -- **Serial收集器**:Default New Generation 显示DefNew -- **ParNew收集器**:ParNew -- **Parallel Scanvenge收集器**:PSYoungGen -- 老年代和新生代同理,也是和收集器名称相关 - -**10082K->0K(89600K)**:GC前该内存区域已使用容量->GC该区域容量(该区域总容量) - -- 如果是新生代,总容量会显示整个新生代内存的9/10,即eden+from/to区 - -- 如果是老年代,总容量则是全部内存大小,无变化 - -#### ParOldGen:32K->9638K(204800K) - -老年代区域没有发生GC,因此本次GC是metaspace引起的 - -#### 10114K->9638K(294400K), - -在显示完区域容量GC的情况之后,会接着显示整个堆内存区域的GC情况:GC前堆内存已使用容量->GC后堆内存容量(堆内存总容量),并且堆内存总容量 = 9/10 新生代 + 老年代,然后堆内存总容量肯定小于初始化的内存大小 - -#### [Meatspace:20158K->20156K(1067008K)], - -metaspace GC 回收2K空间 - - - -## 论证FullGC是否会回收元空间/永久代垃圾 - -```Java -/** - * jdk6/7中: - * -XX:PermSize=10m -XX:MaxPermSize=10m - *

- * jdk8中: - * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m - * - * @author IceBlue - * @create 2020 22:24 - */ -public class OOMTest extends ClassLoader { - public static void main(String[] args) { - int j = 0; - try { - for (int i = 0; i < 100000; i++) { - OOMTest test = new OOMTest(); - //创建ClassWriter对象,用于生成类的二进制字节码 - ClassWriter classWriter = new ClassWriter(0); - //指明版本号,修饰符,类名,包名,父类,接口 - classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); - //返回byte[] - byte[] code = classWriter.toByteArray(); - //类的加载 - test.defineClass("Class" + i, code, 0, code.length);//Class对象 - test = null; - j++; - } - } finally { - System.out.println(j); - } - } -} -``` - -输出结果: - -``` -[GC (Metadata GC Threshold) [PSYoungGen: 10485K->1544K(152576K)] 10485K->1552K(500736K), 0.0011517 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] -[Full GC (Metadata GC Threshold) [PSYoungGen: 1544K->0K(152576K)] [ParOldGen: 8K->658K(236544K)] 1552K->658K(389120K), [Metaspace: 3923K->3320K(1056768K)], 0.0051012 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] -[GC (Metadata GC Threshold) [PSYoungGen: 5243K->832K(152576K)] 5902K->1490K(389120K), 0.0009536 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] - --------省略N行------- - -[Full GC (Last ditch collection) [PSYoungGen: 0K->0K(2427904K)] [ParOldGen: 824K->824K(5568000K)] 824K->824K(7995904K), [Metaspace: 3655K->3655K(1056768K)], 0.0041177 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] -Heap - PSYoungGen total 2427904K, used 0K [0x0000000755f80000, 0x00000007ef080000, 0x00000007ffe00000) - eden space 2426880K, 0% used [0x0000000755f80000,0x0000000755f80000,0x00000007ea180000) - from space 1024K, 0% used [0x00000007ea180000,0x00000007ea180000,0x00000007ea280000) - to space 1536K, 0% used [0x00000007eef00000,0x00000007eef00000,0x00000007ef080000) - ParOldGen total 5568000K, used 824K [0x0000000602200000, 0x0000000755f80000, 0x0000000755f80000) - object space 5568000K, 0% used [0x0000000602200000,0x00000006022ce328,0x0000000755f80000) - Metaspace used 3655K, capacity 4508K, committed 9728K, reserved 1056768K - class space used 394K, capacity 396K, committed 2048K, reserved 1048576K - -进程已结束,退出代码0 - -``` - -通过不断地动态生成类对象,输出GC日志 - -根据GC日志我们可以看出当元空间容量耗尽时,会触发FullGC,而每次FullGC之前,至会进行一次MinorGC,而MinorGC只会回收新生代空间; - -只有在FullGC时,才会对新生代,老年代,永久代/元空间全部进行垃圾收集 \ No newline at end of file diff --git "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/\344\270\200\346\226\207\346\220\236\346\207\202Java\347\232\204SPI\346\234\272\345\210\266.md" "b/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/\344\270\200\346\226\207\346\220\236\346\207\202Java\347\232\204SPI\346\234\272\345\210\266.md" deleted file mode 100644 index 1c69b61c1e..0000000000 --- "a/JDK/Java8\347\274\226\347\250\213\346\234\200\344\275\263\345\256\236\350\267\265/\344\270\200\346\226\207\346\220\236\346\207\202Java\347\232\204SPI\346\234\272\345\210\266.md" +++ /dev/null @@ -1,63 +0,0 @@ -# 1 简介 -SPI,Service Provider Interface,一种服务发现机制。 -![](https://img-blog.csdnimg.cn/7921efaa5683447cbb1bc6cf351c4332.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -有了SPI,即可实现服务接口与服务实现的解耦: -- 服务提供者(如 springboot starter)提供出 SPI 接口。身为服务提供者,在你无法形成绝对规范强制时,适度"放权" 比较明智,适当让客户端去自定义实现 -- 客户端(普通的 springboot 项目)即可通过本地注册的形式,将实现类注册到服务端,轻松实现可插拔 - -## 缺点 -- 不能按需加载。虽然 ServiceLoader 做了延迟加载,但是只能通过遍历的方式全部获取。如果其中某些实现类很耗时,而且你也不需要加载它,那么就形成了资源浪费 -- 获取某个实现类的方式不够灵活,只能通过迭代器的形式获取 - -> Dubbo SPI 实现方式对以上两点进行了业务优化。 - -# 源码 -![](https://img-blog.csdnimg.cn/339efe7e74764bbc91f8ea037c3f69a6.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -应用程序通过迭代器接口获取对象实例,这里首先会判断 providers 对象中是否有实例对象: -- 有实例,那么就返回 -- 没有,执行类的装载步骤,具体类装载实现如下: - -LazyIterator#hasNextService 读取 META-INF/services 下的配置文件,获得所有能被实例化的类的名称,并完成 SPI 配置文件的解析 - - -LazyIterator#nextService 负责实例化 hasNextService() 读到的实现类,并将实例化后的对象存放到 providers 集合中缓存 - -# 使用 -如某接口有3个实现类,那系统运行时,该接口到底选择哪个实现类呢? -这时就需要SPI,**根据指定或默认配置,找到对应实现类,加载进来,然后使用该实现类实例**。 - -如下系统运行时,加载配置,用实现A2实例化一个对象来提供服务: -![](https://img-blog.csdnimg.cn/20201220141747102.png) -再如,你要通过jar包给某个接口提供实现,就在自己jar包的`META-INF/services/`目录下放一个接口同名文件,指定接口的实现是自己这个jar包里的某类即可: -![](https://img-blog.csdnimg.cn/20201220142131599.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -别人用这个接口,然后用你的jar包,就会在运行时通过你的jar包指定文件找到这个接口该用哪个实现类。这是JDK内置提供的功能。 - -> 我就不定义在 META-INF/services 下面行不行?就想定义在别的地方可以吗? - -No!JDK 已经规定好配置路径,你若随便定义,类加载器可就不知道去哪里加载了 -![](https://img-blog.csdnimg.cn/bba23763598a4d19a80616e85623c7c9.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -假设你有个工程P,有个接口A,A在P无实现类,系统运行时怎么给A选实现类呢? -可以自己搞个jar包,`META-INF/services/`,放上一个文件,文件名即接口名,接口A的实现类=`com.javaedge.service.实现类A2`。 -让P来依赖你的jar包,等系统运行时,P跑起来了,对于接口A,就会扫描依赖的jar包,看看有没有`META-INF/services`文件夹: -- 有,再看看有无名为接口A的文件: - - 有,在里面查找指定的接口A的实现是你的jar包里的哪个类即可 -# 适用场景 -## 插件扩展 -比如你开发了一个开源框架,若你想让别人自己写个插件,安排到你的开源框架里中,扩展功能时。 - -如JDBC。Java定义了一套JDBC的接口,但并未提供具体实现类,而是在不同云厂商提供的数据库实现包。 -> 但项目运行时,要使用JDBC接口的哪些实现类呢? - -一般要根据自己使用的数据库驱动jar包,比如我们最常用的MySQL,其`mysql-jdbc-connector.jar` 里面就有: -![](https://img-blog.csdnimg.cn/20201220151405844.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -系统运行时碰到你使用JDBC的接口,就会在底层使用你引入的那个jar中提供的实现类。 -## 案例 -如sharding-jdbc 数据加密模块,本身支持 AES 和 MD5 两种加密方式。但若客户端不想用内置的两种加密,偏偏想用 RSA 算法呢?难道每加一种算法,sharding-jdbc 就要发个版本? - -sharding-jdbc 可不会这么蠢,首先提供出 EncryptAlgorithm 加密算法接口,并引入 SPI 机制,做到服务接口与服务实现分离的效果。 -客户端想要使用自定义加密算法,只需在客户端项目 `META-INF/services` 的路径下定义接口的全限定名称文件,并在文件内写上加密实现类的全限定名 -![](https://img-blog.csdnimg.cn/fea9f40870554ee8b579af6e34e22171.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -![](https://img-blog.csdnimg.cn/2025dd20872942c8a0560338787e9a63.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -这就显示了SPI的优点: -- 客户端(自己的项目)提供了服务端(sharding-jdbc)的接口自定义实现,但是与服务端状态分离,只有在客户端提供了自定义接口实现时才会加载,其它并没有关联;客户端的新增或删除实现类不会影响服务端 -- 如果客户端不想要 RSA 算法,又想要使用内置的 AES 算法,那么可以随时删掉实现类,可扩展性强,插件化架构 \ No newline at end of file diff --git "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\227(15)-\345\216\237\345\255\220\351\201\215\345\216\206\344\270\216\351\235\236\351\230\273\345\241\236\345\220\214\346\255\245\346\234\272\345\210\266.md" "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\22715\344\271\213\345\216\237\345\255\220\351\201\215\345\216\206\344\270\216\351\235\236\351\230\273\345\241\236\345\220\214\346\255\245\346\234\272\345\210\266(Atomic-Variables-and-Non-blocking-Synchron.md" similarity index 68% rename from "JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\227(15)-\345\216\237\345\255\220\351\201\215\345\216\206\344\270\216\351\235\236\351\230\273\345\241\236\345\220\214\346\255\245\346\234\272\345\210\266.md" rename to "JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\22715\344\271\213\345\216\237\345\255\220\351\201\215\345\216\206\344\270\216\351\235\236\351\230\273\345\241\236\345\220\214\346\255\245\346\234\272\345\210\266(Atomic-Variables-and-Non-blocking-Synchron.md" index 5aece1fb28..13eabf7add 100644 --- "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\227(15)-\345\216\237\345\255\220\351\201\215\345\216\206\344\270\216\351\235\236\351\230\273\345\241\236\345\220\214\346\255\245\346\234\272\345\210\266.md" +++ "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\263\273\345\210\22715\344\271\213\345\216\237\345\255\220\351\201\215\345\216\206\344\270\216\351\235\236\351\230\273\345\241\236\345\220\214\346\255\245\346\234\272\345\210\266(Atomic-Variables-and-Non-blocking-Synchron.md" @@ -1,30 +1,32 @@ -非阻塞算法,用底层的原子机器指令代替锁,确保数据在并发访问中的一致性。 -非阻塞算法被广泛应用于OS和JVM中实现线程/进程调度机制和GC及锁,并发数据结构中。 +近年并发算法领域大多数研究都侧重非阻塞算法,这种算法用底层的原子机器指令代替锁来确保数据在并发访问中的一致性,非阻塞算法被广泛应用于OS和JVM中实现线程/进程调度机制和GC以及锁,并发数据结构中。 + +与锁的方案相比,非阻塞算法都要复杂的多,他们在可伸缩性和活跃性上(避免死锁)都有巨大优势。 -与锁相比,非阻塞算法复杂的多,在可伸缩性和活跃性上(避免死锁)有巨大优势。 非阻塞算法,即多个线程竞争相同的数据时不会发生阻塞,因此能更细粒度的层次上进行协调,而且极大减少调度开销。 # 1 锁的劣势 独占,可见性是锁要保证的。 -许多JVM都对非竞争的锁获取和释放做了很多优化,性能很不错。 -但若一些线程被挂起然后稍后恢复运行,当线程恢复后还得等待其他线程执行完他们的时间片,才能被调度,所以挂起和恢复线程存在很大开销。 -其实很多锁的粒度很小,很简单,若锁上存在激烈竞争,那么 调度开销/工作开销 比值就会非常高,降低业务吞吐量。 +许多JVM都对非竞争的锁获取和释放做了很多优化,性能很不错了。 + +但是如果一些线程被挂起然后稍后恢复运行,当线程恢复后还得等待其他线程执行完他们的时间片,才能被调度,所以挂起和恢复线程存在很大的开销,其实很多锁的力度很小的,很简单,如果锁上存在着激烈的竞争,那么多调度开销/工作开销比值就会非常高。 + +与锁相比volatile是一种更轻量的同步机制,因为使用volatile不会发生上下文切换或者线程调度操作,但是volatile的指明问题就是虽然保证了可见性,但是原子性无法保证,比如i++的字节码就是N行。 -而与锁相比,volatile是一种更轻量的同步机制,因为使用volatile不会发生上下文切换或线程调度操作,但volatile的指明问题就是虽然保证了可见性,但是原子性无法保证。 +如果一个线程正在等待锁,它不能做任何事情,如果一个线程在持有锁的情况下呗延迟执行了,例如发生了缺页错误,调度延迟,那么就没法执行。如果被阻塞的线程优先级较高,那么就会出现priority invesion的问题,被永久的阻塞下去。 -- 若一个线程正在等待锁,它不能做任何事情 -- 若一个线程在持有锁情况下被延迟执行了,如发生缺页错误,调度延迟,就没法执行 -- 若被阻塞的线程优先级较高,就会出现priority invesion问题,被永久阻塞 # 2 硬件对并发的支持 -独占锁是悲观锁,对细粒度的操作,更高效的应用是乐观锁,这种方法需要借助**冲突监测机制,来判断更新过程中是否存在来自其他线程的干扰,若存在,则失败重试**。 -几乎所有现代CPU都有某种形式的原子读-改-写指令,如compare-and-swap等,JVM就是使用这些指令来实现无锁并发。 +独占锁是悲观所,对于细粒度的操作,更高效的应用是乐观锁,这种方法需要借助**冲突监测机制来判断更新过程中是否存在来自其他线程的干扰,如果存在则失败重试**。 + +几乎所有的现代CPU都有某种形式的原子读-改-写指令,例如compare-and-swap等,JVM就是使用这些指令来实现无锁并发。 + ## 2.1 比较并交换 + CAS(Compare and set)乐观的技术。Java实现的一个compare and set如下,这是一个模拟底层的示例: + ```java @ThreadSafe public class SimulatedCAS { - @GuardedBy("this") private int value; public synchronized int get() { @@ -45,7 +47,9 @@ public class SimulatedCAS { == compareAndSwap(expectedValue, newValue)); } } + ``` + ## 2.2 非阻塞的计数器 ```java public class CasCounter { @@ -63,12 +67,14 @@ public class CasCounter { return v + 1; } } + ``` Java中使用AtomicInteger。 竞争激烈一般时,CAS性能远超基于锁的计数器。看起来他的指令更多,但无需上下文切换和线程挂起,JVM内部的代码路径实际很长,所以反而好些。 -但激烈程度较高时,开销还是较大,但会发生这种激烈程度非常高的情况只是理论,实际生产环境很难遇到。况且JIT很聪明,这种操作往往能非常大的优化。 +但激烈程度较高时,它的开销还是较大,但是你会发生这种激烈程度非常高的情况只是理论,实际生产环境很难遇到。 +况且JIT很聪明,这种操作往往能非常大的优化。 为确保正常更新,可能得将CAS操作放到for循环,从语法结构看,使用**CAS**比使用锁更加复杂,得考虑失败情况(锁会挂起线程,直到恢复)。 但基于**CAS**的原子操作,性能基本超过基于锁的计数器,即使只有很小的竞争或不存在竞争! @@ -76,19 +82,37 @@ Java中使用AtomicInteger。 在轻度到中度争用情况下,非阻塞算法的性能会超越阻塞算法,因为 CAS 的多数时间都在第一次尝试时就成功,而发生争用时的开销也不涉及**线程挂起**和**上下文切换**,只多了几个循环迭代。 没有争用的 CAS 要比没有争用的锁轻量得多(因为没有争用的锁涉及 CAS 加上额外的处理,加锁至少需要一个CAS,在有竞争的情况下,需要操作队列,线程挂起,上下文切换),而争用的 CAS 比争用的锁获取涉及更短的延迟。 -CAS的缺点是,它使用调用者来处理竞争问题,通过重试、回退、放弃,而锁能自动处理竞争问题,例如阻塞。 +CAS的缺点是它使用调用者来处理竞争问题,通过重试、回退、放弃,而锁能自动处理竞争问题,例如阻塞。 -原子变量可看做更好的volatile类型变量。AtomicInteger在JDK8里面做了改动。 -![](https://img-blog.csdnimg.cn/0f94ab5e4b6045e5aa83d99cbc9c03c4.png) +原子变量可以看做更好的volatile类型变量。 +AtomicInteger在JDK8里面做了改动。 +```java +public final int getAndIncrement() { + return unsafe.getAndAddInt(this, valueOffset, 1); +} + +``` JDK7里面的实现如下: -![](https://img-blog.csdnimg.cn/d2f94066894a4501b6dd5e6d9ad4a8c1.png) -Unsafe是经过特殊处理的,不能理解成常规的Java代码,1.8在调用getAndAddInt时,若系统底层: -- 支持fetch-and-add,则执行的就是native方法,使用fetch-and-add -- 不支持,就按照上面getAndAddInt那样,以Java代码方式执行,使用compare-and-swap +```java +public final int getAndAdd(int delta) { + for(;;) { + intcurrent= get(); + intnext=current+delta; + if(compareAndSet(current,next)) + returncurrent; + } + } + +``` +Unsafe是经过特殊处理的,不能理解成常规的Java代码,区别在于: +- 1.8在调用getAndAddInt的时候,如果系统底层支持fetch-and-add,那么它执行的就是native方法,使用的是fetch-and-add +- 如果不支持,就按照上面的所看到的getAndAddInt方法体那样,以java代码的方式去执行,使用的是compare-and-swap 这也正好跟openjdk8中Unsafe::getAndAddInt上方的注释相吻合: -以下包含在不支持本机指令的平台上使用的基于 CAS 的 Java 实现 -![](https://img-blog.csdnimg.cn/327bda8392cf4158ab94049e67f9b169.png) +```java +// The following contain CAS-based Java implementations used on +// platforms not supporting native instructions +``` # 3 原子变量类 J.U.C的AtomicXXX。 @@ -140,11 +164,18 @@ public class CasNumberRange { } } } + ``` + + # 4 非阻塞算法 + Lock-free算法,可以实现栈、队列、优先队列或者散列表。 + ## 4.1 非阻塞的栈 -Trebier算法,1986年提出。 + +Trebier算法,1986年提出的。 + ```java public class ConcurrentStack { AtomicReference> top = new AtomicReference>(); @@ -179,9 +210,13 @@ Trebier算法,1986年提出。 } } } + ``` + ## 4.2 非阻塞的链表 -J.U.C的ConcurrentLinkedQueue也是参考这个由Michael and Scott,1996年实现的算法。 + +有点复杂哦,实际J.U.C的ConcurrentLinkedQueue也是参考了这个由Michael and Scott,1996年实现的算法。 + ```java public class LinkedQueue { @@ -222,14 +257,19 @@ public class LinkedQueue { } } } + ``` + ## 4.3 原子域更新 -AtomicReferenceFieldUpdater,一个基于反射的工具类,能对指定类的指定的volatile引用字段进行原子更新。(该字段不能是private的) + +AtomicReferenceFieldUpdater,一个基于反射的工具类,它能对指定类的指定的volatile引用字段进行原子更新。(注意这个字段不能是private的) 通过调用AtomicReferenceFieldUpdater的静态方法newUpdater就能创建它的实例,该方法要接收三个参数: + * 包含该字段的对象的类 * 将被更新的对象的类 * 将被更新的字段的名称 + ```java AtomicReferenceFieldUpdater updater=AtomicReferenceFieldUpdater.newUpdater(Dog.class,String.class,"name"); Dog dog1=new Dog(); @@ -239,4 +279,5 @@ AtomicReferenceFieldUpdater updater=AtomicReferenceFieldUpdater.newUpdater(Dog.c class Dog { volatile String name="dog1"; } + ``` \ No newline at end of file diff --git "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\347\272\277\347\250\213\346\261\240ThreadPoolExecutor.md" "b/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\347\272\277\347\250\213\346\261\240ThreadPoolExecutor.md" deleted file mode 100644 index 9bba52ac2a..0000000000 --- "a/JDK/\345\271\266\345\217\221\347\274\226\347\250\213/Java\347\272\277\347\250\213\346\261\240ThreadPoolExecutor.md" +++ /dev/null @@ -1,1267 +0,0 @@ -# 1 为什么要用线程池 -## 1.1 线程the more, the better? -1、线程在java中是一个对象,更是操作系统的资源,线程创建、销毁都需要时间。 -如果创建时间+销毁时间>执行任务时间就很不合算。 -2、Java对象占用堆内存,操作系统线程占用系统内存,根据JVM规范,一个线程默认最大栈 -大小1M,这个栈空间是需要从系统内存中分配的。线程过多,会消耗很多的内存。 -3、操作系统需要频繁切换线程上下文(大家都想被运行),影响性能。 - -线程使应用能够更加充分合理地协调利用CPU、内存、网络、I/O等系统资源. -线程的创建需要开辟虚拟机栈、本地方法栈、程序计数器等线程私有的内存空间; -在线程销毁时需要回收这些系统资源. -频繁地创建和销毁线程会浪费大量的系统资源,增加并发编程风险. - -在服务器负载过大的时候,如何让新的线程等待或者友好地拒绝服务? - -这些都是线程自身无法解决的; -所以需要通过线程池协调多个线程,并实现类似主次线程隔离、定时执行、周期执行等任务. - -# 2 线程池的作用 -● 利用线程池管理并复用线程、控制最大并发数等 - -● 实现任务线程队列缓存策略和拒绝机制 - -● 实现某些与时间相关的功能 -如定时执行、周期执行等 - -● 隔离线程环境 -比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大; -因此,通过配置独立的线程池,将较慢的交易服务与搜索服务隔离开,避免各服务线程相互影响. - -在开发中,合理地使用线程池能够带来3个好处 - - **降低资源消耗** 通过重复利用已创建的线程,降低创建和销毁线程造成的系统资源消耗 - - **提高响应速度** 当任务到达时,任务可以不需要等到线程创建就能立即执行 - - **提高线程的可管理性** 线程是稀缺资源,如果过多地创建,不仅会消耗系统资源,还会降低系统的稳定性,导致使用线程池可以进行统一分配、调优和监控。 - -# 3 概念 -1、**线程池管理器** -用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务; -2、**工作线程** -线程池中线程,在没有任务时处于等待状态,可以循环的执行任务; -3、**任务接口** -每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等; -4、**任务队列** -用于存放没有处理的任务。提供缓冲机制。. - -- 原理示意图 -![](https://img-blog.csdnimg.cn/20191009015833132.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -# 4 线程池API -## 4.1 接口定义和实现类 -![](https://img-blog.csdnimg.cn/2019100901595683.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -### 继承关系图 -![线程池相关类图](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyOTA5XzQ2ODU5NjgtZWFhYWY4ZmQ4ODQ5Nzc1Ny5wbmc?x-oss-process=image/format,png) -可以认为ScheduledThreadPoolExecutor是最丰富的实现类! - -## 4.2 方法定义 -### 4.2.1 ExecutorService -![](https://img-blog.csdnimg.cn/20191009020347726.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -### 4.2.2 ScheduledExecutorService -#### public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit); -![](https://img-blog.csdnimg.cn/20191013013014872.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -#### public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit); -![](https://img-blog.csdnimg.cn/20191013013113751.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -#### 以上两种都是创建并执行一个一次性任务, 过了延迟时间就会被执行 -#### public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); - -![](https://img-blog.csdnimg.cn/20191013013412305.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -创建并执行一个周期性任务 -过了给定的初始延迟时间,会第一次被执行 -执行过程中发生了异常,那么任务就停止 - -一次任务 执行时长超过了周期时间,下一次任务会等到该次任务执行结束后,立刻执行,这也是它和`scheduleWithFixedDelay`的重要区别 - -#### public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); -创建并执行一个周期性任务 -过了初始延迟时间,第一次被执行,后续以给定的周期时间执行 -执行过程中发生了异常,那么任务就停止 - -一次任务执行时长超过了周期时间,下一 次任务 会在该次任务执 -行结束的时间基础上,计算执行延时。 -对于超过周期的长时间处理任务的不同处理方式,这是它和`scheduleAtFixedRate`的重要区别。 - -### 实例 -- 测试例子 -![](https://img-blog.csdnimg.cn/20191013153615841.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 测试实现 -![](https://img-blog.csdnimg.cn/20191013153730175.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 运行结果 -![](https://img-blog.csdnimg.cn/2019101315391641.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -可以看出超过core的线程都在等待,线程池线程数量为何达不到最大线程数呢?那这个参数还有什么意义, 让我们继续往下阅读吧! - - -### 4.2.2 Executors工具类 -你可以自己实例化线程池,也可以用`Executors`创建线程池的工厂类,常用方法如下: - -`ExecutorService` 的抽象类`AbstractExecutorService `提供了`submit`、`invokeAll` 等方法的实现; -但是核心方法`Executor.execute()`并没有在这里实现. -因为所有的任务都在该方法执行,不同实现会带来不同的执行策略. - -通过`Executors`的静态工厂方法可以创建三个线程池的包装对象 -- ForkJoinPool、 -- ThreadPoolExecutor -- ScheduledThreadPoolExecutor - -● Executors.newWorkStealingPool -JDK8 引入,创建持有足够线程的线程池支持给定的并行度; -并通过使用多个队列减少竞争; -构造方法中把CPU数量设置为默认的并行度. -返回`ForkJoinPool` ( JDK7引入)对象,它也是`AbstractExecutorService` 的子类 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyOTA2XzQ2ODU5NjgtM2I0YThlOGMxNDA4Zjg5Mi5wbmc?x-oss-process=image/format,png) - - -● Executors.newCachedThreadPool -创建的是一个无界的缓冲线程池。它的任务队列是一个同步队列。 -任务加入到池中 -- 如果池中有空闲线程,则用空闲线程执行 -- 如无, 则创建新线程执行。 - -池中的线程空闲超过60秒,将被销毁。线程数随任务的多少变化。 -`适用于执行耗时较小的异步任务`。池的核心线程数=0 ,最大线程数= Integer.MAX_ _VALUE -`maximumPoolSize` 最大可以至`Integer.MAX_VALUE`,是高度可伸缩的线程池. -若达到该上限,相信没有服务器能够继续工作,直接OOM. -`keepAliveTime` 默认为60秒; -工作线程处于空闲状态,则回收工作线程; -如果任务数增加,再次创建出新线程处理任务. - -● Executors.newScheduledThreadPool -能定时执行任务的线程池。该池的核心线程数由参数指定,线程数最大至`Integer.MAX_ VALUE`,与上述相同,存在OOM风险. -`ScheduledExecutorService`接口的实现类,支持**定时及周期性任务执行**; -相比`Timer`,` ScheduledExecutorService` 更安全,功能更强大. -与`newCachedThreadPool`的区别是**不回收工作线程**. - -● Executors.newSingleThreadExecutor -创建一个单线程的线程池,相当于单线程串行执行所有任务,保证按任务的提交顺序依次执行. -只有-个线程来执行无界任务队列的单-线程池。该线程池确保任务按加入的顺序一个一 -个依次执行。当唯一的线程因任务 异常中止时,将创建一个新的线程来继续执行 后续的任务。 -与newFixedThreadPool(1)的区别在于,单线程池的池大小在`newSingleThreadExecutor`方法中硬编码,不能再改变的。 - - -● Executors.newFixedThreadPool -创建一个固定大小任务队列容量无界的线程池 -输入的参数即是固定线程数; -既是核心线程数也是最大线程数; -不存在空闲线程,所以`keepAliveTime`等于0. -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyODE5XzQ2ODU5NjgtOGNkOTFmM2M2ZWFkYTlkZS5wbmc?x-oss-process=image/format,png) -其中使用了 LinkedBlockingQueue, 但是没有设置上限!!!,堆积过多任务!!! - -下面介绍`LinkedBlockingQueue`的构造方法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyOTEwXzQ2ODU5NjgtZmNlMjYxZGJlMzBkZWY3MS5wbmc?x-oss-process=image/format,png) -使用这样的无界队列,如果瞬间请求非常大,会有OOM的风险; -除`newWorkStealingPool` 外,其他四个创建方式都存在资源耗尽的风险. - -不推荐使用其中的任何创建线程池的方法,因为都没有任何限制,存在安全隐患. - - `Executors`中默认的线程工厂和拒绝策略过于简单,通常对用户不够友好. -线程工厂需要做创建前的准备工作,对线程池创建的线程必须明确标识,就像药品的生产批号一样,为线程本身指定有意义的名称和相应的序列号. -拒绝策略应该考虑到业务场景,返回相应的提示或者友好地跳转. -以下为简单的ThreadFactory 示例 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzk3XzQ2ODU5NjgtZDIwMjUyODdhODJhZGQ5NS5wbmc?x-oss-process=image/format,png) - -上述示例包括线程工厂和任务执行体的定义; -通过newThread方法快速、统一地创建线程任务,强调线程一定要有特定意义的名称,方便出错时回溯. - -- 单线程池:newSingleThreadExecutor()方法创建,五个参数分别是ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())。含义是池中保持一个线程,最多也只有一个线程,也就是说这个线程池是顺序执行任务的,多余的任务就在队列中排队。 -- 固定线程池:newFixedThreadPool(nThreads)方法创建 -[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NZEi0e3y-1570557031347)(https://uploadfiles.nowcoder.com/images/20190625/5088755_1561474494512_5D0DD7BCB7171E9002EAD3AEF42149E6 "图片标题")] - -池中保持nThreads个线程,最多也只有nThreads个线程,多余的任务也在队列中排队。 -[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SId8FBO1-1570557031347)(https://uploadfiles.nowcoder.com/images/20190625/5088755_1561476084467_4A47A0DB6E60853DEDFCFDF08A5CA249 "图片标题")] - -[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6uzv6UAk-1570557031348)(https://uploadfiles.nowcoder.com/images/20190625/5088755_1561476102425_FB5C81ED3A220004B71069645F112867 "图片标题")] -线程数固定且线程不超时 -- 缓存线程池:newCachedThreadPool()创建,五个参数分别是ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue())。 -含义是池中不保持固定数量的线程,随需创建,最多可以创建Integer.MAX_VALUE个线程(说一句,这个数量已经大大超过目前任何操作系统允许的线程数了),空闲的线程最多保持60秒,多余的任务在SynchronousQueue(所有阻塞、并发队列在后续文章中具体介绍)中等待。 - -为什么单线程池和固定线程池使用的任务阻塞队列是LinkedBlockingQueue(),而缓存线程池使用的是SynchronousQueue()呢? -因为单线程池和固定线程池中,线程数量是有限的,因此提交的任务需要在LinkedBlockingQueue队列中等待空余的线程;而缓存线程池中,线程数量几乎无限(上限为Integer.MAX_VALUE),因此提交的任务只需要在SynchronousQueue队列中同步移交给空余线程即可。 - -- 单线程调度线程池:newSingleThreadScheduledExecutor()创建,五个参数分别是 (1, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue())。含义是池中保持1个线程,多余的任务在DelayedWorkQueue中等待。 -- 固定调度线程池:newScheduledThreadPool(n)创建,五个参数分别是 (n, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue())。含义是池中保持n个线程,多余的任务在DelayedWorkQueue中等待。 - -有一项技术可以缓解执行时间较长任务造成的影响,即限定任务等待资源的时间,而不要无限的等待 - -先看第一个例子,测试单线程池、固定线程池和缓存线程池(注意增加和取消注释): - -``` -public class ThreadPoolExam { - public static void main(String[] args) { - //first test for singleThreadPool - ExecutorService pool = Executors.newSingleThreadExecutor(); - //second test for fixedThreadPool -// ExecutorService pool = Executors.newFixedThreadPool(2); - //third test for cachedThreadPool -// ExecutorService pool = Executors.newCachedThreadPool(); - for (int i = 0; i < 5; i++) { - pool.execute(new TaskInPool(i)); - } - pool.shutdown(); - } -} - -class TaskInPool implements Runnable { - private final int id; - - TaskInPool(int id) { - this.id = id; - } - - @Override - public void run() { - try { - for (int i = 0; i < 5; i++) { - System.out.println("TaskInPool-["+id+"] is running phase-"+i); - TimeUnit.SECONDS.sleep(1); - } - System.out.println("TaskInPool-["+id+"] is over"); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } -} -``` - -如图为排查底层公共缓存调用出错时的截图 -![有意义的线程命名](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzQ5XzQ2ODU5NjgtODU1MDI1MzM5MDZjMzNmMi5wbmc?x-oss-process=image/format,png) -绿色框采用自定义的线程工厂,明显比蓝色框默认的线程工厂创建的线程名称拥有更多的额外信息:如调用来源、线程的业务含义,有助于快速定位到死锁、StackOverflowError 等问题. - -# 5 创建线程池 -首先从`ThreadPoolExecutor`构造方法讲起,学习如何自定义`ThreadFactory`和`RejectedExecutionHandler`; -并编写一个最简单的线程池示例. -然后,通过分析`ThreadPoolExecutor`的`execute`和`addWorker`两个核心方法; -学习如何把任务线程加入到线程池中运行. - -- ThreadPoolExecutor 的构造方法如下 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzMyXzQ2ODU5NjgtYTVmOTU1Yjc5MmJkNDUzZS5wbmc?x-oss-process=image/format,png) - -- 第1个参数: corePoolSize 表示常驻核心线程数 -如果等于0,则任务执行完之后,没有任何请求进入时销毁线程池的线程; -如果大于0,即使本地任务执行完毕,核心线程也不会被销毁. -这个值的设置非常关键; -设置过大会浪费资源; -设置过小会导致线程频繁地创建或销毁. - -- 第2个参数: maximumPoolSize 表示线程池能够容纳同时执行的最大线程数 -从第1处来看,必须>=1. -如果待执行的线程数大于此值,需要借助第5个参数的帮助,缓存在队列中. -如果`maximumPoolSize = corePoolSize`,即是固定大小线程池. - -- 第3个参数: keepAliveTime 表示线程池中的线程空闲时间 -当空闲时间达到`keepAliveTime`时,线程会被销毁,直到只剩下`corePoolSize`个线程; -避免浪费内存和句柄资源. -在默认情况下,当线程池的线程数大于`corePoolSize`时,`keepAliveTime`才起作用. -但是当`ThreadPoolExecutor`的`allowCoreThreadTimeOut = true`时,核心线程超时后也会被回收. - -- 第4个参数: TimeUnit表示时间单位 -keepAliveTime的时间单位通常是TimeUnit.SECONDS. - -- 第5个参数: workQueue 表示缓存队列 -当请求的线程数大于`maximumPoolSize`时,线程进入`BlockingQueue`. -后续示例代码中使用的LinkedBlockingQueue是单向链表,使用锁来控制入队和出队的原子性; -两个锁分别控制元素的添加和获取,是一个生产消费模型队列. - -- 第6个参数: threadFactory 表示线程工厂 -它用来生产一组相同任务的线程; -线程池的命名是通过给这个factory增加组名前缀来实现的. -在虚拟机栈分析时,就可以知道线程任务是由哪个线程工厂产生的. - -- 第7个参数: handler 表示执行拒绝策略的对象 -当超过第5个参数`workQueue`的任务缓存区上限的时候,即可通过该策略处理请求,属于一种简单的限流保护。 -友好的拒绝策略可以是如下: -1. 保存到数据库进行削峰填谷,空闲时再提取出来执行 -2. 转向某个提示页面 -3. 打印日志 - -### 2.1.1 corePoolSize(核心线程数量) -线程池中应该保持的主要线程的数量.即使线程处于空闲状态,除非设置了`allowCoreThreadTimeOut`这个参数,当提交一个任务到线程池时,若线程数量Integer 有32位; -最右边29位表工作线程数; -最左边3位表示线程池状态,可表示从0至7的8个不同数值 -线程池的状态用高3位表示,其中包括了符号位. -五种状态的十进制值按从小到大依次排序为 -RUNNING < SHUTDOWN < STOP < TIDYING =核心线程数 或线程创建失败,则将当前任务放到工作队列中 - // 只有线程池处于 RUNNING 态,才执行后半句 : 置入队列 - if (isRunning(c) && workQueue.offer(command)) { - int recheck = ctl.get(); - - // 只有线程池处于 RUNNING 态,才执行后半句 : 置入队列 - if (! isRunning(recheck) && remove(command)) - reject(command); - // 若之前的线程已被消费完,新建一个线程 - else if (workerCountOf(recheck) == 0) - addWorker(null, false); - // 核心线程和队列都已满,尝试创建一个新线程 - } - else if (!addWorker(command, false)) - // 抛出RejectedExecutionException异常 - // 若 addWorker 返回是 false,即创建失败,则唤醒拒绝策略. - reject(command); - } -``` -发生拒绝的理由有两个 -( 1 )线程池状态为非RUNNING状态 -(2)等待队列已满。 - -下面继续分析`addWorker` - -## addWorker 源码解析 - -根据当前线程池状态,检查是否可以添加新的任务线程,若可以则创建并启动任务; -若一切正常则返回true; -返回false的可能性如下 -1. 线程池没有处于`RUNNING`态 -2. 线程工厂创建新的任务线程失败 -### 参数 -- firstTask -外部启动线程池时需要构造的第一个线程,它是线程的母体 -- core -新增工作线程时的判断指标 - - true -需要判断当前`RUNNING`态的线程是否少于`corePoolsize` - - false -需要判断当前`RUNNING`态的线程是否少于`maximumPoolsize` -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUzMjIwXzQ2ODU5NjgtMjg2MDRmYjVkYTE5MjJlNC5wbmc?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzg5XzQ2ODU5NjgtOTk1ZmFlOTQyOTQwMjFjNy5wbmc?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyODMwXzQ2ODU5NjgtM2Y3NzViOWQ1MThmMzc4My5wbmc?x-oss-process=image/format,png) - -这段代码晦涩难懂,部分地方甚至违反代码规约,但其中蕴含丰富的编码知识点 - -- 第1处,配合循环语句出现的label,类似于goto 作用 -label 定义时,必须把标签和冒号的组合语句紧紧相邻定义在循环体之前,否则会编译出错. -目的是 在实现多重循环时能够快速退出到任何一层; -出发点似乎非常贴心,但在大型软件项目中,滥用标签行跳转的后果将是灾难性的. -示例代码中在`retry`下方有两个无限循环; -在`workerCount`加1成功后,直接退出两层循环. - -- 第2处,这样的表达式不利于阅读,应如是 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzg1XzQ2ODU5NjgtMDg2ZTlkNWY5ZGEyYWZkNC5wbmc?x-oss-process=image/format,png) - -- 第3处,与第1处的标签呼应,`AtomicInteger`对象的加1操作是原子性的; -`break retry`表 直接跳出与`retry` 相邻的这个循环体 - -- 第4处,此`continue`跳转至标签处,继续执行循环. -如果条件为false,则说明线程池还处于运行状态,即继续在`for(;)`循环内执行. - -- 第5处,`compareAndIncrementWorkerCount `方法执行失败的概率非常低. -即使失败,再次执行时成功的概率也是极高的,类似于自旋原理. -这里是先加1,创建失败再减1,这是轻量处理并发创建线程的方式; -如果先创建线程,成功再加1,当发现超出限制后再销毁线程,那么这样的处理方式明显比前者代价要大. - -- 第6处,`Worker `对象是工作线程的核心类实现,部分源码如下 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUzMjMyXzQ2ODU5NjgtYzkzNTI3ODJjNTZjM2Q2Ny5wbmc?x-oss-process=image/format,png) -它实现了`Runnable`接口,并把本对象作为参数输入给`run()`中的`runWorker (this)`; -所以内部属性线程`thread`在`start`的时候,即会调用`runWorker`. - -# 总结 -线程池的相关源码比较精炼,还包括线程池的销毁、任务提取和消费等,与线程状态图一样,线程池也有自己独立的状态转化流程,本节不再展开。 -总结一下,使用线程池要注意如下几点: -(1)合理设置各类参数,应根据实际业务场景来设置合理的工作线程数。 -(2)线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 -(3)创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。 - -线程池不允许使用Executors,而是通过ThreadPoolExecutor的方式创建,这样的处理方式能更加明确线程池的运行规则,规避资源耗尽的风险。 - - - - - -进一步查看源码发现,这些方法最终都调用了ThreadPoolExecutor和ScheduledThreadPoolExecutor的构造函数 -而ScheduledThreadPoolExecutor继承自ThreadPoolExecutor - -## 0.2 ThreadPoolExecutor 自定义线程池 -[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5A6eRvc8-1570557031390)(https://uploadfiles.nowcoder.com/images/20190625/5088755_1561476436402_10FB15C77258A991B0028080A64FB42D "图片标题")] -它们都是某种线程池,可以控制线程创建,释放,并通过某种策略尝试复用线程去执行任务的一个管理框架 - -,因此最终所有线程池的构造函数都调用了Java5后推出的ThreadPoolExecutor的如下构造函数 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyODEwXzQ2ODU5NjgtYmY0MTAwOTU5Nzk4NjA1OC5wbmc?x-oss-process=image/format,png) - -## Java默认提供的线程池 -Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUzMDc1XzQ2ODU5NjgtNGYxOGI1ZTk2ZWIxZDkzMC5wbmc?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyOTg5XzQ2ODU5NjgtYjdlYzU5YTgwMDQ0MmIyNi5wbmc?x-oss-process=image/format,png) - -我们只需要将待执行的方法放入 run 方法中,将 Runnable 接口的实现类交给线程池的 -execute 方法,作为他的一个参数,比如: -```java -Executor e=Executors.newSingleThreadExecutor(); -e.execute(new Runnable(){ //匿名内部类 public void run(){ -//需要执行的任务 -} -}); - -``` -# 线程池原理 - 任务execute过程 - - 流程图 -![](https://img-blog.csdnimg.cn/20191014020916959.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 示意图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzMwXzQ2ODU5NjgtYTA3YjhiMzIzMzMxYzE1ZS5wbmc?x-oss-process=image/format,png) - -ThreadPoolExecutor执行execute()分4种情况 - - 若当前运行的线程少于`corePoolSize`,则创建新线程来执行任务(该步需要获取全局锁) - - 若运行的线程多于或等于`corePoolSize`,且工作队列没满,则将新提交的任务存储在工作队列里。即, 将任务加入`BlockingQueue` - - 若无法将任务加入`BlockingQueue`,且没达到线程池最大数量, 则创建新的线程来处理任务(该步需要获取全局锁) - - 若创建新线程将使当前运行的线程超出`maximumPoolSize`,任务将被拒绝,并调用`RejectedExecutionHandler.rejectedExecution()` - -采取上述思路,是为了在执行`execute()`时,尽可能避免获取全局锁 -在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁 - -## 实例 -![](https://img-blog.csdnimg.cn/20191013231005279.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- 结果 -![](https://img-blog.csdnimg.cn/20191014015653362.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -# **源码分析** -```java - /** - * 检查是否可以根据当前池状态和给定的边界(核心或最大) - * 添加新工作线程。如果是这样,工作线程数量会相应调整,如果可能的话,一个新的工作线程创建并启动 - * 将firstTask作为其运行的第一项任务。 - * 如果池已停止此方法返回false - * 如果线程工厂在被访问时未能创建线程,也返回false - * 如果线程创建失败,或者是由于线程工厂返回null,或者由于异常(通常是在调用Thread.start()后的OOM)),我们干净地回滚。 - * - * @param core if true use corePoolSize as bound, else - * maximumPoolSize. (A boolean indicator is used here rather than a - * value to ensure reads of fresh values after checking other pool - * state). - * @return true if successful - */ - private boolean addWorker(Runnable firstTask, boolean core) { - retry: - for (;;) { - int c = ctl.get(); - int rs = runStateOf(c); - - - /** - * Check if queue empty only if necessary. - * - * 如果线程池已关闭,并满足以下条件之一,那么不创建新的 worker: - * 1. 线程池状态大于 SHUTDOWN,也就是 STOP, TIDYING, 或 TERMINATED - * 2. firstTask != null - * 3. workQueue.isEmpty() - * 简单分析下: - * 状态控制的问题,当线程池处于 SHUTDOWN ,不允许提交任务,但是已有任务继续执行 - * 当状态大于 SHUTDOWN ,不允许提交任务,且中断正在执行任务 - * 多说一句:若线程池处于 SHUTDOWN,但 firstTask 为 null,且 workQueue 非空,是允许创建 worker 的 - * - */ - if (rs >= SHUTDOWN && - ! (rs == SHUTDOWN && - firstTask == null && - ! workQueue.isEmpty())) - return false; - - for (;;) { - int wc = workerCountOf(c); - if (wc >= CAPACITY || - wc >= (core ? corePoolSize : maximumPoolSize)) - return false; - // 如果成功,那么就是所有创建线程前的条件校验都满足了,准备创建线程执行任务 - // 这里失败的话,说明有其他线程也在尝试往线程池中创建线程 - if (compareAndIncrementWorkerCount(c)) - break retry; - // 由于有并发,重新再读取一下 ctl - c = ctl.get(); // Re-read ctl - // 正常如果是 CAS 失败的话,进到下一个里层的for循环就可以了 - // 可如果是因为其他线程的操作,导致线程池的状态发生了变更,如有其他线程关闭了这个线程池 - // 那么需要回到外层的for循环 - if (runStateOf(c) != rs) - continue retry; - // else CAS failed due to workerCount change; retry inner loop - } - } - - /* * - * 到这里,我们认为在当前这个时刻,可以开始创建线程来执行任务 - */ - - // worker 是否已经启动 - boolean workerStarted = false; - // 是否已将这个 worker 添加到 workers 这个 HashSet 中 - boolean workerAdded = false; - Worker w = null; - try { - // 把 firstTask 传给 worker 的构造方法 - w = new Worker(firstTask); - // 取 worker 中的线程对象,Worker的构造方法会调用 ThreadFactory 来创建一个新的线程 - final Thread t = w.thread; - if (t != null) { - //先加锁 - final ReentrantLock mainLock = this.mainLock; - // 这个是整个类的全局锁,持有这个锁才能让下面的操作“顺理成章”, - // 因为关闭一个线程池需要这个锁,至少我持有锁的期间,线程池不会被关闭 - mainLock.lock(); - try { - // Recheck while holding lock. - // Back out on ThreadFactory failure or if - // shut down before lock acquired. - int rs = runStateOf(ctl.get()); - - // 小于 SHUTTDOWN 即 RUNNING - // 如果等于 SHUTDOWN,不接受新的任务,但是会继续执行等待队列中的任务 - if (rs < SHUTDOWN || - (rs == SHUTDOWN && firstTask == null)) { - // worker 里面的 thread 不能是已启动的 - if (t.isAlive()) // precheck that t is startable - throw new IllegalThreadStateException(); - // 加到 workers 这个 HashSet 中 - workers.add(w); - int s = workers.size(); - if (s > largestPoolSize) - largestPoolSize = s; - workerAdded = true; - } - } finally { - mainLock.unlock(); - } - // 若添加成功 - if (workerAdded) { - // 启动线程 - t.start(); - workerStarted = true; - } - } - } finally { - // 若线程没有启动,做一些清理工作,若前面 workCount 加了 1,将其减掉 - if (! workerStarted) - addWorkerFailed(w); - } - // 返回线程是否启动成功 - return workerStarted; - } -``` -看下 `addWorkFailed` -![workers 中删除掉相应的 worker,workCount 减 1 -private void addWor](https://upload-images.jianshu.io/upload_images/4685968-77abdc7bff21cca6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -![记录 workers 中的个数的最大值,因为 workers 是不断增加减少的,通过这个值可以知道线程池的大小曾经达到的最大值](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzA4XzQ2ODU5NjgtMDc4NDcyYjY4MmZjYzljZC5wbmc?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUzNjMzXzQ2ODU5NjgtMzNmNTE0NTc3ZTk3ZGMzNS5wbmc?x-oss-process=image/format,png) - - - - -`worker` 中的线程 `start` 后,其 `run` 方法会调用 `runWorker ` -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyODgwXzQ2ODU5NjgtYTAwOWJjMDJhMjI0ZGNlMi5wbmc?x-oss-process=image/format,png) -继续往下看 `runWorker` -```java -// worker 线程启动后调用,while 循环(即自旋!)不断从等待队列获取任务并执行 -// worker 初始化时,可指定 firstTask,那么第一个任务也就可以不需要从队列中获取 -final void runWorker(Worker w) { - Thread wt = Thread.currentThread(); - // 该线程的第一个任务(若有) - Runnable task = w.firstTask; - w.firstTask = null; - // 允许中断 - w.unlock(); - - boolean completedAbruptly = true; - try { - // 循环调用 getTask 获取任务 - while (task != null || (task = getTask()) != null) { - w.lock(); - // 若线程池状态大于等于 STOP,那么意味着该线程也要中断 - /** - * 若线程池STOP,请确保线程 已被中断 - * 如果没有,请确保线程未被中断 - * 这需要在第二种情况下进行重新检查,以便在关中断时处理shutdownNow竞争 - */ - if ((runStateAtLeast(ctl.get(), STOP) || - (Thread.interrupted() && - runStateAtLeast(ctl.get(), STOP))) && - !wt.isInterrupted()) - wt.interrupt(); - try { - // 这是一个钩子方法,留给需要的子类实现 - beforeExecute(wt, task); - Throwable thrown = null; - try { - // 到这里终于可以执行任务了 - task.run(); - } catch (RuntimeException x) { - thrown = x; throw x; - } catch (Error x) { - thrown = x; throw x; - } catch (Throwable x) { - // 这里不允许抛出 Throwable,所以转换为 Error - thrown = x; throw new Error(x); - } finally { - // 也是一个钩子方法,将 task 和异常作为参数,留给需要的子类实现 - afterExecute(task, thrown); - } - } finally { - // 置空 task,准备 getTask 下一个任务 - task = null; - // 累加完成的任务数 - w.completedTasks++; - // 释放掉 worker 的独占锁 - w.unlock(); - } - } - completedAbruptly = false; - } finally { - // 到这里,需要执行线程关闭 - // 1. 说明 getTask 返回 null,也就是说,这个 worker 的使命结束了,执行关闭 - // 2. 任务执行过程中发生了异常 - // 第一种情况,已经在代码处理了将 workCount 减 1,这个在 getTask 方法分析中说 - // 第二种情况,workCount 没有进行处理,所以需要在 processWorkerExit 中处理 - processWorkerExit(w, completedAbruptly); - } -} -``` -看看 `getTask() ` -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzgwXzQ2ODU5NjgtNWU5NDc3MzE5M2Q5Y2Y0OS5wbmc?x-oss-process=image/format,png) -```java -// 此方法有三种可能 -// 1. 阻塞直到获取到任务返回。默认 corePoolSize 之内的线程是不会被回收的,它们会一直等待任务 -// 2. 超时退出。keepAliveTime 起作用的时候,也就是如果这么多时间内都没有任务,那么应该执行关闭 -// 3. 如果发生了以下条件,须返回 null -// 池中有大于 maximumPoolSize 个 workers 存在(通过调用 setMaximumPoolSize 进行设置) -// 线程池处于 SHUTDOWN,而且 workQueue 是空的,前面说了,这种不再接受新的任务 -// 线程池处于 STOP,不仅不接受新的线程,连 workQueue 中的线程也不再执行 -private Runnable getTask() { - boolean timedOut = false; // Did the last poll() time out? - - for (;;) { - // 允许核心线程数内的线程回收,或当前线程数超过了核心线程数,那么有可能发生超时关闭 - - // 这里 break,是为了不往下执行后一个 if (compareAndDecrementWorkerCount(c)) - // 两个 if 一起看:如果当前线程数 wc > maximumPoolSize,或者超时,都返回 null - // 那这里的问题来了,wc > maximumPoolSize 的情况,为什么要返回 null? - // 换句话说,返回 null 意味着关闭线程。 - // 那是因为有可能开发者调用了 setMaximumPoolSize 将线程池的 maximumPoolSize 调小了 - - // 如果此 worker 发生了中断,采取的方案是重试 - // 解释下为什么会发生中断,这个读者要去看 setMaximumPoolSize 方法, - // 如果开发者将 maximumPoolSize 调小了,导致其小于当前的 workers 数量, - // 那么意味着超出的部分线程要被关闭。重新进入 for 循环,自然会有部分线程会返回 null - int c = ctl.get(); - int rs = runStateOf(c); - - // Check if queue empty only if necessary. - if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { - // CAS 操作,减少工作线程数 - decrementWorkerCount(); - return null; - } - - int wc = workerCountOf(c); - - // Are workers subject to culling? - boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; - - if ((wc > maximumPoolSize || (timed && timedOut)) - && (wc > 1 || workQueue.isEmpty())) { - if (compareAndDecrementWorkerCount(c)) - return null; - continue; - } - - try { - Runnable r = timed ? - workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : - workQueue.take(); - if (r != null) - return r; - timedOut = true; - } catch (InterruptedException retry) { - // 如果此 worker 发生了中断,采取的方案是重试 - // 解释下为什么会发生中断,这个读者要去看 setMaximumPoolSize 方法, - // 如果开发者将 maximumPoolSize 调小了,导致其小于当前的 workers 数量, - // 那么意味着超出的部分线程要被关闭。重新进入 for 循环,自然会有部分线程会返回 null - timedOut = false; - } - } -} -``` -到这里,基本上也说完了整个流程,回到 execute(Runnable command) 方法,看看各个分支,我把代码贴过来一下: -```java -/** - * Executes the given task sometime in the future. The task - * may execute in a new thread or in an existing pooled thread. - * - * If the task cannot be submitted for execution, either because this - * executor has been shutdown or because its capacity has been reached, - * the task is handled by the current {@code RejectedExecutionHandler}. - * - * @param command the task to execute - * @throws RejectedExecutionException at discretion of - * {@code RejectedExecutionHandler}, if the task - * cannot be accepted for execution - * @throws NullPointerException if {@code command} is null - */ - public void execute(Runnable command) { - if (command == null) - throw new NullPointerException(); - /* - * Proceed in 3 steps: - * - * 1. If fewer than corePoolSize threads are running, try to - * start a new thread with the given command as its first - * task. The call to addWorker atomically checks runState and - * workerCount, and so prevents false alarms that would add - * threads when it shouldn't, by returning false. - * - * 2. If a task can be successfully queued, then we still need - * to double-check whether we should have added a thread - * (because existing ones died since last checking) or that - * the pool shut down since entry into this method. So we - * recheck state and if necessary roll back the enqueuing if - * stopped, or start a new thread if there are none. - * - * 3. If we cannot queue task, then we try to add a new - * thread. If it fails, we know we are shut down or saturated - * and so reject the task. - */ - //表示 “线程池状态” 和 “线程数” 的整数 - int c = ctl.get(); - // 如果当前线程数少于核心线程数,直接添加一个 worker 执行任务, - // 创建一个新的线程,并把当前任务 command 作为这个线程的第一个任务(firstTask) - if (workerCountOf(c) < corePoolSize) { - // 添加任务成功,即结束 - // 执行的结果,会包装到 FutureTask - // 返回 false 代表线程池不允许提交任务 - if (addWorker(command, true)) - return; - - c = ctl.get(); - } - - // 到这说明,要么当前线程数大于等于核心线程数,要么刚刚 addWorker 失败 - - // 如果线程池处于 RUNNING ,把这个任务添加到任务队列 workQueue 中 - if (isRunning(c) && workQueue.offer(command)) { - /* 若任务进入 workQueue,我们是否需要开启新的线程 - * 线程数在 [0, corePoolSize) 是无条件开启新线程的 - * 若线程数已经大于等于 corePoolSize,则将任务添加到队列中,然后进到这里 - */ - int recheck = ctl.get(); - // 若线程池不处于 RUNNING ,则移除已经入队的这个任务,并且执行拒绝策略 - if (! isRunning(recheck) && remove(command)) - reject(command); - // 若线程池还是 RUNNING ,且线程数为 0,则开启新的线程 - // 这块代码的真正意图:担心任务提交到队列中了,但是线程都关闭了 - else if (workerCountOf(recheck) == 0) - addWorker(null, false); - } - // 若 workQueue 满,到该分支 - // 以 maximumPoolSize 为界创建新 worker, - // 若失败,说明当前线程数已经达到 maximumPoolSize,执行拒绝策略 - else if (!addWorker(command, false)) - reject(command); - } -``` -**工作线程**:线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会循环获取工作队列里的任务来执行.我们可以从Worker类的run()方法里看到这点 - -```java - public void run() { - try { - Runnable task = firstTask; - firstTask = null; - while (task != null || (task = getTask()) != null) { - runTask(task); - task = null; - } - } finally { - workerDone(this); - } - } - boolean workerStarted = false; - boolean workerAdded = false; - Worker w = null; - try { - w = new Worker(firstTask); - - final Thread t = w.thread; - if (t != null) { - //先加锁 - final ReentrantLock mainLock = this.mainLock; - mainLock.lock(); - try { - // Recheck while holding lock. - // Back out on ThreadFactory failure or if - // shut down before lock acquired. - int rs = runStateOf(ctl.get()); - - if (rs < SHUTDOWN || - (rs == SHUTDOWN && firstTask == null)) { - if (t.isAlive()) // precheck that t is startable - throw new IllegalThreadStateException(); - workers.add(w); - int s = workers.size(); - if (s > largestPoolSize) - largestPoolSize = s; - workerAdded = true; - } - } finally { - mainLock.unlock(); - } - if (workerAdded) { - t.start(); - workerStarted = true; - } - } - } finally { - if (! workerStarted) - addWorkerFailed(w); - } - return workerStarted; - } -``` -线程池中的线程执行任务分两种情况 - - 在execute()方法中创建一个线程时,会让这个线程执行当前任务 - - 这个线程执行完上图中 1 的任务后,会反复从BlockingQueue获取任务来执行 - -# 线程池的使用 - -## 向线程池提交任务 - 可以使用两个方法向线程池提交任务 -### execute() -用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功.通过以下代码可知execute()方法输入的任务是一个Runnable类的实例. -```java - threadsPool.execute(new Runnable() { - @Override - public void run() { - // TODO Auto-generated method stub - } - }); -``` -从运行结果可以看出,单线程池中的线程是顺序执行的。固定线程池(参数为2)中,永远最多只有两个线程并发执行。缓存线程池中,所有线程都并发执行。 -第二个例子,测试单线程调度线程池和固定调度线程池。 - -```java -public class ScheduledThreadPoolExam { - public static void main(String[] args) { - //first test for singleThreadScheduledPool - ScheduledExecutorService scheduledPool = Executors.newSingleThreadScheduledExecutor(); - //second test for scheduledThreadPool -// ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2); - for (int i = 0; i < 5; i++) { - scheduledPool.schedule(new TaskInScheduledPool(i), 0, TimeUnit.SECONDS); - } - scheduledPool.shutdown(); - } -} - -class TaskInScheduledPool implements Runnable { - private final int id; - - TaskInScheduledPool(int id) { - this.id = id; - } - - @Override - public void run() { - try { - for (int i = 0; i < 5; i++) { - System.out.println("TaskInScheduledPool-["+id+"] is running phase-"+i); - TimeUnit.SECONDS.sleep(1); - } - System.out.println("TaskInScheduledPool-["+id+"] is over"); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } -} -``` -从运行结果可以看出,单线程调度线程池和单线程池类似,而固定调度线程池和固定线程池类似。 -总结: - -- 如果没有特殊要求,使用缓存线程池总是合适的; -- 如果只能运行一个线程,就使用单线程池。 -- 如果要运行调度任务,则按需使用调度线程池或单线程调度线程池 -- 如果有其他特殊要求,则可以直接使用ThreadPoolExecutor类的构造函数来创建线程池,并自己给定那五个参数。 - -### submit() -用于提交需要返回值的任务.线程池会返回一个future类型对象,通过此对象可以判断任务是否执行成功 -并可通过get()获取返回值,get()会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候可能任务没有执行完. - -```java - Future future = executor.submit(harReturnValuetask); - try { - Object s = future.get(); - } catch (InterruptedException e) { - // 处理中断异常 - } catch (ExecutionException e) { - // 处理无法执行任务异常 - } finally { - // 关闭线程池 - executor.shutdown(); - } -``` -## 关闭线程池 -可通过调用线程池的**shutdown**或**shutdownNow**方法来关闭线程池. -它们的原理是遍历线程池中的工作线程,然后逐个调用线程的**interrupt**方法来中断线程,所以无法响应中断的任务可能永远无法终止. -但是它们存在一定的区别 - - - **shutdownNow**首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表 - - **shutdown**只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程. - -只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true. -当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true. -至于应该调用哪一种方法,应该由提交到线程池的任务的特性决定,通常调用shutdown方法来关闭线程池,若任务不一定要执行完,则可以调用shutdownNow方法. - -## 合理配置 - -要想合理地配置线程池,就必须首先 - -### 分析任务特性 - -可从以下几个角度来分析 - - 任务的性质:CPU密集型任务、IO密集型任务和混合型任务 - - 任务的优先级:高、中和低 - - 任务的执行时间:长、中和短 - - 任务的依赖性:是否依赖其他系统资源,如数据库连接。 - -### 任务性质 -可用不同规模的线程池分开处理 - -#### CPU密集型任务(计算型任务) -应配置尽可能小的线程,配置 - ` N(CPU)+1 `或 `N(CPU) * 2` - -#### I/O密集型任务 -相对比计算型任务,需多一些线程,根据具体 I/O 阻塞时长考量 - -> 如Tomcat中默认最大线程数: 200。 - -也可考虑根据需要在一个最小数量和最大数量间自动增减线程数。 - -业务读取较多,线程并不是一直在执行任务,则应配置尽可能多的线程 -`N(CPU)/1 - 阻塞系数(0.8~0.9)` - -一般,生产环境下,CPU使用率达到80,说明被充分利用 - -#### 混合型的任务 -如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量.如果这两个任务执行时间相差太大,则没必要进行分解. - -可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数. - -优先级不同的任务可以使用PriorityBlockingQueue处理.它可以让优先级高 -的任务先执行. - -> 注意 如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行 - -执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行. - -依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU. - -**建议使用有界队列** 有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千. -假如系统里后台任务线程池的队列和线程池全满了,不断抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞,任务积压在线程池里. -如果我们设置成无界队列,那么线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题. -## 2.5 线程池的监控 -如果在系统中大量使用线程池,则有必要对线程池进行监控,方便在出现问题时,可以根据线程池的使用状况快速定位问题.可通过线程池提供的参数进行监控,在监控线程池的时候可以使用以下属性: - - - taskCount:线程池需要执行的任务数量 - - completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。 - - largestPoolSize:线程池里曾经创建过的最大线程数量.通过这个数据可以知道线程池是否曾经满过.如该数值等于线程池的最大大小,则表示线程池曾经满过. - - getPoolSize:线程池的线程数量.如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减. - - getActiveCount:获取活动的线程数. - -通过扩展线程池进行监控.可以通过继承线程池来自定义线程池,重写线程池的 -beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控.例如,监控任务的平均执行时间、最大执行时间和最小执行时间等. -这几个方法在线程池里是空方法. -```java -protected void beforeExecute(Thread t, Runnable r) { } -``` -## 2.6 线程池的状态 -1.当线程池创建后,初始为 running 状态 -2.调用 shutdown 方法后,处 shutdown 状态,此时不再接受新的任务,等待已有的任务执行完毕 -3.调用 shutdownnow 方法后,进入 stop 状态,不再接受新的任务,并且会尝试终止正在执行的任务。 -4.当处于 shotdown 或 stop 状态,并且所有工作线程已经销毁,任务缓存队列已清空,线程池被设为 terminated 状态。 - -# 总结 -## java 线程池有哪些关键属性? -- corePoolSize 到 maximumPoolSize 之间的线程会被回收,当然 corePoolSize 的线程也可以通过设置而得到回收(allowCoreThreadTimeOut(true))。 -- workQueue 用于存放任务,添加任务的时候,如果当前线程数超过了 corePoolSize,那么往该队列中插入任务,线程池中的线程会负责到队列中拉取任务。 -- keepAliveTime 用于设置空闲时间,如果线程数超出了 corePoolSize,并且有些线程的空闲时间超过了这个值,会执行关闭这些线程的操作 -- rejectedExecutionHandler 用于处理当线程池不能执行此任务时的情况,默认有抛出 RejectedExecutionException 异常、忽略任务、使用提交任务的线程来执行此任务和将队列中等待最久的任务删除,然后提交此任务这四种策略,默认为抛出异常。 -##线程池中的线程创建时机? -- 如果当前线程数少于 corePoolSize,那么提交任务的时候创建一个新的线程,并由这个线程执行这个任务; -- 如果当前线程数已经达到 corePoolSize,那么将提交的任务添加到队列中,等待线程池中的线程去队列中取任务; -- 如果队列已满,那么创建新的线程来执行任务,需要保证池中的线程数不会超过 maximumPoolSize,如果此时线程数超过了 maximumPoolSize,那么执行拒绝策略。 - -## 任务执行过程中发生异常怎么处理? -如果某个任务执行出现异常,那么执行任务的线程会被关闭,而不是继续接收其他任务。然后会启动一个新的线程来代替它。 - -## 什么时候会执行拒绝策略? -- workers 的数量达到了 corePoolSize,任务入队成功,以此同时线程池被关闭了,而且关闭线程池并没有将这个任务出队,那么执行拒绝策略。这里说的是非常边界的问题,入队和关闭线程池并发执行,读者仔细看看 execute 方法是怎么进到第一个 reject(command) 里面的。 -- workers 的数量大于等于 corePoolSize,准备入队,可是队列满了,任务入队失败,那么准备开启新的线程,可是线程数已经达到 maximumPoolSize,那么执行拒绝策略。 - -# 参考 -- 《码出高效》 - -- 《Java并发编程的艺术》 \ No newline at end of file diff --git "a/Python/\347\210\254\350\231\253/python\351\253\230\347\272\247\347\210\254\350\231\253\345\256\236\346\210\230\344\271\213Headers\344\277\241\346\201\257\346\240\241\351\252\214-Cookie.md" "b/Python/\347\210\254\350\231\253/python\351\253\230\347\272\247\347\210\254\350\231\253\345\256\236\346\210\230\344\271\213Headers\344\277\241\346\201\257\346\240\241\351\252\214-Cookie.md" deleted file mode 100644 index fccea8ba37..0000000000 --- "a/Python/\347\210\254\350\231\253/python\351\253\230\347\272\247\347\210\254\350\231\253\345\256\236\346\210\230\344\271\213Headers\344\277\241\346\201\257\346\240\241\351\252\214-Cookie.md" +++ /dev/null @@ -1,68 +0,0 @@ -### python高级爬虫实战之Headers信息校验-Cookie - -#### 一、什么是cookie - -​ 上期我们了解了User-Agent,这期我们来看下如何利用Cookie进行用户模拟登录从而进行网站数据的爬取。 - -首先让我们来了解下什么是Cookie: - -​ Cookie指某些网站为了辨别用户身份、从而储存在用户本地终端上的数据。当客户端在第一次请求网站指定的首页或登录页进行登录之后,服务器端会返回一个Cookie值给客户端。如果客户端为浏览器,将自动将返回的cookie存储下来。当再次访问改网页的其他页面时,自动将cookie值在Headers里传递过去,服务器接受值后进行验证,如合法处理请求,否则拒绝请求。 - -### 二、如何利用cookie - -​ 举个例子我们要去微博爬取相关数据,首先我们会遇到登录的问题,当然我们可以利用python其他的功能模块进行模拟登录,这里可能会涉及到验证码等一些反爬手段。 - -![截屏2024-03-04 下午7.53.55](https://s2.loli.net/2024/03/04/j7RxseHBKSlGMD5.png) - -换个思路,我们登录好了,通过开发者工具“右击” 检查(或者按F12) 获取到对应的cookie,那我们就可以绕个登录的页面,利用cookie继续用户模拟操作从而直接进行操作了。 - -![截屏2024-03-04 下午8.02.39](https://s2.loli.net/2024/03/04/qLygJpvH6RYTlzE.png) - -利用cookie实现模拟登录的两种方法: - -- [ ] 将cookie插入Headers请求头 - - ``` - Headers={"cookie":"复制的cookie值"} - ``` - - - -- [ ] 将cookie直接作为requests方法的参数 - -``` -cookie={"cookie":"复制的cookie值"} -requests.get(url,cookie=cookie) -``` - -#### 三、利用selenium获取cookie,实现用户模拟登录 - -实现方法:利用selenium模拟浏览器操作,输入用户名,密码 或扫码进行登录,获取到登录的cookie保存成文件,加载文件解析cookie实现用户模拟登录。 - -```python -from selenium import webdriver -from time import sleep -import json -#selenium模拟浏览器获取cookie -def getCookie: - driver = webdriver.Chrome() - driver.maximize_window() - driver.get('https://weibo.co m/login.php') - sleep(20) # 留时间进行扫码 - Cookies = driver.get_cookies() # 获取list的cookies - jsCookies = json.dumps(Cookies) # 转换成字符串保存 - with open('cookies.txt', 'w') as f: - f.write(jsCookies) - -def login: - filename = 'cookies.txt' - #创建MozillaCookieJar实例对象 - cookie = cookiejar.MozillaCookieJar() - #从文件中读取cookie内容到变量 - cookie.load(filename, ignore_discard=True, ignore_expires=True) - response = requests.get('https://weibo.co m/login.php',cookie=cookie) -``` - -#### 四、拓展思考 - -​ 如果频繁使用一个账号进行登录爬取网站数据有可能导致服务器检查到异常,对当前账号进行封禁,这边我们就需要考虑cookie池的引入了。 \ No newline at end of file diff --git "a/Python/\347\210\254\350\231\253/python\351\253\230\347\272\247\347\210\254\350\231\253\345\256\236\346\210\230\344\271\213Headers\344\277\241\346\201\257\346\240\241\351\252\214-User-Agent.md" "b/Python/\347\210\254\350\231\253/python\351\253\230\347\272\247\347\210\254\350\231\253\345\256\236\346\210\230\344\271\213Headers\344\277\241\346\201\257\346\240\241\351\252\214-User-Agent.md" deleted file mode 100644 index 0fa29af29d..0000000000 --- "a/Python/\347\210\254\350\231\253/python\351\253\230\347\272\247\347\210\254\350\231\253\345\256\236\346\210\230\344\271\213Headers\344\277\241\346\201\257\346\240\241\351\252\214-User-Agent.md" +++ /dev/null @@ -1,61 +0,0 @@ -### python高级爬虫实战之Headers信息校验-User-Agent - -​ User-agent 是当前网站反爬策略中最基础的一种反爬技术,服务器通过接收请求头中的user-agen的值来判断是否为正常用户访问还是爬虫程序。 - -​ 下面举一个简单的例子 爬取我们熟悉的豆瓣网: - -```python -import requests -url='https://movie.douban.com/' -resp=requests.get(url) -print(resp.status_code) -``` - -运行结果得到status_code:418 - -说明我们爬虫程序已经被服务器所拦截,无法正常获取相关网页数据。 - -我们可以通过返回的状态码来了解服务器的相应情况 - -- 100–199:信息反馈 -- 200–299:成功反馈 -- 300–399:重定向消息 -- 400–499:客户端错误响应 -- 500–599:服务器错误响应 - -现在我们利用google chrome浏览器来打开豆瓣网,查看下网页。 - -正常打开网页后,我们在页面任意地方右击“检查” 打开开发者工具。 - -image-20240301205014592 - - - -选择:Network-在Name中随便找一个文件点击后,右边Headers显示内容,鼠标拉到最下面。 - -![截屏2024-03-01 下午8.53.05](https://s2.loli.net/2024/03/01/XdjyBL5ClIYnT9F.png) - -User-Agent: - -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 - -我们把这段带到程序中再试下看效果如何。 - -```python -import requests -url='https://movie.douban.com/' -headers={ -"user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36" -} -resp=requests.get(url,headers=headers) -print(resp.status_code) -``` - -完美,执行后返回状态码200 ,说明已经成功骗过服务器拿到了想要的数据。 - -​ 对于User-agent 我们可以把它当做一个身份证,这个身份证中会包含很多信息,通过这些信息可以识别出访问者。所以当服务器开启了user-agent认证时,就需要像服务器传递相关的信息进行核对。核对成功,服务器才会返回给用户正确的内容,否则就会拒绝服务。 - -当然,对于Headers的相关信息还有很多,后续我们再一一讲解,下期见。 - - - diff --git "a/Python/\347\210\254\350\231\253/\345\210\251\347\224\250python\345\256\236\347\216\260\345\260\217\350\257\264\350\207\252\347\224\261.md" "b/Python/\347\210\254\350\231\253/\345\210\251\347\224\250python\345\256\236\347\216\260\345\260\217\350\257\264\350\207\252\347\224\261.md" deleted file mode 100644 index deb2f6262f..0000000000 --- "a/Python/\347\210\254\350\231\253/\345\210\251\347\224\250python\345\256\236\347\216\260\345\260\217\350\257\264\350\207\252\347\224\261.md" +++ /dev/null @@ -1,91 +0,0 @@ -### 利用python实现小说自由 - -#### 一、用到的相关模块 - -1.reuqests模块 - -安装reuqest模块,命令行输入: - -``` -pip install requests -``` - -2.xpath解析 - -​ XPath 即为 XML 路径语言,它是一种用来确定 XML (标准通用标记语言子集)文档中某部分位置的语言。XPath 基于 XML 的树状结构,提供在数据结构树中找寻节点的能力。起初 XPath 的提出的初衷是将其作为一个通用的、介于 XPointer 与 XSL 间的语法模型。但是 XPath 很快的被开发者采用来当作小型查询语言。 - -​ 简单的来说:Xpath(XML Path Language)是一门在 XML 和 HTML 文档中查找信息的语言,可用来在 XML 和 HTML 文档中对元素和属性进行遍历。 - -​ xml 是 一个HTML/XML的解析器,主要的功能是如何解析和提取 HTML/XML 数据。 - -安装xml: - -``` -pip install lxml -``` - - - -#### 二、实现步骤 - -1.首先我们打开一个小说的网址:https://www.qu-la.com/booktxt/17437775116/ - -2.右击“检查” 查看下这个网页的相关代码情况 - - - -我们可以发现所有的内容都被包裹在
    - -通过xpath 解析出每个章节的标题,和链接。 - -3.根据对应的链接获取每个章节的文本。同样的方法找到章节文本的具体位置 - -//*[@id="txt"] - - - -3.for 循环获取所有链接的文本,保存为Txt文件。 - -#### 三、代码展示 - -```python -import requests -from lxml import etree -def getNovel(): - headers = {'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36'}; - html = requests.get('https://www.qu-la.com/booktxt/17437775116/',headers=headers).content - doc=etree.HTML(html) - contents=doc.xpath('//*[ @id="list"]/div[3]/ul[2]') #获取到小说的所有章节 - for content in contents: - links=content.xpath('li/a/@href') #获取每个章节的链接 - for link in links: #循环处理每个章节 - url='https://www.qu-la.com'+link #拼接章节url - html=requests.get(url).text - doc=etree.HTML(html) - content = doc.xpath('//*[@id="txt"]/text()') #获取章节的正文 - title = doc.xpath('//*[@id="chapter-title"]/h1/text()') #获取标题 - #所有的保存到一个文件里面 - with open('books/凡人修仙之仙界篇.txt', 'a') as file: - file.write(title[0]) - print('正在下载{}'.format(title[0])) - for items in content: - file.write(item) - - print('下载完成') -getNovel() #调用函数 -``` - -#### 四、拓展思考 - -1.写一个搜索界面,用户输入书名自主下载对应的小说。 - -2.引入多进程异步下载,提高小说的下载速度。 - - - diff --git a/README.md b/README.md index 8ad293ad89..8e1fc968d4 100755 --- a/README.md +++ b/README.md @@ -16,14 +16,24 @@ 更多精彩内容将发布在公众号 **JavaEdge**,公众号提供大量求职面试资料,后台回复 "面试" 即可领取。 本号系统整理了Java高级工程师必备技能点,帮你理清纷杂面试知识点,有的放矢。 +我本人也是基于这些知识体系,在各种求职征途中拿到百度、携程、华为、中兴、顺丰、帆软、货拉拉等offer。 + + ## 3 笔者简介 + ### [阿里云栖社区博客专家](https://yq.aliyun.com/users/article?spm=a2c4e.8091938.headeruserinfo.3.65993d6eqaQ0O6) +![](https://img-blog.csdnimg.cn/20190712131824494.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + ### [腾讯云自媒体邀约计划作者](https://cloud.tencent.com/developer/user/1752328) + +![](https://img-blog.csdnimg.cn/20190712140323352.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +
    -## 4 目录结构 +## 4 目录结构(不断优化中) |  数据结构与算法  | 操作系统 |  网络  | 面向对象 |   数据存储   |    Java    | 架构设计 |    框架    | 编程规范 |    职业规划    | | :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:| @@ -83,6 +93,15 @@ ### :memo: 职业规划 +## QQ 技术交流群 + +为大家提供一个学习交流平台,在这里你可以自由地讨论技术问题。 + + + +## 微信交流群 + + ### 本人微信 @@ -95,17 +114,4 @@ ### 绘图工具 - [draw.io](https://www.draw.io/) -- keynote - -再分享我整理汇总的一些 Java 面试相关资料(亲自验证,严谨科学!别再看网上误导人的垃圾面试题!!!),助你拿到更多 offer! - -![](https://img-blog.csdnimg.cn/35dcdea77d6d4845a18ef4780309a2a6.png) - -[点击获取更多经典必读电子书!](https://mp.weixin.qq.com/s?__biz=MzUzNTY5MzA3MQ==&mid=2247497273&idx=1&sn=b0f1e2e03cd7de3ce5d93cc8793d6d88&chksm=fa832459cdf4ad4fb046c0beb7e87ecea48f338278846679ef65238af45f0a135720e7061002&token=766333302&lang=zh_CN#rd) - -2023年最新Java学习路线一条龙: - -[![](https://img-blog.csdnimg.cn/0fe00585e984406fbd9c22cedbf4b239.png)](https://www.nowcoder.com/discuss/353159357007339520?sourceSSR=users) - - -再给大家推荐一个学习 前后端软件开发 和准备Java 面试的公众号[【JavaEdge】](https://mp.weixin.qq.com/s?__biz=MzUzNTY5MzA3MQ==&mid=2247498257&idx=1&sn=b09d88691f9bfd715e000b69ef61227e&chksm=fa832871cdf4a1675d4491727399088ca488fa13e0a3cdf2ece3012265e5a3ef273dff540879&token=766333302&lang=zh_CN#rd)(强烈推荐!) +- keynote \ No newline at end of file diff --git "a/Spring/Spring RestTemplate\344\270\272\344\275\225\345\277\205\351\241\273\346\220\255\351\205\215MultiValueMap\357\274\237.md" "b/Spring/Spring RestTemplate\344\270\272\344\275\225\345\277\205\351\241\273\346\220\255\351\205\215MultiValueMap\357\274\237.md" deleted file mode 100644 index 0281c13f2c..0000000000 --- "a/Spring/Spring RestTemplate\344\270\272\344\275\225\345\277\205\351\241\273\346\220\255\351\205\215MultiValueMap\357\274\237.md" +++ /dev/null @@ -1,53 +0,0 @@ -微服务之间的大多都是使用 HTTP 通信,这自然少不了使用 HttpClient。 -在不适用 Spring 前,一般使用 Apache HttpClient 和 Ok HttpClient 等,而一旦引入 Spring,就有了更好选择 - RestTemplate。 - -接口: -![](https://img-blog.csdnimg.cn/f7633a690cec471787c55a5b34722db1.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -想接受一个 Form 表单请求,读取表单定义的两个参数 para1 和 para2,然后作为响应返回给客户端。 - -定义完接口后,使用 RestTemplate 来发送一个这样的表单请求,代码示例如下: -![](https://img-blog.csdnimg.cn/96362640fa3b4354b5b0a32687c302b1.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -上述代码定义了一个 Map,包含了 2 个表单参数,然后使用 RestTemplate 的 postForObject 提交这个表单。 -执行代码提示 400 错误,即请求出错: -![](https://img-blog.csdnimg.cn/a2dfa488fb0f415aacb08441bd35e76d.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -就是缺少 para1 表单参数,why? -# 解析 -RestTemplate 提交的表单,最后提交请求啥样? -Wireshark 抓包: -![](https://img-blog.csdnimg.cn/94de06901df043ac9fa9a8de77ed9fd6.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -实际上是将定义的表单数据以 JSON 提交过去了,所以我们的接口处理自然取不到任何表单参数。 -why?怎么变成 JSON 请求体提交数据呢?注意 RestTemplate 执行调用栈: -![](https://img-blog.csdnimg.cn/63895ac7bac84bde8dce1d208938af54.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -最终使用的 Jackson 工具序列化了表单 -![](https://img-blog.csdnimg.cn/8f4582ece1c244baabe31b43463db638.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -用到 JSON 的关键原因在 -### RestTemplate.HttpEntityRequestCallback#doWithRequest -![](https://img-blog.csdnimg.cn/c779397a10a14cf3a9b5fe3635b22b8b.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -根据当前要提交的 Body 内容,遍历当前支持的所有编解码器: -- 若找到合适编解码器,用之完成 Body 转化 - -看下 JSON 的编解码器对是否合适的判断 -### AbstractJackson2HttpMessageConverter#canWrite -![](https://img-blog.csdnimg.cn/cf45893c65b5498eb2d16638ce7873e5.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -可见,当使用的 Body 为 HashMap,是可完成 JSON 序列化的。 -所以后续将这个表单序列化为请求 Body了。 - -但我还是疑问,为何适应表单处理的编解码器不行? -那就该看编解码器判断是否支持的实现: -### FormHttpMessageConverter#canWrite![](https://img-blog.csdnimg.cn/ee0f19a3ed6c43269a58ddb3179591f5.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -可见只有当我们发送的 Body 是 MultiValueMap 才能使用表单来提交。 -原来使用 RestTemplate 提交表单必须是 MultiValueMap! -而我们案例定义的就是普通的 HashMap,最终是按请求 Body 的方式发送出去的。 -# 修正 -换成 MultiValueMap 类型存储表单数据即可: -![](https://img-blog.csdnimg.cn/ace1283803104462bab2261d6f9789d3.png) -修正后,表单数据最终使用下面的代码进行了编码: -### FormHttpMessageConverter#write -![](https://img-blog.csdnimg.cn/312d71a80572443b8bb153dcdbd15e0e.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -发送出的数据截图如下: -![](https://img-blog.csdnimg.cn/244f8819935244d59083e66ca6b16ea4.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -这就对了!其实官方文档也说明了: -![](https://img-blog.csdnimg.cn/7020d620d5b544a29d022877dcfac1a8.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -> 参考: -> - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/\351\224\256\345\200\274\346\225\260\346\215\256\345\272\223\347\232\204\345\237\272\346\234\254\346\236\266\346\236\204.md" b/Spring/SpringFramework/.md similarity index 100% rename from "\346\225\260\346\215\256\345\255\230\345\202\250/Redis/\351\224\256\345\200\274\346\225\260\346\215\256\345\272\223\347\232\204\345\237\272\346\234\254\346\236\266\346\236\204.md" rename to Spring/SpringFramework/.md diff --git "a/Spring/SpringFramework/Spring AOP\345\210\260\345\272\225\346\230\257\344\273\200\344\271\210\357\274\237.md" "b/Spring/SpringFramework/Spring AOP\345\210\260\345\272\225\346\230\257\344\273\200\344\271\210\357\274\237.md" deleted file mode 100644 index 5625f5ce76..0000000000 --- "a/Spring/SpringFramework/Spring AOP\345\210\260\345\272\225\346\230\257\344\273\200\344\271\210\357\274\237.md" +++ /dev/null @@ -1,78 +0,0 @@ -IoC和AOP生而就是为了解耦和扩展。 - -# 什么是 IoC? - -一种设计思想,将设计好的对象交给Spring容器控制,而非直接在对象内部控制。 - -> 为啥要让容器来管理对象呢?你这程序员咋就知道甩锅呢? -![](https://img-blog.csdnimg.cn/20210512150222541.png) - -拥有初级趣味的码农,可能只是觉着使用IoC方便,就是个用来解耦的,但这还远非容器的益处。 -利用容器管理所有的框架、业务对象,我们可以做到: -- 无侵入调整对象的关系 -- 无侵入地随时调整对象的属性 -- 实现对象的替换 - -这使得框架开发者在后续实现一些扩展就很容易。 - -# 什么是AOP? - -AOP实现了高内聚、低耦合,在切面集中实现横切关注点(缓存、权限、日志等),然后通过切点配置把代码注入到合适的位置。 - -# 基本概念 -- **连接点(Join point)** -就是方法执行 -- **切点(Pointcut)** -Spring AOP默认使用AspectJ查询表达式,通过在连接点运行查询表达式来匹配切点 -- **增强(Advice)** -也叫作通知,定义了切入切点后增强的方式,包括前、后、环绕等。Spring AOP中,把增强定义为拦截器 -- **切面(Aspect)** -切面=切点+增强 - -### Declaring Advice -有如下三类增强: -#### @Before -#### @After -#### @Around -**环绕通知**运行 **around** 匹配方法的执行。它有机会在方法运行之前和之后都工作,并确定方法实际运行何时、如何甚至是否执行。若你需要以线程安全的方式(例如启动和停止计时器)在方法执行前后共享状态,则通常会使用Around advice。 - -使用案例: -```java -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.ProceedingJoinPoint; - -@Aspect -public class AroundExample { - - @Around("com.xyz.myapp.CommonPointcuts.businessService()") - public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { - // start stopwatch - Object retVal = pjp.proceed(); - // stop stopwatch - return retVal; - } -} -``` -around advice 返回的值就是方法调用者看到的返回值。例如,一个简单的缓存aspect可以返回一个值从缓存(如果它有)或调用`procedd`如果它没有。请注意,可以多次调用`procedd`,或者根本不在around advice的主体内调用,这都是合法的。 - -推荐始终使用最不强大的advice形式,以满足需求。 - -使用 **@Around** 注解声明环绕通知时,第一个参数必须是ProceedingJoinPoint类型。 -在通知的方法体中,调用 `proceed()` 会导致基础方法运行。 `proceed()` 也可以在Object[]中传递。数组中的值在进行时用作方法执行的参数。 -#### Advice参数 -Spring 提供全种类的通知,这意味着你在通知的方法签名中声明所需参数,而非和`Object[]`协作。 -如何编写通用的通知,以便了解通知方法当前在通知啥玩意。 -#### Access to the Current JoinPoint -任何通知方法都可能声明类型`org.aspectj.lang.JoinPoint` 的参数(请注意,围绕建议需要申报类型'继续JoinPoint'的第一参数,该参数是 JoinPoint 的子类。 -JoinPoint 接口提供了许多有用的方法: -- getArgs() -返回方法的参数 -- getThis() -返回代理对象 -- getTarget() -返回目标对象 -- getSignature() -Returns a description of the method that is being advised. -- toString() -Prints a useful description of the method being advised. \ No newline at end of file diff --git "a/Spring/SpringFramework/Spring Bean\345\237\272\347\241\200.md" "b/Spring/SpringFramework/Spring Bean\345\237\272\347\241\200.md" index b946fba568..9b5d16377c 100644 --- "a/Spring/SpringFramework/Spring Bean\345\237\272\347\241\200.md" +++ "b/Spring/SpringFramework/Spring Bean\345\237\272\347\241\200.md" @@ -186,3 +186,5 @@ bean元数据定义中的指定类只是初始类引用,可能结合使用的 该方法可确定给定名称bean的类型。 更确切地,返回针对相同bean名称的`BeanFactory.getBean`调用将返回的对象的类型。 且该方法的实现考虑了前面穷举的所有情况,并针对于FactoryBean ,返回FactoryBean所创建的对象类型,和`FactoryBean.getObjectType()`返回一致。 ![](https://img-blog.csdnimg.cn/20200828121006639.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) + +![](https://img-blog.csdnimg.cn/20200825235213822.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) diff --git "a/Spring/SpringFramework/Spring Bean\347\224\237\345\221\275\345\221\250\346\234\237\347\256\241\347\220\206.md" "b/Spring/SpringFramework/Spring Bean\347\224\237\345\221\275\345\221\250\346\234\237\347\256\241\347\220\206.md" index 0f32a53bfe..29d9533994 100644 --- "a/Spring/SpringFramework/Spring Bean\347\224\237\345\221\275\345\221\250\346\234\237\347\256\241\347\220\206.md" +++ "b/Spring/SpringFramework/Spring Bean\347\224\237\345\221\275\345\221\250\346\234\237\347\256\241\347\220\206.md" @@ -59,10 +59,9 @@ public interface InitializingBean { 因其实现了InitializingBean接口,其中只有一个方法,且在Bean加载后就执行。该方法可被用来检查是否所有的属性都已设置好。 ## 8 BeanPostProcess接口 -Spring将调用它们的postProcessAfterInitialization(后初始化)方法,作用与6一样,只不过6是在Bean初始化前执行,而这是在Bean初始化后执行。 +Spring将调用它们的postProcessAfterInitialization(后初始化)方法,作用与6的一样,只不过6是在Bean初始化前执行,而这个是在Bean初始化后执行。 - -> 经过以上工作,Bean将一直驻留在应用上下文中给应用使用,直到应用上下文被销毁。 +经过以上工作,Bean将一直驻留在应用上下文中给应用使用,直到应用上下文被销毁。 ## 9 DispostbleBean接口 - Spring将调用它的destory方法 @@ -259,28 +258,16 @@ public class GiraffeService implements ApplicationContextAware, } ``` ### BeanPostProcessor -允许自定义修改新 bean 实例的工厂钩子——如检查标记接口或用代理包装 bean。 - -- 通过标记接口或类似方式填充bean的后置处理器将实现postProcessBeforeInitialization(java.lang.Object,java.lang.String) -- 而用代理包装bean的后置处理器通常会实现postProcessAfterInitialization(java.lang.Object,java.lang.String) -#### Registration -一个ApplicationContext可在其 Bean 定义中自动检测 BeanPostProcessor Bean,并将这些后置处理器应用于随后创建的任何 Bean。 -普通的BeanFactory允许对后置处理器进行编程注册,将它们应用于通过Bean工厂创建的所有Bean。 -#### Ordering -在 ApplicationContext 中自动检测的 OrderBeanPostProcessor Bean 将根据 PriorityOrdered 和 Ordered 语义进行排序。 -相比之下,在BeanFactory以编程方式注册的BeanPostProcessor bean将按注册顺序应用 -对于以编程方式注册的后处理器,通过实现 PriorityOrdered 或 Ordered 接口表达的任何排序语义都将被忽略。 -对于 BeanPostProcessor bean,并不考虑 **@Order** 注解。 -![](https://img-blog.csdnimg.cn/05510c239a6e41a683eb806181b90347.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) Aware接口是针对某个 **实现这些接口的Bean** 定制初始化的过程, -Spring还可针对容器中 **所有Bean** 或 **某些Bean** 定制初始化过程,只需提供一个实现BeanPostProcessor接口的实现类。 +Spring同样还可针对容器中 **所有Bean**,或 **某些Bean** 定制初始化过程,只需提供一个实现BeanPostProcessor接口的类即可。 -该接口包含如下方法: +该接口中包含两个方法: - postProcessBeforeInitialization 在容器中的Bean初始化之前执行 - postProcessAfterInitialization 在容器中的Bean初始化之后执行 -#### 实例 + +例子如下: ```java public class CustomerBeanPostProcessor implements BeanPostProcessor { @@ -296,10 +283,13 @@ public class CustomerBeanPostProcessor implements BeanPostProcessor { } } ``` + 要将BeanPostProcessor的Bean像其他Bean一样定义在配置文件中 + ```xml - + ``` + # 总结 Spring Bean的生命周期 @@ -435,5 +425,4 @@ Spring 容器可以管理 singleton 作用域下 bean 的生命周期,在此 > 参考 > - https://blog.csdn.net/fuzhongmin05/article/details/73389779 -> - https://yemengying.com/2016/07/14/spring-bean-life-cycle/ -> - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/config/BeanPostProcessor.html \ No newline at end of file +> - https://yemengying.com/2016/07/14/spring-bean-life-cycle/ \ No newline at end of file diff --git "a/Spring/SpringFramework/Spring Exception\344\271\213\345\260\217\345\277\203\350\277\207\346\273\244\345\231\250\345\274\202\345\270\270.md" "b/Spring/SpringFramework/Spring Exception\344\271\213\345\260\217\345\277\203\350\277\207\346\273\244\345\231\250\345\274\202\345\270\270.md" deleted file mode 100644 index fe83f65d7b..0000000000 --- "a/Spring/SpringFramework/Spring Exception\344\271\213\345\260\217\345\277\203\350\277\207\346\273\244\345\231\250\345\274\202\345\270\270.md" +++ /dev/null @@ -1,67 +0,0 @@ -# 错误场景![](https://img-blog.csdnimg.cn/e2023dfa73674c659fe29797527f1a26.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -验证请求的Token合法性的Filter。Token校验失败时,直接抛自定义异常,移交给Spring处理: -![](https://img-blog.csdnimg.cn/122fe2d581fc4684a2ad6deb491987e7.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -![](https://img-blog.csdnimg.cn/016ce84b7da3488fa65148e1e9a27ee7.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -![](https://img-blog.csdnimg.cn/87e550844437408f88f254387b0b2cd6.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -测试HTTP请求: -![](https://img-blog.csdnimg.cn/18d36bd55d484550a4ad5bbee58fa49c.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -日志输出如下:说明**IllegalRequestExceptionHandler**未生效。 -![](https://img-blog.csdnimg.cn/81a7b9c33b7b48b5bc4353d11f6a4089.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -why? -这就需要精通Spring异常处理流程了。 -# 解析 -![](https://img-blog.csdnimg.cn/2c00379066d1490891b909c4013d1fdf.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -当所有Filter被执行完毕,Spring才会处理Servlet相关,而**DispatcherServlet**才是整个Servlet处理核心,它是前端控制器设计模式,提供 Spring Web MVC 的集中访问点并负责职责的分派。 - -在这,Spring处理了请求和处理器的对应关系及**统一异常处理**。 - -Filter内异常无法被统一处理,就是因为异常处理发生在 ***DispatcherServlet#doDispatch()*** -![](https://img-blog.csdnimg.cn/13642402d76c4f2fbadf4da28d7239c4.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -但此时,**过滤器已全部执行完**。 -# Spring异常统一处理 -## ControllerAdvice如何被Spring加载并对外暴露? -#### WebMvcConfigurationSupport#handlerExceptionResolver() -实例化并注册一个ExceptionHandlerExceptionResolver 的实例 -![](https://img-blog.csdnimg.cn/32e6572a6cfd4d93bacc992871875f24.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -最终按下图调用栈,Spring 实例化了ExceptionHandlerExceptionResolver类。 -![](https://img-blog.csdnimg.cn/2055644a405542698006d57e0fba1f50.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -ExceptionHandlerExceptionResolver实现了**InitializingBean** -![](https://img-blog.csdnimg.cn/916c98b8d2dc4144b3ba690d27f19091.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -重写 ***afterPropertiesSet()*** ![](https://img-blog.csdnimg.cn/09a7b4e7ca2b48ef811b35f3c90089f1.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -#### initExceptionHandlerAdviceCache -完成所有 ControllerAdvice 中的ExceptionHandler 初始化:查找所有 **@ControllerAdvice** 注解的 Bean,把它们放入exceptionHandlerAdviceCache。 -这里即指自定义的IllegalRequestExceptionHandler -![](https://img-blog.csdnimg.cn/c1102552aa0a4d478d2ed537ba72cf61.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -![](https://img-blog.csdnimg.cn/184207f02e31465ba45de946e5a1b73c.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -所有被 **@ControllerAdvice** 注解的异常处理器,都会在 **ExceptionHandlerExceptionResolver** 实例化时自动扫描并装载在其exceptionHandlerAdviceCache。 -#### initHandlerExceptionResolvers -当第一次请求发生时,***DispatcherServlet#initHandlerExceptionResolvers()*** 将获取所有注册到 Spring 的 HandlerExceptionResolver 实例(ExceptionHandlerExceptionResolver正是),存到**handlerExceptionResolvers** -![](https://img-blog.csdnimg.cn/9560e3a409254a82af3b655a12fe24cb.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -![](https://img-blog.csdnimg.cn/be51ad0247c24552837631caf23b2004.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -## ControllerAdvice如何被Spring消费并处理异常? -### DispatcherServlet -#### doDispatch() -![](https://img-blog.csdnimg.cn/90c9d7b1254b49da8f865323eeb5e42a.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -执行用户请求时,当查找、执行请求对应的 handler 过程中异常时: -1. 会把异常值赋给 dispatchException -2. 再移交 processDispatchResult() -#### processDispatchResult -![](https://img-blog.csdnimg.cn/287b3c8dae8e48c4a5ec8dccc7143dad.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -当Exception非空时,继续移交 -#### processHandlerException -![](https://img-blog.csdnimg.cn/6b18557adc6844adaaac2562955ef9ef.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -从 handlerExceptionResolvers 获取有效的异常解析器以解析异常。 - -这里的 handlerExceptionResolvers 一定包含声明的IllegalRequestExceptionHandler#IllegalRequestException 的异常处理器的 ExceptionHandlerExceptionResolver 包装类。 -# 修正 -为利用到 Spring MVC 的异常处理机制,改造Filter: -- 手动捕获异常 -- 将异常通过 HandlerExceptionResolver 进行解析处理 - -据此,修改 PermissionFilter,注入 HandlerExceptionResolver: -![](https://img-blog.csdnimg.cn/1b50cf3672214f418f908fd8f736a120.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -然后,在 doFilter 捕获异常并移交 HandlerExceptionResolver: -![](https://img-blog.csdnimg.cn/b83601b115024554854786652e2a2f92.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -现在再用错误 Token 请求,日志输出如下: -![](https://img-blog.csdnimg.cn/0f7cb17d0ccb4b3caf84ff6c78227656.png) 响应体: -![](https://img-blog.csdnimg.cn/e69723db189b4eca9231dc06fe3c7059.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) \ No newline at end of file diff --git "a/Spring/SpringFramework/\344\270\272\344\273\200\344\271\210private\346\226\271\346\263\225\345\212\240\344\272\206@Transactional\357\274\214\344\272\213\345\212\241\344\271\237\346\262\241\346\234\211\347\224\237\346\225\210\357\274\237.md" "b/Spring/SpringFramework/\344\270\272\344\273\200\344\271\210private\346\226\271\346\263\225\345\212\240\344\272\206@Transactional\357\274\214\344\272\213\345\212\241\344\271\237\346\262\241\346\234\211\347\224\237\346\225\210\357\274\237.md" deleted file mode 100644 index 72d83a972c..0000000000 --- "a/Spring/SpringFramework/\344\270\272\344\273\200\344\271\210private\346\226\271\346\263\225\345\212\240\344\272\206@Transactional\357\274\214\344\272\213\345\212\241\344\271\237\346\262\241\346\234\211\347\224\237\346\225\210\357\274\237.md" +++ /dev/null @@ -1,75 +0,0 @@ -现在产品期望用户创建和保存逻辑分离:把User实例的创建和保存逻辑拆到两个方法分别进行。 -然后,把事务的注解 **@Transactional** 加在保存数据库的方法上。 -![](https://img-blog.csdnimg.cn/2f29be1be44f4029a8ed70152d34de03.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -执行程序,异常正常抛出 -![](https://img-blog.csdnimg.cn/5952728511b443f38eb79cb3aae0f0b9.png) -事务未回滚 -![](https://img-blog.csdnimg.cn/4c7e43e4f9944f1fac26d6a7ff83342e.png) -# 源码解析 - debug: -![](https://img-blog.csdnimg.cn/dcfa7c2e44ca4048b5f889b4ab14a542.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -前一段是 Spring 创建 Bean 的过程。当 Bean 初始化之后,开始尝试代理操作,这是从如下方法开始处理的: -### AbstractAutoProxyCreator#postProcessAfterInitialization -```java -public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { - if (bean != null) { - Object cacheKey = getCacheKey(bean.getClass(), beanName); - if (this.earlyProxyReferences.remove(cacheKey) != bean) { - return wrapIfNecessary(bean, beanName, cacheKey); - } - } - return bean; -} -``` -继续 debug,直到 -### AopUtils#canApply -针对切面定义里的条件,确定这个方法是否可被应用创建成代理。 -有段 `methodMatcher.matches(method, targetClass)` 判断这个方法是否符合这样的条件: -```java -public static boolean canApply(Pointcut pc, Class targetClass, boolean hasIntroductions) { - // ... - for (Class clazz : classes) { - Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz); - for (Method method : methods) { - if (introductionAwareMethodMatcher != null ? - introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) : - methodMatcher.matches(method, targetClass)) { - return true; - } - } - } - return false; -} -``` -![](https://img-blog.csdnimg.cn/f6729281ab6443cca152a37c2b1dbb76.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -从 matches() 调用到 -##### AbstractFallbackTransactionAttributeSource#getTransactionAttribute -获取注解中的事务属性,根据属性确定事务的策略。 -![](https://img-blog.csdnimg.cn/7996b41bf8054f28be00f67fd5473cdc.png) -接着调用到 -#### computeTransactionAttribute -根据方法和类的类型确定是否返回事务属性: -![](https://img-blog.csdnimg.cn/ab5481cb131f41b4956021c582b4a78a.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -当上图中条件判断结果为 true,则返回 null,表明该方法不会被代理,从而导致事务注解不会生效。 - -那到底是不是 true 呢? -##### 条件1:allowPublicMethodsOnly() -AnnotationTransactionAttributeSource#publicMethodsOnly属性值 -![](https://img-blog.csdnimg.cn/108a1106eb5946ecab4e182bef6de966.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -publicMethodsOnly 是通过 AnnotationTransactionAttributeSource 的构造方法初始化的,默认为 true。 -![](https://img-blog.csdnimg.cn/591bfd6571814ed29b0b610021724da0.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -##### 条件2:Modifier.isPublic() -根据传入的 `method.getModifiers()` 获取方法的修饰符,该修饰符是 java.lang.reflect.Modifier 的静态属性,对应的几类修饰符分别是: -- PUBLIC: 1 -- PRIVATE: 2 -- PROTECTED: 4 - -这里做了一个位运算,只有当传入的方法修饰符是 public 类型的时候,才返回 true -![](https://img-blog.csdnimg.cn/adafc6c4f16d44478625252d9ac12006.png) -综上两个条件,只有当注解为事务方法为 public 才会被 Spring 处理。 -# 修正 -只需将修饰符从 private 改成 public,其实该问题 IDEA 也会告警,一般都会避免。 -![](https://img-blog.csdnimg.cn/ee7ded3ee775473a8094d28a1d2d0e08.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -调用这个加了事务注解的方法,必须是调用被 Spring AOP 代理过的方法:不能通过类的内部调用或通过 this 调用。 -所以我们的案例的UserService,它Autowired了自身(UserService)的一个实例来完成代理方法的调用。 \ No newline at end of file diff --git "a/Spring/SpringFramework/Spring\347\232\204@Transaction\346\263\250\350\247\243\345\244\261\346\225\210\345\234\272\346\231\257.md" "b/Spring/Spring\347\232\204@Transaction\346\263\250\350\247\243\345\244\261\346\225\210\345\234\272\346\231\257.md" similarity index 100% rename from "Spring/SpringFramework/Spring\347\232\204@Transaction\346\263\250\350\247\243\345\244\261\346\225\210\345\234\272\346\231\257.md" rename to "Spring/Spring\347\232\204@Transaction\346\263\250\350\247\243\345\244\261\346\225\210\345\234\272\346\231\257.md" diff --git "a/TODO/uml/Java\346\200\273\347\273\223\347\237\245\350\257\206\347\202\271.pos" "b/TODO/uml/Java\346\200\273\347\273\223\347\237\245\350\257\206\347\202\271.pos" deleted file mode 100644 index 9dea4e81b1..0000000000 --- "a/TODO/uml/Java\346\200\273\347\273\223\347\237\245\350\257\206\347\202\271.pos" +++ /dev/null @@ -1 +0,0 @@ -{"diagram":{"image":{"x":0,"width":200,"y":0,"pngdata":"iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAsUlEQVR4nO3BAQEAAACCIP+vbkhAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8GXHmAAFMgHIEAAAAAElFTkSuQmCC","height":200},"elements":{"leftChildren":[],"note":"","watermark":"","children":[{"parent":"root","lineStyle":{"randomLineColor":"#4D69FD"},"children":[{"parent":"800ecb4c0776","children":[{"parent":"36d002b102e2","children":[{"parent":"5351911e121f","children":[],"id":"da9b17a0be4d","title":"数据结构: 数组"},{"parent":"5351911e121f","children":[],"id":"00dd5a2b370d","title":"由于数据结构的特点, 查找访问效率高, 增删效率低"},{"parent":"5351911e121f","children":[],"id":"89a058828311","title":"默认数组大小10"},{"parent":"5351911e121f","children":[],"id":"dc80c006aa14","title":"ArrayList存在指定index新增与直接新增,在新增前会有一步校验长度的判断ensureCapacityInternal,如果长度不够需要扩容
    "},{"parent":"5351911e121f","children":[],"id":"d16b9b53101f","title":"ArrayList线程不安全,线程安全版本的数组容器是Vector
    "},{"parent":"5351911e121f","children":[],"id":"5eb59595d8fd","title":"与LinkedList遍历效率对比,性能高很多,ArrayList遍历最大的优势在于内存的连续性,CPU的内部缓存结构会缓存连续的内存片段,可以大幅降低读取内存的性能开销
    "}],"id":"5351911e121f","title":"ArrayList"},{"parent":"36d002b102e2","children":[{"parent":"f817a30f5e08","children":[],"id":"e3e17fe90642","title":"数据结构: 双向链表"},{"parent":"f817a30f5e08","children":[],"id":"3846d194c83e","title":"适合插入删除频繁的情况  内部维护了链表的长度
    "}],"id":"f817a30f5e08","title":"LinkedList
    "}],"id":"36d002b102e2","title":"List"},{"parent":"800ecb4c0776","children":[{"parent":"24f4a3146f29","children":[{"parent":"f4b8a675a029","children":[{"parent":"2353a0a1da34","children":[],"id":"8838e06b887a","title":"数据结构: 数组+链表
    "},{"parent":"2353a0a1da34","children":[],"id":"7a82749e87f8","title":"头插法: 新来的值会取代原有的值,原有的值就顺推到链表中去
    "},{"parent":"2353a0a1da34","children":[],"id":"c1c3800b1ead","title":"Java7在多线程操作HashMap时可能引起死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系, 可能形成环形链表
    "}],"id":"2353a0a1da34","title":"1.7"},{"parent":"f4b8a675a029","children":[{"parent":"cb2e0e4bf56a","children":[],"id":"a743dde50473","title":"数据结构: 数组+链表+红黑树 
    "},{"parent":"cb2e0e4bf56a","children":[{"parent":"4bb6868f0cc3","children":[],"id":"4753ca9df7aa","title":"根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表
    "}],"id":"4bb6868f0cc3","title":"Hashmap中的链表大小超过八个时会自动转化为红黑树,当删除小于六时重新变为链表
    "},{"parent":"cb2e0e4bf56a","children":[],"id":"1c07c2a415b7","title":"尾插法
    "},{"parent":"cb2e0e4bf56a","children":[],"id":"94b8ffa2573f","title":"Java8在同样的前提下并不会引起死循环,原因是扩容转移后前后链表顺序不变,保持之前节点的引用关系
    "}],"id":"cb2e0e4bf56a","title":"1.8"},{"parent":"f4b8a675a029","children":[{"parent":"6d54a11adce4","children":[],"id":"210d702bbf4a","title":"LoadFactory 默认0.75
    "},{"parent":"6d54a11adce4","children":[],"id":"e4055d669e46","title":"
    • 扩容:创建一个新的Entry空数组,长度是原数组的2倍。
    • ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组

    "},{"parent":"6d54a11adce4","children":[{"parent":"af3ec421dc31","children":[],"id":"0c6cfee11d49","title":"因为长度扩大以后,Hash的规则也随之改变
    "},{"parent":"af3ec421dc31","children":[],"id":"316f5ac25324","title":"Hash的公式---> index = HashCode(Key) & (Length - 1)
    原来长度(Length)是8你位运算出来的值是2 ,新的长度是16位运算出来的值不同
    "},{"parent":"af3ec421dc31","children":[],"id":"ff2cf93209dc","title":"HashMap是通过key的HashCode去寻找index的, 如果不进行重写,会出现在一个index中链表的HashCode相等情况,所以要确保相同的对象返回相同的hash值,不同的对象返回不同的hash值,必须要重写equals
    "}],"id":"af3ec421dc31","title":"为什么要ReHash而不进行复制? 
    "}],"id":"6d54a11adce4","title":"扩容机制"},{"parent":"f4b8a675a029","children":[{"parent":"c6437e8e4f45","children":[],"id":"cb7132ed82f1","title":"HashMap源码中put/get方法都没有加同步锁, 无法保证上一秒put的值,下一秒get的时候还是原值,所以线程安全无法保证
    "},{"parent":"c6437e8e4f45","children":[{"parent":"9d6423986311","children":[],"id":"b40b3d560950","title":"Collections.synchronizedMap(Map)"},{"parent":"9d6423986311","children":[{"parent":"cdcaf61438fc","children":[{"parent":"404e074072c9","children":[{"parent":"b926b8dd14c9","children":[],"id":"6bf370941ab4","title":"安全失败机制: 这种机制会使你此次读到的数据不一定是最新的数据。
    如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,ConcurrentHashMap同理"}],"id":"b926b8dd14c9","title":"Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null"},{"parent":"404e074072c9","children":[],"id":"c4669cca3ca2","title":"Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类"},{"parent":"404e074072c9","children":[],"id":"63a75cf7717b","title":"HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75"},{"parent":"404e074072c9","children":[],"id":"8e6e339c6351","title":"当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1"},{"parent":"404e074072c9","children":[{"parent":"13aa27708fb1","children":[],"id":"e72d018a877c","title":"快速失败(fail—fast)是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出ConcurrentModificationException"}],"id":"13aa27708fb1","title":"HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的. 所以,当其他线程改变了HashMap 的结构,如:增加、删除元素,将会抛出ConcurrentModificationException 异常,而 Hashtable 则不会"}],"id":"404e074072c9","title":"与HahsMap的区别"}],"id":"cdcaf61438fc","title":"Hashtable"},{"parent":"9d6423986311","children":[],"id":"3aa13f74a283","title":"ConcurrentHashMap"}],"id":"9d6423986311","title":"确保线程安全的方式"}],"id":"c6437e8e4f45","title":"线程不安全
    "},{"parent":"f4b8a675a029","children":[{"parent":"83e802bbbf44","children":[],"id":"6689f5a28d8d","title":"创建HashMap时最好赋初始值, 而且最好为2的幂,为了位运算的方便
    "},{"parent":"83e802bbbf44","children":[{"parent":"33f1a38f62cd","children":[],"id":"245253ad18f8","title":"实现均匀分布, 在使用不是2的幂的数字的时候,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值,只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的
    "}],"id":"33f1a38f62cd","title":"默认初始化大小为16
    "}],"id":"83e802bbbf44","title":"初始化
    "},{"parent":"f4b8a675a029","children":[],"id":"1bf62446136a","title":"重写equals必须重写HashCode
    "}],"id":"f4b8a675a029","title":"HashMap
    "},{"parent":"24f4a3146f29","children":[{"parent":"099533450f79","children":[{"parent":"6c4d02753b0b","children":[],"id":"1bf9ad750241","title":"这种机制会使你此次读到的数据不一定是最新的数据。如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,HashTable同理
    "}],"id":"6c4d02753b0b","title":"安全失败机制
    "},{"parent":"099533450f79","children":[{"parent":"238f89a01639","children":[],"id":"9a49309bdc28","title":"数据结构: 数组+链表 (Segment 数组、HashEntry 组成)
    "},{"parent":"238f89a01639","children":[{"parent":"940aa3e4031c","children":[],"id":"6d0c1ad1257f","title":"HashEntry跟HashMap差不多的,但是不同点是,他使用volatile去修饰了他的数据Value还有下一个节点next
    "}],"id":"940aa3e4031c","title":"HashEntry
    "},{"parent":"238f89a01639","children":[{"parent":"214133bb3335","children":[{"parent":"c94b076d3d2d","children":[],"id":"ccbe178d4298","title":"继承了ReentrantLock
    "},{"parent":"c94b076d3d2d","children":[],"id":"8e8ef1826155","title":"每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
    如果容量大小是16他的并发度就是16,可以同时允许16个线程操作16个Segment而且还是线程安全的。
    "}],"id":"c94b076d3d2d","title":"segment分段锁
    "},{"parent":"214133bb3335","children":[{"parent":"d93302980fe7","children":[],"id":"26bfa3a12ad8","title":"尝试自旋获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁
    "},{"parent":"d93302980fe7","children":[],"id":"b933436ce63c","title":"如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功
    "}],"id":"d93302980fe7","title":"put
    "},{"parent":"214133bb3335","children":[{"parent":"e791448ffbae","children":[],"id":"65b8e0356631","title":"由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值
    "},{"parent":"e791448ffbae","children":[],"id":"1748a490b610","title":"ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁
    "}],"id":"e791448ffbae","title":"get
    "}],"id":"214133bb3335","title":"并发度高的原因
    "}],"id":"238f89a01639","title":"1.7
    "},{"parent":"099533450f79","children":[{"parent":"bb9fcd3cdf24","children":[],"id":"cefe93491aa8","title":"数组+链表+红黑树
    "},{"parent":"bb9fcd3cdf24","children":[{"parent":"7f1ff4d86665","children":[],"id":"122b03888ad2","title":"抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性
    "}],"id":"7f1ff4d86665","title":"区别
    "},{"parent":"bb9fcd3cdf24","children":[{"parent":"292fc2483f4c","children":[],"id":"1181a0ece609","title":"根据 key 计算出 hashcode
    "},{"parent":"292fc2483f4c","children":[],"id":"17e33984c1e5","title":"判断是否需要进行初始化
    "},{"parent":"292fc2483f4c","children":[],"id":"d0bfa1371c32","title":"即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功
    "},{"parent":"292fc2483f4c","children":[],"id":"1ddaa88f7963","title":"如果当前位置的 hashcode == MOVED == -1,则需要进行扩容
    "},{"parent":"292fc2483f4c","children":[],"id":"5735982de5ef","title":"如果都不满足,则利用 synchronized 锁写入数据
    "},{"parent":"292fc2483f4c","children":[],"id":"4e006a2b0550","title":"如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树
    "}],"id":"292fc2483f4c","title":"put操作
    "},{"parent":"bb9fcd3cdf24","children":[{"parent":"92654a95d638","children":[],"id":"02394dd149e1","title":"根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值
    "},{"parent":"92654a95d638","children":[],"id":"d7b487f3b69b","title":"如果是红黑树那就按照树的方式获取值
    "},{"parent":"92654a95d638","children":[],"id":"1c6f9d2d5016","title":"就不满足那就按照链表的方式遍历获取值
    "}],"id":"92654a95d638","title":"get操作
    "}],"collapsed":false,"id":"bb9fcd3cdf24","title":"1.8
    "}],"id":"099533450f79","title":"ConcurrentHashMap
    "}],"collapsed":false,"id":"24f4a3146f29","title":"Map"},{"parent":"800ecb4c0776","children":[{"parent":"e7046be1aafc","children":[{"parent":"bb49195cb4a7","children":[],"link":{"title":"https://mp.weixin.qq.com/s/0cMrE87iUxLBw_qTBMYMgA","type":"url","value":"https://mp.weixin.qq.com/s/0cMrE87iUxLBw_qTBMYMgA"},"id":"5448b1ead28d","title":"同步容器(如Vector)的所有操作一定是线程安全的吗?"}],"id":"bb49195cb4a7","title":"相关文档"}],"id":"e7046be1aafc","title":"Vector"},{"parent":"800ecb4c0776","children":[{"parent":"6062a460b43c","children":[{"parent":"d695fc5d8846","children":[],"id":"84bbc18befc1","title":"底层实现的就是HashMap,所以是根据HashCode来判断是否是重复元素
    "},{"parent":"d695fc5d8846","children":[],"id":"27e06a58782e","title":"初始化容量是:16, 因为底层实现的是HashMap。加载因子是0.75
    "},{"parent":"d695fc5d8846","children":[],"id":"dbcb9b76077a","title":"无序的
    "},{"parent":"d695fc5d8846","children":[],"id":"2c468ef34635","title":"HashSet不能根据索引去数据,所以不能用普通的for循环来取出数据,应该用增强for循环,查询性能不好
    "}],"id":"d695fc5d8846","title":"HashSet
    "},{"parent":"6062a460b43c","children":[{"parent":"3e5fc407007b","children":[],"id":"34347150ba65","title":"底层是实现的TreeMap
    "},{"parent":"3e5fc407007b","children":[],"id":"3ead4a788049","title":"元素不能够重复,可以有一个null值,并且这个null值一直在第一个位置上
    "},{"parent":"3e5fc407007b","children":[],"id":"432c37f69b42","title":"默认容量:16,加载因子是0.75
    "},{"parent":"3e5fc407007b","children":[],"id":"1bf8f3a4b2c3","title":"TreeMap是有序的,这个有序不是存入的和取出的顺序是一样的,而是根据自然规律拍的序
    "}],"id":"3e5fc407007b","title":"TreeSet
    "}],"id":"6062a460b43c","title":"Set"}],"collapsed":true,"id":"800ecb4c0776","title":"集合"},{"parent":"root","lineStyle":{"randomLineColor":"#F4325C"},"children":[],"id":"ef690530e935","title":"基础"},{"parent":"root","lineStyle":{"randomLineColor":"#A04AFB"},"children":[{"parent":"d61da867cb10","children":[{"parent":"c56b85ccc7f0","children":[],"id":"cd07e14ad850","title":"虚拟机堆
    "},{"parent":"c56b85ccc7f0","children":[],"id":"5f3d1d3f67c2","title":"虚拟机栈
    "},{"parent":"c56b85ccc7f0","children":[],"id":"b44fa08a79e3","title":"方法区
    "},{"parent":"c56b85ccc7f0","children":[],"id":"38e4ffe62590","title":"本地方法栈
    "},{"parent":"c56b85ccc7f0","children":[],"id":"6b76f89e7451","title":"程序计数器
    "}],"id":"c56b85ccc7f0","title":"Java内存区域
    "},{"parent":"d61da867cb10","children":[{"parent":"08f416fb625f","children":[],"id":"95995454f91e","title":"加载->验证->准备->解析->初始化->使用->卸载
    "},{"parent":"08f416fb625f","children":[{"parent":"28edebbeb4d6","children":[],"id":"fddd8578d67e","title":"父类加载 不重复加载
    "}],"id":"28edebbeb4d6","title":"双亲委派原则
    "},{"parent":"08f416fb625f","children":[{"parent":"ff18f9026678","children":[],"id":"ea8db2744206","title":"第一次,在JDK1.2以前,双亲委派模型在JDK1.2引入,ClassLoder在最初已经存在了,为了兼容已有代码,添加了findClass()方法,如果父类加载失败会自动调用findClass()来完成加载
    "},{"parent":"ff18f9026678","children":[],"id":"5c240d502f24","title":"第二次,由双亲委派模型缺陷导致,由于双亲委派越基础的类由越上层的加载器进行加载,如果有基础类型调回用户代码回无法解决而产生,出现线程上下文类加载器,会出现父类加载器请求子类加载器完成类加载的行为
    "},{"parent":"ff18f9026678","children":[],"id":"5d62862bf3e0","title":"第三次,代码热替换、模块热部署,典型:OSGi每一个程序模块都有一个自己的类加载器
    "}],"id":"ff18f9026678","title":"破坏双亲委派模型
    "}],"id":"08f416fb625f","title":"类得加载机制"},{"parent":"d61da867cb10","children":[{"parent":"52bb18b06c6e","children":[],"id":"f61579e4d22a","title":"新生代/年轻代
    "},{"parent":"52bb18b06c6e","children":[],"id":"72db835ecada","title":"老年代
    "},{"parent":"52bb18b06c6e","children":[{"parent":"559cb847a3c3","children":[{"parent":"48946b525dfd","children":[],"id":"b7b6b8d8f03d","title":"字符串存在永久代中,容易出现性能问题和内存溢出
    "},{"parent":"48946b525dfd","children":[],"id":"2f7df7f20543","title":"类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
    "},{"parent":"48946b525dfd","children":[],"id":"5439cd0f53c8","title":"永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
    "},{"parent":"48946b525dfd","children":[],"id":"9c36c0817672","title":"将 HotSpot 与 JRockit 合二为一
    "}],"id":"48946b525dfd","title":"为什么要使用元空间取代永久代的实现?
    "},{"parent":"559cb847a3c3","children":[{"parent":"1d6773397050","children":[],"id":"c33f0e9fe53f","title":"元空间并不在虚拟机中,而是使用本地内存。因此默认情况下,元空间的大小仅受本地内存限制
    "}],"id":"1d6773397050","title":"元空间与永久代区别
    "},{"parent":"559cb847a3c3","children":[{"parent":"afb5323c83d0","children":[],"id":"fbf6f9baf099","title":"-XX:MetaspaceSize:初始空间大小,达到该值会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值
    "},{"parent":"afb5323c83d0","children":[],"id":"8e0c80084aef","title":"-XX:MaxMetaspaceSize:最大空间,默认是没有限制的
    "}],"id":"afb5323c83d0","title":"元空间空间大小设置
    "}],"id":"559cb847a3c3","title":"永久代/元空间
    "},{"parent":"52bb18b06c6e","children":[{"parent":"34170f8e7332","children":[],"id":"9d5df7c5e2eb","title":"根据存活时间
    "}],"id":"34170f8e7332","title":"晋升机制
    "}],"id":"52bb18b06c6e","title":"分代回收
    "},{"parent":"d61da867cb10","children":[{"parent":"c465e3a953e3","children":[{"parent":"2e9e41cbb0e4","children":[],"id":"6ff93afab3be","title":"绝大多数对象都是朝生熄灭的
    "}],"id":"2e9e41cbb0e4","title":"弱分代假说
    "},{"parent":"c465e3a953e3","children":[{"parent":"759979ad09cd","children":[],"id":"a152f857c10b","title":"熬过越多次垃圾收集过程的对象就越难以消亡
    "}],"id":"759979ad09cd","title":"强分代假说
    "},{"parent":"c465e3a953e3","children":[{"parent":"4843a46b8196","children":[],"id":"6481cbce8487","title":"跨代引用相对于同代引用来说仅占极少数
    "}],"id":"4843a46b8196","title":"跨代引用假说
    "}],"id":"c465e3a953e3","title":"分代收集理论
    "},{"parent":"d61da867cb10","children":[{"parent":"85ab22cf15b4","children":[],"id":"0c42af972e09","title":"“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,如果某个对象到GC Roots间没有任何引用链相连接,则证明此对象是不可能再被使用的
    "}],"id":"85ab22cf15b4","title":"可达性分析算法
    "},{"parent":"d61da867cb10","children":[{"parent":"133b15c6580f","children":[{"parent":"9bd71d4114f3","children":[],"id":"e602b5dbf42a","title":"强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它; 当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
    "}],"id":"9bd71d4114f3","title":"强引用 (StrongReference)
    "},{"parent":"133b15c6580f","children":[{"parent":"a4d69c685996","children":[],"id":"aaedb29f6e8f","title":"如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
    "}],"id":"a4d69c685996","title":"软引用 (SoftReference)
    "},{"parent":"133b15c6580f","children":[{"parent":"6cdc46b73fd3","children":[],"id":"7acc452f65ab","title":"在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
    "}],"id":"6cdc46b73fd3","title":"弱引用 (WeakReference)
    "},{"parent":"133b15c6580f","children":[{"parent":"5f58e345e898","children":[],"id":"d189c9591839","title":"如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收
    "}],"id":"5f58e345e898","title":"虚引用 (PhantomReference)
    "}],"id":"133b15c6580f","title":"引用
    "},{"parent":"d61da867cb10","children":[{"parent":"7a6e56139d11","children":[{"parent":"39f66c681caf","children":[{"parent":"5303c5abf34b","children":[],"id":"e983d3f5fd4a","title":"对象存活较多情况
    "},{"parent":"5303c5abf34b","children":[],"id":"b9065a228c05","title":"老年代
    "}],"id":"5303c5abf34b","title":"适用场景
    "},{"parent":"39f66c681caf","children":[{"parent":"733f3bc0b7ab","children":[],"id":"4f9fdc7ae82e","title":"内存空间碎片化
    "},{"parent":"733f3bc0b7ab","children":[],"id":"2a97115e02e3","title":"由于空间碎片导致的提前GC
    "},{"parent":"733f3bc0b7ab","children":[{"parent":"706cff5bc691","children":[],"id":"7a5e8f6db9e2","title":"标记需清除或存货对象
    "},{"parent":"706cff5bc691","children":[],"id":"3da1d4f2af71","title":"清除标记或未标记对象
    "}],"id":"706cff5bc691","title":"扫描了两次
    "}],"id":"733f3bc0b7ab","title":"缺点
    "}],"id":"39f66c681caf","title":"标记请除
    "},{"parent":"7a6e56139d11","children":[{"parent":"463d39f131e0","children":[{"parent":"cc3d3a846262","children":[],"id":"ad8382025fe4","title":"存活对象少比较高效
    "},{"parent":"cc3d3a846262","children":[],"id":"fb2a5786103c","title":"扫描了整个空间(标记存活对象并复制移动)
    "},{"parent":"cc3d3a846262","children":[],"id":"efebc696f64e","title":"年轻代
    "}],"id":"cc3d3a846262","title":"适用场景
    "},{"parent":"463d39f131e0","children":[{"parent":"4a07efc633b8","children":[],"id":"ca57a2a2591a","title":"需要空闲空间
    "},{"parent":"4a07efc633b8","children":[],"id":"74b68744b877","title":"老年代作为担保空间
    "},{"parent":"4a07efc633b8","children":[],"id":"9a2b53e3948c","title":"复制移动对象
    "}],"id":"4a07efc633b8","title":"缺点
    "}],"id":"463d39f131e0","title":"标记复制
    "},{"parent":"7a6e56139d11","children":[{"parent":"6e91691d76fb","children":[{"parent":"ca79fb0161d2","children":[],"id":"3093a8344bf8","title":"对象存活较多情况
    "},{"parent":"ca79fb0161d2","children":[],"id":"171500353dee","title":"老年代
    "}],"id":"ca79fb0161d2","title":"适用场景
    "},{"parent":"6e91691d76fb","children":[{"parent":"2e00b69db6db","children":[],"id":"9cc7fd616235","title":"移动存活对象并更新对象引用
    "},{"parent":"2e00b69db6db","children":[],"id":"9f173f5f0a02","title":"Stop The World
    "}],"id":"2e00b69db6db","title":"缺点
    "}],"id":"6e91691d76fb","title":"标记整理
    "},{"parent":"7a6e56139d11","children":[{"parent":"873b4393cb04","children":[],"id":"e55f55cabd04","title":"没办法解决循环引用的问题
    "}],"id":"873b4393cb04","title":"引用计数
    "}],"id":"7a6e56139d11","title":"垃圾回收机制
    "},{"parent":"d61da867cb10","children":[{"parent":"94c48a9cef88","children":[{"parent":"1871a079226a","children":[{"parent":"ec2e2387d0dc","children":[{"parent":"1e82c0b474da","children":[],"id":"4ae444f631e6","title":"Eden
    "},{"parent":"1e82c0b474da","children":[],"id":"2e6db7cc3c33","title":"Survivor1
    "},{"parent":"1e82c0b474da","children":[],"id":"c07a1cd16c8f","title":"Survivor2
    "},{"parent":"1e82c0b474da","children":[{"parent":"150da6938c96","children":[],"id":"79ab826115b2","title":"通过阈值晋升
    "}],"id":"150da6938c96","title":"Minor GC
    "}],"id":"1e82c0b474da","title":"年轻代"},{"parent":"ec2e2387d0dc","children":[{"parent":"c2b0ef70d896","children":[],"id":"69438d9521b8","title":"Major GC 等价于 Full GC
    "}],"id":"c2b0ef70d896","title":"老年代
    "},{"parent":"ec2e2387d0dc","children":[],"id":"bb363f2d06eb","title":"永久"}],"id":"ec2e2387d0dc","title":"分代情况
    "},{"parent":"1871a079226a","children":[{"parent":"ef9b2adfad54","children":[],"id":"f8a938786281","title":"对CPU资源敏感
    "},{"parent":"ef9b2adfad54","children":[],"id":"f34402ad150a","title":"无法处理浮动垃圾
    "},{"parent":"ef9b2adfad54","children":[],"id":"1b39a82e9c8c","title":"基于标记清除算法 大量空间碎片
    "}],"id":"ef9b2adfad54","title":"缺点
    "}],"id":"1871a079226a","title":"CMS
    "},{"parent":"94c48a9cef88","children":[{"parent":"9b2c5a694136","children":[],"id":"d3247834f765","title":"分区概念 弱化分代
    "},{"parent":"9b2c5a694136","children":[{"parent":"0ac46d282191","children":[],"id":"1eba7428e805","title":"不会产生碎片空间,分配大对象不会提前Full GC
    "}],"id":"0ac46d282191","title":"标记整理算法
    "},{"parent":"9b2c5a694136","children":[{"parent":"20775041ea09","children":[],"id":"df43ec84ba90","title":"使用参数-XX:MaxGCPauseMills,默认为200毫秒,优先处理回收价值收集最大的Region
    "}],"id":"20775041ea09","title":"允许用户设置收集的停顿时间
    "},{"parent":"9b2c5a694136","children":[],"id":"0831e78ce2d4","title":"利用CPU多核条件,缩短STW时间
    "},{"parent":"9b2c5a694136","children":[],"id":"00fc277cb4e8","title":"原始快照算法(SATB)保证收集线程与用户线程互不干扰,避免标记结果出错
    "},{"parent":"9b2c5a694136","children":[{"parent":"64ddc0929330","children":[{"parent":"6bda8393ce9e","children":[],"id":"2194b30b8023","title":"标记STW从GC Roots开始直接可达的对象,借用Minor GC时同步完成
    "}],"id":"6bda8393ce9e","title":"初始标记
    "},{"parent":"64ddc0929330","children":[{"parent":"8954c96bf47e","children":[],"id":"11402483ace1","title":"从GC Roots开始对堆对象进行可达性分析,找出要回收的对象,与用户程序并发执行,重新处理SATB记录下的并发时引用变动对象
    "}],"id":"8954c96bf47e","title":"并发标记
    "},{"parent":"64ddc0929330","children":[{"parent":"dcca5f39bc8c","children":[],"id":"2044b2d42f4a","title":"处理并发阶段结束后遗留下来的少量SATB记录
    "}],"id":"dcca5f39bc8c","title":"最终标记
    "},{"parent":"64ddc0929330","children":[{"parent":"32a05d0acfd4","children":[],"id":"1c5bed528018","title":"根据用户期待的GC停顿时间制定回收计划
    "}],"id":"32a05d0acfd4","title":"筛选回收
    "}],"id":"64ddc0929330","title":"收集步骤
    "},{"parent":"9b2c5a694136","children":[{"parent":"144086fdf009","children":[{"parent":"70dcc7b7411a","children":[{"parent":"bf3ba1d7ab1a","children":[],"id":"6d91b336c4b7","title":"复制一些存活对象到Old区、Survivor区
    "}],"id":"bf3ba1d7ab1a","title":"回收所有Eden、Survivor区
    "}],"id":"70dcc7b7411a","title":"Minor GC/Young GC
    "},{"parent":"144086fdf009","children":[],"id":"44c8c447963a","title":"Mixed GC
    "}],"id":"144086fdf009","title":"回收模式
    "}],"id":"9b2c5a694136","title":"G1
    "},{"parent":"94c48a9cef88","children":[{"parent":"8e74a22eafd1","children":[],"id":"642ff9bd8dbb","title":"G1分区域 每个区域是有老年代概念的,但是收集器以整个区域为单位收集
    "},{"parent":"8e74a22eafd1","children":[],"id":"affafbc6a4da","title":"G1回收后马上合并空闲内存,而CMS会在STW的时候合并
    "}],"id":"8e74a22eafd1","title":"CMS与G1的区别
    "}],"id":"94c48a9cef88","title":"垃圾回收器
    "},{"parent":"d61da867cb10","children":[{"parent":"1dfd2da1ccb2","children":[],"id":"09de7462c062","title":"老年代空间不足
    "},{"parent":"1dfd2da1ccb2","children":[],"id":"7681c3724945","title":"system.gc()通知JVM进行Full GC
    "},{"parent":"1dfd2da1ccb2","children":[],"id":"d1038ecedd23","title":"持久代空间不足
    "}],"id":"1dfd2da1ccb2","title":"Full GC
    "},{"parent":"d61da867cb10","children":[{"parent":"a7686c6448fb","children":[],"id":"750a3f631db0","title":"在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起。是Java中一种全局暂停现象,全局停顿,所有Java代码停止,Native代码可以执行,但不能与JVM交互
    "}],"id":"a7686c6448fb","title":"STW(Stop The World)
    "},{"parent":"d61da867cb10","children":[{"parent":"a7d301228e2a","children":[],"id":"92f20719ac8a","title":"设置堆的最大最小值 -xms -xmx
    "},{"parent":"a7d301228e2a","children":[{"parent":"0715cab0f70c","children":[{"parent":"59b27c4fd939","children":[],"id":"e21f787b02c5","title":"防止年轻代堆收缩:老年代同理
    "}],"id":"59b27c4fd939","title":"-XX:newSize设置绝对大小
    "}],"id":"0715cab0f70c","title":"调整老年和年轻代的比例
    "},{"parent":"a7d301228e2a","children":[],"id":"ff8918fe5f06","title":"主要看是否存在更多持久对象和临时对象
    "},{"parent":"a7d301228e2a","children":[],"id":"24446fac73b9","title":"观察一段时间 看峰值老年代如何 不影响gc就加大年轻代
    "},{"parent":"a7d301228e2a","children":[],"id":"9204d8b66e5f","title":"配置好的机器可以用 并发收集算法
    "},{"parent":"a7d301228e2a","children":[],"id":"b58af9729fb9","title":"每个线程默认会开启1M的堆栈 存放栈帧 调用参数 局部变量 太大了  500k够了
    "},{"parent":"a7d301228e2a","children":[],"id":"c001d0fcef30","title":"原则 就是减少GC STW
    "}],"id":"a7d301228e2a","title":"性能调优
    "},{"parent":"d61da867cb10","children":[{"parent":"febcbf010f6e","children":[],"id":"0e938c11383a","title":"jasvism
    "},{"parent":"febcbf010f6e","children":[],"id":"64f7c1d4e0c1","title":"dump
    "},{"parent":"febcbf010f6e","children":[],"id":"3ae88d77a8e0","title":"监控配置 自动dump
    "}],"id":"febcbf010f6e","title":"FullGC 内存泄露排查
    "},{"parent":"d61da867cb10","children":[{"parent":"163f2b3e6279","children":[{"parent":"341ea9d0a8b1","children":[{"parent":"7b2c9c719a5b","children":[],"id":"52bb357adfd0","title":"开启逃逸分析:-XX:+DoEscapeAnalysis
    关闭逃逸分析:-XX:-DoEscapeAnalysis
    显示分析结果:-XX:+PrintEscapeAnalysis
    "}],"id":"7b2c9c719a5b","title":"Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术
    "}],"id":"341ea9d0a8b1","title":"概念
    "},{"parent":"163f2b3e6279","children":[{"parent":"2951be8a5ab7","children":[{"parent":"19414f3dfc6b","children":[],"id":"b8919c87025b","title":"即一个对象的作用范围逃出了当前方法或者当前线程
    "},{"parent":"19414f3dfc6b","children":[{"parent":"f3f5a4d41ca7","children":[],"id":"78e7003b3c6c","title":"对象是一个静态变量
    "},{"parent":"f3f5a4d41ca7","children":[],"id":"57d7fe2eba87","title":"对象是一个已经发生逃逸的对象
    "},{"parent":"f3f5a4d41ca7","children":[],"id":"f72de2af0484","title":"对象作为当前方法的返回值
    "}],"id":"f3f5a4d41ca7","title":"场景
    "}],"id":"19414f3dfc6b","title":"全局逃逸
    "},{"parent":"2951be8a5ab7","children":[{"parent":"078cd7effe02","children":[],"id":"c4a9820ede62","title":"即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸
    "}],"id":"078cd7effe02","title":"参数级逃逸
    "},{"parent":"2951be8a5ab7","children":[{"parent":"dfb6626fa13e","children":[],"id":"a1f76aa518e6","title":"即方法中的对象没有发生逃逸
    "}],"id":"dfb6626fa13e","title":"没有逃逸
    "}],"id":"2951be8a5ab7","title":"逃逸状态
    "},{"parent":"163f2b3e6279","children":[{"parent":"74538b6f0667","children":[{"parent":"22052ecf3777","children":[],"id":"1bb3c11ad593","title":"开启锁消除:-XX:+EliminateLocks
    关闭锁消除:-XX:-EliminateLocks
    "}],"id":"22052ecf3777","title":"锁消除
    "},{"parent":"74538b6f0667","children":[{"parent":"d19fda364ab3","children":[],"id":"618f15119b55","title":"开启标量替换:-XX:+EliminateAllocations
    关闭标量替换:-XX:-EliminateAllocations
    显示标量替换详情:-XX:+PrintEliminateAllocations
    "}],"id":"d19fda364ab3","title":"标量替换
    "},{"parent":"74538b6f0667","children":[{"parent":"58e0672d4093","children":[],"id":"852d0a874d5c","title":"当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了 GC 压力,提高了应用程序性能
    "}],"id":"58e0672d4093","title":"栈上分配
    "}],"id":"74538b6f0667","title":"逃逸分析优化
    "},{"parent":"163f2b3e6279","children":[{"parent":"0c9fe2ae4750","children":[],"id":"d4e70d6dd352","title":"在平时开发过程中尽可能的控制变量的作用范围了,变量范围越小越好,让虚拟机尽可能有优化的空间
    "}],"id":"0c9fe2ae4750","title":"结论
    "}],"id":"163f2b3e6279","title":"逃逸分析
    "},{"parent":"d61da867cb10","children":[{"parent":"f01a444aff49","children":[{"parent":"e5b794712d9f","children":[{"parent":"69d4722f6204","children":[],"id":"9058d548b851","title":"当堆内存(Heap Space)没有足够空间存放新创建的对象时,会抛出
    "},{"parent":"69d4722f6204","children":[{"parent":"82077529709e","children":[],"id":"e5efc4b324bd","title":"请求创建一个超大对象,通常是一个大数组
    "},{"parent":"82077529709e","children":[],"id":"f6c6d999b2e2","title":"超出预期的访问量/数据量,通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值
    "},{"parent":"82077529709e","children":[],"id":"810ed21d175e","title":"过度使用终结器(Finalizer),该对象没有立即被 GC
    "},{"parent":"82077529709e","children":[],"id":"12f8716631c0","title":"内存泄漏(Memory Leak),大量对象引用没有释放,JVM 无法对其自动回收,常见于使用了 File 等资源没有回收
    "}],"id":"82077529709e","title":"场景
    "},{"parent":"69d4722f6204","children":[{"parent":"bb0032b74d7a","children":[],"id":"4508b1e1415b","title":"针对大部分情况,通常只需要通过 -Xmx 参数调高 JVM 堆内存空间即可
    "},{"parent":"bb0032b74d7a","children":[],"id":"8a21793fb810","title":"如果是超大对象,可以检查其合理性,比如是否一次性查询了数据库全部结果,而没有做结果数限制
    "},{"parent":"bb0032b74d7a","children":[],"id":"7970de1c13ce","title":"如果是业务峰值压力,可以考虑添加机器资源,或者做限流降级
    "},{"parent":"bb0032b74d7a","children":[],"id":"61721dad15fd","title":"如果是内存泄漏,需要找到持有的对象,修改代码设计,比如关闭没有释放的连接
    "}],"id":"bb0032b74d7a","title":"解决方案
    "}],"id":"69d4722f6204","title":"Java heap space
    "},{"parent":"e5b794712d9f","children":[{"parent":"16ea8d379757","children":[],"id":"089b25ad7859","title":"当 Java 进程花费 98% 以上的时间执行 GC,但只恢复了不到 2% 的内存,且该动作连续重复了 5 次,就会抛出
    "},{"parent":"16ea8d379757","children":[],"id":"4cccd7722816","title":"场景与解决方案与Java heap space类似
    "}],"id":"16ea8d379757","title":"GC overhead limit exceeded
    "},{"parent":"e5b794712d9f","children":[{"parent":"27f250898d47","children":[],"id":"b5735a767281","title":"该错误表示永久代(Permanent Generation)已用满,通常是因为加载的 class 数目太多或体积太大
    "},{"parent":"27f250898d47","children":[{"parent":"7adba9e7ee50","children":[],"id":"f92f654bc6f2","title":"程序启动报错,修改 -XX:MaxPermSize 启动参数,调大永久代空间
    "},{"parent":"7adba9e7ee50","children":[],"id":"a1ba084f84ae","title":"应用重新部署时报错,很可能是没有应用没有重启,导致加载了多份 class 信息,只需重启 JVM 即可解决
    "},{"parent":"7adba9e7ee50","children":[],"id":"ac1dd7e1efe5","title":"运行时报错,应用程序可能会动态创建大量 class,而这些 class 的生命周期很短暂,但是 JVM 默认不会卸载 class,可以设置 -XX:+CMSClassUnloadingEnabled 和 -XX:+UseConcMarkSweepGC这两个参数允许 JVM 卸载 class
    "},{"parent":"7adba9e7ee50","children":[],"id":"9908b8d183cb","title":"如果上述方法无法解决,可以通过 jmap 命令 dump 内存对象 jmap-dump:format=b,file=dump.hprof<process-id> ,然后利用 Eclipse MAT功能逐一分析开销最大的 classloader 和重复 class
    "}],"id":"7adba9e7ee50","title":"解决方案
    "}],"id":"27f250898d47","title":"Permgen space
    "},{"parent":"e5b794712d9f","children":[{"parent":"5d9794eb44e3","children":[],"id":"ff167c68cd26","title":"该错误表示 Metaspace 已被用满,通常是因为加载的 class 数目太多或体积太大
    "},{"parent":"5d9794eb44e3","children":[],"id":"9e30222f9cdd","title":"场景与解决方案与Permgen space类似,需注意调整元空间大小参数为 -XX:MaxMetaspaceSize
    "}],"id":"5d9794eb44e3","title":"Metaspace(元空间)
    "},{"parent":"e5b794712d9f","children":[{"parent":"88733d6fd9e1","children":[],"id":"4b3170dc49e7","title":"当 JVM 向底层操作系统请求创建一个新的 Native 线程时,如果没有足够的资源分配就会报此类错误
    "},{"parent":"88733d6fd9e1","children":[{"parent":"2de99f106fd9","children":[],"id":"d2894deebffe","title":"线程数超过操作系统最大线程数 ulimit 限制
    "},{"parent":"2de99f106fd9","children":[],"id":"6b4a4957f1f7","title":"线程数超过 kernel.pid_max(只能重启)
    "},{"parent":"2de99f106fd9","children":[],"id":"697aa13d5593","title":"Native 内存不足
    "}],"id":"2de99f106fd9","title":"场景
    "},{"parent":"88733d6fd9e1","children":[{"parent":"d6e225b4cdbd","children":[],"id":"8f7d0718c783","title":"升级配置,为机器提供更多的内存
    "},{"parent":"d6e225b4cdbd","children":[],"id":"fd893dac8e71","title":"降低 Java Heap Space 大小
    "},{"parent":"d6e225b4cdbd","children":[],"id":"96d3dc50bfcf","title":"修复应用程序的线程泄漏问题
    "},{"parent":"d6e225b4cdbd","children":[],"id":"0ab47fa12a24","title":"限制线程池大小
    "},{"parent":"d6e225b4cdbd","children":[],"id":"28f3069b69cc","title":"使用 -Xss 参数减少线程栈的大小
    "},{"parent":"d6e225b4cdbd","children":[],"id":"5b123c27cf17","title":"调高 OS 层面的线程最大数:执行 ulimia-a 查看最大线程数限制,使用 ulimit-u xxx 调整最大线程数限制
    "}],"id":"d6e225b4cdbd","title":"解决方案
    "}],"id":"88733d6fd9e1","title":"Unable to create new native thread
    "},{"parent":"e5b794712d9f","children":[{"parent":"a207e8502e59","children":[],"id":"ee75aeab507e","title":"虚拟内存(Virtual Memory)由物理内存(Physical Memory)和交换空间(Swap Space)两部分组成。当运行时程序请求的虚拟内存溢出时就会报 Outof swap space? 错误
    "},{"parent":"a207e8502e59","children":[{"parent":"e9aff14f85ee","children":[],"id":"f5d926e1a54d","title":"地址空间不足
    "},{"parent":"e9aff14f85ee","children":[],"id":"0ab3e7bd4ce0","title":"物理内存已耗光
    "},{"parent":"e9aff14f85ee","children":[],"id":"a719e6915e62","title":"应用程序的本地内存泄漏(native leak),例如不断申请本地内存,却不释放
    "},{"parent":"e9aff14f85ee","children":[],"id":"95178fc0d9c2","title":"执行 jmap-histo:live<pid> 命令,强制执行 Full GC;如果几次执行后内存明显下降,则基本确认为 Direct ByteBuffer 问题
    "}],"id":"e9aff14f85ee","title":"场景
    "},{"parent":"a207e8502e59","children":[{"parent":"69838d20af49","children":[],"id":"f6eb75443db8","title":"升级地址空间为 64 bit
    "},{"parent":"69838d20af49","children":[],"id":"4c2d62f88cd0","title":"使用 Arthas 检查是否为 Inflater/Deflater 解压缩问题,如果是,则显式调用 end 方法
    "},{"parent":"69838d20af49","children":[],"id":"8149b8d47a40","title":"Direct ByteBuffer 问题可以通过启动参数 -XX:MaxDirectMemorySize 调低阈值
    "},{"parent":"69838d20af49","children":[],"id":"67cf9ef9b29d","title":"升级服务器配置/隔离部署,避免争用
    "}],"id":"69838d20af49","title":"解决方案
    "}],"id":"a207e8502e59","title":"Out of swap space?
    "},{"parent":"e5b794712d9f","children":[{"parent":"fc5d0b351a1a","children":[],"id":"dcbc57e34a45","title":"有一种内核作业(Kernel Job)名为 Out of Memory Killer,它会在可用内存极低的情况下“杀死”(kill)某些进程。OOM Killer 会对所有进程进行打分,然后将评分较低的进程“杀死”,Killprocessorsacrifice child 错误不是由 JVM 层面触发的,而是由操作系统层面触发的
    "},{"parent":"fc5d0b351a1a","children":[{"parent":"959ac5e8a270","children":[],"id":"a97b65d73f88","title":"默认情况下,Linux 内核允许进程申请的内存总量大于系统可用内存,通过这种“错峰复用”的方式可以更有效的利用系统资源。
    然而,这种方式也会无可避免地带来一定的“超卖”风险。例如某些进程持续占用系统内存,然后导致其他进程没有可用内存。此时,系统将自动激活 OOM Killer,寻找评分低的进程,并将其“杀死”,释放内存资源
    "}],"id":"959ac5e8a270","title":"场景
    "},{"parent":"fc5d0b351a1a","children":[{"parent":"a7ada05bbd00","children":[],"id":"2b1d922217cf","title":"升级服务器配置/隔离部署,避免争用
    "},{"parent":"a7ada05bbd00","children":[],"id":"b9502a1fe60b","title":"OOM Killer 调优
    "}],"id":"a7ada05bbd00","title":"解决方案
    "}],"id":"fc5d0b351a1a","title":"Kill process or sacrifice child
    "},{"parent":"e5b794712d9f","children":[{"parent":"4dfd99c9bc90","children":[],"id":"52471cca5e33","title":"JVM 限制了数组的最大长度,该错误表示程序请求创建的数组超过最大长度限制
    "},{"parent":"4dfd99c9bc90","children":[{"parent":"7a7846a84d01","children":[],"id":"ef42a376573a","title":"检查代码,确认业务是否需要创建如此大的数组,是否可以拆分为多个块,分批执行
    "}],"id":"7a7846a84d01","title":"解决方案
    "}],"id":"4dfd99c9bc90","title":"Requested array size exceeds VM limit
    "},{"parent":"e5b794712d9f","children":[{"parent":"926260b12712","children":[],"id":"7deb1188a580","title":"Direct ByteBuffer 的默认大小为 64 MB,一旦使用超出限制,就会抛出 Directbuffer memory 错误
    "},{"parent":"926260b12712","children":[{"parent":"c8bb4e5de982","children":[],"id":"558937529cce","title":"Java 只能通过 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer,因此,可以通过 Arthas 等在线诊断工具拦截该方法进行排查
    "},{"parent":"c8bb4e5de982","children":[],"id":"04d0566021d0","title":"检查是否直接或间接使用了 NIO,如 netty,jetty 等
    "},{"parent":"c8bb4e5de982","children":[],"id":"a5ff4af8fc72","title":"通过启动参数 -XX:MaxDirectMemorySize 调整 Direct ByteBuffer 的上限值
    "},{"parent":"c8bb4e5de982","children":[],"id":"eb97fd5380b3","title":"检查 JVM 参数是否有 -XX:+DisableExplicitGC 选项,如果有就去掉,因为该参数会使 System.gc()失效
    "},{"parent":"c8bb4e5de982","children":[],"id":"e19382936700","title":"检查堆外内存使用代码,确认是否存在内存泄漏;或者通过反射调用 sun.misc.Cleaner 的 clean() 方法来主动释放被 Direct ByteBuffer 持有的内存空间
    "},{"parent":"c8bb4e5de982","children":[],"id":"b338bbfe6fec","title":"内存容量确实不足,升级配置
    "}],"id":"c8bb4e5de982","title":"解决方案
    "}],"id":"926260b12712","title":"Direct buffer memory
    "}],"collapsed":false,"id":"e5b794712d9f","title":"OOM
    "},{"parent":"f01a444aff49","children":[],"id":"4fca236e0053","title":"内存泄露"},{"parent":"f01a444aff49","children":[],"id":"31b72a753814","title":"线程死锁
    "},{"parent":"f01a444aff49","children":[],"id":"f5310ade47dc","title":"锁争用
    "},{"parent":"f01a444aff49","children":[],"id":"be43a23fb08e","title":"Java进程消耗CPU过高
    "}],"id":"f01a444aff49","title":"JVM调优
    "},{"parent":"d61da867cb10","children":[{"parent":"5c6067b84b1e","children":[],"id":"03fed9bf17f7","title":"Jconsole"},{"parent":"5c6067b84b1e","children":[],"id":"b0ec90eb542b","title":"Jprofiler"},{"parent":"5c6067b84b1e","children":[],"id":"baa86063ec3e","title":"jvisualvm"},{"parent":"5c6067b84b1e","children":[],"id":"c7c342ec412e","title":"MAT"}],"id":"5c6067b84b1e","title":"JVM性能检测工具"},{"parent":"d61da867cb10","children":[{"parent":"9a58c2256f22","children":[],"id":"541fa6a430b7","title":"help dump"},{"parent":"9a58c2256f22","children":[],"id":"63ed06e60104","title":"生产机 dump"},{"parent":"9a58c2256f22","children":[],"id":"29df5ce1b166","title":"mat"},{"parent":"9a58c2256f22","children":[],"id":"a2815d62f70f","title":"jmap"},{"parent":"9a58c2256f22","children":[],"id":"aa872dbc3855","title":"-helpdump"}],"id":"9a58c2256f22","title":"内存泄露"},{"parent":"d61da867cb10","children":[{"parent":"dbe9489e1531","children":[],"id":"30af4ef10e6d","title":"topc -c"},{"parent":"dbe9489e1531","children":[],"id":"f413c8870538","title":"top -Hp pid"},{"parent":"dbe9489e1531","children":[{"parent":"d7a7bb2d1ce7","children":[],"id":"3d609323d047","title":"进制转换"}],"id":"d7a7bb2d1ce7","title":"jstack"},{"parent":"dbe9489e1531","children":[],"id":"28dc67e9bcc8","title":"cat
    "}],"id":"dbe9489e1531","title":"CPU100%"}],"collapsed":true,"id":"d61da867cb10","title":"JVM"},{"parent":"root","lineStyle":{"randomLineColor":"#FF8502"},"children":[],"id":"ecbf53fba47d","title":"多线程"},{"parent":"root","lineStyle":{"randomLineColor":"#F5479C"},"children":[{"parent":"d1383b5af44b","children":[{"parent":"6846db773567","children":[{"parent":"9cfcc064de09","children":[],"id":"3cc1cea3b5d2","title":"Spring 中的 Bean 默认都是单例的"}],"id":"9cfcc064de09","title":"单例模式"},{"parent":"6846db773567","children":[{"parent":"70e04d48b002","children":[],"id":"79b9ad909b14","title":"Spring使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象"}],"id":"70e04d48b002","title":"工厂模式"},{"parent":"6846db773567","children":[{"parent":"18539d17e1a2","children":[],"id":"f10b087651f0","title":"Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller"}],"id":"18539d17e1a2","title":"适配器模式
    "},{"parent":"6846db773567","children":[{"parent":"aa0d772a111e","children":[],"id":"458c4c117473","title":"Spring AOP 功能的实现"}],"id":"aa0d772a111e","title":"代理设计模式
    "},{"parent":"6846db773567","children":[{"parent":"cf993fcce1fa","children":[],"id":"5a9fd93671a5","title":"Spring 事件驱动模型就是观察者模式很经典的一个应用"}],"id":"cf993fcce1fa","title":"观察者模式
    "},{"parent":"6846db773567","children":[],"id":"4761f7394f26","title":"... ..."}],"id":"6846db773567","title":"设计模式"},{"parent":"d1383b5af44b","children":[{"parent":"6be9b379c857","children":[{"image":{"w":721,"h":306,"url":"http://cdn.processon.com/60d5841c07912920c8095f17?e=1624609325&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:vvIb8jvxokB9aizRUTjAy8Dz4NI="},"parent":"8218ea06bbce","children":[{"parent":"f7aea39c3b24","children":[],"id":"408ac2ef4844","title":"Bean 容器找到配置文件中 Spring Bean 的定义"},{"parent":"f7aea39c3b24","children":[],"id":"34992c358614","title":"Bean 容器利用 Java Reflection API 创建一个Bean的实例"},{"parent":"f7aea39c3b24","children":[],"id":"e7981f427e63","title":"如果涉及到一些属性值 利用 set()方法设置一些属性值"},{"parent":"f7aea39c3b24","children":[],"id":"dbf75a139571","title":"如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName()方法,传入Bean的名字"},{"parent":"f7aea39c3b24","children":[],"id":"787517f23307","title":"如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader对象的实例"},{"parent":"f7aea39c3b24","children":[],"id":"6466197a3ddf","title":"如果Bean实现了 BeanFactoryAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader 对象的实例"},{"parent":"f7aea39c3b24","children":[],"id":"feac889b1b8d","title":"与上面的类似,如果实现了其他 *.Aware接口,就调用相应的方法"},{"parent":"f7aea39c3b24","children":[],"id":"91b027b10223","title":"如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessBeforeInitialization() 方法"},{"parent":"f7aea39c3b24","children":[],"id":"b1af63401bf5","title":"如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法
    "},{"parent":"f7aea39c3b24","children":[],"id":"37fa23a9c16a","title":"如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法"},{"parent":"f7aea39c3b24","children":[],"id":"b57d01a401e0","title":"如果有和加载这个 Bean的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessAfterInitialization() 方法"},{"parent":"f7aea39c3b24","children":[],"id":"baf7bd961172","title":"当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法"},{"parent":"f7aea39c3b24","children":[],"id":"da536275f445","title":"当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法"}],"style":{"text-align":"center"},"id":"f7aea39c3b24","title":"Spring Bean 生命周期"}],"id":"8218ea06bbce","title":"生命周期"},{"parent":"6be9b379c857","children":[{"parent":"780f6b30ba11","children":[{"parent":"69fa69a6315a","children":[],"id":"847478f4b37e","title":"唯一 bean 实例,Spring 中的 bean 默认都是单例的
    "}],"id":"69fa69a6315a","title":"singleton
    "},{"parent":"780f6b30ba11","children":[{"parent":"61779d764f15","children":[],"id":"1b7c8dc3e783","title":"每次请求都会创建一个新的 bean 实例
    "}],"id":"61779d764f15","title":"prototype
    "},{"parent":"780f6b30ba11","children":[{"parent":"b84f40579383","children":[],"id":"a2c0221f2a17","title":"每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效
    "}],"id":"b84f40579383","title":"request
    "},{"parent":"780f6b30ba11","children":[{"parent":"2980654747ac","children":[],"id":"df20b0568d9b","title":"每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效
    "}],"id":"2980654747ac","title":"session
    "}],"id":"780f6b30ba11","title":"作用域"},{"parent":"6be9b379c857","children":[{"parent":"94d2392b941d","children":[],"id":"f6facb5c7bdf","title":"单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题
    "},{"parent":"94d2392b941d","children":[{"parent":"c50f84f9dd07","children":[],"id":"6a052cae7ac3","title":"在Bean对象中尽量避免定义可变的成员变量(不太现实)"},{"parent":"c50f84f9dd07","children":[],"id":"be83d8ee0d9c","title":"在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)"}],"id":"c50f84f9dd07","title":"解决方案"}],"id":"94d2392b941d","title":"单例Bean线程不安全"}],"id":"6be9b379c857","title":"Bean"},{"parent":"d1383b5af44b","children":[{"parent":"2bd94a15f271","children":[{"parent":"db24e1d06ef6","children":[],"id":"c975e1461b17","title":"循环依赖其实就是循环引用,一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了构成一个环形调用"}],"id":"db24e1d06ef6","title":"定义"},{"parent":"2bd94a15f271","children":[{"parent":"6429ee8470d0","children":[{"parent":"b444d530df33","children":[],"id":"fe4447498b81","title":"可以解决"}],"id":"b444d530df33","title":"单例setter注入
    "},{"parent":"6429ee8470d0","children":[{"parent":"a628a0c088fe","children":[],"id":"7d2affdf5098","title":"不能解决"}],"id":"a628a0c088fe","title":"多例setter注入"},{"parent":"6429ee8470d0","children":[{"parent":"52ccb73aaa1c","children":[],"id":"c5d72e9224fc","title":"不能解决"}],"id":"52ccb73aaa1c","title":"构造器注入"},{"parent":"6429ee8470d0","children":[{"parent":"e0ddcb98872d","children":[],"id":"8bc315c0e4f4","title":"有可能解决"}],"id":"e0ddcb98872d","title":"单例的代理对象注入
    "},{"parent":"6429ee8470d0","children":[{"parent":"c1883b1d1d04","children":[],"id":"30c4d71d67b7","title":"不能解决"}],"id":"c1883b1d1d04","title":"DependOn循环依赖
    "}],"id":"6429ee8470d0","title":"主要场景"},{"parent":"2bd94a15f271","children":[{"parent":"34656efe2153","children":[],"id":"07eedf3c5055","title":"一级缓存: 用于保存实例化、注入、初始化完成的bean实例
    "},{"parent":"34656efe2153","children":[],"id":"5389148434c4","title":"二级缓存: 用于保存实例化完成的bean实例"},{"parent":"34656efe2153","children":[],"id":"de57eefcdc3f","title":"三级缓存: 用于保存bean创建工厂,以便于后面扩展有机会创建代理对象"}],"id":"34656efe2153","title":"三级缓存"},{"parent":"2bd94a15f271","children":[{"image":{"w":720,"h":309,"url":"http://cdn.processon.com/60d58be3e0b34d7f1166296f?e=1624611315&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:Szb4rjzK4-RP973_dTYTIc1x1yg="},"parent":"7537dcdb4537","children":[],"style":{"text-align":"center"},"id":"813c5d07800b","title":"Spring解决循环依赖"}],"id":"7537dcdb4537","title":"Spring如何解决循环依赖?"},{"parent":"2bd94a15f271","children":[{"parent":"6809a28c84fb","children":[{"parent":"0b65efef98be","children":[],"id":"edb8657d8633","title":"使用@Lazy注解,延迟加载
    "},{"parent":"0b65efef98be","children":[],"id":"713bcafda8f8","title":"使用@DependsOn注解,指定加载先后关系
    "},{"parent":"0b65efef98be","children":[],"id":"ecde0ac2eff5","title":"修改文件名称,改变循环依赖类的加载顺序
    "}],"id":"0b65efef98be","title":"生成代理对象产生的循环依赖
    "},{"parent":"6809a28c84fb","children":[{"parent":"19fc35c94a10","children":[],"id":"a898194da78e","title":"找到@DependsOn注解循环依赖的地方,迫使它不循环依赖
    "}],"id":"19fc35c94a10","title":"使用@DependsOn产生的循环依赖
    "},{"parent":"6809a28c84fb","children":[{"parent":"67569155e5e1","children":[],"id":"fbefaf05f15b","title":"把bean改成单例"}],"id":"67569155e5e1","title":"多例循环依赖"},{"parent":"6809a28c84fb","children":[{"parent":"d1721015c62e","children":[],"id":"086c2cf4a1f1","title":"使用@Lazy注解解决"}],"id":"d1721015c62e","title":"构造器循环依赖"}],"id":"6809a28c84fb","title":"Spring无法解决的循环依赖怎么解决?"}],"id":"2bd94a15f271","title":"循环依赖"},{"parent":"d1383b5af44b","children":[{"parent":"7525c1be5308","children":[],"id":"2efd2dd7603f","title":"Spring是父容器,SpringMVC是子容器,Spring父容器中注册的Bean对SpringMVC子容器是可见的,反之则不行"}],"id":"7525c1be5308","title":"父子容器"},{"parent":"d1383b5af44b","children":[{"parent":"2d661386d933","children":[],"id":"9dedda0f87c3","title":"采用不同的连接器"},{"parent":"2d661386d933","children":[{"parent":"9478703f6033","children":[],"id":"906c6c2186d9","title":"共享链接"}],"id":"9478703f6033","title":"用AOP 新建立了一个 链接"},{"parent":"2d661386d933","children":[],"id":"c0c888325014","title":"ThreadLocal 当前事务"},{"parent":"2d661386d933","children":[],"id":"cdf3bf19398a","title":"前提是 关闭AutoCommit"}],"id":"2d661386d933","title":"事务实现原理"},{"parent":"d1383b5af44b","children":[{"parent":"3fc3209ef908","children":[{"parent":"00348542df62","children":[{"parent":"7e65680e758d","children":[],"id":"9d4a3af2166d","title":"如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务"}],"id":"7e65680e758d","title":"PROPAGATION_REQUIRED"},{"parent":"00348542df62","children":[{"parent":"fbcd20add78f","children":[],"id":"bb981fa897b5","title":"如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行"}],"id":"fbcd20add78f","title":"PROPAGATION_SUPPORTS"},{"parent":"00348542df62","children":[{"parent":"fa9c9e3e91b3","children":[],"id":"f0bd2ab03cb3","title":"如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)"}],"id":"fa9c9e3e91b3","title":"PROPAGATION_MANDATORY"}],"id":"00348542df62","title":"支持当前事务的情况"},{"parent":"3fc3209ef908","children":[{"parent":"be43db2ade73","children":[{"parent":"274fe413f9a9","children":[],"id":"a9835b9de879","title":"创建一个新的事务,如果当前存在事务,则把当前事务挂起"}],"id":"274fe413f9a9","title":"PROPAGATION_REQUIRES_NEW"},{"parent":"be43db2ade73","children":[{"parent":"6de124ee0323","children":[],"id":"73808be05652","title":"以非事务方式运行,如果当前存在事务,则把当前事务挂起"}],"id":"6de124ee0323","title":"PROPAGATION_NOT_SUPPORTED"},{"parent":"be43db2ade73","children":[{"parent":"b7f5bd597d94","children":[],"id":"aade3bc122d2","title":"以非事务方式运行,如果当前存在事务,则抛出异常"}],"id":"b7f5bd597d94","title":"PROPAGATION_NEVER"}],"id":"be43db2ade73","title":"不支持当前事务的情况"},{"parent":"3fc3209ef908","children":[{"parent":"c633fc129e54","children":[{"parent":"7f933d08995d","children":[],"id":"29d2241160ac","title":"如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED"}],"id":"7f933d08995d","title":"PROPAGATION_NESTED"}],"id":"c633fc129e54","title":"其他情况"}],"id":"3fc3209ef908","title":"事务的传播行为"},{"parent":"d1383b5af44b","children":[{"parent":"d09326c239bc","children":[{"parent":"0ddcc4fc0973","children":[],"id":"2b143ba58aa6","title":"实现类"}],"id":"0ddcc4fc0973","title":"静态代理"},{"parent":"d09326c239bc","children":[{"parent":"e88e55b4f4a1","children":[{"parent":"93e7c1779448","children":[{"parent":"2b790def7035","children":[{"parent":"8aadeb882937","children":[],"id":"b8325d0d990f","title":"调用具体方法的时候调用invokeHandler"}],"id":"8aadeb882937","title":"java反射机制生成一个代理接口的匿名类"}],"id":"2b790def7035","title":"实现接口"}],"id":"93e7c1779448","title":"JDK动态代理"},{"parent":"e88e55b4f4a1","children":[{"parent":"ad4aa9aa8708","children":[{"parent":"d62f18134c3b","children":[],"id":"f4f870f8a078","title":"修改字节码生成子类去处理"}],"id":"d62f18134c3b","title":"asm字节码编辑技术动态创建类 基于classLoad装载"}],"id":"ad4aa9aa8708","title":"cglib"}],"id":"e88e55b4f4a1","title":"动态代理"}],"id":"d09326c239bc","title":"AOP"},{"parent":"d1383b5af44b","children":[],"id":"321b75efaafa","title":"IOC"}],"collapsed":true,"id":"d1383b5af44b","title":"Spring
    "},{"parent":"root","lineStyle":{"randomLineColor":"#4D69FD"},"children":[{"parent":"93b7f849fef3","children":[{"parent":"2d833717c2db","children":[{"parent":"688bd93ca78f","children":[{"parent":"adb794ebf462","children":[],"id":"28b48a3fc7d9","title":"MVCC支持高并发、四个隔离级别(默认为可重复读)、支持事务操作、聚簇索引
    "}],"id":"adb794ebf462","title":"InnoDB
    "},{"parent":"688bd93ca78f","children":[{"parent":"9851e7c98465","children":[],"id":"6cda6ad38022","title":"全文索引、压缩、空间函数、崩溃后无法安全恢复
    "}],"id":"9851e7c98465","title":"MyISAM
    "}],"id":"688bd93ca78f","title":"常见"},{"parent":"2d833717c2db","children":[{"parent":"259d8b591849","children":[{"parent":"8af8eb9d1749","children":[],"id":"faac28b5b4bf","title":"只支持insert、select操作,适合日志和数据采集
    "}],"id":"8af8eb9d1749","title":"Archive
    "},{"parent":"259d8b591849","children":[{"parent":"246578695747","children":[],"id":"6b252c309014","title":"会丢弃所有插入数据,不做保存,记录Blackhole日志,可以用于复制数据库到备份库
    "}],"id":"246578695747","title":"Blackhole
    "},{"parent":"259d8b591849","children":[{"parent":"22c9f95864a3","children":[],"id":"c91fe24e6951","title":"可以将CSV文件作为MySQL表处理,不支持索引
    "}],"id":"22c9f95864a3","title":"CSV
    "},{"parent":"259d8b591849","children":[{"parent":"4c62ccaebd05","children":[],"id":"1707d3b04d53","title":"访问MySQL服务器的一个代理,创建远程到MySQL服务器的客户端连接,默认禁用
    "}],"id":"4c62ccaebd05","title":"Federated
    "},{"parent":"259d8b591849","children":[{"parent":"7c87e4ff16d1","children":[],"id":"66796cc84a6b","title":"数据保存在内存中,不需要磁盘I/O,重启后数据会丢失但是表结构会保留
    "}],"id":"7c87e4ff16d1","title":"Memory
    "},{"parent":"259d8b591849","children":[{"parent":"7e8ec900b7d9","children":[],"id":"260f221a82a2","title":"MyISAM变种,可以用于日志或数据仓库,已被放弃
    "}],"id":"7e8ec900b7d9","title":"Merge
    "},{"parent":"259d8b591849","children":[{"parent":"deaf720f74d6","children":[],"id":"f24022cd65fd","title":"集群引擎
    "}],"id":"deaf720f74d6","title":"NDB
    "}],"id":"259d8b591849","title":"其他(可做了解)"}],"collapsed":false,"id":"2d833717c2db","title":"存储引擎"},{"parent":"93b7f849fef3","children":[{"parent":"a5966d5788f2","children":[{"parent":"7adc8a1c18d2","children":[],"id":"a4370f67b89b","title":"binlog记录了数据库表结构和表数据变更,比如update/delete/insert/truncate/create
    "},{"parent":"7adc8a1c18d2","children":[],"id":"4aade95fc08c","title":"主要用来复制和恢复数据"}],"id":"7adc8a1c18d2","title":"binlog
    "},{"parent":"a5966d5788f2","children":[{"parent":"7577f673f85c","children":[],"id":"31aeaae43467","title":"在写入内存后会产生redo log,记录本次在某个页上做了什么修改
    "},{"parent":"7577f673f85c","children":[],"id":"142d34087f0c","title":"恢复写入内存但数据还没真正写到磁盘的数据,redo log记载的是物理变化,文件的体积很小,恢复速度很快
    "}],"id":"7577f673f85c","title":"redo log
    "},{"parent":"a5966d5788f2","children":[{"parent":"4814832d3190","children":[],"id":"c90806ae3f83","title":"undo log是逻辑日志,存储着修改之前的数据,相当于一个前版本
    "},{"parent":"4814832d3190","children":[],"id":"99805bbdffab","title":"用来回滚和多版本控制"}],"id":"4814832d3190","title":"undo log
    "},{"parent":"a5966d5788f2","children":[{"parent":"5dfacdc162c1","children":[],"id":"8d3250892fd5","title":"redo log 记录的是数据的物理变化,binlog 记录的是数据的逻辑变化"},{"parent":"5dfacdc162c1","children":[],"id":"42cefe6fd536","title":"redo log的作用是为持久化而生的,仅存储写入内存但还未刷到磁盘的数据;binlog的作用是复制和恢复而生的,保持主从数据库的一致性,如果整个数据库的数据都被删除了,可以通过binlog恢复,而redo log则不能
    "},{"parent":"5dfacdc162c1","children":[],"id":"5590b5b15841","title":"redo log是MySQL的InnoDB引擎所产生的;binlog无论MySQL用什么引擎,都会有的"},{"parent":"5dfacdc162c1","children":[{"parent":"fa7e2894f9c9","children":[{"parent":"b2f9cb6a36f3","children":[{"parent":"be9575df9c26","children":[],"id":"8b3e3c83afb7","title":"如果写redo log失败了,那我们就认为这次事务有问题,回滚,不再写binlog"},{"parent":"be9575df9c26","children":[],"id":"a1220b7a6966","title":"如果写redo log成功了,写binlog,写binlog写一半了,但失败了怎么办?我们还是会对这次的事务回滚,将无效的binlog给删除(因为binlog会影响从库的数据,所以需要做删除操作)"},{"parent":"be9575df9c26","children":[],"id":"122c9e026c22","title":"如果写redo log和binlog都成功了,那这次算是事务才会真正成功"}],"id":"be9575df9c26","title":"解析"},{"parent":"b2f9cb6a36f3","children":[{"parent":"be4d6b8613d4","children":[{"parent":"b17221822991","children":[],"id":"9383dc884572","title":"如果redo log写失败了,而binlog写成功了。那假设内存的数据还没来得及落磁盘,机器就挂掉了。那主从服务器的数据就不一致了。(从服务器通过binlog得到最新的数据,而主服务器由于redo log没有记载,没法恢复数据)"},{"parent":"b17221822991","children":[],"id":"4f824c63c2b0","title":"如果redo log写成功了,而binlog写失败了。那从服务器就拿不到最新的数据了"}],"id":"b17221822991","title":"MySQL需要保证redo log和binlog的数据是一致的"}],"id":"be4d6b8613d4","title":"结论"},{"parent":"b2f9cb6a36f3","children":[{"parent":"4c6de79ecd53","children":[{"parent":"9f54d23f7748","children":[{"parent":"4387d2b41401","children":[],"id":"a4c2899d7ffa","title":"阶段1:InnoDBredo log 写盘,InnoDB 事务进入 prepare(做好准备) 状态"},{"parent":"4387d2b41401","children":[],"id":"05dea48990c1","title":"阶段2:binlog 写盘,InooDB 事务进入 commit(提交) 状态"},{"parent":"4387d2b41401","children":[],"id":"7e939c251666","title":"每个事务binlog的末尾,会记录一个 XID event,标志着事务是否提交成功,也就是说,恢复过程中,binlog 最后一个 XID event 之后的内容都应该被 purge(清除)"}],"id":"4387d2b41401","title":"过程"}],"id":"9f54d23f7748","title":"MySQL通过两阶段提交来保证redo log和binlog的数据是一致的"}],"id":"4c6de79ecd53","title":"保持一致性的方法"}],"id":"b2f9cb6a36f3","title":"引申问题:在写入某一个log,失败了,那会怎么办?比如先写redo log,再写binlog"}],"id":"fa7e2894f9c9","title":"redo log事务开始的时候,就开始记录每次的变更信息,而binlog是在事务提交的时候才记录"}],"id":"5dfacdc162c1","title":"binlog与redo log的区别"},{"parent":"a5966d5788f2","children":[{"parent":"cea3499ae4ce","children":[{"parent":"e50c2afacca9","children":[],"id":"18fce611a383","title":"默认不开启,需要手动将参数设置为ON"}],"id":"e50c2afacca9","title":"慢查询日志,记录所有执行时间超过long_query_time的所有查询或不使用索引的查询
    "}],"id":"cea3499ae4ce","title":"slow log
    "}],"collapsed":false,"id":"a5966d5788f2","title":"log"},{"parent":"93b7f849fef3","children":[{"parent":"5210cc3e2201","children":[{"parent":"9901dd2630fa","children":[{"parent":"f2714fc25cf8","children":[{"parent":"72e55f7388bc","children":[],"id":"efa64c497a6e","title":"B树在提高了IO性能的同时并没有解决元素遍历的效率低下的问题,为了解决这个问题产生了B+树。B+树只需要去遍历叶子节点就可以实现整棵树的遍历。而在数据库中基于范围的查询是非常频繁的,但B树不支持这样的操作或者说效率太低"}],"id":"72e55f7388bc","title":"为什么选用B+Tree不选择B-Tree?"},{"parent":"f2714fc25cf8","children":[{"parent":"118574a529fa","children":[{"parent":"e98c2b0bce63","children":[],"id":"70ba3b2ec259","title":"和索引中的所有列进行匹配"}],"id":"e98c2b0bce63","title":"全值匹配"},{"parent":"118574a529fa","children":[{"parent":"f6fe4bbe8879","children":[],"id":"6650dea74984","title":"只使用索引的第一列"}],"id":"f6fe4bbe8879","title":"匹配最左前缀"},{"parent":"118574a529fa","children":[{"parent":"a8e32170dad9","children":[],"id":"0736d8171b6b","title":"只匹配某一列值的开头部分"}],"id":"a8e32170dad9","title":"匹配列前缀"},{"parent":"118574a529fa","children":[{"parent":"c3ecae94375c","children":[],"id":"d6ce88217808","title":"查找范围区间"}],"id":"c3ecae94375c","title":"匹配范围值"},{"parent":"118574a529fa","children":[{"parent":"f7a85f0b29a2","children":[],"id":"f2c263321a5b","title":"第一列全匹配,第二列匹配范围区间"}],"id":"f7a85f0b29a2","title":"精确匹配某一列并范围匹配另外一列"},{"parent":"118574a529fa","children":[{"parent":"00e02f358513","children":[],"id":"7eaacfcc4407","title":"覆盖索引"}],"id":"00e02f358513","title":"只访问索引的查询"}],"id":"118574a529fa","title":"适用范围"}],"id":"f2714fc25cf8","title":"B+Tree 索引"},{"parent":"9901dd2630fa","children":[{"parent":"079407222137","children":[{"parent":"2961e3444d75","children":[],"id":"9d63f5055547","title":"只有精确匹配索引所有列的查询才有效"}],"id":"2961e3444d75","title":"等值查询"}],"id":"079407222137","title":"Hash 索引"},{"parent":"9901dd2630fa","children":[{"parent":"ac2bb051dc41","children":[],"id":"b1fc3ee864fd","title":"MyISAM表支持,可以用作地理数据存储"}],"id":"ac2bb051dc41","title":"R- Tree 索引(空间数据索引)"},{"parent":"9901dd2630fa","children":[{"parent":"98bc7018f2e8","children":[],"id":"74d8f80862a6","title":"MyISAM表支持,查找文本中的关键字"}],"id":"98bc7018f2e8","title":"全文索引"}],"id":"9901dd2630fa","title":"常见索引"},{"parent":"5210cc3e2201","children":[{"parent":"eb2a05c840d5","children":[],"id":"38870688a881","title":"InnoDB通过主键聚集数据,若没有主键则会选择一个唯一非空索引代替,若都不存在则会隐式定义一个主键来作为聚簇索引"}],"id":"eb2a05c840d5","title":"聚簇索引"},{"parent":"5210cc3e2201","children":[{"parent":"c1563189924d","children":[],"id":"13d6a347d2e2","title":"会多进行一次扫描(回表操作)"}],"id":"c1563189924d","title":"非聚簇索引"},{"parent":"5210cc3e2201","children":[{"parent":"1c121f6da170","children":[{"parent":"e0e8ecd756d7","children":[],"id":"f3837dae5ad5","title":"索引不能是表达式的一部分,也不能是函数的参数
    "}],"id":"e0e8ecd756d7","title":"独立的列"},{"parent":"1c121f6da170","children":[{"parent":"a3bd55e14de1","children":[],"id":"043d3ebd14c6","title":"在需要使用多列作为查询条件时,联合索引比使用多个单列索引性能更好
    "}],"id":"a3bd55e14de1","title":"多列索引"},{"parent":"1c121f6da170","children":[{"parent":"d28f309facf7","children":[],"id":"cf867ae6885b","title":"将选择行最强的索引列放在最前面
    "},{"parent":"d28f309facf7","children":[],"id":"977d01e8182f","title":"索引的选择性:不重复的索引值和记录总数的对比
    "}],"id":"d28f309facf7","title":"索引列的顺序"},{"parent":"1c121f6da170","children":[{"parent":"6ed32658245b","children":[],"id":"a735cff03984","title":"对BLOG、TEXT、VARCHAR类型的列,使用前缀索引,索引开始的部分字符
    "},{"parent":"6ed32658245b","children":[],"id":"9af199030361","title":"前缀索引的长度选取,需根据索引的选择性来确定
    "}],"id":"6ed32658245b","title":"前缀索引"},{"parent":"1c121f6da170","children":[{"parent":"861978fd7ef6","children":[],"id":"94eacfaadf23","title":"索引包含所有需要查询的字段值"},{"parent":"861978fd7ef6","children":[{"parent":"5d4e9d8774fd","children":[],"id":"5d9d32175efc","title":"索引远小于数据行的大小,只读取索引能够减少数据访问量
    "},{"parent":"5d4e9d8774fd","children":[],"id":"858036b36f18","title":"不用回表"}],"id":"5d4e9d8774fd","title":"优点"}],"id":"861978fd7ef6","title":"覆盖索引"}],"id":"1c121f6da170","title":"索引优化"},{"parent":"5210cc3e2201","children":[{"parent":"a88af8d55657","children":[],"id":"d05b0626264c","title":"大大减少了服务器需要扫描的数据行数"},{"parent":"a88af8d55657","children":[],"id":"f685636c3846","title":"帮助服务器避免进行排序和分组,以及避免创建临时表"},{"parent":"a88af8d55657","children":[],"id":"3df58d5d4a3b","title":"将随机 I/O 变为顺序 I/O"}],"id":"a88af8d55657","title":"索引的优点"},{"parent":"5210cc3e2201","children":[{"parent":"1792cb752801","children":[{"parent":"cef88efe00e5","children":[],"id":"c1e59b66fefb","title":"因为如果不是覆盖索引需要回表"}],"id":"cef88efe00e5","title":"对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效"},{"parent":"1792cb752801","children":[],"id":"64956008bace","title":"对于中到大型的表,索引非常有效"},{"parent":"1792cb752801","children":[],"id":"4070060fdb7d","title":"对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术"}],"id":"1792cb752801","title":"使用条件"}],"collapsed":false,"id":"5210cc3e2201","title":"索引"},{"parent":"93b7f849fef3","children":[{"parent":"1a7dcec2ca47","children":[{"parent":"85c67aaf34a6","children":[{"parent":"35c936f7bdfa","children":[],"id":"a596da0b2488","title":"SIMPLE 简单查询"},{"parent":"35c936f7bdfa","children":[],"id":"f4b9fe60c553","title":"UNION 联合查询"},{"parent":"35c936f7bdfa","children":[],"id":"5f4c581eaea4","title":"SUBQUERY 子查询"}],"id":"35c936f7bdfa","title":"select_type"},{"parent":"85c67aaf34a6","children":[{"parent":"46d559313322","children":[],"id":"c4ee393f6a07","title":"查询的表"}],"id":"46d559313322","title":"table"},{"parent":"85c67aaf34a6","children":[{"parent":"a7b85d2883a5","children":[],"id":"b616d85f45b2","title":"system"},{"parent":"a7b85d2883a5","children":[{"parent":"35007f4ba57f","children":[],"id":"f1988a49fc81","title":"只有一条查询结果&主键/唯一索引"}],"id":"35007f4ba57f","title":"const"},{"parent":"a7b85d2883a5","children":[{"parent":"ad8c20fd30de","children":[],"id":"96881f188e51","title":"链接查询&主键/唯一索引&只有一条查询结果"}],"id":"ad8c20fd30de","title":"eq_ref"},{"parent":"a7b85d2883a5","children":[{"parent":"f42e8c179697","children":[],"id":"f02141ac36a6","title":"非唯一索引"}],"id":"f42e8c179697","title":"ref"},{"parent":"a7b85d2883a5","children":[{"parent":"43897738682a","children":[],"id":"910cd850cb27","title":"使用索引进行范围查询时"}],"id":"43897738682a","title":"range"},{"parent":"a7b85d2883a5","children":[{"parent":"8fef129a816a","children":[],"id":"200bdd342f1e","title":"查询的字段时索引的一部分,覆盖索引"},{"parent":"8fef129a816a","children":[],"id":"462b089e56d4","title":"使用主键排序"}],"id":"8fef129a816a","title":"index"},{"parent":"a7b85d2883a5","children":[{"parent":"1bb5aaa0dd0d","children":[],"id":"764244bd2a17","title":"全表扫描"}],"id":"1bb5aaa0dd0d","title":"all"}],"id":"a7b85d2883a5","title":"type"},{"parent":"85c67aaf34a6","children":[{"parent":"b31b3a54b63b","children":[],"id":"dde0162686c2","title":"可选择的索引"}],"id":"b31b3a54b63b","title":"possible_keys"},{"parent":"85c67aaf34a6","children":[{"parent":"81ad0f7b17de","children":[],"id":"905080a6f852","title":"实际使用的索引"}],"id":"81ad0f7b17de","title":"key"},{"parent":"85c67aaf34a6","children":[{"parent":"e313359e3213","children":[],"id":"9094ee686fb6","title":"扫描的行数"}],"id":"e313359e3213","title":"rows"}],"id":"85c67aaf34a6","title":"使用 Explain 分析 Select 查询语句
    "},{"parent":"1a7dcec2ca47","children":[{"parent":"766730b3f63f","children":[{"parent":"e4c6eb7dee1d","children":[],"id":"f00841c92b2d","title":"只查询必要的列,对使用*永远持怀疑态度"},{"parent":"e4c6eb7dee1d","children":[],"id":"6396af513501","title":"只返回必要的行,使用Limit限制返回行数"},{"parent":"e4c6eb7dee1d","children":[],"id":"e69aa7cfb00a","title":"缓存重复查询的数据,例如使用redis"}],"id":"e4c6eb7dee1d","title":"减少请求的数据量"},{"parent":"766730b3f63f","children":[{"parent":"64889a19144a","children":[],"id":"e3d6b04e7263","title":"使用索引覆盖查询"}],"id":"64889a19144a","title":"减少数据库扫描的行数"}],"id":"766730b3f63f","title":"优化数据访问"},{"parent":"1a7dcec2ca47","children":[{"parent":"879ad88fe515","children":[],"id":"4fcee3d0c287","title":"有时候一个复杂的查询并没有多个简单查询执行迅速"},{"parent":"879ad88fe515","children":[],"id":"344a37829ef0","title":"对于大查询,可以使用切分查询,每次只返回一小部分"},{"parent":"879ad88fe515","children":[],"id":"fd9207da3a3a","title":"对于关联查询,可根据情况进行分解,在应用程序中进行关联"},{"parent":"879ad88fe515","children":[],"id":"27dc6dbe2891","title":"对于大分页等场景, 可采用延迟关联
    "}],"id":"879ad88fe515","title":"重构查询方式"}],"collapsed":false,"id":"1a7dcec2ca47","title":"查询性能优化"},{"parent":"93b7f849fef3","children":[{"parent":"f349bf5b5c84","children":[{"parent":"8a0fefaa1a67","children":[],"id":"9efbed658957","title":"原子性
    "},{"parent":"8a0fefaa1a67","children":[],"id":"1fa9df39ec26","title":"一致性"},{"parent":"8a0fefaa1a67","children":[],"id":"814aae5b441a","title":"隔离性"},{"parent":"8a0fefaa1a67","children":[],"id":"98bb1a37de0d","title":"持久性"}],"collapsed":false,"id":"8a0fefaa1a67","title":"ACID"},{"parent":"f349bf5b5c84","children":[{"parent":"d4c5e6c7b97d","children":[{"parent":"46a0e40a540b","children":[],"id":"97fc28724d63","title":"事务读取未提交的数据"}],"id":"46a0e40a540b","title":"脏读"},{"parent":"d4c5e6c7b97d","children":[{"parent":"0a6b6f451a92","children":[],"id":"80ae6dfb6f8c","title":"一个事务内,多次读同一数据。A事务未结束,B事务访问同一数据,A事务读取两次可能不一致
    "}],"id":"0a6b6f451a92","title":"不可重复读"},{"parent":"d4c5e6c7b97d","children":[{"parent":"c653b25154d9","children":[],"id":"dd1fc239cca6","title":"一个事务在前后两次查询同一范围的时候,后一次查询看到了前一次查询没有看到的行。事务A 按照一定条件进行数据读取, 期间事务B 插入了相同搜索条件的新数据,事务A再次按照原先条件进行读取时,发现了事务B 新插入的数据
    "}],"id":"c653b25154d9","title":"幻读"},{"parent":"d4c5e6c7b97d","children":[{"parent":"ca82f84e0c45","children":[],"id":"0bae386e19f5","title":"一个事务的更新操作会被另一个事务的更新操作所覆盖"},{"parent":"ca82f84e0c45","children":[],"id":"b317c67d4337","title":"例如:T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改"}],"id":"ca82f84e0c45","title":"丢失更新"}],"collapsed":false,"id":"d4c5e6c7b97d","title":"脏读、不可重复读、幻读、丢失更新
    "},{"parent":"f349bf5b5c84","children":[{"parent":"54cad68df1e1","children":[{"parent":"187fad689217","children":[],"id":"40001f009aca","title":"事务可以读取未提交数据(脏读)
    "},{"parent":"187fad689217","children":[],"id":"74d2f0b7e8ff","title":"存在脏读、不可重复读、幻读"}],"id":"187fad689217","title":"读未提交
    "},{"parent":"54cad68df1e1","children":[{"parent":"831af13c1fe3","children":[],"id":"7e9293ed801e","title":"事务只可以读取已经提交的事务所做的修改
    "},{"parent":"831af13c1fe3","children":[],"id":"7214bb4bdc0e","title":"存在不可重复读、幻读"}],"id":"831af13c1fe3","title":"读已提交
    "},{"parent":"54cad68df1e1","children":[{"parent":"ddabe3557266","children":[],"id":"a6098ce57c11","title":"同一个事务多次读取同样记录结果一致
    "},{"parent":"ddabe3557266","children":[],"id":"82158205dff5","title":"存在幻读"},{"parent":"ddabe3557266","children":[],"id":"2d11fbd7845e","title":"InnoDB默认级别
    "}],"id":"ddabe3557266","title":"可重复读
    "},{"parent":"54cad68df1e1","children":[{"parent":"329045116b43","children":[],"id":"f201ca426fb5","title":"读取每一行数据上都加锁
    "},{"parent":"329045116b43","children":[],"id":"9040c49f6995","title":"存在加锁读"}],"id":"329045116b43","title":"可串行化
    "}],"collapsed":false,"id":"54cad68df1e1","title":"事务隔离级别"},{"parent":"f349bf5b5c84","children":[{"parent":"0c3fe336f085","children":[{"parent":"e1911d4cb48b","children":[{"parent":"010c32dd6618","children":[],"id":"7abe6c3095a9","title":"允许事务读一行数据"}],"id":"010c32dd6618","title":"共享锁(读锁)"},{"parent":"e1911d4cb48b","children":[{"parent":"e440236253d1","children":[],"id":"27e41f2eeea3","title":"允许事务删除或更新一行数据"}],"id":"e440236253d1","title":"排他锁(写锁)"},{"parent":"e1911d4cb48b","children":[{"parent":"ae79dcf5ed09","children":[],"id":"b09a0093dd92","title":"事务想要获取一张表中某几行的共享锁
    "}],"id":"ae79dcf5ed09","title":"意向共享锁"},{"parent":"e1911d4cb48b","children":[{"parent":"a843c2d1177e","children":[],"id":"470c8fe9653b","title":"事务想要获取一张表中某几行的排他锁"}],"id":"a843c2d1177e","title":"意向排他锁"}],"id":"e1911d4cb48b","title":"锁类型
    "},{"parent":"0c3fe336f085","children":[{"parent":"3a31e35e17d1","children":[{"parent":"8458c31623f5","children":[],"id":"2fb5f24d4ca4","title":"锁定整张表,是开销最小的策略"},{"parent":"8458c31623f5","children":[],"id":"fc59fa7dd010","title":"开销小,加锁快;无死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低"}],"id":"8458c31623f5","title":"表锁"},{"parent":"3a31e35e17d1","children":[{"parent":"85be4bb1b97c","children":[],"id":"7a8a1338b899","title":"行级锁只对用户正在访问的行进行锁定"},{"parent":"85be4bb1b97c","children":[],"id":"1fe3439f48cb","title":"开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高"},{"parent":"85be4bb1b97c","children":[{"parent":"767fe9aba9f7","children":[],"id":"e87c3357ae96","title":"当行锁涉及到索引失效的时候,会触发表锁的行为"}],"id":"767fe9aba9f7","title":"升级行为"}],"id":"85be4bb1b97c","title":"行锁"},{"parent":"3a31e35e17d1","children":[],"id":"30d6e73385cf","title":"间隙锁"}],"id":"3a31e35e17d1","title":"锁粒度"}],"id":"0c3fe336f085","title":"锁"},{"parent":"f349bf5b5c84","children":[{"parent":"eed5a9459b5a","children":[{"parent":"ce09c69be50f","children":[{"parent":"eed8dbe525bf","children":[],"id":"8e7afdfa1b51","title":"像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读。它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁"}],"id":"eed8dbe525bf","title":"当前读"},{"parent":"ce09c69be50f","children":[{"parent":"64325b6f0da8","children":[],"id":"2025e92bff9a","title":"不加锁的select操作就是快照读。快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。快照读基于MVCC,为了提高并发性能的考虑
    "}],"id":"64325b6f0da8","title":"快照读"}],"id":"ce09c69be50f","title":"当前读与快照读"},{"parent":"eed5a9459b5a","children":[{"parent":"287cd593da0c","children":[{"parent":"9d63e35bb5f6","children":[],"id":"2c3c0a528cb3","title":"存储的每次对某条聚簇索引记录进行修改的时候的事务id"}],"id":"9d63e35bb5f6","title":"trx_id"},{"parent":"287cd593da0c","children":[{"parent":"01688368c063","children":[],"id":"1a6030a34904","title":"一个指针,它指向这条聚簇索引记录的上一个版本的位置
    "}],"id":"01688368c063","title":"roll_pointer
    "}],"id":"287cd593da0c","title":"聚簇索引中的隐藏列"},{"parent":"eed5a9459b5a","children":[{"parent":"629025098291","children":[],"id":"fd5bc24b1ab0","title":"Read View 保存了当前事务开启时所有活跃的事务列表"}],"id":"629025098291","title":"ReadView
    "},{"parent":"eed5a9459b5a","children":[{"parent":"0a9f4b6b3fd8","children":[],"id":"2fda9cd25a51","title":"获取事务自己的版本号,即 事务ID
    "},{"parent":"0a9f4b6b3fd8","children":[],"id":"69d637f03ec4","title":"获取 ReadView
    "},{"parent":"0a9f4b6b3fd8","children":[{"parent":"d234974b39a2","children":[{"parent":"00c306141cc3","children":[],"id":"6793592fd10e","title":"直接读取最新版本ReadView
    "}],"id":"00c306141cc3","title":"读未提交
    "},{"parent":"d234974b39a2","children":[{"parent":"cd3375057c84","children":[],"id":"d9b62e5bee77","title":"每次查询的开始都会生成一个独立的ReadView
    "}],"id":"cd3375057c84","title":"读已提交
    "},{"parent":"d234974b39a2","children":[{"parent":"b9c1beaa17b7","children":[],"id":"37c1b07a6706","title":"可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView
    "}],"id":"b9c1beaa17b7","title":"可重复读
    "}],"id":"d234974b39a2","title":"查询得到的数据,然后与 ReadView 中的事务版本号进行比较
    "},{"parent":"0a9f4b6b3fd8","children":[],"id":"0931e9c7e939","title":"如果不符合 ReadView 规则, 那么就需要 UndoLog 中历史快照
    "},{"parent":"0a9f4b6b3fd8","children":[],"id":"ed2a683bfd62","title":"最后返回符合规则的数据
    "}],"id":"0a9f4b6b3fd8","title":"实现原理"}],"id":"eed5a9459b5a","title":"MVCC"}],"collapsed":false,"id":"f349bf5b5c84","title":"事务"},{"parent":"93b7f849fef3","children":[{"parent":"7bb42e3d47e8","children":[{"parent":"f04740c1a26a","children":[{"parent":"6344bab34462","children":[],"id":"e844fe3ee99d","title":"借助第三方工具pt-online-schema-change"}],"id":"6344bab34462","title":"修改为bigint"},{"parent":"f04740c1a26a","children":[],"id":"c9cc52e3755f","title":"一般用不完就分库分表了, 使用全局唯一id"}],"id":"f04740c1a26a","title":"自增id用完了"},{"parent":"7bb42e3d47e8","children":[{"parent":"827ace408eb2","children":[],"id":"0662d7098e60","title":"设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count参数,减少binlog的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。

    将sync_binlog 设置为大于1的值(比较常见是100~1000)。这样做的风险是,主机掉电时会丢binlog日志。

    将innodb_flush_log_at_trx_commit设置为2。这样做的风险是,主机掉电的时候会丢数据。"}],"id":"827ace408eb2","title":"IO性能瓶颈"}],"id":"7bb42e3d47e8","title":"常见问题"}],"collapsed":true,"id":"93b7f849fef3","title":"MySQL"},{"parent":"root","lineStyle":{"randomLineColor":"#F5479C"},"children":[{"parent":"68e2d53f78c0","children":[{"parent":"a2ba7f2839e7","children":[],"id":"332a1e1ff9de","title":"秒杀的库存扣减"},{"parent":"a2ba7f2839e7","children":[],"id":"fdc0cda55b5c","title":"APP首页的访问流量高峰"},{"parent":"a2ba7f2839e7","children":[],"id":"235928a55a4d","title":"避免数据库打崩"}],"id":"a2ba7f2839e7","title":"为什么使用Redis"},{"parent":"68e2d53f78c0","children":[{"parent":"1bbcd515493d","children":[{"parent":"d6e64200f130","children":[{"parent":"c1b235ccc3a0","children":[],"id":"af253a7e312d","title":"key-value,类似ArrayList"}],"id":"c1b235ccc3a0","title":"String"},{"parent":"d6e64200f130","children":[],"id":"805ec0ae37ce","title":"Hash"},{"parent":"d6e64200f130","children":[],"id":"18c40debca54","title":"set"},{"parent":"d6e64200f130","children":[],"id":"5aba119e78a1","title":"zset"},{"parent":"d6e64200f130","children":[],"id":"466e3ae93922","title":"List"}],"id":"d6e64200f130","title":"必会项"},{"parent":"1bbcd515493d","children":[{"parent":"e08a90c96b17","children":[{"parent":"aac9ee1c9de2","children":[],"id":"6529bc24a01c","title":"统计网站UV(独立访客,每个用户每天只记录一次)"}],"id":"aac9ee1c9de2","title":"HyperLogLog
    "},{"parent":"e08a90c96b17","children":[{"parent":"e9b70d2f5e4a","children":[],"id":"09ba2789d093","title":"计算地理位置,类似功能附近的人"}],"id":"e9b70d2f5e4a","title":"Geo"},{"parent":"e08a90c96b17","children":[{"parent":"36770e0d1f7f","children":[],"id":"951025427dd0","title":"消息的多播,发布/订阅,无法持久化"}],"id":"36770e0d1f7f","title":"Pub/Sub"},{"parent":"e08a90c96b17","children":[{"parent":"504b0480286f","children":[],"id":"ff3c30b3b218","title":"用户签到,短视频属性(特效,加锁),用户在线状态,活跃状态
    "}],"id":"504b0480286f","title":"BitMap"},{"parent":"e08a90c96b17","children":[{"parent":"750183397ab0","children":[{"parent":"d912173e6c29","children":[],"id":"c0d25c9698a3","title":"缓存穿透"},{"parent":"d912173e6c29","children":[],"id":"cea22ee9fb6b","title":"海量数据去重"}],"id":"d912173e6c29","title":"使用场景"},{"parent":"750183397ab0","children":[{"parent":"d1cce1828064","children":[{"parent":"1c2955281250","children":[],"id":"ebe498b35f4e","title":"可以通过建立一个白名单来存储可能会误判的元素"}],"id":"1c2955281250","title":"存在误判"},{"parent":"d1cce1828064","children":[{"parent":"e4d933c94529","children":[],"id":"0f690c41bf85","title":"可以采用Counting Bloom Filter"}],"id":"e4d933c94529","title":"删除困难"}],"id":"d1cce1828064","title":"缺点"},{"parent":"750183397ab0","children":[{"parent":"3ee30b7a6a80","children":[],"id":"c672a0977842","title":"redisson"},{"parent":"3ee30b7a6a80","children":[],"id":"3525627855c4","title":"guava"}],"id":"3ee30b7a6a80","title":"现成的轮子"},{"parent":"750183397ab0","children":[{"parent":"e23b985c75be","children":[{"parent":"015aee9efba2","children":[],"id":"7a36e20faba7","title":"可进行设置, 默认值为0.03"}],"id":"015aee9efba2","title":"预估数据量n以及期望的误判率fpp"},{"parent":"e23b985c75be","children":[{"parent":"2aa25fd1f61c","children":[{"parent":"2b742b98e5af","children":[],"style":{"font-size":19},"id":"6e1979c1417e","title":""}],"id":"2b742b98e5af","title":"Bit数组大小"},{"parent":"2aa25fd1f61c","children":[{"parent":"744b38ebf3de","children":[],"style":{"font-size":20},"id":"19690dd504f2","title":""}],"id":"744b38ebf3de","title":"哈希函数选择"}],"id":"2aa25fd1f61c","title":"hash函数的选取以及bit数组的大小"}],"id":"e23b985c75be","title":"实现"}],"id":"750183397ab0","title":"BloomFilter (布隆过滤器)"},{"parent":"e08a90c96b17","children":[{"parent":"a3817cda1299","children":[],"id":"74b95271d80f","title":"可持久化的消息队列,支持多播"}],"id":"a3817cda1299","title":"Stream"}],"id":"e08a90c96b17","title":"加分项"}],"id":"1bbcd515493d","title":"数据结构"},{"parent":"68e2d53f78c0","children":[{"parent":"7742814a28a8","children":[{"parent":"a5a4136392f3","children":[{"parent":"dac8a72c8af3","children":[],"id":"2e0bddd4ebac","title":"Redis中大量key同一时间失效, 导致大量请求落库
    "}],"id":"dac8a72c8af3","title":"概念"},{"parent":"a5a4136392f3","children":[{"parent":"4f3f99b22654","children":[{"parent":"cae81e94657c","children":[],"id":"f458c0739724","title":"加随机过期时间, 避免大量key同时失效"},{"parent":"cae81e94657c","children":[],"id":"daaa9f606c2b","title":"设置热点数据永不过期
    "},{"parent":"cae81e94657c","children":[],"id":"1d88c6e21c3c","title":"如果Redis是集群部署, 将热点数据均匀分布在不同的Redis库中也能避免全部失效的问题"}],"id":"cae81e94657c","title":"预防方案"},{"parent":"4f3f99b22654","children":[{"parent":"c3bea9f91a7e","children":[],"id":"a7313b4d8479","title":"设置本地缓存(ehcache)+限流(hystrix),尽量避免请求过多打入数据库导致数据库宕机
    "}],"id":"c3bea9f91a7e","title":"万一发生"},{"parent":"4f3f99b22654","children":[{"parent":"88f98afe7a1c","children":[],"id":"921be9f40b92","title":"redis持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据
    "}],"id":"88f98afe7a1c","title":"挂掉以后"}],"id":"4f3f99b22654","title":"解决方案"}],"id":"a5a4136392f3","title":"缓存雪崩"},{"parent":"7742814a28a8","children":[{"parent":"16f48041dfe8","children":[{"parent":"18dad091e32b","children":[],"id":"d4a29ef74f33","title":"缓存和数据库中都没有的数据,而用户不断发起请求, 导致数据库压力过大,严重会击垮数据库
    "}],"id":"18dad091e32b","title":"概念"},{"parent":"16f48041dfe8","children":[{"parent":"c83b9f04cec3","children":[{"parent":"f1ca0b7df56c","children":[],"id":"06abfdcea127","title":"接口层增加校验,比如用户鉴权校验,参数做校验,存在不合法的参数直接做短路操作"},{"parent":"f1ca0b7df56c","children":[{"parent":"0a8f16c78524","children":[],"id":"77c829f772d4","title":"不存在则不请求"}],"id":"0a8f16c78524","title":"布隆过滤器"}],"id":"f1ca0b7df56c","title":"过滤方式"},{"parent":"c83b9f04cec3","children":[{"parent":"e9827ebf2fbc","children":[],"id":"f58a9e45bbfb","title":"将对应Key的Value对写为null、或其他提示信息,缓存有效时间可以设置短点,如30秒"}],"id":"e9827ebf2fbc","title":"临时返回"}],"id":"c83b9f04cec3","title":"解决方案"}],"id":"16f48041dfe8","title":"缓存穿透"},{"parent":"7742814a28a8","children":[{"parent":"89e2121fac66","children":[{"parent":"72782b925676","children":[],"id":"ccdc7563e860","title":"一个非常热点的Key,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库
    "}],"id":"72782b925676","title":"概念"},{"parent":"89e2121fac66","children":[{"parent":"f2b90513c9c0","children":[],"id":"48a839f3015b","title":"互斥锁"},{"parent":"f2b90513c9c0","children":[],"id":"217261d1bab5","title":"设置热点数据永不过期"}],"id":"f2b90513c9c0","title":"解决方案"}],"id":"89e2121fac66","title":"缓存击穿"},{"parent":"7742814a28a8","children":[{"parent":"796744a8869c","children":[{"parent":"9cb88abbc2df","children":[],"id":"d4df0f21db8b","title":"数据库与缓存内数据不一致问题"}],"id":"9cb88abbc2df","title":"定义"},{"parent":"796744a8869c","children":[{"parent":"cd7299545586","children":[{"parent":"2a161277de43","children":[],"id":"f7e64d411a34","title":"保持最终一致性, 当缓存中没有时会从数据库读取"}],"id":"2a161277de43","title":"设置过期时间"},{"parent":"cd7299545586","children":[{"parent":"787b437c7b36","children":[{"parent":"6a186a44f666","children":[],"id":"71f5b508690c","title":"第一步成功(操作数据库),第二步失败(删除缓存),会导致数据库里是新数据,而缓存里是旧数据"},{"parent":"6a186a44f666","children":[],"id":"26959b0b8788","title":"如果第一步(操作数据库)就失败了,直接返回错误(Exception),不会出现数据不一致
    "},{"parent":"6a186a44f666","children":[{"parent":"2daec7e7e0d5","children":[{"parent":"cc61025444c8","children":[],"id":"27c9e98de34b","title":"不一致,但出现概率很低"}],"id":"cc61025444c8","title":"
    • 缓存刚好失效
    • 线程A查询数据库,得一个旧值
    • 线程B将新值写入数据库
    • 线程B删除缓存
    • 线程A将查到的旧值写入缓存

    "}],"id":"2daec7e7e0d5","title":"并发场景"}],"id":"6a186a44f666","title":"原子性被破坏情景"},{"parent":"787b437c7b36","children":[{"parent":"a9947ca1b3c6","children":[],"id":"221e14e48d66","title":"将需要删除的key发送到消息队列中
    "},{"parent":"a9947ca1b3c6","children":[],"id":"d340f9c73068","title":"自己消费消息,获得需要删除的key
    "},{"parent":"a9947ca1b3c6","children":[],"id":"cdd19d4a6cd3","title":"不断重试删除操作,直到成功
    "}],"id":"a9947ca1b3c6","title":"删除缓存失败的解决思路"},{"parent":"787b437c7b36","children":[{"parent":"7b07db7a011b","children":[],"id":"51870fc09c5a","title":"高并发下表现优异,在原子性被破坏时表现不如意"}],"id":"7b07db7a011b","title":"结论"}],"id":"787b437c7b36","title":"先更新数据库,再删除缓存"},{"parent":"cd7299545586","children":[{"parent":"99a36d602e49","children":[{"parent":"42e69abbccbe","children":[],"id":"4d13edae29bf","title":"第一步成功(删除缓存),第二步失败(更新数据库),数据库和缓存的数据还是一致的
    "},{"parent":"42e69abbccbe","children":[],"id":"362019fddab4","title":"如果第一步(删除缓存)就失败了,我们可以直接返回错误(Exception),数据库和缓存的数据还是一致的"},{"parent":"42e69abbccbe","children":[{"parent":"2217b4dc2182","children":[{"parent":"a9df8552a646","children":[],"id":"9d945a5b687c","title":"不一致"}],"id":"a9df8552a646","title":"
    • 线程A删除了缓存
    • 线程B查询,发现缓存已不存在
    • 线程B去数据库查询得到旧值
    • 线程B将旧值写入缓存
    • 线程A将新值写入数据库
    "}],"id":"2217b4dc2182","title":"并发场景"}],"id":"42e69abbccbe","title":"原子性被破坏情景"},{"parent":"99a36d602e49","children":[{"parent":"bb57d06a2d89","children":[],"id":"4925061129d7","title":"将删除缓存、修改数据库、读取缓存等的操作积压到队列里边,实现串行化
    "}],"id":"bb57d06a2d89","title":"并发下解决数据库与缓存不一致的思路"},{"parent":"99a36d602e49","children":[{"parent":"db6def4c48a6","children":[],"id":"8def4023ab41","title":"高并发下表现不如意,在原子性被破坏时表现优异"}],"id":"db6def4c48a6","title":"结论"}],"id":"99a36d602e49","title":"先删除缓存,再更新数据库
    "},{"parent":"cd7299545586","children":[{"parent":"f9b9c73e9e93","children":[{"parent":"f55f8ee1671a","children":[],"id":"626ca445c252","title":"先删除缓存
    "},{"parent":"f55f8ee1671a","children":[],"id":"315c5d4f0763","title":"再写数据库"},{"parent":"f55f8ee1671a","children":[{"parent":"23aabe812e01","children":[],"id":"fae527875f52","title":"延时时间要大于数据库一次写操作的时间
    "},{"parent":"23aabe812e01","children":[],"id":"774f81a5a8a0","title":"需要考虑Redis和数据库的主从同步时间
    "}],"id":"23aabe812e01","title":"休眠一段时间"},{"parent":"f55f8ee1671a","children":[],"id":"10db51291f20","title":"再次删除缓存"}],"id":"f55f8ee1671a","title":"先删除缓存,再更新数据库"}],"id":"f9b9c73e9e93","title":"延时双删
    "}],"id":"cd7299545586","title":"解决方案"}],"id":"796744a8869c","title":"双写一致性"},{"parent":"7742814a28a8","children":[{"parent":"13012e650d03","children":[{"parent":"66c624a52b7f","children":[{"parent":"3f21b7a753e0","children":[{"parent":"07452410b695","children":[],"id":"f1badcd09c76","title":"可重入锁"},{"parent":"07452410b695","children":[],"id":"e220542f12fa","title":"乐观锁
    "},{"parent":"07452410b695","children":[],"id":"bd7cd43665fd","title":"公平锁"},{"parent":"07452410b695","children":[],"id":"28b6c9db34d6","title":"读写锁"},{"parent":"07452410b695","children":[],"id":"0f0046ee2fb4","title":"Redlock"},{"parent":"07452410b695","children":[],"id":"e3e5074fe0ee","title":"BloomFilter (布隆过滤器)"}],"id":"07452410b695","title":"功能概括"}],"id":"3f21b7a753e0","title":"Redisson"},{"parent":"66c624a52b7f","children":[{"parent":"c34fec094423","children":[{"parent":"d2c674dedd38","children":[],"id":"2b3737289a9b","title":"setnx key 不存在,才会设置它的值,否则什么也不做
    "}],"id":"d2c674dedd38","title":"加锁"},{"parent":"c34fec094423","children":[{"parent":"5c1eb865ad35","children":[],"id":"8132cd2aac6c","title":"expire 设置过期时间
    "}],"id":"5c1eb865ad35","title":"避免死锁"},{"parent":"c34fec094423","children":[{"parent":"6c746237d083","children":[],"id":"4cd136dcf59a","title":"Redis 2.6.12扩展了 set 命令
    "}],"id":"6c746237d083","title":"保证setnx与expire的原子性
    "}],"id":"c34fec094423","title":"操作"},{"parent":"66c624a52b7f","children":[{"parent":"3b3a67a4f68f","children":[{"parent":"945ee50567d1","children":[{"parent":"dfad1fb29323","children":[{"parent":"750122fd9d6d","children":[],"id":"2fce14e8226c","title":"加锁时,先设置一个过期时间,然后我们开启一个守护线程,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行续期,重新设置过期时间"},{"parent":"750122fd9d6d","children":[],"id":"c000eca69ea1","title":"Redisson的看门狗"}],"id":"750122fd9d6d","title":"评估操作共享资源的时间不准确"}],"id":"dfad1fb29323","title":"客户端 1 操作共享资源耗时太久,导致锁被自动释放,之后被客户端 2 持有"}],"id":"945ee50567d1","title":"锁过期"},{"parent":"3b3a67a4f68f","children":[{"parent":"bf25f0444016","children":[{"parent":"bbb2d7133150","children":[{"parent":"ea7ea3f77745","children":[{"parent":"fc68dca74343","children":[],"id":"6310889577fc","title":"UUID"},{"parent":"fc68dca74343","children":[],"id":"7462c19e34f5","title":"自己的线程 ID"}],"id":"fc68dca74343","title":"添加唯一标识"},{"parent":"ea7ea3f77745","children":[{"parent":"bb6006e00c6e","children":[{"parent":"6b33de287b91","children":[],"id":"37d6c65470fd","title":"Redis 处理每一个请求是单线程执行的,在执行一个 Lua 脚本时,其它请求必须等待"}],"id":"6b33de287b91","title":"Lua 脚本"}],"id":"bb6006e00c6e","title":"原子性"},{"parent":"ea7ea3f77745","children":[{"parent":"bbadb586bfed","children":[],"id":"59c6c1ed4a41","title":"唯一标识加锁"},{"parent":"bbadb586bfed","children":[],"id":"80b475a0dbea","title":"操作共享资源"},{"parent":"bbadb586bfed","children":[],"id":"b2b7aee63272","title":"释放锁:Lua 脚本,先 GET 判断锁是否归属自己,再 DEL 释放锁"}],"id":"bbadb586bfed","title":"严谨的流程"}],"id":"ea7ea3f77745","title":"没有检查这把锁是否归自己持有"}],"id":"bbb2d7133150","title":"客户端 1 操作共享资源过程中GC或因其他原因超时释放,  导致释放了客户端 2 的锁
    "}],"id":"bf25f0444016","title":"释放别人的锁"},{"parent":"3b3a67a4f68f","children":[{"parent":"b05bbbfca458","children":[{"parent":"1e47c82d5213","children":[],"id":"d41070261ebd","title":"客户端 1 在主库上执行 SET 命令,加锁成功
    "},{"parent":"1e47c82d5213","children":[],"id":"ff99a0fb95f0","title":"此时,主库异常宕机,SET 命令还未同步到从库上(主从复制是异步的)"},{"parent":"1e47c82d5213","children":[],"id":"b585e9c1979d","title":"从库被哨兵提升为新主库,这个锁在新的主库上,丢失了"}],"id":"1e47c82d5213","title":"场景"},{"parent":"b05bbbfca458","children":[{"parent":"6d09d47be5d2","children":[{"parent":"4cab76cdd425","children":[{"parent":"2a920be28d4e","children":[],"id":"f784806d22dc","title":"不再需要部署从库和哨兵实例,只部署主库
    "},{"parent":"2a920be28d4e","children":[],"id":"5b380f8d976a","title":"但主库要部署多个,官方推荐至少 5 个实例"}],"id":"2a920be28d4e","title":"使用前提"},{"parent":"4cab76cdd425","children":[{"parent":"50ee74e743dd","children":[],"id":"964b265a6b2d","title":"客户端先获取「当前时间戳T1」"},{"parent":"50ee74e743dd","children":[],"id":"314294fbf295","title":"客户端依次向这 5 个 Redis 实例发起加锁请求(用前面讲到的 SET 命令),且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁"},{"parent":"50ee74e743dd","children":[],"id":"ca66b200ce03","title":"如果客户端从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败"},{"parent":"50ee74e743dd","children":[],"id":"6f679cab274f","title":"加锁成功,去操作共享资源"},{"parent":"50ee74e743dd","children":[],"id":"e71de8c8d3b3","title":"加锁失败,向「全部节点」发起释放锁请求( Lua 脚本)"}],"id":"50ee74e743dd","title":"使用流程"},{"parent":"4cab76cdd425","children":[{"parent":"c8b84a886178","children":[],"id":"0dca2da045c0","title":"网络延迟"},{"parent":"c8b84a886178","children":[],"id":"0794df0823fa","title":"进程暂停(GC)"},{"parent":"c8b84a886178","children":[],"id":"5485ba1922fd","title":"时钟漂移"}],"id":"c8b84a886178","title":"请考虑好分布式系统的NPC问题"}],"id":"4cab76cdd425","title":"Redlock"}],"id":"6d09d47be5d2","title":"解决方案"}],"id":"b05bbbfca458","title":"主从发生切换分布锁安全问题
    "}],"id":"3b3a67a4f68f","title":"问题"}],"id":"66c624a52b7f","title":"分布式锁"}],"id":"13012e650d03","title":"并发竞争"},{"parent":"7742814a28a8","children":[{"parent":"b36a30a83fbc","children":[],"id":"65abd363741b","title":"bigkey命令 找到干掉"},{"parent":"b36a30a83fbc","children":[],"id":"e9ff6a38dec0","title":"Redis 4.0引入了memory usage命令和lazyfree机制"}],"id":"b36a30a83fbc","title":"大Key"},{"parent":"7742814a28a8","children":[{"parent":"c7346bf92b6a","children":[],"id":"e74b5d1c63a1","title":"设置缓存时间不失效"},{"parent":"c7346bf92b6a","children":[],"id":"69508638ffc6","title":"多级缓存"},{"parent":"c7346bf92b6a","children":[],"id":"bdc6ecf6b73c","title":"布隆过滤器"},{"parent":"c7346bf92b6a","children":[],"id":"30abdb86e98d","title":"读写分离"}],"id":"c7346bf92b6a","title":"热点key"},{"parent":"7742814a28a8","children":[{"parent":"29f0b4fd4759","children":[{"parent":"7ce1965bb4b7","children":[{"parent":"5d0a0294f45d","children":[],"id":"3367ed8b7920","title":"没有offset、limit参数,会一次查出全部"},{"parent":"5d0a0294f45d","children":[],"id":"47780bc0614e","title":"keys算法是遍历算法,复杂度为O(n),因为Redis单线程的特性,会顺序执行指令,其他指令必须等待keys执行结束才可以执行"}],"id":"5d0a0294f45d","title":"缺点"}],"id":"7ce1965bb4b7","title":"keys"},{"parent":"29f0b4fd4759","children":[{"parent":"b7569f19ae71","children":[{"parent":"c9d9d6881a2c","children":[],"id":"b5bc3f2b03ab","title":"同样可以正则表达式匹配,limit可控条数,游标分布进行不会阻塞"}],"id":"c9d9d6881a2c","title":"优点"},{"parent":"b7569f19ae71","children":[{"parent":"477b5ab0ad6c","children":[],"id":"bab36d1d140e","title":"返回结果会重复"},{"parent":"477b5ab0ad6c","children":[],"id":"8f7198524c90","title":"如果遍历过程出现数据修改,不能确定改动后的数据能不能遍历到"}],"id":"477b5ab0ad6c","title":"缺点"}],"id":"b7569f19ae71","title":"scan"}],"id":"29f0b4fd4759","title":"搜索海量key"}],"id":"7742814a28a8","title":"常见问题"},{"parent":"68e2d53f78c0","children":[{"parent":"3fd6a16f4f94","children":[{"parent":"10c0fbbaf18c","children":[],"id":"3ff27aab32ff","title":"zset滑动窗口实现"},{"parent":"10c0fbbaf18c","children":[],"id":"c2637409763a","title":"在量大时会消耗很多存储空间"}],"id":"10c0fbbaf18c","title":"简单限流"},{"parent":"3fd6a16f4f94","children":[{"parent":"f5de183ac704","children":[{"parent":"d5400b7edf80","children":[],"id":"b4fb7a7e554e","title":"漏斗Funnel、漏斗算法实现makeSpace"},{"parent":"d5400b7edf80","children":[],"id":"76106014ff32","title":"在每次灌水前调用makeSpace,给漏斗腾出空间,腾出的空间取决于水流的速度
    "}],"id":"d5400b7edf80","title":"单体实现"},{"parent":"f5de183ac704","children":[{"parent":"d62e25e3e13c","children":[],"id":"b04c03869678","title":"将漏斗对象按字段存储到hash结构中,灌水时将hash结构字段去出进行逻辑运算后,再将新值重填到hash结构中,完成频度检测"},{"parent":"d62e25e3e13c","children":[{"parent":"6d2dccb3f68e","children":[],"id":"c6c702826f75","title":"加锁处理,如果失败重试会导致性能下降"},{"parent":"6d2dccb3f68e","children":[],"id":"a030aea687cd","title":"Redis4.0提供的Redis-Cell解决此问题"}],"id":"6d2dccb3f68e","title":"无法保证这三个操作的原子性"}],"id":"d62e25e3e13c","title":"分布式实现"}],"id":"f5de183ac704","title":"漏斗限流"}],"id":"3fd6a16f4f94","title":"限流操作"},{"parent":"68e2d53f78c0","children":[{"parent":"ada2330a2425","children":[{"parent":"668b1454d74f","children":[{"parent":"4d48d557b5e0","children":[],"id":"7fbadfbbcfcc","title":"5分钟一次"},{"parent":"4d48d557b5e0","children":[],"id":"d00795ceede8","title":"冷备"},{"parent":"4d48d557b5e0","children":[],"id":"f8401d16c024","title":"恢复的时候比较快"},{"parent":"4d48d557b5e0","children":[],"id":"3bbfd52a1687","title":"快照文件生成时间久,消耗cpu"},{"parent":"4d48d557b5e0","children":[],"id":"ccc6b9dcf9fb","title":"采用COW机制来实现持久化,fork进程处理"}],"id":"4d48d557b5e0","title":"RDB"},{"parent":"668b1454d74f","children":[{"parent":"9e12decdb68e","children":[],"id":"1d0805523949","title":"appendOnly"},{"parent":"9e12decdb68e","children":[],"id":"89f5517d6396","title":"数据齐全"},{"parent":"9e12decdb68e","children":[],"id":"7491a69d002c","title":"只对内存进行修改的指令记录,进行恢复时相当于‘重放’所有执行指令"},{"parent":"9e12decdb68e","children":[{"parent":"91d9756055c8","children":[],"id":"5d34419b1718","title":"定期做AOF瘦身"}],"id":"91d9756055c8","title":"回复慢文件大"}],"id":"9e12decdb68e","title":"AOF"},{"parent":"668b1454d74f","children":[{"parent":"0aa38f4c9668","children":[],"id":"7f7ef8d8748a","title":"同时将RDB与AOF存放在一起,AOF只存储自持久化开始到持久化结束期间的AOF日志"},{"parent":"0aa38f4c9668","children":[],"id":"a52194b87860","title":"在重启时先加载RDB的内容,然后重放AOF日志,提升效率"}],"id":"0aa38f4c9668","title":"混合持久化"}],"id":"668b1454d74f","title":"持久化"},{"parent":"ada2330a2425","children":[{"parent":"58b8b6e3fee6","children":[],"id":"b7cb33dbb795","title":"主从数据采取异步同步"},{"parent":"58b8b6e3fee6","children":[],"id":"f596df0cfe11","title":"保证最终一致性,从节点会努力追赶主节点,保证最终情况下从节点与主节点一致"},{"parent":"58b8b6e3fee6","children":[{"parent":"c9582a62c7b9","children":[{"parent":"09c563a5c389","children":[],"id":"b12a602a099d","title":"Redis将对自己状态产生修改影响的指令记录在本地内存buffer中,然后异步将buffer中的指令同步到从节点"},{"parent":"09c563a5c389","children":[],"id":"6985e3bc5bf5","title":"复制内存buffer是定长的环形数组,如果数组内容满了,就会从头开始覆盖"},{"parent":"09c563a5c389","children":[],"id":"e0065aa5e0d6","title":"在网络不好的情况下,可能导致无法通过指令流来同步"}],"id":"09c563a5c389","title":"增量同步"},{"parent":"c9582a62c7b9","children":[{"parent":"7e28cef0ac53","children":[],"id":"923021194d4e","title":"快照同步十分耗费资源"},{"parent":"7e28cef0ac53","children":[],"id":"b6a46fbc2eea","title":"首先在主节点进行bgsave,将当前内存快照到磁盘文件,然后再将快照传送到从节点"},{"parent":"7e28cef0ac53","children":[],"id":"19da982e6ab9","title":"快照文件接受完毕后,先将当前内存数据清空,然后执行全量加载,加载后通知主节点进行增量同步"},{"parent":"7e28cef0ac53","children":[],"id":"4954c29c2a50","title":"在此期间buffer依旧向前移动,如果快照同步时间过长或复制buffer太小,就会导致快照同步后依旧无法增量同步,导致死循环"},{"parent":"7e28cef0ac53","children":[],"id":"d4a0e45138a3","title":"所以务必要配置合适的复制buffer大小,避免快照复制死循环"}],"id":"7e28cef0ac53","title":"快照同步"}],"id":"c9582a62c7b9","title":"同步方式"}],"id":"58b8b6e3fee6","title":"数据同步机制"},{"parent":"ada2330a2425","children":[{"parent":"14d4ebf6fb81","children":[],"id":"6d287264815f","title":"哨兵负责监控主从节点健康,当主节点挂掉时,自动选择最优节点成为主节点,原主节点恢复后会变为从节点"},{"parent":"14d4ebf6fb81","children":[{"parent":"22cf1ee3be06","children":[],"id":"0ec54367a481","title":"Redis采用异步复制,所以当主节点挂掉,从节点可能没有收到全部的同步消息,产生消息丢失"},{"parent":"22cf1ee3be06","children":[{"parent":"fea18cac6d2a","children":[],"id":"bb55da53f849","title":"表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服务"}],"id":"fea18cac6d2a","title":"min-slaves-to-write 1"},{"parent":"22cf1ee3be06","children":[{"parent":"9d3ebc781908","children":[],"id":"85f361fff22e","title":"表示如果10s内没有收到从节点的反馈,就意味着同步不正常"}],"id":"9d3ebc781908","title":"min-slaves-max-lag 10"}],"id":"22cf1ee3be06","title":"消息丢失"}],"id":"14d4ebf6fb81","title":"哨兵"},{"parent":"ada2330a2425","children":[],"id":"6a204b455e04","title":"集群"}],"id":"ada2330a2425","title":"高可用"},{"parent":"68e2d53f78c0","children":[{"parent":"0bbbfddb1bdf","children":[],"id":"1c62574854ce","title":"合并读、写命令,减少网络开销"}],"id":"0bbbfddb1bdf","title":"管道"},{"parent":"68e2d53f78c0","children":[{"parent":"550cc736d1a3","children":[{"parent":"27b517127004","children":[],"id":"697a6405075e","title":"创建一个定时器,当key设置有过期时间,且到达过期时间,由定时器任务立即删除"},{"parent":"27b517127004","children":[],"id":"fbf01411fe0d","title":"可以快速释放掉不必要的内存占用 , 但是CPU压力很大
    "}],"id":"27b517127004","title":"定时删除"},{"parent":"550cc736d1a3","children":[{"parent":"5ad26b89a5fb","children":[],"id":"1954cef851e8","title":"在客户端访问key的时候,进行过期检查,如果过期了立即删除"}],"id":"5ad26b89a5fb","title":"惰性策略(惰性删除)"},{"parent":"550cc736d1a3","children":[{"parent":"1494d4d816a6","children":[],"id":"80ed3dbfa838","title":"Redis会将设置了过期时间的key放入一个独立的字典中,定期遍历(默认每秒进行10次扫描)来删除到底的key"},{"parent":"1494d4d816a6","children":[{"parent":"3c1730cc5539","children":[],"id":"418e5db9ebc8","title":"定期遍历不会遍历所有的key,而是采取贪心策略"},{"parent":"3c1730cc5539","children":[],"id":"c03a6231598c","title":"从过期字典中随机选出20个key"},{"parent":"3c1730cc5539","children":[],"id":"d50e70020d97","title":"删除这20个key中已经过期的key"},{"parent":"3c1730cc5539","children":[],"id":"2de36c1ef778","title":"如果过期的key的比例超过1/4,则重复步骤"},{"parent":"3c1730cc5539","children":[],"id":"c646a5a92ec1","title":"为保证不会循环过度,扫描时间上线默认不会超过25ms"}],"id":"3c1730cc5539","title":"贪心策略"}],"id":"1494d4d816a6","title":"定时扫描(定时删除)"},{"parent":"550cc736d1a3","children":[{"parent":"15e0714a86f6","children":[],"id":"0d3ad6052dae","title":"从节点不会进行过期扫描,对过期的处理时被动的,主节点在key到期时,会在AOF里增加一条del指令,同步到从节点,从节点通过指令删除key"}],"id":"15e0714a86f6","title":"从节点过期策略"}],"id":"550cc736d1a3","title":"过期策略"},{"parent":"68e2d53f78c0","children":[{"parent":"1c19ee20a68c","children":[{"parent":"35311e5d526f","children":[],"id":"6ff15aa765c3","title":"采用key/value+链表实现,当字典元素被访问时移动到表头,当空间满的时候踢掉链表尾部的元素"},{"parent":"35311e5d526f","children":[{"parent":"9b7510be7f91","children":[],"id":"dbcf0b911044","title":"Redis的LRU采用一种近似LRU的算法,为每个key增加一个额外字段长度为24bit,为最后一次访问的时间戳"},{"parent":"9b7510be7f91","children":[],"id":"3f37f69602f0","title":"采取懒惰方式处理,当执行写入操作时如果超出最大内存就执行一次LRU淘汰算法,随机采样5(数量可设置)个key,淘汰掉最旧的key,如果淘汰后依旧超出最大内存则继续淘汰"}],"id":"9b7510be7f91","title":"Redis的LRU"}],"id":"35311e5d526f","title":"LRU"}],"id":"1c19ee20a68c","title":"淘汰机制"},{"parent":"68e2d53f78c0","children":[{"parent":"ef4cb7bfd92a","children":[],"id":"77a7955156bf","title":"在调用套接字方法的时候默认是阻塞的,比如,read方法需要读取规定字节后返回,如果没有线程就会卡在那里,直到新数据来或者链接关闭
    "},{"parent":"ef4cb7bfd92a","children":[],"id":"f329207efe37","title":"write方法一般不会阻塞,除非内核为套接字分配的写缓冲区已经满了"},{"parent":"ef4cb7bfd92a","children":[],"id":"9bf3cb96e12d","title":"非阻塞的IO提供了一个选项Non_Blocking,打开时读写都不会阻塞 读多少写多少取决于内核的套接字字节分配"},{"parent":"ef4cb7bfd92a","children":[],"id":"9235ef78e11c","title":"非阻塞IO也有问题线程要读数据读了一点就返回了线程什么时候知道继续读?写一样"},{"parent":"ef4cb7bfd92a","children":[],"id":"eb7e9d548e40","title":"一般都是select解决但是性能低现在都是epoll"}],"id":"ef4cb7bfd92a","title":"多路IO复用"}],"collapsed":true,"id":"68e2d53f78c0","title":"Redis"},{"parent":"root","lineStyle":{"randomLineColor":"#13A3ED"},"children":[{"parent":"27d73b325f5b","children":[{"parent":"c126505c846f","children":[{"parent":"3e6fdbd260dc","children":[{"parent":"e3bbe630c77b","children":[],"id":"a52aafb21566","title":"所有节点拥有数据的最新版本"}],"id":"e3bbe630c77b","title":"数据一致性(consistency)"},{"parent":"3e6fdbd260dc","children":[{"parent":"16f0be57497c","children":[],"id":"13e807d7a20d","title":"数据具备高可用性"}],"id":"16f0be57497c","title":"可用性(availability)"},{"parent":"3e6fdbd260dc","children":[{"parent":"7394b794bc3c","children":[],"id":"6f3f4c1e3e1f","title":"容忍网络出现分区,分区之间网络不可达"}],"id":"7394b794bc3c","title":"分区容错性(partition-tolerance)"}],"id":"3e6fdbd260dc","title":"概念"},{"parent":"c126505c846f","children":[{"parent":"ea8b927aa992","children":[],"id":"ea6785f04b60","title":"在容忍网络分区的条件下,“强一致性”和“极致可用性”无法同时达到"}],"id":"ea8b927aa992","title":"定义"}],"id":"c126505c846f","title":"CAP理论"},{"parent":"27d73b325f5b","children":[{"parent":"d63e14aab5d1","children":[{"parent":"94404e1c447b","children":[],"id":"dd9e2dc4af2a","title":"用来维护各个服务的注册信息 , 各个服务通过注册清单的服务名获取服务具体位置(IP地址会变,服务名一般不会变)
    "}],"id":"94404e1c447b","title":"服务治理:Eureka"},{"parent":"d63e14aab5d1","children":[{"parent":"885fa0bda765","children":[],"id":"f1bd36f604b7","title":"客户端可以从Eureka Server中得到一份服务清单,在发送请求时通过负载均衡算法,在多个服务器之间选择一个进行访问"}],"id":"885fa0bda765","title":"客户端负载均衡:Ribbon"},{"parent":"d63e14aab5d1","children":[{"parent":"84ec94898586","children":[],"id":"320c4c3f6ad0","title":"在远程服务出现延迟, 宕机情况时提供断路器、线程隔离等功能"}],"id":"84ec94898586","title":"服务容错保护:Hystrix"},{"parent":"d63e14aab5d1","children":[{"parent":"3f725be6c509","children":[],"id":"5d6df34cab52","title":"整合了Ribbon与Hystrix"}],"id":"3f725be6c509","title":"声明式服务调用:Feign"},{"parent":"d63e14aab5d1","children":[{"parent":"b19ae03b802a","children":[],"id":"1cee4018ba68","title":"解决路由规则与服务实例的维护间题, 签名校验、 登录校验冗余问题
    "}],"id":"b19ae03b802a","title":"API网关服务:Zuul"},{"parent":"d63e14aab5d1","children":[{"parent":"261553fe2960","children":[{"parent":"510230e16d5b","children":[],"id":"605eb3a4eb2a","title":"通过接口获取数据、并依据此数据初始化自己的应用"}],"id":"510230e16d5b","title":"Client
    "},{"parent":"261553fe2960","children":[{"parent":"f97a17502384","children":[],"id":"f0fc3067f1d7","title":"提供配置文件的存储、以接口的形式将配置文件的内容提供出去"}],"id":"f97a17502384","title":"Server"}],"id":"261553fe2960","title":"分布式配置中心:Config"}],"id":"d63e14aab5d1","title":"基础功能"}],"collapsed":true,"id":"27d73b325f5b","title":"Spring Cloud"},{"parent":"root","lineStyle":{"randomLineColor":"#A04AFB"},"children":[],"id":"bc354f9b6903","title":"Zookeeper"},{"parent":"root","lineStyle":{"randomLineColor":"#74C11F"},"children":[],"id":"96167bd3313f","title":"Dubbo"},{"parent":"root","lineStyle":{"randomLineColor":"#F4325C"},"children":[{"parent":"2967bbb0ce6c","children":[{"parent":"12f64511f568","children":[{"parent":"75e771d94bbf","children":[{"parent":"53caf8da86e2","children":[],"id":"696233005cce","title":"NameServer是几乎无状态的, 可以横向扩展, 节点之间相互无通信, 可以通过部署多态机器来标记自己是一个伪集群"},{"parent":"53caf8da86e2","children":[],"id":"20cf4d49dd61","title":"NameServer的压力不会太大, 主要开销在维持心跳和提供Topic-Broker的关系数据"},{"parent":"53caf8da86e2","children":[],"id":"c96708a3cb62","title":"Broker向NameServer发送心跳时, 会携带上当前自己所负责的所有Topic信息(万级别), 若Topic个数很多会导致一次心跳中,就Topic的数据就几十M, 网络情况差的情况下, 网络传输失败, 心跳失败, 导致NameServer误认为Broker心跳失败"}],"id":"53caf8da86e2","title":"主要负责对于源数据的管理,包括了对于Topic和路由信息的管理"}],"id":"75e771d94bbf","title":"NameServer"},{"parent":"12f64511f568","children":[{"parent":"daad2e0ee47a","children":[{"parent":"a3a4b2fa4290","children":[],"id":"a876bb50d0bb","title":"Broker是具体提供业务的服务器, 单个Broker节点与所有NameServer节点保持长连接及心跳, 并会定时将Topic信息注册到NameServer"},{"parent":"a3a4b2fa4290","children":[],"id":"888065bfed04","title":"底层通信和连接基于Netty实现"},{"parent":"a3a4b2fa4290","children":[],"id":"6ff21d8f55f8","title":"Broker负责消息存储, 以Topic为维度支持轻量级队列, 单机可以支撑上万队列规模, 支持消息推拉模型"}],"id":"a3a4b2fa4290","title":"消息中转角色,负责存储消息,转发消息"}],"id":"daad2e0ee47a","title":"Broker"},{"parent":"12f64511f568","children":[{"parent":"5c20008f41b6","children":[{"parent":"cfa99e651913","children":[],"id":"ecf9bb3145fc","title":"同步发送: 发送者向MQ执行发送消息API,同步等待,直到消息服务器返回发送结果。一般用于重要通知消息,例如重要通知邮件、营销短信
    "},{"parent":"cfa99e651913","children":[],"id":"8f1cefc4cec3","title":"异步发送: 发送这向MQ执行发送消息API,指定消息发送成功后的回调函数,然后调用消息发送API后,立即返回.消息发送者线程不阻塞,直到运行结束,消息发送成功或失败的回调任务在一个新线程执行,一般用于可能链路耗时较长而对响应时间敏感的业务场景,例如用户视频上传后通知启动转码服务"},{"parent":"cfa99e651913","children":[],"id":"93201d04d89e","title":"单向发送: 发送者MQ执行发送消息API时,直接返回,不等待消息服务器的结果,也不注册回调函数,简单的说就是只管发送,不在乎消息是否成功存储在消息服务器上,适用于某些耗时非常短但对可靠性要求并不高的场景,例如日志收集"}],"id":"cfa99e651913","title":"消息生产者,负责产生消息,一般由业务系统负责产生消息"}],"id":"5c20008f41b6","title":"Producer"},{"parent":"12f64511f568","children":[{"parent":"d7f396e0831a","children":[{"parent":"2167651c5963","children":[],"id":"eac97c7046ef","title":"Pull: 拉取型消费,主动从消息服务器拉去信息, 只要批量拉取到消息, 用户应用就会启动消费过程, 所以Pull成为主动消费型"},{"parent":"2167651c5963","children":[],"id":"e1d302c81ee1","title":"Push: 推送型消费者,封装了消息的拉去,消费进度和其他的内部维护工作, 将消息到达时执行回调接口留给用户程序来实现. 所以Push成为被动消费型, 但从实现上看还是从消息服务器中拉取消息, 不同于Pull的时Push首先要注册消费监听器, 当监听器触发后才开始消费消息"}],"id":"2167651c5963","title":"消息消费者,负责消费消息,一般是后台系统负责异步消费"}],"id":"d7f396e0831a","title":"Consumer"}],"id":"12f64511f568","title":"基础组成"},{"parent":"2967bbb0ce6c","children":[{"parent":"604a2b077915","children":[{"parent":"5f5dd32d8097","children":[{"parent":"ee7415aca487","children":[{"parent":"e8d48c3eb98c","children":[],"id":"c36cc866bf9a","title":"配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢失(异步刷盘丢失少量消息,同步刷盘一条不丢)。性能最高"}],"id":"e8d48c3eb98c","title":"优点"},{"parent":"ee7415aca487","children":[{"parent":"043f2943a836","children":[],"id":"df0bc145fab6","title":"单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响"}],"id":"043f2943a836","title":"缺点"}],"id":"ee7415aca487","title":"一个集群无Slave,全是Master"}],"id":"5f5dd32d8097","title":"多Master"},{"parent":"604a2b077915","children":[{"parent":"1257c8b9aac5","children":[{"parent":"ea64577686e3","children":[{"parent":"90097b5c030f","children":[],"id":"e53091b80fab","title":"即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,因为Master 宕机后,消费者仍然可以从Slave消费,此过程对应用透明。不需要人工干预。性能同多 Master 模式几乎一样"}],"id":"90097b5c030f","title":"优点"},{"parent":"ea64577686e3","children":[{"parent":"4ef440c8e995","children":[],"id":"27e509d14ece","title":"Master宕机,磁盘损坏情况,会丢失少量消息"}],"id":"4ef440c8e995","title":"缺点"}],"id":"ea64577686e3","title":"每个Master配置一个Slave,有多对Master-Slave,HA采用异步复制方式,主备有短暂消息延迟,毫秒级"}],"id":"1257c8b9aac5","title":"多Master, 多Salve, 异步复制"},{"parent":"604a2b077915","children":[{"parent":"4d53b0661b8f","children":[{"parent":"e4a1b95dba5b","children":[{"parent":"9f79418a5eed","children":[],"id":"a5fc7ac37b70","title":"数据与服务都无单点,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高"}],"id":"9f79418a5eed","title":"优点"},{"parent":"e4a1b95dba5b","children":[{"parent":"6dd53df1911c","children":[],"id":"57d816d31bf1","title":"性能比异步复制模式略低,大约低10%左右,发送单个消息的RT会略高。目前主宕机后,备机不能自动切换为主机,后续会支持自动切换功能"}],"id":"6dd53df1911c","title":"缺点"}],"id":"e4a1b95dba5b","title":"每个Master配置一个Slave,有多对Master-Slave,HA采用同步双写方式,主备都写成功,向应用才返回成功"}],"id":"4d53b0661b8f","title":"多Master, 多Salve, 双写一致"}],"id":"604a2b077915","title":"支持集群模式"},{"parent":"2967bbb0ce6c","children":[{"parent":"43da161a94fa","children":[],"id":"1a296e9626d0","title":"只有发送成功后返回CONSUME_SUCCESS, 消费才是完成的"},{"parent":"43da161a94fa","children":[{"parent":"8fc82e05be66","children":[{"parent":"d99a334f4878","children":[],"id":"40fed29109ad","title":"当确认批次消息消费失败 (RECONSUME_LATER) 时, RocketMQ会把这批消息重发回Broker (此处非原Topic 而是这个消费者组的 RETRY Topic)
    "},{"parent":"d99a334f4878","children":[],"id":"ec26b776e2f4","title":"在延迟的某个时间点(默认是10秒,业务可设置)后,再次投递到这个ConsumerGroup"},{"parent":"d99a334f4878","children":[],"id":"c226bf1b7a9e","title":"如果一直这样重复消费都持续失败到一定次数(默认16次),就会投递到死信队列。人工干预解决"}],"id":"d99a334f4878","title":"顺序消息重试"},{"parent":"8fc82e05be66","children":[],"id":"813946fc3144","title":"无序消息重试"}],"id":"8fc82e05be66","title":"消息重试"},{"parent":"43da161a94fa","children":[{"parent":"a38425631022","children":[],"id":"0964ff7a4b1c","title":"RocketMQ 以 Consumer Group (消费者组)+ Queue (队列) 为单位管理消费进度, 通过 Consumer Offset 标记这个这个消费组在这条Queue上的消费进度"},{"parent":"a38425631022","children":[],"id":"76e7f5c236a6","title":"如果已存在的消费组出现了新消费实例的时候,依靠这个组的消费进度,可以判断第一次是从哪里开始拉取的"},{"parent":"a38425631022","children":[],"id":"65266364b4b8","title":"每次消息成功后,本地的消费进度会被更新,然后由定时器定时同步到broker,以此持久化消费进度"},{"parent":"a38425631022","children":[],"id":"ddcbe167cfd1","title":"但每次记录消费进度时,只会将一批消息中最小的offset值更新为消费进度值"}],"id":"a38425631022","title":"ACK机制"},{"parent":"43da161a94fa","children":[{"parent":"ce692c70d0c6","children":[],"id":"8c1e99f1aebf","title":"由于消费进度只记录了一个下标,就可能出现拉取了100条消息如 100 - 200 的消息,后面99条都消费结束了,只有101消费一直没有结束的情况"},{"parent":"ce692c70d0c6","children":[],"id":"88f2e23d3c86","title":"RocketMQ为了保证消息肯定被消费成功,消费进度只能维持在101,直到101也消费结束,本地消费进度才能标记200消费结束"},{"parent":"ce692c70d0c6","children":[],"id":"df1747541718","title":"在这种情况下,如果RocketMQ机器断电,或者被kill, 此处的消费进度就还是101, 当队列重新分配实例时, 从broker获取的消费进度维持在101, 就会出现重复消费的情况
    "},{"parent":"ce692c70d0c6","children":[],"id":"3528a1c8bcec","title":"对于这个场景,RocketMQ暂时无能为力,所以业务必须要保证消息消费的幂等性"}],"id":"ce692c70d0c6","title":"重复消费"},{"parent":"43da161a94fa","children":[{"parent":"b6fc9dbb9580","children":[],"id":"939f53dff9c0","title":"RocketMQ支持按照时间回溯消费,时间维度精确到毫秒,可以向前回溯,也可以向后回溯"}],"id":"b6fc9dbb9580","title":"消息回溯"}],"id":"43da161a94fa","title":"消费保证"},{"parent":"2967bbb0ce6c","children":[{"parent":"403b9cd18917","children":[{"parent":"4439dd28bd16","children":[{"parent":"40d9bda89e49","children":[],"id":"07d61c838390","title":"由于 NameServer 节点是无状态的,且各个节点直接的数据是一致的,故存在多个 NameServer 节点的情况下,部分 NameServer 不可用也可以保证 MQ 服务正常运行"}],"id":"40d9bda89e49","title":"NameServer 集群"},{"parent":"4439dd28bd16","children":[{"parent":"5e1a97f2162e","children":[{"parent":"2abd6a7fd911","children":[],"id":"f4b8c1adb5df","title":"由于Slave只负责读,当 Master 不可用,它对应的 Slave 仍能保证消息被正常消费"},{"parent":"2abd6a7fd911","children":[],"id":"8139525d3db2","title":"由于配置多组 Master-Slave 组,其他的 Master-Slave 组也会保证消息的正常发送和消费"}],"id":"2abd6a7fd911","title":"一个 Master 可以配置多个 Slave,同时也支持配置多个 Master-Slave 组"}],"id":"5e1a97f2162e","title":"Broker 主从, 多主从"},{"parent":"4439dd28bd16","children":[{"parent":"f82965d473fb","children":[],"id":"7e71895d91ef","title":"Consumer 的高可用是依赖于 Master-Slave 配置的,由于 Master 能够支持读写消息,Slave 支持读消息,当 Master 不可用或繁忙时, Consumer 会被自动切换到从 Slave 读取(自动切换,无需配置)"}],"id":"f82965d473fb","title":"Consumer 自动切换"},{"parent":"4439dd28bd16","children":[{"parent":"f3c6854c53d6","children":[],"id":"ad3cb7405f89","title":"在创建Topic时, 将Topic的多个 Message Queue 创建在多个 Broker组 上, 这样当一个Broker组的Master不可用后,其他组的Master仍然可用,Producer仍然可以发送消息
    "}],"id":"f3c6854c53d6","title":"Producer 连接多个 Broker"}],"id":"4439dd28bd16","title":"集群"},{"parent":"403b9cd18917","children":[{"parent":"a4cb838b16ea","children":[{"parent":"c2592c074d4c","children":[],"id":"07ef479a1045","title":"在返回写成功状态时,消息已经被写入磁盘中。即消息被写入内存的PAGECACHE 中后,立刻通知刷新线程刷盘,等待刷盘完成,才会唤醒等待的线程并返回成功状态, 超时会返回错误
    "}],"id":"c2592c074d4c","title":"同步刷盘"},{"parent":"a4cb838b16ea","children":[{"parent":"3d7e8bfce714","children":[],"id":"4fe92f30b6e8","title":"在返回写成功状态时,消息可能只是被写入内存的 PAGECACHE 中。当内存的消息量积累到一定程度时,触发写操作快速写入, 不返回错误"}],"id":"3d7e8bfce714","title":"异步刷盘"}],"id":"a4cb838b16ea","title":"刷盘机制"},{"parent":"403b9cd18917","children":[{"parent":"55e8e026c976","children":[{"parent":"7e5670f14a09","children":[],"id":"8859c3e13bfe","title":"Master 和 Slave 均写成功后才反馈给客户端写成功状态"}],"id":"7e5670f14a09","title":"同步复制"},{"parent":"55e8e026c976","children":[{"parent":"953b201d922e","children":[],"id":"c97878eed758","title":"只要 Master 写成功,就反馈客户端写成功状态"}],"id":"953b201d922e","title":"异步复制"}],"id":"55e8e026c976","title":"消息的主从复制"}],"id":"403b9cd18917","title":"高可用"},{"parent":"2967bbb0ce6c","children":[{"parent":"e56a055b5068","children":[{"parent":"16961909ae98","children":[],"id":"944ff5256f15","title":"RocketMQ提供了MessageQueueSelector队列选择机制"},{"parent":"16961909ae98","children":[],"id":"68d213356c50","title":"顺序发送 顺序消费由 消费者保证"}],"id":"16961909ae98","title":"Hash取模法"}],"id":"e56a055b5068","title":"顺序消费"},{"parent":"2967bbb0ce6c","children":[{"parent":"7ce52753f364","children":[{"parent":"4fb6ee988efc","children":[],"id":"8c2657879e36","title":"使用业务端逻辑保持幂等性"}],"id":"4fb6ee988efc","title":"原则"},{"parent":"7ce52753f364","children":[{"parent":"1bfaf765406c","children":[],"id":"b0239da60ed7","title":"对于同一操作发起的一次请求或者多次请求的结果是一致的"}],"id":"1bfaf765406c","title":"幂等性"},{"parent":"7ce52753f364","children":[{"parent":"56d747f52657","children":[],"id":"e39d27d3a0ab","title":"保证每条消息都有唯一编号(比如唯一流水号),重复消费时主键冲突不再处理消息"}],"id":"56d747f52657","title":"去重策略"}],"id":"7ce52753f364","title":"消息去重"},{"parent":"2967bbb0ce6c","children":[{"parent":"09e95e6eab93","children":[{"parent":"c8a5405fb7c5","children":[],"id":"3570c5920213","title":"暂不能被Consumer消费的消息, 需要 Producer 对消息的二次确认后,Consumer才能去消费它"}],"id":"c8a5405fb7c5","title":"Half Message (半消息)"},{"parent":"09e95e6eab93","children":[{"parent":"332f741948f5","children":[],"id":"be923878148d","title":"A服务先发送个 Half Message 给 Broker 端,消息中携带 B服务"},{"parent":"332f741948f5","children":[],"id":"55ea8b7092e3","title":"当A服务知道 Half Message 发送成功后, 执行本地事务"},{"parent":"332f741948f5","children":[],"id":"4187239edfc5","title":"如果本地事务成功,那么 Producer 像 Broker 服务器发送 Commit , 这样B服务就可以消费该 Message"},{"parent":"332f741948f5","children":[],"id":"08a1fccc842a","title":"如果本地事务失败,那么 Producer 像 Broker 服务器发送 Rollback , 那么就会直接删除上面这条半消息"},{"parent":"332f741948f5","children":[],"id":"4da11302aa7f","title":"如果因为网络等原因迟迟没有返回失败还是成功,那么会执行RocketMQ的回调接口,来进行事务的回查"}],"id":"332f741948f5","title":"流程"},{"parent":"09e95e6eab93","children":[],"id":"a5095a559452","title":"最终一致性"},{"parent":"09e95e6eab93","children":[],"id":"5c502c270ff1","title":"最大努力通知"}],"id":"09e95e6eab93","title":"事务消息"},{"parent":"2967bbb0ce6c","children":[{"parent":"539927f8e2c1","children":[],"id":"88eabed09a58","title":"Producer 和 NameServer 节点建立一个长连接"},{"parent":"539927f8e2c1","children":[],"id":"fc3d758dc575","title":"定期从 NameServer 获取 Topic 信息 
    "},{"parent":"539927f8e2c1","children":[],"id":"e2e50215339f","title":"并且向 Broker Master 建立链接 发送心跳"},{"parent":"539927f8e2c1","children":[],"id":"2063f1f65200","title":"发送消息给 Broker Master"},{"parent":"539927f8e2c1","children":[],"id":"0d92c5a8e5e8","title":"Consumer 从 Mater 和 Slave 一起订阅消息"}],"id":"539927f8e2c1","title":"一次完整的通信流程"},{"parent":"2967bbb0ce6c","children":[{"parent":"9081c42a0f2a","children":[],"id":"1219a6e079fa","title":"不再被正常消费 "},{"parent":"9081c42a0f2a","children":[],"id":"5e70a2a44651","title":"保存3天"},{"parent":"9081c42a0f2a","children":[],"id":"50aa84db0020","title":"面向消费者组 "},{"parent":"9081c42a0f2a","children":[],"id":"d4a503883c07","title":"控制台 重发 重写消费者 单独消费"}],"id":"9081c42a0f2a","title":"死信队列"},{"parent":"2967bbb0ce6c","children":[{"parent":"cc10915c102e","children":[{"parent":"712c8d74cc5a","children":[],"id":"e8d44e34f882","title":"生产者将消息发送给Rocket MQ的时候,如果出现了网络抖动或者通信异常等问题,消息就有可能会丢失"},{"parent":"712c8d74cc5a","children":[{"parent":"e4ba8bf00b71","children":[],"id":"4d4c85447a76","title":"如果消息还没有完成异步刷盘,RocketMQ中的Broker宕机的话,就会导致消息丢失"},{"parent":"e4ba8bf00b71","children":[],"id":"a16ee1c4706e","title":"如果消息已经被刷入了磁盘中,但是数据没有做任何备份,一旦磁盘损坏,那么消息也会丢失"}],"id":"e4ba8bf00b71","title":"消息需要持久化到磁盘中,这时会有两种情况导致消息丢失"},{"parent":"712c8d74cc5a","children":[],"id":"f521198db453","title":"消费者成功从RocketMQ中获取到了消息,还没有将消息完全消费完的时候,就通知RocketMQ我已经将消息消费了,然后消费者宕机,但是RocketMQ认为消费者已经成功消费了数据,所以数据依旧丢失了"}],"id":"712c8d74cc5a","title":"常见场景"},{"parent":"cc10915c102e","children":[{"parent":"6e0281cde36d","children":[],"id":"d1b9b4c61dab","title":"事务消息"},{"parent":"6e0281cde36d","children":[],"id":"d5c1c1f6fd31","title":"同步刷盘"},{"parent":"6e0281cde36d","children":[],"id":"dd9b94bf2957","title":"主从机构的话,需要Leader将数据同步给Followe"},{"parent":"6e0281cde36d","children":[],"id":"29c1212c50c6","title":"消费时无法异步消费,只能等待消费完成再通知RocketMQ消费完成"}],"id":"6e0281cde36d","title":"确保消息零丢失"},{"parent":"cc10915c102e","children":[],"id":"f6dabcb64d3a","title":"上述方案会使性能和吞吐量大幅下降, 需按场景谨慎使用"}],"id":"cc10915c102e","title":"消息丢失"},{"parent":"2967bbb0ce6c","children":[{"parent":"ce3c57e6ab49","children":[{"parent":"12b15ec01705","children":[{"parent":"c74aa299e8ad","children":[],"id":"24987ee3c4db","title":"在业务允许的情况下, 根据一定的丢弃策略来丢弃消息"},{"parent":"c74aa299e8ad","children":[],"id":"036abd6de052","title":"修复Consumer不消费问题,使其恢复正常消费,根据业务需要看是否要暂停"},{"parent":"c74aa299e8ad","children":[],"id":"81368743808e","title":"停止消费 加机器 加Topic, 编写临时处理分发程序消费
    "}],"id":"c74aa299e8ad","title":"丢弃, 扩容"}],"id":"12b15ec01705","title":"解决思想"}],"id":"ce3c57e6ab49","title":"消息堆积"},{"parent":"2967bbb0ce6c","children":[{"parent":"8a5fb1ce1bf4","children":[],"id":"05af140b142a","title":"RocketMQ支持定时消息,但是不支持任意时间精度,支持特定的level,例如定时5s,10s,1m等"}],"id":"8a5fb1ce1bf4","title":"定时消息"},{"parent":"2967bbb0ce6c","children":[{"parent":"4aa9ff9ad75c","children":[{"parent":"954e7ff26b10","children":[],"id":"e4f3f3d220ec","title":"单机吞吐量:十万级"},{"parent":"954e7ff26b10","children":[],"id":"c4af3edfc858","title":"可用性:非常高,分布式架构"},{"parent":"954e7ff26b10","children":[],"id":"6a45885fa4c5","title":"消息可靠性:经过参数优化配置,消息可以做到零丢失"},{"parent":"954e7ff26b10","children":[],"id":"77955f421cf0","title":"功能支持:MQ功能较为完善,还是分布式的,扩展性好"},{"parent":"954e7ff26b10","children":[],"id":"2159848b2077","title":"支持10亿级别的消息堆积,不会因为堆积导致性能下降"},{"parent":"954e7ff26b10","children":[],"id":"8d732d9ac0d4","title":"源码是java,我们可以自己阅读源码,定制自己公司的MQ,可以掌控"},{"parent":"954e7ff26b10","children":[],"id":"079ef0e7e12f","title":"天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况"},{"parent":"954e7ff26b10","children":[],"id":"24933c880163","title":"RoketMQ在稳定性上可能更值得信赖,这些业务场景在阿里双11已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择RocketMQ"}],"id":"954e7ff26b10","title":"优点"},{"parent":"4aa9ff9ad75c","children":[{"parent":"6e817ad0d7e2","children":[],"id":"5bc9518a9f6f","title":"支持的客户端语言不多,目前是java及c++,其中c++不成熟"},{"parent":"6e817ad0d7e2","children":[],"id":"bee9d1ce399f","title":"社区活跃度不是特别活跃那种"},{"parent":"6e817ad0d7e2","children":[],"id":"9e4046411e3c","title":"没有在 mq 核心中去实现JMS等接口,有些系统要迁移需要修改大量代码"}],"id":"6e817ad0d7e2","title":"缺点"}],"id":"4aa9ff9ad75c","title":"优缺点总结"}],"collapsed":true,"id":"2967bbb0ce6c","title":"RocketMQ"},{"parent":"root","lineStyle":{"randomLineColor":"#7754F6"},"children":[],"id":"6b402feebcdf","title":"分布式锁"},{"parent":"root","lineStyle":{"randomLineColor":"#FFCA01"},"children":[{"parent":"426e047d8f9d","children":[{"parent":"4b90dd9864c9","children":[{"parent":"057add18a139","children":[{"parent":"3097acf2ab15","children":[],"id":"df6d551163bc","title":"假如在第一阶段所有参与者都返回准备成功,那么协调者则向所有参与者发送提交事务命令,等待所有事务都提交成功之后,返回事务执行成功"},{"parent":"3097acf2ab15","children":[],"id":"d16589167a2c","title":"假如在第一阶段有一个参与者返回失败,那么协调者就会向所有参与者发送回滚事务的请求,即分布式事务执行失败"}],"id":"3097acf2ab15","title":"准备阶段: 协调者(事务管理器)给每个参与者发送Prepare消息,参与者要么直接返回失败,
    要么在本地执行事务,写本地的redo和undo日志,但不做提交"},{"parent":"057add18a139","children":[{"parent":"8c5177c0c885","children":[],"id":"ab3d2da5a1c6","title":"如果第二阶段提交失败, 执行的是回滚事务操作, 那么会不断重试, 直到所有参与者全部回滚, 不然在第一阶段准备成功的参与者会一直阻塞"},{"parent":"8c5177c0c885","children":[],"id":"acc12723768c","title":"如果第二阶段提交失败, 执行的是提交事务, 也会不断重试, 因为有可能一些参与者已经提交成功, 所以只能不断重试, 甚至人工介入处理
    "}],"id":"8c5177c0c885","title":"提交阶段: 协调者收到参与者的失败消息或者超时,直接给每个参与者发送回滚消息;
    否则,发送提交消息. 参与者根据协调者的指令执行提交或回滚操作,释放锁资源"}],"id":"057add18a139","title":"流程"},{"parent":"4b90dd9864c9","children":[],"id":"fb30798578aa","title":"同步阻塞协议"},{"parent":"4b90dd9864c9","children":[{"parent":"7a977c8d1831","children":[],"id":"1cce93f1a7a6","title":"同步阻塞导致长久资源锁定, 效率低"},{"parent":"7a977c8d1831","children":[],"id":"ffb01d2f2712","title":"协调者是一个单点, 存在单点故障问题, 参与者将一直处于锁定状态"},{"parent":"7a977c8d1831","children":[],"id":"143f109fefcf","title":"脑裂问题, 在提交阶段,如果只有部分参与者接收并执行了提交请求,会导致节点数据不一致
    "}],"id":"7a977c8d1831","title":"缺点"},{"parent":"4b90dd9864c9","children":[],"id":"75a98189b051","title":"是数据库层面解决方案"}],"id":"4b90dd9864c9","title":"2PC(两段式提交)"},{"parent":"426e047d8f9d","children":[{"parent":"e8856f8bce0a","children":[{"parent":"1eee8b12a46d","children":[],"id":"623a1ea6da3e","title":"CanCommit阶段: 协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应
    "},{"parent":"1eee8b12a46d","children":[{"parent":"1a7358388525","children":[],"id":"21e1c7f27605","title":"假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行"},{"parent":"1a7358388525","children":[],"id":"3e2483073c0d","title":"假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断"}],"id":"1a7358388525","title":"PreCommit阶段: 协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作"},{"parent":"1eee8b12a46d","children":[{"parent":"24711faa09db","children":[{"parent":"28223e1f26b4","children":[],"id":"7ccba5096def","title":"发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求"},{"parent":"28223e1f26b4","children":[],"id":"2dd11bddf0c0","title":"事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源"},{"parent":"28223e1f26b4","children":[],"id":"b7a3a303e9f7","title":"响应反馈 事务提交完之后,向协调者发送Ack响应"},{"parent":"28223e1f26b4","children":[],"id":"211c7b4aa22e","title":"完成事务 协调者接收到所有参与者的ack响应之后,完成事务"}],"id":"28223e1f26b4","title":"执行提交"},{"parent":"24711faa09db","children":[{"parent":"8abf9b8b22a0","children":[],"id":"f7664385e997","title":"发送中断请求 协调者向所有参与者发送abort请求"},{"parent":"8abf9b8b22a0","children":[],"id":"8f46d4cf1026","title":"事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源"},{"parent":"8abf9b8b22a0","children":[],"id":"18777ca3eaf1","title":"反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息"},{"parent":"8abf9b8b22a0","children":[],"id":"d27ccae9a9b6","title":"中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断"}],"id":"8abf9b8b22a0","title":"中断事务: 协调者没有接收到参与者发送的ACK响应, 或响应超时
    "}],"id":"24711faa09db","title":"doCommit阶段: 该阶段进行真正的事务提交"}],"id":"1eee8b12a46d","title":"流程"},{"parent":"e8856f8bce0a","children":[{"parent":"bef994d87c61","children":[],"id":"cce362d1b257","title":"降低了阻塞范围,在等待超时后协调者或参与者会中断事务"},{"parent":"bef994d87c61","children":[],"id":"c30827441ec6","title":"避免了协调者单点问题,doCommit阶段中协调者出现问题时,参与者会继续提交事务"}],"id":"bef994d87c61","title":"优点"},{"parent":"e8856f8bce0a","children":[{"parent":"0c7767c1b90d","children":[],"id":"5942b8825222","title":"脑裂问题依然存在,即在参与者收到PreCommit请求后等待最终指令,如果此时协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致"}],"id":"0c7767c1b90d","title":"缺点"},{"parent":"e8856f8bce0a","children":[],"id":"5bb33349d790","title":"是数据库层面解决方案"}],"id":"e8856f8bce0a","title":"3PC(三段式提交)"},{"parent":"426e047d8f9d","children":[{"parent":"b0b8679322ca","children":[{"parent":"006e94b9f913","children":[],"id":"59b9eb0f94d6","title":"Try阶段: 完成所有业务检查, 预留必须业务资源"},{"parent":"006e94b9f913","children":[],"id":"c5313a0fb055","title":"Confirm阶段: 真正执行业务, 不作任何业务检查, 只使用Try阶段预留的业务资源, Confirm操作必须保证幂等性
    "},{"parent":"006e94b9f913","children":[],"id":"471ecf6d09dc","title":"Cancel阶段: 释放Try阶段预留的业务资源, Cancel操作必须保证幂等性
    "}],"id":"006e94b9f913","title":"流程"},{"parent":"b0b8679322ca","children":[{"parent":"23870fd0159e","children":[],"id":"752def670283","title":"因为Try阶段检查并预留了资源,所以confirm阶段一般都可以执行成功"},{"parent":"23870fd0159e","children":[],"id":"a233a7cbf2e3","title":"资源锁定都是在业务代码中完成,不会block住DB,可以做到对db性能无影响"},{"parent":"23870fd0159e","children":[],"id":"7d19b1ea9c94","title":"TCC的实时性较高,所有的DB写操作都集中在confirm中,写操作的结果实时返回(失败时因为定时程序执行时间的关系,略有延迟)"}],"id":"23870fd0159e","title":"优点"},{"parent":"b0b8679322ca","children":[{"parent":"2ab2502c2344","children":[],"id":"c8b2dc57628d","title":"因为事务状态管理,将产生多次DB操作,这将损耗一定的性能,并使得整个TCC事务时间拉长"},{"parent":"2ab2502c2344","children":[],"id":"4d1e258645f4","title":"事务涉及方越多,Try、Confirm、Cancel中的代码就越复杂,可复用性就越底"},{"parent":"2ab2502c2344","children":[],"id":"56449003ad04","title":"涉及方越多,这几个阶段的处理时间越长,失败的可能性也越高"}],"id":"2ab2502c2344","title":"缺点"},{"parent":"b0b8679322ca","children":[],"id":"4c6d4fa72f28","title":"是业务层面解决方案"}],"id":"b0b8679322ca","title":"TCC(Try、Confirm、Cancel)"},{"parent":"426e047d8f9d","children":[],"id":"121687a469fc","title":"XA"},{"parent":"426e047d8f9d","children":[],"id":"48c137315bda","title":"最大努力通知"},{"parent":"426e047d8f9d","children":[],"id":"c6b7184c14c3","title":"本地消息表(ebay研发出的)"},{"parent":"426e047d8f9d","children":[{"parent":"3b31deded74d","children":[],"id":"84f89c2ef8a1","title":"事务消息"}],"id":"3b31deded74d","title":"半消息/最终一致性(RocketMQ)"}],"collapsed":true,"id":"426e047d8f9d","title":"分布式事务"}],"root":true,"theme":"dark_caihong","id":"root","title":"Java","structure":"mind_right"}},"meta":{"exportTime":"2022-02-16 17:35:03","member":"60b8501a63768975c7bcc153","diagramInfo":{"creator":"60b8501a63768975c7bcc153","created":"2021-06-23 17:55:24","modified":"2022-02-16 17:28:13","title":"Java知识点","category":"mind_free"},"id":"60d3050c1e08532a43b7f737","type":"ProcessOn Schema File","version":"1.0"}} \ No newline at end of file diff --git a/TODO/uml/ReplicaManager#appendRecordstxt b/TODO/uml/ReplicaManager#appendRecordstxt deleted file mode 100644 index cf9eb42aa7..0000000000 --- a/TODO/uml/ReplicaManager#appendRecordstxt +++ /dev/null @@ -1,20 +0,0 @@ -@startuml -title: ReplicaManager#appendRecords - -actor ReplicaManager as ReplicaManager - -alt requiredAcks值合法 -ReplicaManager -> ReplicaManager : 写入消息集到本地日志 -ReplicaManager -> ReplicaManager : 构建写入结果状态 -alt 等待其他副本完成写入 -ReplicaManager -> ReplicaManager : 创建延时请求对象 -ReplicaManager -> ReplicaManager : 交由 Puratory 管理 -else -ReplicaManager -> ReplicaManager : 调用回调逻辑 -end -else requiredAcks值非法 -ReplicaManager -> ReplicaManager : 构造特定异常对象 -ReplicaManager -> ReplicaManager : 封装进回调函数执行 -end - -@enduml \ No newline at end of file diff --git a/TODO/uml/mysql.xmind b/TODO/uml/mysql.xmind deleted file mode 100644 index 2c439b7a2e..0000000000 Binary files a/TODO/uml/mysql.xmind and /dev/null differ diff --git "a/TODO/uml/redis\344\274\230\345\214\226.xmind" "b/TODO/uml/redis\344\274\230\345\214\226.xmind" deleted file mode 100644 index f4dfaa071a..0000000000 Binary files "a/TODO/uml/redis\344\274\230\345\214\226.xmind" and /dev/null differ diff --git a/TODO/uml/spring.xmind b/TODO/uml/spring.xmind deleted file mode 100644 index f9fd71b68e..0000000000 Binary files a/TODO/uml/spring.xmind and /dev/null differ diff --git "a/Tomcat/HttpServlet\344\270\272\344\273\200\344\271\210\350\246\201\345\256\236\347\216\260serializable\357\274\237.md" "b/Tomcat/HttpServlet\344\270\272\344\273\200\344\271\210\350\246\201\345\256\236\347\216\260serializable\357\274\237.md" deleted file mode 100644 index 0d6bacd6c7..0000000000 --- "a/Tomcat/HttpServlet\344\270\272\344\273\200\344\271\210\350\246\201\345\256\236\347\216\260serializable\357\274\237.md" +++ /dev/null @@ -1,8 +0,0 @@ -HttpServlet为什么要实现serializable?在什么情况下,servlet会被序列化? -如果未显示定义serialVersionUID,系统会用什么算法给指定一个? -![](https://img-blog.csdnimg.cn/d745c434cdc8418e9519cb821fbd9317.png) -![](https://img-blog.csdnimg.cn/71dc3f2b6ef14d1099605a75c4412639.png) -Serializable是可序列化。 -简单点将,就是实现了这个接口后,实例就可以转化为数据流了。 - -Servlet 是有状态的,所以需要持久化到本地(钝化),然后当 Tomcat 重启时,重新加载出来。比如Servlet存储了一些用户登录信息,而当时分布式缓存 redis 也还没流行,所以需要支持可序列化。 \ No newline at end of file diff --git "a/Tomcat/Servlet\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.md" "b/Tomcat/Servlet\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.md" deleted file mode 100644 index c3343e881b..0000000000 --- "a/Tomcat/Servlet\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237.md" +++ /dev/null @@ -1,67 +0,0 @@ -Servlet从创建直到毁灭的整个过程: -- Servlet 初始化后调用 init () 方法 -- Servlet 调用 service() 方法来处理客户端的请求 -- Servlet 销毁前调用 destroy() 方法 -- 最后,Servlet 是由 JVM 的垃圾回收器进行GC -# init() -只调用一次。在第一次创建 Servlet 时被调用,在后续每次用户请求时不再调用。因此,它是用于一次性初始化。 - -Servlet 创建于用户第一次调用对应于该 Servlet 的 URL 时,但是您也可以指定 Servlet 在服务器第一次启动时被加载。 - -当用户调用一个 Servlet 时,就会创建一个 Servlet 实例,每一个用户请求都会产生一个新的线程,适当的时候移交给 doGet 或 doPost 方法。init() 方法简单地创建或加载一些数据,这些数据将被用于 Servlet 的整个生命周期。 -```java -public void init() throws ServletException { - // 初始化代码... -} -``` -## service() -执行实际任务的主要方法。Servlet 容器(即 Web 服务器)调用 service() 方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。 - -每次服务器接收到一个 Servlet 请求时,服务器会产生一个新的线程并调用服务。service() 方法检查 HTTP 请求类型(GET、POST、PUT、DELETE 等),并在适当的时候调用 doGet、doPost、doPut,doDelete 等方法。 -```java -public void service(ServletRequest request, - ServletResponse response) - throws ServletException, IOException{ -} -``` -service() 方法由容器调用,service 方法在适当的时候调用 doGet、doPost、doPut、doDelete 等方法。所以,您不用对 service() 方法做任何动作,您只需要根据来自客户端的请求类型来重写 doGet() 或 doPost() 即可。 - -doGet() 和 doPost() 方法是每次服务请求中最常用的方法。下面是这两种方法的特征。 -## doGet() -GET 请求来自于一个 URL 的正常请求,或者来自于一个未指定 METHOD 的 HTML 表单,它由 doGet() 方法处理。 -```java -public void doGet(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException { - // Servlet 代码 -} -``` -## doPost() -POST 请求来自于一个特别指定了 METHOD 为 POST 的 HTML 表单,它由 doPost() 方法处理。 -```java -public void doPost(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException { - // Servlet 代码 -} -``` -## destroy() 方法 -destroy() 方法只会被调用一次,在 Servlet 生命周期结束时被调用。destroy() 方法可以让您的 Servlet 关闭数据库连接、停止后台线程、把 Cookie 列表或点击计数器写入到磁盘,并执行其他类似的清理活动。 - -在调用 destroy() 方法之后,servlet 对象被标记为垃圾回收。destroy 方法定义如下所示: -```java - public void destroy() { - // 终止化代码... - } -``` -# 架构 -![](https://img-blog.csdnimg.cn/64b79221818a4830add2e3333b09355c.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_13,color_FFFFFF,t_70,g_se,x_16) -- 第一个到达服务器的 HTTP 请求被委派到 Servlet 容器 -- Servlet 容器在调用 service() 方法之前加载 Servlet -- 然后 Servlet 容器处理由多个线程产生的多个请求,每个线程执行一个单一的 Servlet 实例的 service() 方法 - -![](https://img-blog.csdnimg.cn/4a408cce640d4fccae2efb410bab50e1.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -Spring 设计成了枚举 -![](https://img-blog.csdnimg.cn/7267c2be0c0d4f03b7b93405ea8779d5.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -Tomat 是设计成普通常量,没有范围,可随便突破。 -区别在于,枚举进行了范围限制。 \ No newline at end of file diff --git "a/Tomcat/Servlet\350\247\204\350\214\203\345\217\212\345\256\271\345\231\250.md" "b/Tomcat/Servlet\350\247\204\350\214\203\345\217\212\345\256\271\345\231\250.md" index e98a8c06de..67d8a821f9 100644 --- "a/Tomcat/Servlet\350\247\204\350\214\203\345\217\212\345\256\271\345\231\250.md" +++ "b/Tomcat/Servlet\350\247\204\350\214\203\345\217\212\345\256\271\345\231\250.md" @@ -1,71 +1,33 @@ -# 1 遥远的CGI -实现Web动态内容的技术,最早使用的是CGI(Common Gateway Interface,通用网关接口)技术,根据用户输入的请求动态地传送HTML数据。 -CGI并不是开发语言,而只是能够利用为它编写的程序来实现Web服务器的一种协议。 -可用来实现电子商务网站、搜索引擎处理和在线登记等功能。当用户在Web页面中提交输入的数据时,Web浏览器就会将用户输入的数据发送到Web服务器上。在服务器上,CGI程序对输入的数据进行格式化,并将这个信息发送给数据库或服务器上运行的其他程序,然后将结果返回给Web服务器。最后,Web服务器将结果发送给Web浏览器,这些结果有时使用新的Web页面显示,有时在当前Web页面中显示。 - -编写自定义CGI脚本需要相当多的编程技巧,多数CGI脚本是由Perl,Java,C和C++等语言编写的,服务器上通常很少运行用JavaScript编写的服务器脚本,不管使用何种语言,Web页面设计者都需要控制服务器,包括所需要的后台程序(如数据库),这些后台程序提供结果或来自用户的消息。即使拥有基于服务器的网站设计工具,编写CGI程序也要求程序设计者有一定的经验。 - -由于每一次对于动态内容的请求都需要启动一个新的CGI程序,因而会增加Web服务器的负担,所以CGI的一个很大缺陷是容易影响Web服务器的运行速度。 -## 脚本编程 -由于CGI程序与HTML文档需要分开编写、分开运行,要将两者融合在一起并不容易,因此,CGI程序的维护与编写比较困难。为了解决这一问题,一些厂商推出了脚本语言来增强网页开发功能。 - -脚本语言是一种文本型编程语言,可嵌入到HTML文档中。脚本语言分客户端和服务器端两种类型,分别在Web浏览器和Web服务器中运行。 - -客户端脚本语言主要有JavaScript、Jscript(Microsoft公司的JavaSCript版本)和VBscript等。当Web浏览器需要浏览使用客户端脚本语言编写的Web页面时,Web服务器将客户端脚本连同Web页面一起传送到Web浏览器,Web浏览器同时显示HTML的显示效果和客户端脚本的运行效果, 客户端脚本可减轻Web服务器的处理负担,提高Web页面的响应速度。 - -服务器端脚本语言主要有ASP,JSP,PHP和LiveWire等。当Web浏览器需要浏览使用服务器端脚本语言编写的Web页面时,Web服务器运行Web页面中的服务器端脚本,将由脚本语言的运行结果与Web页面的HTML部分生成的新的Web页面传送到Web浏览器,Web浏览器显示生成的新的Web页面, 服务器端脚本可减少不同Web浏览器的运行差异,提高Web页面的实用性。 - -这期间,Java 的 Servlet模型也就诞生了。 -# 2 Servlet -一种基于Java技术的Web组件,用于生成动态内容,由容器管理。类似于其它Java技术组件,Servlet 是平台无关的Java类组成,并且由Java Web服务器加载执行。 - +> Servlet是一种基于Java技术的Web组件,用于生成动态内容,由容器管理。类似于其它Java技术组件,Servlet 是平台无关的Java类组成,并且由Java Web服务器加载执行。 通常由Servlet容器提供运行时环境。Servlet 容器,有时候也称作为Servlet引擎,作为Web服务器或应用服务器的一部分 。通过请求和响应对话,提供Web客户端与Servlets 交互的能力。容器管理Servlets实例以及它们的生命周期。 - -## 主要版本 ![](https://img-blog.csdnimg.cn/9bdc4d3780174fe6a963291dfea900bb.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -## 核心组件 -|核心组件 API|说明 | 起始版本| Spring framework 代表实现 | -|--|--|--|--| -|javax. servlet .Servlet | 动态内容组件| 1.0| DispatcherServlet| -|javax. servlet .Filter | Servlet过滤器| 2.3| CharacterEncodingFilter| -|javax. servlet .ServletContext | Servlet应用上下文| | | -|javax. servlet .AsyncContext | 异步上下文|3.0 | 无| -|javax. servlet .ServletContextistener | ServletContext生命周期监听器| 2.3 | ContextLoaderListener | -|javax . servlet . ServletRequestlistener | ServletRequest生命周期监听器| 2.3 | RequestContextListener | -|javax. servlet .http.HttpSessionListener | 生命周期监听器| 2.3| HttpSessionMutexListener | -|javax. servlet .Asynclistener | 异步上下文监听器| 3.0| StandardServletAsyncWebRequest | -|javax. servlet .ServletContainerInitializer | Servlet容器初始化器|3.0 | SpringServletContainerInitializer | - -浏览器发给服务端的是一个HTTP请求,HTTP服务器收到请求后,需调用服务端程序处理请求。 -> HTTP服务器怎么知道要调用哪个处理器方法? +浏览器发给服务端的是一个HTTP格式的请求,HTTP服务器收到这个请求后,需要调用服务端程序来处理。 -最简单的就是在HTTP服务器代码写一堆if/else:若是A请求就调x类m1方法,若是B请求就调o类的m2方法。 -这种设计的致命点在于HTTP服务器代码跟业务逻辑耦合,若你新增了业务方法,竟然还得改HTTP服务器代码。 -**面向接口编程**算得上是解决耦合问题的银弹,我们可定义一个接口,各业务类都实现该接口,没错,他就是Servlet接口,实现了Servlet接口的业务类也叫作Servlet。 +那HTTP服务器怎么知道要调用哪个处理器方法。最直接的做法是在HTTP服务器代码里写一大堆if else:如果是A请求就调X类的M1方法,如果是B请求就调Y类的M2方法。 -解决了业务逻辑和HTTP服务器的耦合问题,那又有问题了:对特定请求,HTTP服务器又如何知道: -- 哪个Servlet负责处理请求? -- 谁负责实例化Servlet? +这样设计的问题在于HTTP服务器的代码跟业务逻辑耦合,若新增业务方法还要改HTTP服务器的代码! +面向接口编程是解决耦合问题的银弹,可以定义一个接口,各种业务类都必须实现这个接口,这个接口就是Servlet接口,也把实现了Servlet接口的业务类叫作Servlet。 -显然HTTP服务器不适合负责这些,否则又要和业务逻辑耦合。 +对特定请求,HTTP服务器如何知道: +- 由哪个Servlet处理? +- Servlet该由谁实例化? -于是,诞生了Servlet容器。 -# 3 Servlet容器 -用于加载和管理业务类。 - -HTTP服务器不直接和业务类交互,而是把请求先交给Servlet容器,Servlet容器内部将请求转发到具体Servlet。 -若该Servlet还没创建,就加载并实例化之,然后调用该Servlet的接口方法。 +显然HTTP服务器并不适合负责这些,否则又和业务类耦合。 +# Servlet容器 +于是,又设计Servlet容器,以加载和管理业务类。HTTP服务器不直接跟业务类打交道,而是把请求交给Servlet容器,Servlet容器会将请求转发到具体Servlet。 +若该Servlet还没创建,就加载并实例化这个Servlet,然后调用这个Servlet的接口方法。 因此Servlet接口其实是Servlet容器跟具体业务类之间的接口: ![](https://img-blog.csdnimg.cn/20210716140239681.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -左:HTTP服务器直接调用具体业务类,但紧耦合。 -右:HTTP服务器不直接调用业务类,而是把请求移交给容器,容器通过Servlet接口调用业务类。因此Servlet接口和Servlet容器,实现了HTTP服务器与业务类的解耦。 +左边表示HTTP服务器直接调用具体业务类,但它们紧耦合。 +右边,HTTP服务器不直接调用业务类,而是把请求交给容器,容器通过Servlet接口调用业务类。因此Servlet接口和Servlet容器,实现HTTP服务器与业务类解耦。 + +**Servlet接口和Servlet容器**这一整套规范叫作**Servlet规范**。Tomcat按Servlet规范要求实现了Servlet容器,同时也具有HTTP服务器功能。 +若实现新业务,只需实现一个Servlet,并把它注册到Tomcat(Servlet容器),剩下的事情就由Tomcat帮忙处理了。 -**Servlet接口和Servlet容器**这一整套规范叫作**Servlet规范**。Tomcat按Servlet规范要求实现了Servlet容器,又兼备HTTP服务器功能。 -若实现新业务,只需实现一个Servlet,并把它注册到Tomcat(Servlet容器),剩下的事情就由Tomcat帮忙。 -# 4 Servlet接口 +# Servlet接口 Servlet接口定义了下面五个方法: ![](https://img-blog.csdnimg.cn/20210716140638840.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) @@ -93,7 +55,8 @@ ServletConfig的作用就是封装Servlet的初始化参数。 有接口一般就有抽象类,抽象类用来实现接口和封装通用的逻辑,因此Servlet规范提供了GenericServlet抽象类,可以通过扩展它来实现Servlet。虽然Servlet规范并不在乎通信协议是什么,但是大多数的Servlet都是在HTTP环境中处理的,因此Servet规范还提供了HttpServlet来继承GenericServlet,并且加入了HTTP特性。这样我们通过继承HttpServlet类来实现自己的Servlet,只需要重写两个方法:doGet和doPost。 ![](https://img-blog.csdnimg.cn/20210716162226262.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -# 5 Servlet容器 + +# Servlet容器 ## 工作流程 当客户请求某个资源时 - HTTP服务器用ServletRequest对象封装客户的请求信息 @@ -121,31 +84,22 @@ Servlet容器通过读取配置文件,就能找到并加载Servlet。Web应用 | - WEB-INF/classes/ -- 存放你的应用类,比如Servlet类 | - META-INF/ -- 目录存放工程的一些信息 ``` -# 6 ServletContext +# ServletContext +Servlet规范定义了ServletContext接口对应一个Web应用。 -> 定义了一系列servlet用于与其servlet容器通信的方法。如获取文件的 MIME 类型、调度请求或写入日志文件。 -> 每个JVM的Web应用程序都有一个上下文。(Web 应用程序是安装在服务器 URL 名称空间(如 -> `/catalog`)的特定子集下并可能通过 。war 文件安装的服务和内容的集合。如果在部署描述符中标 -> -> 分布式系统下,则每个机器节点都有一个上下文实例。在这种情况下,上下文不能用作共享全局信息的位置(因为信息不会是真正的全局的)。应该改用数据库等外部资源。ServletContext -> 对象包含在 ServletConfig 对象中,当服务器初始化时,Web 服务器会提供该对象。 +Web应用部署好后,Servlet容器在启动时会加载Web应用,并为每个Web应用创建一个全局的上下文环境ServletContext对象,其为后面的Spring容器提供宿主环境。 -Servlet规范定义了ServletContext接口对应一个Web应用。比如一个 SpringBoot 应用,那就只有一个ServletContext。 -不要和 Spring 的 applicationContext 混为一谈,因为一个应用中,可以有多个Spring 的 applicationContext。 +可以把ServletContext看成是一个全局对象,一个Web应用可能有多个Servlet,这些Servlet可以通过全局的ServletContext共享数据,这些数据包括Web应用的初始化参数、Web应用目录下的文件资源等。 -Web应用部署好后,Servlet容器在启动时会加载Web应用,并为每个Web应用创建一个全局的上下文环境ServletContext对象,为后面的Spring容器提供宿主环境。 +ServletContext持有所有Servlet实例,所以还能通过它来实现Servlet请求的转发。 -可将**ServletContext**看做一个全局对象,一个Web应用可能有多个Servlet,这些Servlet可通过全局ServletContext共享数据: -- Web应用的初始化参数 -- Web应用目录下的文件资源等 - -**ServletContext** 持有所有Servlet实例,所以也能实现Servlet请求的转发。 Tomcat&Jetty在启动过程中触发容器初始化事件,Spring的ContextLoaderListener会监听到这个事件,它的contextInitialized方法会被调用,在这个方法中,Spring会初始化全局的Spring根容器,这个就是Spring的IoC容器。 -IoC容器初始化完毕后,Spring将其存储到**ServletContext**,便于以后获取。 +IoC容器初始化完毕后,Spring将其存储到ServletContext中,便于以后获取。 ![](https://img-blog.csdnimg.cn/20210716220353539.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) ServletContext就是用来共享数据的,比如SpringMVC需要从ServletContext拿到全局的Spring容器,把它设置成自己的父容器。 + Tomcat&Jetty在启动过程中还会扫描Servlet,一个Web应用中的Servlet可以有多个,以SpringMVC中的DispatcherServlet为例,这个Servlet实际上是一个标准的前端控制器,用以转发、匹配、处理每个Servlet请求。 Servlet一般会延迟加载,当第一个请求达到时,Tomcat&Jetty发现DispatcherServlet还没有被实例化,就调用DispatcherServlet的init方法,DispatcherServlet在初始化的时候会建立自己的容器,叫做SpringMVC 容器,用来持有Spring MVC相关的Bean。同时,Spring MVC还会通过ServletContext拿到Spring根容器,并将Spring根容器设为SpringMVC容器的父容器,请注意,Spring MVC容器可以访问父容器中的Bean,但是父容器不能访问子容器的Bean, 也就是说Spring根容器不能访问SpringMVC容器里的Bean。说的通俗点就是,在Controller里可以访问Service对象,但是在Service里不可以访问Controller对象。 @@ -174,42 +128,14 @@ DispatcherServlet中的成员变量都是初始化好后就不会被改变了, ![](https://img-blog.csdnimg.cn/20210716204054195.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) ![](https://img-blog.csdnimg.cn/20210716210859693.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) ![](https://img-blog.csdnimg.cn/20210716210934704.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -# 扩展机制 -引入了Servlet规范后,无需关心Socket网络通信、HTTP协议或你的业务类是如何被实例化和调用的,因为这些都被Servlet规范标准化了,我们只需关心怎么实现业务逻辑。 -有规范看着很方便,但若规范不能满足你的个性需求,就没法用了,因此设计一个规范或者一个中间件,要充分考虑到可扩展性。 -Servlet规范提供了两种扩展机制:Filter和Listener。 -## Filter -过滤器,该接口允许你对请求和响应做一些统一的定制化处理。Filter是基于过程的,它是过程的一部分,是基于过程行为的。比如: -- 根据请求频率限制访问 -- 根据地区不同修改响应内容 - - 过滤器是 Servlet 的重要标准之一: -- 请求和响应的统一处理 -- 访问日志记录 -- 请求权限审核 - ...... -都发挥重要作用 - -### 工作原理 -Web应用部署完成后,Servlet容器需要实例化Filter并把Filter链接成一个FilterChain。当请求进来时,获取第一个Filter并调用doFilter方法,doFilter方法负责调用这个FilterChain中的下一个Filter。 - -## Listener -监听器,Listener是基于状态的,任何行为改变同一个状态,触发的事件是一致的。当Web应用在Servlet容器中运行时,Servlet容器内部会不断的发生各种事件,如Web应用的启动和停止、用户请求到达等。 Servlet容器提供了一些默认的监听器来监听这些事件,当事件发生时,Servlet容器会负责调用监听器的方法。当然,你可以定义自己的监听器去监听你感兴趣的事件,将监听器配置在web.xml中。比如Spring就实现了自己的监听器,来监听ServletContext的启动事件,目的是当Servlet容器启动时,创建并初始化全局的Spring容器。 -# X FAQ -**service方法为什么把request和response都当作输入参数,而不是输入参数只有request,response放到返回值里呢?** -方便责任链模式下层层传递。 - -在SpringBoot项目中,为什么没有web.xml了? -SpringBoot是以嵌入式的方式来启动Tomcat。对于SpringBoot来说,Tomcat只是个JAR包。SpringBoot通过Servlet3.0规范中 **@WebServlet** 注解或者API直接向Servlet容器添加Servlet,无需web.xml。 - -## 分不清的xxx容器 -### Servlet容器 +# 分不清的xxx容器 +## Servlet容器 用于管理Servlet生命周期。 -### SpringMVC容器 +## SpringMVC容器 管理SpringMVC Bean生命周期。 -### Spring容器 +## Spring容器 用于管理Spring Bean生命周期。包含许多子容器,其中SpringMVC容器就是其中常用的,DispatcherServlet就是SpringMVC容器中的servlet接口,也是SpringMVC容器的核心类。 Spring容器主要用于整个Web应用程序需要共享的一些组件,比如DAO、数据库的ConnectionFactory等,SpringMVC容器主要用于和该Servlet相关的一些组件,比如Controller、ViewResovler等。 @@ -238,10 +164,38 @@ org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApp ![](https://img-blog.csdnimg.cn/20210716201457871.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70)servlet容器初始化成功后被spring监听,创建spring容器放入servlet容器中,访问到达,初始化dispatcher servlet时创建springmvc容器,通过servletContext拿到spring容器,并将其作为自己的父容器,spring mvc容器会定义controller相关的bean,spring会定义业务逻辑相关的bean。 +# 扩展机制 +引入了Servlet规范后,无需关心Socket网络通信、HTTP协议或你的业务类是如何被实例化和调用的,因为这些都被Servlet规范标准化了,我们只需关心怎么实现业务逻辑。 + +有规范看着很方便,但若规范不能满足你的个性需求,就没法用了,因此设计一个规范或者一个中间件,要充分考虑到可扩展性。 +Servlet规范提供了两种扩展机制:Filter和Listener。 +## Filter +过滤器,该接口允许你对请求和响应做一些统一的定制化处理。Filter是基于过程的,它是过程的一部分,是基于过程行为的。比如: +- 根据请求频率限制访问 +- 根据地区不同修改响应内容 + + 过滤器是 Servlet 的重要标准之一: +- 请求和响应的统一处理 +- 访问日志记录 +- 请求权限审核 + ...... +都发挥重要作用 + +### 工作原理 +Web应用部署完成后,Servlet容器需要实例化Filter并把Filter链接成一个FilterChain。当请求进来时,获取第一个Filter并调用doFilter方法,doFilter方法负责调用这个FilterChain中的下一个Filter。 + +## Listener +监听器,Listener是基于状态的,任何行为改变同一个状态,触发的事件是一致的。当Web应用在Servlet容器中运行时,Servlet容器内部会不断的发生各种事件,如Web应用的启动和停止、用户请求到达等。 Servlet容器提供了一些默认的监听器来监听这些事件,当事件发生时,Servlet容器会负责调用监听器的方法。当然,你可以定义自己的监听器去监听你感兴趣的事件,将监听器配置在web.xml中。比如Spring就实现了自己的监听器,来监听ServletContext的启动事件,目的是当Servlet容器启动时,创建并初始化全局的Spring容器。 + +# FAQ +**service方法为什么把request和response都当作输入参数,而不是输入参数只有request,response放到返回值里呢?** +方便责任链模式下层层传递。 + +在SpringBoot项目中,为什么没有web.xml了? +SpringBoot是以嵌入式的方式来启动Tomcat。对于SpringBoot来说,Tomcat只是个JAR包。SpringBoot通过Servlet3.0规范中 **@WebServlet** 注解或者API直接向Servlet容器添加Servlet,无需web.xml。 > 参考 > - https://blog.csdn.net/zhanglf02/article/details/89791797 > - https://docs.oracle.com/cd/E19146-01/819-2634/abxbh/index.html > - https://matthung0807.blogspot.com/2020/09/tomcat-how-servlet-construct.html -> - https://github.com/heroku/devcenter-embedded-tomcat -> - https://biang.io/blog/mixed/cgi \ No newline at end of file +> - https://github.com/heroku/devcenter-embedded-tomcat \ No newline at end of file diff --git "a/Tomcat/Tomcat\350\277\236\346\216\245\345\231\250\350\256\276\350\256\241\346\236\266\346\236\204.md" "b/Tomcat/Tomcat\350\277\236\346\216\245\345\231\250\350\256\276\350\256\241\346\236\266\346\236\204.md" index 9642be5a67..79cb6dce68 100644 --- "a/Tomcat/Tomcat\350\277\236\346\216\245\345\231\250\350\256\276\350\256\241\346\236\266\346\236\204.md" +++ "b/Tomcat/Tomcat\350\277\236\346\216\245\345\231\250\350\256\276\350\256\241\346\236\266\346\236\204.md" @@ -1,39 +1,48 @@ -# 1 Tomcat 核心功能 +# 导读 +为什么要学习Tomcat的设计思路,对我们这些 crud 工程师有用吗?现代背景下,我们不仅可以学到Tomcat的架构,学会从宏观上怎么去设计一个复杂系统,怎么设计顶层模块,以及模块之间关系。 + +# Tomcat总体架构 +Tomcat主要实现了2个核心功能: - 处理Socket连接,负责网络字节流与Request和Response对象的转化 -因此Tomcat设计了连接器(Connector),负责对外交流 - 加载和管理Servlet,以及具体处理Request请求 -设计了容器(Container),负责内部处理 -# 2 Tomcat支持的I/O模型 +因此Tomcat设计了两个核心组件: +- 连接器(Connector) +负责对外交流 +- 容器(Container) +负责内部处理。 + +> **Tomcat支持哪些I/O模型呢?** + - NIO 非阻塞I/O,采用Java NIO类库实现。 - NIO.2 异步I/O,采用JDK 7最新的NIO.2类库实现。 - APR -采用Apache可移植运行库实现,是C/C++编写的本地库 +采用Apache可移植运行库实现,是C/C++编写的本地库。 + +> **Tomcat支持哪些应用层协议呢?** -![](https://img-blog.csdnimg.cn/d562828da21147818236f6e2d2bbc8d2.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -# 3 Tomcat支持的应用层协议 - HTTP/1.1 大部分Web应用采用的访问协议。 - AJP 用于和Web服务器集成(如Apache)。 - HTTP/2 HTTP 2.0大幅度的提升了Web性能。 -# 4 Service -Tomcat为 **支持多种I/O模型和应用层协议**,一个容器可能对接多个连接器。 -但单独的连接器或容器都无法对外提供服务,需**组装**才能正常协作,而组装后的整体,就称为Service组件。所以,Service并不神奇,只是在连接器和容器外面多包了一层,把它们组装在一起。 -Tomcat内可能有多个Service,在Tomcat中配置多个Service,可实现通过不同端口号访问同一台机器上部署的不同应用。 -![](https://img-blog.csdnimg.cn/94069010a40243b3b33fc96ae5f0f3d5.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -最顶层是Server(即一个Tomcat实例)。一个Server中有一或多个Service,一个Service中有多个连接器和一个容器。 +## Service +Tomcat为支持**多种I/O模型和应用层协议**,一个容器可能对接多个连接器。 +但单独的连接器或容器都无法对外提供服务,需**组装**起来才能正常工作,组装后的整体称为Service组件。 -连接器与容器之间通过标准的ServletRequest/ServletResponse通信。 -# 5 连接器架构 -连接器对Servlet容器屏蔽了 **协议及I/O模型的区别**,处理Socket通信和应用层协议的解析,得到Servlet请求。 -所以无论是HTTP、AJP,最终在容器中获取到的都是标准ServletRequest对象。 +Service只是在连接器和容器外面多包了一层,把它们组装在一起。 +Tomcat内可能有多个Service,通过在Tomcat中配置多个Service,可实现通过不同端口号访问同一台机器上部署的不同应用。 +![](https://img-blog.csdnimg.cn/20210717161052233.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70)最顶层是Server(即一个Tomcat实例)。一个Server中有一或多个Service,一个Service中有多个连接器和一个容器。 -## 5.1 功能需求 +连接器与容器之间通过标准的ServletRequest和ServletResponse通信。 +## 连接器 +连接器对Servlet容器屏蔽了协议及I/O模型等的区别,无论是HTTP还是AJP,在容器中获取到的都是一个标准的ServletRequest对象。 + +连接器的详细功能: - 监听网络端口 - 接受网络连接请求 - 读取网络请求字节流 @@ -44,15 +53,15 @@ Tomcat内可能有多个Service,在Tomcat中配置多个Service,可实现通 - 将Tomcat Response转成网络字节流 - 将响应字节流写回给浏览器。 -那它应该有哪些子模块呢? -优秀的模块化设计应该考虑高内聚、低耦合。连接器需完成如下高内聚功能: +提完了需求,就需要考虑它应该有哪些子模块呢? +优秀的模块化设计应该考虑高内聚、低耦合。通过分析连接器的功能,发现连接器需要完成如下高内聚的功能: - 网络通信 - 应用层协议解析 - Tomcat Request/Response与ServletRequest/ServletResponse的转化 -因此Tomcat设计3个组件实现这3功能:Endpoint、Processor和Adapter。 +因此Tomcat的设计者设计了3个组件来实现这3个功能:Endpoint、Processor和Adapter。 -组件间通过抽象接口交互,以封装变化:将系统中经常变化的部分和稳定的部分隔离,有助于增加复用性,并降低系统耦合度。 +组件之间通过抽象接口交互。这样做还有一个好处是封装变化。这是面向对象设计的精髓,将系统中经常变化的部分和稳定的部分隔离,有助于增加复用性,并降低系统耦合度。 不管网络通信I/O模型、应用层协议、浏览器端发送的请求信息如何变化,但整体处理逻辑不变: - Endpoint @@ -62,42 +71,51 @@ Tomcat内可能有多个Service,在Tomcat中配置多个Service,可实现通 - Adapter 提供ServletRequest对象给容器 -若要支持新的I/O方案、新的应用层协议,只需要实现相关具体子类,而上层通用处理逻辑不变。 +如果要支持新的I/O方案、新的应用层协议,只需要实现相关具体子类,上层通用的处理逻辑不会变。 -由于I/O模型和应用层协议可自由组合,比如NIO + HTTP或者NIO.2 + AJP。Tomcat将网络通信和应用层协议解析放在一起考虑,设计了ProtocolHandler接口,封装这两种变化点。 -## 5.2 ProtocolHandler -各种协议和通信模型的组合有相应的具体实现类,如: +由于I/O模型和应用层协议可自由组合,比如NIO + HTTP或者NIO.2 + AJP。Tomcat的设计者将网络通信和应用层协议解析放在一起考虑,设计ProtocolHandler接口,封装这两种变化点。各种协议和通信模型的组合有相应的具体实现类。 +比如:Http11NioProtocol和AjpNioProtocol ![](https://img-blog.csdnimg.cn/20210717231048532.png) ![](https://img-blog.csdnimg.cn/20210717231106745.png) ![](https://img-blog.csdnimg.cn/20210717231144614.png) -Tomcat设计了一系列抽象基类封装稳定部分,抽象基类AbstractProtocol实现了ProtocolHandler接口。 -每种应用层协议有自己的抽象基类,如AbstractAjpProtocol、AbstractHttp11Protocol,具体协议实现类扩展了协议层抽象基类。 + +除了这些变化点,系统也存在一些相对稳定部分,因此Tomcat设计了一系列抽象基类来封装这些稳定部分,抽象基类AbstractProtocol实现了ProtocolHandler接口。每一种应用层协议有自己的抽象基类,比如AbstractAjpProtocol和AbstractHttp11Protocol,具体协议的实现类扩展了协议层抽象基类。 ![](https://img-blog.csdnimg.cn/20210717232129435.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -如此设计,尽量地将稳定的部分放到抽象基类,同时每一种I/O模型和协议的组合都有相应的具体实现类,我们在使用时可以自由选择。 +这样设计是为尽量将稳定的部分放到抽象基类,同时每一种I/O模型和协议的组合都有相应的具体实现类,我们在使用时可以自由选择。 -**Endpoint和Processor放在一起抽象成了ProtocolHandler组件**: -![](https://img-blog.csdnimg.cn/8005f739f3f442b28ed568395af2435e.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) +连接器模块的核心组件: +- Endpoint +- Processor +- Adapter + +Endpoint和Processor放在一起抽象成了ProtocolHandler组件 +![](https://img-blog.csdnimg.cn/20210717232632164.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) + +# 顶层组件 +## ProtocolHandler组件 连接器用ProtocolHandler处理网络连接、应用层协议,包含如下重要部件 -### 5.2.1 Endpoint +### Endpoint 通信端点,即通信监听的接口,是具体的Socket接收和发送处理器,是对传输层的抽象,因此Endpoint用来实现TCP/IP协议。 Endpoint是一个接口,对应的抽象实现类是AbstractEndpoint,而AbstractEndpoint的具体子类,比如在NioEndpoint和Nio2Endpoint中,有两个重要的子组件:Acceptor和SocketProcessor。 + #### Acceptor 用于监听Socket连接请求。SocketProcessor用于处理接收到的Socket请求,它实现Runnable接口,在run方法里调用协议处理组件Processor进行处理。 为了提高处理能力,SocketProcessor被提交到线程池来执行。而这个线程池叫作执行器(Executor)。 -### 5.2.2 Processor +### Processor Processor用来实现应用层的HTTP协议,接收来自Endpoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理。 Processor是一个接口,定义了请求的处理等方法。它的抽象实现类AbstractProcessor对一些协议共有的属性进行封装,没有对方法进行实现。具体的实现有AjpProcessor、Http11Processor等,这些具体实现类实现了特定协议的解析方法和请求处理方式。 连接器的组件图: -![](https://img-blog.csdnimg.cn/8180c30045194e29b5c1d1ef500fa9a9.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) +![](https://img-blog.csdnimg.cn/20210717234148960.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) Endpoint接收到Socket连接后,生成一个SocketProcessor任务提交到线程池处理,SocketProcessor的run方法会调用Processor组件去解析应用层协议,Processor通过解析生成Request对象后,会调用Adapter的Service方法。 一个连接器对应一个监听端口,比如一扇门,一个web应用是一个业务部门,进了这个门后你可以到各个业务部门去办事。 Tomcat配置的并发数是endpoint里那个线程池。 -### 5.2.3 Adapter + +## Adapter 由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的Request类来“存放”这些请求信息。 ProtocolHandler接口负责解析请求并生成Tomcat Request类,但这个Request对象不是标准ServletRequest,不能用Tomcat Request作为参数调用容器。 @@ -111,8 +129,12 @@ ProtocolHandler接口负责解析请求并生成Tomcat Request类,但这个Req 对象转化的性能消耗还是比较少的,Tomcat对HTTP请求体采取了延迟解析策略,即TomcatRequest对象转化成ServletRequest时,请求体的内容都还没读取,直到容器处理这个请求的时候才读取。 Adapter一层使用的是适配器设计模式,好处是当容器版本升级只修改Adaper组件适配到新版本容器就可以了,protocal handler组件代码不需要改动。 -# 6 Tomcat V.S Netty -**为何Netty常用做底层通讯模块,而Tomcat作为web容器?** -可将Netty理解成Tomcat中的连接器,都负责网络通信、利用了NIO。但Netty素以高性能高并发著称,为何Tomcat不直接将连接器替换成Netty? -- Tomcat的连接器性能已经足够好了,同样是Java NIO编程,底层原理类似 -- Tomcat做为Web容器,需考虑Servlet规范,Servlet规范规定了对HTTP Body的读写是阻塞的,因此即使用到Netty,也不能充分发挥其优势。所以Netty一般用在非HTTP协议/Servlet场景。 \ No newline at end of file + +# FAQ +**如何设计复杂系统** +首先要分析需求,根据高内聚低耦合的原则确定子模块,然后找出子模块中的变化点和不变点,用接口和抽象基类去封装不变点,在抽象基类中定义模板方法,让子类自行实现抽象方法,也就是具体子类去实现变化点。 + +**tomcat和netty有什么区别呢?为什么netty常常用做底层通讯模块,而tomcat作为web容器呢?** +可以把Netty理解成Tomcat中的连接器,它们都负责网络通信,都利用了NIO。但Netty素以高性能高并发著称,为什么Tomcat不把连接器替换成Netty呢? +- Tomcat的连接器性能已经足够好了,同样是Java NIO编程,底层原理差不多 +- Tomcat做为Web容器,需要考虑到Servlet规范,Servlet规范规定了对HTTP Body的读写是阻塞的,因此即使用到了Netty,也不能充分发挥它的优势。所以Netty一般用在非HTTP协议和Servlet的场景下。 \ No newline at end of file diff --git a/out/TODO/uml/ReplicaManager#appendRecordstxt/ReplicaManager#appendRecordstxt.png b/out/TODO/uml/ReplicaManager#appendRecordstxt/ReplicaManager#appendRecordstxt.png deleted file mode 100644 index f0f9ca8bf5..0000000000 Binary files a/out/TODO/uml/ReplicaManager#appendRecordstxt/ReplicaManager#appendRecordstxt.png and /dev/null differ diff --git a/out/TODO/uml/ReplicaManager#fetchMessages/ReplicaManager#fetchMessages.png b/out/TODO/uml/ReplicaManager#fetchMessages/ReplicaManager#fetchMessages.png deleted file mode 100644 index 0e78971422..0000000000 Binary files a/out/TODO/uml/ReplicaManager#fetchMessages/ReplicaManager#fetchMessages.png and /dev/null differ diff --git a/out/TODO/uml/appendRecords/appendRecords.png b/out/TODO/uml/appendRecords/appendRecords.png deleted file mode 100644 index 4834cb85f7..0000000000 Binary files a/out/TODO/uml/appendRecords/appendRecords.png and /dev/null differ diff --git a/out/TODO/uml/processFetchRequest/processFetchRequest.png b/out/TODO/uml/processFetchRequest/processFetchRequest.png deleted file mode 100644 index 546e4be796..0000000000 Binary files a/out/TODO/uml/processFetchRequest/processFetchRequest.png and /dev/null differ diff --git "a/\345\244\247\346\225\260\346\215\256/\345\244\247\346\225\260\346\215\256\345\205\245\351\227\250(\344\272\224)-\345\210\206\345\270\203\345\274\217\350\256\241\347\256\227\346\241\206\346\236\266MapReduce.md" "b/\345\244\247\346\225\260\346\215\256/\345\244\247\346\225\260\346\215\256\345\205\245\351\227\250(\344\272\224)-\345\210\206\345\270\203\345\274\217\350\256\241\347\256\227\346\241\206\346\236\266MapReduce.md" index 0282e5cbcc..45ec7c21b8 100644 --- "a/\345\244\247\346\225\260\346\215\256/\345\244\247\346\225\260\346\215\256\345\205\245\351\227\250(\344\272\224)-\345\210\206\345\270\203\345\274\217\350\256\241\347\256\227\346\241\206\346\236\266MapReduce.md" +++ "b/\345\244\247\346\225\260\346\215\256/\345\244\247\346\225\260\346\215\256\345\205\245\351\227\250(\344\272\224)-\345\210\206\345\270\203\345\274\217\350\256\241\347\256\227\346\241\206\346\236\266MapReduce.md" @@ -2,32 +2,39 @@ 源自于Google的MapReduce论文,发表于2004年12月。 Hadoop MapReduce是Google MapReduce的克隆版 -## 优点 -海量数量离线处理 -易开发 -易运行 -## 缺点 + +- 优点 +海量数量离线处理&易开发&易运行 + +- 缺点 实时流式计算 -# 2 MapReduce编程模型 -## wordcount词频统计 -![](https://img-blog.csdnimg.cn/img_convert/d30aef6db2174113c919b20226580eed.png) +# 2 MapReduce编程模型之通过wordcount词频统计分析案例入门 +![](https://upload-images.jianshu.io/upload_images/4685968-7563d21e44338bbd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # MapReduce执行流程 - 将作业拆分成Map阶段和Reduce阶段 + - Map阶段: Map Tasks + - Reduce阶段、: Reduce Tasks + ## MapReduce编程模型执行步骤 -- 准备map处理的输入数据 -- Mapper处理 -- Shuffle -- Reduce处理 -- 结果输出 -![](https://img-blog.csdnimg.cn/img_convert/b3514b6cf271ddcffe0b15c1c5a9521d.png) +◆准备map处理的输入数据 + +◆Mapper处理 + +◆Shuffle + +◆Reduce处理 + +◆结果输出 + +![](https://upload-images.jianshu.io/upload_images/4685968-e1f014833eae5eb8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - InputFormat -![](https://img-blog.csdnimg.cn/img_convert/fd11b71639ce1170689a5265dd1b525d.png) -![](https://img-blog.csdnimg.cn/img_convert/da4cd5f1a39b89744943351ea7897f71.png) -![](https://img-blog.csdnimg.cn/img_convert/1d604b7e3f0846ce6529d0f603c983d4.png) -![](https://img-blog.csdnimg.cn/img_convert/74a2e3d81bbb2a2a0781697e85a72f30.png) +![](https://upload-images.jianshu.io/upload_images/4685968-1907b9114cc42568.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-3ac372af0238dc7b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-2f4a418b7d835e6b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-b3e573bdacdb712d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) #### OutputFormat OutputFormt接口决定了在哪里以及怎样持久化作业结果。Hadoop为不同类型的格式提供了一系列的类和接口,实现自定义操作只要继承其中的某个类或接口即可。你可能已经熟悉了默认的OutputFormat,也就是TextOutputFormat,它是一种以行分隔,包含制表符界定的键值对的文本文件格式。尽管如此,对多数类型的数据而言,如再常见不过的数字,文本序列化会浪费一些空间,由此带来的结果是运行时间更长且资源消耗更多。为了避免文本文件的弊端,Hadoop提供了SequenceFileOutputformat,它将对象表示成二进制形式而不再是文本文件,并将结果进行压缩。 @@ -35,32 +42,33 @@ OutputFormt接口决定了在哪里以及怎样持久化作业结果。Hadoop为 Split InputFormat OutputFormat -Combiner +C ombiner Partitioner -![](https://img-blog.csdnimg.cn/img_convert/b23a2498ad2bb7feef2f9cf94f652d70.png) +![](https://upload-images.jianshu.io/upload_images/4685968-ae3ef868da0913d0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 3.1 Split -![](https://img-blog.csdnimg.cn/img_convert/1f8b94642f966347e5fbc19579f3a8b5.png) +![](https://upload-images.jianshu.io/upload_images/4685968-d6bf69ad9f81e9e4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## 3.2 InputFormat # 4 MapReduce 1.x 架构 -![](https://img-blog.csdnimg.cn/img_convert/978682a09b5d5e6e4c849aade6714cb3.png) +![](https://upload-images.jianshu.io/upload_images/4685968-fb32aecae4a71f2f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -![](https://img-blog.csdnimg.cn/img_convert/4d8cba7a246ba45e895e6b0aac5826c2.png) -![](https://img-blog.csdnimg.cn/img_convert/1358b4bbd10d5ffcf4cf4f3f28d63b64.png) -![](https://img-blog.csdnimg.cn/img_convert/040342a516484c19a2ddac1c5216ed98.png) -![](https://img-blog.csdnimg.cn/img_convert/c643cc847b1bca3a9616f6f9b7f605f5.png) +![](https://upload-images.jianshu.io/upload_images/4685968-decb115b06993cc7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-a282262365c87c60.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-b232e0cc860fd46d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-f4c54d1443a85677.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 5 MapReduce 2.x 架构 -![](https://img-blog.csdnimg.cn/img_convert/b3760e5dff90acb65e3c7c0ae1c9f81e.png) +![](https://upload-images.jianshu.io/upload_images/4685968-263326493524cfda.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 6 Java 实现 wordCount -![](https://img-blog.csdnimg.cn/img_convert/db8dbeb277b8a8b4ed7bf9dd02f6993e.png) -![clean package](https://img-blog.csdnimg.cn/img_convert/60c4b7482beb8c6d2db82e1298cda8b3.png) -![上传到Hadoop服务器](https://img-blog.csdnimg.cn/img_convert/d75de8ab33fec0b89631c4a242d9b5ea.png) -![全路径没有问题](https://img-blog.csdnimg.cn/img_convert/d7cf025e655cd15ffeac59f2b8df3d84.png) -![](https://img-blog.csdnimg.cn/img_convert/dd9815d16b7b059ee5370081c8593f6b.png) +![](https://upload-images.jianshu.io/upload_images/4685968-b2f88b1b8ad3d584.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![clean package](https://upload-images.jianshu.io/upload_images/4685968-d81f900ba386685c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![上传到Hadoop服务器](https://upload-images.jianshu.io/upload_images/4685968-7fb51bf009c4e2db.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![全路径没有问题](https://upload-images.jianshu.io/upload_images/4685968-4c23190abdf6b39e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-8f625f9805d6e160.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 7 重构 -![](https://img-blog.csdnimg.cn/img_convert/bc92c2418c746f4a52013bc7a10b4ec4.png) +![](https://upload-images.jianshu.io/upload_images/4685968-e23904523132b5f9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 8 Combiner编程 -![](https://img-blog.csdnimg.cn/img_convert/ed77f8d1c4a79eb349e0b816e9b4d7b3.png) +![](https://upload-images.jianshu.io/upload_images/4685968-49b3ab702137d23e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 9 Partitoner -![](https://img-blog.csdnimg.cn/img_convert/95c49f4305150ef0987d6f6733b7ddaf.png) -![](https://img-blog.csdnimg.cn/img_convert/9cbb065c8bbba1da3972cbbe1eae4301.png) \ No newline at end of file +![](https://upload-images.jianshu.io/upload_images/4685968-bd17e7b57d287240.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-4572a64d4f8206b0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/Linux/\344\270\200\346\226\207\346\220\236\346\207\202select\343\200\201poll\345\222\214epoll\345\214\272\345\210\253.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/Linux/\344\270\200\346\226\207\346\220\236\346\207\202select\343\200\201poll\345\222\214epoll\345\214\272\345\210\253.md" deleted file mode 100644 index 743dcb131b..0000000000 --- "a/\346\223\215\344\275\234\347\263\273\347\273\237/Linux/\344\270\200\346\226\207\346\220\236\346\207\202select\343\200\201poll\345\222\214epoll\345\214\272\345\210\253.md" +++ /dev/null @@ -1,375 +0,0 @@ -# 1 select -select本质上是通过设置或检查存放fd标志位的数据结构进行下一步处理。 -这带来缺点: -- 单个进程可监视的fd数量被限制,即能监听端口的数量有限 -单个进程所能打开的最大连接数有`FD_SETSIZE`宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试 -一般该数和系统内存关系很大,具体数目可以`cat /proc/sys/fs/file-max`察看。32位机默认1024个,64位默认2048。 -![](https://img-blog.csdnimg.cn/20201103231954394.png#pic_center) - -- 对socket是线性扫描,即轮询,效率较低: -仅知道有I/O事件发生,却不知是哪几个流,只会无差异轮询所有流,找出能读数据或写数据的流进行操作。同时处理的流越多,无差别轮询时间越长 - O(n)。 - - -当socket较多时,每次select都要通过遍历`FD_SETSIZE`个socket,不管是否活跃,这会浪费很多CPU时间。如果能给 socket 注册某个回调函数,当他们活跃时,自动完成相关操作,即可避免轮询,这就是**epoll**与**kqueue**。 - -## 1.1 调用过程 -![](https://img-blog.csdnimg.cn/20201103234504233.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -```c -asmlinkage long sys_poll(struct pollfd * ufds, unsigned int nfds, long timeout) -{ - int i, j, fdcount, err; - struct pollfd **fds; - struct poll_wqueues table, *wait; - int nchunks, nleft; - - /* Do a sanity check on nfds ... */ - if (nfds > NR_OPEN) - return -EINVAL; - - if (timeout) { - /* Careful about overflow in the intermediate values */ - if ((unsigned long) timeout < MAX_SCHEDULE_TIMEOUT / HZ) - timeout = (unsigned long)(timeout*HZ+999)/1000+1; - else /* Negative or overflow */ - timeout = MAX_SCHEDULE_TIMEOUT; - } - // 2. 注册回调函数__pollwait - poll_initwait(&table); - wait = &table; - if (!timeout) - wait = NULL; - - err = -ENOMEM; - fds = NULL; - if (nfds != 0) { - fds = (struct pollfd **)kmalloc( - (1 + (nfds - 1) / POLLFD_PER_PAGE) * sizeof(struct pollfd *), - GFP_KERNEL); - if (fds == NULL) - goto out; - } - - nchunks = 0; - nleft = nfds; - while (nleft > POLLFD_PER_PAGE) { /* allocate complete PAGE_SIZE chunks */ - fds[nchunks] = (struct pollfd *)__get_free_page(GFP_KERNEL); - if (fds[nchunks] == NULL) - goto out_fds; - nchunks++; - nleft -= POLLFD_PER_PAGE; - } - if (nleft) { /* allocate last PAGE_SIZE chunk, only nleft elements used */ - fds[nchunks] = (struct pollfd *)__get_free_page(GFP_KERNEL); - if (fds[nchunks] == NULL) - goto out_fds; - } - - err = -EFAULT; - for (i=0; i < nchunks; i++) - // - if (copy_from_user(fds[i], ufds + i*POLLFD_PER_PAGE, PAGE_SIZE)) - goto out_fds1; - if (nleft) { - if (copy_from_user(fds[nchunks], ufds + nchunks*POLLFD_PER_PAGE, - nleft * sizeof(struct pollfd))) - goto out_fds1; - } - - fdcount = do_poll(nfds, nchunks, nleft, fds, wait, timeout); - - /* OK, now copy the revents fields back to user space. */ - for(i=0; i < nchunks; i++) - for (j=0; j < POLLFD_PER_PAGE; j++, ufds++) - __put_user((fds[i] + j)->revents, &ufds->revents); - if (nleft) - for (j=0; j < nleft; j++, ufds++) - __put_user((fds[nchunks] + j)->revents, &ufds->revents); - - err = fdcount; - if (!fdcount && signal_pending(current)) - err = -EINTR; - -out_fds1: - if (nleft) - free_page((unsigned long)(fds[nchunks])); -out_fds: - for (i=0; i < nchunks; i++) - free_page((unsigned long)(fds[i])); - if (nfds != 0) - kfree(fds); -out: - poll_freewait(&table); - return err; -} -``` -```c -static int do_poll(unsigned int nfds, unsigned int nchunks, unsigned int nleft, - struct pollfd *fds[], struct poll_wqueues *wait, long timeout) -{ - int count; - poll_table* pt = &wait->pt; - - for (;;) { - unsigned int i; - - set_current_state(TASK_INTERRUPTIBLE); - count = 0; - for (i=0; i < nchunks; i++) - do_pollfd(POLLFD_PER_PAGE, fds[i], &pt, &count); - if (nleft) - do_pollfd(nleft, fds[nchunks], &pt, &count); - pt = NULL; - if (count || !timeout || signal_pending(current)) - break; - count = wait->error; - if (count) - break; - timeout = schedule_timeout(timeout); - } - current->state = TASK_RUNNING; - return count; -} -``` - -1. 使用copy_from_user从用户空间拷贝fd_set到内核空间 -2. 注册回调函数`__pollwait` -![](https://img-blog.csdnimg.cn/908c942c09594f048dc2bebc05608ff9.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -3. 遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或datagram_poll) -4. 以tcp_poll为例,核心实现就是`__pollwait`,即上面注册的回调函数 -5. `__pollwait`,就是把current(当前进程)挂到设备的等待队列,不同设备有不同等待队列,如tcp_poll的等待队列是sk->sk_sleep(把进程挂到等待队列中并不代表进程已睡眠)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒。 -```c -void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *_p) -{ - struct poll_wqueues *p = container_of(_p, struct poll_wqueues, pt); - struct poll_table_page *table = p->table; - - if (!table || POLL_TABLE_FULL(table)) { - struct poll_table_page *new_table; - - new_table = (struct poll_table_page *) __get_free_page(GFP_KERNEL); - if (!new_table) { - p->error = -ENOMEM; - __set_current_state(TASK_RUNNING); - return; - } - new_table->entry = new_table->entries; - new_table->next = table; - p->table = new_table; - table = new_table; - } - - /* 添加新节点 */ - { - struct poll_table_entry * entry = table->entry; - table->entry = entry+1; - get_file(filp); - entry->filp = filp; - entry->wait_address = wait_address; - init_waitqueue_entry(&entry->wait, current); - add_wait_queue(wait_address,&entry->wait); - } -} -``` -```c -static void do_pollfd(unsigned int num, struct pollfd * fdpage, - poll_table ** pwait, int *count) -{ - int i; - - for (i = 0; i < num; i++) { - int fd; - unsigned int mask; - struct pollfd *fdp; - - mask = 0; - fdp = fdpage+i; - fd = fdp->fd; - if (fd >= 0) { - struct file * file = fget(fd); - mask = POLLNVAL; - if (file != NULL) { - mask = DEFAULT_POLLMASK; - if (file->f_op && file->f_op->poll) - mask = file->f_op->poll(file, *pwait); - mask &= fdp->events | POLLERR | POLLHUP; - fput(file); - } - if (mask) { - *pwait = NULL; - (*count)++; - } - } - fdp->revents = mask; - } -} -``` -6. poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值 -7. 若遍历完所有fd,还没返回一个可读写的mask掩码,则调schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。若超过一定超时时间(schedule_timeout指定),还没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有无就绪的fd -8. 把fd_set从内核空间拷贝到用户空间 -## 1.2 缺点 -内核需要将消息传递到用户空间,都需要内核拷贝动作。需要维护一个用来存放大量fd的数据结构,使得用户空间和内核空间在传递该结构时复制开销大。 - -- 每次调用select,都需把fd集合从用户态拷贝到内核态,fd很多时开销就很大 -- 同时每次调用select都需在内核遍历传递进来的所有fd,fd很多时开销就很大 -- select支持的文件描述符数量太小了,默认最大支持1024个 -- 主动轮询效率很低 -# 2 poll -和select类似,只是描述fd集合的方式不同,poll使用`pollfd`结构而非select的`fd_set`结构。 -管理多个描述符也是进行轮询,根据描述符的状态进行处理,但**poll没有最大文件描述符数量的限制**。 - -poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。 -- 它将用户传入的数组拷贝到内核空间 -- 然后查询每个fd对应的设备状态: - - 如果设备就绪 -在设备等待队列中加入一项继续遍历 - - 若遍历完所有fd后,都没发现就绪的设备 -挂起当前进程,直到设备就绪或主动超时,被唤醒后它又再次遍历fd。这个过程经历多次无意义的遍历。 - -没有最大连接数限制,因其基于链表存储,其缺点: -- 大量fd数组被整体复制于用户态和内核地址空间间,而不管是否有意义 -- 如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd - -所以又有了epoll模型。 -# 3 epoll -epoll模型修改主动轮询为被动通知,当有事件发生时,被动接收通知。所以epoll模型注册套接字后,主程序可做其他事情,当事件发生时,接收到通知后再去处理。 - -可理解为**event poll**,epoll会把哪个流发生哪种I/O事件通知我们。所以epoll是事件驱动(每个事件关联fd),此时我们对这些流的操作都是有意义的。复杂度也降到O(1)。 -```c -asmlinkage int sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) -{ - int error; - struct file *file, *tfile; - struct eventpoll *ep; - struct epitem *epi; - struct epoll_event epds; - - error = -EFAULT; - if (copy_from_user(&epds, event, sizeof(struct epoll_event))) - goto eexit_1; - - /* Get the "struct file *" for the eventpoll file */ - error = -EBADF; - file = fget(epfd); - if (!file) - goto eexit_1; - - /* Get the "struct file *" for the target file */ - tfile = fget(fd); - if (!tfile) - goto eexit_2; - - /* The target file descriptor must support poll */ - error = -EPERM; - if (!tfile->f_op || !tfile->f_op->poll) - goto eexit_3; - - /* - * We have to check that the file structure underneath the file descriptor - * the user passed to us _is_ an eventpoll file. And also we do not permit - * adding an epoll file descriptor inside itself. - */ - error = -EINVAL; - if (file == tfile || !IS_FILE_EPOLL(file)) - goto eexit_3; - - /* - * At this point it is safe to assume that the "private_data" contains - * our own data structure. - */ - ep = file->private_data; - - /* - * Try to lookup the file inside our hash table. When an item is found - * ep_find() increases the usage count of the item so that it won't - * desappear underneath us. The only thing that might happen, if someone - * tries very hard, is a double insertion of the same file descriptor. - * This does not rapresent a problem though and we don't really want - * to put an extra syncronization object to deal with this harmless condition. - */ - epi = ep_find(ep, tfile); - - error = -EINVAL; - switch (op) { - case EPOLL_CTL_ADD: - if (!epi) { - epds.events |= POLLERR | POLLHUP; - - error = ep_insert(ep, &epds, tfile); - } else - error = -EEXIST; - break; - case EPOLL_CTL_DEL: - if (epi) - error = ep_remove(ep, epi); - else - error = -ENOENT; - break; - case EPOLL_CTL_MOD: - if (epi) { - epds.events |= POLLERR | POLLHUP; - error = ep_modify(ep, epi, &epds); - } else - error = -ENOENT; - break; - } - - /* - * The function ep_find() increments the usage count of the structure - * so, if this is not NULL, we need to release it. - */ - if (epi) - ep_release_epitem(epi); - -eexit_3: - fput(tfile); -eexit_2: - fput(file); -eexit_1: - DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_ctl(%d, %d, %d, %u) = %d\n", - current, epfd, op, fd, event->events, error)); - - return error; -} - -``` - -## 3.1 触发模式 -**EPOLLLT**和**EPOLLET**两种: - -- LT,默认的模式(水平触发) -只要该fd还有数据可读,每次 `epoll_wait` 都会返回它的事件,提醒用户程序去操作, -- ET是“高速”模式(边缘触发) -![](https://img-blog.csdnimg.cn/20201103232957391.png#pic_center) -只会提示一次,直到下次再有数据流入之前都不会再提示,无论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读完,即读到read返回值小于请求值或遇到EAGAIN错误 - -epoll使用“事件”的就绪通知方式,通过`epoll_ctl`注册fd,一旦该fd就绪,内核就会采用类似回调机制激活该fd,`epoll_wait`便可收到通知。 -### EPOLLET触发模式的意义 -若用`EPOLLLT`,系统中一旦有大量无需读写的就绪文件描述符,它们每次调用`epoll_wait`都会返回,这大大降低处理程序检索自己关心的就绪文件描述符的效率。 -而采用`EPOLLET`,当被监控的文件描述符上有可读写事件发生时,`epoll_wait`会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用`epoll_wait`时,它不会通知你,即只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你。这比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。 - -## 3.2 优点 -- 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口) -- 效率提升,不是轮询,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数 -即Epoll最大的优点就在于它只关心“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll -- 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。 -- epoll通过内核和用户空间共享一块内存来实现的 - -表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。 - -epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现。 - -select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。 -- 对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。 -- 对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。 -- 对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。 -# 4 总结 -select,poll,epoll都是IO多路复用机制,即可以监视多个描述符,一旦某个描述符就绪(读或写就绪),能够通知程序进行相应读写操作。 -但select,poll,epoll本质上都是**同步I/O**,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。 - -- select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。 - -- select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。 - -> 参考 -> - Linux下select/poll/epoll机制的比较 -> - https://www.cnblogs.com/anker/p/3265058.html \ No newline at end of file diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\346\214\207\344\273\244\345\222\214\350\277\220\347\256\227\346\230\257\345\246\202\344\275\225\345\215\217\344\275\234\346\236\204\346\210\220CPU\347\232\204\357\274\237.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\346\214\207\344\273\244\345\222\214\350\277\220\347\256\227\346\230\257\345\246\202\344\275\225\345\215\217\344\275\234\346\236\204\346\210\220CPU\347\232\204\357\274\237.md" deleted file mode 100644 index 5b4462873b..0000000000 --- "a/\346\223\215\344\275\234\347\263\273\347\273\237/\350\256\241\347\256\227\346\234\272\347\273\204\346\210\220\345\216\237\347\220\206/\346\214\207\344\273\244\345\222\214\350\277\220\347\256\227\346\230\257\345\246\202\344\275\225\345\215\217\344\275\234\346\236\204\346\210\220CPU\347\232\204\357\274\237.md" +++ /dev/null @@ -1,85 +0,0 @@ -连通“指令”和“计算”这两大功能,才能构建完整的CPU。 -# 1 指令周期(Instruction Cycle) -计算机每执行一条指令的过程,可分解为如下步骤: -1. Fetch(取指令) -指令放在存储器,通过PC寄存器和指令寄存器取出指令的过程,由控制器(Control Unit)操作。 -从PC寄存器找到对应指令地址,据指令地址从内存把具体指令加载到指令寄存器,然后PC寄存器自增 -2. Decode(指令译码) -据指令寄存器里面的指令,解析成要进行何操作,是R、I、J中的哪一种指令,具体要操作哪些寄存器、数据或内存地址。该阶段也是由控制器执行。 -3. Execute(执行指令) -实际运行对应的R、I、J这些特定的指令,进行算术逻辑操作、数据传输或者直接的地址跳转。无论是算术操作、逻辑操作的R型指令,还是数据传输、条件分支的I型指令,都由算术逻辑单元(ALU)操作,即由运算器处理。 -如果是一个简单的无条件地址跳转,那可直接在控制器里完成,无需运算器。 -4. 重复1~3 - -这就是个永动机般的“FDE”循环,即指令周期。 -![](https://img-blog.csdnimg.cn/171cff94c628488b823513f4e38973eb.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -CPU还有两个Cycle: -## Machine Cycle,机器周期或者CPU周期 -CPU内部操作速度很快,但访问内存速度却慢很多。 -每条指令都需要从内存里面加载而来,所以一般把从内存里面读取一条指令的最短时间,称为CPU周期。 -## Clock Cycle,时钟周期及机器主频 -一个CPU周期,通常由几个时钟周期累积。一个CPU周期时间,就是这几个Clock Cycle总和。 - -对于一个指令周期,取出一条指令,然后执行它,至少需两个CPU周期: -- 取出指令,至少得一个CPU周期 -- 执行指令,至少也得一个CPU周期 -因为执行完的结果,还要写回内存 -## 三个周期(Cycle)之间的关系 -![](https://img-blog.csdnimg.cn/9d86b21db1494646a5fcde3bd86fb75d.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -一个指令周期,包含多个CPU周期,而一个CPU周期包含多个时钟周期。 -# 2 建立数据通路 -名字是什么其实并不重要,一般可以认为,数据通路就是我们的处理器单元,通常由两类原件组成: -- 操作元件,也叫组合逻辑元件(Combinational Element),就是ALU -在特定的输入下,根据下面的组合电路的逻辑,生成特定的输出。 -- 存储元件,也叫状态元件(State Element) -如在计算过程中要用到的寄存器,无论是通用寄存器还是状态寄存器,都是存储元件。 - -通过数据总线把它们连接起来,就可完成数据存储、处理和传输,即建立了数据通路。 -## 控制器 -可以把它看成只是机械地重复“Fetch - Decode - Execute“循环中的前两个步骤,然后把最后一个步骤,通过控制器产生的控制信号,交给ALU去处理。 -### 控制器将CPU指令解析成不同输出信号 -目前Intel CPU支持2000个以上指令。说明控制器输出的控制信号,至少有2000种不同组合。 - -运算器里的ALU和各种组合逻辑电路,可认为是一个**固定功能的电路**。 -控制器“翻译”出来的,就是不同控制信号,告诉ALU去做不同计算。正是控制器,才让我们能“编程”实现功能,才铸就了“存储程序型计算机”。 -- 指令译码器将输入的机器码,解析成不同操作码、操作数,然后传输给ALU计算 -![](https://img-blog.csdnimg.cn/cb0596de354c42baad326015b7a6f151.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -# 3 CPU对硬件电路的要求 -搭建CPU,还得在数字电路层面,实现如下功能。 -## ALU -就是个无状态,根据输入计算输出结果的第一个电路。 -## 支持状态读写的电路元件 - 寄存器 -要有电路能存储上次计算结果。 - -该计算结果不一定要立刻给下游电路使用,但可在需要时直接用。常见支持状态读写的电路: -- 锁存器(Latch) -- D触发器(Data/Delay Flip-flop)电路 -## 自动”电路,按固定周期实现PC寄存器自增 -自动执行Fetch - Decode - Execute。 - -我们的程序执行,并非靠人工拨动开关执行指令。得有个“自动”电路,无休止执行一条条指令。 - -看似复杂的各种函数调用、条件跳转,只是修改了PC寄存器保存的地址。PC寄存器里面的地址一修改,计算机即可加载一条指令新指令,往下运行。 -PC寄存器还叫程序计数器,随时间变化,不断计数。数字变大了,就去执行一条新指令。所以,我们需要的就是个自动计数的电路。 -## 译码电路 -无论是decode指令,还是对于拿到的内存地址去获取对应的数据或者指令,都要通过一个电路找到对应数据,就是“译码器”电路。 - -把这四类电路,通过各种方式组合在一起就能组成CPU。要实现这四种电路中的中间两种,我们还需要时钟电路的配合。下一节,我们一起来看一看,这些基础的电路功能是怎么实现的,以及怎么把这些电路组合起来变成一个CPU。 -# 总结 -至此,CPU运转所需的数据通路和控制器介绍完了,也找出完成这些功能,需要的4种基本电路: -- ALU这样的组合逻辑电路 -- 存储数据的锁存器和D触发器电路 -- 实现PC寄存器的计数器电路 -- 解码和寻址的译码器电路 - -> CPU 好像一个永不停歇的机器,一直在不停地读取下一条指令去运行。那为什么 CPU 还会有满载运行和 Idle 闲置的状态呢? - -操作系统内核有 idle 进程,优先级最低,仅当其他进程都阻塞时被调度器选中。idle 进程循环执行 HLT 指令,关闭 CPU 大部分功能以降低功耗,收到中断信号时 CPU 恢复正常状态。 CPU在空闲状态就会停止执行,即切断时钟信号,CPU主频会瞬间降低为0,功耗也会瞬间降为0。由于这个空闲状态是十分短暂的,所以你在任务管理器也只会看到CPU频率下降,不会看到降为0。 当CPU从空闲状态中恢复时,就会接通时钟信号,CPU频率就会上升。所以你会在任务管理器里面看到CPU的频率起伏变化。 - -uptime 命令查看平均负载 -![](https://img-blog.csdnimg.cn/ebdc106681da4dba974a91c1147092a5.png) -满载运行就是平均负载为1.0(一个一核心CPU),定义为特定时间间隔内运行队列中的平均线程数。 -load average 表示机器一段时间内的平均load,越低越好。过高可能导致机器无法处理其他请求及操作,甚至死机。 - -当CUP执行完当前系统分配的任务,为省电,系统将执行空闲任务(idle task),该任务循环执行HLT指令,CPU就会停止指令的执行,且让CPU处于HALT状态,CPU虽停止指令执行,且CPU部分功能模块将会被关闭(以低功耗),但CPU的LAPIC(Local Advanced Programmable Interrupt Controller)并不会停止工作,即CPU将会继续接收外部中断、异常等外部事件(事实上,CPU HALT状态的退出将由外部事件触发)。“Idle 闲置”是一种低功耗的状态,cpu在执行最低功耗的循环指令。实际上并非啥都没干,而是一直在干最最轻松的事儿。 -当CPU接收到这些外部事件,将会从HALT状态恢复,执行中断服务函数,且当中断服务函数执行完毕后,指令寄存器(CS:EIP)将会指向HLT指令的下一条指令,即CPU继续执行HLT指令之后的程序。 \ No newline at end of file diff --git "a/\346\223\215\344\275\234\347\263\273\347\273\237/\351\233\266\346\213\267\350\264\235\357\274\210Zero Copy\357\274\211\346\212\200\346\234\257\345\210\260\345\272\225\346\230\257\344\273\200\344\271\210\357\274\237.md" "b/\346\223\215\344\275\234\347\263\273\347\273\237/\351\233\266\346\213\267\350\264\235\357\274\210Zero Copy\357\274\211\346\212\200\346\234\257\345\210\260\345\272\225\346\230\257\344\273\200\344\271\210\357\274\237.md" deleted file mode 100644 index ba67fb6257..0000000000 --- "a/\346\223\215\344\275\234\347\263\273\347\273\237/\351\233\266\346\213\267\350\264\235\357\274\210Zero Copy\357\274\211\346\212\200\346\234\257\345\210\260\345\272\225\346\230\257\344\273\200\344\271\210\357\274\237.md" +++ /dev/null @@ -1,25 +0,0 @@ -rabbitmq 这么高吞吐量都是因为零拷贝技术,本文以 kafka 为例讲解。 - -消息从发送到落地保存,broker 维护的消息日志本身就是文件目录,每个文件都是二进制保存,生产者和消费者使用相同格式来处理。 - -Con获取消息时,服务器先从硬盘读取数据到内存,然后将内存中数据通过 socket 发给Con。 - -Linux的零拷贝技术:当数据在磁盘和网络之间传输时,避免昂贵的内核态数据拷贝,从而实现快速数据传输。 -Linux平台实现了这样的零拷贝机制,但Windows必须要到Java 8的60更新版本才能“享受”到。 -![](https://img-blog.csdnimg.cn/img_convert/74e0931f6470ebdcadd5ed9fbd8897f2.png) -- os将数据从磁盘读入到内核空间的页缓存 -- 应用程序将数据从内核空间读入到用户空间缓存 -- 应用程序将数据写回到内核空间到 socket 缓存 -- os将数据从 socket 缓冲区复制到网卡缓冲区,以便将数据经网络发出 - -该过程涉及: -- 4 次上下文切换 -- 4 次数据复制 -有两次复制操作是 CPU 完成的。但该过程中,数据完全无变化,仅是从磁盘复制到网卡缓冲区。 - -而通过“零拷贝”技术,能去掉这些没必要的数据复制操作, 也减少了上下文切换次数。 -现代的 unix 操作系统提供一个优化的代码路径,将数据从页缓存传输到 socket; -在 Linux 中,是通过 sendfile 系统调用完成的。Java 提供了访问这个系统调用的方法:**FileChannel.transferTo** API -![](https://img-blog.csdnimg.cn/img_convert/633eeab1746ea396f2d67bab8fb3fbc2.png) -使用 sendfile,只需一次拷贝,允许os将数据直接从页缓存发送到网络。 -所以在这个优化的路径中, 只有最后一步:将数据拷贝到网卡缓存中是必须的。 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(7)-\344\274\230\351\233\205\345\234\260\351\203\250\347\275\262 Kafka \351\233\206\347\276\244.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(7)-\344\274\230\351\233\205\345\234\260\351\203\250\347\275\262 Kafka \351\233\206\347\276\244.md" deleted file mode 100644 index e1f5682e02..0000000000 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(7)-\344\274\230\351\233\205\345\234\260\351\203\250\347\275\262 Kafka \351\233\206\347\276\244.md" +++ /dev/null @@ -1,109 +0,0 @@ -生产环境需考量各种因素,结合自身业务需求而制定。看一些考虑因素(以下顺序,可是分了顺序的哦) -# 1 OS -- Kafka不是JVM上的中间件吗?Java又是跨平台语言,把Kafka安装到不同的os有啥区别吗? -区别相当大! - -Kafka的确由Scala/Java编写,编译后源码就是“.class”文件。部署到啥OS应该一样,但毋庸置疑,部署在Linux上的生产环境是最多的,具体原因你能谈笑风生吗? -## 1.1 I/O模型 -I/O模型其实就是os执行I/O指令的方法,主流的I/O模型通常有5种类型: -1. 阻塞式I/O -e.g. Java中Socket的阻塞模式 -2. 非阻塞式I/O -e.g. Java中Socket的非阻塞模式 -3. I/O多路复用 -e.g. Linux中的系统调用`select`函数 -4. 信号驱动I/O -e.g. epoll系统调用则介于第三种和第四种模型之间 -5. 异步I/O -e.g. 很少有Linux支持,反而Windows系统提供了一个叫IOCP线程模型属于该类 - -I/O模型与Kafka的关系几何?Kafka Client 底层使用了Java的selector,而selector -- 在Linux上的实现机制是epoll -- 在Windows平台上的实现机制是select - -因为这点,Kafka部署在Linux上更有优势,能获得更高效的I/O性能。 -## 1.2 数据网络传输效率 -- Kafka生产和消费的消息都是通过网络传输,但消息保存在哪呢? -肯定是磁盘! - -故Kafka需在磁盘和网络间进行大量数据传输。在Linux部署Kafka能够享受到零拷贝技术带来的快速数据传输特性。 -## 1.3 社区生态 -社区对Windows平台上发现的Kafka Bug不做任何承诺。 -# 2 磁盘 -## 2.1 机械硬盘 or SSD -- 前者便宜且容量大,但易坏! -- 后者性能优势大,但是贵! - -建议是使用普通机械硬盘即可。 -- Kafka虽然大量使用磁盘,可多是顺序读写操作,一定程度规避了机械磁盘最大的劣势,即随机读写慢。从这一点上来说,使用SSD并没有太大性能优势,机械磁盘物美价廉 -- 而它因易损坏而造成的可靠性差等缺陷,又由Kafka在软件层面提供机制来保证 -## 2.2 是否应该使用磁盘阵列(RAID) -使用RAID的主要优势: -- 提供冗余的磁盘存储空间 -- 提供负载均衡 - -对于Kafka -- Kafka自己实现了冗余机制,提供高可靠性 -- 通过分区设计,也能在软件层面自行实现负载均衡 - -RAID优势也就没有那么明显了。虽然实际上依然有很多大厂确实是把Kafka底层的存储交由RAID,只是目前Kafka在存储这方面提供了越来越便捷的高可靠性方案,因此在线上环境使用RAID似乎变得不是那么重要了。 - -综上,追求性价比的公司可不搭建RAID,使用普通磁盘组成存储空间即可。使用机械磁盘完全能够胜任Kafka线上环境。 -## 2.3 磁盘容量 -集群到底需要多大? -Kafka需要将消息保存在磁盘上,这些消息默认会被保存一段时间然后自动被删除。 -虽然这段时间是可以配置的,但你应该如何结合自身业务场景和存储需求来规划Kafka集群的存储容量呢? - -假设有个业务 -- 每天需要向Kafka集群发送1亿条消息 -- 每条消息保存两份以防止数据丢失 -- 消息默认保存两周时间 - -现在假设消息的平均大小是1KB,那么你能说出你的Kafka集群需要为这个业务预留多少磁盘空间吗? - -计算: -- 每天1亿条1KB的消息,存两份 -`1亿 * 1KB * 2 / 1000 / 1000 = 200GB` - -- 一般Kafka集群除消息数据还存其他类型数据,比如索引数据 -再为其预留10%磁盘空间,因此总的存储容量就是220GB - -- 要存两周,那么整体容量即为 -220GB * 14,大约3TB -- Kafka支持数据的压缩,假设压缩比是0.75 -那么最后规划的存储空间就是0.75 * 3 = 2.25TB - -总之在规划磁盘容量时你需要考虑下面这几个元素: -- 新增消息数 -- 消息留存时间 -- 平均消息大小 -- 备份数 -- 是否启用压缩 -# 3 带宽 -对于Kafka这种通过网络进行大数据传输的框架,带宽易成为瓶颈。 - -普通以太网络,带宽主要有两种: -- 1Gbps的千兆网络 -- 10Gbps的万兆网络 - -以千兆网络为例,说明带宽资源规划。真正要规划的是所需的Kafka服务器的数量。假设机房环境是千兆网络,即1Gbps,现在有业务,其目标或SLA是在1小时内处理1TB的业务数据。 - -到底需要多少台Kafka服务器来完成这个业务呢? -### 计算 -带宽1Gbps,即每秒处理1Gb数据 -假设每台Kafka服务器都是安装在专属机器,即每台Kafka机器上没有混入其他服务 -通常情况下你只能假设Kafka会用到70%的带宽资源,因为总要为其他应用或进程留一些资源。超过70%的阈值就有网络丢包可能性,故70%的设定是一个比较合理的值,也就是说单台Kafka服务器最多也就能使用大约700Mb带宽。 - -这只是它能使用的最大带宽资源,你不能让Kafka服务器常规性使用这么多资源,故通常要再额外预留出2/3的资源,即 -`单台服务器使用带宽700Mb / 3 ≈ 240Mbps` -这里的2/3其实是相当保守的,可以结合机器使用情况酌情减少该值 - -有了240Mbps,可以计算1小时内处理1TB数据所需的服务器数量了。 -根据这个目标,每秒需要处理2336Mb的数据,除以240,约等于10台服务器。 -如果消息还需要额外复制两份,那么总的服务器台数还要乘以3,即30台。 -# 总结 -部署Kafka环境,一开始就要思考好实际场景下业务所需的集群环境,不能仅从单个维度上进行评估。 - -> 参考 -> - Linux内核模型架构 -> - Kafka核心技术与实战 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(\344\270\203) - \344\274\230\351\233\205\345\234\260\351\203\250\347\275\262 Kafka \351\233\206\347\276\244.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(\344\270\203) - \344\274\230\351\233\205\345\234\260\351\203\250\347\275\262 Kafka \351\233\206\347\276\244.md" new file mode 100644 index 0000000000..7d79f7f122 --- /dev/null +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(\344\270\203) - \344\274\230\351\233\205\345\234\260\351\203\250\347\275\262 Kafka \351\233\206\347\276\244.md" @@ -0,0 +1,127 @@ +既然是集群,必然有多个Kafka节点,只有单节点构成的Kafka伪集群只能用于日常测试,不可能满足线上生产需求。 +真正的线上环境需要考量各种因素,结合自身的业务需求而制定。看一些考虑因素(以下顺序,可是分了顺序的哦) + +# 1 操作系统 - OS +可能你会问Kafka不是JVM上的大数据框架吗?Java又是跨平台的语言,把Kafka安装到不同的操作系统上会有什么区别吗? +区别相当大! + +确实,Kafka由Scala/Java编写,编译后源码就是“.class”文件。 +本来部署到哪个OS应该一样,但是不同OS的差异还是给Kafka集群带来了相当大的影响。 +毋庸置疑,部署在Linux上的生产环境是最多的。 + +考虑操作系统与Kafka的适配性,Linux系统显然要比其他两个特别是Windows系统更加适合部署Kafka。可具体原因你能谈笑风生吗? +## 1.1 I/O模型 +I/O模型可以近似认为I/O模型就是OS执行I/O指令的方法。 +主流的I/O模型通常有5种类型: +1. 阻塞式I/O +e.g. Java中Socket的阻塞模式 +2. 非阻塞式I/O +e.g. Java中Socket的非阻塞模式 +3. I/O多路复用 +e.g. Linux中的系统调用`select`函数 +4. 信号驱动I/O +e.g. epoll系统调用则介于第三种和第四种模型之间 +5. 异步I/O +e.g. 很少有Linux支持,反而Windows系统提供了一个叫IOCP线程模型属于该类 + +我在这里不详细展开每一种模型的实现细节,因为那不是本文重点。 + +言归正传,I/O模型与Kafka的关系几何? +Kafka Client 底层使用了Java的selector,而selector +- 在Linux上的实现机制是epoll +- 在Windows平台上的实现机制是select + +因此在这一点上将Kafka部署在Linux上是有优势的,能够获得更高效的I/O性能。 +## 1.2 数据网络传输效率 +Kafka生产和消费的消息都是通过网络传输的,而消息保存在哪里呢? +肯定是磁盘! +故Kafka需要在磁盘和网络间进行大量数据传输。 +Linux有个零拷贝(Zero Copy)技术,就是当数据在磁盘和网络进行传输时避免昂贵内核态数据拷贝从而实现快速数据传输。Linux平台实现了这样的零拷贝机制,但有些令人遗憾的是在Windows平台上必须要等到Java 8的60更新版本才能“享受”到。 + +一句话,在Linux部署Kafka能够享受到零拷贝技术所带来的快速数据传输特性带来的极致快感。 + +## 1.3 社区生态 +社区目前对Windows平台上发现的Kafka Bug不做任何承诺。因此,Windows平台上部署Kafka只适合于个人测试或用于功能验证,千万不要应用于生产环境。 + +# 2 磁盘 +## 2.1 灵魂拷问:机械硬盘 or 固态硬盘 +- 前者便宜且容量大,但易坏! +- 后者性能优势大,但是贵! + + +建议是使用普通机械硬盘即可。 +- Kafka虽然大量使用磁盘,可多是顺序读写操作,一定程度上规避了机械磁盘最大的劣势,即随机读写慢。从这一点上来说,使用SSD并没有太大性能优势,机械磁盘物美价廉 +- 而它因易损坏而造成的可靠性差等缺陷,又由Kafka在软件层面提供机制来保证 + + +## 2.2 是否应该使用磁盘阵列(RAID) +使用RAID的两个主要优势在于: +- 提供冗余的磁盘存储空间 +- 提供负载均衡 + +不过就Kafka而言 +- Kafka自己实现了冗余机制提供高可靠性 +- 通过分区的设计,也能在软件层面自行实现负载均衡 + +如此说来RAID的优势也就没有那么明显了。虽然实际上依然有很多大厂确实是把Kafka底层的存储交由RAID的,只是目前Kafka在存储这方面提供了越来越便捷的高可靠性方案,因此在线上环境使用RAID似乎变得不是那么重要了。 +综上,追求性价比的公司可以不搭建RAID,使用普通磁盘组成存储空间即可。使用机械磁盘完全能够胜任Kafka线上环境。 + +## 2.3 磁盘容量 +集群到底需要多大? +Kafka需要将消息保存在磁盘上,这些消息默认会被保存一段时间然后自动被删除。 +虽然这段时间是可以配置的,但你应该如何结合自身业务场景和存储需求来规划Kafka集群的存储容量呢? + +假设有个业务 +- 每天需要向Kafka集群发送1亿条消息 +- 每条消息保存两份以防止数据丢失 +- 消息默认保存两周时间 + +现在假设消息的平均大小是1KB,那么你能说出你的Kafka集群需要为这个业务预留多少磁盘空间吗? + +计算: +- 每天1亿条1KB的消息,存两份 +`1亿 * 1KB * 2 / 1000 / 1000 = 200GB` + +- 一般Kafka集群除消息数据还存其他类型数据,比如索引数据 +再为其预留10%磁盘空间,因此总的存储容量就是220GB + +- 要存两周,那么整体容量即为 +220GB * 14,大约3TB +- Kafka支持数据的压缩,假设压缩比是0.75 +那么最后规划的存储空间就是0.75 * 3 = 2.25TB + +总之在规划磁盘容量时你需要考虑下面这几个元素: +- 新增消息数 +- 消息留存时间 +- 平均消息大小 +- 备份数 +- 是否启用压缩 + +# 3 带宽 +对于Kafka这种通过网络进行大数据传输的框架,带宽容易成为瓶颈。 +普通的以太网络,带宽主要有两种:1Gbps的千兆网络和10Gbps的万兆网络,特别是千兆网络应该是一般公司网络的标准配置了 +以千兆网络为例,说明带宽资源规划。 + +真正要规划的是所需的Kafka服务器的数量。 +假设机房环境是千兆网络,即1Gbps,现在有业务,其目标或SLA是在1小时内处理1TB的业务数据。 +那么问题来了,你到底需要多少台Kafka服务器来完成这个业务呢? + +### 计算 +带宽1Gbps,即每秒处理1Gb数据 +假设每台Kafka服务器都是安装在专属机器,即每台Kafka机器上没有混入其他服务 +通常情况下你只能假设Kafka会用到70%的带宽资源,因为总要为其他应用或进程留一些资源。超过70%的阈值就有网络丢包可能性,故70%的设定是一个比较合理的值,也就是说单台Kafka服务器最多也就能使用大约700Mb带宽。 + +这只是它能使用的最大带宽资源,你不能让Kafka服务器常规性使用这么多资源,故通常要再额外预留出2/3的资源,即 +`单台服务器使用带宽700Mb / 3 ≈ 240Mbps` +这里的2/3其实是相当保守的,可以结合机器使用情况酌情减少该值 + +有了240Mbps,可以计算1小时内处理1TB数据所需的服务器数量了。 +根据这个目标,每秒需要处理2336Mb的数据,除以240,约等于10台服务器。 +如果消息还需要额外复制两份,那么总的服务器台数还要乘以3,即30台。 + +# 总结 +与其盲目上马一套Kafka环境然后事后费力调整,不如在一开始就思考好实际场景下业务所需的集群环境。在考量部署方案时需要通盘考虑,不能仅从单个维度上进行评估。 + +# 参考 +- Linux内核模型架构 +- Kafka核心技术与实战 diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(4) -Kafka\351\227\250\346\264\276\347\237\245\345\244\232\345\260\221.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(\345\233\233) -Kafka\351\227\250\346\264\276\347\237\245\345\244\232\345\260\221.md" similarity index 100% rename from "\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(4) -Kafka\351\227\250\346\264\276\347\237\245\345\244\232\345\260\221.md" rename to "\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\345\256\236\346\210\230/Kafka\345\256\236\346\210\230(\345\233\233) -Kafka\351\227\250\346\264\276\347\237\245\345\244\232\345\260\221.md" diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\351\253\230\346\200\247\350\203\275\345\216\237\347\220\206\345\210\206\346\236\220.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\351\253\230\346\200\247\350\203\275\345\216\237\347\220\206\345\210\206\346\236\220.md" index 2c90d94a66..65c0b5f134 100644 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\351\253\230\346\200\247\350\203\275\345\216\237\347\220\206\345\210\206\346\236\220.md" +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/Kafka/Kafka\351\253\230\346\200\247\350\203\275\345\216\237\347\220\206\345\210\206\346\236\220.md" @@ -214,5 +214,25 @@ Kafka 中存储的一般都是海量的消息数据,为了避免日志文件 ## 4.3 消息写入的性能 我们现在大部分企业仍然用的是机械结构的磁盘,如果把消息以随机的方式写入到磁盘,那么磁盘首先要做的就是寻址,也就是定位到数据所在的物理地址,在磁盘上就要找到对应的柱面、磁头以及对应的扇区;这个过程相对内 存来说会消耗大量时间,为了规避随机读写带来的时间消耗,kafka 采用顺序写的方式存储数据。 -即使是这样,但是频繁的 I/O 操作仍然会造成磁盘的性能瓶颈,所以 kafka 还有一个性能策略。 +即使是这样,但是频繁的 I/O 操作仍然会造成磁盘的性能瓶颈,所以 kafka 还有一个性能策略 + +## 4.4 零拷贝 +消息从发送到落地保存,broker 维护的消息日志本身就是文件目录,每个文件都是二进制保存,生产者和消费者使用相同的格式来处理。在消费者获取消息时,服务器先从硬盘读取数据到内存,然后把内存中的数据原封不动的通 过 socket 发送给消费者。 + +虽然这个操作描述起来很简单, 但实际上经历了很多步骤 +![](https://uploadfiles.nowcoder.com/files/20190425/5088755_1556181389694_16782311-d53f60c9f4f81137.png) + +▪ 操作系统将数据从磁盘读入到内核空间的页缓存 +▪ 应用程序将数据从内核空间读入到用户空间缓存中 +▪ 应用程序将数据写回到内核空间到 socket 缓存中 +▪ 操作系统将数据从 socket 缓冲区复制到网卡缓冲区,以便将数据经网络发出 + +这个过程涉及到 4 次上下文切换以及 4 次数据复制,并且有两次复制操作是由 CPU 完成。但是这个过程中,数据完全没有进行变化,仅仅是从磁盘复制到网卡缓冲区。 + +通过“零拷贝”技术,可以去掉这些没必要的数据复制操作, 同时也会减少上下文切换次数。现代的 unix 操作系统提供 一个优化的代码路径,用于将数据从页缓存传输到 socket; +在 Linux 中,是通过 sendfile 系统调用来完成的。 +Java 提供了访问这个系统调用的方法:FileChannel.transferTo API +![](https://uploadfiles.nowcoder.com/files/20190425/5088755_1556181389536_16782311-4bbdb21c5f83a6e9.png) + +使用 sendfile,只需要一次拷贝就行,允许操作系统将数据直接从页缓存发送到网络上。所以在这个优化的路径中, 只有最后一步将数据拷贝到网卡缓存中是需要的。 diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\344\270\273\344\273\216\345\244\215\345\210\266\345\216\237\347\220\206\345\217\212\350\277\207\346\234\237key\345\244\204\347\220\206.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\344\270\273\344\273\216\345\244\215\345\210\266\345\216\237\347\220\206\345\217\212\350\277\207\346\234\237key\345\244\204\347\220\206.md" deleted file mode 100644 index 54ebe3ee6d..0000000000 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\344\270\273\344\273\216\345\244\215\345\210\266\345\216\237\347\220\206\345\217\212\350\277\207\346\234\237key\345\244\204\347\220\206.md" +++ /dev/null @@ -1,215 +0,0 @@ -在Redis复制的基础上(不包括Redis Cluster或Redis Sentinel作为附加层提供的高可用功能),使用和配置主从复制非常简单,能使得 【Redis从服务器】(下文称R)能精确得复制 【Redis主服务器】(下文称M)的内容。 -每当 R 和 M 之间的连接断开时, R 会自动重连到 M,并且无论这期间 M 发生了什么, R 都将尝试让自身成为 M 的精确副本。 -# 1 依赖机制 -该系统的运行依靠如下重要的机制: -## 1.1 更新 R -当一个 M 和一个 R 连接正常时, M 会发送一连串命令流保持对 R 的更新,以便将自身数据集的改变复制给 R,这包括客户端的写入、key 的过期或被逐出等 -## 1.2 部分重同步 -当 M 和 R 断连后,因为网络问题、或者是主从意识到连接超时, R 重新连接上 M 并会尝试进行部分重同步:它会尝试只获取在断开连接期间内丢失的命令流。 -## 1.3 全量重同步 -当无法进行部分重同步时, R 会请求全量重同步。 -这涉及到一个更复杂过程,比如M需要创建所有数据的快照,将之发送给 R ,之后在数据集更改时持续发送命令流到 R。 - -Redis使用默认的异步复制,低延迟且高性能,适用于大多数 Redis 场景。但是,R会异步确认其从M周期接收到的数据量。 - -客户端可使用 WAIT 命令来请求同步复制某些特定的数据。但WAIT命令只能确保在其他 Redis 实例中有指定数量的已确认的副本:在故障转移期间,由于不同原因的故障转移或是由于 Redis 持久性的实际配置,故障转移期间确认的写入操作可能仍然会丢失。 -# 2 Redis 复制特点 -- Redis 使用异步复制,R 和 M 之间异步地确认处理的数据量 -- 一个 M 可有多个 R -- R 可接受其他 R 的连接 -除了多个 R 可以连接到同一 M,R 间也可以像层级连接其它 R。Redis 4.0起,所有 sub-R 将会从 M 收到完全一样的复制流 -- Redis 复制在 M 侧是非阻塞的 -M 在一或多 R 进行初次同步或者是部分重同步时,可以继续处理查询请求 -- 复制在 R 侧大部分也是非阻塞 -当 R 进行初次同步时,它可以使用旧数据集处理查询请求,假设在 redis.conf 中配置了让 Redis 这样做。否则,你可以配置如果复制流断开, Redis R 会返回一个 error 给客户端。但在初次同步后,旧数据集必须被删除,同时加载新的数据集。 R 在这个短暂的时间窗口内(如果数据集很大,会持续较长时间),会阻塞到来的连接请求。自 Redis 4.0 开始,可以配置 Redis 使删除旧数据集的操作在另一个不同的线程中进行,但是,加载新数据集的操作依然需要在主线程中进行并且会阻塞 R -- 复制可被用在可伸缩性,以便只读查询可以有多个 R 进行(例如 O(N) 复杂度的慢操作可以被下放到 R ),或者仅用于数据安全和高可用 -- 可使用复制来避免 M 将全部数据集写入磁盘造成的开销:一种典型的技术是配置你的 M 的 `redis.conf`以避免对磁盘进行持久化,然后连接一个 R ,配置为不定期保存或是启用 AOF。但是,这个设置必须小心处理,因为重启的 M 将从一个空数据集开始:如果一个 R 试图与它同步,那么这个 R 也会被清空! - -# 1 单机“危机” -- 容量瓶颈 -- 机器故障 -- QPS瓶颈 - -![](https://img-blog.csdnimg.cn/20200904132333485.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) - -- 一主多从 -![](https://img-blog.csdnimg.cn/20200904150126617.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) - -## 主从复制作用 -- 数据副本 -- 扩展读性能 - -1. 一个M可以有多个R -2. 一个R只能有一个M -3. 数据流向是单向的,M => R - -# 2 实现复制的操作 -## 2.1 命令:Rof -- 异步执行,很耗时间 -![](https://img-blog.csdnimg.cn/20200904150903762.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -无需重启,但是不便于配置管理。 -## 2.2 配置 -```shell -Rof ip port -R-read-only yes -``` -虽然可统一配置,但需重启。 -# 3 全量复制 -1. M执行`bgsave`,在本地生成一份RDB -![](https://img-blog.csdnimg.cn/7c7eda61e919428d9090b56a4c61505e.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -2. M将RDB发给salve,若RDB复制时间>60s(repl-timeout) -![](https://img-blog.csdnimg.cn/6cfb55cdc5ca441c9de0cc67d060590e.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -则replica就会认为复制失败,可适当调大该参数(对于千兆网卡的机器,一般每秒传输100MB,6G文件,很可能超过60s) -![](https://img-blog.csdnimg.cn/5a4ce67217eb4f41a622d630d9268ccf.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -3. M在生成RDB时,会将所有新的写命令缓存在内存中,在salve保存了rdb之后,再将新的写命令复制给salve -4. 若在复制期间,内存缓冲区持续消耗超过64MB,或者一次性超过256MB,则停止复制,复制失败 -5. R node接收到RDB之后,清空自己的旧数据,然后重新加载RDB到自己的内存中,同时**基于旧的数据版本**对外提供服务 -6. 如果R开启了AOF,那么会立即执行BGREWRITEAOF,重写AOF - -![](https://img-blog.csdnimg.cn/20210401150137560.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -RDB生成、RDB通过网络拷贝、R旧数据的清理、R aof rewrite,很耗费时间。 -如果复制的数据量在4G~6G之间,那么很可能全量复制时间消耗到1分半到2分钟。 -## 3.1 全量复制开销 -1. bgsave时间 -2. RDB文件网络传输时间 -3. 从节点清空数据时间 -4. 从节点加载RDB的时间 -5. 可能的AOF重写时间 -## 3.2 全量同步细节 -M 开启一个后台save进程,以便生成一个 RDB 文件。同时它开始缓冲所有从客户端接收到的新的写入命令。当后台save完成RDB文件时, M 将该RDB数据集文件发给 R, R会先将其写入磁盘,然后再从磁盘加载到内存。再然后 M 会发送所有缓存的写命令发给 R。这个过程以指令流的形式完成并且和 Redis 协议本身的格式相同。 - -当主从之间的连接因为一些原因崩溃之后, R 能够自动重连。如果 M 收到了多个 R 要求同步的请求,它会执行一个单独的后台保存,以便于为多个 R 服务。 -### 加速复制 -默认情况下,M接收SYNC命令后执行BGSAVE,将数据先保存到磁盘,若磁盘性能差,则**写入磁盘会消耗大量性能**。 -因此在Redis 2.8.18时进行改进,可以设置无需写入磁盘直接发生RDB快照给R,加快复制速度。 - -复制SYNC策略:磁盘或套接字。仅仅接受差异就无法继续复制过程的新副本和重新连接副本需要进行所谓的“完全同步”。 RDB文件从主数据库传输到副本数据库。传输可以通过两种不同的方式进行:1)支持磁盘:Redis主服务器创建一个新过程,将RDB文件写入磁盘。后来,该文件由父进程逐步传输到副本。 2)无盘:Redis主服务器创建一个新进程,该进程将RDB文件直接写入副本套接字,而完全不接触磁盘。使用磁盘支持的复制,在生成RDB文件的同时,只要生成RDB文件的当前子级完成工作,就可以将更多副本排入队列并与RDB文件一起使用。如果使用无盘复制,则一旦传输开始,新的副本将排队,并且当当前副本终止时将开始新的传输。当使用无盘复制时,主服务器在开始传输之前等待一段可配置的时间(以秒为单位),以希望多个副本可以到达并且传输可以并行化。使用慢速磁盘和快速(大带宽)网络时,无盘复制效果更好。 -修改配置: -```java -repl-diskless-sync yes (默认no) -``` -# 4 增量复制 -1. 如果全量复制过程中,M-R网络连接中断,那么salve重连M时,会触发增量复制 -2. M直接从自己的backlog中获取部分丢失的数据,发送给R node -3. msater就是根据R发送的psync中的offset来从backlog中获取数据的 -![](https://img-blog.csdnimg.cn/20200905001841252.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -# 5 M关闭持久化时的复制安全性 -在使用 Redis 复制功能时的设置中,推荐在 M 和 R 中启用持久化。 -当不可能启用时,例如由于非常慢的磁盘性能而导致的延迟问题,应该配置实例来避免重启后自动重新开始复制。 - -关闭持久化并配置了自动重启的 M 是危险的: -1. 设置节点 A 为 M 并关闭它的持久化设置,节点 B 和 C 从 节点 A 复制数据 -2. 节点 A 宕机,但它有一些自动重启系统可重启进程。但由于持久化被关闭了,节点重启后其数据集是空的! -3. 这时B、C 会从A复制数据,但A数据集空,因此复制结果是它们会销毁自身之前的数据副本! - -当 Redis Sentinel 被用于高可用并且 M 关闭持久化,这时如果允许自动重启进程也是很危险的。例如, M 可以重启的足够快以致于 Sentinel 没有探测到故障,因此上述的故障模式也会发生。 -任何时候数据安全性都是很重要的,所以如果 M 使用复制功能的同时未配置持久化,那么自动重启进程这项就该被禁用。 - -> 用Redis主从同步,写入Redis的数据量太大,没加频次控制,导致每秒几十万写入,主从延迟过大,运维频频报警,在主库不挂掉的情况下,这样大量写入会不会造成数据丢失? -> 若主从延迟很大,数据会堆积到redis主库的发送缓冲区,会导致主库OOM。 - -# 6 复制工作原理 -- 每个 M 都有一个 replication ID :一个较大的伪随机字符串,标记了一个给定的数据集。 -![](https://img-blog.csdnimg.cn/20200905221152322.png#pic_center) -每个 M 也持有一个偏移量,M 将自己产生的复制流发送给 R 时,发送多少个字节的数据,自身的偏移量就会增加多少,目的是当有新的操作修改自己的数据集时,它可据此更新 R 的状态。 -![](https://img-blog.csdnimg.cn/20200905221515483.png#pic_center) - -复制偏移量即使在没有一个 R 连接到 M 时,也会自增,所以基本上每一对给定的 -`Replication ID, offset` -都会标识一个 M 数据集的确切版本。 - -## psync -R使用`psync`从M复制,psync runid offset -![](https://img-blog.csdnimg.cn/20200905233312575.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) - -M会根据自身情况返回响应信息: -- 可能是FULLRESYNC runid offset触发全量复制 -- 可能是CONTINUE触发增量复制 - -R 连接到 M 时,它们使用 PSYNC 命令来发送它们记录的旧的 M replication ID 和它们至今为止处理的偏移量。通过这种方式, M 能够仅发送 R 所需的增量部分。 -但若 M 的缓冲区中没有足够的命令积压缓冲记录,或者如果 R 引用了不再知道的历史记录(replication ID),则会转而进行一个全量重同步:在这种情况下, R 会得到一个完整的数据集副本,从头开始。即: -- 若R重连M,那么M仅会复制给R缺少的部分数据 -- 若第一次连接M,那么会触发全量复制 - -Redis使用复制保证数据同步,以2.8版本为界: -## 2.8前性能较差的复制和命令传播 -首先是从服务器发生同步操作sync,主服务器执行bgsave生成一个全量RDB文件,然后传输给从服务器。 -同时主服务器会把这一过程中执行的写命令写入缓存区。从服务器会把RDB文件进行一次全量加载。 -加载完毕后,主服务器会把缓存区中的写命令传给从服务器。从服务器执行命令后,主从服务器的数据就一致了。 -这种方式每次如果网络出现故障,故障重连后都要进行全量数据的复制。对主服务器的压力太大,也会增加主从网络传输的资源消耗。 -## 2.8后的优化 -增加部分重同步功能,就是同步故障后的一部分数据,而非全量数据。这种优化在量级非常大的情况下效率提升很明显。 - -## 4.0的PSYNC2 -# 7 复制的完整流程 -![](https://img-blog.csdnimg.cn/20190705083122154.png) -> R如果跟M有网络故障,断开连接会自动重连。 -> M如果发现有多个R都重新连接,仅会启动一个rdb save操作,用一份数据服务所有R。 - -1. R启动,仅保存M的信息,包括M的`host`和`ip`,但复制流程尚未开始M host和ip配置在 `redis.conf` 中的 Rof -2. R内部有个定时任务,每s检查是否有新的M要连接和复制,若发现,就跟M建立socket网络连接。 -3. R发送ping命令给M -4. 口令认证 - 若M设置了requirepass,那么salve必须同时发送Mauth的口令认证 -5. M **第一次执行全量复制**,将所有数据发给R -6. M后续持续将写命令,异步复制给R - -## heartbeat -主从节点互相都会发送heartbeat信息。 -M默认每隔10秒发送一次heartbeat,salve node每隔1秒发送一个heartbeat。 - -# 8 断点续传 -Redis 2.8开始支持主从复制的断点续传 -![](https://img-blog.csdnimg.cn/2019070508465819.png) - -主从复制过程,若网络连接中断,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。 - -## M和R都会维护一个offset -- M在自身基础上累加offset,R亦是 -- R每秒都会上报自己的offset给M,同时M保存每个R的offset - -M和R都要知道各自数据的offset,才能知晓互相之间的数据不一致情况。 - -## backlog -M会在内存中维护一个backlog,默认1MB。M给R复制数据时,也会将数据在backlog中同步写一份。 - -`backlog主要是用做全量复制中断时候的增量复制`。 - -M和R都会保存一个replica offset还有一个M id,offset就是保存在backlog中的。若M和R网络连接中断,R会让M从上次replica offset开始继续复制。但若没有找到对应offset,就会执行resynchronization。 - -## M run id -- info server,可见M run id -![](https://img-blog.csdnimg.cn/20200905232843801.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) - -根据host+ip定位M node,是不靠谱的,如果M node重启或者数据出现了变化,那么R node应该根据不同的run id区分,run id不同就做全量复制。 -如果需要不更改run id重启redis,可使用: -```shell -redis-cli debug reload -``` -# 9 无磁盘化复制 -M在内存中直接创建RDB,然后发送给R,不会在自己本地持久化。 -只需要在配置文件中开启` repl-diskless-sync yes `即可. -```shell -等待 5s 再开始复制,因为要等更多 R 重连 -repl-diskless-sync-delay 5 -``` -# 10 处理过期key -Redis 的过期机制可以限制 key 的生存时间。此功能取决于 Redis 实例计算时间的能力,但是,即使使用 Lua 脚本更改了这些 key,Redis Rs 也能正确地复制具有过期时间的 key。 - -为实现功能,Redis 不能依靠主从使用同步时钟,因为这是一个无法解决的问题并且会导致 race condition 和数据不一致,所以 Redis 使用三种主要的技术使过期的 key 的复制能够正确工作: -1. R 不会让 key 过期,而是等待 M 让 key 过期。当一个 M 让一个 key 到期(或由于 LRU 删除)时,它会合成一个 DEL 命令并传输到所有 R -2. 但由于这是 M 驱动的 key 过期行为,M 无法及时提供 DEL 命令,所以有时 R 的内存中仍可能存在逻辑上已过期的 key 。为处理该问题,R 使用它的逻辑时钟以报告只有在不违反数据集的一致性的读取操作(从主机的新命令到达)中才存在 key。用这种方法,R 避免报告逻辑过期的 key 仍然存在。在实际应用中,使用 R 程序进行缩放的 HTML 碎片缓存,将避免返回已经比期望的时间更早的数据项 -3. 在Lua脚本执行期间,不执行任何 key 过期操作 -当一个Lua脚本运行时,概念上讲,M 中的时间是被冻结的,这样脚本运行的时候,一个给定的键要么存在or不存在。这可以防止 key 在脚本中间过期,保证将相同的脚本发送到 R ,从而在二者的数据集中产生相同的效果。 - -一旦 R 被提升 M ,它将开始独立过期 key,而不需要任何旧 M 帮助。 - -# 11 重新启动和故障转移后的部分重同步 -Redis 4.0 开始,当一个实例在故障转移后被提升为 M 时,它仍然能够与旧 M 的 R 进行部分重同步。为此,R 会记住旧 M 的旧 replication ID 和复制偏移量,因此即使询问旧的 replication ID,也可以将部分复制缓冲提供给连接的 R 。 - -但是,升级的 R 的新 replication ID 将不同,因为它构成了数据集的不同历史记录。例如,M 可以返回可用,并且可以在一段时间内继续接受写入命令,因此在被提升的 R 中使用相同的 replication ID 将违反一对复制标识和偏移对只能标识单一数据集的规则。 - -另外,R 在关机并重新启动后,能够在 RDB 文件中存储所需信息,以便与 M 进行重同步。这在升级的情况下很有用。当需要时,最好使用 SHUTDOWN 命令来执行 R 的保存和退出操作。 - -> 参考 -> - https://raw.githubusercontent.com/antirez/redis/2.8/00-RELEASENOTES -> - https://redis.io/topics/replication \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\345\221\275\344\273\244.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\345\221\275\344\273\244.md" index e8d1ca0c3c..1704ae2675 100644 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\345\221\275\344\273\244.md" +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\345\221\275\344\273\244.md" @@ -315,7 +315,7 @@ Redis 4.0.0 起,HSET 是万数值,允许多个字段/值对。 如果 key 指定的哈希集不存在,会创建一个新的哈希集并与 key 关联 如果字段已存在,该操作无效果 -# 4 list +# 4 list 结构 双向列表,适用于最新列表,关注列表 ## 1. lpush @@ -364,9 +364,9 @@ start 和 end 偏移量都是基于0的下标,即list的第一个元素下标 移除并且返回 key 对应的 list 的第一个元素 ### 返回值 bulk-string-reply返回第一个元素的值,或者当 key 不存在时返回 nil。 -## 7 rpop +##7. rpop 移除并返回存于 key 的 list 的最后一个元素。 -### 返回值 +###返回值 bulk-string-reply最后一个元素的值,或者当 key 不存在的时候返回 nil ## 8 bl-pop key [key ...] timeout 阻塞列表的弹出 diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\345\244\215\345\210\266.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\345\244\215\345\210\266.md" new file mode 100644 index 0000000000..db08ef6c1f --- /dev/null +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\345\244\215\345\210\266.md" @@ -0,0 +1,208 @@ +> 全是干货的技术号: +> 本文已收录在github,欢迎 star/fork: +> https://github.com/Wasabi1234/Java-Interview-Tutorial + +在Redis复制的基础上(不包括Redis Cluster或Redis Sentinel作为附加层提供的高可用功能),使用和配置主从复制非常简单,能使得从 Redis 服务器(下文称 slave)能精确得复制主 Redis 服务器(下文称 master)的内容。每次当 slave 和 master 之间的连接断开时, slave 会自动重连到 master 上,并且无论这期间 master 发生了什么, slave 都将尝试让自身成为 master 的精确副本。 + +该系统的运行依靠三个重要机制: +1. 当一个 master 实例和一个 slave 实例连接正常时, master 会发送一连串命令流保持对 slave 的更新,以便将自身数据集的改变复制给 slave,这包括客户端的写入、key 的过期或被逐出等等 +2. 当 master 和 slave 断连后,因为网络问题、或者是主从意识到连接超时, slave 重新连接上 master 并会尝试进行部分重同步:这意味着它会尝试只获取在断开连接期间内丢失的命令流 +3. 当无法进行部分重新同步时, slave 会请求全量重同步。这涉及到一个更复杂过程,比如master 需要创建所有数据的快照,将之发送给 slave ,之后在数据集更改时持续发送命令流到 slave + + +Redis使用默认的异步复制,低延迟且高性能,适用于大多数 Redis 场景。但是,slave会异步确认其从master周期接收到的数据量。 + +客户端可使用 WAIT 命令来请求同步复制某些特定的数据。但是,WAIT 命令只能确保在其他 Redis 实例中有指定数量的已确认的副本:在故障转移期间,由于不同原因的故障转移或是由于 Redis 持久性的实际配置,故障转移期间确认的写入操作可能仍然会丢失。 + +# Redis 复制特点 +- Redis 使用异步复制,slave 和 master 之间异步地确认处理的数据量 +- 一个 master 可以拥有多个 slave +- slave 可以接受其他 slave 的连接。除了多个 slave 可以连接到同一 master , slave 之间也可以像层级连接其它 slave。Redis 4.0 起,所有的 sub-slave 将会从 master 收到完全一样的复制流 +- Redis 复制在 master 侧是非阻塞的,即master 在一或多 slave 进行初次同步或者是部分重同步时,可以继续处理查询请求 +- 复制在 slave 侧大部分也是非阻塞的。当 slave 进行初次同步时,它可以使用旧数据集处理查询请求,假设在 redis.conf 中配置了让 Redis 这样做的话。否则,你可以配置如果复制流断开, Redis slave 会返回一个 error 给客户端。但是,在初次同步之后,旧数据集必须被删除,同时加载新的数据集。 slave 在这个短暂的时间窗口内(如果数据集很大,会持续较长时间),会阻塞到来的连接请求。自 Redis 4.0 开始,可以配置 Redis 使删除旧数据集的操作在另一个不同的线程中进行,但是,加载新数据集的操作依然需要在主线程中进行并且会阻塞 slave +- 复制可被用在可伸缩性,以便只读查询可以有多个 slave 进行(例如 O(N) 复杂度的慢操作可以被下放到 slave ),或者仅用于数据安全和高可用 +- 可使用复制来避免 master 将全部数据集写入磁盘造成的开销:一种典型的技术是配置你的 master 的 `redis.conf`以避免对磁盘进行持久化,然后连接一个 slave ,配置为不定期保存或是启用 AOF。但是,这个设置必须小心处理,因为重启的 master 将从一个空数据集开始:如果一个 slave 试图与它同步,那么这个 slave 也会被清空! + +# 1 单机“危机” +- 容量瓶颈 +- 机器故障 +- QPS瓶颈 + + + +![](https://img-blog.csdnimg.cn/20200904132333485.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70#pic_center) + +- 一主多从 +![](https://img-blog.csdnimg.cn/20200904150126617.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70#pic_center) + +## 主从复制作用 +- 数据副本 +- 扩展读性能 + +## 总结 +1. 一个master可以有多个slave +2. 一个slave只能有一个master +3. 数据流向是单向的,master => slave + +# 2 实现复制的操作 +如下两种实现方式: + +### slaveof 命令 +- 异步执行,很耗时间 +![](https://img-blog.csdnimg.cn/20200904150903762.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70#pic_center) + +无需重启,但是不便于配置的管理。 + +### 配置 +```shell +slaveof ip port +slave-read-only yes +``` +虽然可统一配置,但是需要重启。 + +# 3 全量复制 +1. master执行`bgsave`,在本地生成一份RDB快照client-output-buffer-limit slave 256MB 64MB 60 +2. master node将RDB快照发送给salve node,若RDB复制时间超过60秒(repl-timeout),那么slave node就会认为复制失败,可适当调大该参数(对于千兆网卡的机器,一般每秒传输100MB,6G文件,很可能超过60s) +3. master node在生成RDB时,会将所有新的写命令缓存在内存中,在salve node保存了rdb之后,再将新的写命令复制给salve node +4. 若在复制期间,内存缓冲区持续消耗超过64MB,或者一次性超过256MB,那么停止复制,复制失败 +5. slave node接收到RDB之后,清空自己的旧数据,然后重新加载RDB到自己的内存中,同时**基于旧的数据版本**对外提供服务 +6. 如果slave node开启了AOF,那么会立即执行BGREWRITEAOF,重写AOF + +RDB生成、RDB通过网络拷贝、slave旧数据的清理、slave aof rewrite,很耗费时间 + +如果复制的数据量在4G~6G之间,那么很可能全量复制时间消耗到1分半到2分钟 +## 3.1 全量复制开销 +1. bgsave时间 +2. RDB文件网络传输时间 +3. 从节点清空数据时间 +4. 从节点加载RDB的时间 +5. 可能的AOF重写时间 + +## 3.2 全量同步细节 +master 开启一个后台save进程,以便生成一个 RDB 文件。同时它开始缓冲所有从客户端接收到的新的写入命令。当后台save完成RDB文件时, master 将该RDB数据集文件发给 slave, slave会先将其写入磁盘,然后再从磁盘加载到内存。再然后 master 会发送所有缓存的写命令发给 slave。这个过程以指令流的形式完成并且和 Redis 协议本身的格式相同。 + +当主从之间的连接因为一些原因崩溃之后, slave 能够自动重连。如果 master 收到了多个 slave 要求同步的请求,它会执行一个单独的后台保存,以便于为多个 slave 服务。 + +# 4 增量复制 + +1. 如果全量复制过程中,master-slave网络连接中断,那么salve重连master时,会触发增量复制 +2. master直接从自己的backlog中获取部分丢失的数据,发送给slave node +3. msater就是根据slave发送的psync中的offset来从backlog中获取数据的 +![](https://img-blog.csdnimg.cn/20200905001841252.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70#pic_center) + +# 5 master关闭持久化时的复制安全性 +在使用 Redis 复制功能时的设置中,推荐在 master 和 slave 中启用持久化。 +当不可能启用时,例如由于非常慢的磁盘性能而导致的延迟问题,应该配置实例来避免重启后自动重新开始复制。 + +关闭持久化并配置了自动重启的 master 是危险的: +1. 设置节点 A 为 master 并关闭它的持久化设置,节点 B 和 C 从 节点 A 复制数据 +2. 节点 A 宕机,但它有一些自动重启系统可重启进程。但由于持久化被关闭了,节点重启后其数据集是空的! +3. 这时B、C 会从A复制数据,但A数据集空,因此复制结果是它们会销毁自身之前的数据副本! + +当 Redis Sentinel 被用于高可用并且 master 关闭持久化,这时如果允许自动重启进程也是很危险的。例如, master 可以重启的足够快以致于 Sentinel 没有探测到故障,因此上述的故障模式也会发生。 +任何时候数据安全性都是很重要的,所以如果 master 使用复制功能的同时未配置持久化,那么自动重启进程这项就该被禁用。 + +# 6 复制工作原理 +- 每个 master 都有一个 replication ID :一个较大的伪随机字符串,标记了一个给定的数据集。 +![](https://img-blog.csdnimg.cn/20200905221152322.png#pic_center) + +- 每个 master 也持有一个偏移量,master 将自己产生的复制流发送给 slave 时,发送多少个字节的数据,自身的偏移量就会增加多少,目的是当有新的操作修改自己的数据集时,它可据此更新 slave 的状态。 +![](https://img-blog.csdnimg.cn/20200905221515483.png#pic_center) + +复制偏移量即使在没有一个 slave 连接到 master 时,也会自增,所以基本上每一对给定的 +`Replication ID, offset` +都会标识一个 master 数据集的确切版本。 + +## psync +slave使用`psync`从master复制,psync runid offset +![](https://img-blog.csdnimg.cn/20200905233312575.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70#pic_center) + +master会根据自身情况返回响应信息: +- 可能是FULLRESYNC runid offset触发全量复制 +- 可能是CONTINUE触发增量复制 + +slave 连接到 master 时,它们使用 PSYNC 命令来发送它们记录的旧的 master replication ID 和它们至今为止处理的偏移量。通过这种方式, master 能够仅发送 slave 所需的增量部分。 +但若 master 的缓冲区中没有足够的命令积压缓冲记录,或者如果 slave 引用了不再知道的历史记录(replication ID),则会转而进行一个全量重同步:在这种情况下, slave 会得到一个完整的数据集副本,从头开始。即: +- 若slave重连master,那么master仅会复制给slave缺少的部分数据 +- 若第一次连接master,那么会触发全量复制 + + +# 7 复制的完整流程 +![](https://img-blog.csdnimg.cn/20190705083122154.png) + +> slave如果跟master有网络故障,断开连接会自动重连。 +> master如果发现有多个slave都重新连接,仅会启动一个rdb save操作,用一份数据服务所有slave。 + + +1. slave启动,仅保存master的信息,包括master的`host`和`ip`,但复制流程尚未开始master host和ip配置在 `redis.conf` 中的 slaveof +2. slave内部有个定时任务,每s检查是否有新的master要连接和复制,若发现,就跟master建立socket网络连接。 +3. slave发送ping命令给master +4. 口令认证 - 若master设置了requirepass,那么salve必须同时发送masterauth的口令认证 +5. master **第一次执行全量复制**,将所有数据发给slave +6. master后续持续将写命令,异步复制给slave + +## heartbeat +主从节点互相都会发送heartbeat信息。 +master默认每隔10秒发送一次heartbeat,salve node每隔1秒发送一个heartbeat。 + +# 8 断点续传 +Redis 2.8开始支持主从复制的断点续传 +![](https://img-blog.csdnimg.cn/2019070508465819.png) + +主从复制过程,若网络连接中断,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。 + +## master和slave都会维护一个offset +- master在自身基础上累加offset,slave亦是 +- slave每秒都会上报自己的offset给master,同时master保存每个slave的offset + +master和slave都要知道各自数据的offset,才能知晓互相之间的数据不一致情况。 + +## backlog +master会在内存中维护一个backlog,默认1MB。master给slave复制数据时,也会将数据在backlog中同步写一份。 + +`backlog主要是用做全量复制中断时候的增量复制`。 + +master和slave都会保存一个replica offset还有一个master id,offset就是保存在backlog中的。若master和slave网络连接中断,slave会让master从上次replica offset开始继续复制。但若没有找到对应offset,就会执行resynchronization。 + +## master run id +- info server,可见master run id +![](https://img-blog.csdnimg.cn/20200905232843801.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70#pic_center) + +根据host+ip定位master node,是不靠谱的,如果master node重启或者数据出现了变化,那么slave node应该根据不同的run id区分,run id不同就做全量复制。 +如果需要不更改run id重启redis,可使用: +```shell +redis-cli debug reload +``` + +# 9 无磁盘化复制 +master在内存中直接创建RDB,然后发送给slave,不会在自己本地持久化。 +只需要在配置文件中开启` repl-diskless-sync yes `即可. + +```shell +等待 5s 再开始复制,因为要等更多 slave 重连 +repl-diskless-sync-delay 5 +``` + +# 10 处理过期key +Redis 的过期机制可以限制 key 的生存时间。此功能取决于 Redis 实例计算时间的能力,但是,即使使用 Lua 脚本更改了这些 key,Redis slaves 也能正确地复制具有过期时间的 key。 + +为实现这功能,Redis 不能依靠主从使用同步时钟,因为这是一个无法解决的问题并且会导致 race condition 和数据不一致,所以 Redis 使用三种主要的技术使过期的 key 的复制能够正确工作: +1. slave 不会让 key 过期,而是等待 master 让 key 过期。当一个 master 让一个 key 到期(或由于 LRU 算法删除)时,它会合成一个 DEL 命令并传输到所有 slave +2. 但由于这是 master 驱动的 key 过期行为,master 无法及时提供 DEL 命令,所以有时 slave 的内存中仍然可能存在逻辑上已过期的 key 。为了处理这问题,slave 使用它的逻辑时钟以报告只有在不违反数据集的一致性的读取操作(从主机的新命令到达)中才存在 key。用这种方法,slave 避免报告逻辑过期的 key 仍然存在。在实际应用中,使用 slave 程序进行缩放的 HTML 碎片缓存,将避免返回已经比期望的时间更早的数据项 +3. 在Lua脚本执行期间,不执行任何 key 过期操作。当一个Lua脚本运行时,从概念上讲,master 中的时间是被冻结的,这样脚本运行的时候,一个给定的键要么存在要么不存在。这可以防止 key 在脚本中间过期,保证将相同的脚本发送到 slave ,从而在二者的数据集中产生相同的效果。 + +一旦 slave 被提升 master ,它将开始独立过期 key,而不需要任何旧 master 帮助。 + +# 11 重新启动和故障转移后的部分重同步 +Redis 4.0 开始,当一个实例在故障转移后被提升为 master 时,它仍然能够与旧 master 的 slave 进行部分重同步。为此,slave 会记住旧 master 的旧 replication ID 和复制偏移量,因此即使询问旧的 replication ID,也可以将部分复制缓冲提供给连接的 slave 。 + +但是,升级的 slave 的新 replication ID 将不同,因为它构成了数据集的不同历史记录。例如,master 可以返回可用,并且可以在一段时间内继续接受写入命令,因此在被提升的 slave 中使用相同的 replication ID 将违反一对复制标识和偏移对只能标识单一数据集的规则。 + +另外,slave 在关机并重新启动后,能够在 RDB 文件中存储所需信息,以便与 master 进行重同步。这在升级的情况下很有用。当需要时,最好使用 SHUTDOWN 命令来执行 slave 的保存和退出操作。 + +# 参考 +- https://raw.githubusercontent.com/antirez/redis/2.8/00-RELEASENOTES +- https://redis.io/topics/replication + + +![](https://img-blog.csdnimg.cn/20200825235213822.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\346\225\260\346\215\256\347\273\223\346\236\204\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\346\225\260\346\215\256\347\273\223\346\236\204\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" deleted file mode 100644 index 8cf19f5f5b..0000000000 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/Redis\346\225\260\346\215\256\347\273\223\346\236\204\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" +++ /dev/null @@ -1,552 +0,0 @@ -# 1 概述 -## 数据结构和内部编码 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWEyYzI5MzFmYzlkMGQ1OGEucG5n?x-oss-process=image/format,png) -## 无传统关系型数据库的 Table 模型 -schema 所对应的db仅以编号区分。同一 db 内,key 作为顶层模型,它的值是扁平化的。即 db 就是key的命名空间。 -key的定义通常以 `:` 分隔,如:`Article:Count:1` -常用的Redis数据类型有:string、list、set、map、sorted-set -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWEwNzBkZWExMTNlYWY4ZDEucG5n?x-oss-process=image/format,png) -## redisObject通用结构 -Redis中的所有value 都是以object 的形式存在的,其通用结构如下 -![](https://img-blog.csdnimg.cn/2021061610354559.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- type 数据类型 -指 string、list 等类型 -- encoding 编码方式 -指的是这些结构化类型具体的实现方式,同一个类型可以有多种实现。e.g. string 可以用int 来实现,也可以使用char[] 来实现;list 可以用ziplist 或者链表来实现 -- lru -本对象的空转时长,用于有限内存下长时间不访问的对象清理 -- refcount -对象引用计数,用于GC -- ptr 数据指针 -指向以 encoding 方式实现这个对象实际实现者的地址。如:string 对象对应的SDS地址(string的数据结构/简单动态字符串) - -## 单线程 -![](https://img-blog.csdnimg.cn/20210205224515421.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -### 单线程为何这么快? -- 纯内存 -- 非阻塞I/O -- 避免线程切换和竞态消耗 -![](https://img-blog.csdnimg.cn/20210205224529469.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- 一次只运行一条命令 -- 拒绝长(慢)命令 -keys, flushall, flushdb, slow lua script, mutil/exec, operate big value(collection) -- 其实不是单线程 -fysnc file descriptor -close file descriptor - -# 2 string -Redis中的 string 可表示很多语义 -- 字节串(bits) -- 整数 -- 浮点数 - -redis会根据具体的场景完成自动转换,并根据需要选取底层的实现方式。 -例如整数可以由32-bit/64-bit、有符号/无符号承载,以适应不同场景对值域的要求。 - -- 字符串键值结构,也能是 JSON 串或 XML 结构 -![](https://img-blog.csdnimg.cn/20210205224713852.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -## 内存结构 -在Redis内部,string的内部以 int、SDS(简单动态字符串 simple dynamic string)作为存储结构 -- int 用来存放整型 -- SDS 用来存放字节/字符和浮点型SDS结构 -### SDS -```c -typedef struct sdshdr { - // buf中已经占用的字符长度 - unsigned int len; - // buf中剩余可用的字符长度 - unsigned int free; - // 数据空间 - char buf[]; -} -``` -- 结构图![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQ3OWZlMGFhYzZmYmU0NGQucG5n?x-oss-process=image/format,png) -存储的内容为“Redis”,Redis采用类似C语言的存储方法,使用'\0'结尾(仅是定界符)。 -SDS的free 空间大小为0,当free > 0时,buf中的free 区域的引入提升了SDS对字符串的处理性能,可以减少处理过程中的内存申请和释放次数。 -### buf 的扩容与缩容 -当对SDS 进行操作时,如果超出了容量。SDS会对其进行扩容,触发条件如下: -- 字节串初始化时,buf的大小 = len + 1,即加上定界符'\0'刚好用完所有空间 -- 当对串的操作后小于1M时,扩容后的buf 大小 = 业务串预期长度 * 2 + 1,也就是扩大2倍。 -- 对于大小 > 1M的长串,buf总是留出 1M的 free空间,即2倍扩容,但是free最大为 1M。 -### 字节串与字符串 -SDS中存储的内容可以是ASCII 字符串,也可以是字节串。由于SDS通过len 字段来确定业务串的长度,因此业务串可以存储非文本内容。对于字符串的场景,buf[len] 作为业务串结尾的'\0' 又可以复用C的已有字符串函数。 -### SDS编码的优化 -value 在内存中有2个部分:redisObject和ptr指向的字节串部分。 -在创建时,通常要分别为2个部分申请内存,但是对于小字节串,可以一次性申请。 - -incr userid:pageview (单线程:无竞争)。缓存视频的基本信息(数据源在MySQL) -![](https://img-blog.csdnimg.cn/20210205230250128.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -```java -public VideoInfo get(Long id) { - String redisKey = redisPrefix + id; - VideoInfo videoInfo e redis.get(redisKey); - if (videoInfo == null) { - videoInfo = mysql.get(id); - if (videoInfo != null) { - // 序列化 - redis.set(redisKey serialize(videoInfo)): - } - } -} -``` - - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWUwMjdmOWUwYmY4NWQ2MzgucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LThhYmU4MTk1OTZkNzE1ZmYucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQyYzYzMTk4ZDMzZWU2Y2QucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTZlMDM2MWI2NGRhODI1MzMucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTIxZWVjZDkwNzI2MmJjN2QucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTYxYTUwYWI3Y2QzN2FhYjkucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTRhOTg4NmI4ZGEyNGZlZDIucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWJiODU2MDBiM2I1Mjg4ODgucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTI3NjAzOTRkNTk5M2FkODgucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQwNzZjOWM1ZGQwZjUwNzMucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWMwODNlYzM5MzQ0ZmEyM2EucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQxYzRmZGNlN2Q3ZDA2YTQucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTJlN2RiOGNiNTU2Mzg3ZWQucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTBhODMxZmE4NjQwNmJjMjkucG5n?x-oss-process=image/format,png) -![String类型的value基本操作](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTdjZGRlZTM3NGU4M2I3ZDYucG5n?x-oss-process=image/format,png) -除此之外,string 类型的value还有一些CAS的原子操作,如:get、set、set value nx(如果不存在就设置)、set value xx(如果存在就设置)。 - -String 类型是二进制安全的,也就是说在Redis中String类型可以包含各种数据,比如一张JPEG图片或者是一个序列化的Ruby对象。一个String类型的值最大长度可以是512M。 - -在Redis中String有很多有趣的用法 -* 把String当做原子计数器,这可以使用INCR家族中的命令来实现:[INCR](https://github.com/antirez/redis-doc/blob/master/commands/incr), [DECR](https://github.com/antirez/redis-doc/blob/master/commands/decr), [INCRBY](https://github.com/antirez/redis-doc/blob/master/commands/incrby)。 -* 使用[APPEND](https://github.com/antirez/redis-doc/blob/master/commands/append)命令来给一个String追加内容。 -* 把String当做一个随机访问的向量(Vector),这可以使用[GETRANGE](https://github.com/antirez/redis-doc/blob/master/commands/getrange)和 [SETRANGE](https://github.com/antirez/redis-doc/blob/master/commands/setrange)命令来实现 -* 使用[GETBIT](https://github.com/antirez/redis-doc/blob/master/commands/getbit) 和[SETBIT](https://github.com/antirez/redis-doc/blob/master/commands/setbit)方法,在一个很小的空间中编码大量的数据,或者创建一个基于Redis的Bloom Filter 算法。 -# List -可从头部(左侧)加入元素,也可以从尾部(右侧)加入元素。有序列表。 - -像微博粉丝,即可以list存储做缓存。 -```bash -key = 某大v - -value = [zhangsan, lisi, wangwu] -``` -所以可存储一些list型的数据结构,如: -- 粉丝列表 -- 文章的评论列表 - -可通过lrange命令,即从某元素开始读取多少元素,可基于list实现分页查询,这就是基于redis实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。 - -搞个简单的消息队列,从list头推进去,从list尾拉出来。 - -List类型中存储一系列String值,这些String按照插入顺序排序。 - -## 5.1 内存数据结构 -List 类型的 value对象,由 linkedlist 或 ziplist 实现。 -当 List `元素个数少并且元素内容长度不大`采用ziplist 实现,否则使用linkedlist - -### 5.1.1 linkedlist实现 -链表的代码结构 -```c -typedef struct list { - // 头结点 - listNode *head; - // 尾节点 - listNode *tail; - // 节点值复制函数 - void *(*dup)(void * ptr); - // 节点值释放函数 - void *(*free)(void *ptr); - // 节点值对比函数 - int (*match)(void *ptr, void *key); - // 链表长度 - unsigned long len; -} list; - -// Node节点结构 -typedef struct listNode { - struct listNode *prev; - struct listNode *next; - void *value; -} listNode; -``` -linkedlist 结构图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTg1NzAyZDg2NzYxZDFiZjMucG5n?x-oss-process=image/format,png) -### 5.1.2 ziplist实现 -存储在连续内存 -![](https://img-blog.csdnimg.cn/ac58e5b93d294a958940f681d4b165e5.png) -- zlbytes -ziplist 的总长度 -- zltail -指向最末元素。 -- zllen -元素的个数。 -- entry -元素内容。 -- zlend -恒为0xFF,作为ziplist的定界符 - -linkedlist和ziplist的rpush、rpop、llen的时间复杂度都是O(1): -- ziplist的lpush、lpop都会牵扯到所有数据的移动,时间复杂度为O(N) -由于List的元素少,体积小,这种情况还是可控的。 - -ziplist的Entry结构: -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTJkNWQ1YWI3NGRhYmZhOTkucG5n?x-oss-process=image/format,png) -记录前一个相邻的Entry的长度,便于双向遍历,类似linkedlist的prev指针。 -ziplist是连续存储,指针由偏移量来承载。 -Redis中实现了2种方式实现: -- 当前邻 Entry的长度小于254 时,使用1字节实现 -- 否则使用5个字节 - -> 当前一个Entry长度变化时,可能导致后续的所有空间移动,虽然这种情况发生可能性较小。 - -Entry内容本身是自描述的,意味着第二部分(Entry内容)包含了几个信息:Entry内容类型、长度和内容本身。而内容本身包含:类型长度部分和内容本身部分。类型和长度同样采用变长编码: -- 00xxxxxx :string类型;长度小于64,0~63可由6位bit 表示,即xxxxxx表示长度 -- 01xxxxxx|yyyyyyyy : string类型;长度范围是[64, 16383],可由14位 bit 表示,即xxxxxxyyyyyyyy这14位表示长度。 -- 10xxxxxx|yy..y(32个y) : string类型,长度大于16383. -- 1111xxxx :integer类型,integer本身内容存储在xxxx 中,只能是1~13之间取值。也就是说内容类型已经包含了内容本身。 -- 11xxxxxx :其余的情况,Redis用1个字节的类型长度表示了integer的其他几种情况,如:int_32、int_24等。 -由此可见,ziplist 的元素结构采用的是可变长的压缩方法,针对于较小的整数/字符串的压缩效果较好 - -- LPUSH命令 -在头部加入一个新元素 -- RPUSH命令 -在尾部加入一个新元素 - -当在一个空的K执行这些操作时,会创建一个新列表。当一个操作清空了一个list时,该list对应的key会被删除。若使用一个不存在的K,就会使用一个空list。 -```bash -LPUSH mylist a   # 现在list是 "a" -LPUSH mylist b   # 现在list是"b","a" -RPUSH mylist c   # 现在list是 "b","a","c" (注意这次使用的是 RPUSH) -``` -list的最大长度是`2^32 – 1`个元素(4294967295,一个list中可以有多达40多亿个元素)。 - -从时间复杂度的角度来看,Redis list类型的最大特性是:即使是在list的头端或者尾端做百万次的插入和删除操作,也能保持稳定的很少的时间消耗。在list的两端访问元素是非常快的,但是如果要访问一个很大的list中的中间部分的元素就会比较慢了,时间复杂度是O(N) -## 适用场景 -- 社交中使用List进行时间表建模,使用 LPUSH 在用户时间线中加入新元素,然后使用 LRANGE 获得最近加入元素 -- 可以把[LPUSH] 和[LTRIM] 命令结合使用来实现定长的列表,列表中只保存最近的N个元素 -- 做MQ,依赖BLPOP这种阻塞命令 -# Set -类似List,但无序且其元素不重复。 - -向集合中添加多次相同的元素,集合中只存在一个该元素。在实际应用中,这意味着在添加一个元素前不需要先检查元素是否存在。 - -支持多个服务器端命令来从现有集合开始计算集合,所以执行集合的交集,并集,差集都很快。 - -set的最大长度是`2^32 – 1`个元素(一个set中可多达40多亿个元素)。 -## 内存数据结构 -Set在Redis中以intset 或 hashtable存储: -- 对于Set,HashTable的value永远为NULL -- 当Set中只包含整型数据时,采用intset作为实现 - -### intset -核心元素是一个字节数组,从小到大有序的存放元素 -![](https://img-blog.csdnimg.cn/20200911231000505.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -结构图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTczN2MzY2NiZDAxMzcxZjEucG5n?x-oss-process=image/format,png) -因为元素有序排列,所以SET的获取操作采用二分查找,复杂度为O(log(N))。 - -进行插入操作时: -- 首先通过二分查找到要插入位置 -- 再对元素进行扩容 -- 然后将插入位置之后的所有元素向后移动一个位置 -- 最后插入元素 - -时间复杂度为O(N)。为使二分查找的速度足够快,存储在content 中的元素是定长的。 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTkwODQ2NzBlNzk2MTJlMGIucG5n?x-oss-process=image/format,png) -当插入2018 时,所有的元素向后移动,并且不会发生覆盖。 -当Set 中存放的整型元素集中在小整数范围[-128, 127]内时,可大大的节省内存空间。 -IntSet支持升级,但是不支持降级。 - -- Set 基本操作 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LThkYmNkMTJiMDRlZmJjZmQucG5n?x-oss-process=image/format,png) -## 适用场景 -无序集合,自动去重,数据太多时不太推荐使用。 -直接基于set将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于JVM内存里的HashSet进行去重,但若你的某个系统部署在多台机器呢?就需要基于redis进行全局的set去重。 - -可基于set玩交集、并集、差集操作,比如交集: -- 把两个人的粉丝列表整一个交集,看看俩人的共同好友 -- 把两个大v的粉丝都放在两个set中,对两个set做交集 - -全局这种计算开销也大。 - -- 记录唯一的事物 -比如想知道访问某个博客的IP地址,不要重复的IP,这种情况只需要在每次处理一个请求时简单的使用SADD命令就可以了,可确保不会插入重复IP - -- 表示关系 -你可以使用Redis创建一个标签系统,每个标签使用一个Set表示。然后你可以使用SADD命令把具有特定标签的所有对象的所有ID放在表示这个标签的Set中 -如果你想要知道同时拥有三个不同标签的对象,那么使用SINTER - -- 可使用[SPOP](https://github.com/antirez/redis-doc/blob/master/commands/spop) 或 [SRANDMEMBER](https://github.com/antirez/redis-doc/blob/master/commands/srandmember) 命令从集合中随机的提取元素。 - -# Hash/Map -一般可将结构化的数据,比如一个对象(前提是这个对象未嵌套其他的对象)给缓存在redis里,然后每次读写缓存的时候,即可直接操作hash里的某个字段。 -```json -key=150 - -value={ - “id”: 150, - “name”: “zhangsan”, - “age”: 20 -} -``` -hash类的数据结构,主要存放一些对象,把一些简单的对象给缓存起来,后续操作的时候,可直接仅修改该对象中的某字段的值。 -```c -value={ - “id”: 150, - “name”: “zhangsan”, - “age”: 21 -} -``` -因为Redis本身是一个K.V存储结构,Hash结构可理解为subkey - subvalue -这里面的subkey - subvalue只能是 -- 整型 -- 浮点型 -- 字符串 - -因为Map的 value 可表示整型和浮点型,因此Map也可以使用` hincrby` 对某个field的value值做自增操作。 - -## 内存数据结构 -hash有HashTable 和 ziplist 两种实现。对于数据量较小的hash,使用ziplist 实现。 -### HashTable 实现 -HashTable在Redis 中分为3 层,自底向上分别是: -- dictEntry:管理一个field - value 对,保留同一桶中相邻元素的指针,以此维护Hash 桶中的内部链 -- dictht:维护Hash表的所有桶链 -- dict:当dictht需要扩容/缩容时,用户管理dictht的迁移 - -dict是Hash表存储的顶层结构 -```c -// 哈希表(字典)数据结构,Redis 的所有键值对都会存储在这里。其中包含两个哈希表。 -typedef struct dict { - // 哈希表的类型,包括哈希函数,比较函数,键值的内存释放函数 - dictType *type; - // 存储一些额外的数据 - void *privdata; - // 两个哈希表 - dictht ht[2]; - // 哈希表重置下标,指定的是哈希数组的数组下标 - int rehashidx; /* rehashing not in progress if rehashidx == -1 */ - // 绑定到哈希表的迭代器个数 - int iterators; /* number of iterators currently running */ -} dict; -``` -Hash表的核心结构是dictht,它的table 字段维护着 Hash 桶,桶(bucket)是一个数组,数组的元素指向桶中的第一个元素(dictEntry)。 - -```c -typedef struct dictht { - //槽位数组 - dictEntry **table; - //槽位数组长度 - unsigned long size; - //用于计算索引的掩码 - unsigned long sizemask; - //真正存储的键值对数量 - unsigned long used; -} dictht; -``` -结构图![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTEwYzRhMWFkMGY3MDExMTcucG5n?x-oss-process=image/format,png) -Hash表使用【链地址法】解决Hash冲突。当一个 bucket 中的 Entry 很多时,Hash表的插入性能会下降,此时就需要增加bucket的个数来减少Hash冲突。 -#### Hash表扩容 -和大多数Hash表实现一样,Redis引入负载因子判定是否需要增加bucket个数: -```bash -负载因子 = Hash表中已有元素 / bucket数量 -``` -扩容后,bucket 数量是原先2倍。目前有2 个阀值: -- 小于1 时一定不扩容 -- 大于5 时一定扩容 - -- 在1 ~ 5 之间时,Redis 如果没有进行`bgsave/bdrewrite` 操作时则会扩容 -- 当key - value 对减少时,低于0.1时会进行缩容。缩容之后,bucket的个数是原先的0.5倍 -### ziplist 实现 -这里的 ziplist 和List#ziplist的实现类似,都是通过Entry 存放元素。 -不同的是,Map#ziplist的Entry个数总是2的整数倍: -- 第奇数个Entry存放key -- 下个相邻Entry存放value - -ziplist承载时,Map的大多数操作不再是O(1)了,而是由Hash表遍历,变成了链表的遍历,复杂度变为O(N) -由于Map相对较小时采用ziplist,采用Hash表时计算hash值的开销较大,因此综合起来ziplist的性能相对好一些 - -哈希键值结构 -![](https://img-blog.csdnimg.cn/919d84762fc44637b12de9e9ebf11a94.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTE1MTAyZWVkODNjMzA4ZWMucG5n?x-oss-process=image/format,png) -特点: -- Map的map -- Small redis -- field不能相同,value可相同 - -```bash -hget key field O(1) -# 获取 hash key 对应的 field 的 value - -hset key field value O(1) -# 设置 hash key 对应的 field 的 value - -hdel key field O(1) -# 删除 hash key 对应的 field 的 value -``` -## 实操 -```bash -127.0.0.1:6379> hset user:1:info age 23 -(integer) 1 -127.0.0.1:6379> hget user:1:info age -"23" -127.0.0.1:6379> hset user:1:info name JavaEdge -(integer) 1 -127.0.0.1:6379> hgetall user:1:info -1) "age" -2) "23" -3) "name" -4) "JavaEdge" -127.0.0.1:6379> hdel user:1:info age -(integer) 1 -127.0.0.1:6379> hgetall user:1:info -1) "name" -2) "JavaEdge" -``` - -```bash -hexists key field O(1) -# 判断hash key是否有field -hlen key O(1) -# 获取hash key field的数量 -``` - -```bash -127.0.0.1:6379> hgetall user:1:info -1) "name" -2) "JavaEdge" -127.0.0.1:6379> HEXISTS user:1:info name -(integer) 1 -127.0.0.1:6379> HLEN user:1:info -(integer) 1 -``` - -```bash -hmget key field1 field2... fieldN O(N) -# 批量获取 hash key 的一批 field 对应的值 -hmset key field1 value1 field2 value2...fieldN valueN O(N) -# 批量设置 hash key的一批field value - -``` -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWU3NjM0MmRkMDlkNjAwNzYucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTc3M2FjMWI5NGVhZDA3MTAucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTFmZDkzNTYyMWNjYzg5NTUucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTJhODk3MjllYTA0ODg2YmQucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTM3Y2ZiOTcxY2JlMDhiOTQucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTFmMzJmMGJkMjk0MDFhZmEucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTE3OTRmMjk0NTMzZjQ2ZGMucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWVhMTVmZDBkNjg2YjlkNmQucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTFmNzRhMjEwOTE5YjJhN2UucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LThhMWE1MjNhNWE4NDJiOTAucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTI1OThiNTVlN2FlNzA1ZGMucG5n?x-oss-process=image/format,png) -![方便单条更新,但是信息非整体,不便管理](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LThiODAyYmFhODgyNzBlZTgucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWE2ZDZhOGZmMmVmYTM4NmIucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTIwMjg4MjgzZDE1ODUyYWQucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWI1YTNmZGQwODdjMTIzM2MucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTJjNzdkNWQzYTc1OTc5NzMucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTQ0NGYzNWU3MzM5MzRjNTkucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTA5OTc2M2M1YzM0YjM4YzUucG5n?x-oss-process=image/format,png) -Redis Hashes 保存String域和String值之间的映射,所以它们是用来表示对象的绝佳数据类型(比如一个有着用户名,密码等属性的User对象) -``` -| `1` | `@cli` | - -| `2` | `HMSET user:1000 username antirez password P1pp0 age 34` | - -| `3` | `HGETALL user:1000` | - -| `4` | `HSET user:1000 password 12345` | - -| `5` | `HGETALL user:1000` | -``` -一个有着少量数据域(这里的少量大概100上下)的hash,其存储方式占用很小的空间,所以在一个小的Redis实例中就可以存储上百万的这种对象 - -Hash的最大长度是2^32 – 1个域值对(4294967295,一个Hash中可以有多达40多亿个域值对) -# Sorted sets(zset) -有序集合,去重但可排序,写进去时候给个分数,可以自定义排序规则。比如想根据时间排序,则写时可以使用时间戳作为分数。 - -排行榜:将每个用户以及其对应的什么分数写进去。 -```bash -127.0.0.1:6379> zadd board 1.0 JavaEdge -(integer) 1 -``` - -获取排名前100的用户: -```bash -127.0.0.1:6379> zrevrange board 0 99 -1) "JavaEdge" -``` -用户在排行榜里的排名: -```bash -127.0.0.1:6379> zrank board JavaEdge -(integer) 0 -``` - -```bash -127.0.0.1:6379> zadd board 85 zhangsan -(integer) 1 -127.0.0.1:6379> zadd board 72 wangwu -(integer) 1 -127.0.0.1:6379> zadd board 96 lisi -(integer) 1 -127.0.0.1:6379> zadd board 62 zhaoliu -(integer) 1 - -# 获取排名前3的用户 -127.0.0.1:6379> zrevrange board 0 3 -1) "lisi" -2) "zhangsan" -3) "wangwu" -4) "zhaoliu" - -127.0.0.1:6379> zrank board zhaoliu -(integer) 1 -``` -类似于Map的key-value对,但有序 -- key :key-value对中的键,在一个Sorted-Set中不重复 -- value : 浮点数,称为 score -- 有序 :内部按照score 从小到大的顺序排列 -## 基本操作 -由于Sorted-Set 本身包含排序信息,在普通Set 的基础上,Sorted-Set 新增了一系列和排序相关的操作: -- Sorted-Set的基本操作 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTI2OTNlZDUxNGU0Njc5MTgucG5n?x-oss-process=image/format,png) -## 内部数据结构 -Sorted-Set类型的valueObject 内部结构有两种: -1. ziplist -![](https://img-blog.csdnimg.cn/20200911183043109.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -实现方式和Map类似,由于Sorted-Set包含了Score的排序信息,ziplist内部的key-value元素对的排序方式也是按照Score递增排序的,意味着每次插入数据都要移动之后的数据,因此ziplist适于元素个数不多,元素内容不大的场景。 -2. skiplist+hashtable -![](https://img-blog.csdnimg.cn/20200911183355830.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -更通用的场景,Sorted-Set使用sliplist来实现。 -### 8.2.1 zskiplist -和通用的跳表不同的是,Redis为每个level 对象增加了span 字段,表示该level 指向的forward节点和当前节点的距离,使得getByRank类的操作效率提升 -- 数据结构 -![](https://img-blog.csdnimg.cn/20200911184359226.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -- 结构示意图![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTk5OGJhNjJjZTQ1MTE1YTcucG5n?x-oss-process=image/format,png) - -每次向skiplist 中新增或者删除一个节点时,需要同时修改图标中红色的箭头,修改其forward和span的值。 - -需要修改的箭头和对skip进行查找操作遍历并废弃过的路径是吻合的。span修改仅是+1或-1。 -zskiplist 的查找平均时间复杂度 O(Log(N)),因此add / remove的复杂度也是O(Log(N))。因此Redis中新增的span 提升了获取rank(排序)操作的性能,仅需对遍历路径相加即可(矢量相加)。 - -还有一点需要注意的是,每个skiplist的节点level 大小都是随机生成的(1-32之间)。 -- zskiplistNode源码 -![](https://img-blog.csdnimg.cn/20200911185457885.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -### 8.2.2 hashtable -zskiplist 是zset 实现顺序相关操作比较高效的数据结构,但是对于简单的zscore操作效率并不高。Redis在实现时,同时使用了Hashtable和skiplist,代码结构如下: -![](https://img-blog.csdnimg.cn/20200911190122653.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -Hash表的存在使得Sorted-Set中的Map相关操作复杂度由O(N)变为O(1)。 - -Redis有序集合类型与Redis的集合类型类似,是非重复的String元素的集合。不同之处在于,有序集合中的每个成员都关联一个Score,Score是在排序时候使用的,按照Score的值从小到大进行排序。集合中每个元素是唯一的,但Score有可能重复。 - -使用有序集合可以很高效的进行,添加,移除,更新元素的操作(时间消耗与元素个数的对数成比例)。由于元素在集合中的位置是有序的,使用get ranges by score或者by rank(位置)来顺序获取或者随机读取效率都很高。(本句不确定,未完全理解原文意思,是根据自己对Redis的浅显理解进行的翻译)访问有序集合中间部分的元素也非常快,所以可以把有序集合当做一个不允许重复元素的智能列表,你可以快速访问需要的一切:获取有序元素,快速存在测试,快速访问中间的元素等等。 - -简短来说,使用有序集合可以实现很多高性能的工作,这一点在其他数据库是很难实现的。 - -## 应用 -* 在大型在线游戏中创建一个排行榜,每次有新的成绩提交,使用[ZADD]命令加入到有序集合中。可以使用[ZRANGE]命令轻松获得成绩名列前茅的玩家,你也可以使用[ZRANK]根据一个用户名获得该用户的分数排名。把ZRANK 和 ZRANGE结合使用你可以获得与某个指定用户分数接近的其他用户。这些操作都很高效。 - -* 有序集合经常被用来索引存储在Redis中的数据。比如,如果你有很多用户,用Hash来表示,可以使用有序集合来为这些用户创建索引,使用年龄作为Score,使用用户的ID作为Value,这样的话使用[ZRANGEBYSCORE]命令可以轻松和快速的获得某一年龄段的用户。zset有个ZSCORE的操作,用于返回单个集合member的分数,它的操作复杂度是O(1),这就是收益于你这看到的hash table。这个hash table保存了集合元素和相应的分数,所以做ZSCORE操作时,直接查这个表就可以,复杂度就降为O(1)了。 - -而跳表主要服务范围操作,提供O(logN)的复杂度。 -# Bitmaps -位图类型,String类型上的一组面向bit操作的集合。由于 strings是二进制安全的blob,并且它们的最大长度是512m,所以bitmaps能最大设置 2^32个不同的bit。 -# HyperLogLogs -pfadd/pfcount/pfmerge。 -在redis的实现中,使用标准错误小于1%的估计度量结束。这个算法的神奇在于不再需要与需要统计的项相对应的内存,取而代之,使用的内存一直恒定不变。最坏的情况下只需要12k,就可以计算接近2^64个不同元素的基数。 -# GEO -geoadd/geohash/geopos/geodist/georadius/georadiusbymember -Redis的GEO特性在 Redis3.2版本中推出,这个功能可以将用户给定的地理位置(经、纬度)信息储存起来,并对这些信息进行操作。 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/\345\246\202\344\275\225\350\247\243\345\206\263Redis\347\232\204\345\271\266\345\217\221\347\253\236\344\272\211\351\227\256\351\242\230.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/\345\246\202\344\275\225\350\247\243\345\206\263Redis\347\232\204\345\271\266\345\217\221\347\253\236\344\272\211\351\227\256\351\242\230.md" deleted file mode 100644 index 07b6285907..0000000000 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/Redis/\345\246\202\344\275\225\350\247\243\345\206\263Redis\347\232\204\345\271\266\345\217\221\347\253\236\344\272\211\351\227\256\351\242\230.md" +++ /dev/null @@ -1,6 +0,0 @@ -# Redis的并发竞争问题 -- 多客户端同时并发写一个key,可能本来应该先到的数据后到了,导致数据版本出错 -- 多客户端同时获取一个key,修改值之后再写回去,只要顺序错了,数据也错了 -# 解决方案 -其实Redis本身就有解决这个问题的CAS类的乐观锁方案。 -![](https://img-blog.csdnimg.cn/20190509175418361.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/Java\351\253\230\346\200\247\350\203\275\347\263\273\347\273\237\347\274\223\345\255\230\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" "b/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/Java\351\253\230\346\200\247\350\203\275\347\263\273\347\273\237\347\274\223\345\255\230\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" index f22b2aedaf..6ba0533ac0 100644 --- "a/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/Java\351\253\230\346\200\247\350\203\275\347\263\273\347\273\237\347\274\223\345\255\230\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" +++ "b/\346\225\260\346\215\256\345\255\230\345\202\250/\346\266\210\346\201\257\351\230\237\345\210\227/Java\351\253\230\346\200\247\350\203\275\347\263\273\347\273\237\347\274\223\345\255\230\347\232\204\346\234\200\344\275\263\345\256\236\350\267\265.md" @@ -1,3 +1,5 @@ +![](https://img-blog.csdnimg.cn/2020081311184130.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70#pic_center) + # 1 屈服于现实的磁盘 MQ都使用磁盘来存储消息。这样服务器下电也不会丢数据。绝大多数用于生产系统的服务器,都会使用多块磁盘组成磁盘阵列,这样即使其中的一块异常,也可把数据从其他磁盘中恢复。 diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\345\233\236\346\272\257\347\256\227\346\263\225.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\345\233\236\346\272\257\347\256\227\346\263\225.md" deleted file mode 100644 index c001787db6..0000000000 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\345\233\236\346\272\257\347\256\227\346\263\225.md" +++ /dev/null @@ -1,68 +0,0 @@ -DFS利用的就是是回溯算法思想,但其实回溯还可用在如正则表达式匹配、编译原理中的语法分析等。 - -数学问题也可,如数独、八皇后、0-1背包、图的着色、旅行商问题、全排列等。 -# 理解“回溯算法” -> 若人生可重来,如何才能在岔路口做出最正确选择,让自己的人生“最优”? - -贪心算法,在每次面对岔路口的时候,都做出看起来最优的选择,期望这一组选择可以使得我们的人生达到“最优”。但不一定能得到的是最优解。 - -> 如何确保得到最优解? - -回溯算法很多时候都应用在“搜索”问题:在一组可能解中,搜索期望解。 -处理思想,类似枚举搜索:枚举所有解,找到满足期望的解。 - -为规律枚举所有可能解,避免遗漏、重复,将问题求解过程分为多个阶段。 -每个阶段,都要面对一个岔路口,先随意选一条路走,当发现这条路走不通(不符合期望的解),就回退到上一个岔路口,另选一种走法继续走。 -# 八皇后 -8x8的棋盘,往里放8个棋子(皇后),每个棋子所在的行、列、对角线都不能有另一个棋子。 -![](https://img-blog.csdnimg.cn/0532b7d902884ce29a148695e4872ca0.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_11,color_FFFFFF,t_70,g_se,x_16) - -把这个问题划分成8个阶段,依次将8个棋子放到第一行、第二行、第三行……第八行。 -放置过程中,不停地检查当前方法,是否满足要求 -- 满足 -跳到下一行继续放置棋子 -- 不满足 -换种方法尝试 - -适合递归实现: -![](https://img-blog.csdnimg.cn/4e7b69d48d3a4d04a3faafba4e91f90b.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -# 0-1背包 -经典解法是动态规划,但还有简单但没那么高效的回溯解法。 - -- 有一背包,背包总承载重量Wkg -- 有n个物品,每个物品重量不等且不可分割 -- 期望选择几件物品,装载到背包中。在不超过背包所能装载重量的前提下,求背包中物品的总重量max? - -这个背包问题,物品不可分割,要么装要么不装,所以叫0-1背包,就无法通过贪心解决了。 - -- 对于每个物品来说,都有两种选择,装 or 不装 -- 对于n个物品,就有 $2^n$ 种装法,去掉总重量超过Wkg的,从剩下的装法中选择总重量最接近Wkg的 -- 但如何才能不重复地穷举出这 $2^n$ 种装法? - -这就能回溯,把物品依次排列,整个问题分解为n个阶段,每个阶段对应一个物品怎么选择; -- 先对第一个物品进行处理,选择装进去 or 不装进去 -- 再递归处理剩下物品 - -搜索剪枝的技巧:当发现 已选择物品重量 > Wkg,就停止探测剩下物品。 -![](https://img-blog.csdnimg.cn/e47c8d0cd7ae4203a3c8b109b62e4f01.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -## 正则表达式 -假设正表达式中只包含`*`、`?`通配符且现在规定: -- `*` 匹配任意多个(大于等于0个)任意字符 -- `?` 匹配0或1个任意字符 - -如何用回溯算法,判断某给定文本,是否匹配给定的正则表达式? -依次考察正则表达式中的每个字符,当是非通配符时,就直接跟文本的字符进行匹配: -- 相同 -继续往下处理 -- 不同 -回溯 - -遇到特殊字符时,就有多种处理方式,如`*`有多种匹配方案,可匹配任意个文本串中的字符,先随意选择一种匹配方案,然后继续考察剩下字符: -- 若中途发现无法继续匹配,就回到岔路口,重新选择一种匹配方案,再继续匹配剩下字符。 -![](https://img-blog.csdnimg.cn/31c5e631f10743328322e91c5387fa70.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -# 总结 -回溯算法思想很简单,大部分都是用来解决广义搜索问题:从一组可能解中,选出一个满足要求的解。 - -回溯非常适合用递归实现,剪枝是提高回溯效率的一种技巧,无需穷举搜索所有情况。 - -回溯算法可解决很多问题,如我们开头提到的深度优先搜索、八皇后、0-1背包问题、图的着色、旅行商问题、数独、全排列、正则表达式匹配等。 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204/\345\246\202\344\275\225\350\256\276\350\256\241\345\255\230\345\202\250\347\244\276\344\272\244\345\271\263\345\217\260\345\245\275\345\217\213\345\205\263\347\263\273\347\232\204\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204/\345\246\202\344\275\225\350\256\276\350\256\241\345\255\230\345\202\250\347\244\276\344\272\244\345\271\263\345\217\260\345\245\275\345\217\213\345\205\263\347\263\273\347\232\204\346\225\260\346\215\256\347\273\223\346\236\204.md" deleted file mode 100644 index 7546256101..0000000000 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204/\345\246\202\344\275\225\350\256\276\350\256\241\345\255\230\345\202\250\347\244\276\344\272\244\345\271\263\345\217\260\345\245\275\345\217\213\345\205\263\347\263\273\347\232\204\346\225\260\346\215\256\347\273\223\346\236\204.md" +++ /dev/null @@ -1,118 +0,0 @@ -x博中,两个人可以互相关注,互加好友,那如何存储这些社交网络的好友关系呢? - -这就要用到:图。 -# 什么是“图”?(Graph) -和树比起来,这是一种更加复杂的非线性表结构。 - -树的元素称为节点,图中元素叫作顶点(vertex)。图中的一个顶点可以与任意其他顶点建立连接关系,这种建立的关系叫作边(edge)。 -![](https://img-blog.csdnimg.cn/9e8d606b57b24f239a46a36734d4249c.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -社交网络就是典型的图结构。 - -把每个用户看作一个顶点。如果两个用户之间互加好友,就在两者之间建立一条边。 -所以,整个微信的好友关系就可用一张图表示。 -每个用户有多少个好友,对应到图中就叫作顶点的度(degree),即跟顶点相连接的边的条数。 - -不过微博的社交关系跟微信还有点不同,更复杂一点。微博允许单向关注,即用户A关注用户B,但B可不关注A。 - -> 如何用图表示这种单向社交关系呢? - -这就引入边的“方向”。 - -A关注B,就在图中画一条从A到B的带箭头的边,表示边的方向。A、B互关,就画一条从A指向B的边,再画一条从B指向A的边,这种边有方向的图叫作“有向图”。边没有方向的图也就叫“无向图”。 -![](https://img-blog.csdnimg.cn/f4ef3aa2589649838a55c13b5d559459.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -无向图中有“度”:一个顶点有多少条边。 -有向图中,把度分为: -- 入度(In-degree) -有多少条边指向这个顶点,即有多少粉丝 -- 出度(Out-degree) -有多少条边是以这个顶点为起点指向其他顶点。对应到微博的例子,即关注了多少人 - -QQ社交关系更复杂,不仅记录用户之间的好友关系,还记录了两个用户之间的亲密度,如何在图中记录这种好友关系亲密度呢? -这就要用到带权图(weighted graph),每条边都有个权重(weight),可以通过这个权重来表示QQ好友间的亲密度。 -![](https://img-blog.csdnimg.cn/3807928549ef4d99af2744c1350d8630.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -# 存储 -## 邻接矩阵存储方法 -最直观的一种存储方法,邻接矩阵(Adjacency Matrix)。 - -依赖一个二维数组: -- 无向图 -如果顶点i与顶点j之间有边,就将A[i][j]和A[j][i]标记为1 -- 有向图 -如果顶点i到顶点j之间,有一条箭头从顶点i指向顶点j的边,那我们就将A[i][j]标记为1 -如果有一条箭头从顶点j指向顶点i的边,我们就将A[j][i]标记为1 -- 带权图,数组中就存储相应的权重 -![](https://img-blog.csdnimg.cn/43973239a1424f979308c26e8c1781ba.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -> 简单、直观,但比较浪费存储空间! - -无向图,若A[i][j]==1,则A[j][i]==1。实际上,只需存储一个即可。即无向图的二维数组,如果将其用对角线划分为上下两部分,则只需利用上或下面这样一半空间就够了,另外一半其实完全浪费。 -如果存储的是稀疏图(Sparse Matrix),即顶点很多,但每个顶点的边并不多,则更浪费空间。 -如微信有好几亿用户,对应到图就是好几亿顶点。但每个用户好友并不很多,一般也就三五百个而已。如果我们用邻接矩阵来存储,那绝大部分的存储空间都被浪费了。 - -但这也并不是说,邻接矩阵的存储方法就完全没有优点。首先,邻接矩阵的存储方式简单、直接,因为基于数组,所以在获取两个顶点的关系时,就非常高效。其次,用邻接矩阵存储图的另外一个好处是方便计算。这是因为,用邻接矩阵的方式存储图,可以将很多图的运算转换成矩阵之间的运算。比如求解最短路径问题时会提到一个Floyd-Warshall算法,就是利用矩阵循环相乘若干次得到结果。 -## 邻接表存储方法 -针对上面邻接矩阵比较浪费内存空间,另外一种图存储,邻接表(Adjacency List)。 - -有点像散列表?每个顶点对应一条链表,链表中存储的是与这个顶点相连接的其他顶点。图中画的是一个有向图的邻接表存储方式,每个顶点对应的链表里面,存储的是指向的顶点。对于无向图来说,也是类似的,不过,每个顶点的链表中存储的,是跟这个顶点有边相连的顶点,你可以自己画下。 -![](https://img-blog.csdnimg.cn/ee3d7c7dc6f146c6adb34bcafe3b19d5.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -- 邻接矩阵存储较浪费空间,但更省时 -- 邻接表较节省存储空间,但较耗时 - -如上图示例,若要确定是否存在一条从顶点2到顶点4的边,就要遍历顶点2的链表,看其中是否存在顶点4,而链表存储对缓存不友好。所以邻接表查询两个顶点之间的关系较为低效。 - -基于链表法解决冲突的散列表中,若链过长,为提高查找效率,可将链表换成其他更高效数据结构,如平衡二叉查找树。 -邻接表长得很像散列。所以,也可将邻接表同散列表一样进行“优化”。 - -可将邻接表中的链表改成平衡二叉查找树。实际可选用红黑树。即可更快速查找两个顶点之间是否存在边。 -这里的二叉查找树也可换成其他动态数据结构,如跳表、散列表。 -还可将链表改成有序动态数组,通过二分查找快速定位两个顶点之间是否存在边。 -# 如何存储微博、微信等社交网络中的好友关系? -虽然微博有向图,微信是无向图,但对该问题,二者思路类似,以微博为例。 - -数据结构服务于算法,选择哪种存储方法和需支持的操作有关。 -对于微博用户关系,需支持如下操作: -- 判断用户A是否关注了用户B -- 判断用户A是否是用户B的粉丝 -- 用户A关注用户B -- 用户A取消关注用户B -- 根据用户名称的首字母排序,分页获取用户的粉丝列表 -- 根据用户名称的首字母排序,分页获取用户的关注列表 - -因为社交网络是一张稀疏图,使用邻接矩阵存储比较浪费存储空间。所以,这里采用邻接表。 - -但一个邻接表存储这种有向图也是不够的。查找某用户关注了哪些用户很容易,但若想知道某用户都被哪些用户关注了,即粉丝列表就没法了。 - -因此,还需一个逆邻接表,存储用户的被关注关系: -- 邻接表,每个顶点的链表中,存储的就是该顶点指向的顶点 -查找某个用户关注了哪些用户 -- 逆邻接表,每个顶点的链表中,存储的是指向该顶点的顶点 -查找某个用户被哪些用户关注 -![](https://img-blog.csdnimg.cn/69ba70ef961d441c9753c31258af84a8.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -基础的邻接表不适合快速判断两个用户是否为关注与被关注关系,所以进行优化,将邻接表的链表改为支持快速查找的动态数据结构。 - -> 那是红黑树、跳表、有序动态数组还是散列表呢? - -因需按照用户名称首字母排序,分页获取用户的粉丝列表或关注列表,跳表最合适:插入、删除、查找都非常高效,时间复杂度$O(logn)$,空间复杂度稍高,是$O(n)$。 -跳表存储数据先天有序,分页获取粉丝列表或关注列表,非常高效。 - -对小规模数据,如社交网络中只有几万、几十万个用户,可将整个社交关系存储在内存,该解决方案没问题。 - -> 但像微博上亿用户,数据量太大,无法全部存储在内存,何解? - -可通过哈希算法等数据分片方案,将邻接表存储在不同机器: -- 机器1存储顶点1,2,3的邻接表 -- 机器2存储顶点4,5的邻接表 -逆邻接表的处理方式同理。 - -当要查询顶点与顶点关系时,利用同样的哈希算法,先定位顶点所在机器,然后再在相应机器上查找。 -![](https://img-blog.csdnimg.cn/295354cd7c904fa793e2784269c58241.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -还能借助外部存储(比如硬盘),因为外部存储的存储空间比内存多很多: -如用下表存储这样一个图。为高效支持前面定义的操作,可建多个索引,比如第一列、第二列,给这两列都建立索引。 -![](https://img-blog.csdnimg.cn/b95b76c1fcce4f4a9df754bd80616f04.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -> 参考 -> - https://chowdera.com/2021/03/20210326155939001z.html -> - https://www.zhihu.com/question/20216864 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\350\216\267\345\217\226Top 10\345\276\256\345\215\232\347\203\255\346\220\234\345\205\263\351\224\256\350\257\215\357\274\237.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\350\216\267\345\217\226Top 10\345\276\256\345\215\232\347\203\255\346\220\234\345\205\263\351\224\256\350\257\215\357\274\237.md" deleted file mode 100644 index 0c21f06f0a..0000000000 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\350\216\267\345\217\226Top 10\345\276\256\345\215\232\347\203\255\346\220\234\345\205\263\351\224\256\350\257\215\357\274\237.md" +++ /dev/null @@ -1,178 +0,0 @@ -搜索引擎每天会接收大量的用户搜索请求,它会把这些用户输入的搜索关键词记录下来,然后再离线统计分析,得到最热门TopN搜索关键词。 - -现在有一包含10亿个搜索关键词的日志文件,如何能快速获取到热门榜Top 10搜索关键词? -可用堆解决,堆的几个应用:优先级队列、求Top K和求中位数。 -# 优先级队列 -首先应该是一个队列。队列最大的特性FIFO。 但优先级队列中,数据出队顺序是按优先级来,优先级最高的,最先出队。 - -方法很多,但堆实现最直接、高效。因为堆和优先级队列很相似。一个堆即可看作一个优先级队列。很多时候,它们只是概念上的区分。 -- 往优先级队列中插入一个元素,就相当于往堆中插入一个元素 -- 从优先级队列中取出优先级最高的元素,就相当于取出堆顶元素 - -优先级队列应用场景非常多:赫夫曼编码、图的最短路径、最小生成树算法等,Java的PriorityQueue。 -## 合并有序小文件 -- 有100个小文件 -- 每个文件100M -- 每个文件存储有序字符串 - -将这100个小文件合并成一个有序大文件,就用到优先级队列。 -像归排的合并函数。从这100个文件中,各取第一个字符串,放入数组,然后比较大小,把最小的那个字符串放入合并后的大文件中,并从数组中删除。 - -假设,这最小字符串来自13.txt这个小文件,就再从该小文件取下一个字符串并放入数组,重新比较大小,并且选择最小的放入合并后的大文件,并且将它从数组中删除。依次类推,直到所有的文件中的数据都放入到大文件为止。 - -用数组存储从小文件中取出的字符串。每次从数组取最小字符串,都需循环遍历整个数组,不高效,如何更高效呢? -就要用到优先级队列,即堆:将从小文件中取出的字符串放入小顶堆,则堆顶元素就是优先级队列队首,即最小字符串。 -将这个字符串放入大文件,并将其从堆中删除。 -再从小文件中取出下一个字符串,放入到堆 -循环该 过程,即可将100个小文件中的数据依次放入大文件。 - -删除堆顶数据、往堆插数据时间复杂度都是$O(logn)$,该案例$n=100$。 -这不比原来数组存储高效多了? -# 2 高性能定时器 -有一定时器,维护了很多定时任务,每个任务都设定了一个执行时间点。 -定时器每过一个单位时间(如1s),就扫描一遍任务,看是否有任务到达设定执行时间。若到达,则执行。 -![](https://img-blog.csdnimg.cn/a8005a51f4ed46ada66d75c0f198c5f0.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -显然这样每过1s就扫描一遍任务列表很低效: -- 任务约定执行时间离当前时间可能还很久,这样很多次扫描其实都无意义 -- 每次都要扫描整个任务列表,若任务列表很大,就很耗时 - -这时就该优先级队列上场了。按任务设定的执行时间,将这些任务存储在优先级队列,队首(即小顶堆的堆顶)存储最先执行的任务。 - -这样,定时器就无需每隔1s就扫描一遍任务列表了。 - -> $队首任务执行时间点 - 当前时间点相减 = 时间间隔T$ - -T就是,从当前时间开始,需等待多久,才会有第一个任务要被执行。 -定时器就能设定在T秒后,再来执行任务。 -当前时间点 ~ $(T-1)s$ 时间段,定时器无需做任何事情。 - -当Ts时间过去后,定时器取优先级队列中队首任务执行 -再计算新的队首任务执行时间点与当前时间点差值,将该值作为定时器执行下一个任务需等待时间。 - -如此设计,定时器既不用间隔1s就轮询一次,也无需遍历整个任务列表,性能大大提高。 -# 利用堆求Top K -求Top K的问题抽象成两类: -## 静态数据集合 -数据集合事先确定,不会再变。 - -可维护一个大小为K的小顶堆,顺序遍历数组,从数组中取数据与堆顶元素比较: -- >堆顶 -删除堆顶,并将该元素插入堆 -- <堆顶 -do nothing,继续遍历数组 - -等数组中的数据都遍历完,堆中数据就是Top K。 - -遍历数组需要$O(n)$时间复杂度 -一次堆化操作需$O(logK)$时间复杂度 -所以最坏情况下,n个元素都入堆一次,所以时间复杂度就是$O(nlogK)$ -## 动态数据集合 -数据集合事先并不确定,有数据动态地加入到集合中,也就是求实时Top K。 -一个数据集合中有两个操作: -- 添加数据 -- 询问当前TopK数据 - -若每次询问Top K大数据,都基于当前数据重新计算,则时间复杂度$O(nlogK)$,n表示当前数据的大小。 -其实可一直都维护一个K大小的小顶堆,当有数据被添加到集合,就拿它与堆顶元素对比: -- >堆顶 -就把堆顶元素删除,并且将这个元素插入到堆中 -- <堆顶 -do nothing。无论何时需查询当前的前K大数据,都可以里立刻返回给他 -# 利用堆求中位数 -求**动态数据**集合中的中位数: -- 数据个数奇数 -把数据从小到大排列,第$\frac{n}{2}+1$个数据就是中位数 -- 数据个数是偶数 -处于中间位置的数据有两个,第$\frac{n}{2}$个、第$\frac{n}{2}+1$个数据,可随意取一个作为中位数,比如取两个数中靠前的那个,即第$\frac{n}{2}$个数据 - -一组静态数据的中位数是固定的,可先排序,第$\frac{n}{2}$个数据就是中位数。 -每次询问中位数,直接返回该固定值。所以,尽管排序的代价比较大,但是边际成本会很小。但是,如果我们面对的是动态数据集合,中位数在不停地变动,如果再用先排序的方法,每次询问中位数的时候,都要先进行排序,那效率就不高了。 - -借助堆,不用排序,即可高效地实现求中位数操作: -需维护两个堆: -- 大顶堆 -存储前半部分数据 -- 小顶堆 -存储后半部分数据 && 小顶堆数据都 > 大顶堆数据 - -即若有n(偶数)个数据,从小到大排序,则: -- 前 $\frac{n}{2}$ 个数据存储在大顶堆 -- 后$\frac{n}{2}$个数据存储在小顶堆 - -大顶堆中的堆顶元素就是我们要找的中位数。 - -n是奇数也类似: -- 大顶堆存储$\frac{n}{2}+1$个数据 -- 小顶堆中就存储$\frac{n}{2}$个数据 - -数据动态变化,当新增一个数据时,如何调整两个堆,让大顶堆堆顶继续是中位数, 若: -- 新加入的数据 ≤ 大顶堆堆顶,则将该新数据插到大顶堆 -- 新加入的数据大于等于小顶堆的堆顶元素,我们就将这个新数据插入到小顶堆。 - -这时可能出现,两个堆中的数据个数不符合前面约定的情况,若: -- n是偶数,两个堆中的数据个数都是 $\frac{n}{2}$ -- n是奇数,大顶堆有 $\frac{n}{2}+1$ 个数据,小顶堆有 $\frac{n}{2}$ 个数据 - -即可从一个堆不停将堆顶数据移到另一个堆,以使得两个堆中的数据满足上面约定。 - -插入数据涉及堆化,所以时间复杂度$O(logn)$,但求中位数只需返回大顶堆堆顶,所以时间复杂度$O(1)$。 - -利用两个堆还可快速求其他百分位的数据,原理类似。 -“如何快速求接口的99%响应时间? - -中位数≥前50%数据,类比中位数,若将一组数据从小到大排列,这个99百分位数就是大于前面99%数据的那个数据。 - -假设有100个数据:1,2,3,……,100,则99百分位数就是99,因为≤99的数占总个数99%。 - -> 那99%响应时间是啥呢? - -若有100个接口访问请求,每个接口请求的响应时间都不同,如55ms、100ms、23ms等,把这100个接口的响应时间按照从小到大排列,排在第99的那个数据就是99%响应时间,即99百分位响应时间。 - -即若有n个数据,将数据从小到大排列后,99百分位数大约就是第n*99%个数据。 -维护两个堆,一个大顶堆,一个小顶堆。假设当前总数据的个数是n,大顶堆中保存n*99%个数据,小顶堆中保存n*1%个数据。大顶堆堆顶的数据就是我们要找的99%响应时间。 - -每插入一个数据时,要判断该数据跟大顶堆、小顶堆堆顶的大小关系,以决定插入哪个堆: -- 新插入数据 < 大顶堆的堆顶,插入大顶堆 -- 新插入的数据 > 小顶堆的堆顶,插入小顶堆 - -但为保持大顶堆中的数据占99%,小顶堆中的数据占1%,每次新插入数据后,都要重新计算,这时大顶堆和小顶堆中的数据个数,是否还符合99:1: -- 不符合,则将一个堆中的数据移动到另一个堆,直到满足比例 -移动的方法类似前面求中位数的方法 - -如此,每次插入数据,可能涉及几个数据的堆化操作,所以时间复杂度$O(logn)$。 -每次求99%响应时间时,直接返回大顶堆中的堆顶即可,时间复杂度$O(1)$。 -# 含10亿个搜索关键词的日志文件,快速获取Top 10 -很多人肯定说使用MapReduce,但若将场景限定为单机,可使用内存为1GB,你咋办? - -用户搜索的关键词很多是重复的,所以首先要统计每个搜索关键词出现的频率。 -可通过散列表、平衡二叉查找树或其他一些支持快速查找、插入的数据结构,记录关键词及其出现次数。 - -假设散列表。 -顺序扫描这10亿个搜索关键词。当扫描到某关键词,去散列表中查询: -- 存在,对应次数加一 -- 不存在,插入散列表,并记录次数1 - -等遍历完这10亿个搜索关键词后,散列表就存储了不重复的搜索关键词及出现次数。 - -再根据堆求Top K方案,建立一个大小为10小顶堆,遍历散列表,依次取出每个搜索关键词及对应出现次数,然后与堆顶搜索关键词对比: -- 出现次数 > 堆顶搜索关键词的次数 -删除堆顶关键词,将该出现次数更多的关键词入堆。 - -以此类推,当遍历完整个散列表中的搜索关键词之后,堆中的搜索关键词就是出现次数最多的Top 10搜索关键词了。 - -但其实有问题。10亿的关键词还是很多的。 -假设10亿条搜索关键词中不重复的有1亿条,如果每个搜索关键词的平均长度是50个字节,那存储1亿个关键词起码需要5G内存,而散列表因为要避免频繁冲突,不会选择太大的装载因子,所以消耗的内存空间就更多了。 -而机器只有1G可用内存,无法一次性将所有的搜索关键词加入内存。 - -> 何解? - -因为相同数据经哈希算法后的哈希值相同,可将10亿条搜索关键词先通过哈希算法分片到10个文件: -- 创建10个空文件:00~09 -- 遍历这10亿个关键词,并通过某哈希算法求哈希值 -- 哈希值同10取模,结果就是该搜索关键词应被分到的文件编号 - -10亿关键词分片后,每个文件都只有1亿关键词,去掉重复的,可能就只剩1000万,每个关键词平均50个字节,总大小500M,1G内存足矣。 - -针对每个包含1亿条搜索关键词的文件: -- 利用散列表和堆,分别求Top 10 -- 10个Top 10放一起,取这100个关键词中,出现次数Top 10关键词,即得10亿数据的Top 10热搜关键词 \ No newline at end of file diff --git "a/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\347\232\204\346\210\230\347\225\245\350\256\276\350\256\241.md" "b/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\347\232\204\346\210\230\347\225\245\350\256\276\350\256\241.md" deleted file mode 100644 index 22d890033e..0000000000 --- "a/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\347\232\204\346\210\230\347\225\245\350\256\276\350\256\241.md" +++ /dev/null @@ -1,78 +0,0 @@ -模型设计,DDD 分两阶段,战略设计和战术设计。 -# 战略设计 -战略设计的子域、限界上下文和上下文映射图等概念大致分为: -- 业务划分 -以区分不同业务,即划分识别出来的业务概念 -- 落地成解决方案 -将划分出来的业务落地 - -# 业务概念的划分 -首先需要明确: -## 问题是什么 -我们要解决的问题就是领域问题,相关概念如,子域、核心域、支撑域、通用域等。其实都是:如何分解问题。 -## 如何解决 -领域(Domain)是一号位,对应待解决的问题。解决问题通常思路是分而治之,DDD就把一个大领域分解成若干小领域(子域(Subdomain))。 - -DDD首先要建立起一套通用语言,拥有一致的业务词汇表,它们对应模型。 -接着,要分类词汇,即把它们划分到不同子域。**关键就是分离关注点。** - -比如,做个项目管理软件,就要有用户、项目、团队,不同人还要扮演不同角色。 -第一步,至少先分开身份管理、项目管理,因为关注点不同: -- 身份管理,关注用户身份信息,如用户名、密码 -- 项目管理,关注项目和团队等 - -这就有了俩子域:身份管理,项目管理。 - -若直接给结果,你可能会觉得很好理解。但划分出不同子域还是容易出问题的,因为有些概念不易区分。 -比如,用户应该怎么划分?放在身份管理是合适的,但项目管理也要用到呀! - -根据单一职责原则,它给了我们一个重要思考维度,**变化从何而来**? -不同角色的人关注不同变化,所以,虽然我们用的词都是“用户”,但想表达的含义其实不同,最好分开这些不同的含义,即分开不同角色: -- 身份管理中,它是“用户” -- 项目管理中,就成了“项目成员” -# 业务概念的落地 -DDD问题层面的概念已经阐述完毕。接下来,就是解决方案层面。 - -切分出的子域,怎样落实到代码? -首先要解决的就是这些子域如何组织? -- 写一个程序把所有子域都放里面 -- 每个子域单独做个应用 -- 有一些在一起,有一些分开 - -这就引出DDD的 -# 限界上下文(Bounded Context) -限定了通用语言自由使用的边界,一旦出界,含义便无法保证。 -比如,同样是“订单”,不加限制,很难区分它在哪种场景。一旦定义了限界上下文,那交易上下文的“订单”和物流上下文的“订单”肯定不同。就是因为订单这个说法,在不同边界内,含义不同。 - -注意,子域和限界上下文不一定是一一对应的,可能在一个限界上下文中包含了多个子域,也可能在一个子域横跨了多个限界上下文。 - -限界上下文是在解决方案层面,所以,就可以把限界上下文看作一个独立系统。限界上下文与微服务的理念契合,每个限界上下文都可成为一个独立服务。 - -限界上下文是完全独立的,不会为了完成一个业务需求要跑到其他服务中去做很多事,这恰是很多微服务出问题的点,比如一个业务功能要调用很多其他系统功能。 - -有限界上下文,就可以把整个业务分解到不同的限界上下文中,但是,尽管我们拆分了系统,它们终究还是一个系统,免不了交互。 -比如: -- 一个用户下了订单,这是在订单上下文中完成的 -- 用户要去支付,这是在支付上下文中完成的 - -我们要通过某种途径让订单上下文的一些信息发送到支付上下文。所以,要有一种描述方式,描述不同限界上下文之间交互的方式-上下文映射图(Context Map)。 -DDD 给我们提供了一些描述这种交互的方式,比如: -- 合作关系(Partnership) -- 共享内核(Shared Kernel) -- 客户-供应商(Customer-Supplier) -- 跟随者(Conformist) -- 防腐层(Anticorruption Layer) -防腐层是最具防御性的一种关系,就是在外部模型和内部模型之间建立起一个翻译层,将外部模型转化为内部模型。但凡有可能,就要建立防腐层,将外部模型完全隔离开。 -- 开放主机服务(Open Host Service) -- 发布语言(Published Language) -- 各行其道(Separate Ways) -- 大泥球(Big Ball of Mud) -要规避 - -这么多交互方式,主要是为让你在头脑中仔细辨认,看看限界上下文之间到底在以怎样的方式交互。 - -知道不同限界上下文之间交互方式后,不同交互方式就可落地为不同协议。 -常用协议如:REST API、RPC 或是 MQ, 按需选型即可。 - -在我们定义好不同的限界上下文,将它们之间的交互呈现出来之后,就得到了一张上下文映射图。 -上下文映射图是可以帮助我们理解系统的各个部分之间,是怎样进行交互的,建立全局性认知。 \ No newline at end of file diff --git "a/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/\345\205\205\350\241\200\346\250\241\345\236\213\343\200\201\350\264\253\350\241\200\351\242\206\345\237\237\346\250\241\345\236\213.md" "b/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/\345\205\205\350\241\200\346\250\241\345\236\213\343\200\201\350\264\253\350\241\200\351\242\206\345\237\237\346\250\241\345\236\213.md" index 351028cad7..4bda8731aa 100644 --- "a/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/\345\205\205\350\241\200\346\250\241\345\236\213\343\200\201\350\264\253\350\241\200\351\242\206\345\237\237\346\250\241\345\236\213.md" +++ "b/\346\236\266\346\236\204/DDD\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241/\345\205\205\350\241\200\346\250\241\345\236\213\343\200\201\350\264\253\350\241\200\351\242\206\345\237\237\346\250\241\345\236\213.md" @@ -10,16 +10,15 @@ - 只有状态的对象就是所谓的“贫血对象”(常称为VO——Value Object) - 只有行为的对象就是我们常见的N层结构中的Logic/Service/Manager层(对应到EJB2中的Stateless Session Bean)。(曾经Spring的作者Rod Johnson也承认,Spring不过是在沿袭EJB2时代的“事务脚本”,也就是面向过程编程) -贫血领域模型是一个存在已久的反模式,它不是个好东西。 +贫血领域模型是一个存在已久的反模式,目前仍有许多拥趸者。Martin Fowler曾经和Eric Evans聊天谈到它时,都觉得这个模型似乎越来越流行了。作为领域模型的推广者,他们觉得这不是一件好事。 -- 贫血领域模型的基本特征 -它第一眼看起来还真像这么回事儿。项目中有许多对象,它们的命名都是根据领域来的。对象之间有着丰富的连接方式,和真正的领域模型非常相似。但当你检视这些对象的行为时,会发现它们基本上没有任何行为,仅仅是一堆getter/setter。 +贫血领域模型的基本特征是:它第一眼看起来还真像这么回事儿。项目中有许多对象,它们的命名都是根据领域来的。对象之间有着丰富的连接方式,和真正的领域模型非常相似。但当你检视这些对象的行为时,会发现它们基本上没有任何行为,仅仅是一堆getter/setter。 其实这些对象在设计之初就被定义为只能包含数据,不能加入领域逻辑。这些逻辑要全部写入一组叫Service的对象中。这些Service构建在领域模型之上,使用这些模型来传递数据。 -## 反模式的恐怖 -它完全和面向对象设计背道而驰。面向对象设计主张将数据和行为绑定在一起,而贫血领域模型则更像是一种面向过程设计。 +这种反模式的恐怖之处在于,它完全和面向对象设计背道而驰。 +面向对象设计主张将数据和行为绑定在一起,而贫血领域模型则更像是一种面向过程设计,Martin Fowler和Eric在Smalltalk时就极力反对这种做法。更糟糕的时,很多人认为这些贫血领域对象是真正的对象,从而彻底误解了面向对象设计的涵义。 -贫血领域模型的根本问题在于,它**引入了领域模型设计的所有成本,却没有带来任何好处**。 +如今,面向对象的概念已经传播得很广泛了,而要反对这种贫血领域模型的做法,还需要更多论据。贫血领域模型的根本问题在于,它**引入了领域模型设计的所有成本,却没有带来任何好处**。 最主要的成本是将对象映射到数据库中,从而产生了一个O/R(对象关系)映射层。只有当你充分使用了面向对象设计来组织复杂的业务逻辑后,这一成本才能够被抵消。如果将所有行为都写入到Service对象,那最终你会得到一组事务处理脚本,从而错过了领域模型带来的好处。正如martin在企业应用架构模式一书中说到的,领域模型并不一定是最好的工具。 将行为放入领域模型,这点和分层设计(领域层、持久化层、展现层等)并不冲突。因为领域模型中放入的是和领域相关的逻辑——验证、计算、业务规则等。如果你要讨论能否将数据源或展现逻辑放入到领域模型中,这就不在本文论述范围之内了。 @@ -36,7 +35,7 @@ Eric Evans的Domain Driven Design一书中提到: 服务层很薄——所有重要的业务逻辑都写在领域层。他在服务模式中复述了这一观点: 如今人们常犯的错误是不愿花时间将业务逻辑放到合适的领域模型中,从而逐渐形成面向过程的程序设计。 -为什么这种反模式会那么常见呢。我怀疑是因为大多数人并没有使用过一个设计良好的领域模型,特别是那些以数据为中心的开发人员。此外,有些技术也会推动这种反模式,比如J2EE的Entity Bean,这会让我更倾向于使用POJO领域模型。 +我不清楚为什么这种反模式会那么常见。我怀疑是因为大多数人并没有使用过一个设计良好的领域模型,特别是那些以数据为中心的开发人员。此外,有些技术也会推动这种反模式,比如J2EE的Entity Bean,这会让我更倾向于使用POJO领域模型。 总之,如果你将大部分行为都放置在服务层,那么你就会失去领域模型带来的好处。如果你将所有行为都放在服务层,那你就无可救药了。 @@ -69,7 +68,7 @@ Eric Evans的Domain Driven Design一书中提到: 如果一个对象包含其他对象,那就将职责继续委托下去,由具体的 POJO 执行业务逻辑,将策略模式更加细粒度,而不是写 ifelse。 -> 参考 -> - 《领域驱动设计》 -> - https://www.zhihu.com/question/20360521/answer/14891150 -> - https://www.martinfowler.com/bliki/AnemicDomainModel.html \ No newline at end of file +参考 +- 《领域驱动设计》 +- https://www.zhihu.com/question/20360521/answer/14891150 +- https://www.martinfowler.com/bliki/AnemicDomainModel.html \ No newline at end of file diff --git "a/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Dubbo/Dubbo\347\232\204SPI\346\234\272\345\210\266.md" "b/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Dubbo/Dubbo\345\222\214JDK\347\232\204SPI\346\234\272\345\210\266\345\257\271\346\257\224\350\257\246\350\247\243.md" similarity index 56% rename from "\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Dubbo/Dubbo\347\232\204SPI\346\234\272\345\210\266.md" rename to "\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Dubbo/Dubbo\345\222\214JDK\347\232\204SPI\346\234\272\345\210\266\345\257\271\346\257\224\350\257\246\350\247\243.md" index 15237562fb..8c1547fbe1 100644 --- "a/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Dubbo/Dubbo\347\232\204SPI\346\234\272\345\210\266.md" +++ "b/\346\236\266\346\236\204/\345\210\206\345\270\203\345\274\217\345\276\256\346\234\215\345\212\241/Dubbo/Dubbo\345\222\214JDK\347\232\204SPI\346\234\272\345\210\266\345\257\271\346\257\224\350\257\246\350\247\243.md" @@ -1,34 +1,66 @@ -Dubbo 没使用 Java SPI,而重新实现了一套功能更强的 SPI。 +# 1 SPI简介 +SPI,Service Provider Interface,一种服务发现机制。 -Dubbo SPI 逻辑封装在 ExtensionLoader 类,通过 ExtensionLoader,可加载指定实现类。Dubbo SPI 所需配置文件需放置在 `META-INF/dubbo` 路径:![](https://img-blog.csdnimg.cn/20201220143821997.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) +比如一接口有3个实现类,那么在系统运行时,这个接口到底该选择哪个实现类? +这就需要SPI,**根据指定或默认的配置,找到对应的实现类,加载进来,然后使用该实现类实例**。 +![](https://img-blog.csdnimg.cn/20201220141747102.png) -配置内容如下: +在系统实际运行的时候,会加载你的配置,用实现A2实例化一个对象来提供服务。 + +比如你要通过jar包给某个接口提供实现,然后你就在自己jar包的`META-INF/services/`目录下放一个接口同名文件,指定接口的实现是自己这个jar包里的某个类。 +![](https://img-blog.csdnimg.cn/20201220142131599.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + +别人用这个接口,然后用你的jar包,就会在运行时通过你的jar包的那个文件找到这个接口该用哪个实现类。这是JDK提供的功能。 + +比如你有个工程A,有个接口A,接口A在工程A没有实现类,系统运行时怎么给接口A选个实现类呢? +可以自己搞个jar包,`META-INF/services/`,放上一个文件,文件名即接口名,接口A,接口A的实现类=`com.javaedge.service.实现类A2`。 + +让工程A来依赖你的jar包,然后在系统运行时,工程A跑起来,对于接口A,就会扫描依赖的jar包,看看有没有`META-INF/services`文件夹。 +如果有,再看看有没有名为接口A的文件,如果有,在里面找一下指定的接口A的实现是你的jar包里的哪个类! + +# 2 适用场景 +插件扩展的场景,比如你开发了一个开源框架,若你想让别人自己写个插件,安排到你的开源框架里中,扩展功能 + +## 2.1 Java中的SPI +经典的思想体现,其实大家平时都在用,比如JDBC。Java定义了一套JDBC的接口,但并未提供其实现类。 + +但实际上项目运行时,要使用JDBC接口的哪些实现类呢? +一般要根据自己使用的数据库引入: +- MySQL,`mysql-jdbc-connector.jar` +![](https://img-blog.csdnimg.cn/20201220151405844.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +系统运行时碰到你使用JDBC的接口,就会在底层使用你引入的那个jar中提供的实现类。 + +## 2.2 Dubbo中的SPI +Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类,通过 ExtensionLoader,可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下 +![](https://img-blog.csdnimg.cn/20201220143821997.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +配置内容如下。 ```java -Protocol protocol = ExtensionLoader - .getExtensionLoader(Protocol.class) - .getAdaptiveExtension(); +Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); ``` -Dubbo要判断系统运行时,应该选用该Protocol接口的哪个实现类。 -它会去找一个你配置的Protocol,将你配置的Protocol实现类,加载进JVM,将其实例化。 +Dubbo要判断一下,在系统运行时,应该选用这个Protocol接口的哪个实现类。 +它会去找一个你配置的Protocol,将你配置的Protocol实现类,加载进JVM,将其实例化。 微内核,可插拔,大量的组件,Protocol负责RPC调用的东西,你可以实现自己的RPC调用组件,实现Protocol接口,给自己的一个实现类即可。 -这行代码就是Dubbo里大量使用的,对很多组件都保留一个接口和多个实现,然后在系统运行的时候动态根据配置去找到对应实现类。 -若你没配置,那就走默认实现。 -# 实例 -![](https://img-blog.csdnimg.cn/2020122014531547.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) +这行代码就是Dubbo里大量使用的,就是对很多组件,都是保留一个接口和多个实现,然后在系统运行的时候动态根据配置去找到对应的实现类。如果你没配置,那就走默认的实现。 + +### 2.2.1 实例 +![](https://img-blog.csdnimg.cn/2020122014531547.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) + 在Dubbo自己的jar里 在`/META_INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol`文件中: -![](https://img-blog.csdnimg.cn/20201220145724211.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20201220150004508.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20201220150358794.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20201220145724211.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20201220150004508.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +![](https://img-blog.csdnimg.cn/20201220150358794.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) 即可看到Dubbo的SPI机制默认流程,就是Protocol接口 -- **@SPI("dubbo")** +- @SPI("dubbo") 通过SPI机制提供实现类,实现类是通过将`dubbo`作为默认key去配置文件里找到的,配置文件名称为接口全限定名,通过`dubbo`作为key可以找到默认的实现类`org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol` > Dubbo的默认网络通信协议,就是dubbo协议,用的DubboProtocol > 在 Java 的 SPI 配置文件里每一行只有一个实现类的全限定名,在 Dubbo的 SPI配置文件中是 key=value 的形式,我们只需要对应的 key 就能加载对应的实现。 -# 源码 -```java + +### 实现源码 + +```csharp /** * 返回指定名字的扩展。如果指定名字的扩展不存在,则抛异常 {@link IllegalStateException}. */ @@ -91,7 +123,7 @@ private T createExtension(String name) { 比如这个Protocol接口搞了俩`@Adaptive`注解了方法,在运行时会针对Protocol生成代理类,该代理类的那俩方法中会有代理代码,代理代码会在运行时动态根据url中的protocol来获取key(默认是dubbo),也可以自己指定,如果指定了别的key,那么就会获取别的实现类的实例。通过这个url中的参数不同,就可以控制动态使用不同的组件实现类 -# 扩展Dubbo组件 +# 3 扩展Dubbo组件 自己写个工程,可以打成jar包的那种哦 - 里面的`src/main/resources`目录下 - 搞一个`META-INF/services` @@ -106,5 +138,10 @@ private T createExtension(String name) { 这个时候provider启动的时候,就会加载到我们jar包里的`my=com.javaedge.MyProtocol`这行配置,接着会根据你的配置使用你定义好的MyProtocol了,这个就是简单说明一下,你通过上述方式,可以替换掉大量的dubbo内部的组件,就是扔个你自己的jar包,然后配置一下即可~ - Dubbo的SPI原理图 ![](https://img-blog.csdnimg.cn/20190709133144886.png) -Dubbo中提供了大量的类似上面的扩展点。 -你要扩展一个东西,只需自己写个jar,让你的consumer或者是provider工程,依赖它,在你的jar里指定目录下配置好接口名称对应的文件,里面通过`key=实现类`然后对对应的组件,用类似``用你的哪个key对应的实现类来实现某个接口,你可以自己去扩展dubbo的各种功能,提供你自己的实现! \ No newline at end of file + +Dubbo中提供了大量的类似上面的扩展点. +你要扩展一个东西,只需自己写个jar,让你的consumer或者是provider工程,依赖它,在你的jar里指定目录下配置好接口名称对应的文件,里面通过`key=实现类`然后对对应的组件,用类似``用你的哪个key对应的实现类来实现某个接口,你可以自己去扩展dubbo的各种功能,提供你自己的实现! + +参考 +- 《Java工程师面试突击第1季》 +- https://dubbo.apache.org/zh-cn/docs/source_code_guide \ No newline at end of file diff --git "a/\350\201\214\344\270\232\345\217\221\345\261\225/Java\347\263\273\347\273\237\347\272\277\344\270\212\347\224\237\344\272\247\351\227\256\351\242\230\346\216\222\346\237\245\344\270\200\346\212\212\346\242\255.md" "b/\350\201\214\344\270\232\345\217\221\345\261\225/Java\347\263\273\347\273\237\347\272\277\344\270\212\347\224\237\344\272\247\351\227\256\351\242\230\346\216\222\346\237\245\344\270\200\346\212\212\346\242\255.md" deleted file mode 100644 index 601e23ba5b..0000000000 --- "a/\350\201\214\344\270\232\345\217\221\345\261\225/Java\347\263\273\347\273\237\347\272\277\344\270\212\347\224\237\344\272\247\351\227\256\351\242\230\346\216\222\346\237\245\344\270\200\346\212\212\346\242\255.md" +++ /dev/null @@ -1,178 +0,0 @@ -# 1 环境 -## 1.1 Dev -可以随意使用任何熟悉的工具排查。只要问题能重现,排查就不会太难,最多就是把程序调试到各种框架源码,所以这也是为何面试都会问源码,不求都看过,但要有思路知道如何去看能解决问题。 - -## 1.2 Test -比开发环境少了debug,不过也可使用jvisualvm或Arthas,附加到远程JVM进程。 - -还有测试环境是允许造数据来模拟我们需要的场景的哦,因此这时遇到问题记得主动沟通测试人员造数据让bug更容易复现。 - -## 1.3 Prd -该环境下开发人员的权限最低,所以排查问题时障碍很大: -- 无法使用调试工具从远程附加进程 -- 快速恢复为先,即使在结婚,也得赶紧修复线上问题。而且生产环境流量大、网络权限严格、调用链路复杂,因此更容易出问题,也是出问题最多的环境。 - -# 2 监控 -生产环境出现问题时,因为要尽快恢复应用,就不可能保留完整现场用于排查和测试。因此,是否有充足的信息(日志、监控和快照)可以了解历史、还原bug 场景。 -最常用的就是 ELK 的日志了,注意: -- 确保错误、异常信息可被完整记录到文件日志 -- 确保生产上程序的日志级别是INFO以上 -记录日志要使用合理的日志优先级,DEBUG用于开发调试、INFO用于重要流程信息、WARN用于需要关注的问题、ERROR用于阻断流程的错误 - -生产环境需开发配合运维才能做好完备监控: -## 主机维度 -对CPU、内存、磁盘、网络等资源做监控。如果应用部署在虚拟机或k8s集群,那么除了对物理机做基础资源监控外,同样还要对虚拟机或Pod监控。监控层数取决于应用的部署方案,有一层OS就要做一层监控。 -## 网络维度 -监控专线带宽、交换机基本情况、网络延迟 - -## 所有的中间件和存储都要做好监控 -不仅仅是监控进程对CPU、内存、磁盘IO、网络使用的基本指标,更重要的是监控组件内部的一些重要指标。比如最常用的Prometheus,就提供了大量exporter对接各种中间件和存储系统 -## 应用层面 -需监控JVM进程的类加载、内存、GC、线程等常见指标(比如使用Micrometer来做应用监控),此外还要确保能够收集、保存应用日志、GC日志 - -我们再来看看快照。这里的“快照”是指,应用进程在某一时刻的快照。通常情况下,我们会为生产环境的Java应用设置-XX:+HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath=…这2个JVM参数,用于在出现OOM时保留堆快照。这个课程中,我们也多次使用MAT工具来分析堆快照。 - -# 分析定位问题的最佳实践 -定位问题,首先要定位问题出在哪个层次:Java应用程序自身问题还是外部因素导致。 -- 可以先查看程序是否有异常,异常信息一般比较具体,可以马上定位到大概的问题方向 -- 如果是一些资源消耗型的问题可能不会有异常,我们可以通过指标监控配合显性问题点来定位。 - -一般问题原因可归类如下: -## 程序发布后 Bug -回滚,再慢慢通过版本差异分析根因。 -## 外部因素 -比如主机、中间件或DB问题。 -这种按主机层面问题、中间件或存储(统称组件)的问题分为: -### 主机层 -可使用工具排查: -#### CPU相关 -使用top、vmstat、pidstat、ps -#### 内存相关 -使用free、top、ps、vmstat、cachestat、sar -#### IO相关 -使用lsof、iostat、pidstat、sar、iotop、df、du -#### 网络相关 -使用ifconfig、ip、nslookup、dig、ping、tcpdump、iptables - -#### 组件 -从如下方面排查: -- 组件所在主机是否有问题 -- 组件进程基本情况,观察各种监控指标 -- 组件的日志输出,特别是错误日志 -- 进入组件控制台,使用一些命令查看其运作情况。 - -## 系统资源不够造成系统假死 -> 通常先通过重启和扩容解决问题,之后再分析,最好能留个快照。 - -系统资源不够,一般可能: -### CPU使用高 -若现场还在,具体分析流程: -- 在服务器执行`top -Hp pid` -查看进程中哪个线程CPU使用高 -- 输入大写的P将线程按照 CPU 使用率排序,并把明显占用CPU的线程ID转换为16进制 -- 在jstack命令输出的线程栈中搜索这个线程ID,定位出问题的线程当时的调用栈 - -若无法直接在服务器执行top,可采样定位:间隔固定时间运行一次jstack,采样几次后,对比采样得出哪些线程始终处于运行状态,找出问题线程。 - -若现场没了,可排除法分析。CPU使用高,一般是由下面的因素引起的: -- 突发压力 -可通过应用之前的负载均衡的流量或日志量确认,诸如Nginx等反向代理都会记录URL,可依靠代理的Access Log进行细化定位,也可通过监控观察JVM线程数的情况。压力问题导致CPU使用高的情况下,如果程序的各资源使用没有明显不正常,之后可以通过压测+Profiler(jvisualvm就有这个功能)进一步定位热点方法;如果资源使用不正常,比如产生了几千个线程,就需要考虑调参 - -- GC -可通过JVM监控GC相关指标、GC Log确认。如果确认是GC压力,那么内存使用也很可能会不正常,需要按照内存问题分析流程做进步分析。 -- 死循环或不正常处理流程 -可以结合应用日志分析。一般情况下,应用执行过程中都会产生一些日志,可以重点关注日志量异常部分。 -### 内存泄露或OOM -最简单的就是堆转储后使用MAT分析。堆转储,包含了堆现场全貌和线程栈信息,一般观察支配树图、直方图就可以马上看到占用大量内存的对象,可以快速定位到内存相关问题 -Java进程对内存的使用不仅仅是堆区,还包括线程使用的内存(线程个数*每一个线程的线程栈)和元数据区。每一个内存区都可能产生OOM,可以结合监控观察线程数、已加载类数量等指标分析 -注意看JVM参数的设置是否有明显不合理的,限制了资源。 - -### IO问题 -除非是代码问题引起的资源不释放等问题,否则通常都不是由Java进程内部因素引发的。 - -### 网络 -一般也是由外部因素引起。对于连通性问题,结合异常信息通常比较容易定位;对于性能或瞬断问题,可以先尝试使用ping等工具简单判断,如果不行再使用tcpdump或Wireshark。 - -# 迷茫时的最佳实践 -偶尔可能分析和定位难题,会迷失自我。如果你也这样,可参考如下经验 -## cause or result? -比如业务执行的很慢,而且线程数增多,那就可能是: -- 代码逻辑有问题、依赖的外部服务慢 -使得自己的业务逻辑执行缓慢,在访问量不变情况下,就需要更多线程处理。比如,10 TPS的并发原先一次请求1s即可完成,10个线程可支撑;现在执行完成需要10s,就需100个线程 -- 请求量增大 -使得线程数增多,应用本身CPU不足,上下文切换问题导致处理变慢 - -这时就需要多结合监控指标和各服务的入口流量,分析慢是cause or result。 - -## 探求规律 -如果没头绪,那就试试总结规律吧! -比如 -- 有一堆服务器做负载均衡,出问题时可分析监控和日志看请求是否是均匀分布的,可能问题都集中在某个机器节点上 -- 应用日志一般会记录线程名称,出问题时可分析日志是否集中在某类线程 -- 若发现应用开启大量TCP连接,通过netstat可分析出主要集中连接到哪个服务 - -探求到了规律,就很容易突破了。 - -## 调用拓扑 -比如看到Nginx返回502,一般可认为是下游服务的问题导致网关无法完成请求转发。 -对于下游服务,不能想当然就认为是我们的Java程序,比如在拓扑上可能Nginx代理的是Kubernetes的Traefik Ingress,链路是Nginx->Traefik->应用,如果一味排查Java程序的健康,则始终找不到根因。 - -有时虽然使用了Feign进行服务调用,出现连接超时也不一定就是服务端问题,有可能是客户端通过URL调用服务端,并非通过Eureka的服务发现实现的客户端负载均衡。即客户端连接的是Nginx代理而非直接连接应用,客户端连接服务出现的超时,其实是Nginx代理宕机所致。 - -## 资源限制 -观察各种监控指标,如果发现曲线慢慢上升然后稳定在一个水平线,一般就是资源达到瓶颈。 - -观察网络带宽曲线时,如果带宽上升到120MB左右不动了,很可能就是打满了1GB的网卡或传输带宽 -观察到数据库活跃连接数上升到10个不动了,很可能是连接池打满了 - -观察监控一旦看到任何这样曲线,都要引起重视。 - -## 连锁反应 -CPU、内存、IO和网络相辅相成,一个资源出现瓶颈,很可能同时引起其他资源连锁反应。 - -内存泄露后对象无法回收会造成大量Full GC,CPU会大量消耗在GC从而引起CPU使用增加 - -经常会把数据缓存在内存队列进行异步IO,网络或磁盘出现问题时,就很可能会引起内存暴涨。 - -所以出问题时,要综合考虑避免误判 -## 客户端or服务端or传输问题? -比如MySQL访问慢了,可能: -- 客户端原因,连接池不够导致连接获取慢、GC停顿、CPU占满 -- 传输过程问题 -包括光纤可能被挖断了呀、防火墙、路由表等设置有问题 -- 真的服务端背锅了 - -这都需要逐一排查区分。 - -服务端慢一般可以看到MySQL出慢日志,传输慢一般可以通过ping来简单定位,排除了这两个可能,并且仅仅是部分客户端出现访问慢的情况,就需要怀疑是客户端本身的问题。对于第三方系统、服务或存储访问出现慢的情况,不能完全假设是服务端的问题。 - -第七,快照类工具和趋势类工具需要结合使用。比如,jstat、top、各种监控曲线是趋势类工具,可以让我们观察各个指标的变化情况,定位大概的问题点;而jstack和分析堆快照的MAT是快照类工具,用于详细分析某一时刻应用程序某一个点的细节。 - -一般情况下,我们会先使用趋势类工具来总结规律,再使用快照类工具来分析问题。如果反过来可能就会误判,因为快照类工具反映的只是一个瞬间程序的情况,不能仅仅通过分析单一快照得出结论,如果缺少趋势类工具的帮助,那至少也要提取多个快照来对比。 - -第八,不要轻易怀疑监控。我曾看过一个空难事故的分析,飞行员在空中发现仪表显示飞机所有油箱都处于缺油的状态,他第一时间的怀疑是油表出现故障了,始终不愿意相信是真的缺油,结果飞行不久后引擎就断油熄火了。同样地,在应用出现问题时,我们会查看各种监控系统,但有些时候我们宁愿相信自己的经验,也不相信监控图表的显示。这可能会导致我们完全朝着错误的方向来排查问题。 - -如果你真的怀疑是监控系统有问题,可以看一下这套监控系统对于不出问题的应用显示是否正常,如果正常那就应该相信监控而不是自己的经验。 - -第九,如果因为监控缺失等原因无法定位到根因的话,相同问题就有再出现的风险,需要做好三项工作: - -做好日志、监控和快照补漏工作,下次遇到问题时可以定位根因; -针对问题的症状做好实时报警,确保出现问题后可以第一时间发现; -考虑做一套热备的方案,出现问题后可以第一时间切换到热备系统快速解决问题,同时又可以保留老系统的现场。 - -# 总结 -## 分析问题必须讲理 -靠猜是猜不出来的,需要提前做好基础监控的建设。监控的话,需要在基础运维层、应用层、业务层等多个层次进行。定位问题的时候,我们同样需要参照多个监控层的指标表现综合分析。 - -## 定位问题要先对原因进行大致分类 -比如是内部问题还是外部问题、CPU相关问题还是内存相关问题、仅仅是A接口的问题还是整个应用的问题,然后再去进一步细化探索,一定是从大到小来思考问题;在追查问题遇到瓶颈的时候,我们可以先退出细节,再从大的方面捋一下涉及的点,再重新来看问题。 - -## 经验很重要 -遇到重大问题的时候,往往也需要根据直觉来第一时间找到最有可能的点,这里甚至有运气成分。我还和你分享了我的九条经验,建议你在平时解决问题的时候多思考、多总结,提炼出更多自己分析问题的套路和拿手工具。 - -定位到问题原因后,要做好复盘回溯。每次故障的解决都是宝贵经验,复盘不止是记录问题,更是为了架构优化。 -复盘可重点关注如下: -- 记录完整的时间线、处理措施、上报流程等信息 -- 分析问题的根本原因 -- 给出短、中、长期改进方案,包括但不限于代码改动、SOP、流程,并记录跟踪每一个方案进行闭环 -- 定期组织团队回顾过去的故障 \ No newline at end of file diff --git "a/\350\201\214\344\270\232\345\217\221\345\261\225/\345\277\205\350\257\273\344\271\246\347\261\215.md" "b/\350\201\214\344\270\232\345\217\221\345\261\225/\345\277\205\350\257\273\344\271\246\347\261\215.md" deleted file mode 100644 index efc142ed09..0000000000 --- "a/\350\201\214\344\270\232\345\217\221\345\261\225/\345\277\205\350\257\273\344\271\246\347\261\215.md" +++ /dev/null @@ -1,357 +0,0 @@ -以下皆出自本人亲自翻阅过的书籍,体验良好,豆瓣大众也以为然,遂列举,以供后浪规划学习。 - -* * * - -1 JavaSE -======== - -1.1 基础 ------- - -### 《Java 核心技术:卷1 》 - -适合转行及大一的CS专业新生们 - -![](https://img-blog.csdnimg.cn/20200124010655430.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - - - -1.2 进阶 ------- - -### Java 编程思想 中文第四版 - -即使是最晦涩的概念,在Bruce Eckel的文字亲和力和小而直接的编程示例面前也会化解于无形。从Java的基础语法到最高级特性(深入的面向对象概念、多线程、自动项目构建、单元测试和调试等),本书都能逐步指导你轻松掌握。 - -作者拥有多年教学经验,对C、C++以及Java语言都有独到、深入的见解,以通俗易懂及小而直接的示例解释了一个个晦涩抽象的概念。包含了Java语言基础语法以及高级特性,适合各个层次的Java程序员阅读。 - -![](https://img-blog.csdnimg.cn/2020041022402693.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - - -###  On Java 8 (Java 编程思想 英文第五版) - -![](https://img-blog.csdnimg.cn/20200410224536738.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - - -### Effective Java中文版(第3版) - -90个条目,每个条目讨论Java程序设计中的一条规则。这些规则反映了最有经验的优秀程序员在实践中常用的一些有益的做法。 - -每一章都涉及软件设计的一个主要方面,并不一定需要按部就班地从头读到尾,每个条目都有一定程度的独立性。相互之间经常交叉引用,因此可以很容易地在书中找到自己需要的内容。 - -本书的目标是帮助读者更加有效地使用Java编程语言及其基本类库:java.lang、java.util和java.io,以及子包,如java.util.concurrent和java.util.function。 - -![](https://img-blog.csdnimg.cn/20200124010747616.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - -并发 --- - -### 《Java并发编程实战》 - -并发领域圣经,适合进阶选手的阅读,由 JDK 并发包作者亲自执笔,科学权威地讲解了并发的设计原理。 - -![](https://img-blog.csdnimg.cn/20200124012004430.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - -2 JVM -===== - -2.1 基础 ------- - -### 深入理解Java虚拟机(第3版) - -这是一部从工作原理和工程实践两个维度深入剖析JVM的著作,是计算机领域公认的经典,繁体版在台湾也颇受欢迎。 - -第3版在第2版的基础上做了重大修订,内容更丰富、实战性更强:根据新版JDK对内容进行了全方位的修订和升级,围绕新技术和生产实践新增逾10万字,包含近50%的全新内容,并对第2版中含糊、瑕疵和错误内容进行了修正。 - -![](https://img-blog.csdnimg.cn/20200124010454852.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - -Inside the Java 2 Virtual Machine -================================= - -3 操作系统(Linux) -============= - - 3.1 基础 -------- - -鳥哥的Linux私房菜(第四版) ----------------- - -![](https://img-blog.csdnimg.cn/20200124011312222.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - -深入理解计算机系统(原书第3版) ----------------- - -和第2版相比,本版内容上最大变化是,从以IA32和x86-64为基础转变为完全以x86-64为基础。主要更新如下: - -基于x86-64,大量地重写代码,首次介绍对处理浮点数据的程序的机器级支持。 - -处理器体系结构修改为支持64位字和操作的设计。 - -引入更多的功能单元和更复杂的控制逻辑,使基于程序数据流表示的程序性能模型预测更加可靠。 - -扩充关于用GOT和PLT创建与位置无关代码的讨论,描述了更加强大的链接技术(比如库打桩)。 - -增加了对信号处理程序更细致的描述,包括异步信号安全的函数等。 - -采用新函数,更新了与协议无关和线程安全的网络编程。 - -![](https://img-blog.csdnimg.cn/8af0446b4d08458991f91cb69286303d.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - - - - - -《**UNIX环境高级编程**》第三版 -------------------- - -被誉为UNIX编程“圣经”的Advanced Programming in the UNIX Environment一书的第3版。在本书第2版出版后的8年中,UNIX行业发生了巨大的变化,特别是影响UNIX编程接口的有关标准变化很大。本书在保持前一版风格的基础上,根据最新的标准对内容进行了修订和增补,反映了最新的技术发展。书中除了介绍UNIX文件和目录、标准I/O库、系统数据文件和信息、进程环境、进程控制、进程关系、信号、线程、线程控制、守护进程、各种I/O、进程间通信、网络IPC、伪终端等方面的内容,还在此基础上介绍了众多应用实例,包括如何创建数据库函数库以及如何与网络打印机通信等 - -![](https://img-blog.csdnimg.cn/20200124012046460.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - -4 Spring 框架 -=========== - -4.1 基础 ------- - -### Spring实战(第4版) - -入门经典书籍。第5版最新但是设计不适合初学者,所以推荐四版。适合刚开始学习Spring 框架的Java 开发人员快速上手。 - -![](https://img-blog.csdnimg.cn/20200926042752695.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -=================================================================================================================================================================================================== - -5 数据库(MySQL) -============ - -5.1 基础 ------- - -### 《SQL 必知必会》 - -本书是深受世界各地读者欢迎的SQL经典畅销书,内容丰富,文字简洁明快,针对Oracle、SQL Server、MySQL、DB2、PostgreSQL、SQLite等各种主流数据库提供了大量简明的实例。与其他同类图书不同,它没有过多阐述数据库基础理论,而是专门针对一线软件开发人员,直接从SQL SELECT开始,讲述实际工作环境中最常用和最必需的SQL知识,实用性极强。通过本书,读者能够从没有多少SQL经验的新手,迅速编写出世界级的SQL! - -![](https://img-blog.csdnimg.cn/20200124011613817.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - -### 《高性能 MySQL》第三版 - -![](https://img-blog.csdnimg.cn/20200124011712860.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - - - -6 Redis -======= - -6.1 基础 ------- - -### Redis设计与实现 - -![](https://img-blog.csdnimg.cn/20200124011831993.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9qYXZhZWRnZS5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70) - -### Redis开发与运维 - -本书全面讲解Redis基本功能及其应用,并结合线上开发与运维监控中的实际使用案例,深入分析并总结了实际开发运维中遇到的“陷阱”,以及背后的原因, 包含大规模集群开发与管理的场景、应用案例与开发技巧,为高效开发运维提供了大量实际经验和建议。本书不要求读者有任何Redis使用经验,对入门与进阶DevOps的开发者提供有价值的帮助。主要内容包括:Redis的安装配置、API、各种高效功能、客户端、持久化、复制、高可用、内存、哨兵、集群、缓存设计等,Redis高可用集群解决方案,Redis设计和使用中的问题,最后提供了一个开源工具:Redis监控运维云平台CacheCloud。 - -![](https://img-blog.csdnimg.cn/20200630094214256.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### Redis 深度历险:核心原理与应用实践 - -《Redis 深度历险:核心原理与应用实践》分为基础和应用篇、原理篇、集群篇、拓展篇、源码篇共 5 大块内容。基础和应用篇讲解对读者来说最有价值的内容,可以直接应用到实际工作中;原理篇、集群篇让开发者透过简单的技术表面看到精致的底层世界;拓展篇帮助读者拓展技术视野和夯实基础,便于进阶学习;源码篇让高阶的读者能够读懂源码,掌握核心技术实力。 - -适合人群:有 Redis 基础,渴望深度掌握 Redis 技术原理的中高级后端开发者;渴望成功进入大型互联网企业研发部的中高级后端开发者;需要支撑公司 Redis 中间件运维工作的初中级运维工程师;对 Redis 中间件技术好奇的中高级前端技术研究者。 - -![](https://img-blog.csdnimg.cn/20200630094508786.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -6.2 进阶 ------- - -### Redis5设计与源码分析 - -本书系统讲解Redis 5设计、数据结构、底层命令实现,以及持久化、主从复制、集群的实现。 - -![](https://img-blog.csdnimg.cn/20200630094640802.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -7 软件设计 -====== - - 7.1 基础 -------- - -### 《Head First设计模式》 - -![](https://img-blog.csdnimg.cn/20200630094740534.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### 大话设计模式 - -![](https://img-blog.csdnimg.cn/20200630094820433.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### 设计模式之禅(第2版) - -本书是设计模式领域公认的3本经典著作之一,“极具趣味,容易理解,但讲解又极为严谨和透彻”是本书的写作风格和方法的最大特点。深刻解读6大设计原则和28种设计模式的准确定义、应用方法和最佳实践,全方位比较各种同类模式之间的异同,详细讲解将不同的模式组合使用的方法。 - -![](https://img-blog.csdnimg.cn/20201011051617541.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### 代码整洁之道 - -本书提出:代码质量与其整洁度成正比。干净的代码,既在质量上较为可靠,也为后期维护、升级奠定了良好基础。本书给出一系列行之有效的整洁代码操作实践,并辅以来自现实项目的正、反两面的范例。 - -遵循这些规则,就能编写出干净的代码,有效提升代码质量。涵盖从命名到重构的多个编程方面。 - -![](https://img-blog.csdnimg.cn/20211006015429607.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -### UNIX编程艺术 - -本书主要介绍了Unix系统领域中的设计和开发哲学、思想文化体系、原则与经验,由公认的Unix编程大师、开源运动领袖人物之一Eric S. Raymond倾力多年写作而成。包括Unix设计者在内的多位领域专家也为本书贡献了宝贵的内容。本书内容涉及社群文化、软件开发设计与实现,覆盖面广、内容深邃,完全展现了作者极其深厚的经验积累和领域智慧。 - -![](https://img-blog.csdnimg.cn/20211010013840628.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - - - -8 架构 -==== - -8.1 基础 ------- - -### 《大型网站技术架构:核心原理与案例分析》\- 面试架构知识点核心书籍 - -通过梳理大型网站技术发展历程,剖析大型网站技术架构模式,深入讲述大型互联网架构设计的核心原理,并通过一组典型网站技术架构设计案例,为读者呈现一幅包括技术选型、架构设计、性能优化、Web 安全、系统发布、运维监控等在内的大型网站开发全景视图。了解大型网站的解决方案和开发理念。 - -![](https://img-blog.csdnimg.cn/20200926043702445.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -8.2 进阶 ------- - -### 亿级流量网站架构核心技术 - -京东架构师经验之谈,总结并梳理了亿级流量网站高可用和高并发原则,通过实例详细介绍了如何落地这些原则。 - -分为四部分:概述、高可用原则、高并发原则、案例实战。从负载均衡、限流、降级、隔离、超时与重试、回滚机制、压测与预案、缓存、池化、异步化、扩容、队列等多方面详细介绍了亿级流量网站的架构核心技术,让读者看后能快速运用到实践项目中。  - -![](https://img-blog.csdnimg.cn/20201130145340753.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### 企业应用架构模式 - -本书作者是当今面向对象软件开发的权威,他在一组专家级合作者的帮助下,将40多种经常出现的解决方案转化成模式,最终写成这本能够应用于任何一种企业应用平台的、关于解决方案的、不可或缺的手册。本书获得了2003年度美国软件开发杂志图书类的生产效率奖和读者选择奖。本书分为两大部分。第一部分是关于如何开发企业应用的简单介绍。第二部分是本书的主体,是关于模式的详细参考手册,每个模式都给出使用方法和实现信息 - -![](https://img-blog.csdnimg.cn/20201002040637862.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### 架构整洁之道 - -创造“Clean神话”的Bob大叔在架构领域的登峰之作,围绕“架构整洁”这一重要导向,系统地剖析其缘起、内涵及应用场景,涵盖软件研发完整过程及所有核心架构模式。 - -![](https://img-blog.csdnimg.cn/20211005030603809.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -========================================================================================================================================================================================= - -### 数据密集型应用系统设计 - -全书分为三大部分: - -第一部分,主要讨论有关增强数据密集型应用系统所需的若干基本原则。首先开篇第1章即瞄准目标:可靠性、可扩展性与可维护性,如何认识这些问题以及如何达成目标。第2章我们比较了多种不同的数据模型和查询语言,讨论各自的适用场景。接下来第3章主要针对存储引擎,即数据库是如何安排磁盘结构从而提高检索效率。第4章转向数据编码(序列化)方面,包括常见模式的演化历程。 - -第二部分,我们将从单机的数据存储转向跨机器的分布式系统,这是扩展性的重要一步,但随之而来的是各种挑战。所以将依次讨论数据远程复制(第5章)、数据分区(第6章)以及事务(第7章)。接下来的第8章包括分布式系统的更多细节,以及分布式环境如何达成一致性与共识(第9章)。 - -第三部分,主要针对产生派生数据的系统,所谓派生数据主要指在异构系统中,如果无法用一个数据源来解决所有问题,那么一种自然的方式就是集成多个不同的数据库、缓存模块以及索引模块等。首先第10章以批处理开始来处理派生数据,紧接着第11章采用流式处理。第12章总结之前介绍的多种技术,并分析讨论未来构建可靠、可扩展和可维护应用系统可能的新方向或方法。 - -![](https://img-blog.csdnimg.cn/20211006191349733.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - - - -9 DDD -===== - - 9.1 基础 -------- - -### 领域驱动设计模式、原理与实践 - -![](https://img-blog.csdnimg.cn/20200926052219965.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### 实现领域驱动设计 - -领域驱动设计(DDD)教我们如何做好软件的,同时也是教我们如何更好地使用面向对象技术的。它为我们提供了设计软件的全新视角,同时也给开发者留下了一大难题:如何将领域驱动设计付诸实践?Vaughn Vernon 的这本《实现领域驱动设计》为我们给出了全面的解答。 - -《实现领域驱动设计》分别从战略和战术层面详尽地讨论了如何实现DDD,其中包含了大量的最佳实践、设计准则和对一些问题的折中性讨论。《实现领域驱动设计》共分为14 章,在DDD 战略部分,《实现领域驱动设计》向我们讲解了领域、限界上下文、上下文映射图和架构等内容,战术部分包括实体、值对象、领域服务、领域事件、聚合和资源库等内容。一个虚构的案例研究贯穿全书,这对于实例讲解DDD 实现来说非常有用。 - -《实现领域驱动设计》在DDD 的思想和实现之间建立起了一座桥梁,架构师和程序员均可阅读,同时也可以作为一本DDD 参考书。 - -![](https://img-blog.csdnimg.cn/20200926044228475.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -9.2 进阶 ------- - -### 领域驱动设计 - -领域驱动设计方面的经典之作。全书围绕设计和开发实践,结合项目案例,向读者阐述如何在真实的软件开发中应用领域驱动设计。给出了领域驱动设计的系统化方法,并将人们普遍接受的一些实践综合到一起,融入了作者的见解和经验,展现了一些可扩展的设计新实践、已验证过的技术以及便于应对复杂领域的软件项目开发的基本原则。 - -![](https://img-blog.csdnimg.cn/20200926044555766.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - - -10 计算机网络 -======== - -10.1 基础 -------- - -### 《图解HTTP》 - -本书对HTTP协议进行全面系统介绍。作者由HTTP协议的发展历史娓娓道来,严谨细致地剖析了HTTP协议的结构,列举诸多常见通信场景及实战案例,最后延伸到Web安全、最新技术动向等方面。本书的特色为在讲解的同时,辅以大量生动形象的通信图例,更好地帮助读者深刻理解HTTP通信过程中客户端与服务器之间的交互情况。读者可通过本书快速了解并掌握HTTP协议的基础,前端工程师分析抓包数据,后端工程师实现REST API、实现自己的HTTP服务器等过程中所需的HTTP相关知识点本书均有介绍。 - -![](https://img-blog.csdnimg.cn/202009260447331.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -11 数据结构与算法 -========== - -11.1 基础 -------- - -### 算法(第4版) - -Sedgewick畅销著作的最新版,反映了经过几十年演化而成的算法核心知识体系,全面论述排序、搜索、图处理和字符串处理的算法和数据结构,涵盖每位程序员应知应会的50种算法,全新的Java实现代码,采用模块化的编程风格,所有代码均可供读者使用。 - -![](https://img-blog.csdnimg.cn/20200703165148393.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### 大话数据结构 - -本书为超级畅销书《大话设计模式》作者程杰潜心三年推出的扛鼎之作!以一个计算机教师教学为场景,讲解数据结构和相关算法的知识。通篇以一种趣味方式来叙述,大量引用了各种各样的生活知识来类比,并充分运用图形语言来体现抽象内容,对数据结构所涉及到的一些经典算法做到逐行分析、多算法比较。与市场上的同类数据结构图书相比,本书内容趣味易读,算法讲解细致深刻,是一本非常适合自学的读物。 - -本书以一个计算机教师教学为场景,讲解数据结构和相关算法的知识。通篇以一种趣味方式来叙述,大量引用了各种各样的生活知识来类比,并充分运用图形语言来体现抽象内容,对数据结构所涉及到的一些经典算法做到逐行分析、多算法比较。与市场上的同类数据结构图书相比,本书内容趣味易读,算法讲解细致深刻,是一本非常适合自学的读物。 - -![](https://img-blog.csdnimg.cn/20200703165428511.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - - -11.2 进阶 -------- - -### 程序员代码面试指南(第2版) - -程序员代码面试"神书”!书中对IT名企代码面试各类题目的最优解进行了总结,并提供了相关代码实现。针对当前程序员面试缺乏权威题目汇总这一痛点,本书选取将近300道真实出现过的经典代码面试题,帮助广大程序员的面试准备做到接近万无一失。"刷”完本书后,你就是"题王”!《程序员代码面试指南:IT名企算法与数据结构题目最优解(第2版)》采用题目解答的方式组织内容,并把面试题类型相近或者解法相近的题目尽量放在一起,读者在学习本书时很容易看出面试题解法之间的联系,使知识的学习避免碎片化。本书所收录的所有面试题都给出了最优解讲解和代码实现,并且提供了一些普通解法和最优解法的运行时间对比,让读者真切地感受到最优解的魅力!书中收录了大量新题和最优解分析,这些内容源自笔者多年来"死磕自己”的深入思考。提升算法和数据结构等方面能力。 - -![](https://img-blog.csdnimg.cn/2020070316532482.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -12 网络编程 -======= - -12.1 基础 -------- - -### Netty 实战 - -本书中文版基于Netty4.1.9做了修订。Netty之父”Trustin Lee作序推荐。无论是构建高性能的Web、游戏服务器、推送系统、RPC框架、消息中间件还是分布式大数据处理引擎,都离不开Netty,在整个行业中,Netty广泛而成功的应用,使其成为了Java高性能网络编程的卓绝框架。无论是想要学习Spring 5 、Spark、Cassandra等这样的系统,还是通过学习Netty来构建自己的基于Java的高性能网络框架,或者是更加具体的高性能Web或者游戏服务器等,本书都将是你的超强拍档。 - -![](https://img-blog.csdnimg.cn/20201030002004281.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -13 消息队列 -======= - -RabbitMQ 实战 ------------ - -![](https://img-blog.csdnimg.cn/20210703182055114.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\344\275\240\346\200\216\344\271\210\346\200\273\346\230\257\350\203\275\345\206\231\345\207\272\344\270\244\344\270\211\345\215\203\350\241\214\347\232\204controller\347\261\273\357\274\237.md" "b/\351\207\215\346\236\204/\344\275\240\346\200\216\344\271\210\346\200\273\346\230\257\350\203\275\345\206\231\345\207\272\344\270\244\344\270\211\345\215\203\350\241\214\347\232\204controller\347\261\273\357\274\237.md" deleted file mode 100644 index 8fe5608175..0000000000 --- "a/\351\207\215\346\236\204/\344\275\240\346\200\216\344\271\210\346\200\273\346\230\257\350\203\275\345\206\231\345\207\272\344\270\244\344\270\211\345\215\203\350\241\214\347\232\204controller\347\261\273\357\274\237.md" +++ /dev/null @@ -1,126 +0,0 @@ -你一定经常见到一个两三千行的 controller 类,类之所以发展成如此庞大,有如下原因: -- 长函数太多 -- 类里面有特别多的字段和函数 -量变引起质变,可能每个函数都很短小,但数量太多 -# 1 程序的modularity -你思考过为什么你不会把all code写到一个文件?因为你的潜意识里明白: -- 相同的功能模块无法复用 -- 复杂度远超出个人理解极限 - -一个人理解的东西是有限的,在国内互联网敏捷开发环境下,更没有人能熟悉所有代码细节。 - -解决复杂的最有效方案就是分而治之。所以,各种程序设计语言都有自己的模块划分(**modularity**)方案: -- 从最初的按文件划分 -- 到后来使用OO按类划分 - -开发者面对的不再是细节,而是模块,模块数量显然远比细节数量少,理解成本大大降低,开发效率也提高了,再也不用 996, 每天都能和妹纸多聊几句了。 - -modularity,本质就是分解问题,其背后原因,就是个人理解能力有限。 - -> 说这么多我都懂,那到底怎么把大类拆成小类? - -# 2 大类是怎么来的? -## 2.1 职责不单一 -**最容易产生大类的原因**。 - -CR一段代码: -![](https://img-blog.csdnimg.cn/6544c51823ed4e45a1182220fae18d03.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -该类持有大类的典型特征,包含一坨字段:这些字段都缺一不可吗? -- userId、name、nickname等应该是一个用户的基本信息 -- email、phoneNumber 也算是和用户相关联 -很多应用都提供使用邮箱或手机号登录方式,所以,这些信息放在这里,也能理解 -- authorType,作者类型,表示作者是签约作者还是普通作者,签约作者可设置作品的付费信息,但普通作者无此权限 -- authorReviewStatus,作者审核状态,作者成为签约作者,需要有一个申请审核的过程,该状态字段就是审核状态 -- editorType,编辑类型,编辑可以是主编,也可以是小编,权限不同 - -这还不是 User 类的全部。但只看这些内容就能看出问题: -- 普通用户既不是作者,也不是编辑 -作者和编辑这些相关字段,对普通用户无意义 -- 对那些成为作者的用户,编辑的信息意义不大 -因为作者不能成为编辑。编辑也不会成为作者,作者信息对成为编辑的用户无意义 - -总有一些信息对一部分人毫无意义,但对另一部分人又必需。出现该问题的症结在于只有“一个”用户类。 - -普通用户、作者、编辑,三种不同角色,来自不同业务方,关心的是不同内容。仅因为它们都是同一系统的用户,就把它们都放到一个用户类,导致任何业务方的需求变动,都会反复修改该类,**严重违反单一职责原则**。 -所以破题的关键就是职责拆分。 - -虽然这是一个类,但它把不同角色关心的东西都放在一起,就愈发得臃肿了。 - -只需将不同信息拆分即可: -```java -public class User { - private long userId; - private String name; - private String nickname; - private String email; - private String phoneNumber; - ... -} - -public class Author { - private long userId; - private AuthorType authorType; - private ReviewStatus authorReviewStatus; - ... -} - -public class Editor { - private long userId; - private EditorType editorType; - ... -} -``` -拆出 Author、Editor 两个类,将和作者、编辑相关的字段分别移至这两个类里。 -这俩类分别有个 userId 字段,用于关联该角色和具体用户。 -## 2.2 字段未分组 -有时觉得有些字段确实都属于某个类,结果就是,这个类还是很大。 - -之前拆分后的新 User 类: -```java -public class User { - private long userId; - private String name; - private String nickname; - private String email; - private String phoneNumber; - ... -} -``` -这些字段应该都算用户信息的一部分。但依然也不算是个小类,因为该类里的字段并不属于同一种类型的信息。 -如,userId、name、nickname算是用户的基本信息,而 email、phoneNumber 则属于用户的联系方式。 - -需求角度看,基本信息是那种一旦确定一般就不变的内容,而联系方式则会根据实际情况调整,如绑定各种社交账号。把这些信息都放到一个类里面,类稳定程度就差点。 - -据此,可将 User 类的字段分组: -```java -public class User { - private long userId; - private String name; - private String nickname; - private Contact contact; - ... -} - -public class Contact { - private String email; - private String phoneNumber; - ... -} -``` -引入一个 Contact 类(联系方式),把 email 和 phoneNumber 放了进去,后面再有任何关于联系方式的调整就都可以放在这个类里面。 -此次调整,把不同信息重新组合,但每个类都比原来要小。 - -前后两次拆分到底有何不同? -- 前面是根据职责,拆分出不同实体 -- 后面是将字段做了分组,用类把不同的信息分别封装 - -大类拆解成小类,本质上是个设计工作,依据单一职责设计原则。 - -若把大类都拆成小类,类的数量就会增多,那人们理解的成本是不是也会增加呢? -**这也是很多人不拆分大类的借口。** - -各种程序设计语言中,本就有如包、命名空间等机制,将各种类组合在一起。在你不需要展开细节时,面对的是一个类的集合。 -再进一步,还有各种程序库把这些打包出来的东西再进一步打包,让我们只要面对简单的接口,而不必关心各种细节。 - -软件正这样层层封装构建出来的。 \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\346\234\200\345\245\275\347\232\204\347\274\226\347\250\213\350\257\255\350\250\200\346\230\257\346\200\216\346\240\267\347\232\204?.md" "b/\351\207\215\346\236\204/\346\234\200\345\245\275\347\232\204\347\274\226\347\250\213\350\257\255\350\250\200\346\230\257\346\200\216\346\240\267\347\232\204?.md" deleted file mode 100644 index ad8c2bd0c4..0000000000 --- "a/\351\207\215\346\236\204/\346\234\200\345\245\275\347\232\204\347\274\226\347\250\213\350\257\255\350\250\200\346\230\257\346\200\216\346\240\267\347\232\204?.md" +++ /dev/null @@ -1,56 +0,0 @@ -> 没有语言是完美的。 - -因语言演化,不同时期不同版本的程序员写的代码,在用同一门语言在编程。所以,我们经常看到各种不同时期风格代码并存。 - -新的语言特性都是为提高代码表达性,减少犯错几率。多用新语言特性写代码,绝对没毛病! - -> 那应该如何使用“新”语言特性,让代码写得更好? -# Optional -![](https://img-blog.csdnimg.cn/4b605b543d3e47198645f4fd71780cd2.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -暂不考虑缺乏封装问题。这段代码有问题。因为未考虑对象可能为 null。 -更好的写法: -![](https://img-blog.csdnimg.cn/a73f3114268c42c5b601fb0fff3af399.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -这种写法很稀缺,所以,新项目总是各种NPE。如果你要问程序员为什么不写对象为 null 的判断,答曰:忘了。 - -> 空指针的发明者 Tony Hoare 将其称为“自己犯下的十亿美元错误”。 - -还好Java 8有Optional,它提供了一个对象容器,你需要从中“取出(get)”你所需要对象,但取出前,你需判断该对象容器中是否真的存在一个对象。 -![](https://img-blog.csdnimg.cn/e2a57e7b800d42999c06093ddc8e468a.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -你再不会忘掉判断对象是否存在,因为你需要从 Optional 取出存在里面的对象。正是这**多余的**一步,避免你“忘”了。 - -更简洁的写法: -![](https://img-blog.csdnimg.cn/d4376180d76143b6a8772ec32071d89c.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -项目中所有可能为 null 的返回值,都要返回 Optional,可大大减少各种意外惊喜。 -# 函数式编程 -![](https://img-blog.csdnimg.cn/662b603e9ed742d4a15a58cb2a7d0c8a.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -准备参数的代码: -- 筛选出审核通过的章节 -- 再把章节转换成与翻译引擎通信的格式 -- 最后把所有得到的单个参数打包成一个完整的章节参数。 - -Java8后,不是不需要遍历集合,而是有了更好的遍历集合方式。函数式编程,大部分操作都可归结成列表转换,最核心的列表转换就是 map、filter 和 reduce。 - -大部分循环语句都是在对一个元素集合进行操作,而这些操作基本上都可以用列表操作进行替代。 - -再CR这段代码,有一循环语句,这循环语句在处理的是一个集合中的元素,可用列表转换: -![](https://img-blog.csdnimg.cn/be9db1cc30e04b2286dd71e5fcfd99e0.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) - -有人可能说这段代码还不如我原来的循环语句简单。两种写法根本差异是抽象层次不同,可读性完全不同: -- 循环语句是在描述实现细节 -必须要做一次“阅读理解”知晓其中细节才能知晓整个场景 -- 列表转换的写法是在描述做什么 -基本上和我们用语言叙述过程对应。 - -> 其实大多数人选择循环语句只是因为对列表转换不熟练,多写即可。 - -为什么我的感觉实践中,使用这种风格,为写出来的代码更难理解? -你在列表转换过程中写了太多代码!很多人直接在列表转换过程中写 lambda。lambda 本身相当于一个匿名函数,所以,很多人写成长函数了。 -lambda 是为了写短小代码提供的便利,所以,lambda 中写出大片代码,根本就是违反 lambda 设计初衷的。最好的 lambda 应只有一行代码。 - -> 那若一个转换过程中就有很多操作咋办? - -提取出一个函数!就像 toSectionParameter:完成从 Section 到 SectionParameter 转换。这样一来,列表转换的本身就完全变成了一个声明,这样的写法才是能发挥出列表转换价值的写法。 -# 总结 -代码风格逐步演化,每个程序员对语言的理解程度都有所差异,所以,我们的屎山项目中,各种代码风格并存,各具风骚,加重代码理解难度,这其实就是:不一致的坏味道。 - -编程风之所以格会过时,是因为它存在问题,新风格就是用更好方案,注意跟上时代,拥抱变化,多用新特性! \ No newline at end of file diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\255\226\347\225\245\346\250\241\345\274\217(Strategy-Pattern).md" "b/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\255\226\347\225\245\346\250\241\345\274\217(Strategy-Pattern).md" index ef6bcb6e23..4e0c46e216 100644 --- "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\255\226\347\225\245\346\250\241\345\274\217(Strategy-Pattern).md" +++ "b/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\347\255\226\347\225\245\346\250\241\345\274\217(Strategy-Pattern).md" @@ -1,106 +1,101 @@ -[相关源码](https://github.com/Wasabi1234/Java-DesignPatterns-Tuitorial) -# 1 简介 -## 1.1 定义 +# 0.0 相关源码链接 +https://github.com/Wasabi1234/design-patterns + +# 1 定义 +![](https://upload-images.jianshu.io/upload_images/4685968-f3e6ce1684ece913.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 也叫做政策模式(Policy Pattern) -- wiki -对象有某个行为,但是在不同的场景中,该行为有不同的实现算法.。比如每个人都要“交个人所得税”,但是“在美国交个人所得税”和“在中国交个人所得税”就有不同的算税方法. +- 维基百科 +对象有某个行为,但是在不同的场景中,该行为有不同的实现算法. +比如每个人都要“交个人所得税”,但是“在美国交个人所得税”和“在中国交个人所得税”就有不同的算税方法. - 定义 Define a family of algorithms,encapsulate each one,and make them interchangeable. -定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。 +定义一组算法,将每个算法都封装起来,并且使它们之间可以互换. -常见 if/else 结构。 -## 1.2 类型 -行为型。 -在`运行时`(**非编译时**)改变软件的算法行为。 +在`运行时`(非编译时)改变软件的算法行为 +- 主要思想 +定义一个通用的问题,使用不同的算法来实现,然后将这些算法都封装在一个统一接口的背后. -## 1.3 主要思想 -定义一个通用的问题,使用不同的算法来实现,然后将这些算法都封装在一个统一接口。 +![策略模式的通用类图](https://upload-images.jianshu.io/upload_images/4685968-ad1caf184324decf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +策略模式使用的就是面向对象的继承和多态机制 -策略模式使用的就是OOP的继承和多态。 +策略模式的三个角色 +● Context 封装角色 +也叫做上下文角色,起承上启下封装作用; +屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化. -## 1.4 主要角色 -### 通用类图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWFkMWNhZjE4NDMyNGRlY2YucG5n?x-oss-process=image/format,png) +● Strategy抽象策略角色 +策略、算法家族的抽象,通常为接口,定义每个策略或算法必须具有的方法和属性 -- Context 封装角色 -即上下文角色,起承上启下的封装作用。屏蔽高层模块对策略&算法的直接访问,封装可能存在的变化。 +● ConcreteStrategy具体策略角色 +实现抽象策略中的操作,含有具体的算法 -- Strategy 抽象策略角色 -策略&算法家族的抽象,通常为接口,定义每个策略或算法必须具有的方法和属性。 +### 通用源码 +- 抽象策略角色,它是一个非常普通的接口,在我们的项目中就是一个普通得不能再普通的接口了,定义一个或多个具体的算法 -- ConcreteStrategy 具体策略角色 -实现抽象策略中的操作,含有具体的算法。 -### 通用源码 -- 抽象策略角色 -一个非常普通的接口,在项目中就是一个普通接口,定义一或多个具体算法。 # 2 适用场景 -一个对象,其行为有些固定不变,有些又容易变化。对于这些容易变化的行为,我们不希望将其实现绑定在对象中,而希望能够动态地针对不同场景产生不同应对的策略。 -这时就要用到策略模式,就是为了应对对象中复杂多变的行为而产生的: -- 系统有很多类,而他们的区别仅在于行为不同 +针对一个对象,其行为有些是固定的不变的,有些是容易变化的,针对不同情况有不同的表现形式。那么对于这些容易变化的行为,我们不希望将其实现绑定在对象中,而是希望以动态的形式,针对不同情况产生不同的应对策略。那么这个时候就要用到策略模式了。简言之,策略模式就是为了应对对象中复杂多变的行为而产生的。 + +- 系统有很多类,而他们的区别仅仅在于他们的行为不同 - 一个系统需要动态地在几种算法中选择一种 + # 3 优点 - 符合开闭原则 - 避免使用多重条件转移语句 -e.g. 省去大量 if/else、switch,降低代码耦合度 +比如省去大量的 if/else 和 switch 语句,降低代码的耦合 - 提高算法的保密性和安全性 -只需知道策略的业务功能,而不关心内部实现 +只需知道策略的作用,而不关心内部实现 + # 4 缺点 -- 客户端必须知道所有的策略类,并决定使用哪个策略类 +- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类 - 产生很多策略类 # 5 相关设计模式的差异 -## 5.1 V.S 工厂模式 +## 策略模式和工厂模式 - 行为型 -接收已经创建好的对象,从而实现不同的行为 +接收已经创建好的对象,从而实现不同的行为 - 创造型 -接收指令,创建符合要求的具体对象 - -## 5.2 V.S 状态模式 -- 若系统中某类的某行为存在多种实现方式,客户端需知道到底使用哪个策略 -- 若系统中某对象存在多种状态,不同状态下的行为又具有差异,状态之间会自动转换,客户端不需要关心具体状态 -## 5.3 V.S 模板模式 -- 策略模式:只有选择权(由用户自己选择已有的固定算法) -- 模板模式,侧重点不是选择,你没得选择,你必须这么做。你可以参与某一部分内容自定义 +接收指令,创建出符合要求的具体对象 + +## 策略模式和状态模式 +- 若系统中某个类的某个行为存在多种实现方式,客户端需要知道到底使用哪个策略 +- 若系统中某个对象存在多种状态,不同状态下的行为又具有差异性,状态之间会自动转换,客户端不需要关心具体状态 + # 6 实战 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTBlYmMwOGY0MWUwN2NkY2EucG5n?x-oss-process=image/format,png) -- 促销策略接口 -![](https://img-blog.csdnimg.cn/20201104133917501.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -- 返现策略 -![](https://img-blog.csdnimg.cn/20201104134155926.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -- 立减策略 -![](https://img-blog.csdnimg.cn/2020110413472547.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -- 满减策略 -![](https://img-blog.csdnimg.cn/20201104135011162.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center) -- 测试类 -![](https://img-blog.csdnimg.cn/20201104135935601.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70#pic_center) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTg0NDA3NWYwMWE5ZTM0OWIucG5n?x-oss-process=image/format,png) +![](https://upload-images.jianshu.io/upload_images/4685968-0ebc08f41e07cdca.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +![](https://upload-images.jianshu.io/upload_images/4685968-98e2b70fe0d9a3f0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-ecbce7b0043a7490.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-5dab16664b2d6639.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-57e3f0490d67cfb0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-8a75a258378f8a69.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![image.png](https://upload-images.jianshu.io/upload_images/4685968-844075f01a9e349b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 改造后的测试类 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTQ5OTFkMmVhYWQ5MzU3YzEucG5n?x-oss-process=image/format,png) +![](https://upload-images.jianshu.io/upload_images/4685968-4991d2eaad9357c1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 可见 if/else 语句过多,采取策略+工厂模式结合 - 策略工厂 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTIzMDA4OGNhMjYwZGIyNTYucG5n?x-oss-process=image/format,png) +![](https://upload-images.jianshu.io/upload_images/4685968-230088ca260db256.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - 最新测试类 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWFjZjgwZGE0ZmE1ZWE5NTQucG5n?x-oss-process=image/format,png) +![](https://upload-images.jianshu.io/upload_images/4685968-acf80da4fa5ea954.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - 输出结果 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTdkMjYwMzNhMWIzOWJkNmEucG5n?x-oss-process=image/format,png) +![](https://upload-images.jianshu.io/upload_images/4685968-7d26033a1b39bd6a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) # 7 源码应用解析 ## JDK中的比较器接口 - 策略比较器 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTMwNzY2Njg5NmMzZDE4MDAucG5n?x-oss-process=image/format,png) -![具体策略](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQ5MjhkZDE2YmVhNDRhNjAucG5n?x-oss-process=image/format,png) +![](https://upload-images.jianshu.io/upload_images/4685968-307666896c3d1800.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![具体策略](https://upload-images.jianshu.io/upload_images/4685968-d928dd16bea44a60.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 比如Arrays类中的 sort 方法通过传入不同比较接口器的实现达到不同排序策略 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWY5MjA3MzcxMmUzMGNlNjYucG5n?x-oss-process=image/format,png) +![](https://upload-images.jianshu.io/upload_images/4685968-f92073712e30ce66.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## JDK中的TreeMap 类似于促销活动中有促销策略对象,在T reeMap 中也有比较器对象 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTQyNGY3ODdkYTE3ZDQ4NzYucG5n?x-oss-process=image/format,png) +![](https://upload-images.jianshu.io/upload_images/4685968-424f787da17d4876.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) compare 方法进步加工 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTMyZTAyNDU2NTQyYzFlNDgucG5n?x-oss-process=image/format,png) +![](https://upload-images.jianshu.io/upload_images/4685968-32e02456542c1e48.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## Spring 中的Resource 不同访问策略 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTY2ZDYxOTExNzdmYWFmMmEucG5n?x-oss-process=image/format,png) +![](https://upload-images.jianshu.io/upload_images/4685968-66d6191177faaf2a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ## Spring 中bean 的初始化ceInstantiationStrategy - 两种 bean 的初始化策略 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LThmYTVlNDRlNDkxYWFmZGMucG5n?x-oss-process=image/format,png) \ No newline at end of file +![](https://upload-images.jianshu.io/upload_images/4685968-8fa5e44e491aafdc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) diff --git "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\256\277\351\227\256\350\200\205\350\256\276\350\256\241\346\250\241\345\274\217(Visitor).md" "b/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\256\277\351\227\256\350\200\205\350\256\276\350\256\241\346\250\241\345\274\217(Visitor).md" deleted file mode 100644 index 79a79b824c..0000000000 --- "a/\351\207\215\346\236\204/\350\256\276\350\256\241\346\250\241\345\274\217/\350\256\276\350\256\241\346\250\241\345\274\217\345\256\236\346\210\230-\350\256\277\351\227\256\350\200\205\350\256\276\350\256\241\346\250\241\345\274\217(Visitor).md" +++ /dev/null @@ -1,150 +0,0 @@ -# 1 简介 -## 1.1 定义 -封装某些作用于某种数据结构中各元素的操作,它可以在不改变数据结构的前提下定义作用于这些数据元素的新的操作 - -## 思想 -将数据结构和数据操作分离 - -## 目的 -稳定的数据结构和易变的操作的解耦 - -## 适用场景 -假如一个对象中存在着一些与本对象不相干(或者关系较弱)的操作,可以使用访问者模式把这些操作封装到访问者中去,这样便避免了这些不相干的操作污染这个对象。 - -假如一组对象中,存在着相似的操作,可以将这些相似的操作封装到访问者中去,这样便避免了出现大量重复的代码 - -访问者模式适用于对功能已经确定的项目进行重构的时候适用,因为功能已经确定,元素类的数据结构也基本不会变了;如果是一个新的正在开发中的项目,在访问者模式中,每一个元素类都有它对应的处理方法,每增加一个元素类都需要修改访问者类,修改起来相当麻烦。 - -# 2 示例 -如果老师教学反馈得分大于等于85分、学生成绩大于等于90分,则可以入选成绩优秀奖;如果老师论文数目大于8、学生论文数目大于2,则可以入选科研优秀奖。 - -在这个例子中,老师和学生就是Element,他们的数据结构稳定不变。从上面的描述中,我们发现,对数据结构的操作是多变的,一会儿评选成绩,一会儿评选科研,这样就适合使用访问者模式来分离数据结构和操作。 - -## 2.1 创建抽象元素 - -```java -public interface Element { - void accept(Visitor visitor); -} -``` - -## 2.2 创建具体元素 -创建两个具体元素 Student 和 Teacher,分别实现 Element 接口 - -```java -public class Student implements Element { - private String name; - private int grade; - private int paperCount; - - public Student(String name, int grade, int paperCount) { - this.name = name; - this.grade = grade; - this.paperCount = paperCount; - } - - @Override - public void accept(Visitor visitor) { - visitor.visit(this); - } - - ...... - -} -public class Teacher implements Element { - private String name; - private int score; - private int paperCount; - - public Teacher(String name, int score, int paperCount) { - this.name = name; - this.score = score; - this.paperCount = paperCount; - } - - @Override - public void accept(Visitor visitor) { - visitor.visit(this); - } - - ...... - -} -``` - -## 2.3 创建抽象访问者 - -```java -public interface Visitor { - - void visit(Student student); - - void visit(Teacher teacher); -} -``` - -## 2.4 创建具体访问者 -创建一个根据分数评比的具体访问者 GradeSelection,实现 Visitor 接口 - -```java -public class GradeSelection implements Visitor { - - @Override - public void visit(Student student) { - if (student != null && student.getGrade() >= 90) { - System.out.println(student.getName() + "的分数是" + student.getGrade() + ",荣获了成绩优秀奖。"); - } - } - - @Override - public void visit(Teacher teacher) { - if (teacher != null && teacher.getScore() >= 85) { - System.out.println(teacher.getName() + "的分数是" + teacher.getScore() + ",荣获了成绩优秀奖。"); - } - } -} -``` - -## 2.5 调用 - -```java -public class VisitorClient { - - public static void main(String[] args) { - // 抽象元素 => 具体元素 - Element element = new Student("lijiankun24", 90, 3); - // 抽象访问者 => 具体访问者 - Visitor visitor = new GradeSelection(); - // 具体元素 接收 具体访问者的访问 - element.accept(visitor); - } -} -``` - -上述代码即是一个简单的访问者模式的示例代码,输出如下所示: - - -上述代码可以分为三步: -1. 创建一个元素类的对象 -2. 创建一个访问类的对象 -3. 元素对象通过 Element#accept(Visitor visitor) 方法传入访问者对象 - -# 3 ASM 中的访问者模式 -ASM 库就是 Visitor 模式的典型应用。 - -## 3.1 ASM 中几个重要的类 -- ClassReader -将字节数组或者 class 文件读入到内存当中,并以树的数据结构表示,树中的一个节点代表着 class 文件中的某个区域 -可以将 ClassReader 看作是 Visitor 模式中的访问者的实现类 - -- ClassVisitor(抽象类) -ClassReader 对象创建之后,调用 ClassReader#accept() 方法,传入一个 ClassVisitor 对象。在 ClassReader 中遍历树结构的不同节点时会调用 ClassVisitor 对象中不同的 visit() 方法,从而实现对字节码的修改。在 ClassVisitor 中的一些访问会产生子过程,比如 visitMethod 会产生 MethodVisitor 的调用,visitField 会产生对 FieldVisitor 的调用,用户也可以对这些 Visitor 进行自己的实现,从而达到对这些子节点的字节码的访问和修改。 -在 ASM 的访问者模式中,用户还可以提供多种不同操作的 ClassVisitor 的实现,并以责任链的模式提供给 ClassReader 来使用,而 ClassReader 只需要 accept 责任链中的头节点处的 ClassVisitor。 -- ClassWriter -ClassVisitor 的实现类,它是生成字节码的工具类,它一般是责任链中的最后一个节点,其之前的每一个 ClassVisitor 都是致力于对原始字节码做修改,而 ClassWriter 的操作则是老实得把每一个节点修改后的字节码输出为字节数组。 - -## 3.2 ASM 的工作流程 -1. ClassReader 读取字节码到内存中,生成用于表示该字节码的内部表示的树,ClassReader 对应于访问者模式中的元素 -2. 组装 ClassVisitor 责任链,这一系列 ClassVisitor 完成了对字节码一系列不同的字节码修改工作,对应于访问者模式中的访问者 Visitor -3. 然后调用 ClassReader#accept() 方法,传入 ClassVisitor 对象,此 ClassVisitor 是责任链的头结点,经过责任链中每一个 ClassVisitor 的对已加载进内存的字节码的树结构上的每个节点的访问和修改 -4. 最后,在责任链的末端,调用 ClassWriter 这个 visitor 进行修改后的字节码的输出工作 \ No newline at end of file diff --git "a/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/kafka\351\253\230\346\200\247\350\203\275\346\200\273\347\273\223.md" "b/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/kafka\351\253\230\346\200\247\350\203\275\346\200\273\347\273\223.md" deleted file mode 100644 index f6a8ac9109..0000000000 --- "a/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/kafka\351\253\230\346\200\247\350\203\275\346\200\273\347\273\223.md" +++ /dev/null @@ -1,21 +0,0 @@ -kafka高性能原理 -============== -       最近翻了下kafka官方关于kafka设计的文档,面试上用,这里就总结下自己了解到的kafka设计上支持那么大吞吐量的原因 -消息传递及存储 ------------- -       从上层设计来说,kafka的生产者支持批量发送消息(可以设置发送的内容最大大小和最长等待时间)当这些批量的消息到达kafak的broker上后会通过硬盘的线性写操作将日志记录进硬盘,这种操作的速度是很快的(中间也涉及到操作系统的pagecache,kafka也可以设置这种缓存刷盘的频率比如:一秒刷一次,每条消息刷一次,按照操作系统的配置去刷),这个是说从生产者发送消息给broker很快,那么消费者消费速度呢?kafka在消费上使用pull的方式去主动向broker节点请求获取消息,而具体的offset是由消费者去指定的(这个offset其实broker上也有维护一份,但是我理解的是拉取offset的决定权是掌握在消费者手里的,只不过如果消费者挂了后,其他替代的消费者如何知道原来的offset呢,那就需要broker也存一份),Kafka底层是通过linux的sendfile函数直接将消息存储的消息内容转发到网络的socket buffer然后在copy到NIC buffer发送到网络上。这个用到的是零拷贝技术,而正常的情况是需要以下几步: - -> 1,从硬盘读取到pagecache -> 2,从pagecache读取到用户内存 -> 3,从用户内存读到socket buffer中 -> 4,从socket buffer读取到 NIC buffer中然后NIC自动硬件发送(这步是不需要耗费CPU时间的) ->>kafka使用零拷贝总共节省了从pagecache拷贝到用户内存和从用户内存拷贝到socket buffer的两次拷贝,节省了拷贝过程中用户态和心态的切换,同时因为网卡,显卡,声卡等支持了DMA也就是直接访问主内存而不需要经过CPU,那么网卡可以直接访问硬盘的pagecache而不需要在经过pagechche到socket buffer的这一步拷贝真正实现了零拷贝。 - - -      而kafka消费方式是通过消费者拉取的方式而消费者可以根据自己的消费速度批量拉取消息,消息又都是顺序读,所以kafka在发送消息给消费者的时候速度也很快。同时,kafka也支持数据的压缩,这种压缩的数据在生产者,broker,消费者都是一致的可以直接传输。 - -集群 ---------------- - -       说到大吞吐量必须也得涉及到kafka集群,现将Kafka集群我认为的重点知识记录如下: -       主要涉及两个方面吧,**一个是多boker节点,一个是主从复制**。Kafka使用多croker节点来进行负载均衡,而生产者按照topic发送消息到broker的规则可以选用轮询或者指定规则,消费者按照group进行消费,每个group中只会有一个消费者消费同一条消息,如果同一个group中有消费者挂了,那么这个消费者对应消费的broker也会分配到同一个group中的其他消费者上。但是如果broker挂了呢?这就需要用到kafka的主从节点设置了。其实broker的从节点数据同步方式跟普通的消费者没什么区别,而在同步数据的时候主节点会维护一套ISR节点群,在这个节点群的从节点,kafka认为他们的数据是比较完整的,如果主节点挂了之后,这些从节点的任意一台节点都可以替换主节点。那么怎么保证一个消息会被同步到从节点了呢,这个可以在生产者配置acks=0,1,-1来决定一条消息只有在收到多少个从节点的确认后才算真正的落地成功,当选择-1的时候那么在ISR集合中的所有节点都要收到这条消息并返回确认后,这条消息才算发送成功,这个时候延迟也会比较高,所以可以根据线上系统的特点来综合判断这个配置如何设置。 \ No newline at end of file diff --git "a/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(23-8) - Redis\345\223\250\345\205\265\344\270\273\345\244\207\345\210\207\346\215\242\347\232\204\346\225\260\346\215\256\344\270\242\345\244\261\351\227\256\351\242\230.md" "b/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(23-8) - Redis\345\223\250\345\205\265\344\270\273\345\244\207\345\210\207\346\215\242\347\232\204\346\225\260\346\215\256\344\270\242\345\244\261\351\227\256\351\242\230.md" new file mode 100644 index 0000000000..e86a788c0e --- /dev/null +++ "b/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(23-8) - Redis\345\223\250\345\205\265\344\270\273\345\244\207\345\210\207\346\215\242\347\232\204\346\225\260\346\215\256\344\270\242\345\244\261\351\227\256\351\242\230.md" @@ -0,0 +1,60 @@ +# 1 数据丢失的两个场景 + +主备切换的过程,可能会导致数据丢失 + +## 1.1 异步复制 + +由于 `master => slave`的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机,于是这些数据就丢失了 + +![](https://ask.qcloudimg.com/http-save/1752328/93jl4vswgy.png) + +## 1.2 脑裂导致 + +脑裂,也就是说,某个master所在节点突然脱离正常的网络,无法和其他slave机器连接,但实际上master还运行着 + +此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master + +这个时候,集群里就会有两个master,也就是所谓的`脑裂` + +此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了 + +因此旧master再次恢复时,会被作为一个slave挂到新的master上去,自己的数据会被清空,重新从新的master复制数据 + +![](https://ask.qcloudimg.com/http-save/1752328/q5320luqi1.png) + +# 2 数据丢失的解决方案 + +如下配置可以减少异步复制和脑裂导致的数据丢失 + +``` +min-slaves-to-write 1 +min-slaves-max-lag 10 +``` + +配置要求至少有1个slave,数据复制和同步的延迟不能超过10秒 + +一旦所有的slave,数据复制和同步的延迟都超过了10秒钟,master就不再接收任何请求! + +## 2.1 异步复制数据丢失解决方案 + +`min-slaves-max-lag` 配置 + +即可确保,一旦slave复制数据和ack延时过长,就认为可能master宕机后损失的数据太多了,那么就拒绝写请求 + +这样就可把master宕机时由于部分数据未同步到slave导致的数据丢失降低在可控范围 + +![](https://ask.qcloudimg.com/http-save/1752328/znnqfrs21u.png) + +## 2.2 脑裂数据丢失解决方案 + +若一个master出现了脑裂,跟其他slave失去连接,那么开始的两个配置可以确保 + +若不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求 + +这样脑裂后的旧master就不会接受client的新数据,也就避免了数据丢失 + +上面的配置就确保了,如果跟任何一个slave丢了连接,在10秒后发现没有slave给自己ack,那么就拒绝新的写请求 + +因此在脑裂场景下,最多就丢失10秒的数据 + +![](https://ask.qcloudimg.com/http-save/1752328/aamxilr8we.png) \ No newline at end of file diff --git "a/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(28) - \345\246\202\344\275\225\350\247\243\345\206\263Redis\347\232\204\345\271\266\345\217\221\347\253\236\344\272\211\351\227\256\351\242\230.md" "b/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(28) - \345\246\202\344\275\225\350\247\243\345\206\263Redis\347\232\204\345\271\266\345\217\221\347\253\236\344\272\211\351\227\256\351\242\230.md" new file mode 100644 index 0000000000..097a0ca853 --- /dev/null +++ "b/\351\235\242\350\257\225\351\242\230\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225\347\263\273\345\210\227/\347\252\201\347\240\264Java\351\235\242\350\257\225(28) - \345\246\202\344\275\225\350\247\243\345\206\263Redis\347\232\204\345\271\266\345\217\221\347\253\236\344\272\211\351\227\256\351\242\230.md" @@ -0,0 +1,24 @@ + +# 1 面试题 +redis的并发竞争问题是什么?如何解决这个问题?了解Redis事务的CAS方案吗? + +# 2 考点分析 +这个也是线上非常常见的一个问题,就是多客户端同时并发写一个key,可能本来应该先到的数据后到了,导致数据版本错了。或者是多客户端同时获取一个key,修改值之后再写回去,只要顺序错了,数据就错了。 + +而且redis自己就有天然解决这个问题的CAS类的乐观锁方案 + +# 3 详解 +- redis并发竞争问题以及解决方案 +![](https://img-blog.csdnimg.cn/20190509175418361.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +# 参考 + +《Java工程师面试突击第1季-中华石杉老师》 + +# X 交流学习 +![](https://img-blog.csdnimg.cn/20190504005601174.jpg) +## [Java交流群](https://jq.qq.com/?_wv=1027&k=5UB4P1T) +## [博客](https://blog.csdn.net/qq_33589510) + +## [Github](https://github.com/Wasabi1234) +