java面试概要

高频面试题

1 说一个栈溢出的例子

方法无限递归。

2 说几个堆溢出的例子

循环创建对象,或者一次性从数据库查出很多数据。

3 说一个元空间溢出的例子

循环利用反射创建对象。

4 你知道什么是跨域问题吗

浏览器为了让自己更安全,设置了一种同源策略,“协议+域名+端口” 三者都相同才算同源,不然会阻止一些请求。

5 如何解决跨域问题?

(1)创建一个过滤器,然后把一些来源,请求头和请求方法等都设置成全部允许
(2)可以在类或者方法上面加@CrossOrigin 注解
(3)还可以在 nginx 配置文件配置允许跨域

6 你知道哪些加密算法

(1)我知道有 MD5 算法,MD5 算法主要用的是 哈希函数,不可逆,
无论传入多长的字符串,MD5 都会输出长度为 128bits 的一个串。
(2)还有 DES 加密算法,他的话是主要是 64 位为分组对数据加密,
它的密钥长度 是 56 位,加密解密用同一算法。
(3)对 RSA 也了解一点,这种加密算法是将两个大素数相乘很容易,
但想要对这个乘积进行因式分解就比较困难了,所以一般将乘积作为
加密密钥。

7 如何保证你写的接口的安全性

最好使用 https 协议,在 http 和 tcp 之间添加一层加密层(SSL 层),
这一层负责数据的加密和解密,这样就会防止数据被抓包;
第二种做法可以在每次请求中加入当前的时间,服务器端会拿到当前
时间和消息中的时间相减,看看是否在一个固定的时间范围内比如 5
分钟内;这样恶意请求的数据包是无法更改里面时间的,所以 5 分钟
后就视为非法请求。
加签名:比如使用 md5 算法,将需要提交的数据通过某种方式组合和
一个字符串,然后通过 md5 生成一段加密字符串,客户端和服务端
都计算一份,防止数据传输被篡改。

8 sql 语句什么时候会出现 filesort

一般 sql 有 order by 排序产生,索引使用不当的情况下会出现,
比如 select * from a where type = 5 order by createtime desc,这类语句
一般会产生 Using filesort 这个选项,即使在 type 和 createtime 上
分别添加了索引。因为先根据 type 的索引从所有数据信息中挑选出
满足 type = 5 条件的,然后根据 createtime 列的索引信息对挑选的
数据进行排序,所以产生了 Using filesort
解决办法:可以在 type, createtime 两列上建立一个联合索引。

9 Springboot 动态切换数据源这个你知道吗

这个我切换过,需要继承 Spring 的 AbstractRoutingDataSource 抽
象类,然后重写 determineCurrentLookupKey()这个方法(可以说是重
写一个什么什么 key 方法),因为这个方法是设置数据源的,然后这
个方法里面 return 一个数据源,这个数据源我们可以事先配几个数
据源,然后程序启动加载全部数据源设置进子类里面,最后根据就可
以根据传值获取想要的数据源了。

10 Arraylist 在多线程下会出现什么异常

ConcurrentModificationException(并发修改异常)
为什么:
在多个线程进行 add 操作时可能会导致 elementData 数组越界,还会
导致一个线程的值覆盖另一个线程添加的值
那怎么处理
(1)使用 Vector 容器,也是加了 synchronizedList
(2)使用 Collections 的静态方法 synchronizedList(List< T> list)
3
(3)采用 CopyOnWriteArrayList 容器

11 那你跟我说说 CopyOnWriteArrayList 原理

就是往一个容器添加元素的时候,不直接往当前容器添加,而是
先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添
加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做
的好处是我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,
有点读写分离的思想。

12 那你觉得 CopyOnWriteArrayList 会有什么问题

(1)内存占用问题。因为 CopyOnWrite 的写时复制机制,所以在进
行写操作的时候,内存里会同时驻扎两个对象的内存。
(2)数据一致性问题,因为还没复制完全的时候,线程读取的还是
旧的数据里面的数据,只能保证最终一致性。

13 Hashmap 在多线程情况下会出现什么问题

ConcurrentModificationException(并发修改异常)
put 的时候导致的多线程数据不一致。多线程同时会抢到一个位置,
覆盖了数据。在扩容的时候,jdk1 8 之前是采用头插法,当两个线程
同时检测到 hashmap 需要扩容,在进行同时扩容的时候有可能会造
成链表的死循环。
怎么解决
(1)HashTable 直接 syschronize 锁 put 方法,不推荐
(2)ConcurrentHashMap(推荐)

14 Hashmap 跟 ConcurrentHashMap 区别

(1)Hashmap 允许 key 和 val 为 null,ConcurrentHashMap 不允许
(2)Hashmap 线程不安全,ConcurrentHashMap 线程安全
jdk1 7 对整个数组进行分段(每段都是由若干个 hashEntry 对象
组成的链表),每个分段都有一个 Segment 分段锁),每个 Segment
分段锁只会锁住它锁守护的那一段数据,多线程访问不同数据段的数
据,不会加锁。ConcurrentHashMap :jdk1 8ConcurrentHashMap 在
JDK1 8 中采用 Node+CAS+Synchronized 实现线程安全,取消了 segment
分段锁,直接使用 Table 数组存储键值对(与 1 8 中的 HashMap 一样),
主要是使用 Synchronized+CAS 的方法来进行并发控制,锁的粒度大大
降低了。

15 假如一张数据量很大的表,百万,千万级别的,如何提高深度分页的查询速度

这个我知道我主要有两种方式可以优化
这个可以走覆盖索引

select * from orders_history where type=8 and
id=(select id from orders_history where type=8 limit 100000,1)1
秒多
limit 100;

先走覆盖索引查出 id,再根据 id 找详情。
如果 id 递增的话,用 between 更快,能达到几十毫秒级别的

select * from orders_history where type=2
and id between 1000000 and 1000100 limit 100;

几十毫秒

16 查找表中多余的重复记录,重复记录是根据单个字段(Id)来判断

select * fromwhere Id in (select Id fromgroup byId having
count(Id) > 1)

17 删除表中多余的重复记录(多个字段),只留有 rowid 最小的记录

delete from 表 a where (a  Id,a  seq) in (select Id,seq fromgroup by
Id,seq having count() > 1) and rowid not in (select min(rowid) fromgroup by Id,seq having count()>1)

18 查找表中多余的重复记录(多个字段),不包含 rowid 最小的记录

select * from 表 a where (a  Id,a  seq) in (select Id,seq fromgroup by
Id,seq having count() > 1) and rowid not in (select min(rowid) fromgroup by Id,seq having count()>1)

19 假如给一张数据量很大的表加索引,或者加字段,需要怎么操作

肯定不能直接加,如果直接加字段操作会锁表,还可能造成数据
库卡死。
我的想法有 2 种方法,
① 创建一个临时的新表,首先复制旧表的结构(包含索引)
② 给新表加上新增的字段
③ 把旧表的数据复制过来
④ 删除旧表,重命名新表的名字为旧表的名字
不过上述这种会造成比较少的数据损失,在第三步可能还有数据
写入进旧表
还有一种方法可以在从库进行加字段操作,然后主从切换,这样
数据丢失可能性很小

20 线程池的核心线程数是 new 一个线程池就立马创建吗

这个可以自己设置的,默认情况下是当有任务提交的时候才开始
创建,而且就算空闲的线程足以处理新任务,它仍然会创建新的线程
去 处 理 , 直 到 核 心 线 程 数 达 到 最 大 值 。 当 然 我 们 可 以 调 用
prestartAllCoreThreads()方法先创建所有核心线程。正常情况下,核心
线程池中的线程一但创建了就不会自动被销毁,除非设置了
allowCoreThreadTimeOut=true,或者是线程在执行任务的时候报了异
常。

21 核心线程数能为 0 吗

可以为 0,当核心线程数为 0 的时候,会创建一个非核心线程进行任务处理,其他的跟线程池工作原理一样。

22 线程池如何判断是否需要回收线程

工作线程启动后,就进入 runWorker(Worker w)方法。
里面是一个 while 循环,线程池会判断 getTask()是否返回 null,
如果返回 null 就会被回收。

23 Redis setnx 分布式锁有哪些问题

业务执行时间过长,可能会把其他线程的锁给删了
如何解决:
我使用的 redisson,他有个看门狗机制,会检测程序是否执行完,
没执行完会给锁增加过期时间。

24 那你说说这个看门狗机制的实现原理

当我们在调用 redission 的 lock 方法时没有指定超时时间,就会
使用看门狗的默认时间 30 秒,只要抢锁成功的线程,就会开启一个
延迟任务,超过看门狗时间的 1/3 就会重新给这个锁设置过期时间。

25 Threadlocal 你了解吗

ThreadLocal 提 供 线 程 局 部 变 量 。 每 一 个 线 程 在 访 问
ThreadLocalget 或 set 方法都有自己的变量副本,线程之间数据互不
干扰。
是怎么实现的?
当线程请求时,会给每个线程 new 一个自己的 ThreadLocalMap,
当执行 set 方法时,值是保存在当前线程的 ThreadLocalMap(跟
hashmap 差不多,是个 entry,保存键值对)里,以当前 threadlocal
对象为 map 的 key,设置的值为 map 的 value,当执行 get 方法中,
先通过线程名称获取当前线程的 ThreadLocalMap,然后再通过当前
threadlocal 对象获取自己的 value 属性。

27 为什么要使用弱引用(threadlocal 为什么使用完要 remove)

底层用了弱引用,其实底层是用了 2 层包装,第一次包装是将
thredlocal 对象变成一个弱引用对象,第二层是定义了一个 entry 对象
继承包装后的 threadlocal 对象实现扩展。

为什么要使用弱引用(threadlocal为什么使用完要remove)
避免内存泄露,因为方法执行完毕后,栈帧中的引用断开,但是
此时 Threadlocalmap 里面的 threadlocal 对象还引用着堆里面的实例,
造成内存泄露,如果改为弱引用,只要垃圾回收就会回收,降低了内
存泄露的风险,remove 可以把 threadlocalmap 的 key 设置成 null,这
样 gc 就会回收了。

28 你在项目中有用过 threadlocal 吗?

以前做了一个登录用过,就是业务要区分用户是是登录用户还是
临时用户,在拦截器里面创建了一个 threadlocal 对象,然后把用户的
信息对象存进去,比如临时用户给他生成一个标识,然后再传到
controller 进行不同业务逻辑的判断。

29 你项目中用过 syschronize 吗

用过,以前用过一个 OkHttpClient 类,创建这个类的对象时没有
用同一个 OkHttpClient 实例并重复使用,每次请求都创建一个新的连
9
接对象,而每个实例都有自己的连接池和线程池,从而导致线程大量
堆积,我创建了一个单例模式,然后引入 syschronize,然后双重校验,
并且加 volatile 修饰这个单例属性。

class GetOkHttpClient {
public static volatile OkHttpClient instance = null;
private GetOkHttpClient() {}
public static OkHttpClient getInstance() {
if (instance == null) {
//DCL(双端检索)机制
synchronized (GetOkHttpClient  class) {
if (instance == null) {
instance = new OkHttpClient();
}
}
}
return instance;
}
}

30 谈谈 Syschronize 锁(syschronize 是怎么加锁的)

其实每个 java 对象都是由对象头,实例数据和对其填充三部分组成,
Syschronize 的锁是存在 java 对象头的 mark word 中,锁升级主要看
mark word 中锁的标志位和释放偏向锁的标志位。
Jdk14 后 syschronize 做了什么优化
Jdk14 及之前无锁直接上升到重量级锁,1 4 之后引入偏向锁和轻量
级锁的概念,让并发性更好。

31 那你解释一下这 4 种锁升级是怎么升级的

(1)无锁:一个线程都还进来,此时是无锁的状态。
(2)偏向锁:当一个线程来访问时,会将 markword 偏向锁标识改成
当前线程 id,下次该线程访问只要判断标识位是否是当前线程,如果
10
是则自动获取锁,不需要跟重量级锁一样加锁解锁导致内核态和用户
态重复切换,当出现锁竞争时需要释放该锁。
(3)轻量级锁:如果 A 持有偏向锁,B 来抢锁失败了,会一直 cas
抢锁,此时锁升级为轻量级锁,a 线程也会由之前的偏向锁转为轻量
级锁,并且还是持有该锁。
(4)重量级锁:当竞争线程较多,并且 cas 自旋长时间抢不到锁会
升级为重量级锁。

32 Syschronized 修饰静态和非静态的区别

静态方法锁的是 Class 实例,非静态方法或属性锁的是对象的实例,
修饰代码块时锁的是传入的对象,即 this。

33 Redis 5 种数据结构

String、Hash、List、Set、Zset

34 Redis5 种数据结构你都使用了哪些,怎么用的

(1)String 用的最多,比如存登录用户的 token,还有项目中一些键
值对的地方
(2)Hash 结构这个也用过,比如文件推送平台,需要区分上游哪个
平台传送过来的文件,然后文件还要再一次区分是那种类型的文件。
或者针对用户手机唯一 id,大数据会给一个接口先判断用户是属于哪
种类型的用户,例如 4 种类型 A,B,C,D,我们平台还需要根据用户所
使用的手机类型做进一步区分,比如安卓和 ios,然后再给这个用户
推送不同的广告,后台会先配置 A,B,C,D 四种类型的用户,比如少年,
青年,中年,老年。这个最为 hash 的 key 区分最外层,然后内层的
11
map 再根据手机类型进行区分。
(3)Set 结构也用过,比如文件推送过了,我们可以把当天的文件名
保存进一个 set 集合,这样通过 set 的 sismember 命令可以快速判断。
或者把拉黑的用户 id 存入 set 集合中,用户进入首页直接判断是否在
set 集合中。
(4)Zset 也用过,比如我们平台会给用户 app 上面推广告,后台会
配置这个广告的运营时间,然后会把这个广告的点击数存在 zset 当做
是分数,公司后台会根据不同的时间投放点击数不同的广告,这个到
了时间我就会根据 zset 的 zrangebyscore 取的对应的广告进行投放。

35 这 5 种数据结构底层是什么数据结构有了解吗

(1)String:底层用的是动态字符串,有动态扩容的能力,如果字符
串小于 1M 扩容是字符串 2 倍+1,如果大于 1M,则为扩展后的字符串
长度+1M+1,加 1 是结束标识,在字符串长度不一样的时候还会采用不
同的编码格式加快查询效率。
(2)List:redis 最早用的是 ziplist(压缩链表),但是当元素个数过
多,它的查找效率就会降低。而且如果在 ziplist 里新增或修改数据,
ziplist 占用的内存空间还需要重新分配(3 0 版本及之后废弃)。
Redis 先是在 3 0 版本中设计实现了 quicklist。quicklist 结构在 ziplist
基础上,使用链表将 ziplist 串联起来,链表的每个元素就是一个ziplist。
这种设计减少了数据插入时内存空间的重新分配,并且 quicklist 限制
了每个节点上 ziplist 的大小,查询效率不会那么低。

Redis 在 5 0 版本中,又实现了 listpack 结构(没有完全替换,不深入
介绍,只要达 quicklist 即可)。
(3)Set :Dict 字典,值为键,value 是 null。
(4)Zset :当元素个数小于 128 并且每个元素小于 64 字节采用 ziplist
存储,来节省内存。其他情况采用 Dict 字典加跳表的数据结构,DictJI
检查键的唯一性,跳表实现快速排序和查找。
(5)HASH:默认采用 ziplist,ziplist 相连的两个节点保存 key 和 value。
当数据量大时,ziplist 会有查询效率问题,会转成 Dict 结构存储。

36 Redis 网络模型了解吗(了解即可,有点难)

我知道有 3 种 select,poll 和 epoll
都使用了 io 多路复用原理。
但是 Select 监听文件(linux 一切东西都是以文件形式表现)有上限,
还涉及不少内核拷贝,并且有事件就绪了需要遍历所有的文件找出就
绪的事件。
Poll 采用链表存储文件解决了 select 监听 select 监听文件上限的问题,
但是有事件发生依然需要遍历整个文件。
Epoll 采用红黑树解决了监听文件上限问题,并且加快了查询就绪事
件的效率,并且添加效率也很快。

37 Linux 基本指令

(1)ls:列出所有文件及文件夹。
(2)Pwd:找出当前所在的文件目录。
(3)ps-ef|grep 名称,找出这个应用进程。
(4)rm -rf 递归删除,
(5)cp -rf 递归复制。
(6)Chmod:给文件改权限。
(7)lsof -i:端口号 查看端口是否被占用。
(8)cat 文件名 | grep 关键字,从文件中查找该关键字的记录。
(9)tail -f 文件名查看文件里面的内容,实时打印。
(10)vi:编辑文件
(11)set nu:给文件标识行数。
(12)Linux 创建文件的几种方式:touch 文件名,vi 和 vim,echo。

38 Mysql 默认的隔离级别

可重复读

39 说下不可重复读和幻读的区别

不可重复读:在并发更新时,另一个事务前后查询数据不一样
幻读:删除或者新增产生数量变化时,另一事务修改或者删除发现影
响的行数不一样

40 知道什么是当前读和快照读吗

最普通的查询语句就是当前读,不加锁
加锁是快照度,比如 insert,delete,update,selet* from *** for update

41 Mysql 是怎么实现可重复读的

(1)快照读:基于 mvcc 多版本并发控制实现的,及 Undo Log +read
view 实现的,Undo Log 保存了历史快照,Read View 可见性规则帮助
判断当前版本的数据是否可见,当事务执行 SQL 语句前,会得到一个
14
Read View,可重复读隔离级别下,一个事务里只会获取一次 read view,
后面都是共用的,从而保证每次查询的数据都是一样的。
(2)当前读:基于行数加间隙锁实现的。

42 间隙锁了解吗?

间隙锁主要是在索引记录之间的间隙加锁,从而保证某个间隙内的数
据在锁定情况下不会发生任何变化

43 Mysql 如何排查死锁

可以修改 MySql 系统参数 innodb_print_all_deadlocks 设置成 1,
开启状态,这样当发生死锁时,死锁日志会记录到 MySQL 的错误日
志文件中。
也可以查看线上的服务器日志

44 Mysql 有哪几种日志文件

我知道的主要有 6 种日志,重做日志(redo log)、回滚日志(undo
log)、二进制日志(binlog)、错误日志(errorlog)、慢查询日志(slow
query log)、一般查询日志(general log)

45 都有什么作用

(1)binlog 是 MySQ 服务层维护的一种二进制日志,主要做主
从复制、数据恢复和备份;
(2)undo log innerdb 储存引擎层面的日志,提供回滚和多版本
并发控制下的读(MVCC);
(3)redo log 数据备份和数据提交;
(4)errorlog mysql 服务器执行错误时记录进这个错误日志;
15
(5)slow query log mysql 开启了慢查询,慢 sql 会写入这里;
(6)general log 记录所有的操作日志一般不开启,耗费数据库
性能。

46 redo log 与 binlog 的区别

(1)redo log 是在 InnoDB 存储引擎层产生,而 binlog 是 MySQL
数据库的上层产生的
(2)写入磁盘的时间点不同,binlog 在事务提交完成后进行一
次写入。而 redo log 在事务进行中不断地被写入
(3)binlog 在写满或者重启之后,会生成新的 binlog 文件,redo
log 是循环使用

47 Mysql 锁你了解几种(都有什么作用)

主要了解 4 种
(1)行锁 锁定一行,锁的是索引,解决当多个线程对数据库进
行操作时,会带来数据不一致的情况,会有死锁情况;
(2)表锁 锁定整张表,也是防止解决当多个线程对数据库进行
操作时,会带来数据不一致的情况,不会有死锁;
(3)间隙锁 只有在可重复读的情况下才会有可能产生此锁,可
以避免幻读,锁的是索引的一段间隔,会有死锁情况;
(4)意向锁,是一种表级锁,与行锁可以同时存在,目的是防
止加表锁时需要全表扫描有没有行锁,不会有死锁情况。

48 什么是索引下推

在 mysql5.6 之前,没有索引下推,比如建立了一个联合索引,先会从第一个索引里面找到合适的数据,再回表查,再过滤,造成多次回表。
而 5.6 之后有引入索引下推,主要直接就可以根据两个联合索引
过滤出需要的数据,再回表,减少了回表查的次数。

49 Springmvc 的请求流程

(1)请求先到前端控制器 DispatcherServlet,如果有过滤器先到过滤
器。
(2)然后 DispatcherServlet 会根据请求的 url 找到合适的控制器
(3)然后控制器调用 service 调用 dao 处理业务逻辑
(4)最后会返回一个 ModelAndView
(5)DispatcherServlet 渲染 ModelAndView 最后展示在页面

50 过滤器,拦截器,DispatcherServlet 执行顺序是什么样的

