一、基础篇
Java 基础
1.面向对象的特征
可以说是三大特征,也可以说是四大特征
①第一个是抽象:就是把有共同特征的一类事物,构造成类,只关注它的属性和行为,而不关注这些行为的细节
②第二个是封装:就是把数据和操作数据的方法绑定起来,私有化,对外只提供一个最简单的方法
③第三个是继承:子类继承父类的属性,比如说有多个类定义了共性的内容时,为了提高代码的复用性,就把这些类中的共性内容抽取出来,定义在一个独立的类中,然后再使用其他类去继承这个类中的共性部分,实现了复用,这就是继承
④第四个是多态:顾名思义就是一个对象具有多种形态,也可以理解为事务存在的多种形态,具体表现就是一个父类引用指向子类实现,多态实现的前提是,必须存在继承或者实现关系。
2.final, finally, finalize 的区别
①首先是final修饰符,被final修饰的类,是最终类,不能作为父类被子类继承,所以说一个类不能同时被final和abstract同时修饰; 被final修饰的变量,它的属性不能改变,所以在创建的时候必须给出初始值,此后只能被使用; 被final修饰的方法也同样只能被使用,不能被重写。
②然后是finally,finally是异常处理语句结构的一部分,不管有没有异常发生,finally里的代码块总是会被执行,除非是虚拟机停止才不被执行,所以在有需要无论发生什么都必须执行的代码,就可以放在finally块里,比如IO流,JDBC连接的资源释放,就适合写在这里。
③finalize ,finalize()是Object中的方法,当垃圾回收器将要回收对象所占内存之前被调用。
但在java中,如果内存充足,垃圾回收可能永不执行
3.int 和 Integer 有什么区别
JAVA数据类型有基本数据类型和引用数据类型,为了方便把基本数据类型当成对象处理,JAVA引入了基本数据类型对应的封装类,int的封装类就是Integer
所以说他们的区别,第一点就是数据类型不同,int是基本数据类型,Integer是引用数据类型;第二点是默认值不同,int默认值是0,integer默认值是null;第三点是int可以直接存储数值,而Integer必须要实例化对象,指向对象的地址。还有值得提的一点就是如果在-128到正127之间的数值,保存在JAVA的常量池中,他们被装箱成Integer对象后,会在内存中被重用,始终只存在一个对象。
parseInt()是把String转换成int,是基本类型。
valueOf()还可以接受int类型参数,返回的封装类Integer。
4.重载和重写的区别
方法重写和方法重载都是实现多态的方式,区别在于重载是编译时的多态,重写是运行时的多态;
重载发生在一个类中,同名的方法如果有不同的参数列表,参数类型不同,参数个数不同或者都不相同,就是方法重载,和返回值没有关系;
重写是发生在父类和子类之间,重写要求子类重写的方法和父类被重新写方法有相同的参数列表,有兼容的返回类型(子类<父类),比父类被重新写方法更好访问,不能比父类被重写方法声明更多的异常。
5.说说反射的用途及实现
反射的核心是JVM在运行的时候,才动态加载类或者调用方法或属性,事先不需要知道运行的对象是谁。
主要用途,就是开发各种通用的框架,比如Spring,通过xml文件去配置JavaBean;
实现比如可以获取class对象,创建实例,获取方法,调用方法,获取类的成员变量信息,获取构造器信息等。
①类名.class
②对象名.getClass
③class.forName(全限名)
6.说说自定义注解的场景及实现
java中有四种元注解:@Retention、@Inherited、@Documented、@Targe
使用场景有
①类属性自动赋值
②验证对象属性完整性
③代替配置文件功能,像spring基于注解的配置
④可以生成文档,像java代码注释中的@see,@param等
7.HTTP 请求的 GET 与 POST 方式的区别
一般我们在浏览器输入一个网址访问网站都是GET请求;
在FORM表单中,可以通过设置Method指定提交方式为GET或者POST提交方式,默认为GET提交方式
1.请求方式上
GET请求,请求的数据会附加在URL后面,用问号?分割URL和传输的数据,多个参数直接用&号连接,采用的编码格式是ASCII。
Post请求,会把请求的数据放在HTTP的请求体中
2.传输数据大小上
GET请求受到URL长度的限制,因此传输数据较小。(URL不存在参数上限的问题,这个限制是特定浏览器和服务器的限制)
POST理论上不会受到限制,除非是服务器规定限制提交数据的大小。
3.安全性上
POST的安全性更高,GET请求会把数据暴露在地址栏中,如果是用户名和密码明文出现在URL上,很容易被别人查看浏览器的历史记录而获取到账号密码
4.GET是从服务器上获取数据,而POST是用来向服务器上传递数据
8.如何解决ajax跨域
有三种解决方案
第一是①在响应头中添加Header允许访问,因为JavaScript无法控制HTTP头,需要通过目标域返回的HTTP头来授权是否允许跨域访问,在Controller中加@CrossOrigin注解
第二是②使用jsonp,只支持get请求,不支持post请求,使用时要把dataType改成jsonp,jsonp写jsonpCallback,后端获取get请求中的jsonpCallback,构造回调结构
第三是③使用接口网关,nginx、springcloud zuul
实际上就是通过"同源"的域名,不同的项目名进行区分,通过nginx拦截匹配,转发到对应的网址。整个过程,两次请求,第一次请求nginx服务器,第二次nginx服务器通过拦截匹配分发到对应的网址。
9.session 与 cookie 区别
①存储位置:Cookie存储在浏览器或者本地,Session只能存在服务器上
②存储对象:Cookie只能存储String类型的对象,Session能够存储任意的java对象
③安全性:Cookie有安全隐患,拦截或者在本地文件中能找到cookie,进而攻击,Session相对安全
④性能上:Session占用服务器的性能,Session过多,会增加服务器的压力
⑤存储大小上:单个Cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个Cookie
Session没有大小的限制,和服务器的内存大小有关系。
session的工作原理:
当浏览器请求服务器后,服务器会读取浏览器的cookie中有无sessionid,如果没有读到,那么容器将创建一个session对象,同时会为该session分配一个sessionid,当服务器响应时,这个sessionid会随着响应报文写入客户端的cookie中,那么下次再访问时,这个sessionid随着cookie传入服务器,服务器获取cookie中的sessionid,获取到后在容器中获取和sessionid对应的session对象。
10.JDBC 流程
执行一次JDBC连接,一般需要6个步骤。
第一步,导入jar包。
第二步,注册JDBC的驱动程序,需要初始化驱动程序,这样就可以打开和数据库的通信。
(可以不注册驱动)
第三步,创建一个连接,用DriverManager.getConnection()方法来创建一个Connection连接对象,它代表一个数据库的物理连接。
第四步,执行一个查询,需要使用一个Statement或PreparedStatement类型的对象,并且提交一个SQL语句到数据库中执行。
(关系:PreparedStatement继承自Statement,两者都是接口)
(区别:PreparedStatement可以使用占位符,防止SQL注入,而且是预编译的,批处理比Statement效率高)
第五步,是执行完查询获取到一个结果集,从结果集中获取检索到的结果。
最后一步就是释放资源,减少资源的浪费。
11.MVC 设计思想
Model-View-Controller
把一个应用的输入,处理,输出按照Model,Controller,View的方式分离,这样应用就被分成三层也就是模型层, 控制层,视图层
视图层代表用户交互界面,对于web应用来说,就是HTML界面
模型层就是业务流程、状态的处理以及业务规则的制定
控制层就是从客户接收请求,将模型与视图匹配在一起,就是一个分发器
12.equals 与 == 的区别
①"=="(比较的是地址值)
在编译String s1=“a"的时候.
其实是jvm在常量池中创建了一个内容为"a"的地址值,然后让s1去指向"a”,而不是把"a"直接赋值给s1;
在编译String s2=“a"的时候常量池中已经有了"a"的地址值,所以让s2直接指向常量池中的"a”,这样s1和s2的地址值都是常量池中"a"的地址值,所以通过双等号的运算结果是true。
然而在第二种情况下执行String s2=new String(“a”)的时候,每new一次就会出现一个新的对象,所以这种情况是直接在堆内存中开辟了一块新的空间去储存"a",所以此时s1和s2的地址值是不一样的,自然==的结果就为false
②equals 比较的是地址指向的内容是否相等
先比较地址是否相同,如果地址相同,值就一定相等,如果地址不同,再判断是不是String类型,然后比较字符串的长度,如果不同直接false,相同再转成字符数组,来循环比较,如果都相同就是true,否则就是false
13.java有以下四种创建多线程的方式
- 1:继承Thread类创建线程
- 2:实现Runnable接口创建线程
- 3:使用Callable和FutureTask创建线程
- 4:使用线程池,例如用Executor框架创建线程
callable和Runable的区别
Callable与Runnable的功能大致相似,Callable中有一个call()函数,但是call()函数有返回值,
而Runnable的run()函数不能将结果返回给客户程序。
实现Runnable 与 继承Thread相比有什么优势?
实现Runnable接口避免了单继承的局限性,所以较为常用。
实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。
继承Thread类,线程对象和线程任务耦合在一起。
一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。
实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。
Runnable接口对线程对象和线程任务进行解耦。
什么是线程安全
当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这
个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类时线程安全的。
如果一段代码可以保证多个线程访问的时候正确操作共享数据,那么它是线程安全的。
①sleep()
sleep() 方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间,没有释放锁。
②wait()
wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程。
14.线程池参数
①corePoolSize——线程池核心线程大小数
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁;
②maximumPoolSize——线程池最大线程数量
一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接将任务交给这个空闲线程来执行,
如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程。
线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制。
③keepAliveTime——空闲线程存活时间
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁。
④unit——空闲线程存活时间单位
⑤workQueue——工作队列(阻塞队列)
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。
⑥threadFactory——线程工厂
创建一个新线程时使用的工厂,可以用来设定线程名等等。
⑦handler——拒绝策略
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,这里就拒绝。
AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
CallerRunsPolicy:由调用线程处理该任务
*创建线程池的4种方式
①newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
②newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
③newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
④newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
15.String常用方法
一、判断
①equals equalsIgnoreCase——相等
②contains——包含
③startsWith endsWith——以…开头或结尾
二、改变内容
①toUpperCase toLowerCase——大写、小写
②replace——替换
③subString——截取
④trim——去除字符串前后空格
三、长度
①length()
②indexof
③charAt
*String StringBuffer StringBuilder
String底层是final修饰的是字符数组(不可变)
StringBuffer安全 同步锁 慢 append在原来的基础上拼接
StringBuilder不安全 无锁 快 append在原来的基础上拼接
16.深拷贝和浅拷贝
对于基本类型,深拷贝和浅拷贝都是一样的,都是对原始数据的复制,修改原始数据,不会对复制数据产生影响。
两者的区别,在于对引用属性的复制。
①浅拷贝
浅拷贝复制引用属性时,仅仅复制指针值,没有复制指向的对象。
②深拷贝
深拷贝完整复制一份该属性指向的对象,两个对象修改时,互不影响。
Object.clone()方法属于浅拷贝。
如果想使用深拷贝,必须在类里面重写clone()方法。
要想调用类的clone()方法,类必须实现Cloneable接口,
该接口是个空接口,如果不实现该接口,调用clone()方法会抛出异常CloneNotSupportedException。
17.java8新特性
①Lambda表达式
函数式接口
Lambda表达式允许把函数作为一个方法的参数(函数作为参数传递进方法中),或者把代码看成数据在最简单的形式中,一个lambda可以由用逗号分隔的参数列表、–>符号与函数体三部分表示。
②**流(Stream) **
流就是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列,生成一个新集合。
1.创建 Stream
一个数据源(如: 集合、数组), 获取一个流。
2.中间操作
一个中间操作链,对数据源的数据进行处理。
3.终止操作(终端操作)
一个终止操作,执行中间操作链,并产生结果 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7M9bzrtB-1639059913921)(C:\Users\Administrator\Desktop\cz\20200525225333187.jpg)]
二、Java 集合
1.List 和 Set 和Map区别
List
一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。
常用的实现类有 ArrayList、LinkedList 和 Vector。
Arraylist: Object数组
Vector: Object数组
LinkedList: 双向循环链表
Set
一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。
常用实现类是 HashSet、LinkedHashSet 以及 TreeSet
HashSet(无序,唯一)
基于 HashMap 实现的,底层采用 HashMap 来保存元素
LinkedHashSet
LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的
TreeSet(有序,唯一)
红黑树(自平衡的排序二叉树)
Map
是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。
Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。
常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap
3.ArrayList 与 LinkedList 区别
主要差别在于数据结构不同,ArrayList是基于动态数组实现的,内存空间连续,而LinkedList是基于双向链表实现的。
因为结构的不同,在查询比较多的情况下,ArrayList要优于LinkedList,因为LinkedList要移动指针
在新增和删除操作比较多的情况下,LinkedList要优于ArrayList,因为ArrayList要移动数据进行排序
LinkedList需要更多的内存,因为ArrayList的每个索引的位置都是实际数据,而LinkedList中的每个节点存储的是实际数据和前后节点的位置。
4.ArrayList 与 Vector 区别
ArrayList和Vector都实现了List接口,都是有序、可重复的集合
区别主要在两个方面。
一方面是线程安全上,Vector的线程安全,因此效率会低一些,而ArrayList是非线程安全,效率会高一些。
另一方面在扩容上,ArrayList增长为原来的1.5倍,Vector增加为原来的一倍,都可以设置初始的空间大小。
(另外Vector可以设置增长的空间大小,而ArrayList没有这样的方法。)
5.HashMap 和 HashTable 的区别
① Hashtable是线程安全,而HashMap则非线程安全,Hashtable的实现方法里面大部分都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合。
②HashMap的键和值都可以为null,而Hashtable的键值都不能为null。
③ HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。HashMap扩展容量是当前容量翻倍,Hashtable扩展容量是容量翻倍+1。
④两者的哈希算法不同
HashMap是先对key(键)求hashCode码,然后把这个码和数组初始长度减一做&(与运算)
就可以计算出此键值对应该保存到数组的那个位置上(hash&(n-1))。
hashtable直接计算key的哈希码,然后与2的31次方做&(与运算),然后对数组长度取余数计算位置。
⑤HashMap是对Map接口的实现,HashTable实现了Map接口和Dictionary抽象类。
6.HashSet 和 HashMap 区别
HashMap | HashSet |
---|---|
实现了Map接口 | 实现Set接口 |
存储键值对 | 仅存储对象 |
调用put()向map中添加元素 | 调用add()方法向Set中添加元素 |
HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false |
HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 | HashSet较HashMap来说比较慢 |
HashMap
线程不安全,底层是数组加链表或者红黑树实现,初始容量16,扩容为原来的2倍,扩充因子是0.75
key和value都可以为null值,key不可以重复,但是value可以重复,如果key重复,value值会覆盖,
HashMap是先对key(键)求hashCode码,然后把这个码和数组初始长度减一做&(与运算)
就可以计算出此键值对应该保存到数组的那个位置上,如果重复,就用倒插法,先来的放后边。
如果链表的长度大于等于8时,链表就会转为树结构。
链表长度超过8转换成树结构
如果桶中的链表元素个数小于等于6时,树结构会还原成链表。
因为红黑树的平均查找长度是log(n),长度为8的时候,平常查找长度为3,
如果继续使用链表,平均查找长度是8/2=4,因此才有转换为树的必要。
如果链表长度小于等于6,查找长度是6/2=3,虽然速度也很快,但是转换为树结构,和生成树也需要时间,因为不用转成树
而且选择6和8,中间有一个差值7,可以有效的防止链表和树结构频繁转换。
假如链表个数超过8就转换成树结构,链表个数小于8就转换成链表结构,如果有一个HashMap不停的插入、删除元素。链表个数在8左右
徘徊,就会频繁的发生树转链表,链表转树,效率会很低。
ConcurrentHashMap
底层是数组加链表或者红黑树实现,
jdk1.7中采用Segment分段数组的方式进行实现,采取分段锁来保证安全性,对每一个段加锁,这样就比hashtable给整个数组加锁要效率高,理论上效率要高16倍。
JDK1.8 做了优化,直接用Node 数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized 和 CAS来操作,在头结点加锁,整个看起来就像是优化过且线程安全的 HashMap。
HashMap多线程条件下会出现死循环
线程1扩容完成,还没来得及把原数据重新排序赋值,切换到线程2,线程2完成这个顺序,把原数据重新排序完成,线程1再来排序,元素A移动节点头的位置,指向元素B的entry,在这之前元素B的next也指向元素A的entry,出现死循环。也有key相同,出现元素丢失的情况。
三、Java 锁机制
1.说说线程安全问题
比如说有一个arraylist数组,有两个线程往数组里赋值,正常顺序是A线程赋值,然后让size加1,随后B线程赋值,然后size+1,但在实际运行中,可能A线程赋完值,还没Size+1,这个时候cpu正好轮转给B线程,这个时候B线程赋值的位置还没有size+1,所以和A线程操作了同一个位置上的值,这样的线程就是不安全的。
当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这
个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类时线程安全的。
如果一段代码可以保证多个线程访问的时候正确操作共享数据,那么它是线程安全的。
为了解决这种线程安全问题,可以加锁
synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完整性,使用方法一般是加在方法上。
虽然加synchronized关键字,可以让我们的线程变得安全,但是我们在用的时候,也要注意缩小synchronized的使用范围,如果随意使用时很影响程序的性能,别的对象想拿到锁,结果你没用锁还一直把锁占用,这样就有点浪费资源。
或者lock锁
2.volatile 实现原理
volatile是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。
volatile的用法比较简单,只需要在声明一个可能被多线程同时访问的变量时,使用volatile修饰就可以了。
原理:
为了提高处理器的执行速度,在处理器和内存之间增加了多级缓存来提升。但是由于引入了多级缓存,就存在缓存数据不一致问题。
但是,对于volatile变量,当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。
但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议
缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
因此volatile保证了可见性
①volatile与可见性
Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。
②volatile与有序性
volatile除了可以保证数据的可见性之外,还有一个强大的功能,那就是他可以禁止指令重排优化,使程序执行的顺序按照代码的先后顺序执行
③volatile与原子性
为了保证原子性,需要通过字节码指令monitorenter和monitorexit,但是volatile和这两个指令之间是没有任何关系的。
所以,volatile是不能保证原子性的。
3.synchronized 实现原理
显式同步:
查看带有Synchronized语句块的class文件可以看到在同步代码块的起始位置插入了moniterenter指令,在同步代码块结束的位置插入了monitorexit指令。(JVM需要保证每一个monitorenter都有一个monitorexit与之相对应,但每个monitorexit不一定都有一个monitorenter)
隐式同步:
但是查看同步方法的class文件时,同步方法并没有通过指令monitorenter和monitorexit来完成,而被翻译成普通的方法调用和返回指令,只是在其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。
monitorenter和monitorexit。当一条线程进行执行的遇到monitorenter指令的时候,它会去尝试获得锁,如果获得锁那么锁计数+1(为什么会加一呢,因为它是一个可重入锁,所以需要用这个锁计数判断锁的情况),如果没有获得锁,那么阻塞。当它遇到monitorexit的时候,锁计数器-1,当计数器为0,那么就释放锁
4.synchronized 与 lock 的区别
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VYxvl1vY-1639059913922)(C:\Users\Administrator\Desktop\cz\微信截图_20211019085725.png)]
两者都是锁,用来控制并发冲突,
①区别在于synchronized 是一个关键字,是jvm层面上的,Lock是个接口,提供的功能更加丰富
②synchronized获取锁的线程执行完同步代码会自动释放锁,线程执行过程中发生异常,JVM会让线程释放锁。
而Lock必须手动释放,并且代码中出现异常会导致unlock代码不执行,所以Lock一般在Finally中释放。
③synchronized假设A线程获得锁,B线程等待,如果A线程阻塞,B线程会一直等待下去。
Lock可以尝试获得锁,线程不用一直等待
④synchronized无法判断锁的状态,lock可以提供trylock()判断锁的状态
⑤synchronized不可中断,是非公平锁,而lock锁可以通过 lock.lockInterruptibly()方法中断锁
⑥性能上synchronized少量同步,lock适合大量同步
5.CAS 乐观锁
CAS是一种有名的无锁算法。
synchronized关键字保证同步,加锁,这种锁机制会产生一些问题
多线程时,加锁释放锁会耗费很多时间,引发性能问题
独占锁是悲观锁,synchronized是独占锁
而更有效的是乐观锁,CAS就是乐观锁
CAS有三个参数,内存位置的值—V,预期原值—A,新值—B
如果内存位置的值V与预期原值A相匹配,那么处理器会自动将该位置更新为新值;否则,处理器不做任何操作
总结如下:
- CAS(Compare And Swap)比较并替换,是线程并发运行时用到的一种技术
- CAS是原子操作,保证并发安全,而不能保证并发同步
- CAS是CPU的一个指令(需要JNI调用Native方法,才能调用CPU的指令)
- CAS是非阻塞的、轻量级的乐观锁
CAS优点
非阻塞的轻量级的乐观锁,通过CPU指令实现,在资源竞争不激烈的情况下性能高,
相比synchronized重量锁,synchronized会进行比较复杂的加锁、解锁和唤醒操作
CAS缺点
①会产生ABA问题
②自旋时间过长,消耗CPU资源,如果资源竞争激烈,多线程自旋长时间消耗资源
何时使用
**乐观锁适用于写比较少的情况下(多读场景),**即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的吞吐量。
但如果是多写的情况,一般会经常发生冲突,这就会导致CAS算法会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用
悲观锁就比较合适
6.ABA 问题
ABA问题: 线程C、D;线程D将A修改为B后又修改为A,此时C线程以为A没有改变过,java的原子类AtomicStampedReference,通过控制变量值的版本号来保证CAS的正确性。具体解决思路就是在变量前追加上版本号,每次变量更新的时候把版本号加一,那么A - B - A就会变成1A - 2B - 3A
7.乐观锁的业务场景及实现方式
8.锁优化/锁升级
锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。
锁的四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)
(1)偏向锁:
大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。
偏向锁的升级
当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致还是线程1获取锁对象,则无需使用CAS来加锁、解锁;
如果不一致,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID,那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,线程2可以竞争将其设置为偏向锁;
如果存活,那么立刻查找该线程的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
(2)轻量级锁
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
轻量级锁升级为重量级
CAS乐观锁如果自旋时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。
重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
9.单例模式
单例模式是一种对象创建模式,用于生产一个对象的实例,它可以确保系统中一个类只产生一个实例,这样做有两个好处:
1.对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。
2.由于new操作的次数减少,所以系统内存的使用评率也会降低,这将减少GC压力,缩短GC停顿时间。
①饿汉模式(天生线程安全)
public final class EagerSingleton {private static EagerSingleton singObj = new EagerSingleton();private EagerSingleton() {}public static EagerSingleton getSingleInstance() {return singObj;}
}
这种写法就是所谓的饥饿模式,每个对象在没有使用之前就已经初始化了。这就可能带来潜在的性能问题:如果这个对象很大呢?没有使用这个对象之前,就把它加载到了内存中去是一种巨大的浪费。
针对这种情况,我们可以对以上的代码进行改进,使用一种新的设计思想——延迟加载(Lazy-load Singleton)。
②懒汉模式(非线程安全)
public final class LazySingleton {private static LazySingleton singObj = null;private LazySingleton() {}public static LazySingleton getSingleInstance() {if (null == singObj ) {singObj = new LazySingleton();}return singObj;}
}
③懒汉模式(线程安全)
public final class ThreadSafeSingleton {private static ThreadSafeSingleton singObj = null;private ThreadSafeSingleton() {}public static Synchronized ThreadSafeSingleton getSingleInstance() {if (null == singObj ) {singObj = new ThreadSafeSingleton();}return singObj;}
}
④懒汉模式(双重锁校验)
public final class DoubleCheckedSingleton {private volatile static DoubleCheckedSingletonsingObj = null;private DoubleCheckedSingleton() {}public static DoubleCheckedSingleton getSingleInstance() {if (null == singObj ) {Synchronized(DoubleCheckedSingleton.class) {if (null == singObj) {singObj = new DoubleCheckedSingleton();}}}return singObj;}
}
四、数据库
Mybatis执行sql语句的批量操作
①foreach标签
②循环调用方法
③执行器切换为batch 对相同的sql进行一次预编译,然后设置参数,最后统一执行操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vw6kO30X-1639059913923)(C:\Users\Administrator\Desktop\cz\微信图片_20211028084936.png)]
SQL语句数据库层面的解析过程
①先查询高速缓存(library cache)
②语句合法性检查(data dict cache)
③语言含义检查(data dict cache)
④获得对象解析锁(control structer)
⑤数据访问权限的核对(data dict cache)
⑥确定最佳执行计划。
1.数据库设计的三大范式
第一范式
在关系模式R中,所有属性只包含原子值,原子性: 每一列不可拆分
第二范式
当且仅当关系模式R满足第一范式,必须有主键,每张表只描述一件事
第三范式
目的: 消除传递依赖 关联性: 从表的外键必须使用主表的主键
2.如何优化数据库 MySQL
①Sql语句及索引的优化(索引优化 1.Join语句优化 2.避免索引失效)
②数据库表结构的优化:符合三大范式
③系统配置的优化
④硬件的优化(扩大虚拟内存等)
3.MySQL索引使用的注意事项
什么是索引?
索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。
索引是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。
更通俗的说,索引就相当于目录。为了方便查找书中的内容,通过对内容建立索引形成目录。索引是一个文件,它是要占据物理空间的。
索引有哪些优缺点?
索引的优点
- 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
- 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
索引的缺点
- 时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率;
- 空间方面:索引需要占物理空间。
索引设计的原则
1.适合索引的列是出现在where子句中的列,或者连接子句中指定的列
2.基数较小的类,索引效果较差,没有必要在此列建立索引
3.使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间
4.不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。
创建索引的原则
索引虽好,但也不是无限制的使用,最好符合一下几个原则
1) 最左前缀匹配原则,组合索引非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
适合索引的列是出现在where子句中的列
2)较频繁作为查询条件的字段才去创建索引
3)更新频繁字段不适合创建索引
基数较小的类,不适合创建索引
4)若是不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低)
5)不要过度索引,能使用扩展索引,尽量不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可, 索引需要额外的磁盘空间,并降低写操作的性能
使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间
6)定义有外键的数据列一定要建立索引。
7)对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。
8)对于定义为text、image和bit的数据类型的列不要建立索引。
4.数据库索引的原理
索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。
索引的原理很简单,就是把无序的数据变成有序的查询
①把创建了索引的列的内容进行排序
②对排序结果生成倒排表
③在倒排表内容上拼上数据地址链
④在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据
5.什么情况下索引会失效
1、索引列中使用了运算或者函数
2、or语句中前后字段没有同时都为索引
3、数据类型出现隐式转化, 例如字符串比较没有使用单引号
4、like语句以%开头
5、索引字段中使用is null 、is not null 、!=、<>
6、复合索引中没有遵循最佳左前缀原则:最佳左前缀原则即要使复合索引生效,必须要先有左边的字段,再使用右边的字段时才会生效,直接使用右边的字段索引不会生效
7、查询结果大于全表的30%
6.为什么要用 B-Tree
使用B树的好处
B树可以在内部节点同时存储键和值,因此,把频繁访问的数据放在靠近根节点的地方将会大大提高热点数据的查询效率。这种特性使得B树在特定数据重复多次查询的场景中更加高效。
使用B+树的好处
由于B+树的内部节点只存放键,不存放值,因此,一次读取,可以在内存页中获取更多的键,有利于更快地缩小查找范围。 B+树的叶节点由一条链相连,因此,当需要进行一次全数据遍历的时候,B+树只需要使用O(logN)时间找到最小的一个节点,然后通过链进行O(N)的顺序遍历即可。而B树则需要对树的每一层进行遍历,这会需要更多的内存置换次数,因此也就需要花费更多的时间
7.聚集索引与非聚集索引的区别
- 聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据
- 非聚簇索引:将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因
澄清一个概念:innodb中,在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引,辅助索引叶子节点存储的不再是行的物理位置,而是主键值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BZwcAcd9-1639059913923)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20211015144718293.png)]
8.说说反模式设计
9.说说分库与分表设计
10.分库与分表带来的分布式困境与应对之策
11.SQL语句优化
1)尽量选择较小的列–需要哪些列就使用哪些列 长度较小的列
2)将where中用的比较频繁的字段建立索引–有多个条件,使用索引的列排在前面
3)select子句中避免使用‘*’
4)避免在索引列上使用计算、not in 和<>等操作
5)当只需要一行数据的时候使用limit 1
6)保证单表数据不超过200W,适时分割表。针对查询较慢的语句,可以使用explain 来分析该语句具体的执行情况。
7)避免改变索引列的类型。
8)选择最有效的表名顺序,from子句中写在最后的表是基础表,将被最先处理,在from子句中包含多个表的情况下,你必须选择记录条数最少的表作为基础表。
9)尽量缩小子查询的结果
12.MySQL 遇到的死锁问题
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
常见的解决死锁的方法
1、如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
2、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
3、对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
如果业务处理不好可以用分布式事务锁或者使用乐观锁
13.存储引擎的 InnoDB 与 MyiSAM
概述
-
Innodb引擎:Innodb引擎提供了对数据库ACID事务的支持。并且还提供了行级锁和外键的约束。它的设计的目标就是处理大数据容量的数据库系统。
-
MyISAM引擎(原本Mysql的默认引擎):不提供事务的支持,也不支持行级锁和外键。
区别
-
InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引。
-
InnoDB的主键索引的叶子节点存储着行数据,因此主键索引非常高效。
-
MyISAM索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据。
-
InnoDB非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效。
14.limit 50000 加载很慢怎么解决
15.选择合适的数据存储方案
16.选择合适的分布式主键
17.SELECT语句执行顺序
开始->FROM子句->WHERE子句->GROUP BY子句->HAVING子句->ORDER BY子句->SELECT子句->LIMIT子句->最终结果
18.一条SQL语句在MySQL中执行过程
19.B+树深度优先,广度优先
20.引擎
MySQL存储引擎MyISAM与InnoDB区别
存储引擎Storage engine:MySQL中的数据、索引以及其他对象是如何存储的,是一套文件系统的实现。
常用的存储引擎有以下:
- Innodb引擎:Innodb引擎提供了对数据库ACID事务的支持。并且还提供了行级锁和外键的约束。它的设计的目标就是处理大数据容量的数据库系统。
- MyISAM引擎(原本Mysql的默认引擎):不提供事务的支持,也不支持行级锁和外键。
- MEMORY引擎:所有的数据都在内存中,数据的处理速度快,但是安全性不高。
MyISAM与InnoDB区别
MyISAM | Innodb | |
---|---|---|
存储结构 | 每张表被存放在三个文件:frm-表格定义、MYD(MYData)-数据文件、MYI(MYIndex)-索引文件 | 所有的表都保存在同一个数据文件中(也可能是多个文件,或者是独立的表空间文件),InnoDB表的大小只受限于操作系统文件的大小,一般为2GB |
存储空间 | MyISAM可被压缩,存储空间较小 | InnoDB的表需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引 |
可移植性、备份及恢复 | 由于MyISAM的数据是以文件的形式存储,所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作 | 免费的方案可以是拷贝数据文件、备份 binlog,或者用 mysqldump,在数据量达到几十G的时候就相对痛苦了 |
文件格式 | 数据和索引是分别存储的,数据.MYD ,索引.MYI | 数据和索引是集中存储的,.ibd |
记录存储顺序 | 按记录插入顺序保存 | 按主键大小有序插入 |
外键 | 不支持 | 支持 |
事务 | 不支持 | 支持 |
锁支持(锁是避免资源争用的一个机制,MySQL锁对用户几乎是透明的) | 表级锁定 | 行级锁定、表级锁定,锁定力度小并发能力高 |
SELECT | MyISAM更优 | |
INSERT、UPDATE、DELETE | InnoDB更优 | |
select count(*) | myisam更快,因为myisam内部维护了一个计数器,可以直接调取。 | |
索引的实现方式 | B+树索引,myisam 是堆表 | B+树索引,Innodb 是索引组织表 |
哈希索引 | 不支持 | 支持 |
全文索引 | 支持 | 不支持 |
创建索引:create index index_name on 表名(列名,列名)
MyISAM索引与InnoDB索引的区别?
- InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引。
- InnoDB的主键索引的叶子节点存储着行数据,因此主键索引非常高效。
- MyISAM索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据。
- InnoDB非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效。
InnoDB引擎的4大特性
- 插入缓冲(insert buffer)
- 二次写(double write)
- 自适应哈希索引(ahi)
- 预读(read ahead)
存储引擎选择
如果没有特别的需求,使用默认的Innodb
即可。
MyISAM:以读写插入为主的应用程序,比如博客系统、新闻门户网站。
Innodb:更新(删除)操作频率也高,或者要保证数据的完整性;并发量高,支持事务和外键。比如OA自动化办公系统。
索引使用场景(重点)
where
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JQzZ9Rau-1639059913924)(D:\上课资料\day11\pics\format,png)]
上图中,根据id
查询记录,因为id
字段仅建立了主键索引,因此此SQL执行可选的索引只有主键索引,如果有多个,最终会选一个较优的作为检索的依据。
-- 增加一个没有建立索引的字段
alter table innodb1 add sex char(1);
-- 按sex检索时可选的索引为null
EXPLAIN SELECT * from innodb1 where sex='男';
1234
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jwrG5b9W-1639059913925)(D:\上课资料\day11\pics\format2,png)]
可以尝试在一个字段未建立索引时,根据该字段查询的效率,然后对该字段建立索引(
alter table 表名 add index(字段名)
),同样的SQL执行的效率,你会发现查询效率会有明显的提升(数据量越大越明显)。
order by
当我们使用order by
将查询结果按照某个字段排序时,如果该字段没有建立索引,那么执行计划会将查询出的所有数据使用外部排序(将数据从硬盘分批读取到内存使用内部排序,最后合并排序结果),这个操作是很影响性能的,因为需要将查询涉及到的所有数据从磁盘中读到内存(如果单条数据过大或者数据量过多都会降低效率),更无论读到内存之后的排序了。
但是如果我们对该字段建立索引alter table 表名 add index(字段名)
,那么由于索引本身是有序的,因此直接按照索引的顺序和映射关系逐条取出数据即可。而且如果分页的,那么只用取出索引表某个范围内的索引对应的数据,而不用像上述那取出所有数据进行排序再返回某个范围内的数据。(从磁盘取数据是最影响性能的)
join
对
join
语句匹配关系(on
)涉及的字段建立索引能够提高效率
索引覆盖
如果要查询的字段都建立过索引,那么引擎会直接在索引表中查询而不会访问原始数据(否则只要有一个字段没有建立索引就会做全表扫描),这叫索引覆盖。因此我们需要尽可能的在select
后只写必要的查询字段,以增加索引覆盖的几率。
这里值得注意的是不要想着为每个字段建立索引,因为优先使用索引的优势就在于其体积小。
索引有哪几种类型?重要
主键索引: 数据列不允许重复,不允许为NULL,一个表只能有一个主键。
唯一索引: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。
- 可以通过
ALTER TABLE table_name ADD UNIQUE (column);
创建唯一索引 - 可以通过
ALTER TABLE table_name ADD UNIQUE (column1,column2);
创建唯一组合索引
普通索引: 基本的索引类型,没有唯一性的限制,允许为NULL值。
- 可以通过
ALTER TABLE table_name ADD INDEX index_name (column);
创建普通索引 - 可以通过
ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3);
创建组合索引
全文索引: 是目前搜索引擎使用的一种关键技术。
- 可以通过
ALTER TABLE table_name ADD FULLTEXT (column);
创建全文索引
索引的基本原理
索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。
索引的原理很简单,就是把无序的数据变成有序的查询
- 把创建了索引的列的内容进行排序
- 对排序结果生成倒排表
- 在倒排表内容上拼上数据地址链
- 在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据
索引算法有哪些?
索引算法有 BTree算法和Hash算法
BTree算法
BTree是最常用的mysql数据库索引算法,也是mysql默认的算法。因为它不仅可以被用在=,>,>=,<,<=和between这些比较操作符上,而且还可以用于like操作符,只要它的查询条件是一个不以通配符开头的常量, 例如:
-- 只要它的查询条件是一个不以通配符开头的常量
select * from user where name like 'jack%';
-- 如果一通配符开头,或者没有使用常量,则不会使用索引,例如:
select * from user where name like '%jack';
1234
Hash算法
Hash Hash索引只能用于对等比较,例如=,<=>(相当于=)操作符。由于是一次定位数据,不像BTree索引需要从根节点到枝节点,最后才能访问到页节点这样多次IO访问,所以检索效率远高于BTree索引。
创建索引时需要注意什么?
- 非空字段:应该指定列为NOT NULL,除非你想存储NULL。在mysql中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值;
- 取值离散大的字段:(变量各个取值之间的差异程度)的列放到联合索引的前面,可以通过count()函数查看字段的差异值,返回值越大说明字段的唯一值越多字段的离散程度高;
- 索引字段越小越好:数据库的数据存储以页为单位一页存储的数据越多一次IO操作获取的数据越大效率越高。
使用索引查询一定能提高查询的性能吗?为什么
通常,通过索引查询数据比全表扫描要快。但是我们也必须注意到它的代价。
- 索引需要空间来存储,也需要定期维护, 每当有记录在表中增减或索引列被修改时,索引本身也会被修改。 这意味着每条记录的INSERT,DELETE,UPDATE将为此多付出4,5 次的磁盘I/O。 因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢。使用索引查询不一定能提高查询性能,索引范围查询(INDEX RANGE SCAN)适用于两种情况:
- 基于一个范围的检索,一般查询返回结果集小于表中记录数的30%
- 基于非唯一性索引的检索
百万级别或以上的数据如何删除
关于索引:由于索引需要额外的维护成本,因为索引文件是单独存在的文件,所以当我们对数据的增加,修改,删除,都会产生额外的对索引文件的操作,这些操作需要消耗额外的IO,会降低增/改/删的执行效率。所以,在我们删除数据库百万级别数据的时候,查询MySQL官方手册得知删除数据的速度和创建的索引数量是成正比的。
- 所以我们想要删除百万数据的时候可以先删除索引(此时大概耗时三分多钟)
- 然后删除其中无用数据(此过程需要不到两分钟)
- 删除完成后重新创建索引(此时数据较少了)创建索引也非常快,约十分钟左右。
- 与之前的直接删除绝对是要快速很多,更别说万一删除中断,一切删除会回滚。那更是坑了。
B树和B+树的区别
-
在B树中,你可以将键和值存放在内部节点和叶子节点;但在B+树中,内部节点都是键,没有值,叶子节点同时存放键和值。
-
B+树的叶子节点有一条链相连,而B树的叶子节点各自独立。
使用B树的好处
B树可以在内部节点同时存储键和值,因此,把频繁访问的数据放在靠近根节点的地方将会大大提高热点数据的查询效率。这种特性使得B树在特定数据重复多次查询的场景中更加高效。
使用B+树的好处
由于B+树的内部节点只存放键,不存放值,因此,一次读取,可以在内存页中获取更多的键,有利于更快地缩小查找范围。 B+树的叶节点由一条链相连,因此,当需要进行一次全数据遍历的时候,B+树只需要使用O(logN)时间找到最小的一个节点,然后通过链进行O(N)的顺序遍历即可。而B树则需要对树的每一层进行遍历,这会需要更多的内存置换次数,因此也就需要花费更多的时间
Hash索引和B+树所有有什么区别或者说优劣呢?
首先要知道Hash索引和B+树索引的底层实现原理:
hash索引底层就是hash表,进行查找时,调用一次hash函数就可以获取到相应的键值,之后进行回表查询获得实际数据。B+树底层实现是多路平衡查找树。对于每一次的查询都是从根节点出发,查找到叶子节点方可以获得所查键值,然后根据查询判断是否需要回表查询数据。
那么可以看出他们有以下的不同:
- hash索引进行等值查询更快(一般情况下),但是却无法进行范围查询。
因为在hash索引中经过hash函数建立索引之后,索引的顺序与原顺序无法保持一致,不能支持范围查询。而B+树的的所有节点皆遵循(左节点小于父节点,右节点大于父节点,多叉树也类似),天然支持范围。
- hash索引不支持使用索引进行排序,原理同上。
- hash索引不支持模糊查询以及多列索引的最左前缀匹配。原理也是因为hash函数的不可预测。AAAA和AAAAB的索引没有相关性。
- hash索引任何时候都避免不了回表查询数据,而B+树在符合某些条件(聚簇索引,覆盖索引等)的时候可以只通过索引完成查询。
- hash索引虽然在等值查询上较快,但是不稳定。性能不可预测,当某个键值存在大量重复的时候,发生hash碰撞,此时效率可能极差。而B+树的查询效率比较稳定,对于所有的查询都是从根节点到叶子节点,且树的高度较低。
因此,在大多数情况下,直接选择B+树索引可以获得稳定且较好的查询速度。而不需要使用hash索引。
数据库为什么使用B+树而不是B树
- B树只适合随机检索,而B+树同时支持随机检索和顺序检索;
- B+树空间利用率更高,可减少I/O次数,磁盘读写代价更低。一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗。B+树的内部结点并没有指向关键字具体信息的指针,只是作为索引使用,其内部结点比B树小,盘块能容纳的结点中关键字数量更多,一次性读入内存中可以查找的关键字也就越多,相对的,IO读写次数也就降低了。而IO读写次数是影响索引检索效率的最大因素;
- B+树的查询效率更加稳定。B树搜索有可能会在非叶子结点结束,越靠近根节点的记录查找时间越短,只要找到关键字即可确定记录的存在,其性能等价于在关键字全集内做一次二分查找。而在B+树中,顺序检索比较明显,随机检索时,任何关键字的查找都必须走一条从根节点到叶节点的路,所有关键字的查找路径长度相同,导致每一个关键字的查询效率相当。
- B-树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。B+树的叶子节点使用指针顺序连接在一起,只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作。
- 增删文件(节点)时,效率更高。因为B+树的叶子节点包含所有关键字,并以有序的链表结构存储,这样可很好提高增删效率。
B+树在满足聚簇索引和覆盖索引的时候不需要回表查询数据,
在B+树的索引中,叶子节点可能存储了当前的key值,也可能存储了当前的key值以及整行的数据,这就是聚簇索引和非聚簇索引。 在InnoDB中,只有主键索引是聚簇索引,如果没有主键,则挑选一个唯一键建立聚簇索引。如果没有唯一键,则隐式的生成一个键来建立聚簇索引。
当查询使用聚簇索引时,在对应的叶子节点,可以获取到整行数据,因此不用再次进行回表查询。
什么是聚簇索引?何时使用聚簇索引与非聚簇索引
- 聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据
- 非聚簇索引:将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因
澄清一个概念:innodb中,在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引,辅助索引叶子节点存储的不再是行的物理位置,而是主键值
何时使用聚簇索引与非聚簇索引
非聚簇索引一定会回表查询吗?
不一定,这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回表查询。
举个简单的例子,假设我们在员工表的年龄上建立了索引,那么当进行select age from employee where age < 20
的查询时,在索引的叶子节点上,已经包含了age信息,不会再次进行回表查询。
联合索引是什么?为什么需要注意联合索引中的顺序?
MySQL可以使用多个字段同时建立一个索引,叫做联合索引。在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引。
具体原因为:
MySQL使用索引时需要索引有序,假设现在建立了"name,age,school"的联合索引,那么索引的排序为: 先按照name排序,如果name相同,则按照age排序,如果age的值也相等,则按照school进行排序。
当进行查询时,此时索引仅仅按照name严格有序,因此必须首先使用name字段进行等值查询,之后对于匹配到的列而言,其按照age字段严格有序,此时可以使用age字段用做索引查找,以此类推。因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。此外可以根据特例的查询或者表结构进行单独的调整。
事务
什么是数据库事务?
事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。
事务最经典也经常被拿出来说例子就是转账了。
假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
事务的四大特性(ACID)介绍一下?
关系性数据库需要遵循ACID规则,具体内容如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2GT5iYtX-1639059913925)(D:\上课资料\day11\pics\format5,png)]
- 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
- 一致性: 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
- 隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
- 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
什么是脏读?幻读?不可重复读?
- 脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
- 不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
- 幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。
事务的七个传播行为propagation
- REQUIRED:表示如果当前存在一个事务,则加入该事务,否则将新建一个事务;
- REQUIRES_NEW:表示不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务;
- SUPPORTS:表示如果当前存在事务,就加入该事务;如果当前没有事务,以非事务方式执行;
- NOT_SUPPORTED: 表示不使用事务;如果当前存在事务,就把当前事务暂停,以非事务方式执行;
- MANDATORY:表示必须在一个已有的事务中执行,如果当前没有事务,则抛出异常;
- NEVER:表示以非事务方式执行,如果当前存在事务,则抛出异常;
- NESTED:这个是嵌套事务;
如果当前存在事务,则在嵌套事务内执行;
如果当前不存在事务,则创建一个新的事务;
嵌套事务使用数据库中的保存点来实现,即嵌套事务回滚不影响外部事务,但外部事务回滚将导致嵌套事务回滚;
SQL 标准定义了四个隔离级别:
- READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
- READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
- REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
这里需要注意的是:Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别
事务隔离机制的实现基于锁机制和并发调度。其中并发调度使用的是MVVC(多版本并发控制),通过保存修改的旧版本信息来支持并发一致性读和回滚等特性。
因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容):,但是你要知道的是InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)**并不会有任何性能损失。
InnoDB 存储引擎在 分布式事务 的情况下一般会用到**SERIALIZABLE(可串行化)**隔离级别。
MVVC
多版本并发控制:读取数据时通过一种类似快照的方式把数据保存下来,这样读锁就和写锁不冲突了,不同事务的session会看到自己特定版本的数据,也叫版本链
对MySQL的锁了解吗
当数据库有并发事务的时候,可能会产生数据的不一致,这时候需要一些机制来保证访问的次序,锁机制就是这样的一个机制。
就像酒店的房间,如果大家随意进出,就会出现多人抢夺同一个房间的情况,而在房间上装上锁,申请到钥匙的人才可以入住并且将房间锁起来,其他人只有等他使用完毕才可以再次使用。
隔离级别与锁的关系
在Read Uncommitted级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突
在Read Committed级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁;
在Repeatable Read级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。
SERIALIZABLE 是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成。
按照锁的粒度分数据库锁有哪些?锁机制与InnoDB锁算法
在关系型数据库中,可以按照锁的粒度把数据库锁分为行级锁(INNODB引擎)、表级锁(MYISAM引擎)和页级锁(BDB引擎 )。
MyISAM和InnoDB存储引擎使用的锁:
- MyISAM采用表级锁(table-level locking)。
- InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁
行级锁,表级锁和页级锁对比
行级锁 行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。
特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
表级锁 表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。
特点:开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。
页级锁 页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。
特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
从锁的类别上分MySQL都有哪些锁呢?像上面那样子进行锁定岂不是有点阻碍并发效率了
从锁的类别上来讲,有共享锁和排他锁。
共享锁: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。
排他锁: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。
用上面的例子来说就是用户的行为有两种,一种是来看房,多个用户一起看房是可以接受的。 一种是真正的入住一晚,在这期间,无论是想入住的还是想看房的都不可以。
锁的粒度取决于具体的存储引擎,InnoDB实现了行级锁,页级锁,表级锁。
他们的加锁开销从大到小,并发能力也是从大到小。
MySQL中InnoDB引擎的行锁是怎么实现的?
答:InnoDB是基于索引来完成行锁
例: select * from tab_with_index where id = 1 for update;
for update 可以根据条件来完成行锁锁定,并且 id 是有索引键的列,如果 id 不是索引键那么InnoDB将完成表锁,并发将无从谈起
InnoDB存储引擎的锁的算法有三种
- Record lock:单个行记录上的锁
- Gap lock:间隙锁,锁定一个范围,不包括记录本身
- 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
什么是死锁?怎么解决?
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
常见的解决死锁的方法
1、如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
2、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
3、对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
如果业务处理不好可以用分布式事务锁或者使用乐观锁
数据库的乐观锁和悲观锁是什么?怎么实现的?
数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:乐一般会使用版本号机制或CAS算法实现。
两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
存储过程与函数
什么是存储过程?有哪些优缺点?
存储过程是一个预编译的SQL语句,优点是允许模块化的设计,就是说只需要创建一次,以后在该程序中就可以调用多次。如果某次操作需要执行多次SQL,使用存储过程比单纯SQL语句执行要快。
优点
1)存储过程是预编译过的,执行效率高。
2)存储过程的代码直接存放于数据库中,通过存储过程名直接调用,减少网络通讯。
3)安全性高,执行存储过程需要有一定权限的用户。
4)存储过程可以重复使用,减少数据库开发人员的工作量。
缺点
1)调试麻烦,但是用 PL/SQL Developer 调试很方便!弥补这个缺点。
2)移植问题,数据库端代码当然是与数据库相关的。但是如果是做工程型项目,基本不存在移植问题。
3)重新编译问题,因为后端代码是运行前编译的,如果带有引用关系的对象发生改变时,受影响的存储过程、包将需要重新编译(不过也可以设置成运行时刻自动编译)。
4)如果在一个程序系统中大量的使用存储过程,到程序交付使用的时候随着用户需求的增加会导致数据结构的变化,接着就是系统的相关问题了,最后如果用户想维护该系统可以说是很难很难、而且代价是空前的,维护起来更麻烦。
触发器
什么是触发器?触发器的使用场景有哪些?
触发器是用户定义在关系表上的一类由事件驱动的特殊的存储过程。触发器是指一段代码,当触发某个事件时,自动执行这些代码。
使用场景
- 可以通过数据库中的相关表实现级联更改。
- 实时监控某张表中的某个字段的更改而需要做出相应的处理。
- 例如可以生成某些业务的编号。
- 注意不要滥用,否则会造成数据库及应用程序的维护困难。
- 大家需要牢记以上基础知识点,重点是理解数据类型CHAR和VARCHAR的差异,表存储引擎InnoDB和MyISAM的区别。
MySQL中都有哪些触发器?
在MySQL数据库中有如下六种触发器:
- Before Insert
- After Insert
- Before Update
- After Update
- Before Delete
- After Delete
如何优化数据库
第一、优化索引、SQL语句、分析慢查询
第二、设计表的时候严格按照数据库的设计范式来设计数据库
第三、我们可以加上redis缓存,将经常被访问到的数据,但是不需要经常变化的数据放入至redis缓存服务器里面,这样的话能够节约太强盘I/0
第四、还可优化硬件,在硬件层面,我们可以使用更好的一些硬盘(固态硬盘),使用一些磁盘阵列技术(raid 0,raid1,raid5)
raid0 最简单的 (两块硬盘硬盘相加100G + 100 G = 200G)
raid1 镜像卷 把同样的数据写两份。可以随机从A/B里面读取,更高。硬盘坏了一块,数据也不会丢失
raid5 3块硬盘,坏了一块,另外两块也能工作。
第五、如果以上都做速度还是慢,先不要去切分可以使用MySQL内部自带的表分区技术,来将数据分成不同的文件。这样能够让磁盘在读取的时候效率更高。
第六、可以再做垂直分表,可以将不经常读数据放到另外一个表里面去。这样能够节约磁盘IO
第七、如果发现我们的效率还是不够高,我们可以采用主从的方式来将数据读写分离。
第八、数据量特别大,我们优化起来会很困难可以使数数据库中间件的方式,将数据进行分库分表分机器。(原理,数据路由)
第九、此外,我们还可以采用一些更快的存储方式,例如NoSQL来存储一些我们需要经常访问到的数据。从数据库里面取出来后,再到NoSQL取出一些其他的数据。
第十、此外还有一些表引擎 选择,参数优化和一些相关小技巧都是优化MySQL的方式
第十一、我们还可以将我们的业务系统在架构级别进行缓存,静态化和分式式。
第十二、不使用全文索引,使用xunsearch,ES或者云服务器上的索引。
如何优化SQL语句—五星重要
1)尽量选择较小的列–需要哪些列就使用哪些列 长度较小的列
2)将where中用的比较频繁的字段建立索引–有多个条件,使用索引的列排在前面
3)select子句中避免使用‘*’
4)避免在索引列上使用计算、not in 和<>等操作
5)当只需要一行数据的时候使用limit 1
6)保证单表数据不超过200W,适时分割表。针对查询较慢的语句,可以使用explain 来分析该语句具体的执行情况。
7)避免改变索引列的类型。
8)选择最有效的表名顺序,from子句中写在最后的表是基础表,将被最先处理,在from子句中包含多个表的情况下,你必须选择记录条数最少的表作为基础表。
9)避免在索引列上面进行计算。
10)尽量缩小子查询的结果
1、HTTPS 协议需要到 CA (Certificate Authority,证书颁发机构)申请证书,一般免费证书较少,因而需要一定费用。(以前的网易官网是http,而网易邮箱是 https 。)
2、HTTP 是超文本传输协议,信息是明文传输,HTTPS 则是具有安全性的 SSL 加密传输协议。
3、HTTP 和 HTTPS 使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4、HTTP 的连接很简单,是无状态的。HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。(无状态的意思是其数据包的发送、传输和接收都是相互独立的。无连接的意思是指通信双方都不长久的维持对方的任何信息。)
五、Redis缓存使用
往redis里进行写操作是单线程的,所有的操作都是原子性的。
单线程还快的原因:①纯内存操作 ②单线程反而避免多线程频繁上下文切换带来的性能问题 ③非阻塞的IO多路复用机制
1.Redis介绍优缺点
1)优点:
①读写快,读的速度能达到每秒11万次,写的速度能达到每秒8万多次
②支持数据持久化,RDB快照和AOF两种方式
③支持事务,redis所有的操作都是原子性的,还支持对几个操作合并后的原子性执行
④数据结构丰富,除了支持String类型的value还支持hash,set,list,zset
⑤支持主从复制,主机会自动把数据同步到从机,可以读写分离
2)缺点:
①数据库容量受到物理内存限制,不能用作大量数据的高性能读写,适合较小数据量的高性能操作和运算
②redis没有自动容错和恢复功能,主从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的ip才可以恢复
③主机宕机,宕机前有一部分数据没能同步到从机上,切换ip后,还会引起数据不一致的问题,降低系统的可用性
④redis较难支持在线扩容,在集群容量达到上限时,在线扩容会变得很复杂,为了避免这个问题,运维人员在系统上线时,必须保证有足够的空间,这对资源造成了很大浪费
2.Redis有哪些数据类型,及各应用场景
redis是高性能非关系型的键值对数据库
redis可以存储键和五种不同类型的值之间的映射——键的类型只能字符串,值得类型有五种
分别是:String,hash,list,set,sorted—set
①存储的数据:单个数据,最简单的数据存储类型,也是最常用的数据存储类型
存储数据的格式:一个存储空间保存一个数据
存储内容:通常使用字符串,如果字符串以整数的形式展示,可以作为数字操作使用(但是仍是字符串)
②hash类型
新的存储需求:对一系列存储的数据进行编组,方便管理,典型应用存储对象信息
需要的内存结构:一个存储空间保存多少个键值对数据
hash类型:底层使用哈希表结构实现数据存储
3.Redis 内存淘汰机制
①惰性过期:
只有当访问一个key时,才会判断该key是否已经过期,过期则清除。
这个策略可以最大化的节省CPU资源,但是对内存非常不友好。
极端情况可能会出现大量过期的key没有再次被访问,从而不会被清除,而占用大量的内存。
②定期过期:
每隔一定的时间,就会扫描一定数量的数据库的expires字典中,一定数量的key,并清除其中已经过期的key。
这个策略是两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果
4.Redis 持久化机制(快照和RDB优缺点)
Redis提供两种持久化机制RDB和AOF机制:
1)RDB(Redis DataBase)持久化方式:
是指用数据集快照的方式(半持久化模式)
记录redis数据库的所有键值对,在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。
优点:
1.只有一个文件dump.rdb,方便持久化。
2.容灾性好,一个文件可以保存到安全的磁盘。
3.性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以是IO最大化。
(使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高性能)
4.相对于数据集大时,比AOF的启动效率更高。
缺点:
1.数据安全性低,RDB是间隔一段时间进行持久化的,如果在持久化之前redis发生故障,这段时间的数据就会丢失,所以这种方式更适合数据要求不严谨的时候。
2 备份时占用内存,因为Redis 在备份时会独立创建一个子进程,将数据写入到一个临时文件(此时内存中的数据是原来的两倍),最后再将临时文件替换之前的备份文件。
3 由于RDB通过fork子进程来协助完成持久化,如果数据集较大,可能会使cpu一直在fork子进程中,导致整个服务器停止几百毫秒,甚至是1秒钟
2)AOF(Append-only file)持久化方式:
以日志的形式记录服务器处理的每一个写,删除操作,查询操作不会记录,以文本的方式记录,可以打开文件查看到详细的记录。
优点:
1.数据安全,redis中提供了三种同步策略,每秒同步,每修改同步,不同步。
每秒同步,效率比较高,但一旦宕机,这一秒修改的数据会丢失。
每修改同步,每执行一次命令就同步记录到磁盘,效率相对较低。
不同步,操作系统需要刷的时候再去刷。
2.通过append模式写文件,即使中途服务器宕机,不会影响已经存在的内容,可以通过redis-check-aof工具解决数据一致性问题。
3.AOF机制的rewrite模式。可以删除其中的某些流水账的记录(比如误操作的flushall)
缺点:
1.AOF文件比RDB文件大,且恢复速度慢。
2.数据集大的时候,比rdb启动效率低。
3.运行效率没有RDB高
如果两个都配置,恢复时会优先加载AOF
5.Redis 集群方案与实现
①主从模式:
主从模式是三种模式中最简单的,在主从复制中,数据库分为两类:主数据库(master)和从数据库(slave)。其中主从复制有如下特点:
主数据库可以进行读写操作,当读写操作导致数据变化时会自动将数据同步给从数据库
从数据库一般都是只读的,并且接收主数据库同步过来的数据
一个master可以拥有多个slave,但是一个slave只能对应一个master
slave挂了不影响其他slave的读和master的读和写,重新启动后会将数据从master同步过来
master挂了以后,不影响slave的读,但redis不再提供写服务,master重启后redis将重新对外提供写服务
master挂了以后,不会在slave节点中重新选一个master
缺点
master节点在主从模式中唯一,若master挂掉,则redis无法对外提供写服务
工作机制
当slave启动后,主动向master发送SYNC命令。master接收到SYNC命令后在后台保存快照(RDB持久化)和缓存保存快照这段时间的命令,然后将保存的快照文件和缓存的命令发送给slave。slave接收到快照文件和命令后加载快照文件和缓存的执行命令。
复制初始化后,master每次接收到的写命令都会同步发送给slave,保证主从数据一致性。
②哨兵模式:
主从模式的弊端就是不具备高可用性,当master挂掉以后,Redis将不能再对外提供写入操作,因此sentinel应运而生。
sentinel中文含义为哨兵,顾名思义,它的作用就是监控redis集群的运行状况,特点如下:
sentinel模式是建立在主从模式的基础上,如果只有一个Redis节点,sentinel就没有任何意义
当master挂了以后,sentinel会在slave中选择一个做为master,并修改它们的配置文件,其他slave的配置文件也会被修改,比如slaveof属性会指向新的master
当master重新启动后,它将不再是master而是做为slave接收新的master的同步数据
sentinel因为也是一个进程有挂掉的可能,所以sentinel也会启动多个形成一个sentinel集群
多sentinel配置的时候,sentinel之间也会自动监控
当主从模式配置密码时,sentinel也会同步将配置信息修改到配置文件中,不需要担心
一个sentinel或sentinel集群可以管理多个主从Redis,多个sentinel也可以监控同一个redis
sentinel最好不要和Redis部署在同一台机器,不然Redis的服务器挂了以后,sentinel也挂了
工作机制
每个sentinel以每秒钟一次的频率向它所知的master,slave以及其他sentinel实例发送一个 PING 命令。
如果一个实例距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值。
则这个实例会被sentinel标记为主观下线。如果一个master被标记为主观下线,则正在监视这个master的所有sentinel要以每秒一次的频率确认master的确进入了主观下线状态。
当有足够数量的sentinel(大于等于配置文件指定的值)在指定的时间范围内确认master的确进入了主观下线状态, 则master会被标记为客观下线。
在一般情况下, 每个sentinel会以每 10 秒一次的频率向它已知的所有master,slave发送 INFO 命令。
当master被sentinel标记为客观下线时,sentinel向下线的master的所有slave发送 INFO 命令的频率会从 10 秒一次改为 1 秒一次
若没有足够数量的sentinel同意master已经下线,master的客观下线状态就会被移除
若master重新向sentinel的 PING 命令返回有效回复,master的主观下线状态就会被移除
当使用sentinel模式的时候,客户端就不要直接连接Redis,而是连接sentinel的ip和port,由sentinel来提供具体的可提供服务的Redis实现,这样当master节点挂掉以后,sentinel就会感知并将新的master节点提供给使用者
③集群模式:
sentinel模式基本可以满足一般生产的需求,具备高可用性。但是当数据量过大到一台服务器存放不下的情况时,主从模式或sentinel模式就不能满足需求了,这个时候需要对存储的数据进行分片,将数据存储到多个Redis实例中。cluster模式的出现就是为了解决单机Redis容量有限的问题,将Redis的数据根据一定的规则分配到多台机器。
cluster可以说是sentinel和主从模式的结合体,通过cluster可以实现主从和master重选功能,所以如果配置两个副本三个分片的话,就需要六个Redis实例。因为Redis的数据是根据一定规则分配到cluster的不同机器的,当数据量过大时,可以新增机器进行扩容。
使用集群,只需要将redis配置文件中的cluster-enable配置打开即可。每个集群中至少需要三个主数据库才能正常运行,新增节点非常方便
集群模式的选择
主从模式,当主节点挂掉后,redis将无法对外提供写服务,不能保证缓存的高可用。
Sentinel模式,保证了缓存的高可用,但是当数据量过大时,不支持扩展,适合缓存较少的系统。
Cluster模式,既满足了高可用性,对新增节点非常方便,推荐使用。
双写一致性
先更新数据库,再删除缓存 写的时间要大于读的时间 或者 延迟双删
6.缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级
(1)缓存雪崩:
①描述:
redis缓存中,数据大批量到达过期时间,而查询数据量巨大,导致数据库压力过大,甚至宕机。
和缓存击穿不同的是,缓存击穿是并发查询同一条数据,而缓存雪崩是不同数据都过期了,很多数据都差不到,从而去查询数据库。
②解决方案:
①缓存数据的过期时间设置为随机,防止出现同一时间,大量数据过期。
②如果缓存数据库是分布式部署,要把热点数据均匀分布在不同的缓存数据库中,即使个别机器宕机,仍然可以提供服务。
③设置热点数据永不过期。
④缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
(2)缓存穿透
①描述:
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大
②解决方案:
①接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
②从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
(3)缓存预热
a.提前给redis中嵌入部分数据,再提供服务
b.不可能将所有数据都写入redis,因为数据量太大了,第一耗费的时间太长了,第二redis根本就容纳不下所有的数据
c.需要更具当天的具体访问情况,试试统计出频率较高的热数据
d.然后将访问频率较高的热数据写入到redis,肯定是热数据也比较多,我们也得多个服务并行的读取数据去写,并行的分布式的缓存预热
e.然后将嵌入的热数据的redis对外提供服务,这样就不至于冷启动,直接让数据库奔溃了
(4)缓存更新
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
1)定时去清理过期的缓存;
2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡
(5)缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级
7.使用Redis缓存的合理性问题
①热点数据,缓存才有价值
对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。
对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。
②频繁修改的数据,看情况考虑使用缓存
数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。
对于上面两个例子,寿星列表、导航信息都存在一个特点,就是信息修改频率不高,读取通常非常高的场景。
那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。
③数据不一致性
一般会对缓存设置失效时间,一旦超过失效时间,就要从数据库重新加载,因此应用要容忍一定时间的数据不一致。还有一种是在数据更新时立即更新缓存,不过这也会更多系统开销和事务一致性问题。
④缓存更新机制
使用缓存过程中,我们经常会遇到缓存数据的不一致性和与脏读现象,我们有什么解决策略呢?
一般情况下,我们采取缓存双淘汰机制,在更新数据库的时候淘汰缓存。此外,设定超时时间,例如30分钟。极限场景下,即使有脏数据入cache,这个脏数据也最多存在三十分钟。
⑤缓存可用性
缓存是提高数据读取性能的,缓存数据丢失和缓存不可用不会影响应用程序的处理。因此,一般的操作手段是,如果Redis出现异常,我们手动捕获这个异常,记录日志,并且去数据库查询数据返回给用户。
⑥缓存服务降级
服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
⑦缓存预热
在新启动的缓存系统中,如果没有任何数据,在重建缓存数据过程中,系统的性能和数据库复制都不太好,那么最好的缓存系统启动时就把热点数据加载好,例如对于缓存信息,在启动缓存加载数据库中全部数据进行预热。一般情况下,我们会开通一个同步数据的接口,进行缓存预热。
⑧缓存穿透
如果因为不恰当的业务,或者恶意攻击持续地发请求某些不存在的数据,由于缓存没有保存该数据,所有的请求都会落到数据库上,会对数据库造成很大压力,甚至奔溃。一个简单的对策是将不存在的数据也缓存起来。8.你的Redis是如何与mysql数据进行同步的
六、Spring
Spring的两种依赖注入方式:
setter方法注入与构造方法注入
1.spring包括哪些模块
Spring框架由7个定义良好的模块(组件)组成,各个模块可以独立存在,也可以联合使用。
(1)Spring Core:核心容器提供了Spring的基本功能。核心容器的核心功能是用Ioc容器来管理类的依赖关系.Spring采用的模式是调用者不理会被调用者的实例的创建,由Spring容器负责被调用者实例的创建和维护,需要时注入给调用者。这是目前最优秀的解耦模式。
(2)Spring AOP:Spring的AOP模块提供了面向切面编程的支持。SpringAOP采用的是纯Java实现。Spring AOP采用基于代理的AOP实现方案,AOP代理由Ioc容器负责生成、管理,依赖关系也一并由Ioc容器管理,尽管如此,Spring Ioc容器并不依赖于AOP,这样我们可以自由选择是否使用AOP。
(3)Spring ORM:提供了与多个第三方持久层框架的良好整合。
(4)Spring DAO: Spring进一步简化DAO开发步骤,能以一致的方式使用数据库访问技术,用统一的方式调用事务管理,避免具体的实现侵入业务逻辑层的代码中。
(5**)Spring Context**:它是一个配置文件,为Spring提供上下文信息,提供了框架式的对象访问方法。Context为Spring提供了一些服务支持,如对国际化(i18n)、电子邮件、校验和调度功能。
(6)**Spring Web:**提供了基础的针对Web开发的集成特性,例如多方文件上传,利用Servlet listeners进行IoC容器初始化和针对Web的applicationContext.
(7) **Spring MVC:**提供了Web应用的MVC实现。Spring的MVC框架并不是仅仅提供一种传统的实现,它提供了一种清晰的分离模型,在领域模型代码和web form之间。并且,还可以借助Spring框架的其他特性
BeanFactory和ApplicationContext的区别
ApplicationContext继承了BeanFactory,BeanFactory是Spring中比较原始的Factory,它不支持AOP、Web等Spring插件,
而ApplicationContext不仅包含了BeanFactory的所有功能,还支持Spring的各种插件,还以一种面向框架的方式工作以及对上下文进行
分层和实现继承。
BeanFactory和FactoryBean有什么区别
相同点:都是用来创建Bean对象的
不同点:BeanFactory是提供了IOC容器最基本的形式,给具体的IOC容器的实现提供了规范,
使用BeanFactory创建对象的时候,必须严格遵循生命周期流程,太复杂了
如果想简单的自定义某个对象的创建,并且创建完成后,想把对象交给spring管理,那么就实现FactoryBean接口,更灵活一些
有三个方法。
isSIngleton getObject getObjectType
1、springBoot异步怎么实现
1、在application启动类中,加上@EnableAsync注解,Spring Boot 会自动扫描异步任务。
2、创建com.weiz.tasks包,在tasks包里增加AsyncTask 异步任务类,加上@Component 注解,然后在需要异步执行的方法前面加上@Async注解,这样Spring Boot容器扫描到相关异步方法之后,调用时就会将这些方法异步执行。
说明:@Async 加上这个注解,就表示该方法是异步执行方法。
1.Spring Bean 的生命周期
一、定义bean,bean的定义有三种方式实现
1)xml配置方式
2)通过 @Configuration Annoation注解方式声明对象
3)Properties文件方式等
二、读取bean的定义,要想成为SpringBean必须首先要实现一个接口BeanDefinitionReader,这个接口对Bean进行了统一的规范,这个接口主要是Bean的读取器,要从文件中把Bean的定义给读取进来,(它的实现有xml、注解等多种方式。)
三、读取到了Bean的定义信息后,需要实现BeanFactoryPostProcessor,也就是Bean的增强处理,这些增强处理可以对Bean里声明的属性信息进行添加、修改、删除等,经过增强处理后的Bean才可以被实例化
四、创建bean,在BeanFactory中Bean是通过反射方式创建出来的,(也就是首先获取Bean的信息,然后创建出对象),此时创建出来的Bean还不完整,不能直接使用,称为不完整Bean
五、对bean进行属性的初始化,也就是给属性赋值
六、在属性初始化后,执行BeanPostProcessorBefore,也就是Bean的前置增强处理
七、初始化Bean-init,属性初始化后的Bean,依然是不完整Bean
八、在Bean-init初始化后,需要执行BeanPostProcessorAfter,也就是Bean的后置增强处理
九、经过上面的五、六、七、八步后,Bean的信息才完整,才能够被使用
2.Spring IOC 如何实现
IOC也叫IOC容器,实际就是一个map<key,value>,里面存放的是各种对象
IOC控制反转,利用的是java反射机制来实现。
控制反转的意思就是,本来被调用者的实例是由调用者来创建的,但这样的缺点是耦合性太强,
而IOC则是统一交给spring来管理创建,将对象交给容器管理,你只需要在spring配置文件中配置相应的bean,以及设置相关的属性,让spring容器来生成类的实例对象以及管理对象。
在spring容器启动的时候,spring会把你在配置文件中配置的bean都初始化好,然后在你需要调用的时候,就把它已经初始化好的那些bean分配给你需要调用这些bean的类,也就是DI注入的过程,注解就是Autowired以及resource。
注解:service,controller,component,repository,bean
3.说说Spring AOP
Aspect-Oriented Programming, 一般称为面向切面编程,是OOP,也就是面向对象编程的一种补充。
就是把那些与业务无关,但是对多个对象产生影响的公共代码,抽取并封装成一个可重用的模块,这个模块叫切面(Aspect),这样做可以减少项目中的重复代码,降低了模块之间的耦合,同时提高了可维护性,这就是springAOP
可用于日志,权限认证,事务处理方面,可以对某些方法进行增强处理
4.Spring Bean的作用域
作用域 描述
①singleton 在spring IoC容器仅存在一个Bean实例,Bean以单例方式存在,bean作用域范围的默认值。
②prototype 每次从容器中调用Bean时,都返回一个新的实例,即每次调用getBean()时,相当于执行newXxxBean()。
③request 每次HTTP请求都会创建一个新的Bean,该作用域仅适用于web的Spring WebApplicationContext环境。
④session 同一个HTTPSession共享一个Bean,不同Session使用不同的Bean。该作用域仅适用于web的Spring WebApplicationContext环境。
⑤application(global-session)
限定一个Bean的作用域为ServletContext的生命周期。该作用域仅适用于web的Spring WebApplicationContext环境
5.动态代理(CGLIB 与 JDK)
①JDK动态代理:
JDK是基于反射机制,生成一个实现代理接口的匿名类,然后重写方法,实现方法的增强.
它生成类的速度很快,但是运行时因为是基于反射,调用后续的类操作会很慢.
而且他是只能针对接口编程的。
②CGLIB动态代理:
CGLIB是基于继承机制,继承被代理类,所以方法不要声明为final,然后重写父类方法达到增强了类的作用.
(它底层是基于asm第三方框架,是对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理)
生成类的速度慢,但是后续执行类的操作时候很快。
可以针对类和接口.
③两者对比:
JDK动态代理是面向接口的。
CGLib动态代理是通过字节码底层继承要代理类来实现(如果被代理类被final关键字所修饰,那么会失败)。
④使用注意:
如果要被代理的对象是个实现类,那么Spring会使用JDK动态代理来完成操作(Spirng默认采用JDK动态代理实现机制);
如果要被代理的对象不是个实现类那么,Spring会强制使用CGLib来实现动态代理。
*Spring AOP执行过程
1.Spring 创建IOC容器
Spring 扫描包中的所有由@Service 和@Component修饰的类,创建对象,放在Spring IOC容器中。
2.寻找切面类
创建完对象后,开始找@Aspect 修饰的切面类并获取切面类中的所有方法。
3.寻找切面类的方法中带有表达式的部分
接下来,Spring找到所有有表达式修饰的方法
4.查找有相应方法的类
随后,Spring检查它所扫描到的所有类,并将上一步中找到的方法与所有类进行对照,找出有这个方法的类,这个类就是被代理类。
5.创建动态对象
最后,Spring根据上一步找到的被代理类以及切面类创建动态类的动态对象并放入Spring IOC容器中。
一条sql语句查询设置默认时间是10秒,如果超过10秒就会写进慢查询日志slow-log-file,
6.Spring 事务实现方式
用户的每次请求都对应了一个业务逻辑方法。
而一个业务逻辑方法经常有一系列对数据库的访问操作,这些访问操作应该绑定成一个事务执行
Spring事务管理提供了两种方式:
①编程式事务:程序员自己手动编码实现,通过Transaction Template管理,类似于JDBC的事务管理,实际很少使用
②声明式事务:建立在AOP的基础上,本质是对方法前后进行拦截,在方法开始前加一个事务,方法执行后,根据结果判断提交或者回滚,只需要在配置文件中做相关的事务声明规则配上注解就可以使用
7.如何自定义注解实现功能
①@Target注解,是专门用来限定某个自定义注解能够被应用在哪些Java元素上面的
②@Retention注解,翻译为持久力、保持力。即用来修饰自定义注解的生命力。
注解的生命周期有三个阶段:
1、Java源文件阶段;2、编译到class文件阶段;
3、运行期阶段。同样使用了RetentionPolicy枚举类型定义了三个阶段:
③@Documented注解,是被用来指定自定义注解是否能随着被定义的java文件生成到JavaDoc文档当中。
④@Inherited注解,是指定某个自定义注解如果写在了父类的声明部分,那么子类的声明部分也能自动拥有该注解。@Inherited注解只对那些@Target被定义为ElementType.TYPE的自定义注解起作用。
8.Spring MVC 是线程安全的吗?如果不是,怎么解决方案
首先对于spring的IOC来说,对象是由Spring来帮我们管理,也就是在Spring启动的时候,在Spring容器中,由Spring给我们创建的,Spring会帮我们维护,一般都是单例的,也就是一个对象。
SpringMVC的Controller默认是单例的,对于同一个Controller而言,在整个web生命周期内只有一个对象。如果在Controller里写了一个成员变量,这个变量是对所有线程可见的。
是会出现线程安全问题;
有几种解决方法:
1、在Controller中使用ThreadLocal变量
2、在spring配置文件Controller中声明 scope=“prototype”,每次都创建新的controller,所以在使用spring开发web 时要注意,默认Controller、Dao、Service都是单例的。
(备注:此时虽然可能避免上述的线程安全问题,但是jvm必须为每次请求创建新的对象,在请求完成之后销毁对象,这必然会增加系统的性能开销。)
9.Spring MVC 启动执行流程
①用户发送请求到前端控制器—DispatcherServlet
②DispatcherServlet收到请求并调用处理器映射器—HandlerMapping
③HandlerMapping根据XML配置、注解查找与request请求相对应的处理器方法—Handler,生成处理器对象和处理器拦截器,一并返回给DispatcherServlet
④DispatcherServlet调用处理器适配器—HandlerAdapter,HandlerAdapter经过适配,调用具体的后端控制器—Controller
⑤Controller执行完,返回一个ModelAndView
⑥HandlerAdapter把ModelAndView返回给DispatcherServlet
⑦DispatcherServlet把ModelAndView传给ViewReslover视图解析器,解析后返回具体的view
⑧DispatcherServlet根据View进行渲染视图,响应给客户
10.Spring 框架中用到了哪些设计模式
①工厂设计模式(BeanFactory和ApplicationContext创建Bean对象)
②单例设计模式(Bean默认都是单例,日志对象)
③代理设计模式(AOP JDK动态代理 CGlib动态代理 )
④模板方法,解决重复代码(postProcessBeanFactory,jdbc Tempelete)
⑤观察者模式 (对象和对象之间有依赖关系,一个对象发生改变,这个对象所依赖的对象也会做出反应 比如事件驱动模型,监听)
⑥适配器模式(把一个接口转换成客户希望的另一个接口,使接口不兼容的类可以一起工作 比如AOP通知,SpringMVC的DispatcherServlet调用HandlerMapping解析请求对应的Handler,Controller是要适配的类)
⑦装饰者模式
11.设计模式
①创建型模式:5种。工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
②结构型模式:7种。适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式
③行为型模式:11种。策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式
还有两类:并发型模式和线程池模式
12.spring、springMVC、springBoot、springCloud区别
①spring
spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器。
②springMVC
springMVC是web项目开发的一种很常见的模式,C(控制器)将V(视图、用户客户端)与M(模块,业务)分开构成了MVC 。
③springBoot
Spring Boot就是把以前的手动配置的过程自动化封装了,提供默认的配置,说白了就是规范大于配置,一切自动完成。
采用 Spring Boot可以大大的简化你的开发模式,能快速开发单个微服务。
④springCloud
SpringCloud是一个基于 Spring Boot实现的云应用开发工具,spring boot做较少的配置,就可成为 spring cloud 中的一个微服务;
Spring Boot专注于快速、方便集成的单个微服务个体,Spring Cloud关注全局的服务治理框架,依赖于springboot开发。
13.SpringBoot启动原理
1、@ComponentScan
@ComponentScan的功能其实就是自动扫描并加载符合条件的组件(比如@Component和@Repository等)或者bean定义,最终将这些bean定义加载到IoC容器中。
我们可以通过basePackages等属性来细粒度的定制@ComponentScan自动扫描的范围,如果不指定,则默认Spring框架实现会从声明@ComponentScan所在类的package进行扫描。
2.@Configuration:
@Configuration注解用来定义配置类,可以用来替代xml配置文件。
这里的启动类标注了Configuration之后本身就是一个IOC容器的配置类。
其搭档注解的@Bean标注的方法,返回值都会作为一个Bean定义,注册到Spring的IOC容器中,方法名默认作为bean的ID
3.@EnableAutoConfiguration
借助@Import的帮助,将所有符合自动配置条件的bean定义加载到IoC容器,根据类路径中的jar依赖,为项目进行自动配置
14.SpringBoot——starter
SpringBoot拥有很多方便使用的starter,比如spring-boot-starter-log4j、mybatis-spring-boot-starter.jar等,各自都代表了一个相对完整的功能模块。SpringBoot-starter是一个集成接合器,完成两件事:
①引入模块所需的相关jar包
②自动配置各自模块所需的属性
15.SpringCloud五大组件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EN33HFU4-1639059913926)(C:\Users\Administrator\Desktop\cz\image-20210928095046931.png)]
1、注册中心,eureka,因为是有很多个服务,所以说第一个要把这些服务注册到注册中心去,注册进去之后,以后要调用这些服务,要先从这里去取。
2、同一个服务要注册多个,Ribbon,做一个负载均衡的作用,比如说我从注册中心拉取了一堆服务列表,调用一个服务,他可能有三个,五个,那么我要使用哪个,这个时候我需要一个负载均衡策略来选择使用哪一个,这个操作是交给Robbie实现的
3、除了正常的调用,有可能我调用一个服务,但是这个服务宕掉了,这个时候为了保证服务还能继续进行,要用到hystrix,他主要功能 :熔断,服务降级,线程隔离,缓存(在一个请求内 用到时候少)
4、众多的服务有一个配置,但这些配置如果放在各个项目里,很难去维护和管理,所以有一个统一的配置中心config。
5、最后一个就是网关,我们有这么多的微服务,最终都是要暴露给前端调用的,而前端如果是一个服务就有一个地址,非常不好管理,这个时候网关可以解决这个问题,统一地址,还可以做限流或者权限认证(shrio安全框架)。
16.@Autowired和@Resource
@Resource默认按照ByName自动注入,@Resource有两个重要的属性:name和type,而Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以,如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不制定name也不制定type属性,这时将通过反射机制使用byName自动注入策略。
@Autowired注解是按照类型(byType)装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值,可以设置它的required属性为false。如果我们想使用按照名称(byName)来装配,可以结合@Qualifier注解一起使用。
17.源码角度分析SpringMVC
tomcat启动的时候,会通知spring初始化容器,然后springMVC会遍历变量容器中的bean,
在IOC容器初始化的时候,会建立所有URL和Controller的对应关系,保存到Map<url,controller>
这样就可以根据request,快速定位到controller,因为最终处理request的是controller里的方法,
而Map中只有url和controller的对应关系,所以要根据request的url进一步确认controller中的method,
然后通过反射获取该方法上的注解和参数,再通过反射调用这个方法,获取一个ModelAndView结果视图,
但在此之前,要进行参数绑定,把request中的参数绑定到方法的形参上,springMVC提供了两种绑定的方法:
①通过@RequestParam注解进行绑定,方法参数前声明就可以了
②通过参数名称进行绑定
前提是必须获取方法中参数的名称,Java反射只能获取参数的类型,不能获取方法名,springMVC是通过asm框架来读取字节码文件,从而获取参数的名称
更推荐使用注解参数绑定。
18.Spring循环依赖
@Autowired注解已经解决了这个问题
一级缓存是单例池(SingletonObjects)
二级缓存是早期曝光对象(EarlySingletonObjects)
三级缓存是早期曝光对象工厂(SingletonFactories)
当A和B两个类发生循环依赖引用时,
在A完成实例化后,就用实例化后的对象去创建一个对象工厂,并且添加到三级缓存中。
(如果A被AOP代理,那么通过这个工厂,获取到的就是A代理后的对象。
如果A没被AOP代理,那么就是实例化的对象。)
当A进行属性注入的时候,就会创建B,而B又依赖了A,所以创建B的同时又会去调用getBean(a)来获取需要的依赖,
此时的getBean(a)会从缓存中获取,
第一步先获取三级缓存中的工厂,第二步调用对象工厂的getObject方法获取对应的对象(A),得到这个对象之后,注入到B。
B完成它的生命周期,创建完成再把B注入到A里,A再完成它的生命周期。
七、分布式
1.什么是集群
2.什么是分布式
3.nginx负载均衡规则
Token鉴权:
客户端用账号和密码请求登录
服务端收到请求,去验证账号、密码
验证成功后,服务端会发一个 Token,再把这个 Token 发送给客户端
客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里
客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
总的来说就是客户端在首次登陆以后,服务端再次接收http请求的时候,就只认token了,请求只要每次把token带上就行了,服务器端会
拦截所有的请求,然后校验token的合法性,合法就放行,不合法就返回401(鉴权失败)。
hystrix熔断
原理:在调用指定服务时,如果说这个服务的失败率达到你输入的一个阈值,将断路器从closed状态,转变为open状态,
指定服务时无法被访问的,如果你访问就直接走fallback降级方法,
在一定的时间内,open状态会再次转变为half open状态,允许一个请求发送到我的指定服务,如果成功,断路器转变为closed,
如果失败,服务再次转变为open状态,会再次循环到half open,直到断路器回到一个closed状态。
①hystrix可以给feign及resttemplate进行服务降级
②hystrix在controller进行降级
hystrix线程隔离
为了充分让消费者服务器的tomcat有充分的线程去调用其他服务
实现方式:
1、hystrix线程池,在服务器内通过请求的转移,解放tomcat
2、信号量:对消费者服务线请求某个服务的并发线程数量做限制
@GetMapping("/find")
@HystrixCommand(fallbackMethod = "findGoodsFallBack",commandProperties = {@HystrixProperty(name = "execution.isolation.strategy",value = "THREAD"),@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000"),@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),//开启断路器@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "2"),//请求失败总数的阈值@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),//10秒内,请求失败的百分比@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "5000")//有打开状态到半开状态的时间
})
public List<GoodsType> find(){String url="http://GOODSTYPE-PROVIDER/goodstype/find";List<GoodsType> list=restTemplate.getForObject(url,List.class);return list;
}
token防止表单重复提交
后端创建token,前端页面一加载就获取后端这个token,
要能够防止表单重复提交,就要标识用户的每一次访问请求,使得每一次访问对服务端来说都是唯一确定的。为了标识用户的每次访问请求,
可以在用户请求一个表单域时增加一个隐藏表单项,这个表单项的值每次都是唯一的token。
当用户在请求时生成这个唯一的token时,同时将这个token保存在用户的Session中,
等用户提交请求时检查这个token和当前的Session中保存的token是否一致。
如果一致,说明没有重复提交,否则用户提交上来的token已经不是当前的这个请求的合法token。
4.session 分布式处理
session一般存在服务器中,当分布式分开处理业务模块时,session在每个服务器上时非共享的,这时我们可以用第三方应用来作为一个存储需要共享session的容器,比如redis。
实现原理也就是当我们存进这个session的时候将需要的数据存进redis,在redis存数据的时候需要一个key,也就是我们自定义的标识,value也就是需要存入的信息,因为是session,再设置一个有效期时间,一般都赋予30分钟这是默认时间,
当存进去数据后,将key存进cookie中(当cookie被禁用就重写url)
这时不管用户去哪一个模块去访问的时候,后端都可以去获取cookie里的key来判断是否有过session,如果没有则定位到登录页面,
如果有就刷新时间,也就是重新赋予redis数据,至于为什么要重新赋予数据,也就是为了保证用户的体验,不能说让用户只有30分钟的
访问这个总体应用的时间,这样用户体验不好,同时又解决了我们的session共享问题。
5.选择合适的分布式主键方案
mq和kafka
6.选择合适分布式事务解决方案
①两段提交
第一阶段:准备阶段,每个参与者都需要开启事务,执行SQL,但不提交,进入准备状态,并且通知TransactionManager准备OK
第二阶段:当TransactionManager收到了所有参与者的通知之后,如果都通过,向所有的参与者发送commit请求,如果有任意一个失败,则发送rollback命令,进行回滚
(问题,性能低,TransactionManager没有超时时间,存在单点故障问题)
②三段提交
在两段提交的基础上,引入了超时时间机制,并且多出一个步骤,在提交事务之前,再询问一下,数据库的日志信息是否已经完善
③TCC机制(补偿事务)
每一个操作,都要注册一个与之对应的确认和补偿(撤销)操作
Try:尝试去预执行具体的业务代码,比如下订单
如果try都成功了:confirm:再次执行confirm的代码
但有一个try失败了:cancel:再次执行cancel的代码
④MQ分布式事务
A发送prepare消息到消息中间件MQ中的QA位,B要从MQ中的QB位取消息,发送成功后,A执行本地事务,如果事务执行成功,就commit,消息中间件把消息下发到消费端,也就是从QA位转移到QB位,如果执行失败,就回滚,消息中间件把这条prepare消息删除,B接收到消息,进行消费,如果消费失败就不断重试‘
发送消息有confirm机制保证发送到MQ中,手动ack机制保证消费了MQ中的消息
7.选择合适分布式锁解决方案
在实现的时候要注意的几个关键点:
1、锁信息必须是会过期超时的,不能让一个线程长期占有一个锁而导致死锁;
2、同一时刻只能有一个线程获取到锁。
3、redis本身就是单线程的
redis 分布式锁原理
过程分析:
A尝试去获取锁lockkey,通过setnx(lockkey,currenttime+timeout)命令,对lockkey进行setnx,将value值设置为当前时间+锁超时时间;
如果返回值为1,说明redis服务器中还没有lockkey,也就是没有其他用户拥有这个锁,A就能获取锁成功;
在进行相关业务执行之前,先执行expire(lockkey),对lockkey设置有效期,防止死锁。
因为如果不设置有效期的话,lockkey将一直存在于redis中,其他用户尝试获取锁时,执行到setnx(lockkey,currenttime+timeout)时,将不能成功获取到该锁;
执行相关业务;释放锁,A完成相关业务之后,要释放拥有的锁,也就是删除redis中该锁的内容,del(lockkey),接下来的用户才能进行重新设置锁新值。
8.聊高可用
9.聊聊高并发
首先这个服务器得建集群,避免宕机
然后是数据库方面,得使用redis来缓存热点数据,因为他是内存处理的,所以很适合这种小数据的高性能处理。
再一个比如说,下订单做这个写操作,使用这个MQ消息队列中间件来处理,避免直接操作数据库,把数据库请求死。
还有hystrix线程池做线程隔离,以及熔断、降级服务,
10.聊聊高性能
八、微服务
Feign调用过程
①在启动类上添加注解@EnableFeignClients
②创建Feign的接口
@FeignClient(value = “GOODSTYPE-PROVIDER”,fallback = GoodsTypeFeignFallBack.class) ,字符串就是服务提供者的名字
@RequestMapping(value = “/goodstype/find”, method = RequestMethod.GET)
List findGoods();
③在Controller中注入接口Feign,通过Feign对象调用服务提供者
(降级就再写一个Feign接口的实现类,加@Component注解,在实现类中重写接口中的方法,这个方法就是降级方法)
前后端分离如何实现
如何解决跨域
微服务哪些框架
RPC技术是什么
说说RPC的实现原理
说说DUBBO实现原理
什么是RestFul
如何设计一个良好的API
如何理解RestFul API的幂等性
描述CAP定理、BASE理论
①Consistency
这里的一致性是强一致性,强一致性的意思就是例如节点A更新了数据,节点B能同时更新,这样客户端在每次读取获得数据都是最近更新的。
②Availability
可用性指的是非故障的节点需要在合理的时间返回合理的响应。
合理的响应的意思也就是不能搞个报错,不能是超时失败。
举个例子比如说节点A更新了数据,同时要发布到节点B上,但是中间传输的电缆被挖掘机挖断了,此时用户去访问节点B,此时节点B应该返回老的数据,而不应该报错。这就是可用性。让用户感觉系统还是能用的。
③Partition tolerance
分区容错性,指的是当网络分区了,系统还能正常的运行和响应。比如节点A和节点B无法通信,你要考虑这个时候系统如何应该。虽然网络分区的概率低而且时间短但是这种情况是会发生的。所以理论上是牺牲C或者A,P是一定要达到的。
CAP所说的CAP三者只能存在两者,所以CA是可以能搭配的。就是在系统没有P的时候,CA搭配。也就是说当系统不存在分区情况的时候要满足C和A,当系统出现分区情况的之后视情况抛弃C或者A。
怎么考虑数据一致性
数据一致性的最终实现方案
微服务和SOA的区别
如何对服务进行拆分
微服务如何管理数据库
如何应对微服务链式调用异常
微服务的快速追踪与定位
微服务的安全
九、消息队列 MQ
1.消息队列是什么
消息队列(Message Queue)是一种应用间的通信方式,消息发送后可以立即返回,有消息系统来确保信息的可靠专递。
消息发布者只管把消息发布到MQ中而不管谁来取,
消息使用者只管从MQ中取消息而不管谁发布的,这样发布者和使用者都不用知道对方的存在。
2.为什么使用消息队列
优点
①**异步处理-**相比传统的串并行方式,提高了项目效率
②应用解耦-系统间通过消息通信,不关心其他系统的处理
③流量削锋-可以通过消息队列长度控制请求量;可以缓解短时间内的高并发请求
④日志处理-解决大量日志传输
解耦:A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?那如果 C 系统现在不需要了呢?A 系统负责人几乎崩溃…A 系统跟其它各种乱七八糟的系统严重耦合,A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。如果使用 MQ,A 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况。
就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦。
异步:A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求。如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms。
削峰:减少高峰时期对服务器压力。
缺点
可用性降低。
本来系统正常运行,加入消息队列之后,消息队列出现异常会影响系统正常运行,复杂度提高。
加入消息队列后,需要考虑:一致性问题,如何保证消息不被重复消费,如何保证消息准确传输,这样复杂性增大。
3.消息队列有哪些应用场景
Active Rabbit Rocket Kafka
rabbit底层是erlang
Rocket是阿里的底层是Java,可以二次开发和改造
大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的
4.消息的重发补偿解决方案
5.消息的幂等性解决方案
对于每条消息,MQ内部生成一个全局唯一、与业务无关的消息ID。
当MQ消费者接收到消息时,先根据ID判断消息是否重复发送,再决定是否将消息落地到数据库中。
这样,有了这个ID作为去重的依据就能保证一条消息只能一次落地到数据库。
6.消息的堆积解决方案
①生产者
给消息设置年龄,超时就丢弃
考虑使用队列最大长度限制
减少发布频率
②消费者
增加消费者的处理能力,优化代码;使用JDK的队列缓存数据,多线程去处理(一般考虑顺序问题,采用单例线程)
建立新的queue,消费者同时订阅新旧queue,采用订阅模式
默认情况下,rabbitmq消费者为单线程串行消费
设置并发消费两个关键属性
concurrentConsumers和prefetchCount
concurrentConsumers:设置的是对每个listener在初始化的时候设置的并发消费者的个数
prefetchCount:每次从broker里面取的待消费的消息的个数
7.如何实现消息队列
8.如何保证消息的有序性
9.如何保证MQ的消息准确性
消息的可靠性问题:
1、生产者向交换机写入消息要可靠confirm
2、交换机向消息队列写入消息要可靠return
3、消费者从队列取出消息后,成功消费要可靠手动ack
用 MQ 有个基本原则,就是数据不能多一条,也不能少一条
开启 confirm 模式,在生产者那里设置开启 confirm 模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个 ack 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个 nack 接口,告诉你这个消息接收失败,你可以重试
开启 RabbitMQ 的持久化,就是消息写入之后会持久化到磁盘,哪怕是 RabbitMQ 自己挂了,恢复之后会自动读取之前存储的数据
创建 queue 的时候将其设置为持久化
这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。
发送消息的时候将消息的 deliveryMode 设置为 2
必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据
RabbitMQ 如果丢失了数据,主要是因为你消费的时候,刚消费到,还没处理,结果进程挂了,RabbitMQ 认为你都消费了,这数据就丢了
这个时候得用 RabbitMQ 提供的 ack 机制,简单来说,就是你必须关闭 RabbitMQ 的自动 ack ,
transChannel.basicAck(envelope.getDeliveryTag(),false);
channel.basicConsume(“java2102”, false, defaultConsumer)
可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 ack 一把
安全方面
安全要素与STRIDE威胁
防范常见的WEB攻击
服务端通信安全攻防
HTTPS原理剖析
HTTPS降级攻击
SSL授权与认证
需求分析
你如何对需求原型进行理解和拆分
说说你对功能性需求的理解
说说你对非功能性需求的理解
你针对产品提出哪些交互和改进意见
你如何理解用户痛点
性能
性能指标有哪些
如何发现性能瓶颈
性能调优的常见手段
业务工程
你系统中的前后端分离是如何做的
说说你的开发流程
你和团队是如何沟通的
你如何进行代码评审
说说你对技术与业务的理解
说说你在项目中经常遇到的 Exception
说说你在项目中遇到感觉最难Bug,怎么解决的
说说你在项目中遇到印象最深困难,怎么解决的
你觉得你们项目还有哪些不足的地方
你是否遇到过 CPU 100%,如何排查与解决
你是否遇到过内存 OOM,如何排查与解决
说说你对敏捷开发的实践
说说你对开发运维的实践
Nacos和Eureka的区别
nacos和eureka都是注册中心,都具有各自的负载均衡策略.
eureka分为Eureka Server(Eureka服务)和Eureka Client(Eureka客户端),所有Eureka Server 通过Replicate进行数据同步。
无论Eureka Client向哪个Eureka Server中注册信息,最终所有Eureka Server中都会存储注册的信息,这些信息都缓存到Eureka Server的本地。
Eureka Client向Eureka Server注册信息的时候我们称它为服务提供者,当获取注册的信息时称为服务消费者,所以很多Eureka Client既是服务提供者,又是服务消费者。
服务提供者在启动后,每隔30秒向Eureka Server发送一次心跳,以证明自己的可用。当Eureka Server超过90秒没有收到提供者的心跳后,会认为这个提供者已经宕机,销毁实例。
Nacos有自己的配置中心,Eureka需要配合config实现配置中心,且不提供管理界面,nacos是动态刷新的,它采用Netty保持长连接实时推送,eureka需要配合MQ实现配置动态刷新
阿里的nacos : 性能最好
他同时支持AP和CP模式,他根据服务注册选择临时和永久来决定走AP模式还是CP模式
eureka: 可以做注册中心,完全AP,支持注册中心之间的节点复制,同时支持服务端同时注册多个注册中心节点,所以不存节点信息不一致的情况
list存对象,不用遍历去重
①用set无序的特性,建一个hashset集合,用set.addAll()把list集合放入,再放回到新建的list集合。
②java8新特性里stream流有个distinct方法。
JAVA中的IO体系
字节流和字符流。
Stream结尾都是字节流,reader和writer结尾的都是字符流。
两者的区别就是读写的时候,一个按字节读写,一个是按字符,实际使用通常差不多。
使用:
在读写文件需要对内容按行处理,比如比较特定字符,处理某一行数据的时候,一般使用字符流。
只读写文件,和文件内容无关,一般选择字节流
NIO和IO到底有什么区别?有什么关系?
核心区别:
①NIO是以块的方式处理数据,但是IO是以最基础的字节流的形式去写入和读出的。所以在效率上的话,肯定是NIO效率比IO效率会高出很多。
②NIO不在是和IO一样用OutputStream和InputStream 输入流的形式来进行处理数据的,但是又是基于这种流的形式,而是采用了通道和缓冲区的形式来进行处理数据的。
③还有一点就是NIO的通道是可以双向的,但是IO中的流只能是单向的。
④还有就是NIO的缓冲区(其实也就是一个字节数组)还可以进行分片,可以建立只读缓冲区、直接缓冲区和间接缓冲区,只读缓冲区很明显就是字面意思,直接缓冲区是为加快 I/O 速度,而以一种特殊的方式分配其内存的缓冲区。
⑤补充一点:NIO比传统的BIO核心区别就是,NIO采用的是多路复用的IO模型,普通的IO用的是阻塞的IO模型,两个之间的效率肯定是多路复用效率更高。
@transactional注解
**@Transactional **:(propagation=Propagation.NOT_SUPPORTED,readOnly=true)
readOnly:该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。例如:@Transactional(readOnly=true)
①propagation 属性
事物传播行为介绍:
@Transactional(propagation=Propagation.REQUIRED) :如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)
@Transactional(propagation=Propagation.NOT_SUPPORTED) :容器不为这个方法开启事务
@Transactional(propagation=Propagation.REQUIRES_NEW) :不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务
@Transactional(propagation=Propagation.MANDATORY) :必须在一个已有的事务中执行,否则抛出异常
@Transactional(propagation=Propagation.NEVER) :必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)
@Transactional(propagation=Propagation.SUPPORTS) :如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.
②Isolation属性
@Transactional(isolation = Isolation.READ_UNCOMMITTED):读取未提交数据(会出现脏读, 不可重复读) 基本不使用
@Transactional(isolation = Isolation.READ_COMMITTED):读取已提交数据(会出现不可重复读和幻读)
@Transactional(isolation = Isolation.REPEATABLE_READ):可重复读(会出现幻读)
@Transactional(isolation = Isolation.SERIALIZABLE):串行化
MYSQL: 默认为REPEATABLE_READ级别
SQLSERVER: 默认为READ_COMMITTED
什么是脏读?幻读?不可重复读?
-
脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
-
不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
-
幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。
Filter过滤器怎么实现的?
Filter接口中有一个doFilter方法,当我们编写好Filter,并配置对哪个web资源进行拦截后,WEB服务器每次在调用web资源的service方法之前,都会先调用一下filter的doFilter方法,因此,在该方法内编写代码可达到如下目的:
① 调用目标资源之前,让一段代码执行。
② 是否调用目标资源(即是否让用户访问web资源)。
③ 调用目标资源之后,让一段代码执行。
web服务器在调用doFilter方法时,会传递一个filterChain对象进来,filterChain对象是filter接口中最重要的一个对象,它也提供了一个
doFilter方法,开发人员可以根据需求决定是否调用此方法,调用该方法,则web服务器就会调用web资源的service方法,即web资源就会被访问,否则web资源不会被访问。
v-show和v-if的区别(VUE)
v-show是display和block切换
v-if是删除和创建标签
重复切换的情况下,show效率更高
dubbo和zookeeper
服务分两个角色:(springcloud不分角色)
1、服务提供者 只能被别的服务调用
2、服务消费者 只能调用别的服务
3、RPC调用方式 (springcloud是Http)
4、注册中心用DUBBO+zooleeper (springcloud是eureka或nacos)
zooleeper 注册中心、服务消费者、服务提供者、服务监控中心
消费者向注册中心拉取服务列表,注册中心有个负载均衡算法,会给出建议,调用哪个服务提供者
服务监控中心会记录消费者和提供者的调用时间
没有配置、熔断、降级
也是增量更新
${}#{}区别
redis 缓存雪崩等缓存问题
sql优化与注入
spring cloud 组件
分页查询怎么实现
Restful编码风格
1、提交方法语义化
查询:get
添加:post
修改:put
删除:delete
2、建议使用路径传参
Mybatis
1.模糊查询like语句应该怎么写?
在java代码中增加sql通配符
如果在sql语句中拼接通配符,则会引起sql注入
2.MyBatis框架的使用场合
MyBatis专注于SQL本身,是一个灵活的DAO层解决方案,
适合对性能要求比较高,或者需求变化较多的项目
3.MyBatis是如何进行分页的?分页插件的原理是什么?
MyBatis使用RowBounds对象进行分页,它是针对ResultSet的结果集执行的内存分页,而不是物理分页,也可以直接编写SQL实现分页或者使用分页插件来分页。RowBounds在方法参数添加 RowBounds(int offset, int limit)的对象即可,不需要添加limit
分页插件的原理是实现MyBatis提供的接口来实现自定义插件,然后再插件的拦截方法内拦截待执行的SQL,然后重写SQL
举例:select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10
4. Dao 接口里的方法,参数不同时,方法能重载吗?
Mapper接口里的方法不可以重载,因为使用的是全限名+方法名来保存和查找的策略,Mapper接口的工作原理是JDK动态代理,MyBatis运行时会使用JDK动态代理为Mapper接口生成代理对象proxy,代理对象会拦截接口方法,转而执行MapperStatement所代表的sql,然后把执行的结果返回
5. MyBatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?
使用resultMap标签,定义数据库列名和对象属性名之间的映射关系。
使用sql列的别名功能,把列的别名写成对象属性名。
有了列名和属性名的映射关系后,MyBatis通过反射,创建对象,同时使用反射给对象的属性逐一赋值并返回
6. Mybaits 的优缺点?
优点:
①基于SQL语句编程,非常灵活,不会对应用程序或者数据库的现有设计造成影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;
②提供XML标签,支持编写动态SQL语句,并可重用。
③与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
④很好的与各种数据库兼容,能够与 Spring 很好的集成;
⑤提供映射标签,支持对象与数据库的ORM字段关系映射;
提供对象关系映射标签,支持对象关系组件维护。
缺点:
①SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
②SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。7. 谈谈你对MyBatis的理解?
7. 谈谈你对MyBatis的理解?
MyBatis是一个可以自定义SQL、存储过程和高级映射的持久层框架,内部封装了JDBC,开发时只需要关注SQL语句本身。
程序员直接编写原生态SQL,可以严格控制sql执行性能,灵活度高。
8. MyBatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复?
不同的Xml映射文件,如果配置了namespace,id可以重复。
如果没有配置namespace,id不能重复。
原因是namespace+id是作为Map<String,MapperStatement>的key使用的,如果没有namespace,id重复会导致数据互相覆盖。有了namespace,id就可以重复,namespace不同,namespace+id自然也就不同。
9.#{}和${}的区别是什么?
#是预编译处理, Mybatis在处理#̲时,会将sql中的#{}替换为…时,就是把$替换成变量的值。
使用#可以有效的防止SQL注入,提高系统安全性
10.当实体类中的属性名和表中的字段名不一样 ,怎么办?
通过在查询的sql语句中定义字段名的别名。
通过resultMap标签来映射字段名和实体类属性名的一一对应的关系。
11.Mybatis映射文件中,如果A标签通过include引用了B标签的内容,请问B标签能否定义在A标签的后面,还是说必须定义在A标签的前面?
Mybatis解析Xml映射文件是按照顺序解析的,但是被引用的B标签定义在任何地方Mybatis都可以正确识别。
原理是Mybatis解析A标签,发现A标签引用了B标签,但是B标签尚未解析到,此时会将A标签标记为未解析状态,然后继续解析余下的标签,包含B标签。
待所有标签解析完毕,Mybatis会重新解析那些被标记为未解析的标签,此时再解析A标签时,B标签已经存在,A标签也就可以正常解析完成了。
12.MyBats中的 一对一、一对多的关联查询 ?
定义一个resultMap,使用association属性实现一对一的关联查询;使用collection属性实现一对多的关联查询。
13.MyBatis实现一对一有几种方式?具体怎么操作的?
①联合查询:是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成;
②嵌套查询:先查一个表,根据这个表里面的结果的外键id,去再另外一个表里面查询数据,也是通过association配置,但另外一个表的 查询通过select属性配置。
14.MyBatis 的好处是什么?
①把sql语句从 Java 源程序中独立出来,放在单独的XML文件中编写,给程序的维护带来了很大便利。
②能自动将结果集转换成Java Bean对象,大大简化了Java数据库编程的重复工作。
③程序员可以结合数据库自身的特点灵活控制sql语句,能够完成复杂查询。
15.讲一下MyBatis的一级、二级缓存(说说MyBatis的缓存)?
MyBatis的缓存分为一级缓存和二级缓存。
一级缓存放在session里面,默认就有。
二级缓存是mapper级别的缓存,存放在它的命名空间里,默认是不打开的,使用二级缓存属性类需要实现Serializable序列化接口,可在它的映射文件中配置cache标签
16.Mapper 编写有哪几种方式?
①接口实现类继承 SqlSessionDaoSupport。
需要编写mapper 接口,mapper 接口实现类、mapper.xml 文件。
1)在 sqlMapConfig.xml 中配置 mapper.xml 的位置
2)定义 mapper 接口
3)实现类集成 SqlSessionDaoSupportmapper 方法中可以 this.getSqlSession()进行数据增删改查。
4)spring 配置
②使用 org.mybatis.spring.mapper.MapperFactoryBean,
1)在 sqlMapConfig.xml 中配置 mapper.xml 的位置
2)定义 mapper 接口:
A.mapper.xml 中的 namespace 为 mapper 接口的地址
B.mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致
C.Spring 中定义
③使用mapper扫描器:
1)mapper.xml 文件编写:
2)定义 mapper 接口,注意 mapper.xml 的文件名和 mapper 的接口名称保持一致,且放在同一个目录
3)配置 mapper 扫描器:
使用扫描器后从 spring 容器中获取 mapper 的实现对象。
17.MyBatis在mapper中如何传递多个参数?
直接在方法中传递参数,xml文件用#符号来获取
使用@param注解或者多个参数封装成 map
18.MyBatis 是否支持延迟加载?如果支持,它的实现原理是什么?
MyBatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在 MyBatis配置文件中,可以通过 lazyLoadingEnabled=true/false 配置是否启用延迟加载。
原理是:使用CGLIB创建目标对象的代理对象,当调用目标方法时会进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。
19.Xml 映射文件中,除了常见的 select、insert、updae、delete标签之外,还有哪些标签?
resultMap、parameterMap、sql、include、selectKey,加上动态sql的9个标签,其中sql为sql片段标签,通过include标签标签引入sql片段,selectKey标签为不支持自增的主键生成策略标签。
20.通常一个Xml映射文件,都会写一个Dao接口与之对应,请问这个 Dao 接口的工作原理是什么,是否可以重载?
不能重载,因为通过Dao寻找Xml对应sql的时候,使用全限名+方法名的保存和寻找策略。Dao接口工作原理为jdk动态代理原理,运行时会为dao生成proxy,代理对象会拦截接口方法,去执行对应的sql返回数据。
21.什么是MyBatis的接口绑定?有哪些实现方式?
接口绑定是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定。
接口绑定有两种实现方式:
①通过注解绑定,在接口的方法上面加上@Select、@Update等注解,里面包含Sql语句来绑定;
②通过xml里面写SQL来绑定,要指定xml映射文件里面的namespace必须为接口的全路径名。
当Sql语句比较简单时候,用注解绑定;当SQL语句比较复杂时候,用xml绑定,一般用 xml 绑定的比较多。
22.MyBatis如何获取自动生成的(主)键值?
在insert标签中使用useGeneratedKeys和keyProperty两个属性来获取自动生成的主键值。
23.使用MyBatis的mapper接口调用时有哪些要求?
①Mapper 接口方法名和 mapper.xml 中定义的每个 sql 的 id 相同;
②Mapper 接口方法的输入参数类型和 mapper.xml 中定义的每个 sql 的parameterType 的类型相同;
③Mapper 接口方法的输出参数类型和 mapper.xml 中定义的每个 sql 的resultType 的类型相同;
④Mapper.xml 文件中的 namespace 即是 mapper 接口的类路径。
24.MyBatis 动态sql有什么用?执行原理?有哪些动态sql?
MyBatis动态sql可以在Xml映射文件内,以标签的形式编写动态sql。
执行原理是根据表达式的值完成逻辑判断并动态拼接 sql 的功能。
MyBatis提供了9种动态sql标签:trim、where、set、foreach、if、choose、when、otherwise、bind。
25.MyBatis是否可以映射 Enum 枚举类?
MyBatis不单可以映射枚举类,还可以映射任何对象到表的一列上。
映射方式为自定义一个TypeHandler,实现TypeHandler的setParameter()和getResult()接口方法。
TypeHandler有两个作用,一是完成从javaType至jdbcType的转换,二是完成jdbcType至javaType的转换,体现为setParameter()和getResult()两个方法,分别代表设置sql问号占位符参数和获取列查询结果。
26.Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别?
Mybatis不仅可以执行一对一、一对多的关联查询,还可以执行多对一,多对多的关联查询。
多对一查询,其实就是一对一查询,只需要把selectOne()修改为selectList()即可;
多对多查询,其实就是一对多查询,只需要把selectOne()修改为selectList()即可。
关联对象查询有两种实现方式:
一种是单独发送一个sql去查询关联对象,赋给主对象,然后返回主对象。
另一种是使用嵌套查询,嵌套查询的含义为使用join查询,一部分列是A对的属性值,另外一部分列是关联对象B的属性值,好处是只发一个sql查询,就可以把主对象和其关联对象查出来。
27.MyBatis里面的动态Sql是怎么设定的?用什么语法?
MyBatis里面的动态Sql一般是通过if节点来实现,通过OGNL语法来实现,但是如果要写的完整,必须配合where,trim节点,where节点是判断包含节点有内容就插入where,否则不插入,trim节点是用来判断如果动态语句是以and或or开始,那么会自动把这个and或者or取掉。
28.简述MyBatis的插件运行原理,以及如何编写一个插件?
运行原理:MyBatis只能编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor四种接口的插件,MyBatis通过动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这四种接口对象的方法时,就会进入拦截方法。具体就是调用InvocationHandler的invoke()方法,只会拦截指定需要拦截的方法。
编写插件:实现MyBatis的Interceptor接口并复写 ntercept()方法,然后给插件编写注解,指定要拦截哪一个接口的哪些方法,最后在配置文件中配置编写的插件。
29.为什么说Mybatis是半自动 ORM 映射工具?它与全自动的区别在哪里?
Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以称之为半自动ORM映射工具。
30.resultType和resultMap的区别?
类的名字和数据库相同时,可以直接设置resultType参数为Pojo类。若不同,需要设置resultMap将结果名字和Pojo名字进行转换。
useGeneratedKeys=“true” keyProperty=“id”
mybatis获取最新添加的ID
面试真题
1.redis雪崩、redis持久化方法、maven版本管理、RibbitMQ,补偿机制、 redis应用位置、feign调用流程、熔断机制、高并发回馈、 高并发限流、数据库索引、索引存储、B+树深度优先、广度优先、 集合使用、volatile关键字、内存可见、流程引擎active、list存对象,不用遍历去重
2.(博彦)HashMap的了解,Thread类的继承有什么好处,callable和Runable的区别, SpringMVC和SpringBoot的区别,说一下对JavaIO的了解,如果有10万条数据怎么查, git如何使用分支
35.(博彦科技)final可不可以修抽象类,volatile修饰的对象存贮在哪里?,红黑树和avl树那个效率高?,Springbean的生命周期,linux的创建文件和创建文件夹,linux如何进行全文搜索,SpringCloud的组件运用,红黑树的时间复杂度,时间复杂度和空间复杂度,你怎么理解?feign的底层调用原理是什么?RestController和Controller有什么区别?如何优化索引?x’a’b’f
RestController是ResponseBody和Controller合在一起,ResponseBody是把对象放入响应体的方法
4.(博彦)如何使用多线程多线程的场景,字符流和字节流有什么区别和联系,如何保证定时任务集群定时任务只执行一此. 你用过什么maven命令,lefter join和right join的区别
5.(汉克时代)核心线程数是10,最大线程数是20,100个线程使用线程池应该怎么分配,怎么创建线程池,hashmap的put方法,hashmap的key可否为null,hashmap是否线程安全体现在哪,hashtable和cocurrenthashmap,分布式锁产生原因和解决方案,设定使用redis实现分布式锁的时候如果过期时间比程序运行时间小导致所失效怎么办,怎么同步redis和数据库数据,索引最左原则
29.(汉克)hashmap底层,单点登入,idea的快捷键,合并分支,线程池的创建方式,如何保证redis和mysql的一致性,为什么双删只删一次不可以吗,异常处理的注解
3.(软通动力)说一说最近的项目,你负责什么模块,Nacos和Eureka的区别,如何设计一个注册中心, Nginx和GateWay的区别,Redis在你项目中的用处,List去重, 如何设计场景提示订单已付款的消息,如果微服务注册中心宕机了怎么办其他微服务还会调用吗? select A,B from where B=‘xxx’ 这个A,B都是索引。B这个索引有效吗?什么是负载均衡?
17.(软通动力)JVM 新老代 GC,MQ消息丢失处理,Redis,
28.(软通动力)Io流,索引的类型,除了索引还有什么方式加快查询,mq的消息补偿机制
19.(海康威视)HashMap、JVM、synchronized与lock、reentrantlock、数据库索引等级、批量查询sql、mybatis批量查询、eureka服务注册原理、比如集群2主3从宕机会发生的问题、sql优化、redis中String类型的实现底层原理
34.(海康二面)微服务分了多少个节点,整个项目怎么拆分为多个微服务。springboot介绍、RestController注解、hashmap怎么用的、ArraryList和hashmap区别、怎么存有序且唯一的数据、sql分表连接查询优化、sql执行过程、vue生命周期、rabbitmq消息确认机制、rabbit最终怎么确定消息到达
26.(海康一面)jvm、jvm调优、redis和数据库同步、分库分表、当你的数据库里有2kw数据,redis中有20w数据、如何保证其中的全是热点数据、什么是线程、线程安全是什么?如何保证、线程的最大数量。强引用、弱引用、软引用、虚引用、项目中数据库的数量、rabbitmq的消息保证不丢失
5.hashmap底层、hashmap为什么设置初始长度为16、用过联合主键吗、用过多列索引吗、索引列为abc 有几种组合方式、 水平分表和垂直分表是什么、线程池中的最大连接数、redis与数据库的一致性问题、redis的集群
6.redis的持久化机制、当你的数据库里有2kw数据,redis中有20w数据,如何保证其中的全是热点数据。 JVM调过优吗、你在项目中遇到最难的问题是什么、什么是线程安全,都有什么锁,悲观锁和乐观锁都是什么。
7.介绍一下你的项目、redis和数据库同步问题、更新redis之后服务发生问题导致数据库没更新怎么办、sql排序使用什么、 sqlorder by的字段要不要加索引、为什么要加索引、不考虑数据量的情况下,有了redis为什么还要用mysql
8.介绍一下hashMap的底层,如何保证key的唯一性。
9.jvm内存的属性、gc回收、线程属性、线程池大小、redis数据一致性、redis数据类型、 sleep和wait区别、 强弱软虚引用、start和run区别、oracle和mysql区别、线程排序
10.redis分布式事务、ribbit消息队列、springcloud组件、索引、三种关联关键字、mybaits配置是自动还是手动、 spring事务注解、springAOP功能、设计模式、springboot注解、dubbo原理、redis能做什么、redis数据一致性
11.(永鑫科技)springboot不用自带tomcat、mysql语句连表查、left join和join区别、IO流、jdk1.8新特性(stream/函数式接口)、 静态代码块中存放创建list和map集合会产生什么问题、如何保证线程安全、线程的应用环境、慢查询、事务的四大特性、
12.(永鑫科技)最近业务最复杂的项目,考虑过高并发场景吗,你的推荐模块如何设计的,数据库怎么调优的,索引的失效条件
13.(永鑫科技)springboot与springmvc区别,sql优化,string stringbuffer区别,数据库建表怎么建的,es分词器,java8新特性,char varchar,==equal
14.(永鑫科技)javajdk1.8新特性,==和equas的区别、string、stringBuider、stringBuffer区别,深拷贝与浅拷贝的区别、 深拷贝怎么实现,sql优化、varchar与char的区别,事务隔离级别
15.(中软国际)Spring IOC和AOP,Redis的数据类,JVM内存结构,Redis的持久化,Linux常用命令, synchronized和Lock锁的区别,Mysql事务的特性,MySQL的索引优化,如何分析一个SQL语句执行的快慢?, Redis的数据类型,线程的创建方式,如何解决缓存穿透,SpringMVC执行流程, RabbittMQ的消息丢失解决方案,Spring Cloud的常用组件,#和$的区别,MyBatis的逻辑分页和物理分页的区别?Spring的事务有哪些?
16.(永鑫科技)java中使用的版本,java8新特性,堆和栈有什么区别,事务的隔离级别,varchar和char的区别, 深复制浅复制,SQL优化,SpringMVC和SpringBoot的区别,
18.(华为)调用外部接口怎么操作需要考虑什么问题、线程的属性配置、mybatis批量导入、springboot异步处理、 redis定时任务、线程配置数据(核心线程数、拒绝策略、最小等待时间)、索引失效、建索原则、日志同步、 根据日志异常处理问题、服务线上测试怎么解决问题、同类中A调用B,B是异步,怎么运行、map集合、jdk1.8特性、 如何使用函数式接口、如何使用Stream流、序列化、Integer和int数据区间(缓存区),如何删除list中数据(具体代码)、 迭代循环、分段锁、优化数据库效率、debug怎么操作(快捷键)、springcloud组件(配置)、redis雪崩穿透解决办法
20.缓存穿透、缓存雪崩、synchronized和threadlocal的区别、threadlocal的实现方式、Eurka关于CAP理论、Eurka的操作原理、悲观锁和乐观锁、数据库的锁、数据库的连接方式
21.数据库中枚举列,怎么查询每个枚举的所有条数、数据库中枚举列可否加索引、分布式事务的解决方式、redis双写一致性问题、服务之间的调用是通过什么调用的、AOC和IOP、你在项目中的异常处理是怎么做的、异常处理是通过什么注解实现的、Spring中如何使用事务、为什么通过注解即可实现
22.springcloud组件,redis持久化,水平越权,线程安全解决,线程安全原因,数据库索引,时间片轮转机制,线程池应用,设计模式,简单工厂模式缺点,设计模式原则,redis单线程特点,hashmap时间复杂度,redis数据类型
23.(暖芯迦(全栈))MyBatis原理,Http流程,vue生命周期,v-show和v-if的区别,如何查询一个数据库对象之后完成主键自增,如何进行一个批量插入在MyBatis的框架中,如何进行数据库优化,分库分表有了解过吗?,Redis的常用淘汰策略,如何保证Redis存储一个key-value键值对占用的内存空间最小,你有用到Redis吗?,你如何使用Vue?,数据库如何分析索引?
24.jvm,redis和数据库同步,分库分表,当你的数据库里有2kw数据,redis中有20w数据,如何保证其中的全是热点数据,jvm调优过吗,线程池最大数,用到什么线程池
25.(讯策)Spring cloud组件、熔断、降级、限流、Eureka掉了之后服务还能不能访问到、自我保护机制、Bean生命周期、Redis持久化、RabbitMQ、MyBatis执行流程、VUE、前端异步刷新、业务交流(金融业务,主要技术栈ssm,也有redis和cloud,视频面)
27.(凯捷国际)jvm 垃圾回收器几种 你用哪种 为什么用 原理是什么。jvm启动参数是什么,你怎么配置的。mysql的隔离级别。mysql的储存引擎。实现aop的小框架(叫 aspectj),aop的组件,spring怎么解决循环依赖,为什么用三级缓存。mybatis怎么做数据源管理。mybatis执行器。topic是什么。vhost是什么。rabbitmq的模型。项目流程是什么。分布式事务
30.介绍项目,项目中有多少个接口,自己开发了多久,接口中都用到了什么注解,编码的流程是什么
31.(citydo)gateway支不支持长链接(支持)
32.(菜鸟)自我介绍,介绍项目,项目中遇到的难点,Java中的容器,nio,final关键字,多线程的使用场景,
36.(长遥科技)innodb和myisym有什么区别?,Redis的数据类型,Dubbo的原理,RandomAccess的接口有什么作用?
37.(香浓科技)Redis你用过什么取出数据的常用的命令?
38.(涂鸦)filter怎么实现的,springmvc工作流程,mybatis插件,transcational注解属性,使用这个注解遇到的坑,事务隔离级别和传播行为,spring的了解,bean作用域,ioc的底层实现,springcloud插件,gateway为什么高效,feign调用底层,日志怎么查看,幂等性问题怎么解决,springcloud遇到过什么难点,自己技术方面有什么不足
WEB开发岗位,笔试题
- 有一个除法接口定义如下,请给出具体实现以及对应的单测。
public interface Calculator {double divide(double dividend, double divisor);
}
- 有一个实时显示页面访问次数的工具,接口定义如下,请给出具体实现以及对应的单测。
public interface Vistor {int times(int lastNum);
}
面试问答
1.做个自我介绍。
面试官,你好。
我叫孙振峰,这次面试的岗位是后端开发,学历是统招本科
在校期间考取了英语四级,工信部的高级软件工程师的证书,
平时喜欢逛逛CSDN,力扣这些技术网站,
到目前为止,我在大连的科艺公司干了三年的后端开发。
在工作期间,参加了4个项目,主要是教育类,管理类还有商品类,
应用的技术包括jvm,mysql数据库,spring全家桶,springcloud的各种组件,
对各种技术和业务场景都很熟悉。。
*为什么离职
关于离职的原因呢,现在的公司各方面也都还不错,但是接触的技术和项目都相对有限,
所以根据我个人的发展情况,想找一家有互联网背景的公司,
正好我家里有亲戚在杭州,然后杭州的技术和互联网发展前景都比较好,
所以想离职来杭州,接触一些新的技术,发展好可能在这定居。
2.为什么选择杭州。
一方面,杭州的技术和互联网发展的情况都比较好,另一方面家里亲戚也在这边。
3.说说自己的优点和缺点
我的优点是喜欢接触新的技术,学习能力比较强,做事情很投入。
缺点是应变能力有点差,但是为了弥补,我习惯做事提前做好计划,避免出现预期以外的事。
4.说说最近/最得意的项目
德医精诚这个项目,是针对医院、规培机构线上学习和考试的一个项目。
甲方是一个专门对医院信息化做软件的公司。
主要包括试题管理、试卷管理、用户管理、科目管理、统计分析模块。
我负责的主要是试题部分的批量导入以及CRUD和试题难度系数的确定,通过redis缓存热点试题,减轻持久层的压力。
根据前端对试卷试题数量、分数、难度系数的设置,通过算法进行抽题组卷。
使用RabbitMQ来处理分布式事务,利用layUI插件做考试结果的视图分析。
5.对加班的看法
我可以接受加班
这个行业,加班肯定不可避免,干这么多年,尤其是项目时间紧的时候,加班太平常了。
但要是因为我自己工作效率低的话,虽然一般不会出现这种情况昂,但是可能会因为刚入职,要熟悉代码环境,
这种情况那我会在入职前期主动加班,尽快熟悉,然后提高工作效率。
6.职业规划
比较喜欢研究技术,想往架构方面发展。
7.有什么想问的
技术面:瀑布流开发还是敏捷式开发,用什么技术栈等
人事面:晋升机制,福利待遇,如果被贵公司录取,需要准备点什么
贵公司对新入公司的员工有没有什么培训项目,如果有这样的机会,是否可以参加?
8.这个职务你有什么优势,为什么选择你
优势:
第一,我准备长期在杭州发展,
再一个适应能力强,擅长和同事沟通,能很快的进入工作状态,
还有身边好几个朋友都是干IT的,如果他们想来杭州发展,
9.企业项目流程:
公司中有很多组 每个组接的项目不一样 用到的技术也不一样
一般接到了项目先有外部的设计书,
之后进行基本设计就是我们内部设计画图
然后项目组领导他们就准备详细的计划书,为了coding做准备
完事了就开始编码了
自己做单体测试,一般是用junit
之后内连 就是我们吧自己做的串起来一起跑一遍测试一遍
之后外联 就是跟外边的人一起跑 一起测试
经过一堆测试之后保守就是项目上线了
面向对象和面向切面
总的来说:
面向切面AOP是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,
从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性。
主要的功能是:日志记录,性能统计,安全控制,事务处理,异常处理等等
面向对象OOP针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。
面向对象是以纵向考虑架构,而切面编程则是架构的辅助,以横向考虑架构,两者在架构上相辅相成,用在JAVA的组件化,解耦合。
难度系数反映题目的难易程度,描述考生在答题时的失分情况。一般地,难度系数的计算公式为:
L=1-X/W
其中,L为难度系数,X为样本平均得分,W为试卷总分(对于单题而言,W为该题的分值)。
这是在有足够答题数据的前提下建立的难度计算公式,而题库类的产品中题目被作答的次数是有一个累积的过程,对于新入库的题目,这个计算公式并不适用。针对题库产品的特性以及题目难度系数计算公式的适用问题,我们按以下步骤来确定并校准题目的难度系数:
-
人工标记题目初始难度
新题目在录入、解析的环节中,由教研人员根据一定的标准(如上述第二部分中“难题”的标准),给题目录入一个初始难度值,难度值的范围为1~10共10个等级,这个值越大代表这道题的难度越大。 -
题目被大量作答后,提取正确率并计算难度系数
根据公式L=1-X/W计算该题难度系数。 -
比对步骤1和步骤2中产生的难度值,确定题目的最终难度系数
如果难度值为1~3,而难度系数为0.7~0.9,则用人工初始难度值转化为该题的难度系数,并把这道题交由教研人员重新评估题目的难度值,并检查此题是否出现在了超纲的位置。此外的其他情形,都用新计算出来的难度系数来取代初始难度值。 -
步骤3中教研人员重新评估题目难度值的环节中如果发现严重的偏差,则在修正后用难度系数来取代初始难度值。
项目:redis rdb持久条件 异常处理 并发量 3000-5000 模块拆分
easyPOI
Excel 这个是最基本常用的注解,注解在模型字段上,可添加列名、列的排序、列宽、格式等属性
ExcelTarget 用于外层的模型实体,可注解行高、字体大小等属性
ExcelEnity 用于标记实体内部类是否继续穿透
ExcelCollection 用于注解集合字段
ExcelIgnore 忽略这个属性问题:并发注册问题
重复注册问题
我们在做完功能之后,对首页上所有功能做了并发性能测试,结果测出在做注册功能时会碰到同一个用户名或者手机号在并发量高的情况下会出现多次注册的问题。
我们的需求要求每个手机号或用户名只能被注册一次,在用户注册填写用户名手机号时会通过ajax去后台异步校验是否有重复记录,在并发情况下,会出现多个注册用户同时去后台校验一个未被注册的用户名时,都会成功通过,从而导致注册的用户名或者手机号重复。
解决方案:利用redis来解决,用户输入用户名去后台校验一旦成功,首先去redis中查看是否有这条用户名,如果有,则视为此用户名被别的用户抢占了,如果没,就放进去,然后返回校验成功。在别的用户去redis找这条数据只能排队去查时都能查到,视为用户名被抢占了,返回用户名校验不通过。
阻塞队列的种类
ArrayBlockingQueue:由数组结构构成的有界阻塞队列。
LinkedBlockingQueue:由链表构成的有界(默认大小值为Integer.MAX_VALUE)阻塞队列。
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
SynchronousQueue:不存储元素的阻塞队列,即单元素阻塞队列。
LinkedTransferQueue:由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:由链表结果组成的双向阻塞队列。
nginx网关,支持高并发,最多可以5万,用户访问的总入口,也就是前端页面的容器,流量网关
gateway,受不了高并发,,是针对每一个业务微服务来得,属于业务网关
gateway的主要功能有,路由,断言,过滤器,利用它的这些特性,可以做流控。
nginx做网关,更多的是做总流量入口,反向代理,负载均衡等,还可以用来做web服务器
dobbu+zookeeper对应的是SpringCloud
介绍
SpringBoot 该项目每个微服务内部都是使用SpringBoot进行搭建的,emmm,直接牛逼
SpringCloud 该项目是由好几个微服务组成的,微服务之间的注册和调用等是通过SpringCloud来完成的。使用到了Eureka,Zuul,Ribbon,Feign
MybatisPlus 该项目使用MybatisPlus来完成对mysql的持久层操作
SpringData 该项目虽然没有使用JPA来完成对mysql的操作,但是其他数据库(MongoDB,redis,ElasticSearch)都是使用SpringData来操作的
JWT 该项目用jwt实现单点登录,对用户的请求进行认证。采用的是无状态登录
Rsa 一个非对称机密算法,将token的载荷和秘钥进行加密放入签名域
FastDFS 一个轻量级的分布式文件系统,用于项目上传图片等文件
RabbitMQ 该技术是基于AMQP协议的消息代理软件,通过该技术实现了手机,验证码的发送以及数据库之间数据的同步
Mysql 该项目用Mysql来存储主要的数据(用户信息,学科信息,发布的试卷信息,用户订阅的考试信息,用户的考试亲狂)
MD5 一个不可逆加密算法,该项目用md5来实现对用户密码的加密
Druid 该项目使用Druid来作为mysql的数据源
MongoDB 该项目用MongoDB来存储关于试卷的数据(试题信息,试卷信息)以及日志信息
ElasticSearch 该项目用ElasticSearch来存储用于搜索的用户数据,并实现搜索和聚合等功能
Redis 该项目用Redis来做部分数据的缓存,并且用redis来存储手机和邮箱的验证码信息
Nginx 该项目用Nginx来实现反向代理
Quartz 定时任务框架,该项目用Quartz来实现某些操作的定
Swagger2 该项目用swagger2实现对RESTful风格的api进行统一描述和可视化调用
Lombok 该项目使用Lombok来简化实体类和日志
Logback 该项目使用logback来实现日志的输出和持久化
Hibernate-validator 该项目使用hibernate-validator来进行部分实体类的数据校验
Docker 该项目使用的服务器是用docker进行统一管理的
阿里云短信服务服务 该项目使用的短信服务是由阿里云提供的
Git 该项目用git来进行版本管
项目问题
第一个项目最熟。
题库。
模块。用户管理项目。科室分类。
1招生模块——项目(冲突发邮件)—注册—学生模块—打卡签到,降班等等—班主任模块
系统,服务,项目之间的调用 feign调
PDA大连港项目(百年港口)
分布式项目
客运和货运
业务
货运:进、出口运输
自己有运输公司:汽车运输——集装箱为主,火车运输,轮船运输
理想状态货物来,不落地转港,但无法实现,需要停留一段时间,需要地方存一定时间
存货物:冷冻库,其他是堆场(类型:集装箱、矿石区、化学品区)根据存放时间收费
**港:**分区域——旅顺港区,大连港区,大窑湾区等
分类型——深水港区(1号、2号、3号等)、潜水港区(1号、2号、3号等)
海关接口:检疫,是否合格,走私,传染,保护动物等等
(通过检疫单号,能查到货物))
装载业务、卸载业务:龙门吊,也需要排班
排港业务,类似排班
入港申请及审批模块
1.入港申请——个人——依托于物流公司
通过PC端,移动端小程序等等,
可以填写入港申请,信息:货物类型,单位数量(立方米,集装箱),计划入港时间,预计出港时间,公司,法人等等。
2、大连港人员:审批,
入港人或公司的合法性(国家查询检验的接口)
时间的审核:
①审核要求的入港时间是否有合适的堆场
②如果拒绝,通过查询堆场排班,查出最近能空闲出的堆场,给出建议,预计进港时间,以此时间为准
③个人或物流公司可以查看到信息
堆场情况,天气状况,临时突发演习等等,限定24小时内给出反馈,是否接受
④大连港审核人员:查看反馈,不同意就没业务,如果同意就指定发通行证号,进港路线,交订违约金,如果没到港,或取消扣除一定违约金。
排港模块(没有用户的事)
运输公司 船的类型 子母船等
十、JVM解析
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。
1.JVM的生命周期:
*启动。启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。
*运行。main()作为该程序初始线程的起点,任何其他线程均由该线程启动。
*消亡。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。
当在电脑上运行一个程序时,就会运行一个java虚拟机,java虚拟机总是开始于main方法,main方法是程序的起点。
java的线程一般分为两种:守护线程和普通线程。守护线程是java虚拟机自己使用的线程,比如GC线程就是一个守护线程,当然你可以把自己的线程设置为守护线程,注意:main方法启动的初始线程不是守护线程。
只要java虚拟机中还有普通线程在执行,java虚拟机就不会停止,如果有足够的权限,你可以调用exit()方法终止线程。
2.JVM的体系结构:
1) 类装载器(ClassLoader)(用来装载.class文件)
2) 执行引擎(执行字节码,或者执行本地方法)
3) 运行时数据区(方法区、堆、虚拟机栈、程序计数器、本地方法栈)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FL641L7G-1639059913927)(D:\上课资料\day5\JVM\pics\1203840-20181123165755801-1226339641.png)]
首先我们对运行时数据区中的5个区域进行分析:
3、运行时数据区:
3.1 堆:
所有线程共享的内存区域,在虚拟机启动时创建。
用来存储对象实例,如:String a = new String()中new String()创建了一个对象,该对象存放在堆内存中,而a 是存放在栈中的,堆中new String() 存放了栈中 a 的内存地址。
可以通过-Xmx和-Xms控制堆的大小
当在堆中没有内存完成实例分配,且堆也无法再扩展时,会报OutOfMemoryError异常。
java堆是垃圾回收器的主要工作区域。java堆还可以分为新生代、老年代。但是垃圾回收器的永久代是在方法区中的,不在堆中。
(新生代:新建的对象由新生代分配内存;老年代:存放经过多次垃圾回收器回收仍然存活的对象;永久代:存放静态文件,如java类、方法等,永久代存放在方法区,对垃圾回收没有显著的影响)
3.1.1 新生代:
分为三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1:1。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到老年代中。
因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,
复制算法的基本思想就是:
将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。
复制算法不会产生内存碎片。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。
紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。(动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。)经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样**,都会保证名为To的Survivor区域是空的**。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
GC可分为三种:Minor GC Major GC 和 Full GC
Minor GC :是清理新生代。触发条件:当Eden区满时,触发Minor GC。
Major GC:是清理老年代。是 Major GC 还是 Full GC,大家应该关注当前的 GC 是否停止了所有应用程序的线程,还是能够并发的处理而不用停掉应用程序的线程。
Full GC :是清理整个堆空间—包括年轻代和老年代。触发条件:调用System.gc时,系统建议执行Full GC,但是不必然执行;老年代空间不足;方法区空间不足;通过Minor GC后进入老年代的平均大小大于老年代的可用内存;由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
3.1.2 老年代:
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
MajorGC采用标记—清除算法:
首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。
MajorGC的耗时比较长,因为要扫描再回收。
MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。
当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。
3.1.3 永久代
(永久代是在方法区中的,而不在堆中,这里只是为了总结GC的运行机制并和新生代、老年代进行比较才将永久代放在这里写):
指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域. 它和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的MetaSpace区域所取代。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入java堆中. 这样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制.采用元空间取代永久代的原因:(1)为了解决永久代的OOM问题,元数据和class对象存在永久代中,容易出现性能问题和内存溢出。(2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出(因为堆空间有限,此消彼长)。(3)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低(4)Oracle 可能会将HotSpot 与 JRockit 合二为一。
3.2 方法区:
所有线程共享
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
当方法区无法满足内存的分配需求时,报OutOfMemoryError异常
方法区中有一个运行时常量池,用于存储编译期生成的各种字面量与符号引用,当常量池无法再申请到内存时报OutOfMemoryError异常。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HHQnMJsv-1639059913927)(D:\上课资料\day5\JVM\pics\image-20210323165523181.png)]
3.3 虚拟机栈:
线程私有,声明周期与线程同步。
存储一些方法的局部变量表(基本类型、对象引用)、操作数栈、动态链接、方法出口等信息。
每个虚拟机线程都有一个私有的栈,一个线程的java栈在线程创建的时候被创建。
每个方法执行的同时都会创建一个栈帧,每个方法被调用直至完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
当线程请求的栈深度大于虚拟机允许的深度时报StackOverFlowError异常。
当栈的扩展无法申请到足够的内存时报OutOfMemoryError异常。
3.4 本地方法栈:
主要是为虚拟机使用到的Native方法服务,Native 方法就是一个java调用非java代码的接口,该方法的实现由非java语言实现。Native方法用native修饰,没有方法体,因为方法体中的实现是非java语言的。
有时java需要调用操作系统的一些方法,而操作系统基本都是C语言写的,这时就需要使用到Native方法了。
Native方法关键字修饰的方法是一个原生态的方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI(Java Native Interface)接口调用其他语言来实现对底层的访问。
3.5 程序计数器:
当前线程所执行的字节码的行号指示器,当前线程私有,由于他只是存储行号,一般就是一个数字,所以不会出现OutOfMemoryError异常。
其特点是:如果正在执行java方法,则这个计数器记录的是正在执行的虚拟机字节码指令地址,如果正在执行Native方法,则这个计数器为空(undefined),此内存区域是唯一一个在java虚拟机中没有规定任何OutOfMemoryError异常情况的区域。
使用场景:A线程先获取CPU时间片执行,当执行到一半的时候,B线程过来了,且优先级比A线程的高,所以处理器又去执行B线程了,把A线程挂起,当B线程执行完了以后,再回过头来执行A线程,这时就需要知道A线程已经执行的位置,也就是查看A中的程序计数器中的指令。
总结:java对象存放在堆中,常量存放在方法区的常量池中,虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据放在方法区,以上区域都是线程共享的。栈是线程私有的,存放该方法的局部变量(基本类型、对象引用)操作数栈、动态链接、方法出口等信息。一个java程序对应一个JVM,一个方法对应一个java栈。
4、垃圾收集器的种类:
4.1.1 Serial 收集器:
这个收集器是一个单线程的收集器,但它的单线程的意义不仅仅说明它会只使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。串行垃圾回收器是为单线程环境而设计的,如果你的程序不需要多线程,启动串行垃圾回收。串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩;垃圾收集的过程中会Stop The World(服务暂停)
4.1.2 ParNew收集器:
ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩
4.1.3 Parallel 收集器:
Parallel 收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-压缩
4.1.4 Parallel Old 收集器:
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供
4.1.5 CMS收集器:
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew)
优点:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量
4.1.6 G1收集器:
空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。
G1的新生代收集跟ParNew类似,当新生代占用达到一定比例的时候,开始出发收集。和CMS类似,G1收集器收集老年代对象会有短暂停顿。
5.1 java源码编译机制:
java源码是不能被机器识别的,需要经过编译器编译成JVM可以执行的.class字节码文件,再由解释器编译运行,即:Java源文件(.java) – Java编译器 --> Java字节码文件 (.class) – Java解释器 --> 执行。流程图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j3nDRbRH-1639059913928)(D:\上课资料\day5\JVM\pics\1203840-20181126150035709-1870725159.png)]
java中字符只以Unicode存在,字符转换发生在JVM和OS交界处
5.2 类加载机制(ClassLoader):
java程序并不是一个可执行文件,是由多个独立的类文件组成。这些类文件并不是一次性全部装入内存,而是依据程序逐步载入。
JVM的类加载是通过ClassLoader及其子类来完成的,累的层次关系和加载顺序可以由下图来描述:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x2Q8ap9a-1639059913928)(D:\上课资料\day5\JVM\pics\1203840-20181126150751061-1266795792.png)]
1)Bootstrap ClassLoader
是JVM的根ClassLoader,由C++实现;加载Java的核心API:$JAVA_HOME中jre/lib/rt.jar中所有class文件的加载,这个jar中包含了java规范定义的所有接口以及实现;JVM启动的时候就开始初始化此ClassLoader。
2)Extension ClassLoader
加载java扩展API(lib/ext中的类)
3)App ClassLoader
加载Classpath目录下定义的class
4)Custom ClassLoader
属于应用程序根据自身需要自定义的ClassLoader,如tomcat、Jboss都是会根据J2EE规范自行实现ClassLoader
注意:加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
双亲委派机制
JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归。如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
作用:1)避免重复加载;2)更安全。如果不是双亲委派,那么用户在自己的classpath编写了一个java.lang.Object的类,那就无法保证Object的唯一性。所以使用双亲委派,即使自己编写了,但是永远都不会被加载运行。
破坏双亲委派机制
双亲委派机制并不是一种强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。
线程上下文类加载器,这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那么这个类加载器就是应用程序类加载器。像JDBC就是采用了这种方式。这种行为就是逆向使用了加载器,违背了双亲委派模型的一般性原则。
6、JVM调优
先我们需要知道系统当前的运行状况,也就是系统的性能好坏,才能判断是否需要调优。如果系统的响应时间很短,计算机的资源使用也很低,那我们做系统调优就完全是为了调优而调优。那么衡量系统性能的指标到底有哪些呢?
- **响应时间:**响应时间是衡量系统性能的重要指标之一,响应时间越短,性能越好,一般一个接口的响应时间是在毫秒级。响应时间还包括数据库响应时间、服务端响应时间、网络响应时间、客户端响应时间。
- **TPS:**指系统接口的 TPS(每秒事务处理量),因为 TPS 体现了接口的性能,TPS 越大,性能越好。在系统中,吞吐量分为两种:磁盘吞吐量和网络吞吐量。
- **计算机资源分配使用率:**通常由 CPU 占用率、内存使用率、磁盘 I/O、网络 I/O 来表示资源使用率。这几个参数好比一个木桶,如果其中任何一块木板出现短板,任何一项分配不合理,对整个系统性能的影响都是毁灭性的。
JVM 调优都做些什么?
具体来说 JVM 调优需要包括两方面:合理地设置 JVM 的内存空间和选择合适的垃圾回收器。
- **内存空间的分配设置:**JVM 内存分配不合理带来的性能表现并不会像内存溢出问题这么突出,最直接的表现就是频繁的 GC,这会导致上下文切换等性能问题,从而降低系统的吞吐量、增加系统的响应时间。具体的实现包括调整堆内存空间减少 Full GC、调整年轻代减少 MinorGC、设置合理的 Eden 和 Survivor 区的比例。
- **选择合适的垃圾回收器:**垃圾回收主要是指堆和方法区的回收,堆中的回收主要是对象的回收,方法区的回收主要是废弃常量和无用的类的回收。垃圾收集器的种类很多,不同的场景有不同的选择。对于每次操作的响应时间要求比较高的,我们可以选择响应速度较快的 GC回收器,比如 CMS 回收器和 G1 回收器;而对系统吞吐量有较高要求时,就可以选择 Parallel Scavenge 回收器来提高系统的吞吐量。
是否需要 JVM 调优?
一般项目肯定是不需要进行 JVM 调优的,因为 JVM 本身就是为这种低延时、高并发、大吞吐的服务设计和优化的,我们很少需要去改变什么。所以,我们往往更偏重于应用服务本身的调优。
在一些应用中,比如大数据计算引擎,是一种非常极端的 JVM 应用,对延时的要求并不高,但对吞吐量要求很高,会有大量的短生命周期对象产生,同时也有大量的对象生存时间非常久,我们就需要对特定的一些 JVM 参数进行修改。
再比如生产环境中出现内存溢出,我们需要判断是由于大峰值下没有限流,瞬间创建大量对象而导致的内存溢出,还是是由于内存泄漏而导致的内存溢出。对于内存泄漏导致的,这种问题就是程序的 Bug,我们需要及时找到问题代码进行修改,而不是调整 JVM。
JVM 在很大程度上减轻了 Java 开发人员投入到对象生命周期管理的精力。在使用对象的时候,JVM 会自动分配内存给对象,在不使用的时候,垃圾回收器会自动回收对象,释放占用的内存。所以一般情况下我们是不需要调优的。当然事无绝对,某些特殊场景就需要我们进行参数调整,但调整的前提一定是你对 JVM 的运行原理非常熟悉才行。
java的类加载机制
类加载的时机
隐式加载 new 创建类的实例,
显式加载:loaderClass,forName等
访问类的静态变量,或者为静态变量赋值
调用类的静态方法
使用反射方式创建某个类或者接口对象的Class对象。
初始化某个类的子类
直接使用java.exe命令来运行某个主类
类加载的过程
我们编写的java文件都是保存着业务逻辑代码。java编译器将 .java 文件编译成扩展名为 .class 的文件。.class 文件中保存着java转换后,虚拟机将要执行的指令。当需要某个类的时候,java虚拟机会加载 .class 文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程被称为类的加载。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XWK7l4NE-1639059913928)(D:\上课资料\day5\java类加载机制\pics\image-20210323161822933.png)]
加载Loading
类加载过程的一个阶段,ClassLoader通过一个类的完全限定名查找此类字节码文件,并利用字节码文件创建一个class对象。
JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存中,并生成一个代表该类的 java.lang.Class
对象。
验证Verification
目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身的安全,主要包括四种验证:文件格式的验证,元数据的验证,字节码验证,符号引用验证。
JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保证 JVM 安全的重要屏障,下面是一些主要的检查。
- 确保二进制字节流格式符合预期(比如说是否以
cafe bene
开头)。 - 是否所有方法都遵守访问控制关键字的限定。
- 方法调用的参数个数和类型是否正确。
- 确保变量在使用之前被正确初始化了。
- 检查变量是否被赋予恰当类型的值。
准备Preparation
为类变量(static修饰的字段变量)分配内存并且设置该类变量的初始值,(如static int i = 5 这里只是将 i 赋值为0,在初始化的阶段再把 i 赋值为5),这里不包含final修饰的static ,因为final在编译的时候就已经分配了。这里不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象分配到Java堆中。
JVM 会在该阶段对类变量(也称为静态变量,static
关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。
也就是说,假如有这样一段代码:
public String chenmo = "沉默";
public static String wanger = "王二";
public static final String cmower = "沉默王二";
chenmo 不会被分配内存,而 wanger 会;但 wanger 的初始值不是“王二”而是 null
。
需要注意的是,static final
修饰的变量被称作为常量,和类变量不同。常量一旦赋值就不会改变了,所以 cmower 在准备阶段的值为“沉默王二”而不是 null
解析Resolution
这里主要的任务是把常量池中的符号引用替换成直接引用
该阶段将常量池中的符号引用转化为直接引用。
what?符号引用,直接引用?
符号引用以一组符号(任何形式的字面量,只要在使用时能够无歧义的定位到目标即可)来描述所引用的目标。
在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如 com.Wanger
类引用了 com.Chenmo
类,编译时 Wanger 类并不知道 Chenmo 类的实际内存地址,因此只能使用符号 com.Chenmo
。
直接引用通过对符号引用进行解析,找到引用的实际内存地址。
初始化Initialization
这里是类记载的最后阶段,如果该类具有父类就进行对父类进行初始化,执行其静态初始化器(静态代码块)和静态初始化成员变量。(前面已经对static 初始化了默认值,这里我们对它进行赋值,成员变量也将被初始化)
该阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法的过程。
oh,no,上面这段话说得很抽象,不好理解,对不对,我来举个例子。
String cmower = new String("沉默王二");
上面这段代码使用了 new
关键字来实例化一个字符串对象,那么这时候,就会调用 String 类的构造方法对 cmower 进行实例化。
类加载器
类加载器的任务是根据类的全限定名来读取此类的二进制字节流到 JVM 中,然后转换成一个与目标类对象的java.lang.Class 对象的实例,在java 虚拟机提供三种类加载器,引导类加载器,扩展类加载器,系统类加载器。
一般来说,Java 程序员并不需要直接同类加载器进行交互。JVM 默认的行为就已经足够满足大多数情况的需求了。不过,如果遇到了需要和类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就不得不花大量的时间去调试
ClassNotFoundException
和 NoClassDefFoundError
等异常。
对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class 对象不 equals
)。
站在程序员的角度来看,Java 类加载器可以分为三种。
1)启动类加载器(Bootstrap Class-Loader),加载 jre/lib
包下面的 jar 文件,比如说常见的 rt.jar。
2)扩展类加载器(Extension or Ext Class-Loader),加载 jre/lib/ext
包下面的 jar 文件。
3)应用类加载器(Application or App Clas-Loader),根据程序的类路径(classpath)来加载 Java 类。
类加载器的层次关系:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f9bREIzL-1639059913929)(D:\上课资料\day5\java类加载机制\pics\image-20210323163350645.png)]
public class Test {public static void main(String[] args) {ClassLoader loader = Test.class.getClassLoader();while (loader != null) {System.out.println(loader.toString());loader = loader.getParent();}}}
每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 类名.class.getClassLoader()
可以获取到此引用;然后通过 loader.getParent()
可以获取类加载器的上层类加载器。
这段代码的输出结果如下:
sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$ExtClassLoader@15db9742
第一行输出为 Test 的类加载器,即应用类加载器,它是 sun.misc.Launcher$AppClassLoader
类的实例;第二行输出为扩展类加载器,是 sun.misc.Launcher$ExtClassLoader
类的实例。那启动类加载器呢?
按理说,扩展类加载器的上层类加载器是启动类加载器,但在我这个版本的 JDK 中, 扩展类加载器的 getParent()
返回 null
。所以没有输出。
forName和loaderClass区别
Class.forName()得到的class是已经初始化完成的。
Classloader.loaderClass得到的class是还没有链接(验证,准备,解析三个过程被称为链接)的。
双亲委派
双亲委派模式要求除了顶层的启动类加载器之外,其余的类加载器都应该有自己的父类加载器,但是在双亲委派模式中父子关系采取的并不是继承的关系,而是采用组合关系来复用父类加载器的相关代码。
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {// 增加同步锁,防止多个线程加载同一类synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {c = parent.loadClass(name, false);} else { // ExtClassLoader没有继承BootStrapClassLoaderc = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();// AppClassLoader去我们项目中查找是否有这个文件,如有加载进来// 没有就到用户自定义ClassLoader中加载。如果没有就抛出异常c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}
工作原理
如果一个类收到了类加载的请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行,如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最后到达顶层的启动类加载器,如果父类能够完成类的加载任务,就会成功返回,倘若父类加载器无法完成任务,子类加载器才会尝试自己去加载,这就是双亲委派模式。就是每个儿子都很懒,遇到类加载的活都给它爸爸干,直到爸爸说我也做不来的时候,儿子才会想办法自己去加载。
优势
采用双亲委派模式的好处就是Java类随着它的类加载器一起具备一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类的时候,就没有必要子类加载器(ClassLoader)再加载一次。其次是考虑到安全因素,Java核心API中定义类型不会被随意替换,假设通过网路传递一个名为java.lang.Integer的类,通过双亲委派的的模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字类,发现该类已经被加载,并不会重新加载网络传递过来的java.lang.Integer.而之际返回已经加载过的Integer.class,这样便可以防止核心API库被随意篡改。可能你会想,如果我们在calsspath路径下自定义一个名为java.lang.SingInteger?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器,最终会通过系统类加载器加载该类,但是这样做是不允许的,因为java.lang是核心的API包,需要访问权限,强制加载将会报出如下异常。
java.lang.SecurityException:Prohibited package name: java.lang
类与类加载器
在JVM中标识两个Class对象,是否是同一个对象存在的两个必要条件
类的完整类名必须一致,包括包名。
加载这个ClassLoader(指ClassLoader实例对象)必须相同。
双亲委派模式的破坏者:线程上下文类加载器
在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。
线程上下文类加载器(contextClassLoader)是从 JDK 1.2 开始引入的,我们可以通过java.lang.Thread类中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源,如下图所示,以jdbc.jar加载为例
对象的创建过程
当虚拟机遇到一个new的指令的时候,首先去检查这个指令是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载,解析和初始化过。如果没有则执行相应初始化的过程。在类加载检查通过后,接下来虚拟机将为新生对象分配内存,对象所需要的内存的大小在类加载完成后便可以完成确定。内存分配完成以后,虚拟机需要将分配的内存空间都初始化为零值,保证了对象的实例字段在Java代码中可以不赋予初值就直接使用,程序能访问到这些字段的数据类型对应的零值。再接下来对象需要进行必要的设置,这个对象是哪个类的实例,如何才能找到这个类的元数据信息,如何找到对象的哈希码,对象的GC分带年龄。
Java堆如果是规整的采取:指针碰撞,
Java堆如果不是规整的话:空闲列表,在内存中直接分配一个足够大的内存空间划分给对象。
对象创建是非常平凡的,在多线程的程序中会产生线程安全的问题,所以解决这个问题有两种方式
使用CSA配上失败重试的方式来保证原子性
内存分配动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一个小块的内存成为本地分配缓冲,TLAB,哪个线程需要分配内存就在哪个线程的TALB上分配,只有在TALB用完之后才会重新分配新的TALB的时候才会同步锁定。
对象的内存布局
对象的内存布局一般分为三个部分:对象头,示例数据,对齐填充
对象头中存放着对象自身的运行时数据,如哈希码,GC分带年龄,锁状态标志,偏向线程ID,线程持有的锁。
对象头另外一部分还有类型指针,对象指向它类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个java数组,那在对象头中还必须用一块用于记录数组长度的数据。因为虚拟机可以通过普通java对象的元数据信息确定java对象的大小。
对象的访问定位方式
句柄和直接指针
如果使用句柄的话,要在java堆中开辟一个句柄池,用来存放句柄地址,句柄地址中包含对象实例数据(堆)和类型数据(方法区)各自的地址信息。
是用句柄的好处就是引用中存储的是稳定的句柄地址,当被移动时只会修改句柄中的实例数据指针,而引用地址不会被改变。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次访问指针定位的时间开销,引用直接指向存放实例数据的堆内存,在该内存中存放着指向方法区的类型数据地址。
冒泡排序
冒泡排序:依次比较相邻的数据,将小数据放在前,大数据放在后;即第一趟先比较第1个和第2个数,大数在后,小数在前,再比较第2个数与第3个数,大数在后,小数在前,以此类推则将最大的数"滚动"到最后一个位置;第二趟则将次大的数滚动到倒数第二个位置…第n-1(n为无序数据的个数)趟即能完成排序。
//冒泡排序:相邻两个两两比较
for(int i=0;i<a.length-1;i++){for(int j=0;j<a.length-i-1;j++){if(a[j]<a[j+1]){int temp=a[j];a[j]=a[j+1];a[j+1]=temp;}}
}
选择排序
选择排序( Selection sort)是一种简单直观的排序算法。
它的工作原理是每一趟从待排序的数据元素中选出最小(或最大)的一个元素,
顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。
选择排序算法通过选择和交换来实现排序,其排序流程如下:
(1)首先从原始数组中选择最小的1个数据,将其和位于第1个位置的数据交换。
(2)接着从剩下的n-1个数据中选择次小的1个元素,将其和第2个位置的数据交换
(3)然后,这样不断重复,直到最后两个数据完成交换。最后,便完成了对原始数组的从小到大的排序。
//选择排序:一个与剩余其他依次相比较
for(int i=0;i<a.length;i++){for(int j=i;j<a.length;j++){if(a[j]<a[i]){int temp=a[j];a[j]=a[i];a[i]=temp;}}
}
分布式的优势
分布式的优势:
系统之间的耦合度大大降低,可以独立开发、独立部署、独立测试,开发效率大大提升。
系统之间的耦合度降低,从而系统更易于扩展。
服务的复用性更高。比如,当我们将用户系统作为单独的服务后,该公司所有的产品都可以使用该系统作为用户系统,无需重复开发。
).addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass©;
}
return c;
}
}
#### 工作原理如果一个类收到了类加载的请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行,如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最后到达顶层的启动类加载器,如果父类能够完成类的加载任务,就会成功返回,倘若父类加载器无法完成任务,子类加载器才会尝试自己去加载,这就是双亲委派模式。就是每个儿子都很懒,遇到类加载的活都给它爸爸干,直到爸爸说我也做不来的时候,儿子才会想办法自己去加载。#### 优势采用双亲委派模式的好处就是Java类随着它的类加载器一起具备一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类的时候,就没有必要子类加载器(ClassLoader)再加载一次。其次是考虑到安全因素,Java核心API中定义类型不会被随意替换,假设通过网路传递一个名为java.lang.Integer的类,通过双亲委派的的模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字类,发现该类已经被加载,并不会重新加载网络传递过来的java.lang.Integer.而之际返回已经加载过的Integer.class,这样便可以防止核心API库被随意篡改。可能你会想,如果我们在calsspath路径下自定义一个名为java.lang.SingInteger?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器,最终会通过系统类加载器加载该类,但是这样做是不允许的,因为java.lang是核心的API包,需要访问权限,强制加载将会报出如下异常。java.lang.SecurityException:Prohibited package name: java.lang#### 类与类加载器在JVM中标识两个Class对象,是否是同一个对象存在的两个必要条件
类的完整类名必须一致,包括包名。
加载这个ClassLoader(指ClassLoader实例对象)必须相同。#### 双亲委派模式的破坏者:线程上下文类加载器在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。线程上下文类加载器(contextClassLoader)是从 JDK 1.2 开始引入的,我们可以通过java.lang.Thread类中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源,如下图所示,以jdbc.jar加载为例#### 对象的创建过程当虚拟机遇到一个new的指令的时候,首先去检查这个指令是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载,解析和初始化过。如果没有则执行相应初始化的过程。在类加载检查通过后,接下来虚拟机将为新生对象分配内存,对象所需要的内存的大小在类加载完成后便可以完成确定。内存分配完成以后,虚拟机需要将分配的内存空间都初始化为零值,保证了对象的实例字段在Java代码中可以不赋予初值就直接使用,程序能访问到这些字段的数据类型对应的零值。再接下来对象需要进行必要的设置,这个对象是哪个类的实例,如何才能找到这个类的元数据信息,如何找到对象的哈希码,对象的GC分带年龄。Java堆如果是规整的采取:指针碰撞,
Java堆如果不是规整的话:空闲列表,在内存中直接分配一个足够大的内存空间划分给对象。
对象创建是非常平凡的,在多线程的程序中会产生线程安全的问题,所以解决这个问题有两种方式
使用CSA配上失败重试的方式来保证原子性
内存分配动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一个小块的内存成为本地分配缓冲,TLAB,哪个线程需要分配内存就在哪个线程的TALB上分配,只有在TALB用完之后才会重新分配新的TALB的时候才会同步锁定。#### 对象的内存布局对象的内存布局一般分为三个部分:对象头,示例数据,对齐填充对象头中存放着对象自身的运行时数据,如哈希码,GC分带年龄,锁状态标志,偏向线程ID,线程持有的锁。对象头另外一部分还有类型指针,对象指向它类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个java数组,那在对象头中还必须用一块用于记录数组长度的数据。因为虚拟机可以通过普通java对象的元数据信息确定java对象的大小。#### 对象的访问定位方式##### 句柄和直接指针如果使用句柄的话,要在java堆中开辟一个句柄池,用来存放句柄地址,句柄地址中包含对象实例数据(堆)和类型数据(方法区)各自的地址信息。
是用句柄的好处就是引用中存储的是稳定的句柄地址,当被移动时只会修改句柄中的实例数据指针,而引用地址不会被改变。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次访问指针定位的时间开销,引用直接指向存放实例数据的堆内存,在该内存中存放着指向方法区的类型数据地址。#### 冒泡排序冒泡排序:依次比较相邻的数据,将小数据放在前,大数据放在后;即第一趟先比较第1个和第2个数,大数在后,小数在前,再比较第2个数与第3个数,大数在后,小数在前,以此类推则将最大的数"滚动"到最后一个位置;第二趟则将次大的数滚动到倒数第二个位置......第n-1(n为无序数据的个数)趟即能完成排序。```java
//冒泡排序:相邻两个两两比较
for(int i=0;i<a.length-1;i++){for(int j=0;j<a.length-i-1;j++){if(a[j]<a[j+1]){int temp=a[j];a[j]=a[j+1];a[j+1]=temp;}}
}
选择排序
选择排序( Selection sort)是一种简单直观的排序算法。
它的工作原理是每一趟从待排序的数据元素中选出最小(或最大)的一个元素,
顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。
选择排序算法通过选择和交换来实现排序,其排序流程如下:
(1)首先从原始数组中选择最小的1个数据,将其和位于第1个位置的数据交换。
(2)接着从剩下的n-1个数据中选择次小的1个元素,将其和第2个位置的数据交换
(3)然后,这样不断重复,直到最后两个数据完成交换。最后,便完成了对原始数组的从小到大的排序。
//选择排序:一个与剩余其他依次相比较
for(int i=0;i<a.length;i++){for(int j=i;j<a.length;j++){if(a[j]<a[i]){int temp=a[j];a[j]=a[i];a[i]=temp;}}
}
分布式的优势
分布式的优势:
系统之间的耦合度大大降低,可以独立开发、独立部署、独立测试,开发效率大大提升。
系统之间的耦合度降低,从而系统更易于扩展。
服务的复用性更高。比如,当我们将用户系统作为单独的服务后,该公司所有的产品都可以使用该系统作为用户系统,无需重复开发。