大三了,真的要去找实习了来提升自己了,学历没有优势只能在项目和八股文上努力一下,通过B站黑马程序员的八股文教学,自己也二刷了,结合ChatGpt、deepSeek总结了一下,(还没有写完,这只是一部分,JVM篇、数据库篇和常见集合篇),有点多,但最好还是结合项目经验来描述,除了JVM是偏向理论的,其他都是可以自己动手来验证实践的。
它是线程私有的,每个线程都有一份,是内部保存的字节码的行号,用来记录正在执行的字节码指令的地址。
(1)Java堆是线程共享的区域,主要用来存储对象实例,数组等,当它内存不足时就会抛出OOM(内存泄漏)错误;
(2)Java堆由新生代和老年代组成。新生代被划分为三部分,分别是Eden区和两个大小严格相同的幸存者区,而老年代主要用来存储生命周期长的对象,一般是老的对象;
(3)Java堆在JDK1.7和JDK1.8有一些区别。1.7中有一个永久代,主要用于存储类信息、静态变量、常量、编译后的代码;而1.8之后就移除了永久代,把数据存储到本地内存的元空间中,目的是为了防止内存溢出;
(1)什么是虚拟机栈?
·虚拟机栈是每个线程运行时分配的一块内存区域;
·每个栈由多个栈帧组成,分别对应着每次方法调用时所占用的内存;
·每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法;
(2)垃圾回收是否涉及栈内存?
·垃圾回收主要针对的是堆内存,因为栈中的栈帧随着方法调用而创建,方法执行结束而销毁,当栈帧弹出后栈内存就会释放,因此不存在垃圾回收
(3)栈内存分配越大越好吗?
·浪费内存资源。如果栈内存分配过大,而应用程序实际使用较少,会导致大量内存被浪费。
·减少可用线程数量。如果栈内存分配过大,JVM将无法为新线程分配足够的内存,从而抛出OOM错误。栈内存默认是1024K(1M)。
·影响启动速度。栈内存分配过大会增加JVM启动时的初始化成本,导致程序启动时间变长。
(4)什么情况下方法是线程安全的?
·当方法没有形参和没有返回局部变量时是线程安全的。
(5)什么情况下会导致栈内存溢出?
·栈帧过多时会导致栈内存溢出(递归调用)
·栈帧过大(比较少见)
(1)栈内存一般用来存储局部变量和方法调用,但堆内存主要用来存储Java对象和数组。堆会进行GC垃圾回收,但栈不需要;
(2)栈内存是线程私有的,而堆内存是线程共享的;
(3)两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常;
(1)方法区是各个线程共享的内存区域;
(2)它主要用来存储类的信息、运行时常量池;
(3)虚拟机启动的时候创建,关闭虚拟机的时候释放;
(4)如果方法区中的内存无法满足分配请求,就会抛出OOM
(1)先解释什么是常量池:它可以看作一张常量表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息;
(2)当类被加载时,他的常量池信息会放入运行时常量池,并将里面的符号地址变为真实地址;
(1)直接内存不属于JVM中的内存结构,不由JVM进行管理。是虚拟机的系统内存;
(2)常见于NIO操作,用于数据缓冲区,分配回收成本较高,单独写性能好,不受JVM内存回收管理;
JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而使Java程序能够运行启动
它是线程私有的,每个线程都有一份,是内部保存的字节码的行号,用来记录正在执行的字节码指令的地址。
(1)启动类加载器:加载JAVA_HOME/jre/lib目录下的库;
(2)扩展类加载器:加载JAVA_HOME/jre/lib/ext目录下的类;
(3)应用类加载器:加载classPath下的类
(4)自定义类加载器:自定义类继承ClassLoader,实现自定义类加载规则
加载某一个类,先委派上一级的加载器进行加载,如果上级加载器也有上级,则继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类(向上委托,向下执行)
(1)通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后无需重复加载,保证了唯一性;
(2)保证类库API不会被修改,确保安全性;
(1)加载:查找和导入class文件
(2)验证:保证加载类的准确性
(3)准备:为类变量分配内存并设置类变量初始值
(4)解析:把类的符号引用转化为直接应用
(5)初始化:对类的静态变量、静态代码块执行初始化操作
(6)使用:JVM开始从入口方法开始执行用户的程序代码
(7)卸载:当用户程序代码执行完毕后,JVM开始销毁创建Class对象
如果一个或多个对象没有任何引用指向它,那么这个对象就是垃圾,如果被定位了垃圾,则有可能被垃圾回收器回收。
(1)引用计数法:每个对象使用一个引用计数器,当由新的引用指向该对象时,计数器+1,当引用失效或被删除时,计数器-1,如果计数器为0,说明该对象不可达变成垃圾;
这个方法实现简单,实时性强,但很少被使用,因为它有个致命缺点,就是不能解决对象之间互相引用的情况—两个对象互相引用,此时引用计数器永不为0,可能会导致内存泄漏。并且这个方法的维护成本比较高;
(2)可达性分析算法:从GC Root(垃圾回收的根节点)开始,沿着引用链向下搜索,能被访问的对象称为“可达对象”,无法访问的对象即为“不可达对象”,视为垃圾被回收。
GC Root 通常包括:虚拟机栈应用的对象(方法中的局部变量);方法区中静态属性引用的对象;方法区中常量引用的对象;本地方法栈中JNI(Native方法)引用的对象。
它的优点是:支持循环引用;不需要维护引用计数器,专注于引用链的追踪,更加高效;缺点:垃圾回收时需要暂停程序,进行全局的可达性分析;算法复杂;
(1)标记清除算法:分为“标记”、“清除”两个阶段,先根据可达性分析算法得出的垃圾进行标记,接着对标记为垃圾的内存进行回收;这个方法效率高,但存在有磁盘碎片,内存不连续的缺点,所以用的不多;
(2)标记整理算法:跟标记清除算法多了几个步骤,将存活对象都向内存一端移动,然后清理边界以外的垃圾,这个方法没有内存碎片,但多了几个步骤导致效率低;适合存活率高的场景,因此常用于老年代。
(3)复制算法:将原有的内存空间分为大小严格相等的内存区域(From区和To区),新生代的对象首先分配到From区,当垃圾回收时,遍历From区,将所有存活的对象复制到To区,然后清空From区中所有的数据,接着From区和To区互换角色;优点是没有内存碎片,效率高实现简短;缺点是浪费内存;适合对象生命周期短的场景,因此常用于新生代。
(1)堆的区域划分
·堆被分为两部分:新生代和老年代 1:2
·对于新生代,被分为三个区域。分别是Eden区和两个survivor区(From区和To区) 8:1:1
(2)分代回收的策略
·新创建的对象,都会先分配到伊甸园区;
·当伊甸园区内存不足时,标记伊甸园区和from区的存活对象;
·根据复制算法将存活对象复制到to区;
·清除伊甸园区和from区的所有数据,接着将from区和to区角色互换;
·当伊甸园区内存不足时重复以上操作,当幸存者区中的对象达到晋升阈值(默认15),将将晋升到老年代
1)Minor GC 是对于新生代进行垃圾回收,当新生代的伊甸园区被填满时,就会触发Minor GC,主要用于清理生命周期较短的对象,特点是效率高,停顿时间短;
(2)Mixed GC 是一种G1垃圾回收器中专有回收类型,同时回收新生代和老年代的部分区域,当老年代使用率超过阈值(默认45%)触发,特点是局部回收,更短的停顿时间,适合大堆内存;
(3)Full GC 回收整个堆,包括新生代和老年代(甚至元空间);触发条件为老年代空间不足、元空间或永久代空间不足、调用了System.gc()、JVM空间分配失败;它的特点是停顿时间长、开销高和频率低;
1)串行垃圾回收器(新生代):单线程工作;在垃圾回收期间,会暂停所有的应用线程(STW);复制算法+标记整理算法;优点:简单高效,单线程开销小;缺点:暂停时间较长,不适合多线程、高并发场景;
(2)并行垃圾回收器(新生代):多线程回收,追求高吞吐量;复制算法+标记整理算法;优点:适合多核CPU,垃圾回收效率高;缺点:GC时会暂停所有的应用线程;
(3)CMS(并发)垃圾回收器(老年代):老年代垃圾回收器,目标是降低GC暂停时间;使用标记清除算法;并发收集阶段允许应用程序线程和垃圾回收线程同时工作;优点:停顿时间较短,适合需要低延迟的场景;缺点:可能产生内存碎片,对CPU性能有较高要求;
(4)G1垃圾回收器(JDK9之后默认使用,新生代+老年代):适合大内存应用,目标是提供可预测的低延迟;堆被分为多个Region,每个区域可以存储新生代或老年代的对象;采用复制算法;工作流程:1.初始标记(STW)、2.并发标记:识别可回收的Regina、3.最终标记(STW):修正标记阶段遗漏、4.筛选回收:按收益优先回收垃圾最多的Region;优点:可控的暂停时间、减少内存碎片;缺点:实现复杂,堆内存分区和标记开销较大;
(1)强引用:通过直接赋值给对象的方式创建,在任何情况下不会被垃圾回收,除非程序终止或手动将引用设置为null;
(2)软引用:需要配合SortReference使用,适合缓存场景,当内存不足时会被垃圾回收;
(3)弱引用:需要配合WeakReference使用,生命周期短,只要进行垃圾回收就会把弱引用对象回收;
(4)虚引用:必须配合Reference(引用队列)使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放内存;
(1)设置堆空间大小:通过-Xms 设置堆内存的初始大小,-Xmx 设置堆内存的最大大小,通过初始、最大大小为相同值,以减少堆大小动态调整的开销;
(2)虚拟机栈的设置:通过-Xss 设置每个栈的大小,通常512k-1m合理范围,默认是1M
(3)新生代中伊甸园区和两个幸存者区的大小比例:通过-xx:SurvivorRatio = 设置伊甸园区和每个幸存者区的大小比例, 一般为8:1:1(默认),如果伊甸园区太大会增加垃圾回收的开销;如果太小会增加GC的次数和对象晋升为老年代;
(4)新生代晋升为老年代的阈值:通过-XX:MaxtenuringThreshold= 设置对象晋升为老年代的年龄条件,默认是15
(5)设置垃圾回收器:根据需要选择垃圾回收器,-XX:+PrintGCDetails -XX:+PrintGCDateStamps
内存泄漏是指程序中不再使用的对象仍然被引用,导致这些对象无法被垃圾回收,可能导致内存溢出。
(1) 通过jmap或设置jvm参数获取堆内存快照dump文件;
(2) 通过工具,VisualVM去分析dump文件,VisualVM 可以加载离线的dump文件;
(3) 通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出现问题;
(4) 找到对应的代码,通过阅读上下文情况,进行修复即可;
(1)使用top命令查看占用cup的情况;
(2)通过top命令查看后,可以查看哪一个进程占用CPU较高;
(3)使用ps命令查看进程中的线程信息;
(4)使用jstack命令查看进程中那些线程出现问题,最终定位问题;
数组是一种用连续的内存空间存储相同数据类型数据的线性数据结构;
数组的寻址公式:a[i] = baseAddress + i * dataTypeSize; 地址等于数组的首地址+索引数据类型大小,如果索引从1开始,就需要增加一个减法操作,对于CPU来说就多了一个指令,性能不高。
(1)Array List底层是用动态扩展的数组实现的;
(2)ArrayList初始容量为0,当第一次添加数据的时候才初始容量为10;
(3)在进行扩展时容量是原来的1.5倍,每次扩展都需要拷贝数据;
(4)在添加数据的时候有以下情况:首先判断当前数组是否有足够容量存储新数据,如果容量不足,将调用grow方法进行扩容(原来的1.5倍),确保新增数据有地方存储后,将新元素添加到位于size的位置上,返回添加成功布尔值。
(1)利用Arrays.asList 将数组转换为List。转换之后如果修改了数组内容,list将受影响,因为它的底层使用的是Arrays类中的一个内部类ArrayList来构造集合,在这个集合的构造器中,把我们传入的这个集合做了包装而已,最终指向的都是同一个内存地址(浅拷贝);
(2)利用list.toArray 将List转化为数组。转化之后修改list内容,数组不受影响,在底层它是进行了数组的拷贝,跟原来的没啥关系(深拷贝)
(1)底层数据结构不同。ArrayList是动态数组的数据结构实现的;LinkedList是双向链表的数据结构实现的;
(2)操作数据效率不同。
·查找:ArrayList的内存是连续的,可按照下标索引随机访问,时间复杂度为O(1),如果根据值来查找元素需要遍历数组,时间复杂度为O(n), 而LinkedList没有索引,需要遍历链表查找元素,时间复杂度为O(n);
·增删:ArrayList插入和删除尾部元素时间复杂度为O(1),其他部分需要移动后续元素,时间复杂度为O(n); LinkedList插入和删除头部或尾部时间复杂度为O(1),其他节点需要遍历链表,时间复杂度为O(n);
(3)内存空间占用不同。ArrayList底层是数组,内存连续,节省空间;LinkedList底层是双向链表,需要存储数据和两个指针,更占用内存;
(1)二叉树是一种树形数据结构,其中每个节点最多有两个子节点,分别成为左子节点和右子节点。分为为满二叉树:除了叶子节点,所有节点都有两个子节点,并且叶子节点都位于同一层;完全二叉树:树的每一层节点都尽量靠左排列,只有最后一层的节点可能不满;
(2)二叉搜索树是二叉树的一种扩展,其左子树的所有节点的值都小于根结点的值,右子树所有结点的值都大于根结点的值,左、右子树本身也是二叉搜索树;中序遍历(左根右)会生成一个升序的序列;插入和查找的时间复杂度是O(h), 其中h是树的高度。在极端最坏的情况下,二叉搜索树会退化为链表(如连续插入升序数据),此时查找效率下降为O(n);
(3)红黑树是一种自平衡的二叉搜索树,通过附加的颜色属性(红色或黑色)以及旋转操作,保证树的平衡,从而提高效率;红黑树满足以下性质:
·每个节点要么是红色,要么是黑色;
·根节点是黑色;
·叶子节点都是空的黑色节点;
·红色节点的子节点必须是黑色(既没有连续的红色节点);
·从任何节点都其每个叶子节点的所有路径中,包含相同数量的黑色节点;
查找、插入、删除最坏情况下的时间复杂度为O(logn),通过选择和重新着色保持树的平衡;
(1)散列表用名为哈希表,是一种基于键值对存储的数据结构,通过散列函数将键映射到对应的存储位置(由数组演化而来,根据下标进行随机访问数据);平均情况下,插入、删除和查找的时间复杂度为O(1);
(2)散列冲突又称为哈希冲突,是指两个不同的键经过散列函数计算出相同的索引,映射到相同的存储位置;常见的解决方法为:开放地址法:如果发生冲突,按某种规则寻找下一个空位置;拉链法:将冲突的元素存储在同一索引的链表中;再散列法:通过另一个散列函数重新计算索引;
(3)散列表的每个下标位置称之为桶或槽,每个桶会对应一条链表,当发生冲突后的元素将放到相同槽位对应的链表或红黑树;查找时,现根据散列函数找到对应的链表,在链表中搜索目标键值;
HashMap的底层是使用哈希表的数据结构,即数组+链表或红黑树
(1)当我们往HashMap中put元素时,利用key值和hashCode方法计算出hash值(数组下标);
(2)存储数据时,如果出现hash值相同的key,有两种情况如下:
·如果key相同,则覆盖原始值;
·如果key不相同(即哈希冲突),则将当前的键值对放入链表或红黑树;
(3)获取数据时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值;
(1)jdk1.8之前采用的是传统的拉链法,将链表和数组相结合,当遇到哈希冲突时,将冲突的值加到链表中即可;
(2)jdk1.8在解决哈希冲突有了较大变化,当链表长度大于阈值(默认为8)并且数组长度达到63时,将链表转化为红黑树,以减少搜索时间。当数组扩容时resize( )时,如果红黑树的节点数小于等于6个,将退化成链表;
(1)判断键值对数组table是否为空,否则执行resize()方法进行扩容(容量初始化为16);
(2)根据键值key计算哈希值找到数组索引;
(3)判断table[i] 是否为空,如果为空直接添加新数据;
(4)table[i]不可空,如下操作:
·判断table[i]的首个元素是否和key值一样,如果相同直接覆盖value值;
·判断当前槽位是否为红黑树,如果是红黑树直接插入数据,遍历过程中如果发现key已经存在直接覆盖value值;
·如果是链表就遍历链表,在链表尾部插入数据,接着判断如果链表长度到达8且数组长度大于等于64,链表将转化为红黑树,遍历过程中如果发现key已经存在直接覆盖value值;
1)在添加元素或初始化的时候可能需要调用resize( )方法进行扩容,第一次添加数据会初始化数组长度为16,之后每次扩容都需要达到扩容阈值,扩容阈值=数组长度*加载因子(默认0.75);
(2)每次扩容之后,新容量都是之前容量的两倍;
(3)扩容之后,会创建一个新数组,需要把旧数组的数据拷贝到新数组中,规则为:
·没有哈希冲突的节点,直接使用e.hash & (newCap-1) 计算新数组的索引位置;
·如果当前索引位置是红黑树,直接添加到新数组;
·如果是链表,需要遍历链表,判断(e.hash & olcCap)是否等于0,如果为0添加到新数组原始索引位置,不为0就移动到原始索引+旧容量的索引位置;(可能拆分链表,减少每个桶的哈希冲突)
Jdk1.7和hashMap在进行扩容的时候,链表使用头插法。
·线程1:读取当前hashMap数据,准备扩容时,线程2介入;
·线程2:也读取hashMap,接着扩容。原来链表顺序为AB,因为头插法扩容后为BA,线程2执行结束;
·线程1:A的next指向B,形成B->A->A的循环;
因此jdk8将扩容做了修改,链表变为尾插法;
(1)TreeMap是基于红黑树实现的有序映射,保证键值对按照键的顺序排列。
(2)有序性:键值对按照键的自然顺序(通过Comparable实现)或自定义排序顺序(通过构造时传入的Comparator)进行排序;唯一性:键是唯一的,映射出键值对;
(3)TreeMap的每个节点由TreeMap.Entry对象表示,它包含键、值、左右节点和父节点的信息;
(1)HashSet的底层是基于HashMap实现的,每个元素作为HashMap的键,而值是一个固定的常量PRESENT; 当添加元素时,实际上调用HashMap.put(key, PRESENT)方法;通过HashMap的键的唯一性来保证HashSet元素的唯一性;基本操作(添加、删除、查找)的时间复杂度为O(1);
(2)TreeSet的底层是基于红黑树实现的,具体来说,是通过TreeMap实现的,它的元素存储在TreeMap的键中;当添加元素时,实际上调用HashMap.put(key, PRESENT)方法;通过TreeMap的键的唯一性和红黑树的性质保证了元素的唯一性和有序性,基本操作(添加、删除、查找)的时间复杂度为O(log N);
(1)可以借用一些运维工具比如Skywalking,可以检测出哪个接口运行时间比较长;
(2)在MySQL中开启慢日志查询(solw_query_log = 1),可以设置慢日志的时间为2秒(long_query_time = 2),当SQL语句执行时间超过2秒,就会视为慢查询记录;
(1)通过key和key_len检查是否命中了索引(索引本身存在是否有失效的情况)
(2)通过type字段查看sql是否有进一步的优化空间,是否存在全索引扫描或全盘扫描;
(3)通过extra建议判断,是否出现了回表的情况,如果是则尝试添加索引或修改返回字段来修改;
(1)索引是提高数据查询速度的数据结构;
(2)有了索引不需要全盘扫描,提高数据检索的效率,降低数据库的I/O操作;
(3)通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗;
(1)MySql的InnoDB存储引擎采用的是B+树的数据结构来存储索引的,常用于等值查询和范围查询;
(2)非叶子节点只存储指针和索引值,而叶子节点用于存储数据,减少I/O操作,磁盘读写代价低;(叶子节点存储数据)
(3)B+树是有序的,按关键字升序排列,使得范围查询非常高效;(有序性)
(4)所有的叶子节点都在同一级,从根节点到叶子节点的路径长度相同,查询效率高;(查询稳定)
(5)所有数据存储到叶子节点,而叶子节点是一个有序链表,使得范围查询和全索引扫描更加高效(有序链表)
(1)聚簇索引(聚集索引)指的是将数据和索引放到一起,B+树的叶子节点存储了整行数据,一个表有且只有一个;
(2)非聚簇索引(二级索引)指的是将数据和索引分开存储,B+树的叶子节点存储的是对应的主键,一个表可以有多个;
(3)回表查询就是通过二级索引找到对应的主键值,到聚集索引中查找整行数据;
覆盖查询是指查询中所需要的数据都可以通过索引直接获取,不需要回表查询,覆盖查询可以提高查询效率,节省时间和资源;而超大数据分页时可以使用覆盖查询+子查询来解决;
(1)数据量比较大且查询比较频繁的表;
(2)常作为查询条件、排序、分组的字段;
(3)字段内容区分度高,尽量建立唯一索引;
(4)内容较长时,使用前缀索引;
(5)尽量使用联合索引,避免单列索引,联合索引很多时候可以覆盖索引,避免回表;
(6)索引并不是多多益善,索引越多,维护索引结构的代价也越大,会影响增删改的效率;
(7)如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它;
(1)违背最左前缀法则:最左前缀法则指的是复合索引只能从最左边开始使用,即查询条件必须从索引的最左边开始匹配,必须按照索引列的顺序来写;
(2)范围查询右边的列,不能使用索引;
(3)在索引字段上进行函数操作或计算;
(4)查询字段于索引字段不匹配,造成隐式类型转换;
(5)以“%”开头的like模糊查询;
(1)表设计优化
·选择合适的数据类型,尽量使用最小的数据类型(比如使用INT替代BIGINT,使用CHAR替代VARCHAR)来减少存储和查询成本;
·分区表:对于数据量极大的表可以考虑分区表,将表按某些规则分割成多个小表,提高查询效率;
·主从复制、读写分离:如果数据库读的操作比较多,为了避免写操作所造成的性能影响,可以采用读写分离的架构;
(2)索引优化:
·创建适当的索引,避免过多的索引,查照索引的创建原则;
·覆盖索引:使用索引覆盖查询,避免回表,调高效率;
(3)SQL语句优化:
·尽量明确指定需要的列,避免使用SELECT *,以减少不必要的数据传输;
·SQL语句要避免造成索引失效的写法;
·尽量用union all 替代 union ,union多了一次过滤,效率较低;
·避免子查询,特别是当子查询返回大量数据时,可以考虑JOIN或WITH子句替代
事务时一组操作的集合,把所有的操作作为一个整体一起向系统提交或撤销操作请求,它是一个不可分割的工作单位,这些操作要么都成功,要么都不成功。事务的特性如下:
(1)原子性:事务是不可分割的最小操作单位,要么全部成功,要么全部失败;
(2)一致性:事务完成时,必须使所有的数据保持一致状态;
(3)隔离性:数据库系统提交的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行;
(4)持久性:事务一旦提交或回滚,它对数据库中的数据的改变是永久的;
(1)脏读:一个事务读取了另一个事务未提交的修改数据。此时,如果第二个事务回滚,前一个事务读取的数据就是无效的;
(2)不可重复读:一个事务在读取某个数据后,另一个事务修改了这条数据,导致第一个事务再次读取同一个数据得到不相同的值;
(3)幻读:一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,有发现这行数据已经存在,好像出现了“幻影”;解决方案是将事务隔离。MySQL的默认的隔离级别是可重复读,以下是四个隔离级别(效率下降,安全性上升)
·读未提交:允许脏读、不可重复读和幻读;
·读已提交:避免脏读,但允许不可重复读和幻读;
·可重复读:避免脏读和不可重复读,允许幻读;
·串行化:避免所有的并发问题,但性能较差;
(1)重做日志记录的是数据页的物理变化,发生服务宕机时可以同步数据;
(2)回滚日志记录的是逻辑日志,当事务回滚时通过逆操作恢复原来的数据;
(3)重做日志确保了事务的持久性,回滚日志确保了事务的原子性和一致性;
MVCC(多版本并发控制)是一种数据库并发控制机制,旨在通过维护数据的多个版本来提高并发性,同时确保事务的隔离性,主要用于避免数据库的“脏读”和“不可重复读”现象,主要原理如下:
(1)多个版本的记录:每条数据记录都有多个版本,每个版本对应着不同的事务。通过在数据库中为每个数据记录维护一个时间戳或事务ID,可以实现对不同版本的区分;
(2)事务读取和写入:事务在读取数据时,会读取某一时刻数据库中的最新版本,确保读到的是他自己能看到的版本。读操作通常不会加锁,因此可以并发进行读操作;写操作会创建新版本,而不是直接覆盖旧版本。旧版本依旧保留,直到没有事务访问它才会被清理;
(3)数据版本控制:数据的版本信息通常会存储到隐藏的系统字段中,比如创建时间、事务ID等。这使得数据库能够追踪每个版本的产生时间和状态;
(4)提交与回滚:提交事务时,会将该事务对数据的修改永久保存,并生成新版本;如果事务回滚,则不会产生新版本,系统会通过撤销该事务所做的操作,恢复到回滚前的版本;
MySQL主从复制的核心是二进制日志binlog(记录的是DDL(数据定义语句)和DML(数据操纵语句)
(1)主库在提交事务时,会把数据变更记录在二进制日志文件Binlog中;
(2)从库读取主库的二进制日志文件,写入到从库的中继日志Relay Log;
(3)从库重做中继日志中的事件,将改变反映他自己的数据;
分库分表是数据库设计中的一种策略,用于提高数据库的性能、可扩展性和管理效率,尤其是在数据量非常大时。它的基本思想是将数据分散存储到多个数据库或数据表中,减少单个数据库或表的压力,从而提高系统的整体性能。
(1)水平分库:将数据按某种规则(如用户ID、时间)分散到不同的数据库中,可以有效分散负载,减少单库的存储压力;
(2)垂直分库:将不同业务模块或不同的数据表分到不同的数据库中(按业务划分)
(3)水平分表:将数据按某种规则(如用户ID、时间)分散到不同的数据表中,避免单表数据量过大;
(4)垂直分表:将数据表按字段拆分成多个表,达到冷热数据分离