什么缓存一致问题

在谈缓存一致性协议之前我们先了解一下缓存一致性问题是什么,它是怎么出现的。

现在处理器处理能力上要远胜于主内存(DRAM),主内存执行一次内存读写操作,所需的时间可能足够处理器执行上百条的指令,为了弥补处理器与主内存处理能力之间的鸿沟,引入了高速缓(Cache),来保存一些CPU从内存读取的数据,下次用到该数据直接从缓存中获取即可,以加快读取速度,随着多核时代的到来,每块CPU都有多个内核,每个内核都有自己的缓存,这样就会出现同一个数据的副本就会存在于多个缓存中,在读写的时候就会出现数据 不一致的情况。

CPU Cache 和 Cache Line

CPU Cache

缓存名称 是还共享 描述
一级缓存(L1 Cache) CPU CORE独享 制造成本很高因此它的容量有限,但是读取速度很快
二级缓存(L2 Cache) CPU CORE独享 是一级缓存的缓冲器,存储那些CPU处理时需要用到、一级缓存又无法存储的数据,读取速度低于一级缓存
三级缓存(L3 Cache) 多个CPU CORE共享的 可以看作是二级缓存的缓冲器,读写速度低于二级缓存,CPU主要通过三级缓存与总线通信

Cache Line

数据在缓存中不是以独立的项来存储的,它不是一个单独的变量,也不是一个单独的指针,它在数据缓存中以缓存行存在的,也称缓存行为缓存条目。目前主流的CPU Cache的Cache Line大小通常是64字节,并且它有效地引用主内存中的一块地址。一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量

如果需要详细了解缓存行可以参考另一篇文章 聊聊CacheLine

计算机世界的局部性

局部性原理:在CPU访问存储设备时,无论是存取数据或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理。

  • 时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。比如程序中的循环、递归对数据的循环访问,主要体现在指令读取的局部性
  • 空间局部性(Spatial Locality):如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。比如程序中的数据组的读取或者对象的连续创建,对内存都是顺序的读写,主要体现在对程序数据引用的局部性

保证缓存一致性的一点儿思考

根据缓存一致性问题的描述,如果可以做到在读取的时候读到最新的数据 ,CPU对同一个共享数据的写入操作只在一个核上运行,并且将更改后的内容及时写回主内存即可。

由于现在超线程技术,一个核可能出现多个线程,平时听到的4核8线程,6核12线程,它是通过在物理CPU核上采用特殊的硬件指令来模拟两个内核运行,旨在利用充分利用CPU闲置资源,所以上面说对同一共享数据的写入只有在一个核上运行并不是很准确,应该是在保证数据只在一个缓存中被修改,并同步回主内存

MESI是什么

MESI是众多缓存一致性协议中的一种,也在Intel系列中广泛使用的缓存一致性协议
缓存行(Cache line)的状态有Modified、Exclusive、 ShareInvalid,而MESI 命名正是以这4中状态的首字母来命名的。该协议要求在每个缓存行上维护两个状态位,使得每个数据单位可能处于M、E、S和I这四种状态之一,各种状态含义如下:

状态 含义 描述
M 修改 表示缓存行数据被修改了,并且没有更新至主内存。处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没有。简单的可理解为缓存行数据独占被修改且未同步
E 独享(互斥) 表示缓存行数据是独占的。处于这一状态的数据,只有在本CPU中有缓存,其它CPU中没有缓存该数据,且其数据没有修改与主内存中一致。简单的可理解为缓存行数据独占且未被修改
S 共享 表示缓存行数据是共享的。处于这一状态的数据在多个CPU中都有缓存,且与内存一致
I 无效 表示缓存行数据是无效的。本CPU中的这份缓存已经无效。

上面对于S状态的描述,我们可能会想到另一种状态,数据被共享但是与内存中不一致的情况,这就是我们MESI协议需要解决的问题

MESI是如果保证缓存一致性

MESI协议对不同的状态增了不同的监听任务,监听任务的规则如下

  • 一个处于M状态的缓存行,必须时刻监听所有试图读取该缓存行对应的主存地址的操作,如果监听到,则必须在此操作执行前把其缓存行中的数据写回主内存
  • 一个处于S状态的缓存行,必须时刻监听使该缓存行无效或者独享该缓存行的请求,如果监听到,则必须把其缓存行状态设置为I
  • 一个处于E状态的缓存行,必须时刻监听其他试图读取该缓存行对应的主存地址的操作,如果监听到,则必须把其缓存行状态设置为S。

MESI消息

消息名 消息类型 描述
Read 请求 通知其它处理器,主内存当前处理器准备读取某个数据。该消息包含待读取数据的内存地址
Read Response 响应 该消息包含被请求的读取消息的数据。可能是主内存提供的,也可能是嗅探到Read消息的其它处Cache提供的,主要看嗅探到Read消息的Cache中缓存行的状态
Invalidate 请求 通知其它处理器将其高速缓存中指定内存地址对应的缓存行状态置为I(无效) ,也就是通知其它处理器将指定内存地址的副本数据删除
Invalidate Acknowledge 响应 接收到Invalidate消息的处理器必须回复此响应,以表示删除了其高速缓存上的相应副本数据(这里的删除是逻辑删除,其实只是更新了缓存条件的Flag值)
Read Invalidate 请求 从名字可以推断出该消息是一个复合消息,是由Read消息和Invalidate消息组合而成。它的作用是通知其它处理,发送该消息的处理器准备更新一个数据,请求其它处理器删除其高速缓存中相应的副本数据。接收到该消息的处理器必须回复两个响应消息,Read Response、Invalidate Acknowledge消息,发送该消息的处理器期望收到一个Read Response以及多个Invalidate Acknowledge。
Writeback 请求 该消息包含需要写入主内存的数据及其对应的内存地址

MESI协议的处理流程

在这里我们只讨论多核情况下的数据读取X,我们以两核为例,CPUA 拥有 L1A高速缓存,CPUB拥有L1B高速缓存

MESI协议在数据的读定时,是通过往总线中发送消息请求响应来保证数据的一致性的,下面我们看一下数据的读取流程

数据读取流程

场景

CPUA需要读取数据X

处理流程

CPUA需要读取数据X,会根据数据的地址在自己的缓存L1A中找到对应的缓存行,然后判断缓存行的状态

  • 如果缓存行的状态是M、E、S,说明该缓存行的数据对于当前读请求是可用的,直接从缓存行中获取地址A对应的数据

  • 如果缓存行的状态是I,则说明该缓存行的数据是无效的,则CPUA会向总线发送Read消息,说’我现在需要地址A的数据,谁可以提供?‘,其它处理器(CPUB)会监听总线上的消息,收到消息后,会从消息中解析出需要读取的地址,然后在自己缓存(L1B)中查找缓存行,这时候根据找到缓存行的状态会有以下几种情况

    • 状态为S/E , CPUB会构造Read Response消息,将相应缓存行中的数据放到消息中,发送到总线同时更新自己缓存行的状态为S,CPUA收到响应消息后,会将消息中的数据存入相应的缓存行中,同时更新缓存行的状态为S

    • 状态为M,会先将自己缓存行中的数据写入主内存,并响应Read Response消息同时将L1B中的相应的缓存行状态更新为S

    • 状态为I或者在自己的缓存中不存在地址A的数据,那么主内存会构造Read Response消息,从主内存读取包含指定地址的块号数据放入消息(缓存行大小和内存块大小一致所以可以存放的下),并将消息发送到总线

      CPUA获接收到总线消息之后,解析出数据保存在自己的缓存中

写流程

场景

CPUA需要对地址A的X数据进行写操作

处理流程

任何一个处理器执行内存操作时,必须拥有相应数据的所有权。

CPUA会先根据内存地址在自己的缓存中L1A中找相应的缓存行,判断缓存行的不同状态,可能会了现下列几种情况

  1. E/M时,说明当前CPUA已经拥有了相应数据的所有权,此时CPUA会直接将数据写入缓存行中,并更新缓存行状态为M,此时不需要向总线发送任何消息。
  2. S时,说明数据被共享,其它CPU中有可能存有该数据的副本,则CPUA向总线发送Invalidate 消息以获取数据的所有权,其它处理器(CPUB)收到Invalidate消息后,会将其高速缓存中相应的缓存行状态更新为I,表示已经逻辑删除相应的副本数据,并回复Invalidate Acknowledge消息,CPUA收到所有处理器的响应消息后,会将数据更新到相应的缓存行之中,同时修改缓存行状态为E,此时拥有数据的所有权,会对缓存行数据进行更新,最终该缓存行状态为M
  3. I时,说明当前处理器中不包含该数据的有效副本,则CPUA向总线发送Read Invalidate消息` ,表明”我要读数据X,希望主存告诉我X的值,同时请示其它处理器将自己缓存中包含该数据的缓存行并且状态不是I的缓存行置为无效
  • 其它处理器(CPUB)收到Invalidate 消息后,如果缓存行不为I的话,会将其高速缓存中相应的缓存行状态更新为I,表示已经逻辑删除相应的副本数据,并回复``Invalidate Acknowledge消息`
  • 主内存收到Read消息后,会响应Read Response消息将需要读取的数据告诉CPUA
  • CPUA收到所有处理器的Invalidate Acknowledge消息和主内存的Read Response消息后,会将数据更新到相应的缓存行之中,同时修改缓存行状态为E,此时拥有数据的所有权,会对缓存行数据进行更新,最终该缓存行状态为M

MESI会有哪些问题

​ 从上面处理过程中,其实不难发现,MESI主要是靠在总线上传递消息,并对消息增加不同的监听,来保证一个线程对共享变量的更新,对其它处理器上运行的线程是可见。但是消息传递是要时间的,一个请求,多个响应,每次都会涉及到CPU的切换,对于CPU这么频繁的读取,消息传递产生的时间是一种致命的影响,会导致引起缓存一致性流量风暴,导致各种各样的性能问题和稳定性问题。