先到过滤器再到 DispatcherServlet 再到拦截器

51 Spring 三级缓存存的都是什么对象

一级,保存的都是初始化后的 Bean
二级,还没进行属性注入,经过三级缓存处理可能是原对象或代理对)
三级,存放一个对象工厂,和 lambda 表达式,里面保存的都是刚实
例化的对象

52 你在开发中哪些地方用到了反射

文件交换平台,因为对文件操作的步骤配置都不相同,这个我是用了
一个枚举,然后枚举第一值是后台配的文件操作步奏具体的 code,
第二个值为文件操作类的包名类名,比如 1 代表解压,2 代表转 A 码,
3 代码压缩,则后台配置,1,2,3,这样就可以直接通过这个后台配
置的 code 找到找到类的包名类名通过反射生成对象进行具体操作。

53 那你这样写的好处是什么?

提高了程序的灵活性和扩展性,因为再写一个类我只要配置一个枚举
就行了,不用在代码里面到处 new 新的对象。

54 Java new 一个对象要做哪些事

(1)在堆区分配对象需要的内存
(2)对所有实例变量赋默认值
(3)执行实例初始化代码
(4)Spring 通过三级缓存解决循环依赖问题(较难,不过多描述,
可以看视频理解,尚硅谷周瑜的手写 spring,很详细,还有教你如何
处理)

java常规题

JAVA的基本数据类型:

四个整数类型:byte,short, int, lang
两个浮点数类型:float,double
一个布尔值:boolean
一个字符类型:char

面向对象三大特性

封装:将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问。

继承:是类与类的一种关系,是一种“is a”的关系。比如“狗”继承“动物”,这里动物类是狗类的父类或者基类,狗类是动物类的子类或者派生类。
子类拥有父类的所有属性和方法(除了private修饰的属性不能拥有)从而实现了实现代码的复用;
Java中的继承是单继承,即一个类只有一个父类。

多态:就是一个方法可以根据发送对象的不同而采用多种不同的行为方式。
一个对象的实际类型是确定的,但可以指向对象的引用类型有很多。

多态存在的条件:
1,有继承关系
2,子类重写父类的方法
3,父类引用指向子类的对象

String str=new String(“abc”);创建了几个对象

我们可以把上面这行代码分成String str、=、"abc"和new String()四部分来看待。String str只是定义了一个名为str的String类型的变量,因此它并没有创建对象;=是对变量str进行初始化,将某个对象的引用(或者叫句柄)赋值给 它,显然也没有创建对象;现在只剩下new String(“abc”)了。那么,new String(“abc”)为什么又能被看成"abc"和new String()呢?

我们来看一下被我们调用了的String的构造器:

public String(String original) { //other code … } 大家都知道,我们常用的创建一个类的实例(对象)的方法有以下两种:

一、使用new创建对象。

二、调用Class类的newInstance方法,利用反射机制创建对象。

我们正是使用new调用了String类的上 面那个构造器方法创建了一个对象,并将它的引用赋值给了str变量。同时我们注意到,被调用的构造器方法接受的参数也是一个String对象,这个对象正 是"abc"。由此我们又要引入另外一种创建String对象的方式的讨论——引号内包含文本。

这种方式是String特有的,并且它与new的方式存在很大区别。

String str=“abc”;

毫无疑问,这行代码创建了一个String对象。

String a=“abc”; String b=“abc”; 那这里呢?

答案还是一个。

String a=“ab”+“cd”; 再看看这里呢?

答案是三个。
说到这里,我们就需要引入对字符串池相关知识的回顾了。

在JAVA虚拟机(JVM)中存在着一个字符 串池,其中保存着很多String对象,并且可以被共享使用,因此它提高了效率。由于String类是final的,它的值一经创建就不可改变,因此我们 不用担心String对象共享而带来程序的混乱。字符串池由String类维护,我们可以调用intern()方法来访问字符串池。

我们再回头看看String a=“abc”;,这行代码被执行的时候,JAVA虚拟机首先在字符串池中查找是否已经存在了值为"abc"的这么一个对象,它的判断依据是String 类equals(Object obj)方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用;如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返 回。因此,我们不难理解前面三个例子中头两个例子为什么是这个答案了。

只有使用引号包含文本的方式创建的 String对象之间使用“+”连接产生的新对象才会被加入字符串池中。对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的 新对象都不会被加入字符串池中,对此我们不再赘述。因此我们提倡大家用引号包含文本的方式来创建String对象以提高效率,实际上这也是我们在编程中常 采用的。

i++和++i有什么区别

1,i++返回原来的值,++i返回加1后的值

二、 i++ 不能作为左值,而++i 可以。

JDK和JRE有什么区别?

JRE 是java运行环境,他是运行编译后的ajva程序所必需的一切包,包括java虚拟机(JVM),java基础类库,java命令和其他基础设施,但是他不能用于创建新程序。
JDK是java开发工具包,功能齐全的SDKforjava,它拥有JRE所拥有的一切,还包含了java源码的编译器文档和demo例子程序,他能够创建和编译程序是提供给程序员使用的。

==和equals的区别

1.功能不同
==是判断两个变量或实例是不是指向同一个内存地址的值,
equals是判断两个变量或实例指向的内存空间的值是不是相同的。
2,定义不同
equals在java中是一个方法
=在java中只是一个运算符

为什么重写equals还要重写hashcode?

Object 的 hashcode 方法是本地方法,也就是用 c 或 c++ 实现的,该方法直接返回对象的内存地址。
如果没有重写hashCode(),则任何对象的hashCode()值都不相等(而hashmap想让部分值的hashCode值一样,所以就要重写)

没有重写hashCode(),则任何对象的hashCode()值都不相等。
HashMap中的比较key是这样的,先求出key的hashcode(),比较其值是否相等,若相等再比较equals(),若相等则认为他们是相等的。若equals()不相等则认为他们不相等。
如果只重写equals没有重写hashCode(),就会导致相同的key值也被hashcode认为是不同的key值(因为没有重写hashCode(),则任何对象的hashCode()值都不相等),就会在hashmap中存储相同的key值(map中key值不能相同),这就不符合条件了。

final在java中有什么作用?

当用final修饰一个类时,表明这个类不能被继承,也就是说,如果一个类你永远不会让你被继承,就可以使用final修饰。
final写在方法上时,表示这个方法不能被重写
final写在变量上,表示是常量,值不能再次被改变

final的原理

相信大家都具备基本的常识: 被final修饰的变量是不能够被改变的. 但是这里的"不能够被改变"对于不同的数据类型是有不同的含义的.
当final修饰的是一个基本数据类型数据时, 这个数据的值在初始化后将不能被改变; 当final修饰的是一个引用类型数据时, 也就是修饰一个对象时, 引用在初始化后将永远指向一个内存地址, 不可修改. 但是该内存地址中保存的对象信息, 是可以进行修改的.

final修饰的变量会指向一块固定的内存, 这块内存中的值不能改变.

java中的 Math.round(-1.5)等于多少?

Math的round方法是四舍五入,如果参数是负数,则往大的数如Math.round(-1.5)=-1。

如果使用了无界队列,那么会有什么问题?

如果使用无界队列,那么可能会导致OOM甚至宕机

String,StringBuffer,StringBuilder之间的区别

String是final修饰的,不可变,每次操作都会产生新的String对象
StringBuffer和StringBuilder都是在原对象上操作
StringBuffer是线程安全的,StringBuilder是线程不安全的
StringBuffer方法都是synchronized修饰的
性能:StringBuilder>StringBuffer >String

使用场景:经常需要改变字符串内容时使用后面两个
优先使用StringBuilder,多线程使用共享变量是使用StringBuffer

重载和重写的区别

重载:发生在同一个类中,方法名必须相同,参数类型不同,个数不同,顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写:发生在父子类中,方法名,参数列表必须相同,返回值范围小于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法。

接口和抽象类的区别

抽象类可以存着普通成员函数,而接口中只能存着public abstract方法。
抽象类汇总的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。
抽象类只能继承一个,接口可以实现多个。

接口的设计目的,是对类的行为进行约束(更准确的说是一个(有)约束,因为接口不能规定类不可以有什么行为,也就是提供一种机制,可以强制要求不同的类具有相同的行为。它值约束了行为的有无,但不对如果实现行为进行限制)。

而抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为(记为行为集合A)。且其中一部分行为的实现方式一致时(A的非真子类,记为B),可以让这些类都派生于一个抽象类,在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的,而A见B得部分,留给各个子类自己实现。正是因为A-B在这里没有实现,所有抽象类不允许实例化出来(否则当调用到A-B时,无法执行)

抽象类是对类本质的抽象,表达的是 is a 的关系,
比如BMW is a Car。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。

而接口是对行为的抽象,表达的是like a 的关系。
比如: Bird like a Aircraft (鸟像飞行器一样可以飞) ,
但其本质上 is a Bird。 接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁,是如果实现的,接口并不关心

使用场景:当你关注一个事务的本质的时候,用抽象类;当你关注一个操作的时候。用接口。

抽象类的功能要远超过接口,但是定义抽象类的代价高,因为高级语言来说(从实际设计上来说也是) 每个类只能继承一个类,在这个类中。你必须继承或编写出其所有子类的所有共性。虽然接口在功能上回弱化许多,但是它只针对一个动作的描述,而且你可以在一个类中同时实现多个接口。在设计阶段会降低难度。

CLASS文件的加载过程?

类加载过程主要分三步:

分别是加载>连接>初始化

加载:把java的字节码加载到JVM内存中,并映射成JVM认可的数据结构。

连接:分为三个小阶段:1,验证;检查加载到的字节信息是否符合JVM规范。
2,准备:创建类或接口的静态变量,并赋初始值 半初始化状态。
3:解析:把符号引用转为直接引用

最后进行初始化。

List和Set的区别

List: 有序,按照对象进入的顺序保存对象,可重复,允许多个null元素对象,可以使用Iterator取出所有的元素,在逐一遍历,还可以使用get(int index)获取指定下标的元素。
Set:无序,不可重复,最多允许有一个null元素对象,取元素时只能用Iterator接口取得所有的元素,在逐一遍历各个元素。

hashCode与equals的区别

hashCode介绍:hashCode()的作用是获取哈希码,也称为散列码;它实际上是返回一个int的整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在Object.java中,java中的任何类都包含hashCode()函数。
散列表储存的是键值对(key-value),它的特点是:能根据(键)快速的检索出对应的值。这其中就利用到了散列码!(可以加速找到对象)

为什么要有hashCode

以HashSet如果检查重复为例子来说明为什么要有hashCode:
对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,看该位置是否有值,如果没有,HashSet会假设对象没有重复出现。但是如果发现有值,这时就会调用equals()方法来检查两个对象是否真的相同。如果两者相同,hashSet就不会让其加入操作成功,如果不同的话,就会重新散列到其他位置,这样就大大减少了equals的次数,相应就大大提高了执行速度。

如果两个对象相等,则hashcode一定也是相同的。
两个对象相等,对两个对象分别调用equals方法都返回true
两个对象有相同的hashcode值,它们也不一定是相同的
因此,equals方法被覆盖过,则hashCode方法也必须覆盖
hashCode()的默认行为是对堆上的对象产生独特值,如果没有重新hashCode(),则该class的两个对象无论如果都不会相等(即使这两个对象指向相同的数据)

ArrayList和LinkedList区别

ArrayList: 基于动态数组,连续的内存存储,适合下标访问(随机访问),扩容机制:因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能,甚至超过linkedList(需要创建大量的node对象)

LinkedList:基于链表,可以储存在分散的内存中,适合做数据插入及删除操作,不适合查询。需要逐一遍历。
遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体内通过get(i)取得某元素时都需要对list重新进行遍历,性能消耗极大。

ConcurrentHashMap源码解析

当map进行put的时候首先会用CAS的方式去查看当前数组是否为空,如果为空则进行初始化。
如果不为空,则根据hash得到数组下标,查看数组的第一个元素是否为空,如果为空则直接存储。
如果不为空, 则同时它还会用CAS的方式查看当前数组是否正在扩容,如果在扩容则协助扩容,如果没有则使用synchronized的方式锁住hash槽,也就是代表的链表,然后使用尾插法插入。

HashMap和HashTable有什么区别?其底层实现原理是什么?

区别:
(1)HashMap方法没有synchronized修饰,线程非安全,
HashTable线程安全;
(2)HashMap允许key和value为null,而HashTable不允许

底层实现:数组+链表实现
jdk8开始链表高度到8,数组长度超过64,链表转变为红黑 树,元素以内部类Node节点存在。
(1)计算key的hash值,二次hash然后对数组长度取模,对应到数组下标,
(2)如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组。
(3)如果产生hash冲突,先进行equal比较,相同则取代元素,不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表
(4)key为null,存在下标0位置。
数组扩容,hashMap在key存储达到上限时会进行扩大数组容量的数组扩容。

线程安全的HashMap:ConcurrentHashMap

jkd7:
数据结构:ReentrantLock+Segment+HashEntry,volatile
一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构。
元素查询:二次hash,第一次Hash定位到Segment,第二次Hash定位到元素所在链表头部
锁:Segment分段锁,Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响,并发度get方法无需加锁,volatile保证。

jdk8:
数据结构:synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性。
查找,替换,赋值操作都使用CAS。
锁:锁链表的head节点,不影响其他元素的读写,细粒度更细,效率更高,扩容时,阻塞所以的读写操作,并发扩容。
读操作无锁:
Node的val和next使用volatile修饰,读写线程对该变量互相可见
数组用volatile修饰,保证扩容时被线程感知。

如果实现一个ioc容器

1.配置文件配置包扫描路径
2,递归包扫描获取的.class文件
3,反射,确定需要交给ioc管理的类
4,对需要注入的类进行依赖管理

更详细:

(1)配置文件中需要指定扫描的包路径
(2)定义一些注解,分别表示访问控制层,业务服务层,数据持久层,依赖注入注解,获取配置文件注解。
(3)从配置文件中获取需要扫描的包路径,获取到当前路径下的文件信息及文件夹信息,我们将当前路径下所有已.class结尾的文件添加到一个Set集合中进行储存。
(4)遍历这个Set集合,获取在类上有指定注解的类,并将其交给IOC容器,定义一个安全的Map用来储存这些对象。
(5)遍历这个容器,获取到每一个类的实例,判断里面是否有依赖其他类的实例,然后进行递归注入。

什么是字节码?他的好处是什么

java中的编译器和解释器:
java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特点系统的机器猫执行,在java中,这个供虚拟机理解的代码叫作字节码(即扩展名为.class的文件),它不面向任何特点的处理器,只面向虚拟机。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的。
java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行。虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特点机器上的机器码,然后在特点的机器上运行,这也就是解释了java的编译和解释并存的特点。

java源代码——>编译器------>jvm可执行的字节码(虚拟指令)——>jvm—>jvm中的解释器---->机器可执行的二进制机器码—>程序运行

采用字节码的好处:
java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以java程序运行时比较高效,而且,由于字节码不专针对一个特点的机器,因此,java程序无需重新编译便可在多种不同的计算机上运行。

JAVA类加载器

JDK自带有三个类加载器:bootstrap ClassLoader,ExtClassLoader,AppClassLoader。
BootStrapClassLoader是ExtClassLoader的父类加载器,默认加载%JAVA_HOME%lib下的jir包和class文件。
ExtClassLoader是AppClassLoader的父类加载器,
负责加载%JAVA_HOME%lib/ext文件夹下的jar包和class类。

AppClassLoader是自定义类加载器的父类,负责加载classpath下的;类文件。系统类加载器,线程上下文加载器
继承ClassLoader实现自定义类加载器。

双亲委派机制

当类加载器向上发起委派,实际上是向上层类加载器查找缓存,是否加载了该类,有则直接返回,没有继续向上。
达到顶层BootStrapClassLoader后如果没有找到则向下查找,查找加载路径,有则加载返回,没有则继续向下查找,直到回到发起请求的类加载器。

双亲委派的模型的好处:
主要是为了安全性,避免用户自己编写的类动态替换java的一些核心类,比如String
同时也避免了类的重复加载,因为JVM中区分不同类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是两个不同的类。

java中的异常体系

java中的所有异常都来自顶级父类Throwable
Throwable下有两个子类Exception和Error。
Error是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行。
Exception不会导致程序停止,又分为两部分RunTimeException运行时异常和CheckedException检查异常。
RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。CheckedException常常发生在程序编译过程中,会导致程序编译不通过。

JVM内存结构

JVM的内存模型中分别有:
堆:堆中存放由new创建的对象和数组类变量

(statis修饰的变量),程序在一加载的时候就在堆中为类变量分配内存,堆中的内存地址存放在栈中

实例变量:当你使用java关键字new的时候,系统在堆中开辟并不一定是连续的空间分配给变量,是根据零散的堆内存地址,通过哈希算法换算为一长串数字以保证这个变量在堆中的‘物理位置’,实例变量的生命周期,当实例变量的引用丢失后,就被GC列入可回收名单,但并不是马上被释放堆中内存。

局部变量:由声明在某方法,或某代码段里(比如for循环)执行到它的时候在栈中开辟内存,当局部变量一旦脱离作用域,内存立即释放。

虚拟机栈:用来保存方法帧和局部变量,而对象总是在堆上分配。栈通常都比堆小,也不会在多线程之间分享,而堆被整个JVM的所有线程共享。
元空间:
本地方法栈:本地方法区主要存储C和C#的本地方法
程序计数器:每个线程都有一个程序计数器,是线程私有的,就是一个指针指向方法区中的方法字节码(用来存储指向类似于一条指令的地址,也即将要执行的指令代码),用于执行引擎读取下一条指令。它是一块非常狭小的空间。

类加载机制

类加载机制就是,虚拟机把Class文件加载到内存,并对数据进行校验,转换解析和初始化形成虚拟机可以直接使用的java类型,即java.lang.Class

主要有三个步骤:

一,装载,查找和导入Class文件,

(1)通过一个类的全限定名获取此类的二进制字节流
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3) 在java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
在我们的装载阶段完成之后,这个时候在我们的内存当中,我们的运行时数据区的方法区以及堆已经有数据了。

方法区中: 类信息,静态变量,常量
堆 中: 代表被加载类的java.lang.Class对象

即时编译之后的热点代码并不在这个阶段进入方法区。

