typora-copy-images-to: Typora素材
1.Java基础
1.1 面向对象
1.1.1 面向对象和面向过程的区别
1.面向对象更多的是以执行者的角度来对待问题,面向过程更多的是以组织者的角度来对待问题。
2.面向对象的思维主要是关注与找谁来帮我们做这件事,而不关注具体的流程。
1.1.2 面向对象的三个基本特征
封装
继承
多态
1.2 封装、继承、多态
封装:
将一类事物的共同特性封装成抽象的类,让抽象类具备此类事物的基本特征(属性)和功能(方法)。
继承:
让某个类继承封装好的抽象类从而获得抽象类的基本特征和功能。并且可以添加自己独有的特征和功能。继承的实现方式有两种,一种是实现继承(直接使用基类的属性和方法),一种是接口继承(仅使用基类的属性和方法的名称,但是子类必须实现基类的方法)。
多态:
一个类的相同方法在不同的情况下可以有不同的表现形式。简单来说就是一个父类可以有不同的子类。
举例:
假设我们现在有三个职业、分别是老师、java开发工程师、公交车司机。所谓的封装就是将这三个职业的特性进行抽取,比如我们可以抽取一个people类、people类有姓名、年龄等属性,有吃饭,工作等方法。所谓继承就是我们可以分别定义teacher类、让teacher类继承people类、teacher类就有people类的属性和方法。所谓的多态就是指老师、java开发工程师、公交车的工作属性或者说方法的表现形式是不同的。
1.3 JDK.JRE和JVM的区别
JDK是Java开发工具包,提供了Java的开发环境和运行环境。JDK包含了JRE和Java的开发工具
JRE是Java运行时环境,只提供Java的运行环境。JRE包含了JVM和Java程序所需要的核心类库。
JVM是Java虚拟机,java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此java可以实现跨平台。
具体来说,JDK包含了JRE和编译Java源码的编辑器以及Java程序调试和分析的工具。而JRE又包含了JVM。
1.4 ==和equals的区别
==:
对于基本数据类型来说,==比较的是==号两边的值是否相同
对于引用数据类型来说,==比较的是==号两边引用指向的地址是否相同。
equals:
默认比较的是地址值,但是对于String类和Integer类等,内部重写了equals方法,把他们变成了值的对比。
String类重写equals方法的步骤:
1.判断两个对象是否为同一对象,若当前对象和比较的对象是同一个对象,即return true。也就是Object中的equals方法。
2.判断传入的对象是否是String类型,若是String类型,则比较两个字符串的长度,即value.length的长度。
2.1 若长度不相同,则return false
2.2 若长度相同,则按照数组value中的每一位进行比较,不同,则返回false。若每一位都相同,则返回true。
3.若当前传入的对象不是String类型,则直接返回false
Object类中的equals方法:
public boolean equals(Object obj) {
return (this == obj);
}
String类中equals方法:
public boolean equals(Object anObject) {
//判断两个对象是否为同一个对象
//如果是同一个对象,则直接返回true
if (this == anObject) {
return true;
}
//如果不是同一个对象
//判断是否是String类型
if (anObject instanceof String) {
//是string类型
String anotherString = (String)anObject;
int n = value.length;
//判断两个字符串的长度是否相同
//如果相同,会判断每一个位是否相同
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
1.5 Java中的数据类型
基本数据类型:
1.数值型
(1)整数型:byte(1个字节)、short(2)、int(4)、long(8)
(2)浮点型:float(4)、double(8)
2.字符型:char(2)
3.布尔型:boolean(1)
引用数据类型:
1.类
2.接口
3.数组
1.6 哈希值
哈希值是一种算法,让同一个类的对象按照自己不同的特征向量尽可能拥有不同的哈希值。
对于Object类来说,hashcode()方法会返回对象的内存地址经过数据处理的结构,因此,由于每个对象地址不同,其哈希值也不同。
对于String类来说,hashcode()方法会根据String类中的字符串的内容返回哈希值,只要字符串所在的堆空间是相同的,那么哈希值也是相同的
对于Integer类来说,hashcode方法返回的值就是Integer对象所包含的那个整数的数值。比如:Integer i1 = new Integer(100),那么i1的哈希值就是100.所以两个大小相同的Integer对象,返回的哈希值也是相同的。
两个对象的哈希值一样,其equals方法不一定为true。
两个对象的equals方法为true,其哈希值一定相等。
1.7 final
1.7.1 final的作用
1.被final修饰的变量叫常量
如果final修饰的是基本数据类型,那么这个数据的值是不可改变的。
如果final修饰的是引用数据类型,那么这个数据的引用指向的地址是不能改变的,并非值。
2.被final修饰的方法不可被重写
3.被final修饰的类叫最终类,最终类不可被继承
1.7.2 final、finally、finalize的区别
(1)final可以修饰类、方法和变量
(2)finally一般出现在try-catch代码块中,表示不管是否出现异常,finally中的代码块都会执行(如释放资源)
(3)finalize是一个方法,此方法属于Object类,该方法一般由垃圾回收器来调用。当我们调用System.gc()方法的时候,由垃圾回收器调用finalize()方法,回收垃圾。
1.8 String类
1.8 .1 String、StringBuilder和StringBuffer的区别
1.String声明的是一个不可变的对象(String类利用了final修饰的char类型数组存储字符),即每次操作String都会产生新的对象,然后将指针指向新的地址。而StringBuilder和StringBuffer声明的对象是可变的。
2.StringBuilder是线程不安全的,StringBuffer是线程安全的(StringBuffer的每个方法都加了synchronized关键字)
三者如何选择:
1.如果需要频繁的更改字符串的内容,则不要使用String
2.如果是多线程环境,推荐使用StringBuffer
3.在开发环境中,优先采用StringBuilder。
StringBuilder > StringBuffer > String
注:
只有在多个线程访问同一个资源的时候,我们才考虑使用线程安全的问题。但是在实际开发环境中,我们使用StringBuilder的时候往往是用来解决某些特定的问题(比如:字符串的拼接),而此时StringBuilder是出现在具体的业务方法中的。当多个线程访问资源,,每调用一次业务方法,就会在栈内存开辟出一块独立的空间,此空间是被这次的调用独占的,因此StringBuilder也是被独占的,因此就不存在线程安全的问题。
1.8.2 String类的常用方法
equals():比较字符串的内容。
indexOf():参数为一个字符,返回指定字符的索引,如果字符串中有多个该字符,则返回索引最小的那个字符对应的索引
charAt():参数为int类型的索引,返回指定索引处的字符
replace():字符串替换
有两个参数,第一个为要替换的字符,第二个为替换后的字符,如果要替换的字符有多个相同的,则全部替换。
trim():去除两端的空白
length():返回字符串的长度
toLowerCase():将字符串转换成小写字母
toUpperCase():将字符串转换成大写字母
subString():截取字符串
有两个参数,第一个参数是要截取的字符串的开始位置的索引,第二个是结束位置的索引。
split():分隔字符串,返回一个分割后的字符串数组。
有一个参数,此参数为拿什么来进行字符串的分割。
getBytes():返回字符串的byte类型数组。
1.8.3 反转字符串
1.使用StringBuilder的reverse()方法
public static String reverse1(String str){
StringBuilder stringBuilder = new StringBuilder();
StringBuilder append = stringBuilder.append(str);
StringBuilder reverse = append.reverse();
String str1 = reverse.toString();
return str1;
}
2.利用栈先进后出的特点
public static String reverse2(String str){
char[] chars = str.toCharArray();
Stack<Object> stack = new Stack<>();
StringBuilder sb = new StringBuilder();
for (char c : chars) {
stack.push(c);
}
for (int i = 0; i < chars.length; i++) {
sb.append(stack.pop());
}
String str2 = sb.toString();
return str2;
}
1.8.4 String和StringBuilder的相互转换
String转StringBuilder:通过构造方法
String s1 = "湖人总冠军";
StringBuilder s2 = new StringBuilder(s1);
StringBuilder转String:利用StringBuilder的toString()方法
StringBuilder s1 = new StringBuilder();
s1.append("湖人总冠军");
String string = s1.toString();
1.9 抽象类和抽象方法
被abstract修饰的类叫抽象类,被abstract修饰的方法叫抽象方法。
抽象类用于描述一种类型的事物应该具备的基本特征(属性)和功能(方法),具体如何去完成这些行为由子类通过方法重写来实现,也就是说抽象类只有声明的功能没有功能实现的方法。
比如:人出行都是要乘坐交通工具的,但是人和人乘坐的交通工具是不同的。所以我们只能规定人具有出行的功能,但是具体如何出行需要其子类来实现。
注意事项:
1.抽象类无法创建对象,只有被子类继承以后,才可以创建子类的对象。
2.具有抽象方法的一定是抽象类,但是抽象类不一定要有抽象方法(可以有非抽象方法)。
1.10 抽象类和接口
1.抽象类使用abstract修饰,接口使用interface修饰
2.抽象类和接口都不能被实例化,即不能被new。
3.一个类只可以继承一个类,但是可以实现多个接口,即单继承,多实现。
4.抽象类中可以有抽象方法,也可以有非抽象方法,但是接口中只能有抽象方法。
1.11 反射
1.11.1 什么是反射
java的反射机制是指在运行状态中,对于任意一个类,都能知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java的反射机制。
1.11.2 反射机制的应用场景
(1)使用JDBC连接数据库时,使用Class.forName()通过反射加载数据库的驱动程序。
(2)Spring框架通过xml配置模式装载Bean的过程:
1)将所有xml文件或者properties配置文件加载到内存中
2)Java类里面解析xml或者properties中的内容,得到对应的实体类的字节码字符串以及相关的属性
3)使用反射机制,根据获得的实体类的字节码字符串获得某个类的Class实例
4)动态配置实例的属性
1.11.3 java中如何实现反射
(1)通过new对象实现反射机制
Student stu = new Student();
Class classsObj = stu.getClass();
(2)通过路径实现反射机制
Class classObj = Class.forName("fanshe.Student")
(3)通过类名实现反射机制
Class classObj = Student.class;
1.12 Java中创建对象的方式
1.使用new关键字
People people = new People();
2.使用Class类的newInstance方法
People people = People.class.newInstance();
3.使用Constructor的newInstance方法
People people = People.class.getConstructor().newInstance();
4.使用clone方法
People people = new People();
People people = (People)people.clone();
1.13 方法重载和方法重写
1.方法重写指的是子类继承父类,并重写父类中的方法;方法重载是指一个类中,以统一的方式处理不同参数的一种手段。
2.方法重写的两个方法名、参数、返回值都必须相同。方法重载的方法名相同,参数必须不同(参数的顺序、个数、类型至少一个不同),返回值任意。
3.方法重写的访问修饰符的范围要大于等于父类的范围;方法重载的访问修饰符是任意的
2.容器
2.1 集合体系
1.单列集合Collection
1.1 List(存取有序、有索引、元素可重复)
1.1.1 ArrayList:线程不安全,底层是数组,因此查询快,增删慢
1.1.2 LinkedList:线程不安全,底层是链表,因此增删快,查询慢
1.1.3 Vector:线程安全,底层是数组
1.2 Set(存取无序、无索引、元素不可重复)
1.2.1 HashSet:无序、底层是HashMap
1.2.2 TreeSet:有序、底层是TreeMap
2.双列集合Map
2.1 HashMap:线程不安全,允许有一个key为null,存取无序、底层是数组+链表(JDK1.8之前)或者数组+链表/红黑树
2.2 TreeMap:线程不安全,有序(根据key的字典顺序排列)、底层是红黑树
2.3 HashTable:线程安全,不允许key和value为null、底层是数组+链表
2.2 HashMap
2.2.1 HashMap的底层原理
HashMap是基于HashMap算法实现的,我们通过put(key,value)存储,get(key,value)来获取。
存储:
当传入key时,HashMap会根据hash算法计算出key的哈希值,根据key的hash值将键值对保存到bucket中。当计算出的哈希值相同时,我们称之为哈希冲突。HashMap的做法是用链表来存储相同的哈希值的value。
随着存储元素的增多,发生的哈希冲突就会变多,此时链表就会变得很长,而链表的特点是增删快、查找慢,这就导致我们的查找效率变得很低,因为从JDK1.8以后,当链表的长度大于等于8且数组的长度小于64的时候,会将链表转换为红黑树。
问题1:为什么选择红黑树?
红黑树是一个自平衡的二叉查找树,也就是说红黑树的查询效率非常的高。
问题2:为什么不直接把整个链表变成红黑树?
(1)构造红黑树比构造链表复杂,在链表的节点不多的时候,数组+链表的结构的性能是强于数组+链表+红黑树的。
(2)HashMap频繁的扩容,会造成底部的红黑树不断进行拆分和重组,这是十分耗时的。
获取:
当我们调用get()方法时,HashMap会根据hash算法计算出key的哈希值,根据key的hash值去bucket中寻找键值对所在的位置。如果有两个值对象储存在同一个bucket,在找到对应的bucket位置以后,会用key调用equals方法去找到链表中正确的节点,从而取出键值对。
2.2.2 HashMap扩容
当前HashMap的容量不够时(put了过多的元素),HashMap会进行扩容。
HashMap的默认负载因子为0.75,也就是说,当一个map填满了75%的bucket时候,会进行扩容。
扩容步骤:
(1)首先会创建一个原来HashMap底层数组2倍的数组
(2)遍历旧数组中的每一个元素,计算每个元素在新数组中的存储位置
(3)逐个将旧数组中的元素迁移到新数组
(4)将HashMap指向新的数组
(5)重新设置HashMap的阈值
2.2.3 initialCapacity初始容量和loadFactor
initialCapacity表示HashMap的初始容量,官方要求我们输入2的N次方幂,比如:2、4、8、16等,当我们输入的值不是2的N次幂时,系统会自动找一个距离我们输入的值最近的2的N次方幂的值作为初始容量。比如,当我们输入7的时候,系统会使用8作为初始容量。当我们在定义HashMap时没有指定初始容量,会默认取值为16.
loadFactor表示HashMap的负载因子,默认为0.75.负载因子表示一个散列表的空间利用率。initialCapacity和loadFactor的积就是一个HashMap的容量。
2.2.4 HashMap和Hashtable的区别
(1)HashMap是线程非安全的,而Hashtable是线程安全的(Hashtable内部的方法基本都使用了synchronized修饰)
(2)Hashtable的效率低于HashMap
(3)在HashMap中允许一个key为null,允许value为null,而Hashtable不允许任何一个key和value为null;
(4)HashMap的默认初始容量为16,Hashtable默认的初始容量为11
(5)HashMap在扩容时,会把容量变为原来的2倍。Hashtable在扩容时,会把容量变为原来的2倍+1
(6)在JDK1.8之前,HashMap和Hashtable的底层都是数组+链表的结果,但是从JDK1.8开始,当链表的长度大于8且数组长度小于64时,HashMap会将链表转换成红黑树。而Hashtable没有这种机制。
2.2.5 HashMap和TreeMap如何选择
对于在Map中进行插入、删除、定位等操作,无疑HashMap是最好的选择。但是当要对Map的key进行有序的遍历的时候,TreeMap是更好的选择。
2.2.6 HashMap和ConcurrentHashMap的区别
(1)ConcurrentHashMap对底层的数组进行了分隔,将数组分成了多个段(Segment),然后在每一个段(Segment)上加了Lock锁进行保护,因此ConcurrentHashMap是线程安全的。但HashMap是非线程安全的。
(2)HashMap允许一个key为null,允许value为null。而ConcurrentHashMap不允许。
2.3 Hashtable为什么 是线程安全的
Hashtable中诸如put()、contains()等方法都使用了synchronized关键字实现了线程同步。假设有两个线程,如果线程1访问了Hashtable中的同步方法,如果线程2也想同时访问Hashtable中的同步方法就会进入阻塞状态。如:线程1使用put()方法添加元素,那么线程2不仅不能使用put()方法进行添加元素,还不能使用contains()等方法,效率较低。
2.4 ConcurrentHashMap为什么是线程安全的
ConcurrentHashMap使用分段锁的技术,ConcurrentHashMap将数据分成一段一段进行存储,即分成了很多个Segment,然后给每一段配一把锁,当一个线程占用锁访问数据的时候,其它段的数据也能被其它线程访问到。
2.5 Hashtable和ConcurrentHashMap
Hashtable和ConcurrentHashMap实现线程安全的方式是不一样的。
(1)Hashtable使用synchronized来保证线程安全,这种方式效率非常低下
(2)ConcurrentHashMap对整个数组进行分段(Segment),并给每个段(Segment)配锁。当多个线程访问不同段的资源时,就不存在竞争的问题,提高了并发访问率(默认分配成16个段,理论上性能可达到Hashtable的16倍)。
2.6 如何确保一个集合不被修改
可以使用Collections.unmodifiableCollection(Collection c)方法 来创建一个只读集合,这样的话,可以改变集合的任何操作都会被抛出异常。
2.7 Array和ArrayLsit
1.Array是数组,ArrayList是集合。
2.数组可以存储基本数据类型,也可以存储对象,而集合只能存储对象。
3.数组可以固定大小,集合是自动扩容的。
3.数据库
3.1 MySQL
3.1.1 事务的四大隔离级别
1.读未提交(READ_UNCOMMITED):会出现脏读、不可重复读、幻读的问题
2.读已提交(READ_COMMITED):会出现不可重复读、幻读的问题
3.可重复读(REPEATABLE_READ):会出现幻读的问题
4.串行化(SERIALIZIABLE):不会出现任何问题,但是效率最低
MySQL的默认隔离级别是可重复读
Oracle的默认隔离级别是读已提交
3.1.2 脏读、不可重复读、幻读
脏读:读取到别的事务尚未提交的数据。
不可重复读:一个事务多次读取同一数据,得到的数据不同。
幻读:一个事务多次读取数据,得到的结果集不同。
不可重复读和幻读的区别在于,不可重复读是指在多次读取之间,有别的事务修改了数据内容,而幻读是指在多次读取之间,有别的事务增加或者删除了数据,导致数据量发生变化。
3.1.3 什么是事务,事务的四大特性是什么
l,要么同时成功,要么同时失败。
事务的四大特性(也叫事务的酸性,ACID):
(1)原子性:事务是操作的最小单位,不可再分割。要么同时成功,要么同时失败
(2)一致性:事务操作前后,数据的总量不变
(3)独立性:每个事务之间相互独立,互不影响
(4)持久性:事务一旦提交,会在数据库持久化进行存储
3.1.4 什么是数据库的三范式
什么是范式,简而言之,就是一种设计数据库的规范。
第一范式:设计表时,表中的每个字段必须是不可再分的
第二范式:设计表时,必须设计主键,非主键字段要依赖主键字段
第三范式:设计表时,非主键字段之间不能相互依赖
假设要设计一张user表,user包含id、用户名、密码、用户信息四个属性。
第一范式就是说,user表的所有字段都是不可再分的。假设用户信息包含用户姓名、联系电话两个部分,在设计表的时候必须将用户信息设计成用户姓名、联系电话两个字段。
第二范式就是说,user表必须要有主键存在,常常设置为id
第三范式就是说,user表除了id之外的字段之间,不能存在依赖关系。
3.1.5 char和varchar的区别
char:固定的长度,假设设定char的长度为10,即char(10),当输入的数据为“abc”的时候,占用的空间依然是10个字节。abc占三个字节,其余7个是空字节。
varchar:可变的长度,存储的值是每个值占用的字节再加上一个用来记录其长度的字节。
char的优点是效率高,缺点是占用空间较多。varchar的优点是节省空间,但是效率低于char
3.1.6 float和double的区别
float最多可以存储8位的十进制数,在内存中占用4个字节
double最多可以存储16位的十进制数,在内存中占用8个字节
3.1.7 Mysql中的内连接、左外连接、右外连接
首先,内连接的关键字是[inner] join,左外连接的关键字是left [outer] join,右外连接的关键字是right [outer] join
然后,内连接是把两个表的交集部分显示出来;左外连接是显示左表的所有数据,显示右表符合条件的数据;右外连接和左外连接相反
3.1.8 什么是子查询
子查询就是嵌套查询,子查询一般可以分为三类:
(1)子查询的结果是单行单列,此时将子查询的结果作为查询的条件,使用大于,小于等运算符去判断是否符合条件
(2)子查询的结果是多行单列的,此时也将子查询的结果作为查询的条件,使用in运算符去判断是否符合条件
(3)子查询的结果是多行多列的,此时将子查询的结果最为一张虚拟表进行使用
3.1.9 关键字的书写顺序和执行顺序
书写顺序:select、from、where、group by、having、order by
执行顺序:from、where、group by、having、select、order by
having关键字的作用:having配合group by使用,用于对分组之后的数据进行限制,可以使用聚合函数
3.1.10 Mysql的引擎(表的类型)
InnoDB:
InnoDB提供了对数据库ACID事务的支持,并且提供了行级锁和外键的约束,它的设计目标就是处理大数据容量的数据库系统。MySQL在运行的时候,InnoDB会在内存中建立缓存池,用于缓存数据和索引。但是该引擎不支持全文搜索,同时启动也比较慢,它是不会保存表的行数的,所以当进行select count(*) from table指令的时候,需要进行全表扫描。由于锁的力度小,写操作是不会锁定全表的,所以在并发度较高的场景下使用会提升效率。
MyISAM:
MySQL的默认引擎,但是不提供事务的支持,也不支持行级锁和外键。因此当执行插入和更新语句时需要锁定这张表。因此,效率会降低。但是MyISAM保存了表的行数,于是当进行select count(*) from table操作时,可以直接读取已经保存的值而不需要进行全表扫描。所以,当对表的读操作远远多于写操作时,并且不需要事务支持时,可以选择使用MyISAM。
3.1.11 MySQL的表锁和行锁
MyISAM只支持表锁,InnoDB支持表锁和行锁。
表锁:开销小、加锁快、不会出现死锁,但是由于锁的粒度较大,发生锁冲突的概率就高,并发量就低。
行锁:开销大、加锁慢、会出现死锁,但是由于锁的粒度较小,发生锁冲突的概率就低,并发量就高。
3.1.12 sql优化
1.为频繁查询的字段建立索引
2.尽量避免使用select * from,只查询需要的字段即可
3.选择正确的引擎,查询较多时选用MyISAM,插入较多时选择InnoDB
4.索引尽量不要超过6个,超过6个会占用过多的资源
5.使用模糊查询时,不要在like后面的字符串前后都加上%或者
3.1.13 Mysql的默认服务端口是3306
3.1.14 模糊查询
%:表示%可以匹配零个或多个字符的任意字符串
_:表示_可以匹配任何单个字符
3.1.15 分页查询
使用limit关键字,如下:
select * from user limit m,n
表示撇去前m条数据,查询出n条数据
3.2 Redis
3.2.1 Redis简介
3.2.1.1 什么是Redis
Redis是一个由C语言开发的、基于键值对的、非关系型的、高速缓存数据库。
3.2.1.2 Redis 为什么这么快
(1)完全基于内存,绝大多数请求是纯粹的内存操作。
(2)数据结构相对简单
3.2.2 Redis中的数据类型
Redis中的数据类型有:string(字符串)、hash(字典)、list(列表)、set(集合)、zset(有序集合)
(对象) (统一单列)
注:上述所说的数据类型指的是键值对的value,redis中的key一定是string类型
3.2.3 数据类 型的选择问题
(1)如果只是要存储简单的键值对,比如存储短信验证码时的电话号码和验证码,使用string即可(string也可以是数字)
(2)如果要存储的value是一个对象,比如商品,商品包括名称、价格等多个属性,使用hash存储。
(3)如果要存储一些有序且相对固定的数据,使用list。比如微博的关注列表
(4)如果要对两组数据进行一个求交集、并集或者差集的操作,比如微博的共同关注,使用set存储,因为redis中为set提供了求交集(sinter)、并集(sunion)或者差集(sdiff)的功能
(5)如果要对一组数据进行排序,比如畅销榜,使用zset存储
3.2.4 持久化及缓存
RDB:
RDB是redis默认的持久化方式。此种方式会按照一定的时间段将内存中的数据以快照的形式保存到硬盘上,对应产生的文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。
优点:
(1)只有一个文件dump.rdb,方便持久化
(2)容灾性好,一个文件可以保存到安全的硬盘
(3)使用单独的子进程进行持久化,主进程不会进行任何IO操作,保证了redis的高性能。
(4)比AOF启动效率高
缺点:数据安全性低。RDB是间隔一段时间进行持久化,如果在两次持久化之间发生故障,就会出现数据丢失的情况。
AOF:
将redis每次执行的写命令记录到单独的日志文件中,当重启redis时,会重新从持久化的日志文件恢复数据。
优点:数据安全。可以解决数据一致性的问题
缺点:AOF文件比RDB文件大,且恢复速度较慢,比RDB启动效率低。
分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。
可靠性
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
1、互斥性。在任意时刻,只有一个客户端能持有锁。
2、不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
3、具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
4、解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了
缓存:命中:失效:更新:
redis缓存必须实现序列化
常用注解为:
1.@Cacheable 触发缓存填充
2.@CacheEvict 触发缓存驱逐
3.@CachePut 更新缓存而不会干扰方法执行
@Caching 重新组合要在方法上应用的多个缓存操作
@CacheConfig 在类级别共享一些常见的缓存相关设置
3.2.5 如何选择redis的持久化方式
(1)应该同时使用两种持久化方式。在这种情况下,当redis重启时,会先加载AOF文件来恢复数据,这样得到的数据就不会存在缺失的可能。重启之后,redis采用RDB的方式进行持久化,定时的生成RDB快照,非常便于数据库的备份。
(2)如果允许部分数据丢失,可以使用RDB的方式进行持久化。RDB恢复数据集的速度要快于AOF。
3.2.6 什么是缓存穿透,如何解决
缓存穿透是指查询一个一定不存在的数据,由于缓存不命中,需要从数据库进行查询,在数据库查询不到数据则不写入缓存,这将导致不存在的数据每次都要去数据库查询,失去了缓存的意义。缓存穿透如果发生了,就有可能搞垮我们的数据库。从而导致整个服务瘫痪。
解决方案:
由于请求参数是不合法的,我们可以使用布隆过滤器提前拦截请求。如果参数不合法就不让请求到数据库。
当我们从数据库找不到的时候,我们就将数据库返回的空结果写入缓存,下次再请求的时候,就可以从缓存中获取了。此时,我们需要给空结果设置一个较短的过期时间,最长不超过五分钟。
3.2.7 什么是缓存雪崩,如何解决
所谓的缓存雪崩就是指:redis的缓存挂掉了,所有的请求走数据库。
因为内存昂贵而且一般容量较小,所以Redis不可能把所有数据都保存起来。因此Redis需要对数据设置过期时间,并采用惰性删除+定期删除两种策略对过期键进行删除。如果缓存设置的过期时间是相同的,并且Redis恰好将这部分数据全部删除了,这就会导致在这段时间内,这些缓存同时失效,全部请求到数据。
解决方案:
在设置缓存过期时间的时候加上一个随机值,这样就会大幅度的减少缓存同时过期的可能。
对于redis挂掉,请求全部走数据库的情况,有以下三种可能:
(1)事发前:搭建redis集群,实现Redis的高可用
(2)事发时:万一redis真的挂了,我们可以使用Hystrix(熔断器)进行服务熔断、服务降级或者服务限流,避免数据库崩溃,从而导致整个服务瘫痪。
(3)事发后:redis持久化,重启之后从磁盘上加载数据,恢复缓存数据。
3.2.8 什么是缓存击穿,如何解决
缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库。
解决方案:
一般情况就是将该热点key的信息设置为永不过期。
3.2.9 如何保证缓存和数据库信息的一致
(1)合理的设置缓存的过期时间
(2)新增、更改、删除数据库操作时,同步更新redis,可以使用事务机制来保证数据的一致。
3.2.10 过期删除策略
(1)定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间立即清除。该策略可以立即清除过期数据,对内存十分友好,但是会占用大量的CPU资源去处理过期的数据。从而影响缓存的响应时间和吞吐。
(2)惰性过期:只有当访问一个key时,才会判断该key是否过期,过期则清除。该策略可以最大化的节省CPU资源。但对内存非常不友好。可能出现大量的过期key没有被再次访问,也就不会被清除,十分占用内存资源。
(3)定期过期:每隔一定时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除已经过期的key。
redis中同时使用了惰性过期和定期过期两种策略。
3.2.11 Redis淘汰策略有哪些(内存淘汰策略)
no-eviction:禁止驱逐数据。当内存不足以容纳新写入的数据时,新的写入操作会报错
allkeys-lru:当内存不足以容纳新写入的数据时,移除最少使用的key
allkey-radom:当内存不足以容纳新写入的数据时,随机移除某个key
volatile-lru:当内存不足以容纳新写入的数据时,在设置了过期时间的键空间中,移除最少使用的key
volatile-radom:当内存不足以容纳新写入的数据时,在设置了过期时间的键空间中,随机移除某个key
volatile-rttl:当内存不足以容纳新写入的数据时,在设置了过期时间的键空间中,过期时间最早的key优先被移除
3.3 关系型数据库和非关系型数据库
关系型数据库就是基于关系模型创建的数据库。所谓的关系模型就是指:一对一、一对多、多对多等关系模型。
一对一:比如一个丈夫对应一名妻子
一对多:比如一个老师对应多个学生
多对多:比如多个部门对应多个员工
常见的关系型数据库有MySQL、Oracle等
关系型数据库的优势:
(1)通用的SQL语言使得关系型数据库使用方便
(2)SQL语言具有强大的查询能力,可以进行十分复杂的查询
非关系型数据库主要是基于非关系模型的数据库。
常见的非关系模型有:
键值对模型:存储的数据是键值对模型的,redis数据库采用的此种模型
列模型:存储的数据是一列一列的
文档类模型:存储的数据是一个个文档,MongoDB数据库采用此种模型
非关系型数据库的优势:
(1)无需经过SQL解析,因此读写性能很高
(2)数据之间关系不紧密,即没有耦合性,因此扩展方便
(3)可以存储多种数据类型,如图片、文档等
4.多线程
4.1 进程和线程
进程指的是正在运行的程序
线程指的是进程的执行路径
一个程序中有一个或者多个进程,一个进程中有一个或者多个线程
4.2 实现多线程的方式
1.继承Thread类
2.实现runnanle()接口
3.实现callable()接口
4.通过线程池
4.3 runnable和callable有什么不同
1.runnable接口中的方法为run(),callable接口中的方法为call()
2.run()方法没有返回值,call()方法有返回值
3.run()方法只能抛出运行时异常,call()方法允许抛出异常,也可以捕获异常。
4.4 run()和start()有什么区别
1.run()方法用于封装线程执行的代码,调用run()方法相当于普通方法的调用。
2.start()方法用于启动线程,然后由jvm调用run()方法。
3.run()方法可以多次调用,start()方法只可以调用一次。
4.5 线程的状态
1.NEW(新建状态):当创建出线程对象,未调用start()方法之前,线程出于新建状态。此时线程没有执行资格,更没有执行权。
2.RUNNABLE(就绪状态):当线程启动(即调用start()方法)之后,线程进入就绪状态。此时线程有执行资格,但是没有执行权,需要和其它线程一起抢夺CPU的执行权。进入就绪状态的几种可能如下:
(1)新创建的线程调用start()方法之后
(2)处于阻塞状态的线程获取到锁以后
(3)处于等待状态的线程被唤醒以后且能获得锁
(4)处于超时等待状态的线程被唤醒或者超时时间已到且能获得锁
3.BLOCKED(阻塞状态):当线程进入synchronized块或者方法时,锁被其它线程占用。进入阻塞状态的几种可能如下:
(1)当前线程需要获得的锁正在被其它线程占用
(2)等待状态的线程被唤醒后无法获得锁
(3)等待超时状态的线程被唤醒或者超时时间已到,但是无法获得锁
4.WAITING(无限期等待状态):当线程调用wait()方法,或者其它线程调用join()方法,并未设置时间值,线程则进入等待状态。
5.TIME_WAITING(限期等待状态):当线程调用wait()方法,或者其它线程调用join()方法,且设置了时间值,线程则进入超时等待状态。
6.TERMINATED(终止状态):当线程执行完毕,或者发生异常,线程进入终止状态。
(1)run()方法正常执行完毕
(2)未捕获的异常终止了run()方法的执行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0IaAoZ40-1621934084660)(C:\Users\1\Desktop\Typora素材\线程执行流程图.png)]
4.6 sleep()和wait()有什么区别
1.sleep()来自于Thread类,wait()来自于Object类。
2.两者都让出了CPU的执行权,但是sleep()不释放锁,即其它需要锁的线程无法执行;wait()释放锁,其它需要锁的线程可以执行
3.sleep()必须设置参数(即休眠时间),到时间会自动恢复,wait()方法可以设置参数,也可以不设置参数,如果未设置参数则需要使用notify()/notifyAll()唤醒。
4.7 notify()和notifyAll()有什么区别
notifyAll()会唤醒所有线程,将所有线程从等待池移动到锁池,参与锁的竞争
notify()只会唤醒一个线程,具体唤醒哪个线程由虚拟机控制。
4.8 线程池
4.8.1 为什么要使用线程池
1.降低资源消耗,能有效的防止频繁的创建线程和销毁线程对性能带来的开销。
2.提高效应速度,当任务到达时,不需要创建线程对象即可立即执行。
3.提高线程的可管理性,可统一分配、调优和监控
4.8.2 创建线程池
1.new ThreadPoolExcutor(),最原始的方式
2.调用Excutors类的方法:
(1)newCachedThreadPool():一个任务创建一个线程
(2)newFixedThreadPool(int n):创建固定数目的线程
(3)newSingleThreadExecutor:只创建一个工作线程
4.8.3 线程池的状态
1.RUNNING:接收新的任务,处理等待队列中的任务
2.SHUTDOWN:不接收新的任务,但是会处理等待队列中的任务。调用线程池的shutdown()方法,线程池由RUNNING -> SHUTDOWN。
3.STOP:不接收新的任务,不再处理等待队列的任务,中断正在执行的任务。调用线程池的shutdownNow()方法时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
4.TIDYING:销毁所有任务。当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
5.TERMINATED:线程池彻底终止。线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
4.8.4 线程池中的executor()和submit()方法
1.executor()和submit()都可以用于线程的启动
2.executor()只能执行Runnable类型的任务,submit()可以执行Runnable和Callable类型的任务。也就是说executor()没有返回值,submit()可以有返回值。
4.9 Java中如何保证多个线程之间的安全
1.使用线程安全的类:从java1.5开始,提供了java.util.concurrent包,在此包中增加了在并发编程中很常用的工具类.比如ConcurrentHashMap
2.使用自动锁synchronized(JVM来实现)
1.同步代码块:
synchronized (this) {
// ...
}
2.同步方法:
public synchronized void 方法名 () {
// ...
}
3.同步类:
public void func() {
synchronized (类名.class) {
// ...
}
}
3.使用锁Lock(JDK实现)
Lock lock = new ReetrantLock();
//加锁
lock.lock();
try{
//...
} catch(Exception e){
//...
}finally{
//手动释放锁
lock.unlock();
}
4.10 synchronized和lock的区别
1.锁的实现
synchronized是由JVM实现的,ReetrantLock是由JDK实现的。
2.使用方法
synchronized不需要用户手动去释放锁,当synchronized代码执行完成后,系统会自动让线程释放对锁的占用; ReentrantLock则需要用户手动去释放锁,若没有主动释放锁,就有可能导致出现死锁现象。需要lock()、unlock()来完成。
3.加锁是否公平
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的(构造方法可以传入boolean值,传入的值为true表示公平锁,传入的值为false表示非公平锁)。
4.等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock 可中断,而 synchronized 不行。
5.锁绑定多个条件
一个ReentrantLock 可以同时绑定多个 Condition 对象。
4.11 线程的相关方法
1.sleep(int 毫秒值):让当前线程休眠指定的毫秒值
2.join():如果有一个线程A正在运行,此时要求线程B必须先执行完毕,再执行线程A,则需要线程B调用join()方法。当线程B执行完毕,会继续执行线程A。
3.interrupt():通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
4.setPriority(int n):设置线程的优先级,参数n为线程的优先级,n的范围为1-10,默认为5,数字越大优先级越高,如果超出范围,则会抛出非法参数异常。
5.框架
5.1 Spring
5.1.1 Spring框架的优势是什么
1.方便解耦,简化开发。
Spring提供了IOC技术,可以将对象的创建权和管理权交给spring容器。我们在使用某个对象的时候,不需要去创建对象,而是通过注入的方式直接调用。
2.Spring提供了AOP技术
Spring提供的AOP技术可以让我们对一些业务逻辑进行集中处理,比如日志打印,异常处理。
3.Spring支持声明式事务
声明式事务管理是针对系统层面的服务,不侵入逻辑代码。我们只需要通过修改配置,即可进行事务管理。
4.spring可以集成很多优秀的框架,如:mybatis。
5.spring降低了对java相关api的使用难度。
比如spring对jdbc连接驱动进行了封装,我们只需要使用JDBCTemplate等模板类即可。
5.1.2 AOP
5.1.2.1 AOP简介
AOP就是面向切面编程。面向切面编程是一种通过预编译方式和运行期动态代理技术实现的程序功能统一维护的技术。它可以在程序运行期间,在不修改代码的情况下对方法进行增强。
利用AOP我们可以对一些业务逻辑进行隔离,从而使得业务逻辑各个部分之间的耦合度降低,提高程序的可复用性。
简单来说,AOP就是统一处理某一个问题的思想,比如:统一处理日志、异常。
5.1.2.2 动态代理技术
Spring中实现AOP的动态代理技术主要有两种:JDK动态代理和cglib动态代理。JDK动态代理是基于接口实现的动态代理技术。cglib动态代理技术是基于父类的动态代理技术。
1.jdk动态代理
(1)目标接口类
public interface TargetInterface {
public void method();
}
(2)目标类
public class Target implements TargetInterface {
@Override
public void method() {
System.out.println("Target running....");
}
}
(3)动态代理代码
Target target = new Target(); //创建目标对象
//创建代理对象
TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance(target.getClass()
.getClassLoader(),target.getClass().getInterfaces(),new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("前置增强代码...");
Object invoke = method.invoke(target, args);
System.out.println("后置增强代码...");
return invoke;
}
}
);
(4)调用代理对象
proxy.method();
2.cglib动态代理
(1)目标类
public class Target {
public void method() {
System.out.println("Target running....");
}
}
(2)动态代理代码
Target target = new Target(); //创建目标对象
Enhancer enhancer = new Enhancer(); //创建增强器
enhancer.setSuperclass(Target.class); //设置父类
enhancer.setCallback(new MethodInterceptor() { //设置回调
@Override
public Object intercept(Object o, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
System.out.println("前置代码增强....");
Object invoke = method.invoke(target, objects);
System.out.println("后置代码增强....");
return invoke;
}
});
Target proxy = (Target) enhancer.create(); //创建代理对象
(3)调用代理对象
proxy.method();
5.1.2.3 AOP的重点概念
切点:指被增强的方法
通知:指封装增强业务逻辑的方法的
切面:切点+通知
织入:将切点与通知结合的过程
以声明式事务控制为例,切点就是目标对象内部的方法,通知就是事务控制。
5.1.2.4 通知的类型
1.前置通知
2.后置通知
3.环绕通知
4.异常抛出通知
5.最终通知
5.1.3 IOC
5.1.3.1 IOC简介
IOC就是控制反转的意思。所谓控制反转就是将对象的创建权、对象的生命周期、对象之间的依赖关系全部交由spring容器来管理。
5.1.3.2 Spring IOC常用的注入方式
1.构造方法注入
通过在bean标签中配置<constructor-arg>子标签
2.seteer属性注入
通过在bean标签中配置<property>子标签
3.注解方式注入
5.1.4 Spring常见注解
Spring原始注解(主要作用是代替spring配置文件中的bean标签):
@Component:使用在类上用于实例化bean
@Controller:使用在web层类上用于实例化Bean
@Service:使用在service层类上用于实例化Bean
@Repository:使用在dao层类上用于实例化Bean
@Autowired:使用在字段上用于根据类型依赖注入
@Qualifier:结合@Autowired一起使用用于根据名称进行依赖注入, 不可单独使用
@Resource:相当于@Autowired和@Qualifier,按照名称进行注入
@Value:注入普通属性
@Scope:标注Bean的作用范围
@PostConstruct:使用在方法上标注该方法时Bean的初始化方法
@PreDestory:使用在方法上标注该方法时Bean的销毁方法
Spring新注解():
@Configuration:指定当前类是一个Spring配置类,当容器创建时会 从该类上加载注解
@ComponentScan:用于指定Spring在初始化容器时要扫描的包
@Bean:使用在方法上,标注将该方法的返回值存储到Spring容器中
@PropertySource:用于加载.properties文件中的配置
@Import:用于导入其它配置类
5.1.5 spring中的bean的作用域
singleton:容器中只存在一个bean实例,是系统的默认值。此种情况下,当程序启动加载配置信息的时候,bean就会创建
prototype:容器中存在多个bean实例,每次调用getBean()方法都会产生一个bean实例
request:web项目中,spring创建一个bean对象,将其存储到request域中
session:web项目中,spring创建一个bean对象,将其存储到session域中
global session:web项目中,应用在Portlet环境中,如果没有Portlet环境,相当于session。
5.1.6 声明式事务控制
Spring的声明式事务控制就是采用声明的方式来处理事务。这里所说的声明,就是指通过配置的方式代替编码的方式来处理事务。其优势是,事务管理的控制不侵入业务逻辑代码,控制事务只需要通过配置的方式就可以实现。
声明式事务控制的原理就是AOP技术。此时切点就是具体的业务方法,通知就是事务控制。
5.1.7 spring的事务的隔离级别
1.读未提交(READ_UNCOMMITED):会出现脏读、不可重复读、幻读的问题
2.读已提交(READ_COMMITED):会出现不可重复读、幻读的问题
3.可重复读(REPEATABLE_READ):会出现幻读的问题
4.串行化(SERIALIZIABLE):不会出现任何问题,但是效率最低
5.默认(DEFAULT):spring默认的隔离级别。此级别代表使用数据库本身的隔离级别,比如使用MySQL数据库时为可重复读,使用Oracle数据库时为读已提交
5.1.8 spring中用到的设计模式
工厂模式:spring中可以通过BeanFactory创建bean对象
单例模式:spring中的bean默认是singleton,即单例的
代理模式:spring的aop是基于jdk的动态代理或者基于cglib的动态代理
模板方法:RestTemplate、JDBCTemplate
5.1.9 BeanFactory和ApplicationContext
BeanFactory和ApplicationContext是Spring的两大核心接口,都 可以当做Spring的容器。其中ApplicationContext是BeanFactory的子接口。
BeanFactory采用的是延迟加载的形式来注入Bean的,只有在使用某个 Bean时(调用getBean()方法)时,才会对Bean进行实例化。 ApplicationContext在容器启动的时候,就一次性创建了所有的 Bean。这种情况有利于我们发现配置错误,可以检查Bean所依赖的属性是否已 经注入。
ApplicationContext的不足是占用内存空间,当配置的bean比较多的时候,程序的启动就会比较慢。
5.2 Spring MVC
5.2.1 Spring MVC简介
Spring MVC是一种基于java实现的MVC设计模型的轻量级web框架。MVC就是model(模型)、view(视图)、controller(控制器)的简称。
5.2.2 Spring MVC执行流程
1.客户端浏览器发起请求
2.请求到达前端控制器DispatcherServlet
3.前端控制器DipatcherServlet接受到请求以后调用处理器映射器HandlerMapping
4.映射器HandlerMapping找到处理请求的处理器controller(可以根据xml配置或者注解进行配置),生成处理器对象以及处理器拦截器并返回给前端控制器DispatcherServlet
5.前端控制器DispatcherServlet调用处理器适配器HandlerAdapter
6.适配器HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)
7.Controller执行完成返回ModelAndView
8.处理器适配器HandlerAdapter将ModelAndView返回给前端控制器DispatcherServlet
9.前端控制器DispatcherServlet将ModelAndView传给ViewResolver视图解析器
10.视图解析器ViewResolver解析后返回View
11.前端控制器DispatcherServlet根据View进行视图渲染(将数据模型填充到视图中)
12.前端控制器DispatcherServlet响应客户端请求
5.3 Spring Boot
5.4 Spring Cloud
5.5 Mybatis
6.项目
6.1 项目简介
畅行租车项目是一个在网上进行车辆租赁的平台。此平台主要功能是向用户租车。顾客可以在线选择车型、预定车辆。运营商可以在后台进行车辆管理、订单管理等功能。此项目主要借助于Spring Boot、Spring Cloud、Mybatis框架进行开发。数据库采用MySQL、MongoDB、Redis。整合RabbitMQ、FastDFS、Freemarker等技术完成开发。
主要负责页面管理工程、车辆管理工程、分布式文件系统工程。具体如下:
一、页面管理工程:
(1)主要负责页面的预览功能开发,利用Freemarker技术进行页面静态化。
(2)主要负责页面的发布功能开发,利用RabbitMQ消息中间键。
(3)主要负责页面的增删改查功能。
二、车辆管理工程:
(1)主要负责车辆的预览功能开发,借助于Spring Cloud远程调用页面管理模块的预览接口。
(2)主要负责车辆的发布功能开发,借助于Spring Cloud远程调用页面管理模块的发布接口。
(3)主要负责车辆的增删改查功能开发。
三、分布式文件系统:
(1)借助于FastDFS技术,完成分布式文件系统的搭建及代码编写,供其它工程使用。
6.2 RabbitMQ
6.2.1 为什么使用RabbitMQ
首先,RabbitMQ是一个消息队列,我们可以将消息队列当做一个存放消息的容器,生产者会向消息队列发送消息,消费者会从消息队列中获取消息。
消息队列时分布式系统的重要组成部分。使用消息队列可以有效的解决以下问题:
1.任务异步处理
将不需要同步处理的并且耗时较长的操作由消息队列通知消费者进行异步处理,减少了应用程序的响应时间。
2.应用程序解耦合
MQ相当于一个中介,生产方通过MQ与消费方进行交互,两者之间的代码进行解耦合。
3.削峰填谷
假如一个应用处理请求的能力为每秒十次,在高并发的情况下,一秒钟有30个请求同时到来,而在接下来几秒都没有 请求到达。在这种情况下,如果直接拒绝20个请求,应用在生下来的几秒钟都将处于一个空闲的状态,而且对用户体验很 不友好。因此就需要将高并发的请求进行一个均摊,让系统负载保持在一定的水平上,尽可能处理多的任务。
使用rabbitmq的原因是:
1.springboot默认集成了rabbitmq,提供了rabbitmqTemplate,使得rabbitmq使用比较简单。
2.rabbitmq的高并发性能比较好
6.2.2 rabbitmq的组成部分
Broker:消息队列服务进程,包括Exchange和Queue
Exchange:交换机,按一定的规则将消息转发到队列中
Queue:消息队列,存储消息的队列,消息到达队列并转发给消费方
Producer:消息生产者。即生产客户端,生产方客户端将消息发送给MQ
Consumer:消息消费者。即消费客户点,从MQ获取消息
6.2.3 rabbitmq执行流程
发送消息:
1.生产者和Broker建立连接
2.生产者和Broker建立通道
3.生产者将消息发送给Broker,由交换机Exchange进行转发
4.Exchange将消息转发到指定的队列queue
接收消息:
1.消费者与broker建立连接
2.消费者与broker建立通道
3.消费者监听指定的队列queue
4.当有消息到达执行队列时,broker将消息发送给消费者
5.消费者接收到消息
6.2.4 工作模式
1.工作队列模式work queues
2.发布订阅模式publish/subscribe
3.路由模式routing(项目使用此种模式)
1.每个消费者监听自己的队列,并且设置routingkey(路由key)
2.生产者将消息发送给交换机,由交换机根据routingkey将消息发送到指定队列
4.通配符模式topics
5.Header模式
6.RPC远程调用模式
6.2.5 开发步骤
1.导入依赖:spring-boot-starter-amqp
2.配置文件:
rabbitmq:
host:127.0.0.1
port:5672
username:guest
password:guest
3.定义配置类
定义交换机的名称 public static final String EX_ROUTING_CMS_POSTPAGE = "ex_routing_cms_postpage";
//交换机配置使用direct类型(路由模式的交换机类型)
@Bean(EX_ROUTING_CMS_POSTPAGE)
public Exchange EX_ROUTING_CMS_POSTPAGE() {
return ExchangeBuilder.directExchange(EX_ROUTING_CMS_POSTPAGE).durable(true).build();
}
4.生产端代码
业务方法向mq发送消息
rabbitTemplate.convertAndSend(路由器,routingkey,发送消息的内容);
5.消费端代码
监听消息队列
@RabbitListener()
6.3 FastDFS
6.3.1 为什么使用FastDFS
FastDFS是一个分布式文件系统。优点:
1.一台计算机的文件系统处理能力扩充到多台计算机同时处理
2.一台计算机挂了还有另外副本计算机提供数据
3.每台计算机可以放在不同的区域,这样用户就可以就近访问,提高访问速度。
FastDFS非常适合存储图片等小 文件。FastDFS不对文件进行分块,所以它就没有分块合并的开销。而且fastDFS采用socket通信,通信速度很快。
6.3.2 FastDFS原理
FastDFS包括Tracker Server和Storage Server。客户端请求Tracker server进行文件上传、下载,通过Tracker Server进行调度,最终由Storage Server完成上传和下载。
1)Tracker
Tracker Server的作用是负载均衡和调度,通过Tracker Server在文件上传时可以根据一些策略找到Storage server提供文件上传服务。可以将tracker称为追踪服务器或调度服务器。
FastDFS集群中的Tracker Server可以有多台,Tracker Server之间是相互平等关系同时提供服务,Tracker server不存在单点故障。客户端请求Tracker server采用轮循方式,如果请求的tracker无法提供服务则换另一Tracker Server
2)Storage
Storage Server作用是文件存储,客户端上传的文件最终存储在Storage服务器上,Storage server没有实现自己的文件系统而是使用操作系统的文件系统来管理文件。可以将Storage称为存储服务器。
Storage集群采用了分组存储方式。storage集群由一个或者多个组构成,集群存储总容量为集群中所有组的存储容量之和。一个组由一台或者多台存储服务器组成,组内的storage server之间是平等关系,不同组的storage server之间不会相互通信,同组内的storage server之间会相互连接进行文件同步,从而保证同组内的每个storage上的文件完全一致。一个组的存储容量为该组内的存储服务器容量最小的那个。
采用分组存储方式的好处是灵活、可控性强。比如上传文件时,可以由客户端直接指定上传到的组也可以由tracker进行调度选择。一个分组的存储服务器访问压力较大时,可以在该组增加存储服务器来扩充服务能力(纵向扩容)。当系统容量不足时,可以通过增加组来扩充容量。
6.3.3 文件上传流程
1.Storage定时向Tracker上传自己的状态信息
2.客户端Client向Tracke发送上传连接请求
3.Tracker查询可用的Storage
4.Tracker将可用Storage的IP和端口返回给客户端Client
5.Client将文件上传到Storage(包括file comment和metadata)
6.storage生成file_id
7.storage将上传内容写入磁盘
8.storage向client返回file_id(路径信息和文件名)
9.Client将返回的信息进行存储
注:
客户端上传文件后存储服务器(Storage Server)将文件id返回客户端,此文件ID用于以后访问该文件的索引信息。
6.3.4 文件下载流程
1.Storage定时向Tracker上传自己的状态信息
2.客户端Client向Tracker发送下载连接请求
3.Tracker查询可用的Storage
4.Tracker将可用Storage的IP和端口返回给客户端Client
5.客户端Client将要下载的文件的file_id传送给Storage Server
6.Storage Server拿到file_id查找文件
7.Storage Server将file_content返回给客户端Client
6.3.5 开发代码
上传文件
@Test
public void testUpload(){
try {
//加载fastdfs-client.properties配置文件
ClientGlobal.initByProperties("config/fastdfs-client.properties");
//定义TrackerClient,用于请求TrackerServer
TrackerClient trackerClient = new TrackerClient();
//连接tracker
TrackerServer trackerServer = trackerClient.getConnection();
//获取Stroage
StorageServer storeStorage = trackerClient.getStoreStorage(trackerServer);
//创建stroageClient
StorageClient1 storageClient1 = new StorageClient1(trackerServer,storeStorage);
//向stroage服务器上传文件
//本地文件的路径
String filePath = "I:/月亮.jpg";
//上传成功后拿到文件Id
String fileId = storageClient1.upload_file1(filePath, "jpg", null);
System.out.println(fileId);
} catch (Exception e) {
e.printStackTrace();
}
}
下载文件
@Test
public void testDownload(){
try {
//加载fastdfs-client.properties配置文件
ClientGlobal.initByProperties("config/fastdfs-client.properties");
//定义TrackerClient,用于请求TrackerServer
TrackerClient trackerClient = new TrackerClient();
//连接tracker
TrackerServer trackerServer = trackerClient.getConnection();
//获取Stroage
StorageServer storeStorage = trackerClient.getStoreStorage(trackerServer);
//创建stroageClient
StorageClient1 storageClient1 = new StorageClient1(trackerServer,storeStorage);
//下载文件
//文件id
String fileId = "group1/M00/00/00/wKhlg15wfoCAPuE9ABfoTZ7dyek300.jpg";
byte[] bytes = storageClient1.download_file1(fileId);
//使用输出流保存文件
FileOutputStream fileOutputStream = new FileOutputStream(new File("i:/download.png"));
fileOutputStream.write(bytes);
} catch (IOException e) {
e.printStackTrace();
} catch (MyException e) {
e.printStackTrace();
}
}
6.4 前后端如何进行数据交互
通过接口的形式:
使用SpringMVC编写Controller方法,对外暴露Http接口,在Controller方法上使用RequestMapping、 PostMapping、GetMapping等注解定义Http接口。
6.5 系统的异常是怎么处理的
系统对异常的处理使用统一的异常处理流程。
1、自定义异常类型。
2、自定义错误代码及错误信息。
3、对于可预知的异常由程序员在代码中主动抛出自定义异常类型的异常,抛出异常时需要指定错误代码。
4、对于不可预知的异常(运行时异常)由SpringMVC统一捕获Exception类型的异常,由统一的异常捕获类来解析 处理,并转换为与自定义异常类型一致的信息格式(错误代码+错误信息)。
5、可预知的异常及不可预知的运行时异常最终会采用统一的信息格式(错误代码+错误信息)来表示,最终也会随请求响应给客户端。
6.6 Freemarker
Freemarker是一个模板引擎。就是说,它可以基于模板和要改变的数据动态生成要输出的文本,在我们项目中的话就是用来生成html网页。实现页面静态化.
包括
模板:Template:
数据模型:DataModel
调用freemakerapi生成文本文件(Outputfile)
6.6.1模板四要素
1.文本:
2.注释:
3.指令:-----------------><#list>
4.插值:------------------>${username}
6.6.2指令
1.<#assign…>:声明变量
2.<#include>:引入子模板
3.判断
<#if>
条件成立的值,
<#else>
条件不成立的值
</#if>
4.循环
<#list stulist as stu>
${stu_index} …循环索引
</#list>
内建函数
1 ?eval: 转json;
2 ?size:集合元素的个数
3 ?c :转为字符串输出,避免118,893