MESI总结

CPU除了在做内存数据传输的时候和总线交互 ,而且还会通过不停在嗅探总线上发生的数据交换,跟踪其他缓存在做什么。当一个缓存代表它所属的处理器去读写内存时,其它处理器都会得到通知,以此来使自己的缓存保持同步。只要某个处理器一写内存,其它处理器马上知道这块内存在它们的缓存段中已失效。

思考

  • 通过上面的描述看来,在多个线程共享变量的情况下,MESI协议已经能够保障一个线程对共享变量的更新对其它处理器上运行的线程来说是可见的;既然如此,JAVA中的可见问题为何会存在?

  • MESI频繁的消息请求与响应带来的性能问题如何解决?

在java中字符串是我们比较常用的一个类型,字符串是不可变的,类被声明为final , 存储字符的char[] value数据也被声明为final ,我们对String真的了解么?我们看一下String是有多么的疯狂。本文中是在JDK8下面测试,不同的JDK可能会有不一样的结果。

测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private static String B = "B";
private static String K = "K";
private static final String B1 = "B";
private static final String K1 = "K";
private static void demo1() {
String s1 = "BK";
String s2 = "BK";
String emp = "";
String s3 = "B" + "K";
String s4 = "B" + emp + "K";
String s5 = "B" + new String("K");
String s6 = new String("BK");
String s7 = s6.intern();
String s8 = "B";
String s9 = "K";
String s10 = s8 + s9;
String s11 = B + K;
String s12 = B1 + K1;
System.out.println("1 : s1 == s2 : " + (s1 == s2));
System.out.println("2 : s1 == s3 : " + (s1 == s3));
System.out.println("3 : s1 == s4 : " + (s1 == s4));
System.out.println("4 : s1.equals(s4): " + s1.equals(s4));
System.out.println("5 : s1 == s5 : " + (s1 == s5));
System.out.println("6 : s1 == s10 : " + (s1 == s10));
System.out.println("7 : s5 == s6 : " + (s5 == s6));
System.out.println("8 : s1 == s7 : " + (s1 == s7));
System.out.println("9 : s1 == s11 : " + (s1 == s11));
System.out.println("10: s1 == s12 : " + (s1 == s12));
}
public static void main(String[] args) {
demo1();
}

看到这里可以停下来想一下每一个输出的结果是什么?

收藏一下本文,回家在电脑上亲自试一下结果,结果可能出乎你的意料。

输出结果

1
2
3
4
5
6
7
8
9
10
1 : s1 == s2 : true
2 : s1 == s3 : true
3 : s1 == s4 : false
4 : s1.equals(s4): true
5 : s1 == s5 : false
6 : s1 == s10 : false
7 : s5 == s6 : false
8 : s1 == s7 : true
9 : s1 == s11 : false
10: s1 == s12 : true

看到结果可能中有些会和我们想象中的不一样,出乎你的意料,到现在头脑已经有些疯狂了,静下心来仔细想一下

为什么是这样的结果

  • 常量池中一般存放.class文件中的常量,主要包含字面量(如文本字符串、声明为final的常量值等)和
    符号引用量(类和接口的全限定名、字段名称和描述符、方法名称和描述符)这些信息会存储在常量池中,这个常量池被称为静态常量池
  • 在类完成装载操作之后,在运行阶段也可以将新的常量放到池中,比如String的intern()方法就是这样的,这时候操作的常量池被称为动态常量池
  • 结果1. s1 == s2 : true
    对于这条输出应该不会有问题,”BK”是一个字符串常量,在编译阶段就会存放到静态常量池中比如存放地址为0x01,所以两个变量都指向常量池的同一个对象,比较它们的地址相等,结果是true
  • 结果2 : s1 == s3 : true
    s1的指向常量池中”BK”的内存地址0x01
    s3因为是两个常量相加,编译器会将其优化为s3="BK"是终指向的也地址0x01
    所以两个对象的地址也是相同的,结果为true
  • 结果3 : s1 == s4 : false
    s4因为连接的字符中存在一个变量emp引用类型所以不编译器不会对其进行优化,产生的对象不会被加入到字符串池中,而是在运行时在堆上创建一个新的对象s4值为”BK”,并将s4指向堆上对象的引用地址 0x02
    这时s1 的地址为0x01 s4的地址为0x02两个变量指向了不同的地址,所以返回结果是false
  • 结果4 : s1.equals(s4): true
    因为使用的是equals方法比较,所以首先比较两个对象地址是还相同,如果不相同,再去比较两个地址里面的内容是还相等,很显然,两个对象引用的地址不同,内容相同所以结果是true
  • 结果5 : s1 == s5 : false
    String s5 = "B" + new String("K");
    B是常量会在常量池,new操作这部分不是已知字面量,只能运行时才能确定结果,在编译器不优化的情况下,运行时会在堆上创建一个对象值为”BK”的对象, 同时让s5指各它的地址0x03
    s1的地址是0x01,所以比较两个对象的地址不是同一个结果 为false
  •  结果6 : s1 == s10 : false
    1
    2
    3
    >  String s8 = "B";
    > String s9 = "K";
    > String s10 = s8 + s9;
    在编译时s8,s9的字面量是确定的,所以在常量池中会有BKs8,s9 分别指向常量池的两个地址
    s10赋值时,使用的是s8,s9两个变量,变量初始化时候是指向常量池,但是在运行时候指向什么地址,鬼才知道,所以在编译期是不可预料的,编译器是不做优化的,只有在运行时才会在堆中拼接B和K生成新对象在堆中,并将引用赋给s10,比如这时候分配的地址是0x04,这时候对比s1的地址0x01s10的地址0x04, 返回结果一定是false
  • 结果 7 : s5 == s6 : false
    s5和s6的赋值时,因为存在new对象,所以在编译其无法确定其字面量,只能在运行时才会确定,所以s5和s6都是堆上的两个对象,在比较两个对象的地址,一定是不相等的,所以结果一定是false
  • 结果8 : s1 == s7 : true
    String s7 = s6.intern();
    在运行到该行代码时,s6的值是确定的,然后调用intern方法,发现常量池中已经存在BK,所以s7指向常量池中的地址,在比较s1s7的值时,返回结果为 true
  • 结果9 : s1 == s11 : false
    String s11 = B + K;
    BK是静态变量,在编译期是无法确定字面量,所以只能在运行时才能确定其真实值,所以s11指向的是堆上的一个地址,在比较s1s11时候,返回的结果为false
  • 结果10: s1 == s12 : true
    String s12 = B1 + K1;
    因为B1K1static final修饰
    对于static final类型,在类加载的准备阶段就会被赋上正确的值,因为static final类型被认为是常量,两个常量相加之后的值也是常量,字面量是确定的,这时候BK在常量池中已经存在,所以s12也是指向常量池中的地址,在比较s1s12的地址返回的结果是true

总结

按照下面的规则来判断,不会被String搞迷路

  • 变量在定义时如果存在new String()非static final修饰的变量进行+运算,都只能在运行时才能确定结果,所产生的对象一定是在堆上面
  • 如果一定变量在定义时字面量已经确定,会在常量池中创建,并且变量指向常量池中的地址
  • 在编译期可以确定的常量才会被放入常量池,在运行时的变量,如果不调用intern方法是不会把常量添加到常量池中的
  • statci final 修饰的变量在准备阶段已经确定正确的值,会被认为是常量,存放在常量池中

再来一发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 比如我们玩游戏时候经常用的QWER四个键,可以组合出不同的操作
*/
private static void demo2() throws NoSuchFieldException, IllegalAccessException {
//定义操作A QWER
String operateA = "QWER";
//获取字符串对象中存储字符的value字段 private final char value[];
Field valueFieldString = String.class.getDeclaredField("value");
valueFieldString.setAccessible(true);
//获取value数组中的值 [Q,W,E,R]
char[] value = (char[]) valueFieldString.get(operateA);
//将value数组的值改为 [Q,Q,Q,Q]
value[1] = 'Q';
value[2] = 'Q';
value[3] = 'Q';
//定义操作B和操作A一样 QWER
String operateB = "QWER";
System.out.println("1.operateA :" + operateA);
System.out.println("2.operateB :" + operateB);
System.out.println("3.operateA == operateB :" + (operateA == operateB));
System.out.println("4.\"QWER\" == operateB :" + ("QWER" == operateB));
System.out.println("5.\"QQQQ\" == operateA : " + ("QQQQ" == operateA));
System.out.println("6.operateA.equals(\"QQQQ\") : " + operateA.equals("QQQQ"));
System.out.println("7.operateA.equals(\"QWER\") : " + operateA.equals("QWER"));
System.out.println("8.\"QWER\".equals(\"QQQQ\") : " + "QWER".equals("QQQQ"));
}

输出结果

1
2
3
4
5
6
7
8
1.operateA :QQQQ
2.operateB :QQQQ
3.operateA == operateB :true
4."QWER" == operateB :true
5."QQQQ" == operateA : false
6.operateA.equals("QQQQ") : true
7.operateA.equals("QWER") : true
8."QWER".equals("QQQQ") : true

为什么会输出这样的结果

图片.png

没错,这结果简直让人抓狂,太离谱了,

1
2
3
6.skillA.equals("QQQQ") : true
7.skillA.equals("QWER") : true
8."QWER".equals("QQQQ") : true

凭直觉大多数人会认为6 和 7 应该是一个对一个错,8应该是false,可这结果结果倒底怎么了,刚看到这结果感觉很惊讶what a fuck !

代码逻辑

  1. 首先我们先定义一个操作A QWER,
  2. 对A底层的字符数组进行修改,修改为QQQQ(直接对底层数据修改,直接改的地址里面存放的内容,而不是通过String运算符修改)
  3. 再定义一个操作B,同样为QWER
  4. 然后进行各种比较,判断输出内容

