自我介绍
面试官您好,我叫xxx,目前是南京邮电大学计算机学院研二的学生,很荣幸能够参加xx公司的(暑期)实习面试。我在研一学年专业成绩排名第三,获得了研究生国家奖学金,同时目前发表了一篇SCI二区的论文,并且公开了两项发明专利,已经达到毕业条件。
我之前有过一段在博世软件中心实习的实习的经历,实习的项目主要是后端开发项目,在里面我主要负责的工作是:用户登录、权限管理、数据可视化工作。
除此之外,我在去年8月份参加了中国软件杯大学生软件设计大赛,参赛的项目是智能简历解析系统,在其中我主要做的工作是:整个系统的开发工作,最终我们团队获得了国家级二等奖,并且申请了一项发明专利。
平时自己也会写写自己的个人博客,同时我也买了一台阿里云的服务器,在上面搭建一些小的应用来练手。
如果能够有幸通过面试的话,我可以实习的时间为为4到6个月。
之前CSDN、知乎上分享了一篇基于Picgo
+gitee
+Typora
搭建个人笔记系统获得了300多点赞;
HR面试
个人技能部分
JavaSE
==和equals()的区别
==
对于基本数据类型和引用类型有些区别:
- 对于基本数据类型,
==
比较的是值 - 对于引用数据类型,
==
比较的是对象的内存地址值
equals()
不能用于判断基本数据类型的变量,只能·
面向对象的三大特性
对象定义了私有的属性,不直接对外暴露接口,而是通过提供的公有的get()
/set()
方法来进行一系列的操作
不同类型的对象,相互之间会具有一部分相同点,可以将这部分内容抽象成一个父类,例如学生A和学生B可以首先抽象出一个学生类,这样有新的学生了就可以继承自这个学生类,并且可以在新类中添加新的属性和方法,这样就可以复用之前的代码,提高代码的重用性,节省创建新类的时间,提高开发效率。
继承需要注意下面三点特征:
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法), 但是父类的私有属性和方法子类只是拥有,无法访问
- 子类可以拥有自己特有的属性和方法,实现对父类的扩展
- 子类可以重写父类的方法
一个对象具有多种状态,具体表现为父类的引用指向子类的实例。
多态的特点:
- 对象类型和引用类型之间具有继承/实现关系;
- 引用类型变量发出的方法调用的到底是哪个类,必须在程序运行期间才能确定;
- 多态不能调用只在子类存在而不在父类中存在的方法
- 如果子类重写了父类的方法,真正执行的是子类重写的方法;如果没有重写,则执行的是父类中的方法
接口和抽象类的区别
共同点:
- 接口和抽象类都不能被实例化
- 他们都可以包含抽象方法
- 都可以有默认实现的方法,Java8中提供default关键字在接口中定义默认方法
区别:
- 接口主要用于对类的行为进行约束,你实现了某个接口就可以具有接口中的某些方法。抽象类主要强调的是代码的复用,强调继承关系
- 一个类只能继承自一个父类,但可以通过实现多个接口来弥补Java中无法多继承的缺陷
- 接口中的成员变量只能是
public static final
类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。
Java对象的大小必须是8比特的倍数
对象分成三部分:
- 对象头
- 实例数据
- 填充字节:为了满足Java对象必须是8比特的倍数这一条件设计的
泛型
- 泛型类
- 实现泛型接口
- 泛型方法
具体的使用场景:
- 使用集合的之后指定其装的元素的类型
- 在Springboot项目中定义统一的返回值类型Result,使用泛型来接受不同类型的数据。
- 集合工具类
反射
注解
Java序列化
序列化:将Java中的类对象转换为二进制字节流的过程
反序列化:将二进制字节流转换为Java中的对象的过程
代理模式
- 静态代理
- 动态代理
- jdk动态代理
cglib
动态代理机制
Java集合
集合框架
- Collection 单列集合
- List 有序,可重复
- vector 数组结构,线程安全,性能相对较低
- ArrayList 数组结构,非线程安全
- LinkedList 链表结构,非线程安全,底层使用的是双向链表
- Set 无序,唯一
- HashSet 哈希表结构
- LinkedHashSet 哈希表和链表结构
- TreeSet 底层使用红黑树结构
- HashSet 哈希表结构
- List 有序,可重复
- Map 双列集合
- HashTable 哈希表结构,线程安全 Properties
- HashMap 哈希表结构,非线程安全 LinkedHashMap 哈希表+链表结构
- ConcurrentHashMap 哈希表结构,线程安全
- TreeMap 红黑树的结构
ArrayList底层实现原理
- ArrayList底层是使用动态数组实现的
- ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10
- ArrayList在进行扩容的时候是原来的1.5倍,每次扩容都需要拷贝数组
- ArrayList在添加数据的时候
- 确保数组已使用的长度
size+1之
后能够存下下一个数据 - 计算数组的容量,如果当前数组已使用长度+1大于当前数组的长度,则调用grow方法进行扩容,扩容为原来的1.5倍
- 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上
- 返回添加成功布尔值
- 确保数组已使用的长度
1 | ArrayList list = new ArrayList(10); |
根据源码,这个只是声明和实例了一个ArrayList
,指定了容量为10的数组,未进行扩容。
ArrayList扩容机制
在使用空参构造进行初始化的时候,会创建了一个长度为0的空数组,并且有一个变量size
,代表当前数组的长度和下一个元素的位置。
在插入第一个元素的时候,系统会创建一个长度为10的数组,
扩容情况1:当数组存满的时候,会创建一个新数组长度是原来的1.5倍,然后再把所有的元素拷贝到新数组中;如果继续添加元素长度不够了,那么就会按照同样的方式继续扩容1.5倍
扩容情况2:当使用addAll()
方法进行扩容的时候,扩容1.5倍数组长度不够用的情况下,则扩容会以实际元素长度大小为准。
ArrayList和LinkedList的区别是什么?
底层数据结构
ArrayList底层是动态数组的数据结构实现,扩容时需要进行数组的拷贝
LinkedList是双向链表的数据结构实现
效率
- ArrayList按照下标查询的时间复杂度为O(1),【内存是连续的,根据寻址公式】,LinkedList不支持下标查询
- 查找(未知索引):ArrayList需要遍历,链表也需要遍历,时间复杂度都是O(n)
- 新增和删除
空间占用
- ArrayList底层是数组,内存连续,节省内存
- LinkedList是双向链表需要存储数据和两个指针,更占用内存
线程是否安全
- 这两个都不是线程安全的
- 如果需要保证线程安全,有两种方案:
- 在方法内使用,局部变量是线程安全的
- 使用线程安全的ArrayList和LinkedList
HashMap扩容机制
HashMap
底层实现是数组+链表+红黑树
使用空参构造的时候,是将loadFactor
初始化为0.75
put操作:
如果当前位置没有值,直接在数据中添加键值对即Node对象
如果数组长度超过16 * 0.75 =12的时候,进行扩容,扩容到原来的2倍
如果当前位置哈希值一致并再判断key值是否一致:
- key值一致:进行覆盖更新
- key值不一致:则在其后以链表的形式进行追加,如果链表长度达到8,则会调用
treeifyBin()
方法,此方法会根据HashMap数组长度来判断是否需要转换为红黑树,只有当数组长度大于等于64的时候才会进行转换红黑树的操作,以减少搜索时间。否则只会执行resize()方法对数组进行扩容。
Java并发
线程和进程的区别
当一个程序被运行,从磁盘加载这个程序的代码到内存中,这个时候就开启了一个进程。分为两种:
- 多实例进程
- 可以打开多个的
- 单实例进程
- 微信等只能打开一份的
二者对比:
- 进程是正在运行程序的实例,进程中包含了线程,每个线程可以执行不同的任务
- 不同的进程使用不同的内存空间,在当前进程下所有线程可以共享内存空间
- 线程更轻量,线程上下文切换成本一般要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
并发和并行的区别
在多核CPU下:
- 并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU
- 并行是同一时间动手做多件事情的能力,4核CPU同时执行4个线程
创建线程的方式有哪些?
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 线程池创建线程
线程池相关面试题
- 线程池核心参数?
- 如何确定核心线程数量
- 如何确定线程池大小?
- CPU密集型:将线程数设置为
N+1
。这里的N是机器的CPU核心数,多加1是因为可以防止某个线程发生缺页中断,或者其他原因导致任务暂停带来的影响。 - IO密集型:将线程数设置为
2N
,这样的设置可以在线程进行I/O操作的时候,让出CPU执行权给其他线程使用,提高使用效率。
- CPU密集型:将线程数设置为
线程池中有哪些常见的阻塞队列
- ArrayBlockingQueue:基于数组实现,按照先进先出的原则进行操作;
- LinkedBlockingQueue:基于链表实现的可选有界或者无界阻塞队列,也可以按照先进先出的原则进行操作;
- PriorityBlockingQueue:基于堆结构实现的优先级阻塞队列,元素按照优先级进行排序;
- SynchronousQueue:一个不存储元素的阻塞队列,用于线程之间的直接传输;
- DelayQueue:基于优先级队列实现的延时阻塞队列,元素按照延时时间进行处理。
如何给线程池命名
- 使用guava的
ThreadFactoryBuilder
线程池的拒绝策略
在线程池的内部类中,存在内部类就是其拒绝策略,主要有以下四种:
- AbortPolicy:会抛出
RejectedExecutionException
来拒绝新任务 - CallerRunsPolicy:调用执行自己的线程运行任务
- DiscardPolicy:不处理新任务,直接丢弃
- DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求
线程池的创建方式
方式一:通过**ThreadPoolExecutor
**构造函数来创建(推荐)
七个参数
方式二:通过Executor框架的工具类Executors来创建
常见内置的线程池
- FixedThreadPool
- SingleThreadExecutor
- CachedThreadPool
- ScheduleThreadPool
为什么不建议使用Executors创建线程池?
- 无法给线程的名称根据业务进行自定义的命名
- 参考阿里巴巴的Java开发手册
- 使用ThreadPoolExecutor的方式可以让写代码的人更明确线程池的运行规则,避免资源耗尽的风险。
- 几种自带的线程池默认的请求队列长度为Integer.
MAX_VALUE
,可能会创建大量的线程导致OOM异常
线程池的使用场景
调用多个接口进行数据汇总
可以使用线程池+Future来提升性能
- 报表汇总
调用多个接口来汇总数据,如果所有的接口或者部分接口没有依赖关系,就可以使用线程池+Future来提升性能
实现Runnable接口和Callable接口有什么区别?
- Runnable无法获取线程执行之后的结果
- Callable接口的call方法有返回值,是个泛型,和Future、FutureTask配合使用可以来获取异步执行的结果
- Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
线程的run()和start()有什么区别?
start():用来启动线程,通过该线程调用run方法执行run方法中所执行的逻辑代码。start方法只能调用一次
run():封装了要被线程执行的代码,可以被调用多次。
线程包括哪些状态,状态之间是如何变化的?
- 创建线程对象进入新建状态
- 线程获得CPU执行权进入运行状态
- 如果没有获得CPU执行权限:
- 如果没有获取锁,进入阻塞状态
- 如果线程调用wait()方法,进入等待状态
- 超时等待状态
- 线程执行完毕进入死亡状态
新建T1、T2、T3三个线程,如何保证他们按顺序执行?
join()方法
notify()和notifyAll()有什么区别?
- notifyAll():唤醒所有wait的线程
- notify():只随机唤醒一个wait线程
wait()和sleep()方法有什么不同?
共同点:效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态。
不同点:
- 方法的归属不同
- sleep()是Thread的静态方法
- 而wait()、wait(long)都是Object的成员方法,每个对象都有
- 醒来时机不同
- 执行sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来
- wait(long)和wait()还可以被notify唤醒,wait()如果不唤醒就一直等待下雨。
- 他们都可以被打断唤醒
- 锁特性不同(重点)
- wait方法的调用必须先获取wait对象的锁【配合synchronize使用】,而sleep则无限制
- wait方法执行后会释放对象锁,允许其他线程获得该对象锁(我放弃CPU,但是你们还可以用)
- sleep如果在synchronized代码块中执行,并不会释放对象锁(我放弃CPU,你们也不能用)
如何停止一个正在运行的线程
有三种方式停止线程
- 使用退出标志,volatile boolean flag = false;
- 使用stop方法强行终止(不推荐,方法已作废)
- 调用interrupt方法中断线程
- 打断阻塞的线程(sleep、wait、join),线程会抛出
InterruptedException
- 打断正常的线程,可以根据打断状态来标记是否退出线程
- 打断阻塞的线程(sleep、wait、join),线程会抛出
synchronized关键字的底层原理
多个窗口卖票的案例,多个线程模拟多个窗口
synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其他线程再想获取这个对象锁时就会阻塞。
底层是monitor,其结构中包括下面三个部分:
- WaitSet:关联的是等待的线程,即调用wait()方法的线程,处于waiting状态的线程
- EntryList:关联的是处于阻塞状态的线程,并且只能关联一个线程;
- Owner:存储当前获取锁的线程,只有一个线程能获取
谁抢到了Owner哪个线程就拿到了锁
javap -v xx.class 查看class字节码文件
Monitor被翻译为监视器,是由jvm提供,C++实现的
synchronized锁升级
Monitor实现的锁属于重量级锁,里面涉及到了用户态【Java代码】和内核态【CPU层面的东西】的切换、进程的上下文切换,成本较高,性能比较低。
jdk1.6之后引入了两种新型锁机制:偏向锁和轻量级锁,他们的引入是为了解决在没有多线程竞争或者基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。
Java中的synchronized有三种【从上到下依次降级】:
- 重量级锁
- 轻量级锁
- 偏向锁
- 无锁
对象内存结构
synchronized和ReentrantLock有什么区别?
语法层面
- synchronized是关键字,源码在JVM中,用C++实现
- ReentrantLock是实现Lock接口的类,源码由JDK提供,用java语言实现
- 使用synchronized时,退出同步代码块会自动释放锁,而使用Lock时,需要手动调用unlock方法释放锁。
功能层面
- 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
- Lock提供了许多synchronized不具备的功能,例如公平锁、可打断、可超时、多条件变量
- Lock有适合不同场景的实现,如ReentrantLock、ReentrantReadWriteLock
性能层面
- 没有竞争时,synchronized做了很多优化,如偏向锁、轻量级锁,性能也还不错
- 在竞争激烈时,Lock的实现通常会有更好的性能
二者都是可重入锁
synchronized在使用时,退出同步代码块锁就会自动释放,而使用Lock时,需要手动调用unlock方法释放锁
synchronized依赖于jvm,而ReentrantLock依赖于API
reentrantLock比synchronized增加了一些高级功能
- 等待可中断
- 可实现公平锁
- 可实现选择性通知(锁可以绑定多个条件)
ReentrantLock底层原理是什么?
- 可重入锁,这一点跟
synchronized
一致 - 可中断
- 可以设置超时时间
- 可以设置超时时间
ReentrantLock
主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者实现是类似的
聊一聊concurrentHashMap
底层数据结构:
- jdk1.7采用分段的数组+链表实现;
Segment
数组的大小默认是 16,也就是说默认可以同时支持 16 个线程并发写。 - jdk1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑树
加锁的方式:
- jdk1.7采用Segment分段锁,锁定的范围比较大,底层使用的是ReentrantLock
- jdk1.8采用CAS添加新节点,采用synchronized锁定链表或红黑树的首节点,相较于Segment分段锁粒度更细,性能更好
Java内存模型JMM
- JMM是java内存模型,定义了共享内存中多线程程序的读写操作的行为规范, 通过这些规则来规范对内存的读写操作从而保证指令的正确性。
- JMM把内存分为两块,一块是私有线程的工作区域 (工作内存),一块是所有线程的共享区域(主内存)
- 线程跟线程之间是相互隔离的,线程和线程的交互需要通过主内存来共享数据
- 工作线程之间不存在线程安全问题
工作内存中的数据是对线程私有的,线程之间无法互相访问
主内存:多个线程如果想要同步数据的话,只能通过主内存进行同步
CAS你知道吗?
Compare And Swap,它体现了一种乐观锁的思想,在无锁的情况下保证线程操作共享数据的原子性。
JUC(java.util.concurrent)包下有很多使用的地方:AQS框架、AtomicXXX类
操作共享变量的时候使用自旋锁,效率上更高一些
- 因为没有加锁,所以线程不会陷入阻塞,效率较高
- 如果竞争激烈,重试频繁发生,效率会受影响
底层调用的是Unsafe类中的方法,否是操作系统提供的,基于C/C++实现的
CAS底层实现
CAS底层依赖于一个Unsafe类来直接调用操作系统底层的CAS指令,借助于C/C++来调用底层的CAS操作
ReentrantLock中有一段CAS代码:
- 当前值
- 期望值
- 更新后的值
CAS存在什么问题?
- ABA问题
- 一个变量V初始的时候值为A,并且准备赋值的时候检查其值仍然为A,那么就能说明这中间的值没有被修改过了吗?
- 可能是先被修改为B然后再修改为A,这个问题就被叫做ABA问题
- 解决思路:在变量前面追加上版本号或者时间戳
- 循环时间长开销大
- 只能保证一个共享变量的原子操作
乐观锁和悲观锁
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。【重试/自旋】
- 自旋锁:通过循环不断地尝试获取锁,直到成功为止
- 锁的保持时间短的情况;
- 并发冲突较少的情况;
- 获取锁的操作是不可阻塞的情况。
- 自旋锁:通过循环不断地尝试获取锁,直到成功为止
synchronized是基于悲观锁的思想,最悲观的估计,需要防止其他线程来修改共享变量,上锁之后其他的线程都无法进行修改直到释放锁。
高并发下,乐观锁相较于悲观锁来说,不存在锁竞争导致的线程阻塞问题,也不会有死锁的问题,在性能上也会更胜一筹。
悲观锁适合用于写较多的情况下,读较少【并发写的场景】
乐观锁适合用于写较少的情况下,读较多【并发读的场景】
如何实现乐观锁
- 版本号机制
- CAS算法
谈谈你对Volatile关键字的理解
- 保证线程之间的可见性
- 禁止进行指令重排序
用volatile
修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见
JVM虚拟机中有一个JIT(即使编译器)给代码做了优化
解决方案一:
解决方案二:
volatile使用技巧
- 写变量让volatile修饰的变量在代码的最后位置
- 读变量让volatile修饰的变量在代码最开始的位置
Future
什么是AQS
- 抽象队列同步器,是一种锁机制,像ReentrantLock、Semaphore都是基于AQS实现的
AQS的核心原理是:如果被请求的共享资源是空闲的,则将当前线程设置为有效的工作线程,并且将共享资源设置为锁定状态;如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配机制,这个机制是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH队列是一个虚拟的双向队列
AbstractQueuedSynchronizer,抽象队列同步器。他是构建锁或者其他同步组件的基础框架。
AQS常见的实现类
- ReentrantLock 阻塞式锁
- Semaphore 信号量
- CountDownLatch 倒计时锁
CountDownLatch
CountDownLatch允许count个线程阻塞在一个地方,直到所有线程的任务全都执行完毕。
CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制在对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。
原理:
CountDownLatch是共享锁的一种实现,它默认构造AQS的state值为count。
闭锁/倒计时锁同来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行)
在实际开发中如果想要计算多个线程的运行总时间,就可以使用CountDownLatch进行计算
1 | private ExecutorService es = Executors.newFixedThreadPool(500); |
多线程实际开发中的实际场景(数据汇总)
谈谈对ThreadLocal的理解
线程内部的存储类,让多个线程只操作自己内部的值,实现了线程的隔离
ThreadLocal可以实现对【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免竞争引发的线程安全问题
ThreadLocal同时实现了线程内的资源共享
每个线程内都有一个ThreadLocalMap类型的成员变量,用来存储资源对象
- get:以ThreadLocal自己作为key,到当前线程中查找关联的资源值
- set:以ThreadLocal自己作为key,资源对象作为value,放入当前线程的ThreadLocalMap集合中
- remove:以ThreadLocal自己作为key,移除当前线程所关联的资源值
ThreadLocal内存泄漏问题
ThreadLocalMap中key为弱引用,值为强引用,key会被垃圾回收释放内存,但是关联的value的内存并不会释放。一般来说建议使用remove方法主动释放key,value
强引用不管什么情况都不会被垃圾回收
弱引用一旦进行垃圾回收就会被回收
Entry对象继承自WeakReference,其中key为使用弱引用的ThreadLocal对象,value为线程变量的副本
使用remove清除对象
JVM
什么是程序计数器
程序计数器是线程私有的,每个线程一份,内部保存字节码的行号,用于记录正在执行的字节码指令的地址。
你能给我详细介绍Java的堆吗?
Java堆是线程共享的区域:主要用来保存对象实例,数组等,当堆中没有内存空间可以分配给实例,也无法再扩展的时候,则抛出OutOfMemoryError
异常(OOM)
S0和S1被称为幸存者区Survivor,一个对象来了会到Eden区,假如这个对象在垃圾回收之后还能存活,就会被复制到S0或者S1,假如移动一定次数之后,还能够存活,就会被移动到老年代区域。
老年代:主要保存的是声明周期长的对象,一般是一些老的对象。
回答
Java堆是线程共享的区域:主要用来保存对象实例,数组等,内存不够则抛出OOM异常
组成包括:年轻代和老年代,其中年轻代又分为Eden和两个大小严格相同的Survivor区,老年代主要保存生命周期长的对象,一般是一些老的对象。
JDK7和JDK8的区别:
- Java7中有一个永久代,存储类信息、静态变量、常量、编译后的代码;
- Java8中移除了永久代,把数据存储到了本地内存的元空间中,防止内存溢出。
什么是虚拟机栈?
- 每个线程运行时所需要的内存,被称为虚拟机栈,先进后出,栈内存也是线程安全的
- 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
垃圾回收是否涉及栈内存?
垃圾回收主要指的是堆内存,当栈帧弹栈以后,内存就会释放
栈内存分配的越大越好吗?
未必,默认的栈内存通常为1024k
栈帧过大会导致线程数变少,例如机器总内存为512M,目前能活动的线程数则为512个,如果把栈内存改为2048k,那么能活动的栈帧就会减半
方法内的局部变量是否安全?
- 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
栈内存溢出情况
- 栈帧过多导致栈溢出,典型问题:递归调用
- 栈帧过大导致栈内存溢出,
栈堆的区别是什么?
- 栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的。堆会GC垃圾回收,而栈不会。
- 栈内存是线程私有的,而堆内存是线程共有的。
- 两者异常错误不同,但如果堆内存或者栈内存不足都会抛出异常
- 栈空间不足:
java.lang.StackOverFlowError
- 堆空间不足:
java.lang.OutOfMemoryError
能不能解释一下方法区
- 方法区是各个线程共享的内存区域
- 主要存储类的信息、运行时常量池
- 虚拟机启动的时候创建,关闭虚拟机时释放
- 如果方法区中的内存无法满足分配请求,则会抛出
OutOfMemoryError:Metaspace
jdk8以前,方法区是存在堆内存中的永久代中的;jdk8以后,方法区是存在元空间中的
运行时常量池
- 常量池:可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 当类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
你听过直接内存吗?
直接内存:
- 并不属于JVM中的内存结构,不由JVM进行管理。是虚拟机的系统内存【即操作系统内存】
- 常见于NIO操作时,用于数据缓冲区,它分配回收成本较高,但读写性能高。
什么是类加载器,类加载器有哪些?
类加载器
JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让java程序能够运行起来
类加载器类型
- 启动类加载器:主要加载
JAVA_HOME/jre/lib
目录下的库 - 扩展类加载器:主要加载
JAVA_HOME/jre/lib/ext
目录下的类 - 应用类加载器:用于加载
classPath
下的类 - 自定义加载器:自定义继承
ClassLoader
,实现自定义类加载规则
什么是双亲委派模型?(*)
加载某一个类,先委托上一级的类加载器进行加载,如果上级类加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类。
如果子类加载器也无法加载这个类,那么会抛出ClassNotFoundException
异常。
例如,我们自定义的Student类,加载的时候先找AppClassLoader
,发现有上级,则继续往上找ExtClassLoader
,还有上级继续往上委托找,然后发现里面没有lib
和ext
里面没有,然后由AppClassLoader
加载器进行加载
JVM为什么采用双亲委派机制?
(1)通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性
(2)为了安全,保证类库API不会被修改
类装载的执行过程
类从加载到虚拟机中开始,直到卸载为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这七个阶段。其中验证、准备和解析这三部分统称为连接。
加载
- 通过类的全名,获取类的二进制数据流
- 解析类的二进制数数据流为方法区内的数据结构(JAVA类模型)
- 创建
java.lang.Class
类的实例,表示该类型。作为方法区这个类的各种数据的访问入口。
验证
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
- Class文件在其常量池会通过字符串记录自己将要使用的其他类或者方法,检查他们是否存在
准备
- static变量,分配空间在准备阶段完成(设置默认值),赋值在初始化阶段完成
- static变量是final的基本类型,以及字符串常量,值已确定,赋值在准备阶段完成
- static变量是final的引用类型,那么复制也会在初始化阶段完成
解析
把类的符号引用转换为直接引用
比如:方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接指向方法
初始化
对静态变量、静态代码块执行初始化操作
- 如果说初始化一个类的时候,其父类尚未初始化,则优先初始化其父类
- 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行
静态代码块通过static关键字进行修饰,随着类加载而加载,并且自动触发,只执行一次。
使用场景:在类加载的时候需要做一些数据初始化的时候使用。例如Redis的Java客户端,使用静态初始化配置一些连接池的信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 package com.heima;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class JedisConnectionFactory {
private static final JedisPool jedisPool;
static {
// 配置连接池
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(8);
poolConfig.setMaxIdle(8);
poolConfig.setMinIdle(0);
poolConfig.setMaxWaitMillis(1000);
// 创建连接池对象
jedisPool = new JedisPool(poolConfig, "1.94.65.33", 6379, 1000, "123321");
}
public static Jedis getJedis() {
return jedisPool.getResource();
}
}
使用
JVM开始从入口方法开始执行用户的程序代码
- 调用静态类成员信息(比如:静态字段、静态方法)
- 使用new关键字为其创建对象实例
卸载
当用户程序代码执行完毕后,JVM便开始销毁创建的Class文件
回答
- 加载:查找和导入class文件
- 链接
- 验证:保证加载类的准确性
- 准备:为静态变量分配内存并设置类变量初始值
- 解析:把类中的符号引用转换为直接引用
- 初始化:对类的静态变量、静态代码块进行初始化操作
- 使用:JVM开始从入口方法执行用户的代码
- 卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class文件
对象什么时候可以被垃圾回收器回收(*)
垃圾回收主要是回收堆中的对象,堆是一个共享的区域,我们创建的对象或者数组都存在这个区域
如果一个对象或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能被垃圾回收器回收。
如果要定位什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法。
引用记数法
一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可以被回收
1 | String demo = new String("123") |
当对象间出现了循环引用的话,则引用计数法就会失效
循环引用会导致内存泄漏
可达性分析算法
现在的虚拟机采用的都是通过可达性分析算法来确定那些内容是垃圾
- Java虚拟机中垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到表示可以回收。
哪些对象可以作为GC Root?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(一般说的Native方法)引用的对象
JVM垃圾回收算法有哪些?
- 标记清除算法
- 复制算法
- 标记整理算法
标记清除算法
将垃圾回收分为2个阶段,分别是标记和清除
- 根据可达性分析算法得出的垃圾进行标记
- 对这些标记为可回收的内容进行垃圾回收
优点:标记和清除速度较快
缺点:碎片化比较严重,内存不连贯,可能导致没有连续的空间来存储较大的对象例如数组。
一般这个算法用的比较少
标记整理算法
清除之后会把存活的对象进行整理,避免了内存碎片的整理
优缺点同标记清除算法,解决了标记清除算法的碎片化问题,但是多了对象内存移动的步骤,效率有一定的影响。
很多老年代的垃圾回收器会采用这个算法
复制算法
复制算法会将原有的内存空间一分为二,每次只用其中的一块,正在使用的对象复制到另一个存储空间,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收;无碎片,内存使用率低。
优点:
- 在垃圾对象多的情况,效率较高
- 清理后,内存无碎片
缺点:
- 分配的2块内存空间,在同一个时刻,只能使用一半,内存使用效率低
年轻代的垃圾回收器一般会使用复制算法
JVM中的分代回收
分代收集算法
在Java8中,堆被分成了两份:新生代和老年代【1:2】
年轻代被分为三个区域:
- Eden区,新生的对象都分配到这里
- 幸存者区survivor(分成from和to)
- Eden区,from区,to区【8:1:1】
工作机制
- 新创建的对象,都会先分配到eden区
- 当eden区内存不足的时候,标记Eden区和from区的存活对象
- 将存活对象使用复制算法复制到to区,复制完毕后Eden和from区内存得到释放
- 经过一段时间后Eden的内存出现不足,继续进行标记清除
- 当幸存者区域的对象经过多次回收(最多15次),则晋升老年代
Minor GC、Mixed GC、Full GC的区别是什么?
MinorGC【young GC】发生在新生代的垃圾回收,暂停时间短(STW)
Mixed GC 新生代和老年代部分区域的垃圾回收,G1收集器特有
Full GC:新生代和老年代完整垃圾回收,暂停时间长(SWT),应尽力避免
什么是STW
STW(Stop-The-World):暂停所有应用程序线程,等待垃圾回收的完成
在垃圾回收事件过程中,会产生应用程序的停顿,停顿产生时整个应用程序线程都会被暂停,没有响应。
什么情况下会触发Full GC
- 调用Sysytem.gc()
- 未指定老年代和新生代的大小
- 老年代空间不足
- jdk1.7及以前的永久代空间满了
- 空间分配担保失败
JVM有哪些垃圾回收器(*)
在jvm中,实现了多种垃圾收集器,包括:
- 串行垃圾收集器(Serial收集器)
- 并行垃圾收集器
- CMS(并发)垃圾收集器(Concurrent Mark-Sweep)
- G1垃圾收集器
串行垃圾收集器
Serial和Serial Old串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑
- Serial作用于新生代,采用复制算法
- Serial Old作用于老年代,采用标记整理算法
垃圾回收时,只有一个线程在工作,并且Java应用中的所有线程都要暂停(SWT,stop the world),等待垃圾回收的完成。
并行垃圾回收器(很多jdk版本默认的)
Parallel New和Parallel Old是一个并行垃圾回收器,JDK8默认使用此垃圾回收器
- Parallel New作用于新生代,采用复制算法
- Parallel Old作用于老年代,采用标记整理算法
垃圾回收时,多个线程在工作,并且Java应用中素有的线程都要暂停(STW),等待垃圾回收的完成。
CMS(并发)垃圾收集器
CMS全称Concurrent Mark-Sweep,是一款并发的、使用标记清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。
其最大的特点是在进行垃圾回收时,应用仍然能正常进行。
总结
详细聊一下G1垃圾回收器(*)
G False或者G One垃圾回收器
- 应用在新生代和老年代,在JDK9之后默认使用G1
- 划分多个区域,每个区域都可以充当Eden、survivor、old、humongous,其中humongous专门为大对象准备
- 采用复制算法【没有内存碎片】
- 特别注重响应时间和吞吐量
- 分成三个阶段:新生代回收、并发标记、混合收集
- 如果并发失败(即回收速度赶不上创建新对象速度),则会触发Full GC
Yong GC(年轻/新生代垃圾回收)
- 初始时,所有区域都处于空闲状态
- 创建了一些对象,挑出一些空闲区域作为Eden区存储这些对象
- 当Eden区需要垃圾回收时,挑出一个空闲区域作为幸存区,使用可达性分析算法来判断哪些对象可以存活,然后采用复制算法复制存活对象到幸存者区中,需要短暂暂停用户线程 STW,幸存者区是比较少的,所以暂停时间相对来说也比较短
G1收集器中Eden占比**5%-6%**左右
- 随着时间的流逝,Eden区内存又不足了
- 将Eden以及之前幸存者中的存活对象采用复制算法,复制到新的幸存者区,其中较老的对象晋升至老年代
Yong GC+并发标记阶段
当老年代占用内存超过阈值(默认是45%)后,触发并发标记,这时无需暂停用户线程。
重新标记阶段需要STW
混合收集阶段
- 并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程
- 这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少)的区域(这些是Garbage First名称的由来)
混合收集阶段,参与复制的有Eden、survivor、old
复制完成,内存得到释放。进入下一轮新生代回收、并发标记、混合收集
强引用、软引用、弱引用、虚引用
强引用
只有所有GC Roots对象都不通过【强引用】引用该对象,该对象才能被垃圾回收,即使发生OOM也不会回收
1 | User user = new User(); |
软引用
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次发出垃圾回收
1 | User user = new User(); |
弱引用
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
1 | User user = new User(); |
ThreadLocal中,其键是弱引用类型,在垃圾回收的时候会被清理,而值为强引用,不会被回收
虚引用
必须配合队列使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存。
1 | User user = new User(); |
如何定位解决OOM异常
OOM异常的原因是,在进行一次Full GC之后,内存空间仍然不够,如果继续创建对象就会报错
使用jmap heap 跟上进程号就可以查看堆的信息,就可以适当的进行调整
OOM应用不一定会使我们的应用挂掉,如果某些请求并不需要申请堆内存的空间创建对象,那么就不会挂掉
OOM造成的原因:
- 一次申请的太多
- 更改申请的对象
- 内存资源耗尽未释放
- 找到未释放的对象进行释放;采用池化的思想
- 本身资源不够
- jmap heap查看堆信息
MySQL
了解过索引吗(什么是索引?)
索引是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护者满足特定查找算法的数据结构,也就是B+树,这些结构以引用的形式指向数据,这样就可以在B+树中高效的查找到所需要的数据。
索引是帮助MySQL高效获取数据的数据结构(有序)
索引提高了数据检索的效率,降低数据库的IO成本(不需要全表扫描)
索引列数据进行排序,降低了数据的排序成本,也降低了CPU的消耗
索引的底层的数据结构了解过吗?
MySQL5.7版本以后的默认存储引擎InnoDB
采用的是B+树的数据结构来存储索引的。
- 阶数更多,路径更短;
- 磁盘读写代码B+树更低,非叶子节点只存储指针,叶子节点存储数据;
- B+树便于扫库和区间查询
索引的底层使用的是B+树,B+树是一种在B树上进行优化的数据结构,其中非叶子结点只存放指针和键值,只有在叶子节点上才存放相应的数据
相较于B树而言:
- 磁盘读写代价B+树更低
- B树在查找的过程中查找路径上的节点的数据也会进行加载,所以磁盘的读写代价更大
- 查询效率B+树更加稳定
- 所有的数据全部存储在叶子节点上
- B+树便于扫库和区间查询
- 所有的叶子节点是通过双向指针链接的,在进行范围查询的时候会非常方便
什么是聚簇索引和非聚簇索引?
题目中也可能会问什么是聚集索引和二级索引(二级索引就是非聚集索引)
聚簇索引(聚集索引):将数据和索引存储在一起,也就是B+树的叶子节点保存了数据库的整行数据,必须有,且只有一个。如果没有InnoDB
会自动创建一个隐藏主键DB_ROW_ID
非聚簇索引(二级索引):将索引和数据分开存储,B+树的叶子节点存储的是对应的主键,可以有多个。
聚集索引选取规则:
- 如果存在主键,主键索引就是聚集索引
- 如果不存在主键,将使用第一个唯一索引作为聚集索引
- 如果表中没有主键,或者没有合适的唯一索引,则
innoDB
会自动生成一个DB_ROW_ID
作为隐藏的聚集索引。
什么是回表?
直接问什么是回表查询需要再解释一下什么是聚簇索引和非聚簇索引
先通过二级索引找到对应的主键值,再拿着这个主键去聚簇索引中去查询找到整行的数据。
什么是覆盖索引?
覆盖索引是指查询使用了索引,并且需要返回的列在该索引中已经能够查询到了。例如我们使用主键id进行查询,它会直接走聚集索引,一次索引扫描直接返回全部数据,这样的性能更高。
如果按照二级索引查询数据的时候,返回的列中没有创建索引,有可能会触发回表查询,应该尽量避免使用select *
进行查询,尽量在返回的列中都包含添加索引的字段。
MySQL超大分页处理
1 | select * from tb_sku t, (select id from tb_sku order by id limit 90000000,10) a |
索引的创建原则有哪些?
- 先陈述自己在实际工作中是如何使用的
- 主键索引
- 唯一索引
- 根据业务创建的索引(复合/联合索引)
1)针对数据量较大,且查询比较频繁的表建立索引。单表超过10万条数据(增加用户体验)
2)针对常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引。
3)尽量选择区分度高的列作为索引,尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度高,使用索引的效率越高。
4)如果是字符串类型的字段,字段的长度较长,可以针对与字段的特点建立前缀索引
5)尽量使用联合索引(复合索引),减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率。
6)要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率。
7)如果索引列不能存储NULL值,请在创建表的时候使用NOT NULL约束他。当优化器知道每列是否包含NULL值的时候,它可以更好地确定那个索引最有效地用于查询。
什么情况下索引会失效?
1)违反了最左前缀法则。
- 如果索引了多列,就要遵循最左前缀法则,指的是查询从索引的最左前列开始,并且不跳过索引中的列,匹配最左前缀法则,走索引:
- 违反了最左前缀法则,索引会失效
- 如果复合最左法则,但是出现中间跳过某一列,则只有最左索引失效。
2)范围查询右边的列,不能使用索引。例如:where name = “小米” and status > 1 and address = “北京”; 这个情况下address索引就无法生效了。
3)不要在索引列上进行运算操作【如字符串的substring操作】,索引将会失效。
4)字符串不加单引号,造成索引失效
5)以%
开头的Like模糊查询,索引失效。如果仅仅是尾部模糊匹配,索引不会失效;如果是头部模糊匹配,索引会消失。
通常情况下如果需要判断这条sql的索引是否失效可以使用EXPLAIN执行计划来进行分析。
谈谈你对sql优化的经验
- 表的设计优化
- 索引优化 (参考优化创建原则和索引失效)
- SQL语句优化
- 主从复制、读写分离
- 分库分表 (后面有专门的章节介绍)
表的设计的优化
参考阿里开发手册嵩山版
比如设置合适的数值(tinyint
、int
、bigint
)根据实际情况进行选择
设置合适的字符串类型(char
和varchar
)
SQL语句优化
- 避免使用select *
- SQL语句要避免索引失效的写法
- 避免where子句中对字段进行表达式操作
- 尽量用union all代替union,union回多一次过滤,效率低
- 能用inner join就不用left join和right join,如必须使用,一定要以小表为驱动
主从复制、读写分离
事务的特性是什么?
ACID来自于下面四个单词的首字母
- 原子性Atomicity:事务中的所有操作要么全部成功要么全部执行失败,事务是一个不可分割的最小执行单元,要么全部完成,要么全部不完成
- 一致性Consistency:一致性是事务执行的结果必须使数据库从一个一致性状态转变到另一个一致性状态。即事务执行前后数据库的完整性约束没有被破坏。
- 隔离性Isolation:隔离性要求事务的执行不受其他事务的影响,即事务之间应该相互隔离。多个事务并发执行时,每个事务都感觉不到其他事务的存在,互相之间不会产生影响。
- 持久性Durability:持久性是指事务一旦成功提交,所有的修改都会永久保存在数据库中,及时数据库发生故障,事务的提交结果也不会丢失。
- 事务特性
- 隔离级别
- MVCC
并发事务会带来哪些问题?怎么去解决这些问题?MySQL默认的隔离级别是?
并发事务的问题:脏读、丢失修改、不可重复度、幻读
- 脏读:一个事务读取到了另一个事务尚未提交的数据就是脏读。
- 丢失修改:有两个事务,第一个事务读取一个数据的时候,第二个事务也访问了这个数据,第一个事务修改这个数据之后,第二个数据也修改了这个数据,那么第一个事务的修改就丢失了。
- 不可重复读:有点类似于接口的幂等性问题,一个事务先后读取同一条记录,但是两次读取的数据值不同,被称之为不可重复读。【重点在与第二个事务对数据进行了修改】
- 幻读:一个事务查询数据的时候,没有对应的数据行,第二个事务在之后插入了几条数据,然后第一个事务在读取数据的时候,出现了原本根本不存在的数据,好像出现了幻觉【重点在于记录的新增,多次执行DQL语句的时候发现记录增加了】
如何解决这些并发事务产生的问题呢?
解决方案:对事务进行隔离
隔离级别:读已提交、读取未提交、可重复读、串行化
- 读取未提交:允许读取事务尚未提交的数据,属于最低的隔离级别,可能会导致脏读、不可重复读和幻读
- 读取已提交:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读和不可能重复度仍然有可能会发生
- 可重复读(是MySQL默认的隔离级别):对同一字段多次读取的结果是一致的,除非数据是被事务自己本身所修改,可以阻止脏读和不可重复读,但是幻读仍然会产生。
- 串行化:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,可以防止脏读、不可重复读和幻读。
事务的隔离级别越高,数据越安全,但是性能也越低
redo log和undo log的区别?(*)
- redo log记录的是数据页的物理变化,服务宕机可以用来同步数据
- undo log记录的是逻辑日志,当事务回滚时,通过逆操作【insert一条记录则记录一条delete】恢复原来的数据
- redo log保证了事务的持久性,undo log保证了事务的原子性和一致性
日志文件
redo log是重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性的。服务宕机时可以用来同步数据。
该日志由两部分组成:重做日志缓存(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者是在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中,用于刷新脏页到磁盘,发生错误时,进行数据恢复使用。
作用:当刷新脏页数据到磁盘时发生错误的时候,可以使用redo log日志文件进行恢复,从而保证了事务的持久性
undo log
回滚日志,用于记录数据被修改之前的信息,作用包括两个:提供回滚和MVCC。undo log和redo log记录物理日志不一样,他是逻辑日志。
- 可以认为当delete一条记录时,undo logo中会记录一条对应的insert记录,反之亦然。
- 当update一条记录时,它记录一条对应相反的update记录。当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。
作用:undo log可以保证事务的原子性和一致性。
- 缓冲池
- 主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查操作时,先操作缓冲池中的数据(如果缓冲池中没有数据,则从磁盘加载并缓存),并以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度
- 数据页
- 时
InnoDB
存储引擎磁盘管来的最小单元,每个页的大小默认为16KB。页中存储的是行数据
- 时
把数据页加载到内存中,在内存中进行操作,这样性能更高。当操作完成之后,会按照一定的频率再把数据同步到磁盘中。
在内存中尚未同步到磁盘中的数据页被称为脏页,只有把脏页同步到磁盘中才叫持久化。
如果服务器宕机了,同步脏页数据到磁盘中失败了,数据会产生丢失,违背了事务的特性持久性
redo log主要可以用来保证事务的持久性
WAL(write-AHead Logging) 先写日志,如果脏页数据能够正常同步到磁盘中的时候,redo log就没什么用了
redo log和binlog有什么区别?(*)
适用对象不同
- binlog是MySQL Server层实现的日志,所有的存储引擎都是可以使用的
- redo log是InnoDB存储引擎实现的日志,MyISAM无法使用,这也是在崩溃恢复上InnoDB优于MyISAM的原因
文件格式不同
binlog是逻辑日志,记录的是这个语句的原始逻辑,比如给这个id=2的字段c加1操作
redo log是物理日志,记录的是在某个数据页上做了什么修改
写入方式不同:
binlog是追加写,写满一个文件就会新建一个文件继续写入,不会覆盖以前的日志,保存的是全量的日志信息。
redo log是循环写,日志空间大小是固定的,写到最后就会从头开始重新写,保存未被刷入磁盘的脏页日志。
用途:
binlog用于备份恢复、主从复制,保证MySQL集群架构的数据一致性
redo log用于断电等故障崩溃恢复
MySQL数据库的备份、主备、主主、主从都离不开binlog,都需要binlog来同步数据,从而保证数据一致性。
binlog会记录所有涉及更新数据的逻辑操作,并且是顺序写
binlog的记录格式
binlog三种记录格式,可以通过binlog_format
参数进行指定
statement:记录SQL语句原文,存在一个缺陷就是更新时间为now的情况,会导致时间不一致的问题
row:该记录格式的内容看不到详细信息,需要通过mysqlbinlog工具解析出来【有详细的数据】,
update_time=now()
变成了具体的时间update_time=1627112756247
- 优点:可以解决时间不一致的问题,从而保证数据的一致性问题;
- 缺点:占用存储空间,恢复时更消耗IO资源,影响执行速度。
mixed:记录的内容是前两者的混合,mysql会判断这条sql语句是否会引起数据不一致的问题,如果是则使用row格式,否则使用statement格式
binslog的写入机制
事务执行的过程中,先将日志写入到binlog cache
redo log保证事务的持久性
undo log保证事务的原子性和一致性
事务的隔离性是如何保证的?(*)
- 锁
- 排他锁(如果一个事务获取了一行数据的排他锁,其他事务就不能再获取改行的其他锁)
- MVCC
- 多版本并发控制
MVCC,Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突。
MVCC的具体实现主要是依赖于数据库记录中的隐藏字段、undo log日志和readView
隐藏字段
DB_TRX_ID
:最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID,是自增的DB_ROLL_PTR
:回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本DB_ROW_ID
:隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段
undo log
- 回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。
- 当insert的时候 ,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除。
- 而update、delete的时候,产生的undo log日志不仅仅在回滚时需要,mvcc版本访问也需要,不会立即删除。
undo log版本链
不同事务或相同事务对同一条记录进行修改,会导致该记录的undo log生成一条记录版本链表,链表的头部是最新的就记录,链表的尾部是最早的旧记录。然后隐藏字段中的DB_ROLL_PTR
回滚指针会指向链表的开头。
readview
- 读视图是快照读SQL执行时MVCC提交数据的依据,记录并维护系统当前活跃的事务(未提交的)id
- 当前读
- 当前读是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如select … lock in share mode(共享锁),select … for update、insert、delete(排他锁)都是一种当前读。
- 快照读
- 简单的select(不加锁)就是快照读,快照读读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
- Read Commited(读取已提交
RC
):每次select都,都生成一个快照读 - Repeatable Read(可重复读
RR
):开启事务后第一个select语句才是快照读的地方
readview中包含了四个核心字段
字段 | 含义 |
---|---|
m_ids |
当前活跃的事务ID |
min_trx_id |
最小活跃事务ID |
max_trx_id |
预分配事务ID,当前最大事务ID+1(因为事务ID是自增的) |
creator_trx_id |
ReadView创建者的事务ID |
MySQL的主从同步原理
MySQL主从复制的核心就是二进制日志binlog
二进制日志binlog
记录了所有的DDL(数据定义语言)语句和DML(数据操纵语言)语句,但不包括数据查询(select、show)语句
复制分成三步:
- Master主库在事务提交时,会把数据变更记录在二进制文件
binlog
中 - 从库读取主库的二进制文件
binlog
,写入到从库的中继日志replay log - slave重做中继日志中的事件,将改变反映他自己的数据
你们项目用过分库分表吗?
什么情况下需要进行分库分表?
- 前提,项目业务逐渐增多,或者业务发展比较迅速。阿里巴巴规约中单表行数超过500万条或者单表容量超过2G才推荐进行分库分表
- 优化已经解决不了性能问题(主从读写分离、查询索引…)
- IO瓶颈(磁盘IO、网络IO)、CPU瓶颈(聚合查询、连接数太多)
拆分策略
垂直拆分
- 垂直分库
- 垂直分表
水平拆分
- 水平分库
- 水平分表
垂直分库:以表为依据,根据业务将不同表拆分到不同库中。(用户、商品、订单)
特点:
- 按业务对数据分级管理、维护、监控和扩展
- 在高并发下,提高磁盘IO和数据量连接数
垂直分表:以字段为依据,根据字段属性讲不同字段拆分到不同表中
拆分规则:
- 把不常用的字段单独放在一张表
- 把text、blob等大字段拆分出来放在附表中
特点:
- 冷热数据分离【热数据经常被查询,冷数据不常被查询】
- 减少IO过度争抢,两表互不影响
水平分库:将一个库的数据拆分到多个库中
路由规则:
- 根据ID节点取模
- 按ID也就是范围路由,节点1(1-100万),节点2(100万到200万)
特点:
- 解决了单库大数据量,高并发的性能瓶颈问题
- 提高了系统的稳定性和可用性
水平分表:将一个表的数据拆分到多个表中(可以在同一个库内)
特点:
- 优化单一表数据量过大而产生的性能瓶颈问题
- 避免IO争抢并减少锁表的几率
分库之后的问题:
- 分布式事务一致性问题?
- 跨节点关联查询
- 跨节点进行分页、排序函数
- 主键避重【ID重复问题】
添加中间件用于分库分表
- sharding-sphere
- mycat
MySQL中如何定位慢查询
慢查询的原因:
- 聚合查询
- 多表查询
- 表数据量过大查询
- 深度分页查询
表象:页面加载过慢、接口压测响应时间过长(超过1s)
解决方法1:开源工具
- 调试工具Arthas
- 运维工具:Prometheus、Skywalking
解决方法2:MySQL自带的慢日志查询
需要开启慢日志查询
1、介绍当时产生问题的场景(我们当时的接口测试的时候非常慢,压测的结果大概为5秒钟)
2、我们系统中采用了运维工具Skywalking,可以检测出哪个接口,最终是因为sql问题
3、在MySQL中开启了慢日志查询,我们设置的值就是2秒,一旦sql执行时间超过2秒就会被记录到日志中(调试阶段)生产环境中如果开启慢日志查询则会有一定的性能损耗。
一条SQL执行的很慢,是如何进行分析的呢?
使用MySQL的SQL执行计划来分析具体执行慢的原因
可以使用EXPLAIN
或者DESC
命令进行分析
重要的字段如下所示:
- possible_key:当前sql可能会使用到的索引
- key:当前sql实际命中的索引
- key_len:索引占用的大小
- Extra:额外的优化建议
- Using where; Using index:查找使用了索引,需要的数据都在索引列中能找到,不需要回表查询数据
- Using index condition:查找使用了索引,但是需要回表查询数据
- type:这条sql的连接类型,性能由好到差为NULL、system、const、eq_ref、ref、range、index、all
- system:查询系统中的表
- const:根据主键索引查询
- eq_ref:主键索引查询或唯一索引查询【只能返回一条数据】
- ref:索引查询【查询出来的数据可能是多条】
- range:返回查询
- index:索引树扫描【需要进行优化】
- all:全盘扫描【需要进行优化】
采用mysql自带的分析工具EXPLAIN
- 通过key和key_len检查是否命中了索引(索引本身存在是否有失效的情况)
- 通过type字段查看sql是否有进一步的优化空间,是否存在全索引扫描或者全盘扫描
- 通过extra建议判断是否出现了回表的情况,如果出现了,可以尝试添加索引或者修改返回字段来修复
怎么看项目里慢查询语句有哪些?
- MySQL慢查询日志
- 在MySQL的配置我呢间中开启慢查询日志功能,设置合适的阈值,MySQL将会自动记录执行时间,超过阈值的查询语句将会被记录到慢查询日志文件中,可以通过查询慢查询日志找出慢查询语句。
- show variables like ‘%slow_query_log%’;
- explain + 慢的语句\G
- IO
- CPU
- 网络带宽
怎么优化慢sql?举例说明
- 索引优化
- 查询优化
- 优化sql查询语句的写法,尽量避免全表扫描和不必要的数据读取。可以通过添加where条件,合理使用join操作,减少子查询的方式来优化查询,另外避免使用select * 查询所有列
- 分页查询优化
- 适当调整
innodb_buffer_pool_size
、sort_buffer_size
等内存参数,避免过度使用临时表和磁盘文件。 - 使用数据库集群或者读写分离
- 对于一些大查询可以从 从库中查询,减轻主库的压力。
MySQL的mylsam和innoDB存储引擎有什么区别?怎么选择?
在MySQL中可以使用show engines;
命令查看存储引擎,由于我本人使用的是MySQL8的版本,因此默认的存储引擎是InnoDB
,InnoDB
有四个特点:
- 支持事务,事务是可以在一系列操作中保证数据的一致性
- 行级锁,
InnoDB
使用行级锁,而MyISAM
使用表级锁定。行级锁定只锁定需要修改的行,可以提高并发性能,而表级锁定需要锁定整个表,可以导致锁定冲突。 - 支持外键,
InnoDB
支持外键,可以确保数据的完整性和一致性,而MyISAM
不支持外键。 - 崩溃异常恢复。
- ACID支持:
InnoDB
遵循ACID(原子性、一致性、隔离性、持久性)的原则,可以确保数据的完整性和一致性,而MyISAM
不支持。- 原子性Atomicity:事务中的所有操作要么全部成功要么全部执行失败,事务是一个不可分割的最小执行单元,要么全部完成,要么全部不完成
- 一致性Consistency:一致性是事务执行的结果必须使数据库从一个一致性状态转变到另一个一致性状态。即事务执行前后数据库的完整性约束没有被破坏。
- 隔离性Isolation:隔离性要求事务的执行不受其他事务的影响,即事务之间应该相互隔离。多个事务并发执行时,每个事务都感觉不到其他事务的存在,互相之间不会产生影响。
- 持久性Durability:持久性是指事务一旦成功提交,所有的修改都会永久保存在数据库胡总,及时数据库发生故障,事务的提交结果也不会丢失。
而myISAM
的话不支持事务,并且myISAM
使用的是表级锁。行级锁可以提高并发性,减少锁冲突的可能性
myISAM
不支持外键约束
对于崩溃恢复的话,InnoDB
也可以给到一个很好的支持,具有很好的可靠性和稳定性,而myISAM
对于这方面的支持是比较弱的。
综上所述,选择MyISAM还是InnoDB存储引擎取决于项目的具体需求和场景。如果项目需要支持事务、外键约束,或者有大量的并发写入操作,推荐选择InnoDB存储引擎。如果项目只需要简单的数据存储和查询,并且对事务和并发性要求不高,可以考虑使用MyISAM存储引擎。
MyISAM在处理大量数据的时候比INnoDB更快,但是在处理大量并发的时候INnoDB比MyISAM更快。
RabbitMQ消息队列和Redis消息队列有什么区别?项目里应该怎么选型?
- 数据模型的差异
- RabbitMQ是基于AMQP协议,核心理念是买你想消息的队列,数据存储是以消息为主
- Redis则是基于键值对的数据结构存储系统,虽然也可以用List模拟队列,但是更适合用于当做缓存
- 消息传递方式
- RabbitMQ采用了AMQP协议中的push模式,生产者直接将消息推送到消息队列中
- Redis则需要消费者主动pull模式,消费这需要不断地从Redis中读取消息队列数据
- 消息持久性
- RabbitMQ支持消息的持久化,保证了消息的可靠传输
- Redis虽然可以定期将数据持久化到磁盘,但这仍有丢失数据的风险
- 集群模式:
- RabbitMQ支持集群模式,可以实现高可用和负载均衡
- Redis也支持主从集群,但主节点写数据存在单点风险
- 处理能力
- RabbitMQ具有较好的路由、消息分发、事件传播等消息队列功能
- Redis较适合处理较小的消息,对大消息数据的处理能力较差
应用场景:
- 可靠性传输场景,例如订单处理、消息推送等,建议使用RabbitMQ
- 高读写性能场景,如分布式session、分布式锁等,建议使用Redis
- 如果对消息队列的需求较轻,Redis也是一种可选方案
MySQL三大日志
- 错误日志
- 查询日志
- 慢查询日志
- 事务日志
- 二进制日志
binlog
(归档日志)
重要的:
binlog
(归档日志)redo log
(事务日志)undo log
(回滚日志)
MySQL基础架构
MySQL存储引擎
想要查看MySQL存储引擎,可以使用show engines;
命令查看
可以看出MySQL默认的存储引擎是Innodb,Innodb支持事务、行级锁以及外键
- 事务:
- 行级锁:同一时间可以有多个用户同时对不同的行进行读写操作,而不会互相影响。这有助于提高数据库的并发性能,多个用户可以同时访问数据库而不发生冲突。
- 外键:
- 崩溃恢复:
MySQL存储引擎架构采用的是插件式架构,支持多种存储引擎。
设计MySQL的表结构要考虑什么问题?
- 主键设计要合理
- 优先考虑逻辑删除,而不是物理删除
- 一张表的字段不宜过多
- 尽可能使用not null定义字段
- 设计表时,评估那些字段需要加索引
MySQL的char和varchar有什么区别?
- 存储方式
- char是固定长度的字符串,它需要占用固定数量的存储空间,长度如果不足的地方会使用空格进行填充
- varchar是可变长度的字符串,它根据实际存储的数据的长度来分配存储空间,不会浪费额外的空间
- 性能
- char对于较短的字符串可能会浪费存储空间,由于是固定长度的存储方式,插入和检索数据时速度较快
- varchar是可变长度的存储方式,存储效率高,但是在插入和检索数据的时候稍慢
- 适用场景:
- char用于固定长度的字符串,例如电话号码、国家代码等
- varchar用于长度不固定的字符串,例如用户名、地址等
sql语句
Redis
Redis有哪些数据类型
- String:底层原理SDS 简单动态字符串
- List:底层原理双向链表+压缩链表
- set:底层整数数组和哈希表
- hash:压缩列表和哈希表
- zset:压缩列表和跳表
Redis中Hash底层结构
Redis中String底层结构
Redis是C语言编写的,但是Redis的String类型并不是C语言中的字符数组,而是自己编写的SDS(Simple Dynamic String,简单动态字符串)来作为底层实现。
Zset底层原理
压缩列表
什么时候使用压缩列表呢?
- 有序集合保存的元素数量小于128个
- 有序集合保存的所有元素的长度小于64字节
除此之外就是用跳表
What Why How
什么是跳表?
跳表就是在链表的基础上添加多级索引构成的一种数据结构
为什么用跳表?
当数据量特别大的时候,其查找元素的时间复杂度为O(logN)
,其查找的思想类似于二分法,其插入删除的时间开销也是O(logN)
为什么不采用树形结构而采用跳表结构?
- Zset有一个核心操作是范围查找,跳表很方便的就可以进行这个操作,但是红黑树不可以
- 跳表的时间比红黑树或者平衡二叉树简单
Redis为什么不用b+树?MySQL为什么不用跳表?
这个问题在于 Redis是直接操作内存的并不需要磁盘io而MySQL需要去读取io,所以mysql要使用b+树的方式减少磁盘io,B+树的原理是 叶子节点存储数据,非叶子节点存储索引,每次读取磁盘页时就会读取一整个节点,每个叶子节点还有指向前后节点的指针,为的是最大限度的降低磁盘的IO;因为数据在内存中读取耗费的时间是从磁盘的IO读取的百万分之一
而Redis是 内存中读取数据,不涉及IO,因此使用了跳表,跳表明显是更快更简单的方式。
Redis使用场景
需要结合业务进行回答
缓存用户基本的信息
JWT缓存
缓存穿透
缓存穿透:是指查询一条数据,缓存中不存在,并且在MySQL中也查询不到该条数据,导致数据不会直接写入缓存,每次请求都会到数据库
例如一个请求api/news/getById/1
解决方案1:缓存空值
如果查询的这个对象在数据库中是不存在的,则会在缓存中缓存空值,{key: 1, value: null}
优点:简单
缺点:消耗内存,可能会发生数据不一致的情况
解决方案2:布隆过滤器
在查询数据的时候,首先在布隆过滤器中进行判断该数据是否存在于缓存中,如果存在则从缓存中进行查询,如果不存在直接拦截请求进行返回。
在进行缓存预热的时候就需要将布隆过滤器预热了。
布隆过滤器
bitmap(位图):相当于是一个以(bit)位为单位的数组,数组中每个单位只能存储二进制0和1
布隆过滤器可以检索一个元素是否在一个集合中
首先初始化的时候会创建一个全为0的数组,流程如下:
- 存储数据的时候,id为1的数据,通过多个hash函数获取哈希值,根据哈希值计算出数组对应的位置全部修改为1
- 查询数据的时候,会根据相同的hash函数计算出hash值,然后判断对应的位置是否都为1,如果为都为1则说明缓存中存在,否则不存在。
缺点:布隆过滤器可能会存在一定概率的误判,即一个数据计算hash值在布隆过滤器中均为1但是实际上并不存在,产生了hash碰撞
误判率:数组越小误判率越高,数组越大误判率越小,但是带来了更多的内存消耗
缓存雪崩
缓存雪崩是指同一时间段内,有大量的缓存key失效或者Redis服务器宕机,导致大量请求到达数据库中,带来了很大压力
解决方案1:给不同key的TTL添加随机值
解决方案2:利用Redis集群提高服的可用性
- 哨兵模式
- 集群模式
解决方案3:给缓存业务添加降级限流策略
- Nginx
- Spring Cloud Gateway
解决方案4:给业务添加多级缓存
- Guava
- Caffeine
缓存击穿
给key设置了过期时间,当key过期的时候,恰好这个时间点对这个key有大量的并发请求过来,这些并发请求可能会瞬间把DB击垮。
解决方案1:互斥锁
能够保证数据的强一致性,但是性能较差
解决方案2:设置逻辑过期时间
能够保证高可用,性能是比较好的
两种方案都是可以的,需要根据具体的适用场景来进行选择,比如金融行业是需要保证数据的强一致性的选择方案1,互联网行业需要保证用户体验选择方案2
3中常用的缓存读写策略
旁路缓存模式Cache Aside Pattern
比较适合请求较多的场景,服务端需要同时维系db
和cache
,并且是以db
的结果为准。
写:先更新db
;然后直接删除缓存
读:从cache中读取数据,如果读取到就返回;缓存中没有数据的话,就从db中读取返回,再将数据放到缓存中
如果先删除缓存再更新数据库存在数据不一致问题?
线程A写入数据,线程B随后读取数据A
线程A先把缓存中的数据删除了,线程B从数据库中获取到了旧的数据,然后线程A才将数据库中的数据更新为最新值,就会产生数据不一致的问题
先更新数据库再删除缓存也存在一定的问题
线程A请求数据data,线程B随后写入数据data,并且数据data在线程A请求之前不在缓存中的话,也有可能产生数据一致性的问题。
理解:线程A读取缓存中没有数据,然后从db中获取数据data,线程B此时写入db数据,然后此时发现缓存中没有数据不需要进行删除,此时线程A再将读取到的旧数据更新到缓存中,则数据库和缓存中的数据就会不一致了【缓存中为旧数据,数据库中为新数据】
双写一致性
当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库中的数据要保持一致
- 读数据:缓存命中,直接返回;缓存未命中,写入缓存,设定超时时间
- 写数据:延迟双删
- 删除缓存
- 操作数据库
- 延时删除缓存
共享锁:读锁readLock,加锁之后,其他线程可以共享读操作,但是不能进行写操作
排他锁:独占锁,writeLock,加锁之后,阻塞其他线程的读写操作
读写锁在业务必须保持强一致的情况下才会进行使用。
允许短暂的不一致
能够保证最终的一致性,就是使用了消息队列的服务,主要取决于MQ的可靠性
Redis的持久化方式有哪些?
Redis的读写操作都是在内存中,所以Redis的性能才会高,但是当Redis重启之后,内存中的数据就会丢失。因此为了保证内存中的数据不会丢失,Redis实现了数据持久化的机制,这个机制会把数据存储到磁盘,这样在Redis重启之后,就能够从磁盘中恢复原有的数据了。
Redis的持久化方式有两种:
- 将某一时刻的内存中的所有数据,以二进制的方式写入磁盘
- 使用
bgsave
来fork一个子进程执行RDB,避免主进程受到影响 - save是主进程来执行RDB,会阻塞所有命令
RDB触发规则:
手动触发
- save
- bgsave
自动触发
配置触发
在Redis安装目录下的redis.conf配置文件中搜索/snapshot即可快速定位,配置文件默认注释了下面三行的数据,通过配置规则来触发RDB的持久化,需要开启或这根据自己的需求按照规则进行配置。
#save 900 1 #save 300 10 #save 60 10000
shutdown触发
flushall触发
RDB优点:
- 性能高:RDB持久化是通过生成一个快照文件来保存数据,因此在恢复数据时速度非常快。
- 文件紧凑:RDB文件是二进制格式的数据库文件,相较于AOF文件来说,文件体积比较小。
RDB缺点:
可能会丢失数据:由于RDB是定期生成的快照文件,如果Redis意外宕机,最近一次的修改可能会丢失。
AOF日志(Append Only File)追加文件
- 每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里,看作是命令日志文件;
- AOF默认是关闭的,需要手动在
redis.conf
配置文件中将appendonly的值No改为Yes
文件记录的频率有三种:
- always:同步刷盘;可靠性最高,几乎不丢数据;性能影响大
- everysec:每秒刷盘;性能适中;最多丢失一秒钟的数据
- no:操作系统控制;性能最好;可靠性差,可能会丢失大量数据
因为是记录文件,AOF文件会比RDB文件大得多。而AOF文件会记录对同一个key的多次写操作,但实际上只有最后一次写操作才有意义。通过BGREWRITEAOF
命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。
Redis持久化默认开启为RDB持久化
AOF日志记录的是操作命令,不是实际的数据,所以用AOF方法做故障恢复时,需要把日志都执行一遍,一旦AOF日志非常多,势必会造成Redis的恢复操作缓慢。
为了解决这个问题,Redis增加了RDB快照。所谓的快照,就是记录某一个瞬间的东西,比如当我们给风景拍照的时候,那一个瞬间的画面和信息就记录到了一张照片。
因此在Redis恢复数据的时候,RDB恢复数据的效率会比AOF高一些,因为直接将RDB文件读入内存就可以,不需要像AOF那样还需要额外执行操作命令的步骤才能恢复数据。
RDB的执行原理
bgsave
开始时会fork主进程得到子进程,子进程共享主进程的内存数据,完成fork后读取内存数据并写入RDB文件。
Redis数据过期策略
Redis对数据设置数据的有效时间,数据过期以后,就需要将数据从内存中删除掉。可以按照不同的规则进行删除,这种删除规则就被称之为数据的删除策略(数据过期策略)
每隔一段时间,就会对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数据量的随机key进行检查,并删除其中的过期key)
slow模式:定时任务,执行频率默认为10Hz,每次不超过25ms,以通过修改配置文件redis.conf
的hz选项来调整这个次数
fast模式:执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms
Redis数据淘汰策略
默认的淘汰策略是:noeviction
,不淘汰任何key,当内存写满时不允许写入新数据,直接报错
LRU:最近最少使用
LFU:最少使用频率
平时开发中使用较多的是allkeys-lru(结合自己的业务场景)
内存用完了会看数据的淘汰策略是什么?如果是默认的配置就会直接报错
Redis分布式锁
如何合理的控制锁的有效时长?
- 根据业务执行时间预估
- 给锁续期
开一个线程进行监控如果业务还没有完成但是锁要过期的话进行续期
redisson实现分布式锁的执行流程
看门狗watch dog:
一个线程一旦获取锁成功以后,看门狗就会给持有锁的线程续期(默认是10秒续期一次)
线程2循环到一定次数如果失败也会直接结束获取锁失败,一般情况业务执行时间是非常快的,增加这个等待重试机制增加了高并发下的性能
Redis分布式锁可重入
如果是同一个线程是可重入的,如果是不同线程则会互斥。
在Redis进行存储的时候采用的是hash结构来存储线程信息和重入的次数
Redis分布式锁-主从一致性
redisson锁不能解决主从一致性问题,但是可以已使用其提供的红锁进行解决(红锁需要多个线程同时持有锁),但这样的性能就大大降低了,如果业务中非要保证数据的强一致性,可以采用zookeeper实现分布式锁。
Redis是单线程还是多线程的?
Redis单线程是指【接收客户端请求->请求解析->进行数据读写等操作->发送数据给客户端】这个过程是由一个线程(主线程)来完成的,这也是我们常说Redis是单线程的原因。
但是Redis程序并不是单线程的,Redis在启动的时候,是会启动后台线程BIO的:
Redis大key会有什么问题?怎么解决?
四种问题:
- 客户端超时阻塞
- 引发网络阻塞
- 阻塞工作线程
- 内存分布不均
解决方法
- 拆分成多个小key,降低单key大小,读取可以用mget批量读取
- 设置合理的过期时间。
- 启用内存淘汰策略。
- 数据分片
- 删除大Key
Redis为什么快?
- 基于内存,内存的访问速度比磁盘快很多
- Redis内置了很多优化过后的数据结构
- 单线程事件循环和 IO 多路复用
redis实现分布式锁,以及setnx可能存在的问题
计算机网络
OSI七层模型
- 物理层
+ - 数据链路层
+ - 网络层
+ - 传输层
- TCP/IP
- 会话层
+ - 表示层
+ - 应用层
- HTTP
- Telnet
- SMTP
- POP3/IMAP
- FTP
- ssh
- DNS
- WebSocket
- RTP
TCP/IP四层网络
应用层:常见的一些应用协议,例如HTTP、HTTPS、DNS、FTP等等
传输层:TCP/IP
网络层:路由和交换
网络接口层:
浏览网页的全过程
- 浏览器输入指定网页的URL,例如meituan.com
- 浏览器通过DNS协议,获取域名对应的IP地址
- DNS流程:首先获取浏览器缓存
- 缓存中没有去host文件
- 然后看网卡配置信息中可以是自动获取DNS服务器或者指定
- 我一般会指定谷歌的8.8.8.8DNS服务器或者阿里云的223.5.5.5、223.6.6.6或者腾讯的119.29.29.29
- 紧接着浏览器根据IP地址和端口号,向目标服务器发送一个TCP连接请求
- 浏览器在TCP连接上,向服务器发送一个HTTP请求报文,请求获取网页内容,
- 服务器收到http请求报文后,服务器收到 HTTP 请求报文后,处理请求,并返回 HTTP 响应报文给浏览器。
- 浏览器收到http响应报文后,解析响应体中的html代码,同时根据html中其他资源的url,再次发起http请求,获取这些资源,直到网页加载显示。
- 浏览器在不需要和服务器通信的时候,可以主动关闭TCP连接或者等待服务器关闭连接。
介绍下TCP和UDP的区别
- 连接性:
- TCP 是面向连接的协议,通信双方在传输数据之前需要先建立连接,然后进行可靠的数据传输,最后再释放连接;而 UDP 是无连接的协议,通信双方之间直接发送数据包,不需要建立连接,也不保证数据传输的可靠性。
- 数据可靠性:
- TCP 提供可靠的数据传输服务,通过序号、确认和重传机制来保证数据的可靠性,确保数据不会丢失、损坏或者乱序;而 UDP 不提供数据传输的可靠性保证,发送方发送的数据包有可能丢失、重复或者乱序。
- 通信效率:
- TCP 的通信效率相对较低,因为它需要建立连接、维护状态、进行数据确认和重传等操作,而且因为可靠性保证的需要可能引入较大的延迟;而 UDP 的通信效率相对较高,因为它是无连接的,不需要进行连接建立和状态维护,也不需要数据确认和重传操作。
- 适用场景:
- TCP 适用于对数据传输可靠性要求较高的场景,如网页浏览、文件传输、电子邮件等应用;而 UDP 适用于对实时性要求较高、但数据传输可靠性要求较低的场景,如音频、视频、在线游戏等应用。
总的来说,TCP 提供了可靠的、面向连接的数据传输服务,适用于对数据完整性和顺序性要求较高的场景;而 UDP 提供了高效的、无连接的数据传输服务,适用于对实时性要求较高、但数据传输可靠性要求较低的场景。选择 TCP 还是 UDP 取决于具体的应用需求和场景。
GET请求和POST请求的区别
根据restful api的规范,GET的语义是从服务器获取指定的资源,这个资源可以是静态的文本、页面、图片视频等
- 语义:GET通常用于获取或者查询资源,而POST通常用于创建或者修改资源
- 幂等:GET请求是幂等的,而POST请求是非幂等的
- 格式:
- 缓存:GET请求通常可以被浏览器缓存或者预加载,以提高网页加载速度,因为GET请求具有幂等性,所以可以安全地进行缓存
- 安全性:GET请求的参数都在URL链接中,不安全;POST请求参数在请求体中,相对安全
HTTP和HTTPS请求的区别
- HTTP是超文本传输协议,信息是明文传输的,存在安全风险的问题;HTTPS则解决HTTP不安全的缺陷,在TCP和HTTP网络层之间加入了SSL和TLS安全协议,使得报文能够安全传输;
- HTTP连接建立相对简单,TCP进行三次握手之后就可以进行HTTP报文传输了。而HTTPS在TCP三次握手之后,还需要进行SSL/TLS的握手过程,才可以进行加密传输;
- 两者默认的端口是不同的,HTTP默认端口是80,HTTPS的默认端口是443;
- HTTPS协议需要向CA机构申请数字证书,来确保服务器的身份是可信的。
HTTPS中采取的加密算法有哪些?
- 非对称加密
非对称加密中有公钥和私钥两个密钥,其中公钥用来加密,私钥用来解密,具体来说客户端使用公钥来对数据加密,服务器端采用私钥进行加密。
代价较高;效率较低
- 对称加密
对称加密算法的特点是加密和解密采用的是相同的密钥,HTTPS协议中,客户端和服务器端需要才通信开始时协商一个对称密钥,之后就使用该密码进行加密解密。
使用非对称加密对对称加密的密钥进行加密,保护密钥不被窃取
TCP的拥塞控制是如何实现的
- 慢开始
- 拥塞避免
- 快重传
- 快恢复
- 慢启动(Slow Start): 当连接建立时,发送方会初始化一个拥塞窗口(Congestion Window,cwnd),初始值通常比较小。发送方会以指数增长的方式逐步增加拥塞窗口的大小,直到达到一个阈值(拥塞阈值)为止。在此过程中,发送方可以向网络发送的数据量逐渐增加,以便测试网络的容量。
- 拥塞避免(Congestion Avoidance): 一旦拥塞窗口的大小达到了拥塞阈值,TCP 就会进入拥塞避免阶段。在拥塞避免阶段,发送方以线性增长的方式逐步增加拥塞窗口的大小,而不是指数增长。这样可以更加谨慎地控制发送速率,以避免引起网络拥塞。
- 快重传(Fast Retransmit): 当发送方发送的数据丢失或者超时未收到确认时,接收方会触发快重传机制,立即重传丢失的数据包,而不必等待超时。这样可以更快地发现丢失的数据包并尽快进行重传,以减少网络拥塞的可能性。
- 快恢复(Fast Recovery): 在快重传后,发送方不会立即将拥塞窗口的大小减半,而是将拥塞窗口的大小设置为拥塞阈值的一半,并继续使用拥塞避免算法进行调整。这样可以更快地恢复发送速率,并尽可能避免网络拥塞。
HTTP各个版本的区别和改进
HTTP1.1
相较于HTTP1.0
性能上的改进:
- 使用长连接的方式改善了HTTP1.0短连接带来的性能开销
- 状态响应码的增加,错误响应码新增了24种
- 缓存机制
- 带宽
- Host头:请求头中会加入Host字段,告诉服务器所要访问的地址
HTTP1.1
和HTTP2.0
的区别:
- 多路复用:同一个连接上可以同时传输多个请求和响应(HTTP1.1长连接的升级版本)
- 二进制帧:减少传输的数据量和带宽消耗
- 头部压缩:1.1支持Body压缩,不支持Header压缩;而2.0支持Header压缩,专门为Header压缩设计了HPACK算法,减少网络开销
- 服务器推送:可以在客户端请求一个资源时,将其他相关资源一并推送给客户端
HTTP2.0
和HTTP3.0
的区别:
- 传输协议:3.0新增了QUIC
- 建立连接:2.0需要经过三次握手,3.0中QUIC协议的特性建立连接仅需要0-RTT或者1-RTT,这意味着QUIC在最佳的情况下不需要额外的往返时间就可以建立连接
- 队头阻塞:2.0多请求复用一个TCP连接,一旦发生丢包,就会阻塞所有的HTTP请求,QUIC协议一个连接建立多个不同的数据流,这些流之间独立互不影响,某个数据流发生丢包,其数据流不受影响(本质上是多路复用+轮询)
- 错误恢复:3.0具有更好的错误恢复机制,当出现丢包、延迟等网络问题时,可以更快地进行恢复和重传,而HTTP2.0需要依赖TCP的错误恢复机制。
- 安全性:HTTP2.0使用TLS协议进行加密,而HTTP3.0基于QUIC协议,包含了内置的机密和身份验证机制,可以提供更强的安全性
HTTP的常见状态码
1xx:提示信息,是协议处理的中间状态,实际开发中使用的比较少
2xx:成功,报文已经被收到并被正确处理,例如200 OK
3xx:重定向,资源位置发生了变化,需要客户端重新发送请求;例如301永久重定向,资源已经不存在了;302临时重定向
4xx:客户端错误,请求报文有误,服务器无法处理;
5xx:服务器内部错误,常见500,502网关错误和503服务器繁忙,稍后重试
三次握手和四次挥手
- 客户端向服务器端发送建立连接的请求,进入到syn_send状态
- 服务器端处于监听状态,收到请求后便处于syn_rcvd状态
- 客户端再给服务器端发送数据包,告知收到
第三次握手需要让服务器端确保客户端的接收功能也是正常的
TCP通过四次挥手来保证数据完整传输的
四次挥手需要确保双方后面都不会再发送数据的情况下再进行断开连接
过程:
- 客户端给服务器端发送一个带有
FIN
结束标识和seq=1
的断开连接的请求包,主要目的是告诉客户端我这边不再发送数据包了,但是可以接收数据。 - 服务器端会给客户端发送一个
ack
包,表示自己已经知道客户端不会再向自己发送数据了 - 服务端给客户端发送一个断开连接的请求包,告知客户端自己这边也不会发送数据了
- 客户端给服务器端做出最后的响应,当服务端收到响应之后就会断开连接并释放资源;客户端这边需要等待一段时间再去断开连接,等待的时间是两倍的请求时间。
为什么不能将服务器端的两次请求合并成一个?
第二次挥手和第三次挥手之间是有一定的延迟性的,延迟可能是几秒、几十秒甚至几分钟
第二次挥手直接进行一次确认可以保证不会触发TCP的超时重传机制,减少额外的开销
为什么不能三次挥手:
服务器端收到客户端断开连接的请求之后,可能还有一些数据没有发送完毕,这时候先回复ACK,表示收到了断开连接的请求,等到全部数据接收完毕再发送FIN,断开服务器端和客户端之间的连接。
Spring
单例bean是线程安全的吗?
不是线程安全的
Spring框架中有一个@Scope注解,默认的值就是singleton,单例的。
因为一般在spring的bean中注入的都是无状态的对象,没有线程安全问题,如果在bean中定义了可修改的成员变量,是要考虑线程安全问题的,可以使用多例或者加锁来解决。【是否有状态就看当前的对象是否能被修改】
Spring bean并没有可变的状态(比如Service类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。
Springboot启动流程
IOC容器初始化流程
Bean生命周期
Bean循环依赖
SpringMVC执行流程
什么是AOP,你们项目中有没有使用到AOP
对AOP的理解,有没有真的使用过AOP
AOP被称为面向切片编程,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”,减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。
AOP的底层使用的是动态代理技术
常见AOP使用场景:
- 记录操作日志
使用aop提供的环绕通知,@Around(“pointcut”)
- 缓存处理
- Spring中内置的事务处理
操作日志处理的思路:
提供一个切面类,定义一个环绕通知:
核心是:使用AOP中的环绕通知+切点表达式(要找到记录日志的方法),通过环绕通知的参数获取请求方法的参数(类、方法、注解、请求方式等),获取到这些参数以后,保存到数据库。
谈谈你对AOP的理解?
谈谈你对IOC的理解
Spring事务是如何实现的
Spring支持编程式事务和声明式事务管理两种方式。
- 编程式事务控制:需使用TransactionTemplate来进行实现,对业务代码有侵入性,项目中很少使用
- 声明式事务管理:声明式事务管理建立在AOP之上的,其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编制到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或回滚事务。
Spring中事务失效的场景有哪些?
解决方法是在catch块中添加throw new RuntimeException异常
Spring的bean的生命周期
Spring-bean的循环依赖(循环引用)
SpringMVC启动流程
Springboot-自动配置原理
Spring框架常见注解
Springboot启动原理
设计模式
单例模式
Java中单例模式可以通过多种方式实现,其中比较常见的包括懒汉式和饿汉式。
- 懒汉式单例模式:在需要的时候才创建实例
- 饿汉式单例模式:在类加载的时候就创建实例
单例模式确保类只有一个实例,并提供全局访问点。通常用于管理全局资源,如日志记录、数据库连接池等。
工厂设计模式
开闭原则:对扩展开放,对修改关闭
通过个工厂类来实现对应对象的创建,只需要传递对应的对象名即可
抽象工厂:提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
具体工厂:主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
抽象产品:定义了产品的规范,描述了产品的主要特性和功能。
具体产品:实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
优点:
- 用户只需要知道具体工厂的名称就可以得到所要的产品,无需知道产品的具体创建过程;
- 在系统增加新的产品时,只需要添加具体产品类和对应的具体工厂类,无需对原工厂进行任何修改,满足开闭原则;
缺点:每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可以生产多个产品等级的产品。
优点:当一个产品族中的多个对象被设计成一起工作时,他能保证客户端始终只使用同一个产品族中的对象。
缺点:当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改
工厂模式最大的目的就是为了解耦,Spring底层就大量用到了工厂模式
策略模式
该模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响使用算法的客户
它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不冉的对象对这些算法进行管理。
举例:
登录案例(工厂模式+策略模式)
例如gitee
的登录方式,有很多中(账号、密码、QQ、微博、微信、钉钉等)
责任链模式
装饰器模式
在不改变原有类功能的基础上,动态增加一些额外的功能
定义一个简单的人类,刚开始只会走路,经过装饰之后,添加了一些技能,如骑车、开车等。
Linux
常见的负载均衡算法有哪些?
- 轮询
- 轮询
- 加权轮询
- 随机
- 加权随机
- 最少连接
- 一致性哈希
- 客户端发送请求到Nginx服务器
- Nginx服务器收到请求后,使用一定的负载均衡算法(如轮询、IP哈希、最少连接数等)
- Nginx将请求转发给选中后的后端服务器
- 后端服务器处理请求并返回响应给Nginx
- Nginx将响应返回给客户端
常见技术场景
单点登录这块是如何实现的?
单点登录叫做SSO(Single Sign On),只需要登录一次,就可以访问所有信任的应用系统
- 解释什么是单点登录
- 介绍自己项目中涉及到的单点登录(即使没涉及也要说明思路)
- 介绍单点登录的解决方案,以JWT为例
- 用户访问其他系统,会在网关判断token是否有效
- 如果token无效则会返回401(认证失败)前端跳转到登录页面
- 用户发送登录请求,返回浏览器一个Token,浏览器把token保存到cookie
- 再去访问其他服务的时候都需要携带token,由网关统一验证后路由到目标服务
权限认证是如何实现的?
后台管理系统,更注重权限控制,最常见的就是RBAC模型来指导实现权限
RBAC(Role-Based Access Control)基于角色的访问控制
- 3个基础部分组成:用户、角色、权限
- 具体实现
- 5张表(用户表、角色表、权限表、用户角色中间表、角色权限中间表)
- 7张表(用户表、角色表、权限表、菜单表、用户角色中间表、角色权限中间表、权限菜单中间表)
张三登录系统—->查询张三拥有的角色列表—->在根据角色查询拥有的权限
结合权限框架:
- Apache shiro
- Spring Security【推荐】
上传数据的安全性你们怎么控制?
使用非对称加密(或对称加密),给前端一个公钥让他把数据加密后传到后台,后台负责解密后处理数据。
你负责项目的时候遇到哪些棘手的问题?怎么解决?
- 设计模式?
- 登录功能【使用工厂模式和策略模式】解决了增加登录方式所导致的经常修改代码的问题
- 线上bug
- 调优
- 组件封装
生产环境中问题如何排查
- 先分析日志
- 远程debug
消息队列中间件
削峰
解耦
异步
技术选型
RabbitMQ如何保证消息不丢失?
适用场景:
- 异步发送(验证码、短信、邮件)
- MySQL和Redis,ES之间的数据同步
- 分布式事务
- 削峰填谷
消息可能丢失的环节:
各个环节都有可能存在消息丢失的可能。
生产者发送消息没有到达交换机或者没有到达队列
MQ宕机消息丢失
消费者服务宕机消息丢失
生产者确认机制
RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否发送成功。
nack publish-confirm
ack publish-return
消息失败之后如何处理?
- 回调方法即时重发
- 记录日志
- 保存到数据库然后定时重发,成功发送后即刻删除表中的数据【定时任务】
消息持久化
MQ默认是内存存储信息,开启持久化功能可以确保缓存在MQ中消息不丢失
- 交换机持久化
- 队列持久化
- 消息持久化,SpringAMQP中的消息默认是持久的,可以通过MessageProperties中的DeliveryMode来制定
消费者确认
RabbitMQ支持消费者确认机制,即:消费者处理消息后向MQ发送ack回执,MQ收到ack回执之后才会删除该消息。SpringAMQP允许配置三种确认模式:
- manual:手动ack,需要在业务代码结束后,调用api发送ack
- auto:自动ack,由spring检测listener代码是否出现异常,如果没有异常则返回ack,抛出异常则返回nack【选择这个模式】
- none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除
可以利用Spring的retry机制,在消费者出现异常时利用本地重试,设置重试次数,当次数达到了以后,如果消息依然失败,将消息投递到异常交换机,交由人工处理。
RabbitMQ消息的重复问题是如何解决的?
消费者消费完毕需要给MQ发送确认
网络抖动
消费者挂了
解决方案:
每条消息设置一个唯一的标识id
幂等方案:【分布式锁、数据库锁(乐观锁、悲观锁)】
RabbitMQ中死信交换机(延迟队列了解过吗)
延迟队列:进入队列的消息会被延迟消费的队列
场景:超时订单、限时优惠【优惠剩余时间】、定时发布【抖音作品选择明天进行发布】
当一个队列中的消息满足下列情况之一,就可以称之为死信(dead letter):
消费者使用basic.reject或者basic.nack声明消费失败,并且消息的requeue参数设置为fasle
消息是一个过期消息,超时无人消费
要投递的队列消息堆积满了,最早的消息可能成为死信
如果该队列配置了dead-letter-exchange属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机就会被称为死信交换机(Dead Letter Exchange,简称DLX)
死信交换机也可以绑定搞一个死信队列
TTL,也就是Time-To-Live。如果一个队列中的消息TTL结束仍未消费,则会变为死信,ttl超时分为两种情况:
- 消息所在的队列设置了存活时间
- 消息本身设置了存活时间
延迟队列通过TTL+死信队列实现
如何解决消息堆积问题?
如果有100万消息堆积在MQ,如何解决?
解决消息堆积有三种思路:
- 增加更多消费者
- 在
什么是RabbitMQ
是一个开源的消息中间件,使用Erlang 语言开发。这种语言天然适用分布式场景。
RabbitMQ也非常适用于在分布式应用程序之间传递消息。RabbitMQ有非常多的显著的特点:
- 消息传递模式,包括发布/订阅模式、点对点和工作队列等,使其灵活运用于各种消息通信场景。
- 消息路由和交换机:RabbitMQ引入了Exchange的概念,用于将消息路由到一个或多个队列。这允许根据消息的内容、
RabbitMQ的原理
RabbitMQ的四大核心:
- 生产者
- 消费者
- 队列
- 交换机
AMQ协议是一种二进制的协议,它定义了一组规则和标准,以确保消息可以在不同的应用程序和平台之间传递消息。
生产者消费者模型
项目
项目1:博世实习
如何实现的单点登录?
简单介绍一下单点登录的原理
ThreadLocal在项目中有使用过吗?
有的,我们是使用ThreadLocal保存用户的状态信息,这样就可以随时随地获取到用户的信息了。实际项目流程是这样设计的:
- 首先请求到网关,我在网关这边设计了一个全局过滤器。
- 当他拦截下来请求后,将请求进行解析,从请求头中拿到用户的访问Token,这个Token进行解密后,转为JWT格式的Token,并校验签名是否正确以及Token是否过期,如果没有问题就获取到用户账号,并且将账号信息写到请求头中,转发给后面的业务服务。
ThreadLocal主要解决的问题是:
- 线程安全性,通过为每个线程提供独立的变量副本,避免了多线程之间的竞争和相互干扰,从而保证了数据的线程安全性
- 数据隔离性:
- 避免线程间传递参数:
方法
get()
set(value)
remove()
initialValue()
SpringSecurity的认证流程?
密码加密的过程
原有的md5加密【位数为32位】的方法 是一种信息摘要算法,他可以通过彩虹表和哈希碰撞来进行破解,网上也有很多的在线解密平台,可以直接将密码破解出来。因此我们密码加密采用的是BCryptPasswordEncoder(),这可以有效的防止彩虹表和碰撞攻击,这个方法每一次生成的密码都是不一致的,然后在进行校验的时候是调用match()方法,如果新旧密码是一致的,则返回true。
自动产生了一个随机盐加入到原密码中,这样如果a用户和b用户的密码一样,但是算出来的加密值是不一样的,并在在反运算的时候会自动将盐提取出来,然后得出正确的密码。
它的存储位数是【60】位字段
双Token如何实现无感刷新机制?
续约token在我们的项目中是有设计过的,恰好也是我做的。当时产品经理给到的需求是用户可以保持一个长期登录或者自动登录的效果,避免用户状态频繁的过期,给用户带来不便。
我当时就使用了双Token的方式进行设计,这个方案的提出后来经过mentor的评估,他也认为没有问题,于是我就这么做了。
我给您说一下大概的思路:
首先是登录,在登录的时候,无论任何方式认证,最后都是返回Token到前端。
在返回Token的时候,是生成两个Token:
- 一个是Access Token,我管他叫访问令牌。我出于安全考虑,比如防止令牌被恶意使用,给他设置的有效期为6 个小时,每次请求资源时携带这个令牌
- 另一个是Refresh Token,我管他叫刷新令牌,这个令牌不能用来访问资源,只能用来刷新访问令牌,就是每当访问令牌过期,前端携带这个Refresh Token获取新的Access Token,这个刷新Token的有效期我设置为7天,当然这个也时间的设置是需要根据项目的需求来的,也是写在配置文件中的。
RabbitMQ如何去实现
- 创建消息队列
- 在RabbitMQ中创建一个消息队列,用于存储要发送的异步消息
- 发送消息
- 在我们的应用中,如果员工在修改了自己的信息或者项目经理修改了员工所在项目状态的时候,就会将变更发布到消息队列当中,
- 消息消费者
- 我们编写了一个消息消费者的应用程序,该应用程序连接到RabbitMQ并监听特定的消息队列。一旦有新消息到达队列,消费者就会接收到消息并进行相应的逻辑处理
- 异步处理
如何让一个用户强制下线
在我们的项目中使用的是JWT,所以要在后台实现让用户强制下线的操作可以按照下面的步骤进行:
- 维护Token的黑名单
- 实现登出接口
- Token验证时候检查黑名单
- Token过期机制
- 可以通过将用户的token过期时间设置为当前时间,从而在用户下次进行需要进行权限操作的时候token失效而退出
项目2:智能简历解析系统
回答话术
我们采用Nginx作为前端项目的部署服务器,前端采用vue编写的,前端写完会使用webpack进行编译构建,把vue文件转为编译后的js、css、html之类的静态资源,然后把这些静态资源打包发布到nginx服务器。
部署的方式也很简单,将静态资源更新到nginx的html目录下,然后修改nginx配置文件,将root目录指定到项目路径,这样前端请求域名根路径下的静态资源时,直接在nginx端进行响应了。
前后端交互式怎么配合的?
登录时手机验证码做过限流吗?
有的,这个必须要做限流,我们是利用Redis的zset结合时间窗口限流算法进行实现的。
我是这样考虑的,用户有很多行为是无意义或者非法的,比如频繁发送短信、频繁修改个人信息等行为,都应该进行限流。
所以我就针对这些频繁的行为进行限流,设计了一个通用的限流接口。思路上是使用时间窗口限流算法,具体实现我是利用redis的zset实现的。
比如用户五分钟内只能发送三条验证码,或者10分钟内只能发送8个验证码,于是我就将用户的发短信行为设计为redis的key,格式是场景:行为:用户唯一标识,score分数值是时间戳,value也是时间戳。
具体流程为:
- 用户每次发生限流行为,都会记录这个行为以Redis的zset的方式进行记录
- 在业务处理流程中,使用java api进行查询判断,其本质就是调用了redis的zcount命令,这个命令可以传入起始分值和结束分值。我当前时间戳作为结束分值,然后用当前时间戳减去限流时间,比如5分钟的毫秒值,求出来5分钟前的时间戳,于是根据这两个时间戳作为分值,范围查询zset中出现的次数,就得到用户在5分钟内,这个行为一共触发了几次。
- 后续的业务,就是不同的场景中,根据不同的需求进行校验。
利用redis的zset进行实现
时间窗口限流
key的格式:【场景:行为:用户唯一标识:(手机号、用户名)】
score:时间戳
value:时间戳
key: COMMON:LOGIN:PHONE:15061131871
redis的命令:ZOUNT key min max
返回指定分值范围内的成员数量
Java:
- 记录限流行为(将内容存到redis中)
- 查询用户行为在指定时间段的行为
- java业务代码自行根据需求判断是否进行限流
续约Token是怎么设计的
避免用户
你们项目的数据库是怎么设计的?分了几个库和表,你负责设计过那些数据库?
数据库设计是在项目初期阶段完成的。当时我们先确定了基本需求以及采用的架构后,围绕业务涉及数据库表结构。
我们一共分了6个数据库,分别是:权限管理、系统管理、用户管理
我当时主要负责权限管理、
我们设计数据库的思路就是,围绕业务进行设计。
首先我们是拆分出来有哪些业务模块,优先梳理出核心的业务,围绕核心业务设计数据库表,这部分工作主要是产品经理完成的,他会先给出初期的需求文档。
然后我围绕我负责的几个模块,分析相关的业务流程,每一个流程的业务设计到哪些实体,整个业务流程围绕这几个实体是怎么运转的,当我把这个想明白后,我直接使用powerdesigner绘制物理数据模型
登录是怎么做的?
Redis的时间窗口限流算法
面经
腾讯
2024年3月14日
腾讯会议半小时
流程
自我介绍
收获
- 自我介绍改进
- 不合理的自我介绍可能会带来一定的坑 注意!!!
常见的攻击手段或者可以利用的漏洞有哪些?
- SQL注入(SQL Injection):通过在用户输入中注入恶意的SQL代码,从而执行未经授权的数据库操作。
- 跨站脚本(Cross-Site Scripting,XSS):通过在网页中插入恶意的脚本代码,利用用户对网站的信任执行恶意操作。
- 跨站请求伪造(Cross-Site Request Forgery,CSRF):利用受害者已经登录的身份,在不知情的情况下发送恶意请求,以执行某些操作。
- 文件包含漏洞:通过未经正确验证的用户输入,将恶意文件包含到网站中,导致执行恶意代码。
- 未经授权访问漏洞:未正确验证用户身份或权限的情况下,允许用户访问受限资源或执行受限操作。
- 身份验证漏洞:例如弱密码、密码重置漏洞、会话劫持等,可以导致攻击者获取未授权的访问权限。
- 不安全的文件上传:允许用户上传任意文件,但未正确验证文件类型、大小和内容,导致恶意文件上传和执行。
- XML外部实体注入(XML External Entity Injection,XXE):利用XML解析器解析恶意的外部实体或远程文件,从而获取敏感信息或执行恶意操作。
- 服务端请求伪造(Server-Side Request Forgery,SSRF):利用服务端发起的HTTP请求执行恶意操作,例如访问内部系统、绕过防火墙等。
- 缓冲区溢出漏洞:通过向缓冲区输入超出其容量的数据,覆盖其他内存区域,从而执行恶意代码。
http1.1和http2.0的区别
- 多路复用
- 头部压缩
- 服务器推送
- 流量优先级
- 二进制协议
- 连接复用
CSRF的原理
- 攻击者在恶意网站中放置恶意代码,该代码会向受害者的浏览器发送带有伪造请求的http请求
- 受害者在登录过某个网站并保留会话状态时,访问了包含恶意外码的网站
- 受害者的浏览器执行了恶意代码,向目标网站发送了伪造的http请求,由于浏览器会自动发送已登录的用户凭证,因此目标网站会误以为请求是合法的。
- 目标网站收到请求后,会按照受害者的权限执行请求,导致攻击者攻击者所期望的恶意行为发生,比如修改用户资料等
单索引和联合索引的区别?会用在什么场景?
索引如果过多会发生什么
- 存储空间占用增加
- 写操作性能下降
- 查询性能下降
- 维护成本增加
- 内存占用增加
java对象是怎么回收的?
美团
2024年3月25日 一面
岗位:后端开发
2024年4月2日 二面
基础研发平台-企业平台研发部
杭州xx公司
杭州xx公司
菜鸟Java后端面试
2024年4月19日
常用设计模式有哪些?
美团测开
【面试职位】:【转正实习】测试开发工程师 点击查看职位详情介绍
【面试时间】:北京时间04月24日 17:00
【面试部门】:SaaS事业部-SaaS技术部
【面试形式】:美团视频面试