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 @@

- +
diff --git "a/2020/09/05/Redis\344\270\255\347\232\204\346\214\201\344\271\205\345\214\226\346\234\272\345\210\266/index.html" "b/2020/09/05/Redis\344\270\255\347\232\204\346\214\201\344\271\205\345\214\226\346\234\272\345\210\266/index.html" index 3475751..0eb8ce4 100644 --- "a/2020/09/05/Redis\344\270\255\347\232\204\346\214\201\344\271\205\345\214\226\346\234\272\345\210\266/index.html" +++ "b/2020/09/05/Redis\344\270\255\347\232\204\346\214\201\344\271\205\345\214\226\346\234\272\345\210\266/index.html" @@ -32,8 +32,8 @@ - + @@ -356,8 +356,8 @@

- +
diff --git a/atom.xml b/atom.xml index b9538ab..7a12602 100644 --- a/atom.xml +++ b/atom.xml @@ -450,10 +450,10 @@ - - + + diff --git a/css/main.css b/css/main.css index c60a25f..cf73ad7 100644 --- a/css/main.css +++ b/css/main.css @@ -1240,7 +1240,7 @@ pre .javascript .function { } .links-of-author a::before, .links-of-author span.exturl::before { - background: #a383e5; + background: #13ffff; border-radius: 50%; content: ' '; display: inline-block; diff --git a/search.xml b/search.xml index c971517..68798fe 100644 --- a/search.xml +++ b/search.xml @@ -94,50 +94,6 @@ HTTP - - JVM如何进行类加载 - /2020/09/01/JVM%E5%A6%82%E4%BD%95%E5%8A%A0%E8%BD%BDJava%E7%B1%BB/ - Jvm在进行类加载时分为三个环节,分别为加载,链接以及初始化。

-

加载

加载是指查找字节流,并且据此创建类的过程。加载的class文件可以来源于本地磁盘,也可以来自于网络或者运行时计算生成等等。在加载阶段需要完成以下三件事:

-
    -
  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. -
  3. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  4. -
  5. 在内存中生成代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  6. -
-

在加载完成以后,外部的二进制流就会按照设定的格式存储在方法区之中。

-

加载器

对于任意一个类,都必须由加载它的类加载器和这个类本身一起确立其在Java虚拟机中的唯一性。这会影响例如equals()方法,isAssignableForm()方法以及instanceof的判定。下面的代码就演示了不同的类加载器对instanceof的影响,其中Java虚拟机中同时存在了两个Main类,一个由虚拟机的应用程序加载类进行加载,另一个使用自定义的ClassLoader进行加载。

-
public class Main {
public static void main(String[] args) throws Exception {
ClassLoader loader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
InputStream in = getClass().getResourceAsStream(name+".class");
if (in == null) {
return super.loadClass(name);
}
byte[] b = in.readAllBytes();
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = loader.loadClass("Main").newInstance();
System.out.println(obj.getClass()); //class Main
System.out.println(obj instanceof Main); //false
}
}
- -

除了启动类加载器,其余的加载器都是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)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 判断是否已经被加载了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
// 从父加载器加载
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 父类是启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载失败
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
- -

从上面的代码也可以看出破环双亲委派模式的方法,就是直接在子加载器中覆盖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会将其置于同一个方法中,成为,然后在初始化阶段运行。在初始化阶段,JVM会通过加锁的方式来保证方法只会执行一次。只有当初始化完成以后,类才正式的成为了可执行的状态。

-

接下来分析一下单例模式延迟初始化。当调用Singleton.getInstance()方法时,程序会访问LazyHolder.INSTANCE这一静态字段,触发对于LazyHolder的初始化,继而创建一个新的Singleton实例。由于类初始化是线程安全的,因此可以保证在任何情况下,有且只有一个Singleton实例。

-
public class Singleton{
private Singleton(){}
private static class LazyHolder{
static final Singleton INSTANCE=new Singleton();
}
public static Singleton getInstance(){
return LazyHolder.INSTANCE;
}
public static void main(){

}
}
]]>
- - Java - - - Java - JVM - -
JVM中的垃圾回收算法 /2020/10/03/JVM%E4%B8%AD%E7%9A%84%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95/ @@ -209,6 +165,50 @@ JVM + + JVM如何进行类加载 + /2020/09/01/JVM%E5%A6%82%E4%BD%95%E5%8A%A0%E8%BD%BDJava%E7%B1%BB/ + Jvm在进行类加载时分为三个环节,分别为加载,链接以及初始化。

+

加载

加载是指查找字节流,并且据此创建类的过程。加载的class文件可以来源于本地磁盘,也可以来自于网络或者运行时计算生成等等。在加载阶段需要完成以下三件事:

+
    +
  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. +
  3. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  4. +
  5. 在内存中生成代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  6. +
+

在加载完成以后,外部的二进制流就会按照设定的格式存储在方法区之中。

+

加载器

对于任意一个类,都必须由加载它的类加载器和这个类本身一起确立其在Java虚拟机中的唯一性。这会影响例如equals()方法,isAssignableForm()方法以及instanceof的判定。下面的代码就演示了不同的类加载器对instanceof的影响,其中Java虚拟机中同时存在了两个Main类,一个由虚拟机的应用程序加载类进行加载,另一个使用自定义的ClassLoader进行加载。

+
public class Main {
public static void main(String[] args) throws Exception {
ClassLoader loader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
InputStream in = getClass().getResourceAsStream(name+".class");
if (in == null) {
return super.loadClass(name);
}
byte[] b = in.readAllBytes();
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = loader.loadClass("Main").newInstance();
System.out.println(obj.getClass()); //class Main
System.out.println(obj instanceof Main); //false
}
}
+ +

除了启动类加载器,其余的加载器都是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)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 判断是否已经被加载了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
// 从父加载器加载
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 父类是启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载失败
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
+ +

从上面的代码也可以看出破环双亲委派模式的方法,就是直接在子加载器中覆盖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会将其置于同一个方法中,成为,然后在初始化阶段运行。在初始化阶段,JVM会通过加锁的方式来保证方法只会执行一次。只有当初始化完成以后,类才正式的成为了可执行的状态。

+

接下来分析一下单例模式延迟初始化。当调用Singleton.getInstance()方法时,程序会访问LazyHolder.INSTANCE这一静态字段,触发对于LazyHolder的初始化,继而创建一个新的Singleton实例。由于类初始化是线程安全的,因此可以保证在任何情况下,有且只有一个Singleton实例。

+
public class Singleton{
private Singleton(){}
private static class LazyHolder{
static final Singleton INSTANCE=new Singleton();
}
public static Singleton getInstance(){
return LazyHolder.INSTANCE;
}
public static void main(){

}
}
]]>
+ + Java + + + JVM + Java + +
Kubernetes调度器 /2021/02/01/Kubernetes%E8%B0%83%E5%BA%A6%E5%99%A8/ @@ -277,77 +277,6 @@ 日志 - - Redis中的客户端与服务器实现 - /2020/09/10/Redis%E4%B8%AD%E7%9A%84%E5%AE%A2%E6%88%B7%E7%AB%AF%E4%B8%8E%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%AE%9E%E7%8E%B0/ - Redis服务器可以与多个客户端建立网络连接,每个客户端也可以向服务器发送命令请求。Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。

-

客户端

客户端结构

每个进行连接的客户端,都会创建一个redisClient结构,保存了客户端当前的信息。