分析

编译阶段搞的事情

1、由于QWER在编译阶段是一个字面量,所以QWER在常量池中分配空间0x01,并存储

2、operateA指向常量池中QWER所在的地址0x01

3、operateB的字面量也是QWER,这时候常量池中也存在,引用直接指向地址0x01

最终的结果是operateAoperateB指向了同一个地址0x01 ,字面量为QWER的地址是0x01

字面量为QQQQ的变量指向了0x05的地址

运行阶段搞的事情

  1. 读取operateA的值,然后通过反射获取到字符存储数据的char[]数组value

  2. 将value里面的内容个性为QQQQ

    String类equals方法的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public boolean equals(Object anObject) {
    if (this == anObject) {
    return true;
    }
    if (anObject instanceof String) {
    String anotherString = (String)anObject;
    int n = value.length;
    if (n == anotherString.value.length) {
    char v1[] = value;
    char v2[] = anotherString.value;
    int i = 0;
    while (n-- != 0) {
    if (v1[i] != v2[i])
    return false;
    i++;
    }
    return true;
    }
    }
    return false;
    }

结果分析

接下来就是进行各种比较了,在看结果之间先看一下String equals方法的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}

先判断对象的地址是不是同一个,如果指向同一个地址,那么就认为两个对象相等

如果指向的地址不相等,然后判断长度是还相等,如果长度不相等,则返回false

如果地址不等,长度相等的话,就取出地址中的值,逐位进行比较,如果有一位不相等则返回false ,否则返回 true

接下来我们逐个看一下结果

  • 1.operateA :QQQQ
    ​ 在运行到该行代码时候,地址中的值已经被修改了,所以operateA的值为QQQQ

  • 2.operateB :QQQQ
    operateB和operateA指向了同一个引用,在运行到该行代码时候,地址中的值已经是QQQQ了 ,所以operateB的值为QQQQ

  • 3.operateA == operateB :true
    因为operateA和operateB的指向的地址都是0x01所以比较两个对象的地址值是true

  • 4.”QWER” == operateB :true
    “QWER”这个匿名变量的字面量是个常量,并且在常量池中已经存在,所以指向常量池的0x01地址,operateB的地址也是0x01所以比较两个对象的地址值是true

  • 5.”QQQQ” == operateA : false
    “QQQQ”这个匿名变量的字面量是个常量,在常量池中不存在,所以会被加入到常量池中地址为 0x05,operateA的地址也是0x01所以比较两个对象的地址值是false

  • 6.operateA.equals(“QQQQ”) : true
    operateA指向的内存地址是0x01,但是值是QQQQ
    “QQQQ”指向的内存地址是0x05,值为QQQQ

    在对比equals方法时,先比较两个对象的地址值是false,然后再去比较两个地址中的值是否相等,因为0x01地址中的值是QQQQ与0x05地址的值QQQQ的值相等,所以结果是 true

  • 7.operateA.equals(“QWER”) : true
    “QWER” 指向的内存地址是0x01,值是QQQQ
    operateA指向的内存地址是0x01,值是QQQQ
    在对比equals方法时,先比较两个对象的地址值是false,然后再去比较两个地址中的值是否相等,因为0x01地址中的值已经是QQQQ与0x05的值相等,所以结果是 true

  • 8.”QWER”.equals(“QQQQ”) : true
    “QWER”指向的内存地址是0x01,值是QQQQ
    “QQQQ”指向的内存地址是0x05,值为QQQQ
    在对比equals方法时,先比较两个对象的地址值是false,然后再去比较两个地址中的值是否相等,因为0x01地址中的值是QQQQ与0x05地址的值QQQQ的值相等,所以结果是 true

总结

其实这个示例中,主要是直接操作了底层的数组,破坏了字符串的不变性,才会出现这么奇怪的现象。

此文结束

喜欢可以关注个人公众号albk

关注公众号albk ,回复LS+对应序号获取相关学习资料,资料会定期更新,如果有需要的资料可以在公众号留言,尽量帮大家找需要的学习资源,取关后无法再次获取资源

视频来源于网络资料整理,如有侵权请联系作者删除

扫码关注

1.大数据的统计学基础

2.大数据的矩阵计算基础课程

3.基于案例学数据挖掘

4.大数据技术-SQL优化

5.量化投资基础计算与模型

6.金融时间序列分析

7.推荐系统

8.python 数据分析

9.数据库引擎开发

10.深入学习BI ETL ELT Kettle视频教程 数据仓库 数据处理 数据质量

11.机器学习-吴恩达

12.oracle五部曲

13.新版Andy老师商业智能数据仓库BI-ETL培训视频全集

14.炼数成金openstack

15.Oracle EBS R12制造 财务 DBA模块(整理)

16.MySQL数据库运维全套视频教程 阿里巴巴DBA讲授

17.Python数据分析案例实战

18.R语言 数据分析全套

19.机器读心术之文本挖掘与自然语言处理

20.机器学习及其matlab实现

21.深度学习框架Tensorflow学习与应用

22.深入JVM内核—原理、诊断与优化

23.实战Java高并发程序设计

24.python网络程序开发

25.Hadoop应用开发实战案例教程

26.机器学习

27.spark大数据平台新

28.Spark大数据分析平台

29.Spark Mllib机器学习算法源码及实战详解

30.大数据hadoop 26期

31.大数据的Java基础 14课

32.架构师必备大规模高性能分布式存储系统设计与实现

33.Kafka原理剖析及实战演练

34.Python自然语言分析

35.mahout机器学习平台

36.Zookeeper分布式系统开发实战

37.从Docker到Kubernetes之技术实战

38.Storm大数据开发视频教程

39.linux内核探秘(第二期)

40.Hadoop数据分析平台(第四版)

41.深度学习框架Caffe学习与应用

42.python魔鬼训练营

43.算法导论视频教程附讲义完整版15课

44.RapidMiner大数据挖掘平台原理与应用视频教程

45.MySQL数据库查询优化

46.基于案例学Java服务器端程序设计

47.Scala语言入门

48.数据分析与SPSS(完整)共12周

49.大型电商分布式系统实践

50.大型分布式系统案例实战

51.大型网站高并发架构与自动化运维实战

52.高可用架构设计与实践

53.实战Docker到Kubernetes技术系列视频教程

54.Mycat从入门到精通

55.NoSQL与NewSQL数据库引航

56.深度玩转Excel

57.数据分析与SAS

58.DB2数据库性能优化

59.goldengate 从入门到精通

60.Hadoop视频[共44集适合入门]

61.Oracle数据库职业直通车

62.大数据的Linux基础

63.数据库设计

64.数据库系统实现技术内幕

65.hive数据仓库实践

66.Mysql从小白到大神

67.抽样调查

68.大数据算法导论

73.炼数 区块链技术从入门到精通

74.快速上手Jmeter

75.小马哥 Java 微服务实践 - Spring Cloud 系列

如何调用一个对象的方法?我们可以通过实例化对象直接调用、使用反射机制、通过代理对象等,本文介绍一种新的方法MethodHandle,这种方法在开发中很少会用到,但使用起来感觉很顺手。

什么是MethodHandle?

MethodHandle是JDK1.7实现了JSR-292,新加入的java.lang.invoke包中的一个重要组成部分,这个包的主要是在单纯通过符号引用来确定调用的目标方法以外,提供一种析的动态确定目标方法的机制。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.bk.exercise;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
/**
* @author BK
* @description:
* @date 2019-08-19 22:34
*/
public class MethodHandlerTest {
static class Person{
public void sing(String songName){
System.out.println("I'm sing " + songName);
}
}
/**
* 实现调用Person类的sing方法
* @param args
*/
public static void main(String[] args) throws Throwable {
Person zhangsan = new Person();
// void.class是方法返回的类型 String.class 是方法的入参类型
MethodType mt = MethodType.methodType(void.class,String.class);
MethodHandle handler = MethodHandles.lookup()
//找到zhangsan对象中签名和mt指定的签名是一致的sing方法
.findVirtual(zhangsan.getClass(), "sing", mt)
//非静态方法的第一个参数隐藏的this指针,这步相当设置第一个参数this指针
.bindTo(zhangsan);
handler.invokeExact("Summer train ");
}
}

运行结果如下显示

1
I'm sing Summer train 

从示例可以看出来,使用MethodHandle并没有什么困难,可以实现方法的调用,可是很多人会问,反射不也可以做相同的事情么,我们经常会使用反射,而不会使用这种方法。

MethodHandle与Reflection的区别

如果站在Java语言的角度来看,他们确实是有很多相似之处,但是从他们其它角度来看,确有很大的区别

  1. 从本质上来讲,ReflectionMethodHandle机构都是在模拟方法调用,但Reflection是模拟的Java代码层面的方法调用,而MethodHandle是在模拟字节码层面的方法调用。
  2. Reflection中的Method是重量级的,MethodHandle对象是轻量级的,Reflection中包含了方法的签名、描述符以及方法属性表中各种属性、执行权限等,而MethodHandle仅仅包含与执行该方法相关的信息
  3. MethodHandle是字符码的方法指令调用模拟,理论上可以模拟出和JVM上一样的优化如方法内联,但是Reflection去调用方法则不可以
  4. Reflection API的设计目标是为Java语言服务的,而MethodHandle设计成可服务于所有Java虚拟机之上的语言

写在最后

MethodHandles.lookup中的findStatic()findVirtual()findSpecia()三个谅正是为了对应invokestaticinvokevirtual&invokeinterface、invokespecial这几条字节码指令的执行权限校验行为,本文中只讲到了findVirtual()方法,其它两个方法读者可以自行尝试一下。

java.lang.invoke包中的内容可以看一看,别有一番有洞天

java具备面向对象的3个基本特征,继承、封装、多态。重载、重写是多态特征的一些最基本的表现,本文我们简单聊一下重载和重写

重载

示例一

