导图社区 Java全栈知识
Java全栈知识大纲包括JAVA基础、JAVA集合、JAVA并发、JAVA IO、JVM调优、Java新版本、数据结构与算法、数据库、开发基础等等,助你大概了解要学习的内容。
编辑于2023-03-15 15:58:20 广东JAVA全栈
JAVA基础
语法基础
# a = a + b 与 a += b 的区别 += 隐式的将加操作的结果类型强制转换为持有结果的类型。如果两个整型相加,如 byte、short 或者 int,首先会将它们提升到 int 类型,然后在执行加法操作。  (因为 a+b 操作会将 a、b 提升为 int 类型,所以将 int 类型赋值给 byte 就会编译出错) # 3*0.1 == 0.3 将会返回什么? true 还是 false? false,因为有些浮点数不能完全精确的表示出来。 # 能在 Switch 中使用 String 吗? 从 Java 7 开始,我们可以在 switch case 中使用字符串,但这仅仅是一个语法糖。内部实现在 switch 中使用字符串的 hash code。 # 对equals()和hashCode()的理解? 为什么在重写 equals 方法的时候需要重写 hashCode 方法? 因为有强制的规范指定需要同时重写 hashcode 与 equals 是方法,许多容器类,如 HashMap、HashSet 都依赖于 hashcode 与 equals 的规定。 有没有可能两个不相等的对象有相同的 hashcode? 有可能,两个不相等的对象可能会有相同的 hashcode 值,这就是为什么在 hashmap 中会有冲突。相等 hashcode 值的规定只是说如果两个对象相等,必须有相同的hashcode 值,但是没有关于不相等对象的任何规定 两个相同的对象会有不同的 hash code 吗? 不能,根据 hash code 的规定,这是不可能的 # final、finalize 和 finally 的不同之处? final 是一个修饰符,可以修饰变量、方法和类。如果 final 修饰变量,意味着该变量的值在初始化后不能被改变。 Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的,但是什么时候调用 finalize 没有保证。 finally 是一个关键字,与 try 和 catch 一起用于异常的处理。finally 块一定会被执行,无论在 try 块中是否有发生异常。 # String、StringBuffer与StringBuilder的区别? 第一点: 可变和适用范围。String对象是不可变的,而StringBuffer和StringBuilder是可变字符序列。每次对String的操作相当于生成一个新的String对象,而对StringBuffer和StringBuilder的操作是对对象本身的操作,而不会生成新的对象,所以对于频繁改变内容的字符串避免使用String,因为频繁的生成对象将会对系统性能产生影响。 第二点: 线程安全。String由于有final修饰,是immutable的,安全性是简单而纯粹的。StringBuilder和StringBuffer的区别在于StringBuilder不保证同步,也就是说如果需要线程安全需要使用StringBuffer,不需要同步的StringBuilder效率更高。 # 接口与抽象类的区别? 一个子类只能继承一个抽象类, 但能实现多个接口 抽象类可以有构造方法, 接口没有构造方法 抽象类可以有普通成员变量, 接口没有普通成员变量 抽象类和接口都可有静态成员变量, 抽象类中静态成员变量访问类型任意,接口只能public static final(默认) 抽象类可以没有抽象方法, 抽象类可以有普通方法;接口在JDK8之前都是抽象方法,在JDK8可以有default方法,在JDK9中允许有私有普通方法 抽象类可以有静态方法;接口在JDK8之前不能有静态方法,在JDK8中可以有静态方法,且只能被接口类直接调用(不能被实现类的对象调用) 抽象类中的方法可以是public、protected; 接口方法在JDK8之前只有public abstract,在JDK8可以有default方法,在JDK9中允许有private方法 # this() & super()在构造方法中的区别? 调用super()必须写在子类构造方法的第一行, 否则编译不通过 super从子类调用父类构造, this在同一类中调用其他构造均需要放在第一行 尽管可以用this调用一个构造器, 却不能调用2个 this和super不能出现在同一个构造器中, 否则编译不通过 this()、super()都指的对象,不可以在static环境中使用 本质this指向本对象的指针。super是一个关键字 # Java移位运算符? java中有三种移位运算符 << :左移运算符,x << 1,相当于x乘以2(不溢出的情况下),低位补0 >> :带符号右移,x >> 1,相当于x除以2,正数高位补0,负数高位补1 >>> :无符号右移,忽略符号位,空位都以0补齐# 1.2 泛型
封装
封装 利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。 优点: 减少耦合: 可以独立地开发、测试、优化、使用、理解和修改 减轻维护的负担: 可以更容易被程序员理解,并且在调试的时候可以不影响其他模块 有效地调节性能: 可以通过剖析确定哪些模块影响了系统的性能 提高软件的可重用性 降低了构建大型系统的风险: 即使整个系统不可用,但是这些独立的模块却有可能是可用的 以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。 注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改 gender 属性使用的数据类型时,也可以在不影响客户端代码的情况下进行。 
继承
继承 继承实现了 IS-A 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal,从而获得 Animal 非 private 的属性和方法。继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 向上转型 。 
多态
多态 多态分为编译时多态和运行时多态: 编译时多态主要指方法的重载 运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定 运行时多态有三个条件: 继承 覆盖(重写) 向上转型 下面的代码中,乐器类(Instrument)有两个子类: Wind 和 Percussion,它们都覆盖了父类的 play() 方法,并且在 main() 方法中使用父类 Instrument 来引用 Wind 和 Percussion 对象。在 Instrument 引用调用 play() 方法时,会执行实际引用对象所在类的 play() 方法,而不是 Instrument 类的方法 
泛型
# 为什么需要泛型? 适用于多种数据类型执行相同的代码  如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法;通过泛型,我们可以复用为一个方法:  泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型) 看下这个例子:  我们在使用上述list中,list中的元素都是Object类型(无法约束其中的类型),所以在取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现java.lang.ClassCastException异常。引入泛型,它将提供类型的约束,提供编译前的检查:  # 泛型类如何定义使用? 从一个简单的泛型类看起:  多元泛型  # 泛型接口如何定义使用? 简单的泛型接口  # 泛型方法如何定义使用? 泛型方法,是在调用方法的时候指明泛型的具体类型。 定义泛型方法语法格式  调用泛型方法语法格式  说明一下,定义泛型方法时,必须在返回值前边加一个<T>,来声明这是一个泛型方法,持有一个泛型T,然后才可以用泛型T作为方法的返回值。 Class<T>的作用就是指明泛型的具体类型,而Class<T>类型的变量c,可以用来创建泛型类的对象。 为什么要用变量c来创建对象呢?既然是泛型方法,就代表着我们不知道具体的类型是什么,也不知道构造方法如何,因此没有办法去new一个对象,但可以利用变量c的newInstance方法去创建对象,也就是利用反射创建对象。 泛型方法要求的参数是Class<T>类型,而Class.forName()方法的返回值也是Class<T>,因此可以用Class.forName()作为参数。其中,forName()方法中的参数是何种类型,返回的Class<T>就是何种类型。在本例中,forName()方法中传入的是User类的完整路径,因此返回的是Class<User>类型的对象,因此调用泛型方法时,变量c的类型就是Class<User>,因此泛型方法中的泛型T就被指明为User,因此变量obj的类型为User。 当然,泛型方法不是仅仅可以有一个参数Class<T>,可以根据需要添加其他参数。 为什么要使用泛型方法呢?因为泛型类要在实例化的时候就指明类型,如果想换一种类型,不得不重新new一次,可能不够灵活;而泛型方法可以在调用的时候指明类型,更加灵活。 # 泛型的上限和下限? 在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。 上限  下限  # 如何理解Java中的泛型是伪泛型? 泛型中类型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
注解
# 注解的作用? 注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。它主要的作用有以下四方面: 生成文档,通过代码里标识的元数据生成javadoc文档。 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。 运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例。 # 注解的常见分类? Java自带的标准注解,包括@Override、@Deprecated和@SuppressWarnings,分别用于标明重写某个方法、标明某个类或方法过时、标明要忽略的警告,用这些注解标明后编译器就会进行检查。 元注解,元注解是用于定义注解的注解,包括@Retention、@Target、@Inherited、@Documented @Retention用于标明注解被保留的阶段 @Target用于标明注解使用的范围 @Inherited用于标明注解可继承 @Documented用于标明是否生成javadoc文档 自定义注解,可以根据自己的需求定义注解,并可用元注解对自定义注解进行注解。
异常
# Java异常类层次结构? Throwable 是 Java 语言中所有错误与异常的超类。 Error 类及其子类:程序中无法处理的错误,表示运行应用程序中出现了严重的错误。 Exception 程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。  运行时异常 都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。 运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。 非运行时异常 (编译异常) 是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。 # 可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)区别? 可查异常(编译器要求必须处置的异常): 正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。 除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try- catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。 不可查异常(编译器不要求强制处置的异常) 包括运行时异常(RuntimeException与其子类)和错误(Error)。 # throw和throws的区别? 异常的申明(throws) 在Java中,当前执行的语句必属于某个方法,Java解释器调用main方法执行开始执行程序。若方法中存在检查异常,如果不对其捕获,那必须在方法头中显式声明该异常,以便于告知方法调用者此方法有异常,需要进行处理。 在方法中声明一个异常,方法头中使用关键字throws,后面接上要声明的异常。若声明多个异常,则使用逗号分割。如下所示:  异常的抛出(throw) 如果代码可能会引发某种错误,可以创建一个合适的异常类实例并抛出它,这就是抛出异常。如下所示:  # Java 7 的 try-with-resource? 如果你的资源实现了 AutoCloseable 接口,你可以使用这个语法。大多数的 Java 标准资源都继承了这个接口。当你在 try 子句中打开资源,资源会在 try 代码块执行后或异常处理后自动关闭。  # 异常的底层? 提到JVM处理异常的机制,就需要提及Exception Table,以下称为异常表。我们暂且不急于介绍异常表,先看一个简单的 Java 处理异常的小例子。  使用javap来分析这段代码(需要先使用javac编译)  看到上面的代码,应该会有会心一笑,因为终于看到了Exception table,也就是我们要研究的异常表。 异常表中包含了一个或多个异常处理者(Exception Handler)的信息,这些信息包含如下 from 可能发生异常的起始点 to 可能发生异常的结束点 target 上述from和to之前发生异常后的异常处理者的位置 type 异常处理者处理的异常的类信息
反射
# 什么是反射? JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。  # 反射的使用? 在Java中,Class类与java.lang.reflect类库一起对反射技术进行了全力的支持。在反射包中,我们常用的类主要有Constructor类表示的是Class 对象所表示的类的构造方法,利用它可以在运行时动态创建对象、Field表示Class对象所表示的类的成员变量,通过它可以在运行时动态修改成员变量的属性值(包含private)、Method表示Class对象所表示的类的成员方法,通过它可以动态调用对象的方法(包含private) Class类对象的获取  Constructor类及其用法 Field类及其用法 Method类及其用法 # getName、getCanonicalName与getSimpleName的区别? getSimpleName:只获取类名 getName:类的全限定名,jvm中Class的表示,可以用于动态加载Class对象,例如Class.forName。 getCanonicalName:返回更容易理解的表示,主要用于输出(toString)或log打印,大多数情况下和getName一样,但是在内部类、数组等类型的表示形式就不同了。
SPI机制
# 什么是SPI机制? SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦。 SPI整体机制图如下:  当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的工具类是:java.util.ServiceLoader # SPI机制的应用? SPI机制 - JDBC DriverManager 在JDBC4.0之前,我们开发有连接数据库的时候,通常会用Class.forName("com.mysql.jdbc.Driver")这句先加载数据库相关的驱动,然后再进行获取连接等的操作。而JDBC4.0之后不需要用Class.forName("com.mysql.jdbc.Driver")来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的SPI扩展机制来实现。 JDBC接口定义 首先在java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商来提供的。 mysql实现 在mysql的jar包mysql-connector-java-6.0.6.jar中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。 postgresql实现 同样在postgresql的jar包postgresql-42.0.0.jar中,也可以找到同样的配置文件,文件内容是org.postgresql.Driver,这是postgresql对Java的java.sql.Driver的实现。 使用方法 上面说了,现在使用SPI扩展来加载具体的驱动,我们在Java中写连接数据库的代码的时候,不需要再使用Class.forName("com.mysql.jdbc.Driver")来加载驱动了,而是直接使用如下代码:  # SPI机制的简单示例? 我们现在需要使用一个内容搜索接口,搜索的实现可能是基于文件系统的搜索,也可能是基于数据库的搜索。 先定义好接口  文件搜索实现  数据库搜索实现  resources 接下来可以在resources下新建META-INF/services/目录,然后新建接口全限定名的文件:com.cainiao.ys.spi.learn.Search,里面加上我们需要用到的实现类  测试方法  可以看到输出结果:文件搜索 hello world 如果在com.cainiao.ys.spi.learn.Search文件里写上两个实现类,那最后的输出结果就是两行了。 这就是因为ServiceLoader.load(Search.class)在加载某接口时,会去META-INF/services下找接口的全限定名文件,再根据里面的内容加载相应的实现类 这就是spi的思想,接口的实现由provider实现,provider只用在提交的jar包里的META-INF/services下根据平台定义的接口新建文件,并添加进相应的实现类内容就好。
JAVA集合
Collection
# 知识体系架构  # 介绍 容器,就是可以容纳其他Java对象的对象。*Java Collections Framework(JCF)*为Java开发者提供了通用的容器,其始于JDK 1.2,优点是: 降低编程难度 提高程序性能 提高API间的互操作性 降低学习难度 降低设计和实现相关API的难度 增加程序的重用性 Java容器里只能放对象,对于基本类型(int, long, float, double等),需要将其包装成对象类型后(Integer, Long, Float, Double等)才能放到容器里。很多时候拆包装和解包装能够自动完成。这虽然会导致额外的性能和空间开销,但简化了设计和编程。
Set
TreeSet
HashSet
LinkedHashSet
List
ArrayList
# ArrayList的底层? ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现。除该类未实现同步外,其余跟Vector大致相同。每个ArrayList都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。前面已经提过,Java泛型只是编译器提供的语法糖,所以这里的数组是一个Object数组,以便能够容纳任何类型的对象。 # ArrayList自动扩容? 每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过ensureCapacity(int minCapacity)方法来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。 数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。 # ArrayList的Fail-Fast机制? ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
Vector
LinkedList
Queue
LinkedList
PriorityQueue
Map
# Map有哪些类? TreeMap 基于红黑树实现。 HashMap 1.7基于哈希表实现,1.8基于数组+链表+红黑树。 HashTable 和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高(1.7 ConcurrentHashMap 引入了分段锁, 1.8 引入了红黑树)。 LinkedHashMap 使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。 # JDK7 HashMap如何实现? 哈希表有两种实现方式,一种开放地址方式(Open addressing),另一种是冲突链表方式(Separate chaining with linked lists)。Java7 HashMap采用的是冲突链表方式。  从上图容易看出,如果选择合适的哈希函数,put()和get()方法可以在常数时间内完成。但在对HashMap进行迭代时,需要遍历整个table以及后面跟的冲突链表。因此对于迭代比较频繁的场景,不宜将HashMap的初始大小设的过大。 有两个参数可以影响HashMap的性能: 初始容量(inital capacity)和负载系数(load factor)。初始容量指定了初始table的大小,负载系数用来指定自动扩容的临界值。当entry的数量超过capacity*load_factor时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。 # JDK8 HashMap如何实现? 根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。 为了降低这部分的开销,在 Java8 中,当链表中的元素达到了 8 个时,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。  # HashSet是如何实现的? HashSet是对HashMap的简单包装,对HashSet的函数调用都会转换成合适的HashMap方法  # 什么是WeakHashMap? 我们都知道Java中内存是通过GC自动管理的,GC会在程序运行过程中自动判断哪些对象是可以被回收的,并在合适的时机进行内存释放。GC判断某个对象是否可被回收的依据是,是否有有效的引用指向该对象。如果没有有效引用指向该对象(基本意味着不存在访问该对象的方式),那么该对象就是可回收的。这里的有效引用 并不包括弱引用。也就是说,虽然弱引用可以用来访问对象,但进行垃圾回收时弱引用并不会被考虑在内,仅有弱引用指向的对象仍然会被GC回收。 WeakHashMap 内部是通过弱引用来管理entry的,弱引用的特性对应到 WeakHashMap 上意味着什么呢? WeakHashMap 里的entry可能会被GC自动删除,即使程序员没有调用remove()或者clear()方法。 WeakHashMap 的这个特点特别适用于需要缓存的场景。在缓存场景下,由于内存是有限的,不能缓存所有对象;对象缓存命中可以提高系统效率,但缓存MISS也不会造成错误,因为可以通过计算重新得到。
TreeMap
HashMap
HashTable
LinkedHashMap
JAVA并发
理论基础
# 带着BAT大厂的面试问题去理解 多线程的出现是要解决什么问题的? 线程不安全是指什么? 举例说明 并发出现线程不安全的本质什么? 可见性,原子性和有序性。 Java是怎么解决并发问题的? 3个关键字,JMM和8个Happens-Before 线程安全是不是非真即假? 不是 线程安全有哪些实现思路? 如何理解并发和并行的区别? # 为什么需要多线程 众所周知,CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为: CPU 增加了缓存,以均衡与内存的速度差异;// 导致 可见性问题 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;// 导致 原子性问题 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。// 导致 有序性问题 # 线程不安全示例 如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。 以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值有可能小于 1000。  
并发关键字
JUC全局观
JUC原子类
JUC锁
JUC集合类
JUC线程池
JUC工具类
JAVA IO
基础IO
5种IO模型
零拷贝
JVM调优
类加载机制
内存结构
GC垃圾回收
问题排查
Java新版本
Java8 特性
Java9 特性
数据结构与算法
数据结构基础
算法思想
常见排序算法
大数据处理算法
加密算法
数据库
原理和SQL
# 什么是事务?事务基本特性ACID? 事务指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。  事务基本特性ACID?: A原子性(atomicity) 指的是一个事务中的操作要么全部成功,要么全部失败。 C一致性(consistency) 指的是数据库总是从一个一致性的状态转换到另外一个一致性的状态。比如A转账给B100块钱,假设中间sql执行过程中系统崩溃A也不会损失100块,因为事务没有提交,修改也就不会保存到数据库。 I隔离性(isolation) 指的是一个事务的修改在最终提交前,对其他事务是不可见的。 D持久性(durability) 指的是一旦事务提交,所做的修改就会永久保存到数据库中。 # 数据库中并发一致性问题? 在并发环境下,事务的隔离性很难保证,因此会出现很多并发一致性问题。 丢失修改 T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。  读脏数据 T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。  不可重复读 T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。  幻影读 T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。  # 事务的隔离等级? 未提交读(READ UNCOMMITTED) 事务中的修改,即使没有提交,对其它事务也是可见的。 提交读(READ COMMITTED) 一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。 可重复读(REPEATABLE READ) 保证在同一个事务中多次读取同样数据的结果是一样的。 可串行化(SERIALIZABLE) 强制事务串行执行。  # ACID靠什么保证的呢? A原子性(atomicity) 由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql C一致性(consistency) 一般由代码层面来保证 I隔离性(isolation) 由MVCC来保证 D持久性(durability) 由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,事务提交的时候通过redo log刷盘,宕机的时候可以从redo log恢复 # SQL 优化的实践经验? 1.对查询进行优化,要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。 2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:  最好不要给数据库留NULL,尽可能的使用 NOT NULL填充数据库. 备注、描述、评论之类的可以设置为 NULL,其他的,最好不要使用NULL。 不要以为 NULL 不需要空间,比如:char(100) 型,在字段建立时,空间就固定了, 不管是否插入值(NULL也包含在内),都是占用 100个字符的空间的,如果是varchar这样的变长字段, null 不占用空间。 可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:  3.应尽量避免在 where 子句中使用 != 或 <> 操作符,否则将引擎放弃使用索引而进行全表扫描。 4.应尽量避免在 where 子句中使用 or 来连接条件,如果一个字段有索引,一个字段没有索引,将导致引擎放弃使用索引而进行全表扫描,如:  可以这样查询:  5.in 和 not in 也要慎用,否则会导致全表扫描,如:  对于连续的数值,能用 between 就不要用 in 了:  很多时候用 exists 代替 in 是一个好的选择:  用下面的语句替换:  6.下面的查询也将导致全表扫描:  若要提高效率,可以考虑全文检索。 7.如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然 而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:  可以改为强制查询使用索引:  8.应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:  应改为:  9.应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:  应改为:  10.不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。 11.在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。 12.不要写一些没有意义的查询,如需要生成一个空表结构:  这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:  13.Update 语句,如果只更改1、2个字段,不要Update全部字段,否则频繁调用会引起明显的性能消耗,同时带来大量日志。 14.对于多张大数据量(这里几百条就算大了)的表JOIN,要先分页再JOIN,否则逻辑读会很高,性能很差。 15.select count(*) from table;这样不带任何条件的count会引起全表扫描,并且没有任何业务意义,是一定要杜绝的。 16.索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有 必要。 17. 应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。 18. 尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连 接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。 19. 尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。 20. 任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。 21. 尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。 22. 避免频繁创建和删除临时表,以减少系统表资源的消耗。临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件, 最好使用导出表。 23. 在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。 24.如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。 25.尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。 26.使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。 27.与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时 间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。 28.在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。无需在执行存储过程和触发器的每个语句后向客户端发送 DONE_IN_PROC 消息。 29.尽量避免大事务操作,提高系统并发能力。 30.尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。 # Buffer Pool、Redo Log Buffer 和undo log、redo log、bin log 概念以及关系? Buffer Pool 是 MySQL 的一个非常重要的组件,因为针对数据库的增删改操作都是在 Buffer Pool 中完成的 Undo log 记录的是数据操作前的样子 redo log 记录的是数据被操作后的样子(redo log 是 Innodb 存储引擎特有) bin log 记录的是整个操作记录(这个对于主从复制具有非常重要的意义) # 从准备更新一条数据到事务的提交的流程描述?  首先执行器根据 MySQL 的执行计划来查询数据,先是从缓存池中查询数据,如果没有就会去数据库中查询,如果查询到了就将其放到缓存池中 在数据被缓存到缓存池的同时,会写入 undo log 日志文件 更新的动作是在 BufferPool 中完成的,同时会将更新后的数据添加到 redo log buffer 中 完成以后就可以提交事务,在提交的同时会做以下三件事 将redo log buffer中的数据刷入到 redo log 文件中 将本次操作记录写入到 bin log文件中将 bin log 文件名字和更新内容在 bin log 中的位置记录到redo log中,同时在 redo log 最后添加 commit 标记
MySQL
# 能说下myisam 和 innodb的区别吗? myisam引擎是5.1版本之前的默认引擎,支持全文检索、压缩、空间函数等,但是不支持事务和行级锁,所以一般用于有大量查询少量插入的场景来使用,而且myisam不支持外键,并且索引和数据是分开存储的。 innodb是基于B+Tree索引建立的,和myisam相反它支持事务、外键,并且通过MVCC来支持高并发,索引和数据存储在一起。 # 说下MySQL的索引有哪些吧? 索引在什么层面? 首先,索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。 有哪些? B+Tree 索引 哈希索引能以 O(1) 时间进行查找,但是失去了有序性; nnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。 哈希索引 哈希索引能以 O(1) 时间进行查找,但是失去了有序性; InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。 全文索引 MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。查找条件使用 MATCH AGAINST,而不是普通的 WHERE。 全文索引一般使用倒排索引实现,它记录着关键词到其所在文档的映射。 InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。 空间数据索引 MyISAM 存储引擎支持空间数据索引(R-Tree),可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。 # 什么是B+树?为什么B+树成为主要的SQL数据库的索引实现? 什么是B+Tree? B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询的性能。在 B+ Tree 中,一个节点中的 key 从左到右非递减排列,如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。  为什么是B+Tree? 为了减少磁盘读取次数,决定了树的高度不能高,所以必须是先B-Tree; 以页为单位读取使得一次 I/O 就能完全载入一个节点,且相邻的节点也能够被预先载入;所以数据放在叶子节点,本质上是一个Page页; 为了支持范围查询以及关联关系, 页中数据需要有序,且页的尾部节点指向下个页的头部; B+树索引可分为聚簇索引和非聚簇索引? 主索引就是聚簇索引(也称聚集索引,clustered index) 辅助索引(有时也称非聚簇索引或二级索引,secondary index,non-clustered index)。  如上图,主键索引的叶子节点保存的是真正的数据。而辅助索引叶子节点的数据区保存的是主键索引关键字的值。 假如要查询name = C 的数据,其搜索过程如下:a) 先在辅助索引中通过C查询最后找到主键id = 9; b) 在主键索引中搜索id为9的数据,最终在主键索引的叶子节点中获取到真正的数据。所以通过辅助索引进行检索,需要检索两次索引。 之所以这样设计,一个原因就是:如果和MyISAM一样在主键索引和辅助索引的叶子节点中都存放数据行指针,一旦数据发生迁移,则需要去重新组织维护所有的索引。 # 那你知道什么是覆盖索引和回表吗? 覆盖索引指的是在一次查询中,如果一个索引包含或者说覆盖所有需要查询的字段的值,我们就称之为覆盖索引,而不再需要回表查询。 而要确定一个查询是否是覆盖索引,我们只需要explain sql语句看Extra的结果是否是“Using index”即可。 比如:  # 什么是MVCC? 说说MySQL实现MVCC的原理? 什么是MVCC? MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。 在Mysql的InnoDB引擎中就是指在已提交读(READ COMMITTD)和可重复读(REPEATABLE READ)这两种隔离级别下的事务对于SELECT操作会访问版本链中的记录的过程。 这就使得别的事务可以修改这条记录,反正每次修改都会在版本链中记录。SELECT可以去版本链中拿记录,这就实现了读-写,写-读的并发执行,提升了系统的性能。 MySQL的InnoDB引擎实现MVCC的3个基础点 1.隐式字段  如上图,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键;DB_TRX_ID是当前操作该记录的事务ID; 而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本;delete flag没有展示出来。 2.undo log  从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录 3.ReadView 已提交读和可重复读的区别就在于它们生成ReadView的策略不同。 ReadView中主要就是有个列表来存储我们系统中当前活跃着的读写事务,也就是begin了还未提交的事务。通过这个列表来判断记录的某个版本是否对当前事务可见。假设当前列表里的事务id为[80,100]。 如果你要访问的记录版本的事务id为50,比当前列表最小的id80小,那说明这个事务在之前就提交了,所以对当前活动的事务来说是可访问的。 如果你要访问的记录版本的事务id为90,发现此事务在列表id最大值和最小值之间,那就再判断一下是否在列表内,如果在那就说明此事务还未提交,所以版本不能被访问。如果不在那说明事务已经提交,所以版本可以被访问。 如果你要访问的记录版本的事务id为110,那比事务列表最大id100都大,那说明这个版本是在ReadView生成之后才发生的,所以不能被访问。 这些记录都是去undo log 链里面找的,先找最近记录,如果最近这一条记录事务id不符合条件,不可见的话,再去找上一个版本再比较当前事务的id和这个版本事务id看能不能访问,以此类推直到返回可见的版本或者结束。 举个例子,在已提交读隔离级别下: 比如此时有一个事务id为100的事务,修改了name,使得的name等于小明2,但是事务还没提交。则此时的版本链是  那此时另一个事务发起了select 语句要查询id为1的记录,那此时生成的ReadView 列表只有[100]。那就去版本链去找了,首先肯定找最近的一条,发现trx_id是100,也就是name为小明2的那条记录,发现在列表内,所以不能访问。 这时候就通过指针继续找下一条,name为小明1的记录,发现trx_id是60,小于列表中的最小id,所以可以访问,直接访问结果为小明1。 那这时候我们把事务id为100的事务提交了,并且新建了一个事务id为110也修改id为1的记录,并且不提交事务  这时候版本链就是  这时候之前那个select事务又执行了一次查询,要查询id为1的记录。 已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。 如果你是已提交读隔离级别,这时候你会重新一个ReadView,那你的活动事务列表中的值就变了,变成了[110]。按照上的说法,你去版本链通过trx_id对比查找到合适的结果就是小明2。 如果你是可重复读隔离级别,这时候你的ReadView还是第一次select时候生成的ReadView,也就是列表的值还是[100]。所以select的结果是小明1。所以第二次select结果和第一次一样,所以叫可重复读! 这就是Mysql的MVCC,通过版本链,实现多版本,可并发读-写,写-读。通过ReadView生成策略的不同实现不同的隔离级别。 # MySQL 锁的类型有哪些呢? 说两个维度: 共享锁(简称S锁)和排他锁(简称X锁) 读锁是共享的,可以通过lock in share mode实现,这时候只能读不能写。 写锁是排他的,它会阻塞其他的写锁和读锁。从颗粒度来区分,可以分为表锁和行锁两种。 表锁和行锁 表锁会锁定整张表并且阻塞其他用户对该表的所有读写操作,比如alter修改表结构的时候会锁表。 行锁又可以分为乐观锁和悲观锁 悲观锁可以通过for update实现 乐观锁则通过版本号实现。 两个维度结合来看: 共享锁(行锁):Shared Locks 读锁(s锁),多个事务对于同一数据可以共享访问,不能操作修改 使用方法: 加锁:SELECT * FROM table WHERE id=1 LOCK IN SHARE MODE 释锁:COMMIT/ROLLBACK 排他锁(行锁):Exclusive Locks 写锁(X锁),互斥锁/独占锁,事务获取了一个数据的X锁,其他事务就不能再获取该行的读锁和写锁(S锁、X锁),只有获取了该排他锁的事务是可以对数据行进行读取和修改 使用方法: DELETE/ UPDATE/ INSERT -- 加锁 SELECT * FROM table WHERE ... FOR UPDATE -- 加锁 COMMIT/ROLLBACK -- 释锁 意向共享锁(IS) 一个数据行加共享锁前必须先取得该表的IS锁,意向共享锁之间是可以相互兼容的 意向排它锁(IX) 一个数据行加排他锁前必须先取得该表的IX锁,意向排它锁之间是可以相互兼容的 意向锁(IS、IX)是InnoDB引擎操作数据之前自动加的,不需要用户干预; 意义: 当事务操作需要锁表时,只需判断意向锁是否存在,存在时则可快速返回该表不能启用表锁 意向共享锁(IS锁)(表锁):Intention Shared Locks 表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁 前必须先取得该表的IS锁。 意向排它锁(IX锁)(表锁):Intention Exclusive Locks 表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他 锁前必须先取得该表的IX锁。 # 你们数据量级多大?分库分表怎么做的? 首先分库分表分为垂直和水平两个方式,一般来说我们拆分的顺序是先垂直后水平。 垂直分库 基于现在微服务拆分来说,都是已经做到了垂直分库了 垂直分表 垂直切分是将一张表按列切分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。 在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库、用户数据库等。  水平分表 首先根据业务场景来决定使用什么字段作为分表字段(sharding_key),比如我们现在日订单1000万,我们大部分的场景来源于C端,我们可以用user_id作为sharding_key,数据查询支持到最近3个月的订单,超过3个月的做归档处理,那么3个月的数据量就是9亿,可以分1024张表,那么每张表的数据大概就在100万左右。 比如用户id为100,那我们都经过hash(100),然后对1024取模,就可以落到对应的表上了。  # 那分表后的ID怎么保证唯一性的呢? 因为我们主键默认都是自增的,那么分表之后的主键在不同表就肯定会有冲突了。有几个办法考虑: 设定步长,比如1-1024张表我们分别设定1-1024的基础步长,这样主键落到不同的表就不会冲突了。 分布式ID,自己实现一套分布式ID生成算法或者使用开源的比如雪花算法这种 分表后不使用主键作为查询依据,而是每张表单独新增一个字段作为唯一主键使用,比如订单表订单号是唯一的,不管最终落在哪张表都基于订单号作为查询依据,更新也一样。 # 分表后非sharding_key的查询怎么处理呢? 可以做一个mapping表,比如这时候商家要查询订单列表怎么办呢?不带user_id查询的话你总不能扫全表吧?所以我们可以做一个映射关系表,保存商家和用户的关系,查询的时候先通过商家查询到用户列表,再通过user_id去查询。 大宽表,一般而言,商户端对数据实时性要求并不是很高,比如查询订单列表,可以把订单表同步到离线(实时)数仓,再基于数仓去做成一张宽表,再基于其他如es提供查询服务。 数据量不是很大的话,比如后台的一些查询之类的,也可以通过多线程扫表,然后再聚合结果的方式来做。或者异步的形式也是可以的。  # MySQL主从复制? 主要涉及三个线程: binlog 线程、I/O 线程和 SQL 线程。 binlog 线程 : 负责将主服务器上的数据更改写入二进制日志中。 I/O 线程 : 负责从主服务器上读取二进制日志,并写入从服务器的中继日志中。 SQL 线程 : 负责读取中继日志并重放其中的 SQL 语句。  全同步复制 主库写入binlog后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响。 半同步复制 和全同步不同的是,半同步复制的逻辑是这样,从库写入日志成功后返回ACK确认给主库,主库收到至少一个从库的确认就认为写操作完成。 # MySQL主从的延迟怎么解决呢? 这个问题貌似真的是个无解的问题,只能是说自己来判断了,需要走主库的强制走主库查询。 # MySQL读写分离方案? 主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。 读写分离能提高性能的原因在于: 主从服务器负责各自的读和写,极大程度缓解了锁的争用; 从服务器可以使用 MyISAM,提升查询性能以及节约系统开销; 增加冗余,提高可用性。 读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。 
Redis
# 什么是Redis,为什么用Redis? Redis是一种支持key-value等多种数据结构的存储系统。可用于缓存,事件发布或订阅,高速队列等场景。支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。 读写性能优异 Redis能读的速度是110000次/s,写的速度是81000次/s (测试条件见下一节)。 数据类型丰富 Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。 原子性 Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。 丰富的特性 Redis支持 publish/subscribe, 通知, key 过期等特性。 持久化 Redis支持RDB, AOF等持久化方式 发布订阅 Redis支持发布/订阅模式 分布式 Redis Cluster # 为什么Redis 是单线程的以及为什么这么快? redis完全基于内存,绝大部分请求是纯粹的内存操作,非常快速. 数据结构简单,对数据操作也简单,redis中的数据结构是专门进行设计的 采用单线程模型, 避免了不必要的上下文切换和竞争条件, 也不存在多线程或者多线程切换而消耗CPU, 不用考虑各种锁的问题, 不存在加锁, 释放锁的操作, 没有因为可能出现死锁而导致性能消耗 使用底层模型不同,它们之间底层实现方式及与客户端之间的 通信的应用协议不一样,Redis直接构建了自己的VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求 使用了多路IO复用模型,非阻塞IO # Redis 一般有哪些使用场景? 可以结合自己的项目讲讲,比如 热点数据的缓存 缓存是Redis最常见的应用场景,之所有这么使用,主要是因为Redis读写性能优异。而且逐渐有取代memcached,成为首选服务端缓存的组件。而且,Redis内部是支持事务的,在使用时候能有效保证数据的一致性。 限时业务的运用 redis中可以使用expire命令设置一个键的生存时间,到时间后redis会删除它。利用这一特性可以运用在限时的优惠活动信息、手机验证码等业务场景。 计数器相关问题 redis由于incrby命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动、分布式序列号的生成、具体业务还体现在比如限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等。 分布式锁 这个主要利用redis的setnx命令进行,setnx:"set if not exists"就是如果不存在则成功设置缓存同时返回1,否则返回0 ,这个特性在俞你奔远方的后台中有所运用,因为我们服务器是集群的,定时任务可能在两台机器上都会运行,所以在定时任务中首先 通过setnx设置一个lock,如果成功设置则执行,如果没有成功设置,则表明该定时任务已执行。 当然结合具体业务,我们可以给这个lock加一个过期时间,比如说30分钟执行一次的定时任务,那么这个过期时间设置为小于30分钟的一个时间就可以,这个与定时任务的周期以及定时任务执行消耗时间相关。 在分布式锁的场景中,主要用在比如秒杀系统等。 # Redis 有哪些数据类型? 5种基础数据类型,分别是:String、List、Set、Zset、Hash。   三种特殊的数据类型 分别是 HyperLogLogs(基数统计), Bitmaps (位图) 和 geospatial (地理位置) # 谈谈Redis 的对象机制(redisObject)? 比如说, 集合类型就可以由字典和整数集合两种不同的数据结构实现, 但是, 当用户执行 ZADD 命令时, 他/她应该不必关心集合使用的是什么编码, 只要 Redis 能按照 ZADD 命令的指示, 将新元素添加到集合就可以了。 这说明, 操作数据类型的命令除了要对键的类型进行检查之外, 还需要根据数据类型的不同编码进行多态处理. 为了解决以上问题, Redis 构建了自己的类型系统, 这个系统的主要功能包括: redisObject 对象. 基于 redisObject 对象的类型检查. 基于 redisObject 对象的显式多态函数. 对 redisObject 进行分配、共享和销毁的机制.  下图对应上面的结构  # Redis 数据类型有哪些底层数据结构?  简单动态字符串 - sds 压缩列表 - ZipList 快表 - QuickList 字典/哈希表 - Dict 整数集 - IntSet 跳表 - ZSkipList # 为什么要设计sds? 常数复杂度获取字符串长度 由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。而对于 C 语言,获取字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。通过 strlen key 命令可以获取 key 的字符串长度。 杜绝缓冲区溢出 我们知道在 C 语言中使用 strcat 函数来进行两个字符串的拼接,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。而对于 SDS 数据类型,在进行字符修改的时候,会首先根据记录的 len 属性检查内存空间是否满足需求,如果不满足,会进行相应的空间扩展,然后在进行修改操作,所以不会出现缓冲区溢出。 减少修改字符串的内存重新分配次数 C语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。 而对于SDS,由于len属性和alloc属性的存在,对于修改字符串SDS实现了空间预分配和惰性空间释放两种策略: 空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。 惰性空间释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 alloc 属性将这些字节的数量记录下来,等待后续使用。(当然SDS也提供了相应的API,当我们有需要时,也可以手动释放这些未使用的空间。) 二进制安全 因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而所有 SDS 的API 都是以处理二进制的方式来处理 buf 里面的元素,并且 SDS 不是以空字符串来判断是否结束,而是以 len 属性表示的长度来判断字符串是否结束。 兼容部分 C 字符串函数 虽然 SDS 是二进制安全的,但是一样遵从每个字符串都是以空字符串结尾的惯例,这样可以重用 C 语言库<string.h> 中的一部分函数。 # Redis 一个字符串类型的值能存储最大容量是多少? 512M # 为什么会设计Stream? 用过Redis做消息队列的都了解,基于Reids的消息队列实现有很多种,例如: PUB/SUB,订阅/发布模式 但是发布订阅模式是无法持久化的,如果出现网络断开、Redis 宕机等,消息就会被丢弃; 基于List LPUSH+BRPOP 或者 基于Sorted-Set的实现 支持了持久化,但是不支持多播,分组消费等 消费组消费图  # Redis Stream用在什么样场景? 可用作时通信等,大数据分析,异地数据备份等  客户端可以平滑扩展,提高处理能力  # Redis Stream消息ID的设计是否考虑了时间回拨的问题? XADD生成的1553439850328-0,就是Redis生成的消息ID,由两部分组成:时间戳-序号。时间戳是毫秒级单位,是生成消息的Redis服务器时间,它是个64位整型(int64)。序号是在这个毫秒时间点内的消息序号,它也是个64位整型。 可以通过multi批处理,来验证序号的递增:  由于一个redis命令的执行很快,所以可以看到在同一时间戳内,是通过序号递增来表示消息的。 为了保证消息是有序的,因此Redis生成的ID是单调递增有序的。由于ID中包含时间戳部分,为了避免服务器时间错误而带来的问题(例如服务器时间延后了),Redis的每个Stream类型数据都维护一个latest_generated_id属性,用于记录最后一个消息的ID。若发现当前时间戳退后(小于latest_generated_id所记录的),则采用时间戳不变而序号递增的方案来作为新消息ID(这也是序号为什么使用int64的原因,保证有足够多的的序号),从而保证ID的单调递增性质。 强烈建议使用Redis的方案生成消息ID,因为这种时间戳+序号的单调递增的ID方案,几乎可以满足你全部的需求。但同时,记住ID是支持自定义的,别忘了! # Redis Stream消费者崩溃带来的会不会消息丢失问题? 为了解决组内消息读取但处理期间消费者崩溃带来的消息丢失问题,STREAM 设计了 Pending 列表,用于记录读取但并未处理完毕的消息。命令XPENDIING 用来获消费组或消费内消费者的未处理完毕的消息。演示如下:  每个Pending的消息有4个属性: 消息ID 所属消费者 IDLE,已读取时长 delivery counter,消息被读取次数 上面的结果我们可以看到,我们之前读取的消息,都被记录在Pending列表中,说明全部读到的消息都没有处理,仅仅是读取了。那如何表示消费者处理完毕了消息呢?使用命令 XACK 完成告知消息处理完成,演示如下:  有了这样一个Pending机制,就意味着在某个消费者读取消息但未处理后,消息是不会丢失的。等待消费者再次上线后,可以读取该Pending列表,就可以继续处理该消息了,保证消息的有序和不丢失。 # Redis Steam 坏消息问题,死信问题? 正如上面所说,如果某个消息,不能被消费者处理,也就是不能被XACK,这是要长时间处于Pending列表中,即使被反复的转移给各个消费者也是如此。此时该消息的delivery counter就会累加(上一节的例子可以看到),当累加到某个我们预设的临界值时,我们就认为是坏消息(也叫死信,DeadLetter,无法投递的消息),由于有了判定条件,我们将坏消息处理掉即可,删除即可。删除一个消息,使用XDEL语法,演示如下:  注意本例中,并没有删除Pending中的消息因此你查看Pending,消息还会在。可以执行XACK标识其处理完毕! # Redis 的持久化机制是什么?各自的优缺点?一般怎么用? RDB持久化是把当前进程数据生成快照保存到磁盘上的过程; 针对RDB不适合实时持久化的问题,Redis提供了AOF持久化方式来解决. AOF是“写后”日志,Redis先执行命令,把数据写入内存,然后才记录日志。日志里记录的是Redis收到的每一条命令,这些命令是以文本形式保存。 Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。 这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。 如下图所示,T1 和 T2 时刻的修改,用 AOF 日志记录,等到第二次做全量快照时,就可以清空 AOF 日志,因为此时的修改都已经记录到快照中了,恢复时就不再用日志了。  这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势, 实际环境中用的很多。 # RDB 触发方式? 触发rdb持久化的方式有2种,分别是手动触发和自动触发。 手动触发 save命令:阻塞当前Redis服务器,直到RDB过程完成为止,对于内存 比较大的实例会造成长时间阻塞,线上环境不建议使用 bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子 进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短 bgsave流程图如下所示  自动触发 redis.conf中配置save m n,即在m秒内有n次修改时,自动触发bgsave生成rdb文件; 主从复制时,从节点要从主节点进行全量复制时也会触发bgsave操作,生成当时的快照发送到从节点; 执行debug reload命令重新加载redis时也会触发bgsave操作; 默认情况下执行shutdown命令时,如果没有开启aof持久化,那么也会触发bgsave操作; # RDB由于生产环境中我们为Redis开辟的内存区域都比较大(例如6GB),那么将内存中的数据同步到硬盘的过程可能就会持续比较长的时间,而实际情况是这段时间Redis服务一般都会收到数据写操作请求。那么如何保证数据一致性呢? RDB中的核心思路是Copy-on-Write,来保证在进行快照操作的这段时间,需要压缩写入磁盘上的数据在内存中不会发生变化。在正常的快照操作中,一方面Redis主进程会fork一个新的快照进程专门来做这个事情,这样保证了Redis服务不会停止对客户端包括写请求在内的任何响应。另一方面这段时间发生的数据变化会以副本的方式存放在另一个新的内存区域,待快照操作结束后才会同步到原来的内存区域。 举个例子:如果主线程对这些数据也都是读操作(例如图中的键值对 A),那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据(例如图中的键值对 C),那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。  # 在进行RDB快照操作的这段时间,如果发生服务崩溃怎么办? 很简单,在没有将数据全部写入到磁盘前,这次快照操作都不算成功。如果出现了服务崩溃的情况,将以上一次完整的RDB快照文件作为恢复内存数据的参考。也就是说,在快照操作过程中不能影响上一次的备份数据。Redis服务会在磁盘上创建一个临时文件进行数据操作,待操作成功后才会用这个临时文件替换掉上一次的备份 # 可以每秒做一次RDB快照吗? 对于快照来说,所谓“连拍”就是指连续地做快照。这样一来,快照的间隔时间变得很短,即使某一时刻发生宕机了,因为上一时刻快照刚执行,丢失的数据也不会太多。但是,这其中的快照间隔时间就很关键了。 如下图所示,我们先在 T0 时刻做了一次快照,然后又在 T0+t 时刻做了一次快照,在这期间,数据块 5 和 9 被修改了。如果在 t 这段时间内,机器宕机了,那么,只能按照 T0 时刻的快照进行恢复。此时,数据块 5 和 9 的修改值因为没有快照记录,就无法恢复了。  所以,要想尽可能恢复数据,t 值就要尽可能小,t 越小,就越像“连拍”。那么,t 值可以小到什么程度呢,比如说是不是可以每秒做一次快照?毕竟,每次快照都是由 bgsave 子进程在后台执行,也不会阻塞主线程。 这种想法其实是错误的。虽然 bgsave 执行时不阻塞主线程,但是,如果频繁地执行全量快照,也会带来两方面的开销: 一方面,频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。另一方面,bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork 这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主线程了。 那么,有什么其他好方法吗?此时,我们可以做增量快照,就是指做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。这个比较好理解。 但是它需要我们使用额外的元数据信息去记录哪些数据被修改了,这会带来额外的空间开销问题。那么,还有什么方法既能利用 RDB 的快速恢复,又能以较小的开销做到尽量少丢数据呢?RDB和AOF的混合方式。 # AOF是写前日志还是写后日志? AOF日志采用写后日志,即先写内存,后写日志。  为什么采用写后日志? Redis要求高性能,采用写日志有两方面好处: 避免额外的检查开销:Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。 不会阻塞当前的写操作 但这种方式存在潜在风险: 如果命令执行完成,写日志之前宕机了,会丢失数据。 主线程写磁盘压力大,导致写盘慢,阻塞后续操作。 # 如何实现AOF的? AOF日志记录Redis的每个写命令,步骤分为:命令追加(append)、文件写入(write)和文件同步(sync)。 命令追加 当AOF持久化功能打开了,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器的 aof_buf 缓冲区。 文件写入和同步 关于何时将 aof_buf 缓冲区的内容写入AOF文件中,Redis提供了三种写回策略:  Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘; Everysec,每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘; No,操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。 三种写回策略的优缺点 上面的三种写回策略体现了一个重要原则:trade-off,取舍,指在性能和可靠性保证之间做取舍。 关于AOF的同步策略是涉及到操作系统的 write 函数和 fsync 函数的,在《Redis设计与实现》中是这样说明的: 为了提高文件写入效率,在现代操作系统中,当用户调用write函数,将一些数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区的空间被填满或超过了指定时限后,才真正将缓冲区的数据写入到磁盘里。 这样的操作虽然提高了效率,但也为数据写入带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失。为此,系统提供了fsync、fdatasync同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘里,从而确保写入数据的安全性。 # 什么是AOF重写? Redis通过创建一个新的AOF文件来替换现有的AOF,新旧两个AOF文件保存的数据相同,但新AOF文件没有了冗余命令。  # AOF重写会阻塞吗? AOF重写过程是由后台进程bgrewriteaof来完成的。主线程fork出后台的bgrewriteaof子进程,fork会把主线程的内存拷贝一份给bgrewriteaof子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。 所以aof在重写时,在fork进程时是会阻塞住主线程的。 # AOF日志何时会重写? 有两个配置项控制AOF重写的触发: auto-aof-rewrite-min-size:表示运行AOF重写时文件的最小大小,默认为64MB。 auto-aof-rewrite-percentage:这个值的计算方式是,当前aof文件大小和上一次重写后aof文件大小的差值,再除以上一次重写后aof文件大小。也就是当前aof文件比上一次重写后aof文件的增量大小,和上一次重写后aof文件大小的比值。 # AOF重写日志时,有新数据写入咋整? 重写过程总结为:“一个拷贝,两处日志”。在fork出子进程时的拷贝,以及在重写时,如果有新数据写入,主线程就会将命令记录到两个aof日志内存缓冲区中。如果AOF写回策略配置的是always,则直接将命令写回旧的日志文件,并且保存一份命令至AOF重写缓冲区,这些操作对新的日志文件是不存在影响的。(旧的日志文件:主线程使用的日志文件,新的日志文件:bgrewriteaof进程使用的日志文件) 而在bgrewriteaof子进程完成会日志文件的重写操作后,会提示主线程已经完成重写操作,主线程会将AOF重写缓冲中的命令追加到新的日志文件后面。这时候在高并发的情况下,AOF重写缓冲区积累可能会很大,这样就会造成阻塞,Redis后来通过Linux管道技术让aof重写期间就能同时进行回放,这样aof重写结束后只需回放少量剩余的数据即可。 最后通过修改文件名的方式,保证文件切换的原子性。 在AOF重写日志期间发生宕机的话,因为日志文件还没切换,所以恢复数据时,用的还是旧的日志文件。 # 主线程fork出子进程的是如何复制内存数据的? fork采用操作系统提供的写时复制(copy on write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成阻塞。fork子进程时,子进程时会拷贝父进程的页表,即虚实映射关系(虚拟内存和物理内存的映射索引表),而不会拷贝物理内存。这个拷贝会消耗大量cpu资源,并且拷贝完成前会阻塞主线程,阻塞时间取决于内存中的数据量,数据量越大,则内存页表越大。拷贝完成后,父子进程使用相同的内存地址空间。 但主进程是可以有数据写入的,这时候就会拷贝物理内存中的数据。如下图(进程1看做是主进程,进程2看做是子进程):  在主进程有数据写入时,而这个数据刚好在页c中,操作系统会创建这个页面的副本(页c的副本),即拷贝当前页的物理数据,将其映射到主进程中,而子进程还是使用原来的的页c. # 在重写日志整个过程时,主线程有哪些地方会被阻塞? fork子进程时,需要拷贝虚拟页表,会对主线程阻塞。 主进程有bigkey写入时,操作系统会创建页面的副本,并拷贝原有的数据,会对主线程阻塞。 子进程重写日志完成后,主进程追加aof重写缓冲区时可能会对主线程阻塞 # 为什么AOF重写不复用原AOF日志? 两方面原因: 父子进程写同一个文件会产生竞争问题,影响父进程的性能。 如果AOF重写过程中失败了,相当于污染了原本的AOF文件,无法做恢复数据使用。 # Redis 过期键的删除策略有哪些? 在单机版Redis中,存在两种删除策略: 惰性删除:服务器不会主动删除数据,只有当客户端查询某个数据时,服务器判断该数据是否过期,如果过期则删除。 定期删除:服务器执行定时任务删除过期数据,但是考虑到内存和CPU的折中(删除会释放内存,但是频繁的删除操作对CPU不友好),该删除的频率和执行时间都受到了限制。 在主从复制场景下,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除。由于主节点的惰性删除和定期删除策略,都不能保证主节点及时对过期数据执行删除操作,因此,当客户端通过Redis从节点读取数据时,很容易读取到已经过期的数据。 Redis 3.2中,从节点在读取数据时,增加了对数据是否过期的判断:如果该数据已过期,则不返回给客户端;将Redis升级到3.2可以解决数据过期问题。 # Redis 内存淘汰算法有哪些? Redis共支持八种淘汰策略,分别是noeviction、volatile-random、volatile-ttl、volatile-lru、volatile-lfu、allkeys-lru、allkeys-random 和 allkeys-lfu 策略。 怎么理解呢?主要看分三类看: 不淘汰 noeviction (v4.0后默认的) 对设置了过期时间的数据中进行淘汰 随机:volatile-random ttl:volatile-ttl lru:volatile-lru lfu:volatile-lfu 全部数据进行淘汰 随机:allkeys-random lru:allkeys-lru lfu:allkeys-lfu LRU算法:LRU 算法的全称是 Least Recently Used,按照最近最少使用的原则来筛选数据。这种模式下会使用 LRU 算法筛选设置了过期时间的键值对。 Redis优化的LRU算法实现: Redis会记录每个数据的最近一次被访问的时间戳。在Redis在决定淘汰的数据时,第一次会随机选出 N 个数据,把它们作为一个候选集合。接下来,Redis 会比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据从缓存中淘汰出去。通过随机读取待删除集合,可以让Redis不用维护一个巨大的链表,也不用操作链表,进而提升性能 LFU 算法:LFU 缓存策略是在 LRU 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用 LFU 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。 Redis的LFU算法实现: 当 LFU 策略筛选数据时,Redis 会在候选集合中,根据数据 lru 字段的后 8bit 选择访问次数最少的数据进行淘汰。当访问次数相同时,再根据 lru 字段的前 16bit 值大小,选择访问时间最久远的数据进行淘汰。 Redis 只使用了 8bit 记录数据的访问次数,而 8bit 记录的最大值是 255,这样在访问快速的情况下,如果每次被访问就将访问次数加一,很快某条数据就达到最大值255,可能很多数据都是255,那么退化成LRU算法了。所以Redis为了解决这个问题,实现了一个更优的计数规则,并可以通过配置项,来控制计数器增加的速度。 # Redis的内存用完了会发生什么? 如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容 # Redis如何做内存优化? 1.: 缩减键(key)和值(value)的长度, key长度:如在设计键时,在完整描述业务情况下,键值越短越好。 value长度:值对象缩减比较复杂,常见需求是把业务对象序列化成二进制数组放入Redis。首先应该在业务上精简业务对象,去掉不必要的属性避免存储无效数据。其次在序列化工具选择上,应该选择更高效的序列化工具来降低字节数组大小。以JAVA为例,内置的序列化方式无论从速度还是压缩比都不尽如人意,这时可以选择更高效的序列化工具,如: protostuff,kryo等,下图是JAVA常见序列化工具空间压缩对比。 2.共享对象池 对象共享池指Redis内部维护[0-9999]的整数对象池。创建大量的整数类型redisObject存在内存开销,每个redisObject内部结构至少占16字节,甚至超过了整数自身空间消耗。所以Redis内存维护一个[0-9999]的整数对象池,用于节约内存。 除了整数值对象,其他类型如list,hash,set,zset内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。 3.字符串优化 4.编码优化 5.控制key的数量 # Redis key 的过期时间和永久有效分别怎么设置? EXPIRE 和 PERSIST 命令 # Redis 中的管道有什么用? 一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应,这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。 这就是管道(pipelining),是一种几十年来广泛使用的技术。例如许多 POP3 协议已经实现支持这个功能,大大加快了从服务器下载新邮件的过程。 # 什么是redis事务? Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。 总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
MongoDB
ElasticSearch
数据库
常见数据库
SQL DB
MySQL
# 能说下myisam 和 innodb的区别吗? myisam引擎是5.1版本之前的默认引擎,支持全文检索、压缩、空间函数等,但是不支持事务和行级锁,所以一般用于有大量查询少量插入的场景来使用,而且myisam不支持外键,并且索引和数据是分开存储的。 innodb是基于B+Tree索引建立的,和myisam相反它支持事务、外键,并且通过MVCC来支持高并发,索引和数据存储在一起。 # 说下MySQL的索引有哪些吧? 索引在什么层面? 首先,索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。 有哪些? B+Tree 索引 哈希索引能以 O(1) 时间进行查找,但是失去了有序性; InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。 哈希索引 哈希索引能以 O(1) 时间进行查找,但是失去了有序性; InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。 全文索引 MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。查找条件使用 MATCH AGAINST,而不是普通的 WHERE。 全文索引一般使用倒排索引实现,它记录着关键词到其所在文档的映射。 InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。 空间数据索引 MyISAM 存储引擎支持空间数据索引(R-Tree),可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。 # 什么是B+树?为什么B+树成为主要的SQL数据库的索引实现? 什么是B+Tree? B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询的性能。在 B+ Tree 中,一个节点中的 key 从左到右非递减排列,如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。  为什么是B+Tree? 为了减少磁盘读取次数,决定了树的高度不能高,所以必须是先B-Tree; 以页为单位读取使得一次 I/O 就能完全载入一个节点,且相邻的节点也能够被预先载入;所以数据放在叶子节点,本质上是一个Page页; 为了支持范围查询以及关联关系, 页中数据需要有序,且页的尾部节点指向下个页的头部; B+树索引可分为聚簇索引和非聚簇索引? 主索引就是聚簇索引(也称聚集索引,clustered index) 辅助索引(有时也称非聚簇索引或二级索引,secondary index,non-clustered index)。  如上图,主键索引的叶子节点保存的是真正的数据。而辅助索引叶子节点的数据区保存的是主键索引关键字的值。 假如要查询name = C 的数据,其搜索过程如下:a) 先在辅助索引中通过C查询最后找到主键id = 9; b) 在主键索引中搜索id为9的数据,最终在主键索引的叶子节点中获取到真正的数据。所以通过辅助索引进行检索,需要检索两次索引。 之所以这样设计,一个原因就是:如果和MyISAM一样在主键索引和辅助索引的叶子节点中都存放数据行指针,一旦数据发生迁移,则需要去重新组织维护所有的索引。 # 那你知道什么是覆盖索引和回表吗? 覆盖索引指的是在一次查询中,如果一个索引包含或者说覆盖所有需要查询的字段的值,我们就称之为覆盖索引,而不再需要回表查询。 而要确定一个查询是否是覆盖索引,我们只需要explain sql语句看Extra的结果是否是“Using index”即可。 比如:  # 什么是MVCC? 说说MySQL实现MVCC的原理? 什么是MVCC? MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。 在Mysql的InnoDB引擎中就是指在已提交读(READ COMMITTD)和可重复读(REPEATABLE READ)这两种隔离级别下的事务对于SELECT操作会访问版本链中的记录的过程。 这就使得别的事务可以修改这条记录,反正每次修改都会在版本链中记录。SELECT可以去版本链中拿记录,这就实现了读-写,写-读的并发执行,提升了系统的性能。 MySQL的InnoDB引擎实现MVCC的3个基础点 1.隐式字段  如上图,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键;DB_TRX_ID是当前操作该记录的事务ID; 而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本;delete flag没有展示出来。 2.undo log  从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录 3.ReadView 已提交读和可重复读的区别就在于它们生成ReadView的策略不同。 ReadView中主要就是有个列表来存储我们系统中当前活跃着的读写事务,也就是begin了还未提交的事务。通过这个列表来判断记录的某个版本是否对当前事务可见。假设当前列表里的事务id为[80,100]。 如果你要访问的记录版本的事务id为50,比当前列表最小的id80小,那说明这个事务在之前就提交了,所以对当前活动的事务来说是可访问的。 如果你要访问的记录版本的事务id为90,发现此事务在列表id最大值和最小值之间,那就再判断一下是否在列表内,如果在那就说明此事务还未提交,所以版本不能被访问。如果不在那说明事务已经提交,所以版本可以被访问。 如果你要访问的记录版本的事务id为110,那比事务列表最大id100都大,那说明这个版本是在ReadView生成之后才发生的,所以不能被访问。 这些记录都是去undo log 链里面找的,先找最近记录,如果最近这一条记录事务id不符合条件,不可见的话,再去找上一个版本再比较当前事务的id和这个版本事务id看能不能访问,以此类推直到返回可见的版本或者结束。 举个例子,在已提交读隔离级别下: 比如此时有一个事务id为100的事务,修改了name,使得的name等于小明2,但是事务还没提交。则此时的版本链是  那此时另一个事务发起了select 语句要查询id为1的记录,那此时生成的ReadView 列表只有[100]。那就去版本链去找了,首先肯定找最近的一条,发现trx_id是100,也就是name为小明2的那条记录,发现在列表内,所以不能访问。 这时候就通过指针继续找下一条,name为小明1的记录,发现trx_id是60,小于列表中的最小id,所以可以访问,直接访问结果为小明1。 那这时候我们把事务id为100的事务提交了,并且新建了一个事务id为110也修改id为1的记录,并且不提交事务  这时候版本链就是  这时候之前那个select事务又执行了一次查询,要查询id为1的记录。 已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。 如果你是已提交读隔离级别,这时候你会重新一个ReadView,那你的活动事务列表中的值就变了,变成了[110]。按照上的说法,你去版本链通过trx_id对比查找到合适的结果就是小明2。 如果你是可重复读隔离级别,这时候你的ReadView还是第一次select时候生成的ReadView,也就是列表的值还是[100]。所以select的结果是小明1。所以第二次select结果和第一次一样,所以叫可重复读! 这就是Mysql的MVCC,通过版本链,实现多版本,可并发读-写,写-读。通过ReadView生成策略的不同实现不同的隔离级别。 # MySQL 锁的类型有哪些呢? 说两个维度: 共享锁(简称S锁)和排他锁(简称X锁) 读锁是共享的,可以通过lock in share mode实现,这时候只能读不能写。 写锁是排他的,它会阻塞其他的写锁和读锁。从颗粒度来区分,可以分为表锁和行锁两种。 表锁和行锁 表锁会锁定整张表并且阻塞其他用户对该表的所有读写操作,比如alter修改表结构的时候会锁表。 行锁又可以分为乐观锁和悲观锁 悲观锁可以通过for update实现 乐观锁则通过版本号实现。 两个维度结合来看: 共享锁(行锁):Shared Locks 读锁(s锁),多个事务对于同一数据可以共享访问,不能操作修改 使用方法: 加锁:SELECT * FROM table WHERE id=1 LOCK IN SHARE MODE 释锁:COMMIT/ROLLBACK 排他锁(行锁):Exclusive Locks 写锁(X锁),互斥锁/独占锁,事务获取了一个数据的X锁,其他事务就不能再获取该行的读锁和写锁(S锁、X锁),只有获取了该排他锁的事务是可以对数据行进行读取和修改 使用方法: DELETE/ UPDATE/ INSERT -- 加锁 SELECT * FROM table WHERE ... FOR UPDATE -- 加锁 COMMIT/ROLLBACK -- 释锁 意向共享锁(IS) 一个数据行加共享锁前必须先取得该表的IS锁,意向共享锁之间是可以相互兼容的 意向排它锁(IX) 一个数据行加排他锁前必须先取得该表的IX锁,意向排它锁之间是可以相互兼容的 意向锁(IS、IX)是InnoDB引擎操作数据之前自动加的,不需要用户干预; 意义: 当事务操作需要锁表时,只需判断意向锁是否存在,存在时则可快速返回该表不能启用表锁 意向共享锁(IS锁)(表锁):Intention Shared Locks 表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁 前必须先取得该表的IS锁。 意向排它锁(IX锁)(表锁):Intention Exclusive Locks 表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他 锁前必须先取得该表的IX锁。 # 你们数据量级多大?分库分表怎么做的? 首先分库分表分为垂直和水平两个方式,一般来说我们拆分的顺序是先垂直后水平。 垂直分库 基于现在微服务拆分来说,都是已经做到了垂直分库了 垂直分表 垂直切分是将一张表按列切分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。 在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库、用户数据库等。  水平分表 首先根据业务场景来决定使用什么字段作为分表字段(sharding_key),比如我们现在日订单1000万,我们大部分的场景来源于C端,我们可以用user_id作为sharding_key,数据查询支持到最近3个月的订单,超过3个月的做归档处理,那么3个月的数据量就是9亿,可以分1024张表,那么每张表的数据大概就在100万左右。 比如用户id为100,那我们都经过hash(100),然后对1024取模,就可以落到对应的表上了。  # 那分表后的ID怎么保证唯一性的呢? 因为我们主键默认都是自增的,那么分表之后的主键在不同表就肯定会有冲突了。有几个办法考虑: 设定步长,比如1-1024张表我们分别设定1-1024的基础步长,这样主键落到不同的表就不会冲突了。 分布式ID,自己实现一套分布式ID生成算法或者使用开源的比如雪花算法这种 分表后不使用主键作为查询依据,而是每张表单独新增一个字段作为唯一主键使用,比如订单表订单号是唯一的,不管最终落在哪张表都基于订单号作为查询依据,更新也一样。 # 分表后非sharding_key的查询怎么处理呢? 可以做一个mapping表,比如这时候商家要查询订单列表怎么办呢?不带user_id查询的话你总不能扫全表吧?所以我们可以做一个映射关系表,保存商家和用户的关系,查询的时候先通过商家查询到用户列表,再通过user_id去查询。 大宽表,一般而言,商户端对数据实时性要求并不是很高,比如查询订单列表,可以把订单表同步到离线(实时)数仓,再基于数仓去做成一张宽表,再基于其他如es提供查询服务。 数据量不是很大的话,比如后台的一些查询之类的,也可以通过多线程扫表,然后再聚合结果的方式来做。或者异步的形式也是可以的。  # MySQL主从复制? 主要涉及三个线程: binlog 线程、I/O 线程和 SQL 线程。 binlog 线程 : 负责将主服务器上的数据更改写入二进制日志中。 I/O 线程 : 负责从主服务器上读取二进制日志,并写入从服务器的中继日志中。 SQL 线程 : 负责读取中继日志并重放其中的 SQL 语句。  全同步复制 主库写入binlog后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响。 半同步复制 和全同步不同的是,半同步复制的逻辑是这样,从库写入日志成功后返回ACK确认给主库,主库收到至少一个从库的确认就认为写操作完成。 # MySQL主从的延迟怎么解决呢? 这个问题貌似真的是个无解的问题,只能是说自己来判断了,需要走主库的强制走主库查询。 # MySQL读写分离方案? 主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。 读写分离能提高性能的原因在于: 主从服务器负责各自的读和写,极大程度缓解了锁的争用; 从服务器可以使用 MyISAM,提升查询性能以及节约系统开销; 增加冗余,提高可用性。 读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。 
数据类型
存储引擎
InnoDB
MyISAM
索引
B+树索引
# 什么是B+树?为什么B+树成为主要的SQL数据库的索引实现? 什么是B+Tree? B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询的性能。在 B+ Tree 中,一个节点中的 key 从左到右非递减排列,如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。  为什么是B+Tree? 为了减少磁盘读取次数,决定了树的高度不能高,所以必须是先B-Tree; 以页为单位读取使得一次 I/O 就能完全载入一个节点,且相邻的节点也能够被预先载入;所以数据放在叶子节点,本质上是一个Page页; 为了支持范围查询以及关联关系, 页中数据需要有序,且页的尾部节点指向下个页的头部; B+树索引可分为聚簇索引和非聚簇索引? 主索引就是聚簇索引(也称聚集索引,clustered index) 辅助索引(有时也称非聚簇索引或二级索引,secondary index,non-clustered index)。  如上图,主键索引的叶子节点保存的是真正的数据。而辅助索引叶子节点的数据区保存的是主键索引关键字的值。 假如要查询name = C 的数据,其搜索过程如下:a) 先在辅助索引中通过C查询最后找到主键id = 9; b) 在主键索引中搜索id为9的数据,最终在主键索引的叶子节点中获取到真正的数据。所以通过辅助索引进行检索,需要检索两次索引。 之所以这样设计,一个原因就是:如果和MyISAM一样在主键索引和辅助索引的叶子节点中都存放数据行指针,一旦数据发生迁移,则需要去重新组织维护所有的索引。 # 那你知道什么是覆盖索引和回表吗? 覆盖索引指的是在一次查询中,如果一个索引包含或者说覆盖所有需要查询的字段的值,我们就称之为覆盖索引,而不再需要回表查询。 而要确定一个查询是否是覆盖索引,我们只需要explain sql语句看Extra的结果是否是“Using index”即可。 比如: 
哈希索引
全文索引
空间数据索引
事务
MVCC
性能优化
分库分表
读写分离主从复制
PostgreSQL
NoSQL DB
Redis
# 什么是Redis,为什么用Redis? Redis是一种支持key-value等多种数据结构的存储系统。可用于缓存,事件发布或订阅,高速队列等场景。支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。 读写性能优异 Redis能读的速度是110000次/s,写的速度是81000次/s (测试条件见下一节)。 数据类型丰富 Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。 原子性 Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。 丰富的特性 Redis支持 publish/subscribe, 通知, key 过期等特性。 持久化 Redis支持RDB, AOF等持久化方式 发布订阅 Redis支持发布/订阅模式 分布式 Redis Cluster # 为什么Redis 是单线程的以及为什么这么快? redis完全基于内存,绝大部分请求是纯粹的内存操作,非常快速. 数据结构简单,对数据操作也简单,redis中的数据结构是专门进行设计的 采用单线程模型, 避免了不必要的上下文切换和竞争条件, 也不存在多线程或者多线程切换而消耗CPU, 不用考虑各种锁的问题, 不存在加锁, 释放锁的操作, 没有因为可能出现死锁而导致性能消耗 使用底层模型不同,它们之间底层实现方式及与客户端之间的 通信的应用协议不一样,Redis直接构建了自己的VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求 使用了多路IO复用模型,非阻塞IO # Redis 一般有哪些使用场景? 可以结合自己的项目讲讲,比如 热点数据的缓存 缓存是Redis最常见的应用场景,之所有这么使用,主要是因为Redis读写性能优异。而且逐渐有取代memcached,成为首选服务端缓存的组件。而且,Redis内部是支持事务的,在使用时候能有效保证数据的一致性。 限时业务的运用 redis中可以使用expire命令设置一个键的生存时间,到时间后redis会删除它。利用这一特性可以运用在限时的优惠活动信息、手机验证码等业务场景。 计数器相关问题 redis由于incrby命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动、分布式序列号的生成、具体业务还体现在比如限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等。 分布式锁 这个主要利用redis的setnx命令进行,setnx:"set if not exists"就是如果不存在则成功设置缓存同时返回1,否则返回0 ,这个特性在俞你奔远方的后台中有所运用,因为我们服务器是集群的,定时任务可能在两台机器上都会运行,所以在定时任务中首先 通过setnx设置一个lock,如果成功设置则执行,如果没有成功设置,则表明该定时任务已执行。 当然结合具体业务,我们可以给这个lock加一个过期时间,比如说30分钟执行一次的定时任务,那么这个过期时间设置为小于30分钟的一个时间就可以,这个与定时任务的周期以及定时任务执行消耗时间相关。 在分布式锁的场景中,主要用在比如秒杀系统等。 # Redis 有哪些数据类型? 5种基础数据类型,分别是:String、List、Set、Zset、Hash。   三种特殊的数据类型 分别是 HyperLogLogs(基数统计), Bitmaps (位图) 和 geospatial (地理位置) # 谈谈Redis 的对象机制(redisObject)? 比如说, 集合类型就可以由字典和整数集合两种不同的数据结构实现, 但是, 当用户执行 ZADD 命令时, 他/她应该不必关心集合使用的是什么编码, 只要 Redis 能按照 ZADD 命令的指示, 将新元素添加到集合就可以了。 这说明, 操作数据类型的命令除了要对键的类型进行检查之外, 还需要根据数据类型的不同编码进行多态处理. 为了解决以上问题, Redis 构建了自己的类型系统, 这个系统的主要功能包括: redisObject 对象. 基于 redisObject 对象的类型检查. 基于 redisObject 对象的显式多态函数. 对 redisObject 进行分配、共享和销毁的机制.  下图对应上面的结构  # Redis 数据类型有哪些底层数据结构?  简单动态字符串 - sds 压缩列表 - ZipList 快表 - QuickList 字典/哈希表 - Dict 整数集 - IntSet 跳表 - ZSkipList # 为什么要设计sds? 常数复杂度获取字符串长度 由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。而对于 C 语言,获取字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。通过 strlen key 命令可以获取 key 的字符串长度。 杜绝缓冲区溢出 我们知道在 C 语言中使用 strcat 函数来进行两个字符串的拼接,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。而对于 SDS 数据类型,在进行字符修改的时候,会首先根据记录的 len 属性检查内存空间是否满足需求,如果不满足,会进行相应的空间扩展,然后在进行修改操作,所以不会出现缓冲区溢出。 减少修改字符串的内存重新分配次数 C语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。 而对于SDS,由于len属性和alloc属性的存在,对于修改字符串SDS实现了空间预分配和惰性空间释放两种策略: 空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。 惰性空间释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 alloc 属性将这些字节的数量记录下来,等待后续使用。(当然SDS也提供了相应的API,当我们有需要时,也可以手动释放这些未使用的空间。) 二进制安全 因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而所有 SDS 的API 都是以处理二进制的方式来处理 buf 里面的元素,并且 SDS 不是以空字符串来判断是否结束,而是以 len 属性表示的长度来判断字符串是否结束。 兼容部分 C 字符串函数 虽然 SDS 是二进制安全的,但是一样遵从每个字符串都是以空字符串结尾的惯例,这样可以重用 C 语言库<string.h> 中的一部分函数。 # Redis 一个字符串类型的值能存储最大容量是多少? 512M # 为什么会设计Stream? 用过Redis做消息队列的都了解,基于Reids的消息队列实现有很多种,例如: PUB/SUB,订阅/发布模式 但是发布订阅模式是无法持久化的,如果出现网络断开、Redis 宕机等,消息就会被丢弃; 基于List LPUSH+BRPOP 或者 基于Sorted-Set的实现 支持了持久化,但是不支持多播,分组消费等 消费组消费图  # Redis Stream用在什么样场景? 可用作时通信等,大数据分析,异地数据备份等  客户端可以平滑扩展,提高处理能力  # Redis Stream消息ID的设计是否考虑了时间回拨的问题? XADD生成的1553439850328-0,就是Redis生成的消息ID,由两部分组成:时间戳-序号。时间戳是毫秒级单位,是生成消息的Redis服务器时间,它是个64位整型(int64)。序号是在这个毫秒时间点内的消息序号,它也是个64位整型。 可以通过multi批处理,来验证序号的递增:  由于一个redis命令的执行很快,所以可以看到在同一时间戳内,是通过序号递增来表示消息的。 为了保证消息是有序的,因此Redis生成的ID是单调递增有序的。由于ID中包含时间戳部分,为了避免服务器时间错误而带来的问题(例如服务器时间延后了),Redis的每个Stream类型数据都维护一个latest_generated_id属性,用于记录最后一个消息的ID。若发现当前时间戳退后(小于latest_generated_id所记录的),则采用时间戳不变而序号递增的方案来作为新消息ID(这也是序号为什么使用int64的原因,保证有足够多的的序号),从而保证ID的单调递增性质。 强烈建议使用Redis的方案生成消息ID,因为这种时间戳+序号的单调递增的ID方案,几乎可以满足你全部的需求。但同时,记住ID是支持自定义的,别忘了! # Redis Stream消费者崩溃带来的会不会消息丢失问题? 为了解决组内消息读取但处理期间消费者崩溃带来的消息丢失问题,STREAM 设计了 Pending 列表,用于记录读取但并未处理完毕的消息。命令XPENDIING 用来获消费组或消费内消费者的未处理完毕的消息。演示如下:  每个Pending的消息有4个属性: 消息ID 所属消费者 IDLE,已读取时长 delivery counter,消息被读取次数 上面的结果我们可以看到,我们之前读取的消息,都被记录在Pending列表中,说明全部读到的消息都没有处理,仅仅是读取了。那如何表示消费者处理完毕了消息呢?使用命令 XACK 完成告知消息处理完成,演示如下:  有了这样一个Pending机制,就意味着在某个消费者读取消息但未处理后,消息是不会丢失的。等待消费者再次上线后,可以读取该Pending列表,就可以继续处理该消息了,保证消息的有序和不丢失。 # Redis Steam 坏消息问题,死信问题? 正如上面所说,如果某个消息,不能被消费者处理,也就是不能被XACK,这是要长时间处于Pending列表中,即使被反复的转移给各个消费者也是如此。此时该消息的delivery counter就会累加(上一节的例子可以看到),当累加到某个我们预设的临界值时,我们就认为是坏消息(也叫死信,DeadLetter,无法投递的消息),由于有了判定条件,我们将坏消息处理掉即可,删除即可。删除一个消息,使用XDEL语法,演示如下:  注意本例中,并没有删除Pending中的消息因此你查看Pending,消息还会在。可以执行XACK标识其处理完毕! # Redis 的持久化机制是什么?各自的优缺点?一般怎么用? RDB持久化是把当前进程数据生成快照保存到磁盘上的过程; 针对RDB不适合实时持久化的问题,Redis提供了AOF持久化方式来解决. AOF是“写后”日志,Redis先执行命令,把数据写入内存,然后才记录日志。日志里记录的是Redis收到的每一条命令,这些命令是以文本形式保存。 Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。 这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。 如下图所示,T1 和 T2 时刻的修改,用 AOF 日志记录,等到第二次做全量快照时,就可以清空 AOF 日志,因为此时的修改都已经记录到快照中了,恢复时就不再用日志了。  这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势, 实际环境中用的很多。 # RDB 触发方式? 触发rdb持久化的方式有2种,分别是手动触发和自动触发。 手动触发 save命令:阻塞当前Redis服务器,直到RDB过程完成为止,对于内存 比较大的实例会造成长时间阻塞,线上环境不建议使用 bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子 进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短 bgsave流程图如下所示  自动触发 redis.conf中配置save m n,即在m秒内有n次修改时,自动触发bgsave生成rdb文件; 主从复制时,从节点要从主节点进行全量复制时也会触发bgsave操作,生成当时的快照发送到从节点; 执行debug reload命令重新加载redis时也会触发bgsave操作; 默认情况下执行shutdown命令时,如果没有开启aof持久化,那么也会触发bgsave操作; # RDB由于生产环境中我们为Redis开辟的内存区域都比较大(例如6GB),那么将内存中的数据同步到硬盘的过程可能就会持续比较长的时间,而实际情况是这段时间Redis服务一般都会收到数据写操作请求。那么如何保证数据一致性呢? RDB中的核心思路是Copy-on-Write,来保证在进行快照操作的这段时间,需要压缩写入磁盘上的数据在内存中不会发生变化。在正常的快照操作中,一方面Redis主进程会fork一个新的快照进程专门来做这个事情,这样保证了Redis服务不会停止对客户端包括写请求在内的任何响应。另一方面这段时间发生的数据变化会以副本的方式存放在另一个新的内存区域,待快照操作结束后才会同步到原来的内存区域。 举个例子:如果主线程对这些数据也都是读操作(例如图中的键值对 A),那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据(例如图中的键值对 C),那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。  # 在进行RDB快照操作的这段时间,如果发生服务崩溃怎么办? 很简单,在没有将数据全部写入到磁盘前,这次快照操作都不算成功。如果出现了服务崩溃的情况,将以上一次完整的RDB快照文件作为恢复内存数据的参考。也就是说,在快照操作过程中不能影响上一次的备份数据。Redis服务会在磁盘上创建一个临时文件进行数据操作,待操作成功后才会用这个临时文件替换掉上一次的备份 # 可以每秒做一次RDB快照吗? 对于快照来说,所谓“连拍”就是指连续地做快照。这样一来,快照的间隔时间变得很短,即使某一时刻发生宕机了,因为上一时刻快照刚执行,丢失的数据也不会太多。但是,这其中的快照间隔时间就很关键了。 如下图所示,我们先在 T0 时刻做了一次快照,然后又在 T0+t 时刻做了一次快照,在这期间,数据块 5 和 9 被修改了。如果在 t 这段时间内,机器宕机了,那么,只能按照 T0 时刻的快照进行恢复。此时,数据块 5 和 9 的修改值因为没有快照记录,就无法恢复了。  所以,要想尽可能恢复数据,t 值就要尽可能小,t 越小,就越像“连拍”。那么,t 值可以小到什么程度呢,比如说是不是可以每秒做一次快照?毕竟,每次快照都是由 bgsave 子进程在后台执行,也不会阻塞主线程。 这种想法其实是错误的。虽然 bgsave 执行时不阻塞主线程,但是,如果频繁地执行全量快照,也会带来两方面的开销: 一方面,频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。另一方面,bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork 这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主线程了。 那么,有什么其他好方法吗?此时,我们可以做增量快照,就是指做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。这个比较好理解。 但是它需要我们使用额外的元数据信息去记录哪些数据被修改了,这会带来额外的空间开销问题。那么,还有什么方法既能利用 RDB 的快速恢复,又能以较小的开销做到尽量少丢数据呢?RDB和AOF的混合方式。 # AOF是写前日志还是写后日志? AOF日志采用写后日志,即先写内存,后写日志。  为什么采用写后日志? Redis要求高性能,采用写日志有两方面好处: 避免额外的检查开销:Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。 不会阻塞当前的写操作 但这种方式存在潜在风险: 如果命令执行完成,写日志之前宕机了,会丢失数据。 主线程写磁盘压力大,导致写盘慢,阻塞后续操作。 # 如何实现AOF的? AOF日志记录Redis的每个写命令,步骤分为:命令追加(append)、文件写入(write)和文件同步(sync)。 命令追加 当AOF持久化功能打开了,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器的 aof_buf 缓冲区。 文件写入和同步 关于何时将 aof_buf 缓冲区的内容写入AOF文件中,Redis提供了三种写回策略:  Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘; Everysec,每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘; No,操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。 三种写回策略的优缺点 上面的三种写回策略体现了一个重要原则:trade-off,取舍,指在性能和可靠性保证之间做取舍。 关于AOF的同步策略是涉及到操作系统的 write 函数和 fsync 函数的,在《Redis设计与实现》中是这样说明的: 为了提高文件写入效率,在现代操作系统中,当用户调用write函数,将一些数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区的空间被填满或超过了指定时限后,才真正将缓冲区的数据写入到磁盘里。 这样的操作虽然提高了效率,但也为数据写入带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失。为此,系统提供了fsync、fdatasync同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘里,从而确保写入数据的安全性。 # 什么是AOF重写? Redis通过创建一个新的AOF文件来替换现有的AOF,新旧两个AOF文件保存的数据相同,但新AOF文件没有了冗余命令。  # AOF重写会阻塞吗? AOF重写过程是由后台进程bgrewriteaof来完成的。主线程fork出后台的bgrewriteaof子进程,fork会把主线程的内存拷贝一份给bgrewriteaof子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。 所以aof在重写时,在fork进程时是会阻塞住主线程的。 # AOF日志何时会重写? 有两个配置项控制AOF重写的触发: auto-aof-rewrite-min-size:表示运行AOF重写时文件的最小大小,默认为64MB。 auto-aof-rewrite-percentage:这个值的计算方式是,当前aof文件大小和上一次重写后aof文件大小的差值,再除以上一次重写后aof文件大小。也就是当前aof文件比上一次重写后aof文件的增量大小,和上一次重写后aof文件大小的比值。 # AOF重写日志时,有新数据写入咋整? 重写过程总结为:“一个拷贝,两处日志”。在fork出子进程时的拷贝,以及在重写时,如果有新数据写入,主线程就会将命令记录到两个aof日志内存缓冲区中。如果AOF写回策略配置的是always,则直接将命令写回旧的日志文件,并且保存一份命令至AOF重写缓冲区,这些操作对新的日志文件是不存在影响的。(旧的日志文件:主线程使用的日志文件,新的日志文件:bgrewriteaof进程使用的日志文件) 而在bgrewriteaof子进程完成会日志文件的重写操作后,会提示主线程已经完成重写操作,主线程会将AOF重写缓冲中的命令追加到新的日志文件后面。这时候在高并发的情况下,AOF重写缓冲区积累可能会很大,这样就会造成阻塞,Redis后来通过Linux管道技术让aof重写期间就能同时进行回放,这样aof重写结束后只需回放少量剩余的数据即可。 最后通过修改文件名的方式,保证文件切换的原子性。 在AOF重写日志期间发生宕机的话,因为日志文件还没切换,所以恢复数据时,用的还是旧的日志文件。 # 主线程fork出子进程的是如何复制内存数据的? fork采用操作系统提供的写时复制(copy on write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成阻塞。fork子进程时,子进程时会拷贝父进程的页表,即虚实映射关系(虚拟内存和物理内存的映射索引表),而不会拷贝物理内存。这个拷贝会消耗大量cpu资源,并且拷贝完成前会阻塞主线程,阻塞时间取决于内存中的数据量,数据量越大,则内存页表越大。拷贝完成后,父子进程使用相同的内存地址空间。 但主进程是可以有数据写入的,这时候就会拷贝物理内存中的数据。如下图(进程1看做是主进程,进程2看做是子进程):  在主进程有数据写入时,而这个数据刚好在页c中,操作系统会创建这个页面的副本(页c的副本),即拷贝当前页的物理数据,将其映射到主进程中,而子进程还是使用原来的的页c. # 在重写日志整个过程时,主线程有哪些地方会被阻塞? fork子进程时,需要拷贝虚拟页表,会对主线程阻塞。 主进程有bigkey写入时,操作系统会创建页面的副本,并拷贝原有的数据,会对主线程阻塞。 子进程重写日志完成后,主进程追加aof重写缓冲区时可能会对主线程阻塞 # 为什么AOF重写不复用原AOF日志? 两方面原因: 父子进程写同一个文件会产生竞争问题,影响父进程的性能。 如果AOF重写过程中失败了,相当于污染了原本的AOF文件,无法做恢复数据使用。 # Redis 过期键的删除策略有哪些? 在单机版Redis中,存在两种删除策略: 惰性删除:服务器不会主动删除数据,只有当客户端查询某个数据时,服务器判断该数据是否过期,如果过期则删除。 定期删除:服务器执行定时任务删除过期数据,但是考虑到内存和CPU的折中(删除会释放内存,但是频繁的删除操作对CPU不友好),该删除的频率和执行时间都受到了限制。 在主从复制场景下,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除。由于主节点的惰性删除和定期删除策略,都不能保证主节点及时对过期数据执行删除操作,因此,当客户端通过Redis从节点读取数据时,很容易读取到已经过期的数据。 Redis 3.2中,从节点在读取数据时,增加了对数据是否过期的判断:如果该数据已过期,则不返回给客户端;将Redis升级到3.2可以解决数据过期问题。 # Redis 内存淘汰算法有哪些? Redis共支持八种淘汰策略,分别是noeviction、volatile-random、volatile-ttl、volatile-lru、volatile-lfu、allkeys-lru、allkeys-random 和 allkeys-lfu 策略。 怎么理解呢?主要看分三类看: 不淘汰 noeviction (v4.0后默认的) 对设置了过期时间的数据中进行淘汰 随机:volatile-random ttl:volatile-ttl lru:volatile-lru lfu:volatile-lfu 全部数据进行淘汰 随机:allkeys-random lru:allkeys-lru lfu:allkeys-lfu LRU算法:LRU 算法的全称是 Least Recently Used,按照最近最少使用的原则来筛选数据。这种模式下会使用 LRU 算法筛选设置了过期时间的键值对。 Redis优化的LRU算法实现: Redis会记录每个数据的最近一次被访问的时间戳。在Redis在决定淘汰的数据时,第一次会随机选出 N 个数据,把它们作为一个候选集合。接下来,Redis 会比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据从缓存中淘汰出去。通过随机读取待删除集合,可以让Redis不用维护一个巨大的链表,也不用操作链表,进而提升性能 LFU 算法:LFU 缓存策略是在 LRU 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用 LFU 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。 Redis的LFU算法实现: 当 LFU 策略筛选数据时,Redis 会在候选集合中,根据数据 lru 字段的后 8bit 选择访问次数最少的数据进行淘汰。当访问次数相同时,再根据 lru 字段的前 16bit 值大小,选择访问时间最久远的数据进行淘汰。 Redis 只使用了 8bit 记录数据的访问次数,而 8bit 记录的最大值是 255,这样在访问快速的情况下,如果每次被访问就将访问次数加一,很快某条数据就达到最大值255,可能很多数据都是255,那么退化成LRU算法了。所以Redis为了解决这个问题,实现了一个更优的计数规则,并可以通过配置项,来控制计数器增加的速度。 # Redis的内存用完了会发生什么? 如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容 # Redis如何做内存优化? 1.: 缩减键(key)和值(value)的长度, key长度:如在设计键时,在完整描述业务情况下,键值越短越好。 value长度:值对象缩减比较复杂,常见需求是把业务对象序列化成二进制数组放入Redis。首先应该在业务上精简业务对象,去掉不必要的属性避免存储无效数据。其次在序列化工具选择上,应该选择更高效的序列化工具来降低字节数组大小。以JAVA为例,内置的序列化方式无论从速度还是压缩比都不尽如人意,这时可以选择更高效的序列化工具,如: protostuff,kryo等,下图是JAVA常见序列化工具空间压缩对比。 2.共享对象池 对象共享池指Redis内部维护[0-9999]的整数对象池。创建大量的整数类型redisObject存在内存开销,每个redisObject内部结构至少占16字节,甚至超过了整数自身空间消耗。所以Redis内存维护一个[0-9999]的整数对象池,用于节约内存。 除了整数值对象,其他类型如list,hash,set,zset内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。 3.字符串优化 4.编码优化 5.控制key的数量 # Redis key 的过期时间和永久有效分别怎么设置? EXPIRE 和 PERSIST 命令 # Redis 中的管道有什么用? 一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应,这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。 这就是管道(pipelining),是一种几十年来广泛使用的技术。例如许多 POP3 协议已经实现支持这个功能,大大加快了从服务器下载新邮件的过程。 # 什么是redis事务? Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。 总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
数据类型
持久化(RDB和AOF机制)
缓存问题
主从复制
哨兵机制
分片机制
运维监控
性能调优
应用场景
ElasticSearch
MongoDB
分布式DB核心
CRUD
索引
事务
复制
分片
理论和基础
数据库是如何工作
数据结构
数据查询的流程
客户端管理器
数据管理器
数据库设计理论
函数依赖
异常
范式
数据库设计流程
需求分析阶段(常用自顶向下)
概念结构设计阶段(常用自底向上)
逻辑结构设计阶段(E-R图)
物理设计阶段
数据库实施阶段
数据库运行与维护阶段
数据库核心知识点
事务ACID
原子性Actomicity
一致性Consistency
隔离性Isolation
持久性Durability
并发一致性问题
封锁
封锁粒度
封锁类型
封锁协议
隔离级别
未提交读READ UNCOMMITTED
提交读READ COMMITTED
可重复读REPEATABLE READ
可串行化SERIALIZABLE
多版本并发控制
SQL语言
语法基础
语法练习
题目进阶
语法优化
开发基础
开发架构和中间件
Spring
# 什么是Spring框架? Spring是一种轻量级框架,旨在提高开发人员的开发效率以及系统的可维护性。 我们一般说的Spring框架就是Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块。比如Core Container中的Core组件是Spring所有组件的核心,Beans组件和Context组件是实现IOC和DI的基础,AOP组件用来实现面向切面编程。 Spring官网列出的Spring的6个特征: 核心技术:依赖注入(DI),AOP,事件(Events),资源,i18n,验证,数据绑定,类型转换,SpEL。 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。 数据访问:事务,DAO支持,JDBC,ORM,编组XML。Web支持:Spring MVC和Spring WebFlux Web框架。 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。 语言:Kotlin,Groovy,动态语言。 # 列举一些重要的Spring模块? 下图对应的是Spring 4.x的版本,目前最新的5.x版本中Web模块的Portlet组件已经被废弃掉,同时增加了用于异步响应式处理的WebFlux组件。 Spring Core:基础,可以说Spring其他所有的功能都依赖于该类库。主要提供IOC和DI功能。 Spring Aspects:该模块为与AspectJ的集成提供支持。 Spring AOP:提供面向切面的编程实现。 Spring JDBC:Java数据库连接。 Spring JMS:Java消息服务。 Spring ORM:用于支持Hibernate等ORM工具。 Spring Web:为创建Web应用程序提供支持。 Spring Test:提供了对JUnit和TestNG测试的支持。 
IOC
# 什么是IOC? 如何实现的? IOC(Inversion Of Controll,控制反转)是一种设计思想,就是将原本在程序中手动创建对象的控制权,交给IOC容器来管理,并由IOC容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。 Spring 中的 IoC 的实现原理就是工厂模式加反射机制。 示例:  # 可以通过多少种方式完成依赖注入? 通常,依赖注入可以通过三种方式完成,即: 构造函数注入 setter 注入 接口注入
AOP
# 什么是AOP? 有哪些AOP的概念? AOP(Aspect-Oriented Programming,面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性。 Spring AOP是基于动态代理的,如果要代理的对象实现了某个接口,那么Spring AOP就会使用JDK动态代理去创建代理对象;而对于没有实现接口的对象,就无法使用JDK动态代理,转 而使用CGlib动态代理生成一个被代理对象的子类来作为代理。  当然也可以使用AspectJ,Spring AOP中已经集成了AspectJ,AspectJ应该算得上是Java生态系统中最完整的AOP框架了。使用AOP之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样可以大大简化代码量。我们需要增加新功能也方便,提高了系统的扩展性。日志功能、事务管理和权限管理等场景都用到了AOP。 AOP包含的几个概念 Jointpoint(连接点):具体的切面点点抽象概念,可以是在字段、方法上,Spring中具体表现形式是PointCut(切入点),仅作用在方法上。 Advice(通知): 在连接点进行的具体操作,如何进行增强处理的,分为前置、后置、异常、最终、环绕五种情况。 目标对象:被AOP框架进行增强处理的对象,也被称为被增强的对象。 AOP代理:AOP框架创建的对象,简单的说,代理就是对目标对象的加强。Spring中的AOP代理可以是JDK动态代理,也可以是CGLIB代理。 Weaving(织入):将增强处理添加到目标对象中,创建一个被增强的对象的过程 总结为一句话就是:在目标对象(target object)的某些方法(jointpoint)添加不同种类的操作(通知、增强操处理),最后通过某些方法(weaving、织入操作)实现一个新的代理目标对象。 # AOP 有哪些应用场景? 举几个例子: 记录日志(调用方法后记录日志) 监控性能(统计方法运行时间) 权限控制(调用方法前校验是否有权限) 事务管理(调用方法前开启事务,调用方法后提交关闭事务 ) 缓存优化(第一次调用查询数据库,将查询结果放入内存对象, 第二次调用,直接从内存对象返回,不需要查询数据库 ) # AOP 有哪些实现方式? 实现 AOP 的技术,主要分为两大类: 静态代理 - 指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强; 编译时编织(特殊编译器实现) 类加载时编织(特殊的类加载器实现)。 动态代理 - 在运行时在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。 JDK 动态代理 JDK Proxy 是 Java 语言自带的功能,无需通过加载第三方类实现; Java 对 JDK Proxy 提供了稳定的支持,并且会持续的升级和更新,Java 8 版本中的 JDK Proxy 性能相比于之前版本提升了很多; JDK Proxy 是通过拦截器加反射的方式实现的; JDK Proxy 只能代理实现接口的类; JDK Proxy 实现和调用起来比较简单; CGLIB CGLib 是第三方提供的工具,基于 ASM 实现的,性能比较高; CGLib 无需通过接口来实现,它是针对类实现代理,主要是对指定的类生成一个子类,它是通过实现子类的方式来完成调用的。
CGLib
bean
事务
注解
Spring Boot
Spirng Security
MyBatic
JPA
日志框架
Tomat
自动化测试
Cucumber
# 测试驱动开发TDD  测试金字塔 UNIT TEST->IT TEST ->SYSTEM TEST TDD主要应用于UNIT TEST(单元测试) 常用的单元测试工具有,JUnit,TestNG,Stub,Mock # 行为驱动开发BDD  Given...When...Then 站在用户的角度进行测试,从而减少验收所需的时间 # 使用Cucumber编写BDD脚本 开发环境  引入依赖包 
开发工具
Git
Maven
架构
分布式
MQ
kafka
# kafka特性 高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个主题可以分多个分区,消费组对 进行分区消息操作; 可扩展性:kafka集群支持热扩展; 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失; 容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败); 高并发:支持数千个客户端同时读写; # 使用场景 日志收集: 一个公司可以用kafka手机各种服务的log,通过kafka统一接口服务的方式开放给各种consumer。例如Hadoop、Hbase、Solr等 消息系统:解耦和生产和消费者、缓存消息等 用户活动跟踪:kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到Hadoop、数据仓库中做离线分析和挖掘 运营指标:kafka也经常用来记录运营监控数据,包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告; 流式处理:比如spark streaming和storm; # 技术优势 可伸缩性:Kafka的两个重要特性造就了它的可伸缩性。 kafka集群在运行期间可以轻松地扩展或收缩(可以添加或删除代理),而不会宕机 可以扩展一个kafka主题来包含更多的分区。由于一个分区无法扩展多个代理,所以它的容量受到代理磁盘空间的限制。能够增加分区和代理的数量意味着单个主题可以存储的数量是没有限制的。 容错性和可靠性:kafka的设计方式方便使某个代理的故障能够被集群中的其他代理检测到。由于每个主题都可以在多个代理上复制,所以集群可以在不中断服务的情况下从此类故障中恢复并继续运行。 吞吐量:代理能够以超快的速度有效地存储和检索数据 # 概念详解  Producer(泼丢蛇)Broker (布洛克) Partition (爬t神) topic (top皮克)Leader (粒的) Follower (父牢尔) Producer 生产者即数据的发布者,该角色将消息发布到Kafka的topic中,broker接收到生产者发送的消息后,broker将该消息追加到当前用于追加数据的segment文件中。生产者发送的消息,存储到一个partition中,生产者也可以指定数据存储的partition Consumer 消费者可以从broker中读取数据。消费者可以消费多个topic中的数据。 Topic 在kafka中,使用一个类别属性来划分数据的所属类,划分数据的这个类称为topic。如果把kafka看做为一个数据库,topic可以理解为数据库中的一张表,topic的名称即为表名 Partition topic中的数据分割为一个或多个partition。每个topic至少有一个partition。每个partition中的数据使用多个segment文件存储。partition中的数据是有序的,partition间的数据丢失了数据的顺序。如果topic有多个partition,消费数据时就不能保住数据的顺序。在需要严格保住消息的消费顺序场景下,需要将partition数目设为1 Partition offset 每条消息都有一个当前Partition下唯一的64字节的offset,它指明了这条消息的起始位置。 Replicas of partition 副本
微服务
DevOps
Linux
虚拟机
centos8部署docker 执行yum install -y yum-utils报错,No match for argument,Unable to find a match  这是两个问题,我们先解决第一个问题: 第一个问题是服务器的语言环境有问题,可以通过下面命令进行设置解决: echo “export LC_ALL=en_US.UTF-8” >> /etc/profile echo “export LC_CTYPE=en_US.UTF-8” >> /etc/profile source /etc/profile 这个时候在通过yum install -y yum-utils device-mapper-persistent-data lvm2 进行安装,第一个问题就不报了  现在我们解决第二个问题: 第二个问题是因为centos8项目官方已于2021年底停止维护,相关源已无法使用,所以网上22年前的换源教程都已无法使用。 进入配置文件目录 #进入配置文件目录 cd /etc/yum.repos.d/ 删除所有的.repo源文件 #删除旧的配置文件 rm *.repo # 对每个文件进行确认:输入“y”回车确认  下载可用的.repo文件 wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-vault-8.5.2111.repo  运行 yum makecache 生成缓存 yum makecache  重新运行 yum install -y yum-utils device-mapper-persistent-data lvm2 命令 安装依赖  成功安装!!!!!
Docker
# Docker及系统版本 Docker从17.03版本之后分为CE(Community Edition: 社区版)和EE(Enterprise Edition: 企业版)。相对于社区版本,企业版本强调安全性,但需付费使用。这里我们使用社区版本即可。 Docker支持64位版本的CentOS 7和CentOS 8及更高版本,它要求Linux内核版本不低于3.10。 # Docker的自动化安装 Docker官方和国内daocloud都提供了一键安装的脚本,使得Docker的安装更加便捷。 官方的一键安装方式:  国内 daocloud一键安装命令:  执行上述任一条命令,耐心等待即可完成Docker的安装。 # Docker启动 启动Docker的命令:  # Docker删除 删除安装包:  删除镜像、容器、配置文件等内容:  # Docker其他常见命令 安装完成Docker之后,这里汇总列一下常见的Docker操作命令: 搜索仓库镜像:docker search 镜像名 拉取镜像:docker pull 镜像名 查看正在运行的容器:docker ps 查看所有容器:docker ps -a 删除容器:docker rm container_id 查看镜像:docker images 删除镜像:docker rmi image_id 启动(停止的)容器:docker start 容器ID 停止容器:docker stop 容器ID 重启容器:docker restart 容器ID 启动(新)容器:docker run -it ubuntu /bin/bash 进入容器:docker attach 容器ID或docker exec -it 容器ID /bin/bash,推荐使用后者。 # 开机自启动 设置开机启动 systemctl enable docker.service 关闭开机启动 systemctl disable docker.service 如果已经启动的项目.则使用update更新: docker update --restart = always 容器id
DockerDesktop
DockerHub
docker commit -m="【描述】" -a="【作者】" 【容器id】 yumik0522/admin:1.0 docker login docker push yumik0522/admin:1.0
Compose
# Compose命令 首次构建 docker compose up 构建后修改 docker compose up -d
Dockerfile
# 将jar包制作成镜像 Dockerfile如下,目录结果data/Dockerfile ,data/yyy.jar FROM kdvolder/jdk8 ADD admin.jar admin.jar EXPOSE 8773 ENTRYPOINT ["java","-jar","data/admin.jar"] 执行以下命令,构建镜像(注意后面有.) docker build -t admin . 通过DockerDesktop,运行镜像获得容器ID,就可以通过docker hub上传镜像
network
通过network建立容器间通信,建立桥接后可以用容器名就等价于ip
CI/CD
监控体系
其他
JAVA基础
面向对象
封装
封装 利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。 优点: 减少耦合: 可以独立地开发、测试、优化、使用、理解和修改 减轻维护的负担: 可以更容易被程序员理解,并且在调试的时候可以不影响其他模块 有效地调节性能: 可以通过剖析确定哪些模块影响了系统的性能 提高软件的可重用性 降低了构建大型系统的风险: 即使整个系统不可用,但是这些独立的模块却有可能是可用的 以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。 注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改 gender 属性使用的数据类型时,也可以在不影响客户端代码的情况下进行。 
继承
继承 继承实现了 IS-A 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal,从而获得 Animal 非 private 的属性和方法。继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 向上转型 。 
多态
多态 多态分为编译时多态和运行时多态: 编译时多态主要指方法的重载 运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定 运行时多态有三个条件: 继承 覆盖(重写) 向上转型 下面的代码中,乐器类(Instrument)有两个子类: Wind 和 Percussion,它们都覆盖了父类的 play() 方法,并且在 main() 方法中使用父类 Instrument 来引用 Wind 和 Percussion 对象。在 Instrument 引用调用 play() 方法时,会执行实际引用对象所在类的 play() 方法,而不是 Instrument 类的方法 
类图
知识点
数据类型
包装类型
缓存池
字符串
String
StringBuffer
StringBuilder
运算
继承
访问权限
抽象类与接口
super
重写与重载
Object 通用方法
equals()
hashCode()
toString()
clone()
关键字
final
static
图谱 & Q/A
# a = a + b 与 a += b 的区别 += 隐式的将加操作的结果类型强制转换为持有结果的类型。如果两个整型相加,如 byte、short 或者 int,首先会将它们提升到 int 类型,然后在执行加法操作。 (因为 a+b 操作会将 a、b 提升为 int 类型,所以将 int 类型赋值给 byte 就会编译出错) # 3*0.1 == 0.3 将会返回什么? true 还是 false? false,因为有些浮点数不能完全精确的表示出来。 # 能在 Switch 中使用 String 吗? 从 Java 7 开始,我们可以在 switch case 中使用字符串,但这仅仅是一个语法糖。内部实现在 switch 中使用字符串的 hash code。 # 对equals()和hashCode()的理解? 为什么在重写 equals 方法的时候需要重写 hashCode 方法? 因为有强制的规范指定需要同时重写 hashcode 与 equals 是方法,许多容器类,如 HashMap、HashSet 都依赖于 hashcode 与 equals 的规定。 有没有可能两个不相等的对象有相同的 hashcode? 有可能,两个不相等的对象可能会有相同的 hashcode 值,这就是为什么在 hashmap 中会有冲突。相等 hashcode 值的规定只是说如果两个对象相等,必须有相同的hashcode 值,但是没有关于不相等对象的任何规定 两个相同的对象会有不同的 hash code 吗? 不能,根据 hash code 的规定,这是不可能的 # final、finalize 和 finally 的不同之处? final 是一个修饰符,可以修饰变量、方法和类。如果 final 修饰变量,意味着该变量的值在初始化后不能被改变。 Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的,但是什么时候调用 finalize 没有保证。 finally 是一个关键字,与 try 和 catch 一起用于异常的处理。finally 块一定会被执行,无论在 try 块中是否有发生异常。 # String、StringBuffer与StringBuilder的区别? 第一点: 可变和适用范围。String对象是不可变的,而StringBuffer和StringBuilder是可变字符序列。每次对String的操作相当于生成一个新的String对象,而对StringBuffer和StringBuilder的操作是对对象本身的操作,而不会生成新的对象,所以对于频繁改变内容的字符串避免使用String,因为频繁的生成对象将会对系统性能产生影响。 第二点: 线程安全。String由于有final修饰,是immutable的,安全性是简单而纯粹的。StringBuilder和StringBuffer的区别在于StringBuilder不保证同步,也就是说如果需要线程安全需要使用StringBuffer,不需要同步的StringBuilder效率更高。 # 接口与抽象类的区别? 一个子类只能继承一个抽象类, 但能实现多个接口 抽象类可以有构造方法, 接口没有构造方法 抽象类可以有普通成员变量, 接口没有普通成员变量 抽象类和接口都可有静态成员变量, 抽象类中静态成员变量访问类型任意,接口只能public static final(默认) 抽象类可以没有抽象方法, 抽象类可以有普通方法;接口在JDK8之前都是抽象方法,在JDK8可以有default方法,在JDK9中允许有私有普通方法 抽象类可以有静态方法;接口在JDK8之前不能有静态方法,在JDK8中可以有静态方法,且只能被接口类直接调用(不能被实现类的对象调用) 抽象类中的方法可以是public、protected; 接口方法在JDK8之前只有public abstract,在JDK8可以有default方法,在JDK9中允许有private方法 # this() & super()在构造方法中的区别? 调用super()必须写在子类构造方法的第一行, 否则编译不通过 super从子类调用父类构造, this在同一类中调用其他构造均需要放在第一行 尽管可以用this调用一个构造器, 却不能调用2个 this和super不能出现在同一个构造器中, 否则编译不通过 this()、super()都指的对象,不可以在static环境中使用 本质this指向本对象的指针。super是一个关键字 # Java移位运算符? java中有三种移位运算符 << :左移运算符,x << 1,相当于x乘以2(不溢出的情况下),低位补0 >> :带符号右移,x >> 1,相当于x除以2,正数高位补0,负数高位补1 >>> :无符号右移,忽略符号位,空位都以0补齐# 1.2 泛型
泛型机制
基本使用
泛型类
泛型接口
泛型方法
泛型的上下限
泛型数组
类型擦除
泛型的多态
Q&A
注解机制
内置注解
元注解
注解与反射接口
深入理解注解
注解的应用场景
异常机制
异常的层次结构
异常基础
异常实践
深入理解异常
反射机制
反射基础
反射的使用
反射机制执行的流程
SPI机制
SPI机制的简单示例
SPI机制的广泛应用
SPI机制深入理解
JAVA集合
Collection
# 知识体系架构  # 介绍 容器,就是可以容纳其他Java对象的对象。*Java Collections Framework(JCF)*为Java开发者提供了通用的容器,其始于JDK 1.2,优点是: 降低编程难度 提高程序性能 提高API间的互操作性 降低学习难度 降低设计和实现相关API的难度 增加程序的重用性 Java容器里只能放对象,对于基本类型(int, long, float, double等),需要将其包装成对象类型后(Integer, Long, Float, Double等)才能放到容器里。很多时候拆包装和解包装能够自动完成。这虽然会导致额外的性能和空间开销,但简化了设计和编程。
Set
TreeSet
HashSet
LinkedHashSet
List
ArrayList
# ArrayList的底层? ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现。除该类未实现同步外,其余跟Vector大致相同。每个ArrayList都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。前面已经提过,Java泛型只是编译器提供的语法糖,所以这里的数组是一个Object数组,以便能够容纳任何类型的对象。 # ArrayList自动扩容? 每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过ensureCapacity(int minCapacity)方法来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。 数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。 # ArrayList的Fail-Fast机制? ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
Vector
LinkedList
Queue
LinkedList
PriorityQueue
Map
# Map有哪些类? TreeMap 基于红黑树实现。 HashMap 1.7基于哈希表实现,1.8基于数组+链表+红黑树。 HashTable 和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高(1.7 ConcurrentHashMap 引入了分段锁, 1.8 引入了红黑树)。 LinkedHashMap 使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。 # JDK7 HashMap如何实现? 哈希表有两种实现方式,一种开放地址方式(Open addressing),另一种是冲突链表方式(Separate chaining with linked lists)。Java7 HashMap采用的是冲突链表方式。  从上图容易看出,如果选择合适的哈希函数,put()和get()方法可以在常数时间内完成。但在对HashMap进行迭代时,需要遍历整个table以及后面跟的冲突链表。因此对于迭代比较频繁的场景,不宜将HashMap的初始大小设的过大。 有两个参数可以影响HashMap的性能: 初始容量(inital capacity)和负载系数(load factor)。初始容量指定了初始table的大小,负载系数用来指定自动扩容的临界值。当entry的数量超过capacity*load_factor时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。 # JDK8 HashMap如何实现? 根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。 为了降低这部分的开销,在 Java8 中,当链表中的元素达到了 8 个时,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。  # HashSet是如何实现的? HashSet是对HashMap的简单包装,对HashSet的函数调用都会转换成合适的HashMap方法  # 什么是WeakHashMap? 我们都知道Java中内存是通过GC自动管理的,GC会在程序运行过程中自动判断哪些对象是可以被回收的,并在合适的时机进行内存释放。GC判断某个对象是否可被回收的依据是,是否有有效的引用指向该对象。如果没有有效引用指向该对象(基本意味着不存在访问该对象的方式),那么该对象就是可回收的。这里的有效引用 并不包括弱引用。也就是说,虽然弱引用可以用来访问对象,但进行垃圾回收时弱引用并不会被考虑在内,仅有弱引用指向的对象仍然会被GC回收。 WeakHashMap 内部是通过弱引用来管理entry的,弱引用的特性对应到 WeakHashMap 上意味着什么呢? WeakHashMap 里的entry可能会被GC自动删除,即使程序员没有调用remove()或者clear()方法。 WeakHashMap 的这个特点特别适用于需要缓存的场景。在缓存场景下,由于内存是有限的,不能缓存所有对象;对象缓存命中可以提高系统效率,但缓存MISS也不会造成错误,因为可以通过计算重新得到。
TreeMap
HashMap
HashTable
LinkedHashMap
WeakHashMap
多线程
理论基础
# 为什么需要多线程 众所周知,CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为: CPU 增加了缓存,以均衡与内存的速度差异;// 导致 可见性问题 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;// 导致 原子性问题 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。// 导致 有序性问题 # 线程不安全示例 如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。 以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值有可能小于 1000。  
线程基础
简介
进程与线程
并发与并行
同步与异步
分类
守护线程
非守护线程
生命周期
新建
就绪
运行
阻塞
死亡
创建线程的方式
继承Thread类
实现Runnable接口
匿名内部类
Callable+FutureTask
线程池
并发关键字
synchronized
volatile
final
Java锁
乐观锁&悲观锁
自旋锁&适应性自旋锁
无锁&轻量级锁&偏向锁&重量级锁
公平锁&非公平锁
可重入锁&不可重入锁
怕他锁&共享锁
JUC全局观
JUC原子类
CAS
Unsafe
原子类
JUC锁
JUC集合类
JUC线程池
JUC工具类
ThreadLocal
Redis
数据结构
5种基础类型

