目录
- 一、java基础
- 1.JDK、JRE和JVM的区别
- 2.常见的几种线程池
- 3.HashMap的负载因子为什么为0.75
- 3.Java的数据类型
- 4.Float、Double在JVM的表达方式及使用陷阱
- 5.接口和抽象的区别
- 6.什么是对象深复制和浅复制
- 7.什么是序列化
- 8.JDK1.8 HashMap扩容时做了哪些优化?(JDK1.7和JDK1.8的区别)
- 9. String、StringBuilder、StringBuffer的区别
- 10.String类型在JVM是如何存储的
- 11.IO流
- 12.静态语言和动态语言的区别(强类型语言和弱类型语言的区别)
- 13.错误与异常的区别
- 14.单例模式的五种实现方式
- 15.内存溢出和内存泄漏的区别及详解
- 堆和栈的区别
- 设计模式
- 二 、线程
- 三、JVM虚拟机
- 四、反射
- 五、数据结构
- 六、数据结构
- 七、javaweb
- mysql
- Mybatis
- Spring
- 1.谈谈你对Spring的理解
- SpringMVC
- SpringBoot
- Ridis
- Linux
一、java基础
1.JDK、JRE和JVM的区别
- JDK(Java Development Kit):是java的核心,运行java程序必须要有的东西,里面包括java运行环境JRE、java工具和java基础类库(java开发者使用的功能性类库)。
- JRE(Java Runtime Environment):运行java程序所必须的环境,里面包括java虚拟机JVM的实现和java核心类库(JVM工作所需的类库)。
- JVM(Java Virtual Machine):是java跨平台特性的核心,通过JVM屏蔽了底层系统(windows、linux、Max等等)的差异,实现一次编译,到处运行。JVM可以理解为在操作系统上模拟安装了一个CPU来处理java程序相关的东西,它主要负责将java程序生成的字节码文件解释成具体系统平台上的机器指令。
2.常见的几种线程池
1.newCachedThreadPool.创建一个可缓存的线程池,如果线程池超过处理需要,可以灵活回收空闲线程 若无可回收,创建新线程
2.newFixedThreadPool,创建一个定长的线程池,可以控制线程的最大并发数,超出的线程会在队列中华等待
3.newScheduledThreadPool:创建一个定长的线程池,支持定时及周期性任务执行
4.newSingleThreadExecutor:创建一个单线程化的线程池,他会用唯一的工作线程来执行任务,保证所有的任务按照顺序来执行
3.HashMap的负载因子为什么为0.75
HashMap负载因子,与扩容机制有关;即若当前容器的容量,达到设定最大值,就需要要执行扩容操作。
举个例子:当前的容器容量是16,负载因子是0.75;16*0.75=12,也就是说,当容量达到了12的时就会执行扩容操作。
作用很简单,相当于是一个扩容机制的阈值。当超过了这个阈值,就会触发扩容机制。HashMap源码已经为我们默认指定了负载因子是0.75。
当负载因子为(1.0)时:意味将会出现大量的hash冲突,底层的红黑树会变得异常复杂,对于查询效率极为不利,这种情况就是牺牲时间来保证空间的利用率
当负载因子为(0.5)时:也就意味着数组的元素当达到一半的时候,就发生了扩容,这样就会使hash冲突减少,数组下的链表长度或是红黑树的高度就会降低,查询的效率就会降低,但是时间效率是提升了,但是空间利用率降低了
那为什么是(0.75)呢:
这是原码的解释:
大概意思为:默认负载因子 (.75) 在时间和空间成本之间提供了良好的折衷。
3.Java的数据类型
4.Float、Double在JVM的表达方式及使用陷阱
在jvm中Float和Double的存储方式为科学计数法的的方式存储
float的存储占4字节,分为三个部分,分别为符号位,底数以及指数部分。
double的存储占8字节,分为三个部分,分别为符号位,底数以及指数部分。
在数值超出他的精度范围后,会通过四舍五入的方式得到一个近似值
浮点运算很少是精确的,只要是超过精度能表示的范围就会产生误差。往往产生误差不是 因为数的大小,而是因为数的精度。因此,产生的结果接近但不等于想要的结果。尤其在使用 float 和 double 作精确运 算的时候要特别小心。
可以考虑采用一些替代方案来实现。如通过 String 结合 BigDecimal 或 者通过使用 long 类型来转换。
5.接口和抽象的区别
- 抽象类要被子类继承,接口要被子类实现
- 在接口定义变量为常量,抽象类为成员变量(抽象类中的成员变量可以实现多个权限 public private protected final等,接口中只能用 public static final修饰为常量)
- 接口可继承接口,并可多继承接口,但类只能单继承。
- 接口中大部分是抽象方法,抽象类即可以有抽象方法也可以普通方法
- 如果是java7,那么接口中包含的内容有:
1.常量
2.抽象方法
如果是java8,还可以额外包含有:
3.默认方法
4.静态方法
如果是java9,还可以额外包含有:
5.私有方法
6.什么是对象深复制和浅复制
浅复制:只对对象及变量值进行复制,引用对象地址不变
深复制:不仅对象及变量值进行复制,引用对象地址也进行复制
7.什么是序列化
序列化(深 clone 一种实现)
在Java 语言里深复制一个对象,常常可以先使对象实现 Serializable 接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里,写到磁盘,或从磁盘通过流里读出来,便可以重建对象。
在传递和保存对象时.保证对象的完整性和可传递性。对象转换为有序字节流,以便在网络上传输或者保存在本地文件中。
序列化最重要的作用:在传递和保存对象时.保证对象的完整性和可传递性。对象转换为有序字节流,以便在网络上传输或者保存在本地文件中。
反序列化的最重要的作用:根据字节流中保存的对象状态及描述信息,通过反序列化重建对象。
总结:核心作用就是对象状态的保存和重建。
8.JDK1.8 HashMap扩容时做了哪些优化?(JDK1.7和JDK1.8的区别)
在JDK1.7时HashMap是由数组和链表组成的
而JDK1.8则新增红黑树结构
当链表的长度大于8时会转换为红黑树存储,以提升元素的操作性能
9. String、StringBuilder、StringBuffer的区别
1.String被final是字符串常量,而其他两个是字符串变量
10.String类型在JVM是如何存储的
首先常量池在编译期间就会生成,用来储存各种字面量和符号引用,比如String a = “abc”,其中"abc"在编译阶段就会被放入常量池,当然,常量池也会在运行期也会被拓展,用的比较多的是String类的Intern()方法,intern方法就是将new出来的String字符串放入常量池中,然后返回这个字符串在常量池中的地址
关于字符串拼接:
String a = "happy1";
String b = "happy";
final String c = "happy";
String d = b + "1";
String e = c + "1";
String f = "happy" + "1";
System.out.println(a == d);//false
System.out.println(a == e);//true
System.out.println(a == f);//true
- 字符串相加的时候,都是静态字符串相加的结果会添加到常量池,如果常量池有这个字符串,则直接返回其引用,所以a、e、f 相等。
- 字符串相加的时候 ,如果相加的内容有变量,则不会添加到常量池,而是在程序底层先创建了一个StringBuffer,然后调用append()方法,例如 String d = b + “1”, 会把b 和 1 加入到该StringBuffer,然后调用toString方法返回拼接后的字符串,而该tostring方法是这样的:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
返回的是一个new的String,这个String地址一定不会和现有的对象相等了
intern()方法
new 出来的String字符串不会放在常量池中,可以使用intern()方法将把字符串加入常量池中,返回值就是这个字符串在常量池的地址,但是调用intern()的值不会改变,还是存在堆里的地址,需要重新把回值赋值给调用intern()方法的值
String str1 = "a";
String str2 = "b";
String str3 = "ab";
String str4 = str1 + str2;
String str5 = new String("ab");
System.out.println(str5 == str3);//false
System.out.println(str5.intern() == str3);//true
System.out.println(str5.intern() == str4);//false
System.out.println(str5.intern() == str4.intern());//true
11.IO流
类型 | 输入流 | 输出流 |
---|---|---|
字符流 | InputSteam | OutputSteam |
字节流 | Reader | Writer |
缓冲流 | BufferInputStream BufferReader | BuferInputStream BufferWriter |
数据流 | DateInputStream | DateOutputStream |
对象流 | ObjectInputStream | ObjectOutputStream |
12.静态语言和动态语言的区别(强类型语言和弱类型语言的区别)
-
动态语言
静态语言无需再声明变量是就指定数据类型,而是在运行时才能确定变量的数据类型,可以改变变量的数据类型, 比如常见的有Php、JavaScript、Python等语言。 -
静态语言
变量必须声明数据类型,在编译阶段就可以确定变量的数据类型,如果要改变数据类型,必须经过强制转换。比如常见的有Java、C++、C#等语言。区别:
1.动态语言变量的数据类型无需声明;静态语言变量的数据类型必须声明
2.动态语言变量可以赋不同数据类型的值;静态语言变量如果需要赋不同数据类型的值,需要经过强制转换
3.动态语言在运行时确定变量的数据类型;静态语言在编译期间就可以确定变量的数据类型
4.动态语言不需要声明变量的数据类型不安全;静态语言不需要声明变量的数据类型安全
5.动态语言书写简单,可以任意发挥;静态语言可以实现复杂的业务逻辑
13.错误与异常的区别
- 错误:
一般指程序运行时遇到的硬件或操作系统的错误,如内存溢出、不能读取硬盘分区、 硬 件驱动错误等。这是致命的,将导致程序无法运行,同时也是程序本身不能处理 的。 - 异常:
指在运行环境正常的情况下遇到的运行时错误。异常是非致命的,但也会导致程序 的非正常终止。 Java可以捕获和处理异常。
14.单例模式的五种实现方式
1、饿汉式(线程安全,调用效率高,但是不能延时加载):
public class ImageLoader{
private static ImageLoader instance = new ImageLoader;
private ImageLoader(){}
public static ImageLoader getInstance(){
return instance;
}
}
一上来就把单例对象创建出来了,要用的时候直接返回即可,这种可以说是单例模式中最简单的一种实现方式。但是问题也比较明显。单例在还没有使用到的时候,初始化就已经完成了。也就是说,如果程序从头到位都没用使用这个单例的话,单例的对象还是会创建。这就造成了不必要的资源浪费。所以不推荐这种实现方式。
2.懒汉式(线程安全,调用效率不高,但是能延时加载):
public class SingletonDemo2 {
//类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
private static SingletonDemo2 instance;
//构造器私有化
private SingletonDemo2(){}
//方法同步,调用效率低
public static synchronized SingletonDemo2 getInstance(){
if(instance==null){
instance=new SingletonDemo2();
}
return instance;
}
}
3.Double CheckLock实现单例:DCL也就是双重锁判断机制(由于JVM底层模型原因,偶尔会出问题,不建议使用):
public class SingletonDemo5 {
private volatile static SingletonDemo5 SingletonDemo5;
private SingletonDemo5() {
}
public static SingletonDemo5 newInstance() {
if (SingletonDemo5 == null) {
synchronized (SingletonDemo5.class) {
if (SingletonDemo5 == null) {
SingletonDemo5 = new SingletonDemo5();
}
}
}
return SingletonDemo5;
}
}
4.静态内部类实现模式(线程安全,调用效率高,可以延时加载)
public class SingletonDemo3 {
private static class SingletonClassInstance{
private static final SingletonDemo3 instance=new SingletonDemo3();
}
private SingletonDemo3(){}
public static SingletonDemo3 getInstance(){
return SingletonClassInstance.instance;
}
}
5.枚举类(线程安全,调用效率高,不能延时加载,可以天然的防止反射和反序列化调用)
public enum SingletonDemo4 {
//枚举元素本身就是单例
INSTANCE;
//添加自己需要的操作
public void singletonOperation(){
}
}
15.内存溢出和内存泄漏的区别及详解
- 内存溢出是指在申请内存时,已经没有内存空间可以使用
- 内存泄露是指在申请内存后,内存无法释放已申请的内存空间,内存泄露堆积后会非常严重,最终会导致内存泄露
解决办法:
修改jvm启动日志,直接增加内存
检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
jps:列出系统上正在运行的Java应用
jmap:查看指定线程,Java堆内存占用信息
jstack:查看指定线程,Java堆栈信息信息
jconsole:可视化内存监控工具,用于监视Java虚拟机,也可以监视本地和远程JVM。它还可以监视和管理应用程序。
jvisualvm:一个更直观的可视化监控工具
堆和栈的区别
首先堆是一个动态的概念,栈是一个静态的概念,栈是在编译的时候确定的。堆是在运行的时候确定的,栈的大小在编译的时候就确定好了,而堆的大小在运行的时候是变化的,取决于运行时所用到的具体的数据,在访问效率上,堆的速度要慢于栈的速度,在访问权限方面,不同函数的数据不可以共享的,也同样适用于多线程,线程下的栈数据也是不能够共享的,对堆来说,是在进程上的,只要是在进程上,在application内都是可以被访问堆内的数据,在用到庞大内存时,一般会使用堆,一般用完就释放掉
设计模式
二 、线程
1.创建线程的方式
2.线程的生命周期
3.进程和线程的区别
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
线程池的使用
线程池的类别:
1. Executors.newFixedThreadPool:创建⼀个固定⼤⼩的线程池,可控制并发的线程数,超出的线程会在队列中等待;
2. Executors.newCachedThreadPool:创建⼀个可缓存的线程池,若线程数超过处理所需,缓存⼀段时间后会回收,若线程数不够,则新建线程;
3. Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执⾏顺序;
4. Executors.newScheduledThreadPool:创建⼀个可以执⾏延迟任务的线程池;
5. Executors.newSingleThreadScheduledExecutor:创建⼀个单线程的可以执⾏延迟任务的线程池;
6. Executors.newWorkStealingPool:创建⼀个抢占式执⾏的线程池(任务执⾏顺序不确定)【JDK1.8 添加】。
7. ThreadPoolExecutor:最原始的创建线程池的⽅式,它包含了 7 个参数可供设置,
我最常用的线程池:
newFixedThreadPool:固定线程数的线程池。corePoolSize = maximumPoolSize,keepAliveTime为0,工作队列使用无界的LinkedBlockingQueue。适用于为了满足资源管理的需求,而需要限制当前线程数量的场景,适用于负载比较重的服务器。
newCachedThreadPool: 按需要创建新线程的线程池。核心线程数为0,最大线程数为 Integer.MAX_VALUE,keepAliveTime为60秒,工作队列使用同步移交 SynchronousQueue。该线程池可以无限扩展,当需求增加时,可以添加新的线程,而当需求降低时会自动回收空闲线程。适用于执行很多的短期异步任务,或者是负载较轻的服务器。
三、JVM虚拟机
四、反射
五、数据结构
六、数据结构
七、javaweb
mysql
1. mysql 的执行顺序
1.from 对查询指定的表计算笛卡尔积
2.on 按照 join_condition 过滤数据
3.join 添加关联外部表数据
4.where 按照where_condition过滤数据
5.group by 进行分组操作
6.having 按照having_condition过滤数据
7.select 选择指定的列
8.distinct 指定列去重
9.order by 按照order_by_condition排序
10.limit 取出指定记录量
2.事务的四大特性
① 原子性:事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;例如转账的这两个关键操作(将张三的余额减少200元,将李四的余额增加200元)要么全部完成,要么全部失败。
② 一致性: 确保从一个正确的状态转换到另外一个正确的状态,这就是一致性。例如转账业务中,将张三的余额减少200元,中间发生断电情况,李四的余额没有增加200元,这个就是不正确的状态,违反一致性。又比如表更新事务,一部分数据更新了,但一部分数据没有更新,这也是违反一致性的;
③ 隔离性:并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
④ 持久性:一个事务被提交之后,对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
3.mysql的两种引擎(MyISAM,InnoDB)比较
- MyISAM是非事物安全的,InnoDB是事物安全的。
- MyISAM锁的粒度是表级的,InnoDB支持行级锁以及表级,默认情况下是采用行级锁。
- MyISAM支持全文类型索引,InnoDB不支持(之前)。
- MyISAM不支持外键,InnoDB支持外键,
Mybatis
Spring
1.谈谈你对Spring的理解
Spring是一个开源框架,为简化企业开发而生,他是一个IOC和AOP容器框架
- IOC(控制反转):在传统开发的时候,
2.Spring AOP的术语
Aspect:切面,由一系列切点、增强和引入组成的模块对象,可定义优先级,从而影响增强和引入的执行顺序。事务管理(Transaction management)在java企业应用中就是一个很好的切面样例。所以他不是一个被代理的对象。
Join point:接入点,程序执行期的一个点,例如方法执行、类初始化、异常处理。 在Spring AOP中,接入点始终表示方法执行。
Advice:增强,切面在特定接入点的执行动作,包括 “around,” “before” and "after"等多种类型。包含Spring在内的许多AOP框架,通常会使用拦截器来实现增强,围绕着接入点维护着一个拦截器链。
Pointcut:切点,用来匹配特定接入点的谓词(表达式),增强将会与切点表达式产生关联,并运行在任何切点匹配到的接入点上。通过切点表达式匹配接入点是AOP的核心,Spring默认使用AspectJ的切点表达式。
Introduction:引入,为某个type声明额外的方法和字段。Spring AOP允许你引入任何接口以及它的默认实现到被增强对象上。
Target object:目标对象,被一个或多个切面增强的对象。也叫作被增强对象。既然Spring AOP使用运行时代理(runtime proxies),那么目标对象就总是代理对象。
AOP proxy:AOP代理,为了实现切面功能一个对象会被AOP框架创建出来。在Spring框架中AOP代理的默认方式是:有接口,就使用基于接口的JDK动态代理,否则使用基于类的CGLIB动态代理。但是我们可以通过设置proxy-target-class=“true”,完全使用CGLIB动态代理。
Weaving:织入,将一个或多个切面与类或对象链接在一起创建一个被增强对象。织入能发生在编译时 (compile time )(使用AspectJ编译器),加载时(load time),或运行时(runtime) 。Spring AOP默认就是运行时织入,可以通过枚举AdviceMode来设置。
3.事务属性的种类:
传播行为、隔离级别、只读和事务超时
- 传播行为定义了被调用方法的事务边界。
- 隔离级别
在操作数据时可能带来 3 个副作用,分别是脏读、不可重复读、幻读。为了避免这 3 中副作用的发生,在标准的 SQL 语句中定义了 4 种隔离级别,分别是未提交读、已提交读、可重复读、可序列化。而在 spring 事务中提供了 5 种隔离级别来对应在 SQL 中定义的 4 种隔离级别,如下:
- 只读如果在一个事务中所有关于数据库的操作都是只读的,也就是说,这些操作只读取数据库中的数据,而并不更新数据,那么应将事务设为只读模式( READ_ONLY_MARKER ) , 这样更有利于数据库进行优化 。
因为只读的优化措施是事务启动后由数据库实施的,因此,只有将那些具有可能启动新事务的传播行为 (PROPAGATION_NESTED 、 PROPAGATION_REQUIRED 、 PROPAGATION_REQUIRED_NEW) 的方法的事务标记成只读才有意义。
如果使用 Hibernate 作为持久化机制,那么将事务标记为只读后,会将 Hibernate 的 flush 模式设置为 FULSH_NEVER, 以告诉 Hibernate 避免和数据库之间进行不必要的同步,并将所有更新延迟到事务结束。 - 业务超时
如果一个事务长时间运行,这时为了尽量避免浪费系统资源,应为这个事务设置一个有效时间,使其等待数秒后自动回滚。与设置“只读”属性一样,事务有效属性也需要给那些具有可能启动新事物的传播行为的方法的事务标记成只读才有意义。
4.Spring容器中Bean包含五种作用域
- singleton:单例(默认),对象在工厂初始化时创建
- prototype:原型(多例),对象在工厂初始化后创建即获取对象时创建
- request:在Web环境下,同一次请求创建一个实例
- session:在Web环境下,同一次会话创建一个实例
- globalSession
5.@RestController注解详解
@RestController注解相当于@ResponseBody + @Controller合在一起的作用;
如果只是使用@RestController注解Controller,则Controller中的方法无法返回jsp页面,配置的视图解析器InternalResourceViewResolver不起作用,返回的内容就是Return 里的内容。例如:本来应该到success.jsp页面的,则其显示success;
如果需要返回到指定页面,则需要用 @Controller配合视图解析器InternalResourceViewResolver才行;
如果需要返回JSON、XML或自定义mediaType内容到页面,则需要在对应的方法上加上@ResponseBody注解。
6.Spring Bean的生命周期
- 填充属性
- 执行Aware接口方法(为了使自定义的对象能够方便的获取到容器中的对象 )
- 执行before方法
- 调用init - method 方法
- after
- 生成完整对象
- 使用后销毁
7.关于Spring的三级缓存
Spring框架中的Bean管理器负责创建、缓存和提供bean实例。Bean实例可以在Spring中被称为单例(singleton),这意味着对于每个Bean定义,只有一个对象实例存在。而Spring使用了三级缓存来管理这种单例Bean。
三级缓存包括:
singletonObjects: 在这个缓存中,Spring将生成的Bean实例放入内存中,以便快速访问它们。这是Bean实例池。
earlySingletonObjects:此处保存一些早期创建的Bean实例,如有些实例需要提前进行先创建而不需要之后才注入。
singletonFactories: 对于一些Bean实例,Spring可能需要延迟实例处理或者实例的状态还没有完全就绪,此时Spring会将Bean实例化的方式放入到此缓存中,等待以后调用。
以WebApplicationContext为例,当一个web应用程序启动时,spring就创建了一个所谓的root(根)ApplicationContext对象,ContextLoaderListener监听器负责读取配置文件并创建 WebApplicationContext ,使得我们的应用程序可以引用servletContext属性及资源(如JDBC datasource 或者是 JMS ConnectionFactory)。 对于这个root servlet context,通常初始化application尽量少的bean,而这些bean是全局共享、线程安全且仅需初始化一次的对象,这些都是singleton beans。这些singleton beans都会放置在一级缓存中,可以被完全初始化后直接使用,而其他的对象就会放到三级缓存中等待。
需要注意的是,虽然使用缓存有助于提高应用程序的性能,但它也需要占用内存。在使用Spring时,如果过度渲染或滥用缓存容器,则可能会导致内存耗尽和性能下降问题。因此,在使用Spring的Bean管理器时,应该考虑到这个问题,并适当地使用缓存来优化应用程序的性能。
SpringMVC
1.SpringMVC五大核心组件
1.DispatcherServlet 请求入口
2.HandlerMapping 请求派发,负责请求和控制器建立一一对应的关系
3.Controller 处理器
4.ModelAndView 封装模型信息和视图信息
5.ViewResolver 视图处理器,定位页面
2.SpringMVC拦截器的三大方法
- preHandle()方法:该方法在控制器方法前执行,该返回值,表示是否中断后续操作,当其返回值为true时,表示继续向下执行,如果返回值为false,会中断后续的所有操作(包括调用下一个拦截器和控制器类中的方法执行等)
- postHandle():该方法在控制器方法调用之后,且解析视图之前执行,可以通过此方法对请求域中的模型和视图做出进一步的修改
- afterCompletion():该方法在整个请求完成,即视图渲染结束之后执行。可以通过此方法实现一些资源清理、记录日志信息等工作
3.在Spring MVC中,若要实现上传功能,则需要使用的核心组件
在Spring MVC中实现上传功能,主要依赖MultipartHttpServletRequest从读取请求中的文件,然后对读取到的MultipartFile类型进行处理。
SpringBoot
Ridis
常用指令
功能 | 指令 |
---|---|
keys * | 查看当前库所有key (匹配:keys *1) |
exists key | 判断某个key是否存在 |
type key | 查看你的key是什么类型 |
del key | 删除指定的key数据 |
unlink key | 根据value选择非阻塞删除(仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作) |
expire key 10 | 10秒钟:为给定的key设置过期时间 |
ttl key | 查看还有多少秒过期,-1表示永不过期,-2表示已过期 |
select | 命令切换数据库 |
dbsize | 查看当前数据库的key的数量 |
flushdb | 清空当前库 |
flushall | 通杀全部库 |
Redis的数据类型
String
如果存储数字的话,是用int类型的编码;如果存储非数字,小于等于39字节的字符串,是embstr;大于39个字节,则是raw编码。
数据结构:String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.
功能 | 指令 |
---|---|
set key value | 添加键值对 |
get key | 查询对应键值 |
get key | 查询对应键值 |
append key value | 将给定的value 追加到原值的末尾 |
strlen key | 获得值的长度 |
setnx key value | 只有在 key 不存在时 设置 key 的值 |
incr key | 将 key 中储存的数字值增1只能对数字值操作,如果为空,新增值为1 |
decr key | 将 key 中储存的数字值减1只能对数字值操作,如果为空,新增值为-1 |
incrby / decrby key | <步长>将 key 中储存的数字值增减。自定义步长。 |
mset key1 value1 key2 value2 … | 同时设置一个或多个 key-value对 |
mget key1 key2 key3… | 同时获取一个或多个 value |
msetnx key1 value1 key2 alue2> … | 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。(原子性,有一个失败则都失败) |
List
如果列表的元素个数小于512个,列表每个元素的值都小于64字节(默认),使用ziplist编码,否则使用linkedlist编码 (ziplist内存地址是连续的),Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
quickList:
功能 | 指令 |
---|---|
lpush/rpush key value1 value2 value3 … | 从左边/右边插入一个或多个值。 |
lpop/rpop key从左边/右边吐出一个值。值在键在,值光键亡。 | |
rpoplpush key1 key2 | 从key1列表右边吐出一个值,插到key2列表左边。 |
lrange key start stop | 按照索引下标获得元素(从左到右) |
lrange mylist 0 -1 | 0左边第一个,-1右边第一个,(0-1表示获取所有) |
lindex key index | 按照索引下标获得元素(从左到右) |
llen key | 获得列表长度 |
Hash
哈希类型元素个数小于512个,所有值小于64字节的话,使用ziplist编码,否则使用hashtable编码。
功能 | 指令 |
---|---|
hset key field value | 给 key 集合中的 field 键赋值 value |
hget key1 field | 从 key1 集合 field 取出 value |
hmset key1 field1 value1 field2 value2 … | 批量设置hash的值 |
Set
Set数据结构是dict字典,字典是用哈希表实现的
功能 | 指令 |
---|---|
sadd key value1 value2 … | 将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略 |
smembers key | 取出该集合的所有值。 |
sismember key value | 判断集合 key 是否为含有该 value 值,有1,没有0 |
scard key | 返回该集合的元素个数。 |
spop key | 随机从该集合中吐出一个值。 |
Zset
当有序集合的元素个数小于128个,每个元素的值小于64字节时,使用ziplist编码,否则使用skiplist(跳跃表)编码
Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。
不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。
zset底层使用了两个数据结构
(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。
(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。
功能 | 指令 |
---|---|
zadd key score1 value1 score2 value2… | 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。 |
zrange key start stop [WITHSCORES] | 返回有序集 key 中,下标在 start stop 之间的元素带WITHSCORES,可以让分数一起和值返回到结果集。zrangebyscore key minmax [withscores] [limit offset count] |
zincrby key increment value | 为元素的score加上增量 |
zrem key value | 删除该集合下,指定值的元素 |
zcount key min max | 统计该集合,分数区间内的元素个数 |
zrank key value | 返回该值在集合中的排名,从0开始。 |
Redis新数据类型
1.Bitmaps
2.HyperLogLog
3.Geospatial
缓存穿透、缓存击穿、缓存雪崩
缓存穿透
问题描述
key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
解决方案:
(1)对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟
(2)设置可访问的名单(白名单):
使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
(3)采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。
布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)
将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。
(4)进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务
缓存击穿
问题描述
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决问题:
(1)预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长
(2)实时调整:现场监控哪些数据热门,实时调整key的过期时长
(3)使用锁:
(1)就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db。
(2)先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key
(3)当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key;
(4)当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法。
缓存雪崩
问题描述
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
缓存雪崩与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key正常访问
解决方案
缓存失效时的雪崩效应对底层系统的冲击非常可怕!
解决方案:
(1)构建多级缓存架构:nginx缓存 + redis缓存 +其他缓存(ehcache等)
(2)使用锁或队列:
用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况
(3)设置过期标志更新缓存:
记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。
(4)将缓存失效时间分散开:
比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
代码详解及解决方案
Redis 内存淘汰策略
volatile-lru:当内存不足以容纳新写入数据时,从设置了过期时间的key中使用LRU(最近最少使用)算法进行淘汰;
allkeys-lru:当内存不足以容纳新写入数据时,从所有key中使用LRU(最近最少使用)算法进行淘汰。
volatile-lfu:4.0版本新增,当内存不足以容纳新写入数据时,在过期的key中,使用LFU算法进行删除key。
allkeys-lfu:4.0版本新增,当内存不足以容纳新写入数据时,从所有key中使用LFU算法进行淘汰;
volatile-random:当内存不足以容纳新写入数据时,从设置了过期时间的key中,随机淘汰数据;。
allkeys-random:当内存不足以容纳新写入数据时,从所有key中随机淘汰数据。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的key中,根据过期时间进行淘汰,越早过期的优先被淘汰;
noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。
Redis 的持久化机制有哪些,优缺点说说
Redis提供了RDB和AOF两种持久化机制
1.RDB
就是把内存数据以快照的形式保存到磁盘上。什么是快照?可以这样理解,给当前时刻的数据,拍一张照片,然后保存下来。
备份是如何执行的:
Redis会单独创建一个子进程来进行持久化,会将数据写入一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化的文件,这个过程中,主进程不会进行任何的io操作,这就确保了极高的性能,如果需要进行大规模数据的恢复,且对数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效,RDB的缺点是最后一次的持久化数据可能丢失
RDB持久化,是指在指定的时间间隔内,执行指定次数的写操作,将内存中的数据集快照写入磁盘中,它是Redis默认的持久化方式。执行完操作后,在指定目录下会生成一个dump.rdb文件,Redis 重启的时候,通过加载dump.rdb文件来恢复数据。RDB触发机制主要有以下几种:
RDB 的优点:
适合大规模的数据恢复场景,如备份,全量复制等
RDB缺点:
没办法做到实时持久化/秒级持久化。
Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改
2.AOF
讲一下什么AOF
AOF(append only file) 持久化,采用日志的形式来记录每个写操作(增量保存),将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动指出就会读取该文件重新构建数据,换言之,redsi重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
AOF的持久化流程:
- 客户端的请求写命令会被append追加到AOF缓冲区中
- AOF缓冲区会根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中
- AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量
- redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的
AOF和RDB同时开启,redis听谁的?
AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)
AOF同步频率设置
appendfsync always
始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好
appendfsync everysec
每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
appendfsync no
redis不主动进行同步,把同步时机交给操作系统。
Rewrite压缩
AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集
原理:AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),redis4.0版本后的重写,是指上就是把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。
重写流程
(1)bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。
(2)主进程fork出子进程执行重写操作,保证主进程不会阻塞。
(3)子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。
(4)1).子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。2).主进程把aof_rewrite_buf中的数据写入到新的AOF文件。
(5)使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。
AOF的优点:
数据的一致性和完整性更高
可以进行异常的恢复(redis-check-aof–fix appendonly.aof)
备份机制更稳健,丢失数据概率更低。
可读的日志文本,通过操作AOF稳健,可以处理误操作。
AOF的缺点:
即使有些操作时重复的也会全部记录,aof文件的大小要大于rdb格式的文件
aof在恢复大数据集时的速度比rdb的恢复速度要慢
根据fsync策略不同,aof速度可能会慢于rdb
bug出现的可能性更多
详细
怎么实现Redis的高可用?
我们在项目中使用Redis,肯定不会是单点部署Redis服务的。因为,单点部署一旦宕机,就不可用了。为了实现高可用,通常的做法是,将数据库复制多个副本以部署在不同的服务器上,其中一台挂了也可以继续提供服务。Redis 实现高可用有三种部署模式:主从模式,哨兵模式,集群模式。
主从模式:
主从模式中,Redis部署了多台机器,有主节点,负责读写操作,有从节点,只负责读操作。从节点的数据来自主节点,实现原理就是主从复制机制
主从复制包括全量复制,增量复制两种。
- Slave启动成功连接到master后会发送一个sync命令
- Master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步
- 全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
- 增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步
- 但是只要是重新连接master,一次完全同步(全量复制)将被自动执行
哨兵模式:
主从模式中,一旦主节点由于故障不能提供服务,需要人工将从节点晋升为主节点,同时还要通知应用方更新主节点地址。显然,多数业务场景都不能接受这种故障处理方式。Redis从2.8开始正式提供了Redis Sentinel(哨兵)架构来解决这个问题。
哨兵模式,由一个或多个Sentinel实例组成的Sentinel系统,它可以监视所有的Redis主节点和从节点,并在被监视的主节点进入下线状态时,自动将下线主服务器属下的某个从节点升级为新的主节点。但是呢,一个哨兵进程对Redis节点进行监控,就可能会出现问题(单点问题),因此,可以使用多个哨兵来进行监控Redis节点,并且各个哨兵之间还会进行监控
Cluster集群模式:
哨兵模式基于主从模式,实现读写分离,它还可以自动切换,系统可用性更高。但是它每个节点存储的数据是一样的,浪费内存,并且不好在线扩容。因此,Cluster集群应运而生,它在Redis3.0加入的,实现了Redis的分布式存储。对数据进行分片,也就是说每台Redis节点上存储不同的内容,来解决在线扩容的问题。并且,它也提供复制和故障转移的功能。
- 各个节点之间是怎么通信的呢?
Redis Cluster集群通过Gossip协议进行通信,节点之前不断交换信息,交换的信息内容包括节点出现故障、新节点加入、主从节点变更信息、slot信息等等。常用的Gossip消息分为4种,分别是:ping、pong、meet、fail。 - Hash Slot插槽算法
插槽算法把整个数据库被分为16384个slot(槽),每个进入Redis的键值对,根据key进行散列,分配到这16384插槽中的一个。使用的哈希映射也比较简单,用CRC16算法计算出一个16 位的值,再对16384取模。数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点都可以处理这16384个槽。
在生成 RDB期间,Redis 可以同时处理写请求么?
可以的,Redis提供两个指令生成RDB,分别是save和bgsave。
如果是save指令,会阻塞,因为是主线程执行的。
如果是bgsave指令,是fork一个子进程来写入RDB文件的,快照持久化完全交给子进程来处理,父进程则可以继续处理客户端的请求。
Redis的Hash 冲突怎么办
哈希表查找速率很快的,有点类似于Java中的HashMap,它让我们在O(1) 的时间复杂度快速找到键值对。首先通过key计算哈希值,找到对应的哈希桶位置,然后定位到entry,在entry找到对应的数据,Redis为了解决哈希冲突,采用了链式哈希。链式哈希是指同一个哈希桶中,多个元素用一个链表来保存,它们之间依次用指针连接。
聊聊Redis 事务机制
Redis通过MULTI、EXEC、WATCH等一组命令集合,来实现事务机制。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
Redis执行事务的流程如下:
开始事务(MULTI)
命令入队
执行事务(EXEC)、撤销事务(DISCARD )
MySQL与Redis 如何保证双写一致性
缓存延时双删
删除缓存重试机制
读取biglog异步删除缓存
Redis的跳跃表
跳跃表:
跳跃表是有序集合zset的底层实现之一
跳跃表支持平均O(logN),最坏 O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。
跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(如表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点。
跳跃表就是在链表的基础上,增加多级索引提升查找效率。
说说Redis的常用应用场景
缓存
排行榜
计数器应用
共享Session
分布式锁
社交网络
消息队列
位操作
-
缓存
我们一提到redis,自然而然就想到缓存,国内外中大型的网站都离不开缓存。合理的利用缓存,比如缓存热点数据,不仅可以提升网站的访问速度,还可以降低数据库DB的压力。并且,Redis相比于memcached,还提供了丰富的数据结构,并且提供RDB和AOF等持久化机制,强的一批。 -
排行榜
当今互联网应用,有各种各样的排行榜,如电商网站的月度销量排行榜、社交APP的礼物排行榜、小程序的投票排行榜等等。Redis提供的zset数据类型能够实现这些复杂的排行榜。
比如,用户每天上传视频,获得点赞的排行榜可以这样设计:
1.用户Jay上传一个视频,获得6个赞,可以酱紫:
zadd user:ranking:2021-03-03 Jay 3
2.过了一段时间,再获得一个赞,可以这样:
zincrby user:ranking:2021-03-03 Jay 1
3.如果某个用户John作弊,需要删除该用户:
zrem user:ranking:2021-03-03 John
4.展示获取赞数最多的3个用户
zrevrangebyrank user:ranking:2021-03-03 0 2
-
计数器应用
各大网站、APP应用经常需要计数器的功能,如短视频的播放数、电商网站的浏览数。这些播放数、浏览数一般要求实时的,每一次播放和浏览都要做加1的操作,如果并发量很大对于传统关系型数据的性能是一种挑战。Redis天然支持计数功能而且计数的性能也非常好,可以说是计数器系统的重要选择。 -
共享Session
如果一个分布式Web服务将用户的Session信息保存在各自服务器,用户刷新一次可能就需要重新登录了,这样显然有问题。实际上,可以使用Redis将用户的Session进行集中管理,每次用户更新或者查询登录信息都直接从Redis中集中获取。 -
分布式锁
几乎每个互联网公司中都使用了分布式部署,分布式服务下,就会遇到对同一个资源的并发访问的技术难题,如秒杀、下单减库存等场景。
用synchronize或者reentrantlock本地锁肯定是不行的。
如果是并发量不大话,使用数据库的悲观锁、乐观锁来实现没啥问题。
但是在并发量高的场合中,利用数据库锁来控制资源的并发访问,会影响数据库的性能。
实际上,可以用Redis的setnx来实现分布式的锁。 -
社交网络
赞/踩、粉丝、共同好友/喜好、推送、下拉刷新等是社交网站的必备功能,由于社交网站访问量通常比较大,而且传统的关系型数据不太适保存 这种类型的数据,Redis提供的数据结构可以相对比较容易地实现这些功能。 -
消息队列
消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。 -
位操作
用于数据量上亿的场景下,例如几亿用户系统的签到,去重登录次数统计,某用户是否在线状态等等。腾讯10亿用户,要几个毫秒内查询到某个用户是否在线,能怎么做?千万别说给每个用户建立一个key,然后挨个记(你可以算一下需要的内存会很恐怖,而且这种类似的需求很多。这里要用到位操作——使用setbit、getbit、bitcount命令。原理是:redis内构建一个足够长的数组,每个数组元素只能是0和1两个值,然后这个数组的下标index用来表示用户id(必须是数字哈),那么很显然,这个几亿长的大数组就能通过下标和元素值(0和1)来构建一个记忆系统。
Redis的过期策略
set key的时候,可以给它设置一个过期时间,比如expire key 60。指定这key60s后过期,60s后,redis是如何处理的嘛?我们先来介绍几种过期策略:
定时过期:
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即对key进行清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
惰性过期:
只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
Redis分布式锁
分布式锁,是控制分布式系统不同进程共同访问共享资源的一种锁的实现。
1.命令setnx + expire分开写(当该服务挂掉后,别的线程永远获取不到锁)
2.set key ex px nx(问题:锁过期释放了,业务还没执行完。锁被别的线程误删。)
3.set ex px nx + 校验唯一随机值,再删除,操作时,再校验(问题:锁过期释放了,业务还没执行完的问题)
3.lua脚本