二, 连接阶段

     (1) 验证,确保Class文件中的字节流包含的信息完全符合当前虚拟机的要求,并且还要去我们的信息不会危害虚拟机自身的安全,导致虚拟机的崩溃。
      (2)    准备,为类的静态变量分配内存,并将其初始化为默认值
      ![在这里插入图片描述](https://img-blog.csdnimg.cn/6697e9233fd44413b42cb8014cfb9340.png)

不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
这里不会为实例变量(也就是没加static关键字)分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到java堆中。

在这里插入图片描述

进行分配内存的只是包括类变量(静态变量),而不包括实例变量,实例变量是在对象实例化时随着对象一起分配在java堆中的,通常情况下,初始值为零值,假如public static int a = 1;那么a在准备阶段过后的初始值为0.不为1,这个时候只是开辟了内存空间,并没有执行java代码,a赋值为1的指令是程序被编译后,存放于类构造器()方法之中,所以a被赋值Wie1是在初始化阶段才会执行。
(3) 解析, 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用限定符7类符号引用进行。

三,初始化

初始化阶段是执行类构造器()方法 的过程

在准备阶段,类变量已赋值过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化变量和其他资源,比如赋值。

JVM初始化步骤:
假如这个类还没有被加载和连接,则程序先加载并连接该类
假如该类的直接父类还没有被初始化,则先初始化其直接父类
假如类中有初始化语句,则系统依次执行这些初始化语句

为什么新生代和老年代的比例是8:1:1

因为新生代的小型对象比较容易死亡,而老年代的对象会存活的时间较长,所以多分配空间给老年代。

谈一下你对volatile的理解

volatile关键字能让内存模型中的变量变为可见性,并且能够通过内存屏障防止CPU的指令重排。
CPU的指令重排序是指在单线程的情况下不管先执行那条指令都能得到最终一致性的结果,因此为了优化性能便不等待某一指令执行完成。而是先执行下一指令从而使得性能得到提升。但是在多线程情况下如果进行指令重排那么就会出现最终一致性不能保证的问题。

volatile的可见性和禁止指令重排序是如果实现的

volatile的禁止指令重排和可见性都是通过JVM提供的内存屏障保证的。
在这里插入图片描述

调用底层 lock; addl汇编指令锁定一个空值,从而实现了可见性和禁止指令重排。
Lock用于在多处理器中执行指令时对共享内存的独占使用,它的作用是能够将当前处理器对应缓存的内存刷新到内存,并使其它处理器对应的缓存失效,另外还提供了有序的指令无法越过这个内存屏障的作用。

描述一下对象的创建过程

在这里插入图片描述

首先通过new #2 在堆内存中申请一块根据数据类型大小的空间,
在这个过程中m的会有一个半初始化的过程这个时候他的默认值为0
只有当调用了invokespecial #3 <T.>构造方法后才将T变量值为8赋予
并执行astore_1与T建立连接。

对象在内存中的布局

在这里插入图片描述
markword对象头占据8个字节。储存了锁信息,GC标记信息,hashCode

在这里插入图片描述

klass pointer 类指向 默认压缩后是4个字节,未压缩是8个字节
成员变量占据数据类型对应字节。
padding为确保在64位虚拟机中都是能被8整除的字节,使其填充相应的字节以对应64位虚拟机。

用户态与内核态

内核态是指操作系统与硬件之间的联系;
用户态是指应用的操作,用户态不能直接访问硬件,比较经由内存态处理
但是内存态需要消耗大量资源,所以在尽量在用户态解决问题。

对象怎么分配?

当一个对象创建出来的时候会首先分配到栈,如果不能分配在栈上那么会根据它的大小来考虑是否分配到老年代,如果不大那么就会分配到线程的本地缓存TLAB里,他会生成给每一个线程一个独立的内存空间,只有当内存空间满时才会去申请新生代的内存空间

什么对象会进入老年代?

1,躲过15此minor gc之后就会进入老年代

2,动态对象年龄判断
假如说当前放对象的Survivor区域里一批对象的 总大小大于了这块Survivor区域的内存大小的50% ,那么此时大于等于这批对象年龄的对象,就可以直接进入老年代了

另外我们要理清楚一个概念,这个实际这个规则运行的时候是如下的逻辑:年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n以上的对象都放入老年代

在没有回收的情况下 所有对象存活着
比如说一块s区块 100M 如果第一次有20M 不到老年代 第二次来了51M存活 如果之前的20M全部存活,那么这51M和20M将全部到老年代

另外一种情况 在这3次的对象都持续引用,不能回收的情况下,比如说一块s区块 100M 如果第一次有10M 不到老年代 第二次20M 第三次 31M 那么就会由于20+31>50了 那么第一次的10M就会到老年代了。

3,大对象直接进入老年代

4,MinorGC后的对象太多无法放入Survivor区
如果在Minor GC之后发现剩余的存活对象太多了,没办法放入另外一块Survivor,那么这个时候就必须得把这些对象直接转移到老年代中去

5,老年代空间分配担保规则
在执行任何一次Minor GC之前,JVM会检查一下老年代可用的可用内存空间,是否大于新生代所有对象的总大小

为啥会检查这个呢?因为最极端的情况下,可能新生代的Minor GC过后,所有对象都存活下来了,那岂不是新生代所有对象全部都要进入老年代?

如果说发现老年代的内存大小是大于新生代所有对象的,此时就可以放心大胆的对新生代发起一次Minor GC了,也可以转移到老年代去。

但是假如执行Minor GC之前,发现老年代的可用内存已经小于了新生代的全部对象大小了,那么这个时候是不是有可能在Minor GC之后新生代的对象全部存活下来,然后全部需要转移到老年代去,但是老年代空间又不够?

所以假如Minor Gc之前,发现老年代的可用内存已经小于看新生代的全部对象大小了,就会看一个-XX:-HandlePromotionFailure的参数是否设置了,如果有这个参数,那么就会继续进行下一步判断,

下一步判断,就是看老年代的内存大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小。

举个例子,之前每次Minor GC后,平均都有10MB左右的对象会进入老年代,那么此时老年代可用内存大于10MB

这就说明很可能这次Minor GC过后也是差不多10MB左右的对象会进入老年代,此时老年代空间是够的

如果上面那个步骤判断失败了,或者是 -XX:-HandlePromotionFailure“参数没设置,此时就会直接触发一次Full GC,就是对老年代进行垃圾回收,尽量腾出来一些内存空间,然后再执行Minor GC

如果上面2个步骤都判断成功了,那么就是说可以冒点风险尝试一下Minor GC 此时进行Minor GC,此时进行Minor GC有几种可能:

(1)Minor GC过后,剩余的存活对象的大小,是小于Survivor区的大小的,那么此时存活对象进入Survicor区域即可

(2)Minor GC过后,剩余的存活对象的大小是大于Survivor区域的大小,但是是小于老年代可用内存大小的,此时就直接进入老年代即可

(3)Minor GC过后,剩余的存活对象的大小,大于了Survivor区域的大小,也大于了老年代可用内存的大小,此时老年代都放不下这些存活对象了,就会发生Handle Promotion Failure的情况,这个时候就会触发一次Full GC

Full GC就是对老年代进行垃圾回收,同时也一般会对新生代进行垃圾回收。

因为这个时候必须把老年代理的没人引用的对象给回收掉,然后才可能让Minor GC过后剩余的存活对象进入老年代里面

如果要Full GC过后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时就会导致所谓的OOM内存溢出了

CMS收集器和G1收集器的区别

区别一:使用的范围不一样
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用。
G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用。

区别二:使用的算法不一样
CMS收集器是使用“标记-清除”算法进行的垃圾回收。
G1收集器使用的是“标记-整理”算法进行的垃圾回收。

区别三:CMS收集器和G1收集器的优劣性
CMS收集器以最小的停顿时间为目标的收集器,容易产生内存碎片。
G1收集器不会产生内存碎片。

区别四:垃圾回收的过程不一样
CMS收集器:初始标记→并发标记→重新标记→标记清除
G1收集器:初始标记→并发标记→最终标记→筛选回收

CMS垃圾收集器

优点:并发收集,低停顿
缺点,会产生大量空间碎片

采用的是标记,清除算法,整个过程分为4个步骤,
(1)初始标记
(2)并发标记
(3) 重新标记
(4)并发清除

GC如果想查找到存活的对象,根据GCRoot可达分析算法 根据GCRoot引用链遍历存活对象。根据GCRoot遍历过程中, 按照是否访问过该对象 分为三种不同颜色。

白色:本对象没有访问过(没有被GCRoot扫描过) (有可能是垃圾对象):

灰色: 本对象已经被访问过(被GCRoot扫描过)且本对象的所有属性没有访问过;本对象所有属性都访问过后,本对象由灰色变为黑色

黑色: 本对象已经被访问过(被GCRoot扫描过),且本对象的所有属性都被访问过
在这里插入图片描述
(1)初始标记时,所有对象都在白色容器中;
(2)并发标记时(用户线程与GC线程同时运行),将本对象引用到的其他对象移动灰色容器中,如果本对象没有引用到其他对象或者其他对象已经标记过,则该对象放到黑色容器中。
(3)重复以上这些操作,到灰色容器为空时,则停止。

(4)结束后,如果在白色容器中任然存在的对象,则认为就是与GCRoot没有直接关联,则认为就是为不可达对象,可以被垃圾回收线程清理。

三色标记为什么存在漏标问题

在并发标记阶段会出现漏标问题, 由于是GC线程和用户线程同时运行的, 参考下图模型: 在这里插入图片描述
当遍历的C对象的时候,C对象已经存放到灰色容器中,突然用户线程修改C对象属性.E=Null; E对象与C对象断开,则E对象为垃圾对象,但是用户线程修改B对象的属性=E对象,在进行最终标记阶段时B对象已经为黑色不会继续遍历B对象,就会导致E对象会被垃圾线程清理,这个过程称作为漏标问题。

C.属性E=null;B.属性E=E;

漏标的问题满足两个条件:

  1. 至少有一个黑色对象指向了白色对象

  2. 所有灰色对象扫描完整个链时,删除之前所有的白色对象。

参考上图"漏标问题-模型",可能出现以下两种情况:

在并发标记阶段的时候, 用户线程与GC线程同时运行;

条件: GCRoot遍历到C节点, C节点变为灰色, B节点变为黑色

情况1: GC线程先执行, 扫描到E对象, 为可达对象, 没有问题 (不会产生漏标问题)

情况2: 1.用户线程执行灰色对象C.E=null

       2.GCRoot线程在执行的时候无法扫描到E对象,E对象是一个垃圾对象  

       3.用户线程执行黑色对象B.E=E (导致漏标问题)

漏标解决方案

CMS收集器中处理漏标问题(增量更新)

  • 简单说就是会重新扫描黑色对象的整个引用链。(关联了白色对象的黑色对象)

满足了第一个条件(灰色对象不在关联白色对象的时候,当黑色对象如果关联了白色对象的时候会记录该黑色对象,

然后在重新标记的时候,将记录的黑色对象变为灰色,从新开始修正标记,但是这种方案能够确保垃圾都被清理,缺点就是效率非常低,

因为会扫描到整个黑色对象所有引用。

  • 好处: 不会产生浮动垃圾

  • 坏处: 会扫描整个引用链, 比较耗时.

G1收集器中处理漏标问题 (原始快照SATB)

  • 原始快照简单理解为备份 (备份被断开引用的白色对象)

如果用户线程在灰色C对象断开一个白色E对象的时候, 会记录原始快照(断开的白色E对象),

在重新标记阶段时候比白色E对象变为灰色为起始点扫描整个链.

  • 好处: B黑色对象引用了E灰色对象, 则直接将E灰色变为黑色, 不用遍历整个引用链.

  • 坏处: E灰色对象如果为垃圾对象, 则此次垃圾回收不会被清除掉(浮动垃圾), 只能下次清除.

G1和CMS漏标处理对比

G1采用原始快照 - 会产生浮动垃圾, 不会扫描整个引用链, 效率快

CMS采用增量更新 - 不会产生浮动垃圾, 会扫描整个引用链,效率慢,

相对于来说原始快照方式比增量更新方式容易产生浮动垃圾,但是执行效率比增量更新要高。

GC如何判断对象可以被回收

(1)引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时进行回收。
(2)可达性分析法,从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots 没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。
引用计数法,可能会出现A引用了B,B又引用了A,这个时候就算他们都不在使用了,但因为相互引用计数器=1,导致永远无法被回收。

GC Roots的对象有
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)所引用的对象

可达性算法的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是在虚拟机自动建立Finalizer队列中判断是否需要执行finalize()方法。

当对象变成(GC Roots)不可达时,GC会判断该 对象是否覆盖了finalize方法,若未覆盖,则直接将其回收,若对象未执行过finalize方法,则将其放入F-Queue队列,由低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”
每个对象只能触发一次finalize()方法
由于finalize()方法运行代价昂贵,不确定性大,无法保证各个对象的调用顺序,不推荐使用,建议遗忘。

实现线程的几种方式?

1,继承Thread类重写run方法

2,实现Runnable接口

3,实现Callable接口
观察上文两种线程的执行方式,存在什么缺点。显然,以上两种线程的执行un方法时是没有返回值的,而实际上也会存在需要得到线程执行的返回结果的情况,那么怎么办呢?这时就可以考虑使用第三种线程的实现方式。

优点:可以获取到线程的执行结果。
缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。

线程的生命周期?线程有几种状态?

线程通常有五种状态,创建,就绪,运行,阻塞和死亡状态。

阻塞的情况又分为三种;
(1)等待阻塞:运行的线程执行wait方法,该线程会释放所占用的所有资源,JVM会把该线程放入“等待池”。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒,wais是object类的方法。
(2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
(3)其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程设置为阻塞状态。当sleep状态超时,join等待线程终止或者超时,或者I/O处理完毕时,线程重新转入就绪状态。sleep是Thread类的方法。

(1)新建状态(New):新创建了一个线程对象
(2)就绪状态(Runnable):线程对象创建后,其他的线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行。等待CPU的使用权。
(3)运行状态(Running):就绪状态的线程获取了CPU使用权,执行程序代码。
(4)阻塞状态(Blocker):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
(5)死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束了生命周期。

多线程中sleep(),wait(),join(),yield()的区别

1.锁池
所以需要竞争同步锁的线程都会放在锁池当众,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后悔进入就绪队列进行等待CPU资源分配
2.等待池
当我们调用wais()方法后,线程会放到等待池中,等待池的线程是不会去竞争同步锁的,只有调用了notify()或者notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放到锁池,而notifyAll()是将等待池的所有线程放到锁池当中。

sleep是Thread类的静态本地方法,wais则是Object类的本地方法。
sleep方法不会释放lock,但是wait会释放,而且会加入等待队列中。

sleep就是把CPU的执行资格和执行权释放出去,不在运行此线程,当定时时间结束在取回CPU资源,参加CPU的调度,获取到CPU资源后就可以继续运行了,如果sleep时该线程有锁,那么sleep不会释放这个锁,而是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁。也就是说无法执行程序。如果在睡眠期间其他线程调用了这个线程的interrupt方法,那么这个线程也会抛出interruptexception异常返回,这点跟wait是一样的。

sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
sleep不需要被唤醒(休眠之后退出阻塞),但是wait需要(不指定时间需要被别人中断)。
sleep一般用于当前线程休眠,或者轮询暂停操作,wait则多用于多线程之间的通信。
sleep会让出CPU执行时间切强制上下文切换,而wait则不一定,wait后可能还是有机会重新竞争到锁继续执行。

yield(执行后线程之间进入就绪状态,马上释放了CPU的执行权,但是依然保留了CPU的执行资格,所以有可能CPU下次进行线程调度还会让这个线程获取到执行权继续执行。
join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那么线程B会进入阻塞队列,这道线程A结束或中断线程。

public static void main(String[] args)throws InterruptedException{
     Thread ti =new Thread(new Runnable(){
     @Override
     public void run(){
       try{
          Thread.sleep(3000);
          } catch (InterruptedException e) {
             e.printStackTrace();
             }
             System.out.println("222222222")
       }
     });
 
      t1.start();
      t1.join();
      System.out.println("1111");

}

返回结果:222222222
1111

死锁产生的 4 个必要条件

①互斥: 某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进
程就不能再访问,直到该进程访问结束。
②占有且等待: 一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正
在等待其他进程释放该资源。
③不可抢占: 别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人
的资源抢过来。
④ 循环等待: 存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资
源。

线程安全

不是线程安全,应该是内存安全,堆是共享内存,可以被所以线程访问。

当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的。(在多线程运行状态下能够获得与单线程运行一致的结果)

是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。
堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。

是每个线程独有的,保存期运行状态和局部自动变量的,栈在线程开始的时候初始化,每个线程的栈相互独立,因此栈是线程安全的,操作系统在切换线程的时候回自动切换栈,栈空间不需要在高级语言里面显式的分配和释放。

目前主流操作系统都是多任务的,即多个进程同时运行,为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程,这是由操作系统保障的。
在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存) 。进程内所有线程都可以访问到该区域,这就是操作问题的潜在原因。

ThreadLocal的原理和使用场景

每一个Thread对象均包含有一个ThreadLocalMap类型的成员变量threadLocal,它储存本线程中所有ThreadLocal对象及其对应的值。
ThreadLocalMap由一个个Entry对象构成
Entry继承自WeakReference<ThreadLocal<?>>,一个Entry由ThreadLocal对象和Object构成,由此可见,Entry的key是ThreadLocal对象,并且是一个弱引用。当没有指向key的强引用后,该key就会被垃圾收集器回收。

当执行set方法时,ThreadLocal首先获取当前线程对象,然后获取当前线程的ThreadLocalMap对象,再以当前ThreadLocal对象为key,将值储存进ThreadLocalMap对象中。

get方法执行过程类似,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。在以ThreadLocal对象为key,获取对应的value。

由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多线程访问容器的互斥性。

使用场景:
(1)在进行对象跨层传递的时候,使用ThreadLocal可以避免多长传递,打破层次间的约束。
(2)线程间数据隔离
(3)进行事务操作,用于储存线程事务信息。
(4)数据库连接,Session会话管理。

spring框架在事务开始时会给当前线程绑定一个JDBC ,在整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性,spring框架就是使用ThreadLocal来实现这个隔离的。

ThreadLocal内存泄漏原因,如何避免

内存泄漏为查询在申请内存后,无法释放已申请的内存空间,一次内存泄漏危害可以忽略,但内存泄漏堆积后果很严重,无论多少内存,迟早会被占光。
不在会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏。
在这里插入图片描述

强引用:使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这个对象。如果想取消强引用和某个对象之间的关联,可以显式的将引用赋值为null,这样JVM在合适的时候就会回收这个对象。

弱引用: JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中。
用java.lang.ref.WeakReference类表示可以在缓存中使用弱引用。

ThreadLocal的实现原理每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例
value为线程变量的副本。

key使用强引用:
当hradLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

key使用弱引用
当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get()。remove()方法的时候会被清除value值。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal正确的使用方法

每次使用完ThreadLocal都调用它的remove()方法清除数据
将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保住任何时候都能通过ThreadLocal的弱引用访问到Entry的值,进而清除掉。

并发,并行,串行的区别

串行在时间上不可能发生重叠,前一个任务没搞定,下一个任务就只能等着。
并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行
并发运行两个任务彼此干扰。统一时间点,只有一个任务运行,交替执行。

Lock和Synchronized的区别

Lock是一个类Synchronized是java的关键字

Synchronized有两种释放锁的机制,一种是在获取锁的线程执行完代码后,会释放锁。一种是线程执行发生异常,jvm会让线程释放锁。

Lock则是在finally中必须将锁释放,否则容易出现线程死锁的问题。

锁的获取方式上Synchronized,A线程获取到锁,B线程等待,如果A线程阻塞,B线程也会一直等待。

Lock则有多种获取锁的方式:可以尝试去获取锁

锁状态方面:Synchronized无法判断锁状态,而Lock可以判断锁状态。

锁类型:Synchronized是可重入,不可中断,非公平锁。Lock是默认公平锁,当传入false时是非公平锁

Synchronized:不可绑定多个条件,Lock可以实现分组唤醒需要唤醒的锁。

CAS的原理是什么?

CAS的ABA问题如何解决

CAS(compare and swap)比较并且交换。
在读取一个内存值并进行计算后再次与原先取到的内存值进行比较如果内存值一致则将计算结果赋值,如果不相等则重复读取计算并比较操作。也就是自旋锁,也叫无锁。

ABA问题,A在读取内存值并进行计算时,B线程曾对该内存值进行修改且赋予的值与A线程读取时的值相等导致A线程认为是同一个值并赋予计算结果的问题。
解决方法:在每次读取值时添加一个版本号即可。

描述一下synchrnoized和reentrantlock的底层实现及重入的底层原理

synchrnoized (CAS)
是通过1.8JVM虚拟机的ho+spot实现下的
compareAndSwapInt对应的C和C++的Unsafe_CompareAndSwapObject
下的Unsafe_CompareAndSwapInt函数。

cmpxchg=compare and exchange

最后追踪到atomic_linux_x86.inIine.hpp。
调用了一条Lock_IF_MP(mp) cmpxchgl的汇编指令表示如果在多线程下需要先Lock这条指令,

最终实现:cmpxchg = cas修改变量值
lock cmpxchg指令
硬件:lock指令在执行后面指令的时候锁定一个北桥信号
(不采用锁总线的方式)

Synchronized的锁升级策略了解吗?

一般来说的流程为

无锁->偏向锁->轻量级锁->重量级锁

偏向锁
在无锁竞争的情况下,为了减少代价,引入偏向锁。

当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;

如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,

如果没有存活(无锁竞争),那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;

如果存活(有锁竞争),那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

总结:

偏向锁首先是在无锁的情况下,用CAS向markword中写入当前线程ID,同时也要自己栈帧中记录线程ID。
如果第二个线程进来,就要看markword中的线程id是否存活,没存活的话肯定就是无锁竞争了,可以直接cas将其重置为无锁状态,然后再设置为偏向锁。
如果存活的话肯定是有竞争了,但是因为偏向锁不会主动释放锁,所以还要看看markword线程id记录的栈帧中是否还有这个线程id,如果有的话证明这个线程1还要继续用,那么先暂停线程1,撤销偏向锁,升级为轻量级锁。不然继续设置为无锁。
轻量级锁
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁时间不长的情况。

线程1获取轻量级锁会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间,然后使用cas把对象头中的内容替换为线程1存储的锁记录的地址。

所以同一时间只会有一个成功写入对象头中的,其他的线程就需要自旋等待。

如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

线程池的作用和参数

  1. 降低资源消耗:提高线程来利用率,降低创建和消耗线程的消耗。
    2,提高响应速度:任务来了,直接有线程可用可执行,而不是先创建线程,再执行,
    3,提高线程的可管理性:线程是稀缺资源,使用线程池可用统一分配调优监控。

(CorePoolSize)代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是一个常驻线程。

(maxinumPoolsize):代表最大线程数,他与核心线程数相对于,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数。

(keepaliveTime, unit)表示超出核心线程数之外的线程空间存活时间,也就是核心线程不会消除,但是超出核心线程数部分线程如果空闲一定时间则会被消除,我们可以通过(setKeepAliveTime)来设置空闲时间。

(workQueue)用来存放执行的任务,假设我们现在核心线程都已被使用还有任务进来则全部放入队列,直到整个队列被放满单任务还再持续进入则会开始创建新线程。

(ThreadFactory)实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以自定义线程工厂,一般我们会根据业务来指定不同的工厂。

(Handler) 任务拒绝策略,有两种情况,第一种是当我们调用shudown等方法关闭线程池后,这时候即使线程池内部还有没执行完的任务在执行,但是由于线程池已经关闭,我们再继续向线程池提交任务就会被拒绝,另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提交任务时。也会执行拒绝策略。

线程池中阻塞队列的作用?为什么是先添加队列而不是先创建线程

(1)一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留当前想要继续入队的任务。
阻塞队列可以保证任务队列中没有任务是阻塞获取任务的线程,是的线程进入wait状态,释放CPU资源。
阻塞队列自带阻塞和唤醒功能,不需要额外处理,我任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活,不至于一直占用CPU资源。

(2)在创建新线程的时候,是要获取全局锁的,这个时候其他的就得阻塞,影响了整体效率。

就好比一个企业里面有10个(core)正式工的名额,最多招10个正式工,要是认为超过正式工人数(task>core)的情况下
工厂领导(线程池)不是首先扩招工人,还是这10个人,但是认为可以稍微积压一下,即先放到队列去(代价低)。10个正式工慢慢干,迟早会干完的,要是认为还在继续增加,超过正式工的加班忍耐极限了(队列满了),就得招外包帮忙了(临时工)要是正式工加上外包还是不能完成任务,那新来的任务就会被领导拒绝了(线程池拒绝策略)

拒绝策略有哪些?

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务

spring是什么?

他是一个轻量级开源的j2EE框架,它是一个容器框架,用来装javabean(java对象),中间层框架(万能胶) 可以起一个链接作用,比如说把Struts和hibernate粘合在一起运用,可以让我们的企业开发更快,更简洁。

Spring是一个轻量级的控制反转(IOC)和面向切面(AOP)的容器框架。

从大小和开心两方面而言spring都是轻量级的。
通过控制反转(IOC)的技术达到松耦合的目的,
提供面向切面编程的丰富支持,允许通过分离应用的业务逻辑和系统级服务进行内聚性的开发
包含并管理应用对象(Bean)的配置和生命周期,这个意义上是一个容器。
将简单的组件配置,组合成为复杂的应用,这个意义上是一个框架。

AOP的理解

系统是由许多不同的组件所组成的,没一个组件各负责一个特定功能,除了实现自身核心功能之外,这些组件还经常承担着额外的职责。例如日志,事务管理和安全这样的核心服务经常融入到自身具有核心业务逻辑的组件中取。
这些系统服务经常被称为横切关注点,因为他们会跨越系统的多个组件。
当我们需要为分散的对象引入公共行为的时候,OOP(面向对象)则显得无能为力,也就是说,OOP允许你定义从上到下的关系,单比不适合从左到右的关系。例如日志功能。

日志代码往往水平的散布在所以对象层次中,而与塔塔所散布到的对象的核心功能毫无相关。

在OOP设计中,她导致了大量代码的重复,而不利于各个模块的重用。

**AOP:**将程序中交叉业务逻辑(比如安全,日志,事务等),封装成一个切面。然后注入到目标对象(具体业务逻辑)中取。AOP可以对某个对象或某些对象的功能进行增强,比如对象中的方法进行增强,可以在执行某个方法之前额外的做一些事情,在某个方法执行之后额外的做一些事情。

Spring:AOP的实现原理:通过动态代理

主要分为两种,一个是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译可以在编译期间织入相关“方面”的代码。

Spring提供了两种方式来生成代理对象:JDKProxy和Cglib,具体使用哪种根据AdvisedSupport对象的配置来决定,默认的策略是如果目标类是接口,则使用JDK动态代理技术,否则使用Cglib来生成代理。

#{}和¥{}的区别:是预编译处理,是一个占位符,¥{}是字符串替换,是拼接符。

Mybatis在处理#{}时会将sql中的#{}替换成?号,调用PreparedStatement来赋值;

使用#{}可以有效的防止SQL注入,提供系统安全性。

关于IOC的理解

容器概念,控制反转,依赖注入

ioc容器:实际上就是个map(key,value),里面存的是各种对象(在xml里配置的bean节点,@repository,@service,@controller,@component),在项目启动的时候会读取配置文件里面的bean节点,根据全限定类名使用反射创建对象放到map里,扫描到打上上述注解的类或是通过反射创建对象放到map里。

这个时候map里就有各种对象了,接下来我们在代码里需要用到里面的对象时,再通过DI注入(@autowired,@resource等注解),xml里bean节点内的ref属性,项目启动的时候回读取xml节点ref属性根据id注入,也会扫描这些注解,根据类型或id注入;id就是对象名。

控制反转:
没有引入IOC容器之前,对象A依赖于对象B,那么对象A在初始化或者运行到某一个点的时候,自己必须主动去创建对象B或者引用已经创建的对象B,无论是创建还是使用对象B,控制权都在自己手上。

引入IOC容器之后,对象A和对象B之间失去了之间联系,当对象A运行需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。

通过前后对比,不难看出:对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是控制反转这个名称的由来。

全部对象的控制权全部上缴给第三方IOC容器,所以IOC容器成了整个系统的关键核心,它起到了一种类似粘合剂的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个粘合剂,对象和对象之间会彼此失去联系,这就是有人把IOC容器比喻成粘合剂的由来。

Spring是如何解决循环依赖问题的?

通过三级缓存机制:
在Spring的DefaultSingletonBeanRegistry类中,存在着三个map容器,分别是 单例池容器(singletonObjects) 缓存创建完成单例Bean的地方。映射创建Bean的原始工厂(singletonFactories)。
和映射Bean的早期引用,(earlySingletonObjects ) 在这个Map里Bean是不完整的。

BeanFactory和ApplicationContext有什么区别?

ApplicationContext 是BeanFactory的子接口
ApplicationContext 提供了更完整的功能:
(1)继承MessageSource,因此支持国际化。
(2)统一的资源文件访问方式。
(3)提供在监听器中注册bean的事件。
(4)同时加载多个配置文件。
(5)载入多个(有继承关系)上下文,使得每一个上下文都专注于一个特定的层次,比如应用的web层。

BeanFactory采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化,这样,我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直到第一次调用getBean方法才会抛出异常。(运行时发现异常)

ApplicationContext ,它是在容器启动时,一次性创建了所有的Bean,这样,在容器启动时,我们就可以发现Spring中存在的配置问题,这样有利于检查所依赖属性是否注入。ApplicationContext 启动后预载入所有的单实例Bean,通过预载入单实例Bean,确保当你需要的时候,你就不用等待,因为他们已经创建好了。

相对于进步的BeanFactory,ApplicationContext 唯一的不足是占用内存空间,当应用程序配置Bean较多时程序启动较慢。

BeanFactory通常以编程的方式被创建,ApplicationContext 还能以声明的方式创建,如使用ContextLoader。

BeanFactory和ApplicationContext 都支持BeanPricessor,BeanFactoryPostProcessor的使用,但是两者之间的区别是:
BeabFactory需要手动注册,ApplicationContext 则是自动注册。

spring的生命周期

1,解析类得到BeanDefinition
2,如果有多个构造方法,则要推断构造方法
3,确定好构造方法后,进行实例化得到一个对象
4,对对象中的加了@Aurowired注解的属性进行属性填充
5,回调Aware方法,比如BeanNameAware,BeanFactoryAware。
6,调用BeanPostProcessor的初始化前的方法
7,调用初始化方法
8,调用BeanPostProcessor的初始化后的方法,在这里会进行AOP
9,如果当前创建的bean是单例的则会把bean放入单例池
10,使用bean
11,spring容器关闭时调用DisposableBean中destory()方法

spring支持的几种bean的作用域

singleton:默认 每个容器中只有一个bean的实例,单例的模式由BeanFactory自身来维护。该对象的生命周期和spring IOC容器是一致的(但是在第一次被注入时才会创建)。

prototype:为每个bean请求提供一个实例,在每次注入时都会创建一个新的对象

request :bean被定义在每个HTTP请求中创建一个单例对象,也就是说在单个请求中都会复用这一个单例对象。

session:与request范围类似,确保每个session中有一个bean的实例,在session过期后,bean会随之失效。

application : bean被定义在ServletContext的生命周期中复用一个单例对象。

websocket:bean被定义为在websocket的生命周期中复用一个单例对象。

global-session:全局作用域,global-session和Portlet应用相关。当你的应用部署在Portlet容器中工作时,它包含很多portlet
如果你想要声明所以的portket公用全局的储存变量的话,那么这全局变量需要储存在global-session中,全局作用域与Servlet中的session作用域效果相同。

Spring框架中的单例Bean是线程安全的么?

Spring中的Bean是默认是单例模式的,框架并没有对bean进行多线程的封装处理。

如果Bean是有状态的,那就需要开发人员自己来进行线程安全的保证,最简单的办法就是改变bean的作用域,
把“singleton”改为“prototype”这样每次请求Bean就相当于new Bean()这样就可以保证线程的安全了。

有状态就是有数据存储功能

无状态就是不会保存数据 , controller,service和dao层本身并不是线程安全的,如果只是调用里面的方法,而且多线程调用一个实例的方法,会在内存中复制变量,这是自己线程的工作内存,是安全的。

Dao会操作数据库Connection,Connection是带有状态的,比如说数据库事务,spring的事务管理器使用ThreadLocal为不同线程维护了一套独立的connection副本,保证线程之间不会互相影响(Spring是如果保证事务获取同一个Connection的)

不要在bean中声明任何有状态的实例变量或者类变量,如果必须如此,那么就使用ThreadLocal把变量变为线程私有的,如果bean的实例变量或类变量需要在多个线程之间共享,那么就只能使用synchronized,lock,CAS等这些实现线程同步的方法了

Spring框架中用到了哪些设计模式

简单工厂:由一个工厂根据传入的参数,动态决定应该创建哪一个产品。
Spring中的BeanFactory就是简单工厂模式额体现,根据传入一个唯一的标识来获得Bean对象,但是否,是在传入参数后创建还是传入参数前创建这个要根据具体情况来定

工厂方法:
实现了FactoryBean接口的bean是一类叫作factory的bean,其特点是,spring会在使用getBean()调用获得该bean时,会自动调用该bean的getObject()方法,所以返回的不是factory这个bean,而是这个bean,getObject()方法的返回值。

单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

装饰器模式:动态的给一个对象添加一些额外的职责,就增加功能来说,Decorator模式相比生成子类更为灵活。
Spring中用到的保证器模式在类名上有两种表现;一种是类名中含有Wrapper,另一种是类名含有Decorator。

动态代理:
切面在应用运行的时刻被织入,一般情况下,在织入切面时,AOP容器会为目标对象动态的创建一个代理对象,
SpringAOP就是以这个方式织入切面的。

织入:把切面应用到目标对象并创建新的代理对象的过程。

观察者模式:
spring的事件驱动模型使用的是观察者模式,spring中Observer模式常用的地方是Iistener的实现。

策略模式:
Spring空间额资源访问Resource接口,该接口提供了更强的资源访问能力,Spring框架本身大量使用了Resource接口来访问底层资源。

Spring事务的实现方式和原理以及隔离级别

在使用Spring空间时,可以有两种使用事务的方式,一种是编程式的,一种是申明式的,@Transactional注解就是申明式的

首先,事务这个概念是数据库层面的,Spring只是基于数据库中的事务进行了扩展,以及提供了一些能让程序员更加方便操作事务的方式。

比如我们可以通过在某个方法是增加@Transactional注解,就可以开启事务,在这个方法中所有的sql都会在一个事务中执行,统一成功或者失败。

在一个方法上加上@Transactional注解后Spring会基于这个类生成一个代理对象,会将这个代理对象作为bean,当在使用这个代理对象的方法时,如果这个方法上存在@Transactional注解,那么代理逻辑会先把事务的自动提交设置为false,然后再去执行原本的业务逻辑方法,如果执行业务逻辑方法没有出现异常,那么代理事务就会将事务进行提交,如果执行业务逻辑方法出现了异常,那么则会将事务回滚。

当然,针对那些异常回滚事务是可以配置的,可以利用@Transactional注解中的rollbacjFor属性进行配置,默认情况下回对RuntimeException和Error进行回滚。

Spring事务隔离级别就是数据库的隔离级别:外加一个默认级别。

read uncommitted(未提交读)
read committed(提交读,不可重复读) (or默认级别)
repeatable read (可重复读) (mysql默认级别)
serializable(可串行化)

数据库的配置级别是Read Commited,而Spring的配置隔离级别是Repeatable Read,请问这时隔离级别是以哪个为准?

答案是以Spring为准,他会覆盖数据库的配置,如果spring设置的隔离级别数据库不支持,效果取决于数据库。

Spring事务传播机制

多个事务方法相互调用时,事务如何在这些方法间传播

方法A是一个事务的方法时,方法A执行过程中调用了方法B,那么方法B有无事务以及方法B对事务的要求不同都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B的事务执行也有影响,这个影响具体是什么就由两个方法所定义的事务传播类型所决定。

REQUIRED(Spring默认的事务传播类型):如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务。

SUPPORTS:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。

REQUIRES_NEW:创建一个新事务,如果当前存在事务,则挂起该事务。

NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起当前事务。

NEVER:不使用事务,如果当前事务存在,则抛出异常

NESTED:如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)

和REQUIRES_NEW的区别
REQUIRES_NEW是新建一个事务并且新开启的这个事务与原有事务无关,而NESTED则是当前存在事务时(我们把当前事务称之为父事务)会开启一个嵌套事务(称之为一个子事务)。在NESTED情况下父事务回滚时,子事务也会回滚,而
REQUIRES_NEW情况下,原有事务回滚,不会影响开启的事务。

和REQUIRED的区别
REQUIRED情况下,在调用存在事务时,则被调用方和调用方使用同一事务,那么被调用方出现异常时,由于公用一个事务,所以无论调用方是否catch起异常,事务都会被回滚,而在NESTED情况下,被调用方发生异常时,调用方可以catch其异常,这样只有子事务回滚,父事务不受影响。

Spring事务什么时候会失效?

Spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了!异常情况有如下几种

1,发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理类,而是UserService对象本身!

2,方法不是public的
@Transactional只能用于public的方法上,否则事务不会生效,如果要用在public 方法上,可以开启aspectj代理模式,

3,数据库不支持事务
4,没有被Spring管理
5,异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)