-
typedef struct redisClient {
// 套接字描述符
// -1代表伪客户端,来自于AOF文件或者LUA脚本
// 普通客户端为大于-1的整数,记录套接字描述符
int fd;
// 当前正在使用的数据库
redisDb *db;
// 当前正在使用的数据库的 id (号码)
int dictid;
// 客户端的名字
robj *name; /* As set by CLIENT SETNAME */
// 查询缓冲区
sds querybuf;
// 查询缓冲区长度峰值
size_t querybuf_peak; /* Recent (100ms or more) peak of querybuf size */
// 参数数量
int argc;
// 参数对象数组
robj **argv;
// 记录被客户端执行的命令
struct redisCommand *cmd, *lastcmd;
// 请求的类型:内联命令还是多条命令
int reqtype;
// 剩余未读取的命令内容数量
int multibulklen; /* number of multi bulk arguments left to read */
// 命令内容的长度
long bulklen; /* length of bulk argument in multi bulk request */
// 回复链表
list *reply;
// 回复链表中对象的总大小
unsigned long reply_bytes; /* Tot bytes of objects in reply list */
// 已发送字节,处理 short write 用
int sentlen; /* Amount of bytes already sent in the current
buffer or object being sent. */
// 创建客户端的时间
time_t ctime; /* Client creation time */
// 客户端最后一次和服务器互动的时间
time_t lastinteraction; /* time of the last interaction, used for timeout */
// 客户端的输出缓冲区超过软性限制的时间
time_t obuf_soft_limit_reached_time;
// 客户端状态标志
// 记录客户端的角色以及客户端所处状态
//
int flags; /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */
// 当 server.requirepass 不为 NULL 时
// 代表认证的状态
// 0 代表未认证, 1 代表已认证
int authenticated; /* when requirepass is non-NULL */
// 复制状态
int replstate; /* replication state if this is a slave */
// 用于保存主服务器传来的 RDB 文件的文件描述符
int repldbfd; /* replication DB file descriptor */
// 读取主服务器传来的 RDB 文件的偏移量
off_t repldboff; /* replication DB file offset */
// 主服务器传来的 RDB 文件的大小
off_t repldbsize; /* replication DB file size */
sds replpreamble; /* replication DB preamble. */
// 主服务器的复制偏移量
long long reploff; /* replication offset if this is our master */
// 从服务器最后一次发送 REPLCONF ACK 时的偏移量
long long repl_ack_off; /* replication ack offset, if this is a slave */
// 从服务器最后一次发送 REPLCONF ACK 的时间
long long repl_ack_time;/* replication ack time, if this is a slave */
// 主服务器的 master run ID
// 保存在客户端,用于执行部分重同步
char replrunid[REDIS_RUN_ID_SIZE+1]; /* master run id if this is a master */
// 从服务器的监听端口号
int slave_listening_port; /* As configured with: SLAVECONF listening-port */
// 事务状态
multiState mstate; /* MULTI/EXEC state */
// 阻塞类型
int btype; /* Type of blocking op if REDIS_BLOCKED. */
// 阻塞状态
blockingState bpop; /* blocking state */
// 最后被写入的全局复制偏移量
long long woff; /* Last write global replication offset. */
// 被监视的键
list *watched_keys; /* Keys WATCHED for MULTI/EXEC CAS */
// 这个字典记录了客户端所有订阅的频道
// 键为频道名字,值为 NULL
// 也即是,一个频道的集合
dict *pubsub_channels; /* channels a client is interested in (SUBSCRIBE) */
// 链表,包含多个 pubsubPattern 结构
// 记录了所有订阅频道的客户端的信息
// 新 pubsubPattern 结构总是被添加到表尾
list *pubsub_patterns; /* patterns a client is interested in (SUBSCRIBE) */
sds peerid; /* Cached peer ID. */
// 固定大小的缓冲区
// 回复偏移量
int bufpos;
// 回复缓冲区
char buf[REDIS_REPLY_CHUNK_BYTES];
} redisClient;
- -

Redis服务器的clients属性是一个链表,保存了所有客户端的状态。

-
struct redisServer{
//...
//一个保存所有客户端状态的链表
list* clients;
//...
};
- -

套接字描述符

fd属性记录了客户端正在使用的套接字描述符。根据客户端的不同类型,fd的值可以为-1或者大于-1的整数。

-

如果fd的值为-1,代表该客户端为伪客户端。伪客户端的请求来源于AOF文件或者LUA脚本而不是来自于网络。这种客户端不需要使用套接字进行连接。普通客户端的fd属性的值为大于-1的整数,记录客户端套接字的描述符。

-

名字

客户端的名称记录在name属性中。在默认情况下,客户端是没有名字的。通过CLIENT SETNAME命令可以设置名字。

-
127.0.0.1:6379> CLIENT LIST
id=5 addr=127.0.0.1:42718 fd=8 name= ...
127.0.0.1:6379> CLIENT SETNAME test-name
OK
127.0.0.1:6379> CLIENT LIST
id=5 addr=127.0.0.1:42718 fd=8 name=test-name ...
- -

如果客户端没有设置名字,那么name属性将会指向NULL,否则指向一个字符串对象,保存客户端的名字。

-

标志

客户端的flag属性记录了客户端的角色,其值可以为单个标志或者多个标志的二进制值。

-

命令请求

客户端的输入输出缓冲区用于保存客户端发送的命令请求,用querybuf属性进行保存。

-

例如当客户端发送了SET key value命令,那么querybuf的内容将为以下值

-
*3
$3
set
$3
key
$3
value
- -

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链表的末尾。

-

关闭普通客户端

当出现以下情况时会出现客户端关闭:

-
    -
  • 客户端与服务器的网络连接被关闭。
  • -
  • 发送了带有不符合协议格式的命令请求。
  • -
  • 客户端成为CLIENT KILL命令的目标
  • -
  • 发送的命令请求大小超过了输入缓冲区的大小
  • -
  • 命令回复超过了输出缓冲的大小
  • -
  • 用户为服务器设置了timeout配置选项。当空转时间超过该选项值时会被关闭。
  • -
-

伪客户端

服务器在初始化时会创建执行Lua脚本的Redis命令的伪客户端,并将该伪客户端关联在服务器状态的lua_client属性值中。在服务器关闭时,该客户端才会被关闭。

-

服务器在载入AOF文件时,会创建执行AOF文件的伪客户端,在执行完成后,立即关闭该客户端。

-

服务器

命令请求过程

发送命令请求

Redis服务器的命令请求来自与Redis客户端。客户端会将命令请求转化为协议格式,通过连接到服务器的套接字,将协议的命令请求发送给服务器。

-

读取命令请求

当套接字变得可读时,会调用命令处理器来执行以下操作

-
    -
  • 读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区中。

    -
  • -
  • 对输入缓冲区的命令请求进行分析,提取命令参数。

    -
  • -
  • 调用命令执行器执行指定的命令。

    -
  • -
-

命令执行器

先根据客户端的argv[0]参数,在命令表中查找参数指定的命令,并保存到cmd属性值中。cmd的属性是redisCommand,其结构如下:

-
struct redisCommand {
// 命令名字
char *name;
// 实现函数
redisCommandProc *proc;
// 参数个数
int arity;
// 字符串表示的 FLAG
char *sflags;
// 实际 FLAG
int flags;
// 从命令中判断命令的键参数。在 Redis 集群转向时使用。
redisGetKeysProc *getkeys_proc;
// 指定哪些参数是 key
int firstkey; /* The first argument that's a key (0 = no keys) */
int lastkey; /* The last argument that's a key */
int keystep; /* The step between first and last key */
// microseconds 记录了命令执行耗费的总毫微秒数
// calls 是命令被执行的总次数
long long microseconds, calls;
};
- -

在查找到命令以后,执行预备操作,例如检查cmd指针是否为NULL,参数个数是否正确等等。当完成预备操作后,服务器才开始真正的执行命令。redisCommandProc是一个函数指针的别名,参数为redisClient。

-
typedef void redisCommandProc(redisClient *c);
- -

在服务器真正实行命令时,只需要调用以下语句即可。

-
client->cmd->proc(client);
- -

被调用的命令实现函数会执行指定的操作,产生相应的命令回复,保存到输出缓冲区之中。

-

在执行完成以后,服务器还需要执行一些后续操作,例如打印慢日志,AOF日志输出。完成后续操作以后,就可以处理下一个命令请求了。

-

serverCron函数

Redis中的serverCron函数会每隔一段执行一次,负责管理服务器的资源,保证服务器运转良好。

-

在serverCron会进行例如更新服务器时间缓存,执行持久化操作,关闭客户端等等。