先看一下重载的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.bk.exercise.base;

/**
* @author BK
* @description:
* @date 2019-08-19 00:52
*/
public class OverLoadTest {
static abstract class Fruit{
}
static class Apple extends Fruit{
}
static class Banana extends Fruit{
}
public void eat (Fruit fruit){
System.out.println("eat fruit .");
}
public void eat (Apple fruit){
System.out.println("eat apple .");
}
public void eat (Banana fruit){
System.out.println("eat banana .");
}

public static void main(String[] args) {
Fruit apple = new Apple();
Fruit banana = new Banana();
OverLoadTest test = new OverLoadTest();
test.eat(apple);
test.eat(banana);
}
}

大家可以先停下来,想一想运行之后的结果是什么?

在OverLoadTest test固定的前提下, test会选择哪个重载版本呢,他的选择依据是什么?

解析

在上述代码中Fruit apple = new Apple(); Fruit是静态类型,而Apple是动态类型

test会选择哪个,取决于入参的数量和数据类型,虽然代码中定义了两种静态类型动态类型不同的变量,但编译器在重载时是通过参数的静态类型是而不是动态类型作为判断依据的,并且静态类型在编译期就是可以知道的,因此在编译阶段Javac就已经根据参数决定使用哪个重载版本了,这种根据静态类型判断方法的执行版本叫做静态分配重载就是静态分配的典型应用。

运行结果

1
2
eat fruit .
eat fruit .

其它情况

大多数情况下,重载版本并不是唯一 的,往往很难确定一个“合适的版本”,静态类型只能通过语言上的规则去理解和推断

重写

示例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.bk.exercise.base;

/**
* @author BK
* @description:
* @date 2019-08-19 01:16
*/
public class OverRide {
static abstract class Fruit{
public abstract void eat ();
}
static class Apple extends Fruit {
public void eat() {
System.out.println("eat apple .");
}
}
static class Banana extends Fruit {
public void eat() {
System.out.println("eat banana .");
}
}
public static void main(String[] args) {
Fruit apple = new Apple();
Fruit banana = new Banana();
apple.eat();
banana.eat();
}
}

运行之后的结果如下

1
2
eat apple .
eat banana .

这个结果不会出乎意料,对于习惯了面向对象思维的Java程序员觉得这样的结果是理所当然的,但是JVM是如何知道要调用哪个方法的?这个就和JVM动态分配有关

invokevirtual指令的多态查询过程

  1. 找到操作数栈顶的第一个元素所指向的对象的实际类型A
  2. 如果在类型A中找到与常量中的描述符和简单名称都相符的方法,并且权限访问验证通过,则调用这个方法的直接引用,如果不通过,则抛出IllegalAccessError异常
  3. 如果没有找到与常量中的描述符和简单名称都相符的方法,则按继承关系从下往上依次对A的各个父类进行第2步的搜索和验证过程
  4. 如果不有找到合适的方法,则抛出java.lang.AbstractMethodError异常

由于invokevirtual指令执行的第一步就是在运行期间确定接收者的实际类型,所以两调用中invokevirtual指令把常量池中的类型方法符号引用解析到了不同的直接引用上,这个过程就是Java语言方法重载的本质。我们把这种在运行期根据实际类型确定方法版本的分派过程称为动态分派

JVM动态分配的实现

JVM的动态分派实现是在类在的方法区中建立一个虚方法表(vtable),vtable中存放着各个方法的实际入口

如果某方法在子类中没有被重写,那子类的vtable和父类vtable里面的地址入口都是指向父类的入口

如果某方法在子类中重写了,子类方法表中的地址将会被替换为指向子类实现版本的入口地址。

在JVM选择调用哪个方法的时候就是先查询动态类型的vtable和父类型的vtable。

总结

重载是静态的,重写是动态的

在我们开发过程中,对于大的对象使用过后,为了help gc ,我们会手动将大对象置为null,背后的原理是什么,是不是最佳的实践。

案例一

首先我们先看一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.bk.exercise.stack;
/**
* @author BK
* @description: 栈变量槽
* 启动参数 : -Xms20M -Xmx20M -verbose:gc
* @date 2019-08-18 23:22
*/
public class StackSlot {
public static void main(String[] args) {
byte[] byteArrays = new byte[5 * 1024 * 1024];
byteArrays = null ;
System.gc();
}
}

对于上面这段代码,我们在使用完byteArrays数组之后,然后将其置为null,通过System.gc()去建议JVM做一次GC,在GC后我们看到GC日志如下,真的做了将byteArrays所占空间回收掉了

1
2
[GC (System.gc())  6537K->5656K(19968K), 0.0006770 secs]
[Full GC (System.gc()) 5656K->368K(19968K), 0.0032760 secs]

这种做法是否推荐使用

虽然案例一说明手动将使用过的大对象赋值为null在某些情况下确实是有用的,但是这种做法不是最佳实践,这样的做法在一些极特殊情形下可以使用,比如一些对象占用内存较大,方法的栈帧长时间不能被回收方法调用次数太少达不过JIT的编译条件下可以使用,其它情况下不推荐使用

为什么不推荐

不推荐的原因如下

  • 从编码的角度来讲,合理的使用变量作用作用域来控制变量回收才是最优雅的解决方法,后面原理解析会说原因
  • 从执行角度来讲,使用null值的操作来优化内存回收是建立在对字节码执行引擎概念模型的理解之上的,JVM真正运行的代码是JIT编译后的本地代码,如果上述代码作为热点代码,经过JIT编译优化后做了无用代码消除,这时对变量设置为null的操作是会被消除掉的,这时候将变量设置为null就是没有意义的了

读者可以在这里停下来细想一下

原理解析

在线程运行中,线程栈中存在的基本单位是栈帧(Slot),栈帧中存储了局部变量表操作数栈动态连接方法返回地址附加信息,而且为了节省空间,栈帧是可以被重用

在方法体中定义的变量,如果PC计数器的值已超过变量的作用域,那么这个变量对应的Slot就可以交给其它变量使用

局部变量表中的引和的对象做为GC Roots的一部分,所以被局部变量表引用的数据,是不会被GC回收的,所以只要保证变量的作用域合理,再加上Slot可重用的特性,保证不需要的变量从局部变量表中清除,在GC发生的时候就可以保证对象被回收

案例二

基于上面的结论我们看一下案例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.bk.exercise.stack;
/**
* @author BK
* @description: 栈变量槽
* 启动参数 : -Xms20M -Xmx20M -verbose:gc
* @date 2019-08-18 23:22
*/
public class StackSlot1 {
public static void main(String[] args) {
{
byte[] byteArrays = new byte[5 * 1024 * 1024];
}
System.gc();
}
}

GC日志显示如下

1
2
[GC (System.gc())  6537K->5648K(19968K), 0.0009879 secs]
[Full GC (System.gc()) 5648K->5488K(19968K), 0.0033692 secs]

看GC日志我们发现,代码已经离开了byteArrays的作用域了,但是占用的内存却没有释放,主要原因是因为后面没有对局部变量表读写,所以被byteArrays占用的Slot还未被清除,给其它变量复用,所以bytesArrays仍然作为GC Roots的一个成员,无法被回收的。

我们对代码做一个细微的改动,在gc之前增加一个对局部变量表的读/写操作,再看一下效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.bk.exercise.stack;
/**
* @author BK
* @description: 栈变量槽
* 启动参数 : -Xms20M -Xmx20M -verbose:gc
* @date 2019-08-18 23:22
*/
public class StackSlot1 {
public static void main(String[] args) {
{
byte[] byteArrays = new byte[5 * 1024 * 1024];
}
int x = 10 ;
System.gc();
}
}

看GC日志显示如下

1
2
[GC (System.gc())  6537K->5668K(19968K), 0.0006792 secs]
[Full GC (System.gc()) 5668K->374K(19968K), 0.0030587 secs]

byteArrays占用的空间已经被回收

啰嗦两句

到这里可能有人会问,如果不加int x = 10 这个操作岂不是就不会回收了,确实是这样的,细想一下如果作用域后面没有其它操作了,那说明方法也该结束了,方法结束后局部变量表中引用的对象,如果不被其它线程引用的话,在GC时直接就被回收了

总结

在日常开发中,将变量分配合理的作用域是一个比较推荐的编码规则。

回到开篇,不使用的大对象为什么要手动设置null,真的有效吗?,希望可以从本文中总结出自己的答案。欢迎大家在评论区留言,一起探讨。

在当下随着系统规模变得越来越大,分布式的处理方式越来越受到业界的亲睐,大多数系统正在从集中式向分布式架构的变革,在分布式系统中,系统部署在不同的节点、数据分散在不同的数据库中,如果对这些数据进行分布式的事务处理具体非常大的挑战,分布事环境中,我们可以碰到 通信异常网络分区节点故障等故障,虽然存在着各种问题,但是在分布式领域,为了保证分布应用的可靠性,而分式式事务是无法回避的。

分布式事务

在一个分布式应用中,一个业务操作由多个分式的操作序列(子事务)组成,如何使多个子事务能够保证ACID的特性就是分布式事务。

分布式中的经典理论

CAP

  • (A)可用性:在分布式系统,如果想得到可用性,我们就是不允许数据有单点问题、应用存在单点故障,所以在分布式系统中数据副本、应用集群是保证可用性的关键,它给分布式系统带来 的巨大好处是不言而喻的

  • (C)一致性:在存在多个数据副本情况下,多个副本之间数据如何保证一致

  • (P)分区容错性:分布式系统所在的网络如果我们认为是一个大网络,网络分区就是这个大网络被划分成了多个子网络,而在多个子网络之前因为网络故障是无法互相通信的,这就是网络分区

在出现网络分区时候,如果不是整个网络出现故障,分布式系统仍然需要保证对外提供满足一致性和可用性的服务,这就需要系统在可用性和一致性之间做出一个权衡

