JVM
1.jvm的内存区域划分
思路:程序计数器->虚拟机栈->本地方法栈->堆->方法区->永久代->元空间->直接内存
# 程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭。
# 程序计数器相关
# 什么是程序计数器
# 程序计数器,又叫寄存器,它可以看成是当前线程所执行的字节码的行号指示器。
# 程序计数器的作用
# 作用就是记住下一条jvm指令的执行地址。它也是程序控制流的指示器,循环、跳转、异常等基础功能都需要依赖这个计数器来完成。
# 程序计数器的特点
# 1.线程私有 每个线程都有自己的程序计数器,简单的说,CPU有一个调度器组件,当多个线程运行的时候,会给各个线程分配时间片,如果在时间片内线程没有执行完,就会将此线程变为阻塞状态,之后去执行其他线程,在切换线程的时候,就会用到程序计数器来保存此线程中下一个jvm命令执行的地址。每个线程都有自己的程序计数器,即私有。
# 2.不会存在内存溢出 就是这么设计的。
# 为什么又叫寄存器
# 物理上计数器是通过寄存器实现的,寄存器是cpu组件中读取数据最快的一个单元,因为我们指定读取地址的这个动作特别的频繁,所以设计的时候,将cpu的寄存器当作是程序计数器来使用。
# 计数器记录值的不同
# 如果此时线程执行的是一个java方法,那么这个计数器记录的就是正在执行的jvm字节码指令的地址。
# 如果此时线程执行的是一个本地(native)方法,那么这个计数器的值应为空(undefined)
# 虚拟机栈相关
# 什么是虚拟机栈
# 每个线程运行时所需要的内存,就叫做虚拟机栈,有多少个线程就有多少个栈
# 什么是栈桢
# 每个方法运行的时候需要的内存,一个栈桢对应着一次方法的调用
# 什么是进栈/出栈
# 进栈:调用方法,给方法分配一块栈桢空间,之后进入栈内,压栈运行 出栈:方法执行完毕,将此栈桢弹出栈中,即释放此方法占用的内存。
# 栈的组成
# 每个栈由多个栈桢组成,每个线程只能有一个活动栈桢,对应着当前正在执行的那个方法。
# 栈的数据结构
# 可以理解成一个弹夹,具有先进后出的特点
# 垃圾回收是否涉及到栈内存
# 不涉及,因为栈中的栈桢中的方法,每次调用后都会弹出栈,自动释放内存,所以不需要垃圾回收,垃圾回收主要针对的是堆内存中无用的对象。
# 栈内存分配越大越好嘛
# 不是,总内存是固定的,栈内存越大,那么可创建的线程数量就越少。
# 虚拟机栈中会出现的两种异常
# StackOverflowError异常:栈桢过多会导致,即栈满了,无法分配新的栈桢内存了,比如方法里有递归调用。栈溢出
# OutOfMemoryError异常:如果java虚拟机栈的容量可以动态扩展,那么当栈扩展时无法申请到足够的内存就会抛出此异常。如栈桢过大。不太常见。内存溢出
# 方法里的局部变量是否线程安全
# 安全。判断是否安全,主要看这个变量对于多个线程来说是否为共享的。因为局部变量是方法中定义的变量,对于各个线程来说是私有的,也可以说每个栈桢中都有属于自己的变量,各个栈桢互相之间不会影响,不过要注意,如果用static修饰了,那么就不安全了,因为static唯一,各个线程共享。
# 本地方法栈相关面试题
# 什么是本地方法栈
# jvm调用本地方法的时候,需要给本地方法一个内存空间。
# 本地方法栈的作用
# 为运行本地方法提供内存空间
# 本地方法栈会出现异常嘛
# 与虚拟机栈一样,本地方法栈也会在栈深度溢出和栈扩展失败时抛出两种异常StackOverflowError、OutOfMemoryError
# HotSpot虚拟机直接将本地方法栈和虚拟机栈合二为一。
# 堆相关面试题
# 什么是堆
# java堆是垃圾收集器管理的内存区域(也叫GC堆),java堆是虚拟机所管理的内存中最大的一块,即被所有线程共享的一块内存区域,在虚拟机启动时创建。
# 堆的作用
# 存放对象实例,即通过new关键字创建的对象都会使用堆内存。
# 堆的特点
# 共享的 它是共享的,所以堆中的对象都需要考虑线程安全的问题。
# 有垃圾回收机制
# 堆内存溢出
# 通过new关键字创建的对象如果一直被使用,所以不会被回收,创建了大量的对象之后,就会造成堆内存溢出。
# 堆是连续的内存空间嘛
# java堆可以处于物理上不连续的内存空间,但是在逻辑上他应该被视为连续的,多数虚拟机出于实现简单、存储高效的考虑,可能会要求连续的内存空间。
# 堆是固定大小的嘛
# java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展的来实现的。
# 方法区相关
# 什么是方法区
# 方法区与java堆一样,是各个线程共享的内存区域。逻辑上是堆的一个组成部分。在jvm启动时被创建。
# 方法区的作用
# 用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
# 方法区会出现内存溢出嘛
# 会。jdk1.8之前会导致永久代内存溢出。jdk1.8之后会导致元空间内存溢出。
# 常量池/运行时常量池相关
# 什么是常量池
# 就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
# 什么是运行时常量池
# 运行时常量池是方法区的一部分 常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址(内存地址)。
# class文件里都包含什么
# 一个类的二进制字节码文件大约分为三部分:
# 类的基本信息 (类的版本、字段、方法、接口等描述信息)
# 常量池 (常量池表,用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。)
# 包含的虚拟机指令
# 常量池会抛出OutOfMemoryError异常嘛
# 常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛OutOfMemoryError异常
# 串池相关
# 为什么要引入串池
# 为了避免字符串被重复创建,jvm引入了串池的概念,当一个字符串变成字符串对象时,会去串池中查找是否有同名的字符串,如果有,就将已经存在的字符串地址赋给该字符串对象,如果没有,就将该字符串放入串池。从而降低对内存的消耗。
# 串池存在于何处
# jdk1.6存在于常量池中,jdk1.8时存在于堆中。串池的本质是一个HashTable,字符串是它的key。
# 面试题参考:https://www.cnblogs.com/flyinghome/p/12750118.html串池部分习题
# 说一下什么是永久代
# 永久代是 HotSpot 虚拟机对方法区的具体实现,永久代本身也存在于虚拟机堆中,在 JDK 1.7 中,移除永久代的工作就已经开始了,存储在永久代中的数据转移到了虚拟机堆或者 Native Memory 中。可以认为永久代是用来存放一些类信息的。
# 说一下什么是元空间
# 从 JDK 1.8 开始,HotSpot 虚拟机完全移除了永久代,改为在 Native Memory 中存放这些数据,新的空间被称为元空间
# 直接内存相关
# 什么是直接内存
# 即系统内存,不是虚拟机运行时数据区的一部分
# 直接内存特点
# 1.常见于NIO操作时,用于数据缓冲区。
# 2.分配回收成本较高,但读写性能高。
# 3.不受JVM内存回收管理。
# 如何使用
# 直接内存是如何分配、释放内存的?底层原理是什么
# 直接内存会出现内存溢出异常嘛
# 会,因为是内存,虽然不会受到java堆的大小的限制,但是会受到物理机总内存大小的限制。
# jdk1.4中新加入了NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样在一些场景中能显著提高性能,因为避免了在java堆中和native堆中来回复制数据。 这里主要是通过Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法。ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存。
2.类加载过程
思路:类加载的时机->类加载的过程(加载->验证->准备->解析->初始化->使用->卸载)
//什么时候会初始化一个类
// 1.遇到new A() 方式来实例化类的对象的时候,就会触发类的加载到初始化的全过程,把这个类准备好,然后再实例化一个对象出来。
// 2.或者是包含main() 方法的主类,必须是立马初始化的。
// 3.还有一个很重要的规则,初始化一个类的时候,如果发现它的父类还没初始化,那么会先加载-->初始化他的父类。比如 代码为 public class A extends B { ... }
//类加载的过程
// 1.加载阶段:
// 代码中包含main()方法的主类一定会在JVM进程启动之后被加载到内存,此时如果main方法中有类似A a = new A;这样的使用了别的类,此时就会从对应的.class字节码文件加载对应的类到内存里去。
// 2.验证阶段:
// 简单来说就是根据java虚拟机的规范,会校验你加载进来的.class文件中的内容,是否符合指定的规范。假如.class文件被篡改了,里面的字节码压根不符合规范,那么jvm是没发去执行这个字节码的。所以把.class文件加载到内存里之后,必须验证一下是否符合规范,后续才可以交给jvm来运行。
// 3.准备阶段*:
// 比如现在有test类,里面有一个A变量 public class test { public static int A; } 那么test.class文件内容被加载到内存后,先会进行验证,确认内容是规范的之后,进入准备阶段,即给test类分配一定的内存空间,然后给他里面的类变量A分配内存空间,来一个默认的初始值 0 。
// 4.解析阶段:
// 这个阶段比较复杂,简单的说就是把符号引用替换为直接引用的过程。
// 5.初始化阶段:
// 初始化阶段,会正式执行我们的类初始化的代码了。比如下面这段代码,在准备阶段是不会给变量A开辟内存空间的,只是会给个初始值0,在初始化阶段这段赋值的代码才会执行。下面的静态代码块也是一样,在此阶段执行。
// public class test {
// public static int A = configuration.getInt("replica.flush.interval");
//
// public static Map<String,Replica> replicas;
//
// static {
// methodA();
// }
//
// public static void methodA(){
// this.replicas = new HashMap<String,Replica>();
// }
// }
3.双亲委派机制
思路:java的类加载器->双亲委派机制->Tomcat这种web容器中的类加载器应该如何设计实现
# java的类加载器
# 1.启动类加载器(Bootstrap ClassLoader)
# 它负责加载我们在机器上安装的java目录下的核心类的。(lib目录下一些java的核心类库)。一旦JVM启动,那么首先就会依托启动类加载器,去加载你的java安装目录下lib目录中的核心类库。
# 2.扩展类加载器(Extension ClassLoader)
# 这个类加载器其实也是类似的,就是lib\ext目录下的一些类是需要使用这个类加载器来加载的。
# 3.应用程序类加载器(Application ClassLoader)
# 这个类加载器负责去加载ClassPath环境变量所指定的路径中的类,可以大致理解为去加载我们自己写的代码,这个类加载器负责将我们写好类加载到内存里。
# 4.自定义加载器
# 可以自定义类加载器,去根据自己的需求加载你的类。
# 编译的时候,可以采用一些小工具对字节码进行加密,或者做混淆处理。之后类加载的时候,对加密的类,考虑采用自定义的类加载器来解密文件即可。这样可以防止代码泄露。
# 双亲委派机制
# JVM的类加载器是有亲子层级结构的。基于这个亲子层级结构,就有一个双亲委派机制。
# 就是假设你的应用程序类加载器要加载一个类,他会首先委派给自己的父类加载器(扩展类加载器)去加载,最终传导到最顶层的启动类加载器去加载。
# 但是如果启动类加载器在自己负责的范围里没有找到这个类,就会下推给它的子类加载器,依次类推,直到找到能处理的加载器。
# 这个就是所谓的双亲委派模型。这样可以避免多层级的加载器结构重复加载某些类。
# Tomcat这种web容器中的类加载器应该如何设计实现
# Tomcat自定义了Common、Catalina、Shared等类加载器,其实就是用来加载Tomcat自己的一些核心基础类库的。
# 然后Tomcat为每个部署在里面的web应用都有一个对应的webapp类加载器,负责加载我们部署的这个web应用的类。
# 至于Jsp类加载器,则是给每个JSP都准备了一个Jsp类加载器。而且,Tomcat是打破了双亲委派机制的。
# 每个WebApp负责加载自己对应的那个Web应用的class文件,也就是我们写好的某个系统打包好的war包中所有class文件,不会传导给上层类加载器去加载。
4.垃圾回收算法
思路:如何判断对象是否可以回收->垃圾回收算法(标记清除/标记整理/复制)->垃圾清理过程(选说)
# 如何判断对象是否可以回收
# 1.引用计数法:
# 即只要一个对象被引用一次,计数就会+1,如果一个变量不再引用它了,计数-1,什么时候变为0了,即没有人引用它了,就会作为垃圾被回收掉了。
# 但是这种方式有个弊端,看这种情况A对象引用B,B对象引用A,即它们两个的引用计数一直都为1,最终都不会被回收,最终导致内存溢出。Java采用的不是这种。
# 2.可达性分析算法
# Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。
# 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收。
# 简单的理解是:这种算法首先要确定一系列根对象(可以理解成肯定不能被当成垃圾被回收的对象),如果一个对象直接、间接被根对象引用就不会被当成垃圾回收掉,否则会被回收。
# 哪些对象可以作为GC Root对象呢?
# (1)java虚拟机栈中的引用的对象。
# (2)方法区中的类静态属性引用的对象。 (一般指被static修饰的对象,加载类的时候就加载到内存中。)
# (3)方法区中的常量引用的对象。
# (4)本地方法栈中的JNI(native方法)引用的对象
# 比如:List list = new ArrayList(); list.add("a"); list = null; 在赋值为null之后,就在文件中找不到这个对象了,说明被回收掉了。
# 3.判断对象的引用(四种引用)
# 一句话:只要你的对象被方法的局部变量、类的静态变量给引用了,就不会回收他们。
# 4.没有GC Roots引用的对象,是一定会立马被回收嘛?
# 不会 还有一个finalize()方法可以拯救他自己。
# 假如有一个对象要被垃圾回收了,那么假如这个对象重写了Object类的finalize()方法,此时会先尝试调用一下他的finalize方法,看是否把自己这个实例对象给了某个GC Roots变量,如果重新让某个GC Roots变量引用了自己,那么就不用被垃圾回收了。
# 垃圾回收算法
# 1.标记清除:速度快,但是造成空间不连续,从而产生内存碎片
# 2.标记整理:速度慢,但是不会产生内存碎片
# 3.复制:不会产生内存碎片,但是占用双倍的内存空间
# 新生代采用的是改良过的复制算法,分为一个Eden区(占80%)和两个Survivor(一般为两个,也可多个,各占10%)。
# 老年代采用标记整理算法。速度很慢。就是将存活的对象在内存中尽量移动到一边去,让他们靠在一起,避免产生过多内存碎片,然后再一次性把垃圾对象都回收掉。
# 老年代不采用这种优化过的复制算法是因为老年代的存活对象太多了,采用复制算法来回挪动大量的对象,效率更差
# 垃圾清理过程参考下一个问题
5.垃圾回收机制
思路:5个内存区域哪些会垃圾回收->JVM分代模型->介绍一下常见的垃圾回收器->
新生代、老年代垃圾回收过程、触发时机、新生代中的对象什么时候会进入老年代->
老年代空间分配担保规则->Concurrent Mode Failure->方法区的垃圾回收
# 5个内存区域哪些会垃圾回收
# JVM里垃圾回收针对的是堆(新生代、老年代),还有方法区(永久代),不会针对方法的栈桢。方法一旦执行完毕,栈桢出栈,里面的局部变量直接就从内存里清理掉了。
# 程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭。不需要垃圾回收。
# JVM分代模型
# 根据我们写代码的方式的不同,采用不同的方式创建和使用对象,对象的生命周期是不同的。所以JVM将java堆划分为两个区域,一个是年轻代,一个是老年代。
# 年轻代用来存放创建和使用完立马就要回收的对象。而创建完需要一直长期存在的对象则放在老年代中。
# 为什么要分为年轻代和老年代
# 与垃圾回收有关,对于年轻代里的对象,特点是创建之后很快就会被回收,所以需要一种垃圾回收算法。
# 对于老年代的对象,特点是需要长期存在,所以需要另外一种垃圾回收算法。所以需要分成两个区域来存放。
# 垃圾回收器
# 1.Serial和Serial Old垃圾回收器:
# 分别用来回收新生代和老年代的垃圾对象。工作原理就是单线程运行。这个一般不用了。
# 2.ParNew和CMS垃圾回收器:
# ParNew现在一般都是用在新生代的垃圾回收器,CMS是用在老年代的垃圾回收器,都是多线程并发的机制,性能更好。现在一般是线上生产系统的标配组合。-XX:UseParNewGC选项
# ParNew垃圾回收器默认情况下的线程数量就是跟CPU的核数是一样的。比如机器为4核CPU,那么此时ParNew的垃圾回收线程数就会是4个线程。这个一般不需要手动去调节,也不建议随意动这个参数。可以使用-XX:ParallelGCThreads参数设置线程的数量。
# CMS默认启动的垃圾回收线程数量是(CPU核数 + 3)/ 4
# 3.G1垃圾回收器:
# 统一收集新生代和老年代,采用了更加优秀的算法和设计机制。
# Serial垃圾回收器好还是ParNew垃圾回收器好(单线程回收器好还是多线程好)
# 启动系统的时候服务端模式/客户端模式。-server:服务器模式,-client:客户端模式。一般系统部署到4核8G的Linux服务器上,那么就应该用服务器模式,如果系统运行在比如Windows上的客户端程序,那么就应该是客户端模式。
# 服务器模式可以充分利用多核CPU资源,可以提高性能。客户端模式如果是单核CPU运行多个线程,反而加重了性能开销,并且单CPU运行多线程会导致频繁的线上上下文切换,有效率开销。
# 所以不同模式采用不同的垃圾回收器,最合适的才是最好的。
# 新生代垃圾回收过程
# 新生代分为Eden区(占80%)和两个Survivor(一般为两个,也可多个,各占10%)。新生代会STW。
# 1.创建新对象,大多数放在Eden区
# 2.Eden满了(或达到一定比例),触发Minor GC,把有用的复制到Survivor1, 同时清空Eden区。
# 3.Eden区再次满了,触发Minor GC, 把Eden和Survivor1中有用的,复制到Survivor2, 同时清空Eden,Survivor1。
# 4.Eden区第三次满了,触发Minor GC, 把Eden和Survivor2中有用的,复制到Survivor1, 同时清空Eden,Survivor2。形成循环,Survoivor1和Survivor中来回清空、复制,过程中有一个Survivor处于空的状态用于下次复制的。
# 新生代垃圾回收触发时机
# Eden满了。
# 老年代垃圾回收过程
# 这里用CMS垃圾收集器举例。CMS垃圾回收器采取的是垃圾回收线程和系统工作线程尽量同时执行的模式来处理的。来避免STW时间过长。
# CMS在执行一次垃圾回收的过程一共分为4个阶段:1.初始标记 2.并发标记 3.重新标记 4.并发清理。
# 1.初始标记阶段。这个阶段会让系统的工作线程全部停止,进入“Stop the World”状态。标记出来所有GC Roots直接引用的对象。虽然说要造成“Stop the World”暂停一切工作线程,但是其实影响不大,因为他的速度很快,仅仅标记GC Roots直接引用的那些对象罢了
# 2.并发标记阶段。这个阶段会让系统线程可以随意创建各种新对象,继续运行。在运行期间可能会创建新的存活对象,也可能会让部分存活对象失去引用,变成垃圾对象。在这个过程中,垃圾回收线程,会尽可能的对已有的对象进行GC Roots追踪。第二个阶段,就是对老年代所有对象进行GC Roots追踪,其实是最耗时的。他需要追踪所有对象是否从根源上被GC Roots引用了,但是这个最耗时的阶段,是跟系统程序并发运行的,所以其实这个阶段不会对系统运行造成影响的。
# 3.重新标记阶段。因为第二阶段里,你一边标记存活对象和垃圾对象,一边系统在不停运行创建新对象,让老对象变成垃圾。所以第二阶段结束之后,绝对会有很多存活对象和垃圾对象,是之前第二阶段没标记出来的,所以此时进入第三阶段,要继续让系统程序停下来,再次进入“Stop the World”阶段。然后重新标记下在第二阶段里新创建的一些对象,还有一些已有对象可能失去引用变成垃圾的情况。这个重新标记的阶段,是速度很快的,他其实就是对在第二阶段中被系统程序运行变动过的少数对象进行标记,所以运行速度很快。
# 4.并发清理阶段。这个阶段就是让系统程序随意运行,然后他来清理掉之前标记为垃圾的对象即可。这个阶段其实是很耗时的,因为需要进行对象的清理,但是他也是跟系统程序并发运行的,所以其实也不影响系统程序的执行。
# 最耗时的,其实就是对老年代全部对相关进行GC Roots追踪,标记出来到底哪些可以回收,然后就是对各种垃圾对象从内存里清理掉。但是他的第二阶段和第四阶段,都是和系统程序并发执行的,所以基本这两个最耗时的阶段对性能影响不大。只有第一个阶段和第三个阶段是需要“Stop the World”的,但是这两个阶段都是简单的标记而已,速度非常的快,所以基本上对系统运行响应也不大。
#
# 再说一下并发回收垃圾导致CPU资源紧张问题
# 并发标记的时候,需要对GC Roots进行深度追踪,看所有对象里面到底有多少是存活的。但是因为老年代里存活对象是比较多的,这个过程会追踪大量的对象,所以耗时较高。并发清理,又需要把垃圾对象从各种随机的内存位置清理掉,也是比较耗时的。所以在这两个阶段,CMS的垃圾回收线程是比较耗费CPU资源的。
# CMS默认启动的垃圾回收线程数量是(CPU核数 + 3)/ 4。比如2核CPU,本来CPU资源就有限,结果此时CMS还会有个(2+3)/ 4 = 1个垃圾线程,去占用宝贵的一个CPU。所以其实CMS这个并发垃圾回收的机制,第一个问题就会消耗CPU资源。
# 会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况?若有,jvm是怎么处理的,若没有,jvm是如何保证的呢?
# 新生代垃圾回收会直接Stop the World,系统不能运行了,所以必须等垃圾回收完了,才能再次gc;
# 老年代垃圾回收是有可能的,因为采用了并发收集的机制,一边回收,系统一边运行,也许没回收完就再次触发full gc,此时会进入stop the world,用serial old来回收
# 老年代垃圾回收触发时机
# 1.是老年代可用内存小于新生代全部对象的大小,如果没开启空间担保参数,会直接触发Full GC,所以一般空间担保参数都会打开;
# 2.是老年代可用内存小于历次新生代GC后进入老年代的平均对象大小,此时会提前Full GC;
# 3.是新生代Minor GC后的存活对象大于Survivor,那么就会进入老年代,此时老年代内存不足。
# 4.“-XX:CMSInitiatingOccupancyFaction”参数。(默认92%)。如果老年代可用内存大于历次新生代GC后进入老年代的对象平均大小,但是老年代已经使用的内存空间超过了这个参数指定的比例,也会自动触发Full GC。
# 新生代里的对象一般在什么场景下会进入老年代
# 1.躲过15次GC之后进入老年代
# 被转移到另一块Survivor区域中年龄+1 -XX:MaxTenuringThreshold参数来设置年龄
# 2.动态对象年龄判断
# 它的规则是,假如说当前放对象的Survivor区域里,一批对象的总大小大于了这块Survivor区域的内存大小的50%,那么此时大于等于这批对象年龄的对象,就可以直接进入老年代了。无须等到MaxTenuringThreshold中要求的年龄。
# 这个规则的意义是希望那些可能是长期存活的对象,尽早进入老年代。
# 3.大对象直接进入老年代
# -XX:PretenureSizeThreshold 可以把他的值设置为字节数,比如1048576字节,就是1MB。意思为如果你创建一个大于等于这个大小的对象,比如超大数组等,此时会直接把这个大对象放入到老年代里去。
# 压根不会经过新生代。避免新生代中出现这种大对象,屡次躲过GC,还要在两个Survivor区域里来回复制多次之后才能进入,耗费时间。
# 老年代空间分配担保规则/触发Minor GC之前会如何检查老年代大小,涉及哪几个步骤和条件?
# 1.首先,在执行任何一次Minor GC之前,JVM会先检查一下老年代可用的内存空间,是否大于新生代所有对象的总大小。
# 原因就是最极端情况下,Minor GC后,所有对象都活了,那难道所有对象都要进入老年代嘛。
# 2.如果说发现老年代的内存大小是大于新生代所有对象的,此时可以放心的执行Minor GC了。
# 如果小于,就会看一个参数的设置了 -XX:-HandlePromotionFailure ,如果设置了,进行下一步的判断,看看老年代内存大小是否大于之前每一次Minor GC后进入老年代的对象的平均大小。
# 如果判断发现失败了,或者参数没有设置,此时就会直接触发一次Full GC,就是对老年代进行垃圾回收,尽量腾出一些内存空间,然后再执行Minor GC。
# 如果判断成功了并且参数设置了。意味着可以冒险尝试一下Minor GC,此时有下面几种可能:
# 1> Minor GC后,剩余存活对象的大小 < Survivor区大小 ,此时存活对象进入Survivor区域即可。
# 2> Minor GC后,剩余存活对象的大小 > Survivor区大小,但是 剩余存活对象的大小 < 老年代可用内存大小,此时直接进入老年代即可。
# 3> Minor GC后,剩余存活对象的大小 > Survivor区大小,并且 剩余存活对象的大小 > 老年代可用内存大小,此时就会发生Handle Promotion Failure的情况,
# 这个时候就会触发一次Full GC。如果Full GC过后,老年代还是没有足够空间存放Minor GC过后剩余存活的对象,此时就会导致OOM 内存溢出了。
# 说一下Concurrent Mode Failure问题
# 在并发清理阶段,CMS只不过是回收之前标记好的垃圾对象。但是这个阶段系统一直在运行,可能会随着系统运行让一些对象进入老年代,同时还变成垃圾对象,这种垃圾对象是“浮动垃圾”。
# 因为他虽然成为了垃圾,但是CMS只能回收之前标记出来的垃圾对象,不会回收他们,需要等到下一次GC的时候才会回收他们。所以为了保证在CMS垃圾回收期间,还有一定的内存空间让一些对象可以进入老年代,一般会预留一些空间。
# CMS垃圾回收的触发时机,其中有一个就是当老年代内存占用达到一定比例了,就自动执行GC。
# “-XX:CMSInitiatingOccupancyFaction”参数可以用来设置老年代占用多少比例的时候触发CMS垃圾回收,默认92%。预留8%的空间给并发回收期间,系统程序把一些新对象放入老年代中。
# 如果CMS垃圾回收期间,系统程序要放入老年代的对象大于了可用内存空间。此时就会触发Concurrent Mode Failure。
# 此时就会自动用“Serial Old”垃圾回收器替代CMS,就是直接强行把系统程序“Stop the World”,重新进行长时间的GC Roots追踪,标记出来全部垃圾对象,不允许新的对象产生。然后一次性把垃圾对象都回收掉,完事儿了再恢复系统线程。
# 方法区的垃圾回收
# 下面几种情况下都满足的情况下,方法区里的类会被回收:
# 1.首先该类的所有实例对象都已经从java堆里被回收
# 2.其次加载这个类的ClassLoader已经被回收
# 3.最后,对该类的Class对象没有任何引用。
6.对G1有一定的了解
思路:介绍G1->G1的核心设计思路是什么->Region->新生代Region垃圾回收->
大对象Region->混合回收流程(成功+失败)->G1的一些参数
# G1介绍
# G1垃圾回收器也会有新生代和老年代的概念,但是只不过是逻辑上的概念。并且它是可以同时回收新生代和老年代的对象的,不需要两个垃圾回收器配合起来运作,他一个人就可以搞定所有的垃圾回收。
# G1的核心设计思路是什么
# 它会追踪每个Region里的回收价值,即每个Region里的对象有多少是垃圾,如果对这个Region进行垃圾回收,需要耗费多长时间,可以回收掉多少垃圾。
# 最后在垃圾回收的时候,尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,同时在有限的时间内尽量回收尽可能多的垃圾对象。
# region
# 1.就是把Java堆内存拆分为多个大小相等的Region。新生代可能包含了某些Region,老年代可能包含了某些Reigon,并且不是固定的,有可能新生的某个region回收之后,下次就被分配给了老年代。
# 所以没有所谓新生代给多少内存,老年代给多少内存这一说了。实际上新生代和老年代各自的内存区域是不停的变动的,由G1自动控制。
# 2.JVM启动的时候会自动分配堆里有多少个Region,每个Region的大小都是多少。因为JVM最多可以有2048个Region,并且Region的大小必须是2的倍数。堆大小除以2048。
# 比如堆大小是4G(4096MB),除以2048个Region,每个Region的大小就是2MB。也可以通过手动方式来指定,则是“-XX:G1HeapRegionSize”
# 新生代Region垃圾回收
# 1.刚开始的时候,默认新生代对堆内存的占比是5%,可以通过“-XX:G1NewSizePercent”来设置新生代初始占比。随着系统运行,新生代越来越大,但是最多占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”来设置。
# 2.在G1中,其实还是有新生代、老年代的区分。即新生代的Region里哪些属于Eden,哪些属于Survivor。
# 3.一旦新生代达到了设定的占据堆内存的最大大小60%。G1就会用之前说过的复制算法来进行垃圾回收,STW,然后在Eden和Survivor中来回操作。
# 因为G1是可以设定目标GC停顿时间的,也就是G1执行GC的时候最多可以让系统停顿多长时间(-XX:MaxGCPauseMills)默认值是200ms。之后会选择最合适的一部分Region进行回收。
# 大对象Region
# 以前说是那种大对象也是可以直接进入老年代的,那么现在在G1的这套内存模型下。G1提供了专门的Region来存放大对象,而不是让大对象进入老年代的Region中。
# 在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2MB,只要一个大对象超过了1MB,就会被放入大对象专门的Region中。而且一个大对象如果太大,可能会横跨多个Region来存放。
# 大对象虽然不属于新生代和老年代,但是在新生代、老年代在回收的时候,会顺带带着大对象Region一起回收,所以这就是在G1内存模型下对大对象的分配和回收的策略。
# 混合回收
# G1不会单独对老年代进行回收,每次回收的时候,都会带着新生代。
# G1有一个参数,是“-XX:InitiatingHeapOccupancyPercent”,他的默认值是45%。意思就是说,如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段。
# 过程如下:
# 1.首先会触发一个“初始标记”的操作
# 先停止系统程序的运行,然后对各个线程栈内存中的局部变量代表的GC Roots,以及方法区中的类静态变量代表的GC Roots,进行扫描,标记出来他们直接引用的那些对象。仅仅只是标记一下GC Roots直接能引用的对象,这个过程速度是很快的。
# 2.接着会进入“并发标记”的阶段
# 这个阶段会允许系统程序的运行,同时进行GC Roots追踪,从GC Roots开始追踪所有的存活对象。这个并发标记阶段还是很耗时的,因为要追踪全部的存活对象。但是这个阶段是可以跟系统程序并发运行的,所以对系统程序的影响不太大。而且JVM会对并发标记阶段对对象做出的一些修改记录起来,比如说哪个对象被新建了,哪个对象失去了引用。
# 3.接着是下一个阶段,最终标记阶段
# 这个阶段会进入“Stop the World”,系统程序是禁止运行的,但是会根据并发标记阶段记录的那些对象修改,最终标记一下有哪些存活对象,有哪些是垃圾对象。
# 4.最后一个阶段,就是“混合回收“阶段(Mixed GC)
# 这个阶段会计算老年代中每个Region中的存活对象数量,存活对象的占比,还有执行垃圾回收的预期性能和效率。接着会停止系统程序,然后全力以赴尽快进行垃圾回收,此时会选择部分Region进行回收,因为必须让垃圾回收的停顿时间控制在我
# 们指定的范围内。而且大家需要在这里有一点认识,其实老年代对堆内存占比达到45%的时候,触发的是“混合回收“,也就是说,此时垃圾回收不仅仅是回收老年代,还会回收新生代,还会回收大对象。
# 混合回收失败的情况
# 如果在进行Mixed回收的时候,无论是年轻代还是老年代都基于复制算法进行回收,都要把各个Region的存活对象拷贝到别的Region里去,此时万一出现拷贝的过程中发现没有空闲Region可以承载自己的存活对象了,就会触发一次失败。
# 一旦失败,立马就会切换为停止系统程序,然后采用单线程进行标记、清理和压缩整理,空闲出来一批Region,这个过程是极慢极慢的。 回收失败时Full GC,是采用Serial Old回收器。
# G1的一些参数
# 1.-XX:G1MixedGCCountTarget(默认值是8次)
# G1是允许执行多次混合回收的,比如先停止工作,执行一次混合回收回收掉 一些Region,接着恢复系统运行,然后再次停止系统运行,再执行一次混合回收回收掉一些Region。
# 这个参数可以控制这个。就是在一次混合回收的过程中,最后一个阶段执行几次混合回收,默认值是8次。意味着最后一个阶段,先停止系统运行,混合回收一些Region,再恢复系统运行,接着再次禁止系统运行,混合回收一些Region,反复8次。这样可以尽可能让系统不要停顿时间过长。
# 2.-XX:G1HeapWastePercent(默认值是5%)
# 在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉。
# 这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会 立即停止混合回收,意味着本次混合回收就结束了。
# 而且从这里也能看出来G1整体是基于复制算法进行Region垃圾回收的,不会出现内存碎片的问题,不需要像CMS那样标记-清理之后,再进行内存碎片的整理。
# 3.-XX:G1MixedGCLiveThresholdPercent(默认值是85%)
# 意思就是确定要回收的Region的时候,必须是存活对象低于85%的Region才可以进行回收。否则要是一个Region的存活对象多余85%,你还回收他干什么?这个时候要把85%的对象都拷贝到别的Region,这个成本是很高的。
Mysql
1. mysql的架构设计
思路:sql接口->查询解析器->查询优化器->执行器->存储引擎接口->整个流程
# 组成
# 架构主要由sql接口、查询解析器、查询优化器、执行器、存储引擎接口组成。
# 流程
# 1.客户端发送请求(SQL语句)给mysql服务端
# 2.mysql连接池接收到请求之后,将sql语句发送给sql接口去处理
# 3.之后由查询解析器去解析sql,比如判断语法是否正确等
# 4.如果没有问题,就交给查询优化器去选择一个最优的查询路径来提升效率,生成执行计划
# 5.之后由执行器根据这个执行计划去调用储存引擎接口,完成一系列执行计划语句。
2. Buffer Pool
思路:概念->chunk机制->数据页->缓存页->free链表->哈希表->flush链表->LRU链表->预读机制
# 概念:
# 1.本质上就是数据库的一个内存组件,数据库一启动,就会按照我们设置的大小并稍微加大一点去找操作系统申请一块内存区域。
# 2.默认128M,可以通过配置文件中的innodb_buffer_pool_size来配置。
# 3.之后按照缓存页默认的16KB的大小和800个字节左右的描述数据的大小,在Buffer Pool中划分出来一个个的缓存页和对应的描述数据。只不过一开始里面都是空的。
# 4.单个Buffer Pool,如果高并发下,多个线程会加锁串行排队执行,性能会差很多。实际生产中,会使用多个Buffer Pool优化并发能力。可以通过参数innodb_buffer_pool_instances=4来设置Buffer Pool的数量的。
# 5.生产环境基于机器配置来设置Buffer Pool的大小,即Buffer Pool设置为机器内存的50%~60%比较合适 32G->20G 128G->80G
# 6.这里还有一个公式:Buffer Pool总大小=(chunk大小*Buffer Pool数量)的倍数 比如:默认的一个chunk大小为128M,机器内存为32G,我们想要给Buffer Pool总大小就为20G,那么Buffer Pool的数量就可以是16个
#
# chunk机制
# 1.Buffer Pool是由多个chunk组成的,通过innodb_buffer_pool_chunk_size参数来控制,默认128M。
# 2.每个chunk里就是一系列的描述数据块和缓存页,每个Buffer Pool里的多个chunk共享一套free、flush、lru这些链表
# 3.mysql运行期间时不允许动态调整Buffer Pool大小的,但是可以基于chunk机制动态调整Buffer Pool的大小。即申请一系列的chunk之后分配给Buffer Pool就可以了。
#
# 数据页
# 1.mysql对数据抽象出来一个数据页的概念,即磁盘文件中有很多的数据页,每个数据页中存放了多行数据。每个数据页默认大小为16KB。
# 2.加载的时候是把当前要更新的那条数据所在的数据页都加载到内存中去,这么做的原因就是防止一条一条的加载造成效率低。最后将数据刷到磁盘上的时候也是以数据页为单位的。
# 3.数据页主要由以下部分组成:
# (1)文件头(占38字节)
# (2)数据页头(56字节)
# (3)最大记录、最小记录(26字节)
# (4)多个数据行(不固定)
# (5)空闲区域(不固定)
# (6)数据页目录(不固定)
# (7)文件尾部(8字节)
# 4.那么将数据插入到数据页过程为,一开始这个数据页的数据行区域为空,随着不停的插入多行数据,数据行区域越来越大,空闲区域越来越小,最后满了。
# 5.大量的数据页都是按照顺序一页一页存放的,两两相邻的数据页之间会采用双向链表的格式互相引用,即数据页包含两个指针,一个指向上一个数据页的物理地址,一个指向下一个数据页的物理地址。
# 6.数据页之间是组成双向链表的,数据页内部的数据行是组成单向链表的。
#
# 缓存页
# 1.磁盘上我们叫做数据页,而Buffer Pool中的数据页通常被称为缓存页,与数据页一一对应。
# 2.每个缓存页也都有一个描述信息,这个描述信息本身也是一块数据,放在所有缓存页的最前面。
# (1)数据页所属的表空间
# (2)数据页的编号
# (3)这个缓存页在Buffer Pool中的地址
# (4)其他信息
# 3.Buffer Pool中的描述数据大概相当于缓存页的5%,假设设置Buffer Pool大小为128M,实际上Buffer Pool的真实大小会超出一些,比如130多M这样。
#
# free链表
# 1.数据结构为双向链表,链表里的每个节点都是一个空闲的缓存页的描述数据块的地址,即每个空闲的缓存页对应的描述数据块都会被放入这个链表里。
# 2.要注意这个链表本身就是由Buffer Pool里的描述数据块组成的,可以认为每个描述数据块里都有两个指针,一个是free_pre,一个是free_next,分别指向上一个free链表的节点和下一个节点。
# 3.除此以外,free链表里只有一个基础节点是不属于Buffer Pool的,大概40字节左右,里面存放了free链表的头和尾节点的地址,还有free链表里当前有多少个节点。
#
# 哈希表数据结构
# 1.表空间号+数据页号作为key,缓存页的地址作为value。每次将一个数据页缓存之后,就往里存入一个key-value对。
# 2.当要使用一个数据页的时候,通过key去哈希表中查一下进而去判断此数据页是否被缓存了。
#
# flush链表
# 1.数据结构为双向链表,链表里的每个节点都是一个被修改过的缓存页的描述数据块的地址。
# 2.所有被修改过的缓存页,都会把它的描述数据块加到flush链表中去,意思为这些数据页都是脏页,后续都要刷新到磁盘上去的。
# 3.此链表的作用是判断哪些缓存页是脏页,防止缓存页被修改了然后与磁盘上的数据页数据不一致。
#
# LRU链表(Least Recently Used)
# 1.Buffer Pool中没有空闲的缓存页了,此时再有数据页想要加载进来就需要淘汰一部分了,即把被修改过的缓存页刷到磁盘上去,之后清空这个缓存页。
# 清空哪个缓存页,涉及到一个缓存命中率的概念,即看哪个缓存页经常被查询、修改,就不动这个,淘汰掉最少使用的。
# 2.LRU链表,采取的是冷热数据分离的思想。即将LRU链表拆为冷数据和热数据两部分,通过参数innodb_old_blocks_pct参数控制,默认37,即冷数据占比37%,
# 当数据页第一次被加载到内存中去时,放到冷数据链表的头部。通过参数innodb_old_blocks_time参数,默认值1000毫秒
# 即加载结束后,再过1s,又访问了这个缓存页,它才会被移入到热数据链表的头部,就是为了防止刚加载此缓存页,在冷链表的头部,随后在1s内再次访问此数据页,然后很久没人访问了的问题。
# 3.LRU链表的热数据区域访问规则又被优化过,即只有在热数据区域的后3/4部分的缓存页被访问了,才会被移动到头部去。这样可以尽量的减少链表中的节点移动了。通过这种方式保证活跃的都放在头部,淘汰的时候从尾部开始淘汰就行了。
# 4.将LRU链表的冷数据区域的缓存页刷入到磁盘中有以下几个时机:
# (1)后台一个线程运行一个定时任务,定时将一部分缓存页刷到磁盘里去,之后清空并加入到free链表中去。
# (2)在mysql不忙的时候,把flush链表中的缓存页都刷入到磁盘中。防止热数据链中的数据一直都不被刷入到磁盘中去。
#
# 预读机制
# 1.设计预读机制原因是为了提升性能。即比如挨着的多个数据页都要缓存到缓存页中,需要多次磁盘IO,通过预读可以减少磁盘IO的次数。
# 2.具体实现方式就是加载一个数据页的时候,会连带着把它相邻的其他数据页也加载了。这就会导致没有被查询的但是通过预读机制进来的也排在了LRU链表前面了,这个就不合理了。
# 3.触发预读机制的情况
# (1)innodb_read_ahead_threshold 默认为56,意思是访问的数据页数量超过此阈值,就会触发预读,把下一相邻区中的所有数据页都加载到缓存里去。
# (2)如果Buffer Pool里缓存了一个区里的13个连续的数据页,而且他们都被频繁的访问,此时就会直接触发预读,把这个区里的其他数据页都加载到缓存中去。
# (3)可以通过参数innodb_random_ahead来控制,默认OFF,即关闭。
# 这里要注意,全表扫描也可能会导致频繁被访问的缓存页被淘汰。因为如果表数据量大,会将大量的只被访问过一次的数据页都缓存到了缓存页中去,将原来靠前的缓存页挤到最后。
3. Innodb存储引擎
思路:存储引擎概念->常用存储引擎及区别->Buffer Pool->Undo log->redo log->
redo log block->redo log buffer->补充一个binlog
# 存储引擎概念
# 1.存储引擎就是存储数据,建立索引,更新查询数据等等技术的实现方式。存储引擎是基于表的,而不是基于库的。所以存储引擎也可被称为表类型。
# 2.Oracle,SqlServer等数据库只有一种存储引擎。Mysql提供了插件式的存储引擎架构。所以mysql存在多种存储引擎。
# 3.可以通过执行show engines 语句,来查询当前数据库支持的存储引擎。查看mysql数据库默认的存储引擎:show variables like '%storage_engine%' 。
# 常用存储引擎及区别
# 1.Mysql5.0支持的存储引擎包含:InnoDB、MyISAM、BDB、MEMORY、MERGE、EXAMPLE、NDB Cluster、ARCHIVE、CSV、BLACKHOLE、FEDERATED等,其中InnoDB和BDB提供事务安全表,其它存储引擎是非事务安全表。
# 2.Mysql5.5之前默认的是MyISAM,之后改为了InnoDB。
# 3.MyISAM与InnoDB区别
# (1)InnoDB支持事务,外键,MyISAM不支持。
# (2)MyISAM速度 > InnoDB
# (3)InnoDB行锁(适合高并发),MyISAM表锁
# (4)空间使用上 InnoDB > MyISAM
# (5)内存使用上 InnoDB > MyISAM
# (6)批量插入速度 InnoDB < MyISAM
# (7)存储方式不同
# InnoDB
# 1>使用共享表空间存储,这种方式创建的表的表结构保存在.frm文件中,数据和索引保存在innodb_data_home_dir和innodb_data_file_path定义的表空间中,可以是多个文件。
# 2>使用多表空间存储,这种方式创建的表的表结构仍然存在.frm文件中,但是每个表的数据和索引单独保存在.ibd中。
# MyISAM
# 每个MyISAM在磁盘上存储成3个文件,其文件名都和表名相同,但拓展名分别是:
# 1>.frm(存储表定义)
# 2>.MYD(MYData,存储数据)
# 3>.MYI(MYIndex,存储索引)
#
# Buffer Pool(缓冲池)
# 见上一个关于Buffer Pool的描述即可。
# undo log(回滚日志)
# 1.设计的目的是用来事务回滚。里面记录的东西很简单,比如我们事务里是一条insert语句,那么回滚日志就会对应有一个主键id和delete操作
# 2.insert语句的undo log日志,类型是TRX_UNDO_INSERT_REC,这个undo log里包含了以下内容:
# (1)这条日志的开始位置
# (2)主键的各列长度和值
# (3)表id
# (4)undo log日志编号
# (5)undo log日志类型
# (6)这条日志的结束位置
# 3.那么如果现在buffer pool里的一个缓存页里插入了一条数据,执行了insert语句了,需要回滚,
# 我们就可以通过undo log日志找到具体操作的哪个表,主键是什么,直接定位到那个表和主键对应的缓存页,之后删除对应的那条数据就可以了。
# 4.undo log版本链。简单来说,每条数据都有两个隐藏字段
# (1)trx_id:就是最近一次更新这条数据的事务的id。
# (2)roll_pointer:就是指向了你更新这个事务之前生成的undo log。
# 这么设计的目的是可以基于undo log 多版本链条实现ReadView机制。即保存一个快照链条,让你可以读到之前的快照值。
# redo log(重做日志)
# 1.记录下来对数据的操作,设计的目的是防止更新完内存中的数据但还没来得及更新磁盘上的数据的时候宕机了,造成数据丢失.
# 2.但是它也是在内存中的,有丢失的风险。可以通过innodb_flush_log_at_trx_commit配置来防止数据丢失。
# 0:提交事务的时候,不会将redo log buffer里的数据落到磁盘中去。
# 1:落入磁盘中,才会认为是事务提交成功了。(建议使用此策略,可以保证数据绝对不会丢失)
# 2:把redo log写入磁盘文件对应的os cache缓存里去,而不是直接进入磁盘文件。
# 3.redo log它里面需要记录的就是表空间号+数据页号+偏移量+修改几个字节的值+具体的值。
# 本质上就是在对某个表空间的某个数据页的某个偏移量的地方修改了几个字节的值,具体修改的值是什么。
# 4.根据你修改了数据页几个字节的值,redo log就划分为了不同的类型:
# (1)MLOG_1BYTE:这个类型的日志指的就是修改了1个字节的值。
# (2)MLOG_2BYTE:这个类型的日志指的就是修改了2个字节的值。以此类推。还有4个8个等的日志类型。
# (3)MLOG_WRITE_STRING:这个类型的日志指的就是修改了一大串的值。
# 5.所以一条redo log看起来大概的结构就是
# (1)日志类型(类似MLOG_1BYTE),表空间ID,数据页号,数据页中的偏移量,具体修改的数据。
# (2)如果类型是MLOG_WRITE_STRING,结构就是: 日志类型,表空间ID,数据页号,数据页中的偏移量,修改数据长度,具体修改的数据。
# 6.实际情况下,redo log都会写入一个目录中的文件里,这个目录可以通过show variables like 'datadir' 来查看,可以通过innodb_log_group_home_dir参数来设置这个目录的。
# 7.然后redo log是有多个的,可以通过innodb_log_files_in_group指定日志文件的数量,默认2个,分别为ib_logfile0和ib_logfile1。
# 先写第一个,写满了就写第二个,第二个也满了,就覆盖第一个继续写就可以了。
# 使用参数innodb_log_file_size指定每个redo log文件的大小,默认48MB。
# redo log block
# 1.一种数据结构,对于redo log来说,也不是单行单行的写入日志文件的,它是用一个redo log block来存放多个单行日志的。
# 2.一个redo log block是512字节,分为三部分
# (1)12字节的header块头,这里又分为4个部分
# 1> 4个字节的block no,就是块唯一编号。
# 2> 2个字节的data length,就是block里写入了多少字节数据。
# 3> 2个字节的first record group,第一个日志分组偏移量。就是说每个事务都会有多个redo log,是一个redo log group,即一组redo log,那么在这个block里的第一组redo log的偏移量,就是这两个字节存储的。
# 4> 4个字节的checkpoint on。
# (2)496字节的body块体
# (3)4字节的trailer块尾
# 3.那么其实对于redo log而言,就是不停的追加写入到redo log磁盘文件中的一个redo log block里去的,一个block最多放496字节的redo log日志。
# 那么我们写文件的时候,按照字节一个字节一个字节的写入,那么每512个字节组合起来就是一个redo log block。
# redo log buffer(缓冲)
# 1.是mysql专门设计用来缓冲redo log写入的。那么它也是跟操作系统申请的一片连续内存,里面划分出来多个空的redo log block。可以通过参数innodb_log_buffer_size指定其大小,默认16M。
# 2.那么写入一条redo log的时候,就会先从第一个redo log block开始写入,写满了就继续写下一个,直到写满所有的redo log block。
# 满了之后,会强制将redo log block刷入磁盘文件中去。即将512字节的redo log block追加到redo log日志文件里去。
# 3.我们平时执行一个事务的过程中,每个事务可能有多个增删改查的操作,那么就会有多个redo log,可以把他们看成一组redo log,
# 他们会先在别的地方暂存,执行完了之后,再把一组redo log写入到redo log buffer的block里去的。如果一组redo log太多,可能会存放在两个redo log block中去。
# 如果一个redo log group比较小,那么也可能是多个redo log group是在一个redo log block里的。
# 4.那么redo log block是哪些时候会刷入到磁盘文件里去的:
# (1)如果写入redo log buffer的日志已经占据了redo log buffer总容量的一半了,也就是超过了8M的redo log在缓冲里,此时就会把他们刷入到磁盘文件里去。
# (2)一个事务提交的时候,必须把他的那些redo log所在的redo log block都刷入到磁盘文件里去。只有这样才会保证数据绝对不会丢失。
# (3)有一个后台线程每隔1s定时将redo log buffer里的redo log block刷到磁盘文件中去。
# (4)mysql关闭的时候,redo log block都会刷到磁盘文件中去。
# binlog(归档日志)
# 1.不是innodb存储引擎特有的日志文件,是属于mysql server自己的日志文件。提交事务的时候,同时会将这次更新的binlog日志写入到磁盘文件中去。
# 2.binlog日志的刷盘策略sync_binlog
# 0:将binlog写入os cache内存缓存中,而不是直接写入磁盘(默认)
# 1:在提交事务的时候,强制把binlog直接写入到磁盘文件中(此策略可保证数据绝对不会丢失)
# 整体流程
# 1.先把要更新的那条记录从磁盘文件加载到缓冲池中,加独占锁,并把旧值写入undo log中
# 2.之后更新缓冲池中的记录,之后将操作写入到redo log buffer中
# 3.提交事务的时候,将redo log buffer中的数据写入到磁盘的redo log中去,同时会将这次更新的binlog日志写入到磁盘文件中去。
# 4.之后将这次更新的binlog文件名和这次更新操作在binlog日志中的位置都写入到redo log日志文件里去。
# 5.同时在redo log日志文件里写一个commit标记。这个操作结束后,才算是完成了事务的提交。commit标记的意义在于保持redo log和binlog日志一致。
# 6.最后mysql有一个后台的IO线程,会在之后的某个时间里,随机把buffer pool中的修改后的脏数据刷回到磁盘中去。
4. MVCC机制
思路:MVCC介绍->undo log多版本链条->ReadView->RC->RR
# MVCC介绍
# 1.多版本并发控制(Multi-Version Concurrency Control)是MySQL的InnoDB引擎实现隔离级别的一种具体方式。
# 2.多个事务并发运行的时候,同时读写一个数据,可能会出现脏写、脏读、不可重复读、幻读几个问题。
# 3.然后mysql实现MVCC机制的时候,是基于undo log多版本链条+ReadView机制来实现的,默认的RR隔离级别就是基于这套机制来实现的。
# undo log版本链
# 1.简单来说,每条数据都有两个隐藏字段
# (1)trx_id:就是最近一次更新这条数据的事务的id。
# (2)roll_pointer:就是指向了你更新这个事务之前生成的undo log。(或者说上一个事务的undo log值,如果是新增的数据,没有上一个undo log 值就是空)
# 这么设计的目的是可以基于undo log 多版本链条实现ReadView机制。即保存一个快照链条,让你可以读到之前的快照值。
# ReadView
# 1.MVCC 中维护了一个 ReadView 结构,它是基于undo log多版本链条实现的一套读视图机制。简单的来说,就是你执行一个事务的时候,就给你生成一个ReadView。
# 然后再有查询的时候,根据ReadView进行判断的机制,你就知道你应该读取哪个版本的数据了。
# 2.那么ReadView机制意思就是
# (1)如果是你事务自己更新的数据,或者在你生成ReadView之前其他事务修改的值并提交了,自己是可以读到的。
# (2)如果是你生成ReadView的时候,已经存在的未提交的其他事务,或者你生成ReadView之后,才开启的事务修改的数据,就算提交了,也是读不到的。
# 3.ReadView结构如下:
# (1)m_ids: 这个就是说此时有哪些事务在mysql里执行还没提交的。
# (2)min_trx_id: m_ids里最小的值。
# (3)max_trx_id: mysql下一个要生成的事务id,就是最大事务id。
# (4)creator_trx_id: 就是你这个事务的id。
# Read Committed隔离级别(RC)基于ReadView机制的实现原理
# 1.RC隔离级别,实际上意思就是说在你事务运行期间,只要别的事务修改数据还提交了,你就是可以读到人家修改的数据的。所以是会发生不可重复读、幻读的问题。
# 2.核心要点就是
# (1)当一个事务A处于RC级别的时候,A每次发起查询的时候,都会重新生成一个ReadView。
# (2)每次查询的ReadView里,m_ids列表是否包括这个已经提交的事务了,如果不包含,说明提交了,就判定可以读到了。
# 3.具体流程
# 见图
# Repeatable Read隔离级别(RR)基于ReadView机制的实现原理(Mysql默认)
# 1.RR隔离级别,实际上意思就是说事务A读一条数据,无论读多少次,都是一个值,别的事务B修改之后哪怕提交了,你也读不到。
# 2.核心要点就是
# (1)ReadView一旦生成了就不会改变了。所以基于这个ReadView看到的值始终都是一样的。
# 3.具体流程
# (1)RR隔离级别处理不可重复读问题
# (2)RR隔离级别处理幻读问题
# 这里有个误区,就是很多人说RR级别不能处理幻读的问题。
# 网上也是这么说的,那么我也有这样的疑惑,刚才重新复习发现了这个问题,下面是解释:
# mysql读取数据分为两种,一个是快照读,一个是当前读
# 比如select * from t where 这种属于快照读 。delete update insert 实际上也要先读取数据 这种属于当前读。
# 快照读是使用readview+undo log来处理幻读问题的。当前读是使用间歇锁来处理幻读的。
undo log版本链
RC隔离级别实现
RR隔离级别解决不可重复读问题
RR隔离级别解决幻读问题
5. 索引
思路:索引作用->页分裂->主键索引->索引页->聚簇索引->二级索引->索引B+树的维护->索引数量->
覆盖索引->几种规则->设计索引的原则
# 索引作用
# 索引的作用是提高查询速度,如果没有索引,当我们要查询一条数据的时候,就会将一个个数据页加载到Buffer Pool中,
# 并且一行行数据的查询,即全表扫描,每次查询都会将所有数据页的每条数据都过一遍,效率低。
# 页分裂
# 索引要求后一个数据页的主键值都要大于前一个数据页的主键值,那么如果插入的时候,主键不是自增的,
# 就可以会出现后一个数据页的主键值中有的主键是小于前一个数据页的主键值的。那么此时就会触发页分裂,即将前一个数据页里主键值较大的,移动到后一个数据页中去,
# 然后把新添加的主键值较小的数据移动到前一个数据页中去,以此来保证新数据页主键值一定大于上一个数据页的所有主键值。
# 主键索引
# 为了防止全表扫描,需要针对主键去创建一个索引,其实就是主键目录,即把每个数据页的页号还有数据页里最小的主键值放在一起,
# 组成一个索引的目录。搜索的时候通过页号和最小主键值就可以定位数据在哪个数据页里了。
# 索引页
# 实际上索引数据就存储在数据页里的,此时索引放在页里后,就会有索引页。数据页越多,索引页也就越多。
# 这么做是为了防止大量的数据情况下,主键目录里就要存储大量的数据页和最小主键值。浪费资源。
# 之后又将索引页多加一个层级出来,在这个更高的索引层级里,保存了每个索引页和索引页里的最小主键值。
# 这样我们在查询数据的时候,就可以先去最顶层的索引页中去找,之后通过二分查找定位到下一步去哪个索引页里找,最后定位到数据所在的数据页里。
# 那么为了防止最顶层的索引页里存放的下层索引号的页号也太多了,此时会再次分裂,再加一层索引页,就像是一棵树一样,即B+树。
# 索引的页存储物理结构,就是用B+树来实现的。
# 聚簇索引
# 1.从上面总结的,可以看出最下层的索引页都是有指针引用数据页的,索引实际上索引页和数据页之间是有指针连接起来的。
# 另外,索引页内部,对于一个层级内的索引页,互相之间都是基于指针组成双向链表的。
# 在这颗B+树里,最底层的一层就是数据页,即叶子节点。那么我们就把这种B+树索引结构里,叶子节点就是数据页本身的叫做聚簇索引。
# 2.在页分裂的时候或者插入数据的时候,都会对应的去维护你的上层索引数据结构,即不同的数据页和最小主键值,
# 然后随着数据页越来越多,一个索引页放不下了,此时就会再拉出新的索引页了,同时再搞一个上层的索引页,
# 上层索引页里存放的索引条目就是下层索引页的页号和最小主键值。通常而言,亿级的大表,基本上索引的层级也就3到4层左右。
# 3.聚簇索引默认是按照主键来组织的,所以在crud的时候,一方面更新数据页,另一方面也会自动维护B+树结构的聚簇索引。
# 4.聚簇索引是innodb存储引擎默认创建的一套基于主键的索引结构,并且表里的数据都是直接放到聚簇索引里,作为叶子节点的数据页。
# 所以我们可以看出,其实基于主键的搜索其实就是从聚簇索引的根节点开始进行二分查找,一路找到对应的数据页里,基于页目录直接定位到主键对应的数据就可以了。
# 二级索引
# 1.即对主键外的其他字段建立索引。
# 2.当我们对一些主键以外的其他字段比如name建立索引的时候,会重新建立一个B+树,叶子节点也是数据页,只不过这个数据页里只是放主键和name字段,
# 并且叶子节点的数据页的name值都是按照大小排序的,然后name字段的索引B+树也会构建多层级的索引页,这个索引页存放的就是下一层的页号和最小name字段的值,
# 规则都是一样的,但是要注意,我们查询name的B+树的时候,只能查到主键值和name值,并找不到这行数据所有的字段值,所以最后还需要回表,
# 即根据主键再去聚簇索引里从根节点开始,一路找到叶子节点,将所有的数据都取出来。当然联合索引比如name+age这种,原理一样。当我们执行crud的时候,需要维护所有的索引。
# 索引B+树的维护
# 1.表最开始只有一个数据页,这个数据页是属于聚簇索引的一部分,并且是空的。此时插入数据就往这里写入就可以了,也没必要弄索引页。
# 这个初始的数据页也叫跟页,每个数据页内部默认就有一个基于主键的页目录,所以此时根据主键来搜索是没问题的。
# 2.之后数据越来越多,数据页满了,就会新加一个数据页,然后把跟页里面的数据都拷贝到这里,同时再搞一个新的数据页,根据你的主键值的大小进行移动,
# 让两个新的数据页根据主键值排序,保证第二个>第一个。此时跟页就升级为索引页了,里面放的是两个数据页的页号和他们里面最小的主键值
# 3.随着数据再次增多,数据页也越来越多,一个索引页放不下了,索引页也分裂成了两个,此时跟页继续往上走一个层级引用了两个索引页。以此类推。
# 索引数量
# 索引虽然会提高效率,但是过多的索引也会导致维护成本过高,进而导致增删改的速度变差了。所以不建议搞太多索引。
# 覆盖索引
# 针对联合索引顺序是key(name1,name2,name3) ,而查询的又是select name1,name2,name3 from table name order by name1,name2,name3 的类似的语句,
# 这个时候,需要的字段值直接在索引树里就能提取出来,不需要回表到聚簇索引,这种查询方式就是覆盖索引。
# 那么这么情况是理想情况,如果真的必须要回表,也可以尽量使用limit where之类的语句限定一下回表到聚簇索引的次数。
# 几种规则
# 1.等值匹配规则
# 就是我们写的sql里,where条件里的几个字段都是基于等值来查询,都是用的等于号,而且字段的名称和顺序都和联合索引的一模一样,此时就是等值匹配规则。此时百分百可以用联合索引来查询的。
# 2.最左侧列匹配
# 就是假设联合索引顺序是key(name1,name2,name3),那么我们在查询的时候必须按照这个顺序来写,可以只查name1和name2,但是不能跨过name2去查name1和name3,这样事没法在索引里找的。
# 3.最左前缀匹配原则
# 即like ‘1%’ 是会走索引的,而like ‘%1’就不会走索引了。
# 4.范围查找规则
# 比如 age < 5也是可以走索引的,但是还有一个规则就是你的where语句里如果有范围查询,那只有对联合索引里最左侧的列进行范围查询才能用到索引。
# 5.等值匹配+范围匹配的规则
# 就是假设联合索引顺序是key(name1,name2,name3) 而sql为 where name1 = ‘ xx’ and name2 > 1 and name3 = 1 。
# 这种情况下,name1和name2都会走索引,但是name3不会走,即范围查询后的都不会去走索引。
# 6.order by使用索引
# 就是假设联合索引顺序是key(name1,name2,name3) 那么sql order by name1,name2,name3 desc 和 order by name1,name2,name3 asc都是会走索引的,
# 但是如果类似 order by name1 desc ,order by name2,name3 asc 就不可以。
# 7.group by 使用索引
# 与order by类似,最好也是按照联合索引里最左侧的字段开始,按顺序排列开来,这样的话就可以完美的运用索引来直接提取一组一组的数据了。
# 设计索引的原则
# 1.针对sql语句中的where条件、order by条件、group by条件去设计。即设计一个聚合索引,
# 尽量的让where、order by、group by后面跟的字段都是联合索引的最左侧开始的部分字段。尽量保证where、order by、group by语句都可以用上索引。
# 2.建立索引,最好要选择基数大的哪些字段,这样才会发挥出B+树快速二分查找的优势来,其次尽量对那些字段类型比较小的列来设计索引。
# 比如tinyint之类的,因为它字段类型小,那么这个字段本身占用的磁盘空间小,搜索的时候性能也会好一点。
# 那么如果确实需要对varchar这种里面值可能特别大的字段(比如name)设计索引,那么也没办法,可以换一种策略,即紧紧针对这个字段的前20个字符建立索引。
# 那么索引树里的这个字段的值就是会提取前20个字符而已。但是假如此时你用order by name或group by name 就不会用上索引了。
# 3.尽量不要让查询语句里条件加个函数,否则也不会用上索引。
# 4.索引不要设计太多,建议两三个联合索引就可以基本覆盖这个表的全部查询了。
# 5.建议大家主键要用自增的,别用uuid之类的。因为主键自增不会导致聚簇索引频繁的分裂,主键自增都是有序的,自然新增一个页就行了。
6. 事务
思路:四大特性->隔离级别->为什么默认RR而不是RC->脏读、不可重复读、幻读->
数据事务的实现原理->Spring的7种事务传播行为
# 事务四大特性
# 1. 原子性:事务是由一系列动作组成,原子性就是保证这些动作要么全部成功,要么都不起作用。
# 2. 一致性:一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
# 拿转账举例子,转帐前两个账户的总金额是100,那么转账之后总金额还要是100。
# 3. 隔离性:是当多个用户同一时刻访问数据库的同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
# 4. 持久性:是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
# 事务隔离级别以及可以避免的问题
# 1. 串行化:强制事务排序(串行化),不会互相冲突。每个读数据行增加共享锁。可避免脏读、不可重复读、幻读的发生。
# 2. 可重复读:当用户读取某范围数据行时,另一事务在此范围内插入新行,当用户再次读取此范围数据行时,读取到新的幻影行。
# 可避免脏读、不可重复读的发生。(mysql默认)新版MySQL采用Next-Key锁来解决幻读问题。
# 3. 读已提交:一个事务只能看到已提交事务所做的改变。可避免脏读的发生。(oracle、sqlserver默认)
# 4. 读未提交:所有事务可看到其他未提交事务的结果。最低级别,任何情况都无法保证。
# 为什么mysql用的是repeatable而不是read committed
# 1.在 5.0之前只有statement一种格式,而这种格式在读已提交(Read Commited)这个隔离级别下主从复制是有bug的,因此Mysql将可重复读(Repeatable Read)作为默认的隔离级别!
# 2.bug:在主库上面执行先删除后插入,但是在从库如果binlog为statement格式,记录的顺序就是先插入后删除,从库执行的顺序和主库不一致,最后主库有数据,从库的数据被删掉了。
# 为什么默认的隔离级别都会选用read commited
# 1. repeatable存在间隙锁会使死锁的概率增大
# 2. 在RR隔离级别下,条件列未命中索引会锁表!而在RC隔离级别下,只锁行。
# 说一下数据库中的脏读、不可重复读、幻读
# 1. 脏读:脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
# 2. 不可重复读:不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
# 3. 幻读:幻读是事务非独立执行时发生的一种现象。例:例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,
# 而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。
# 不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
# 幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)
# 数据事务的实现原理
# 我们这里以 MySQL 的 InnoDB 引擎为例来简单说一下。
# 1.MySQL InnoDB 引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志) 来保证事务的原子性。
# 2.MySQL InnoDB 引擎通过 锁机制、MVCC 等手段来保证事务的隔离性( 默认支持的隔离级别是 REPEATABLE-READ )。
# 保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。
# Spring的7种事务传播行为
# 1、propagation_required:默认事务类型,如果没有,就新建一个事务;如果有,就加入当前事务。适合绝大多数情况。
# 2、propagation_requires_new:如果没有,就新建一个事务;如果有,就将当前事务挂起。
# 3、propagation_nested:如果没有,就新建一个事务;如果有,就在当前事务中嵌套其他事务。
# 4、propagation_supports:如果没有,就以非事务方式执行;如果有,就使用当前事务。
# 5、propagation_not_supported:如果没有,就以非事务方式执行;如果有,就将当前事务挂起。即无论如何不支持事务。
# 6、propagation_never:如果没有,就以非事务方式执行;如果有,就抛出异常。
# 7、propagation_mandatory:如果没有,就抛出异常;如果有,就使用当前事务。
7. explain
思路:explain(id、select_type、table、partitions、type、possible_keys、key、
key_len、ref、rows、filtered、Extra)->常见的sql优化。
# id
# 每一个select都会有一个id,如果是特别复杂的sql,会有多个执行计划,可以用id来区分。id越大,优先级越高,越先执行。
# select_type
# 本条执行计划的查询类型 如:
# (1)primary:主查询
# 使用union 就会出现primary、subquery、union的情况 select * from t1 where id in (select id from t2) 那么t1就是主查询,t2就是子查询。
# 使用子查询也会出现primary、subquery的情况
# (2)subquery:子查询
# (3)union:使用了union
# (4)union result:使用了union多出来一个去重的计划
# (5)simple:一般的单表或者多表查询,都是simple
# table
# 本条执行计划操作的表名
# partitions
# 表分区
# type
# 访问类型。针对当前这个表的访问方法。比如const、ref、range、index、all等。
# 性能由好到差的排序:system-->const-->eq_ref-->ref-->ref_or_null-->index_merge-->index_subquery-->range-->index-->all
# 1.all:全表扫描,即扫描聚簇索引的叶子节点的所有数据。
# explain select * from film where rating ='G';
# 2.index:全索引扫描(Full Index Scan), index 与 ALL 区别为 index 类型只遍历索引树. MYSQL 遍历整个索引来查找匹配的行.
# (直接扫描二级索引的叶子节点。也就是扫描二级节点的所有数据。)]
# explain select title from film;
# 3.range:索引范围扫描, 常见于 '<', '<=', '>', '>=', 'between' 等操作符.
# 基于二级索引进行范围查询。
# explain select * from film where film_id > 100;
# 4.ref:使用非唯一性索引或者唯一索引的前缀扫描, 返回匹配某个单独值的记录行。
# select * from payment where customer_id = 10
# 5.eq_ref:类似ref, 区别就在使用的索引是唯一索引. 在联表查询中使用 primary key 或者 unique key 作为关联条件。(针对被驱动表如果基于主键进行等值匹配。)
# explain select * from film a left join film_text b on a.film_id = b.film_id;
# 6.const/system:当 MySQL 对查询某部分进行优化, 并转换为一个常量时, 使用这些类型访问. 如将主键置于 where 列表中,
# MySQL 就能将该查询转换为一个常量, system 是 const 类型的特例, 当查询的表只有一行的情况下使用 system。
# select * from film where film_id = 1;
# 7.null:MySQL 不用访问表或者索引就直接能到结果.
# explain select 1 from dual where 1;
# 8.ref_or_null:基于二级索引查询允许值为null。
# 9.index_merge:单表查询,基于多个索引提取数据后进行合并。
# possible_keys
# 可能会用到的索引,可能有多个。
# key
# 实际用到的索引。
# key_len
# 索引的长度。
# ref
# 使用某个字段的索引,进行等值匹配搜索的时候,跟索引列进行等值匹配的那个目标值的一些信息。
# rows
# 是预估通过别的索引或者别的方式访问这个表的时候,大概可能会读取到多少条数据
# filtered
# 经过搜索条件过滤后剩余数据的百分比
# Extra
# 一些额外的信息
# (1)Using join buffer(Block Nested Loop) 嵌套循环的意思 如:select * from t1 join t2
# (2)using where 一般是直接对一个表进行扫描没用到索引,然后where里好几个条件。或者用到了索引,但是除了索引以外还有其他的筛选条件。
# (3)using index
# (4)using temporary 使用临时表 如:select * from t1 union select * from t2 即把结果集放到临时表里去重。
# (5)using index condition 先在二级索引里找,之后查出来的结果再做筛选 如:select * from t1 where x1 > 'xxx' and x1 like 'xx%'
# (6)using filesort 主要用于排序时,如果排序字段有索引,那么直接去索引里查即可,如果没有,就要基于内存或者磁盘文件来排序。
# sql优化
# 1. 查询少用 *,改成查询哪些列就写出哪些列。
# 2. 少用not in,会导致放弃索引,可以改成not exists
# 3. 少用is null和is not null ,会导致放弃索引,因为索引不索引空值的,可以使用缺省值 之后使用a>0或a>' '代替a is not nul即不允许字段为空。
# 4. 使用like注意:a like ‘%100%’,可以使用a like '100%' or a like '%100' 这种方式会使用索引进行两个范围的查询。
# 5. 使用union all代替union 因为union会有一个去除重复记录的操作。
# 6. where后面的条件的顺序,比如sex =1 and name = '张三' cup占用率会小于 name = '张三' and sex = 1
# 7. 在FROM后面的表中的列表顺序会对SQL执行性能影响,在没有索引及ORACLE没有对表进行统计分析的情况下,ORACLE会按表出现的顺序进行链接,由此可见表的顺序不对时会产生十分耗服物器资源的数据交叉。(注:如果对表进行了统计分析,ORACLE会自动先进小表的链接,再进行大表的链接)
# 8. 对于索引的字段不要进行函数\表达式的处理,否则会导致放弃索引。
# 9. 查询的列上和order by的列上尽量要加上索引。
# 10. 避免where语句对字段进行null判断,可以加上缺省值
# 11. 避免使用or 改成使用union all
# 12. 避免使用in 改成使用between
# 13. 建立索引,索引列对应的数据尽量不要重复。一个表不要大于6个
# 14. 尽量使用数值型 少用字符型 这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。
# 15. 尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
# 16. 避免频繁创建和删除临时表,以减少系统表资源的消耗。
# 17. 在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。
# 18. 如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。
# 19. 尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。
8.Mysql的锁机制
思路:锁的分类->MyISAM 表锁->InnoDB 行锁->死锁
# 锁的分类
# 1.从对数据操作的类型分类
# (1)读锁(共享锁 S):
# (2)写锁(排他锁 X):
# 2.从对数据操作的粒度分类
# (1)表级锁:
# 开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低(MyISAM 和 MEMORY 存储引擎采用的是表级锁)
# (2)行级锁:
# 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高(InnoDB 存储引擎既支持行级锁也支持表级锁,但默认情况下是采用行级锁);
# (3)页面锁:
# 开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
# 从锁的角度来说表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用。
# 行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。
# MyISAM 表锁
# 1.MyISAM 的表锁有两种模式:
# (1)表共享读锁 (Table Read Lock):不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;
# (2)表独占写锁 (Table Write Lock):会阻塞其他用户对同一表的读和写操作;
# 2.MyISAM 表的读操作与写操作之间,以及写操作之间是串行的。当一个线程获得对一个表的写锁后, 只有持有锁的线程可以对表进行更新操作。
# 其他线程的读、 写操作都会等待,直到锁被释放为止。
# 默认情况下,写锁比读锁具有更高的优先级:当一个锁释放时,这个锁会优先给写锁队列中等候的获取锁请求,然后再给读锁队列中等候的获取锁请求。
# InnoDB 行锁
# 1.InnoDB 实现了以下两种类型的行锁:
# (1)共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
# (2)排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
# 2.为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB 还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁:
# (1)意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁。
# (2)意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的 IX 锁。
# 索引失效会导致行锁变表锁。比如 vchar 查询不写单引号的情况
# 3.加锁机制
# (1)乐观锁:
# 会“乐观地”假定大概率不会发生并发更新冲突,访问、处理数据过程中不加锁,
# 只在更新数据时再根据版本号或时间戳判断是否有冲突,有则处理,无则提交事务。
# 用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式
# (2)悲观锁:
# 会“悲观地”假定大概率会发生并发更新冲突,访问、处理数据前就加排他锁,
# 在整个数据处理过程中锁定数据,事务提交或回滚后才释放锁。
# 另外与乐观锁相对应的,悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。
# 4.锁模式(InnoDB有三种行锁的算法)
# (1)记录锁(Record Locks)
# 1> 单个行记录上的锁。对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项。
# 2> 比如:SELECT * FROM table WHERE id = 1 FOR UPDATE; 它会在 id=1 的记录上加上记录锁,以阻止其他事务插入,更新,删除 id=1 这一行。
# 3> 在通过 主键索引 与 唯一索引 对数据行进行 UPDATE 操作时,也会对该行数据加记录锁:UPDATE SET age = 50 WHERE id = 1; id 列为主键列或唯一索引列
# (2)间隙锁(Gap Locks)
# 1> 当我们使用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁。
# 对于键值在条件范围内但并不存在的记录,叫做“间隙”。InnoDB 也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。
# 2> 对索引项之间的“间隙”加锁,锁定记录的范围(对第一条记录前的间隙或最后一条将记录后的间隙加锁),不包含索引项本身。
# 其他事务不能在锁范围内插入数据,这样就防止了别的事务新增幻影行。
# 3> 间隙锁基于非唯一索引,它锁定一段范围内的索引记录。间隙锁基于下面将会提到的Next-Key Locking 算法,
# 请务必牢记:使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
# 4> SELECT * FROM table WHERE id BETWEN 1 AND 10 FOR UPDATE; 即所有在(1,10)区间内的记录行都会被锁住,
# 所有id 为 2、3、4、5、6、7、8、9 的数据行的插入会被阻塞,但是 1 和 10 两条记录行并不会被锁住。
# GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况
# (3)临键锁(Next-key Locks)
# 1> 临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。可以理解为一种特殊的间隙锁,也可以理解为一种特殊的算法。
# 2> 临键锁的主要目的,也是为了避免幻读(Phantom Read)。对于行的查询,都是采用该方法。如果把事务的隔离级别降级为RC,临键锁则也会失效。
# 3> 每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。
# 需要强调的一点是,InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。
# 4> FOR UPDATE仅适用于InnoDB,且必须在交易区块(BEGIN/COMMIT)中才能生效。
# 在进行事务操作时,通过“for update”语句,MySQL会对查询结果集中每行数据都添加排他锁,其他线程对该记录的更新与删除操作都会阻塞。排他锁包含行锁、表锁。
# InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
# 要测试锁定的状况,可以利用MySQL的Command Mode ,开二个视窗来做测试。
# 死锁
# 1.死锁产生
# (1)死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。
# (2)当事务试图以不同的顺序锁定资源时,就可能产生死锁。多个事务同时锁定同一个资源时也可能会产生死锁。
# (3)锁的行为和顺序和存储引擎相关。以同样的顺序执行语句,有些存储引擎会产生死锁有些不会——死锁有双重原因:真正的数据冲突;存储引擎的实现方式。
# 2.检测死锁
# 数据库系统实现了各种死锁检测和死锁超时的机制。InnoDB存储引擎能检测到死锁的循环依赖并立即返回一个错误。
# 3.死锁恢复
# 死锁发生以后,只有部分或完全回滚其中一个事务,才能打破死锁,InnoDB目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚。
# 所以事务型应用程序在设计时必须考虑如何处理死锁,多数情况下只需要重新执行因死锁回滚的事务即可。
# 4.外部锁得死锁检测
# 发生死锁后,InnoDB 一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务。
# 但在涉及外部锁,或涉及表锁的情况下,InnoDB 并不能完全自动检测到死锁, 这需要通过设置锁等待超时参数 innodb_lock_wait_timeout 来解决
# 5.死锁影响性能
# 死锁会影响性能而不是会产生严重错误,因为InnoDB会自动检测死锁状况并回滚其中一个受影响的事务。
# 在高并发系统上,当许多线程等待同一个锁时,死锁检测可能导致速度变慢。
# 有时当发生死锁时,禁用死锁检测(使用innodb_deadlock_detect配置选项)可能会更有效,这时可以依赖innodb_lock_wait_timeout设置进行事务回滚。
# 6.MyISAM避免死锁
# 在自动加锁的情况下,MyISAM 总是一次获得 SQL 语句所需要的全部锁,所以 MyISAM 表不会出现死锁。
# 7.InnoDB避免死锁
# (1)为了在单个InnoDB表上执行多个并发写入操作时避免死锁,可以在事务开始时通过为预期要修改的每个元祖(行)使用
# SELECT ... FOR UPDATE语句来获取必要的锁,即使这些行的更改语句是在之后才执行的。
# (2)在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁、更新时再申请排他锁,
# 因为这时候当用户再申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁
# (3)如果事务需要修改或锁定多个表,则应在每个事务中以相同的顺序使用加锁语句。
# 在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会
# (4)通过SELECT ... LOCK IN SHARE MODE获取行的读锁后,如果当前事务再需要对该记录进行更新操作,则很有可能造成死锁。
# (5)改变事务隔离级别
# 如果出现死锁,可以用 show engine innodb status;命令来确定最后一个死锁产生的原因。返回结果中包括死锁相关事务的详细信息,
# 如引发死锁的 SQL 语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等。据此可以分析死锁产生的原因和改进措施。
redis
1. redis数据结构
思路:String->Hash->List->Set->Zset->每种类型具体应用->每种类型的常用方法
# 1. String:
# (1)单值存储
# set key value
# (2)对象存储
# 比如将user表数据存入redis中
# set user:1 value(json格式数据) 或
# mset user:1:name 张三 user:1:age 22
# (3)分布式锁控制
# setnx product:10001 true # 返回1代表获取锁成功,0失败。
# del product:10001 # 释放锁
# set product:10001 true ex 10 nx # 设置键为product:10001的值为true,并给定时间,即ex为10s 防止程序意外终止导致死锁
# (4)计数器
# 业务场景 文章/视频的阅读量、播放量统计
# incr article:readcount:1001 # incr article:readcount:{文章id}
# (5)Web集群session共享
# spring session + redis实现session共享
# (6)分布式系统全局序列号
# incr orderId # redis自增方式获取唯一id值,不过这种方式会耗费资源,对于高并发来说不合适
# incrby orderId 1000 # redis批量生成序列号提升性能,这种方式一次获取1000个id,之后放入内存慢慢用,分布式系统中其它的线程再去获取实际上获取的是1001-2000的值也不会导致重复。
# 2. Hash:
# (1)对象存储
# hset user id 1 hset user name 张三 hset user age 22
# hmset user 1:name 张三 1:money 100 hmget user 1:name 1:money
# (2)单点登录
# 用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果
# (3)电商购物车
# 比如以用户id为key、商品id为field、商品数量为value 用户部门关系 以部门id为key、用户id为field、用户信息为value
# 3. List:
# (1)异步队列
# 将需要延后处理的任务结构体序列化成字符串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理。rpush生产消息,lpop消费消息。
# (2)秒杀抢购场景(防止超卖)
# 事先将商品放入list中,因为list的pop操作是原子性的,所以即使有多个用户同时请求,也是依次pop,list空了pop抛出异常就代表商品卖完了。
# (3)分页
# 可以利用lrange命令,做基于redis的分页功能。
# (4)模拟数据结构
# 先进后出 Stack(栈) = lpush + lpop
# 先进先出 Queue(队列) = lpush + rpop
# (5)监听
# Blocking MQ(阻塞队列) = lpush + brpop
# brpop/blpop key timeout 从key列表表尾/表头弹出一个元素,若列表中没有元素,阻塞等待timeout秒,如果timeout=0,则一直阻塞等待
# 4. Set:无序、唯一
# (1)去重
# 利用其唯一特性,可以用来做比如:抽奖活动,每个用户只能参与一次活动、一个用户只能中奖一次等等去重场景。不用jvm自带的set是因为如果系统是集群部署的,比较麻烦
# (2)交集并集差集
# 根据其取交集并集差集的方法,获取比如两个人共同好友,全部好友,独有好友等。
# (3)微信点赞、收藏等。
# 5. Zset:有序
# (1)多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。
2. 过期策略
思路:定期删除->惰性删除->定时删除->内存淘汰策略->两个误区
# 定期删除
# Redis每隔100ms就随机抽取一些设置了过期时间的key,判断其是否过期,如果过期就删除了。
# 惰性删除
# Redis的key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。
# 定时删除
# 在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除。
#
# 定期删除、惰性删除仍然会存在大量过期key堆积在内存中的情况。定时删除虽然内存会及时释放,但是十分消耗CPU资源。大并发下,CPU应该用在处理请求上而不是删除key上。
#
# 内存淘汰策略
# 1.volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
# 2.volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
# 3.volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
# 4.allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
# 5.allkeys-random:从数据集中任意选择数据淘汰
# 6.no-enviction(驱逐):禁止驱逐数据,新写入操作会报错
# 两个误区
# 1.数据莫名丢失
# Redis是基于内存的,而内存是有限的,假如内存容量全部存满,再继续往里存的时候,就会顶掉一部分数据,就会造成数据莫名其妙就丢失了
# 2.设置了过期时间不代表就会被删除
# 数据过期不代表就意味着会被从内存中删除掉,只是标记了一下。所以可能会造成时间到了,内存占用没有下降的原因。
3. 持久化
思路:AOF->RDB->各自的优缺点
# AOF
# 1.将每一个写命令通过文件追加的方式,写到文件里。
# 2.恢复速度慢,文件大、但是可以保证数据不丢失
# RDB(默认)
# 1.按照一定的时间周期策略把内存中的数据以快照的形式保存到硬盘的二进制文件中。
# 2.恢复速度快,文件小、但是会造成数据丢失。
# 可以同时开启,同时开启时默认AOF。
4. 主从复制
思路:主从架构->主从同步过程->主从复制的断点续传->全量、增量、异步复制->无磁盘化复制
# 主从架构设计思路
# 多个服务器,一主多从,主负责写入数据,从负责读取数据。
# 主从同步过程
# 1.先保证主服务器的开启,之后从服务器通过命令或者重启配置项可以同步到主服务器。
# 2.从服务器启动之后,读取同步的配置,根据配置决定是否使用当前数据响应客户端,然后发送SYNC命令。
# 当主服务器接收到同步命令时,会执行bgsave命令备份数据,但是主服务器此时不会拒绝客户端的读/写请求,而是将写命令写入缓存区。
# 从服务器未收到主服务器备份的快照文件的时候,会根据其配置决定使用现有数据响应客户端/拒绝。
# 3.bgsave命令执行完了之后,开始向从服务器发送备份文件,这个时候从服务器就会丢弃所有的现有数据,开始载入发送过来的快照文件。
# 4.当主服务器发送完备份文件后,从服务器就会执行这些写入命令。此时就会把bgsave执行之后的缓存区内的写命令也发送给从服务器,从服务器完成备份文件解析,就开始像往常一样,接收命令,等待命令写入。
# 5.缓冲区的命令发送完成后,当主服务器执行第一条写命令后,就同时往服务器发送同步写命令,从服务器就跟主服务器保持一致了。而此时当从服务器完成主服务器发送的缓冲区命令后,就开始等待主服务器的命令了。
# 断点续传
# 1.从 redis2.8 开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。
# 2.master node会在内存中维护一个 backlog,master和slave都会保存一个replica offset还有一个master run id,offset就是保存在 backlog中的。
# 如果 master和 slave网络连接断掉了,slave会让 master从上次 replica offset开始继续复制,如果没有找到对应的offset,那么就会执行一次全量复制。
# 3.如果根据 host+ip定位 master node,是不靠谱的,如果 master node重启或者数据出现了变化,那么 slave node应该根据不同的 run id区分。
# 全量、增量、异步复制
# 1.全量复制
# (1)master执行bgsave,在本地生成一份rdb快照文件
# (2)master node将rdb快照文件发送给salve node,如果rdb复制时间超过60秒(repl-timeout),那么slavenode就会认为复制失败,
# 可以适当调节大这个参数。对于千兆网卡的机器,一般每秒传输100MB,6G文件,很可能超过60s
# (3)master node在生成rdb时,会将所有新的写命令缓存在内存中,在salve node保存了rdb之后,再将新的写命令复制给salve node。
# client-output-buffer-limit slave 256MB 64MB 60,如果在复制期间,内存缓冲区持续消耗超过64MB,或者一次性超过256MB,那么停止复制,复制失败。
# (4)slave node接收到rdb之后,清空自己的旧数据,然后重新加载rdb到自己的内存中,同时基于旧的数据版本对外提供服务
# (5)如果slave node开启了AOF,那么会立即执行bg rewrite aof,重写AOF
# 2.增量复制
# (1)如果全量复制过程中,master-slave网络连接断掉,那么 slave重新连接 master时,会触发增量复制。
# (2)master直接从自己的 backlog中获取部分丢失的数据,发送给 slave node,默认 backlog就是 1MB
# (3)master就是根据 slave发送的 psync中的 offset来从 backlog中获取数据的。
# 3.异步复制
# master 每次接收到写命令之后,先在内部写入数据,然后异步发送给slave node。
# 无磁盘化复制
# master 在内存中直接创建RDB,然后发送给slave,不会在自己本地落地磁盘了。只需要在配置文件中开启repl-diskless-sync yes即可。
# 还有一个重要配置是 repl-diskless-sync-delay 5 即等待 5s 后再开始复制,因为要等更多 slave 重新连接过来。
5. 哨兵
思路:概念作用结构->数据丢失->处理办法->主观宕机、客观宕机->自动发现机制->选举算法->configuration epoch
# 概念作用结构
# 1.哨兵是 redis 集群机构中非常重要的一个组件。哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。哨兵至少需要三个实例。
# 2.哨兵集群必须部署2个以上节点。如果哨兵集群仅仅部署了个2个哨兵实例,那么它的majority就是2(2的majority=2,3的majority=2,5的majority=3,4的majority=2),
# 如果其中一个哨兵宕机了,就无法满足majority>=2这个条件,那么在master发生故障的时候也就无法进行主从切换。
# 3.主要负责监控redis master和slave进程是否正常工作。如果某个redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
# 如果master node挂掉了,会自动转移到slave node上。如果故障转移发生了,通知client客户端新的master地址。
# 主备切换的数据丢失问题
# 1.异步导致的数据丢失
# 因为 master->slave 的复制是异步的,所以可能有部分数据还没复制到 slave,master 就宕机了,此时这部分数据就丢失了。
# 2.脑裂导致的数据丢失
# 脑裂,也就是说,某个 master 所在机器突然脱离了正常的网络,跟其他 slave 机器不能连接,但是实际上 master 还运行着。
# 此时哨兵可能就会认为master 宕机了,然后开启选举,将其他 slave 切换成了 master。这个时候,集群里就会有两个 master ,也就是所谓的脑裂。
# 此时虽然某个 slave 被切换成了 master,但是可能 client 还没来得及切换到新的 master,还继续向旧 master 写数据。
# 因此旧 master 再次恢复的时候,会被作为一个 slave 挂到新的 master 上去,自己的数据会清空,重新从新的 master 复制数据。
# 而新的 master 并没有后来 client 写入的数据,因此,这部分数据也就丢失了。
# 主备切换导致数据丢失的处理办法
# 1.主要用到两个配置。min-slaves-to-write 1 min-slaves-max-lag 10
# 2.表示,要求同步的时候集群里至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒。如果说一旦所有的 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master 就不会再接收任何请求了。
# 3.有了min-slaves-max-lag这个配置,就可以确保说,一旦 slave 复制数据和 ack 延时太长,就认为可能 master 宕机后损失的数据太多了,
# 那么就拒绝写请求,这样可以把 master 宕机时由于部分数据未同步到 slave 导致的数据丢失降低的可控范围内。
# 4.如果一个 master 出现了脑裂,跟其他 slave 丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的 slave 发送数据,
# 而且 slave 超过 10 秒没有给自己 ack 消息,那么就直接拒绝客户端的写请求。因此在脑裂场景下,最多就丢失 10 秒的数据。
# 主观宕机sdown、客观宕机odown
# 首先先提两个概念
# (1)quorum: 确认odown的最少哨兵数量
# (2)majority: 授权进行主从切换的最少的哨兵数量
# 如果quorum < majority, 比如majority就是3,quorum设置为2,那么3个哨兵授权就可以进行主备切换。
# 如果quorum >= majority, 那么必须quorum数量的哨兵都授权,比如5个哨兵,quorum是5,那么必须5个哨兵都同一授权,才能进行主备切换。
# 1.sdown 是主观宕机,就一个哨兵如果自己觉得一个 master 宕机了,那么就是主观宕机
# sdown 达成的条件很简单,如果一个哨兵 ping 一个 master,超过了is-master-down-after-milliseconds指定的毫秒数之后,就主观认为 master 宕机了;
# 2.odown 是客观宕机,如果 quorum 数量的哨兵都觉得一个 master 宕机了,那么就是客观宕机
# 如果一个哨兵在指定时间内,收到了 quorum 数量的其它哨兵也认为那个 master 是 sdown 的,那么就认为是 odown 了。
# 哨兵之间的自动发现机制
# 1.哨兵互相之间的发现,是通过 redis 的pub/sub系统实现的,每个哨兵都会往__sentinel__:hello这个 channel 里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在。
# 2.每隔两秒钟,每个哨兵都会往自己监控的某个 master+slaves 对应的__sentinel__:hellochannel 里发送一个消息,内容是自己的 host、ip 和 runid 还有对这个 master 的监控配置。
# 3.每个哨兵也会去监听自己监控的每个 master+slaves 对应的__sentinel__:hellochannel,然后去感知到同样在监听这个 master+slaves 的其他哨兵的存在。
# 4.每个哨兵还会跟其他哨兵交换对master的监控配置,互相进行监控配置的同步。
# 5.哨兵会负责自动纠正 slave 的一些配置,比如 slave 如果要成为潜在的 master 候选人,哨兵会确保 slave 复制现有 master 的数据;
# 如果slave连接到了一个错误的master上,比如故障转移之后,那么哨兵会确保它们连接到正确的 master 上。
# slave-master的选举算法
# 如果一个 master 被认为 odown 了,而且 majority 数量的哨兵都允许主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个 slave 来,会考虑 slave 的一些信息:
# 1.跟 master 断开连接的时长
# 2.slave 优先级
# 3.slave的replicate offset大小,越大,优先级越高
# 4.run id大小,越小,优先级越高。
# 如果一个 slave 跟 master 断开连接的时间已经超过了down-after-milliseconds的 10 倍,外加 master 宕机的时长,那么 slave 就被认为不适合选举为 master。
# (down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state
# 接下来会对 slave 进行排序:
# 1.按照 slave 优先级进行排序,slave priority 越低,优先级就越高。
# 2.如果 slave priority 相同,那么看 replica offset,哪个 slave 复制了越多的数据,offset 越靠后,优先级就越高。
# 3.如果上面两个条件都相同,那么选择一个 run id 比较小的那个 slave。
# configuration epoch
# 哨兵会对一套 redis master+slave进行监控,会有相应的监控的配置
# 执行切换的按个哨兵,会从要切换到新master (slave->master) 那里得到一个configuration epoch,这就是version号,
# 每次切换的version都是唯一的,如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout时间,然后接替继续执行切换,此时会重新获取一个新的configuration epoch,作为新的version号。
6. 单线程模型原理
思路:文件事件处理器介绍->客户端与redis通信的一次流程
# 文件事件处理器
# Redis基于reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器,file event handler,这个文件事件处理器是单线程的,因此Redis才叫做单线程的模型。
# 文件事件处理器的结构包含4个部分:多个socket,IO多路复用程序,文件事件分派器,事件处理器等。
# (1)多个socket:与客户端的socket请求建立连接
# (2)IO多路复用程序:监听多个socket,并把socket放入到一个队列中排队
# (3)文件事件分派器:文件事件分派器每次从队列中取出一个socket,并分给对应的事件处理器
# (4)事件处理器:处理请求
# 1>如果是客户端要连接redis,那么会为socket关联连接应答处理器
# 2>如果是客户端要写数据到redis,那么会为socket关联命令请求处理器
# 3>如果是客户端要从redis读数据,那么会为socket关联命令回复处理器
# 总结: 每次我们一个socket请求过来 和 redis中的 server socket建立连接后,通过IO多路复用程序,就会往队列中插入一个socket,文件事件分派器就是将队列中的socket取出来,分派到对应的处理器,
# 在处理器处理完成后,才会从队列中在取出一个。这里也就是用一个线程,监听了客户端的所有请求,被称为Redis的单线程模型。
# 客户端与redis通信的一次流程
# 1.redis启动初始化的时候,redis会将连接应答处理器跟AE_READABLE事件关联起来,
# 2.接着如果一个客户端跟redis发起连接,此时会产生一个AE_READABLE事件,然后由连接应答处理器来处理跟客户端建立连接,创建客户端对应的socket,同时将这个socket的AE_READABLE事件跟命令请求处理器关联起来。
# 3.当客户端向redis发起请求的时候(不管是读/写,都一样),首先就会在socket产生一个AE_READABLE事件,然后由对应的命令请求处理器来处理。这个命令请求处理器就会从socket中读取请求相关数据,然后进行执行和处理。
# 4.接着redis这边准备好了给客户端的响应数据之后,就会将socket的AE_WRITABLE事件跟命令回复处理器关联起来,当客户端这边准备好读取响应数据时,就会在socket上产生一个AE_WRITABLE事件,会由对应的命令回复处理器来处理,
# 就是将准备好的响应数据写入socket,供客户端来读取。
# 5.命令回复处理器写完之后,就会删除这个socket的AE_WRITABLE事件和命令回复处理器的关联关系。
7. 熟悉缓存与数据库双写一致性
思路:两种有问题的更新策略->延时双删->延时双删+重试机制
# 两种有问题的更新策略
# 1.先更新/删除数据库,再更新/删除缓存
# 这种方式是存在问题的,不建议采用,问题在于,如果数据库被成功更新/删除之后,此时由于网络问题或者程序报错等问题导致缓存没有被成功的更新/删除。那么就会造成数据库和缓存中的值不一致的情况。
# 2.先删除缓存,再更新数据库
# 此种方式乍一看可以解决第一种策略的问题,但是还是会有问题的,比如线程A刚刚删除完缓存中的数据,还没来得及更新数据库的时候,线程B进来,发现缓存中已经没有了对应的数据,
# 就去数据库中查出了未被更新的数据并存入了缓存中,之后数据库中原旧数据才会被删除。那么此时也会出现数据库和缓存中的值不一致的情况。
#
# 延时双删
# 1.先删除缓存。
# 2.更新数据库。
# 3.过比如1s 再删除缓存。这个1s是根据业务接口的处理时间+几百毫秒确定的。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
#
# 延时双删+重试机制
# 延时双删这么做可能会影响吞吐量。处理办法是将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后了,再返回。这么做,加大吞吐量。
# 那么如果第二次删除失败了。可以使用一套重试机制
# 1.更新数据库数据
# 2.缓存因为种种问题删除失败
# 3.将需要删除的key发送至消息队列
# 4.自己消费消息,获得需要删除的key
# 5.继续重试删除操作,直到成功
8. 缓存雪崩、缓存穿透、缓存击穿技术方案
思路:缓存雪崩->缓存穿透->缓存击穿
# 缓存雪崩
# 1.当缓存服务器重启或大量缓存在一个时间段都同时失效,那么请求来的时候在缓存中查不到,会再去数据库中查,请求过多就可能会导致系统崩溃。这就是缓存雪崩。
# 2.举个例子:对于系统 A,假设每天高峰期每秒5000个请求,本来缓存在高峰期可以扛住每秒4000个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时1秒5000个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。
# 此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。这就是缓存雪崩。
# 3.缓存雪崩的事前事中事后的解决方案如下。
# 事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。
# 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
# 事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
# 用户发送一个请求,系统 A 收到请求后,先查本地 ehcache 缓存,如果没查到再查 redis。如果 ehcache 和 redis 都没有,再查数据库,将数据库中的结果,写入ehcache和redis 中。
# 限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?走降级!可以返回一些默认的值,或者友情提示,或者空白的值。
# 好处:
# (1)数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。
# (2)只要数据库不死,就是说,对用户来说,2/5 的请求都是可以被处理的。
# (3)只要有 2/5 的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来一次。
# 缓存穿透
# 1.正常的缓存系统都是先根据key去缓存中查找,查到了就返回,查不到就再去数据库中查,那么一些恶意的请求会故意查询大量的缓存中没有的key,
# 这就会使得这些请求都去访问数据库,请求量越大,后端系统的压力就越大,最后就很有可能导致崩溃,这就叫做缓存穿透。
# 2.举个例子:对于系统A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。比如查的id都是负数
# 这样的话,缓存中不会有,请求每次都直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。
# 3.解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如set -999 UNKNOWN。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。
# 缓存击穿
# 1.跟缓存雪崩类似,区别在于这里针对某一个key缓存,而雪崩是针对很多key。
# 2.举个例子:缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
# 3.解决方式也很简单,可以将热点数据设置为永远不过期;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。
9.redis的Cluster工作原理
思路:介绍->分布式寻址算法->高可用和主备切换原理->gossip协议->节点间的内部通讯机制->集群模式的工作原理
# 介绍
# 1.redis cluster,主要是针对海量数据+高并发+高可用的场景。redis cluster 支撑 N 个 redis master node,每个 master node 都可以挂载多个 slave node。
# 这样整个 redis 就可以横向扩容了。如果你要支撑更大数据量的缓存,那就横向扩容更多的 master 节点,每个 master 节点就能存放更多的数据了。
# 换句话说就是,自动将数据进行分片,每个 master 上放一部分数据。提供内置的高可用支持,部分 master 不可用时,还是可以继续工作的。
# 2.在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,
# cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus用了另外一种二进制的协议,gossip协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。
# 分布式寻址算法
# 1. hash 算法(大量缓存重建)
# 来了一个 key,首先计算 hash 值,然后对节点数取模。然后打在不同的 master 节点上。一旦某一个 master 节点宕机,所有请求过来,都会基于最新的剩余 master 节点数去取模,尝试去取数据。
# 这会导致大部分的请求过来,全部无法拿到有效的缓存,导致大量的流量涌入数据库。
# 2.一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
# 一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。
# 来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,遇到的第一个 master 节点就是 key 所在位置。
# 在一致性哈希算法中,如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。
# 燃鹅,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。
# 这样就实现了数据的均匀分布,负载均衡。
# 3.redis cluster 的 hash slot 算法
# redis cluster 有固定的16384个 hash slot,对每个key计算CRC16值,然后对16384取模,可以获取 key 对应的 hash slot。
# redis cluster 中每个 master 都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot。
# hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。
# 移动 hash slot 的成本是非常低的。客户端的 api,可以对指定的数据,让他们走同一个 hash slot,通过hash tag来实现。
# 任何一台机器宕机,另外两个节点,不影响的。因为 key 找的是 hash slot,不是机器。
# Redis cluster的高可用和主备切换原理
# redis cluster的高可用原理基本跟哨兵是一样的
# 1.判断节点宕机
# 主观宕机:如果一个节点认为另外一个节点宕机,那么就是pfail
# 客观宕机:如果多个节点都认为另外一个节点宕机了,那么就是fail
# 在cluster-node-timeout内,某个节点一直没有返回pong,那么就被认为pfail。
# 如果一个节点认为某个节点pfail了,那么会在gossip ping消息中,ping给其他节点,如果超过半数的节点都认为pfail了,那么就会变成fail。
# 2.从节点过滤
# 对宕机的 master node,从其所有的 slave node 中,选择一个切换成 master node。
# 检查每个 slave node 与 master node 断开连接的时间,如果超过了cluster-node-timeout * cluster-slave-validity-factor,那么就没有资格切换成master。
# 3.从节点选举
# 每个从节点,都根据自己对 master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。
# 所有的 master node 开始 slave 选举投票,给要进行选举的 slave 进行投票,如果大部分 master node(N/2 + 1)都投票给了某个从节点,那么选举通过,那个从节点可以切换成 master。从节点执行主备切换,从节点切换为主节点。
# 4.与哨兵比较
# 整个流程跟哨兵相比,非常类似,所以说,redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。
# gossip协议
# gossip 协议包含多种消息,包含ping,pong,meet,fail等等。
# 1.meet:某个节点发送 meet 给新加入的节点,让新节点加入集群中,然后新节点就会开始与其它节点进行通信。redis-trib.rb add-node。其实内部就是发送了一个 gossip meet消息给新加入的节点,通知那个节点去加入我们的集群。
# 2.ping:每个节点都会频繁给其它节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换元数据。
# 3.pong:返回 ping 和 meeet,包含自己的状态和其它信息,也用于信息广播和更新。
# 4.fail:某个节点判断另一个节点 fail 之后,就发送 fail 给其它节点,通知其它节点说,某个节点宕机啦。
# ping消息深入
# ping 时要携带一些元数据,如果很频繁,可能会加重网络负担。
# 每个节点每秒会执行 10 次 ping,每次会选择 5 个最久没有通信的其它节点。当然如果发现某个节点通信延时达到了cluster_node_timeout / 2,那么立即发送 ping,避免数据交换延时过长,落后的时间太长了。
# 比如说,两个节点之间都 10 分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以cluster_node_timeout可以调节,如果调得比较大,那么会降低 ping 的频率。
# 每次 ping,会带上自己节点的信息,还有就是带上 1/10 其它节点的信息,发送出去,进行交换。至少包含3个其它节点的信息,最多包含总节点数减2个其它节点的信息。
# redis cluster节点间的内部通讯机制
# 集群元数据的维护有两种方式:集中式、Gossip协议。redis cluster节点间采用 gossip协议进行通信。
# 1.集中式
# (1)是将集群元数据(节点信息、故障等等)几种存储在某个节点上。集中式元数据集中存储的一个典型代表,就是大数据领域的storm。它是分布式的大数据实时计算引擎,是集中式的元数据存储的结构,
# 底层基于 zookeeper(分布式协调的中间件)对所有元数据进行存储维护。
# (2)好处在于,元数据的读取和更新,时效性非常好,一旦元数据出现了变更,就立即更新到集中式的存储中,其它节点读取的时候就可以感知到
# (3)不好在于,所有的元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力。
# 2.gossip协议
# (1)所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。
# (2)好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到所有节点上去更新,降低了压力
# (3)不好在于,元数据的更新有延时,可能导致集群中的一些操作会有一些滞后。
# 1> 10000 端口:每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如 7001,那么用于节点间通信的就是 17001端口。
# 每个节点每隔一段时间都会往另外几个节点发送ping消息,同时其它几个节点接收到ping之后返回pong。
# 2> 交换的信息:信息包括故障信息,节点的增加和删除,hash slot 信息等等。
# redis集群模式的工作原理
# 1.自动将数据进行分片,每个master上放一部分数据.提供内置的高可用支持,部分master不可用时,还是可以继续工作的。
# 支撑N个redis master node,每个master node都可以挂载多个slave node高可用,因为每个master都有salve节点,那么如果mater挂掉,redis cluster这套机制,就会自动将某个slave切换成master
# 2.如果你的数据量很少,主要是承载高并发高性能的场景,比如你的缓存一般就几个G,单机足够了
# replication,一个mater,多个slave,要几个slave跟你的要求的读吞吐量有关系,然后自己搭建一个sentinal集群,去保证redis主从架构的高可用性,就可以了
# 3.如果你的数据量很大,redis cluster,主要是针对海量数据+高并发+高可用的场景,海量数据,如果你的数据量很大,那么建议就用redis cluster
10.你们公司redis是怎么部署的
思路:机器个数->机器配置->怎么保证的高可用->redis里大概数据量
# 1.redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例,每个主实例挂了一个从实例,
# 5个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒5万,5台机器最多是25万读写请求/s。
# 2.机器的配置。32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的是10g内存,一般线上生产环境,redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。5台机器对外提供读写,一共有 50g 内存。
# 3.因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。
# 4.你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。
elasticsearch
1. elasticsearch的节点
思路: 解释节点->master节点->data节点->Client节点
# 节点就是es的实例。ES集群由若干节点组成,这些节点在同一个网络内,cluster-name相同。共分为:
# 1. master节点:它负责 维护索引元数据、切换 primary shard 和 replica shard 身份、管理集群范畴的变更,例如创建或删除索引,添加节点到集群或从集群删除节点。
# master节点无需参与文档层面的变更和搜索,这意味着仅有一个master节点并不会因流量增长而成为瓶颈。任意一个节点都可以成为 master 节点
要是 master 节点宕机了,那么会重新选举一个节点为 master 节点。
# 2. data节点:持有数据和倒排索引。默认情况下,每个节点都可以通过设定配置文件elasticsearch.yml中的node.data属性为true(默认)成为数据节点。
# 如果需要一个专门的主节点,应将其node.data属性设置为false
# 3. Client节点:如果将node.master属性和node.data属性都设置为false,那么该节点就是一个客户端节点,扮演一个负载均衡的角色,将到来的请求路由到集群中的各个节点。
2. elasticsearch的分片
思路: 分片原因->分片的类别和作用
# 分片原因
# 1.单个节点由于物理机硬件限制,存储的文档是有限的,如果一个索引包含海量文档,则不能在单个节点存储。
# 2.ES提供分片机制,同一个索引可以存储在不同分片(数据容器)中,这些分片又可以存储在集群中不同节点上。
# 分片的类别和作用
# 1.分片分为主分片(primary shard)以及从分片(replica shard)。
# (1)主分片:
# 索引中的主分片的数量在索引创建后就固定下来了,但是从分片的数量可以随时改变。
# (2)从分片:
# 从分片只是主分片的一个副本,它用于提供数据的冗余副本。
# 从分片作用是,在硬件故障时提供数据保护,同时服务于搜索和检索这种只读请求。
# 2.默认设置5个主分片和一组从分片(即每个主分片有一个从分片对应),但是从分片没有被启用(主从分片在同一个节点上没有意义),因此集群健康值显示为黄色(yellow)。
3. 倒排索引
思路:倒排索引含义->倒排索引分析->lucense
# 含义
# 1.倒排索引,用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。它是文档检索系统中最常用的数据结构。
# 分析
# 1.简单来说:将每个文档的内容分成若干个词,并将词和文档 ID 建立映射关系。如:每个词就相当于一个key,包含这个词的文档集合相当于value。之后将包含这些文档返回给用户就可以了。
# 注意:倒排索引中的词项根据字典顺序升序排列
# lucense
# 1.介绍
# 简单来说,lucene 就是一个 jar 包,里面包含了封装好的各种建立倒排索引的算法代码。
# 我们用 Java 开发的时候,引入 lucene jar,然后基于 lucene 的 api 去开发就可以了。
# 通过 lucene,我们可以将已有的数据建立索引,lucene 会在本地磁盘上面,给我们组织索引的数据结构。
# 2.内部结构
# (1)索引(Index): 在Lucene中一个索引是放在一个文件夹中的。
# (2)段(Segment): 一个索引可以包含多个段,段与段之间是独立的,添加新文档可以生成新的段,不同的段可以合并。
# (3)segments.gen和segments_X是段的元数据文件,也即它们保存了段的属性信息。
# (4)文档(Document): 文档是我们建索引的基本单位,不同的文档是保存在不同的段中的,一个段可以包含多篇文档。 新添加的文档是单独保存在一个新生成的段中,随着段的合并,不同的文档合并到同一个段中。
# (5)域(Field): 一篇文档包含不同类型的信息,可以分开索引,比如标题,时间,正文,作者等,都可以保存在不同的域里。 不同域的索引方式可以不同,在真正解析域的存储的时候,我们会详细解读。
# (6)词(Term): 词是索引的最小单位,是经过词法分析和语言处理后的字符串。
4. elasticsearch的分布式架构原理
思路: 先说假设环境->shard->非master节点宕机->master节点宕机
# 1.这里首先假设有三台机器 机器1,机器2,机器3,每台机器上都部署一个es节点,es 集群多个节点,会自动选举一个节点为 master 节点,这个 master 节点其实就是干一些管理的工作的,
# 比如维护索引元数据、负责切换 primary shard 和 replica shard 身份等。要是 master 节点宕机了,那么会重新选举一个节点为 master 节点。
# 2.创建一个索引,将这个索引分为多个shard,比如三个,每个shard存储这个索引的部分数据,并分别放在三台机器上。这样,所有的操作,都会在多台机器上并行分布式执行,提高了吞吐量和性能。
# 3.接着就是这个 shard 的数据实际是有多个备份,就是说每个 shard 都有一个 primary shard,负责写入数据,但是还有几个 replica shard。
# primary shard 写入数据之后,会将数据同步到其他几个 replica shard 上去。
# 一般来说一个shard 的 primary shard和replica shard是不在一台机器上的。
# 这样,每个 shard 的数据都有多个备份,如果某个机器宕机了,没关系啊,还有别的数据副本在别的机器上呢。高可用了吧。
# 4.如果是非 master节点宕机了,那么 master 节点会让那个宕机节点上的 primary shard 的身份转移到其他机器上的 replica shard。
# 接着你要是修复了那个宕机机器,重启了之后,master 节点会将原来的primary shard改成replica shard。
# 5.如果是master节点宕机了,那么会重新选举出一个es节点作为master节点,等到原master节点恢复后,也不会重新变成master节点了。
# 新master节点会将旧master节点上的primary shard身份变成replica shard。
5. elasticsearch的读写删除更新数据的原理
思路: es读数据的工作原理->es写数据的工作原理->es删除/更新数据底层原理
# es读数据的工作原理
# 1. 客户端发送请求到一个 coordinate node(协调节点)。
# 2. coordinate node 对 doc id 进行哈希路由,将请求转发到对应的 node,此时会使用 round-robin 随机轮询算法,在 primary shard 以及其所有 replica 中随机选择一个,让读请求负载均衡。
# 3. 每个 shard 将自己的搜索结果(其实就是一些 doc id)返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。
# 4. 接着由协调节点根据 doc id 去各个节点上拉取实际的 document 数据,最终返回给客户端。
# 5. 写请求是写入 primary shard,然后同步给所有的 replica shard;读请求可以从 primary shard 或 replica shard 读取,采用的是随机轮询算法。
# es写数据的工作原理
# 1.简单说法
# (1).客户端选择一个 node 发送请求过去,这个 node 就是 coordinating node(协调节点)。
# (2).coordinating node 对 document 进行路由,将请求转发给对应的 node(primary shard)。
# (3).primary shard 处理请求,然后将数据同步到 replica node。
# (4).coordinating node 如果发现 primary node 和所有 replica node 都搞定之后,就返回响应结果给客户端。
# 2.详细说法
# 能背下来看这个
# (1).先写入内存 buffer,在 buffer 里的时候数据是搜索不到的;同时将数据写入 translog 日志文件。
# (2).如果 buffer 快满了,或者到一定时间(一般是1s),就会将内存 buffer 数据 refresh 到一个新的 segment file 中,
# 但是此时数据不是直接进入 segment file 磁盘文件,而是先进入 (操作系统的缓存)os cache 。这个过程就是 refresh。
# 即每秒都产生一个新的磁盘文件(segment file)。但是如果 buffer 里面此时没有数据,那当然不会执行 refresh 操作。
# 只要 buffer 中的数据被 refresh 操作刷入 os cache中,这个数据就可以被搜索到了。
# 只要数据被输入 os cache 中,buffer 就会被清空了,因为不需要保留 buffer 了,数据在 translog 里面已经持久化到磁盘去一份了。
# 所以说es是准实时的。有个1s的延迟,可以通过代码手动refresh。
# (3).随着这个过程推进,translog 会变得越来越大。当 translog 达到一定长度的时候,就会触发 commit 操作。
# 第一步,就是将 buffer 中现有数据 refresh 到 os cache 中去,清空 buffer。
# 然后,将一个 commit point 写入磁盘文件,里面标识着这个 commit point 对应的所有 segment file,同时强行将 os cache 中目前所有的数据都 fsync 到磁盘文件中去。
# 最后清空 现有 translog 日志文件,重启一个 translog,此时 commit 操作完成。这个 commit 操作叫做 flush。
# 默认 30 分钟自动执行一次 flush,但如果 translog 过大,也会触发 flush。
# flush 操作就对应着 commit 的全过程,我们可以通过 es api,手动执行 flush 操作,手动将 os cache 中的数据 fsync 强刷到磁盘上去。
# (4).translog 日志文件的作用是什么?你执行 commit 操作之前,数据要么是停留在 buffer 中,要么是停留在 os cache 中,
# 无论是 buffer 还是 os cache 都是内存,一旦这台机器死了,内存中的数据就全丢了。
# 所以需要将数据对应的操作写入一个专门的日志文件 translog 中,一旦此时机器宕机,再次重启的时候,es 会自动读取 translog 日志文件中的数据,恢复到内存 buffer 和 os cache 中去。
# (5).translog 其实也是先写入 os cache 的,默认每隔 5 秒刷一次到磁盘中去,所以默认情况下,可能有 5 秒的数据会仅仅停留在 buffer 或者 translog 文件的 os cache 中,
# 如果此时机器挂了,会丢失 5 秒钟的数据。但是这样性能比较好,最多丢 5 秒的数据。也可以将 translog 设置成每次写操作必须是直接 fsync 到磁盘,但是性能会差很多。
# (6).数据写入 segment file 之后,同时就建立好了倒排索引。
# 背不下来看这个
# (1).数据先写入内存 buffer,然后每隔 1s,将数据 refresh 到 os cache,到了 os cache 数据就能被搜索到(所以我们才说 es 从写入到能被搜索到,中间有 1s 的延迟)。
# (2).每隔 5s,将数据写入 translog 文件(这样如果机器宕机,内存数据全没,最多会有 5s 的数据丢失),
# (3).translog 大到一定程度,或者默认每隔 30mins,会触发 commit 操作,将缓冲区的数据都 flush 到 segment file 磁盘文件中。
# es删除/更新数据底层原理
# 1.如果是删除操作,commit 的时候会生成一个 .del 文件,里面将某个 doc 标识为 deleted 状态,那么搜索的时候根据 .del 文件就知道这个 doc 是否被删除了。
# 2.如果是更新操作,就是将原来的 doc 标识为 deleted 状态,然后新写入一条数据。
# 3.buffer 每 refresh 一次,就会产生一个 segment file,所以默认情况下是 1 秒钟一个 segment file,这样下来 segment file 会越来越多,此时会定期执行 merge。
# 每次 merge 的时候,会将多个 segment file 合并成一个,同时这里会将标识为 deleted 的 doc 给物理删除掉,然后将新的 segment file 写入磁盘,
# 这里会写一个 commit point,标识所有新的 segment file,然后打开 segment file 供搜索使用,同时删除旧的 segment file。
6. elasticsearch的master的选举机制
思路:master-eligible node->脑裂->选举的发起->选举谁->什么时候选举成功
# 1.master-eligible node
# 当配置文件中配置的node.master:true的时候,表示这个node是一个master的候选节点,可以参与选举。也被叫做master-eligible node。
# 2.脑裂
# (1)上面提到了,集群中可能会有多个master-eligible node,此时就要进行master选举。保证只有一个能当选master,如果多个当选了,就会出现脑裂情况,破坏数据的一致性。
# (2)为了避免产生脑裂,ES采用了常见的分布式系统思路,保证选举出的master被多数派(quorum)的master-eligible node认可,以此来保证只有一个master。
# 这个quorum通过以下配置进行配置:conf/elasticsearch.yml: discovery.zen.minimum_master_nodes:2
# (3)上面的听上去合理,可以解决脑裂的问题,但是有bug。因为上述流程并没有限制在选举过程中,一个Node只能投一票,那么什么场景下会投两票呢?
# 比如NodeB投NodeA一票,但是NodeA迟迟不成为Master,NodeB等不及了发起了下一轮选主,这时候发现集群里多了个Node0,Node0优先级比NodeA还高,那NodeB肯定就改投Node0了。
# 假设Node0和NodeA都处在等选票的环节,那显然这时候NodeB其实发挥了两票的作用,而且投给了不同的人。
# 解决办法如下:
# 比如raft算法中就引入了选举周期(term)的概念,保证了每个选举周期中每个成员只能投一票,如果需要再投就会进入下一个选举周期,term+1。
# 假如最后出现两个节点都认为自己是master,那么肯定有一个term要大于另一个的term,而且因为两个term都收集到了多数派的选票,
# 所以多数节点的term是较大的那个,保证了term小的master不可能commit任何状态变更(commit需要多数派节点先持久化日志成功,由于有term检测,不可能达到多数派持久化条件)。
# 这就保证了集群的状态变更总是一致的。
# 3.选举的发起
# (1)当一个节点发现包括自己在内的多数派的master-eligible节点认为集群没有master时,就可以发起master选举。
# (2)发起的条件如下:
# 1> 该master-eligible节点的当前状态不是master。
# 2> 该master-eligible节点通过ZenDiscovery模块的ping操作询问其已知的集群其他节点,没有任何节点连接到master。
# 3> 包括本节点在内,当前已有超过minimum_master_nodes个节点没有连接到master。
# 4.选举谁
# (1)先看节点的clusterStateVersion,值越大,优先级越高。这是为了保证新Master拥有最新的clusterState(即集群的meta),避免已经commit的meta变更丢失。
# 因为Master当选后,就会以这个版本的clusterState为基础进行更新。(一个例外是集群全部重启,所有节点都没有meta,需要先选出一个master,然后master再通过持久化的数据进行meta恢复,再进行meta同步)。
# (2)当clusterStateVersion相同时,节点的Id越小,优先级越高。即总是倾向于选择Id小的Node,这个Id是节点第一次启动时生成的一个随机字符串。
# 之所以这么设计,应该是为了让选举结果尽可能稳定,不要出现都想当master而选不出来的情况。
# 5.什么时候选举成功
# 假设Node_A选Node_B当Master,Node_A会向Node_B发送join请求,那么此时
# (1)如果Node_B已经成为Master,Node_B就会把Node_A加入到集群中,然后发布最新的cluster_state, 最新的cluster_state就会包含Node_A的信息。
# 相当于一次正常情况的新节点加入。对于Node_A,等新的cluster_state发布到Node_A的时候,Node_A也就完成join(选票)了。
# (2)如果Node_B在竞选Master,那么Node_B会把这次join当作一张选票。
# 对于这种情况,Node_A会等待一段时间,看Node_B是否能成为真正的Master,直到超时或者有别的Master选成功。
# (3)如果Node_B认为自己不是Master(现在不是,将来也选不上),那么Node_B会拒绝这次join。对于这种情况,Node_A会开启下一轮选举。
# 假设Node_A选自己当Master:
# 此时NodeA会等别的node来join,即等待别的node的选票,当收集到超过半数的选票时,认为自己成为master,
# 然后变更cluster_state中的master node为自己,并向集群发布这一消息。
7. elasticsearch的优化
思路:设计阶段调优->写入调优->查询调优->数十亿级别如何提高查询效率
# 设计阶段调优
# 1.根据业务每天增量,采取基于日期模板创建索引。
# 2.使用别名进行索引管理。
# (1)使用别名可以提高检索效率。
# 比如索引 index_2021-1-1(别名=index_2021-1) index_2021-1-2(别名=index_2021-1) index_2021-2-1(别名=index_2021-2)
# (2)简化从Elasticsearch中删除数据的过程。
# (3)用户无感知的重建索引。即使用别名指向更改前和更改后的索引。
# (4)但是要注意es批量插入不能使用别名,会报错。
# 3.针对需要分词的字段,合理的设置分词器。
# 4.Mapping阶段充分结合各个字段的属性,是否需要检索、是否需要存储等。
# 写入调优
# 1.写入前副本数设置为0;
# 2.写入前关闭refresh_interval设置为-1,禁用刷新机制;
# 3.写入过程中:采取bulk批量写入;
# 4.写入后恢复副本数和刷新间隔;
# 5.尽量使用自动生成的id。
# 查询调优
# 1.禁用wildcard;
# 2.禁用批量terms(成百上千的场景);
# 3.充分利用倒排索引机制,能keyword类型尽量keyword;
# 4.数据量大时候,可以先基于时间敲定索引再检索;
# 5.设置合理的路由机制。
# 数十亿级别如何提高查询效率
# 1.filesystem cache
# 你往 es 里写的数据,实际上都写到磁盘文件里去了,查询的时候,操作系统会将磁盘文件里的数据自动缓存到 filesystem cache 里面去。
# es 的搜索引擎严重依赖于底层的 filesystem cache,你如果给 filesystem cache 更多的内存,尽量让内存可以容纳所有的 idx segment file
# 索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。最佳的情况下,就是你的机器的内存,至少可以容纳你的总数据量的一半。
# 2.字段个数
# es中尽量存放用来检索的少量字段,不会被搜索的字段存入mysql/hbase中。
# hbase 的特点是适用于海量数据的在线存储,就是对 hbase 可以写入海量数据,但是不要做复杂的搜索,做很简单的一些根据 id 或者范围进行查询的这么一个操作就可以了。
# 从 es 中根据 name 和 age 去搜索,拿到的结果可能就 20 个 doc id,然后根据 doc id 到 hbase 里去查询每个 doc id 对应的完整的数据,给查出来,再返回给前端。
# 写入 es 的数据最好小于等于,或者是略微大于 es 的 filesystem cache 的内存容量。然后你从 es 检索可能就花费 20ms,然后再根据 es 返回的 id 去 hbase 里查询,查 20 条数据,可能也就耗费个 30ms,可能你原来那么玩儿,1T
# 数据都放 es,会每次查询都是 5~10s,现在可能性能就会很高,每次查询就是 50ms。
#
# 理解:
# 比如说你现在有一行数据。id,name,age .... 30 个字段。但是你现在搜索,只需要根据 id,name,age 三个字段来搜索。
# 如果你傻乎乎往 es 里写入一行数据所有的字段,就会导致说 90% 的数据是不用来搜索的,结果硬是占据了 es 机器上的 filesystem cache 的空间,单条数据的数据量越大,就会导致 filesystem cahce 能缓存的数据就越少。
# 其实,仅仅写入 es 中要用来检索的少数几个字段就可以了,比如说就写入 es id,name,age 三个字段,然后你可以把其他的字段数据存在 mysql/hbase 里,我们一般是建议用 es + hbase 这么一个架构。)
# 3.数据预热
# 假如说,哪怕是你就按照上述的方案去做了,es 集群中每个机器写入的数据量还是超过了 filesystem cache 一倍,比如说你写入一台机器 60G 数据,结果 filesystem cache 就 30G,还是有 30G
# 数据留在了磁盘上。这时候可以使用定时任务每1min或者单独弄一套系统定时的去访问这些热数据,将这些数据缓存到filesystem cache 中。这样别人访问的时候,性能就会好很多。因为直接走内存了。
# 4. 冷热分离
# es 可以做类似于 mysql 的水平拆分,就是说将大量的访问很少、频率很低的数据,单独写一个索引,然后将访问很频繁的热数据单独写一个索引。
# 最好是将冷数据写入一个索引中,然后热数据写入另外一个索引中,这样可以确保热数据在被预热之后,尽量都让他们留在 filesystem os cache 里,别让冷数据给冲刷掉。
# 5. document模型设计
# es的join查询性能本来就不好,所以最好是先在 Java 系统里就完成关联,将关联好的数据直接写入 es 中。搜索的时候,就不需要利用 es 的搜索语法来完成 join/nested/parent-child之类的关联搜索了。
# 6. 分页优化
# es 的分页是较坑的,为啥呢?举个例子吧,假如你每页是 10 条数据,你现在要查询第 100 页,实际上是会把每个 shard 上存储的前 1000 条数据都查到一个协调节点上,
# 如果你有个 5 个 shard,那么就有 5000 条数据,接着协调节点对这 5000 条数据进行一些合并、处理,再获取到最终第 100 页的 10 条数据。所以分的页越大,性能越差。
# 处理方案就是跟产品经理说就默认分页越大,性能越差,尽量避免翻那么深。如果类似于微博中,下拉刷微博,刷出来一页一页的,你可以用 scroll api,
# scroll 会一次性给你生成所有数据的一个快照,然后每次滑动向后翻页就是通过游标 scroll_id 移动,获取下一页下一页这样子,性能会比上面说的那种分页性能要高很多很多,基本上都是毫秒级的。
# 初始化时必须指定 scroll 参数,告诉 es 要保存此次搜索的上下文多长时间。你需要确保用户不会持续不断翻页翻几个小时,否则可能因为超时而失败。
# 除了用 scroll api,你也可以用 search_after 来做,search_after 的思想是使用前一页的结果来帮助检索下一页的数据,显然,这种方式也不允许你随意翻页,你只能一页页往后翻。
# 初始化时,需要使用一个唯一值的字段作为 sort 字段。
8. elasticsearch的读写一致性等。
思路:乐观锁方法控制->对于写操作的控制->对于读操作的控制
# 1.乐观锁方法控制
# 可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖,由应用层来处理具体的冲突;
# 2.对于写操作的控制
# 另外对于写操作,一致性级别支持quorum/one/all,默认为quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,分片将会在一个不同的节点上重建。
# 3.对于读操作的控制
# 对于读操作,可以设置replication为sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置replication为async时,也可以通过设置搜索请求参数_preference为primary来查询主分片,确保文档是最新版本。
zookeeper
1. zookeeper的集群结构和角色
思路:集群结构->Leader->Follower->Observer->总结
# Zookeeper集群是一个基于主从复制的高可用集群。集群数量为奇数,并且超过N+1台,只要集群中大多数节点都处于可用状态,那么集群就是可用的。
# 集群中由一个Leader节点和N个Follower节点组成,所有的Server之间能彼此通信。一旦Leader节点出了问题,则自动完成新Leader的选举并恢复服务。
# 为了支持更多的客户端,需要增加更多的server,但是Server越多,投票阶段延迟越大,会影响性能,引入观察者,观察者不参与投票,多加入Observer节点,提高伸缩性,同时不影响吞吐率。
# Leader:
# ZooKeeper集群工作的核心
# 1.维护与各Follower及Observer之间的心跳。
# 2.完成读写的操作,完成后将写操作广播给其他服务器,只要有半数的节点(不包括Observer)写入成功,这个写的请求就会被提交。
# Follower:
# 一个集群中可以同时存在多个Follower。
# 1.响应leader的心跳。
# 2.处理客户端的读请求。
# 3.将写的请求提交给Leader。
# 4.选举领导者时进行投票。
# Observer:
# 角色跟Follower类似,作用是扩展系统,提高读取速度。
# 1.处理客户端的读请求。
# 2.将写的请求提交给Leader。
# 3.不参与投票。
2. zookeeper的数据模型及znode节点
思路:数据结构->节点组成->节点类型->节点属性
# 数据结构
# Zookeeper提供的命名空间与标准的文件系统非常类似,一个名称就是一个由“/”分割的路径序列,跟Linux的标准文件系统的结构是一致的;每一个节点就是一个路径。
# 节点组成
# 每个节点称为一个Znode。每个Znode由三部分组成:
# 1.stat:此为状态信息,描述该Znode的版本,权限等信息。
# 2.data:与该Znode关联的数据。
# 3.children:该Znode下的子节点。
# 节点类型
# Znode有两种,分别为临时节点和永久节点。节点的类型在创建时即被确定,并且不能改变。
# 1.临时节点
# 该节点的生命周期依赖于创建它们的会话。一旦会话结束,临时节点将被自动删除,当然也可以手动删除。临时节点不允许拥有子节点。
# 2.永久节点
# 该节点的生命周期不依赖于会话,并且只有在客户端显示执行删除操作的时候,他们才能被删除。
# 3.序列化特性
# Znode还有一个序列化的特性,如果创建的时候指定的话,该Znode的名字后面会自动追加一个不断增加的序列号。序列号对于此节点的父节点来说是唯一的,这样便会记录每个子节点创建的先后顺序。他的格式为 “%10d”(10位数字,没有数值的数位用0补充,例如:0000000001)
# 这样便会存在四种类型的Znode节点。分别对应:PERSISTENT:永久节点、EPHEMERAL:临时节点、PERSISTENT_SEQUENTIAL:永久节点、序列化、EPHEMERAL_SEQUENTIAL:临时节点、序列化
# 节点属性
# 每个Znode都包含了一系列的属性,通过命令get,可以获得节点的属性。
# 1.dataVersion:数据版本号,每次对节点进行set操作,dataVersion的值都会增加1(即使设置的是相同的数据),可有效避免了数据更新时出现的先后顺序问题。
# 2.cversion:子节点的版本号。当Znode的子节点有变化时,cversion的值就会增加1。
# 3.aclVersion:ACL的版本号。
# 4.cZxid:Znode创建的事务id。
# 5.mZxid:Znode被修改的事务id,即每次对Znode的修改都会更新mZxid。
# 6.ctime:节点创建时的时间戳。
# 7.mtime:节点最新一次更新发生时的时间戳。
# 8.ephemeralOwner:如果该节点为临时节点,ephemeralOwner值表示与该节点绑定的session id,如果不是,ephemeralOwner值为0。
# 在client和server通信之前,首先需要建立连接,该连接称为session。连接建立后,如果发生连接超时、授权失败,或者显式关闭连接,连接便处于CLOSED状态,此时session结束。
3. zookeeper的master选举机制
思路:投票算法->几个概念(服务器ID、选举状态、数据ID、逻辑时钟)->全新集群选举->非全新集群选举
# 投票算法
# zookeeper默认的算法是FastLeaderElection,采用投票数大于半数则胜出的逻辑。
# 概念
# 1.服务器ID
# 比如有三台服务器,编号分别为1,2,3。编号越大在选择算法中的权重越大。
# 2.选举状态
# (1)LOOKING:竞选状态。
# (2)FOLLOWING:随从状态,同步leader状态,参与投票。
# (3)OBSERVING:观察状态,同步leader状态,不参与投票。
# (4)LEADING:领导者状态。
# 3.数据ID
# 服务器中存放的最新数据version。值越大说明数据越新,在选举算法中数据越新权重越大。
# 4.逻辑时钟
# 也叫投票的次数,同一轮投票过程中的逻辑时钟值是相同的,每投完一次票这个数据就会增加,
# 然后与接收到的其他服务器返回的投票信息中的数值相比,根据不同的值做出不同的判断。
# 全新集群选举
# 假设目前有5台服务器,每台服务器均没有数据,他们的编号分别是1,2,3,4,5 按编号依次启动,他们的选举过程如下:
# 1.服务器1启动,给自己投票,然后发投票信息,由于其他机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking。
# 2.服务器2启动,给自己投票,同时与之前启动得服务器1交换结果,由于服务器2得编号大所以服务器2胜出,但此时投票数没有大于半数,所以两个服务器得状态依然是Looking。
# 3.服务器3启动,给自己投票,同时与之前启动得服务器1,2交换信息,由于服务器3得编号最大,所以服务器3胜出,此时投票数正好大于半数,所以服务器3成为领导者,服务器1,2成为小弟。
# 4.服务器4启动,给自己投票,同时与之前启动得服务器1,2,3交换信息,尽管服务器4得编号大,但之前服务器3已经胜出,所以服务器4只能成为小弟。
# 5.服务器5启动,后面的逻辑同服务器4成为小弟。
# 非全新集群选举
# 对于运行正常的zookeeper集群,中途有机器down掉,需要重新选举时,选举过程就需要加入数据ID,服务器ID和逻辑时钟。
# 1.数据ID:数据新的 version就大,数据每次更新都会更新version。
# 2.服务器ID:就是我们配置的myid中的值,每个机器一个。
# 3.逻辑时钟:这个值从0开始递增,每次选举对应一个值。如果在同一次选举中,这个值是一致的。
# 这样选举的标准就变成:
# (1)逻辑时钟小的选举结果被忽略,重新投票
# (2)统一逻辑时钟后,数据id大的胜出
# (3)数据id相同的情况下,服务器id大的胜出,根据这个规则选出leader。
4. zookeeper的watch观察机制
思路:作用->watcher的工作流程->watcher监听机制->Watcher特性->watcher类型->
客户端串行执行->应用:zookeeper实现服务注册与发现的大致原理
# 作用
# watcher是zooKeeper中一个非常核心功能 ,客户端watcher可以监控节点的数据变化以及它子节点的变化,一旦这些状态发生变化,
# zooKeeper服务端就会通知所有在这个节点上设置过watcher的客户端 ,从而每个客户端都很快感知,它所监听的节点状态发生变化,而做出对应的逻辑处理。
# watcher的工作流程
# 客户端在向zookeeper服务器注册watcher的同时,会将watcher对象存储在客户端的watcherManager中,
# 当zookeeper服务器触发watcher事件后,会向客户端发送通知,客户端线程从watchermanager中取出对应的watcher对象执行回调逻辑。
# watcher监听机制
# Watcher 监听机制是 Zookeeper 中非常重要的特性,我们基于 zookeeper 上创建的节点,可以对这些节点绑定监听事件。
# 比如可以监听节点数据变更、节点删除、子节点状态变更等事件,通过这个事件机制,可以基于 zookeeper 实现分布式锁、集群管理等功能。
# Watcher特性
# Watcher具有一次性,无论是服务端还是客户端,一旦一个Watcher被触发,ZooKeeper都会将其从相应的存储中移除。因此Watcher需要反复注册。
# 即如果还要继续监听这个节点,就需要我们在客户端的监听回调中,再次对节点的监听watch事件设置为True。否则客户端只能接收到一次该节点的变更通知。
# watcher类型
# 1.DataWatches,基于znode节点的数据变更从而触发 watch 事件,触发条件getData()、exists()、setData()、 create()。
# 2.Child Watches,基于znode的孩子节点发生变更触发的watch事件,触发条件 getChildren()、 create()。
# 而在调用 delete() 方法删除znode时,则会同时触发Data Watches和Child Watches,如果被删除的节点还有父节点,则父节点会触发一个Child Watches。
# 客户端串行执行
# 最终Watcher会被放入一个队列中串行执行。
# zookeeper实现服务注册与发现的大致原理
# 经典使用场景:zookeeper为dubbo提供服务的注册与发现,作为注册中心。
# zookeeper的服务注册与发现,主要应用的是zookeeper的znode节点数据模型和watcher机制,大致的流程如下:
# 1.服务注册:
# 服务提供者(Provider)启动时,会向zookeeper服务端注册服务信息,也就是创建一个节点,例如:用户注册服务com.xxx.user.register,
# 并在节点上存储服务的相关数据(如服务提供者的ip地址、端口等)。
# 2.服务发现:
# 服务消费者(Consumer)启动时,根据自身配置的依赖服务信息,向zookeeper服务端获取注册的服务信息并设置watch监听,
# 获取到注册的服务信息之后,将服务提供者的信息缓存在本地,并进行服务的调用。
# 3.服务通知:
# 一旦服务提供者因某种原因宕机不再提供服务之后,客户端与zookeeper服务端断开连接,zookeeper服务端上服务提供者对应服务节点会被删除(例如:用户注册服务com.xxx.user.register),
# 随后zookeeper服务端会异步向所有消费用户注册服务com.xxx.user.register,且设置了watch监听的服务消费者发出节点被删除的通知,消费者根据收到的通知拉取最新服务列表,更新本地缓存的服务列表。
5.说一下zookeeper的ZAB协议
其他问法:怎么保证主从节点的状态同步/Zookeeper如何通过Zab协议来保证分布式事务的最终一致性。
思路:ZAB介绍->ZAB的两种模式->分布式事务的最终一致性
# Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。
# ZAB 协议包括两种基本的模式:
# 1.崩溃恢复模式
# 当整个 zookeeper 集群服务刚刚启动或者 Leader 服务器宕机、重启或者网络故障导致不存在过半的服务器与 Leader 服务器保持正常通信时,
# 所有进程(服务器)进入崩溃恢复模式,首先选举产生新的 Leader 服务器,然后集群中 Follower 服务器开始与新的 Leader 服务器进行数据同步,
# 当集群中超过半数机器与该 Leader服务器完成数据同步之后(状态同步保证了 leader 和 server 具有相同的系统状态。),退出恢复模式进入消息广播模式,
# 2.消息广播模式
# 一旦 leader 已经和多数的 follower 进行了状态同步后,它就可以开始广播消息了,即进入广播状态。
# 这时候当一个 server 加入 ZooKeeper 服务中,它会在恢复模式下启动,发现 leader,并和 leader 进行状态同步。
# 待到同步结束,它也参与消息广播。ZooKeeper 服务一直维持在 Broadcast 状态,直到 leader 崩溃了或者 leader 失去了大部分的 followers 支持。
# 即Leader 服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。
# Zookeeper 是通过 Zab 协议来保证分布式事务的最终一致性。
# 为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加 上了zxid。
# 实现中zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一 个新的epoch,标识当前属于那个leader的统治时期。第32位用于递增计数。
# epoch:可以理解为皇帝的年号,当新的皇帝leader产生后,将有一个新的epoch年号。
# 每个Server在工作过程中有三种状态:
# (1)LOOKING:当前Server不知道leader是谁,正在搜寻。
# (2)LEADING:当前Server即为选举出来的leader。
# (3)FOLLOWING:leader已经选举出来,当前Server与之同步。
6.zookeeper的分布式锁
思路:原理->具体实现
# 原理
# 实际上是利用ZooKeeper的临时顺序节点的特性实现分布式锁。
# 具体实现
# 1.假设现在有一个客户端A,需要加锁,那么就在"/Lock"路径下创建一个临时顺序节点。
# 然后获取"/Lock"下的节点列表,判断自己的序号是否是最小的,如果是最小的序号,则加锁成功!
# 2.现在又有另一个客户端,客户端B需要加锁,那么也是在"/Lock"路径下创建临时顺序节点。
# 依然获取"/Lock"下的节点列表,判断自己的节点序号是否最小的。发现不是最小的,加锁失败,接着对自己的上一个节点进行监听。
# 3.怎么释放锁呢,其实就是把临时节点删除。假设客户端A释放锁,把节点01删除了。
# 那就会触发节点02的监听事件,客户端就再次获取节点列表,然后判断自己是否是最小的序号,如果是最小序号则加锁。
# 4.如果多个客户端其实也是一样,一上来就会创建一个临时节点,然后开始判断自己是否是最小的序号,
# 如果不是就监听上一个节点,形成一种排队的机制。也就形成了锁的效果,保证了多台服务器只有一台执行。
# 5.假设其中有一个客户端宕机了,根据临时节点的特点,ZooKeeper会自动删除对应的临时节点,相当于自动释放了锁。
# 参考: https://juejin.cn/post/6854573210756972557