String
Hash
List
Set
Zset
3种特殊类型
Hydrogen
GEO
BitMap
Stream类型(v5.0)
对象机制
redisObject
对象共享
对象淘汰
底层数据结构
核心知识
# Redis线程模型,单线程快的原因 Redis基于Reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器file event handler。这个文件事件处理器,它是单线程的,所以Redis才叫做单线程的模型,它采用IO多路复用机制来同时监听多个Socket,根据Socket上的事件类型来选择对应的事件处理器来处理这个事件。可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了Redis内部的线程模型的简单性。 文件事件处理器的结构包含4部分:多个Socket、IO多路复用程序,文件事件分派器以及事件处理器(命令请求处理器、命令回复处理器、连接应答处理器等)。 多个Socket可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个Socket,会将Socket放入一个队列中排队,每次从队列中取出一个Socket给事件分派器,事件分派器把Socket给对应的事件处理器。 然后一个Socket的事件处理完之后,IO多路复用程序才会将队列中的下一个Socket给事件分派器。文件事件分派器会根据每个Socket当前产生的事件,来选择对应的事件处理器来处理。 单线程快的原因: 1)纯内存操作 2)核心是基于非阻塞的IO多路复用机制 3)单线程反而避免了多线程的频繁上下文切换带来的性能问题
持久化
订阅/发布
事件机制
事务
缓存问题
# Redis缓存问题 在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节。所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问Mysql等数据库。这样可以大大缓解数据库的压力。当缓存库出现时,必须要考虑如下问题: 缓存穿透 缓存穿击 缓存雪崩 缓存污染(或者满了) 缓存和数据库一致性 当我们使用缓存的时候 应考虑是否会出现缓存问题。
一致性
# 数据库和缓存一致性  一般来说,我们会在查询接口判断缓存是否存在,有则返回,无则查询数据库。 在修改删除接口更改数据库时,采用直接删除缓存。 在多并发的场景下,对查询接口和更新接口操作缓存,无论哪种情况都会产生线程问题 如下图:  综合来说,采用先写数据库,再删除缓存 
缓存击穿
# 缓存击穿 问题来源 缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。 解决方案 设置热点数据永远不过期。 接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些 服务 不可用时候,进行熔断,失败快速返回机制。 加互斥锁 
缓存穿透
# 缓存穿透  解决方案 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截; 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击 布隆过滤器。bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小
缓存雪崩
# 缓存雪崩 问题来源 缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。 解决方案 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。 设置热点数据永远不过期 
缓存污染
# 缓存污染(或满了) 缓存污染问题说的是缓存中一些只会被访问一次或者几次的的数据,被访问完后,再也不会被访问到,但这部分数据依然留存在缓存中,消耗缓存空间。 缓存污染会随着数据的持续增加而逐渐显露,随着服务的不断运行,缓存中会存在大量的永远不会再次被访问的数据。缓存空间是有限的,如果缓存空间满了,再往缓存里写数据时就会有额外开销,影响Redis性能。这部分额外开销主要是指写的时候判断淘汰策略,根据淘汰策略去选择要淘汰的数据,然后进行删除操作。 # 最大缓存设置多大 系统的设计选择是一个权衡的过程:大容量缓存是能带来性能加速的收益,但是成本也会更高,而小容量缓存不一定就起不到加速访问的效果。一般来说,我会建议把缓存容量设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销。 对于 Redis 来说,一旦确定了缓存最大容量,比如 4GB,你就可以使用下面这个命令来设定缓存的大小了:  不过,缓存被写满是不可避免的, 所以需要数据淘汰策略。 # 缓存淘汰策略 Redis共支持八种淘汰策略,分别是noeviction、volatile-random、volatile-ttl、volatile-lru、volatile-lfu、allkeys-lru、allkeys-random 和 allkeys-lfu 策略。 怎么理解呢?主要看分三类看: 不淘汰 noeviction (v4.0后默认的) 对设置了过期时间的数据中进行淘汰 随机:volatile-random ttl:volatile-ttl lru:volatile-lru lfu:volatile-lfu 全部数据进行淘汰 随机:allkeys-random lru:allkeys-lru lfu:allkeys-lfu
淘汰策略
noeviction
volatile-random
volatile-ttl
volatile-lru
volatile-lfu
allkeys-lru
allkeys-random
allkeys-lfu(v4.0)
应用实践
缓存场景问题
热点数据缓存
限时业务
优惠活动
短信验证码
计数器相关问题
分布式锁
秒杀活动
互斥锁
社交
排行榜
点赞
关注&相互关注
Feed流实现
附近的人或物
用户签到
UV统计
性能调优
Spring开发集成
大厂经验
高可用|可扩展
主从复制
全量复制
增量复制(v2.8)
哨兵机制(Redis Sentinel)
分片技术(Redis Cluster)
概念基础
MySQL
# 能说下myisam 和 innodb的区别吗? myisam引擎是5.1版本之前的默认引擎,支持全文检索、压缩、空间函数等,但是不支持事务和行级锁,所以一般用于有大量查询少量插入的场景来使用,而且myisam不支持外键,并且索引和数据是分开存储的。 innodb是基于B+Tree索引建立的,和myisam相反它支持事务、外键,并且通过MVCC来支持高并发,索引和数据存储在一起。 # 说下MySQL的索引有哪些吧? 索引在什么层面? 首先,索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。 有哪些? B+Tree 索引 哈希索引能以 O(1) 时间进行查找,但是失去了有序性; InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。 哈希索引 哈希索引能以 O(1) 时间进行查找,但是失去了有序性; InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。 全文索引 MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。查找条件使用 MATCH AGAINST,而不是普通的 WHERE。 全文索引一般使用倒排索引实现,它记录着关键词到其所在文档的映射。 InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。 空间数据索引 MyISAM 存储引擎支持空间数据索引(R-Tree),可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。 # 什么是B+树?为什么B+树成为主要的SQL数据库的索引实现? 什么是B+Tree? B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询的性能。在 B+ Tree 中,一个节点中的 key 从左到右非递减排列,如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。  为什么是B+Tree? 为了减少磁盘读取次数,决定了树的高度不能高,所以必须是先B-Tree; 以页为单位读取使得一次 I/O 就能完全载入一个节点,且相邻的节点也能够被预先载入;所以数据放在叶子节点,本质上是一个Page页; 为了支持范围查询以及关联关系, 页中数据需要有序,且页的尾部节点指向下个页的头部; B+树索引可分为聚簇索引和非聚簇索引? 主索引就是聚簇索引(也称聚集索引,clustered index) 辅助索引(有时也称非聚簇索引或二级索引,secondary index,non-clustered index)。  如上图,主键索引的叶子节点保存的是真正的数据。而辅助索引叶子节点的数据区保存的是主键索引关键字的值。 假如要查询name = C 的数据,其搜索过程如下:a) 先在辅助索引中通过C查询最后找到主键id = 9; b) 在主键索引中搜索id为9的数据,最终在主键索引的叶子节点中获取到真正的数据。所以通过辅助索引进行检索,需要检索两次索引。 之所以这样设计,一个原因就是:如果和MyISAM一样在主键索引和辅助索引的叶子节点中都存放数据行指针,一旦数据发生迁移,则需要去重新组织维护所有的索引。 # 那你知道什么是覆盖索引和回表吗? 覆盖索引指的是在一次查询中,如果一个索引包含或者说覆盖所有需要查询的字段的值,我们就称之为覆盖索引,而不再需要回表查询。 而要确定一个查询是否是覆盖索引,我们只需要explain sql语句看Extra的结果是否是“Using index”即可。 比如:  # 什么是MVCC? 说说MySQL实现MVCC的原理? 什么是MVCC? MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。 在Mysql的InnoDB引擎中就是指在已提交读(READ COMMITTD)和可重复读(REPEATABLE READ)这两种隔离级别下的事务对于SELECT操作会访问版本链中的记录的过程。 这就使得别的事务可以修改这条记录,反正每次修改都会在版本链中记录。SELECT可以去版本链中拿记录,这就实现了读-写,写-读的并发执行,提升了系统的性能。 MySQL的InnoDB引擎实现MVCC的3个基础点 1.隐式字段  如上图,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键;DB_TRX_ID是当前操作该记录的事务ID; 而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本;delete flag没有展示出来。 2.undo log  从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录 3.ReadView 已提交读和可重复读的区别就在于它们生成ReadView的策略不同。 ReadView中主要就是有个列表来存储我们系统中当前活跃着的读写事务,也就是begin了还未提交的事务。通过这个列表来判断记录的某个版本是否对当前事务可见。假设当前列表里的事务id为[80,100]。 如果你要访问的记录版本的事务id为50,比当前列表最小的id80小,那说明这个事务在之前就提交了,所以对当前活动的事务来说是可访问的。 如果你要访问的记录版本的事务id为90,发现此事务在列表id最大值和最小值之间,那就再判断一下是否在列表内,如果在那就说明此事务还未提交,所以版本不能被访问。如果不在那说明事务已经提交,所以版本可以被访问。 如果你要访问的记录版本的事务id为110,那比事务列表最大id100都大,那说明这个版本是在ReadView生成之后才发生的,所以不能被访问。 这些记录都是去undo log 链里面找的,先找最近记录,如果最近这一条记录事务id不符合条件,不可见的话,再去找上一个版本再比较当前事务的id和这个版本事务id看能不能访问,以此类推直到返回可见的版本或者结束。 举个例子,在已提交读隔离级别下: 比如此时有一个事务id为100的事务,修改了name,使得的name等于小明2,但是事务还没提交。则此时的版本链是  那此时另一个事务发起了select 语句要查询id为1的记录,那此时生成的ReadView 列表只有[100]。那就去版本链去找了,首先肯定找最近的一条,发现trx_id是100,也就是name为小明2的那条记录,发现在列表内,所以不能访问。 这时候就通过指针继续找下一条,name为小明1的记录,发现trx_id是60,小于列表中的最小id,所以可以访问,直接访问结果为小明1。 那这时候我们把事务id为100的事务提交了,并且新建了一个事务id为110也修改id为1的记录,并且不提交事务  这时候版本链就是  这时候之前那个select事务又执行了一次查询,要查询id为1的记录。 已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。 如果你是已提交读隔离级别,这时候你会重新一个ReadView,那你的活动事务列表中的值就变了,变成了[110]。按照上的说法,你去版本链通过trx_id对比查找到合适的结果就是小明2。 如果你是可重复读隔离级别,这时候你的ReadView还是第一次select时候生成的ReadView,也就是列表的值还是[100]。所以select的结果是小明1。所以第二次select结果和第一次一样,所以叫可重复读! 这就是Mysql的MVCC,通过版本链,实现多版本,可并发读-写,写-读。通过ReadView生成策略的不同实现不同的隔离级别。 # MySQL 锁的类型有哪些呢? 说两个维度: 共享锁(简称S锁)和排他锁(简称X锁) 读锁是共享的,可以通过lock in share mode实现,这时候只能读不能写。 写锁是排他的,它会阻塞其他的写锁和读锁。从颗粒度来区分,可以分为表锁和行锁两种。 表锁和行锁 表锁会锁定整张表并且阻塞其他用户对该表的所有读写操作,比如alter修改表结构的时候会锁表。 行锁又可以分为乐观锁和悲观锁 悲观锁可以通过for update实现 乐观锁则通过版本号实现。 两个维度结合来看: 共享锁(行锁):Shared Locks 读锁(s锁),多个事务对于同一数据可以共享访问,不能操作修改 使用方法: 加锁:SELECT * FROM table WHERE id=1 LOCK IN SHARE MODE 释锁:COMMIT/ROLLBACK 排他锁(行锁):Exclusive Locks 写锁(X锁),互斥锁/独占锁,事务获取了一个数据的X锁,其他事务就不能再获取该行的读锁和写锁(S锁、X锁),只有获取了该排他锁的事务是可以对数据行进行读取和修改 使用方法: DELETE/ UPDATE/ INSERT -- 加锁 SELECT * FROM table WHERE ... FOR UPDATE -- 加锁 COMMIT/ROLLBACK -- 释锁 意向共享锁(IS) 一个数据行加共享锁前必须先取得该表的IS锁,意向共享锁之间是可以相互兼容的 意向排它锁(IX) 一个数据行加排他锁前必须先取得该表的IX锁,意向排它锁之间是可以相互兼容的 意向锁(IS、IX)是InnoDB引擎操作数据之前自动加的,不需要用户干预; 意义: 当事务操作需要锁表时,只需判断意向锁是否存在,存在时则可快速返回该表不能启用表锁 意向共享锁(IS锁)(表锁):Intention Shared Locks 表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁 前必须先取得该表的IS锁。 意向排它锁(IX锁)(表锁):Intention Exclusive Locks 表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他 锁前必须先取得该表的IX锁。 # 你们数据量级多大?分库分表怎么做的? 首先分库分表分为垂直和水平两个方式,一般来说我们拆分的顺序是先垂直后水平。 垂直分库 基于现在微服务拆分来说,都是已经做到了垂直分库了 垂直分表 垂直切分是将一张表按列切分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。 在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库、用户数据库等。  水平分表 首先根据业务场景来决定使用什么字段作为分表字段(sharding_key),比如我们现在日订单1000万,我们大部分的场景来源于C端,我们可以用user_id作为sharding_key,数据查询支持到最近3个月的订单,超过3个月的做归档处理,那么3个月的数据量就是9亿,可以分1024张表,那么每张表的数据大概就在100万左右。 比如用户id为100,那我们都经过hash(100),然后对1024取模,就可以落到对应的表上了。  # 那分表后的ID怎么保证唯一性的呢? 因为我们主键默认都是自增的,那么分表之后的主键在不同表就肯定会有冲突了。有几个办法考虑: 设定步长,比如1-1024张表我们分别设定1-1024的基础步长,这样主键落到不同的表就不会冲突了。 分布式ID,自己实现一套分布式ID生成算法或者使用开源的比如雪花算法这种 分表后不使用主键作为查询依据,而是每张表单独新增一个字段作为唯一主键使用,比如订单表订单号是唯一的,不管最终落在哪张表都基于订单号作为查询依据,更新也一样。 # 分表后非sharding_key的查询怎么处理呢? 可以做一个mapping表,比如这时候商家要查询订单列表怎么办呢?不带user_id查询的话你总不能扫全表吧?所以我们可以做一个映射关系表,保存商家和用户的关系,查询的时候先通过商家查询到用户列表,再通过user_id去查询。 大宽表,一般而言,商户端对数据实时性要求并不是很高,比如查询订单列表,可以把订单表同步到离线(实时)数仓,再基于数仓去做成一张宽表,再基于其他如es提供查询服务。 数据量不是很大的话,比如后台的一些查询之类的,也可以通过多线程扫表,然后再聚合结果的方式来做。或者异步的形式也是可以的。  # MySQL主从复制? 主要涉及三个线程: binlog 线程、I/O 线程和 SQL 线程。 binlog 线程 : 负责将主服务器上的数据更改写入二进制日志中。 I/O 线程 : 负责从主服务器上读取二进制日志,并写入从服务器的中继日志中。 SQL 线程 : 负责读取中继日志并重放其中的 SQL 语句。  全同步复制 主库写入binlog后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响。 半同步复制 和全同步不同的是,半同步复制的逻辑是这样,从库写入日志成功后返回ACK确认给主库,主库收到至少一个从库的确认就认为写操作完成。 # MySQL主从的延迟怎么解决呢? 这个问题貌似真的是个无解的问题,只能是说自己来判断了,需要走主库的强制走主库查询。 # MySQL读写分离方案? 主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。 读写分离能提高性能的原因在于: 主从服务器负责各自的读和写,极大程度缓解了锁的争用; 从服务器可以使用 MyISAM,提升查询性能以及节约系统开销; 增加冗余,提高可用性。 读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。 
概念和基础
数据类型
核心知识
存储引擎
InnoDB
MyISAM
索引
B+树索引
# 什么是B+树?为什么B+树成为主要的SQL数据库的索引实现? 什么是B+Tree? B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询的性能。在 B+ Tree 中,一个节点中的 key 从左到右非递减排列,如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。  为什么是B+Tree? 为了减少磁盘读取次数,决定了树的高度不能高,所以必须是先B-Tree; 以页为单位读取使得一次 I/O 就能完全载入一个节点,且相邻的节点也能够被预先载入;所以数据放在叶子节点,本质上是一个Page页; 为了支持范围查询以及关联关系, 页中数据需要有序,且页的尾部节点指向下个页的头部; B+树索引可分为聚簇索引和非聚簇索引? 主索引就是聚簇索引(也称聚集索引,clustered index) 辅助索引(有时也称非聚簇索引或二级索引,secondary index,non-clustered index)。  如上图,主键索引的叶子节点保存的是真正的数据。而辅助索引叶子节点的数据区保存的是主键索引关键字的值。 假如要查询name = C 的数据,其搜索过程如下:a) 先在辅助索引中通过C查询最后找到主键id = 9; b) 在主键索引中搜索id为9的数据,最终在主键索引的叶子节点中获取到真正的数据。所以通过辅助索引进行检索,需要检索两次索引。 之所以这样设计,一个原因就是:如果和MyISAM一样在主键索引和辅助索引的叶子节点中都存放数据行指针,一旦数据发生迁移,则需要去重新组织维护所有的索引。 # 那你知道什么是覆盖索引和回表吗? 覆盖索引指的是在一次查询中,如果一个索引包含或者说覆盖所有需要查询的字段的值,我们就称之为覆盖索引,而不再需要回表查询。 而要确定一个查询是否是覆盖索引,我们只需要explain sql语句看Extra的结果是否是“Using index”即可。 比如: 
哈希索引
全文索引
空间数据索引
事务
MVCC
锁
高可用可扩展
分库分表
主从复制
应用实践
性能优化
场景问题
大厂经验
Spring
# 什么是Spring框架? Spring是一种轻量级框架,旨在提高开发人员的开发效率以及系统的可维护性。我们一般说的Spring框架就是Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块。比如Core Container中的Core组件是Spring所有组件的核心,Beans组件和Context组件是实现IOC和DI的基础,AOP组件用来实现面向切面编程。Spring官网列出的Spring的6个特征: 核心技术:依赖注入(DI),AOP,事件(Events),资源,i18n,验证,数据绑定,类型转换,SpEL。 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。 数据访问:事务,DAO支持,JDBC,ORM,编组XML。 Web支持:Spring MVC和Spring WebFlux Web框架。 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。 语言:Kotlin,Groovy,动态语言。
Spring组件
Core Container(Spring的核心容器)
Data Access/Integration(数据访问/集成)
Web模块
AOP、Aspects、Instrumentation和Messaging
Test模块
核心知识
SpringMVC
Bean的生命周期
IOC
AOP
SpringBoot
常用注解
1
@Controller
@RestController
@Component
@Repository
@Service
2
@ResponseBody
@RequestMapping
@Autowired
@PathVariable
@RequestParam
@RequestHeader
3
@ModelAttribute
@SessionAttribute
@Valid
@CookieValue
分布式
# 什么是Spring框架? Spring是一种轻量级框架,旨在提高开发人员的开发效率以及系统的可维护性。我们一般说的Spring框架就是Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块。比如Core Container中的Core组件是Spring所有组件的核心,Beans组件和Context组件是实现IOC和DI的基础,AOP组件用来实现面向切面编程。Spring官网列出的Spring的6个特征: 核心技术:依赖注入(DI),AOP,事件(Events),资源,i18n,验证,数据绑定,类型转换,SpEL。 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。 数据访问:事务,DAO支持,JDBC,ORM,编组XML。 Web支持:Spring MVC和Spring WebFlux Web框架。 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。 语言:Kotlin,Groovy,动态语言。
分布式特征
分布性
对等性
自治性
并发性
分布式理论基础
CAP理论
Base理论
分布式算法
一致性Hash算法
Paxos算法
Raft算法
ZAB算法
分布式全局唯一ID
UUID
使用redis实现
雪花算法-Snowflake
百度-UidGenerator
美团Leaf
分布式任务
分布式缓存
分布式事务
seata
开发架构
SpringCloud
SpringCloudAlibaba
Dubbo
seata
nginx
nacos
redis
Rocketmq
Kafka
中心主题
主题
主题
主题
中心主题
主题
主题
主题
中心主题
主题
主题
主题
中心主题
主题
主题
主题
中心主题
主题
主题
主题