要保证一致性,就需要将不一致的应用踢掉,停止提供服务,等数据同步之后再继续对外提供应用,这就会导致可用性的下降

要保证可用性,就要接受出现数据不一致的情况

在分布式系统中,系统中的组件必然会被部署在不同的机房、网络,否则就无所谓分布式了,所以分区是一个无法避免的问题,根据CAP理论,我们应该在C和A之间寻求平衡

BASE

基本可用(Basically Available): 是在出现故障时,允许以一种有损的架构方式,继续对外提供服务,保证核心业务可用,有损可以体现在很多方面,比如损失响应时间、功能上的缺失、降级托底数据等

软状态(Soft State):软状态对应的就是硬状态,是指允许系统中的数据存在不影响系统整体可用性的中间状态,业务上的软状态以付款为例,付款状态分为已付款、未付款,软状态是允许存在一种付款中的状态

最终一致(Eventually consistent):强调系统中所有的数据,在经过时间周期T的同步后, 最终一定可以达到一致的状态,不要求实时保证强一致性。

如何保证分布式下的一致性

2PC(两阶段提交)

2PC是Two-Phase-Commit的缩写,即两阶段提交。2PC被认为是一种一致协议,该协议将事务分为两个阶段,角色分为,协调者和参与者,二阶段提交将一个事务的处理过程分为了投票和执行两个阶段,其核心是对每个事务都采用先尝试后提交的处理方式,因此也可以将二阶段提交看作一个强一致性的算法,

阶段1:提交事务请求

  1. 事务询问:协议者向所有参与发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应
  2. 执行事务:各参与者节点执行事务操作,并将undo和redo信息记录事务日志中,在这步已经锁定事务资源
  3. 各参与者向协调者反馈事务询问的响应:如果参与者执行事务成功,就返回YES给协调者,标识事务可以执行,如果参者执行事务失败,则反馈NO给协调者,标识事务执行失败

阶段2:执行事务提交
在该阶段协调者会根据各参与者的反馈情况,来决定如何处理该分布式事务
情况一:收到的反馈都是YES执行事务提交

  1. 发送提交请求
    协调者向所有参与者节点发出 Commit 请求。
    2 .事务提交。参与者接收到 Commit 请求后,会正式执行事务提交操作,井在完成提交之后释放在整个事务执行期间占用的事务资源。
    3 .反馈事务提交结果。参与者在完成事务提交之后,向协调者发送 Ack 消息。
    4 .完成事务。协调者接收到所有参与者反馈的 Ack 消息后,完成事务。
    情况二:收到的反馈中包含NO或者在等待超时之后,中断事务
    发送回滚请求。协调者向所有参与者节点发出Rollback 请求。
    2 .事务回滚。参与者接收到 Rollback 请求后,会利用其在阶段一中记录的 Undo 信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。
    3 .反馈事务回滚结果。参与者在完成事务回滚之后,向协调者发送 Ack 消息。
    4 .中断事务。协调者接收到所有参与者反馈的 Ack 消息后,完成事务中断。

2pc缺点

同步阻塞
在阶段1的第二步,已经将事务资源锁定,一致到阶段2参与者在等待其它参与 者响应的过程中,得了处于一个阻塞状态,无法进行其它任何操作。极大限制了系统的性能
协调者单点问题
协调者在整个分布式事务中处于一个关键位置 ,如果协调者在第二阶段出现问题会导致事务无法完成,被锁定的事务资源将永远无法释放,
数据不一致
协调者在向所有参与者发出Commit请求后或在发送请求前崩溃,导致只有部分参与者收到Commit请求然后进行事务提交,未收到请求的则在自身等待超时之后中断事务,就会导致整个分布式系统中出现数据不一致的情况
太过保守,容错机制缺陷
在参与者与协调者失联的情况下, 协调者依靠自身的超时机制来决定是否中断事务,这种策略比较保守,没有一个比较完善的容错机制,任意一个节点的失败都会导致整个事务的失败。

3PC(三阶段提交)

3PC,是Three-Phase Commit缩写,即三阶段提交,是2PC的改进版,将整个事务处理分为CanCommit、PreCommit、doCommit3个阶段

3PC将2PC的第一阶段一分为CanCommit、PreCommit两个阶段,来减少同不阻塞影响的范围
引入客户端超时机构,在参与者与协调者失联情况下,在等待超时之后仍会提交事务,这种容错机制,有一种赌概率的成分
这种赌概率的做法,同样会带来数据不一致的情况

TCC

TRYING 阶段主要是对业务系统做检测及资源预留
CONFIRMING 阶段主要是对业务系统做确认提交,TRYING阶段执行成功并开始执行CONFIRMING阶段时,默认CONFIRMING阶段是不会出错的。只要TRYING成功,CONFIRMING一定成功。
CANCELING 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。

TCC的优点和限制

TCC事务的优点如下:

解决了跨应用业务操作的原子性问题,在诸如组合支付、账务拆分场景非常实用。
TCC实际上把数据库层的二阶段提交上提到了应用层来实现,对于数据库来说是一阶段提交,规避了数据库层的2PC性能低下问题。

TCC事务的缺点

TCC的Try、Confirm和Cancel操作功能需业务提供,开发成本高。由于网络原因,可能会重复提交和取消,需要commit和cacel接口支持幂等。
当然,对TCC事务的这个缺点是否是缺点,是一个见仁见智的事情。
到底要不要使用TCC到底要不要使用TCC事务,取决于以下几点:
是否真正有保证跨应用业务操作的原子性需求。
研发上能否投入资源开发相对应的TCC接口。
当然还有最后一点,能否搞定一个稳定的、高可用的、扩展性强的TCC事务管理器。

Paxos

待补充

ZAB

待补充

目录

[TOC]

#谈谈http1.0 ,1.1 ,2.0

  • http1.0主要规定了浏览器与服务器只保持短暂的连接,浏览器每次请求都需要向服务器建立一个TCP连接,服务器处理完之后,断开TCP连接

  • http1.1协议是一个标准化的协议,针对http1.0的缺陷进行了改进

    • 连接可复用,节省了三次握手和四次挥手的时间,在并发高的时候性能有很大的提升,可以通过设置connection:close在每次请示处理完之后关闭连接,http1.1默认请求头中connection:keepalive。
    • 引入额外的缓存控制机制
    • 引入内容协商
    • 支持响应分块
    • 允许在第一个应答被完全发送之前发送第二个请求,降低通信延迟
    • 安全级别更高
  • http2.0

    • 所有数据二进制传输
    • 赋予请求优先级:为请求分配优先级顺序,主要是为了在同一个连接里面发送多个请求时,解决因带宽低而导致响应变慢的问题,发送多个请求时不再需要按顺序发送
    • 头信息压缩(有效减少带宽使用)
    • server push(服务端主动推送消息至客户端)
    • 信道复用:通过单一的TCP连接,无限制处理多个HTTP请求,所有请求都在一个TCP连接上完成,因此TCP的处理效率得到了提高
    • 分帧传输
    • 服务器提示功能:服务端主动提示客户端请求所需的资源,如果客户端提前知道所需要的资源,并且本地已经缓存的情况下,可以避免发送不必要的请求

HTTPS

我们在电脑或者手机上打开网页时,有时候会看到一些很突兀的广告,这其实就是http的通信内容被第三方篡改了,强行插入的内容

由于http协议传输的文本数据是没有加密的,都是明文传输,所以在网络传输过程中可能存在窃听风险、篡改风险、冒充风险,https主要通过对通信内容协商加密、对通信内容增加校验机制、配备证书,解决了http协议存在的三种风险

