面试汇总
一、java基础
1、==和equals
答:对比的是栈中的值,基本数据类型是变量值,存在栈中,引用数据类型对比的是堆中内存的对象的地址,equals默认会采用比较,因此一般会重写。重写后的equals比较的是两个字符串的内容。
2、final
答:final修饰类,类不可以被继承;修饰方法,方法不可以被重写;修饰变量,变量不可以修改。
3、String、StringBuffer、StringBuilder有什么区别及使用场景
答:String是由final修饰的,不可变,每次操作都会产生新的对象。Stringbuffer和Stringbulider都是在原对象上操作的,Stringbuffer是线程安全的,Stringbulider是线程不安全的,StringBuffer是由synchronized修饰的。
性能:StringBuilder > StringBuffer > String
场景:经常需要改变字符串内容时用StringBuffer和StringBuilder。优先使用StringBuilder,多线程使用共享变量时使用StringBuilder。
4、重载和重写的区别
答:重载:发生在同一个类中,方法名必须相同,参数类型不同,个数不同,顺序不同,方法返回值和访问修饰符可以不同。
重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符大于等于父类,如果父类访问修饰符为private则子类不能重写该方法。
5、接口和抽象类的区别?
抽象类可以存在普通成员函数,接口只能是public abstract方法;
抽象的成员变量可以是各种类型的,接口只能是public static final类型;
抽象类只能单继承,接口可以多实现;
接口设计的目的是对类的行为进行约束;而抽象类的目的是代码复用,当不同的类具有某些相同的行为,可以将这些类都派生出一个抽象类。
6、hashCode和equals
答:hashCode的作用是获取哈希码,返回一个int整数,哈希码的作用是确定该对象在哈希表中的索引位置,可以帮助我们快速的找出该对象的位置。
以hashSet如何检查重复为例说明为什么要有hashCode。当一个对象加入hashSet时,HashSet会先计算出该对象的hashCode的值来判断该对象加入的位置,看该位置是否有值,如果没值,hashSet会默认该对象没有重复出现。但
是如果有值,这时会调用equals方法来检查该对象是否真的相同,如果两者相同,HashSet则不会让该对象加入成功,如果不同的话,就会散列到其他位置。这样就大大减少了equals的次数,大大提高了执行速度
7、session的分布式方案
二、集合
1、List和Set的区别
答:List:有序、可重复、允许多个NULL元素,可以使用iteritor取出所有元素,也可以使用get(i)获取指定的元素
Set:无序、不可重复、最多允许一个NULL元素,取元素时只能用Iteritor取得所有元素,在逐一便利各个元素
2、ArrayList和LinkedList的区别
答:ArrayList是基于动态数组,连续内存存储,扩容机制是因为数据长度固定,超出数组长度存元素时要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会涉及到数据的移动,(复制一步,插入新元素),我
们可以使用尾插法同时制定好数组的初始容量可以极大的提高性能,甚至可以超过linkedList。因为linkedList插入元素虽然不需要进行移动,但是linkedList 内部维护了node的一个类,每次插入数据都会创建一个node对象,也会造成
大量的性能消耗。
LinkedList是基于链表,分散的储存在内存中,适合做插入和删除数据操作,不适合做查询,适合插入删除是因为链表在内存中不同的位置,插入元素的时候只需要将指针指向要插入的元素即可。不适合做查询是因为链表需要一
个一个遍历,遍历LinkedList要用iterator来遍历不要使用for循环来遍历,主要是因为for循环体内通过get(i)取得某一元素时需要重新遍历,消耗的性能比较大
3、HashMap和HashTable的区别?底层实现是什么?
区别:
HashMap是线层不安全的,HashTable是线层安全的,是因为后者方法有synchronized修饰;
HashMap允许key和value尾NULL,HashTable不允许
底层实现:
数组加链表实现
jdk8开始,当链表高度到8,数组长度大于64,链表转为红黑树,元素内部以node节点存在。
计算key的hash值,找到对应数组的下标,如果该位置没有值,则直接创建node存在该数组,如果有值,则产生哈希冲突,这时会调用equals进行比较相同则替换该元素,不同则插入链表,当链表高度到8并且数组长度达到64则转换为红黑树,长度低于6则将红黑树转换回链表 。
扩容:
4、ConcurrentHashMap原理,jdk7和jdk8版本的区别
jdk7:
数据结构:ReetranLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构,Segment的个数是可以通过构造函数指
定,它属于分段锁,首先根据key的hashcode定位到属于哪一个Segment,然后将这一段锁起来,hashTable是将整个数组锁起来,锁的粒度要小,Segment有多少个
就可以允许多少个线程同时操作。
元素查询:二次Hash,第一次hash定位到所在Segment,第二次Hash定位到链表所在头部。
get()方法无需加锁,Volatile保证。
jdk8:
数据结构:Synchronized+CAS+Node+红黑树,Synchronized在jdk7的时候是重量锁,在jdk8之后做了优化变为轻量锁,Synchornized主要是为了扩容、hash冲突
等场景会使用Synchronized。查找、替换、赋值操作都使用CAS,CAS是乐观锁,CAS比ReetranLock和Synchronized效率在某些场景要高一点。
锁:锁链表的Head节点,不影响其他元素的读写,锁粒度更小
三、线程
1、线程的生命周期
答:线程通常有五种状态:创建、就绪、运行、阻塞和死亡状态。
阻塞的情况又分三种:
等待阻塞:运行的线程执行wait方法,该线程会释放所有资源,JVM会把该线程放入等待池中,进入这个状态是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是object的方法。
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池。
其他阻塞:运行的线程执行sleep或join方法,会把该线程置为阻塞状态,当sleep状态超时、join等待线程超时或者终止。线程重新转入就绪状态。sleep是Thread类的方法。
2、sleep()、wait()、join()、yield()的区别
答:锁池:所有需要竞争同步锁的线程都会放到锁池当中,比如当前线程的锁已经被其中的一个线程得到,则其他线程需要在这个锁池进行等待,当前面线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪列等待CPU资源分配。
等待池:当我们调用wait()方法后,线程会放到等待池中,等待池的线程是不会竞争同步锁,只有调用了notify或者notifyAll后才会竞争锁,notify是随机从等待池选出一个线程放入等待此池,notifyAll是将等待池中所有线程都放入锁池。
1、sleep是Thread的静态本地方法,wait是Object的本地方法。
2、sleep方法不会释放lock,但是wait会释放,而且会加入等待队列当中。
3、sleep方法不依赖Synchronized,但是wait会依赖Synchronized
4、sleep不需要被唤醒,但是wait需要
5、sleep一般用于当前线程休眠,wait则多用于多线程之间的通信
yield()执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行。
join()执行后线程会进入阻塞状态,例如线程B调用线程A的join(),那么线程B会进入阻塞队列,知道线程A结束或者中断线程
3、谈谈守护线程的理解
答:守护线程:为所有非守护线程提供服务的线程。任何一个守护线程都是整个JVM中所有非守护线程的保姆,它依赖着整个线程而运行,当其他线程结束了,没有要
执行的了,程序就结束了,直接把它中断。
作用:举例,GC垃圾回收线程就是一个经典的守护线程,当我们的程序不再有任何运行的线程,程序就不会产生垃圾了,垃圾回收器也就无事可做了,所以当垃圾回收
器是jvm中仅剩的线程时,垃圾回收期会自动离开。守护线程必须在程序开始前设置。
4、ThreadLocal的原理和使用场景
5、ThreadLocal内存泄露原因,如何避免?
6、并发的三大特性:
原子性、可见性、有序性
7、为什么用线程池?解释下线程池参数
1、降低资源消耗:提高线程利用率,降低创建和销毁线程的消耗
2、提高相应速度:任务来了直接有线程可用可执行,而不是先创建在执行
3、提高线程的可管理性
corePoolSize:代表核心线程数,也就是正常情况下创建的线程数,这些线程创建之后并不会消除,而是一种常驻线程
maxinumPoolSize:代表最大线程数,他与核心线程数相对应,表示最大可允许创建的线程数,比如当前任务较多,核心线程数都用完了,还无法满足需求时,此时会去创建新的线程,但是线程内部线程总数不会超过线程最大线程数
keepAliveTime:表示超出核心线程数之外的线程空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定时间会被消除。
workQueue:用来存在待执行的任务,假设我们核心线程数都被使用了,任务再进来会将任务先放到队列,知道整个队列被放满但是任务还在持续进入则会开始创建新的线程。
Handler:任务拒绝策略,有两种情况,第一种当我们调用了shutDown等方法关闭线程池后,这时线程池内部还有未执行完的任务正在执行,但是由于线程池已经关闭,我们想继续线程池提交任务就会遭到决绝。另一种情况是当达到最大线程数,线程池已经没有能力提交新任务时,也会遭到拒绝。
8、线程池处理流程
答:线程池执行任务时,首先需要判断核心线程是否已满,如果未满,则创建核心线程执行,如果已满,判断任务队列是否已满,如果未满,将任务放到队列中,如果已满判断最大线程数数是否达到,如果未达到,创建临时线程执行,如果达到,则采用拒绝策略
9、线程池中阻塞队列的作用?为什么先添加队列而不是先创建最大线程?
答:一般队列只能保证一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列可以保存住当前想要继续入列的任务。
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执
行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活,不至于一直占用cpu资源。
10、线程池中线程复用原理
四、反射
五、异常
1、java中的异常体系
答:java中所有的异常都来源顶级父类Throwable
Throwable下有两个子类Exception和Error
Error是程序无法处理的错误,一旦出现这个错误,程序将被迫停止运行
Exception不会导致程序停止运行,又分为两个部分RunTimeException运行时异常和CheckedException检查异常
RunTimeException常常发生在程序运行过程中,会导致当前线程执行失败。CheckedException常常发生在编译过程中,会导致程序编译不通过
六、java8特性
七、jvm
1、java类加载器有哪些?
答:JDK自带的三个类加载器:BootStrapClassLoader、ExtClassLoader、AppClassLoader
BootStrapClassLoader是ExtClassLoader的父类加载器,负责默认加载%JAVA_HOME%lib下的jar包和class文件。
ExtClassLoader是AppClassLoader的父类加载器,负责默认加载%JAVA_HOME%lib/txt文件夹下的jar包和class文件。
AppClassLoader加载的是classpath下的类文件。他是系统默认类加载器以及线程上下文类加载器
2、双亲委托模型
BootStrapClassLoader、ExtClassLoader、AppClassLoader。他们都有各自的缓存,会把加载过得类放到缓存里面
AppClassLoader系统类默认加载器,当加载一个类的时候,首先AppClassLoader会找自己的缓存,有则返回,没有向上委派:实际上就是查找缓存,是否加载了该
类,有则返回,没有则继续向上,委派到BootStrapClassLoader的时候,缓存中还是没有,则到加载路径去找,有则加载返回,没有则向下查找。
3、GC如何判断对象可以被回收
答:引用计数法:每个对象有一个引用计数器的属性,新增一个引用时计数加1,引用释放时计数减1,计数为0可以被回收。(java不使用该方法),该方法有缺陷,
当A引用B,B引用A,这时就算他们不使用了,但因为互相引用计数器=1永远无法会被回收;
可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可引用的,那么虚拟机就判断是可回收对象。
GC roots的对象有:
虚拟机栈中引用的对象。
方法区中类静态属性引用的对象
方法区中常量引用的对象
Native方法
4、JVM内存模型
答:堆:对象和数组都是存在堆里,堆是JVM管理的重点,堆分为新生代和老年代,默认新生代占堆内存的三分之一,老年代占堆内存的三分之二。新生代分为Eden(十分之八)、S1、S2
栈:栈分为JVM栈和本地方法栈。
程序计数器:存放每一个线程执行到了方法的哪一步。
方法区:以前属于堆内存,1.8之后从内存中移到了操作系统中,改为元数据区。
5、一个对象从加载到JVM,再到被GC清除,都经历了什么过程?
1、用户创建一个对象,JVM首先需要到方法区去找对象的类型信息。然后再创建对象。
2、JVM创建要实例化一个对象,首先要在堆中创建一个对象->半初始化状态。
3、首先在堆内存的新生代的Eden,经过一个GC,对象如果存活,从Eden进入S区,在后续的没次GC,如果对象一只存活,就会在S区一直拷贝。每移动一次年龄加1,经过多次GC(年龄15 )就会把对象放入老年代,老年代继续GC,如果继续存活就直到方法结束,就变成一个垃圾。
4、当方法结束后,栈中的指针会先移除掉,堆中的对象,经过FullGC就会被标记为垃圾,然后被GC线程清理掉。
八、git
九、MAVEN
七、mysql
1、索引的基本原理
2、mysql的聚族索引和非聚族索引的区别
答:都是B+树的数据结构。
聚族索引:将数据储存和索引放到了一会、并且按照一定的顺序组织的,找到索引也就找到了数据,数据的物理存放顺序与索引的顺序是一致的,即只要索引是相邻的,那么对应的数据也一定存在相邻的磁盘。
非聚族索引:叶子节点不存储数据,储存的是数据行地址,也就是根据索引查找数据行的位置再去磁盘查找数据,类似目录。
优势:查询通过聚族索引可以直接获取数据,相比非聚族索引要二次查找效率要高。
聚族索引对于范围查询的效率更高,因为其数据是按照大小排序的。
聚族索引适合用在排序的场合,非聚族索引不适合。
InnoDb中一定有主键,主键一定是聚族索引。
3、mysql索引的数据结构和各自优劣
索引的数据结构和具体的存储引擎有关,在mysql中使用最多的是Hash索引和B+索引,InnoDB存储引擎的默认索引的实现为B+树索引,对于Hash索引来说,底层的数据结构就是Hash表,因此在绝大多数需求为单条记录查询的时候,可以选择Hash索引。其余场景选择B+树索引。
B+树:它是一个平衡的多叉树,从根节点到每个叶子节点的高度差值不超过1,而且同等级的节点间有指针相互连接。
如果是等值查询的话,那么hash索引有绝大的优势,因为只需要经过一次算法即可找到相应的键值,前提是健是唯一的,如果不是唯一的,则还需要根据链表扫描查询。
如果是范围查询、以及模糊查询、以及排序Hash索引就没有用物之地了。
4、索引的设计原则
答:1、经常更新或者删除的字段不适合做索引
2、经常where条件的字段适合做索引
3、基数较小的字段没必要做索引
5、mysql锁的类型有哪些
6、mysql执行计划怎么看
7、事务的基本特性和隔离级别
8、怎么处理慢查询
9、ACID靠什么保证的
10、什么事MVCC
11、mysql主从同步原理
12、简述MYISAM和InnoDB的区别
13、如何实现分库分表?
14、简述mysql中索引类型对数据库性能的影响
15、分库分表如何解决唯一ID?
16、雪花算法原理
八、redis
1、REDiS是什么?
答:reids是一个基于键值对key-value的内存数据库,可以作为数据库、缓存、消息中间件。它是一种NOSQL(非关系型数据库)。它的读写数据非常快,数据类型比较丰富。
2、五种数据类型
1、string是redis最基本的类型,可以理解成与memcached一模一样的类型,一个key对应一个value。value不仅是string,也可以是数字。string类型是二进制安全的,意思是redis的string类型可以包含任何数据,比如jpg图片或者序列化的对象。string类型的值最大能存储512M。
2、Hash是一个键值(key-value)的集合。redis的hash是一个string的key和value的映射表,Hash特别适合存储对象。常用命令:hget,hset,hgetall等。
3、list列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边) 常用命令:lpush、rpush、lpop、rpop、lrange(获取列表片段)等。应用场景:list应用场景非常多,也是Redis最重要的数据结构之一,比如twitter的关注列表,粉丝列表都可以用list结构来实现。数据结构:list就是链表,可以用来当消息队列用。redis提供了List的push和pop操作,还提供了操作某一段的api,可以直接查询或者删除某一段的元素。实现方式:redis list的是实现是一个双向链表,既可以支持反向查找和遍历,更方便操作,不过带来了额外的内存开销。
4、set是string类型的无序集合。集合是通过hashtable实现的。set中的元素是没有顺序的,而且是没有重复的。常用命令:sdd、spop、smembers、sunion等。应用场景:redis set对外提供的功能和list一样是一个列表,特殊之处在于set是自动去重的,而且set提供了判断某个成员是否在一个set集合中。
5、zset和set一样是string类型元素的集合,且不允许重复的元素。常用命令:zadd、zrange、zrem、zcard等。使用场景:sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set结构。和set相比,sorted set关联了一个double类型权重的参数score,使得集合中的元素能够按照score进行有序排列,redis正是通过分数来为集合中的成员进行从小到大的排序。实现方式:Redis sorted set的内部使用HashMap和跳跃表(skipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。
3、数据类型应用场景总结
4、实际项目中使用缓存有遇到什么问题或者会遇到什么问题你知道吗?
缓存和数据库数据一致性问题:分布式环境下非常容易出现缓存和数据库间数据一致性问题,针对这一点,如果项目对缓存的要求是强一致性的,那么就不要使用缓存。我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率,而无法保证两者间的强一致性。合适的策略包括合适的缓存更新策略,更新数据库后及时更新缓存、缓存失败时增加重试机制。
5、你对redis的持久化机制了解吗?能讲一下吗?区别
答:redis为了保证效率,数据缓存在了内存中,但是会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件中,以保证数据的持久化。Redis的持久化策略有两种:
1、RDB:快照形式是直接把内存中的数据保存到一个dump的文件中,定时保存,保存策略。
2、AOF:把所有的对Redis的服务器进行修改的命令都存到一个文件里,命令的集合。Redis默认是快照RDB的持久化方式。当Redis重启的时候,它会优先使用AOF文件来还原数据集,因为AOF文件保存的数据集通常比RDB文件所保存的数据集更完整。
RDB是怎么工作的?
默认Redis是会以快照"RDB"的形式将数据持久化到磁盘的一个二进制文件dump.rdb。工作原理简单说一下:当Redis需要做持久化时,Redis会fork一个子进程,子进程将数据写到磁盘上一个临时RDB文件中。当子进程完成写临时文件后,将原来的RDB替换掉,这样的好处是可以copy-on-write。
RDB的优点是:这种文件非常适合用于备份:比如,你可以在最近的24小时内,每小时备份一次,并且在每个月的每一天也备份一个RDB文件。这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。RDB非常适合灾难恢复。
使用AOF做持久化,每一个写命令都通过write函数追加到appendonly.aof中
AOF可以做到全程持久化,只需要在配置中开启 appendonly yes。这样redis每执行一个修改数据的命令,都会把它添加到AOF文件中,当redis重启时,将会读取AOF文件进行重放,恢复到redis关闭前的最后时刻。
使用AOF的优点是会让redis变得非常耐久。可以设置不同的fsync策略,aof的默认策略是每秒钟fsync一次,在这种配置下,就算发生故障停机,也最多丢失一秒钟的数据。缺点是对于相同的数据集来说,AOF的文件体积通常要大于RDB文件的体积。根据所使用的fsync策略,AOF的速度可能会慢于RDB。
该用哪一个呢?
如果你非常关心你的数据,但仍然可以承受数分钟内的数据丢失,那么可以额只使用RDB持久。AOF将Redis执行的每一条命令追加到磁盘中,处理巨大的写入会降低Redis的性能,不知道你是否可以接受。数据库备份和灾难恢复:定时生成RDB快照非常便于进行数据库备份,并且RDB恢复数据集的速度也要比AOF恢复的速度快。当然了,redis支持同时开启RDB和AOF,系统重启后,redis会优先使用AOF来恢复数据,这样丢失的数据会最少。
6、REDIS过期键的删除策略
答:1、定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。
2、定期删除:每隔一段时间,程序对数据库进行一次检查,删除里面的过期键,至于要删除哪些数据库的哪些过期键,则由算法决定。
3、惰性删除:放任过期键不管,每次从键空间中获取键时,检查该键是否过期,如果过期,就删除该键,如果没有过期,就返回该键。
7、REDIS的线程模型,单线程为什么效率这么高
答:线程模型:Redis确实是单进程单线程的模型,因为Redis完全是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章的采用单线程的方案了(毕竟采用多线程会有很多麻烦)。
效率为什么这么高:第一:Redis完全基于内存,绝大部分请求是纯粹的内存操作,非常迅速,数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度是O(1)。第二:数据结构简单,对数据操作也简单。第三:采用单线程,避免了不必要的上下文切换和竞争条件,不存在多线程导致的CPU切换,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有死锁问题导致的性能消耗。第四:使用多路复用IO模型,非阻塞IO。
8、缓存雪崩、缓存穿透、缓存击穿
缓存雪崩:一般缓存都是定时任务去刷新,或者查不到之后去更新缓存的,定时任务刷新就有一个问题。举个栗子:如果首页所有Key的失效时间都是12小时,中午12点刷新的,我零点有个大促活动大量用户涌入,假设每秒6000个请求,本来缓存可以抗住每秒5000个请求,但是缓存中所有Key都失效了。此时6000个/秒的请求全部落在了数据库上,数据库必然扛不住,真实情况可能DBA都没反应过来直接挂了,此时,如果没什么特别的方案来处理,DBA很着急,重启数据库,但是数据库立马又被新流量给打死了。这就是我理解的缓存雪崩。
同一时间大面积失效,瞬间Redis跟没有一样,那这个数量级别的请求直接打到数据库几乎是灾难性的,你想想如果挂的是一个用户服务的库,那其他依赖他的库所有接口几乎都会报错,如果没做熔断等策略基本上就是瞬间挂一片的节奏,你怎么重启用户都会把你打挂。
解决方案:处理缓存雪崩简单,在批量往Redis存数据的时候,把每个Key的失效时间都加个随机值就好了,这样可以保证数据不会再同一时间大面积失效。
setRedis(key, value, time+Math.random()*10000);
如果Redis是集群部署,将热点数据均匀分布在不同的Redis库中也能避免全部失效。或者设置热点数据永不过期,有更新操作就更新缓存就好了(比如运维更新了首页商品,那你刷下缓存就好了,不要设置过期时间),电商首页的数据也可以用这个操作,保险。
缓存穿透:缓存穿透是指缓存和数据库中都没有的数据,而用户(黑客)不断发起请求,举个栗子:我们数据库的id都是从1自增的,如果发起id=-1的数据或者id特别大不存在的数据,这样的不断攻击导致数据库压力很大,严重会击垮数据库。
解决方案:缓存穿透我会在接口层增加校验,比如用户鉴权,参数做校验,不合法的校验直接return,比如id做基础校验,id<=0直接拦截。
从缓存取不到数据,在数据库中也没有找到,这时可以将key-value写为key-null。缓存时间设置30秒,这样可以防止攻击用户反复用一个id暴力攻击。
还有一个高级用法布隆过滤器(Bloom Filter) 这个也能很好的预防缓存穿透的发生,他的原理也很简单,就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查DB刷新KV再return。
缓存击穿:至于缓存击穿嘛,这个跟缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,打崩了DB,而缓存击穿不同的是缓存击穿是指一个Key非常热点,在不停地扛着大量的请求,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发直接落到了数据库上,就在这个Key的点上击穿了缓存。
解决方案:设置热点数据永不过期,或者加上互斥锁就搞定了
9、简述redis的事务实现
10、Redis和Memcached的区别
答:1、存储方式上:memcache会把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。redis有部分数据存在硬盘上,这样能保证数据的持久性。
2、数据支持类型上:memcache对数据类型的支持简单,只支持简单的key-value,,而redis支持五种数据类型。
3、使用底层模型不同:它们之间底层实现方式以及与客户端之间通信的应用协议不一样。redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
4、value的大小:redis可以达到1GB,而memcache只有1MB。
11、淘汰策略
答:Redis有六种淘汰策略
| volatile-lru | 从已设置过期时间的KV集中优先对最近最少使用(less recently used)的数据淘汰 |
| volitile-ttl | 从已设置过期时间的KV集中优先对剩余时间短(time to live)的数据淘汰 |
| volitile-random | 从已设置过期时间的KV集中随机选择数据淘汰 |
| allkeys-lru | 从所有KV集中优先对最近最少使用(less recently used)的数据淘汰 |
| allKeys-random | 从所有KV集中随机选择数据淘汰 |
| noeviction | 不淘汰策略,若超过最大内存,返回错误信息 |
12、主从复制
13、哨兵
14、集群
15、常见的缓存淘汰算法
九、消息队列
1、消息队列的优缺点、使用场景
优点:解耦、异步、削峰
1、解耦:
A 系统产生了一个比较关键的数据,很多系统需要 A 系统将数据发过来,强耦合(B,C,D,E 系统可能参数不一样、一会需要一会不需要数据,A 系统要不断修改代码维护)
A 系统还要考虑 B、C、D、E 系统是否挂了,是否访问超时?是否重试?
维护这个代码,不需要考虑人家是否调用成功,失败超时
如果新系统需要数据,直接从 MQ 里消费即可,如果某个系统不需要这条数据就取消对 MQ 消息的消费即可。
总结:通过一个 MQ 的发布订阅消息模型(Pub/Sub), 系统 A 跟其他系统就彻底解耦了
2、异步:
削峰:
高峰期每秒 5000 个请求,每秒对 MySQL 执行 5000 条 SQL(一般MySQL每秒 2000 个请求差不多了),如果MySQL被打死,然后整个系统就崩溃,用户就没办法使用系统了。但是高峰期过了之后,每秒钟可能就 50 个请求,对整个系统没有任何压力。
使用 MQ 进行削峰的场景
5000 个请求写入到 MQ 里面,系统 A 每秒钟最多只能处理 2000 个请求(MySQL 每秒钟最多处理 2000 个请求),系统 A 从 MQ 里慢慢拉取请求,每秒钟拉取 2000 个请求。MQ,每秒钟 5000 个请求进来,结果只有 2000 个请求出去,结果导致在高峰期(21小时),可能有几十万甚至几百万的请求积压在 MQ 中,这个是正常的,因为过了高峰期之后,每秒钟就 50 个请求,但是系统 A 还是会按照每秒 2000 个该请求的速度去处理。只要高峰期一过,系统 A 就会快速的将积压的消息给解决掉。
缺点:系统可用性降低,MQ 可能挂掉,导致整个系统崩溃、系统复杂性变高,可能发重复消息,导致插入重复数据;消息丢了;消息顺序乱了;系统 B,C,D 挂了,导致 MQ 消息积累,磁盘满了;一致性问题,本来应该A,B,C,D 都执行成功了再返回,结果A,B,C 执行成功 D 失败
2、RabbitMQ如何确保消息发送、消息接收
答:发送方确认机制:信道需要设置为confirm模式,则所有在信道发送的消息都会分配一个唯一ID,一旦消息被投递到queue(可持久化的消息需要写入磁盘),信道会发送一个确认给生产者(包含消息唯一ID),发送的方式是通过实现一个ConfirmCallback接口,通过回调confirm方法返回ack。如果消息队列发生了内部错误,导致投递失败,这时会实现一个ReturnCallback接口,也会回调一个方法。返回nack。通过这两方法来确认消息是否投递成功。另外发送消息是异步的。不需要非要返回才能发送第二条消息。
接收方确认机制:消费方在声明队列时,可以指定nack,当nack=false,RabbitMq会等待消费者返回ack才会将消息从队列中删除。如果nack=ture(默认),则消息已投递就会被删除。这里要注意的是消息必须要被确认,rabbitMq没有超时机制,如果消息不被确认,那么RabbitMq将不会投递下一条消息。如果消费方收到消息之后还没来得及返回ack就挂掉了,消息队列将会将这条消息重新发给下一个消费者,这样会有一个隐患,就是消息重复消费。
3、如何保证消息队列不被重复消费
答:生产者发送消息时带上一个全局唯一ID,消费者拿到消息后先根据这个ID去redis里查一下之前有没有消费过,没有消费过就处理,并且写这个id到redis,如果消费过了则不处理。
4、RabbitMq死信队列,延时队列
死信队列:相当于延长了它的生命周期,正常一条消息被消息消费会返回一个ack,消息超时或者被拒绝的话会返回一个noack和rejeck,并且没有设置重新消费,而且设置了死信队列,那么就会将消息放到死信队列中。
消息在队列存活的时间超过TTL。
消息队列数量已经超过了最大队列数量
延时队列:正常一条消息发送给消费端会直接被消费掉,如果我不想让他直接消费掉,可以给它设置一个TTL,只有设置的时间到了才可以被消费。这就是延时队列。
5、Kafka、ActiveMQ、RabbitMQ、RocketMQ对比
ActiveMQ:JMS规范,支持事务。支持的数据量比较小,官方维护越来越少。目前没有大规模的使用场景
RabbitMQ:erlang语言,性能比较好,吞吐量都在万级。消息积累会严重影响性能。
Kafka:支持的数据量非常大,生产环境有大规模的使用,ELK就是用的kafka,因为日志计较多,kafka支持的吞吐量比较大。吞吐量百万级。但是有可能会丢数据。
RocketMQ:采用java实现,方便二次开发,吞吐量十万级。
6、RabbitMQ事务消息机制
7、如何保证消息的有序性
答:MQ只需要保证局部有序,不需要保证全部有序。
生产者把一组消息放到同一个队列里,而消费者一次性消费这个队列的消息。
8、使用Mq如何保证分布式事务的最终一致性?
答:分布式事务:业务相关的多个操作,要保证他们同时成功或者同时失败。
最终一致性:与之对应的是强一致性。
MQ要保证事务的最终一致性就要做到两点:1、生产者要百分百的消息投递,事务消息机制。2、消费者保证幂等消费。
十、Spring
1、如何实现一个IOC容器
2、谈谈你对AOP的理解
答:面向接口编程,在我们业务系统中,除了实现自身业务功能外,还有一些额外的功能需要实现,例如日志,权限,在传统OOP的设计中,它导致大量的代码重复
AOP是将程序中的交叉业务逻辑(事务,权限,日志)封装成一个切面,然后注入到目标对象中去,AOP可以对某些对象的功能进行增强,可以在执行方法之前额外的做一些事情,也可以在执行方法之后额外的做一些功能。
3、谈谈对IOC的理解
答:容器,控制反转,依赖注入
IOC容器:实际上就是一个map,里面存放的是各种对象(在xml中配置的bean节点,@Repository、@Controller、@Component),在项目启动的时候会读取配置文件的bean节点,根据全限定类名使用反射创建对象放入map里。
依赖注入:这时候map里就有对象了,接下来我们在代码中使用到里面的对象时,再通过DI注入(autowired、resource)
控制反转:没有引入IOC之前,对象A调用对象B,对象A在初始化或者运行到某一点时自己必须主动创建对象B,控制权在自己手中。
引入IOC之后,对象A和对象B失去可直接联系,当对象A需要对象B的时候,IOC会创建一个对象B注入到对象A需要的地方。
通过前后的对比,可以看出创建对象的过程由主动变为被动。控制权颠倒过来了,所以叫控制反转。
4、BeanFactory和 AppicationContext有什么区别?
答:AppicationContext是BeanFactory的子接口。
ApplicationContext提供了更完整的功能:
1、继承MessageSource,因此支持国际化。
2、统一的资源文件访问方式
3、提供监听器中注册bean事件
4、同时加载多个配置文件
另外,BeanFactory采用的延迟加载的形式来注入bean的,即只有在使用某个bean时(调用getBean)才会对Bean进行实例化加载。这样,我们就不能发现一些存在的Spring的配置问题。如果bean的某一属性没有注入,知道第一次调用getBean方法才会抛出异常。
ApplicationContext,它是在容器启动时一次性创建了所有Bean,这样在容器启动时,我们就可以发现Spring存在的配置错误问题,有利于检查所依赖属性是否注入。ApplicationContext启动时预载入所有的实例bean,确保在需要用到它的时候,就不用等待,因为已经创建好了。
相对于BeanFactory,ApplicationContext唯一不足的是占用空间,因为它是在容器启动时一次性创建了所有Bean。当应用程序Bean较多时程序启动较慢。
5、描述一下Bean的生命周期
答:1、解析类得到BeanDefinition;
2、如果有多个构造方法,则要推断构造方法;
3、确认好构造方法后,进行实例化得到一个对象;
4、对对象中加了AutoWired注解的属性进行属性填充;
5、回调Aware方法,比如BeanNameAware,BeanFactoryAware;
6、调用BeanPostProcessor的初始化前的方法;
7、调用初始化方法;
8、调用BeanPostProcessor的初始化后的方法,这里会进行AOP;
9、如果当前的bean是单例的则把bean放入单例池;
10、使用bean;
11、Spring容器关闭时调用dispostableBean中的destory()方法;
6、解释下Spring支持的几种bean的作用域。
答:singleton、prototype、request、session、global-session
7、Spring框架中单利bean是线程安全的吗?
8、Spring框架中都用到哪些设计模式
1、简单工厂:Spring的BeanFactory就是简单工厂模式的体现,根据传入一个唯一标识来创建对象。
2、工厂方法:实现了FactoryBean接口的Bean是一类叫做factory的bean,其特点是Spring会使用getBean()调用获得该Bean时,会自动调用bean的getObject方法,,所以返回的不是factory这个bean,而是bean. getObject的返回值。
3、单例模式:Spring创建实例的时候默认都是单例模式
4、适配器模式:Spring中有一个叫handAdptor
5、装饰者模式:Spring中用到的装饰者模式在类名上有两种表现:一种是类名带有Wrapper,另一种类名中含有Decorator
6、动态代理:SpringAop用到的就是动态代理
7、观察者模式:spring 中监听器用到的就是观察者模式
8、策略模式:Spring框架针对不同类型的文件采用不同类型的Resource去访问
9、事务的实现方式和原理以及隔离级别
答:编程式和声明式
10、Spring事务传播机制
11、Spring事务什么时候会失效?
12、什么是Bean的自动装配?
十一、mybatis
1、mybatis的优缺点
2、mybatis和Hibernate的区别
3、#{} 和&{}的区别
十二、SpringMVC
1、SpringMvc的执行流程
2、SpringMvc的九大组件
十三、Springboot
1、SpringBoot自动配置原理
2、如何理解SpringBoot中的Starter
十四、Dubbo、Zookeeper
十五、数据结构及算法
十六、设计模式
十七、其他
十八、SpringCloud
十九、ES
二十、linux