程序猿本猿从事开发五年
断断续续记录下的一些笔记
以及其他博主的一些优秀文章
不仅于此,后续慢慢再补。。。
微不足道的知识点
文章目录
- 空哈希表/不存在的key
- round robin轮询(默认)
- weight权重
- IP_hash
- 一致性hash
- url_hash(第三方)
- fair(第三方)
一、JVM
https://www.cnblogs.com/JesseP/p/11750847.html
1. JVM内存模型
- Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。
- 方法区(Method Area),方法区与Java堆一样,是各个线程共享的内存区域,它用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 程序计数器(Program Counter Register),程序计数器是一块较小的内存空间。
- 它的作用可以看做是当前线程所执行的字节码的行号指示器。当前线程执行通过程序计算器的偏移量找到对应的字节码,再根据字节码执行对应的指令完成任务。
- JVM栈(JVM Stacks),与程序计数器一样,Java虚拟机栈也是线程私有的,他的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)
- 本地方法栈(Native Method Stacks),本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
2.解释内存中的栈(stack)、堆(heap)、和方法区(area)的用法
栈内存
中存储的是基本数据类型
,堆内存
中存储的是数组
、引用数据类型
等。
以下代码以存在于方法中为前提,非类全局变量:
String str2 = "hello";
上面的语句中变量str放在栈上,而“hello”是字符串是不可变的对象,这个值是存储在堆内存的字符串池中。
class myTest { public void printText(){ String str = new String("hello"); } }
上面的语句中变量str放在栈上,用new创建出来的字符串对象放在堆上,而“hello”存储在堆内存的字符串池中。
- 根据提供的代码,在Java虚拟机的内存模型中,不同的部分将会在内存中的不同位置:
类的定义部分:类的定义部分,包括类的成员变量、方法等,在类加载时被加载到方法区中。对于这段代码来说,类myTest的定义部分将被加载到方法区中。
方法区:在方法区中,存储类的定义信息,包括类的成员变量、方法、常量等。
执行方法区中的printText()方法:在方法区中
执行printText()方法时
,会在方法栈(栈帧)中创建
一个新的栈帧
,用于存储
该方法的局部变量和操作栈
。局部变量str引用:在printText()方法的栈帧中,将会在
局部变量表
中创建一个名为str的局部变量
,并分配内存空间
用于存储该局部变量的值
。字符串对象"hello":在
堆内存
中,会创建一个字符串对象
"hello",通过在堆
中动态分配内存
来存储字符串的值
。当使用new关键字创建一个String对象时,会在堆中创建一个新的字符串对象。str的值引用字符串对象:
局部变量
str持有的是字符串对象在堆中的引用
。这个引用保存在局部变量表中的str槽位中。总结:在这段代码中,类myTest的定义部分存储在方法区中。当执行printText()方法时,在方法栈中创建一个新的栈帧,其中包含局部变量str的引用。字符串对象"hello"将在堆内存中创建,并被局部变量str引用。这样,字符串"hello"的值将位于堆内存中,而局部变量str的引用将位于方法栈中的局部变量表中。
3. 对象分配规则
- 年轻代(Eden区+Survivor区(from space区 + to space区))、老年代。
- 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
- 大对象直接进入老年代(大对象:可通过参数指定对象超过多少直接进入老年代 -XX:PretenureSizeThreshold=1M)。这样做的目的是避免在年轻代(Eden区和Survivor区)之间发生大量的内存拷贝。
- 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计算器,每次发生Minor GC时,Eden区存活下来的对象将进入Survivor区,并设定年龄为1,Survivor区中存活下来的对象的年龄加1,年龄达到阈值(默认15)将进入老年区。
- 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
- 空间分配担保。每次进行GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一个Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Minor GC,false则进行Full GC。
4. 什么是类的加载
类的加载指的是将类的.calss文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后再堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
什么是类加载器?类加载器有哪些?
类加载器:JVM在运行时会产生三个默认的类加载器(引导类加载器、扩展类加载器、应用程序类加载器)。这三个加载器组成一个层级关系,每个类加载器负责加载不同作用域的class文件,有时开发者也会自定义一些类加载器来加载自己的class文件。
- 启动类加载器:加载/lib路径下的核心类库
- 扩展类加载器:加载/lib/extl路径下的
- 应用类加载器:加载系统类路径classpath,也就是我们经常用到的
- 自定义类加载器:加载/lib路径下的核心类库扩展类
双亲委派机制
1.如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
2.如果父类加载器还存在父类加载器,则进一步向上委托,一次递归,请求最终将到达顶层的启动类加载器
3.如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此类加载任务,子加载器才会常识自己去加载,这就是双亲委派模式+
优势:
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
ps:在jvm中表示两个class对象是否为同一个类存在两个必要条件:
- 类的完整类名名必须一致,包括包名
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
只要这两个条件不满足其一,就是两个不相同的类
5. 类的生命周期
类的生命周期包括这几个部分,加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程,如下图
- 加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象
- 连接,连接又包含三块内容:验证、准备、初始化。
- 1)验证,文件格式、元数据、字节码、符号引用验证;
- 2)准备,为类的静态变量分配内存,并将其初始化为默认值;
- 3)解析,把类中的符号引用转换为直接引用
- 初始化,为类的静态变量赋予正确的初始值
- 使用,new出对象程序中使用
- 卸载,执行垃圾回收
6. 对象的创建过程
对象的创建过程包括这几个部分,new指令、类加载检测、对象内存分配、值初始化,设置对象头,执行init,如下图
7. 引用的分类
- 强引用:任何时候都不会被回收,即使报了OOM错误。清除方法需要手动将对象置null,如调用List的clear方法等
- 软引用:soft,内存不够时,为了避免oom问题,会回收软引用对应的对象。
- 弱引用:weak,无论内存是否足够,只要发生了垃圾回收,就会进行回收。
- 虚引用:phantom,任意时候都会被回收。
8. JVM命令行工具
9. OOM排查
- 先查看应用进程号pid: ps -ef | grep 应用名
先查看垃圾回收情况: jstat -gc pid 5000(时间间隔)- dump 查看方法栈信息:jstack -l pid > /home/test/jstack.txt
- dump 查看JVM内存分配以及使用情况:jmap -heap pid > /home/test/jmapHeap.txt
使用说明:
当 JVM 发生 OutOfMemoryError(OOM)时,可以结合使用 jstat、jstack和jmap等工具来定位问题。下面是使用这些工具的一般步骤:
- 使用 jstat 监控 JVM 内存和垃圾回收情况:
- 运行命令
jstat -gcutil <pid> <interval> <count>
,其中<pid>
是 JVM 进程的 ID,<interval>
是监控时间间隔,<count>
是监控次数。- 可以观察 JVM 的垃圾回收情况,查看各个内存区域的使用情况,以及垃圾回收的次数和时间等信息。
- 使用 jstack 获取 JVM 线程信息:
- 运行命令
jstack <pid>
,其中<pid>
是 JVM 进程的 ID。- jstack 会打印出当前 JVM 进程中的线程堆栈信息,可以查看每个线程的状态、调用堆栈,以及锁的情况。
- 可以通过查看线程的堆栈信息,找到可能导致内存泄漏或者死锁的问题。
- 使用 jmap 生成 JVM 的内存快照:
- 运行命令
jmap -dump:format=b,file=<dumpfile> <pid>
,其中<dumpfile>
是生成的内存快照文件的路径和文件名,<pid>
是 JVM 进程的 ID。- jmap 会生成一个二进制文件,包含了 JVM 进程的堆内存快照。
- 可以使用工具如 Eclipse Memory Analyzer(MAT)等来分析这个内存快照文件,查找内存泄漏和大对象等问题。
通过使用上述工具,可以从不同角度了解 JVM 的内存和线程情况,从而帮助定位 OutOfMemoryError 的问题。根据具体的问题场景,可以进一步分析并采取相应的解决措施,如调整 JVM 参数、修复内存泄漏、优化代码等。
场景案例:
- 使用 jstat 监控 JVM 内存和垃圾回收情况:
- 场景:你的应用在运行一段时间后出现频繁的垃圾回收,并且观察到老年代内存占用逐渐增加。
- 方案:运行命令
jstat -gcutil <pid> <interval> <count>
,观察老年代使用情况。- 结果:你发现老年代使用率持续增加,垃圾回收次数频繁,说明可能存在内存泄漏问题。
- 使用 jstack 获取 JVM 线程信息:
- 场景:你的应用在运行时出现了卡顿,无法响应请求。怀疑是死锁引起的。
- 方案:运行命令
jstack <pid>
,查看线程堆栈信息。- 结果:在线程堆栈信息中,你发现多个线程都在等待获取某个锁资源,而这些锁资源被其他线程占用。这表明出现了死锁,其中一个线程正在等待被占用的锁资源,导致应用无法继续执行。
- 使用 jmap 生成 JVM 的内存快照:
- 场景:你的应用在运行一段时间后出现了内存溢出错误,且持续增长。
- 方案:运行命令
jmap -dump:format=b,file=<dumpfile> <pid>
,生成内存快照文件。- 结果:使用工具如 Eclipse Memory Analyzer(MAT)分析内存快照文件后,你发现大量对象无法释放,存在内存泄漏。分析对象的引用关系,定位到具体的代码位置,并进行相应的修复,解决内存泄漏问题。
这些场景仅是示例,实际情况可能更加复杂。使用 jstat、jstack和jmap等工具可以帮助你从多个角度定位和解决 JVM 的内存和线程相关问题,但需要结合具体情况和其他诊断工具的结果进行综合分析。
10. 垃圾回收算法
https://blog.51cto.com/u_9177933/2978290
https://www.cnblogs.com/1549983239yifeng/p/14673428.html
什么是垃圾回收?
垃圾收集是回收以前分配的内存的机制, 以便将来的内存分配可以重用内存。在大多数语言中, 垃圾收集是自动化的。不需要自己释放内存。
哪些内存需要回收?
回收堆内存:
可达性分析(Reachability Analysis)的基本思路:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路成为引用链(Reference Chain),当一个 对象到GC Roots没有任何引用链相联(不可达),则证明这个对象是不可用的,所以它们将会被判定为是可回收的对象。
回收方法区–判断是否废弃的条件:
1、该类的所有实例都已经被回收,即Java堆中不存在该类以及任何派生子类的实例。
2、加载该类的类加载器已经被回收。
3、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
什么时候回收?,
当Eden区或者S区不够用了,会执行一次MinorGC
当老年代空间不够用了,会执行一次FullGC
当方法区不够用了,会执行一次FullGC
如何回收?
根据垃圾回收器搭配算法进行判断垃圾、回收垃圾。
- 注意
JVM GC只回收
堆内存
和方法区内
的对象。而栈内存
的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内。我们需要记住一个单词:
stop-the-world
。它会在任何一种GC算法中发生。stop-the-world 意味着JVM因为需要执行GC而停止
应用程序的执行。当stop-the-world 发生时,除GC所需的线程外,所有的
线程
都进入等待
状态,直到GC任务完成。GC优化很多时候就是减少stop-the-world 的发生。
10.1.“标记–清除”算法
首先标记出所有需要被回收的对象,然后在标记完成后统一回收掉所有被标记的对象。
执行步骤:
标记:遍历内存区域,对需要回收的对象打上标记。
清除:再次遍历内存,对已经标记过的内存进行回收。
图解:
缺点:
效率问题;遍历了两次内存空间(第一次标记,第二次清除)。
空间问题:容易产生大量内存碎片,当再需要一块比较大的内存时,无法找到一块满足要求的,因而不得不再次出发GC。
10.2.“复制算法”算法
将内存划分为等大的两块,每次只使用其中的一块。当一块用完了,触发GC时,将该块中存活的对象复制到另一块区域,然后一次性清理掉这块没有用的内存。下次触发GC时将那块中存活的的又复制到这块,然后抹掉那块,循环往复。
图解:
优点
相对于标记–清理算法解决了内存的碎片化问题。
效率更高(清理内存时,记住首尾地址,一次性抹掉)。
缺点:内存利用率不高,每次只能使用一半内存。
改进:
研究表明,新生代中的对象大都是“朝生夕死”的,即生命周期非常短而且对象活得越久则越难被回收。在发生GC时,需要回收的对象特别多,存活的特别少,因此需要搬移到另一块内存的对象非常少,所以不需要1:1划分内存空间。而是将整个新生代按照8 : 1 : 1的比例划分为三块,最大的称为Eden(伊甸园)区,较小的两块分别称为To Survivor和From Survivor。
首次GC时,只需要将Eden存活的对象复制到To。然后将Eden区整体回收。再次GC时,将Eden和To存活的复制到From,循环往复这个过程。这样每次新生代中可用的内存就占整个新生代的90%,大大提高了内存利用率。【相关推荐:Java视频教程】
但不能保证每次存活的对象就永远少于新生代整体的10%,此时复制过去是存不下的,因此这里会用到另一块内存,称为老年代,进行分配担保,将对象存储到老年代。若还不够,就会抛出OOM。
老年代:存放新生代中经过多次回收仍然存活的对象(默认15次)。
10.3.“标记–整理”算法
因为前面的复制算法当对象的存活率比较高时,这样一直复制过来,复制过去,没啥意义,且浪费时间。所以针对老年代提出了“标记整理”算法。
执行步骤:
标记:对需要回收的进行标记
整理:让存活的对象,向内存的一端移动,然后直接清理掉没有用的内存。
图解:
10.4.分代收集算法(常用)
当前大多商用虚拟机都采用这种分代收集算法,这个算法并没有新的内容,只是根据对象的存活的时间的长短,将内存分为了新生代和老年代,这样就可以针对不同的区域,采取对应的算法。如:
新生代,每次都有大量对象死亡,有老年代作为内存担保,采取复制算法。
老年代,对象存活时间长,采用标记整理,或者标记清理算法都可。
MinorGC和FullGC的区别MinorGC:发生在新生代的垃圾回收,因为新生代的特点,MinorGC非常频繁,且回收速度比较快,每次回收的量也很大。
FullGC:发生在老年代的垃圾回收,也称MajorGC,速度比较慢,相对于MinorGc慢10倍左右。进行一次FullGC通常会伴有多次多次MinorGC。
11.垃圾回收器与垃圾
这些算法只是天上飞的理念,是一种方法论,但是真正的垃圾回收还需要有落地实现,所以垃圾回收器应运而生。
jvm的垃圾回收器大体上的分类主要包括四种:串行、并行、并发(CMS)和G1。
串行垃圾回收器(Serial):它为单线程环境设计并且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环境。
并行垃圾回收器(Parallel):多个垃圾回收线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理等弱交互场景。
并发垃圾回收器(CMS):用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程。互联网公司多用它,适用于对响应时间有要求的场景。
G1垃圾回收器:G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收。新生代可配置的回收器:Serial、ParNew、Parallel Scavenge
老年代配置的回收器:CMS、Serial Old、Parallel Old
新生代和老年代区域的回收器之间进行连线,说明他们之间可以搭配使用。
11.1.Serial 垃圾回收器
Serial收集器是最基本的、发展历史最悠久的收集器。俗称为:
串行回收器
,采用复制算法
进行垃圾回收特点
串行回收器是指使用单线程进行垃圾回收的回收器。每次回收时,串行回收器只有一个工作线程。
对于并行能力较弱的单CPU计算机来说,串行回收器的专注性和独占性往往有更好的性能表现。
它存在Stop The World问题,即垃圾回收时,要停止程序的运行。
使用
-XX:+UseSerialGC
参数可以设置新生代使用这个串行回收器
11.2.ParNew 垃圾回收器
ParNew其实就是Serial的并行版本,除了使用了多线程以外,其余参数都一模一样。俗称为:
并行回收器
,采用复制算法
进行垃圾回收特点
ParNew默认开启的线程数与CPU数量相同,如果需要进行性能调优(根据CPU与线程的最优搭配:(线程等待时间+线程cpu时间)/线程cpu时间) * cpu数量)
可以通过参数
-XX:ParallelGCThreads
来设置线程数。它是目前新生代首选的垃圾回收器,因为除了ParNew之外,它是唯一一个能与老年代CMS配合工作的。
它同样存在Stop The World问题
使用
-XX:+UseParNewGC
参数可以设置新生代使用这个并行回收器
11.3.ParallelGC 垃圾回收器
ParallelGC使用复制算法回收垃圾,也是多线程的。
特点
就是非常关注系统的吞吐量,
吞吐量
=代码运行时间
/(代码运行时间
+垃圾收集时间
)
-XX:MaxGCPauseMillis
:设置最大垃圾收集停顿时间,可以把虚拟机在GC停顿的时间控制在MaxGCPauseMillis范围内,如果希望减少GC停顿时间可以将MaxGCPauseMillis设置的很小,但是会导致GC频繁
,从而增加了GC的总时间
,降低
了吞吐量
。所以需要根据实际情况设置该值。
-Xx:GCTimeRatio
:设置吞吐量大小,它是一个0到100之间的整数,默认情况下他的取值是99
,那么系统将花费不超过1/(1+n)
的时间用于垃圾回收,也就是1/(1+99)=1%
的时间。另外还可以指定
-XX:+UseAdaptiveSizePolicy
打开自适应模式,在这种模式下,新生代的大小、eden、from/to的比例,以及晋升老年代的对象年龄参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。使用-XX:+UseParallelGC参数可以设置新生代使用这个并行回收器
11.4.ParallelOldGC 垃圾回收器
老年代
ParallelOldGC
回收器也是一种多线程的回收器,和新生代的ParallelGC回收器一样,也是一种关注吞吐量的回收器,他使用了标记压缩算法
进行实现。
-XX:+UseParallelOldGc
进行设置老年代使用该回收器
-XX:+ParallelGCThreads
也可以设置垃圾收集时的线程数量。
11.5.CMS 垃圾回收器
CMS全称为:Concurrent Mark Sweep意为并发标记清除,他使用的是
标记清除法
。主要关注系统停顿时间。使用
-XX:+UseConcMarkSweepGC
进行设置老年代使用该回收器。使用
-XX:ConcGCThreads
设置并发线程数量。特点
CMS并不是独占的回收器,也就说CMS回收的过程中,应用程序仍然在不停的工作,又会有新的垃圾不断的产生,所以在使用CMS的过程中应该确保应用程序的内存足够可用。
CMS不会等到应用程序
饱和
的时候才去回收垃圾,而是在某一阀值的时候开始回收,回收阀值可用指定的参数进行配置:-XX:CMSInitiatingoccupancyFraction
来指定,默认为68
,也就是说当老年代的空间使用率
达到68%
的时候,会执行
CMS回收。如果内存使用率增长的很快,在CMS执行的过程中,已经出现了内存不足的情况,此时CMS回收就会失败,虚拟机将启动老年代
串行
回收器;SerialOldGC
进行垃圾回收,这会导致应用程序中断,直到垃圾回收完成后才会正常工作。这个过程GC的停顿时间可能较长,所以
-XX:CMSInitiatingoccupancyFraction
的设置要根据实际的情况。之前我们在学习算法的时候说过,标记清除法有个缺点就是存在
内存碎片
的问题,那么CMS有个参数设置-XX:+UseCMSCompactAtFullCollecion
可以使CMS回收完成之后进行一次碎片整理
。
-XX:CMSFullGCsBeforeCompaction
参数可以设置进行多少次CMS回收之后,对内存进行一次压缩
。
11.6.G1 垃圾回收器
G1适用于全堆,既可以在新生代使用和老年代使用。G1与之前的收集器有很大的不同,是从不同的角度去设计的。
回想下之前的垃圾收集器的特点:
1.年轻代和老年代都是各自独立的连续的内存块。
2.年轻代Eden+from+to使用复制算法
3.老年代的收集必须扫描全部老年代内存空间。
4.都是以尽可能少而快速地执行GC为设计原则G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:
1、G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
2、G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还是存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留
CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器——G1垃圾收集器。G1是在2012年才在jdk1.7u4中可用。 oracle官方计划在jdk9中将G1变成默认的垃圾收集器以替代CMS。它是一 款面向服务端应用的收
器,主要应用在多CPU和大内存服务器环境下,极大的减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换java8以前的CM:
集器。主要改变是Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个大小一样的region ,每个region从1M到32M不等。- - 个region有可能属于Eden, Survivor或 者Tenured内存区域。
底层原理
G1的最大好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可。
G1收集器大致可分为如下步骤:
**初始标记:**仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿,但耗时很短。)
**并发标记:**从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)
**最终标记:**为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。且对象的变化记录在线程Remembered Set Logs里面,把Remembered Set Logs里面的数据合并到Remembered Set中。(需要线程停顿,但可并行执行。)
**筛选回收:**对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(可并发执行)
12.常见的JVM调优场景
- 堆内存调优:通过调整堆内存大小来确保应用程序有足够的内存,并避免过多的垃圾回收。可以通过-Xmx和-Xms参数来设置堆内存大小。
- GC调优:选择合适的垃圾回收算法和调整垃圾回收器的参数,以提高垃圾回收的效率。可以通过参数如-XX:+UseG1GC或-XX:+UseConcMarkSweepGC来选择垃圾回收算法。
- 线程调优:合理配置线程池的大小,以避免线程过多导致的内存和CPU消耗过高。可以通过参数如-Xss和-XX:ParallelGCThreads来调整线程的大小和数量。
- 类加载调优:通过预加载和缓存类的方式来优化类加载性能。可以通过参数如-XX:+UseFastClassIndex和-XX:+UseFastEmptyMethods来优化类加载。
- JIT调优:通过设置适当的编译器参数来调整JIT编译器的行为,以提高应用程序的性能。可以通过参数如-XX:CompileThreshold和-XX:+PrintCompilation来调整JIT编译器的阈值和输出详细的编译信息。
- IO调优:使用合适的IO库和配置,以提升IO操作的效率。可以通过参数如-XX:+UseCompressedOops和-XX:+UseCompressedStrings来启用压缩对象指针和字符串,以减少内存消耗。
- 配置文件调优:根据应用程序的需求和硬件环境,调整JVM的配置文件,以获取最佳的性能和资源利用率。
二、JAVA SE
1.基础数据类型与引用数据类型
存储上的区别
1.基本数据类型是存放在栈中的简单数据段。
2.引用数据类型是存放在堆内存中的对象,在栈内存中存放的是堆内存中具体内容的引用地址,通过这个地址可以快速查找到对象。
比较上的区别
1.基本数据类型的比较是值的比较
var a = ‘{}’
var b = ‘{}’
console.log(a == b) //true
直接比较值,看起来一样那么他就是一样的。
2.引用类型的比较是引用的比较
var a = {}
var b = {}
console.log(a == b) //false
赋值上的区别
1.基本数据类型的赋值是简单赋值,如果一个变量向另一个变量赋值基本类型的值,会在变量对象上创建一个新值,然后把这个值复制到为新变量分配的位置上。
var a = 10
var b = a
a++
console.log(a) //11
console.log(b) //10
a在内存中保存的值是10,当使用a的值来初始化b时,b中也保存了10,这两个值是完全独立的,相当于b把a的值复制了一份,所以两个变量的任何操作都不会互相影响。
2.引用类型的赋值是对象引用
var a = {}
var b = a
a.name = “haha”
console.log(a.name) //haha
console.log(b.name) //haha
b.age = 29
console.log(a.age) //29
console.log(b.age) //29
当一个变量向另一个变量赋值引用类型的值时,同样也会将栈内存中的值复制一份放到新变量分配的空间中,但是引用类型保存在栈内存中的变量是一个地址,这个地址指向的是堆内存中的对象,所以这个变量其实复制了一个地址,两个地址指向同一个对象,改变其中任何一个变量都会互相影响。
※字符串的存储和不可变性
- 字符串池(String Pool):字符串字面量会被存储在字符串池中,这是一块特殊的内存区域,用于优化内存使用和性能。
- 不可变性:字符串是不可变的对象。一旦创建,它的内容就不能改变。任何对字符串的修改操作都会生成一个新的字符串对象,而不会修改原来的字符串对象。
2. 浅拷贝与深拷贝
https://blog.youkuaiyun.com/i_silence/article/details/106457958
栈内存
中存储的都是基本数据类型
,堆内存
中存储了引用数据类型
等。**浅拷贝:**只拷贝
栈内存
中的数据,不拷贝堆内存
中数据;对于栈中的数据,拷贝对象与原对象分别指向不同的值;对于堆中的数据,拷贝对象与原对象都指向同一个实例值。
- for循环拷贝
- .clone()
- System.arraycopy()
- Arrays.copyOf()
**深拷贝:**既拷贝
栈内存
中的数据,又拷贝堆内存
中的数据,拷贝对象与原对象分别指向不同的实例值。
- 通过原始构造方法set注入属性;
- 实现Cloneable接口重写clone方法,先将对象进行浅拷贝,再对非基础类型的值进行set注入。
- 实现Serializable
3.数据结构
1.1 Iterator
1.2 List,Set,Map的区别
三者都属于集合,用来存储元素。
- List存储元素有序可重复。
- Set存储元素无序不可重复,set的实现为HashMap的子集。
- Map存储键值对形式的数据,键Key可为空,不可重复,值value可重复。
1.2 HashMap
https://www.cnblogs.com/zengcongcong/p/11295349.html
- 谈谈HashMap
hashMap是基于哈希表的Map接口的实现。有几个重要的参数,加载因子和初始大小,初始容量为16,扩容方式为2N,加载因子默认0.75,当 HashMap 中的元素数量超过其容量乘以负载因子时(默认值为 0.75),它就会自动扩容。JDK1.7前以数组加链表实现,JDK1.8后加入了红黑树,当数组长度大于等于 64 且链表长度大于 8 时会将链表转换成红黑树。当调用set(key,val)方法时,会获取key的hash值,再通过类似(hash % array.length)的计算方式计算出应该存放在数组中哪个链表的索引位置,再然后判断该位置是否已存在链表,不存在则直接将新元素放到这个位置,存在则判断该链表中是否存在相同的key,存在则更新值,不存在则从链表尾部插入;当调用get(key)方法时,会将key hash出一个数组下标,根据在数组中的链表(红黑树)equals遍历是否相等,相等取出对应元素。- 1.8之前 在 扩容 resize 的时候会进行 rehash,在并发的情况下会导致链表闭环而导致死锁
- 尽量给 HashMap 设置初始化容量,否则会发生多次扩容影响性能
- JDK 1.7前 使用头插法,1.8后使用尾插法,主要为了解决 rehash 出现的死循环问题
1.3 ArrayList
ArrayList是基于动态数组的数据结构
因为是数组,所以ArrayList在初始化的时候,有名为elementData初始大小为0的数组,插入新元素的时候,会判断是否需要扩容,扩容后是1.5倍原容量,扩容方式是利用数组的复制,因此有一定的开销;
另外,ArrayList在进行元素插入的时候,需要移动插入位置之后的所有元素,位置越靠前,需要位移的元素越多,开销越大,相反,插入位置越靠后的话,开销就越小了,如果在最后面进行插入,那就不需要进行位移;
ArrayList的扩容机制:
使用无参构造方法构建时,初始化赋值一个长度为
0
的空数组(elementData <-- DEFAULTCAPACITY_EMPTY_ELEMENTDATA);当执行add一个元素
进去后会判断当前数组是否等于初始数组,若相等则数组长度才扩大为10
,往后
每次add操作都会判断当前所需数组长度
是否大于
当前数组长度,若大于则执行1.5倍
扩容,在扩容时,会在hugeCapacity方法中判断新的长度是否大于Integer.MAX_VALUE - 8
,小于
则copy到新数组,大于
则抛出OOM
,然后将所需数组长度
与MAX_ARRAY_SIZE
进行比较,如果大于
则返回Integer.MAX_VALUE
,否则返回MAX_ARRAY_SIZE
.Integer.MAX_VALUE = 2,147,483,647(2.1亿)
JDK1.8的扩容机制是利用数组的复制,因此有一定的开销;通过二进制位移运算后,扩容后长度为原来的1.5倍;
- int oldCapacity = elementData.length;
- int newCapacity = oldCapacity + (oldCapacity >> 1);
位移运算:<< 表示左位移运算符,将整数的二进制向左移动指定的位数,并在右边用 0 填充。例如,想将整数 10 的二进制值 1010 向左移动 1 位,你可以使用表达式 x << 1;
>> 表示右位移运算符,将整数的二进制向右移动指定的位数,例如,将整数 10 的二进制值 1010 向右移动 1 位,你可以使用表达式 x >> 1,结果等于101,转为10进制等于5;
二进制:逢二进一,从右开始计算,01=1 ,此时再进来一个1就会变成02,但再二进制中逢二进1,变成10. 倒数第一位是个位数,倒数第二位是2的2-1次方,倒数第n位是2的N-1次方;
二进制转十进制:将二进制中每一位的值相加。
所以在>>位移运算符中,移动多少个位置就是减少多少个2的幂次方,并且最后一位肯定是一个1或0,肯定是一个整数。<<同理。
线程安全问题:
- fail-fast:发现遍历的同时其他人来修改,立即抛出错误(ConcurrentModificationExecption)。
- fail-safe:发现遍历的同时其他人来修改,应当有应对策略,比如牺牲数据一致性保证遍历的正常执行。
fail-fast的实现原理
当进行ArrayList遍历的时候,会先创建一个迭代器。 其中,cursor为下一个元素的返回值,lastRet为最后一个元素的索引,-1表示结束。modCount是集合被修改的次数。expectedModCount为迭代器修改次数,迭代器初始化时会等于modCount。当迭代器执行next()时,会调用一个CheckForComdification()方法判断,这个方法会比较expectedModCount和modCount的值,如果两者不相等,证明集合在遍历期间元素发生了修改,这时候会抛出ConcurrentModificationException异常。
fail-safe的实现原理
当CopyOnWriteArrayList进行遍历的时候,会先创建一个迭代器。这个时候,会现将原来的数组复制一份放至snapshot中。cursor是遍历时的元素下标,当cursor <snapshot.length时,表明仍有下一个元素,也就是hashNext方法的实现原理。CopyOnWriteArrayList在添加元素的时候,是先获取当前的元素,然后List大小加1,将新元素放入其中。但是,CopyOnWriteArrayList在遍历的时候,是对snapshot进行遍历,所以遍历的时候List的元素发生了改变,并不会对当前遍历造成影响,因此也不会报错。
小结
1.ArrayList就是fail-fast典型的代表,遍历的同时不能修改,一旦发生修改则尽快失败。
2.CopyOnWriteArrayList是fail-last的典型代表,遍历时同时可以修改,原理是读写分离。
1.4 LinkedList
LinkedList是基于链表的数据结构
LinkedList有一个内部类作为存放元素的单元,里面有三个属性,用来存放元素本身以及前后2个单元的引用,另外LinkedList内部还有一个header属性,用来标识起始位置,LinkedList的第一个单元和最后一个单元都会指向header,因此形成了一个双向的链表结构。
LinkedList是采用双向链表实现的。所以它也具有链表的特点,每一个元素(结点)的地址不连续,通过引用找到当前结点的上一个结点和下一个结点,即插入和删除效率较高,只需要常数时间,而get和set则较为低效。
LinkedList的方法和使用和ArrayList大致相同,由于LinkedList是链表实现的,所以额外提供了在头部和尾部添加/删除元素的方法,也没有ArrayList扩容的问题了。另外,ArrayList和LinkedList都可以实现栈、队列等数据结构,但LinkedList本身实现了队列的接口,所以更推荐用LinkedList来实现队列和栈。
ArrayList在逻辑上和物理上都是连续的,因此占用内存中的连续内存,LinkedList在逻辑上是连续的,但物理上不一定连续,因此可能会因为元素的删除而产生更多更零散的内存碎片。
1.4 ArrayList和LinkedList的区别
ArrayList和LinkedList的区别有以下几点:
- ArrayList是实现了基于动态数组的数据结构,而LinkedList是基于链表的数据结构;
- 对于随机访问元素,Array获取数据的时间复杂度是O(1),但是要删除数据却是开销很大的,因为这需要重排数组中的所有数据。ArrayList想要get(int index)元素时,直接返回index位置上的元素,而LinkedList需要通过for循环进行查找,虽然LinkedList已经在查找方法上做了优化,比如index < size / 2,则从左边开始查找,反之从右边开始查找,但是还是比ArrayList要慢。
- 对于添加和删除操作add和remove,LinkedList是更快的。因为LinkedList不像ArrayList一样,不需要改变数组的大小,也不需要在数组装满的时候要将所有的数据重新装入一个新的数组,这是ArrayList最坏的一种情况,时间复杂度是O(n),而LinkedList中插入或删除的时间复杂度仅为O(1)。ArrayList在插入数据时还需要更新索引(除了插入数组的尾部)。 ArrayList想要在指定位置插入或删除元素时,主要耗时的是System.arraycopy动作,会移动index后面所有的元素;LinkedList主耗时的是要先通过for循环找到index,然后直接插入或删除。这就导致了两者并非一定谁快谁慢。
适用场景
很多场景下都是ArrayList更受欢迎。但有些情况下LinkedList更为合适,比如:
\1) 你的应用不会随机访问数据。因为如果你需要LinkedList中的第n个元素的时候,你需要从第一个元素顺序数到第n个数据,然后读取数据。
\2) 你的应用有更多的插入和删除元素操作,更少的读取数据。因为插入和删除元素不涉及重排数据,所以它要比ArrayList要快。
以上就是关于ArrayList和LinkedList的差别。你需要一个不同步的基于索引的数据访问时,请尽量使用ArrayList。ArrayList很快,也很容易使用。但是要记得要给定一个合适的初始大小,尽可能的减少更改数组的大小
1.5. 如何判断两个单向链表相交
首先要搞清楚单链表相交的概念和特点:指的是他们存在完全重合的部分,不是交叉到一个点(因为每一个节点最多只能有一个下一节点,因此在相交节点之后,链表不可能再分为两个链表)
判断其是否相交的方法有以下几种。
*(1)最传统方法*
双重循环,依次查找,是否有地址相同的节点,时间复杂度O(list1.len * list2.len),空间复杂度O(1)。
*(2)hash法*
先遍历第一个链表,将每个节点的地址hash并保存,然后遍历第二个链表的节点并计算hash值,遇到相同的hash值且地址相同则判断为相交,且第一次hash地址相同的节点必为相交的第一个节点。时间复杂度O(list1.len + list2.len),空间复杂度O(ist1.len)。
*(3)根据尾节点判断*
因为两个链表相交必然从某个节点开始往后完全重合。遍历两个链表至尾节点,判断两个尾节点是否相同即可得到结论。O(list1.len + list2.len),空间复杂度O(1)。
*(4)其中一个首尾相接*
将其中一个首尾相接,遍历另一个链表,如果能到达第一个链表的首部,则表明相交。O(list1.len + list2.len),空间复杂度O(1)。
*(5)计算相差的长度找第一个相交节点*
分别遍历两个链表,记录长度(若已知则不需此步),短链表从头结点开始,长链表从第| list1.len - list2.len | 个节点开始,依次遍历并比较,相等的第一个节点则为相交的第一个节点。也可以使用hash法如(2)所示。
4. 抽象(Abstract)和接口(interface)的区别
abstract
如果一个class类定义了方法,但没有具体的方法体,这种方法就是抽象方法,抽象方法使用abstract关键字来修饰。抽象方法只能在抽象类里面,抽象类无法进行实例化,只能通过别的类通过 implement 来实现抽象类。
interface
接口就是比抽象还要更加抽象的代码结构。他不能有字段,只能包含常量、默认方法等,接口的的所有方法默认都是public abstract,因此这两个修饰符写不写都一样。总结:
抽象
和接口
的区别在于一个类只能继承一个抽象类,而一个接口可以继承多个接口(接口直接允许多重继承
);抽象类
可以有构造方法,而接口
不允许有构造方法;抽象类
可以使用任意访问权限修饰符,而接口
只能使用public访问权限修饰符;
5. implement与extends的区别(实现与继承)
implements
关键字用于类实现接口。一个类可以实现多个接口,通过使用逗号分隔。类实现接口时,需要实现接口中定义的所有抽象方法,并且可以选择性地实现接口的默认方法。接口提供了一种“约定”,实现类必须满足接口中定义的所有方法。
extends
关键字用于类与类之间的继承关系。一个类只能继承一个父类(单继承),但可以通过继承传递形成一个类的继承层级。子类继承父类时,继承了父类的属性和方法,并且可以通过覆盖(重写)父类中的方法,实现自己的逻辑。总结:
implements
用于类实现接口,类需要实现接口中定义的所有方法;extends
用于类继承父类,子类继承了父类的属性和方法,并且可以通过重写父类方法来实现自己的逻辑。在设计中,
implements
和extends
可以使类和接口、类之间建立起继承或实现的关系,使得代码具有更好的层次结构、扩展性和重用性。
三、线程
https://blog.youkuaiyun.com/qq_36387471/article/details/105479238
1. 线程池的实现原理:
提交任务到线程池,判断线程池中线程数是否达到核心线程数设定值,否则创建线程并对线程加全局锁,是则判断是否达到最大线程数,如果没达到最大线程数则添加任务到阻塞队列等待可用线程,如果达到最大线程数,则执行饱和策略。
2. 线程池七个核心参数?
**创建线程池有两种方法,分别是Executors工厂方法创建和new ThreadPoolExecutor()自定义创建
Executors提供四种线程池,分别为可缓存线程池、定长线程池、周期线程池、单线程化的线程池
【线程池核心参数】
核心线程数:
机器CPU核数:根据机器
操作类型:分为CPU密集型和IO密集型
CPU密集型:就是你的运算逻辑大部分是CPU操作,IO比较少,这里的IO可以分为磁盘IO和网络IO。
IO密集型:就是你的运算逻辑大部分是IO,而CPU运算的比较少。
实际在生产中并没有完全按照这个计算公式来展开,而是按照公式估算,然后会往上调或者往下调,多次实践取最优值最大线程数:
当最大线程数 大于 核心线程数,如果对列满的情况,则线程数会新增,增加到最大线程数一样。
阻塞对列大小:也是很重要的参数,切记无可设置无限大。一般是1024或者512,需要慢慢的不断调大,是把双刃剑
(keepAliveTime)线程最大等待的空闲时间:
这个参数仅仅只有当最大线程数 大于 核心线程数 的时候,如果线程超过时间还没有被使用,就会被回收。
keepAliveTime的实现方式,从工作队列 poll 任务时,加上了超时限制,如果线程在 keepAliveTime 的时间内 poll 不到任务,那我就认为这条线程没事做,可以干掉了
3. 创建线程有几种方式?
①. 继承Thread类
②. 实现Runnable接口
③. 通过Callable和Future创建线程
④. 通过线程池创建线程;
4. 线程有哪些状态?
①. 创建状态
②. 就绪状态
③. 运行状态
④. 阻塞状态
⑤. 死亡状态
5. sleep()和wait()的区别?
sleep()方法是Thread类的静态方法,是线程用来控制自身流程的。而wait()方法是Object类的方法,用于线程间的通信。
调用wait()的时候方法会释放当前持有的锁,而sleep方法不会释放锁。
① sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
② 调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
6. 怎么保证多线程的运行安全【并发编程三要素】
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作(atomic,synchronized);
可见性:一个线程对主内存的修改可以及时被其他线程看到(synchronized);
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序(happens-before原则);
7. volatile
https://www.finclip.com/news/f/56043.html
7. Synchronized
https://www.cnblogs.com/aspirant/p/11470858.html
7. ReentrantLock
https://www.bilibili.com/video/BV1D54y1H7Bh?p=5&vd_source=709265081bd4c68f0764abc8857d9693
7. Synchronized与ReentrantLock的区别
https://zhuanlan.zhihu.com/p/505977223
小结
synchronized 和 ReentrantLock 都是 Java 中提供的可重入锁,二者的主要区别有以下 5 个:
- 用法不同:synchronized 可以用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用于代码块。
- 获取锁和释放锁的机制不同:synchronized 是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁。
- 锁类型不同:synchronized 是非公平锁,而 ReentrantLock 默认为非公平锁,也可以手动指定为公平锁。
- 响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
- 底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。
8. 类锁与对象锁的区别
https://zhuanlan.zhihu.com/p/127171227
9. 死锁是什么?怎么防止死锁
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
10. Threalocal
https://blog.youkuaiyun.com/h2604396739/article/details/83033302
四、Spring
1. IoC 控制反转
IoC是一种设计思想,在java中将设计好的对象交给IoC容器控制,IoC容器会在你需要的时候给你相应的对象。
- Spring 容器使用依赖注入来管理组成应用程序的 Bean 对象。
- 容器通过读取提供的配置元数据 Bean Definition 来接收对象进行实例化,配置和组装的指令。
- 该配置元数据 Bean Definition 可以通过 XML,Java 注解或 Java Config 代码提供。
依赖注入有哪几种方式?
- 接口注入
- 构造函数注入
- setter注入
Spring中有多少种IoC容器?
- BeanFactory:就像一个包含Bean集合的工厂类。它会在客户端要求时实例化Bean对象。
- ApplicationContext:扩展了BeanFactory接口,它在BeanFactory基础上提供了一些额外的功能。
IoC的好处:
- 可以资源集中管理,实现资源的可配置和易管理。
- 降低耦合度。
- 减少对象的创建和内存消耗
- 使得整个程序的维护性、扩展性、灵活性变高。
2. AOP 面向切面
什么是AOP?
AOP是Spring中的一种面向切面编程思想,通过代理将代码切入到类的指定方法、指定位置上的一种技术。
AOP的相关概念
Aspect:切面,切入系统的一个切面。比如事务管理是一个切面,权限管理也是一个切面。
Join point:连接点,也就是可以进行横向切入的位置。
Advice:通知,切面在某个连接点执行的操作(分为: Before advice , After returning advice , After throwing advice , --------------After (finally) advice , Around advice );
pointcut:切点,符合切点表达式的连接点,也就是真正被切入的地方;
3. ★Spring Bean
Bean的生命周期:
- 根据配置元数据实例化Bean对象==》Spring使用依赖注入填充所有属性==》Aware相关的属性注入到Bean对象==》调用相应的方法,进一步初始化Bean对象==》调用destroy方法销毁容器
Spring Bean 怎么解决循环依赖的问题?
首先造成循环依赖主要是由于模块设计时没有充分考虑各模块职责划分导致的,在模块设计的时候就应该避免这个问题。清除各个模块功能职责,建立有层次,自下而上的依赖关系。
出现循环依赖的原因:Bean a 依赖 Bean b ,a 未完成实例化, 开始查找 b。b没有被找到,容器则会去创建,在创建 b 过程中,又发现 b 依赖 a。
spring使用三级缓存来解决依赖循环问题,三级缓存其实就是利用了三个map加一个创建标识。
实例化A的时候,会去Bean容器中创建Bean A,去一级缓存中找是否存在A的实例对象,不存在则判断A是否在创建中(第一次进来是不会在创建中的),如果A在创建中,则去二级、三级缓存中找。没找到则创建单例对象A,并把早期对象A暴露到三级缓存中,标记A创建中。为A注入依赖,发现A依赖B,则创建B,同上。为B注入依赖时也发现B依赖A,则去创建A,此时,在一级缓存中依然拿不到A的实例对象,因为A还不是一个完整的对象(没完成依赖注入及后续处理),因为此时A被标识为创建中,可以去二级缓存中拿,拿不到再去三级缓存中拿,上面已经把A存放到了三级缓存中,拿到A的早其对象后注入到B,B完成实例化后放到一级缓存,并从二级、三级缓存中清空,实例化后完整的B注入到A中,同上,A放到一级缓存清空二三级缓存。
除了靠Spring三级缓存解决以外,我们还可以加@lazy注解来延迟依赖的加载,或者使用setter注入。
注解分为两类
1、一类是使用Bean,即是把已经在xml文件中配置好的Bean或者代码中添加了@Bean的对象实例拿来用,完成属性、方法的组装;比如**@Autowired , @Resource**,可以通过**byTYPE(@Autowired)、byNAME(@Resource)**的方式获取Bean;
2、一类是注册Bean,@Component , @Repository , @ Controller , @Service , @Configration这些注解都是把你要实例化的对象转化成一个Bean,放在IoC容器中,等你要用的时候,它会和上面的**@Autowired , @Resource**配合到一起,把对象、属性、方法完美组装。
两种方式的区别:
- @Component注解作用于类上,而@Bean注解作用于配置类中的某一个方法上;
- @Component表明告知Spring为当前的这个类创建一个bean,而@Bean表明告知Spring此方法将会返回一个对象,将返回的对象注入到容器中。
bean的实战应用与使用场景
https://www.cnblogs.com/yayuya/p/17379856.html
B模块导入了A模块,springboot启动B模块,A模块作为公共包不启动,springboot会扫描B模块中的配置类中的注解配置,但由于A模块没有启动且B模块类中没有继承A模块的EmployeeRolePath类,所以B模块中EmployeeRolePath的注解配置没有被扫描,所以A模块调B模块EmployeeRolePath中的get uri不会有值。如果在A模块中使用@Bean注入EmployeeRolePath类,那么EmployeeRolePath类也会被注入到spring容器中,则在B模块启动的时候就会扫描到EmployeeRolePath中的注解配置。
@Bean public EmployeeRolePath employeeRolePath() { return new EmployeeRolePath(); }
其他应用场景:org.springframework.security.config.annotation.web.reactive.ServerHttpSecurityConfiguration#setAuthenticationManager
spring提供的ServerHttpSecurityConfiguration类下面就有一些使用了@Autowired去注入Bean,使用这些类的这些属性的时候就需要在自己模块中去定义这些Bean。
ps:
同一个包下,没有实现类的接口不会被加入到spring容器中。
4. 代理
- 什么是动态代理?
动态代理是在程序运行时通过反射机制操作字节码,从而动态的创建 字节码文件,进而创建代理对象。- jdk动态代理和cglib动态代理的区别:
jdk:先有实现接口的类的对象,然后对对象进行代理
cglib:只要有类(无论是否实现接口),对类做代理,简单来说就是对指定的类生成一个子类,覆盖其中的方法(继承)- jdk动态代理和cglib动态代理在spring中:
当Bean实现接口时,Spring默认使用jdk动态代理
当Bean没有实现接口时,Spring使用cglib动态代理
如果Bean实现接口,也可以强制使用cglib动态代理(在spring配置中加入)场景扩展:java项目中,为什么在同一个类中调用方法不会触发@Cacheable注解??
在同一个类中调用方法不会触发
@Cacheable注解的原因是,
@Cacheable 注解通常是在利用AOP(面向切面编程)技术,结合动态代理来实现的。当您通过代理对象调用被代理方法时,AOP会根据注解的配置,在方法执行前检查缓存,并决定是否执行缓存逻辑。然而,当您在同一个类中直接调用方法时,并没有经过代理,因此缓存相关的逻辑也不会触发。要在同一个类中触发
@Cacheable
注解的逻辑,您可以将带有缓存注解的方法抽取到一个单独的类(通常是一个Service或者Component)中,并在原类中引用该类并调用相应的方法。确保在调用时,是通过代理对象调用,而不是直接调用。例如:
// 缓存相关的方法定义在CacheService类中 @Service public class CacheService { @Cacheable("myCache") public String getValue(String key) { // 根据key获取值的逻辑... } } // 调用缓存相关方法的类 public class MyClass { private CacheService cacheService; // 引用CacheService public MyClass(CacheService cacheService) { this.cacheService = cacheService; } public void doSomething() { // 通过代理对象缓存逻辑将会被触发 String value = cacheService.getValue("key"); } }
在上面的示例中,
CacheService
类包含了带有@Cacheable
的方法getValue()
。MyClass
类引用了CacheService
并调用了doSomething()
方法,在这个方法中通过代理对象调用了getValue()
方法。因为经过了代理,所以缓存相关的逻辑将会被触发。请确保在调用带有
@Cacheable
注解的方法时,通过代理对象进行调用,这样才能触发缓存的相关逻辑。
5. Spring 事务
什么是事务?
- 数据库事务(简称:事务,Transaction)是指数据库执行过程中的一个逻辑单位,由一系列数据库操作组成的一个完整的逻辑过程。
事务的特性 ACID:
原子性:事务作为一个整体被执行,事务中对数据库的操作要么全部完成,要么都不完成。事务执行过程中发生错误,会被回滚到事务开始前的状态。
一致性:类似“质量守恒定律”,在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
隔离性:一个事务的执行不能干扰其他事务。即一个事务内部的操作及数据对其他并行的事务没有影响,并发执行的各个事务之间不能互相影响。
持续性:指一个事务一旦提交,它对数据库中数据的改变是永久性的。接下来的其他操作或故障不对其执行结果有任何影响。
事务的七种传播级别:
REQUIRED(默认):支持使用当前事务,如果当前事务不存在,创建一个新事务。
SUPPORTS:支持使用当前事务,如果当前事务不存在,则不使用事务。
MANDATORY:中文翻译为强制,支持使用当前事务,如果当前事务不存在,则抛出Exception。
REQUIRES_NEW:创建一个新事务,如果当前事务存在,把当前事务挂起。
NOT_SUPPORTED:无事务执行,如果当前事务存在,把当前事务挂起。
NEVER:无事务执行,如果当前有事务则抛出Exception。
NESTED:嵌套事务,如果当前事务存在,那么在嵌套的事务中执行。如果当前事务不存在,则表现跟REQUIRED一样。扩展
Spring Transactional一直是RD的事务神器,但是如果用不好,反会伤了自己。下面总结@Transactional经常遇到的几个场景: @Transactional 加于private方法, 无效 @Transactional 加于未加入接口的public方法, 再通过普通接口方法调用, 无效 @Transactional 加于接口方法, 无论下面调用的是private或public方法, 都有效 @Transactional 加于接口方法后, 被本类普通接口方法直接调用, 无效 @Transactional 加于接口方法后, 被本类普通接口方法通过接口调用, 有效 @Transactional 加于接口方法后, 被它类的接口方法调用, 有效 @Transactional 加于接口方法后, 被它类的私有方法调用后, 有效
6. Spring 中使用了哪些设计模式
- 工厂模式:Spring 中的 BeanFactory,根据传入的唯一标识来获取 Bean 对象
- 单例模式:提供了全局的访问点 BeanFactory
- 代理模式:AOP; JDK动态代理、CGLib 字节码生成代理
- 装饰器模式:依赖注入使用 BeanWrapper
- 观察者模式:Spring 的 Listener 的实现,如 ApplicationListener
- 策略模式:Bean 实例化的时候决定采用何种方式实例化,反射或者 CGLIB 动态字节码生成
五、 Maven
常见Maven项目部署问题?
- 导入项目后配置JDK:
- 导入项目后配置Maven:
- 导入项目后配置语法版本:
- 导入项目后检查编码格式(因项目而异):
- B工程中引用到A工程的类,需要引入A工程依赖:
- 编译项目时需要先编译父工程,把父工程中的modules注释后进行编译,编译后解除注释:
- 被依赖的子工程A加了plugin插件,引用了依赖A的工程B打包会报错:
- 在parent POM 中加入 dependencyManagement 进行版本管理
六、Spring MVC
1. Spring MVC工作原理
- 用户发送请求到前端控制器 DispatcherServlet
- DispatcherServlet 收到请求调用 HandlerMapping 处理器映射器
- HandlerMapping 找到具体的处理器返回给 DispatcherServlet
- DispatcherServlet 调用 HandlerAdapter处理器适配器
- HandlerAdapter 经过适配调用具体的控制器(Controller)
- Controller 返回 ModelAndView 给 DispatcherServlet
- DispatcherServlet 调用 ViewReslover 视图解析器
- ViewReslover 返回 View 给 DispatcherServlet
- DispatcherServlet 对 View 进行数据渲染后返回给用户
七、Spring Boot
1. SpringBoot 自动装配原理
- 启动类上的 @SpringBootApplication 注解上的 @EnableAutoConfiguration 上的 @Import({AutoConfigurationImportSelector.class})
- AutoConfigurationImportSelector 通过 SpringFactoriesLoader.loadFactoryNames 扫描所有具有 META-INF/spring.factories 的 jar 包
八、Spring Cloud
https://www.cnblogs.com/aishangJava/p/11927311.html
1. Eureka:服务注册与发现
Eureka心跳机制
在应用启动后,节点们将会向Eureka Server发送心跳,默认周期为30秒,如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认90秒)。
eureka自我保护机制是什么?
如果在15分钟内超过85%的客户端节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,Eureka Server自动进入自我保护机制,此时会出现以下几种情况:
- Eureka Server不再从注册列表中移除因为长时间没收到心跳而应该过期的服务。
- Eureka Server仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上,保证当前节点依然可用。
- 当网络稳定时,当前Eureka Server新的注册信息会被同步到其它节点中
2. Ribbon:负载均衡客户端
Ribbon提供了七种负载均衡策略:http://www.wityx.com/post/758_1_1.html
***负载均衡原理:***https://blog.youkuaiyun.com/wudiyong22/article/details/80829808
LoadBalancerClient(RibbonLoadBalancerClient是实现类)在初始化的时候(execute方法),会通过ILoadBalance(BaseLoadBalancer是实现类)向Eureka注册中心获取服务注册列表,并且每10s一次向EurekaClient发送“ping”,来判断服务的可用性,如果服务的可用性发生了改变或者服务数量和之前的不一致,则从注册中心更新或者重新拉取。LoadBalancerClient有了这些服务注册列表,就可以根据具体的IRule来进行负载均衡
3. Fegin:服务接口调用
什么是Fegin?
Feign是一个声明式的服务客户端,使得编写web服务客户端变得非常容易,Feign采用的是基于接口的注解。
Feign整合了Ribbon与Hystrix,具有负载均衡、降级、熔断的功能。
拓展
Fegin的配置优先与Ribbon
Fegin默认请求超过1秒会触发重试
4. Hystrix:降级、熔断、限流、监控
https://www.cnblogs.com/yawen/p/6655352.html
降级:降级是当某个微服务响应时间过长,或不可用了,当发生这种问题的时候我们直接对应的策略(这是一个方法,写在调用端)来快速返回这个请求,不让他卡在那 ,这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。
熔断:在服务客户端根据yml文件配置中的值判断某个请求在设定时间内请求失败次数达到设定阈值,则会触发熔断,不再发送该请求,而是调用降级方法。
- 熔断状态:
- Closed:熔断器关闭状态,调用失败次数积累,到了阈值(或一定比例)则启动熔断机制;
- Open:熔断器打开状态,此时内部直接返回降级方法,不走网络。但设计了一个时钟选项,默认的时钟达到了一定时间(默认5秒),到了这个时间,进入半开状态;
- Half-Open:半开状态,允许定量的服务请求,如果调用都成功(或一定比例)则认为恢复了,关闭熔断器,否则认为还没好,又回到熔断器打开状态;
限流:限制某个微服务的可用线程
hystrix通过线程池的方式来管理你的微服务调用(全局线程池默认大小:10),对每个command创建一个自己的线程池,执行调用。通过线程池隔离来保证不同调用不会相互干扰和每一个调用的并发限制。并发超过核心线程数(加等待线程数),会直接返回降级方法。在@HystrixCommand注解中可以使用参数新建线程池。更多功能参考@HystrixCommand参数详解监控:https://blog.youkuaiyun.com/chengqiuming/article/details/80783680
例如监控user服务:1.打开http://localhost:9000/hystrix/
2.输入需要监控的服务http://localhost:5000/actuator/hystrix.stream
5. Zuul:统一网关处理
https://www.pianshen.com/article/8342843266/
Zuul包含了对请求的路由和过滤两个最主要的功能:
其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础。
而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。
Zuul和Eureka进行整合,将Zuul自身注册为Eureka服务治理下的应用,同时从Eureka中获得其他微服务的消息,也即以后的访问微服务都是通过Zuul跳转后获得.
Zuul的过滤器
Zuul大部分功能都是通过过滤器来实现的,这些过滤器类型对应于请求的典型生命周期。
PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
ERROR:在其他阶段发生错误时执行该过滤器。
除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。例如,我们可以定制一种STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务。Zuul的作用
Zuul可以通过加载动态过滤机制,从而实现以下各项功能:
验证与安全保障: 识别面向各类资源的验证要求并拒绝那些与要求不符的请求。
审查与监控: 在边缘位置追踪有意义数据及统计结果,从而为我们带来准确的生产状态结论。
动态路由: 以动态方式根据需要将请求路由至不同后端集群处。
压力测试: 逐渐增加指向集群的负载流量,从而计算性能水平。
负载分配: 为每一种负载类型分配对应容量,并弃用超出限定值的请求。
静态响应处理: 在边缘位置直接建立部分响应,从而避免其流入内部集群。
多区域弹性: 跨越AWS区域进行请求路由,旨在实现ELB使用多样化并保证边缘位置与使用者尽可能接近。
Zuul和Nginx的区别
相同点:Zuul和Nginx都可以实现负载均衡、反向代理(隐藏真实IP地址),过滤请求,实现网关的效果。
不同点:Nginx–C语言开发 Zuul–Java语言开发
Zuul负载均衡实现:采用Ribbon+Eureka实现本地负载均衡
Nginx负载均衡实现:采用服务器来实现负载均衡
Nginx相比Zuul功能更加强大,因为Nginx整合一些脚本语言(Nginx+lua)
Nginx适合于服务端负载均衡
Zuul适合微服务中实现网关
Zuul限流
- 流算法
- 漏桶: leakey bucket,原理:桶的下方的小孔会以一个相对恒定的速率漏水,而不管入桶的水流量,这样就达到了控制出水口的流量
- 令牌桶: token bucket,原理:以相对恒定的速率向桶中加入令牌,请求来时于桶中取令牌,取到了就放行,没能取到令牌的请求则丢弃
- 限流粒度
粗粒度
- 网关限流
- 单个服务
- 细粒度
- user: 认证用户或者匿名,针对某个用户粒度进行限流
- origin: 客户机的IP,针对请求客户机的IP进行限流
- url: 特定url,针对请求的url粒度进行限流
- serviceId: 特定服务,针对某个服务的id粒度进行限流
鉴权流程:
6. Config Server:配置中心
config server是什么?
config server是一个分布式配置中心,服务消费端可以通过bootstrap.yml文件从config server读取github中的配置信息。服务消费端可以同时存在application.yml与bootstrap.yml文件。同属性配置内容,后者比前者优先级高。
config访问配置文件,是需要一个具体的访问规则的, 那么这个访问规则到底是什么呢? 我们可以在官网找到:
/{application}/{profile}[/{label}] /{application}-{profile}.yml /{label}/{application}-{profile}.yml /{application}-{profile}.properties /{label}/{application}-{profile}.properties
application就是配置文件的名字, profile就是对应的环境 label就是不同的分支 , 对于yml 和properties类型config可以完美转换
7. Sleuth+Zipkin:分布式链路跟踪
分布式链路跟踪介绍:
本节课来讨论微服务“跟踪"大家先看几个问题,对于一个大型的微服务架构系统,会有哪些常见问题?
如何串联调用链,快速定位问题
如何厘清微服务之间的依赖关系
如何进行各个服务接口的性能分折
如何跟踪业务流的处理
Zipkin简介:
Zipkin是 Twitter开源的分布式跟踪系统,基于 Dapper的论文设计而来。它的主要功能是收集系统的时序数据,从而追踪微服务架构的系统延时等问题。 Zipkin还提供了一个非常友好的界面,来帮助分析追踪数据。
为什么要Zipkin?
因为sleuth对于分布式链路的跟踪仅仅是一些数据的记录, 这些数据我们人为来读取和处理难免会太麻烦了,所以我们一般吧这种数据上交给Zipkin Server 来统一处理.
zipkin server 数据持久化问题
刚刚我们介绍了如何把分布式链路调用信息上传到 zipkin server 但是 有一个问题:
当zipkin重启后我们的分布式链路数据全部清空了。
因为zipkin server 默认数据是存储在内存当中, 所以当你服务重启之后内存自然而然也就清空了。
使用Elasticsearch 做数据持久化
我们这里借用ES来做数据持久化, 当然 还可以用ELK来做, 我们这里演示Zipkin整合ES
关于Elasticsearch 具体的介绍 我们本文暂时不讨论, 没学习过的同学可以把他当作mysql来看待
Elasticsearch 下载地址:https://www.elastic.co/cn/downloads/elasticsearch
下载完是个压缩包 解压出来 打开bin目录 找到elasticsearch.bat文件启动
等他启动一会儿然后在页面上输入localhost:9000看见信息说明Elasticsearch 启动好了。
8. 微服务总结
这里我画了张图简单总结了以下我们的spring cloud 的学习:
- 由上图可以发现, spring cloud 把各个组件相互配合起来, 整合成一套成熟的微服务架构体系
- 其中, 由eureka做服务注册与发现,很好的把各个服务链接起来
- ribbon+fegin提供了微服务的调用和负载均衡解决方案
- hystrix 负责监控微服务之间的调用情况,以及降级和熔断保护
- Hystrix dashboard监控Hystrix的熔断情况以及监控信息以图形化界面展示
- spring cloud config 提供了统一的配置中心服务
- 所有外来的请求由zuul统一进行路由和转发,起到了API网关的作用
- Sleuth+Zipkin把我们微服务的追踪数据记录下来并展示方便我们进行后续分析
九、微服务
https://www.cnblogs.com/aishangJava/p/11927311.html
1. 微服务技术栈
- 维度(springcloud)
- 服务开发:springboot spring springmvc
- 服务配置与管理:Netfix公司的Archaiusm ,阿里的Diamond
- 服务注册与发现:Eureka,Zookeeper
- 服务调用:Rest RPC gRpc
- 服务熔断器:Hystrix
- 服务负载均衡:Ribbon Nginx
- 服务接口调用:Fegin
- 消息队列:Kafka Rabbitmq activemq
- 服务配置中心管理:SpringCloudConfig
- 服务路由(API网关)Zuul
- 事件消息总线:SpringCloud Bus
十、Docker
1. Docker架构
Docker使用C/S架构,Client通过接口与Server进程通信实现容器的构建,运行和发布,如图:
Host(Docker 宿主机):
安装了Docker程序,并运行了Docker daemon的主机。
Docker daemon(Docker 守护线程):
运行在宿主机上,Docker守护进程,用户通过Docker client(Docker命令)与Docker daemon交互。
Images(镜像):
将软件环境打包好的模板,用来创建容器的,一个镜像可以创建多个容器。
镜像分层结构:
位于下层的镜像称为父镜像(Parent Image),最底层的称为基础镜像(Base Image)。最上层为“可读写”层,其下的均为“只读”
层。
Containers(容器):
Docker的运行组件,启动一个镜像就是一个容器,容器与容器之间相互隔离,并且互不影响。
Docker Client(Docker 客户端):
Docker命令行工具,用户是用Docker Client与Docker daemon进行通信并返回结果给用户。也可以使用其他工具通过Docker Api 与Docker daemon通信。
Registry(仓库服务注册):
经常会和仓库(Repository)混为一谈,实际上Registry上可以有多个仓库,每个仓库可以看成是一个用户,一个用户的仓库放了多个镜像。仓库分为了公开仓库(Public Repository)和私有仓库(Private Repository),最大的公开仓库是官方的Docker Hub,国内也有如阿里云、时速云等,可以给国内用户提供稳定快速的服务。用户也可以在本地网络内创建一个私有仓库。当用户创建了自己的镜像之后就可以使用 push 命令将它上传到公有或者私有仓库,这样下次在另外一台机器上使用这个镜像时候,只需要从仓库上 pull 下来就可以了
2. 常用命令
镜像常用操作:
查找镜像:docker search 关键词
下载镜像:docker pull 镜像名:TAG
# Tag表示版本,有些镜像的版本显示latest,为最新版本
查看镜像:docker images
# 查看本地所有镜像
删除镜像:docker rmi -f 镜像 ID 或者镜像名:TAG
# 删除指定本地镜像
# -f 表示强制删除
获取元信息:docker inspect 镜像ID或者镜像名:TAG
# 获取镜像的元信息,详细信息
容器常用操作:
运行:
docker run --name 容器名 -i -t -p 主机端口:容器端口 -d -v 主机目录:容器目录:ro 镜像ID或镜像名:TAG
# --name 指定容器名,可自定义,不指定自动命名
# -i 以交互模式运行容器
# -t 分配一个伪终端,即命令行,通常-it组合来使用
# -p 指定映射端口,讲主机端口映射到容器内的端口
# -d 后台运行容器
# -v 指定挂载主机目录到容器目录,默认为rw读写模式,ro表示只读
容器列表:
docker ps -a -q
# docker ps查看正在运行的容器
# -a 查看所有容器(运行中、未运行)
# -q 只查看容器的ID
启动容器:
docker start 容器ID或容器名
停止容器:
docker stop 容器ID或容器名
删除容器:
docker rm -f 容器ID或容器名
# -f 表示强制删除
查看日志:docker logs 容器ID或容器名
进入正在运行容器:
docker exec -it 容器ID或者容器名 /bin/bash
# 进入正在运行的容器并且开启交互模式终端
# /bin/bash是固有写法,作用是因为docker后台必须运行一个进程,否则容器就会退出,在这里表示启动容器后启动bash
# 也可以用docker exec在运行中的容器执行命令
拷贝文件:
docker cp 主机文件路径 容器ID或容器名:容器路径
#主机中文件拷贝到容器中
docker cp 容器ID或容器名:容器路径 主机文件路径
#容器中文件拷贝到主机中
获取容器元信息:
docker inspect 容器ID或容器名
查看容器日志信息:docker logs --tail 50 --follow --timestamps ID/NAME
Docker生成镜像的两种方式:
更新镜像:
先使用基础镜像创建一个容器,然后对容器内容进行更改,然后使用docker commit命令提交为一个新的镜像(以tomcat为例)。
根据基础镜像,创建容器
docker run --name mytomcat -p 80:8080 -d tomcat
修改容器内容
docker exec -it mytomcat /bin/bash cd webapps/ROOT rm-f index.jsp echo hello world > index.html exit
提交为新镜像
docker commit -m="描述消息"-a="作者"容器ID或容器名 镜像名:TAG # 例: # docker commit -m="修改了首页" -a="村口督屎" mytomcat huaan/tomcat:v1.0
使用新镜像运行容器
docker run --name tom -p8080:8080 -d huaan/tomcat:v1.0
使用Dockerfile构建SpringBoot应用镜像
把你的项目打包成可执行jar包
把jar包上传到Linux服务器
在jar包路径下创建Dockerfile文件vi Dockerfile
vi Dockerfile # 指定基础镜像,本地没有会从dockerHub pull下来 FROM java:8 #作者 MAINTAINER huaan # 把可执行jar包复制到基础镜像的根目录下 ADD luban.jar /luban.jar # 镜像要暴露的端口,如要使用端口,在执行docker run命令时使用-p生效 EXPOSE 80 # 在镜像运行为容器后执行的命令 ENTRYPOINT ["java","-jar","/luban.jar"]
使用docker build命令构建镜像,基本语法
docker build -t huaan/mypro:v1 . # -f指定Dockerfile文件的路径# -t指定镜像名字和TAG# .指当前目录,这里实际上需要一个上下文路径
运行刚才构建的镜像
docker run --name pro -p80:80 -d镜像名:TAG
Dockerfile常用指令:
FROM
FROM指令是最重要的一个并且必须为Dockerfile文件开篇的第一个非注释行,用于为镜像文件构建过程指定基础镜像,后续的指令运行于此基础镜像提供的运行环境。
这个基础镜像可以是任何可用镜像,默认情况下docker build会从本地仓库找指定的镜像文件,如果不存在就会从Docker Hub上拉取
语法:
FROM <image> FROM <image>:<tag> FROM <image>@<digest>
MAINTAINER(depreacted)
Dockerfile的制作者提供的本人详细信息
Dockerfile不限制MAINTAINER出现的位置,但是推荐放到FROM指令之后
语法:name可以是任何文本信息,一般用作者名称或者邮箱
MAINTAINER <name>
LABEL
给镜像指定各种元数据
一个Dockerfile可以写多个LABEL,但是不推荐这么做,Dockerfile每一条指令都会生成一层镜像,如果LABEL太长可以使用\符号换行。构建的镜像会继承基础镜像的LABEL,并且会去掉重复的,但如果值不同,则后面的值会覆盖前面的值。
语法:
LABEL <key>=<value> <key>=<value> <key>=<value>...
COPY
用于从宿主机复制文件到创建的新镜像文件
注意:如果你的路径中有空白字符,通常会使用第二种格式
规则:
必须是build上下文中的路径,不能是其父目录中的文件
如果是目录,则其内部文件或子目录会被递归复制,但目录自身不会被复制
如果指定了多个,或在中使用了通配符,则必须是一个目录,则必须以/符号结尾
如果不存在,将会被自动创建,包括其父目录路径
语法:
COPY <src>...<dest> COPY ["<src>",..."<dest>"] # <src>:要复制的源文件或者目录,可以使用通配符 # <dest>:目标路径,即正在创建的image的文件系统路径;建议<dest>使用绝对路径,否则COPY指令则以WORKDIR为其起始路径
ADD
基本用法和COPY指令一样,ADD支持使用TAR文件和URL路径
规则:
和COPY规则相同如果为URL并且没有以/结尾,则
指定的文件将被下载到
如果是一个本地系统上压缩格式的tar文件,它会展开成一个目录;但是通过URL获取的tar文件不会自动展开
如果有多个,直接或间接使用了通配符指定多个资源,则必须是目录并且以/结尾
语法:
ADD <src>...<dest> ADD ["<src>",..."<dest>"]
EXPOSE
用于给容器打开指定要监听的端口以实现和外部通信。
用于指定传输层协议,可以是TCP或者UDP,默认是TCP协议
EXPOSE可以一次性指定多个端口,例如:EXPOSE 80/tcp 80/udp
语法:
EXPOSE <port>[/<protocol>] [<port>[/<protocol>]...]
RUN
用来指定docker build过程中运行指定的命令
语法:
RUN <command> RUN ["<executable>","<param1>","<param2>"]
第一种格式里面的参数一般是一个shell命令,以 /bin/sh -c 来运行它
第二种格式中的参数是一个JSON格式的数组,当中是要运行的命令,后面是传递给命令的选项或者参数;但是这种格式不会用/bin/sh -c来发起,所以常见的shell
操作像变量替换和通配符替换不会进行;如果你运行的命令依赖shell特性,可以替换成类型以下的格式
RUN ["/bin/bash","-c","<executable>","<param1>"]
CMD
容器启动时运行的命令
语法:
CMD <command> CMD ["<executable>","<param1>","<param2>"] CMD ["<param1>","<param2>"]
前两种语法和RUN相同,第三种语法用于为ENTRYPOINT指令提供默认参数
RUN和CMD区别:
RUN指令运行于镜像文件构建过程中,CMD则运行于基于Dockerfile构建出的新镜像文件启动为一个容器的时候。
CMD指令的主要目的在于给启动的容器指定默认要运行的程序,且在运行结束后,容器也将终止;不过,CMD命令可以被docker run的命令行选项给覆盖。
Dockerfile中可以存在多CMD指令,但是只有最后一个会生效。
ENTRYPOINT
类似于CMD指令功能,用于给容器指定默认运行程序
语法:
ENTRYPOINT<command> ENTRYPOINT["<executable>","<param1>","<param2>"]
ENTRYPOINT和CMD区别:
和CMD不同的是ENTRYPOINT启动的程序不会被docker run命令指定的参数所覆盖,而且,这些命令行参数会被当做参数传递给ENTRYPOINT指定的程序
(但是,docker run命令的–entrypoint参数可以覆盖ENTRYPOINT)。
docker run命令传入的参数会覆盖CMD指令的内容并且附加到ENTRYPOINT命令最后作为其参数使用。
同样,Dockerfile中可以存在多个ENTRYPOINT指令,但是只有最后一个会生效
Dockerfile中如果既有CMD又有ENTRYPOINT,并且CMD是一个完整可执行命令,那么谁在最后谁生效。
3. 实例:mysql、redis
docker pull mysql:5.7 \#创建三个要挂载的目录↓ mkdir -p /my/mysql/conf mkdir -p /my/mysql/data mkdir -p /my/mysql/logs \#复制文件并修改字符(安装默认版本的mysql不能在表中输入中文,需要修改配置文件)↓ docker cp mysql:cd /my/mysql/conf/ vi /my/mysql/conf/mysqld.conf character-set-server=utf8 \#最终启动命令↓ docker run --name mysql -p 3306:3306 -v /my/mysql/conf:/etc/mysql/mysql.conf.d/ -v /my/mysql/data:/var/lib/mysql -v /my/mysql/logs:/logs -e MYSQL_ROOT_PASSWORD=设置数据库密码 -d mysql:5.7 docker pull redis mkdir /usr/local/docker vi /usr/local/docker/redis.conf ①创建配置文件目录存放redis.conf,文件从官网下载http://download.redis.io/redis-stable/redis.conf。 ③修改启动默认配置(从上至下依次): bind 127.0.0.1 #注释掉这部分,这是限制redis只能本地访问 protected-mode no #默认yes,开启保护模式,限制为本地访问 daemonize no#默认no,改为yes意为以守护进程方式启动,可后台运行,除非kill进程,改为yes会使配置文件方式启动redis失败 databases 16 #数据库个数(可选),我修改了这个只是查看是否生效。 dir ./ #输入本地redis数据库存放文件夹(可选) appendonly yes #redis持久化(可选) docker run -p 6379:6379 --name redis -v /my/redis/redis.conf:/etc/redis/redis.conf -v /my/redis/data:/data -d redis redis-server /etc/redis/redis.conf --appendonly yes --requirepass "123456" 命令解释说明: -p 6379:6379 端口映射:前表示主机部分,:后表示容器部分。 --name myredis 指定该容器名称,查看和进行操作都比较方便。 -v 挂载目录,规则与端口映射相同。 为什么需要挂载目录:个人认为docker是个沙箱隔离级别的容器,这个是它的特点及安全机制,不能随便访问外部(主机)资源目录,所以需要这个挂载目录机制。 -d redis 表示后台启动redis redis-server /etc/redis/redis.conf 以配置文件启动redis,加载容器内的conf文件,最终找到的是挂载的目录/usr/local/docker/redis.conf --appendonly yes 开启redis 持久化 --requirepass 密码
4. Docker网络
Docker允许通过外部访问容器或容器互联的方式来提供网络服务。安装Docker时,会自动安装一块Docker网卡称为docker0,用于Docker各容器及宿主机的网络通信,网段为172.0.0.1。
Docker网络中有三个核心概念:沙盒(Sandbox)、网络(Network)、端点(Endpoint)
- 沙盒:提供了容器的虚拟网络栈,也即端口套接字、IP路由表、防火墙等内容。隔离容器网络与宿主机网络,形成了完全独立的容器网络环境。
- 网络:可以理解为Docker内部的虚拟子网,网络内的参与者相互可见并能够进行通讯。Docker的虚拟网络和宿主机网络是存在隔离关系的,其目的主要是形成容器间的安全通讯环境。
- 端点:位于容器或网络隔离墙之上的洞,主要目的是形成一个可以控制的突破封闭的网络环境的出入口。当容器的端点与网络的端点形成配对后,就如同在这两者之间搭建了桥梁,便能够进行数据传输了。
Docker的四种网络模式:
Bridge(默认):桥接模式,主要用来对外通信的,docker容器默认的网络使用的就是bridge。
使用bridge模式配置容器自定的网络配置:
# 配置容器的主机名
docker run --name t1 --network bridge -h [自定义主机名] -it --rm busybox
# 自定义DNS
docker run --name t1 --network bridge --dns114.114 -it–rm busybox
# 给host文件添加一条
docker run --name t1 --network bridge --add-host [hostname]:[ip] -it --rm busybox
Host:host类型的网络就是主机网络的意思,绑定到这种网络上面的容器,内部使用的端口直接绑定在主机上对应的端口,而如果容器服务没有使用端口,则无影响。
None:从某种意义上来说,none应该算不上网络了,因为它不使用任何网络,会形成一个封闭网络的容器。
Container:共享另外一个容器的network namespace,和host模式差不多,只是这里不是使用宿主机网络,而是使用的容器网络。
开放端口:
-p 选项的使用:
-p
#将指定的容器端口映射到主机所有地址的一个动态端口
-p :
#将容器端口映射到指定的主机端口
-p ::
#将指定的容器端口映射到主机指定的动态端口
-p ::
#将指定的容器端口映射至主机指定的端口
#动态端口指随机端口,可以使用docker port命令查看具体映射结果
-P(大写) 暴露所有端口(所有端口指构建镜像时EXPOSE的端口)
自定义docker0桥的网络属性信息:/etc/docker/daemon.json文件:
{
“bip”: “192.168.1.5/24”
}
核心选项为bip,即bridge ip之意,用于指定docker0桥自身的IP地址;其它选项可通过此地址计算得出远程连接创建自定义的桥
docker network create -d bridge --subnet"172.26.0.0/16"–gateway"172.26.0.1" mybr0
5. Compose
简介:
Compose的作用是“定义和运行多个Docker容器的应用”。使用Compose,你可以在一个配置文件(yml格式)中配置你应用的服务,然后使用一个命令,即可创建并启动配置中引用的所有服务。
Compose中两个重要概念:
服务(service):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例。
项目 (project):由一组关联的应用容器组成的一个完整业务单元,在 docker-compose.yml文件中定义。
使用Docker Compose编排SpringCloud微服务:
使用docker-compose一次性来编排三个微服务:eureka服务(eureka-server-2.0.2.RELEASE.jar)、user服务(user-2.0.2.RELEASE.jar)、power服务(power-2.0.2.RELEASE.jar)
创建一个工作目录和docker-compose模板文件。
工作目录下创建三个文件夹eureka、user、power,并分别构建好三个服务的镜像文件以eureka的Dockerfile为例:
# 基础镜像 FROM java:8 # 作者 MAINTAINER huaan # 把可执行jar包复制到基础镜像的根目录下 ADD eureka-server-2.0.2.RELEASE.jar /eureka-server-2.0.2.RELEASE.jar # 镜像要暴露的端口,如要使用端口,在执行docker run命令时使用-p生效 EXPOSE 3000 # 在镜像运行为容器后执行的命令 ENTRYPOINT ["java","-jar","/eureka-server-2.0.2.RELEASE.jar"]
目录文件结构:
compose docker-compose.yml eureka Dockerfile eureka-server-2.0.2.RELEASE.jar user Dockerfile user-2.0.2.RELEASE.jar power Dockerfile power-2.0.2.RELEASE.jar
- 编写docker-compose模板文件:
version: '1' services: eureka: image: eureka:v1 ports: - 8080:8080 user: image: user:v1 ports: - 8081:8081 power: image: power:v1 ports: - 8082:8082
- 启动微服务,可以加上参数-d后台启动 -f指定启动文件
docker-compose up -d -f /compose_lnmp/kafka/docker-compose.yml
- 查看服务日志
docker-compose logs -f /compose_lnmp/kafka/docker-compose.yml
- 查看编排服务状态
docker-compose ps -f /compose_lnmp/kafka/docker-compose.yml
- 资源回收
docker-compose down -f /compose_lnmp/kafka/docker-compose.yml
- 停止编排服务
docker-compose stop -f /compose_lnmp/kafka/docker-compose.yml
十一、Redis
https://www.yuque.com/haolonglong/qyws71/mz7i9f
1. redis是什么?
redis可以理解就是一个数据库,不过与传统数据库不同的是 redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业务场景。除此之外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。
2. Redis支持的数据类型
https://blog.youkuaiyun.com/javaxiaibai0414/article/details/88666453
String字符串 :
格式:set key value
string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。
string类型是Redis最基本的数据类型,一个键最大能存储512MB。
Hash(哈希):
格式:hmset name key1 value1 key2 value2
Redis hash 是一个键值对集合(name==>(key=>value))
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
List(列表) :
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
格式:lpush name value
在 key 对应 list 的头部添加字符串元素
格式: rpush name value
在 key 对应 list 的尾部添加字符串元素
格式: lrem name index
key 对应 list 中删除 count 个和 value 相同的元素
格式: llen name
返回 key 对应 list 的长度
Set(集合) :
格式:sadd name value
Redis的Set类型是String类型的无序集合
集合是通过哈希表实现的,所以添加、删除、查找的空间复杂度都是O(1)。
zset(sorted set:有序集合):
格式:zadd name value score value
Redis zset 和 set一样也是string类型的集合,且不允许值重复。
不同的是zset每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的元素从小到大排序。
zset的值(value)是唯一的,但分数(score)可以是重复的。
3. Redis常见操作命令
http://doc.redisfans.com
linux登录redis:redis-cli -h 远程IP -p 端口 例:redis-cli -h 124.32.54.87 -p 4578
再 输入 auth 密码
若密码为空 直接 输入 auth 回车
查看集群信息: INFO
del:删除键
EXISTS key:检查给定
key
是否存在 **返回值:**若key
存在,返回1
,否则返回0
。EXPIRE key seconds:为给定
key
设置生存时间,单位秒,当key
过期时(生存时间为0
),它会被自动删除。PEXPIRE key milliseconds:这个命令和 EXPIRE 命令的作用类似,但是它以毫秒为单位设置
key
的生存时间,而不像 EXPIRE 命令那样,以秒为单位。使用 PERSIST 命令可以在 不删除
key
的情况下,移除key
的生存时间,让key
重新成为一个『持久的』(persistent)key
。KEYS pattern
查找所有符合给定模式
pattern
的key
。
KEYS *
匹配数据库中所有key
。
KEYS h?llo
匹配hello
,hallo
和hxllo
等。
KEYS h*llo
匹配hllo
和heeeeello
等。
KEYS h[ae]llo
匹配hello
和hallo
,但不匹配hillo
。PTTL key:这个命令类似于 TTL 命令,但它以毫秒为单位返回
key
的剩余生存时间,而不是像 TTL 命令那样,以秒为单位。RENAME key newkey:将
key
改名为newkey
。当key
和newkey
相同,或者key
不存在时,返回一个错误。当newkey
已经存在时, RENAME 命令将覆盖旧值。RENAMENX key newkey:当且仅当
newkey
不存在时,将key
改名为newkey
。当key
不存在时,返回一个错误。TYPE key:返回
key
所储存的值的类型。SORT key:
SORT key
返回键值从小到大排序的结果。SORT key DESC
返回键值从大到小排序的结果。# 开销金额列表 redis> LPUSH today_cost 30 1.5 10 8 (integer) 4 # 排序 redis> SORT today_cost 1) "1.5" 2) "8" 3) "10" 4) "30" # 逆序排序 redis 127.0.0.1:6379> SORT today_cost DESC 1) "30" 2) "10" 3) "8" 4) "1.5"
SCAN 命令及其相关的 SSCAN 命令、 HSCAN 命令和 ZSCAN 命令都用于增量地迭代(incrementally iterate)一集元素(a collection of elements):
String类型:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
将字符串值
value
关联到key
。如果
key
已经持有其他值, SET 就覆写旧值,无视类型。对于某个原本带有生存时间(TTL)的键来说, 当 SET 命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。
SETNX key value
将
key
的值设为value
,当且仅当key
不存在。若给定的
key
已经存在,则 SETNX 不做任何动作。SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
SETEX key seconds value
将值
value
关联到key
,并将key
的生存时间设为seconds
(以秒为单位)。如果
key
已经存在, SETEX 命令将覆写旧值。APPEND key value
如果
key
已经存在并且是一个字符串, APPEND 命令将value
追加到key
原来的值的末尾。如果
key
不存在, APPEND 就简单地将给定key
设为value
,就像执行SET key value
一样。**返回值:**追加
value
之后,key
中字符串的长度。SETBIT key offset value
对
key
所储存的字符串值,设置或清除指定偏移量上的位(bit)。位的设置或清除取决于
value
参数,可以是0
也可以是1
。当
key
不存在时,自动生成一个新的字符串值。字符串会进行伸展(grown)以确保它可以将
value
保存在指定的偏移量上。当字符串值进行伸展时,空白位置以0
填充。
offset
参数必须大于或等于0
,小于 2^32 (bit 映射被限制在 512 MB 之内)。BITCOUNT key [start] [end]
计算给定字符串中,被设置为
1
的比特位的数量。一般情况下,给定的整个字符串都会被进行计数,通过指定额外的
start
或end
参数,可以让计数只在特定的位上进行。
start
和end
参数的设置和 GETRANGE 命令类似,都可以使用负数值:比如-1
表示最后一个位,而-2
表示倒数第二个位,以此类推。不存在的
key
被当成是空字符串来处理,因此对一个不存在的key
进行BITCOUNT
操作,结果为0
。GET key
返回
key
所关联的字符串值。如果
key
不存在那么返回特殊值nil
。假如
key
储存的值不是字符串类型,返回一个错误,因为 GET 只能用于处理字符串值。STRLEN key
返回
key
所储存的字符串值的长度。当
key
储存的不是字符串值时,返回一个错误。Hash类型:
HSET key field value
将哈希表
key
中的域field
的值设为value
。如果
key
不存在,一个新的哈希表被创建并进行 HSET 操作。如果域
field
已经存在于哈希表中,旧值将被覆盖。HMSET key field value [field value …]
同时将多个
field-value
(域-值)对设置到哈希表key
中。此命令会覆盖哈希表中已存在的域。
如果
key
不存在,一个空哈希表被创建并执行 HMSET 操作。HDEL key field [field …]
删除哈希表
key
中的一个或多个指定域,不存在的域将被忽略。HGET key field
返回哈希表
key
中给定域field
的值。HSETNX key field value
将哈希表
key
中的域field
的值设置为value
,当且仅当域field
不存在。若域
field
已经存在,该操作无效。如果
key
不存在,一个新哈希表被创建并执行 HSETNX 命令。**返回值:**设置成功,返回
1
。如果给定域已经存在且没有操作被执行,返回0
。HVALS key
返回哈希表
key
中所有域的值。# 非空哈希表 redis> HMSET website google www.google.com yahoo www.yahoo.com OK redis> HVALS website 1) "www.google.com" 2) "www.yahoo.com"
空哈希表/不存在的key
redis> EXISTS not_exists
(integer) 0redis> HVALS not_exists
(empty list or set)
List类型
LPUSH key value [value …]
将一个或多个值
value
插入到列表key
的表头如果有多个
value
值,那么各个value
值按从左到右的顺序依次插入到表头: 比如说,对空列表mylist
执行命令LPUSH mylist a b c
,列表的值将是c b a
,这等同于原子性地执行LPUSH mylist a
、LPUSH mylist b
和LPUSH mylist c
三个命令。如果
key
不存在,一个空列表会被创建并执行 LPUSH 操作。当
key
存在但不是列表类型时,返回一个错误。LSET key index value
将列表
key
下标为index
的元素的值设置为value
。当
index
参数超出范围,或对一个空列表(key
不存在)进行 LSET 时,返回一个错误。LPOP key
移除并返回列表
key
的头元素。redis> LLEN course (integer) 0 redis> RPUSH course algorithm001 (integer) 1 redis> RPUSH course c++101 (integer) 2 redis> LPOP course # 移除头元素 "algorithm001"
Set类型:
SADD key member [member …]
将一个或多个
member
元素加入到集合key
当中,已经存在于集合的member
元素将被忽略。假如
key
不存在,则创建一个只包含member
元素作成员的集合。SMEMBERS key
返回集合
key
中的所有成员。SCARD key
返回集合
key
的基数(集合中元素的数量)。SINTER key [key …]
返回一个集合的全部成员,该集合是所有给定集合的交集。
不存在的
key
被视为空集。当给定集合当中有一个空集时,结果也为空集(根据集合运算定律)。
SDIFF key [key …]
返回一个集合的全部成员,该集合是所有给定集合之间的差集。
不存在的
key
被视为空集。SUNION key [key …]
返回一个集合的全部成员,该集合是所有给定集合的并集。
SISMEMBER key member
判断
member
元素是否集合key
的成员。
4. Redis实现分布式锁
https://www.cnblogs.com/gxyandwmm/p/9588383.html
Redis分布式锁实现的三个核心要素:
加锁
解锁
锁超时
上面的功能,存在着三个致命问题:
setnx和expire的非原子性
if(setnx(key,1) == 1){ //此处挂掉了..... expire(key,30) try { do something ...... }catch() { } finally { del(key) } }
设想一个极端场景,当某线程执行setnx,成功得到了锁:
setnx刚执行成功,还未来得及执行expire指令,节点1 Duang的一声挂掉了。
这样一来,这把锁就没有设置过期时间,变得“长生不老”,别的线程再也无法获得锁了。
怎么解决呢?setnx指令本身是不支持传入超时时间的,Redis 2.6.12以上版本为set指令增加了可选参数,伪代码如下:set(key,1,30,NX),这样就可以取代setnx指令。
超时后使用del 导致误删其他线程的锁
加锁: String threadId = Thread.currentThread().getId() set(key,threadId ,30,NX) doSomething..... 解锁: if(threadId .equals(redisClient.get(key))){ del(key) }
又是一个极端场景,假如某线程成功得到了锁,并且设置的超时时间是30秒。
如果某些原因导致线程A执行的很慢很慢,过了30秒都没执行完,这时候锁过期自动释放,线程B得到了锁。
随后,线程A执行完了任务,线程A接着执行del指令来释放锁。但这时候线程B还没执行完,线程A实际上删除的是线程B加的锁。
怎么避免这种情况呢?可以在del释放锁之前做一个判断,验证当前的锁是不是自己加的锁。
至于具体的实现,可以在加锁的时候把当前的线程ID当做value,并在删除之前验证key对应的value是不是自己线程的ID。
但是,这样做又隐含了一个新的问题,**if判断和释放锁是两个独立操作,不是原子性**。 我们都是追求极致的程序员,所以这一块要用Lua脚本来实现: String luaScript = 'if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end'; redisClient.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId)); 这样一来,验证和删除过程就是原子操作了。 ```java /** * 解锁 * @param key * @param value * @return */ public Boolean unlock(String key, String value) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; RedisScript<Boolean> redisScript = new DefaultRedisScript(script,Boolean.class); Boolean result = redisTemplate.execute(redisScript, new StringRedisSerializer(), new FastJsonRedisSerializer(Boolean.class), Collections.singletonList(key),value); if(SUCCESS.equals(result)) { return true; } return false; } ```
出现并发的可能性
还是刚才第二点所描述的场景,虽然我们避免了线程A误删掉key的情况,但是同一时间有A,B两个线程在访问代码块,仍然是不完美的。
怎么办呢?我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”。
当过去了29秒,线程A还没执行完,这时候守护线程会执行expire指令,为这把锁“续命”20秒。守护线程从第29秒开始执行,每20秒执行一次。
当线程A执行完任务,会显式关掉守护线程。
另一种情况,如果节点1 忽然断电,由于线程A和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。
5. Redis实现消息队列
基于Redis实现消息队列典型方案
基于List的 LPUSH+BRPOP 的实现
PUB/SUB,订阅/发布模式基于SortedSet有序集合的实现
基于 Stream 类型的实现
6. Redis事务
https://www.yuque.com/haolonglong/qyws71/ug0dq6
1 Redis事务的概念:
Redis 提供的不是严格的事务,Redis 只保证串行执行命令,并且能保证全部执行,但是执行命令失败时并不会回滚,而是会继续执行下去。
2 Redis事务没有隔离级别的概念:
批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。
3 Redis不保证原子性:
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
4 Redis事务的三个阶段:
- 开始事务
- 命令入队
- 执行事务
5 Redis事务相关命令:
watch key1 key2 … : 监视一或多个key,如果在事务执行之前,被监视的key被其他命令改动,则事务被打断 ( 类似乐观锁 )
multi : 标记一个事务块的开始( queued )
exec : 执行所有事务块的命令 ( 一旦执行exec后,之前加的监控锁都会被取消掉 )
discard : 取消事务,放弃事务块中的所有命令
unwatch : 取消watch对所有key的监控
6 Redis事务使用案例:
(1)正常执行
(2)放弃事务
(3)若在事务队列中存在命令性错误(类似于java编译性错误),则执行EXEC命令时,所有命令都不会执行
(4)若在事务队列中存在语法性错误(类似于java的1/0的运行时异常),则执行EXEC命令时,其他正确命令会被执行,错误命令抛出异常。
(5)使用watch案例一:使用watch检测balance,事务期间balance数据未变动,事务执行成功
案例二:使用watch检测balance,在开启事务后(标注1处),在新窗口执行标注2中的操作,更改balance的值,模拟其他客户端在事务执行期间更改watch监控的数据,然后再执行标注1后命令,执行EXEC后,事务未成功执行。
一但执行 EXEC 开启事务的执行后,无论事务使用执行成功, WARCH 对变量的监控都将被取消。故当事务执行失败后,需重新执行WATCH命令对变量进行监控,并开启新的事务进行操作。
7 总结:
watch指令类似于乐观锁,在事务提交时,如果watch监控的多个KEY中任何KEY的值已经被其他客户端更改,则使用EXEC执行事务时,事务队列将不会被执行,同时返回Nullmulti-bulk应答以通知调用者事务执行失败。
7. Redis持久化机制
RDB
RDB持久化是将当前进程中的数据生成快照保存到硬盘(因此也称作快照持久化),保存的文件后缀是rdb;当Redis重新启动时,可以读取快照文件恢复数据。
触发条件
RDB持久化的触发分为手动触发和自动触发两种。
- 手动触发
save命令和bgsave命令都可以生成RDB文件。
save命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在Redis服务器阻塞期间,服务器不能处理任何命令请求。save1
而bgsave命令会创建一个子进程,由子进程来负责创建RDB文件,父进程(即Redis主进程)则继续处理请求。
bgsave1
bgsave命令执行过程中,只有fork子进程时会阻塞服务器,而对于save命令,整个过程都会阻塞服务器,因此save已基本被废弃,线上环境要杜绝save的使用;后文中也将只介绍bgsave命令。此外,在自动触发RDB持久化时,Redis也会选择bgsave而不是save来进行持久化;下面介绍自动触发RDB持久化的条件。
自动触发
save m n
自动触发最常见的情况是在配置文件中通过save m n,指定当m秒内发生n次变化时,会触发bgsave。
例如,查看redis的默认配置文件(Linux下为redis根目录下的redis.conf),可以看到如下配置信息:
其中save 900 1的含义是:当时间到900秒时,如果redis数据发生了至少1次变化,则执行bgsave;save 300 10和save 60 10000同理。当三个save条件满足任意一个时,都会引起bgsave的调用。
8. 主从复制、读写分离原理
读写分离、主从机制
读写分离配置
https://blog.youkuaiyun.com/seven_2016/article/details/81952246
9. 缓存淘汰机制
https://blog.youkuaiyun.com/weixin_43559950/article/details/118420099
当Redis内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换,这样会使得redis的性能急剧下降。
在生产环境中,是不允许redis出现交换行为的,为了限制最大使用内存,redis提供了配置参数maxmemory来限制内存超出期望大小。当实际内存超出maxmemory时,redis提供了几种可选策略来让用户自己决定该如何腾出新的空间继续提供读写服务。有以下几种策略:
1.noeviction:继续读,停止写。这样做能够保证不丢失数据,但是会使得线上的业务无法继续进行。这是redis的默认淘汰策略。
2.volatile-lru:尝试淘汰设置了过期时间的key,而没有设置过期时间的key不会被淘汰。淘汰策略为LRU,即最少使用的key优先被淘汰,也就是优先淘汰最近最少使用的key。这样做可以保证需要持久化的数据不会突然丢失。
3.volatile-ttl:尝试淘汰设置了过期时间的key,只是淘汰的策略变为比较key的剩余寿命ttl的值,ttl越小越优先被淘汰。也就是优先淘汰快消亡的key。
4.volatile-random:尝试淘汰设置了过期时间的key,只是淘汰的策略变为随机淘汰,即淘汰过期key集合中随机的key。
5.allkeys-lru:区别于volatile-lru,这个策略是对全体key集合进行LRU策略淘汰,而不单单只是过期key集合。
6.allkeys-random:作用于全体key集合的随机淘汰。显然,volatile-xxx策略只会针对带过期时间的key进行淘汰,而allkeys-xxx策略会对所有的key进行淘汰。
如果只是拿redis做缓存,应该使用allkeys-xxx策略,客户端写缓存时不需要携带过期时间。如果还想同时使用redis的持久化功能,那就使用volatile-xxx策略,这样可以保留没有设置过期时间的key,它们是永久的key。
10. 哨兵模式(一主二从三哨兵搭建)
https://www.cnblogs.com/yueminghai/p/10756780.html
目的:
让看看这篇文章的的人能够知道:软件架构、软件的安装、配置、基本运维的操作、高可用测试、也包含我自己,能够节省对应的时间。
软件架构:
生产环境使用三台服务器搭建redis哨兵集群,3个redis实例(1主2从)+ 3个哨兵实例。生产环境能够保证在哨兵存活两台的情况下,只有一台redis能够继续提供服务(一主两从三哨兵)
主虚拟机1 从虚拟机2 从虚拟机3 172.16.48.129 172.16.48.130 172.16.48.131
11. redis缓存雪崩、穿透、击穿概念及解决方法
缓存雪崩
对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。
这就是缓存雪崩。
大约在 3 年前,国内比较知名的一个互联网公司,曾因为缓存事故,导致雪崩,后台系统全部崩溃,事故从当天下午持续到晚上凌晨 3~4 点,公司损失了几千万。缓存雪崩的事前事中事后的解决方案如下。
- 事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。
- 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
- 事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
用户发送一个请求,系统 A 收到请求后,先查本地 ehcache 缓存,如果没查到再查 redis。如果 ehcache 和 redis 都没有,再查数据库,将数据库中的结果,写入 ehcache 和 redis 中。
限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?走降级!可以返回一些默认的值,或者友情提示,或者空白的值。
好处:
- 数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。
- 只要数据库不死,就是说,对用户来说,2/5 的请求都是可以被处理的。
- 只要有 2/5 的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来一次。
缓存穿透
对于系统A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。
黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。
举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。
解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如
set -999 UNKNOWN
。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。
缓存击穿
缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
解决方式也很简单,可以将热点数据设置为永远不过期;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。
12. pipeline(利用管道优化批量操作)
https://blog.youkuaiyun.com/w1lgy/article/details/84455579
十二、MQ
1. 消息中间件
消息中间件介绍&为什么要使用消息中间件&什么时候使用消息中间件:
打个比方,我们客户端发送一个下单请求给订单系统(order),订单系统发送了一个请求给我们的库存系统告诉他需要更改库存。我已经下单了,这里,我们可以把每一个请求看作一条消息,但是我们的客户端需要订单系统告诉我们这条消息的处理结果(我们到底有没有下单成功),但是,订单系统不需要知道库存系统对这条消息的处理情况,因为无论你库存系统是否修改成功,我订单还是下了,因为是先下完订单(下成功了)才去修改库存,哪怕库存系统修改出现问题,那也只是库存系统的问题,这个问题不会影响订单系统。*如果这里你能理解的话, 那么我们就能发现 我们用户发送的这条消息(下订单), 是需要同步的(我需要知道结果), 订单发送给库存的消息,是可以异步的(我不想知道你库存到底改了没, 我只是通知你我这边成功下了一个订单)*
那么如果我们还按原来的方式去实现这个需求的话, 那么结果会是这样:
那可能有同学说了, 我们订单系统开辟线程去访问库存系统不就好了吗?
使用线程池解决 确实可以, 但是也有他的缺点, 那么 到底怎么来完美解决这个问题呢?
如果这张图能理解的话, 那么 这个消息系统, 就是我们的消息中间件。
2. MQ的四大作用
异步通信:异步通信,减少线程等待,特别是处理批量等大事务、耗时操作。
系统解耦:系统不直接调用,降低依赖,特别是不在线也能保持通信最终完成。
削峰平谷:压力大的时候,缓冲部分请求消息,类似于背压处理。
可靠通信:提供多种消息模式、服务质量、顺序保障等。
3. RabbitMq
导语:我们刚刚介绍了什么是消息中间件, 那么RabbitMq就是对于消息中间件的一种实现,市面上还有很多很多实现, 比如RabbitMq、ActiveMq、ZeroMq、kafka,以及阿里开源的RocketMQ等等 我们这节主要讲RabbitMq
AMQP
这里引用百度的一句话 再加以我的理解: AMQP 其实和Http一样 都是一种协议, 只不过 Http是针对网络传输的, 而AMQP是基于消息队列的.
AMQP 协议中的基本概念:
• Broker: 接收和分发消息的应用,我们在介绍消息中间件的时候所说的消息系统就是Message Broker。
• Virtual host: 出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中,类似于网络中的namespace概念。当多个不同的用户使用同一个RabbitMQ server提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建exchange/queue等。
• Connection: publisher/consumer和broker之间的TCP连接。断开连接的操作只会在client端进行,Broker不会断开连接,除非出现网络故障或broker服务出现问题。
• Channel: 如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立TCP Connection的开销将是巨大的,效率也较低。Channel是在connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id帮助客户端和message broker识别channel,所以channel之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建立TCP connection的开销。
• Exchange: message到达broker的第一站,根据分发规则,匹配查询表中的routing key,分发消息到queue中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)。
• Queue: 消息最终被送到这里等待consumer取走。一个message可以被同时拷贝到多个queue中。
• Binding: exchange和queue之间的虚拟连接,binding中可以包含routing key。Binding信息被保存到exchange中的查询表中,用于message的分发依据。
Exchange的类型
direct(路由键模式):
这种类型的交换机的路由规则是根据一个routingKey的标识,交换机通过一个routingKey与队列绑定 ,在生产者生产消息的时候 指定一个routingKey 当绑定的队列的routingKey 与生产者发送的一样 那么交换机会吧这个消息发送给对应的队列。
fanout(广播模式):
这种类型的交换机路由规则很简单,只要与他绑定了的队列, 他就会吧消息发送给对应队列(与routingKey没关系)
topic(主题模式):(因为*在这个笔记软件里面是关键字,所以下面就用’星’替换掉了)
这种类型的交换机路由规则也是和routingKey有关 只不过 topic他可以根据:星,#( 星号代表过滤一单词,#代表过滤后面所有单词, 用.隔开)来识别routingKey 我打个比方 假设 我绑定的routingKey 有队列A和 B A的routingKey是:星.user B的routingKey是: #.user
那么我生产一条消息routingKey 为: error.user 那么此时 2个队列都能接受到, 如果改为 topic.error.user 那么这时候 只有B能接受到了
headers:
这个类型的交换机很少用到,他的路由规则 与routingKey无关 而是通过判断header参数来识别的, 基本上没有应用场景,因为上面的三种类型已经能应付了。
4. kafka
https://mp.weixin.qq.com/s?__biz=MzU1NDA0MDQ3MA==&mid=2247483958&idx=1&sn=dffaad318b50f875eea615bc3bdcc80c&chksm=fbe8efcfcc9f66d9ff096fbae1c2a3671f60ca4dc3e7412ebb511252e7193a46dcd4eb11aadc&scene=21#wechat_redirect
kafka简介
Kafka最初由Linkedin公司开发,是一个分布式的、分区的、多副本的、多订阅者,基于zookeeper协调的分布式日志系统(也可以当做MQ系统),常用于web/nginx日志、访问日志、消息服务等等,Linkedin于2010年贡献给了Apache基金会并成为顶级开源项目。
kafka的特性
- 高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒;
- 可扩展性:kafka集群支持热扩展;
- 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止丢失;
- 容错性:允许集群中的节点失败(若分区副本数量为n,则允许n-1个节点失败);
- 高并发:单机可支持数千个客户端同时读写;
kafka的应用场景
- 日志收集:一个公司可以用Kafka收集各种服务的log,通过kafka以统一接口开放给各种消费端,例如hadoop、Hbase、Solr等。
- 消息系统:解耦生产者和消费者、缓存消息等。
- 用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索记录、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖掘。
- 运营指标:Kafka也经常用来记录运营监控数据。
- 流式处理
kafka架构
https://www.jianshu.com/p/abbc09ed6703
整体来看,kafka架构中包含四大组件:生产者、消费者、kafka集群、zookeeper集群。对照上面的结构图,我们先来搞清楚几个很重要的术语,(看图!对照图理解~)
1、broker
kafka 集群包含一个或多个服务器,每个服务器节点称为一个broker。
2、topic
每条发布到kafka集群的消息都有一个类别,这个类别称为topic,其实就是将消息按照topic来分类,topic就是逻辑上的分类,同一个topic的数据既可以在同一个broker上也可以在不同的broker结点上。
3、partition
分区,每个topic被物理划分为一个或多个分区,每个分区在物理上对应一个文件夹,该文件夹里面存储了这个分区的所有消息和索引文件。在创建topic时可指定parition数量,生产者将消息发送到topic时,消息会根据 分区策略 追加到分区文件的末尾,属于顺序写磁盘,因此效率非常高(经验证,顺序写磁盘效率比随机写内存还要高,这是Kafka高吞吐率的一个很重要的保证)。
上面提到了分区策略,所谓分区策略就是决定生产者将消息发送到哪个分区的算法。Kafka 为我们提供了默认的分区策略,同时它也支持自定义分区策略。kafka允许为每条消息设置一个key,一旦消息被定义了 Key,那么就可以保证同一个 Key 的所有消息都进入到相同的分区,这种策略属于自定义策略的一种,被称作"按消息key保存策略",或Key-ordering 策略。
同一主题的多个分区可以部署在多个机器上,以此来实现 kafka 的伸缩性。同一partition中的数据是有序的,但topic下的多个partition之间在消费数据时不能保证有序性,在需要严格保证消息顺序消费的场景下,可以将partition数设为1,但这种做法的缺点是降低了吞吐,一般来说,只需要保证每个分区的有序性,再对消息设置key来保证相同key的消息落入同一分区,就可以满足绝大多数的应用。
4、offset
partition中的每条消息都被标记了一个序号,这个序号表示消息在partition中的偏移量,称为offset,每一条消息在partition都有唯一的offset,消息者通过指定offset来指定要消费的消息。
正常情况下,消费者在消费完一条消息后会递增offset,准备去消费下一条消息,但也可以将offset设成一个较小的值,重新消费一些消费过的消息,可见offset是由consumer控制的,consumer想消费哪一条消息就消费哪一条消息,所以kafka broker是无状态的,它不需要标记哪些消息被消费过。
5、producer
生产者,生产者发送消息到指定的topic下,消息再根据分配规则append到某个partition的末尾。
6、consumer
消费者,消费者从topic中消费数据。
7、consumer group
消费者组,每个consumer属于一个特定的consumer group,可为每个consumer指定consumer group,若不指定则属于默认的group。
同一topic的一条消息只能被同一个consumer group内的一个consumer消费,但多个consumer group可同时消费这一消息。这也是kafka用来实现一个topic消息的广播和单播的手段,如果需要实现广播,一个consumer group内只放一个消费者即可,要实现单播,将所有的消费者放到同一个consumer group即可。
用consumer group还可以将consumer进行自由的分组而不需要多次发送消息到不同的topic。
8、leader
每个partition有多个副本,其中有且仅有一个作为leader,leader会负责所有的客户端读写操作。
9、follower
follower不对外提供服务,只与leader保持数据同步,如果leader失效,则选举一个follower来充当新的leader。当follower与leader挂掉、卡住或者同步太慢,leader会把这个follower从ISR列表中删除,重新创建一个follower。
10、rebalance
同一个consumer group下的多个消费者互相协调消费工作,我们这样想,一个topic分为多个分区,一个consumer group里面的所有消费者合作,一起去消费所订阅的某个topic下的所有分区(每个消费者消费部分分区),kafka会将该topic下的所有分区均匀的分配给consumer group下的每个消费者,如下图,
rebalance表示"重平衡",consumer group内某个消费者挂掉后,其他消费者自动重新分配订阅主题分区的过程,是 Kafka 消费者端实现高可用的重要手段。如下图Consumer Group A中的C2挂掉,C1会接收P1和P2,以达到重新平衡。同样的,当有新消费者加入consumer group,也会触发重平衡操作。
kafka吞吐量为什么这么高
1、顺序读写磁盘
Kafka是将消息持久化到本地磁盘中的,一般人会认为磁盘读写性能差,可能会对Kafka性能提出质疑。实际上不管是内存还是磁盘,快或慢的关键在于寻址方式,磁盘分为顺序读写与随机读写,内存一样也分为顺序读写与随机读写。基于磁盘的随机读写确实很慢,但基于磁盘的顺序读写性能却很高,一般而言要高出磁盘的随机读写三个数量级,一些情况下磁盘顺序读写性能甚至要高于内存随机读写。
2、page cache
为了优化读写性能,Kafka利用了操作系统本身的Page Cache,就是利用操作系统自身的内存而不是JVM空间内存。这样做是因为,
> JVM中一切皆对象,对象的存储会带来额外的内存消耗;
> 使用JVM会受到GC的影响,随着数据的增多,垃圾回收也会变得复杂与缓慢,降低吞吐量;
另外操作系统本身对page cache做了大量优化,通过操作系统的Page Cache,Kafka的读写操作基本上是基于系统内存的,读写性能也得到了极大的提升。
3、零拷贝
零拷贝是指Kafka利用 linux 操作系统的 “zero-copy” 机制在消费端做的优化。首先来看一下消费端在消费数据时,数据从broker磁盘通过网络传输到消费端的整个过程:
> 操作系统从磁盘读取数据到内核空间(kernel space)的page cache;
> 应用程序读取page cache的数据到用户空间(user space)的缓冲区;
> 应用程序将用户空间缓冲区的数据写回内核空间的socket缓冲区(socket buffer);
> 操作系统将数据从socket缓冲区复制到硬件(如网卡)缓冲区;
整个过程如上图所示,这个过程包含4次copy操作和2次系统上下文切换,而上下文切换是CPU密集型的工作,数据拷贝是I/O密集型的工作,性能其实非常低效。
零拷贝就是使用了一个名为sendfile()的系统调用方法,将数据从page cache直接发送到Socket缓冲区,避免了系统上下文的切换,消除了从内核空间到用户空间的来回复制。从上图可以看出,"零拷贝"并不是说整个过程完全不发生拷贝,而是站在内核的角度来说的,避免了内核空间到用户空间的来回拷贝。
4、分区分段
Kafka的message是按topic分类存储的,topic中的数据又是按照一个一个的partition即分区存储到不同broker节点。每个partition对应了操作系统上的一个文件夹,partition实际上又是按照segment分段存储的。这也非常符合分布式系统分区分桶的设计思想。
通过这种分区分段的设计,Kafka的message消息实际上是分布式存储在一个一个小的segment中的,每次文件操作也是直接操作的segment。为了进一步的查询优化,Kafka又默认为分段后的数据文件建立了索引文件,就是文件系统上的.index文件。这种分区分段+索引的设计,不仅提升了数据读取的效率,同时也提高了数据操作的并行度。
总之,Kafka采用顺序读写、Page Cache、零拷贝以及分区分段等这些设计,再加上在索引方面做的优化,另外Kafka数据读写也是批量的而不是单条的,使得Kafka具有了高性能、高吞吐、低延时的特点。
kafka简易测试命令
- 进入kafka容器:docker exec -it ${CONTAINER ID} /bin/bash
- 进入kafka安装目录:cd /opt/kafka/
- 查看topic列表:bin/kafka-topics.sh --zookeeper localhost:2181 --list
- 创建一个名为 testk 4分区1副本的topic:bin/kafka-topics.sh --zookeeper localhost:2181 --create --topic testk --partitions 4 --replication-factor 1
- 查看topic信息:bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic testk
- 运行一个从最新数据开始的消费者:bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic testk
- 运行一个从头开始的消费者:bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --from-beginning --topic testk
- 运行一个生产者:bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic testk
- 查看消费者组:bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list
- 查看offset消费情况:bin/kafka-consumer-groups.sh --describe --bootstrap-server localhost:9092 --group console-consumer-81040
kafka重试机制
new KafkaProducer()后创建一个后台线程KafkaThread扫描RecordAccumulator中是否有消息;
调用KafkaProducer.send()发送消息,实际上只是把消息保存到RecordAccumulator中;
后台线程KafkaThread扫描到RecordAccumulator中有消息后,将消息发送到kafka集群;
如果发送成功,那么返回成功;
如果发送失败,那么判断是否允许重试。如果不允许重试,那么返回失败的结果;如果允许重试,把消息再保存到RecordAccumulator中,等待后台线程KafkaThread扫描再次发送;
5. kafka+Flink实时流计算
https://blog.youkuaiyun.com/lmalds/article/details/51780950
GPT:下面是一个使用Apache Flink来读取Kafka的示例代码:
import org.apache.flink.api.common.serialization.SimpleStringSchema; import org.apache.flink.streaming.api.datastream.DataStream; import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer; import java.util.Properties; public class KafkaToFlink { public static void main(String[] args) throws Exception { // 设置执行环境 StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(2); // 设置整个程序的并行度为2 // 重启策略:Flink提供了可配置的重启策略来处理任务的故障和中断。当发生rebalance时,可以配置重启策略,在任务失败后自动重启并恢复上一次的状态。这样可以确保在rebalance期间 不会丢失任何数据,任务会在重新分配后继续处理。 env.setRestartStrategy(RestartStrategies.fixedDelayRestart( NUM_RESTART_ATTEMPTS, Time.seconds(DELAY_BETWEEN_RESTART_ATTEMPTS))); // 设置Kafka配置参数 Properties properties = new Properties(); //private val ZOOKEEPER_HOST = "master:2181,worker1:2181,worker2:2181" //private val KAFKA_BROKER = "master:9092,worker1:9092,worker2:9092" //properties.setProperty("zookeeper.connect", "localhost:9092"); // zookeeper服务器地址 properties.setProperty("bootstrap.servers", "localhost:9092"); // Kafka服务器地址 properties.setProperty("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); //指定序列化 properties.setProperty("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); //指定序列化 properties.setProperty("group.id", "flink-consumer"); // 消费者组ID properties.setProperty("acks", "all"); //确认模式 properties.setProperty("retries", "1"); //发送失败重试 properties.setProperty("enable.auto.commit", "true"); // 自动提交offset,每1s提交一次 properties.setProperty("auto.commit.interval.ms", "1000"); //properties.setProperty("batch.size", "16384"); //批量提交 properties.setProperty("linger.ms", "1"); //提交消息的时间窗口 properties.setProperty("enable.idempotence","true"); //开启幂等 //properties.setProperty("transactional.id","tx0001"); //开启事务 //订阅的消息 String inputTopic = "上游源表名"; // 创建Kafka消费者,不设置key分组 FlinkKafkaConsumer<String> kafkaConsumer = new FlinkKafkaConsumer<>(inputTopic, properties); //kafkaConsumer.setStartFromEarliest(); // 设置从最早的消息开始消费 // 从Kafka读取数据 DataStream<String> kafkaStream = env.addSource(kafkaConsumer); // 使用keyBy()方法将数据按键值进行分区,分发到不同的任务中进行处理。 DataStream<Result> transformedStream = kafkaStream .map(new MyMapper()) // 替换为自定义的转换逻辑 .keyBy(data -> data.getKey()); // 使用数据的键值进行分区 // TODO: 在这里添加数据处理和转换的逻辑 // kafkaStream.map(...) //生产的消息 String outputTopic = "供给下游的表名"; // 输出到sink kafkaStream.print(); // 将转换后的数据输出到Kafka进行处理结果的存储 Properties producerProps = new Properties(); producerProps.setProperty("bootstrap.servers", "localhost:9092"); transformedStream.addSink(new FlinkKafkaProducer<>(outputTopic, producerProps)); // 执行任务 env.execute("Kafka to Flink"); } }
上述代码中需要根据实际的Kafka配置参数进行替换,比如
bootstrap.servers
设置为实际Kafka服务器的地址,group.id
设置为消费者组的ID,topic
设置为要消费的Kafka主题。接下来,你可以在TODO标记的地方添加你自己的数据处理和转换逻辑,例如使用map
操作来解析结构化数据并将其写入到sink中。请确保在应用程序中包含Flink和Kafka的依赖配置,以使代码能够正确编译和运行。
6. kafka与rabbitMQ选型对比
RabbitMQ Kafka 开发语言 erlang scala,Java 架构模型 ① 遵循AMQP;② 生产者、消费者、broker。③ broker由exchange、binding、queue组成;④ consumer消费位置由broker通过确认机制保存; ① 不遵循AMQP;② 生产者、消费者、kafka集群、zookeeper集群;③ kafka集群由多个broker节点组成,消息按照topic分类,每个topic又划分为多个partition;④ broker无状态,offset由消费者指定; 可靠性 RabbitMQ可靠性更好,支持事务,支持消息确认机制 高可用 采用镜像队列,即主从模式,数据是异步同步的,当消息过来,主从全部写完后,回ack,这样保障了数据的一致性。 每个分区都有一个或多个副本,这些副本保存在不同的broker上,其中有且仅有一个分区副本作为leader,其余的作为follower,当leader不可用时,会选举follower作为新leader继续提供服务。只有leader提供读写服务,follower从leader同步拉取数据然后备份。 吞吐量 kafka更高 是否支持事务 支持 不支持 负载均衡 需要外部支持才能实现(如:loadbalancer) kafka利用zk和分区机制实现负载均衡 是否支持消费者Push 不支持 支持 是否支持消费者Pull 支持 支持 适用场景 kafka的优势主要体现在吞吐量上,它主要用在高吞吐量的场景。比如日志采集。具有较高的严谨性,数据丢失的可能性更小,同时具备较高的实时性,用在对实时性、可靠性要求较高的消息传递上。
7. 使用MQ如何保证消息的顺序性?
https://blog.youkuaiyun.com/qq_34936541/article/details/103248863
举个例子,我们以前做过一个 mysql binlog 同步的系统,压力还是非常大的,日同步数据要达到上亿,就是说数据从一个 mysql 库原封不动地同步到另一个 mysql 库里面去(mysql -> mysql)。常见的一点在于说比如大数据 team,就需要同步一个 mysql 库过来,对公司的业务系统的数据做各种复杂的操作。
你在 mysql 里增删改一条数据,对应出来了增删改 3 条 binlog 日志,接着这三条 binlog 发送到 MQ 里面,再消费出来依次执行,起码得保证人家是按照顺序来的吧?不然本来是:增加、修改、删除;你楞是换了顺序给执行成删除、修改、增加,不全错了么。
本来这个数据同步过来,应该最后这个数据被删除了;结果你搞错了这个顺序,最后这个数据保留下来了,数据同步就出错了。
先看看顺序会错乱的俩场景:
- RabbitMQ:一个 queue,多个 consumer。比如,生产者向 RabbitMQ 里发送了三条数据,顺序依次是 data1/data2/data3,压入的是 RabbitMQ 的一个内存队列。有三个消费者分别从 MQ 中消费这三条数据中的一条,结果消费者2先执行完操作,把 data2 存入数据库,然后是 data1/data3。这不明显乱了。
- Kafka:比如说我们建了一个 topic,有三个 partition。生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。
消费者从 partition 中取出来数据的时候,也一定是有顺序的。到这里,顺序还是 ok 的,没有错乱。接着,我们在消费者里可能会搞 多个线程来并发处理消息。因为如果消费者是单线程消费处理,而处理比较耗时的话,比如处理一条消息耗时几十 ms,那么 1 秒钟只能处理几十条消息,这吞吐量太低了。而多个线程并发跑的话,顺序可能就乱掉了。
解决方案
RabbitMQ
拆分多个 queue,每个 queue 一个 consumer,就是多一些 queue 而已,确实是麻烦点;或者就一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。
Kafka
- 一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。
- 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。
十三、Linux
1. 防火墙
https://blog.youkuaiyun.com/SERE0211/article/details/106999838
列出所有端口:netstat -ntlp
查看开发的端口:firewall-cmd --list-ports
开放指定端口:firewall-cmd --zone=public --add-port=8080/tcp --permanent
开放区间端口:firewall-cmd --zone=public --add-port=8080-9000/tcp --permanent
删除端口:firewall-cmd --zone=public --remove-port=8080/tcp --permanent
重新加载配置:firewall-cmd --reload
查看所有打开的端口:firewall-cmd --zone=public --list-ports
查看端口:firewall-cmd --zone=public --query-port=8080/tcp
启动服务:systemctl start firewalld
关闭服务:systemctl stop firewalld
重启服务:systemctl restart firewalld
查看状态:systemctl status firewalld
开启启动:systemctl enable firewalld
开机禁用:systemctl disable firewalld
查看已启动的服务列表:systemctl list-unit-files | grep enabled
firewall-cmd防火墙管理工具常用参数:
–permanent #表示设置为永久
–state #查看服务状态
–get-default-zone #查询默认的区域名称
–set-default-zone= #设置默认的区域,不需要reload或者重启服务,是即改即生效,并且是永久的
–get-zones #显示可用的区域名称
–get-services #显示预先定义的服务
–get-active-zones #显示当前正在使用的区域与网卡名称
–add-source=/ #将来源于此IP或子网的流量导向指定的区域,例如:–add-source=192.168.2.110/24
–remove-source=/ #不再将此IP或子网的流量导向某个指定区域,例如:–remove-source=192.168.2.110/24
–add-interface= #将来自该接口的所有流量到指定区域,例如:–add-interface=eth0
–change-interface= #将接口已有绑定区域而与其他区域关联,例如:–change-interface=eth0
–list-all #列出默认区域的所有配置(接口、源、服务和端口)
–list-all-zones #列出所有区域的所有配置(接口、源、服务和端口)
–add-service= #设置默认区域允许该服务,例如:–add-service=ssh
–remove-service= #设置默认区域不再允许该服务,例如:–remove-service=ssh
–add-port=/ #添加允许该端口,例如:–add-port=8080/tcp
–remove-port=/ #删除允许该端口,例如:–remove-port=8080/tcp
–reload #让永久生效的配置规则立即生效
–panic-on #开启应急状况模式(阻断所有的网络连接)
–panic-off #关闭应急状况模式(阻断所有的网络连接)
2. SVN
https://www.cnblogs.com/zhongyehai/p/10620058.html
3. FTP
https://cshihong.github.io/2018/10/25/FTP%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%90%AD%E5%BB%BA%E5%92%8C%E9%85%8D%E7%BD%AE/
安装FTP,要求服务端口为3333,自动分配端口为7000-8000区间端口。
安装完毕后,需要root用户修改/etc/vsftpd/路径下vsftpd.conf配置:
禁止匿名登录:anonymous_enable=NO
启用本地系统用户:local_enable=YES
启动写入权限:write_enable=YES
本地用户上传文件的权限掩码:local_umask=002
显示目录说明文件:dirmessage_enable=YES
记录ftp传输过程:xferlog_enable=YES
日志路径:xferlog_file=/var/log/xferlog
使用标准的ftp xferlog:xferlog_std_format=YES
以独立运行的方式监听服务:listen=YES
默认服务端口:listen_port=3333
自动分配端口:pasv_enable=YES
最小被动端口:pasv_min_port=7000
最大被动端口:pasv_max_port=8000
#将列表中用户锁定在自己的home目录上:chroot_list_enable=YES
#锁定用户列表:chroot_list_file=/etc/vsftpd/chroot_list
listen_ipv6=NO
listen_port=3333
pam_service_name=vsftpd
userlist_enable=YES
tcp_wrappers=YES
因为前面安装了python3,建立了软连接指向python,而防火墙的firewall-cmd和firewall文件只支持python2。所以需要将/usr/bin/firewall-cmd与/usr/sbin/firewalld中的第一行
#!/usr/bin/python 改为 #!/usr/bin/python2
防火墙开放3333端口:
firewall-cmd --zone=public -add-port=3333/tcp --permanent
防火墙开放7000-8000区间端口:
firewall-cmd --zone=public -add-port=7000-8000/tcp --permanent
重启防火墙配置:
Firewall-cmd --reload
启动ftp服务
systemctl start vsftpd
chkconfig vsftpd on
4. 查询命令
查看服务器实时日志:tail -f /var/log/messages
查看CPU:ps aux --sort=-pcpu |head -10
查询字符集:locale
查询CPU使用情况:top
根据PID查看进程资源信息:cd /proc/{pid} -> ls -ail
查看运行内存使用情况:free -h
查看磁盘使用情况:df -h
查询进程:ps aux|grep svn
查看指定目录磁盘使用情况:df -h /home/rdmapp/
查看指定目录下每个文件夹磁盘使用情况:du --max-depth=1 -h /home/rdmapp/
查询zlib版本:rpm -qa|grep zlib-devel
查询python版本:python --version
查询setuptools版本:pip show setuptools
5.安装命令
rpm包安装:rpm -ivhU zlib*.rpm --nodeps --force
whl包安装:pip install *.whl --force-reinstall
压缩包安装:python setup.py install
6.卸载
rpm包卸载:rpm -e zlib*.rpm
mysql卸载:yum remove mysql
pip卸载:python -m pip uninstall pip
使用pip卸载:pip uninstall pandas
7.压缩/打包
zip test.zip test1 test2
tar zxvf backports.lzma-0.0.14.tar.gz
tar -cvf test.tar test
tar -zcvf test.tar.gz test
8.解压/解包
unzip test.zip
unzip -d /tmp/ test.zip
tar -xvf test.tar
tar -zxvf test.tar.gz test
9.字符集
locale
export LANG=zh_CN.UTF-8
export LC_ALL=zh_CN.UTF-8
10.用户/分组/权限
新增用户:useradd xiaoming
设置用户密码:passwd xiaoming(回车后输入新密码)
删除用户:userdel xiaoming
删除用户及用户home目录:userdel -r xiaoming
添加“开发”分组:groupadd dev
删除“开发”分组:groupdel dev
修改分组列表:vi /etc/group
将“xiaoming”添加到“开发”和“测试”分组:usermod -G dev,test xiaoming
将“xiaoming”主组改为master组:usermod -g master xiaoming
修改权限:
Linux的文件调用权限分为三级:文件所有者、用户组、其他用户
- :文件d:目录 r:读 4 w:写 2 x:执行 1
rwx rwx rwx -->777
rwx rwx r-x -->775
rwx r-x r-x -->755
Owner Group OtherUsers
将文件设置为拥有者及同组的用户可写入,其他用户不允许写入:
chmod ug+w,o-w file.txt file2.txt
or
chmod 775 file.txt file2.txt
将文件设置为所有人可执行:
chmod +x _bz2.cpython-36m-x86_64-linux-gnu.so
设置指定目录及子目录下权限为775:
chmod -R 775 /home/rdm-ftp/
修改文件属主为daraexch用户:
chown -R dataexch:dataexch /home/rdmapp/datafiles/input
11.软连接
建立软连接:
ln -s /usr/local/python3/bin/pip3 /usr/bin/pip
十四、Nginx
1.部署与配置
docker安装Nginx
docker启动Nginx容器,并映射端口、静态资源路径、配置文件、日志
docker run -d -p 8082:80 --name nginx -v /compose_lnmp/nginx/www:/usr/share/nginx/html -v /compose_lnmp/nginx/conf/nginx.conf:/etc/nginx/nginx.conf
-v /compose_lnmp/nginx/logs:/var/log/nginx nginx
修改配置文件,解决跨域问题
修改配置文件,配置防盗链
配置浏览器缓存
配置sll,使用HTTPS协议
conf文件中upstream模块 配置keepalived提高吞吐量。
配置负载均衡算法
安装keepalived配置双主热备实现nginx高可用。
安装keepalived配置双主热备实现LVS+Nginx高可用。
(F:\软件开发学习资料\慕课网–电商平台\慕课网-Java架构师体系课:跟随千万级项目从0到100全过程高效成长\阶段二:从单体到高可用集群演进(6-8周)\01.LVS+Nginx实现高可用集群)
2.配置文件详解
一个Nginx配置文件通常包含3个模块:
全局块:比如工作进程数,定义日志路径;
Events块:设置处理轮询事件模型,每个工作进程最大连接数及http层的keep-alive超时时间;
http块:路由匹配、静态文件服务器、反向代理、负载均衡等。
# 全局块
user www-data;
worker_processes 2; ## 默认1,一般建议设成CPU核数1-2倍
error_log logs/error.log; ## 错误日志路径
pid logs/nginx.pid; ## 进程id
# Events块
events {
# 使用epoll的I/O 模型处理轮询事件。
# 可以不设置,nginx会根据操作系统选择合适的模型
use epoll;
# 工作进程的最大连接数量, 默认1024个
worker_connections 2048;
# http层面的keep-alive超时时间
keepalive_timeout 60;
# 客户端请求头部的缓冲区大小
client_header_buffer_size 2k;
}
# http块
http {
include mime.types; # 导入文件扩展名与文件类型映射表
default_type application/octet-stream; # 默认文件类型
# 日志格式及access日志路径
log_format main '$remote_addr - $remote_user [$time_local] $status '
'"$request" $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log logs/access.log main;
# 允许sendfile方式传输文件,默认为off。
sendfile on;
tcp_nopush on; # sendfile开启时才开启。
# http server块
# 简单反向代理
server {
listen 80;
server_name domain2.com www.domain2.com;
access_log logs/domain2.access.log main;
# 转发动态请求到web应用服务器
location / {
proxy_pass http://127.0.0.1:8000;
deny 192.24.40.8; # 拒绝的ip
allow 192.24.40.6; # 允许的ip
}
# 错误页面
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
# 负载均衡
upstream backend_server {
server 192.168.0.1:8000 weight=5; # weight越高,权重越大
server 192.168.0.2:8000 weight=1;
server 192.168.0.3:8000;
server 192.168.0.4:8001 backup; # 热备
}
server {
listen 80;
server_name big.server.com;
access_log logs/big.server.access.log main;
charset utf-8;
client_max_body_size 10M; # 限制用户上传文件大小,默认1M
location / {
# 使用proxy_pass转发请求到通过upstream定义的一组应用服务器
proxy_pass http://backend_server;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
3.负责均衡算法
round robin轮询(默认)
轮询方式,依次将请求分配到各个后台服务器中,默认的负载均衡方式。
适用于后台机器性能一致的情况。
挂掉的机器可以自动从服务列表中剔除。weight权重
根据权重来分发请求到不同的机器中,指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。
例如:
upstream bakend { server 192.168.0.14 weight=10; server 192.168.0.15 weight=10; }
IP_hash
根据请求者ip的hash值将请求发送到后台服务器中,可以保证来自同一ip的请求被打到固定的机器上,可以解决session问题。
例如:
upstream bakend { ip_hash; server 192.168.0.14:88; server 192.168.0.15:80; }
IP_hash存在节点数量变化导致重新分配节点,从而造成用户session数据、服务器缓存数据丢失问题。解决办法:一致性hash。
一致性hash
根据请求者ip的hash值将请求发送到后台服务器中,可以保证来自同一ip的请求被打到固定的机器上,可以解决session
- 首先求出memcached服务器(节点)的哈希值,并将其配置到0~232的圆(continuum)上。
- 然后采用同样的方法求出存储数据的键的哈希值,并映射到相同的圆上。
- 然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。如果超过232仍然找不到服务器,就会保存到第一台memcached服务器上。
- 从下图的状态中添加一台memcached服务器。余数分布式算法由于保存键的服务器会发生巨大变化而影响缓存的命中率,但Consistent Hashing中,只有在园(continuum)上增加服务器的地点逆时针方向的第一台服务器上的键会受到影响。
另外,一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题。例如系统中只有两台服务器,其环分布如下,
此时必然造成大量数据集中到Node A上,而只有极少量会定位到Node B上。为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器ip或主机名的后面增加编号来实现。例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点:
同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三个虚拟节点的数据均定位到Node A上。这样就解决了服务节点少时数据倾斜的问题。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。
考虑到分布式系统每个节点都有可能失效,并且新的节点很可能动态的增加进来,如何保证当系统的节点数目发生变化时仍然能够对外提供良好的服务,这是值得考虑的,尤其实在设计分布式缓存系统时,如果某台服务器失效,对于整个系统来说如果不采用合适的算法来保证一致性,那么缓存于系统中的所有数据都可能会失效(即由于系统节点数目变少,客户端在请求某一对象时需要重新计算其hash值(通常与系统中的节点数目有关),由于hash值已经改变,所以很可能找不到保存该对象的服务器节点),因此一致性hash就显得至关重要,良好的分布式cahce系统中的一致性hash算法应该满足以下几个方面:
- 平衡性(Balance)
平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。
- 单调性(Monotonicity)
单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲区加入到系统中,那么哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲区中去,而不会被映射到旧的缓冲集合中的其他缓冲区。简单的哈希算法往往不能满足单调性的要求,如最简单的线性哈希:x = (ax + b) mod §,在上式中,P表示全部缓冲的大小。不难看出,当缓冲大小发生变化时(从P1到P2),原来所有的哈希结果均会发生变化,从而不满足单调性的要求。哈希结果的变化意味着当缓冲空间发生变化时,所有的映射关系需要在系统内全部更新。而在P2P系统内,缓冲的变化等价于Peer加入或退出系统,这一情况在P2P系统中会频繁发生,因此会带来极大计算和传输负荷。单调性就是要求哈希算法能够应对这种情况。
- 分散性(Spread)
在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。
- 负载(Load)
负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。
- 平滑性(Smoothness)
平滑性是指缓存服务器的数目平滑改变和缓存对象的平滑改变是一致的。
url_hash(第三方)
根据请求的url的hash值将请求分到不同的机器中,当后台服务器为缓存的时候效率高。
例如:
在upstream中加入hash语句,server语句中不能写入weight等其他的参数,hash_method是使用的hash算法
upstream backend { server squid1:3128; server squid2:3128; hash $request_uri; hash_method crc32; } tips: 复制代码 upstream bakend{#定义负载均衡设备的Ip及设备状态 ip_hash; server 127.0.0.1:9090 down; server 127.0.0.1:8080 weight=2; server 127.0.0.1:6060; server 127.0.0.1:7070 backup; } 复制代码 在需要使用负载均衡的server中增加 proxy_pass http://bakend/; 每个设备的状态设置为: 1.down 表示单前的server暂时不参与负载 2.weight 默认为1.weight越大,负载的权重就越大。 3.max_fails :允许请求失败的次数默认为1.当超过最大次数时,返回proxy_next_upstream 模块定义的错误 4.fail_timeout:max_fails次失败后,暂停的时间。 5.backup: 其它所有的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻。 nginx支持同时设置多组的负载均衡,用来给不用的server来使用。 client_body_in_file_only 设置为On 可以讲client post过来的数据记录到文件中用来做debug client_body_temp_path 设置记录文件的目录 可以设置最多3层目录 location 对URL进行匹配.可以进行重定向或者进行新的代理 负载均衡
fair(第三方)
根据后台响应时间来分发请求,响应时间短的分发的请求多。
例如:
upstream backend { server server1; server server2; fair; }
十五、ES
https://blog.youkuaiyun.com/zhoushimiao1990/article/details/103226496
1.简介
ES是一个基于RESTful web接口并且构建在Apache Lucene之上的开源分布式搜索引擎。同时ES还是一个分布式文档数据库,其中每个字段均可被索引,而且每个字段的数据均可被搜索,能够横向扩展至数以百计的服务器存储以及处理PB级的数据。可以在极短的时间内存储、搜索和分析大量的数据。通常作为具有复杂搜索场景情况下的核心发动机。
2.基本概念
ES中有几个基本概念:索引(index)、类型(type)、文档(document)、映射(mapping)等。我们将这几个概念与传统的关系型数据库中的库、表、行、列等概念进行对比,如下表:
2.1 倒排索引
https://www.cnblogs.com/1832921tongjieducn/p/13618030.html
倒排索引(Inverted Index):倒排索引是实现“单词-文档矩阵”的一种具体存储形式,通过倒排索引,可以根据单词快速获取包含这个单词的文档列表。倒排索引主要由两个部分组成:“单词词典”和“倒排文件”。
单词词典(Lexicon):搜索引擎的通常索引单位是单词,单词词典是由文档集合中出现过的所有单词构成的字符串集合,单词词典内每条索引项记载单词本身的一些信息以及指向“倒排列表”的指针。
倒排列表(PostingList):倒排列表记载了出现过某个单词的所有文档的文档列表及单词在该文档中出现的位置信息,每条记录称为一个倒排项(Posting)。根据倒排列表,即可获知哪些文档包含某个单词。
倒排文件(Inverted File):所有单词的倒排列表往往顺序地存储在磁盘的某个文件里,这个文件即被称之为倒排文件,倒排文件是存储倒排索引的物理文件。
关于这些概念之间的关系,通过下图可以比较清晰的看出来。
3.集群概念
ES中有几个基本概念:索引(index)、类型(type)、文档(document)、映射(mapping)等。我们将这几个概念与传统的关系型数据库中的库、表、行、列等概念进行对比,如下表:
4.Rest风格接口
### 创建 index
PUT http://localhost:9200/questions
### 删除一个Index
DELETE http://localhost:9200/questions
### 设置index中的文档属性采用ik分词
POST http://localhost:9200/questions/_mapping
Content-Type: application/json
{
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word"
},
"content": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word"
}
}
}
### questions 中添加文档
POST http://localhost:9200/questions/_create/1
Content-Type: application/json
{
"id":1,
"title":"Java基本数据类型有哪些",
"content":"面时候为啥要问基本类型这么简单问题呀,我们要如何回答呢?"
}
### questions 中添加文档
POST http://localhost:9200/questions/_create/2
Content-Type: application/json
{
"id":2,
"title":"int类型的范围",
"content":"为啥要了解int类型的范围呢?"
}
### questions 中添加文档
POST http://localhost:9200/questions/_create/3
Content-Type: application/json
{
"id":3,
"title":"常用集合类有哪些",
"content":"为啥企业经常问集合呀?该如何回复呢"
}
### questions 中添加文档
POST http://localhost:9200/questions/_create/4
Content-Type: application/json
{
"id":4,
"title":"线程的run方法和start方法有啥区别",
"content":"run方法可以执行线程的计算过程, start也可以执行线程的计算过程,用途一样么?"
}
### 更新questions索引中的文档
POST http://localhost:9200/questions/_doc/4/_update
Content-Type: application/json
{
"doc": {
"title": "Java线程的run方法和start方法有啥区别"
}
}
### 删除questions中的一个文档
DELETE http://localhost:9200/questions/_doc/2
### 查询数据
GET http://localhost:9200/questions/_doc/4
### 收索 ES
POST http://localhost:9200/questions/_search
Content-Type: application/json
{
"query": { "match": {"title": "类型" } }
}
### 多字段搜索
POST http://localhost:9200/questions/_search
Content-Type: application/json
{
"query": {
"bool": {
"should": [
{ "match": { "title": "java类型" }},
{ "match": { "content": "java类型"}}
]
}
}
}
十六、分布式事务
https://blog.youkuaiyun.com/weixin_44254243/article/details/112613544
1.什么是事务?
数据库事务(简称“事务”,Transaction),是数据库执行过程中的一个逻辑单位,
十七、数据库
1.hbase与hive
两者的共同点:
1.hbase与hive都是架构在hadoop之上的。都是用hadoop作为底层存储两者的区别:
2.Hive是建立在Hadoop之上为了减少MapReduce jobs编写工作的批处理系统,HBase是为了支持弥补Hadoop对实时操作的缺陷的项目 。
3.想象你在操作RMDB数据库,如果是全表扫描,就用Hive+Hadoop,如果是索引访问,就用HBase+Hadoop 。
4.Hive query就是MapReduce jobs可以从5分钟到数小时不止,HBase是非常高效的,肯定比Hive高效的多。
5.Hive本身不存储和计算数据,它完全依赖于HDFS和MapReduce,Hive中的表纯逻辑。
6.hive借用hadoop的MapReduce来完成一些hive中的命令的执行
7.hbase是物理表,不是逻辑表,提供一个超大的内存hash表,搜索引擎通过它来存储索引,方便查询操作。
8.hbase是列存储。所以Hbase可以对数据进行增改删等操作,但Hive是不行的,只能追加数据。
9.HDFS作为底层存储,HDFS是存放文件的系统,而Hbase负责组织文件。
10.hive需要用到HDFS存储文件,需要用到MapReduce计算框架。
十八、IO模型
1.磁盘I/O读写的基本原理
它是一种数据的流从源头流到目的地。比如文件拷贝,包括了输入流和输出流。输入流从文件中读取数据存储到进程(process)中,输出流从进程中读取数据然后写入到目标文件。用户程序进行IO操作,依赖于底层的IO实现,主要是底层的read&write两大系统调用。read系统调用,并不直接从物理设备把数据读取到内存中,而是将数据从内核缓冲区复制到进程缓冲区;write系统调用,也不是直接把数据写入到物理设备中,而是把数据从进程缓冲区复制到内核缓冲区。
2.磁盘I/O流分类
按流向可分为:输入流(InputStream、Reader)和输出流(OutputStream、Writer)
按功能可分为:字节流(InputStream、FileInputStream、BufferedInputStream)和字符流(Reader、BufferedReader)
字节流
的操作以8为为单位对二进制数据进行操作,对数据不需要进行转换,所有的类都是InputStream和OutputStream的子类(以InputStream和OutputStream为后缀)。
字符流
的操作以字符为单位对数据进行操作,在读取的时候将二进制数据转换成字符,在写的时候则是将字符转换成二进制数据,这些类都是Reader和Writer的子类(以Reader和Writer为后缀)。
3.网络I/O模型
BIO (Blocking I/O):同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。NIO提供了与传统BIO模型中的Socket和ServerSocket相对应de的SocketChannel和ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
AIO (Asynchronous I/O): AIO也就是 NIO 2。在 Java 7 中引入了NIO的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。
总结来说的话:
BIO:线程发起 IO 请求,不管内存是否准备好 IO 操作,从发起请求起,线程一直阻塞,直到操作完成。
NIO:线程发起 IO 请求,立即返回;内存做好 IO 操作的准备之后,通过调用注册的回调函数通知线程做 IO 操作,线程开始阻塞,直到操作完成。
AIO:线程发起 IO 请求,立即返回;内存做好 IO 操作的准备之后,做 IO 操作,直到操作完成或者失败,通过调用注册的回调函数通知线程做 IO 操作完成或者失败。
4.按通道分类
以下是一些常见的协议和通信方式,对于全双工通道、半双工通道和单通道进行了分类:
全双工通道的协议/通信方式:
- TCP (Transmission Control Protocol):TCP是一种面向连接的协议,支持全双工通信。它可以在客户端和服务器之间双向传输数据,并提供高可靠性和数据完整性。
- WebSocket:WebSocket是一种在单个 TCP 连接上进行全双工通信的协议,通常用于实时的双向通信,例如聊天应用或实时游戏。
半双工通道的协议/通信方式:
- 对讲机:对讲机是一种半双工通信设备,允许两个或多个用户在一段时间内交替发送和接收语音消息。
- 单根线缆(如 RS-485):RS-485是一种用于串行通信的标准,可以实现半双工通信,即数据在两个方向上进行传输,但不能同时进行。
单通道的协议/通信方式:
- 广播电视:广播电视系统将信息广播给多个接收者,但接收者无法向广播源发送数据或回复。
- 无线电:类似广播电视,无线电通信只能在一个方向上传输,接收者无法向发送者发送数据。
比较优缺点如下:
全双工通道的优点:
- 允许同时进行双向通信,提供更高的通信效率和灵活性。
- 可以实现实时的双向数据交换和互动。
- 通信质量较高,数据可靠性和完整性较好。
全双工通道的缺点:
- 占用更多的资源,包括带宽和处理能力。
- 网络延迟可能会影响通信性能。
半双工通道的优点:
- 比单通道通信更灵活,双方可以在一定时间内交替发送和接收数据。
- 通信资源需求较低,适用于一些资源受限的场景。
半双工通道的缺点:
- 数据传输的速率相对较低,因为双方不能同时发送和接收数据。
- 通信延迟可能会增加,因为需要交替进行发送和接收。
单通道通信的优点:
- 简单、低成本,适用于一些简单的通信场景。
单通道通信的缺点:
- 无法进行双向通信,只能在一个方向上进行数据传输。
- 通信效率较低,数据传输速率较慢。
总结: 全双工通道可以同时进行双向通信,提供高效、可靠的数据传输。半双工通道允许交替进行发送和接收,资源需求较低但传输速率较慢。单通道通信只能在一个方向上传输数据,通信效率较低。选择使用哪种协议或通信方式需要根据具体应用需求和资源限制进行权衡。
5.使用InputStream+NIO来传输网络数据
ChatGPT:根据您的要求,以下是一个使用InputStream和NIO(Non-blocking IO)进行网络数据传输的示例:
import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; public class NIONetworkInputStreamExample { public static void main(String[] args) { try { // 创建一个SocketChannel对象 SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); // 连接到服务器 socketChannel.connect(new InetSocketAddress("www.example.com", 80)); // 等待连接完成 while (!socketChannel.finishConnect()) { // 可以在等待连接的同时执行其他操作 } // 获取输入流 InputStream inputStream = socketChannel.socket().getInputStream(); // 读取网络数据 ByteBuffer buffer = ByteBuffer.allocate(1024); int bytesRead; while ((bytesRead = inputStream.read(buffer.array())) != -1) { buffer.clear(); buffer.limit(bytesRead); String response = new String(buffer.array(), 0, bytesRead); System.out.println(response); } // 关闭输入流和SocketChannel inputStream.close(); socketChannel.close(); } catch (IOException e) { e.printStackTrace(); } } }
在上述例子中,我们使用SocketChannel来创建一个非阻塞的网络连接,并通过connect()方法连接到指定的服务器和端口。在connect()方法之后,我们使用循环等待连接的完成,直到finishConnect()方法返回true。
接下来,我们通过socketChannel.socket().getInputStream()方法获取网络连接的输入流,即从服务器读取数据的流。
然后,我们使用一块ByteBuffer来读取网络数据。在循环中,我们调用inputStream的read()方法,将数据读取到ByteBuffer中。如果read()方法返回-1,表示已经读取完所有的数据,我们就退出循环。
通过将ByteBuffer的position设置为0,limit设置为实际读取的字节数,我们可以使用ByteBuffer的array()方法获取数据,并转换为字符串进行打印。
最后,我们关闭输入流和SocketChannel。
这个示例展示了如何使用InputStream和NIO进行网络数据传输,NIO是一种非阻塞IO模型,可以实现在进行IO操作时继续执行其他任务,不需要阻塞等待IO操作完成。
6.NIO中的Selector是如何使用的,请结合上述代码举例
在Java NIO中,Selector是一个基于事件驱动的多路复用器,用于监视多个Channel的状态并选择已经准备就绪的Channel进行IO操作。
下面是一个结合上述代码的例子,展示如何使用Selector来进行网络数据传输:
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.Set; public class NIONetworkSelectorExample { public static void main(String[] args) { try { // 创建一个Selector对象 Selector selector = Selector.open(); // 创建一个SocketChannel对象 SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); // 连接到服务器 socketChannel.connect(new InetSocketAddress("www.example.com", 80)); // 注册到Selector并监听连接事件 socketChannel.register(selector, SelectionKey.OP_CONNECT); // 循环处理事件 while (true) { // 等待事件 selector.select(); // 获取已触发的事件集合 Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); // 处理事件 while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); // 处理连接事件 if (key.isConnectable()) { SocketChannel channel = (SocketChannel) key.channel(); // 完成连接 if (channel.finishConnect()) { // 注册读事件 channel.register(selector, SelectionKey.OP_READ); // 发送数据 String requestData = "Hello, World!"; byte[] requestBytes = requestData.getBytes(); ByteBuffer buffer = ByteBuffer.wrap(requestBytes); while (buffer.hasRemaining()) { channel.write(buffer); } } } // 处理读事件 if (key.isReadable()) { SocketChannel channel = (SocketChannel) key.channel(); // 读取数据 ByteBuffer buffer = ByteBuffer.allocate(1024); int bytesRead = channel.read(buffer); if (bytesRead > 0) { buffer.flip(); byte[] responseBytes = new byte[bytesRead]; buffer.get(responseBytes); String response = new String(responseBytes); System.out.println(response); } else if (bytesRead == -1) { // 读取到流的末尾,需要关闭连接 channel.close(); } } // 从已触发的事件集合中移除当前事件 keyIterator.remove(); } } } catch (IOException e) { e.printStackTrace(); } } }
在上述例子中,我们创建了一个Selector对象,并打开一个SocketChannel,并将其配置为非阻塞模式。
然后,我们使用SocketChannel的connect()方法连接到服务器,并将SocketChannel注册到Selector上,并监听连接事件(OP_CONNECT)。
接下来,我们进入一个无限循环,在循环中调用selector的select()方法来等待已经准备就绪的事件。如果没有事件准备就绪,select()方法将会阻塞,直到有事件发生。
一旦有事件触发,我们通过selectedKeys()方法获取所有已经准备就绪的事件集合,并使用迭代器遍历。
然后,我们根据事件的类型(isConnectable()、isReadable())来处理不同的事件。
在连接事件(OP_CONNECT)中,我们通过SocketChannel的finishConnect()方法来确认连接是否完成。如果连接完成,我们注册读事件(OP_READ),并使用SocketChannel的write()方法将数据发送给服务器。
在读事件(OP_READ)中,我们创建一个ByteBuffer来读取数据,并使用SocketChannel的read()方法将数据读取到ByteBuffer中。然后,我们根据读取的字节数来处理数据。
最后,我们从已触发的事件集合中移除当前事件,以便下一次循环能正确获取新的已触发事件集合。
通过使用Selector,我们可以将多个Channel注册到同一个Selector上,并监视它们的状态。当有事件发生时,我们可以选择性地处理已准备就绪的事件,而不需要为每个Channel创建一个线程来处理。这样可以大大提高系统的并发性能。
7.TCP与UCP
https://www.bilibili.com/video/BV1kV411j7hA/?spm_id_from=333.337.search-card.all.click&vd_source=709265081bd4c68f0764abc8857d9693
TCP
三次握手: 客户端带着
SYN
包向服务端发起连接,服务端收到连接请求; 服务端带着
SYN
包和ACK
包回复客户端同意请求; 客户端收到回复后对SYN和ACK包进行验证,验证通过后给服务端发送一个
ACK
包作为对服务端发过来的包验证通过的确认。四次挥手:客户端带着
FIN
包向服务端请求关闭连接,并表示自己不会再发送新的数据,但可以接收数据,自己进入等待关闭状态; 服务端接收到FIN包后,发送一个
ACK
包作为确认,表示收到FIN包,可以继续接收传输中的数据,但要求对方不可以发送新数据; 服务端确认没有传输中的数据后,发送
FIN
包给客户端,表示自己也不会再发送数据,请求关闭连接,自己进入等待关闭状态,若等到超时没有收到对方的ACK,会重发FIN包; 客户端收到FIN包后,发送一个
ACK
包表示确认关闭,继续进入等待状态,若等待超时服务端依然没有给自己发送数据,即关闭连接; 服务端收到ACK后立即关闭连接;
❤三次握手和四次挥手都解决了信道不可靠的问题,使连接可以可靠地连接和可靠地断开连接。
在 TCP 连接建立的过程中,客户端发送连接请求后,如果收到服务端发送过来的 SYN 包和 ACK 包(通常是 SYN+ACK 包),客户端会进行以下验证:
- 检查ACK号是否合法: 客户端会检查 ACK 包中的确认号(Acknowledgment Number)是否合法。合法的确认号应该是客户端发送的 SYN 包的序列号加1,表示客户端期待接收到服务端发送的下一个数据包的序号。
- 验证TCP标志位: 客户端会检查 ACK 包中的 TCP 标志位,确认 ACK 标志位是否被设置,表明服务端接收到了客户端发送的 SYN 包。
- 检查连接状态: 如果 ACK 包合法且包含了正确的 TCP 标志位,客户端就会认为连接请求被服务端正确处理了,并且 TCP 连接已经建立。客户端将发送最后一个 ACK 包作为对服务端 SYN+ACK 包的确认。
- 其他验证: 在某些情况下,客户端还可能进行一些额外的验证,如检查服务端的 通信端口号、TCP选项字段、窗口大小等,以确保连接的安全和可靠性。
总的来说,客户端会根据 TCP 协议规定的验证流程来验证服务端发送的 SYN+ACK 包,以确保连接的正确建立。如果验证通过,客户端会发送最后一个 ACK 包作为连接的确认,完成 TCP 三次握手建立连接的过程。