https协议在http和TCP协议之间增了TLS/SSL加密层,使用HTTPS

  • 先用非对称加密传输密码,然后用这个密码对称加密数据,使得第三方无法获得通信内容
  • 发送方将数据的哈希结果写到数据中,接收方解密后对比数据的哈希结果,如果不一致则说明被修改。由于传输数据加密,第三方无法修改哈希结果。
  • 权威机构颁发证书,再加上证书校验机制,避免第三方伪装`参与通信.

GET 和 POST 的区别

GET 请求通常用于查询、获取数据,而 POST 请求则用于发送数据、
GET所有数据都显示在URL中,安全性较差,POST数据不会显示在URL中,在发送敏感数据时,要使用POST,因为POST请求不会被浏览器历史记录保存
GET后退按钮/刷新无害,POST数据会被重新提交(浏览器应该告知用户数据会被重新提交)。
GET书签可收藏,POST为书签不可收藏
GET能被缓存,POST不能缓存
GET发送数据时对数据长度有限制最大为2M,POST无限制
GET只允许 ASCII字符。POST没有限制。也允许二进制数据。

CORS ( Cross-Origin Resource Sharing )

CORS,跨域资源共享,它允许浏览器向跨源服务,发出请求,从而克服AJAX只能同源使用的限制。
跨域访问只会在浏览器访问时才才会出现,使用curl访问时,是不会出现跨域问题的。

  • 跨域的时候允许的请求方式:get、head、post ,其它请求是不允许跨访问的,需要预请求判断是否支持跨域(OPTIONS)
  • 跨域的时候允许的Content-Type: text/plain、multipart/form-data、application/x-www-form-urlencoded

解决域中的方法

  • 服务端通过header告诉浏览器支持跨域
  • 使用JSONP

在服务端响应时候增加header信息如下

1
'Access-Control-Allow-Orign': '*'

*代表,支持任何域名跨域访问

但这样做是不安全的,我们可以限制只有固定域名可以跨域
'Access-Control-Allow-Orign': 'http://domain.com'

这种方法只能设置一个域名,如果是多个域,可以在后端服务做判断,对于允许的域名响应时设置该响应头信息

只有拿到响应的头信息浏览器才知道是否支持跨域。浏览器发起的真实请求已经被服务器处理,并已交内容返回至浏览器,只是浏览器会根据响应中的跨域设置,对请求返回内容进行了处理。

其它头信息

  • ‘Access-Control-Allow-Headers’: ‘X-TEST-CORS’ //允许跨域的头信息
  • ‘Access-Control-Allow-Methods’: ‘PUT,DELETE’ //允许跨域的方法
  • ‘Access-Control-Max-Age’: ‘1000’ //用一标识允许在1000秒不再发送OPTIONS预请求。时间单位是秒

jsonp实现方式

原理

其实在网页中,浏览器对于link,script,image标签中加载的请求,是允许跨越访问的,Jsonp就是利用这种方式,访问到服务器的内容,对内容进处理后,现返回的。

缓存

Cache-control 可缓存性

  • public: 是指请求返回的过程中,所经过的路径中,包括中间的代理服务器,都可以对返回内容进行缓存
  • private: 只有发起请求的客户端才可以缓存
  • no-cache: 任何地方都不可以缓存,每次请求会经过服务器验证,是否可以使用缓存
  • no-store客户端不会发送任何和缓存的请求头,直接忽略缓存

到期

  • max-age= //客户端会使用max-age
  • s-maxage= //只有在代理服务器中才生效
  • max-stale= //发起请求主动携带此请求头,表示如果缓存已经过期了,客户端可以在max-stale这个时间内还可以使用过期的缓存,而不去请求新的内容

重新验证

  • must-revalidate max-age缓存已过期 ,必须到源服务端验证获取数据
  • proxy-revalidate //使用在缓存服务器,指定缓存过期后,必须到尖服务器获取数据
    -其它
    no-store 永远到服务器端拿最新的数据
    no-trasform 禁止代理服务器,对返回的数据进行压缩或其它处理

    对于代理服务器的请求头都是约定,只是一种规范,没有强制性限制,代理服务器可以遵守也可以不遵守

  • 示例
    1
    'Cache-Control': 'max-age=10000 , no-cache'
  • 使用技巧

如果过期时间设置得不合理时,会导致客户端缓存的时间过长无法获取到最新的数据,这也是我们前端开发常见的问题

将打包完成的js,根据内容进行hash将hash码追回到js文件名中,页面中引用新的文件名,这样就可以达到一个更新缓存的目的,这也是一种常用的方案。

数据验证

Last-Modified

服务端返回头信息Last-Modified,告诉客户端资源的上次修改时间,客户端后续请求会带着请求头,If-Modified-SinceIf-Unmodified-Since,对应的值是服务端 之前返回的Last-Modified的值,服务端会对比上次修改时间和头信息的时间,服务器会通知客户端是否可以直接使用缓存

Etag(数据签名)

对资源的内容进行签名,如果数据有修改,数据签名就会发生变化,最简单的实现方法就是对资源内容进行HASH计算,得到一个唯一的标识,将hash值通过etag头返回给浏览器,
浏览器器会在将etag的值,在请求时候通过 If-Match或者If-None-Match请求头带上,服务器端对比头里面的etag和服务端真实的etag,告诉浏览器是否可以使用缓存

重定向(Redirect)

302(临时重定向)

通过302,服务端告诉浏览器你需要的资源在哪里,去指定地址去加载,浏览器会再发送请求到重定向的地址获取资源
服务端响应状态302 + 在头信息中增加Location告诉客户端所请求的资源临时重定向到的地址

301(永久重定向)

如果是永久跳转,可以使用301 + Location
301指定资源永久跳转,浏览器下次访问的时候,会直接指向新的地址,浏览器会尽可能时间长的缓存301重定向的地址,除非主动清理缓存,下次访问会从disk cache直接获取

使用时候要谨慎,使用之服务端无法控制

CSP ( Content-Security-Policy )

  1. 限制浏览器只能通过http和https来访问,无法通过inline script执行

    'Content-Security-Policy' : 'default-src http: https:'

    在html中写 这种方法称为inline script

  2. 限制只能加载本站的域名脚本

    'Content-Security-Policy':'default-src \'self\''

    目的是为了防止被嵌入脚本后,执行外部有风险的脚本,这种方式也会影响图片不可以使用外链,只能加载本站脚本和图片

  3. 上一条中会影响图片加载,可以使用如下配置,只对脚本作限制

    'Content-Security-Policy':'script-src \'self\''

  4. 限制只能访问本站和指定网站的资源

    'Content-Security-Policy':'default-src \'self\' http://albk.tech/'

  5. 限制FORM表单提交到其它域名

    'Content-Security-Policy':'default-src \'self\'; from-action \'self\''

  6. 发生CPS设置的情况时候产生cps-report报告

    'Content-Security-Policy':'script-src \'self\'; from-action \'self\'; report-uri /xxx'

    xxx接收报告的本站路径

  7. 设置只产cps-report不限制加载

    'Content-Security-Policy-Report-Only':'script-serc \'self\'; from-action \'self\'; report-uri /xxx'

  8. 除了header其它实现方式

    可以在html中使用meta标签

    <meta http-equiv="Content-Security-Policy" content = "script-serc 'self'; from-action 'self'">

    meta中无法使用; report-uri /xxx,如果要使用report-uri必须使用header

  9. 其它策略指定

  • child-src
  • connect-src
  • font-src
  • img-src
  • media-src
  • object-src
  • script-src
  • style-src
  • form-action

    参考资料: CPS策略指令

数据协商

请示

  • Accept,指定需要的数据类型
  • Accept-Encoding 数据以什么方式压缩传输
  • Accept-Language:接受的语言类型
  • User-Agent :表示浏览器的一些信息,可用于判断返回PC或者H5页面

返回

  • Content-Type : 返回的数据格式
  • Content-Encoding: gzip
  • Content-Language
  • X-Content-Type-Options: nosniff

    IE浏览器,会自动预测返回的内容,导致一些安全问题, 这引头会禁止自动预测

  • Vary: ‘X-Test-Cache’

    除了针对URL做缓存,还可以对不同的Vary进行区分,来缓存
    设置了Vary之后,只能Vary的值和value相等时才会使用缓存,一般用于区分 PC ,H5 ,APP端或者判断语言类型,对不同的值 进行判断,实现细粒度的缓存


http长连接

因为http请求底层是TCP连接,建立需要三次握手,断开连接需要四次挥手,每次创建连接是一个很大的开销,如果客户端,可以维护一些固定大小的长连接,是可以很在程序上提升效率。

一般浏览器会维护一定大小的长连接,比如chorm浏览器默认是对同一个域名维护6个长连接,如果同一个域名请求过多,就出现了排队现象

在电商网站请求同一域名请求校多的时候,就会拆成不同的域名来优化网站响应速度,例如我们会看到一些网站打开,会看到通过http://img1.albk.tech,http://img2.albk.tech不同的域名去加载图片

Cookie

  • Set-Cookie设置cookie
  • max-age和expires设置过期时间
  • Secure只有在https的时候才发送
  • HttpOnly无法通过document.cookie访问,可防止CSRS攻击

TCP和UDP的区别

TCP协议是面向有连接的字符流传输,在使用TCP协议之前,需要客户端和服务端通过三次握手在双方之前建立连接,会有数据重传、流量控制等功能保证数据可以无差错、不丢失、不重复、按序传输。

TCP连接是点到点的全双工可靠信道。

TCP首部开销20字符

UCP协议是面向无连接的报文传输,UDP尽最大努力交付,但是不保证可靠交付。

UDP支持一对一、一对多、多对多、多对一的不可靠信道

UDP首部开销只有8字节

sendRedirect与foward区别

foward是服务器端控制页面转向,在客户端的浏览器地址中不会显示转向后的地址

sendRedirect则是完全的跳转,浏览器中会显示跳转的地址并重新发送请求链接。

原理:

​ forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后再将这些内容返回给浏览器,浏览器根本不知道服务器发送的这些内容是从哪来的,所以地址栏还是原来的地址。

​ redirect是服务器端根据逻辑,发送一个状态码,告诉浏览器重新去请求的那个地址,浏览器会用刚才的所有参数重新发送新的请求。

再谈HTTPS

共享密钥加密:加密和解密使用同一个密钥,但是如何在互联网上安全的转移密钥,这是共享密钥的一个困境

公开密钥加密:加密和解密使用一对非对称的密钥,一把叫公钥,一把叫私钥,公钥任何人都可以获得,加密后的数据只有私钥可以解密

  • HTTPS采用混合加密机制:使用公开密钥加密后安全的交换稍后在共享密码加密中要使用的密钥,在确保交换的密钥是安全的前提下,使用共享密钥加密方式进行通信

可能聪明的你已经发现,如何证明公开密钥是正确的?

HTTPS协议引入了数字证书认证机构或相关机构办法的公开密钥证书,证书中包含两部分信息服务器的公开密钥数据证书认证机构的数据签名,

  1. 服务器将这份由数据证书认证机构办法的证书发送给客户端

  2. 客户端拿到证书后,使用公开密钥向数据证书认证机构确认证书上的数据签名正确,是确保服务器的真实性

  3. 认证通过可以说明公开密钥的是可信的

如何将证书安全的转移至客户端

大多数浏览器开发商发布版本时,会事先在内部植入常用认证机关的公开密钥,以确保公开密钥安全转移

服务端如何校验客户端的真实性

HTTPS中可以使用客户端证书,客户端证书一秀是由安全性极高的认证机构颁发的证书,因为客户端证书是收费的, 一般用于特殊的业务,比如网上银行,确认使用的终端是授权的

HTTPS这么安全为不一直使用HTTPS呢

  • 因为HTTPS的每次通信都会加密,这会消耗更多的CPU、内存等资源及大影响了服务器性能
  • 并不是所有网站、所有业务的数据都是敏感数据,一般只在包含敏感数据时才使用HTTPS加密通信
  • 如果一直使用HTTPS,对于访问量较高的网站,会影响网站的性能
  • HTTPS证书的开销也是需要考虑的因素
  • HTTPS要比HTTP慢2-100倍

OSI七层模型

应用层:FTP,DNS,HTTP都属于该层

表示层:数据的表现式,如加密

会话层:对应用会话的管理同步

传输层:提供处于网络连接中的两台计算机的数据传输,TCP,UDP都属于该层,主要解决了端口到端口的问题

网络传输层:用来处理在网络上流动的数据包,作用不是在众多的选项内选择一条传输路线,路由器工作在该层,主要记录了,源IP,目标IP

链路层:用来处理连接网络的硬件部分,硬件上的范畴都在链接层的作用范围内,交换机工作在该层,主要是基于源的自动学习 ,源MAC,目标MAC

物理层:网线,网口

交换机的记录了IP与端口

路由器记录了网段与端口

目录

[TOC]

从注册中心到 IGMP:集群构建方式的演进与实践

一、集群的构建方式

传统的注册中心方式

原理

传统的注册中心方式存在一个中心化服务,像 ZooKeeper、Etcd 等。节点启动时向注册中心注册自身信息(如 IP 地址、端口、服务名称等),同时从注册中心获取其他节点信息以完成节点发现和集群构建。

优点

  • 易于管理:注册中心作为集中管理点,能方便查看和管理集群所有节点信息。
  • 可靠性高:借助注册中心的监控和故障转移机制,可保障集群高可用性。
  • 解耦服务:服务提供者和消费者通过注册中心交互,无需直接知晓对方信息,降低服务间耦合度。

缺点

  • 单点故障:注册中心是系统单点,若出现故障,可能致使整个集群无法正常工作。
  • 性能瓶颈:集群规模较大时,注册中心可能成为性能瓶颈,影响系统响应速度。
  • 复杂度高:需额外维护注册中心,增加系统复杂度和运维成本。

TCP 模式

原理

TCP 模式下,节点要预先配置种子节点列表。新节点启动后尝试连接种子节点,通过种子节点获取集群中其他节点信息来加入集群。

优点

  • 不受网络限制:不依赖网络多播功能,适用于各种网络环境。
  • 精确控制:可精确控制节点加入顺序和集群规模,便于管理。

缺点

  • 配置复杂:需预先知晓种子节点信息,配置相对复杂。
  • 种子节点故障影响大:若种子节点出现故障,可能影响新节点加入。

组播方式

原理

组播方式基于 IGMP(Internet Group Management Protocol,互联网组管理协议)。节点通过向特定多播组地址和端口发送、接收消息实现节点发现和集群构建。新节点启动时向多播组发送宣告消息,其他节点接收后将其加入集群。

优点

  • 配置简单:无需预先知道其他节点信息,新节点可自动发现集群中的其他节点。
  • 自动扩展:新节点加入集群无需人工干预,系统可自动扩展。

缺点

  • 网络依赖:依赖网络多播功能,部分网络环境(如云环境、部分企业网络)可能不支持多播。
  • 网络流量问题:多播消息在网络中传播,可能产生大量网络流量,影响网络性能。

二、再谈节点发现的组播和 TCP 模式

组播模式(IGMP 实现)

启动

当节点启动并配置为组播模式时:

  1. 加入多播组:操作系统层面,节点网络接口通过 IGMP 协议向本地路由器发送加入特定多播组请求。
  2. 监听多播端口:节点在配置的多播端口监听针对该多播组的消息。

发现

  1. 发送宣告消息:新节点启动后,构造含自身信息(如 IP 地址、端口等)的宣告消息,封装成网络数据包,目的地址设为配置的多播组 IP 地址,源地址为自身 IP 地址,端口号为配置的多播端口,然后发送到本地网络。
  2. 接收消息并处理:其他节点在监听端口接收到新节点宣告消息后,根据目的地址和端口号过滤,将符合条件的数据包传递给框架(如 Hazelcast)解析,提取关键信息,将新节点加入本地集群节点列表。

数据同步

节点通过组播消息进行数据同步。如节点状态变化时,向多播组发送状态更新消息,其他节点接收后更新本地集群状态信息,保证各节点对集群状态认知一致。

消息处理

  • 消息封装与解析:框架(如 Hazelcast)将需发送信息封装成特定格式消息,含元数据标识消息类型和用途;接收消息后进行解析提取关键信息。
  • 业务逻辑处理:依据消息类型和内容执行相应业务逻辑,如接收新节点宣告消息将其加入集群,接收状态更新消息更新本地集群状态。

错误处理和重试机制

  • 错误检测:框架监控组播消息发送和接收过程,检测错误,如多次发送失败或消息格式错误,触发错误处理逻辑。
  • 重试机制:出现错误时,尝试重新发送消息或采取其他恢复措施,确保消息正常传递和处理,保证集群稳定性和一致性。

健康检查

节点间定期通过点对点方式发送心跳消息进行健康检查。若一定时间未收到某节点心跳消息,认为该节点故障,将其从集群节点列表移除。

TCP 模式

启动

节点启动后读取配置的种子节点列表。

发现

  1. 连接种子节点:新节点尝试与种子节点建立 TCP 连接,成功后向其发送请求获取集群其他节点信息。
  2. 获取节点信息:种子节点收到请求后,将自身维护的集群节点列表发送给新节点。
  3. 加入集群:新节点收到列表后,尝试与其他节点建立连接,完成节点发现和集群加入过程。

数据同步、消息处理、错误处理和重试机制、健康检查

与组播模式类似,但消息通过 TCP 连接传递,而非组播。

三、引入 Hazelcast 框架和 Vert.x 实现一个简单的分布式系统

原理

Hazelcast 是开源的分布式内存数据网格,提供分布式数据结构和集群管理功能。Vert.x 用于在 JVM 上构建响应式、分布式和高性能应用程序。结合二者可实现简单分布式系统,利用 Hazelcast 集群管理功能实现节点发现和数据同步,用 Vert.x 构建业务逻辑。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import io.vertx.spi.cluster.hazelcast.HazelcastClusterManager;
import com.hazelcast.config.Config;
import com.hazelcast.config.JoinConfig;
import com.hazelcast.config.MulticastConfig;

public class DistributedSystemExample {
public static void main(String[] args) {
// 配置 Hazelcast
Config hazelcastConfig = new Config();
JoinConfig joinConfig = hazelcastConfig.getNetworkConfig().getJoin();
joinConfig.getTcpIpConfig().setEnabled(false);

MulticastConfig multicastConfig = joinConfig.getMulticastConfig();
multicastConfig.setEnabled(true);
multicastConfig.setMulticastGroup("224.2.2.3");
multicastConfig.setMulticastPort(54327);

// 创建 Hazelcast 集群管理器
HazelcastClusterManager clusterManager = new HazelcastClusterManager(hazelcastConfig);

// 配置 Vert.x 选项
VertxOptions options = new VertxOptions().setClusterManager(clusterManager);

// 创建集群化的 Vertx 实例
Vertx.clusteredVertx(options, res -> {
if (res.succeeded()) {
Vertx vertx = res.result();
System.out.println("Clustered Vert.x instance created successfully.");

// 在这里可以添加业务逻辑
} else {
System.err.println("Failed to create clustered Vert.x: " + res.cause().getMessage());
}
});
}
}

代码解释

  1. 配置 Hazelcast:创建 Hazelcast 配置对象,禁用 TCP 发现机制,启用多播发现机制,配置多播组 IP 地址和端口。
  2. 创建 Hazelcast 集群管理器:用配置好的 Hazelcast 配置对象创建集群管理器。
  3. 配置 Vert.x 选项:将 Hazelcast 集群管理器设置到 Vert.x 选项中。
  4. 创建集群化的 Vertx 实例:调用 Vertx.clusteredVertx 方法创建实例,根据结果处理。

四、总结

本文介绍了传统注册中心方式、TCP 模式和组播方式三种集群构建方式,分析了各自优缺点。重点阐述组播模式下基于 IGMP 协议的节点发现过程,涵盖启动、发现、数据同步、消息处理、错误处理和重试机制及健康检查等方面。还引入 Hazelcast 框架和 Vert.x 实现简单分布式系统并给出示例代码。

不同集群构建方式适用于不同场景,传统注册中心方式适用于对管理和可靠性要求高的场景;TCP 模式适用于网络环境复杂、需精确控制的场景;组播方式适用于配置简单、自动扩展需求高的场景。实际应用中,需根据业务需求和网络环境选择合适方式。结合 Hazelcast 和 Vert.x 可方便构建分布式系统,利用其功能实现节点发现、数据同步和业务逻辑处理。

Mysql数据库事务的4个特性

  • 原子性(Atomicity)
    原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
  • 一致性(Consistency)
    事务前后数据的完整性必须保持一致
  • 隔离性(Isolation)
    事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离
  • 持久性(Durability)
    持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响

Mysql的事务隔离级别,以及每种级别下会产生的问题和对问题的理解

  • Read uncommitted:隔离性最差的一种方式,违反了为数据库事务的隔离性,事务A读取了事务B对数据C修改后但未提交的内容,即事务B还未提交,还在执行过程中;

    在这种级别下会出现脏读

  • Read Committed(读已提交/不可重复读):违反了数据库事务的一致性,是指在同一个事务内,对同一个数据集合的连续读取两次,读 到的数据是不一样的。也就是说,Read Committed模式下,一个事务既可以感知另一个事务添加新数据,也能感知这个事务对数据的修改。

    RC模式,避免了脏读,但是可能会造成不可重复读。

    select时会受update的影响出现在同一个事务中两次读到数据不一致,主要体现在数据行的内容不一致,
    Mysql使用了Record-Lock,同一时刻只允许一个事务对同一数据修改,解决了脏读

  • Repeatable Read(可重读): MySQL默认级别,解决了不可重复读,但会出现幻读的情况。每个事务独立执行,如果一个事务添加了新数据,并已提交完毕;另外一个正在执行的事务能看到新加的数据。但如果一个事务是修改数据后提交完毕,另一个正在执行的事务是看不到这种修改的。在新增加数据的情况下,破环的事务了的隔离性。

    在RR这种隔离级别下会出一幻读

    幻读是select会受insertdelete的影响下会出现幻读行,体现在同事个事务中连续两次读到的数量不一致

    但是mysql在RR级别下,通过Next-Key Lock(Rercord-Lock+ Gap-Lock)算法,避免了不可重复读的现象。所以InnoDB存储引擎在RR级别下就达到了3度的隔离。

  • Serializable(串行化执行):最高隔离性级别。同时执行的两个事务完全隔离,每个事务有独立的运行空间。主要应用于InnoDB存储引擎的分布式事务,而且分布式事务中要求数据库的隔离级别必须为Serializable

Mysql的过引方法有哪些?

B+树和HASH

为什么不使用B树,要使用B+树

B+树是二次查询树的优化,有自我调整平衡的能力和节点顺序能力,同时支持范围查询

InnoDB和MyISAM区别

  • innodb支持事务MyISAM不支持事务
  • MyISAM锁的粒度是表级,而InnoDB支持行级锁定
  • InnoDB数据的存储使用了聚集的方式,表的数据存储是按主键的顺序进行存放,如果没有显示指定主键,引擎会为每一行生成一个6字节的ROWID作为主键,索引文件就是数据文件,
  • MyISAM适合存储报表的数据,无事务,简单查询,InnoDB适用于有事务处理的情况
  • 主索引的区别,InnoDB是聚集索引数据文件本身就是索引文件。而MyISAM是非聚集索引,索引文件(MYI)和数据文件(MYD)是分离的,索引文件仅保存数据记录的地址
  • 辅助索引的区别:InnoDB的辅助索引data域存储相应记录主键的值而不是地址。而MyISAM的辅助索引和主索引没有多大区别,只是主索引要求key是唯一的,而辅助索引的key可以重复

数据库如何解决事务冲突

​ 大多数数据库使用加锁和数据版本管理两种策略,它们是两种不同的思想:
锁分为乐观锁(optimistic locking)与悲观锁(pessimistic locking)
​ 悲观锁(读写互相阻塞)
​ 乐观锁(写写阻塞)
数据版本管理
一种乐观锁的实现,所有的事务都可以同时修改相同的数据,每个事务都持有所有数据的一个拷贝,最终只会有一个事务的修改会提交成功持久化,其它事务将会回滚,这种方式因为读写不会相互阻塞,所以带来了性能上的提升,这处方式缺点就是相同的数据会存在多份,所以需要巨大的磁盘空间开销

如何判断数据库进入了死锁

依据lock manager的哈希表,能画出一个依赖关系图(类似上面的截图)。如果在图中出现了环路,即意味着出现了死锁。检查是否出现环路是非常耗时的,因为依赖关系图的数据量通常很庞大;所以,一般采用更简单的方法:判断是否超时。如果事务申请的锁未在指定的超时时间内分配,则认为事务进入了死锁。

进入死锁之后如何处理?

出现死锁时,Lock manger将选择其中一个事务回滚以解除死锁状态。选择哪一个事务回滚,这是个很复杂的问题,要考虑以下方面:

  • 回滚涉及数据量最小的事务(造成混滚的代价最小)
  • 回滚最新提交的事务(因为其它事务等待的时间更长)
  • 回滚耗时更短的事务(避免长时间等待,线程饿死
  • 即使回滚,又有多少其它事务会受此回滚的影响?

索引为什么可以提高查询效率?

Mysql存储是根据事务将依次次数据持久化在磁盘上,我们使用时候的不是按写入顺序读取,所以在我们使用数据时候,会产生很多随机IO磁盘,通过索引,将数据通过一定的规则整理,将随机读变成顺序度,从而减少磁盘IO,提升查询效率

MySQL为何将节点大小设置为页的整数倍?

局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。如果一次IO读取的数据我们称之为一页(page)。具体一页有多大数据跟操作系统有关,一般为4k或8k。

MySQL巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了读取一个节点只需一次I/O。假设B+Tree的高度为h,一次检索最多需要h-1次I/O(根节点常驻内存),复杂度O(h) = O(logmN)。实际应用场景中,M通常较大,常常超过100,因此树的高度一般都比较小,通常不超过3。

磁盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数据项的数量越多,树的高度越低。这就是为什么每个数据项,即索引字段要尽量的小,比如int占4字节,要比bigint8字节少一半。

当索引的KEY越小(字段越小),一页可以存储的索引越多,一次载入到内存中的数据也就越多,这也解释了为什么B+树,要求把真实的数据放到叶子节点中,而不是内层节点,一旦放到内层节点,一次读到的索引会大幅度减少,导致频繁的IO,另一方面也会导致树的高度增加。

Mysql中的联合索引

非统计查询时必须符合最左原则

如果是统计操作,并且是覆盖索引的,则优化器会选择走联合索引,通过索引覆盖来优化count(*)

优化器选择不使用索引的情况?

如果表字段X存在辅助索引,而在执行SQL, select * from table1 where x>400 时,在查看扫行计划时如果未选择辅助索引,这种情况下多发生于范围查找、join连接操作,原因如下:

  1. 如果被查询出来的数据量占整张表的20%左右时,可能会导致优化器放弃辅助索引,而改为聚集索引查询,因为如果使用辅助索引,会导致大量的随机读 ,改为聚集索引,使用顺序读性能远远高于随机读。
  2. 如果查出来的数据量较少时,优化器是会选择辅助索引

Mysql5.6的几个优化

MRR :目的就是减少磁盘的随机访问,并且将随机访问转化为较为顺序的数据访问,优化适合用于range , ref ,eq-ref

主要原理就是,将辅助索引查询到的数据放于缓存中,对缓存中的数据根据RowID进行排序,然后用较为顺序的方式来访问数据,这样就减少了随机读 ,并且减少了缓冲页中被替换的次数.

​ 此外MRR还可以将某些范围查询拆分为键值对,来进行批量数据查询,这样的好处就是可以在拆分过程中,直接过滤掉一部分不符合条件的数据。

​ 执行计划的Extra中会看到using MRR

ICP : 索引条件下压,就是将where部分的过滤操作,下沉到存储引擎层,在某些查询下,可以大大减少返回上层的数据,从而提高整体性能。 优化适用于range , ref , eq_ref , ref_or_null,执行计划Extra中会看到Using index condition

mysql中的hash索引

innodb中冲突机制采用链表方式,哈希函数采用除法散列方式,Hash索引只能用来搜索等值查询,无法处理范围查询,所以使用场景比较少。

mysql中的锁

共享锁(S):是Share的缩写,共享锁的锁粒度是行或者元组(多行)。一个事务获得了共享锁之后,就可以对锁定范围内的数据进行读操作。
排他锁(X):是eXclusive的缩写,排他锁的粒度和共享锁一样,也是行或者元组。一个事务获得排他锁之后,就可以对锁定范围内的数据执行insert/delete/update操作。
意向锁:意向锁是一种表级锁,锁的粒度是整张表。分为意向共享锁(IS)和意向排他锁(IX)

意向锁的存在价值在于在定位到特定的行所持有的锁之前,提供一种更粗粒度的锁,可以理解为一种快速失败的机制,可以大大节约引擎对于锁的定位和处理的性能,因为在存储引擎内部,锁是由一块独立的数据结构维护的,锁的数量直接决定了内存的消耗和并发性能。例如,事务A对表t的某些行修改(DML通常会产生X锁),需要对t加上意向排它锁,在A事务完成之前,B事务来一个全表操作(alter table等),此时直接在表级别的意向排它锁就能告诉B需要等待(因为t上有意向锁),而不需要再去行级别判断。 

兼容:SS、IS和IS、IS和IX、IX和IX

意向锁之间彼此不会冲突,因为它们都只是“意向”,所以是可以兼容的。在加行锁之前会使用意向锁判断是否冲突;

不兼容:除了IX和IX只要存在任意一个排它锁都不会兼容

MYSQL执行计划的中的TYPE有哪些

效率由好到差

  • system : 表里面仅一行记录
  • const :表示通过索引一次就找到,表中有多行记录,但是最多只有匹配到一行记录
  • eq_ref: 使用主键和唯一索引,只有一行记录匹配到
  • ref: 只使用到普通索引,没有使用主键和唯一 索引
  • ref_or_null: 类似 ref,不同的是mysql会在检索的时候额外的搜索包含null 值的记录
  • index_merge :索引合并
  • unique_subquery:主键子查询
  • index_subquery:非主键子查询
  • range :使用范围查询
  • index: 使用索引
  • all :全表扫描

INNODB和MYISAM中的表存储

  • INNODB

    • 独立表空间下: .frm 、 .ibd
    • 共享表空间下: .frm 、.idbdata1
    • 分区下: .frm 、.par、 p0.ibd 、 p1.ibd 、  ….. 、pn.ibd 。只有在独立表空间打开时才可以做分区(innodb_file_per_table =1 )
  • MYISAM

    • 无分区:.frm 、 .myd 、.myi
    • 有分区:.frm、 .par 、.myd(1-n) 、myi(1-n)

MYSQL的分区方式

  • RANGE分区:根据年或者主键等分区
  • LIST分区:适合枚举区分,值固定
  • HASH分区:预先确认分区数,如果确保数据可以在预先确定的分区中可以平均分布,可以考虑
  • KEY分区:与HASH分区类似,但它的KEY可以不是整数

MYSQL中的组合索引

适合单独查询返回结果很多,组合查询返回很少数据

组合索引使用时要满足最左原则(驱动索引)

仅存在等值查询时,组合索引顺序不影响性能

如果存在等值+范围时,建立索引时先等值后范围,等值列前置

组合索引的排序,使用时要与建立时的顺序一致,全正或全负

0%