关注微信公众号:CoderLi,回复面试获取PDF版本
本文档为本人整理网上资源以及自己的一些知识点、为面试准备的。当时整理的时候并没有考虑到发布出来、所以对于引用整理的网上的一些文章链接可能并没有列出来、抱歉!如有请评论告知,谢谢
虚拟拼接
B2B 相关流程
判断一个类型不再被使用
标记存活的对象、统一回收未被标记的对象
缺点
缺点
从垃圾收集的停顿时间来看、不移动对象停顿时间更加短、甚至可以不停顿、但是从整个程序的吞吐量来看、移动对象会更加划算。因为内存分配和访问相比垃圾收集的频率要高很多,这部分的耗时增加、吞吐量就会下降了。
Parallel Old 使用的就是标记整理算法、关注点就是吞吐量
CMS 使用的就是标记-清除算法、关注点就是停顿时间
还有一种就是和稀泥的方法、就是 CMS 那样、平时采用标记-清除算法、暂时容忍内存碎片、直到内存碎片化程度大到影响对象分配、再采用标记-整理算法收集一次、以获得内存的规整
垃圾收集器在新生代中建立了名为记忆集 Remember Set 的数据结构、用来避免整个老年代加进 GC Roots 扫描
记忆集是一种用于记录从非收集区域指向收集区域集合的抽象数据结构
收集器只要通过记忆集判断出某一块非收集区域是否存在指向了收集区域的指针就可以了、并不需要了解这些跨代引用的详细细节。
第三种就是称为 卡表 的方式去实现的。
记忆集是抽象概念、卡表是具体的实现。
卡表最简单的形式可以只是一个字节数组、HotSpot 确实也是这么做的。
字节数组中每个元素都对应着其标示的内存区域中一块特定大小的内存,这一块内存称为卡页。
当且仅当都满足以下两个条件、会产生对象消失的问题
要解决这个并发扫描时对象消息的问题、只需要破坏其中的任意一条就可以了
单线程工作的收集器
ParNew 收集器出了支持多线程并行收集之外、其他与Serial 收集器相比并无太多创新。
还有一个重要的特性 : 除了 Serial 收集器、它是第二个可以与 CMS 收集器配合工作
新生代收集器、同样是基于标记-复制算法实现的收集器。
Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量,CMS 收集器则更加关注缩短垃圾收集器用户线程停止的时间。
是 Serial 收集器的老年代版本,单线程收集器、使用标记整理算法。
用途
是 Parallel Scavenge 收集器的老年代版本、支持多线程并发收集,基于标记整理算法实现。
concurrent mark sweep 收集器是以获取最短停顿时间为目标的收集器。是基于标记-清除算法实现的。
初始标记
STW 事件、此阶段的目标主要是标记老年代中所有存活的对象、包括 GC Roots 的直接引用、以及由年轻代存活对象所引用的对象(这个也很重要、因为老年代是独立进行回收的)
并发标记 在此阶段、垃圾收集器遍历老年代、标记所有存活的对象。此阶段、垃圾收集线程与应用线程并发执行、不用暂停应用。在此阶段、并非所有老年代中存活的对象都在此阶段被被标记、因为标记过程中对象的引用关系还在发生变化。
并发预清理 同样不需要 STW 、此阶段记录在并发标记过程中新增的关系引用。然后以此关系链中的黑色对象作为根、重新遍历其引用关系、标记新增的对象。这个在三色标记中有提及到。
并发可取消预清理 同样不需要 STW 、尝试在 最终标记之前尽可能多做一些工作。
最终标记 最后一次 STW 。本阶段的目标是完成老年代中所有存活对象的标记。因为之前的预清理阶段是并发的、有可能对象之间的引用关系变化没有很好的记录下来、所有需要 STW 来处理。
在这个阶段之后、老年代中的所有存活对象都被标记了
并发清除 不需要 STW、目标是删除未使用的对象、并回收他们占用的空间。
并发重置 不需要 STW 、重置 CMS 相关的内部数据、为下次 GC 准备。
CMS 有三个缺点
是一款面向服务器的垃圾收集器,支持新生代和老年代的垃圾收集,主要针对配备多核处理器及大容量内存多机器。
G1 最主要的设计目标是 : 可预测可配置的 STW 停顿时间。
G1 有一些独特的实现。首先、堆不再分成连续的年轻代和老年代空间(不再坚持固定大小以及固定数量的分代区域划分)。而是划分为多个大小相等的存放对象的小堆区。每个小堆区可能是Eden Survivor 区或者 Old 区、但是在同一时刻只能属于某个代。
在逻辑上, 所有的Eden区和Survivor区合起来就是新生代,所有的Old区合起来就是老年代,且新生代和老年代各自的内存Region区域由G1自动控制,不断变动
在 G1 收集器出现之前的所有其他收集器、包括 CMS 在内、垃圾收集的目标范围要么是整个新生代、要么是整个老年代、要么就是整个堆。G1 跳出这个樊笼、 它可以面向堆内存任何部分来组成回收集 Collection Set CSet、衡量标准不再是它属于哪个分代、而是哪块内存的垃圾数量最多、回收收益最大、这就是 G1 的 Mixed GC 模式。
当对象大小超过 Region 的一半、则认为是巨型对象,直接被分配到老年代的巨型对象区 Humongous Regions 。
每个 Region 中最多只有一个巨型对象、巨型对象可以占多个 Region。
G1 把堆内存划分为一个个 Region 的意义在于
使用记忆集避免全堆作为 GC Roots 扫描、但在 G1 收集器上记忆集的应用要复杂很多、它的每个 Region 都维护有自己的记忆集、这些记忆集都会记录下别的 Region 指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。G1 的记忆集在存储结构的本质上是一个哈希表、key是 Region 的起始地址、Value 是一个集合、里面存储的元素是卡表的索引号。这种双向的卡表结构(卡表是我指向谁、这种结构还记录了谁指向我)。由于每个 Region 都需要维护一个记忆集、所以G1 收集器比其他传统的垃圾收集器有着更高的内存占用负担。G1 耗费相当于 Java 堆容量的 10% - 20% 的额外内存来维持收集器的工作。
CMS 采用增量更新、G1 采用原始快照来实现。此外垃圾收集对用户线程的影响还体现在回收过程中新建对象的内存分配上、程序要继续运行肯定会持续有新对象被创建,G1 为每个 Region 创建了两个名为 TAMS(Top At Mark Start) 的指针、把Region 中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址必须要在这两个指针位置以上。G1 收集器默认在这个地址以上的对象时被隐式标记过的、默认它们是存活的、不纳入回收的范围。与CMS 中的 Concurrent Mode Failure 失败会导致 Full GC 类似,如果内存回收的速度赶不上内存分配的速度、G1 收集器也要被冻结用户线程、导致 Full GC 而产生长时间 STW
用户通过 -XX:MaxGCPauseMills参数指定停顿时间、这个时间只是垃圾收集器发生前的期望。G1 收集器的停顿预测模式是以衰减均值为理论基础实现的、在垃圾收集过程中、G1 收集器会记录每个 Region 的回收耗时、每个Region 里藏卡数量等各个可测量的步骤花费的成本。
针对新生代和老年代、G1 提供了两种 GC 模式、Young GC 和 Mixed GC 、都会导致 STW
在应用程序刚启动时、G1 还未执行过并发阶段、也就没有获得额外的信息,处于初始的 fully young 模式。
在年轻代空间用满之后、应用线程被暂停、年轻代中存活的对象被复制到存活区、如果没有存活区、则选择任意一部分空闲的小堆区作为存活区。
复制过程称为转移 Evacuation,这和前面讲过的年轻代收集器基本是一样的工作原理。
主要是为了 Mixed GC 计算找出回收收益较高的 Region 区域。
当堆内存的总体使用比例达到一定数值时、会触发并发标记、默认值是 45% 、可以通过参数 InitiatingHeapOccupancyPercent 来设置。
阶段1 初始标记 此阶段标记所有从 GC Root直接可达的对象。当达到出发条件时、G1 并不会立即发起并发标记周期、而是等待下一次新生代收集、利用新生代收集的 STW 时间段、完成初始标记、这种方式称为借道
阶段2 Root Region 扫描 在初始标记暂停之后、新生代收集也完成对象复制到 Survivor 的工作、应用线程也开始活跃起来、此时为了保证标记算法的正确性,所有新复制到 Survivor 分区的对象、需要找出哪些对象存在对老年代对象的引用,把这些对象标记成根。这个过程称为根分区扫描。同时扫描的 Survivor 分区也被称为根分区;根分区的扫描必须要在下一次新生代垃圾收集启动前完成、因为每次 GC 产生新的存活对象集合。
阶段3 并发标记 标记线程与应用线程并发执行、标记各个堆中 Region的存活对象信息、这个步骤可能会被新的 Young GC 打断、所有标记任务必须在堆满前就扫描完成、如果并发标记耗时很长、那么有可能在并发标记过程中、又经历几次新生代收集
阶段4 再次标记 STW 以完成标记过程、标记在并发阶段发生变化的对象和未被标记的对象、同时完成存活数据计算
阶段5 清理
与 CMS 相比、G1 有很多优点、暂不论可以指定最大暂停时间、分Region的内存布局、按收益动态确定收集 这些创新性设计带来的红利。单从最传统的算法理论看、G1 也更有发展潜力、与CMS 的标记清除算法不同、G1 从整体上看是基于标记整理算法实现、但从局部(两个 Region 之间) 上看又是基于标记复制算法实现的、但是无论如何这两种算法都以为着 G1 运作期间不会产生内存碎片、垃圾收集完成之后能提供规整的可用内存。这个特性有利于程序长时间运作
比起 CMS G1 弱项也可以列举不少、如在用户程序运行过程中、G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 高。
在发生 Minor GC 之前、虚拟机会检查老年代中最大可用的连续空间是否大于所有新生代对象的总空间、如果大于、那么 Minor GC 是安全的
如果不成立、则检查老年代中最大可用连续空间是否大于历次晋升到老年代的对象的平均大小、如果大于、那么进行 Minor GC 、如果小于、则进行 Full GC
如果执行完 Minor GC 发现晋升的对象很多、老年代无法存放、还是要进行 Full GC
项目相关
打印 JVM 初始参数
-XX:+PrintFlagsFinal
# 或者
-XX:+PrintFlagsInitial
查看 Java 进程
jps
查看进程中某个参数的值
jinfo -flag XXXX <pid>
查看 Java 进程内存的容量和使用量
jstat -gc <pid>
查看 JVM 内线程的情况
jstack <pid>
本质上是一个语法糖、继承自 Enum 类。会自动生成 valuse 方法、还有 valueOf 方法
Enum 的 == 和 euqal 是一样的。
Enum 不支持 Clone 方法
Enum 方法支持序列化反序列化,但是反序列化出来的对象还是 JVM 中原来的对象
由编译器生成的,在源代码中没有出现的,都会被标记为
synthetic
。当然有一些例外的情况:默认的构造函数、类的初始化方法、以及枚举类中的value
和valueOf
方法
序列化就是将对象的状态信息转为可以存储或者传输的形式的过程
https://www.cnblogs.com/-coder-li/p/13100015.html
Iterable
Iterator
我们都知道在 ArrayList 中 forEach 中的时候 remove 会导致 ConcurrentModificationException
数组 + 链表+红黑树
table 的默认长度是16 loadFactor 的值是 0.75
下标的计算
JDK 1.7 HashMap 扩容的时候、会因为链表元素的倒置进而导致循环链表
容量为2,负载因子为1、可以使加入5之后进行扩容
再进一步
进而
在 JDK1.8 的时候、在扩容处理链表的时候、增加了头尾两个元素、将链表元素倒置问题解决了、循环链表的问题也就解决了。
但是无论如何并发的情况下、元素还是会丢失
遗留类、很多功能和 HashMap 类似、但是它是线程安全的、任意时刻只有一个线程写 HashTable 、并发性不如 ConcurrentHashMap
继承自 HashMap、在 HashMap 的基础上、通过维护一条双向链表、解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题
我们可以通过重写 removeEldestEntry 方法来实现一个 LRU 队列
依赖于 HashMap 实现的
默认最小顶堆
修饰符 | 本类 | 同包 | 子类 | 其他 |
---|---|---|---|---|
private | ️ | |||
default | ️ | ️ | ||
protected | ️ | ️ | ️ | |
public | ️ | ️ | ️ | ️ |
byte、short、int、long 都缓存了 -128 到 127
character 缓存了 0 到 127
万物皆可抛
Error 代表 JVM 本身的错误、并不是程序员能通过代码处理的、Error 很少出现的。OutOfMemoryError、StackOverflowError
Exception 能被程序员处理的
异常分为
需要明确的是:检查和非检查是对于javac来说的,这样就很好理解和区分了。
也就是说:try…catch…finally中的return 只要能执行,就都执行了,他们共同向同一个内存地址(假设地址是0×80)写入返回值,后执行的将覆盖先执行的数据,而真正被调用者取的返回值就是最后一次写入的。那么,按照这个思想,下面的这个例子也就不难理解了。
finally中的return 会覆盖 try 或者catch中的返回值。
public static void main(String[] args)
{
int result;
result = foo();
System.out.println(result); /2
result = bar();
System.out.println(result); /2
}
@SuppressWarnings("finally")
public static int foo()
{
trz{
int a = 5 / 0;
} catch (Exception e){
return 1;
} finally{
return 2;
}
}
@SuppressWarnings("finally")
public static int bar()
{
try {
return 1;
}finally {
return 2;
}
}
finally中的return会抑制(消灭)前面try或者catch块中的异常
class TestException
{
public static void main(String[] args)
{
int result;
try{
result = foo();
System.out.println(result); //输出100
} catch (Exception e){
System.out.println(e.getMessage()); //没有捕获到异常
}
try{
result = bar();
System.out.println(result); //输出100
} catch (Exception e){
System.out.println(e.getMessage()); //没有捕获到异常
}
}
//catch中的异常被抑制
@SuppressWarnings("finally")
public static int foo() throws Exception
{
try {
int a = 5/0;
return 1;
}catch(ArithmeticException amExp) {
throw new Exception("我将被忽略,因为下面的finally中使用了return");
}finally {
return 100;
}
}
//try中的异常被抑制
@SuppressWarnings("finally")
public static int bar() throws Exception
{
try {
int a = 5/0;
return 1;
}finally {
return 100;
}
}
}
finally中的异常会覆盖(消灭)前面try或者catch中的异常
class TestException
{
public static void main(String[] args)
{
int result;
try{
result = foo();
} catch (Exception e){
System.out.println(e.getMessage()); //输出:我是finaly中的Exception
}
try{
result = bar();
} catch (Exception e){
System.out.println(e.getMessage()); //输出:我是finaly中的Exception
}
}
//catch中的异常被抑制
@SuppressWarnings("finally")
public static int foo() throws Exception
{
try {
int a = 5/0;
return 1;
}catch(ArithmeticException amExp) {
throw new Exception("我将被忽略,因为下面的finally中抛出了新的异常");
}finally {
throw new Exception("我是finaly中的Exception");
}
}
//try中的异常被抑制
@SuppressWarnings("finally")
public static int bar() throws Exception
{
try {
int a = 5/0;
return 1;
}finally {
throw new Exception("我是finaly中的Exception");
}
}
}
上面的3个例子都异于常人的编码思维,因此我建议:
不要在fianlly中使用return。
不要在finally中抛出异常。
减轻finally的任务,不要在finally中做一些其它的事情,finally块仅仅用来释放资源是最合适的。
将尽量将所有的return写在函数的最后面,而不是try … catch … finally中。
反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
cls = Class.forName("java.lang.String");
Object array = Array.newInstance(cls,25);
//往数组里添加内容
Array.set(array,0,"hello");
参数化类型
有三种使用方式: 泛型类、泛型接口、泛型方法
Java 实现泛型的方式是 类型擦除式泛型
C# 选择的泛型实现方式是 具现化式泛型
在 C# 里面 List 和 List 是两种类型,而 Java 的泛型只存在源代码中、编译之后的字节码文件中、全部泛型都被替换你为原来的裸类型,并且在相应的地方插入了强制转型代码、对于 List List 它们是同一个类型
signature 是新加入的一个属性、它的作用存储参数化类型的具体类型
擦除法仅仅是对方法对 Code 属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息
随机存取并不意味着你可以在真正随机的位置进行读写操作,它只是意味着你可以跳过文件中某些部分进行操作,并且支持同时读写,不要求特定的存取顺序。
这使得RandomAccessFile可以覆盖一个文件的某些部分、或者追加内容到它的末尾、或者删除它的某些内容,当然它也可以从文件的任何位置开始读取文件。
它在java.io包中是一个特殊的类,既不是输入流也不是输出流,它两者都可以做到。他是Object的直接子类。通常来说,一个流只有一个功能,要么读,要么写。但是RandomAccessFile既可以读文件,也可以写文件。
线程的六个状态
不正确的方式
stop 终止线程时、会抛出 ThreadDeath 的 Error
正确的方式
如果线程处于 wait 、join 、 sleep 阻塞时、那么 interrupt 会生效、该线程的中断状态标志将会被清除、抛出 InterruptionException
如果线程是被 IO/NIO 阻塞、那么IO 操作将会抛出特殊的异常值、达到终止线程的目标
使用 volatile 标志位、虽然是经常使用的、但是如果线程处于休眠状态、那么就永远不会去检查这个 volatile 的标志位了
执行该方法的线程会放弃当前的 CPU 时间片、与其他线程一起等待 CPU 的调度、但是可能再次被 CPU 调度到它
必须持有对象锁、才能调用
wait 方法导致当前线程等待、加入该对象的等待集合中、并且放弃当前持有的对象锁
notify、notifyAll 方法唤醒一个或所有正在等待这个对象锁的线程
虽然 wait 会自动解锁、但是对顺序有要求、如果在 notify 被调用之后、才开始调用 wait 方法、线程永远处于 waiting 状态
park 等待许可、unpark 为指定线程提供许可、但是不能叠加、其实就是一个标志位
多次调用 unpark 之后、再调用 park 线程会直接运行、但不会叠加、连续多次调用 park 只会第一次拿到许可执行、后续会进入等待状态。
官方建议应该在循环中检测等待条件、原因是处于等待状态的线程可能会收到错误警报和伪唤醒、如果不在循环中检查等待条件、程序就会在没有满足条件的情况下退出
伪唤醒是指线程并非因为 notify、notifyAll 、unpark 等 api 调用而唤醒、而是更底层原因导致的
值得注意的一点是、park 是不会释放锁的、这个也会造成死锁
锁降级指的是写锁降级成为读锁。锁降级是指把持住当前拥有的写锁的同时、再获取到读锁、随后释放写锁的过程。
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
// 缓存无效
if (!cacheValid) {
// Must release read lock before acquiring write lock
// 释放读锁
rwl.readLock().unlock();
// 尝试获取写锁
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
// 再次判断获取是否无效
if (!cacheValid) {
// 获取数据
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
// 锁降级
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
// 经过很长的时间做一些处理、而且不想我刚刚自己更新的数据被别人改了
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
如果只是希望最后使用数据的时候,拿到的是最新的数据,而不一定是自己刚修改过的数据,那么先解写锁,再上读锁,然后使用数据也无妨
lockInterruptibly 允许在等待时由其他线程调用等待线程的 Thread.interrupt 方法来中断等待线程的等待而返回,这时不用获取锁、而会抛出一个 InterruptedException
lock 方法不允许 Thread#interrupt中断、即即使检测到 Thread#isInterrupted 一样会继续尝试获取锁、失败则继续休眠、只是在最后获取锁成功之后才响应中断。
newFixedThreadPool 创建的任务队列是无界的、导致提交的任务可以无限的放置到该任务队列中(堆积大量的请求)、将内存涨爆
newSingleThreadExecutor 同样也是因为任务队列是无界的
newCachedThreadPool 的话、因为它使用的任务队列是 SynchronousQueue 、这个队列只能存储一个任务、而缓存线程池的最大线程数是 Integer.MAX_VALUE 可以创建无限多的线程、同样也会导致 OOM
newScheduledThreadPool 同样也是因为可以无限创建线程
用于执行低级别、不安全的方法。如直接访问/管理 内存资源、用于提升 Java 执行效率、增强 Java 对底层资源的操作能力。
Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力
使用堆外内存的原因
典型的应用:
DirectByteBuffer 是 Java 用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池、如 Netty、MINA 等NIO 框架中应用广泛。
DirectByteBuffer 对于堆外内存的创建、使用、销毁等逻辑均由 Unsafe 提供的 API 来实现
使用 Cleaner 对象用于跟踪 DirectByteBuffer 对象的垃圾回收、以实现当 DirectByteBuffer 被垃圾回收时、分配的堆外内存一起被释放。
Cleaner 继承自 Java 四大引用类型之一的虚引用、众所周知、无法通过虚引用获取与之关联的对象实例、且当对象仅被虚引用引用时、在任何发生 GC 的时候、其均可被回收。
当某个被 Cleaner 引用的对象被回收时、JVM 垃圾收集器会将此对象的引用加入到对象引用中的 pending 链表中、等待 Reference-Handler 进行相关处理、其中 Reference-Handler 为一个拥有最高优先级的守护线程、会循环不断的处理 pending 链表中的对象引用、执行 Cleaner 的 clean 方法进行相关清理工作
比较并替换
并发计算的时候经常使用到的一种技术、CAS 操作包含三个操作数、内存地址、期望值、新值。
执行 CAS 操作的时候、将内存地址的值与预期值进行比较、如果匹配、则将该内存地址的值设置为新值,否则处理器不做任何操作
CAS 是一条 CPU 的原子指令、不会造成所谓的数据不一致问题
典型应用
AtomicInteger、ConcurrentHashMap 都有应用到 CAS
带来的问题
AtomicInteger
AtomicReference
AtomicStampedReference 解决了 ABA 问题、加了版本号
非公平锁比公平锁有更好的性能、因为公平锁获取锁的线程永远都是等待最久的线程、而非公平锁可能是刚刚到来的线程、减少了上下文的切换
可重入锁
线程可以进入任意它已经获取了锁的同步代码块/方法中、最大的作用就是避免死锁
自旋锁
是指尝试获取锁的过程不会立即阻塞、而是采用循环的方式尝试获取锁。这样的好处是减少上下文切换、坏处是循环会消耗 CPU
一个老古董、它是与计算机硬件有关的一个概念。
计算机多核缓存多线程的问题、
为了保证共享内存的正确性、内存模型定义了共享内存系统中多线程程序读写操作行为的规范
通过这些规则来规范对内存的读写操作、从而保证指令执行的正确性。
内存模型解决并发问题主要采用的两种方式
是一种符合内存模型规范、屏蔽了各种硬件和操作系统的访问和差异、保证了 Java 程序在各种平台对内存的访问都能保证一致的机制和规范。
Java 内存模型规定了所有的变量都存储在主内存中、每条线程还有自己的工作内存。
线程的工作内存保存了该线程中用到的变量的主内存副本拷贝、线程对变量的所有操作都必须在工作内存中进行、而不能直接读写主内存。
JMM 就作用于工作内存和主存之间数据同步的过程、它规定了如何做数据同步以及什么时候做数据同步。
越是基础的类越是由上层的类加载器进行加载
第一次破坏、双亲委派机制未出的时候
第二次破坏
服务提供接口 Service Provider Interface SPI
一个典型的例子便是 JNDI服务、JNDI 现在已经是 Java 的标准服务、它的代码由启动类加载器来完成从、肯定属于 Java 中很基础的类型。
但 JNDI 存在的目的就是对资源进行查找和集中管理、它需要调用其他厂商实现的 SPI 代码,现在的问题是、启动类绝不可能认识或加载这些类。
为了解决这个问题、Java 的引入了一个不太优雅的设计
线程上下文类加载器。这个类加载器可以通过 Thread setContextClassLoader 方法进行设置、如果创建线程时没有设置、它将会从父线程中继承一个、如果在应用程序全局都没有设置过、那这个类加载器默认就是应用程序类加载器
第三次破坏
静态代理
动态代理
JDK 动态代理
实现 InvocationHandler
Proxy.newProxyInstance 创建一个代理对象
Cglib
JDK 和 Cglib 动态代理对比
代理对象的所有接口方法调用都会转发到 InvocationHandler.invoke 方法、
对于从 Object 中继承的方法、JDK Proxy 会把 hashCode equals toString 这三个非接口的方法转发给 InvocationHandler 、其余的 Object 方法不会转发
总体架构
AliasRegistry 其中的一个实现类 SimpleAliasRegistry
private final Map<String, String> aliasMap = new ConcurrentHashMap<>(16);
key 是alias,value 是 name
public interface Resource extends InputStreamSource
ResourceLoader 资源加载的抽象
DefaultResourceLoader
设计模式
相关组件
本质上没有什么区别只是在使用上有约束比如说 ChildBeanDefinition 必须有 父的 BeanDefinition、而 RootBeanDefinition 不能有。
在容器初始化的时候、往 BeanDefinitionRegistry 注册使用的是 GenericBeanDefinition
而在 getBean 的时候回将其整合为 RootBeanDefinition 继承它的父 BeanDefinition
循环依赖、就是两个或两个以上的 bean 互相持有对方。
Spring 中的循环包括
对于构造器循环、Spring 无法解决
对于 Setter 循环、Spring 只能解决 singleton 类型的。通过三级缓存来实现
第一级缓存 : 创建好并且是填充好属性完成初始化的 bean
第二级缓存 : 创建好但是没有完成填充和初始化的 bean
第三级缓存 : 包裹着创建好但是没有完成填充和初始化的 bean 的 ObjectFactory ,第二级缓存是来源于第三级的 getObject 方法返回的 bean
为啥三级缓存、二级不行吗?
其实是可以的、但是这样却无法给用户一个扩展接口。
假如我们取消了上面所说的第三级缓存、只留下第一级缓存和第二级缓存、那么当出现一个循环依赖的时候、我们就没办法在它获取一个没完成属性填充的 bean 的时候做一些额外的事情、因为这个时候你只是单纯的在一个 Map 里面取出一个 value、无法给用户一个扩展接口
假如我们取消了上面所说的第二级缓存、只留下第一级缓存和第三级缓存、那么当存在 A 与 B 互相依赖、A 与 C 互相依赖的时候、ObjectFactory 的 getBean 方法就会被调用两次、也就是说 SmartInstantiationAwareBeanPostProcessor 的 getEarlyBeanReference 会被调用两次。
假如我们把第二级缓存和第一级缓存整合成一个、那问题更加大了、其他线程直接从这一层缓存获取到一个没有填充属性没有初始化的 bean 直接使用、直接爆炸
为啥不能解决构造器循环依赖
扯蛋吧、都没办法创建出一个 bean 怎么搞
ApplicationContextAwareProcessor 实现了 BeanPostProcessor。
在 postProcessBeforeInitialization 中帮助
实现注入这些对象
private final List<BeanPostProcessor> beanPostProcessors;
在 ApplicationContext 中 它会去主动注册 BeanPostProcessor registerBeanPostProcessors
PriorityOrdered、Ordered 接口
PropertyEditor 是提供给 AWT 使用的。Java 也提供了默认实现 PropertyEditorSupport
我们使用 xml 配置文件为某个属性设置值的时候、写入的都是一个字符串类型、但是属性的实际类型可能是 Integer、Object 等、这个时候就需要做一层转换。
AOP 能将那些与业务模块无关、但是却公共的一些逻辑、比如说日志、事物处理等封装起来,减少了系统重复代码、降低了模块间的耦合,有利于扩展和维护
Spring AOP 就是基于动态代理的、如果代理的对象、实现了某个接口、那么 Spring AOP 会使用 JDK Proxy 去创建兑现、而对于没有实现接口的对象、就无法使用 JDK Proxy 去进行代理、这时候 Spring AOP 就会使用 cglib、这个时候 Spring AOP 会使用 CGlib 生成一个被代理对象的子类作为代理。
当然我们可以使用 AspectJ ,Spring AOP 已经集成了 AspectJ、AspectJ 应该算得上是 Java 生态系统中最完整的 AOP 框架。
Spring AOP 属于运行时增强、而AspectJ 是编译时增强。Spring AOP 基于代理、而 AspectJ 基于字节码操作。
如果我们切面比较少、两者性能差异不大、如果切末太多、最好选择 AspectJ 它比 Spring AOP 快很多。
常见的两种解决办法
声明式事务
TransactionDefinition 接口中定义了五个表示隔离级别常量
支持当前事务的
不支持当前事务的
其他的
在启动 Spring 容器的时候、有一个 AutowiredAnnotationBeanPostProcessor ,当扫描到有
Spring AOP 中动态代理主要有两种方式、JDK 动态代理和 CGLIB 动态代理
静态代理与动态代理区别在于生成 AOP 代理对象的时机不同、相对来说 AspectJ 的静态代理方式具有更好的性能、但是静态编译需要特定的编译器处理、而 Spring AOP 则无需特定的编译器处理。
https://juejin.im/post/6844903860658503693
https://blog.csdn.net/ThinkWon/article/details/104397516
类似 Java 的 ArrayList 采用预分配冗余空间的方式减少内存频繁分配
扩容机制
常用命令
set name1 value1
set name2 value2
mget name1 name2
mset name1 boy name2 girl name3 unknown
expire name1 5 #5s 后过期
setex name1 5 ljx
setnx name1 ljx
相当于 Java 中的 LinkedList 是链表
插入和删除操作非常快、时间复杂度为 O(1) 索引定位很慢 O(n)
当列表弹出最后一个元素后、该数据结构自动被删除、内存被回收
lpush listName values #往列表中的头部加入一个或多个元素
lpop listName#从列表中的头部取出一个元素
rpush listName values #往列表中的尾部加入一个或多个元素
rpop listName #从列表中的尾部取出一个元素
lindex listName index #相当于 Java 的 get(index)
lrange listName start stop #返回列表中指定区间的元素、0可以代表第一个元素、-1可以代表最后一个元素
redis 底层存储的不是一个简单的 linkedList 而是称为快速链表的一个结构
在列表元素较小的情况下、会使用一块连续的内存存储、即 ziplist、它将所有元素紧挨着存储、分配的是一块连续的内存、当数据量比较多才会改成 quicklist、因为普通链表的附加指针空间太大、会比较浪费空间、会加重内存碎片化
相当于 Java 中 HashMap 数据结构同样是数组+链表二维结构
当 hash 移除了最后一个元素之后、该数据结构会自动删除、内存被回收
不同的是 redis 中 hash 的 key 只能是字符串、另外 rehash 的方式也不一样,java 中的 rehash 是一次性进行 rehash 的、 是一个耗时操作、为了提高性能、不阻塞服务、所以采取了渐进性 rehash 策略
渐进性 rehash 会在 rehash 的同时、保留新旧两个 hash 结构、查询时会同时查询两个 hash 结构、然后再后续的定时任务重以及 hash 的子指令重、循序渐进将旧的 hash 内容一点点迁移到新的 hash 结构中
相当于 Java 中的 HashSet 、value 都是 null
当集合中最后一个元素移除后、数据结构自动删除、内存被回收
类似 Java 中 SortedSet 和 HashMap 的结合体、一方面是个 Set 保证内部value 的唯一性、另一方面给每个 value 赋予一个 score 代表这个 value 的排序权重、使用的是跳跃列表。
当集合中最后一个元素被移除后、数据结构自动删除、内存被回收
如果 score 值都是一样的、那么它会比较 value 的值
限流需求中存在一个滑动时间窗口、我们可以使用 zset 的 score 来圈出这个时间窗口。我们只要保留这个时间窗口、窗口之外的数据都可以砍掉。zset 的 value 填毫秒的时间戳
一个用户单独使用 zset 、每个请求作为这个 zset 的值存在里面、score 为请求到达的时间、value 为达到的时间戳
因为连续的几个 Redis 操作都是针对同一个 key 的、使用 pipeline 可以显著提升 Redis 的存取效率。
但是这中方式去做限流、缺点很明显、一定时间窗口内允许大量的请求的话、会花费大量的存储空间
使用 pipeline 的好处 https://www.jianshu.com/p/86ef67514a0f
multi 和 pipeline 的区别 https://juejin.im/post/6844903635252412430
漏斗算法
public class FunnelRateLimiter {
static class Funnel {
int capacity; // 漏斗的容量
float leakingRate; // 漏斗的流速
int leftQuota; // 漏斗的剩余容量
long leakingTs; // 上一次放水的时间
public Funnel(int capacity, float leakingRate) {
this.capacity = capacity;
this.leakingRate = leakingRate;
this.leftQuota = capacity;
this.leakingTs = System.currentTimeMillis();
}
void makeSpace() {
long nowTs = System.currentTimeMillis();
long deltaTs = nowTs - leakingTs;
int deltaQuota = (int) (deltaTs * leakingRate);//可释放的容量
if (deltaQuota < 0) { // 间隔时间太长,整数数字过大溢出
this.leftQuota = capacity; this.leakingTs = nowTs; return;
}
if (deltaQuota < 1) { // 腾出空间太小,最小单位是 1
return;
}
this.leftQuota += deltaQuota;
this.leakingTs = nowTs;
if (this.leftQuota > this.capacity) {
this.leftQuota = this.capacity;
}
}
boolean watering(int quota) { makeSpace();
if (this.leftQuota >= quota) {
this.leftQuota -= quota;
return true;
}
return false;
}
}
private Map<String, Funnel> funnels = new HashMap<>();
public boolean isActionAllowed(String userId, String actionKey, int capacity, float leakingRate) {
String key = String.format("%s:%s", userId, actionKey);
Funnel funnel = funnels.get(key);
if (funnel == null) {
funnel = new Funnel(capacity, leakingRate);
funnels.put(key, funnel);
}
return funnel.watering(1); // 需要 1 个 quota }
}
分布式的漏斗算法可以使用 hash 这个数据结构来完成。将 Funnel 对象的内容按字段存储到一个 hash 的结构中、灌水将 hash 结构的字段取出来之后进行逻辑运算、再将新值回填到 hash 结构、完成一次限流检查。
但是有个问题就是、这三个过程无法保证是原子性、如果想要原子性、那就要涉及到加锁控制、而一旦加锁、意味着加锁失败、加锁失败就意味着重试或者放弃
Redis 4.0 提供了限流模块 redis-cell、使用的就是漏斗算法 、并且提供了原子的限流指令
除 Redis 之外、Node.js 也是单线程的、Nginx 也是单线程的
因为所有的数据都在内存中、所有的运算都是内存基本的
多路复用 API :Epoll
事件轮询 API、操作系统提供给用户程序的
输入是读写描述符、输出是预知对应的可读可写事件。同时提供一个 timeout 参数、如果没有任何事件到来、那么就等待 timeout 时间,线程就处于阻塞状态。一旦期间有任何事件到来就可以立即返回。
这个其实是一个死循环、事件循环。
服务器套接字 serversocket 对象的读操作是指调用 accept 接受客户端新连接
Redis 会将每个客户端套接字都关联一个指令队列、客户端的指令通过队列来排队进行顺序处理、先到先服务。
Redis 同样会为每个客户端套接字关联一个响应队列。Redis 服务器通过响应队列来将指令的结果返回给客户端。
服务器除了要响应 IO 事件外、还要除了其他事情、比如定时任务就是非常重要的一件事、如果线程阻塞在 select 系统调用上、定时任务将无法得到准时的调度
Redis 的定时任务会记录在一个称为最小堆的数据结果中、这个堆中、最快要执行的任务排在堆的最上方。在每个循环周期、Redis 都会将最小堆里面已经到点的任务立即进行处理。处理完毕后、将最快要执行的任务还需要的时间记录下来、这个时间就是系统调用的 timeout 参数。因为 Redis 知道未来的 timeout 时间内、没有其他定时任务需要处理、所以安心的进行系统调用等待
分布式锁的本质实现的目标就是在 redis 里面占一个茅坑、当别的进程要进来占的时候、发现已经有人蹲了、只好放弃或稍后再试
占坑使用的是 setnx (set if not exists) 只允许一个客户端占坑、先到先得
使用完之后、调用 del 删除茅坑
setnx lockName value
do something
del lockName
但是有可能异常并没有执行 del 操作、那么锁就会得不到释放、那么我们可以在拿到锁之后加上一个过期时间、保证锁会被释放
setnx lockName value
expire lockName 5
do something
del lockName
但是也有可能在 setnx 和 expire 之间进程突然挂掉、导致 expire没有执行、也会造成死锁
这种问题的一个原因就是 setnx 和 expire 两个指令不是一个原子操作
redis 2.8 中加入了 set 的扩展参数、setnx 和 expire 可以一起执行
set lockName value ex 5 nx
在加锁和释放锁之间执行时间太长、导致 redis 过期删除了锁、那么这个时候、第二个线程重新持有了这把锁、紧接着第一个线程就把这个锁释放掉、那么第三个线程就会在第二个线程逻辑执行完拿到了这个锁。
为了避免这个问题、redis 分布式锁不要用于执行较长时间的任务
还有一个比较安全的做法是、就是 set 指令存放的 value 是一个随机数、释放锁之前先判断锁释放一致、然后再删除 key、
但是比较 value 和 删除不是一个原子操作、redis 也没有提供相关的指令、这个时候可以使用 lua 脚本、因为 lua 脚本可以保证多个指令的原子性执行
Redisson 有解决这个问题、当发现超时后、业务还没执行完、会有监控线程去重新设置这个时间
https://juejin.im/post/6844903830442737671
可重入锁是指线程在持有锁的情况下、再次请求锁。如果一个锁支持同一个线程多次获取锁、那么这个锁就是可重入锁
Redis 中分布式锁要支持可重入、那么就要重写 set 方法、使用 ThreadLocal 存储当前持有的锁
为了使用 RedLock 需要提供多个 Redis 实例、这些实例之间相互独立没有主从关系、同很多分布式算法一样、redLock 也是采用大多数机制
加锁时、它会向过半节点发送 set key value ex exTime nx 指令、只要过半节点成功、则认为加锁成功、释放锁时、需要向所有节点发送del 指令
Redis 的 list 数据结构可以作为消息队列、使用 lpush、rpush 结合使用 rpop lpop
如果队列空了的话、那么客户端就会陷入 pop 的死循环、不断的 pop
这不仅浪费客户端的 cpu 而且也会拉高 redis 的 qps
在客户端可以通过 sleep 来实现、让线程休眠、但是这样子会导致消息的延迟。虽然客户端的 CPU 下来了、Redis 的 QPS 也下来了
这个时候我们可以使用 blpop / brpop
阻塞读是在队列没有数据的时候进入休眠、而在消息到来的时候马上醒过来、消息的延迟几乎为 0
但是需要主要的是、如果客户线程一致阻塞在那里、那么 redis 会把这个链接当成是空闲链接、闲置过久、服务器会主动断开这个链接、
这个时候 brpop blpop 都会抛出异常、注意捕获异常重试
可以使用 zset 来实现、以消息的到达时间为 score 、然后客户端多个线程轮询获取 zset 的到期任务进行处理
考虑并发问题、zrem 返回大于 0 才算成功夺取到任务
位图不是特殊的数据结构、内容其实就是普通的字符串、也就是 byte 数组。可以使用 get/set 设置或获取整个位图的内容。
也可以使用getbit / setbit把它作为数组处理
统计和查找
位图统计指令 bitcount 用来统计指定范围内 1 的个数
位图查找指令 bitpos 用来查找指定范围内出现的第一个0 或 1
指定范围 [ start end ] 必须是 8的倍数
布隆过滤器可以理解为不怎么精确的 set 结构、当你使用 contains 方法的时候、它可能会误判
Redis 官方提供的布隆过滤器在 4.0 的时候才以插件的形式出现
布隆过滤器有两个指令、一个是 add 一个是 exist
布隆过滤器对于已经见过的元素肯定不会误判、只会误判那些没见过的元素
布隆过滤器有两个参数、一个是错误率一个是初始大小
初始大小过大、浪费空间、过小导致错误率上升
向布隆过滤器添加 key 时、会使用多个 hash 函数对 key 进行 hash 、每个 hash 都会算到一个不同的位置、然后再把对应位置的值变为1
exist 则是判断其对应的各个位置是否都是为1 、只要有一个不为1 则这个key 是不存在于布隆过滤器中的
redis 会将每个设置了过期时间的 key 放入到一个独立的字典中、以后会定期的遍历整个集合、来删除过期的 key
除了定时遍历之外、还会采用惰性删除过期的 key 、所谓的惰性删除就是每次客户端来获取key的时候、发现这个 key 已经过期了、就会将其删除掉
定时删除时集中处理、惰性删除时零散处理
每秒进行十次
为了保证扫描不回出现循环过度、导致线程卡死现象、算法还增加了扫描时间上限、默认不回超过 25ms
从库不回进行过期扫描、所以主库过期的 key 的 del 指令没有及时同步到从库的话、会出现主从数据的不一致。主库没有的数据在从库还存在。比如上面说的分布式锁的算法漏洞就是因为同步延迟产生的。
有两种方式
redis 使用操作系统的多进程、cpw copy on write 机制来实现快照持久化
redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,父进程继续处理客户端请求、子进程和它的父进程共享内存里面的代码段和数据段。子进程做数据持久化、他不会修改内存数据结构、只会对数据结构进行遍历读取、然后持久化到磁盘中。
而父进程响应客户端的请求、对内存数据结构进行修改。
这个时候会使用到操作系统的 copy on write 机制来对数据段页面进行分离
数据段是由很多操作系统的页组成的、当父进程对其中一个页面进行数据修改的时候、只会将那个共享页复制一份出来,只对这个复制出来的页面进行修改。所以对子进程来说、数据还是 fork 进程那一瞬间的数据
aof 日志存储的是 redis 服务器的顺序指令序列、AOF 只记录对内存进行修改的指令记录
Redis 会在收到客户端修改指令后、先进行参数检验、如果没有问题、就立即将该指令文本存储到 AOF 中、也就是先存到磁盘、然后再执行指令
提供了 bgaofrewrite 指令对 aof 日志进行瘦身、远离就是开辟一个子进程对内存进行遍历转换成一系列 Redis 操作指令、序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到新的 AOF 文件中、追加后替换旧的 AOF 日志文件】瘦身工作就完成了。
aof 日志是以文件的形式存在的、当程序对 aof 进行 write 操作时、实际上是写到了内核文件描述分配的一个内存缓冲中、然后内核会异步将脏数据刷新到磁盘。这就意味着如果机器突然宕机,AOF 日志内容可能还没有来得及完全刷到磁盘中,这个时候就会出现日志丢失
Linux 提供了 fsync 函数可以将指定文件的内容强制从内核刷新到磁盘。只要 Redis 进程实时调用 fsync 函数就可以保证 aof 日志不丢失。但是 fsync 是一个磁盘 IO 操作、他很慢。如果Redis 没执行一条指令就要 fsync 一次,那么Redis 高性能的地位就不保了。
三种刷盘策略
重启 Redis 时、我们很少使用 rdb 来恢复内存、因为会丢失大量的数据、我们通常使用 AOF 日志重放、但重放 AOF 日志性能相对于 rdb 来说满的很多。这样在 Redis 实例很大的情况、启动需要花费很长的时间。
Redis 4.0 为了解决这个问题、提出了新的持久化策略–混合持久化。将 rdb 文件的内容和增量的 AOF 文件存在一起。这里的 AOF 不再是全量日志而是自持久化开始之后的
于是在 Redis 重启的时候、先加载rdb 的内容、然后重放增量 AOF 日志即可
完全代替之前的 AOF 全量文件重放、因此重启效率大幅提升。
配置参数 maxmemory 来限制使用内存的大小
当使用的内存大小大于配置的内存大小、redis 提供了几种可选的策略来让用户决定该如何腾出新的空间以继续提供读写服务
处理 key 过期方式有几种处理和懒惰处理、但是对于 LRU 的淘汰只能是懒惰处理
当 redis 执行写操作的时候、发现内存超出 maxmemory、那么就会采样出 5 个 key、然后淘汰掉最旧的 key、如果淘汰之后还是超出 maxmemory 那就继续随机采样、直至低于 maxmemory
如何采样就是看 maxmemory policy 的配置的、如果是 allkeys 则从所有的 key 中、如果是 volatile 就从设置了过期时间的 key 中。
每次采样的个数、则是通过 maxmemory_samples 的设置、默认为5
淘汰组是一个数组、它的大小是 maxmemory_samples 在每一次淘汰循环中、新随机出来的 key 列表和 淘汰池中的 key 进行融合、淘汰掉最旧的 key 之后、保留剩余较旧的 key 放入淘汰组、等待下一个循环
每个 key 都增加了一个额外的小字段、这个字段的长度是 24个 bit 也就是最后一次访问的时间戳
redis 内部实际上并不是只有主线程、还有几个异步线程专门来处理一些特别耗时的操作
del —> unlink
对删除操作进行懒处理、丢给后台线程异步回收内存
主线程将对象从 “大树” 中摘除后、会将这个key 的内存回收操作包装成一个任务、塞进一部任务队列、后台线程会从这个异步队列中取任务。
不是所有的 unlink 操作都要延后处理、如果 key 所占用的内存很少、延后处理就没有必要、这个时候 redis 会将对应的 key 内存立即回收、跟 del 指令一样
从海量 key 中查出特定规则的 key、最简单暴力就是使用 keys 命令
但是它是有缺点的
增加了一个新指令 scan
scan cursor Match keyPattern Count countNumber
我们可以将 sentinel 集群看作是一个 ZK 集群、它是集群高可用的心脏、一般由 3-5个节点、这样子挂了个别节点、集群还能正常运转
sentinel 负责持续监控主从节点的健康、当主节点挂掉时、自动选择一个最优的从节点切换为主节点。
客户端来连接集群时、会首先连接 sentinel 通过sentinel 查询主节点的地址、然后再去连接主节点进行数据交互、当主节点发生故障时、客户端会重新 sentinel要地址、sentinel 会将最新的主节点地址告诉客户端。如此一来应用程序就可以自动完成节点的切换
旧的 Master 重新上线
Redis 采用异步复制、意味着当主节点挂掉时、从节点可能没有收到全部同步消息、这部分同步消息就丢失了。如果主从延迟特别打、那么丢失的数据就可能会特别多。sentinel无法保证消息完全不丢失,但是尽可能保证消息少丢失。有两个选项可以限制主从延迟过大
第一个参数表示主节点必须至少有一个从节点再进行正常复制、否则就停止对外写服务、丧失可用性
何为正常?第二个参数就是控制整个的、单位是秒、表示如果 10s 没有收到从节点的反馈、就意味着从节点不正常、要么网络断开、要么一直没有非反馈
sentinel 进行主从切换的时候、客户端如何知道地址变更了
Codis 时无状态的、知识一个转发的代理中间件
每个槽位都会映射到后面多个 Redis 之一
不同Codis 实例之间槽位关系如何同步?
将槽位关系存储在 zk 上、并且提供了 dashboard 可以用来观察和修改槽位关系,当槽位关系发生变化时、Codis Proxy 会监听到变化并重新同步槽位关系
扩容时如何找到槽位对应的 key
在迁移的过程中、 codis 还是会接收到新的请求搭载当前正在迁移的槽位上、因为当前槽位的数据同时存在新旧两个槽位中、当Codis 接收到位于前一槽位中的key后、会立即强制对当前的单个key 进行迁移、迁移完成后、再将请求转发到 新的 Redis 实例。
Codis 的代价
优点
去中心化
将所有数据划分为16384 个槽、相比 codis 的1024 更为精细,每个节点负责其中一部分槽位。槽位的信息存储在每个节点。
当客户端来连接集群时、他会的到一份集群槽位的配置信息、这样、但客户要查找某个 key 时、可以直接定位到目标节点
Redis 集群采用 Gossip 协议来广播自己的状态以及自己对整个集群认知的改变、比如发现某个节点失联了、它会将这个信息向整个集群广播、其他节点也就可以收到这个失联信息、如果一个节点收到了某个节点失联的数量已经到达集群的大多数、就可以标记为该节点下线、然后向整个集群广播、强迫其他节点也接受这个节点下线的事实
mutil 指示事务开始
exec 指示事务执行
discard 指示事务丢弃
事务仔遇到指令失败后、后面的指令还继续执行
Redis 的事务根本不能算原子性、仅仅是满足事务的 隔离性、隔离性中的串行化、当前执行的事务有着不被其他事务打断的权利
Redis 为事务提供了一个 discard 指令、用于丢弃事务缓存队列中所有的指令、执行 exec 之前
可以结合 pipeline 进行优化
乐观锁
watch 会在事务开始之前盯住一个或多个关键变量、当事务执行时、也就是服务器收到 exec 指令要顺序执行缓存的事务队列时、Redis 会检测关键变量自 watch 知乎、是否被修改了、包括当前事务所在的客户端。如果关键变量被人动过了、exec 指令就会返回 null 回复告知客户端事务执行失败
Redis 禁止在 mutil 和 exec 之间执行 watch 指令、而必须在 mutil 之前做好盯住关键变量、否则就会出错
Redis 并不总是可以将空闲内存立即归还操作系统
如果当前 Redis 内存有 10G 、当你删除 1GB 的 key 之后、你再去观察内存、内存不有太大的变化。因为操作系统回收内存是以页为单位的,如果这个页上只要有一个 key 还在使用、那么他就不能被回收。所以即使你删掉了 1GB 的key 但是这些key 分散在很多页面中、每个页面还有其他key 存在、导致内存不会马上被回收
如果你执行 flushdb 再观察内存、会发现内存确实被回收了、原因是所有的key都被干掉了、大部分之前使用的页面都干净了、会立即被操作系统回收
虽然 Redis 无法保证立即回收已经删除的 key 的内存、但是会重用那些尚未被回收的空闲内存、就好比电影院的人走了、但是座位还在、下一波观众来了、直接坐就行了、而操作系统回收则是把作为都给搬走
CAP 原理
Redis 支持主从同步和从从同步、从从同步时新增功能、为了减轻主库同步负担
Redis 同步是指令流的、主节点会将那些对自己的状态产生修改的指令记录在内存的 buffer 中、然后异步的将 buffer 的指令同步到从节点、从节点一遍执行主节点同步过来的指令流达到和主节点一样的状态、像主节点反馈自己同步到哪里了(偏移量)
因为内存的 buffer 是有限的、所以 Redis 主库不能将所有的指令都记录在内存 buffer 中、Redis 复制内存buffer 是一个定长的环形数组、如果数组内容满了就会从头开始覆盖前面的内容
如果因为某些原因、从节点在短时间无法和主节点进行同步、Redis 的主节点那些没有同步的指令在 buffer 中已经被覆盖了、那么从节点不能通过指令流来进行同步、这个时候需要用到更加复杂的同步机制、快照同步
快照同步时一个非常耗费资源的操作、他首先需要在主库进行一次 bgsave 将当前内存数据全部快照到磁盘中、然后再将快照文件的内容全部传送到从节点。从节点将快照文件接受完毕之后(收到之后落盘)、立即执行一次全量加载、加载之前清空内存的数据、加载完之后再通知主节点进行增量同步
再整个快照同步进行的过程中、主节点的复制 buffer 还在不停的往前移动、如果快照同步的时间过长或复制 buffer 过小、都会导致同步期间的复制buffer 再次被覆盖、这样会导致再次发起快照同步
当从节点刚刚加入到集群时、它必须先进行一次快照同步、同步完成之后在进行增量同步
所谓的无盘复制、指主服务器直接通过套接字将快照内容发送到从节点、生成快照时一个遍历过程、主节点会一边遍历内存、一边将序列化内容发送到从节点、从节点还是跟之前一样、先将接收到内容存储到磁盘、然后一次性加载
Redis 的复制是异步执行的、wait 指令可以让异步复制变成同步复制
wait 1 0
第一个参数是从库的数量、第二个是时间、单位是毫秒
表示等待 N 个从库同步、最大等待时间为 t 如果为 0 则达标为无线等待
wait 阻塞服务
在 Kafka 中还有两个特别重要的概念、主题和分区。Kafka 中的消息以主题为单位进行归类、producer 负责将消息发送到特定主题、而消费者负责订阅主题进行消费。
主题是一个逻辑上的概念、它还可以细分为多个分区、一个分区只属于一个主题,可以称它为主题分区。
Kafka 保证的是分区有序而不是主题有序。
Kafka 为分区引入了多副本机制、通过增加副本数量可以提升容灾能力。同一分区的不同副本保存的是相同的消息(在同一时刻,副本之间并发完全一样),副本之间是 一主多从的关系、其中 Leader 副本负责处理读写请求、follower 副本只与 leader 副本的进行消息同步。
副本位于不同的 broker 中、当leader 副本出现故障时、从 follower 副本中重新选举新的 leader 副本对外提供服务,kafka 通过多副本机制实现了故障自动转移。
Kafka 消费端也具备一定的容灾能力、Consumer 使用拉模式从服务端拉取消息、并保存消费的具体位置、当消费者当即后恢复上线时可以更加之前的消费位置继续拉消息、不会造成消息丢失。
分区中的所有副本统称为AR assined replicas 、所有与 leader 副本保持一定程度同步的副本 包括 leader 副本组成 ISR in sync repilcas ,ISR 是 AR 的一个子集。
消息会先发到 leader 副本、然后 follower 副本才能从 leader 副本中拉取消息进行同步,同步期间、follower 副本相对于 leader 副本会有一定程度的滞后。这时一个可接受的滞后范围
与 leader 副本滞后过多的副本、不包括 leader 副本、组成 OSR out of sync replicas
AR = ISR + OSR 、正常情况下、OSR 应该为 空
leader 副本负责维护和跟踪 ISR 集合所有 follower 副本滞后的状态、当 follower 副本落后太多或失效时、leader 副本会把它从 ISR 集合中剔除。如果 follower 副本追上了 leader 副本、那么 leader 副本会把它从 OSR 及中中转移至 ISR 集合。
默认情况下、当 leader 副本发生故障时、只有在 ISR 集合中的副本才有资格被选举为新的 leader、而在 OSR 及中中的副本没有任何机会
ISR 和 HW hight watermark 以及 LEO log end offset 有紧密的关系
消费者只能拉取 HW 之前的消息、而 LEO 标识当前日志文件中下一条待写入消息的 offset。LEO 的大小相当于当前日志分区中最后一条消息的offset 加 1 。
分区 ISR 集合中每个副本都会维护自身的 LEO、而 ISR中最小的 LEO则为分区的 HW、对消费者而言只能消费HW之前的消息。
由此可见、Kafka 的复制机制既不是完全的同步复制也不是单纯的异步复制。事实上、同步复制要求所有工作的 follower 副本都复制完、这条消息才被认为成功提交,这种方式极大地影响了性能。而在异步复制方式下、follower 副本异步地从 leader 副本中复制数据、数据只要被 leader 副本写入就被认为是成功提交、这种情况下、如果 follower 副本还没有完全复制完 leader 副本、突然 leader 宕机、则会造成数据丢失。
Kafka 使用的这种 ISR 的方式有效地权限了数据可靠性和性能之间的关系
producer 是线程安全的、可以在多个线程中共享单个 kafka Producer 实例
对于同一个分区而言、如果消息 records1 与 records2 之前发送、那么 producer 就可以保证对应的 callback1 在 callback2 之前调用、也就是说 回调也可以保证分区有序。
生产者需要用序列化器将对象转换成字节数组才能通过网络发送给 Kafka、而消费者需要用反序列化器将字节数组转换成响应的对象。
消息通过 send 方法发往 broker 的过程中、有可能需要经过拦截器、序列化器和分区器的一系列作用滞后才能被正在的发往 broker
如果在 record 中指定了 partition 字段、那么就不需要分区器、如果没有指定则依赖分区器、根据 key 这个值来计算 partition 的值、分区器的作用就是为消息分配分区。
如果 key 部位 null 则默认的分区器对 key 进行哈希、然后根据hash 值来计算分区号、如果 key 为 null 那么消息将会以轮询的方式发往主题的各个可用分区。
生产者拦截器、消费者拦截器
主线程和 sender 线程、缓存到 RecordAccumulator 中、Sender 从 RecordAccumulator 中获取消息将其发送到Kafka
消息收集器的作用是为了Sender 线程能够批量发送、进而减少网络资源的消耗提升性能
ProducerBatch 一个消息批次、ProducerRecord 被包含在 ProducerBatch 中
Sender 从 RecordAccumulator 中获取缓存消息之后、会进一步将原本的 <分区,Deque> 保存形式变为 然后 请求从 Sender 线程发往Kafka 之前还会保存到 InFlightRequests 中、InFlightRequests 保存对象的具体形式为 Map 从 InFlightRequests 中找到负载最小的 Node、然后获取和更新这些元数据 元数据是指 Kafka 集群的元数据、这些元数据具体记录了集群中有哪些主题、这些主题有哪些分区、每个分区的 leader 副本在哪里节点上、follower 副本分配在哪些节点上、那些副本在AR ISR 中、集群中有哪些节点、控制器又是哪一个等信息。 当客户端没有需要使用的元数据信息时、或者到期更新元数据就会发起请求去负载最低的节点中获取元数据 消费者负责订阅Kafka 中的主题、并且从订阅的主题上拉取消息。 在 Kafka 的消费中还有消费组、每个消费者都有一个对应的消费组、当消息发布到主题后、只会投递给订阅它的每个消费组的一个消费者 如果消费者过多、出现了消费者的个数大于分区的个数、就会有消费者分配不到任何分区 一个正常的消费逻辑需要具备一下几个步骤 消息的消费一般有两种模式、推模式和拉模式。推模式时服务端主动将消息推送给消费者、而拉模式时消费者主动向服务端发起请求来拉取消息。 Kafka 时基于拉模式的 可以简单地认为 poll 方法只是拉取一下消息而已、但就其内部逻辑而言并不简单、它涉及消费的位移、消费者协调器、组协调器、分区分配的分发、再均衡的逻辑、心跳等内容。 消费者的位移存储在 Kafka 内部主题 __consumer_offsets 对于位移的提交的具体时间的把握也很有把握、可能会造成重复消费和消息丢失的现象。 当前 poll 操作所拉取的消息集为 [x+2,x+7]、x+2 代表上上一次提交的消费位移、说明已经完成了一次 x+1 之前包括x+1的所有消息的消费 x+5 表示当前正在处理的位置,如果拉取到消息之后就位移提交了、也就是提交 x+8、那么当消费 x+5 的时候遇到异常、在故障恢复之后、我们重新拉取的消息从 x+8开始的、也就是说、x+5 到 x+7之间到消息并没有被消费、如此便发生了消息丢失的现象。 如果位移提交的动作是在消费完所有拉取到的消息之后菜执行的、那么当消费 x+5的时候遇到异常、在故障恢复之后、我们重新拉取的消息是从 x+2 开始的、也就是 x+2到 x+4之间到消息又重新消费了一遍、 在 Kafka 中默认的消费位移提交的方式是自动提交、当然这个默认提交不是没消费一条消息就提交一次、而是定期提交、这个定期提交的周期时间默认是 5s、 在默认的情况下、消费者每隔5s会将拉取到的每隔分区中最大的消息位移进行提交。提交的动作是在poll 方法的逻辑里完成的。每次真正向服务端发起拉取请求之前会检查释放可以进行位移提交、如果可以、就提交上次轮询的位移。 commitAsync 提交的时候同样也会有失败的情况发生、我们可以设置一个在本保存它提交的位移、每次提交前就改变它、再遇到位移提交失败需要重试的时候、可以检查所提交的位移和序号的值的大小,如果提交的位移小于序号的值、那就说明有更大的位移已经提交了,不需要进行重新提交、如果两者相同、则可以提交。代码正确的情况下、是不会出现提交位移的大小大于序号的大小的。 当一个新的消费组简历的时候、他根本没有可以查找的消费位移、或者消费组订阅了一个新的主题、他也没有可以查找的消费位移。 在 kafka 中每当消费者查找不到所记录的消费位移时、就会根据消费者客户端的桉树的配置来决定从何处开始进行消费、这个参数默认值时 latest 表示从分区末尾开始消费消息。如果将参数配置为 earliest 那么消费者会从最开始处消费。 当然还可以将其设置为 none 那么当找不到消费位移的时候就会报错 指分区的所属权从一个消费者转移到另一个消费者的行为 在再均衡期间、消费组内的消费者无法读取到消息。在再均衡发生的这小段时间内、消费组会变得不可用、另外、当一个分区重新分配给另一个消费者时、消费者当前的状态就会丢失。比如消费者消费完某个分区的一部分消息时还没来得及提交位移消费就发生了再均衡、之后这个分区分配给了另一个组内的消费者、原来被消费的那部分消息又被重新消费一遍、也就发生了重复消费。 可以配合 ConsumerRebalanceListener 里面的两个方法、一个是在再均衡开始之前和消费者停止读取消息之后被调用,可以通过这个方法提交消费位移 还有一个方法是在再均衡完成之后消费可以开始读取消费之前被调用 或者配合 seek 指定拉取的位置 KafkaProducer 是线程安全的、KafkaConsumer 却是非线程安全的,consumer 中定义了一个 acquire 方法用来检查当前是否只有一个线程在操作、若有其他线程正在操作则会抛出 ConcurrentModificationException 优点是每个线程可以按顺序消费各个分区中的消息、缺点也很明显每个线程都在维护一个独立的TCP 连接 这个会造成不小的系统开销 每个处理消息的 RecordHandler 类在处理完消息之后都将对应的消费位移保存到共享变量 offsets 中 每次 poll 之前将其提交 但是这样子存在风险、假如有个处理线程 RecordHandler1 正在处理 0-99 的消息、而另一个 RecordHandler2 已经处理完 offset 为 100-199 的消息并进行位移提交、此时如果 1 发生异常、则之后消费只能从 200 开始而无法再次消费 0-99的消息、造成消息丢失的现象。 max.poll.records 配置 consumer 在一次拉取请求中拉取的最大消息数、默认值为 500条、如果消息的大小都比较小、可以适当调大这个参数值来提升一定的消费速度 hearbeat.interval.ms 默认值 3000 当使用 Kafka 分组管理功能时、心跳到消费者协调器之间的预计时间、心跳用于确保消费者绘画保持活动状态、当所有消费者加入或离开时方便重新平衡、该值必须比 session.timeout.ms 小、通常是 1/3 也可以调整得更低。 session.timeout.ms 默认值 10000 组管理协议中用来检查消费者释放失效的超市时间 max.poll.interval.ms 默认值 300000 当通过消费组管理消费者时、该配置指定拉取消息最长空闲时间、若超过这个时间间隔还没发起 poll 操作、则消费组认为该消费者已经离开了消费组、将进行再均衡操作 分区使用多副本机制来提升可靠性、但只有 leader 副本对外提供读写服务、而 follower 副本只负责在内部进行消息同步 提供两种日志清理策略 Kafka 是依赖于磁盘来存储和缓存消息的 顺序写盘的速度不仅比随机写盘的速度快、而且比随机写内存的速度快。 Kafka 在设计时采用了文件追加的方式来写入消息、即只能在日志文件的尾部追加新的消息、并且不允许修改写入的消息、这种方式属于典型的顺序写盘操作,所以计算Kafka 使用磁盘作为存储介质、他所能承载的吞吐量也不容小觑 页缓存是操作系统实现的一种主要的磁盘缓存、以此来减少对磁盘 IO 的操作,具体来说就是将磁盘的数据缓存到内存,把磁盘的访问变为对内存的访问 Linux操作系统中的参数用来指定当脏页数量达到系统内存的百分之几后会触发 pdflush 处理脏页 对一个进程而已、他会在进程内缓存处理所需的数据、然而这些数据有可能还缓存在操作系统的页缓存中、同一份数据可能被缓存两次、除非使用 Direct IO 的方式 此外 Java 对象的内存开销非常大、通常是真实数据大小的几倍甚至更多、空间使用率低下。 还有就是垃圾回收会随着堆内数据增多而变得越来越慢 所以使用操作系统的页缓存不仅可以省去来进程内的缓存消耗,同时还可以通过结构紧凑的字节码代替对象节省更多的空间、而且大内存的时候不用担心 GC 带来的性能问题。此外 Kafka 重启 页缓存依然是有效的、页缓存和文件之间的一致性交由操作系统来负责、比进程内维护更加安全有效。 Kafka 使用了大量的页缓存、这也是 Kafka 实现高吞吐的重要因素之一。 零拷贝指的是数据直接从磁盘文件复制到网卡设备中、而不需要经由应用程序之手。零拷贝大大提高了应用程序的性能、减少了内核和用户模式之间的上下文切换。 从上面的过程中、数据平白无故地从内核模式到用户模式走了一圈、浪费了两次复制过程、第一次是从内核模式复制到用户模式、第二次是从用户模式再复制回内核模式、上面的 2、3 步。而且在上面的过程中内核和用户模式的上下文切换也是 4 次。 零拷贝技术通过 DMA 技术将文件内容复制到内核模式下的 Read Buffer 中、不过没有数据复制到Socket Buffer 相反只有包含数据的位置和长度信息的文件描述符被加到 Socket Buffer 中。DMA 引擎将数据从内核模式中传递到网卡设备、这里只经历了2次复制就从磁盘中创送出去、上下文切花也变成了 2 次。 零拷贝是针对内核模式而言的、数据在内核模式下实现了零拷贝。 Kafka 集群中会有一个或多个 broker ,其中一个 broker 会被选举为控制器,他负责管理整个集群中所有分区和副本的状态、当某个分区的 leader 副本出现故障时、由控制器复制为该分区选举新的 leader 副本、当检查到某个分区的 ISR 集合发生变化时,由控制器负责同志所有 broker 更新其元数据信息 服务提供者 高可用的时候 然后配置集群的某个地址 服务消费者 再在 RestTemplate 加上 @LoadBalance 注解 工具类框架 使用客户端负载均衡调用非常简单 负载均衡策略 Eureka 服务治理机制强调了 CAP 原理中的AP、即可用性和可靠性、他与 ZK 强调 CP 一致性、可靠性 的服务治理框架最大的区别就是、Eureka 为了实现更高的服务可用性、失去了一定的一致性 、在极端情况下它宁愿接受故障的实例也不要丢弃”健康“的实例。 具备功能、服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等 @EnableCircuitBreaker 开启断路器功能 @EnbleFeignClients 写锁比读锁有更高的优先级、一个写锁请求可能会被插入到读锁队列的前面。 acid 死锁是指两个或多个事务在同一资源上相互占用、并请求锁定对方占用的资源、从而导致恶性循环的现象 为了解决这种问题、InnoDB 存储引擎、检测到死锁的循环依赖、并立即返回一个错误。还有一种解决方式、查询时间到达锁等待超时的设定后放弃锁请求。 InnoDB 目前处理死锁的方法是、将持有最少行排它锁的事务进行回滚 事务日志采用追加的方式、顺序IO 预写式日志、修改数据需要写两次磁盘 InnoDB 采用两阶段锁定协议、在事务执行过程中、随时都可以执行锁定、锁只有在执行 Commit 或者 Rollback 的时候才会释放、并且所有锁是在同一时刻被释放的 显式锁定 可以认为 MVCC 是行级锁的一个变种、但是它在很多情况下避免了加锁操作、因此开销更低。 MVCC 的实现、是通过保存数据在某个时间点的快照来实现的、也就是说、不管需要执行多长时间、每个事务看到的数据都是一致的。 根据事务开始的时间不同、每个事务对同一张表、同一时刻看到的数据可能是不一样的。 InnoDB 的 MVCC 通过在每行记录后面保存两个隐藏的列来实现、这两个列、一个保存行创建的系统版本号、一个保存行的删除时候的系统版本号。每开始一个新的事务、系统版本号就会递增、事务开始的时刻系统版本号会作为事务的版本号、用来和查询每行记录的版本号进行比较。 MVCC 只在 RR 和 RC 两个隔离级别下工作、其他的隔离级别都和 MVCC 不兼容。 InnoDB 的数据存储在表空间中、由一系列的数据文件组成。InnoDB 可以将每个表的数据和索引放在单独的文件中。 InnoDB 采用 MVCC 来保持高并发、并且实现了四个标准的隔离级别。默认级别是 RR、并且通过间隙锁策略防止幻读的出现。间隙锁使得 InnoDb 不仅仅锁定查询涉及的行、还会对索引中间的间隙进行锁定、以防止幻影行插入 。 InnoDB 表基于聚集索引建立的、聚集索引对主键查询由很高的性能、不过二级索引必须包含主键列、所以如果主键列很大的话、其他的所有索引都会很大、因此、若表上的索引很多的话、主键应当尽可能小 InnoDB 内部做了很多优化、包括磁盘读取数据时采用的可预测性预读、能够在内存中创建 hash 索引以加速读操作的自适应哈希索引、以及能够加速插入操作的插入缓冲 不支持事务和行级锁 MyISAM 会将表存储在两个文件中、数据文件和索引文件 如果 Mysql 在执行查询过程中需要使用到临时表来保存中间结果、内部使用的临时表就是 Memory 表、如果超出了 Memory 表的限制、或者含有 Text 字段、则临时表转换成 MyISAM 表 varchar 适合用 varchar char 适合用 char B-Tree 通常意味着所有的值都是按顺序存储的、并且每个叶子页到根的距离相同 B-Tree 索引能够加快访问数据的速度、因为存储引擎不再需要进行全表扫描来获取需要的数据、取而代之的是从索引的根节点进行搜索。根节点的槽中存放了指向子节点的指针、存储引擎根据这些指针向下查找。通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点、这些指针实际上定义了子节点页中值的上限和下限。最终存储引擎要么找到对应的值、要么该记录不存在 。 B-Tree 对索引列是顺序组织存储的、所以很适合范围查找。 索引对多个值进行排序的依据是创建索引时的顺序、 B-Tree 索引适用于全键值、键值范围或键前缀查找(最左前缀查找) 索引还可以用于查询中的 order by 才足以、如果 order by 子句满足上面所说的查询类型 https://learnku.com/articles/38925#389a5d B-Tree 索引的限制 哈希索引 只有精确匹配索引的所有的列查询才有效 对于每一行数据、存储引擎都会对所有的索引列计算一个哈希码、哈希码是一个较小的值。哈希索引将所有的哈希码存储在索引中、同时在哈希表中保存指向每个数据行的指针 。 只有 Memory 引擎显式支持哈希索引、这也是 Memory 引擎表,默认的索引类型、Memory 引擎同时也支持 B-Tree 索引。值得一提的是、memory 引擎是支持非唯一哈希索引的、如果多个列的哈希值相同、索引会以链表的方式存放多个记录指针到同一个哈希条目中 。 优点 缺点 InnoDB 引擎有一个特殊的功能叫做 “自适应哈希索引”。当 InnoDB 注意到某些索引值被使用得非常频繁时、它会在内存中基于 B-Tree 索引之上再创建一个哈希索引、这样就让B-Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。 选择合适的索引列顺序 是一种数据存储的方式。 InnoDB的聚集索引实际上在同一个结构中保存了 B-Tree 索引和数据行 聚集索引的数据行实际上是存放在索引的叶子页中。聚集 表示数据行和相邻的键值紧凑地存储在一起。一个表只能有一个聚集索引。 叶子页包含了行的全部数据、但是节点页只包含了索引的列 聚集索引的列就是主键列 优点 缺点 MyISAM 和 InnoDB 的索引对比 最好使用 Auto_increment 自增列、这样可以保证数据行是按顺序写入 UUID 主键插入行不仅花费时间更长,而且索引占用的空间也更大。这一方面由于主键字段更长、另一方面毫无疑问由于页分裂和碎片导致的。 当达到页的最大填充因子时、默认是页大小的 15/16 ,剩余的空间用于以后的修改。下一条记录就会写入新的页中。 按照这种方式、主键页就会近似地被填满。这就是期望的结果。(二级索引页可能是不一样的) 而使用 UUID 插入 因为新行的主键值不一定比之前插入的大、所以 InnoDB 无法简单地总是把新行插入到索引的最好、而是需要为新行找一个合适的位置–通常是已有数据的中间位置-并且分配空间。这会增加很多额外的工作、导致数据分布不够优化、缺点如下 把这些随机值载入到聚集索引以后、也会需要做 optimize table 来重建表并优化页的填充。 InnoDB 应该尽可能地按主键顺序插入数据、并且尽可能使用单调增加聚集键的值来插入新行 顺序主键什么时候回造成更坏的结果 如果索引的叶子节点中已经包含要查询的数据、那么就不需要进行回表。如果一个索引包含所有需要查询的字段的值、我们就称之为覆盖索引 InnoDB 存储引擎支持事务、行锁设计、支持外键、并支持类似于 Oracle 的非锁定读、即默认读取操作不会产生锁。 InnoDB存储引擎的表单独存放到一个独立的 ibd 文件中 InnoDB 通过使用多版本控制并发 MVCC 来获得高并发性、并且实现了 SQL 标准的 4 种隔离级别、默认为 RR 级别。同时使用一种称为 next-key locking 的策略避免幻读现象的产生。 InnoDB 存储引擎还提供了插入缓冲、二次写、自适应哈希索引、预读。 对于表中数据的存储、InnoDB 存储引起采用聚集的方式、每张表的存储都是按主键的顺序进行存放的、如果没有显式地在表定义的时候指定主键、InnoDB 存储引擎会在为每一个行生成一个6字节的 ROWID、并以此为主键。 MyISAM 引擎不支持事务、表锁设计、由MYI 和 MYD 组成、它的缓冲池只缓存索引文件、而不缓存数据文件 Memory 将表中的数据放在内存中、如果数据库发生重启或崩溃、表中的数据将会消失、非常适合存储临时数据的临时表 Memory 存储引擎默认使用哈希索引、而不是我们熟悉的 B+树索引。只支持表锁、并发性能差、并且不支持 BLOB 和TEXT 类型。最重要的是变长字段是按照定长字段方式存储的、因此会浪费内存。还有一点就是、Mysql 数据库使用 Memory 作为临时表来存放查询的中间结果、如果中间结果集大于 Memory 存储引擎的容量设置、又或者中间结果含有 TEXT 或 BLOB 类型的字段、就会使用 MyISAM 存储表作为临时表、但是MyISAM 只缓存索引数据、不缓存数据文件、因此查询性能不太好。 后台线程主要的作用是负责刷新内存池中的数据、保证缓冲池的内存缓存是最近的数据、此外将已修改的数据文件刷新到磁盘文件、同时保证在数据库异常的情况下 InnoDB 能恢复到正常运行的状态。 Master Thread 核心后台线程、主要负责将缓冲池的数据一步刷新到磁盘、保证数据的一致性、包括脏页的刷新、合并插入缓冲、UNDO 页的回收 IO Thread 在InnoDB 中大量使用了 AIO 来处理写IO请求、这样可以提高数据库的性能。wirte、read、inset buffer 、log IO Purge Thread 事务提交之后、其使用的 undolog 可能不再需要、因为需要 Purge Thread 来回收已经使用并分配的 undo 页。 page cleaner thread 刷新脏页 通常来说、数据库中的缓冲池是通过 LRU 算法进行管理的、即最频繁使用的页在 LRU 列表的前端、而最少使用的页在 LRU 列表的尾端。当缓冲池不能存放新读取的页时、先释放 LRU 列表中尾端的页 缓冲池的页的大小是 16KB、它将传统的 LRU 算法做了一些优化、在LRU 列表中加入了 midpoint 位置、新读取到的页虽然是最新访问的页、但是不能直接放入 LRU 列表的首部、而是放入 midpoint 。这个位置大概是 LRU 列表长度的 5/8 midpoint 之后的列表称为 old 列表、之前的称为 new 列表、可以认为 new 列表中的页都是最为活跃的数据热点数据 LRU 列表从 old 部分加入到 new 部分时、称此事发生的操作为 page made young、而因为 innodb_old_blocks_time 的设置而导致页没有从old 转到 new 部分到操作称为 page not made young 自适应哈希索引、Lock 信息、insert buffer 等页不需要LRU算法进行维护 LRU 用来管理已经读取的页、但当数据库刚启动时、LRU 列表是空的、没有任何页、这时页都放在 Free 列表中。当需要从缓冲池中分页时、首先从 Free 列表中查找释放有可用的空闲页、如果有则从Free 中删除、放入LRU 列表中。否则根据 LRU 算法淘汰 LRU 列表末尾的页。 在 LRU 列表中的页被修改后、称为脏页、也就是缓冲池中的页和磁盘的页数据产生了不一致。这时候 DB 会通过 checkpoint 机制将脏页刷新回磁盘、脏页即存在 LRU 列表、也存在 Flush 列表中 摘抄自小林大神 https://www.cnblogs.com/xiaolincoding/p/12442435.html Http 是什么 超文本传输协议、是在计算机世界里面专门在两点之间传输文字、图片、音效、视频等超文本数据等约定和规范 五大类 HTTP 状态码 http 常见的字段 get 和 post get 方法的含义是从服务器获取资源、这个资源可以是静态的文本、页面、图片视频等 post 方法则是相反操作、他向 URI 指定的资源提交数据、数据就在报文的 body 里 get 和 post 都是安全和幂等的吗 get 是安全且幂等的 post 是不安全也不是幂等的 优点 简单、header + body 灵活和易于扩展 http 协议里各类请求方法、URL/URI、状态码、头字段等每个组成要求都没有被固定死、允许自定义和扩充 同时 http 在 osi 第七层、则它的下层可以随意变化 https 就是在 http 和tcp 层之间增加了 ssl 和tls 安全传输层、http/3 设置把tcp层换成udp的quic 应用广泛和跨平台 缺点 无状态 好处是不需要额外资源记录状态信息、减轻服务端负担 坏处就是做一些关联系操作时会非常麻烦 cookie 明文传输 不安全 https 的方式解决、引入 SSL/TLS 层 持久连接 管道网络 管道机制运行同时发出A请求和B请求,但是服务器还是先回应A请求、完成后在回应B请求。要是前面回应特别慢、后面就会有许多请求排队等着、这称为队头阻塞 队头阻塞 Http/1.1 的性能一般后续的http/2 和 http/3 就是在优化http性能 区别 https 解决了什么问题 https 如何解决的 SSL/TLS 握手、四次通信 客户端向服务器发起加密通信请求 发送的信息:客户端支持的 SSL/TLS 协议版本、 客户端生产的随机数 用于产生会话密钥 客户端支持的加密套件 RSA 服务端收到请求后、响应如下内容 确认SSL/TLS 版本、如果不支持、则关闭加密通信 服务器生产随机数 用于产生会话密钥 确认密码套件 返回数字证书 浏览器收到回应后、确认证书的真实性、从证书中取出公钥、使用它加密报文 一个随机数 用于产生会话密钥 加密通信算法改变通知、表示随后的信息将用会话密钥加密通信 客户端握手结束通知、这一项同时把之前所有的内容的发生的数据做成一个摘要、供服务端检验 服务端收到第三个随机数之后、通过协商加密算、计算出本次通信的会话密钥、然后向客户端发送最后的信息 加密通信算法改变通知 服务器握手结束通知 请求报文 响应报文 http 是无状态的、让http 具备状态、解决方法有两种、一种是在客户端中保持状态、一种是在服务端保持状态 服务端发送客户端特殊的信息、以文本的方式存放在客户端、存放在请求头、响应头。客户端再次请求相同的服务器时、将cookie 回发、服务器收到之后惠根据cookie 生成与客户端一致的内容 在服务器中保存用户的信息、检查是否包含对应的状态信息 那这样子也是需要客户端那边支持、提供一个id给服务器去判断客户端是谁 两者比较 UDP TCP 发送的滑动窗口 接收的滑动窗口 A 向 B 发出连接请求报文段、这时首部中的 SYN=1 seq=x、SYN=1 报文不能携带数据、但是要消耗一个序号、这时客户端进入一个同步已发送的状态 B 收到了连接请求的报文段后、如果同意建立连接、则向A发送确认、在确认报文中 SYN=1 ACK=1 seq=y ack=x+1 、这个也是不能携带数据、但是需要消耗一个序号、这时B 进入到一个同步收到的状态 A 收到B的确认之后、还要给B确认、ACK=1 seq=x+1 ack = y+1 这个报文可以携带数据、如果不携带则不消耗序号、下一次数据报文段序号依旧可以使用 seq=x+1 此时tcp 连接已经确认了、A进入连接确认状态 https://blog.csdn.net/qzcsu/article/details/72861891 一句话,主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。 如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。 如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。 客户端主动关闭、服务端被动关闭 MSL maximum segment lifetime 最大的报文寿命、TCP 允许不同的实现可以设置不同的MSL 值元数据的更新
重要的参数
消费者
消息消费
位移的提交
指定位移消费
再均衡
消费者拦截器
多线程实现
配置参数
主题和分区
配置
分区管理
日志清理
日志删除
磁盘存储
页缓存
零拷贝
控制器
可靠性
Spring Boot /Cloud
Spring Cloud Eureka
@EnableEurekaServer
eureka.clent.register-with-eureka
: 由于该应用为注册中心,所以设置为 false, 代表不向注册中心注册自己。eureka.client.fetch-registry
: 由于注册中心的职责就是维护服务实例,它并不需要去检索服务, 所以也设置为 false@EnableDiscoveryClient
eureka.clent.register-with-eureka
: 不需要改变、默认为true 就好eureka.client.fetch-registry
: 不需要改变、默认为true 就好eureka.instance.hostname = peerl
eureka.client.serviceUrl.defaultZone = http://peer2:1112/eureka/
客户端负载均衡 Spring Cloud Ribbon
服务容错保护 Hystrix
Spring Cloud Feign
Mysql
概述
逻辑架构
查询解析、分析、优化、缓存
并发控制
事务
死锁
事务日志
Mysql 中的锁
多版本并发控制
InnoDB 引擎
MyISAM 引擎
Memory 引擎
选择不同的存储引擎
Schema 与数据类型优化
varchar 和 char
datetime & timestamp
索引
索引的类型
索引的优点
高性能索引策略
多列索引
聚集索引
InnoDB 表中按主键顺序插入行
覆盖索引
体系结构
后台线程
内存
LRU List Free List Flush List
Http
具体含义
常见状态码
1xx
提示信息、表示目前协议处理等中间状态、还需要后续的操作
2xx
成功、报文已经收到并被正确处理
200、204、206
3xx
重定向、资源位置发生变动、需要客户端重新发送请求
301、302、304
4xx
客户端错误、请求报文有误、服务器无法处理
400、403、404
5xx
服务器错误、服务器在处理请求时内部发生了错误
500、501、502
1xx 属于提示信息、协议处理中的一种中间状态、实际用到的比较少
2xx 表示服务成功处理了客户端的请求、
200 OK 表示一切正常、如果非 HEAD 请求、服务器返回的响应都会有 body 数据
204 no content 常见成功状态码、与200 基本相同、只是没有了 body 数据
206 partial content 时用于 HTTP 分块下载或断点续传、表示返回的 body 数据并不是资源的全部、而是其中的一部分
3xx
301 表示永久重定向、说明资源已经不在了、需要用新的 URL访问
302 表示临时重定向、说明资源还在、但暂时需要用另一个URL访问
301 302 都会在响应头里面使用 Location 字段、指明后续需要跳转的 URL、浏览器回自动重定向到新的 URL
304 not modified 不含有跳转到含义、表示资源未修改、重定向已存在的缓冲文件、页陈伟缓存重定向、用于缓存控制
4xx
400 bad request 请求报文有错误、笼统的错误
403 forbidden 服务器禁止访问资源、并不是客户端请求出错
404 not found 表示请求的资源在服务区上不存在或未找到、所以无法提供给客户端
5xx 服务器内部发生了错误
500 与 400 一样、笼统的错误
501 not implement 请求的功能还没有实现
502 bad gateway 作为网关或者代理返回的错误码
503 service unavailable 表示服务器当前很忙、暂时无法响应服务器
Http 特性
Http/1.1 性能如何
http 和 https
Http 有哪些方法
get post 的区别
put 和 post 区别
http 报文
cookie 和 session的区别
cookie 介绍
session机制
计算机网络
传输层
滑动窗口
TCP 连接的建立
为啥需要需要三次握手
TCP连接的释放
为啥客户端最后需要等待 2MSL