收录有:Java+ 计算机基础 + 数据库 + 常用框架 + 中间件 + 开发工具 + 项目 等
逐渐完善,慢慢积累。部分图片来源于网络,侵删。
答案为本人基于自己的理解,如有大佬认为不足可评论区指正
源代码-> 词法分析器 -> 语法分析器 -> 语义分析器 -> 字节码生成器
保证Java一次编译到处运行,屏蔽了机器底层机器码。保证Java不面向任何的处理器而只是面向于虚拟机。
String a1 = "";
String b1 = "";
here:
for (int i = 1; i <= 4; i++) {
a1 = "外层循环第"+i+"层";
for (int j = 1; j <= 4; j++) {
b1 = "内层循环第"+j+"层";
if (2 == j & 2 == i) {
break here;
}
}
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
首先这个方法的返回值还是一个哈希值。为什么不直接返回key.hashCode()
呢?还要与 (h >>> 16)异或。先看一个例子:
h = key.hashcode() 1111 1101 1101 1111 0101 1101 0010 1111
^
h >>> 16 0000 0000 0000 0000 1111 1101 1101 1111
----------------------------------------------------------
h ^ (h >>> 16) 1111 1101 1101 1111 1010 0000 1111 0000
h = key.hashcode() 1111 1101 1101 1111 0101 1101 0010 1111
对比h = key.hashcode()
与 h ^ (h >>> 16)
发现,将h无符号右移16位相当于将高区16位移动到了低区的16位,再与原hashcode
做异或运算,可以将高低位二进制特征混合起来。例子中可以看出高区的16位并没有变化。低区的16位发 生了较大的变化。这样做的目的是什么呢?
我们计算出的hash值在后面会参与到元素index的计算中。计算公式为 hash & (length - 1)。
仔细观察上文不难发现,高区的16位很有可能会被数组槽位数的二进制码锁屏蔽,如果我们不做刚才移位异或运算,那么在计算index时将丢失高区特征。如果没有上面这个异或操作,假设里两个hash值只有高位一点点的差异,然后在计算index过程中还丢失了高位的信息,那么就计算出同一个index。这也是将性能做到极致的一种体现!!!
异或运算能更平均的保留各部分的特征,如果采用 & 运算计算出来的值会向1靠拢,采用 | 运算计算出来的值会向0靠拢
为了让hash后的结果更加均匀
如果 length = 17 那么 hash & (17 - 1) 。16转化为二进制包含更多的0,这样一来计算会被更多的0屏蔽。
便于扩容后的重新计算index。
由于我们要维护hashmap的大小为2^n,这样就使得len-1的二进制中全部都是1。进行位运算时可以降低hash碰撞的出现。
负载因子主要与扩容有关,如果将负载因子设置为1,空间利用的就更加充分了,但是这样一来会增大hash碰撞的出现,有些位置的链表会过长,不利于查找。如果设置的过小的话虽然降低了hash碰撞的发生,但是会频繁触发扩容机制。所以为了折中,应用泊松定理,将负载因子设置为0.75是对空间与时间的取舍。
1.多线程的put操作可能导致元素丢失
2.put和get并发时可能导致get为null
3.jdk 1.7 中并发put导致的循环链表导致get出现死循环
不能,只是能降低冲突的概率,完全解决冲突是不可能的。
1.拉链法(链地址):Java中的HashMap在存储数据的时候就是用的拉链法来实现的,拉链发就是把具有相同散列地址的关键字 (同义词)值放在同一个单链表中,称为同义词链表。
2.线性探测法:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。
3.二次探测法:冲突发生时,在表的左右进行跳跃式探测,比较灵活。
共享内存法
volatile,synchronized
wait/notify机制
来自Object类的方法。当满足某种情况时A线程调用wait()方法放弃CPU时间片,并进入阻塞状态。当满足某种条件时,B线程调用notify()方法通知A线程。唤醒A线程,并让它进入可运行状态。
Lock/Condition机制
Condition是Java提供了来实现等待/通知的类,Condition类还提供比wait/notify更丰富的功能,Condition对象是由lock对象所创建的。但是同一个锁可以创建多个Condition的对象,即创建多个对象监视器。这样的好处就是可以指定唤醒线程。notify唤醒的线程是随机唤醒一个。
volatile为什么会出现:(字节跳动)
首先先分析一下没有volatile的情况下线程在自己的私有内存中对共享变量做出了改变之后无法及时告知其他线程,这就是volatile的作用,解决内存可见性问题。这种问题用synchronized关键字可以解决。但是一个问题是synchronized是重量级锁,同一时间内只允许一个线程去操作共享变量。操作完成之后在将改变后的变量值刷新回共享内存空间中。这样一来的话并发性就没有了。而且synchronized关键词的使用基于操作系统实现,会使得线程从用户态陷入内核态。这一步是很耗时间的。于使volatile应运而生。它是一个轻量级的synchronized。只是用来解决内存可见性问题的。
变量被volatile关键字修饰后,底层汇编指令中会出现一个lock前缀指令。会导致以下两种事情的发生:
指令重排序:编译器在不改变单线程程序语义的前提下,重新安排语句的执行顺序,指令重排序在单线程下不会有问题,但是在多线程下,可能会出现问题。
volatile有序性的保证就是通过禁止指令重排序来实现的。指令重排序包括编译器和处理器重排序,JMM会分别限制这两种指令重排序。禁止指令重排序又是通过加内存屏障实现的。
// 内存屏障(memory barriers):一组处理器指令,用于实现对内存操作的顺序限制。
添加了volatile关键字可以避免半初始化的指令重排。
java中只有对变量的赋值和读取是原子性的,其他的操作都不是原子性的。所以即使volatile即使能保证被修饰的变量具有可见性,但是不能保证原子性。
闭锁可以用来确保某些活动直到其他活动全部结束之后才进行;
主要包含两个方法,一个是countDown()
,一个是await()
;以及一个计数器变量cnt
。countDown()
方法用来给计数器cnt
减一;
await()
方法是用来阻塞当前线程,直到计数器为0的时候在唤醒线程继续执行;
信号量,用于多个共享资源的互斥使用,也可以用来控制线程的并发量,类似于线程池的作用。
可以用于限制线程的并发数。
又叫线程本地变量、或线程本地存储。
作用:
ThreadLocal
为解决多线程下的线程安全问题提供了一个新思路,它通过为每一个线程提供一个独立的变量副本解决了线程并发访问共享变量出现的安全问题。在很多情况下ThreadLocal
比直接使用synchronized同步机制解决线程安全问题更加方便、简洁。且拥有更加高的并发性。
原理:
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
ThreadLocal.ThreadLocalMap
类型的成员变量threadLocals
,这个threadLocals
就是用来存储实际的变量副本的,键值为当前ThreadLocal
的引用,value为变量副本(即T类型的变量)。threadLocals
为空,当通过ThreadLocal
变量调用get()方法或者set()方法,就会对Thread类中的threadLocals
进行初始化,并且以当前ThreadLocal
对象引用为键值,以ThreadLocal
要保存的副本变量为value,存到threadLocals
。threadLocals
里面查找。ThreadLocal
内存泄漏
由于ThreadLocalMap
的key是弱引用,而Value
是强引用。这就导致了一个问题,ThreadLocal
在没有外部对象强引用时,发生GC时(无论是否OOM)弱引用Key会被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key
的情况,外部读取ThreadLocalMap
中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap
对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value
,这条强引用链会导致Entry
不会回收, Value
也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。
解决办法:每次使用完ThreadLocal
,都调用它的remove()
方法,清除数据。
ThreadLocal
应用场景
最常见的ThreadLocal
使用场景为 用来解决 数据库连接、Session管理等。
分为 线程私有部分与线程共有部分。
私有部分:虚拟机栈、本地方法栈(JNI)、程序计数器。
共有部分:方法区、堆(年轻代、老年代)。
标记清除:会产生大量的内存碎片
复制算法:用于新生代的垃圾回收。不会产生内存碎片,但是会浪费约10%的内存空间。
标记压缩(清除):用于老年代的垃圾回收。耗时较长,但是不会产生内存碎片也不会浪费空间。
分代收集:根据对象的存活周期的不同将内存划分为几块
serial:年轻代单线程串行垃圾收集器
CMS:老年代多线程并行垃圾收集器
缺点是 由于以最短停顿为目标,所以会导致吞吐量低。标记清除算法带来的内存碎片问题。
G1:填补了CMS的不足,是当前服务端最优的垃圾收集器。通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
**初始标记(STW)、并发标记、最终标记(STW)、筛选清除。**G1收集过程中整体来看基于标记整理算法,局部来看基于复制算法。所以在收集垃圾过程中不会产生垃圾碎片。
AQS(AbstractQueuedSynchronizer)
是J.U.C
包下lock实现的核心。主要是其提供的一个FIFO的队列来维护获取锁失败而进入阻塞的线程,以及一个volatile关键字修饰的state变量表示当前同步状态。当一个线程获取到同步状态(修改state=1),那么其他线程便无法获取,转而被构造成节点并加入同步队列中。加入队列的过程基于CAS算法。即比较当前线程认为的尾节点与当前节点,比较成功后才能正式加入队列尾部。队列头节点表示的为当前正在运行的线程,该线程执行结束后会激活它下面的一个线程进入执行状态。
FIFO同步队列控制并发。
注意区分对象创建问题与类加载过程问题!
/**
单例设计模式---饿汉
缺点:上来就初始化了一个对象,浪费资源
**/
public class test {
// 静态的对象实例
private static test instance = new test();
private test() {};
// 供外界调用的获取实例的方法
public static getInstance () {
return instance;
}
}
/**
单例设计模式---懒汉 解决了浪费资源的问题但是又带来了线程不安全问题。
**/
public class test {
private static test instance;
private test() {};
public static getInstance () {
if (instance == null) {
instance = new test();
}else {
return instance;
}
}
}
// 单例设计模式---懒汉 加锁保证线程安全
public class test {
private static test instance;
private test() {};
// synchronized 保证线程安全,缺点锁粒度太大,影响性能
public static synchronized getInstance () {
if (instance == null) {
instance = new test();
}else {
return instance;
}
}
}
// 单例设计模式---懒汉 加锁 并双重锁定检查(Double Check Lock){DCL单例}
// 注意:多线程环境下的指令重排可能会产生问题
public class test {
private static volatile test instance;
private test() {};
// synchronized 保证线程安全,缺点锁粒度太大,影响性能
public static getInstance () {
if (instance == null) { //第一次检查
synchronized (test.class) {
if (instance == null) { // 第二次检查
instance = new test();
}
}
}else {
return instance;
}
}
}
volatile两个作用:保持线程可见性;禁止指令重排序。
DCL单例需要加volatile,来禁止指令重排。
由于由于java编译器允许处理器乱序执行(以获得最优的性能),new对象的操作不是原子性的。这句代码最终会被编译成多条汇编指令。所以需要volatile关键字来禁止指令重排。
创建一个对象的过程中一旦出现了指令重排,可能就会获得半初始化的对象,即还没来得及赋值就先建立了引用关系。要避免这种情况的发生就要使用volatile关键字修饰实列变量。
mark word
与 class pointer
组成对象头。 class pointer
指向对象的类。padding
作用是将对象在内存中占用的字节数补齐到被8整除。(64位OS下)
主要包括锁的信息,有一个锁升级的过程。还记录了对象的年龄,四位二进制,超过16岁进入老年代。以及类指针。
在synchronized大(优化)升级之前,是重量级锁,锁操作都要经过OS。向OS内核去申请。(jdk1.5之后
)到现在的synchronized是有一个复杂的锁升级过程。
无锁 -> 偏向锁 -> 自旋锁(轻量级锁) -> (重量级锁)悲观锁。
以上的升级状态都记录在对象头中。
**偏向锁:**hotspot虚拟机认为大多数时间是不存在锁竞争的,所以每次都会把锁分配给上一次获得锁的线程,直到出现了锁竞争。
**自旋锁:**线程之间以CAS的方式进行锁资源的争抢。当一个线程自旋超过了10次或者当前自旋等待的线程超过了CPU核数的1/2(升级后优化为自适应自旋),会进行锁升级。
synchronized: 向OS申请资源,从用户态切换到内核态。线程挂起进入等待队列,等待OS的调度。然后再映射回用户空间。
hotspot使用的是直接指针方式。
栈上分配:需要满足标量替换以及无逃逸(在一个方法中使用)。比在堆中分配时间快一倍。且无需GC回收。方法执行结束自动出栈。
堆中分配:无法进行栈上分配:判断对象个头,大对象直接入老年代。否则在伊甸区分配。伊甸区分配前先判断是否符合线程本地分配(由于线程争先恐后的在内存中分配,会加锁,效率不高,所以JVM做了一级优化,直接将对象分配到线程的私有空间中,这一操作不需要锁)
具体过程如下图所示:
Object o = new Object();
// o普通对象指针(Oops)4 字节(开启压缩占 4 字节,没开启占 8 字节),object对象占 16 字节
IPC
state
状态值使用volatile
修饰保证内存的可见性。因为涉及到多线程对state的修改,必须保证其对所有线程的可见性。synchronized
是依靠jvm
以及配合操作系统来实现,是一个关键字。reentrantLock
是jdk1.5
之后提供的API层面的互斥锁。synchronized
只需要添加上相关关键字即可,加锁与释放过程由操作系统完成。reentrantLock
则需要手动加锁与释放锁。synchronized
优化之后性能与reentrantLock
已经不相上下了,官方甚至更建议使用synchronized
关键字。reentrantLock
要强于synchronized
ReentrantLock
提供了三个高级功能:
lock.lockInterruptibly()
来实现这个机制。ReentrantLock
默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。ReentrantLock
对象可以同时绑定对个对象。ReenTrantLock
提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。要说清楚锁升级的过程。
每个对象(在对象头中)有一个监视器锁(monitor)
,当monitor被占用时就处于锁定状态。线程执行monitorenter
(汇编指令)尝试获取monitor的所有权。
底层字节码被编译成monitorenter
和monitorexit
两个指令。线程执行monitorexit
指令,monitor计数器减1,如果减到0了,表示当前线程不在拥有该监视器锁。等待队列中的线程有机会获得锁资源。
这个锁就是一个引用对象,也就是要加锁或者解锁的对象。
如果指明了锁对象如Synchronized(this) 则对this对象进行加锁
如果直接在方法上添加Synchronized,则锁定的是该方法所在对象
如果是对静态方法使用Synchronized,则是对静态方法所对应的类对象加锁
对一个对象加锁不影响对该对象其他方法的使用
重入性就是在一个同步方法中调用另一个同步方法,主要是为了防止自己把自己锁死的情况发生。
jvm
对于重入锁的操作也很简单,在执行 monitorenter
指令时,如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁(而不是已拥有了锁则不能继续获取),就把锁的计数器 +1,其实本质上就通过这种方式实现了可重入性。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。
非公平是指在获取锁的行为上,并不是按照线程申请顺序进行分配的,当锁被释放后,所有线程都有机会获取到锁,这样提高了性能,但是可能会出现某些线程饥饿的情况。
当 Synchronized升级为重量级锁时,他是一个悲观锁。获取不到锁资源线程的线程由OS统一管理,涉及到用户态到内核态的切换。
乐观锁就是,当一个线程想要对变量进行操作时,先读取变量值,然后真正更改时会再次对当前值与自己之前读取的值是否相同,相同才会进行更改,不相同的话就会再次读取,然后在进行对比更改。主要是基于CAS实现。
CAS(compare and swap) :它涉及到3个操作数:1.内存值,预期值, 新值,只有当内存值和预期值相等的时候(证明没有其他线程在使用),才会将内存值设置为预期值。
CAS具有原子性,他的原子性由CPU保证,由JNI调用c++硬件代码实现,jdk
中提供了unsafe来进行这些操作。
不一定
乐观锁的情况下,如果线程并发度确实很高,那么大多数的线程都会处于自旋等待以获取锁对象的状态。这样会导致CPU占用过高。
CAS另一个缺点就是ABA问题。一个值从A改为B又改为A,则CAS认为没有发生变化,解决的方式是使用版本号来记录操作次数。
ReentrantLock的可重入功能基于AQS的同步状态:state。 其原理大致为:当某一线程获取锁后,将state值+1,并记录下当前持有锁的线程,再有线程来获取锁时,判断这个线程与持有锁的线程是否是同一个线程,如果是,将state值再+1,如果不是,阻塞线程。
AQS框架是用来构建锁的同步器框架,包括了常用的ReentrantLock
,ReadWriteLock
,CountDownLatch
等都是基于AQS框架来实现的。
AQS使用一个FIFO队列表示排队等待锁的线程,队列头结点称作“哨兵节点”或者“哑结点”,它不与任何线程关联。其他的节点与等待线程关联,每个阶段维护一个等待状态waitStatus。
AQS中有一个表示状态的字段state,例如ReentrantLock
用它来表示线程重入锁的次数,Semphore
用它表示剩余的许可数量,FutureTask
用它表示任务的状态。对state变量值的更新都采用CAS操作保证更新操作的原子性。
ReentrantLock
内部持有了一个sync对象,这个对象实现了AQS,并且加锁的时候使用CAS算法,在所对象申请的时候,在锁等待node链表中查看当前申请的锁的对象是否是同一个对象,如果是的话,进行重入。
CountDownLatch
CyclicBarrier
Semaphore
等更加高级的同步框架java线程池中的对象被抽象成work对象,基于AQS,存放在线程池中的HashSet中,等待执行的任务则存放在成员变量workQueue中
不是
execute():无返回值
submit():返回Future对象。可以通过get()方法获取返回值。如果线程没有执行完成会阻塞。
直接使用InvocationHandler
接口进行实现,同时利用Proxy类设置动态请求对象;使用CGLIB来避免对于代理设计模式需要使用接口实现的限制。
HashMap
底层是一个Entry数组,当发生hash冲突的时候,hashmap
是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入(头插法)。多线程环境下执行插入操作时,可能会发生多个线程同时获取了链表的头节点。可能会造成线程写入的操作被覆盖。
HashMap
:线程不安全,ConcurrentHashMap:
线程安全。JDK1.7之前由分段锁(继承了可重入锁)实现。JDK1.8之后由CAS+Synchronized实现。一种可以实现LRU的数据结构。是有序的HashMap
。
GC Roots
为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。JNI
中引用的对象jdk 1.8
元空间实现)的内存。启动类加载器、扩展类加载器、应用程序类加载器
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。向上委派,向下查找
优点:
API
被破坏。以双亲委任制的方式去加载class文件。
垃圾回收器不会马上释放内存空间,而是在某个对象被标记为垃圾之后的下一次GC时才会对内存空间进行释放。
我们可以手动执行System.gc()
,通知虚拟机进行GC,但是Java语言规范并不保证GC一定会执行。
浅拷贝:原始对象的引用与副本对象的引用指向堆中的同一个对象。
深拷贝:将原始对象完整复制一份放到堆内存中,原始对象的引用与副本对象的引用指向堆中的不同对象。
Object 类提供的 clone( ) 只能实现浅拷贝。
调用System.gc()
方法会通知jvm
进行垃圾回收,但是并不保证一定会垃圾回收。
每个 Java 应用程序都有一个 Runtime
类实例,使应用程序能够与其运行的环境相连接。可以通过 getRuntime
方法获取当前运行时。 Runtime.getRuntime().gc()
。Runtime.gc()
与System.gc()
并没有实质性的区别,唯一的区别就是前者比较好写一点儿。
当一个对象不再被引用时,GC过程中会调用该对象的finalize() 方法(继承自Object类)对该对象进行回收。
会,主要是对常量池以及类的卸载。对类的卸载需要满足三个条件。
ClassLoader
已经被回收。Cloneable标识一个类可以被克隆,Serializable标识一个类可以被序列化。
集合类没有实现这两个接口,但是集合类的具体实现类实现了这两个接口。集合类接口不是具体的容器,所以不需要实现这两个接口,没有任何意义。
Iterator可以用来遍历Set、List。 ListIterator只能用来遍历List 。
ListIterator实现了Iterator,在Iterator的基础上有了更强大的功能,比如增加、替换元素。还支持向前遍历。
atomic 主要利用 CAS (Compare And Swap) 和 volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
装饰模式和适配器模式
不会,其他线程不受主线程结束的影响。
单次GC的时间其实是不可控的,但是取了平均值,GC就可以动态去调整heap的大小,或者其他的一些GC参数,从而保证每次GC的时间不会超过这个平均值。
IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性的复制到另外一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性的复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
虚拟机栈空间以栈帧为基本单位
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟街运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。每一个方法从调用开始到执行完成的过程,都是一个栈帧在虚拟机栈里面从出栈到入栈的过程。
不同的引用类型,主要体现的是对象不同的可达性状态和对垃圾收集的影响。
强、软、弱、虚
Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java
程序在各种平台下都能达到一致的内存访问效果。
查找关键报错信息,确定是StackOverflowError还是OutOfMemoryError
如果是StackOverflowError,检查代码是否递归调用方法等。
如果是OutOfMemoryError,检查是否有死循环创建线程等,通过-Xss降低的每个线程栈大小的容量。
利用异或性质。
public class 交换两个变量 {
public static void main(String[] args) {
int a = 2;
int b = 3;
a = a ^ b;
b = a ^ b;
a = a ^ b;
System.out.println(a);
System.out.println(b);
}
}
System.gc()
方法的调用,因为该方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存。java.lang.OutOfMemoryError:
Java heap spaceJUC包下的原子类都是基于CAS操作的。JAVA中的CAS操作都是通过sun包下Unsafe类实现,而Unsafe类中的方法都是native方法,由JVM本地实现。Unsafe中对CAS的实现是C++写的,从上图可以看出最后调用的是Atomic:comxchg这个方法,这个方法的实现放在hotspot下的os_cpu包中,说明这个方法的实现和操作系统、CPU都有关系。
基于性能考虑,核心线程数的设置与日常流量有关。最大线程数与最大峰值流量(如秒杀场景下) 有关,超过最大线程数后反而会导致机器的性能变低。并且合理的设置阻塞队列的长度。
阻塞队列的几种实现:
LinkedBlockingQueue
SynchronousQueue
ArrayBlockingQueue;
根据线程池的运行原理,如果选用了无界的等待队列,那么会导致最大线程数参数失效。因为队列永远不会装满,我们的服务器面对高并发时也就无法发挥最优性能。
与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。
线程池这样设计实际上是构建了一个生产者消费者模型,它将线程和任务两者解耦,从而良好的缓冲任务,复用线程。线程池的运行分为两大部分,任务管理、线程管理。
任务管理充当生产者,当任务提交后,线程池会判断该任务后续的流转:(1)直接申请线程执行该任务;(2)缓冲到队列中等待线程执行;(3)拒绝该任务。
线程管理充当消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。
阿姆达尔定律:
设置的线程数 = CPU 核数 * (1 + IO time / CPU computing time)
举例说明,假设4核 CPU,每个任务中的 IO 任务占总任务的80%,CPU时间占用20%。则线程数应设置为:4 * (1 + 4) = 20个线程,这里的20个线程对应的是4核心的 CPU。
队列大小 = 线程数 * (目标相应时间/任务实际处理时间)等待队列一定要使用有界队列,否则会拖垮整个系统。
半解释半编译。首先由Javac将Java文件编译为class文件,然后由jvm解释执行。
首先两者继承自Throwable类,Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。 Error 是指程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。
异常分为 运行时异常 与 其他异常。
IOException
使用 try catch finally 进行处理java语言本身是静态类型的语言,但是由于有了反射机制使得java具有了一定的动态特性。
反射机制是 Java 语言提供的一种基础功能,赋予程序在运行时自省(introspect,官方用语)的能力。通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。
反射是动态代理的基础,动态代理提供了运行时的代理模式。是Spring AOP的实现方式。
LinkedHashMap
通常提供的是遍历顺序符合插入顺序,它的实现是通过为条目(键值对)维护一个双向链表。注意,通过特定构造函数,我们可以创建反映访问顺序的实例,所谓的 put、get 都算作访问。
对于 TreeMap
,它的整体顺序是由键的顺序关系决定的,通过 Comparator 或Comparable(自然顺序)来决定。
此为JDK1.8之前:
- 抽象类中可定义构造函数,接口中不可
- 抽象类中可有抽象方法和具体方法,接口中只有抽象方法
- 抽象类的成员权限可为 public、默认、protected,而接口成员只可为public
- 抽象类中可包含静态方法,接口中不可包含
JDK1.8之后,接口中加入了 default,即默认方法,允许接口中可有具体方法;可包含静态方法但是不包含静态代码块
共享内存:两个进程通过对一块共享空 间的访问实现通信。各进程对共享空间的访问是互斥的。又可以细分为基于数据结构(共享空间放一个长度为10的数组,这种共享方式比较慢,是一种低级的通信方式)、基于存储区(在内存中划一块共享储存区,数据的形式、存放位置都由进程控制,而不是OS,相比之下这种共享方式更快,是一种高级通信方式)。
**共享内存通信的优缺点:**可以解决消息队列通信带来的数据拷贝带来的开销问题。
消息队列:进程间的数据交换以格式化信息为单位,进程通过OS提供的发送消息/接收消息两个原语进行数据交换。消息队列是保存在内核中的消息链表。
直接通信方式:消息直接挂到接收进程的消息缓冲队列上
间接通信方式:消息要先发送到中间实体(信箱)中。
消息队列通信的优缺点:
首先解决了管道通信的不适合进程频繁通信的问题。但是它的缺点是:一,通信不及时。二,附件有大小的限制。三,通信过程中会存在着用户态与内核态之间的数据拷贝带来的开销。
管道/匿名管道通信:管道是指用于连接读写进程的一个共享文件,又名pipe,其实就是内存中开辟的一个大小固定的缓冲区。单管道只能进行半双工通信。Linux中管道符为 | 。
管道通信的优缺点:
管道通信的效率低,不适合进程间频繁的交换数据,好处是简单,我们很容易的就可以知道管道中的数据被另一个进程读取。
有名管道:匿名管道由于没有名字,只能用于亲缘关系的进程间通信,有名管道严格遵循先进先出,以磁盘方式存在,实现本机任意两进程通信
信号:信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生
信号量:信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间的同步,主要解决与同步相关的问题并避免竞争条件
Socket:前面提到的管道、消息队列、共享内存、信号量和信号都是在同一台主机上进行进程间通信,那要想跨网络与不同主机上的进程之间通信,就需要 Socket 通信了。
现在大多数都采用了虚拟内存技术,所以需要页面置换算法。在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断(要访问的资源不存在,由用户态陷入内核态调用相关资源,是一种内中断)从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。
最佳置换算法(OPT):是一种仅存在于理论中的算法,因为无法得知哪一个页面是最长时间没有被访问的。
先进先出置换算法(FIFO):把调入内存的页面根据调入的先后顺序排成一个队列,队列的大小为OS为进程分配的多少个内存块。需要置换的时候将第一个进入队列的页调出。该算法并不符合实际的运行规律。
最近最久未使用置换算法(LRU):每次淘汰的是最近最久未使用的页面。页表中用访问字段记录该页面自上次访问以来所经历的时间t,当需要淘汰一个页面时,选择现有页中t值最大的。算法性能较好,但是开销较大
时钟置换算法(CLOCK):时钟置换算法是一种兼顾性能与开销的算法。
实现过程:
- 为页面设置一个访问位,然后将内存中的页面通过指针连结成一个循环队列。
- 当某个页被访问时,访问位置1。当需要淘汰一个页时,只需要检查访问位,0则换出;如果是1则置0,暂不换出,继续检查下一个页面访问位。最多经过两轮检查会找到一个淘汰页面。
为了维护OS安全,设置了特权级概念。运行于不同特权级的进程所拥有的权限也不同。大部分进程都是运行在用户态的。只有当需要完成一些自己本身权限无法完成的业务时,会通过系统调用切换到内核态来让操作系统帮忙执行。
为什么需要进程调度?由于cpu资源的有限性,导致需要一套完整的算法来对进程做一个管理。
最短剩余时间优先 :是最短作业优先的抢占版本,按照剩余运行时间进行调度。当一个新作业到达时,其整个的运行时间与当前运行进程的剩余时间对比。如果新的进程所需时间更少会将当前运行进程挂起,运行新的进程。
时间片轮转算法(RR,Round-Robin):每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。让就绪进程以FCFS 的方式按时间片轮流使用CPU 的调度方式,即将系统中所有的就绪进程按照FCFS 原则,排成一个队列,每次调度时将CPU 分派给队首进程,让其执行一个时间片,时间片的长度从几个ms
到几百ms
。在一个时间片结束时,发生时钟中断,调度程序据此暂停当前进程的执行,将其送到就绪队列的末尾,并通过上下文切换执行当前的队首进程,进程可以未使用完一个时间片,就出让CPU(如阻塞)。
多级反馈队列(Multilevel Feedback Queue) :如果一个进程需要执行100个时间片,如果采用时间片轮转算法则需要交换100次,多级队列专门为了这种需要连续执行多个时间片的进程考虑。它设置多个队列。每个队列时间片的大小也不相同。假设现在有四个队列。时间片分别为 1、2、4、8那么一个需要100时间片的进程仅需要交换7次(100/(1+2+4+8)= 7)。
进程在队列1中执行结束后进入队列2…依次直到进程耗尽时间片。最上面的队列优先级最高,只有当上一个队列没有进程排队,才能调度当前队列的进程。
进程同步:多个进程按照一定的顺序执行。
多个进程在运行过程中因资源争夺而造成的一种僵局。
解决死锁方法多角度分析,有 预防、避免、检测 和 解除
死锁的预防:只要破坏四个必要条件中的任何一个就能够预防死锁的发生。
1、静态分配策略:可以破坏死锁产生的第二个条件(占有并等待)。所谓静态分配策略,就是指一个进程必须在执行前就申请到它所需要的全部资源,并且知道它所要的资源都得到满足之后才开始执行。进程要么占有所有的资源然后开始执行,要么不占有资源,不会出现占有一些资源等待一些资源的情况。静态分配策略逻辑简单,实现也很容易,但这种策略 严重地降低了资源利用率,因为在每个进程所占有的资源中,有些资源是在比较靠后的执行时间里采用的,甚至有些资源是在额外的情况下才是用的,这样就可能造成了一个进程占有了一些 几乎不用的资源而使其他需要该资源的进程产生等待 的情况。
2、层次分配策略:破坏了产生死锁的第四个条件(循环等待)。在层次分配策略下,所有的资源被分成了多个层次,一个进程得到某一次的一个资源后,它只能再申请较高一层的资源;当一个进程要释放某层的一个资源时,必须先释放所占用的较高层的资源,按这种策略,是不可能出现循环等待链的,因为那样的话,就出现了已经申请了较高层的资源,反而去申请了较低层的资源,不符合层次分配策略,证明略。
死锁的避免:上面提到的 破坏 死锁产生的四个必要条件之一就可以成功 预防系统发生死锁 ,但是会导致 低效的进程运行 和 资源使用率 。而死锁的避免相反,它的角度是允许系统中同时存在四个必要条件 ,只要掌握并发进程中与每个进程有关的资源动态申请情况,做出 明智和合理的选择 ,仍然可以避免死锁,因为四大条件仅仅是产生死锁的必要条件。
我们将系统的状态分为 安全状态 和 不安全状态 ,每当在未申请者分配资源前先测试系统状态,若把系统资源分配给申请者会产生死锁,则拒绝分配,否则接受申请,并为它分配资源。如果操作系统能够保证所有的进程在有限的时间内得到需要的全部资源,则称系统处于安全状态,否则说系统是不安全的。很显然,系统处于安全状态则不会发生死锁,系统若处于不安全状态则可能发生死锁。那么如何保证系统保持在安全状态呢?通过算法,其中最具有代表性的 避免死锁算法 就是 Dijkstra 的银行家算法,银行家算法用一句话表达就是:当一个进程申请使用资源的时候,银行家算法 通过先 试探 分配给该进程资源,然后通过 安全性算法 判断分配后系统是否处于安全状态,若不安全则试探分配作废,让该进程继续等待,若能够进入到安全的状态,则就 真的分配资源给该进程。
死锁的避免(银行家算法)改善解决了 资源使用率低的问题 ,但是它要不断地检测每个进程对各类资源的占用和申请情况,以及做 安全性检查 ,需要花费较多的时间。
死锁的检测:对资源的分配加以限制可以 预防和避免 死锁的发生,但是都不利于各进程对系统资源的充分共享。解决死锁问题的另一条途径是 死锁检测和解除 。这种方法对资源的分配不加以任何限制,也不采取死锁避免措施,但系统 定时地运行一个 “死锁检测” 的程序,判断系统内是否出现死锁,如果检测到系统发生了死锁,再采取措施去解除它。# 进程-资源分配图操作系统中的每一刻时刻的系统状态都可以用进程-资源分配图来表示,进程-资源分配图是描述进程和资源申请及分配关系的一种有向图,可用于检测系统是否处于死锁状态。用一个方框表示每一个资源类,方框中的黑点表示该资源类中的各个资源,每个键进程用一个圆圈表示,用 有向边 来表示进程申请资源和资源被分配的情况。
死锁检测步骤:知道了死锁检测的原理,我们可以利用下列步骤编写一个 死锁检测 程序,检测系统是否产生了死锁。
- 如果进程-资源分配图中无环路,则此时系统没有发生死锁
- 如果进程-资源分配图中有环路,且每个资源类仅有一个资源,则系统中已经发生了死锁。
- 如果进程-资源分配图中有环路,且涉及到的资源类有多个资源,此时系统未必会发生死锁。如果能在进程-资源分配图中找出一个 既不阻塞又非独立的进程 ,该进程能够在有限的时间内归还占有的资源,也就是把边给消除掉了,重复此过程,直到能在有限的时间内 消除所有的边 ,则不会发生死锁,否则会发生死锁。(消除边的过程类似于 拓扑排序)
死锁的解除:当死锁检测程序检测到存在死锁发生时,应设法让其解除,让系统从死锁状态中恢复过来,常用的解除死锁的方法有以下四种:
- 立即结束所有进程的执行,重新启动操作系统 :这种方法简单,但以前所在的工作全部作废,损失很大。
- 撤销涉及死锁的所有进程,解除死锁后继续运行 :这种方法能彻底打破死锁的循环等待条件,但将付出很大代价,例如有些进程可能已经计算了很长时间,由于被撤销而使产生的部分结果也被消除了,再重新执行时还要再次进行计算。
- 逐个撤销涉及死锁的进程,回收其资源直至死锁解除。
- 抢占资源 :从涉及死锁的一个或几个进程中抢占资源,把夺得的资源再分配给涉及死锁的进程直至死锁解除。
对于一些很大的程序,将很快需要用到的页装入内存,当需要访问的页不在内存时再由OS外存调入(依据页面置换算法)。当内存吃紧时,再由OS将内存中暂时不用的信息换出到外存。这就是虚拟内存技术。
将CPU资源从一个进程分配到另一个进程的机制。在切换的过程中,操作系统需要先存储当前进程的状态(包括内存空间的指针,当前执行完的指令等等),再读入下一个进程的状态,然后执行此进程。
进程切换分两步:
线程切换不需要切换页目录。
是时分操作系统分配给每个正在运行的进程微观上的一段CPU时间。
路由是网络层组件
称 路由择域信息库 (RIB, Routing Information Base),是一个存储在 路由器 或者联网计算机中的电子表格(文件)或类数据库。 路由表存储着指向特定 网络地址 的路径(在有些情况下,还记录有路径的路由度量值)。
在维护路由表信息的时候,如果在拓扑发生改变后,网络收敛缓慢产生了不协调或者矛盾的路由选择条目,就会发生路由环路的问题。这种情况下会导致用户的IP数据包不停在网络上循环发送,最终造成网络资源的严重浪费。
RIP协议是一种基于距离度量的路由选择协议。
OSPF(开放最短路径优先)协议解决路由回路问题。
两个原因:
http的安全问题:
https通过使用SSL具有了加密(混合加密)、认证(数字证书,用于存放公钥,保证其不被篡改以及可信性)、完整性保护(摘要算法)等功能。
SSL:安全套接字层,位于传输层与应用层之间的一种协议层。通过相互认证、使用数字签名确保完整性。使用加密保证私密性。以实现客户机服务器之间的安全通讯。该协议由两层组成:SSL记录协议和SSL握手协议。
如果是两次挥手,客户机请求关闭服务端直接关闭了,有可能服务端的数据并没有传输完成,造成数据丢失。如果是三次挥手,服务端数据传输完成立即关闭连接。可能会导致本次tcp
连接产生的报文残留在网络中。所以需要四次挥手。
TCP是传输控制协议,提供面向连接的可靠的字节流服务。通过三次握手建立连接。之后才能进行数据传输。TCP提供超时重传、流量控制、拥塞控制等功能。
UDP是用户数据报协议,是一个简单的面向无连接的协议。UDP不提供可靠服务,由于传输数据之前不需要建立连接,所以传输速度很快。其主要使用场景有流媒体传输。
利用滑动窗口实现流量控制,如果发送方把数据发送得过快,接收方可能会来不及接收,这就会造成数据的丢失。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收。
客户机经历了:close -> SYN-sent -> estab-listen
服务端经历了:close -> listen -> SYN-RCVD ->estab-listen
拥塞窗口概念。初始化cwnd = 1
慢开始、拥塞避免、快重传、快恢复。
ARP:地址解析协议。
每个主机都会在自己的ARP缓冲区中建立一个ARP列表,以表示IP地址和MAC地址之间的对应关系。
源主机向当前网段的所有主机发送ARP数据包,包含了源主机IP、MAC地址,以及目标主机IP地址。
本地网络的主机收到数据包后检查其中的目标IP是否为自己的。如果不是则忽略,如果是的话就将自己的MAC地址写入数据包,并将数据包中源主机的IP、MAC地址写入自己的ARP缓冲区中。
ICMP是Internet Control Message Protocol
,因特网控制报文协议。它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由器是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。ICMP报文有两种:差错报告报文和询问报文。
封装成帧、透明传输、差错检测(循环冗余检测保证传输过程中的数据准确性)。
网络层协议负责的是提供主机间的逻辑通信,运输层协议负责的是提供进程间的逻辑通信。
静态路由是由管理员手工配置的,适合比较简单的网络或需要做路由特殊控制。而动态路由则是由动态路由协议自动维护的,不需人工干预,适合比较复杂大型的网络。
客户端发出一个请求,在服务器做出响应之前客户端发过来的请求线程会被挂起,这就是阻塞。此时线程只能等完成这次的请求之后才可以去处理其他的事件,这就叫做同步。
客户端发出了一个请求,然后不等服务器处理就直接返回给客户端了。此时请求线程没有被挂起,这就是非阻塞。线程可以去处理其他事件,这就是异步。服务器通过回调函数来处理这个请求。
点到点信道的数据链路层协议:
PPP(point-to-point protocal)
协议:互联网用户通常需要连接到某个 ISP 之后才能接入到互联网,PPP 协议是用户计算机和 ISP 进行通信时所使用的数据链路层协议。
使用广播信道的数据链路层协议:
载波监听、多点接入、冲突检测协议:
url
中携带域名的IP
地址,并返回给浏览器ip
地址服务器通过三次握手建立TCP
连接HTTP
请求,随后服务器将浏览器请求的资源发送给浏览器TCP
连接(四次挥手)HTTP1.1相较于HTTP1.0 增加了长连接功能,该功能不会主动的去断开一个TCP连接,这样的话就不用每次发送HTTP请求时都重新建立TCP连接。因为重复的建立、断开TCP连接费时又费资源。
HTTP2.0主要有以下几个新特性:
头部压缩、多路复用、二进制帧层
Accept:可以接收的响应内容格式
Connection:客户端想要优先使用的连接类型,keep-alive、upgrade
Host:客户端告诉服务端它请求的资源所在的主机与端口号
cookie:客户端请求时携带的数据
首先,TCP协议建立连接前需要双方确认信息,用于防止伪造连接以及精准控制整个数据传输过程中数据完整有效。这样就会造成TCP连接的资源消耗,其中包括:数据包信息、条件状态、序列号等等。SYN攻击就是故意不完成建立连接所需要的三次握手过程,造成连接一方的资源耗尽。
SYN攻击: SYN洪泛攻击的基础是依靠TCP建立连接时三次握手的设计。 第三个数据包验证连接发起人在第一次请求中使用的源IP地址上具有接受数据包的能力,即其返回是可达的。 据统计,在所有 黑客 攻击事件中,SYN攻击是最常见又最容易被利用的一种攻击手法。
**如何检测:**Linux中使用 netstat -n -p TCP | grep SYN_RECV
命令检测是否被SYN攻击。
如何防范?主要有两大类,一类是通过防火墙、路由器等过滤网关防护,另一类是通过加固TCP/IP协议栈防范。
HTTP设计为无状态的话服务端就可以根据需求将请求分发到服务集群的任意节点上。有利于做负载均衡。
依靠超时重传实现可靠传输。TCP每发送一个数据报后会开启一个计时器,等待目标服务器确认收到了这个报文段。如果计时器时间内没有收到确认,则会重发这个报文段。
可以
1xx
类状态码属于提示信息,是协议处理中的一种中间状态,实际用到的比较少。
2xx
类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。
3xx
类状态码表示客户端请求的资源发送了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向。
「301 Moved Permanently」表示永久重定向,说明请求的资源已经不存在了,需改用新的 URL 再次访问。
「302 Found」表示临时重定向,说明请求的资源还在,但暂时需要用另一个 URL 来访问。
4xx
类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。
「404 Not Found」表示请求的资源在服务器上不存在或未找到,所以无法提供给客户端。
5xx
类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。
假设每个网页 url 平均长度 64 字节,则 10 亿个 url 大约需要 60 G 内存。
使用布隆过滤器,针对 10 亿个 url,我们分配 100 亿个 bit,大约 1.2 G, 相比 100 G 内存,提升了近百倍
cat a.txt b.txt | grep string
netstat
ps -ef
cp -r
rm -r
wc 命令 - c 统计字节数 - l 统计行数 - w 统计字数。
是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行打印出来。
job -l
kill -9 pid
find、whereis、locate等等
history
df -hl Size(总空间) Used(已用) Avail(可用) Use%(使用百分比) Mounted on(挂载区)
cat a.txt | more
IO多路复用的目的就是为了设计一个高性能的网络服务器,可供多个客户端去连接。之所以我们不用多线程的方式去设计是因为多线程环境下的上下文切换带来的消耗也是很大的。
在Linux系统中,一切都是文件,每一个网络连接(socket)在内核中都是以文件描述符(fd)
的形式存放。
select实现IO多路复用的实现方式
#select函数实现
使用 fd_set 实现,里面装的是文件描述符,大小限制为 1024bit
select()函数会将 fd_set 数组从用户态一次性的拷入内核态,交由内核处理会大大提升效率
当有数据到达一个描述符时,select函数会返回(在此之前是阻塞的)
然后遍历 fd_set 找到那个有数据到达的文件描述符 O(n)的时间复杂度获取到相关描述符。
缺点:
1、fd_set 不可重用
2、用户态到内核态数据拷贝的开销
3、O(n)时间复杂度的轮询
epoll
实现IO多路复用的实现方式
// epoll函数实现
epoll_create() 创建一个白板(是一块用户态和内核态共享的一块内存空间)存放fd_events
epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上。
epoll_wait 通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符 O(1)的时间复杂度获取到相关描述符。
优点:
完全解决了select的所有问题。
epoll支持两种触发模式:
LT:水平触发
当 epoll_wait()
检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait()
会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
ET:边缘触发
和 LT 模式不同的是,通知之后进程必须立即处理事件。
下次再调用 epoll_wait() 时不会再得到事件到达的通知。很大程度上减少了 epoll 事件被重复触发的次数,
因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
cat miaoshalog | wc -w failperson
netstat
左连接、右连接、内连接
笛卡尔积,又叫cross join,是SQL中两表连接的一种方式。 假如A表中的数据为m行,B表中的数据有n行,那么A和B做笛卡尔积,结果为m*n行。 通常我们都要在实际SQL中避免直接使用笛卡尔积,因为它会使“数据爆炸”
Mybatis中使用#{ }代替数据部分防止SQL注入。
innodb
引擎以页的形式将数据储存到磁盘,查询时将页读入内存,在叶子节点中查取数据,叶节点内部通过二分法查找,找不到转到该页指向的下一个页继续查询。
Atomicity:事务本身被视为不可分割的最小单元,事务的操作要么全部成功要么全部失败回滚。
Consistency:数据库在事务的执行前后都保持一致,所有事务对同一数据的读取结果都相同。
Isolation:一个事务的操作在提交之前,对其他事务是不可见的
Durability:一旦事务提交之后对于数据库的更改就是永久不可回退的
事务:MySQL事务是在引擎层实现的,MySQL原生myISAM存储引擎不支持事务。
原子性:利用undo log 实现的
持久性:利用redo log 实现的
一致性:是利用 原子性、持久性、隔离性来实现的。事务的四大特性中一致性是目的,其他都是保证一致性的手段。
redo log:记录了数据操作在物理层面的修改,事务进行中会不断的产生redo log 在事务进行提交时一次flush操作保存到磁盘中。
undo log: 记录事务的修改操作,可以实现事务的回滚。
事务的隔离性由MVCC(多版本并发控制)与锁实现:因而隔离性也可以叫做并发控制。
innodb存储引擎中实现了三种隔离级别,分别为读未提交、读已提交、可重复读。其中后两者的实现均基于MVCC,其原理为根据read view(当前未提交事务视图)在事务回滚连中往寻找,直到找到了合适的记录。在聚簇索引中存在两个隐藏列为trx_id:当前行最近的事务改变、roll_pointer:当前旧版本在undo log(事务回滚链路)中位置的指针。
RU: 读未提交
RC: 读已提交 每次读语句开始时新建视图
RR: 可重复读(解决不可重复读问题) 每次事务开始前创建视图
串行化: 锁实现
InnoDB
储存引擎标准实现的锁只有两种:行级锁、意向锁。
InnoDB
实现了如下两种标准的行级锁:
InnoDB
支持两种意向锁(即为表级别的锁):
加意向锁表明某个事务正在锁定一行或者将要锁定一行。首先申请意向锁的动作是InnoDB
完成的,怎么理解意向锁呢?例如:事务A要对一行记录r进行上X锁,那么InnoDB
会先申请表的IX锁,再锁定记录r的X锁。在事务A完成之前,事务B想要来个全表操作,此时直接在表级别的IX就告诉事务B需要等待而不需要在表上判断每一行是否有锁。意向排它锁存在的价值在于节约InnoDB
对于锁的定位和处理性能。
InnoDB
有3种行锁的算法:
页的概念:一块小的且连续的内存空间
聚簇索引:
InnoDB
储存引擎中,聚簇索引就是按照每张表的主键构造一颗B+树,同时叶子节点中存放的就是整张表的行记录数据,也将聚簇索引的叶子节点称为数据页。
一般建表会用一个自增主键做聚簇索引,没有的话MySQL会默认创建,但是这个主键如果更改代价较高(页撕裂),故建表时要考虑自增ID不能频繁update这点。
我们日常工作中,根据实际情况自行添加的索引都是辅助索引,辅助索引就是一个为了寻找主键索引的二级索引,先找到主键索引再通过主键索引找数据。非聚簇索引的叶子节点存储的是数据行的主键信息。
聚簇索引的优点:
聚簇索引的缺点:
InnoDB
表,我们一般都会定义一个自增的ID列为主键InnoDB
表,我们一般定义主键为不可更新。非聚簇索引又叫二级索引,该索引的叶子节点保存的是数据行的主键值,想要得到结果还需要使用主键值去聚簇索引(主键索引)中进行二次检索。这一过程称之为回表。
最长搜索的字段放最右侧。范围搜索后面的字段的索引会失效。尽量使用覆盖索引。
全称多版本并发控制,与之相对的是基于锁的并发控制。
MVCC最大的优势:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能
MVCC实现
而 MVCC 利用了多版本快照的思想,写操作更新最新的版本快照,而读操作去读旧版本快照(read view)根据隔离级别不同读取的规则也不同,没有互斥关系。
联合索引的数据结构依然是B+树。其非叶子节点储存的是第一个关键字的索引。叶子节点存储的是三个关键字的顺序。且按照字段从左到右排序。
如图,index(年龄, 姓氏,名字),叶节点上data域存储的是三个关键字的数据。且是按照年龄、姓氏、名字的顺序排列的。
如果跳过年纪按照后面两个字段搜索,会导致全表扫描。
select_type
: 查询类型,有简单查询、联合查询、子查询等key
: 实际使用到的索引,如果为null,表示没有使用到索引。type
:显示查询使用了何种索引类型,all < index < range < reftable
:显示这一行的数据是关于哪张表的rows
: 根据表统计信息及索引选用情况,大致估算出找到所需数据所需要读取的行数。id
: select查询的序列号,包含一组数字,表示查询中执行select子句的顺序。using filesort
等等。数据库表的自增 ID 达到上限之后,再申请时它的值就不会在改变了,继续插入数据时会导致报主键冲突错误。因此在设计数据表时,尽量根据业务需求来选择合适的字段类型。可以考虑使用bigint
类型。
在java的开发中,货币在数据库中MySQL常用Decimal
和Numric
类型表示,这两种类型被MySQL实现为同样的类型。
DECIMAL和NUMERIC值作为字符串存储,而不是作为二进制浮点数,以便保存那些值的小数精度。
不使用float或者double的原因:因为float和double是以二进制存储的,所以有一定的误差。
通过获取死锁日志来获取死锁信息。
mysql
使用几个特殊的表名来作为监控的开关。比如在数据库中创建一个表名为innodb_monitor
的表用于开启标准监控。创建一个表名为 innodb_lock_monitor
的表开启锁监控。MySQL 通过检测是否存在这个表名来决定是否开启监控,至于表的结构和表里的内容无所谓。相反的,如果要关闭监控,则将这两个表删除即可。
事务1
select age from table where id > 2
事务2
Insert into table(id , age) values (5, 10)
commit
事务1
select age from table where id > 2
commit
事务1两个相同的select语句执行了两次,两次的查询结果不相同,这就是产生了幻读。
redo log 常用作MySQL服务器异常宕机后的数据恢复工作,复杂保证事务的持久性
undo log 常用于记录被改动的数据,负责事务的一致性。
各种隔离级别保证事务的一致性。
利用 redo log 做持久性,redo log主要记录了data在物理层面的修改。redo log 在事务进行提交时一次flush操作保存到磁盘中。
事务的每次提交都需要等到从机的落盘完成后才可以提交。
bin log 是MySQL数据库的二进制日志,用于记录用户对数据库操作的SQL语句((除了数据查询语句)信息。
innodb
引擎独有的日志,用于记录事务操作的变化,记录的是数据修改之后的值,不管事务是否提交都会记录下来。在实例和介质失败时,redo log文件就能派上用场,如数据库掉电,InnoDB
存储引擎会使用redo log恢复到掉电前的时刻,以此来保证数据的完整性。innodb
存储引擎层面的重做日志是物理日志。innodb
存储引擎的重做日志在事务进行中不断地被写入,并日志不是随事务提交的顺序进行写入的。//开启事务并设置隔离级别为读已提交,表count两个字段 name, money
A事务 select * from count 结果name = Tom money = 1000
B事务 update money = 2000 from count where name = Tom B事务提交
A事务 select * from count 结果name = Tom money = 2000 显然A事务对一个数据行两次读操作结果不一致,这就导致了不可重复读问题
HA(High Availability)检测工具应运而生。HA工具一般部署在第三台服务器上,同时连接主从,检测主从是否存活,如果主库宕机则及时将仓库升级为主库,将原来的主库降级为从库。
B+跟B*树不同B+树的非叶子节点不保存关键字记录的指针,只进行数据索引,这样使得B+树每个非叶子节点所能保存的关键字大大增加
非叶子结点中仅含有其子节点的索引,不包含实际数据。
myisam
,写操作多的情况下使用innodb
存储引擎。innodb
。索引下推在非主键索引上的优化,可以有效减少回表的次数,大大提升了查询的效率。
举例:
表k中建立有联合索引(name, age),执行如下语句
select * from k where name like '张%' and age = 10 and sex = 1;
不可重复读的重点是修改:在同一事务中,同样的条件,第一次读的数据和第二次读的「数据不一样」。(因为中间有其他事务提交了修改)
幻读的重点在于新增或者删除:在同一事务中,同样的条件,第一次和第二次读出来的「记录数不一样」。(因为中间有其他事务提交了插入/删除)
在一个支持MVCC的系统中,读操作被分为当前读与快照读
快照读:简单的select操作,不加锁。
select * from table where ?;
当前读:插入/更新/删除操作,需要加锁
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
就是我们最经常用到的 SELECT、UPDATE、INSERT、DELETE。 主要用来对数据库的数据进行一些操作。
其实就是我们在创建表的时候用到的一些sql,比如说:CREATE、ALTER、DROP等。DDL主要是用在定义或改变表的结构,数据类型,表之间的链接和约束等初始化工作上
Statement
或PreparedStatement
对象。stmt.executeUpdate(sql)
执行语句,返回即查询解决。进行update操作还涉及到redo log、binlog。
redo log: (innodb引擎引入的日志,本质上类似于记账账本,不直接在mysql中进行存储,而视在空闲时利用redo log进行数据到磁盘的写入,innodb引擎利用redo log保证数据的不丢失)。其是物理日志,记录某个数据页上做的修改,大小固定,循环写入,一旦空间用完会清除。
binlog: MySQL server提供的功能,逻辑日志,击记录的是sql语句的原始逻辑,如“给 id = 2 的一行数据的c字段增加1”。追加写入,binlog文件到一定大小后会切换到下一个
过程:
假设table k表中定义了两个索引分别为主键索引id以及非主键索引name。那么如下sql语句
select * from k where name between 3 and 5
则需要进行回表查询,而
select id from k where name between 3 and 5
由于name索引的叶子节点存储的即为其主键id值,这一过程是不需要回表查询的。索引name覆盖了我们的查询需求,称之为覆盖索引。
在查询性能上两者差距微乎其微,在更新性能上由于普通索引可以利用change buffer的优化机制性能更优。
count 方法可以返回表内精确的行数,每执行一次都会进行一次全表扫描, 以避免由于其他连接进行 delete 和 insert 引起结果不精确。 在某些索引下是好事,但是如果表中有主键, count (*) 的速度就会很慢,特别在千万记录以上的大表。
两种情况,内存大小允许的情况下仅使用快排在内存中排序,否则的话需要用到外部硬盘空间,在这块空间中MySQL将数据分为12块进行归并排序。
Redis
(Remote Dictionary Server) 是一个使用 C 语言编写的,开源的高性能非关系型(NoSQL)的键值对数据库。Redis
可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。redis
每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。另外redis
也常用来做分布式锁。
优点:
缺点:
因为redis
的高性能与高并发。
因为Redis
是基于内存的操作,CPU不是Redis
的瓶颈,Redis
的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现而且省去了很多上下文切换线程的时间,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。
redis
使用 epoll
多路I/O复用技术 ,单个线程可以处理大量的并发连接。epoll
是一种高效的多路复用技术。
缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm
的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用 redis
或 memcached
之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis
或 memcached
服务的高可用,整个程序架构上较为复杂。
string、list、hash、set、zset。
String多应用于简单的键值对缓存;hash储存结构化数据,比如一个对象。
计数器、分布式会话缓存、分布式锁实现等等。
SkipList
是在有序链表的基础上进行了扩展,解决了有序链表结构查找特定值困难的问题,查找特定值的时间复杂度为O(logn)
,他是一种可以代替平衡树的数据结构。
它的效率和红黑树以及 AVL 树不相上下,但跳表的原理相当简单,只要你能熟练操作链表,就能轻松实现一个 SkipList
。
rdb
文件中的写操作。主线程继续处理命令。使用单独的子线程来进行持久化。主线程不进行任何的IO操作。保证redis
的高性能。缺点是可能会丢失一些数据。Redis
执行的每次写命令记录到单独的日志文件中,当重启Redis
会重新将持久化的日志中文件恢复数据。AOF有一个重写模式,当日志文件过大时可以对其进行压缩。AOF往往效率低于RDB一些。AOF的追写策略:建议使用每秒同步一次(everysec)
策略。
rewrite机制:rewrite会记录上次重写时AOF文件的大小,当AOF文件是上一次大小的二倍且大于64M时触发。
一般来说两者配合使用效果最佳,当 Redis
重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
如果可以容忍数分钟内的数据丢失,可以只选用RDB方式,还比较快。
不推荐只使用AOF方式。
expire设置过期时间
persist设置键永不过期,多用于热点数据。
noeviction
:当内存不足以容纳新写入数据时,新写入操作会报错。
allkeys-lru
:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
allkeys-random
:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
volatile-lru
:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
volatile-random
:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl
:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除
redis
以单线程模式运行,但是通过使用 I/O 多路复用来监听多个套接字(socket), 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis
服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis
内部单线程设计的简单性。
主从连接过程:
salveof no one
命令会使从机反仆为主。**作用:**数据冗余、故障恢复、负载均衡、高可用的基石。使用slave of 命令将某一台redis变为从机。
Redis
集群中 Master 主服务器工作的状态脑裂
发生,节点个数一般配置为 2n+1。为什么有了哨兵模式还需要集群?
redis
的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台redis
服务器都存储相同的数据,很浪费内存,所以在redis3.0上加入了cluster模式,实现的redis
的分布式存储,也就是说每台redis
节点上存储不同的内容。
数据分配策略?
采用一种叫做哈希槽
(hash slot)的方式来分配数据,redis cluster
默认分配了 16384 个slot。将key的 hashCode % 16384
得出数据的槽位。
分布式寻址算法
redis
cluster(集群) 的 hash slot (槽)算法首先面对海量数据,一台redis
肯定是不够用的,一致性hash算法主要是用来将数据按照一定的算法规律存储到指定的redis
服务器中。
常规的hash算法会导致一个问题:当redis
的实例个数变了那么所有的hash值都需要重新计算,这是非常耗时的。一致性hash的出现解决了这种问题。
hash(IP) % 2^32 -1
求出redis
主机在圆环中的位置,hash(key) % 2^32-1
求出数据在环上的位置,从该位置顺时针查找到的第一个主机即该数据存储的位置。Redis
官方站提出了一种权威的基于 Redis
实现分布式锁的方式名叫 Redlock
,此种方式比原先的单节点的方法更安全。它可以保证以下特性:
Redis
节点存活就可以正常提供服务秒杀开始前,商品数据以及库存都预热到redis。
Redisson
、jedis
、lettuce
等等,官方推荐使用Redisson
。
Jedis
是Redis
的Java实现的客户端,其API提供了比较全面的Redis
命令的支持;Redisson
实现了分布式和可扩展的Java数据结构,和Jedis
相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis
特性。Redisson
的宗旨是促进使用者对Redis
的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
Redisson
解决了锁的自动续期问题,只要业务还在执行,Redisson
就会为锁自动续期。
Redis
事务的本质是通过 MULTI、EXEC、WATCH、discard 等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
multi : 标记一个事务块的开始( queued )
exec : 执行所有事务块的命令 ( 一旦执行exec后,之前加的监控锁都会被取消掉 )
discard : 取消事务,放弃事务块中的所有命令
unwatch : 取消watch对所有key的监控
如果一个事务中的命令出现错误,那么所有命令都不会执行。
Redis
事务不保证多条指令的原子性。
基于Lua脚本可以保证脚本中的指令一次性按顺序执行。
redis
支持五种类型。memcached
支持文本类型与二进制类型。redis
是单线程的多路IO复用模型,memcached
是多线程的非阻塞IO模式。redis
支持数据持久化,memcached
不支持redis
适用于复杂的数据结构环境,有持久化需求。memcached
适用于纯keys指令可以扫描得出指定模式的key列表
但是问题是由于redis
是单线程的,keys指令会导致线程阻塞一段时间,此时的线上服务会有短暂停顿直到keys指令执行完毕。
使用scan指令可以做到无阻塞的提取出指定模式的key列表。但有一定的重复几率。再做一遍去重就可以。
解决缓存穿透的问题。
是redis
中的一种数据结构,它将MySQL数据库中所有可能存在的数据都缓存到布隆过滤器中。当攻击者访问不存在的数据时迅速返回避免请求打到数据库上导致数据库宕机问题。
原理:
Bloom Filter 是一种空间效率很高的随机数据结构,Bloom filter 可以看做是对 bit-map 的扩展。当一个元素被加入集合时,通过 K 个 Hash 函数将这个元素映射成一个位阵列(Bit array)中的 K 个点,把它们置为 1。
检索时,我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它。
值得注意的是:如果这些点有任何一个 0,则被检索元素一定不在。
如果都是 1,则被检索元素很可能在。
优点
空间效率和查询效率都远远超过一般的算法,布隆过滤器存储空间和插入 / 查询时间都是常数O(k)。
另外, 散列函数相互之间没有关系,方便由硬件并行实现。
布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。
缺点
布隆过滤器的缺点和优点一样明显。
误算率是其中之一。随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表就可以。
另外,一般情况下不能从布隆过滤器中删除元素. 我们很容易想到把位数组变成整数数组,每插入一个元素相应的计数器加 1, 这样删除元素时将计数器减掉就可以了。然而要保证安全地删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面。这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。
一个线程尝试去获取锁lock,通过setnx
(lock,uuid
,过期时间)。如果lock不存在就会设置成功,返回true,否则返回false。
获取分布式锁成功之后,需要使用expire
命令设置锁有效期,防止死锁。
执行相关业务逻辑
释放锁,首先获取到lock对应的value,将此value与uuid
对比,如果相同的话执行delete指令删除锁。注意!上述两个步骤需要保证原子性。需要使用lua
脚本。
redis数据模型:
在Redis中,会给每一个key-value键值对分配一个字典实体,就是dicEntry
。dicEntry
包含三部分: key的指针、val的指针、next指针,next指针指向下一个dicteEntry形成链表,这个next指针可以将多个哈希值相同的键值对链接在一起,通过链地址法来解决哈希冲突的问题
sds :Simple Dynamic String,简单动态字符串,存储字符串数据。
redisObject:Redis的5种常用类型都是以RedisObject来存储的,redisObject中的type字段指明了值的数据类型(也就是5种基本类型)。ptr字段指向对象所在的地址。
Redis是单线程的。在单线程程序中,任务一个一个地做,必须做完一个任务后,才会去做另一个任务。因而redis的操作保证了原子性。
BGSAVE:后台处理,不会阻塞工作线程。
SAVE:会导致工作线程的阻塞。
由于Redis主线程为单线程模型,大key也会带来一些问题,如:
redis4.0之前的大key的发现与删除方法
redis-rdb-tools
工具。redis
实例上执行bgsave
,然后对生成的rdb
文件进行分析,找到其中的大KEY。redis-cli --bigkeys
命令。可以找到某个实例5种数据类型(String、hash、list、set、zset)的最大key。由于在redis4.0前,没有lazy free机制;针对扫描出来的大key,DBA只能通过hscan、sscan、zscan方式渐进删除若干个元素,但面对过期键删除的场景,这种取巧的删除就无能为力。我们只能祈祷自动清理过期key刚好在系统低峰时,降低对业务的影响。
Redis 4.0之后的大key的发现与删除方法
Redis 4.0引入了memory usage命令和lazy free机制,不管是对大key的发现,还是解决大key删除或者过期造成的阻塞问题都有明显的提升。
worker_processes number | auto; // 设置工作线程数,是Nginx服务器实现并发处理服务的关键所在。
worker_connections number; // 设置最大连接数
keepalive_timeout timeout [header_timeout]; // 配置连接超时时间
Nginx是多进程的,启动时会先启动一个 Master 进程,然后由 Master 进程启动 子Worker 工作进程,Master主要作配置读取,维护 Worker 进程启动-销毁等,Worker进程对请求进行处理,Worker进程之间通过共享内存进行通信,启动Nginx时,默认设置Worker进程数为CPU的核心数。、
DispatcherServlet
捕获DispatcherServlet
对请求URL进行解析(核心方法doDispatch()
),得到请求资源标识符(URI)然后根据该URI,调用HandlerMapping
获得该Handler(Controller)
配置的所有相关的对象DispatcherServlet
根据获得的Handler,选择一个合适的HandlerAdapter(执行目标方法的反射工具)
DispatcherServlet
返回一个ModelAndView
对象ModelAndView
,选择一个适合的ViewResolver
(视图解析器)ViewResolver
结合Model和View,来渲染视图DispatcherServlet
响应给客户端ModelAndView
对象。viewResolver
的唯一作用是根据ModelAndView
得到view
对象,视图对象才能真正的转发或者重定向到页面(并将模型中的数据暴露到请求域中)。ModelAndView
, request, response) 进行页面渲染。InternalResourceView
视图。Annotation其实是一种接口。通过java的反射机制相关的API来访问Annotation信息。相关类(框架或工具中的类)根据这些信息来决定如何使用该程序元素或改变它们的行为。
多个bean之间的互相引用,导致一个闭环的出现。
采用三级缓存模式来解决循环依赖问题。
singletonFactories : //单例对象工厂的cache
earlySingletonObjects ://提前暴光的单例对象的Cache
singletonObjects://单例对象的cache
注意:构造器注入导致的循环依赖无法解决。
假设现在有两个bean X Y互相依赖,且都是单例的,X开始生命周期后直到X通过构造器以及创建对象后,会有一个暴露阶段,此时会将X的一个ObiectFcatory
对象暴露出去并存入二级缓存中。然后会进行X的属性注入,这是会将Y注入,但是还没有Y,然后进入到Y bean的生命周期。一直到Y暴露出自己的ObjectFcatory
对象暴露出去并存入二级缓存中后,Y进行依赖注入,需要注入X,然后二级缓存中有X的一个对应的工厂对象。至此完成了循环依赖。需要注意的是此过程仅适用于由于属性注入引起的循环依赖,对于由于构造器注入引起的循环依赖不能解决,原因是ObiectFcatory
对象是在根据构造器通过反射创建对象后才产生的。对于构造器注入引起的循环依赖无法起作用。
1.1 首先spring通过BeanDefinitionReader
会将xml、Java类型的配置文件解析为BeanDefinition
类型注册到容器中。BeanDefinition
实际上是一个用来存储class信息的对象。它里面包含了一个类的基本信息、类的父类的信息、是否懒加载、是否为单例等等。beandifinition
定义了bean的基本信息,根据它来创造bean然后<BeanDefinition
,beanName
>分别作为BeanFactory
中的。
1.2 BeanDefinition
会转化为mergebeandefinition
,其中包括了BeanDefinition
以及 parent BeanDefinition
的信息。
1.3 配置 BeanDefinition
的depends-on BeanDefinition
1.4 根据BeanDefinition
中指定的class信息,以及构造器信息最终通过反射获取到BeanDefinition
的实列对象(注意此时还不是一个bean,经过后续的一些操作才会变成一个完整的bean)。
1.5 判断对象是否允许循环依赖?是否需要AOP,属性注入。
1.6 判断是否需要暴露。需要的话会将一个objectFactory对象存入一个二级缓存中。
1.7 spring bean 的实例化完成,加入到spring 的单例缓冲池中(一个map)。
调用init-method
进行bean的初始化,主要用于项目的一些依赖(配置文件或者数据库连接等等)
destory-metnod
方法进行bean的销毁。
BeanDefinition
并注册到容器中;这一步需要XmlBeanDefinitionReader
的配合。BeanFactoryPostProcesser
,包含一个可以传入beanFactory
引用的方法,获取到容器之后可以做很多事情。由于BeanFactoryPostProcesser
工作在bean实例化之前,所以可以通过beanFactory
获取到map从而手动修改或者移除beanDefinition
BeanPostProcesser
,工作于bean实例化或者初始化前后。其包含两个方法。是spring作为扩展接口留给开发人员使用的。public interface BeanPostProcessor {
//在初始化之前调用
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
//在初始化之后调用
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
创建事件传播器对象
beanDefinition
实例化
BeanFactory:
是Spring里面最底层的接口,提供了最简单的容器的功能,包含了各种Bean的定义,读取bean配置文件,管理bean的加载、实例化、控制bean的生命周期、维护bean之间的依赖关系等等。
ApplicationContext
:继承了BeanFactory
接口。是spring中更高一级的容器。提供了比BeanFactory
更多的功能。
区别:
BeanFactory
采用懒加载形式注入bean,ApplicationContext
在容器启动时一次性创建所有bean,这样在容器启动时就可以发现Spring中存在的配置错误。ApplicationContext
启动后预载入所有的单实例Bean,通过预载入单实例bean ,确保当你需要的时候,你就不用等待,因为它们已经创建好了。
默认为单例的,可以通过xml文件中的scope标签来做更改。如原型模式、request、session。
构造器注入:无参构造器注入,有参构造器注入。
set方法注入:要求被注入的属性必须有set方法。
Spring自动将某个bean的引用装配给了指定属性,这一过程叫做自动装配 。Spring提供了三种自动装配的策略。
//无需自动装配
int AUTOWIRE_NO = 0;
//按名称自动装配bean属性
int AUTOWIRE_BY_NAME = 1;
//按类型自动装配bean属性
int AUTOWIRE_BY_TYPE = 2;
//按构造器自动装配
int AUTOWIRE_CONSTRUCTOR = 3;
//过时方法,Spring3.0之后不再支持
上面介绍的是基于xml配置文件的自动装配过程。下面介绍基于注解的自动装配过程。
基于注解的自动装配:
@Autowired
注解可以实现bean的自动装配。默认是按照类型进行装配的。但是如果匹配到同一类型的多个实例,再通过byName
来确定要装配的bean
@SpringBootApplication
启动类注解,等同于@SpringBootConfiguration、 @EnableAutoConfiguration、 @ComponentScan
这三个注解
AOP意为面向切面编程,与OOP一样,是一种编程理念,如果把OOP看作是自上而下的层层抽象,那么AOP就是从左至右的相同功能模块的抽取和封装。开发中使用AOP可以大大减少冗余代码,降低模块之间的耦合度,并且有利于未来的扩展性。比如商城业务中好多的微服务模块都要先进行用户验证。我们就可以把验证用户这一功能抽取出来作为一个切面。
举一个例子:
@Component("landlord")
public class Landlord {
// 下面方法是连接点
public void service() {
// 仅仅只是实现了核心的业务功能
System.out.println("签合同");
System.out.println("收房租");
}
}
@Component // 标识为一个Bean
@Aspect // 标识为一个切面
class Broker {
// 前置通知,表示在连接点方法之前执行
// 定义了 execution 的正则表达式,Spring 通过这个正则表达式判断具体要拦截的是哪一个类的哪一个方法
@Before("execution(* pojo.Landlord.service())")
public void before(){
System.out.println("带租客看房");
System.out.println("谈价格");
}
// 后置通知,表示在切入点方法之后执行
@After("execution(* pojo.Landlord.service())")
public void after(){
System.out.println("交钥匙");
}
}
AspectJ是AOP的一种实现,是目前Java开发社区中最流行的AOP框架,拥有更好的性能。
利用HttpMessageConverter
的实现类将http的请求转化为Java对象的。同时,响应的时候还可以利用HttpMessageConverter
的实现类将Java对象转化为http响应的格式。
DispatcherServlet
是SpringMVC
的核心入口。
不是线程安全的,对于单例Bean,所有线程都共享一个单例实例Bean,因此是存在资源的竞争。
但如果单例Bean,是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如SpringMVC
的 Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身。
定时任务 指的是应用程序在指定的时间执行预先定义好的程序片段 . 在 Spring 中使用定时任务非常简便,分为三步:
SpringBoot
启动类上使用 @EnableScheduling
开启定时任务功能是一个ORM(对象关系映射)框架。
Mybatis
内部封装了jdbc
,使得开发者只需要关注sql
语句本身,而不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。
mybatis
通过xml或注解的方式将要执行的各种statement配置起来,并通过java对象和statement中sql
的动态参数进行映射生成最终执行的sql
语句,最后由mybatis
框架执行sql
并将结果映射为java对象并返回。
MyBatis
避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis
可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJO映射成数据库中的记录。
是一种注入攻击,它通过将任意代码插入数据库查询,使得攻击者完全控制数据库服务器。 攻击者可以使用SQL注入漏洞绕过应用程序安全措施;可以绕过网页或Web应用程序的身份验证和授权,并检索整个SQL数据库的内容;还可以使用SQL注入来添加,修改和删除数据库中的记录。
使用#{}可以有效的防止SQL注入,MyBatis
启用了预编译功能,在SQL执行前,会先将上面的SQL发送给数据库进行编译;执行时,直接使用编译好的SQL,替换占位符“?”就可以了。因为SQL注入只能对编译过程起作用,所以这样的方式就很好地避免了SQL注入的问题。
原理:
在框架底层,是JDBC中的PreparedStatement
类在起作用,PreparedStatement
是我们很熟悉的Statement的子类,它的对象包含了编译好的SQL语句。这种“准备好”的方式不仅能提高安全性,而且在多次执行同一个SQL时,能够提高效率。原因是SQL已编译好,再次执行时无需再编译。
--Mybatis在处理#{}时
select id,name,age from student where id =#{id}
当前端把id值1传入到后台的时候,就相当于:
select id,name,age from student where id ='1'
--Mybatis在处理${}时
select id,name,age from student where id =${id}
当前端把id值1传入到后台的时候,就相当于:
select id,name,age from student where id = 1
Mybatis 中优先使用 #{}。当需要动态传入表名或列名时,使用 ${} 。
#{}是预编译处理,${}是字符串替换,#{}可预防SQL注入,提高系统安全性,将SQL中的#{}替换成?占位符
合理利用缓存可以避免频繁操作数据库,减轻数据库压力,同时提高系统的性能。
一级缓存是sqlSession
级别的,Mybatis
对缓存提供支持,但是在没有配置的默认情况下,它只开启一级缓存。一级缓存在操作数据库时需要构造sqlSession
对象,在对象中有一个数据结构(HashMap)
用于存储缓存数据。不同的sqlSession
之间的缓存数据区域是互相不影响的。也就是他只能作用在同一个sqlSession
中,不同的sqlSession
中的缓存是互相不能读取的。当在同一个sqlSession
中执行两次相同的sql
语句时,第一次执行完毕会将数据库中查询的数据写到缓存(内存)。
二级缓存是mapper级别的缓存,多个sqlSession
去操作同一个mapper的sql
语句,它们可以公用二级缓存,二级缓存是跨sqlSession
的。
sql
的id相同。sql
的 resultType
的类型相同mapper.xml
中定义的每一个sql
的parameterType
类型相同。SqlSessionFactory
SqlSessionFactory
创建 SqlSession
sqlsession
执行数据库操作MyBatis先封装SQL,接着调用JDBC操作数据库,最后把数据库返回的表结果封装成Java类。
MyBatis也有四大核心对象:
SqlSession
对象,该对象中包含了执行SQL语句的所有方法。类似于JDBC里面的Connection。SqlSession
传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。类似于JDBC里面的Statement/PrepareStatement
。MappedStatement
对象,该对象是对映射SQL的封装,用于存储要映射的SQL语句的id、参数等信息。ResultHandler
对象,用于对返回的结果进行处理,最终得到自己想要的数据格式或类型。可以自定义返回类型。MyBatis
的配置文件。mybatis-config.xml
为MyBatis
的全局配置文件,用于配置数据库连接信息。MyBatis
配置文件mybatis-config.xml
中加载。mybatis-config.xml
文件可以加载多个映射文件,每个文件对应数据库中的一张表。MyBatis
的环境配置信息构建会话工厂SqlSessionFactory
。SqlSession
对象,该对象中包含了执行SQL语句的所有方法。MyBatis
底层定义了一个Executor接口来操作数据库,它将根据SqlSession
传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。MappedStatement
对象。在Executor接口的执行方法中有一个MappedStatement
类型的参数,该参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。preparedStatement
对象设置参数的过程。首先,调用以及被调用的微服务双方都应该被注册到注册中心。
Spring Boot启动APP上标注 @EnableFeignClients
注解。
编写远程调用接口并标注@FeignClient
注解。(括号内添加所要调用的微服务名称)
接口中的方法为实际想要调用的服务的方法签名,并使用@PostMapping
注解映射为一个post类型的HTTP请求。
核心原理就是通过一系列的封装和处理,将以Java注解的方式定义的远程调用API接口,最终转化为HTTP的请求与响应结果。
从上图可以看到,Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的 Request 请求。
微服务启动时,feign对添加了@FeignClient
的接口扫描,创建远程接口的本地JDK Proxy代理实例。然后注入到Spring IOC容器中。当远程接口的方法被调用,由Proxy代理实例去完成真正的远程访问,并且返回结果。
Feign的方法处理器 MethodHandler
。它用来解析方法上的url
,以及@postMapping
注解中包含的数据,并生成一个http请求模板。
在 MethodHandler
中具体由requestTemplate
发起request请求。Request 经过编码交给httpclient
发送到远程调用服务。
feign内部有Ribbon实现了客户端的负载均衡。从注册中心读取所有可用的服务提供者,在客户端每次调用接口时采用如轮询负载均衡算法选出一个服务提供者调用,因此,Ribbon
是一个客户端负载均衡器。
常见的负载均衡策略有:
挡在众多微服务前面的一堵墙,用来管理、授权、流量限制等等。可以保护后台的微服务。Spring Cloud Gateway旨在为微服务架构提供一种简单而有效的统一的API路由管理方式。Spring Cloud Gateway作为Spring Cloud生态系中的网关,目标是替代ZUUL,其不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。
gateway:
# 路由的示例
routes:
- id: product_route
uri: lb://mall-product
predicates:
# 根据path进行匹配
- Path=/api/product/**
# 根据host进行匹配,可以一次匹配多个host。
- Host=catmall.com, item.catmall.com
filters:
- RewritePath=/api/(?>.*),/$\{segment}
Nacos | Eureka | Consul | CoreDNS | Zookeeper | |
---|---|---|---|---|---|
一致性协议 | CP+AP | AP | CP | — | CP |
健康检查 | TCP/HTTP/MYSQL/Client Beat | Client Beat | TCP/HTTP/gRPC/Cmd | — | Keep Alive |
负载均衡策略 | 权重/ metadata/Selector | Ribbon | Fabio | RoundRobin | — |
雪崩保护 | 有 | 有 | 无 | 无 | 无 |
自动注销实例 | 支持 | 支持 | 不支持 | 不支持 | 支持 |
访问协议 | HTTP/DNS | HTTP | HTTP/DNS | DNS | TCP |
监听支持 | 支持 | 支持 | 支持 | 不支持 | 支持 |
多数据中心 | 支持 | 支持 | 支持 | 不支持 | 不支持 |
跨注册中心同步 | 支持 | 不支持 | 支持 | 不支持 | 不支持 |
SpringCloud集成 | 支持 | 支持 | 支持 | 不支持 | 支持 |
Dubbo集成 | 支持 | 不支持 | 不支持 | 不支持 | 支持 |
K8S集成 | 支持 | 不支持 | 支持 | 支持 | 不支持 |
服务注册原理:在nacos
的服务端,有一个用来管理微服务实例的容器,注册中心将微服务的实例交由ServiceHolder
处理,ServiceHolder
为微服务提供空间并将它的所有实例挂在该空间下。服务注册完成后提供者将于注册中心维护心跳机制,心跳机制可以保证注册中心可以及时的剔除失效的实例。
服务发现原理:服务完成注册之后,消费者可以向注册中心订阅某个服务,并提交一个监视器,当注册中心的服务发生变更时监听器会收到通知,然后消费者可以更新本地的服务实例列表,以保证所有的服务均可用。
nacos的负载均衡:Nacos
的客户端在获取到服务的完整实例列表后,会在客户端进行负载均衡算法来获取一个可用的实例,模式使用的是随机获取的方式。
服务容器负责启动,加载,运行服务提供者。
服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者在启动时,向注册中心订阅自己所需的服务。
注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
是指事务的参与方位于不同的分布式系统的节点上。
CAP理论:是设计分布式系统的基础理论依据。强一致性、可用性、分区容错性。
BASE理论:是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。是对CAP中AP的一个扩展。
两阶段提交协议2PC:
两阶段提交协议中,存在一个节点作为协调者,其他参与事务的节点作为参与者。
第一阶段(准备阶段):
第二阶段(提交阶段):
当协调者节点从所有参与者节点获得的相应消息都为"同意"时
若任一参与者节点在第一阶段返回的响应消息为"中止"。
三阶段提交协议3PC:
与两阶段提交不同的是,三阶段提交有两个改动点。
也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit
、PreCommit
、DoCommit
三个阶段。
1. XA:该协议采用两阶段提交(2PC),即整个事务控制过程经历了两个阶段。
2. TCC(try-confirm-cancel):
try阶段:尝试执行,完成所有的业务检查,预留完成业务所必须的资源。
confirm阶段:当TCC事务管理器决定commit全局事务时,就会逐个执行Try操作指定的Confirm操作,将Try未完成的事项最终完成。不作任何业务检查,只使用Try阶段预留的业务资源。
cancel阶段:Cancel 是对Try操作的一个回撤。当TCC事务管理器决定rollback全局事务时,就会逐个执行Try操作指定的Cancel操作,将Try操作已完成的事项全部撤回。
3. MQ事务:
Seata
提供一站式的分布式事务解决方案。可以提供AT(默认使用,基于2PC两阶段模式)、 TCC、 SAGA 、XA事务模式。
Seata
的3 + 1个概念:
TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行。
对于每个微服务对应的数据库都需要添加一张回滚日志表。用于自动的回滚一些数据。
下载seata-server
软件包,github.com/seata/seata/releases
导入依赖 spring-cloud-starter-alibaba-seata
启动 seata-server
所有想要使用到分布式事务的微服务使用seata DatasourceProxy
代理自己的数据源
@GlobaTransactional
开启全局事务。(只需要给分布式事务入口标上该注解即可),对于远程调用的方法不需要标。
启动各个微服务即可。
由于Netflix的hystrix
停止更新,所以改用spring cloud Alibaba的sentinel组件实现系统的限流、熔断、降级。
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式服务架构的轻量级高可用流量控制产品,由阿里中间件团队开源。主要以流量为切入点,从**流量限制、服务熔断降级、**系统负载保护等多个维度来帮助您保护服务的稳定性。
sentinel项目分为7个主要部分,其中最主要的 就是sentinel-core模块。限流、熔断、降级、系统保护等都在这里实现。
流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状。
限流:对于打入集群的请求流量在入口处进行控制,使服务能够承担不超过自己能力的流量压 力。
熔断:A服务调用B服务,由于各种各样的原因导致请求时间过长。这样的情况次数过多就应该考虑将B服务直接断路。凡是调用B服务直接返回错误数据。
降级:由于服务器的压力较大而对一些服务进行有针对的降级,从而保证核心业务的正常运行。
对比内容 | Sentinel | Hystrix |
---|---|---|
隔离策略 | 信号量隔离 | 线程池隔离/信号量隔离 |
熔断降级策略 | 基于响应时间或失败比率 | 基于失败比率 |
实时指标实现 | 滑动窗口 | 滑动窗口(基于 RxJava) |
规则配置 | 支持多种数据源 | 支持多种数据源 |
扩展性 | 多个扩展点 | 插件的形式 |
基于注解的支持 | 支持 | 支持 |
限流 | 基于 QPS,支持基于调用关系的限流 | 不支持 |
流量整形 | 支持慢启动、匀速器模式 | 不支持 |
系统负载保护 | 支持 | 不支持 |
控制台 | 开箱即用,可配置规则、查看秒级监控、机器发现等 | 不完善 |
常见框架的适配 | Servlet、Spring Cloud、Dubbo、gRPC 等 | Servlet、Spring Cloud Netflix |
ActiveMQ
是一种开源的,实现了JMS1.1规范的,面向消息(MOM)的中间件,为应用程序提供高效的、可扩展的、稳定的和安全的企业级消息通信。
实现系统间的通信,实现系统间解耦、异步、削峰等作用。
原理就是生产者生产消息, 把消息发送给activemq
。 Activemq
接收到消息, 然后查看有多少个消费者, 然后把消息转发给消费者, 此过程中生产者无需参与。 消费者接收到消息后做相应的处理和生产者没有任何关系。
订阅发布模式,没有订阅者的话消息会被丢弃。点对点模式消息会保存到activeMQ
服务器中。
订阅发布模式随着订阅的增长性能会逐渐降低,点对点模式不会。
点对点模式的话, 如果消息发送不成功此消息默认会保存到 activemq
服务端直到有消费者将其消费, 所以此时消息是不会丢失的。
MySQL数据库中添加一张消息消费记录表,记录已经消费过的消息的ID,每当一个消息进来先判断它是否被执行过,如果执行过就放弃。如果没执行过就开始执行消息,消息执行完之后将该消息的ID存入表中。
由connection创建session时有两个参数供选择,一个是事务,一个是签收机制。什么是签收机制?消费者接受到消息后,需要告诉消息服务器,我收到消息了。当消息服务器收到回执后,本条消息将失效。因此签收将对PTP模式产生很大影响。如果消费者收到消息后,并不签收,那么本条消息继续有效,很可能会被其他消费者消费掉!从而导致重复消费。
消息的签收有三种可供选择:
AUTO_ACKNOWLEDGE://表示在消费者receive消息的时候自动的签收
CLIENT_ACKNOWLEDGE://表示消费者receive消息后必须手动的调用acknowledge()方法进行签收
DUPS_OK_ACKNOWLEDGE://签不签收无所谓了,只要消费者能够容忍重复的消息接受,当然这样会降低Session的开销
一般生产上会选取CLIENT_ACKNOWLEDGE作为签收策略。因为接收到了消息,并不意味着成功的处理了消息,假设我们采用手动签收的方式,只有在消息成功处理的前提下才进行签收,那么只要消息处理失败,那么消息还有效,仍然会继续消费,直至成功处理!
特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
---|---|---|---|---|
单机吞吐量 | 万级 | 万级 | 10万级 | 10万级 |
时效性 | ms | 微秒级 | ms | ms |
可用性 | 高 | 高 | 非常高 | 非常高 |
消息可靠性 | 可能会丢失 | 可以到达0丢失 | 可以到达0丢失 | |
Java编写,阿里开源。 | 适用于大数据领域 |
搭建集群、外部持久化(服务器意外宕机,消息依然存在)、签收、事务。
秒杀环境下,一旦redis
库存扣减成功了,就相当于完成了购物操作,由于秒杀的特殊性,如果对每一个对于数据库的写订单、减库存操作的立即执行的话,对数据库的压力过大。
于是,我们将每一条秒杀成功的消息封装后存入消息队列中,然后给用户返回“抢购排队中”的结果。然后将消息队列中的下单操作一个个的写入数据库中、下订单、写订单详情。最终返回用户“秒杀成功”。比起多线程同步的修改数据库的操作,这样一来大大的缓解了数据库的连接压力。
连接工厂、连接、会话(目的地(queue、topic)、生产者、消费者)包含两个参数事务、确认机制。其中事务偏向于生产者,确认机制偏向于消费者。
jms是异步通信,发送方发送消息后就可以继续其它业务,而不用阻塞等等接收方响应。但接收方在接收消息上有两种模式:一种是同步接收消息,一种是异步接收消息。下面的示例中也会分别演示
onMessage
方法,接收者不用阻塞等待,可执行其它业务。 实现接口MessageListener
,注册监听器 consumer.setMessageListener(this);
(异步接收) ,实现 onMessage
方法。Activemq
在项目中主要是完成系统之间通信,并且将系统之间的调用进行解耦。例如在添加、修改商品信息后,需要将商品信息同步到索引库、同步缓存中的数据以及生成静态页面一系列操作。在此场景下就可以使用activemq
。一旦后台对商品信息进行修改后,就向activemq
发送一条消息,然后通过activemq
将消息发送给消息的消费端,消费端接收到消息可以进行相应的业务处理。
可以使用url中带参数,把token传递给服务端。http的get方式。
分库情况下:可以使用mycat数据库中间件实现多个表的统一管理。虽然物理上是把一个表中的数据保存到多个数据库中,但是逻辑上还是一个表,使用一条sql语句就可以把数据全部查询出来。
session提供了commit以及rollback方法进行事务的提交与回滚。在事务状态下进行发送操作,消息并未真正投递到中间件。而只有进行session.commit操作之后,消息才会发送到中间件,再转发到适当的消费者进行处理。如果是调用rollback操作,则表明,当前事务期间内所发送的消息都取消掉。
开启事务后,producer发送message时在message中带有transaction_ID。broker收到message后判断是否有transaction_ID,如果有就把message保存在transaction store中,等待commit或者rollback消息。所以ActiveMQ
的事务是针对broker而不是producer的,不管session是否commit,broker都会收到message。如果producer发送模式选择了persistent,那么message过期后会进入死亡队列。在message进入死亡队列之前,ActiveMQ
会删除message中的transaction_ID,这样过期的message就不在事务中了,不会保存在transaction store中,会直接进入死亡队列。
levelDB、kahaDB、JDBC
默认使用自动签收机制。生产上推荐使用client_ackonwlege模式。可以确保消息被消费后才签收。这样的话就不会有重复消费的问题出现了。
是一个开源的分布式协同服务系统,Zookeeper的设计目标是将那些复杂容易出错的分布式一致性服务封装起来。
服务的注册与发现、分布式锁、集群管理、负载均衡等等。
共享的、树形结构,由一系列的 ZNode数据节点组成,类似文件系统(目录不能存数据)。ZNode存有数据信息,如版本号等等。ZNode之间的层级关系,像文件系统中的目录结构一样。并且它是将数据存在内存中,这样可以提高吞吐、减少延迟。
ZooKeeper会给每个更新请求,分配一个全局唯一的递增编号(zxid),编号的大小体现事务操作的先后顺序。
全称 Zookeeper Atomic Broadcast (Zookeeper原子广播)。
Zookeeper 是通过 Zab 协议来保证分布式事务的最终一致性。是一种支持崩溃恢复的原子广播协议。
zab有两种基本模式:
启动过程或Leader出现网络中断、崩溃退出与重启等异常情况时。
当选举出新的Leader后,同时集群中已有过半的机器与该Leader服务器完成了状态同步之后,ZAB就会退出恢复模式。
create -e或者-s /aaa "bbb"
get /aaa
ls /aaa
set /aaa "ccc"
delete /aaa
ZooKeeper允许用户在指定节点上注册Watcher,当触发特定事件时,ZooKeeper服务端会把相应的事件通知到相应的客户端上,属于ZooKeeper一个重要的特性。
get /aaa watch 监控节点变化
启动时主动到服务端拉取信息,同时,在制定节点注册Watcher监听。一旦有配置变化,服务端就会实时通知订阅它的所有客户端。
Docker是一个容器化平台,它以容器的形式将您的应用程序及其所有依赖项打包在一起,以确保您的应用程序在任何环境中无缝运行。
Docker分客户端和服务端概念,Docker服务端有一个守护线程以及多个工作线程概念(类似于nginx)。Docker客户端与Docker守护进程通信,**Docker守护进程负责构建,运行和分发Docker容器。**工作线程负责从仓库拉取镜像。
Docker镜像是Docker容器的源代码,Docker镜像用于创建容器。使用build命令创建镜像。
Docker容器包括应用程序及其所有依赖项,作为操作系统的独立进程运行。
希望将过去所学的一些知识做一个系统的深入理解。秒杀项目运用场景多,涉及的问题与中间件较为复杂,更有利于对web服务的深入学习。
本项目主要是为了模拟一种高并发的场景,请求到达nginx后首先经由负载轮询策略到达某一台服务器中(后端部署了两台服务器)。为了解决秒杀场景下的入口大流量、瞬时高并发问题。引入了redis作为缓存中间件,主要作用是缓存预热、预减库存等等。引入秒杀令牌与秒杀大闸机制来解决了入口大流量问题。引入线程池技术来解决了浪涌(高并发)问题。
直接由数据库操作库存的sql语句如下所示。依靠MySQL中的排他锁实现
update table_prmo set num = num - 1 WHERE id = 1001 and num > 0
利用redis
的单线程特性预减库存处理秒杀超卖问题!!!
Redis
缓存中;(缓存预热)Redis
中进行预减库存(decrement),当Redis
中的库存不足时,直接返回秒杀失败,否则继续进行第3步;mysql
唯一索引(商品索引)+ 分布式锁
设置热点数据永远不过期。设置不同的失效时间
使用canal组件实现(canal的原理,模拟MySQL的主从复制机制)
更新数据库后立即删缓存,然后下一次查缓存找不到数据后会再次从数据库同步到缓存。
非分布式事务:系统中使用Spring提供的事务功能即可。
分布式事务:将减库存与生成订单操作组合为一个事务。要么一起成功,要么一起失败。
CAP理论(只能保证 CP、AP)、BASE理论(最终一致性,基本可用性、柔性事务)。
分布式事务的两个协议以及几种解决方案:
seata
分布式事务控制组件。
秒杀令牌(token)加秒杀大闸限制入口流量。线程池技术限制瞬时并发数。验证码做防刷功能。
封IP,nginx
中有一个设置,单个IP访问频率和次数多了之后有一个拉黑操作。
分布式锁。redission
客户端实现分布式锁。
decrement API减库存,increment API回增库存。以上的指令都是原子性的。
典型的缓存雪崩问题,给缓存中的数据的过期时间加随机数。
组redis
集群,主从模式、哨兵模式、集群模式。
主从模式中:如果主机宕机,使用slave of no one 断开主从关系并且把从机升级为主机。
哨兵模式中:自动监控master / slave的运行状态,基本原理是:心跳机制+投票裁决。
每个sentinel会向其它sentinel、master、slave定时发送消息(哨兵定期给主或者从和slave发送ping包(IP:port),正常则响应pong,ping和pong就叫心跳机制),以确认对方是否“活”着,如果发现对方在指定时间(可配置)内未回应,则暂时认为对方已挂(所谓的“主观认为宕机” Subjective Down,简称SDOWN)。
若master被判断死亡之后,通过选举算法,从剩下的slave节点中选一台升级为master。并自动修改相关配置。
那就把能提前放入cdn服务器的东西都放进去,反正把所有能提升效率的步骤都做一下,减少真正秒杀时候服务器的压力。
token+redis
解决分布式会话问题。
Token是服务端生成的一串字符串,作为客户端进行请求的一个令牌,当第一次登录后,服务器生成一个userToken
便将此Token返回给客户端,存入cookie中保存,以后客户端只需带上这个userToken
前来请求数据即可,无需再次带上用户名和密码。二次登录时,只需要去redis
中获取对应token的value,验证用户信息即可。
// 用户第一次登录时,经过相关信息的验证后将对应的登录信息以及凭证(token)存入reids中
String uuid = UUID.rondom().toString();
redisTemplate.opsForValue().set(uuid, userModel);
// token下发到客户端存入cookie中进行保存
// 再次登录时cookie携带着token到redis中找到对应的value不为空,表示该用户已经登陆过了,如果查询结果为空,则让该用户重新登陆,然后将用户信息保存到redis中。
// 一般设置一个过期时间,表示的就是多久后用户的登录态就失效了。
先说一下核心参数:
一个任务进来,先判断当前线程池中的核心线程数是否小于corePoolSize
。小于的话会直接创建一个核心线程去提交业务。如果核心线程数达到限制,那么接下来的任务会被放入阻塞队列中排队等待执行。当核心线程数达到限制且阻塞队列已满,开始创建非核心线程来执行阻塞队列中的 业务。当线程数达到了maximumPoolSize
且阻塞队列已满,那么会采用拒绝策略处理后来的业务。
一、限流、削峰部分的设计。
入口大流量限制
例如有10W用户来抢购10件商品,我们只放100个用户进来。
采取发放令牌机制(控制流量),根据商品id和一串uuid
产生一个令牌存入redis
中同时引入了秒杀大闸,目的是流量控制,比如当前活动商品只有100件,我们就发放500个令牌,秒杀前会先发放令牌,令牌发放完则把后来的用户挡在这一层之外,控制了流量。
获取令牌后会对比redis
中用户产生的令牌,对比成功才可以购买商品
// 设置秒杀大闸
redistemplate.opsForValue().set("door_count"+promoId, itemModel.getStock()*5)
// 发放令牌时,先去redis获取当前大闸剩余令牌数
int dazha = redistemplate.opsForValue().get("door_count"+promoId)
if (dazha <= 0) {
// 抛出一个异常
throw new exception;
}else {
String tocken = UUIDUtils.getUUID()+promoId;
// 用户只有拥有这个token才有资格下单
redistemplate.opsForValue().set(userToken, token);
}
高并发流量的限制(泄洪):利用线程池技术,维护一个具有固定线程数的线程池。每次只放固定多用户访问服务,其他用户排队。另外一种实现方式就是J.U.C
包中的信号量(Semaphore)机制。可以有效的限制线程的进入。
二、用户登录的问题(分布式会话)
做完了分布式扩展之后,发现有时候已经登录过了但是系统仍然会提示去登录,后来经过查资料发现是cookie和session的问题。然后通过设置cookie跨域分享以及利用redis
存储token信息得以解决。
redis
设置热点数据永不过期作为异步下单的中间件,利用队列排队下单缓解数据库的并发压力。
CPU密集型业务:N+1;IO密集型业务:2N+1
基础架构下的tps是2000
经过做动静分离、nginx
反向代理并做了分布式扩展、引入redis
中间件后达到了2500 tps。
轮询、权重、IP_hash、最少连接。
首先多台设备登录属于SSO问题,用户登录一端之后另外一端可以通过扫码等形式登录。虽然用户登录了多台设备,但是用户名是一样的。为用户办法的token是相同的。我们为一个用户只会颁发一个token。
设置最大线程数来限制浪涌流量
ThreadPoolExecutor.AbortPolicy://丢弃任务并抛出RejectedExecutionException异常。
DiscardPolicy://丢弃任务,但是不抛出异常。
DiscardOldestPolicy://丢弃队列最前面的任务,然后重新提交被拒绝的任务
CallerRunsPolicy://由调用线程(提交任务的线程)处理该任务
无效,会从redis中删除,
设置为秒杀商品的个数减去核心线程数最合适。
jstat -gc vmid count
jstat -gc 12538 5000 // 表示将12538进程对应的Java进程的GC情况,每5秒打印一次
跟随用户的请求会动态变化,令牌桶机制可以控制每秒生成令牌的个数。
redis中库存减成功后,生成一条消息包含了商品信息、用户信息消息由MQ的生产者生产,经由queue模式发送给消费方,即订单生成的业务模块,在该模块会消费这条消息,根据其中的信息进行订单的生成,以及数据库的修改操作。
TPS:单机2000
item表、item_stock表、order表、用户信息表、
将查库存、减库存两个sql
语句作为一个事务进行控制,保证每一个库存只能被一个用户消费。两条语句都执行成功进行事务提交,否则回滚。但这样会导致并发很低。但也没办法。
update table set stock = stock-1 where prom_id = ? and stock > 1;
前端限制:一次点击之后按钮置灰几秒钟。
后端限制:由于秒杀令牌的设置,用户的一个下单请求会先判断用户当前是否已经持有令牌了,因为用户全局只能获取一次令牌,然后存入到Redis缓存中。用户有令牌的话直接返回 “正在抢购中”。
项目基于多个微服务模块实现,使用Nacos做为微服务的注册与发现中心。引入gateway组件作为服务入口的统一管理。使用Open Feign作为服务间通信的组件,基于请求与响应的方式。使用seata组件提供分布式事务的服务。基于ES实现商品的检索等等。
查询商品详情,也就是从商城主页面点击一个商品到展示一个商品的详细信息这一步骤。由于逻辑比较复杂,而且有些数据还需要进行远程调用。
**(获取SKU基本信息 -> 获取SKU图片信息 -> 获取SKU促销信息 -> 获取SPU销售属性 -> 获取规格参数组以及组下参数-> 获取SPU详情)**为了优化这一部分的时间,引入了线程池技术从而多线程异步的方式执行以上任务。
但是异步任务又涉及到一个异步编排的问题,也就是说有些任务的先后顺序是有要求的。可以使用completableFuture
来完成异步编排。
抽取SSO单点登录模块,一切的登录请求由该模块处理。是基于相同顶级域名的SSO实现。
创建线程池之后,核心线程开始执行任务,核心线程占用完了,其他任务进入阻塞队列,队列满了但运行线程小于最大线程数,创建非核心线程立刻运行任务,空闲线程开始执行任务。没有空闲线程了并且队列也满了,使用拒绝策略拒绝其他任务。
拒绝策略:调用者执行、抛弃老任务策略、丢弃新任务策略、直接抛异常策略等等
单点登录英文全称Single Sign On,简称就是SSO。它的解释是:在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。
我们在浏览器(Browser)中访问一个应用,这个应用需要登录,我们填写完用户名和密码后,完成登录认证。这时,我们在这个用户的session中标记登录状态为yes(已登录),同时在浏览器(Browser)中写入Cookie,这个Cookie是这个用户的唯一标识。下次我们再访问这个应用的时候,请求中会带上这个Cookie,服务端会根据这个Cookie找到对应的session,通过session来判断这个用户是否登录。如果不做特殊配置,这个Cookie的名字叫做jsessionid
,值在服务端(server)是唯一的。
同顶级域名下的单点登录:
一个企业一般情况下只有一个域名,通过二级域名区分不同的系统。比如我们有个域名叫做:a.com,同时有两个业务系统分别为:app1.a.com和app2.a.com。我们要做单点登录(SSO),需要一个登录系统(微服务),叫做:sso.a.com。
我们只要在sso.a.com登录,app1.a.com和app2.a.com就也登录了。通过上面的登陆认证机制,我们可以知道,在sso.a.com中登录了,其实是在sso.a.com的服务端的session中记录了登录状态,同时在浏览器端(Browser)的sso.a.com下写入了Cookie。那么我们怎么才能让app1.a.com和app2.a.com登录呢?这里有两个问题:
解决办法:针对cookie跨域问题,将cookie域设置为顶域,即属于a.com。这样所有的子域系统都能访问到顶域的cookie。针对session问题,可以使用Spring-session 解决session共享问题。
spring-session 配合redis 将session 进行统一管理。
方便客户、不需要记住多个 ID 和密码。
默认不可以,但可以设置。
服务端使用使用@CrossOrigin
注解时。只需要在网页端设置跨域 XMLHttpRequest
请求的 withCredentials
属性就可以正常设置和获取跨域 Cookie。
分别压测了动静分离、redis中间件引入前的商城主页面接口。
解决数据库缓存的一致性问题。另外一种解决方式是采用改完数据库删缓存的方式。
canal通过模拟MySQL的从机,从而完成类似于MySQL的主从复制的过程。然后canal将从MySQL读到的数据同步到redis、ES中。
后面分布式事务中有详细讲解。
2pc,后面分布式事务中有详细讲解。
其中N为CPU的核心数。
可以依靠 Open Feign 组件内部封装的Ribbon实现提供相同服务的微服务的负载均衡。
@FeignClient("mall-coupon") // 标识了要去调用的那个微服务
public interface CouponFeignService {
@PostMapping("/coupon/spubounds/save")
R saveSpuBounds(@RequestBody SpuBoundTo spuBoundTo);
@PostMapping("/coupon/skufullreduction/saveinfo")
R saveSkuReduction(@RequestBody SkuReductionTo skuReductionTo);
}
在使用@FeignClient
注解的时候 是默认使用了Ribbon进行客户端的负载均衡的,默认的是随机的策略,那么如果我们想要更改策略的话,需要修改消费者yml
中的配置,如下:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.BestAvailableRule #配置规则 最空闲连接策略
加第三方登录(微博、QQ等)功能。完善SSO功能。
采用分库分表的方式设置数据库。为每一个微服务设置独立的数据库。
分布式ID的特点:全局唯一性、递增性、高可用性。
常见解决方案:UUID、雪花算法、UidGenerator、Leaf
雪花算法概要:SnowFlake
是Twitter公司采用的一种算法,目的是在分布式系统中产生全局唯一且趋势递增的ID。SnowFlake
算法在同一毫秒内最多可以生成多少个全局唯一ID呢: 同一毫秒的ID数量 = 1024 X 4096 = 4194304。雪花算法的实现主要依赖于数据中心ID和数据节点ID这两个参数。
微服务的特点:技术异构性、隔离性、可扩展性、易于优化性。
用户往购物车中添加商品并不会直接操作数据库,而是通过操作redis或者cookie然后最终通过redis的持久化机制存储到MySQL中。所以频繁的添加购物车不会有问题。
双写模式: 我们采取MySQL作为主要的数据存储,利用MySQL的事务特性维护数据一致性,使用ElasticSearch进行数据汇集和查询,此时es与数据库的同步方案就尤为重要。
保证es与数据库的同步方案:
1、首先添加商品入数据库,添加商品成功后,商品入ES,若入ES失败,将失败的商品ID放入redis的缓存队列(或MQ),且失败的商品ID入log文件(若出现redis挂掉,可从日志中取异常商品ID然后再入ES),
task任务每秒刷新一下redis缓存队列,若是从缓存队列中取到商品ID,则根据商品ID从数据库中获取商品数据然后入ES。
canal组件控制一致性
若入ES失败,将失败的商品ID放入redis的缓存队列(或MQ),且失败的商品ID入log文件(若出现redis挂掉,可从日志中取异常商品ID然后再入ES),
task任务每秒刷新一下redis缓存队列,若是从缓存队列中取到商品ID,则根据商品ID从数据库中获取商品数据然后入ES。