什么是bean的自动装配,有哪些方式。

开启自动装配,只需要在xml配置文件中定义“autowire”属性。

autowire属性有五种装配的方式:

no-缺省情况下,自动配置是通过“ref”属性手动设定。
手动装配:以value或ref的方式明确指定属性值都是手动装配。
需要通过“ref”属性来链接bean。

byName-根据bean的属性名称进行自动装配

Cutomer的属性名称是person,Spring会将bean会将bean id为
person的bean通过setter方法进行自动装配。

byType-根据bean的类型进行自动装配。
Cutomer的属性person的类型为Person,Spring会将Person类型通过setter方法进行自动装配。

constructor-类似byType,不过是应用于构造器的参数。如果一个bean与构造器参数类型相同,则进行自动装配,否则导致异常。

Cutomer构造函数的参数person的类型为Person,Spring会将Person类型通过构造方法进行自动装配。

autodetect-如果有默认的构造器,则通过constructor方式进行 自动装配。

如果有默认的构造器,则通过constructor方式进行自动装配,否则使用byType方式进行自动装配。

@Autowired自动装配bean,可以在字段,setter方法,构造函数上使用。

Spring Boot,Spring MVC 和Spring 有什么区别,

Spring是一个IOC容器,用来管理Bean,使用依赖注入实现控制反转,可以很方便的整合各种框架,提供AOP机制弥补OOP的代码重复问题,更方便将不同类不同方法中的共同处理抽取成切面,自动注入给方法执行,比如日志,异常等。

Springmvc是spring对web框架的一个解决方案,提供了一个总的前端控制器Servlet,用来接收请求,然后定义了一套路由政策(url到handle的映射) 及适配handle,将handle结果使用视图解析技术生成视图展现给前端

Springboot是spring提供的一个快速开发工具包,让程序员能更方便,更快速的开发spring+springmvc应用,简化了配置
(约定了默认配置),整合了一系列的解决方案(starter机制),redis,mongodb,es可以开箱即用。

SpringMVC的工作流程

(1)用户发送请求至前端控制器DispatcherServlet。
(2)DispatcherServlet收到请求调用HandlerMapping处理器映射器。
(3)处理器映射器找到具体的处理器(可以根据xml配置,注解进行查找),生成处理器及处理器拦截器(如果有则生成)——并返回给DispatcherServlet
(4)DispatcherServlet调用HandlerAdapter处理器适配器。
(5)HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)
(6)Controller执行完成返回 ModelAndView
(7)HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
(8)DispatcherServlet将ModelAndView
传给ViewReslover视图解析器。
(9)ViewReslover解析后返回具体View
(10)DispatcherServlet根据View进行渲染视图(将模型数据填充至视图中)。
(11)DispatcherServlet响应用户

Spring MVC的主要组件

Handler:也就是处理器,他直接对应着MVC中的C也就是Controller层,他的具体表现形式有很多,可以是类,也可以是方法,在Controller层中@RequestMapping标注的所有方法都可以看成是一个Handler,只要可以实际处理请求就可以是Handler。

1(HandlerMapping)
initHandlerMappings(context),处理映射器,根据用户请求的资源url来查找Handler的,在SpringMVC中会有很多请求,每个请求都需要一个Handler处理,具体接收到一个请求之后使用哪个Handler进行,这就是HandlerMapping需要做的事。

2(HandlerAdapter)
initHandlerAdapters(context),适配器,是因为SpringMVC中的Handler可以是任意的形式,只有能出来请求就ok,但是Servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法,如何让固定的Servlet处理方法调用灵活的Handler来进行处理呢?这就是根据HanlderAdapter要做的事情。
Handler是用来干活的工具;HandlerMapping用于根据需要干的活找到相应的工具;HandlerAdapter是使用工具干活的人。

3(HandlerExceptionResolver)
其他组件都是用来干活的,在干活的过程中难免会出现问题,出问题之后怎么办,这就需要一个专门的角色处理异常情况,在SpringMVC中就是HandlerExceptionResolver。
具体来说,此组件的作用是根据异常设置ModelAndView,之后再交给render方法进行渲染。

4(ViewResolver)
ViewResolver用来将String类型的视图名和Locale解析为View类型的视图,View是用来渲染页面的,也就是将程序返回的参数填入模板里生成html(也可能是其他类型)文件。这了就要两个关键问题,使用哪个模板,用什么技术(规则)填入参数?这就是ViewResolver主要要做的工作,ViewResolver需要找到渲染所用的模板和所用的技术(视图的类型)进行渲染,具体的渲染过程则交由不同的视图自己完成。

5(RequestToViewNameTranslator)
ViewResolver是根据ViewName查找View,但是Handler处理完后并没有设置View也没有设置ViewName,这时就需要request获取ViewName了,如果从request中获取就是RequestToViewNameTranslator要做的事情了,
RequestToViewNameTranslator在SpringMVC容器只可以配置一个所用所有request到ViewName的转换规则都要在Translator里面实现。

6(LocaleResolver)
LocaleResolver,解析视图需要两个参数,一是视图名,另一个是Locale,视图名是处理器返回的,Locale是从哪里来的?
这就是LocaleResolver要做的事情,
LocaleResolver用于从request解析出Locale,就是zh-ch之类,表示一个区域,有了这个就可以对不同区域的用户显示不同的结果,
SpringMVC主要有两个地方用了Locale:一个是ViewResolver视图解析的时候:二是用到国际化资源或者主题的时候。

7(ThemeResolver)
ThemeResolver,用于解析主题,SpringMVC中一个主题对应一个properties文件,里面存放着跟当前主题相关的所有资源,如图片,css样式等,SpringMVC的主题也支持国际化,
同一个主题不同区域也可以显示不同的风格,SpringMVC中跟主题相关的类有ThemeResolver,ThemeSource和Theme。
主题是通过一系列资源来具体体现的,要得到一个主题的资源,首先要得到资源的名称,这就是ThemeResolver的工作,
然后通过主题名称找到对应的主题(可以理解为一个配置)文件,这是ThemeSourece的工作,最后从主题中获取资源就可以了。

8(MultipartResolver)
MultipartResolver,用于处理上传请求,处理方法是将普遍的request保证成MultipartHTTPServletResolver,后者可以直接调用getFile方法获取File,如果上传多个文件,还可以调用getFileMap得到FileName->File结构的Map,此组件中一共有三个方法,作用分别是判断是不是上传请求,将request包装成
MultipartHTTPServletResolver,处理完后清理上传过程汇总产生的临时资源。

