diff --git "a/2020/09/01/JVM\345\246\202\344\275\225\345\212\240\350\275\275Java\347\261\273/index.html" "b/2020/09/01/JVM\345\246\202\344\275\225\345\212\240\350\275\275Java\347\261\273/index.html" index 7f2b3ab..569cdfb 100644 --- "a/2020/09/01/JVM\345\246\202\344\275\225\345\212\240\350\275\275Java\347\261\273/index.html" +++ "b/2020/09/01/JVM\345\246\202\344\275\225\345\212\240\350\275\275Java\347\261\273/index.html" @@ -32,8 +32,8 @@ - + @@ -317,8 +317,8 @@
加载是指查找字节流,并且据此创建类的过程。加载的class文件可以来源于本地磁盘,也可以来自于网络或者运行时计算生成等等。在加载阶段需要完成以下三件事:
-在加载完成以后,外部的二进制流就会按照设定的格式存储在方法区之中。
-对于任意一个类,都必须由加载它的类加载器和这个类本身一起确立其在Java虚拟机中的唯一性。这会影响例如equals()方法,isAssignableForm()方法以及instanceof的判定。下面的代码就演示了不同的类加载器对instanceof的影响,其中Java虚拟机中同时存在了两个Main类,一个由虚拟机的应用程序加载类进行加载,另一个使用自定义的ClassLoader进行加载。
-public class Main { |
除了启动类加载器,其余的加载器都是java.lang.ClassLoader的子类。
-启动类加载器(bootstrap class loader)由C++实现,没有对应的Java对象。其他的类加载器都需要先由另一个类加载器加载至java虚拟机中,才能执行类加载的工作。
-启动类加载器负责加载最为基础重要的类(比如JAVA_HOME/jre/lib/rt.jar、resource.jar或sun.boot.class.path路径下的内容)。启动类加载器只会加载包名为java,javax和sun开头的类。
-扩展类加载器(JAVA9以后称为平台类加载器)的父类加载器是启动类加载器,在sun.misc.Launcher$ExtClassLoader中以Java实现。它负责加载相对次要、但又通用的类,(从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库)。
-应用类加载器的父类加载器则是扩展类加载器,在sun.misc.Launcher$AppClassLoader中以Java实现。。它负责加载应用程序路径下的类。(负责加载环境变量classpath或系统属性java.class.path指定路径下的类库)。如果应用程序中没有定义过自己的加载器,那么会是程序中默认的加载器。
-除了Java提供的类加载器之外,还可以加入自定义的类加载器,来实现特殊的加载方式。例如可以对class文件进行加密,加载时再利用自定义的类加载器进行解密。
-当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。这一规则被称为双亲委派机制。双亲委派模型要求除了启动类加载器,都必须要有自己的父类加载器。双亲委派机制可以避免类的重复加载,同时可以防止核心API库被篡改。双亲委派模型的代码在loadClass之中。
-protected Class<?> loadClass(String name, boolean resolve) |
从上面的代码也可以看出破环双亲委派模式的方法,就是直接在子加载器中覆盖loadClass()方法。在编写自己的类加载逻辑时,应该尽量去重写findClass方法来完成加载,来防止双亲委派模型被破坏。
-链接,是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段。
-在验证阶段,需要确保被加载的类能够满足java虚拟机的约束条件。
-准备阶段的目的是为了给被加载类的静态字段分配内存,将其初始化为默认值。例如以下一段Java代码
-public static int value=1; //在准备阶段赋值为0 |
该代码在准备阶段只会将value的值设置为0,而将value设置为1则会在之后的初始化阶段进行。而如果该字段加上了final修饰,那么会在准备阶段赋值为1。
-public static final int value=1; //在准备阶段赋值为1 |
在加载至JVM之前,这个类无法知道其他类及其方法,字段所对应的具体位置。因此在引用这些成员时,需要生成符号引用。
-解析阶段的目的在于将这些符号引用解析成为实际引用。如果在该过程中出现符号引用指向了一个未被加载的类,字段或者方法,那么将触发这个类的加载(不一定触发链接以及初始化)。
-对于静态字段的直接赋值操作以及所有静态代码块的代码,JVM会将其置于同一个方法中,成为
接下来分析一下单例模式延迟初始化。当调用Singleton.getInstance()方法时,程序会访问LazyHolder.INSTANCE这一静态字段,触发对于LazyHolder的初始化,继而创建一个新的Singleton实例。由于类初始化是线程安全的,因此可以保证在任何情况下,有且只有一个Singleton实例。
-public class Singleton{ |
加载是指查找字节流,并且据此创建类的过程。加载的class文件可以来源于本地磁盘,也可以来自于网络或者运行时计算生成等等。在加载阶段需要完成以下三件事:
+在加载完成以后,外部的二进制流就会按照设定的格式存储在方法区之中。
+对于任意一个类,都必须由加载它的类加载器和这个类本身一起确立其在Java虚拟机中的唯一性。这会影响例如equals()方法,isAssignableForm()方法以及instanceof的判定。下面的代码就演示了不同的类加载器对instanceof的影响,其中Java虚拟机中同时存在了两个Main类,一个由虚拟机的应用程序加载类进行加载,另一个使用自定义的ClassLoader进行加载。
+public class Main { |
除了启动类加载器,其余的加载器都是java.lang.ClassLoader的子类。
+启动类加载器(bootstrap class loader)由C++实现,没有对应的Java对象。其他的类加载器都需要先由另一个类加载器加载至java虚拟机中,才能执行类加载的工作。
+启动类加载器负责加载最为基础重要的类(比如JAVA_HOME/jre/lib/rt.jar、resource.jar或sun.boot.class.path路径下的内容)。启动类加载器只会加载包名为java,javax和sun开头的类。
+扩展类加载器(JAVA9以后称为平台类加载器)的父类加载器是启动类加载器,在sun.misc.Launcher$ExtClassLoader中以Java实现。它负责加载相对次要、但又通用的类,(从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库)。
+应用类加载器的父类加载器则是扩展类加载器,在sun.misc.Launcher$AppClassLoader中以Java实现。。它负责加载应用程序路径下的类。(负责加载环境变量classpath或系统属性java.class.path指定路径下的类库)。如果应用程序中没有定义过自己的加载器,那么会是程序中默认的加载器。
+除了Java提供的类加载器之外,还可以加入自定义的类加载器,来实现特殊的加载方式。例如可以对class文件进行加密,加载时再利用自定义的类加载器进行解密。
+当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。这一规则被称为双亲委派机制。双亲委派模型要求除了启动类加载器,都必须要有自己的父类加载器。双亲委派机制可以避免类的重复加载,同时可以防止核心API库被篡改。双亲委派模型的代码在loadClass之中。
+protected Class<?> loadClass(String name, boolean resolve) |
从上面的代码也可以看出破环双亲委派模式的方法,就是直接在子加载器中覆盖loadClass()方法。在编写自己的类加载逻辑时,应该尽量去重写findClass方法来完成加载,来防止双亲委派模型被破坏。
+链接,是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段。
+在验证阶段,需要确保被加载的类能够满足java虚拟机的约束条件。
+准备阶段的目的是为了给被加载类的静态字段分配内存,将其初始化为默认值。例如以下一段Java代码
+public static int value=1; //在准备阶段赋值为0 |
该代码在准备阶段只会将value的值设置为0,而将value设置为1则会在之后的初始化阶段进行。而如果该字段加上了final修饰,那么会在准备阶段赋值为1。
+public static final int value=1; //在准备阶段赋值为1 |
在加载至JVM之前,这个类无法知道其他类及其方法,字段所对应的具体位置。因此在引用这些成员时,需要生成符号引用。
+解析阶段的目的在于将这些符号引用解析成为实际引用。如果在该过程中出现符号引用指向了一个未被加载的类,字段或者方法,那么将触发这个类的加载(不一定触发链接以及初始化)。
+对于静态字段的直接赋值操作以及所有静态代码块的代码,JVM会将其置于同一个方法中,成为
接下来分析一下单例模式延迟初始化。当调用Singleton.getInstance()方法时,程序会访问LazyHolder.INSTANCE这一静态字段,触发对于LazyHolder的初始化,继而创建一个新的Singleton实例。由于类初始化是线程安全的,因此可以保证在任何情况下,有且只有一个Singleton实例。
+public class Singleton{ |
每个进行连接的客户端,都会创建一个redisClient结构,保存了客户端当前的信息。
-typedef struct redisClient { |
Redis服务器的clients属性是一个链表,保存了所有客户端的状态。
-struct redisServer{ |
fd属性记录了客户端正在使用的套接字描述符。根据客户端的不同类型,fd的值可以为-1或者大于-1的整数。
-如果fd的值为-1,代表该客户端为伪客户端。伪客户端的请求来源于AOF文件或者LUA脚本而不是来自于网络。这种客户端不需要使用套接字进行连接。普通客户端的fd属性的值为大于-1的整数,记录客户端套接字的描述符。
-客户端的名称记录在name属性中。在默认情况下,客户端是没有名字的。通过CLIENT SETNAME命令可以设置名字。
-127.0.0.1:6379> CLIENT LIST |
如果客户端没有设置名字,那么name属性将会指向NULL,否则指向一个字符串对象,保存客户端的名字。
-客户端的flag属性记录了客户端的角色,其值可以为单个标志或者多个标志的二进制值。
-客户端的输入输出缓冲区用于保存客户端发送的命令请求,用querybuf属性进行保存。
-例如当客户端发送了SET key value命令,那么querybuf的内容将为以下值
-*3 |
querybuf的大小最大为1GB,如果超过,Redis会关闭该连接。
-在保存到缓冲区之后,会对请求的内容进行分析,并将得出的命令参数和个数保存到argv属性和argc属性中去。 例如对于刚才的命令,argc的值会设置为3,argv会设置为[set,key,value]数组。
-当分析出argc和argv之后,服务器会根据argv[0]的值,在命令表中查找对应的命令实现函数,然后将cmd属性指向该命令结构。然后使用cmd属性所指向的redisCommand结构并设置命令参数信息,调用命令实现函数,执行对应的命令。
-执行命令完成后,命令回复会被保存到输出缓冲区之中。每个客户端都有一个可变大小的缓冲区和固定大小的缓冲区。
-其中固定大小的缓冲区有buf和bufpos两个属性组成。buf属性是一个字节数组,bufpos记录了buf数组目前已使用的字节数量。其作用是保存那些长度比较少的回复。
-可变大小的缓冲区有reply属性保存。reply是一个链表连接多个字符串对象,用于保存那些长度比较大的回复,例如非常长的字符串,很多项组成的列表,很多元素的集合。
-authenticated属性记录了客户端是否通过了身份验证。如果其值为0代表未通过身份验证。
-客户端使用connect函数创建连接时,服务器会调用连接事件处理器,为客户端创建相应的客户端状态,并将该客户端状态添加到服务器状态结构clients链表的末尾。
-当出现以下情况时会出现客户端关闭:
-服务器在初始化时会创建执行Lua脚本的Redis命令的伪客户端,并将该伪客户端关联在服务器状态的lua_client属性值中。在服务器关闭时,该客户端才会被关闭。
-服务器在载入AOF文件时,会创建执行AOF文件的伪客户端,在执行完成后,立即关闭该客户端。
-Redis服务器的命令请求来自与Redis客户端。客户端会将命令请求转化为协议格式,通过连接到服务器的套接字,将协议的命令请求发送给服务器。
-当套接字变得可读时,会调用命令处理器来执行以下操作
-读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区中。
-对输入缓冲区的命令请求进行分析,提取命令参数。
-调用命令执行器执行指定的命令。
-先根据客户端的argv[0]参数,在命令表中查找参数指定的命令,并保存到cmd属性值中。cmd的属性是redisCommand,其结构如下:
-struct redisCommand { |
在查找到命令以后,执行预备操作,例如检查cmd指针是否为NULL,参数个数是否正确等等。当完成预备操作后,服务器才开始真正的执行命令。redisCommandProc是一个函数指针的别名,参数为redisClient。
-typedef void redisCommandProc(redisClient *c); |
在服务器真正实行命令时,只需要调用以下语句即可。
-client->cmd->proc(client); |
被调用的命令实现函数会执行指定的操作,产生相应的命令回复,保存到输出缓冲区之中。
-在执行完成以后,服务器还需要执行一些后续操作,例如打印慢日志,AOF日志输出。完成后续操作以后,就可以处理下一个命令请求了。
-Redis中的serverCron函数会每隔一段执行一次,负责管理服务器的资源,保证服务器运转良好。
-在serverCron会进行例如更新服务器时间缓存,执行持久化操作,关闭客户端等等。
-]]>每个进行连接的客户端,都会创建一个redisClient结构,保存了客户端当前的信息。
+typedef struct redisClient { |
Redis服务器的clients属性是一个链表,保存了所有客户端的状态。
+struct redisServer{ |
fd属性记录了客户端正在使用的套接字描述符。根据客户端的不同类型,fd的值可以为-1或者大于-1的整数。
+如果fd的值为-1,代表该客户端为伪客户端。伪客户端的请求来源于AOF文件或者LUA脚本而不是来自于网络。这种客户端不需要使用套接字进行连接。普通客户端的fd属性的值为大于-1的整数,记录客户端套接字的描述符。
+客户端的名称记录在name属性中。在默认情况下,客户端是没有名字的。通过CLIENT SETNAME命令可以设置名字。
+127.0.0.1:6379> CLIENT LIST |
如果客户端没有设置名字,那么name属性将会指向NULL,否则指向一个字符串对象,保存客户端的名字。
+客户端的flag属性记录了客户端的角色,其值可以为单个标志或者多个标志的二进制值。
+客户端的输入输出缓冲区用于保存客户端发送的命令请求,用querybuf属性进行保存。
+例如当客户端发送了SET key value命令,那么querybuf的内容将为以下值
+*3 |
querybuf的大小最大为1GB,如果超过,Redis会关闭该连接。
+在保存到缓冲区之后,会对请求的内容进行分析,并将得出的命令参数和个数保存到argv属性和argc属性中去。 例如对于刚才的命令,argc的值会设置为3,argv会设置为[set,key,value]数组。
+当分析出argc和argv之后,服务器会根据argv[0]的值,在命令表中查找对应的命令实现函数,然后将cmd属性指向该命令结构。然后使用cmd属性所指向的redisCommand结构并设置命令参数信息,调用命令实现函数,执行对应的命令。
+执行命令完成后,命令回复会被保存到输出缓冲区之中。每个客户端都有一个可变大小的缓冲区和固定大小的缓冲区。
+其中固定大小的缓冲区有buf和bufpos两个属性组成。buf属性是一个字节数组,bufpos记录了buf数组目前已使用的字节数量。其作用是保存那些长度比较少的回复。
+可变大小的缓冲区有reply属性保存。reply是一个链表连接多个字符串对象,用于保存那些长度比较大的回复,例如非常长的字符串,很多项组成的列表,很多元素的集合。
+authenticated属性记录了客户端是否通过了身份验证。如果其值为0代表未通过身份验证。
+客户端使用connect函数创建连接时,服务器会调用连接事件处理器,为客户端创建相应的客户端状态,并将该客户端状态添加到服务器状态结构clients链表的末尾。
+当出现以下情况时会出现客户端关闭:
+服务器在初始化时会创建执行Lua脚本的Redis命令的伪客户端,并将该伪客户端关联在服务器状态的lua_client属性值中。在服务器关闭时,该客户端才会被关闭。
+服务器在载入AOF文件时,会创建执行AOF文件的伪客户端,在执行完成后,立即关闭该客户端。
+Redis服务器的命令请求来自与Redis客户端。客户端会将命令请求转化为协议格式,通过连接到服务器的套接字,将协议的命令请求发送给服务器。
+当套接字变得可读时,会调用命令处理器来执行以下操作
+读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区中。
+对输入缓冲区的命令请求进行分析,提取命令参数。
+调用命令执行器执行指定的命令。
+先根据客户端的argv[0]参数,在命令表中查找参数指定的命令,并保存到cmd属性值中。cmd的属性是redisCommand,其结构如下:
+struct redisCommand { |
在查找到命令以后,执行预备操作,例如检查cmd指针是否为NULL,参数个数是否正确等等。当完成预备操作后,服务器才开始真正的执行命令。redisCommandProc是一个函数指针的别名,参数为redisClient。
+typedef void redisCommandProc(redisClient *c); |
在服务器真正实行命令时,只需要调用以下语句即可。
+client->cmd->proc(client); |
被调用的命令实现函数会执行指定的操作,产生相应的命令回复,保存到输出缓冲区之中。
+在执行完成以后,服务器还需要执行一些后续操作,例如打印慢日志,AOF日志输出。完成后续操作以后,就可以处理下一个命令请求了。
+Redis中的serverCron函数会每隔一段执行一次,负责管理服务器的资源,保证服务器运转良好。
+在serverCron会进行例如更新服务器时间缓存,执行持久化操作,关闭客户端等等。
]]>redisDb结构的expires字典保存了数据库中所有键的过期时间。
-typedef struct redisDb { |
过期字典的键是一个指针指向键空间中的某一个键对象,值是一个long long类型的整数,保存了毫秒精度的UNIX时间戳。当执行设置过期时间的命令时,服务器会在数据库的过期字典中关联给定的数据库键以及过期时间。
-PERSIST命令可以移除一个键的过期时间。当需要移除过期时间时,会从expires字典之中移除给定键的键值对关联。
-TTL和PTTL命令可以返回秒或毫秒为单位的剩余生存时间。其都是通过计算两个键的过期时间与当前时间之差来实现的。
-通过在设置键的同时,创建一个定时器。在定时器结束时,立即删除该键。该策略对内存友好,可以保证过期键最快的被删除。但是在比较多过期键的情况下,会很占用CPU资源,导致服务器的响应时间降低。
-惰性删除指的是当从键空间中获取值时,检查该键是否过期,如果过期的话,则删除该键,否则返回该键。惰性删除策略不会在删除其他无关键上花费CPU时间,属于CPU友好型策略。但是如果一个键已经过期但是一致没有被访问到,那么该键永远不会过期,所占用的内存永远不会释放。
-每隔一段时间,对数据库进行检查,删除其中的过期键。其难点在于确定执行的时长与频率。如果执行的太频繁,会导致占用过多CPU,如果执行的太少,会导致内存浪费的情况。
-Redis中整合了惰性删除和定期删除两种策略,取得了合理使用CPU时间和内存空间之间的平衡。
-所有读写redis命令在执行之前都会调用惰性删除的代码进行判断,惰性删除的代码如下:
-/* |
如果键已经过期,那么该函数会将输入的键从数据库之中删除,否则不做任何动作。当处理后的键仍然存在时,命令会按照存在的情况继续执行,否则按照不存在的情况执行。
-每当Redis的周期操作函数serverCron函数执行时,会调用activeExpireCycle函数进行过期键的清理。其会在规定的时间内,分次遍历数据库中的各个数据库,从expires字典中随机检查一部分过期时间,并删除其中的过期键。在扫描的时候,会提供一个全局变量记录上一次扫描的进度,并在下一次调用时,接着上一次的进度进行新一轮的检查工作。
-当一个键已经过期时并被删除时,Redis会像AOF日志之中添加一条DEL命令,来记录该键已经被删除。在进行AOF重写的时候,程序会对数据库中的键进行检查,已过期的键不会保存到重写以后的AOF日志之中。
-在生成RDB文件的时候,如果一个键已经过期,那么不会被保存到RDB文件之中。在载入RDB文件的时候,如果以主服务器的方式运行,那么过期的键不会被载入到数据库之中。如果以从服务器的方式运行,那么无论键是否过期都会被载入,因为在于主服务器同步的时候,从服务器的数据就会清空,所以对从服务器也没有影响。
-当处于服务器处于复制模式下时,服务器的过期删除键由主服务器控制。在主服务器删除一个过期键之后,会向从服务器发送一个DEL命令,告诉从服务器删除该键。从服务器即使键到达了过期时间也不会删除,知道收到DEL命令时才进行删除。通过主服务器统一的删除过期键可以保证主从一致性。
-]]>在一次全量复制中,对于主库由两个耗时的操作:生成 RDB 文件和传输 RDB 文件。
如果从库数量很多,会导致出现主库忙于生成RDB文件,同时占用大量的网络资源用于传输RDB文件。可以通过“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。
具体的方式为在部署主从集群的时候,可以选择通过手动选择一个从库用于级联其他从库。这些从库在进行同步时,不用再和主库进行交互了,只要和级联的从库进行写操作同步就行了,这就可以减轻主库上的压力。
+]]> +redisDb结构的expires字典保存了数据库中所有键的过期时间。
+typedef struct redisDb { |
过期字典的键是一个指针指向键空间中的某一个键对象,值是一个long long类型的整数,保存了毫秒精度的UNIX时间戳。当执行设置过期时间的命令时,服务器会在数据库的过期字典中关联给定的数据库键以及过期时间。
+PERSIST命令可以移除一个键的过期时间。当需要移除过期时间时,会从expires字典之中移除给定键的键值对关联。
+TTL和PTTL命令可以返回秒或毫秒为单位的剩余生存时间。其都是通过计算两个键的过期时间与当前时间之差来实现的。
+通过在设置键的同时,创建一个定时器。在定时器结束时,立即删除该键。该策略对内存友好,可以保证过期键最快的被删除。但是在比较多过期键的情况下,会很占用CPU资源,导致服务器的响应时间降低。
+惰性删除指的是当从键空间中获取值时,检查该键是否过期,如果过期的话,则删除该键,否则返回该键。惰性删除策略不会在删除其他无关键上花费CPU时间,属于CPU友好型策略。但是如果一个键已经过期但是一致没有被访问到,那么该键永远不会过期,所占用的内存永远不会释放。
+每隔一段时间,对数据库进行检查,删除其中的过期键。其难点在于确定执行的时长与频率。如果执行的太频繁,会导致占用过多CPU,如果执行的太少,会导致内存浪费的情况。
+Redis中整合了惰性删除和定期删除两种策略,取得了合理使用CPU时间和内存空间之间的平衡。
+所有读写redis命令在执行之前都会调用惰性删除的代码进行判断,惰性删除的代码如下:
+/* |
如果键已经过期,那么该函数会将输入的键从数据库之中删除,否则不做任何动作。当处理后的键仍然存在时,命令会按照存在的情况继续执行,否则按照不存在的情况执行。
+每当Redis的周期操作函数serverCron函数执行时,会调用activeExpireCycle函数进行过期键的清理。其会在规定的时间内,分次遍历数据库中的各个数据库,从expires字典中随机检查一部分过期时间,并删除其中的过期键。在扫描的时候,会提供一个全局变量记录上一次扫描的进度,并在下一次调用时,接着上一次的进度进行新一轮的检查工作。
+当一个键已经过期时并被删除时,Redis会像AOF日志之中添加一条DEL命令,来记录该键已经被删除。在进行AOF重写的时候,程序会对数据库中的键进行检查,已过期的键不会保存到重写以后的AOF日志之中。
+在生成RDB文件的时候,如果一个键已经过期,那么不会被保存到RDB文件之中。在载入RDB文件的时候,如果以主服务器的方式运行,那么过期的键不会被载入到数据库之中。如果以从服务器的方式运行,那么无论键是否过期都会被载入,因为在于主服务器同步的时候,从服务器的数据就会清空,所以对从服务器也没有影响。
+当处于服务器处于复制模式下时,服务器的过期删除键由主服务器控制。在主服务器删除一个过期键之后,会向从服务器发送一个DEL命令,告诉从服务器删除该键。从服务器即使键到达了过期时间也不会删除,知道收到DEL命令时才进行删除。通过主服务器统一的删除过期键可以保证主从一致性。
]]>ZooKeeper是Kafka用来负责集群元数据的管理以及控制器的选举。
-Producer将消息发送到Broker中去,Broker负责将收到的消息存储到磁盘当中,然后Consumer负责从Broker订阅并且消费消息。
-Kafka还有两个重要的概念就是主题Topic以及分区Partition。Kafka中的消息以主题为单位进行归类,生产者负责将消息发送到特定的主题,消费者负责订阅主题并进行消费。
-主题是一个逻辑上的概念,还可以细分为多个分区,一个分区只属于单个主题。同一主题下不同分区包含的消息是不同的。分区在存储层面可以看作一个可以追加的日志,消息在追加到分区日志文件的时候会分配一个特定的偏移量offset。offset是消息在分区中的唯一标识,Kafka通过offset来保证消息在分区中的顺序性。但是offset不跨越分区,因此Kafka只保证分区有序而不保证主题有序。
-Kafka的分区可以分布在不同的服务器上,横跨多个Broker,以此来提供比单个Broker更强大的性能。当消息发送到Broker之前,会根据分区规则选择存储到哪个分区。
-Kafka为分区引入了多副本(Replica)机制,通过增加副本数量可以提升容灾能力。同一分区的不同副本中保存的是相同的消息,但在同一时刻副本之间的消息可能并非完全相同,副本之间是一主多从的关系,其中leader副本负责处理读写请求,follower副本只负责与leader副本消息同步。副本处于不同的broker中,当leader副本出现故障时,从follower副本中重新选举新的leader副本继续对外提供服务。通过该机制使得Kafka集群中某个Broker失效时仍然能够对外提供服务。
-分区中所有的副本称为AR(Assigned Replicas),所有与leader副本保持一定程度同步的副本组成ISR(In-Sync-Replicas)。当消费者发送消息时,会先发送到leader节点,然后follower节点才能从leader副本中拉取消息进行同步,同步期间follower副本相对于leader副本会有一定程度上的落后。与leader副本同步滞后过多的副本称为OSR(Out-of-Sync Replicas)。当leader副本发生故障时,只有ISR集合中的副本才有资格选举为新的leader。
-Kafka的消费端也有容灾能力,Consumer使用拉(Pull)模式从服务端拉取消息,并且保存消费的具体位置。当消费者宕机重启时可以根据之前保存的消费位置重新拉取需要的消息进行消费。
-]]>ZooKeeper是Kafka用来负责集群元数据的管理以及控制器的选举。
+Producer将消息发送到Broker中去,Broker负责将收到的消息存储到磁盘当中,然后Consumer负责从Broker订阅并且消费消息。
+Kafka还有两个重要的概念就是主题Topic以及分区Partition。Kafka中的消息以主题为单位进行归类,生产者负责将消息发送到特定的主题,消费者负责订阅主题并进行消费。
+主题是一个逻辑上的概念,还可以细分为多个分区,一个分区只属于单个主题。同一主题下不同分区包含的消息是不同的。分区在存储层面可以看作一个可以追加的日志,消息在追加到分区日志文件的时候会分配一个特定的偏移量offset。offset是消息在分区中的唯一标识,Kafka通过offset来保证消息在分区中的顺序性。但是offset不跨越分区,因此Kafka只保证分区有序而不保证主题有序。
+Kafka的分区可以分布在不同的服务器上,横跨多个Broker,以此来提供比单个Broker更强大的性能。当消息发送到Broker之前,会根据分区规则选择存储到哪个分区。
+Kafka为分区引入了多副本(Replica)机制,通过增加副本数量可以提升容灾能力。同一分区的不同副本中保存的是相同的消息,但在同一时刻副本之间的消息可能并非完全相同,副本之间是一主多从的关系,其中leader副本负责处理读写请求,follower副本只负责与leader副本消息同步。副本处于不同的broker中,当leader副本出现故障时,从follower副本中重新选举新的leader副本继续对外提供服务。通过该机制使得Kafka集群中某个Broker失效时仍然能够对外提供服务。
+分区中所有的副本称为AR(Assigned Replicas),所有与leader副本保持一定程度同步的副本组成ISR(In-Sync-Replicas)。当消费者发送消息时,会先发送到leader节点,然后follower节点才能从leader副本中拉取消息进行同步,同步期间follower副本相对于leader副本会有一定程度上的落后。与leader副本同步滞后过多的副本称为OSR(Out-of-Sync Replicas)。当leader副本发生故障时,只有ISR集合中的副本才有资格选举为新的leader。
+Kafka的消费端也有容灾能力,Consumer使用拉(Pull)模式从服务端拉取消息,并且保存消费的具体位置。当消费者宕机重启时可以根据之前保存的消费位置重新拉取需要的消息进行消费。
+]]>代理模式在不改变原始类的情况下,通过添加代理类来给原始类添加附加功能。代理模式经常用在比如监控,统计,鉴权这样的非业务逻辑之中。同时RPC框架也可以看成一种代理模式,被称为远程代理,在使用RPC的时候,不需要考虑与服务器交互的细节,只关注业务逻辑即可。
-代理模式通过针对每个类都创建一个代理类,具体实现代码如下所示:
-//java 11 |
在上面代码之中,代理类AProxy和原始类B都实现了A接口,在使用的时候将B对象使用构造函数传入AProxy中,然后调用原始类来执行代码,同时在调用代码之前检查相关的权限信息。
-在刚才实现的代理模式中,需要将原始类中的所有方法都重新实现一遍。同时如果有多个类需要使用代理,那么就需要写多个代理类,会导致类的个数不必要的增多,可以使用动态代理来解决这一个问题。Java本身就已经提供了动态代理的语法,使用jdk动态代理实现的代码如下:
-//java 11 |
动态代理十分有用,可以帮助在编写代码时减少模板代码,减少维护和开发的成本,Spring AOP的底层实现原理就是基于这样的动态代理的。
-桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化,其思路是不要过度使用继承,而是优先拆分部分组件,通过组合的方式来进行扩展。桥接模式的目的是为了避免直接继承带来的子类爆炸。
-例如如果对于图形类而言,有是什么形状这一维度,也有颜色这一维度,那么就可以使用桥接模式来编写代码。具体代码如下:
-//kotlin 1.4.0 |
在实际使用中,如果出现一个类出现多个可以单个可以单独变化的维度,那么可以使用桥接模式进行设计。
-装饰器模式能够在运行器动态的给某个对象增加功能,同时将核心功能与附加功能分开。Java标准库的IO库就是装饰器模式的经典案例。
-例如我们需要给FileInputStream添加缓存功能以及GZIP解压缩功能,那么实现的代码如下:
-//java 11 |
这样的代码就是典型的装饰器模式的代码。通过查看源码可以看到GZIPInputStream,BufferedInputStream,FileInputStream都是InputStream的子类。在这个例子中,核心功能指的就是FileInputStream这个真正读取数据的源头,附加工作指的是缓冲和压缩这两个功能。如果我们仍然需要新增附加功能,就可以继承InputStream独立的进行扩展。
-在装饰器模式中,装饰器类和原始类都继承自同样的父类,装饰器模式有个特点就是可以对原始类嵌套使用多个装饰器。
-适配器模式用于将不兼容的接口转换为可兼容的接口,让原本不能一起工作的类一起工作。适配器模式可以通过继承以及组合的方式进行实现,通过继承来实现适配器模式的代码如下:
-//java 11 |
在这段代码之中,ITarget表示要转化成的接口的定义,Adaptee是一组不兼容的ITarget接口的类,Adaptor将Adaptee转化成为符合ITarget接口定义的类。
-在实际应用之中,适配器模式可以看作一种补救设计上缺陷的方法。例如Java中有很多日志框架例如log4j,logback,大部分的日志都提供了相似的功能但没有实现统一的接口。Slf4j提供了打印日志的统一接口规范,像log4j这样的日志框架需要将接口改为符合slf4j的接口规范,就使用到了适配器模式。
-门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。例如一个系统A,提供了a、b、c、d 四个接口。系统B需要调用a、b、d三个接口,利用门面模式,可以提供一个包含a、b、d三个接口调用的门面接口x,供B系统使用,这就是门面模式。
-门面模式不仅让子系统更加的易用,有时还可以解决性能上的问题。例如客户端APP之前可能需要使用三次调用服务器端的接口才能获取到想要的数据,而使用门面模式之后,就可以只发送一次请求,提升了响应速度。
-组合模式将对象组织成为树形的结构,表示部分与整体之间的层次结构。例如对于文件系统,可以将文件和目录进行区分,定义成为File和Directory两个类,在下面的代码中实现了打印当前目录下递归打印文件的功能,就使用了组合模式。
-//java 11 |
组合模式将一组对象抽象成树形结构,将单个对象和组合对象都看做树中的节点,以统一处理逻辑,并且它利用树形结构的特点,递归地处理每个子树,依次简化代码实现。使用场景必须要能够表示成为树形结构是其前提,因此其使用场景比较有局限性。
-享元模式指的是复用被共享的变量,其意图在于复用对象,节省内存。如果在一个系统中存在大量重复对象,且这些对象是不可变变量,就可以将其设计为享元,在内存中只保留一份实例,供多处代码引用。因此,享元模式就是通过工厂方法创建对象,在工厂方法内部,很可能返回缓存的实例,而不是新创建实例,从而实现不可变实例的复用。
-享元模式在Java标准库中有许多的的应用,例如Integer.valueOf这个静态工厂创建实例,当传入的int范围在-128到+127的范围之间时,会直接返回缓存的Integer实例:
-//java 11 |
代理模式在不改变原始类的情况下,通过添加代理类来给原始类添加附加功能。代理模式经常用在比如监控,统计,鉴权这样的非业务逻辑之中。同时RPC框架也可以看成一种代理模式,被称为远程代理,在使用RPC的时候,不需要考虑与服务器交互的细节,只关注业务逻辑即可。
+代理模式通过针对每个类都创建一个代理类,具体实现代码如下所示:
+//java 11 |
在上面代码之中,代理类AProxy和原始类B都实现了A接口,在使用的时候将B对象使用构造函数传入AProxy中,然后调用原始类来执行代码,同时在调用代码之前检查相关的权限信息。
+在刚才实现的代理模式中,需要将原始类中的所有方法都重新实现一遍。同时如果有多个类需要使用代理,那么就需要写多个代理类,会导致类的个数不必要的增多,可以使用动态代理来解决这一个问题。Java本身就已经提供了动态代理的语法,使用jdk动态代理实现的代码如下:
+//java 11 |
动态代理十分有用,可以帮助在编写代码时减少模板代码,减少维护和开发的成本,Spring AOP的底层实现原理就是基于这样的动态代理的。
+桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化,其思路是不要过度使用继承,而是优先拆分部分组件,通过组合的方式来进行扩展。桥接模式的目的是为了避免直接继承带来的子类爆炸。
+例如如果对于图形类而言,有是什么形状这一维度,也有颜色这一维度,那么就可以使用桥接模式来编写代码。具体代码如下:
+//kotlin 1.4.0 |
在实际使用中,如果出现一个类出现多个可以单个可以单独变化的维度,那么可以使用桥接模式进行设计。
+装饰器模式能够在运行器动态的给某个对象增加功能,同时将核心功能与附加功能分开。Java标准库的IO库就是装饰器模式的经典案例。
+例如我们需要给FileInputStream添加缓存功能以及GZIP解压缩功能,那么实现的代码如下:
+//java 11 |
这样的代码就是典型的装饰器模式的代码。通过查看源码可以看到GZIPInputStream,BufferedInputStream,FileInputStream都是InputStream的子类。在这个例子中,核心功能指的就是FileInputStream这个真正读取数据的源头,附加工作指的是缓冲和压缩这两个功能。如果我们仍然需要新增附加功能,就可以继承InputStream独立的进行扩展。
+在装饰器模式中,装饰器类和原始类都继承自同样的父类,装饰器模式有个特点就是可以对原始类嵌套使用多个装饰器。
+适配器模式用于将不兼容的接口转换为可兼容的接口,让原本不能一起工作的类一起工作。适配器模式可以通过继承以及组合的方式进行实现,通过继承来实现适配器模式的代码如下:
+//java 11 |
在这段代码之中,ITarget表示要转化成的接口的定义,Adaptee是一组不兼容的ITarget接口的类,Adaptor将Adaptee转化成为符合ITarget接口定义的类。
+在实际应用之中,适配器模式可以看作一种补救设计上缺陷的方法。例如Java中有很多日志框架例如log4j,logback,大部分的日志都提供了相似的功能但没有实现统一的接口。Slf4j提供了打印日志的统一接口规范,像log4j这样的日志框架需要将接口改为符合slf4j的接口规范,就使用到了适配器模式。
+门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。例如一个系统A,提供了a、b、c、d 四个接口。系统B需要调用a、b、d三个接口,利用门面模式,可以提供一个包含a、b、d三个接口调用的门面接口x,供B系统使用,这就是门面模式。
+门面模式不仅让子系统更加的易用,有时还可以解决性能上的问题。例如客户端APP之前可能需要使用三次调用服务器端的接口才能获取到想要的数据,而使用门面模式之后,就可以只发送一次请求,提升了响应速度。
+组合模式将对象组织成为树形的结构,表示部分与整体之间的层次结构。例如对于文件系统,可以将文件和目录进行区分,定义成为File和Directory两个类,在下面的代码中实现了打印当前目录下递归打印文件的功能,就使用了组合模式。
+//java 11 |
组合模式将一组对象抽象成树形结构,将单个对象和组合对象都看做树中的节点,以统一处理逻辑,并且它利用树形结构的特点,递归地处理每个子树,依次简化代码实现。使用场景必须要能够表示成为树形结构是其前提,因此其使用场景比较有局限性。
+享元模式指的是复用被共享的变量,其意图在于复用对象,节省内存。如果在一个系统中存在大量重复对象,且这些对象是不可变变量,就可以将其设计为享元,在内存中只保留一份实例,供多处代码引用。因此,享元模式就是通过工厂方法创建对象,在工厂方法内部,很可能返回缓存的实例,而不是新创建实例,从而实现不可变实例的复用。
+享元模式在Java标准库中有许多的的应用,例如Integer.valueOf这个静态工厂创建实例,当传入的int范围在-128到+127的范围之间时,会直接返回缓存的Integer实例:
+//java 11 |