面试重难点
面试重难点
面试技巧
服务限流
-
自我介绍
我是中北大学软件学院大四学生。在校期间主要专注于java后端的学习,绩点3.6,看到了实习岗位内容,很感兴趣,希望能来尝试一下。
-
反问
如果我有幸入职,对于我这个岗位,您对我1到3年职业规划的建议是什么呢?
请问我的技术水平,有哪些是还需要提高的呢?如果我有幸实习,希望可以尽快弥补
-
HR面
-
擅长
成绩全专业15%,绩点3.6
- 课程
- 社会活动
- 比赛
-
公司了解
- 产品
- 服务
-
为什么面试
- 职位理解
- 职位匹配
-
个人优势和劣势
-
Java
-
io实现
分类 字节输入流 字节输出流 字符输入流 字符输出流 抽象基类 InputStream
OutputStream
Reader
Writer
节点流 访问文件(处理单位为单个字节/字符或数组) FileInputStream
FileOutputStream
FileReader
FileWriter
节点流 访问数组 ByteArrayInputStream
ByteArrayOutputStream
CharArrayReader
CharArrayWriter
处理流 缓冲流(处理单位为行) BufferedInputStream
BufferedOutputStream
BufferedReader
BufferedWriter
处理流 转换流(处理文件乱码) InputStreamReader
OutputStreamWriter
处理流 对象流(实现序列化和反序列化到文件) ObjectInputStream
ObjectOutputStream
处理流 打印流(默认输出到控制台) printOutputStream
PrintWriter
-
集合源码
-
-
构造方法
public ArrayList()
无参构造,初始化数组长度为0,第一次添加元素扩容时,会扩容到默认容量10public ArrayList(int initialCapacity)
指定数组容量public ArrayList(Collection<? extends E> c)
使用集合来创建
-
扩容机制
空参构造,不初始化数组长度,在第一次添加数据时,初始化数组长度为10。
添加一个元素,如果数组所需最小容量(size+1)大于数组长度就会触发扩容,扩容更新容量(1.5*size)为原来的1.5倍
- 更新容量小于最小容量
- 使用最小容量扩容
- 更新容量小于最大安全容量
- 最小容量大于最大安全容量,使用最大容量扩容
- 最小容量小于最大安全容量,使用最大安全容量扩容
Object[] elementData
数据数组size
数组长度minCapacity
最小容量newCapacity
更新容量MAX_ARRAY_SIZE
最大安全容量Integer.MAX_VALUE
最大容量
- 更新容量小于最小容量
-
自定义扩容
ensureCapacity()
-
EMPTY_ELEMENTDATA 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 默认构造和指定为0构造,添加元素后容量变化的凭证
-
transient Object[] elementData; 序列化节省空间
-
protected transient int modCount = 0; 增删方法添加该字段,提供fail-fast迭代器
-
-
- 扰动函数
(h = key.hashCode()) ^ (h >>> 16);
低16位与高16位做异或操作^,减少hash碰撞
- 扩容
- 链表引发
- 链表长度大于8,判断是否转为红黑树
- 数组大小大于64,转红黑树
- 数组大小小于64,扩容
- 链表长度大于8,判断是否转为红黑树
- 数组引发
- 数组元素大于容量与扩容因子之积,扩容
- 不同版本
- 1.7,直接计算下标,头插
- 1.8,旧数组&hash,尾插
- 过程
- 每次扩容时都是将容量翻倍,即创建一个2倍大的新数组,然后再将旧数组中的数组迁移到新数组里。由于HashMap中数组的容量为2^N,所以可以用位移运算计算新容量,效率很高。在数据迁移时,为了兼顾性能,不会重新计算一遍每个key的哈希值,而是根据位移运算后(左移翻倍)多出来的最高位来决定,如果高位为0则元素位置不变,如果高位为1则元素的位置是在原位置基础上加上旧的容量。
- 链表引发
- put
- HashMap 通过 key 的 hashCode 经过 hash() 处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置
- 如果当前位置无元素的话直接放在当前位置
- 如果当前位置有元素的话,通过key的equals方法进行判断,如果返回true的话直接更新当前位置,如果false的话需遍历链表,存在即覆盖,否则新增,此时链表时间复杂度为O(n)
- HashMap 通过 key 的 hashCode 经过 hash() 处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置
- get
- HashMap 通过 key 的 hashCode 经过 hash() 处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置
- 如果该位置无链表的话直接返回
- 如果该位置有链表的话需遍历链表,然后通过key对象的equals方法逐一比对查找
- HashMap 通过 key 的 hashCode 经过 hash() 处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置
- 扰动函数
-
- 原理
- 通过部分锁定+CAS算法来进行实现线程安全的
- 初始化
- 通过自旋和 CAS 初始化操作,扩容阈值0.75
- 扩容
- 1.7
- 扩容时,对待扩容Segment内部会进行扩容,不影响其他Segment对象
- 扩容时,先生成新的数组,然后转移元素到新数组中
- 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值
- 1.8
- 如果某个线程put时,发现没有正在进行扩容,那么该线程一起进行扩容
- 如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中, 然后判断是否超过阈值,超过了则进行扩容
- ConcurrentHashMap是支持多个线程同时扩容的。扩容前也是生成一个新数组,在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作
- 1.7
- put
- 先计算哈希值
- 再确定map是否初始化
- 插入数据
- 数组插入,cas
- 链表插入,先扩容,加锁
- get
- get方法是非阻塞,无锁的。重写Node类,通过volatile修饰next来实现每次获取都是最新设置的值
- 原理
-
- 原理
- cow(写时复制),类比linux
- 实现机制
- 复制数组+可重入锁
- add/set
- 添加的时候就上锁,并复制一个新数组,增加操作在新数组上完成,将array指向到新数组中,最后解锁
- 缺点
- 内存占用:如果CopyOnWriteArrayList经常要增删改里面的数据,比较耗费内存的。
- 数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
- 原理
HaahMap HashTable ConcurrentHashMap 底层实现 1.7数组+链表
1.8数组+链表/红黑树数组+链表 1.7Segment+数组+链表
1.8Synchronized 和 CAS数组+链表/红黑树初始容量及扩容 初始容量16
扩容因子0.75
newsize = oldsize*2初始容量11
扩容因子0.75
newsize = olesize*2+1初始容量16
扩容因子0.75
newsize = oldsize*2线程安全 不安全 安全 安全 寻址方式 hash=(h = key.hashCode()) ^ (h >>> 16)
index = hash & (tab.length – 1)hash=key.hashcode()
index = (hash & 231-1) % tab.lengthhash=(h = key.hashCode() ^ (h >>> 16)) & 231-1
index = hash & (tab.length – 1)null key一个为null,value多个为null 不支持,抛出异常 不支持,抛出异常 -
fail-fast&&fail-safe
- 快速失败(fail-fast)
- 在使用迭代器对集合对象进行遍历的时候,如果 A 线程正在对集合进行遍历,此时 B 线程对集合进行修改(增加、删除、修改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常。
- 安全失败(fail-safe)
- 采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常。
- 快速失败(fail-fast)
-
String创建对象
String str = "Hello";
- 类加载时期,在堆上创建对象,引用驻留在字符串常量池
- 运行时期,字符串常量池查找
- 存在,引用赋给局部变量表
- 不存在,在堆上创建对象,引用驻留在字符串常量池,引用赋给局部变量表
String str = new String("Hello");
- 类加载时期,字符串常量池查找
- 不存在,在堆上创建对象,引用驻留在字符串常量池
- 运行时期,在堆上创建对象,引用赋给局部变量表
- 类加载时期,字符串常量池查找
- 实际优化
- HotSpot会把无用的new String("")清除
- intern引用驻留
- jdk1.7复制一个副本放到常量池
- jdk1.8将在堆上的地址引用复制到常量池
- 详细解释
- 在类加载阶段, JVM会在堆中创建对应这些 class 文件常量池中的字符串对象实例 ,并在字符串常量池中驻留其引用。具体在resolve阶段执行。这些常量全局共享。而resolve阶段是懒加载的,所以只会字面量进入Class的常量池,不会在堆上创建实例,更不会驻留字符串常量池;具体在ldc是才会进入。所以我们只需要注意字面量是否重复出现及是否调用intern()方法即可。
- 请别再拿“String s = new String("xyz");创建了多少个String实例”来面试了吧
-
Sql
-
执行顺序
(1)from (2)on (3)join (4)where (5)group by (6)having (7)select (8)distinct (9)union (10)order by
-
关键字
left join ... on # on关键字是对left join的右表进行条件过滤,但是依旧会返回左表的所有内容,右表不满足on条件都置为null。 # and关键字,不管是左表还是右表的条件,左表的内容依旧不变,不符合and筛选条件的右表置为null。 # where关键字,不管你左表还是右表,只要不满足where筛选条件的两个表都会过滤掉。
GROUP BY # select语句后必须包含聚合函数或分组列 # HAVING:用于对分组后的数据进行筛选。
order by ... ASC 升序排序 order by ... DESC 降序
SUM() AVG() MAX() MIN() COUNT() distinct limit if (condition, true_result, false_result) ifnull(false_result,0) round(num,n) group_concat([distinct] 字段 [order by 排序字段] [separator '分隔符']) REGEXP SUBSTRING(column_name, start, length):这将从列的值中提取一个子字符串,从指定的起始位置开始,直到指定的长度。 UPPER(expression):这会将字符串表达式转换为大写。 LOWER(expression):这会将字符串表达式转换为小写。 CONCAT(string1, string2, ...):这会将两个或多个字符串连接成一个字符串。 LENGTH(str) CHAR_LENGTH(str)
# 行转列 SELECT product_id, 'store1' store, store1 price FROM products WHERE store1 IS NOT NULL UNION SELECT product_id, 'store2' store, store2 price FROM products WHERE store2 IS NOT NULL UNION SELECT product_id, 'store3' store, store3 price FROM products WHERE store3 IS NOT NULL; # 列转行 SELECT product_id, SUM(IF(store = 'store1', price, NULL)) 'store1', SUM(IF(store = 'store2', price, NULL)) 'store2', SUM(IF(store = 'store3', price, NULL)) 'store3' FROM Products1 GROUP BY product_id ;
-
-
binlog
逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于
MySQL Server
层。MySQL
数据库的数据备份、主备、主主、主从都离不开binlog
,需要依靠binlog
来同步数据,保证数据一致性。事务执行后提交
-
redo log
物理日志,记录内容是数据页面修改,属于
InnoDB
存储引擎。redo log
(重做日志)是InnoDB
存储引擎独有的,它让MySQL
拥有了崩溃恢复能力。事务执行中提交
-
undo log
回滚日志,记录内容是语句的反向逻辑,实现事务回滚和支持mvcc快照读,属于
InnoDB
存储引擎。InnoDB
通过数据行的DB_TRX_ID
和Read View
来判断数据的可见性,如不可见,则通过数据行的DB_ROLL_PTR
找到undo log
中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建Read View
之前已经提交的修改和该事务本身做的修改。DB_TRX_ID(6字节)
:表示最后一次插入或更新该行的事务 id。此外,delete
操作在内部被视为更新,只不过会在记录头Record header
中的deleted_flag
字段将其标记为已删除DB_ROLL_PTR(7字节)
回滚指针,指向该行的undo log
。如果该行未被更新,则为空DB_ROW_ID(6字节)
:如果没有设置主键且该表没有唯一非空索引时,InnoDB
会使用该 id 来生成聚簇索引
Read View
主要是用来做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务”事务执行中提交
-
Read view 匹配条件规则
- 如果数据事务ID
trx_id < min_limit_id
,表明生成该版本的事务在生成Read View前,已经提交(因为事务ID是递增的),所以该版本可以被当前事务访问。 - 如果
trx_id>= max_limit_id
,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。 - 如果
min_limit_id =<trx_id< max_limit_id
,需要分3种情况讨论- 如果
m_ids
包含trx_id
,则代表Read View生成时刻,这个事务还未提交,但是如果数据的trx_id
等于creator_trx_id
的话,表明数据是自己生成的,因此是可见的。 - 如果
m_ids
包含trx_id
,并且trx_id
不等于creator_trx_id
,则Read View生成时,事务未提交,并且不是自己生产的,所以当前事务也是看不见的; - 如果
m_ids
不包含trx_id
,则说明你这个事务在Read View生成之前就已经提交了,修改的结果,当前事务是能看见的。
- 如果
- 如果数据事务ID
-
-
慢Sql
-
发现慢SQL
show processlist
查询当前慢sql的语句- 开启慢日志
set global slow_query_log=1;
-
优化慢SQL
explain sql语句
的方式查看慢sql的执行计划- type 表示表的连接类型
- key 表示实际使用的索引
- rows 扫描出的行数(估算的行数)
- 分析该SQL语句索引使用情况,全表扫描情况
-
-
Sql死锁
-
事务之间对资源访问顺序的交替
A用户访问A资源,锁住A时请求B资源;B用户访问B资源,锁住B时请求A资源,产生死锁
- 实例
- 一个用户A 访问表A(锁住了表A),然后又访问表B;另一个用户B 访问表B(锁住了表B),然后企图访问表A;这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B才能继续,同样用户B要等用户A释放表A才能继续,这就死锁就产生了。
- 解决方法
- 多用户操作多表资源时,按照相同资源访问顺序进行处理
- 实例
-
并发修改同一记录
A用户访问A资源后,获取到共享锁,企图修改A资源;B用户修改A资源后,获取到独占锁,企图访问A资源
- 实例
- 用户A查询一条纪录,然后修改该条纪录;这时用户B修改该条纪录,这时用户A的事务里锁的性质由查询的共享锁企图上升到独占锁,而用户B里的独占锁由于A有共享锁存在所以必须等A释放掉共享锁,而A由于B的独占锁而无法上升的独占锁也就不可能释放共享锁,于是出现了死锁。这种死锁由于比较隐蔽,但在稍大点的项目中经常发生。
- 一般更新模式由一个事务组成,此事务读取记录,获取资源(页或行)的共享 (S) 锁,然后修改行,此操作要求锁转换为排它 (X) 锁。如果两个事务获得了资源上的共享模式锁,然后试图同时更新数据,则一个事务尝试将锁转换为排它 (X) 锁。共享模式到排它锁的转换必须等待一段时间,因为一个事务的排它锁与其它事务的共享模式锁不兼容;发生锁等待。第二个事务试图获取排它 (X) 锁以进行更新。由于两个事务都要转换为排它 (X) 锁,并且每个事务都等待另一个事务释放共享模式锁,因此发生死锁。
- 解决方法
- 乐观锁
- 悲观锁
- 更新锁
- 实例
-
索引不当导致全表扫描
事务A执行不满足条件的复杂SQL,执行全表扫描,企图从行级锁升级为表级锁,成功拿到表级锁;多个相同事务A执行不满足条件的复杂SQL,执行全表扫描,企图从行级锁升级为表级锁
- 解决方法
- SQL语句中不要使用太复杂的关联多表的查询
- 使用“执行计划”对SQL语句进行分析,对于有全表扫描的SQL语句,建立相应的索引进行优化。
- 解决方法
-
避免死锁
- 尽量让数据表中的数据检索都通过索引来完成,避免无效索引导致行锁升级为表锁。
- 合理设计索引,尽量缩小锁的范围。
- 尽量减少查询条件的范围,尽量避免间隙锁或缩小间隙锁的范围。
- 尽量控制事务的大小,减少一次事务锁定的资源数量,缩短锁定资源的时间。
- 如果一条SQL语句涉及事务加锁操作,则尽量将其放在整个事务的最后执行。
- 尽可能使用低级别的事务隔离机制。
-
产生死锁的必要条件:
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
- 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
- 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
- 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
-
-
Sql规范
- 多表关联最多2张
- 使用left join或right join代替not in和exist
- 不使用外键和触发器
- 查找避免索引失效
- 逻辑删除
- 更新使用id作为条件,更新非索引字段
- 增加 id有顺序
-
Sql索引失效
- Select * from
- 复合索引未用左列字段
- like以%开头
- or子句索引缺少条件
- where子句索引列存在运算
- where子句索引列使用函数
- where子句索引列隐式类型转换
-
Sql默认隔离级别
- 默认隔离级别为RR
- 原因
- RC隔离级别下的基于binlog的主从复制会发生问题
- 解决方案
- 使用RR隔离级别下的间隙锁
- 切换RC隔离级别的binlog日志模式为row
- 原因
- 互联网项目隔离级别为RC
- 在RR隔离级别下,存在间隙锁,易导致出现死锁
- 在RR隔离级别下,条件列未命中索引会锁表;而在RC隔离级别下,只锁行。
- 在RC隔离级别下,半一致性读(semi-consistent)特性增加了update操作的并发性。
- 默认隔离级别为RR
-
分库分表
-
垂直分表:把一个宽表的字段按照访问频率、是否是大字段的原则拆分为多个表,这样既能使业务清晰,还能提高部分性能。拆分后,尽量从业务角度避免联查,否则性能方面将得不偿失。
-
垂直分库:把多个表按照业务的耦合性来进行分类,分别存放在不同的数据库中,这些库可以分布在不同的服务器,从而使访问压力被分摊在多个服务器,大大提高性能,同时能提高整体架构的业务清晰度,不同的业务库可根据自身情况定制优化方案。但是它需要解决跨库带来的所有复杂问题。
-
水平分库:把一个表的数据(按数据行)分到多个不同的库,每个库只有这个表的部分数据,这些库可以分布在不同的服务器,从而使访问压力被多服务器负载,提升性能。它不仅需要解决跨库带来的问题,还需要解决数据路由的问题。
-
水平分表:把一个表的数据(按数据行)分到多个同一个数据库的多张表中,每个表的数据只有这个表的部分数据,这样做能小幅提升性能,它仅仅作为水平分库的一个补充优化。
-
-
Sql实践
JVM
-
类加载&对象创建
-
-
加载
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口
-
连接
-
验证
文件格式验证、元数据验证、字节码验证、符号引用验证
-
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段
-
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
-
-
初始化
初始化阶段是执行初始化方法
<clinit> ()
方法的过程 -
卸载
卸载类需要满足 3 个要求:
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被 GC
-
-
-
类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程
-
分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。
-
初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值
-
设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式
-
执行 init 方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,
<init>
方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行<init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来
-
-
-
cms&g1
- cms步骤
- 初始标记: 暂停所有的其他线程,标记GCRoots直接关联的对象以及年轻代指向老年代的对象,速度很快 ;
- 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 并发预处理:利用卡表处理并发标记阶段,发生的跨代引用问题。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
- CMS的缺点
- 预留空间:CMS垃圾收集器可以一边回收垃圾,一边处理用户线程,那需要在这个过程中保证有充足的内存空间供用户使用。(92%)
- 浮动垃圾:由于垃圾回收和用户线程是同时进行的,在进行标记或者清除的同时,用户的线程还会去改变对象的引用,使得原来某些对象不是垃圾,但是当 CMS 进行清理的时候变成了垃圾,CMS 收集器无法收集,只能等到下一次 GC。
- 内存碎片:CMS本质上是实现了标记清除算法的收集器(从过程就可以看得出),这会意味着会产生内存碎片。
- g1步骤
- Minor GC
- 根扫描:初始标记的过程
- 更新&&处理 RSet:处理RSet的信息并且扫描,将老年代对象持有年轻代对象的相关引用都加入到GC Roots下,避免被回收掉
- 复制对象:扫描之后存活的对象往空的Survivor区或者老年代存放,其他的Eden区进行清除
- Mixed GC
- 初始标记:复用了扫描GC Roots的操作
- 并发标记:GC线程与用户线程一起执行,GC线程负责收集各个 Region 的存活对象信息
- 最终标记:标记那些在并发标记阶段发生变化的对象
- 筛选回收:清点和重置标记状态;会根据停顿预测模型(其实就是设定的停顿时间),来决定本次GC回收多少Region。
- Minor GC
- G1缺点
- 浮动垃圾:由于垃圾回收和用户线程是同时进行的,在进行标记或者清除的同时,用户的线程还会去改变对象的引用,使得原来某些对象不是垃圾,但是当 G1 给进行清理的时候变成了垃圾,G1 收集器无法收集,只能等到下一次 GC。
- 性能问题:添加Rset结构,对性能要求高
- 引用变更解决
- cms增量更新
- g1SATB快照
- cms步骤
-
常见名词
- 引用计数法
- 通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。
- 可达性分析
- 可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
- GC Roots
- 可达性分析的跟对象集合,静态变量、常量、本地方法栈、虚拟机栈
- oopmap
- 存储栈上的对象引用的信息的集合
- 安全点和安全区域
- 更新oopmap的特定位置和特定区域
- 主动式中断和抢占式中断
- 系统先强制中断再判断是否为安全点、线程根据标志位自动中断
- 跨代引用
- 新生代和老年代对象的引用关系
- 卡表/Rset
- 存储跨代对象,解决老年代到新生代的引用问题
- 写屏障、伪共享问题
- 虚拟机层面对 “引用类型字段赋值” 这个动作的 AOP 切面
- 三色标记法
- 把遍历对象图过程中遇到的对象,按照 “是否访问过” 这个条件标记成三种颜色
- 增量更新和原始快照
- 解决引用变更的技术,仍然存在一些浮动垃圾
- Region
- g1中地址分配单位
- 引用计数法
网络
-
- 同步阻塞 I/O
- 数据准备阶段阻塞,数据拷贝阶段阻塞
- 同步非阻塞 I/O
- 数据准备阶段非阻塞,数据拷贝阶段阻塞
- 应用程序不断进行 I/O 系统调用轮询数据是否已经准备好
- I/O 多路复用
- 数据准备阶段非阻塞,数据拷贝阶段阻塞
- select
- 传入文件数组
- 遍历检查文件状态
- 返回文件个数
- poll
- 去除文件数组大小限制
- epoll
- 无需传入数组,仅提供修改部分
- 异步io检查文件状态
- 返回文件描述符
- 同步阻塞 I/O
-
HTTPS
-
核心
- 以非对称加密密钥(RSA)加密对称加密密钥(AES)
- 简单来说,我们使用浏览器的根证书(CA公钥)来确保数字证书的真实性(验证),数字证书(服务器公钥)来确保服务器对称公钥的保密性(加密)。
- 私钥签名,公钥验证;公钥加密,私钥解密
-
数字签名
解决篡改问题
- 数字签名就是一个文件的摘要加密后的信息。数字签名是和源文件一起发送给接收方的,接收方收到后对文件用摘要算法算出一个摘要,然后和数字签名中的摘要进行比对,两者不一致的话说明文件被篡改了。
-
数字证书
解决伪造问题
- 数字证书是一个经证书授权中心生成的文件,数字证书里一般会包含公钥、公钥拥有者名称、CA的数字签名、有效期、授权中心名称、证书序列号等信息。其中CA的数字签名是验证证书是否被篡改的关键,它其实就是对证书里面除了CA的数字签名以外的内容进行摘要算法得到一个摘要,然后CA机构用他自己的私钥对这个摘要进行加密就生成了CA的数字签名,CA机构会公开它的公钥,验证证书时就是用这个公钥解密CA的数字签名,然后用来验证证书是否被篡改
-
多线程
-
future和futureTask详解
public interface Future<V>
是一个异步计算的结果接口public class FutureTask<V>
实现RunnableFuture()
接口,间接实现Runnable()
、Future()
,将runnable()
包装为callable()
执行
-
线程状态转换
-
手写生产者消费者
-
Synchronized
public class SynchronizedData { private Integer number = 0; private Integer max = 10; public synchronized void put() throws InterruptedException { if (number < max) { number++; notifyAll(); } else { wait(); } } public synchronized void get() throws InterruptedException { if (number > 0) { number--; notify(); } else { wait(); } } }
-
ReentrantLock
public class ReentrantLockData { private Integer number = 0; private Integer max = 10; private Lock lock = new ReentrantLock(); private Condition putCondition = lock.newCondition(); private Condition getCondition = lock.newCondition(); public void put() throws InterruptedException { lock.lock(); if (number < max) { number++; getCondition.signal(); } else { putCondition.await(); } lock.unlock(); } public void get() throws InterruptedException { lock.lock(); if (number > 0) { number--; putCondition.signal(); } else { getCondition.await(); } lock.unlock(); } }
-
BlockingQueue
public class BlockingQueueData { BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(10); public void put() throws InterruptedException { blockingQueue.put("产品"); } public void get() throws InterruptedException { blockingQueue.take(); } }
-
Semaphore
- 同步信号量
- 用途:防止被抢占 初始为空
- 值的含义:值为资源可以使用的个数,信号量小于0,则线程进行等待,信号量大于0,表示可用资源个数。初始值0.
- 互斥信号量
- 用途:对临界区上锁 初始为满
- 值的含义:只有两个值0或1,0表示资源正在被占用,线程等待。1表示,资源没有被使用,线程可以进入。初始值为1
public class SemaphoreData { Semaphore provider = new Semaphore(10); Semaphore consumer = new Semaphore(0); Semaphore mutex = new Semaphore(1); private Integer number = 0; public void put() { try { provider.acquire(); mutex.acquire(); number++; } catch (InterruptedException e) { e.printStackTrace(); } finally { mutex.release(); consumer.release(); } } public void get() { try { consumer.acquire(); mutex.acquire(); number--; } catch (InterruptedException e) { e.printStackTrace(); } finally { mutex.release(); provider.release(); } } }
- 同步信号量
-
-
Sychronized
- 加锁方式
- 同步代码块(monitorenter和monitorexit)
- 同步方法(ACC_SYNCHRONIZED)
- 操作流程
- 首先会进入 EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
- 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;
- 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);
- 锁升级
- 锁升级是从偏向锁到轻量级锁,再到重量级锁的过程。在多线程情况下,当一个线程访问同一个对象时,JVM首先偏向该线程,把对象头中的mark field设置成当前线程ID。如果又有其他线程访问这个对象,JVM会尝试将偏向锁升级为轻量级锁,进入CAS操作的过程,如果多个线程之间竞争不激烈,轻量级锁通过自旋等待竞争排队的线程自动释放,效率比较高。如果多个线程之间竞争比较激烈,则锁升级为重量级锁,释放线程的系统开销比较高。偏向锁和轻量级锁比重量级锁更为轻量,在使用中能够大大提高系统的性能和并发能力。
- 锁消除
- Java虚拟机通过对运行上下文的扫描,经过逃逸分析,去除不可能存在竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。实质是JIT编译器的同步省略或者叫同步消除
- 锁粗化
- 按理来说,同步块的作用范围应该尽可能小,但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗。锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。
- 自适应自旋锁
- 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而允许自旋等待持续相对更长的时间。另一方面,如果对于某个锁,自旋很少成功获得过锁,那在以后要获取这个锁时将有可能直接省略掉自旋过程,以避免浪费处理器资源
- 加锁方式
-
AQS
- 核心思想
- 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
- 操作流程
- AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
- 公平锁和非公平锁
- 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
- 非公平锁在 CAS 失败后,和公平锁一样都会进入到
tryAcquire
方法,在tryAcquire
方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
- 条件队列
- AQS中实现ConditionObject,它实现了Condition接口,实现一个绑定在锁上的条件队列Condition,替代了 Object 监视器方法
- 核心思想
-
Reentrantlock
- 具体实现了tryAcquire、nofairTryAcquire、tryRelease
-
ThreadLocal
- 原理
- 每个Thread维护着一个ThreadLocalMap的引用
- ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
- 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象
- 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
- ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
- 内存泄漏
- key使用强引用:在当前ThreadLocal没有外部强引用时,ThreadLocalMap的Entry还保持着ThreadLocal的强引用,ThreadLocal不会被GC。如果没有手动删除,并且当前线程结束了,就导致了Entry的内存泄漏。
- key使用弱引用:在当前ThreadLocal没有外部强引用时,ThreadLocalMap只保持着ThreadLocal的弱引用,无论有没有手动删除,ThreadLocal都会被GC,只要下一次cleanSomeSlots(),expungeStaleEntry()被调用,value就会被清除,否则也会引起内存泄漏。
- 建议remove手动清除
- 引用类型
- 强引用:我们常常 new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
- 软引用:使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
- 弱引用:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
- 虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知
- 原理
-
线程同步和线程通信
- 线程同步
- 互斥同步(也称为阻塞同步,属于一种悲观的并发策略)
- synchronized、J.U.C包中的锁(锁重入、公平锁\非公平锁)
- 非阻塞同步(基于冲突检测的乐观并发策略,使用了硬件指令集提供的CAS功能)
- J.U.C包里面的整数原子类
- 无同步方案(线程本地存储)
- Java中可以通过java.lang.ThreadLocal类来实现线程本地存储的功能
- 互斥同步(也称为阻塞同步,属于一种悲观的并发策略)
- 线程通信
- 同步
- 消息队列
- 管程
- 线程同步
-
happen-before
JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。
-
as-if-serial
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提供并行度),(单线程)程序的执行结果不能被改变。编译器,runtime和处理器都必须遵守as-if-serial语义。as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。
设计模式
-
单例模式
- 满足要求
- 构造私有、变量私有静态、方法公有静态
- 懒汉式和饿汉式
- 饿汉式,在装载类时就完成实例化
- 懒汉式,在调用方法时完成实例化
- 双重校验锁
- 第一重校验在Sychronized外,提高性能,避免不必要的加锁
- 第二重校验在Sychronized内,防止多次创建,可能多个线程通过第一重校验,创建实例前不加第二重校验的话,可能会线程安全的创建两次实例
- volatile/final
- 修饰成员变量,防止指令重排
- 枚举
- 自动支持序列化机制,防止反序列化重新创建新的对象,防止反射破坏
- 满足要求
-
代理模式
-
静态代理
代理对象与目标对象要实现相同的接口,然后通过调用相同的方法来调用目标对象的方法(聚合关系)
- 定义一个接口:ITeacherDao
- 目标对象TeacherDAO实现接口ITeacherDAO
- 使用静态代理方式,就需要在代理对象TeacherDAOProxy中也实现ITeacherDAO
- 调用的时候通过调用代理对象的方法来调用目标对象
-
动态代理
-
JDK
-
代理类核心为
Proxy.newProxyInstance()
- 参数为
目标对象使用的类加载器
、目标对象实现的接口类型
、事情处理器方法
- 返回值为
代理对象
- 参数为
-
事件处理器核心为
InvocationHandler()
可以使用匿名内部类或实现接口
- 重写
invoke()
方法,执行代理内容
- 重写
-
-
Cglib
- 代理类核心为
Enhancer()
- 设置
目标对象
- 设置
对象拦截器
- 设置
- 对象拦截器核心为
MethodInterceptor()
- 重写
intercept()
方法,执行代理内容
- 重写
- 代理类核心为
-
-
-
工厂模式
- 简单工厂模式
- 针对产品
- 工厂方法模式
- 针对产品品牌
- 抽象工厂模式
- 针对产品族(类)
- 简单工厂模式
算法
- 排序
Spring
-
-
单例setter注入
-
三级缓存
Spring中有三个缓存,用于存储单例的Bean实例,这三个缓存是彼此互斥的,不会针对同一个Bean的实例同时存储。如果调用getBean,则需要从三个缓存中依次获取指定的Bean实例。 读取顺序依次是一级缓存 > 二级缓存 > 三级缓存。
- singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的bean实例(成品)
- earlySingletonObjects 二级缓存,用于保存实例化完成的bean实例(半成品)
- singletonFactories 三级缓存,用于保存bean创建工厂,以便于后面扩展有机会创建代理对象(工厂)
-
解决过程
- 创建对象A,实例化的时候把A对象工厂放入三级缓存
- A注入属性时,发现依赖B,转而去实例化B
- 创建对象B,实例化的时候把B对象工厂放入三级缓存;注入属性时发现依赖A,依次从一级到三级缓存查询A,从三级缓存通过对象工厂拿到A,把A放入二级缓存,同时删除三级缓存中的A,此时,B已经实例化并且初始化完成,把B放入一级缓存。
- 接着继续创建A,顺利从一级缓存拿到实例化并且初始化完成的B对象,A对象创建也完成,删除二级缓存中的A,同时把A放入一级缓存。
- 最后,一级缓存中保存着实例化,初始化都完成的A,B对象
-
-
构造器注入
- @lazy
-
为什么需要三级缓存
为了在没有循环依赖的情况下,延迟代理对象的创建,使 Bean 的创建符合 Spring 的设计原则。
- Spring 一开始提前暴露的并不是实例化的 Bean,而是将 Bean 包装起来的 ObjectFactory。实际上涉及到 AOP,如果创建的 Bean 是有代理的,那么注入的就应该是代理 Bean,而不是原始的 Bean。但是 Spring 一开始并不知道 Bean 是否会有循环依赖,通常情况下(没有循环依赖的情况下),Spring 都会在完成填充属性,并且执行完初始化方法之后再为其创建代理。如果出现了循环依赖的话,Spring 就不得不为其提前创建代理对象,否则注入的就是一个原始对象,而不是代理对象。
- 二级缓存也是可以解决循环依赖的。如果 Spring 选择二级缓存来解决循环依赖的话,那么就意味着所有 Bean 都需要在实例化完成之后就立马为其创建代理,而Spring 的设计原则是在 Bean 初始化完成之后才为其创建代理。
-
-
SpringMvc过程
- 请求传入dispatcherservlet
- 传入处理器映射器,转换为controller,返回执行链
- 传入处理器适配器,调用底层的方法,返回ModelAndView
- 传入视图解析器,返回View
- 解析View,返回响应
-
SpringBoot自动配置
-
@EnableAutoConfiguration
:启用 SpringBoot 的自动配置机制自动装配核心功能的实现实际是通过
AutoConfigurationImportSelector
类。- 获取需要自动装配的所有配置类,读取
META-INF/spring.factories
- 筛选,
@ConditionalOnXXX
中的所有条件都满足,该类才会生效
- 获取需要自动装配的所有配置类,读取
-
@Configuration
:允许在上下文中注册额外的 bean 或导入其他配置类 -
@ComponentScan
: 扫描被@Component
(@Service
,@Controller
)注解的 bean,注解默认会扫描启动类所在的包下所有的类 。
-
-
Spring事务
- 事务级别
- 数据库默认
- ru rc rr s
- 事务传播行为
- 支持当前事务
- 不存在当前事务、新事务执行
- 不存在当前事务、非事务执行
- 不存在当前事务抛异常
- 不支持当前事务
- 存在挂起当前事务、新事务执行
- 存在挂起当前事务、非事务执行
- 存在当前事务抛异常
- 嵌套
- 支持当前事务
- 事务级别
-
Spring设计模式
-
工厂模式
Spring IOC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。 IOC 容器负责创建对象,将对象连接在一起,配置这些对象,并从创建中处理这些对象的整个生命周期,直到它们被完全销毁。
-
BeanFactory
:延迟注入(使用到某个 bean 的时候才会注入),相比于ApplicationContext
来说会占用更少的内存,程序启动速度更快。 -
ApplicationContext
:容器启动的时候,不管你用没用到,一次性创建所有 bean 。ApplicationContext
扩展了BeanFactory
。 -
ClassPathXmlApplication
:把上下文文件当成类路径资源。 -
FileSystemXmlApplication
:从文件系统中的 XML 文件载入上下文定义信息。 -
XmlWebApplicationContext
:从Web系统中的XML文件载入上下文定义信息。
-
-
代理模式
Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理。常用于计算接口运算时间、记录日志。
AspectJ AOP 基于字节码操作的AOP。
-
单例模式
Spring 中 bean 的默认作用域就是 singleton(单例)的。 Spring 通过
ConcurrentHashMap
实现单例注册表的特殊方式实现单例模式 -
模板方法模式
jdbcTemplate、redisTemplate、restTemplate
-
-
Spring的启动流程
- 初始化Spring容器,注册内置的BeanPostProcessor的BeanDefinition到容器中
- 将配置类的BeanDefinition注册到容器中
- 调用refresh()方法刷新容器
-
bean的创建过程
- 推断构造方法
- 依赖注入
- 初始化前
- 初始化
- 初始化后
Redis
-
缓存问题
-
redis的五大基本数据类型
- string 缓存 session共享 分布式锁
- list 消息队列
- hash 存储对象
- set 集合
- zset 排行榜
-
redis的两种持久化方式,redis的默认持久化方式
- aof 命令
- rdb 快照 默认
-
redis的删除策略
- 定时删除
- 创建一个定时器,当key有过期时间时,时间一到,定时器任务就会立即执行 将expires区域和k-v区域都删除
- 优点:节约内存
- 缺点:cpu压力较大,此时无论cpu负载量多高,都会占用cpu来释放,影响redis的吞吐量
- 惰性删除
- 数据过期后,并不会立刻删除 等到该数据下次访问的时候,redis才会删除该数据,并返回该值为nil
- 优点:节省cpu的资源
- 缺点:可能存在大量的,无人访问的数据会一直存在服务器
- 定期删除
- 每秒钟定期对redis中每个库的数据进行轮询 轮询的数据,对过期的数据随机删除一部分 如果随机删除的数据占轮训数据的比例超过一定值,继续轮询删除
- 定时删除
-
缓存双写不一致问题
-
先删除缓存,再更新数据库
- 问题
- 请求1先把cache中的A数据删除;请求2从DB中读取数据将将数据A写入cache;请求1再把DB中的A数据更新。
- 解决方案
- 延迟双删策略:先删除缓存;再更新数据库;休眠1秒,再次删除缓存;
- 问题
-
先更新数据库,再删除缓存
-
问题
- 缓存刚好失效,请求1从DB读数据A;请求2写更新数据 A 到数据库并把删除cache中的A数据;请求1将数据A写入cache。
- 更新数据库成功,删除缓存时没有成功,那么每次读取缓存时都是错误的数据。
-
解决方案
- 删除重试机制,利用消息队列和数据库的日志实现
-
-
-
分布式锁
-
关键命令
- SETNX
- SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
- EXPIRE
- EXPIRE key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
- DELETE
- DELETE key:删除key
- SETNX
-
实现思想
-
获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
-
获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
-
释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
-
-
-
底层数据结构
TYPE ENCODING 解释 REDIS_STRING
REDIS_ENCODING_INT
使用整数值实现的字符串对象。 REDIS_STRING
REDIS_ENCODING_EMBSTR
使用 embstr
编码的简单动态字符串实现的字符串对象。REDIS_STRING
REDIS_ENCODING_RAW
使用简单动态字符串实现的字符串对象。 REDIS_LIST
REDIS_ENCODING_ZIPLIST
使用压缩列表实现的列表对象。 REDIS_LIST
REDIS_ENCODING_LINKEDLIST
使用双端链表实现的列表对象。 REDIS_HASH
REDIS_ENCODING_ZIPLIST
使用压缩列表实现的哈希对象。 REDIS_HASH
REDIS_ENCODING_HT
使用字典实现的哈希对象。 REDIS_SET
REDIS_ENCODING_INTSET
使用整数集合实现的集合对象。 REDIS_SET
REDIS_ENCODING_HT
使用字典实现的集合对象。 REDIS_ZSET
REDIS_ENCODING_ZIPLIST
使用压缩列表实现的有序集合对象。 REDIS_ZSET
REDIS_ENCODING_SKIPLIST
使用跳跃表和字典实现的有序集合对象。 -
redis的同步策略
主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。
- 全量同步
- Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。
- 增量同步
- Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
- 全量同步
-
redis集群
-
Redis 主从模式
-
在master数据库中的数据更新后,自动将更新的数据同步到slave数据库上
-
优缺点
-
优点
- 高可靠性,在master数据库出现故障后,可以切换到slave数据库
- 读写分离,slave库可以扩展master库节点的读能力,有效应对大并发量的读操作 缺点: 不具备自动容错和恢复能力,主节点故障,从节点需要手动升为主节点,可用性较低
-
缺点
- 不具备自动容错和恢复能力,主节点故障,从节点需要手动升为主节点,可用性较低
-
-
-
Redis 哨兵模式
-
哨兵模式相比于主从模式,主要多了一个哨兵集群,哨兵集群的主要作用如下:
-
监控所有服务器是否正常运行:通过发送命令返回监控服务器的运行状态,处理监控主服务器、从服务器外,哨兵之间也相互监控。
-
故障切换:当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换master。同时那台有问题的旧主也会变为新主的从,也就是说当旧的主即使恢复时,并不会恢复原来的主身份,而是作为新主的一个从。
-
-
优缺点
- 优点
-
哨兵模式是基于主从模式的,解决可主从模式中master故障不可以自动切换故障的问题。
-
缺点
-
浪费资源,集群里所有节点保存的都是全量数据,数据量过大时,主从同步会严重影响性能
- Redis主机宕机后,投票选举结束之前,谁也不知道主机和从机是谁,此时Redis也会开启保护机制,禁止写操作,直到选举出了新的Redis主机。
- 只有一个master库执行写请求,写操作会单机性能瓶颈影响
-
- 优点
- Redis 自研
- 客户端分片
- 客户端分片是把分片的逻辑放在Redis客户端实现,通过Redis客户端预先定义好的路由规则,把对Key的访问转发到不同的Redis实例中,查询数据时把返回结果汇集
- 优缺点
- 优点
- Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强。
- 缺点
- 客户端sharding不支持动态增删节点。服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。
- 运维成本比较高,集群的数据出了任何问题都需要运维人员和开发人员一起合作,减缓了解决问题的速度,增加了跨部门沟通的成本。
- 在不同的客户端程序中,维护相同的路由分片逻辑成本巨大。
- 优点
- 代理分片
- 代理分片将客户端分片模块单独分了出来,作为Redis客户端和服务端的桥梁
- 优缺点
- 优点
- 解决了服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整的问题。
- 缺点
- 是由于Redis客户端的每个请求都经过代理才能到达Redis服务器,这个过程中会产生性能损失。
- 优点
- Redis Cluster
- 结构
- 主备复制,和主从模式一样,在master库中的数据更新后,自动将更新的数据同步到slave库上
- 对外服务,是外部在对Redis进行读取操作,访问master进行写操作,访问slave进行读操作
- Redis Bus是用于节点之间的信息交路,交互的信息有以下几个:
- 数据分片(slot)和节点的对应关系
- 集群中每个节点可用状态
- 集群结构发生变更时,通过一定的协议对配置信息达成一致。数据分片的迁移、主备切换、单点master的发现和其发生主备关系变更等,都会导致集群结构变化。
- 优缺点
- 优点
- 方便地添加和移除节点,增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了,当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了,不需要停掉Redis任何一个节点的服务
- 缺点
- 部署和维护需要一定的成本
- 优点
-
- Redis主从架构中数据丢失
- 异步复制同步丢失
- Redis主节点和从节点之间的复制是异步的,当主节点的数据未完全复制到从节点时就发生宕机了,master内存中的数据会丢失。
- 集群产生脑裂数据丢失
- 由于网络原因,集群出现了分区,master与slave节点之间断开了联系,哨兵检测后认为主节点故障,重新选举从节点为主节点,但主节点可能并没有发生故障。此时客户端依然在旧的主节点上写数据,而新的主节点中没有数据,在发现这个问题之后,旧的主节点会被降为slave,并且开始同步新的master数据,那么之前的写入旧的主节点的数据被刷新掉,大量数据丢失。
-
Linux
-
常用命令
- chmod 授权
- ps -ef | grep 查看进程
- kill -9 杀死进程
- tail -f 实时读取日志
- netstat -anp | grep 查看端口号
- ping 测试地址
- mkdir 创建文件夹
- rmdir 删除文件夹
- touch 创建文件
- rm 删除文件
- mv 移除文件
- cp 复制文件
- cat 查看文件