9 (FlashMapManager)
FlashMapManager,用来管理FlashMap的,FlashMap主要用在redirect中传递参数。

Spring Boot 自动配置原理?

在这里插入图片描述
启动类上的@SpringbootApplication注解,这个注解里会包含一个注解叫@EnableAutoConfiguration.
在@EnableAutoConfiguration里有一个
@Import(AutoConfigurationImportSelector.class)
在AutoConfigurationImportSelector里有一个方法
getCandidateConfigurations,
他会去找到spring.factorles配置文件中的EnableAutoConfiguration下的所有配置加载出来。

如何理解Spring Boot 中的Starter

使用spring+springmvc使用,如果需要引入mybatis等框架,需要到xml中定义mybatis需要的bean。

starter‘就是定义一个starter的jar包,写一个@Configuration配置类,将这些bean定义在里面,然后在starter包的META-INF/spring。factories中写入该配置类,springboot会按照约定来加载该配置类

开发人员只需要将相应的starter包依赖进应用,进行相应的属性配置(使用默认配置时,不需要配置),就可以直接进行代码开发,使用对应的功能了,比如mybatis-spring-boot-starter,

什么是嵌入式?为什么要使用嵌入式服务器?

节省了下载安装tomcat,应用也不需要在打war包,然后放到webapp目录下在运行了。
只需要安装了java的虚拟机,就可以直接在上面部署应用程序了

springboot已经内置了tomca.jar,运行main方法时会去启动tomcat,并利用tomcat的spi机制加载springmvc。

Mybatis的优缺点

优点:

1:基于SQL语句编程,相对灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理:提供XML标签,支持编写动态SQL语句,并可重用。

2:与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关链接;

3,可以很好的与各种数据库兼容(因为MyBatis使用JDBC来链接数据库,所以只有JDBC支持的数据库MyBatis都支持)

4能够于Spring很好的集合;
5,提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护,

缺点:
SQL语句编写工作量大,尤其当字段多时,对开发人员编写SQL语句的功底有一点要求
2,SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

MyBatis与Hibernate有哪些不同?

面向对象,表结构表设计

开发速度的对比:
Hibernate的真正掌握要比Mybatis难些,Mybatis空间相对简单很容易上手,但也相对简陋些。
比起两者的开发速度,不仅仅要考虑到两者的特性和性能,更要根据项目需求去考虑究竟哪一个更适合项目开发,
比如:一个项目中用到的复杂查询基本没有,就是简单的增删改查,这样选择hibernate效率就很快了,因为基本的sql语句已经被封装好了,根本不需要你去写sql语句,这就节省了大量的时间,但是对于一个大型项目,复杂语句较多,这样再去选择hibernate就不是一个太好的选择,选择mybatis就会加快许多,而且语句的管理也比较方便。

开发工作量对比:
Hibernate和MyBatis都要相应的代码生成工具,可以生成简单的DAO层方法,针对高级查询,mybatis需要手动编写SQL语句,已经ResultMap,而Hibernate有良好的映射机制,开发者无心关系SQL的生成与结构映射,可以更专注于业务流程。

SQL优化方面:

Hibernate的查询会将表中的所有字段查询出来,这点会有性能消耗。Hibernate也可以自己写SQL来指定需要查询的字段,但这样就破坏了Hibernate开发的简洁性,而Mybatis的SQL是手动编写的,所以可以按需求指定查询字段。
Hibernate HQL语句的调用需要将SQL打印出来,而Hibernate的SQL被很多人嫌弃因为太丑了,Mybatis的SQL是自己手动写的所有方便调整,但Hibernate具有自己的日志统计,
Mybatis本身不带有日志统计,使用Log4j进行日志记录。

对象管理对比

Hibernate 是完整的对象、关系映射解决方案,它提供了对象状态管理(state management)的功能,使开发者不在需要理会底层数据库系统的细节,也就是说,相对于常见的JDBC/SQL持久层方案中需要管理SQL语句,
Hibernate 采用了更自然的面向对象的视角来持久化java应用中的数据。
换句话说,使用Hibernate 的开发者总是关注对象的状态(state),不必考虑SQL语句的执行吗,这部分细节已经由Hibernate 掌管妥当,只有开发者进行系统性能调优的时候才需要进行了解,而mybatis在这一快没有文档说明,用户需要自己进行详细管理。

缓存机制对比:

相同点:都可以实现自己的缓存或使用其他第三方缓存方案,创建适配器来完全覆盖缓存行为。

不同点:
Hibernate 的二级缓存配置在SessionFactory生成的配置文件中进行详细配置,然后在具体的表-对象映射中配置是哪个缓存。

mybatis的二级缓存都是在每个具体的表-对象映射中进行详细配置,这样真的不同的表可以自定义不同的缓存机制,并且mybatis可以在命名空间中共享相同的缓存配置和管理机制,通过(Cache-ref)实现

两者比较:
因为Hibernate 对查询对象有着良好的管理机制,用户无需关心SQL,所以在使用二级缓存时如果出现脏数据,系统会爆出错误并提示

而Mybatis在这一方面,使用二级缓存时需要特别小心,如果不能完全确定数据更新操作的波及范围,避免Cache的盲目使用,否则,脏数据的出现会给系统的正常运行带来很大的隐患

Hibernate 功能强大,数据库无关性好,O/R映射能力强,如果你对Hibernate 相对精通,而且对Hibernate 进行了适当的封装,那么你的项目整个持久层代码会相当简单,需要写的代码很少,开发速度快,非常爽。

Hibernate 的缺点就是学习门槛不低,要精通门槛更高,而且怎么设计O/R映射,在性能和对象模型之间如果权衡取得平衡,已经怎样用好Hibernate 方面需要你的经验和能力很强才行。

Mybatis入门简单,即学即用,提供了数据库查询的自动对象绑定功能,而且延续了很好的SQL使用经验,对于没有那么高的对象模型要求项目来说,相对完美。

Mybatis的缺点就是框架还是比较简陋,功能尚有缺失,虽然简化了数据绑定代码,但是整个底层数据库查询实际还是要自己写的,工作量也比较大,而且不太容易适应快速数据库修改。

#{}和¥{}的区别是什么?

#{}是预编译处理,是占位符,¥{}是字符串替换,是拼接符。

Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用
PreparedStatement来赋值;

mybatis在处理¥{}时,就是把¥{}替换成变量的值,调用Statement来赋值;

#{}的变量替换是在DBMS中,变量替换后,#{}对应的变量自动加上单引号

${}的变量替换是在DBMS外,变量替换后,¥{}对应的变量不会加上单引号

使用#{}可以有效的防止SQL注入,提供系统安全性。

Mybatis的插件运行原理,如果编写一个插件。

Mybatis只支持针对 ParameterHandler,ResultSetHandler,
StatementHandler,Executor这4中接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现方法拦截功能,没当执行这4中接口对象的方法时,就会进入拦截方法,具体就是 lnvocationHandler的invkoke()方法,拦截那些你指定需要拦截的方法。

编写插件:实现mybatis的Interceprot接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的方法即可,在配置文件汇总配置编写的插件。

在这里插入图片描述

索引的基本原理

索引用来快速的寻找那些具有特点值得记录。如果没有索引,一般来说执行查询时遍历整张表。

索引的原理:就是把无序的数据变成有序的查询

1,把创建了索引的列的内容进行排序
2,对排序结果生成倒排表
3,在倒排表内容上拼上数据地址链
4,在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据。

B+树的数据结构:

除了叶子节点之外其他节点存储的都是指向下一节点的指针,并且数据是有序的。
如果是聚簇索引那么叶子节点会存储一行数据的所有信息,并且叶子节点之间有左右指针相互指向。
如果是非聚簇索引,那么叶子节点不存储表中的数据,而是存储该列对应的主键,

B+树和B树有什么区别?

B树:每个节点都存储key和data,所有节点组成这颗树,并且叶子节点指针为null。
在这里插入图片描述

B+树:只有叶子节点存储data,叶子节点包含了这颗树的所有键值,叶子节点不存储指针。
在这里插入图片描述
后来,在B+树上增加了顺序访问指针,也就是每个叶子节点增加一个指向相邻叶子节点的指针,这样一棵树成了数据库系统实现索引的首选数据结构。

原因有很多,最主要的是这棵树矮胖,呵呵。一般来说,索引很大,往往以索引文件的形式存储的磁盘上,索引查找时产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的时间复杂度。树高度越小,I/O次数越少。

那为什么是B+树而不是B树呢,因为它内节点不存储data,这样一个节点就可以存储更多的key。

在MySQL中,最常用的两个存储引擎是MyISAM和InnoDB,它们对索引的实现方式是不同的。

MyISAM

data存的是数据地址。索引是索引,数据是数据。索引放在XX.MYI文件中,数据放在XX.MYD文件中,所以也叫非聚集索引。
在这里插入图片描述
InnoDB

data存的是数据本身。索引也是数据。数据和索引存在一个XX.IDB文件中,所以也叫聚集索引。
在这里插入图片描述
了解了数据结构再看索引,一切都不费解了,只是顺着逻辑推而已。另加两种存储引擎的区别:

1、MyISAM是非事务安全的,而InnoDB是事务安全的

2、MyISAM锁的粒度是表级的,而InnoDB支持行级锁

3、MyISAM支持全文类型索引,而InnoDB不支持全文索引

4、MyISAM相对简单,效率上要优于InnoDB,小型应用可以考虑使用MyISAM

5、MyISAM表保存成文件形式,跨平台使用更加方便

6、MyISAM管理非事务表,提供高速存储和检索以及全文搜索能力,如果在应用中执行大量select操作可选择

7、InnoDB用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,可选择。

mysql聚簇和非聚簇索引的区别

都是B+树的数据结构

聚簇索引:将数据储存与索引放到了一起,并且是按照一定的顺序组织的,找到索引也就找到了数据,数据的物理存放顺序与索引顺序是一致的,即:只要索引是相邻的,那么对应的数据一定也是相邻的存放在磁盘上的。

非聚簇索引:叶子节点存储数据,存储的是数据行地址,也就是说根据索引查找数据行的位置再取磁盘查找数据,这个就有点类似一本书的目录,比如我们要找第三章第一节,那我们先再这个目录里面找,找到对应的页码再去对应的页码看文章。

优势:
1,查询通过聚簇索引可以直接获取数据,相比非聚簇索引需要第二次查询(非覆盖索引的情况下)效率要高。
2,聚簇索引对于范围查询的效率很高,因为其数据是按照大小排列的。
3,聚簇索引适合在用在排序的场合,非聚簇索引不合适。

劣势:

1,维护索引很昂贵,特别是插入新行或者主键被更新至要分页(page split)的时候。建议在大量插入新行后,选在负载较低的时间段,通过OPTIMIZE TABLE优化表,因为必须被移动的行数据可能造成碎片,使用独享表空间可以弱化碎片。

2,表因为使用UUID(随机ID)作为主键,使用数据存储稀疏,这就会出现聚簇索引有可能有比全表扫描更慢,所以建议使用int的auto_increment作为主键,

3,如果主键比较大的话,那辅助索引将会变得更大,因为辅助索引的叶子储存的是主键值;过长的主键值,会导致非叶子节点占用更多的物理空间。

InnDB中一定有主键,主键一定是聚簇索引,不手动设置,则会使用unique索引,没有unique索引,则会使用数据库内部的一个行隐藏id来当主键索引,在聚簇索引之上创建的所有索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引,像复合索引,前缀索引,唯一索引,辅助索引叶子节点储存的不在是行的物理位置,而是主键值。

MyISM使用的是非聚簇索引,没有聚簇索引,非聚簇索引的两颗B+树看上去没有什么不同,节点的结构完全一致只是储存的内容不同而已,主键索引B+树的节点储存了主键,辅助键索引B+树储存了辅助键。表数据储存在独立的地方,这两颗B+树的叶子节点都会使用一个地址指向真正的表数据,对对于表数据来说,这两个键没有任何区别,由于索引树是独立的,通过辅助键索引无需访问主键的索引树。

如果涉及到大数据量的排卵,全表扫描,count之类的操作的话,还是MyISM占优势些,因为索引所占空间小,这些操作是需要在内存中完成的。

Explain语句中各个字段分别代表什么

在这里插入图片描述

比较重要的参考字段有:
type 具体的查询级别
possible_keys 可能用到的索引
key 实际上使用的索引
ref
rows 预计的需要读取的记录条数
Extra 一些额外信息,可通过官方文档查询到sql的优化办法

type 的各种查询级别

1, ALL 全表扫描

2, index 索引全扫描

3, range 索引范围扫描,常用语<,<=,>=,between等操作

4, ref 使用非唯一索引扫描或唯一索引前缀扫描,返回单条记录,常出现在关联查询中

5, eq_ref 类似ref,区别在于使用的是唯一索引,使用主键的关联查询

6, const/system 单条记录,系统会把匹配行中的其他列作为常数处理,如主键或唯一索引查询

7, null MySQL不访问任何表或索引,直接返回结果

由上至下,效率越来越高

索引覆盖是什么

索引覆盖就是一个SQL在执行时,可以利用索引来快速查找,并且此SQL所要查询的字段在当前索引对应的字段中都包含了,那么就表示此SQL走完索引后不用在回表了,所需要的字段都在当前索引的叶子节点上存在,可以直接作为结果返回。

最左匹配原则是什么

当一个SQL想要利用索引时,就一定要提供该索引所对应的字段中最左边的字段,也就是排在最前面的字段,比如针对a,b,c三个字段建立了一个联合索引,那么在写一个SQL时就一定要提供a字段的条件,这样才能用到联合索引,这是由于在建立a,b,c三个字段的联合索引时,底层的B+树是按照a,b,c三个字段从左往由去比较大小进行排序的,所以如果需要利用B+树进行快速查找也得符合这个规则

mysql索引的数据结构,各自优势

索引的数据结构和具体存储引擎的实现有关,在MySQL中使用较多的所有有Hash索引,B+树索引等,InnoDB存储引擎的默认索引实现为:B+树索引,对于哈希索引来说,底层数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快:其余大部分场景,建议使用BTree索引。

B+树:

B+树是一个平衡的多叉树,从根节点到每个叶子节点的高度差值不超过1,而且同层次的节点间有指针相互连接。
在B+树上的常规检索,从根节点到叶子节点的搜索效率基本相当,不会出现大幅波动,而且基于索引的顺序扫描时,也可以利用双向指针快速左右移动,效率非常高,因此,B+树索引被广泛应用于数据库,文件系统等场景。

在这里插入图片描述

哈希索引:

哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,速度非常快。

在这里插入图片描述

如果是等值查询,那么哈希索引明显有绝对优势,因为只需要经过一次算法即可找到相应的键值;前提是键值都是唯一的。
如果键值不是唯一的,就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到相应的数据;

如果是范围查询检索,这时候哈希索引就毫无用武之地了,因为原先是有序的键值,经过哈希算法后,有可能变成不连续的了,就没办法在利用索引完成范围查询检索;

哈希索引也没办法利用索引完成排序,以及like“xxx%”这样的部分模糊查询(这种部分模糊查询,其实本质上也是范围查询)

哈希索引也不支持多列联合索引的最左匹配规则;

B+树索引的关键字检索效率比较平均,不像B树那样波动幅度大,在有大量重复键值情况下,哈希索引的效率也是极低的,因为存在哈希碰撞的问题。

索引设计的原则

查询更快,占用空间更小

1,适合索引的列是出现在where子句中的列,或者连接子句中指定的列。
2,基数较小的表,索引效果较差,没有必要在此列建立索引
3,使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间,如果搜索词超过索引前缀长度,则使用索引排除不匹配的行,然后检查其余行是否可能匹配。
4,不要过度索引,索引需要额外的磁盘空间,并降低写操作的性能,在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长,索引只保存需要的索引有利于查询即可。
5,定义有外键的数据列一定要建立索引。
6,更新频繁字段不适合创建索引
7,若是不能有效区分数据的列不适合做索引列(如性别,男女位置,最多也就三种,区分度太低)

8尽量的扩展索引,不要新建索引,比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可
9,对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。
10,对于定义为text,image和bit的数据类型的类不要建立索引

mysql锁的类型有哪些

基于锁的属性分类:分享锁,排他锁。
基于锁的颗粒分类;行级锁(INNODB),表级锁(INNODB,MYISAM),页级锁(BDB引擎),记录锁,间歇锁,临建锁。

基于锁的状态分类:意向共享锁,意向排他锁

共享锁(Share Lock)
共享锁又称读锁,简称S锁;当一个事务为数据加上读锁之后,
其他事务只能对该数据加读锁,而不能对数据加写锁,
直到所有的读锁释放之后其他事务才能对其进行加持写锁,共享锁的特性主要是为了支持并发的读写数据,读写数据的时候不支持修改,避免出现重复读的问题。

排它锁(eXclusive Lock)
排它锁又称写锁,简称X锁;当一个事务为数据加上写锁时,其他请求将不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁,排它锁的目的是在数据修改时候,不允许其他人同时修改,也不允许其他人读取,避免了出现脏数据和脏读的问题。

表锁:
表锁是指上锁的时候锁住的是整个表,当下一个事务访问该表的时候,必须等前一个事务释放了锁才能进行对表进行访问:
特点:粒度大,加锁简单,容易冲突;

行锁:
行锁是指上锁的时候锁住的是表的某一行或多行记录,其他事务访问同一张表时,只有被锁住的记录不能访问,其他的记录可正常访问;
特点:粒度小,加锁比表锁麻烦,不容易冲突,相比表锁支持的并发要高。

记录锁(Record Lock)
记录锁也属于行锁中的一种,只不过记录锁的范围只是表中的某一条记录,记录锁是说事务在加锁后锁住的只是表的某一条记录。
精确条件命中,并且命中条件字段是唯一索引
加了记录锁之后数据可以避免数据在查询的时候被修改的重复读问题,也避免了再修改事务未提交前被其他事务读取的脏读问题。

页锁:
页级锁是MYSQL中锁定粒度介于行级锁和表级锁中间的一种锁,表级锁速度快,单冲突多,行级锁冲突少,但速度慢。
索引取了折中的页级,异常锁定相邻的一组记录;
特点:开销和加锁时间界于表锁行锁之间;会出现死锁,锁定粒度介于表锁和行锁之间,并发度一般。

间隙锁(Gap Lock)
属于行锁中的一种,间隙锁是在事务加锁后锁住的是表记录的某一个区间,当表的相邻ID之间出现空隙则会形成一个区间,遵循左开右闭原则。
范围查询并且查询未命中记录,查询条件必须命中索引,间隙锁只会出现在REPEATABLE_READ(重复读)的事务级别中。
触发条件:防止幻读问题,事务并发的时候,如果没有间隙锁,就会发生如下图的问题,在同一个事务里A事务的两个查询出的结果会不一样。
比如表里面的数据ID为 1,4,5,7,10,那么会形成以下几个间隙区间,-n-1区间,1-4区间,7-10区间,10-n区间(-n代表负无限大,n代表正无穷大)

临建锁(Next-Key Lock)

也属于行锁的一种,并且他是INNODB的行锁默认算法,总结来说他就是记录锁和间隙锁的组合,临建锁会把查询出来的记录锁住,同时也会把该范围查询内的所有间隙空间也会锁住,再之它会把相邻的下一个区间也会锁住。
触发条件;范围查询并命中,查询命中索引。
结合记录锁和间隙锁的特性,临建锁避免了再范围查询时出现脏读,重复读,幻读问题。加了临建锁之后,在范围区间内数据不允许被修改和插入。

如果当事务A加锁成功之后就设置一个状态告诉后面的人,已经有人对表里的行加了一个排它锁了,你们不能对整个表加共享锁或排它锁了,那么后面需要对整个表加锁的人只需要获取这个状态就知道自己是不是可以对表加锁,避免了对整个索引树的每个节点扫描是否加锁,而这个状态就是意向锁。

意向共享锁:
当一个事务试图对整个表进行加共享锁之前,首先需要获得这个表的意向共享锁

意向排他锁:
当一个事务试图对整个表进行加排他锁之前首先需要获得这个表的意向排他锁。

mysql怎样查看sql执行计划

在select前面加explain即可

索引的常见失效场景

模:模糊查询LIKE以%开头
型:数据类型错误
数:对所以字段使用内部函数
空:索引列为null
运:索引列进行四则运算(±*/)
最:复合索引不按索引列最左开始查询
快:全表查找预计比索引更快

事务的基本特性和隔离级别

事务基本特性ACID分别是:

A原子性:一个事务执行是要么都成功要么都失败。

A的原子性由 undo log日志来保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql。