-]]>
- - Redis - - - Redis - -
Redis中的哨兵机制 /2020/09/27/Redis%E4%B8%AD%E7%9A%84%E5%93%A8%E5%85%B5%E6%9C%BA%E5%88%B6/ @@ -533,6 +462,77 @@
  • 有序集合保存的元素个数小于128个
  • 有序集合保存的所有元素个数都小于64字节
  • +]]> + + Redis + + + Redis + +
    + + Redis中的客户端与服务器实现 + /2020/09/10/Redis%E4%B8%AD%E7%9A%84%E5%AE%A2%E6%88%B7%E7%AB%AF%E4%B8%8E%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%AE%9E%E7%8E%B0/ + Redis服务器可以与多个客户端建立网络连接,每个客户端也可以向服务器发送命令请求。Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。

    +

    客户端

    客户端结构

    每个进行连接的客户端,都会创建一个redisClient结构,保存了客户端当前的信息。

    +
    typedef struct redisClient {
    // 套接字描述符
    // -1代表伪客户端,来自于AOF文件或者LUA脚本
    // 普通客户端为大于-1的整数,记录套接字描述符
    int fd;
    // 当前正在使用的数据库
    redisDb *db;
    // 当前正在使用的数据库的 id (号码)
    int dictid;
    // 客户端的名字
    robj *name; /* As set by CLIENT SETNAME */
    // 查询缓冲区
    sds querybuf;
    // 查询缓冲区长度峰值
    size_t querybuf_peak; /* Recent (100ms or more) peak of querybuf size */
    // 参数数量
    int argc;
    // 参数对象数组
    robj **argv;
    // 记录被客户端执行的命令
    struct redisCommand *cmd, *lastcmd;
    // 请求的类型:内联命令还是多条命令
    int reqtype;
    // 剩余未读取的命令内容数量
    int multibulklen; /* number of multi bulk arguments left to read */
    // 命令内容的长度
    long bulklen; /* length of bulk argument in multi bulk request */
    // 回复链表
    list *reply;
    // 回复链表中对象的总大小
    unsigned long reply_bytes; /* Tot bytes of objects in reply list */
    // 已发送字节,处理 short write 用
    int sentlen; /* Amount of bytes already sent in the current
    buffer or object being sent. */
    // 创建客户端的时间
    time_t ctime; /* Client creation time */
    // 客户端最后一次和服务器互动的时间
    time_t lastinteraction; /* time of the last interaction, used for timeout */
    // 客户端的输出缓冲区超过软性限制的时间
    time_t obuf_soft_limit_reached_time;
    // 客户端状态标志
    // 记录客户端的角色以及客户端所处状态
    //
    int flags; /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */
    // 当 server.requirepass 不为 NULL 时
    // 代表认证的状态
    // 0 代表未认证, 1 代表已认证
    int authenticated; /* when requirepass is non-NULL */
    // 复制状态
    int replstate; /* replication state if this is a slave */
    // 用于保存主服务器传来的 RDB 文件的文件描述符
    int repldbfd; /* replication DB file descriptor */
    // 读取主服务器传来的 RDB 文件的偏移量
    off_t repldboff; /* replication DB file offset */
    // 主服务器传来的 RDB 文件的大小
    off_t repldbsize; /* replication DB file size */
    sds replpreamble; /* replication DB preamble. */
    // 主服务器的复制偏移量
    long long reploff; /* replication offset if this is our master */
    // 从服务器最后一次发送 REPLCONF ACK 时的偏移量
    long long repl_ack_off; /* replication ack offset, if this is a slave */
    // 从服务器最后一次发送 REPLCONF ACK 的时间
    long long repl_ack_time;/* replication ack time, if this is a slave */
    // 主服务器的 master run ID
    // 保存在客户端,用于执行部分重同步
    char replrunid[REDIS_RUN_ID_SIZE+1]; /* master run id if this is a master */
    // 从服务器的监听端口号
    int slave_listening_port; /* As configured with: SLAVECONF listening-port */
    // 事务状态
    multiState mstate; /* MULTI/EXEC state */
    // 阻塞类型
    int btype; /* Type of blocking op if REDIS_BLOCKED. */
    // 阻塞状态
    blockingState bpop; /* blocking state */
    // 最后被写入的全局复制偏移量
    long long woff; /* Last write global replication offset. */
    // 被监视的键
    list *watched_keys; /* Keys WATCHED for MULTI/EXEC CAS */
    // 这个字典记录了客户端所有订阅的频道
    // 键为频道名字,值为 NULL
    // 也即是,一个频道的集合
    dict *pubsub_channels; /* channels a client is interested in (SUBSCRIBE) */
    // 链表,包含多个 pubsubPattern 结构
    // 记录了所有订阅频道的客户端的信息
    // 新 pubsubPattern 结构总是被添加到表尾
    list *pubsub_patterns; /* patterns a client is interested in (SUBSCRIBE) */
    sds peerid; /* Cached peer ID. */
    // 固定大小的缓冲区
    // 回复偏移量
    int bufpos;
    // 回复缓冲区
    char buf[REDIS_REPLY_CHUNK_BYTES];
    } redisClient;
    + +

    Redis服务器的clients属性是一个链表,保存了所有客户端的状态。

    +
    struct redisServer{
    //...
    //一个保存所有客户端状态的链表
    list* clients;
    //...
    };
    + +

    套接字描述符

    fd属性记录了客户端正在使用的套接字描述符。根据客户端的不同类型,fd的值可以为-1或者大于-1的整数。

    +

    如果fd的值为-1,代表该客户端为伪客户端。伪客户端的请求来源于AOF文件或者LUA脚本而不是来自于网络。这种客户端不需要使用套接字进行连接。普通客户端的fd属性的值为大于-1的整数,记录客户端套接字的描述符。

    +

    名字

    客户端的名称记录在name属性中。在默认情况下,客户端是没有名字的。通过CLIENT SETNAME命令可以设置名字。

    +
    127.0.0.1:6379> CLIENT LIST
    id=5 addr=127.0.0.1:42718 fd=8 name= ...
    127.0.0.1:6379> CLIENT SETNAME test-name
    OK
    127.0.0.1:6379> CLIENT LIST
    id=5 addr=127.0.0.1:42718 fd=8 name=test-name ...
    + +

    如果客户端没有设置名字,那么name属性将会指向NULL,否则指向一个字符串对象,保存客户端的名字。

    +

    标志

    客户端的flag属性记录了客户端的角色,其值可以为单个标志或者多个标志的二进制值。

    +

    命令请求

    客户端的输入输出缓冲区用于保存客户端发送的命令请求,用querybuf属性进行保存。

    +

    例如当客户端发送了SET key value命令,那么querybuf的内容将为以下值

    +
    *3
    $3
    set
    $3
    key
    $3
    value
    + +

    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链表的末尾。

    +

    关闭普通客户端

    当出现以下情况时会出现客户端关闭:

    +
      +
    • 客户端与服务器的网络连接被关闭。
    • +
    • 发送了带有不符合协议格式的命令请求。
    • +
    • 客户端成为CLIENT KILL命令的目标
    • +
    • 发送的命令请求大小超过了输入缓冲区的大小
    • +
    • 命令回复超过了输出缓冲的大小
    • +
    • 用户为服务器设置了timeout配置选项。当空转时间超过该选项值时会被关闭。
    • +
    +

    伪客户端

    服务器在初始化时会创建执行Lua脚本的Redis命令的伪客户端,并将该伪客户端关联在服务器状态的lua_client属性值中。在服务器关闭时,该客户端才会被关闭。

    +

    服务器在载入AOF文件时,会创建执行AOF文件的伪客户端,在执行完成后,立即关闭该客户端。

    +

    服务器

    命令请求过程

    发送命令请求

    Redis服务器的命令请求来自与Redis客户端。客户端会将命令请求转化为协议格式,通过连接到服务器的套接字,将协议的命令请求发送给服务器。

    +

    读取命令请求

    当套接字变得可读时,会调用命令处理器来执行以下操作

    +
      +
    • 读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区中。

      +
    • +
    • 对输入缓冲区的命令请求进行分析,提取命令参数。

      +
    • +
    • 调用命令执行器执行指定的命令。

      +
    • +
    +

    命令执行器

    先根据客户端的argv[0]参数,在命令表中查找参数指定的命令,并保存到cmd属性值中。cmd的属性是redisCommand,其结构如下:

    +
    struct redisCommand {
    // 命令名字
    char *name;
    // 实现函数
    redisCommandProc *proc;
    // 参数个数
    int arity;
    // 字符串表示的 FLAG
    char *sflags;
    // 实际 FLAG
    int flags;
    // 从命令中判断命令的键参数。在 Redis 集群转向时使用。
    redisGetKeysProc *getkeys_proc;
    // 指定哪些参数是 key
    int firstkey; /* The first argument that's a key (0 = no keys) */
    int lastkey; /* The last argument that's a key */
    int keystep; /* The step between first and last key */
    // microseconds 记录了命令执行耗费的总毫微秒数
    // calls 是命令被执行的总次数
    long long microseconds, calls;
    };
    + +

    在查找到命令以后,执行预备操作,例如检查cmd指针是否为NULL,参数个数是否正确等等。当完成预备操作后,服务器才开始真正的执行命令。redisCommandProc是一个函数指针的别名,参数为redisClient。

    +
    typedef void redisCommandProc(redisClient *c);
    + +

    在服务器真正实行命令时,只需要调用以下语句即可。

    +
    client->cmd->proc(client);
    + +

    被调用的命令实现函数会执行指定的操作,产生相应的命令回复,保存到输出缓冲区之中。

    +

    在执行完成以后,服务器还需要执行一些后续操作,例如打印慢日志,AOF日志输出。完成后续操作以后,就可以处理下一个命令请求了。

    +

    serverCron函数

    Redis中的serverCron函数会每隔一段执行一次,负责管理服务器的资源,保证服务器运转良好。

    +

    在serverCron会进行例如更新服务器时间缓存,执行持久化操作,关闭客户端等等。

    ]]>
    Redis @@ -619,8 +619,8 @@ Redis - Redis 日志 + Redis 持久化
    @@ -828,36 +828,6 @@ 数据结构 - - Redis如何删除过期键 - /2020/09/09/Redis%E5%A6%82%E4%BD%95%E5%88%A0%E9%99%A4%E8%BF%87%E6%9C%9F%E9%94%AE/ - 通过EXPIRE,PEXPIRE,SETEX等指令,Redis可以为数据库中的某键设置生存时间,在经过指定时间之后,服务器就会删除生存时间为0的键。

    -

    设置过期时间

    保存过期时间

    redisDb结构的expires字典保存了数据库中所有键的过期时间。

    -
    typedef struct redisDb {
    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict; /* The keyspace for this DB */
    // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
    dict *expires; /* Timeout of keys with a timeout set */
    // ...
    } redisDb;
    - -

    过期字典的键是一个指针指向键空间中的某一个键对象,值是一个long long类型的整数,保存了毫秒精度的UNIX时间戳。当执行设置过期时间的命令时,服务器会在数据库的过期字典中关联给定的数据库键以及过期时间。

    -

    移除过期时间

    PERSIST命令可以移除一个键的过期时间。当需要移除过期时间时,会从expires字典之中移除给定键的键值对关联。

    -

    返回过期时间

    TTL和PTTL命令可以返回秒或毫秒为单位的剩余生存时间。其都是通过计算两个键的过期时间与当前时间之差来实现的。

    -

    过期键删除策略

    可能的删除策略

    定时删除

    通过在设置键的同时,创建一个定时器。在定时器结束时,立即删除该键。该策略对内存友好,可以保证过期键最快的被删除。但是在比较多过期键的情况下,会很占用CPU资源,导致服务器的响应时间降低。

    -

    惰性删除

    惰性删除指的是当从键空间中获取值时,检查该键是否过期,如果过期的话,则删除该键,否则返回该键。惰性删除策略不会在删除其他无关键上花费CPU时间,属于CPU友好型策略。但是如果一个键已经过期但是一致没有被访问到,那么该键永远不会过期,所占用的内存永远不会释放。

    -

    定期删除

    每隔一段时间,对数据库进行检查,删除其中的过期键。其难点在于确定执行的时长与频率。如果执行的太频繁,会导致占用过多CPU,如果执行的太少,会导致内存浪费的情况。

    -

    Redis中的删除策略实现

    Redis中整合了惰性删除和定期删除两种策略,取得了合理使用CPU时间和内存空间之间的平衡。

    -

    惰性删除实现

    所有读写redis命令在执行之前都会调用惰性删除的代码进行判断,惰性删除的代码如下:

    -
    /*
    * 检查 key 是否已经过期,如果是的话,将它从数据库中删除。
    *
    * 返回 0 表示键没有过期时间,或者键未过期。
    * 返回 1 表示键已经因为过期而被删除了。
    */
    int expireIfNeeded(redisDb *db, robj *key) {
    // 取出键的过期时间
    mstime_t when = getExpire(db,key);
    mstime_t now;
    // 没有过期时间
    if (when < 0) return 0;
    // 如果服务器正在进行载入,那么不进行任何过期检查
    if (server.loading) return 0;
    // 当前时间
    now = server.lua_caller ? server.lua_time_start : mstime();
    // 当服务器运行在 replication 模式时
    // 附属节点并不主动删除 key
    // 它只返回一个逻辑上正确的返回值
    // 真正的删除操作要等待主节点发来删除命令时才执行
    // 从而保证数据的同步
    if (server.masterhost != NULL) return now > when;
    // 运行到这里,表示键带有过期时间,并且服务器为主节点
    // 如果未过期,返回 0
    if (now <= when) return 0;
    server.stat_expiredkeys++;
    // 向 AOF 文件和附属节点传播过期信息
    propagateExpire(db,key);
    // 发送事件通知
    notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
    "expired",key,db->id);
    // 将过期键从数据库中删除
    return dbDelete(db,key);
    }
    - -

    如果键已经过期,那么该函数会将输入的键从数据库之中删除,否则不做任何动作。当处理后的键仍然存在时,命令会按照存在的情况继续执行,否则按照不存在的情况执行。

    -

    定期删除实现

    每当Redis的周期操作函数serverCron函数执行时,会调用activeExpireCycle函数进行过期键的清理。其会在规定的时间内,分次遍历数据库中的各个数据库,从expires字典中随机检查一部分过期时间,并删除其中的过期键。在扫描的时候,会提供一个全局变量记录上一次扫描的进度,并在下一次调用时,接着上一次的进度进行新一轮的检查工作。

    -

    AOF,RDB和复制功能处理过期键

    AOF文件处理

    当一个键已经过期时并被删除时,Redis会像AOF日志之中添加一条DEL命令,来记录该键已经被删除。在进行AOF重写的时候,程序会对数据库中的键进行检查,已过期的键不会保存到重写以后的AOF日志之中。

    -

    RDB文件处理

    在生成RDB文件的时候,如果一个键已经过期,那么不会被保存到RDB文件之中。在载入RDB文件的时候,如果以主服务器的方式运行,那么过期的键不会被载入到数据库之中。如果以从服务器的方式运行,那么无论键是否过期都会被载入,因为在于主服务器同步的时候,从服务器的数据就会清空,所以对从服务器也没有影响。

    -

    复制功能处理

    当处于服务器处于复制模式下时,服务器的过期删除键由主服务器控制。在主服务器删除一个过期键之后,会向从服务器发送一个DEL命令,告诉从服务器删除该键。从服务器即使键到达了过期时间也不会删除,知道收到DEL命令时才进行删除。通过主服务器统一的删除过期键可以保证主从一致性。

    -]]>
    - - Redis - - - Redis - -
    Redis实现主从复制原理 /2020/09/11/Redis%E5%AE%9E%E7%8E%B0%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E5%8E%9F%E7%90%86/ @@ -905,6 +875,36 @@

    主从级联分担全量复制压力

    在一次全量复制中,对于主库由两个耗时的操作:生成 RDB 文件和传输 RDB 文件。

    如果从库数量很多,会导致出现主库忙于生成RDB文件,同时占用大量的网络资源用于传输RDB文件。可以通过“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。

    具体的方式为在部署主从集群的时候,可以选择通过手动选择一个从库用于级联其他从库。这些从库在进行同步时,不用再和主库进行交互了,只要和级联的从库进行写操作同步就行了,这就可以减轻主库上的压力。

    +]]> + + Redis + + + Redis + +
    + + Redis如何删除过期键 + /2020/09/09/Redis%E5%A6%82%E4%BD%95%E5%88%A0%E9%99%A4%E8%BF%87%E6%9C%9F%E9%94%AE/ + 通过EXPIRE,PEXPIRE,SETEX等指令,Redis可以为数据库中的某键设置生存时间,在经过指定时间之后,服务器就会删除生存时间为0的键。

    +

    设置过期时间

    保存过期时间

    redisDb结构的expires字典保存了数据库中所有键的过期时间。

    +
    typedef struct redisDb {
    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict; /* The keyspace for this DB */
    // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
    dict *expires; /* Timeout of keys with a timeout set */
    // ...
    } redisDb;
    + +

    过期字典的键是一个指针指向键空间中的某一个键对象,值是一个long long类型的整数,保存了毫秒精度的UNIX时间戳。当执行设置过期时间的命令时,服务器会在数据库的过期字典中关联给定的数据库键以及过期时间。

    +

    移除过期时间

    PERSIST命令可以移除一个键的过期时间。当需要移除过期时间时,会从expires字典之中移除给定键的键值对关联。

    +

    返回过期时间

    TTL和PTTL命令可以返回秒或毫秒为单位的剩余生存时间。其都是通过计算两个键的过期时间与当前时间之差来实现的。

    +

    过期键删除策略

    可能的删除策略

    定时删除

    通过在设置键的同时,创建一个定时器。在定时器结束时,立即删除该键。该策略对内存友好,可以保证过期键最快的被删除。但是在比较多过期键的情况下,会很占用CPU资源,导致服务器的响应时间降低。

    +

    惰性删除

    惰性删除指的是当从键空间中获取值时,检查该键是否过期,如果过期的话,则删除该键,否则返回该键。惰性删除策略不会在删除其他无关键上花费CPU时间,属于CPU友好型策略。但是如果一个键已经过期但是一致没有被访问到,那么该键永远不会过期,所占用的内存永远不会释放。

    +

    定期删除

    每隔一段时间,对数据库进行检查,删除其中的过期键。其难点在于确定执行的时长与频率。如果执行的太频繁,会导致占用过多CPU,如果执行的太少,会导致内存浪费的情况。

    +

    Redis中的删除策略实现

    Redis中整合了惰性删除和定期删除两种策略,取得了合理使用CPU时间和内存空间之间的平衡。

    +

    惰性删除实现

    所有读写redis命令在执行之前都会调用惰性删除的代码进行判断,惰性删除的代码如下:

    +
    /*
    * 检查 key 是否已经过期,如果是的话,将它从数据库中删除。
    *
    * 返回 0 表示键没有过期时间,或者键未过期。
    * 返回 1 表示键已经因为过期而被删除了。
    */
    int expireIfNeeded(redisDb *db, robj *key) {
    // 取出键的过期时间
    mstime_t when = getExpire(db,key);
    mstime_t now;
    // 没有过期时间
    if (when < 0) return 0;
    // 如果服务器正在进行载入,那么不进行任何过期检查
    if (server.loading) return 0;
    // 当前时间
    now = server.lua_caller ? server.lua_time_start : mstime();
    // 当服务器运行在 replication 模式时
    // 附属节点并不主动删除 key
    // 它只返回一个逻辑上正确的返回值
    // 真正的删除操作要等待主节点发来删除命令时才执行
    // 从而保证数据的同步
    if (server.masterhost != NULL) return now > when;
    // 运行到这里,表示键带有过期时间,并且服务器为主节点
    // 如果未过期,返回 0
    if (now <= when) return 0;
    server.stat_expiredkeys++;
    // 向 AOF 文件和附属节点传播过期信息
    propagateExpire(db,key);
    // 发送事件通知
    notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
    "expired",key,db->id);
    // 将过期键从数据库中删除
    return dbDelete(db,key);
    }
    + +

    如果键已经过期,那么该函数会将输入的键从数据库之中删除,否则不做任何动作。当处理后的键仍然存在时,命令会按照存在的情况继续执行,否则按照不存在的情况执行。

    +

    定期删除实现

    每当Redis的周期操作函数serverCron函数执行时,会调用activeExpireCycle函数进行过期键的清理。其会在规定的时间内,分次遍历数据库中的各个数据库,从expires字典中随机检查一部分过期时间,并删除其中的过期键。在扫描的时候,会提供一个全局变量记录上一次扫描的进度,并在下一次调用时,接着上一次的进度进行新一轮的检查工作。

    +

    AOF,RDB和复制功能处理过期键

    AOF文件处理

    当一个键已经过期时并被删除时,Redis会像AOF日志之中添加一条DEL命令,来记录该键已经被删除。在进行AOF重写的时候,程序会对数据库中的键进行检查,已过期的键不会保存到重写以后的AOF日志之中。

    +

    RDB文件处理

    在生成RDB文件的时候,如果一个键已经过期,那么不会被保存到RDB文件之中。在载入RDB文件的时候,如果以主服务器的方式运行,那么过期的键不会被载入到数据库之中。如果以从服务器的方式运行,那么无论键是否过期都会被载入,因为在于主服务器同步的时候,从服务器的数据就会清空,所以对从服务器也没有影响。

    +

    复制功能处理

    当处于服务器处于复制模式下时,服务器的过期删除键由主服务器控制。在主服务器删除一个过期键之后,会向从服务器发送一个DEL命令,告诉从服务器删除该键。从服务器即使键到达了过期时间也不会删除,知道收到DEL命令时才进行删除。通过主服务器统一的删除过期键可以保证主从一致性。

    ]]>
    Redis @@ -960,26 +960,6 @@ 拥塞控制
    - - Kafka基本概念 - /2021/01/26/kafka%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/ - 一个经典的Kafka架构包括若干个Producer、Broker、Consumer和ZooKeeper集群。

    -

    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)模式从服务端拉取消息,并且保存消费的具体位置。当消费者宕机重启时可以根据之前保存的消费位置重新拉取需要的消息进行消费。

    -]]>
    - - Kafka - - - Kafka - -
    java使用自定义注解 /2020/07/20/java%E4%BD%BF%E7%94%A8%E8%87%AA%E5%AE%9A%E4%B9%89%E6%B3%A8%E8%A7%A3/ @@ -1019,6 +999,26 @@ 注解 + + Kafka基本概念 + /2021/01/26/kafka%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/ + 一个经典的Kafka架构包括若干个Producer、Broker、Consumer和ZooKeeper集群。

    +

    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)模式从服务端拉取消息,并且保存消费的具体位置。当消费者宕机重启时可以根据之前保存的消费位置重新拉取需要的消息进行消费。

    +]]>
    + + Kafka + + + Kafka + +
    netty常用概念 /2021/01/28/netty%E5%B8%B8%E7%94%A8%E6%A6%82%E5%BF%B5/ @@ -1229,49 +1229,6 @@ 操作系统 - - 设计模式之结构型模式 - /2020/09/04/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E7%BB%93%E6%9E%84%E5%9E%8B%E6%A8%A1%E5%BC%8F/ - 结构型模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。

    -

    代理模式

    代理模式在不改变原始类的情况下,通过添加代理类来给原始类添加附加功能。代理模式经常用在比如监控,统计,鉴权这样的非业务逻辑之中。同时RPC框架也可以看成一种代理模式,被称为远程代理,在使用RPC的时候,不需要考虑与服务器交互的细节,只关注业务逻辑即可。

    -

    代理模式通过针对每个类都创建一个代理类,具体实现代码如下所示:

    -
    //java 11
    interface A {
    void doSomething();
    }

    class ANeedProxy implements A {
    @Override
    public void doSomething() {
    //TODO
    }
    }

    class AProxy implements A {
    private final A a;

    AProxy(A a) {
    this.a = a;
    }

    @Override
    public void doSomething() {
    if (checkPermission()) {
    this.a.doSomething();
    } else {
    throw new SecurityException("Forbidden");
    }
    }

    private boolean checkPermission() {
    return Math.random() < 0.5;
    }

    public static void main(String[] args) {
    AProxy proxy = new AProxy(new ANeedProxy());
    proxy.doSomething();
    }
    }
    -

    在上面代码之中,代理类AProxy和原始类B都实现了A接口,在使用的时候将B对象使用构造函数传入AProxy中,然后调用原始类来执行代码,同时在调用代码之前检查相关的权限信息。

    -

    动态代理

    在刚才实现的代理模式中,需要将原始类中的所有方法都重新实现一遍。同时如果有多个类需要使用代理,那么就需要写多个代理类,会导致类的个数不必要的增多,可以使用动态代理来解决这一个问题。Java本身就已经提供了动态代理的语法,使用jdk动态代理实现的代码如下:

    -
    //java 11
    class ADynamicProxyHandler implements InvocationHandler {
    private final Object proxiedObject;

    ADynamicProxyHandler(Object proxiedObject) {
    this.proxiedObject = proxiedObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (checkPermission()) {
    return method.invoke(proxiedObject, args);
    } else {
    throw new SecurityException("Forbidden");
    }
    }

    private boolean checkPermission() {
    return Math.random() < 0.5;
    }

    public static void main(String[] args) {
    B b = new B();
    ADynamicProxyHandler handler = new ADynamicProxyHandler(b);
    A a = (A) Proxy.newProxyInstance(b.getClass().getClassLoader(), b.getClass().getInterfaces(), handler);
    a.doSomething();
    }
    }
    -

    动态代理十分有用,可以帮助在编写代码时减少模板代码,减少维护和开发的成本,Spring AOP的底层实现原理就是基于这样的动态代理的。

    -

    桥接模式

    桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化,其思路是不要过度使用继承,而是优先拆分部分组件,通过组合的方式来进行扩展。桥接模式的目的是为了避免直接继承带来的子类爆炸。

    -

    例如如果对于图形类而言,有是什么形状这一维度,也有颜色这一维度,那么就可以使用桥接模式来编写代码。具体代码如下:

    -
    //kotlin 1.4.0
    abstract class Color protected constructor(protected val name: String) {
    abstract fun bepaint(shape: String)
    }

    class Grey : Color("灰色") {
    override fun bepaint(shape: String) = println("灰色的$shape");
    }

    class Green : Color("绿色") {
    override fun bepaint(shape: String) = println("绿色的$shape");
    }

    abstract class Shape protected constructor(protected val color: Color) {
    abstract fun draw()
    }

    class Circle(color: Color) : Shape(color) {
    override fun draw() = color.bepaint("正方形")
    }

    class Rectangle(color: Color) : Shape(color) {
    override fun draw() = color.bepaint("长方形")
    }

    fun main() {
    val white = Grey()
    val rectangle = Rectangle(white)
    rectangle.draw()
    }
    - -

    在实际使用中,如果出现一个类出现多个可以单个可以单独变化的维度,那么可以使用桥接模式进行设计。

    -

    装饰器模式

    装饰器模式能够在运行器动态的给某个对象增加功能,同时将核心功能与附加功能分开。Java标准库的IO库就是装饰器模式的经典案例。

    -

    例如我们需要给FileInputStream添加缓存功能以及GZIP解压缩功能,那么实现的代码如下:

    -
    //java 11
    public class Decorator {
    public static void main(String[] args) throws IOException {
    InputStream in = new GZIPInputStream( //提供解压缩
    new BufferedInputStream( //提供缓存
    new FileInputStream("a.txt")
    )
    );
    }
    }
    - -

    这样的代码就是典型的装饰器模式的代码。通过查看源码可以看到GZIPInputStream,BufferedInputStream,FileInputStream都是InputStream的子类。在这个例子中,核心功能指的就是FileInputStream这个真正读取数据的源头,附加工作指的是缓冲和压缩这两个功能。如果我们仍然需要新增附加功能,就可以继承InputStream独立的进行扩展。

    -

    在装饰器模式中,装饰器类和原始类都继承自同样的父类,装饰器模式有个特点就是可以对原始类嵌套使用多个装饰器。

    -

    适配器模式

    适配器模式用于将不兼容的接口转换为可兼容的接口,让原本不能一起工作的类一起工作。适配器模式可以通过继承以及组合的方式进行实现,通过继承来实现适配器模式的代码如下:

    -
    //java 11
    interface ITarget{
    void fa();
    }
    public class Adaptee {
    void f1();
    }

    public class Adaptor extends Adaptee implements ITarget{
    @Override
    void fa(){
    super.f1();
    }
    }
    -

    在这段代码之中,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
    abstract class FileNode {
    protected final String path;
    protected FileNode(String path) {
    this.path = path;
    }
    abstract List<FileNode> list();
    abstract String toTreeString();
    }

    class File extends FileNode {
    File(String path) {
    super(path);
    }
    @Override List<FileNode> list() {
    return Collections.emptyList();
    }

    @Override String toTreeString() {
    return super.path + "\n";
    }
    }

    class Directory extends FileNode {
    Directory(String path) {
    super(path);
    }
    @Override List<FileNode> list() {
    var file = new java.io.File(super.path);
    return Arrays.stream(file.listFiles()).map(f -> {
    if (f.isFile()) {
    return new File(f.getPath());
    } else {
    return new Directory(f.getPath());
    }
    }).collect(Collectors.toList());
    }
    @Override String toTreeString() {
    StringBuilder ret = new StringBuilder();
    ret.append(super.path).append("\n");
    list().forEach(node -> ret.append(node.toTreeString()));
    return ret.toString();
    }
    }
    - -

    组合模式将一组对象抽象成树形结构,将单个对象和组合对象都看做树中的节点,以统一处理逻辑,并且它利用树形结构的特点,递归地处理每个子树,依次简化代码实现。使用场景必须要能够表示成为树形结构是其前提,因此其使用场景比较有局限性。

    -

    享元模式

    享元模式指的是复用被共享的变量,其意图在于复用对象,节省内存。如果在一个系统中存在大量重复对象,且这些对象是不可变变量,就可以将其设计为享元,在内存中只保留一份实例,供多处代码引用。因此,享元模式就是通过工厂方法创建对象,在工厂方法内部,很可能返回缓存的实例,而不是新创建实例,从而实现不可变实例的复用。

    -

    享元模式在Java标准库中有许多的的应用,例如Integer.valueOf这个静态工厂创建实例,当传入的int范围在-128到+127的范围之间时,会直接返回缓存的Integer实例:

    -
    //java 11
    public class Main {
    public static void main(String[] args) {
    Integer n1 = Integer.valueOf(100);
    Integer n2 = Integer.valueOf(100);
    System.out.println(n1 == n2); // true
    Integer n3 = Integer.valueOf(200);
    Integer n4 = Integer.valueOf(200);
    System.out.println(n3 == n4); // false
    }
    }
    -]]>
    - - 设计模式 - - - 设计模式 - -
    设计模式之创建型模式 /2020/09/03/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E5%88%9B%E5%BB%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/ @@ -1358,6 +1315,49 @@ 设计模式 + + 设计模式之结构型模式 + /2020/09/04/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E7%BB%93%E6%9E%84%E5%9E%8B%E6%A8%A1%E5%BC%8F/ + 结构型模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。

    +

    代理模式

    代理模式在不改变原始类的情况下,通过添加代理类来给原始类添加附加功能。代理模式经常用在比如监控,统计,鉴权这样的非业务逻辑之中。同时RPC框架也可以看成一种代理模式,被称为远程代理,在使用RPC的时候,不需要考虑与服务器交互的细节,只关注业务逻辑即可。

    +

    代理模式通过针对每个类都创建一个代理类,具体实现代码如下所示:

    +
    //java 11
    interface A {
    void doSomething();
    }

    class ANeedProxy implements A {
    @Override
    public void doSomething() {
    //TODO
    }
    }

    class AProxy implements A {
    private final A a;

    AProxy(A a) {
    this.a = a;
    }

    @Override
    public void doSomething() {
    if (checkPermission()) {
    this.a.doSomething();
    } else {
    throw new SecurityException("Forbidden");
    }
    }

    private boolean checkPermission() {
    return Math.random() < 0.5;
    }

    public static void main(String[] args) {
    AProxy proxy = new AProxy(new ANeedProxy());
    proxy.doSomething();
    }
    }
    +

    在上面代码之中,代理类AProxy和原始类B都实现了A接口,在使用的时候将B对象使用构造函数传入AProxy中,然后调用原始类来执行代码,同时在调用代码之前检查相关的权限信息。

    +

    动态代理

    在刚才实现的代理模式中,需要将原始类中的所有方法都重新实现一遍。同时如果有多个类需要使用代理,那么就需要写多个代理类,会导致类的个数不必要的增多,可以使用动态代理来解决这一个问题。Java本身就已经提供了动态代理的语法,使用jdk动态代理实现的代码如下:

    +
    //java 11
    class ADynamicProxyHandler implements InvocationHandler {
    private final Object proxiedObject;

    ADynamicProxyHandler(Object proxiedObject) {
    this.proxiedObject = proxiedObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (checkPermission()) {
    return method.invoke(proxiedObject, args);
    } else {
    throw new SecurityException("Forbidden");
    }
    }

    private boolean checkPermission() {
    return Math.random() < 0.5;
    }

    public static void main(String[] args) {
    B b = new B();
    ADynamicProxyHandler handler = new ADynamicProxyHandler(b);
    A a = (A) Proxy.newProxyInstance(b.getClass().getClassLoader(), b.getClass().getInterfaces(), handler);
    a.doSomething();
    }
    }
    +

    动态代理十分有用,可以帮助在编写代码时减少模板代码,减少维护和开发的成本,Spring AOP的底层实现原理就是基于这样的动态代理的。

    +

    桥接模式

    桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化,其思路是不要过度使用继承,而是优先拆分部分组件,通过组合的方式来进行扩展。桥接模式的目的是为了避免直接继承带来的子类爆炸。

    +

    例如如果对于图形类而言,有是什么形状这一维度,也有颜色这一维度,那么就可以使用桥接模式来编写代码。具体代码如下:

    +
    //kotlin 1.4.0
    abstract class Color protected constructor(protected val name: String) {
    abstract fun bepaint(shape: String)
    }

    class Grey : Color("灰色") {
    override fun bepaint(shape: String) = println("灰色的$shape");
    }

    class Green : Color("绿色") {
    override fun bepaint(shape: String) = println("绿色的$shape");
    }

    abstract class Shape protected constructor(protected val color: Color) {
    abstract fun draw()
    }

    class Circle(color: Color) : Shape(color) {
    override fun draw() = color.bepaint("正方形")
    }

    class Rectangle(color: Color) : Shape(color) {
    override fun draw() = color.bepaint("长方形")
    }

    fun main() {
    val white = Grey()
    val rectangle = Rectangle(white)
    rectangle.draw()
    }
    + +

    在实际使用中,如果出现一个类出现多个可以单个可以单独变化的维度,那么可以使用桥接模式进行设计。

    +

    装饰器模式

    装饰器模式能够在运行器动态的给某个对象增加功能,同时将核心功能与附加功能分开。Java标准库的IO库就是装饰器模式的经典案例。

    +

    例如我们需要给FileInputStream添加缓存功能以及GZIP解压缩功能,那么实现的代码如下:

    +
    //java 11
    public class Decorator {
    public static void main(String[] args) throws IOException {
    InputStream in = new GZIPInputStream( //提供解压缩
    new BufferedInputStream( //提供缓存
    new FileInputStream("a.txt")
    )
    );
    }
    }
    + +

    这样的代码就是典型的装饰器模式的代码。通过查看源码可以看到GZIPInputStream,BufferedInputStream,FileInputStream都是InputStream的子类。在这个例子中,核心功能指的就是FileInputStream这个真正读取数据的源头,附加工作指的是缓冲和压缩这两个功能。如果我们仍然需要新增附加功能,就可以继承InputStream独立的进行扩展。

    +

    在装饰器模式中,装饰器类和原始类都继承自同样的父类,装饰器模式有个特点就是可以对原始类嵌套使用多个装饰器。

    +

    适配器模式

    适配器模式用于将不兼容的接口转换为可兼容的接口,让原本不能一起工作的类一起工作。适配器模式可以通过继承以及组合的方式进行实现,通过继承来实现适配器模式的代码如下:

    +
    //java 11
    interface ITarget{
    void fa();
    }
    public class Adaptee {
    void f1();
    }

    public class Adaptor extends Adaptee implements ITarget{
    @Override
    void fa(){
    super.f1();
    }
    }
    +

    在这段代码之中,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
    abstract class FileNode {
    protected final String path;
    protected FileNode(String path) {
    this.path = path;
    }
    abstract List<FileNode> list();
    abstract String toTreeString();
    }

    class File extends FileNode {
    File(String path) {
    super(path);
    }
    @Override List<FileNode> list() {
    return Collections.emptyList();
    }

    @Override String toTreeString() {
    return super.path + "\n";
    }
    }

    class Directory extends FileNode {
    Directory(String path) {
    super(path);
    }
    @Override List<FileNode> list() {
    var file = new java.io.File(super.path);
    return Arrays.stream(file.listFiles()).map(f -> {
    if (f.isFile()) {
    return new File(f.getPath());
    } else {
    return new Directory(f.getPath());
    }
    }).collect(Collectors.toList());
    }
    @Override String toTreeString() {
    StringBuilder ret = new StringBuilder();
    ret.append(super.path).append("\n");
    list().forEach(node -> ret.append(node.toTreeString()));
    return ret.toString();
    }
    }
    + +

    组合模式将一组对象抽象成树形结构,将单个对象和组合对象都看做树中的节点,以统一处理逻辑,并且它利用树形结构的特点,递归地处理每个子树,依次简化代码实现。使用场景必须要能够表示成为树形结构是其前提,因此其使用场景比较有局限性。

    +

    享元模式

    享元模式指的是复用被共享的变量,其意图在于复用对象,节省内存。如果在一个系统中存在大量重复对象,且这些对象是不可变变量,就可以将其设计为享元,在内存中只保留一份实例,供多处代码引用。因此,享元模式就是通过工厂方法创建对象,在工厂方法内部,很可能返回缓存的实例,而不是新创建实例,从而实现不可变实例的复用。

    +

    享元模式在Java标准库中有许多的的应用,例如Integer.valueOf这个静态工厂创建实例,当传入的int范围在-128到+127的范围之间时,会直接返回缓存的Integer实例:

    +
    //java 11
    public class Main {
    public static void main(String[] args) {
    Integer n1 = Integer.valueOf(100);
    Integer n2 = Integer.valueOf(100);
    System.out.println(n1 == n2); // true
    Integer n3 = Integer.valueOf(200);
    Integer n4 = Integer.valueOf(200);
    System.out.println(n3 == n4); // false
    }
    }
    +]]>
    + + 设计模式 + + + 设计模式 + +
    设计模式之创建型模式 /2020/09/09/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E8%A1%8C%E4%B8%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/ diff --git a/sitemap.xml b/sitemap.xml index ec20310..427dd2a 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -207,77 +207,77 @@ - http://qian0817.top/tags/%E7%BD%91%E7%BB%9C/ + http://qian0817.top/tags/%E5%88%86%E5%B8%83%E5%BC%8F/ 2021-02-01 daily 0.6 - http://qian0817.top/tags/Java/ + http://qian0817.top/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/ 2021-02-01 daily 0.6 - http://qian0817.top/tags/JVM/ + http://qian0817.top/tags/%E7%BD%91%E7%BB%9C/ 2021-02-01 daily 0.6 - http://qian0817.top/tags/Redis/ + http://qian0817.top/tags/HTTP/ 2021-02-01 daily 0.6 - http://qian0817.top/tags/%E5%88%86%E5%B8%83%E5%BC%8F/ + http://qian0817.top/tags/JVM/ 2021-02-01 daily 0.6 - http://qian0817.top/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/ + http://qian0817.top/tags/Java/ 2021-02-01 daily 0.6 - http://qian0817.top/tags/HTTP/ + http://qian0817.top/tags/Kubernetes/ 2021-02-01 daily 0.6 - http://qian0817.top/tags/Kubernetes/ + http://qian0817.top/tags/MYSQL/ 2021-02-01 daily 0.6 - http://qian0817.top/tags/MYSQL/ + http://qian0817.top/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/ 2021-02-01 daily 0.6 - http://qian0817.top/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/ + http://qian0817.top/tags/%E6%97%A5%E5%BF%97/ 2021-02-01 daily 0.6 - http://qian0817.top/tags/%E6%97%A5%E5%BF%97/ + http://qian0817.top/tags/Redis/ 2021-02-01 daily 0.6 @@ -312,14 +312,14 @@ - http://qian0817.top/tags/Kafka/ + http://qian0817.top/tags/%E6%B3%A8%E8%A7%A3/ 2021-02-01 daily 0.6 - http://qian0817.top/tags/%E6%B3%A8%E8%A7%A3/ + http://qian0817.top/tags/Kafka/ 2021-02-01 daily 0.6 @@ -377,14 +377,14 @@ - http://qian0817.top/categories/%E7%BD%91%E7%BB%9C/ + http://qian0817.top/categories/%E5%88%86%E5%B8%83%E5%BC%8F/ 2021-02-01 daily 0.6 - http://qian0817.top/categories/Java/ + http://qian0817.top/categories/%E7%BD%91%E7%BB%9C/ 2021-02-01 daily 0.6 @@ -398,28 +398,28 @@ - http://qian0817.top/categories/Redis/ + http://qian0817.top/categories/Java/ 2021-02-01 daily 0.6 - http://qian0817.top/categories/%E5%88%86%E5%B8%83%E5%BC%8F/ + http://qian0817.top/categories/Kubernetes/ 2021-02-01 daily 0.6 - http://qian0817.top/categories/Kubernetes/ + http://qian0817.top/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/ 2021-02-01 daily 0.6 - http://qian0817.top/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/ + http://qian0817.top/categories/Redis/ 2021-02-01 daily 0.6