├── .gitignore ├── .idea ├── checkstyle-idea.xml ├── encodings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── learningSummary.iml ├── misc.xml ├── modules.xml ├── sonarlint │ └── issuestore │ │ └── index.pb └── vcs.xml ├── JVM核心知识 ├── JVM垃圾回收总结.md ├── OOM怎么办,教你生成dump文件以及查看.md └── 从JVM讲到类加载机制,很简单的.md ├── Java基础 ├── Java基础不简单,讲一讲枚举.md ├── String类.md ├── hashcode和equals区别与联系.md ├── java泛型.md └── 怎么在Java中自定义注解.md ├── MySQL数据库 ├── Canal+Kafka实现MySQL与Redis数据同步.md ├── MySQL与MVVC机制.md ├── MySQL主从复制读写分离,能讲一下吗.md ├── 什么是脏读、不可重复读、幻读.md ├── 什么是雪花ID?.md ├── 必须了解的mysql三种log.md ├── 要精通SQL优化?那就学一学explain吧.md ├── 谈谈MYSQL索引是如何提高查询效率的.md └── 超详细canal入门.md ├── README.md ├── SpringCloud微服务架构 ├── SpringCloud微服务系列之Gateway网关.md ├── SpringCloud微服务系列之Gateway过滤器.md ├── SpringCloud微服务系列之OpenFeign.md └── SpringCloud微服务系列之注册中心.md ├── 中间件 ├── RabbitMQ入门.md ├── RabbitMQ如何防止消息丢失.md └── pulsar中间件入门.md ├── 分布式 ├── 3千字带你搞懂XXL-JOB任务调度平台.md ├── SpringBoot多环境配置.md ├── ZooKeeper入门.md ├── skywalking调用链追踪.md ├── 三千字Apollo配置中心总结.md ├── 从秒杀聊到ZooKeeper分布式锁.md ├── 日志集中分析平台ELK.md └── 超详细的Sentinel入门.md ├── 大数据 ├── WordCount.md └── 学习大数据从安装hadoop开始.md ├── 常用的设计模式 ├── 代理模式以及应用.md ├── 原型模式在实战中的应用.md ├── 教你用构建者-生成器-模式优雅地创建对象.md ├── 教你用策略模式解决多重if-else.md ├── 模板模式以及实战应用.md ├── 装饰者模式与IO流.md ├── 观察者模式以及实际项目应用.md ├── 责任链模式.md └── 适配器模式与SpringMVC.md ├── 并发编程的艺术 ├── 五千字详细讲解并发编程的AQS.md ├── 多线程开发,先学会线程池吧.md ├── 并发编程里的悲观锁和乐观锁.md ├── 死磕synchronized关键字底层原理.md └── 面试官问我什么是JMM.md ├── 必学的优秀技术框架 ├── 5千字的SpringMVC总结,我觉得你会需要.md ├── SpringBoot启动类启动流程.md ├── SpringMVC全局异常处理.md ├── dynamic-datasource源码分析.md ├── mybatis-plus全解.md ├── mybatis-plus整合多数据源.md ├── spring有哪些设计模式.md └── 从设计模式的角度剖析Mybatis源码.md ├── 数据结构与算法 ├── leetcode算法题分享(字符串).md ├── leetcode经典算法题分享(哈希表).md └── 八种经典排序算法.md ├── 缓存服务 ├── Redis-缓存雪崩、缓存击穿、缓存穿透.md ├── 基于Redis实现分布式锁.md ├── 布隆过滤器.md └── 深入探索redis的五种数据类型.md ├── 网络编程 ├── JWT.md ├── NIO入门.md ├── Netty进阶之粘包和拆包.md ├── Reactor模式.md └── 超详细Netty入门.md ├── 遇到的坑 └── List集合的坑.md └── 面经分享 ├── 记一次高级java面试.md └── 面试题总结-基础篇.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | -------------------------------------------------------------------------------- /.idea/checkstyle-idea.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 36 | -------------------------------------------------------------------------------- /.idea/learningSummary.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/sonarlint/issuestore/index.pb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yehongzhi/learningSummary/c8117ef6c9f1d218258d7dae55253ff65a830b68/.idea/sonarlint/issuestore/index.pb -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /JVM核心知识/JVM垃圾回收总结.md: -------------------------------------------------------------------------------- 1 | # 思维导图 2 | 3 | ![](https://static.lovebilibili.com/jvm_gc_swdt.jpg) 4 | 5 | # 前言 6 | 7 | Java相对于C/C++语言来说,最明显的特点在于Java引入了自动垃圾回收。垃圾回收(Garbage Collection简称GC)可以使程序员不在需要关心JVM内存管理的问题,专注于写程序本身。平时程序员是很难感知到GC的存在,但是如果**涉及到一些性能调优,线上的问题排查等等,深入地了解GC是必不可少的**。往往通过一些JVM参数的设置能就使系统性能提高不少。 8 | 9 | # 一、JVM内存区域 10 | 11 | 要深入了解GC,首先要明白GC会回收哪些数据,数据位于哪个区域。接着我们看一下JVM的内存区域。 12 | 13 | ![](https://static.lovebilibili.com/jvm_gc_01.png) 14 | 15 | 从图中可以看出,内存区域分为五个: 16 | 17 | - 虚拟机栈:线程私有,由一个个栈帧组成,每个栈帧对应着一个调用的方法,保存有方法的局部变量等信息。方法被调用时栈帧入栈,方法结束调用时栈帧出栈。入栈出栈的时机很清楚,所以不需要进行GC。 18 | - 本地方法栈:与虚拟机栈非常类似,本地方法栈与虚拟机栈的区别在于,虚拟机栈执行的是Java方法,本地方法栈执行的是本地方法(Native Method)。这块区域也不需要进行GC。 19 | - 程序计数器:线程私有的,它的作用可以看做是当前线程所执行的字节码的行号指示器。我们知道JVM的多线程是通过CPU时间片轮转(即线程轮流切换并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程中可能会被挂起,而另一个线程获取到时间片开始执行。**在JVM中,就是通过程序计数器来记录某个线程的字节码执行位置,当被挂起的线程重新获取到时间片的时候,就知道上次被挂起时执行到哪个位置了**。这块区域也不需要GC。 20 | - 方法区:在Java8之前有永久代的概念,在堆中实现,受GC的管理,主要存储类的信息,常量,静态变量,由于永久代有 -XX:MaxPermSize 的上限,所以很容易造成 OOM。在Java8之后,永久代被移除,然后把方法区的实现移到了本地内存中的元空间中,这样方法区就不受 JVM 的控制了。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。所以Java8以后,方法区也不需要GC。 21 | - 堆:堆是Java对象的存储区域,任何new字段分配的**Java对象实例和数组**,都被分配在了堆上。GC主要作用于这个区域,对这两类数据进行回收。 22 | 23 | # 二、如何判断对象是否可回收 24 | 25 | 上面讲了GC主要作用的区域是在堆中,那么又是怎么判断是否可以回收的呢?在GC里面有两种算法来判断,一种是引用计数,对象引用的次数为0就是垃圾,另一种是可达性算法,如果一个对象不在以GC Root根节点为起点的引用链中,则视为垃圾。 26 | 27 | ## 2.1 引用计数算法 28 | 29 | 首先看引用计数法,简单点说对象被引用,就会在此对象的对象头上计数器加一,每当有一个引用失效时计数器的值减一,如果没有引用(引用次数为0)则此对象可回收。但是这种算法很难解决对象之间互相循环引用的问题。 30 | 31 | ## 2.2 可达性算法 32 | 33 | 所谓的GC Roots就是一组必须活跃的引用,基本思路就是从一系列的GC Root一直往下搜索,通过GC Root串成的一条线称为引用链,如果有对象不在任何一条以GC Root为起点的引用链中,则此对象就会被GC回收,这就是可达性算法。 34 | 35 | ![](https://static.lovebilibili.com/jvm_gc_02.png) 36 | 37 | 哪些对象可作为GC Root对象呢: 38 | 39 | - 虚拟机栈(栈帧中的本地变量表)中引用的对象 40 | - 方法区中类静态属性引用的对象 41 | - 方法区中常量引用的对象 42 | - 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象 43 | 44 | # 三、常见的垃圾回收算法 45 | 46 | 上面已经讲了如何判断哪些对象时可回收的。那么判断完是否可回收后,GC又是使用什么算法进行回收的呢?这就要讲一讲垃圾回收的几种方式: 47 | 48 | - 标记清除法 49 | - 标记整理法 50 | - 复制算法 51 | - 分代收集算法 52 | 53 | ## 3.1 标记清除法 54 | 55 | 其实很简单,分为**标记**和**清除**两个步骤。第一步根据可达性算法标记被回收的对象,第二步回收被标记的对象。 56 | 57 | ![](https://static.lovebilibili.com/jvm_gc_03.png) 58 | 59 | 明显这种垃圾回收算法的缺点是很容易产生内存碎片。 60 | 61 | ## 3.2 标记整理法 62 | 63 | 前面两个步骤和标记清除算法一样,而不同的是在标记清除算法的基础上多了一步整理的过程。如图所示,整理步骤的时候,将所有存活的对象都往左边移动,然后清理另一端的所有区域,这样就不会产生内存碎片。 64 | 65 | ![](https://static.lovebilibili.com/jvm_gc_04.png) 66 | 67 | 虽然不会产生内存碎片,但是由于频繁地移动存活的对象,所以效率十分低下。 68 | 69 | ## 3.3 复制算法 70 | 71 | 把内存分成两份,分别是A区域和B区域,第一步根据可达性算法把存活的对象标记出来,第二步把存活的对象复制到B区域,第三步把A区域全部清空。这就是复制算法。 72 | 73 | ![](https://static.lovebilibili.com/jvm_gc_05.png) 74 | 75 | 复制算法不会产生内存碎片,并且不需要频繁移动存活的对象,而缺点就是内存利用不充分,比如一块500M的内存,要分成两份,只能利用到250M。 76 | 77 | ## 3.4 分代收集算法 78 | 79 | 分代搜集算法是**针对对象的不同特性**,而使用适合的算法,这里面并没有实际上的新算法产生。与其说分代收集算法是第四个算法,不如说它是对前三个算法的实际应用。 80 | 81 | 首先我们先探讨一下对象的不同特性,内存中的对象其实可以根据生命周期的长短大致分为三种: 82 | 83 | - 夭折对象(新生代):朝生夕死的对象,比如方法里的局部变量。 84 | - 持久对象(老年代):存活的比较久但还是要死的对象,比如缓存对象,单例对象等等。 85 | - 永久对象(永久代):对象生成后几乎不灭的对象,例如String池中的对象(享元模式)、加载过的类信息等等。 86 | 87 | 上述的对象对应在内存中的区域就是,夭折对象和持久对象在Java堆中,永久对象在方法区。 88 | 89 | 分代算法的原理就是根据对象的存货周期不同将堆分为年轻代和老年代。新生代又分为Eden 区,from Survivor 区(S0区),to Survivor 区(S1区),比例为8:1:1。 90 | 91 | ![](https://static.lovebilibili.com/jvm_gc_06.png) 92 | 93 | 先看年轻代的GC,年轻代采用的回收算法是复制算法。新建的对象被创建后就会分配在Eden 区,当Eden区将满时,就会触发GC。 94 | 95 | ![](https://static.lovebilibili.com/jvm_gc_07.png) 96 | 97 | 在这一步GC会把大部分夭折对象回收,根据可达性算法标记出存活的对象,把存活对象复制到S0区,然后清空Eden 区。 98 | 99 | ![](https://static.lovebilibili.com/jvm_gc_08.png) 100 | 101 | 接着继续到下一次触发GC时,就会把Eden区和S0区的存活对象复制到S1区,然后清空Eden区和S0区。每次垃圾回收后S0和S1区的角色互换。每次GC后,如果对象存活下来则年龄加一。 102 | 103 | ![](https://static.lovebilibili.com/jvm_gc_09.png) 104 | 105 | 我们知道在年轻代中存活得越久的对象,年龄会越大,如果存活对象的年龄达到了我们设定的阈值,则会从S0(或S1)晋升到老年代。由于老年代的对象一般不会经常回收,所以采用的算法是标记整理法,老年代的回收次数相对较少,每次回收时间比较长。 106 | 107 | # 四、Stop the world 108 | 109 | Java中Stop The World机制简称STW,**执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集器之外),当垃圾回收完成后,再继续运行**,所以尽量减少STW的时间,就是优化JVM的主要目标。 110 | 111 | # 五、常见的垃圾收集器 112 | 113 | 垃圾收集器其实就是上面讲的算法的具体实现,目前没有说哪个垃圾收集器是最好的,只有根据应用的特点选择最合适的,所以说合适的才是最好的。 114 | 115 | 常见的垃圾收集器除了G1垃圾收集器外,都是只作用于一个区域,要么年轻代要么老年代,所以一般是配合使用,总共有7种,怎么配合使用,请看下面这张图,有连线的就是可以配合使用的。 116 | 117 | ![](https://static.lovebilibili.com/jvm_gc_10.png) 118 | 119 | ## 5.1 Serial收集器 120 | 121 | Serial收集器作用于年轻代,单线程的垃圾收集器,单线程意味着它只会使用一个CPU或者一个线程去完成垃圾回收的工作,当它在垃圾回收时,由于SWT机制,其他工作线程都会被暂时挂起,直到垃圾回收完成。这种垃圾收集器适用于Client模式的应用,在单CPU的环境下,由于没有和其他线程交互的开销,可以专心垃圾回收的工作,能够把单线程的优势发挥到极致,简单高效。通过-XX:+UseSerialGC可以开启这种回收模式。 122 | 123 | ![](https://static.lovebilibili.com/jvm_gc_11.png) 124 | 125 | ## 5.2 ParNew收集器 126 | 127 | ParNew 收集器是Serial收集器的多线程版本,作用于年轻代,默认开启的收集线程数和cpu数量一样,运行数量可以通过修改ParallelGCThreads设定。 128 | 129 | ![](https://static.lovebilibili.com/jvm_gc_12.png) 130 | 131 | ## 5.3 Parallel Scavenge收集器 132 | 133 | Parallel Scavenge收集器也被称为吞吐量优先收集器,作用于年轻代,多线程采用复制算法的垃圾收集器,跟ParNew 收集器有些类似。和ParNew 收集器不同的是,Parallel Scavenge收集器关注的是吞吐量,它提供了两个参数来控制吞吐量,分别是-XX:MaxGCPauseMillis(控制最大的垃圾收集停顿时间)、 -XX:GCTimeRatio(直接设置吞吐量大小)。 134 | 135 | 如果设置了-XX:+UseAdaptiveSizePolicy参数,虚拟机就会根据系统的运行情况收集监控信息,动态调整新生代的大小,Eden,Survivor比例等,以尽可能地达到我们设定的最大垃圾收集时间或吞吐量大小这两个指标,这种调节方式称为GC的自适应调节策略。这也是Parallel Scavenge收集器和ParNew 收集器最大的区别。 136 | 137 | ## 5.4 Serial Old收集器 138 | 139 | Serial Old 收集器是工作在老年代的单线程垃圾收集器,采用的算法是标记整理算法。在Client模式下可以和Serial收集器配合使用,如果在Server模式的应用,在JDK1.5之前可以和Parallel Scavenge收集器配合使用,另一种使用场景则是CMS垃圾收集器的后备预案,在发生**Concurrent Mode Failure**使用。 140 | 141 | ## 5.5 Parallel Old收集器 142 | 143 | Parallel Old 收集器是Parallel Scavenge收集器的老年代版本,多线程收集,采用标记整理算法。下图是Parallel Scavenge收集器和Parallel Old 收集器配合工作的过程图。 144 | 145 | ![](https://static.lovebilibili.com/jvm_gc_13.png) 146 | 147 | ## 5.6 CMS收集器 148 | 149 | CMS收集器是一种以获取最短回收停顿时间为目标的收集器,采用标记-清除算法。适用于希望系统停顿时间短,给用户更好的体验的场景。 150 | 151 | CMS收集器运行时主要分为四个步骤: 152 | 153 | - 初始标记:标记GC Roots能直接关联的对象。存在Stop The World。 154 | - 并发标记:GC Roots Tracing,可以和用户线程并发执行。 155 | - 重新标记:标记期间产生的对象存活的再次判断,修正对这些对象的标记,执行时间相对并发标记短,存在Stop The World。 156 | - 并发清除:清除对象,可以和用户线程并发执行。 157 | 158 | ![](https://static.lovebilibili.com/jvm_gc_14.png) 159 | 160 | CMS收集器的缺点在于: 161 | 162 | - 对CPU资源比较敏感。 163 | - 无法处理浮动垃圾。可能出现 「Concurrent Mode Failure」而导致另一次 Full GC 的产生,由于在并发清理时用户线程还在运行,所以清理垃圾同时新的垃圾也会不断产生,这部分垃圾(即浮动垃圾)只能在下一次 GC 时再清理掉。 164 | - 采用的是标记清除算法,所以会产生内存碎片。内存碎片会导致大对象无法分配到连续的内存空间,然后会产生Full GC,影响应用的性能。 165 | 166 | ## 5.7 G1收集器 167 | 168 | G1垃圾回收器主要是面向服务端的垃圾回收器,年轻代和老年代都可使用。运作时,整体上采用标记整理算法,局部上看是采用复制算法,两种算法都不会产生内存碎片,所以回收器在回收后能产生连续的内存空间。 169 | 170 | 它是专门针对以下场景设计的: 171 | 172 | - 像CMS收集器一样,能与应用程序线程并发执行。 173 | - 整理空闲空间更快。 174 | - 需要GC停顿时间更好预测。 175 | - 不希望牺牲大量的吞吐性能。 176 | - 不需要更大的Java Heap。 177 | 178 | G1垃圾回收器的内存分区不再采用传统的内存分区,将新生代,老年代的物理空间划分取消了。 179 | 180 | ![](https://static.lovebilibili.com/jvm_gc_15.png) 181 | 182 | 取而代之的是,把堆内存分成若干个Region(区域),每次收集的时候,只收集其中几个区域,以此来控制垃圾回收产生的STW。G1垃圾回收器和传统的垃圾回收器的最大区别就在于,**弱化了分代概念,引入了分区的思想**。 183 | 184 | ![](https://static.lovebilibili.com/jvm_gc_16.png) 185 | 186 | G1中每代的存储地址都不是连续的,而是使用了不连续的大小相同的Region。除此之外G1中还多了一个H,H代表Humongous,用于存储巨大对象(humongous object),当对象大小大于等于region一半的对象,就直接分配到了老年代,防止了反复拷贝移动。 187 | 188 | G1垃圾回收过程可分为四步: 189 | 190 | - 初始标记。收集所有GC根(对象的起源指针,根引用),STW,在年轻代完成。 191 | - 并发标记。标记存活对象。 192 | - 最终标记。是最后一个标记阶段,STW,很短,完成所有标记工作。 193 | - 筛选回收。回收没有存活对象的Region并加入可用Region队列。 194 | 195 | ![](https://static.lovebilibili.com/jvm_gc_17.png) 196 | 197 | # 总结 198 | 199 | 本文的简述了JVM的垃圾回收的理论知识,思路是先搞懂GC作用的区域是在堆中,然后介绍可达性算法的作用是为了标记存活的对象,知道哪些是可回收对象,接着就是使用垃圾回收算法进行回收,然后介绍了常见的几种垃圾回收算法(标记清除,复制算法,标记整理),最后再介绍常见的几种垃圾回收器。 200 | 201 | 对于垃圾回收器的介绍,这里只是简单的描述,并没有深入地讲解,因为每一个垃圾回收器如果展开细述都能讲上半天,所以有兴趣的话,可以自己再去探索一下,个人认为CMS和G1垃圾回收器是比较重要的两种。 202 | 203 | 这篇文章就讲到这里了,希望看完之后能对你有所帮助,感谢大家的阅读。 204 | 205 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 206 | 207 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 208 | 209 | ![](https://static.lovebilibili.com/dashacha.png) 210 | 211 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /JVM核心知识/OOM怎么办,教你生成dump文件以及查看.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: OOM怎么办,教你生成dump文件以及查看 3 | date: 2021-05-30 21:32:13 4 | index_img: https://static.lovebilibili.com/dump_index.jpg 5 | tags: 6 | - JVM 7 | --- 8 | 9 | > **文章已收录Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 10 | 11 | # 前言 12 | 13 | 在日常开发中,即使代码写得有多谨慎,免不了还是会发生各种意外的事件,比如服务器内存突然飙高,又或者发生内存溢出(OOM)。当发生这种情况时,我们怎么去排查,怎么去分析原因呢? 14 | 15 | 这时就引出这篇文章要讲的dump文件,各位看官且往下看。 16 | 17 | # 什么是dump文件 18 | 19 | dump文件是一个进程或者系统在某一个给定的时间的快照。 20 | 21 | dump文件是用来给驱动程序编写人员调试驱动程序用的,这种文件必须用专用工具软件打开。 22 | 23 | dump文件中包含了程序运行的模块信息、线程信息、堆栈调用信息、异常信息等数据。 24 | 25 | 在服务器运行我们的Java程序时,是无法跟踪代码的,所以当发生线上事故时,dump文件就成了一个很关键的分析点。 26 | 27 | # 如何生成dump文件 28 | 29 | 这里介绍两种方式,一种是主动的,一种是被动的。 30 | 31 | ## 方式一 32 | 33 | 主动生成dump文件。首先要查找运行的Java程序的pid。 34 | 35 | 使用`top`命令: 36 | 37 | ![](https://static.lovebilibili.com/dump_01.png) 38 | 39 | 然后使用jmap命令生成dump文件。file后面是保存的文件名称,1246则是java程序的PID。 40 | 41 | ```shell 42 | jmap -dump:format=b,file=user.dump 1246 43 | ``` 44 | 45 | ![](https://static.lovebilibili.com/dump_02.png) 46 | 47 | ## 方式二 48 | 49 | 其实在很多时候我们是不知道何时会发生OOM,所以需要在发生OOM时自动生成dump文件。 50 | 51 | 其实很简单,只需要在启动时加上如下参数即可。HeapDumpPath表示生成dump文件保存的目录。 52 | 53 | ```shell 54 | -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\tmp 55 | ``` 56 | 57 | 我们还需要模拟出OOM错误,以此触发产生dump文件,首先写个接口: 58 | 59 | ```java 60 | private static Map map = new HashMap<>(); 61 | 62 | @RequestMapping("/oom") 63 | public String oom() throws Exception { 64 | for (int i = 0; i < 100000; i++) { 65 | map.put("key" + i, "value" + i); 66 | } 67 | return "oom"; 68 | } 69 | ``` 70 | 71 | 然后在启动时设置堆内存大小为32M。 72 | 73 | ```shell 74 | -Xms32M -Xmx32M 75 | ``` 76 | 77 | 因为要后台启动,并且输出日志,所以最后启动命令就是这样: 78 | 79 | ```shell 80 | nohup java -jar -Xms32M -Xmx32M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local user-0.0.1-SNAPSHOT.jar > log.file 2>&1 & 81 | ``` 82 | 83 | 然后请求oom的接口,查看日志,果然发生了OOM错误。 84 | 85 | ![](https://static.lovebilibili.com/dump_03.png) 86 | 87 | 查看保存dump的目录,果然生成了对应的dump文件。 88 | 89 | ![](https://static.lovebilibili.com/dump_04.png) 90 | 91 | # 如何查看dump文件 92 | 93 | 这里我介绍使用`Jprofiler`,有可视化界面,功能也比较完善,能够打开JVM工具(通过-XX:+HeapDumpOnOutOfMemoryError JVM参数触发)创建的hporf文件。 94 | 95 | 安装过程这里就省略了,网上谷歌,百度自行查找。我们把刚刚自动生成的`java_pid1257.hprof`用`Jprofiler`打开,看到是这个样子。 96 | 97 | ![](https://static.lovebilibili.com/dump_05.png) 98 | 99 | 明显可以看出HashMap的Node对象,还有String对象的实例很多,占用内存也是最多的。这里还不够明显,我们看Biggest Objects。 100 | 101 | ![](https://static.lovebilibili.com/dump_06.png) 102 | 103 | 这里就看出是UserController类的HashMap占用了大量的内存。所以造成OOM的原因不难看出,就是在UserController里的Map集合。 104 | 105 | # 总结 106 | 107 | 当然线上的代码量,类的数量,实例的数量都非常庞大,所以没有那么简单就能找出报错的原因,但是要用什么工具,怎么用至少要知道,那么当遇到问题时,才不会慌张。 108 | 109 | 我问过一些技术大佬,为什么技术大佬代码写得不是很多,但是工资却特别高。大佬说,那是因为当线上出现问题时,大佬能解决大家解决不了的问题,这种能力就体现出他个人的价值。 110 | 111 | 一句话讲完,业务代码大部分程序员都会写,而线上排错能力并不是大部分程序员都会排。 112 | 113 | 这篇文章就讲到这里了,感谢大家的阅读,希望看完大家能有所收获! 114 | 115 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 116 | 117 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 118 | 119 | ![](https://static.lovebilibili.com/dashacha.png) 120 | 121 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /Java基础/String类.md: -------------------------------------------------------------------------------- 1 | > **文章已收录Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 2 | 3 | # String类 4 | 5 | 在Java中String类的使用的频率可谓相当高。它是Java语言中的核心类,在java.lang包下,主要用于字符串的比较、查找、拼接等等操作。如果要深入理解一个类,最好的方法就是看看源码: 6 | 7 | ```java 8 | public final class String implements java.io.Serializable, Comparable, CharSequence { 9 | /** The value is used for character storage. */ 10 | private final char value[]; 11 | 12 | /** Cache the hash code for the string */ 13 | private int hash; // Default to 0 14 | 15 | //... 16 | } 17 | ``` 18 | 19 | 从源码中,可以看出以下几点: 20 | 21 | - String类被final关键字修饰,表示String类不能被继承,并且它的成员方法都默认为final方法。 22 | - String类实现了Serializable、CharSequence、 Comparable接口。 23 | - String类的值是通过char数组存储的,并且char数组被private和final修饰,字符串一旦创建就不能再修改。 24 | 25 | 下面通过几个问题不断加深对String类的理解。 26 | 27 | ## 问题一 28 | 29 | 上面说字符串一旦创建就不能再修改,String类提供的replace()方法不就可以替换修改字符串的内容吗? 30 | 31 | 实际上replace()方法并没有对原字符串进行修改,而是创建了一个新的字符串返回,看看源码就知道了。 32 | 33 | ```java 34 | public String replace(char oldChar, char newChar) { 35 | if (oldChar != newChar) { 36 | int len = value.length; 37 | int i = -1; 38 | char[] val = value; /* avoid getfield opcode */ 39 | while (++i < len) { 40 | if (val[i] == oldChar) { 41 | break; 42 | } 43 | } 44 | if (i < len) { 45 | char buf[] = new char[len]; 46 | for (int j = 0; j < i; j++) { 47 | buf[j] = val[j]; 48 | } 49 | while (i < len) { 50 | char c = val[i]; 51 | buf[i] = (c == oldChar) ? newChar : c; 52 | i++; 53 | } 54 | //创建一个新的字符串返回 55 | return new String(buf, true); 56 | } 57 | } 58 | return this; 59 | } 60 | ``` 61 | 62 | 其他方法也是一样,无论是sub、concat还是replace操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。 63 | 64 | ## 问题二 65 | 66 | **为什么要使用final关键字修饰String类?** 67 | 68 | 首先要讲final修饰类的作用,**被final修饰的类不能被继承**,类中的所有成员方法都会被隐式地指定为final方法。也就是不能拥有子类,成员方法也不能被重写。 69 | 70 | 回到问题,String类被final修饰主要基于安全性和效率两点考虑。 71 | 72 | - 安全性 73 | 74 | 因为字符串是不可变的,所以**是多线程安全的**,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。 75 | 76 | String被许多的Java类(库)用来当做参数,比如网络连接地址URL,文件路径path,还有反射机制所需要的String参数等,假若String不是固定不变的,将会引起各种安全隐患。 77 | 78 | - 效率 79 | 80 | **字符串不变性保证了hash码的唯一性**,因此可以放心的进行缓存,这也是一种性能优化手段,意味着不必每次都取计算新的哈希码。 81 | 82 | **只有当字符串是不可变的,字符串池才有可能实现**,字符串常量池是java堆内存中一个特殊的存储区域,当创建一个String对象,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。 83 | 84 | # 字符串常量池 85 | 86 | 字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串我们使用的非常多。JVM为了提高性能和减少内存的开销,所以在实例化字符串的时候使用字符串常量池进行优化。 87 | 88 | 池化思想其实在Java中并不少见,字符串常量池也是类似的思想,当创建字符串时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。 89 | 90 | 我们可以写个简单的例子证明: 91 | 92 | ```java 93 | public static void main(String[] args) throws Exception { 94 | String s1 = "abc"; 95 | String s2 = "abc"; 96 | System.out.println(s1 == s2);//true 97 | } 98 | ``` 99 | 100 | ![](https://static.lovebilibili.com/string_final_01.png) 101 | 102 | 还有一个面试中经常问的,new String(“abc”)创建了几个对象? 103 | 104 | 这可能就是想考你对字符串常量池的理解,我一般回答是一个或者两个对象。 105 | 106 | 如果之前"abc"字符串没有使用过,毫无疑问是创建两个对象,堆中创建了一个String对象,字符串常量池创建了一个,一共两个。 107 | 108 | 如果之前已经使用过了"abc"字符串,则不会再在字符串常量池创建对象,而是从字符串常量缓冲区中获取,只会在堆中创建一个String对象。 109 | 110 | ```java 111 | String s1 = "abc"; 112 | String s2 = new String("abc"); 113 | //s2这行代码,只会创建一个对象 114 | ``` 115 | 116 | # 字符串拼接 117 | 118 | 字符串的拼接在Java中是很常见的操作,但是拼接字符串并不是简简单单地使用"+"号即可,还有一些要注意的点,否则会造成效率低下。 119 | 120 | 比如下面这段代码: 121 | 122 | ```java 123 | public static void main(String[] args) throws Exception { 124 | String s = ""; 125 | for (int i = 0; i < 10; i++) { 126 | s+=i; 127 | } 128 | System.out.println(s);//0123456789 129 | } 130 | ``` 131 | 132 | 在循环内使用+=拼接字符串会有什么问题呢?我们反编译一下看看就知道了。 133 | 134 | ![](https://static.lovebilibili.com/string_final_02.png) 135 | 136 | 其实反编译后,我们可以看到String类使用"+="拼接的底层其实是使用StringBuilder,先初始化一个StringBuilder对象,然后使用append()方法拼接,最后使用toString()方法得到结果。 137 | 138 | 问题在于如果在循环体内使用+=拼接,会创建很多临时的StringBuilder对象,拼接后再调用toString()赋给原String对象。这会生成大量临时对象,严重影响性能。 139 | 140 | 所以在循环体内进行字符串拼接时,建议使用StringBuilder或者StringBuffer类,例子如下: 141 | 142 | ```java 143 | public static void main(String[] args) throws Exception { 144 | StringBuilder s = new StringBuilder(); 145 | for (int i = 0; i < 10; i++) { 146 | s.append(i); 147 | } 148 | System.out.println(s.toString());//0123456789 149 | } 150 | ``` 151 | 152 | StringBuilder和StringBuffer的区别在于,StringBuffer的方法都被sync关键字修饰,所以是线程安全的,而StringBuilder则是线程不安全的(效率高)。 153 | 154 | # 总结 155 | 156 | 回顾一下,本文介绍了String类的不可变的特点,还有字符串常量池的作用,最后简单地从JVM编译的层面对字符串拼接提出一点建议。所谓温故而知新,即使是一些很基础很常见的类,如果深入去探索的话,也会有一番收获。 157 | 158 | 这篇文章就讲到这里了,感谢大家的阅读,希望看完大家能有所收获! 159 | 160 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 161 | 162 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 163 | 164 | ![](https://static.lovebilibili.com/dashacha.png) 165 | 166 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /Java基础/hashcode和equals区别与联系.md: -------------------------------------------------------------------------------- 1 | > **文章已收录Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 2 | 3 | # 写在前面 4 | 5 | 其实很早我就注意到阿里巴巴Java开发规范有一句话:`只要重写 equals,就必须重写 hashCode`。 6 | 7 | ![](https://static.lovebilibili.com/hashcode_equals_1.png) 8 | 9 | 我想很多人都会问为什么,所谓`知其然知其所以然`,对待知识不单止知道结论还得知道原因。 10 | 11 | # hashCode方法 12 | 13 | hashCode()方法的作用是获取哈希码,返回的是一个int整数 14 | 15 | ![](https://static.lovebilibili.com/hashcode_equals_2.png) 16 | 17 | 学过数据结构的都知道,哈希码的作用是确定对象在哈希表的索引下标。比如HashSet和HashMap就是使用了hashCode方法确定索引下标。如果两个对象返回的hashCode相同,就被称为“哈希冲突”。 18 | 19 | # equals方法 20 | 21 | equals()方法的作用很简单,就是判断两个对象是否相等,equals()方法是定义在Object类中,而所有的类的父类都是Object,所以如果不重写equals方法则会调用Object类的equals方法。 22 | 23 | ![](https://static.lovebilibili.com/hashcode_equals_3.png) 24 | 25 | Object类的equals方法是用“==”号进行比较,在很多时候,因为==号比较的是两个对象的内存地址而不是实际的值,所以不是很符合业务要求。所以很多时候我们需要重写equals方法,去比较对象中每一个成员变量的值是否相等。 26 | 27 | # 问题来了 28 | 29 | > 重写equals()方法就可以比较两个对象是否相等,为什么还要重写hashcode()方法呢? 30 | 31 | 因为HashSet、HashMap底层在添加元素时,会先判断对象的hashCode是否相等,如果hashCode相等才会用equals()方法比较是否相等。换句话说,HashSet和HashMap在判断两个元素是否相等时,**会先判断hashCode,如果两个对象的hashCode不同则必定不相等**。 32 | 33 | ![](https://static.lovebilibili.com/hashcode_equals_8.png) 34 | 35 | 下面我们做一个试验,有一个User类,只重写equals()方法,然后放到Set集合中去重。 36 | 37 | ```java 38 | public class User { 39 | 40 | private String id; 41 | 42 | private String name; 43 | 44 | private Integer age; 45 | 46 | public User(String id, String name, Integer age) { 47 | this.id = id; 48 | this.name = name; 49 | this.age = age; 50 | } 51 | 52 | @Override 53 | public boolean equals(Object o) { 54 | if (this == o) return true; 55 | if (o == null || getClass() != o.getClass()) return false; 56 | User user = (User) o; 57 | return Objects.equals(id, user.id) && 58 | Objects.equals(name, user.name) && 59 | Objects.equals(age, user.age); 60 | } 61 | 62 | //getter、setter、toString方法 63 | } 64 | ``` 65 | 66 | 然后我们循环创建10个成员变量的值都是一样的User对象,最后放到Set集合中去重。 67 | 68 | ```java 69 | public static void main(String[] args) { 70 | List list = new ArrayList<>(); 71 | for (int i = 0; i < 10; i++) { 72 | User user = new User("1", "张三", 18); 73 | list.add(user); 74 | } 75 | Set set = new HashSet<>(list); 76 | for (User user : set) { 77 | System.out.println(user); 78 | } 79 | List users = list.stream().distinct().collect(Collectors.toList()); 80 | System.out.println(users); 81 | } 82 | ``` 83 | 84 | 按道理我们预期会去重,只剩下一个“张三”的user,但实际上因为没有重写hashCode方法,所以没有去重。 85 | 86 | ![](https://static.lovebilibili.com/hashcode_equals_4.png) 87 | 88 | 接着我们在User类里面重写一些hashCode方法再试试,其他不变。 89 | 90 | ```java 91 | public class User { 92 | //其他不变 93 | 94 | //重写hashCode方法 95 | @Override 96 | public int hashCode() { 97 | return Objects.hash(id, name, age); 98 | } 99 | } 100 | ``` 101 | 102 | 再运行,结果正确。 103 | 104 | ![](https://static.lovebilibili.com/hashcode_equals_5.png) 105 | 106 | 究其原因在于HashSet会先判断hashCode是否相等,如果hashCode不相等就直接认为两个对象不相等,不会再用equals()比较了。我们不妨看看重写hashCode方法和不重写hashCode方法的哈希码。 107 | 108 | 这是不重写hashCode方法的情况,每个user对象的哈希码都不一样,所以HashSet会认为都不相等。 109 | 110 | ![](https://static.lovebilibili.com/hashcode_equals_6.png) 111 | 112 | 这是重写hashCode方法的情况,因为是用对象所有的成员变量的值计算出的哈希码,所以只要两个对象的成员变量都是相等的,则生成的哈希码是相同的。 113 | 114 | ![](https://static.lovebilibili.com/hashcode_equals_7.png) 115 | 116 | 那么有些人看到这里,就会问,如果两个对象返回的哈希码都是一样的话,是不是就**一定相等**? 117 | 118 | 答案是不一定的,因为HashSet、HashMap判断哈希码相等后还会再用equals()方法判断。 119 | 120 | 总而言之: 121 | 122 | - 哈希码不相等,则两个对象一定不相同。 123 | - 哈希码相等,两个对象不一定相同。 124 | - 两个对象相同,则哈希码和值都一定相等。 125 | 126 | # 总结 127 | 128 | 所以回到开头讲的那句,`只要重写 equals,就必须重写 hashCode`,这是一个很重要的细节,如果不注意的话,很容易发生业务上的错误。 129 | 130 | 特别是有时候我们明明用了HashSet,distinct()去重,但是就是不生效,这时应该回头看看重写了equals()和hashCode()方法了吗? 131 | 132 | 那么这篇文章就写到这里了,感谢大家的阅读。 133 | 134 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 135 | 136 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 137 | 138 | ![](https://static.lovebilibili.com/dashacha.png) 139 | 140 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /Java基础/java泛型.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 其实在开发中经常会看到泛型的使用,但是很多人对其也是一知半解,大概知道这是一个类似标签的东西。比如最常见的给集合定义泛型。 4 | 5 | ```java 6 | List list = new ArrayList<>(); 7 | Map map = new HashMap<>(); 8 | ``` 9 | 10 | 那么什么是泛型,为什么使用泛型,怎么使用泛型,接着往下看。 11 | 12 | # 什么是泛型 13 | 14 | > Java泛型是J2SE1.5中引入的一个新特性,其本质是参数化类型,也就是说所操作的数据类型被指定为一个参数(type parameter),这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。-- 百度百科 15 | 16 | 这句话读起来有点拗口,但是我们要抓住他说的关键,`参数化类型`和`可以用在类、接口和方法的创建中`,我们知道泛型是在什么地方使用。 17 | 18 | # 为什么使用泛型 19 | 20 | 一般我在思考这种问题时,会反过来思考,假如没有泛型会怎么样? 21 | 22 | 我们以最简单的`List`集合为例子,假如没有泛型: 23 | 24 | ```java 25 | public static void main(String[] args) { 26 | List list = new ArrayList(); 27 | list.add("good"); 28 | list.add(100); 29 | list.add('a'); 30 | for(int i = 0; i < list.size(); i++){ 31 | String val = (String) list.get(i); 32 | System.out.println("val:" + val); 33 | } 34 | } 35 | ``` 36 | 37 | 很显然在没有泛型的时候,List默认是Object类型,所以List里的元素可以是任意的,看起来集合里装着任意类型的参数是“挺不错”,但是任意的类型的缺点也是很明显的,就是要开发者对集合中的元素类型在预知的情况下进行操作,否则编译时不会提示错误,但是运行时很容易出现类型转换异常(ClassCastException)。 38 | 39 | 如果没有泛型,第二个小问题是,我们把一个对象放进了集合中,但是集合并不会记住这个对象的类型,再次取出时统统都会变成Object类,但是在运行时仍然为其本身的类型。 40 | 41 | 所以引入泛型就可以解决以上两个问题: 42 | 43 | - 类型安全问题。使用泛型,则会在编译期就能发现类型转换异常的错误。 44 | - 消除类型强转。泛型可以消除源代码中的许多强转类型的操作,这样可以使代码更加可读,并减少出错的机会。 45 | 46 | # 泛型的特性 47 | 48 | 泛型只有在编译阶段有效,在运行阶段会被擦除。 49 | 50 | 下面做个试验,请看代码: 51 | 52 | ```java 53 | List stringArrayList = new ArrayList(); 54 | List integerArrayList = new ArrayList(); 55 | 56 | Class classStringArrayList = stringArrayList.getClass(); 57 | Class classIntegerArrayList = integerArrayList.getClass(); 58 | System.out.println(classStringArrayList == classIntegerArrayList); 59 | ``` 60 | 61 | 结果是true,由此可看出,在运行时`ArrayList`和`ArrayList`都会被擦除成`ArrayList`。 62 | 63 | Java 泛型擦除是 Java 泛型中的一个重要特性,其目的是避免过多的创建类而造成的运行时的过度消耗。 64 | 65 | # 泛型的使用方式 66 | 67 | 在上文也提到泛型有三种使用方式:泛型类、泛型接口、泛型方法。 68 | 69 | ## 泛型类 70 | 71 | 基本语法: 72 | 73 | ```java 74 | public class 类名<泛型标识, 泛型标识, ...> { 75 | private 泛型标识 变量名; 76 | } 77 | ``` 78 | 79 | 示例代码: 80 | 81 | ```java 82 | public class GenericClass { 83 | private T t; 84 | } 85 | ``` 86 | 87 | 在泛型类里面,泛型形参T可用在返回值和方法参数上,例如: 88 | 89 | ```java 90 | public class GenericClass { 91 | 92 | private T t; 93 | 94 | public GenericClass() { 95 | } 96 | 97 | public void setValue(T t) {//作为参数 98 | this.t = t; 99 | } 100 | 101 | public T getValue() {//作为返回值 102 | return t; 103 | } 104 | } 105 | ``` 106 | 107 | 当我们创建类实例时,就可以传入类型实参: 108 | 109 | ```java 110 | public static void main(String[] args) throws Exception { 111 | //泛型传入了String类型 112 | GenericClass generic = new GenericClass(); 113 | //这里就限制了setValue()方法只能传入String类型 114 | generic.setValue("abc"); 115 | //限制了getValue()方法得到的也是String类型 116 | String value = generic.getValue(); 117 | System.out.println(value); 118 | } 119 | ``` 120 | 121 | 这里与普通类创建实例不同的地方在于,泛型类的构造需要在类名后面添加上<泛型>,这个尖括号传的是什么类型,T就代表什么类型。泛型类的作用就体现在这里,他限制了这个类的使用了泛型标识的方法的返回值和参数。 122 | 123 | ## 泛型方法 124 | 125 | 基本语法: 126 | 127 | ```java 128 | 修饰符 返回值类型 方法名(形参列表){ 129 | } 130 | ``` 131 | 132 | 示例: 133 | 134 | ```java 135 | //静态方法 136 | public static E getObject1(Class clazz) throws Exception { 137 | return clazz.newInstance(); 138 | } 139 | 140 | //实例方法 141 | public E getObject2(Class clazz) throws Exception { 142 | return clazz.newInstance(); 143 | } 144 | ``` 145 | 146 | 使用示例: 147 | 148 | ```java 149 | public static void main(String[] args) throws Exception { 150 | GenericClass generic = new GenericClass<>(); 151 | Object object1 = GenericClass.getObject1(Object.class); 152 | System.out.println(object1); 153 | Object object2 = generic.getObject2(Object.class); 154 | System.out.println(object2); 155 | } 156 | ``` 157 | 158 | 其实泛型方法比较简单,就是在返回值前加上尖括号<泛型标识>来表示泛型变量。不过对于初学者来说,很容易会跟泛型类的泛型方法混淆,特别是泛型类里定义了泛型方法的情况。 159 | 160 | ```java 161 | public class GenericClass { 162 | private T t; 163 | 164 | public GenericClass() { 165 | } 166 | //泛型类的方法 167 | public void setValue(T t) { 168 | this.t = t; 169 | } 170 | //泛型类的方法 171 | public T getValue() { 172 | return t; 173 | } 174 | //静态泛型方法,区别在于在返回值前需要加上尖括号<泛型标识> 175 | public static E getObject1(Class clazz) throws Exception { 176 | return clazz.newInstance(); 177 | } 178 | //实例泛型方法,区别在于在返回值前需要加上尖括号<泛型标识> 179 | public E getObject2(Class clazz) throws Exception { 180 | return clazz.newInstance(); 181 | } 182 | } 183 | ``` 184 | 185 | ## 泛型接口 186 | 187 | 基本语法: 188 | 189 | ```java 190 | public 接口名称 <泛型标识, 泛型标识, ...>{ 191 | 泛型标识 方法名(); 192 | } 193 | ``` 194 | 195 | 示例: 196 | 197 | ```java 198 | public interface Generator { 199 | T next(); 200 | } 201 | ``` 202 | 203 | 当实现泛型接口不传入实参时,与泛型类定义相同,需要将泛型的声明加在类名后面: 204 | 205 | ```java 206 | public class ObjectGenerator implements Generator { 207 | @Override 208 | public T next() { 209 | return null; 210 | } 211 | } 212 | ``` 213 | 214 | 当实现泛型接口传入实参时,当泛型参数传入了具体的实参后,则所有使用到泛型的地方都会被替换成传入的参数类型。比如下面的例子中,next()方法的返回值就变成了Integer类型: 215 | 216 | ```java 217 | public class IntegerGenerator implements Generator { 218 | @Override 219 | public Integer next() { 220 | Random random = new Random(); 221 | return random.nextInt(10); 222 | } 223 | } 224 | ``` 225 | 226 | ## 泛型通配符 227 | 228 | 类型通配符一般是使用 **?** 代替具体的类型实参,这里的 **?** 是具体的类型实参,而不是类型形参。它跟Integer,Double,String这些类型一样都是一种实际的类型,我们可以把 **?** 看作是所有类型的父类。 229 | 230 | 类型通配符又可以分为类型通配符上限和类型通配符下限。 231 | 232 | ### 类型通配符上限 233 | 234 | 基本语法: 235 | 236 | ```java 237 | 类/接口 238 | ``` 239 | 240 | 要求该泛型的类型,只能是实参类型,或实参类型的 **子类** 类型。 241 | 242 | 比如: 243 | 244 | ```java 245 | public class ListUtils { 246 | public void doing(T t) { 247 | System.out.println("size: " + t.size()); 248 | for (Object o : t) { 249 | System.out.println(o); 250 | } 251 | } 252 | } 253 | ``` 254 | 255 | 这样就限制了传入的泛型只能是List的子类,如下所示: 256 | 257 | ```java 258 | public static void main(String[] args) throws Exception { 259 | ListUtils utils = new ListUtils<>(); 260 | List list = Arrays.asList("1", "2", "3"); 261 | utils.doing(list); 262 | } 263 | ``` 264 | 265 | ### 类型通配符下限 266 | 267 | 基本语法: 268 | 269 | ```java 270 | 类/接口 271 | ``` 272 | 273 | 要求该泛型的类型,只能是实参类型,或实参类型的 **父类** 类型 274 | 275 | 比如: 276 | 277 | ```java 278 | public class ListUtils { 279 | //静态泛型方法,使用super关键字定义通配符下限 280 | public static void copy(Collection parentList, Collection childList) { 281 | for (E e : childList) { 282 | parentList.add(e); 283 | } 284 | } 285 | } 286 | ``` 287 | 288 | 限制了传入的List类型: 289 | 290 | ```java 291 | public static void main(String[] args) throws Exception { 292 | Integer i1 = new Integer(10); 293 | Integer i2 = new Integer(20); 294 | List integers = new ArrayList<>(); 295 | integers.add(i1); 296 | integers.add(i2); 297 | List numbers = new ArrayList<>(); 298 | //childList传入的是List,所以parentList传入的只能是Integer本身或者其父类的List 299 | ListUtils.copy(numbers, integers); 300 | System.out.println(numbers);//[10, 20] 301 | } 302 | ``` 303 | 304 | ## 泛型使用注意点 305 | 306 | 不能在静态字段使用泛型。 307 | 308 | ![](https://static.lovebilibili.com/fanxing_01.png) 309 | 310 | 不能和基本类型一起使用。 311 | 312 | ![](https://static.lovebilibili.com/fanxing_02.png) 313 | 314 | 异常类不能使用泛型。 315 | 316 | ![](https://static.lovebilibili.com/fanxing_03.png) 317 | 318 | # 总结 319 | 320 | 最后总结一下泛型的作用: 321 | 322 | - 提高了代码的可读性,这点毋庸置疑。 323 | - 解决类型安全的问题,在编译期就能发现类型转换异常的错误。 324 | - 可拓展性强,可以使用泛型通配符定义,不需要定义实际的数据类型。 325 | - 提高了代码的重用性。可以重用数据处理算法,不需要为每一种类型都提供特定的代码。 326 | 327 | 非常感谢你的阅读,希望这篇文章能给到你帮助和启发。 328 | 329 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 330 | 331 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 332 | 333 | ![](https://static.lovebilibili.com/dashacha.png) 334 | 335 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /Java基础/怎么在Java中自定义注解.md: -------------------------------------------------------------------------------- 1 | > **文章已收录Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 2 | 3 | # 什么是注解 4 | 5 | 注解是JDK1.5引入的新特性,主要用于简化代码,提高编程的效率。其实在日常开发中,注解并不少见,比如Java内置的`@Override`、`@SuppressWarnings`,或者Spring提供的`@Service`、`@Controller`等等,随着这些注解使用的频率越来越高,作为开发人员当真有必要深入学习一番。 6 | 7 | # Java内置的注解 8 | 9 | 先说说Java内置的三个注解,分别是: 10 | 11 | `@Override`:检查当前的方法定义是否覆盖父类中的方法,如果没有覆盖,编译器就会报错。 12 | 13 | `@SuppressWarnings`:忽略编译器的警告信息。 14 | 15 | ![](https://static.lovebilibili.com/zhujie_3.png) 16 | 17 | ![](https://static.lovebilibili.com/zhujie_4.png) 18 | 19 | `@Deprecated`:用于标识该类或方法已过时,建议开发人员不要使用该类或方法。 20 | 21 | ![](https://static.lovebilibili.com/zhujie_1.png) 22 | 23 | ![](https://static.lovebilibili.com/zhujie_2.png) 24 | 25 | # 元注解 26 | 27 | 元注解其实就是描述注解的注解。主要有四个元注解,分别是: 28 | 29 | ## @Target 30 | 31 | 用于描述注解的使用范围,也就是注解可以用在什么地方,取值有: 32 | 33 | CONSTRUCTOR:用于描述构造器。 34 | 35 | FIELD:用于描述字段。 36 | 37 | LOCAL_VARIABLE:用于描述局部变量。 38 | 39 | METHOD:用于描述方法。 40 | 41 | PACKAGE:用于描述包。 42 | 43 | PARAMETER:用于描述参数。 44 | 45 | TYPE:用于描述类,包括class,interface,enum。 46 | 47 | ## @Retention 48 | 49 | **表示需要在什么级别保存该注释信息,用于描述注解的生命周期**,取值由枚举RetentionPoicy定义。 50 | 51 | ![](https://static.lovebilibili.com/zhujie_5.png) 52 | 53 | SOURCE:在源文件中有效(即源文件保留),仅出现在源代码中,而被编译器丢弃。 54 | 55 | CLASS:在class文件中有效(即class保留),但会被JVM丢弃。 56 | 57 | RUNTIME:JVM将在运行期也保留注释,因此可以通过反射机制读取注解的信息。 58 | 59 | 如果只是做一些检查性操作,使用SOURCE,比如@Override,@SuppressWarnings。 60 | 61 | 如果要在编译时进行一些预处理操作,就用 CLASS。 62 | 63 | 如果需要获取注解的属性值,去做一些运行时的逻辑,可以使用RUNTIME。 64 | 65 | ## @Documented 66 | 67 | 将此注解包含在 javadoc 中 ,它代表着此注解会被javadoc工具提取成文档。它是一个标记注解,没有成员。 68 | 69 | ![](https://static.lovebilibili.com/zhujie_6.png) 70 | 71 | ## @Inherited 72 | 73 | 是一个标记注解,用来指定该注解可以被继承。使用 @Inherited 注解的 Class 类,表示这个注解可以被用于该 Class 类的子类。 74 | 75 | # 自定义注解 76 | 77 | 下面实战一下,自定义一个注解@LogApi,用于方法上,当被调用时即打印日志,在控制台显示调用方传入的参数和调用返回的结果。 78 | 79 | ## 定义注解 80 | 81 | 首先定义注解`@LogApi`,在方法上使用,为了能在反射中读取注解信息,当然是设置为`RUNTIME`。 82 | 83 | ```java 84 | @Target(value = ElementType.METHOD) 85 | @Documented 86 | @Retention(value = RetentionPolicy.RUNTIME) 87 | public @interface LogApi { 88 | } 89 | ``` 90 | 91 | 这种没有属性的注解,属于标记注解。 92 | 93 | 多说几句,如果需要传递属性值,也可以设置属性值value,比如`@RequestMapping`注解。 94 | 95 | ```java 96 | @Target({ElementType.METHOD, ElementType.TYPE}) 97 | @Retention(RetentionPolicy.RUNTIME) 98 | @Documented 99 | @Mapping 100 | public @interface RequestMapping { 101 | @AliasFor("path") 102 | String[] value() default {}; 103 | } 104 | ``` 105 | 106 | 如果在使用时。只设置value值,可以忽略value,比如这样: 107 | 108 | ```java 109 | //完整是@RequestMapping(value = {"/list"}) 110 | //忽略value不写 111 | @RequestMapping("/list") 112 | public Map list() throws Exception { 113 | Map userMap = new HashMap<>(); 114 | userMap.put("1号佳丽", "李嘉欣"); 115 | userMap.put("2号佳丽", "袁咏仪"); 116 | userMap.put("3号佳丽", "张敏"); 117 | userMap.put("4号佳丽", "张曼玉"); 118 | return userMap; 119 | } 120 | ``` 121 | 122 | ## 标记注解 123 | 124 | 刚刚定义完注解之后,就可以在需要的地方标记注解,很简单。 125 | 126 | ```java 127 | @LogApi 128 | @RequestMapping("/list") 129 | public Map list() throws Exception { 130 | //业务代码... 131 | } 132 | ``` 133 | 134 | ## 解析注解 135 | 136 | 最关键的一步来了,解析注解,一般在项目中会使用Spring的AOP技术解析注解,当然如果只需要解析一次的话,也可以使用Spring容器的生命周期函数。 137 | 138 | 这里的场景是打印每次方法被调用的日志,所以使用AOP比较合适。 139 | 140 | 创建一个切面类`LogApiAspect`进行解析。 141 | 142 | ```java 143 | @Aspect 144 | @Component 145 | public class LogApiAspect { 146 | //切面点为标记了@LogApi注解的方法 147 | @Pointcut("@annotation(io.github.yehongzhi.user.annotation.LogApi)") 148 | public void logApi() { 149 | } 150 | 151 | //环绕通知 152 | @Around("logApi()") 153 | @SuppressWarnings("unchecked") 154 | public Object around(ProceedingJoinPoint joinPoint) throws Throwable { 155 | long starTime = System.currentTimeMillis(); 156 | //通过反射获取被调用方法的Class 157 | Class type = joinPoint.getSignature().getDeclaringType(); 158 | //获取类名 159 | String typeName = type.getSimpleName(); 160 | //获取日志记录对象Logger 161 | Logger logger = LoggerFactory.getLogger(type); 162 | //方法名 163 | String methodName = joinPoint.getSignature().getName(); 164 | //获取参数列表 165 | Object[] args = joinPoint.getArgs(); 166 | //参数Class的数组 167 | Class[] clazz = new Class[args.length]; 168 | for (int i = 0; i < args.length; i++) { 169 | clazz[i] = args[i].getClass(); 170 | } 171 | //通过反射获取调用的方法method 172 | Method method = type.getMethod(methodName, clazz); 173 | //获取方法的参数 174 | Parameter[] parameters = method.getParameters(); 175 | //拼接字符串,格式为{参数1:值1,参数2::值2} 176 | StringBuilder sb = new StringBuilder(); 177 | for (int i = 0; i < parameters.length; i++) { 178 | Parameter parameter = parameters[i]; 179 | String name = parameter.getName(); 180 | sb.append(name).append(":").append(args[i]).append(","); 181 | } 182 | if (sb.length() > 0) { 183 | sb.deleteCharAt(sb.lastIndexOf(",")); 184 | } 185 | //执行结果 186 | Object res; 187 | try { 188 | //执行目标方法,获取执行结果 189 | res = joinPoint.proceed(); 190 | logger.info("调用{}.{}方法成功,参数为[{}],返回结果[{}]", typeName, methodName, sb.toString(), JSONObject.toJSONString(res)); 191 | } catch (Exception e) { 192 | logger.error("调用{}.{}方法发生异常", typeName, methodName); 193 | //如果发生异常,则抛出异常 194 | throw e; 195 | } finally { 196 | logger.info("调用{}.{}方法,耗时{}ms", typeName, methodName, (System.currentTimeMillis() - starTime)); 197 | } 198 | //返回执行结果 199 | return res; 200 | } 201 | } 202 | ``` 203 | 204 | 定义完切面类后,需要在启动类添加启动AOP的注解。 205 | 206 | ```java 207 | @SpringBootApplication 208 | //添加此注解,开启AOP 209 | @EnableAspectJAutoProxy 210 | public class UserApplication { 211 | public static void main(String[] args) { 212 | SpringApplication.run(UserApplication.class, args); 213 | } 214 | 215 | } 216 | ``` 217 | 218 | ## 测试 219 | 220 | 我们再在Controller控制层增加一个有参数的接口。 221 | 222 | ```java 223 | @LogApi 224 | @RequestMapping("/get/{id}") 225 | public String get(@PathVariable(name = "id") String id) throws Exception { 226 | HashMap user = new HashMap<>(); 227 | user.put("id", id); 228 | user.put("name", "关之琳"); 229 | user.put("经典角色", "十三姨"); 230 | return JSONObject.toJSONString(user); 231 | } 232 | ``` 233 | 234 | 启动项目,然后请求接口`list()`,我们可以看到控制台出现被调用方法的日志信息。 235 | 236 | ![](https://static.lovebilibili.com/zhujie_7.png) 237 | 238 | 请求有参数的接口`get()`,可以看到参数名称和参数值都被打印在控制台。 239 | 240 | ![](https://static.lovebilibili.com/zhujie_8.png) 241 | 242 | 这种记录接口请求参数和返回值的功能,在实际项目中基本上都会使用,因为这能利于系统的排错和性能调优等等。 243 | 244 | 我们也可以在这个例子中,学会使用注解和切面编程,可谓是一举两得! 245 | 246 | # 总结 247 | 248 | 注解的使用能大大地减少开发的代码量,所以在实际项目的开发中会使用到非常多的注解。特别是做一些公共基础的功能,比如日志记录,事务管理,权限控制这些功能,使用注解就非常高效且优雅。 249 | 250 | 对于自定义注解,主要有三个步骤,**定义注解,标记注解,解析注解**,并不是很难。 251 | 252 | 这篇文章讲到这里了,感谢大家的阅读,希望看完这篇文章能有所收获! 253 | 254 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 255 | 256 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 257 | 258 | ![](https://static.lovebilibili.com/dashacha.png) 259 | 260 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /MySQL数据库/MySQL与MVVC机制.md: -------------------------------------------------------------------------------- 1 | > **文章已收录Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 2 | 3 | # 前言 4 | 5 | 无论是上一篇文章讲的事务隔离级别,还是之前讲的undo log日志,其实都涉及到MVCC机制,那么什么是MVCC机制,它的作用是什么,下面就让我们带着问题一起学习吧。 6 | 7 | # 什么是MVCC 8 | 9 | MVCC全称是多版本并发控制 (Multi-Version Concurrency Control),只有在InnoDB引擎下存在。MVCC机制的作用其实就是避免同一个数据在不同事务之间的竞争,提高系统的并发性能。 10 | 11 | 它的特点如下: 12 | 13 | - 允许多个版本同时存在,并发执行。 14 | - 不依赖锁机制,性能高。 15 | - 只在读已提交和可重复读的事务隔离级别下工作。 16 | 17 | # 为什么使用MVCC 18 | 19 | 在早期的数据库中,只有读读之间的操作才可以并发执行,读写,写读,写写操作都要阻塞,这样就会导致MySQL的并发性能极差。 20 | 21 | 采用了MVCC机制后,只有写写之间相互阻塞,其他三种操作都可以并行,这样就可以提高了MySQL的并发性能。 22 | 23 | # MVCC机制的原理 24 | 25 | 在讲解MVCC机制的原理之前首先要介绍几个概念。 26 | 27 | ## ReadView 28 | 29 | ReadView可以理解为数据库中某一个时刻所有未提交事务的快照。ReadView有几个重要的参数: 30 | 31 | - m_ids:表示生成ReadView时,当前系统正在活跃的读写事务的事务Id列表。 32 | - min_trx_id:表示生成ReadView时,当前系统中活跃的读写事务的最小事务Id。 33 | - max_trx_id:表示生成ReadView时,当前时间戳InnoDB将在下一次分配的事务id。 34 | - creator_trx_id:当前事务id。 35 | 36 | 所以当创建ReadView时,可以知道这个时间点上未提交事务的所有信息。 37 | 38 | ## 隐藏列 39 | 40 | InnoDB存储引擎中,它的聚簇索引记录中都包含两个必要的隐藏列,分别是: 41 | 42 | - trx_id:事务Id,每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的`事务id`赋值给`trx_id`隐藏列。 43 | - roll_pointer:回滚指针,每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到`undo log`中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。 44 | 45 | ## 事务链 46 | 47 | 每次对记录进行修改时,都会记录一条undo log信息,每一条undo log信息都会有一个roll_pointer属性(INSERT操作没有这个属性,因为之前没有更早的版本),可以将这些undo日志都连起来,串成一个链表。事务链如下图一样: 48 | 49 | ![](https://static.lovebilibili.com/mysql_mvvc_01.png) 50 | 51 | ## 原理 52 | 53 | 我们都知道,MySQL事务隔离级别有四种,分别是读未提交(Read Uncommitted,简称RU)、读已提交(Read Committed,简称RC)、可重复读(Repeatable Read,简称RR)、串行化(Serializable),只有RC和RR才跟MVCC机制相关,RU和Serializable都不会使用到MVCC机制。因为在读未提交(RU)级别下是直接返回记录上的最新值,Serializable级别下则会对所有读取的行都加锁。 54 | 55 | RC和RR隔离级别的实现就是通过版本控制来完成,核心处理逻辑就是**判断所有版本中哪个版本是当前事务可见的处理**,通过什么判断呢?就是上文讲到的ReadView,ReadView包含了当前系统活跃的读写事务的信息,判断的逻辑如下: 56 | 57 | - 如果被访问版本的trx_id属性值小于ReadView的最小事务Id,表示该版本的事务在生成 ReadView 前已经提交,所以该版本可以被当前事务访问。 58 | - 如果被访问版本的trx_id属性值大于ReadView的最大事务Id,表示该版本的事务在生成 ReadView 后才生成,所以该版本不可以被当前事务访问。 59 | - 如果被访问版本的trx_id属性值在m_ids列表最小事务Id和最大事务Id之间,那就需要判断一下 trx_id 属性值是不是包含在 m_ids 列表中,如果包含的话,说明创建 ReadView 时生成该版本的事务还是活跃的,所以该版本不可以访问;如果不包含的话,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。 60 | 61 | 我们下面举例说明RC和RR隔离级别的区别,假如有一条user数据,初始值name="刘德华",然后经过下面的更新,时间点如下: 62 | 63 | ![](https://static.lovebilibili.com/mysql_mvvc_02.png) 64 | 65 | RC隔离级别的MVCC: 66 | 67 | **RC隔离级别的事务在每次查询开始时都会生成一个独立的 ReadView**。 68 | 69 | 在T4时间点时,版本链如下所示: 70 | 71 | ![](https://static.lovebilibili.com/mysql_mvvc_03.png) 72 | 73 | 在T4时间点的Select语句执行时,当前时间系统正在活跃的事务有trx_id为100和200都未提交,所以此时生成的ReadView的事务列表是[100,200],因此查询语句会根据当前版本链中小于事务列表中的最大的版本数据,即查询到的是刘德华。 74 | 75 | 在T6时间点时,版本链如下所示: 76 | 77 | ![](https://static.lovebilibili.com/mysql_mvvc_04.png) 78 | 79 | 在T6时间点的Select语句执行时,当前时间系统正在活跃的事务有trx_id为200未提交,所以此时生成的ReadView的事务列表时[200],因此查询语句会根据当前版本链中小于事务列表中的最大的版本数据,即查询到的是古天乐。 80 | 81 | 在T8时间点时,版本链如下所示: 82 | 83 | ![](https://static.lovebilibili.com/mysql_mvvc_05.png) 84 | 85 | 在T6时间点的Select语句执行时,当前时间系统正在活跃的事务都已经提交,所以此时生成的ReadView的事务列表为空,因此查询语句会直接查询当前数据库最新数据,即查询到的是麦长青。 86 | 87 | 由于每次查询都会生成新的ReadView,所以有可能出现不可重复读的问题。 88 | 89 | RR隔离级别的MVCC: 90 | 91 | **RR隔离级别的事务在第一次读取数据时生成ReadView,之后的查询都不会再生成,所以一个事务的查询结果每次都是一样的**。 92 | 93 | 因为三次查询都是在同一个事务tx_300中。 94 | 95 | 所以在第一次查询,也就是T4时间点时会生成ReadView,事务列表为[100,200],所以当前可见版本的查询结果为刘德华。 96 | 97 | 第二次查询,T6时间点不会生成新的ReadView,所以查询结果依然是刘德华。 98 | 99 | 第三次查询,T8时间一样,不会生成ReadView,沿用T4时间点生成的ReadView,所以查询结果依然是刘德华。 100 | 101 | ![](https://static.lovebilibili.com/mysql_mvvc_06.png) 102 | 103 | 由于在同一个事务中,RR级别的事务在查询中只会生成一个ReadView,所以能解决不可重复读的问题。 104 | 105 | # 总结 106 | 107 | 要理解MVCC机制,关键在于要理解ReadView、隐藏列、事务链三者在其中的作用。还有就是只有RC和RR的隔离级别才会使用MVCC机制,两者最大的区别在于生成ReadView的时机的不同,RC级别生成ReadView的时机是每次查询都会生成新的ReadView,而RR级别是在当前事务第一次查询时生成,并且生成的ReadView会一直沿用到事务提交为止,保证可重复读。 108 | 109 | 这篇文章就讲到这里了,感谢大家的阅读,希望看完大家能有所收获! 110 | 111 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 112 | 113 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 114 | 115 | ![](https://static.lovebilibili.com/dashacha.png) 116 | 117 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /MySQL数据库/MySQL主从复制读写分离,能讲一下吗.md: -------------------------------------------------------------------------------- 1 | # 思维导图 2 | 3 | ![](https://static.lovebilibili.com/mysql_zxfz_wedt.png) 4 | 5 | >微信公众号已开启:【**java技术爱好者**】,还没关注的记得关注哦~ 6 | > 7 | >**文章已收录到我的Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 8 | 9 | # 前言 10 | 11 | 在很多项目,特别是互联网项目,在使用MySQL时都会采用主从复制、读写分离的架构。 12 | 13 | 为什么要采用主从复制读写分离的架构?如何实现?有什么缺点?让我们带着这些问题开始这段学习之旅吧! 14 | 15 | # 为什么使用主从复制、读写分离 16 | 17 | 主从复制、读写分离一般是一起使用的。目的很简单,就是**为了提高数据库的并发性能**。你想,假设是单机,读写都在一台MySQL上面完成,性能肯定不高。如果有三台MySQL,一台mater只负责写操作,两台salve只负责读操作,性能不就能大大提高了吗? 18 | 19 | 所以**主从复制、读写分离就是为了数据库能支持更大的并发**。 20 | 21 | 随着业务量的扩展、如果是单机部署的MySQL,会导致I/O频率过高。采用**主从复制、读写分离可以提高数据库的可用性**。 22 | 23 | # 主从复制的原理 24 | 25 | ①当Master节点进行insert、update、delete操作时,会按顺序写入到binlog中。 26 | 27 | ②salve从库连接master主库,Master有多少个slave就会创建多少个binlog dump线程。 28 | 29 | ③当Master节点的binlog发生变化时,binlog dump 线程会通知所有的salve节点,并将相应的binlog内容推送给slave节点。 30 | 31 | ④I/O线程接收到 binlog 内容后,将内容写入到本地的 relay-log。 32 | 33 | ⑤SQL线程读取I/O线程写入的relay-log,并且根据 relay-log 的内容对从数据库做对应的操作。 34 | 35 | ![](https://static.lovebilibili.com/mysql_zcfz_1.png) 36 | 37 | # 如何实现主从复制 38 | 39 | 我这里用三台虚拟机(Linux)演示,IP分别是104(Master),106(Slave),107(Slave)。 40 | 41 | 预期的效果是一主二从,如下图所示: 42 | 43 | ![](https://static.lovebilibili.com/mysql_zcfz_2.png) 44 | 45 | ## Master配置 46 | 47 | 使用命令行进入mysql: 48 | 49 | ```java 50 | mysql -u root -p 51 | ``` 52 | 53 | 接着输入root用户的密码(密码忘记的话就网上查一下重置密码吧~),然后创建用户: 54 | 55 | ```java 56 | //192.168.0.106是slave从机的IP 57 | GRANT REPLICATION SLAVE ON *.* to 'root'@'192.168.0.106' identified by 'Java@1234'; 58 | //192.168.0.107是slave从机的IP 59 | GRANT REPLICATION SLAVE ON *.* to 'root'@'192.168.0.107' identified by 'Java@1234'; 60 | //刷新系统权限表的配置 61 | FLUSH PRIVILEGES; 62 | ``` 63 | 64 | 创建的这两个用户在配置slave从机时要用到。 65 | 66 | 接下来在找到mysql的配置文件/etc/my.cnf,增加以下配置: 67 | 68 | ```properties 69 | # 开启binlog 70 | log-bin=mysql-bin 71 | server-id=104 72 | # 需要同步的数据库,如果不配置则同步全部数据库 73 | binlog-do-db=test_db 74 | # binlog日志保留的天数,清除超过10天的日志 75 | # 防止日志文件过大,导致磁盘空间不足 76 | expire-logs-days=10 77 | ``` 78 | 79 | 配置完成后,重启mysql: 80 | 81 | ```java 82 | service mysql restart 83 | ``` 84 | 85 | 可以通过命令行`show master status\G;`查看当前binlog日志的信息(后面有用): 86 | 87 | ![](https://static.lovebilibili.com/mysql_zcfz_3.png) 88 | 89 | ## Slave配置 90 | 91 | Slave配置相对简单一点。从机肯定也是一台MySQL服务器,所以和Master一样,找到/etc/my.cnf配置文件,增加以下配置: 92 | 93 | ```properties 94 | # 不要和其他mysql服务id重复即可 95 | server-id=106 96 | ``` 97 | 98 | 接着使用命令行登录到mysql服务器: 99 | 100 | ```java 101 | mysql -u root -p 102 | ``` 103 | 104 | 然后输入密码登录进去。 105 | 106 | 进入到mysql后,再输入以下命令: 107 | 108 | ``` 109 | CHANGE MASTER TO 110 | MASTER_HOST='192.168.0.104',//主机IP 111 | MASTER_USER='root',//之前创建的用户账号 112 | MASTER_PASSWORD='Java@1234',//之前创建的用户密码 113 | MASTER_LOG_FILE='mysql-bin.000001',//master主机的binlog日志名称 114 | MASTER_LOG_POS=862,//binlog日志偏移量 115 | master_port=3306;//端口 116 | ``` 117 | 118 | 还没完,设置完之后需要启动: 119 | 120 | ```java 121 | # 启动slave服务 122 | start slave; 123 | ``` 124 | 125 | 启动完之后怎么校验是否启动成功呢?使用以下命令: 126 | 127 | ```java 128 | show slave status\G; 129 | ``` 130 | 131 | 可以看到如下信息(摘取部分关键信息): 132 | 133 | ``` 134 | *************************** 1. row *************************** 135 | Slave_IO_State: Waiting for master to send event 136 | Master_Host: 192.168.0.104 137 | Master_User: root 138 | Master_Port: 3306 139 | Connect_Retry: 60 140 | Master_Log_File: mysql-bin.000001 141 | Read_Master_Log_Pos: 619 142 | Relay_Log_File: mysqld-relay-bin.000001 143 | Relay_Log_Pos: 782 144 | Relay_Master_Log_File: mysql-bin.000001 //binlog日志文件名称 145 | Slave_IO_Running: Yes //Slave_IO线程、SQL线程都在运行 146 | Slave_SQL_Running: Yes 147 | Master_Server_Id: 104 //master主机的服务id 148 | Master_UUID: 0ab6b3a6-e21d-11ea-aaa3-080027f8d623 149 | Master_Info_File: /var/lib/mysql/master.info 150 | SQL_Delay: 0 151 | SQL_Remaining_Delay: NULL 152 | Slave_SQL_Running_State: Slave has read all relay log; waiting for the slave I/O thread to update it 153 | Master_Retry_Count: 86400 154 | Auto_Position: 0 155 | ``` 156 | 157 | 另一台slave从机配置一样,不再赘述。 158 | 159 | ## 测试主从复制 160 | 161 | 在master主机执行sql: 162 | 163 | ```sql 164 | CREATE TABLE `tb_commodity_info` ( 165 | `id` varchar(32) NOT NULL, 166 | `commodity_name` varchar(512) DEFAULT NULL COMMENT '商品名称', 167 | `commodity_price` varchar(36) DEFAULT '0' COMMENT '商品价格', 168 | `number` int(10) DEFAULT '0' COMMENT '商品数量', 169 | `description` varchar(2048) DEFAULT '' COMMENT '商品描述', 170 | PRIMARY KEY (`id`) 171 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品信息表'; 172 | ``` 173 | 174 | 接着我们可以看到两台slave从机同步也创建了商品信息表: 175 | 176 | ![](https://static.lovebilibili.com/mysql_zcfz_5.png) 177 | 178 | 主从复制就完成了!java技术爱好者有点东西哦~ 179 | 180 | # 读写分离 181 | 182 | 主从复制完成后,我们还需要实现读写分离,master负责写入数据,两台slave负责读取数据。怎么实现呢? 183 | 184 | 实现的方式有很多,以前我公司是采用AOP的方式,通过方法名判断,方法名中有get、select、query开头的则连接slave,其他的则连接master数据库。 185 | 186 | 但是通过AOP的方式实现起来代码有点繁琐,有没有什么现成的框架呢,答案是有的。 187 | 188 | Apache ShardingSphere 是一套开源的分布式数据库中间件解决方案组成的生态圈,它由 JDBC、Proxy两部分组成。 189 | 190 | ShardingSphere-JDBC定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。 191 | 192 | 读写分离就可以使用ShardingSphere-JDBC实现。 193 | 194 | ![](https://static.lovebilibili.com/mysql_zcfz_6.png) 195 | 196 | 下面演示一下SpringBoot+Mybatis+Mybatis-plus+druid+ShardingSphere-JDBC代码实现。 197 | 198 | ## 项目配置 199 | 200 | 版本说明: 201 | 202 | ```java 203 | SpringBoot:2.0.1.RELEASE 204 | druid:1.1.22 205 | mybatis-spring-boot-starter:1.3.2 206 | mybatis-plus-boot-starter:3.0.7 207 | sharding-jdbc-spring-boot-starter:4.1.1 208 | ``` 209 | 210 | 添加sharding-jdbc的maven配置: 211 | 212 | ```xml 213 | 214 | org.apache.shardingsphere 215 | sharding-jdbc-spring-boot-starter 216 | 4.1.1 217 | 218 | ``` 219 | 220 | 然后在application.yml添加配置: 221 | 222 | ```yaml 223 | # 这是使用druid连接池的配置,其他的连接池配置可能有所不同 224 | spring: 225 | shardingsphere: 226 | datasource: 227 | names: master,slave0,slave1 228 | master: 229 | type: com.alibaba.druid.pool.DruidDataSource 230 | driver-class-name: com.mysql.jdbc.Driver 231 | url: jdbc:mysql://192.168.0.108:3306/test_db?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT 232 | username: yehongzhi 233 | password: YHZ@1234 234 | slave0: 235 | type: com.alibaba.druid.pool.DruidDataSource 236 | driver-class-name: com.mysql.jdbc.Driver 237 | url: jdbc:mysql://192.168.0.109:3306/test_db?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT 238 | username: yehongzhi 239 | password: YHZ@1234 240 | slave1: 241 | type: com.alibaba.druid.pool.DruidDataSource 242 | driver-class-name: com.mysql.jdbc.Driver 243 | url: jdbc:mysql://192.168.0.110:3306/test_db?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT 244 | username: yehongzhi 245 | password: YHZ@1234 246 | props: 247 | sql.show: true 248 | masterslave: 249 | load-balance-algorithm-type: round_robin 250 | sharding: 251 | master-slave-rules: 252 | master: 253 | master-data-source-name: master 254 | slave-data-source-names: slave0,slave1 255 | ``` 256 | 257 | sharding.master-slave-rules是标明主库和从库,一定不要写错,否则写入数据到从库,就会导致无法同步。 258 | 259 | load-balance-algorithm-type是路由策略,round_robin表示轮询策略。 260 | 261 | 启动项目,可以看到以下信息,代表配置成功: 262 | 263 | ![](https://static.lovebilibili.com/mysql_zcfz_8.png) 264 | 265 | 编写Controller接口: 266 | 267 | ```java 268 | /** 269 | * 添加商品 270 | * 271 | * @param commodityName 商品名称 272 | * @param commodityPrice 商品价格 273 | * @param description 商品价格 274 | * @param number 商品数量 275 | * @return boolean 是否添加成功 276 | * @author java技术爱好者 277 | */ 278 | @PostMapping("/insert") 279 | public boolean insertCommodityInfo(@RequestParam(name = "commodityName") String commodityName, 280 | @RequestParam(name = "commodityPrice") String commodityPrice, 281 | @RequestParam(name = "description") String description, 282 | @RequestParam(name = "number") Integer number) throws Exception { 283 | return commodityInfoService.insertCommodityInfo(commodityName, commodityPrice, description, number); 284 | } 285 | ``` 286 | 287 | 准备就绪,开始测试! 288 | 289 | ## 测试 290 | 291 | 打开POSTMAN,添加商品: 292 | 293 | ![](https://static.lovebilibili.com/mysql_zcfz_9.png) 294 | 295 | 控制台可以看到如下信息: 296 | 297 | ![](https://static.lovebilibili.com/mysql_zcfz_11.png) 298 | 299 | 查询数据的话则通过slave进行: 300 | 301 | ![](https://static.lovebilibili.com/mysql_zcfz_12.png) 302 | 303 | ![](https://static.lovebilibili.com/mysql_zcfz_13.png) 304 | 305 | 就是这么简单! 306 | 307 | # 缺点 308 | 309 | 尽管主从复制、读写分离能很大程度保证MySQL服务的高可用和提高整体性能,但是问题也不少: 310 | 311 | - **从机是通过binlog日志从master同步数据的,如果在网络延迟的情况,从机就会出现数据延迟。那么就有可能出现master写入数据后,slave读取数据不一定能马上读出来**。 312 | 313 | 可能有人会问,有没有事务问题呢? 314 | 315 | 实际上这个框架已经想到了,我们看回之前的那个截图,有一句话是这样的: 316 | 317 | ![](https://static.lovebilibili.com/mysql_zcfz_14.png) 318 | 319 | 320 | 321 | > 微信公众号已开启:【java技术爱好者】,没关注的同学记得关注哦~ 322 | > 323 | > 坚持原创,持续输出兼具广度和深度的技术文章。 324 | 325 | 上面所有例子的代码都上传Github了: 326 | 327 | > https://github.com/yehongzhi/mall 328 | 329 | **你的点赞是我创作的最大动力**~ 330 | 331 | **拒绝做一条咸鱼,我是一个努力让大家记住的程序员。我们下期再见!!!** 332 | 333 | -------------------------------------------------------------------------------- /MySQL数据库/什么是脏读、不可重复读、幻读.md: -------------------------------------------------------------------------------- 1 | > **文章已收录Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 2 | 3 | # 脏读、不可重复读、幻读 4 | 5 | 在现代关系型数据库中,事务机制是非常重要的,假如在多个事务并发操作数据库时,如果没有有效的机制进行避免就会导致出现脏读,不可重复读,幻读。 6 | 7 | ## 脏读 8 | 9 | 1、在事务A执行过程中,事务A对数据资源进行了修改,事务B读取了事务A修改后的数据。 10 | 11 | 2、由于某些原因,事务A并没有完成提交,发生了RollBack操作,则事务B读取的数据就是脏数据。 12 | 13 | 这种读取到另一个事务未提交的数据的现象就是脏读(Dirty Read)。 14 | 15 | ![](https://static.lovebilibili.com/mysql_sw_1.png) 16 | 17 | ## 不可重复读 18 | 19 | 事务B读取了两次数据资源,在这两次读取的过程中事务A修改了数据,导致事务B在这两次读取出来的数据不一致。 20 | 21 | 这种在同一个事务中,前后两次读取的数据不一致的现象就是不可重复读(Nonrepeatable Read)。 22 | 23 | ![](https://static.lovebilibili.com/mysql_sw_2.png) 24 | 25 | ## 幻读 26 | 27 | 事务B前后两次读取同一个范围的数据,在事务B两次读取的过程中事务A新增了数据,导致事务B后一次读取到前一次查询没有看到的行。 28 | 29 | 幻读和不可重复读有些类似,但是幻读强调的是集合的增减,而不是单条数据的更新。 30 | 31 | ![](https://static.lovebilibili.com/mysql_sw_3.png) 32 | 33 | ## 第一类更新丢失 34 | 35 | 事务A和事务B都对数据进行更新,但是事务A由于某种原因事务回滚了,把已经提交的事务B的更新数据给覆盖了。这种现象就是第一类更新丢失。 36 | 37 | ![](https://static.lovebilibili.com/mysql_sw_4.png) 38 | 39 | ## 第二类更新丢失 40 | 41 | 其实跟第一类更新丢失有点类似,也是两个事务同时对数据进行更新,但是事务A的更新把已提交的事务B的更新数据给覆盖了。这种现象就是第二类更新丢失。 42 | 43 | ![](https://static.lovebilibili.com/mysql_sw_5.png) 44 | 45 | # 事务隔离级别 46 | 47 | 为了解决以上的问题,主流的关系型数据库都会提供四种事务的隔离级别。事务隔离级别从低到高分别是:读未提交、读已提交、可重复读、串行化。事务隔离级别等级越高,越能保证数据的一致性和完整性,但是执行效率也越低。所以在设置数据库的事务隔离级别时需要做一下权衡,**MySQL默认是可重复读的级别**。 48 | 49 | ## 读未提交 50 | 51 | 读未提交(Read Uncommitted),是最低的隔离级别,所有的事务都可以看到其他未提交的事务的执行结果。只能防止第一类更新丢失,不能解决脏读,可重复读,幻读,所以很少应用于实际项目。 52 | 53 | ## 读已提交 54 | 55 | 读已提交(Read Committed), 在该隔离级别下,一个事务的更新操作结果只有在**该事务提交之后,另一个事务才可能读取到同一笔数据更新后的结果**。可以防止脏读和第一类更新丢失,但是不能解决可重复读和幻读的问题。 56 | 57 | ## 可重复读 58 | 59 | 可重复读(Repeatable Read),MySQL默认的隔离级别。在该隔离级别下,一个事务多次读同一个数据,在这个事务还没结束时,其他事务不能访问该数据(包括了读写),这样就可以在同一个事务内两次读到的数据是一样的。可以防止脏读、不可重复读、第一类更新丢失、第二类更新丢失的问题,不过还是会出现幻读。 60 | 61 | ## 串行化 62 | 63 | 串行化(Serializable),这是最高的隔离级别。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。在这个级别,可以解决上面提到的所有并发问题,但可能导致大量的超时现象和锁竞争,通常不会用这个隔离级别。 64 | 65 | # 总结 66 | 67 | 下面我们对事务的隔离级别和对并发问题的解决情况,请看下图: 68 | 69 | ![](https://static.lovebilibili.com/mysql_sw_6.png) 70 | 71 | 这篇文章就讲到这里了,感谢大家的阅读,希望看完大家能有所收获! 72 | 73 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 74 | 75 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 76 | 77 | ![](https://static.lovebilibili.com/dashacha.png) 78 | 79 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /MySQL数据库/什么是雪花ID?.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 什么是雪花ID? 3 | date: 2021-05-30 21:32:44 4 | index_img: https://static.lovebilibili.com/snowflake_index.png 5 | tags: 6 | - MySQL 7 | --- 8 | 9 | > **文章已收录Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 10 | 11 | # 为什么使用雪花ID 12 | 13 | 在以前的项目中,最常见的两种主键类型是自增Id和UUID,在比较这两种ID之前首先要搞明白一个问题,就是为什么主键有序比无序查询效率要快,因为自增Id和UUID之间最大的不同点就在于有序性。 14 | 15 | 我们都知道,当我们定义了主键时,数据库会选择表的主键作为聚集索引(B+Tree),mysql 在底层是以数据页为单位来存储数据的。 16 | 17 | 也就是说如果主键为`自增 id `的话,mysql 在写满一个数据页的时候,直接申请另一个新数据页接着写就可以了。**如果一个数据页存满了,mysql 就会去申请一个新的数据页来存储数据**。如果主键是`UUID`,为了确保索引有序,mysql 就需要将每次插入的数据都放到合适的位置上。**这就造成了页分裂,这个大量移动数据的过程是会严重影响插入效率的**。 18 | 19 | 一句话总结就是,InnoDB表的数据写入顺序能和B+树索引的叶子节点顺序一致的话,这时候存取效率是最高的。 20 | 21 | 但是为什么很多情况又不用`自增id`作为主键呢? 22 | 23 | - 容易导致主键重复。比如导入旧数据时,线上又有新的数据新增,这时就有可能在导入时发生主键重复的异常。为了避免导入数据时出现主键重复的情况,要选择在应用停业后导入旧数据,导入完成后再启动应用。显然这样会造成不必要的麻烦。而UUID作为主键就不用担心这种情况。 24 | - 不利于数据库的扩展。当采用自增id时,分库分表也会有主键重复的问题。UUID则不用担心这种问题。 25 | 26 | 那么问题就来了,`自增id`会担心主键重复,`UUID`不能保证有序性,有没有一种ID既是有序的,又是唯一的呢? 27 | 28 | 当然有,就是`雪花ID`。 29 | 30 | # 什么是雪花ID 31 | 32 | snowflake是Twitter开源的分布式ID生成算法,结果是64bit的Long类型的ID,有着全局唯一和有序递增的特点。 33 | 34 | ![](https://static.lovebilibili.com/snowflake_01.png) 35 | 36 | - 最高位是符号位,因为生成的 ID 总是正数,始终为0,不可用。 37 | - 41位的时间序列,精确到毫秒级,41位的长度可以使用69年。时间位还有一个很重要的作用是可以根据时间进行排序。 38 | - 10位的机器标识,10位的长度最多支持部署1024个节点。 39 | - 12位的计数序列号,序列号即一系列的自增ID,可以支持同一节点同一毫秒生成多个ID序号,12位的计数序列号支持每个节点每毫秒产生4096个ID序号。 40 | 41 | 缺点也是有的,就是强依赖机器时钟,如果机器上时钟回拨,有可能会导致主键重复的问题。 42 | 43 | # Java实现雪花ID 44 | 45 | 下面是用Java实现雪花ID的代码,供大家参考一下。 46 | 47 | ```java 48 | public class SnowflakeIdWorker { 49 | /** 50 | * 开始时间:2020-01-01 00:00:00 51 | */ 52 | private final long beginTs = 1577808000000L; 53 | 54 | private final long workerIdBits = 10; 55 | 56 | /** 57 | * 2^10 - 1 = 1023 58 | */ 59 | private final long maxWorkerId = -1L ^ (-1L << workerIdBits); 60 | 61 | private final long sequenceBits = 12; 62 | 63 | /** 64 | * 2^12 - 1 = 4095 65 | */ 66 | private final long maxSequence = -1L ^ (-1L << sequenceBits); 67 | 68 | /** 69 | * 时间戳左移22位 70 | */ 71 | private final long timestampLeftOffset = workerIdBits + sequenceBits; 72 | 73 | /** 74 | * 业务ID左移12位 75 | */ 76 | private final long workerIdLeftOffset = sequenceBits; 77 | 78 | /** 79 | * 合并了机器ID和数据标示ID,统称业务ID,10位 80 | */ 81 | private long workerId; 82 | 83 | /** 84 | * 毫秒内序列,12位,2^12 = 4096个数字 85 | */ 86 | private long sequence = 0L; 87 | 88 | /** 89 | * 上一次生成的ID的时间戳,同一个worker中 90 | */ 91 | private long lastTimestamp = -1L; 92 | 93 | public SnowflakeIdWorker(long workerId) { 94 | if (workerId > maxWorkerId || workerId < 0) { 95 | throw new IllegalArgumentException(String.format("WorkerId必须大于或等于0且小于或等于%d", maxWorkerId)); 96 | } 97 | 98 | this.workerId = workerId; 99 | } 100 | 101 | public synchronized long nextId() { 102 | long ts = System.currentTimeMillis(); 103 | if (ts < lastTimestamp) { 104 | throw new RuntimeException(String.format("系统时钟回退了%d毫秒", (lastTimestamp - ts))); 105 | } 106 | 107 | // 同一时间内,则计算序列号 108 | if (ts == lastTimestamp) { 109 | // 序列号溢出 110 | if (++sequence > maxSequence) { 111 | ts = tilNextMillis(lastTimestamp); 112 | sequence = 0L; 113 | } 114 | } else { 115 | // 时间戳改变,重置序列号 116 | sequence = 0L; 117 | } 118 | 119 | lastTimestamp = ts; 120 | 121 | // 0 - 00000000 00000000 00000000 00000000 00000000 0 - 00000000 00 - 00000000 0000 122 | // 左移后,低位补0,进行按位或运算相当于二进制拼接 123 | // 本来高位还有个0<<63,0与任何数字按位或都是本身,所以写不写效果一样 124 | return (ts - beginTs) << timestampLeftOffset | workerId << workerIdLeftOffset | sequence; 125 | } 126 | 127 | /** 128 | * 阻塞到下一个毫秒 129 | * 130 | * @param lastTimestamp 131 | * @return 132 | */ 133 | private long tilNextMillis(long lastTimestamp) { 134 | long ts = System.currentTimeMillis(); 135 | while (ts <= lastTimestamp) { 136 | ts = System.currentTimeMillis(); 137 | } 138 | 139 | return ts; 140 | } 141 | 142 | public static void main(String[] args) { 143 | SnowflakeIdWorker snowflakeIdWorker = new SnowflakeIdWorker(7); 144 | for (int i = 0; i < 10; i++) { 145 | long id = snowflakeIdWorker.nextId(); 146 | System.out.println(id); 147 | } 148 | } 149 | } 150 | ``` 151 | 152 | main方法,测试结果如下: 153 | 154 | ```java 155 | 184309536616640512 156 | 184309536616640513 157 | 184309536616640514 158 | 184309536616640515 159 | 184309536616640516 160 | 184309536616640517 161 | 184309536616640518 162 | 184309536616640519 163 | 184309536616640520 164 | 184309536616640521 165 | ``` 166 | 167 | # 总结 168 | 169 | 在大部分公司的开发项目中里,雪花ID是主流的ID生成策略,除了自己实现之外,目前市场上也有很多开源的实现,比如: 170 | 171 | - 美团开源的[Leaf](https://github.com/Meituan-Dianping/Leaf) 172 | - 百度开源的[UidGenerator](https://github.com/baidu/uid-generator) 173 | 174 | 有兴趣的可以自行观摩一下,那么这篇文章就写到这里了,感谢大家的阅读。 175 | 176 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 177 | 178 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 179 | 180 | ![](https://static.lovebilibili.com/dashacha.png) 181 | 182 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /MySQL数据库/必须了解的mysql三种log.md: -------------------------------------------------------------------------------- 1 | > **文章已收录Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 2 | 3 | # 前言 4 | 5 | 大家有没有想过为什么MySQL数据库可以实现主从复制,实现持久化,实现回滚的呢?其实关键在于MySQL里的三种`log`,分别是: 6 | 7 | - binlog 8 | - redo log 9 | - undo log 10 | 11 | 这三种log也是面试经常会问的问题,下面我们一起来探讨一下吧。 12 | 13 | # 一、binlog 14 | 15 | binlog应该是日常中听的最多的关于mysql中的log。 16 | 17 | > 那么什么是binlog呢? 18 | 19 | binlog是用于**记录数据库表结构和表数据变更的二进制日志**,比如insert、update、delete、create、truncate等等操作,不会记录select、show操作,因为没有对数据本身发生变更。 20 | 21 | > binlog文件长什么样子呢? 22 | 23 | 使用`mysqlbinlog`命令可以查看。 24 | 25 | ![](https://static.lovebilibili.com/mysql_log_1.png) 26 | 27 | 会记录下每条变更的sql语句,还有执行开始时间,结束时间,事务id等等信息。 28 | 29 | > 如何查看binlog是否打开,如果没打开怎么设置? 30 | 31 | 使用命令`show variables like '%log_bin%';`查看binlog是否打开。 32 | 33 | ![](https://static.lovebilibili.com/mysql_log_2.png) 34 | 35 | 如果像上图一样,没有开启binlog,那怎么开启呢? 36 | 37 | 找到`my.cnf`配置文件,增加下面配置(mysql版本5.7.31): 38 | 39 | ```cnf 40 | # 打开binlog 41 | log-bin=mysql-bin 42 | # 选择ROW(行)模式 43 | binlog-format=ROW 44 | ``` 45 | 46 | 修改后,重启mysql,配置生效。 47 | 48 | 执行`SHOW MASTER STATUS;`可以查看当前写入的binlog文件名。 49 | 50 | ![](https://static.lovebilibili.com/mysql_log_3.png) 51 | 52 | > binlog用来干嘛的呢? 53 | 54 | 第一,用于主从复制。一般在公司中做一主二从的结构时,就需要master节点打开binlog日志,从机订阅binlog日志的信息,因为binlog日志记录了数据库数据的变更,所以当master发生数据变更时,从机也能随着master节点的数据变更而变更,做到主从复制的效果。 55 | 56 | ![](https://static.lovebilibili.com/mysql_log_5.jpg) 57 | 58 | 第二,用于数据恢复。因为binlog记录了数据库的变更,所以可以用于数据恢复。我们看到上面图中有个字段叫Position,这个参数是用于记录binlog日志的指针。当我们需要恢复数据时,只要指定**--start-position**和**--stop-position**,或者指定**--start-datetime**和**--stop-datetime**,那么就可以恢复指定区间的数据。 59 | 60 | # 二、redo log 61 | 62 | 假设有一条update语句: 63 | 64 | ```sql 65 | UPDATE `user` SET `name`='刘德华' WHERE `id`='1'; 66 | ``` 67 | 68 | 我们想象一下mysql修改数据的步骤,肯定是先把`id`='1'的数据查出来,然后修改名称为'刘德华'。再深层一点,mysql是使用页作为存储结构,所以MySQL会先把这条记录所在的页加载到内存中,然后对记录进行修改。但是我们都知道mysql支持持久化,最终数据都是存在于磁盘中。 69 | 70 | 假设需要修改的数据加载到内存中,并且修改成功了,但是还没来得及刷到磁盘中,这时数据库宕机了,那么这次修改成功后的数据就丢失了。 71 | 72 | 为了避免出现这种问题,MySQL引入了redo log。 73 | 74 | ![](https://static.lovebilibili.com/mysql_log_4.png) 75 | 76 | 如图所示,当执行数据变更操作时,首先把数据也加载到内存中,然后在内存中进行更新,更新完成后写入到redo log buffer中,然后由redo log buffer在写入到redo log file中。 77 | 78 | redo log file记录着xxx页做了xxx修改,所以即使mysql发生宕机,也可以通过redo log进行数据恢复,也就是说在内存中更新成功后,即使没有刷新到磁盘中,但也不会因为宕机而导致数据丢失。 79 | 80 | > redo log与事务机制是如何配合工作的? 81 | 82 | 83 | 84 | ![](https://static.lovebilibili.com/mysql_log_7.png) 85 | 86 | 如图所示: 87 | 88 | 第1-3步骤就是把数据变更,然后写入到内存中。 89 | 90 | 第4步记录到redo log中,然后把记录置为prepare(准备)状态。 91 | 92 | 第5,6步提交事务,提交事务之后,第7步把记录状态改成commit(提交)状态。 93 | 94 | 保证了事务与redo log的一致性。 95 | 96 | > binlog和redo log都可以数据恢复,有什么区别? 97 | 98 | redo log是恢复在内存更新后,还没来得及刷到磁盘的数据。 99 | 100 | binlog是存储所有数据变更的情况,理论上只要记录在binlog上的数据,都可以恢复。 101 | 102 | 举个例子,**假如不小心整个数据库的数据被删除了,能使用redo log文件恢复数据吗**? 103 | 104 | 不可以使用redo log文件恢复,只能使用binlog文件恢复。因为redo log文件不会存储历史所有的数据的变更,当内存数据刷新到磁盘中,redo log的数据就失效了,也就是redo log文件内容是会被覆盖的。 105 | 106 | > binlog又是在什么时候记录的呢? 107 | 108 | 答,在提交事务的时候。 109 | 110 | ![](https://static.lovebilibili.com/mysql_log_8.png) 111 | 112 | # 三、undo log 113 | 114 | undo log的作用主要**用于回滚**,mysql数据库的事务的原子性就是通过undo log实现的。我们都知道原子性是指对数据库的一系列操作,要么全部成功,要么全部失败。 115 | 116 | undo log主要存储的是数据的逻辑变化日志,比如说我们要`insert`一条数据,那么undo log就会生成一条对应的delete日志。简单点说,undo log记录的是数据修改之前的数据,因为需要支持回滚。 117 | 118 | 那么当需要回滚时,只需要利用undo log的日志就可以恢复到修改前的数据。 119 | 120 | undo log另一个作用是**实现多版本控制(MVCC)**,undo记录中包含了记录更改前的镜像,**如果更改数据的事务未提交**,对于隔离级别大于等于read commit的事务而言,**不应该返回更改后数据,而应该返回老版本的数据**。 121 | 122 | # 总结 123 | 124 | 学完之后,我们知道这三种日志在mysql中都有着重要的作用,再回顾一下: 125 | 126 | - binlog主要用于复制和数据恢复。 127 | - redo log用于恢复在内存更新后,还没来得及刷到磁盘的数据。 128 | - undo log用于实现回滚和多版本控制。 129 | 130 | 这篇文章就讲到这里了,感谢大家的阅读,希望看完大家能有所收获! 131 | 132 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 133 | 134 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 135 | 136 | ![](https://static.lovebilibili.com/dashacha.png) 137 | 138 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! 139 | 140 | -------------------------------------------------------------------------------- /MySQL数据库/要精通SQL优化?那就学一学explain吧.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | > **文章已收录Github精选,欢迎Star**:[https://github.com/yehongzhi](https://github.com/yehongzhi/learningSummary) 4 | 5 | # 前言 6 | 7 | 在MySQL中,我们知道加索引能提高查询效率,这基本上算是常识了。但是有时候,我们加了索引还是觉得SQL查询效率低下,我想看看**有没有使用到索引,扫描了多少行,表的加载顺序**等等,怎么查看呢?其实MySQL自带的SQL分析神器**Explain执行计划**就能完成以上的事情! 8 | 9 | # Explain有哪些信息 10 | 11 | 先确认一下试验的MySQL版本,这里使用的是`5.7.31`版本。 12 | 13 | ![](https://static.lovebilibili.com/mysql_explain_01.png) 14 | 15 | 只需要在SQL语句前加上explain关键字就可以查看执行计划,执行计划包括以下信息:id、select_type、table、partitions、type、possible_keys、key、key_len、ref、rows、filtered、Extra,总共12个字段信息。 16 | 17 | ![](https://static.lovebilibili.com/mysql_explain_02.png) 18 | 19 | 然后创建三个表: 20 | 21 | ```sql 22 | CREATE TABLE `tb_student` ( 23 | `id` int(10) NOT NULL AUTO_INCREMENT, 24 | `name` varchar(36) NOT NULL, 25 | PRIMARY KEY (`id`), 26 | KEY `index_name` (`name`) USING BTREE 27 | ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COMMENT='学生表'; 28 | 29 | CREATE TABLE `tb_class` ( 30 | `id` INT(10) primary key not null auto_increment, 31 | `name` VARCHAR(36) NOT NULL, 32 | `stu_id` INT(10) NOT NULL, 33 | `tea_id` INT(10) NOT NULL 34 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班级表'; 35 | 36 | CREATE TABLE `tb_teacher` ( 37 | `id` INT(10) primary key not null auto_increment, 38 | `name` VARCHAR(36) NOT NULL 39 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='教师表'; 40 | ``` 41 | 42 | # Explain执行计划详解 43 | 44 | explain的使用很简单,只需要在SQL语句前加上关键字`explain`即可,关键是怎么看explain执行后返回的字段信息,这才是重点。 45 | 46 | ## 一、id 47 | 48 | SELECT识别符。这是SELECT的查询序列号。**SQL执行的顺序的标识,SQL从大到小的执行**。id列有以下几个注意点: 49 | 50 | - id相同时,执行顺序由上至下。 51 | - id不同时,如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行。 52 | 53 | ```sql 54 | EXPLAIN SELECT * FROM `tb_student` WHERE id IN (SELECT stu_id FROM tb_class WHERE tea_id IN(SELECT id FROM tb_teacher WHERE `name` = '马老师')); 55 | ``` 56 | 57 | ![](https://static.lovebilibili.com/mysql_explain_03.png) 58 | 59 | 根据原则,当id不同时,SQL从大到小执行,id相同则从上到下执行。 60 | 61 | ## 二、select_type 62 | 63 | 表示select查询的类型,用于区分各种复杂的查询,例如普通查询,联合查询,子查询等等。 64 | 65 | ### SIMPLE 66 | 67 | 表示最简单的查询操作,也就是查询SQL语句中没有子查询、union等操作。 68 | 69 | ### PRIMARY 70 | 71 | 当查询语句中包含复杂查询的子部分,表示复杂查询中最外层的 select。 72 | 73 | ### SUBQUERY 74 | 75 | 当 `select` 或 `where` 中包含有子查询,该子查询被标记为SUBQUERY。 76 | 77 | ### DERIVED 78 | 79 | 在SQL语句中包含在`from`子句中的子查询。 80 | 81 | ### UNION 82 | 83 | 表示在union中的第二个和随后的select语句。 84 | 85 | ### UNION RESULT 86 | 87 | 代表从`union`的临时表中读取数据。 88 | 89 | ```sql 90 | EXPLAIN SELECT u.`name` FROM ((SELECT s.id,s.`name` FROM `tb_student` s) UNION (SELECT t.id,t.`name` FROM tb_teacher t)) AS u; 91 | ``` 92 | 93 | ``代表是id为2和3的select查询的结果进行union操作。 94 | 95 | ![](https://static.lovebilibili.com/mysql_explain_04.png) 96 | 97 | ### MATERIALIZED 98 | 99 | `MATERIALIZED`表示物化子查询,子查询来自视图。 100 | 101 | ## 三、table 102 | 103 | 表示输出结果集的表的表名,并不一定是真实存在的表,也有可能是别名,临时表等等。 104 | 105 | ## 四、partitions 106 | 107 | 表示SQL语句查询时匹配到的分区信息,对于非分区表值为NULL,当查询的是分区表则会显示分区表命中的分区情况。 108 | 109 | ## 五、type 110 | 111 | 需要重点关注的一个字段信息,表示查询使用了哪种类型,在 `SQL`优化中是一个非常重要的指标,依次从优到差分别是:**system > const > eq_ref > ref > range > index > ALL**。 112 | 113 | ### system和const 114 | 115 | **单表中最多有一条匹配行,查询效率最高,所以这个匹配行的其他列的值可以被优化器在当前查询中当作常量来处理**。通常出现在根据主键或者唯一索引进行的查询,system是const的特例,表里只有一条元组匹配时(系统表)为system。 116 | 117 | ![](https://static.lovebilibili.com/mysql_explain_05.png) 118 | 119 | ![](https://static.lovebilibili.com/mysql_explain_06.png) 120 | 121 | ### eq_ref 122 | 123 | primary key 或 unique key 索引的所有部分被连接使用 ,最多只会返回一条符合条件的记录,所以这种类型常出现在多表的join查询。 124 | 125 | ![](https://static.lovebilibili.com/mysql_explain_07.png) 126 | 127 | ### ref 128 | 129 | 相比**eq_ref**,不使用唯一索引,而是使用普通索引或者唯一性索引的部分前缀,可能会找到多个符合条件的行。 130 | 131 | ![](https://static.lovebilibili.com/mysql_explain_08.png) 132 | 133 | ### range 134 | 135 | 使用索引选择行,仅检索给定范围内的行。一般来说是针对一个有索引的字段,给定范围检索数据,通常出现在where语句中使用 `bettween...and`、`<`、`>`、`<=`、`in` 等条件查询 。 136 | 137 | ![](https://static.lovebilibili.com/mysql_explain_09.png) 138 | 139 | ### index 140 | 141 | 扫描全表索引,通常比ALL要快一些。 142 | 143 | ![](https://static.lovebilibili.com/mysql_explain_10.png) 144 | 145 | ### ALL 146 | 147 | **全表扫描,MySQL遍历全表来找到匹配行**,性能最差。 148 | 149 | ![](https://static.lovebilibili.com/mysql_explain_11.png) 150 | 151 | ## 六、possible_keys 152 | 153 | 表示在查询中可能使用到的索引来查找,别列出的索引并不一定是最终查询数据所用到的索引。 154 | 155 | ## 七、key 156 | 157 | 跟possible_keys有所区别,key表示查询中实际使用到的索引,若没有使用到索引则显示为NULL。 158 | 159 | ## 八、key_len 160 | 161 | 表示查询用到的索引key的长度(字节数)。如果单列索引,那么就会把整个索引长度计算进去,如果是联合索引,不是所有的列都用到,那么就只计算实际用到的列,因此可以**根据key_len来判断联合索引是否生效**。 162 | 163 | ## 九、ref 164 | 165 | 显示了哪些列或常量被用于查找索引列上的值。常见的值有:`const`,`func`,`null`,字段名。 166 | 167 | ## 十、rows 168 | 169 | mysql估算要找到我们所需的记录,需要读取的行数。可以通过这个数据很直观的显示 `SQL` 性能的好坏,一般情况下 `rows` 值越小越好。 170 | 171 | ## 十一、filtered 172 | 173 | 指返回结果的行占需要读到的行(rows列的值)的百分比,一般来说越大越好。 174 | 175 | ## 十二、Extra 176 | 177 | 表示额外的信息。此字段能够给出让我们深入理解执行计划进一步的细节信息。 178 | 179 | ### Using index 180 | 181 | 说明在select查询中使用了覆盖索引。覆盖索引的好处是一条SQL通过索引就可以返回我们需要的数据。 182 | 183 | ![](https://static.lovebilibili.com/mysql_explain_12.png) 184 | 185 | ### Using where 186 | 187 | 查询时没使用到索引,然后通过where条件过滤获取到所需的数据。 188 | 189 | ![](https://static.lovebilibili.com/mysql_explain_13.png) 190 | 191 | ### Using temporary 192 | 193 | 表示在查询时,MySQL需要创建一个临时表来保存结果。临时表一般会比较影响性能,应该尽量避免。 194 | 195 | ![](https://static.lovebilibili.com/mysql_explain_14.png) 196 | 197 | 有时候使用DISTINCT去重时也会产生Using temporary。 198 | 199 | ![](https://static.lovebilibili.com/mysql_explain_15.png) 200 | 201 | ### **Using filesort** 202 | 203 | 我们知道索引除了查询中能起作用外,排序也是能起到作用的,所以当SQL中包含 ORDER BY 操作,而且**无法利用索引完成排序操作**的时候,MySQL不得不选择相应的排序算法来实现,这时就会出现**Using filesort**,应该尽量避免使用**Using filesort**。 204 | 205 | ![](https://static.lovebilibili.com/mysql_explain_16.png) 206 | 207 | # 总结 208 | 209 | 一般优化SQL语句第一步是要知道这条SQL语句有哪些需要优化的,explain执行计划就相当于一面镜子,能把详细的执行情况给开发者列出来。所以说善用explain执行计划,能解决80%的SQL优化问题。 210 | 211 | explain的信息中,一般我们要关心的是type,看是什么级别,如果是在互联网公司一般需要在range以上的级别,接着关心的是Extra,有没有出现filesort或者using template,一旦出现就要想办法避免,接着再看key使用的是什么索引,还有看filtered筛选比是多少。 212 | 213 | 这篇文章就讲到这里了,希望大家看完之后能对SQL优化有更深入的理解,感谢大家的阅读。 214 | 215 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 216 | 217 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 218 | 219 | ![](https://static.lovebilibili.com/dashacha.png) 220 | 221 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /MySQL数据库/谈谈MYSQL索引是如何提高查询效率的.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 谈谈MYSQL索引是如何提高查询效率的 3 | date: 2021-05-30 21:43:36 4 | index_img: https://static.lovebilibili.com/mysql_suoyin_index.jpg 5 | tags: 6 | - MySQL 7 | --- 8 | 9 | > **文章已收录Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 10 | 11 | # 前言 12 | 13 | 我们都知道当查询数据库变慢时,需要建索引去优化。但是只知道索引能优化显然是不够的,我们更应该知道索引的原理,因为不是加了索引就一定会提升性能。那么接下来就一起探索MYSQL索引的原理吧。 14 | 15 | # 什么是索引 16 | 17 | 索引其实是一种能高效帮助MYSQL获取数据的数据结构,通常保存在磁盘文件中,好比一本书的目录,能加快数据库的查询速度。除此之外,索引是有序的,所以也能提高数据的排序效率。 18 | 19 | 通常MYSQL的索引包括聚簇索引,覆盖索引,复合索引,唯一索引,普通索引,通常底层是B+树的数据结构。 20 | 21 | 总结一下,索引的优势在于: 22 | 23 | - 提高查询效率。 24 | - 降低数据排序的成本。 25 | 26 | 缺点在于: 27 | 28 | - 索引会占用磁盘空间。 29 | - 索引会降低更新表的效率。因为在更新数据时,要额外维护索引文件。 30 | 31 | # 索引的类型 32 | 33 | - 聚簇索引 34 | 35 | 索引列的值必须是唯一的,并且不能为空,一个表只能有一个聚簇索引。 36 | 37 | - 唯一索引 38 | 39 | 索引列的值是唯一的,值可以为空。 40 | 41 | - 普通索引 42 | 43 | 没有什么限制,允许在定义索引的列中插入重复值和空值。 44 | 45 | - 复合索引 46 | 47 | 也叫组合索引,用户可以在多个列上组合建立索引,遵循“最左匹配原则”,在条件允许的情况下使用复合索引可以替代多个单列索引的使用。 48 | 49 | # 索引的数据结构 50 | 51 | 我们都知道索引的底层数据结构采用的是B+树,但是在讲B+树之前,要先知道B树,因为B+树是在B树上面进行改进优化的。 52 | 53 | 首先讲一下B树的特点: 54 | 55 | - B树的每个节点都存储了多个元素,每个内节点都有多个分支。 56 | - 节点中元素包含键值和数据,节点中的键值从小到大排序。 57 | - 父节点的数据不会出现在子节点中。 58 | - 所有的叶子节点都在同一层,叶节点具有相同的深度。 59 | 60 | ![](https://static.lovebilibili.com/mysql_suoyin_1.png) 61 | 62 | 在上面的B树中,假如我们要找值等于18的数据,查找路径就是磁盘块1->磁盘块3->磁盘块8。 63 | 64 | 过程如下: 65 | 66 | 第一次磁盘IO:首先加载磁盘块1到内存中,在内存中遍历比较,因为17<18<50,所以走中间P2,定位到磁盘块3。 67 | 68 | 第二次磁盘IO:加载磁盘块3到内存,依然是遍历比较,18<25,所以走左边P1,定位到磁盘块8。 69 | 70 | 第三次磁盘IO:加载磁盘块8到内存,在内存中遍历,18=18,找到18,取出data。 71 | 72 | 如图所示: 73 | 74 | ![](https://static.lovebilibili.com/mysql_suoyin_2.png) 75 | 76 | 如果data存储的是行数据,直接返回,如果存的是磁盘地址则根据磁盘地址到磁盘中取出数据。可以看出B树的查询效率是很高的。 77 | 78 | > B树存在着什么问题,需要改进优化呢? 79 | 80 | 第一个问题:B树在范围查询时,性能并不理想。假如要查询13到30之间的数据,查询到13后又要回到根节点再去查询后面的数据,就会产生多次的查询遍历。 81 | 82 | 第二个问题:因为非叶子节点和叶子节点都会存储数据,所以占用的空间大,一个页可存储的数据量就会变少,树的高度就会变高,磁盘的IO次数就会变多。 83 | 84 | 基于以上两个问题,就出现了B树的升级版,B+树。 85 | 86 | B+树与B树最大的区别在于两点: 87 | 88 | - B+树只有叶子节点存储数据,非叶子节点只存储键值。而B树的非叶子节点和叶子节点都会存储数据。 89 | - B+树的最底层的叶子节点会形成一个双向有序链表,而B树不会。 90 | 91 | 如图所示: 92 | 93 | ![](https://static.lovebilibili.com/mysql_suoyin_3.png) 94 | 95 | > B+树的等值查询过程是怎么样的? 96 | 97 | 如果在B+树中进行等值查询,比如查询等于13的数据。 98 | 99 | 查询路径为:磁盘块1->磁盘块2->磁盘块6。 100 | 101 | 第一次IO:加载磁盘块1,在内存中遍历比较,13<17,走左边,找到磁盘块2。 102 | 103 | 第二次IO:加载磁盘块2,在内存中遍历比较,10<13<15,走中间,找到磁盘块6。 104 | 105 | 第三次IO:加载磁盘块6,依次遍历,找到13=13,取出data。 106 | 107 | 所以B+树在等值查询的效率是很高的。 108 | 109 | > B+树的范围查询过程又是怎么样呢? 110 | 111 | 比如我们要进行范围查询,查询大于5并且小于15的数据。 112 | 113 | 查询路径为:磁盘块1->磁盘块2->磁盘块5->磁盘块6。 114 | 115 | 第一次IO:加载磁盘块1,比较得出5<17,然后走左边,找到磁盘块2。 116 | 117 | 第二次IO:加载磁盘块2,比较5<10,然后还是走左边,找到磁盘块5。 118 | 119 | 第三次IO:加载磁盘块5,然后找大于5的数据。 120 | 121 | 第四次IO:由于最底层是有序的双向链表,所以继续往右遍历即可,直到不符合小于15的数据为止。 122 | 123 | 过程如图所示: 124 | 125 | ![](https://static.lovebilibili.com/mysql_suoyin_4.png) 126 | 127 | 所以在范围查询的时候,是不需要像B树一样,再回到根节点,这就是底层采用双向链表的好处。 128 | 129 | 所以B+树的优势在于,能**保证等值查询和范围查询的快速查找**。 130 | 131 | # InnoDB索引 132 | 133 | 我们常用的MySQL存储引擎一般是InnoDB,所以接下来讲讲几种不同的索引的底层数据结构,以及查找过程。 134 | 135 | ## 聚簇索引 136 | 137 | 前面讲过,每个InnoDB表有且仅有一个聚簇索引。除此之外,聚簇索引在表的创建有以下几点规则: 138 | 139 | - 在表中,如果定义了主键,InnoDB会将主键索引作为聚簇索引。 140 | - 如果没有定义主键,则会选择第一个不为NULL的唯一索引列作为聚簇索引。 141 | - 如果以上两个都没有。InnoDB 会使用一个6 字节长整型的隐式字段 ROWID字段构建聚簇索引。该ROWID字段会在插入新行时自动递增。 142 | 143 | 除了聚簇索引之外的索引都称为非聚簇索引,区别在于,聚簇索引的叶子节点存储的数据是整行数据,而非聚簇索引存储的是该行的主键值。 144 | 145 | 比如有一张user表,如图所示: 146 | 147 | ![](https://static.lovebilibili.com/mysql_suoyin_5.png) 148 | 149 | 底层的数据结构就像这样: 150 | 151 | ![](https://static.lovebilibili.com/mysql_suoyin_6.png) 152 | 153 | 当我们用主键值去查询的时候,查询效率是很快的,因为可以直接返回数据。 154 | 155 | ![](https://static.lovebilibili.com/mysql_suoyin_7.png) 156 | 157 | ## 普通索引 158 | 159 | 也就是用得最多的一种索引,比如我要为user表的age列创建索引,SQL语句可以这样写: 160 | 161 | ```sql 162 | CREATE INDEX INDEX_USER_AGE ON `user`(age); 163 | ``` 164 | 165 | 普通索引属于非聚簇索引,所以叶子节点存储的是主键值,底层的数据结构大概长这个样子: 166 | 167 | ![](https://static.lovebilibili.com/mysql_suoyin_8.png) 168 | 169 | 比如要查询age=33的数据,那么首先查到磁盘块7的age=33的数据,获取到主键值,主键值为4。 170 | 171 | 接着再通过主键值等于4,查询到该行的数据。所以总得来说,底层会进行两次查询。 172 | 173 | 这种先通过查询主键值,再通过主键值查询到数据的过程就叫做回表查询。 174 | 175 | ## 覆盖索引 176 | 177 | 既然上面提到了回表查询,那么自然而然会想到,有没有什么办法能避免回表查询呢?答案肯定是有的,那就是使用覆盖索引。 178 | 179 | 覆盖索引不是一种索引的类型,而是一种使用索引的方式。假设你需要查询的列是建立了索引,查询的结果在索引列上就能获取,那就可以用覆盖索引。 180 | 181 | 比如上面的例子,我们通过age=33查询,我需要查询的结果就只要age这一列,那就可以用到覆盖索引,如图所示: 182 | 183 | ![](https://static.lovebilibili.com/mysql_suoyin_9.png) 184 | 185 | 使用到覆盖索引的话,就能避免回表查询,所以在写SQL语句时尽量不要写`SELECT *`。 186 | 187 | # 总结 188 | 189 | 这篇文章主要讲的是索引的类型,索引的数据结构,以及InnoDB表中常用的几种索引。当然,除了上述讲的这些之外,还有很多关于索引的知识,比如索引失效的场景,索引创建的原则等等,由于篇幅过长,留着以后再讲。 190 | 191 | 那么这篇文章就写到这里了,感谢大家的阅读。 192 | 193 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 194 | 195 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 196 | 197 | ![](https://static.lovebilibili.com/dashacha.png) 198 | 199 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /中间件/pulsar中间件入门.md: -------------------------------------------------------------------------------- 1 | > 文章已收录到我的[Github](https://github.com/yehongzhi/learningSummary)精选,欢迎Star! 2 | 3 | # 简单介绍 4 | 5 | Pulsar 是一个用于服务器到服务器的消息系统,具有多租户、高性能等优势。最初是由 Yahoo 开发,目前由 Apache 软件基金会 管理。是 Apache 软件基金会顶级项目,是下一代云原生分布式消息流平台,集消息、存储、轻量化函数式计算为一体,采用计算与存储分离架构设计,支持多租户、持久化存储、多机房跨区域数据复制,具有强一致性、高吞吐、低延时及高可扩展性等流数据存储特性,被看作是云原生时代实时消息流传输、存储和计算最佳解决方案。 6 | 7 | # 特性 8 | 9 | - Pulsar 的单个实例原生支持多个集群,可跨机房在集群间无缝地完成消息复制。 10 | 11 | - 极低的发布和端到端延迟。 12 | 13 | - 可无缝扩展到超过一百万个 topic。 14 | 15 | - 简单易用的客户端API,支持Java、Go、Python和C++。 16 | 17 | - 支持多种 topic 订阅模式(独占订阅、共享订阅、故障转移订阅)。 18 | 19 | - 通过 Apache BookKeeper 提供的持久化消息存储机制保证消息传递。 20 | 21 | - 基于 Pulsar Functions 的 serverless connector 框架 Pulsar IO 使得数据更易移入、移出 Apache Pulsar。 22 | 23 | - 由轻量级的 serverless 计算框架 Pulsar Functions 实现流原生的数据处理。 24 | 25 | - 分层式存储可在数据陈旧时,将数据从热存储卸载到冷/长期存储(如S3、GCS)中。 26 | 27 | # 架构 28 | 29 | ![](https://static.lovebilibili.com/pulsar_01.png) 30 | 31 | 这是在官网的架构图,涉及到几个组件,这里简单说明一下: 32 | 33 | Broker:Broker负责消息的传输,Topic的管理以及负载均衡,Broker**不负责消息的存储**,是个**无状态**组件。 34 | 35 | Bookie:负责**消息的的持久化**,采用Apache BookKeeper组件,BookKeeper是一个分布式的WAL系统。 36 | 37 | Producer:生产者,封装消息并将消息以同步或者异步的方式发送到Broker。 38 | 39 | Consumer:消费者,以订阅Topic的方式消费消息,并确认。Pulsar中还定义了Reader角色,也是一种消费者,区别在于,它可以从指定置位获取消息,且不需要确认。 40 | 41 | Zookeeper:元数据存储,负责集群的配置管理,包括租户,命名空间等,并进行一致性协调。 42 | 43 | # 四种订阅模式 44 | 45 | 在介绍Pulsar特性时,讲过支持多种订阅模式,总共有四种,分别是独占(exclusive)订阅、共享(shared)订阅、故障转移(failover)订阅、键(key_shared)共享。 46 | 47 | ![](https://static.lovebilibili.com/pulsar_02.png) 48 | 49 | ## 独占(Exclusive) 50 | 51 | 独占模式:同时只有一个消费者可以启动并消费数据;通过 `SubscriptionName` 标明是同一个消费者),适用范围较小。 52 | 53 | ![](https://static.lovebilibili.com/pulsar_03.png) 54 | 55 | ## 共享(Shared) 56 | 57 | 可以有 N 个消费者同时运行,消息按照 `round-robin` 轮询投递到每个 `consumer` 中;当某个 `consumer` 宕机没有 `ack` 时,该消息将会被投递给其他消费者。这种消费模式可以提高消费能力,但消息无法做到有序。 58 | 59 | ![](https://static.lovebilibili.com/pulsar_05.png) 60 | 61 | ## 故障转移(Failover) 62 | 63 | 故障转移模式:在独占模式基础之上可以同时启动多个 `consumer`,一旦一个 `consumer` 挂掉之后其余的可以快速顶上,但也只有一个 `consumer` 可以消费;部分场景可用。 64 | 65 | ![](https://static.lovebilibili.com/pulsar_04.png) 66 | 67 | ## 键共享(KeyShared) 68 | 69 | 基于共享模式;相当于对同一个`topic`中的消息进行分组,同一分组内的消息只能被同一个消费者有序消费。 70 | 71 | ![](https://static.lovebilibili.com/pulsar_06.png) 72 | 73 | # 下载安装 74 | 75 | 我这里安装的是2.9.1版本的pulsar,链接地址如下: 76 | 77 | > https://www.apache.org/dyn/mirrors/mirrors.cgi?action=download&filename=pulsar/pulsar-2.9.1/apache-pulsar-2.9.1-bin.tar.gz 78 | 79 | 下载完成之后,上传到Linux服务器,然后使用命令解压: 80 | 81 | > tar -zxvf apache-pulsar-2.9.0-bin.tar.gz 82 | 83 | 单机版的话,使用命令后台启动: 84 | 85 | > ./bin/pulsar-daemon start standalone 86 | 87 | 终止后台运行的命令: 88 | 89 | > ./bin/pulsar-daemon stop standalone 90 | 91 | # SpringBoot整合 92 | 93 | 在Linux服务器上启动完成之后,就到了使用Java客户端进行操作的步骤,首先引入Maven依赖: 94 | 95 | ```xml 96 | 97 | io.github.majusko 98 | pulsar-java-spring-boot-starter 99 | 1.1.0 100 | 101 | ``` 102 | 103 | application.yml配置文件加上配置: 104 | 105 | ```yml 106 | #pulsar的服务地址 107 | pulsar: 108 | service-url: pulsar://192.168.0.105:6650 109 | ``` 110 | 111 | 增加个配置类`PulsarConfig`: 112 | 113 | ```java 114 | @Configuration 115 | public class PulsarConfig { 116 | 117 | @Bean 118 | public ProducerFactory producerFactory() { 119 | return new ProducerFactory().addProducer("testTopic", String.class); 120 | } 121 | } 122 | ``` 123 | 124 | 增加个记录Topic名称的常量类: 125 | 126 | ```java 127 | /** 128 | * Pulsar中间件的topic名称 129 | * 130 | * @author yehongzhi 131 | * @date 2022/4/9 5:57 PM 132 | */ 133 | public class TopicName { 134 | 135 | private TopicName(){} 136 | /** 137 | * 测试用的topic 138 | */ 139 | public static final String TEST_TOPIC = "testTopic"; 140 | } 141 | ``` 142 | 143 | 增加消息生产者`PulsarProducer`类: 144 | 145 | ```java 146 | /** 147 | * Pulsar生产者 148 | * 149 | * @author yehongzhi 150 | * @date 2022/4/9 5:23 PM 151 | */ 152 | @Component 153 | public class PulsarProducer { 154 | 155 | @Resource 156 | private PulsarTemplate template; 157 | 158 | /** 159 | * 发送消息 160 | */ 161 | public void send(String topic, T message) { 162 | try { 163 | template.send(topic, message); 164 | } catch (PulsarClientException e) { 165 | e.printStackTrace(); 166 | } 167 | } 168 | } 169 | ``` 170 | 171 | 增加Topic名称为"testTopic"的消费者: 172 | 173 | ```java 174 | /** 175 | * topic名称为"testTopic"对应的消费者 176 | * 177 | * @author yehongzhi 178 | * @date 2022/4/9 6:00 PM 179 | */ 180 | @Component 181 | public class TestTopicPulsarConsumer { 182 | 183 | private static final Logger log = LoggerFactory.getLogger(TestTopicPulsarConsumer.class); 184 | 185 | //SubscriptionType.Shared,表示共享模式 186 | @PulsarConsumer(topic = TopicName.TEST_TOPIC, 187 | subscriptionType = SubscriptionType.Shared, 188 | clazz = String.class) 189 | public void consume(String message) { 190 | log.info("PulsarRealConsumer content:{}", message); 191 | } 192 | 193 | } 194 | ``` 195 | 196 | 最后增加一个`PulsarController`测试发送消息: 197 | 198 | ```java 199 | @RestController 200 | @RequestMapping("/pulsar") 201 | public class PulsarController { 202 | 203 | @Resource 204 | private PulsarProducer pulsarProducer; 205 | 206 | @PostMapping(value = "/sendMessage") 207 | public CommonResponse sendMessage(@RequestParam(name = "message") String message) { 208 | pulsarProducer.send(TopicName.TEST_TOPIC, message); 209 | return CommonResponse.success("done"); 210 | } 211 | } 212 | ``` 213 | 214 | `CommonResponse`公共响应体: 215 | 216 | ```java 217 | public class CommonResponse { 218 | 219 | private String code; 220 | 221 | private Boolean success; 222 | 223 | private T data; 224 | 225 | public static CommonResponse success(T t){ 226 | return new CommonResponse<>("200",true,t); 227 | } 228 | 229 | public CommonResponse(String code, Boolean success, T data) { 230 | this.code = code; 231 | this.success = success; 232 | this.data = data; 233 | } 234 | //getter、setter方法 235 | } 236 | ``` 237 | 238 | 启动项目,然后使用postman测试: 239 | ![](https://static.lovebilibili.com/pulsar_07.png) 240 | 241 | ![](https://static.lovebilibili.com/pulsar_08.png) 242 | 243 | # 总结 244 | 245 | 以上就是pulsar中间件的简单入门,分别介绍了pulsar的特性,架构,订阅模式,还有个整合SpringBoot的小例子。最后跟其他主流的中间件做个对比,供大家参考一下: 246 | 247 | ![](https://static.lovebilibili.com/pulsar_09.jpeg) 248 | 249 | 感谢大家的阅读,希望这篇文章对你有所帮助。 250 | 251 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 252 | 253 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 254 | 255 | ![](https://static.lovebilibili.com/dashacha.png) 256 | 257 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! 258 | -------------------------------------------------------------------------------- /分布式/SpringBoot多环境配置.md: -------------------------------------------------------------------------------- 1 | > **文章已收录到我的Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 2 | 3 | # 前言 4 | 5 | 一般来说,在日常开发中都会分多个环境,比如git代码分支会分为dev(开发)、release(测试)、pord(生产)等多个环境。可以说每个环境对应的配置信息(比如数据库、缓存、消息队列MQ等)都不相同。因此不同的环境肯定需要对应不同的配置文件。接下来学习一下怎么配置多环境的配置文件。 6 | 7 | # SpringBoot多环境配置 8 | 9 | 因为SpringBoot做多环境配置比较简单,而且现在大部分项目基本都会使用SpringBoot,所以这里就介绍怎么用SpringBoot做多环境配置。 10 | 11 | ## 单文件版本 12 | 13 | 单文件在实际中使用得并不多,不过也可以实现多环境配置,这里简单介绍一下。以`application.yml`配置文件举例,你要在一个配置文件里面配置多个环境的配置,肯定需要分割线将其隔开,所以SpringBoot就规定了使用`---`进行隔开每个环境。 14 | 15 | ```yaml 16 | spring: 17 | application: 18 | name: mydemo 19 | profiles: 20 | active: prod # 选择prod环境配置 21 | #整合mybatis 22 | mybatis-plus: 23 | mapper-locations: classpath:mapper/*Mapper.xml 24 | type-aliases-package: com.yehongzhi.mydemo.model 25 | configuration: 26 | log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 27 | --- 28 | # 开发环境 29 | server: 30 | port: 8080 31 | spring: 32 | profiles: dev 33 | datasource: 34 | driver-class-name: com.mysql.jdbc.Driver 35 | url: jdbc:mysql://DEV_IP:3306/user?createDatabaseIfNotExist=true 36 | username: root 37 | password: 123456 38 | --- 39 | # 测试环境 40 | server: 41 | port: 8090 42 | spring: 43 | profiles: release 44 | datasource: 45 | driver-class-name: com.mysql.jdbc.Driver 46 | url: jdbc:mysql://RELEASE_IP:3306/user?createDatabaseIfNotExist=true 47 | username: root 48 | password: 123456 49 | --- 50 | # 生产环境 51 | server: 52 | port: 8888 53 | spring: 54 | profiles: prod 55 | datasource: 56 | driver-class-name: com.mysql.jdbc.Driver 57 | url: jdbc:mysql://PROD_IP:3306/user?createDatabaseIfNotExist=true 58 | username: root 59 | password: 123456 60 | ``` 61 | 62 | 单文件配置多环境的缺点很明显,就是会导致这个`application.yml`文件非常大,不够清晰。最好是一个环境单独一个文件,这样就清晰很多。于是乎就有了多文件版本。 63 | 64 | ## 多文件版本 65 | 66 | 一般SpringBoot的配置文件都是叫`application.yml`或者`application.properties`,这里用`application.yml`举例,配置多环境配置文件,文件名需要满足这样的格式:`application-{profile}.yml`。看下图就明白了。 67 | 68 | ![](https://static.lovebilibili.com/springBoot_application_1.png) 69 | 70 | 换而言之,dev环境的配置文件就叫做`application-dev.yml`,那么怎么选择哪个环境的配置文件呢,其实很简单,只需要在`application.yml`加上如下配置: 71 | 72 | ```yaml 73 | spring: 74 | profiles: 75 | active: dev 76 | ``` 77 | 78 | 这就表示选择加载`application-dev.yml`文件,何以见得? 79 | 80 | 一般在启动完成之后,我们可以在控制台搜索关键字`profiles`找到对应的环境。 81 | 82 | ![](https://static.lovebilibili.com/springBoot_application_2.png) 83 | 84 | 所以我们就可以在application.yml里面,通过`spring.profiles.active`切换不同的环境。这就是多文件版本。 85 | 86 | 但是我们在平时开发时发现,这个配置要经常改来改去,非常麻烦,有没有不用改这个配置就可以切换的方法呢?当然有。 87 | 88 | 首先在`pom.xml`文件增加以下环境变量的配置。 89 | 90 | ```xml 91 | 92 | 93 | dev 94 | 95 | dev 96 | 97 | 98 | 99 | release 100 | 101 | release 102 | 103 | 104 | 105 | prod 106 | 107 | prod 108 | 109 | 110 | 111 | ``` 112 | 113 | 接着在`application.yml`配置文件中使用`@profiles.active@`来配置环境变量。 114 | 115 | ```yaml 116 | spring: 117 | profiles: 118 | active: '@profiles.active@' 119 | ``` 120 | 121 | 接着刷新Maven,可以在IDEA右侧中选择对应的环境,如下图: 122 | 123 | ![](https://static.lovebilibili.com/springBoot_application_3.png) 124 | 125 | 当需要切换环境时,就不需要改配置文件的内容,只需要勾选对应的环境即可,就方便很多。 126 | 127 | ## 结合Nacos配置中心 128 | 129 | 一般在项目开发中,都需要配置信息能够在运行时更改配置,于是乎就有了配置中心的概念。配置中心当然也有多环境的配置。 130 | 131 | 在Nacos配置中心就有命名空间的概念,我们可以使用命名空间来实现多环境配置。首先引入Maven依赖: 132 | 133 | ```xml 134 | 135 | 136 | com.alibaba.cloud 137 | spring-cloud-starter-alibaba-nacos-config 138 | 2.0.2.RELEASE 139 | 140 | 141 | 142 | 143 | dev 144 | 145 | dev 146 | 147 | 148 | 149 | release 150 | 151 | release 152 | 153 | 154 | 155 | prod 156 | 157 | prod 158 | 159 | 160 | 161 | ``` 162 | 163 | 第二步,启动Nacos,然后在创建对应的命名空间和配置文件。 164 | 165 | ![](https://static.lovebilibili.com/springBoot_application_4.png) 166 | 167 | ![](https://static.lovebilibili.com/springBoot_application_5.png) 168 | 169 | 第三步,在项目中增加`bootstrap.yml`文件。 170 | 171 | ```yaml 172 | spring: 173 | application: 174 | name: mydemo 175 | profiles: 176 | active: '@profiles.active@' 177 | cloud: 178 | nacos: 179 | config: 180 | server-addr: 127.0.0.1:8848 181 | file-extension: yaml 182 | group: DEFAULT_GROUP 183 | namespace: a4a33d52-371b-451a-a3c1-d01c1d343331 #dev命名空间的ID 184 | enabled: true 185 | prefix: ${spring.application.name} 186 | refresh-enabled: true 187 | ``` 188 | 189 | 在IDEA配置项目启动时设置环境变量。 190 | 191 | ![](https://static.lovebilibili.com/springBoot_application_6.png) 192 | 193 | 这样就完成了,启动项目,就可以读到Nacos配置中心的dev命名空间的`mydemo-dev.yaml`文件。 194 | 195 | 因为DataId的定义规则是`${prefix}-${spring.profiles.active}.${file-extension}`。 196 | 197 | > prefix默认规则是获取${spring.application.name}的值。可以通过spring.cloud.nacos.config.prefix进行配置。 198 | > 199 | > spring.profiles.active即为当前环境对应的profile。可以通过spring.profiles.active进行配置。 200 | > 201 | > file-extension为配置文件的数据格式。可以通过spring.cloud.nacos.config.file-extension进行配置。 202 | 203 | # 总结 204 | 205 | 以上就是多环境配置的三种方式,多环境配置基本上是创建新项目的基本操作,所以掌握多环境配置还是很有必要的。感谢大家的阅读,希望看完之后能对你有所收获。 206 | 207 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 208 | 209 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 210 | 211 | ![](https://static.lovebilibili.com/dashacha.png) 212 | 213 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /分布式/skywalking调用链追踪.md: -------------------------------------------------------------------------------- 1 | # 思维导图 2 | 3 | ![](https://static.lovebilibili.com/skywalking_swdt.png) 4 | 5 | > **文章已收录Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 6 | 7 | # 概述 8 | 9 | **[skywalking](https://github.com/apache/skywalking)**又是一个优秀的国产开源框架,2015年由个人吴晟(华为开发者)开源 , 2017年加入Apache孵化器。 10 | 11 | skywalking是分布式系统的**应用程序性能监视工具**,专为微服务、云原生架构和基于容器(Docker、K8s、Mesos)架构而设计。SkyWalking 是**观察性分析平台和应用性能管理系统**。提供**分布式追踪、服务网格遥测分析、度量聚合和可视化一体化**解决方案(官网介绍)。 12 | 13 | # 一、OpenTracing规范 14 | 15 | **OpenTracing是一种分布式系统链路跟踪的设计原则、规范、标准。** 16 | 17 | 类似JDBC的规范,主要为了提供一套标准的JDBC API。OpenTracing也是一样,是为了统一提供一套链路追踪的标准API,所制定的一种规范。 18 | 19 | OpenTracing通过提供平台无关、厂商无关的API,使得开发人员能够方便的添加(或更换)追踪系统的实现。 20 | 21 | 类似于JDBC的规范由各个数据库厂商实现一样,OpenTracing规范也是有很多实现的产品,下面介绍一下落地的产品。 22 | 23 | ## 1.1 实现OpenTracing的产品 24 | 25 | **Jaeger**:Jaeger是由Uber公司开源发布的,受到Dapper和OpenZipkin启发。后端使用Go语言,前端(用户界面)使用React 。优点是上传采用的是udp传输,效率高速度快。缺点就是丢包,影响了整条调用链,而且不支持告警和JVM监控。 26 | 27 | **Zipkin**:SpringCloud官方推荐,可以**与SpringCloud有良好集成**,实现方式是拦截请求,发送(http)数据到zipkin服务。缺点在于**不支持告警,不支持JVM监控,通信方式使用Http请求向Zipkin上报信息,比较耗性能**。 28 | 29 | **SkyWalking**:国人(吴晟)开发,支持dubbo,SpringCloud,SpringBoot集成,**代码无侵入,通信方式采用GRPC,性能较好,实现方式是java探针,支持告警,支持JVM监控,支持全局调用统计**等等,功能较完善。缺点是**依赖较多**,需要ElasticSearch,JDK环境,Nacos注册中心等。 30 | 31 | ## 1.2 skywalking的特点 32 | 33 | ![](https://static.lovebilibili.com/skywalking_1.png) 34 | 35 | 比较重要的特点,我觉得是轻量高效,对代码无侵入性。对于微服务,支持dubbo,SpringBoot,SpringCloud集成。 36 | 37 | # 二、安装部署 38 | 39 | 环境:CentOS 7.5,MySQL 5.7.26,Nacos 1.3.1(注册中心),JDK 1.8,**skywalking 8.1.0**。 40 | 41 | 除了skywalking之外,其他需要用到的组件我就不介绍怎么安装了,比较简单。安装skywalking其实很简单,下面一步一步来讲解。 42 | 43 | 第一步,下载。在[官网](http://skywalking.apache.org/downloads/)下载即可,选择8.1.0版本,如果要使用ES作为存储仓库,那就要选择es7的版本。 44 | 45 | ![](https://static.lovebilibili.com/skywalking_2.png) 46 | 47 | 48 | 49 | 第二步,解压。找到config目录下的application.yml文件,然后修改配置。 50 | 51 | ![](https://static.lovebilibili.com/skywalking_3.png) 52 | 53 | 需要修改的配置内容如下: 54 | 55 | ```yaml 56 | cluster: 57 | selector: ${SW_CLUSTER:nacos} 58 | #单机模式 59 | standalone: 60 | #使用nacos作为注册中心 61 | nacos: 62 | # 注册到nacos的服务名 63 | serviceName: ${SW_SERVICE_NAME:"SkyWalking_OAP_Cluster"} 64 | #nacos服务端的地址 65 | hostPort: ${SW_CLUSTER_NACOS_HOST_PORT:192.168.0.105:8848} 66 | # Nacos Configuration namespace命名空间 67 | namespace: ${SW_CLUSTER_NACOS_NAMESPACE:"public"} 68 | core: 69 | selector: ${SW_CORE:default} 70 | default: 71 | #skywalking服务端的REST绑定的IP 72 | restHost: ${SW_CORE_REST_HOST:192.168.0.107} 73 | #skywalking服务端的REST调用的端口 74 | restPort: ${SW_CORE_REST_PORT:12800} 75 | #skywalking服务端GRPC通信绑定的IP 76 | gRPCHost: ${SW_CORE_GRPC_HOST:192.168.0.107} 77 | #skywalking服务端GRPC通信绑定的端口 78 | gRPCPort: ${SW_CORE_GRPC_PORT:11800} 79 | storage: 80 | #选择使用mysql 81 | selector: ${SW_STORAGE:mysql} 82 | #默认使用h2,不会持久化,重启skyWalking之前的数据会丢失 83 | h2: 84 | driver: ${SW_STORAGE_H2_DRIVER:org.h2.jdbcx.JdbcDataSource} 85 | url: ${SW_STORAGE_H2_URL:jdbc:h2:mem:skywalking-oap-db} 86 | user: ${SW_STORAGE_H2_USER:sa} 87 | metadataQueryMaxSize: ${SW_STORAGE_H2_QUERY_MAX_SIZE:5000} 88 | #使用mysql作为持久化存储的仓库 89 | mysql: 90 | properties: 91 | #数据库连接地址 92 | jdbcUrl: ${SW_JDBC_URL:"jdbc:mysql://192.168.0.107:3306/swtest"} 93 | #用户名 94 | dataSource.user: ${SW_DATA_SOURCE_USER:yehongzhi} 95 | #密码 96 | dataSource.password: ${SW_DATA_SOURCE_PASSWORD:Yehongzhi520.} 97 | ``` 98 | 99 | 默认是web管理界面是8080端口,如果要修改端口号,可以修改webapp目录下的webapp.yml。 100 | 101 | ```yaml 102 | #web管理界面的端口 103 | server: 104 | port: 8080 105 | ``` 106 | 107 | 第三步,添加mysql数据驱动包。因为在lib目录下是没有mysql数据驱动包的,所以修改完配置启动是会报错,启动失败的。为什么作者不提前在lib目录下放一个数据驱动包呢,还要我们手动去添加。网上貌似没有这个问题的讨论,我的理解是**因为框架不知道你用的是什么版本的mysql数据库,所以不知道放什么版本的数据库驱动包,使用者用的是什么版本的mysql,就自己放对应的数据库驱动包**。 108 | 109 | 我这里用的是5.7.26版本的mysql,所以我下载了一个8.0.17的驱动包,添加到/oap-libs目录下。 110 | 111 | ![](https://static.lovebilibili.com/skywalking_4.png) 112 | 113 | 第三步,启动。在/bin目录上一级,直接使用`./bin/startup.sh`启动即可。启动之后,可以使用`jps`命令查看进程,可以看到这两个java程序在运行状态。 114 | 115 | ![](https://static.lovebilibili.com/skywalking_5.png) 116 | 117 | 打开配置的Nacos控制台,可以看到服务列表注册了名为“SkyWalking_OAP_Cluster”的服务。 118 | 119 | ![](https://static.lovebilibili.com/skywalking_6.png) 120 | 121 | 可以看到mysql建了很多表。 122 | 123 | ![](https://static.lovebilibili.com/skywalking_7.png) 124 | 125 | 说明启动成功了,打开配置对应的地址http://192.168.0.109:8080/,可以看到skywalking的web界面。 126 | 127 | ![](https://static.lovebilibili.com/skywalking_8.png) 128 | 129 | # 三、整合SpringCloud工程 130 | 131 | 整合其实很简单,不需要引入依赖,也不需要添加任何代码,我们只需要在启动jar包时配置参数即可。 132 | 133 | ``` 134 | -javaagent:D:\apache-skywalking-apm-bin-es7\agent\skywalking-agent.jar 135 | -Dskywalking.agent.service_name=consumer 136 | -Dskywalking.collector.backend_service=192.168.0.109:11800 137 | #解释一下上面这三个参数的意思 138 | #-javaagent:填的是skywalking-agent.jar的本地磁盘的路径 139 | #-Dskywalking.agent.service_name:在skywalking上显示的服务名 140 | #-Dskywalking.collector.backend_service:skywalking的collector服务的IP及端口 141 | ``` 142 | 143 | 我们一般用IDEA开发就这样设置即可。 144 | 145 | ![](https://static.lovebilibili.com/skywalking_9.png) 146 | 147 | 接下来我按照这个配置,启动一个Consumer工程和Provider工程,并且注册到Nacos注册中心。 148 | 149 | ![](https://static.lovebilibili.com/skywalking_10.png) 150 | 151 | 然后使用Consumer工程的接口调用Provider工程的接口,可以看到调用链的效果。 152 | 153 | ![](https://static.lovebilibili.com/skywalking_11.png) 154 | 155 | 非常清晰地看到服务之间调用的情况,耗时等等。其他还有很多功能就不一一介绍了,读者可以自己探索一下。 156 | 157 | # 四、谈谈架构设计 158 | 159 | 可能前面还有一些疑问,比如为什么要设置GRPC的端口号,设置存储仓库为mysql,启动之后有两个java进程等等。不妨看看架构,一切问题都明白了。首先看官网的一张架构图。 160 | 161 | ![](https://static.lovebilibili.com/skywalking_15.png) 162 | 163 | 可以看到主要有四个部分。 164 | 165 | **上面的Agent** :负责从应用中,收集tracing(调用链数据)和metric(指标),发送给 SkyWalking OAP 服务器。目前支持 SkyWalking、Zikpin、Jaeger 等提供的 Tracing 数据信息。而我们目前采用的是,SkyWalking Agent 收集 SkyWalking Tracing 数据,传递给SkyWalking OAP 服务器。 166 | 167 | **中间的SkyWalking OAP**:负责接收 Agent 发送的 Tracing 和Metric的数据信息,然后进行分析(Analysis Core) ,存储到外部存储器( Storage ),最终提供查询( Query )功能。 168 | 169 | **左边的SkyWalking UI**:负责提供web控制台,查看链路,查看各种指标,性能等等。 170 | 171 | **右边的Storage**:数据存储。目前支持ES、MySQL、H2等多种存储器。 172 | 173 | # 总结 174 | 175 | 这篇文章就介绍到这里,这里仅仅只是入门,简单使用Skywalking,实际上里面还有很多功能我没有介绍,有兴趣的同学可以按照上面的教程安装部署,然后自己探索一下。 176 | 177 | 在现在微服务架构比较流行的环境下,如果没有一个调用链追踪框架,会导致很难排查线上服务调用的问题。skywalking是目前发展势头最快的技术框架的技术框架,因为对代码是无侵入性的,所以目前很多公司都采用Skywalking。 178 | 179 | ![](https://static.lovebilibili.com/skywalking_14.png) 180 | 181 | 这篇文章就讲到这里了,感谢大家的阅读。 182 | 183 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 184 | 185 | **拒绝做一条咸鱼,我是一个努力让大家记住的程序员。我们下期再见!!!** 186 | 187 | ![](https://static.lovebilibili.com/dashacha.png) 188 | 189 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /分布式/日志集中分析平台ELK.md: -------------------------------------------------------------------------------- 1 | # 思维导图 2 | 3 | ![](https://static.lovebilibili.com/elk_swdt.png) 4 | 5 | > **文章已收录Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 6 | 7 | # 概述 8 | 9 | 我们都知道,在生产环境中经常会遇到很多异常,报错信息,需要查看日志信息排查错误。现在的系统大多比较复杂,即使是一个服务背后也是一个集群的机器在运行,**如果逐台机器去查看日志显然是很费力的,也不现实**。 10 | 11 | 如果能把日志全部收集到一个平台,然后像百度,谷歌一样**通过关键字搜索出相关的日志**,岂不快哉。于是就有了**集中式日志系统**。ELK就是其中一款使用最多的开源产品。 12 | 13 | # 一、什么是ELK 14 | 15 | ELK其实是Elasticsearch,Logstash 和 Kibana三个产品的首字母缩写,这三款都是开源产品。 16 | 17 | **ElasticSearch**(简称ES),是一个实时的分布式搜索和分析引擎,它可以用于全文搜索,结构化搜索以及分析。 18 | 19 | **Logstash**,是一个数据收集引擎,主要用于进行数据收集、解析,并将数据发送给ES。支持的数据源包括本地文件、ElasticSearch、MySQL、Kafka等等。 20 | 21 | **Kibana**,为 Elasticsearch 提供了分析和 Web 可视化界面,并生成各种维度表格、图形。 22 | 23 | ![](https://static.lovebilibili.com/elk_2.png) 24 | 25 | # 二、搭建ELK 26 | 27 | 环境依赖:CentOS7.5,JDK1.8,ElasticSearch7.9.3,Logstash 7.9.3,Kibana7.9.3。 28 | 29 | ## 2.1 安装ElasticSearch 30 | 31 | 首先,到[官网](https://www.elastic.co/cn/downloads/elasticsearch)下载安装包,然后使用`tar -zxvf`命令解压。 32 | 33 | ![](https://static.lovebilibili.com/elk_3.png) 34 | 35 | 找到config目录下的elasticsearch.yml文件,修改配置: 36 | 37 | ```yaml 38 | cluster.name: es-application 39 | node.name: node-1 40 | #对所有IP开放 41 | network.host: 0.0.0.0 42 | #HTTP端口号 43 | http.port: 9200 44 | #elasticsearch数据文件存放目录 45 | path.data: /usr/elasticsearch-7.9.3/data 46 | #elasticsearch日志文件存放目录 47 | path.logs: /usr/elasticsearch-7.9.3/logs 48 | ``` 49 | 50 | 配置完之后,因为ElasticSearch使用非root用户启动,所以创建一个用户。 51 | 52 | ```java 53 | # 创建用户 54 | useradd yehongzhi 55 | # 设置密码 56 | passwd yehongzhi 57 | # 赋予用户权限 58 | chown -R yehongzhi:yehongzhi /usr/elasticsearch-7.9.3/ 59 | ``` 60 | 61 | 然后切换用户,启动: 62 | 63 | ``` 64 | # 切换用户 65 | su yehongzhi 66 | # 启动 -d表示后台启动 67 | ./bin/elasticsearch -d 68 | ``` 69 | 70 | 使用命令`netstat -nltp`查看端口号: 71 | 72 | ![](https://static.lovebilibili.com/elk_4.png) 73 | 74 | 访问http://192.168.0.109:9200/可以看到如下信息,表示安装成功。 75 | 76 | ![](https://static.lovebilibili.com/elk_5.png) 77 | 78 | ## 2.2 安装Logstash 79 | 80 | 首先在官网下载安装压缩包,然后解压,找到/config目录下的logstash-sample.conf文件,修改配置: 81 | 82 | ```yaml 83 | input { 84 | file{ 85 | path => ['/usr/local/user/*.log'] 86 | type => 'user_log' 87 | start_position => "beginning" 88 | } 89 | } 90 | 91 | output { 92 | elasticsearch { 93 | hosts => ["http://192.168.0.109:9200"] 94 | index => "user-%{+YYYY.MM.dd}" 95 | } 96 | } 97 | ``` 98 | 99 | input表示输入源,output表示输出,还可以配置filter过滤,架构如下: 100 | 101 | ![](https://static.lovebilibili.com/elk_11.png) 102 | 103 | 配置完之后,要有数据源,也就是日志文件,准备一个user.jar应用程序,然后后台启动,并且输出到日志文件user.log中,命令如下: 104 | 105 | ```shell 106 | nohup java -jar user.jar >/usr/local/user/user.log & 107 | ``` 108 | 109 | 接着再后台启动Logstash,命令如下: 110 | 111 | ```shell 112 | nohup ./bin/logstash -f /usr/logstash-7.9.3/config/logstash-sample.conf & 113 | ``` 114 | 115 | 启动完之后,使用`jps`命令,可以看到两个进程在运行: 116 | 117 | ![](https://static.lovebilibili.com/elk_8.png) 118 | 119 | ## 2.3 安装Kibana 120 | 121 | 首先还是到[官网](https://www.elastic.co/cn/downloads/kibana)下载压缩包,然后解压,找到/config目录下的kibana.yml文件,修改配置: 122 | 123 | ```yaml 124 | server.port: 5601 125 | server.host: "192.168.0.111" 126 | elasticsearch.hosts: ["http://192.168.0.109:9200"] 127 | ``` 128 | 129 | 和elasticSearch一样,不能使用root用户启动,需要创建一个用户: 130 | 131 | ``` 132 | # 创建用户 133 | useradd kibana 134 | # 设置密码 135 | passwd kibana 136 | # 赋予用户权限 137 | chown -R kibana:kibana /usr/kibana/ 138 | ``` 139 | 140 | 然后使用命令启动: 141 | 142 | ``` 143 | #切换用户 144 | su kibana 145 | #非后台启动,关闭shell窗口即退出 146 | ./bin/kibana 147 | #后台启动 148 | nohup ./bin/kibana & 149 | ``` 150 | 151 | 启动后在浏览器打开http://192.168.0.111:5601,可以看到kibana的web交互界面: 152 | 153 | ![](https://static.lovebilibili.com/elk_6.png) 154 | 155 | ## 2.4 效果展示 156 | 157 | 全部启动成功后,整个过程应该是这样,我们看一下: 158 | 159 | ![](https://static.lovebilibili.com/elk_12.png) 160 | 161 | 浏览器打开http://192.168.0.111:5601,到管理界面,点击“Index Management”可以看到,有一个`user-2020.10.31`的索引。 162 | 163 | ![](https://static.lovebilibili.com/elk_10.png) 164 | 165 | 点击`Index Patterns`菜单栏,然后创建,命名为user-*。 166 | 167 | ![](https://static.lovebilibili.com/elk_9.png) 168 | 169 | 最后,就可以到Discover栏进行选择,选择user-*的Index Pattern,然后搜索关键字,就找到相关的日志了! 170 | 171 | ![](https://static.lovebilibili.com/elk_7.png) 172 | 173 | # 三、改进优化 174 | 175 | 上面只是用到了核心的三个组件简单搭建的ELK,实际上是有缺陷的。如果Logstash需要添加插件,那就全部服务器的Logstash都要添加插件,扩展性差。所以就有了**FileBeat**,占用资源少,只负责采集日志,不做其他的事情,这样就轻量级,把Logstash抽出来,做一些滤处理之类的工作。 176 | 177 | ![](https://static.lovebilibili.com/elk_13.png) 178 | 179 | FileBeat也是官方推荐用的日志采集器,首先下载Linux安装压缩包: 180 | 181 | ``` 182 | https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.9.3-linux-x86_64.tar.gz 183 | ``` 184 | 185 | 下载完成后,解压。然后修改filebeat.yml配置文件: 186 | 187 | ```yaml 188 | #输入源 189 | filebeat.inputs: 190 | - type: log 191 | enabled: true 192 | paths: 193 | - /usr/local/user/*.log 194 | #输出,Logstash的服务器地址 195 | output.logstash: 196 | hosts: ["192.168.0.110:5044"] 197 | #输出,如果直接输出到ElasticSearch则填写这个 198 | #output.elasticsearch: 199 | #hosts: ["localhost:9200"] 200 | #protocol: "https" 201 | ``` 202 | 203 | 然后Logstash的配置文件logstash-sample.conf,也要改一下: 204 | 205 | ``` 206 | #输入源改成beats 207 | input { 208 | beats { 209 | port => 5044 210 | codec => "json" 211 | } 212 | } 213 | ``` 214 | 215 | 然后启动FileBeat: 216 | 217 | ```shell 218 | #后台启动命令 219 | nohup ./filebeat -e -c filebeat.yml >/dev/null 2>&1 & 220 | ``` 221 | 222 | 再启动Logstash: 223 | 224 | ```shell 225 | #后台启动命令 226 | nohup ./bin/logstash -f /usr/logstash-7.9.3/config/logstash-sample.conf & 227 | ``` 228 | 229 | 怎么判断启动成功呢,看Logstash应用的/logs目录下的logstash-plain.log日志文件: 230 | 231 | ![](https://static.lovebilibili.com/elk_14.png) 232 | 233 | # 写在最后 234 | 235 | 目前,很多互联网公司都是采用ELK来做日志集中式系统,原因很简单:**开源、插件多、易扩展、支持数据源多、社区活跃、开箱即用**等等。我见过有一个公司在上面的架构中还会加多一个Kafka的集群,主要是基于日志数据量比较大的考虑。但是呢,基本的三大组件ElasticSearch,Logstash,Kibana是不能少的。 236 | 237 | 希望这篇文章能帮助大家对ELK有一些初步的认识,感谢大家的阅读。 238 | 239 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 240 | 241 | **拒绝做一条咸鱼,我是一个努力让大家记住的程序员。我们下期再见!!!** 242 | 243 | ![](https://static.lovebilibili.com/dashacha.png) 244 | 245 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /大数据/WordCount.md: -------------------------------------------------------------------------------- 1 | > **文章已收录到我的Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 2 | 3 | # MapReduce介绍 4 | 5 | MapReduce主要分为两个部分,分别是map和reduce,采用的是“分而治之”的思想,Mapper负责“分”,把一个庞大的任务分成若干个小任务来进行处理,而Reduce则是负责对map阶段的结果进行汇总。 6 | 7 | 比如我们要统计一个很大的文本,里面每个单词出现的频率,也就是WordCount。怎么工作呢?请看下图: 8 | 9 | ![](https://static.lovebilibili.com/wordcount_01.png) 10 | 11 | 在map阶段把input输入的文本拆成一个一个的单词,key是单词,value则是出现的次数。接着到Reduce阶段汇总,相同的key则次数加1。最后得到结果,输出到文件保存。 12 | 13 | # WordCount例子 14 | 15 | 下面进入实战,怎么实现WordCount的功能呢? 16 | 17 | ## 创建项目 18 | 19 | 首先我们得创建一个maven项目,依赖如下: 20 | 21 | ```xml 22 | 23 | 26 | 4.0.0 27 | io.github.yehongzhi 28 | hadooptest 29 | 1.0-SNAPSHOT 30 | jar 31 | 32 | 33 | 34 | apache 35 | http://maven.apache.org 36 | 37 | 38 | 39 | 40 | 41 | org.apache.hadoop 42 | hadoop-common 43 | 2.6.5 44 | 45 | 46 | org.apache.hadoop 47 | hadoop-hdfs 48 | 2.6.5 49 | 50 | 51 | org.apache.hadoop 52 | hadoop-mapreduce-client-core 53 | 2.6.5 54 | 55 | 56 | org.apache.hadoop 57 | hadoop-mapreduce-client-jobclient 58 | 2.6.5 59 | 60 | 61 | org.apache.hadoop 62 | hadoop-mapreduce-client-common 63 | 2.6.5 64 | 65 | 66 | org.apache.hadoop 67 | hadoop-client 68 | 2.6.5 69 | 70 | 71 | org.apache.hadoop 72 | hadoop-core 73 | 1.2.0 74 | 75 | 76 | 77 | ``` 78 | 79 | 第一步是Mapper阶段,创建类WordcountMapper: 80 | 81 | ```java 82 | /** 83 | * Mapper有四个泛型参数需要填写 84 | * 第一个参数KEYIN:默认情况下,是mr框架所读到的一行文本的起始偏移量,类型为LongWritable 85 | * 第二个参数VALUEIN:默认情况下,是mr框架所读的一行文本的内容,类型为Text 86 | * 第三个参数KEYOUT:是逻辑处理完成之后输出数据的key,在此处是每一个单词,类型为Text 87 | * 第四个参数VALUEOUT:是逻辑处理完成之后输出数据的value,在此处是次数,类型为Intwriterable 88 | * */ 89 | public class WordcountMapper extends Mapper { 90 | 91 | @Override 92 | protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { 93 | //将输入的文本转成String 94 | String string = value.toString(); 95 | //使用空格分割每个单词 96 | String[] words = string.split(" "); 97 | //输出为<单词,1> 98 | for (String word : words) { 99 | //将单词作为key,次数1作为value 100 | context.write(new Text(word), new IntWritable(1)); 101 | } 102 | } 103 | } 104 | ``` 105 | 106 | 接着到Reduce阶段,创建类WordcountReduce: 107 | 108 | ```java 109 | /** 110 | * KEYIN, VALUEIN, 对应mapper阶段的KEYOUT,VALUEOUT的类型 111 | * 112 | * KEYOUT, VALUEOUT,则是reduce逻辑处理结果的输出数据类型 113 | * 114 | * KEYOUT是单词,类型为Text 115 | * VALUEOUT是总次数,类型为IntWritable 116 | */ 117 | public class WordcountReduce extends Reducer { 118 | @Override 119 | protected void reduce(Text key, Iterable values, Context context) throws IOException, InterruptedException { 120 | int count = 0; 121 | //次数相加 122 | for (IntWritable value : values) { 123 | count += value.get(); 124 | } 125 | //输出<单词,总次数> 126 | context.write(key, new IntWritable(count)); 127 | } 128 | } 129 | ``` 130 | 131 | 最后再创建类WordCount,提供入口: 132 | 133 | ```java 134 | public class WordCount { 135 | public static void main(String[] args) throws Exception { 136 | Configuration configuration = new Configuration(); 137 | Job job = Job.getInstance(configuration); 138 | //指定本程序的jar包所在的本地路径 把jar包提交到yarn 139 | job.setJarByClass(WordCount.class); 140 | /* 141 | * 告诉框架调用哪个类 142 | * 指定本业务job要是用的mapper/Reducer业务类 143 | */ 144 | job.setMapperClass(WordcountMapper.class); 145 | job.setReducerClass(WordcountReduce.class); 146 | /* 147 | * 指定mapper输出数据KV类型 148 | */ 149 | job.setMapOutputKeyClass(Text.class); 150 | job.setMapOutputValueClass(IntWritable.class); 151 | 152 | //指定最终的输出数据的kv类型 153 | job.setOutputKeyClass(Text.class); 154 | job.setOutputValueClass(IntWritable.class); 155 | //指定job 的输入文件所在的目录 156 | FileInputFormat.setInputPaths(job, new Path(args[0])); 157 | // 指定job 的输出结果所在的目录 158 | FileOutputFormat.setOutputPath(job, new Path(args[1])); 159 | boolean completion = job.waitForCompletion(true); 160 | System.exit(completion ? 0 : 1); 161 | } 162 | } 163 | ``` 164 | 165 | 写到这里就完成了。接下来使用maven打包成jar包,上传到部署了hadoop的服务器。 166 | 167 | ![](https://static.lovebilibili.com/wordcount_02.png) 168 | 169 | ## 上传文件到hadoop 170 | 171 | 接着上传需要统计单词的文本文件上去hadoop,这里我随便拿一个redis的配置文件(字数够多,哈哈)上传上去。 172 | 173 | 先改个名字为`input.txt`然后用ftp工具上传到`/usr/local/hadoop-3.2.2/input`目录,接着在hadoop创建`/user/root`文件夹。 174 | 175 | ```shell 176 | hdfs dfs -mkdir /user 177 | hdfs dfs -mkdir /user/root 178 | hadoop fs -mkdir input 179 | //上传文件到hdfs 180 | hadoop fs -put /usr/local/hadoop-3.2.2/input/input.txt input 181 | //上传成功之后,可以使用下面的命令查看 182 | hadoop fs -ls /user/root/input 183 | ``` 184 | 185 | ## 执行程序 186 | 187 | 第一步先启动hadoop,到`sbin`目录下使用命令`./start-all.sh`,启动成功后,使用`jps`查看到以下进程。 188 | 189 | ![](https://static.lovebilibili.com/wordcount_03.png) 190 | 191 | 执行以下命令执行jar包: 192 | 193 | ```shell 194 | hadoop jar /usr/local/hadoop-3.2.2/jar/hadooptest-1.0-SNAPSHOT.jar WordCount input output 195 | # /usr/local/hadoop-3.2.2/jar/hadooptest-1.0-SNAPSHOT.jar 表示jar包的位置 196 | # WordCount 是类名 197 | # input 是输入文件所在的文件夹 198 | # output 输出的文件夹 199 | ``` 200 | 201 | ![](https://static.lovebilibili.com/wordcount_04.png) 202 | 203 | 这表示运行成功,我们打开web管理界面,找到output文件夹。 204 | 205 | ![](https://static.lovebilibili.com/wordcount_05.png) 206 | 207 | 输出结果就是这个文件,下载下来。 208 | 209 | ![](https://static.lovebilibili.com/wordcount_06.png) 210 | 211 | 然后打开该文件,可以看到统计结果,以下截图为其中一部分结果: 212 | 213 | ![](https://static.lovebilibili.com/wordcount_07.png) 214 | 215 | # 遇到的问题 216 | 217 | 如果出现Running Job一直没有响应,更改`mapred-site.xml`文件内容: 218 | 219 | 更改前: 220 | 221 | ```xml 222 | 223 | 224 | mapreduce.framework.name 225 | yarn 226 | 227 | 228 | ``` 229 | 230 | 更改后: 231 | 232 | ```xml 233 | 234 | 235 | mapreduce.job.tracker 236 | hdfs://192.168.1.4:8001 237 | true 238 | 239 | 240 | ``` 241 | 242 | 然后重新启动hadoop,再执行命令运行jar包任务。 243 | 244 | # 总结 245 | 246 | WordCount相当于大数据的HelloWord程序,对刚入门的同学来说能够通过这个例子学习MapReduce的基本操作,还有搭建环境,还是很有帮助的。接下来还会继续学习大数据相关的知识,希望这篇文章对你有所帮助。 247 | 248 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 249 | 250 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 251 | 252 | ![](https://static.lovebilibili.com/dashacha.png) 253 | 254 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! 255 | 256 | -------------------------------------------------------------------------------- /大数据/学习大数据从安装hadoop开始.md: -------------------------------------------------------------------------------- 1 | > **文章已收录到我的Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 2 | 3 | # 前言 4 | 5 | 最近上手学习大数据,大数据当然离不开核心的Hadoop,所以首先要搭建一个Hadoop环境。我本机电脑配置不太高,又是学习阶段,所以就整个单机版的玩玩,下面记录一下步骤,希望对大家有所帮助。 6 | 7 | # 前期准备 8 | 9 | | 名称 | 版本 | 来源 | 10 | | ------------ | -------- | ------------------------------------------------------------ | 11 | | 虚拟机CentOS | CentOS 7 | 略 | 12 | | JDK | 1.8 | [Oracle官网](https://www.oracle.com/java/technologies/downloads/) | 13 | | hadoop | 3.2.2 | [hadoop官网](https://hadoop.apache.org/releases.html) | 14 | 15 | 虚拟机安装,和JDK安装就不多说了,对于做Java开发的来说都是小菜一碟。 16 | 17 | ## 关闭防火墙 18 | 19 | 要关闭防火墙首先得看开没开,查看防火墙状态,使用命令`systemctl status firewalld.service`。 20 | 21 | ![](https://static.lovebilibili.com/hadoop_danji_1.png) 22 | 23 | 得先用su root,切换到root,然后使用关闭防火墙命令`systemctl status firewalld.service`。关了之后在查看状态,如图所示已经关闭了。 24 | 25 | ![](https://static.lovebilibili.com/hadoop_danji_2.png) 26 | 27 | 但是如果重新开机还是会自动启动,所以要设置开机禁止防火墙。使用命令`systemctl disable firewalld.service`。相反,开机启动防火墙就是`systemctl enable firewalld.service`。 28 | 29 | ## 设置静态IP 30 | 31 | 为什么要设置静态IP呢,因为有时候虚拟机设置的网络IP地址是自动分配的,自动分配的IP问题就出在每次启动虚拟机的时候会随机分配一个IP,这个IP是不固定的,那么当我们用远程工具连接的时候就很不方便,每次都得先使用命令`ip addr`查询虚拟机的IP地址。 32 | 33 | 废话不多说,打开`vim /etc/sysconfig/network-scripts/ifcfg-enp0s3`文件编辑。 34 | 35 | ![](https://static.lovebilibili.com/hadoop_danji_3.png) 36 | 37 | 上面这个就是自动获取IP地址的配置,怎么改成静态IP呢?很简单,看下面配置。 38 | 39 | 1)BOOTPROTO="static" 40 | 41 | 2)ONBOOT="yes" 42 | 43 | 3)配置IPADDR、NETMASK、GATEWAY、DNS1、DNS2。 44 | 45 | ```shell 46 | TYPE="Ethernet" 47 | PROXY_METHOD="none" 48 | BROWSER_ONLY="no" 49 | BOOTPROTO="static" 50 | DEFROUTE="yes" 51 | IPV4_FAILURE_FATAL="no" 52 | IPV6INIT="yes" 53 | IPV6_AUTOCONF="yes" 54 | IPV6_DEFROUTE="yes" 55 | IPV6_FAILURE_FATAL="no" 56 | IPV6_ADDR_GEN_MODE="stable-privacy" 57 | NAME="enp0s3" 58 | UUID="57b0050a-6c8c-4b6f-be68-3810f8ab9b5d" 59 | DEVICE="enp0s3" 60 | ONBOOT="yes" 61 | IPADDR=192.168.1.4 62 | NETMASK=255.255.255.0 63 | GATEWAY=192.168.1.1 64 | DNS1=119.29.29.29 65 | DNS2=8.8.8.8 66 | ``` 67 | 68 | 接着重新加载`systemctl restart network`。 69 | 70 | 然后验证的话,可以输入`ip addr`命令查看一下IP地址。最后ping一下百度,看网络是否通畅。 71 | 72 | ## 关闭selinux服务 73 | 74 | 打开`vim /etc/selinux/config`编辑文件,修改如下配置: 75 | 76 | ```shell 77 | SELINUX=disabled 78 | ``` 79 | 80 | 重启服务器生效,命令为`reboot`。 81 | 82 | ## 修改主机名 83 | 84 | 修改主机名+域名映射,在访问的时候就可以不用IP地址而用域名,方便很多。 85 | 86 | 使用命令`vim /ect/hostname`,修改主机名为`hadooptest100`。 87 | 88 | 域名映射,使用命令`vim /ect/hosts`,修改以下配置: 89 | 90 | ```shell 91 | 192.168.1.4 hadooptest100 92 | ``` 93 | 94 | 接着重启服务器生效。 95 | 96 | ## 设置SSH免密登录 97 | 98 | 执行以下命令。 99 | 100 | ```shell 101 | ssh-keygen -t rsa 102 | cat /home/hadoop/.ssh/id_rsa.pub >> /home/hadoop/.ssh/authorized_keys 103 | chmod 700 /home/hadoop/.ssh 104 | chmod 600 /home/hadoop/.ssh/authorized_keys 105 | ``` 106 | 107 | 使用`ssh localhost`验证,如果不需要密码即可登录则表示设置成功。使用`exit`命令退出登录。 108 | 109 | # 安装Hadoop 110 | 111 | 首先把压缩包放在`usr/local/`目录下,然后解压`tar -zxvf /usr/local/hadoop-3.2.2`。 112 | 113 | ![](https://static.lovebilibili.com/hadoop_danji_4.png) 114 | 115 | ## 配置环境变量 116 | 117 | 使用命令`vim /etc/profile`编辑,在末尾加上: 118 | 119 | ```shell 120 | export JAVA_HOME=/usr/local/jdk1.8.0_161 121 | export PATH=$JAVA_HOME/bin:$PATH 122 | export HADOOP_HOME=/usr/local/hadoop-3.2.2 123 | export HADOOP_COMMON_LIB_NATIVE_DIR=$HADOOP_HOME/lib/native 124 | export HADOOP_OPTS=-Djava.library.path=$HADOOP_HOME/lib 125 | export PATH=$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$PATH 126 | ``` 127 | 128 | wq保存退出之后,使用命令`source /etc/profile`重新加载配置。最后我们使用`hadoop version`验证。 129 | 130 | ![](https://static.lovebilibili.com/hadoop_danji_5.png) 131 | 132 | ## 核心文件设置 133 | 134 | 进入到hadoop安装目录的${HADOOP_HOME}/etc/hadoop/目录,设置core-site.xml文件: 135 | 136 | ```xml 137 | 138 | 139 | hadoop.tmp.dir 140 | /hadoop/data/tmp 141 | hadoop tmp dir 142 | 143 | 144 | fs.default.name 145 | hdfs://hadooptest100:9000 146 | 147 | 148 | ``` 149 | 150 | 设置hdfs-site.xml文件: 151 | 152 | ```xml 153 | 154 | 155 | dfs.replication 156 | 1 157 | 设置副本数 158 | 159 | 160 | 161 | dfs.namenode.name.dir 162 | file:///hadoop/data/dfs/name 163 | nameNode 164 | 165 | 166 | 167 | dfs.datanode.data.dir 168 | file:///hadoop/data/dfs/data 169 | dataNode 170 | 171 | 172 | 173 | dfs.permissions 174 | false 175 | permission 176 | 177 | 178 | 179 | dfs.secondary.http.address 180 | hadooptest100:50070 181 | SNN path 182 | 183 | 184 | ``` 185 | 186 | 设置mapred-site.xml文件: 187 | 188 | ```xml 189 | 190 | 191 | mapreduce.framework.name 192 | yarn 193 | 194 | 195 | ``` 196 | 197 | 设置yarn-site.xml文件: 198 | 199 | ```xml 200 | 201 | 202 | 203 | yarn.resourcemanager.hostname 204 | hosttest100 205 | 206 | 207 | 208 | yarn.nodemanager.aux-services 209 | mapreduce_shuffle 210 | 211 | 212 | ``` 213 | 214 | 设置hadoop_evn.sh文件: 215 | 216 | ```shell 217 | export HDFS_DATANODE_USER=root 218 | export HDFS_DATANODE_SECURE_USER=root 219 | export HDFS_NAMENODE_USER=root 220 | export HDFS_SECONDARYNAMENODE_USER=root 221 | export YARN_RESOURCEMANAGER_USER=root 222 | export YARN_NODEMANAGER_USER=root 223 | export JAVA_HOME=/usr/local/jdk1.8.0_161 224 | export HADOOP_HOME=/usr/local/hadoop-3.2.2 225 | ``` 226 | 227 | cd到sbin目录下,设置start-dfs.sh、stop-dfs.sh文件,在前面加上: 228 | 229 | ```shell 230 | HDFS_DATANODE_USER=root 231 | HDFS_DATANODE_SECURE_USER=hdfs 232 | HDFS_NAMENODE_USER=root 233 | HDFS_SECONDARYNAMENODE_USER=root 234 | ``` 235 | 236 | 设置start-yarn.sh、stop-yarn.sh文件,在前面加上: 237 | 238 | ```shell 239 | YARN_RESOURCEMANAGER_USER=root 240 | HDFS_DATANODE_SECURE_USER=yarn 241 | YARN_NODEMANAGER_USER=root 242 | ``` 243 | 244 | # 启动hadoop 245 | 246 | 首先需要格式化,切换到bin目录下,使用以下命令。 247 | 248 | ```shell 249 | hdfs namenode -format 250 | ``` 251 | 252 | 接着就cd到sbin目录下,使用命令`./start-all.sh`一键启动hadoop。 253 | 254 | ![](https://static.lovebilibili.com/hadoop_danji_6.png) 255 | 256 | 然后使用`jps`命令查看java进程。 257 | 258 | ```shell 259 | [root@hadooptest100 sbin]# jps 260 | 24530 NameNode 261 | 24738 DataNode 262 | 25198 SecondaryNameNode 263 | 26110 NodeManager 264 | 1918 Jps 265 | ``` 266 | 267 | 最后可以在浏览器输入hadoop服务器的ip加上9870端口号,访问hadoop的管理界面。 268 | 269 | ![](https://static.lovebilibili.com/hadoop_danji_7.png) 270 | 271 | # 总结 272 | 273 | 这里需要说明的是,我是为了方便才使用root用户部署hadoop,在生产环境切记不可使用root用户。所谓万事开头难,部署完hadoop环境之后,下一步我们就可以学习MapReduce了。感谢大家的阅读,希望能对你有所帮助。 274 | 275 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 276 | 277 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 278 | 279 | ![](https://static.lovebilibili.com/dashacha.png) 280 | 281 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /常用的设计模式/代理模式以及应用.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 代理模式以及应用 3 | date: 2020-04-19 14:56:04 4 | index_img: https://static.lovebilibili.com/proxy_index.jpg 5 | tags: 6 | - java 7 | - 设计模式 8 | --- 9 | 10 | # 代理模式 11 | 12 | **代理模式的定义:**代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。 13 | 14 | 15 | 16 | 通俗点说,就是一个中介,比如有一个广州人,是个本地人,有两套房,他要租出去收租,但是除了收租,他还要去找租客,带租客看房,还要准备租房合同,核算水电费等等,很麻烦。这个本地人他也不想这么折腾,他只想**完成他的核心业务(收钱)**,其他杂七杂八的事情就不想管,但是总要有人去做,那就找**租房中介**,也就是二手房东。二手房东就代理这个广州本地人把房子租给租客。这个道理就是这么简单。 17 | 18 | 他们这些在广州有房子的本地人都可以找中介公司去代理租房是一样的。因为很多广州本地人都有这个需求,干脆就搞一个中介公司来专门去做租房子的事情。 19 | 20 | **代理模式,运用在编程里,也是这个道理,有一些非核心业务的代码,在很多地方都需要用到的逻辑,可以交给代理对象完成,程序员只需要关心核心业务的逻辑即可。** 21 | 22 | # 实现代理模式的三种方式 23 | 24 | 项目就基于上一篇`模板模式`的文章的项目进行试验。 25 | 26 | ## 静态代理 27 | 28 | 假设原来有一个接口`UserService`,controller层调用`userService`的`getAllUser()`方法。如下所示: 29 | 30 | ```java 31 | public interface UserService { 32 | /** 33 | * 获取所有用户信息 34 | * 35 | * @return List 36 | * @author Ye hongzhi 37 | * @date 2020/4/12 38 | */ 39 | List getAllUser() throws Exception; 40 | } 41 | ``` 42 | 43 | ```java 44 | @RestController 45 | @RequestMapping("/xiaoniu") 46 | public class UserController { 47 | 48 | @Resource(name = "userService") 49 | private UserService userService; 50 | 51 | @RequestMapping("/getAllUser") 52 | public List getAllUser()throws Exception{ 53 | return userService.getAllUser(); 54 | } 55 | } 56 | ``` 57 | 58 | 如果用静态代理实现记录日志信息,怎么记录呢? 59 | 60 | 首先创建一个代理类`UserServiceProxy`,实现`UserService`接口,然后在`UserServiceProxy`里面创建一个成员变量`userService`,再写一个有参构造器来初始化`userService`。代码如下: 61 | 62 | ```java 63 | public class UserServiceProxy implements UserService { 64 | 65 | private UserService userService; 66 | 67 | public UserServiceProxy(UserService userService) { 68 | this.userService = userService; 69 | } 70 | 71 | @Override 72 | public List getAllUser() throws Exception { 73 | System.out.println("记录日志:执行getAllUser()方法前"); 74 | List userList = userService.getAllUser(); 75 | System.out.println(userList); 76 | System.out.println("记录日志:执行getAllUser()方法后"); 77 | return userList; 78 | } 79 | } 80 | ``` 81 | 82 | 所以在controller层调用的方式就要改一下,是用代理类`UserServiceProxy`调用`getAllUser()`方法。如下: 83 | 84 | ```java 85 | @RestController 86 | @RequestMapping("/xiaoniu") 87 | public class UserController { 88 | 89 | @Resource(name = "userService") 90 | private UserService userService; 91 | 92 | @RequestMapping("/getAllUser") 93 | public List getAllUser()throws Exception{ 94 | return new UserServiceProxy(userService).getAllUser(); 95 | } 96 | } 97 | ``` 98 | 99 | 然后启动项目,调用一下接口,就可以看到控制台打印如下日志: 100 | 101 | ```java 102 | /* 103 | 记录日志:执行getAllUser()方法前 104 | [User{id=1, name='大司马', age=36, job='厨师'}, User{id=2, name='朴老师', age=36, job='主播'}, User{id=3, name='王刚', age=30, job='厨师'}, User{id=4, name='大sao', age=32, job='美食up主'}, User{id=5, name='姚大秋', age=35, job='主持人'}] 105 | 记录日志:执行getAllUser()方法后 106 | */ 107 | ``` 108 | 109 | 这就是静态代理的实现思路,很简单。但是一般我们肯定是不用这种方式。因为这种方式太笨了,很容易就可以看出几个缺点。 110 | 111 | **1.要实现接口,也就是目标的方法要定义一个接口方法,实际上是运用了java多态的特性** 112 | 113 | **2.第一点还不是致命的,因为JDK动态代理也是必须要定义接口;致命的是每一个你想代理的接口你都要去创建一个代理类去实现,假设有很多要代理的接口,那就创建很多代理类,这样显得很臃肿** 114 | 115 | 假设还是不理解为什么要动态代理,不妨我们再多加一个支付接口`PayService`,这个支付接口我们也要加上日志记录。 116 | 117 | 用静态代理怎么做?很简单呀,再创建一个`PayServiceProxy`类不就完了吗,如果还有`OrderService`(订单), 118 | 119 | `WarehouseService`(仓库)等等。那就要创建很多`XXXServiceProxy`类。如果使用动态代理,就没必要创建这么多代理类,创建一个代理类就够了! 120 | 121 | > 动态代理就是为了解决静态代理的这个缺点产生的。 122 | 123 | ## JDK动态代理 124 | 125 | JDK本身就带有动态代理,必须要满足一个条件,就是要有接口。原理其实和静态代理是一样的,也是用代理类去实现接口,但是代理类不是一开始就写好的,而是在程序运行时通过反射创建字节码文件然后加载到JVM。也就是动态生成的代理类对象。 126 | 127 | 下面就是用`JDK动态代理`实现代理模式。 128 | 129 | ```java 130 | public class LogRecordProxy implements InvocationHandler { 131 | 132 | private T target; 133 | 134 | public LogRecordProxy(T t) { 135 | this.target = t; 136 | } 137 | 138 | @Override 139 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 140 | System.out.println("记录日志:执行" + method.getName() + "方法前"); 141 | Object result = method.invoke(target, args); 142 | System.out.println(result); 143 | System.out.println("记录日志:执行" + method.getName() + "方法后"); 144 | return result; 145 | } 146 | 147 | /** 148 | * 获取代理对象的方法 149 | * */ 150 | @SuppressWarnings("unchecked") 151 | public T getProxy() throws Exception { 152 | return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); 153 | } 154 | } 155 | ``` 156 | 157 | 在controller层,就要改成这样。 158 | 159 | ```java 160 | @RestController 161 | @RequestMapping("/xiaoniu") 162 | public class UserController { 163 | 164 | @Resource 165 | private UserService userService; 166 | 167 | @RequestMapping("/getAllUser") 168 | public List getAllUser() throws Exception { 169 | //获取代理对象 170 | UserService userServiceProxy = new LogRecordProxy<>(userService).getProxy(); 171 | return userServiceProxy.getAllUser(); 172 | } 173 | } 174 | ``` 175 | 176 | 假设有一个`PayService`也要做日志记录,就可以直接使用。 177 | 178 | ```java 179 | @Resource(name = "payService") 180 | private PayService payService; 181 | 182 | @RequestMapping("/pay") 183 | public String pay(@RequestParam(name = "channel") String channel, 184 | @RequestParam(name = "amount") String amount 185 | )throws Exception{ 186 | //获取代理对象,实际上就在构造器上改一下传入的参数即可 187 | PayService payServiceProxy = new LogRecordProxy<>(payService).getProxy(); 188 | return payServiceProxy.pay(channel,amount); 189 | } 190 | ``` 191 | 192 | 很多文章给的例子都不带泛型,也可以,就是获取的代理对象需要强转一下,强转成对应的接口类。 193 | 194 | **注意:这里一定要用接口接收代理对象,不能用实现类!** 195 | 196 | 因为返回的对象已经不是实现类的对象,而是和实现类有共同的接口类的代理类对象,所以当然只能用接口类去接收。 197 | 198 | > 这也是为什么一再强调要面向接口编程的原因,因为面向接口编程可以做更多的扩展。假设是面向实现类去编程,那就不能用JDK动态代理去扩展了! 199 | 200 | ## CGLB动态代理 201 | 202 | 那如果有些场景真的没有接口呢,我们怎么运用代理模式? 203 | 204 | 首先引入maven配置 205 | 206 | ```xml 207 | 208 | cglib 209 | cglib 210 | 2.2.2 211 | 212 | ``` 213 | 214 | 然后创建一个方法拦截器`LogRecordInterceptor`,要实现`MethodInterceptor`类,如下: 215 | 216 | ```java 217 | public class LogRecordInterceptor implements MethodInterceptor { 218 | 219 | private Object target; 220 | 221 | public LogRecordInterceptor(Object target) { 222 | this.target = target; 223 | } 224 | 225 | @Override 226 | public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { 227 | System.out.println("记录日志:执行" + method.getName() + "方法前,参数:" + Arrays.toString(args)); 228 | Object result = method.invoke(target, args); 229 | System.out.println(result); 230 | System.out.println("记录日志:执行" + method.getName() + "方法后,参数:" + Arrays.toString(args)); 231 | return result; 232 | } 233 | } 234 | ``` 235 | 236 | 然后再创建一个工厂类`InterceptorFactory`,用于创建代理对象。 237 | 238 | ```java 239 | public class InterceptorFactory { 240 | 241 | @SuppressWarnings("unchecked") 242 | public static T getInterceptor(Class clazz, MethodInterceptor methodInterceptor) { 243 | Enhancer enhancer = new Enhancer(); 244 | enhancer.setSuperclass(clazz); 245 | enhancer.setCallback(methodInterceptor); 246 | return (T) enhancer.create(); 247 | } 248 | } 249 | ``` 250 | 251 | 接着我们就可以创建一个没有接口的类,我这里就创建一个数学工具类进行测试 252 | 253 | ```java 254 | public class MathUtil { 255 | /** 256 | * 获取一个数的平方 257 | * */ 258 | public String getSquare(int num) { 259 | return String.valueOf(num * num); 260 | } 261 | } 262 | ``` 263 | 264 | 然后在controller层定义一个接口来测试 265 | 266 | ```java 267 | @RequestMapping("/getSquare") 268 | public String getSquare(@RequestParam(name = "num") Integer num) throws Exception { 269 | MathUtil mathUtil = InterceptorFactory.getInterceptor(MathUtil.class, new LogRecordInterceptor(new MathUtil())); 270 | return mathUtil.getSquare(num); 271 | } 272 | ``` 273 | 274 | 用浏览器或者`POSTMAN`工具调用接口,就可以在控制台看到以下输出: 275 | 276 | ```java 277 | /* 278 | 记录日志:执行getSquare方法前,参数:[2] 279 | 4 280 | 记录日志:执行getSquare方法后,参数:[2] 281 | */ 282 | ``` 283 | 284 | 这样就实现没有定义接口也可以实现动态代理! 285 | 286 | 实际上,定义接口的也可以用这种方法来进行扩展,比如上面的`userService`接口 287 | 288 | ```java 289 | @RestController 290 | @RequestMapping("/xiaoniu") 291 | public class UserController { 292 | 293 | @Resource 294 | private UserService userService; 295 | 296 | @RequestMapping("/getAllUser") 297 | public List getAllUser() throws Exception { 298 | UserServiceImpl userServiceProxy = InterceptorFactory 299 | .getInterceptor(UserServiceImpl.class, 300 | new LogRecordInterceptor(userService)); 301 | return userServiceProxy.getAllUser(); 302 | } 303 | } 304 | ``` 305 | 306 | 调用接口我们在控制台也是可以看到以下输出日志: 307 | 308 | ```java 309 | /* 310 | 记录日志:执行getAllUser方法前,参数:[] 311 | [User{id=1, name='大司马', age=36, job='厨师'}, User{id=2, name='朴老师', age=36, job='主播'}, User{id=3, name='王刚', age=30, job='厨师'}, User{id=4, name='大sao', age=32, job='美食up主'}, User{id=5, name='姚大秋', age=35, job='主持人'}] 312 | 记录日志:执行getAllUser方法后,参数:[] 313 | */ 314 | ``` 315 | 316 | # 总结 317 | 318 | **以上就是代理模式的一些通俗的解释,还有三种实现的方式的学习** 319 | 320 | 多说几句,我们都知道`Spring`框架有两个核心技术,一个叫控制反转`IOC`,另一个叫切面编程`AOP`。切面编程大家都很熟悉,用的就是代理模式,那么`AOP`实现的代理模式用的是`JDK动态代理`还是`CLB动态代理`? 321 | 322 | 答曰:**两个都用!** 323 | 324 | 最简单的,我们看`Spring`的事务管理,就是用代理模式实现的,如果有兴趣,其实我们自己也可以通过`JDK动态代理`手写实现事务管理,其实不是很难。篇幅有限,以后可以单独写一篇文章详细说明`Spring`的事务管理,敬请期待。更多的设计模式实战经验的分享,就关注java技术小牛吧。 325 | 326 | 100 327 | 328 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /常用的设计模式/教你用构建者-生成器-模式优雅地创建对象.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 教你用构建者(生成器)模式优雅地创建对象 3 | date: 2020-04-27 23:13:53 4 | index_img: https://static.lovebilibili.com/builder_model.jpg 5 | tags: 6 | - 设计模式 7 | - java 8 | --- 9 | 10 | # 为什么要用构建者模式 11 | 很多博客文章上来就先抛出一个定义,我们不妨反过来问一句为什么要用构建者模式。 12 | 首先我们创建一个`User`类,然后采用有参构造器的方式创建对象。 13 | 14 | ```java 15 | public class User { 16 | 17 | private String id; 18 | 19 | private String name; 20 | 21 | private String gender; 22 | 23 | private String address; 24 | 25 | private Integer age; 26 | 27 | private String phone; 28 | 29 | //省略无参构造器,有参构造器,getter,setter方法... 30 | } 31 | ``` 32 | ```java 33 | public static void main(String[] args) throws Exception { 34 | String id = UUID.randomUUID().toString().replaceAll("-", ""); 35 | User user = new User(id, "张三", "男", "广州天河", 20, "135461852xx"); 36 | } 37 | ``` 38 | 我们通过有参构造器创建对象,并且赋值,看起来没什么问题,因为我们经常看到有人是这样写的。 39 | 事实上,如果`User`对象里面有更多的字段,通过有参构造器去创建对象是很难一眼看出字段具体是什么意思,我们经常要看着`User构造器`的代码,然后对照顺序才能看出字段的代表什么意思。 40 | ```java 41 | public User(String id, String name, String gender, String address, Integer age, String phone) { 42 | this.id = id; 43 | this.name = name; 44 | this.gender = gender; 45 | this.address = address; 46 | this.age = age; 47 | this.phone = phone; 48 | } 49 | ``` 50 | 比如通过上面这个,我们可以知道第一个参数是id,第二个参数是名字,第三个是性别... 51 | ## 使用有参构造器的缺点: 52 | 这显然不利于代码的维护性,对于不熟悉业务的新入职的员工,如果看到这种方式构建一个对象,估计要看上一会,有些项目我遇过一个构造器十几个参数的,更加离谱。而且一般老代码还不敢乱动他的这个构造器,一不小心你动了构造器里面的一个参数的顺序,直接GG;或者你在他原有的构造器后面加多一个参数,你会发现他很多地方都引用了这个有参构造器,你很多地方都要去修改,是真的恶心。 53 | 54 | ## 解决方法一 使用无参构造器,通过setter方法设置属性值 55 | 56 | ```java 57 | public static void main(String[] args) throws Exception { 58 | User user = new User(); 59 | user.setId(UUID.randomUUID().toString().replaceAll("-", "")); 60 | user.setName("张三"); 61 | user.setAge(20); 62 | user.setGender("男"); 63 | user.setPhone("135461852xx"); 64 | user.setAddress("广州天河"); 65 | out.println(user); 66 | } 67 | ``` 68 | 上面这样,显然比直接用有参构造器要好很多,因为这样就可以创建对象和赋值分开进行,一眼就可以看出对什么属性值赋值,而且如果加一个字段,我们不需要再每一处都去修改,因为用的是**无参构造器**,是不是这样写就是万全之计呢? 69 | 也不是,因为这样创建对象和赋值是分开的,各个参数的初始化被放到了不同的方法中调用,这会导致严重的线程不安全问题(使用构造器则不会有这个问题),对象在一连串的set方法中,可能会出现状态不一致的情况,这是应该尽量避免的。 70 | 71 | ## 解决方法二 通过构建者模式,链式调用构建方法设置属性值 72 | 73 | 什么是链式编程,就是调用一个方法,返回值是他本身,可以继续调用下一个方法,返回又是他本身,如此调用下去,看上去就像一条链子一样。典型的例子可以看`java8`新特性的`Stream`流操作。我们可以使用构建者模式,也能达到这种效果,并且线程安全,而且能直观地看到属性值的意思。总得来说,既保证线程安全,也很具有代码的可读性。先看结果代码: 74 | 75 | ```java 76 | public static void main(String[] args) throws Exception { 77 | String id = UUID.randomUUID().toString().replaceAll("-", ""); 78 | User user = UserBuilder.getInstance() 79 | .newPojo() 80 | .addId(id) 81 | .addName("张三") 82 | .addGender("男") 83 | .addAge(20) 84 | .addPhone("135461852xx") 85 | .addAddress("广州天河") 86 | .build(); 87 | } 88 | ``` 89 | 怎么实现呢?其实很简单,我们只需要创建一个`UserBuilder`类即可。代码如下: 90 | ```java 91 | public class UserBuilder { 92 | 93 | private User user; 94 | 95 | private UserBuilder() { 96 | } 97 | 98 | public static UserBuilder getInstance() { 99 | return new UserBuilder(); 100 | } 101 | 102 | public UserBuilder newPojo() { 103 | this.user = new User(); 104 | //返回本身 105 | return this; 106 | } 107 | 108 | public UserBuilder addId(String id) { 109 | this.user.setId(id); 110 | //返回本身 111 | return this; 112 | } 113 | 114 | public UserBuilder addName(String name) { 115 | this.user.setName(name); 116 | return this; 117 | } 118 | 119 | public UserBuilder addGender(String gender) { 120 | this.user.setGender(gender); 121 | return this; 122 | } 123 | 124 | public UserBuilder addAge(Integer age) { 125 | this.user.setAge(age); 126 | return this; 127 | } 128 | 129 | public UserBuilder addAddress(String address) { 130 | this.user.setAddress(address); 131 | return this; 132 | } 133 | 134 | public UserBuilder addPhone(String phone) { 135 | this.user.setPhone(phone); 136 | return this; 137 | } 138 | 139 | public User build() { 140 | return this.user; 141 | } 142 | } 143 | ``` 144 | 那么是不是这种方式就是万全之计呢,就一定没有缺点吗? 145 | 146 | ### 构造者模式的缺点还是有的 147 | 148 | 1.代码冗长。如果一个对象的属性很多,那我们在创建一个对象时,链式就会变得很长,但是这也没有办法,无论采用构造器还是builder模式都会很长。如果非要变得简洁一点,那就只有采用原型模式(克隆)等其他方式了。 149 | 2.会产生很多`Builder`类。我们可以放在一个包下统一管理应该问题不大。 150 | 第二个缺点实际上可以使用`Lombok`插件,然后在实体类上使用`@Builder`注解,就不会产生过多的`Builder`类了。但是有些公司的技术总监不太建议使用`Lombok`,那就莫得办法了... 151 | 152 | ### 注意点 153 | 154 | 有很多博客的示范代码,`Builder`类的`addXXX`方法会写成`setXXX`方法,这是一个隐患。因为很多框架,对Setter方法比较敏感,往往会对`Setter`方法做一些处理,所以`Builder`类里的设置属性值方法尽量不要用`setXXX`命名,防止出现一些不明原因的错误。 155 | 156 | # 结束语 157 | 一般我们在项目中创建复杂的对象时,建议采用这种构建者模式创建对象。这样可以使代码可读性更好。 158 | 在java源码中,我们也可以看到构建者模式的应用。比如在`StringBuilder`类中: 159 | ```java 160 | @Override 161 | public StringBuilder append(CharSequence s) { 162 | super.append(s); 163 | return this; 164 | } 165 | 166 | /** 167 | * @throws IndexOutOfBoundsException {@inheritDoc} 168 | */ 169 | @Override 170 | public StringBuilder append(CharSequence s, int start, int end) { 171 | super.append(s, start, end); 172 | return this; 173 | } 174 | 175 | @Override 176 | public StringBuilder append(char[] str) { 177 | super.append(str); 178 | return this; 179 | } 180 | ``` 181 | `StringBuilder`的`append()`方法也是通过返回`this`对象实现链式构建对象,人们经常说这个`StringBuilder`类线程不安全是因为`append()`方法没有用`synchronized`修饰。`StringBuffer`则用了`synchronized`修饰,所以就是线程安全的。 182 | 还有`Mybatis`框架中,构建`SqlSessionFactory`对象是使用`SqlSessionFactoryBuilder `类进行构建,构建者模式运用非常广泛,非常值得学习。更多的设计模式实战经验的分享,就关注java技术小牛吧。 183 | 184 | 100 185 | 186 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /常用的设计模式/教你用策略模式解决多重if-else.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: 教你用策略模式解决多重if-else 4 | date: 2020-04-05 14:12:40 5 | index_img: https://static.lovebilibili.com/strategy_index.jpg 6 | tags: 7 | - 设计模式 8 | - java 9 | --- 10 | 11 | # 写在前面 12 | 13 | 很多人可能在公司就是做普通的CRUD的业务,对于设计模式,即使学了好像也用处不大,顶多就在面试的时候能说上几种常见的单例模式,工厂模式。而在实际开发中,设计模式似乎很难用起来。 14 | 15 | 在现在的环境下,程序员的竞争已经非常激烈了,要体现出自身的价值,最直接的体现当然是差异化。这无需多说,我认为在实际开发中能运用设计模式,是很能体现差异化的。设计模式是一些前人总结的较好的方法,使程序能有更好的扩展性,可读性,维护性。 16 | 17 | 下面举个例子,使用策略模式解决多重if-else的代码结构。想学习更多的设计模式的实战经验,那就点个关注吧,谢谢大佬。 18 | 19 | # 使用if-else 20 | 21 | 假设我们要开发一个支付接口,要对接多种支付方式,通过渠道码区分各种的支付方式。于是定义一个枚举`PayEnum`,如下: 22 | 23 | ```java 24 | public enum PayEnum { 25 | ALI_PAY("ali","支付宝支付"), 26 | WECHAT_PAY("wechat","微信支付"), 27 | UNION_PAY("union","银联支付"), 28 | XIAO_MI_PAY("xiaomi","小米支付"); 29 | /**渠道*/ 30 | private String channel; 31 | /**描述*/ 32 | private String description; 33 | PayEnum(String channel, String description) { 34 | this.channel = channel; 35 | this.description = description; 36 | } 37 | /**以下省略字段的get、set方法*/ 38 | ``` 39 | 40 | 创建一个`PayController`类,代码如下: 41 | 42 | ```java 43 | @RestController 44 | @RequestMapping("/xiaoniu") 45 | public class PayController { 46 | @Resource(name = "payService") 47 | private PayService payService; 48 | /** 49 | * 支付接口 50 | * @param channel 渠道 51 | * @param amount 消费金额 52 | * @return String 返回消费结果 53 | * @author Ye hongzhi 54 | * @date 2020/4/5 55 | */ 56 | @RequestMapping("/pay") 57 | public String pay(@RequestParam(name = "channel") String channel, 58 | @RequestParam(name = "amount") String amount 59 | )throws Exception{ 60 | return payService.pay(channel,amount); 61 | } 62 | } 63 | ``` 64 | 65 | 再创建一个`PayService`接口以及实现类`PayServiceImpl` 66 | 67 | ```java 68 | public interface PayService { 69 | /** 70 | * 支付接口 71 | * @param channel 渠道 72 | * @param amount 金额 73 | * @return String 74 | * @author Ye hongzhi 75 | * @date 2020/4/5 76 | */ 77 | String pay(String channel,String amount)throws Exception; 78 | } 79 | ``` 80 | 81 | ```java 82 | @Service("payService") 83 | public class PayServiceImpl implements PayService { 84 | private static String MSG = "使用 %s ,消费了 %s 元"; 85 | @Override 86 | public String pay(String channel, String amount) throws Exception { 87 | if (PayEnum.ALI_PAY.getChannel().equals(channel)) { 88 | //支付宝 89 | //业务代码... 90 | return String.format(MSG,PayEnum.ALI_PAY.getDescription(),amount); 91 | }else if(PayEnum.WECHAT_PAY.getChannel().equals(channel)){ 92 | //微信支付 93 | //业务代码... 94 | return String.format(MSG,PayEnum.WECHAT_PAY.getDescription(),amount); 95 | }else if(PayEnum.UNION_PAY.getChannel().equals(channel)){ 96 | //银联支付 97 | //业务代码... 98 | return String.format(MSG,PayEnum.UNION_PAY.getDescription(),amount); 99 | }else if(PayEnum.XIAO_MI_PAY.getChannel().equals(channel)){ 100 | //小米支付 101 | //业务代码... 102 | return String.format(MSG,PayEnum.XIAO_MI_PAY.getDescription(),amount); 103 | }else{ 104 | return "输入渠道码有误"; 105 | } 106 | } 107 | } 108 | ``` 109 | 110 | 然后通过浏览器,我们可以看到效果 111 | 112 | ![](https://static.lovebilibili.com/01.png) 113 | 114 | ![](https://static.lovebilibili.com/02.png) 115 | 116 | 这样看,以上代码的确可以实现需求,通过渠道码区分支付方式,可是看到上面那么多达4个的`if-else`的代码结构,已经开始显示出问题了。假设有更多的支付方式,那么这段代码就要写更多的`else if`去判断,这显然会不利于代码的扩展,这样会导致这个支付的方法越写越长。 117 | 118 | 在设计模式六大原则中,其中一个原则叫做`开闭原则`,对扩展开放,对修改关闭,应尽量在不修改原有代码的情况下进行扩展。 119 | 120 | 基于上面提到的`开闭原则`,我们可以使用策略模式进行重构。 121 | 122 | # 使用策略模式重构代码 123 | 124 | 定义一个策略接口类`PayStrategy` 125 | 126 | ```java 127 | public interface PayStrategy { 128 | String MSG = "使用 %s ,消费了 %s 元"; 129 | String pay(String channel,String amount)throws Exception; 130 | } 131 | ``` 132 | 133 | 然后再创建四种策略实现类实现接口 134 | 135 | ```java 136 | @Component("aliPayStrategy") 137 | public class AliPayStrategyImpl implements PayStrategy{ 138 | @Override 139 | public String pay(String channel, String amount) throws Exception { 140 | return String.format(MSG, PayEnum.ALI_PAY.getDescription(),amount); 141 | } 142 | } 143 | ``` 144 | 145 | ```java 146 | @Component("wechatPayStrategy") 147 | public class WechatPayStrategyImpl implements PayStrategy{ 148 | @Override 149 | public String pay(String channel, String amount) throws Exception { 150 | return String.format(MSG, PayEnum.WECHAT_PAY.getDescription(),amount); 151 | } 152 | } 153 | ``` 154 | 155 | ```java 156 | @Component("unionPayStrategy") 157 | public class UnionPayStrategyImpl implements PayStrategy{ 158 | @Override 159 | public String pay(String channel, String amount) throws Exception { 160 | return String.format(MSG, PayEnum.UNION_PAY.getDescription(),amount); 161 | } 162 | } 163 | ``` 164 | 165 | ```java 166 | @Component("xiaomiPayStrategy") 167 | public class XiaomiPayStrategyImpl implements PayStrategy{ 168 | @Override 169 | public String pay(String channel, String amount) throws Exception { 170 | return String.format(MSG, PayEnum.XIAO_MI_PAY.getDescription(),amount); 171 | } 172 | } 173 | ``` 174 | 175 | 看到这里实际上已经很清晰了,思路就是通过渠道码,动态获取到具体的实现类,这样就可以实现不需要`if else`判断。怎么通过渠道码获取实现类呢? 176 | 177 | 在`PayEnum`枚举加上`BeanName`字段,然后增加一个通过渠道码获取`BeanName`的方法 178 | 179 | ```java 180 | ALI_PAY("ali","支付宝支付","aliPayStrategy"), 181 | WECHAT_PAY("wechat","微信支付","wechatPayStrategy"), 182 | UNION_PAY("union","银联支付","unionPayStrategy"), 183 | XIAO_MI_PAY("xiaomi","小米支付","xiaomiPayStrategy"); 184 | /**策略实现类对应的 beanName*/ 185 | private String beanName; 186 | /** 187 | * 通过渠道码获取枚举 188 | * */ 189 | public static PayEnum findPayEnumBychannel(String channel){ 190 | PayEnum[] enums = PayEnum.values(); 191 | for (PayEnum payEnum : enums){ 192 | if(payEnum.getChannel().equals(channel)){ 193 | return payEnum; 194 | } 195 | } 196 | return null; 197 | } 198 | //构造器 199 | PayEnum(String channel, String description, String beanName) { 200 | this.channel = channel; 201 | this.description = description; 202 | this.beanName = beanName; 203 | } 204 | ``` 205 | 206 | 这时候还差一个获取Spring上下文对象的工具类,于是我们创建一个`SpringContextUtil`类 207 | 208 | ```java 209 | @Component 210 | public class SpringContextUtil implements ApplicationContextAware { 211 | /** 212 | * 上下文对象实例 213 | */ 214 | private static ApplicationContext applicationContext; 215 | /** 216 | * 获取applicationContext 217 | */ 218 | private static ApplicationContext getApplicationContext() { 219 | return applicationContext; 220 | } 221 | /** 222 | * 通过name获取Bean 223 | * */ 224 | public static Object getBean(String name){ 225 | return getApplicationContext().getBean(name); 226 | } 227 | /** 228 | * 通过name,以及Clazz返回指定的Bean 229 | * */ 230 | public static T getBean(String name,Class clazz){ 231 | return getApplicationContext().getBean(name,clazz); 232 | } 233 | @Override 234 | @Autowired 235 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 236 | SpringContextUtil.applicationContext = applicationContext; 237 | } 238 | ``` 239 | 240 | 接着定义一个工厂类,通过渠道码获取对应的策略实现类 241 | 242 | ```java 243 | public class PayStrategyFactory { 244 | /** 245 | * 通过渠道码获取支付策略具体实现类 246 | * */ 247 | public static PayStrategy getPayStrategy(String channel){ 248 | PayEnum payEnum = PayEnum.findPayEnumBychannel(channel); 249 | if(payEnum == null){ 250 | return null; 251 | } 252 | return SpringContextUtil.getBean(payEnum.getBeanName(),PayStrategy.class); 253 | } 254 | } 255 | ``` 256 | 257 | 最后我们再改造一下原来的`PayServiceImpl`的`pay`方法 258 | 259 | ```java 260 | @Override 261 | public String pay(String channel, String amount) throws Exception { 262 | PayStrategy payStrategy = PayStrategyFactory.getPayStrategy(channel); 263 | if(payStrategy == null){ 264 | return "输入渠道码有误"; 265 | } 266 | return payStrategy.pay(channel,amount); 267 | } 268 | ``` 269 | 270 | 哇喔!突然间代码就显得清爽很多了! 271 | 272 | 小伙伴们看到这里,get到新的技能了吗? 273 | 274 | > 假设需要增加新的支付方式,就不需要再使用else if 去判断,而是在枚举中定义一个新的枚举对象,然后再增加一个策略实现类,实现对应的方法,那就可以很轻松地扩展。也实现了开闭原则。 275 | 276 | # 写在最后 277 | 278 | 设计模式运用得熟练的话,很多代码可以写得很优雅。更多的设计模式实战经验的分享,就关注java技术小牛吧。 279 | 280 | 100 281 | 282 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /常用的设计模式/观察者模式以及实际项目应用.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 观察者模式以及实际项目应用 3 | date: 2020-05-02 20:44:32 4 | index_img: https://static.lovebilibili.com/oberver_index.jpg 5 | tags: 6 | - java 7 | - 设计模式 8 | --- 9 | 10 | # 观察者模式 11 | 12 | ## 定义 13 | 14 | 观察者模式(Observer),又叫**发布-订阅模式(Publish/Subscribe)**,定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新。 15 | 16 | ## 通俗解释 17 | 18 | 比如我们在宿舍打斗地主,我们要找个人来“放风”,这个人在门口观察,如果有宿管过了检查,那么就通知宿舍其他的小伙伴停止斗地主回床上睡觉。这种模式就被称为观察者模式。 19 | 20 | 从这个例子看,“宿管是否过来宿舍”是订阅的主题,观察者是放风的人,订阅者是打斗地主的小伙伴,被观察者就是宿管。 21 | 22 | ## 不使用观察者模式的问题 23 | 24 | 假设我们基于之前在策略模式讲的电子支付的例子,支付完成后要发送消息,发送的消息有:短信,公众号消息,APP站内消息,邮箱。如果不使用观察者模式,怎么做呢?看代码: 25 | 26 | ```java 27 | @Override 28 | public String pay(String channel, String amount) throws Exception { 29 | PayStrategy payStrategy = PayStrategyFactory.getPayStrategy(channel); 30 | if(payStrategy == null){ 31 | return "输入渠道码有误"; 32 | } 33 | String msg = payStrategy.pay(channel, amount); 34 | //发送短信 35 | System.out.println("发送短信:"+msg); 36 | //发送微信公众号消息 37 | System.out.println("发送微信公众号消息:"+msg); 38 | //发送邮件 39 | System.out.println("发送邮件:"+msg); 40 | //发送APP系统信息 41 | System.out.println("发送APP系统信息:"+msg); 42 | return msg; 43 | } 44 | ``` 45 | 46 | 启动项目是没有问题的,我们调用接口后可以看到控制台打印以下信息: 47 | 48 | ```java 49 | /** 50 | 发送短信:使用 支付宝支付 ,消费了 100 元 51 | 发送微信公众号消息:使用 支付宝支付 ,消费了 100 元 52 | 发送邮件:使用 支付宝支付 ,消费了 100 元 53 | 发送APP系统信息:使用 支付宝支付 ,消费了 100 元 54 | */ 55 | ``` 56 | 57 | **但是我们很明显可以看出有以下的问题:** 58 | 59 | - 每次支付如果需要新增一种消息通知方式,则要修改原来的类,不利于维护。 60 | - 违反了开闭原则,对拓展开放,对修改关闭。 61 | - 违反了单一职责原则,支付不应该糅杂消息通知的功能。 62 | 63 | 上面就从代码演示了为什么要使用观察者模式,很多文章说不清楚,单纯地抛出一个概念和一些简单的例子,实际项目中肯定是没有那么简单。 64 | 65 | ## 使用观察者模式优化 66 | 67 | 这里的话,我不使用`java`自带的`Observer`和`Observable`来做,因为实际项目中一般都会使用`Spring`框架,`Spring`框架有一个事件机制,也是使用观察者模式的这种设计模式,而且在实际项目中我们往往会采用这种成熟度更高的框架,就像代理模式我们也很少会直接使用原生的`JDK动态代理`,而是采用`SpringAOP`来实现。 68 | 69 | ## 创建支付的事件 70 | 71 | ```java 72 | //继承ApplicationEvent类 73 | public class PayEvent extends ApplicationEvent { 74 | //消息体 75 | private Map map; 76 | //订阅主题 77 | private String topic; 78 | 79 | public PayEvent(Object source, Map map, String topic) { 80 | //调用父类的构造器 81 | super(source); 82 | this.map = map; 83 | this.topic = topic; 84 | } 85 | 86 | public Map getMap() { 87 | return map; 88 | } 89 | public void setMap(Map map) { 90 | this.map = map; 91 | } 92 | public String getTopic() { 93 | return topic; 94 | } 95 | public void setTopic(String topic) { 96 | this.topic = topic; 97 | } 98 | } 99 | ``` 100 | 101 | ## 创建事件监听类 102 | 103 | ```java 104 | //短信监听,实现ApplicationListener接口,重写onApplicationEvent()方法 105 | @Component 106 | public class SmsListener implements ApplicationListener { 107 | 108 | @Override 109 | public void onApplicationEvent(PayEvent payEvent) { 110 | //订阅主题 111 | String topic = payEvent.getTopic(); 112 | //消息体 113 | Map map = payEvent.getMap(); 114 | //发送短信 115 | System.out.println("订阅主题是:" + topic + ";发送短信:" + map.get("msg")); 116 | } 117 | } 118 | ``` 119 | 120 | ```java 121 | //公众号监听 122 | @Component 123 | public class WechatListener implements ApplicationListener { 124 | 125 | @Override 126 | public void onApplicationEvent(PayEvent payEvent) { 127 | String topic = payEvent.getTopic(); 128 | Map map = payEvent.getMap(); 129 | System.out.println("订阅主题是:" + topic + ";发送公众号消息:" + map.get("msg")); 130 | } 131 | } 132 | ``` 133 | 134 | ```java 135 | //邮箱监听 136 | @Component 137 | public class MailListener implements ApplicationListener { 138 | 139 | @Override 140 | public void onApplicationEvent(PayEvent payEvent) { 141 | String topic = payEvent.getTopic(); 142 | Map map = payEvent.getMap(); 143 | System.out.println("订阅主题是:" + topic + ";发送邮件:" + map.get("msg")); 144 | } 145 | } 146 | ``` 147 | 148 | ```java 149 | //App站内消息监听 150 | @Component 151 | public class AppListener implements ApplicationListener { 152 | 153 | @Override 154 | public void onApplicationEvent(PayEvent payEvent) { 155 | String topic = payEvent.getTopic(); 156 | Map map = payEvent.getMap(); 157 | System.out.println("订阅主题是:" + topic + ";发送App站内消息:" + map.get("msg")); 158 | } 159 | } 160 | ``` 161 | 162 | ## 重构PayServiceImpl类 163 | 164 | ```java 165 | @Override 166 | public String pay(String channel, String amount) throws Exception { 167 | PayStrategy payStrategy = PayStrategyFactory.getPayStrategy(channel); 168 | if(payStrategy == null){ 169 | return "输入渠道码有误"; 170 | } 171 | String msg = payStrategy.pay(channel, amount); 172 | Map map = new HashMap<>(); 173 | map.put("msg",msg); 174 | //创建一个支付事件 175 | PayEvent payEvent = new PayEvent(this, map, "支付"); 176 | //获取Spring的ApplicationContext容器,发布事件,监听类监听到事件后就会发送消息 177 | SpringContextUtil.getApplicationContext().publishEvent(payEvent); 178 | return msg; 179 | } 180 | ``` 181 | 182 | 然后我们启动项目,调用接口,控制台就可以打印的信息: 183 | 184 | ```java 185 | /** 186 | 订阅主题是:支付;发送App站内消息:使用 支付宝支付 ,消费了 100 元 187 | 订阅主题是:支付;发送邮件:使用 支付宝支付 ,消费了 100 元 188 | 订阅主题是:支付;发送短信:使用 支付宝支付 ,消费了 100 元 189 | 订阅主题是:支付;发送公众号消息:使用 支付宝支付 ,消费了 100 元 190 | */ 191 | ``` 192 | 193 | ## 异步监听事件,实现解耦 194 | 195 | 改造之后是否就一劳永逸了呢,实际上并非如此。因为上面的消息发送的监听类是同步的,也就是如果发送消息出现异常,那就会导致支付的接口无法正常返回。请看以下代码: 196 | 197 | ```java 198 | @Component 199 | public class WechatListener implements ApplicationListener { 200 | 201 | @Override 202 | public void onApplicationEvent(PayEvent payEvent) { 203 | String topic = payEvent.getTopic(); 204 | Map map = payEvent.getMap(); 205 | //在发送微信公众号消息的逻辑中制造异常 206 | System.out.println(10 / 0); 207 | System.out.println("订阅主题是:" + topic + ";发送公众号消息:" + map.get("msg")); 208 | } 209 | } 210 | ``` 211 | 212 | ```java 213 | @Override 214 | public String pay(String channel, String amount) throws Exception { 215 | PayStrategy payStrategy = PayStrategyFactory.getPayStrategy(channel); 216 | if(payStrategy == null){ 217 | return "输入渠道码有误"; 218 | } 219 | String msg = payStrategy.pay(channel, amount); 220 | Map map = new HashMap<>(); 221 | map.put("msg",msg); 222 | //创建一个支付事件 223 | PayEvent payEvent = new PayEvent(this, map, "支付"); 224 | //获取Spring的ApplicationContext容器,发布事件 225 | SpringContextUtil.getApplicationContext().publishEvent(payEvent); 226 | //发送消息后的逻辑,打印日志到控制台 227 | System.out.println("发送消息后的逻辑代码..."); 228 | return msg; 229 | } 230 | ``` 231 | 232 | 我们在发送公众号消息的逻辑里制造了一个异常,然后在`pay()`方法中加了一个打印日志在发布支付的事件后面,接下来调用接口,结果是: 233 | 234 | ```java 235 | /** 236 | 订阅主题是:支付;发送App站内消息:使用 支付宝支付 ,消费了 100 元 237 | 订阅主题是:支付;发送邮件:使用 支付宝支付 ,消费了 100 元 238 | 订阅主题是:支付;发送短信:使用 支付宝支付 ,消费了 100 元 239 | java.lang.ArithmeticException: / by zero 240 | ...... 241 | */ 242 | ``` 243 | 244 | 发送消息后的逻辑是没有被执行。这样显然是不符合业务要求的,因为在很多时候,发送消息失败是不能影响支付流程的,应该异步进行。怎么异步进行发送消息呢? 245 | 246 | 很简单,只需要两个步骤。 247 | 248 | **第一步**:在监听类或者方法上添加`@Async`注解,例如: 249 | 250 | ```java 251 | @Component 252 | @Async//加上异步执行的注解 253 | public class WechatListener implements ApplicationListener { 254 | 255 | @Override 256 | public void onApplicationEvent(PayEvent payEvent) { 257 | String topic = payEvent.getTopic(); 258 | Map map = payEvent.getMap(); 259 | System.out.println(10 / 0); 260 | System.out.println("订阅主题是:" + topic + ";发送公众号消息:" + map.get("msg")); 261 | } 262 | } 263 | ``` 264 | 265 | **第二步**:在`SpringBoot`启动类上添加`@EnableAsync`注解,例如: 266 | 267 | ```java 268 | @SpringBootApplication 269 | @EnableAsync//添加启用异步的注解 270 | public class StrategyApplication { 271 | public static void main(String[] args) { 272 | SpringApplication.run(StrategyApplication.class, args); 273 | } 274 | } 275 | ``` 276 | 277 | 然后就可以实现异步监听了,调用接口,我们可以看到控制台打印的日志如下: 278 | 279 | ```java 280 | /** 281 | 订阅主题是:支付;发送App站内消息:使用 支付宝支付 ,消费了 100 元 282 | 订阅主题是:支付;发送邮件:使用 支付宝支付 ,消费了 100 元 283 | 订阅主题是:支付;发送短信:使用 支付宝支付 ,消费了 100 元 284 | 发送消息后的逻辑代码... 285 | 使用 支付宝支付 ,消费了 100 元 286 | java.lang.ArithmeticException: / by zero 287 | ...... 288 | */ 289 | ``` 290 | 291 | 明显可以看到支付后的逻辑也能正常执行下去,证明实现了异步监听! 292 | 293 | ## 扩展 294 | 295 | 在`Spring`里提供了许多的监听器,这里只是介绍了其中一种。 296 | 297 | 还有一种叫`SpringApplicationRunListener`也是很常用的监听器,可以监听`SpringBoot`项目启动的事件,用于在启动项目时加载一些配置。 298 | 299 | 还有一种叫`SmartApplicationListener`,这种监听器可以设置优先级。假设发送消息需要按顺序先发送短信,再发送公众号,再发送邮箱...,那就可以使用这种监听器实现,这里就不多做介绍了,小伙伴有兴趣的话,我可以再写一篇文章详细介绍。 300 | 301 | ## 总结 302 | 303 | 经过重构之后,我们可以明显看到,如果以后要增加一种新的消息通知方式,是不需要修改`PayServiceImpl`的,我们只需要再增加一个监听类即可,这就符合了`开闭原则`。有利于代码的维护。而且最重要是解耦,支付的业务逻辑和发送消息的业务逻辑不会再糅合在一起了,符合`职责单一`原则。 304 | 305 | 在很多框架中,观察者模式都有应用,对于学习很多例如`zookeeper`、`消息中间件`、`微服务注册中心`等知识是有很大帮助的。在实际项目中,观察者模式也是一种很常用的设计模式。比如有一种业务场景,通讯录的部门里有员工离职,需要通知其他依赖于通讯录的应用都要同步部门的员工,那就可以使用这种方式来实现。 306 | 307 | 100 308 | 309 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! 310 | 311 | -------------------------------------------------------------------------------- /并发编程的艺术/多线程开发,先学会线程池吧.md: -------------------------------------------------------------------------------- 1 | # 思维导图 2 | 3 | ![](https://static.lovebilibili.com/ThreadPool_swdt.png) 4 | 5 | > **文章已收录Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 6 | 7 | # 前言 8 | 9 | 在实际开发场景中,我们经常要使用多线程开发应用,比如实现异步操作,或者为了提高程序的效率等等。但是以前我见过有实习生在使用的时候是直接new Runable(),然后start()。没有使用线程池,可能很多初学者对线程池在多线程开发中没有足够的认识,所以我写一篇文章讲讲线程池,希望对大家有所启发。 10 | 11 | # 一、什么是线程池 12 | 13 | 线程池借鉴了"池化"技术的思想,线程池能够对线程的生命周期进行管理,对线程重复利用,并且能够以一种简单的方式将任务的提交与执行相解耦。 14 | 15 | 举个例子来说,线程就像是某个公司的客服小姐姐,每天都要接很多客户的电话,如果同时有1000个客户打电话进来咨询,按正常的逻辑,那就需要1000个客服小姐姐,但是在现实中往往需要考虑成本问题,招这么多人费用太多了,于是就可以这样优化,可以招100个人成立一个客服中心,如果同时超过100个人则提示让客户等待,等有空闲的客服小姐姐时就去响应客户。实现效益最大化。这就是一个池化技术在现实生活中类似的例子。 16 | 17 | # 二、为什么使用线程池 18 | 19 | 一种技术的出现,肯定是要解决存在的问题。如果不用线程池,会怎么样呢?很简单,需要时创建线程,线程跑完销毁,如果频繁去做这两个动作,就会造成比较大的资源消耗。所以线程池主要就是解决这个问题。 20 | 21 | 因此在《java并发编程的艺术》书中就提到以下几点: 22 | 23 | - **降低资源消耗**。通过重复使用已创建的线程,降低线程创建和销毁造成的资源消耗。 24 | - **提高响应速度**。当有任务到达时,任务可以不需要的等到线程创建就能立即执行。 25 | - **提高线程的可管理性**。使用线程池可以进行统一的分配,调优和监控。 26 | 27 | # 三、Executor 28 | 29 | 创建线程池主要使用ThreadPoolExecutor这个类,所以我们先看一张类图。 30 | 31 | ![](https://static.lovebilibili.com/Executor.png) 32 | 33 | 一般来说,遵守面向接口编程的思想,我们都喜欢使用ExecutorService接口接收线程池实例。如下: 34 | 35 | ```java 36 | public static void main(String[] args) throws Exception { 37 | //创建线程池 38 | ExecutorService executor = new ThreadPoolExecutor(10, 10, 1, TimeUnit.SECONDS, new LinkedBlockingDeque<>(10)); 39 | } 40 | ``` 41 | 42 | 这里可以看到创建线程池是使用ThreadPoolExecutor构造器来创建。构造器的参数有什么意义呢,继续往下看。 43 | 44 | ## 3.1 七个关键参数 45 | 46 | ```java 47 | /** 48 | * corePoolSize 核心线程数 49 | * maximumPoolSize 最大线程数 50 | * keepAliveTime 线程存活时间 51 | * unit keepAliveTime的时间单位,有日,小时,分钟,秒等等 52 | * workQueue 工作队列 53 | * threadFactory 线程工厂,用于创建线程 54 | * handler 饱和策略 55 | */ 56 | public ThreadPoolExecutor(int corePoolSize, 57 | int maximumPoolSize, 58 | long keepAliveTime, 59 | TimeUnit unit, 60 | BlockingQueue workQueue, 61 | ThreadFactory threadFactory, 62 | RejectedExecutionHandler handler) { 63 | //省略... 64 | } 65 | ``` 66 | 67 | 那么这7个参数,在线程池工作时,起到什么作用呢?直接看一张图就明白了。 68 | 69 | ![](https://static.lovebilibili.com/ThreadPool.png) 70 | 71 | 这里有两个参数需要讲解一下,工作队列workQueue和饱和策略handler。 72 | 73 | 工作队列的类是BlockingQueue,是一个接口,我们先看看类图,看一下有哪些子类可以使用。 74 | 75 | ![](https://static.lovebilibili.com/BlockingQueue.png) 76 | 77 | 可以看到有很多实现的子类,功能也各有不同。下面讲几个有代表性的。 78 | 79 | **DelayQueue**是无界的队列,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。 80 | 81 | **LinkedBlockingDeque**是基于双向链表实现的双向并发阻塞队列,该阻塞队列同时支持FIFO和FILO两种操作方式,即可以从队列的头和尾同时操作(添加或删除);并且该阻塞队列是支持线程安全。可以指定队列的容量,如果不指定默认容量大小是`Integer.MAX_VALUE`。 82 | 83 | **ArrayBlockingQueue**是基于数组实现的有界阻塞队列,此队列按先进先出的原则对元素进行排序。新元素插入到队列的尾部,获取元素的操作则从队列的头部进行。 84 | 85 | **PriorityBlockingQueue**是带优先级的无界阻塞队列,每次出队都返回优先级最高或者最低的元素(规则可以通过实现Comparable接口自己制定),内部是使用平衡二叉树实现的,遍历不保证有序。 86 | 87 | 饱和策略只要看RejectedExecutionHandler接口,以及其实现子类。 88 | 89 | ![](https://static.lovebilibili.com/RejectedExecutionHandler.png) 90 | 91 | 饱和策略主要有四种,如果要自定义饱和策略也很简单,实现RejectedExecutionHandler接口,重写rejectedExecution()方法即可。下面介绍JDK里的四种饱和策略。 92 | 93 | - AbortPolicy,直接抛出异常,简单粗暴。 94 | - CallerRunsPolicy,在任务被拒绝添加后,会调用当前线程池的所在的线程去执行被拒绝的任务。 95 | - DiscardPolicy,什么都不做,既不抛出异常,也不会执行。 96 | - DiscardOldestPolicy,当任务被拒绝添加时,会抛弃任务队列中最旧的任务(也就是最先加入队列的任务),再把这个新任务添加进去。 97 | 98 | ## 3.2 Executors 99 | 100 | Executors类提供了四种线程池,根据使用不同的参数去new ThreadPoolExecutor实现。简单介绍一下。 101 | 102 | 第一种是**newFixedThreadPool**,这是创建固定大小的线程池,核心线程数和最大线程数都设置相同的值,使用LinkedBlockingQueue作为工作队列,当corePoolSize满了之后就加入到LinkedBlockingQueue队列中。LinkedBlockingQueue默认大小为Integer.MAX_VALUE,所以会有OOM的风险。 103 | 104 | ```java 105 | public static ExecutorService newFixedThreadPool(int nThreads) { 106 | return new ThreadPoolExecutor(nThreads, nThreads, 107 | 0L, TimeUnit.MILLISECONDS, 108 | new LinkedBlockingQueue()); 109 | } 110 | ``` 111 | 112 | 第二种是**newSingleThreadExecutor**,创建线程数为1的线程池,并且使用了LinkedBlockingQueue,核心线程数和最大线程数都为1,满了就放入队列中,执行完了就从队列取一个。也就是创建了一个具有缓冲队列的单线程的线程池。跟上面的问题一样,队列的容量默认是Integer.MAX_VALUE,也会有OOM的风险。 113 | 114 | ```java 115 | public static ExecutorService newSingleThreadExecutor() { 116 | return new FinalizableDelegatedExecutorService 117 | (new ThreadPoolExecutor(1, 1, 118 | 0L, TimeUnit.MILLISECONDS, 119 | new LinkedBlockingQueue())); 120 | } 121 | ``` 122 | 123 | 第三种是**newCachedThreadPool**,创建可缓冲的线程池,没有大小限制。核心线程数是0,最大线程数是Integer.MAX_VALUE,所以当有新任务时,任务会放入SynchronousQueue队列中,SynchronousQueue只能存放大小为1,所以会立刻新起线程。如果在工作线程在指定时间(60秒)空闲,则会自动终止。 124 | 125 | ```java 126 | public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { 127 | return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 128 | 60L, TimeUnit.SECONDS, 129 | new SynchronousQueue(), 130 | threadFactory); 131 | } 132 | ``` 133 | 134 | 第四种是**newScheduledThreadPool**,支持定时及周期性任务执行的线程池。 135 | 136 | ```java 137 | public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { 138 | return new ScheduledThreadPoolExecutor(corePoolSize); 139 | } 140 | 141 | public ScheduledThreadPoolExecutor(int corePoolSize) { 142 | super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, 143 | new DelayedWorkQueue()); 144 | } 145 | ``` 146 | 147 | ## 3.3 使用规范 148 | 149 | 在阿里java开发规范中,是**强制**不允许使用Executors创建线程池,我们不妨看看。 150 | 151 | ![](https://static.lovebilibili.com/ThreadPool_1.png) 152 | 153 | 假如有人头铁不信,那我们写一段代码模拟一下。 154 | 155 | ```java 156 | public class ThreadTest { 157 | private static AtomicInteger num = new AtomicInteger(); 158 | public static void main(String[] args) throws Exception { 159 | //创建线程池 160 | ExecutorService executor = Executors.newCachedThreadPool(); 161 | while (true) { 162 | executor.execute(() -> { 163 | try { 164 | System.out.println("线程数:" + num.incrementAndGet()); 165 | Thread.sleep(10000); 166 | } catch (Exception e) { 167 | e.printStackTrace(); 168 | } 169 | }); 170 | } 171 | } 172 | } 173 | ``` 174 | 175 | 然后设置JVM的参数`-Xms5M -Xmx5M`,运行一小段时间,就会看到报错了。 176 | 177 | ![](https://static.lovebilibili.com/ThreadPool_2.png) 178 | 179 | 第二个问题是线程数的设置,设置多少线程数比较合适呢? 180 | 181 | 如果是**cpu密集型**的应用,cpu密集的意思是**执行的任务大部分时间是在做计算和逻辑判断**,这种情况显然不能设置太多的线程数,否则花在线程之间的切换时间就变多,效率就会变得低下。所以一般这种情况设置**线程数为cpu核数+1**即可。 182 | 183 | cpu核数可以通过`Runtime`获取。 184 | 185 | ```java 186 | Runtime.getRuntime().availableProcessors() 187 | ``` 188 | 189 | 如果是**IO密集型**的应用,IO密集的意思是**执行的任务需要执行大量的IO操作,比如网络IO,磁盘IO**,对CPU的使用率较低,因为在IO操作的特点需要等待,那么就可以把CPU切换到其他线程。所以可以设置**线程数为CPU核数的两倍+1**。 190 | 191 | # 絮叨 192 | 193 | 经过学习之后,我们就要养成使用多线程不能直接new一个Thread,然后start(),要有使用线程池的意识。其次要理解线程池参数的意义,根据实际情况去设置。 194 | 195 | 并发编程往往是实际开发中比较容易出问题,希望看完这篇文章能减少一些不必要的错误。 196 | 197 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 198 | 199 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 200 | 201 | ![](https://static.lovebilibili.com/dashacha.png) 202 | 203 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /并发编程的艺术/并发编程里的悲观锁和乐观锁.md: -------------------------------------------------------------------------------- 1 | # 思维导图 2 | 3 | ![](https://static.lovebilibili.com/bllock_swdt.png) 4 | 5 | > **文章已收录Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 6 | 7 | # 悲观锁 8 | 9 | 悲观锁是平时开发中经常用到的一种锁,比如`ReentrantLock`和`synchronized`等就是这种思想的体现,它总是假设别的线程在拿线程的时候都会修改数据,所以每次拿到数据的时候都会上锁,这样别的线程想拿这个数据就会被阻塞。如图所示: 10 | 11 | ![](https://static.lovebilibili.com/bllock_1.png) 12 | 13 | `synchronized`是悲观锁的一种实现,一般我们都会有这样使用: 14 | 15 | ```java 16 | private static Object monitor = new Object(); 17 | 18 | public static void main(String[] args) throws Exception { 19 | //锁一段代码块 20 | synchronized (monitor){ 21 | 22 | } 23 | } 24 | //锁实例方法,锁对象是this,即该类实例本身 25 | public synchronized void doSome(){ 26 | 27 | } 28 | //锁静态方法,锁对象是该类,即XXX.class 29 | public synchronized static void add(){ 30 | 31 | } 32 | ``` 33 | 34 | 我们以最简单的同步代码块来分析,其实就是将synchronized作用于一个给定的实例对象monitor,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有monitor实例对象锁,**如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行synchronized内包裹的代码块**。 35 | 36 | 从上面的分析中可以看出,悲观锁是独占和排他的,只要操作资源都会对资源进行加锁。假设**读多写少**的情况下,使用悲观锁的效果就不是很好。这时就引出了接下来要讲的乐观锁。 37 | 38 | # 乐观锁 39 | 40 | 乐观锁,顾名思义它总是假设最好的情况,线程每次去拿数据时都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。如图所示: 41 | 42 | ![](https://static.lovebilibili.com/bllock_2.png) 43 | 44 | 一般乐观锁在java中是通过无锁编程实现的,最常见的就是CAS算法,比如Java并发包中的原子类的递增操作就是通过CAS算法实现的。 45 | 46 | CAS算法,其实就是Compare And Swap(比较与交换)的意思。目的就是将内存的值更新为需要的值,但是有个条件,内存值必须与期待的原内存值相同。展开来说,我们有三个变量,内存值M,期望的内存值E,更新值U,**只有当M==E时,才会将M更新为U**。 47 | 48 | CAS算法实现的乐观锁在很多地方有应用,比如并发包的原子类AtomicInteger类。在自增的时候就使用到CAS算法。 49 | 50 | ```java 51 | public final int getAndIncrement() { 52 | return unsafe.getAndAddInt(this, valueOffset, 1); 53 | } 54 | 55 | //var1 是this指针 56 | //var2 是偏移量 57 | //var4 是自增量 58 | public final int getAndAddInt(Object var1, long var2, int var4) { 59 | int var5; 60 | do { 61 | //获取内存,称之为期待的内存值E 62 | var5 = this.getIntVolatile(var1, var2); 63 | //var5 + var4的结果是更新值U 64 | //这里使用JNI方法,每个线程将自己内存中的内存值M与var5期望值比较, 65 | //如果相同则更新为var5 + var4,返回true跳出循环。 66 | //如果不相同,则把内存值M更新为最新的内存值,然后自旋,直到更新成功为止 67 | } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); 68 | //返回更新后的值 69 | return var5; 70 | } 71 | 72 | public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); 73 | ``` 74 | 75 | 所以可以看出CAS算法其实是无锁的。好处是在读多写少的情况下,性能是比较好的。那么CAS算法的缺点其实也是很明显的。 76 | 77 | - **ABA问题**。线程C将内存值A改成了B后,又改成了A,而线程D会认为内存值A没有改变过,这个问题就称为ABA问题。解决办法很简单,在变量前面加上版本号,每次变量更新的时候变量的**版本号都`+1`**,即`A->B->A`就变成了`1A->2B->3A`。 78 | - 在写多读少的情况下,也就是频繁更新数据,那么会导致其他线程经常更新失败,那么就会进入自旋,自旋时会**占用CPU资源**。如果资源竞争激烈,多线程自旋的时间长,导致**消耗资源**。 79 | 80 | # 使用场景 81 | 82 | 在**读多写少的场景**下,更新时很少发生冲突,**使用乐观锁**,减少了上锁和释放锁的开销,可以有效地提升系统的性能。 83 | 84 | 相反,在**写多读少的场景**下,如果使用乐观锁会导致更新时经常产生冲突,然后线程会循环重试,这样会增大CPU的消耗。在这种情况下,**建议可以使用悲观锁**。 85 | 86 | # 总结 87 | 88 | 在日常的开发中,悲观锁和乐观锁应该是见得最多,用得最多的锁,比如最常见的`synchronized`和`ReentrantLock`是悲观锁,并发包中的原子类和ConcurrentHashMap则用了乐观锁。锁的实现并不复杂,关键是搞懂这两种锁的思想,这样才能在合适的地方使用合适的锁。 89 | 90 | 这篇文章就讲到这里了,希望看完后能有所收获,感谢你的阅读。 91 | 92 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 93 | 94 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 95 | 96 | ![](https://static.lovebilibili.com/dashacha.png) 97 | 98 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! 99 | 100 | -------------------------------------------------------------------------------- /并发编程的艺术/死磕synchronized关键字底层原理.md: -------------------------------------------------------------------------------- 1 | > **文章已收录Github精选,欢迎Star**:[https://github.com/yehongzhi](https://github.com/yehongzhi/learningSummary) 2 | 3 | # 前言 4 | 5 | 作为Java程序员,我们都知道在多线程的情况下,为了保证线程安全,经常会使用synchronized和Lock锁。Lock锁之前写过一篇[《不得不学的AQS》](https://mp.weixin.qq.com/s/FjLl9POXHqI8ca9EQHCCCw),已经详细讲解过Lock锁的底层原理。这次我们讲一下日常开发中常用的关键字synchronized,想要用得好,底层原理必须要搞明白。 6 | 7 | synchronized是JDK自带的一个关键字,在JDK1.5之前是一个重量级锁,所以从性能上考虑大部分人会选择Lock锁,不过毕竟是JDK自带的关键字,所以在JDK1.6后对它进行优化,引入了偏向锁,轻量级锁,自旋锁等概念。 8 | 9 | # 一、synchronized的使用方式 10 | 11 | 在语法上,要使用synchronized关键字,需要把任意一个非null对象作为"锁"对象,也就是需要一个**对象监视器(Object Monitor)**。总的来说有三种用法: 12 | 13 | ## 1.1 作用在实例方法 14 | 15 | 修饰实例方法,相当于对当前实例对象this加锁,this作为对象监视器。 16 | 17 | ```java 18 | public synchronized void hello(){ 19 | System.out.println("hello world"); 20 | } 21 | ``` 22 | 23 | ## 1.2 作用在静态方法 24 | 25 | 修饰静态方法,相当于对当前类的Class对象加锁,当前类的Class对象作为对象监视器。 26 | 27 | ```java 28 | public synchronized static void helloStatic(){ 29 | System.out.println("hello world static"); 30 | } 31 | ``` 32 | 33 | ## 1.3 修饰代码块 34 | 35 | 指定加锁对象,对给定对象加锁,括号括起来的对象就是对象监视器。 36 | 37 | ```java 38 | public void test(){ 39 | SynchronizedTest test = new SynchronizedTest(); 40 | synchronized (test){ 41 | System.out.println("hello world"); 42 | } 43 | } 44 | ``` 45 | 46 | # 二、synchronized锁的原理 47 | 48 | 在讲原理前,我们先讲一下Java对象的构成。在JVM中,对象在内存中分为三块区域:对象头,实例数据和对齐填充。如图所示: 49 | 50 | ![](https://static.lovebilibili.com/synchronized_01.png) 51 | 52 | **对象头**: 53 | 54 | - Mark Word,用于存储对象自身运行时的数据,如哈希码(Hash Code),GC分代年龄,锁状态标志,偏向线程ID、偏向时间戳等信息,它会根据对象的状态复用自己的存储空间。它是**实现轻量级锁和偏向锁的关键**。 55 | - 类型指针,对象会指向它的类的元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例。 56 | - Array length,如果对象是一个数组,还必须记录数组长度的数据。 57 | 58 | **实例数据**: 59 | 60 | - 存放类的属性数据信息,包括父类的属性信息。 61 | 62 | **对齐填充**: 63 | 64 | - 由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。 65 | 66 | ## 2.1 同步代码块原理 67 | 68 | 为了看底层实现原理,使用`javap -v xxx.class`命令进行反编译。 69 | 70 | ![](https://static.lovebilibili.com/synchronized_02.png) 71 | 72 | 这是使用同步代码块被标志的地方就是刚刚提到的对象头,它会关联一个monitor对象,也就是括号括起来的对象。 73 | 74 | 1、**monitorenter**,如果当前monitor的进入数为0时,线程就会进入monitor,并且把进入数+1,那么该线程就是monitor的拥有者(owner)。 75 | 76 | 2、如果该线程已经是monitor的拥有者,又重新进入,就会把进入数再次+1。也就是可重入的。 77 | 78 | 3、**monitorexit**,执行monitorexit的线程必须是monitor的拥有者,指令执行后,monitor的进入数减1,如果减1后进入数为0,则该线程会退出monitor。其他被阻塞的线程就可以尝试去获取monitor的所有权。 79 | 80 | > monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁; 81 | 82 | 总的来说,synchronized的底层原理是通过monitor对象来完成的。 83 | 84 | ## 2.2 同步方法原理 85 | 86 | 比如说使用synchronized修饰的实例方法。 87 | 88 | ```java 89 | public synchronized void hello(){ 90 | System.out.println("hello world"); 91 | } 92 | ``` 93 | 94 | 同理使用`javap -v`反编译。 95 | 96 | ![](https://static.lovebilibili.com/synchronized_03.png) 97 | 98 | 可以看到多了一个标志位**ACC_SYNCHRONIZED**,作用就是一旦执行到这个方法时,就会先判断是否有标志位,如果有这个标志位,就会先尝试获取monitor,获取成功才能执行方法,方法执行完成后再释放monitor。**在方法执行期间,其他线程都无法获取同一个monitor**。归根结底还是对monitor对象的争夺,只是同步方法是一种隐式的方式来实现。 99 | 100 | ## 2.3 Monitor 101 | 102 | 上面经常提到monitor,它内置在每一个对象中,任何一个对象都有一个monitor与之关联,synchronized在JVM里的实现就是基于进入和退出monitor来实现的,底层则是通过成对的MonitorEnter和MonitorExit指令来实现,因此每一个Java对象都有成为Monitor的潜质。所以我们可以理解monitor是一个同步工具。 103 | 104 | # 三、synchronized锁的优化 105 | 106 | 前面讲过JDK1.5之前,synchronized是属于重量级锁,重量级需要依赖于底层操作系统的Mutex Lock实现,然后操作系统需要切换用户态和内核态,这种切换的消耗非常大,所以性能相对来说并不好。 107 | 108 | 既然我们都知道性能不好,JDK的开发人员肯定也是知道的,于是在JDK1.6后开始对synchronized进行优化,增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。锁的等级从无锁,偏向锁,轻量级锁,重量级锁逐步升级,并且是单向的,不会出现锁的降级。 109 | 110 | ## 3.1 自适应性自旋锁 111 | 112 | 在说自适应自旋锁之前,先讲自旋锁。上面已经讲过,当线程没有获得monitor对象的所有权时,就会进入阻塞,当持有锁的线程释放了锁,当前线程才可以再去竞争锁,但是如果按照这样的规则,就会浪费大量的性能在阻塞和唤醒的切换上,特别是线程占用锁的时间很短的话。 113 | 114 | 为了避免阻塞和唤醒的切换,在没有获得锁的时候就不进入阻塞,而是不断地循环检测锁是否被释放,这就是自旋。在占用锁的时间短的情况下,自旋锁表现的性能是很高的。 115 | 116 | 但是又有问题,由于线程是一直在循环检测锁的状态,就会占用cpu资源,如果线程占用锁的时间比较长,那么自旋的次数就会变多,占用cpu时间变长导致性能变差,当然我们也可以通过参数`-XX:PreBlockSpin`设置自旋锁的自旋次数,当自旋一定的次数(时间)后就挂起,但是设置的自旋次数是多少比较合适呢? 117 | 118 | 如果设置次数少了或者多了都会导致性能受到影响,而且占用锁的时间在业务高峰期和正常时期也有区别,所以在JDK1.6引入了自适应性自旋锁。 119 | 120 | 自适应性自旋锁的意思是,自旋的次数不是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。 121 | 122 | 表现是如果此次自旋成功了,很有可能下一次也能成功,于是允许自旋的次数就会更多,反过来说,如果很少有线程能够自旋成功,很有可能下一次也是失败,则自旋次数就更少。这样能最大化利用资源,随着程序运行和性能监控信息的不断完善,虚拟机对锁的状况预测会越来越准确,也就变得越来越智能。 123 | 124 | ## 3.2 锁消除 125 | 126 | 锁消除是一种锁的优化策略,这种优化更加彻底,在JVM编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。这种优化策略可以消除没有必要的锁,节省毫无意义的请求锁时间。比如StringBuffer的append()方法,就是使用synchronized进行加锁的。 127 | 128 | ```java 129 | public synchronized StringBuffer append(String str) { 130 | toStringCache = null; 131 | super.append(str); 132 | return this; 133 | } 134 | ``` 135 | 136 | 如果在实例方法中StringBuffer作为局部变量使用append()方法,StringBuffer是不可能存在共享资源竞争的,因此会自动将其锁消除。例如: 137 | 138 | ```java 139 | public String add(String s1, String s2) { 140 | //sb属于不可能共享的资源,JVM会自动消除内部的锁 141 | StringBuffer sb = new StringBuffer(); 142 | sb.append(s1).append(s2); 143 | return sb.toString(); 144 | } 145 | ``` 146 | 147 | ## 3.3 锁粗化 148 | 149 | 如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。意思是将多个连续加锁、解锁的操作连接在一起,扩展成为一个范围更大的锁。 150 | 151 | ## 3.4 偏向锁 152 | 153 | 偏向锁是JDK1.6引入的一个重要的概念,JDK的开发人员经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。也就是说在很多时候我们是假设有多线程的场景,但是实际上却是单线程的。所以偏向锁是在单线程执行代码块时使用的机制。 154 | 155 | 原理是什么呢,我们前面提到锁的争夺实际上是Monitor对象的争夺,还有每个对象都有一个对象头,对象头是由Mark Word和Klass pointer 组成的。一旦有线程持有了这个锁对象,标志位修改为1,就进入**偏向模式**,同时会把这个**线程的ID记录在对象的Mark Word中**,当同一个线程再次进入时,就不再进行同步操作,这样就省去了大量的锁申请的操作,从而提高了性能。 156 | 157 | 一旦有多个线程开始竞争锁的话呢?那么偏向锁并不会一下子升级为重量级锁,而是先升级为轻量级锁。 158 | 159 | ## 3.5 轻量级锁 160 | 161 | 如果获取偏向锁失败,也就是有多个线程竞争锁的话,就会升级为JDK1.6引入的轻量级锁,Mark Word 的结构也变为轻量级锁的结构。 162 | 163 | 执行同步代码块之前,JVM会在线程的栈帧中创建一个锁记录(Lock Record),并将Mark Word拷贝复制到锁记录中。然后尝试通过CAS操作将Mark Word中的锁记录的指针,指向创建的Lock Record。如果成功表示获取锁状态成功,如果失败,则进入自旋获取锁状态。 164 | 165 | 自旋锁的原理在上面已经讲过了,如果自旋获取锁也失败了,则升级为重量级锁,也就是把线程阻塞起来,等待唤醒。 166 | 167 | ## 3.6 重量级锁 168 | 169 | 重量级锁就是一个悲观锁了,但是其实不是最坏的锁,因为升级到重量级锁,是因为线程占用锁的时间长(自旋获取失败),锁竞争激烈的场景,在这种情况下,让线程进入阻塞状态,进入阻塞队列,能减少cpu消耗。所以说在不同的场景使用最佳的解决方案才是最好的技术。synchronized在不同的场景会自动选择不同的锁,这样一个升级锁的策略就体现出了这点。 170 | 171 | ## 3.7 小结 172 | 173 | 偏向锁:适用于单线程执行。 174 | 175 | 轻量级锁:适用于锁竞争较不激烈的情况。 176 | 177 | 重量级锁:适用于锁竞争激烈的情况。 178 | 179 | # 四、Lock锁与synchronized的区别 180 | 181 | 我们看一下他们的区别: 182 | 183 | - synchronized是Java语法的一个关键字,加锁的过程是在JVM底层进行。Lock是一个类,是JDK应用层面的,在JUC包里有丰富的API。 184 | - synchronized在加锁和解锁操作上都是自动完成的,Lock锁需要我们手动加锁和解锁。 185 | - Lock锁有丰富的API能知道线程是否获取锁成功,而synchronized不能。 186 | - synchronized能修饰方法和代码块,Lock锁只能锁住代码块。 187 | - Lock锁有丰富的API,可根据不同的场景,在使用上更加灵活。 188 | - synchronized是非公平锁,而Lock锁既有非公平锁也有公平锁,可以由开发者通过参数控制。 189 | 190 | 个人觉得在锁竞争不是很激烈的场景,使用synchronized,语义清晰,实现简单,JDK1.6后引入了偏向锁,轻量级锁等概念后,性能也能保证。而在锁竞争激烈,复杂的场景下,则使用Lock锁会更灵活一点,性能也较稳定。 191 | 192 | # 总结 193 | 194 | 学习synchronized关键字的底层原理不是钻牛角尖,其实是从底层原理上知道了synchronized在什么场景使用会有什么样的效果,我们都知道没有最好的技术,只有最适合的技术,所以在学完之后,希望对大家有所帮助,写出更加高效的代码。所谓不积跬步无以至千里,一步一个脚印,哪怕现在还是菜鸟,总有一天也会成为雄鹰。 195 | 196 | 那么这篇文章就写到这里了,感谢大家的阅读,希望看完后能有所收获! 197 | 198 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 199 | 200 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 201 | 202 | ![](https://static.lovebilibili.com/dashacha.png) 203 | 204 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /并发编程的艺术/面试官问我什么是JMM.md: -------------------------------------------------------------------------------- 1 | # 思维导图 2 | 3 | ![](https://static.lovebilibili.com/JMM_9.png) 4 | 5 | > **文章已收录Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 6 | 7 | # 面试官:讲讲什么是JMM 8 | 9 | 你要是整这个我可就不困了。 10 | 11 | ![](https://static.lovebilibili.com/nishuozhegejiubukunle.jpg) 12 | 13 | JMM就是Java内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以**java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。** 14 | 15 | Java内存模型规定**所有的变量都存储在主内存**中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,**线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行**。**线程不能直接读写主内存中的变量**。 16 | 17 | 不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。 18 | 19 | 如果听起来抽象的话,我可以画张图给你看看,会直观一点: 20 | 21 | ![](https://static.lovebilibili.com/JMM_1.png) 22 | 23 | 每个线程的工作内存都是独立的,线程操作数据只能在工作内存中进行,然后刷回到主存。这是 Java 内存模型定义的线程基本工作方式。 24 | 25 | 温馨提醒一下,这里有些人会把Java内存模型误解为**Java内存结构**,然后答到堆,栈,GC垃圾回收,最后和面试官想问的问题相差甚远。**实际上一般问到Java内存模型都是想问多线程,Java并发相关的问题**。 26 | 27 | # 面试官:那JMM定义了什么 28 | 29 | 这个简单,整个Java内存模型实际上是围绕着三个特征建立起来的。分别是:原子性,可见性,有序性。这三个特征可谓是整个Java并发的基础。 30 | 31 | ## 原子性 32 | 33 | 原子性指的是一个操作是不可分割,不可中断的,一个线程在执行时不会被其他线程干扰。 34 | 35 | **面试官拿笔写了段代码,下面这几句代码能保证原子性吗**? 36 | 37 | ```java 38 | int i = 2; 39 | int j = i; 40 | i++; 41 | i = i + 1; 42 | ``` 43 | 44 | 第一句是基本类型赋值操作,必定是原子性操作。 45 | 46 | 第二句先读取i的值,再赋值到j,两步操作,不能保证原子性。 47 | 48 | 第三和第四句其实是等效的,先读取i的值,再+1,最后赋值到i,三步操作了,不能保证原子性。 49 | 50 | JMM只能保证基本的原子性,如果要保证一个代码块的原子性,提供了monitorenter 和 moniterexit 两个字节码指令,也就是 synchronized 关键字。因此在 synchronized 块之间的操作都是原子性的。 51 | 52 | ## 可见性 53 | 54 | 可见性指当一个线程修改共享变量的值,其他线程能够立即知道被修改了。Java是利用volatile关键字来提供可见性的。 当变量被volatile修饰时,这个变量被修改后会立刻刷新到主内存,当其它线程需要读取该变量时,会去主内存中读取新值。而普通变量则不能保证这一点。 55 | 56 | 除了volatile关键字之外,final和synchronized也能实现可见性。 57 | 58 | synchronized的原理是,在执行完,进入unlock之前,必须将共享变量同步到主内存中。 59 | 60 | final修饰的字段,一旦初始化完成,如果没有对象逸出(指对象为初始化完成就可以被别的线程使用),那么对于其他线程都是可见的。 61 | 62 | ## 有序性 63 | 64 | 在Java中,可以使用synchronized或者volatile保证多线程之间操作的有序性。实现原理有些区别: 65 | 66 | volatile关键字是使用内存屏障达到禁止指令重排序,以保证有序性。 67 | 68 | synchronized的原理是,一个线程lock之后,必须unlock后,其他线程才可以重新lock,使得被synchronized包住的代码块在多线程之间是串行执行的。 69 | 70 | # 面试官:给我讲一下八种内存交互操作吧 71 | 72 | 好的,面试官,内存交互操作有8种,我画张图给你看吧: 73 | 74 | ![](https://static.lovebilibili.com/JMM_2.png) 75 | 76 | - lock(锁定),作用于**主内存**中的变量,把变量标识为线程独占的状态。 77 | - read(读取),作用于**主内存**的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的load操作使用。 78 | - load(加载),作用于**工作内存**的变量,把read操作主存的变量放入到工作内存的变量副本中。 79 | - use(使用),作用于**工作内存**的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。 80 | - assign(赋值),作用于**工作内存**的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。 81 | - store(存储),作用于**工作内存**的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。 82 | - write(写入):作用于**主内存**中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。 83 | - unlock(解锁):作用于**主内存**的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。 84 | 85 | 我再补充一下JMM对8种内存交互操作制定的规则吧: 86 | 87 | - 不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。 88 | - 不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。 89 | - 不允许线程将没有assign的数据从工作内存同步到主内存。 90 | - 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。 91 | - 一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。 92 | - 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。 93 | - 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。 94 | - 一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。 95 | 96 | # 面试官:讲一下volatile关键字吧 97 | 98 | 内心:这可以重头戏呀,可不能出岔子~ 99 | 100 | 很多并发编程都使用了volatile关键字,主要的作用包括两点: 101 | 102 | 1. **保证线程间变量的可见性。** 103 | 2. **禁止CPU进行指令重排序。** 104 | 105 | ## 可见性 106 | 107 | volatile修饰的变量,当一个线程改变了该变量的值,其他线程是立即可见的。普通变量则需要重新读取才能获得最新值。 108 | 109 | volatile保证可见性的流程大概就是这个一个过程: 110 | 111 | ![](https://static.lovebilibili.com/JMM_3.png) 112 | 113 | ## volatile一定能保证线程安全吗 114 | 115 | 先说结论吧,volatile不能一定能保证线程安全。 116 | 117 | 怎么证明呢,我们看下面一段代码的运行结果就知道了: 118 | 119 | ```java 120 | /** 121 | * @author Ye Hongzhi 公众号:java技术爱好者 122 | **/ 123 | public class VolatileTest extends Thread { 124 | 125 | private static volatile int count = 0; 126 | 127 | public static void main(String[] args) throws Exception { 128 | Vector threads = new Vector<>(); 129 | for (int i = 0; i < 100; i++) { 130 | VolatileTest thread = new VolatileTest(); 131 | threads.add(thread); 132 | thread.start(); 133 | } 134 | //等待子线程全部完成 135 | for (Thread thread : threads) { 136 | thread.join(); 137 | } 138 | //输出结果,正确结果应该是1000,实际却是984 139 | System.out.println(count);//984 140 | } 141 | 142 | @Override 143 | public void run() { 144 | for (int i = 0; i < 10; i++) { 145 | try { 146 | //休眠500毫秒 147 | Thread.sleep(500); 148 | } catch (Exception e) { 149 | e.printStackTrace(); 150 | } 151 | count++; 152 | } 153 | } 154 | } 155 | ``` 156 | 157 | 为什么volatile不能保证线程安全? 158 | 159 | 很简单呀,可见性不能保证操作的原子性,前面说过了count++不是原子性操作,会当做三步,先读取count的值,然后+1,最后赋值回去count变量。需要保证线程安全的话,需要使用synchronized关键字或者lock锁,给count++这段代码上锁: 160 | 161 | ```java 162 | private static synchronized void add() { 163 | count++; 164 | } 165 | ``` 166 | 167 | ## 禁止指令重排序 168 | 169 | 首先要讲一下as-if-serial语义,不管怎么重排序,(单线程)程序的执行结果不能被改变。 170 | 171 | 为了使指令更加符合CPU的执行特性,最大限度的发挥机器的性能,提高程序的执行效率,只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做**指令的重排序**。 172 | 173 | 重排序的种类分为三种,分别是:编译器重排序,指令级并行的重排序,内存系统重排序。整个过程如下所示: 174 | 175 | ![](https://static.lovebilibili.com/JMM_5.png) 176 | 177 | 指令重排序在单线程是没有问题的,不会影响执行结果,而且还提高了性能。但是在多线程的环境下就不能保证一定不会影响执行结果了。 178 | 179 | **所以在多线程环境下,就需要禁止指令重排序**。 180 | 181 | volatile关键字禁止指令重排序有两层意思: 182 | 183 | - 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。 184 | 185 | - 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。 186 | 187 | 下面举个例子: 188 | 189 | ```java 190 | private static int a;//非volatile修饰变量 191 | private static int b;//非volatile修饰变量 192 | private static volatile int k;//volatile修饰变量 193 | 194 | private void hello() { 195 | a = 1; //语句1 196 | b = 2; //语句2 197 | k = 3; //语句3 198 | a = 4; //语句4 199 | b = 5; //语句5 200 | //以下省略... 201 | } 202 | ``` 203 | 204 | 变量a,b是非volatile修饰的变量,k则使用volatile修饰。所以语句3不能放在语句1、2前,也不能放在语句4、5后。但是语句1、2的顺序是不能保证的,同理,语句4、5也不能保证顺序。 205 | 206 | 并且,执行到语句3的时候,语句1,2是肯定执行完毕的,而且语句1,2的执行结果对于语句3,4,5是可见的。 207 | 208 | ## volatile禁止指令重排序的原理是什么 209 | 210 | 首先要讲一下内存屏障,内存屏障可以分为以下几类: 211 | 212 | - LoadLoad 屏障:对于这样的语句Load1,LoadLoad,Load2。在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。 213 | 214 | - StoreStore屏障:对于这样的语句Store1, StoreStore, Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。 215 | - LoadStore 屏障:对于这样的语句Load1, LoadStore,Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。 216 | - StoreLoad 屏障:对于这样的语句Store1, StoreLoad,Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。 217 | 218 | 在每个volatile读操作后插入LoadLoad屏障,在读操作后插入LoadStore屏障。 219 | 220 | ![](https://static.lovebilibili.com/JMM_6.png) 221 | 222 | 在每个volatile写操作的前面插入一个StoreStore屏障,后面插入一个SotreLoad屏障。 223 | 224 | ![](https://static.lovebilibili.com/JMM_8.png) 225 | 226 | 大概的原理就是这样。 227 | 228 | 面试官:讲得还不错,基本上都讲到了,时间也不早了,今天的面试就到这吧,回去等通知吧~ 229 | 230 | 啊?就这? 231 | 232 | 233 | 234 | # 总结 235 | 236 | 要学习并发编程,java内存模型是第一站了。原子性,有序性,可见性这三大特征几乎贯穿了并发编程,可谓是基础知识。对于后面要深入学习起到铺垫作用。 237 | 238 | 在这篇文章中,如果面试的话,重点是Java内存模型(JMM)的工作方式,三大特征,还有volatile关键字。为什么喜欢问volatile关键字呢,因为**volatile关键字可以扯出很多东西,比如可见性,有序性,还有内存屏障等等**。可以一针见血地看出面试者的技术水平,毕竟面试官也想高效地筛选出符合要求的人才嘛。 239 | 240 | 上面所有例子的代码都上传Github了: 241 | 242 | > https://github.com/yehongzhi/mall 243 | 244 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 245 | 246 | **拒绝做一条咸鱼,我是一个努力让大家记住的程序员。我们下期再见!!!** 247 | 248 | ![](https://static.lovebilibili.com/dashacha.png) 249 | 250 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! 251 | 252 | -------------------------------------------------------------------------------- /必学的优秀技术框架/SpringBoot启动类启动流程.md: -------------------------------------------------------------------------------- 1 | # 思维导图 2 | 3 | ![](https://static.lovebilibili.com/SpringBoot_7.png) 4 | 5 | > **文章已收录Github精选,欢迎Star**:https://github.com/yehongzhi/learningSummary 6 | 7 | # 前言 8 | 9 | SpringBoot一开始最让我印象深刻的就是通过一个启动类就能启动应用。在SpringBoot以前,启动应用虽然也不麻烦,但是还是有点繁琐,要打包成war包,又要配置tomcat,tomcat又有一个server.xml文件去配置。 10 | 11 | 然而SpringBoot则内置了tomcat,通过启动类启动,配置也集中在一个application.yml中,简直不要太舒服。好奇心驱动,于是我很想搞清楚启动类的启动过程,那么开始吧。 12 | 13 | # 一、启动类 14 | 15 | 首先我们看最常见的启动类写法。 16 | 17 | ```java 18 | @SpringBootApplication 19 | public class SpringmvcApplication { 20 | public static void main(String[] args) { 21 | SpringApplication.run(SpringmvcApplication.class, args); 22 | } 23 | } 24 | ``` 25 | 26 | 把启动类分解一下,实际上就是两部分: 27 | 28 | - @SpringBootApplication注解 29 | - 一个main()方法,里面调用SpringApplication.run()方法。 30 | 31 | # 二、@SpringBootApplication 32 | 33 | 首先看@SpringBootApplication注解的源码。 34 | 35 | ```java 36 | @Target(ElementType.TYPE) 37 | @Retention(RetentionPolicy.RUNTIME) 38 | @Documented 39 | @Inherited 40 | @SpringBootConfiguration 41 | @EnableAutoConfiguration 42 | @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) 43 | public @interface SpringBootApplication { 44 | 45 | } 46 | ``` 47 | 48 | 很明显,@SpringBootApplication注解由三个注解组合而成,分别是: 49 | 50 | - @ComponentScan 51 | - @EnableAutoConfiguration 52 | - @SpringBootConfiguration 53 | 54 | ## 2.1 @ComponentScan 55 | 56 | ```java 57 | @Retention(RetentionPolicy.RUNTIME) 58 | @Target(ElementType.TYPE) 59 | @Documented 60 | @Repeatable(ComponentScans.class) 61 | public @interface ComponentScan { 62 | 63 | } 64 | ``` 65 | 66 | 这个注解的作用是**告诉Spring扫描哪个包下面类,加载符合条件的组件**(比如贴有@Component和@Repository等的类)或者bean的定义。 67 | 68 | 所以有一个basePackages的属性,如果默认不写,则从声明@ComponentScan所在类的package进行扫描。 69 | 70 | 所以**启动类最好定义在Root package下**,因为一般我们在使用@SpringBootApplication时,都不指定basePackages的。 71 | 72 | ## 2.2 @EnableAutoConfiguration 73 | 74 | ```java 75 | @Target(ElementType.TYPE) 76 | @Retention(RetentionPolicy.RUNTIME) 77 | @Documented 78 | @Inherited 79 | @AutoConfigurationPackage 80 | @Import(AutoConfigurationImportSelector.class) 81 | public @interface EnableAutoConfiguration { 82 | 83 | } 84 | ``` 85 | 86 | 这是一个复合注解,看起来很多注解,实际上关键在@Import注解,它会加载AutoConfigurationImportSelector类,然后就会触发这个类的selectImports()方法。根据返回的String数组(配置类的Class的名称)加载配置类。 87 | 88 | ```java 89 | public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, 90 | ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered { 91 | //返回的String[]数组,是配置类Class的类名 92 | @Override 93 | public String[] selectImports(AnnotationMetadata annotationMetadata) { 94 | if (!isEnabled(annotationMetadata)) { 95 | return NO_IMPORTS; 96 | } 97 | AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata); 98 | //返回配置类的类名 99 | return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); 100 | } 101 | } 102 | ``` 103 | 104 | 我们一直点下去,就可以找到最后的幕后英雄,就是SpringFactoriesLoader类,通过loadSpringFactories()方法加载META-INF/spring.factories中的配置类。 105 | 106 | ![](https://static.lovebilibili.com/SpringBoot_1.png) 107 | 108 | ![](https://static.lovebilibili.com/SpringBoot_2.png) 109 | 110 | 这里使用了spring.factories文件的方式加载配置类,提供了很好的扩展性。 111 | 112 | 所以@EnableAutoConfiguration注解的作用其实就是开启自动配置,自动配置主要则依靠这种加载方式来实现。 113 | 114 | ## 2.3 @SpringBootConfiguration 115 | 116 | **@SpringBootConfiguration继承自@Configuration,二者功能也一致**,标注当前类是配置类, 117 | 并会将当前类内声明的一个或多个以@Bean注解标记的方法的实例纳入到spring容器中,并且实例名就是方法名。 118 | 119 | ## 2.4 小结 120 | 121 | 我们在这里画张图把@SpringBootApplication注解包含的三个注解分别解释一下。 122 | 123 | ![](https://static.lovebilibili.com/SpringBoot_3.png) 124 | 125 | # 三、SpringApplication类 126 | 127 | 接下来讲main方法里执行的这句代码,这是SpringApplication类的静态方法run()。 128 | 129 | ```java 130 | //启动类的main方法 131 | public static void main(String[] args) { 132 | SpringApplication.run(SpringmvcApplication.class, args); 133 | } 134 | 135 | //启动类调的run方法 136 | public static ConfigurableApplicationContext run(Class primarySource, String... args) { 137 | //调的是下面的,参数是数组的run方法 138 | return run(new Class[] { primarySource }, args); 139 | } 140 | 141 | //和上面的方法区别在于第一个参数是一个数组 142 | public static ConfigurableApplicationContext run(Class[] primarySources, String[] args) { 143 | //实际上new一个SpringApplication实例,调的是一个实例方法run() 144 | return new SpringApplication(primarySources).run(args); 145 | } 146 | ``` 147 | 148 | 通过上面的源码,发现实际上最后调的并不是静态方法,而是实例方法,需要new一个SpringApplication实例,这个构造器还带有一个primarySources的参数。所以我们直接定位到构造器。 149 | 150 | ```java 151 | public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) { 152 | this.resourceLoader = resourceLoader; 153 | //断言primarySources不能为null,如果为null,抛出异常提示 154 | Assert.notNull(primarySources, "PrimarySources must not be null"); 155 | //启动类传入的Class 156 | this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); 157 | //判断当前项目类型,有三种:NONE、SERVLET、REACTIVE 158 | this.webApplicationType = WebApplicationType.deduceFromClasspath(); 159 | //设置ApplicationContextInitializer 160 | setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); 161 | //设置监听器 162 | setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); 163 | //判断主类,初始化入口类 164 | this.mainApplicationClass = deduceMainApplicationClass(); 165 | } 166 | 167 | //判断主类 168 | private Class deduceMainApplicationClass() { 169 | try { 170 | StackTraceElement[] stackTrace = new RuntimeException().getStackTrace(); 171 | for (StackTraceElement stackTraceElement : stackTrace) { 172 | if ("main".equals(stackTraceElement.getMethodName())) { 173 | return Class.forName(stackTraceElement.getClassName()); 174 | } 175 | } 176 | } 177 | catch (ClassNotFoundException ex) { 178 | // Swallow and continue 179 | } 180 | return null; 181 | } 182 | ``` 183 | 184 | 以上就是创建SpringApplication实例做的事情,下面用张图来表示一下。 185 | 186 | ![](https://static.lovebilibili.com/SpringBoot_4.png) 187 | 188 | 创建了SpringApplication实例之后,就完成了SpringApplication类的初始化工作,这个实例里包括监听器、初始化器,项目应用类型,启动类集合,类加载器。如图所示。 189 | 190 | ![](https://static.lovebilibili.com/SpringBoot_5.png) 191 | 192 | 得到SpringApplication实例后,接下来就调用实例方法run()。继续看。 193 | 194 | ```java 195 | public ConfigurableApplicationContext run(String... args) { 196 | //创建计时器 197 | StopWatch stopWatch = new StopWatch(); 198 | //开始计时 199 | stopWatch.start(); 200 | //定义上下文对象 201 | ConfigurableApplicationContext context = null; 202 | Collection exceptionReporters = new ArrayList<>(); 203 | //Headless模式设置 204 | configureHeadlessProperty(); 205 | //加载SpringApplicationRunListeners监听器 206 | SpringApplicationRunListeners listeners = getRunListeners(args); 207 | //发送ApplicationStartingEvent事件 208 | listeners.starting(); 209 | try { 210 | //封装ApplicationArguments对象 211 | ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); 212 | //配置环境模块 213 | ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); 214 | //根据环境信息配置要忽略的bean信息 215 | configureIgnoreBeanInfo(environment); 216 | //打印Banner标志 217 | Banner printedBanner = printBanner(environment); 218 | //创建ApplicationContext应用上下文 219 | context = createApplicationContext(); 220 | //加载SpringBootExceptionReporter 221 | exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, 222 | new Class[] { ConfigurableApplicationContext.class }, context); 223 | //ApplicationContext基本属性配置 224 | prepareContext(context, environment, listeners, applicationArguments, printedBanner); 225 | //刷新上下文 226 | refreshContext(context); 227 | //刷新后的操作,由子类去扩展 228 | afterRefresh(context, applicationArguments); 229 | //计时结束 230 | stopWatch.stop(); 231 | //打印日志 232 | if (this.logStartupInfo) { 233 | new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); 234 | } 235 | //发送ApplicationStartedEvent事件,标志spring容器已经刷新,此时所有的bean实例都已经加载完毕 236 | listeners.started(context); 237 | //查找容器中注册有CommandLineRunner或者ApplicationRunner的bean,遍历并执行run方法 238 | callRunners(context, applicationArguments); 239 | } 240 | catch (Throwable ex) { 241 | //发送ApplicationFailedEvent事件,标志SpringBoot启动失败 242 | handleRunFailure(context, ex, exceptionReporters, listeners); 243 | throw new IllegalStateException(ex); 244 | } 245 | 246 | try { 247 | //发送ApplicationReadyEvent事件,标志SpringApplication已经正在运行,即已经成功启动,可以接收服务请求。 248 | listeners.running(context); 249 | } 250 | catch (Throwable ex) { 251 | //报告异常,但是不发送任何事件 252 | handleRunFailure(context, ex, exceptionReporters, null); 253 | throw new IllegalStateException(ex); 254 | } 255 | return context; 256 | } 257 | ``` 258 | 259 | 结合注释和源码,其实很清晰了,为了加深印象,画张图看一下整个流程。 260 | 261 | ![](https://static.lovebilibili.com/SpringBoot_6.png) 262 | 263 | # 总结 264 | 265 | 表面启动类看起来就一个@SpringBootApplication注解,一个run()方法。其实是经过高度封装后的结果。我们可以从这个分析中学到很多东西。比如使用了spring.factories文件来完成自动配置,提高了扩展性。在启动时使用观察者模式,以事件发布的形式通知,降低耦合,易于扩展等等。 266 | 267 | 那么SpringBoot的启动类分析就讲到这里了,感谢大家的阅读。 268 | 269 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 270 | 271 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 272 | 273 | ![](https://static.lovebilibili.com/dashacha.png) 274 | 275 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /缓存服务/Redis-缓存雪崩、缓存击穿、缓存穿透.md: -------------------------------------------------------------------------------- 1 | > **文章已收录Github精选,欢迎Star**:[https://github.com/yehongzhi](https://github.com/yehongzhi/learningSummary) 2 | 3 | # 前言 4 | 5 | Redis作为目前使用最广泛的缓存,相信大家都不陌生。但是使用缓存并没有这么简单,还要考虑缓存雪崩,缓存击穿,缓存穿透的问题,什么是缓存雪崩,击穿,穿透呢,出现这些问题又怎么解决呢,接下来学习一下吧。 6 | 7 | # 缓存雪崩 8 | 9 | **什么是缓存雪崩?** 10 | 11 | 当某一个时刻出现大规模的缓存失效的情况,那么就会导致大量的请求直接打在数据库上面,导致数据库压力巨大,如果在高并发的情况下,可能瞬间就会导致数据库宕机。这时候如果运维马上又重启数据库,马上又会有新的流量把数据库打死。这就是缓存雪崩。 12 | 13 | ![](https://static.lovebilibili.com/redis_hc_1.png) 14 | 15 | **分析:** 16 | 17 | 造成缓存雪崩的关键在于在同一时间大规模的key失效。为什么会出现这个问题呢,有几种可能,第一种可能是Redis宕机,第二种可能是采用了相同的过期时间。搞清楚原因之后,那么有什么解决方案呢? 18 | 19 | **解决方案:** 20 | 21 | 1、在原有的失效时间上加上一个随机值,比如1-5分钟随机。这样就避免了因为采用相同的过期时间导致的缓存雪崩。 22 | 23 | 如果真的发生了缓存雪崩,有没有什么兜底的措施? 24 | 25 | 2、使用熔断机制。当流量到达一定的阈值时,就直接返回“系统拥挤”之类的提示,防止过多的请求打在数据库上。至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果。 26 | 27 | 3、提高数据库的容灾能力,可以使用分库分表,读写分离的策略。 28 | 29 | 4、为了防止Redis宕机导致缓存雪崩的问题,可以搭建Redis集群,提高Redis的容灾性。 30 | 31 | # 缓存击穿 32 | 33 | **什么是缓存击穿?** 34 | 35 | 其实跟缓存雪崩有点类似,缓存雪崩是大规模的key失效,而缓存击穿是一个热点的Key,有大并发集中对其进行访问,突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增。这种现象就叫做缓存击穿。 36 | 37 | **分析:** 38 | 39 | 关键在于某个热点的key失效了,导致大并发集中打在数据库上。所以要从两个方面解决,第一是否可以考虑热点key不设置过期时间,第二是否可以考虑降低打在数据库上的请求数量。 40 | 41 | **解决方案:** 42 | 43 | 1、上面说过了,如果业务允许的话,对于热点的key可以设置永不过期的key。 44 | 45 | 2、使用互斥锁。如果缓存失效的情况,只有拿到锁才可以查询数据库,降低了在同一时刻打在数据库上的请求,防止数据库打死。当然这样会导致系统的性能变差。 46 | 47 | # 缓存穿透 48 | 49 | **什么是缓存穿透?** 50 | 51 | 我们使用Redis大部分情况都是通过Key查询对应的值,假如发送的请求传进来的key是不存在Redis中的,那么就查不到缓存,查不到缓存就会去数据库查询。假如有大量这样的请求,这些请求像“穿透”了缓存一样直接打在数据库上,这种现象就叫做缓存穿透。 52 | 53 | **分析:** 54 | 55 | 关键在于在Redis查不到key值,这和缓存击穿有根本的区别,区别在于**缓存穿透的情况是传进来的key在Redis中是不存在的**。假如有黑客传进大量的不存在的key,那么大量的请求打在数据库上是很致命的问题,所以在日常开发中要对参数做好校验,一些非法的参数,不可能存在的key就直接返回错误提示,要对调用方保持这种“不信任”的心态。 56 | 57 | ![](https://static.lovebilibili.com/redis_hc_2.png) 58 | 59 | **解决方案:** 60 | 61 | 1、**把无效的Key存进Redis中**。如果Redis查不到数据,数据库也查不到,我们把这个Key值保存进Redis,设置value="null",当下次再通过这个Key查询时就不需要再查询数据库。这种处理方式肯定是有问题的,假如传进来的这个不存在的Key值每次都是随机的,那存进Redis也没有意义。 62 | 63 | 2、**使用布隆过滤器**。布隆过滤器的作用是某个 key 不存在,那么就一定不存在,它说某个 key 存在,那么很大可能是存在(存在一定的误判率)。于是我们可以在缓存之前再加一层布隆过滤器,在查询的时候先去布隆过滤器查询 key 是否存在,如果不存在就直接返回。 64 | 65 | ![](https://static.lovebilibili.com/redis_hc_3.png) 66 | 67 | # 总结 68 | 69 | 这三个问题在使用Redis的时候是肯定会遇到的,而且是非常致命性的问题,所以在日常开发中一定要注意,每次使用Redis时,都要对其保持严谨的态度。还有一个需要注意的是要做好熔断,一旦出现缓存雪崩,击穿,穿透这种情况,至少还有熔断机制保护数据库不会被打死。 70 | 71 | 那么这篇文章就讲到这里了,感谢大家的阅读,希望看完之后能有所收获。 72 | 73 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 74 | 75 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 76 | 77 | ![img](https://static.lovebilibili.com/dashacha.png) 78 | 79 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! 80 | -------------------------------------------------------------------------------- /缓存服务/基于Redis实现分布式锁.md: -------------------------------------------------------------------------------- 1 | > **文章已收录Github精选,欢迎Star**:[https://github.com/yehongzhi](https://github.com/yehongzhi/learningSummary) 2 | 3 | # 前言 4 | 5 | 如果在一个分布式系统中,我们从数据库中读取一个数据,然后修改保存,这种情况很容易遇到并发问题。因为读取和更新保存不是一个原子操作,在并发时就会导致数据的不正确。这种场景其实并不少见,比如电商秒杀活动,库存数量的更新就会遇到。如果是单机应用,直接使用本地锁就可以避免。如果是分布式应用,本地锁派不上用场,这时就需要引入分布式锁来解决。 6 | 7 | 由此可见分布式锁的目的其实很简单,就是**为了保证多台服务器在执行某一段代码时保证只有一台服务器执行**。 8 | 9 | 为了保证分布式锁的可用性,至少要确保锁的实现要同时满足以下几点: 10 | 11 | - 互斥性。在任何时刻,保证只有一个客户端持有锁。 12 | - 不能出现死锁。如果在一个客户端持有锁的期间,这个客户端崩溃了,也要保证后续的其他客户端可以上锁。 13 | - 保证上锁和解锁都是同一个客户端。 14 | 15 | 一般来说,实现分布式锁的方式有以下几种: 16 | 17 | - 使用MySQL,基于唯一索引。 18 | - 使用ZooKeeper,基于临时有序节点。 19 | - **使用Redis,基于setnx命令**。 20 | 21 | 本篇文章主要讲解Redis的实现方式。 22 | 23 | # 实现思路 24 | 25 | Redis实现分布式锁主要利用Redis的`setnx`命令。`setnx`是`SET if not exists`(如果不存在,则 SET)的简写。 26 | 27 | ```shell 28 | 127.0.0.1:6379> setnx lock value1 #在键lock不存在的情况下,将键key的值设置为value1 29 | (integer) 1 30 | 127.0.0.1:6379> setnx lock value2 #试图覆盖lock的值,返回0表示失败 31 | (integer) 0 32 | 127.0.0.1:6379> get lock #获取lock的值,验证没有被覆盖 33 | "value1" 34 | 127.0.0.1:6379> del lock #删除lock的值,删除成功 35 | (integer) 1 36 | 127.0.0.1:6379> setnx lock value2 #再使用setnx命令设置,返回0表示成功 37 | (integer) 1 38 | 127.0.0.1:6379> get lock #获取lock的值,验证设置成功 39 | "value2" 40 | ``` 41 | 42 | 上面这几个命令就是最基本的用来完成分布式锁的命令。 43 | 44 | 加锁:使用`setnx key value`命令,如果key不存在,设置value(加锁成功)。如果已经存在lock(也就是有客户端持有锁了),则设置失败(加锁失败)。 45 | 46 | 解锁:使用`del`命令,通过删除键值释放锁。释放锁之后,其他客户端可以通过`setnx`命令进行加锁。 47 | 48 | key的值可以根据业务设置,比如是用户中心使用的,可以命令为`USER_REDIS_LOCK`,value可以使用uuid保证唯一,用于标识加锁的客户端。保证加锁和解锁都是同一个客户端。 49 | 50 | 那么接下来就可以写一段很简单的加锁代码: 51 | 52 | ```java 53 | private static Jedis jedis = new Jedis("127.0.0.1"); 54 | 55 | private static final Long SUCCESS = 1L; 56 | 57 | /** 58 | * 加锁 59 | */ 60 | public boolean tryLock(String key, String requestId) { 61 | //使用setnx命令。 62 | //不存在则保存返回1,加锁成功。如果已经存在则返回0,加锁失败。 63 | return SUCCESS.equals(jedis.setnx(key, requestId)); 64 | } 65 | 66 | //删除key的lua脚本,先比较requestId是否相等,相等则删除 67 | private static final String DEL_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; 68 | 69 | /** 70 | * 解锁 71 | */ 72 | public boolean unLock(String key, String requestId) { 73 | //删除成功表示解锁成功 74 | Long result = (Long) jedis.eval(DEL_SCRIPT, Collections.singletonList(key), Collections.singletonList(requestId)); 75 | return SUCCESS.equals(result); 76 | } 77 | ``` 78 | 79 | ![](https://static.lovebilibili.com/redis_lock_1.png) 80 | 81 | ## 问题一 82 | 83 | 这仅仅满足上述的第一个条件和第三个条件,保证上锁和解锁都是同一个客户端,也保证只有一个客户端持有锁。 84 | 85 | 但是第二点没法保证,因为如果一个客户端持有锁的期间突然崩溃了,就会导致无法解锁,最后导致出现死锁的现象。 86 | 87 | ![](https://static.lovebilibili.com/redis_lock_2.png) 88 | 89 | 所以要有个超时的机制,在设置key的值时,需要加上有效时间,如果有效时间过期了,就会自动失效,就不会出现死锁。然后加锁的代码就会变成这样。 90 | 91 | ```java 92 | public boolean tryLock(String key, String requestId, int expireTime) { 93 | //使用jedis的api,保证原子性 94 | //NX 不存在则操作 EX 设置有效期,单位是秒 95 | String result = jedis.set(key, requestId, "NX", "EX", expireTime); 96 | //返回OK则表示加锁成功 97 | return "OK".equals(result); 98 | } 99 | ``` 100 | 101 | ![](https://static.lovebilibili.com/redis_lock_3.png) 102 | 103 | 但是聪明的同学肯定会问,有效时间设置多长,假如我的业务操作比有效时间长,我的业务代码还没执行完就自动给我解锁了,不就完蛋了吗。 104 | 105 | 这个问题就有点棘手了,在网上也有很多讨论,第一种解决方法就是靠程序员自己去把握,预估一下业务代码需要执行的时间,然后设置有效期时间比执行时间长一些,保证不会因为自动解锁影响到客户端业务代码的执行。 106 | 107 | 但是这并不是万全之策,比如网络抖动这种情况是无法预测的,也有可能导致业务代码执行的时间变长,所以并不安全。 108 | 109 | 有一种方法比较靠谱一点,就是给锁续期。在Redisson框架实现分布式锁的思路,就使用watchDog机制实现锁的续期。当加锁成功后,同时开启守护线程,默认有效期是30秒,每隔10秒就会给锁续期到30秒,只要持有锁的客户端没有宕机,就能保证一直持有锁,直到业务代码执行完毕由客户端自己解锁,如果宕机了自然就在有效期失效后自动解锁。 110 | 111 | ![](https://static.lovebilibili.com/redis_lock_4.png) 112 | 113 | ## 问题二 114 | 115 | 但是聪明的同学可能又会问,你这个锁只能加一次,不可重入。可重入锁意思是在外层使用锁之后,内层仍然可以使用,那么可重入锁的实现思路又是怎么样的呢? 116 | 117 | 在Redisson实现可重入锁的思路,使用Redis的哈希表存储可重入次数,当加锁成功后,使用`hset`命令,value(重入次数)则是1。 118 | 119 | ```java 120 | "if (redis.call('exists', KEYS[1]) == 0) then " + 121 | "redis.call('hset', KEYS[1], ARGV[2], 1); " + 122 | "redis.call('pexpire', KEYS[1], ARGV[1]); " + 123 | "return nil; " + 124 | "end; " 125 | ``` 126 | 127 | 如果同一个客户端再次加锁成功,则使用`hincrby`自增加一。 128 | 129 | ```java 130 | "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + 131 | "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + 132 | "redis.call('pexpire', KEYS[1], ARGV[1]); " + 133 | "return nil; " + 134 | "end; " + 135 | "return redis.call('pttl', KEYS[1]);" 136 | ``` 137 | 138 | ![](https://static.lovebilibili.com/redis_lock_6.png) 139 | 140 | 解锁时,先判断可重复次数是否大于0,大于0则减一,否则删除键值,释放锁资源。 141 | 142 | ```java 143 | protected RFuture unlockInnerAsync(long threadId) { 144 | return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, 145 | "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " + 146 | "return nil;" + 147 | "end; " + 148 | "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " + 149 | "if (counter > 0) then " + 150 | "redis.call('pexpire', KEYS[1], ARGV[2]); " + 151 | "return 0; " + 152 | "else " + 153 | "redis.call('del', KEYS[1]); " + 154 | "redis.call('publish', KEYS[2], ARGV[1]); " + 155 | "return 1; "+ 156 | "end; " + 157 | "return nil;", 158 | Arrays.asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId)); 159 | } 160 | ``` 161 | 162 | ![](https://static.lovebilibili.com/redis_lock_7.png) 163 | 164 | 为了保证操作原子性,加锁和解锁操作都是使用lua脚本执行。 165 | 166 | ## 问题三 167 | 168 | 上面的加锁方法是加锁后立即返回加锁结果,如果加锁失败的情况下,总不可能一直轮询尝试加锁,直到加锁成功为止,这样太过耗费性能。所以需要利用发布订阅的机制进行优化。 169 | 170 | 步骤如下: 171 | 172 | 当加锁失败后,订阅锁释放的消息,自身进入阻塞状态。 173 | 174 | 当持有锁的客户端释放锁的时候,发布锁释放的消息。 175 | 176 | 当进入阻塞等待的其他客户端收到锁释放的消息后,解除阻塞等待状态,再次尝试加锁。 177 | 178 | ![](https://static.lovebilibili.com/redis_lock_5.png) 179 | 180 | # 总结 181 | 182 | 以上的实现思路仅仅考虑在单机版Redis上,如果是集群版Redis需要考虑的问题还要再多一点。Redis由于他的高性能读写能力,所以在并发高的场景下使用Redis分布式锁会多一点。 183 | 184 | 问题一,二,三其实就是redis分布式锁不断改良发展的过程,第一个问题是设置有效期防止死锁,并且引入守护线程给锁续期,第二个问题是支持可重入锁,第三个问题是加锁失败后阻塞等待,等锁释放后再次尝试加锁。Redisson框架解决这三个问题的思路也非常值得学习。 185 | 186 | 这篇文章就写到这里了,非常感谢大家的阅读,希望看完之后能得到一些启发和收获。 187 | 188 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 189 | 190 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 191 | 192 | ![img](https://static.lovebilibili.com/dashacha.png) 193 | 194 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /缓存服务/布隆过滤器.md: -------------------------------------------------------------------------------- 1 | > **文章已收录Github精选,欢迎Star**:[https://github.com/yehongzhi](https://github.com/yehongzhi/learningSummary) 2 | 3 | # 概念 4 | 5 | 布隆过滤器(BloomFilter)是由一个叫“布隆”的小伙子在1970年提出的,它是一个很长的二进制向量,主要**用于判断一个元素是否在一个集合中**。 6 | 7 | # 原理 8 | 9 | 在介绍原理之前,要先讲一下**Hash函数**的概念。 10 | 11 | 我们在Java中的HashMap,HashSet其实也接触过hashcode()这个函数,哈希函数是可以将任意大小的输入数据转换成特定大小的输出数据的函数,转换后的数据称为**哈希值**。 12 | 13 | 哈希函数有以下特点: 14 | 15 | - 如果根据同一个哈希函数得到的哈希值不同,那么这两个哈希值的原始输入值肯定不同。 16 | - 如果根据同一个哈希函数得到的两个哈希值相等,两个哈希值的原始输入值有可能相等,有可能不相等。 17 | 18 | 布隆过滤器是由一个很长的二进制向量和一系列的哈希函数组成。那么布隆过滤器是怎么判断一个元素是否在一个集合中的呢? 19 | 20 | 假设布隆过滤器的底层存储结构是一个长度为16的位数组,初始状态时,它的所有位置都设置为0。 21 | 22 | ![](https://static.lovebilibili.com/redis_bl_01.png) 23 | 24 | 当有变量添加到布隆过滤器中,通过K个映射函数将变量映射到位数组的K个点,并把这K个点的值设置为1(假设有三个映射函数)。 25 | 26 | ![](https://static.lovebilibili.com/redis_bl_02.png) 27 | 28 | 查询某个变量是否存在的时候,我们只需要通过同样的K个映射函数,找到对应的K个点,判断K个点上的值是否全都是1,**如果全都是1则表示很可能存在**,如果**K个点上有任何一个是0则表示一定不存在**。 29 | 30 | # 特性 31 | 32 | 第一个问题,为什么说全都是1的情况是很可能存在,而不是一定存在呢? 33 | 34 | 还记得前面说的哈希函数的特点,根据同一个哈希函数得到相同的哈希值,输入值不一定相等。类似于Java中两个对象的hashcode相等,但是不一定相等的道理。说白了,映射函数得到位数组上映射点全都是1,不一定是要查询的这个变量之前存进来时设置的,也有可能是其他变量映射的点。 35 | 36 | 所以这里引出了布隆过滤器的其中一个特点,**存在一定的误判**。 37 | 38 | 第二个问题,布隆过滤器能不能删除元素呢? 39 | 40 | 答案是不能的。因为在位数组上的同一个点有可能有多个输入值映射,如果删除了会影响布隆过滤器里其他元素的判断结果。 41 | 42 | ![](https://static.lovebilibili.com/redis_bl_03.png) 43 | 44 | 如上图,如果删除obj1,把4,7,15置为0,那么判断obj2是否存在时就会导致因为映射点7是0,结果判断obj2是不存在的,结果出错。 45 | 46 | 这是第二个特点,**不能删除布隆过滤器里的元素。** 47 | 48 | # 优缺点 49 | 50 | **优点:** 51 | 52 | - 在空间和时间方面,都有着巨大的优势。因为不是存完整的数据,是一个二进制向量,能节省大量的内存空间,时间复杂度方面,是根据映射函数查询,假设有K个映射函数,那么时间复杂度就是O(K)。 53 | - 因为存的不是元素本身,而是二进制向量,所以在一些对**保密性**要求严格的场景有一定优势。 54 | 55 | **缺点:** 56 | 57 | - **存在一定的误判。**存进布隆过滤器里的元素越多,误判率越高。 58 | - **不能删除布隆过滤器里的元素。**随着使用的时间越来越长,因为不能删除,存进里面的元素越来越多,占用内存越来越多,误判率越来越高,最后不得不重置。 59 | 60 | # 应用于缓存穿透 61 | 62 | **用于缓解缓存穿透。**缓存穿透的问题主要是因为传进来的key在Redis中是不存在的,那么就会直接打在DB上,造成DB压力增大。 63 | 64 | ![](https://static.lovebilibili.com/redis_hc_2.png) 65 | 66 | 针对这种情况,可以在Redis前加上布隆过滤器,预先把数据库中的数据加入到布隆过滤器中,因为布隆过滤器的底层数据结构是一个二进制向量,所以占用的空间并不是很大。**在查询Redis之前先通过布隆过滤器判断是否存在,如果不存在就直接返回,如果存在的话,按照原来的流程还是查询Redis,Redis不存在则查询DB**。 67 | 68 | 这里主要利用的是**布隆过滤器判断结果是不存在的话就一定不存在**这一个特点,但是由于布隆过滤器有一定的误判,所以并不能说完全解决缓存穿透,但是能很大程度缓解缓存穿透的问题。 69 | 70 | ![](https://static.lovebilibili.com/redis_bl_04.png) 71 | 72 | # 布隆过滤器插件 73 | 74 | 我们知道布隆过滤器的底层原理之后,理论上是可以自己 75 | 76 | 在Redis4.0后,官方提供了布隆过滤器的插件功能,布隆过滤器可以作为一个插件加载到Redis服务器直接使用。 77 | 78 | 首先安装Redis,网上有很多安装教程,这里就不多赘述。这里我用的是Redis6.0.10版本。安装完Redis之后,下载插件,使用git命令拉取: 79 | 80 | ```shell 81 | git clone https://github.com/RedisBloom/RedisBloom.git 82 | ``` 83 | 84 | 拉取下来之后会得到一个RedisBloom的项目。 85 | 86 | ![](https://static.lovebilibili.com/redis_bl_05.png) 87 | 88 | 然后cd到文件夹/RedisBloom,使用make命令编译。 89 | 90 | ![](https://static.lovebilibili.com/redis_bl_06.png) 91 | 92 | 编译完成后生成一个redisbloom.so文件。 93 | 94 | ![](https://static.lovebilibili.com/redis_bl_07.png) 95 | 96 | 在启动Redis时,加载布隆过滤器模块到服务器中即可。 97 | 98 | ```shell 99 | ./src/redis-server --loadmodule /usr/local/RedisBloom/redisbloom.so 100 | ``` 101 | 102 | 最后使用客户端测试一下。 103 | 104 | ```she 105 | redis-6.0.10]$ ./src/redis-cli 106 | 107 | 127.0.0.1:6379> bf.add user sam 108 | (integer) 1 109 | 127.0.0.1:6379> bf.add user jack 110 | (integer) 1 111 | 127.0.0.1:6379> bf.exists user jack 112 | (integer) 1 113 | 127.0.0.1:6379> bf.exists user tom 114 | (integer) 0 115 | ``` 116 | 117 | 布隆过滤器的基本指令如下: 118 | 119 | - bf.add 添加元素到布隆过滤器 120 | - bf.exists 判断元素是否在布隆过滤器 121 | - bf.madd 添加多个元素到布隆过滤器 122 | - bf.mexists 判断多个元素是否在布隆过滤器 123 | 124 | ```shell 125 | 127.0.0.1:6379> bf.madd user mike rose 126 | 1) (integer) 1 127 | 2) (integer) 1 128 | 127.0.0.1:6379> bf.mexists user blue mike 129 | 1) (integer) 0 130 | 2) (integer) 1 131 | ``` 132 | 133 | 在实际开发中,一般都是使用Java程序操作Redis,不太可能直接使用命令行判断,Java程序怎么操作呢? 134 | 135 | 首先引入依赖,我使用的是SpringBoot2.0,所以引入依赖是3.15.0。 136 | 137 | ```xml 138 | 139 | org.redisson 140 | redisson-spring-boot-starter 141 | 3.15.0 142 | 143 | ``` 144 | 145 | 接着写个main方法示范一下。 146 | 147 | ```java 148 | public static void main(String[] args) throws Exception { 149 | Config config = new Config(); 150 | config.useSingleServer().setAddress("redis://192.168.0.109:6379"); 151 | RedissonClient client = Redisson.create(config); 152 | 153 | RBloomFilter bloomFilter = client.getBloomFilter("user"); 154 | //尝试初始化,预计元素55000000,期望误判率0.03 155 | bloomFilter.tryInit(55000000L, 0.03); 156 | //添加元素到布隆过滤器中 157 | bloomFilter.add("tom"); 158 | bloomFilter.add("mike"); 159 | bloomFilter.add("rose"); 160 | bloomFilter.add("blue"); 161 | System.out.println("布隆过滤器元素总数为:" + bloomFilter.count());//布隆过滤器元素总数为:4 162 | System.out.println("是否包含tom:" + bloomFilter.contains("tom"));//是否包含tom:true 163 | System.out.println("是否包含lei:" + bloomFilter.contains("lei"));//是否包含lei:false 164 | client.shutdown(); 165 | } 166 | ``` 167 | 168 | # 总结 169 | 170 | 布隆过滤器有着明显的优缺点,所以在使用的时候需要充分地考虑场景,还是那句话,没有最好的技术,看菜下饭才是硬道理。除了在缓存穿透中使用之外,其实还可以使用于元素去重,web拦截器等等。 171 | 172 | 这篇文章就讲到这里,感谢大家的阅读,希望看完之后能有所收获。 173 | 174 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 175 | 176 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 177 | 178 | ![img](https://static.lovebilibili.com/dashacha.png) 179 | 180 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /缓存服务/深入探索redis的五种数据类型.md: -------------------------------------------------------------------------------- 1 | > **文章已收录Github精选,欢迎Star**:[https://github.com/yehongzhi](https://github.com/yehongzhi/learningSummary) 2 | 3 | # 前言 4 | 5 | Redis是一个**开源**的使用C语言编写、支持网络、可**基于内存**亦**可持久化**的日志型、**Key-Value**的NoSQL数据库。 6 | 7 | 一般来说,我们都是使用关系型数据库MySQL来存储数据,但是面对着流量高峰,会对MySQL造成巨大的压力,导致数据库性能很差,这时就要使用缓存中间件来降低数据库的压力,这是Redis最常见的使用场景。除了作为缓存使用之外,Redis还有很多使用场景,比如分布式锁,计数,队列等等。 8 | 9 | 所以Redis对于程序员来说可以算得上是必修课。 10 | 11 | # 安装Redis 12 | 13 | 安装Redis很简单,因为网上教程很多,这里就不再详细讲解,推荐看菜鸟教程:https://www.runoob.com/redis/redis-install.html 14 | 15 | # Redis的特点 16 | 17 | 要用好Redis,首先要明白它的特点: 18 | 19 | - **读写速度快**。redis官网测试读写能到10万左右每秒。速度快的原因这里简单说一下,第一是因为**数据存储在内存中**,我们知道机器访问内存的速度是远远大于访问磁盘的,其次是**Redis采用单线程的架构**,避免了上下文的切换和多线程带来的竞争,也就不存在加锁释放锁的操作,减少了CPU的消耗,第三点是**采用了非阻塞IO多路复用机制**。 20 | - **数据结构丰富**。 Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构。这也是这篇文章要讲的。 21 | - **支持持久化**。Redis提供了RDB和AOF两种持久化策略,能最大限度地保证Redis服务器宕机重启后数据不会丢失。 22 | - **支持高可用**。可以使用主从复制,并且提供哨兵机制,保证服务器的高可用。 23 | - **客户端语言多**。因为Redis受到社区和各大公司的广泛认可,所以客户端语言涵盖了所有的主流编程语言,比如Java,C,C++,PHP,NodeJS等等。 24 | 25 | # Redis的数据结构 26 | 27 | 下面我们就学习Redis的数据结构,也是使用Redis要知道的最基础的知识。 28 | 29 | Redis是一个Key-Value型的内存数据库,它所有的key都是字符串,而value常见的数据类型有五种:string,list,set,zset,hash。 30 | 31 | ![](https://static.lovebilibili.com/redis_kv_01.png) 32 | 33 | Redis的这些数据结构,在底层都是使用redisObject来进行表示。redisObject中有三个重要的属性,分别是**type、 encoding 和 ptr**。 34 | 35 | **type**表示保存的value的类型。通常有以下几种,也就是常见的五种数据结构: 36 | 37 | - 字符串 REDIS_STRING 38 | - 列表 REDIS_LIST 39 | - 集合 REDIS_SET 40 | - 有序集合 REDIS_ZSET 41 | - 字典 REDIS_HASH 42 | 43 | **encoding**表示保存的value的编码,通常有以下几种: 44 | 45 | ```c 46 | #define REDIS_ENCODING_RAW 0 // 编码为字符串 47 | #define REDIS_ENCODING_INT 1 // 编码为整数 48 | #define REDIS_ENCODING_HT 2 // 编码为哈希表 49 | #define REDIS_ENCODING_ZIPMAP 3 // 编码为 zipmap 50 | #define REDIS_ENCODING_LINKEDLIST 4 // 编码为双端链表 51 | #define REDIS_ENCODING_ZIPLIST 5 // 编码为压缩列表 52 | #define REDIS_ENCODING_INTSET 6 // 编码为整数集合 53 | #define REDIS_ENCODING_SKIPLIST 7 // 编码为跳跃表 54 | ``` 55 | 56 | **ptr**是一个指针,指向实际保存的value的数据结构。 57 | 58 | 这里要特别说明一下的是,数据类型和编码方式是有一定关系的,所以数据类型和编码方式是可以确定底层采用什么数据结构存储数据的。 59 | 60 | ![](https://static.lovebilibili.com/redis_kv_02.png) 61 | 62 | # string(字符串) 63 | 64 | 这是redis中最常用的数据类型,字符串对象的 encoding 有三种,分别是:**int、raw、embstr**。 65 | 66 | 常用的命令有常用命令: **set、get、decr、incr、mget** 等。 67 | 68 | 我们知道Redis是用C语言开发的,但是底层存储不是使用C语言的字符串类型,而是自己开发了一种数据类型SDS进行存储,SDS即*Simple Dynamic String* ,是一种动态字符串。我们可以在github找到源码。 69 | 70 | ```c 71 | struct sdshdr{ 72 | int len;/*字符串长度*/ 73 | int free;/*未使用的字节长度*/ 74 | char buf[];/*保存字符串的字节数组*/ 75 | } 76 | ``` 77 | 78 | ![](https://static.lovebilibili.com/redis_kv_03.png) 79 | 80 | SDS与C语言的字符串有什么区别呢? 81 | 82 | - C语言获取字符串长度是从头到尾遍历,时间复杂度是O(n),而SDS有len属性记录字符串长度,时间复杂度为O(1)。 83 | - 避免缓冲区溢出。SDS在需要修改时,会先检查空间是否满足大小,如果不满足,则先扩展至所需大小再进行修改操作。 84 | - 空间预分配。当SDS需要进行扩展时,Redis会为SDS分配好内存,并且根据特定的算法分配多余的free空间,避免了连续执行字符串添加带来的内存分配的消耗。 85 | - 惰性释放。如果需要缩短字符串,不会立即回收多余的内存空间,而是用free记录剩余的空间,以备下次扩展时使用,避免了再次分配内存的消耗。 86 | - 二进制安全。c语言在存储字符串时采用N+1的字符串数组,末尾使用'\0'标识字符串的结束,如果我们存储的字符串中间出现'\0',那就会导致识别出错。而SDS因为记录了字符串的长度len,则没有这个问题。 87 | 88 | 字符串类型的应用是非常广泛的,比如可以把对象转成JSON字符串存储到Redis中作为缓存,也可以使用decr、incr命令用于计数器的实现,又或者是用setnx命令为基础实现分布式锁等等。 89 | 90 | 需要注意的是:**Redis 规定了字符串的长度不得超过 512 MB。** 91 | 92 | # hash(字典) 93 | 94 | 哈希对象的编码有两种,分别是:**ziplist、hashtable**。 95 | 96 | 当哈希对象保存的键值对数量小于 512,并且所有键值对的长度都小于 64 字节时,使用ziplist(压缩列表)存储;否则使用 hashtable 存储。 97 | 98 | Redis中的hashtable跟Java中的HashMap类似,都是通过"数组+链表"的实现方式解决部分的哈希冲突。直接看源码定义。 99 | 100 | ```c 101 | typedf struct dict{ 102 | dictType *type;//类型特定函数,包括一些自定义函数,这些函数使得key和value能够存储 103 | void *private;//私有数据 104 | dictht ht[2];//两张hash表 105 | int rehashidx;//rehash索引,字典没有进行rehash时,此值为-1 106 | unsigned long iterators; //正在迭代的迭代器数量 107 | }dict; 108 | 109 | typedef struct dictht{ 110 | //哈希表数组 111 | dictEntry **table; 112 | //哈希表大小 113 | unsigned long size; 114 | //哈希表大小掩码,用于计算索引值 115 | //总是等于 size-1 116 | unsigned long sizemask; 117 | //该哈希表已有节点的数量 118 | unsigned long used; 119 | }dictht; 120 | 121 | typedf struct dictEntry{ 122 | void *key;//键 123 | union{ 124 | void val; 125 | unit64_t u64; 126 | int64_t s64; 127 | double d; 128 | }v;//值 129 | struct dictEntry *next;//指向下一个节点的指针 130 | }dictEntry; 131 | ``` 132 | 133 | 我们再看一个结构图就比较清楚了。 134 | 135 | ![](https://static.lovebilibili.com/redis_kv_04.webp) 136 | 137 | 下面讲一下扩容和收缩。当哈希表保存的键值太多或者太少时,就会通过rehash来进行相应的扩容和收缩。 138 | 139 | **扩容和收缩的过程**: 140 | 141 | 1、如果执行扩展操作,会基于原哈希表创建一个大小等于 ht[0].used*2n 的哈希表(也就是每次扩展都是根据原哈希表已使用的空间扩大一倍创建另一个哈希表)。相反如果执行的是收缩操作,每次收缩是根据已使用空间缩小一倍创建一个新的哈希表。 142 | 143 | 2、重新利用哈希算法,计算索引值,然后将键值对放到新的哈希表位置上。 144 | 145 | 3、所有键值对都迁徙完毕后,释放原哈希表的内存空间。 146 | 147 | 在redis中执行扩容和收缩的规则是: 148 | 149 | - 服务器目前没有执行 BGSAVE 命令或者 BGREWRITEAOF (持久化)命令,并且负载因子大于等于1。 150 | 151 | - 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF (持久化)命令,并且负载因子大于等于5。 152 | 153 | 负载因子 = 哈希表已保存节点数量 / 哈希表大小。 154 | 155 | **渐进式rehash** 156 | 157 | 什么是渐进式,也就是说扩容和收缩不是一次性,集中式地完成,而是通过多次逐渐地完成的。为什么要采用这种方式呢?如果是几十个键值,那么rehash当然可以瞬间完成,如果是几十万,几百万的键值要一次性进行rehash,势必会导致redis性能严重下降,自然而然地redis开发者就想到采用渐进式rehash。过程如下: 158 | 159 | 在rehash时,会使用rehashidx字段保存迁移的进度,rehashidx为0表示迁移开始。 160 | 161 | 在迁移过程中ht[0]和ht[1]会同时保存数据,ht[0]指向旧哈希表,ht[1]指向新哈希表,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]的元素迁移到ht[1]中。 162 | 163 | 随着字典操作的不断执行,最终会在某个时间节点,ht[0]的所有键值都会被迁移到ht[1]中,rehashidx设置为-1,代表迁移完成。如果没有执行字典操作,redis也会通过定时任务去判断rehash是否完成,没有完成则继续rehash。 164 | 165 | rehash完成后,ht[0]指向的旧表会被释放, 之后会将新表的持有权转交给ht[0], 再重置ht[1]指向NULL。 166 | 167 | **渐进式rehash的优缺点**: 168 | 169 | 优点是把rehash操作分散到每一个字典操作和定时函数上,避免了一次性集中式rehash带来的服务器压力。 170 | 171 | 缺点是在rehash期间需要使用两个hash表,占用内存稍大。 172 | 173 | hash类型的常用命令有:hget、hset、hgetall 等。 174 | 175 | # list(链表) 176 | 177 | 列表对象的编码有两种,分别是:ziplist、linkedlist。当列表的长度小于 512,并且所有元素的长度都小于 64 字节时,使用ziplist存储;否则使用 linkedlist 存储。 178 | 179 | Redis中的linkedlist类似于Java中的LinkedList,是一个链表,底层的实现原理也和LinkedList类似。这意味着list的插入和删除操作效率会比较快,时间复杂度是O(1)。我们看源码: 180 | 181 | ```c 182 | typedef struct listNode { 183 | struct listNode *prev; 184 | struct listNode *next; 185 | void *value; 186 | } listNode; 187 | 188 | typedef struct listIter { 189 | listNode *next; 190 | int direction; 191 | } listIter; 192 | 193 | typedef struct list { 194 | listNode *head; 195 | listNode *tail; 196 | void *(*dup)(void *ptr); 197 | void (*free)(void *ptr); 198 | int (*match)(void *ptr, void *key); 199 | unsigned long len; 200 | } list; 201 | ``` 202 | 203 | 我们可以看到,listNode就是链表中的节点元素,通过prev和next组成双向链表。 204 | 205 | ![](https://static.lovebilibili.com/redis_kv_05.png) 206 | 207 | list则记录了头结点head,尾结点tail,还有链表长度len,match函数用于比较两个节点的值是否相等,操作起来更加方便。 208 | 209 | ![](https://static.lovebilibili.com/redis_kv_06.png) 210 | 211 | list类型常用的命令有:lpush、rpush、lpop、rpop、lrange等。 212 | 213 | 214 | 215 | # set(集合) 216 | 217 | set类型的特点很简单,无序,不重复,跟Java的HashSet类似。它的编码有两种,分别是intset和hashtable。如果value可以转成整数值,并且长度不超过512的话就使用intset存储,否则采用hashtable。 218 | 219 | hashtable在前面讲hash类型时已经讲过,这里的set集合采用的hashtable几乎一样,只是哈希表的value都是NULL。这个不难理解,比如用Java中的HashMap实现一个HashSet,我们只用HashMap的key就是了。 220 | 221 | 我们讲一讲intset,先看源码。 222 | 223 | ```c 224 | typedef struct intset{ 225 | uint32_t encoding;//编码方式 226 | 227 | uint32_t length;//集合包含的元素数量 228 | 229 | int8_t contents[];//保存元素的数组 230 | }intset; 231 | ``` 232 | 233 | encoding有三种,分别是INTSET_ENC_INT16、INSET_ENC_INT32、INSET_ENC_INT64,代表着整数值的取值范围。Redis会根据添加进来的元素的大小,选择不同的类型进行存储,可以尽可能地节省内存空间。 234 | 235 | length记录集合有多少个元素,这样获取元素个数的时间复杂度就是O(1)。 236 | 237 | contents,存储数据的数组,数组按照从小到大有序排列,不包含任何重复项。 238 | 239 | ![](https://static.lovebilibili.com/redis_kv_07.png) 240 | 241 | 这里我们可能会提出疑问,如果一开始存的是INTSET_ENC_INT16(范围在-32,768~32,767),如果这时添加了一个40000的数,怎么升级为INSET_ENC_INT32呢? 242 | 243 | 升级过程是这样的: 244 | 245 | 1、根据新元素的类型扩展数组contents的空间。 246 | 247 | 2、从尾部将数据插入。 248 | 249 | 3、根据新的编码格式重置之前的值,因为这时的contents存在着两种编码的值。从插入的数据的位置,也就是尾部,从后到前将之前的数据按照新的编码格式进行移动和设置。从后到前调整是为了防止数据被覆盖。 250 | 251 | 升级的优点在于,根据存储的数据大小选择合适的编码方式,节省了内存。 252 | 253 | 缺点在于,升级会消耗系统资源。而且升级是不可逆的,也就是一旦对数组进行升级,编码就会一直保持升级后的状态。 254 | 255 | set数据类型常用的命令有:sadd、spop、smembers、sunion等等。 256 | 257 | Redis为set类型提供了求交集,并集,差集的操作,可以非常方便地实现譬如共同关注、共同爱好、共同好友等功能。 258 | 259 | # zset(有序集合) 260 | 261 | zset是Redis中比较有特色的数据类型,它和set一样是不可重复的,区别在于多了score值,用来代表排序的权重。也就是当你需要一个有序的,不可重复的集合列表时,就可以考虑使用这种数据类型。 262 | 263 | zset的编码有两种,分别是:ziplist、skiplist。当zset的长度小于 128,并且所有元素的长度都小于 64 字节时,使用ziplist存储;否则使用 skiplist 存储。 264 | 265 | 这里要讲一下skiplist,也就是跳跃表。它的底层实现比较复杂,这里简单地提一下。 266 | 267 | ![](https://static.lovebilibili.com/redis_kv_08.png) 268 | 269 | 跳跃表的数据结构如上图所示,为什么要设计成这样呢?好处在于查询的时候,可以减少时间复杂度,如果是一个链表,我们要插入并且保持有序的话,那就要从头结点开始遍历,遍历到合适的位置然后插入,如果这样性能肯定是不理想的。 270 | 271 | 所以问题的关键在于**能不能像使用二分查找一样定位到插入的点**,答案就是使用跳跃表。比如我们要插入38,那么查找的过程就是这样。 272 | 273 | 首先从L4层,查询87,需要查询1次。 274 | 275 | 然后到L3层,查询到在->24->87之间,需要查询2次。 276 | 277 | 然后到L2层,查询->48,需要查询1次。 278 | 279 | 然后到L1层,查询->37->48,查询2次。确定在37->48之间是插入点。 280 | 281 | 有没有发现经过L4,L3,L2层的查询后已经跳过了很多节点,当到了L1层遍历时已经把范围缩小了很多。这就是跳跃表的优势。这种方式有点类似于二分查找,所以他的时间复杂度为**O(logN)**。 282 | 283 | 其实生活中也有这种例子,类似于快递填写的地址是省->市->区->镇->街,当快递公司在送快递时就根据地址层层缩小范围,最终锁定在一个很小的区域去搜索,提高了效率。 284 | 285 | zet常用的命令有:zadd、zrange、zrem、zcard等。 286 | 287 | zset的特点非常适合应用于开发排行榜的功能,比如三天阅读排行榜,游戏排行榜等等。 288 | 289 | # 总结 290 | 291 | Redis能够受到社区的认可,并且在互联网中如此欢迎,除了速度快之外,很大原因也跟丰富的数据类型有关,而且很多数据类型的底层实现也是会考虑到内存空间的使用,尽可能地节省内存空间。 292 | 293 | 其实很多人是知道Redis常用的五种数据类型,但是对于底层的实现,就没有深入去研究,当然我以前也是没有深入的。那么在面试时就没有产生差异化,要从面试中脱颖而出最重要就是要跟普通程序员不一样,这样才能突出自身的价值。所以深入学习Redis的数据结构还是很有用的。 294 | 295 | 希望这篇文章能让大家对Redis有更深入的理解,感谢大家的阅读。 296 | 297 | **觉得有用就点个赞吧,你的点赞是我创作的最大动力**~ 298 | 299 | **我是一个努力让大家记住的程序员。我们下期再见!!!** 300 | 301 | ![img](https://static.lovebilibili.com/dashacha.png) 302 | 303 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! 304 | 305 | -------------------------------------------------------------------------------- /网络编程/Reactor模式.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Reactor模式 3 | date: 2020-07-01 22:13:02 4 | index_img: https://static.lovebilibili.com/reactor_index.jpg 5 | tags: 6 | - java 7 | - NIO 8 | --- 9 | 10 | # 思维导图 11 | ![在这里插入图片描述](https://user-gold-cdn.xitu.io/2020/6/30/17305cc05ba3465c?w=894&h=436&f=png&s=45997) 12 | # 一、Reactor模式介绍 13 | 本文主要参考Doug Lea(大神)的“**Scalable IO in Java**”中讲述的Reactor模式。 14 | 15 | 原文地址:[http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf](http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf) 16 | 17 | 有兴趣的可以看看这本书,受益匪浅! 18 | 19 | 20 | 21 | ## 1.1 什么是Reactor模式 22 | Reactor模式一般翻译成"**反应器模式**",也有人称为"**分发者模式**"。它是将客户端请求提交到一个或者多个服务处理程序的设计模式。工作原理是由**一个线程来接收所有的请求**,然后**派发这些请求到相关的工作线程中**。 23 | ## 1.2 为什么使用Reactor模式 24 | 在java中,没有NIO出现之前都是使用socket编程。socket的接收请求是阻塞的,需要处理完一个请求才能处理下一个请求,所以在面对高并发的服务请求时,性能就会很差。 25 | 26 | 那有人就会说使用多线程(如下图所示)。接收到一个请求,就创建一个线程处理,这样就不会阻塞了。实际上这样的确是可以在提升性能上起到一定的作用,**但是当请求很多的时候,就会创建大量的线程,维护线程需要资源的消耗,线程之间的切换也需要消耗性能**。而且系统创建线程的数量也是有限的,所以当高并发时,会直接把系统拖垮。 27 | ![在这里插入图片描述](https://user-gold-cdn.xitu.io/2020/6/30/17305cc05ef76206?w=736&h=316&f=png&s=203508) 28 | 由于以上的问题,提出了Reactor模式。 29 | 30 | 基于Java,Doug Lea(Java并发包作者)提出了三种形式,**单Reactor单线程,单Reactor多线程和多Reactor多线程**。 31 | # 二、Reactor模式的演进过程 32 | 在介绍三种Reactor模式前,先简单地说明三个角色: 33 | > `Reactor`:负责响应事件,将事件分发到绑定了对应事件的Handler,如果是连接事件,则分发到Acceptor。 34 | 35 | > `Handler`:事件处理器。负责执行对应事件对应的业务逻辑。 36 | 37 | > `Acceptor`:绑定了 connect 事件,当客户端发起connect请求时,Reactor会将accept事件分发给Acceptor处理。 38 | 39 | ## 2.1 单Reactor单线程 40 | ![在这里插入图片描述](https://user-gold-cdn.xitu.io/2020/6/30/17305cc05bf6fbe7?w=714&h=310&f=png&s=84409) 41 | ### 工作流程 42 | >只有一个`select`循环接收请求,客户端(client)注册进来由`Reactor`接收注册事件,然后再由reactor分发(dispatch)出去,由下面的处理器(Handler)去处理。 43 | 44 | ### 通俗解释 45 | 一个餐厅里只有一个既是前台也是服务员的人,负责接待客人,也负责把客人点的菜下达给厨师。 46 | 47 | ### 单Reactor单线程的特点 48 | 单线程的问题实际上是很明显的。只要其中一个Handler方法阻塞了,那就会导致所有的client的Handler都被阻塞了,也会导致注册事件也无法处理,无法接收新的请求。所以这种模式用的比较少,因为不能充分利用到多核的资源。 49 | 50 | 这种模式仅仅只能处理Handler比较快速完成的场景。 51 | 52 | ## 2.2 单Reactor多线程 53 | ![在这里插入图片描述](https://user-gold-cdn.xitu.io/2020/6/30/17305cc05fac64c3?w=721&h=496&f=png&s=181611) 54 | ### 工作流程 55 | >在**单Reactor多线程**中,注册接收事件都是由`Reactor`来做,其它的计算,编解码由一个线程池来做。从图中可以看出工作线程是多线程的,监听注册事件的`Reactor`还是单线程。 56 | ### 通俗解释 57 | 相当于餐厅里有一个前台,多个服务员。前台只负责接待客人,服务员只负责服务客人。 58 | ### 单Reactor多线程的特点 59 | 对比**单Reactor单线程**模型,多线程Reactor模式在Handler读写处理时,交给工作线程池处理,不会导致Reactor无法执行,因为Reactor分发和Handler处理是分开的,能充分地利用资源。从而提升应用的性能。 60 | 61 | 缺点: 62 | Reactor只在主线程中运行,承担所有事件的监听和响应,如果短时间的高并发场景下,依然会造成性能瓶颈。 63 | ## 2.3 多Reactor多线程 64 | ![在这里插入图片描述](https://user-gold-cdn.xitu.io/2020/6/30/17305cc05faeebc1?w=686&h=483&f=png&s=121267) 65 | ### 工作流程 66 | >1、mainReactor负责监听客户端请求,专门处理新连接的建立,将建立好的连接注册到subReactor。 67 | >2、subReactor 将分配的连接加入到队列进行监听,当有新的事件发生时,会调用连接相对应的Handler进行处理。 68 | 69 | ### 通俗解释 70 | 相当于餐厅里有多个前台和多个服务员,前台只负责接待客人,服务员只负责服务客人。 71 | ### 多Reactor多线程的特点 72 | mainReactor 主要是用来处理客户端请求连接建立的操作。 73 | subReactor主要做和建立起来的连接做数据交互和事件业务处理操作,每个subReactor一个线程来处理。 74 | >这样的模型使得每个模块更加专一,耦合度更低,能支持更高的并发量。许多框架也使用这种模式,比如接下来要讲的Netty框架就采用了这种模式。 75 | # 三、在Netty中的应用 76 | Netty可谓是框架中精品中的极品,要用一张图或者一段话来总结概括不太可能,所以下面我仅分析一下Netty框架的架构模型。在下一篇文章再继续深入探究Netty。 77 | ![在这里插入图片描述](https://user-gold-cdn.xitu.io/2020/6/30/17305cc0641d4ed5?w=855&h=726&f=png&s=400825) 78 | 这个架构实际上跟多Reactor多线程模型比较像。 79 | > 1、BossGroup相当于mainReactor,负责建立连接并且把连接注册到WorkGroup中。WorkGroup负责处理连接对应的读写事件。 80 | 81 | > 2、BossGroup和WorkGroup是两个线程池,里面有多个NioEventGroup(实际上是线程),默认BossGroup和WorkGroup里的线程数是cpu核数的两倍(源码中有体现)。 82 | 83 | > 3、每一个NioEventGroup都是一个无限循环,负责监听相对应的事件。 84 | 85 | > 4、Pipeline(通道)里包含多个ChannelHandler(业务处理),按顺序执行。 86 | # 写在最后 87 | 其实上面的这些模型都只是一种思想,很多人可能觉得学习思想不是很重要。实际上要学习一门技术,要先有天上飞的理论才有落地的产品。世界上的事物大多都是如此。 88 | 89 | 最后借用大神**Doug Lea**的名言: 90 | >分享知识和分享苹果是不一样的,苹果会越分越少,而自己的知识并不会因为给了别人就减少了,知识的分享更能激荡出不一样的火花。 91 | 92 | **创作不易**,觉得有用就**点个赞**吧。 93 | 94 | 下一篇讲Netty框架,想第一时间看到我更新的文章,可以微信搜索公众号「`java技术爱好者`」,**拒绝做一条咸鱼,我是一个努力让大家记住的程序员。我们下期再见!!!** 95 | ![在这里插入图片描述](https://user-gold-cdn.xitu.io/2020/6/30/17305cc08a7ed5d7?w=1180&h=528&f=png&s=152520) 96 | 97 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /遇到的坑/List集合的坑.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: List集合的坑 3 | date: 2020-06-21 17:24:09 4 | index_img: https://static.lovebilibili.com/List集合的坑.jpg 5 | tags: 6 | - java 7 | - 集合 8 | - 经验总结 9 | --- 10 | 11 | > 学如逆水行舟,不进则退 12 | 13 | 经过几年的工作经验,我发现`List`有很多坑,之前公司有些实习生一不小心就踩到了,所以我打算写一篇文章总结一下,希望看到这篇文章的人能不再踩到坑,代码没bug。做个快乐的程序员。 14 | 15 | 16 | 17 | ### 迭代时删除元素 18 | 使用`for-each`迭代遍历时,删除集合中的元素,会报错。 19 | ```java 20 | private static List list = new ArrayList<>(); 21 | 22 | static { 23 | //初始化集合 24 | for (int i = 1; i <= 10; i++) { 25 | list.add(String.valueOf(i)); 26 | } 27 | } 28 | 29 | public static void main(String[] args) { 30 | //使用for-each迭代时删除元素 31 | for (String str : list) { 32 | if ("1".equals(str)) { 33 | list.remove(str); 34 | } 35 | } 36 | } 37 | ``` 38 | 或者你使用迭代器`Iterator`遍历时,删除元素。 39 | ```java 40 | public static void main(String[] args) { 41 | //使用Iterator迭代器遍历时,删除元素 42 | Iterator it = list.iterator(); 43 | while (it.hasNext()) { 44 | String str = it.next(); 45 | if ("1".equals(str)) { 46 | list.remove(str); 47 | } 48 | } 49 | } 50 | ``` 51 | 以上两种情况都会报这个错: 52 | ```java 53 | Exception in thread "main" java.util.ConcurrentModificationException 54 | at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) 55 | at java.util.ArrayList$Itr.next(ArrayList.java:851) 56 | ``` 57 | 这就是不正确的删除姿势,那怎么删呢? 58 | 59 | 使用`for-i`循环遍历删除(亲测有效): 60 | ```java 61 | public static void main(String[] args) { 62 | //使用Iterator迭代器遍历时,删除元素 63 | for (int i = 0; i < list.size(); i++) { 64 | String s = list.get(i); 65 | if ("1".equals(s)) { 66 | list.remove(s); 67 | } 68 | } 69 | list.forEach(System.out::println);//2 3 4 5 6 7 8 9 10 70 | } 71 | ``` 72 | 使用`for-i`循环倒序遍历,删除元素。 73 | ```java 74 | public static void main(String[] args) { 75 | //使用for-i倒序遍历,删除元素 76 | for (int i = list.size() - 1; i >= 0; i--) { 77 | String str = list.get(i); 78 | if ("1".equals(str)) { 79 | list.remove(str); 80 | } 81 | } 82 | list.forEach(System.out::println);//2 3 4 5 6 7 8 9 10 83 | } 84 | ``` 85 | 使用`Iterator`的`remove()`方法删除。 86 | ```java 87 | public static void main(String[] args) { 88 | //使用Iterator迭代器遍历时,删除元素 89 | Iterator it = list.iterator(); 90 | while (it.hasNext()) { 91 | String str = it.next(); 92 | if ("1".equals(str)) { 93 | it.remove(); 94 | } 95 | } 96 | list.forEach(System.out::println);//2 3 4 5 6 7 8 9 10 97 | } 98 | ``` 99 | 要么潇洒一点,用`Lambda`表达式。在java8中,`List`增加了一个`removeIf()`方法用于删除。 100 | ```java 101 | public static void main(String[] args) { 102 | //使用removeIf()遍历时,删除元素。删除集合中为1的元素 103 | list.removeIf(str -> "1".equals(str)); 104 | list.forEach(System.out::println);//2 3 4 5 6 7 8 9 10 105 | } 106 | ``` 107 | ### 使用asList()获得集合删除/增加 108 | 看代码演示: 109 | ```java 110 | public static void main(String[] args) { 111 | List nums = Arrays.asList(1, 2, 3, 4, 5, 6); 112 | nums.add(7); 113 | } 114 | ``` 115 | ```java 116 | public static void main(String[] args) { 117 | List nums = Arrays.asList(1, 2, 3, 4, 5, 6); 118 | nums.remove(1); 119 | } 120 | ``` 121 | 如果你进行以上操作,就会看到报错: 122 | ```java 123 | Exception in thread "main" java.lang.UnsupportedOperationException 124 | at java.util.AbstractList.remove(AbstractList.java:161) 125 | ``` 126 | 为什么会报这个错,看一下源代码就知道了! 127 | ```java 128 | private static class ArrayList extends AbstractList implements RandomAccess, java.io.Serializable { 129 | 130 | } 131 | ``` 132 | `ArrayList`不是`util`包的`ArrayList`,而是`Arrays`的一个内部类。因为继承了`AbstractList`抽象类,但是又没有实现`add()`、`remove()`方法。所以会调用抽象类的`add()`和`remove()`。 133 | 你猜猜抽象类的`add()`怎么着? 134 | ```java 135 | public void add(int index, E element) { 136 | throw new UnsupportedOperationException(); 137 | } 138 | 139 | public E remove(int index) { 140 | throw new UnsupportedOperationException(); 141 | } 142 | ``` 143 | 所以不能用`asList()`得到的集合去增删了! 144 | ### 通过subList()方法获得集合后增删 145 | 当使用`subList()`方法获得集合后删除,原(父)集合也会被删除。 146 | ```java 147 | public static void main(String[] args) { 148 | List subList = list.subList(0, 5); 149 | System.out.println(list);//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 150 | System.out.println(subList);//[1, 2, 3, 4, 5] 151 | subList.remove("1"); 152 | System.out.println(list);//[2, 3, 4, 5, 6, 7, 8, 9, 10] 153 | System.out.println(subList);//[2, 3, 4, 5] 154 | } 155 | ``` 156 | 当使用`subList()`方法获得集合后增加元素,原(父)集合也会增加。 157 | ```java 158 | public static void main(String[] args) { 159 | List subList = list.subList(0, 5); 160 | System.out.println(list);//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 161 | System.out.println(subList);//[1, 2, 3, 4, 5] 162 | subList.add("11"); 163 | System.out.println(list);//[1, 2, 3, 4, 5, 11, 6, 7, 8, 9, 10] 164 | System.out.println(subList);//[1, 2, 3, 4, 5, 11] 165 | } 166 | ``` 167 | 大家看一下源码就知道什么原因了。 168 | ```java 169 | private class SubList extends AbstractList implements RandomAccess { 170 | public void add(int index, E e) { 171 | rangeCheckForAdd(index); 172 | checkForComodification(); 173 | //父集合添加元素 174 | parent.add(parentOffset + index, e); 175 | this.modCount = parent.modCount; 176 | this.size++; 177 | } 178 | 179 | public E remove(int index) { 180 | rangeCheck(index); 181 | checkForComodification(); 182 | //父集合删除元素 183 | E result = parent.remove(parentOffset + index); 184 | this.modCount = parent.modCount; 185 | this.size--; 186 | return result; 187 | } 188 | } 189 | ``` 190 | 如果希望截取的集合是和原集合互不干扰的话,可以这样: 191 | ```java 192 | List subList = new ArrayList<>(list.subList(0, 5)); 193 | ``` 194 | ### 使用Collections.unmodifiableList()创建不可变集合也是可变的。 195 | 当不可变集合的原集合改变时,不可变集合也跟着改变。演示代码: 196 | ```java 197 | public static void main(String[] args) { 198 | List unmodifiableList = Collections.unmodifiableList(list); 199 | System.out.println(unmodifiableList);//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 200 | //删除原集合元素 201 | list.remove("1"); 202 | System.out.println(unmodifiableList);//[2, 3, 4, 5, 6, 7, 8, 9, 10] 203 | } 204 | ``` 205 | 看源码就知道原因了: 206 | ```java 207 | UnmodifiableList(List list) { 208 | super(list); 209 | this.list = list; 210 | } 211 | ``` 212 | **因为不可变集合的成员变量的引用是指向原集合的,所以当原集合改变时,不可变集合也会随之改变**。 213 | 214 | 解决方式:使用`Guava`工具包的`ImmutableList.copyOf()`方法创建。 215 | ```java 216 | public static void main(String[] args) throws Exception { 217 | List unmodifiableList = ImmutableList.copyOf(list); 218 | list.remove("1"); 219 | System.out.println(unmodifiableList);//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 220 | } 221 | ``` 222 | 223 | 创作不易,觉得有用就**点个赞**吧。 224 | 225 | 想第一时间看到我更新的文章,可以微信搜索公众号「`java技术爱好者`」,**拒绝做一条咸鱼,我是一个在互联网荒野求生的程序员。我们下期再见!!!** 226 | ![在这里插入图片描述](https://static.lovebilibili.com/erweimaguanzhu.png) 227 | 228 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! -------------------------------------------------------------------------------- /面经分享/记一次高级java面试.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 记一次高级java面试 3 | date: 2020-05-24 20:20:49 4 | index_img: https://static.lovebilibili.com/interview_summary_1.jpg 5 | tags: 6 | - java 7 | - 面试 8 | --- 9 | 10 | # 记录一次高级JAVA开发面试题目 11 | 面试时间大概40多分钟,问了有十几个问题,回忆一下记录下来,总结经验,以供参考。 12 | 13 | 14 | 15 | **1、 static关键字的作用,平时开发用在什么地方?** 16 | 答:主要有三种用法。 17 | ①修饰成员变量,用static修饰的成员变量就成为静态变量,静态变量只会存在一份,在类被加载时会初始化,且只会加载一次,通过类名访问。一般可以用static和final定义一些String类型,boolean类型,int类型的变量作为常量,可以减少资源的消耗。 18 | ②static修饰方法,该方法就被定义为静态方法,静态方法是不能被方法重写的,通过类名调用。一般用static定义一些工具类的方法。 19 | ③用static修饰代码块,该代码块就被定义为静态代码块,静态代码块在类初始化时被执行,且执行一次。一般用于初始化一些静态的成员变量的值。 20 | 21 | **2、static修饰的成员变量和非static修饰的成员变量有什么区别?分别存在什么区域?** 22 | 答:静态成员变量在内存中只会存在一份,是通过类名访问,存在于静态区中。非静态成员变量是随着对象的创建而存在的,可以有多份,通过创建的对象访问,存在于堆内存中。 23 | 24 | **3、说一下类初始化的顺序。** 25 | 答:静态成员变量、静态代码块、实例成员变量,实例代码块,构造器,实例方法。 26 | 27 | **4、常用的集合类型有哪些?** 28 | 答:有Map、Set、List是比较常用的。 29 | 30 | **5、List常用的实现类有哪些?ArrayList和LinkedList底层实现原理是什么?** 31 | 答:List常用的实现类有ArrayList和LinkedList。ArrayList底层原理是数组+动态扩容机制实现的,LinkedList底层原理是用Node结点形成的链表实现的。 32 | 33 | **6、在开发中如何选择使用ArrayList和LinkedList?** 34 | 答:ArrayList是数组实现,所以通过下标访问效率最快,但是缺点是如果增删比较频繁的情况下,需要经常扩容,性能不是很好。LinkedList在增删的情况下,效率较高,但是访问集合中的元素时都需要从第一个元素开始遍历,效率较低。所以如果增删的情况较多的时候,可以使用LinkedList。查询较多时使用ArrayList。 35 | 36 | **7、List集合如果要排序有哪些实现方式?** 37 | ①使用List接口定义的sort()方法。 38 | ```java 39 | list.sort(Comparator.comparingInt(User::getAge)); 40 | ``` 41 | ②使用Collections的sort()方法,排序的对象需要实现Comparable接口,重写compareTo()方法。 42 | ```java 43 | //实现Comparable接口 44 | public class User implements Comparable { 45 | //重写compareTo方法 46 | @Override 47 | public int compareTo(User user) { 48 | return Integer.compare(this.getAge(), user.getAge()); 49 | } 50 | } 51 | ``` 52 | 使用Collections的sort()方法 53 | ```java 54 | Collections.sort(list); 55 | //如果不想实现Comparable接口,也可以使用这个方法 56 | Collections.sort(list,Comparator.comparingInt(User::getAge)); 57 | ``` 58 | ③使用Stream流操作的sort()方法,传入一个Comparator接口。 59 | ```java 60 | list.stream().sorted(Comparator.comparingInt(User::getAge)).collect(Collectors.toList()); 61 | ``` 62 | 63 | **8、ArrayList是线程安全的吗?有什么方式可以让ArrayList变成线程安全的?** 64 | 答:不是线程安全的。 65 | 使用Collections的synchronizedList()方法包装可获得线程安全的ArrayList。 66 | ```java 67 | List list = Collections.synchronizedList(new ArrayList<>()); 68 | ``` 69 | 70 | **9、你是怎么在项目中使用redis的?** 71 | 答:这其实是考了“redis常用的应用场景”这个问题。 72 | ①利用redis读写速度快的特点,可以做热点数据的储存,降低数据库查询的压力。 73 | ②利用redis键值设置有效期的特性,做一些限时的业务。比如手机验证码。 74 | ③利用setnx命令的特性,可以实现分布式锁。 75 | 76 | **10、使用Redis实现分布式锁的原理是什么?** 77 | 答: 利用setnx命令的特性。使用setnx一个lockKey字符串作为键,当前的时间+上锁时间作为value。如果返回是0,表示已经被上锁了,需要等待锁持有者释放锁;如果返回1,则表示获得了锁。客户端释放锁的话执行del命令删除lockKey对应的键值。 78 | 79 | **11、如果使用分布式锁加锁后,由于一些异常的原因没有执行解锁的操作,怎么办?** 80 | 答:一般解锁操作会放在finally代码块中执行。如果有极端情况下没有执行到解锁的操作,可以通过key对应的时间戳判断是否超时,然后使用GETSET命令去进行解锁,通过判断返回的时间戳是否是超时的key对应的时间戳,确认是否成功上锁。 81 | 82 | **12、如果加分布式锁的时候,业务操作时间比较长,造成长时间的阻塞,有什么解决方案?** 83 | 答:可以在加锁时启动一个watch dog(看门狗)线程,每隔10秒检查一下,如果客户端还持有锁则加长lockKey的生存时间。或者可以考虑用zookeeper实现的分布式锁,因为zk实现原理是基于事件监听的方式来实现。 84 | 85 | **13、MySQL性能优化的策略有哪些?** 86 | ①复杂的多表查询可以拆成多句简单查询。 87 | ②返回尽量少的列,按需返回,严禁使用select *。 88 | ③尽量使用索引列做查询条件和排序条件。 89 | ④使用复合索引要遵循最左匹配原则。 90 | 91 | **14、MySQL索引创建的原则是什么?** 92 | ①对于查询频率高的字段,创建索引。 93 | ②对排序、分组、联合查询频率高的字段创建索引。 94 | ③如果多个列都需要设置索引,可以考虑创建复合索引。 95 | ④尽量选择数据量较少的列作为索引。 96 | ⑤一个表的索引数量不宜过多,会降低查询的效率。 97 | 98 | 99 | **15、雪花算法是什么原理?** 100 | 答:使用一个 64 bit 的 long 型的数字作为全局唯一 id。是由时间戳、机房id、机器id、序号组成的。结合了UUID的全局唯一的特点,又具有自增有顺序的特点。 101 | 102 | **16、为什么雪花算法生成的主键有字符串类型和long类型两种类型?** 103 | 答:因为后端返回给前端一个long类型时,会有可能产生丢失精度的问题,所以会有字符串的类型,弥补这个问题。 104 | 105 | **17、谈一谈MySQL锁机制。** 106 | 主要有以下几种锁: 107 | 表锁。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。 108 | 行锁。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 109 | 在MySQL中只有InnoDB存储引擎可以使用行锁。行锁又分为以下两种形式: 110 | 读锁(共享锁):当读取一条数据时,会加上读锁,其他事务如果要读取是可以的,如果要修改则要等事务释放才可以。 111 | 写锁(排他锁):这个比较简单,当有一个事务要修改数据时,就会给这些行加上写锁。在加锁期间,不允许其他事务加上任何的锁,只有当这个事务释放了,才可以加锁操作。 112 | 113 | 在这次面试中,其实也不是特别难,大部分都回答得不错,但是有两个问题不是很好。雪花算法为什么主键生成有两种类型这个问题没有答出来,还有分布式锁长时间阻塞的解决方案没有详细展开讲。 114 | 115 | 100 116 | 117 | > 能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流! 118 | 119 | --------------------------------------------------------------------------------