C一致性:指在事务运行完成后总的数据量不变

C一致性由其他三大特性保证,程序代码要保证业务的一致性。

I隔离性:指在事务运行期间对其他事务具有隔离性,不可见

I隔离性由MVCC来保证

D持久性:在事务执行完毕后会将数据持久化到数据库。

D持久性由内存+redo log日志来保证,mysql修改数据的同时在内存和redo log记录这次操作,宕机时可以从redo log恢复。

在InnoDB中 redo log来写盘,innoDB事务进入prepare状态。
如果前面prepare成功,binlog写盘,在继续将事务的日志持久化到binlog,如果持久化成功,那么innDB事务则进入commit状态(在redo log里面写一个commit记录)

redolog的刷盘会在系统空闲时进行。

隔离性有4个隔离级别,分别是:

(read uncommit)读未提交,可能会读到其他事务未提交的数据,也叫作脏读。用户本来应该读取到id=1的用户age应该是10,结果读取到了其他事务还没有提交的事务,结果读取结果=20,这就是脏读

(read commit) 读已提交,两次读取结果不一致,叫作不可重复读。
不可重复读解决了脏读的问题,他只会读取已经提交的事务。
用户开启事务读取id=1用户,查询到age=10,再次读取发现结果=20,在同一个事务里同一个查询读取到不同的结果叫作不可重复读。

(repeatable read)可重复读,这是mysql的默认级别,就是每次读取结果都一样,但是又可能产生幻读。

(serializable)串行,一般不会使用,他会给每一行读取的数据加锁,会导致大量超时和锁竞争的问题。

脏读;某个事务已更新一份数据,另一个事务在此时读取了同一分数据,由于某些原因,前一个RollBack了操作,则后一个事务读取的数据就会是不正确的。

不可重复读:在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。

幻读:在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先去的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。

Mysql有幻读问题吗?

当然有,在除了非串行化的其他隔离级别中都可能出现幻读。

在mysql中使用MVCC多版本并发控制器解决幻读问题。

select * form table where a=x and b>x and c<x 应该怎么建立索引?

应对满足最左匹配原则来建立索引 ab或ac

关心过业务系统里面的sql耗时问题吗?统计过慢查询吗?对慢查询都怎么优化过?

在业务系统中,处理使用主键进行的查询,其他的都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。

慢查询的优化首先要高明白慢的原因是什么?是查询条件没有名字索引?
是load了不需要的数据列?还是数据量太大。

所以优化也是针对这三个方向来的,

首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析和重写。
分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引,
如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表。

ACID靠什么保证的?

A原子性由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql。

C一致性由其他三大特性保证,程序代码要保证业务的一致性。

I隔离性由MVCC来保证

D持久性由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,宕机时可以从redo log恢复。

InnoDB redo log 写盘,InnoDB事务进入prepare状态。
如果前面prepare成功,binlog写盘,再继续将事务的日志持久化到binlog,如果持久化成功,那么InnDB事务则进入commit状态(在redo log里面写一个commit记录)

redolog的刷盘会在系统空闲时进行。

什么是MVCC

多版本并发控制:读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务session会看到自己特点版本的数据,版本链

MVCC只在(READ COMMITTED)和 (REPEATABLE READ)这两个隔离级别汇下工作,其他两个隔离级别都和MVCC不兼容,
因为READ UNCOMMITTED总是读取新的数据行,而不是符合当前事务版本的数据行,而SERIALIZABLE则会对所以读取的行都加锁。

聚簇索引记录中有必要的隐藏列:

trx_id:
用来储存每次对某条聚簇索引记录进行修改的时候的事务id。

roll_pointer:
每次对那条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为他没有老版本)

已提交读和可重复读的区别就在于他们生成ReadView的策略不同。

ReadView的概念:

开始事务时创建ReadView,ReadView维护当前活动事务id,即未提交的事务id,排列生成一个数组访问数据,获取数据中的事务id(获取事务id最大的记录),对比ReadView;如果在ReadView的左边(比ReadView都小),则可以访问(左边意味着事务已提交)。
如果在ReadView的右边(比ReadView都大)或者就在ReadView中,不可以访问,获取roll_pointer,取得上一版本重新对比(在右边意味着,该事务在ReadView生成之后出现,在ReadView中意味着该事务还未提交)

已提交隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView
而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。

这就是mysql的MVCC,通过版本链,实现多版本,可并发读-写,写-读。通过ReadView生成策略的不同实现不同的隔离级别。

mysql主从同步原理

mysql主从同步的过程:

mysql的主从复制中主要有三个线程:master(binlog dump thread),slave(I/O thread,SQL thread),Master一条线程和Slave中的两条线程。

主节点binlog,主从复制的基础是主库记录数据库的所有变更记录到binlog,binlog是数据库服务器启动的那一刻起,保存所有修改数据库结构或内容的一个文件。

主节点log dump线程,当binlog有变动时,log dump线程读取其内容并发送给从节点。

从节点I/O线程接收binlog内容,并将其写入到 relay log文件中,

从节点的SQL线程读取 relay log文件内容对数据更新进行重放,最终保证主从数据库的一致性。

注:主从节点使用binlog文件+position偏移量来定位主从同步的位置,从节点会保存其已接收到的偏移量,如果从节点发送宕机重启,则自动从position的位置发起同步。

由于mysql默认的复制方式是异步的,主库把日志发送给从库后不关心从库是否已经处理,这样会产生一个问题就是假设主库挂了,从库处理失败了,这时候从库升为主库后,日志就丢失了,由此产生两个概念。

全同步复制:
主库写入binlog后强制同步日志到从库,所以的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响。

半同步复制:
和全同步不同的是,半同步复制逻辑是这样的,从库写入日志成功后返回ACK确认给主库,主库收到至少一个从库的确认就认为写操作完成。

MyISAMHE 和InnoDB的区别

MyISAM:

不支持事务,但是每次查询都是原子的;
支持表级锁,即每次操作都是对整个表加锁;
储存表的总行数;
一个MYISAM表有三个文件:索引文件,表结构文件,数据文件;
采用非聚簇索引,索引文件的数据域储存指向数据文件的指针,辅助索引与主索引基本一致,但是辅助索引不用保证唯一性。

InnoDB:
支持ACID的事务,支持事务的四种隔离级别;
支持行级锁及外键约束:因此可以支持写并发;
不储存总行数;

一个InnoDB引擎储存在一个文件空间(共享表空间,表大小不受操作系统控制,一个表可能分部在多个文件里),也有可能为多个(设置独立表空,表大小受操作系统文件大小限制,一般为2G),受操作系统文件大小限制;

主键索引采用聚簇索引(索引的数据域储存数据文件本身),辅索引的数据域储存主键的值;因此从辅助索引查找数据,需要先通过辅助索引找到主键值,再访问辅助索引。最好使用自增主键,防止插入数据时,为维护B+树结构,文件进行大调整。

mysql中索引类型及对数据库的性能影响

普通索引:允许被索引的数据列包含重复的值,

唯一索引:可以保证数据记录的唯一性。

主键:是一种特殊的唯一索引,在一张表中只能定义一个主键索引,主键用于唯一标识一条记录,使用关键字PRIMAPY KEY来创建。

联合索引:索引可以覆盖多个数据列,如像INDEX(columnA,columnB)索引。

全文索引:通过建立倒排索引,可以极大的提升检索效率,解决判断字符是否包含的问题,是目前搜索引擎使用的一种关键技术,
更推荐使用ES。

索引可以极大的提高数据的查询速度,
通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统性能。
但是会降低插入,删除,更新表的速度,因为在执行这些写操作时,还有操作索引文件。

索引需要占用物理空间,除了数据表占数据空间之外,每一个索引还有占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大,如果非聚簇索引很多,一旦聚簇索引改变,那么所有非聚簇索引都会跟着变。

Redis的基本数据类型:

set
list
hash
zset
string

RDB和AOF机制

RDB:Redis DataBase
在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,在替换之前的文件,用二进制压缩储存。

优点:
1,整个redis数据库将只包含一个文件 dump.rdb,方便持久化,
2,容灾性好,方便备份,
3,性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以是IO最大化,使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高性能。
4,相对于数据集大时,比AOF的启动效率更高,

缺点:
1,数据安全性低。RDB是间隔一段时间进行持久化,如果持久化之间redis发生故障,会发生数据丢失,所以这种方式更适合数据要求不严谨的时候
2,由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至一秒。

AOF:Append Only File

以日志的形式记录服务器所处理的每一个写,删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

优点:
1,数据安全,redis中提供了三种同步策略,即每秒同步,每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一定系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失,而每修改同步,我们可以将其视为同步持久化,即每次发送数据变化都会被立即记录到磁盘中,
2,通过append模式编写文件,即使中途服务器宕机也不会破坏已经存在的内容,可以通过redis-check-aof工具解决数据一致性问题。
3,AOF机制的rewrite模式。定期对AOF文件进行重写,以达到压缩的目的

缺点:
1,AOF文件比RDB文件大,且恢复速度慢
2,数据集大的时候,比RDB启动效率低
3,运行效率没有RDB高

总结:
AOF文件比RDB更新频率高,优先使用AOF还原数据
AOF比RDB更安全也更大
RDB性能比AOF好
如果两个都配了优先加载AOF

Redis事务

1,事务开始
multi命令标志着一个事务的开始,multi命令会将客户端状态的flags属性中打开redis_multi标识来完成的。

2,事务入队

当一个客户端切换到事务状态之后,服务器会根据这个客户端发送来的命令来执行不同的操作,如果客户端发送的命令为multi,exec,watch,discard中的一个,就会立即执行这个命令否则会将命令放入一个事务队列里面,然后向客户端返回queued回复。

如果客户端发送的命令为MULTI,EXEC,WATCH,DISCARD中的一个,那么服务器立即执行这个命令。

如果客户端发送的是四个命令以外的其他命令,那么服务器并不立即执行这个命令。
它会首先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态的flags属性关闭redis_multi标识,并且返回错误信息给客户端。
如果正确,将这个命令放入一个事务队列里面,然后向客户端返回queued回复,事务队列是按照FIFO方式保存入队的命令。

3,事务的执行

客户端发送exec命令,服务器执行exec命令逻辑

如果客户端状态的flags属性不包含reids_multi标识或者包含
redis_dirty_exec标识,那么就会直接取消事务的执行。
否则客户端处于事务状态,服务器会遍历客户端的事务队列,然后执行事务队列中的所有命令,最后将返回结果全部返回给客户端。

redis不支持事务回滚机制,但是它会检查每一个事务中的命令是否错误。

redis事务不支持检查那些程序员自己逻辑的错误,例如对String类型的数据库键执行对HashMap类型的操作

watch命令是一个乐观锁,可以为redis事务提供(CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会被执行,监控一直持续到exec命令。

multi命令用于开启一个事务,它总是返回ok,multi命令执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会被立即执行,而是放到一个队列中,当exec命令被调用时,所有队列中的命令才会被执行。

exec:执行所有事务块内的命令,返回的事务块所有命令的返回值,按命令执行的先后顺序排列,当操作被打断后,返回空值null。

通过调用discard,客户端可以清空事务队列,并放弃执行事务,并且客户端可以从事务状态中退出。
unwatch命令可以取消watch对所有key的监控。

Redis的过去键的删除策略

Redis是key-value数据库,我们可以设置redis中缓存的key的过期时间。redis的过期策略就是指当redis中缓存的key过期了,redis怎么处理。

惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除,该策略可以最大化的节省CPU资源,却对内存非常不友好,极端情况可能出现大量过期key没有再次被访问,从而不会被清除,占用大量内存。

定期过期:每隔一定时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案,通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最忧的平衡效果。

(expires字典会保存所有设置了过期时间的key的过期时间数据,其中key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间,键空间是指该redis集群中保存的所有键)

reids中同时使用了惰性过期和定期过期两种过期策略。

Redis线程模型,单线程快的原因。

redis基于Reactor模式开发了网络事件处理器,这个处理器叫作文件处理器 file event handler。这个文件事件处理器,它是单线程的,所以redis才叫作单线程的模型。它采用IO多路复用机制来同时监听多个Socket,根据Socket上的事件类型来选择对应的事件处理器来处理这个事件,可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了Redis内部的线程模型的简单性。

文件事件处理器的结构包含4个部分:多个Socket,IO多路复用程序,文件事件分派器以及事件处理器(命令请求处理器,命令回复处理器,链接应答处理器等)。

多个Socket可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个Socket,会将Socket放入一个队列中排队,每次从队列中取出一个Socket给事件分派器,事件分派器把Socket给对应的事件处理器。
然后一个Socket的事件处理完之后,IO多路复用线程才会将队列中的下一个Socket给事件分派器。文件事件分派器会根据每个Socket当前产生的事件,来选择对应的事件处理器来处理。

单线程快的原因:

1,纯内存操作
2,核心是基于非阻塞的IO多路复用机制
3,单线程反而避免了多线程频繁的上下文切换带来的性能问题。

缓存雪崩,缓存穿透,缓存击穿

缓存雪崩是指缓存同一时间大面积的失效,索引,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案:

(1)缓存数据的过期时间设置分开,防止同一时间大量数据过期现象发生。
(2)给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存,
(3)缓存预热,提交用接口将数据添加到缓存中
(4)互斥锁

缓存穿透是指缓存和数据库中都没有的数据,导致所以的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案

(1)接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截。
(2)从缓存取不到的数据,在数据库中也没有取到,这时可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击。
(3)采用布隆过滤器,将所有可能存在的数据哈希到一个足够强大的bitmap中,一个一定不存在的数据就会被bitmap拦截掉,从而避免了对底层储存系统的查询压力。

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),
这时由于并发用户特别多,同时读缓存没有读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案:
设置热点数据永远不过期

加互斥锁

redis的事务实现

1,事务开始
MULTI命令的执行,标识着一个事务的开始,MULTI命令会将客户端状态的flags属性中打开REDIS_MULTI标识来完成的。

2,事务入队

当一个客户端切换到事务状态之后,服务器会根据这个客户端发送来的命令来执行不同的操作。如果客户端发送的命令为MULTI,EXEC,WATCH,DISCARD中的一个,立即执行这个命令,否则将命令放入一个事务队列里面,然后向客户端返回QUEUED回复。

如果客户端发送的命令为MULTI,EXEC,WATCH,DISCARD中的一个,那么服务器立即执行这个命令。

如果客户端发送的是四个命令以外的其他命令,那么服务器并不立即执行这个命令。
首先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态(redisClient)的flags属性关闭REDIS_MULTI标识,并且返回错误信息给客户端。
如果正确,将这个命令放入一个事务队列里面,然后想客户端返回QUEUED回复,事务队列是按照FIFO方式保存入队的命令。

3,事务执行

客户端发送EXEC命令,服务器执行EXEC命令逻辑

如果客户端状态的flags属性不包含REDIS MULTI标识或者包含REDIS DIRTY_ACS或者REDIS_DIRTY_EXEC标识,那么就直接取消事务的执行。
否则客户端处于事务状态(flags有REDIS MULTI标识),服务器会遍历客户端的事务队列,然后执行事务队列中的所有命令,最后将返回结果全部返回给客户端。

redis不支持事务回滚机制,但是它会检查没一个事务中的命令是否错误。

redis事务不支持检查那些程序员自己逻辑的错误,例如对String类型的数据库键执行对HashMap类型的操作!

