Java Virtual Machine Java程序的运行环境(java二进制字节码的运行环境)
好处:
从下图中可以看出 JVM 的主要组成部分
运行流程:
(1)类加载器(ClassLoader)把Java代码转换为字节码
(2)运行时数据区(Runtime Data Area)把字节码加载到内存中,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层系统去执行,而是有执行引擎运行
(3)执行引擎(Execution Engine)将字节码翻译为底层系统指令,再交由CPU执行去执行,此时需要调用其他语言的本地库接口(Native Method Library)来实现整个程序的功能。
程序计数器:线程私有的,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址。
javap -verbose xx.class 打印堆栈大小,局部变量的数量和方法的参数。
java虚拟机对于多线程是通过线程轮流切换并且分配线程执行时间。在任何的一个时间点上,一个处理器只会处理执行一个线程,如果当前被执行的这个线程它所分配的执行时间用完了【挂起】。处理器会切换到另外的一个线程上来进行执行。并且这个线程的执行时间用完了,接着处理器就会又来执行被挂起的这个线程。
那么现在有一个问题就是,当前处理器如何能够知道,对于这个被挂起的线程,它上一次执行到了哪里?那么这时就需要从程序计数器中来回去到当前的这个线程他上一次执行的行号,然后接着继续向下执行。
程序计数器是JVM规范中唯一一个没有规定出现OOM的区域,所以这个空间也不会进行GC。
线程共享的区域:主要用来保存对象实例,数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
线程共享的区域:主要用来保存对象实例,数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
1、栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的的。堆会GC垃圾回收,而栈不会。
2、栈内存是线程私有的,而堆内存是线程共有的。
3,、两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常。
栈空间不足:java.lang.StackOverFlowError。
堆空间不足:java.lang.OutOfMemoryError。
要想理解类加载器的话,务必要先清楚对于一个Java文件,它从编译到执行的整个过程。
类加载器
JVM只会运行二进制文件,而类加载器(ClassLoader)的主要作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。现有的类加载器基本上都是java.lang.ClassLoader的子类,该类的只要职责就是用于将指定的类找到或生成对应的字节码文件,同时类加载器还会负责加载程序所需要的资源
类加载器种类
类加载器根据各自加载范围的不同,划分为四种类加载器:
该类并不继承ClassLoader类,其是由C++编写实现。用于加载JAVA_HOME/jre/lib目录下的类库。
该类是ClassLoader的子类,主要加载JAVA_HOME/jre/lib/ext目录中的类库。
该类是ClassLoader的子类,主要用于加载classPath下的类,也就是加载开发者自己编写的Java类。
开发者自定义类继承ClassLoader,实现自定义类加载规则。
上述三种类加载器的层次结构如下如下:
类加载器的体系并不是“继承”体系,而是委派体系,类加载器首先会到自己的parent中查找类或者资源,如果找不到才会到自己本地查找。类加载器的委托行为动机是为了避免相同的类被加载多次。
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就返回成功;只有父类加载器无法完成此加载任务时,才由下一级去加载。
(1)通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
(2)为了安全,保证JDK自带类库API不会被修改
类从加载到虚拟机中开始,直到卸载为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。
类加载过程详解
1.加载
通过类的全名,获取类的二进制数据流。
解析类的二进制数据流为方法区内的数据结构(Java类模型)
创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口
2.验证
验证类是否符合JVM规范,安全性检查
(1)文件格式验证:是否符合Class文件的规范 (2)元数据验证
这个类是否有父类(除了Object这个类之外,其余的类都应该有父类)
这个类是否继承(extends)了被final修饰过的类(被final修饰过的类表示类不能被继承)
类中的字段、方法是否与父类产生矛盾。(被final修饰过的方法或字段是不能覆盖的)
(3)字节码验证
主要的目的是通过对数据流和控制流的分析,确定程序语义是合法的、符合逻辑的。
(4)符号引用验证:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量
比如:int i = 3; 字面量:3 符号引用:i
3.准备
为类变量分配内存并设置类变量初始值
static变量,分配空间在准备阶段完成(设置默认值),赋值在初始化阶段完成
static变量是final的基本类型,以及字符串常量,值已确定,赋值在准备阶段完成(编译阶段完成赋值)
static变量是final的引用类型,那么赋值也会在初始化阶段完成
4.解析
把类中的符号引用转换为直接引用
比如:方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接指向方法。
5.初始化
对类的静态变量,静态代码块执行初始化(将静态变量+静态代码块自上而下合并到clinit方法)操作
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
6.使用
JVM 开始从入口方法开始执行用户的程序代码
调用静态类成员信息(比如:静态字段、静态方法)
使用new关键字为其创建对象实例
7.卸载
当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存
小结:
为了让程序员更专注于代码的实现,而不用过多的考虑内存释放的问题,所以,在Java语言中,有了自动的垃圾回收机制,也就是我们熟悉的GC(Garbage Collection)。
有了垃圾回收机制后,程序员只需要关心内存的申请即可,内存的释放由系统自动识别完成。
在进行垃圾回收时,不同的对象引用类型,GC会采用不同的回收时机
换句话说,自动的垃圾回收的算法就会变得非常重要了,如果因为算法的不合理,导致内存资源一直没有释放,同样也可能会导致内存溢出的。
当然,除了Java语言,C#、Python等语言也都有自动的垃圾回收机制。
简单一句就是:如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。
如果要定位什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法
一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收
String demo = new String("123");
String demo = null;
当对象间出现了循环引用的话,则引用计数法就会失效
先执行右侧代码的前4行代码
目前上方的引用关系和计数都是没问题的,但是,如果代码继续往下执行,如下图
虽然a和b都为null,但是由于a和b存在循环引用,这样a和b永远都不会被回收。
优点:
缺点:
现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾。
会存在一个根节点【GC Roots】,引出它下面指向的下一个节点,再以下一个节点节点开始找出它下面的节点,依次往下类推。直到所有的节点全部遍历完毕。
根对象是那些肯定不能当做垃圾回收的对象,就可以当做根对象
局部变量,静态方法,静态变量,类信息
核心是:判断某对象是否与根对象有直接或间接的引用,如果没有被引用,则可以当做垃圾回收
X,Y这两个节点是可回收的,但是并不会马上的被回收!! 对象中存在一个方法【finalize】。当对象被标记为可回收后,当发生GC时,首先会判断这个对象是否执行了finalize方法,如果这个方法还没有被执行的话,那么就会先来执行这个方法,接着在这个方法执行中,可以设置当前这个对象与GC ROOTS产生关联,那么这个方法执行完成之后,GC会再次判断对象是否可达,如果仍然不可达,则会进行回收,如果可达了,则不会进行回收。
finalize方法对于每一个对象来说,只会执行一次。如果第一次执行这个方法的时候,设置了当前对象与RC ROOTS关联,那么这一次不会进行回收。 那么等到这个对象第二次被标记为可回收时,那么该对象的finalize方法就不会再次执行了。
GC ROOTS:
虚拟机栈(栈帧中的本地变量表)中引用的对象
/**
* demo是栈帧中的本地变量,当 demo = null 时,由于此时 demo 充当了 GC Root 的作用,demo与原来指向的实例 new Demo() 断开了连接,对象被回收。
*/
public class Demo {
public static void main(String[] args) {
Demo demo = new Demo();
demo = null;
}
}
方法区中类静态属性引用的对象
/**
* 当栈帧中的本地变量 b = null 时,由于 b 原来指向的对象与 GC Root (变量 b) 断开了连接,所以 b 原来指向的对象会被回收,而由于我们给 a 赋值了变量的引用,a在此时是类静态属性引用,充当了 GC Root 的作用,它指向的对象依然存活!
*/
public class Demo {
public static Demo a;
public static void main(String[] args) {
Demo b = new Demo();
b.a = new Demo();
b = null;
}
}
方法区中常量引用的对象
/**
* 常量 a 指向的对象并不会因为 demo 指向的对象被回收而回收
*/
public class Demo {
public static final Demo a = new Demo();
public static void main(String[] args) {
Demo demo = new Demo();
demo = null;
}
}
本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。
1.根据可达性分析算法得出的垃圾进行标记
2.对这些标记为可回收的内容进行垃圾回收
可以看到,标记清除算法解决了引用计数算法中的循环引用的问题,没有从root节点引用的对象都会被回收。
同样,标记清除算法也是有缺点的:
复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。
如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合。
1)将内存区域分成两部分,每次操作其中一个。
2)当进行垃圾回收时,将正在使用的内存区域中的存活对象移动到未使用的内存区域。当移动完对这部分内存区域一次性清除。
3)周而复始。
优点:
缺点:
标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而解决了碎片化的问题。
1)标记垃圾。
2)需要清除向右边走,不需要清除的向左边走。
3)清除边界以外的垃圾。
优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响。
与复制算法对比:复制算法标记完就复制,但标记整理算法得等把所有存活对象都标记完毕,再进行整理
在java8时,堆被分为了两份:新生代和老年代【1:2】,在java7时,还存在一个永久代。
对于新生代,内部又被分为了三个区域。Eden区,S0区,S1区【8:1:1】
当对新生代产生GC:MinorGC【young GC】
当对老年代代产生GC:Major GC
当对新生代和老年代产生FullGC: 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免
工作机制
当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象
将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放
MinorGC、 Mixed GC 、 FullGC的区别是什么
MinorGC【young GC】发生在新生代的垃圾回收,暂停时间短(STW)
Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有
FullGC: 新生代 + 老年代完整垃圾回收,暂停时间长(STW),应尽力避免
名词解释:
STW(Stop-The-World):暂停所有应用程序线程,等待垃圾回收的完成
原因:
如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量的时候,java虚拟机将抛出一个StackOverFlowError异常
如果java虚拟机栈可以动态拓展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成拓展,或者在建立新线程的时候没有足够的内存去创建对应的虚拟机栈,那java虚拟机将会抛出一个OutOfMemoryError异常
如果一次加载的类太多,元空间内存不足,则会报OutOfMemoryError: Metaspace
1、通过jmap指定打印他的内存快照 dump
有的情况是内存溢出之后程序则会直接中断,而jmap只能打印在运行中的程序,所以建议通过参数的方式的生成dump文件,配置如下:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/app/dumps/ 指定生成后文件的保存目录
2、通过工具, VisualVM(Ecplise MAT)去分析 dump文件
VisualVM可以加载离线的dump文件,如下图
文件-->装入--->选择dump文件即可查看堆快照信息
如果是linux系统中的程序,则需要把dump文件下载到本地(windows环境)下,打开VisualVM工具分析。VisualVM目前只支持在windows环境下运行可视化
3、通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题
4、找到对应的代码,通过阅读上下文的情况,进行修复即可
1.使用top命令查看占用cpu的情况
2.通过top命令查看后,可以查看是哪一个进程占用cpu较高,上图所示的进程为:30978
3.查看当前线程中的进程信息
ps H -eo pid,tid,%cpu | grep 40940
pid 进行id
tid 进程中的线程id
% cpu使用率
4.通过上图分析,在进程30978中的线程30979占用cpu较高
注意:上述的线程id是一个十进制,我们需要把这个线程id转换为16进制才行,因为通常在日志中展示的都是16进制的线程id名称
转换方式:
在linux中执行命令
printf "%x\n" 30979
5.可以根据线程 id 找到有问题的线程,进一步定位到问题代码的源码行号
执行命令
jstack 30978 此处是进程id
有做过jvm的调优吗?常用的jvm参数调优有哪些?
回答话术: jvm参数之前在工作的时候也偶尔做过,但是也不能完全记得住,每次都是在需要的时候去查询文档 一般情况下都是使用默认值,只有在真正需要调优的时候会去重新设置值去覆盖默认值...停顿几秒思考 根据我的回忆,jvm参数分为三种 标准参数: 主要用于查看一些基本信息 比如jvm版本号 X参数: 用于设置内存大小 基本都是传给 JVM 的,默认 JVM 实现这些参数的功能,但是并不保证所有 JVM 实 现都满足,且不保证向后兼容 稳定性好 XX参数 用于设置内存大小 专门用于控制 JVM 的行为,跟具体的 JVM 实现有关,随时可能会在下个版本取消 稳定性差一些 接下来举例说明几个常用的参数 -Xms 内存的初始值大小 以M(兆)为单位 默认为系统内存的1/64 -Xmx 内存的最大值 也是以M(兆)为单位 最大值为系统内存的1/4 一般情况下,会将如下两个参数设置为相同的值,可以避免jvm内存的自动扩展,因为当堆内存大小发生扩 展的时候,就会发生内存的抖动,会影响到程序的稳定性 -Xmn 用于设置新生代的内存大小,一般设置为堆空间的1/3或者1/4,因为新生代大的话,老年代就会变小 -Xss 用于设置每个线程的虚拟机栈的大小也即堆栈的大小 -XX -XX:+UseG1GC、-XX:+UseZGC 设置垃圾回收器 当然还有很多的参数,一般都是需要的时候会按照文档就行设置,具体的就记不住那么多了....
-Xmx1024M 最大堆内存
-Xms1024M 初始化堆内存,正常和最大堆内存相同,减少动态改变的内存损耗
-Xmn384M 年轻代内存
-XX:+PrintGCDetails 打印gc信息,可参考gc的比例进行调优
-XX:+UseConcMarkSweepGC 老年代使用cms,标记-清除算法会产生碎片
-XX:+UseParNewGC 年轻代使用并行收集器
如果jvm持续一段时间频繁的发生Young GC (轻GC) 可能原因有哪些?
首先频繁的YGC 说明会频繁的创建对象并立马被回收了 我想造成这个问题的原因可能有两点 第一点: for循环内部 频繁的创建局部对象从而导致频繁的GC 当然也有可能是死循环导致的 第二点: 年轻代内存大小设置不足,无法满足程序逻辑的需要从而导致频繁GC 为了解决这个问题,要么就是减少对象的创建,当然这个要修改程序,难度和周期都比较长一些,要么就是增大年轻代的内存大小,这样可以更快的解决这个问题.当然了,最可靠的还是修改代码,通过GC日志定位问题
string list hash zet zset
Redis服务器的内存大小可以根据需求进行配置,可以配置为几十MB到几十GB不等。具体而言,可以在redis.conf配置文件中通过 maxmemory 参数来设置Redis实例的最大内存限制。例如,如果要将Redis实例的最大内存限制设置为1GB,则可以将该参数设置为maxmemory 1GB不设置该参数,则Redis将使用系统的所有可用内存。如果一台服务器中不会再安装除了redis之外的其他机器时,最佳内存值为服务器内存的3/4
持久化就是将内存的数据写入到磁盘当中,防止服务突然宕机,造成内存数据的丢失。
AOF无用命令过多存入aof文件解决办法:
执行 BGREWRITEAOF 命令, Redis 将生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令
同时开启RDB跟AOF持久化机制并非混合模式:RDB用来做备份
RDB+AOF混合方式 1 开启混合方式设置 设置aof-use-rdb-preamble的值为 yes yes表示开启,设置为no表示禁用
2 RDB+AOF的混合方式--------->结论:RDB镜像做全量持久化,AOF做增量持久化(增量记录RDB期间正在执行写命令存入AOF),先使用RDB进行快照存储,然后使用AOF持久化记录所有的写操作,当重写策略满足或手动触发重写的时候,将最新的数据存储为新的RDB记录,这样的话,重启服务的时候会从RDB和AOF两部分恢复数据,既保证了数据完整性,又提高了恢复数据的性能。简单来说:混合持久化方式产生的文件一部分是RDB格式,一部分是AOF格式。----》AOF包括了RDB头部+AOF混写
Redis的过期策略是指在Redis中用来决定已过期的键(key)应该如何处理的机制。Redis中的过期键是指在一段时间(设置TTL)之后会被自动删除的键。Redis通过定期检查键是否过期来实现这种自动删除
Redis的过期策略主要有两种:
定期清理有两种模式:
总结:
Redis的过期删除策略: 惰性删除(空间换时间) + 定期删除(时间换空间)两种策略进行配合使用
其实就是想问redis的数据淘汰策略是什么?
内存淘汰策略是当Redis的内存使用达到上限,需要申请额外空间时触发的一种机制,它的目的是为了清除不再使用的缓存数据,从而释放更多的内存空间
Redis的内存淘汰机制有8种:
allkeys-lru(Least Recently Used):Redis会遍历所有键值对,选择最近使用的键值对进行删除。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高 (推荐使用)
allkeys-random:Redis会随机选择一些键值对进行删除。
volatile-lru:Redis只会删除设置了过期时间的键值对中最近使用的那些。
volatile-random:Redis只会随机删除设置了过期时间的键值对。
volatile-ttl:Redis只会删除设置了过期时间的键值对中,剩余时间最短的那些。
allkeys-lfu(Least Frequently Used): 对全体key,基于LFU算法进行淘汰 即最少使用频率 会统计每个key的访问频率,值越小淘汰优先级越高。
volatile-lfu:对设置了TTL的key,基于LFU算法进行淘汰
数据淘汰策略-使用建议
优先使用 allkeys-lru 策略。充分利用 LRU 算法的优势,把最近最常访问的数据留在缓存中如果业务有明显的冷热数据区分,建议使用。
如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用 allkeys-random,随机选择淘汰。
如果业务中有置顶的需求,可以使用 volatile-lru 策略,同时置顶数据不设置过期时间,这些数据就一直不删除会淘汰其他设置过期时间的数据
如果业务中有短时高频访问的数据,可以使用 allkeys-lfu 或 volatile-lfu 策略
数据库有1000万数据,Redis只能缓存20w数据如何保证Redis中的数据都是热点数据?
使用allkeys-lfu(挑选最近访问频率较低的数据淘汰)淘汰策略,留下来的都是经常访问的热点数据
主要看数据淘汰策略是什么? 如果是默认的配置(noeviction ),会直接报错
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发读能力,就需要搭建主从集群,实现读写分离。
缺点: 无法保证redis高可用 一旦主节点挂掉,就会丧失写数据的能力 为了保证redis集群的高可用性,redis提供了哨兵模式
redis提供了哨兵(sentinel)机制来实现主从集群的自动故障恢复,是保证redis集群高可用的一种手段.哨兵的结构和作用如下:
哨兵的作用:
监控: Sentinel会不断检查master和slave是否按预期工作 ping/pong 机制
自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
通知: Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
工作流程:
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
客观下线: 若超过指定数量(可在redis配置文件中设置)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数的一半
Sentinel选举-目的选出完成故障转移Sentinel节点
哨兵选主规则
首先判断主与从节点断开时间长短,如超过指定值就排除该从节点
然后判断从节点的slave-priority值,越小优先级越高 (slave-priority是一个配置选项,用来指定一个从服务器(slave)在主服务器(master)选举过程中的优先级)
如果slave-prority一样,则判断slave节点的offset值,越大优先级越高 一个更高的偏移量意味着该从服务器接收了更多的数据,因此它的数据更加完整,更接近于原主服务器的当前状态。
最后是判断slave节点的运行id大小,越小优先级越高
关于脑裂问题的分析:
集群脑裂是由于主节点和从节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到主节点,所以通过洗举的方式提升了一个从节点为主,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将老的主节点降为从节点,这时再从新master同步数据,就会导致数据丢失
解决:
可以修改redis的配置,可以设置最少的从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求就可以避免大量的数据丢失
话术:
你们使用redis是单点还是集群,哪种集群?
主从(1主1从)+哨兵就可以。单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点 尽量不做分片集群。因为集群维护起来比较麻烦,并且集群之间的心跳检测和数据通信会消耗大量的网络带宽,也没有办法使用lua脚本和事务
redis哨兵模式 哨兵先启动,服务后启动 哨兵还会监视redis服务吗 后面主挂了,哨兵选举还生效吗?
即使哨兵先启动,在Redis服务(主节点或从节点)启动之前,哨兵也会尝试连接并监控配置中指定的Redis节点。如果在哨兵启动时Redis服务尚未运行,哨兵会定期尝试重新连接,直到服务启动并且可以被哨兵监控到。因此,即使哨兵先于Redis服务启动,它仍然能够在Redis服务启动后正常监控Redis服务。
在哨兵模式的基础之上可以解决海量数据存储问题和高并发写问题
特征和作用
集群中有多个master,每个master保存不同数据,每个master都可以有多个slave节点
master之间通过ping监测彼此健康状态(自动完成故障转移)
客户端请求可以访问集群任意节点,最终都会被转发到正确节点
redis存储和读取数据规则:
Redis 分片集群引入了哈希槽的概念,Redis 集群有 16384 个哈希,每个 key通过CRC16 校验后对16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。
| 核心组件 | 概念 | | ---------------------------------------- | ------------------------------------------------------------ | | Producer | 生产者,可以向Broker topic发布消息的客户端 | | Consumer | 消费者,从Broker topic订阅取消息的客户端 | | Broker | Broker英文翻译为经纪人 是一个kafka实例,简单说就是一台kafka服务器,kafkaCluster表示集群。一个Linux服务器上可以启动一个或多个 kafka服务器实例。 | | Topic | 主题,Kafka将消息分门别类,每一类的消息称之为一个主题。可以理解为一个队列,生产者和消费者面向的都是一个topic | | Partition | Topic的分区,每个 Topic 可以有多个分区,同一个Topic 在不同分区的数据是不重复的,每个Partition是一个有序的队列。分区作用是做负载,提高 kafka 的吞吐量。每个partition都由一系列有序的、不可变的消息组成,这些消息被连续的追加到partition中。partition中的每个消息都有一个连续递增的序列号叫做offset,偏移量offset在每个分区中是唯一的。 话题下分区只支持增加,不支持减少 | | Offset | 生产者Offset:消息写入的时候,每一个分区都有一个offset,这个offset就是生产者的offset,同时也是这个分区的最新最大的offset。 消费者Offset:某个分区的offset情况。例如:生产者写入的offset是最新值是10,当一个Consumer开始消费时,从0消费,一直消费到了5,消费者的offset为5。 | | Replication | Partition(分区)的副本。每个分区可以有多个Replication,由一个Leader和若干个Follower组成。Leader负责接收生产者push的消息和消费者poll消费消息。Follower会实时从自己的Leader中同步数据保持同步。Leader故障时,某个Follower会上位为新的Leader。保证高可用。分区副本数量(包含leader)不能大于>Broker数量 | | Message | kafka集群存储的消息是以topic为类别记录的,每个消息(也叫记录record)是由生产者进行发送(ProducerRecord),消费者进行消费(ConsumerRecords) | | ConsumerGroup(CG) | 消费者组,由多个Consumer组成,每个ConsumerGroup中可以有多个consumer,每个consumer属于一个ConsumerGroup。同一个Topic下的某一个分区只能被某个消费者组内的同一个消费者所消费,但可以被多个 consumer group 消费 | | In-sync Replicas(ISR) | (ISR)已同步副本:表示存活且副本都已和Leader同步的的broker集合,是Leader所有replicas副本的子集。如果某个副本节点宕机,该副本就会从ISR集合中剔除。 |
Kafka中的Broker、Topic、Consumer都会注册到zookeeper
要注意理解:
topic是一个逻辑概念,真正存储数据的是分区 分区中数据是分段存储的(segment)
由于ZooKeeper是一个为分布式系统设计的协调服务,它非常适合用于管理Kafka这样的分布式消息系统。然而,ZooKeeper自身也是一个复杂的系统,需要单独管理和维护,这也是为什么Kafka社区在最新版本中努力减少对ZooKeeper的依赖。在Kafka 2.8.0及以后的版本中,引入了KRaft模式(Kafka自己的Raft实现),旨在使Kafka不再依赖ZooKeeper。这种模式仍在不断发展和完善中。
RabbitMQ(双端(生产者,消费者)确认+持久化)
或者消息的可靠性
消息丢失的场景有如下三种
1,设置异步发送
案例代码如下
//同步发送 一般不采用这种方式
RecordMetadata recordMetadata = kafkaProducer.send(record).get();
//异步发送
kafkaProducer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (e != null) {
System.out.println("消息发送失败 | 记录日志");
}
//获取消息信息
long offset = recordMetadata.offset();
int partition = recordMetadata.partition();
String topic = recordMetadata.topic();
}
});
2,消息重试
//设置重试次数
prop.put(ProducerConfig.RETRIES_CONFIG,10);
发送确认机制acks
确认机制 | 说明 |
---|---|
acks=0 | 生产者在成功写入消息之前不会等待任何来自服务器的响应,消息有丢失的风险,但是速度最快 |
acks=1(默认值) | 只要集群首领节点收到消息,生产者就会收到一个来自服务器的成功响应 |
acks=-1/all | 只有当所有参与赋值的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应 |
注意: kafka的每个分区都是按照偏移量进行存储数据的 如下图所示
消费者组中的消费者假如按照如下的消费流程执行
P1消费到了偏移量4了, 那么下次消费就会从5开始,这样才不会导致重复消费,或者是丢失消息 也就是说每个消费者消费者消费完消息都会做一个标记 该标记称为消费者偏移量 那么下次消费找对应的偏移量即可
消费者默认是自动按期提交已经消费的偏移量,默认是每隔5S提交一次,如果出现重平衡的情况,可能会重复消费或者丢失消息数据
重平衡示意图: 消费者2宕机 那么它负责的分区3就需要其他消费者进行消费
如果消费者2已经消费到了3位置,也提交了偏移量3 此时是没有问题的
如果消费者2已经消费到了6为止,但是只提交到了3,新接手的消费者1就会从3继续往后消费,此时3-6中间的消息就会被重复消费
如果消费者2在宕机之后将偏移量提交到了3,但它实际才消费了1,新接收的消费者1就会从3就行往后消费 此时1-2之间的消息就会丢失
导致以上可能存在问题的本质就是由于消息者提交消费偏移量的过程是自动的 如何解决呢?
禁止自动提交偏移量,改为在代码中手动进行提交
手动提交方式有如下三种
总结:
需要从三个层面去解决这个问题:
应用场景:
topic分区中消息只能由消费者组中的唯一一个消费者处理,所以消息肯定是按照先后顺序进行处理的。但是它也
仅仅是保证Topic的一个分区顺序处理,不能保证跨分区的消息先后处理顺序。 所以,如果你想要顺序的处理
Topic的所有消息,那就只提供一个分区。
做如下操作:
总结:
Kafka 的服务器端由被称为 Broker 的服务进程构成,即一个 Kafka 集群由多个 Broker 组成
这样如果集群中某一台机器宕机,其他机器上的 Broker 也依然能够对外提供服务。这其实就是 Kafka 提供高可用
的手段之一
一个topic有多个分区,每个分区有多个副本,其中有一个leader,其余的是follower 副本存储在不同的broker中
所有的分区副本的内容是都是相同的,如果leader发生故障时,会自动将其中一个follower提升为leader
如下图所示:
关于follower有更详细的分类和重新选择主节点的关键因素 即分区备份机制
如图所示:
ISR(in-sync replica)需要同步复制保存的follower 数据更加完整
普通 需要异步复制保存的follower
如果leader失效后,需要选出新的leader,选举的原则如下:
第一:选举时优先从ISR中选定,因为这个列表中follower的数据是与leader同步的
第二:如果ISR列表中的follower都不行了,就只能从其他follower中选取
可以通过如下配置进行设置:
总结:
可以从两个层面回答,第一个是集群,第二个是复制机制
集群:
一个kafka集群由多个broker实例组成,即使某一台宕机,也不耽误其他broker继续对外提供服务
复制机制:
一个topic有多个分区,每个分区有多个副本,有一个leader,其余的是follower,副本存储在不同的broker中
所有的分区副本的内容是都是相同的,如果leader发生故障时,会自动将其中一个follower提升为leader,保证了
系统的容错性、高可用性
解释一下复制机制中的ISR?
ISR(in-sync replica)需要同步复制保存的follower
分区副本分为了两类,一个是ISR,与leader副本同步保存数据,另外一个普通的副本,是异步同步数据,当
leader挂掉之后,会优先从ISR副本列表中选取一个作为leader