大厂面试题分享第一期
- Redis持久化方式
- AOF优缺点
- RDB优缺点
- 如何保证Redis和Myql的一致性
- 索引下推
- 输入url到浏览器发生了什么
- ReentranLock底层原理
- SpringBoot 的启动流程
Redis持久化方式
Redis提供了两种主要的持久化机制,分别是AOF(Append-Only File)和RDB(Redis Database)
- AOF日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里
- RDB快照:将某一个时刻的内存数据,以二进制的方式写入磁盘
- AOF持久化:是将Redis的操作命令追加到一个只追加文件中。通过记录所有的写操作命令,AOF文件可以重建整个数据集。AOF持久化适用于数据的持久性和故障恢复。
这里以 [set name xiaolin」命令作为例子,Redis 执行了这条命令后,记录在 AOF 日志里的内容如下
Redis 提供了3种写回硬盘的策略,在 Redis.conf配置文件中的 appendfsync 配置项可以有以下3种参数可填:
- Always,这个单词的意思是「总是」,所以它的意思是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘:
- Everysec,这个单词的意思是「每秒」,所以它的意思是每次写操作命令执行完后,先将命令写入到AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;
- No,意味着不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也,就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。
- RDB持久化:是将Redis的内存数据以快照的方式写入磁盘文件。该机制会在指定的时间间隔或者达到一定的数据变化量时,将当前数据库的数据集合保存到磁盘上的一个二进制文件中。RDB持久化适用于数据备份和恢复,以及冷启动时快速加载数据。
AOF优缺点
优点:
- 提供了更好的数据安全性,因为它默认每接受到一个写命令就会追加到文件末尾,即使Redis服务器宕机,也只会丢失最后一次写入前的数据;
- AOF支持多种同步策略(如everysec、always等),可以根据需要调整数据安全性和性能之间的平衡。同时,AOF文件在Redis启动时可以通过重写机制优化,减少文件体积,加快恢复速度。
缺点:
因为记录了每一个写操作,所以AOF文件通常比RDB文件更大,消耗更多的磁盘空间。并且,频繁的磁盘IO操作(尤其是同步策略设置为always时)可能会对Redis的写入性能造成一定影响。
RDB优缺点
优点:
- RDB通过快照的形式保存某一时刻的数据状态,文件体积小,备份和恢复的速度非常快。
- RDB是在主线程之外通过fork子进程来进行的,不会阻塞服务器处理命令请求,对Redis服务的性能影响较小。最后,由于是定期快照,RDB文件通常比AOF文件小得多。
缺点:
- RDB方式在两次快照之间,如果Redis服务器发生故障,这段时间的数据将会丢失。并且,如果在RDB创建快照到恢复期间有写操作,恢复后的数据可能与故障前的数据不完全一致
如何保证Redis和Myql的一致性
如何保证Redis和Myql的一致性
索引下推
在MySQL5.6之前,当查询使用到复合索引时,MySQL会先根据索引的最左前缀原则,在索引上查找到满足条件的记录的主键或行指针,然后再根据这些主键或行指针到数据表中查询完整的行记录。之后,MySQL再根据WHERE子句中的其他条件对这些行进行过滤。这种方式可能导致大量的数据行被检索出来,但实际上只有很少的行满足WHERE子句中的所有条件。
从MySQL5.6开始引入的一个特性,索引下推通过减少回表的次数来提高数据库的查询效率;
注意:索引下推是为了减少回表而发明的。
索引下推的产生一定围绕着回表,没有回表那就没必要产生索引下推,因为上面也说了索引下推的目的就是减少回表,而不是避免回表。(题外话:避免回表使用索引覆盖——建立覆盖索引)
为了解决这个问题,MySQL 5.6引入了索引下推优化。
索引下推(index condition pushdown )简称ICP,在Mysql5.6的版本上推出,用于优化查询。
需求: 建立了(name,age)组合索引,查询users表中 “名字第一个字是张,年龄为10岁的所有记录”。
SELECT * FROM users WHERE user_name LIKE '张%' AND user_age = 10;
根据最左前缀法则,该语句在搜索索引树的时候,只能匹配到名字第一个字是‘张’的记录,接下来是怎么处理的呢?当然就是从该记录开始,逐个回表,到主键索引上找出相应的记录,再比对 age
这个字段的值是否符合。
图1: 在 (name,age) 索引里面特意去掉了 age 的值,这个过程 InnoDB 并不会去看 age 的值,只是按顺序把“name 第一个字是’张’”的记录一条条取出来回表。因此,需要回表 4 次
MySQL 5.6引入了索引下推优化,可以在索引遍历过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表次数。
图2: InnoDB 在 (name,age) 索引内部就判断了 age 是否等于 10,对于不等于 10 的记录,直接判断并跳过,减少回表次数.
总结
如果没有索引下推优化(或称ICP优化),当进行索引查询时,首先根据索引来查找记录,然后再根据where条件来过滤记录;
在支持ICP优化后,MySQL会在取出索引的同时,判断是否可以进行where条件过滤再进行索引查询,也就是说提前执行where的部分过滤操作,在某些场景下,可以大大减少回表次数,从而提升整体性能。
输入url到浏览器发生了什么
- 用户输入URL(Uniform Resource Locator):用户在浏览器的地址栏输入网址,例如:https://www.example.com。
- DNS解析:浏览器将解析输入的URL,首先检查其是否符合有效URL的规范。接下来,浏览器会通过DNS(Domain Name System)将域名解析为对应的IP地址。DNS解析过程可能涉及本地缓存、本地域名服务器、根域名服务器、顶级域名服务器和权威域名服务器。
- 获取MAC地址:当浏览器得到IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。
通过将 IP 地址与本机的子网掩码相结合,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代头转发,此时同样可以通过 ARP 协议来获取网关的 MAC地址,此时目的主机的 MAC 地址应该为网关的地址
- 建立TCP连接:在获取到目标服务器的IP地址后,浏览器会向该服务器发送一个TCP连接请求。这个过程通常包括“三次握手”。
- 发送HTTP请求:TCP连接建立后,浏览器会通过这个连接向服务器发送一个HTTP(Hypertext Transfer Protocol)请求。请求中包含了请求方法(例如:GET或POST)、请求的资源路径、HTTP版本、请求头(包含浏览器信息、语言、编码等信息)等。
- 服务器处理请求:服务器接收到浏览器的请求后,会根据请求的资源路径查找对应的资源,并进行相关处理(例如执行服务器脚本、查询数据库等)。
- 服务器响应:服务器处理完请求后,会生成一个HTTP响应,包含HTTP响应状态码(例如200表示成功),响应头(包含响应内容类型、编码等信息)和响应体(请求的资源内容,如HTML文档)。
- 浏览器接收响应:浏览器收到服务器的响应后,会根据响应头信息解析响应体中的内容。如果响应内容是HTML文档,浏览器会进行下一步的解析和渲染
ReentranLock底层原理
简单介绍一下AQS
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁 实现的,
ReentrantLock 主要利用 CAS + AQS队列来实现,通过重写了 AQS 的 tryAcquire 和 tryRelease方法实现的 lock 和 unlock。
- 它的原理是基于AQS(AbstractQueuedSynchronizer ),多个线程抢锁时,如果争抢失败则会进入阻塞队列。等待唤醒,重新尝试加锁。
- AQS的子类有公平锁FairSync和非公平锁NofairSync,ReentrantLock的无参构造默认是非公平锁,有参构造参数是true可以设置成公平锁。
- 公平锁:ReentrantLock调用lock方法,最终会调用Sync类的tryAcquire函数,获取资源。当前线程只有在队列为空或者时队首节点的时候,才能获取资源,否则会被加入到阻塞队列中。
- 非公平锁:调用lock方法时 lock:直接利用CAS尝试将state从 0 改为 1,如果成功,拿锁直接走,如果失败了,执行sync的tryAcquire,不同的是tryAcquire还会调用nofairTryAcquire。在nofairTryAcquire中会再次判断当前锁是否被占用
- 如果当前锁没有占用(state==0),直接再次尝试将state从0 改为 1 如果成功,拿锁直接走。
- 如果当前锁被占用(state!=0):如果被自己占用,则计数器会state++(可重入锁),如果被其他线程占用加入AQS队列
- 公平锁和非公平锁的区别: 非公平锁在调用NonfairSync的lock的时候就会马上进行CAS抢锁,抢不到就和公平锁一样进入tryAcquire方法尝试抢锁,如果发现锁被释放了(state==0),非公平锁马上CAS抢锁,而不会管阻塞队列里是否有线程等待。而公平锁会排队等待。
SpringBoot 的启动流程
构造方法
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {this.resourceLoader = resourceLoader;Assert.notNull(primarySources, "PrimarySources must not be null");this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));// 获取应用类型,根据是否加载Servlet类判断是否是web环境this.webApplicationType = WebApplicationType.deduceFromClasspath();this.bootstrappers = new ArrayList<>(getSpringFactoriesInstances(Bootstrapper.class));// 读取META-INFO/spring.factories文件,获取对应的ApplicationContextInitializer装配到集合setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));// 设置所有监听器setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));// 推断main函数this.mainApplicationClass = deduceMainApplicationClass();
}
当启动springboot应用程序时,会先创建SpringApplication对象,在其构造方法中进行参数的初始化工作:
- 推断并设置当前web应用类型
- 获取所有初始化器、监听器。扫描所有META-INF/spring.factories文件中分别读取key为ApplicationContextInitializer、ApplicationListener 三种接口类型的实现类,并分别设置对应的属性,将文件中的内容放到缓存对象中,方便后续获取
- 定位main应用程序类
/*** Run the Spring application, creating and refreshing a new* {@link ApplicationContext}.** @param args the application arguments (usually passed from a Java main method)* @return a running {@link ApplicationContext}*/public ConfigurableApplicationContext run(String... args) {// 启动一个秒表计时器,用于统计项目启动时间StopWatch stopWatch = new StopWatch();stopWatch.start();// 创建启动上下文对象即spring根容器DefaultBootstrapContext bootstrapContext = createBootstrapContext();// 定义可配置的应用程序上下文变量ConfigurableApplicationContext context = null;/*** 设置jdk系统属性* headless直译就是无头模式,* headless模式的意思就是明确Springboot要在无鼠键支持的环境中运行,一般程序也都跑在Linux之类的服务器上,无鼠键支持,这里默认值是true;*/configureHeadlessProperty();/*** 获取运行监听器 getRunListeners, 其中也是调用了上面说到的getSpringFactoriesInstances 方法* 从spring.factories中获取配置*/SpringApplicationRunListeners listeners = getRunListeners(args);// 启动监听器listeners.starting(bootstrapContext, this.mainApplicationClass);try {// 包装默认应用程序参数,也就是在命令行下启动应用带的参数,如--server.port=9000ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);///*** 准备环境 prepareEnvironment 是个硬茬,里面主要涉及到* getOrCreateEnvironment、configureEnvironment、configurePropertySources、configureProfiles* environmentPrepared、bindToSpringApplication、attach诸多方法可以在下面的例子中查看*/ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);// 配置忽略的 beanconfigureIgnoreBeanInfo(environment);// 打印 SpringBoot 标志,即启动的时候在控制台的图案logo,可以在src/main/resources下放入名字是banner的自定义文件Banner printedBanner = printBanner(environment);// 创建 IOC 容器context = createApplicationContext();// 设置一个启动器,设置应用程序启动context.setApplicationStartup(this.applicationStartup);// 配置 IOC 容器的基本信息 (spring容器前置处理)prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);/*** 刷新IOC容器* 这里会涉及Spring容器启动、自动装配、创建 WebServer启动Web服务即SpringBoot启动内嵌的 Tomcat*/refreshContext(context);/*** 留给用户自定义容器刷新完成后的处理逻辑* 刷新容器后的扩展接口(spring容器后置处理)*/afterRefresh(context, applicationArguments);// 结束计时器并打印,这就是我们启动后console的显示的时间stopWatch.stop();if (this.logStartupInfo) {// 打印启动完毕的那行日志new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);}// 发布监听应用上下文启动完成(发出启动结束事件),所有的运行监听器调用 started() 方法listeners.started(context);// 执行runner,遍历所有的 runner,调用 run 方法callRunners(context, applicationArguments);} catch (Throwable ex) {// 异常处理,如果run过程发生异常handleRunFailure(context, ex, listeners);throw new IllegalStateException(ex);}try {// 所有的运行监听器调用 running() 方法,监听应用上下文listeners.running(context);} catch (Throwable ex) {// 异常处理handleRunFailure(context, ex, null);throw new IllegalStateException(ex);}// 返回最终构建的容器对象return context;}
复制代码
- 在SpringApplication对象创建完成后,开始执行run方法,来完成整个启动,启动过程中最主要有两个方法,一个是prepareContext,另外一个是refreshContext,之前的处理逻辑包含创建定时器、上下文对象的创建、banner的打印等各个准备工作,方便后续来进行调用
- 在prepareContext方法主要完成对上下文对象的初始化操作,包括属性的设置,比如把Environment环境变量设置给Spring容器
- 在refreshContext方法中会进行整个容器刷新过程,会调用Spring中AbstractApplicationContext#refresh方法,其中有13个关键的方法,来完成整个spring IOC容器的创建过程,这里会涉及Spring容器启动、自动装配、创建 WebServer启动Web服务即SpringBoot启动内嵌的 Tomcat
listeners.started(context);
发布容器已启动的事件callRunners(context, applicationArguments);
遍历运行器,并调用run方法