WATCH命令是一个乐观锁,可以为Redis事务提供check_and_set(CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。

MULTI命令用于开启一个事务,它总是返回ok,MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队里中,当EXEC命令被调用时,所以队列中的命令才会被执行。

EXEC:执行所以事务块内的命令,返回的事务块所以命令的返回值,按命令执行的先后顺序排列,当操作被打断时,返回空值 null。

通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务,并且客户端从事务状态中退出。
UNWATCH命令可以取消watch对所以key的监控。

setnx做分布式锁:

主要需要考虑几个问题

1,需要考虑可能出来的死锁

2,需要设置超时时间

3,锁误删

4,可重入以及锁续期可以使用redisson类似AQS的实现,看门狗监听机制

5,操作非原子性依然会存在误删,所以使用lur脚本使得操作全在redis中进行

6,redlock:红锁
意思是机制都只操作单节点,技术Redis通过sentinel保证高可用,如果这个master节点由于某些原因发送了主从切换,那么就会出现锁丢失的情况(rediis同步设置可能数据丢失),redlock从多个节点申请锁,当一版节点获取成功,锁才算获取成功,redisson有相应的实现。

redis集群方案

主从 :

哨兵模式:

sentinel,哨兵是reids集群中非常重要的一个组件,主要功能有,

集群监控:复制监控redis master和slave 进程是否正常工作。
消息通知:如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
故障转移:如果 master node 挂掉了,会自动转移到slave node上。
配置中心:如果故障转移发生了,通知client客户端新的master地址。

哨兵用于实现redis集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
故障转移时,判断一个 master node是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举。
即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的。
哨兵通常需要3个实例,来保证自己的健壮性。
哨兵+redis主从的部署框架,是不保证数据零丢失的,只能保证redis集群的高可用性,
在实际使用中,要在测试环境和生产环境进行严格的测试也演练。

RedisCluster是一种服务端Sharding技术,3.0版本开始正式提供,采用slot(槽)的概念,一共分出16384个槽,将请求发送到任意节点,接收到请求的节点会查询请求发送到正确的节点上执行。

方案说明:
通过哈希的方式,将数据分片,每个节点均分储存一定的哈希槽(哈希值)区间的数据,默认分配了16384个槽位
每份数据分片会储存在多个互为主从的多节点上
数据写入先写主节点,在同步到从节点(支持配置为阻塞同步)
同一分片多个节点间的数据不保存强一致性
读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点。
扩容时需要把旧节点的数据迁移一部分到新节点。

在redis cluster框架下,每个redis要开发两个端口号,比如一个是6379,另一个就是加 1w的端口号,比如16379。

16379端口号是用来进行节点间通信的,也就是cluster bus的通信,用来进行故障检测,配置更新,故障转移授权。cluster bus 用了另一种二进制的协议,gossip协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。

优点:

无中心结构,支持动态扩容,对业务透明
具备Sentinel的监控和自动Failover(故障转移)能力
客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
高性能,客户端直连redis服务,失去了proxy代理的损耗

缺点:
运维也很复杂,数据迁移需要人工干预
只能使用0号数据库
不支持批量操作(oipeline管道操作)
分布式逻辑和储存模块耦合等

Redis Sharding是redis Cluster出来之前,业界普遍使用的多redis实例集群方法,其主要思想是采用哈希算法将redis数据的key进行散列,通过hash函数,特点的key会映射到特点的redis节点上,java redis客户端驱动redis,支持redis sharding功能,即Shardedjedis以及结合缓存池的ShardedjedisPool。

优点:
优势在于非常简单服务端的redis实例彼此独立,相互无关联,每个redis实例像服务器一样运行,非常容易线性扩展,系统的灵活性很强。

缺点:

由于sharding处理放到客户端,规模进一步扩大时给运维带来挑战。

客户端sharding不支持动态增删节点,服务端redis实例群括补结构有变化时,每个客户端都需要更新调整,链接不能共享,当应用规模增大时,资源浪费制约优化。

redis主从复制的核心原理

通过执行slaveof命令或设置slaveof选项,让一个服务器去复制另一个服务器的数据,主数据库可以进行读写操作,当操作导致数据变化时会自动将数据同步给从数据库,而从数据库一般是只读的,并接受主数据库同步过来的数据,一个主数据库可以有多个从数据库,而一个从数据库只能拥有一个主数据库。

全量复制:
(1)主节点通过bgsave命令fock子进程进行RDB持久化,该过程是非常消耗CPU,内存(页表复制),硬盘IO的。
(2)主节点通过网络将RDB文件发送给从节点,对主从节点的带宽会带来很大的消耗,
(3)从节点清空老数据,载入新RDB文件的过程是阻塞的,无法响应客户端的命令,如果从节点执行bgrewriteaof,也会带来额外的消耗。

部分复制:
(1)复制偏移量:执行复制的双方,主从节点,分别维护一个复制偏移量offset
(2)复制积压缓冲区:主节点内部维护了一个固定长度的,先进先出(FIFO)队列,作为复制积压缓冲区,当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。
(3)服务器运行ID(runid):每个redis节点,都有其运行ID,运行ID由节点在启动时自动生成,主节点会将自己的运行ID发送给从节点,从节点会将主节点的运行ID存起来,从节点Redis断开连接的时候,就是根据运行ID来判断同步的进度
如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(根据offset和复制积压缓冲区情况而定)。
如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线钳同步的Redis节点并不是当前的主节点。只能进行全量复制。

过程原理图:
在这里插入图片描述

Springcloud的几大组件:

注册中心:eureka,zk,nacos

负载均衡:Ribbon,从一个服务的多台机器中选择一台(被调用的服务地址有多个),Ribbon也是通过发起http请求,来进行调用。
Feign:基于动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求,简化服务器间的调用。在Ribbon的基础上做了进一步的封装。在引入Spring Cloud Feign后,我们只需要创建一个接口并用注解的方式来配置它,即可完成对服务提供方的接口绑定。
调用远程就像调用本地服务一样。

服务熔断:Hystrix发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同的服务调用的隔离, 通过统计接口超时次数返回默认值,来实现服务熔断和降级

路由网关:Zuul,如果前端,服务端要调用后端系统,统一从Zuul网关进入由Zuul网关转发请求给对应的服务,通过与Eureka进行整合,将自身注册为Eureka下的应用,从Eureka下获取所有服务的实例,来进行服务的路由,Zuul还提供了一套过滤器机制,开发者可以自己指定那些规则的请求需要执行校验逻辑,只有通过校验逻辑的请求才会被路由到具体的服务实例上,否则返回错误提示。

Gateway 是什么

Spring Cloud Gateway 是 Spring 官方开发的网关,Spring Cloud Gateway 旨在为微服务
架构提供一种简单而有效的统一的 API 路由管理方式。Spring Cloud Gateway 作为
Spring Cloud 生态系中的网关,目标是替代 ZUUL,其不仅提供统一的路由方式,并且
提供了网关基本的功能,例如:安全,监控/埋点,和限流等。

Zuul 和 Gateway 的区别

①相同点:
(1)底层都是 servlet
(2)两者均是 web 网关,处理的是 http 请求
②不同点:
(1)内部实现:
gateway 对比 zuul 多依赖了 spring-webflux,在 spring 的支持下,功能更强大,内
部实现了限流、负载均衡等,扩展性也更强,但同时也限制了仅适合于 Spring Cloud
套件
zuul 则可以扩展至其他微服务框架中,其内部没有实现限流、负载均衡等。
(2)是否支持异步
zuul 仅支持同步
gateway 支持异步。理论上 gateway 则更适合于提高系统吞吐量(但不一定能有更
好的性能),最终性能还需要通过严密的压测来决定
(3)框架设计的角度
gateway 具有更好的扩展性,并且其已经发布了 2.0.0 的 RELESE 版本,稳定性也是
非常好的

CAP理论,BASE理论

Consistency(一致性)
即更新操作成功并返回客户端后,索引节点在同一时间的数据完全一致。
对于客户端来说,一致性指的是并发访问时更新过的数据如果获取的问题。
从服务端来看,则是更新如果复制分布到整个系统,以保证数据最终一致。

Availability(可用性):
即服务一直可用,而且是正常响应时间,系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。

Partition Tolerance(分区容错性):

即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。分区容错性要求能够使应用虽然是一个分布式系统,而看上去去好像是在一个可以运作正常的整体,比如现在的分布式系统中有某一个或者几个机器宕机掉了,其他剩下的机器还能够正常运作满足系统需求,对于用户而言并没有什么体验上的影响,

CP和AP:分区容错性是必须保证的,当发生网络分区的时候,如果要继续服务,那么强一致性和可用性只能2选1。

BASE是Basically Available(基本可用),Soft state(软状态)和Eventually consistent(最终一致性)

BASE理论是对CAP中一致性和可用性权衡的结果,其来源对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,单每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

基本可用:
响应时间上的损失:正常情况下,用户可以使用系统全部功能,但是系统访问量突然剧增,系统的部分非核心功能无法使用。
软状态:数据同步允许一定的延迟
最终一致性:系统中所以的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态,不要求实时。

分布式事务解决方案

2PC

最大的缺点是一个阻塞协议,RM在执行分支事务后需要等待TM的决定,此时服务会阻塞并锁定资源,由于其阻塞机制和最差时间复杂度高,因此这种设计不能适应随着事务涉及的服务数量增加而扩展的需要,很难用于并发较高以及子事务生命周期较长的分布式服务中。

TCC

2PC通常都是在垮库的DB层面,而TCC则在应用层面的处理,需要通过业务逻辑来实现,这种分布式事务的优势在于,可以让应用自己定义数据操作的粒度,使得降低锁冲突,提高吞吐量成为可能,而不足之处在于对于应用的侵入性非常强,业务逻辑的每个分钟都需要实现try、confirm、cancel三个操作、
此外其实现难度也比较大,需要按照网络状态。系统故障等不同失败原因实现不同的回滚策略。
典型使用场景: 满减,登录送优惠卷等。

可靠消息最终一致性

该事务方案适合执行周期长其实时性要求不高的场景,引入消息机制后,同步的事务操作变为基于消息执行的异步操作,避免了分布式事务中同步阻塞操作的影响,并实现了两个服务的解耦,
典型使用场景:注册送积分,登录送优惠卷等。

最大努力通知

该方案是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务,允许发起通知方处理业务失败,在接收方收到通知后积极进行失败处理,无法发起通知费如何处理结果都不会影响到接收通知方的后续处理。 发起通知方需提供查询执行情况接口,用于接收通知方校对结果。
典型的使用场景: 银行通知,支付结果通知等。

负载均衡算法,类型

(1)轮询法
将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际连接数和当前的系统负载。

(2)随机法
通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问,由概率统计理论可以得知,随着客户端调用服务端的次数增多。其实际效果越来越接近于平均分配调用量到后端每一台服务器,也就是轮询的结果。

(3)源地址哈希法
源地址哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得出的一个数值,用该数值对服务器列表的大小进行取模运算,得出的结果便是客户端要访问服务器的序号,采用源地址哈希进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。

(4)加权轮询法
不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此他们的抗压能力也不相同,给配置高,负载低的机器配置更高的权重,让其处理更多的请求;而配置低,负载高的机器,给其分配较低的权重,降低系统负载,加权轮询能很好的处理这一问题,并将请求顺序按照权重分配到后端。

(5)加权随机法
与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重,不同的是,他是按照权重随机请求后端服务器,而非顺序。

(6)最小链接法
最小链接算法比较灵活和智能,由于后端服务器的配置不尽相同,对应请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求,尽可能的提高后端服务器的理由效率,将负责合理的分流到每一台服务器。

类型:
DNS方式实现负责均衡
硬件负载均衡:F5和A10
软件负载均衡:
Nginx,Haproxy,Lvs。其中的区别:

Nginx:七层负载均衡,支持HTTP,E-mail协议,同时也支持4层负载均衡;

HAproxy:支持七层规则,性能也很不错,OpenStack默认使用的负载均衡软件就是HAproxy。
LVS:运行在内核态,性能是软件负载均衡中最高的,严格来说工作在三层,所以更通用一些,适合各种应用服务。

分布式架构下,Session共享有什么方案

(1)采用无状态服务,抛弃session
(2)存入cookie(有安全风险)
(3)服务器直接进行Session同步,这样可以保证每个服务器上都有全部的Session信息,bug当服务器数量比较多的时候,同步是会有延迟甚至同步失败;
(4)IP绑定策略
使用Nginx(或其他负载均衡软硬件)中的IP绑定策略,同一个IP只能在指定的同一个机器访问,但是这样做就失去了负载均衡的意义,当挂掉一台服务器的时候,会影响一批用户的使用,风险很大。
(5)使用Redis储存
把Session放到redis中储存,虽然结构上变得复杂,并且需要多访问一次Redis,但是这种方案带来的好处也是很大的;
实现了Session共享;
可以水平扩展(增加redis服务器)
服务器重启Session不丢失(不过也有注意Session在redis中的刷新/失效机制)
不仅可以跨服务器Session共享,甚至可以跨平台(例如网页端和APP端)

RPC,RMI的理解

RPC:在本地调用远程的函数,远程过程调用,可以跨语言实现 httpClient。

RMI:远程方法调用,java中用于实现RPC的一种机制,RPC的java版本,是j2EE的网络调用机制,跨JVM调用对象的方法,面向对象的思维方式

直接或间接实现接口 java.rmi.Remote 成为存在于服务器端的远程对象,供客户端访问并提供一定的服务

远程对象必须实现 (java.rmi.server.UniCastRemoteObject类),这样才能保证客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理,用于与服务器端的通信,而骨架也可以认为是服务器端的一个代理,用于接收客户端的请求之后调用远程方法来响应客户端的请求。

手写一个快速排序

 首先取一个数作为基准key,将比它大的放在右边,比它小的放在左边。
  然后取一个数作为key,递归-1,调用其他数比较并交换直到完成排序

     public class QuickSort {
        
        static int[] arr = new int[]{3,2,6,8,9,10,1};

        public static void main(String[] args){
              quick(arr,0,arr.lenght-1);
              for (int i : arr){
                    System.out.println(i);
              }
        }
        
        public static void quick(int[] arr,int left,int right){
            if(left>right){
                  return;
            }
            if(left > right){
              return;
             }
             int key = arr[left];
             int l = left;
             int r = right;
             while(l ! = r){
                  while(arr[r] >= key && l < r){
                      r--;
                  } 
                  while(arr[l] <= key && l < r){
                      l--;
                  }

                  if(l < r){
                     int temp = arr[l];
                     arr[l] = arr[r];
                     arr[r] = temp;
                  } 
             }
             
             arr[left] = arr[l];
             arr[l] = key;
             quick(arr,left,l-1);
             quick(arr,l+1,right);
        }
        

}


手写一个归并排序

手写一个反转链表

反转链表有两种实现方式:
一种通过栈的先进后出机制将其反转。
一种通过递归将其反转。

栈的实现方式:

public ListNode reverseList(ListNode head){

    if(head == null) return null;
    Stack<ListNode> stack = new Stack<>();
    stack.push(head); //存入第一个节点
    while (head.next != null) {
       stack.push(head.next);// 存入其他节点
       head = head.next; // 指针移动的下一位
    }
    
    //反转链表
    ListNode listNode = stack.pop(); //反转第一个元素
    ListNode listNode = listNode; //临时节点,在下面的while中记录上一个节点
    while (!stack.isEmpty()){
    ListNode item = stack.pop();//当前节点
    listNode.next = item;
    lastNode = item;
    }
    lastNode.next = null; //最后一个节点赋值为ull(不然会造成死循环)
    return listNode;
}

递归实现

public static ListNode reverseList(ListNode head) {
    if (head == null || head.next == null) return head;
    // 从下一个节点开始递归
    ListNode reverse = reverseList(head.next);
    head.next.next = head; // 设置下一个节点的 next 为当前节点
    head.next = null; // 把当前节点的 next 赋值为 null,避免循环引用
    return reverse;
}

单例模式知道吗?

单例模式就是 保证一个类只有一个实例化对象,并且提供一个可以被全局访问的对象。

单例模式分为饿汉单例和懒汉单例。

饿汉单例就是等待对象被需求时才进行创建。

懒汉式单例就是对象和类一起在初始化阶段创建出来。

懒汉式单例具体实现方法:首先创建一个局部变量,然后将构造器私有化,最后提供一个可以被外部访问的方法。

public class Single {
    //首先创建一个局部变量
    private static Single instance;

    //然后将构造器私有化
    private Single(){
    System.out.println("创建了Single对象")
    }
    //最后提供一个可以被外部访问的方法。
    public static Single getInstance(){
    if(instance == null) instance = new Single();
    return instance;
    }
}

饿汉式单例就是在创建局部变量时就让他跟类一起加载。

public class Single {
    //首先创建一个局部变量
    private static Single instance = new instance;

    //然后将构造器私有化
    private Single(){
    System.out.println("创建了Single对象")
    }
    //最后提供一个可以被外部访问的方法。
    public static synchronized Single getInstance(){
    return instance;
    }

但是单例模式在多线程状态下要用同步关键字synchronized来保证同步,并且需要volatile来保证变量对其他线程的可见性和防止发生指令重排。在代码上我们使用双重校验的方式。

public class Single {
    //首先创建一个局部变量
    private volatile static Single instance;

    //然后将构造器私有化
    private Single(){
    System.out.println("创建了Single对象")
    }
    //最后提供一个可以被外部访问的方法。
    public static synchronized Single getInstance(){
    if(instance == null){
      synchronized  (Single.class){
       if(instance == null){
         instance = new Single();
         }
      }
    }
    return instance;
    }
}

手写一个线程安全的单例模式

单例模式在多线程状态下要用同步关键字synchronized来保证同步,并且需要volatile来保证变量对其他线程的可见性和防止发生指令重排。在代码上我们使用双重校验的方式。

public class Single {

   //首先创建一个局部变量
   private volatile static Single instance;

   //然后将构造器私有化
   private Single(){
    System.out.println("创建了Single对象")
   }
   //最后提供一个可以被外部访问的方法
   public static synchronized Single getInstance(){

   //做双重校验
   if(instance == null){
    synchronized (Single.class){
    if(instance == null){
     instance = new Single();
     }
    }
   }
   return instance;
   }
}
}

进程和线程有什么区别?

简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
线程的划分尺度小于进程,使得多线程程序的并发性高。
另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.

进 程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个 进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序 健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

进程间的通信方式有哪些?

主要有管道,消息队列,信号量,信号,共享内存,套接字socket。

1,管道

管道主要包括无名管道和命名管道。

1.2 普通管道PIPE:
1)它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。
2)它只能用于具有亲缘关系的进程之间的通信(父子或兄弟进程之间)。
3)它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read,write等函数,但是它不是普通的文件,并不属于其他任何文件系统,并且只保存在内存中

1.2 命名管道FIFO

1)FIFO可以在无关的进程之间交换数据。
2)FIFO有路径名与之相关联,它以一种特殊的设备文件形式存在于文件系统中。

2,消息队列

消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标记。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限制的缺点。有写权限的进程可以按照一点的规则向消息队列中添加新消息;有读权限的进程则可以从消息队列中读取信息。

1)消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
2)消息队列独立于发送与接收进程。进程终止时,消息队列及内容并不会被删除。
3)消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

3 信号量semaphore

信号量(semaphore) 是一个计数器,可以用来控制多个进程对共享资源的访问,信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

1)信号量用于进程间同步,若要在进程间传递数据需要结合共享内存
2)信号量基于操作系统的PV操作,程序对信号量的操作都是原子操作。
3)每次对信号量的PV操作不仅限于对信号量值加1或减1,而且可以加减任意正整数。
4)支持信号量组

信号signal

信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

共享内存(Shared Memory)

它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程对共享内存中数据的更新,这种方式需要依靠某种同步操作,如互斥锁和信号量等。

1)共享内存是最快的一种IPC,因为进程是直接对内存进行存取。
2)因为多个进程可以同时操作,所以需要进行同步
3)信号量+共享内存通常结合在一起使用,信号量用来同步共享内存的访问。

套接字SOCKET

socket也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信。

线程间的通信方式有哪些?

1,临界区:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。

2,互斥量Synchronized/Lock : 采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问
3,信号量Semphare: 为控制具有有限数量的用户资源而设计的,它允许多个线程在同一个时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。
4,事件(信号)Wait/Notify: 通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作

二:JAVA有哪几种IO模型?有什么区别?

BIO 同步阻塞IO。 可靠性差,吞吐量低,适用于连接比较少且比较固定的场景,JDK1.4之前唯一的选择。

编程模型最简单
在这里插入图片描述

NIO 同步非阻塞IO。 可靠性比较好,吞吐量比较高,适用于连接比较多并且连接比较短(轻操作),例如聊天室。JDK1.4开始支持

编程模型最复杂
在这里插入图片描述

AIO 异步非阻塞IO 可靠性是最好的,吞吐量是非常高的。适用于连接比较多,并且连接比较长(重操作) 例如相册服务器
JDK7之后开始支持。
编程模型比较简单,但是需要操作系统来支持
在这里插入图片描述

同步,异步-针对请求,和阻塞,非阻塞-针对客户端。
在一个网络请求中,客户端会发一个请求到服务端。

1,客户端发了请求后,就一直等着服务端响应 客户端: 阻塞。
请求: 同步

2,客户端发了请求后,就去干别的事情了,时不时的过来检查服务端是否给出了响应。 客户端: 非阻塞。 请求:同步。

3,换成异步请求。客户端发了请求后,就坐在椅子上,等待服务端返回响应。 客户端: 阻塞。 请求:异步。

4,客户端发了请求后,就去干别的事情了,等到服务端给出响应后,在过来处理业务逻辑。 客户端:非阻塞。请求:异步。

TCP和UDP有什么区别?

TCP是一种面向连接的,可靠的,传输层通信协议。
特点:好比是打电话,面向连接的,点对点的通信,高可靠的,效率比较低,占用的系统资源比较多。

UDP 是一种简单的无连接的,不可靠的,传输层通信协议。
特点:好比是广播: 不需要连接,发送方不管接收方有没有准备好,直接发消息,可以进行广播发送的,传输不可靠,有可能丢失消息;效率比较高,协议就会比较简单,占用系统资源较少。

三:JAVA NIO的几个核心组件是什么?分别有什么作用?

分别有
Channel
数据流

Buffer
用来储存Channel流的缓存区

Selector
相当于一个总的管理器
在这里插入图片描述
类似一个流,每个Channel对应一个buffer缓存区。channel会注册到selector

slelct会根据channel上发生的读写事件,将请求交给某个空闲的线程处理。
selector对应一个或者多个线程

Buffer和Channel都是可读可写的。

四:select,poll和epoll有什么区别?

他们是NIO中多路复用的三种实现机制,是由Linux操作系统提供的。

首先要了解用户态和内存态的概念;操作系统为例保护系统安全,将内核划分为两个部分,一个是用户空间,一个是内核空间,用户空间不能直接访问底层的硬件设备,必须通过内核空间。

文件描述符 File Descriprot(FD) : 是一个抽象的概念,形式上是一个整数,实际上是一个索引值。指向内存中每个进程维护进程所打开的文件的记录表。当程序打开或者创建一个文件时,内核就会向进程返回一个FD。
这个概念只在 Unix,Linux中存在。

select机制: 会维护一个FD的集合 fd_set。将fd_set从用户空间复制到内核空间,激活socket, fd_set是一个数组结构。x64 2048

poll机制: 和selecter机制是差不多的,把fd_set结构进行了优化,FD集合的大小就突破了操作系统的限制。用pollfd结构来替换fd_set,通过链表实现的

Epoll: Event Poll.Epoll不再扫描所有的FD,只将用户关心的FD的世界存放到内核的一个事件表汇总,这样可以减少用户态与内核态之间需要拷贝的数据

简单总结一下他们的不同点:
操作机制 – 操作方式 – 底层实现 – 最大连接数 – IO效率
select: ----遍历 -------- 数组 ------- 受限于内核 – 一般
poll : ----- 遍历 -------- 链表 ------- -无上限 ------- 一般
epoll : — 事件回调 — 红黑树 ---- 无上限 -------- 高

java中的NIO当中是用的哪种机制?可以查看(DefaultSelectorProvider)源码。在windows下(WindowsSelectorProvider)。

在Linux下,根据Linux的内核版本,2.6版本以上,就是(EPollSelectorprovuder),否则就是默认的(PollSelectorProvider)。

select 1984年出现,poll 1997年出现, EPoll 2002年出现。

五:描述下HTTP和HTTPS的区别

HTTP:是互联网上应用最为广泛的一种网络通信协议,基于TCP,可以使浏览器工作更为高效,减少网络传输

HTTPS:是HTTP的加强版,可以认为是HTTP+SSL(Secure Socket Layer).在HTTP的基础上增加了一系列的安全机制。一方面保证数据传输安全,另一方面对访问者增加了验证机制。是目前现行架构下,最为安全的解决方案。

1,HTTP的连接是简单无状态的,HTTPS的数据传输是经过证书加密的,安全性更高。
2,HTTP是免费的,而HTTPS需要申请证书,而证书通常是需要收费的,并且费用一般不低。
3,他们的传输协议不同,所以他们使用端口号也是不一样的,HTTP默认是80端口,而HTTPS默认是443端口。

HTTPS的缺点:

  1,HTTPS的握手协议比较费时,所以会影响服务的响应速度以及吞吐量。
  2,HTTPS也并不是完全安全的。他的证书体系其实并不是完全安全的,并且HTTPS在面对DDOS这样的攻击时,几乎起不到任何作用。
  3,证书需要花钱,并且功能越强大的证书花费越高。

TCP是怎么保证可靠性的?

1,建立连接 :
通过三次握手建立连接,保证连接实体真实存在

2,序号机制:保证数据是按序,完整到达

3,合理分片:tcp会按最大传输单元(MTU)合理分片,接收方会缓存未按序到达的数据,重新排序后交给应用层

4,数据校验: TCP报文头有校验和,用于校验报文是否损坏

5,超时重传 : 如果发生一直收不到应答,可能是发生数据丢失,也可能是应答丢失,发生方在等待一段时间之后都会进行重传

