1、1.7中底层是数组+链表,1.8中底层是数组+链表+红黑树(数组的默认初始容量是16、扩容因子为0.75,每次采用2倍的扩容。也就是说,每当我们数组中的存储容量达到75%的时候,就需要对数组容量进行2倍的扩容。链表长度大于等于8时会树化,小于等于6时进行链化。),加入红黑树的目的是提高HashMap插入和查询整体效率。
2、1.7中链表插入使用的是头插法,1.8中链表插入使用的是尾插法,因为1.8中插入key和value时需要判断链表元素个数,所以需要遍历链表统计元素的个数,所以正好就直接使用尾插法。
3、1.7中哈希算法比较复杂,存在各种右移与异或运算,1.8中进行了简化,因为复杂的哈希算法的目的就是提高散列性,来提供HashMap的整体效率,而1.8中新增了红黑树,所以可以适当简化哈希算法,节省CPU资源。
put时间复杂度:
put操作的流程:
第一步:key.hashcode(),时间复杂度O(1)。
第二步:找到桶以后,判断桶里是否有元素,如果没有,直接new一个entey节点插入到数组中。时间复杂度O(1)。
第三步:如果桶里有元素,并且元素个数小于6,则调用equals方法,比较是否存在相同名字的key,不存在则new一个entry插入都链表尾部。时间复杂度O(1)+O(n)=O(n)。
第四步:如果桶里有元素,并且元素个数大于6,则调用equals方法,比较是否存在相同名字的key,不存在则new一个entry插入都链表尾部。时间复杂度O(1)+O(logn)=O(logn)。红黑树查询的时间复杂度是logn。
HashMap的容量是2次幂的原因?
因为底层在put的时候和resize的时候都使用了代码(容量-1)&hash 进行了按位与运算,如果容量为2的次幂,则 转换为二进制为全1,进行按位与之后得到的值更加分散,减少了hash碰撞。
1.7中存在永久代,1.8中没有永久代,替换它的是元空间,元空间所占的内存不是虚拟机内部,而是本地内存空间,这么做的原因是,不管永久代还是元空间,他们都是方法区的具体实现,之所以元空间的内存改成本地内存,官方的说法是为了和JRockit统一,不过额外 还有一些原因,比如方法区所存储的类信息通常是比较难确定的,所以对于方法区的大小是比较难指定的,太小了容易出现方法区溢出,太大了又会占用太多虚拟机的空间,而转移到本地内存后则不会影响虚拟机所占用的内存。
底层都是char数组
String是final修饰的,不可变的,每次操作都会产生新的String对象
StringBuffer和StringBuilder都是在原对象上操作
StringBuffer是线程安全的,StringBuilder是线程不安全的
StringBuffer方法都是synchronized修饰的
性能:StringBuilder>StringBuffer>String
stringbuilder和stringbuffer 都是继承了AbstractStringBuilder抽象类
场景:经常需要改变字符串内容时使用StringBuilder、StringBuffer
优先使用StringBuilder,多线程使用共享变量时使用StringBuffer
为什么stringbuilder线程不安全
// 存储的字符串(通常情况一部分为字符串内容,一部分为默认值)
char[] value;
// 数组已经使用数量
int count;
上面的这两个属性均位于它的抽象父类AbstractStringBuilder中
如果查看构造方法我们会发现,在创建StringBuilder时会设置数组value的初始化长度。
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
默认是传入字符串长度加16。这就是count存在的意义,因为数组中的一部分内容为默认值。
当调用append方法时会对count进行增加,增加值便是append的字符串的长度,具体实现也在抽象父类中。
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
我们所说的线程不安全的发生点便是在append方法中count的“+=”操作。我们知道该操作是线程不安全的,那么便会发生两个线程同时读取到count值为5,执行加1操作之后,都变成6,而不是预期的7。这种情况一旦发生便不会出现预期的结果。所以说StringBuilder是线程不安全的。
扩容
有个ensureCapacityInternal()的方法,追进去看看,然后发现它是这么扩容的 int newCapacity = (value.length << 1) + 2;增加为自身长度的一倍然后再加2;这个时候如果还是放不下,那就直接扩容到它需要的长度 newCapacity = minCapacity;
1.ArrayList是基于索引的数据接口,它的底层是数组。它可以以O(1)时间复杂度对元素进行随机访问。与此对应,LinkedList是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)。
2. 相对于ArrayList,LinkedList的插入,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。
3. LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。
4. ArrayList和LinkedList都实现了List接口,但是LinkedList还额外实现了Deque接口
接口的设计目的,是对类的行为进行约束(更准确的说是一种“有”约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现行为进行限制。
而抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类。在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自己实现。正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A-B时,无法执行)。
抽象类是对类本质的抽象,表达的是 is a的关系,比如:BMw is a car。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。
而接口是对行为的抽象,表达的是like a的关系。比如:Bird like a Aircraft(像飞行器一样可以飞),但其本质上is a Bird。接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现的,接口并不关心。
使用场景:当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。
抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为高级语言来说(从实际设计上来说也是)每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口。在设计阶段会降低难度
1.首先CopyonwriteArrayList内部也是用过数组来实现的,在向CopyonwriteArrayList添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上进行
2.并且,写操作会加锁,防止出现并发写入丢失数据的问题
3.写操作结束之后会把原数组指向新数组
4.CopyOnWriteArrayList允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场景,但是CopyOnWriteArrayList会比较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很高的场景
1.sleep是线程中的方法,而wait是Object中的方法
2.sleep不会释放锁,wait会释放锁
3.sleep方法不依赖于同步器synchronized,但是wait需要依赖于synchronized关键字
4.sleep不需要被唤醒(休眠后推出阻塞),wait需要被唤醒(不指定时间需要被别人中断)
1.final是一个修饰符,可以用来修饰变量、方法和类。如果final修饰变量,意味着该变量的值在初始化后不能被改变。
final来修饰类 方法 属性都表示其值不可变,也就是说类不可继承,方法不可重写,属性不可覆盖。
2.finally是一个关键字,与try和catch一起用于异常的处理。finally块一定会被执行,无论在try块中是否发生异常
3.finalize方法是在对象被回收之前调用的方法,给对象自己最后一次复活的机会,但是什么时候调用finalize没有保证。
finalize的理解
将资源释放和清理放在finalize方法中非常不好,非常影响性能,严重时甚至会引起OOM,从java9开始就被标注为@Deprecated,不建议被使用了
为什么不好?
第一:finalize方法的调用次序并不能保证
第二:finalize中的代码并不能保证被执行
第三:如果finalize中的代码出现异常,会发现根本没有异常输出
第四:重写了finalize方法的对象在第一次被gc时,并不能及时释放它占用的内存,因为要等着FinalizerThread调用完finalize,把它从第一个unfinalized队列移除后,第二次gc时才能真正释放内存。
可以想象gc本就因为内存不足引起,finalize调用又很慢(两个队列的移除操作,都是串行执行的,用来释放连接类的资源也应该不快),不能及时释放内存,对象释放不及时就会逐渐移入老年代,老年代垃圾积累过多就会容易full gc,full gc后释放速度如果仍然跟不上创建新对象的速度,就会出现OOM
1.简单的来讲HashCode就像是一个签名,当两个对象的Hashcode一样的时候,两个对象就可能一样,但如果Hashcode不一样,那么肯定不是同一个对象。相当于先确定一个大的范围,再用equals去比较。
2.hashcode可以减少equals比较的次数,提高运算效率。(equals方法效率比较低)
3.如果对象1和对象2相等,说明他们的散列码相等!反过来就不一样了!。
4.hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等.
1.“==”操作符专门用来比较两个变量的值是否相等,也就是用于比较变量所对应的内存中所存储的数值是否相同,要比较两个基本类型的数据或两个引用变量是否相等,只能用“==”操作符。
2.如果a和b都是对象,则a==b是比较两个对象的引用。equals方法是用于比较两个独立对象的内容是否相同,就好比去比较两个人的长相是否相同,它比较的两个对象是独立的。
3.如果要比较对象中得属性则需要重写equals方法,但是重写了equals方法,不重写hashCode方法时,可能会出现equals方法返回为true,而hashCode方法却返回不同的结果。这是因为不重写hashcode方法调用的object类中的hashcode方法,使得得到的两个哈希值不同。
1.Collection是一个接口,它是set、list等容器的父接口
2.Collections是一个工具类,提供了一系列的静态方法来辅助容器操作,这些方法包括对容器的搜索、排序、线程安全化等等。
Collections.sort底层排序方式
首先先判断需要排序的数据量是否大于60。
小于60:使用插入排序,插入排序是稳定的
大于60的数据量会根据数据类型选择排序方式:
基本类型:使用快速排序。因为基本类型。1、2都是指向同一个常量池不需要考虑稳定性。
Object类型:使用归并排序。因为归并排序具有稳定性。
1.安全性
Hashtable是线程安全,HashMap是非线程安全。
HashMap的性能会高于Hashtable,我们平时使用时若无特殊需求建议使用HashMap,在多线程环境下可以使用Hashtable…
JDK1.7 中,由于多线程对HashMap进行扩容,调用了HashMap#transfer(),具体原因:某个线程执行过程中,被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变,造成死循环、数据丢失。
JDK1.8 中,由于多线程对HashMap进行put操作,调用了HashMap#putVal(),具体原因:假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。
2.是否可以使用null作为key
Hashtable则不允许null 作为key,而HashMap可以使用null作为key,不过建议还是尽量避免这样使用。HashMap以null作为key时,总是存储在table数组的第一个节点上。
3.默认容量及如何扩容
HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。
HashMap扩容时是当前容量翻倍即:capacity(容量)*2,Hoshtoble 扩容时是容量翻倍+1
即:capacity (2)+1.
1:为什么要序列化
网络传输的数据都必须是二进制数据,但是在Java中都是对象,是没有办法在网络中进行传输的,所以就需要对Java对象进行序列化,而且这个要求这个转换算法是可逆的,不然要是不可逆那鬼知道你传过来的是个什么东西
2:Java原生序列化
只要让类实现 Serializable接口就行,序列化具体的实现是由ObjectOutputStream和ObjectlnputStream来实现的
Java序列化的缺点:1:序列化码流太大2:序列化效率低。
一般建议使用第三方的JSON、Hessian、ProtoBuf等效率高的方式
1.接口和抽象类的概念不一样。接口是对动作的抽象,抽象类是对根源的抽象。
2.抽象类表示的是,这个对象是什么。(例如:男人,女人的抽象类是人)
3.接口表示的是,这个对象能做什么。
(例如:“吃东西”定义成一个接口,让人和动物去实现它,因为都可以吃东西)。
4.同时在Java中,一个类只能继承一个抽象类(正如人不可能同时是生物和非生物),但是可以实现多个接口(吃饭接口、走路接口)。
5.当你关注一个事物的本质的时候,用抽象类:当你关注一个操作的时候,用接口。
6.抽象类的功能要远超过接口(因为抽象类中可以定义构造器,可以有抽象方法和具体方法,
7.而接口中不能定义构造器而且其中的方法全部都是抽象方法),但是定义抽象类的代价高(每个类只能继承一个抽象类)
1.Overload是重载的意思,Override是覆盖的意思,也就是重写。
2.重载Overload表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同(即参数个数或类型不同)。
3.重写Override表示子类中的方法可以与父类中的某个方法的名称和参数完全相同,通过子类创建的实例对象调用这个方法时,将调用子类中的定义方法,这相当于把父类中定义的那个完全相同的方法给覆盖了,这也是面向对象编程的多态性的一种表现。
性质:
1.每个结点要么是红的要么是黑的。
2.根结点是黑的。
3.每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。
4.如果一个结点是红的,那么它的两个儿子都是黑的。
5.对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。
时间复杂度:O(logn)
1.开放地址法(再散列法)
开放地执法有一个公式:Hi=(H(key)+di) MOD m i=1,2,…,k(k<=m-1) 其中,m为哈希表的表长。di 是产生冲突的时候的增量序列。如果di值可能为1,2,3,…m-1,称线性探测再散列。如果di取1,则每次冲突之后,向后移动1个位置.如果di取值可能为1,-1,2,-2,4,-4,9,-9,16,-16,…kk,-kk(k<=m/2),称二次探测再散列。如果di取值可能为伪随机数列。称伪随机探测再散列。
2.再哈希法Rehash
当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突时。缺点:计算时间增加。比如上面第一次按照姓首字母进行哈希,如果产生冲突可以按照姓字母首字母第二位进行哈希,再冲突,第三位,直到不冲突为止.这种方法不易产生聚集,但增加了计算时间。
3.链地址法(拉链法)
将所有关键字为同义词的记录存储在同一线性链表中.基本思想:将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。对比JDK 1.7 hashMap的存储结构是不是很好理解。至于1.8之后链表长度大于6rehash 为树形结构不在此处讨论。
4.建立一个公共溢出区
假设哈希函数的值域为[0,m-1],则设向量HashTable[0…m-1]为基本表,另外设立存储空间向量OverTable[0…v]用以存储发生冲突的记录
多态是什么?
多态是同一个行为具有多个不同表现形式或形态的能力。
多态就是同一个接口,使用不同的实例而执行不同操作
多态的优点
1.消除类型之间的耦合关系
2. 可替换性
3. 可扩充性
4. 接口性
5. 灵活性
6. 简化性
多态存在的三个必要条件
1.继承
2.重写
3.父类引用指向子类对象
可替换性(substitutability)。多态对已存在代码具有可替换性。例如,多态对圆Circle类工作,对其他任何圆形几何体,如圆环,也同样工作。
可扩充性(extensibility)。多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。
接口性(interface-ability)。多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。如图8.3 所示。图中超类Shape规定了两个实现多态的接口方法,computeArea()以及computeVolume()。子类,如Circle和Sphere为了实现多态,完善或者覆盖这两个接口方法。
灵活性(flexibility)。它在应用中体现了灵活多样的操作,提高了使用效率。
简化性(simplicity)。多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。
(1)虚拟机遇到一条new指令时,首先检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经加载、连接和初始化。如果没有,就执行该类的加载过程。
(2)为该对象分配内存。
A、假设Java堆是规整的,所有用过的内存放在一边,空闲的内存放在另外一边,中间放着一个指针作为分界点的指示器。那分配内存只是把指针向空闲空间那边挪动与对象大小相等的距离,这种分配称为“指针碰撞”
B、假设Java堆不是规整的,用过的内存和空闲的内存相互交错,那就没办法进行“指针碰撞”。虚拟机通过维护一个列表,记录哪些内存块是可用的,在分配的时候找出一块足够大的空间分配给对象实例,并更新表上的记录。这种分配方式称为“空闲列表“。
C、使用哪种分配方式由Java堆是否规整决定。Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
D、分配对象保证线程安全的做法:虚拟机使用CAS失败重试的方式保证更新操作的原子性。(实际上还有另外一种方案:每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,TLAB。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才进行同步锁定。虚拟机是否使用TLAB,由-XX:+/-UseTLAB参数决定)
(3)虚拟机为分配的内存空间初始化为零值(默认值)
(4)虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到对象的元数据信息、对象的Hash码、对象的GC分代年龄等信息。这些信息存放在对象的对象头中。
(5) 执行方法,把对象按照程序员的意愿进行初始化。
死锁的产生有四个必要的条件
互斥使用,即当资源被一个线程占用时,别的线程不能使用
不可抢占,资源请求者不能强制从资源占有者手中抢夺资源,资源只能由占有者主动释放
请求和保持,当资源请求者在请求其他资源的同时保持对原因资源的占有
循环等待,多个线程存在环路的锁依赖关系而永远等待下去,例如T1占有T2的资源,T2占有T3的资源,T3占有T1的资源,这种情况可能会形成一个等待环路
两种技术乍一看上去有点矛盾的
封装性告诉我们私有的 别的地方不能用,而反射呢告诉我们可以用私有的 这样看不就白封装了吗
两者之间不矛盾
对于封装性我们 设计一个类的时候 属性 方法 构造器等等 该私有的时候私有(private) 该 公共的时候公共(public)
封装性给我们的启示是:当我们看到一个类写了一些私有的方法,一些公共的方法时 就告诉我们私有的方法就不要用了
就用公共的方法就可以了 因为私有的方法可能类内部用了 这里体现了封装性。比如单例模式 你要想造对象 就不要用
私有的构造器了 我已经把对象造好了直接用就行
反射 告诉我们可以调 但是不建议调私有的方法,因为可能公共的方法更好 加了一些逻辑 。
封装性解决的问题是 建议 调那个的问题 公共的调就可以了 私有的不要掉了就 私有的属性不建议你直接修改 建议你通过get set方法修改。
反射解决的是能不能调的问题
所以两者不矛盾。
1.类初始化过程
(1)一个类要参加实例需要先加载并初始化该类
main方法所在类需要先加载和初始化
(2)一个子类要初始化需要先初始化父类
(3)一个类初始化就是执行<clinit>()方法
<clinit>()方法是由静态类变量显示赋值代码和静态代码块组成
类变量显示赋值代码和静态代码块代码从上到下顺序执行(注意细节)
<clinit>()方法只执行一次
2.实例初始化过程
(1)实例初始化就是执行<init>()方法
<init>()方法可能重载有多个,有几个构造器就有几个<init>()方法
<init>()方法由非静态实例变量显示赋值代码和非静态代码块、对应构造器代码组成
非静态实例变量显示赋值代码和静态代码块代码从上到下执行,而对应构造器的代码最后执行(注意细节)
每次创建实例对象,调用对应构造器,执行的就是对应的<init>()方法
<init>()方法的首行是super()或super(实参列表),即对应父类的<init>()方法
3.方法的重写
(1)哪些方法不可以被重写
final方法
静态方法
private等子类中不可见的方法
(2)对象的多态性
子类如果重写了父类的方法,通过子类对象调用的一定是子类重写过得代码
非静态方法默认的调用对象是this
this对象在构造器或则说<init>方法中就是正在创建的对象
1.加载
通过一个类的全限定名来获取定义此类的二进制字节流。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存中生成一个代表这个类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
对于数组类,其本身不同股哦类加载器创建,它是由java虚拟机直接创建的。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型最终靠类加载器去创建。
加载完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法去中,然后再内存中实例化一个java.lang.class类的对象,这个对象将作为程序访问方法区中的这些数据的外部接口。
2.链接
验证 - 验证类是否符合Class规范、合法性、安全性检查
文件格式验证,元数据验证,字节码验证,符号引用验证
准备 - 为static变量分配空间,设置默认值
解析 - 将常量池的符号引用解析为直接引用
符号引用就是一组符号来描述目标,可以是任何字面量,符号引用就是具体指向目标的指针、相对偏移量或一个简介定位到目标的句柄。
3.初始化
合并所有类的静态变量和静态代码块,生成类构造器
4.卸载
1.该类的所有实例都已经被回收,也就是java堆中不存在该类的任何实例
2.加载该类的classloader已经被回收
3.该类对应的java.lang.class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法
符合以上条件,jvm就会在垃圾回收的时候,对类进行卸载,类的卸载过程其实就是在方法区清空类信息,java类的整个生命周期就结束了
类加载器有:
启动类加载器:BootStrapClassLoader
扩展类加载器:ExtensionsClassLoader
应用类加载器:ApplicationClassLoadder
自定义类加载器:
1.继承ClassLoader
2.重写findClass方法,在findClass方法中获取类的字节码,并调用ClassLoader中的defineClass方法来加载类,获取class对象。
注意:如果要打破双亲委派机制 ,需要重写loadClass方法
优点:
解决了哈希表聚堆的现象,减少了平均查找长度。删除结点相对于线性探测更加易于实现,只需要找到相应的节点再删除就可以了
缺点:
拉链法的指针需要额外的空间
集合遍历的时候
failfast;一旦发现遍历的同时其他人来修改,则立刻抛出异常
failsave:发现遍历的同时,其他人来修改,应当有应对的策略,例如牺牲一致性来让整个遍历运行完成
ArrayList是迭代器是failfast
CopyOnWriteArrayList是迭代器是failsave,牺牲了一致性
java对象概括起来分为:对象头、对象体和对齐字节
对象头中有:
MarkWOrd(标记字),主要用来表示对象的线程锁的状态,另外还可以用来配合GC、存放该对象的hashcode
Classword,是一个指向方法区中class信息的指针,因为着该对象可随时知道自己是哪个class的实例
数组长度,占用八个字节,这是可选 的,只有当本对象是一个数组对象时才会有这部分
对象体:
用来保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型
对齐字:
为了减少堆内存的碎片空间
1.基于内存:Redis是使用内存存储,没有磁盘IO上的开销。数据存在内存中,读写速度快。
2.单线程实现( Redis 6.0以前):Redis使用单个线程处理请求,避免了多个线程之间线程切换和锁资源争用的开销。
3.IO多路复用模型:Redis 采用 IO 多路复用技术。Redis 使用单线程来轮询描述符,将数据库的操作都转换成了事件,不在网络I/O上浪费过多的时间。
4.高效的数据结构:Redis 每种数据类型底层都做了优化,目的就是为了追求更快的速度。
1.首先利用setnx来保证:如果key不存在才能获取到锁,如果key存在,则获取不到锁
2.然后还要利用lua脚本来保证多个redis操作的原子性
3.同时还要考虑到锁过期,所以需要额外的一个看门狗定时任务来监听锁是否需要续约
4.同时还要考虑到redis节点挂掉后的情况,所以需要采用红锁的方式来同时向N/2+1个节点申请锁,都申请到了才证明获取锁成功,这样就算其中某个redis节点挂掉了,锁也不能被其他客户端获取到
RDB:Redis DataBase 将某一个时刻的内存快照(Snapshot),以二进制的方式写入磁盘。
手动触发:
1.save命令,使Redis处于阻塞状态,直到RDB持久化完成,才会响应其他客户端发来的命令,所以在生产环境一定要慎用
2.bgsave命令,fork出一个子进程执行持久化,主进程只在fork过程中有短暂的阻塞,子进程创建之后,主进程就可以响应客户端请求了
自动触发:
1.save m n:在m秒内,如果有n个键发生改变,则自动触发持久化,通过bgsave执行,如果设置多个、只要满足其一就会触发,配置文件有默认配置(可以注释掉)
2.flushall:用于清空redis所有的数据库,flushdb清空当前redis所在库数据(默认是0号数据库),会清空RDB文件,同时也会生成dump.rdb、内容为空
3.主从同步+全量同步时会自动触发bgsave命令,生成rdb发送给从节点
优点:
1、整个Redis数据库将只包含一个文件dump.rdb,方便持久化。
2、容灾性好,方便备份。
3、性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以是IO最大化。使用单独子进程来进行持久化,主进程不会进行任何lO操作,保证了redis的高性能
4.相对于数据集大时,比AOF的启动效率更高。
缺点:
1、数据安全性低。RDB是间隔一段时间进行持久化,如果持久化之间redis发生故障,会发生数据丢失。所以这
种方式更适合数据要求不严谨的时候)
2、由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。会占用cpu
AOF:Append Only File 以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录,调操作系统命令进程刷盘
1、所有的写命令会追加到AOF缓冲中。
2、AOF缓冲区根据对应的策略向硬盘进行同步操作。
3、随着AF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。
4、当Redis重启时,可以加载AOF文件进行数据恢复。
同步策略:
1.每秒同步:异步完成,效率非常高,一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失
2.每修改同步:同步持久化,每次发生的数据变化都会被立即记录到磁盘中,最多丢一条
3.不同步:由操作系统控制,可能丢失较多数据
优点:
1、数据安全
2、通过append模式写文件,即使中途服务器宕机也不会破坏已经存在的内容,可以通过redis-check-aof 工具
解决数据一致性问题。
3、AOF机制的rewrite模式。定期对AOF文件进行重写,以达到压缩的目的
缺点:
1、AOF文件比RDB文件大,且恢复速度慢。
2、数据集大的时候,比rdb启动效率低。
3、运行效率没有RDB高
两者相比:
AOF文件比RDB更新频率高,优先使用AOF还原数据。
AOF比RDB更安全也更大
RDB性能比AOF好
如果两个都配了优先加载AOF
Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。
惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)
Redis中同时使用了惰性过期和定期过期两种过期策略。
缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
1.缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
2.给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存。
3.缓存预热
4.互斥锁
缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
1.接口层增加校验,如用户鉴权校验,id做基础校验,id<=o的直接拦截;
2.从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
3.采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案
1.设置热点数据永远不过期。
2.加互斥锁
通过执行slaveof命令或设置slaveof选项,让一个服务器去复制另一个服务器的数据。主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库。而从数据库一般是只读的,并接受主数据库同步过来的数据。一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。
全量复制:
(1)主节点通过bgsave命令fork子进程进行RDB持久化,该过程是非常消耗CPU、内存(页表复制)、硬盘IO的
(2)主节点通过网络将RDB文件发送给从节点,对主从节点的带宽都会带来很大的消耗
(3)从节点清空老数据、载入新RDB文件的过程是阻塞的,无法响应客户端的命令;如果从节点执行bgrewriteaof,也会带来额外的消耗
部分复制:
1.复制偏移量:执行复制的双方,主从节点,分别会维护一个复制偏移量offset
2.复制积压缓冲区:主节点内部维护了一个固定长度的、先进先出(FIFO)队列作为复制积压缓冲区,当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。
3.服务器运行ID(runid):每个Redis节点,都有其运行ID,运行ID由节点在启动时自动生成,主节点会将自己的运行ID发送给从节点,从节点会将主节点的运行ID存起来。从节点Redis断开重连的时候,就是根据运行ID来
判断同步的进度:
如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况);
如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。
String:字符串
List:列表
Hash:哈希表
Set:无序集合
Sorted Set:有序集合
bitmap:布隆过滤器
GeoHash:坐标,借助Sorted Set实现,通过zset的score进行排序就可以得到坐标附近的其它元素,通过将score还原成坐标值就可以得到元素的原始坐标
HyperLogLog:统计不重复数据,用于大数据基数统计
Streams:内存版的kafka
由于缓存和数据库是分开的,无法做到原子性的同时进行数据修改,可能出现缓存更新失败,或者数据库更新失败的情况,这时候会出现数据不一致,影响前端业务
1.先更新数据库,再更新缓存。缓存可能更新失败,读到老数据
2.先删缓存,再更新数据库。并发时,读操作可能还是会将旧数据读回缓存
3.先更新数据库,再删缓存。也存在缓存删除失败的可能
最经典的缓存+数据库读写的模式,Cache Aside Patten。
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
更新的时候,先更新数据库,然后再删除缓存。
为什么是删除而不是更新?
删除更加轻量,延迟加载的一种实现,更新可能涉及多个表、比较耗时
延时双删:先删除缓存,再更新数据库,休眠1s、再次删除缓存。写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据,并发还是可能读到旧值覆盖缓存
终极方案:
将访问操作串行化
1.先删缓存,将更新数据库的操作放进有序队列中
2.从缓存查不到的查询操作,都进入有序队列
会面临的问题:
1.读请求积压,大量超时,导致数据库的压力:限流、熔断
2.如何避免大量请求积压:将队列水平拆分,提高并行度。
3.保证相同请求路由正确
Redis的数据结构有:
1.字符串:可以用来做最简单的数据,可以缓存某个简单的字符串,也可以缓存某个json格式的字符串,Redis分布式锁的实现就利用了这种数据结构,还包括可以实现计数器、Session共享、分布式ID
2.哈希表:可以用来存储一些key-value对,更适合用来存储对象
3.列表:Redis的列表通过命令的组合,既可以当做栈,也可以当做队列来使用,可以用来缓存类似微信公众号、微博等消息流数据
4.集合:和列表类以,也可以存储多个元素,但是不能重复,集合可以进行交集、并集、差集操作,从而可以实现类似,我和某人共同关注的人、朋友圈点赞等功能
5.有序集合:集合是无序的,有序集合可以设置顺序,可以用来实现排行榜功能
主从
哨兵模式:
sentinel,哨兵是redis集群中非常重要的一个组件,主要有以下功能:
哨兵用于实现redis集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
主从切换
是指某个master不可用时,它的其中一个从节点升级为master的操作。
通过投票机制来判断master是否不可用,参与投票的是所有的master,所有的master之间维持着心跳,如果一半以上的master确定某个master失联,则集群认为该master挂掉,此时发生主从切换。
通过选举机制来确定哪一个从节点升级为master。选举的依据依次是:网络连接正常->5秒内回复过INFO命令->10*down-after-milliseconds内与主连接过的->从服务器优先级->复制偏移量->运行id较小的。选出之后通过slaveif no ont将该从服务器升为新master。
通过配置文件参数【cluster-node-timeout】指定心跳超时时间,默认是15秒。
Redis Cluster是一种服务端Sharding技术,3.0版本开始正式提供。采用slot(槽)的概念,一共分成16384个槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行
方案说明
在redis cluster架构下,每个redis 要放开两个端口号,比如一个是6379,另外一个就是加1w的端口号,比如16379。
16379端口号是用来进行节点间通信的,也就是cluster bus的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus用了另外一种二进制的协议,gossip协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。
优点
缺点
Redis Sharding是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方法。其主要思想是采用哈希算法将Redis数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。Java redis客户端驱动jedis,支持Redis Sharding功能,即Shardedjedis以及结合缓存池的ShardedjedisPool
优点
优势在于非常简单,服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强
缺点
由于sharding处理放到客户端,规模进一步扩大时给运维带来挑战。
客户端sharding不支持动态增删节点。服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。连接不能共享,当应用规模增大时,资源浪费制约优化
家所熟知的 Redis 确实是单线程模型,指的是执行 Redis 命令的核心模块是单线程的,而不是整个 Redis 实例就一个线程,Redis 其他模块还有各自模块的线程的。
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。
因为文件事件分派器队列的消费是单线程的`,所以Redis才叫单线程模型。
Redis 不仅仅是单线程
一般来说 Redis 的瓶颈并不在 CPU,而在内存和网络。如果要使用 CPU 多核,可以搭建多个 Redis 实例来解决。
其实,Redis 4.0 开始就有多线程的概念了,比如 Redis 通过多线程方式在后台删除对象、以及通过 Redis 模块实现的阻塞命令等。
内存不够的话,可以加内存或者做数据结构优化和其他优化等 但网络的性能优化才是大头,网络 IO 的读写在 Redis 整个执行期间占用了大部分的 CPU 时间,如果把网络处理这部分做成多线程处理方式,那对整个 Redis 的性能会有很大的提升。Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型
1、单reactor单线程,前台接待员、服务员时同一个人,全程为顾客服务。
2、单reactor多线程,1个前台接待,多个服务员,接待员只负责接待。
3、主从reactor多线程,多个前台接待,多个服务员。
数据倾斜
如果节点的数量很少,而hash环空间很大(一般是 0 ~ 2^32),直接进行一致性hash上去,大部分情况下节点在环上的位置会很不均匀,挤在某个很小的区域。最终对分布式缓存造成的影响就是,集群的每个实例上储存的缓存数据量不一致,会发生严重的数据倾斜。
缓存雪崩
如果每个节点在环上只有一个节点,那么可以想象,当某一集群从环中消失时,它原本所负责的任务将全部交由顺时针方向的下一个集群处理。例如,当group0退出时,它原本所负责的缓存将全部交给group1处理。这就意味着group1的访问压力会瞬间增大。设想一下,如果group1因为压力过大而崩溃,那么更大的压力又会向group2压过去,最终服务压力就像滚雪球一样越滚越大,最终导致雪崩。
引入虚拟节点
解决上述两个问题最好的办法就是扩展整个环上的节点数量,因此我们引入了虚拟节点的概念。一个实际节点将会映射多个虚拟节点,这样Hash环上的空间分割就会变得均匀。
同时,引入虚拟节点还会使得节点在Hash环上的顺序随机化,这意味着当一个真实节点失效退出后,它原来所承载的压力将会均匀地分散到其他节点上去。
Redis 是一个服务,独立的进程,用户的程序需要与它建立连接才能向它发请求,读写数据。
RocksDB 和LevelDB 是一个库,嵌入在用户的程序中,用户程序直接调用接口读写数据。
LevelDB 具体是什么?
它是一个 NOSQL 存储引擎,它和 Redis 不是一个概念。Redis 是一个完备的数据库,而 LevelDB 它只是一个引擎。如果将数据库必须成一辆高级跑车,那么存储引擎就是它的发动机,是核心是心脏。有了这个发动机,我们再给它包装上一系列的配件和装饰,就可以成为数据库。不过也不要小瞧了配件和装饰,做到极致那也是非常困难,将 LevelDB 包装成一个简单易用的数据库需要加上太多太多精致的配件。LevelDB 和 RocksDB 出来这么多年,能够在它的基础上做出非常一个完备的生产级数据库寥寥无几。
在使用 LevelDB 时,我们还可以将它看成一个 Key/Value 内存数据库。它提供了基础的 Get/Set API,我们在代码里可以通过这个 API 来读写数据。你还可以将它看成一个无限大小的高级 HashMap,我们可以往里面塞入无限条 Key/Value 数据,只要磁盘可以装下。
正是因为它只能算作一个内存数据库,它里面装的数据无法跨进程跨机器共享。在分布式领域,LevelDB 要如何大显身手呢?
这就需要靠包装技术了,在 LevelDB 内存数据库的基础上包装一层网络 API。当不同机器上不同的进程要来访问它时,都统一走网络 API 接口。这样就形成了一个简易的数据库。如果在网络层我们使用 Redis 协议来包装,那么使用 Redis 的客户端就可以读写这个数据库了。
如果要考虑数据库的高可用性,我们在上面这个单机数据库的基础上再加上主从复制功能就可以变身成为一个主从结构的分布式 NOSQL 数据库。在主从数据库前面加一层转发代理(负载均衡器如 LVS、F5 等),就可以实现主从的实时切换。
如果你需要的数据容量特别大以至于单个机器的硬盘都容不下,这时候就需要数据分片机制将整个数据库的数据分散到多台机器上,每台机器只负责一部分数据的读写工作。数据分片的方案非常多,可以像 Codis 那样通过转发代理来分片,也可以像 Redis-Cluster 那样使用客户端转发机制来分片,还可以使用 TiDB 的 Raft 分布式一致性算法来分组管理分片。最简单最易于理解的还是要数 Codis 的转发代理分片。
如果要考虑数据库的高可用性,我们在上面这个单机数据库的基础上再加上主从复制功能就可以变身成为一个主从结构的分布式 NOSQL 数据库。在主从数据库前面加一层转发代理(负载均衡器如 LVS、F5 等),就可以实现主从的实时切换。
如果你需要的数据容量特别大以至于单个机器的硬盘都容不下,这时候就需要数据分片机制将整个数据库的数据分散到多台机器上,每台机器只负责一部分数据的读写工作。数据分片的方案非常多,可以像 Codis 那样通过转发代理来分片,也可以像 Redis-Cluster 那样使用客户端转发机制来分片,还可以使用 TiDB 的 Raft 分布式一致性算法来分组管理分片。最简单最易于理解的还是要数 Codis 的转发代理分片。
索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。
素引的原理:就是把无序的数据变成有序的查询
1.把创建了索引的列的内容讲行排序
2.对排序结果生成倒排表
3.在倒排表内容上拼上数据地址链
4.在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据
关心过业务系统里面的sql耗时吗?统计过慢查询吗?对慢查询都怎么优化过?
在业务系统中,除了使用主键进行查询,其他的都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。
慢查询的优化首先要搞明白慢的原因是什么?是查询条件没有命中索引?是load了不需要的数据列?还是数据量太大?
所以优化也是针对这三个方向来的,
1.首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。
2.分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。
3.如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表。
数据库的 事务(Transaction) 是一种机制、一个操作序列,包含了一组数据库操作命令。
事务基本特性ACID分别是:
原子性指的是一个事务中的操作要么全部成功,要么全部失败。
一致性指的是数据库总是从一个一致性的状态转换到另外一个一致性的状态。比如A转账给B100块钱,假设A只有90块,支付之前我们数据库里的数据都是符合约束的,但是如果事务执行成功了,我们的数据库数据就破坏约束了,因此事务不能成功,这里我们说事务提供了一致性的保证
隔离性指的是一个事务的修改在最终提交前,对其他事务是不可见的。
持久性指的是一旦事务提交,所做的修改就会永久保存到数据库中。
隔离性有4个隔离级别,分别是:
1.read uncommit读未提交,可能会读到其他事务未提交的数据,也叫做脏读。
用户本来应该读取到id=1的用户age应该是10,结果读取到了其他事务还没有提交的事务,结果读取结果age=20,这就是脏读。
2.read commit 读已提交,两次读取结果不一致,叫做不可重复读。不可重复读解决了脏读的问题,他只会读取已经提交的事务。用户开启事务读取id=1用户,查询到age=10,再次读取发现结果=20,在同一个事务里同一个查询读取到不同的结果叫做不可重复读。
3.repeatable read可重复复读,这是mysql的默认级别,就是每次读取结果都一样,但是有可能产生幻读。(MySQL默认的隔离级别)
4.serializable串行,一般是不会使用的,他会给每一行读取的数据加锁,会导致大量超时和锁竞争的问题。
当一个SQL想要利用索引时,就一定要提供该索引所对应的字段中最左边的字段,也就是排在最前面的字段,比如针对a,b,c三个字段建立了一个联合索引,那么在写一个sql时就一定要提供a字段的条件,这样才能用到联合索引,这是由于在建立a,b,c三个字段的联合索引时,底层的B+树是按照a,b,c三个字段从左往右去比较大小进行排序的,所以如果想要利用B+树进行快速查找也得符合这个规则
多版本并发控制:读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务session会看到自己特定版本的数据,版本链
MVCC只在READ COMMITTED和REPEATABLE READ两个隔离级别下工作。其他两个隔离级别够和MVCC不兼容,因为READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。
聚簇索引记录中有两个必要的隐藏列:
trx_id:用来存储每次对某条聚簇索引记录进行修改的时候的事务id。
roll_pointer:每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本)
已提交读和可重复读的区别就在于它们生成ReadView的策略不同。
开始事务时创建readview,readView维护当前活动的事务id,即未提交的事务id,排序生成一个数组
访问数据,获取数据中的事务id(获取的是事务id最大的记录),对比readview:
如果在readview的左边(比readview都小),可以访问(在左边意味着该事务已经提交)
如果在readview的右边(比readview都大)或者就在readview中,不可以访问,获取roll_pointer,取上一版本重新对比(在右边意味着,该事务在readview生成之后出现,在readview中意味着该事务还未提交)
已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。
这就是Mysql的MVCC,通过版本链,实现多版本,可并发读-写,写-读。通过ReadView生成策略的不同实现不同的隔离级别。
1.所有键值分布在整颗树中(索引值和具体data都在每个节点里);
2.任何一个关键字出现且只出现在一个结点中;
3.搜索有可能在非叶子结点结束(最好情况O(1)就能找到数据);
4.在关键字全集内做一次查找,性能逼近二分查找;
B+树是B-树的变体,拥有B树的特点,也是一种多路搜索树, 它与 B- 树的不同之处在于:
所有关键字存储在叶子节点出现,内部节点(非叶子节点并不存储真正的 data)
为所有叶子结点增加了一个链指针
Mysql索引使用的是B+树,因为索引是用来加快查询的,而B+树通过对数据进行排序所以是可以提高查询速度的,然后通过一个节点中可以存储多个元素,从而可以使得B+树的高度不会太高,在MysqI中一个Innodb页就是一个B+树节点,一个Innodb页默认16kb,所以一般情况下一颗两层的B+树可以存2000行左右的数据,然后通过利用B+树叶子节点存储了所有数据并且进行了排序,并且叶子节点之间有指针,可以很好的支持全表扫描,范围查找等SQL语句。
1.检查是否走了索引,如果没有则优化SQL利用索引
2.检查所利用的索引,是否是最优索引
3.检查所查字段是否都是必须的,是否查询了过多字段,查出了多余数据
4.检查表中数据是否过多,是否应该进行分库分表了
5.检查数据库实例所在机器的性能配置,是否太低,是否可以适当增加资源
MyISAM:
不支持事务,但是每次查询都是原子的:
支持表级锁,即每次操作暴对整个表加锁;
存储表的总行数:
一个MYISAM表有三个文件:索引文件、表结构文件、数据文件:
采用非聚集索引,索引文件的数据城存储指向数据文件的指针。辅索引与主索引基本一致,但是辅索引不用保证唯一性。
InnoDb:
支持ACID的事务,支持事务的四种隔离级别;
支持行级锁及外键约束:因此可以支持写并发;
不存储总行数:
一个innoDb引学存储在一个文件空问(共享表空问,表大小不受操作系统控制,一个表可能分布在多个文件里),也有可能为多个(设置为独立表空,表大小受操作系统文件大小限制,一般为2G),受操作系统文件大小的限制;
InnoDB数据引擎使用B+树构造索引结构,其中的索引类型依据参与检索的字段不同可以分为主索引和非主索引;依据B+树叶子节点上真实数据的组织情况又可以分为聚族索引和非聚族索引。每一个索引B+树结构都会有一个独立的存储区域来存放,并且在需要进行检索时将这个结构加载到内存区域。真实情况是InnoDB引擎会加载索引B+树结构到内存的Buffer Pool区域。
mysql主从同步的过程:
Mysql的主从复制中主要有三个线程:master (binlog dump thread)、s1ave (I/o thread、SQL thread),Master一条线程和Slave中的两条线程。
1.主节点binlog,主从复制的基础是主库记录数据库的所有变更记录到binlog。binlog是数据库服务器启动的那一刻起,保存所有修改数据库结构或内容的一个文件。
2.主节点log dump线程,当binlog有变动时,log dump线程读取其内容并发送给从节点。
3.从节点I/o线程接收binlog内容,并将其写入到relay log文件中。
4.从节点的SQL线程读取relay log文件内容对数据更新进行重放,最终保证主从数据库的一致性。
注:主从节点使用binglog文件+position偏移量来定位主从同步的位置,从节点会保存其已接收到的偏移量,如果从节点发生宕机重启,则会自动从position的位置发起同步。由于mysql默认的复制方式是异步的,主库把日志发送给从库后不关心从库是否已经处理,这样会产生一个问题就是假设主库挂了,从库处理失败了,这时候从库升为主库后,日志就丢失了。由此产生两个概念。
全同步复制
主库写入binlog后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响。
半同步复制
和全同步不同的是,半同步复制的逻辑是这样,从库写入日志成功返回ACK确认给主库,主库收到至少一个从库的确认就认为写操作完成。
基于锁的属性分类:共享锁、排他锁。
基于锁的粒度分类:行级锁(INNODB)、表级锁(INNODB、MYISAM)、页级锁(BDB引擎)、记录锁、间隙锁、临键锁。
基于锁的状态分类:意向共享锁、意向排它锁。
共享锁(Share Lock)
共享锁又称读锁,简称s锁;当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其进行加持写锁。共享锁的特性主要是为了支持并发的读取数据,读取数据的时候不支持修改,避免出现重复读的问题。
排他锁(eXclusive Lock)
排他锁又称写锁,简称x锁;当一个事务为数据加上写锁时,其他请求将不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。排他锁的目的是在数据修改时候,不允许其他人同时修改,也不允许其他人读取。避免了出现脏数据和脏读的问题。
表锁
表锁是指上锁的时候锁住的是整个表,当下一个事务访问该表的时候,必须等前一个事务释放了锁才能进行对表进行访问;
行锁
行锁是指上锁的时候锁住的是表的某一行或多行记录,其他事务访问同一张表时,只有被锁住的记录不能访问,其他的记录可正常访问;
特点:粒度小,加锁比表锁麻烦,不容易冲突,相比表锁支持的并发要高;
锁定一行:
select * from table where a=8 for updatee;
记录锁(Record Lock)
记录锁也属于行锁中的一种,只不过记录锁的范围只是表中的某一条记录,记录锁是说事务在加锁后锁住的只是表的某一条记录。精准条件命中,并且命中的条件字段是唯一索引
加了记录锁之后数据可以避免数据在查询的时候被修改的重复读问题,也避免了在修改的事务未提交前被其他事务读取的脏读问题。
页锁
页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。
特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
间隙锁(Gap Lock)
属于行锁中的一种,间隙锁是在事务加锁后其锁住的是表记录的某一个区间,当表的相邻ID之间出现空隙则会形成一个区间,遵循左开右闭原则。
范围查询并且查询未命中记录,查询条件必须命中索引、间隙锁只会出现在REPEATABLE_READ(重复读)的事务级别中。
触发条件:防止幻读问题,事务并发的时候,如果没有间隙锁,就会发生如下图的问题,在同一个事务里,A事务的两次查询出的结果会不一样。
比如表里面的数据ID为1,4,5,7,10,那么会形成以下几个间隙区间,-n-1区间,1-4区间,7-10区间,10-n区间(-n代表负无穷大,n代表正无穷大)
什么是间隙锁:
当我们用范围条件查找而不是相等条件检索数据,并请求共享和排它锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,Innodb也会对这个间隙加锁,这种锁 机制就是所谓的间隙锁(Next-Key)
危害:
因为query执行过程中通过范围查找的话,他会锁定整个范围内所有的索引键值,即使这个键值并不存在。
间隙锁有一个比较致命的弱点,就是当锁定一个范围的键值之后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定范围内的任何数据。在某些场景下这可能会对性能造成很大的危害
临建锁(Next-Key Lock)
也属于行锁的一种,并且它是INNODB的行锁默认算法,总结来说它就是记录锁和间隙锁的组合,临键锁会把查询出来的记录锁住,同时也会把该范围查询内的所有间隙空间也会锁住,再之它会把相邻的下一个区间也会锁住
触发条件:范围查询并命中,查询命中了索引。
结合记录锁和间隙锁的特性,临键锁避免了在范围查询时出现脏读、重复读、幻读问题。加了临键锁之后,在范围区间内数据不允许被修改和插入。
如果当事务A加锁成功之后就设置一个状态告诉后面的人,已经有人对表里的行加了一个排他锁了,你们不能对整个表加共享锁或排它锁了,那么后面需要对整个表加锁的人只需要获取这个状态就知道自己是不是可以对表加锁,避免了对整个索引树的每个节点扫描是否加锁,而这个状态就是意向锁。
意向共享锁
当一个事务试图对整个表进行加共享锁之前,首先需要获得这个表的意向共享锁。
意向排他锁
当一个事务试图对整个表进行加排它锁之前,首先需要获得这个表的意向排它锁。
索引是建立在数据库表中的某些列的上面。在创建索引的时候,应该考虑在哪些列上可以创建索引,在哪些列上不能创建索引。一般来说,应该在这些列上创建索引:
1、在经常需要搜索的列上,可以加快搜索的速度;
2、在作为 主键的列上,强制该列的唯一性和组织表中数据的排列结构;
3、在经常用在连接的列上,这些列主要是一些 外键,可以加快连接的速度;在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的;
4、在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;
5、在经常使用在WHERE子句中的列上面创建 索引,加快条件的判断速度
数据库设计时就要考虑到效率和优化问题
一开始就要分析哪些表会存储较多的数据量,对于数据量较大的表的设计往往是粗粒度的,也会冗余一些必要的字段,已达到尽量用最少的表、最弱的表关系去存储海量的数据。 并且在设计表时,一般都会对主键建立聚集索引,含有大数据量的表更是要建立索引以提供查询性能。对于含有计算、数据交互、统计这类需求时,还要考虑是否有必要采用存储 过程。
1 第一范式(确保每列保持原子性)
2 第二范式(确保表中的每列都和主键相关)
3 第三范式(确保每列都和主键列直接相关,而不是间接相关)
二级索引:叶子节点中存储主键值,每次查找数据时,根据索引找到叶子节点中的主键值,根据主键值再到聚簇索引中得到完整的一行记录。
那么InnoDB有了聚簇索引,为什么还要有二级索引呢?
聚簇索引的叶子节点存储了一行完整的数据,而二级索引只存储了主键值,相比于聚簇索引,占用的空间要少。当我们需要为表建立多个索引时,如果都是聚簇索引,那将占用大量内存空间,所以InnoDB中主键所建立的是聚簇索引,而唯一索引、普通索引、前缀索引等都是二级索引。
普通索引、唯一索引、主键索引、组合索引、全文索引
1、应用范畴不同:
主键属于索引的一种。在数据库关系图中为表定义主键将自动创建主键索引,主键索引是唯一索引的特定类型。该索引要求主键中的每个值都唯一。当在查询中使用主键索引时,它还允许对数据的快速访问。
2、种类不同:
根据数据库的功能,可以在数据库设计器中创建三种索引:唯一索引、主键索引和聚集索引。而,主键只是其中的一种。
3、创建方式不同:
当创建或更改表时可通过定义 PRIMARY KEY 约束来创建主键。一个表只能有一个 PRIMARY KEY 约束,而且 PRIMARY KEY 约束中的列不能接受空值。
由于 PRIMARY KEY 约束确保唯一数据,所以经常用来定义标识列。经常在WHERE子句中的列上面创建索引。
主键一定是唯一性索引,唯一性索引并不一定就是主键。
一个表中可以有多个唯一性索引,但只能有一个主键。
主键列不允许空值,而唯一性索引列允许空值。
乐观锁和悲观锁是两种思想。
悲观锁是基于一种悲观的态度类来防止一切数据冲突,它是以一种预防的姿态在修改数据之前把数据锁住,然后再对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作,直到前面一个人把锁释放后下一个人数据加锁才可对数据进行加锁,然后才可以对数据进行操作,一般数据库本身锁的机制都是基于悲观锁的机制实现的;
特点:可以完全保证数据的独占性和正确性,因为每次请求都会先对数据进行加锁, 然后进行数据操作,最后再解锁,而加锁释放锁的过程会造成消耗,所以性能不高;
应用场景:数据库中的行锁,表锁,读锁,写锁
乐观锁是对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁(这使得多个任务可以并行的对数据进行操作),只有到数据提交的时候才通过一种机制来验证数据是否存在冲突(一般实现方式是通过加版本号然后进行版本号的对比方式实现);
特点:乐观锁是一种并发类型的锁,其本身不对数据进行加锁通而是通过业务实现锁的功能,不对数据进行加锁就意味着允许多个请求同时访问数据,同时也省掉了对数据加锁和解锁的过程,这种方式因为节省了悲观锁加锁的操作,所以可以一定程度的的提高操作的性能,不过在并发非常高的情况下,会导致大量的请求冲突,冲突导致大部分操作无功而返而浪费资源,所以在高并发的场景下,乐观锁的性能却反而不如悲观锁。
适用于读多写少的场景
实现方法:cas,版本号机制
版本号机制就是给表中的数据加上一个版本号,如果进行了一次修改则版本号+1,如果再有其他线程来修改,发现版本号不一致,则修改失败
聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据
非聚簇索引:将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因
1、堆区和方法区是所有线程共享的,栈、本地方法栈、程序计数器是每个线程独有的
2、什么是gc root,JVM在进行垃圾回收时,需要找到“垃圾”对象,也就是没有被引用的对象,但是直接找“垃圾”对象是比较耗时的,所以反过来,先找“非垃圾”对象,也就是正常对象,那么就需要从某些“根”开始去找,根据这些“根”的引用路径找到正常对象,而这些“根”有一个特征,就是它只会引用其他对象,而不会被其他对象引用,例如:栈中的本地变量、方法区中的静态变量、本地方法栈中的变量、正在运行的线程等可以作为gc root。
对于还在正常运行的系统:
1.可以使用jmap来查看VM中各个区域的使用情况
2.可以通过jstack来查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁
3.可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc比较频繁,那么就得进行调优了
4.通过各个命令的结果,或者jvisualvm等工具来进行分析
5.首先,初步猜测频繁发送fullgc的原因,如果频繁发生fullgc但是又一直没有出现内存溢出,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入到了老年代,尝试加大年轻代的大小,如果改完之后,fullgc减少,则证明修改有效
6.同时,还可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存
对于已经发生了OOM的系统:
1.一般生产系统中都会设置当系统发生了OOM时,生成当时的dump文件(-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/usr/local/base)
2.我们可以利用jsisualvm等工具来分析dump文件
3.根据dump文件找到异常的实例对象,和异常的线程(占用CPU高),定位到具体的代码
4.然后再进行详细的分析和调试
总之,调优不是一蹴而就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题
引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以靣收,
可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。
引用计数法,可能会出现A引用了B,B又引用了A,这时候就算他们都不再使用了,但因为相互引用计数器=1永远无法被回收。
GC Roots的对象有:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象
可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是在由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法。
当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活
每个对象只能触发一次finalize(0方法
由于finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用,建议遗忘它。
JVM中存在三个默认的类加载器:
JVM在加载一个类时,会调用AppClassLoader的loadClass方法来加载这个类,不过在这个方法中,会先使用ExtClassLoader的loadClass方法来加载类,同样ExtClassLoader的loadClass方法中会先使用BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接成功,如果BootstrapClassLoader没有加载到,那么ExtClassLoader就会自己尝试加载该类,如果没有加载到,那么则会由AppClassLoader来加载这个类。
所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap进行加载,如果没加载到才由自己进行加载。
1.年轻代
a.Eden区 (8)
b. From Survivor区(1)
c. To Survivor区 (1)
2.老年代
默认对象的年龄达到15后,就会进入老年代
java中的编译器和解释器:
Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。
编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就是解释了Java的编译与解释并存的特点。
Java源代码—>编译器—>jvm可执行的Java字节码(即虚拟指令)—>jvm---->jvm中解释器—>机器可执行的二进制机器码---->程序运行。
采用字节码的好处:
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
JVM调优主要就是通过定制JVM运行参数来提高JAVA应用程度的运行数据
JVM参数大致可以分为三类:
1、标注指令:-开头,这些是所有的HotSpot都支持的参数。可以用java -help 打印出来。
2、非标准指令:-X开头,这些指令通常是跟特定的HotSpot版本对应的。可以用java-X打印出来。
3、不稳定参数:-XX开头,这一类参数是跟特定HotSpot版本对应的,并且变化非常大。详细的文档资料非常少。JDK1.8版本下,有几个常用的不稳定指令:
java-XX:+PrintCommandLineFlags:查看当前命令的不稳定指令。
java-XX:+PrintFlagslnitial:查看所有不稳定指令的默认值。
1.内存泄漏(Memory Leak)是指本来无用的对象却继续占用内存,没有再恰当的时机释放占用的内存。不使用的内存,却没有被释放,这个就叫做内存泄漏。
2.比较典型的场景是:每一个请求进来,或者每一次操作处理,都分配了内存,却有一部分不能回收(或未释放),那么随着处理的请求越来越多,内存泄漏也就越来越严重。
3.与内存溢出的关系!
如果存在严重的内存泄漏问题,随着时间的推移,则必然会引起内存溢出。
内存泄漏一般是资源管理问题和程序BUG,内存溢出则是内存空间不足和内存泄漏的最终结果。
由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen
元空间是方法区的在HotSpot jvm 中的实现,方法区主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。,理论上取决于32位/64位系统可虚拟的内存大小。可见也不是无限制的,需要配置参数。
指的是在gc发生的过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何相应。
怎么样避免stw呢?
1.对象不用时最好显式设置为null
2.尽量少用System.gc()
3.尽量少用静态变量
4.尽量使用StringBuffer,而不用String来累加字符串
5.分散对象创建或则删除的时间
6.尽量少用finalize函数
7.使用软引用类型
强引用:平时经常new的对象就是强引用,垃圾回收器不会回收掉被引用的对象
软引用(SoftReference):在发生内存溢出之前,会将这些对象列进回收范围之中进行二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常
弱引用(WeakReference):系统gc时候,只要发现弱引用,不管系统堆空间是否充足都会回收掉。
虚引用([PhantomReference):随时都可能被垃圾回收,虚引用必须和引用队列一起使用(引引用队列只是为了存放被GC回收的对象的引用型对象)。
原子性
原子性是指在一个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。就好比转账,从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元,2个操作必须全部完成。
private long count = 0;
public void calc(){
count++;
}
1:将count从主存读到工作内存中的副本中
2:+1的运算
3:将结果写入工作内存
4:将工作内存的值刷回主存(什么时候刷入由操作系统决定,不确定的)
那程序中原子性指的是最小的操作单元,比如自增操作,它本身其实并不是原子性操作,分了3步的,包括读取变量的原始值、进行加1操作、写入工作内存。所以在多线程中,有可能一个线程还没自增完,可能才执行到第二部,另一个线程就已经读取了值,导致结果错误。那如果我们能保证自增慢作是一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据。
关键字:synchronized
可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。
//线程1
boolean stop = false;
while(!stop){
dosomething();
}
//线程2
stop = true;
如果线程2改变了stop的值,线程1一定会停止吗?不一定。当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
关键字:volatile、synchronized、final
有序性
虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题。
int a = 0;
bool flag = false;
public void write() {
a = 2;//1
flag = true;//2
}
public void multiply() {
if (flag){ //3
int ret = a * a;//4
}
}
write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果,再到线程1,这时候a才赋值为2,很明显迟了一步
关键字:volatile、synchronized
volatile本身就包含了禁止指令重排序的语义,而synchronized关键字是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则明确的。
synchronized关键字同时满足以上三种特性,但是volatile关键字不满足原子性。
在某些情况下,volatile的同步机制的性能确实要优于锁(使用synchronized关键字或java.util.concurrent包里面的锁)因为volatile的总开销要比锁低
内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
强引用:使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使VM在合适的时间就会回收该对象。
弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用
java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。
ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,
value为线程变量的副本
hreadLocalMap使用ThreadLocal的弱引用作为key,如果一I个ThreadLocal不存在外部强引用时,
Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null,而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链(红色链条)
key使用强引用
当hreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
key使用弱引用
当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocalt也会被回收。当key为nul,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal正确的使用方法
1.每次使用完ThreadLkal都调用它的remove()方法清除数据
2.将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。
每一个Thread对象均含有一个ThreadLocalMap类型的成员变量threadLocals,它存储本线程中所有
ThreadLocal对象及其对应的值
ThreadLocalMap由一个个Entry对象构成
Entry继承自weakReference
见,Entry的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该key就会被垃圾收集器回收
当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。
get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。
由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。
使用场景:
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。
Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的
connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离
1.ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
2.ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
3.如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMapt也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清楚Entry对象
4.ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)
首先不管是公平锁和非公平锁,它们的底层实现都会使用AQS来进行排队,它们的区别在于:线程在使用lockO方法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队,如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。
不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段。
另外,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。
1.对于加了volatile关键字的成员变量,在对这个变量进行修改时,会直接将CPU高级缓存中的数据写回到主内存,对这个变量的读取也会直接从主内存中读取,从而保证了可见性
2.在对volatile修饰的成员变量进行读写时,会插入内存屏障,而内存屏障可以达到禁止重排序的效果,从而可以保证有序性
1.如果使用的无界队列,那么可以继续提交任务是没关系的
2.如果使用的有界队列,提交任务时,如果队列满了,如果核心线程数没有达到上限,那么则增加线程,如果线程数已经达到了最大值,则使用拒绝策略进行拒绝
1.偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了
2.轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程
3.如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
4.自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。
线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时:
1.如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
2.如果此时线程池中的数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
3.如果此时线程池中的数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
4.如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过handler所指定的策略来处理此任务。
5.当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数
FixedThreadPool代表定长线程池,底层用的LinkedBlockingQueue,表示无界的阻塞队列。
因为线程的每次创建和消费都会消耗较长的时间,因此在需要大量使用线程的时候,线程池相比于每次都new线程对象来说,能够节约很长的时间,提升并发的效率。
线程和进程的区别:进程是操作系统进行资源分配的最小单元。线程是操作系统进行任务分配的最小单元,线程隶属于进程。
如何开启线程?
1、继承Thread类,重写run方法。
2、实现Runnable接口,实现run方法。
3、实现Callable接口,实现call方法。通过FutureTask创建一个线程,获取到线程执行的返回值。
4、通过线程池来开启线程。
怎么保证线程安全?
加锁:1、JVM提供的锁,也就是Synchronized关键字。2、JDK提供的各种锁Lock。
1、AQS是一个JAVA线程同步的框架。是JDK中很多锁工具的核心实现框架。
2、在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中,这个线程队列,就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行的。在不同的场景下,有不用的意义。
3、在可重入锁这个场景下,state就用来表示加锁的次数。0标识无锁,每加一次锁,state就加1。释放锁state就减1。
① 底层实现上来说,synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法,ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。
synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁,ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。
② 是否可手动释放:
synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。
③ 是否可中断
synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。
④ 是否公平锁
synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。
⑤ 锁是否可绑定条件Condition
synchronized不能绑定; ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。
⑥ 锁的对象
synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。
1、synchronized(原子性、可见性、有序性)
Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍然可以访问该object中的非加锁代码块。
2、Lock(原子性、可见性、有序性)
synchronized是Java语言的关键字,是内置特性,而ReentrantLock是一个类(实现Lock接口的类),通过该类可以实现线程的同步。
3、volatile(可见性、有序性)
volatile是一个类型修饰符(type specifier)。它是被设计用来修饰被不同线程访问和修改的变量。确保本条指令不会因编译器的优化而省略,且要求每次直接读值。
阻塞队列是一个支持附加操作的队列。
这两个附加操作是:
在队列为空时。获取元素的线程会等待队列为非空。
当队列满时,存储元素的线程会等待队列可用。
阻塞队列常用语生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿走元素的线程。
为了解决负载均衡的问题,充分利用CPU资源,为了提高CPU的使用率,采用多线程的方式去同时完成几件事情而不互相干扰。
cas的全称是Compare ans swap,也就是比较并交换。
包含三个参数:V, A, B
其中v是要读些的内存位置,A表示旧的预期值,B表示新值
CAS指令执行的时候,当且仅当V的值等于A的值时,才会将A的值设置为B的值,如果V和A不同,说明可能其他线程做了更新,那么当前线程就什么也不做,最后,CAS返回的是V的真实值。
而在多线程情况下,当多个线程同时使用CAS操作一个变量时,只有一个会成功并更新值,其余线程均会失败,但是失败 的线程不会被挂起,而是不断的再次循环重试。正是基于这样的原理,cas即时是没有使用锁,也能发现其他线程对当前线程的干扰,从而进行即时的处理。
java.util.concurrent.atomic包下的原子类就是用的cas
Consistency(一致性):
即更新操作成功并返回客户端后,所有节点在同一时间的数据完全一致。
对于客户端来说,一致性指的是并发访问时更新过的数据如何获取的问题。
从服务端来看,则是更新如何复制分布到整个系统,以保证数据最终一致。
Availability(可用性):
即服务一直可用,而且是正常响应时间。系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。
Partition Tolerance(分区容错性):
即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。分区容错性要求能够使应用虽然是一个分布式系统,而看上去却好像是在一个可以运转正常的整体。比如现在的分布式系统中有某一个或者几个机器宕掉了,其他剩下的机器还能够正常运转满足系统需求,对于用户而言并没有什么体验上的影响。
CP和AP:分区容错是必须保证的,当发生网络分区的时候,如果要继续服务,那么强一致性和可用性只能2选1
BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)
BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
基本可用:
1.响应时间上的损失:正常情况下,处理用户请求需要0.5s返回结果,但是由于系统出现故障,处理用户请求的
时间变为3s。
2.系统功能上的损失:正常情况下,用户可以使用系统的全部功能,但是由于系统访问量突然剧增,系统的部分非核心功能无法使用。
软状态:数据同步允许一定的延迟
最终一致性:系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态,不要求实时
Failover Cluster失败自动切换:dubbo的默认容错方案,当调用失败时自动切换到其他可用的节点,具体的重试次数和间隔时间可通过引用服务的时候配置,默认重试次数为1也就是只调用一次。
Failback Cluster失败自动恢复:在调用失败,记录日志和调用信息,然后返回空结果给consumer,并且通过定时任务每隔5秒对失败的调用进行重试
Failfast Cluster快速失败:只会调用一次,失败后立刻抛出异常
Failsafe Cluster失败安全:调用出现异常,记录日志不抛出,返回空结果
Forking Cluster并行调用多个服务提供者:通过线程池创建多个线程,并发调用多个provider,结果保存到阻塞队列,只要有一个provider成功返回了结果,就会立刻返回结果
Broadcast Cluster广播模式:逐个调用每个provider,如果其中一台报错,在循环调用结束后,抛出异常。
1.随机:从多个服务提供者随机选择一个来处理本次请求,调用量越大则分布越均匀,并支持按权重设置随机概率
2.轮询:依次选择服务提供者来处理请求,并支持按权重进行轮询,底层采用的是平滑加权轮询算法
3.最小活跃调用数:统计服务提供者当前正在处理的请求,下次请求过来则交给活跃数最小的服务器来处理
4.一致性哈希:相同参数的请求总是发到同一个服务提供者
1.首先Dubbo会将程序员所使用的@DubboService注解或@Service注解进行解析得到程序员所定义的服务参数,包括定
义的服务名、服务接口、服务超时时间、服务协议等等,得到一个ServiceBean。
2.然后调用ServiceBean的export方法进行服务导出
3.然后将服务信息注册到注册中心,如果有多个协议,多个注册中心,那就将服务按单个协议,单个注册中心进行注册
4.将服务信息注册到注册中心后,还会绑定一些监听器,监听动态配置中心的变更
5.还会根据服务协议启动对应的Web服务器或网络框架,比如Tomcat、Netty等
1.当程序员使用@Reference注解来引入一个服务时,Dubbo会将注解和服务的信息解析出来,得到当前所引用的服务
名、服务接口是什么
2.然后从注册中心进行查询服务信息,得到服务的提供者信息,并存在消费端的服务目录中
3.并绑定一些监听器用来监听动态配置中心的变更
4.然后根据查询得到的服务提供者信息生成一个服务接口的代理对象,并放入Spring容器中作为Bean
1、采用无状态服务,抛弃session
2、存入cookie(有安全风险)
3、服务器之间进行Session同步,这样可以保证每个服务器上都有全部的Session信息,不过当服务器数量比较多的时候,同步是会有延迟甚至同步失败;
4、IP绑定策略
使用Nginx(或其他复杂均衡软硬件)中的IP绑定策略,同一个IP只能在指定的同一个机器访问,但是这样做失去了负载均衡的意义,当挂掉一台服务器的时候,会影响一批用户的使用,风险很大;
5、使用Redis存储
把Session放到Redis中存储,虽然架构上变得复杂,并且需要多访问一次Redis,但是这种方案带来的好处也是很大的:
(1)实现了 Session共享;
(2)可以水平扩展(增加Redis服务器);
(3)服务器重启Session不丢失(不过也要注意Session在Redis中的刷新/失效机制)
(4)不仅可以跨服务器Session共享,甚至可以跨平台(例如网页端和APP端)。
底层协议:springcloud基于http协议,dubbo基于Tcp协议,决定了dubbo的性能相对会比较好
注册中心:Spring Cloud使用的eureka,dubbo推荐使用zookeeper
模型定义:dubbo将一个接口定义为一个服务,SpringCloud则是将一个应用定义为一个服务
SpringCloud是一个生态,而Dubbo是SpringCloud生态中关于服务调用一种解决方案(服务治理)
new Zookeeper(String connectString, int sessionTimeout, Watcher watcher)
这个watcher将作为整个zooKeeper会话期间的上下文,一直被保存在客户端zKwatchManager的defaultwatcher
也可以动态添加watcher:getData(),exists,getChildren。
分布式环境下的观察者模式:通过客服端和服务端分别创建有观察者的信息列表。客户端调用相应接口时,首先将对应的Watch事件放到本地的zKWatchManager中进行管理。服务端在接收到客户端的请求后根据请求类型判断是否含有Watch事件,并将对应事件放到WatchManager中进行管理。
在事件触发的时候服务端通过节点的路径信息查询相应的Watch事件通知给客户端,客户端在接收到通知后,首先查询本地的ZKWatchManager获得对应的Watch信息处理回调操作。这种设计不但实现了一个分布式环境下的观察者模式,而且通过将客户端和服务端各自处理Watch事件所需要的额外信息分别保存在两端,减少彼此通信的内容。大大提升了服务的处理性能
客户端实现过程:
1.标记该会话是一个带有Watch事件的请求
2.通过 DataWatchRegistration 类来保存 watcher事件和节点的对应关系
3.客户端向服务器发送请求,将请求封装成一个Packet对象,并添加到一个等待发送队列outgoingQueue中
4.调用负责处理队列outgoingQueue的SendThread线程类中的readResponse方法接收服务端的回调,并在最后执行 finishPacket ()方法将Watch 注册到ZKWatchManager,sendThread 通讨发送path 路径和watcher为true,到server注册watch事件。
ZKWatchManager保存了 Map
服务端实现过程:
1.解析收到的请求是否带有Watch注册事件,通过FinalRequestProcessor类中的processRequest函数实现的。当getDataRequest.getWatch()值为True时,表明该请求需要进行Watch 监控注册。
2.将对应的Watch事件存储到WatchManager,通过zks.getzKDatabase().getData函数实现,WatchManger该类中有HashMap
服务端触发过程:
1.调用WatchManager中的方法触发数据变更事件
2.封装了一个具有会话状态、事件类型、数据节点3种属性的WatchedEvent对象。之后查询该节点注册的Watch事件,如果为空说明该节点没有注册过Watch事件。如果存在Watch事件则添加到定义的Wathcers集合中,并在WatchManager管理中删除。最后,通过调用process方法向客户端发送通知
客户端回调过程:
1.使用SendThread.readResponse()方法来统一处理服务端的相应
2.将收到的字节流反序列化转换成WatcherEvent对象。调用eventThread.queueEvent()方法将接收到的事件交给EventThread线程进行处理
3.从ZKWatchManager中查询注册过的客户端Watch信息。查询到后,会将其从zKWatchManager的管理中删除。因此客户端的Watcher机制是一次性的,触发后就会被删除
4.将查询到的Watcher存储到waitingEvents队列中,调用EventThread类中的run方法循环取出在waitingEvents 队列中等待的Watcher事件进行处理
对于Zookeeper集群,整个集群需要从集群节点中选出一个节点作为Leader,大体流程如下:
1.集群中各个节点首先都是观望态,一开始都会投票给自己,认为自己比较适合作为leader
2.然后相互交互投票,每个节点会收到其他节点发过来的选票,然后pk,先比较zxid,zxid大者获胜,zxid如果相等则比较myid,myid大者获胜
3.一个节点收到其他节点发过来的选票,经过PK后,如果PK输了,则改票,此节点就会投给zxid或myid更大的节点,并将选票放入自己的投票箱中,并将新的选票发送给其他节点
4.如果pk是平局则将接收到的选票放入自己的投票箱中
5.如果pk赢了,则忽略所接收到的选票
6.当然一个节点将一张选票放入到自己的投票箱之后,就会从投票箱中统计票数,看是否超过一半的节点都和自己所投的节点是一样的,如果超过半数,那么则认为当前自己所投的节点是leader
7.集群中每个节点都会经过同样的流程,pk的规则也是一样的,一旦改票就会告诉给其他服务器,所以最终各个节点中的投票箱中的选票也将是一样的,所以各个节点最终选出来的leader也是一样的,这样集群的leader就选举出来了
分布式锁所要解决的问题的本质是:能够对分布在多台机器中的线程对共享资源的互斥访问。在这个原理上可以有很多的实现方式:
1.基于Mysql,分布式环境中的线程连接同一个数据库,利用数据库中的行锁来达到互斥访问,但是Mysql的加锁和释放锁的性能会比较低,不适合真正的实际生产环境
2.基于zookeeper,zookeeper中的数据是存在内存的,所以相对于Mysql性能上是适合实际环境的,并且基于zookeeper的顺序节点、临时节点、Watch机制能非常好的来实现的分布式锁
3.基于Redis,Redis中的数据也是在内存,基于Redis的消费订阅功能、数据超时时间,lua脚本等功能,也能很好的实现的分布式锁
在单体架构中,多个线程都是属于同一个进程的,所以在线程并发执行时,遇到资源竞争时,可以利用ReentrantLock、synchronized等技术来作为
锁,来控制共享资源的使用。
而在分布式架构中,多个线程是可能处于不同进程中的,而这些线程并发执行遇到资源竞争时,利用ReentrantLock、synchronized等技术是没办法来控制多个进程中的线程的,所以需要分布式锁,意思就是,需要一个分布式锁生成器,分布式系统中的应用程序都可以来使用这个生成器所提供的锁,从而达到多个进程中的线程使用同一把锁。
目前主流的分布式锁的实现方案有两种:
1.zookeeper:利用的是zookeeper的临时节点、顺序节点、watch机制来实现的,zookeeper分布式锁的特点是高一致性,因为zookeeper保证的是CP,所以由它实现的分布式锁更可靠,不会出现混乱
2.redis:利用redis的setnx、lua脚本、消费订阅等机制来实现的,redis分布式锁的特点是高可用,因为redis保证的是Ap,所以由它实现的分布式锁可能不可靠,不稳定(一旦redis中的数据出现了不一致),可能会出现多个客户端同时加到锁的情况
远程过程调用
RPC要求在调用方中放置被调用的方法的接口。调用方只要调用了这些接口,就相当于调用了被调用方的实际方法,十分易用。于是,调用方可以像调用内部接口一样调用远程的方法,而不用封装参数名和参数值等操作。
包含
1.动态代理,封装调用细节
2.序列化与反序列化,数据传输与接收
3.通信,可以选择七层的http,四层的tcp/udp
4.异常处理等
首先,调用方调用的是接口,必须得为接口构造一个假的实现。显然,要使用动态代理。这样,调用方的调用就被动态代理接收到了。
第二,动态代理接收到调用后,应该想办法调用远程的实际实现。这包括下面几步:
1.识别具体要调用的远程方法的IP、端口
2.将调用方法的入参进行序列化
3.通过通信将请求发送到远程的方法中
这样,远程的服务就接收到了调用方的请求。它应该:
1.反序列化各个调用参数
2.定位到实际要调用的方法,然后输入参数,执行方法
3.按照调用的路径返回调用的结果
命名服务:
通过指定的名字来获取资源或者服务地址。Zookeeper可以创建一个全局唯一的路径,这个路径就可以作为一个名字。被命名的实体可以是集群中的机器,服务的地址,或者是远程的对象等。一些分布式服务框架(RPC、RMI)中的服务地址列表,通过使用命名服务,客户端应用能够根据特定的名字来获取资源的实体、服务地址和提供者信息等
配置管理:
实际项目开发中,经常使用.properties或者xml需要配置很多信息,如数据库连接信息、fps地址端口等等。程序分布式部署时,如果把程序的这些配置信息保存在zk的znode节点下,当你要修改配置,即znode会发生变化时,可以通过改变zk中某个目录节点的内容,利用watcher通知给各个客户端,从而更改配置。
集群管理:
集群管理包括集群监控和集群控制,就是监控集群机器状态,剔除机器和加入机器。zookeeper可以方便集群机器的管理,它可以实时监控znode节点的变化,一旦发现有机器挂了,该机器就会与zk断开连接,对应的临时目录节点会被删除,其他所有机器都收到通知。新机器加入也是类似。
1.分布式架构是指将单体架构中的各个部分拆分,然后部署不同的机器或进程中去,SOA和微服务基本上都是分布式架构的
2.SOA是一种面向服务的架构,系统的所有服务都注册在总线上,当调用服务时,从总线上查找服务信息,然后调用
3.微服务是一种更彻底的面向服务的架构,将系统中各个功能个体抽成一个个小的应用程序,基本保持一个应用对应的一个服务的架构
1.Eureka:注册中心
2.Nacos:注册中心、配置中心
3.Consul:注册中心、配置中心
4.Spring Cloud Config:配置中心
5.Feign/OpenFeign:RPC调用
6.Kong:服务网关
7.Zuul:服务网关
8.Spring Cloud Gateway:服务网关
9.Ribbon:负载均衡
10.Spring CLoud Sleuth:链路追踪
11.Zipkin:链路追踪
12.Seata:分布式事务
13.Dubbo:RPC调用
14.Sentinel:服务熔断
15.Hystrix:服务熔断
Eureka:服务注册与发现
注册:每个服务都向Eureka登记自己提供服务的元数据,包括服务的ip地址、端口号、版本号、通信协议等。eureka将各个服务维护在了一个服务清单中(双层Map,第一层key是服务名,第二层key是实例名,value是服务地址加端口)。同时对服务维持心跳,剔除不可用的服务,eureka集群各节点相互注册每个实例中都有一样的服务清单。
发现:eureka注册的服务之间调用不需要指定服务地址,而是通过服务名向注册中心咨询,并获取所有服务实例清单(缓存到本地),然后实现服务的请求访问。
Ribbon:服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台(被调用方的服务地址有多个),Ribbon也是通过发起http请求,来进行的调用,只不过是通过调用服务名的地址来实现的。虽然说Ribbon不用去具体请求服务实例的ip地址或域名了,但是每调用一个接口都还要手动去发起Http请求
Feign:基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求,简化服务间的调用,在Ribbon的基础上进行了进一步的封装。单独抽出了一个组件,就是Spring Cloud Feign。在引入SpringCloud Feign后,我们只需要创建一个接口并用注解的方式来配置它,即可完成对服务提供方的接口绑定。
调用远程就像调用本地服务一样
Hystrix:发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,通过统计接口超时次数返回默认值,实现服务熔断和降级
Zuul:如果前端、移动端要调用后端系统,统一从zuul网关进入,由zuul网关转发请求给对应的服务,通过与Eureka进行整合,将自身注册为Eureka下的应用,从Eureka下获取所有服务的实例,来进行服务的路由。zuul还提供了一套过滤器机制,开发者可以自己指定哪些规则的请求需要执行校验逻辑,只有通过校验逻辑的请求才会被路由到具体服务实例上,否则返回错误提示。
限流一般需要结合容量规划和压测来进行。当外部请求接近或者达到系统的最大阈值时,触发限流,采取其他的手段进行降级,保护系统不被压垮。常见的降级策略包括延迟处理、拒绝服务、随机拒绝等。
计数器法:
1、将时间划分为固定的窗口大小,例如1s
2、在窗口时间段内,每来一个请求,对计数器加1。
3、当计数器达到设定限制后,该窗口时间内的之后的请求都被丢弃处理。
4、该窗口时间结束后,计数器清零,从新开始计数。
滑动窗口计数法:
1.将时间划分为细粒度的区间,每个区间维持一个计数器,每进入一个请求则将计数器加一。
2.多个区间组成一个时间窗口,每流逝一个区间时间后,则抛弃最老的一个区间,纳入新区间。
3.若当前窗口的区间计数器总和超过设定的限制数量,则本窗口内的后续请求都被丢弃。
漏桶算法:如果外部请求超出当前阈值,则会在容易里积蓄,一直到溢出,系统并不关心溢出的流量。从出口处限制请求速率,并不存在计数器法的临界问题,请求曲线始终是平滑的。无法应对突发流量,相当于一个空桶+固定处理线程
令牌桶算法:假设一个大小恒定的桶,这个桶的容量和设定的阈值有关,桶里放着很多令牌,通过一个固定的速率,往里边放入令牌,如果桶满了,就把令牌丢掉,最后桶中可以保存的最大令牌数永远不会超过桶的大小。当有请求进入时,就尝试从桶里取走一个令牌,如果桶里是空的,那么这个请求就会被拒绝。
分布式容错框架
1.阻止故障的连锁反应,实现熔断
2.快速失败,实现优雅降级
3.提供实时的监控和告警
资源隔离:线程隔离,信号量隔离
1.线程隔离:Hystrix会给每一个Command分配一个单独的线程池,这样在进行单个服务调用的时候,就可以在独立的线程池里面进行,而不会对其他线程池造成影响
2.信号量隔离:客户端需向依赖服务发起请求时,首先要获取一个信号量才能真正发起调用,由于信号量的数量有限,当并发请求量超过信号量个数时,后续的请求都会直接拒绝,进入fallback流程。信号量隔离主要是通过控制并发请求量,防止请求线程大面积阻塞,从而达到限流和防止雪崩的目的。
熔断和降级:调用服务失败后快速失败
熔断是为了防止异常不扩散,保证系统的稳定性
降级:编写好调用失败的补救逻辑,然后对服务直接停止运行,这样这些接口就无法正常调用,但又不至于直接报错,只是服务水平下降
1.通过HystrixCommand或者HystrixObservableCommand将所有的外部系统(或者称为依赖)包装起来,整个包装对象是单独运行在一个线程之中(这是典型的命令模式)。
2.超时请求应该超过你定义的阈值
3.为每个依赖关系维护一个小的线程池(或信号量);如果它变满了,那么依赖关系的请求将立即被拒绝,而不是排队等待。
4.统计成功,失败(由客户端抛出的异常),超时和线程拒绝。
5.打开断路器可以在一段时间内停止对特定服务的所有请求,如果服务的错误百分比通过阈值,手动或自动的关闭断路器。
6.当请求被拒绝、连接超时或者断路器打开,直接执行fallback逻辑。
7.近乎实时监控指标和配置变化。
1.服务熔断是指,当服务A调用的某个服务B不可用时,上游服务A为了保证自己不受影响,从而不再调用服务B,直接返回一个结果,减轻服务A和服务B的压力,直到服务B恢复。
2.服务降级是指,当发现系统压力过载时,可以通过关闭某个服务,或限流某个服务来减轻系统压力,这就是服务降级。
相同点:
1.都是为了防止系统崩溃
2.都让用户体验到某些功能暂时不可用
不同点:熔断是下游服务故障触发的,降级是为了降低系统负载
1.当服务A调用服务B,服务B调用C,此时大量请求突然请求服务A,假如服务A本身能抗住这些请求,但是如果服务C抗不住,导致服务C请求堆积,从而服务B请求堆积,从而服务A不可用,这就是服务雪崩,解决方式就是服务降级和服务熔断。
2.服务限流是指在高并发请求下,为了保护系统,可以对访问服务的请求进行数量上的限制,从而防止系统不被大量请求压垮,在秒杀中,限流是非常重要的。
开发运维一体化。
敏捷开发:目的就是为了提高团队的交付效率,快速代,快速试错。
每个月固定发布新版本,以分支的形式保存到代码仓库中。快速入职。任务面板、站立会议。团队人员灵活流动,同时形成各个专家代表。测试环境-生产环境-》开发测试环境SIT、集成测试环境、压测环境STR、预投产环境、生产环境PRD。文档优先。晨会、周会、需求拆分会。
链路追踪:1、基于日志。形成全局事务ID,落地到日志文件。filebeat-logstash-Elasticsearch形成大型报表。2、基于MQ,往往需要架构支持。经过流式计算形成一些可视化的结果。
持续集成:SpringBoot maven pom->build->shell;Jenkins。
AB发布:1、蓝绿发布、红黑发布老版本和新版本是同时存在的。2、灰度发布、金丝雀发布。
微服务是由Martin Fowler大师提出的。微服务是一种架构风格,通过将大型的单体应用划分为比较小的服务单元,从而降低整个系统的复杂度。
优点:
1、服务部署更灵活:每个应用都可以是一个独立的项目,可以独立部署,不依赖于其他服务,耦合性降低。
2、技术更新灵活:在大型单体应用中,技术要进行更新,往往是非常困难的。而微服务可以根据业务特点,灵活选择技术栈。
3、应用的性能得到提高:大型单体应用中,往往启动就会成为一个很大的难关。而采用微服务之后,整个系统的性能是能够得到提高的。
4、更容易组合专门的团队:在单体应用时,团队成员往往需要对系统的各个部分都要有深入的了解,门槛是很高的。而采用微服务之后,可以给每个微服务组建专门的团队。
5、代码复用:很多底层服务可以以REST API的方式对外提供统一的服务,所有基础服务可以在整个微服务系统中通用。
缺点:
1、服务调用的复杂性提高了:网络问题、容错问题、负载问题、高并发问题。。。。。。
2、分布式事务:尽量不要使用微服务事务。
3、测试的难度提升了:
4、运维难度提升:单体架构只要维护一个环境,而到了微服务是很多个环境,并且运维方式还都不一样。所以对部署、监控、告警等要求就会变得非常困难。
ActiveMQ:JMS规范,支持事务、支持XA协议,没有生产大规模支撑场景、官方维护越来越少
RabbitMQ:erlang语言开发、性能好、高并发,支持多种语言,社区、文档方面有优势,erlang语言不利于java程序员二次开发,依赖开源社区的维护和升级,需要学习AMQP协议、学习成本相对较高
以上吞吐量单机都在万级
kafka:高性能,高可用,生产环境有大规模使用场景,单机容量有限(超过64个分区响应明显变长)、社区更新慢吞
吐量单机百万
rocketmq:java实现,方便二次开发、设计参考了kafka,高可用、高可靠,社区活跃度一般、支持语言较少
吞吐量单机十万
1.pull表示消费者主动拉取,可以批量拉取,也可以单条拉取,所以pull可以由消费者自己控制,根据自己的消息处理能力来进行控制,但是消费者不能及时知道是否有消息,可能会拉到的消息为空
2.push表示Broker主动给消费者推送消息,所以肯定是有消息时才会推送,但是消费者不能按自己的能力来消费消息,推过来多少消息,消费者就得消费多少消息,所以可能会造成网络堵塞,消费者压力大等问题
RocketMQ由NameServer集群、Producer集群、Consumer集群、Broker集群组成,消息生产和消费的大致原理如下:
1.Broker在启动的时候向所有的NameServer注册,并保持长连接,每30s发送一次心跳
2.Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择一台服务器来发送消息
3.Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费
(1)消息发送
1、ack=0,不重试
producer发送消息完,不管结果了,如果发送失败也就丢失了。
2、 ack=1, leader crash
producer发送消息完,只等待1ead写入成功就返回了,leader crash了,这时fo11ower没来及同步,消息丢失。
3、unclean.leader.election.enable 配置true
允许选举ISR以外的副本作为1eader,会导致数据丢失,默认为false。producer发送异步消息完,只等待1ead写入成功就返回了,leader crash了,这时ISR中没有fo11ower,1eader从osR中选举,因为osR中本来落后于Leader造成消息丢失。
解决方案:
1、配置:ack=al1 /-1,tries > 1,unclean.leader.election.enable :false
producer发送消息完,等待fo11ower同步完再返回,如果异常则重试。副本的数量可能影响吞吐量。
不允许选举ISR以外的副本作为1eader。
2、配置:min.insync.replicas >1
副本指定必须确认写操作成功的最小副本数量。如果不能满足这个最小值,则生产者将引发一个异常(要么是NotEnoughReplicas,要么是NotEnoughReplicasAfterAppend)。
min.insync.replicas和ack更大的持久性保证。确保如果大多数副本没有收到写操作,则生产者将引发异常。
3、失败的offset单独记录
producer发送消息,会自动重试,遇到不可恢复异常会抛出,这时可以捕获异常记录到数据库或缓存,进行单独处理。
(2)消费
先commit再处理消息。如果在处理消息的时候异常了,但是offset已经提交了,这条消息对于该消费者来说就是丢失了,再也不会消费到了。
(3)broker的刷盘
减小刷盘间隔
分布式事务:业务相关的多个操作,保证他们同时成功或者同时失败。
最终一致性:与之对应的就是强一致性
MQ中要保护事务的最终一致性,就需要做到两点
1、生产者要保证100%的消息投递。事务消息机制
2、消费者这一端需要保证幂等消费。唯一ID+业务自己实现幂等
分布式MQ的三种语义:
at least once
at most once
exactly once:RocketMQ并不能保证exactly once。商业版本当中提供了exactlylonce的实现机制。
零拷贝:kafka和RocketMQ都是通过零拷贝技术来优化文件读写。
传统文件复制方式:需要对文件在内存中进行四次拷贝。
Java当中对零拷贝进行了封装,Mmap方式通过MappedByteBuffer对象进行操作,而transfile通过FileChannel来进行操作。
Mmap适合比较小的文件,通常文件大小不要超过1.5G~2G之间。
Transfile没有文件大小限制。
RocketMQ当中使用Mmap方式来对他的文件进行读写。commitlog。1G
在kafka当中,他的index日志文件也是通过mmap的方式来读写的。在其他日志文件当中,并没有使用零拷贝的方式。
kafka使用fransfile方式将硬盘数据加载到网卡。
全局有序和局部有序:MQ只需要保证局部有序,不需要保证全局有序。
生产者把一组有序的消息放到同一个队列当中,而消费者一次消费整个队列当中的消息。
RocketMQ中有完整的设计,但是在RabbitMQ和Kafka当中,并没有完整的设计,需要自己进行设计。
RabbitMQ:要保证目标exchange只对应一个队列。并且一个队列只对应一个消费者。
Kafka:生产者通过定制partition分配规则,将消息分配到同一个partition。Topic下只对应一个消费者。
两个误区:1、放飞自我,漫无边际。2、纠结技术细节。
好的方式:1、从整体到细节,从业务场景到技术实现。2、以现有产品为基础。RocketMQ
答题思路:MQ作用、项目大概的样子。
1、实现一个单机的队列数据结构。高效、可扩展。
2、将单机队列扩展成为分布式队列。-分布式集群管理
3、基于Topic定制消息路由策略。-发送者路由策略,消费者与队列对应关系,消费者路由策略
4、实现高效的网络通信。-Netty Http
5、规划日志文件,实现文件高效读写。-零拷贝,顺序写。服务重启后,快速还原运行现场。
6、定制高级功能,死信队列、延迟队列、事务消息等等。-贴合实际,随意发挥。
1.BIO:同步阻塞IO,使用BIO读取数据时,线程会阻塞住,并且需要线程主动去查询是否有数据可读,并且需要处理完一个Socket之后才能处理下一个Socket
2.NIO:同步非阻塞IO,使用NIO读取数据时,线程不会阻塞,但需要线程主动的去查询是否有IO事件
3.AIO:也叫做NIO2.0,异步非阻塞IO,使用AIO读取数据时,线程不会阻塞,并且当有数据可读时会通知给线程,不需要线程主动去查询
Netty同时支持Reactor单线程模型、Reactor多线程模型和Reactor主从多线程模型,用户可根据启动参数配置在这三种模型之间切换。
服务端启动时,通常会创建两个NioEventLoopGroup实例,对应了两个独立的Reactor线程池,bossGroup负责处理客户端的连接请求,workerGroup负责处理/O相关的操作,执行系统Task、定时任务Task等。用户可根据服务端引导类ServerBootstrap配置参数选择Reactor线程模型,进而最大限度地满足用户的定制化需求。
零拷贝指的是,应用程序在需要把内核中的一块区域数据转移到另外一块内核区域去时,不需要经过先复制到用户空间,再转移到标内核区域去了,而直接实现转移。
三次握手
刚开始客户端处于 closed 的状态,服务端处于 listen 状态。然后
1、第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(c)。此时客户端处于 SYN_Send 状态。
2、第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s),同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。
3、第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 establised 状态。
4、服务器收到 ACK 报文之后,也处于 establised 状态,此时,双方以建立起了链接。
Ps:
(1)SYN=1 表示该报文不携带数据,但消耗一个序号 seq=x,seq=x是客户端的初始化序列号,因为tcp是面向字节流的
(2)SYN=1 表示该报文不携带数据,但消耗一个序号 seq=y,seq=y是服务器的初始化序列号,ACK=1是一个确认号
ack=x+1,表示服务器下次接收到的序号希望是x+1。然后服务器进入到SYN-RCVD等待的状态
(3)ACK=1是一个确认号,seq=x+1是上一次服务器回应的序号要求,ack=y+1表示客户下一次接收到的序号希望是y+1
三次握手的作用
三次握手的作用也是有好多的,多记住几个,保证不亏。例如:
1、确认双方的接受能力、发送能力是否正常。
2、指定自己的初始化序列号,为后面的可靠传送做准备。
3、如果是 https 协议的话,三次握手这个过程,还会进行数字证书的验证以及加密密钥的生成到。
单单这样还不足以应付三次握手,面试官可能还会问一些其他的问题,例如:
1、(ISN)是固定的吗?
三次握手的一个重要功能是客户端和服务端交换ISN(Initial Sequence Number), 以便让对方知道接下来接收数据的时候如何按序列号组装数据。
如果ISN是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。
2、什么是半连接队列
服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。
这里在补充一点关于SYN-ACK 重传次数的问题: 服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超 过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s, 2s, 4s, 8s, …
3、三次握手过程中可以携带数据吗
很多人可能会认为三次握手都不能携带数据,其实第三次握手的时候,是可以携带数据的。也就是说,第一次、第二次握手不可以携带数据,而第三次握手是可以携带数据的。
为什么这样呢?大家可以想一个问题,假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据,因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。也就是说,第一次握手可以放数据的话,其中一个简单的原因就是会让服务器更加容易受到攻击了。
而对于第三次的话,此时客户端已经处于 established 状态,也就是说,对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据页没啥毛病。
当进行第一次握手,网络不好可能会堵塞,所以连接的请求并没有到达服务器端;
但是tcp连接有超时重传的机制,所以再一次发送请求,这时候服务器端接收到了你的请求,他也会返回一个请求给你,这是第二次握手;
但是这时候网络环境突然又好了起来,那个堵塞的请求到达了服务器端,服务器端又给你回了一个请求,但是你又不想给服务器发送请求,这时候服务器的资源会进行占用等待你的请求,为了不使服务器的资源继续占用,你又必须发送一个请求给服务器;
所以要进行3次握手
四次挥手
1、第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态。
2、第二次握手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态。
3、第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
4、第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态
5、服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
这里特别需要主要的就是TIME_WAIT这个状态了,这个是面试的高频考点,就是要理解,为什么客户端发送 ACK 之后不直接关闭,而是要等一阵子才关闭。这其中的原因就是,要确保服务器是否已经收到了我们的 ACK 报文,如果没有收到的话,服务器会重新发 FIN 报文给客户端,客户端再次收到 ACK 报文之后,就知道之前的 ACK 报文丢失了,然后再次发送 ACK 报文。
至于 TIME_WAIT 持续的时间至少是一个报文的来回时间。一般会设置一个计时,如果过了这个计时没有再次收到 FIN 报文,则代表对方成功收到 ACK 报文,此时处于 CLOSED 状态。
HTTP:匙互联网上应用最为广泛的一种网络通信协议,基于TCP,可以使浏览器工作更为高效,减少网络传输。
HTTPS:是HTTP的加强版,可以认为是HTTP+SSL(Secure Socket Layer)。在HTTP的基础上增加了一系列的安全机制。一方面保证数据传输安全,另一位方面对访问者增加了验证机制。是目前现行架构下,最为安全的解决方案。
主要区别:
1、HTTP的连接是简单无状态的,HTTPS的数据传输是经过证书加密的,安全性更高。
2、HTTP是免费的,而HTTPS需要申请证书,而证书通常是需要收费的,并且费用一般不低。
3、他们的传输协议不通过,所以他们使用的端口也是不一样的,HTTP默认是80端口,而HTTPS默认是443端口。
HTTPS的缺点:
1、HTTPS的握手协议比较费时,所以会影响服务的响应速度以及吞吐量。
2、HTTPS也并不是完全安全的。他的证书体系其实并不是完全安全的。并且HTTPS在面对DDOS这样的攻击时,几乎起不到任何作用。
3、证书需要费钱,并且功能越强大的证书费用越高。
https通过使用对称加密、非对称加密、数字证书等方式来保证数据的安全传输。
1.客户端向服务端发送数据之前,需要先建立TCP连接,所以需要先建立TCP连接,建立完TCP连接后,服务端会先给客户端发送公钥,客户端拿到公钥后就可以用来加密数据了,服务端到时候接收到数据就可以用私钥解密数据,这种就是通过非对称加密来传输数据
2.不过非对称加密比对称加密要慢,所以不能直接使用非对称加密来传输请求数据,所以可以通过非对称加密的方式来传输对称加密的秘钥,之后就可以使用对称加密来传输请求数据了
3.但是仅仅通过非对称加密+对称加密还不足以能保证数据传输的绝对安全,因为服务端向客户端发送公钥时,可能会被截取
4.所以为了安全的传输公钥,需要用到数字证书,数字证书是具有公信力、大家都认可的,服务端向客户端发送公钥时,可以把公钥和服务端相关信息通过Hash算法生成消息摘要,再通过数字证书提供的私钥对消息摘要进行加密生成数字签名,在把没进行Hash算法之前的信息和数字签名一起形成数字证书,最后把数字证书发送给客户端,客户端收到数字证书后,就会通过数字证书提供的公钥来解密数字证书,从而得到非对称加密要用到的公钥。
5.在这个过程中,就算有中间人拦截到服务端发出来的数字证书,虽然它可以解密得到非对称加密要使用的公钥,但是中间人是办法伪造数字证书发给客户端的,因为客户端上内嵌的数字证书是全球具有公信力的,某个网站如果要支持https,都是需要申请数字证书的私钥的,中间人如果要生成能被客户端解析的数字证书,也是要申请私钥的,所以是比较安全了。
跨域是指浏览器在发起网络请求时,会检查该请求所对应的协议、域名、端口和当前网页是否一致,如果不一致则浏览器会进行限制,比如在www.baidu.com的某个网页中,如果使用ajax去访问www.jd.com是不行的,但是如果是img、iframe、script等标签的srd属性去访问则是可以的,之
所以浏览器要做这层限制,是为了用户信息安全。但是如果开发者想要绕过这层限制也是可以的:
1.response添加header,比如resp.setHeader(“Access-Control-Allow-Origin",“*");表示可以访问所有网站,不受是否同源的限制
2.jsonp的方式,该技术底层就是基于script标签来实现的,因为script标签是可以跨域的
3.后台自己控制,先访问同域名下的接口,然后在接口中再去使用HTTPClient等工具去调用目标接口
4.网关,和第三种方式类似,都是交给后台服务来进行跨域访问
OAuth2.0的使用场景通常称为联合登录。一处注册,多处使用。
SSO Single Sign On单点登录。一处登录,多处同时登录。
SSO的实现关键是将Session信息集中存储。Spring Security
1.浏览器解析用户输入的URL,生成一个HTTP格式的请求
2.先根据URL域名从本地hosts文件查找是否有映射IP,如果没有就将域名发送给电脑所配置的DNS进行域名解析,得到IP地址
3.浏览器通过操作系统将请求通过四层网络协议发送出去
4.途中可能会经过各种路由器、交换机,最终到达服务器
5.服务器搜到请求后,根据请求所指定的端口,将请求传递给绑定了该端口的应用程序,比如8080被tomcat占用了
6.tomcat接收到请求数据后,按照httpt协议的格式进行解析,解析得到所要访问的servlet
7.然后servlet来处理这个请求,如果是SpringlMVC中的DispatchServlet,那么则会找到对应的Controller中的方法,并执行该方法得到结果
8.Tomcat得到响应结果后封装成HTTP响应的格式,并再次通过网络发送给浏览器所在的服务器
9.浏览器所在的服务器拿到结果后再传递给浏览器,浏览器则负责解析并渲染
详细步骤:
ARP解析-获取数据传输下一跳MAC地址
在主机A向主机B发送IP报文前,需要先获得主机B(路由器,代理等)的MAC地址。因此,在进行域名解析之前,主机会先在ARP高速缓存中查看是否主机B的IP地址。如有,就在ARP高速缓存中查出其对应的MAC地址。如果缓存中没有主机B的IP地址项目,主机A自动运行ARP,在局域网中广播ARP请求报文,从而获得主机B的MAC地址,并写入自己的缓存[1]。
DNS域名解析
为了与目标服务器建立连接,需要获得域名对应的IP地址。主机先在自己的DNS缓存中查找是否存在对应的域名记录,若不存在,则主机向本地域名服务器发出DNS请求。本地域名服务器缓存中若存在记录,则返回记录,若不存在,则向根服务器、顶级域名服务器和各级权威服务器发起迭代查询。最后获得域名的IP地址记录。
TCP建立连接-三次握手
客户端发出SYN包,SYN=1,初始序列号seq=x
服务器返回确认,ACK=1,SYN=1,seq=y,ack=x+1
客户端返回确认,ACK=1,seq=x+1,ack=y+1
数据传输-分组转发算法[1]
从数据报文的首部提取目的主机的IP地址D,得出目的网络地址为N。
若N就是与此路由器直接相连的某个网络地址,则进行直接交付,不需要再经过其他的路由器,直接把数据报文交付目的主机(这里包括把目的主机地址D转换为具体的MAC地址,把数据报文封装为MAC帧,再发送此帧);否则就是间接交付,执行3.
若路由表中有目的地址为D的特定主机路由,则把数据报文传送给路由表中所指明的下一跳路由器;否则,执行4.
若路由表中有到达网络N的路由,则把数据报文传送给路由表中所指明的下一跳路由器;否则,执行5.
若路由表中有一个默认路由,则把数据报文传送给路由表中所指明的默认路由器;否则,执行6.
报告转发分组出错。
(注:其实分组转发算法包含在所有的步骤中)
HTTP请求[2]
连接建立后,主机构造HTTP请求报文。请求头中包括请求行和首部字段等信息。
请求行中包括了请求方法、URL、HTTP版本。
首部字段中包含重要的信息和具体参数。
服务器处理请求
服务器收到HTTP请求后,根据请求报文中的信息和数据,调度相应的资源文件。通过这些文件来处理用户的请求。最后将处理结果返回给客户端。
HTTP响应
服务器将处理结果,构造成HTTP响应报文。响应报文中包括响应头和主体信息。
响应头中包括了状态行(状态码、HTTP版本、解释性短语)和其他首部字段。
TCP断开连接-四次挥手
在传输完所有数据后,断开TCP连接。
客户端向服务器发送FIN包,FIN=1,seq=x
服务器返回确认包,ACK=1,ack=x+1,seq=v
服务器向客户端发送FIN包,FIN=1,ACK=1,ack=x+1,seq=w
客户端返回确认包,ACK=1,ack=w+1,seq=x+1
浏览器解析文档[2]
浏览器通过解析HTML,生成DOM树,解析CSS,生成CSS规则树,然后通过DOM树和CSS规则树生成渲染树。渲染树与DOM树不同,渲染树中并没有head、display为none等不必显示的节点。
要注意的是,浏览器的解析过程并非是串连进行的,比如在解析CSS的同时,可以继续加载解析HTML,但在解析执行JS脚本时,会停止解析后续HTML,这就会出现阻塞问题。
浏览器渲染布局
根据渲染树布局,计算CSS样式,即每个节点在页面中的大小和位置等几何信息。HTML默认是流式布局的,CSS和JS会打破这种布局,改变DOM的外观样式以及大小和位置。
最后浏览器绘制各个节点,将页面展示给用户。
1.select模型,使用的是数组来存储Socket连接文件描述符,容量是固定的,需要通过轮询来判断是否发生了IO事件
2.poll模型,使用的是链表来存储Socket连接文件描述符,容量是不固定的,同样需要通过轮询来判断是否发生了IO事件
3.epoll模型,epoll和poll是完全不同的,epoll是一种事件通知模型,当发生了IO事件时,应用程序才进行IO操作,不需要像poll模型那样主动去轮询
1.TCP(Transmission Control Protocol,传输控制协议)和UDP(User Data Protocol ,用户数据报协议)都属于TCP/IP协议簇。
2.连接性
TCP是面向连接的协议,在收发数据前必须和对方建立可靠的连接,建立连接的3次握手、断开连接的4次挥手,为数据传输打下可靠基础;UDP是一个面向无连接的协议,数据传输前,源端和终端不建立连接,发送端尽可能快的将数据扔到网络上,接收端从消息队列中读取消息段。
3.可靠性
TCP提供可靠交付的服务,传输过程中采用许多方法保证在连接上提供可靠的传输服务,如编号与确认、流量控制、计时器等,确保数据无差错,不丢失,不重复且按序到达;UDP使用尽可能最大努力交付,但不保证可靠交付。
4.报文首部
TCP报文首部有20个字节,额外开销大;UDP报文首部只有8个字节,标题短,开销小。
5.TCP协议面向字节流,将应用层报文看成一串无结构的字节流,分解为多个TCP报文段传输后,在目的站重新装配;UDP协议面向报文,不拆分应用层报文,只保留报文边界,一次发送一个报文,接收方去除报文首部后,原封不动将报文交给上层应用。
6.吞吐量控制
TCP拥塞控制、流量控制、重传机制、滑动窗口等机制保证传输质量;UDP没有。
7.双工性
TCP只能点对点全双工通信;UDP支持一对一、一对多、多对一和多对多的交互通信。
TCP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。
TCP/IP和HTTP协议的关系,从本质上来说,二者没有可比性,我们在传输数据时,可以只使用(传输层)TCP/IP协议,但是那样的话,如果没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用到应用层协议,应用层协议有很多,比如HTTP、FTP、TELNET 等,也可以自己定义应用层协议。WEB使用HTTP协议作应用层协议,以封装HTTP 文本信息,然后使用TCP/IP做传输层协议将它发到网络上。
Http协议是建立在TCP协议基础之上的,当浏览器需要从服务器获取网页数据的时候,会发出一次Http请求。Http会通过TCP建立起一个到服务器的连接通道,当本次请求需要的数据完毕后,Http会立即将TCP连接断开,这个过程是很短的,所以Http连接是一种短连接,是一种无状态的连接。
HTTP请求头提供了关于请求,响应或者其他的发送实体的信息。HTTP的头信息包括通用头、请求头、响应头和实体头四个部分。每个头域由一个域名,冒号(:)和域值三部分组成。
通用头标:即可用于请求,也可用于响应,是作为一个整体而不是特定资源与事务相关联。
请求头标:允许客户端传递关于自身的信息和希望的响应形式。
响应头标:服务器和于传递自身信息的响应。
实体头标:定义被传送资源的信息。即可用于请求,也可用于响应。
Ping 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连通性。
Ping 的原理是通过向目的主机发送 ICMP Echo 请求报文,目的主机收到之后会发送 Echo 回答报文。Ping 会根据时间和成功响应的次数估算出数据包往返时间以及丢包率。
频分多路复用( frequency division multiplexing-FDM )
所谓频分多路复用就是将我们的信道资源按频率上进行划分,分成一个个频带的子信道,让每个信号只去用其中的某一个频带的子信道。
时分多路复用( time division multiplexing-TDM )
时分复用则是将时间划分为一段段等长的时分复 用帧(TDM 帧),每个用户在每个 TDM 帧中占 用固定序号的时隙 。
每用户所占用的时隙是周期性出现(其周期就是 TDM 帧的长度)
时分复用的所有用户是在不同的时间占用相同的 频带宽度
波分多路复用(Wavelength division multiplexing-WDM)
码分多路复用( Code division multiplexing-CDM )
(1)cookie数据存放在客户的浏览器上,session数据放在服务器上
(2)cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,如果主要考虑到安全应当使用session
(3)session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,如果主要考虑到减轻服务器性能方面,应当使用COOKIE
(4)单个cookie在客户端的限制是3K,就是说一个站点在客户端存放的COOKIE不能3K。
(5)所以:将登陆信息等重要信息存放为SESSION;其他信息如果需要保留,可以放在COOKIE中
HTTP 协议中的内容都是明文传输,HTTPS 的目的是将这些内容加密,确保信息传输安全。最后一个字母 S 指的是 SSL/TLS 协议,它位于 HTTP 协议与 TCP/IP 协议中间。
1)HTTP明文传输带来的问题
窃听风险
篡改风险
冒充风险
2)SSL/TLS协议的目的
所有信息都是加密传播,无法被窃听
具有校验机制,被篡改后,通信双方可以立刻发现
配备身份证书,防止被冒充
1.超时重传
超时重传:在数据发送的时候,设定一个定时器,当在设置的时间内没有收到相应的ACK确认应答报文就会重发该报文
超时重传的两种情况
数据包丢失
确认应答丢失
超时时间的设置
RTT:就是数据从网络的一端到另一端所需的时间(就是包的往返时间)
RTO:就是超时计时器计算的祭时间
2.流量控制
滑动窗口
拥塞控制
拥塞避免
一、主机向本地域名服务器的查询一般都是采用递归查询。所谓递归查询就是:如果主机所询问的本地域名服务器不知道被查询的域名的IP地址,那么本地域名服务器就以DNS客户的身份,向其它根域名服务器继续发出查询请求报文(即替主机继续查询),而不是让主机自己进行下一步查询。因此,递归查询返回的查询结果或者是所要查询的IP地址,或者是报错,表示无法查询到所需的IP地址。
二、本地域名服务器向根域名服务器的查询的迭代查询。迭代查询的特点:当根域名服务器收到本地域名服务器发出的迭代查询请求报文时,要么给出所要查询的IP地址,要么告诉本地服务器:“你下一步应当向哪一个域名服务器进行查询”。然后让本地服务器进行后续的查询。根域名服务器通常是把自己知道的顶级域名服务器的IP地址告诉本地域名服务器,让本地域名服务器再向顶级域名服务器查询。顶级域名服务器在收到本地域名服务器的查询请求后,要么给出所要查询的IP地址,要么告诉本地服务器下一步应当向哪一个权限域名服务器进行查询。最后,知道了所要解析的IP地址或报错,然后把这个结果返回给发起查询的主机
一个HTTP请求报文由四个部分组成:
请求行:请求方法字段、URL字段、HTTP协议版本号
请求头部:请求的类型
空行:通过一个空行告诉服务器请求头部到此为止
请求数据:若方法 字段是GET,则此项为空,没有数据;若方法字段是post,则通常来说此处放置的就是 要提交的数据,比如用户名和密码:user=admin&password=admin
HTTP响应报文由三部分组成:
响应行:一般有版本协议和状态码组成如HTTP1.1 200 OK
响应头:用于描述服务器的基本信息,以及数据的描述
响应体:相应的消息体:数据、html页面、js代码之类
二进制分帧:在应用层和传输层之间增加了一个二进制分帧层,在二进制分帧层上,http2.0会将所有的信息分割为更小的信息和帧,并对它们采用二进制格式的编码
多路复用:优点:
并行交错发送请求,请求之间话不影响
TCP连接一旦建立可以并行发送请求
消除不必要的延迟,减少页面加载时间
首部压缩:
流量控制
请求优先级
服务器推送
答:Mybstis只支持针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口
的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,拦截那些你指定需要拦截的方法。
编写插件:实现 Mybatis的Interceptor接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,在配置文件中配置编写的插件。
@Intercepts({@signature(type = StatementHandler.class, method = "query", args =
{Statement.class, ResultHandler.class}),
@Signature(type = StatementHandler.class, method = "update", args =
{Statement.class}),
@Signature(type = StatementHandler.class, method = "batch", args = {
Statement.class })})
@Component
invocation.proceed()执行具体的业务逻辑
优点:
1.基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL单独写,解除sql与程序代码的耦合,便于统一管理。
2.与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
3.很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
4.能够与Spring很好的集成;
5.提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。
缺点:
1.SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
2.SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库
spring是一个IOC容器,用来管理Bean,使用依赖注入实现控制反转,可以很方便的整合各种框架,提供AOP机制弥补OOP的代码重复问题、更方便将不同类不同方法中的共同处理抽取成切面、自动注入给方法执行,比如日志、异常等
springmvc是spring对web框架的一个解决方案,提供了一个总的前端控制器Servlet,用来接收请求,然后定义了一套路由策略(url到handle的映射)及适配执行handle,将handle结果使用视图解析技术生成视图展现给前端
springboot是spring提供的一个快速开发工具包,让程序员能更方便、更快速的开发spring+springmvc应用,简化了配置(约定了默认配置),整合了一系列的解决方案(starter机制)、redis、mongodb、es,可以开箱即用
补充:
总:两者作为Spring生态中的组件,产生时间不同,spring mvc很早就诞生,例如之前最主流的企业开发框架ssm,就用到了Spring mvc。Spring Boot作为后起之秀,通过“约定大于配置”来减少许多配置,大大的提高了生产力。
spring mvc
历史:spring mvc诞生在servlet之后,将其封装,简化其开发难度,让开发人员无需处理整个HttpRequest,也无需处理IO流,只需关心业务处理。同时它也进行了切面封装,可以定义全局异常处理器。基于Servlet开发时,IO返回的即是页面显示的,而spring mvc却可以返回一个渲染后页面。
发展:spring mvc发展到现在,已经有@GetMapping, @PostMapping, @RestController等注解,进一步简化了开发。同时REST与前后端分离兴起之后,后端返回前端只需要返回数据即可。
spring boot
历史:spring boot的产生主要是提供工程开发便捷。之前开发Spring工程,除了引入依赖,还需要配置许多上下文容器中的配置,例如我们数据库配置,bean的配置,mvc mapping的声明,都是十分麻烦的。在spring boot上这搭建工程仅需几分钟即可,就像它官网说的那样开箱即用,“just run”。
使用:在spring boot使用上,构建工程时,使用最多的就是引入对应组件的starter,版本交由spring boot管理,省去了解决依赖冲突的工作量。在开发过程中,结合spring以及spring boot引入的一些注解,例如@Configuration, @Bean, @ConditionalOnClass, @ConditionalOnMissingClass等注解,让我们可以更优雅的注入Bean,以及替换掉默认引入的Bean。
总:总之,现在这两个组件我们工程都在使用,搭配起来,开发十分方便。
@Import + @Configuration + Spring spi
自动配置类由各个starter提供,使用@Configuration+@Bean定义配置类,放到META-INF/spring.factories下
使用Spring spi扫描META-INF/spring.factories下的配置类
使用@lmport导入自动配置类
Handler:也就是处理器。它直接应对着MVC中的C也就是Controller层,它的具体表现形式有很多,可以是类,也可以是方法。在Controller层中@RequestMapping标注的所有方法都可以看成是一个Handler,只要可以实际处理请求就可以是Handler
1、HandlerMapping
initHandlerMappings(context),处理器映射器,根据用户请求的资源uri来查找Handler的。在SpringMVC中会有很多请求,每个请求都需要一个Handler处理,具体接收到一个请求之后使用哪个Handler进行,这就是HandlerMapping需要做的事。
2、 HandlerAdapter
initHandlerAdapters(context),适配器。因为SpringMVC中的Handler可以是任意的形式,只要能处理请求就ok,但是Servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法。如何让固定的Servlet处理方法调用灵活的Handler来进行处理呢?这就是HandlerAdapter要做的事情。
Handler是用来干活的工具;HandlerMapping用于根据需要干的活找到相应的工具;HandlerAdapter是使用工具干活的人。
3、 HandlerExceptionResolver
initHandlerExceptionResolvers(context),其它组件都是用来干活的。在干活的过程中难免会出现问题,出问题后怎么办呢?这就需要有一个专门的角色对异常情况进行处理,在SpringMVC中就是
HandlerExceptionResolver。具体来说,此组件的作用是根据异常设置ModelAndView,之后再交给render方法进行渲染。
4、ViewResolver
initViewResolvers(context),ViewResolver用来将String类型的祧图名和Locale解析为View类型的视图。View是用来渲染页面的,也就是将程序返回的参数填入模板里,生成html(也可能是其它类型)文件。这里就有两个关键问题:使用哪个模板?用什么技术(规则)填入参数?这其实是ViewResolver主要要做的工作,ViewResolver需要找到渲染所用的模板和所用的技术(也就是视图的类型)进行渲染,具体的渲染过程则交由不同的视图自己完成
5、 RequestToViewNameTranslator
initRequestToViewNameTranslator(context),ViewResolver是根据ViewName查找View,但有的Handler处理
完后并没有设置View也没有设置ViewName,这时就需要从request获取ViewName了,如何从request中获取ViewName就是RequestToViewNameTranslator要做的事情了。RequestToViewNameTranslator在Spring MVC容器里只可以配置一个,所以所有request到ViewName的转换规则都要在一个Translator里面全部实现。
6、LocaleResolver
initLocaleResolver(context),解析视图需要两个参数:一是视图名,另一个是Locale。视图名是处理器返回的,Locale是从哪里来的?这就是LocaleResolver要做的事情。LocaleResolver用于从request解析出Locale,Locale就是zh-cn之类,表示一个区域,有了这个就可以对不同区域的用户显示不同的结果。SpringMVC主要有两个地方用到了Locale:一是ViewResolver视图解析的时候;二是用到国际化资源或者主题的时候。
7、ThemeResolver
initThemeResolver(context),用于解析主题。SpringMVc中一个主题对应一个properties文件,里面存放着跟当前主题相关的所有资源、如图片、css样式等。SpringMVC的主题也支持国际化,同一个主题不同区域也可以显示不同的风格。SpringMVC中跟主题相关的类有ThemeResolver、ThemeSource和Theme。主题是通过一系列资源来具体体现的,要得到一个主题的资源,首先要得到资源的名称,这是ThemeResolver的工作。然后通过主题名称找到对应的主题(可以理解为一个配置)文件,这是ThemeSource的工作。最后从主题中获取资源就可以了。
8、 MultipartResolver
initMultipartResolver(context),用于处理上传请求。处理方法是将普通的request包装成
MultipartHttpServletRequest,后者可以直接调用getFile方法获取File,如果上传多个文件,还可以调用
getFileMap得到FileName->File结构的Map。此组件中一共有三个方法,作用分别是判断是不是上传请求,将request包装成MultipartHttpServletRequest、处理完后清理上传过程中产生的临时资源。
9、 FlashMapManager
initFlashMapManager(context),用来管理FlashMap的,FlashMap主要用在redirect中传递参数。
控制器是单例模式。
单例模式下就会有线程安全问题。
Spring中保证线程安全的方法
1、将scop设置成非singleton。prototype,request。
2、最好的方式是将控制器设计成无状态模式。在控制器中,不要携带数据。但是可以引用无状态的service和dao。
Spring中的Bean默认是单例模式的,框架并没有对bean进行多线程的封装处理。
如果Bean是有状态的那就需要开发人员自己来进行线程安全的保证,最简单的办法就是改变bean的作用域把"singleton”改为“protopyte’这样每次请求Bean就相当于是new Bean()这样就可以保证线程的安全了。
有状态就是有数据存储功能
无状态就是不会保存数据 controller、service和dao层本身并不是线程安全的,只是如果只是调用里面的方法,而且多线程调用一个实例的方法,会在内存中复制变量,这是自己的线程的工作内存,是安全的。
Dao会操作数据库Connection,Connection是带有状态的,比如说数据库事务,Spring的事务管理器使用
Threadlocal为不同线程维护了一套独立的connection副本,保证线程之间不会互相影响(Spring是如何保证事务获取同一个Connection的)
不要在bean中声明任何有状态的实例变量或类变量,如果必须如此,那么就使用ThreadLocal把变量变为线程私有的,如果bean的实例变量或类变量需要在多个线程之间共享,那么就只能使用synchronized、lock、CAS等这些实现线程同步的方法了。
1、配置文件配置包扫描路径
2、递归包扫描获取.class文件
3、反射、确定需要交给IOC管理的类
4、对需要注入的类进行依赖注入
优点:
第一,资源集中管理,实现资源的可配置和易管理。
第二,降低了使用资源双方的依赖程度,也就是我们说的耦合度。
缺点:
1、创建对象的步骤变复杂了,不直观,当然这是对不习惯这种方式的人来说的。
2、因为使用反射来创建对象,所以在效率上会有些损耗。但相对于程序的灵活性和可维护性来说,这点损耗是微不足道的。
3、缺少IDE重构的支持,如果修改了类名,还需到XML文件中手动修改,这似乎是所有XML方式的缺憾所在。
单例设计模式
简单工厂设计模式
代理设计模式
模板设计模式
观察者设计模式
适配器设计模式
装饰着设计模式
策略设计模式
联系
@Autowired和@Resource注解都是作为bean对象注入的时候使用的
两者都可以声明在字段和setter方法上
注意:如果声明在字段上,那么就不需要再写setter方法。但是本质上,该对象还是作为set方法的实参,通过执行set方法注入,只是省略了setter方法罢了
区别
@Autowired注解是Spring提供的,而@Resource注解是J2EE本身提供的
@Autowird注解默认通过byType方式注入,而@Resource注解默认通过byName方式注入
@Autowired注解注入的对象需要在IOC容器中存在,否则需要加上属性required=false,表示忽略当前要注入的bean,如果有直接注入,没有跳过,不会报错
spring的传播属性有以下七种:
1、propagation_required:意思是如果存在一个事务,则支持
当前事务,如果没有事务,则开启这个事务。
2、propagation_support:意思是如果存在一个事务就支持当前事务,如果没有事务则不执行事务。
3、propagation_mandatory:如果有事务就执行事务,没有则抛出异常。
4、propagation_requires_new:表示总是开启一个新的事务,如果已经存在一个事务则将这个事务挂起。
5、propagation_not_supported:总是以非事务执行,并挂起任何事物
6、propagation_never:总是非事务执行,如果存在一个事务活动,则抛出异常
7、propagation_nested:如果一个活动的事务存在,则运行在一个嵌套事务当中,如果没有事务,则按照Transaction.propagation_required属性执行。
1 #是将传入的值当做字符串的形式,eg:select id,name,age from student where id =#{id},当前端把id值1,传入到后台的时候,就相当于 select id,name,age from student where id =‘1’.
2 $是将传入的数据直接显示生成sql语句,eg:select id,name,age from student where id =${id},当前端把id值1,传入到后台的时候,就相当于 select id,name,age from student where id = 1.
3 使用#可以很大程度上防止sql注入。(语句的拼接)
4 但是如果使用在order by 中就需要使用 $.
5 在大多数情况下还是经常使用#,但在不同情况下必须使用$.
Cglib和jdk动态代理的区别?
1、Jdk动态代理:利用拦截器(必须实现InvocationHandler)加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理
2、 Cglib动态代理:利用ASM框架,对代理对象类生成的class文件加载进来,通过修改其字节码生成子类来处理
什么时候用cglib什么时候用jdk动态代理?
1、目标对象生成了接口 默认用JDK动态代理
2、如果目标对象使用了接口,可以强制使用cglib
3、如果目标对象没有实现接口,必须采用cglib库,Spring会自动在JDK动态代理和cglib之间转换
JDK动态代理和cglib字节码生成的区别?
1、JDK动态代理只能对实现了接口的类生成代理,而不能针对类
2、Cglib是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,并覆盖其中方法的增强,但是因为采用的是继承,所以该类或方法最好不要生成final,对于final类或方法,是无法继承的
Cglib比JDK快?
1、cglib底层是ASM字节码生成框架,但是字节码技术生成代理类,在JDL1.6之前比使用java反射的效率要高
2、在jdk6之后逐步对JDK动态代理进行了优化,在调用次数比较少时效率高于cglib代理效率
3、只有在大量调用的时候cglib的效率高,但是在1.8的时候JDK的效率已高于cglib
4、Cglib不能对声明final的方法进行代理,因为cglib是动态生成代理对象,final关键字修饰的类不可变只能被引用不能被修改
Spring如何选择是用JDK还是cglib?
1、当bean实现接口时,会用JDK代理模式
2、当bean没有实现接口,用cglib实现
3、可以强制使用cglib(在spring配置中加入
Cglib原理
动态生成一个要代理的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截技术拦截所有的父类方法的调用,顺势织入横切逻辑,它比Java反射的jdk动态代理要快
Cglib是一个强大的、高性能的代码生成包,它被广泛应用在许多AOP框架中,为他们提供方法的拦截
最底层的是字节码Bytecode,字节码是java为了保证依次运行,可以跨平台使用的一种虚拟指令格式
在字节码文件之上的是ASM,只是一种直接操作字节码的框架,应用ASM需要对Java字节码、class结构比较熟悉
位于ASM上面的是Cglib,groovy、beanshell,后来那个种并不是Java体系中的内容是脚本语言,他们通过ASM框架生成字节码变相执行Java代码,在JVM中程序执行不一定非要写java代码,只要能生成java字节码,jvm并不关系字节码的来源
位于cglib、groovy、beanshell之上的就是hibernate和spring AOP
最上面的是applications,既具体应用,一般是一个web项目或者本地跑一个程序、
使用cglib代码对类做代理?
使用cglib定义不同的拦截策略?
构造函数不拦截方法
用MethodInterceptor和Enhancer实现一个动态代理
Jdk中的动态代理
JDK中的动态代理是通过反射类Proxy以及InvocationHandler回调接口实现的,但是JDK中所有要进行动态代理的类必须要实现一个接口,也就是说只能对该类所实现接口中定义的方法进行代理,这在实际编程中有一定的局限性,而且使用反射的效率也不高
Cglib实现
使用cglib是实现动态代理,不受代理类必须实现接口的限制,因为cglib底层是用ASM框架,使用字节码技术生成代理类,你使用Java反射的效率要高,cglib不能对声明final的方法进行代理,因为cglib原理是动态生成被代理类的子类
1、进程管理
操作系统的职能之一,主要是对处理机进行管理。为了提高CPU的利用率而采用多道程序技术。通过进程管理来协调多道程序之间的关系,使CPU得到充分的利用。
2、内存管理
内存管理是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。
3、文件系统
文件系统是操作系统用于明确存储设备(常见的是磁盘,也有基于NAND Flash的固态硬盘)或分区上的文件的方法和数据结构;即在存储设备上组织文件的方法。
4、网络通信
网络是用物理链路将各个孤立的工作站或主机相连在一起,组成数据链路,从而达到资源共享和通信的目的。
5、安全机制
安全描述符、访问控制列表、访问令牌、访问掩码等。
6、用户界面
用户界面(User Interface,简称 UI,亦称使用者界面[1])是系统和用户之间进行交互和信息交换的媒介,它实现信息的内部形式与人类可以接受形式之间的转换。用户界面是介于用户与硬件而设计彼此之间交互沟通相关软件,目的在使得用户能够方便有效率地去操作硬件以达成双向之交互,完成所希望借助硬件完成之工作,用户界面定义广泛,包含了人机交互与图形用户接口,凡参与人类与机械的信息交流的领域都存在着用户界面。
7、驱动程序
驱动程序一般指的是设备驱动程序(Device Driver),是一种可以使计算机和设备通信的特殊程序。相当于硬件的接口,操作系统只有通过这个接口,才能控制硬件设备的工作,假如某设备的驱动程序未能正确安装,便不能正常工作。
因此,驱动程序被比作“ 硬件的灵魂”、“硬件的主宰”、和“硬件和系统之间的桥梁”等。
一、进程、线程、协程的概念
进程:
是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
线程:
是进程的一个执行单元,是进程内科调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。
协程:
是一种比线程更加轻量级的存在。一个线程也可以拥有多个协程。其执行过程更类似于子例程,或者说不带返回值的函数调用。
二、进程和线程的区别
地址空间:
线程共享本进程的地址空间,而进程之间是独立的地址空间。
资源:
线程共享本进程的资源如内存、I/O、cpu等,不利于资源的管理和保护,而进程之间的资源是独立的,能很好的进行资源管理和保护。
健壮性:
多进程要比多线程健壮,一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。
执行过程:
每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口,执行开销大。
但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,执行开销小。
可并发性:
两者均可并发执行。
切换时:
进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
其他:
线程是处理器调度的基本单位,但是进程不是。
三、协程和线程的区别
协程避免了无意义的调度,由此可以提高性能,但程序员必须自己承担调度的责任。同时,协程也失去了标准线程使用多CPU的能力。
线程
相对独立
有自己的上下文
切换受系统控制;
协程
相对独立
有自己的上下文
切换由自己控制,由当前协程切换到其他协程由当前协程来控制。
四、何时使用多进程,何时使用多线程?
对资源的管理和保护要求高,不限制开销和效率时,使用多进程。
要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。
五、为什么会有线程?
每个进程都有自己的地址空间,即进程空间,在网络或多用户换机下,一个服务器通常需要接收大量不确定数量用户的并发请求,为每一个请求都创建一个进程显然行不通(系统开销大响应用户请求效率低),因此操作系统中线程概念被引进。
进程通信:
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。
1 匿名管道通信
匿名管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
通过匿名管道实现进程间通信的步骤如下:
父进程创建管道,得到两个⽂件描述符指向管道的两端
父进程fork出子进程,⼦进程也有两个⽂件描述符指向同⼀管道。
父进程关闭fd[0],子进程关闭fd[1],即⽗进程关闭管道读端,⼦进程关闭管道写端(因为管道只支持单向通信)。⽗进程可以往管道⾥写,⼦进程可以从管道⾥读,管道是⽤环形队列实现的,数据从写端流⼊从读端流出,这样就实现了进程间通信。
详细可参考文章:进程间的通信方式——pipe(管道)
2 高级管道通信
高级管道(popen):将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。
3 有名管道通信
有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
4 消息队列通信
消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
5 信号量通信
信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
6 信号
信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
7 共享内存通信
共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
8 套接字通信
套接字( socket ) : 套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
select: 时间复杂度O(n),它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部) select具有O(n)的无差别轮询复杂度, 同时处理的流越多,无差别轮询时间就越长。遍历 ; 有最大连接数的限制,在FD_SETSIZE宏定义
poll:时间复杂度O(n),poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
epoll: 时间复杂度O(1),epoll可以理解为event poll,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的 ;虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接
虚拟内存别称虚拟存储器(Virtual Memory)。电脑中所运行的程序均需经由内存执行,若执行的程序占用内存很大或很多,则会导致内存消耗殆尽。为解决该问题,Windows中运用了虚拟内存技术,即匀出一部分硬盘空间来充当内存使用。当内存耗尽时,电脑就会自动调用硬盘来充当内存,以缓解内存的紧张。若计算机运行程序或操作所需的随机存储器(RAM)不足时,则 Windows 会用虚拟存储器进行补偿。它将计算机的RAM和硬盘上的临时空间组合。
采用补码可以简化计算机硬件电路设计的复杂度 。 对于有符号数,内存要区分符号位和数值位,要是能把符号位和数值位等同起来,让它们一起参与运算,不再加以区分,只用加法器就可以同时实现加法和减法运算,这样硬件电路就变得简单了。
内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。
用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。
为什么要有用户态和内核态?
由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级 – 用户态和内核态。
(1) 硬中断
由与系统相连的外设(比如网卡、硬盘)自动产生的。主要是用来通知操作系统系统外设状态的变化。比如当网卡收到数据包
的时候,就会发出一个中断。我们通常所说的中断指的是硬中断(hardirq)。
(2) 软中断
为了满足实时系统的要求,中断处理应该是越快越好。linux为了实现这个特点,当中断发生的时候,硬中断处理那些短时间
就可以完成的工作,而将那些处理事件比较长的工作,放到中断之后来完成,也就是软中断(softirq)来完成。
buffer(缓冲)是为了提高内存和硬盘(或其他I/0设备)之间的数据交换的速度而设计的。
cache(缓存)是为了提高cpu和内存之间的数据交换速度而设计,也就是平常见到的一级缓存、二级缓存、三级缓存。
内部由ALU(算术逻辑单元)、CU(控制器)、寄存器(PC、IR、PSW、DR、通用寄存器等)、中断系统组成,
外部通过总线与控制总线、数据总线、地址总线进行相连,对数据和程序进行相关的操作。
进程管理模块
它是进程实体的一部分,是操作系统中最重要的记录性数据结构。它是进程管理和控制的最重要的数据结构,每一个进程均有一个PCB,在创建进程时,建立PCB,伴随进程运行的全过程,直到进程撤消而撤消。
PCB一般包括:
1.程序ID(PID、进程句柄):它是唯一的,一个进程都必须对应一个PID。PID一般是整形数字
2.特征信息:一般分系统进程、用户进程、或者内核进程等
3.进程状态:运行、就绪、阻塞,表示进程现的运行情况
4.优先级:表示获得CPU控制权的优先级大小
5.通信信息:进程之间的通信关系的反映,由于操作系统会提供通信信道
6.现场保护区:保护阻塞的进程用
7.资源需求、分配控制信息
8.进程实体信息,指明程序路径和名称,进程数据在物理内存还是在交换分区(分页)中
9.其他信息:工作单位,工作区,文件信息等
区别:
1、含义不同:
B是英文单词“Browser”的首字母,即浏览器的意思;S是英文单词“Server”的首字母,即服务器的意思。B/S就是“Browser/Server”的缩写,即“浏览器/服务器”模式。
C是英文单词“Client”的首字母,即客户端的意思,C/S就是“Client/Server”的缩写,即“客户端/服务器”模式。
2、硬件环境不同:
C/S通常是建立在专用的网络上,小范围的网络环境。而B/S是建立在广域网上的,适应范围强,通常有操作系统和浏览器就行;C/结构比B/S结构更安全,因为用户群相对固定,对信息的保护更强;B/S结构维护升级比较简单,而C/S结构维护升级相对困难。
3、结构不同:
B/S结构是随着互联网的发展,web出现后兴起的一种网络结构模式。这种模式统一了客户端,让核心的业务处理在服务端完成。
C/S结构是一种软件系统体系结构,也是生活中很常见的。这种结构是将需要处理的业务合理地分配到客户端和服务器端,这样可以大大降低通信成本,但是升级维护相对困难。
联系:
B/S结构可以在任何地方进行操作而不用安装任何专门的软件。B/S架构的软件只需要管理服务器就行了,所有的客户端只是浏览器,根本不需要做任何的维护。
只要有一台能上网的电脑就能使用,客户端零维护。而且系统的扩展非常容易,只要能上网,再由系统管理员分配一个用户名和密码,就可以使用了。