八股
java八股文面试
JVM
1、运行数据区
- 方法区/元空间
JKD8后,堆中永久代变成了元空间
线程共享区域,保存对象的类型数据
静态域:存放静态成员
常量池:存放字符串常量对象和基本类型常量 - 堆
线程共享区域,保存对象实例、数组,内存不足抛出OutOfMempryErroe异常
年轻代:伊甸园区+幸存者区(s0+s1);伊甸园区GC一次进入幸存者区
老年代:保存生命周期长的对象;年龄到15后,进入老年代
永久代:1.8后被移除,数据存入元空间,防止内存溢出 - 虚拟机栈
线程私有,存储基本数据类型,引用对象的地址
每个线程运行时需要的内存,先进后出
栈帧对应每次方法调用所占内存
活动栈帧对应当前正在执行的方法 - 本地方法栈
调用非Java代码的接口 - 程序计数器
线程私有,每个线程都有各自的程序计数器
记录正在执行的字节码地址
栈管运行,堆管存储
2、虚拟机栈
方法内局部变量是否线程安全?
- 方法内局部变量没有逃离方法的作用范围,则是线程安全的
- 局部变量引用了对象,或逃离了方法作用范围(return),则线程不安全
3、栈内存溢出
- 栈帧过多,比如递归调用(StackOverFlowError)
4、NIO与BIO
- NIO 使用非阻塞式 I/O,而 BIO 使用阻塞式 I/O。
在阻塞式 I/O 中,当一个 I/O 操作完成之前,线程会一直被阻塞,直到 I/O 操作完成;
在非阻塞式 I/O 中,线程可以继续执行其他任务,直到 I/O 操作完成并返回结果。 - BIO操作磁盘文件时,需要将文件从系统内存读取进Java堆内存再进行操作
- NIO操作时,使用直接内存做为数据缓冲区,
直接内存:Java可直接操作磁盘文件
读写性能高,不受JVM内存回收管理。但回收成本高
5、类加载器
类加载器将字节码加载到JVM中
- Bootstrap 启动类加载器
- ExtClassLoader 扩展类加载器
- AppClassLoader 应用类加载器
类加载器的双亲委派机制
3种加载器如果父类加载器已经可以加载,则不用自身加载
可以避免某一个类被重复加载,保证唯一性
为了安全,保证类库API不会被修改
6、类加载的过程
加载:将字节码文件通过IO流读取到JVM的方法区,并同时在堆中生成Class对像。
验证:校验字节码文件的正确性。
准备:为类的静态变量分配内存,并初始化为默认值;对于final static修饰的变量,在编译时就已经分配好内存了。
解析:将类中的符号引用转换为直接引用。
初始化:对类的静态变量初始化为指定的值,执行静态代码。
7、对象什么时候回收(判断是否GC算法)
- 可达性分析算法
对象没有任何引用指向它,就可以被回收
8、JVM垃圾回收算法
- 标记清除
根据可达性分析进行标记,再清除 - 标记整理
标记清除后,将存活对象都向内存一段移动 - 复制算法
9、强引用、软引用、弱引用、虚引用
- 强引用
只要GC Roots能找到,就不会被回收 - 软引用
多次垃圾回收后,内存依然不够,就会被回收 - 弱引用
只要进行了垃圾回收,就会被回收 - 虚引用
在任何时候都可能被垃圾回收器回收。必须和引用队列联合使用
多线程
线程基础
1、创建线程的方式
- 实现Thread类
- 实现Runnable接口
- 实现Callable接口
- 创建线程池
2、Runnable和Callable有什么区别
- Runnable接口run方法没有返回值
- Callable接口run方法有返回值,FutureTask可以获取异步执行结果
- Runnable接口run方法异常只能内部处理,不能抛出
- Callable接口run方法允许抛出异常
3、线程有哪些状态,状态如何切换
- 新建:新创建了一个线程对象,和其他java对象一样,仅在堆中分配内存。
- 就绪:
线程对象创建后,其他线程调用了该对象的 start() 方法。线程已具备了条件,只须再获得CPU便可立即执行 - 运行:就绪态的线程获得了CPU时间片,执行程序代码
- 阻塞:阻塞状态是线程因为某种原因放弃CPU使用权,有机会转到运行状态。
- 等待阻塞:运行状态中的线程执行 wait 方法,等待阻塞;其他线程调用notify()切换为可执行态
- 同步阻塞:如果没有获得锁,进入阻塞状态,获得锁后切换为可执行态
- 其他阻塞:运行的线程执行sleep(50)方法,进入等待倒计时,时间到了切换为可执行态
- 死亡:线程执行完了或者因异常退出run()方法
4、如何保证T1、T2、T3按顺序执行(join)
- join():阻塞当前线程,让当前线程等待另一个线程执行完毕的方法
- 在线程T2中t1.join;在线程T3中t2.join
5、notify()和notifyALL()区别
- notify():只随机唤醒一个wait线程
- notifyALL():唤醒所有wait线程
6、wait和sleep的不同
共同点:wait()、wait(long)、sleep(long)都是让当前线程暂时放弃CPU使用权,进入阻塞
不同点
- 方法归属不同
- sleep是Thread静态方法。
- wait是Object成员方法
- 醒来时间不同
- wait(long)、sleep(long)等待时间后醒来
- wait()、wait(long)还可以被notify唤醒,不唤醒则一直等待
- 锁特性不同
- wait方法调用必须获取wait对象的锁,而sleep无此限制
Object object = new Object(); Thread t1 = new Thread(() ->{ synchronized (object){ object.wait(); //wait必须与synchronized配合使用 } });
- wait方法执行后会释放锁,允许其他线程获得该对象锁(释放锁不用唤醒或等待时间结束)
- sleep在synchronized中执行,不会释放对象锁
- wait方法调用必须获取wait对象的锁,而sleep无此限制
7、如何停止正在运行的线程
- stop方法,不推荐,已过时
- 使用退出标志,使线程正常退出(run方法执行完成后线程终止)
class MyThread extends Thread { private volatile boolean running = true; //注意volatile线程可见性 public void run() { while (running) { // 线程的执行逻辑 } } public void stopThread() { running = false; } } MyThread thread = new MyThread(); thread.start(); // 停止线程 thread.stopThread();
- interrupt方法中断线程(类似推出标志)
- 打断阻塞线程(sleep、wait、join),线程会抛出InterruptedException
- 打断正常线程,根据打断标志标记是否退出线程
class MyRunnable implements Runnable { public void run() { while (!Thread.currentThread().isInterrupted()) { // 线程的执行逻辑 } } } Thread thread = new Thread(new MyRunnable()); thread.start(); // 停止线程 thread.interrupt();
线程安全
1、synchronized关键字底层原理
- synchronized对象锁,同一时刻只能有一个线程持有
- 底层是monitor实现,内部有三个属性
- owner:关联获得锁的线程,只能关联一个
- entrylist:关联处于阻塞状态的线程
- waitset:关联处于waiting状态的线程
- synchronized有偏向锁状态、轻量级锁状态、重量级锁状态三种形式
2、monitor重量级锁,锁升级
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,性能依次是从高到低。锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
- 重量级锁:底层使用monitor,会涉及用户态与内核态的切换,性能较低
- 轻量级锁:线程加锁时间错开(没有竞争)时使用。修改了对象头的锁标志,修改使用了CAS
- 偏向锁:很长时间都只被一个线程使用锁时使用。第一次修改标志用CAS,之后不用。
一旦锁发生了竞争,都会升级为重量级锁。
3、JMM(Java内存模型)
定义了共享内存中多线程程序读写操作
JMM把内存分为:私有的工作内存、共享的主内存
线程共享:堆、方法区
线程私有:程序计数器、虚拟机栈、本地方法栈
4、CAS(比较再交换)
(compare And Swap)比较再交换,乐观锁/自旋锁,其他线程可以修改共享变量
线程读取数据操作完成,再把数据写入共享空间,
比较是否与读取数据时相同,不同则重新读取再做操作。
没有加锁,线程不会陷入阻塞,效率较高
如果竞争激烈,重试频繁发生,效率会受影响
实现:Unsafe类的compareAndSwap方法
5、乐观锁与悲观锁
- 乐观锁
CAS,不怕其他线程修改共享数据(乐观地认为别的线程不会修改共享数据) - 悲观锁
synchronized、ReentrantLock,防止其他线程修改数据(悲观地认为别的线程会修改共享数据)
6、volatile(线程可见性)
- 用volatile修饰共享变量
- 防止编译器优化
- 让一个线程对共享变量的修改对另一个线程可见
- 禁止指令重排序
技巧:- 写变量让volatile修饰的变量在代码最后位置
- 读变量让volatile修饰的变量在代码最开始位置
7、AQS(抽象队列同步器)
AbstractQueuedSynchronizer 抽象队列同步器,是一种锁机制
- 常见实现
- ReentrantLock 阻塞式锁
- Semaphora 信号量
- CountDownLatch 倒计时锁
- AQS内部维护了一个先进先出的双向队列,存储排队的线程
AQS可以是非公平锁、公平锁。取决于新来的线程直接竞争还是排队。 - AQS内部有一个属性state,相当于一个资源,默认为0(无锁)
如果队列中有一个线程修改成功了state为1,则相当于当前线程获取了资源 - 多个线程修改state时,使用CAS操作,保证原子性
8、ReentrantLock可重入锁
- 利用CAS+AQS实现
- ReentrantLock支持重新进入的锁,调用lock获取锁后,再次调用lock是不会阻塞的
- 支持公平锁与非公平锁,无参默认是非公平锁,可传参设置为公平锁
9、synchronized和ReentrantLock区别
public class Syn implements Runnable{
Object lock = new Object();
@Override
public void run() {
synchronized (lock){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
ReentrantLock lock = new ReentrantLock();
public static void lock1() {
try {
lock.lock();
} finally {
lock.unlock();
}
}
- 语法
- 使用synchronized时,退出同步代码块,锁会自动释放
- 使用lock时,需要手动调用unlock方法释放锁
- 功能
- 二者都属于悲观锁,都有互斥、同步、可重入功能
- Lock提供了synchronized不具备功能
- 公平锁:可传参设置为公平锁
- 可打断:打断后,不会进入阻塞状态
- 可超时:设置等待锁时间
- 多条件变量:多个线程设置await等待、signal执行(t1.signalAll,唤醒t1.await所有线程)
- Lock有不同实现,如ReentrantLock、ReentrantReadWriteLock(读写锁)
- 性能
- 没有竞争时,synchronized做了很多优化,如偏向锁、轻量级锁,性能好
- 竞争激烈时,lock的实现性能更好
10、死锁产生条件
- 一个线程需要同时获取多把锁,此时容易发生死锁
- jdk自带的jps可检测死锁
11、ConcurrentHashMap线程安全的HashMap
- 底层与HashMap一样,数组+链表/红黑树
- 使用synchronized锁定链表或红黑树的首节点(数组的单个位置)
- CAS添加新节点
12、如何保证多线程执行安全(并发问题出现根本原因)
- 原子性 synchronized或lock锁解决
- 内存可见性 volatile解决
- 有序性 (指令重排列) volatile解决
线程池
1、线程池核心参数、执行原理
- corePoolSize 核心线程数
- maximumPoolSize 最大线程数 = (核心线程数+救急线程数)
- keepAliveTime 生存时间:救济线程的生存时间,生存时间内没有新任务,此线程资源会释放
- unit 时间单位:救济线程生存时间单位
- workQueue 阻塞队列:核心线程已满,任务进入阻塞队列;阻塞队列满了,创建救急线程
- threadFactory 线程工厂:定制线程对象创建,可设置线程名字
- handler 拒绝策略:核心线程数、 阻塞队列、救急线程数全满,触发拒绝策略
执行原理
- 提交任务
- 进入核心线程
- 核心线程没满,添加到工作线程并执行
- 核心线程满了,进入阻塞队列
- 进入阻塞队列
- 阻塞队列没满,添加到阻塞队列
- 阻塞队列满了,创建临时线程
- 创建临时线程
- 核心线程数+临时线程数 < 最大线程数
创建临时线程执行任务(也会执行阻塞队列中任务) - 核心线程数+临时线程数 = 最大线程数
触发handler拒绝策略- 直接抛出异常(默认)
- 调用者来执行任务(主线程执行)
- 丢弃阻塞队列最靠前的任务,并执行当前任务(加入阻塞队列)
- 直接丢弃任务
- 核心线程数+临时线程数 < 最大线程数
2、线程池中有哪些常见阻塞队列
workQueue 阻塞队列
- ArrayBlockingQueue
基于数组结构的有界阻塞队列,FIFO- 强制有界(初始化参数)
- 提前初始化Node数组,提前创建
- 一把锁,锁数组
- LinkedBlockingQueue
基于链表结构的有界阻塞队列,FIFO- 默认无界(无参数则为int最大值),支持有界
- 创建节点时才添加数据,入队生成新的Node
- 两把锁,锁链表头与链表位
- DelayedWord
优先级队列,执行时间最靠前的先出队
3、如何确定核心线程数
- IO密集型任务:文件读写、DB读写、网络请求
- 核心线程数设置为2N+1
- CPU密集型任务:计算型、Bitmap转换
- 核心线程数设置为N+1
N为CPU核数
4、线程池种类
使用Executors创建自带的线程池(不建议使用,可用ThreadPoolExecutor自定义线程池)
- 固定线程数的线程池(FixedThreadPool)
- 核心线程数 = 最大线程数(没有临时线程数)
- 阻塞队列为LinkedBlockingQueue,容量为int最大值
- 适用于任务量已知,相对耗时的任务
- 单线程化的线程池(SingleThreadExecutor)
- 核心线程数 = 最大线程数 = 1
- 阻塞队列为LinkedBlockingQueue,容量为int最大值
- 适用于按照顺序执行的任务
如果当前线程意外终止,线程池会创建一个新的线程来代替它,保证线程的可用性。
- 可缓存线程池(CachedThreadPool)
- 核心线程数 = 0;最大线程数 = int最大值
- 适用于任务数比较密集,但每个任务执行时间较短
- 可延时、周期执行的线程池(ScheduledThreadPoolExecutor)
- 可设置执行任务开始时间
使用场景
1、CountDownLatch
CountDownLatch latch = new CountDownLatch(2); //参数2
new Thread(()->{
latch.countDown; //调用一次减1
}).start();
new Thread(()->{
latch.countDown;
}).start();
latch.await(); //为0时唤醒,继续执行
2、控制某个方法允许并发访问线程的数量(信号量)
Semaphore信号量
3、ThreadLocal线程安全
set设置值、get获取值、remove清除值
- ThreadLocal实现资源对象线程隔离,每个线程各用各的资源对象
- ThreadLocal实现线程内的资源共享
ThreadLocal会导致内存泄漏,因为ThreadLocalMap中key是弱引用、值是强引用
内存泄漏:已动态分配的堆内存由于某种原因程序未释放或无法释放
MySQL
优化
定位慢查询
- 运维工具(skywalking)
- mysql中慢查询日志,sql执行超过2秒就会记录
如何分析SQL执行慢
explain或desc加在sql前
显示信息:
- key 当前sql实际命中索引(检查索引是否失效)
- key_len 索引占用大小
- type 连接类型,由好到差依次为以下
- const:根据键查询
- eq_ref:主键索引查询或唯一索引查询
- ref:索引查询
- range:范围查询
- index:索引树扫描
- all:全盘扫描
索引
1、索引的底层数据结构
- MySQL的InnoDB引擎采用B+树,阶数更多,路径更短
- B+树非叶子节点只存储指针,叶子节点存储数据
- B+树的叶子节点是双向链表,且是从小到大的有序链表
2、聚簇索引与非聚簇索引
- 主键就是聚簇索引(聚集索引),叶子节点保存了整行数据
- 非聚簇索引(二级索引),自己创建的索引,叶子节点保存的是对应主键
3、回表查询
- 通过二级索引找到对应主键值,再到聚集索引中查找正行数据
4、覆盖索引
- 查询使用了索引,并需要返回的列,在该索引中已经全部能够找到(不需要回表查询)
#当id、name创建了索引,id为主键
select * from user where id = 1;
#是覆盖索引,聚集索引中包含所有行信息
select id, name from user where name = 'jack';
#是覆盖索引,二级索引中包含主键,且name是索引
5、MySQL超大分页处理
- 当数据量特别大,limit分页查询,需要进行排序,效率低
- 解决:覆盖索引+子查询
6、索引创建原则
- 表数据量大,查询频繁,可以给表创建索引(单表超过10万条)
- 字段常被用于条件、排序、分组,创建索引
- 使用联合索引(复合索引),避免回表
- 控制索引数量
7、索引什么时候失效
使用explain在sql前判断是否索引失效
- 联合索引,违反最左前缀原则
- 范围查询右边的列,不能使用索引
- 不要在索引列上进行运算操作
- 字符串不加单引号(数字类型与String类型的 ‘0’)
- 以百分号%开头like模糊查询,索引失效
8、sql语句优化
- 避免使用select *
- 避免索引失效写法
- 用union all代替union,union会多一次过滤,效率低
- 避免在where字句中对字段进行表达式操作(可能索引失效)
- 多表查询,用小表驱动大表
- 批量操作
事务
事务特性(ACID)
- 原子性(Atomicity):
事务是不可分割的最小操作单元,要么全部完成,要么全部失败 - 一致性(Consistency):
事务完成时,必须使所有数据都保持一致 - 隔离性(Isolation):
允许并发事务同时对其数据进行读写和修改的能力,
隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。 - 持久性(Durability):
事务处理结束后,对数据的修改就是永久的
隔离级别:
- 读未提交(Read uncommitted)
- 读提交(read committed)
Oracle默认的隔离级别
解决脏读:读到事务没提交的数据 - 可重复读(repeatable read)
MySQL默认的隔离级别
解决不可重复读:事务读取同一条数据,读取数据不同 - 串行化(Serializable)
表级锁,读写都加锁,效率低下,安全性高,不能并发。
解决幻读:查询时没有数据,插入时发现已经存在,好像出现幻觉
主从同步
二进制日志(BINLOG)记录了所有的DDL与DML
Redis
缓存
1、缓存问题
缓存穿透
查Redis、数据库都不存在的数据,架空结果缓存:缓存空数据
高并发请求没有命中缓存,则所有请求都会直接查询数据库,造成数据库崩溃
查询一个一定不存在的数据,由于缓存不命中,每次都会去查询数据库,导致缓存失去意义
解决
- null结果缓存(缓存空数据),并加入短暂过期时间
- 布隆过滤器(hash算法,bitmap)
缓存雪崩
大量缓存key同时失效:加时间随机值
设置缓存时,采用了相同过期时间,导致缓存在某一时刻同时失效
大量请求全部转发到DB,导致瞬时压力过重雪崩
解决:在失效时间上加一个随机值,比如1-5分钟
缓存击穿
热点key失效被大量访问:加锁
某一个高频热点key,在失效后,正好有大量请求同时访问,这些key的查询都会落在DB上
解决:加锁,大量并发只让一个人查,存入缓存,其他人再从缓存查
2、如何修改Redis中的数据
双写一致性
当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保存一致
解决同时修改缓存与数据库时,的数据一致性
强一致性的解决方案
业务必须保证强一致性
- 延时双删
- 删除缓存 修改数据库 延时 删除缓存
- 为什么删两次?在修改数据库前,可能没修改完成时被读取到缓存中
延时作用:Redis主从架构,保证从机也同步了数据 - 缺点:有脏数据风险,因为延时时间不好确定
- 读写锁
加入缓存的数据,读多写少
写时加锁,可读取
最终一致性的解决方案
业务可以允许数据短暂不一致
通过MQ异步通知保证数据的最终一致性
修改数据库数据 -> mq发布消息 -> 业务跟新缓存
3、缓存持久化
RDB
(Redis Database)
在指定时间间隔内,执行数据集的时间点快照
保存时将内存中数据集写入磁盘,恢复时再从磁盘写入内存
- 自动触发
- 每多少秒有操作,自动保存dump.rdb文件
- 自动恢复,当redis挂掉,再启动会自动读取dump.rdb文件文件并恢复
- 手动触发
- save(不用):会阻塞redis服务器,此时redis不能处理其他
- bgsave(可使用):不阻塞
AOF
(Append Only File) 追加文件
以日志记录每个写操作。
默认没有开启,在conf里设置
三种保存频率
1、no 系统控制,性能最好,但可靠性差,可能丢失大量数据
2、everysec(一般采用)每秒刷盘,性能适中,最多丢一秒数据
3、always 每次查询时刷盘,可靠性高,但性能影响大
RDB-AOF混合
建议混合使用
RDB和AOF同时开启,重启时只会加载aof文件,不会加载rdb文件
aof优先级高于rdb
4、数据过期删除策略(时间)
惰性删除
设置key过期后不管它,当用到时在检查是否过期,过期则删
优点:对cpu友好,不浪费时间检查过期
缺点:对内存不友好,过期后没有删除
定期删除
每隔一段时间,对key检查,删除过期key
Redis过期策略:惰性删除+定期删除
5、数据淘汰策略(空间、内存)
Redis内存不够,此时向Redis中添加新的key,那么Redis会根据某种规则将内存中的数据删除
- noeviction:不淘汰任何key,但内存满是不允许写入新数据,默认
- volatile-ttl:对设置了ttl的key,比较key的剩余ttl值,小的被淘汰
- allkeys-random:对全体key,随机淘汰
- volatile-random:对设置了ttl的值随机淘汰
- allkeys-lru:对全体key,基于LRU算法进行淘汰(LRU:最近最少使用)
- volatile-lru:对设置了ttl的key,基于LRU算法进行淘汰
- allkeys-lfu:对全体key,基于LFU算法进行淘汰(LFU:最少使用频率)
- volatile-lfu:对设置了ttl的key,基于LFU算法进行淘汰
优先使用:allkeys-lru
分布式锁
获取锁
SET lock value NX EX 10
#NX是互斥,EX是设置超时时间(超时时间防止服务宕机而不会释放锁)
释放锁
DEL key
#释放锁,删除即可
redisson底层采用sentnx命令实现分布式锁(设置ttl,防止宕机后,锁永远无法释放)
SET if not exists(如果key不存在则SET:只能有一个获取锁成功)
setnx lock 1 #获取锁,返回1获取锁成功
setnx lock 2 #因为lock为key值已存在,所以返回0,获取锁失败
del lock 1 #释放锁
setnx实现分布式锁缺点
- 不可重入
- 不可重试
- 超时释放
- 主从一致性
Redisson
依赖、配置、注入使用
redisson实现分布式锁
- 可重入锁:判断获取锁的对象是否是当前线程,是则count++,标识重入次数
- 可重试
- watch dog 锁续期机制:给持有锁的线程续期(默认每隔10秒续期一次)
- 不能解决主从数据一致性
- 使用multiLock实现主从数据一致性:
在所有Redis主节点设置联合锁,使用时必须所有节点都获取锁成功
缺点:运维成本高,实现复杂
- 使用multiLock实现主从数据一致性:
RedissonClient redissonClient;
RLock lock = redissonClient.getLock();
boolean islock = lock.tryLock(1,10,TimeUnit.SECONDS)
//参数:获取锁最大等待时间(期间可重试),锁自动释放时间,时间单位
if(islock){
try{
执行业务
}finally{
lock.unlock(); //释放锁
}
}
redis分布式锁,如何实现
- 使用redisson实现的分布式锁,底层是setnx和lua脚本
redisson实现分布式锁,如何控制锁的有效时常
- 线程获取锁成功后,看门狗会给有锁的线程续期(默认10秒续期一次)
redisson的锁可重入吗
- 可重入
集群
主从复制
读写分离
从机只能读,不能写。
复制原理工作流程
- slave启动,同步初清
slave启动时,会一次性同步master,自生数据被覆盖清除 - 首次连接,全量复制
master收到命令后,触发RDB,将RDB文件发送给slave,实现复制初始化 - 心跳持续,保持通讯
每10秒发心跳,保持通讯 - 进入平稳,增量复制
master继续将收到的修改命令转给slaver,完成同步 - 从机下线,重连续传
slaver下线后,master检查offset偏移量,slaver上线后再发给slaver
缺点
- 复制延时,信号衰减
- master挂了,slaver不会上位成为master
哨兵模式
哨兵sentinel
master主机故障后,根据投票数自动将某一从库转换为主库
哨兵作用:
- 监控redis运行状态,包括master和slave
- master主机down后,自动将从机上位为主机
分片集群
集群cluster,自带sentinel故障转移机制,无需再使用哨兵功能
可将多个master看作一个整体,但每个单独master处理数据不同,扩大了数据存储量
根据槽位把每个key分配到不同的master,每个master负责不同的key
Redis分片集群作用
- 集群有多个master,可以保存不同数据,增大数据存储量
- 整合了主从复制和哨兵模式
Redis分片集群数据如何存储和读取
- 哈希槽,将插槽分配到不同实例
- 根据key计算哈希值,找到插槽所在的实例
为什么快
Redis是单线程的,但为什么还快?
- Redis是纯内存操作
- 单线程可避免不必要的上下文切换可竞争条件
- 使用I/O多路复用模型,非阻塞IO
SSM
1、Spring框架中单例bean是线程安全的吗
- 不是线程安全,对bean中可修改的成员变量,要考虑线程安全问题(加锁)
2、什么是AOP
- 面向切面编程,将与业务无关,可重用的模块抽取出来,做统一处理
- 使用场景:记录操作日志、缓存处理、spring中内置事务处理
3、Spring中事务失效的场景
- 自己吞了异常异常捕获处理。异常捕获不会失效;自己处理异常导致事务无法回滚,失效
解决:手动抛出。在catch块添加 throw new RuntimeException(e) 抛出 - 抛出检查异常。throws FileNotFoundException 。Spring默认指挥回滚非检查异常
解决:@Transactional(rollbackFor=Exception.class) - 非public方法导致事务失效。Spring为方法创建代理、添加事务通知,前提是public的
解决:改为public方法
4、Spring中循环引用
两个bean对象互相持有对方,比如A依赖于B,B依赖于A
- Spring框架三级缓存解决大部分循环依赖
- 一级缓存:单例池,缓存初始化完成的bean对象
- 二级缓存:缓存早期bean对象(未走完生命周期)
- 三级缓存:缓存ObjectFactory对象工厂,用来创建对象
- 构造方法中出现循环依赖
解决:@Lazy 懒加载,需要时再进行bean对象创建
5、Springboot自动配置原理
- @SpringBootApplication中有一个@EnableAutoConfiguration注解
里面通过@Import注解导入相关配置选择器
6、Spring框架常见注解
- @Component、@Controller、@Service
实例化Bean - @Autowired
依赖注入 - @Configuration
指定当前类为Spring配置类 - @ComponentScan
初始化容器时要扫描的包 - @Bean
加入Spring容器 - @Aspect、@Before、@After、@Around
面向切面编程 - @RequestMapping、@PostMapping
请求路径 - @RequestParam
传入参数与接受参数不同时,指定参数名称 - @PathViriable
从请求路径获取参数
7、MyBatis执行流程
- 读取MyBatis配置文件mybatis-config.xml
- 构建会话工厂SqlSessionFactory
- 创建SqlSession对象
- 操作数据库接口,Executor执行器
- Executor接口中有MappedStatement类型参数,封装了映射信息
- 输入参数映射
- 输出结果映射
8、Mybatis是否支持延时加载
- 延时加载,需要用到数据时才加载
支持一对一,一对多关联查询的延时加载 - 默认关闭,可配置启用
9、Mybatis一级、二级缓存
- 一级缓存
HashMap本地缓存,作用域Session,当Session关闭后清空,默认打开 - 二级缓存
HashMap本地缓存,作用域namespace。需要单独开启
当一个作用域(一、二级缓存)增删改后,select中缓存被清空
拦截器
implements HandlerInterceptor //继承拦截器
boolean preHandle(){} //重写方法,返回true放行;返回false拦截
implements WebMvcConfigurer //配置,注册拦截器
void addInterceptors //重写方法,可添加排除拦截的路径
SpringCloud
1、SpringCloud的5大组件
- 注册中心/配置中心 Nacos
- 服务网关 Gateway
- 负载均衡 Ribbon
- 服务调用 Feign
- 服务保护 Sentinel
OpenFeign整合了Ribbon+RestTemplate
2、SpringCloud如何实现服务的注册发现
- 服务注册:
服务提供者需要把自己的信息注册到nacos,由nacos保存这些信息,如:ip、服务名称、端口 - 服务发现:
服务消费者向nacos拉取服务列表信息,如果服务提供者有集群,会使用负载均衡算法,选择一个发起调用 - 服务监控
服务提供者会向nacos发送心跳,如果90秒没收到心跳,则服务从nacos剔除
3、负载均衡如何实现
Nginx 服务器负载均衡
Ribbon 本地负载均衡
- 远程调用的feign会使用Ribbon实现负载均衡
负载均衡策略
- 轮询、
- 权重,响应时间长、权重小
- 随机,
- 区域敏感策略(默认)
以区域可用服务器为基础做服务器选择
4、服务雪崩(降级熔断)
- 服务互相调用,一个服务失败,可能导致整条链路服务失败
- 解决
- 服务降级
当服务调用不可用时,服务自我保护机制,确保服务不会崩溃(不在抛异常,而是返回给用户消息)
在Feign接口中fallback编写降级逻辑(处理服务调用失败,如返回网络连接失败) - 服务熔断
如果10秒内请求失败率超过50%,触发熔断机制
之后每隔5秒重新尝试请求,失败则继续熔断、成功则恢复正常请求
默认关闭,需要手动开启
- 服务降级
5、限流
- Nginx限流
控制速率,漏桶算法,固定速率处理请求,可应对突发流量
控制并发数,限制单个ip的连接数 - 网关限流
令牌桶算法,可根据IP或路径限流,可设置每秒填充速率,和令牌桶总容量
每秒生成令牌,每个令牌对应一个请求,可提前生成令牌
6、分布式事务
分布式系统中,多个业务必须同时成功或失败,叫分布式事务。
CAP和BASE
- CAP(一致性、可用性、分区容错性)
- AP(可用性、分区)
- CP(一致性,分区)
- BASE理论
- 基本可用
- 软状态
- 最终一致性
解决分布式事务思想和模型
强一致性
各个分支事务执行完不要提交,等待彼此结果,而后统一提交(CP)
seata的XA模式,
事务协调者TC统一提交事务
最终一致性
各个分支事务分别提交执行,如有不一致,再想办法恢复(AP)
seata的AT模式,
事务协调者TC检查各个事务是否有失败的,失败则undo-log回滚
seata
TC:Transaction Coordinator
事务协调者,Seata,只有一个
TM:Transaction Manage
事务管理者,标注全局@GlobalTransactional,启动入口动作的微服务模块,只有一个
标注@GlobalTransactional的service,既是TM,也是RM
RM:Rescorce Manage
资源管理器,mysql数据库本身,可以多个RM
7、接口的幂等性问题
幂等:多次访问不会改变业务的状态,保证多次调用结果一致性
- get、delete 是幂等的
- post 新增
不是幂等的 - put 更新
以绝对值更新是幂等的;增量更新则不是幂等的
解决
- 分布式锁,效率低
- token + redis
- 第一次请求,获取token,存入redis中,返回给前端
- 第二次请求,带token业务处理。
token存在则执行业务,并删除redis中的token
token不存在,则不处理业务
RabbitMQ
RabbitMQ模式
- 简单模式
- 轮询模式
- 发布确定模式
生产者
RabbitTemplate.convertAndSend(),可进行消息发布,指定交换机、RoutingKey、消息对象
RabbitTemplate.setReturnsCallback,生产者确认,发消息给交换机确认是否收到,并处理
消费者
@RabbitListener注解,可声明队列、交换机、BindingKey
exchange=@Exchange(delayed=“true”),使用延时消息插件,设置交换机类型为死信交换机
交换机类型
- Fanout(广播)
将接收到的所有消息广播到它知道的所有队列中。 - Direct(定向)
交换机根据接收消息的规则路由到指定队列。
发布者:指定消息的RoutingKey
消费者(队列):设置一个与Exchange交换机绑定的BindingKey - Topics(话题)
与Direct类似,区别是RoutingKey可以是多个单词的列表,并以 . 分割
消息可靠性
- 生产者可靠性
- 生产者重连
由于网络波动,连接失败时重连。可配置重连机制
阻塞重连,会影响性能,建议禁用重试 - 生产者确认机制
生产者投递消息到MQ
成功返回ACK:临时消息(未持久化队列)入队成功;持久消息入队成功且持久化成功
失败返回NACK
缺点:会增加系统负担,对可靠性要求严格可使用确认机制,否则可不用
- 生产者重连
- MQ可靠性
MQ消息保存在内存中,如果MQ宕机,消息会丢失
内存空间有限,可能会导致消息积压、消息阻塞- 数据持久化
交换机持久化、队列持久化、消息持久化。重启后依然存在,不会发生消息阻塞
缺点:性能比非持久化队列差点,因为会写数据进磁盘 - Lazy Queue惰性队列
接受消息后直接存入磁盘而非内存(内存只保留最近消息)
消费者要消费数据才从磁盘读取
3.12版本后,所有队列都是Lazy Queue模式,无法更改
- 数据持久化
- 消费者可靠性
-
消费者确认机制
spring确认消息是否接收成功,返回消息如下- ack:消息处理成功,MQ从队列中删除消息
- nack:消息处理失败,MQ需要再次投递消息(出现运行时异常)
- reject:消息处理失败并拒绝,MQ从队列中删除消息(抛出消息转换异常)
SpringAMQP已经实现消息确认,在yml中配置
none:不处理,会丢失消息。manual:手动模式。auto:自动模式(可设置)
缺点:返回nack,会无限重新进入队列,不断的发给消费者,无限循环
解决:消息失败处理 -
消息失败处理
retry配置,出现异常时,开启本地重试,而不是无限重新入队。
MessageRecover接口的三种实现- 重试耗尽后,直接reject(默认)
- 重试耗尽后,返回nack,重新入队
- 重试耗尽后,将失败消息投递给指定交换机(编写一个处理失败的交换机)
-
消息重复消费如何解决?
与接口的幂等性问题类似
幂等:多次访问不会改变业务的状态,保证多次调用结果一致性
- get、delete 是幂等的
- post 新增
不是幂等的 - put 更新
以绝对值更新是幂等的;增量更新则不是幂等的
解决
- token + redis
- 第一次请求,获取token,存入redis中,返回给前端
- 第二次请求,带token业务处理。
token存在则执行业务,并删除redis中的token
token不存在,则不处理业务
延时消息
应用:超时订单,取消超时订单的业务
死信交换机
变成死信条件,队列设置dead-letter-exchange属性绑定死信交换机
- reject 或 nack消费失败,并重新入队参数为false
- 消息是过期消息(队列或消息本身设置了过期时间,无人消费)
- 投递的队列消息堆积满了,最早的消息可能成为死信
消息堆积解决?
- 增加更多消费者
- 消费者内开启线程池
- MQ消息持久化、惰性队列