Java详细思维导图
2022-08-04 23:27:45 0 举报
AI智能生成
Java详细思维导图
作者其他创作
大纲/内容
java基础
基本数据类型
byte 1字节
char 2字节
short 2字节
int 4字节
float 4字节
long 8字节
double 8字节
boolean 至少1字节
char 2字节
short 2字节
int 4字节
float 4字节
long 8字节
double 8字节
boolean 至少1字节
JDK、JRE、JVM
JDK(Java Development Kit Java 开发工具包),JDK 是提供给 Java 开发人员使用的,其中包含了 Java 的开发工具,也包括了 JRE。其中的开发工具包括编译工具(javac.exe) 打包工具(jar.exe)等。
JRE(Java Runtime Environment Java 运行环境) 是 JDK 的子集,也就是包括 JRE 所有内容,以及开发应用程序所需的编译器和调试器等工具。JRE 提供了库、Java 虚拟机(JVM)和其他组件,用于运行 Java 编程语言、小程序、应用程序。
JVM(Java Virtual Machine Java 虚拟机),JVM 可以理解为是一个虚拟出来的计算机,具备着计算机的基本运算方式,它主要负责把 Java 程序生成的字节码文件,
JRE(Java Runtime Environment Java 运行环境) 是 JDK 的子集,也就是包括 JRE 所有内容,以及开发应用程序所需的编译器和调试器等工具。JRE 提供了库、Java 虚拟机(JVM)和其他组件,用于运行 Java 编程语言、小程序、应用程序。
JVM(Java Virtual Machine Java 虚拟机),JVM 可以理解为是一个虚拟出来的计算机,具备着计算机的基本运算方式,它主要负责把 Java 程序生成的字节码文件,
collection
hashMap
hashmap底层扩容线程安全问题 synchronized用在静态和非静态方法的区别
概念:Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入,通过散列算法,变换成固定长度的输出,该输出就是散列
两个不同的输入值,根据同一散列函数计算出的散列值相同的现象叫做碰撞。
两个不同的输入值,根据同一散列函数计算出的散列值相同的现象叫做碰撞。
常见的Hash函数:
直接定址法:直接以关键字k或者k加上某个常数(k+c)作为哈希地址。
数字分析法:提取关键字中取值比较均匀的数字作为哈希地址。
除留余数法:用关键字k除以某个不大于哈希表长度m的数p,将所得余数作为哈希表地址。
分段叠加法:按照哈希表地址位数将关键字分成位数相等的几部分,其中最后一部分可以比较短。然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。
平方取中法:如果关键字各个部分分布都不均匀的话,可以先求出它的平方值,然后按照需求取中间的几位作为哈希地址。
伪随机数法:采用一个伪随机数当作哈希函数。
衡量一个哈希函数的好坏的重要指标就是发生碰撞的概率以及发生碰撞的解决方案。任何哈希函数基本都无法彻底避免碰撞。
直接定址法:直接以关键字k或者k加上某个常数(k+c)作为哈希地址。
数字分析法:提取关键字中取值比较均匀的数字作为哈希地址。
除留余数法:用关键字k除以某个不大于哈希表长度m的数p,将所得余数作为哈希表地址。
分段叠加法:按照哈希表地址位数将关键字分成位数相等的几部分,其中最后一部分可以比较短。然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。
平方取中法:如果关键字各个部分分布都不均匀的话,可以先求出它的平方值,然后按照需求取中间的几位作为哈希地址。
伪随机数法:采用一个伪随机数当作哈希函数。
衡量一个哈希函数的好坏的重要指标就是发生碰撞的概率以及发生碰撞的解决方案。任何哈希函数基本都无法彻底避免碰撞。
hash的实现
int hash(Object k) //该方法主要是将Object转换成一个整型。
int indexFor(int h, int length) //该方法主要是将hash生成的整型转换成链表数组中的下标。
return h & (length-1); //位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。
扩容
为什么要扩容:上面说过哈希函数的好坏指标就是看哈希碰撞概率,为了避免碰撞进行扩容,否则,hash表会退化为链表
HashMap的扩容条件就是当HashMap中的元素个数(size)超过临界值(threshold)时就会自动扩容。
threshold = loadFactor * capacity。
loadFactor是装载因子,表示HashMap满的程度,默认值为0.75f,设置成0.75有一个好处,那就是0.75正好是3/4,而capacity又是2的幂。所以,两个数的乘积都是整数。
对于一个默认的HashMap来说,默认情况下,当其size大于12(16*0.75)时就会触发扩容。
threshold = loadFactor * capacity。
loadFactor是装载因子,表示HashMap满的程度,默认值为0.75f,设置成0.75有一个好处,那就是0.75正好是3/4,而capacity又是2的幂。所以,两个数的乘积都是整数。
对于一个默认的HashMap来说,默认情况下,当其size大于12(16*0.75)时就会触发扩容。
1.7以前使用头插法扩容,在线程安全情况下导致死循环
线程安全
总结:
HashMap作为一种数据结构,元素在put的过程中需要进行hash运算,目的是计算出该元素存放在hashMap中的具体位置。
hash运算的过程其实就是对目标元素的Key进行hashcode,再对Map的容量进行取模,而JDK 的工程师为了提升取模的效率,使用位运算代替了取模运算,这就要求Map的容量一定得是2的幂。
而作为默认容量,太大和太小都不合适,所以16就作为一个比较合适的经验值被采用了。
为了保证任何情况下Map的容量都是2的幂,HashMap在两个地方都做了限制。
hash运算的过程其实就是对目标元素的Key进行hashcode,再对Map的容量进行取模,而JDK 的工程师为了提升取模的效率,使用位运算代替了取模运算,这就要求Map的容量一定得是2的幂。
而作为默认容量,太大和太小都不合适,所以16就作为一个比较合适的经验值被采用了。
为了保证任何情况下Map的容量都是2的幂,HashMap在两个地方都做了限制。
首先是,如果用户制定了初始容量,那么HashMap会计算出比该数大的第一个2的幂作为初始容量。
另外,在扩容的时候,也是进行成倍的扩容,即4变成8,8变成16。
===================================================
为什么扩容用2的N次方
因为2的N次方,算数组下标取模的时候可以用位运算代替,右移N位,在10万次循环中,位运算比取模运算快20倍;还有就是如果不是2的N次方,比如17,对应的二进制(n-1)只会有一位为1,这样1太少了,&之后趋于0,这样又会增加碰撞
另外,在扩容的时候,也是进行成倍的扩容,即4变成8,8变成16。
===================================================
为什么扩容用2的N次方
因为2的N次方,算数组下标取模的时候可以用位运算代替,右移N位,在10万次循环中,位运算比取模运算快20倍;还有就是如果不是2的N次方,比如17,对应的二进制(n-1)只会有一位为1,这样1太少了,&之后趋于0,这样又会增加碰撞
和 HashTable 总结
HashMap默认的初始化大小为16,之后每次扩充为原来的2倍。
HashTable默认的初始大小为11,之后每次扩充为原来的2n+1。
当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀,所以单从这一点上看,HashTable的哈希表大小选择,似乎更高明些。因为hash结果越分散效果越好。
在取模计算时,如果模数是2的幂,那么我们可以直接使用位运算来得到结果,效率要大大高于做除法。所以从hash计算的效率上,又是HashMap更胜一筹。
但是,HashMap为了提高效率使用位运算代替哈希,这又引入了哈希分布不均匀的问题,所以HashMap为解决这问题,又对hash算法做了一些改进,进行了扰动计算。
HashTable默认的初始大小为11,之后每次扩充为原来的2n+1。
当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀,所以单从这一点上看,HashTable的哈希表大小选择,似乎更高明些。因为hash结果越分散效果越好。
在取模计算时,如果模数是2的幂,那么我们可以直接使用位运算来得到结果,效率要大大高于做除法。所以从hash计算的效率上,又是HashMap更胜一筹。
但是,HashMap为了提高效率使用位运算代替哈希,这又引入了哈希分布不均匀的问题,所以HashMap为解决这问题,又对hash算法做了一些改进,进行了扰动计算。
ArrayBlockingQueue
具有线程安全性和阻塞性的有界队列
ArrayBlockingQueue的线程安全是通过底层的ReentrantLock保证的
构造方法
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0) throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0) throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
Object[] items:队列的底层由数组组成,并且数组的长度在初始化就已经固定,之后无法改变
ReentrantLock lock:控制队列操作的独占锁,在操作队列的元素前需要获取锁,保护竞争资源
Condition notEmpty:条件对象,如果有线程从队列中获取元素时队列为空,就会在此进行等待,直到其他线程向队列后插入元素才会被唤醒
Condition notFull:如果有线程试图向队列中插入元素,且此时队列为满时,就会在这进行等待,直到其他线程取出队列中的元素才会被唤醒
相关操作
子主题
ConcurrentHashMap
--java7
1、ConcurrentHashMap的哪些操作需要加锁?
答:只有写入操作才需要加锁,读取操作不需要加锁
2、ConcurrentHashMap的无锁读是如何实现的?
答:首先HashEntry中的value和next都是有volatile修饰的,其次在写入操作的时候通过调用UNSAFE库延迟同步了主存,保证了数据的一致性
3、在多线程的场景下调用size()方法获取ConcurrentHashMap的大小有什么挑战?ConcurrentHashMap是怎么解决的?
答:size()具有全局的语义,如何能保证在不加全局锁的情况下读取到全局状态的值是一个很大的挑战,ConcurrentHashMap通过查看两次无锁读中间是否发生了写入操作来决定读取到的size()是否可信,如果写入操作频繁,则再退化为全局加锁读取。
4、在有Segment存在的前提下,是如何扩容的?
答:segment数组的大小在一开始初始化的时候就已经决定了,扩容主要扩的是HashEntry数组,基本的思路与HashTable一致,但这是一个线程不安全方法,调用之前需要加锁。
5、为什么 ConcurrentHashMap 的读操作不需要加锁
get操作全程不需要加锁是因为Node的成员val是用volatile修饰的和数组用volatile修饰没有关系,数组用volatile修饰主要是保证在数组扩容的时候保证可见性。
1、ConcurrentHashMap的哪些操作需要加锁?
答:只有写入操作才需要加锁,读取操作不需要加锁
2、ConcurrentHashMap的无锁读是如何实现的?
答:首先HashEntry中的value和next都是有volatile修饰的,其次在写入操作的时候通过调用UNSAFE库延迟同步了主存,保证了数据的一致性
3、在多线程的场景下调用size()方法获取ConcurrentHashMap的大小有什么挑战?ConcurrentHashMap是怎么解决的?
答:size()具有全局的语义,如何能保证在不加全局锁的情况下读取到全局状态的值是一个很大的挑战,ConcurrentHashMap通过查看两次无锁读中间是否发生了写入操作来决定读取到的size()是否可信,如果写入操作频繁,则再退化为全局加锁读取。
4、在有Segment存在的前提下,是如何扩容的?
答:segment数组的大小在一开始初始化的时候就已经决定了,扩容主要扩的是HashEntry数组,基本的思路与HashTable一致,但这是一个线程不安全方法,调用之前需要加锁。
5、为什么 ConcurrentHashMap 的读操作不需要加锁
get操作全程不需要加锁是因为Node的成员val是用volatile修饰的和数组用volatile修饰没有关系,数组用volatile修饰主要是保证在数组扩容的时候保证可见性。
IO
BIO
同步并阻塞,在服务器中实现的模式为一个连接一个线程。也就是说,客户端有连接请求的时候,服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然这也可以通过线程池机制改善。BIO一般适用于连接数目小且固定的架构,这种方式对于服务器资源要求比较高,而且并发局限于应用中,是JDK1.4之前的唯一选择,但好在程序直观简单,易理解。
NIO
同步并非阻塞,在服务器中实现的模式为一个请求一个线程,也就是说,客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到有连接IO请求时才会启动一个线程进行处理。NIO一般适用于连接数目多且连接比较短(轻操作)的架构,并发局限于应用中,编程比较复杂,从JDK1.4开始支持。
AIO
异步并非阻塞,在服务器中实现的模式为一个有效请求一个线程,也就是说,客户端的IO请求都是通过操作系统先完成之后,再通知服务器应用去启动线程进行处理。AIO一般适用于连接数目多且连接比较长(重操作)的架构,充分调用操作系统参与并发操作,编程比较复杂,从JDK1.7开始支持。
IO
按处理的数据单位不同
字节流
FileOutputStream类
FileInputStream类
字符流
FileWriter类
FileReader类
字节/字符流都可以通过传递一个字节/字符数组创建缓冲区读取
每次读取多个字节,减少了系统间的IO操作次数
从而提高读写的效率
每次读取多个字节,减少了系统间的IO操作次数
从而提高读写的效率
模板
// 定义变量,作为有效个数
int len ;
// 定义字节数组,作为装字节数据的容器(字符数组同理)
byte[] b = new byte[1024]; //数组大小一般开1024的倍数
// 循环读取
while (( len= fis.read(b))!=-1) {
// 每次读取后,把数组变成字符串打印
System.out.println(new String(b),0,len);// len 每次读取的有效字节个数
}
// 定义变量,作为有效个数
int len ;
// 定义字节数组,作为装字节数据的容器(字符数组同理)
byte[] b = new byte[1024]; //数组大小一般开1024的倍数
// 循环读取
while (( len= fis.read(b))!=-1) {
// 每次读取后,把数组变成字符串打印
System.out.println(new String(b),0,len);// len 每次读取的有效字节个数
}
按数据流方向不同
输入流
输出流
按功能不同
节点流
处理流
缓冲流
字节缓冲流
BufferedInputStream类
BufferedOutputStream类
字符缓冲流
BufferedReader类
BufferedWriter类
输出流数据追加续写:同理 在传递的输出流对象的构造方法中添加一个参数true
BufferedOutputStream(new FileOutputStream(String name,true))
BufferedWriter(new FileWriter(String name,true))
BufferedOutputStream(new FileOutputStream(String name,true))
BufferedWriter(new FileWriter(String name,true))
转换流
转换流是字节与字符间的桥梁
InputStreamReader类
OutputStreamWriter类
对象流
概念:对象序列化/反序列化
Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象
该字节序列包含该对象的数据、对象的类型和对象中存储的属性等信息
字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息
反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化
对象的数据、对象的类型和对象中存储的数据信息,都可以用来在内存中创建对象
该字节序列包含该对象的数据、对象的类型和对象中存储的属性等信息
字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息
反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化
对象的数据、对象的类型和对象中存储的数据信息,都可以用来在内存中创建对象
ObjectOutputStream类
ObjectInputStream类
打印流
printStream类
构造方法
PrintStream(String fileName)使用指定的文件名创建一个新的打印流
PrintStream(OutputStream out)使用指定的字节输出流创建一个新的打印流
PrintStream(File file)使用指定的文件对象创建一个新的打印流
特有方法
void print(任意类型的值)
void println()
static void setOut(printStream out)改变打印流流向
public class PrintDemo {
public static void main(String[] args) throws IOException {
// 调用系统的打印流,控制台直接输出97
System.out.println(97);
// 创建打印流,指定文件的名称
PrintStream ps = new PrintStream("ps.txt");
// 设置系统的打印流流向,输出到ps.txt
System.setOut(ps);
// 调用系统的打印流,ps.txt中输出97
System.out.println(97);
}
}
public static void main(String[] args) throws IOException {
// 调用系统的打印流,控制台直接输出97
System.out.println(97);
// 创建打印流,指定文件的名称
PrintStream ps = new PrintStream("ps.txt");
// 设置系统的打印流流向,输出到ps.txt
System.setOut(ps);
// 调用系统的打印流,ps.txt中输出97
System.out.println(97);
}
}
数据流
都继承于四种抽象流类型
字节输出流OutputStream
字节输入流InputStream
字符输出流Writer
字符输入流Reader
属性集
java.util.Properties 继承于Hashtable,来表示一个持久的属性集。
它使用键值结构存储数据,每个键及其对应值都是一个字符串。
该类也被许多Java类使用,比如获取系统属性时,System.getProperties方法就是返回一个Properties对象。
它使用键值结构存储数据,每个键及其对应值都是一个字符串。
该类也被许多Java类使用,比如获取系统属性时,System.getProperties方法就是返回一个Properties对象。
构造方法
public Properties():创建一个空的属性列表
基本存储方法
Object setProperty(String key, String value): 保存一对属性
String getProperty(String key) :使用此属性列表中指定的键搜索属性值
Set<String> stringPropertyNames() :所有键的名称的集合
与流相关的方法
void load(InputStream inStream): 从字节输入流中读取键值对
反射
反射机制:在运行过程中,对于任意一个类,都能知道其所有的属性和方法;对于任意一个对象,都能调用其属性和方法;这种动态获取类信息和调用对象方法的功能,就是 Java 反射机制。
⊙在运行时判断任意一个对象所属的类;
⊙在运行时构造任意一个类的对象;
⊙在运行时判断任意一个类所具有的成员变量和方法;
⊙在运行时调用任意一个对象的成员变量和方法;
⊙生成动态代理。
⊙在运行时构造任意一个类的对象;
⊙在运行时判断任意一个类所具有的成员变量和方法;
⊙在运行时调用任意一个对象的成员变量和方法;
⊙生成动态代理。
Thread
进程
进程间通信方式
管道
消息队列
共享内存
信号量
信号
套接字
和线程的区别
进程是资源分配的最小单位,线程是CPU调度的最小单位
线程是进程中执行运算的最小单位
线程状态
1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3.阻塞(BLOCKED):表示线程阻塞于锁。
4.等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5.超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6. 终止(TERMINATED):表示该线程已经执行完毕。
2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3.阻塞(BLOCKED):表示线程阻塞于锁。
4.等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5.超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6. 终止(TERMINATED):表示该线程已经执行完毕。
线程状态切换:
volatile
普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
volatile关键字对于基本类型的修改可以在随后对多个线程的读保持一致,但是对于引用类型如数组,实体bean,仅仅保证引用的可见性,但并不保证引用内容的可见性。
禁止进行指令重排序。
背景:为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。
如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。
在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。
总结下来:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量的缓存行无效,所以线程1再次读取变量的值时会去主存读取。
锁
类别图
synchronized(可重入,非公平)
synchronized 修饰实例方法,相当于是对类的实例进行加锁,进入同步代码前需要获得当前实例的锁
synchronized 修饰静态方法,相当于是对类对象进行加锁
synchronized 修饰代码块,相当于是给对象进行加锁,在进入代码块前需要先获得对象的锁
ReentrantLock(可重入,默认非公平)
ThreadLocal
作用: Threadlocal 主要用来做线程变量的隔离
ThreadLocal提供的只是一个浅拷贝,如果变量是一个引用类型,那么就要考虑它内部的状态是否会被改变,想要解决这个问题可以通过重写ThreadLocal的initialValue()函数来自己实现深拷贝,建议在使用ThreadLocal时一开始就重写该函数。
ThreadLocal与像synchronized这样的锁机制是不同的。首先,它们的应用场景与实现思路就不一样,锁更强调的是如何同步多个线程去正确地共享一个变量,ThreadLocal则是为了解决同一个变量如何不被多个线程共享。从性能开销的角度上来讲,如果锁机制是用时间换空间的话,那么ThreadLocal就是用空间换时间。
ThreadLocal与像synchronized这样的锁机制是不同的。首先,它们的应用场景与实现思路就不一样,锁更强调的是如何同步多个线程去正确地共享一个变量,ThreadLocal则是为了解决同一个变量如何不被多个线程共享。从性能开销的角度上来讲,如果锁机制是用时间换空间的话,那么ThreadLocal就是用空间换时间。
数据结构
子主题
java引用类型
强引用
在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
软引用
软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
弱引用
弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
虚引用
虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。
在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
软引用
软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
弱引用
弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
虚引用
虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。
内存泄露
图列
子主题
线程池
优点:
降低系统资源消耗。通过复用已存在的线程,降低线程创建和销毁造成的消耗;
提高响应速度。当有任务到达时,无需等待新线程的创建便能立即执行;
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗大量系统资源,还会降低系统的稳定性,使用线程池可以进行对线程进行统一的分配、调优和监控。
提高响应速度。当有任务到达时,无需等待新线程的创建便能立即执行;
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗大量系统资源,还会降低系统的稳定性,使用线程池可以进行对线程进行统一的分配、调优和监控。
子主题
核心参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
1.corePoolSize :核心线程数,不管它们创建以后是不是空闲的。线程池需要保持 corePoolSize 数量的线程,除非设置了 allowCoreThreadTimeOut
2.maximumPoolSize: 最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数
3.keepAliveTime :最大线程数可以存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程
4.unit : keepAliveTime 的时间单位,一般设置成秒或毫秒。
5.workQueue : 任务队列,存放等待执行的任务
6.threadFactory: 创建线程的任务工厂,比如给线程命名加上前缀,后面会讲
7.handler : 拒绝任务处理器,当任务处理不过来时的拒绝处理器
8.allowCoreThreadTimeOut : 是否允许核心线程超时销毁,这个参数不在构造函数中,但重要性也很高
2.maximumPoolSize: 最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数
3.keepAliveTime :最大线程数可以存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程
4.unit : keepAliveTime 的时间单位,一般设置成秒或毫秒。
5.workQueue : 任务队列,存放等待执行的任务
6.threadFactory: 创建线程的任务工厂,比如给线程命名加上前缀,后面会讲
7.handler : 拒绝任务处理器,当任务处理不过来时的拒绝处理器
8.allowCoreThreadTimeOut : 是否允许核心线程超时销毁,这个参数不在构造函数中,但重要性也很高
workQueue:
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
较常用的是 LinkedBlockingQueue 和 Synchronous,线程池的排队策略与 BlockingQueue 有关。
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
较常用的是 LinkedBlockingQueue 和 Synchronous,线程池的排队策略与 BlockingQueue 有关。
子主题
handler拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:
AbortPolicy:拒绝并抛出异常。
CallerRunsPolicy:使用当前调用的线程来执行此任务。
DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
DiscardPolicy:忽略并抛弃当前任务。
默认策略为 AbortPolicy。
AbortPolicy:拒绝并抛出异常。
CallerRunsPolicy:使用当前调用的线程来执行此任务。
DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
DiscardPolicy:忽略并抛弃当前任务。
默认策略为 AbortPolicy。
子主题
创建
Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;使用的是LinkedBlockingQueue队列
Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置,后面会详细讲。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置,后面会详细讲。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
执行流程
ThreadPoolExecutor 关键节点的执行流程如下:
当线程数小于核心线程数时,创建线程。
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列已满:若线程数小于最大线程数,创建线程;若线程数等于最大线程数,抛出异常,拒绝任务。
当线程数小于核心线程数时,创建线程。
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列已满:若线程数小于最大线程数,创建线程;若线程数等于最大线程数,抛出异常,拒绝任务。
子主题
JVM
运行时数据区
方法区(该区域线程共享)
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据(1)线程共享的(2)运行时常量池:
虚拟机栈
虚拟机栈是Java执行方法的内存模型。每个方法被执行的时候,都会创建一个栈帧,把栈帧压人栈,当方法正常返回或者抛出未捕获的异常时,栈帧就会出栈。
(1)栈帧:栈帧存储方法的相关信息,包含局部变量数表、返回值、操作数栈、动态链接
a、局部变量表:包含了方法执行过程中的所有变量。局部变量数组所需要的空间在编译期间完成分配,在方法运行期间不会改变局部变量数组的大小。
b、返回值:如果有返回值的话,压入调用者栈帧中的操作数栈中,并且把PC的值指向 方法调用指令 后面的一条指令地址。
c、操作数栈:操作变量的内存模型。操作数栈的最大深度在编译的时候已经确定(写入方法区code属性的max_stacks项中)。操作数栈的的元素可以是任意Java类型,包括long和double,32位数据占用栈空间为1,64位数据占用2。方法刚开始执行的时候,栈是空的,当方法执行过程中,各种字节码指令往栈中存取数据。
d、动态链接:每个栈帧都持有在运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
(2)线程私有
(1)栈帧:栈帧存储方法的相关信息,包含局部变量数表、返回值、操作数栈、动态链接
a、局部变量表:包含了方法执行过程中的所有变量。局部变量数组所需要的空间在编译期间完成分配,在方法运行期间不会改变局部变量数组的大小。
b、返回值:如果有返回值的话,压入调用者栈帧中的操作数栈中,并且把PC的值指向 方法调用指令 后面的一条指令地址。
c、操作数栈:操作变量的内存模型。操作数栈的最大深度在编译的时候已经确定(写入方法区code属性的max_stacks项中)。操作数栈的的元素可以是任意Java类型,包括long和double,32位数据占用栈空间为1,64位数据占用2。方法刚开始执行的时候,栈是空的,当方法执行过程中,各种字节码指令往栈中存取数据。
d、动态链接:每个栈帧都持有在运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
(2)线程私有
本地方法栈
(1)调用本地native的内存模型(2)线程独享
堆(该区域线程共享)
Java对象存储的地方
(1)Java堆是虚拟机管理的内存中最大的一块
(2)Java堆是所有线程共享的区域
(3)在虚拟机启动时创建
(4)此内存区域的唯一目的就是存放对象实例,几乎所有对象实例都在这里分配内存。存放new生成的对象和数组
(5)Java堆是垃圾收集器管理的内存区域,因此很多时候称为“GC堆”
(1)Java堆是虚拟机管理的内存中最大的一块
(2)Java堆是所有线程共享的区域
(3)在虚拟机启动时创建
(4)此内存区域的唯一目的就是存放对象实例,几乎所有对象实例都在这里分配内存。存放new生成的对象和数组
(5)Java堆是垃圾收集器管理的内存区域,因此很多时候称为“GC堆”
堆内存的对象不一定是共享的,每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块”私有”内存中分配,当这部分区域用完之后,再分配新的”私有”内存。,这种方案称为:TLAB分配,即Thread Local Allocation Buffer(https://mp.weixin.qq.com/s/-tfs9nkufS6Hh4tSYkkCxQ)
虚拟机编译优化技术:逃逸分析(标量替换、栈上分配https://mp.weixin.qq.com/s/Owlhu5IFpDAyu0WYcK1EhQ)、 锁消除、 锁膨胀、 方法内联、 空值检查消除、 类型检测消除、 公共子表达式消除等
程序计数器
指向当前线程正在执行的字节码指令,线程私有的
正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)
结构示意图
子主题
各版本jdk下jvm内存区别
JDK1.6、JDK1.7、JDK1.8 JVM 内存模型主要有以下差异:
JDK 1.6:有永久代,静态变量存放在永久代上。
JDK 1.7:有永久代,但已经把字符串常量池、静态变量,存放在堆上。逐渐的减少永久代的使用。
JDK 1.8:无永久代,运行时常量池、类常量池,都保存在元数据区,也就是常说的元空间。但字符串常量池仍然存放在堆上。
JDK 1.6:有永久代,静态变量存放在永久代上。
JDK 1.7:有永久代,但已经把字符串常量池、静态变量,存放在堆上。逐渐的减少永久代的使用。
JDK 1.8:无永久代,运行时常量池、类常量池,都保存在元数据区,也就是常说的元空间。但字符串常量池仍然存放在堆上。
子主题
JVM中的常量池
Class文件常量池。class文件是一组以字节为单位的二进制数据流,在java代码的编译期间,我们编写的java文件就被编译为.class文件格式的二进制数据存放在磁盘中,其中就包括class文件常量池。
运行时常量池:运行时常量池相对于class常量池一大特征就是具有动态性,java规范并不要求常量只能在运行时才产生,也就是说运行时常量池的内容并不全部来自class常量池,在运行时可以通过代码生成常量并将其放入运行时常量池中,这种特性被用的最多的就是String.intern()。
全局字符串常量池:字符串常量池是JVM所维护的一个字符串实例的引用表,在HotSpot VM中,它是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用,底层C++实现就是一个Hashtable。这些被维护的引用所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。
基本类型包装类对象常量池:java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外上面这5种整型的包装类也只是在对应值[-128,127)才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。
运行时常量池:运行时常量池相对于class常量池一大特征就是具有动态性,java规范并不要求常量只能在运行时才产生,也就是说运行时常量池的内容并不全部来自class常量池,在运行时可以通过代码生成常量并将其放入运行时常量池中,这种特性被用的最多的就是String.intern()。
全局字符串常量池:字符串常量池是JVM所维护的一个字符串实例的引用表,在HotSpot VM中,它是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用,底层C++实现就是一个Hashtable。这些被维护的引用所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。
基本类型包装类对象常量池:java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外上面这5种整型的包装类也只是在对应值[-128,127)才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。
堆内存
堆内存的划分图(G1不是这样的哦)
子主题
1、新生代:
(1)使用复制清除算法(Copinng算法),原因是年轻代每次GC都要回收大部分对象。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。
(2)分为Eden、Survivor From、Survivor To,比例默认为8:1:1
(3)内存不足时发生Minor GC
2、老年代:
(1)采用标记-整理算法(mark-compact),原因是老年代每次GC只会回收少部分对象。
3、Perm:用来存储类的元数据,也就是方法区。
(1)Perm的废除:在jdk1.8中,Perm被替换成MetaSpace,MetaSpace存放在本地内存中。原因是永久代进场内存不够用,或者发生内存泄漏。
(2)MetaSpace(元空间):元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
Metaspace used 2425K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 262K, capacity 386K, committed 512K, reserved 1048576K
4、堆内存的划分在JVM里面的示意图:
(1)使用复制清除算法(Copinng算法),原因是年轻代每次GC都要回收大部分对象。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。
(2)分为Eden、Survivor From、Survivor To,比例默认为8:1:1
(3)内存不足时发生Minor GC
2、老年代:
(1)采用标记-整理算法(mark-compact),原因是老年代每次GC只会回收少部分对象。
3、Perm:用来存储类的元数据,也就是方法区。
(1)Perm的废除:在jdk1.8中,Perm被替换成MetaSpace,MetaSpace存放在本地内存中。原因是永久代进场内存不够用,或者发生内存泄漏。
(2)MetaSpace(元空间):元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
Metaspace used 2425K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 262K, capacity 386K, committed 512K, reserved 1048576K
- used:加载的类的空间量。
- capacity: 当前分配块的元数据的空间。
- committed: 空间块的数量。
- reserved:元数据的空间保留(但不一定提交)的量。
4、堆内存的划分在JVM里面的示意图:
子主题
回收
类型(谈堆内存结构模型需先明白垃圾回收器有哪些,不然没意义,难道G1有eden,s0,s1?)
Serial(串行GC)-复制
ParNew(并行GC)-复制
Parallel [ˈpærəlel] Scavenge [ˈskævɪndʒ](并行回收GC)-复制
Serial Old(MSC)(串行GC)-标记-整理
CMS(并发GC)-标记-清除
Parallel [ˈpærəlel] Old(并行GC)--标记-整理
G1(JDK1.7update14才可以正式商用)
1~3用于年轻代垃圾回收:年轻代的垃圾回收称为minor GC
4~6用于年老代垃圾回收(当然也可以用于方法区的回收):年老代的垃圾回收称为full GC
G1独立完成"分代垃圾回收"
并行与并发,并行收集和并发收集是不一样的噢
并行:多条垃圾回收线程同时操作
并发:垃圾回收线程与用户线程一起操作
并行:多条垃圾回收线程同时操作
并发:垃圾回收线程与用户线程一起操作
怎么看gc类型:java -XX:+PrintCommandLineFlags -version
子主题
Serial/Serial Old
特点:
年轻代Serial收集器采用单个GC线程实现"复制"算法(包括扫描、复制)
年老代Serial Old收集器采用单个GC线程实现"标记-整理"算法
Serial与Serial Old都会暂停所有用户线程(即STW)
说明:
STW(stop the world):编译代码时为每一个方法注入safepoint(方法中循环结束的点、方法执行结束的点),在暂停应用时,需要等待所有的用户线程进入safepoint,之后暂停所有线程,然后进行垃圾回收。
适用场合:
CPU核数<2,物理内存<2G的机器(简单来讲,单CPU,新生代空间较小且对STW时间要求不高的情况下使用)
-XX:UseSerialGC:强制使用该GC组合
-XX:PrintGCApplicationStoppedTime:查看STW时间
由于它实现相对简单,没有线程相关的额外开销(主要指线程切换与同步),因此非常适合运行于客户端PC的小型应用程序,或者桌面应用程序(比如swing编写的用户界面程序),以及我们平时的开发、调试、测试等。
年轻代Serial收集器采用单个GC线程实现"复制"算法(包括扫描、复制)
年老代Serial Old收集器采用单个GC线程实现"标记-整理"算法
Serial与Serial Old都会暂停所有用户线程(即STW)
说明:
STW(stop the world):编译代码时为每一个方法注入safepoint(方法中循环结束的点、方法执行结束的点),在暂停应用时,需要等待所有的用户线程进入safepoint,之后暂停所有线程,然后进行垃圾回收。
适用场合:
CPU核数<2,物理内存<2G的机器(简单来讲,单CPU,新生代空间较小且对STW时间要求不高的情况下使用)
-XX:UseSerialGC:强制使用该GC组合
-XX:PrintGCApplicationStoppedTime:查看STW时间
由于它实现相对简单,没有线程相关的额外开销(主要指线程切换与同步),因此非常适合运行于客户端PC的小型应用程序,或者桌面应用程序(比如swing编写的用户界面程序),以及我们平时的开发、调试、测试等。
ParNew/Serial Old
子主题
说明:
ParNew除了采用多GC线程来实现复制算法以外,其他都与Serial一样,但是此组合中的Serial Old又是一个单GC线程,所以该组合是一个比较尴尬的组合,在单CPU情况下没有Serial/Serial Old速度快(因为ParNew多线程需要切换),在多CPU情况下又没有之后的三种组合快(因为Serial Old是单GC线程),所以使用其实不多。
-XX:ParallelGCThreads:指定ParNew GC线程的数量,默认与CPU核数相同,该参数在于CMS GC组合时,也可能会用到
Parallel Scavenge/Parallel Old
特点:
年轻代Parallel Scavenge收集器采用多个GC线程实现"复制"算法(包括扫描、复制)
年老代Parallel Old收集器采用多个GC线程实现"标记-整理"算法
Parallel Scavenge与Parallel Old都会暂停所有用户线程(即STW)
说明:
吞吐量:CPU运行代码时间/(CPU运行代码时间+GC时间)
CMS主要注重STW的缩短(该时间越短,用户体验越好,所以主要用于处理很多的交互任务的情况)
Parallel Scavenge/Parallel Old主要注重吞吐量(吞吐量越大,说明CPU利用率越高,所以主要用于处理很多的CPU计算任务而用户交互任务较少的情况)
年轻代Parallel Scavenge收集器采用多个GC线程实现"复制"算法(包括扫描、复制)
年老代Parallel Old收集器采用多个GC线程实现"标记-整理"算法
Parallel Scavenge与Parallel Old都会暂停所有用户线程(即STW)
说明:
吞吐量:CPU运行代码时间/(CPU运行代码时间+GC时间)
CMS主要注重STW的缩短(该时间越短,用户体验越好,所以主要用于处理很多的交互任务的情况)
Parallel Scavenge/Parallel Old主要注重吞吐量(吞吐量越大,说明CPU利用率越高,所以主要用于处理很多的CPU计算任务而用户交互任务较少的情况)
参数设置:
-XX:+UseParallelOldGC:使用该GC组合
-XX:GCTimeRatio:直接设置吞吐量大小,假设设为19,则允许的最大GC时间占总时间的1/(1 +19),默认值为99,即1/(1+99)
-XX:MaxGCPauseMillis:最大GC停顿时间,该参数并非越小越好
-XX:+UseAdaptiveSizePolicy:开启该参数,-Xmn/-XX:SurvivorRatio/-XX:PretenureSizeThreshold这些参数就不起作用了,虚拟机会自动收集监控信息,动态调整这些参数以提供最合适的的停顿时间或者最大的吞吐量(GC自适应调节策略),而我们需要设置的就是-Xmx,-XX:+UseParallelOldGC或-XX:GCTimeRatio两个参数就好(当然-Xms也指定上与-Xmx相同就好)
适用场合:
1.很多的CPU计算任务而用户交互任务较少的情况
2.不想自己去过多的关注GC参数,想让虚拟机自己进行调优工作
3.对吞吐量要求较高,或需要达到一定的量。
-XX:+UseParallelOldGC:使用该GC组合
-XX:GCTimeRatio:直接设置吞吐量大小,假设设为19,则允许的最大GC时间占总时间的1/(1 +19),默认值为99,即1/(1+99)
-XX:MaxGCPauseMillis:最大GC停顿时间,该参数并非越小越好
-XX:+UseAdaptiveSizePolicy:开启该参数,-Xmn/-XX:SurvivorRatio/-XX:PretenureSizeThreshold这些参数就不起作用了,虚拟机会自动收集监控信息,动态调整这些参数以提供最合适的的停顿时间或者最大的吞吐量(GC自适应调节策略),而我们需要设置的就是-Xmx,-XX:+UseParallelOldGC或-XX:GCTimeRatio两个参数就好(当然-Xms也指定上与-Xmx相同就好)
适用场合:
1.很多的CPU计算任务而用户交互任务较少的情况
2.不想自己去过多的关注GC参数,想让虚拟机自己进行调优工作
3.对吞吐量要求较高,或需要达到一定的量。
ParNew/CMS
说明:
以上只是年老代CMS收集的过程,年轻代ParNew看"ParNew/Serial Old"就好
CMS是多回收线程的,不要被上图误导,默认的线程数:(CPU数量+3)/4
CMS主要注重STW的缩短(该时间越短,用户体验越好,所以主要用于处理很多的交互任务的情况)
特点:
1.年轻代ParNew收集器采用多个GC线程实现"复制"算法(包括扫描、复制)
2.年老代CMS收集器采用多线程实现"标记-清除"算法
初始标记:标记与根集合节点直接关联的节点。时间非常短,需要STW
并发标记:遍历之前标记到的关联节点,继续向下标记所有存活节点。时间较长。
重新标记:重新遍历trace并发期间修改过的引用关系对象。时间介于初始标记与并发标记之间,通常不会很长。需要STW
并发清理:直接清除非存活对象,清理之后,将该线程占用的CPU切换给用户线程
3.初始标记与重新标记都会暂停所有用户线程(即STW),但是时间较短;并发标记与并发清理时间较长,但是不需要STW
关于并发标记期间怎样记录发生变动的引用关系对象,在重新标记期间怎样扫描这些对象
缺点:
并发标记与并发清理:按照说明的第二点来讲,假设有2个CPU,那么其中有一个CPU会用于垃圾回收,而另一个用于用户线程,这样的话,之前是两CPU运行用户线程,现在是一个,那么效率就会急剧下降。也就是说,降低了吞吐量(即降低了CPU使用率)。
并发清理:在这一过程中,产生的垃圾无法被清理(因为发生在重新标记之后)
并发标记与并发清理:由于是与用户线程并发的,所以用户线程可能会分配对象,这样既可能对象直接进入年老代(例如,大对象),也可能进入年轻代后,年轻代发生minor GC,这样的话,实际上要求我们的年老代需要预留一定空间,也就是说要在年老代还有一定空间的情况下就要进行垃圾回收,留出一定内存空间来供其他线程使用,而不能等到年老代快爆满了才进行垃圾回收,通过-XX:CMSInitiatingOccupancyFraction来指定当年老代空间满了多少后进行垃圾回收
标记-清理算法:会产生内存碎片,由于是在老年代,可能会提前触发Full GC(这正是我们要尽量减少的)
参数设置:
-XX:+UseConcMarkSweepGC:使用该GC组合
-XX:CMSInitiatingOccupancyFraction:指定当年老代空间满了多少后进行垃圾回收
-XX:+UseCMSCompactAtFullCollection:(默认是开启的)在CMS收集器顶不住要进行FullGC时开启内存碎片整理过程,该过程需要STW
-XX:CMSFullGCsBeforeCompaction:指定多少次FullGC后才进行整理
-XX:ParallelCMSThreads:指定CMS回收线程的数量,默认为:(CPU数量+3)/4
适用场合:
用于处理很多的交互任务的情况
方法区的回收一般使用CMS,配置两个参数:-XX:+CMSPermGenSweepingEnabled与-XX:+CMSClassUnloadingEnabled
适用于一些需要长期运行且对相应时间有一定要求的后台程序
以上只是年老代CMS收集的过程,年轻代ParNew看"ParNew/Serial Old"就好
CMS是多回收线程的,不要被上图误导,默认的线程数:(CPU数量+3)/4
CMS主要注重STW的缩短(该时间越短,用户体验越好,所以主要用于处理很多的交互任务的情况)
特点:
1.年轻代ParNew收集器采用多个GC线程实现"复制"算法(包括扫描、复制)
2.年老代CMS收集器采用多线程实现"标记-清除"算法
初始标记:标记与根集合节点直接关联的节点。时间非常短,需要STW
并发标记:遍历之前标记到的关联节点,继续向下标记所有存活节点。时间较长。
重新标记:重新遍历trace并发期间修改过的引用关系对象。时间介于初始标记与并发标记之间,通常不会很长。需要STW
并发清理:直接清除非存活对象,清理之后,将该线程占用的CPU切换给用户线程
3.初始标记与重新标记都会暂停所有用户线程(即STW),但是时间较短;并发标记与并发清理时间较长,但是不需要STW
关于并发标记期间怎样记录发生变动的引用关系对象,在重新标记期间怎样扫描这些对象
缺点:
并发标记与并发清理:按照说明的第二点来讲,假设有2个CPU,那么其中有一个CPU会用于垃圾回收,而另一个用于用户线程,这样的话,之前是两CPU运行用户线程,现在是一个,那么效率就会急剧下降。也就是说,降低了吞吐量(即降低了CPU使用率)。
并发清理:在这一过程中,产生的垃圾无法被清理(因为发生在重新标记之后)
并发标记与并发清理:由于是与用户线程并发的,所以用户线程可能会分配对象,这样既可能对象直接进入年老代(例如,大对象),也可能进入年轻代后,年轻代发生minor GC,这样的话,实际上要求我们的年老代需要预留一定空间,也就是说要在年老代还有一定空间的情况下就要进行垃圾回收,留出一定内存空间来供其他线程使用,而不能等到年老代快爆满了才进行垃圾回收,通过-XX:CMSInitiatingOccupancyFraction来指定当年老代空间满了多少后进行垃圾回收
标记-清理算法:会产生内存碎片,由于是在老年代,可能会提前触发Full GC(这正是我们要尽量减少的)
参数设置:
-XX:+UseConcMarkSweepGC:使用该GC组合
-XX:CMSInitiatingOccupancyFraction:指定当年老代空间满了多少后进行垃圾回收
-XX:+UseCMSCompactAtFullCollection:(默认是开启的)在CMS收集器顶不住要进行FullGC时开启内存碎片整理过程,该过程需要STW
-XX:CMSFullGCsBeforeCompaction:指定多少次FullGC后才进行整理
-XX:ParallelCMSThreads:指定CMS回收线程的数量,默认为:(CPU数量+3)/4
适用场合:
用于处理很多的交互任务的情况
方法区的回收一般使用CMS,配置两个参数:-XX:+CMSPermGenSweepingEnabled与-XX:+CMSClassUnloadingEnabled
适用于一些需要长期运行且对相应时间有一定要求的后台程序
G1
子主题
说明:
从上图来看,G1与CMS相比,仅在最后的"筛选回收"部分不同(CMS是并发清除),实际上G1回收器的整个堆内存的划分都与其他收集器不同。
CMS需要配合ParNew,G1可单独回收整个空间
原理:
G1收集器将整个堆划分为多个大小相等的Region
G1跟踪各个region里面的垃圾堆积的价值(回收后所获得的空间大小以及回收所需时间长短的经验值),在后台维护一张优先列表,每次根据允许的收集时间,优先回收价值最大的region,这种思路:在指定的时间内,扫描部分最有价值的region(而不是扫描整个堆内存),并回收,做到尽可能的在有限的时间内获取尽可能高的收集效率。
运作流程:
初始标记:标记出所有与根节点直接关联引用对象。需要STW
并发标记:遍历之前标记到的关联节点,继续向下标记所有存活节点。在此期间所有变化引用关系的对象,都会被记录在Remember Set Logs中
最终标记:标记在并发标记期间,新产生的垃圾。需要STW
筛选回收:根据用户指定的期望回收时间回收价值较大的对象(看"原理"第二条)。需要STW
优点:
停顿时间可以预测:我们指定时间,在指定时间内只回收部分价值最大的空间,而CMS需要扫描整个年老代,无法预测停顿时间
无内存碎片:垃圾回收后会整合空间,CMS采用"标记-清理"算法,存在内存碎片
筛选回收阶段:
由于只回收部分region,所以STW时间我们可控,所以不需要与用户线程并发争抢CPU资源,而CMS并发清理需要占据一部分的CPU,会降低吞吐量。
由于STW,所以不会产生"浮动垃圾"(即CMS在并发清理阶段产生的无法回收的垃圾)
适用范围:
追求STW短:若ParNew/CMS用的挺好,就用这个;若不符合,用G1
追求吞吐量:用Parallel Scavenge/Parallel Old,而G1在吞吐量方面没有优势
判断对象是否要回收的方法
1、可达性分析法:通过一系列“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象不一定会成为可回收对象。进入DEAD状态的线程还可以恢复,GC不会回收它的内存。(把一些对象当做root对象,JVM认为root对象是不可回收的,并且root对象引用的对象也是不可回收的)
2、 以下对象会被认为是root对象:
(1) 虚拟机栈(栈帧中本地变量表)中引用的对象
(2) 方法区中静态属性引用的对象
(3) 方法区中常量引用的对象
(4) 本地方法栈中Native方法引用的对象
3、 对象被判定可被回收,需要经历两个阶段:
(1) 第一个阶段是可达性分析,分析该对象是否可达
(2) 第二个阶段是当对象没有重写finalize()方法或者finalize()方法已经被调用过,虚拟机认为该对象不可以被救活,因此回收该对象。(finalize()方法在垃圾回收中的作用是,给该对象一次救活的机会)
4、 方法区中的垃圾回收:
(1) 常量池中一些常量、符号引用没有被引用,则会被清理出常量池
(2) 无用的类:被判定为无用的类,会被清理出方法区。判定方法如下:A、 该类的所有实例被回收B、 加载该类的ClassLoader被回收C、 该类的Class对象没有被引用
5、 finalize():
(1) GC垃圾回收要回收一个对象的时候,调用该对象的finalize()方法。然后在下一次垃圾回收的时候,才去回收这个对象的内存。
(2) 可以在该方法里面,指定一些对象在释放前必须执行的操作。
发现虚拟机频繁full GC时应该怎么办:(full GC指的是清理整个堆空间,包括年轻代和永久代)
(1) 首先用命令查看触发GC的原因是什么 jstat –gccause 进程id
(2) 如果是System.gc(),则看下代码哪里调用了这个方法
(3) 如果是heap inspection(内存检查),可能是哪里执行jmap –histo[:live]命令
(4) 如果是GC locker,可能是程序依赖的JNI库的原因
(2) 如果是System.gc(),则看下代码哪里调用了这个方法
(3) 如果是heap inspection(内存检查),可能是哪里执行jmap –histo[:live]命令
(4) 如果是GC locker,可能是程序依赖的JNI库的原因
常见的垃圾回收算法
1、Mark-Sweep(标记-清除算法):Major GC(⽼年代GC)
(1)思想:标记清除算法分为两个阶段,标记阶段和清除阶段。标记阶段任务是标记出所有需要回收的对象,清除阶段就是清除被标记对象的空间。
(2)优缺点:实现简单,容易产生内存碎片,需要清除的对象过多时,效率较低
(1)思想:标记清除算法分为两个阶段,标记阶段和清除阶段。标记阶段任务是标记出所有需要回收的对象,清除阶段就是清除被标记对象的空间。
(2)优缺点:实现简单,容易产生内存碎片,需要清除的对象过多时,效率较低
2、Copying(复制清除算法):Minor GC(新⽣代GC)
(1)思想:将可用内存划分为大小相等的两块,每次只使用其中的一块。当进行垃圾回收的时候了,把其中存活对象全部复制到另外一块中,然后把已使用的内存空间一次清空掉。
(2)优缺点:不容易产生内存碎片;可用内存空间少;存活对象多的话,效率低下。
(1)思想:将可用内存划分为大小相等的两块,每次只使用其中的一块。当进行垃圾回收的时候了,把其中存活对象全部复制到另外一块中,然后把已使用的内存空间一次清空掉。
(2)优缺点:不容易产生内存碎片;可用内存空间少;存活对象多的话,效率低下。
3、Mark-Compact(标记-整理算法):Major GC(⽼年代GC)
(1)思想:先标记存活对象,然后把存活对象向一边移动,然后清理掉端边界以外的内存。
(2)优缺点:不容易产生内存碎片;内存利用率高;存活对象多并且分散的时候,移动次数多,效率低下
(1)思想:先标记存活对象,然后把存活对象向一边移动,然后清理掉端边界以外的内存。
(2)优缺点:不容易产生内存碎片;内存利用率高;存活对象多并且分散的时候,移动次数多,效率低下
4、分代收集算法:(目前大部分JVM的垃圾收集器所采用的算法):
(1) 因为新生代每次垃圾回收都要回收大部分对象,所以新生代采用Copying算法。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。
(2) 由于老年代每次只回收少量的对象,因此采用mark-compact算法。
(3) 在堆区外有一个永久代。对永久代的回收主要是无效的类和常量
(1) 因为新生代每次垃圾回收都要回收大部分对象,所以新生代采用Copying算法。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间。
(2) 由于老年代每次只回收少量的对象,因此采用mark-compact算法。
(3) 在堆区外有一个永久代。对永久代的回收主要是无效的类和常量
5、GC使用时对程序的影响?垃圾回收会影响程序的性能,Java虚拟机必须要追踪运行程序中的有用对象,然后释放没用对象,这个过程消耗处理器时间
6、几种不同的垃圾回收类型:
(1)Minor GC:从年轻代(包括Eden、Survivor区)回收内存。
A、当JVM无法为一个新的对象分配内存的时候,越容易触发Minor GC。所以分配率越高,内存越来越少,越频繁执行Minor GC
B、执行Minor GC操作的时候,不会影响到永久代(Tenured)。从永久代到年轻代的引用,被当成GC Roots,从年轻代到老年代的引用在标记阶段直接被忽略掉。
(2)Major GC:清理整个老年代,当eden区内存不足时触发。
(3)Full GC:清理整个堆空间,包括年轻代和老年代。当老年代内存不足时触发
7、Minor GC 和 Full GC 有什么不同呢?
Minor GC:只收集新生代的GC。
Full GC: 收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8及以后,永久代被移除,换为metaspace 元空间)等所有部分的模式。
**Minor GC触发条件:**当Eden区满时,触发Minor GC。
Full GC触发条件:
通过Minor GC后进入老年代的平均大小大于老年代的可用内存。如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC。
老年代空间不够分配新的内存(或永久代空间不足,但只是JDK1.7有的,这也是用元空间来取代永久代的原因,可以减少Full GC的频率,减少GC负担,提升其效率)。
由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
调用System.gc时,系统建议执行Full GC,但是不必然执行。
Minor GC:只收集新生代的GC。
Full GC: 收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8及以后,永久代被移除,换为metaspace 元空间)等所有部分的模式。
**Minor GC触发条件:**当Eden区满时,触发Minor GC。
Full GC触发条件:
通过Minor GC后进入老年代的平均大小大于老年代的可用内存。如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC。
老年代空间不够分配新的内存(或永久代空间不足,但只是JDK1.7有的,这也是用元空间来取代永久代的原因,可以减少Full GC的频率,减少GC负担,提升其效率)。
由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
调用System.gc时,系统建议执行Full GC,但是不必然执行。
介绍下空间分配担保原则
如果YougGC时新生代有大量对象存活下来,而 survivor 区放不下了,这时必须转移到老年代中,但这时发现老年代也放不下这些对象了,那怎么处理呢?其实JVM有一个老年代空间分配担保机制来保证对象能够进入老年代。
在执行每次 YoungGC 之前,JVM会先检查老年代最大可用连续空间是否大于新生代所有对象的总大小。因为在极端情况下,可能新生代 YoungGC 后,所有对象都存活下来了,而 survivor 区又放不下,那可能所有对象都要进入老年代了。
这个时候如果老年代的可用连续空间是大于新生代所有对象的总大小的,那就可以放心进行 YoungGC。
但如果老年代的内存大小是小于新生代对象总大小的,那就有可能老年代空间不够放入新生代所有存活对象,这个时候JVM就会先检查 -XX:HandlePromotionFailure 参数是否允许担保失败,如果允许,就会判断老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次YoungGC,尽快这次YoungGC是有风险的。如果小于,或者 -XX:HandlePromotionFailure 参数不允许担保失败,这时就会进行一次 Full GC。
在允许担保失败并尝试进行YoungGC后,可能会出现三种情况:
① YoungGC后,存活对象小于survivor大小,此时存活对象进入survivor区中
② YoungGC后,存活对象大于survivor大小,但是小于老年大可用空间大小,此时直接进入老年代。
③ YoungGC后,存活对象大于survivor大小,也大于老年大可用空间大小,老年代也放不下这些对象了,此时就会发生“Handle Promotion Failure”,就触发了 Full GC。如果 Full GC后,老年代还是没有足够的空间,此时就会发生OOM内存溢出了。
垃圾回收器
CMS(Concurrent Mark Sweep,并发标记清除)
收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。
从名字就可以知道,CMS是基于“标记-清除”算法实现的。CMS 回收过程分为以下四步:
a.初始标记 (CMS initial mark):主要是标记 GC Root 开始的下级(注:仅下一级)对象,这个过程会 STW,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快。
b.并发标记 (CMS concurrent mark):根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有 STW。
c.重新标记(CMS remark):顾名思义,就是要再标记一次。为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。
d.并发清除(CMS concurrent sweep):清除阶段是清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发进行的。
CMS 的问题:
1. 并发回收导致CPU资源紧张:
在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低程序总吞吐量。CMS默认启动的回收线程数是:(CPU核数 + 3)/ 4,当CPU核数不足四个时,CMS对用户程序的影响就可能变得很大。
2. 无法清理浮动垃圾:
在CMS的并发标记和并发清理阶段,用户线程还在继续运行,就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留到下一次垃圾收集时再清理掉。这一部分垃圾称为“浮动垃圾”。
3. 并发失败(Concurrent Mode Failure):
由于在垃圾回收阶段用户线程还在并发运行,那就还需要预留足够的内存空间提供给用户线程使用,因此CMS不能像其他回收器那样等到老年代几乎完全被填满了再进行回收,必须预留一部分空间供并发回收时的程序运行使用。默认情况下,当老年代使用了 92% 的空间后就会触发 CMS 垃圾回收,这个值可以通过 -XX**:** CMSInitiatingOccupancyFraction 参数来设置。
这里会有一个风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:Stop The World,临时启用 Serial Old 来重新进行老年代的垃圾回收,这样一来停顿时间就很长了。
4.内存碎片问题:
CMS是一款基于“标记-清除”算法实现的回收器,这意味着回收结束时会有内存碎片产生。内存碎片过多时,将会给大对象分配带来麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC 的情况。
为了解决这个问题,CMS收集器提供了一个 -XX**:+UseCMSCompactAtFullCollection 开关参数(默认开启),用于在 Full GC 时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,是无法并发的,这样停顿时间就会变长。还有另外一个参数 -XX:**CMSFullGCsBeforeCompaction,这个参数的作用是要求CMS在执行过若干次不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理(默认值为0,表示每次进入 Full GC 时都进行碎片整理)。
从名字就可以知道,CMS是基于“标记-清除”算法实现的。CMS 回收过程分为以下四步:
a.初始标记 (CMS initial mark):主要是标记 GC Root 开始的下级(注:仅下一级)对象,这个过程会 STW,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快。
b.并发标记 (CMS concurrent mark):根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有 STW。
c.重新标记(CMS remark):顾名思义,就是要再标记一次。为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。
d.并发清除(CMS concurrent sweep):清除阶段是清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发进行的。
CMS 的问题:
1. 并发回收导致CPU资源紧张:
在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低程序总吞吐量。CMS默认启动的回收线程数是:(CPU核数 + 3)/ 4,当CPU核数不足四个时,CMS对用户程序的影响就可能变得很大。
2. 无法清理浮动垃圾:
在CMS的并发标记和并发清理阶段,用户线程还在继续运行,就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留到下一次垃圾收集时再清理掉。这一部分垃圾称为“浮动垃圾”。
3. 并发失败(Concurrent Mode Failure):
由于在垃圾回收阶段用户线程还在并发运行,那就还需要预留足够的内存空间提供给用户线程使用,因此CMS不能像其他回收器那样等到老年代几乎完全被填满了再进行回收,必须预留一部分空间供并发回收时的程序运行使用。默认情况下,当老年代使用了 92% 的空间后就会触发 CMS 垃圾回收,这个值可以通过 -XX**:** CMSInitiatingOccupancyFraction 参数来设置。
这里会有一个风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:Stop The World,临时启用 Serial Old 来重新进行老年代的垃圾回收,这样一来停顿时间就很长了。
4.内存碎片问题:
CMS是一款基于“标记-清除”算法实现的回收器,这意味着回收结束时会有内存碎片产生。内存碎片过多时,将会给大对象分配带来麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC 的情况。
为了解决这个问题,CMS收集器提供了一个 -XX**:+UseCMSCompactAtFullCollection 开关参数(默认开启),用于在 Full GC 时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,是无法并发的,这样停顿时间就会变长。还有另外一个参数 -XX:**CMSFullGCsBeforeCompaction,这个参数的作用是要求CMS在执行过若干次不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理(默认值为0,表示每次进入 Full GC 时都进行碎片整理)。
promotion failed
该问题是在进行Minor GC时,Survivor Space放不下,对象只能放入老年代,而此时老年代也放不下造成的。(promotion failed时老年代CMS还没有机会进行回收,又放不下转移到老年代的对象,因此会出现下一个问题concurrent mode failure,需要stop-the-wold 降级为GC-Serail Old)。
106.641: [GC 106.641: [ParNew (promotion failed): 14784K->14784K(14784K), 0.0370328 secs]106.678: [CMS106.715: [CMS-concurrent-mark: 0.065/0.103 secs] [Times: user=0.17 sys=0.00, real=0.11 secs]
(concurrent mode failure): 41568K->27787K(49152K), 0.2128504 secs] 52402K->27787K(63936K), [CMS Perm : 2086K->2086K(12288K)], 0.2499776 secs] [Times: user=0.28 sys=0.00, real=0.25 secs]
concurrent mode failure
该问题是在执行CMS GC的过程中同时业务线程将对象放入老年代,而此时老年代空间不足,或者在做Minor GC的时候,新生代Survivor空间放不下,需要放入老年代,而老年代也放不下而产生的。
0.195: [GC 0.195: [ParNew: 2986K->2986K(8128K), 0.0000083 secs]0.195: [CMS0.212: [CMS-concurrent-preclean: 0.011/0.031 secs] [Times: user=0.03 sys=0.02, real=0.03 secs]
(concurrent mode failure): 56046K->138K(57344K), 0.0271519 secs] 59032K->138K(65472K), [CMS Perm : 2079K->2078K(12288K)], 0.0273119 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
promotion failed – concurrent mode failure
Minor GC后, Survivor空间容纳不了剩余对象,将要放入老年代,老年代有碎片或者不能容纳这些对象,就产生了concurrent mode failure, 然后进行stop-the-world的Serial Old收集器。
解决办法:-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5 或者调大新生代或者Survivor空间
concurrent mode failure
CMS是和业务线程并发运行的,在执行CMS的过程中有业务对象需要在老年代直接分配,例如大对象,但是老年代没有足够的空间来分配,所以导致concurrent mode failure, 然后需要进行stop-the-world的Serial Old收集器。
解决办法:+XX:CMSInitiatingOccupancyFraction(超过多少空间触发cms回收),调大老年带的空间,+XX:CMSMaxAbortablePrecleanTime
总结一句话:使用标记整理清除碎片和提早进行CMS操作。
Minor GC后, Survivor空间容纳不了剩余对象,将要放入老年代,老年代有碎片或者不能容纳这些对象,就产生了concurrent mode failure, 然后进行stop-the-world的Serial Old收集器。
解决办法:-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5 或者调大新生代或者Survivor空间
concurrent mode failure
CMS是和业务线程并发运行的,在执行CMS的过程中有业务对象需要在老年代直接分配,例如大对象,但是老年代没有足够的空间来分配,所以导致concurrent mode failure, 然后需要进行stop-the-world的Serial Old收集器。
解决办法:+XX:CMSInitiatingOccupancyFraction(超过多少空间触发cms回收),调大老年带的空间,+XX:CMSMaxAbortablePrecleanTime
总结一句话:使用标记整理清除碎片和提早进行CMS操作。
G1(Garbage First)
回收器采用面向局部收集的设计思路和基于Region的内存布局形式,是一款主要面向服务端应用的垃圾回收器。G1设计初衷就是替换 CMS,成为一种全功能收集器。G1 在JDK9 之后成为服务端模式下的默认垃圾回收器,取代了 Parallel Scavenge 加 Parallel Old 的默认组合,而 CMS 被声明为不推荐使用的垃圾回收器。G1从整体来看是基于 标记-整理 算法实现的回收器,但从局部(两个Region之间)上看又是基于 标记-复制 算法实现的。
G1 回收过程,G1 回收器的运作过程大致可分为四个步骤:
1.初始标记(会STW):仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
2.并发标记:从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理在并发时有引用变动的对象。
3.最终标记(会STW):对用户线程做短暂的暂停,处理并发阶段结束后仍有引用变动的对象。
4.清理阶段(会STW):更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,必须暂停用户线程,由多条回收器线程并行完成的。
HotSpot 虚拟机详解:
1、 Java对象创建过程:
(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) 执行方法,把对象按照程序员的意愿进行初始化。
2、 对象的定位访问的方式(通过引用如何去定位到堆上的具体对象的位置):
(1)句柄:使用句柄的方式,Java堆中将会划分出一块内存作为作为句柄池,引用中存储的就是对象的句柄的地址。而句柄中包含了对象实例数据和对象类型数据的地址。
子主题
(2)直接指针:使用直接指针的方式,引用中存储的就是对象的地址。Java堆对象的布局必须必须考虑如何去访问对象类型数据。
子主题
(3)两种方式各有优点:A、使用句柄访问的好处是引用中存放的是稳定的句柄地址,当对象被移动(比如说垃圾回收时移动对象),只会改变句柄中实例数据指针,而引用本身不会被修改。B、使用直接指针,节省了一次指针定位的时间开销。
3、HotSpot的GC算法实现:
(1)HotSpot怎么快速找到GC Root?HotSpot使用一组称为OopMap的数据结构。在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在栈和寄存器中哪些位置是引用。这样子,在GC扫描的时候,就可以直接知道哪些是可达对象了。
2)安全点:A、HotSpot只在特定的位置生成OopMap,这些位置称为安全点。B、程序执行过程中并非所有地方都可以停下来开始GC,只有在到达安全点是才可以暂停。C、安全点的选定基本上以“是否具有让程序长时间执行“的特征选定的。比如说方法调用、循环跳转、异常跳转等。具有这些功能的指令才会产生Safepoint。
(3)中断方式:
A、抢占式中断:在GC发生时,首先把所有线程中断,如果发现有线程不在安全点上,就恢复线程,让它跑到安全点上。
B、主动式中断:GC需要中断线程时,不直接对线程操作,仅仅设置一个标志,各个线程执行时主动去轮询这个标志,当发现中断标记为真就自己中断挂起。轮询标记的地方和安全点是重合的。
A、抢占式中断:在GC发生时,首先把所有线程中断,如果发现有线程不在安全点上,就恢复线程,让它跑到安全点上。
B、主动式中断:GC需要中断线程时,不直接对线程操作,仅仅设置一个标志,各个线程执行时主动去轮询这个标志,当发现中断标记为真就自己中断挂起。轮询标记的地方和安全点是重合的。
(4)安全区域:一段代码片段中,对象的引用关系不会发生变化,在这个区域中任何地方开始GC都是安全的。在线程进入安全区域时,它首先标志自己已经进入安全区域,在这段时间里,当JVM发起GC时,就不用管进入安全区域的线程了。在线程将要离开安全区域时,它检查系统是否完成了GC过程,如果完成了,它就继续前行。否则,它就必须等待直到收到可以离开安全区域的信号。
4、 GC时为什么要停顿所有Java线程?因为GC先进行可达性分析。可达性分析是判断GC Root对象到其他对象是否可达,假如分析过程中对象的引用关系在不断变化,分析结果的准确性就无法得到保证。
5、 CMS收集器:
(1)一种以获取最短回收停顿时间为目标的收集器。
(2)一般用于互联网站或者B/S系统的服务端
(3)基于标记-清除算法的实现,不过更为复杂,整个过程为4个步骤:
A、初始标记:标记GC Root能直接引用的对象
B、并发标记:利用多线程对每个GC Root对象进行tracing搜索,在堆中查找其下所有能关联到的对象。
C、重新标记:为了修正并发标记期间,用户程序继续运作而导致标志产生变动的那一部分对象的标记记录。
D、并发清除:利用多个线程对标记的对象进行清除
(4)由于耗时最长的并发标记和并发清除操作都是用户线程一起工作,所以总体来说,CMS的内存回收工作是和用户线程一起并发执行的。
(5)缺点:
A、对CPU资源占用比较多。可能因为占用一部分CPU资源导致应用程序响应变慢。
B、CMS无法处理浮动垃圾。在并发清除阶段,用户程序继续运行,可能产生新的内存垃圾,这一部分垃圾出现在标记过程之后,因此,CMS无法清除。这部分垃圾称为“浮动垃圾“
C、需要预留一部分内存,在垃圾回收时,给用户程序使用。
D、基于标记-清除算法,容易产生大量内存碎片,导致full GC(full GC进行内存碎片的整理)
6、 对象头部分的内存布局:HotSpot的对象头分为两部分,第一部分用于存储对象自身的运行时数据,比如哈希码、GC分代年龄等。另外一部分用于指向方法区对象类型数据的指针。
7、 偏向锁:偏向锁偏向于第一个获取它的线程,如果在接下来的执行过程,没有其他线程获取该锁,则持有偏向锁的线程永远不需要同步。(当一个线程获取偏向锁,它每次进入这个锁相关的同步块,虚拟机不在进行任何同步操作。当有另外一个线程尝试获取这个锁时,偏向模式宣告结束)
优化
1、一般来说,当survivor区不够大或者占用量达到50%,就会把一些对象放到老年区。通过设置合理的eden区,survivor区及使用率,可以将年轻对象保存在年轻代,从而避免full GC,使用-Xmn设置年轻代的大小
2、对于占用内存比较多的大对象,一般会选择在老年代分配内存。如果在年轻代给大对象分配内存,年轻代内存不够了,就要在eden区移动大量对象到老年代,然后这些移动的对象可能很快消亡,因此导致full GC。通过设置参数:-XX:PetenureSizeThreshold=1000000,单位为B,标明对象大小超过1M时,在老年代(tenured)分配内存空间。
3、一般情况下,年轻对象放在eden区,当第一次GC后,如果对象还存活,放到survivor区,此后,每GC一次,年龄增加1,当对象的年龄达到阈值,就被放到tenured老年区。这个阈值可以同构-XX:MaxTenuringThreshold设置。如果想让对象留在年轻代,可以设置比较大的阈值。
4、设置最小堆和最大堆:-Xmx和-Xms稳定的堆大小堆垃圾回收是有利的,获得一个稳定的堆大小的方法是设置-Xms和-Xmx的值一样,即最大堆和最小堆一样,如果这样子设置,系统在运行时堆大小理论上是恒定的,稳定的堆空间可以减少GC次数,因此,很多服务端都会将这两个参数设置为一样的数值。稳定的堆大小虽然减少GC次数,但是增加每次GC的时间,因为每次GC要把堆的大小维持在一个区间内。
5、一个不稳定的堆并非毫无用处。在系统不需要使用大内存的时候,压缩堆空间,使得GC每次应对一个较小的堆空间,加快单次GC次数。基于这种考虑,JVM提供两个参数,用于压缩和扩展堆空间。(1)-XX:MinHeapFreeRatio 参数用于设置堆空间的最小空闲比率。默认值是40,当堆空间的空闲内存比率小于40,JVM便会扩展堆空间(2)-XX:MaxHeapFreeRatio 参数用于设置堆空间的最大空闲比率。默认值是70, 当堆空间的空闲内存比率大于70,JVM便会压缩堆空间。(3)当-Xmx和-Xmx相等时,上面两个参数无效
6、通过增大吞吐量提高系统性能,可以通过设置并行垃圾回收收集器。(1)-XX:+UseParallelGC:年轻代使用并行垃圾回收收集器。这是一个关注吞吐量的收集器,可以尽可能的减少垃圾回收时间。(2)-XX:+UseParallelOldGC:设置老年代使用并行垃圾回收收集器。
7、尝试使用大的内存分页:使用大的内存分页增加CPU的内存寻址能力,从而系统的性能。-XX:+LargePageSizeInBytes 设置内存页的大小
8、使用非占用的垃圾收集器。-XX:+UseConcMarkSweepGC老年代使用CMS收集器降低停顿。
9、-XXSurvivorRatio=3,表示年轻代中的分配比率:survivor:eden = 2:3
10、JVM性能调优的工具:(1)jps(Java Process Status):输出JVM中运行的进程状态信息(现在一般使用jconsole)(2)jstack:查看java进程内线程的堆栈信息。(3)jmap:用于生成堆转存快照(4)jhat:用于分析jmap生成的堆转存快照(一般不推荐使用,而是使用Ecplise Memory Analyzer)(3)jstat是JVM统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等。(4)VisualVM:故障处理工具
Q&A
看了半天,回答下问题吧,答起了基本就搞懂了
为什么GC时要停用用户线程
常量池在那块JVM区域
包装类的常量池了解么
Integer a = 127;
Integer b = 127;
Integer c = 128;
Integer d = 128;
System.out.println(a==b);
System.out.println(c==d);
新生代和老年代用的有哪些垃圾回收算法
jdk1.8默认用的什么回收器
CMS垃圾收集器负责堆内存哪一块区域,用的什么回收算法
CMS垃圾回收的回收过程
CMS垃圾回收有哪些缺点
CMS重新标记的时候会扫描新生代么,还是只扫描老年代
CMS进行回收时,哪些阶段会停用用户线程
jmap和jstack有啥区别
JVM
运行时数据区域
1.程序计数器:程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值获取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。是线程私有的内存。
2.Java虚拟机栈:与程序计数器一样,Java虚拟机也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈桢在虚拟机栈中入栈到出栈的过程
3.本地方法栈:本地方法栈与虚拟机栈所发挥的作用非常相似,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务的
4.Java堆:对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
5.方法区:方法区用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。运行时常量池是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。在老版jdk,方法区也被称为永久代。在1.8之后,由于永久代内存经常不够用或发生内存泄漏,爆出异常java.lang.OutOfMemoryError,所以在1.8之后废弃永久代,引入元空间的概念。元空间是方法区的HotSpot jvm中的实现,元空间的本质和永久代类似,都是堆JVM规范方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。理论上取决于32位/64位系统可虚拟的内存大小,可见也不是无限制的,需要配置参数。
分代回收
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果依然存活,将会被移到Survivor区,对象在Survivor区中每熬过一次Minor GC,年龄就会加1岁,当它的年龄加到一定程度时,就会被移动到年老代中。
因为年轻代的对象基本都是朝生夕死的,所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另一块上面。复制算法不会产生内存碎片。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空,这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的,MinorGC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,就会将所有对象移动到年老代中
动态年龄计算
HotSpot在遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了suivivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值。
JVM引入动态年龄计算,主要基于如下两点考虑:
1.如果固定按照MaxTenuringThreshold设定的阈值作为晋升条件:a)MaxTenuringThreshold设置的过大,原本应该晋升的对象一直停留在Survivor区,直到Survivor区溢出,一旦溢出发生,Eden+Survivor中对象将不在依据年龄全部提升到老年代,这样对象老化的机制就失效了。b)MaxTenuringThreshold设置的过小,“过早晋升”即对象不能在新生代充分被回收,大量短期对象被晋升到老年代,老年代空间迅速增长,引起频繁的Major GC,分代回收失去了意义,严重影响GC性能。
2.相同应用在不同时间的表现不同:特殊任务的执行或者流量成分的变化,都会导致对象的生命周期分布发生波动,那么固定的阈值设定, 因为无法动态适应变化,会造成和上面相同的问题
常见的垃圾回收机制
1.引用计数法:引用计数法是一种简单但速度很慢的垃圾回收技术。每个对象都含有一个引用计数器,当有引用连接至对象时,引用计数加1,当引用离开作用域被置为null时,引用计数减1。虽然管理引用计数的开销不大,但这项开销在整个程序生命周期中将持续发生。垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象引用计数为0时,就释放其占用的空间。
2.可达性分析算法:这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
CMS的执行过程
1.初始标记(STW initial mark):这个过程从垃圾回收的“根对象”开始,只扫描到能够和“根对象”直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。
2.并发标记(Concurrent marking):这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。
3.并发预清理(Concurrent precleaning):并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代,或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段“重新标记”的工作,因为下一个阶段会Stop The World。
4.重新标记(STW remark):这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象,扫描从“根对象”开始向下追溯,并处理对象关联。
5.并发清理(Concurrent sweeping):清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。
6.并发重置(Concurrent reset):这个阶段,重置CMS收集器的数据结构状态,等待下一次垃圾回收
G1的执行过程
1.标记阶段:首先是初始标记(Initial-Mark),这个阶段也是停顿的(stop-the-word),并且会捎带触发一次young GC
2.并发标记:这个过程在整个堆中进行,并且和应用程序并发运行。并发标记过程可能被young GC中断。在并发标记阶段,如果发现区域对象中的所有对象都是垃圾,那么这个区域会立即回收,同时并发标记过程中,每个区域的对象活性(区域中存活对象的比例)被计算
3.再标记:这个阶段用来补充收集并发标记阶段产生的新的垃圾,与之不同的是,G1中采用了更快的算法SATB
4.清理阶段:选择活性低的区域(同时考虑停顿时间),等待下次young GC一起收集,这个过程也会有停顿(STW)
5.回收/完成:新的young GC清理被计算好的区域。但是有一些区域还是可能存在垃圾对象,可能是这些区域中对象活性比较高,回收不划算,也可能是为了迎合用户设置的时间,不得不舍弃一些区域的收集
G1和CMS的比较
1.CMS收集器是获取最短回收停顿时间为目标的收集器,因为CMS工作时,GC工作线程与用户线程可以并发执行,以此来达到降低停顿时间的目的(只有初始标记和重新标记会STW)。但是CMS收集器对CPU资源非常敏感,在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致应用程序变慢,总吞吐量下降
2.CMS仅作用于老年代,是基于标记清除算法,所以清理的过程中会有大量的空间碎片
3.CMS收集器无法处理浮动垃圾,由于CMS并发清理阶段用户线程还在运行,伴随程序的运行会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉
4.G1是一款面向服务端应用的垃圾收集器,适用于多核处理器、大内存容量的服务端系统。G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或CPU核心)来缩短STW的停顿时间,它满足短时间停顿的同时达到一个高的吞吐量。
5.从JDK9开始,G1称为默认的垃圾收集器,当应用有以下任何一种特性时非常适合用G1:Full GC持续时间太长或者太频繁;对象的创建速率和存活率变动很大;应用不希望停顿时间长(长于0.5甚至1s)
6.G1将空间划分成很多块(Region),然后他们各自进行回收。堆比较大的时候可以采用,采用复制算法,碎片化问题不严重。整体上看属于标记整理算法,局部(region之间)属于复制算法
7.G1需要记忆集来记录新生代和老年代之间的引用关系看,这种数据结构在G1中需要占用大量的内存,可能达到整个堆内存容量的20%甚至更多。而且G1中维护记忆集的成本较高,带来了更高的执行负载,影响效率。所以CMS在小内存应用上的表现要优于G1,而大内存应用上G1更有优势,大小内存的界限时6GB到8GB。
哪些对象可以作为GC Roots
1.虚拟机栈(栈帧中的本地变量表)中引用的对象
2.方法区中静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI(即一般说的Native方法)引用的对象
GC中Stop The World(STW)
在执行垃圾收集算法时,Java应用程序的其他所有除了垃圾收集收集器线程之外的线程都被挂起。此时,系统只能允许GC线程进行运行,其他线程则会全部暂停,等待GC线程执行完毕后才能再次运行。这些工作都是由虚拟机在后台自动发起和自动完成的,是在用户不可见的情况下把用户正常工作的线程全部停下来,这对于很多的应用程序,尤其是那些对于实时性要求很高的程序来说是难以接受的。
但不是说GC必须STW,你也可以选择降低运行速度但是可以并发执行的垃圾算法,这取决于你的业务
垃圾回收算法
1.停止-复制:先暂停程序的运行,然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的对象全部都是垃圾。当对象被复制到新堆时,它们是一个挨着一个的,所以新堆保持紧凑排列,然后就可以按前述方法简单直接地分配了,缺点一是浪费空间,两个堆之间要来回倒腾,二是当程序进入稳定态时,可能只会产生极少地垃圾,甚至不产生垃圾,尽管如此,复制式回收器仍会将所有内存自一处复制到另一处
2.标记-清除:同样是从堆栈和静态存储区出发,遍历所有地引用,进而找出所有存活的对象,每当它找到一个存活地对象,就会给对象一个标记,这个过程不会回收任何对象。只有全部标记工作完成地时候,清理动作才会开始,在清理过程中,没有标记地对象会被释放,不会发生任何复制动作。所以剩下的堆空间是不连续地,垃圾回收器如果要希望得到连续空间地话,就得重新整理剩下地对象
3.标记-整理:它的第一个阶段与标记/清除算法是一摸一样地,均是遍历GC Roots,然后将存活地对象标记。移动所有存活地对象,且按照内存地址依次排列,然后将末端内存地址以后地内存全部回收,因此,第二阶段才称为整理阶段
4.分代收集算法:把Java堆分为新生代和老年代,然后根据各个年代地特点采用最合适地收集算法,新生代中,对象地存活率比较低,所以选用复制算法,老年代中对象存活率高且没有额外空间对它进行分配担保,所以使用“标记-清除”或“标记-整理”算法进行回收
Minor GC和Full GC触发条件
Minor GC触发条件:当Eden区满时,触发Minor GC
Full GC触发条件:
1.调用System.gc()时,系统建议执行Full GC,但是不必然执行
2.老年代空间不足
3.方法区空间不足
4.通过Minor GC后进入老年代的平均大小大于老年代的可用内存
5.由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
对象什么时候进入老年代
1.大对象直接进入老年代。虚拟机提供了一个阈值参数,令大于这个设置值的对象直接在老年代中分配,如果大对象进入新生代,新生代采用的复制算法收集内存,会导致在Eden区和两个Survivor区之间发生大量的内存复制,应该避免这种情况
2.长期存活的对象进入老年代。虚拟机给每个对象定义了一个年龄计数器,对象在Eden区出生,经过一次Minor GC后仍然存活,并且能被Survivor区容纳的话,将被移动到Survivor区中,此时对象的年龄设为1,然后对象在Survivor区中每熬过一次Minor GC,年龄就增加1,当年龄超过设定的阈值时,就会被移动到老年代中。
3.动态对象年龄判定:如果在Survivor空间中所有相同年龄的对象,大小总和大于Survivor空间的一半,那么年龄大于或等于该年龄的对象就直接进入老年代,无需等到阈值中要求的年龄
4.空间分配担保:如果老年代中最大可用的连续空间大于新生代所有对象的总空间,那么Minor GC是安全的,如果老年代中最大可用的连续空间大于历代晋升到老年代的对象的平均大小,就进行一次有风险的Minor GC,如果小于平均值,就进行Full GC来让老年代腾出更多的空间。因为新生代使用的是复制算法,为了内存利用率,只是使用其中一个Survivor空间来做轮换备份,如果大量对象在Minor GC后仍然存活,导致Survivor空间不够用,就会通过分配担保机制,将多出来的对象提前转到老年代,但老年代要进行担保的前提是自己本身还有容纳这些对象的剩余空间,由于无法提前知道会有多少对象存活下来,所以取之前每次晋升到老年代的对象的平均大小作为经验值,与老年代的剩余空间做比较
TLAB
在Java中,典型的对象不在堆上分配的有两种:TLAB和栈上分配(通过逃逸分析)。JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称为TLAB(Thread-local allocation buffer)。默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销,因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。也就是说,Java中每个线程都会有自己的缓冲区称作TLAB,但是每个TLAB都只有一个线程可以操作,TLAB结合bump-the-pointer技术可以实现快速的对象分配,而不需要任何的锁进行同步,也就是说,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可
Java对象分配的过程
1.编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配。如果是在堆上分配,则进入2.
2.如果tlab_top+size<=tlab_end,则在TLAB上直接分配对象并增加tlab_top的值,如果现有的TLAB不足以存放当前对象则3.
3.重新申请一个TLAB,并再次尝试存放当前对象。如果放不下,则4.
4.在Eden区加锁(这个区是多线程共享的),如果eden_top+size<eden_end则将对象存放在Eden区,增加eden_top的值,如果Eden区不足以存放,则5
5.执行一次Young GC(minor collection)
6.经过Young GC之后,如果Eden区仍然不足以存放当前对象,则直接分配到老年代
对象内存分配的两种方法
1.指针碰撞(Serial、ParNew等带Compact过程的收集器):假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就仅仅是那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)
2.空闲列表(CMS这种基于Mark-Sweep算法的收集器):如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单的进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)
JVM类加载过程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段
1.加载:通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口
2.验证:验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
3.准备:准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配到Java堆中
4.解析:解析阶段时虚拟机将常量池中的符号(Class文件内的符号)引用替换为直接引用(指针)的过程
5.初始化:初始化时类加载过程的最后一步,开始执行类中定义的Java程序代码(字节码),init
双亲委派模型
双亲委派的意思是如果类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此,一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载
双亲委派模型的“破坏”
一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动器去加载(在JDK1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现并部署在应用程序ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能认识“这些代码”那该怎么办?
为了解决这个问题,Java设计团队只好引入了一个不太优雅的的设计:线程上下文加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个加载器默认就是应用程序类加载器。
有了线程上下文加载器,就可以做一些“舞弊”的事情了,JNDI服务使用这个线程上下文加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB、JBI等
JVM锁优化和膨胀过程
1.自旋锁:自旋锁其实就是在拿锁时发现已经有线程拿了锁,自己如果去拿会阻塞自己,这个时候会选择一次忙循环尝试。也就是不停循环看是否能等到上个线程自己释放锁。自适应自旋锁指的是例如第一次设置最多自旋10次,结果在自旋的过程中成功获得了锁,那么下一次就可以设置成最多自旋20次
2.锁粗化:虚拟机通过适当扩大加锁的范围以避免频繁的拿锁释放锁的过程
3.锁消除:通过逃逸分析发现其实根本就没有别的线程产生竞争的可能(别的线程没有临界值的引用),或者同步块内进行的是原子操作,而“自作多情”的给自己加上了锁,有可能虚拟机会直接去掉这个锁
4.偏向锁:在大多数的情况下,锁不仅不存在多线程的竞争,而是总是由同一个线程获得,因此为了让线程获得锁的代价更低而引入了偏向锁的概念,偏向锁的意思是如果一个线程获得了一个偏向锁,如果在接下来的一段时间中没有其他线程来竞争锁,那么持有锁的线程再次进入或者退出同一个同步代码块,不需要再次进行抢占锁和释放锁的操作
5.轻量级锁:当存在超过一个线程在竞争同一个同步代码块时,会发生偏向锁的撤销。当前线程会尝试使用CAS来获取锁,当自旋超过指定次数(可以自定义),此时锁会膨胀升级为重量级锁
6.重量级锁:重量级锁依赖对象内部的monitor锁来实现,而monitor又依赖操作系统的MutexLock(互斥锁)。当系统检查到是重量级锁之后,会把等待想要获取锁的线程阻塞,被阻塞的线程不会消耗CPU,但是阻塞或者唤醒一个线程,都需要通过操作系统来实现。
什么情况下需要开始类加载过程的第一个阶段加载
1.遇到new、getstatic、putstatic、或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这四条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
i++操作的字节码指令
1.将int类型常量加载到操作数栈顶
2.将int类型数值从操作数栈顶取出,并存储到局部变量表的第一个slot中
3.将int类型变量从局部变量表的第一个slot中取出,并放到操作数栈顶
4.将局部变量表的第一个slot中的int类型变量加1
5.表示将int类型数值从操作数栈顶取出,并存储到局部变量表的第一个slot中,即i中
JVM性能监控
1.JDK的命令行工具
jps(虚拟机进程状况工具):jps可以列出正在运行的虚拟机的进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称,以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID)
jstat(虚拟机统计信息监视工具):jstat是用来监视虚拟机各种运行状态信息的命令行工具,它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据
jinfo(Java配置信息工具):jinfo的作用是实时地查看和调整虚拟机各项参数
jmap(java内存映像工具):命令用于生成堆转储快照(一般称为heapdump或dump文件),如果不使用jmap命令,要想获取java堆转储快照,还有一些比较“爆力”的手段:譬如-XX:+HeapDumpOnOutOfMemoryError参数,可以让虚拟机在OOM异常出现之后自动生成dump文件,jmap的作用并不仅仅是为了获取dump文件,它还可以查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等
jhat(虚拟机堆转储快照分析工具):jhat命令与jmap搭配使用,来分析jmap生成的堆存储快照。jhat内置了一个微型的Http/Html服务器,生成dump文件的分析结果后,可以在浏览器中查看
jstack(Java堆栈跟踪工具):jstack命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道响应的线程到底在后台做些什么事情,或者等待着什么资源
2.JDK的可视化工具
Jconsole
VisualVM
JVM常见参数
1.-Xms20M:表示设置JVM启动内存的最小值为20M,必须以M为单位
2.-Xmx20M:表示设置JVM启动内存的最大值为20M,必须以M为单位,将-Xmx和-Xms设置为一样可以避免JVM内存自动扩展,大的项目-Xmx和-Xms一般都要设置到10G、20G甚至更高
3.-verbose:gc:表示输出虚拟机中GC的详细情况
4.-Xss128k:表示可以设置虚拟机栈的大小为128k
5.-Xoss128k:表示设置本地方法栈的大小为128k。不过HotSpot并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说这个参数是无效的
6.-XX:PermSize=10M:表示JVM初始分配的永久代(方法区 )的容量,必须以M为单位
7.-XX:MaxPermSize=10M:表示JVM允许分配的永久代(方法区)的对最大容量,必须以M为单位,大部分情况下这个参数默认为64M
8.-Xnoclassgc:表示关闭JVM堆类的垃圾回收
9.-XX:+TraceClassLoading:表示查看类的加载信息
10.-XX:+TraceClassUnLoading:表示查看类的卸载信息
11.-XX:NewRatio=4:表示设置年轻代(包括Eden和两个Survivor区)/老年代的大小比例为1:4,这意味着年轻代占整个堆的1/5
12.-XX:SurvivorRatio=8:表示设置2个Survivor区:1个Eden区的大小比值为2:8,这意味着Survivor区占整个年轻代的1/5,这个参数默认为8
13.-Xmn20M:表示设置年轻代的大小为20M
14.-XX:+HeapDumpOnOutOfMemoryError:表示可以让虚拟机在出现内存溢出异常时Dump出当前的堆内存转储快照
15.-XX:+UseG1GC:表示让JVM使用G1垃圾收集器
16.-XX:+PrintGCDetails:表示在控制台上打印出GC具体细节
17.-XX:+PrintGC:表示在控制台上打印出GC信息
18.-XX:PretenureSizeThreshold=3145728:表示对象大于3145728(3M)时直接进入老年代分配,这里只能以字节作为单位
19.-XX:MaxTenuringThreshold=1:表示对象年龄大于1,自动进入老年代,如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代,对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代被回收的概率
20.-XX:CompileThreshold=1000:表示一个方法被调用1000次之后,会被认为是热点代码,并触发即使编译
21.-XX:+PrintHeapAtGC:表示可以看到每次GC前后堆内存布局
22.-XX:+PrintTLAB:表示可以看到TLAB的使用情况
23.-XX:+UseSpining:开启自旋锁
24.-XX:PreBlockSpin:更改自旋锁的自选次数,使用这个参数必须先开启自旋锁
25.-XX:UseSerialGC:表示使用JVM的串行垃圾回收机制,该机制适用于单核CPU的环境下
26.-XX:+UseParallelGC:表示使用JVM的并行垃圾回收机制,该机制适用于多cpu机制,同时对响应时间无强硬要求的环境下,使用-XX:ParllelGCThreads=设置并行垃圾回收的线程数,此值可以设置与机器处理器数量相等
27.-XX:+UseParallelOldGC:表示年老代使用并行的垃圾回收机制
28.-XX:UseConcMarkSweepGC:表示使用并发模式的垃圾回收机制,该模式适用于对响应时间要求高,具有多CPU的环境下
29.-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值
30.-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低响应时间或者收集频率等,此值建议使用并行收集器,一直打开
JVM调优目标-何时需要做JVM调优
1.Heap内存(老年代)持续上涨达到设置的最大内存值
2.Full GC次数频繁
3.GC停顿时间过长(超过1s)
4.应用出现OutOfMemory等内存异常
5.应用中有使用本地缓存且占用大量内存空间
6.系统吞吐量与性能不高或下降
JVM调优实战
1.Major GC和Minor GC频繁
首先优化Minor GC频繁问题,通常情况下,由于新生代空间较小,Eden区很快被填满,就会频繁Minor GC,因此可以通过增大新生代空间来降低Minor GC的频率,例如在相同的内存分配率的前提下,新生代的Eden区增加一倍,Minor GC的此时就会减少一半
扩容Eden区虽然可以减少Minor GC的此时,但会增加单次Minor GC时间么?扩容后,Minor GC时增加了T1(扫描时间),但省去T2(复制对象)的时间,更重要的是对于虚拟机来说,复制对象的成本要远高于扫描成本,所以单次Minor GC时间更多取决于GC后存活对象的数量,而非Eden区的大小。因此如果堆中短期对象很多,那么扩容新生代,单次Minor GC时间不会显著增加
2.请求高峰期发生GC,导致服务可用性下降
由于跨代引用的存在,CMS在Remark阶段必须扫描整个堆,同时为了避免扫描时新生代有很多对象,增加了可中断的预清理阶段用来等待Minor GC的发生,只是该阶段有时间限制,如果超时等不到MinorGC,Remark时新生代仍然有很多对象,我们的调优策略是,通过参数强制Remark前进行一次Minor GC,从而降低Remark阶段的时间。另外,类似的JVM是如何避免Minor GC时扫描全堆的?通过统计信息显示,老年代持有新生代对象引用的情况不足1%,根据这一特性JVM引入了卡表(card table)来实现这一目的。卡表的具体策略时将老年代的空间分成大小为512B的若干张卡(card)。卡表本身是单字节数组,数组中的每个元素对应着一张卡,当发生老年代引用新生代时,虚拟机将该卡对应的卡表元素设置为适当的值。之后Minor GC时通过扫描卡表就可以很快的识别那些卡中存在老年代指向新生代的引用。这样虚拟机通过空间换时间的方式,避免了全堆扫描
3.STW过长的GC
对于性能要求很高的服务,建议将MaxPermSize和MinPermSize设置成一致(JDK8开始,Perm区完全消失,转而使用元空间,而元空间时直接存在内存中,不在JVM中),Xms和Xmx也设置为相同,这样可以减少内存自动扩容和收缩带来的性能损失。虚拟机启动的时候就会把参数锁设定的内存全部化为私有,即使扩容前有一部分内存不会被用户代码用到,这部分内存存在虚拟机中被标识为虚拟内存,也不会交给其他进程使用
4.外部命令导致系统缓慢
5.由windows虚拟内存导致的长时间停顿
CMS GC问题分析与解决
场景一:动态扩容引起的空间震荡
场景二:显式GC的去与留
场景三:MetaSpace区OOM
场景四:过早晋升
场景五:CMS Old GC频繁
场景六:单次CMS Old GC耗时长
场景七:内存碎片&收集器退化
场景八:对外内存OOM
场景九:JNI引发的GC问题
JMM
JAVA内存模型
1、Java的并发采用“共享内存”模型,线程之间通过读写内存的公共状态进行通讯。多个线程之间是不能通过直接传递数据交互的,它们之间交互只能通过共享变量实现。
2、主要目的是定义程序中各个变量的访问规则
3、Java内存模型规定所有变量都存储在主内存中,每个线程还有自己的工作内存。
(1) 线程的工作内存中保存了被该线程使用到的变量的拷贝(从主内存中拷贝过来),线程对变量的所有操作都必须在工作内存中执行,而不能直接访问主内存中的变量。
(2)不同线程之间无法直接访问对方工作内存的变量,线程间变量值的传递都要通过主内存来完成。
(3)主内存主要对应Java堆中实例数据部分。工作内存对应于虚拟机栈中部分区域。
4、Java线程之间的通信由内存模型JMM(Java Memory Model)控制
(1)JMM决定一个线程对变量的写入何时对另一个线程可见。
(2)线程之间共享变量存储在主内存中
(3)每个线程有一个私有的本地内存,里面存储了读/写共享变量的副本
(4)JMM通过控制每个线程的本地内存之间的交互,来为程序员提供内存可见性保证
5、可见性、有序性
(1)当一个共享变量在多个本地内存中有副本时,如果一个本地内存修改了该变量的副本,其他变量应该能够看到修改后的值,此为可见性。
(2)保证线程的有序执行,这个为有序性。(保证线程安全)
6、内存间交互操作
(1)lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
(2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
(3)read(读取):作用于主内存变量,把主内存的一个变量读取到工作内存中。
(4)load(载入):作用于工作内存,把read操作读取到工作内存的变量载入到工作内存的变量副本中
(5)use(使用):作用于工作内存的变量,把工作内存中的变量值传递给一个执行引擎。
(6)assign(赋值):作用于工作内存的变量。把执行引擎接收到的值赋值给工作内存的变量。
(7)store(存储):把工作内存的变量的值传递给主内存
(8)write(写入):把store操作的值入到主内存的变量中
内存模型示意图
子主题
类加载机制
概念:类加载器把class文件中的二进制数据读入到内存中,存放在方法区,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
这儿我写了一篇文章,可以看:https://www.jianshu.com/p/129bbc7045af
类加载器
1、启动类加载器
⊙启动类加载器是使用C/C++语言实现的,是JVM的一部分
⊙它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
⊙并不继承自java.lang.ClassLolader,,没有父加载器。
⊙加载扩展类和应用程序类加载器
⊙出于安全考虑,Bootstrap启动类 加载器只加载包名为java、javax、sun等开头的类
⊙它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
⊙并不继承自java.lang.ClassLolader,,没有父加载器。
⊙加载扩展类和应用程序类加载器
⊙出于安全考虑,Bootstrap启动类 加载器只加载包名为java、javax、sun等开头的类
2、扩展类加载器
这个类加载器是在类sun.misc.Launcher$ExtClassLoader 中以Java代码的形式实现的。它负责加载**{JRE_HOME}/lib/ext**目录中,或者被java.ext.dirs系统变量所 指定的路径中所有的类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
3、应用程序类加载器
这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径 (ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
步骤:
1、加载:查找并加载类的二进制数据(把class文件里面的信息加载到内存里面)
2、连接:把内存中类的二进制数据合并到虚拟机的运行时环境中
(1)验证:确保被加载的类的正确性。包括:
A、类文件的结构检查:检查是否满足Java类文件的固定格式
B、语义检查:确保类本身符合Java的语法规范
C、字节码验证:确保字节码流可以被Java虚拟机安全的执行。字节码流是操作码组成的序列。每一个操作码后面都会跟着一个或者多个操作数。字节码检查这个步骤会检查每一个操作码是否合法。
D、二进制兼容性验证:确保相互引用的类之间是协调一致的。
(2)准备:为类的静态变量分配内存,并将其初始化为默认值
(3)解析:把类中的符号引用转化为直接引用(比如说方法的符号引用,是有方法名和相关描述符组成,在解析阶段,JVM把符号引用替换成一个指针,这个指针就是直接引用,它指向该类的该方法在方法区中的内存位置)
3、初始化:为类的静态变量赋予正确的初始值。当静态变量的等号右边的值是一个常量表达式时,不会调用static代码块进行初始化。只有等号右边的值是一个运行时运算出来的值,才会调用static初始化。
双亲委派模型
1、当一个类加载器收到类加载请求的时候,它首先不会自己去加载这个类的信息,而是把该请求转发给父类加载器,依次向上。所以所有的类加载请求都会被传递到父类加载器中,只有当父类加载器中无法加载到所需的类,子类加载器才会自己尝试去加载该类。当当前类加载器和所有父类加载器都无法加载该类时,抛出ClassNotFindException异常。
2、意义:提高系统的安全性。用户自定义的类加载器不可能加载应该由父加载器加载的可靠类。(比如用户定义了一个恶意代码,自定义的类加载器首先让系统加载器去加载,系统加载器检查该代码不符合规范,于是就不继续加载了)
采用双亲委派模型的一个好处是保证使用不同类加载器最终得到的都是同一个对象,这样就可以保证Java 核心库的类型安全,比如,加载位于rt.jar包中的 java.lang.Object类,不管是哪个加载器加载这个类,最终都是委托给顶层的BootstrapClassLoader来加载的,这样就可以保证任何的类加载器最终得到的都是同样一个Object对象。
采用双亲委派模型的一个好处是保证使用不同类加载器最终得到的都是同一个对象,这样就可以保证Java 核心库的类型安全,比如,加载位于rt.jar包中的 java.lang.Object类,不管是哪个加载器加载这个类,最终都是委托给顶层的BootstrapClassLoader来加载的,这样就可以保证任何的类加载器最终得到的都是同样一个Object对象。
子主题
3、定义类加载器:如果某个类加载器能够加载一个类,那么这个类加载器就叫做定义类加载器
4、初始类加载器:定义类加载器及其所有子加载器都称作初始类加载器。
5、运行时包:
(1)由同一个类加载器加载并且拥有相同包名的类组成运行时包
(2)只有属于同一个运行时包的类,才能访问包可见(default)的类和类成员。作用是 限制用户自定义的类冒充核心类库的类去访问核心类库的包可见成员。
(1)由同一个类加载器加载并且拥有相同包名的类组成运行时包
(2)只有属于同一个运行时包的类,才能访问包可见(default)的类和类成员。作用是 限制用户自定义的类冒充核心类库的类去访问核心类库的包可见成员。
6、加载两份相同的class对象的情况:A和B不属于父子类加载器关系,并且各自都加载了同一个类。
Tomcat 为何打破双亲委派机制
特点:
1、全盘负责:当一个类加载器加载一个类时,该类所依赖的其他类也会被这个类加载器加载到内存中。
2、缓存机制:所有的Class对象都会被缓存,当程序需要使用某个Class时,类加载器先从缓存中查找,找不到,才从class文件中读取数据,转化成Class对象,存入缓存中。
1、全盘负责:当一个类加载器加载一个类时,该类所依赖的其他类也会被这个类加载器加载到内存中。
2、缓存机制:所有的Class对象都会被缓存,当程序需要使用某个Class时,类加载器先从缓存中查找,找不到,才从class文件中读取数据,转化成Class对象,存入缓存中。
两种类型的类加载器
1、 JVM自带的类加载器(3种):
(1)根类加载器(Bootstrap):
a、C++编写的,程序员无法在程序中获取该类
b、负责加载虚拟机的核心库,比如java.lang.Objectc、没有继承ClassLoader类
(2)扩展类加载器(Extension):
a、Java编写的,从指定目录中加载类库
b、父加载器是根类加载器
c、是ClassLoader的子类
d、如果用户把创建的jar文件放到指定目录中,也会被扩展加载器加载。
(3)系统加载器(System)或者应用加载器(App):
a、Java编写的
b、父加载器是扩展类加载器
c、从环境变量或者class.path中加载类
d、是用户自定义类加载的默认父加载器
e、是ClassLoader的子类
(1)根类加载器(Bootstrap):
a、C++编写的,程序员无法在程序中获取该类
b、负责加载虚拟机的核心库,比如java.lang.Objectc、没有继承ClassLoader类
(2)扩展类加载器(Extension):
a、Java编写的,从指定目录中加载类库
b、父加载器是根类加载器
c、是ClassLoader的子类
d、如果用户把创建的jar文件放到指定目录中,也会被扩展加载器加载。
(3)系统加载器(System)或者应用加载器(App):
a、Java编写的
b、父加载器是扩展类加载器
c、从环境变量或者class.path中加载类
d、是用户自定义类加载的默认父加载器
e、是ClassLoader的子类
2、用户自定义的类加载器:
(1)Java.lang.ClassLoader类的子类
(2)用户可以定制类的加载方式
(3)父类加载器是系统加载器
(4)编写步骤:
A、继承ClassLoader
B、重写findClass方法。从特定位置加载class文件,得到字节数组,然后利用defineClass把字节数组转化为Class对象
(5)为什么要自定义类加载器?
A、可以从指定位置加载class文件,比如说从数据库、云端加载class文件
B、加密:Java代码可以被轻易的反编译,因此,如果需要对代码进行加密,那么加密以后的代码,就不能使用Java自带的ClassLoader来加载这个类了,需要自定义ClassLoader,对这个类进行解密,然后加载。
Q & A
1、什么是双亲委派?
2、为什么需要双亲委派,不委派有什么问题?
3、"父加载器"和"子加载器"之间的关系是继承的吗?
4、双亲委派是怎么实现的?
5、我能不能主动破坏这种双亲委派机制?怎么破坏?
6、为什么重写loadClass方法可以破坏双亲委派,这个方法和findClass()、defineClass()区别是什么?
7、说一说你知道的双亲委派被破坏的例子吧
8、为什么JNDI、JDBC等需要破坏双亲委派?
9、为什么TOMCAT要破坏双亲委派?
10、谈谈你对模块化技术的理解吧!
以上,10个问题,从头开始答,你大概可以坚持到第几题?
Q&A
Java基础
HashMap和ConcurrentHashMap
由于HashMap是线程不同步的,虽然处理数据的效率高,但是在多线程的情况下存在着安全问题,因此设计了CurrentHashMap来解决多线程安全问题
HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的
HashMap的环:若当前线程此时获得entry节点,但是被线程中断无法继续执行,此时线程二进入transfer函数,并把函数顺利执行,此时新表中的某个位置有了节点,之后线程一获得执行权继续执行,因为并发transfer,所以两者都是扩容的同一个链表,当线程一执行到e.next=new table[i]的时候,由于线程二之前数据迁移的原因导致此时new table[i]上就有entry存在,所以线程一执行的时候,会将next节点,设置为自己,导致自己互相使用next引用对方,因此产生链表,导致死循环
在JDK1.7版本中,ConcurrentHashMap维护了一个Segment数组,Segment这个类继承了重入锁ReentrantLock,并且该类里面维护了一个HashEntry<K,V>[] table数组,在写操作put,remove,扩容的时候,会对Segment加锁,所以仅仅影响这个Segment,不同的segment还是可以并发的,所以解决了线程的安全问题,同时又采用分段锁也提升了并发的效率,在JDK1.8版本中,ConcurrentHashMap摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的hashMap
HashMap如果我想要让自己的Object作为k应该怎么办
1.重写HashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但是可能会导致更多的hash碰撞
2.重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这个特性,目的是为了保证key在哈希表中的唯一性(Java建议重写equals方法的时候重写hashcode的方法)
HashMap和TreeMap的区别
对象的内存分配
ThreadLocalContext
volatile
volatile在多处理器开发中保证了共享变量的可见性。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值(线程内存,私有内存)
Atomic类的CAS操作
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并交换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做,整个比较并替换的操作是一个原子操作。如Intel处理器,比较并交换通过指令的cmpxchg系列实现
CAS操作ABA问题
如果这段期间它的值曾经被修改为了B,后来又改回了A,那CAS操作就会误以为它从来没有被改变过,Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性
Synchronized和Lock的区别
1.首先synchronized是java内置关键字在jvm层面,Lock是个java类
2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁,并且可以主动尝试去获取锁
3.synchronized会自动释放锁(a线程执行完同步代码会释放锁;b线程执行过程中发生异常会释放锁),Lock需要在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁
4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了
5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可中断、可公平(两者皆可)
6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题
AQS理论的数据结构
AQS内部有3个对象,一个是state(用于计数器,类似gc的回收计数器),一个是线程标记(当前线程是谁加锁的),一个是阻塞队列
AQS是自旋锁,在等待唤醒的时候,经常会使用自旋的方式,不停地尝试获取锁,直到被其他线程获取成功
AQS有两个队列,同步队列和条件队列。同步队列依赖一个双向链表来完成同步状态的管理,当前线程获取同步状态失败后,同步器会将线程构建成一个节点,并将其加入同步队列中,通过signal或signalAll将条件队列中的节点转移到同步队列
如何指定多个线程的执行顺序
1.设定一个orderNum,每个线程执行结束之后,更新orderNum,指明下一个要执行的线程。并且唤醒所有的等待线程
2.在每一个线程的开始,要while判断orderNum是否等于自己的要求值,不是,则wait,是则执行本线程
为什么要使用线程池
1.减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
2.可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下
核心线程池ThreadPoolExecutor内部参数
1.corePoolSize:指定了线程池中的核心线程数量
2.maximumPoolSize:指定了线程池中的的最大线程数量
3.keepAliveTime:线程池维护线程所允许的空闲时间
4.unit:keepAliveTime的单位
5.workQueue:任务队列,被提交但尚未被执行的任务
6.threadFactory:线程工厂,用于创建线程,一般用默认的即可
7.handler:拒绝策略,当任务太多来不及处理,如何拒绝任务
线程池的执行流程
1.如果正在运行的线程数量小于corePoolSize,那么马上创建线程执行这个任务
2.如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放到队列
3.如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立即运行这个任务
4.如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会执行拒绝策略
线程池都有哪几种工作队列
1.ArrayBlockingQueue:底层是数组,有界队列,如果我们要使用生产者-消费者模式,这是非常好的选择
2.LinkedBlockingQueue:底层是链表,可以当作无界和有界队列来使用,所以大家不要以为它就是无界队列
3.SynchronousQueue:本身不带有空间来存储任何元素,使用上可以选择公平模式和非公平模式
4.PriorityBlockingQueue:无界队列,基于数组,数据结构为二叉堆,数组第一个也是数的根节点总是最小值
举例ArrayBlockingQueue实现并发同步的原理:原理就是读操作和写操作都需要获取到AQS独占锁才能进行操作。如果队列为空,这个时候读操作的线程进入到读线程队列排队,等待写线程写入新的元素,然后唤醒读线程队列的第一个等待线程。如果队列已满,这个时候写操作的线程进入到写线程队列排队,等待读线程将队列元素移除腾出空间,然后唤醒写线程队列的第一个等待线程
线程池的拒绝策略
1.ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常
2.ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常
3.ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
4.ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
线程池的线程数量怎么确定
1.一般来说,如果是CPU密集型应用,则线程池大小设置为N+1
2.一般来说,如果是IO密集型应用,则线程池大小设置为2N+1
3.在IO优化中,线程等待时间所占比例越高,需要越多线程,线程CPU时间所占比例越高,需要越少线程。这样的估算公式可能更适合:最佳线程数目=((线程等待时间+线程CPU时间)/线程CPU时间)*CPU数目
如何实现一个带优先级的线程池
利用priority参数,继承ThreadPoolExecutor使用PriorityBlockingQueue优先级队列
ThreadLocal的原理和实现
ThreadLocal变量,线程局部变量,同一个ThreadLocal所包含的对象,在不同的Thread中有不同的副本。ThreadLocal变量通常被private static修饰。当一个线程结束时,它所使用的所有ThreadLocal相对的实例副本都可被回收
一个线程内可以存放多个ThreadLocal对象,所以其实是ThreadLocal内部维护了一个Map,这个Map不是直接使用的HashMap,而是ThreadLocal实现的一个叫做ThreadLocalMap的静态内部类。而我们使用的get()、set()方法其实都是调用了这个ThreadLocalMap类对应的get()、set()方法。这个储值的Map并非ThreadLocal的成员变量,而是java.lang.Thread类的成员变量。ThreadLocalMap实例时作为java.lang.Thread的成员变量存储的,每个线程有唯一的一个threadLocalMap。这个map以ThreadLocal对象为key,“线程局部变量”为值,所以一个线程下可以保存多个“线程局部变量”。对ThreadLocal的操作,实际委托给当前Thread,每个Thread都会有自己的独立的ThreadLocalMap实例,存储的仓库是Entry[] table;Entry的key为ThreadLocal,value为存储内容;因此在并发环境下,对ThreadLocal的set或get,不会有任何问题。由于Tomcat线程池的原因,最初使用的“线程局部变量”保存的值,在下一次请求依然存在(同一个线程处理),这样每次请求都是在本线程中取值,所以在线程池的情况下,处理完成后主动调用该业务threadLocal的remove()方法,将“线程局部变量”清空,避免本线程下次处理的时候依然存在旧数据。
ThreadLocal为什么要使用弱引用和内存泄漏问题
jre扩展
Java提供的运行环境只是核心类,不能满足用户的多种需求,用户可以自定义类。Java运行环境提供了扩展【\jre\lib\ext】,只需将类打包成jar文件,放入扩展中,就可以直接在程序中import使用了
HashSet和HashMap
HashSet的value存的是一个static finial PRESENT = new Object()。而HashSet的remove是使用HashMap实现,则是map.remove,而map的移除会返回value,如果底层value都是存null,显然将无法分辨是否移除成功
Boolean占几个字节
未精确定义字节。Java语言表达式所操作的boolean值,在编译之后都使用Java虚拟机中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个元素boolean元素占8位
阻塞非阻塞与同步异步的区别
1.同步和异步关注的是消息通知机制,所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用成功,就得到返回值了。而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果,而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
2.阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回,非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
Java SPI
由于双亲委派模型损失了一丢丢灵活性,就比如java.sql.Drive这个东西。JDK只能提供一个规范接口,而不能提供实现。提供实现的是实际的数据库提供商。提供商的库总不能放JDK目录里吧。Java从1.6搞出了SPI就是为了优雅的解决这类问题——JDK提供接口,供应商提供服务,编程人员编码时面向接口编程,然后JDK能够自动找到合适的实现
需要一个目录
META/service
放到ClassPath下面
目录下面放置一个配置文件
文件名是要扩展的接口全名
文件内部为要实现的接口实现类
文件必须是UTF-8编码
如何使用
ServiceLoad.load(xx.class)
ServiceLoad<HelloInterface> loads = ServiceLoad.load(HelloInterface.class)
设计模式
原型模式的应用场景
创建大对象,初始化比较繁琐的对象,通过克隆
hashcode算法
spring
容器相关
BeanFactory 和 ApplicationContext有什么区别?
共同点
BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。其中ApplicationContext是BeanFactory的子接口。
依赖关系
BeanFactory:是Spring里面最底层的接口,包含了各种Bean的定义,读取bean配置文档,管理bean的加载、实例化,控制bean的生命周期,维护bean之间的依赖关系
ApplicationContext接口作为BeanFactory的派生,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能:
继承MessageSource,因此支持国际化
统一的资源文件访问方式
提供在监听器中注册bean的事件
同时加载多个配置文件
载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层
加载方式
BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。
ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。 ApplicationContext启动后预载入所有的单实例Bean,通过预载入单实例bean ,确保当你需要的时候,你就不用等待,因为它们已经创建好了
相对于基本的BeanFactory,ApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢
创建方式
BeanFactory通常以编程的方式被创建
ApplicationContext还能以声明的方式创建,如使用ContextLoader
注册方式
BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册
Spring 如何设计容器的,BeanFactory和ApplicationContext的关系详解?
Spring的作者设计了两个接口表示容器
BeanFactory 简单粗暴,可以理解为就是个HashMap,Key 是 BeanName,Value 是 Bean 实例。通常只提供注册(put),获取(get)这两个功能。我们可以称之为 “低级容器”。
ApplicationContext 可以称之为 “高级容器”。因为他比 BeanFactory 多了更多的功能。他继承了多个接口。因此具备了更多的功能。例如资源的获取,支持多种消息(例如 JSP tag 的支持),对 BeanFactory 多了工具级别的支持等待。所以你看他的名字,已经不是 BeanFactory 之类的工厂了,而是 “应用上下文”, 代表着整个大容器的所有功能。
该接口定义了一个 refresh 方法,此方法是所有阅读 Spring 源码的人的最熟悉的方法,用于刷新整个容器,即重新加载/刷新所有的 bean
高级容器ApplicationContext依赖着低级容器BeanFactory
ApplicationContext 粉红色的 “高级容器”,依赖着 “低级容器”,这里说的是依赖,不是继承哦。他依赖着 “低级容器” 的 getBean 功能。而高级容器有更多的功能:支持不同的信息源头,可以访问文件资源,支持应用事件(Observer 模式)
通常用户看到的就是 “高级容器”。 但 BeanFactory 也非常够用啦!
“低级容器”, 只负载加载 Bean,获取 Bean。容器其他的高级功能是没有的。例如上高级容器的 refresh 刷新 Bean 工厂所有配置,生命周期事件回调等。
总:IoC 在 Spring 里,只需要低级容器就可以实现,2 个步骤:
第一步:加载配置文件,解析成 BeanDefinition 放在 Map 里
第二步:调用 getBean 的时候,从 BeanDefinition 所属的 Map 里,拿出 Class 对象进行实例化,同时,如果有依赖关系,将递归调用 getBean 方法 —— 完成依赖注入
高级容器支持更多功能
高级容器 ApplicationContext,他包含了低级容器的功能,当他执行 refresh 模板方法的时候,将刷新整个容器的 Bean。同时其作为高级容器,包含了太多的功能。一句话,他不仅仅是 IoC。他支持不同信息源头,支持 BeanFactory 工具类,支持层级容器,支持访问文件资源,支持事件发布通知,支持接口回调等等。Won/article/details/104397516
ApplicationContext通常的实现是什么?
FileSystemXmlApplicationContext :此容器从一个XML文件中加载beans的定义,XML Bean 配置文件的全路径名必须提供给它的构造函数。
ClassPathXmlApplicationContext:此容器也从一个XML文件中加载beans的定义,这里,你需要正确设置classpath因为这个容器将在classpath里找bean配置。
WebXmlApplicationContext:此容器加载一个XML文件,此文件定义了一个WEB应用的所有bean。
Spring Bean对象
什么是Spring Bean?
Spring beans 是那些形成Spring应用的主干的java对象。它们被Spring IOC容器初始化,装配,和管理。这些beans通过容器中配置的元数据创建。比如,以XML文件中 的形式定义。
一个 Spring Bean定义包含什么?
一个Spring Bean 的定义包含容器必知的所有配置元数据,包括如何创建一个bean,它的生命周期详情及它的依赖。
如何给Spring 容器提供配置元数据?Spring有几种配置方式
XML配置文件
基于注解的配置
基于java的配置
Spring配置文件包含了哪些信息
Spring配置文件是个XML 文件,这个文件包含了类信息,描述了如何配置它们,以及如何相互调用。
解释Spring支持的几种bean的作用域
singleton : bean在每个Spring ioc 容器中只有一个实例。
prototype:一个bean的定义可以有多个实例。
request:每次http请求都会创建一个bean,该作用域仅在基于web的Spring ApplicationContext情形下有效。
session:在一个HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效
global-session:在一个全局的HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。
注:缺省的Spring bean 的作用域是Singleton。使用 prototype 作用域需要慎重的思考,因为频繁创建和销毁 bean 会带来很大的性能开销。
Spring框架中的单例bean是线程安全的吗?
不是,Spring框架中的单例bean不是线程安全的。
spring 中的 bean 默认是单例模式,spring 框架并没有对单例 bean 进行多线程的封装处理。
实际上大部分时候 spring bean 无状态的(比如 dao 类),所有某种程度上来说 bean 也是安全的,但如果 bean 有状态的话(比如 view model 对象),那就要开发者自己去保证线程安全了,最简单的就是改变 bean 的作用域,把“singleton”变更为“prototype”,这样请求 bean 相当于 new Bean()了,所以就可以保证线程安全了。
有状态就是有数据存储功能
无状态就是不会保存数据
Spring如何处理线程并发问题?
在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域,因为Spring对一些Bean中非线程安全状态采用ThreadLocal进行处理,解决线程安全问题。
ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。
ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
解释Spring框架中bean的生命周期
实例化
Spring对bean进行实例化
填充属性
Spring将值和bean的引用注入到bean对应的属性中
填充Aware接口属性
如果bean实现了BeanNameAware接口,Spring将bean的ID传递给setBean-Name()方法
如果bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入;
如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext()方法,将bean所在的应用上下文的引用传入进来;
调用BeanPostProcessor接口before方法
如果bean实现了BeanPostProcessor接口,Spring将调用它们的post-ProcessBeforeInitialization()方法;
初始化
调用自身初始化方法
如果bean实现了InitializingBean接口,Spring将调用它们的after-PropertiesSet()方法。类似地,如果bean使用initmethod声明了初始化方法,该方法也会被调用;
调用BeanPostProcessor接口after方法
如果bean实现了BeanPostProcessor接口,Spring将调用它们的post-ProcessAfterInitialization()方法;
使用
bean已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁;
销毁
如果bean实现了DisposableBean接口,Spring将调用它的destroy()接口方法。同样,如果bean使用destroy-method声明了销毁方法,该方法也会被调用
哪些是重要的bean生命周期方法? 你能重载它们吗?
有两个重要的bean 生命周期方法,第一个是setup , 它是在容器加载bean的时候被调用。第二个方法是 teardown 它是在容器卸载类的时候被调用。
bean 标签有两个重要的属性(init-method和destroy-method)。用它们你可以自己定制初始化和注销方法。它们也有相应的注解(@PostConstruct和@PreDestroy)。
什么是Spring的内部bean?什么是Spring inner beans?
在Spring框架中,当一个bean仅被用作另一个bean的属性时,它能被声明为一个内部bean。内部bean可以用setter注入“属性”和构造方法注入“构造参数”的方式来实现,内部bean通常是匿名的,它们的Scope一般是prototype。
自动装配
什么是bean装配?
装配,或bean 装配是指在Spring 容器中把bean组装到一起,前提是容器需要知道bean的依赖关系,如何通过依赖注入来把它们装配到一起。
什么是bean的自动装配?
在Spring框架中,在配置文件中设定bean的依赖关系是一个很好的机制,Spring 容器能够自动装配相互合作的bean,这意味着容器不需要和配置,能通过Bean工厂自动处理bean之间的协作。这意味着 Spring可以通过向Bean Factory中注入的方式自动搞定bean之间的依赖关系。自动装配可以设置在每个bean上,也可以设定在特定的bean上。
解释不同方式的自动装配,spring 自动装配 bean 有哪些方式?
在spring中,对象无需自己查找或创建与其关联的其他对象,由容器负责把需要相互协作的对象引用赋予各个对象,使用autowire来配置自动装载模式。
在Spring框架xml配置中共有5种自动装配:
no:默认的方式是不进行自动装配的,通过手工设置ref属性来进行装配bean。
byName:通过bean的名称进行自动装配,如果一个bean的 property 与另一bean 的name 相同,就进行自动装配。
byType:通过参数的数据类型进行自动装配
constructor:利用构造函数进行装配,并且构造函数的参数通过byType进行装配。
autodetect:自动探测,如果有构造方法,通过 construct的方式自动装配,否则使用 byType的方式自动装配。
使用@Autowired注解自动装配的过程是怎样的?
第一步:使用@Autowired注解来自动装配指定的bean。在使用@Autowired注解之前需要在Spring配置文件进行配置,<context:annotation-config />。
第二步:在启动spring IoC时,容器自动装载了一个AutowiredAnnotationBeanPostProcessor后置处理器,当容器扫描到@Autowied、@Resource或@Inject时,就会在IoC容器自动查找需要的bean,并装配给该对象的属性。在使用@Autowired时,首先在容器中查询对应类型的bean:
如果查询结果刚好为一个,就将该bean装配给@Autowired指定的数据;
如果查询的结果不止一个,那么@Autowired会根据名称来查找
如果上述查找的结果为空,那么会抛出异常。解决方法时,使用required=false。
自动装配有哪些局限性?
重写:你仍需用 和 配置来定义依赖,意味着总要重写自动装配。
基本数据类型:你不能自动装配简单的属性,如基本数据类型,String字符串,和类。
模糊特性:自动装配不如显式装配精确,如果有可能,建议使用显式装配。
你可以在Spring中注入一个null 和一个空字符串吗?
可以
Spring数据访问
解释对象/关系映射集成模块
Spring 通过提供ORM模块,支持我们在直接JDBC之上使用一个对象/关系映射映射(ORM)工具,Spring 支持集成主流的ORM框架,如Hiberate,JDO和 iBATIS,JPA,TopLink,JDO,OJB 。Spring的事务管理同样支持以上所有ORM框架及JDBC。
在Spring框架中如何更有效地使用JDBC?
使用Spring JDBC 框架,资源管理和错误处理的代价都会被减轻。所以开发者只需写statements 和 queries从数据存取数据,JDBC也可以在Spring框架提供的模板类的帮助下更有效地被使用,这个模板叫JdbcTemplate
解释JDBC抽象和DAO模块
通过使用JDBC抽象和DAO模块,保证数据库代码的简洁,并能避免数据库资源错误关闭导致的问题,它在各种不同的数据库的错误信息之上,提供了一个统一的异常访问层。它还利用Spring的AOP 模块给Spring应用中的对象提供事务管理服务。
spring DAO 有什么用?
Spring DAO(数据访问对象) 使得 JDBC,Hibernate 或 JDO 这样的数据访问技术更容易以一种统一的方式工作。这使得用户容易在持久性技术之间切换。它还允许您在编写代码时,无需考虑捕获每种技术不同的异常。
JdbcTemplate是什么
jdbcTemplate 类提供了很多便利的方法解决诸如把数据库数据转变成基本数据类型或对象,执行写好的或可调用的数据库操作语句,提供自定义的数据错误处理。
使用Spring通过什么方式访问Hibernate?使用 Spring 访问 Hibernate 的方法有哪些?
在Spring中有两种方式访问Hibernate:
使用 Hibernate 模板和回调进行控制反转
扩展 HibernateDAOSupport 并应用 AOP 拦截器节点
Spring事务
Spring支持的事务管理类型, spring 事务实现方式有哪些?
Spring支持两种类型的事务管理:
编程式事务管理:这意味你通过编程的方式管理事务,给你带来极大的灵活性,但是难维护。
声明式事务管理:这意味着你可以将业务代码和事务管理分离,你只需用注解和XML配置来管理事务。
两种事务管理的选择
大多数Spring框架的用户选择声明式事务管理,因为它对应用代码的影响最小,因此更符合一个无侵入的轻量级容器的思想。声明式事务管理要优于编程式事务管理,虽然比编程式事务管理(这种方式允许你通过代码控制事务)少了一点灵活性。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。
Spring事务的实现方式和实现原理
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
说一下Spring的事务传播行为
spring事务的传播行为说的是,当多个事务同时存在的时候,spring如何处理这些事务的行为。
PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。
说一下 spring 的事务隔离?
spring 有五大隔离级别,默认值为 ISOLATION_DEFAULT(使用数据库的设置),其他四个隔离级别和数据库的隔离级别一致:
ISOLATION_DEFAULT:用底层数据库的设置隔离级别,数据库设置的是什么我就用什么;
ISOLATION_READ_UNCOMMITTED:未提交读,最低隔离级别、事务未提交前,就可被其他事务读取(会出现幻读、脏读、不可重复读);
ISOLATION_READ_COMMITTED:提交读,一个事务提交后才能被其他事务读取到(会造成幻读、不可重复读),SQL server 的默认级别;
ISOLATION_REPEATABLE_READ:可重复读,保证多次读取同一个数据时,其值都和事务开始时候的内容是一致,禁止读取到别的事务未提交的数据(会造成幻读),MySQL 的默认级别;
ISOLATION_SERIALIZABLE:序列化,代价最高最可靠的隔离级别,该隔离级别能防止脏读、不可重复读、幻读。
脏读,幻读,不可重复读的概念
脏读 :表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录 A,此时该事务还未提交,然后另一个事务尝试读取到了记录 A。
不可重复读 :是指在一个事务内,多次读同一数据。
幻读 :指同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录,但是第二次同等条件下查询却有 n+1 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了。
Spring注解
什么是基于Java的Spring注解配置?
基于Java的配置,允许你在少量的Java注解的帮助下,进行你的大部分Spring配置而非通过XML文件。
怎样开启注解装配?
注解装配在默认情况下是不开启的,为了使用注解装配,我们必须在Spring配置文件中配置 <context:annotation-config/>元素。
@Component, @Controller, @Repository, @Service 有何区别?
@Required 注解有什么作用
这个注解表明bean的属性必须在配置的时候设置,通过一个bean定义的显式的属性值或通过自动装配,若@Required注解的bean属性未被设置,容器将抛出BeanInitializationException。
@Autowired 注解有什么作用
@Autowired默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。@Autowired 注解提供了更细粒度的控制,包括在何处以及如何完成自动装配。它的用法和@Required一样,修饰setter方法、构造器、属性或者具有任意名称和/或多个参数的PN方法。
@Autowired和@Resource之间的区别
@Autowired默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)
@Resource默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入
@Qualifier 注解有什么作用
当您创建多个相同类型的 bean 并希望仅使用属性装配其中一个 bean 时,您可以使用@Qualifier 注解和 @Autowired 通过指定应该装配哪个确切的 bean 来消除歧义。
@RequestMapping 注解有什么用?
@RequestMapping 注解用于将特定 HTTP 请求方法映射到将处理相应请求的控制器中的特定类/方法。此注释可应用于两个级别:
类级别:映射请求的 URL
方法级别:映射 URL 以及 HTTP 请求方法
Spring中设计模式的体现
简单工厂
实现方式
BeanFactory。Spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得Bean对象,但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。
实质
由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类。
实现原理
bean容器的启动阶段:
读取bean的xml配置文件,将bean元素分别转换成一个BeanDefinition对象。
然后通过BeanDefinitionRegistry将这些bean注册到beanFactory中,保存在它的一个ConcurrentHashMap中。
将BeanDefinition注册到了beanFactory之后,在这里Spring为我们提供了一个扩展的切口,允许我们通过实现接口BeanFactoryPostProcessor 在此处来插入我们定义的代码。
典型的例子就是:PropertyPlaceholderConfigurer,我们一般在配置数据库的dataSource时使用到的占位符的值,就是它注入进去的。
读取bean的xml配置文件,将bean元素分别转换成一个BeanDefinition对象。
然后通过BeanDefinitionRegistry将这些bean注册到beanFactory中,保存在它的一个ConcurrentHashMap中。
将BeanDefinition注册到了beanFactory之后,在这里Spring为我们提供了一个扩展的切口,允许我们通过实现接口BeanFactoryPostProcessor 在此处来插入我们定义的代码。
典型的例子就是:PropertyPlaceholderConfigurer,我们一般在配置数据库的dataSource时使用到的占位符的值,就是它注入进去的。
容器中bean的实例化阶段:
实例化阶段主要是通过反射或者CGLIB对bean进行实例化,在这个阶段Spring又给我们暴露了很多的扩展点:
各种的Aware接口,比如 BeanFactoryAware,对于实现了这些Aware接口的bean,在实例化bean时Spring会帮我们注入对应的BeanFactory的实例。
BeanPostProcessor接口,实现了BeanPostProcessor接口的bean,在实例化bean时Spring会帮我们调用接口中的方法。
InitializingBean接口,实现了InitializingBean接口的bean,在实例化bean时Spring会帮我们调用接口中的方法。
DisposableBean接口,实现了BeanPostProcessor接口的bean,在该bean死亡时Spring会帮我们调用接口中的方法。
实例化阶段主要是通过反射或者CGLIB对bean进行实例化,在这个阶段Spring又给我们暴露了很多的扩展点:
各种的Aware接口,比如 BeanFactoryAware,对于实现了这些Aware接口的bean,在实例化bean时Spring会帮我们注入对应的BeanFactory的实例。
BeanPostProcessor接口,实现了BeanPostProcessor接口的bean,在实例化bean时Spring会帮我们调用接口中的方法。
InitializingBean接口,实现了InitializingBean接口的bean,在实例化bean时Spring会帮我们调用接口中的方法。
DisposableBean接口,实现了BeanPostProcessor接口的bean,在该bean死亡时Spring会帮我们调用接口中的方法。
设计意义
松耦合。可以将原来硬编码的依赖,通过Spring这个beanFactory这个工厂来注入依赖,也就是说原来只有依赖方和被依赖方,现在我们引入了第三方——spring这个beanFactory,由它来解决bean之间的依赖问题,达到了松耦合的效果.
bean的额外处理。通过Spring接口的暴露,在实例化bean的阶段我们可以进行一些额外的处理,这些额外的处理只需要让bean实现对应的接口即可,那么spring就会在bean的生命周期调用我们实现的接口来处理该bean。
bean的额外处理。通过Spring接口的暴露,在实例化bean的阶段我们可以进行一些额外的处理,这些额外的处理只需要让bean实现对应的接口即可,那么spring就会在bean的生命周期调用我们实现的接口来处理该bean。
单列模式
Spring依赖注入Bean实例默认是单例的。
Spring的依赖注入(包括lazy-init方式)都是发生在AbstractBeanFactory的getBean里。getBean的doGetBean方法调用getSingleton进行bean的创建。
分析getSingleton()方法
Spring的依赖注入(包括lazy-init方式)都是发生在AbstractBeanFactory的getBean里。getBean的doGetBean方法调用getSingleton进行bean的创建。
分析getSingleton()方法
子主题
子主题
单例模式定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
spring对单例的实现:spring中的单例模式完成了后半句话,即提供了全局的访问点BeanFactory。但没有从构造器级别去控制单例,这是因为spring管理的是任意的java对象。
spring对单例的实现:spring中的单例模式完成了后半句话,即提供了全局的访问点BeanFactory。但没有从构造器级别去控制单例,这是因为spring管理的是任意的java对象。
适配器模式
实现方式
SpringMVC中的适配器HandlerAdatper
实质
HandlerAdatper根据Handler规则执行不同的Handler。
实现原理
DispatcherServlet根据HandlerMapping返回的handler,向HandlerAdatper发起请求,处理Handler。
HandlerAdapter根据规则找到对应的Handler并让其执行,执行完毕后Handler会向HandlerAdapter返回一个ModelAndView,最后由HandlerAdapter向DispatchServelet返回一个ModelAndView。
HandlerAdapter根据规则找到对应的Handler并让其执行,执行完毕后Handler会向HandlerAdapter返回一个ModelAndView,最后由HandlerAdapter向DispatchServelet返回一个ModelAndView。
设计意义
HandlerAdatper使得Handler的扩展变得容易,只需要增加一个新的Handler和一个对应的HandlerAdapter即可。
因此Spring定义了一个适配接口,使得每一种Controller有一种对应的适配器实现类,让适配器代替controller执行相应的方法。这样在扩展Controller时,只需要增加一个适配器类就完成了SpringMVC的扩展了。
装饰器模式
实现方式
Spring中用到的包装器模式在类名上有两种表现:一种是类名中含有Wrapper,另一种是类名中含有Decorator。
实质
动态地给一个对象添加一些额外的职责。
就增加功能来说,Decorator模式相比生成子类更为灵活。
代理模式
实现方式
AOP底层,就是动态代理模式的实现。
动态代理
在内存中构建的,不需要手动编写代理类
静态代理
需要手工编写代理类,代理类引用被代理对象
实现原理
切面在应用运行的时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象创建动态的创建一个代理对象。SpringAOP就是以这种方式织入切面的。
织入:把切面应用到目标对象并创建新的代理对象的过程。
观察者模式
实现方式
spring的事件驱动模型使用的是 观察者模式 ,Spring中Observer模式常用的地方是listener的实现。
具体实现
事件机制的实现需要三个部分,事件源,事件,事件监听器
ApplicationEvent抽象类[事件]
继承自jdk的EventObject,所有的事件都需要继承ApplicationEvent,并且通过构造器参数source得到事件源.
该类的实现类ApplicationContextEvent表示ApplicaitonContext的容器事件.
ApplicationListener接口[事件监听器]
继承自jdk的EventListener,所有的监听器都要实现这个接口。
这个接口只有一个onApplicationEvent()方法,该方法接受一个ApplicationEvent或其子类对象作为参数,在方法体中,可以通过不同对Event类的判断来进行相应的处理。
当事件触发时所有的监听器都会收到消息。
ApplicationContext接口[事件源]
ApplicationContext是spring中的全局容器,翻译过来是”应用上下文”。
实现了ApplicationEventPublisher接口。
职责
负责读取bean的配置文档,管理bean的加载,维护bean之间的依赖关系,可以说是负责bean的整个生命周期,再通俗一点就是我们平时所说的IOC容器。
策略模式
实现方式
Spring框架的资源访问Resource接口。该接口提供了更强的资源访问能力,Spring 框架本身大量使用了 Resource 接口来访问底层资源。
Resource 接口介绍
source 接口是具体资源访问策略的抽象,也是所有资源访问类所实现的接口。
Resource 接口主要提供了如下几个方法:
● getInputStream():定位并打开资源,返回资源对应的输入流。每次调用都返回新的输入流。调用者必须负责关闭输入流。
● exists():返回 Resource 所指向的资源是否存在。
● isOpen():返回资源文件是否打开,如果资源文件不能多次读取,每次读取结束应该显式关闭,以防止资源泄漏。
● getDescription():返回资源的描述信息,通常用于资源处理出错时输出该信息,通常是全限定文件名或实际 URL。
● getFile:返回资源对应的 File 对象。
● getURL:返回资源对应的 URL 对象。
最后两个方法通常无须使用,仅在通过简单方式访问无法实现时,Resource 提供传统的资源访问的功能。
Resource 接口本身没有提供访问任何底层资源的实现逻辑,针对不同的底层资源,Spring 将会提供不同的 Resource 实现类,不同的实现类负责不同的资源访问逻辑。
● Spring 为 Resource 接口提供了如下实现类:
● UrlResource:访问网络资源的实现类。
● ClassPathResource:访问类加载路径里资源的实现类。
● FileSystemResource:访问文件系统里资源的实现类。
● ServletContextResource:访问相对于 ServletContext 路径里的资源的实现类.
● InputStreamResource:访问输入流资源的实现类。
● ByteArrayResource:访问字节数组资源的实现类。
这些 Resource 实现类,针对不同的的底层资源,提供了相应的资源访问逻辑,并提供便捷的包装,以利于客户端程序的资源访问。
Resource 接口主要提供了如下几个方法:
● getInputStream():定位并打开资源,返回资源对应的输入流。每次调用都返回新的输入流。调用者必须负责关闭输入流。
● exists():返回 Resource 所指向的资源是否存在。
● isOpen():返回资源文件是否打开,如果资源文件不能多次读取,每次读取结束应该显式关闭,以防止资源泄漏。
● getDescription():返回资源的描述信息,通常用于资源处理出错时输出该信息,通常是全限定文件名或实际 URL。
● getFile:返回资源对应的 File 对象。
● getURL:返回资源对应的 URL 对象。
最后两个方法通常无须使用,仅在通过简单方式访问无法实现时,Resource 提供传统的资源访问的功能。
Resource 接口本身没有提供访问任何底层资源的实现逻辑,针对不同的底层资源,Spring 将会提供不同的 Resource 实现类,不同的实现类负责不同的资源访问逻辑。
● Spring 为 Resource 接口提供了如下实现类:
● UrlResource:访问网络资源的实现类。
● ClassPathResource:访问类加载路径里资源的实现类。
● FileSystemResource:访问文件系统里资源的实现类。
● ServletContextResource:访问相对于 ServletContext 路径里的资源的实现类.
● InputStreamResource:访问输入流资源的实现类。
● ByteArrayResource:访问字节数组资源的实现类。
这些 Resource 实现类,针对不同的的底层资源,提供了相应的资源访问逻辑,并提供便捷的包装,以利于客户端程序的资源访问。
springCore
IOC
BeanDefinition
容器中的每一个 bean 都会有一个对应的 BeanDefinition 实例,该实例负责保存bean对象的所有必要信息,包括 bean 对象的 class 类型、是否是抽象类、构造方法和参数、其它属性等等。当客户端向容器请求相应对象时,容器就会通过这些信息为客户端返回一个完整可用的 bean 实例。
不管是是通过xml配置文件的<Bean>标签,还是通过注解配置的@Bean,还是@Compontent标注的类,还是扫描得到的类,它最终都会被解析成一个BeanDefinition对象,最后我们的Bean工厂就会根据这份Bean的定义信息,对bean进行实例化、初始化等等操作。
不管是是通过xml配置文件的<Bean>标签,还是通过注解配置的@Bean,还是@Compontent标注的类,还是扫描得到的类,它最终都会被解析成一个BeanDefinition对象,最后我们的Bean工厂就会根据这份Bean的定义信息,对bean进行实例化、初始化等等操作。
BeanDefinitionRegistry
抽象出 bean 的注册逻辑
BeanFactory
抽象出了 bean 的管理逻辑,BeanFactory 的实现类就具体承担了 bean 的注册以及管理工作
三者关系
工作流程
①、容器启动阶段
容器启动时,会通过某种途径加载 ConfigurationMetaData。除了代码方式比较直接外,在大部分情况下,容器需要依赖某些工具类,比如: BeanDefinitionReader,BeanDefinitionReader 会对加载的 ConfigurationMetaData进行解析和分析,并将分析后的信息组装为相应的 BeanDefinition,最后把这些保存了 bean 定义的 BeanDefinition,注册到相应的 BeanDefinitionRegistry,这样容器的启动工作就完成了。这个阶段主要完成一些准备性工作,更侧重于 bean 对象管理信息的收集,当然一些验证性或者辅助性的工作也在这一阶段完成。
来看一个简单的例子吧,过往,所有的 bean 都定义在 XML 配置文件中,下面的代码将模拟 BeanFactory 如何从配置文件中加载 bean 的定义以及依赖关系:
// 通常为BeanDefinitionRegistry的实现类,这里以DeFaultListabeBeanFactory为例
BeanDefinitionRegistry beanRegistry = new DefaultListableBeanFactory();
// XmlBeanDefinitionReader实现了BeanDefinitionReader接口,用于解析XML文件
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReaderImpl(beanRegistry);
// 加载配置文件
beanDefinitionReader.loadBeanDefinitions("classpath:spring-bean.xml");
// 从容器中获取bean实例
BeanFactory container = (BeanFactory)beanRegistry;
Business business = (Business)container.getBean("beanName");
②、Bean的实例化阶段
经过第一阶段,所有 bean 定义都通过 BeanDefinition 的方式注册到 BeanDefinitionRegistry 中,当某个请求通过容器的 getBean 方法请求某个对象,或者因为依赖关系容器需要隐式的调用 getBean 时,就会触发第二阶段的活动:容器会首先检查所请求的对象之前是否已经实例化完成。如果没有,则会根据注册的 BeanDefinition 所提供的信息实例化被请求对象,并为其注入依赖。当该对象装配完毕后,容器会立即将其返回给请求方法使用。
BeanFactory 只是 Spring IoC 容器的一种实现,如果没有特殊指定,它采用采用延迟初始化策略:只有当访问容器中的某个对象时,才对该对象进行初始化和依赖注入操作。而在实际场景下,我们更多的使用另外一种类型的容器: ApplicationContext,它构建在 BeanFactory 之上,属于更高级的容器,除了具有 BeanFactory 的所有能力之外,还提供对事件监听机制以及国际化的支持等。它管理的 bean,在容器启动时全部完成初始化和依赖注入操作。
BeanFactory 和 ApplicationContext关系
BeanFactory:是Spring里面最低层的接口,提供了最简单的容器的功能,只提供了实例化对象和拿对象的功能;
ApplicationContext:(这点可以看下代码或者看UML图,applicationcontext继承了多个类,这多个类具有各自功能,所以也就为ac提供了下面几个点)
应用上下文,继承BeanFactory接口,它是Spring的一各更高级的容器,提供了更多的有用的功能;
1) 国际化(MessageSource)
2) 访问资源,如URL和文件(ResourceLoader)
3) 载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层
4) 消息发送、响应机制(ApplicationEventPublisher)
5) AOP(拦截器)
两者装载bean的区别
BeanFactory在启动的时候不会去实例化Bean,中有从容器中拿Bean的时候才会去实例化;
ApplicationContext在启动的时候就把所有的Bean全部实例化了。它还可以为Bean配置lazy-init=true来让Bean延迟实例化;
ApplicationContext:(这点可以看下代码或者看UML图,applicationcontext继承了多个类,这多个类具有各自功能,所以也就为ac提供了下面几个点)
应用上下文,继承BeanFactory接口,它是Spring的一各更高级的容器,提供了更多的有用的功能;
1) 国际化(MessageSource)
2) 访问资源,如URL和文件(ResourceLoader)
3) 载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层
4) 消息发送、响应机制(ApplicationEventPublisher)
5) AOP(拦截器)
两者装载bean的区别
BeanFactory在启动的时候不会去实例化Bean,中有从容器中拿Bean的时候才会去实例化;
ApplicationContext在启动的时候就把所有的Bean全部实例化了。它还可以为Bean配置lazy-init=true来让Bean延迟实例化;
子主题
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
//标识当前context实例的id,最终会通过native方法来生成:System.identityHashCode
String getId();
//返回该context所属的应用名称,默认为空字符串,在web应用中返回的是servlet的contextpath
String getApplicationName();
//返回当前context的名称
String getDisplayName();
//返回context第一次被加载的时间
long getStartupDate();
//返回该context的parent
ApplicationContext getParent();
//返回具有自动装配能力的beanFactory,默认返回的就是初始化时实例化的beanFactory
AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;
}
MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
//标识当前context实例的id,最终会通过native方法来生成:System.identityHashCode
String getId();
//返回该context所属的应用名称,默认为空字符串,在web应用中返回的是servlet的contextpath
String getApplicationName();
//返回当前context的名称
String getDisplayName();
//返回context第一次被加载的时间
long getStartupDate();
//返回该context的parent
ApplicationContext getParent();
//返回具有自动装配能力的beanFactory,默认返回的就是初始化时实例化的beanFactory
AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;
}
Bean的生命周期和钩子函数、spring扩展机制
生命周期图
子主题
public interface BeanPostProcessor {
// 前置处理
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
// 后置处理
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
// 前置处理
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
// 后置处理
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
postProcessBeforeInitialization()方法与 postProcessAfterInitialization()分别对应图中前置处理和后置处理两个步骤将执行的方法。这两个方法中都传入了bean对象实例的引用,为扩展容器的对象实例化过程提供了很大便利,在这儿几乎可以对传入的实例执行任何操作。
注解、AOP等功能的实现均大量使用了 BeanPostProcessor,比如有一个自定义注解,你完全可以实现BeanPostProcessor的接口,在其中判断bean对象的脑袋上是否有该注解,如果有,你可以对这个bean实例执行任何操作,想想是不是非常的简单?
再来看一个更常见的例子,在Spring中经常能够看到各种各样的Aware接口,其作用就是在对象实例化完成以后将Aware接口定义中规定的依赖注入到当前实例中。比如最常见的 ApplicationContextAware接口,实现了这个接口的类都可以获取到一个ApplicationContext对象。
当容器中每个对象的实例化过程走到BeanPostProcessor前置处理这一步时,容器会检测到之前注册到容器的ApplicationContextAwareProcessor,然后就会调用其postProcessBeforeInitialization()方法,检查并设置Aware相关依赖。看看代码吧,是不是很简单:
// 代码来自:org.springframework.context.support.ApplicationContextAwareProcessor
// 其postProcessBeforeInitialization方法调用了invokeAwareInterfaces方法
private void invokeAwareInterfaces(Object bean) {
if (bean instanceof EnvironmentAware) {
((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());
}
if (bean instanceof ApplicationContextAware) {
((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
}
// ......
}
Bean的作用域
Spring对每个bean提供了一个scope属性来表示该bean的作用域。它是bean的生命周期。例如,一个scope为singleton的bean,在第一次被注入时,会创建为一个单例对象,该对象会一直被复用到应用结束。
● singleton:默认的scope,每个scope为singleton的bean都会被定义为一个单例对象,该对象的生命周期是与Spring IOC容器一致的(但在第一次被注入时才会创建)。
● prototype:bean被定义为在每次注入时都会创建一个新的对象。
● request:bean被定义为在每个HTTP请求中创建一个单例对象,也就是说在单个请求中都会复用这一个单例对象。
● session:bean被定义为在一个session的生命周期内创建一个单例对象。
● application:bean被定义为在ServletContext的生命周期中复用一个单例对象。
● websocket:bean被定义为在websocket的生命周期中复用一个单例对象。
● prototype:bean被定义为在每次注入时都会创建一个新的对象。
● request:bean被定义为在每个HTTP请求中创建一个单例对象,也就是说在单个请求中都会复用这一个单例对象。
● session:bean被定义为在一个session的生命周期内创建一个单例对象。
● application:bean被定义为在ServletContext的生命周期中复用一个单例对象。
● websocket:bean被定义为在websocket的生命周期中复用一个单例对象。
AOP
什么是AOP
OOP(Object-Oriented Programming)面向对象编程,允许开发者定义纵向的关系,但并适用于定义横向的关系,导致了大量代码的重复,而不利于各个模块的重用。
AOP(Aspect-Oriented Programming),一般称为面向切面编程,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。可用于权限认证、日志、事务处理等。
Spring AOP and AspectJ AOP 有什么区别?AOP 有哪些实现方式?
AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表
AspectJ是静态代理的增强,所谓静态代理,就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强,他会在编译阶段将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。
Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
JDK动态代理和CGLIB动态代理的区别
原理区别
JDK动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。核心是实现InvocationHandler接口,使用invoke()方法进行面向切面的处理,调用相应的通知;
cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。核心是实现MethodInterceptor接口,使用intercept()方法进行面向切面的处理,调用相应的通知.
代理对象区别
JDK动态代理只能对实现了接口的类生成代理,而不能针对类实现代理;
CGLib是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法(继承)
Spring在选择用JDK还是CGLib的依据是
如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
如果目标对象实现了接口,可以强制使用CGLIB实现AOP
如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换
如何理解 Spring 中的代理?
将 Advice 应用于目标对象后创建的对象称为代理。在客户端对象的情况下,目标对象和代理对象是相同的。
Advice + Target Object = Proxy
解释一下Spring AOP里面的几个名词
切面(Aspect):切面是通知和切点的结合。通知和切点共同定义了切面的全部内容。 在Spring AOP中,切面可以使用通用类(基于模式的风格) 或者在普通类中以 @AspectJ 注解来实现。
连接点(Join point):指方法,在Spring AOP中,一个连接点 总是 代表一个方法的执行。 应用可能有数以千计的时机应用通知。这些时机被称为连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
通知(Advice):在AOP术语中,切面的工作被称为通知。
切入点(Pointcut):切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
引入(Introduction):引入允许我们向现有类添加新方法或属性。
目标对象(Target Object): 被一个或者多个切面(aspect)所通知(advise)的对象。它通常是一个代理对象。也有人把它叫做 被通知(adviced) 对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个 被代理(proxied) 对象。
织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。在目标对象的生命周期里有多少个点可以进行织入:
编译期:切面在目标类编译时被织入。AspectJ的织入编译器是以这种方式织入切面的
类加载期:切面在目标类加载到JVM时被织入。需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入就支持以这种方式织入切面
运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。SpringAOP就是以这种方式织入切面。
Spring通知有哪些类型?
在AOP术语中,切面的工作被称为通知,实际上是程序执行时要通过SpringAOP框架触发的代码段。
Spring切面可以应用5种类型的通知:
前置通知(Before):在目标方法被调用之前调用通知功能;
后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
返回通知(After-returning ):在目标方法成功执行之后调用通知;
异常通知(After-throwing):在目标方法抛出异常后调用通知;
环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
循环依赖
什么是循环依赖?AB相互依赖,A中注入了自己
spring循环依赖的前置条件
1、出现循环依赖的Bean必须要是单例
2、依赖注入的方式不能全是构造器注入的方式(很多博客上说,只能解决setter方法的循环依赖,这是错误的)
2、依赖注入的方式不能全是构造器注入的方式(很多博客上说,只能解决setter方法的循环依赖,这是错误的)
子主题
获取 Bean 流程
1.流程从 getBean 方法开始,getBean 是个空壳方法,所有逻辑直接到 doGetBean 方法中
2.transformedBeanName 将 name 转换为真正的 beanName(name 可能是 FactoryBean 以 & 字符开头或者有别名的情况,所以需要转化下)
3.然后通过 getSingleton(beanName) 方法尝试从缓存中查找是不是有该实例 sharedInstance(单例在 Spring 的同一容器只会被创建一次,后续再获取 bean,就直接从缓存(这儿就开始引申出三级缓存)获取即可)
4.如果有的话,sharedInstance 可能是完全实例化好的 bean,也可能是一个原始的 bean,所以再经 getObjectForBeanInstance 处理即可返回
5.当然 sharedInstance 也可能是 null,这时候就会执行创建 bean 的逻辑,将结果返回
2.transformedBeanName 将 name 转换为真正的 beanName(name 可能是 FactoryBean 以 & 字符开头或者有别名的情况,所以需要转化下)
3.然后通过 getSingleton(beanName) 方法尝试从缓存中查找是不是有该实例 sharedInstance(单例在 Spring 的同一容器只会被创建一次,后续再获取 bean,就直接从缓存(这儿就开始引申出三级缓存)获取即可)
4.如果有的话,sharedInstance 可能是完全实例化好的 bean,也可能是一个原始的 bean,所以再经 getObjectForBeanInstance 处理即可返回
5.当然 sharedInstance 也可能是 null,这时候就会执行创建 bean 的逻辑,将结果返回
三级缓存
// org.springframework.beans.factory.support.DefaultSingletonBeanRegistry
/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
singletonObjects:完成初始化的单例对象的 cache,这里的 bean 经历过 实例化->属性填充->初始化 以及各种后置处理(一级缓存)
earlySingletonObjects:存放原始的 bean 对象(完成实例化但是尚未填充属性和初始化),仅仅能作为指针提前曝光,被其他 bean 所引用,用于解决循环依赖的 (二级缓存)
singletonFactories:在 bean 实例化完之后,属性填充以及初始化之前,如果允许提前曝光,Spring 会将实例化后的 bean 提前曝光,也就是把该 bean 转换成 beanFactory 并加入到 singletonFactories(三级缓存)
earlySingletonObjects:存放原始的 bean 对象(完成实例化但是尚未填充属性和初始化),仅仅能作为指针提前曝光,被其他 bean 所引用,用于解决循环依赖的 (二级缓存)
singletonFactories:在 bean 实例化完之后,属性填充以及初始化之前,如果允许提前曝光,Spring 会将实例化后的 bean 提前曝光,也就是把该 bean 转换成 beanFactory 并加入到 singletonFactories(三级缓存)
三级缓存查找,最开始初始化A肯定缓存是没有的
三级缓存没有则去创建
子主题
解决循环依赖
逻辑图
子主题
Spring 创建 bean 主要分为两个步骤,创建原始 bean 对象,接着去填充对象属性和初始化
每次创建 bean 之前,我们都会从缓存中查下有没有该 bean,因为是单例,只能有一个
当我们创建 beanA 的原始对象后,并把它放到三级缓存中,接下来就该填充对象属性了,这时候发现依赖了 beanB,接着就又去创建 beanB,同样的流程,创建完 beanB 填充属性时又发现它依赖了 beanA,又是同样的流程,不同的是,这时候可以在三级缓存中查到刚放进去的原始对象 beanA,所以不需要继续创建,用它注入 beanB,完成 beanB 的创建
既然 beanB 创建好了,所以 beanA 就可以完成填充属性的步骤了,接着执行剩下的逻辑,闭环完成
每次创建 bean 之前,我们都会从缓存中查下有没有该 bean,因为是单例,只能有一个
当我们创建 beanA 的原始对象后,并把它放到三级缓存中,接下来就该填充对象属性了,这时候发现依赖了 beanB,接着就又去创建 beanB,同样的流程,创建完 beanB 填充属性时又发现它依赖了 beanA,又是同样的流程,不同的是,这时候可以在三级缓存中查到刚放进去的原始对象 beanA,所以不需要继续创建,用它注入 beanB,完成 beanB 的创建
既然 beanB 创建好了,所以 beanA 就可以完成填充属性的步骤了,接着执行剩下的逻辑,闭环完成
doCreateBean
子主题
Q&A
B 中提前注入了一个没有经过初始化的 A 类型对象不会有问题吗?
虽然在创建 B 时会提前给 B 注入了一个还未初始化的 A 对象,但是在创建 A 的流程中一直使用的是注入到 B 中的 A 对象的引用,之后会根据这个引用对 A 进行初始化,所以这是没有问题的。
虽然在创建 B 时会提前给 B 注入了一个还未初始化的 A 对象,但是在创建 A 的流程中一直使用的是注入到 B 中的 A 对象的引用,之后会根据这个引用对 A 进行初始化,所以这是没有问题的。
Spring 是如何解决的循环依赖?
Spring 为了解决单例的循环依赖问题,使用了三级缓存。其中一级缓存为单例池(singletonObjects),二级缓存为提前曝光对象(earlySingletonObjects),三级缓存为提前曝光对象工厂(singletonFactories)。
假设A、B循环引用,实例化 A 的时候就将其放入三级缓存中,接着填充属性的时候,发现依赖了 B,同样的流程也是实例化后放入三级缓存,接着去填充属性时又发现自己依赖 A,这时候从缓存中查找到早期暴露的 A,没有 AOP 代理的话,直接将 A 的原始对象注入 B,完成 B 的初始化后,进行属性填充和初始化,这时候 B 完成后,就去完成剩下的 A 的步骤,如果有 AOP 代理,就进行 AOP 处理获取代理后的对象 A,注入 B,走剩下的流程。
Spring 为了解决单例的循环依赖问题,使用了三级缓存。其中一级缓存为单例池(singletonObjects),二级缓存为提前曝光对象(earlySingletonObjects),三级缓存为提前曝光对象工厂(singletonFactories)。
假设A、B循环引用,实例化 A 的时候就将其放入三级缓存中,接着填充属性的时候,发现依赖了 B,同样的流程也是实例化后放入三级缓存,接着去填充属性时又发现自己依赖 A,这时候从缓存中查找到早期暴露的 A,没有 AOP 代理的话,直接将 A 的原始对象注入 B,完成 B 的初始化后,进行属性填充和初始化,这时候 B 完成后,就去完成剩下的 A 的步骤,如果有 AOP 代理,就进行 AOP 处理获取代理后的对象 A,注入 B,走剩下的流程。
为什么要使用三级缓存呢?二级缓存能解决循环依赖吗?
如果没有 AOP 代理,二级缓存可以解决问题,但是有 AOP 代理的情况下,只用二级缓存就意味着所有 Bean 在实例化后就要完成 AOP 代理,这样违背了 Spring 设计的原则,Spring 在设计之初就是通过 AnnotationAwareAspectJAutoProxyCreator 这个后置处理器来在 Bean 生命周期的最后一步来完成 AOP 代理,而不是在实例化后就立马进行 AOP 代理。
如果没有 AOP 代理,二级缓存可以解决问题,但是有 AOP 代理的情况下,只用二级缓存就意味着所有 Bean 在实例化后就要完成 AOP 代理,这样违背了 Spring 设计的原则,Spring 在设计之初就是通过 AnnotationAwareAspectJAutoProxyCreator 这个后置处理器来在 Bean 生命周期的最后一步来完成 AOP 代理,而不是在实例化后就立马进行 AOP 代理。
什么时候 bean 被放入 3 级缓存
doCreateBean里面,先执行createBeanInstance,实例化Bean;
然后判断是否提前暴露,代码:(mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
然后addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));这句加入;
之后才是填充属性populateBean(beanName, mbd, instanceWrapper);
//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory
doCreateBean里面,先执行createBeanInstance,实例化Bean;
然后判断是否提前暴露,代码:(mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
然后addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));这句加入;
之后才是填充属性populateBean(beanName, mbd, instanceWrapper);
//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory
spring MVC
和spring的关系
子主题
Q & A
为什么需要父子容器?
父子容器的主要作用应该是划分框架边界。有点单一职责的味道。在J2EE三层架构中,在service层我们一般使用spring框架来管理, 而在web层则有多种选择,如spring mvc、struts等。因此,通常对于web层我们会使用单独的配置文件。例如在上面的案例中,一开始我们使用spring-servlet.xml来配置web层,使用applicationContext.xml来配置service、dao层。如果现在我们想把web层从spring mvc替换成struts,那么只需要将spring-servlet.xml替换成Struts的配置文件struts.xml即可,而applicationContext.xml不需要改变。
是否可以把所有类都通过Spring父容器来管理?
Spring的applicationContext.xml中配置全局扫描)
<context:component-scan use-default-filters="false" base-package="cn.javajr">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Service" />
<context:include-filter type="annotation" expression="org.springframework.stereotype.Component" />
<context:include-filter type="annotation" expression="org.springframework.stereotype.Repository" />
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>
很显然这种方式是行不通的,这样会导致我们请求接口的时候产生404。因为在解析@ReqestMapping注解的过程中initHandlerMethods()函数只是对Spring MVC 容器中的bean进行处理的,并没有去查找父容器的bean, 因此不会对父容器中含有@RequestMapping注解的函数进行处理,更不会生成相应的handler。所以当请求过来时找不到处理的handler,导致404。
是否可以把我们所需的类都放入Spring-mvc子容器里面来管理(springmvc的spring-servlet.xml中配置全局扫描)?
这个是把包的扫描配置spring-servlet.xml中这个是可行的。为什么可行因为无非就是把所有的东西全部交给子容器来管理了,子容器执行了refresh方法,把在它的配置文件里面的东西全部加载管理起来来了。虽然可以这么做不过一般应该是不推荐这么去做的,一般人也不会这么干的。如果你的项目里有用到事物、或者aop记得也需要把这部分配置需要放到Spring-mvc子容器的配置文件来,不然一部分内容在子容器和一部分内容在父容器,可能就会导致你的事物或者AOP不生效。
父子容器的主要作用应该是划分框架边界。有点单一职责的味道。在J2EE三层架构中,在service层我们一般使用spring框架来管理, 而在web层则有多种选择,如spring mvc、struts等。因此,通常对于web层我们会使用单独的配置文件。例如在上面的案例中,一开始我们使用spring-servlet.xml来配置web层,使用applicationContext.xml来配置service、dao层。如果现在我们想把web层从spring mvc替换成struts,那么只需要将spring-servlet.xml替换成Struts的配置文件struts.xml即可,而applicationContext.xml不需要改变。
是否可以把所有类都通过Spring父容器来管理?
Spring的applicationContext.xml中配置全局扫描)
<context:component-scan use-default-filters="false" base-package="cn.javajr">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Service" />
<context:include-filter type="annotation" expression="org.springframework.stereotype.Component" />
<context:include-filter type="annotation" expression="org.springframework.stereotype.Repository" />
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>
很显然这种方式是行不通的,这样会导致我们请求接口的时候产生404。因为在解析@ReqestMapping注解的过程中initHandlerMethods()函数只是对Spring MVC 容器中的bean进行处理的,并没有去查找父容器的bean, 因此不会对父容器中含有@RequestMapping注解的函数进行处理,更不会生成相应的handler。所以当请求过来时找不到处理的handler,导致404。
是否可以把我们所需的类都放入Spring-mvc子容器里面来管理(springmvc的spring-servlet.xml中配置全局扫描)?
这个是把包的扫描配置spring-servlet.xml中这个是可行的。为什么可行因为无非就是把所有的东西全部交给子容器来管理了,子容器执行了refresh方法,把在它的配置文件里面的东西全部加载管理起来来了。虽然可以这么做不过一般应该是不推荐这么去做的,一般人也不会这么干的。如果你的项目里有用到事物、或者aop记得也需要把这部分配置需要放到Spring-mvc子容器的配置文件来,不然一部分内容在子容器和一部分内容在父容器,可能就会导致你的事物或者AOP不生效。
springBoot
常见注解
JavaConfig
基于xml
<bean id="bookService" class="cn.moondev.service.BookServiceImpl">
<property name="dependencyService" ref="dependencyService"/>
</bean>
<bean id="otherService" class="cn.moondev.service.OtherServiceImpl">
<property name="dependencyService" ref="dependencyService"/>
</bean>
<bean id="dependencyService" class="DependencyServiceImpl"/>
<property name="dependencyService" ref="dependencyService"/>
</bean>
<bean id="otherService" class="cn.moondev.service.OtherServiceImpl">
<property name="dependencyService" ref="dependencyService"/>
</bean>
<bean id="dependencyService" class="DependencyServiceImpl"/>
基于注解
@Configuration
public class MoonBookConfiguration {
// 如果一个bean依赖另一个bean,则直接调用对应JavaConfig类中依赖bean的创建方法即可
// 这里直接调用dependencyService()
@Bean
public BookService bookService() {
return new BookServiceImpl(dependencyService());
}
@Bean
public OtherService otherService() {
return new OtherServiceImpl(dependencyService());
}
@Bean
public DependencyService dependencyService() {
return new DependencyServiceImpl();
}
}
@ComponentScan
@ComponentScan注解对应XML配置形式中的 <context:component-scan>元素,表示启用组件扫描,Spring会自动扫描所有通过注解配置的bean,然后将其注册到IOC容器中。我们可以通过 basePackages等属性来指定 @ComponentScan自动扫描的范围,如果不指定,默认从声明 @ComponentScan所在类的 package进行扫描。正因为如此,SpringBoot的启动类都默认在 src/main/java下。
@Import
@Import注解用于导入配置类
现在有另外一个配置类,比如: MoonUserConfiguration,这个配置类中有一个bean依赖于 MoonBookConfiguration中的bookService,如何将这两个bean组合在一起?借助 @Import即可:
Conditional
@Conditional注解表示在满足某种条件后才初始化一个bean或者启用某些配置。它一般用在由 @Component、 @Service、 @Configuration等注解标识的类上面,或者由 @Bean标记的方法上。如果一个 @Configuration类标记了 @Conditional,则该类中所有标识了 @Bean的方法和 @Import注解导入的相关类将遵从这些条件。
在Spring里可以很方便的编写你自己的条件类,所要做的就是实现 Condition接口,并覆盖它的 matches()方法。举个例子,下面的简单条件类表示只有在 Classpath里存在 JdbcTemplate类时才生效:
public class JdbcTemplateCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
try {
conditionContext.getClassLoader().loadClass("org.springframework.jdbc.core.JdbcTemplate");
return true;
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return false;
}
}
@Conditional(JdbcTemplateCondition.class)
@Bean
public MyService service() {
......
}
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
try {
conditionContext.getClassLoader().loadClass("org.springframework.jdbc.core.JdbcTemplate");
return true;
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return false;
}
}
@Conditional(JdbcTemplateCondition.class)
@Bean
public MyService service() {
......
}
子主题
@ConfigurationProperties与@EnableConfigurationProperties
当某些属性的值需要配置的时候,我们一般会在 application.properties文件中新建配置项,然后在bean中使用 @Value注解来获取配置的值,比如下面配置数据源的代码。
使用 @Value注解注入的属性通常都比较简单,如果同一个配置在多个地方使用,也存在不方便维护的问题(考虑下,如果有几十个地方在使用某个配置,而现在你想改下名字,你改怎么做?)。对于更为复杂的配置,Spring Boot提供了更优雅的实现方式,那就是 @ConfigurationProperties注解。
@EnableConfigurationProperties注解表示对 @ConfigurationProperties的内嵌支持,默认会将对应Properties Class作为bean注入的IOC容器中,即在相应的Properties类上不用加 @Component注解中。
@EnableAutoConfiguration
注解表示开启Spring Boot自动配置功能,Spring Boot会根据应用的依赖、自定义的bean、classpath下有没有某个类 等等因素来猜测你需要的bean,然后注册到IOC容器中。
该注解上有@Import(EnableAutoConfigurationImportSelector.class)
@Import注解用于导入类,并将这个类作为一个bean的定义注册到容器中,这里它将把EnableAutoConfigurationImportSelector作为bean注入到容器中,而这个类会将所有符合条件的@Configuration配置都加载到容器中
这个类会扫描所有的jar包,将所有符合条件的@Configuration配置类注入的容器中,何为符合条件,看看 META-INF/spring.factories的文件内容:
// 来自 org.springframework.boot.autoconfigure下的META-INF/spring.factories
// 配置的key = EnableAutoConfiguration,与代码中一致
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration\
.....
@EnableAutoConfiguration中导入了EnableAutoConfigurationImportSelector类,而这个类的 selectImports()通过SpringFactoriesLoader得到了大量的配置类,而每一个配置类则根据条件化配置来做出决策,以实现自动配置。
整个流程很清晰,但漏了一个大问题: EnableAutoConfigurationImportSelector.selectImports()是何时执行的?其实这个方法会在容器启动过程中执行: AbstractApplicationContext.refresh()
SpringFactoriesLoader
loadFactoryNames
子主题
从 CLASSPATH下的每个Jar包中搜寻所有 META-INF/spring.factories配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。
执行 loadFactoryNames(EnableAutoConfiguration.class,classLoader)后,得到对应的一组 @Configuration类,
我们就可以通过反射实例化这些类然后注入到IOC容器中,最后容器里就有了一系列标注了 @Configuration的JavaConfig形式的配置类。
这就是 SpringFactoriesLoader,它本质上属于Spring框架私有的一种扩展方案
启动的秘密
1、SpringApplication初始化
SpringBoot整个启动流程分为两个步骤:初始化一个SpringApplication对象、执行该对象的run方法。看下SpringApplication的初始化流程,SpringApplication的构造方法中调用initialize(Object[] sources)方法,其代码如下:
private void initialize(Object[] sources) {
if (sources != null && sources.length > 0) {
this.sources.addAll(Arrays.asList(sources));
}
// 判断是否是Web项目
this.webEnvironment = deduceWebEnvironment();
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 找到入口类
this.mainApplicationClass = deduceMainApplicationClass();
}
初始化流程中最重要的就是通过SpringFactoriesLoader找到 spring.factories文件中配置的ApplicationContextInitializer和 ApplicationListener两个接口的实现类名称,以便后期构造相应的实例。
ApplicationContextInitializer的主要目的是在 ConfigurableApplicationContext做refresh之前,对ConfigurableApplicationContext实例做进一步的设置或处理。ConfigurableApplicationContext继承自ApplicationContext,其主要提供了对ApplicationContext进行设置的能力。
实现一个ApplicationContextInitializer非常简单,因为它只有一个方法,但大多数情况下我们没有必要自定义一个ApplicationContextInitializer,即便是Spring Boot框架,它默认也只是注册了两个实现,毕竟Spring的容器已经非常成熟和稳定,你没有必要来改变它。
而 ApplicationListener的目的就没什么好说的了,它是Spring框架对Java事件监听机制的一种框架实现,具体内容在前文Spring事件监听机制这个小节有详细讲解。这里主要说说,如果你想为Spring Boot应用添加监听器,该如何实现?
Spring Boot提供两种方式来添加自定义监听器:
通过 SpringApplication.addListeners(ApplicationListener<?>...listeners)或者 SpringApplication.setListeners(Collection<?extendsApplicationListener<?>>listeners)两个方法来添加一个或者多个自定义监听器
既然SpringApplication的初始化流程中已经从 spring.factories中获取到 ApplicationListener的实现类,那么我们直接在自己的jar包的 META-INF/spring.factories文件中新增配置即可:
org.springframework.context.ApplicationListener=\cn.moondev.listeners.xxxxListener\
private void initialize(Object[] sources) {
if (sources != null && sources.length > 0) {
this.sources.addAll(Arrays.asList(sources));
}
// 判断是否是Web项目
this.webEnvironment = deduceWebEnvironment();
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 找到入口类
this.mainApplicationClass = deduceMainApplicationClass();
}
初始化流程中最重要的就是通过SpringFactoriesLoader找到 spring.factories文件中配置的ApplicationContextInitializer和 ApplicationListener两个接口的实现类名称,以便后期构造相应的实例。
ApplicationContextInitializer的主要目的是在 ConfigurableApplicationContext做refresh之前,对ConfigurableApplicationContext实例做进一步的设置或处理。ConfigurableApplicationContext继承自ApplicationContext,其主要提供了对ApplicationContext进行设置的能力。
实现一个ApplicationContextInitializer非常简单,因为它只有一个方法,但大多数情况下我们没有必要自定义一个ApplicationContextInitializer,即便是Spring Boot框架,它默认也只是注册了两个实现,毕竟Spring的容器已经非常成熟和稳定,你没有必要来改变它。
而 ApplicationListener的目的就没什么好说的了,它是Spring框架对Java事件监听机制的一种框架实现,具体内容在前文Spring事件监听机制这个小节有详细讲解。这里主要说说,如果你想为Spring Boot应用添加监听器,该如何实现?
Spring Boot提供两种方式来添加自定义监听器:
通过 SpringApplication.addListeners(ApplicationListener<?>...listeners)或者 SpringApplication.setListeners(Collection<?extendsApplicationListener<?>>listeners)两个方法来添加一个或者多个自定义监听器
既然SpringApplication的初始化流程中已经从 spring.factories中获取到 ApplicationListener的实现类,那么我们直接在自己的jar包的 META-INF/spring.factories文件中新增配置即可:
org.springframework.context.ApplicationListener=\cn.moondev.listeners.xxxxListener\
2、 Spring Boot启动流程
Spring Boot应用的整个启动流程都封装在SpringApplication.run方法中,其整个流程真的是太长太长了,但本质上就是在Spring容器启动的基础上做了大量的扩展,按照这个思路来看看源码:
END:
这就是Spring Boot的整个启动流程,其核心就是在Spring容器初始化并启动的基础上加入各种扩展点,这些扩展点包括:ApplicationContextInitializer、ApplicationListener以及各种BeanFactoryPostProcessor等等。你对整个流程的细节不必太过关注,甚至没弄明白也没有关系,你只要理解这些扩展点是在何时如何工作的,能让它们为你所用即可。
整个启动流程确实非常复杂,可以查询参考资料中的部分章节和内容,对照着源码,多看看,我想最终你都能弄清楚的。言而总之,Spring才是核心,理解清楚Spring容器的启动流程,那Spring Boot启动流程就不在话下了。
END:
这就是Spring Boot的整个启动流程,其核心就是在Spring容器初始化并启动的基础上加入各种扩展点,这些扩展点包括:ApplicationContextInitializer、ApplicationListener以及各种BeanFactoryPostProcessor等等。你对整个流程的细节不必太过关注,甚至没弄明白也没有关系,你只要理解这些扩展点是在何时如何工作的,能让它们为你所用即可。
整个启动流程确实非常复杂,可以查询参考资料中的部分章节和内容,对照着源码,多看看,我想最终你都能弄清楚的。言而总之,Spring才是核心,理解清楚Spring容器的启动流程,那Spring Boot启动流程就不在话下了。
① 通过SpringFactoriesLoader查找并加载所有的 SpringApplicationRunListeners,通过调用starting()方法通知所有的SpringApplicationRunListeners:应用开始启动了。
SpringApplicationRunListeners其本质上就是一个事件发布者,它在SpringBoot应用启动的不同时间点发布不同应用事件类型(ApplicationEvent),如果有哪些事件监听者(ApplicationListener)对这些事件感兴趣,则可以接收并且处理。还记得初始化流程中,SpringApplication加载了一系列ApplicationListener吗?这个启动流程中没有发现有发布事件的代码,其实都已经在SpringApplicationRunListeners这儿实现了。
简单的分析一下其实现流程,首先看下SpringApplicationRunListener的源码:
public interface SpringApplicationRunListener {
// 运行run方法时立即调用此方法,可以用户非常早期的初始化工作
void starting();
// Environment准备好后,并且ApplicationContext创建之前调用
void environmentPrepared(ConfigurableEnvironment environment);
// ApplicationContext创建好后立即调用
void contextPrepared(ConfigurableApplicationContext context);
// ApplicationContext加载完成,在refresh之前调用
void contextLoaded(ConfigurableApplicationContext context);
// 当run方法结束之前调用
void finished(ConfigurableApplicationContext context, Throwable exception);
}
SpringApplicationRunListener只有一个实现类: EventPublishingRunListener。①处的代码只会获取到一个EventPublishingRunListener的实例,我们来看看starting()方法的内容:
public void starting() {
// 发布一个ApplicationStartedEvent
this.initialMulticaster.multicastEvent(new ApplicationStartedEvent(this.application, this.args));
}
顺着这个逻辑,你可以在②处的 prepareEnvironment()方法的源码中找到 listeners.environmentPrepared(environment);即SpringApplicationRunListener接口的第二个方法,那不出你所料, environmentPrepared()又发布了另外一个事件 ApplicationEnvironmentPreparedEvent。接下来会发生什么,就不用我多说了吧。
SpringApplicationRunListeners其本质上就是一个事件发布者,它在SpringBoot应用启动的不同时间点发布不同应用事件类型(ApplicationEvent),如果有哪些事件监听者(ApplicationListener)对这些事件感兴趣,则可以接收并且处理。还记得初始化流程中,SpringApplication加载了一系列ApplicationListener吗?这个启动流程中没有发现有发布事件的代码,其实都已经在SpringApplicationRunListeners这儿实现了。
简单的分析一下其实现流程,首先看下SpringApplicationRunListener的源码:
public interface SpringApplicationRunListener {
// 运行run方法时立即调用此方法,可以用户非常早期的初始化工作
void starting();
// Environment准备好后,并且ApplicationContext创建之前调用
void environmentPrepared(ConfigurableEnvironment environment);
// ApplicationContext创建好后立即调用
void contextPrepared(ConfigurableApplicationContext context);
// ApplicationContext加载完成,在refresh之前调用
void contextLoaded(ConfigurableApplicationContext context);
// 当run方法结束之前调用
void finished(ConfigurableApplicationContext context, Throwable exception);
}
SpringApplicationRunListener只有一个实现类: EventPublishingRunListener。①处的代码只会获取到一个EventPublishingRunListener的实例,我们来看看starting()方法的内容:
public void starting() {
// 发布一个ApplicationStartedEvent
this.initialMulticaster.multicastEvent(new ApplicationStartedEvent(this.application, this.args));
}
顺着这个逻辑,你可以在②处的 prepareEnvironment()方法的源码中找到 listeners.environmentPrepared(environment);即SpringApplicationRunListener接口的第二个方法,那不出你所料, environmentPrepared()又发布了另外一个事件 ApplicationEnvironmentPreparedEvent。接下来会发生什么,就不用我多说了吧。
② 创建并配置当前应用将要使用的Environment,Environment用于描述应用程序当前的运行环境,其抽象了两个方面的内容:配置文件(profile)和属性(properties),开发经验丰富的同学对这两个东西一定不会陌生:不同的环境(eg:生产环境、预发布环境)可以使用不同的配置文件,而属性则可以从配置文件、环境变量、命令行参数等来源获取。因此,当Environment准备好后,在整个应用的任何时候,都可以从Environment中获取资源。
总结起来,②处的两句代码,主要完成以下几件事:
判断Environment是否存在,不存在就创建(如果是web项目就创建 StandardServletEnvironment,否则创建 StandardEnvironment)
配置Environment:配置profile以及properties
调用SpringApplicationRunListener的 environmentPrepared()方法,通知事件监听者:应用的Environment已经准备好
总结起来,②处的两句代码,主要完成以下几件事:
判断Environment是否存在,不存在就创建(如果是web项目就创建 StandardServletEnvironment,否则创建 StandardEnvironment)
配置Environment:配置profile以及properties
调用SpringApplicationRunListener的 environmentPrepared()方法,通知事件监听者:应用的Environment已经准备好
③、SpringBoot应用在启动时自定义输出
④、根据是否是web项目,来创建不同的ApplicationContext容器。
⑤、创建一系列 FailureAnalyzer,创建流程依然是通过SpringFactoriesLoader获取到所有实现FailureAnalyzer接口的class,然后在创建对应的实例。FailureAnalyzer用于分析故障并提供相关诊断信息。
⑥、初始化ApplicationContext,主要完成以下工作:
将准备好的Environment设置给ApplicationContext
遍历调用所有的ApplicationContextInitializer的 initialize()方法来对已经创建好的ApplicationContext进行进一步的处理
调用SpringApplicationRunListener的 contextPrepared()方法,通知所有的监听者:ApplicationContext已经准备完毕
将所有的bean加载到容器中
调用SpringApplicationRunListener的 contextLoaded()方法,通知所有的监听者:ApplicationContext已经装载完毕
将准备好的Environment设置给ApplicationContext
遍历调用所有的ApplicationContextInitializer的 initialize()方法来对已经创建好的ApplicationContext进行进一步的处理
调用SpringApplicationRunListener的 contextPrepared()方法,通知所有的监听者:ApplicationContext已经准备完毕
将所有的bean加载到容器中
调用SpringApplicationRunListener的 contextLoaded()方法,通知所有的监听者:ApplicationContext已经装载完毕
⑦、调用ApplicationContext的 refresh()方法,完成IoC容器可用的最后一道工序。从名字上理解为刷新容器,那何为刷新?就是插手容器的启动,联系一下第一小节的内容。那如何刷新呢?且看下面代码:
// 摘自refresh()方法中一句代码
invokeBeanFactoryPostProcessors(beanFactory);
看看这个方法的实现:
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
......
}
获取到所有的 BeanFactoryPostProcessor来对容器做一些额外的操作。BeanFactoryPostProcessor允许我们在容器实例化相应对象之前,对注册到容器的BeanDefinition所保存的信息做一些额外的操作。这里的getBeanFactoryPostProcessors()方法可以获取到3个Processor:
ConfigurationWarningsApplicationContextInitializer$ConfigurationWarningsPostProcessor
SharedMetadataReaderFactoryContextInitializer$CachingMetadataReaderFactoryPostProcessor
ConfigFileApplicationListener$PropertySourceOrderingPostProcessor
不是有那么多BeanFactoryPostProcessor的实现类,为什么这儿只有这3个?因为在初始化流程获取到的各种ApplicationContextInitializer和ApplicationListener中,只有上文3个做了类似于如下操作:
public void initialize(ConfigurableApplicationContext context) {
context.addBeanFactoryPostProcessor(new ConfigurationWarningsPostProcessor(getChecks()));
}
然后你就可以进入到 PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()方法了,这个方法除了会遍历上面的3个BeanFactoryPostProcessor处理外,还会获取类型为 BeanDefinitionRegistryPostProcessor的bean: org.springframework.context.annotation.internalConfigurationAnnotationProcessor,对应的Class为 ConfigurationClassPostProcessor。
ConfigurationClassPostProcessor用于解析处理各种注解,包括:@Configuration、@ComponentScan、@Import、@PropertySource、@ImportResource、@Bean。当处理 @import注解的时候,就会调用<自动配置>这一小节中的 EnableAutoConfigurationImportSelector.selectImports()来完成自动配置功能。
// 摘自refresh()方法中一句代码
invokeBeanFactoryPostProcessors(beanFactory);
看看这个方法的实现:
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
......
}
获取到所有的 BeanFactoryPostProcessor来对容器做一些额外的操作。BeanFactoryPostProcessor允许我们在容器实例化相应对象之前,对注册到容器的BeanDefinition所保存的信息做一些额外的操作。这里的getBeanFactoryPostProcessors()方法可以获取到3个Processor:
ConfigurationWarningsApplicationContextInitializer$ConfigurationWarningsPostProcessor
SharedMetadataReaderFactoryContextInitializer$CachingMetadataReaderFactoryPostProcessor
ConfigFileApplicationListener$PropertySourceOrderingPostProcessor
不是有那么多BeanFactoryPostProcessor的实现类,为什么这儿只有这3个?因为在初始化流程获取到的各种ApplicationContextInitializer和ApplicationListener中,只有上文3个做了类似于如下操作:
public void initialize(ConfigurableApplicationContext context) {
context.addBeanFactoryPostProcessor(new ConfigurationWarningsPostProcessor(getChecks()));
}
然后你就可以进入到 PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()方法了,这个方法除了会遍历上面的3个BeanFactoryPostProcessor处理外,还会获取类型为 BeanDefinitionRegistryPostProcessor的bean: org.springframework.context.annotation.internalConfigurationAnnotationProcessor,对应的Class为 ConfigurationClassPostProcessor。
ConfigurationClassPostProcessor用于解析处理各种注解,包括:@Configuration、@ComponentScan、@Import、@PropertySource、@ImportResource、@Bean。当处理 @import注解的时候,就会调用<自动配置>这一小节中的 EnableAutoConfigurationImportSelector.selectImports()来完成自动配置功能。
⑧、查找当前context中是否注册有CommandLineRunner和ApplicationRunner,如果有则遍历执行它们。
⑨、执行所有SpringApplicationRunListener的finished()方法。
3、tomcat在spring boot中如何启动
排除tomcat,打出war
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 移除嵌入式tomcat插件 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--添加servlet-api依赖--->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 移除嵌入式tomcat插件 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--添加servlet-api依赖--->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
@SpringBootApplication
public class MySpringbootTomcatStarter extends SpringBootServletInitializer {
public static void main(String[] args) {
Long time=System.currentTimeMillis();
SpringApplication.run(MySpringbootTomcatStarter.class);
System.out.println("===应用启动耗时:"+(System.currentTimeMillis()-time)+"===");
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(this.getClass());
}
}
public class MySpringbootTomcatStarter extends SpringBootServletInitializer {
public static void main(String[] args) {
Long time=System.currentTimeMillis();
SpringApplication.run(MySpringbootTomcatStarter.class);
System.out.println("===应用启动耗时:"+(System.currentTimeMillis()-time)+"===");
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(this.getClass());
}
}
重点问题
1、什么是Spring?谈谈对Spring的理解?
1、整体介绍:Spring是一个轻量级的一站式java企业级开发框架,目的是为了解决企业级应用开发的业务逻辑层与其它各层之间的 耦合问题。所谓一站式框架是指 Spring 有 JavaEE 开发的每一层解决方案。如控制层的 SpringMVC,持久层的jdbc等,业务层的Spring的Bean管理,声明式事务等
2、特性:Spring可以做许多事情,它为企业级开发提供了许多丰富的功能,但是这些功能的底层都依赖于它的两个核心特性,也就是依赖注入(DI)和面向切面编程(AOP)
Spring的优缺点?
优点
方便解耦 ,简化开发:Spring就是一个大工厂,可以将所有对象的创建和依赖关系的维护,交给Spring管理
AOP编程的支持:Spring提供面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等功能
声明式事务的支持:只需要通过配置就可以完成对事务的管理,而无需手动编程
方便程序的测试:Spring对Junit4支持,可以通过注解方便的测试Spring程序。
方便集成各种优秀框架:Spring不排斥各种优秀的开源框架,其内部提供了对各种优秀框架的直接支持(如:Struts、Hibernate、MyBatis等)
降低JavaEE API的使用难度:Spring对JavaEE开发中非常难用的一些API(JDBC、JavaMail、远程调用等),都提供了封装,使这些API应用难度大大降低
缺点
Spring明明一个很轻量级的框架,却给人感觉大而全
Spring依赖反射,反射影响性能
使用门槛升高,入门Spring需要较长时间
2、Spring框架中使用了哪些设计模式?
工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实例;
单例模式:Bean默认为单例模式
代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术;
模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如Spring中listener的实现–ApplicationListener
3、谈谈对AOP的理解?
4、谈谈对IOC的理解?
概念
控制反转即IoC (Inversion of Control),它把传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。所谓的“控制反转”概念就是对组件对象控制权的转移,从程序代码本身转移到了外部容器。
Spring IOC 负责创建对象,管理对象(通过依赖注入(DI),装配对象,配置对象,并且管理这些对象的整个生命周期。
作用
管理对象的创建和依赖关系的维护:由容器去管理对象之间复杂的依赖过程
解耦:由容器去维护具体的对象
托管了类的产生过程:比如我们需要在类的产生过程中做一些处理,最直接的例子就是代理,如果有容器程序可以把这部分处理交给容器,应用程序则无需去关心类是如何完成代理的
底层实现原理
工厂模式加反射机制
支持功能(弱)
依赖注入
依赖查找
自动装配
支持集合
指定初始化方法和销毁方法
支持回调某些方法
主要实现方式
依赖注入
依赖查找
5、解释一下Spring Bean的生命周期?
依赖注入
什么是依赖注入?
控制反转IoC是一个很大的概念,可以用不同的方式来实现。其主要实现方式有两种:依赖注入和依赖查找
依赖注入:相对于IoC而言,依赖注入(DI)更加准确地描述了IoC的设计理念。所谓依赖注入(Dependency Injection),即组件之间的依赖关系由容器在应用系统运行期来决定,也就是由容器动态地将某种依赖关系的目标对象实例注入到应用系统中的各个关联的组件之中。组件不做定位查询,只提供普通的Java方法让容器去决定依赖关系。
依赖注入的基本原则
依赖注入的基本原则是:应用组件不应该负责查找资源或者其他依赖的协作对象。配置对象的工作应该由IoC容器负责,“查找资源”的逻辑应该从应用组件的代码中抽取出来,交给IoC容器负责。容器全权负责组件的装配,它会把符合依赖关系的对象通过属性(JavaBean中的setter)或者是构造器传递给需要的对象。
依赖注入有什么优势
依赖注入之所以更流行是因为它是一种更可取的方式:让容器全权负责依赖查询,受管组件只需要暴露JavaBean的setter方法或者带参数的构造器或者接口,使容器可以在初始化时组装对象的依赖关系。其与依赖查找方式相比,主要优势为:
查找定位操作与应用代码完全无关
不依赖于容器的API,可以很容易地在任何容器以外使用应用对象
不需要特殊的接口,绝大多数对象可以做到完全不必依赖容器
有哪些不同类型的依赖注入实现方式?
接口注入(Interface Injection):灵活性较差,Spring4开始废除
Setter方法注入(Setter Injection)
构造器注入(Constructor Injection)三种方式
构造器依赖注入和 Setter方法注入的区别
构造器注入没有 部分注入,Setter注入有部分注入
构造器注入不会覆盖setter属性,Setter注入会覆盖setter属性
构造器注入任意修改都会创造一个新的实例,Setter注入人一修改不会创建新的实例
构造器注入适用于设置很多属性,Setter注入适用于设置少量属性
次要问题
Spring由哪些模块组成?
Spring 总共大约有 20 个模块, 由 1300 多个不同的文件构成。 而这些组件被分别整合在核心容器(Core Container) 、 AOP(Aspect Oriented Programming)和设备支持(Instrmentation) 、数据访问与集成(Data Access/Integeration) 、 Web、 消息(Messaging) 、 Test等 6 个模块中。
谈谈Spring中核心容器(spring context应用上下文) 模块?
这是基本的Spring模块,提供spring 框架的基础功能,BeanFactory 是任何以spring为基础的应用的核心。Spring 框架建立在此模块之上,它使Spring成为一个容器。
Bean 工厂是工厂模式的一个实现,提供了控制反转功能,用来把应用的配置和依赖从真正的应用代码中分离。
Spring框架中有哪些不同类型的事件
上下文更新事件(ContextRefreshedEvent):在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发
上下文开始事件(ContextStartedEvent):当容器调用ConfigurableApplicationContext的Start()方法开始/重新开始容器时触发该事件。
上下文停止事件(ContextStoppedEvent):当容器调用ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。
上下文关闭事件(ContextClosedEvent):当ApplicationContext被关闭时触发该事件。容器被关闭时,其管理的所有单例Bean都被销毁。
请求处理事件(RequestHandledEvent):在Web应用中,当一个http请求(request)结束触发该事件。如果一个bean实现了ApplicationListener接口,当一个ApplicationEvent 被发布以后,bean会自动被通知。
Mybatis
简介
Mybatis也是ORM框架的一种
使用Mybatis的主要原因是Mybatis比Hibernate的灵活性更高
处理复杂的业务的时候会更加方便
映射文件
Statement的实际位置就等于namespace+StatementId
占位符:
#{}解析传递进来的参数数据
更多脑图和最新原创技术文章可关注公众号:Java3y
${}对传递进来的参数原样拼接在SQL中
主键返回
mysql通过LAST_INSERT_ID()实现
oracle通过序列来实现
resultMap
resultMap我们用来告诉Mybaits如何将返回的查询结果进行封装
如果列名和属性名是完全一致的话,那么我们是可以省略的。但是一般来说都是不一样的,所以往往还是需要resultMap
在resultMap内部还可以使用两个节点标签
association:将关联查询信息映射到一个pojo类中。
collection:将关联查询信息映射到一个list集合中。
如果在集合中还有对象的关系要体现出来的话,那我们只能使用ofType。
如果集合中都是对象本身基本属性了,那么可以使用resultMap
resultType
如果是一个简单的对象(没有引用类型)或者返回简单类型数据,我们就可以直接使用resultType来用了
总的来说,resultMap是用得非常多的一个节点
映射关系
一对一
一对多
多对多
其实我们编好了SQL语句,看看在实际中是否需要拿到关联关系的数据如果需要就配置resultMap-->通过association和collection如果不需要直接返回我们原生的resultMap就行了
Mapper代理
需要实现的规范:
mapper.xml中namespace指定为mapper接口的全限定名
此步骤目的:通过mapper.xml和mapper.java进行关联。
mapper.xml中statement的id就是mapper.java中方法名
mapper.xml中statement的parameterType和mapper.java中方法输入参数类型一致
mapper.xml中statement的resultType和mapper.java中方法返回值类型一致.
Mapper代理返回值问题
如果是返回的单个对象,返回值类型是pojo类型,生成的代理对象内部通过selectOne获取记录
如果返回值类型是集合对象,生成的代理对象内部通过selectList获取记录。
快速入门
主配置文件
配置数据库信息
加载映射文件
映射文件
描述具体方法对应的SQL语句
工作流程
通过Reader对象读取Mybatis映射文件通过SqlSessionFactoryBuilder对象创建SqlSessionFactory对象获取当前线程的SQLSession事务默认开启通过SQLSession读取映射文件中的操作编号,从而读取SQL语句提交事务关闭资源
更多脑图和最新原创技术文章可关注公众号:Java3y
Mybatis的SQL语句是需要手写的,在程序中通过映射文件的命名空间.sql语句的id来进行调用!
动态SQL
Mybatis支持一些判断标签,于是我们就可以通过这些标签来完成动态CRUD的操作了
主配置文件
自定义别名,在映射文件中我们就可以使用别名了
加载映射文件
设置延迟加载
配置数据库的信息
Mybatis配置文件详解
缓存
一级缓存
Mybatis默认是开启一级缓存的
一级缓存是基于sqlSession的缓存
实现原理就是:通过一个Map来实现同一个sqlsession再次发出相同的sql,就从缓存中取不走数据库。如果两次中间出现commit操作(修改、添加、删除),本sqlsession中的一级缓存区域全部清空,下次再去缓存中查询不到所以要从数据库查询,从数据库查询到再写入缓存。
与Spring整合之后,使用的是Mappper代理对应,一级缓存是失效的~
同一线程里面两次查询同一数据所使用的sqlsession是不相同的
二级缓存
Mybaits的二级缓存是需要自己在配置文件中配置的
二级缓存是基于Mapper(同一个命名空间)的缓存
查询结果映射的pojo需要实现 java.io.serializable接口
禁用二级缓存
useCache="false"
刷新缓存
执行增删改操作默认是会刷新缓存的,但我们也可以配置不刷新(不建议使用)
flushCache="false"
Mybatis和Ehcache框架整合
优点和缺点
优点:对于访问多的查询请求且用户对查询结果实时性要求不高,此时可采用mybatis二级缓存技术降低数据库访问量,提高访问速度
缺点:因为mybaits的二级缓存区域以mapper为单位划分,当一个商品信息变化会将所有商品信息的缓存数据全部清空
逆向工程
在pom中添加插件
编写自动生成的配置文件
如果出错了,注意版本的问题
mybatis - 动态SQL
Java常见排列算法
归并排序
基本思想
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
代码实现
堆排序
基本思想
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。首先简单了解下堆结构。
堆
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
堆
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
堆排序基本思想及步骤
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
代码实现
子主题
基数排序
排序原理
基数排序不需要进行元素的比较与交换。如果你有一些算法的功底,或者丰富的项目经验,我想你可能已经想到了这可能类似于一些“打表”或是哈希的做法。而计数排序则是打表或是哈希思想最简单的实现。
算法优化
在算法的原理中,我们是以一张二维数组的表来存储这些无序的元素。使用二维数组有一个很明显的不足就是二维数组太过稀疏。数组的利用率为 10%。
在寻求优化的路上,我们想到一种可以压缩空间的方法,且时间复杂度并没有偏离得太厉害。那就是设计了两个辅助数组,一个是 count[],一个是 bucket[]。count 用于记录在某个桶中的最后一个元素的下标,然后再把原数组中的元素计算一下它应该属于哪个“桶”,并修改相应位置的 count 值。直到最大数的最高位也被添加到桶中,或者说,当所有的元素都被被在第 0 个桶中,基数排序就结束了。
在寻求优化的路上,我们想到一种可以压缩空间的方法,且时间复杂度并没有偏离得太厉害。那就是设计了两个辅助数组,一个是 count[],一个是 bucket[]。count 用于记录在某个桶中的最后一个元素的下标,然后再把原数组中的元素计算一下它应该属于哪个“桶”,并修改相应位置的 count 值。直到最大数的最高位也被添加到桶中,或者说,当所有的元素都被被在第 0 个桶中,基数排序就结束了。
代码实现
子主题
冒泡排序
排序原理
大家一定都喝过汽水吧,汽水中常常有许多小小的气泡,往上飘,这是因为组成小气泡的二氧化碳比水要轻,所以小气泡才会一点一点的向上浮。而冒泡排序之所以叫冒泡排序,正是因为这种排序算法的每一个元素都可以向小气泡一样,根据自身大小,一点一点向着数组的一侧移动。
专业术语
子主题
希尔排序
基本思想
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
代码实现
子主题
快速排序
排序原理
用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它左边,所有比它大的数都放到它右边,这个过程称为一趟快速排序。值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。
代码实现
子主题
插入排序
实现思路
1.从数组的第二个数据开始往前比较,即一开始用第二个数和他前面的一个比较,如果 符合条件(比前面的大或者小,自定义),则让他们交换位置。
2.然后再用第三个数和第二个比较,符合则交换,但是此处还得继续往前比较,比如有 5个数8,15,20,45, 17,17比45小,需要交换,但是17也比20小,也要交换,当不需 要和15交换以后,说明也不需要和15前面的数据比较了,肯定不需要交换,因为前 面的数据都是有序的。
3.重复步骤二,一直到数据全都排完。
2.然后再用第三个数和第二个比较,符合则交换,但是此处还得继续往前比较,比如有 5个数8,15,20,45, 17,17比45小,需要交换,但是17也比20小,也要交换,当不需 要和15交换以后,说明也不需要和15前面的数据比较了,肯定不需要交换,因为前 面的数据都是有序的。
3.重复步骤二,一直到数据全都排完。
代码实现
子主题
选择排序
算法思想
从头至尾扫描序列,找出最小的一个元素,和第一个元素交换,接着从剩下的元素中继续这种选择和交换方式,最终得到一个有序序列。
代码实现
子主题
并发
Java内存模型(JMM)
定义
JMM定义了一组规则或规范,该规范定义了一个线程对共享变量写入时,如何确保对另一个线程是可见的。
要点
所有变量存储在主存中。
每个线程都有自己的工作内存,且对变量的操作都是在工作内存中进行的。
不同线程之间无法直接访问彼此工作内存中的变量,要想访问只能通过主存来传递
图解
JMM的8个操作
定义
Read
主存
作用于主存变量。Read操作把一个变量的值从主存传输到工作内存中,以便随后的Load操作使用
Load
工作内存
作用于工作内存的变量。Load 操作把Read操作从主存中得到的变量值载入工作内存的变量副本中。
变量副本可以简单理解为CPU的高速缓存。
变量副本可以简单理解为CPU的高速缓存。
Use
工作内存
作用于工作内存的变量。Use操作把工作内存中的一个变量的值传递给执行引擎。 每当JVM遇到
一个需要使用变量值的字节码指令时,执行Use操作。
一个需要使用变量值的字节码指令时,执行Use操作。
Assign
工作内存
作用于工作内存的变量。执行引擎通过Assign 操作给工作内存的工作内存变量赋值。 每当JVM遇到
一个给变量赋值的字节码指令时,执行Assign操作。
一个给变量赋值的字节码指令时,执行Assign操作。
Store
工作内存
作用于工作内存的变量。Store 操作把工作内存中的一个变量的值传递到主存中,以便随后的Write 操作使用
Write
主存
作用于主存的变量。Write 操作把Store操作从工作内存中得到的变量值放入主存的变量中
Lock
主存
作用于主存的变量,把一个变量标识为某个线程独占状态
Unlock
主存
作用于主存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
图解流程
synchronized
浅谈synchronized
前置知识点
对象头
- 对象头包含三个字段
Mark Word
Mark Word(标记字),用于存储自身运行时的数据,例如GC标志位、哈希码、锁状态等信息。
Class Pointer
Class Pointer(类对象指针),用于存放方法区Class对象的地址,虚拟机通过这个指针来确定这个对象是哪个类的实例。
Array Length
如果对象是一个Java数组,那么此字段必须有,用于记录数组长度的数据;如果对象不是一个Java数组,那么此字段不存在,所以这是一个可选字段。
图解
Mark Word解读
Mark Word字段中存放了Java内置锁的信息
Java内置锁的状态总共有4种,级别由低到高依次为:无锁、偏向锁、轻量级锁和重量级锁。并且4种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行锁升级(从低级别到高级别)。
Java内置锁状态一览
图解
关键信息解读
内置锁信息
lock
锁状态标记位,占两个二进制位,由于希望用尽可能少的二进制位表示尽可能多的信息,因此设置了lock标记。该标记的值不同,整个Mark Word表示的含义就不同。
biased_lock
对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。lock和biased_lock两个标记位组合在一起共同表示Object实例处于什么样的锁状态。
其他
age
4位的Java对象分代年龄。在GC中,对象在Survivor区复制一次,年龄就增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,因此最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
identity_hashcode
31位的对象标识HashCode(哈希码)采用延迟加载技术,当调用Object.hashCode()方法或者System.identityHashCode()方法计算对象的HashCode后,其结果将被写到该对象头中。当对象被锁定时,该值会移动到Monitor(监视器)中。
thread
54位的线程ID值为持有偏向锁的线程ID。
epoch
偏向时间戳。
ptr_to_lock_record
占62位,在轻量级锁的状态下指向栈帧中锁记录的指针。
ptr_to_heavyweight_monitor
占62位,在重量级锁的状态下指向对象监视器的指针。
锁状态解读
无锁状态
java对象刚创建时,还有任何线程来竞争,说明该对象处于无锁状态,此时这时偏向锁标识位是0,锁状态是01。
偏向锁
加锁场景
偏向锁是指一段同步代码一直被同一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。如果内置锁处于偏向状态,当有一个线程来竞争锁时,先用偏向锁,表示内置锁偏爱这个线程,这个线程要执行该锁关联的同步代码时,不需要再做任何检查和切换。偏向锁在竞争不激烈的情况下效率非常高。偏向锁状态的Mark Word会记录内置锁自己偏爱的线程ID。
缺点
如果锁对象时常被多个线程竞争,偏向锁就是多余的,并且其撤销的过程会带来一些性能开销。
轻量级锁
加锁场景
当有两个线程开始竞争这个锁对象时,情况就发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先通过CAS操作占有锁对象,锁对象的Mark Word就指向哪个线程的栈帧中的锁记录。当锁处于偏向锁,又被另一个线程企图抢占时,偏向锁就会升级为轻量级锁。企图抢占的线程会通过自旋的形式尝试获取锁,不会阻塞抢锁线程,以便提高性能。
示意图
重量级锁
过程详解
如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,自旋不会一直持续下去,这时争用线程会停止自旋进入阻塞状态,该锁膨胀为重量级锁。重量级锁会让其他申请的线程之间进入阻塞,性能降低。重量级锁也叫同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,该监视器对象用集合的形式来登记和管理排队的线程。
此时就又回到了synchronized的底层原理了
性能开销
由于JVM轻量级锁使用CAS进行自旋抢锁,这些CAS操作都处于用户态下,进程不存在用户态和内核态之间的运行切换,因此JVM轻量级锁开销较小。而JVM重量级锁使用了Linux内核态下的互斥锁,这是重量级锁开销很大的原因。
偏向锁,轻量级锁,重量级锁的对比
底层原理
前置知识点
Monitor监视器
示意图
monitorenter
// 代码对应指令
monitorexit
定义
Monitor监视器是一个同步工具,相当于一个许可证,拿到许可证的线程即可进入临界区进行操作,没有拿到则需要阻塞等待。重量级锁通过监视器的方式保障了任何时间只允许一个线程通过受到监视器保护的临界区代码。
特点
同步
监视器所保护的临界区代码是互斥地执行的。一个监视器是一个运行许可,任一线程进入临界区代码都需要获得这个许可,离开时把许可归还。
协作
监视器提供Signal机制,允许正持有许可的线程暂时放弃许可进入阻塞等待状态,等待其他线程发送Signal去唤醒;其他拥有许可的线程可以发送Signal,唤醒正在阻塞等待的线程,让它可以重新获得许可并启动执行。
名词解释
Cxq
竞争队列(Contention Queue),所有请求锁的线程首先被放在这个竞争队列中。
EntryList
Cxq中那些有资格成为候选资源的线程被移动到EntryList中。
WaitSet
某个拥有ObjectMonitor的线程在调用Object.wait()方法之后将被阻塞,然后该线程将被放置在WaitSet链表中。
ObjectMonitor的内部抢锁过程
抢锁步骤
每个类/对象(对象包含Object实例和Class实例。)都有一个关联的monitor,monitor里面有一个计数器,从0开始
如果要对对象加锁,那么必须先这个对象获取关联monitor的lock锁
如果一个线程要获取monitor的锁,那么就要先看这个计数器是不是0
如果是0,那么说明没人获取锁,他可以获取锁,然后对计数器 加 1
支持重入锁
代码演示
synchronized(myObject){ // 类的class对象来走的
// 一大堆代码
synchronized(myObject){
// 一大堆代码
}
}
如果不是0,那么说明有其他线程获取到锁了,那么它就什么事也干不了,只能进入阻塞状态,等着获取锁
接着如果出了synchronized修饰的代码片段,会执行monitorexit指令
此时获取锁的线程就会对那个对象的monitor里的计数器减 1,如果有多次重入加锁,那就多次减 1 ,直至减为0
此时锁被释放,其他阻塞住的线程可以重新请求获取锁
只有一个线程能成功获取锁
可以保证原子性、有序性、可见性
原子性
加锁和释放锁,ObjectMonitor
可见性
加锁,在进入synchronized代码块时的读操作,都会强制执行refresh
Load内存屏障
释放锁,在出代码块时,代码块内所有的写操作,都会强制执行flush操作
Store内存屏障
有序性
通过加各种内存屏障,保证有序性
代码块内部不保证有序性
但是同步代码块内部的指令和外部的指令,是不能重排的
底层原理示意图
很简单,JDK 1.6之后,对synchronized内的加锁机制做了大量的优化,这里就是优化为CAS加锁的
你在之前把ReentrantLock底层的源码都读懂了,AQS的机制都读懂了之后,那么synchronized底层的实现差不多的
synchronized的ObjectMonitor的地位就跟ReentrantLock里的AQS是差不多的
线程间通信
定义
当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以避免无效的资源争夺。
通信方法
线程间的通信需要借助同步对象(Object)的监视器来完成,Object对象的wait()、notify()方法就如开关信号,用于完成等待方和通知方之间的通信。
wait()和notify()系列方法需要在同步块中使用,否则JVM会抛出异常
对比
synchronized和locks包的锁有什么不同
其实锁的实现原理都是一个目的,让所有线程看到某种标记
synchronized是通过在对象头设置一个标记。上面加一个mark word
是一种JVM原生的锁实现方式
ReentrantLock以及所有基于Lock接口的实现类,都是通过一个被volatile修饰的int型变量
并保证对所有线程都能拥有对该int的可见性和原子修改,其本质是基于所谓的AQS框架
如何选择
Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现的
synchronized在发生异常时,会自动释放线程粘有的锁,因此不会导致死锁现象的发生
而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此需要再finally块中释放锁
Lock可以让等待锁的线程响应中断,而synchronized却不行
使用synchronized时,等待的线程会一直等待下去,不能响应中断
通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
Lock可以提高多个线程进行读操作的效率
从性能上来说,如果竞争资源不激烈,两者的性能是差不多的
而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized
类似zk的羊群效应?Curator对zk锁的优化类似?
总结
类锁所有对象一把锁,对象锁一个对象一把锁,多个对象多把锁
类锁
类锁就是对jvm中类对应的class对象加锁
SynchronizedTest.init();
public synchronized static void init(){}
synchronized (MyService.class){}
对象锁
对象锁是对单个对象实例加锁
SynchronizedTest synchronizedTest = new SynchronizedTest();
synchronizedTest.init();
public synchronized void init() {}
synchronized (synchronizedTest){}
1.6以后的锁优化
锁消除
锁消除是JIT编译器对synchronized锁做的优化,在编译的时候,JIT会通过逃逸分析技术,来分析synchronized锁对象
是不是只有一个线程来加锁,没有其他的线程来竞争加锁,这个时候编译就不用加入monitorenter和monitorexit的指令
毕竟没有多线程并发的情况,加锁也是浪费性能,总体效果就是好像没有加锁一样,锁消除了。
锁粗化
JIT编译器如果发现对一个对象中多次调用synchronized修饰的方法,这个时候释放锁和添加锁其实都是针对同一个对象而言的。此时就会干脆在底层将这些方法合并成一个synchronized方法,只会执行一次monitorenter和monitorexit指令,效果就好像锁粗化了一样。
自适应性锁
自旋锁
你这个锁里面的代码实际执行的非常快
当其他线程获取锁未成功时,不切换线程,自旋一会,等待这个锁释放,减少上下文切换带来的性能消耗
AQS
AbstractQueueSynchronizer
抽象队列同步器
抽象队列同步器
图解AQS
底层原理
AQS使用volatile修饰的int类型的state标示锁的同步状态
源码
/**
* The synchronization state.
*/
private volatile int state;
* The synchronization state.
*/
private volatile int state;
AQS是CLH队列的一个变种,是一个虚拟队列,不存在队列实例,仅存在节点之间的前后关系。节点类型通过内部类Node定义
源码
节点之间的结构
ReentrantLock
ReentrantLock底层原理
线程1
CAS更新 state = 1
state = 0 -> state = 1
线程1
线程2
CAS更新 state = 1
等待队列
线程2
ReentrantLock源码分析
new ReentrantLock()
NonfairSync ->非公平锁
lock()
直接尝试获取锁
未获取锁成功,再执行acquire()方法
tryAcquire()尝试获取锁
直接尝试获取锁
未获取到锁,看下当前拿到锁的线程是不是自己,是自己则state + 1重入
图解
new ReentrantLock(true)
FairSync -> 公平锁
lock()
acquire()
tryAcquire()尝试获取锁
看看队列里是否有人排队
没人排队的话再尝试获取锁
未获取到锁,看下当前拿到锁的线程是不是自己,是自己则state + 1重入
图解
ReentrantLock与AQS组合关系
等效不可变对象CopyOnWriteArrayList
源码解读
CopyOnWriteArrayList源码中维护了一个array对象数组用于存储集合的每个元素,并且array数组只能通过getArray和setArray方法来访问。
在调用iterator方法的时候,会通过getArray()方法获取array数组,然后可以基于这个数组进行遍历。
新增一个元素,调用add方法的时候,也是通过getArray()获取到对象数组,然后直接新生成一个数组,最后把旧的数组的值复制到新的数组中,然后直接使用新的数组覆盖实例变量array。
特性
CopyOnWriteArrayList实例变量array本质上是一个数组,而数组的各个元素都是一个对象,每个对象内部的状态是可以替换的。因此实例变量并非严格意义上的不可变对象,所以我们称之为等效不可变对象。
适用场景
通过弱一致性提升读请求并发,适合用在数据读多写少的场景
源码运用
JDBC中的数据库驱动程序列表管理
原子类
CAS
即compareAndSet
多线程同时读取主内存中的数据,必然会导致并发安全问题
优点
CAS即基于底层硬件实现,给你保证一定是原子性的
即同一时间只有一个线程可以成功执行CAS
先比较再设置
其他线程执行CAS会失败
并发包下AtomicInteger等类天然支持CAS保证原子性
缺点
只能保证一个变量的原子操作
长时间自旋,开销大
存在ABA问题
Unsafe
Unsafe类可以像C语言一样使用指针操作内存空间
操作系统层面的CAS是一条CPU的原子指令(cmpxchg指令),正是由于该指令具备原子性,因此使用CAS操作数据时不会造成数据不一致的问题,Unsafe提供的CAS方法直接通过native方式(封装C++代码)调用了底层的CPU指令cmpxchg。
Unsafe的CAS操作会将第一个参数(对象的指针、地址)与第二个参数(字段偏移量)组合在一起,计算出最终的内存操作地址。
Unsafe类中三个关键方法
//字段所在的对象、字段内存位置、预期原值及新值。
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
CAS操作的性能问题
在争用激烈的场景下,会导致大量的CAS空自旋。比如,在大量线程同时并发修改一个AtomicInteger时,可能有很多线程会不停地自旋,甚至有的线程会进入一个无限重复的循环中。大量的CAS空自旋会浪费大量的CPU资源,大大降低了程序的性能。
在高并发场景下如何提升CAS操作的性能呢?
可以使用LongAdder替代AtomicInteger。Java 8提供了一个新的类LongAdder,以空间换时间的方式提升高并发场景下CAS操作的性能。LongAdder的核心思想是热点分离,与ConcurrentHashMap的设计思想类似:将value值分离成一个数组,当多线程访问时,通过Hash算法将线程映射到数组的一个元素进行操作;而获取最终的value结果时,则将数组的元素求和。最终,通过LongAdder将内部操作对象从单个value值“演变”成一系列的数组元素,从而减小了内部竞争的粒度。
原理图
LongAdder源码解读
LongAdder的内部成员包含一个base值和一个cells数组。在最初无竞争时,只操作base的值;当线程执行CAS失败后,才初始化cells数组,并为线程分配所对应的元素。LongAdder中没有类似于AtomicLong中的getAndIncrement()或者incrementAndGet()这样的原子操作,所以只能通过increment()方法和longValue()方法的组合来实现更新和获取的操作。
源码
LongAdder
longAccumulate
JUC并发包中原子类
基本原子类
基本原子类的功能是通过原子方式更新Java基础类型变量的值。
● AtomicInteger:整型原子类。
● AtomicLong:长整型原子类。
● AtomicBoolean:布尔型原子类。
● AtomicLong:长整型原子类。
● AtomicBoolean:布尔型原子类。
AtomicInteger
底层原理
主要通过CAS自旋+volatile的方案实现,既保障了变量操作的线程安全性,又避免了synchronized重量级锁的高开销,使得Java程序的执行效率大为提升。说明CAS用于保障变量操作的原子性,volatile关键字用于保障变量的可见性,二者常常结合使用。
源码
数组原子类
数组原子类的功能是通过原子方式更数组中的某个元素的值。
● AtomicIntegerArray:整型数组原子类。
● AtomicLongArray:长整型数组原子类。
● AtomicReferenceArray:引用类型数组原子类。
● AtomicLongArray:长整型数组原子类。
● AtomicReferenceArray:引用类型数组原子类。
引用原子类
● AtomicReference:引用类型原子类。
● AtomicMarkableReference:带有更新标记位的原子引用类型。
● AtomicStampedReference:带有更新版本号的原子引用类型。
● AtomicMarkableReference类将boolean标记与引用关联起来,可以解决使用AtomicBoolean进行原子更新时可能出现的ABA问题。
● AtomicStampedReference类将整数值与引用关联起来,可以解决使用AtomicInteger进行原子更新时可能出现的ABA问题
● AtomicMarkableReference:带有更新标记位的原子引用类型。
● AtomicStampedReference:带有更新版本号的原子引用类型。
● AtomicMarkableReference类将boolean标记与引用关联起来,可以解决使用AtomicBoolean进行原子更新时可能出现的ABA问题。
● AtomicStampedReference类将整数值与引用关联起来,可以解决使用AtomicInteger进行原子更新时可能出现的ABA问题
字段更新原子类
● AtomicIntegerFieldUpdater:原子更新整型字段的更新器。
● AtomicLongFieldUpdater:原子更新长整型字段的更新器。
● AtomicReferenceFieldUpdater:原子更新引用类型中的字段。
● AtomicLongFieldUpdater:原子更新长整型字段的更新器。
● AtomicReferenceFieldUpdater:原子更新引用类型中的字段。
volatile
用来解决可见性和有序性的
在有些罕见的条件下,可以保证原子性 (double / floot)
32位虚拟机中,对于这种64位的操作,可能会有高32位、低32位并发写的问题,volatile是能保证这种数据的原子性的
加了volatile关键字修饰的参数,在读写的时候会强制执行flush和reflush操作
然后通过总线嗅探机制,保证其他线程的可见性
内存屏障
Load
Store
Acquire
Release
等等,这个不用细扣。各个硬件底层的实现都是不一样的,没有统一的说法
底层原理
添加volatile关键字以后,JVM底层在线程的工作内存计算完数据之后,会向CPU发送一条Lock为前缀的指令,该指令会让线程中工作内存的数据立即刷新到主内存中。通过MESI缓存一致性协议,其他线程同时会嗅探主内存中的数据,一旦发现数据被修改过了,会对工作内存的数据进行失效。这样的话,当其他线程再次要使用同一个变量的数据,发现自己工作内存中的数据已经失效了,此时就会重新从主内存中加载,就可以看到第一个线程更新的数据了。
happen-before原则
即规定了在某些条件下,不允许编译器、指令器对你写的代码进行指令重排,以此来保证有序性
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作,比如说在代码里有先对一个lock.lock(),lock.unlock(),lock.lock()
volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个volatile变量的读操作,volatile变量写,再是读,必须保证是先写,再读
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作,thread.start(),thread.interrupt()
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
ThreadLocal
为什么有内存泄漏问题?
底层代码ThreadLocalMap -> K-V,其中key是一个内部静态类继承了WeakReference
即弱引用,弱引用在gc的时候会被直接清理掉
导致有null-value这样的数据大量存在,占用内存空间导致内存泄漏
Java团队做了什么优化?
你在通过ThreadLocal , set、get、remove时,他会自动清理掉map里null为key的
确保不会有很多的null值引用了你的value造成内存的泄漏问题
平时使用ThreadLocal需要注意什么?
尽量避免在 ThradLocal长时间放入数据,不使用时最好及时进行remove,自己主动把数据删除
硬件级别MESI协议
重排
指定重排
javac静态编译器编译成.class文件时指令重排
JIT动态编译.class文件未机器码的时候指令重排
处理器执行指令时的无序处理,比如指令1 指令2 指令3执行顺序可能为1,3,2
内存重排序,当指令写入到硬件组件后(写缓存 高速缓存 无效队列)可能发生重排序
处理器的重排序
指令乱序放入到高速缓存中
猜测执行,比如if语句先不执行条件去执行满足条件里面的逻辑最后在执行条件
可能造成可见性问题的组件
寄存器
写缓冲器
高速缓存
处理可见性问题的操作
flush
将无效队列中invalid message刷新到高速缓存让数据无效强制从其他处理器的高速缓存/主内存中读取
refresh
强制将写缓冲区中数据刷新到高速缓存/主内存中
高速缓存底层的数据结构
拉链散列表多个bucket组成
index确定所在bucket
tag定位cache entry
offset当前缓存变量的偏移量
每个bucket挂多个cache entry
每个cache entry包含三部分
tag:当前缓存行指向主存中数据的地址值
cache line:缓存数据
flag:数据状态 s:共享 invalidate:无效 exclusive:独占式 Modify:修改
高速缓存
tag
cache line
flag
...
tag
cache line
flag
...
tag
cache line
flag
读写流程、原理
多个处理器高速缓存通过总线相连(存在问题:多个写操作阻塞 需要等待其他处理器ack)
给处理器01向总线发送read请求读取数据
总线从主存中读取数据给处理器01
如果数据被多个处理器共享则flag标识为s状态
当变量被修改时,处理器01会往总线发送一个invalidate message消息,等待其他处理器回复ack invalidate消息
所有处理器都返回ack后获取数据修改的独占锁,修改数据flag=exclusive,修改数据完成后为modify状态
此时其他处理器中数据为invalidate状态,其他处理器从处理器01的高速缓存或者主存中读取数据
原理图示意
优化后
优化多个写操作阻塞:
写数据不等待invalidate ack直接写入到写缓冲区中
其他处理器收到invalidate message后直接写入到无效队列中返回ack
处理器01嗅探到invalidate ack消息后从写缓冲区刷新数据到高速缓存中
原理图示意
线程池
为什么要使用线程池
线程的创建和销毁的代价很大
有效控制线程数量,避免创建过多线程
内部组成
线程管理器(ThreadPool)
这个就是负责创建、销毁线程池的
工作线程(PoolWorker)
就是线程池中的一个线程
工作任务(Task)
就是线程池中某个线程的业务代码实现
任务队列(TaskQueue)
这个是扔到线程池里的任务需要排队的队列
常见的线程池
SingleThreadExecutor
单线程池队列
里面就一个线程,然后慢慢去消费。
FixedThreadExecutor
固定数量线程池
根据你设定的线程数量执行,多出来的进入队列排队等待
比如说,线程池里面固定就100个线程,超过这个线程数就到队列里面去排队等待
适用于负载比较均衡的情况
CachedThreadExecutor
自动回收线程池
无论多少任务,根据你的需要任意的创建线程,最短的时间满足你
适用于存在高峰的情况 | ps : 容易崩掉 ,4核8GB的100个线程就差不多了,cpu负载可能就 70%/80%了
高峰过去后,大量线程处于空闲状态,等待60s就会被销毁掉了
ScheduleThreadExecutor
定时任务线程池
线程数量无限制,定时调度执行任务
各种组件源码中常用,比如eureka、rocketmq的心跳等等
常用API
Executor
代表线程池的接口,有个execute()方法,扔进去一个Runnable类型对象,就可以分配一个线程给你执行
ExecutorService
这是Executor的子接口,相当于是一个线程池的接口,有销毁线程池等方法
Executors
线程池的辅助工具类,辅助入口类,可以根据Executors快速创建你需要的线程池
ThreadPoolExecutor
这是ExecutorService的实现类,这才是正儿八经代表一个线程池的类
一般在Executors里创建线程池的时候,内部都是直接创建一个ThreadPoolExecutor的实例对象返回的,然后同时给设置了各种默认参数
Executor
源码
分支主题
实现
分支主题
核心参数配置
corePoolSize
线程池里的核心线程数量
maximumPoolSize
线程池里允许的最大线程数量
keepAliveTime
等待时间,corePoolSize外的线程等待时间大于这个值,则会被清理掉
unit
keepAliveTime的单位
workQueue
工作队列,当前运行的线程数 > corePoolSizes时,多出来的线程进入queue中等待
threadFactory
如果有新的线程需要创建时,就是由这个线程池来进行创建的
handle
线程数超过maximumPoolSize并且queue满了的时候,仍有线程进来所执行的策略
默认直接报错
启动原理示意图
分支主题
分布式与微服务
设计理念
1.可拓展性
2.可用性
3.弹性
4.独立自主
5.分散治理
6.故障隔离
7.自动配置
8.通过DevOps持续交付
2.可用性
3.弹性
4.独立自主
5.分散治理
6.故障隔离
7.自动配置
8.通过DevOps持续交付
分解模式
按业务能力分解
问题:
微服务就是让应用服务松散耦合,但是将应用程序分解成较小的部分还必须要在逻辑上实现。那我们如何将应用程序分解为小型服务呢?
解决方案:
一种策略就是按业务能力分解,业务能力是企业业务价值的体现。业务的功能取决于业务的类型。例如,保险公司的业务能力通常包括销售,市场营销,承保,理赔处理,开票,合规性等。每种业务能力都可以视为一种服务,但它面向的是业务而不是技术。
微服务就是让应用服务松散耦合,但是将应用程序分解成较小的部分还必须要在逻辑上实现。那我们如何将应用程序分解为小型服务呢?
解决方案:
一种策略就是按业务能力分解,业务能力是企业业务价值的体现。业务的功能取决于业务的类型。例如,保险公司的业务能力通常包括销售,市场营销,承保,理赔处理,开票,合规性等。每种业务能力都可以视为一种服务,但它面向的是业务而不是技术。
按子域划分
问题:
按业务功能来分解应用程序或许是个不错的思路。但是我们可能会遇到某些比较难以分解出来的类(God Classes),这种类在多种服务中通用。比如,订单类用于“订单管理”,“接单”,“订单交付”等业务中。那我们该如何来分解呢?
解决方案:
对于这种难以分解出来的(God Classes)类,使用DDD(即领域驱动设计)可以解决。它使用子域和有界上下文概念来解决此问题。DDD将为企业创建的整个域模型分解为子域。每个子域都有一个模型,该模型的范围称为有界上下文。每个微服务将围绕有界的上下文进行开发。
注意:确定子域并不是件容易的事,这需要对业务有一定的了解。像业务功能一样,通过分析业务及其组织结构并确定不同的专业领域来标识子域。
按业务功能来分解应用程序或许是个不错的思路。但是我们可能会遇到某些比较难以分解出来的类(God Classes),这种类在多种服务中通用。比如,订单类用于“订单管理”,“接单”,“订单交付”等业务中。那我们该如何来分解呢?
解决方案:
对于这种难以分解出来的(God Classes)类,使用DDD(即领域驱动设计)可以解决。它使用子域和有界上下文概念来解决此问题。DDD将为企业创建的整个域模型分解为子域。每个子域都有一个模型,该模型的范围称为有界上下文。每个微服务将围绕有界的上下文进行开发。
注意:确定子域并不是件容易的事,这需要对业务有一定的了解。像业务功能一样,通过分析业务及其组织结构并确定不同的专业领域来标识子域。
扼杀者模式
问题:
到目前为止,我们所讨论的设计模式都是分解未开发的应用程序,但是我们所做的工作中有80%是用于已开发的应用程序(brownfield applications)中,这是个大型的整体应用程序。上述所有设计模式并不是适用于它们,因为把它们作为一个整体应用的同时将它们拆分成一个个较小的部分是一项艰巨的任务。
解决方案:
扼杀者模式可以解决此类问题。扼杀者模式是以缠绕类的藤蔓植物作为类比。该解决方案是与Web应用程序配合使用,在Web应用程序之间来回调用,对于每个URL的调用,一个服务可以分为不同的域并作为单独的服务托管。这个想法是一次做一个域,这将会创建两个单独的应用程序,它们并行存在于同一个URL空间中。最终,新重构的应用程序会“扼杀”或者替换原来的应用程序,直到最后可以停止整个应用程序。
到目前为止,我们所讨论的设计模式都是分解未开发的应用程序,但是我们所做的工作中有80%是用于已开发的应用程序(brownfield applications)中,这是个大型的整体应用程序。上述所有设计模式并不是适用于它们,因为把它们作为一个整体应用的同时将它们拆分成一个个较小的部分是一项艰巨的任务。
解决方案:
扼杀者模式可以解决此类问题。扼杀者模式是以缠绕类的藤蔓植物作为类比。该解决方案是与Web应用程序配合使用,在Web应用程序之间来回调用,对于每个URL的调用,一个服务可以分为不同的域并作为单独的服务托管。这个想法是一次做一个域,这将会创建两个单独的应用程序,它们并行存在于同一个URL空间中。最终,新重构的应用程序会“扼杀”或者替换原来的应用程序,直到最后可以停止整个应用程序。
领域驱动设计
CAP
属性: CAP 三个字母分别代表了分布式系统中三个相互矛盾的属性
● Consistency (一致性):CAP 理论中的副本一致性特指强一致性(1.3.4 );
● Availiablity(可用性):指系统在出现异常时已经可以提供服务;
● Tolerance to the partition of network (分区容忍):指系统可以对网络分区(1.1.4.2 )这种异常情 况进行容错处理;
● Availiablity(可用性):指系统在出现异常时已经可以提供服务;
● Tolerance to the partition of network (分区容忍):指系统可以对网络分区(1.1.4.2 )这种异常情 况进行容错处理;
CAP 理论指出:无法设计一种分布式协议,使得同时完全具备CAP 三个属性,即
1)该种协议下的副本始终是强一致性,
2)服务始终是可用的,
3)协议可以容忍任何网络分区异常;
分布式系统协议只能在CAP 这三者间所有折中。
1)该种协议下的副本始终是强一致性,
2)服务始终是可用的,
3)协议可以容忍任何网络分区异常;
分布式系统协议只能在CAP 这三者间所有折中。
热力学第二定律说明了永动机是不可能存在的,不要去妄图设计永动机。与之类似,CAP 理论的意义就在于明确提出了不要去妄图设计一种对CAP 三大属性都完全拥有的完美系统,因为这种系统在理论上就已经被证明不存在。
要么就是AP,要么就是CP;思考下为什么
集群与分布式
集群是个物理形态,分布式是个工作方式。
集群一般是物理集中、统一管理的,而分布式系统则不强调这一点。
集群可能运行着一个或多个分布式系统,也可能根本没有运行分布式系统;分布式系统可能运行在一个集群上,也可能运行在不属于一个集群的多台(2 台也算多台)机器上
分布式是相对中心化而来,强调的是任务在多个物理隔离的节点上进行。中心化带来的主要问题是可靠性,若中心节点宕机则整个系统不可用,分布式除了解决部分中心化问题,也倾向于分散负载,但分布式会带来很多的其他问题,最主要的就是一致性。
集群就是逻辑上处理同一任务的机器集合,可以属于同一机房,也可分属不同的机房。分布式这个概念可以运行在某个集群里面,某个集群也可作为分布式概念的一个节点。
分布式是指将不同的业务分布在不同的地方。而集群指的是将几台服务器集中在一起,实现同一业务。
分布式是以缩短单个任务的执行时间来提升效率的,而集群则是通过提高单位时间内执行的任务数来提升效率。
集群一般是物理集中、统一管理的,而分布式系统则不强调这一点。
集群可能运行着一个或多个分布式系统,也可能根本没有运行分布式系统;分布式系统可能运行在一个集群上,也可能运行在不属于一个集群的多台(2 台也算多台)机器上
分布式是相对中心化而来,强调的是任务在多个物理隔离的节点上进行。中心化带来的主要问题是可靠性,若中心节点宕机则整个系统不可用,分布式除了解决部分中心化问题,也倾向于分散负载,但分布式会带来很多的其他问题,最主要的就是一致性。
集群就是逻辑上处理同一任务的机器集合,可以属于同一机房,也可分属不同的机房。分布式这个概念可以运行在某个集群里面,某个集群也可作为分布式概念的一个节点。
分布式是指将不同的业务分布在不同的地方。而集群指的是将几台服务器集中在一起,实现同一业务。
分布式是以缩短单个任务的执行时间来提升效率的,而集群则是通过提高单位时间内执行的任务数来提升效率。
一句话,就是:“分头做事” 与 “一堆人” 的区别:分布式是指将不同的业务分布在不同的地方。而集群指的是将几台服务器集中在一起,实现同一业务。
副本
副本(replica/copy)指在分布式系统中为数据或服务提供的冗余。对于数据副本指在不同的节点上持久化同一份数据,当出现某一个节点的存储的数据丢失时,可以从副本上读到数据。数据副本是分布式系统解决数据丢失异常的唯一手段。另一类副本是服务副本,指数个节点提供某种相同的服务,这种服务一般并不依赖于节点的本地存储,其所需数据一般来自其他节点。
副本协议是贯穿整个分布式系统的理论核心。
副本协议是贯穿整个分布式系统的理论核心。
一致性
1.强一致性(strong consistency):任何时刻任何用户或节点都可以读到最近一次成功更新的副本数据。强一致性是程度最高的一致性要求,也是实践中最难以实现的一致性。
2.单调一致性(monotonic consistency):任何时刻,任何用户一旦读到某个数据在某次更新后的值,这个用户不会再读到比这个值更旧的值。单调一致性是弱于强一致性却非常实用的一种一致性级别。因为通常来说,用户只关心从己方视角观察到的一致性,而不会关注其他用户的一致性情况。
3.会话一致性(session consistency):任何用户在某一次会话内一旦读到某个数据在某次更新后的值,这个用户在这次会话过程中不会再读到比这个值更旧的值。会话一致性通过引入会话的概念,在单调一致性的基础上进一步放松约束,会话一致性只保证单个用户单次会话内数据的单调修改,对于不同用户间的一致性和同一用户不同会话间的一致性没有保障。实践中有许多机制正好对应会话的概念,例如php 中的session 概念。
4.最终一致性(eventual consistency):最终一致性要求一旦更新成功,各个副本上的数据最终将达 到完全一致的状态,但达到完全一致状态所需要的时间不能保障。对于最终一致性系统而言,一个用户只要始终读取某一个副本的数据,则可以实现类似单调一致性的效果,但一旦用户更换读取的副本,则无法保障任何一致性。
5.弱一致性(week consistency):一旦某个更新成功,用户无法在一个确定时间内读到这次更新的值,且即使在某个副本上读到了新的值,也不能保证在其他副本上可以读到新的值。弱一致性系统一般很难在实际中使用,使用弱一致性系统需要应用方做更多的工作从而使得系统可用。
2.单调一致性(monotonic consistency):任何时刻,任何用户一旦读到某个数据在某次更新后的值,这个用户不会再读到比这个值更旧的值。单调一致性是弱于强一致性却非常实用的一种一致性级别。因为通常来说,用户只关心从己方视角观察到的一致性,而不会关注其他用户的一致性情况。
3.会话一致性(session consistency):任何用户在某一次会话内一旦读到某个数据在某次更新后的值,这个用户在这次会话过程中不会再读到比这个值更旧的值。会话一致性通过引入会话的概念,在单调一致性的基础上进一步放松约束,会话一致性只保证单个用户单次会话内数据的单调修改,对于不同用户间的一致性和同一用户不同会话间的一致性没有保障。实践中有许多机制正好对应会话的概念,例如php 中的session 概念。
4.最终一致性(eventual consistency):最终一致性要求一旦更新成功,各个副本上的数据最终将达 到完全一致的状态,但达到完全一致状态所需要的时间不能保障。对于最终一致性系统而言,一个用户只要始终读取某一个副本的数据,则可以实现类似单调一致性的效果,但一旦用户更换读取的副本,则无法保障任何一致性。
5.弱一致性(week consistency):一旦某个更新成功,用户无法在一个确定时间内读到这次更新的值,且即使在某个副本上读到了新的值,也不能保证在其他副本上可以读到新的值。弱一致性系统一般很难在实际中使用,使用弱一致性系统需要应用方做更多的工作从而使得系统可用。
衡量分布式系统的指标
1.性能:系统的吞吐能力,指系统在某一时间可以处理的数据总量,通常可以用系统每秒处理的总的数据量来衡量;系统的响应延迟,指系统完成某一功能需要使用的时间;系统的并发能力,指系统可以同时完成某一功能的能力,通常也用QPS(query per second)来衡量。上述三个性能指标往往会相互制约,追求高吞吐的系统,往往很难做到低延迟;系统平均响应时间较长时,也很难提高QPS。
2.可用性:系统的可用性(availability)指系统在面对各种异常时可以正确提供服务的能力。系统的可用性可以用系统停服务的时间与正常服务的时间的比例来衡量,也可以用某功能的失败次数与成功次数的比例来衡量。可用性是分布式的重要指标,衡量了系统的鲁棒性,是系统容错能力的体现。
3.可扩展性:系统的可扩展性(scalability)指分布式系统通过扩展集群机器规模提高系统性能(吞吐、延迟、并发)、存储容量、计算能力的特性。好的分布式系统总在追求“线性扩展性”,也就是使得系统的某一指标可以随着集群中的机器数量线性增长。
4.一致性:分布式系统为了提高可用性,总是不可避免的使用副本的机制,从而引发副本一致性的问题。越是强的一致的性模型,对于用户使用来说使用起来越简单。
2.可用性:系统的可用性(availability)指系统在面对各种异常时可以正确提供服务的能力。系统的可用性可以用系统停服务的时间与正常服务的时间的比例来衡量,也可以用某功能的失败次数与成功次数的比例来衡量。可用性是分布式的重要指标,衡量了系统的鲁棒性,是系统容错能力的体现。
3.可扩展性:系统的可扩展性(scalability)指分布式系统通过扩展集群机器规模提高系统性能(吞吐、延迟、并发)、存储容量、计算能力的特性。好的分布式系统总在追求“线性扩展性”,也就是使得系统的某一指标可以随着集群中的机器数量线性增长。
4.一致性:分布式系统为了提高可用性,总是不可避免的使用副本的机制,从而引发副本一致性的问题。越是强的一致的性模型,对于用户使用来说使用起来越简单。
数据分布方式
参考redis章节的:哈希、一致性哈性、带虚拟node的一致性哈希,redis-cluter就是采用的最后种来实现的额,有何优点?不知道的话滚去看那个章节
SpringCloud
SpringBoot
SpringBoot快速入门
SpringBoot与微服务之间的关系
环境搭建
集成springmvc
集成mybatis
整合日志
集成jsp
全局异常捕获
拦截器
springAop对参数拦截
拦截器实例
spring boot使用拦截器(以session校验为例)
过滤器
过滤器注解
@Filter中FilterType包含的类型及意义
过滤器实例
spring boot使用过滤器(以session校验为例)
打包部署
热部署
集成Swagger2构建API管理体系
spring-boot | 多线程并发定时任务
进阶
核心组件
starter
actuator
auto-configuration
cli
性能优化
jvm参数
扫包优化
undertow容器
jta+atomikos分布式事务
SpringBoot核心原理实战
@SpringBootApplication源码解读
纯手写打造SpringBoot雏形
框架整合业务逻辑层
整合jsp视图
内置集成tomcat
常见轮子
1.为什么需要微服务
微服务架构和单体架构
单体架构与微服务的优缺点
单体架构
1.复杂性扩散
2.库的复用与耦合
3.可替代成本较高
微服务
单一职责功能,每个服务都很简单
易于规模化开发,单独团队维护、工作分明,职责清晰
改善故障隔离。一个服务宕机不会影响其他服务
架构上系统更加清晰
微服务架构概念
微服务的4个设计原则和19个解决方案
springCloud和springBoot的关系
微服务与分布式架构的区别
分布式服务架构与微服务架构概念的区别与联系是怎样的?
分布式
不同模块部署在不同服务器上
作用:分布式解决网站高并发带来问题
集群
多台服务器部署相同应用构成一个集群
作用:通过负载均衡设备共同对外提供服务
SOA[组装服务/ESB企业服务总线]
业务系统分解为多个组件,让每个组件都独立提供离散,自治,可复用的服务能力
通过服务的组合和编排来实现上层的业务流程
作用:简化维护,降低整体风险,伸缩灵活
微服务[找到服务/微服务网关open API]
架构设计概念,各服务间隔离(分布式也是隔离),自治(分布式依赖整体组合)其它特性(单一职责,边界,异步通信,独立部署)是分布式概念的跟严格执行
SOA到微服务架构的演进过程
作用:各服务可独立应用,组合服务也可系统应用(巨石应用[monolith]的简化实现策略-平台思想)
springCloud相关认识
springboot+springcloud相关面试题
SpringBoot整合SpringCloud
SpringBoot2.0.3整合SpringCloud
@SpringCloudApplication和@SpringBootApplication的区别
2.ResultFul
简介
webService
由于使用的SOAP协议,使用WSDL,这本质上是使用的XML进行内容通信,速度太慢,处理的效率太低
如果想使用本地接口的方式调用,要利用开发工具根据WSDL文件生成很多工具代码,接口任何变动都回导致工具代码重新生成,开发特别繁琐
RestFul
基于webService而来
项目搭建
SpringCloud系列二:Restful 基础架构(搭建项目环境、创建 Dept 微服务、客户端调用微服务)
服务提供方
服务消息方
3.SpringSecurity
服务提供方安全配置
SpringCloud系列三:SpringSecurity 安全访问(配置安全验证、服务消费端处理、无状态 Session 配置、定义公共安全配置程序类)
服务消费方处理
业务抽取
4.Eureka服务注册与发现
服务注册与发现流程
SpringCloud系列四:Eureka 服务发现框架(定义 Eureka 服务端、Eureka 服务信息、Eureka 发现管理、Eureka 安全配置、Eureka-HA(高可用) 机制、Eureka 服务打包部署)
eureka服务器的搭建
服务提供方的注册
相关配置
Eureka安全机制
HA 高可用
打包部署
5.Ribbon负载均衡
Ribbon基本使用
SpringCloud系列五:Ribbon 负载均衡(Ribbon 基本使用、Ribbon 负载均衡、自定义 Ribbon 配置、禁用 Eureka 实现 Ribbon 调用)
负载均衡的实现
自定义Ribbon路由
全局路由
单服务路由
提供方的信息获取
脱离Eureka使用Ribbon
6.Feign接口服务
Feign基本使用
SpringCloud系列六:Feign接口转换调用服务(Feign 基本使用、Feign 相关配置)
相关配置
数据压缩
日志配置
7.Hystrix 熔断机制
服务的雪崩
SpringCloud系列七:Hystrix 熔断机制(Hystrix基本配置、服务降级、HystrixDashboard服务监控、Turbine聚合监控)
服务提供方熔断
服务消费方降级
HystrixDashboard
turbine
8.zuul路由
zuul基本使用
SpringCloud系列八:Zuul 路由访问(Zuul 的基本使用、Zuul 路由功能、zuul 过滤访问、Zuul 服务降级)
路由配置
过滤访问
安全访问
Feign集成
zuul降级
9.Config 分布式配置中心
架构流程
SpringCloud系列九:SpringCloudConfig 基础配置(SpringCloudConfig 的基本概念、配置 SpringCloudConfig 服务端、抓取配置文件信息、客户端使用 SpringCloudConfig 进行配置、单仓库目录匹配、应用仓库自动选择、仓库匹配模式)
github账号
配置中心搭建
客户端读取配置
配置中心实战
config高可用
自动刷新
SpringCloudBus简介
基于SpringCloudBus实现自动刷新
10.SpringCloudStream 消息驱动
基本概念
RabbitMq集成
Stream的生产者
Stream的消费者
自定义通道
分组
RoutingKey设置
11.SpringCloudSleuth 链路跟踪
基本概念
SpringCloud系列十二:SpringCloudSleuth(SpringCloudSleuth 简介、SpringCloudSleuth 基本配置、数据采集)
跟踪服务
客户端配置
数据持久化
rabbitmq收集器
mysql存储器
12.springCloud中常用技术
邮件发送
Springboot实现发送邮箱
springCloud定时邮件发送
13.springCloud中注解探索
TestApplication上的注解
TestApplication.java
springCloud本身的注解
springCloud常用注解解释
springcloud中常用的注解@
springcloud中常用注解
@Autowired和@Resource的区别
springCloud的各种注解
配置文件注解
@Configuration
@Configuration注解探索
Spring @Configuration注解
@Configuration的使用
@Configuration和@Bean的用法和理解
注解下的其他技术
BeanDefinitionRegistryPostProcessor注册bean
DubboRegistryBeansDefinitionRegistryPostProcessor.java
BeanDefinitionRegistryPostProcessor
实现动态添加到spring容器
BeanDefinitionRegistryPostProcessor
EnvironmentAware
environmentaware接口实现环境变量读取和属性对象的绑定
BeanDefinitionRegistryPostProcessor探索
Spring基础-BeanDefinitionRegistryPostProcessor实现动态添加到spring容器
BeanFactoryPostProcessor
改变bean的定义(BeanFactoryPostProcessor接口)
@EnableConfigurationProperties
@EnableConfigurationProperties注解探索
@ConfigurationProperties和@EnableConfigurationProperties配合使用
在Spring Boot中使用 @ConfigurationProperties 注解 @EnableConfigurationProperties
@ConditionalOnProperty
@ConditionalOnProperty注解探索
ConditionalOnProperty
@ConfigurationProperties
@ConfigurationProperties注解实例文件
DubboProperties.java
@ConfigurationProperties注解探索
@ConfigurationProperties 注解
DubboAutoConfiguration.java
@ConditionalOnClass
@ConditionalOnClass注解探索
@ConditionalOnClass的使用探索
@ConditionalOnMissingClass
@ConditionalOnMissingClass注解探索
@Primary
@Primary注解探索
spring @Primary-在spring中的使用
@ConditionalOnMissingBean
@ConditionalOnMissingBean注解探索
@ConditionalOnMissingBean注解源码分析与示例
配置是否初始化Bean的方法
@ConditionalOnBean
@ConditionalOnBean注解探索
@ConditionalOnBean、@ConditionalOnMissingBean注解源码分析与示例
RestTemplateAutoConfiguration.java
@AutoConfigureAfter
@AutoConfigureAfter注解探索
@AutoConfigureAfter注解解析
@NestedConfigurationProperty
@WebServlet
过滤器注册
@Activate
ActiveLimitForServerFilter.java
Dubbo SPI 之 @Activate注解使用和实现解析
java规范的注解
@PostConstruct和@PreConstruct注解
Java开发之@PostConstruct和@PreConstruct注解
14.springCloud监听器探索
SpringBoot-事件监听的4种实现方式
15.springCloud过滤器探索
过滤器注解
@Filter中FilterType包含的类型及意义
过滤器实例
spring boot使用过滤器(以session校验为例)
16.springCloud拦截器探索
springAop对参数拦截
拦截器实例
spring boot使用拦截器(以session校验为例)
17.springCloud整合框架
springCloud切换环境
第一种:在多个文件中配置
第一种方法在多个文件中配置
第二种:在一个文件中配置
在一个文件中配置
第三种:在pom文件中配置
springCloud整合Eureka
单体Eureka的整合
Server服务端源码
服务端主启动类
ServerApp.java
yml文件配置
application.yml
Client服务提供者源码
服务端主启动类
Police.java
PoliceController.java
PoliceServer.java
yml文件配置
application.yml
Client服务调用者源码
服务端主启动类
PersonServer.java
TestController.java
yml文件配置
application.yml
集群Eureka的整合
springCloud整合Ribbon
springCloud整合Feign (服务调用)
springCloud整合Hystrix
springCloud整合Zuul
springCloud整合dubbo教程
融合springCloud与dubbo无缝替换
Spring Cloud与Dubbo共存方案总结
Springboot dubbo的整合以及与springcloud fein的对比
springCloud整合Feign
Spring Cloud Feign使用详解
Zipkin实现服务调用链跟踪
利用SpringCloud Sleuth和Zipkin实现调用链跟踪(一)
项目源码(不理解)
分布式解决的问题
分布式session一致性
分布式全局Id生产方案
分布式事务问题解决
分布式任务调度平台
分布式配置中心
分布式锁方案解决
分布式日志收集系统
网站跨域问题解决
分布式限流方案
手写微服务事务解决框架
dubbo系统调用接口
dubbo源码探索
dubbo的工作原理
Dubbo的底层原理
Dubbo源代码实现一:切入Spring
Dubbo源代码实现二:服务调用的动态代理和负载均衡
dubbo的源码
Dubbo源码解析
Dubbo实现源码分析
深入理解dubbo之服务发布源码分析
深入理解dubbo之服务引用
Dubbo RPC源码解读
dubbo源码解析-服务暴露原理
dubbo服务端处理请求源码分析
dubbo的环境搭建
dubbo与spring整合
maven+springmvc+dubbo+zookeeper
dubbo与springboot整合
Springboot 整合 Dubbo/ZooKeeper 详解
Dubbo的使用探索
Dubbo学习小记
dubbo与spring cloud
cloud/dubbo对比
spring cloud和dubbo的区别
Dubbo架构向SpringCloud架构兼容,过渡
融合spring cloud与dubbo 无缝替换spring cloud微服务间调用协议
Spring Cloud+Dubbo对Feign进行RPC改造
dubbo的面试探索
dubbo的工作原理
dubbo支持哪些序列化协议
hessian的数据结构
为什么PB的效率是最高的
dubbo负载均衡策略和高可用策略都有哪些?动态代理策略呢?
dubbo四种负载均衡策略
dubbo的spi思想是什么
如何基于dubbo进行服务治理、服务降级、失败重试以及超时重试
分布式服务接口的幂等性如何设计(比如不能重复扣款)
分布式服务接口请求的顺序性如何保证
如何自己设计一个类似dubbo的rpc框架
分布式场景
高并发解决方案
动态资源和静态资源分离
CDN
负载均衡
分布式缓存
数据库读写分离或数据切分
服务分布式部署
并发编程
事务概念
事务与锁
分布式事务产生背景
X/OpenDTP事务模型
标准分布式事务
分布式事务解决方案
两阶段提交
BASE理论与柔性事务
TCC方案
TCC-Transaction 分布式事务 —项目实战
项目文件
tcc执行过程
tcc-transaction-http-order :商城服务,提供商品和商品订单逻辑。
tcc-transaction-http-capital :资金服务,提供账户余额逻辑。
tcc-transaction-http-redpacket :红包服务,提供红包余额逻辑。
补偿性方案
异步确保型与最大努力型
单点登陆方案
单点登陆的问题背景
页面跨域问题
Session跨域共享方案
session的扩展
分布式任务调度方案
Quartz调度的用法
Elastic-Job示例
分布式调度的疑难点
Quartz集群定制化分布式调度
RabbitMQ
AMQP协议概念含义
AMQP协议是一个高级抽象层消息通信协议,RabbitMQ是AMQP协议的实现
消息队列主要有两种形式的目的地
1.队列(queue):点对点的消息通信(point-point)
2.主题(topic):发布(publish)/订阅(subscribe)通信
消息中间件的特点
1.采用异步处理模式
2.发送者和接收者不必了解对方,只需确认消息.发送者和接受者不必同时在线
3.消息中间件存储存储发送,接收消息,可以看做是一个容器
4.使用消息中间件的好处
中间件的使用场景
MQ的使用场景
RabbitMQ的几种典型使用场景
MQ应用场景
项目中使用rabbitmq的原因
承保系统送承保信息到收付系统,之后再送给费管系统
mq使用中的缺点
为什么要是用消息队列以及消息队列的优缺点分析
消息中间件的对比
Kafka、RabbitMQ、RocketMQ等消息中间件的介绍和对比
Kafka、RabbitMQ、RocketMQ等消息中间件的对比
rabbitMq基础使用
使用rabbitmq原因
开源、性能优秀,稳定性保障
提供可靠性消息的投递模式(confirm)、返回模式(return)
与springAMQP完美的整合、api丰富
集群模式非常丰富,表达式配置,HA模式,镜像队列模型
保证数据不丢失的前提是做到高可靠性、可用性
rabbitmq的概念特点
Virtual host:虚拟地址,用于逻辑隔离;一个Virtual host里可以有多个Exchange和Queue,同一个Virtual host不能有重名的Exchange和Queue;
rabbitmq组件详解
rabbitmq channel参数详解
三种Exchange模式
Exchanges Queues中互相跳转
Rabbitmq消息持久化
生产者或者消费者断线重连
ACK 确认机制
消息任务分发与消息ACK确认机制
RabbitMQ入门教程(十二):消息确认Ack
中间件的使用问题
如何保证消息队列的高可用
如何保证消息队列的高可用啊?
如何保证消息不被重复消费
RabbitMQ保证不消费重复数据
insert数据,给这个消息做唯一主键
做redis的set的操作
给消息分配一个全局id,将<id,message>以K-V形式写入redis
如何保证消息的可靠性传输
如何保证消费的可靠性传输
生产者丢数据
写消息的过程中,消息没有到达rabbitmq在网络传输过程中就丢了
confirm模式,重发
消息队列丢数据
rabbitmq接到消息后先暂存到自己的内存里,结果消费者还没来得及消费,rabbitmq挂掉了
处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。
消费者丢数据
消费者丢数据一般是因为采用了自动确认消息模式。至于解决方案,采用手动确认消息即可。
如何保证消息的顺序性
拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点
就一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理
几百万消息在消息队列里积压了几个小时
如何写一个消息队列架构设计
rabbitmq实战实例
rabbitmq HelloWorld实例
保单成本平台子任务
生产者
准备文件
车险保单.txt
车险批单.txt
配置文件policyBatches-message-producer.xml
policyBatches-message-producer.xml
PolicyBatchesMsgSenderImpl
PolicyBatchesMsgSenderImpl.java
TestPolicyBatchesSenderImpl
TestPolicyBatchesSenderImpl.java
消费者
配置文件policyBatches-message-consumer.xml
policyBatches-message-consumer.xml
PolicyBatchesMsgConsumerPolicyImpl
PolicyBatchesMsgConsumerPolicyImpl.java
PolicyBatchesMsgConsumerEndorImpl
PolicyBatchesMsgConsumerEndorImpl.java
TestPolicyBatchesConsumerImpl
TestPolicyBatchesConsumerImpl.java
spring boot中的rabbitMq配置
sprinboot集成rabbitmq教程
Spring Boot系列十三 Spring Boot集成RabbitMQ
Spring Boot系列十五 spring boot集成RabbitMQ 源码分析
RocketMQ
rocketmq学习教材
RocketMQ用户指南v3.2.4.pdf
rocketmq学习链接
rocketmq在windows、linux、docker的安装
Windows环境安装与配置RocketMQ
RocketMQ-Console安装、使用详解
springboot的RocketMq实例
Rocketmq - 1 介绍和核心概念
Rocketmq - 2 集群架构模型和部署结构图
Rocketmq - 3 部署双主环境
Rocketmq - 4 Broker配置文件,存储和HelloWorld
RocketMQ - 5 源码模块分析
RocketMQ - 6 生产者,顺序消息
RocketMQ - 8 消费者 push和pull模式,配置参数
rocketmq - 9 消息重试,消息幂等去重,消息模式
rocketmq - 10 双主双从模式
rocketmq队列
RocketMQ-延迟队列
RocketMQ-死信队列
RocketMQ-重试队列
rocketmq学习教程
黑马RocketMQ大纲
RocketMQ-01
RocketMQ-02
RocketMQ-03
rocketmq基本操作
rocketmq的优缺点
优点
1.解耦
2.削峰
3.数据分发
缺点
1.系统可用性降低
2.系统复杂度提高
重复消费
消息丢失
消息顺序
3.一致性问题
各个角色介绍
Producer:消息的发送者;举例:发信者
Consumer:消息接收者;举例:收信者
Broker:暂存和传输消息;举例:邮局
NameServer:管理Broker;举例:各个邮局的管理机构
Topic:区分消息的种类;一个发送者可以发送消息给一个或者多个Topic;一个消息的接收者可以订阅一个或者多个Topic消息
Message Queue:相当于是Topic的分区;用于并行发送和接收消息
双主双从集群搭建
消息生产发送
同步消息生产
异步消息生产
单向消息生产
负载均衡消费
广播模式消费
顺序消息模式
顺序消息生产
顺序消息消费
延时消息模式
延时消息生产
延时消息消费
发送批量消息
过滤消息模式
过滤消息生产
过滤消息消费
事务消息模式
事务消息生产
事务消息消费
rocketmq实战商城
业务分析
模拟电商网站购物场景中的【下单】和【支付】业务
下单业务(订单系统)
下单流程分析图
下单组件图.png
下单时序图.png
下单业务解决思路
失败补偿机制
校验订单
生成预订单
生成预订单.png
调用服务
扣减库存(库存服务)
扣减优惠券(优惠券服务)
扣减余额(用户服务)
确认订单
确认成功
确认失败
下单失败
发送订单确认失败MQ消息
回退库存
回退库存幂等性处理
数据库乐观锁
回退优惠券
回退余额
取消订单
支付业务
支付流程分析图
支付组件图.png
支付流程图.png
创建支付订单
创建支付订单.png
支付回调
支付后回调.png
rocketmq高级功能
消息的存储
mq消息生产消费流程
消息存储方式.png
消息生成者发送消息
MQ收到消息,将消息进行持久化,在存储中新增一条记录
返回ACK给生产者
MQ push 消息给对应的消费者,然后等待消费者返回ACK
如果消息消费者在指定时间内成功返回ack,那么MQ认为消息消费成功
MQ删除消息
性能对比
文件系统>关系型数据库DB
顺序写的过程,保证当前写的速度
消息存储结构
RocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成 的
刷盘机制
RocketMQ的消息是存储到磁盘上的,这样既能保证断电后恢复, 又可以让存储的消息量超出内存的限制。
同步刷盘
异步刷盘
高可用机制
RocketMQ角色.jpg
rocketmq分布式集群
RocketMQ分布式集群是通过Master和Slave的配合达到高可用性的。
Master和Slave的区别
在Broker的配置文件中,参数 brokerId的值为0表明这个Broker是Master,大于0表明这个Broker是 Slave;
同时brokerRole参数也会说明这个Broker是Master还是Slave;
Master角色的Broker支持读和写,Slave角色的Broker仅支持读,也就是 Producer只能和Master角色的Broker连接写入消息;
Consumer可以连接 Master角色的Broker,也可以连接Slave角色的Broker来读取消息。
消息消费高可用
在Consumer的配置文件中,并不需要设置是从Master读还是从Slave 读
当Master不可用或者繁忙的时候,Consumer会被自动切换到从Slave 读。
当一个Master角色的机器出现故障后,Consumer仍然可以从Slave读取消息,不影响Consumer程序
消息发送高可用
消息发送高可用设计.jpg
Topic的多个Message Queue创建在多个Broker组上(相同Broker名称,不同 brokerId的机器组成一个Broker组)
当一个Broker组的Master不可 用后,其他组的Master仍然可用,Producer仍然可以发送消息
RocketMQ目前还不支持把Slave自动转成Master
如果机器资源不足, 需要把Slave转成Master,则要手动停止Slave角色的Broker,更改配置文 件,用新的配置文件启动Broker。
负载均衡机制
Producer负载均衡
producer负载均衡.png
Producer端,每个实例在发消息的时候,默认会轮询所有的message queue发送,以达到让消息平均落在不同的queue上。
由于queue可以散落在不同的broker,所以消息就发送到不同的broker下
Consumer负载均衡
集群模式
广播模式
失败消息重试
顺序消息失败的重试
消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒)
应用会出现消息消费被阻塞的情况
在使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生
无序消息失败的重试
无序消息的重试只针对集群消费方式生效;
广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。
死信队列处理
当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试;
达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息
消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。
无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。
死信特性
死信消息特性
不会再被消费者正常消费。
有效期与正常消息相同,均为 3 天,3 天后会被自动删除。因此,请在死信消息产生后的 3 天内及时处理
死信队列特性
一个死信队列对应一个 Group ID, 而不是对应单个消费者实例
如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列
一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic
死信处理
某些因素导致消费者无法正常消费该消息,需要排查可疑因素并解决问题
在消息队列 RocketMQ 控制台重新发送该消息,让消费者重新消费一次
消费幂等处理
消息队列 RocketMQ 消费者在接收到消息以后,有必要根据业务上的唯一 Key 对消息做幂等处理的必要性
消费幂等的必要性
发送时消息重复
服务端对客户端应答失败
投递时消息重复
客户端给服务端反馈应答的时候网络闪断
负载均衡时消息重复
消息队列 RocketMQ 的 Broker 或客户端重启、扩容或缩容
处理方式
真正安全的幂等处理,不建议以 Message ID 作为处理依据
最好的方式是以业务唯一标识作为幂等处理的关键依据
rocketmq解决问题
幂等性,RocketMQ解决消息顺序和重复
RocketMQ—顺序消息和重复消息
WebSocket的使用
websocket原理
看完让你彻底搞懂Websocket原理
使用 HTML5 WebSocket 构建实时 Web 应用
websocket实战
Springboot整合Websocket案例(后端向前端主动推送消息)
Java Websocket实例【项目实战系列】
fastdfs服务器
fastdfs服务器简介
fastdfs服务器作用
FastDFS是一个开源的轻量级分布式文件系统,主要是使用它对文件进行管理
主要用到的功能是:文件存储、文件同步、文件访问(文件上传、文件下载)等
fastdfs解决了大容量存储和负载均衡的需求问题
特别适合以文件为载体的在线服务,如相册网站、视频网站等
fastdfs服务器优势
FastDFS充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标
使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务
fastdfs服务器安装
docker安装fastdfs
docker+fastdfs+springboot一键式搭建分布式文件服务器
快速在docker里面FastDFS安装单机版+nginx
使用Docker一键搭建FastDFS+Nginx分布式文件服务器
拉取镜像并启动
docker run -d --restart=always --privileged=true --net=host --name=fastdfs -e IP=192.168.25.134 -e WEB_PORT=80 -v ${HOME}/fastdfs:/var/local/fdfs registry.cn-beijing.aliyuncs.com/tianzuo/fastdfs
测试是否安装成功
docker exec -it fastdfs /bin/bash
echo "Hello FastDFS!">index.html
fdfs_test /etc/fdfs/client.conf upload index.html
http://192.168.25.134/group1/M00/00/00/wKgZhl7psfaAckipAAAADwL5vO455.html
fastdfs服务器原理
FastDFS由跟踪服务器(Tracker Server)、存储服务器(Storage Server)和客户端(Client)构成
Tracker server 追踪服务器
追踪服务器负责接收客户端的请求,选择合适的组合storage server
tracker server 与 storage server之间也会用心跳机制来检测对方是否活着
Tracker需要管理的信息也都放在内存中,并且里面所有的Tracker都是对等的(每个节点地位相等),很容易扩展
客户端访问集群的时候会随机分配一个Tracker来和客户端交互
Storage server 储存服务器
实际存储数据,分成若干个组(group),实际上traker就是管理的storage中的组
组内机器中则存储数据,group可以隔离不同应用的数据,不同的应用的数据放在不同group里面
优点
海量的存储:主从型分布式存储,存储空间方便拓展
fastDFS对文件内容做hash处理,避免出现重复文件
fastDFS结合Nginx集成, 提供网站效率
客户端应用Client服务器
本地的项目所部署在的服务器
fastdfs上传下载文件流程
fastdfs上传文件流程
fastdfs上传文件流程.png
写操作的时候,storage会将他所挂载的所有数据存储目录的底下都创建2级子目录,每一级256个总共65536个
新写的文件会以hash的方式被路由到其中某个子目录下,然后将文件数据作为本地文件存储到该目录中
fastdfs下载文件流程
fastdfs下载文件流程.png
当客户端向Tracker发起下载请求时,并不会直接下载,而是先查询storage server(检测同步状态),返回storage server的ip和端口
然后客户端会带着文件信息(组名,路径,文件名),去访问相关的storage,下载文件
fastdfs服务器实践
微服务粒度划分
单体设计优先
Yagni (You aren't gonna need it)
设计刚刚好的系统
过犹不及
过度设计
敏捷
系统边界识别
![dropped image link](https://martinfowler.com/bliki/images/microservice-verdict/path.png)
在微服务中重构比在单体应用中重构成本高多了
在单体应用中,随着用户的反馈,系统的维护 有利于识别良好的、稳定的系统边界
几种演进方式
良好架构单体应用优先
单体应用组件分离微服务
粗粒度服务,之后再拆分
微服务技术栈的管理
不限死使用的语言和框架, 但是也不允许完全的灵活性
语言和框架
前端
Vuejs
AngularJS
后端
nodejs
typescript
hapi
ruby
grape
java
jersery
spring boot
微服务化的一个好处 :smiley: 就是: 根据工作的不同来选用合理的工具
划分微服务的注意事项
领域驱动的划分
按照业务来组织工作
如何划分微服务?
按照领域模型划分
新项目不建议直接使用微服务
你很可能并不真的理解业务领域, 从而也很难理解各个服务的边界
不能为了微服务而微服务,微服务本身也是有成本的, 如果成本大于收益,得不偿失
微服务架构提倡通过对特定业务领域的分析与建模,将复杂的、 集中的、耦合度高的应用系统分解成**小而专、耦合度低并且高度自治**的一组服务
微服务的“微”并不是一个真正可衡量、看得见、摸得着的“微”。这个“微”所表达的,是一种设计思想和指导方针,是需要团队或者组织共同努力找到的一个平衡点
:!!: **业务独立性**和**团队自主性**。首先,应该保证微服务是具有业务独立性的单元,在这个前提下,由团队来判断当前的服务大小是否合适,考虑到团队的沟通成本,一般不建议超过10个人,或者在超过10个人的团队中,可以再划分子团队。在这种情况下,当团队中大部分成员认为当前的服务是能够容易维护的、容易理解的,这就是我们认为适合团队的、有意义的“微”。
演进式设计
微服务可能出现来回往复的拆分和合并,知道开发人员真正理解了服务的边界应该是什么
根据业务的实际情况作调整, 而不是守着架构一成不变
大多数软件架构的腐化都发生在维护期, 微服务也不是银弹,无法解决这个问题, 因此长期维护的项目架构必须演进 :recycle:
颗粒度控制在什么程度?
职责的单一性
而不是代码的多少
我们的疑问?
需求大小?
代码?
职责?
![防止服务循环依赖](http://jbcdn2.b0.upaiyun.com/2016/01/d71254948a7a4b96c0bd9490e2a71fc0.png)
收集的资料
http://wldandan.github.io/blog/categories/microservices/
https://martinfowler.com/microservices/
http://insights.thoughtworkers.org/evolutionary-architecture-micro-services/
https://martinfowler.com/bliki/MonolithFirst.html
http://www.jiagoushuo.com/article/1000532.html
https://coggle.it/diagram/WPRq45VczAABUIGl/t/%E5%BE%AE%E6%9C%8D%E5%8A%A1%E7%B2%92%E5%BA%A6%E5%88%92%E5%88%86
web服务器
nginx
反向代理
反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。
简单来说就是真实的服务器不能直接被外部网络访问,所以需要一台代理服务器,而代理服务器能被外部网络访问的同时又跟真实服务器在同一个网络环境,当然也可能是同一台服务器,端口不同而已。
简单来说就是真实的服务器不能直接被外部网络访问,所以需要一台代理服务器,而代理服务器能被外部网络访问的同时又跟真实服务器在同一个网络环境,当然也可能是同一台服务器,端口不同而已。
负载均衡
当有2台或以上服务器时,根据规则随机的将请求分发到指定的服务器上处理,负载均衡配置一般都需要同时配置反向代理,通过反向代理跳转到负载均衡。而Nginx目前支持自带3种负载均衡策略,还有2种常用的第三方策略。
策略
RR(默认)
每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
权重
指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。
ip_hash
iphash的每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。
fair(第三方)
按后端服务器的响应时间来分配请求,响应时间短的优先分配。
url_hash(第三方)
按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。
HTTP服务器(包含动静分离)
子主题
正向代理
子主题
Nginx
代理
负载均衡
Https配置
正反向代理的区别
什么是正向代理
正向代理类似一个跳板机,代理访问外部资源
举例说明
客户端必须设置正向代理服务器,当然前提是要知道正向代理服务器的IP地址,还有代理程序的端口。
正向代理总结
正向代理 是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器)
然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端必须要进行一些特别的设置才能使用正向代理。
正向代理的用途
(1)访问原来无法访问的资源,如google
(2) 可以做缓存,加速访问资源
(3)对客户端访问授权,上网进行认证
(4)代理可以记录用户访问记录(上网行为管理),对外隐藏用户信息
什么是反向代理
反向代理总结
初次接触方向代理的感觉是,客户端是无感知代理的存在的,反向代理对外都是透明的,
访问者者并不知道自己访问的是一个代理。因为客户端不需要任何配置就可以访问。
反向代理过程
反向代理(Reverse Proxy)实际运行方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器
从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器。
反向代理作用
(1)保证内网的安全,可以使用反向代理提供WAF功能,阻止web攻击
(2)负载均衡,通过反向代理服务器来优化网站的负载
nginx安装与命令
常用服务器介绍
Nginx教程(7) 正向代理与反向代理【总结】
nginx安装及常见问题
无法访问nginx,要关闭防火墙
目录结构
Conf
配置文件
Html
网页文件
Logs
日志文件
Sbin
二进制程序
常用命令
nginx开启关闭和状态
linux nginx启动重启关闭命令
ps aux|grep nginx 和ps -ef|grep nginx一样
关闭nginx (./sbin/nginx -s stop)
启动停止命令
./nginx -c nginx.conf
如果不指定,默认为NGINX_HOME/conf/nginx.conf
./nginx -s stop
停止
./nginx -s quit
退出
./nginx -s reload
重新加载nginx.conf
发送信号的方式
kill -QUIT 进程号
安全停止
kill -TERM 进程号
立即停止
windows下的nginx
使用nginx+tomcat实现集群
Windows下Nginx的启动、停止等命令
window版nginx部署实践
安装java的环境
卸载与安装jdk1.8
安装mysql数据库
安装tomcat服务器
nginx的安装与配置
Nginx Linux详细安装部署教程
编译安装nginx 1.8.1 及配置
nginx进程的模型
模型及基本概念
发送信号方式
nginx的信号量
nginx的配置结构
conf配置文件结构
配置文件图结构
配置文件结构
配置文件教程
main全局配置
#user nobody
#主模块命令, 指定Nginx的worker进程运行用户以及用户组,默认由nobody账号运行
worker_processes 1;
指定Nginx要开启的进程数。
#error_log logs/error.log;
#错误日志存放目录
#pid
logs/nginx.pid;
worker_rlimit_nofile 100000;
worker进程的最大打开文件数限制
worker_cpu_affinity 0001 0010 0100 1000 0001 00100100 1000;
cpu亲和力配置,让不同的进程使用不同的cpu
event配置
设定nginx的工作模式及连接数上限
use epoll;
use用来指定nginx的工作模式
worker_connections 1024;
设置nginx每个进程最大的连接数,默认是1024
http服务器
include mime.types;
文件扩展名与文件类型映射表
default_type application/octet-stream;
默认文件类型,当文件类型未定义时候就使用这类设置的。
log_format access '$remote_addr - $remote_user [$time_local] "$request" '
设置日志模式
设定请求缓存
server_names_hash_bucket_size 128;client_header_buffer_size 512k;large_client_header_buffers 4 512k;client_max_body_size 100m;
server_tokens off;
隐藏响应header和错误通知中的版本号
tcp_nopush on;
激活tcp_nopush参数可以允许把httpresponse header和文件的开始放在一个文件里发布,积极的作用是减少网络报文段的数量
tcp_nodelay on;
激活tcp_nodelay,内核会等待将更多的字节组成一个数据包,从而提高I/O性能
sendfile on
开启高效传输模式
keepalive_timeout 65;
长连接超时时间,单位是秒
upstream backend_server {server 10.254.244.20:81 weight=1 max_fails=2 fail_timeout=30s;}
upstream表示负载服务器池,定义名字为backend_server的服务器池
轮询
upstream webhost {server 192.168.0.5:6666 ;server 192.168.0.7:6666 ;}
权重
upstream webhost {server 192.168.0.5:6666 weight=2;server 192.168.0.7:6666 weight=3;}
ip_hash
upstream webhost {ip_hash;server 192.168.0.5:6666 ;server 192.168.0.7:6666 ;}
每个请求按访问IP的hash结果分配,这样来自同一个IP的访客固定访问一个后端服务器,有效解决了动态网页存在的session共享问题
url_hash
upstream webhost {server 192.168.0.5:6666 ;server 192.168.0.7:6666 ;hash $request_uri;}
此方法按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率
server虚拟主机
设置一个虚拟机主机,可以包含自己的全局快,同时也可以包含多个locating模块。
listen 80;
server的全局配置,配置监听的端口
server_name localhost;
本server的名称,当访问此名称的时候nginx会调用当前serevr内部的配置进程匹配。
index index.html index.htm index.php;
首页排序
root /data0/abc;
#站点根目录,即网站程序存放目录
error_page 500 502 404 /templates/kumi/phpcms/404.html;
错误页面
rewrite ^/list-([0-9]+)-([0-9]+)\.html$ /index.php?m=content
伪静态 将www.abc.com/list....html的文件转发到index.php。。。
access_log /var/log/nginx/access.log access;
定义本虚拟主机的访问日志
location ~ /.svn/ { deny all;}
location 标签,根目录下的.svn目录禁止访问
location /
location其实是server的一个指令,为nginx服务器提供比较多而且灵活的指令,都是在location中提现的
root html;
相当于默认页面的目录名称,默认是相对路径,可以使用绝对路径配置。
index index.html index.htm;
error_page 500 502 503 504 /50x.html;
#错误页面的文件名称
location = /50x.html
location处理对应的不同错误码的页面定义到/50x.html,这个跟对应其server中定义的目录下。
root html;
定义默认页面所在的目录
location配置语法
location的作用
根据用户请求的网站URL进行匹配,匹配成功即进行相关的操作
location的正则
正则表达式实例教程
nginx location 配置 正则表达式实例详解
“=”精确匹配,内容要同表达式完全一致才匹配成功
“~”大小写敏感
“~*”大小写忽略
“^~”只匹配以 uri 开头
“@”nginx内部跳转
不加任何规则
默认是大小写敏感,前缀匹配,相当于加了“~”与“^~”
匹配优先级
Location解析过程
if指令
return指令
内置变量
日志的配置及切割
Nginx日志格式
Nginx日志分隔
crontab设置作业
反向代理负载均衡
反向代理
DNS域名解析过程
DNS将域名解析为真实ip地址和端口号
查找浏览器DNS缓存
查找本地host文件(ip与域名的关系)
访问nginx服务器地址
从网络运营商获取对应的IP地址
反向代理的好处
反向代理的好处隐藏真实内部ip地址,请求先访问nginx代理服务器(外网可以访问到),在使用nginx服务器转发到真实服务器中
反向代理的配置
当客户端访问www.itmayiedu.com,监听端口号为80直接跳转到真实ip服务器地址 127.0.0.1:8081
外网映射工具
外网映射的作用
在做微信开发或者是对接第三方支付接口时,回调接口可能需要外网访问。
外网映射的工具
natapp、ngrok
反向代理架构
蚂蚁课堂架构.png
公网服务器
局域网服务器
反向代理跳转地址
抓包分析反向代理跳转地址
基础用法
负载均衡
负载均衡的作用
解决高并发,减少单台服务器的压力,拦截到请求,在采用负载均衡算法后,分配到不同的真实服务器上
服务器故障转移
负载均衡、故障转移、失败重试、容错、健康检查
当上游服务器(真实业务逻辑访问的服务器)发生故障时,可以转移到其他上游服务器
服务集群的问题
负载均衡的配置
upstream和location配置
负载均衡的方式
upstream表示负载服务器池,定义名字为backend_server的服务器池
url_hash
upstream webhost {server 192.168.0.5:6666 ;server 192.168.0.7:6666 ;hash $request_uri;}
此方法按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率
ip_hash
upstream webhost {ip_hash;server 192.168.0.5:6666 ;server 192.168.0.7:6666 ;}
每个请求按访问IP的hash结果分配,这样来自同一个IP的访客固定访问一个后端服务器,有效解决了动态网页存在的session共享问题
权重
upstream webhost {server 192.168.0.5:6666 weight=2;server 192.168.0.7:6666 weight=3;}
轮询
upstream webhost {server 192.168.0.5:6666 ;server 192.168.0.7:6666 ;}
负载均衡故障转移
当上游服务器(真实访问服务器),一旦出现故障或者是没有及时相应的话,应该直接轮训到下一台服务器,保证服务器的高可用
负载均衡的故障转移配置
proxy_connect_timeout 1s;
nginx与后端服务器连接的超时时间_发起握手等候响应超时时间
proxy_send_timeout 1s;
nginx发送给上游服务器(真实访问的服务器)超时时间
proxy_read_timeout 1s;
nginx接受上游服务器(真实访问的服务器)超时时间
静态网页的服务器
Nginx是一个HTTP服务器,可以将服务器上的静态文件(如HTML、图片)通过HTTP协议展现给客户端
URL的重写与配置
使用正则匹配请求的url,根据定义的规则进行重写和改变,需ngx_http_rewrite_module模块来支持url重写功能
可重写的功能
可重写变量和含义
重写的语法
判断IP地址来源
如果访问的ip地址为192.168.5.165,则返回403
限制浏览器访问
不允许谷歌浏览器访问 如果是谷歌浏览器返回500
URL重写场景
rewrite语法格式
regex 常用正则表达式
rewrite过程
nginx的动静分离
动静分离教程
【Nginx】实现动静分离
mvvm模式的交互
动静分离的两种方式
伪静态
动静分离实例
虚拟主机配置应用
nginx虚拟主机作用
将一台服务器,拆封多个网站部署
nginx虚拟主机配置
nginx 配置虚拟主机的三种方法
基于域名的虚拟主机
#当客户端访问www.itmayiedu.com,监听端口号为80,直接跳转到data/www目录下文件
#当客户端访问bbs.itmayiedu.com,监听端口号为80,直接跳转到data/bbs目录下文件
基于端口的虚拟主机
#当客户端访问www.itmayiedu.com,监听端口号为8080,直接跳转到data/www目录下文件
#当客户端访问www.itmayiedu.com,监听端口号为8081,直接跳转到data/bbs目录下文件
跨域问题网关配置
跨域的由来
跨域属于浏览器的问题,不是服务器的问题
跨域的解决方案
1.使用jsonp解决网站跨域
不支持post请求,代码书写比较复杂
2.使用HttpClient内部转发
前端Q向后端B发送请求,Q先请求后端A,后端A请求后端B,获取数据后响应前端Q
3.使用设置响应头允许跨域
response.setHeader("Access-Control-Allow-Origin", "*")
4.基于Nginx搭建企业级API接口网关
原理:保证域名和端口号是相同的,根据不同项目名称使用nginx转发到真实服务器地址
5.使用Zuul搭建微服务API接口网关
cors方案用法
简单请求与复杂请求
缓存及Gzip的配置
nginx的缓存配置教程
Nginx缓存原理及配置
静态资源缓存配置
资源压缩配置
nginx的https配置
https配置加强信息传输安全
nginx使用ssl模块配置支持HTTPS访问
信息传输安全概念
HTTPS简介
openssl生成证书
Nginx.conf配置证书
keepalived的配置
nginx高可用概述
安装Keepalived
配置抢占式模式
tomcat
架构图(图中的虚线表示一个请求在 Tomcat 中流转的过程)
子主题
连接器
Tomcat 的整体架构包含了两个核心组件连接器和容器。连接器负责对外交流,容器负责内部处理。连接器用 ProtocolHandler 接口来封装通信协议和 I/O模型的差异,ProtocolHandler 内部又分为 EndPoint 和 Processor 模块,EndPoint负责底层 Socket 通信,Proccesor 负责应用层协议解析。连接器通过适配器 Adapter调用容器。
对 Tomcat 整体架构的学习,我们可以得到一些设计复杂系统的基本思路。首先要分析需求,根据高内聚低耦合的原则确定子模块,然后找出子模块中的变化点和不变点,用接口和抽象基类去封装不变点,在抽象基类中定义模板方法,让子类自行实现抽象方法,也就是具体子类去实现变化点。
容器
运用了组合模式 管理容器、通过 观察者模式 发布启动事件达到解耦、开闭原则。骨架抽象类和模板方法抽象变与不变,变化的交给子类实现,从而实现代码复用,以及灵活的拓展。使用责任链的方式处理请求,比如记录日志等。
类加载器
Tomcat 的自定义类加载器 WebAppClassLoader 为了隔离 Web 应用打破了双亲委托机制,它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载 Web 应用自己定义的类。防止 Web 应用自己的类覆盖 JRE 的核心类,使用 ExtClassLoader 去加载,这样即打破了双亲委派,又能安全加载。
详见上面类加载器
Docker
Docker简介
前提知识+课程定位
是什么
问题:为什么会有docker出现
docker理念
一句话
解决了运行环境和配置问题软件容器,方便做持续集成并有助于整体发布的容器虚拟化技术。
能干嘛
之前的虚拟机技术
容器虚拟化技术
开发/运维(DevOps)
一次构建、随处运行
更快速的应用交付和部署
更便捷的升级和扩缩容
更简单的系统运维
更高效的计算资源利用
企业级
新浪
美团
蘑菇街
......
去哪下
官网
docker官网:http://www.docker.com
docker中文网站:https://www.docker-cn.com/
仓库
Docker Hub官网: https://hub.docker.com/
Docker安装
前提说明
Docker的基本组成
镜像(image)
容器(container)
仓库(repository)
小总结
安装步骤
CentOS6.8安装Docker
yum install -y epel-release
yum install -y docker-io
安装后的配置文件:/etc/sysconfig/docker
启动Docker后台服务:service docker start
docker version验证
CentOS7安装Docker
https://docs.docker.com/install/linux/docker-ce/centos/
安装步骤
官网中文安装参考手册
https://docs.docker-cn.com/engine/installation/linux/docker-ce/centos/#prerequisites
确定你是CentOS7及以上版本
cat /etc/redhat-release
yum安装gcc相关
CentOS7能上外网
分支主题
yum -y install gcc
yum -y install gcc-c++
卸载旧版本
yum -y remove docker docker-common docker-selinux docker-engine
2018.3官网版本
安装需要的软件包
yum install -y yum-utils device-mapper-persistent-data lvm2
设置stable镜像仓库
大坑
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
推荐
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
更新yum软件包索引
yum makecache fast
安装DOCKER CE
yum -y install docker-ce
启动docker
systemctl start docker
测试
docker version
docker run hello-world
配置镜像加速
mkdir -p /etc/docker
vim /etc/docker/daemon.json
systemctl daemon-reload
systemctl restart docker
卸载
systemctl stop docker
yum -y remove docker-ce
rm -rf /var/lib/docker
永远的HelloWorld
阿里云镜像加速
是什么
https://dev.aliyun.com/search.html
注册一个属于自己的阿里云账户(可复用淘宝账号)
获得加速器地址连接
登陆阿里云开发者平台
获取加速器地址
配置本机Docker运行镜像加速器
重新启动Docker后台服务:service docker restart
Linux 系统下配置完加速器需要检查是否生效
网易云加速
基本同上述阿里云
启动Docker后台容器(测试运行 hello-world)
docker run hello-world
run干了什么
底层原理
Docker是怎么工作的
为什么Docker比较比VM快
Docker常用命令
帮助命令
docker version
docker info
docker --help
镜像命令
docker images
列出本地主机上的镜像
OPTIONS说明:
-a :列出本地所有的镜像(含中间映像层)
-q :只显示镜像ID。
--digests :显示镜像的摘要信息
--no-trunc :显示完整的镜像信息
docker search 某个XXX镜像名字
网站
https://hub.docker.com
命令
docker search [OPTIONS] 镜像名字
OPTIONS说明:
--no-trunc : 显示完整的镜像描述
-s : 列出收藏数不小于指定值的镜像。
--automated : 只列出 automated build类型的镜像;
docker pull 某个XXX镜像名字
下载镜像
docker pull 镜像名字[:TAG]
docker rmi 某个XXX镜像名字ID
删除镜像
删除单个
docker rmi -f 镜像ID
删除多个
docker rmi -f 镜像名1:TAG 镜像名2:TAG
删除全部
docker rmi -f $(docker images -qa)
思考
结合我们Git的学习心得,大家猜猜是否会有
docker commit /docker push??
容器命令
有镜像才能创建容器,这是根本前提(下载一个CentOS镜像演示)
docker pull centos
新建并启动容器
docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
OPTIONS说明
启动交互式容器
列出当前所有正在运行的容器
docker ps [OPTIONS]
OPTIONS说明
退出容器
两种退出方式
exit
容器停止退出
ctrl+P+Q
容器不停止退出
启动容器
docker start 容器ID或者容器名
重启容器
docker restart 容器ID或者容器名
停止容器
docker stop 容器ID或者容器名
强制停止容器
docker kill 容器ID或者容器名
删除已停止的容器
docker rm 容器ID
一次性删除多个容器
docker rm -f $(docker ps -a -q)
docker ps -a -q | xargs docker rm
重要
启动守护式容器
docker run -d 容器名
查看容器日志
docker logs -f -t --tail 容器ID
* -t 是加入时间戳
* -f 跟随最新的日志打印
* --tail 数字 显示最后多少条
查看容器内运行的进程
docker top 容器ID
查看容器内部细节
docker inspect 容器ID
进入正在运行的容器并以命令行交互
docker exec -it 容器ID bashShell
重新进入docker attach 容器ID
上述两个区别
attach 直接进入容器启动命令的终端,不会启动新的进程
exec 是在容器中打开新的终端,并且可以启动新的进程
从容器内拷贝文件到主机上
docker cp 容器ID:容器内路径 目的主机路径
小总结
常用命令
Docker 镜像
是什么
UnionFS(联合文件系统)
Docker镜像加载原理
分层的镜像
为什么 Docker 镜像要采用这种分层结构呢
特点
Docker镜像都是只读的
当容器启动时,一个新的可写层被加载到镜像的顶部。
这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。
Docker镜像commit操作补充
docker commit提交容器副本使之成为一个新的镜像
docker commit -m=“提交的描述信息” -a=“作者” 容器ID 要创建的目标镜像名:[标签名]
案例演示
从Hub上下载tomcat镜像到本地并成功运行
docker run -it -p 8080:8080 tomcat
-p 主机端口:docker容器端口
-P 随机分配端口
i:交互
t:终端
故意删除上一步镜像生产tomcat容器的文档
也即当前的tomcat运行实例是一个没有文档内容的容器,
以它为模板commit一个没有doc的tomcat新镜像atguigu/tomcat02
启动我们的新镜像并和原来的对比
启动atguigu/tomcat02,它没有docs
新启动原来的tomcat,它有docs
Docker容器数据卷
是什么
一句话:有点类似我们Redis里面的rdb和aof文件
能干嘛
容器的持久化
容器间继承+共享数据
数据卷
容器内添加
直接命令添加
命令
docker run -it -v /宿主机绝对路径目录:/容器内目录 镜像名
查看数据卷是否挂载成功
容器和宿主机之间数据共享
容器停止退出后,主机修改后数据是否同步
命令(带权限)
docker run -it -v /宿主机绝对路径目录:/容器内目录:ro 镜像名
DockerFile添加
根目录下新建mydocker文件夹并进入
可在Dockerfile中使用VOLUME指令来给镜像添加一个或多个数据卷
File构建
build后生成镜像
获得一个新镜像zzyy/centos
run容器
通过上述步骤,容器内的卷目录地址已经知道
对应的主机目录地址哪??
主机对应默认地址
备注
数据卷容器
是什么
总体介绍
以上一步新建的镜像zzyy/centos为模板并运行容器dc01/dc02/dc03
它们已经具有容器卷
/dataVolumeContainer1
/dataVolumeContainer2
容器间传递共享(--volumes-from)
先启动一个父容器dc01
在dataVolumeContainer2新增内容
dc02/dc03继承自dc01
--volumes-from
命令
dc02/dc03分别在dataVolumeContainer2各自新增内容
回到dc01可以看到02/03各自添加的都能共享了
删除dc01,dc02修改后dc03可否访问
删除dc02后dc03可否访问
再进一步
新建dc04继承dc03后再删除dc03
结论:容器之间配置信息的传递,数据卷的生命周期一直持续到没有容器使用它为止
DockerFile解析
是什么
Dockerfile是用来构建Docker镜像的构建文件,是由一系列命令和参数构成的脚本。
构建三步骤
编写Dockerfile文件
docker build
docker run
文件什么样???
以我们熟悉的CentOS为例
https://hub.docker.com/_/centos/
DockerFile构建过程解析
Dockerfile内容基础知识
1:每条保留字指令都必须为大写字母且后面要跟随至少一个参数
2:指令按照从上到下,顺序执行
3:#表示注释
4:每条指令都会创建一个新的镜像层,并对镜像进行提交
Docker执行Dockerfile的大致流程
(1)docker从基础镜像运行一个容器
(2)执行一条指令并对容器作出修改
(3)执行类似docker commit的操作提交一个新的镜像层
(4)docker再基于刚提交的镜像运行一个新容器
(5)执行dockerfile中的下一条指令直到所有指令都执行完成
小总结
DockerFile体系结构(保留字指令)
FROM
基础镜像,当前新镜像是基于哪个镜像的
MAINTAINER
镜像维护者的姓名和邮箱地址
RUN
容器构建时需要运行的命令
EXPOSE
当前容器对外暴露出的端口
WORKDIR
指定在创建容器后,终端默认登陆的进来工作目录,一个落脚点
ENV
用来在构建镜像过程中设置环境变量
ADD
将宿主机目录下的文件拷贝进镜像且ADD命令会自动处理URL和解压tar压缩包
COPY
类似ADD,拷贝文件和目录到镜像中。
将从构建上下文目录中 的文件/目录复制到新的一层的镜像内的 位置
COPY src dest
COPY ["src", "dest"]
VOLUME
容器数据卷,用于数据保存和持久化工作
CMD
指定一个容器启动时要运行的命令
Dockerfile 中可以有多个 CMD 指令,但只有最后一个生效,CMD 会被 docker run 之后的参数替换
ENTRYPOINT
指定一个容器启动时要运行的命令
ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数
ONBUILD
当构建一个被继承的Dockerfile时运行命令,父镜像在被子继承后父镜像的onbuild被触发
小总结
案例
Base镜像(scratch)
Docker Hub 中 99% 的镜像都是通过在 base 镜像中安装和配置需要的软件构建出来的
自定义镜像mycentos
编写
Hub默认CentOS镜像什么情况
准备编写DockerFile文件
myCentOS内容DockerFile
构建
docker build -t 新镜像名字:TAG .
运行
docker run -it 新镜像名字:TAG
列出镜像的变更历史
docker history 镜像名
CMD/ENTRYPOINT 镜像案例
都是指定一个容器启动时要运行的命令
CMD
Dockerfile 中可以有多个 CMD 指令,但只有最后一个生效,CMD 会被 docker run 之后的参数替换
Case
tomcat的讲解演示
docker run -it -p 8888:8080 tomcat ls -l
ENTRYPOINT
docker run 之后的参数会被当做参数传递给 ENTRYPOINT,之后形成新的命令组合
Case
制作CMD版可以查询IP信息的容器
crul命令解释
问题
如果我们希望显示 HTTP 头信息,就需要加上 -i 参数
WHY
制作ENTROYPOINT版查询IP信息的容器
自定义镜像Tomcat9
mkdir -p /zzyyuse/mydockerfile/tomcat9
在上述目录下touch c.txt
将jdk和tomcat安装的压缩包拷贝进上一步目录
apache-tomcat-9.0.8.tar.gz
jdk-8u171-linux-x64.tar.gz
在/zzyyuse/mydockerfile/tomcat9目录下新建Dockerfile文件
目录内容
构建
构建完成
run
备注
验证
结合前述的容器卷将测试的web服务test发布
总体概述
web.xml
a.jsp
缓存
类别
静态缓存常指的是前端静态页面,html 啊,js等等,常放在静态服务器上,还能通过 CDN 来缩减响应的时间,提高用户访问速度。
分布式缓存常指的是利用 Redis 、Memcached 等分布式缓存中间件来存放一些较为常用的数据,多个应用共享缓存,不仅可以提高访问速率,也算上在高并发下起到保护脆弱的数据库作用,算是高并发利器了!
本地缓存常指的是应用在同一个进程中的缓存组件,交互之间不会有网络开销,当你的项目还用不上分布式缓存,就存一些简单的变量时候可以用本地缓存来解决。最简单的 HashMap 就能作为本地缓存,或者Ehcache、Guava Cache等。
哪些数据适合放入缓存中
● 即时性。例如查询最新的物流状态信息。
● 数据一致性要求不高。例如门店信息,修改后,数据库中已经改了,5分钟后缓存中才是最新的,但不影响功能使用。
● 访问量大且更新频率不高。比如首页的广告信息,访问量,但是不会经常变化。
● 数据一致性要求不高。例如门店信息,修改后,数据库中已经改了,5分钟后缓存中才是最新的,但不影响功能使用。
● 访问量大且更新频率不高。比如首页的广告信息,访问量,但是不会经常变化。
读写策略
并发更新导致的数据覆盖问题
其实不论是先删除再更新,还是先更新再删除,只是后删除出错的概率比较低!一般为了解决最终一致性问题都会设置过期时间,避免脏数据一直存在。
缓存击穿
key 在某个时间突然失效了,那是不是就意味着大量的请求就无法在缓存中获取数据了,而是去请求数据库了,这样很有可能导致数据库被击垮。这就是缓存击穿。
不设置过期时间、互斥锁更新
缓存穿透
缓存穿透意思就是某个不存在的key一直被访问,结果发现数据库中也没有这样的数据,最终导致访问该key的所有请求都直接请求到数据库了。
布隆过滤
缓存雪崩
在某个时间节点,大量的 key 失效,导致大量的请求从缓存中获取不到数据而去请求数据库。
缓存更新一致性问题
先删缓存,然后更新DB
并发会导致未更新DB时,其他线程读取旧数据填充缓存,导致不一致
先更新DB,然后删除缓存
删除缓存而不是更新缓存,原因是懒加载的思想,更新缓存代价可能极高,尤其是在读少写多的场景,见下面第4点
1、先更新DB,再删除缓存
DB更新成功,删除缓存失败或者还没有来得及删除,那么,其他线程从缓存中读取到的就是旧值,还是会发生不一致
2、引入消息队列,删缓存由消息队列来实现
如何保障消息不丢失,消息的延迟带来的短暂不一致性如何解决
3、mysql监听binlog消息的消息队列
解耦,没有侵入业务代码,这样消息延迟的问题依然存在,但是相比单纯引入消息队列的做法更好一点。
而且,如果并发不是特别高的话,这种做法的实时性和一致性都还算可以接受的。
4、为什么是删除,而不是更新缓存?
举个例子:如果数据库1小时内更新了1000次,那么缓存也要更新1000次,但是这个缓存可能在1小时内只被读取了1次,那么这1000次的更新有必要吗?
反过来,如果是删除的话,就算数据库更新了1000次,那么也只是做了1次缓存删除,只有当缓存真正被读取的时候才去数据库加载。
DB更新成功,删除缓存失败或者还没有来得及删除,那么,其他线程从缓存中读取到的就是旧值,还是会发生不一致
2、引入消息队列,删缓存由消息队列来实现
如何保障消息不丢失,消息的延迟带来的短暂不一致性如何解决
3、mysql监听binlog消息的消息队列
解耦,没有侵入业务代码,这样消息延迟的问题依然存在,但是相比单纯引入消息队列的做法更好一点。
而且,如果并发不是特别高的话,这种做法的实时性和一致性都还算可以接受的。
4、为什么是删除,而不是更新缓存?
举个例子:如果数据库1小时内更新了1000次,那么缓存也要更新1000次,但是这个缓存可能在1小时内只被读取了1次,那么这1000次的更新有必要吗?
反过来,如果是删除的话,就算数据库更新了1000次,那么也只是做了1次缓存删除,只有当缓存真正被读取的时候才去数据库加载。
redis
数据类型
string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)
内存
查看内存使用情况
『infomemory』
used_memory:已经使用了的内存大小。
used_memory_rss:redis物理内存的大小。
mem_fragmentation_ratio:内存碎片率。
used_memory:已经使用了的内存大小。
used_memory_rss:redis物理内存的大小。
mem_fragmentation_ratio:内存碎片率。
内存碎片率:mem_fragmentation_ratio = used_memory_rss / used_memory
一般保持在1~1.5之间是最合理的
什么是内存碎片
由于一块连续空闲的空间比所要申请的空间小,导致这块空间不可用,对于内存整体来说就是内存碎片
内存碎片导致的原因
修改、删除、新增都会导致,具体和申请的内存大小,实际使用大小相关
如何解决内存碎片
断电重启
空间置换
启动清理功能: activedefragyes
下面参数都是满足任一条件后就可以进行清理:
active-defrag-ignore-bytes100mb:
碎片达到100MB时,开启清理。
active-defrag-threshold-lower10:
当碎片超过10%时,开启清理。
active-defrag-threshold-upper100:
内存碎片超过100%,尽最大清理。
在处理的过程中,为了避免对正常请求的影响,同时又能保证性能。Redis同时还提供了监控CPU占用比例的参数,在满足以下条件时才会保证清理正常开展:
active-defrag-cycle-min5:
清理内存碎片占用CPU时间的比例不低于此值,保证清理能正常开展。
active-defrag-cycle-max75:
清理内存碎片占用CPU时间的比例不高于此值。一旦超过则停止清理,从而避免在清理时,大量的内存拷贝阻塞Redis,导致其它请求延迟。
redis过期策略
1、设置过期时间
expire key time(以秒为单位)--这是最常用的方式
setex(String key, int seconds, String value)--字符串独有的方式
expire key time(以秒为单位)--这是最常用的方式
setex(String key, int seconds, String value)--字符串独有的方式
三种过期策略
定时删除
含义:在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除
优点:保证内存被尽快释放
缺点:
若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key
定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重
没人用
含义:在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除
优点:保证内存被尽快释放
缺点:
若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key
定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重
没人用
子主题
惰性删除
含义:key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。
优点:删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的key了)
缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存)
定期删除
含义:每隔一段时间执行一次删除过期key操作
优点:
通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用--处理"定时删除"的缺点
定期删除过期key--处理"惰性删除"的缺点
缺点
在内存友好方面,不如"定时删除"
在CPU时间友好方面,不如"惰性删除"
难点
合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)(这个要根据服务器运行情况来定了)
Redis采用的过期策略
惰性删除+定期删除
惰性删除流程
在进行get或setnx等操作时,先检查key是否过期,
若过期,删除key,然后执行相应操作;
若没过期,直接执行相应操作
定期删除流程(简单而言,对指定个数个库的每一个库随机删除小于等于指定个数个过期key)
遍历每个数据库(就是redis.conf中配置的"database"数量,默认为16)
检查当前库中的指定个数个key(默认是每个库检查20个key,注意相当于该循环执行20次,循环体时下边的描述)
如果当前库中没有一个key设置了过期时间,直接执行下一个库的遍历
随机获取一个设置了过期时间的key,检查该key是否过期,如果过期,删除key
判断定期删除操作是否已经达到指定时长,若已经达到,直接退出定期删除。
惰性删除流程
在进行get或setnx等操作时,先检查key是否过期,
若过期,删除key,然后执行相应操作;
若没过期,直接执行相应操作
定期删除流程(简单而言,对指定个数个库的每一个库随机删除小于等于指定个数个过期key)
遍历每个数据库(就是redis.conf中配置的"database"数量,默认为16)
检查当前库中的指定个数个key(默认是每个库检查20个key,注意相当于该循环执行20次,循环体时下边的描述)
如果当前库中没有一个key设置了过期时间,直接执行下一个库的遍历
随机获取一个设置了过期时间的key,检查该key是否过期,如果过期,删除key
判断定期删除操作是否已经达到指定时长,若已经达到,直接退出定期删除。
RDB对过期key的处理
过期key对RDB没有任何影响
从内存数据库持久化数据到RDB文件
持久化key之前,会检查是否过期,过期的key不进入RDB文件
从RDB文件恢复数据到内存数据库
数据载入数据库之前,会对key先进行过期检查,如果过期,不导入数据库(主库情况)
从内存数据库持久化数据到RDB文件
持久化key之前,会检查是否过期,过期的key不进入RDB文件
从RDB文件恢复数据到内存数据库
数据载入数据库之前,会对key先进行过期检查,如果过期,不导入数据库(主库情况)
AOF对过期key的处理
过期key对AOF没有任何影响
从内存数据库持久化数据到AOF文件:
当key过期后,还没有被删除,此时进行执行持久化操作(该key是不会进入aof文件的,因为没有发生修改命令)
当key过期后,在发生删除操作时,程序会向aof文件追加一条del命令(在将来的以aof文件恢复数据的时候该过期的键就会被删掉)
AOF重写
重写时,会先判断key是否过期,已过期的key不会重写到aof文件
redis命令
练手网站:http://try.redis.io/
中文文档:http://redisdoc.com/
中文文档:http://redisdoc.com/
list底层是双向链表(当数据量比较小的时候,数据结构是压缩链表,而当数据量比较多的时候就成为了快速链表),
zset是有序集合,使用跳表来实现,跳表实现见左边图
zset是有序集合,使用跳表来实现,跳表实现见左边图
子主题
事务
关系型数据库具有ACID
原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性(Consistency)
事务前后数据的完整性必须保持一致。
隔离性(Isolation)
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性(Consistency)
事务前后数据的完整性必须保持一致。
隔离性(Isolation)
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
Redis 能保证A(原子性)和 I(隔离性),D(持久性)看是否有配置 RDB或者 AOF 持久化操作,但无法保证一致性,因为 Redis 事务不支持回滚。
可以简单理解为 Redis 中的事务只是比 Pipeline 多了个原子性操作,也就是不会被其他命令给分割
主从复制
实现原理:准备阶段-数据同步阶段-命令传播阶段
子主题
复制命令
SYNC 命令是一个非常耗费资源的操作
每次执行 SYNC 命令,主从服务器需要执行如下动作:
主服务器 需要执行 BGSAVE 命令来生成 RDB 文件,这个生成操作会 消耗 主服务器大量的 CPU、内存和磁盘 I/O 的资源;
主服务器 需要将自己生成的 RDB 文件 发送给从服务器,这个发送操作会 消耗 主服务器 大量的网络资源 (带宽和流量),并对主服务器响应命令请求的时间产生影响;
接收到 RDB 文件的 从服务器 需要载入主服务器发来的 RBD 文件,并且在载入期间,从服务器 会因为阻塞而没办法处理命令请求;
特别是当出现 断线重复制 的情况是时,为了让从服务器补足断线时确实的那一小部分数据,却要执行一次如此耗资源的 SYNC 命令,显然是不合理的。
PSYNC 命令的引入
所以在 Redis 2.8 中引入了 PSYNC 命令来代替 SYNC,它具有两种模式:
全量复制: 用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作;
部分复制: 用于网络中断等情况后的复制,只将 中断期间主节点执行的写命令 发送给从节点,与全量复制相比更加高效。需要注意 的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制;
部分复制的原理主要是靠主从节点分别维护一个 复制偏移量,有了这个偏移量之后断线重连之后一比较,之后就可以仅仅把从服务器断线之后确实的这部分数据给补回来了。
每次执行 SYNC 命令,主从服务器需要执行如下动作:
主服务器 需要执行 BGSAVE 命令来生成 RDB 文件,这个生成操作会 消耗 主服务器大量的 CPU、内存和磁盘 I/O 的资源;
主服务器 需要将自己生成的 RDB 文件 发送给从服务器,这个发送操作会 消耗 主服务器 大量的网络资源 (带宽和流量),并对主服务器响应命令请求的时间产生影响;
接收到 RDB 文件的 从服务器 需要载入主服务器发来的 RBD 文件,并且在载入期间,从服务器 会因为阻塞而没办法处理命令请求;
特别是当出现 断线重复制 的情况是时,为了让从服务器补足断线时确实的那一小部分数据,却要执行一次如此耗资源的 SYNC 命令,显然是不合理的。
PSYNC 命令的引入
所以在 Redis 2.8 中引入了 PSYNC 命令来代替 SYNC,它具有两种模式:
全量复制: 用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作;
部分复制: 用于网络中断等情况后的复制,只将 中断期间主节点执行的写命令 发送给从节点,与全量复制相比更加高效。需要注意 的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制;
部分复制的原理主要是靠主从节点分别维护一个 复制偏移量,有了这个偏移量之后断线重连之后一比较,之后就可以仅仅把从服务器断线之后确实的这部分数据给补回来了。
Redis主从架构
主从核心机制
1.redis采用异步方式复制数据到slave节点,不过从redis2.8起,slave node会周期性的确认自己每次复制的数据量
2.一个master node是可以配置多个slave node的
3.slave node 也可以连接到其他的slave node
4.slave node做复制的时候,是不会阻塞master node的正常工作的
5.slave node做复制时,也不会阻塞对自己的查询操作,他会用旧的数据集提供服务,但是复制完成的时候,需要做删除数据集,加载新数据集,这个时候就会暂停对外服务了。
6.slave node 主要用来进行横向扩容,做读写分离,扩容的slave node可以提高的吞吐量
部署架构
哨兵
架构图
子主题
组成
哨兵节点: 哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的 Redis 节点,不存储数据;
数据节点: 主节点和从节点都是数据节点
数据节点: 主节点和从节点都是数据节点
功能
监控(Monitoring): 哨兵会不断地检查主节点和从节点是否运作正常。
自动故障转移(Automatic failover): 当 主节点 不能正常工作时,哨兵会开始 自动故障转移操作,它会将失效主节点的其中一个 从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
配置提供者(Configuration provider): 客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址。
通知(Notification): 哨兵可以将故障转移的结果发送给客户端。
其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移。而配置提供者和通知功能,则需要在与客户端的交互中才能体现。
自动故障转移(Automatic failover): 当 主节点 不能正常工作时,哨兵会开始 自动故障转移操作,它会将失效主节点的其中一个 从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
配置提供者(Configuration provider): 客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址。
通知(Notification): 哨兵可以将故障转移的结果发送给客户端。
其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移。而配置提供者和通知功能,则需要在与客户端的交互中才能体现。
故障转移
对于主从节点: 主要是 slaveof 配置的变化,新的主节点没有了 slaveof 配置,其从节点则 slaveof 新的主节点。
对于哨兵节点: 除了主从节点信息的变化,纪元(epoch) (记录当前集群状态的参数) 也会变化,纪元相关的参数都 +1 了。
对于哨兵节点: 除了主从节点信息的变化,纪元(epoch) (记录当前集群状态的参数) 也会变化,纪元相关的参数都 +1 了。
客户端原理
Jedis 客户端对哨兵提供了很好的支持。如上述代码所示,我们只需要向 Jedis 提供哨兵节点集合和 masterName ,构造 JedisSentinelPool 对象,然后便可以像使用普通 Redis 连接池一样来使用了:通过 pool.getResource() 获取连接,执行具体的命令。
在整个过程中,我们的代码不需要显式的指定主节点的地址,就可以连接到主节点;代码中对故障转移没有任何体现,就可以在哨兵完成故障转移后自动的切换主节点。之所以可以做到这一点,是因为在 JedisSentinelPool 的构造器中,进行了相关的工作;主要包括以下两点:
在整个过程中,我们的代码不需要显式的指定主节点的地址,就可以连接到主节点;代码中对故障转移没有任何体现,就可以在哨兵完成故障转移后自动的切换主节点。之所以可以做到这一点,是因为在 JedisSentinelPool 的构造器中,进行了相关的工作;主要包括以下两点:
1.遍历哨兵节点,获取主节点信息: 遍历哨兵节点,通过其中一个哨兵节点 + masterName 获得主节点的信息;该功能是通过调用哨兵节点的 sentinel get-master-addr-by-name 命令实现;
2.增加对哨兵的监听: 这样当发生故障转移时,客户端便可以收到哨兵的通知,从而完成主节点的切换。具体做法是:利用 Redis 提供的 发布订阅 功能,为每一个哨兵节点开启一个单独的线程,订阅哨兵节点的 + switch-master 频道,当收到消息时,重新初始化连接池。
2.增加对哨兵的监听: 这样当发生故障转移时,客户端便可以收到哨兵的通知,从而完成主节点的切换。具体做法是:利用 Redis 提供的 发布订阅 功能,为每一个哨兵节点开启一个单独的线程,订阅哨兵节点的 + switch-master 频道,当收到消息时,重新初始化连接池。
选举
故障转移操作的第一步 要做的就是在已下线主服务器属下的所有从服务器中,挑选出一个状态良好、数据完整的从服务器,然后向这个从服务器发送 slaveof no one 命令,将这个从服务器转换为主服务器。但是这个从服务器是怎么样被挑选出来的呢?
1.在失效主服务器属下的从服务器当中, 那些被标记为主观下线、已断线、或者最后一次回复 PING 命令的时间大于五秒钟的从服务器都会被 淘汰。
2.在失效主服务器属下的从服务器当中, 那些与失效主服务器连接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被 淘汰。
3.在 经历了以上两轮淘汰之后 剩下来的从服务器中, 我们选出 复制偏移量(replication offset)最大 的那个 从服务器 作为新的主服务器;如果复制偏移量不可用,或者从服务器的复制偏移量相同,那么 带有最小运行 ID 的那个从服务器成为新的主服务器。
2.在失效主服务器属下的从服务器当中, 那些与失效主服务器连接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被 淘汰。
3.在 经历了以上两轮淘汰之后 剩下来的从服务器中, 我们选出 复制偏移量(replication offset)最大 的那个 从服务器 作为新的主服务器;如果复制偏移量不可用,或者从服务器的复制偏移量相同,那么 带有最小运行 ID 的那个从服务器成为新的主服务器。
redis 哨兵模式
主要功能
集群监控,负责监控redis master和slave进程是否正常工作
消息通知,如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
故障转移,如果master node挂掉了,会自动转移到slave node上
配置中心,如果故障转移发生了,通知client客户端新的master
分布式
1.故障转移时,判断一个master node是宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。
2.即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要的组成部分的故障转移系统本身是单点的,就很坑爹。
核心知识
1.哨兵至少需要3个实例,来保证自己的健壮性。
2.哨兵+redis主从部署架构,是不会保证数据零丢失的,只能保证redis集群的高可用性
3.对于哨兵+redis主从这种复杂的部署结构,尽量在测试环境和生产环境中,都进行充足的测试和演练。
数据丢失
异步复制
因为master->slave的复制是异步的,所以可能有部分数据是没复制到slave,master就宕机了,此时这部分数据就丢失了。
脑裂
某个master所在机器突然脱离了正常的网络,跟其他slave机器不能正常连接,但实际上master还运行着,此时哨兵可能会认为master宕机了,然后开启选举,将其他slave切换成了master, 这个时候,集群就会有两个master, 也就是所谓的脑裂,此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧的master,可能数据也丢失了,因此旧的master再次恢复时,会被当做一个slave挂到新的master上去,自己得数据会被清空,重新从新的master复制数据。
解决方案
min-slaves-to-write 1
min-slaves-max-lag 10
要求至少有1个slave,数据复制和同步得延迟不能超过10秒
如果说一旦所有的slave,数据复制和同步都超过了10秒,那么这个时候,master就不会再接收任何请求了
上面两个配置可以减少异步复制和脑裂导致得数据丢失
min-slaves-max-lag 10
要求至少有1个slave,数据复制和同步得延迟不能超过10秒
如果说一旦所有的slave,数据复制和同步都超过了10秒,那么这个时候,master就不会再接收任何请求了
上面两个配置可以减少异步复制和脑裂导致得数据丢失
1.减少异步复制的数据丢失
有了min-slaves-max-lag这个配置,就可以确保说,一旦slave复制数据和ack延时太长,就认为可能master宕机后损失的数据太多了,那么就拒绝写请求,这样可以把master宕机时,由于部分数据未同步到slave导致的数据丢失降低到最低的可控范围内
有了min-slaves-max-lag这个配置,就可以确保说,一旦slave复制数据和ack延时太长,就认为可能master宕机后损失的数据太多了,那么就拒绝写请求,这样可以把master宕机时,由于部分数据未同步到slave导致的数据丢失降低到最低的可控范围内
2.减少脑裂的数据丢失
如果一个master出现了脑裂,跟其他slave丢了连接,那么上面两个配置可以确保,如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求,这样脑裂后旧的master就不会接受client的新数据,那么就避免了数据丢失。上面配置确保了,如果跟任何一个slave丢了连接,在10秒后,发现没有slave给自己ack,那么就拒绝新的写请求。因此在脑裂场景下,最多就丢失了10秒数据。
如果一个master出现了脑裂,跟其他slave丢了连接,那么上面两个配置可以确保,如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求,这样脑裂后旧的master就不会接受client的新数据,那么就避免了数据丢失。上面配置确保了,如果跟任何一个slave丢了连接,在10秒后,发现没有slave给自己ack,那么就拒绝新的写请求。因此在脑裂场景下,最多就丢失了10秒数据。
底层原理
sdown和odown两种失败状态转换机制
sdown是主观宕机,就一个哨兵如果自己觉得一个master宕机了,那么就是主观宕机,sdown达成的条件很简单,如果一个哨兵ping了一个master, 超过了is-master-down-after-milliseconds指定毫秒数后,就主观认为master宕机。
odown是客观宕机,如果quorum数量的哨兵都觉得一个master宕机了,那么就是客观宕机。sdown到odown转换条件很简单,如果一个哨兵在指定时间内,收到了quorum指定数量的其他哨兵也认为那个master是sdown了,那么就认为是odown了,客观认为master宕机。
哨兵集群的自动发现机制
哨兵互相之间的发现,是通过redis的pub/sub系统实现的,每个哨兵都会往_sentinel_:hello这个channel重发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知其他哨兵的存在
每隔两秒钟,每个哨兵都会往自己监控的某个master+slave对应的_sentinel_:hello channel里发送一个消息,内容是自己的host,ip和runid还有对这个master的监控配置
每个哨兵也会去监控每个master+slave对应的_sentinel_:hello channel, 然后去感知到同样在监控这个master+slave的其他哨兵的存在
每个哨兵还会跟其他哨兵对换对master的监控配置,互相进行监控配置的同步
每隔两秒钟,每个哨兵都会往自己监控的某个master+slave对应的_sentinel_:hello channel里发送一个消息,内容是自己的host,ip和runid还有对这个master的监控配置
每个哨兵也会去监控每个master+slave对应的_sentinel_:hello channel, 然后去感知到同样在监控这个master+slave的其他哨兵的存在
每个哨兵还会跟其他哨兵对换对master的监控配置,互相进行监控配置的同步
slave->master选举算法
如果一个master被认为odown了,而且大部分哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先选举一个slave来,会考虑slave的一些信息。
1.跟master断开连接的市场
2.slave优先级
3.复制到offset
4.run id
1.跟master断开连接的市场
2.slave优先级
3.复制到offset
4.run id
如果一个slave跟master断开连接已经超过了down-after-milliseconds的10倍,外加master的宕机时长,那么slave就会被认为不适合选举为master
down-after-milliseconds * 10 + milliseconds_since_master_is_in_sdown_state
down-after-milliseconds * 10 + milliseconds_since_master_is_in_sdown_state
接下来对slave进行排序
1.按照slave优先级进行排序,slave priority越低,优先级就越高
2.如果slave priority相同,那么看replica offset, 哪个slave复制了越多的数据,offset越靠后,优先级就越高
3.如果上面两个条件都相同,那么选择一个run id比较小的那个slave
1.按照slave优先级进行排序,slave priority越低,优先级就越高
2.如果slave priority相同,那么看replica offset, 哪个slave复制了越多的数据,offset越靠后,优先级就越高
3.如果上面两个条件都相同,那么选择一个run id比较小的那个slave
瓶颈
如果你的数量很少,主要是承载高并发高性能的场景,比如你的缓存一般就几个G,单机就足够了。
replication, 一个master, 多个slave, 要几个slave跟你的要求的吞吐量有关系,然后自己搭建一个sentinel集群,去保证redis主从架构,去保证redis主从架构的高可用性,就可以了。
replication, 一个master, 多个slave, 要几个slave跟你的要求的吞吐量有关系,然后自己搭建一个sentinel集群,去保证redis主从架构,去保证redis主从架构的高可用性,就可以了。
集群
架构图
子主题
子主题
slot槽
Redis 集群中内置了 16384 个哈希槽。当客户端连接到 Redis 集群之后,会同时得到一份关于这个 集群的配置信息,当客户端具体对某一个 key 值进行操作时,会计算出它的一个 Hash 值,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,Redis 会根据节点数量 大致均等 的将哈希槽映射到不同的节点。
再结合集群的配置信息就能够知道这个 key 值应该存储在哪一个具体的 Redis 节点中,如果不属于自己管,那么就会使用一个特殊的 MOVED 命令来进行一个跳转,告诉客户端去连接这个节点以获取数据:
再结合集群的配置信息就能够知道这个 key 值应该存储在哪一个具体的 Redis 节点中,如果不属于自己管,那么就会使用一个特殊的 MOVED 命令来进行一个跳转,告诉客户端去连接这个节点以获取数据:
集群作用
1.数据分区: 数据分区 (或称数据分片) 是集群最核心的功能。集群将数据分散到多个节点,一方面 突破了 Redis 单机内存大小的限制,存储容量大大增加;另一方面 每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。Redis 单机内存大小受限问题,在介绍持久化和主从复制时都有提及,例如,如果单机内存太大,bgsave 和 bgrewriteaof 的 fork 操作可能导致主进程阻塞,主从环境下主机切换时可能导致从节点长时间无法提供服务,全量复制阶段主节点的复制缓冲区可能溢出……
2.高可用: 集群支持主从复制和主节点的 自动故障转移 (与哨兵类似),当任一节点发生故障时,集群仍然可以对外提供服务。
2.高可用: 集群支持主从复制和主节点的 自动故障转移 (与哨兵类似),当任一节点发生故障时,集群仍然可以对外提供服务。
数据分区方案简析
哈希值 % 节点数
哈希取余分区思路非常简单:计算 key 的 hash 值,然后对节点数量进行取余,从而决定数据映射到哪个节点上。
不过该方案最大的问题是,当新增或删减节点时,节点数量发生变化,系统中所有的数据都需要 重新计算映射关系,引发大规模数据迁移。
不过该方案最大的问题是,当新增或删减节点时,节点数量发生变化,系统中所有的数据都需要 重新计算映射关系,引发大规模数据迁移。
一致性哈希分区
一致性哈希算法将 整个哈希值空间 组织成一个虚拟的圆环,范围是 [0 - 232 - 1],对于每一个数据,根据 key 计算 hash 值,确数据在环上的位置,然后从此位置沿顺时针行走,找到的第一台服务器就是其应该映射到的服务器
子主题
与哈希取余分区相比,一致性哈希分区将 增减节点的影响限制在相邻节点。以上图为例,如果在 node1 和 node2 之间增加 node5,则只有 node2 中的一部分数据会迁移到 node5;如果去掉 node2,则原 node2 中的数据只会迁移到 node4 中,只有 node4 会受影响。
一致性哈希分区的主要问题在于,当 节点数量较少 时,增加或删减节点,对单个节点的影响可能很大,造成数据的严重不平衡。还是以上图为例,如果去掉 node2,node4 中的数据由总数据的 1/4 左右变为 1/2 左右,与其他节点相比负载过高。
一致性哈希分区的主要问题在于,当 节点数量较少 时,增加或删减节点,对单个节点的影响可能很大,造成数据的严重不平衡。还是以上图为例,如果去掉 node2,node4 中的数据由总数据的 1/4 左右变为 1/2 左右,与其他节点相比负载过高。
带有虚拟节点的一致性哈希分区
该方案在 一致性哈希分区的基础上,引入了 虚拟节点 的概念。Redis 集群使用的便是该方案,其中的虚拟节点称为 槽(slot)。槽是介于数据和实际节点之间的虚拟概念,每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。
在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽 解耦 了 数据和实际节点 之间的关系,增加或删除节点对系统的影响很小。仍以上图为例,系统中有 4 个实际节点,假设为其分配 16 个槽(0-15);
槽 0-3 位于 node1;4-7 位于 node2;以此类推....
如果此时删除 node2,只需要将槽 4-7 重新分配即可,例如槽 4-5 分配给 node1,槽 6 分配给 node3,槽 7 分配给 node4;可以看出删除 node2 后,数据在其他节点的分布仍然较为均衡。
在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽 解耦 了 数据和实际节点 之间的关系,增加或删除节点对系统的影响很小。仍以上图为例,系统中有 4 个实际节点,假设为其分配 16 个槽(0-15);
槽 0-3 位于 node1;4-7 位于 node2;以此类推....
如果此时删除 node2,只需要将槽 4-7 重新分配即可,例如槽 4-5 分配给 node1,槽 6 分配给 node3,槽 7 分配给 node4;可以看出删除 node2 后,数据在其他节点的分布仍然较为均衡。
节点通信机制简析
哨兵
节点分为 数据节点 和 哨兵节点:前者存储数据,后者实现额外的控制功能
集群
没有数据节点与非数据节点之分:所有的节点都存储数据,也都参与集群状态的维护。为此,集群中的每个节点,都提供了两个 TCP 端口:
普通端口: 即我们在前面指定的端口 (7000等)。普通端口主要用于为客户端提供服务 (与单机节点类似);但在节点间数据迁移时也会使用。
集群端口: 端口号是普通端口 + 10000 (10000是固定值,无法改变),如 7000 节点的集群端口为 17000。集群端口只用于节点之间的通信,如搭建集群、增减节点、故障转移等操作时节点间的通信;不要使用客户端连接集群接口。为了保证集群可以正常工作,在配置防火墙时,要同时开启普通端口和集群端口。
集群端口: 端口号是普通端口 + 10000 (10000是固定值,无法改变),如 7000 节点的集群端口为 17000。集群端口只用于节点之间的通信,如搭建集群、增减节点、故障转移等操作时节点间的通信;不要使用客户端连接集群接口。为了保证集群可以正常工作,在配置防火墙时,要同时开启普通端口和集群端口。
Gossip 协议
节点间通信,按照通信协议可以分为几种类型:单对单、广播、Gossip 协议等。重点是广播和 Gossip 的对比。
广播是指向集群内所有节点发送消息。优点 是集群的收敛速度快(集群收敛是指集群内所有节点获得的集群信息是一致的),缺点 是每条消息都要发送给所有节点,CPU、带宽等消耗较大。
Gossip 协议的特点是:在节点数量有限的网络中,每个节点都 “随机” 的与部分节点通信 (并不是真正的随机,而是根据特定的规则选择通信的节点),经过一番杂乱无章的通信,每个节点的状态很快会达到一致。Gossip 协议的 优点 有负载 (比广播) 低、去中心化、容错性高 (因为通信有冗余) 等;缺点 主要是集群的收敛速度慢。
子主题
消息类型
集群中的节点采用 固定频率(每秒10次) 的 定时任务 进行通信相关的工作:判断是否需要发送消息及消息类型、确定接收节点、发送消息等。如果集群状态发生了变化,如增减节点、槽状态变更,通过节点间的通信,所有节点会很快得知整个集群的状态,使集群收敛。
节点间发送的消息主要分为 5 种:meet 消息、ping 消息、pong 消息、fail 消息、publish 消息。不同的消息类型,通信协议、发送的频率和时机、接收节点的选择等是不同的:
MEET 消息: 在节点握手阶段,当节点收到客户端的 CLUSTER MEET 命令时,会向新加入的节点发送 MEET 消息,请求新节点加入到当前集群;新节点收到 MEET 消息后会回复一个 PONG 消息。
PING 消息: 集群里每个节点每秒钟会选择部分节点发送 PING 消息,接收者收到消息后会回复一个 PONG 消息。PING 消息的内容是自身节点和部分其他节点的状态信息,作用是彼此交换信息,以及检测节点是否在线。PING 消息使用 Gossip 协议发送,接收节点的选择兼顾了收敛速度和带宽成本,具体规则如下:
(1)随机找 5 个节点,在其中选择最久没有通信的 1 个节点;
(2)扫描节点列表,选择最近一次收到 PONG 消息时间大于 cluster_node_timeout / 2 的所有节点,防止这些节点长时间未更新。
PONG消息: PONG 消息封装了自身状态数据。可以分为两种:第一种 是在接到 MEET/PING 消息后回复的 PONG 消息;第二种 是指节点向集群广播 PONG 消息,这样其他节点可以获知该节点的最新信息,例如故障恢复后新的主节点会广播 PONG 消息。
FAIL 消息: 当一个主节点判断另一个主节点进入 FAIL 状态时,会向集群广播这一 FAIL 消息;接收节点会将这一 FAIL 消息保存起来,便于后续的判断。
PUBLISH 消息: 节点收到 PUBLISH 命令后,会先执行该命令,然后向集群广播这一消息,接收节点也会执行该 PUBLISH 命令。
PING 消息: 集群里每个节点每秒钟会选择部分节点发送 PING 消息,接收者收到消息后会回复一个 PONG 消息。PING 消息的内容是自身节点和部分其他节点的状态信息,作用是彼此交换信息,以及检测节点是否在线。PING 消息使用 Gossip 协议发送,接收节点的选择兼顾了收敛速度和带宽成本,具体规则如下:
(1)随机找 5 个节点,在其中选择最久没有通信的 1 个节点;
(2)扫描节点列表,选择最近一次收到 PONG 消息时间大于 cluster_node_timeout / 2 的所有节点,防止这些节点长时间未更新。
PONG消息: PONG 消息封装了自身状态数据。可以分为两种:第一种 是在接到 MEET/PING 消息后回复的 PONG 消息;第二种 是指节点向集群广播 PONG 消息,这样其他节点可以获知该节点的最新信息,例如故障恢复后新的主节点会广播 PONG 消息。
FAIL 消息: 当一个主节点判断另一个主节点进入 FAIL 状态时,会向集群广播这一 FAIL 消息;接收节点会将这一 FAIL 消息保存起来,便于后续的判断。
PUBLISH 消息: 节点收到 PUBLISH 命令后,会先执行该命令,然后向集群广播这一消息,接收节点也会执行该 PUBLISH 命令。
持久化
RDB:RDB 是 Redis 默认的持久化方案。在指定的时间间隔内,执行指定次数的写操作,则会将内存中的数据写入到磁盘中。即在指定目录下生成一个dump.rdb文件。Redis 重启会通过加载dump.rdb文件恢复数据。
优点:
1 适合大规模的数据恢复。
2 如果业务对数据完整性和一致性要求不高,RDB是很好的选择。
缺点:
1 数据的完整性和一致性不高,因为RDB可能在最后一次备份时宕机了。
2 备份时占用内存,因为Redis 在备份时会独立创建一个子进程,将数据写入到一个临时文件(此时内存中的数据是原来的两倍哦),最后再将临时文件替换之前的备份文件。
所以Redis 的持久化和数据的恢复要选择在夜深人静的时候执行是比较合理的。
--------------------------------------------------------------------------------
AOF:Redis 默认不开启。它的出现是为了弥补RDB的不足(数据的不一致性),所以它采用日志的形式来记录每个写操作,并追加到文件中。Redis 重启的会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
优点:数据的完整性和一致性更高
缺点:因为AOF记录的内容多,文件会越来越大,数据恢复也会越来越慢。
优点:
1 适合大规模的数据恢复。
2 如果业务对数据完整性和一致性要求不高,RDB是很好的选择。
缺点:
1 数据的完整性和一致性不高,因为RDB可能在最后一次备份时宕机了。
2 备份时占用内存,因为Redis 在备份时会独立创建一个子进程,将数据写入到一个临时文件(此时内存中的数据是原来的两倍哦),最后再将临时文件替换之前的备份文件。
所以Redis 的持久化和数据的恢复要选择在夜深人静的时候执行是比较合理的。
--------------------------------------------------------------------------------
AOF:Redis 默认不开启。它的出现是为了弥补RDB的不足(数据的不一致性),所以它采用日志的形式来记录每个写操作,并追加到文件中。Redis 重启的会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
优点:数据的完整性和一致性更高
缺点:因为AOF记录的内容多,文件会越来越大,数据恢复也会越来越慢。
Redis 默认开启RDB持久化方式,在指定的时间间隔内,执行指定次数的写操作,则将内存中的数据写入到磁盘中。
RDB 持久化适合大规模的数据恢复但它的数据一致性和完整性较差。
Redis 需要手动开启AOF持久化方式,默认是每秒将写操作日志追加到AOF文件中。
AOF 的数据完整性比RDB高,但记录内容多了,会影响数据恢复的效率。
Redis 针对 AOF文件大的问题,提供重写的瘦身机制。
若只打算用Redis 做缓存,可以关闭持久化。
若打算使用Redis 的持久化。建议RDB和AOF都开启。其实RDB更适合做数据的备份,留一后手。AOF出问题了,还有RDB。
RDB 持久化适合大规模的数据恢复但它的数据一致性和完整性较差。
Redis 需要手动开启AOF持久化方式,默认是每秒将写操作日志追加到AOF文件中。
AOF 的数据完整性比RDB高,但记录内容多了,会影响数据恢复的效率。
Redis 针对 AOF文件大的问题,提供重写的瘦身机制。
若只打算用Redis 做缓存,可以关闭持久化。
若打算使用Redis 的持久化。建议RDB和AOF都开启。其实RDB更适合做数据的备份,留一后手。AOF出问题了,还有RDB。
混合持久化
将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 自持久化开始到持久化结束 的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小,于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。
子主题
Redis 数据备份与恢复
分布式锁
SET NX
REDISSON
重入性
LUA原子性
REDLOCK
Redis持久化意义
1.用作灾难恢复,数据恢复,保证高可用
2.解决缓存雪崩的问题。
3.通过备份数据快速恢复,支撑企业级业务。
Redis持久化的两种方式
RDB
优点
1.rdb会生成多个数据文件,每个数据文件代表了某一个时刻中redis的数据,这种多个数据文件的方式,每个文件都代表了某一个时刻的完整数据快照,非常适合做冷备。
2.rdb对redis对外提供读写服务,影响非常小,可以让redis保持高性能,因为redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行对RDB持久化即可。
3.相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速。
缺点
1.如果想要在redis故障时,尽可能少的丢失数据,那么RDB没有AOF好,一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据,这个问题,就是rdb更大的缺点,就是不合适做第一优先的恢复方案,如果你依赖RDB做第一优先恢复方案,会导致数据丢失的比较多。
2.RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户提供的服务暂停数毫秒,或者数秒。一般不要让RDB的间隔时间太长,否则每次生成的RDB文件太大,对redis本身的性能可能也会有影响。
配置
1.save 60 1000: 每隔60s,如果有超过1000个key发生了变更,那么就生成一个新的dump.rdb文件,就是当新的redis内存中完整数据快照。这个操作称为snapshotting, 快照。也可以手动调用save或者bgsave命令,同步或者异步执行rdb快照生成,save可以设置为多个,就是多个snapshotting的检查点。每到一个检查点,就会check一下,是否有指定key数量发生了变更,如果有,就生成一个新的dump.rdb文件
工作流程
1.redis根据配置尝试自己生成一个rdb快照文件。
2.fork一个子进程出来
3.子进程尝试将数据dump到临时的rdb快照文件中
4.完成rdb快照文件生成之后,就替换之前旧的快照文件dump.rdb,每次生成一个新的快照,就会覆盖之前的老快照,dump.rdb只有一个
模拟实验
通过redis-cli shutdown这种方式去停掉redis,其实是一种安全退出的模式,redis在退出的时候,会将内存中数据立即生成一个完整的rdb快照。
用kill -9 粗暴杀死redis进程,模拟redis异常退出,导致内存数据丢失的场景,这次就发现,redis进程被杀掉,数据没有进dump文件,几条最新的数据丢失了。
AOF
优点
1,AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次sync操作,最多丢失1秒中的数据,每隔1秒,就执行一次sync操作,保证os cache中的数据写入磁盘,redis进程挂了,最多丢掉1秒中数据。
2.AOF日志文件中以Apend-only模式写入,所以没有任何磁盘寻址的开销,写入的性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复。
3.AOF日志文件即使过大的时候,也会出现后台重写操作,也不会影响客户端的读写,因为在rewrite log的时候,会对其中的指令进行压缩,创建出一份需要恢复数据数据的最小日志来。再创建新日志文件的时候,老的日志还是照常写入。当新的merge日志文件ready的时候,再交换老日志文件即可。
4.AOF日志文件的命令通过非常可读的方式记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清除了所有的数据,只要这个时候后台rewrite还没有发生,那么立刻拷贝AOF文件,将最后一条flushall命令给删除,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据。
缺点
1.对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大。
2.AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒同步一次日志文件,当然,每秒一次同步,性能还是很高的,如果你要保证一条数据都不丢,也是可以的,AOF的同步设置每写入一条数据,同步一次,那样redis QPS大降
3.做数据恢复的时候,会比较慢,还有做冷备,定期备份,不太方便,可能要自己手写复杂的脚本去做,做冷备不合适。
配置
1.AOF持久化,默认是关闭的,默认是打开RDB持久化,appendonly yes,可以打开AOF持久化机制,在生产环境里面,一般来说AOF都是要打开的,除非你说随便丢个几分钟的数据也无所谓,打开AOF机制后,redis每次收到一条写命令,就会写入日志文件中,当然是先写入os cache的,然后每隔一定时间再同步一下。
2.即使AOF和RDB都开启了,redis重启的时候,也是优先通过AOF进行数据恢复的,因为AOF数据比较完整。
3.可以配置AOF的同步策略,有三种策略可以选择,一种是每次写入一条数据就执行一次同步,一种是每隔一秒执行一次同步,一种是不主动执行同步。
always: 每次写入一条数据,立即将这个数据对应的写日志同步到磁盘中去,性能非常差,吞吐量很低,确保说redis里的数据一条都不丢。
mysql->内存策略,大量磁盘,OPS到多少,1k~2k,QPS每秒钟的请求数量
redis->内存,磁盘持久化,OPQ到多少,单机,一般来说,上万QPS没问题
redis->内存,磁盘持久化,OPQ到多少,单机,一般来说,上万QPS没问题
everysec:每秒将os cache中的数据同步到磁盘,这个最常用,生产环境一般都这么配置,性能很高,QPS还是可以上万的
no: 仅仅redis负责将数据写入os cache就撒手不管了,然后后面os自己会不时有自己的策略将数据写入磁盘,不可控了。
模拟实验
1.kill -9 杀掉redis进程,重新启动redis进程,发现数据被恢复回来了,从AOF文件中恢复回来的。在appendonly.aof中,可以看到刚写的日志,他们其实就是先写入os cache的,然后1秒以后才同步到磁盘中,才是安全的,不然光是在os cache中,机器只要重启,就什么也没有了。
2.redis进程启动的时候,直接就会从appendonly.aof中加载所有的日志,把内存中数据恢复回来。
AOF rewrite
redis中数据其实是有限的,很多数据很可能会自动过期,可能会被用户删除,可能会被redis用缓存清除算法清除掉。
redis中所有数据会不断淘汰掉旧的,就一部分常用的数据会被自动保留在redis内存中。
所以可能很多之前的已经被清理掉的数据,对应的写日志还停留在AOF中,AOF日志文件就一个,会不断的膨胀,到很大很大。
所以AOF会自动在后台每隔一定时间做rewrite操作,比如日志里已经存放了针对100w数据的写日志了,redis内存就只剩下10w;基于内存中当前的10万数据构成一套最新的日志到AOF中;覆盖之前的老日志,确保AOF日志文件不会过大,保持和redis内存数据量一致。
redis中所有数据会不断淘汰掉旧的,就一部分常用的数据会被自动保留在redis内存中。
所以可能很多之前的已经被清理掉的数据,对应的写日志还停留在AOF中,AOF日志文件就一个,会不断的膨胀,到很大很大。
所以AOF会自动在后台每隔一定时间做rewrite操作,比如日志里已经存放了针对100w数据的写日志了,redis内存就只剩下10w;基于内存中当前的10万数据构成一套最新的日志到AOF中;覆盖之前的老日志,确保AOF日志文件不会过大,保持和redis内存数据量一致。
在redis中,可以配置rewrite的策略
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
比如说上一次AOF rewrite操作,是128mb,然后就会接着128mb继续写aof的日志,如果发现增长的比例,超过了之前的100%, 256mb,就可能去触发一次rewrite,但是此时还是要和min-size, 64mb去做比较, 256mb>64mb,才会触发rewrite
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
比如说上一次AOF rewrite操作,是128mb,然后就会接着128mb继续写aof的日志,如果发现增长的比例,超过了之前的100%, 256mb,就可能去触发一次rewrite,但是此时还是要和min-size, 64mb去做比较, 256mb>64mb,才会触发rewrite
工作流程
1.redis fork一个子进程
2.子进程基于当前内存中的数据,构建日志,开始往一个新的临时的AOF文件中写入日志
3.redis主进程,接收到client新的写操作以后,在内存中写入日志,同时新的日志也继续写入旧的AOF文件
4.子进程写完新的日志文件后,redis主进程将内存中的新日志再次追加到新的AOF文件中
5.用新的日志文件替换掉旧的日志文件
RDB VS AOF
区别
1.RDB也可以做冷备,生成多个文件,每个文件都代表了某一个时刻的完整数据快照,AOF也可以做冷备,只有一个文件,但是你可以每隔一段时间,去copy一份这个文件出来
2.RDB每次写,都是直接写redis内存,只是在一定的时候,才会将数据写入磁盘中,AOF每次都是要写文件的,虽然可以快速写入os cache中,但是还是有一定的时间开销,速度比RDB慢一些。
3.AOF存放指令日志,做数据恢复时,其实是要回访和执行所有的指令日志,来恢复出来内存中所有的数据的,RDP就是一份数据文件,恢复的时候,直接加载到内存中即可。
同时工作
1.如果RDB在执行snapshotting操作,那么redis不会执行AOF rewrite,如果redis在执行AOF rewrite,那么就不会执行RDB snapshotting
2.如果RDB在执行snapshotting,此时用户执行BGREWRITEAOF命令,那么等RDB快照生成后,才会执行AOF rewirte
3.同时有RDB snapshot文件和AOF日志文件,那么redis重启的时候,会优先使用AOF进行数据恢复,因为其中的日志会更完整。
内存回收策略
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
LRU是最近最少使用页面置换算法(LeastRecentlyUsed),也就是首先淘汰最长时间未被使用的页面!
LFU是最近最不常用页面置换算法(LeastFrequentlyUsed),也就是淘汰一定时期内被访问次数最少的页!
比如,第二种方法的时期T为10分钟,如果每分钟进行一次调页,主存块为3,若所需页面走向为2121234
注意,当调页面4时会发生缺页中断
若按LRU算法,应换页面1(1页面最久未被使用)但按LFU算法应换页面3(十分钟内,页面3只使用了一次)
可见LRU关键是看页面最后一次被使用到发生调度的时间长短,
而LFU关键是看一定时间段内页面被使用的频率!
Redis回收算法
介绍
redis是会在数据达到一定程度之后,超过了一个最大的限度之后,就会将数据进行一定的清理,从内存中清理掉一些数据
redis默认情况下就是使用LRU策略,因为内存是有限的
LRU: Least Recently Used, 最近最少使用算法: 将最近一段时间内,最少使用的一些数据,给干掉。比如说有一个key,在最近一小时内,只被访问了一次,还有一个key在最近一个小时内,被访问了1万次
缓存清理设置
maxmemory, 设置redis用来存放数据的最大内存大小,一旦超出这个内存大小之后,就会立即使用LR算法清理掉部分数据
对于64 bit的机器,如果maxmemory设置为0, 那么就默认不限制内存的使用,知道耗尽机器中所有内存为止
maxmemory-policy 可以设置内存达到最大先之后,采取什么策略来处理
清理策略
1.noeviction:如果内存使用达到了maxmemory, client还要继续写入数据,那么就直接报错给客户端
2.allkey-lru: 就是我们常说的LRU算法,移除掉最近最少使用的那些keys对应的数据
3.volatile-lru: 就是采用LRU算法,但是仅针对那些设置了指定存活时间ttl的key才会清理掉
4.allkeys-random: 随机选择一些key来删除
5.volatile-random:随机选择一些设置了ttl的keyL来删除掉
6.volatile-ttl: 移除掉部分keys,选择那些ttl时间比较短的keys
Redis Cluster
介绍
Cluster
自动将数据进行分片,每个master上放一部分数据
提供内置的高可用支持,部分master不可用时,还是可以继续工作的。
Cluster 端口
每个redis要开放两个端口号,比如一个6379,一个加10000的端口号,比如16379
16379端口号是用来进行节点间通信的,也就是cluster bus的东西,集群总线,cluster bus的通信,用来进行故障检测,配置更新,故障转移授权。
Cluster bus用了另外一种二进制的协议,主要用于节点间进行高效的数据交换,占用更少的网络宽带和处理时间。
Cluster bus用了另外一种二进制的协议,主要用于节点间进行高效的数据交换,占用更少的网络宽带和处理时间。
hash slot 算法
redis cluster有固定的16384个hash slot, 对每个key计算CRC16值,然后后对16384取模,可以获取key对应的hash slot
redis cluster中每个master都会持有部分的slot,比如3个master,那么可能每个master都持有5000个hash slot
redis cluster中每个master都会持有部分的slot,比如3个master,那么可能每个master都持有5000个hash slot
hash slot让node增加和移除和简单,增加一个master, 就将其他的master的hash slot移动部分过去,减少一个master, 就将它的hash slot移动到其他master上去
移动hash slot的成本是非常低的
客户端的api可以对指定的数据,让他们走同一个hhash slot, 通过hash tag来实现
移动hash slot的成本是非常低的
客户端的api可以对指定的数据,让他们走同一个hhash slot, 通过hash tag来实现
核心原理
节点间内部通信机制
基础通信原理
节点间采取gossip协议进行通信
和集中式不同,不是将集群元数据(节点信息,故障,等等)集中存储在某个节点上,而是互相之间不断通信,保持整个集群所有节点的数据是完整的。
集中式,好处在于元数据的更新和读取,时效性非常好,一旦元数据出现了变更,立即就更新到集中式的存储中,其他节点读取的时候立即就可以感知到,不好在于,所有的元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力
gossip的好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续的,打到所有的节点去更新,有一定的延时,降低了压力,缺点,元数据更新有延时,可能导致集群的一些操作会由一些滞后。
gossip的好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续的,打到所有的节点去更新,有一定的延时,降低了压力,缺点,元数据更新有延时,可能导致集群的一些操作会由一些滞后。
10000端口
每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如7001,那么用于节点通信的就是17001端口
每个节点每隔一段时间都会往其他几个节点发送ping消息,同时其他几个节点收到ping之后返回pong
故障信息,节点的增加和移除,hash slot信息,等等
gossip协议
gossip包含多种消息,比如ping, pong, meet, fail等
meet: 某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信
redis-trib.rb add-node命令:其实内部就是发送了一个gossip meet的消息,给新加入的节点,通知那个节点去加入我们的集群
ping: 每个节点都会频繁的发送ping给其他节点,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据
每个节点每秒都会频繁的给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据
每隔节点每秒都会频繁的发送ping给其他集群,ping,频繁的互相之间交换数据,互相进行元数据的更新
pong:返回ping和meet,, 包含自己的状态和其他信息,也可以用于信息广播和更新
fail: 某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了。
meet: 某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信
redis-trib.rb add-node命令:其实内部就是发送了一个gossip meet的消息,给新加入的节点,通知那个节点去加入我们的集群
ping: 每个节点都会频繁的发送ping给其他节点,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据
每个节点每秒都会频繁的给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据
每隔节点每秒都会频繁的发送ping给其他集群,ping,频繁的互相之间交换数据,互相进行元数据的更新
pong:返回ping和meet,, 包含自己的状态和其他信息,也可以用于信息广播和更新
fail: 某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了。
ping消息深入
ping很频繁,而且要携带一些元数据,所以可能会加重网络负担
每个节点每秒会执行10次ping, 每次会选择5个最久没有通信的其他节点
当然如果发现某个节点通信延时达到了cluster_node_timeout/2, 那么立即发送ping,避免数据交换延时过长,落后的时间太长了。
所以cluster_node_timeout可以调节,如果调节比较大,那么会降低发送的频率
每次ping, 一个是带上自己节点的信息,还有就是带上1/10其他节点的信息,发送出去,进行数据交换
至少包含3个其他节点的信息,最多包含总节点-2个其他节点的信息
每个节点每秒会执行10次ping, 每次会选择5个最久没有通信的其他节点
当然如果发现某个节点通信延时达到了cluster_node_timeout/2, 那么立即发送ping,避免数据交换延时过长,落后的时间太长了。
所以cluster_node_timeout可以调节,如果调节比较大,那么会降低发送的频率
每次ping, 一个是带上自己节点的信息,还有就是带上1/10其他节点的信息,发送出去,进行数据交换
至少包含3个其他节点的信息,最多包含总节点-2个其他节点的信息
Q&A
redis工作线程只能有一个
6以后只是分了多的线程去进行IO
五大基本类型
String
字符串
数值计算
INCR 对某个int类型的编码+1 可以用作计数限流
秒杀
关于数值计算都可以使用
二进制hitmap
setbit k1 1 1
List
可以模拟栈:同向指令
模拟列表:异向操作
ltrim
帖子
评论
评论
hash
详情页
用户信息等
javahashmap怎么用这个就能怎么用
聚合操作
set
作用:无序,去重
随机事件
集合操作
完成推荐系统!
例如 好友的并集可能认识的人
好友的交集
等
ZSet
Sorted Set
有序集合
怎么排序?排序规则
元素需要有个分值
如果分值相同,按照自身字典序
去重
动态排序
完成排行版
redis内部是从小打到
排行版从大到小
动态分页
排序表如何实现的?skiplist
过期时间设置
消极设置
取值时查看是否过期,过期则返回空
积极设置
每次随机取值查看是否过期,过期清空
redis-cli --raw
会根据字符集翻译成可见字符
Linux
一.服务器注意事项
1.远程服务器不允许关机,只能重启
2.重启前应该关闭服务(例如生产服务,次之网络服务)
3.不要在服务器访问高峰运和地高负载命令(例解,压缩大文件,大量IO操作,如整盘扫描,复制大文件)
4.远程配置防火墙时不要把自已踢出服务器
5.指定合理的密码规范并定期更新
6.合理分配权限
二.Linux注意事项
1.大小写敏感,命令长小写
2.所有内容都是以文件形式保存
3.不靠扩展名(后缀名)区分文件类型
4.所有存储设备都必须挂载后才能使用,包括硬盘,U盘,光盘
三.文件系统结构
相同:linux与window表现出来的文件结构都是树状结构
不同:实际存储的时候,linux可以将树下的某个分支单独出来,并在分区的时候指定一个分区,单独存储这一“分支”
四.分区建议及推荐分区方案
1.分须分区
/:根分区
Swap:交换分区,建议设为内存的2位,但最好不要超过2G
2.推荐分区方案
/:10G
/boot:200M
Swap:2G
/var:
1G以上,可变数据放在其中,例如日志文件,分配大小视用途而定,一般现在的大硬盘,建议分配5G
/home:最大的剩余空间的一半,用户的家目录所在
/usr:最大的剩余空间的一半,最耗用空间的部份。存放软件的地方
五.常用命令
1.文件处理命令
1.命令格式与目录处理命令ls
1.命令格式:命令 -[选项] [参数]
2.ls
别名ll= ls -i
-a 显示隐藏文件,隐藏文件是以“.”开头的文件
-i 显示详细信息
2.目录处理命令
mkdir:创建目录
-p 递归创建
cd:切换目录
cd ..表示返回上一级。..表示上级目录,.表示当前目录
cd:返回家目录
pwd:显示当前目录的绝对路径
rmdir:删除空目录,使用比较少
cp:复制文件或目录(复制同时可实现改名的作用)
-r:复制目录
-p:保留文件属性
mv:剪切文件,改名
rm:删除文件或目上录
-r:删除目录
-f:强制执行
3.文件处理命令
touch:创建文件
文件名不能带"/"符号,不建议带空格的文件名
cat:顺序显示文件内容
-n:带行号
tac:反向显示文件内容
more:分页显示文件内容,但不能向上翻页
f或空格:向下翻页,Enter:向下换行,q或Q:退出
less:分页显示文件内容,可向上翻页,带查找功能
同more,另Page Down,Page Up为向上向下翻页,上下箭头为向上向下换行。
输入/XXX和?XXX为向下上查找字符,下一个按n
head:显示文件前几行,默认10行
-n+“空格”+行数
tail:显示文件末尾几行,默认10行
同head
-f:动态显示,常用于动态显示日志文件
4.链接命令
ln:生成链接文件
-s:创建软链接,默认生成硬链接
软链接:类似Window快捷方式,文件类型为l,日常使用比较多
硬链接:一个或多个具有相同索引节点的文件,具有防止“误删”的功能
不能给目录创建
不能跨区创建
2.权限管理命令
1.权限管理命令chmod
-R 递归修改
r=读权限(查看文件/列表目录中内容),w=写权限(修改文件,不能删/目录中创建,删除文件),x=执行权限(执行文件/进入目录)
权限位:r=4,w=2,x=1
u=所有者,g=所属组,o=其它人,a=所有人
2.其他权限管理命令
chown:修改所有者和所属组。chown user:group 文件或目录
chgrp:修改所属组。chgrp group 文件或目录
umask:显示,设置文件和目录的缺省权限掩码
-S:rwx的形式显示
实际权限=777-掩码
3.文件搜索命令
1.文件搜索命令find
-name:按文件名字查找
-iname:不区分大小写
-size:按文件大小查询,以数据块为单位,1数据块=0.5KB
-user:按所有者查找
-group:按所属组查找
-amin:查找(超过多长时间或多长时间内)访问过的文件
-cmin:查找(超过多长时间或多长时间内)修改过属性的文件
-mmin:查找(超过多长时间或多长时间内)修改过内容的文件
-type:按类型查找。f:文件,d:目录,l:软链接
-inum:按i节点查找
-a:两个条件同时满足
-o:两个条件满足任意一个即可
-exec/-ok:对搜索结果执行操作。例:find / -name init* -exec ls {} ;
可用通配符:*,?,+,—符号
2.其它搜索命令
1.locate:在文件资料库中查找
updatedb:手动更新资料库,tmp临时目录等不在更新范围之内
-i:不区分大小写
2.which:搜索命令位置及别名
3.whereis:搜索命令及帮助文档位置
例:
passwd: /usr/bin/passwd(命令)
passwd: /etc/passwd(配置文件) /usr/share/man/man1/passwd.1.gz /usr/share/man/man5/passwd.5.gz。1是命令的帮助,5是配置文件的帮助
4.grep:在文件内容中搜索关键字
-i:不区分大小写
-v:排除指定字串
4.帮助命令
1.man:查看命令或配置文件的帮助信息
man 1 XXX查看命令的帮助信息,man 5 XXX查看配置文件的帮助信息,前提是有的话
2.whatis:查看命令简短的帮助信息
3.apropos:查看配置文件简短的帮助信息
4.--help
显示常见的选项的帮助信息
5.info:跟man差不多
6.help:获得内置命令的帮助
内置命令:找不到命令所在路径的命令,linux有不少
5.用户管理命令
1.useradd:添加新用户
2.passwd:设置或更改用户密码
3.who:查看登录用户信息
4.w:查看登录用户详细信息
6.压缩解压命令
1.gzip与gunzip或gzip -d
只能压缩文件
压缩后不保留原文件
2.zip与unzip
-r:压缩目录
压缩后保留原文件
3.bzip2与bunzip2或bzip2 -d
只能压缩文件
-k:压缩后保留原文件
4.tar:打包
-c:打包
-x:解包
-v:显示详细信息
-f:指定文件名
-z:打包:打包后通过gzip压缩。2.解包:解压gz格式的打包文档
-j:打包:打包后通过bzip2压缩。2.解包:解压bz2格式的打包文档
7.网络命令
1.write:给在线用户发信息
2.wall:发送广播信息,ctrl+d保存发送
3.ping:测试网络连通性
4.ifconfig:查看/设备网卡信息
5.mail:发送/查看邮件
进入mail后按h显示邮件列表
按d加编号删除邮件
6.last:查看目前与过去登录系统的用户信息
7.lastlog:检查全部/特定用户上次登录的时间
-u:查看特定用户
8.traceroute:显示数据包到主机间的路径
9.netstat:显示网络相关信息
-n:显示IP和端口
-t:显示tcp协议的连接
-u:显示udp协议的连接
-l:显示监听状态的连接
-a:显示所有网络连接,包含-t,-u,-l
-r:查路由表可显示网关,单独使用
10.setup:配置网络,redhat系列独有
11.mount,umount:挂载/卸载
8.关机命启查看当前运行级别退出登录命令
1.关机命令
1.shutdown
可以指定执行时间和警告信息
-h:关机
-r:重启
-c:取消前一个关机命令
2.halt
3.poweroff
4.init 0
2.重启命令
1.reboot
2.init 6
3.runlevel:当看当前级别
4.logout:退出登录,使用完一定要退出
六.文本编辑器Vim
1.Vim工作模式
1.命令模式:插入模式按ESC
2.插入模式:命令模式输入i,a,o
3.编辑模式:命令模式输入:
2.Vim常用操作
:set nu:设置行号
:set nonu:取消行号
:n:到第n行
x:删除光标所在处字符
dd:删除光标所在行
u:取消上一步的操作
:x,yd:删除由x行到y行
yy:复制当前行,nyy复制当前行以下n行
dd:剪切当前行,ndd剪切当前以下n行
R:从光标所在处开始替换字符,按ESC结束
/string:搜索字符串,n下一下
:w:保存
:q:退出
3.Vim常用技巧
跳过不看
七.软件包管理简介
1.软件包分类,二者概念上的区别
1.源码包:原代码安装,脚本安装包
优点
1.开源,可以自行修改源代码
2.可以自由选择所需功能
3.源码包需要编译,更加适合安装的系统,稳定性和效率更高
4.卸载方便,可直接删除安装的位置, 没垃圾
缺点
1.安装步骤较多,尤其安装大型的软件集合时,容易出现拼写等人为错误
2.编译时间较长,比二进制包安装慢
3.安装过程报错新手很难解决
2.二进制包:RPM包,DEB包,编译后的二进制文件
优点
1.包管理系统简单。安装,升级,查询,卸载几个命令就可以实现
2.安装速度比源码包快得多
缺点
1.因为是编译后的,不能再看到源代码
2.功能选译不如源码包灵活
3.存在依赖性的问题
2.RPM包管理
1.rpm命令管理
1.RPM包命令原则
例:httpd-2.2.15-15.el6.centos.1.i686.rpm
httpd:软件包名,而httpd-2.2.15-15.el6.centos.1.i686.rpm是包全名
2.2.15:软件版本
15:发布的次数
el6.centos:适合的Linux平台
i686:适合的硬件平台,noarch:适合任何硬件平台
rpm:rpm包扩展名
2.RPM包依赖性
1.树形依赖:a->b->c
2.环形依赖:a->b->c->a
3.模块依赖:模块依赖查询网站:www.rpmfind.net
3.RPM安装
-i:安装,后跟包全名。例:rpm -ivh 包全名
-U:升级,后跟包全名,大写U
-e:后跟包名
-v:显示详细信息
-h:显示进度
--nodeps:不检测依赖性
4.用rpm安装软件很痛苦
5.查询
-q:查询指定的包是否安装
rpm -q httpd
-qa:查询所有已安装的包
rpm -qa |grep httpd:列出所有已安装的包的包名带“httpd”的清单
-i:查询已安装包中的包信息
rpm -qi 包名
-p:查询没安装包的信息
rpm -qip 包全名
-l:查询包的安装位置
rpm -qlp 包名(装没装都可以查)
-f:查询文件属于哪个包
rpm -qf 文件名
-R:查询包的依赖性
rpm -qRp 包名
6.RPM包校验
-V:校验已安装的RPM包
验证内容
S:文件大小是否改变
M:文件的类型或权限是否被改变
5:文件的MD5校验是否改变
D:设备的中,从代码是否改变
L:文件路径是否改变
U:文件的所有者是否改变
G:文件的所属组是否改变
T:文件的修改时间是否改变
文件类型
c:配置文件
d:普通文档
g:ghost文件,很少见,就是该文件不应该被这个RPM包包含
l:授权文件
r:描述文件
RPM包中文件提取,用于修复文件
rpm2cpio 包全名 | cpio -idv.文件绝对路径
2.yum在线管理
1.IP地址配置和网络yum源
redhat系统可以用setup命令来配置网络参数
2.yum命令
yum list:查询所有可用软件列表
yum search 关键字:搜索服务器上所有和关键字相关的包
yum -y install 包名:自动应答安装包
update:升级,格式跟install一样
remove:删除,同上
安装软件的原则是用什么装什么,升级软件也要有针对性的升级不要“yum -y update”。因为这样会连linux的内核在内,所有包一并更新,更新内核后需要配置才能使用,不配置是进入不了系统的。remove也一样,因为依赖性的问题,删除软件有可以使其它软件崩溃,尽量不要卸载。
软件组
yum grouplist
yum groupinstall 软件组名
yum groupremove 软件组名
3.光盘yum源搭建
1.mount:挂载光盘
2.修改原始的3个网络yum源文件失效
3.让光盘yum源生效,CentOS-Media.repo就是系统准备给用户的光盘yum源文件,使用时修改内容即可
3.源码包管理
源码包和RPM包的区别,安装位置不同
RPM包安装位置:一般情况下是默认的
/etc/:配置文件
/usr/bin/:可执行命令
/usr/lib/:程序所使用的库
/usr/share/doc/:使用手册
/usr/share/man/:帮助文件
源码包安装位置:可以指定位置
建议:/usr/local/软件名/
两者安装位置不同的影响
RPM安装的服务管理
1.绝对路径中运行
例:/etc/rc.d.init.d/httpd start
2.,因为安装位置一般是默认的,所以可以用简化命令运行
例:service httpd start,service是redhat系统专有的
实际上service命令会去/etc/rc.d.init.d/目录查找你所需要运行的服务
类似于window中设定了系统变量
源码代安装的服务
因为安装路径不同,没有了”系统变量“,只能使用绝对路径
源码包安装过程
注意事项
1.源代码保存位置:/usr/local/src/
2.软件安装位置:/usr/local/
3.如何确定安装过程报错
1.安装过程停止
2.并出现error,warning或no的提示
1.安装C语方编译器:gcc
2.下载源码包
3.解压源码包并进入解压的目录,看到INSTALL(安装说明)和README(使用说明)两个文件,最好可以查看一下
4.运行./confure
1.定义需要的功能选项。一定要使用--prefix=/usr/local/软件名
2.检测系统环境是否符号安装要求,例如依赖包是否安装
3.将定义好的功能选择和环境信息写入Makefile用于后续编译
5.运行make,占用时间最长
调用gcc把源码包编译成机器码
6.运行make install
现在才安装到服务器上
7.运行make clean
清空编译产生的临时文件,如果在运行make install之前报错,也可以用这个。清空临时文件
源码包的卸载
make uninstall或不需要卸载命令,直接删除安装目录即可,不会遗留任何垃圾文件
4.脚本安装包
类似于window的安装方式
八.用户和用户组管理
1.用户配置文件
1.用户信息文件:/etc/passwd
1.用户名
2.密码标志
3.UID
UID为1~499是伪用户,不能登录
系统用户默认是500~65535之间
4.GID
5.用户说明
6.家目录
7.登录之后的Shell
2.影子文件:/etc/shadow
1.用户名
2.加密密码
如何为*或!!代表没密码,不能登录
3.密码最后一次修改日期
4.两次密码的修改间隔
5.密码有效期
6.密码有效期到期的警告天数
7.密码过期后的宽限天数
留空或0:到期后马上失效
-1:密码永不失效
8.帐号失效时间,使用时间戳表示
9.保留字段
3.组信息文件/etc/group和组密码文件/etc/gshadow
2.用户管理相关文件
1.用户家目录
root:/root/,权限550
普通用户:/home/用户名/,权限为700
2.用户邮箱
/var/spool/mail/用户名/
3.用户模板
/etc/skel/
3.用户管理命令
1.useradd:用户添加命令
添加用户时有很多默认值,那这些默认值由什么控制?
/etc/default/useradd
/etc/login.defs
2.passwd:修改用户密码
3.usermod:修改用户信息
4.chage:修改密码状态
chage -d 0 登录名:强迫用户第一次登录的时候就需要修改密码
5.userdel:删除用户
-r:删除用户同时删除用户家目录,建议使用
6:id:查看用户ID和组ID的信息
7.su:切换用户
-:连带用户的环境一起切换,切换用户时,一定要带“-”选项
例:su - 用户名
-c:不切换用户,但用这个“用户”的身份来执行某个命令
例:su - root -c "useradd user3"
7.系统添加和删除用户时系统所操作的文件
1./etc/passwd:用户信息文件
2./etc/shadow:2.影子文件
3./etc/group:组信息文件
4./etc/gshadow:组密码文件
5./var/spool/mail/用户名/:邮箱
6./etc/skel/:模板
7./home/用户名:家目录
4.用户组管理命令
1.groupadd:添加用户组
-g:指定组ID
2.groupmod:修改用户组
-g:修改组ID
-n:修改组名
不建议使用,因为还要很多东西要改,如家目录、邮箱。返而还不如直接删除再重建
3.groupdel:删除用户组
如果有用户的”初始组“是这个要删除的组,则不能删除
如果有用户的”附加组“是这个要删除的组,则不影响,可以删除
4.gpasswd:添加,删除附加组中的用户
-a:添加用户进组
-d:从组中删除用户
九.权限管理
1.ACL权限
1.ACL权限简介与开启
1.解决(所有者,所属组,其它人)身份不够用的问题
2.类似Window的权限,抛开(所有者,所属组,其它人)身份,直接给某个用户配权限
3.查看分区ACL权限是否开启
现在的linux默认支持acl
1.df -h:查看系统有那些分区
2.dumpe2fs -h 分区:查看分区详细信息
Default mount options: user_xattr acl默认支持acl
如不支持可手动开启
1.临时开启:mount -o remount,acl /,重新挂载根分区,并挂载加入acl权限
2.永久生效
1.修改/etc/fstab,在defaults后加",acl",一般情况下这个”defaults“就已经包含acl
2.mount -o -remount /:重新挂载文件系统或重启,使配置生效
2.查看与设定ACL权限
1.查看acl权限:getfacl 文件名
2.setfacl 选项 文件名
-m:设acl
例:setfacl -m u:test(用户名):rx(权限) /project(文件或目录)
例:setfacl -m g:test(组名名):rx(权限) /project(文件或目录)
设置完成后,权限位后会多个“+”位,表示除了普通权限外,还有acl权限
3.最大有效权限与删除ACL权限
输入getfacl 文件名
显示出来的“mask”是用来指定最大有效权限的。如果赋予acl权限后,是需要和mask的权限“相与”才能得到用户的真正权限,但不影响“所有者“的权限
修改最大有效权限mask:setfacl -m m:rx(权限) /project(文件或目录)
删除ACL权限
1.setfacl -x u:text(用户名) project(文件或目录)
2.setfacl -x g:text(组名名) project(文件或目录)
3.setfacl -b project(文件或目录)
4.默认ACL权限和递归ACL权限
Linux
awk
https://blog.viakiba.cn/2020/07/17/Linux%E5%91%BD%E4%BB%A4%E4%B9%8BAWK/
https://blog.viakiba.cn/2020/07/17/Linux%E5%91%BD%E4%BB%A4%E4%B9%8BAWK/
grep
https://blog.viakiba.cn/2020/07/17/Linux%E5%91%BD%E4%BB%A4%E4%B9%8BGREP/
https://blog.viakiba.cn/2020/07/17/Linux%E5%91%BD%E4%BB%A4%E4%B9%8BGREP/
sed
https://blog.viakiba.cn/2020/07/17/Linux%E5%91%BD%E4%BB%A4%E4%B9%8BSED/
https://blog.viakiba.cn/2020/07/17/Linux%E5%91%BD%E4%BB%A4%E4%B9%8BSED/
screen
lsof
rsync
scp
..........
详细汇总
网络管理
网络接口相关
ifconfig:查看网络接口信息
ifup/ifdown:开启或关闭接口
临时配置相关
route命令:可以临时地设置内核路由表
hostname命令:可以临时地修改主机名
sysctl命令:可以临时地开启内核的包转发
ifconfig命令:可以临时地设置网络接口的IP参数
网络检测的常用工具:
ifconfig 检测网络接口配置
route 检测路由配置
ping 检测网络连通性
netstat 查看网络状态
lsof 查看指定IP 和/或 端口的进程的当前运行情况
host/dig/nslookup 检测DNS解析
traceroute 检测到目的主机所经过的路由器
tcpdump 显示本机网络流量的状态
安装软件
yum
rpm
wget
管理用户
用户管理
useradd
添加用户
usermod
修改用户
userdel
删除用户
组管理
groupadd
添加组
groupmod
修改组
groupdel
删除组
批量管理用户:
成批添加/更新一组账户:newusers
成批更新用户的口令:chpasswd
组成员管理:
向标准组中添加用户
gpasswd -a <用户账号名> <组账号名>
usermod -G <组账号名> <用户账号名>
从标准组中删除用户
gpasswd -d <用户账号名> <组账号名>
口令管理
口令时效设置:
修改 /etc/login.defs 的相关配置参数
口令维护(禁用、恢复和删除用户口令):
passwd
设置已存在用户的口令时效:
change
切换用户
su
sudo
用户相关的命令:
id:显示用户当前的uid、gid和用户所属的组列表
groups:显示指定用户所属的组列表
whoami:显示当前用户的名称
w/who:显示登录用户及相关信息
newgrp:用于转换用户的当前组到指定的组账号,用户必须属于该组才可以正确执行该命令
查看文件
cat
查看文本文件内容
more
可以分页看
less
不仅可以分页,还可以方便地搜索,回翻等操作
tail -10
查看文件的尾部的10行
head -20
查看文件的头部20行
文件和目录的操作
ls
显示文件和目录列表
cd
切换目录
pwd
显示当前工作目录
mkdir
创建目录
rmdir
删除空目录
touch
生成一个空文件或更改文件的时间
cp
复制文件或目录
mv
移动文件或目录、文件或目录改名
rm
删除文件或目录
ln
建立链接文件
find
查找文件
file/stat
查看文件类型或文件属性信息
echo
把内容重定向到指定的文件中 ,有则打开,无则创建
管道命令 |
将前面的结果给后面的命令,例如:`ls -la | wc `,将ls的结果加油wc命令来统计字数
重定向 > 是覆盖模式,>> 是追加模式
例如:`echo "Java3y,zhen de hen xihuan ni" > qingshu.txt `把左边的输出放到右边的文件里去
进程管理
ps:查找出进程的信息
nice和renice:调整进程的优先级
kill:杀死进程
free:查看内存使用状况
top :查看实时刷新的系统进程信息
作业管理
jobs:列举作业号码和名称
bg: 在后台恢复运行
fg:在前台恢复运行
ctrl+z:暂时停止某个进程
自动化任务
at
cron
管理守护进程
chkconfig
service
ntsysv
打包和压缩文件
压缩
gzip filename
bzip2 filename
tar -czvf filename
解压
gzip -d filename.gz
bzip2 -d filename.bz2
tar -xzvf filename.tar.gz
grep+正则表达式
grep -n mystr myfile
在文件 myfile 中查找包含字符串 mystr的行
grep '^[a-zA-Z]' myfile
显示 myfile 中第一个字符为字母的所有行
Vi编辑器
普通模式
G 用于直接跳转到文件尾
ZZ 用于存盘退出Vi
ZQ 用于不存盘退出Vi
/和? 用于查找字符串
n 继续查找下一个
yy 复制一行
p 粘帖在下一行,P粘贴在前一行
dd 删除一行文本
u 取消上一次编辑操作(undo)
插入模式
使用i或a或o进去插入模式
使用esc返回普通模式
命令行模式
w 保存当前编辑文件,但并不退出
w newfile 存为另外一个名为 “newfile” 的文件
wq 用于存盘退出Vi
q! 用于不存盘退出Vi
q 用于直接退出Vi (未做修改)
设置Vi环境
set autoindent 缩进,常用于程序的编写
set noautoindent 取消缩进
set number 在编辑文件时显示行号
set tabstop=value 设置显示制表符的空格字符个数
set 显示设置的所有选项
vim常用命令总结
权限管理
改变文件或目录的权限:chmod
改变文件或目录的属主(所有者):chown
改变文件或目录所属的组:chgrp
设置文件的缺省生成掩码:umask
文件扩展属性
显示扩展属性:lsattr [-adR] [文件|目录]
修改扩展属性:chattr [-R] [[-+=][属性]] <文件|目录>
修改扩展属性:chattr [-R] [[-+=][属性]] <文件|目录>
查看linux环境
查看linux内核版本
查看GCC版本gcc -v
查看glibc版本ldd --version
查看发行版信息cat /etc/redhat-release
Linux命令速查
Linux命令大全
Linux关闭开启防火墙
数据库
事务的特性(ACID)
原子性。一个事务中的操作要么全部成功,要么全部失败。
持久性。永久保存在数据库中。
一致性。总是从一个一致性的状态转换到另一个一致性的状态
隔离性。一个事务的修改在提交前,其他事务是感知不到的
持久性。永久保存在数据库中。
一致性。总是从一个一致性的状态转换到另一个一致性的状态
隔离性。一个事务的修改在提交前,其他事务是感知不到的
事务的隔离级别
未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)
可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读【RR 的核心思想是:ReadView 创建以后直到事务提交,都不会再次重新生成。】
串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞
--实现rc和rr的区别是readView的生成时机(readview是否每次创建),rr的区别在于,readview是一开始就创建的,所以m_ids里面包含另外一个事务的,通过undo log链条可以找到最开始的版本,所以rr隔离级别下事务不会相互干扰
提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)
可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读【RR 的核心思想是:ReadView 创建以后直到事务提交,都不会再次重新生成。】
串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞
--实现rc和rr的区别是readView的生成时机(readview是否每次创建),rr的区别在于,readview是一开始就创建的,所以m_ids里面包含另外一个事务的,通过undo log链条可以找到最开始的版本,所以rr隔离级别下事务不会相互干扰
MYSQL
日志
redo log
属于MySQL存储引擎InnoDB的事务日志,采用固定大小,循环写入的格式,当redo log写满之后,重新从头开始如此循环写,形成一个环状
为了解决宕机后,Buffer Pool数据丢失而存在的
undo log
于MySQL存储引擎InnoDB的事务日志,起到回滚的作用,它是保证事务原子性的关键。记录的是数据修改前的状态,在数据修改的流程中,同时会记录一条与当前操作相反的逻辑日志到undo log中
为了解决回滚问题
同一个事物内的一条记录被多次修改,那是不是每次都要把数据修改前的状态都写入undo log?
不会,undo log只负责记录事务开始前要修改数据的原始版本,当我们再次对这行数据进行修改,所产生的修改记录会写入到redo log,undo log负责完成回滚,redo log负责完成前滚
为了解决回滚问题
同一个事物内的一条记录被多次修改,那是不是每次都要把数据修改前的状态都写入undo log?
不会,undo log只负责记录事务开始前要修改数据的原始版本,当我们再次对这行数据进行修改,所产生的修改记录会写入到redo log,undo log负责完成回滚,redo log负责完成前滚
bin log
bin log是一种数据库Server层(和什么引擎无关),以二进制形式存储在磁盘中的逻辑日志。bin log记录了数据库所有DDL和DML操作(不包含 SELECT 和 SHOW等命令,因为这类操作对数据本身并没有修改)
HOW VARIABLES LIKE 'log_bin' #看bin log 是否开启
show binary logs # 查看bin log文件名和大小
show variables like 'expire_logs_days' || SET GLOBAL expire_logs_days=30; #查看日志过期时间和设置过期时间
show binary logs # 查看bin log文件名和大小
show variables like 'expire_logs_days' || SET GLOBAL expire_logs_days=30; #查看日志过期时间和设置过期时间
主从同步
子主题
用户在主库master执行DDL和DML操作,修改记录顺序写入bin log;
从库slave的I/O线程连接上Master,并请求读取指定位置position的日志内容;
Master收到从库slave请求后,将指定位置position之后的日志内容,和主库bin log文件的名称以及在日志中的位置推送给从库;
slave的I/O线程接收到数据后,将接收到的日志内容依次写入到relay log文件最末端,并将读取到的主库bin log文件名和位置position记录到master-info文件中,以便在下一次读取用;
slave的SQL线程检测到relay log中内容更新后,读取日志并解析成可执行的SQL语句,这样就实现了主从库的数据一致;
从库slave的I/O线程连接上Master,并请求读取指定位置position的日志内容;
Master收到从库slave请求后,将指定位置position之后的日志内容,和主库bin log文件的名称以及在日志中的位置推送给从库;
slave的I/O线程接收到数据后,将接收到的日志内容依次写入到relay log文件最末端,并将读取到的主库bin log文件名和位置position记录到master-info文件中,以便在下一次读取用;
slave的SQL线程检测到relay log中内容更新后,读取日志并解析成可执行的SQL语句,这样就实现了主从库的数据一致;
和redo log区别
层次不同:redo log 是InnoDB存储引擎实现的,bin log 是MySQL的服务器层实现的,但MySQL数据库中的任何存储引擎对于数据库的更改都会产生bin log。
作用不同:redo log 用于碰撞恢复(crash recovery),保证MySQL宕机也不会影响持久性;bin log 用于时间点恢复(point-in-time recovery),保证服务器可以基于时间点恢复数据和主从复制。
内容不同:redo log 是物理日志,内容基于磁盘的页Page;bin log的内容是二进制,可以根据binlog_format参数自行设置。
写入方式不同:redo log 采用循环写的方式记录;binlog 通过追加的方式记录,当文件大小大于给定值后,后续的日志会记录到新的文件上。
刷盘时机不同:bin log在事务提交时写入;redo log 在事务开始时即开始写入。
relay log
relay log日志文件具有与bin log日志文件相同的格式,从上边MySQL主从复制的流程可以看出,relay log起到一个中转的作用,slave先从主库master读取二进制日志数据,写入从库本地,后续再异步由SQL线程读取解析relay log为对应的SQL命令执行。
slow query log
用来记录在 MySQL 中执行时间超过指定时间的查询语句,在 SQL 优化过程中会经常使用到。通过慢查询日志,我们可以查找出哪些查询语句的执行效率低,耗时严重。
SHOW VARIABLES LIKE 'slow_query%' #查看是否开启慢查询,以及慢查询日志路径
SET GLOBAL slow_query_log=ON; #设置慢查询开启
SHOW VARIABLES LIKE 'long_query_time' #查看慢查询记录的时间阈值
SET GLOBAL long_query_time=0.001; #设置慢查询记录的时间阈值
SHOW VARIABLES LIKE 'slow_query%' #查看是否开启慢查询,以及慢查询日志路径
SET GLOBAL slow_query_log=ON; #设置慢查询开启
SHOW VARIABLES LIKE 'long_query_time' #查看慢查询记录的时间阈值
SET GLOBAL long_query_time=0.001; #设置慢查询记录的时间阈值
general query log
一般查询日志(general query log):用来记录用户的所有操作,包括客户端何时连接了服务器、客户端发送的所有SQL以及其他事件,比如 MySQL 服务启动和关闭等等。MySQL服务器会按照它接收到语句的先后顺序写入日志文件。
show variables like 'general_log'
SET GLOBAL general_log=on
SET GLOBAL general_log=on
error log
主要记录 MySQL 服务器每次启动和停止的时间以及诊断和出错信息。
SHOW VARIABLES LIKE 'log_error'
幻读和不可重复度的区别
脏读是指在一个事务处理过程里读取了另一个事务未提交的数据。
不可重复读是指在同一个事务内,两次相同的查询返回了不同的结果【事务ABC,A开启事务未做操作,B进行了修改并提交,A读取到是B改了后的值,这时C又进行了修改并提交,A此时拿到的B值和最新不一样】
幻读也是指当事务不独立执行时,插入或者删除另一个事务当前影响的数据而发生的一种类似幻觉的现象【指前后读取到的记录的数量不一样】。
前者偏重于修改,后者偏重于插入和删除
不可重复读是指在同一个事务内,两次相同的查询返回了不同的结果【事务ABC,A开启事务未做操作,B进行了修改并提交,A读取到是B改了后的值,这时C又进行了修改并提交,A此时拿到的B值和最新不一样】
幻读也是指当事务不独立执行时,插入或者删除另一个事务当前影响的数据而发生的一种类似幻觉的现象【指前后读取到的记录的数量不一样】。
前者偏重于修改,后者偏重于插入和删除
ReadView
在每个事务开启的时候都会创建一个 ReadView 视图,作用就是用来记录每个事务中的操作的一些 Undo Log 记录
他里面涉及到几个字段。分别是:m_ids、min_trx_id、max_trx_id、creator_trx_id。他们的具体含义如下:
1.m_ids:用于记录活跃中的事务的 ID;
2.min_trx_id:当前活跃的事务中的最小的事务 ID;
3.max_trx_id:下一个即将要生成的事务 ID。注意这里并不是指的最大的事务 ID,这个事务一定是当前的 m_ids 中不存在的。(事务 ID 的生成是递增的);
4.creator_trx_id:当前活跃事务的 ID;
他里面涉及到几个字段。分别是:m_ids、min_trx_id、max_trx_id、creator_trx_id。他们的具体含义如下:
1.m_ids:用于记录活跃中的事务的 ID;
2.min_trx_id:当前活跃的事务中的最小的事务 ID;
3.max_trx_id:下一个即将要生成的事务 ID。注意这里并不是指的最大的事务 ID,这个事务一定是当前的 m_ids 中不存在的。(事务 ID 的生成是递增的);
4.creator_trx_id:当前活跃事务的 ID;
锁
类别
Record Lock(记录锁),记录锁锁定的是索引记录
Gap Lock(间隙锁),他指的是在索引记录之间的间隙上的锁,或者在第一个索引记录之前或最后一个索引记录之后的间隙上的锁。
Next-Key Lock 是索引记录上的记录锁和索引记录之前间隙上的间隙锁的组合
加锁原则
原则 1:加锁的基本单位是 next-key lock。是一个前开后闭区间。
原则 2:查找过程中访问到的对象才会加锁。
优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
索引
结构图
子主题
为什么选择B+树作为索引结构
为什么不使用哈希结构?
我们知道哈希结构,类似k-v结构,也就是,key和value是一对一关系。它用于等值查询还可以,但是范围查询它是无能为力的哦。
为什么不使用二叉树呢?
如果二叉树只有右子树,将会特殊化为一个链表,相当于全表扫描。那么还要索引干嘛呀?因此,一般二叉树不适合作为索引结构
为什么不使用平衡二叉树呢?
平衡二叉树插入或者更新是,需要左旋右旋维持平衡,维护代价大
如果数量多的话,树的高度会很高。因为数据是存在磁盘的,以它作为索引结构,每次从磁盘读取一个节点,操作IO的次数就多啦。
为什么不使用B树呢?
B+树非叶子节点上是不存储数据的,仅存储键值,而B树节点中不仅存储键值,也会存储数据。innodb中页的默认大小是16KB,如果不存储数据,那么就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,如此一来我们查找数据进行磁盘的IO次数有会再次减少,数据查询的效率也会更快。
B+树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的,链表连着的。那么B+树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。
B树在提高了IO性能的同时并没有解决元素遍历的我效率低下的问题,正是为了解决这个问题,B+树应用而生。B+树只需要去遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作或者说效率太低。
我们知道哈希结构,类似k-v结构,也就是,key和value是一对一关系。它用于等值查询还可以,但是范围查询它是无能为力的哦。
为什么不使用二叉树呢?
如果二叉树只有右子树,将会特殊化为一个链表,相当于全表扫描。那么还要索引干嘛呀?因此,一般二叉树不适合作为索引结构
为什么不使用平衡二叉树呢?
平衡二叉树插入或者更新是,需要左旋右旋维持平衡,维护代价大
如果数量多的话,树的高度会很高。因为数据是存在磁盘的,以它作为索引结构,每次从磁盘读取一个节点,操作IO的次数就多啦。
为什么不使用B树呢?
B+树非叶子节点上是不存储数据的,仅存储键值,而B树节点中不仅存储键值,也会存储数据。innodb中页的默认大小是16KB,如果不存储数据,那么就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,如此一来我们查找数据进行磁盘的IO次数有会再次减少,数据查询的效率也会更快。
B+树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的,链表连着的。那么B+树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。
B树在提高了IO性能的同时并没有解决元素遍历的我效率低下的问题,正是为了解决这个问题,B+树应用而生。B+树只需要去遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作或者说效率太低。
类型
非聚簇索引
将数据与索引分开存储,索引结构的叶子节点指向了数据对应的位置
索引顺序与数据物理排列顺序无关
索引顺序与数据物理排列顺序无关
聚簇索引/主键索引
将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据
聚簇索引的顺序就是数据的物理存储顺序;默认会选取主键作为主键索引
聚簇索引的顺序就是数据的物理存储顺序;默认会选取主键作为主键索引
主键索引(非主键索引称为二级索引)
即主索引,根据主键pk_clolum(length)建立索引,不允许重复,不允许空值;
ALTER TABLE 'table_name' ADD PRIMARY KEY pk_index('col');
ALTER TABLE 'table_name' ADD PRIMARY KEY pk_index('col');
唯一索引
用来建立索引的列的值必须是唯一的,允许空值
ALTER TABLE 'table_name' ADD UNIQUE index_name('col');
ALTER TABLE 'table_name' ADD UNIQUE index_name('col');
普通索引
一个索引只包含一个列,一个表可以有多个单列索引
ALTER TABLE 'table_name' ADD INDEX index_name('col1');
ALTER TABLE 'table_name' ADD INDEX index_name('col1');
复合索引(组合索引)
用多个列组合构建的索引,这多个列中的值不允许有空值
ALTER TABLE 'table_name' ADD INDEX index_name('col1','col2','col3');
ALTER TABLE 'table_name' ADD INDEX index_name('col1','col2','col3');
覆盖索引
只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快。
explain的输出结果Extra字段为Using index时,能够触发索引覆盖。
explain的输出结果Extra字段为Using index时,能够触发索引覆盖。
回表,指查询时一些字段值拿不到,需要到主键索引B+树再查一次。
索引下推
如果没有索引下推优化(或称ICP优化),当进行索引查询时,首先根据索引来查找记录,然后再根据where条件来过滤记录;在支持ICP优化后,MySQL会在取出索引的同时,判断是否可以进行where条件过滤再进行索引查询,也就是说提前执行where的部分过滤操作,在某些场景下,可以大大减少回表次数,从而提升整体性能。Mysql5.6之后才有的功能
前缀索引
对文本的前几个字符(具体是几个字符在建立索引时指定)建立索引,这样建立起来的索引更小,所以查询更快。
ALTER table 'table_name' add index title_pre(col(100))
ALTER table 'table_name' add index title_pre(col(100))
最左前缀索引
查询条件包含在了组合索引中,比如存在组合索引(a,b),查询到满足 a 的记录后会直接在索引内部判断 b 是否满足,减少回表次数。同时,如果查询的列恰好包含在组合索引中,即为覆盖索引,无需回表。
索引失效常见原因
1.where 中使用 != 或 <> 或 or 或表达式或函数(左侧)
2.like 语句 % 开头
3.字符串未加’’
4.索引字段区分度过低,如性别
5.未匹配最左前缀
2.like 语句 % 开头
3.字符串未加’’
4.索引字段区分度过低,如性别
5.未匹配最左前缀
函数操作
当在 查询 where = 左侧使用表达式或函数时,如字段 A 为字符串型且有索引, 有 where length(a) = 6查询,这时传递一个 6 到 A 的索引树,不难想象在树的第一层就迷路了。
隐式转换
隐式类型转换和隐式字符编码转换也会导致这个问题。
隐式类型转换对于 JOOQ 这种框架来说一般倒不会出现。
隐式字符编码转换在连表查询时倒可能出现,即连表字段的类型相同但字符编码不同
隐式类型转换对于 JOOQ 这种框架来说一般倒不会出现。
隐式字符编码转换在连表查询时倒可能出现,即连表字段的类型相同但字符编码不同
破坏了有序性
破坏了有序性
怎么建立并用好索引
索引下推:性别字段不适合建索引,但确实存在查询场景怎么办?如果是多条件查询,可以建立联合索引利用该特性优化。
覆盖索引:也是联合索引,查询需要的信息在索引里已经包含了,就不会再回表了。
前缀索引:对于字符串,可以只在前 N 位添加索引,避免不必要的开支。假如的确需要如关键字查询,那交给更合适的如 ES 或许更好。
1、比较运算符能用 “=”就不用“<>”,“=”增加了索引的使用几率。
2、明知只有一条查询结果,那请使用 “LIMIT 1”,“LIMIT 1”可以避免全表扫描,找到对应结果就不会再继续扫描了。
3、为列选择合适的数据类型,能用TINYINT就不用SMALLINT,能用SMALLINT就不用INT,道理你懂的,磁盘和内存消耗越小越好嘛。
4、将大的DELETE,UPDATE or INSERT 查询变成多个小查询,能写一个几十行、几百行的SQL语句是不是显得逼格很高?然而,为了达到更好的性能以及更好的数据控制,你可以将他们变成多个小查询。
5、使用UNION ALL 代替 UNION,如果结果集允许重复的话,因为 UNION ALL 不去重,效率高于 UNION。
6、为获得相同结果集的多次执行,请保持SQL语句前后一致,这样做的目的是为了充分利用查询缓冲。
7、尽量避免使用 “SELECT *”,如果不查询表中所有的列,尽量避免使用 SELECT *,因为它会进行全表扫描,不能有效利用索引,增大了数据库服务器的负担,以及它与应用程序客户端之间的网络IO开销。
8、WHERE 子句里面的列尽量被索引,只是“尽量”哦,并不是说所有的列。因地制宜,根据实际情况进行调整,因为有时索引太多也会降低性能。
9、JOIN 子句里面的列尽量被索引,体会下尽量二字的精髓
10、ORDER BY 的列尽量被索引
11、My sql EXPLAIN 检查索引使用情况以及扫描的行
2、明知只有一条查询结果,那请使用 “LIMIT 1”,“LIMIT 1”可以避免全表扫描,找到对应结果就不会再继续扫描了。
3、为列选择合适的数据类型,能用TINYINT就不用SMALLINT,能用SMALLINT就不用INT,道理你懂的,磁盘和内存消耗越小越好嘛。
4、将大的DELETE,UPDATE or INSERT 查询变成多个小查询,能写一个几十行、几百行的SQL语句是不是显得逼格很高?然而,为了达到更好的性能以及更好的数据控制,你可以将他们变成多个小查询。
5、使用UNION ALL 代替 UNION,如果结果集允许重复的话,因为 UNION ALL 不去重,效率高于 UNION。
6、为获得相同结果集的多次执行,请保持SQL语句前后一致,这样做的目的是为了充分利用查询缓冲。
7、尽量避免使用 “SELECT *”,如果不查询表中所有的列,尽量避免使用 SELECT *,因为它会进行全表扫描,不能有效利用索引,增大了数据库服务器的负担,以及它与应用程序客户端之间的网络IO开销。
8、WHERE 子句里面的列尽量被索引,只是“尽量”哦,并不是说所有的列。因地制宜,根据实际情况进行调整,因为有时索引太多也会降低性能。
9、JOIN 子句里面的列尽量被索引,体会下尽量二字的精髓
10、ORDER BY 的列尽量被索引
11、My sql EXPLAIN 检查索引使用情况以及扫描的行
不要对索引字段做函数操作
对于确定的、写多读少的表或者频繁更新的字段都应该考虑索引的维护成本。
优化
实例
分页优化
select * from table where type = 2 and level = 9 order by id asc limit 190289,10;
方案:
延迟关联
先通过where条件提取出主键,在将该表与原数据表关联,通过主键id提取数据行,而不是通过原来的二级索引提取数据行
例如:select a.* from table a, (select id from table where type = 2 and level = 9 order by id asc limit 190289,10 ) b where a.id = b.id
书签方式
书签方式说白了就是找到limit第一个参数对应的主键值,再根据这个主键值再去过滤并limit
例如:
select * from table where id > (select * from table where type = 2 and level = 9 order by id asc limit 190289, 1) limit 10;
方案:
延迟关联
先通过where条件提取出主键,在将该表与原数据表关联,通过主键id提取数据行,而不是通过原来的二级索引提取数据行
例如:select a.* from table a, (select id from table where type = 2 and level = 9 order by id asc limit 190289,10 ) b where a.id = b.id
书签方式
书签方式说白了就是找到limit第一个参数对应的主键值,再根据这个主键值再去过滤并limit
例如:
select * from table where id > (select * from table where type = 2 and level = 9 order by id asc limit 190289, 1) limit 10;
索引优化
建立覆盖索引
select name from test where city='上海'
alter table test add index idx_city_name (city, name);
姓名加上索引,避免回表
select name from test where city='上海'
alter table test add index idx_city_name (city, name);
姓名加上索引,避免回表
避免在 where 查询条件中使用 != 或者 <> 操作符
SQL中,不等于操作符会导致查询引擎放弃索引索引,引起全表扫描,即使比较的字段上有索引
解决方法:通过把不等于操作符改成or,可以使用索引,避免全表扫描
例如,把column<>’aaa’,改成column>’aaa’ or column<’aaa’,就可以使用索引了
适当使用前缀索引
MySQL 是支持前缀索引的,也就是说我们可以定义字符串的一部分来作为索引
我们知道索引越长占用的磁盘空间就越大,那么在相同数据页中能放下的索引值也就越少,这就意味着搜索索引需要的查询时间也就越长,进而查询的效率就会降低,所以我们可以适当的选择使用前缀索引,以减少空间的占用和提高查询效率
比如,邮箱的后缀都是固定的“@xxx.com”,那么类似这种后面几位为固定值的字段就非常适合定义为前缀索引
alter table test add index index2(email(6));
使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本
需要注意的是,前缀索引也存在缺点,MySQL无法利用前缀索引做order by和group by 操作,也无法作为覆盖索引
小表驱动大表
我们要尽量使用小表驱动大表的方式进行查询,也就是如果 B 表的数据小于 A 表的数据,那执行的顺序就是先查 B 表再查 A 表,具体查询语句如下:
select name from A where id in (select id from B);
select name from A where id in (select id from B);
优化子查询
尽量使用 Join 语句来替代子查询,因为子查询是嵌套查询,而嵌套查询会新创建一张临时表,而临时表的创建与销毁会占用一定的系统资源以及花费一定的时间,同时对于返回结果集比较大的子查询,其对查询性能的影响更大
隐式类型转换导致所有失效
select * from test where skuId=123456
skuId这个字段上有索引,但是explain的结果却显示这条语句会全表扫描
原因在于skuId的字符类型是varchar(32),比较值却是整型,故需要做类型转换
buffer pool
相关命令
# 查看大小,默认128M
mysql> show global variables like 'innodb_buffer_pool_size';
+-------------------------+-----------+
| Variable_name | Value |
+-------------------------+-----------+
| innodb_buffer_pool_size | 134217728 |
+-------------------------+-----------+
1 row in set (0.01 sec)
# 设置大小
mysql> set global innodb_buffer_pool_size = 536870912;
Query OK, 0 rows affected (0.01 sec)
mysql> show global variables like 'innodb_buffer_pool_size';
+-------------------------+-----------+
| Variable_name | Value |
+-------------------------+-----------+
| innodb_buffer_pool_size | 536870912 |
+-------------------------+-----------+
1 row in set (0.01 sec)
如何知道设置大小是否合理
show status like 'Innodb_buffer_pool_%';
Innodb_buffer_pool_read_requests表示读请求的次数。
Innodb_buffer_pool_reads 表示从物理磁盘中读取数据的请求次数。
所以buffer pool的命中率就可以这样得到:buffer pool 命中率 = 1 - (Innodb_buffer_pool_reads/Innodb_buffer_pool_read_requests) * 1
一般都是99%,低于可以考虑加大,也可以把这个做到监控里面
Innodb_buffer_pool_read_requests表示读请求的次数。
Innodb_buffer_pool_reads 表示从物理磁盘中读取数据的请求次数。
所以buffer pool的命中率就可以这样得到:buffer pool 命中率 = 1 - (Innodb_buffer_pool_reads/Innodb_buffer_pool_read_requests) * 1
一般都是99%,低于可以考虑加大,也可以把这个做到监控里面
慢查询处理
profiling
mysql> set profiling=ON;
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> show variables like 'profiling';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| profiling | ON |
+---------------+-------+
1 row in set (0.00 sec)
#这些SQL语句的执行时间都会被记录下来,此时你想查看有哪些语句被记录下来了,可以执行 show profiles;
mysql> show profiles;
+----------+------------+---------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+---------------------------------------------------+
| 1 | 0.06811025 | select * from user where age>=60 |
| 2 | 0.00151375 | select * from user where gender = 2 and age = 80 |
| 3 | 0.00230425 | select * from user where gender = 2 and age = 60 |
| 4 | 0.00070400 | select * from user where gender = 2 and age = 100 |
| 5 | 0.07797650 | select * from user where age!=60 |
+----------+------------+---------------------------------------------------+
5 rows in set, 1 warning (0.00 sec)
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> show variables like 'profiling';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| profiling | ON |
+---------------+-------+
1 row in set (0.00 sec)
#这些SQL语句的执行时间都会被记录下来,此时你想查看有哪些语句被记录下来了,可以执行 show profiles;
mysql> show profiles;
+----------+------------+---------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+---------------------------------------------------+
| 1 | 0.06811025 | select * from user where age>=60 |
| 2 | 0.00151375 | select * from user where gender = 2 and age = 80 |
| 3 | 0.00230425 | select * from user where gender = 2 and age = 60 |
| 4 | 0.00070400 | select * from user where gender = 2 and age = 100 |
| 5 | 0.07797650 | select * from user where age!=60 |
+----------+------------+---------------------------------------------------+
5 rows in set, 1 warning (0.00 sec)
#查看这条SQL语句的具体耗时,那么可以执行以下的命令。
mysql> show profile for query 1;
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000074 |
| checking permissions | 0.000010 |
| Opening tables | 0.000034 |
| init | 0.000032 |
| System lock | 0.000027 |
| optimizing | 0.000020 |
| statistics | 0.000058 |
| preparing | 0.000018 |
| executing | 0.000013 |
| Sending data | 0.067701 |
| end | 0.000021 |
| query end | 0.000015 |
| closing tables | 0.000014 |
| freeing items | 0.000047 |
| cleaning up | 0.000027 |
+----------------------+----------+
15 rows in set, 1 warning (0.00 sec)
分库分表
分库分表后的分页查询
全局视野法
如果要获取第N页的数据(每页S条数据),则将每一个子库的前N页(offset 0,limit N*S)的所有数据都先查出来(有筛选条件或排序规则的话都包含),然后将各个子库的结果合并起来之后,再做查询下top S(可不用带上相同的筛选条件,但还要带上排序规则)即可得出最终结果,这种方式类似es分页的逻辑。
优点: 数据准确,可以跳页
缺点: 深度分页时的性能差,即随着分页参数增加,网络传输数据量越来越大,每个子表每次需要查询的数据越多,性能也越慢
禁止跳页查询
如果要获取第N页的数据,第一页时,是和全局视野法一致,但第二页开始后,需要在每一个子库查询时,加上可以排除上一页的过滤条件(如按时间排序时,获取上一页的最大时间后,需要加上time > ${maxTime_lastPage}的条件;如果没有排序规则,由于是默认主键id的排序规则,也可加上 id > ${maxId_lastPage}的条件),然后再limit S,即可获取各个子库的结果,之后再合并后top S即可得到最终结果。在类似app中列表下拉的场景中,业务上可以禁止跳页查询,此时可以使用这种方式。
优点: 数据准确,性能良好
缺点: 不能跳页
IO瓶颈
第一种:磁盘读IO瓶颈,热点数据太多,数据库缓存放不下,每次查询时会产生大量的IO,降低查询速度 -> 分库和垂直分表。
第二种:网络IO瓶颈,请求的数据太多,网络带宽不够 -> 分库。
CPU瓶颈
第一种:SQL问题,如SQL中包含join,group by,order by,非索引字段条件查询等,增加CPU运算的操作 -> SQL优化,建立合适的索引,在业务Service层进行业务计算。
第二种:单表数据量太大,查询时扫描的行太多,SQL效率低,CPU率先出现瓶颈 -> 水平分表。
第二种:单表数据量太大,查询时扫描的行太多,SQL效率低,CPU率先出现瓶颈 -> 水平分表。
水平分库
概念
以字段为依据,按照一定策略(hash、range等),将一个库中的数据拆分到多个库中。
结果
每个库的结构都一样;
每个库的数据都不一样,没有交集;
所有库的并集是全量数据;
场景
系统绝对并发量上来了,分表难以根本上解决问题,并且还没有明显的业务归属来垂直分库。
分析
库多了,io和cpu的压力自然可以成倍缓解。
水平分表
概念
以字段为依据,按照一定策略(hash、range等),将一个表中的数据拆分到多个表中。
结果
每个表的结构都一样;
每个表的数据都不一样,没有交集;
所有表的并集是全量数据;
每个表的数据都不一样,没有交集;
所有表的并集是全量数据;
场景
系统绝对并发量并没有上来,只是单表的数据量太多,影响了SQL效率,加重了CPU负担,以至于成为瓶颈。推荐:一次SQL查询优化原理分析
分析
表的数据量少了,单次SQL执行效率高,自然减轻了CPU的负担。
垂直分库
概念
以表为依据,按照业务归属不同,将不同的表拆分到不同的库中。
结果
每个库的结构都不一样;
每个库的数据也不一样,没有交集;
所有库的并集是全量数据;
每个库的数据也不一样,没有交集;
所有库的并集是全量数据;
场景
系统绝对并发量上来了,并且可以抽象出单独的业务模块。
分析
到这一步,基本上就可以服务化了。例如,随着业务的发展一些公用的配置表、字典表等越来越多,这时可以将这些表拆到单独的库中,甚至可以服务化。再有,随着业务的发展孵化出了一套业务模式,这时可以将相关的表拆到单独的库中,甚至可以服务化。
垂直分表
概念
以字段为依据,按照字段的活跃性,将表中字段拆到不同的表(主表和扩展表)中。
结果
每个表的结构都不一样;
每个表的数据也不一样,一般来说,每个表的字段至少有一列交集,一般是主键,用于关联数据;
所有表的并集是全量数据;
每个表的数据也不一样,一般来说,每个表的字段至少有一列交集,一般是主键,用于关联数据;
所有表的并集是全量数据;
场景
系统绝对并发量并没有上来,表的记录并不多,但是字段多,并且热点数据和非热点数据在一起,单行数据所需的存储空间较大。以至于数据库缓存的数据行减少,查询时会去读磁盘数据产生大量的随机读IO,产生IO瓶颈。
分析
可以用列表页和详情页来帮助理解。垂直分表的拆分原则是将热点数据(可能会冗余经常一起查询的数据)放在一起作为主表,非热点数据放在一起作为扩展表。这样更多的热点数据就能被缓存下来,进而减少了随机读IO。拆了之后,要想获得全部数据就需要关联两个表来取数据。
但记住,千万别用join,因为join不仅会增加CPU负担并且会讲两个表耦合在一起(必须在一个数据库实例上)。关联数据,应该在业务Service层做文章,分别获取主表和扩展表数据然后用关联字段关联得到全部数据。
分库分表工具
sharding-sphere:jar,前身是sharding-jdbc;
TDDL:jar,Taobao Distribute Data Layer;
Mycat:中间件。
分布式事务
解决方案
XA协议、TCC和Saga事务模型、本地消息表、事务消息和阿里开源的Seata。
数据库事务有ACID四个特性
A(Atomicity)原子:指单个事务中的操作要不都执行,要不都不执行
C(Consistency)一致:指事务前后数据的完整性必须保持一致
I(Isolation)隔离:指多个事务对数据可见性的规则
D(Durability)持久:指事务提交后,就会被永久存储下来
C(Consistency)一致:指事务前后数据的完整性必须保持一致
I(Isolation)隔离:指多个事务对数据可见性的规则
D(Durability)持久:指事务提交后,就会被永久存储下来
2PC/3PC
二阶段提交(英语:Two-phase Commit)是指在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法。通常,二阶段提交也被称为是一种协议(Protocol)。
3PC即三阶段提交,它比2PC多了一个阶段,即把原来2PC的准备阶段拆分成CanCommit和PreCommit两个阶段,同时引入超时机制来解决2PC的同步阻塞问题。
3PC即三阶段提交,它比2PC多了一个阶段,即把原来2PC的准备阶段拆分成CanCommit和PreCommit两个阶段,同时引入超时机制来解决2PC的同步阻塞问题。
子主题
子主题
XA
XA是一种基于2PC协议实现的规范。在2PC中没有明确资源是什么,以及资源是怎么提交的等等,而XA就是数据库实现2PC的规范,已知常用的支持XA的关系型数据库有Mysql、Oracle等
本地消息表
处理流程
● 事务发起方把要处理的业务事务和写消息表这两个操作放在同一个本地事务里
● 事务发起方有一个定时任务轮询消息表,把没处理的消息发送到消息中间件
● 事务被动方从消息中间件获取消息后,返回成功
● 事务发起方更新消息状态为已成功
● 事务发起方有一个定时任务轮询消息表,把没处理的消息发送到消息中间件
● 事务被动方从消息中间件获取消息后,返回成功
● 事务发起方更新消息状态为已成功
子主题
分析
把业务处理和写消息表放在同一个事务是为了失败/异常后可以同时回滚
为什么不直接发消息,而是先写消息表?试想,如果发送消息超时了,即不确定消息中间件收到消息没,那么你是重试还是抛异常回滚事务呢?回滚是不行的,因为可能消息中间件已经收到消息,接收方收到消息后做处理,导致双方数据不一致了;重试也是不行的,因为有可能会一直重试失败,导致事务阻塞。
基于上述分析,消息的接收方是需要做幂等操作的
为什么不直接发消息,而是先写消息表?试想,如果发送消息超时了,即不确定消息中间件收到消息没,那么你是重试还是抛异常回滚事务呢?回滚是不行的,因为可能消息中间件已经收到消息,接收方收到消息后做处理,导致双方数据不一致了;重试也是不行的,因为有可能会一直重试失败,导致事务阻塞。
基于上述分析,消息的接收方是需要做幂等操作的
缺点
消息数据和业务数据耦合,消息表需要根据具体的业务场景制定,不能公用。就算可以公用消息表,对于分库的业务来说每个库都是需要消息表的。
只适用于最终一致的业务场景。例如在 A -> B场景下,在不考虑网络异常、宕机等非业务异常的情况下,A成功的话,B肯定也会成功的。
事务消息
定义
事务消息是通过消息中间件来解耦本地消息表和业务数据表,适用于所有对数据最终一致性需求的场景。现在支持事务消息的消息中间件只有RocketMQ,这个概念最早也是RocketMQ提出的。
流程
● 发起方发送半事务消息会给RocketMQ ,此时消息的状态prepare,接受方还不能拉取到此消息
● 发起方进行本地事务操作
● 发起方给RocketMQ确认提交消息,此时接受方可以消费到此消息了
● 发起方进行本地事务操作
● 发起方给RocketMQ确认提交消息,此时接受方可以消费到此消息了
子主题
异常
步骤1和3失败/异常该如何处理
RocketMQ会定期扫描还没确认的消息,回调给发送方,询问此次事务的状态,根据发送方的返回结果把这条消息进行取消还是提交确认。
可以看出事务消息的本质的借鉴了二阶段提交的思想,它跟本地消息表的做法也很像,事务消息做的事情其实就是把消息表的存储和扫描消息表这两个事情放到消息中间件来做,使得消息表和业务表解耦。
RocketMQ会定期扫描还没确认的消息,回调给发送方,询问此次事务的状态,根据发送方的返回结果把这条消息进行取消还是提交确认。
可以看出事务消息的本质的借鉴了二阶段提交的思想,它跟本地消息表的做法也很像,事务消息做的事情其实就是把消息表的存储和扫描消息表这两个事情放到消息中间件来做,使得消息表和业务表解耦。
TCC(Try-Confirm-Cancel)
核心思想
事务模型采用的是补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿操作。
相当于XA来说,TCC可以不依赖于资源管理器,即数据库,它是通过业务逻辑来控制确认和补偿操作的,所以它用了’Cancel’而非’Rollback’的字眼。它是一个应用层面的2PC。
相当于XA来说,TCC可以不依赖于资源管理器,即数据库,它是通过业务逻辑来控制确认和补偿操作的,所以它用了’Cancel’而非’Rollback’的字眼。它是一个应用层面的2PC。
三个阶段
● Try阶段,对业务资源进行检测和预留
● Confirm阶段,对Try阶段预留的资源进行确认提交,Try阶段执行成功是Confirm阶段执行成功的前提
● Cancel阶段,对Try阶段预留的资源进行撤销或释放
● Confirm阶段,对Try阶段预留的资源进行确认提交,Try阶段执行成功是Confirm阶段执行成功的前提
● Cancel阶段,对Try阶段预留的资源进行撤销或释放
子主题
缺点
● 对于Confirm和Cancel阶段失败后要完全靠业务应用自己去处理
● 每个业务都需要实现Try、Confirm、Cancel三个接口,代码量比较多
● 如果是基于现有的业务想使用TCC会比较困难。一是对于原来的接口要拆分为三个接口,入侵性比较大;二是因为要做“预留”资源的操作,有可能需要对原来的业务模型进行改造。
● 每个业务都需要实现Try、Confirm、Cancel三个接口,代码量比较多
● 如果是基于现有的业务想使用TCC会比较困难。一是对于原来的接口要拆分为三个接口,入侵性比较大;二是因为要做“预留”资源的操作,有可能需要对原来的业务模型进行改造。
Seata
Seata是一个由阿里做背书的分布式事务框架,致力于提供高性能和简单易用的分布式事务服务。Seata将为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造一站式的分布式解决方案。
数据库
关系数据库
MySql
基本语法
连接数据库
mysql -u用户名 -p密码
对库的操作
创建库
CREATE DATABASE [IF NOT EXISTS] 库名[DEFAULT] CHARACTER SET 字符名 | [DEFAULT] COLLATE 校对规则
查看库
SHOW DATABASES
SHOW CREATE DATABASE 库名【查看数据库创建时的详细信息】
删除库
DROP DATABASE [IF EXISTS] 库名
修改库
ALTER DATABASE [IF NOT EXISTS] 库名[DEFAULT] CHARACTER SET 字符名 | [DEFAULT] COLLATE 校对规则
备份库中的数据和
mysqldump -u 用户名 -p 数据库名 > 文件名.sql【window命令】
Source 文件名.sql【在库下执行】
mysql -uroot -p mydb1> c:\test.sql (window命令)
对表的操作
增加表
CREATE TABLE 表名( 列名 类型 )
修改表
ALTER TABLE 表名 ADD ( 列名 数据类型 );
ALTER TABLE 表名 MODIFY( 列名 数据类型 );
查看表
SHOW TABLES
SHOW CREATE TABLE 表名【查看表的创建细节】
DESC 表名【查看表的结构】
删除表
ALTER TABLE表名DROP(列名);
对表中数据操作
增加
INSERT INTO 表名 ( 列名..)VALUES (数据..);
修改
UPDATE 表名SET 列名=值.. , 列名=值WHERE=条件
删除
DELETE FROM 表名 WHERE=条件
TRUNCATE TABLE【先摧毁整张表,再创建表结构】
查看
SELECT 列名FROM 表名,WHERE 条件,GROUP BY 列名,HAVING BY,ORDER BY 列名
SELECT子句执行顺序
1.FROM
2.WHERE
3.GROUP BY
4. HAVING
5.SELECT
6.ORDER BY
聚集函数
AVG()
MAX()
MIN()
COUNT()
SUM()
通配符检索数据
用LIKE操作符进行过滤
%通配符,匹配0个或多个
_通配符,匹配单个字符
排序检索数据
使用ORDER BY 排序数据
按位置、列名排序
按多个列排序
DESC降序
ASC升序【默认】
分组数据
使用GROUP BY分组
使用HAVING过滤分组
WHERE过滤的是行数据
联结表
FROM字句后边不止一个表,就叫联结
内连接(等值连接)【INNER JOIN】,使用ON子句 消除笛卡尔积
外连接【包含没有相关的行】,LEFT(RIGHT,FULL) OUTER JOIN,
FROM子句跟着是两个相同的表叫自连接
索引
创建
CREATE [UNIQUE] INDEX indexName ON mytable(columnname(length));
CREATE [UNIQUE] INDEX indexName ON mytable(columnname(length));
更改
ALTER mytable ADD [UNIQUE] INDEX [indexName] ON(columnname(length));
ALTER mytable ADD [UNIQUE] INDEX [indexName] ON(columnname(length));
删除
DROP INDEX [indexName] ON mytable;
DROP INDEX [indexName] ON mytable;
查看一个表的索引信息
show index from [tableName];
show index from [tableName];
触发器
/* 触发器 */ ------------------
触发程序是与表有关的命名数据库对象,当该表出现特定事件时,将激活该对象
监听:记录的增加、修改、删除。
-- 创建触发器
CREATE TRIGGER trigger_name trigger_time trigger_event ON tbl_name FOR EACH ROW trigger_stmt
参数:
trigger_time是触发程序的动作时间。它可以是 before 或 after,以指明触发程序是在激活它的语句之前或之后触发。
trigger_event指明了激活触发程序的语句的类型
INSERT:将新行插入表时激活触发程序
UPDATE:更改某一行时激活触发程序
DELETE:从表中删除某一行时激活触发程序
tbl_name:监听的表,必须是永久性的表,不能将触发程序与TEMPORARY表或视图关联起来。
trigger_stmt:当触发程序激活时执行的语句。执行多个语句,可使用BEGIN...END复合语句结构
-- 删除
DROP TRIGGER [schema_name.]trigger_name
可以使用old和new代替旧的和新的数据
更新操作,更新前是old,更新后是new.
删除操作,只有old.
增加操作,只有new.
-- 注意
1. 对于具有相同触发程序动作时间和事件的给定表,不能有两个触发程序。
-- 字符连接函数
concat(str1,str2,...])
concat_ws(separator,str1,str2,...)
-- 分支语句
if 条件 then
执行语句
elseif 条件 then
执行语句
else
执行语句
end if;
-- 修改最外层语句结束符
delimiter 自定义结束符号
SQL语句
自定义结束符号
delimiter ; -- 修改回原来的分号
-- 语句块包裹
begin
语句块
end
-- 特殊的执行
1. 只要添加记录,就会触发程序。
2. Insert into on duplicate key update 语法会触发:
如果没有重复记录,会触发 before insert, after insert;
如果有重复记录并更新,会触发 before insert, before update, after update;
如果有重复记录但是没有发生更新,则触发 before insert, before update
3. Replace 语法 如果有记录,则执行 before insert, before delete, after delete, after insert
触发程序是与表有关的命名数据库对象,当该表出现特定事件时,将激活该对象
监听:记录的增加、修改、删除。
-- 创建触发器
CREATE TRIGGER trigger_name trigger_time trigger_event ON tbl_name FOR EACH ROW trigger_stmt
参数:
trigger_time是触发程序的动作时间。它可以是 before 或 after,以指明触发程序是在激活它的语句之前或之后触发。
trigger_event指明了激活触发程序的语句的类型
INSERT:将新行插入表时激活触发程序
UPDATE:更改某一行时激活触发程序
DELETE:从表中删除某一行时激活触发程序
tbl_name:监听的表,必须是永久性的表,不能将触发程序与TEMPORARY表或视图关联起来。
trigger_stmt:当触发程序激活时执行的语句。执行多个语句,可使用BEGIN...END复合语句结构
-- 删除
DROP TRIGGER [schema_name.]trigger_name
可以使用old和new代替旧的和新的数据
更新操作,更新前是old,更新后是new.
删除操作,只有old.
增加操作,只有new.
-- 注意
1. 对于具有相同触发程序动作时间和事件的给定表,不能有两个触发程序。
-- 字符连接函数
concat(str1,str2,...])
concat_ws(separator,str1,str2,...)
-- 分支语句
if 条件 then
执行语句
elseif 条件 then
执行语句
else
执行语句
end if;
-- 修改最外层语句结束符
delimiter 自定义结束符号
SQL语句
自定义结束符号
delimiter ; -- 修改回原来的分号
-- 语句块包裹
begin
语句块
end
-- 特殊的执行
1. 只要添加记录,就会触发程序。
2. Insert into on duplicate key update 语法会触发:
如果没有重复记录,会触发 before insert, after insert;
如果有重复记录并更新,会触发 before insert, before update, after update;
如果有重复记录但是没有发生更新,则触发 before insert, before update
3. Replace 语法 如果有记录,则执行 before insert, before delete, after delete, after insert
存储过程
/* 存储过程 */ ------------------
存储过程是一段可执行性代码的集合。相比函数,更偏向于业务逻辑。
调用:CALL 过程名
-- 注意
- 没有返回值。
- 只能单独调用,不可夹杂在其他语句中
-- 参数
IN|OUT|INOUT 参数名 数据类型
IN 输入:在调用过程中,将数据输入到过程体内部的参数
OUT 输出:在调用过程中,将过程体处理完的结果返回到客户端
INOUT 输入输出:既可输入,也可输出
-- 语法
CREATE PROCEDURE 过程名 (参数列表)
BEGIN
过程体
END
存储过程是一段可执行性代码的集合。相比函数,更偏向于业务逻辑。
调用:CALL 过程名
-- 注意
- 没有返回值。
- 只能单独调用,不可夹杂在其他语句中
-- 参数
IN|OUT|INOUT 参数名 数据类型
IN 输入:在调用过程中,将数据输入到过程体内部的参数
OUT 输出:在调用过程中,将过程体处理完的结果返回到客户端
INOUT 输入输出:既可输入,也可输出
-- 语法
CREATE PROCEDURE 过程名 (参数列表)
BEGIN
过程体
END
用户和权限管理
/* 用户和权限管理 */ ------------------
-- root密码重置
1. 停止MySQL服务
2. [Linux] /usr/local/mysql/bin/safe_mysqld --skip-grant-tables &
[Windows] mysqld --skip-grant-tables
3. use mysql;
4. UPDATE `user` SET PASSWORD=PASSWORD("密码") WHERE `user` = "root";
5. FLUSH PRIVILEGES;
用户信息表:mysql.user
-- 刷新权限
FLUSH PRIVILEGES;
-- 增加用户
CREATE USER 用户名 IDENTIFIED BY [PASSWORD] 密码(字符串)
- 必须拥有mysql数据库的全局CREATE USER权限,或拥有INSERT权限。
- 只能创建用户,不能赋予权限。
- 用户名,注意引号:如 'user_name'@'192.168.1.1'
- 密码也需引号,纯数字密码也要加引号
- 要在纯文本中指定密码,需忽略PASSWORD关键词。要把密码指定为由PASSWORD()函数返回的混编值,需包含关键字PASSWORD
-- 重命名用户
RENAME USER old_user TO new_user
-- 设置密码
SET PASSWORD = PASSWORD('密码') -- 为当前用户设置密码
SET PASSWORD FOR 用户名 = PASSWORD('密码') -- 为指定用户设置密码
-- 删除用户
DROP USER 用户名
-- 分配权限/添加用户
GRANT 权限列表 ON 表名 TO 用户名 [IDENTIFIED BY [PASSWORD] 'password']
- all privileges 表示所有权限
- *.* 表示所有库的所有表
- 库名.表名 表示某库下面的某表
GRANT ALL PRIVILEGES ON `pms`.* TO 'pms'@'%' IDENTIFIED BY 'pms0817';
-- 查看权限
SHOW GRANTS FOR 用户名
-- 查看当前用户权限
SHOW GRANTS; 或 SHOW GRANTS FOR CURRENT_USER; 或 SHOW GRANTS FOR CURRENT_USER();
-- 撤消权限
REVOKE 权限列表 ON 表名 FROM 用户名
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 用户名 -- 撤销所有权限
-- 权限层级
-- 要使用GRANT或REVOKE,您必须拥有GRANT OPTION权限,并且您必须用于您正在授予或撤销的权限。
全局层级:全局权限适用于一个给定服务器中的所有数据库,mysql.user
GRANT ALL ON *.*和 REVOKE ALL ON *.*只授予和撤销全局权限。
数据库层级:数据库权限适用于一个给定数据库中的所有目标,mysql.db, mysql.host
GRANT ALL ON db_name.*和REVOKE ALL ON db_name.*只授予和撤销数据库权限。
表层级:表权限适用于一个给定表中的所有列,mysql.talbes_priv
GRANT ALL ON db_name.tbl_name和REVOKE ALL ON db_name.tbl_name只授予和撤销表权限。
列层级:列权限适用于一个给定表中的单一列,mysql.columns_priv
当使用REVOKE时,您必须指定与被授权列相同的列。
-- 权限列表
ALL [PRIVILEGES] -- 设置除GRANT OPTION之外的所有简单权限
ALTER -- 允许使用ALTER TABLE
ALTER ROUTINE -- 更改或取消已存储的子程序
CREATE -- 允许使用CREATE TABLE
CREATE ROUTINE -- 创建已存储的子程序
CREATE TEMPORARY TABLES -- 允许使用CREATE TEMPORARY TABLE
CREATE USER -- 允许使用CREATE USER, DROP USER, RENAME USER和REVOKE ALL PRIVILEGES。
CREATE VIEW -- 允许使用CREATE VIEW
DELETE -- 允许使用DELETE
DROP -- 允许使用DROP TABLE
EXECUTE -- 允许用户运行已存储的子程序
FILE -- 允许使用SELECT...INTO OUTFILE和LOAD DATA INFILE
INDEX -- 允许使用CREATE INDEX和DROP INDEX
INSERT -- 允许使用INSERT
LOCK TABLES -- 允许对您拥有SELECT权限的表使用LOCK TABLES
PROCESS -- 允许使用SHOW FULL PROCESSLIST
REFERENCES -- 未被实施
RELOAD -- 允许使用FLUSH
REPLICATION CLIENT -- 允许用户询问从属服务器或主服务器的地址
REPLICATION SLAVE -- 用于复制型从属服务器(从主服务器中读取二进制日志事件)
SELECT -- 允许使用SELECT
SHOW DATABASES -- 显示所有数据库
SHOW VIEW -- 允许使用SHOW CREATE VIEW
SHUTDOWN -- 允许使用mysqladmin shutdown
SUPER -- 允许使用CHANGE MASTER, KILL, PURGE MASTER LOGS和SET GLOBAL语句,mysqladmin debug命令;允许您连接(一次),即使已达到max_connections。
UPDATE -- 允许使用UPDATE
USAGE -- “无权限”的同义词
GRANT OPTION -- 允许授予权限
-- root密码重置
1. 停止MySQL服务
2. [Linux] /usr/local/mysql/bin/safe_mysqld --skip-grant-tables &
[Windows] mysqld --skip-grant-tables
3. use mysql;
4. UPDATE `user` SET PASSWORD=PASSWORD("密码") WHERE `user` = "root";
5. FLUSH PRIVILEGES;
用户信息表:mysql.user
-- 刷新权限
FLUSH PRIVILEGES;
-- 增加用户
CREATE USER 用户名 IDENTIFIED BY [PASSWORD] 密码(字符串)
- 必须拥有mysql数据库的全局CREATE USER权限,或拥有INSERT权限。
- 只能创建用户,不能赋予权限。
- 用户名,注意引号:如 'user_name'@'192.168.1.1'
- 密码也需引号,纯数字密码也要加引号
- 要在纯文本中指定密码,需忽略PASSWORD关键词。要把密码指定为由PASSWORD()函数返回的混编值,需包含关键字PASSWORD
-- 重命名用户
RENAME USER old_user TO new_user
-- 设置密码
SET PASSWORD = PASSWORD('密码') -- 为当前用户设置密码
SET PASSWORD FOR 用户名 = PASSWORD('密码') -- 为指定用户设置密码
-- 删除用户
DROP USER 用户名
-- 分配权限/添加用户
GRANT 权限列表 ON 表名 TO 用户名 [IDENTIFIED BY [PASSWORD] 'password']
- all privileges 表示所有权限
- *.* 表示所有库的所有表
- 库名.表名 表示某库下面的某表
GRANT ALL PRIVILEGES ON `pms`.* TO 'pms'@'%' IDENTIFIED BY 'pms0817';
-- 查看权限
SHOW GRANTS FOR 用户名
-- 查看当前用户权限
SHOW GRANTS; 或 SHOW GRANTS FOR CURRENT_USER; 或 SHOW GRANTS FOR CURRENT_USER();
-- 撤消权限
REVOKE 权限列表 ON 表名 FROM 用户名
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 用户名 -- 撤销所有权限
-- 权限层级
-- 要使用GRANT或REVOKE,您必须拥有GRANT OPTION权限,并且您必须用于您正在授予或撤销的权限。
全局层级:全局权限适用于一个给定服务器中的所有数据库,mysql.user
GRANT ALL ON *.*和 REVOKE ALL ON *.*只授予和撤销全局权限。
数据库层级:数据库权限适用于一个给定数据库中的所有目标,mysql.db, mysql.host
GRANT ALL ON db_name.*和REVOKE ALL ON db_name.*只授予和撤销数据库权限。
表层级:表权限适用于一个给定表中的所有列,mysql.talbes_priv
GRANT ALL ON db_name.tbl_name和REVOKE ALL ON db_name.tbl_name只授予和撤销表权限。
列层级:列权限适用于一个给定表中的单一列,mysql.columns_priv
当使用REVOKE时,您必须指定与被授权列相同的列。
-- 权限列表
ALL [PRIVILEGES] -- 设置除GRANT OPTION之外的所有简单权限
ALTER -- 允许使用ALTER TABLE
ALTER ROUTINE -- 更改或取消已存储的子程序
CREATE -- 允许使用CREATE TABLE
CREATE ROUTINE -- 创建已存储的子程序
CREATE TEMPORARY TABLES -- 允许使用CREATE TEMPORARY TABLE
CREATE USER -- 允许使用CREATE USER, DROP USER, RENAME USER和REVOKE ALL PRIVILEGES。
CREATE VIEW -- 允许使用CREATE VIEW
DELETE -- 允许使用DELETE
DROP -- 允许使用DROP TABLE
EXECUTE -- 允许用户运行已存储的子程序
FILE -- 允许使用SELECT...INTO OUTFILE和LOAD DATA INFILE
INDEX -- 允许使用CREATE INDEX和DROP INDEX
INSERT -- 允许使用INSERT
LOCK TABLES -- 允许对您拥有SELECT权限的表使用LOCK TABLES
PROCESS -- 允许使用SHOW FULL PROCESSLIST
REFERENCES -- 未被实施
RELOAD -- 允许使用FLUSH
REPLICATION CLIENT -- 允许用户询问从属服务器或主服务器的地址
REPLICATION SLAVE -- 用于复制型从属服务器(从主服务器中读取二进制日志事件)
SELECT -- 允许使用SELECT
SHOW DATABASES -- 显示所有数据库
SHOW VIEW -- 允许使用SHOW CREATE VIEW
SHUTDOWN -- 允许使用mysqladmin shutdown
SUPER -- 允许使用CHANGE MASTER, KILL, PURGE MASTER LOGS和SET GLOBAL语句,mysqladmin debug命令;允许您连接(一次),即使已达到max_connections。
UPDATE -- 允许使用UPDATE
USAGE -- “无权限”的同义词
GRANT OPTION -- 允许授予权限
三个范式
-- Normal Format, NF
- 每个表保存一个实体信息
- 每个具有一个ID字段作为主键
- ID主键 + 原子表
-- 1NF, 第一范式
字段不能再分,就满足第一范式。
-- 2NF, 第二范式
满足第一范式的前提下,不能出现部分依赖。
消除复合主键就可以避免部分依赖。增加单列关键字。
-- 3NF, 第三范式
满足第二范式的前提下,不能出现传递依赖。
某个字段依赖于主键,而有其他字段依赖于该字段。这就是传递依赖。
将一个实体信息的数据放在一个表内实现。
- 每个表保存一个实体信息
- 每个具有一个ID字段作为主键
- ID主键 + 原子表
-- 1NF, 第一范式
字段不能再分,就满足第一范式。
-- 2NF, 第二范式
满足第一范式的前提下,不能出现部分依赖。
消除复合主键就可以避免部分依赖。增加单列关键字。
-- 3NF, 第三范式
满足第二范式的前提下,不能出现传递依赖。
某个字段依赖于主键,而有其他字段依赖于该字段。这就是传递依赖。
将一个实体信息的数据放在一个表内实现。
备份与还原
/* 备份与还原 */ ------------------
备份,将数据的结构与表内数据保存起来。
利用 mysqldump 指令完成。
-- 导出
mysqldump [options] db_name [tables]
mysqldump [options] ---database DB1 [DB2 DB3...]
mysqldump [options] --all--database
1. 导出一张表
mysqldump -u用户名 -p密码 库名 表名 > 文件名(D:/a.sql)
2. 导出多张表
mysqldump -u用户名 -p密码 库名 表1 表2 表3 > 文件名(D:/a.sql)
3. 导出所有表
mysqldump -u用户名 -p密码 库名 > 文件名(D:/a.sql)
4. 导出一个库
mysqldump -u用户名 -p密码 --lock-all-tables --database 库名 > 文件名(D:/a.sql)
可以-w携带WHERE条件
-- 导入
1. 在登录mysql的情况下:
source 备份文件
2. 在不登录的情况下
mysql -u用户名 -p密码 库名 < 备份文件
事务
在mysql中myisam不支持事务
事务四大特性(ACID)
原子性
事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
一致性
执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
隔离性
并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
持久性
一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
事务带来的问题
脏读
一个事务读取了另一个事务未提交的数据
当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
不可重复读
一个事务两次读取同一个数据,两次读取的数据不一致
指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
幻读
一个事务两次读取一个范围的 记录,两次读取的记录数不一致。
幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
更新丢失
一个事务的更新覆盖了另一个事务的更新,解决办法使用乐观锁或者使用排它锁
指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
事务隔离级别
读未提交:read uncommitted
读已提交:read committed
Oracle默认隔离级别
可重复读:repeatable read
MySQL默认级别
串行化:serializable
常见问题
对号表示没解决
MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。我们可以通过SELECT @@tx_isolation;
InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读),但是可以通过应用加锁读(例如 select * from table for update 语句)来保证不会产生幻读,而这个加锁度使用到的机制就是 Next-Key Lock 锁算法。从而达到了 SQL 标准的 SERIALIZABLE(可串行化) 隔离级别。
设置隔离级别
SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE]
SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE]
START TARNSACTION |BEGIN:显式地开启一个事务。
COMMIT:提交事务,使得对数据库做的所有修改成为永久性。
ROLLBACK:回滚会结束用户的事务,并撤销正在进行的所有未提交的修改。
COMMIT:提交事务,使得对数据库做的所有修改成为永久性。
ROLLBACK:回滚会结束用户的事务,并撤销正在进行的所有未提交的修改。
不可重复读的重点是修改,幻读的重点在于新增或者删除。
索引
查询算法
顺序查找 O(n)
二分查找 有序 O(logn)
二叉排序树查找
特点
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
它的左、右子树也分别为二叉排序树。
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
它的左、右子树也分别为二叉排序树。
原理
若b是空树,则搜索失败,否则;
若x等于b的根节点的数据域之值,则查找成功;否则:
若x小于b的根节点的数据域之值,则搜索左子树;否则:
查找右子树
若x等于b的根节点的数据域之值,则查找成功;否则:
若x小于b的根节点的数据域之值,则搜索左子树;否则:
查找右子树
O(log2N)
多叉平衡查找树
B树
结构特征
d为大于1的一个正整数,称为B-Tree的度。
h为一个正整数,称为B-Tree的高度。
每个非叶子节点由n-1个key和n个指针组成,其中d<=n<=2d。
每个叶子节点最少包含一个key和两个指针,最多包含2d-1个key和2d个指针,叶节点的指针均为null 。
所有叶节点具有相同的深度,等于树高h。
key和指针互相间隔,节点两端是指针。
一个节点中的key从左到右非递减排列。
所有节点组成树结构。
每个指针要么为null,要么指向另外一个节点。
如果某个指针在节点node最左边且不为null,则其指向节点的所有key小于v(key1),其中v(key1)为node的第一个key的值。
如果某个指针在节点node最右边且不为null,则其指向节点的所有key大于v(keym),其中v(keym)为node的最后一个key的值。
如果某个指针在节点node的左右相邻key分别是keyi和keyi+1且不为null,则其指向节点的所有key小于v(keyi+1)且大于v(keyi)。
h为一个正整数,称为B-Tree的高度。
每个非叶子节点由n-1个key和n个指针组成,其中d<=n<=2d。
每个叶子节点最少包含一个key和两个指针,最多包含2d-1个key和2d个指针,叶节点的指针均为null 。
所有叶节点具有相同的深度,等于树高h。
key和指针互相间隔,节点两端是指针。
一个节点中的key从左到右非递减排列。
所有节点组成树结构。
每个指针要么为null,要么指向另外一个节点。
如果某个指针在节点node最左边且不为null,则其指向节点的所有key小于v(key1),其中v(key1)为node的第一个key的值。
如果某个指针在节点node最右边且不为null,则其指向节点的所有key大于v(keym),其中v(keym)为node的最后一个key的值。
如果某个指针在节点node的左右相邻key分别是keyi和keyi+1且不为null,则其指向节点的所有key小于v(keyi+1)且大于v(keyi)。
例如一个度为d的B-Tree,设其索引N个key,则其树高h的上限为logd((N+1)/2),检索一个key,其查找节点个数的渐进复杂度为O(logdN)。
由于插入删除新的数据记录会破坏B-Tree的性质,因此在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持B-Tree性质,这也是索引会降低增删改数据性能的原因。
B+树
结构特征
每个节点的指针上限为2d而不是2d+1。
所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。 (而 B 树的叶子节点并没有包括全部需要查找的信息)
内节点不存储data,只存储key;叶子节点不存储指针。所有的内节点点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 (而B 树的内节点也包含需要查找的有效信息)
数据库索引实际上用的是带有顺序的B+Tree。在经典B+Tree的基础上进行了优化,增加了顺序访问指针。
红黑树
B+Tree/B-Tree的性能会比采用红黑树好
先从B-Tree分析,根据B-Tree的定义,可知检索一次最多需要访问h个节点。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,在实际实现B-Tree还需要使用如下技巧:
每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O。
B-Tree中一次检索最多需要h-1次I/O(根节点常驻内存),渐进复杂度为O(h)=O(logdN)。一般实际应用中,出度d是非常大的数字,通常超过100,因此h非常小(通常不超过3)。
综上所述,用B-Tree作为索引结构效率是非常高的。
而红黑树这种结构,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以红黑树的I/O渐进复杂度也为O(h),效率明显比B-Tree差很多。
每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O。
B-Tree中一次检索最多需要h-1次I/O(根节点常驻内存),渐进复杂度为O(h)=O(logdN)。一般实际应用中,出度d是非常大的数字,通常超过100,因此h非常小(通常不超过3)。
综上所述,用B-Tree作为索引结构效率是非常高的。
而红黑树这种结构,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以红黑树的I/O渐进复杂度也为O(h),效率明显比B-Tree差很多。
- 为什么MYSQL使用B+树作为索引数据结构?
http://blog.csdn.net/kennyrose/article/details/7532032
http://www.xuebuyuan.com/2216918.html
索引的分类
唯一索引
索引列的值必须唯一,但允许有空值
普通索引
即一个索引只包含单个列,一个表可以有多个单列索引(建议一张表索引不要超过5个
优先考虑复合索引)
优先考虑复合索引)
复合索引(联合索引)
即一个索引包含多个列
最左前缀原则
MySQL中的索引可以以一定顺序引用多列,这种索引叫作联合索引。如User表的name和city加联合索引就是(name,city),而最左前缀原则指的是,如果查询的时候查询条件精确匹配索引的左边连续一列或几列,则此列就可以被用到。
查询的时候如果两个条件都用上了,但是顺序不同,如 city= xx and name =xx,那么现在的查询引擎会自动优化为匹配联合索引的顺序,这样是能够命中索引的。
最左前缀原则,在创建联合索引时,索引字段的顺序需要考虑字段值去重之后的个数,较多的放前面。ORDER BY子句也遵循此规则。
主键索引
加速查询+列值唯一+表中只有一个(不可以有 null)
聚簇索引
将数据存储与索引放到了一块,找到索引也就找到了数据
聚簇索引具有唯一性
主键
为什么推荐尽量使用复合索引而不是使用唯一索引呢?
因为MYSQL每次查询只能使用一个索引,如果我们sql语句查询条件包含两个字段,那么使用单值索引需要查询两次,但是复合索引只需要一次即可,有时候覆盖索引完全覆盖可以不回表查询
回表 即普通索引查询方式,则需要先搜索name索引树,得到id的值为3,再到id聚簇索引树搜索一次。这个过程称为回表
覆盖索引
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“覆盖索引”。
全文索引
索引的数据结构
B+Tree 索引
MyISAM: B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。
InnoDB: 其数据文件本身就是索引文件。相比MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。这被称为“聚簇索引(或聚集索引)”。而其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不同的地方。在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。
hash 索引
二者区别
Hash索引定位快
Hash索引指的就是Hash表,最大的优点就是能够在很短的时间内,根据Hash函数定位到数据所在的位置,这是B+树所不能比的。
Hash冲突问题
知道HashMap或HashTable的同学,相信都知道它们最大的缺点就是Hash冲突了。不过对于数据库来说这还不算最大的缺点。
Hash索引不支持顺序和范围查询(Hash索引不支持顺序和范围查询是它最大的缺点。
B+树是有序的,在这种范围查询中,优势非常大
B+树是有序的,在这种范围查询中,优势非常大
B树和B+树区别
B树的所有节点既存放 键(key) 也存放 数据(data);而B+树只有叶子节点存放 key 和 data,其他内节点只存放key。
B树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。
B树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。
索引的优缺点
优点
提高数据检索效率,降低数据库IO成本,将随机IO变为顺序IO
通过索引列对数据排序,降低数据排序成本,降低CPU的消耗
可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
缺点
实际上索引也是一张表,该表保存了主键和索引字段,并指向实体表的记录,所以索引列也是要占用空间的
虽然索引大大提高了查询速度,同时却会降低更新表的速度,如果对表INSERT,UPDATE和DELETE。
因为更新表时,MySQL不仅要不存数据,还要保存一下索引文件每次更新添加了索引列的字段,
都会调整因为更新所带来的键值变化后的索引信息
因为更新表时,MySQL不仅要不存数据,还要保存一下索引文件每次更新添加了索引列的字段,
都会调整因为更新所带来的键值变化后的索引信息
索引只是提高效率的一个因素,如果你的MySQL有大数据量的表,就需要花时间研究建立优秀的索引,或优化查询语句
http://www.cnblogs.com/mxmbk/articles/5226344.html
http://www.cnblogs.com/simplefrog/archive/2012/07/15/2592527.html
http://www.open-open.com/lib/view/open1418476492792.html
http://blog.csdn.net/colin_liu2009/article/details/7301089
http://www.cnblogs.com/hongfei/archive/2012/10/20/2732589.html
http://www.cnblogs.com/simplefrog/archive/2012/07/15/2592527.html
http://www.open-open.com/lib/view/open1418476492792.html
http://blog.csdn.net/colin_liu2009/article/details/7301089
http://www.cnblogs.com/hongfei/archive/2012/10/20/2732589.html
什么是回表?
索引优化
Explain(查询语句执行计划)
作用
表的读取顺序
数据读取操作的操作类型
哪些索引可以使用
哪些索引被实际使用
表之间的引用
每张表有多少行被优化器查询
字段
ID
查询序号,id相同从上往下,id不同id越大优先级越高
select_type
表示示查询中每个select子句的类型
table(重要)
显示这一步所访问数据库中表名称(显示这一行的数据是关于哪张表的),有时不是真实的表名字,可能是简称,例如上面的e,d,也可能是第几步执行的结果的简称
partitions
代表分区表中的命中情况,非分区表,该项为null
type(重要)
对表访问方式,表示MySQL在表中找到所需行的方式,又称“访问类型”。
ALL、index、range、 ref、eq_ref、const、system、NULL(从左到右,性能从差到好)
possible_keys
指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用(该查询可以利用的索引,如果没有任何索引显示 null)
Key(重要)
key列显示MySQL实际决定使用的键(索引),必然包含在possible_keys中
key_len
表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度(key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的)
ref
列与索引的比较,表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
rows(重要)
表示MySQL估计未来找到所需要的行而要读取的行数
Extra(重要)
这一列包含的是不适合在其他列显示的额为信息
什么是复合索引的最左匹配原则?
索引失效的情况
如果查询条件用or,必须or条件中的每个列都加上索引,否则无效。(尽量使用union代替)
复合索引未用左列字段;
like以%开头;
需要类型转换;
where中索引列有运算;
where中索引列使用了函数;
如果mysql觉得全表扫描更快时(数据少)
查询优化
关联查询
保证被驱动表的join字段有索引
left join时,选择小表为驱动表,大表为被驱动表,因为驱动表一定要做全表扫描。
inner join时,mysql会自己帮你把小结果集的表选为驱动表
子查询尽量不要放在被驱动表。因为子查询会生成虚拟表导致有可能使用不到索引
能够直接关联查询,尽量不用子查询。
慢查询
数据量不同,查询条件不同,sql使用的索引可能是不一样的,要构造多种查询条件去测试。
避免所有字段都返回,尽量使用覆盖索引,解决慢sql问题,终归是与库的磁盘IO、CPU做抗争。
避免隐式转换造成的索引无法使用问题。
制好事务粒度,大事务不仅会严重影响数据库的吞吐量,CPU(死锁检测),也会造成主从的延迟,危害极大。
合理的设置数据库连接池的参数,设置sql语句的timeout,查询量大的地方,需要有降级开关。
新增功能,每一条sql语句,都要进行explain
所谓的慢sql,有些sql并不慢,而是坏sql,调用量低,数据量少的情况,并不慢,慢日志无法捕获。这个时候,需要对功能进行压测,压测需要注意两个问题:
a) 压测脚本的选择,如果使用固定的查询条件,会造成mysql命中缓存,或使用固定索引,压测效果不明显
b) 压测数据库的操作,要逐渐放量,避免将库CPU打满,既要盯UMP的性能曲线,又要关注数据库CPU的使用率。
a) 压测脚本的选择,如果使用固定的查询条件,会造成mysql命中缓存,或使用固定索引,压测效果不明显
b) 压测数据库的操作,要逐渐放量,避免将库CPU打满,既要盯UMP的性能曲线,又要关注数据库CPU的使用率。
读写分离
使用读写分离的方式,降低数据库的压力,读写分离能有效降低库的压力
主从延迟问题。读写分离后,无可避免的会有延迟问题,所以需要甄别好,哪些业务是对延迟敏感的,这类业务,需要继续查询主库。为尽量避免延迟问题,需注意以下几点:
a) 从库的压力,不能过大,如果资源允许,尽量主从的硬件资源相同。
b) 避免使用大事务。
c) 尽量避免大批量的删除、更新操作,尤其是无法使用索引的情况。
a) 从库的压力,不能过大,如果资源允许,尽量主从的硬件资源相同。
b) 避免使用大事务。
c) 尽量避免大批量的删除、更新操作,尤其是无法使用索引的情况。
业务隔离,不同业务使用不同从库。识别出业务的黄金流程。重点业务与其他非重点业务使用不同的从库进行隔离。
架构调整,服务化改造,应用拆分
脱库改造,增加缓存。
a) 对于数据要求实时性不高的场景,并且为了快速的减少系统问题,可采取缓存read-through的方式,该方式系统改造量低,简单。但是要注意,避免不存在的key缓存穿透(不存在key设置特殊值、bloomfilter)。缓存雪崩问题。
b) 数据异构,将依赖的底层数据通过binlake或双写等等方式,异构到jimdb
c) 数据异构,将列表类或多条件复杂查询数据,异构到ES。查询需注意深分页及一次查询的数据量过多问题。
a) 对于数据要求实时性不高的场景,并且为了快速的减少系统问题,可采取缓存read-through的方式,该方式系统改造量低,简单。但是要注意,避免不存在的key缓存穿透(不存在key设置特殊值、bloomfilter)。缓存雪崩问题。
b) 数据异构,将依赖的底层数据通过binlake或双写等等方式,异构到jimdb
c) 数据异构,将列表类或多条件复杂查询数据,异构到ES。查询需注意深分页及一次查询的数据量过多问题。
复杂的统计类功能,使用离线计算的方式,避免实时通过库函数进行计算统计
浏览记录、日志类或其他不重要功能,可通过mq,同步写转异步写
数据库垂直拆分,业务隔离
底层资源进行拆分,按业务维度,不同业务拆分为不同应用 ,使用不同的资源。
数据库水平拆分,分库分表
1.库水平拆分会出现很多问题,无法join,无法聚合查询,可采用异构数据到ES等方式解决。
2、将无用的历史数据进行归档。
2、将无用的历史数据进行归档。
不适合使用Mysql场景
复杂、多字段、模糊查询
针对问题1,对于复杂、模糊查询等,更适合使用ES搜索引擎去处理。
a) 如果对数据的实时性要求不高,建议通过binlake或mq的方式,异步构建ES索引。
b) 如果对数据实时性要求很高,可通过双写的方式处理,失败可以采用异步补偿的方式。另外ES本身段刷新有1秒的延迟,1s后数据才可搜索。如果不可接受并且数据修改频率低,可通过setRefresh方法强制刷新,立刻即可搜索到。写入量大的时候慎用。
a) 如果对数据的实时性要求不高,建议通过binlake或mq的方式,异步构建ES索引。
b) 如果对数据实时性要求很高,可通过双写的方式处理,失败可以采用异步补偿的方式。另外ES本身段刷新有1秒的延迟,1s后数据才可搜索。如果不可接受并且数据修改频率低,可通过setRefresh方法强制刷新,立刻即可搜索到。写入量大的时候慎用。
超大文本的存储(text类型)。大文本查询,会耗费mysql大量的内存空间,造成热数据被置换出去,查询效率降低
建议使用nosql库,hbase、es等存储
日志类大数量的存储
建议使用nosql库,hbase、es等存储
超高并发的查询
简单查询,jimdb是非常好选择。如果有业务需要复杂查询,更建议使用ES多集群方式处理。
常见问题
简述在MySQL数据库中MyISAM和InnoDB的区别
MyISAM:
不支持事务,但是每次查询都是原子的;
支持表级锁,即每次操作是对整个表加锁;
存储表的总行数;
一个MYISAM表有三个文件:索引文件、表结构文件、数据文件;
采用菲聚集索引,索引文件的数据域存储指向数据文件的指针。辅索引与主索引基本一致,但是辅索引不用保证唯一性。
InnoDb:
支持ACID的事务,支持事务的四种隔离级别;
支持行级锁及外键约束:因此可以支持写并发;
不存储总行数;
一个InnoDb引擎存储在一个文件空间(共享表空间,表大小不受操作系统控制,一个表可能分布在多个文件里),也有可能为多个(设置为独立表空,表大小受操作系统文件大小限制,一般为2G),受操作系统文件大小的限制;
主键索引采用聚集索引(索引的数据域存储数据文件本身),辅索引的数据域存储主键的值;因此从辅索引查找数据,需要先通过辅索引找到主键值,再访问辅索引;最好使用自增主键,防止插入数据时,为维持B+树结构,文件的大调整。
不支持事务,但是每次查询都是原子的;
支持表级锁,即每次操作是对整个表加锁;
存储表的总行数;
一个MYISAM表有三个文件:索引文件、表结构文件、数据文件;
采用菲聚集索引,索引文件的数据域存储指向数据文件的指针。辅索引与主索引基本一致,但是辅索引不用保证唯一性。
InnoDb:
支持ACID的事务,支持事务的四种隔离级别;
支持行级锁及外键约束:因此可以支持写并发;
不存储总行数;
一个InnoDb引擎存储在一个文件空间(共享表空间,表大小不受操作系统控制,一个表可能分布在多个文件里),也有可能为多个(设置为独立表空,表大小受操作系统文件大小限制,一般为2G),受操作系统文件大小的限制;
主键索引采用聚集索引(索引的数据域存储数据文件本身),辅索引的数据域存储主键的值;因此从辅索引查找数据,需要先通过辅索引找到主键值,再访问辅索引;最好使用自增主键,防止插入数据时,为维持B+树结构,文件的大调整。
是否支持MVCC :仅 InnoDB 支持。应对高并发事务, MVCC比单纯的加锁更高效;MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作;MVCC可以使用 乐观(optimistic)锁 和 悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统一。
Datetime 和 Timestamp 区别
通常我们都会首选 Timestamp
时区
DateTime 类型保存的时间都是当前会话所设置的时区对应的时间。
Timestamp 类型字段的值会随着服务器时区的变化而变化,自动换算成相应的时间,说简单点就是在不同时区,查询到同一个条记录此字段的值会不一样。
# 查看当前会话时区
SELECT @@session.time_zone;
# 设置当前会话时区
SET time_zone = 'Europe/Helsinki';
SET time_zone = "+00:00";
# 数据库全局时区设置
SELECT @@global.time_zone;
# 设置全局时区
SET GLOBAL time_zone = '+8:00';
SET GLOBAL time_zone = 'Europe/Helsinki';
SELECT @@session.time_zone;
# 设置当前会话时区
SET time_zone = 'Europe/Helsinki';
SET time_zone = "+00:00";
# 数据库全局时区设置
SELECT @@global.time_zone;
# 设置全局时区
SET GLOBAL time_zone = '+8:00';
SET GLOBAL time_zone = 'Europe/Helsinki';
存储空间 与 时间范围
DateTime 需要耗费 8 个字节的存储空间
DateTime :1000-01-01 000000 ~ 9999-12-31 235959
Timestamp 只需要使用 4 个字节的存储空间,
Timestamp: 1970-01-01 000001 ~ 2037-12-31 235959
其实用long型数值表示 程序做转换 也可!
使用索引的注意事项
在经常需要搜索的列上,可以加快搜索的速度;
在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。
在经常需要排序的列上创 建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;B+Tree
对于中到大型表索引都是非常有效的,但是特大型表的话维护开销会很大,不适合建索引
在经常用在连接的列上,这 些列主要是一些外键,可以加快连接的速度;
避免 where 子句中对宇段施加函数,这会造成无法命中索引。
在使用InnoDB时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键。
将某一列设置为default null,where 是可以走索引,另外索引列是否设置 null 是不影响性能的。 但是,还是不建议列上允许为空。最好限制not null,因为null需要更多的存储空间并且null值无法参与某些运算。
删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 MySQL 5.7 可以通过查询 sys 库的 chema_unused_indexes 视图来查询哪些索引从未被使用
在使用 limit offset 查询缓慢时,可以借助索引来提高性能
被频繁更新的字段应该慎重建立索引
尽可能的考虑建立联合索引而不是单列索引
为什么索引能提高查询速度
MySql基础组件
连接器
身份认证和权限相关(登录 MySQL 的时候)。
查询缓存
执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。
分析器
没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。
第一步,词法分析,一条 SQL 语句有多个字符串组成,首先要提取关键字,比如 select,提出查询的表,提出字段名,提出查询条件等等。做完这些操作后,就会进入第二步。
第二步,语法分析,主要就是判断你输入的 sql 是否正确,是否符合 MySQL 的语法。
优化器
按照 MySQL 认为最优的方案去执行。
执行器
执行语句,然后从存储引擎返回数据。
存储引擎
MyISAM
文件
frm文件:存储表的定义数据
MYD文件:存放表具体记录的数据
MYI文件:存储索引
特点索引存放的是数据具体存放在磁盘上的地址
InnoDB
一张表最多有16个索引,每个索引的最大长度是255个字节
事务型数据库的首选引擎,支持事务安全表(ACID),支持行锁定和外键,上图也看到了,InnoDB是默认的MySQL引擎。
ISAM
CSV.....
锁
分类
按照锁机制分类
共享锁(读锁)
MyISAM表共享锁
InnoDb行共享锁
InnoDB排它锁(写锁)
排它锁又称:写锁
当一个事务对某几个上写锁时,不允许其他事务写,但允许读
更不允许其他事务给这几行上任何锁。包括写锁。
两个事务不能锁同一个索引
insert ,delete , update在事务中都会自动默认加上排它锁
行锁必须有索引才能实现,否则会自动锁全表,那么就不是行锁了
按照锁的粒度分类
表锁(偏读)
使用表级锁定的主要是MyISAM,MEMORY,CSV等一些非事务性存储引擎。
行锁(偏写)
使用行级锁定的主要是InnoDB存储引擎。
排他锁: X锁
事务中显式加锁:SELECT * FROM table_name WHERE ... FOR UPDATE
共享锁:S锁
事务中显式加锁:SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
意向排他锁:IX锁
意向共享锁:IS锁
页锁(DBD引擎采用)
表级锁和行级锁的区别
表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如OLAP系统
行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统
InnoDB存储引擎 行锁的实现方式
InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁
Record lock:单个行记录上的锁
Gap lock:间隙锁,锁定一个范围,不包括记录本身
(1)防止幻读,以满足相关隔离级别的要求。对于上面的例子,要是不使用间隙锁,如果其他事务插入了empid大于100的任何记录,那么本事务如果再次执行上述语句,就会发生幻读;
(2)为了满足其恢复和复制的需要。
Next-key lock:record+gap 锁定一个范围,包含记录本身
常见问题
innodb对于行的查询使用next-key lock
Next-locking keying为了解决Phantom Problem幻读问题
当查询的索引含有唯一属性时,将next-key lock降级为record key
Gap锁设计的目的是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生
有两种方式显式关闭gap锁:(除了外键约束和唯一性检查外,其余情况仅使用record lock) A. 将事务隔离级别设置为RC B. 将参数innodb_locks_unsafe_for_binlog设置为1
innoDB使用的是行锁myisam使用的是表锁
行锁退化到表锁
更新的时候没有索引或者索引失效时,InnoDB 的行锁变表锁
间隙锁
间隙锁(Gap Lock)是Innodb在可重复读提交下为了解决幻读问题时引入的锁机制,
意向锁
行锁是行级别的,粒度比较小,好,那我要你在拿行锁之前,必须先拿一个假的表锁,表示你想去锁住表里的某一行或者多行记录。
这样,Mysql 在判断表里有没有记录被锁定,就不需要遍历整张表了,它只需要看看,有没有人拿了这个假的表锁。
这样,Mysql 在判断表里有没有记录被锁定,就不需要遍历整张表了,它只需要看看,有没有人拿了这个假的表锁。
逻辑结构修改
锁表
修改表结构会导致表锁,数据量大修改数据很长,导致大量用户阻塞,无法访问
系统升级,加字段正确方法
首先创建一个和你要执行的alter操作的表一样的空的表结构
执行我们赋予的表结构的修改,然后copy原表中的数据到新表里面。
在原表上创建一个触发器在数据copy的过程中,将原表的更新数据的操作全部更新到新的表中来
copy完成之后,用rename table 新表代替原表,默认删除原表
用插件解决加字段问题
pt-online-schema-change
pt-online-schema-change h=127.0.0.1,u=root,D=mysqldemo,t=product_info --alter "modify product_name varchar(150) not null default '' " --execute
如何给数据库加锁
InnoDB
对于普通SELECT语句,InnoDB不会加任何锁;
对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);
开启事务加锁
MyISAM
表锁
MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用LOCK TABLE命令给MyISAM表显式加锁
如何避免死锁
(1)在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。
(2)在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能。
(3)在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时再申请排他锁,因为当用户申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁。
(4)在REPEATABLE-READ隔离级别下,如果两个线程同时对相同条件记录用SELECT...FOR UPDATE加排他锁,在没有符合该条件记录情况下,两个线程都会加锁成功。程序发现记录尚不存在,就试图插入一条新记录,如果两个线程都这么做,就会出现死锁。这种情况下,将隔离级别改成READ COMMITTED,就可避免问题。
(5)当隔离级别为READ COMMITTED时,如果两个线程都先执行SELECT...FOR UPDATE,判断是否存在符合条件的记录,如果没有,就插入记录。此时,只有一个线程能插入成功,另一个线程会出现锁等待,当第1个线程提交后,第2个线程会因主键重出错,但虽然这个线程出错了,却会获得一个排他锁。这时如果有第3个线程又来申请排他锁,也会出现死锁。对于这种情况,可以直接做插入操作,然后再捕获主键重异常,或者在遇到主键重错误时,总是执行ROLLBACK释放获得的排他锁。
死锁检测
Innodb死锁检测
通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况:
InnoDB_row_lock_current_waits:当前正在等待锁定的数量;
InnoDB_row_lock_time:从系统启动到现在锁定总时间长度;
InnoDB_row_lock_time_avg:每次等待所花平均时间;
InnoDB_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
InnoDB_row_lock_waits:系统启动后到现在总共等待的次数;
InnoDB_row_lock_time:从系统启动到现在锁定总时间长度;
InnoDB_row_lock_time_avg:每次等待所花平均时间;
InnoDB_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
InnoDB_row_lock_waits:系统启动后到现在总共等待的次数;
MySQL官方手册中也提到了这个问题,实际上在InnoDB发现死锁之后,会计算出两个事务各自插入、更新或者删除的数据量来判定两个事务的大小。也就是说哪个事务所改变的记录条数越多,在死锁中就越不会被回滚掉。
但是有一点需要注意的就是,当产生死锁的场景中涉及到不止InnoDB存储引擎的时候,InnoDB是没办法检测到该死锁的,这时候就只能通过锁定超时限制参数InnoDB_lock_wait_timeout来解决。
但是有一点需要注意的就是,当产生死锁的场景中涉及到不止InnoDB存储引擎的时候,InnoDB是没办法检测到该死锁的,这时候就只能通过锁定超时限制参数InnoDB_lock_wait_timeout来解决。
这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖跨数据库。我们通过设置合适的锁等待超时阈值,可以避免这种情况发生。
在InnoDB的事务管理和锁定机制中,有专门检测死锁的机制,会在系统中产生死锁之后的很短时间内就检测到该死锁的存在。当InnoDB检测到系统中产生了死锁之后,InnoDB会通过相应的判断来选这产生死锁的两个事务中较小的事务来回滚,而让另外一个较大的事务成功完成。
常规思路
抢占加事务回滚的方式
当事务开始执行时会先获得一个时间戳,数据库程序会根据事务的时间戳决定事务应该等待还是回滚,在这时也有两种机制
wait-die 机制
当执行事务的时间戳小于另一事务时,即事务 A 先于 B 开始,那么它就会等待另一个事务释放对应资源的锁,否则就会保持当前的时间戳并回滚。
wound-wait
当前事务如果先于另一事务执行并请求了另一事务的资源,那么另一事务会立刻回滚,将资源让给先执行的事务,否则就会等待其他事务释放资源
MVCC (多版本并发控制)
https://draveness.me/database-concurrency-control/
分类
MySQL 与 MVCC
MySQL 中实现的多版本两阶段锁协议(Multiversion 2PL)将 MVCC 和 2PL 的优点结合了起来,
每一个版本的数据行都具有一个唯一的时间戳,当有读事务请求时,数据库程序会直接从多个版本的数据项中具有最大时间戳的返回。
更新操作就稍微有些复杂了,事务会先读取最新版本的数据计算出数据更新后的结果,然后创建一个新版本的数据,新数据的时间戳是目前数据行的最大版本 +1:
数据版本的删除也是根据时间戳来选择的,MySQL 会将版本最低的数据定时从数据库中清除以保证不会出现大量的遗留内容。
PostgreSQL 与 MVCC
PostgreSQL 中都是使用乐观并发控制的,这也就导致了 MVCC 在于乐观锁结合时的实现上有一些不同,最终实现的叫做多版本时间戳排序协议(Multiversion Timestamp Ordering),在这个协议中,所有的事务在执行之前都会被分配一个唯一的时间戳,每一个数据项都有读写两个时间戳
当 PostgreSQL 的事务发出了一个读请求,数据库直接将最新版本的数据返回,不会被任何操作阻塞,而写操作在执行时,事务的时间戳一定要大或者等于数据行的读时间戳,否则就会被回滚。
这种 MVCC 的实现保证了读事务永远都不会失败并且不需要等待锁的释放,对于读请求远远多于写请求的应用程序,乐观锁加 MVCC 对数据库的性能有着非常大的提升;虽然这种协议能够针对一些实际情况做出一些明显的性能提升,但是也会导致两个问题,一个是每一次读操作都会更新读时间戳造成两次的磁盘写入,第二是事务之间的冲突是通过回滚解决的,所以如果冲突的可能性非常高或者回滚代价巨大,数据库的读写性能还不如使用传统的锁等待方式。
最佳SQL实践
模糊查询
【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决
说明:索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
说明:索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
外键和级联
【强制】不得使用外键与级联,一切外键概念必须在应用层解决。
说明:以学生和成绩的关系为例,学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风 险;外键影响数据库的插入速度
说明:以学生和成绩的关系为例,学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风 险;外键影响数据库的插入速度
使用外键带来的问题
增加了复杂性
增加了额外工作
外键还会因为需要请求对其他表内部加锁而容易出现死锁情况;
对分库分表不友好
优点
级联操作方便,减轻了程序代码量;
保证了数据库数据的一致性和完整性;
@Transactional
@Transactional事务不要滥用。事务会影响数据库的QPS,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。
大表优化
1. 限定数据的范围
2. 读/写分离
3. 垂直分区
垂直拆分的优点: 可以使得列数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。
垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂;
4. 水平分区
客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的 Sharding-JDBC 、阿里的TDDL是两种比较常用的实现
中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。
连接池带来的池化思想
在连接池中,创建连接后,将其放置在池中,并再次使用它,因此不必建立新的连接。如果使用了所有连接,则会建立一个新连接并将其添加到池中。
连接池还减少了用户必须等待建立与数据库的连接的时间。
分库分表之后,id 主键如何处理?
UUID:不适合作为主键,因为太长了,并且无序不可读,查询效率低。比较适合用于生成唯一的名字的标示比如文件的名字。
数据库自增 id : 两台数据库分别设置不同步长,生成不重复ID的策略来实现高可用。这种方式生成的 id 有序,但是需要独立部署数据库实例,成本高,还会有性能瓶颈。
利用 redis 生成 id : 性能比较好,灵活方便,不依赖于数据库。但是,引入了新的组件造成系统更加复杂,可用性降低,编码更加复杂,增加了系统成本。
Twitter的snowflake算法 :Github 地址:https://github.com/twitter-archive/snowflake。
美团的Leaf分布式ID生成系统 :Leaf 是美团开源的分布式ID生成器,能保证全局唯一性、趋势递增、单调递增、信息安全,里面也提到了几种分布式方案的对比,但也需要依赖关系数据库、Zookeeper等中间件。感觉还不错。美团技术团队的一篇文章:
一条SQL语句执行得很慢的原因有哪些?
大多数情况是正常的,只是偶尔会出现很慢的情况。
数据库在刷新脏页(flush)
当我们要往数据库插入一条数据、或者要更新一条数据的时候,我们知道数据库会在内存中把对应字段的数据更新了,但是更新之后,这些更新的字段并不会马上同步持久化到磁盘中去,而是把这些更新的记录写入到 redo log 日记中去,等到空闲的时候,在通过 redo log 里的日记把最新的数据同步到磁盘中去。
当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。
当我们要往数据库插入一条数据、或者要更新一条数据的时候,我们知道数据库会在内存中把对应字段的数据更新了,但是更新之后,这些更新的字段并不会马上同步持久化到磁盘中去,而是把这些更新的记录写入到 redo log 日记中去,等到空闲的时候,在通过 redo log 里的日记把最新的数据同步到磁盘中去。
当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。
刷脏页有下面4种场景
redolog写满了
内存不够用了
MySQL 认为系统“空闲”的时候
MySQL 正常关闭的时候
拿不到锁
show processlist
show processlist
在数据量不变的情况下,这条SQL语句一直以来都执行的很慢。
没用到索引
字段没有索引
字段有索引,但却没有用索引
函数操作导致没有用上索引
数据库自己选错索引
MySQL高性能优化规范建议
设计范式
分类
1NF
符合1NF的关系中的每个属性都不可再分
2Nf
二是没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的一部分。
3NF
在第二范式的基础上,数据表中如果不存在非关键字段对任一候选关键字段的传递函数依赖则符合第三范式。
BCNF
数据库表中如果不存在任何字段对任一候选关键字段的传递函数依赖则符BCNF范式。
Oracle
.........
NoSQL数据库
Redis
基本知识
介绍
Redis 是一个开源,高级的键值存储和一个适用的解决方案,用于构建高性能,可扩展的 Web 应用程序。Redis 也被作者戏称为 数据结构服务器 ,这意味着使用者可以通过一些命令,基于带有 TCP 套接字的简单 服务器-客户端 协议来访问一组 可变数据结构 。(
优点
异常快 - Redis 非常快,每秒可执行大约 110000 次的设置(SET)操作,每秒大约可执行 81000 次的读取/获取(GET)操作。
支持丰富的数据类型 - Redis 支持开发人员常用的大多数数据类型,例如列表,集合,排序集和散列等等。这使得 Redis 很容易被用来解决各种问题,因为我们知道哪些问题可以更好使用地哪些数据类型来处理解决。
操作具有原子性 - 所有 Redis 操作都是原子操作,这确保如果两个客户端并发访问,Redis 服务器能接收更新的值。
多实用工具 - Redis 是一个多实用工具,可用于多种用例,如:缓存,消息队列(Redis 本地支持发布/订阅),应用程序中的任何短期数据,例如,web应用程序中的会话,网页命中计数等。
数据结构
基本数据结构
字符串 String
string 是最常用的一种数据类型,普通的key/value存储都可以归结为string类型,value不仅是string,也可以是数字。其他几种数据类型的构成元素也都是字符串,注意Redis规定字符串的长度不能超过512M
编码 字符串对象的编码可以是int raw embstr
int编码
保存的是可以用long类型表示的整数值
raw编码
保存长度大于44字节的字符串
embstr编码
保存长度小于44字节的字符串
Redis中对于浮点型也是作为字符串保存的,在需要时再将其转换成浮点数类型
哈希表 Hash
编码
hash对象的编码可以是zipmap或者hashtable
当使用zipmap,也就是压缩列表作为底层实现时,新增的键值是保存到压缩列表的表尾。
hashtable 编码的hash表对象底层使用字典数据结构,哈希对象中的每个键值对都使用一个字典键值对。Redis中的字典相当于Java里面的HashMap,内部实现也差不多类似,都是通过“数组+链表”的链地址法来解决哈希冲突的,这样的结构吸收了两种不同数据结构的优点。
当使用zipmap,也就是压缩列表作为底层实现时,新增的键值是保存到压缩列表的表尾。
hashtable 编码的hash表对象底层使用字典数据结构,哈希对象中的每个键值对都使用一个字典键值对。Redis中的字典相当于Java里面的HashMap,内部实现也差不多类似,都是通过“数组+链表”的链地址法来解决哈希冲突的,这样的结构吸收了两种不同数据结构的优点。
编码转换
当同时满足下面两个条件使用ziplist编码,否则使用hashtable编码
列表保存元素个数小于512个
每个元素长度小于64字节
hash是一个String类型的field和value之间的映射表
Hash特别适合存储对象
所存储的成员较少时数据存储为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht
Hash命令详解
hset/hget
hset hashname hashkey hashvalue
hget hashname hashkey
hmset/hmget
hmset hashname hashkey1hashvalue1 hashkey2 hashvalue2 hashkey3 hashvalue3
hget hashname hashkey1 hashkey2 hashkey3
hsetnx/hgetnx
hincrby/hdecrby
渐进式扩容
渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个 hash 结构,然后在后续的定时任务以及 hash 操作指令中,循序渐进的把旧字典的内容迁移到新字典中。当搬迁完成了,就会使用新的 hash 结构取而代之。
正常情况下,当 hash 表中 元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是 原数组大小的 2 倍。不过如果 Redis 正在做 bgsave(持久化命令),为了减少内存也得过多分离,Redis 尽量不去扩容,但是如果 hash 表非常满了,达到了第一维数组长度的 5 倍了,这个时候就会 强制扩容。
hash 表因为元素逐渐被删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少 hash 表的第一维数组空间占用。所用的条件是 元素个数低于数组长度的 10%,缩容不会考虑 Redis 是否在做 bgsave。
应用场景
对于 hash 数据类型,value 存放的是键值对,比如可以做单点登录存放用户信息。
存放商品信息,实现购物车
优点 / 缺点
优点
同类数据归类整合存储,方便数据管理,比如单个用户的所有商品都放在一个hash表里面。
相比string操作消耗内存cpu更小
缺点
hash结构的存储消耗要高于单个字符串
过期功能不能使用在field上,只能用在key上
redis集群架构不适合大规模使用
列表(链表实现)List
list列表,它是简单的字符串列表,你可以添加一个元素到列表的头部,或者尾部。
编码
列表对象的编码可以是ziplist(压缩列表)和linkedlist(双端链表)。
编码转换
同时满足下面两个条件时使用压缩列表:
列表保存元素个数小于512个
每个元素长度小于64字节
不能满足上面两个条件使用linkedlist(双端列表)编码
常用命令
LPUSH 和 RPUSH 分别可以向 list 的左边(头部)和右边(尾部)添加一个新元素;
LRANGE 命令可以从 list 中取出一定范围的元素;
LINDEX 命令可以从 list 中取出指定下表的元素,相当于 Java 链表操作中的 get(int index) 操作;
实现数据结构
Stack(栈)
LPUSH+LPOP
Queue(队列)
LPUSH + RPOP
Blocking MQ(阻塞队列)
LPUSH+BRPOP
集合 set
集合对象set是string类型(整数也会转成string类型进行存储)的无序集合。注意集合和列表的区别:集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能有重复。
编码
集合对象的编码可以是intset或者hashtable
intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中。
hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,这里的每个字符串对象就是一个集合中的元素,而字典的值全部设置为null。当使用HT编码时,Redis中的集合SET相当于Java中的HashSet,内部的键值对是无序的,唯一的。内部实现相当于一个特殊的字典,字典中所有value都是NULL。
编码转换
当集合满足下列两个条件时,使用intset编码:
集合对象中的所有元素都是整数
集合对象所有元素数量不超过512
集合对象的编码可以是intset或者hashtable
intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中。
hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,这里的每个字符串对象就是一个集合中的元素,而字典的值全部设置为null。当使用HT编码时,Redis中的集合SET相当于Java中的HashSet,内部的键值对是无序的,唯一的。内部实现相当于一个特殊的字典,字典中所有value都是NULL。
编码转换
当集合满足下列两个条件时,使用intset编码:
集合对象中的所有元素都是整数
集合对象所有元素数量不超过512
sadd: 向集合中添加元素 (set不允许元素重复)
smembers: 查看集合中的元素
srem: 删除集合元素
spop: 随机返回删除的key
sdiff :返回两个集合的不同元素 (哪个集合在前就以哪个集合为标准)
smembers: 查看集合中的元素
srem: 删除集合元素
spop: 随机返回删除的key
sdiff :返回两个集合的不同元素 (哪个集合在前就以哪个集合为标准)
应用场景
对于 set 数据类型,由于底层是字典实现的,查找元素特别快,另外set 数据类型不允许重复,利用这两个特性我们可以进行全局去重,比如在用户注册模块,判断用户名是否注册;微信点赞,微信抽奖小程序
另外就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好,可能认识的人等功能。
有序集合 sort set
和集合对象相比,有序集合对象是有序的。与列表使用索引下表作为排序依据不同,有序集合为每一个元素设置一个分数(score)作为排序依据。
编码
有序集合的编码可以使ziplist或者skiplist
有序集合的编码可以使ziplist或者skiplist
当有序结合对象同时满足以下两个条件时,对象使用ziplist编码,否则使用skiplist编码
保存的元素数量小于128
保存的所有元素长度都小于64字节
ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。并且压缩列表内的集合元素按分值从小到大的顺序进行排列,小的放置在靠近表头的位置,大的放置在靠近表尾的位置。
skiplist编码的依序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表
跳跃列表 (SkipList)
使用原因
因为 zset 要支持随机的插入和删除,所以它 不宜使用数组来实现,关于排序问题,我们也很容易就想到 红黑树/ 平衡树 这样的树形结构,为什么 Redis 不使用这样一些结构呢?
性能考虑: 在高并发的情况下,树形结构需要执行一些类似于 rebalance 这样的可能涉及整棵树的操作,相对来说跳跃表的变化只涉及局部
实现考虑: 在复杂度与红黑树相同的情况下,跳跃表实现起来更简单,看起来也更加直观;
实现思路
https://mp.weixin.qq.com/s?__biz=MzA4NTg1MjM0Mg==&mid=2657261425&idx=1&sn=d840079ea35875a8c8e02d9b3e44cf95&scene=21#wechat_redirect
平均时间复杂度
O(log n)
常用命令
zrem: 删除集合中名称为key的元素member
zincrby: 以指定值去自动递增
zcard: 查看元素集合的个数
zcount: 返回score在给定区间中的数量
zrangebyscore: 找到指定区间范围的数据进行返回
zremrangebyrank zset from to: 删除索引
zremrangebyscore zset from to: 删除指定序号
zrank: 返回排序索引 (升序之后再找索引)
zrevrank: 返回排序索引 (降序之后再找索引)
应用场景
对于 zset 数据类型,有序的集合,可以做范围查找,排行榜应用,取 TOP N 操作等。
对于 zset 数据类型,有序的集合,可以做范围查找,排行榜应用,取 TOP N 操作等。
地理位置(GeoHash )
GeoHash 算法将 二维的经纬度 数据映射到 一维 的整数,这样所有的元素都将在挂载到一条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近。当我们想要计算 「附近的人时」,首先将目标位置映射到这条线上,然后在这个一维的线上获取附近的点就行了。
它的核心思想就是把整个地球看成是一个 二维的平面,然后把这个平面不断地等分成一个一个小的方格,每一个 坐标元素都位于其中的 唯一一个方格 中,等分之后的 方格越小,那么坐标也就 越精确
常见命令
增加 geoadd
geoadd company 116.48105 39.996794 juejin
距离 geodist
geodist company juejin ireader km
获取元素位置 geopos
geopos company juejin
获取元素的 hash 值 geohash
geohash company ireader
附近的东西 georadiusbymember 、 georadius
georadiusbymember company ireader 20 km count 3 asc
HyperLogLog
关于基数统计 通常是用来统计一个集合中不重复的元素个数。
实现方案
B 树
B 树最大的优势就是插入和查找效率很高,如果用 B 树存储要统计的数据,可以快速判断新来的数据是否存在,并快速将元素插入 B 树。要计算基础值,只需要计算 B 树的节点个数就行了。
不过将 B 树结构维护到内存中,能够解决统计和计算的问题,但是 并没有节省内存。
不过将 B 树结构维护到内存中,能够解决统计和计算的问题,但是 并没有节省内存。
bitmap
bitmap 可以理解为通过一个 bit 数组来存储特定数据的一种数据结构,每一个 bit 位都能独立包含信息,bit 是数据的最小存储单位,因此能大量节省空间,也可以将整个 bit 数据一次性 load 到内存计算。
bitmap 还有一个明显的优势是 可以轻松合并多个统计结果,只需要对多个结果求异或就可以了,也可以大大减少存储内存。
Java 的 bitSet 实现了该数据结构
应用
1)已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数
8位最多99 999 999,大概需要99m个bit,大概10几m字节的内存即可。可以理解为从0-99 999 999的数字,每个数字对应一个Bit位,所以只需要99M个Bit==1.2MBytes,这样,就用了小小的1.2M左右的内存表示了所有的8位数的电话。
2)2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数
将bit-map扩展一下,用2bit表示一个数即可:0表示未出现;1表示出现一次;2表示出现2次及以上,即重复,在遍历这些数的时候,如果对应位置的值是0,则将其置为1;如果是1,将其置为2;如果是2,则保持不变。或者我们不用2bit来进行表示,我们用两个bit-map即可模拟实现这个2bit-map,都是一样的道理。
给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?
解法一:可以用位图/Bitmap的方法,申请512M的内存,一个bit位代表一个unsigned int值。读入40亿个数,设置相应的bit位,读入要查询的数,查看相应bit位是否为1,为1表示存在,为0表示不存在。
解法一:可以用位图/Bitmap的方法,申请512M的内存,一个bit位代表一个unsigned int值。读入40亿个数,设置相应的bit位,读入要查询的数,查看相应bit位是否为1,为1表示存在,为0表示不存在。
使用
PFADD
PFCOUNT
PFMEGER
存储
稀疏存储方式
多个连续桶的计数值都是零
多个连续桶的计数值都是零
00xxxxxx:前缀两个零表示接下来的 6bit 整数值加 1 就是零值计数器的数量,注意这里要加 1 是因为数量如果为零是没有意义的。比如 00010101 表示连续 22 个零值计数器。
01xxxxxx yyyyyyyy:6bit 最多只能表示连续 64 个零值计数器,这样扩展出的 14bit 可以表示最多连续 16384 个零值计数器。这意味着 HyperLogLog 数据结构中 16384 个桶的初始状态,所有的计数器都是零值,可以直接使用 2 个字节来表示。
1vvvvvxx:中间 5bit 表示计数值,尾部 2bit 表示连续几个桶。它的意思是连续 (xx +1) 个计数值都是 (vvvvv + 1)。比如 10101011 表示连续 4 个计数值都是 11。
上面第三种方式 的计数值最大只能表示到 32,而 HyperLogLog 的密集存储单个计数值用 6bit 表示,最大可以表示到 63。当稀疏存储的某个计数值需要调整到大于 32 时,Redis 就会立即转换 HyperLogLog 的存储结构,将稀疏存储转换成密集存储。
密集存储
16384 个 6 bit 连续串成
一个字节是由 8 个 bit 组成的,这样 6 bit 排列的结构就会导致,有一些桶会 跨越字节边界,我们需要 对这一个或者两个字节进行适当的移位拼接 才可以得到具体的计数值。
位图 bitmap
bitmap 可以理解为通过一个 bit 数组来存储特定数据的一种数据结构,每一个 bit 位都能独立包含信息,bit 是数据的最小存储单位,因此能大量节省空间,也可以将整个 bit 数据一次性 load 到内存计算。
指令
SETBIT
GETBIT
BITCOUNT
BITPOS
BITOP
BITFIELD
GETBIT
BITCOUNT
BITPOS
BITOP
BITFIELD
使用场景
布隆过滤器
布隆过滤器(Bloom Filter) 就是这样一种专门用来解决去重问题的高级数据结构。
使用场景
大数据判断是否存在:这就可以实现出上述的去重功能,如果你的服务器内存足够大的话,那么使用 HashMap 可能是一个不错的解决方案,理论上时间复杂度可以达到 O(1 的级别,但是当数据量起来之后,还是只能考虑布隆过滤器。
解决缓存穿透:我们经常会把一些热点数据放在 Redis 中当作缓存,例如产品详情。 通常一个请求过来之后我们会先查询缓存,而不用直接读取数据库,这是提升性能最简单也是最普遍的做法,但是 如果一直请求一个不存在的缓存,那么此时一定不存在缓存,那就会有 大量请求直接打到数据库 上,造成 缓存穿透,布隆过滤器也可以用来解决此类问题。
爬虫/ 邮箱等系统的过滤:平时不知道你有没有注意到有一些正常的邮件也会被放进垃圾邮件目录中,这就是使用布隆过滤器 误判 导致的。
布隆过滤器原理
布隆过滤器的使用
Redis 4.0
bf.add 添加元素
bf.madd 添加多个元素
bf.exists 查询元素是否存在
bf.mexists
bf.reserve 显式创建过滤器
key
error_rate
error_rate 越低,需要的空间越大
initial_size
表示预计放入的元素数量
Java的 Guava带有布隆过滤器实现
// 创建布隆过滤器对象
BloomFilter<Integer> filter = BloomFilter.create(
Funnels.integerFunnel(),
1500,
0.01);
// 判断指定元素是否存在
System.out.println(filter.mightContain(1));
System.out.println(filter.mightContain(2));
// 将元素添加进布隆过滤器
filter.put(1);
filter.put(2);
System.out.println(filter.mightContain(1));
System.out.println(filter.mightContain(2));
内存回收和内存共享
内存回收 因为c语言不具备自动内存回收功能,当将redisObject对象作为数据库的键或值而不是作为参数存储时其生命周期是非常长的,为了解决这个问题,Redis自己构建了一个内存回收机制,通过redisobject结构中的refcount实现.这个属性会随着对象的使用状态而不断变化。
创建一个新对象,属性初始化为1
对象被一个新程序使用,属性refcount加1
对象不再被一个程序使用,属性refcount减1
当对象的引用计数值变为0时,对象所占用的内存就会被释放
内存共享 refcount属性除了能实现内存回收以外,还能实现内存共享
将数据块的键的值指针指向一个现有值的对象
将被共享的值对象引用refcount加1 Redis的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。对于整数值,判断操作复杂度为o(1),对于普通字符串,判断复杂度为o(n);而对于哈希,列表,集合和有序集合,判断的复杂度为o(n^2).虽然共享的对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象。
redis的使用场景
时间轴、队列应用场景设计
购物车开发与设计实战
Redis与Lua模拟抢红包实战
网站投票设计与开发实战
redis的底层协议
能谈下Redis的底层协议吗
RESP协议
RESP是什么,在Redis怎么体现
基于TCP的应用层协议RESP
RESP底层使用的是TCP的连接方式,通过tcp进行数据传输,然后根据解析规则解析相应信息,完成交互
持久化
分类
RDB
RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发
触发机制
手动触发分别对应save和bgsave命令
save命令:阻塞当前Redis服务器,直到RDB过程完成为止,对于内存 比较大的实例会造成长时间阻塞,线上环境不建议使用
bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子 进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短
自动触发RDB的持久
使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改 时,自动触发bgsave。
执行debug reload命令重新加载Redis时,也会自动触发save操作。
默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则 自动执行bgsave。
如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点,
执行流程
执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进 程,如RDB/AOF子进程,如果存在bgsave命令直接返回。
父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞,通 过info stats命令查看latest_fork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒
父进程fork完成后,bgsave命令返回“Background saving started”信息并不再阻塞父进程,可以继续响应其他命令。
子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后 对原有文件进行原子替换。执行lastsave命令可以获取最后一次生成RDB的 时间,对应info统计的rdb_last_save_time选项。
进程发送信号给父进程表示完成,父进程更新统计信息,具体见 info Persistence下的rdb_*相关选项。
优缺点
优点
RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据 快照。非常适用于备份,全量复制等场景。比如每6小时执行bgsave备份, 并把RDB文件拷贝到远程机器或者文件系统中(如hdfs),用于灾难恢复。
Redis加载RDB恢复数据远远快于AOF的方式。
缺点
RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运 行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。
DB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式 的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题。
AOF
开启方式
配置:appendonly yes,默认不开启
AOF文件名 通过appendfilename配置设置,默认文件名是appendonly.aof
执行流程
命令写入 (append)
追加到aof_buf(缓冲区)中
文件同步(sync)
Redis使用单线程响应命令,如 果每次写AOF文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负 载。先写入缓冲区aof_buf中,还有另一个好处,Redis可以提供多种缓冲区同步硬盘的策略,在性能和安全性方面做出平衡
文件重写(rewrite)
AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的
进程内已经超时的数据不再写入文件。
旧的AOF文件含有无效命令,如del key1、hdel key2、srem keys、set a111、set a222等。重写使用进程内数据直接生成,这样新的AOF文件只保留最终数据的写入命令。
多条写命令可以合并为一个,如:lpush list a、lpush list b、lpush list c可以转化为:lpush list a b c。为了防止单条命令过大造成客户端缓冲区溢 出,对于list、set、hash、zset等类型操作,以64个元素为界拆分为多条。
更小的AOF 文件可以更快地被Redis加载
触发
·手动触发:直接调用bgrewriteaof命令。
自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定自动触发时机
·auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认 为64MB。
auto-aof-rewrite-percentage:代表当前AOF文件空间 (aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的比值。
自动触发时机=aof_current_size>auto-aof-rewrite-minsize&&(aof_current_size-aof_base_size)/aof_base_size>=auto-aof-rewritepercentage
重启加载 (load)
AOF持久化开启且存在AOF文件时,优先加载AOF文件,
AOF关闭或者AOF文件不存在时,加载RDB文件,
加载AOF/RDB文件成功后,Redis启动成功。
AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。
Redis 4.0混合持久化
aof-use-rdb-preamble yes
如果开启了混合持久化,aof在重写时,不再是单纯将内存数据转换为RESP命令写入aof文件,而是将重写这一刻之前的内存做rdb快照处理,并且将rdb快照内容和增量的aof修改内存数据的命令存在一起,都写入新的aof文件,新的aof文件一开始不叫appendonly.aof,等到重写完成后,新的aof文件才会进行改名,原子的覆盖原有的aof文件,完成新旧两个aof文件的替换。
于是在redis重启的时候,可以先加载rdb文件,然后再重放增量的aof日志就可以完全替代之前的aof全量文件重放,因此重启效率大幅得到提高。
于是在redis重启的时候,可以先加载rdb文件,然后再重放增量的aof日志就可以完全替代之前的aof全量文件重放,因此重启效率大幅得到提高。
Lua脚本
基本用法
EVAL script numkeys key [key ...] arg [arg ...]
SCRIPT LOAD script
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
SCRIPT EXISTS script [script ...]
SCRIPT FLUSH
SCRIPT KILL
redis-cli --eval /Users/jihite/activeuser.lua user , 1
主要优势
减少网络开销:多个请求通过脚本一次发送,减少网络延迟
原子操作:将脚本作为一个整体执行,中间不会插入其他命令,无需使用事务
复用:客户端发送的脚本永久存在redis中,其他客户端可以复用脚本
可嵌入性:可嵌入JAVA,C#等多种编程语言,支持不同操作系统跨平台交互
发布订阅
思路
Publisher 往 channel 中发布消息时,关注了指定 channel 的 Consumer 就能够同时受到消息。
关注方式( 模式订阅)
命令
# 订阅频道:
SUBSCRIBE channel [channel ....] # 订阅给定的一个或多个频道的信息
PSUBSCRIBE pattern [pattern ....] # 订阅一个或多个符合给定模式的频道
# 发布频道:
PUBLISH channel message # 将消息发送到指定的频道
# 退订频道:
UNSUBSCRIBE [channel [channel ....]] # 退订指定的频道
PUNSUBSCRIBE [pattern [pattern ....]] #退订所有给定模式的频道
缺点
不持久化消息: 如果 Redis 停机重启,PubSub 的消息是不会持久化的,毕竟 Redis 宕机就相当于一个消费者都没有,所有的消息都会被直接丢弃。
没有 Ack 机制,也不保证数据的连续: PubSub 的生产者传递过来一个消息,Redis 会直接找到相应的消费者传递过去。如果没有一个消费者,那么消息会被直接丢弃。如果开始有三个消费者,其中一个突然挂掉了,过了一会儿等它再重连时,那么重连期间的消息对于这个消费者来说就彻底丢失了。
Stream
Redis 5.0 新增了 Stream 数据结构,这个功能给 Redis 带来了 持久化消息队列
Redis Stream 从概念上来说,就像是一个 仅追加内容 的 消息链表,把所有加入的消息都一个一个串起来,每个消息都有一个唯一的 ID 和内容,这很简单,让它复杂的是从 Kafka 借鉴的另一种概念:消费者组(Consumer Group) (思路一致,实现不同)
Consumer Group:消费者组,可以简单看成记录流状态的一种数据结构。消费者既可以选择使用 XREAD 命令进行 独立消费,也可以多个消费者同时加入一个消费者组进行 组内消费。同一个消费者组内的消费者共享所有的 Stream 信息,同一条消息只会有一个消费者消费到,这样就可以应用在分布式的应用场景中来保证消息的唯一性。
last_delivered_id:用来表示消费者组消费在 Stream 上 消费位置 的游标信息。每个消费者组都有一个 Stream 内 唯一的名称,消费者组不会自动创建,需要使用 XGROUP CREATE 指令来显式创建,并且需要指定从哪一个消息 ID 开始消费,用来初始化 last_delivered_id 这个变量。
pending_ids:每个消费者内部都有的一个状态变量,用来表示 已经 被客户端 获取,但是 还没有 ack 的消息。记录的目的是为了 保证客户端至少消费了消息一次,而不会在网络传输的中途丢失而没有对消息进行处理。如果客户端没有 ack,那么这个变量里面的消息 ID 就会越来越多,一旦某个消息被 ack,它就会对应开始减少。这个变量也被 Redis 官方称为 PEL (Pending Entries List)。
消息
消息 ID
消息 ID 如果是由 XADD 命令返回自动创建的话,那么它的格式会像这样:timestampInMillis-sequence (毫秒时间戳-序列号),例如 1527846880585-5,它表示当前的消息是在毫秒时间戳 1527846880585 时产生的,并且是该毫秒内产生的第 5 条消息。
这些 ID 的格式看起来有一些奇怪,为什么要使用时间来当做 ID 的一部分呢? 一方面,我们要 满足 ID 自增 的属性,另一方面,也是为了 支持范围查找 的功能。由于 ID 和生成消息的时间有关,这样就使得在根据时间范围内查找时基本上是没有额外损耗的。
当然消息 ID 也可以由客户端自定义,但是形式必须是 "整数-整数",而且后面加入的消息的 ID 必须要大于前面的消息 ID。
消息内容
消息内容就是普通的键值对,形如 hash 结构的键值对。
命令
增删改查示例
xadd:追加消息
xdel:删除消息,这里的删除仅仅是设置了标志位,不影响消息总长度
xrange:获取消息列表,会自动过滤已经删除的消息
xlen:消息长度
del:删除Stream
独立消费示例
# 从Stream头部读取两条消息
127.0.0.1:6379> xread count 2 streams codehole 0-0
1) 1) "codehole"
2) 1) 1) 1527851486781-0
2) 1) "name"
2) "laoqian"
3) "age"
4) "30"
2) 1) 1527851493405-0
2) 1) "name"
2) "yurui"
3) "age"
4) "29"
# 从Stream尾部读取一条消息,毫无疑问,这里不会返回任何消息
127.0.0.1:6379> xread count 1 streams codehole $
(nil)
# 从尾部阻塞等待新消息到来,下面的指令会堵住,直到新消息到来
127.0.0.1:6379> xread block 0 count 1 streams codehole $
# 我们从新打开一个窗口,在这个窗口往Stream里塞消息
127.0.0.1:6379> xadd codehole * name youming age 60
1527852774092-0
# 再切换到前面的窗口,我们可以看到阻塞解除了,返回了新的消息内容
# 而且还显示了一个等待时间,这里我们等待了93s
127.0.0.1:6379> xread block 0 count 1 streams codehole $
1) 1) "codehole"
2) 1) 1) 1527852774092-0
2) 1) "name"
2) "youming"
3) "age"
4) "60"
(93.11s)
创建消费者示例
127.0.0.1:6379> xgroup create codehole cg1 0-0 # 表示从头开始消费
OK
# $表示从尾部开始消费,只接受新消息,当前Stream消息会全部忽略
127.0.0.1:6379> xgroup create codehole cg2 $
OK
127.0.0.1:6379> xinfo codehole # 获取Stream信息
1) length
2) (integer) 3 # 共3个消息
3) radix-tree-keys
4) (integer) 1
5) radix-tree-nodes
6) (integer) 2
7) groups
8) (integer) 2 # 两个消费组
9) first-entry # 第一个消息
10) 1) 1527851486781-0
2) 1) "name"
2) "laoqian"
3) "age"
4) "30"
11) last-entry # 最后一个消息
12) 1) 1527851498956-0
2) 1) "name"
2) "xiaoqian"
3) "age"
4) "1"
127.0.0.1:6379> xinfo groups codehole # 获取Stream的消费组信息
1) 1) name
2) "cg1"
3) consumers
4) (integer) 0 # 该消费组还没有消费者
5) pending
6) (integer) 0 # 该消费组没有正在处理的消息
2) 1) name
2) "cg2"
3) consumers # 该消费组还没有消费者
4) (integer) 0
5) pending
6) (integer) 0 # 该消费组没有正在处理的消息
组内消费示例
# >号表示从当前消费组的last_delivered_id后面开始读
# 每当消费者读取一条消息,last_delivered_id变量就会前进
127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole >
1) 1) "codehole"
2) 1) 1) 1527851486781-0
2) 1) "name"
2) "laoqian"
3) "age"
4) "30"
127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole >
1) 1) "codehole"
2) 1) 1) 1527851493405-0
2) 1) "name"
2) "yurui"
3) "age"
4) "29"
127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 2 streams codehole >
1) 1) "codehole"
2) 1) 1) 1527851498956-0
2) 1) "name"
2) "xiaoqian"
3) "age"
4) "1"
2) 1) 1527852774092-0
2) 1) "name"
2) "youming"
3) "age"
4) "60"
# 再继续读取,就没有新消息了
127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole >
(nil)
# 那就阻塞等待吧
127.0.0.1:6379> xreadgroup GROUP cg1 c1 block 0 count 1 streams codehole >
# 开启另一个窗口,往里塞消息
127.0.0.1:6379> xadd codehole * name lanying age 61
1527854062442-0
# 回到前一个窗口,发现阻塞解除,收到新消息了
127.0.0.1:6379> xreadgroup GROUP cg1 c1 block 0 count 1 streams codehole >
1) 1) "codehole"
2) 1) 1) 1527854062442-0
2) 1) "name"
2) "lanying"
3) "age"
4) "61"
(36.54s)
127.0.0.1:6379> xinfo groups codehole # 观察消费组信息
1) 1) name
2) "cg1"
3) consumers
4) (integer) 1 # 一个消费者
5) pending
6) (integer) 5 # 共5条正在处理的信息还有没有ack
2) 1) name
2) "cg2"
3) consumers
4) (integer) 0 # 消费组cg2没有任何变化,因为前面我们一直在操纵cg1
5) pending
6) (integer) 0
# 如果同一个消费组有多个消费者,我们可以通过xinfo consumers指令观察每个消费者的状态
127.0.0.1:6379> xinfo consumers codehole cg1 # 目前还有1个消费者
1) 1) name
2) "c1"
3) pending
4) (integer) 5 # 共5条待处理消息
5) idle
6) (integer) 418715 # 空闲了多长时间ms没有读取消息了
# 接下来我们ack一条消息
127.0.0.1:6379> xack codehole cg1 1527851486781-0
(integer) 1
127.0.0.1:6379> xinfo consumers codehole cg1
1) 1) name
2) "c1"
3) pending
4) (integer) 4 # 变成了5条
5) idle
6) (integer) 668504
# 下面ack所有消息
127.0.0.1:6379> xack codehole cg1 1527851493405-0 1527851498956-0 1527852774092-0 1527854062442-0
(integer) 4
127.0.0.1:6379> xinfo consumers codehole cg1
1) 1) name
2) "c1"
3) pending
4) (integer) 0 # pel空了
5) idle
6) (integer) 745505
常见问题
Stream 消息太多怎么办? | Stream 的上限
定长 Stream 功能。在 xadd 的指令提供一个定长长度 maxlen,就可以将老的消息干掉,确保最多不超过指定长度,
PEL 是如何避免消息丢失的?
在客户端消费者读取 Stream 消息时,Redis 服务器将消息回复给客户端的过程中,客户端突然断开了连接,消息就丢失了。但是 PEL 里已经保存了发出去的消息 ID,待客户端重新连上之后,可以再次收到 PEL 中的消息 ID 列表。不过此时 xreadgroup 的起始消息 ID 不能为参数 > ,而必须是任意有效的消息 ID,一般将参数设为 0-0,表示读取所有的 PEL 消息以及自 last_delivered_id 之后的新消息。
Redis Stream Vs Kafka
Redis 基于内存存储,这意味着它会比基于磁盘的 Kafka 快上一些,也意味着使用 Redis 我们 不能长时间存储大量数据。不过如果您想以 最小延迟 实时处理消息的话,您可以考虑 Redis,但是如果 消息很大并且应该重用数据 的话,则应该首先考虑使用 Kafka。
另外从某些角度来说,Redis Stream 也更适用于小型、廉价的应用程序,因为 Kafka 相对来说更难配置一些。
事务
Redis 可以通过 MULTI,EXEC,DISCARD 和 WATCH 等命令来实现事务(transaction)功能。
> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1
使用 MULTI命令后可以输入多个命令。Redis不会立即执行这些命令,而是将它们放到队列,当调用了EXEC命令将执行所有命令。
Redis 是不支持 roll back 的,因而不满足原子性的(而且不满足持久性)。
Redis事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。
Java库
jedis
Lettuce
Redission
Spring Data Redis
Redis 分布式锁
为什么引入分布式锁
我们在系统中修改已有数据时,需要先读取,然后进行修改保存,此时很容易遇到并发问题。由于修改和保存不是原子操作,在并发场景下,部分对数据的操作可能会丢失。在单服务器系统我们常用本地锁来避免并发带来的问题,然而,当服务采用集群方式部署时,本地锁无法在多个服务器之间生效,这时候保证数据的一致性就需要分布式锁来实现。
实现
Redis 锁主要利用 Redis 的 setnx 命令。
加锁命令:SETNX key value,当键不存在时,对键进行设置操作并返回成功,否则返回失败。KEY 是锁的唯一标识,一般按业务来决定命名。
解锁命令:DEL key,通过删除键值对释放锁,以便其他线程可以通过 SETNX 命令来获取锁。
锁超时:EXPIRE key timeout, 设置 key 的超时时间,以保证即使锁没有被显式释放,锁也可以在一定时间后自动释放,避免资源被永远锁住。
加锁解锁伪代码
if (setnx(key, 1) == 1){
expire(key, 30)
try {
//TODO 业务逻辑
} finally {
del(key)
}
}
存在的问题
SETNX 和 EXPIRE 非原子性
如果 SETNX 成功,在设置锁超时时间后,服务器挂掉、重启或网络问题等,导致 EXPIRE 命令没有执行,锁没有设置超时时间变成死锁。
解决这个问题
使用 lua 脚本
if (redis.call('setnx', KEYS[1], ARGV[1]) < 1)
then return 0;
end;
redis.call('expire', KEYS[1], tonumber(ARGV[2]));
return 1;
// 使用实例
EVAL "if (redis.call('setnx',KEYS[1],ARGV[1]) < 1) then return 0; end; redis.call('expire',KEYS[1],tonumber(ARGV[2])); return 1;" 1 key value 100
锁误解除
如果线程 A 成功获取到了锁,并且设置了过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁;随后 A 执行完成,线程 A 使用 DEL 命令来释放锁,但此时线程 B 加的锁还没有执行完成,线程 A 实际释放的线程 B 加的锁。
通过在 value 中设置当前线程加锁的标识,在删除之前验证 key 对应的 value 判断锁是否是当前线程持有。可生成一个 UUID 标识当前线程,使用 lua 脚本做验证标识和解锁操作。
// 加锁
String uuid = UUID.randomUUID().toString().replaceAll("-","");
SET key uuid NX EX 30
// 解锁
if (redis.call('get', KEYS[1]) == ARGV[1])
then return redis.call('del', KEYS[1])
else return 0
end
String uuid = UUID.randomUUID().toString().replaceAll("-","");
SET key uuid NX EX 30
// 解锁
if (redis.call('get', KEYS[1]) == ARGV[1])
then return redis.call('del', KEYS[1])
else return 0
end
超时解锁导致并发
如果线程 A 成功获取锁并设置过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁,线程 A 和线程 B 并发执行。
A、B 两个线程发生并发显然是不被允许的,一般有两种方式解决该问题
将过期时间设置足够长,确保代码逻辑在锁释放之前能够执行完成。
为获取锁的线程增加守护线程,为将要过期但未释放的锁增加有效时间。
不可重入
当线程在持有锁的情况下再次请求加锁,如果一个锁支持一个线程多次加锁,那么这个锁就是可重入的。如果一个不可重入锁被再次加锁,由于该锁已经被持有,再次加锁会失败。
Redis 可通过对锁进行重入计数,加锁时加 1,解锁时减 1,当计数归 0 时释放锁。
本地记录重入次数虽然高效,但如果考虑到过期时间和本地、Redis 一致性的问题,就会增加代码的复杂性。另一种方式是 Redis Map 数据结构来实现分布式锁,既存锁的标识也对重入次数进行计数。
无法等待锁释放
上述命令执行都是立即返回的,如果客户端不可以等待锁释放就无法使用。
解决思路
可以通过客户端轮询的方式解决该问题,当未获取到锁时,等待一段时间重新获取锁,直到成功获取锁或等待超时。这种方式比较消耗服务器资源,当并发量比较大时,会影响服务器的效率。
另一种方式是使用 Redis 的发布订阅功能,当获取锁失败时,订阅锁释放消息,获取锁成功后释放时,发送锁释放消息。
集群
主备切换
为了保证 Redis 的可用性,一般采用主从方式部署。主从数据同步有异步和同步两种方式,Redis 将指令记录在本地内存 buffer 中,然后异步将 buffer 中的指令同步到从节点,从节点一边执行同步的指令流来达到和主节点一致的状态,一边向主节点反馈同步情况。
在包含主从模式的集群部署方式中,当主节点挂掉时,从节点会取而代之,但客户端无明显感知。当客户端 A 成功加锁,指令还未同步,此时主节点挂掉,从节点提升为主节点,新的主节点没有锁的数据,当客户端 B 加锁时就会成功。
集群脑裂
集群脑裂指因为网络问题,导致 Redis master 节点跟 slave 节点和 sentinel 集群处于不同的网络分区,因为 sentinel 集群无法感知到 master 的存在,所以将 slave 节点提升为 master 节点,此时存在两个不同的 master 节点。Redis Cluster 集群部署方式同理。
当不同的客户端连接不同的 master 节点时,两个客户端可以同时拥有同一把锁。
Redis 以其高性能著称,但使用其实现分布式锁来解决并发仍存在一些困难。Redis 分布式锁只能作为一种缓解并发的手段,如果要完全解决并发问题,仍需要数据库的防并发手段。
zookeeper分布式锁
分布式锁与实现(二)—基于ZooKeeper实现
分布式锁与实现(一) —基于Redis实现
企业部署方案
主从复制模式
工作机制
slave启动后,向master发送SYNC命令,master接收到SYNC命令后通过bgsave保存快照(即上文所介绍的RDB持久化),并使用缓冲区记录保存快照这段时间内执行的写命令
master将保存的快照文件发送给slave,并继续记录执行的写命令
slave接收到快照文件后,加载快照文件,载入数据
master快照发送完后开始向slave发送缓冲区的写命令,slave接收命令并执行,完成复制初始化
此后master每次执行一个写命令都会同步发送给slave,保持master与slave之间数据的一致性
优缺点
优点
master能自动将数据同步到slave,可以进行读写分离,分担master的读压力
master、slave之间的同步是以非阻塞的方式进行的,同步期间,客户端仍然可以提交查询或更新请求
缺点
不具备自动容错与恢复功能,master或slave的宕机都可能导致客户端请求失败,需要等待机器重启或手动切换客户端IP才能恢复
master宕机,如果宕机前数据没有同步完,则切换IP后会存在数据不一致的问题
难以支持在线扩容,Redis的容量受限于单机配置
redis的主从模式搭建及注意事项
Sentinel 模式
基本原理
哨兵模式基于主从复制模式,只是引入了哨兵来监控与自动处理故障。
功能
监控master、slave是否正常运行
当master出现故障时,能自动将一个slave转换为master(大哥挂了,选一个小弟上位)
多个哨兵可以监控同一个Redis,哨兵之间也会自动监控
工作机制
在配置文件中通过 sentinel monitor <master-name> <ip> <redis-port> <quorum> 来定位master的IP、端口,一个哨兵可以监控多个master数据库,只需要提供多个该配置项即可。
一条连接用来订阅master的_sentinel_:hello频道与获取其他监控该master的哨兵节点信息
定期(一般10s一次,当master被标记为主观下线时,改为1s一次)向master和slave发送INFO命令
定期向master和slave的_sentinel_:hello频道发送自己的信息
定期(1s一次)向master、slave和其他哨兵发送PING命令
另一条连接定期向master发送INFO等命令获取master本身的信息
获取到slave信息后,哨兵也会与slave建立两条连接执行监控。
通过INFO命令,哨兵可以获取主从数据库的最新信息,并进行相应的操作,比如角色变更等。
如果被PING的数据库或者节点超时,哨兵认为其主观下线.进行选举,故障恢复的操作需要由选举的领头哨兵来执行
选举采用Raft算法
发现master下线的哨兵节点(我们称他为A)向每个哨兵发送命令,要求对方选自己为领头哨兵
如果目标哨兵节点没有选过其他人,则会同意选举A为领头哨兵
如果有超过一半的哨兵同意选举A为领头,则A当选
如果有多个哨兵节点同时参选领头,此时有可能存在一轮投票无竞选者胜出,此时每个参选的节点等待一个随机时间后再次发起参选请求,进行下一轮投票竞选,直至选举出领头哨兵
选出领头哨兵后,领头者开始对系统进行故障恢复,从出现故障的master的从数据库中挑选一个来当选新的master
所有在线的slave中选择优先级最高的,优先级可以通过slave-priority配置
如果有多个最高优先级的slave,则选取复制偏移量最大(即复制越完整)的当选
如果以上条件都一样,选取id最小的slave
优缺点
优点
哨兵模式基于主从复制模式,所以主从复制模式有的优点,哨兵模式也有
哨兵模式下,master挂掉可以自动进行切换,系统可用性更高
缺点
样也继承了主从模式难以在线扩容的缺点,Redis的容量受限于单机配置
需要额外的资源来启动sentinel进程,实现相对复杂一点,同时slave节点作为备份节点不提供服务
Cluster 模式
Cluster模式实现了Redis的分布式存储,即每台节点存储不同的内容,来解决在线扩容的问题。
无中心结构
所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽
节点的fail是通过集群中超过半数的节点检测失效时才生效
客户端与redis节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
工作机制
在Redis的每个节点上,都有一个插槽(slot),取值范围为0-16383
当我们存取key的时候,Redis会根据CRC16的算法得出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作
为了保证高可用,Cluster模式也引入主从复制模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点
当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点都宕机了,那么该集群就无法再提供服务了
Cluster模式集群节点最小配置6个节点(3主3从,因为需要半数以上),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。
优缺点
优点
无中心架构,数据按照slot分布在多个节点。
集群中的每个节点都是平等的关系,每个节点都保存各自的数据和整个集群的状态。每个节点都和其他所有节点连接,而且这些连接保持活跃,这样就保证了我们只需要连接集群中的任意一个节点,就可以获取到其他节点的数据。
可线性扩展到1000多个节点,节点可动态添加或删除
能够实现自动故障转移,节点之间通过gossip协议交换状态信息,用投票机制完成slave到master的角色转换
缺点
客户端实现复杂,驱动要求实现Smart Client,缓存slots mapping信息并及时更新,提高了开发难度。目前仅JedisCluster相对成熟,异常处理还不完善,比如常见的“max redirect exception”
节点会因为某些原因发生阻塞(阻塞时间大于 cluster-node-timeout)被判断下线,这种failover是没有必要的
数据通过异步复制,不保证数据的强一致性
slave充当“冷备”,不能缓解读压力
批量操作限制,目前只支持具有相同slot值的key执行批量操作,对mset、mget、sunion等操作支持不友好
key事务操作支持有线,只支持多key在同一节点的事务操作,多key分布不同节点时无法使用事务功能
不支持多数据库空间,单机redis可以支持16个db,集群模式下只能使用一个,即db 0
其他
基于客户端分片
Twemproxy
Codis
总结
常见问题
简单介绍一下 Redis 呗!
简单来说 Redis 就是一个使用 C 语言开发的数据库,不过与传统数据库不同的是 Redis 的数据是存在内存中的 ,也就是它是内存数据库,所以读写速度非常快,因此 Redis 被广泛应用于缓存方向。
另外,Redis 除了做缓存之外,Redis 也经常用来做分布式锁,甚至是消息队列。
Redis 提供了多种数据类型来支持不同的业务场景。Redis 还支持事务 、持久化、Lua 脚本、多种集群方案。
分布式缓存常见的技术选型方案有哪些?
分布式缓存的话,使用的比较多的主要是 Memcached 和 Redis。不过,现在基本没有看过还有项目使用 Memcached 来做缓存,都是直接用 Redis。
Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来,随着 Redis 的发展,大家慢慢都转而使用更加强大的 Redis 了。
分布式缓存主要解决的是单机缓存的容量受服务器限制并且无法保存通用的信息。因为,本地缓存只在当前服务里有效,比如如果你部署了两个相同的服务,他们两者之间的缓存数据是无法共同的。
说一下 Redis 和 Memcached 的区别和共同点
共同点
都是基于内存的数据库,一般都用来当做缓存使用。
都有过期策略。
两者的性能都非常高。
区别
Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memecache 把数据全部存在内存之中。
Redis 有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
Redis 在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached 在服务器内存使用完之后,就会直接报异常。
Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的.
Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 引入了多线程 IO )
Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。
Memcached过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。
为什么要用 Redis/为什么要用缓存?
操作缓存就是直接操作内存,所以速度相当快。
一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 redis 的情况,redis 集群的话会更高)。
直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高的系统整体的并发。
Redis 单线程模型
Redis 基于 Reactor 模式来设计开发了自己的一套高效的事件处理模型
这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。
Redis 通过IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。
I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗(和 NIO 中的 Selector 组件很像)。
Redis 没有使用多线程?为什么不使用多线程?
虽然说 Redis 是单线程模型,但是, 实际上,Redis 在 4.0 之后的版本中就已经加入了对多线程的支持。
Redis 4.0 增加的多线程主要是针对一些大键值对的删除操作的命令,使用这些命令就会使用主处理之外的其他线程来“异步处理”。
为什么不使用多线程
单线程编程容易并且更容易维护;
Redis 的性能瓶颈不再 CPU ,主要在内存和网络;
多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。
Redis6.0 之后为何引入了多线程?
Redis6.0 引入多线程主要是为了提高网络 IO 读写性能
虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了, 执行命令仍然是单线程顺序执行。
Redis6.0 的多线程默认是禁用的,只使用主线程。
io-threads-do-reads yes
io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程
Redis 给缓存数据设置过期时间有啥用?
因为内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接Out of memory。
比如我们的短信验证码可能只在1分钟内有效,用户登录的 token 可能只在 1 天内有效
Redis是如何判断数据是否过期的呢?
Redis 通过一个叫做过期字典(可以看作是hash表)来保存数据过期的时间。过期字典的键指向Redis数据库中的某个key(键),过期字典的值是一个long long类型的整数,这个整数保存了key所指向的数据库键的过期时间(毫秒精度的UNIX时间戳)。
过期的数据的删除策略了解么?
惰性删除 :只会在取出key的时候才对数据进行过期检查。这样对CPU最友好,但是可能会造成太多过期 key 没有被删除。
定期删除 : 每隔一段时间抽取一批 key 执行删除过期key操作。并且,Redis 底层会并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
扩展
仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就Out of memory了。
怎么解决这个问题呢?答案就是: Redis 内存淘汰机制。
仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就Out of memory了。
怎么解决这个问题呢?答案就是: Redis 内存淘汰机制。
Redis 内存淘汰机制了解么?
volatile-lru(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
4.0 版本后增加
volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
4.0 版本后增加
allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
Redis 持久化机制
见
数据库 -> NoSql -> Redis -> 持久化
数据库 -> NoSql -> Redis -> 持久化
redis的工作原理
redis的并发量是多少
跟计算机性能有关10万
redis的线程模型是什么
redis 实际上是个单线程工作模型
redis的数据类型有哪些
Redis五种数据类型及应用场景
五种数据类型
string
hash
list
set
zset
redis的过期策略
redis的过期策略详解
设置过期时间
expire key time(以秒为单位)--这是最常用的方式
setex(String key, int seconds, String value)--字符串独有的方式
三种过期策略
定时删除
惰性删除
惰性删除为redis服务器内置策略
定期删除
第一、配置redis.conf 的hz选项,默认为10 (即1秒执行10次,100ms一次,值越大说明刷新频率越快,最Redis性能损耗也越大)
第二、配置redis.conf的maxmemory最大值,当已用内存超过maxmemory限定时,就会触发主动清理策略
redis使用的过期策略:惰性删除+定期删除
为什么redis是单线程的但是还可以支撑高并发
文件事件处理器是单线程的
多个 socket
IO 多路复用程序
文件事件分派器
事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
为啥 redis 单线程模型也能效率这么高
1.纯内存操作
2.核心是基于非阻塞的 IO 多路复用机制
3.单线程反而避免了多线程的频繁上下文切换问题
怎么保证redis是高并发以及高可用的
redis 实现高并发主要依靠主从架构,一主多从
高并发的同时,容纳大量的数据,需要redis集群
redis 高可用,如果是做主从架构部署,那么加上哨兵就可以了
怎么保证redis挂掉之后再重启数据可以进行恢复
Redis数据备份和重启恢复
聊聊redis cluster集群模式的原理
一般如何应对缓存雪崩以及穿透问题吗
解决缓存雪崩的方案
事前:redis高可用,主从+哨兵,redis cluster,避免全盘崩溃
事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL被打死
事后:redis持久化,快速恢复缓存数据
缓存穿透现象以及解决方案
每次系统A从数据库只要没有查到,就写一个空值到缓存里去
如何保证缓存与数据库双写时的数据一致性
redis的并发竞争问题该如何解决
什么是Redis的并发竞争问题
生产环境的redis集群的部署架构是什么样的
使用分布式缓存的时候存在问题
常见分布式缓存问题
redis应用
分布式系统的问题
分布式系统常见的几个问题和解决办法
分布式session问题
分布式跨域问题
分步式事务
分布式任务调度
分布式锁
分布式幂等性
分布式缓存
更新数据时,是先删除缓存再更新DB,还是先更新DB再删除缓存?
redis高性能数据库
完善_Redis高性能缓存数据库
redis高速缓存系统
本地缓存Ehcache
计算机缓存的分类
java中常用的几种缓存类型
客户端缓存
页面缓存
浏览器缓存
App客户端缓存
网络缓存
代理缓存
CDN缓存
服务器缓存
数据库缓存
平台缓存级缓存
Ehcache缓存过期策略
FIFO:First In First Out,先进先出。
LRU:Least Recently Used,最近最少使用
LFU:Least Frequently Used,最不经常使用
Ehcache缓存使用
springboot整合Ehcache网络教程
SpringBoot2.0整合Ehcache缓存技术
代码
@Cacheable(value = "myToken")
@CacheEvict(value = "myToken", allEntries = true)
Ehcache与redis整合
Ehcache与redis整合网络教程
Spring+ehcache+redis两级缓存--缓存实战篇(1)
spring整合redis缓存
文档教程
深度解析SpringBoot2.x整合Spring-Data-Redis
源码
SpringBoot整合Redis,RedisTemplate和注解两种方式的使用
spring + redis + spring-data-redis2.0.10 整合-单机版
spring + redis + spring-data-redis2.0.10 整合-redis Cluster版
springboot之使用redistemplate优雅地操作redis
缓存与DB存在不同步
更新DB缓存网络教程
更新数据时,是先删除缓存再更新DB,还是先更新DB再删除缓存?
出现不同步的情况
如果删了缓存,还没有来得及写库,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据
如果先写了库,再删除缓存前,写库后没有删除掉缓存,则也会出现数据不一致情况
如果是redis集群,或者主从模式,写主读从,由于redis复制存在一定的时间延迟,也有可能导致数据不一致
缓存与DB不同步解决
缓存与DB不同步解决网络教程
采用延时双删策略(双淘汰策略)
设置缓存过期时间
异步更新缓存(基于订阅binlog的同步机制)
redis缓存引发问题
缓存击穿、穿透、雪崩
REDIS 缓存的穿透,雪崩和热点key
Redis缓存雪崩、缓存穿透、热点Key解决方案和分析
缓存穿透、缓存击穿、缓存雪崩区别和解决方案
缓存三大问题及解决方案
缓存击穿
缓存穿透
缓存雪崩
redis分布式session
服务器session作用
Session 是客户端与服务器通讯会话技术, 比如浏览器登陆、记录整个浏览会话信息
分布式session不一致
session是存放在服务器上,客户端会使用同一个Sessionid在多个不同的服务器上获取对应的Session,从而会导致Session不一致问题
分布式Session一致性解决方案
用Nginx 做的负载均衡可以添加ip_hash这个配置
使同一个ip的请求发到同一台服务器
没有负载均衡
用haproxy做的负载均衡可以用 balance source这个配置
使同一个ip的请求发到同一台服务器
使用Session集群令牌存放Redis
基于令牌(Token)方式实现Session解决方案,因为Session本身就是分布式共享连接
分布式session之token解决方案实现
spring-session-data-redis框架整合
把session值缓存到redis中
spring-session-data-redis框架步骤教程
SpringBoot+SpringSession+Redis分布式Session解决方案
springboot2.1入门系列四 Spring Session实现session共享
redis分布式线程锁
出现分布式锁的原因
解决同一业务数据并发处理方案
spring redis锁实例
Spring-data-redis + redis 分布式锁(一)
Spring-data-redis + redis 分布式锁(二)
代码
redis系列:基于redis的分布式锁
代码
大牛技术系列教程
zookeeper基本操作
zookeeper网络教程
Docker安装Zookeeper并进行操作
zookeeper应用场景
注册中心
配置中心
消息中间件
分布式事务
分布式锁
选举策略
负载均衡
zookeeper分布式锁
分布式锁与实现(二)—基于ZooKeeper实现
分布式锁与实现(一) —基于Redis实现
redis令牌桶限流器
Java并发:分布式应用限流 Redis + Lua 实践
接口限流算法:漏桶算法&令牌桶算法
redis防止重复提交
【Redis使用系列】使用Redis做防止重复提交
redis的网站计数器
redis列表消息队列
redis整合主要框架
shiro整合redis使用
springboot+shiro+redis项目整合
源码
文档教程
springboot+shiro+redis项目整合
源码
Mongodb
Neo4j
Hbase
MemcacheDB
........
时序数据库
InfluxDB
Druid
ElasticSearch
Prometheus
········
缓存
为什么引入缓存
空间换时间
CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。 再比如操作系统在 页表方案 基础之上引入了 快表 来加速虚拟地址到物理地址的转换。我们可以把块表理解为一种特殊的高速缓冲存储器(Cache)。
我们为了避免用户在请求数据的时候获取速度过于缓慢,所以我们在数据库之上增加了缓存这一层来弥补。
缓存带来的问题
系统复杂性增加 :引入缓存之后,你要维护缓存和数据库的数据一致性、维护热点缓存等等。
系统开发成本往往会增加 :引入缓存意味着系统需要一个单独的缓存服务,这是需要花费相应的成本的,并且这个成本还是很贵的,毕竟耗费的是宝贵的内存。但是,如果你只是简单的使用一下本地缓存存储一下简单的数据,并且数据量不大的话,那么就不需要单独去弄一个缓存服务。
本地缓存
HashMap 和 ConcurrentHashMap
Ehcache 、 Guava Cache 、 Spring Cache、Caffeine
分布式缓存
本地缓存容量受服务部署所在的机器限制明显。 如果当前系统服务所耗费的内存多,那么本地缓存可用的容量就很少。
本地缓存对分布式架构支持不友好,比如同一个相同的服务部署在多台机器上的时候,各个服务之间的缓存是无法共享的,因为本地缓存只在当前机器上有。
缓存读写模式/更新策略
Cache Aside Pattern(旁路缓存模式)
写:更新 DB,然后直接删除 cache 。
读:从 cache 中读取数据,读取到就直接返回,读取不到的话,就从 DB 中取数据返回,然后再把数据放到 cache 中。
Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 DB 的结果为准。另外,Cache Aside Pattern 有首次请求数据一定不在 cache 的问题,对于热点数据可以提前放入缓存中。
Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。
读:从 cache 中读取数据,读取到就直接返回,读取不到的话,就从 DB 中取数据返回,然后再把数据放到 cache 中。
Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 DB 的结果为准。另外,Cache Aside Pattern 有首次请求数据一定不在 cache 的问题,对于热点数据可以提前放入缓存中。
Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。
如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案
缓存失效时间变短(不推荐,治标不治本) :我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
增加cache更新重试机制(常用): 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将 缓存中对应的 key 删除即可。
Read/Write Through Pattern(读写穿透)
Read/Write Through 套路是:服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 DB,从而减轻了应用程序的职责。
写(Write Through):先查 cache,cache 中不存在,直接更新 DB。 cache 中存在,则先更新 cache,然后 cache 服务自己更新 DB(同步更新 cache 和 DB)。
读(Read Through): 从 cache 中读取数据,读取到就直接返回 。读取不到的话,先从 DB 加载,写入到 cache 后返回响应。
Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。
和 Cache Aside Pattern 一样, Read-Through Pattern 也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。
写(Write Through):先查 cache,cache 中不存在,直接更新 DB。 cache 中存在,则先更新 cache,然后 cache 服务自己更新 DB(同步更新 cache 和 DB)。
读(Read Through): 从 cache 中读取数据,读取到就直接返回 。读取不到的话,先从 DB 加载,写入到 cache 后返回响应。
Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。
和 Cache Aside Pattern 一样, Read-Through Pattern 也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。
Write Behind Pattern(异步缓存写入)
Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 DB 的读写。
但是,两个又有很大的不同:Read/Write Through 是同步更新 cache 和 DB,而 Write Behind Caching 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。
Write Behind Pattern 下 DB 的写性能非常高,尤其适合一些数据经常变化的业务场景比如说一篇文章的点赞数量、阅读数量。 往常一篇文章被点赞 500 次的话,需要重复修改 500 次 DB,但是在 Write Behind Pattern 下可能只需要修改一次 DB 就可以了。
但是,这种模式同样也给 DB 和 Cache 一致性带来了新的考验,很多时候如果数据还没异步更新到 DB 的话,Cache 服务宕机就 gg 了。
但是,两个又有很大的不同:Read/Write Through 是同步更新 cache 和 DB,而 Write Behind Caching 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。
Write Behind Pattern 下 DB 的写性能非常高,尤其适合一些数据经常变化的业务场景比如说一篇文章的点赞数量、阅读数量。 往常一篇文章被点赞 500 次的话,需要重复修改 500 次 DB,但是在 Write Behind Pattern 下可能只需要修改一次 DB 就可以了。
但是,这种模式同样也给 DB 和 Cache 一致性带来了新的考验,很多时候如果数据还没异步更新到 DB 的话,Cache 服务宕机就 gg 了。
缓存穿透
缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。
解决办法
最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。
缓存无效 key
布隆过滤器
缓存雪崩
缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。
解决办法
针对 Redis 服务不可用的情况
采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
限流,避免同时处理大量的请求。
针对热点缓存失效的情况
设置不同的失效时间比如随机设置缓存的失效时间。
缓存永不失效。
如何保证缓存和数据库数据的一致性
见
数据库 -> 缓存 -> 缓存读写模式(更新策略)
数据库 -> 缓存 -> 缓存读写模式(更新策略)
软件架构
设计模式
创建型模式
创建型模式(5种)
工厂方法模式(Factory Method)
说明:普通工厂模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建
多个工厂方法模式 ,是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。
静态工厂方法模式 ,将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。
多个工厂方法模式 ,是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。
静态工厂方法模式 ,将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。
抽象工厂模式(Abstract Factory)
说明:工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?就用到抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。因为抽象工厂不太好理解
单例模式(Singleton)
说明:单例对象(Singleton)是一种常用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在
三种好处
1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。
2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。
3、有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。
建造者模式(Builder)
说明:工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象,所谓复合对象就是指某个类具有不同的属性,其实建造者模式就是前面抽象工厂模式和最后的Test结合起来得到的
原型模式(Prototype)
说明:原型模式虽然是创建型的模式,但是与工程模式没有关系,从名字即可看出,该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象类似的新对象。
结构型模式
子主题
结构型模式(7种)
适配器模式
说明:适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。主要分为三类:类的适配器模式、对象的适配器模式、接口的适配器模式。
装饰器模式(Decorator)
说明:顾名思义,装饰模式就是给一个对象增加一些新的功能,而且是动态的,要求装饰对象和被装饰对象实现同一个接口,装饰对象持有被装饰对象的实例。
代理模式(Proxy)
说明:其实每个模式名称就表明了该模式的作用,代理模式就是多一个代理类出来,替原对象进行一些操作
组合模式(Composite)
说明:组合模式有时又叫部分-整体模式在处理类似树形结构的问题时比较方便
外观模式(Facade)
说明:外观模式是为了解决类与类之家的依赖关系的,像spring一样,可以将类和类之间的关系配置到配置文件中,而外观模式就是将他们的关系放在一个Facade类中,降低了类类之间的耦合度,该模式中没有涉及到接口
桥接模式(Bridge)
说明:桥接模式就是把事物和其具体实现分开,使他们可以各自独立的变化。桥接的用意是:将抽象化与实现化解耦,使得二者可以独立变化,像我们常用的JDBC桥DriverManager一样,JDBC进行连接数据库的时候,在各个数据库之间进行切换,基本不需要动太多的代码,甚至丝毫不用动,原因就是JDBC提供统一接口,每个数据库提供各自的实现,用一个叫做数据库驱动的程序来桥接就行了
享元模式(Flyweight)
说明:享元模式的主要目的是实现对象的共享,即共享池,当系统中对象多的时候可以减少内存的开销,通常与工厂模式一起使用。
行为模式
子主题
行为型模式(11种)
父类与子类
策略模式(strategy)
说明:策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。
模板方法模式(Template Method)
说明:解释一下模板方法模式,就是指:一个抽象类中,有一个主方法,再定义1...n个方法,可以是抽象的,也可以是实际的方法,定义一个类,继承该抽象类,重写抽象方法,通过调用抽象类,实现对子类的调用
两个类之间
观察者模式
说明:是一种对象的行为模式,又叫做发布订阅模式,相当于现在的订阅微信公众号功能。微信公众号是被观察者,关注公众号的用户是观察者。这些被观察者与观察者之间存在一对多的关系,当被观察者发生变化时,会通知观察者,让他们能知悉。公众号推送消息,所有的关注用户都可以接收到消息,就是这个道理。
迭代子模式(Iterator)
说明:顾名思义,迭代器模式就是顺序访问聚集中的对象,一般来说,集合中非常常见,如果对集合类比较熟悉的话,理解本模式会十分轻松。这句话包含两层意思:一是需要遍历的对象,即聚集对象,二是迭代器对象,用于对聚集对象进行遍历访问
责任链模式(Chain of Responsibility)
说明:接下来我们将要谈谈责任链模式,有多个对象,每个对象持有对下一个对象的引用,这样就会形成一条链,请求在这条链上传递,直到某一对象决定处理该请求。但是发出者并不清楚到底最终那个对象会处理该请求,所以,责任链模式可以实现,在隐瞒客户端的情况下,对系统进行动态的调整。
命令模式(Command)
说明:命令模式很好理解,举个例子,司令员下令让士兵去干件事情,从整个事情的角度来考虑,司令员的作用是,发出口令,口令经过传递,传到了士兵耳朵里,士兵去执行。这个过程好在,三者相互解耦,任何一方都不用去依赖其他人,只需要做好自己的事儿就行,司令员要的是结果,不会去关注到底士兵是怎么实现的。
类的状态
备忘录模式(Memento)
说明:主要目的是保存一个对象的某个状态,以便在适当的时候恢复对象,个人觉得叫备份模式更形象些,通俗的讲下:假设有原始类A,A中有各种属性,A可以决定需要备份的属性,备忘录类B是用来存储A的一些内部状态,类C呢,就是一个用来存储备忘录的,且只能存储,不能修改等操作
状态模式(State)
说明:核心思想就是:当对象的状态改变时,同时改变其行为,很好理解!就拿QQ来说,有几种状态,在线、隐身、忙碌等,每个状态对应不同的操作,而且你的好友也能看到你的状态,所以,状态模式就两点:1、可以通过改变状态来获得不同的行为。2、你的好友能同时看到你的变化。
通过中间类
访问者模式(Visitor)
说明:访问者模式就是一种分离对象数据结构与行为的方法,通过这种分离,可达到为一个被访问者动态添加新的操作而无需做其它的修改的效果。
中介者模式
说明:中介者模式使用于降低多个对象和类之间的通信复杂度,该模式提供了一个类作为中介者,该类主要处理各个对象之间的通信,使各个对象直接不需要显性的相互引用,从而解耦。这种模式是对象的一种行为模式。换句话说中介者模式就是将对象直接的引用抽取到具体的中介者中
解释器模式(Interpreter)
说明:解释器模式是我们暂时的最后一讲,一般主要应用在OOP开发中的编译器的开发中,所以适用面比较窄。
补充
补充两类
并发型模式
线程池模式
图片了解关系
六大设计原则
开闭原则(Open Close Principle)
说明:开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。
里氏代换原则(Liskov Substitution Principle)
说明:里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
接口隔离原则(Interface Segregation Principle)
说明:这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。
依赖倒转原则(Dependence Inversion Principle)
说明:这个是开闭原则的基础,具体内容:真对接口编程,依赖于抽象而不依赖于具体。
迪米特法则(最少知道原则)(Demeter Principle)
说明:为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。
合成复用原则(Composite Reuse Principle)
说明:原则是尽量使用合成/聚合的方式,而不是使用继承。
软件性能
性能压测
就是考察当前软件和硬件环境下,系统所能承受的最大负荷,并帮助找出系统的瓶颈所在。
目的
为了系统在线上的处理能力和稳定性维持在一个标准范围内,做到知己知彼,百战不殆。还可以发现内存泄漏、并发与同步的问题。
性能指标
● RepsonseTime-RT:响应时间,用户从客户端发起一个请求开始计算,到客户端接收到服务端的响应结束,整个过程所耗费的时间。
● HitsPerSecond-HPS:用户每秒点击次数,也就是每秒向后台发送的请求次数。
● QPS:系统每秒内处理查询的次数。
● MaxRT:最大响应时间,指用户发出请求到服务端返回响应的最大时间。
● MiniRT:最少响应时间,指用户发出请求到服务端返回响应的最少时间。
● 90%响应时间:将所有用户的响应时间进行升序排序,取90%的位置。
● 性能测试关注点:
吞吐量:每秒钟系统能处理的请求数、任务数。
响应时间:服务处理一个请求或一个任务的耗时。
错误率:一批请求中结果出过错的请求所占比例。
● HitsPerSecond-HPS:用户每秒点击次数,也就是每秒向后台发送的请求次数。
● QPS:系统每秒内处理查询的次数。
● MaxRT:最大响应时间,指用户发出请求到服务端返回响应的最大时间。
● MiniRT:最少响应时间,指用户发出请求到服务端返回响应的最少时间。
● 90%响应时间:将所有用户的响应时间进行升序排序,取90%的位置。
● 性能测试关注点:
吞吐量:每秒钟系统能处理的请求数、任务数。
响应时间:服务处理一个请求或一个任务的耗时。
错误率:一批请求中结果出过错的请求所占比例。
算法与数据结构
复杂度概念
时间复杂度:运行时间长短。
计算方式
大O表示法(渐进时间复杂度):把程序的相对执行时间函数T(n)简化为一个数量级,这个数量级可以是n、n^2、logN等。
推导时间复杂度的几个原则:
如果运行时间是常数量级,则用常数1表示。
只保留时间函数中的最高阶项。
如果最高阶项存在,则省去最高项前面的系数。
时间复杂度对比:O(1) > O(logn) > O(n) > O(nlogn) > O(n^2)。
推导时间复杂度的几个原则:
如果运行时间是常数量级,则用常数1表示。
只保留时间函数中的最高阶项。
如果最高阶项存在,则省去最高项前面的系数。
时间复杂度对比:O(1) > O(logn) > O(n) > O(nlogn) > O(n^2)。
空间复杂度:占用内存大小。
计算方式
常量空间 O(1):存储空间大小固定,和输入规模没有直接的关系。
线性空间 O(n):分配的空间是一个线性的集合,并且集合大小和输入规模n成正比。
二维空间 O(n^2):分配的空间是一个二维数组集合,并且集合的长度和宽度都与输入规模n成正比。
递归空间 O(logn):递归是一个比较特殊的场景。虽然递归代码中并没有显式的声明变量或集合,但是计算机在执行程序时,会专门分配一块内存空间,用来存储“方法调用栈”。执行递归操作所需要的内存空间和递归的深度成正比。
线性空间 O(n):分配的空间是一个线性的集合,并且集合大小和输入规模n成正比。
二维空间 O(n^2):分配的空间是一个二维数组集合,并且集合的长度和宽度都与输入规模n成正比。
递归空间 O(logn):递归是一个比较特殊的场景。虽然递归代码中并没有显式的声明变量或集合,但是计算机在执行程序时,会专门分配一块内存空间,用来存储“方法调用栈”。执行递归操作所需要的内存空间和递归的深度成正比。
常见算法
字符串:暴力匹配、BM、KMP、Trie等。
kmp算法
查找:二分查找、遍历查找等。
排序:冒泡排序、快排、计数排序、堆排序等。
搜索:TFIDF、PageRank等。
聚类分析:期望最大化、k-meanings、k-数位等。
深度学习:深度信念网络、深度卷积神经网络、生成式对抗等。
异常检测:k最近邻、局部异常因子等。
hash算法及常用的hash算法
。。。。。。。
数据结构
数组
读取O(1)、更新O(1)、插入O(n)、删除O(n)、扩容O(n)。
链表
读取O(n)、更新O(1)、插入O(1)、删除O(1)。
栈
入栈O(1)、出栈O(1)。
队列
入队 O(1)、出队 O(1)。
哈希表
写入:O(1)、读取:O(1)、扩容O(n)。
哈希冲突
不同的key通过哈希函数获得的下标有可能是相同的。
解决方式
开放寻址法:例子Threadlocal。
链表法:例子Hashmap。
开放寻址法:例子Threadlocal。
链表法:例子Hashmap。
树
定义
树(tree)是n(n≥0)个节点的有限集。
当n=0时,称为空树。在任意一个非空树中,有如下特点:
有且仅有一个特定的称为根的节点。
当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每一个集合本身又是一个树,并称为根的子树。
当n=0时,称为空树。在任意一个非空树中,有如下特点:
有且仅有一个特定的称为根的节点。
当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每一个集合本身又是一个树,并称为根的子树。
树的遍历
深度优先
前序:根节点、左子树、右子树。
中序:左子树、根节点、右子树。
后序:左子树、右子树、根节点。
广度优先
层序:一层一层遍历。
二叉树
二叉树(binary tree)是树的一种特殊形式。二叉,顾名思义,这种树的每个节点最多有2个孩子节点。注意,这里是最多有2个,也可能只有1个,或者没有孩子节点。
一个二叉树的所有非叶子节点都存在左右孩子,并且所有叶子节点都在同一层级上,那么这个树就是满二叉树。
对一个有n个节点的二叉树,按层级顺序编号,则所有节点的编号为从1到n。如果这个树所有节点和同样深度的满二叉树的编号为从1到n的节点位置相同,则这个二叉树为完全二叉树。
二叉查找树
二叉查找树在二叉树的基础上增加了以下几个条件:
如果左子树不为空,则左子树上所有节点的值均小于根节点的值。
如果右子树不为空,则右子树上所有节点的值均大于根节点的值。
左、右子树也都是二叉查找树。
如果左子树不为空,则左子树上所有节点的值均小于根节点的值。
如果右子树不为空,则右子树上所有节点的值均大于根节点的值。
左、右子树也都是二叉查找树。
作用
查找==》二分查找。
排序==》中序遍历。
查找==》二分查找。
排序==》中序遍历。
实现方式
链表。
数组:对于稀疏二叉树来说,数组表示法是非常浪费空间的。
链表。
数组:对于稀疏二叉树来说,数组表示法是非常浪费空间的。
二叉堆
B树、B-树、B+树、B*树
排序算法
https://www.cnblogs.com/onepixel/p/7674659.html
网络
HTTP
简介
HTTP协议就是客户端和服务器交互的一种通迅的格式
告知服务器意图
HTTP提供了好几种方法给我们使用
GET
PUT
HEAD
DELETE
POST
OPTIONS
总的来说:我们现在盛行的RESTful风格就是充分利用了这些方法
持久连接
在HTTP1.0的时候,每一次进行HTTP通信就会断开一次连接
在HTTP1.1版本,就是持久连接了。一次HTTP连接能够处理多个请求
持久连接为“管线化”方式发送成为了可能:在一次HTTP连接里面,不需要等待服务器响应请求,就能够继续发送第二次请求
常用状态码
2XX--一般表示为成功处理
200 正常处理
204 成功处理,但服务器没有新数据返回,显示页面不更新
206 对服务器进行范围请求,只返回一部分数据
更多脑图和最新原创技术文章可关注公众号:Java3y
3XX--一般表示为重定向
301 请求的资源已分配了新的URI中,URL地址改变了。【永久重定向】
302 请求的资源临时分配了新的URI中,URL地址没变【转发】
303 与302相同的功能,但明确客户端应该采用GET方式来获取资源
304 发送了附带请求,但不符合条件【返回未过期的缓存数据】
307 与302相同,但不会把POST请求变成GET
4XX--客户端出错
400 请求报文语法错误了
401 需要认证身份
403 没有权限访问
404 服务器没有这个资源
更多脑图和最新原创技术文章可关注公众号:Java3y
5XX--服务器出错
500 内部资源出错了
503 服务器正忙
HTTPS简述
HTTPS就是披着SSL的HTTP
HTTP在建立通信线路的时候使用公开私有密钥,当建立完连接后,随后就使用共享密钥进行加密和解密了
HTTPS是基于第三方的认证机构来获取认受认可的证书
过程
用户向web服务器发起一个安全连接的请求
服务器返回经过CA认证的数字证书,证书里面包含了服务器的public key(公钥)
用户拿到数字证书,用自己浏览器内置的CA证书解密得到服务器的public key
用户用服务器的public key加密一个用于接下来的对称加密算法的密钥,传给web服务器
4.1因为只有服务器有private key可以解密,所以不用担心中间人拦截这个加密的密钥
服务器拿到这个加密的密钥,解密获取密钥,再使用对称加密算法,和用户完成接下来的网络通信
网站通信粗略过程
DNS:负责解析域名
HTTP:产生请求报文数据
TCP协议:分割HTTP数据,保证数据运输
IP协议:传输数据包,找到通信目的地地址。
HTTP是不保存状态的协议
HTTP是无状态的,也就是说,它是不对通信状态进行保存的。它并不知道之前通信的对方是谁
由于我们很多时候都是需要知道对方是谁,于是我们就有了Cookie来解决
提升传输效率
使用压缩技术把实体主体压小,在客户端再把数据解析
使用分块传输编码,将实体主体分块传输,当浏览器解析到实体主体就能够显示了。
这种技术可以实现断点续传
服务器与客户端之间的应用程序
代理
网关
能够提供非HTTP请求的操作,访问数据库什么的
隧道
建立一条安全的通信路径,可以使用SSL等加密手段进行通信。
HTTP请求和响应报文组成
请求报文
a、请求行:包含请求方法、URI、HTTP版本信息
b、请求首部字段
c、请求内容实体
d、空行
响应报文
A:一个状态行【用于描述服务器对请求的处理结果。】
B:首部字段【用于描述服务器的基本信息,以及数据的描述,服务器通过这些数据的描述信息,可以通知客户端如何处理等一会儿它回送的数据】
C:一个空行
D:实体内容【服务器向客户端回送的数据】
HTTP1.1版本新特性
a、默认持久连接节省通信量,只要客户端服务端任意一端没有明确提出断开TCP连接,就一直保持连接,可以发送多次HTTP请求
b、管线化,客户端可以同时发出多个HTTP请求,而不用一个个等待响应(理论->未实践)
c、断点续传:实际上就是利用HTTP消息头使用分块传输编码,将实体主体分块传输。
HTTP2
HTTP2与HTTP1.1最重要的区别就是解决了线头阻塞的问题!其中最重要的改动是:多路复用 (Multiplexing)
HTTP2所有性能增强的核心在于新的二进制分帧层(不再以文本格式来传输了)
其他重要改动
使用HPACK对HTTP/2头部压缩
服务器推送
流量控制(针对传输中的流进行控制(TCP默认的粒度是针对连接))
流优先级(Stream Priority)它被用来告诉对端哪个流更重要。
OSI七层模型
OSI(Open System Interconnection)参考模型是国际标准化组织(ISO)制定的一个用于计算机或通信系统间互联的标准体系,一般称为OSI参考模型或七层模型。
只要遵循这个七层协议就可以实现计算机互联
OSI七层模型及各层作用
物理层
定义物理设备标准
所有与网络有关的
数据链路层
STP
网卡,交换机
将物理层接收的数据进行MAC(媒体访问控制)地址的封装和解封装,也可以简单的理解为物理寻址
网络层
ip
控制子网的运行,如逻辑编址,分组传输,路由
传输层
定义一些传输数据的协议和端口。
TCP
UDP
会话层
负责在网络中的两节点建立,维持和终止通信
SMTP, DNS
表示层
确保一个系统的应用层发送的消息可以被另一个系统的应用层读取
Telnet
应用层
文件传输,文件管理,电子邮件的信息处理
HTTP、TFTP, FTP, NFS, WAIS、SMTP
TCP/IP协议
TCP 和 UDP
TCP 是面向连接的、可靠的流协议,通过三次握手建立连接,通讯完成时要拆除连接。
UDP是面向无连接的通讯协议,UDP通讯时不需要接收方确认,属于不可靠的传输,可能会出现丢包现象。
三次握手和四次挥手
通俗易懂地讲解TCP建立连接的三次握手和释放连接的四次挥手
理解TCP/IP三次握手与四次挥手的正确姿势
名词解释
ACK : TCP协议规定,只有ACK=1时有效,也规定连接建立后所有发送的报文的ACK必须为1
SYN(SYNchronization) : 在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文。对方若同意建立连接,则应在响应报文中使SYN=1和ACK=1. 因此, SYN置1就表示这是一个连接请求或连接接受报文。
FIN (finis)即完,终结的意思, 用来释放一个连接。当 FIN = 1 时,表明此报文段的发送方的数据已经发送完毕,并要求释放连接。
三次握手
第一次握手:客户端将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给服务器端,客户端进入SYN_SENT状态,等待服务器端确认。
第二次握手:服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,
并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。
第三次握手:客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。
四次挥手
第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
TCP/IP中的数据包
TCP 中通过序列号与确认应答提高可靠性
网络编程常见术语
网络编程基础(网络基本知识)
Socket套接字
Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面
主机 A 的应用程序要能和主机 B 的应用程序通信,必须通过 Socket 建立连接,而建立 Socket 连接必须需要底层TCP/IP 协议来建立 TCP 连接。
建立 TCP 连接需要底层 IP 协议来寻址网络中的主机。
短连接
连接->传输数据->关闭连接
HTTP是无状态的,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接
短连接是指SOCKET连接后发送后接收完数据后马上断开连接
使用场景
WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源
长连接
连接->传输数据->保持连接 -> 传输数据-> 。。。 ->关闭连接
长连接指建立SOCKET连接后不管是否使用都保持连接,但安全性较差。
使用场景
数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费
线上问题解决
cpu过高
什么情况会导致cpu飙高
业务逻辑问题(死循环)、频繁gc以及上下文切换过多。而最常见的往往是业务逻辑(或者框架逻辑)导致的,可以使用jstack来分析对应的堆栈情况。
1.top命令查看当前cpu运行情况,找出线程占用较高的pid进程
2.top -Hp pid
3.转为为16进制:printf "%x" pid -> nid
4.jstack工具查看线程栈情况:jstack pid | grep nid -A 10
2.top -Hp pid
3.转为为16进制:printf "%x" pid -> nid
4.jstack工具查看线程栈情况:jstack pid | grep nid -A 10
内存过高
1.jps -l 查看当前应用的进程号 pid
2.jstat -gcutil -t -h8 pid 1000 (8代表输出8行,1000代表循环输出间隔时间,即1秒钟打印一次)
3.jmap -dump:live,format=b,file=/opt/app/heap.bin 16292 导出dump到制定位置
【jstat -help 看具体参数信息】
列:[root@dmfw-testmgt ~]# jstat -gcutil -t -h8 16292 1000
Timestamp S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
752543.5 0.00 91.32 33.14 69.81 95.39 93.53 8303 107.062 11 2.926 109.988
752545.2 0.00 91.32 33.30 69.81 95.39 93.53 8303 107.062 11 2.926 109.988
752546.2 0.00 91.32 33.30 69.81 95.39 93.53 8303 107.062 11 2.926 109.988
752547.2 0.00 91.32 33.50 69.81 95.39 93.53 8303 107.062 11 2.926 109.988
752548.2 0.00 91.32 33.50 69.81 95.39 93.53 8303 107.062 11 2.926 109.988
752549.2 0.00 91.32 33.65 69.81 95.39 93.53 8303 107.062 11 2.926 109.988
752550.2 0.00 91.32 33.65 69.81 95.39 93.53 8303 107.062 11 2.926 109.988
752551.2 0.00 91.32 33.85 69.81 95.39 93.53 8303 107.062 11 2.926 109.988
输出信息是Timestamp是距离jvm启动的时间,S0、S1、E是新生代的两个Survivor和Eden,O是老年代区,M是Metaspace,CCS使用压缩比例,YGC和YGCT分别是新生代gc的次数和时间,FGC和FGCT分别是老年代gc的次数和时间,GCT是gc的总时间。虽然发生了gc,但是老年代内存占用率根本没下降,说明有的对象没法被回收(当然也不排除这些对象真的是有用)。
频繁gc
jstat -gc pid 1000
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
11264.0 11776.0 0.0 3984.0 247808.0 124569.7 418304.0 142470.4 132864.0 126726.7 16128.0 15082.0 8575 110.269 12 3.179 113.448
11264.0 11776.0 0.0 3984.0 247808.0 124826.8 418304.0 142470.4 132864.0 126726.7 16128.0 15082.0 8575 110.269 12 3.179 113.448
11264.0 11776.0 0.0 3984.0 247808.0 125213.8 418304.0 142470.4 132864.0 126726.7 16128.0 15082.0 8575 110.269 12 3.179 113.448
11264.0 11776.0 0.0 3984.0 247808.0 125213.8 418304.0 142470.4 132864.0 126726.7 16128.0 15082.0 8575 110.269 12 3.179 113.448
11264.0 11776.0 0.0 3984.0 247808.0 125934.8 418304.0 142470.4 132864.0 126726.7 16128.0 15082.0 8575 110.269 12 3.179 113.448
S0C/S1C、S0U/S1U、EC/EU、OC/OU、MC/MU 分别代表两个 Survivor 区、Eden 区、老年代、元数据区的容量和使用量
YGC/YGT、FGC/FGCT、GCT 则代表 YoungGc、FullGc 的耗时和次数以及总耗时
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
11264.0 11776.0 0.0 3984.0 247808.0 124569.7 418304.0 142470.4 132864.0 126726.7 16128.0 15082.0 8575 110.269 12 3.179 113.448
11264.0 11776.0 0.0 3984.0 247808.0 124826.8 418304.0 142470.4 132864.0 126726.7 16128.0 15082.0 8575 110.269 12 3.179 113.448
11264.0 11776.0 0.0 3984.0 247808.0 125213.8 418304.0 142470.4 132864.0 126726.7 16128.0 15082.0 8575 110.269 12 3.179 113.448
11264.0 11776.0 0.0 3984.0 247808.0 125213.8 418304.0 142470.4 132864.0 126726.7 16128.0 15082.0 8575 110.269 12 3.179 113.448
11264.0 11776.0 0.0 3984.0 247808.0 125934.8 418304.0 142470.4 132864.0 126726.7 16128.0 15082.0 8575 110.269 12 3.179 113.448
S0C/S1C、S0U/S1U、EC/EU、OC/OU、MC/MU 分别代表两个 Survivor 区、Eden 区、老年代、元数据区的容量和使用量
YGC/YGT、FGC/FGCT、GCT 则代表 YoungGc、FullGc 的耗时和次数以及总耗时
磁盘
df -h 查看使用情况
[root@dmfw-testmgt app]# iostat -d -k -x
Linux 3.10.0-693.2.2.el7.x86_64 (dmfw-testmgt) 01/21/2022 _x86_64_ (2 CPU)
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.00 0.01 0.00 0.25 0.04 2.15 17.67 0.01 32.89 38.97 32.87 2.69 0.07
sdb 0.00 0.03 0.00 0.40 0.03 102.45 507.58 0.05 129.89 132.38 129.89 3.17 0.13
dm-0 0.00 0.00 0.00 0.18 0.02 1.47 16.89 0.00 27.66 43.81 27.60 3.08 0.05
dm-1 0.00 0.00 0.00 0.00 0.00 0.00 9.93 0.00 42.68 78.76 38.42 9.87 0.00
dm-2 0.00 0.00 0.00 0.00 0.01 0.15 121.58 0.00 198.71 21.40 201.83 4.42 0.00
dm-3 0.00 0.00 0.00 0.08 0.00 0.53 13.09 0.00 39.37 18.74 39.40 4.99 0.04
dm-4 0.00 0.00 0.00 0.43 0.03 102.45 477.66 0.06 129.03 134.67 129.03 2.99 0.13
后一列%util可以看到每块磁盘写入的程度,而rrqpm/s以及wrqm/s分别表示读写速度,一般就能帮助定位到具体哪块磁盘出现问题了
常用命令
Linux
常用基本命令
ls -a 查看当前目录文件
--help 帮助命令
tab 自动补全
Linux命令-文件管理
ls 查看文件信息
cd 切换工作目录
clear 清屏
pwd 显示当前路径
mkdir 创建目录
touch 创建文件
cp 拷贝文件
-R 复制目录
cp -R [源文件或目录] [目的目录]
mv 移动[重命名]文件
rm 删除文件
-f 强制删除
-r 删除文件夹
cat 查看或合并文件
>
先清空再追加
>>
直接追加
ln 建立链接文件
硬链接
ln 源文件 链接文件
软链接
ln -s 源文件 链接文件
find 文件搜索命令
find [搜索路径] [搜寻关键字]
grep 文件搜索命令
-v 显示不包含匹配文本的所有行(相当于求反)
-n 显示匹配行及行号
-i 忽略大小写
grep [-选项] ‘搜索内容串’文件名
tar 解压
-z 打包同时压缩
-x 解开档案文件
-v 显示详细信息
-f 指定压缩后的文件名
tar -zxvf xxx.tar.gz -C ./dir1 #解压到指定目录
unzip 归档管理
unzip xxxx.zip
yum 安装
-y 自动确认
search 搜索
install 下载
yum -y install xxxx 下载
用户、权限管理
whoami 查看当前用户
who 查看登录用户
exit 退出登录账户
useradd 添加用户账号
passwd 设置用户密码
password + 用户
userdel 删除用户
userdel -r (用户名)
su 切换用户
加 "-" 的就是同时切换到用户目录
用户权限管理
cat /etc/group 查看有哪些用户组
groupadd、groupdel 添加、删除组账号
groups +用户 查看用户在那个组
usermod -g 用户组 用户名 修改用户所在组
chmod 修改文件权限
字母法
数字法 chmod +(1-6的三位数字)
4 -------------r
2 -------------w
1 ---------------x
字·母含义
r 可读
w 可写
x 可执行
chown 修改文件所有者
chown + 用户 +文件
chgrp 修改文件所属组
chgrp + 用户 +文件
系统管理
cal 查看当前日历
date 显示或设置时间
date [MMDDhhmm[[CC]YY][.ss]] +format 需要管理员权限
ps 查看进程信息
-a 显示终端上的所有进程,包括其他用户的进程
-u 显示进程的详细状态
-x 显示没有控制终端的进程
-r 只显示正在运行的进程
ps -ef | grep java 查看当前正在运行的进程。
ps -aux | grep java 显示所有状态
top 查看cpu使用率 是否被攻击
kill 杀进程
kill -9 加id
ifconfig 查看或配置网卡信息
ping 测试远程主机连通性
防火墙管理
firewall-cmd --state
查看防火墙状态 iptable未运行防火墙。
service iptables start
开启防火墙
systemctl stop firewalld.service
关闭防火墙
systemctl disable firewalld.service
禁止firewall开机启动
vim/vi
i
插入文本
set:nu
设置行号
:n
到第n行
:%s/被替换的值/新值/g
替换所有的值
:wq
保存退出
:q!
不保存退出
安装JDK
yum search jdk
yum -y instll java-1.8.0-openjdk
mkdir /usr/local/java 创建一个存放jdk的文件
tar -zxvf jdk-8u181-linux-x64.tar.gz -C /usr/local/java 解压jdk到指定目录
cd /usr/local/java/jdk 进入jdk目录
vim /etc/profile 配置环境变量
JAVA_HOME=/usr/local/java/jdk1.8.0_181
PATH=$JAVA_HOME/bin:$PATH
CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export JAVA_HOME
export PATH
export CLASSPATH
PATH=$JAVA_HOME/bin:$PATH
CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export JAVA_HOME
export PATH
export CLASSPATH
source /etc/profile 刷新环境变量
Java -version 查看jdk版本
安装tomcat
mkdir /usr/local/tomca 创建存放文件
tar -zxvf apache-tomcat-8.5.43.tar.gz -C /usr/local/tomcat/ 解压
cd /usr/local/tomcat 进入tomcat文件
mv apache-tomcat-8.5.43/ tomcat1 改名
./tomcat1/bin/startup.sh 启动tomcat
./tomcat1/bin/shutdown.sh 停止tomcat
安装mysql
tar -zxvf mysql-5.7.27-linux-glibc2.12-x86_64.tar.gz
#解压
mv mysql-5.7.27-linux-glibc2.12-x86_64 mysql
#重命名
cp -r mysql /usr/local
#复制解压后的mysql目录
cd /usr/local/mysql/
进入安装mysql软件目录
mkdir -p /data/mysql
创建数据仓库目录
groupadd mysql
新建一个msyql组
useradd -r -s /sbin/nologin -g mysql mysql -d /usr/local/mysql
新建msyql用户禁止登录shell
cd /usr/local/mysql
pwd
chown -R mysql .
chgrp -R mysql .
chown -R mysql /data/mysql
改变目录属有者
bin/mysqld --initialize --user=mysql --basedir=/usr/local/mysql --datadir=/data/mysql
配置参数
bin/mysql_ssl_rsa_setup --datadir=/data/mysql
安装
cd /usr/local/mysql/support-files
cp my-default.cnf /etc/my.cnf 这里没有话就跳过 去查看这个地址有没有my.cnf文件。如果就把权限改了 chmod 777 my.cnf
cp mysql.server /etc/init.d/mysql3
vim /etc/init.d/mysql
basedir=/usr/local/mysql
datadir=/data/mysql
/etc/init.d/mysql start
启动mysql
chmod 777 /etc/my.cnf
如果无法启动执行下面命令
/etc/init.d/mysql start
mysql -u root -p
登陆
如果出现:-bash: mysql: command not found
--就执行: # ln -s /usr/local/mysql/bin/mysql /usr/bin --没有出现就不用执行
--就执行: # ln -s /usr/local/mysql/bin/mysql /usr/bin --没有出现就不用执行
修改密码
set password=password("123456")
grant all privileges on *.* to 'root'@'%' identified by '123456';
flush privileges;
设置root账户的host地址(修改了才可以远程连接)
vim /etc/profile
export PATH=/usr/local/mysql/bin:$PATH
source /etc/profile
添加系统路径【为设置开机启动提供】
chmod 755 /etc/init.d/mysql
chkconfig --add mysql
chkconfig --level 345 mysql on
配置mysql自启
nacat出现 1130 就是没有设置远程连接
发布项目到tomcat
war包
排除tomcatjar包
修改war包为ROOT.war
jar包
nohup java -jar bills.jar >/root/bills/logs/bills8080.log &
后台启动 输出日志
Nginx
安装nginx
yum install gcc-c++
安装
yum install -y pcre pcre-devel
PCRE安装
yum install -y zlib zlib-devel
zlib安装
yum install -y openssl openssl-devel
openssl安装
tar -zxvf nginx-1.16.1.tar.gz
#把解压nginx-1.16.1.tar.gz包
mv nginx-1.16.1 nginx
#修改文件夹的名字
cp -r nginx /usr/local/src
#把nginx拷贝到/usr/local/src里面
cd /usr/local/src
进入目录
mkdir /usr/nginx
创建安装地址
cd /usr/local/src/nginx
指定安装路径
./configure --prefix=/usr/nginx
子主题
make
编译
make install
安装
cd /usr/nginx
进入nginx
./sbin/nginx
启动
./sbin/nginx -s stop
停止
./sbin/nginx/ -s reload
配置文件改变时 刷新配置
Nginx基础配置
cd /usr/nginx/conf
vim nginx.conf
添加
server {
listen 80;
server_name localhost;
location / { #资源地址
root html;
index index.html index.htm; #首页地址
}
}
listen 80;
server_name localhost;
location / { #资源地址
root html;
index index.html index.htm; #首页地址
}
}
动静分离
删除 webapps/ROOT/WEB-INF/classes/static的静态资源
cd /usr/nginx/ 在nginx的安装目录下创建resources
再把项目里面的static里面的内容放到nginx/resources里面
配置nginx/conf/nginx.conf
#动态分离的配置
location ~* \.(css|js|html)$ {
root resources;#可以使用相对路径和绝对路径 /usr/resources就是绝对路径
expires 7d;#有效天数
}
location ~* \.(avi|txt|png|gif|jpg|mp3|mp4|rmvb)$ {
root resources;
expires 20d;
}
location ~* \.(css|js|html)$ {
root resources;#可以使用相对路径和绝对路径 /usr/resources就是绝对路径
expires 7d;#有效天数
}
location ~* \.(avi|txt|png|gif|jpg|mp3|mp4|rmvb)$ {
root resources;
expires 20d;
}
Docker
docker安装
yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
yum -y install gcc
yum -y install gcc-c++
yum install docker
systemctl start docker
systemctl enable docker
开机自启
docker version
查看版本
docker run hello-world
测试运行 hello-world
docker run -p 8080:80 -d docker.io/nginx
运行nginx
systemctl stop docker
yum -y remover docker
rm -rf /var/lib/docker
卸载docker
配值镜像加速
vim /etc/docker/daemon.json
{
"registry-mirrors": ["https://32xw0apq.mirror.aliyuncs.com"]
}
"registry-mirrors": ["https://32xw0apq.mirror.aliyuncs.com"]
}
systemctl daemon-reload
刷新配置
systemctl restart docker
重启docker
docker命令
docker images
列出所有镜像
docker rmi -f 镜像id
根据镜像id删除镜像
docker 镜像命令
docker images
列表本机上的镜像
docker seach
镜像搜索命令
docker pull
镜像下载命令
docker rmi 镜像删除命令
docker rmi -f $(docker images -aq)
删除所有镜像命令
docker 容器命令
docker run -it -d -p 80:8080 --name="" 镜像名:tag/镜像id [命令or 参数]
启动容器
docker ps
-a
查看正在运行的
-q
查看已经死亡的
docker rm -f $(docker ps -aq)
删除所有运行和未运行的容器
exit:
停止容器并退出
ctrl+P+Q
容器不停止退出
docker exec -it 容器ID bash
重新开启一个终端进入容器
docker attach 容器ID
直接使用原来的终端进入容器
docker start|stop|restart 容器ID或容器名
启动停止重启容器
docker kill 容器ID或容器名称
强制停止容器
docker logs 容器ID|名称
查看容器日志
docker top 容器ID
查看容器运行进程
docker inspect 容器ID|名称
查询容器内部细节
docker cp 容器ID(容器名称):容器内文件或文件夹路径 宿主机的路径
从容器中拷贝文件到主机
docker cp 宿主机的路径 容器ID(容器名称):容器内文件或文件夹路径
从主机拷贝文件到容器中
docker commit -m='新的镜像的描述信息' -a='作者' 容器ID 要创建的目标
自定义一个镜像
添加数据卷
docker run -it -v /宿主机目录:/容器内目录 centos /bin/bash
添加数据卷
docker inspect 容器ID
查看容器卷是否挂载成功
查看Mounts的内容
Dockerfile
关键字
FROM
当前新镜像是基于哪个镜像的
MAINTAINER
镜像维护者的姓名和邮箱地址
RUN
容器构建时需要运行的命令
EXPOSE
当前容器对外暴露的端口[只是提示作用,
WORKDIR
指定在创建容器后,终端默认登陆进来的工作目录
ENV
用来在构建镜像过程中设置环境变量
ADD
将宿主机目录下的文件拷贝进镜像并且ADD命令会自动处理URL和解压tar包
COPY
类似ADD,拷贝文件和目录到镜像中
VOLUME
容器数据卷,用于数据保存和持久化工作
CMD
指定一个容器启动时要运行的命令格式
添加的命令会被替换
ENTEYPONT
指定一个容器启动时要运行的命令
在原基础上在加上命令
构建一个tomcat镜像
mkdir -p /mydocker/mytomcat
#创建目录
cd /mydocker/ mytomcat
touch Dockerfile
vim Dockerfile
FROM centos
#作者
MAINTAINER zyj<361072775@qq.com>
#拷贝tomcat jdk 到镜像并解压
ADD apache-tomcat-8.5.30.tar.gz /usr/local/tomcat
ADD jdk-8u202-linux-x64.tar.gz /usr/local/jdk
#定义交互时登录路径
ENV MYPATH /usr/local
WORKDIR $MYPATH
#配置jdk 和tomcat环境变量
ENV JAVA_HOME /usr/local/jdk/jdk1.8.0_202
ENV CATALINA_HOME /usr/local/tomcat/apache-tomcat-8.5.30
ENV CATALINA_BASE /usr/local/tomcat/apache-tomcat-8.5.30
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_HOME/bin
#设置暴露的端口
EXPOSE 8080
#运行tomcat
CMD ["catalina.sh","run"]
#作者
MAINTAINER zyj<361072775@qq.com>
#拷贝tomcat jdk 到镜像并解压
ADD apache-tomcat-8.5.30.tar.gz /usr/local/tomcat
ADD jdk-8u202-linux-x64.tar.gz /usr/local/jdk
#定义交互时登录路径
ENV MYPATH /usr/local
WORKDIR $MYPATH
#配置jdk 和tomcat环境变量
ENV JAVA_HOME /usr/local/jdk/jdk1.8.0_202
ENV CATALINA_HOME /usr/local/tomcat/apache-tomcat-8.5.30
ENV CATALINA_BASE /usr/local/tomcat/apache-tomcat-8.5.30
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_HOME/bin
#设置暴露的端口
EXPOSE 8080
#运行tomcat
CMD ["catalina.sh","run"]
docker build -t mytomcat:1.0
构建镜像
部署项目
docker cp application.yml bills:/root/bills/application.yml
编写Dockerfile
FROM openjdk:8u181-jdk-alpine
#作者
MAINTAINER zyj<361072775@qq.com>
#创建工作目录
RUN mkdir -p /root/myproject/
#把jar包添加到容器里去
ADD bills.jar /root/myproject/
#声明工作路径
ENV ROOT_DIR /root/myproject/
#指定工作目录
WORKDIR $ROOT_DIR
#查看当前路径
RUN pwd
#查看当前路径文件
RUN ls -lh
#设置暴露的端口
EXPOSE 8080
#运行tomcat
CMD ["java","-jar","bills.jar"]
#作者
MAINTAINER zyj<361072775@qq.com>
#创建工作目录
RUN mkdir -p /root/myproject/
#把jar包添加到容器里去
ADD bills.jar /root/myproject/
#声明工作路径
ENV ROOT_DIR /root/myproject/
#指定工作目录
WORKDIR $ROOT_DIR
#查看当前路径
RUN pwd
#查看当前路径文件
RUN ls -lh
#设置暴露的端口
EXPOSE 8080
#运行tomcat
CMD ["java","-jar","bills.jar"]
修改好的application.yml放到容器里面和jar包的同级目录
重启docker 容器
fastdfs
docker搭建fastdfs
yum -y install docker
下载docker
systemctl start docker
启动docler
systemctl enable docker
开机自启
docker run -d --restart=always --privileged=true --net=host --name=fastdfs -e IP=服务器ip -e WEB_PORT=8888 -v ${HOME}/fastdfs:/var/local/fdfs registry.cn-beijing.aliyuncs.com/tianzuo/fastdfs
挂载在容器上
docker exec -it fastdfs /bin/bash
进入容器
echo "Hello FastDFS!">index.html
创建文件
fdfs_test /etc/fdfs/client.conf upload index.html
测试文件上传
端口开放8888 22122-24000
springboot文件上传
配置application.yml
fdfs:
so-timeout: 2500 # 读取时间
connect-timeout: 600 # 连接超时时间
thumb-image: # 缩略图
width: 100
height: 100
tracker-list: # tracker服务配置地址列表
- 服务器ip:22122
so-timeout: 2500 # 读取时间
connect-timeout: 600 # 连接超时时间
thumb-image: # 缩略图
width: 100
height: 100
tracker-list: # tracker服务配置地址列表
- 服务器ip:22122
upload:
base-url: http://服务器ip:8888/
allow-types:
- image/jpeg
- image/png
- image/bmp
- image/gif
base-url: http://服务器ip:8888/
allow-types:
- image/jpeg
- image/png
- image/bmp
- image/gif
pring:
servlet:
multipart:
max-file-size: 2MB
servlet:
multipart:
max-file-size: 2MB
redis
redis的安装
yum install gcc-c++
安装gcc 目地是编译软件
tar -zxvf redis-5.0.7.tar.gz
解压
cp -r /root/software/redis-5.0.7 /usr/local/src/redis
把解压的文件copy到/usr/local/src里面
make hiredis lua jemalloc linenoise
进行编译依赖项
cd /usr/local/src/redis
make
进行编译
mkdir /usr/local/redis
make install PREFIX=/usr/local/redis
make install PREFIX=/usr/local/redis
在上面的Redis目录安装把它安装到/usr/local/redis里面
cd /usr/local/redis/bin
ls
启动文件即安装成功
which redis-server
查看系统里面是否有redis的服务
mkdir /usr/local/redis/conf
创建一个配置文件的目录
cp /usr/local/src/redis/redis.conf /usr/local/redis/conf
复制一个原始的配置文件
cd /usr/local/redis/bin
./redis-server /usr/local/redis/conf/redis.conf
启动redis服务端
vim /usr/local/redis/conf/redis.conf
修改原始配置文件进行后台启动
cd /usr/local/redis/bin
./redis-cli 默认是-h 127.0.0.1 -p 6379
启动客户端
ping
pong
正常连接
./redis-cli shutdown
停止redis
vim /etc/rc.local
/usr/local/redis/bin/redis-server /usr/local/redis/conf/redis.conf
设置开机自启
bin目录
redis-benchmark
redis性能测试工具
redis-check-aof
检查aof日志的工具
redis-check-rdb
检查rdb日志的工具
redis-cli
连接用的客户端
redis-server
:redis服务进程
子主题
redis配置
daemonize:
如需要在后台运行,把该项的值改为yes
pdifile:
把pid文件放在/var/run/redis.pid,可以配置到其他地址
bind:
指定redis只接收来自该IP的请求,如果不设置0.0.0.0,那么将处理所有请求,在生产环节中最好设置该项
port
监听端口,默认为6379
timeout:
设置客户端连接时的超时时间,单位为秒
loglevel:
等级分为4级,debug,revbose,notice和warning。生产环境下一般开启notice
logfile:
配置log文件地址,默认使用标准输出,即打印在命令行终端的端口上
database:
设置数据库的个数,默认使用的数据库是0
save:
设置redis进行数据库镜像的频率
rdbcompression:
在进行镜像备份时,是否进行压缩
dbfilename:
镜像备份文件的文件名
dir:
数据库镜像备份的文件放置的路径
slaveof:
设置该数据库为其他数据库的从数据库
masterauth:
当主数据库连接需要密码验证时,在这里设定
requirepass:
设置客户端连接后进行任何其他指定前需要使用的密码
maxclients:
限制同时连接的客户端数量
maxmemory:
设置redis能够使用的最大内存
appendonly:
开启appendonly模式后,redis会把每一次所接收到的写操作都追加到appendonly.aof文件中,当redis重新启动时,会从该文件恢复出之前的状态
appendfsync:
设置appendonly.aof文件进行同步的频率
vm_enabled:是否开启虚拟内存支持
vm_swap_file:设置虚拟内存的交换文件的路径
vm_max_momery:设置开启虚拟内存后,redis将使用的最大物理内存的大小,默认为0
vm_page_size:设置虚拟内存页的大小
vm_pages:设置交换文件的总的page数量
vm_max_thrrads:设置vm IO同时使用的线程数量
vm_swap_file:设置虚拟内存的交换文件的路径
vm_max_momery:设置开启虚拟内存后,redis将使用的最大物理内存的大小,默认为0
vm_page_size:设置虚拟内存页的大小
vm_pages:设置交换文件的总的page数量
vm_max_thrrads:设置vm IO同时使用的线程数量
redis命令
select index
命令切换数据库
dbsize
查看当前数据库的key的数量
flushdb:
清空当前库
Flushall;
通杀全部库
keys * 获取当前库所有的key
select index 选择第index个库
move k1 1 将当前的数据库key移动到某个数据库,目标库有,则不能移动
flushdb 清空当前库
randomkey 从当前数据库中随机返回
type key 类型
del key1 删除key
exists key 判断是否存在key
expire key 10 设置key的过期时间单位是秒
pexpire key 1000 设置key的过期时间单位是毫秒
persist key 删除过期时间
ttl key 查看还有多少秒过期,-1表示永不过期,-2表示已过期
select index 选择第index个库
move k1 1 将当前的数据库key移动到某个数据库,目标库有,则不能移动
flushdb 清空当前库
randomkey 从当前数据库中随机返回
type key 类型
del key1 删除key
exists key 判断是否存在key
expire key 10 设置key的过期时间单位是秒
pexpire key 1000 设置key的过期时间单位是毫秒
persist key 删除过期时间
ttl key 查看还有多少秒过期,-1表示永不过期,-2表示已过期
string类型相关命令
set key value 存放key-vulue
set name cxx 存放key=name value=cxx
get name 获取key=name的值
getrange name 0 -1 字符串分段 0 -1是全部 0 -2 ==n-1
getset name new_cxx 设置值,返回旧值
mset key1 key2 批量设置
mget key1 key2 批量获取
setnx key value 不存在就插入(not exists)
setrange key index value 从index开始替换value
incr age 递增
incrby age 10 递增
decr age 递减
decrby age 10 递减
incrbyfloat 增减浮点数
append 追加
strlen 长度
object encoding key 得到key 的类型 string里面有三种编码
int 用于能够作用64位有符号整数表示的字符串
embstr 用于长度小于或等于44字节 Redis3.x中是39字节,这种类型的编码在内存使用时性能更好
raw 用于长度大于44字节的
set name cxx 存放key=name value=cxx
get name 获取key=name的值
getrange name 0 -1 字符串分段 0 -1是全部 0 -2 ==n-1
getset name new_cxx 设置值,返回旧值
mset key1 key2 批量设置
mget key1 key2 批量获取
setnx key value 不存在就插入(not exists)
setrange key index value 从index开始替换value
incr age 递增
incrby age 10 递增
decr age 递减
decrby age 10 递减
incrbyfloat 增减浮点数
append 追加
strlen 长度
object encoding key 得到key 的类型 string里面有三种编码
int 用于能够作用64位有符号整数表示的字符串
embstr 用于长度小于或等于44字节 Redis3.x中是39字节,这种类型的编码在内存使用时性能更好
raw 用于长度大于44字节的
list【集合数组】
lpush key values l=left r =rigth
lpush mylist a b c 左插入
rpush mylist x y z 右插入
lrange mylist 0 -1 取出数据集合 0 -1是取出所有 0 1取第第一个和第二个
lpop mylist 弹出集合最后一个元素 弹出之后就没有了哦
rpop mylist 弹出第一个元素 弹出之后就没有了哦
llen mylist 长度
lrem mylist count value 删除
|-COUNT 的值可以是以下几种:
|--count > 0 : 从表头开始向表尾搜索,移除与 VALUE 相等的元素,数量为 COUNT 。
|--count < 0 : 从表尾开始向表头搜索,移除与 VALUE 相等的元素,数量为 COUNT 的绝对值。
|--count = 0 : 移除表中所有与 VALUE 相等的值。
lindex mylist 2 指定索引的值
lset mylist 2 n 索引设值
ltrim mylist 0 4
|--对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
下标 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。
你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。
linsert mylist before a 插入
linsert mylist after a 插入
|--命令用于在列表的元素前或者后插入元素。 当指定元素不存在于列表中时,不执行任何操作。
当列表不存在时,被视为空列表,不执行任何操作。 如果 key 不是列表类型,返回一个错误。
rpoplpush list list2 转移列表的数据
|--命令用于移除列表的最后一个元素,并将该元素添加到另一个列表并返回。
lpush mylist a b c 左插入
rpush mylist x y z 右插入
lrange mylist 0 -1 取出数据集合 0 -1是取出所有 0 1取第第一个和第二个
lpop mylist 弹出集合最后一个元素 弹出之后就没有了哦
rpop mylist 弹出第一个元素 弹出之后就没有了哦
llen mylist 长度
lrem mylist count value 删除
|-COUNT 的值可以是以下几种:
|--count > 0 : 从表头开始向表尾搜索,移除与 VALUE 相等的元素,数量为 COUNT 。
|--count < 0 : 从表尾开始向表头搜索,移除与 VALUE 相等的元素,数量为 COUNT 的绝对值。
|--count = 0 : 移除表中所有与 VALUE 相等的值。
lindex mylist 2 指定索引的值
lset mylist 2 n 索引设值
ltrim mylist 0 4
|--对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
下标 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。
你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。
linsert mylist before a 插入
linsert mylist after a 插入
|--命令用于在列表的元素前或者后插入元素。 当指定元素不存在于列表中时,不执行任何操作。
当列表不存在时,被视为空列表,不执行任何操作。 如果 key 不是列表类型,返回一个错误。
rpoplpush list list2 转移列表的数据
|--命令用于移除列表的最后一个元素,并将该元素添加到另一个列表并返回。
hash Map<String,Map<String,Object>>
hset myhash name cxx
|--命令用于为哈希表中的字段赋值 。
|--如果哈希表不存在,一个新的哈希表被创建并进行 HSET 操作。
|--如果字段已经存在于哈希表中,旧值将被覆盖。
hget myhash name
hmset myhash name cxx age 25 note "i am notes"
hmget myhash name age note
hgetall myhash 获取所有的
hexists myhash name 是否存在
hsetnx myhash score 100 设置不存在的 如果存在,不做处理
hincrby myhash id 1 递增
hdel myhash name 删除
hkeys myhash 只取key
hvals myhash 只取value
hlen myhash 长度
|--命令用于为哈希表中的字段赋值 。
|--如果哈希表不存在,一个新的哈希表被创建并进行 HSET 操作。
|--如果字段已经存在于哈希表中,旧值将被覆盖。
hget myhash name
hmset myhash name cxx age 25 note "i am notes"
hmget myhash name age note
hgetall myhash 获取所有的
hexists myhash name 是否存在
hsetnx myhash score 100 设置不存在的 如果存在,不做处理
hincrby myhash id 1 递增
hdel myhash name 删除
hkeys myhash 只取key
hvals myhash 只取value
hlen myhash 长度
set
sadd myset redis
smembers myset 数据集合
srem myset set1 删除
sismember myset set1 判断元素是否在集合中
scard key_name 个数
sdiff key1 key2 …… | sinter | sunion 操作:集合间运算:差集 | 交集 | 并集
srandmember key count 随机获取集合中的元素
spop 从集合中弹出一个元素
smembers myset 数据集合
srem myset set1 删除
sismember myset set1 判断元素是否在集合中
scard key_name 个数
sdiff key1 key2 …… | sinter | sunion 操作:集合间运算:差集 | 交集 | 并集
srandmember key count 随机获取集合中的元素
spop 从集合中弹出一个元素
zset
zadd zset 1 one
zadd zset 2 two
zadd zset 3 three
zincrby zset 1 one 增长分数
zscore zset two 获取分数
zrange zset 0 -1 withscores 范围值
zrangebyscore zset 10 25 withscores 指定范围的元素
zrangebyscore zset 10 25 withscores limit 1 2 分页
Zrevrangebyscore zset 10 25 withscores 指定范围的值
zcard zset 元素数量
zcount zset 获得指定分数范围内的元素个数
zadd zset 2 two
zadd zset 3 three
zincrby zset 1 one 增长分数
zscore zset two 获取分数
zrange zset 0 -1 withscores 范围值
zrangebyscore zset 10 25 withscores 指定范围的元素
zrangebyscore zset 10 25 withscores limit 1 2 分页
Zrevrangebyscore zset 10 25 withscores 指定范围的值
zcard zset 元素数量
zcount zset 获得指定分数范围内的元素个数
redis相关类型
.字符串类型Map<String,String>
list数据类型Map<String,List<Object>>
hash数据类型Map<String,Map<String,Object>>
set数据类型Map<String,Set<Object,Object>>
zset(sortset)数据类型
redis.conf
Units单位
配置大小单位,开头定义了一些基本的度量单位,只支持bytes,不支持bit
对大小写不敏感
INCLUDES包含
NETWORK网络
bind
prot运行端口
Tcp-backlog
Tcp-backlog
tcp-keepalive 300
GRNERAL通用
daemonize no
是否以守护模式启动,默认为no,配置为yes时以守护模式启动,这时redis instance
supervised no
可以通过upstart和systemd管理Redis守护进程,这个参数是和具体的操作系统相关的。
pidfile /var/run/redis_6379.pid 配置pid文件路径。
当redis以守护模式启动时,如果没有配置pidfile,pidfile默认值是/var/run/redis.pid
loglevel notice
日志级别。可选项有:debug(记录大量日志信息,适用于开发、测试阶段); verbose(较多日志信息); notice(适量日志信息,使用于生产环境);warning(仅有部分重要、关键信息才
日志级别
debug(记录大量日志信息,适用于开发、测试阶段);
verbose(较多日志信息);
notice(适量日志信息,使用于生产环境);
warning(仅有部分重要、关键信息才
syslog-enabled no
是否把日志记录到系统日志。
syslog-ident
设置系统日志的id 如 syslog-ident redis
databases 16
设置数据库的数目。默认的数据库是DB 0 ,可以在每个连接上使用select <dbid> 命令选择一个不同的数据库,dbid是一个介于0到databases - 1 之间的数值。
always-show-logo yes
是否一直显示日志
SNAPSHOTTING快照
save
保存数据到磁盘。格式是:save <seconds> <changes> ,含义是在 seconds 秒之后至少有 changes个keys 发生改变则保存一次。
save 900 1 900秒有一条数据改变就保存
save 300 10 300秒有10条数据改变就保
save 60 10000 600秒有10000条数据改变就保存
stop-writes-on-bgsave-error yes
子主题
默认情况下,如果 redis 最后一次的后台保存失败,redis 将停止接受写操作,这样以一种强硬的方式让用户知道数据不能正确的持久化到磁盘, 否则就会没人注意到灾难的发生。 如果后台保存进程重新启动工作了,redis 也将自动的允许写操作。然而你要是安装了靠谱的监控,你可能不希望 redis 这样做,那你就改成 no 好了。
rdbcompression yes
是否在dump .rdb数据库的时候压缩字符串,默认设置为yes。如果你想节约一些cpu资源的话,可以把它设置为no,这样的话数据集就可能会比较大。
rdbchecksum yes
是否CRC64校验rdb文件,会有一定的性能损失(大概10%)
dbfilename dump.rdb
rdb文件的名字。
dir ./
数据文件保存路径指redis.conf配置文件所在的路径
SECURITY安全
cofig get requirpass
获取密码
cofig set requirpass = ”123456“
auth:123456
LIMITS限制
maxmemory-policy
allkeys-lru
加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键
volatile-lru
加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键
volatile-lfu:
从所有配置了过期时间的键中驱逐使用频率最少的键
allkeys-lfu:
从所有键中驱逐使用频率最少的键
redis持久化
RDB
dump.rdb
优点
适合恢复大量的数据
缺点
但有可能会丢失数据
AOF
appendonly.aof 在配置文件中要开启
优点
数据持久化比较文档 只可能会丢失一秒钟的数据
缺点
没执行一次命令都会写入文件上 文件会过大 恢复时间长
redis的复制
主从复制
①拷贝多个redis.conf文件
②开启daemonize yes
③Pid文件名字
④指定端口
⑤Log文件名字【可以不配置】
⑥Dump.rdb名字
②开启daemonize yes
③Pid文件名字
④指定端口
⑤Log文件名字【可以不配置】
⑥Dump.rdb名字
缺点
主机down了 必须手动指定主机
角色
master
一个
可读可写
slave
多个
可读
哨兵模式
主机挂了 投票新选 会有从机自动顶上
集群
cd /sur/local/redis_cluster
准备一个redis-server
创建多个redis 修改端口 配置文件 cluster-enabled yes 开启集群 appendonly yes 打开aop持久化
同时启动所有redis
docker pull inem0o/redis-trib
下载镜像
docker run -it --net host inem0o/redis-trib create --replicas 1
47.105.128.151:7000 47.105.128.151:7001
47.105.128.151:7002 47.105.128.151:7003
47.105.128.151:7004 47.105.128.151:7005
47.105.128.151:7000 47.105.128.151:7001
47.105.128.151:7002 47.105.128.151:7003
47.105.128.151:7004 47.105.128.151:7005
info replication
查看角色
docker部署redis
docker run -d --name redis -p 6390:6379 redis --requirepass "123456"
springboot集成redis+注解
application.yml
redis:
host: 47.105.128.151
port: 6390
password: 123456
jedis:
pool:
max-active: 20
max-idle: 8
min-idle: 0
max-wait: 5000
host: 47.105.128.151
port: 6390
password: 123456
jedis:
pool:
max-active: 20
max-idle: 8
min-idle: 0
max-wait: 5000
注解
CacheEvict
删除
Cacheput
修改 和 添加
Cacheable
查看
EnableCaching
开启缓存
zookeeper
安装
yum -y install openjdk-1.8.0
配置java环境变量
tar -zxvf zookeeper-3.4.14.tar.gz -C /usr/local
#解压到/usr/local下面
cd /usr/local/
#进入目录
mv zookeeper-3.4.14/ zookeeper
#修改名字
cd /usr/local/zookeeper/conf
#打开zk的配置文件目录
mv zoo_sample.cfg zoo.cfg
#修改zoo_sample.cfg 为zoo.cfg [一定要改]
vim zoo.cfg
#编辑zoo.cfg
./zkServer.sh start
#启动
./zkServer.sh stop
#停止
./zkServer.sh status
查看zk的运行状态
znode
PERSISTENT-持久化目录节点 客户端与zookeeper断开连接后,该节点依旧存在
PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点 客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号
EPHEMERAL-临时目录节点 客户端与zookeeper断开连接后,该节点被删除
EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点 客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号
节点操作
创建节点
create [-s] [-e] path data acl
查看节点内容
get /sanguo
更新节点内容
set path data
删除节点
delete /
zookeeper集群搭建
cd /usr/local/
makdir zk-cluster
cd zk-cluster
makdir zk-cluster
cd zk-cluster
.新建一个集群的文件夹及数据目录
cp
准备三个zookeeper
touch myid
vim zk1/conf/zoo.cfg
dubbo
分布式远程调用
结构
service项目 提供者
提供者
dubbo:application应用程序名称
name
id
dubbo:regist注册中心
address 默认为zookeeper
Redis
要求服务器时间一致
Multicast
Simple
不支持集群
dubbo:protocol 使用的协议
port 端口
20880
当端口为-1时,会自动增加
name 协议的名称
dubbo(默认)
redis
dubbo:service 向外暴露的服务
dubbo:proviter
timeout
proviter
全局配置优先级低
类上优先级中
方法上 优先级高
consumer 比提供者的优先级高
全局配置优先级低
类上优先级中
方法上 优先级高
retries
proviter
2默认 一共三次
0不重试 会有一次
consumer 比提供者的优先级高
2默认 一共三次
0不重试 会有一次
幂等性操作:执行的结果一致
update
dekele
select
非幂等性操作:执行的结果不一致
insert
version
指定包+类名
作用
在消费者做远程调用时能够处理本地的业务逻辑
做远程调用失败时能够有解决的方案
dubbo:annocation:注解形式
DubboService
timeout
retries
version
子主题
子主题
EnableDubbo
启用Dubbo
api项目 接口
domain 项目实体类
实体类必须实现序列化四种方式
Hessian默认
FST
fastjson
java自带序列化
mapper项目 接口
common 项目公共类
web项目
消费者
dubbo:application应用程序名称
dubbo:regist注册中心
double:reference应用远程服务
DubboReference
timeout
超时时间
check
启动时检查
retries
失败重试次数
version
灰度发布
url
宕机直连
子主题
配置文件加载顺序
JVM 启动 -D 参数优先
XML 次之
properties文件
负载均衡策略
Random LoadBalance【默认的】
RoundRobin LoadBalance
轮循,按公约后的权重设置轮循比率。
LeastActive LoadBalance
最少活跃调用数,相同活跃数的随机
ConsistentHash LoadBalance
一致性 Hash,相同参数的请求总是发到同一提供者。
集群容错
失败自动切换Failover Cluster 默认
快速失败Failfast Cluster
安全失败Failsafe Cluster
失败自动恢复Failback Cluster
并行调用Forking Cluster
逐个调用 一个报错直接报错Broadcast Cluster
服务熔断与降级
@EnableHystrix
启用
@DefaultProperties
出错后执行的方法
方法返回值必须一致
@HyxtrixCommand
作用在方法上,出错时会生效
@EnableCircuitBreaker
启用hystrix断路保存
收藏
0 条评论
下一页