6,流量控制:当接收方来不及处理发送方的数据,能通过滑动窗口,提示发送方降低发生的速率,防止报丢失

7,拥塞控制: 网络层拥堵造成的拥塞,包括慢启动,拥塞避免,快速重传三种机制

TCP的滑动窗口原理

1.概述

滑动窗口通俗来讲就是一种流量控制技术。

它本质上是描述接收方的TCP数据报缓冲区大小的数据,发送方根据这个数据来计算自己最多能发送多长的数据,如果发送方收到接收方的窗口大小为0的TCP数据报,那么发送方将停止发送数据,等到接收方发送窗口大小不为0的数据报的到来

ping命令的底层用的是什么协议?

ping命令使用的是TCP下的ICMP子协议,有些防火墙会屏蔽ICMP协议,所以有时候ping的结果只能作为参考,ping不通并不一定说明对方IP不存在。

ping的原理是向指定的IP地址发送一定长度的数据包,按照约定,若指定IP地址存在的话,会返回同样大小的数据包,当然如果没有在特定的时间内返回,就是超时,就认为指定的IP不存在。

OSI 7层模型有哪些?

分别是:

应用层:负责对软件提供接口时程序能使用网络服务

表示层:应用程序和网络之间的翻译官

会话层:负责在网络中的两节点之间建立和维持通信

传输层:建立端到端之间的连接,数据的分段和重组

网络层:将网络地址翻译成对于的mac地址,指导数据包的转发

数据链路层: 将网络层接收到的数据报封装为特定的数据帧,使其在不可靠的物理链路上进行可靠的数据传输

物理层:

服务的稳定性治理

在保证代码符合高可用规范下,用监控,压测,演练等来确保服务的稳定性

监控

监控如果做到了360无死角,则可以第一时间主动发现系统异常,定位到了解决则是相对明确的。那么稳定性自然也有很高的保障,可以说监控是稳定性保障的前提。

那么如何做监控?做哪些监控?这个是每个工程师要细致思考的问题。监控不仅仅是发一条短信出来,告知工程师“系统异常”,而是应该快速告知工程师,哪个系统的哪个接口出现了什么样的异常,越具体,就能够帮助工程师快速定位问题,也就给工程师止血和恢复提供了更快的可能。

监控的告警方式一般有短信、电话、邮件,或者可以使用钉钉通知、钉钉机器人等方式。

应用性能监控

load监控
load监控是对于容器和应用层面的监控,可以监控整个容器水位消耗,对业务负载能够起一个快速的决策。比如正常情况下。

load详解文章:https://www.ruanyifeng.com/blog/2011/07/linux_load_average_explained.html

简单来说,对于单核CPU,load为1表示已经满载了,如果超过1意味着好负荷了,有任务已经无法直接占用CPU资源需要一直等待,系统也可能即将崩溃。同理,对于多核CPU,load则对应具体的核数。一般来说4核CPU的话,load为4表示满载。正常情况下,70%的负荷率是比较正常的,即单核CPU的Load要小于等于0.7。

java线程数

java中线程是宝贵的资源,各种代码运行后都是以线程的方式来运转,线程是生产实例的最小单位,因此监控线程数就可以监控java应用的实际运行情况。相对load来说,java线程更纯粹,load包含了整个CPU的所有消耗,包括其他系统内置的消耗,而java线程则是统计了实际为应用本身提供服务的情况。

特别在多线程的情况下,可以快速发现是否有线程泄露等问题,这类问题往往通过其他指标无法直接观看。比如发现一个接口的RT已经接近超时,机器load飙升,但是接口本身又没有发现问题,这时候可以看看线程的行为。

GC监控

GC是垃圾收集的意思(GarbageCollection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。

简单来说,JAVA运行期间的数据是以对象的方式存在的,而对象又存在生命周期,在时刻运行期间会产生大量的对象占用内存,因此需要有垃圾对象回收的策略,称之为GC。

GC监控可以判断编码中的对象内存泄露,可以对JVM本身进行,也可以对不合理的编码方式进行监控优化。

业务指标监控

大盘可以根据业务情况区分,比如订单创建大盘、用户登录大盘、账单支付成功大盘等,大盘可以帮助从全局角度观察同比环比趋势。通过趋势图可以对波峰、波谷、毛刺进行分析定位问题和优化系统。比如在突然的流量暴涨和暴跌,可以在数据大盘图快速定位。

成功率

又称之为“接口健康度”,及在指定时间内成功请求/总请求的比率,这个是接口稳定性里最重要的指标,可以通过这个指标直接判定系统的可用性。

健康度可以根据自己业务目标设置告警等级,比如某些非核心功能,可能设置低于70%成功率告警,对于某些核心功能,比如IM的会话送达率,则可能设置99.99%以上。

RT值

接口重要的几个指标,除了QPS外,就是RT了,RT表示一个接口的响应能力,RT越短说明接口提供服务的效率越高,相反,RT越大则代表提供服务能力越弱,一个系统内部出现多种情况的故障时,一般伴随的就对外提供能力变弱,响应的就是RT会明显加长。

因此通过监控RT可以很好的监控一个系统的提供服务能力的强弱,不仅在故障期间能够更为直观,在平常也可以给工程师提供一个性能优化的视图。

主日志

除了几个常规指标外,还有一个指标叫主日志,即把核心的错误日志都打印到同一个文件,通过关键的关键字进行错误分类,进而实现对错误分类和错误详情进行告警,帮助快速处理和解决线上问题,实现免登机器定位错误的能力。

DB监控

相对应用来说,DB往往是整个系统的性能瓶颈。优化一个准则,也是尽量把流量挡在DB外。也很容易理解,代码或硬件问题,可以通过集群和分布式来解决,但是所有写流量基本上都要集中在一个数据库实例中。因此数据库crash了往往会造成整体功能不可用。

当然在方案设计时候,可以显示的优化DB层面的设计,比如读写分离、数据缓存、分库分表等。这个对数据库的设计在其他内容中介绍,本文还是关注如何做好DB监控。

QPS

qps 每秒处理的查询数。

QPS和TPS是判断整体数据库的核心指标,该指标也表示了当前的数据库运行能力,当然应用层不好判定具体暴涨业务的时候,可以通过数据库的SQL来对数据进行定位,辅助定位到异常的SQL语句,进而分析故障原因。

TPS

tps 每秒处理的事务数

慢SQL

数据查询变慢的一个原因,就是慢SQL,慢SQL顾名思义,就是一条执行很慢的SQL,主要是查询慢,一般来说SQL查询都是毫秒级别,到了秒级的话,就是基本慢SQL了。慢SQL严重性不仅是本身查询慢,而是慢SQL会大量消耗数据库本身的资源,比如数据库的线程、CPU、内存等,从而使得整体服务能力下降,最终拖垮整个数据库。

压测

相对运行期间监控来说,压测是运行前的一个提前发现问题的操作。压测也需要做到常规化、持续化执行。压测可以用自动化的手段来在真实环境下获得系统的稳定性问题,提前发现系统异常和薄弱环节。

像大型的双十一,就会用提前压测的方法模拟双十一甚至超过几倍的请求,来进行全链路探测系统问题,并及时进行修复解决,这样当然就不怕双十一期间的自然大流量完全压垮系统。也提前做到了心中有数。

压测一般用我们上述提及的指标作为衡量标准,比如load是否到1,成功率是否下跌,主日志是否出现错误等等。

演练

监控发现问题治理,压测探查系统薄弱瓶颈,而演练则是在生产上真实的创建故障,用来发现系统稳定性、鲁棒性和自动恢复性,还能检测应用负责人是否有快速响应系统异常的能力、止血和修复的能力。

演练类似消防演习,即人工的采取某些措施,比如断网、停DB、MOCK缓存失败等。

总结

稳定性治理三板斧:监控、压测和演练。应用容器三要素:load、线程、GC。业务数据监控四要素:大盘、成功率、RT值、主日志。DB监控三要素:QPS、TPS、慢SQL。

系统稳定性压倒一切,只有保障了好了稳定性,才能帮助业务蓬勃增长,因此稳定性治理始终是工程师基本能力之一。

ElasticSearch和solr的区别?

架构的选择!!!
当单纯的对已有数据进行搜索时,Solr更快。
当实时建立了索引时,Solr会产生io阻塞,查询性能较差。
ElasticSearch具有明显的优势。
随着数据量的增加,Solr的搜索效率会变得更低,而ElasticSearch却没有明显的变化。

1,es基本是开箱即用,非常简单,Solr安装略微复杂一些。
2,Solr利用Zookeeper进行分布式管理,而ElasticSearch自身带有分布式协调管理功能。
3,Solr支持更多格式的数据,比如JSON,XML,CSV,而ElasticSearch仅支持JSON格式。
4,Solr官方提供的功能更多,而ElasticSearch本身更注重于核心功能,高级功能多有第三方插件提供,例如图形化界面需要kibana的友好支持。
5,Solr查询快,单更新索引时慢(即插入删除慢),用于电商等查询多的应用;
ES建立索引快(即查询慢),即实时性查询快,用于 facebook新浪等搜索;
Solr是传统搜索应用的有力解决方案,但ElasticSearch更适合新兴的实时搜索应用。
6,Solr比较成熟,有一个更大,更成熟的用户,开发,和贡献者社区,而ElasticSearch相对开发维护者较少,更新太快,学习使用成本较高。

ElasticSearch是一个基于Apache Lucene(TM)的开源搜索引擎。无论在开源还是专有领域,Lucene可以被认为是性能最好,功能最全额搜索引擎库。但是他只是一个库,所以需要使用java等开发语言将其集成到你的应用中。
ElasticSearch也使用java开发并使用Lucene作为其核心来实现所以索引和搜索的功能,但是他的目的是通过简单点的
RESTFUL API来隐藏Lucene的复杂性,从而让全文搜索变得简单。

Solr可以独立运行,运行在jetty,Tomcat等这些Servlet容器中
Solr的实现方法很简单用POST方法想Solr服务器发送一个描述Field及其内容的XML文档,Solr根据XML文档添加,删除,更新索引。Solr搜索只需要发送HTTP GET请求,然后对Solr返回XMl,json等格式的查询结果进行解析,组织页面布局。Solr不提供UI的功能,Solr提供了一个管理界面,通过管理界面可以查询Solr的配置和运行情况。

ES的核心概念!

ElasticSearch是面向文档,关系行数据库和ElasticSearch客观的对比!一切都是JSON。
在这里插入图片描述
物理设计:
ElasticSearch在后台把每个索引划分成多个分片,每分分片可以在集群中的不同服务器间迁移。
一个人就是一个集群!

逻辑设计:
一个索引类型中,包含多个问答。比如文档1,文档2.
当我们索引一篇文档时,可以通过这样的一个顺序找到他
:索引>类型>文档id,通过这个组合我们就能索引到某个具体的文档:注意ID不必是整数,实际上他是个字符串。

文档:
就相当于我们的一条条数据

类型:
类型是文档的逻辑容器,类型对应字段得定义称为映射。

索引:
就是数据库
索引是映射类型的容器,ElasticSearch中的索引是一个非常大的文档集合,索引储存了映射类型的字段和其他设置,然后他们被存储到了各个分片上。

节点和分片是如何工作的:
一个集群至少有一个节点,而一个节点就是一个ElasticSearch进程,节点可以有多个索引默认的,如果创建索引,那么索引将会有5个分片(primary shard,又称主分片)构成的,每一个主分片会有一个副本(replica shard,又称复制分片)

倒排索引:
ElasticSearch使用的是倒排索引的结构,采用Lucene倒排索引作为底层,这个结构用于快速的全文搜索,一个索引由文档中所有不重复的列表构成,对于每一个都要一个包含它的文档类别,例如现在有两个文档,每个文档内容如下:
在这里插入图片描述
为了创建倒排索引,我们首先要将每个文档拆分为独立的词(或称为词条或者tokens),然后创建一个包含所有不重复的词条的排序列表,然后列出每个词条出现在哪个文档:
在这里插入图片描述
现在我们试图搜索to forever,只需要查看包含每个词条的文档
在这里插入图片描述
两个文档都匹配,但是第一个文档比第二个匹配程度更高,如果没有别的条件,现在,这两个包含关键字的文档都将返回。

在ElasticSearch中,索引被分为多个分片,每个分片是一个Lucene的索引,所以一个ElasticSearch索引是由多个Lucene索引组成的。

RabbitMQ 两种消费模式

pull模式

消费者主动从消息中间件中拉取消息。
pull模式是每次接收消息都需要去拉取一下队列的信息,由于是拉取的,实用性较差,不能及时有效的获取最新的消息,能有效降低内存消耗。

push模式

消息中间件主动将消息推送给消费者。
push模式接收消息是最有效的一种消息处理方式,当我们使用该模式时,我们的消费端只要启动后,就想当年与使用了订阅模式,只要生产端不断推送消息,消费端就会持续接收消息。

RabbitMQ 的五种工作模式

RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此不予学习。那么也就剩下5种。但是其实3、4、5这三种都属于订阅模型,只不过进行路由的方式不同。
在这里插入图片描述
在这里插入图片描述

1,基本消费模型(Hello World)

在这里插入图片描述

RabbitMQ是一个消息代理:它接受和转发消息。 你可以把它想象成一个邮局:当你把邮件放在邮箱里时,你可以确定邮差先生最终会把邮件发送给你的收件人。 在这个比喻中,RabbitMQ是邮政信箱,邮局和邮递员。
RabbitMQ与邮局的主要区别是它不处理纸张,而是接受,存储和转发数据消息的二进制数据块。

P (producer/ publisher):生产者,一个发生消息的用户应用程序。
C (consumer): 消费者,消费和接收有关类似的消息,消费者是一个主要用来等待接收消息的用户应用程序。

队列 (红色区域)rabbitmq内部类似邮箱的一个概念,虽然消息流经rabbitmq和你的应用程序,但是它们只有存储在队列中,队列受主机的内存和磁盘限制,实质上是一个大的消息缓冲区,许多生产者可以发送消息到一个队列,许多消费者可以尝试从一个队列接收数据。

总之:生产者将消息发送到队列,消费者从队列中获取消息,队列是存储消息的缓冲区。

2,work消息模型(Work queues)

在这里插入图片描述
工作队列,又称任务队列。主要思想就是避免执行资源密集任务时,必须等待它的任务执行完成。相反我们稍后完成任务,我们将任务封装为消息并将其发送到队列。在后台运行的工作进程将获取任务并最终执行任务,当您的运行许多工人时,任务将在他们之间共享,但是一个消息只能被一个消费者获取。
总之:让多个消费者绑定到一个队列,共同消息队列中的消息,队列中的消息一旦消费,就会消失,因此任务不会被重复执行。

3,发布订阅模式

在这里插入图片描述
解读:

1,1个生产者,多个消费者

2,每一个消费者都有自己的一个队列

3,生产者没有将消息直接发送到队列,而是发送到了交换机

4,每个队列都要绑定到交换机。

5,生产者发送的消息,经过减环节到达队列,实现一个消息被多个消费者获取的目的

X(EXCHANGES): 交换机一方面: 接收生产者发送的消息,另一方面:知道如何处理消息,例如递交给某个特别队列,递交给所有队列,或是将消息丢弃。到底如果操作,取决于Exchange的类型。

Exchange类型有以下几种:
Fanout:广播,将消息交给所有绑定到交换机的队列

Direct:定向,把消息交给符合指定routing key 的队列

Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!

广播模式(Fanout)

Fanout,也称为广播。
在广播模式下,消息发送流程是这样的:

1) 可以有多个消费者
2) 每个消费者有自己的queue(队列)
3) 每个队列都要绑定到Exchange(交换机)
4) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
5) 交换机把消息发送给绑定过的所有队列
6) 队列的消费者都能拿到消息。实现一条消息被多个消费者消费

订阅模式-Direct

在这里插入图片描述
在Direct模型下,队列与交换机绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key),消息的发送方在向Exchange发送消息时,也必须指定消息的routing key。

P:生产者,向Exchange发送消息时,会指定一个routing key。
X: Exchange(交换机),接收生产者的消息,然后把消息递交给与 routing key完全匹配的队列

C1:消费者,其所在队列指定了需要routing key 为 error 的消息

C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息

订阅模型-Topic

在这里插入图片描述
Topic 类型的 Exchange 与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型 Exchange 可以让队列在绑定 Routing key 的时候使用通配符!

通配符规则:
中间以“.”分隔。
符号#可以匹配多个词,符号*可以匹配一个词语。

Mq 如何保证消息的顺序性

①拆分多个队列,每个队列一个消费者
②消费者从队列中提取出消息后,不直接消费消息,而是将消息保存在内存队列中,根
据关键值(比如订单 id)进行哈希操作,将关键值相同的消息发送到相同的消息队列里
面,消费者线程直接去消息队列获取消息消费,这样就可以保证消息的消费顺序

死信队列

死信队列是什么

死信,在官网中对应的单词为“Dead Letter”,可以看出翻译确实非常的简单粗暴。那么死信是个什么东西呢?

“死信”是RabbitMQ中的一种消息机制,当你在消费消息时,如果队列里的消息出现以下情况:

消息被否定确认,使用 channel.basicNack 或 channel.basicReject ,并且此时requeue 属性被设置为false。
消息在队列的存活时间超过设置的生存时间(TTL)时间。
消息队列的消息数量已经超过最大队列长度。
那么该消息将成为“死信”。

“死信”消息会被RabbitMQ进行特殊处理,如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃。

如何配置死信队列

这一部分将是本文的关键,如何配置死信队列呢?其实很简单,大概可以分为以下步骤:

1,配置业务队列,绑定到业务交换机上
2,为业务队列配置死信交换机和路由key
3,为死信交换机配置死信队列

注意,并不是直接声明一个公共的死信队列,然后所以死信消息就自己跑到死信队列里去了。而是为每个需要使用死信的业务队列配置一个死信交换机,这里同一个项目的死信交换机可以共用一个,然后为每个业务队列分配一个单独的路由key。

有了死信交换机和路由key后,接下来,就像配置业务队列一样,配置死信队列,然后绑定在死信交换机上。也就是说,死信队列并不是什么特殊的队列,只不过是绑定在死信交换机上的队列。死信交换机也不是什么特殊的交换机,只不过是用来接受死信的

交换机,所以可以为任何类型【Direct、Fanout、Topic】。一般来说,会为每个业务队列分配一个独有的路由key,并对应的配置一个死信队列进行监听,也就是说,一般会为每个重要的业务队列配置一个死信队列。

死信队列的变化

那么“死信”被丢到死信队列中后,会发生什么变化呢?

如果队列配置了参数 x-dead-letter-routing-key 的话,“死信”的路由key将会被替换成该参数对应的值。如果没有设置,则保留该消息原有的路由key。

举个栗子:

如果原有消息的路由key是testA,被发送到业务Exchage中,然后被投递到业务队列QueueA中,如果该队列没有配置参数x-dead-letter-routing-key,则该消息成为死信后,将保留原有的路由keytestA,如果配置了该参数,并且值设置为testB,那么该消

息成为死信后,路由key将会被替换为testB,然后被抛到死信交换机中。

死信队列的应用场景

通过上面的信息,我们已经知道如何使用死信队列了,那么死信队列一般在什么场景下使用呢?

一般用在较为重要的业务队列中,确保未被正确消费的消息不被丢弃,一般发生消费异常可能原因主要有由于消息信息本身存在错误导致处理异常,处理过程中参数校验异常,或者因网络波动导致的查询异常等等,当发生异常时,当然不能每次通过日志来获

取原消息,然后让运维帮忙重新投递消息(没错,以前就是这么干的= =)。通过配置死信队列,可以让未正确处理的消息暂存到另一个队列中,待后续排查清楚问题后,编写相应的处理代码来处理死信消息,这样比手工恢复数据要好太多了。

总结

死信队列其实并没有什么神秘的地方,不过是绑定在死信交换机上的普通队列,而死信交换机也只是一个普通的交换机,不过是用来专门处理死信的交换机。

总结一下死信消息的生命周期:

1,业务消息被投入业务队列

2,消费者消费业务队列的消息,由于处理过程中发生异常,于是进行了nck或者reject操作

3,被nck或reject的消息由RabbitMQ投递到死信交换机中

4,死信交换机将消息投入相应的死信队列

5,死信队列的消费者消费死信消息

死信消息是RabbitMQ为我们做的一层保证,其实我们也可以不使用死信队列,而是在消息消费异常时,将消息主动投递到另一个交换机中,当你明白了这些之后,这些Exchange和Queue想怎样配合就能怎么配合。比如从死信队列拉取消息,然后发送邮件、

短信、钉钉通知来通知开发人员关注。或者将消息重新投递到一个队列然后设置过期时间,来进行延时消费。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值