1、String、StringBuffer、StringBuilder的区别
- String是不可变的,如果修改,产生新的对象;StringBuffer和StringBuilder是可变的
- StringBuffer是线程安全的,StringBuilder是不安全的。
2、ArrayList和LinkedList有哪些区别
- ArrayList:底层数组;适合随机查找。但是扩容影响性能
- LinkedList:底层链表;适合删除和添加,实现了Deque接口,可以当队列使用
3、CopyOnWriteArrayList的底层原理是怎样的
- 首先CopyOnWriteArrayList内部是通过数组实现的,添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上执行
- 写操作加锁,防止并发问题
- 写操作结束后会把原来的数组指向新数组
- CopyOnWriteArrayList允许在写操作时读数据,大大提高了读的性能,因此适合读多写少的场景;但是CopyOnWriteArrayList会比较占内存,同时可能读到的数据不是最新数据,不适合对实时性要求高的场景
4、HashMap的扩容机制原理
- 默认容量:16
- 加载因子:0.75
- 扩容数组翻倍
1.7:
- 先生成新数组
- 遍历老数组中每个位置上的链表上的每个元素
- 取每个元素的key,并基于新数组长度,计算出每个元素在新数组的下标
- 将元素添加到新数组中
- 所有元素转移完成之后,将新数组赋值给HashMap的table属性
1.8:
- 先生成新数组
- 遍历老数组中的每个位置上的链表和红黑树
- 如果是链表,则直接将链表中的每个元素重新计算,并添加到新数组中
- 如果是红黑树,则先遍历红黑树,计算出红黑树中每个元素对应在新数组中的下标位置
-
- 统计每个下标位置的元素个数
- 如果大于8,生产一个新的红黑树,并将节点的添加到新数组的对应位置
- 如果没有超过8,生成一个链表,将链表头的位置添加到数组对应的位置
- 所有元素转移完成之后,将新数组赋值给HashMap的table属性
5、ConcurrentHashMap的扩容机制
1.7:
- 基于Segment分段实现的
- 每个Segment相当于一个小型的HashMap
- 每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
- 先生成新的数组,然后转移元素到新数组中
- 扩容的判断也是每个Segment内部单独判断的
1.8:
- 不在基于Segment实现
- 当某个线程进行put时,如果发现其他线程正在扩容,那么该线程一起扩容。
- 扩容之前线程池一个数组
- 在转移元素时,将原数组分组,将每组分给不同 的线程进行元素转移
6、ThreadLocal的底层原理
- ;是Java中提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以任意时刻、任意方法中获取缓存
- ThreadLoca底层是通过ThreadLocalMap实现的,每个Thread对象中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
- 如果在线程池中使用会造成内存泄漏,所以在使用结束时需要调用remove方法
7、如何理解volatile关键字
对于加了volatile关键字的属性,在对这个属性修改时,会直接将工作内存中的数据写回到主内存,对这个变量的读取也会直接从主存中读取,从而保证了可见性;底层是通过内存屏障实现的,禁止指令重排,保证了有序性。但是不保证原子性。
8、ReentrantLock中的公平锁和非公平锁的底层实现
底层实现使用AQS来进行排队的,区别在于:公平锁会检查AQS队列中是否有线程,如果有,直接排队;非公平锁不会检查是否有线程在排队,直接竞争。但是竞争失败之后,会排队。
获取锁的步骤:
9、ReentrantLock中tryLock()和lock()的区别
- tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,加到锁返回true,否则返回false
- lock()表示阻塞 加锁。线程会阻塞直到加到锁为止,没有返回值
10、CountDownLatch和Semaphore的区别和底层原理
countDownLatch表示计数器,可以给它设置一个数字,当线程调用await()将会阻塞,其他线程调用countDown()方法可以对数字减一,当数字被减到0时,所有的await线程都会被唤醒。对应的底层原理是,调用await()方法的线程会利用AQS排队,一旦数字被减为0,则会将AQS中排队的线程依次唤醒。
Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使用该信号量,通过acquire()来回去许可,如果没有许可则阻塞线程,并通过AQS排队,可以通过release()方法来释放许可,当某个线程释放了许可后,会从AQS中正在排队的第一个线程开始依次唤醒,知道没有空闲许可。
11、Sychronized的偏向锁、轻量级锁、重量级锁
- 偏向锁:在锁对象的对象头中记录一下当前获得到该锁的线程id,该线程下次如果再获取锁就可以直接获取了
- 轻量级锁:由偏向锁升级而来,当一个线程获取到锁之后,此时这把锁是偏向锁,如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,就是为了和重量级锁区分来,轻量级锁底层是通过自旋锁来实现的,并不会阻塞线程
- 如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
- 自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。
12、Sychronized和ReentrantLock的区别
- Sychronized是一个关键字,ReentrantLock是一个类
- Syncronized会自动加锁和释放锁,ReentrantLock需要程序员手动加锁和释放锁
- Syncronized是底层是JVM层面的锁,ReentrantLock是API层面的锁
- Syncronized是非公平锁,ReentrantLock可以是公平锁也可以是非公平锁
- Syncronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识锁的状态
- Syncronized底层有一个锁升级的过程
13、线程池底层工作原理
线程生命周期
- NEW:新建
- RUNNABLE:运行
- BLOACLED:阻塞
- WAITING:等待
- TIMED_WAITING:超时等待
- TERMINATED:终结
线程池参数
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数。如果阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize
- keepAliveTime:大于核心线程的线程,在运行完之后不会立即销毁,而是超过keepAliveTime再销毁
- unit:keepAliveTime的时间单位
- workQueue:用于保存执行任务的阻塞队列
-
- ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;(基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略.)
- LinkedBlockingQueue:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常高于ArrayBlockingQueue (基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的.)
- SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量高于LinkedBlockingQueue (一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。)
- priorityBlockingQuene:具有优先级的无界阻塞队列
- threadFactory:创建新线程
- handler:线程的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必然会采取一种策略处理
-
- AbortPolicy:直接抛出异常,默认策略;
- CallerRunsPolicy:用调用者所在的线程来执行任务;
- DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
- DiscardPolicy:直接丢弃任务;
线程执行流程
- 提交任务
- 判断线程数是否小于核心线程数?
-
- 是直接执行
- 判断阻塞队列是否尾满?
-
- 是:放入阻塞队列
- 判断线程数是否大于最大线程数
-
- 执行拒绝策略
- 执行线程
- 结束
14、JVM中那些是线程共享区域
堆和方法区是线程共享的;栈、本地方法栈、程序计数器是线程独有的
15、JVM哪些可以作为GC Root
特征:只会引用其他对象,不会被其他对象引用
- 栈中的本地变量
- 方法区中的静态变量
- 本地方法栈中的变量
- 正在运行的线程
16、项目排查JVM问题
对于正在运行中的系统:
已经发生了OOM的系统:
17、类加载器双亲委派模型
默认类加载器
- BootstrapClassLoader:主要加载的是 jre/lib下里面的类
- ExtClassLoader:主要加载 jre/lib/ext 下面的类
- AppClassLoader:主要加载自定义的类,classPath下的
JVM在加载一个类时,会调用AppClassLoader的loadClass方法来加载这个类,不过在这个方法中,会先使用ExtClassLoader的 loadClass 方法来加载,同样 ExtC
lassLoader 的 loadClass 方法会先使用 BootstrapClassLoader 来加载类,如果 BootstrapClassLoader 加载到了就直接成功,如果没有加载到,那么 ExtClassLoader 就会尝试自己加载该类,如果没有加载到,那么则会由 AppClassLoader 来加载这个类。
为什么要用双亲委派机制?
- 提供了一个沙箱安全环境,防止修改JVM源码
- 防止一个类多次加载
18、Tomcat中为什么要是用自定义 类加载器
一个Tomcat 中可以部署多个应用,而每个应用中存在很多类,而且各个应用中的类书独立的,全类名是可以相同的。如果使用AppClassLoader,那么只有一个类可以被加载。所以,Tomcat 为每个应用生成了一个类加载器,名字叫做WebAppClassLoader,这样每个应用就可以加载自己的类,从而达到应用之间的隔离,不出现冲突。另外,Tomcat 还利用自定义类加载器实现了热加载功能。
19、浏览器发出一个请求到收到相应经历了哪些步骤
- 浏览器解析用户输入的URL,生成一个HTTP格式的请求
- 先根据URL域名从本地hosts文件查找是否有映射ip,如果没有就将域名发送给电脑配置的DNS进行域名解析,得到IP地址
- 浏览器将操作系统将请求通过四层网络协议发送出去
- 途中可能经过各种路由器、交换机。最终到达服务器
- 服务器收到请求后,根据请求所指定的端口,将请求传递给绑定了端口的应用程序
- Tomcat收到请求后,按照http协议的格式进行解析,解析到要访问的 servlet
- 然后 servlet 来处理这个请求,如果是 SpringMVC 中的 DispathcerServlet,那么则会找到对应的Controller 中的方法,并执行该方法得到的结果
- Tomcat 得到相应后封装成 HTTP 响应格式,并再次通过网络发送给浏览器所在的服务器
- 浏览器所在的服务器拿到结果后再传给浏览器,浏览器负责解析并渲染
OSI七层协议模型主要是:应用层(Application)、表示层(Presentation)、会话层(Session)、传输层(Transport)、网络层(Network)、数据链路层(Data Link)、物理层(Physical)
五层体系结构包括:应用层、运输层、网络层、数据链路层和物理层。
四层是应用层、运输层、网络层、网络接口层
20、跨域请求是什么?怎么解决?
跨域是指浏览器在发起网络请求时,会检查请求所对应的协议、域名、端口和当前网页是否一致,如果不一致则浏览器就会进行限制。如果开发者想要绕过这层限制也是可以的:
- response添加header,比如resp.setHeader("Access-Control-Allow_origin". "*");表示可以访问所有的网站,不受是否同源的限制
- jsonp 的方式,该技术底层就是基于script标签来实现的,因为scrip标签是可以跨域的
- 后台自己控制,先访问同域名下的接口。然后再接口中使用HTTPClient等工具调用目标接口
- 网关,和第三种方式类似,都是交给后台服务来进行跨域访问
21、Spring 的 bean 生命周期
class文件 -> beanDefinition -> 实例化 -> 依赖注入 -> 初始化前 -> 初始化 -> 初始化后(AOP) -> 销毁
- Bean 容器中找到配置文件中 Spring Bean 定义
- Bean 容器利用 反射 API 创建一个Bean实例
- 如果涉及到一些属性值,利用set()方法设置一些属性值
- 如果 Bean实现了 BeanNameAware 接口,调用 setBeanName() 方法,传入bean的名字
- 如果 Bean 实现了 BeanClassLoaderAware,调用 setBeanClassLoader()方法,传入ClassLoader对象的实例
- 如果 Bean 实现了 BeanFactoryAware 接口,调用setBeanClassFactory() 方法,传入ClassLoader对象实例
- 与上面类似,如果实现了其他Aware接口,就调用相关的方法
- 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执行postProcessorBeforeInitialization() 方法
- 如果 Bean 实现了 initializingBean 接口,执行 afterPropertiesSet() 方法
- 如果 Bean 在配置文件中的定义包含了 init-method属性,执行指定方法
- 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor对象,执行 postProcessorAfterInitialization() 方法
- 当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法
- 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法
22、Spring中Bean是线程安全的吗
- Spring本身并没有针对Bean做线程安全的处理,所以:
-
- prototype:原型Bean(多例) ,每次都会创建新对象,线程之间并不存在Bean共享,所以不存在线程安全的问题
- singleton:单例Bean
-
-
- 无状态Bean是线程安全的。无状态就是线程不会对Bean进行查询以外的操作
- 有状态Bean是线程不安全的
-
23、ApplicationContext 和 BeanFactory 的区别
BeanFactory 是 Spring 中非常核心的组件,表示bean 工厂,可以生成 Bean,维护 Bean,而 ApplicationContext 继承了 BeanFactory,所以 ApplicationContext 拥有 BeanFactory 的所有特点,也是一个 Bean 工厂,但是 ApplicationContext 除了继承 BeanFactory 外,还继承了诸如 EnvironmentCapable、MessageSource、ApplicationEventPublisher等接口,从而 ApplicationContext 还有获取环境变量、国际化、事件发布等功能,这是 BeanFactory 不具备的。
24、Spring 中的事务是如何实现的
- Spring 事务底层是基于数据库事务和AOP机制
- 解析使用了@Transactional注解的 Bean,Spring 会创建一个代理对象
- 如果加了,那么则利用事务管理器创建一个数据库连接
- 修改数据库连接的 autocommit 属性为 false,禁止自动提交
- 执行方法中的SQL
- 如果没有出现异常或者手动回滚就直接提交事务
- 根据设置的异常类型回滚事务,如果不匹配,仍然提交事务
事务的问题与隔离级别
- 脏读:一个事务读到了另一个没有提交的事务数据
- 不可重复读:一个事务范围多次查询却返回了不同的结果
- 幻读:事务在插入已经检查过不存在的记录时,发现这些数据已经存在了
- 串行化:可避免所有
- 可重复读:可避免脏读、不可重复读
- 读提交:避免脏读
- 读未提交:无法保证
事务的四大特性
- 原子性:原子性是指事务包含的操作要么全部成功,要么全部失败
- 一致性:事务必执行前后必须处于一致性状态
- 隔离性:并发事务之间要相互隔离
- 持久性:事务一旦被提交,对数据库中的改变就是永久的,即使遇到故障也不会丢失
25、Spring 中什么时候 @Transactional 会失效
- 在非public修饰的方法上会失效,因为底层cglib是基于父子类来实现的,子类不能重载父类的方法
- 在同一个类方法中调用,会导致失效;解决方式:1、使用 AopContext.currentProxy();2、将这个类维护成成员变量
26、Spring 容器启动流程
- 加载 spring 内置的 bean
- 调用 BeanFactoryPostProcessor 后置处理器
- 扫描 所有的 BeanDefinition 对象,并存在一个Map中
- 过滤BeanDefinition,对于多例Bean不需要在容器启动时创建
- 根据 BeanDefinition 创建 Bean 实例,期间包括了 合并 BeanDefinition、推断构造方法、实例化、属性填充、初始化、初始化后等步骤,其中AOP就是发生在初始化后这一步骤
- 单例 Bean创建完成后,Spring 会发布一个容器启动事件
- Spring 启动结束
27、Spring 用到了哪种设计模式
- 工厂模式:BeanFactory
- 装饰器模式:BeanWrapper
- 代理模式:AOP
- 观察者模式:时间监听机制
- 模板模式:JdbcTemplate
- 责任链模式:BeanPostProcessor
28、Spring MVC 的底层工作流程
- 请求URL到前端控制器DispatcherServlet
- DispatcherServlet 收到请求调用处理器映射器 HandlerMapping
- HandlerMapping 根据具体的 url 找到具体的处理器,生成处理器执行链返回给 DispatcherServlet
- DispatcherServlet 根据处理器 Handler 获取处理器适配器 HandlerAdapter 处理一系列操作;比如参数封装,数据格式转换等
- 执行处理器 Handler
- 处理器返回 ModelAndView
- HandlerAdapter 将Handler执行结果返回 DispatcherServlet
- DispatchServlet 将 ModelAndView 传给 VIewReslover 视图解析器
- VIewReslover 解析后返回具体View
- DispatchServlet 对View 进行渲染
- DispatchServlet 响应用户
29、Spring Boot 自动装配流程
从注解反向看自动装配
从注解反向看自动装配
1. @SpringBootApplication
a. @ComponentScan:包扫描
b. @SpringBootConfiguration
ⅰ. @Configuration:标记当前类为配置类
c. @EnableAutoConfiguration:自动装配
ⅰ. @Import({AutoConfigurationImportSelector.class}):向Spring 容器中注入AutoConfigurationImportSelector 自动装配类
ⅱ. Spring 调用 selectImports 方法
ⅲ. this.getAutoConfigurationEntry(annotationMetadata) :加载配置类
1. this.getCandidateConfigurations(annotationMetadata, attributes);
2. 加载 META-INF/spring.factories 文件下面的类
3. 排重
4. 移除要排除的类
5. 筛选满足条件的类
从正向流程看自动配置
- SpringApplication.run(DemoApplication.class, args):启动Spring Boot项目
- refreshContext(context):启动Spring IOC 容器
- invokeBeanFactoryPostProcessors(beanFactory):调用 Bean 工厂的后置处理器
- beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false):获取实现BeanDefinitionRegistryPostProcessor 接口的后置处理器
- invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());:调用实现BeanDefinitionRegistryPostProcessor接口的postProcessBeanDefinitionRegistry 方法
- beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE):bean中选取有@Configuration的类
- parser.parse(candidates):处理所有的@Configuration的配置类,解析配置类
- processImports:处理@Import 注解。获取到 AutoConfigurationImportSelector
- 调用 AutoConfigurationImportSelector 的 process 方法。将配置类导入
- Spring IOC 容器 加载内置 BeanFactoryPostProcessor:ConfigurationClassPostProcessor(解析配置类@Configuration),@SpringBootApplication 注解里面刚好有
- IOC 容器进行 refreshContext(context):启动Spring IOC 容器
- invokeBeanFactoryPostProcessors(beanFactory):调用 Bean 工厂的后置处理器,也就是调用 ConfigurationClassPostProcessor 的 postProcessBeanDefinitionRegistry 方法,进行配置类解析(解包含 @Configuration,@ComponentScan,@Import 等注解)
- 解析@Import 注解后,获取到 @EnableAutoConfiguration 注解上的 AutoConfigurationImportSelector类
- 调用 AutoConfigurationImportSelector 类的 process() 方法,META-INF/spring.factories 文件下面的类进行解析
30、Spring Boot 是如何启动 Tomcat 的
- Spring Boot 在启动的时会创建一个Spring容器
- 利用 @ConditionalOnClass 技术来判断当前 classPath中是否依赖 Tomcat,如果存在就会生成一个启动Tomcat的Bean
- Spring 容器创建完之后,就会获取启动 Tomcat 的Bean,并创建 Tomcat 对象,并绑定端口,然后启动 Tomcat
31、Spring Boot 中配置文件的加载顺序
- 命令行参数
- Java系统参数(System.getProperties)
- 操作系统环境变量
- jar包外部的 application-{profile}.properties 或 application.yml(带 spring.profile)配置文件
- jar包内部的 application-{profile}.properties 或 application.yml(带 spring.profile)配置文件,再来加载不带 profile的
- jar包外部的 application.properties 或 application.yml(不带 spring.profile)配置文件
- jar包内部的 application.properties 或 application.yml(不带 spring.profile)配置文件
- @Configuration 注解类的@PropertySource
32、Mybatis 存在哪些优点和缺点
优点:
- 基于SQL语句编程,相当灵活,不会对应用程序或者数据库现有程序造成影响,SQL单独写,解除SQL与应用程序的耦合,便于统一管理
- 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量的冗余代码。不需要手动开关连接
- 很好的与各种数据库兼容
- 能够与Spring 很好的继承
- 提供映射标签,支持对象与数据库的 ORM 关系字段映射;提供对象关系映射标签,支持对象关系组件维护
缺点:
- SQL 语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求
- SQL 语句依赖数据库,导致数据库移植性差,不能随意更换数据库
33、Mybatis 中 #{} 和 ${} 的区别是什么
- #{} 是预编译处理、是占位符, ${} 是字符串替换、是拼接符
- Mybatis 在处理 #{} 时,会将 SQL中的 #{} 替换为 ?,调用 preparedStatement 来赋值
- Mybatis 在处理 ${} 时,就是会把 ${},替换成变量的值,调用 statement 来赋值
- 使用 #{} 可以防止SQL注入
34、什么是CAP理论
CAP理论是分布式领域中非常重要的一个知道理论,C(Consistency)表示强一致性,A(Availablility)表示可用性,P(Partition Tolerance)表示分区容错性,CAP理论支出在目前硬件的条件下,一个分布式系统是必须要保证分区容错性的,在这个前提下,分布式系统要么保证CP,要么保证AP,无法同时保证CAP
分区容错性表示,一个分布式系统虽然是分布式的,但是对外看上去应该是一个整体,不能由于分布式系统内部的某个节点挂了,或者网络出现了故障,而导致系统出现异常。所以,对于分布式系统而言是一定要保证分区容错性的。
强一致性表示,一个分布式系统中各个节点之间能及时的同步数据,在同步数据的过程中,是不能对外提供服务的,不然会造成数据不一致,所以强一致性和可用性是不能同时满足的。
可用性表示,一个分布式系统对外要保证可用。
35、什么是 BASE 理论
由于不能同时出现CAP,所以出现了BASE理论:
- BA:Basically Available,表示基本可用,表示可以允许一定程度的不可用,不如由于系统故障,请求时间边长,或者由于系统故障导致部分核心功能不可用,都是允许的
- S:Soft state,表示分布式系统可以处于一种中间状态,比如数据正在同步
- E:Eventually consistent,表示最终一致性,不要求分布式系统数据实时一致,允许在经过一段时间再打到一致,在打到一致的过程中,系统也是可用的
36、什么是 RPC
RPC,表示远程过程调用,对于Java这种面向对象语言,也可以理解为远程方法调用,RPC调用和HTTP调用是有区别的,RPC表示的是一种调用远程方法的方式,可以使用HTTP协议、或直接基于TCP协议来实现RPC。在Java中,我们可以直接使用某个服务接口的代理对象来执行方法,而底层则通过构造HTTP请求来调用远端的方法,所以,有一种说法是RPC协议是HTTP协议之上的一种协议,也是可以理解的、
37、分布式ID是什么?有哪些解决方案
- UUID,方案复杂性低,但是会影响空间和性能
- 利用单机数据库的自增主键,作为分布式ID的生成器,复杂度适中,ID较uuid更短,但是收到单机数据库性能限制,并发量大的时候,此方案也不是最优方案
- 利用redis、zookeeper的特性来生成id,比如redis的自增命令、zookeeper的顺序节点,这种方案和单机数据库相比,性能有所提高,可以适当选用
- 雪花算法,一切问题如果能直接用算法解决,那就是最合适的,利用雪花算法也可以生成分布式ID,底层原理就是通过某台机器在某一毫秒内对某一个数字自增,这种方案也能保证分布式架构中的系统唯一id,但是只能保证自增趋势。业界存在tinyid、leaf等开源中间件实现了雪花算法
38、分布式锁的使用场景是什么?有哪些方案?
- zookeeper:利用的是zookeeper的临时节点、顺序节点、watch机制来实现的,zookeeper分布式锁的特点是高一致性,因为zookeeper保证的是CP,所以由它实现的分布式锁更可靠,不会出现混乱
- redis:利用redis的setnx、lua脚本、消息消费订阅等机制来实现的,redis分布式锁的特点是高可用,因为redis保证的是AP,所以由它实现的分布式锁可能不可靠,不稳定,可能出现多个客户端同时加锁的情况。
39、什么是分布式事务?有哪些实现方案?
- 本地消息表:创建订单时,将减库存消息加入到本地事务中,一起提交到数据库存入本地消息表,然后调用库存系统,如果调用成功则修改本地消息状态为成功,如果库存调用失败,则由后台定时任务从本地消息表中取出未成功的消息,重调用库存系统
- 消息队列:目前RocketMQ中支持事务消息,它的工作原理是:
-
- 生产者订单系统发送一条half消息到Broker,half消息对消费者而言是不可实现的
- 在创建订单,根据创建订单是否成功,向Broker发送commit或rollback
- 并且生产者订单系统还可以提供Broker回调接口,当Broker发现一段时间half消息没有收到任何操作命令,则会主动调用此接口来查询订单是否创建成功
- 一旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务结束
- 如果消费失败,则根据重试策略进行重试,最后还是失败则进入死信队列,等待进一步处理
- Seata:分布式事务框架
40、什么是ZAB协议
ZAB协议是Zookeeper用来实现一致性的原子广播协议,该协议描述了Zookeeper是如何实现一致性的,分为三个阶段
- 领导者选举阶段:从Zookeeper集群中选取一个节点作为Leader,所有的写请求都会由Leader实现
- 数据同步阶段:集群中所有节点中的数据要和Leader节点保持一致,如果不一致则要进行同步
- 请求广播阶段:当Leader节点收到写请求时,会利用两阶段提交来广播该写操作,使得写请求想事务一样在其他节点上执行,打到节点上的数据实时一致
但值得注意的是,Zookeeper只是尽量达到强一致性,实际上仍然是最终一致性
41、为什么Zookeeper可以作为注册中心
可以用Zookeeper的临时节点和watch机制来实现注册中心的自动注册和发现,另外Zookeeper中的数据都是存在内存中的,并且Zookeeper底层采用的nio,多线程模型,所以Zookeeper的性能也是比较高的,所以可以用来做注册中心,但是如果考虑到注册可用性的话,Zookeeper则不太合适,因为Zookeeper是CP的,它注重一致性,所以集群数据不一致时,集群将不可用,所以用redis、eureka、Nacos来作为注册中心更合适。
42、Zookeeper中的领导者选举的流程
- 集群中各个节点首先是LOOKING状态,一开始都会投票给自己,认为自己比较适合当Leader
- 然后相互交互投票,每个节点会受到其他节点发过来的选票,然后PK,先比较zxid,zxid大者获胜,如果zxid相同则比较myid,myid大者获胜
- 一个节点收到其他节点发过来的选票,经过PK后,如果PK输了,则改票,此节点就会投各zxid或者myid更大的节点,并将选票放入自己的选票箱中,并将新的选票发送个其他节点
- 如果pk是平局则将收到的选票放入自己的选票箱中
- 如果pk赢了,则忽略所收到的选票
- 当然一个节点将一张选票放入到自己的投票箱之后,就会从投票箱中统计票数,看是否超过一般的节点都和自己所投的节点是一样的,如果超过半数,那么则认为当前自己所投的节点是Leader
- 集群中每个节点都会经过同样的流程,pk的规则也是一样的,一旦改票就会告诉其他服务器,所以最终各个节点中的投票箱中的选票也将是一样的,所以各个节点最终选出来的Leader也是一样的,这样集群的Leader就选举出来了
43、Zookeeper 集群中节点之间数据是如何同步的
- 首先集群启动时,会先进行领导者选举,确定哪个节点是Leader,哪些节点是Follower和Observer
- 然后Leader会和其他节点进行数据同步,采用发送快照和发送Diff日志的方式
- 集群在工作过程中,所有的写请求都会交给Leader节点进行处理,从节点只能处理读请求
- Leader节点收到几个一个写请求时,会通过两阶段机制来处理
- Leader节点会将该写请求对应的入日志发送给其他的Follower节点,并等待Follower节点持久化日志成功
- Follow节点收到日志后会进行持久化,如果持久化成功则发送一个ACK给Leader节点
- 当Leader节点收到半数以上的ACK后,就会开始提交,更新Leader节点本地的内存数据
- 然后发送Commit命令给Follower节点,Follower节点收到commit命令后就会更新各自本地内存数据
- 同时Leader节点还是将当前写请求直接发送给Observer节点,Observer节点收到Leader发送过来的写请求后直接执行更新本地内存数据
- 最后Leader节点返回客户端请求相应成功
- 通过同步机制和两阶段提交机制来达到集群节点数据一致
44、Dubbo支持哪些负载均衡策略
- 随机:从多个服务提供者中随机选择一个来处理本次请求,调用量越大则分布越均匀,并支持按权重设置随机概率
- 轮询:一次选择服务提供者来处理请求,并支持按权重进行轮询,底层采用的是平滑加权轮询算法
- 最小活跃调用数:统计服务提供者当前正在处理的请求,下次请求过来交给活跃数最小的服务来处理
- 一致性哈希:相同参数的请求总是发送到同一个服务提供者
45、Dubbo是如何完成服务导出的
- 首先Dubbo会将程序员所使用的@DubboService 注解 或 @Server 注解进行解析的到程序员所定义的服务参数,包括定义的服务名、服务接口、服务超时时间、服务协议等等,得到一个ServiceBean
- 然后调用Service的export方法进行服务导出
- 然后将服务信息注册到注册中心,如果有多个协议,多个注册中心,那就将服务按单个协议,单个注册中心进行注册
- 将服务信息注册到注册中心后,还会绑定一些监听器,监听动态配置中心的变更
- 还会根据服务协议启动对应的Web容器或网络框架,比如Tomcat、Netty等
46、Dubbo是如何完成引用服务的
- 当程序员使用@Reference 注解来引入一个服务时,Dubbo会将注册和服务的信息解析出来,得到当前所引用的服务名、服务接口是什么
- 然后从注册中心进行查询服务信息,得到服务的提供者信息,并存在消费端的服务目录中
- 并绑定一些监听器来监听动态配置中心的变更
- 然后根据查询得到的服务提供者信息生成一个服务接口的代理对象,并放入Spring容器中作为Bean
47、Spring Cloud常用组件
- Eureka:注册中心
- Nacos:注册中心、配置中心
- Consul:注册中心、配置中心
- Spring Cloud Config:配置中心
- Feign/OpenFeign:RPC调用
- Kong:服务网关
- Zuul:服务网关
- Spring Cloud GateWay:服务网关
- Ribbon:负载均衡
- Spring Cloud Sleuth:链路追踪
- Zipkin:链路追踪
- Seata:分布式事务
- Dubbo:RPC调用
- Sentienl:服务熔断
- hystrix:服务熔断
48、Spring Cloud 和 Dubbo 的区别
Spring Cloud是一个微服务框架,提供了微服务领域中的很多功能组件,Dubbo 一开始是一个RPC调用框架,核心是解决服务间的调用问题,Spring Cloud 是一个大而全的框架,Dubbo 则更侧重于服务调用,所以Dubbo所提供的功能没有Spring Cloud 全面,但是Dubbo的服务调用性能比Spring Cloud 高,不过Spring Cloud 和Dubbo 并不是对立的,是可以结合起来一起使用的。
49、什么是服务雪崩?什么是服务限流?
- 当服务A调用服务B,服务B调用服务C,此时大量请求突然请求服务A,加入服务A本身能抗住这些请求,但是如果服务C扛不住,导致C请求堆积,从而服务B请求堆积,从而导致A服务不可用,这就是服务雪崩,解决方式就是服务降级和服务熔断
- 服务限流是指在高并发请求下,为了保护系统,可以对访问服务的请求进行数量上的限制,从而防止系统不被大量请求压垮,在秒杀中,限流是非常重要的
50、什么是服务熔断?什么是服务降级?区别是什么?
- 服务熔断是指,当服务A调用的某个服务B不可用时,上游A为了保证自己不受影响,从而不再调用服务B,直接返回一个结果,减轻服务A和服务B的压力,直到服务B恢复
- 服务降级是指,当发现系统压力过载时,可以通过关闭某个服务,或限流某个服务来减轻系统压力,这就是服务降级
相同点:
- 都是为了防止系统崩溃
- 都让用户体验到某也功能暂时不可用
不同点:熔断是下游服务故障触发的,降级是为了降低系统负载
51、BIO和NIO、AIO的区别
- BIO:同步阻塞IO,使用BIO读取数据时,线程会阻塞住,并且需要线程主动去查询是否有数据可读,并且需要处理完一个socket之后才能处理下一个socket
- NIO:同步非阻塞IO,使用NIO读取数据时,线程不会阻塞,但需要线程主动的去查询是否有IO事件
- AIO:也叫做NIO2.0,异步非阻塞IO,使用AIO读取数据时,线程不会阻塞,并且当有数据可读时会通知给线程,不需要线程主动去查询
52、零拷贝
使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的JVM堆内存进行Socket读写,JVM会直接将队内存Buffer拷贝一份到直接内存中,然后才能写入到Socket中。JVM对堆内存的数据是不能直接写入Socket中的。相比于堆外直接内存,消息在发送过程中多了一次缓存区的内存拷贝。
53、netty和Tomcat的区别
netty是一个基于NIO的异步网络通信框架,性能高,封装了原生NIO编码的复杂度,开发者可以直接使用Netty来开发高效的各种网络服务,并且编码简单
Tomcat是一个Web服务器,是一个servlet容器,基本上Tomcat内部只会运行Servlet程序,并处理Http请求,而Netty封装的是底层IO模型,关注的是网络数据的传输,而不关心具体的协议,可定制型高。
54、netty线程模型
- Netty抽象出两组线程池BossGroup和WorkerGroup,BossGroup专门负责接收客户端的连接,WorkerGroup专门负责网络的读写
- BossGroup和WorkGroup类型都是NioEventLoopGroup
- NioEventLoopGroup 相当于一个事件循环线程组,这个组中含有多个事件循环线程,每一个事件循环线程是NioEventLoop
- 每个NioEventLoop都有一个Selector,用于监听注册在其上的SocketChannel的网络通信
- 每个Boss NioEventLoop线程内部循环执行的步骤有3步
-
- 处理accept事件,与client建立连接,生成 NIOSocketChannel
- 将 NIOSocketChannel 注册到某个 worker NIOEventLoop上的 selector
- 处理任务队列的任务,即runAllTasks
- 每个worker NIOEventLoop线程循环执行的步骤
-
- 轮询注册到自己 selector 上的所有 NIOsocketChannel 的 read,write事件
- 处理I/O事件,即read、write事件,在对应 NIOSocketChannel处理业务
- runAllTasks 处理任务队列 TaskQueue 的任务,一些耗时的业务处理一般可以放入 TaskQueue 中慢慢处理,这样不影响数据在 pipeline 中的流动处理
- 每个 worker NIOEventLoop 处理 NIOSocketChannel 业务时,会使用 pipeline,管道中维护了很多 handler 处理器用来处理 channel 中的数据
55、netty的高性能体现在哪些方面
- NIO模型,用最少的资源做更多的事情
- 内存零拷贝,尽量减少不必要的内存拷贝,实现了更高效率的传输
- 内存池设计,申请的内存可以重复使用,只要指直接内存。内部实现使用一颗二叉查找树管理内存分配情况
- 串行化处理读写:避免使用锁带来的性能开销。即消息的处理尽可能再用户一个线程内完成,期间不尽兴线程切换,这样就避免了多线程竞争和锁同步锁。表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程进行运行,这种局部无锁化的串行线程设计相比一个队里对个工作线程模型性能更优
- 高性能序列化协议:支持protobuf等高性能序列化协议
- 高效并发编程的体现:volatile的大量、正确使用;CAS和原子类的广泛使用;线程安全容器的使用;通过读写锁提升并发性能
56、Redis的数据结构和应用场景
- 字符串:可以用来做简单的数据存储,可以换成json格式数据,Redis分布式锁的实现就是用了这种数据结构,还包括可以实现计数器,Session共享、分布式ID
- 哈希表:用来存储一些key-value对,更适合来存储对象
- 列表:Redis的列表通过命令的组合,既可以当做栈,也可以当做队列来使用,可以用来缓存类似微信工作号、微博等消息流数据
- 集合:和列表类似,
- 也可以存储多个元素,但是不可以重复,集合可以进行交集、并集、差集等操作,从而可以实现类似我和某人共同关注的人,朋友圈点赞等功能
- 有序集合:集合是无序的,有序集合可以设置顺序,可以用来实现排行榜功能
57、redis分布式锁的实现
- 首先利用setnx来保证:如果key不存在才能获取到锁,如果key存在,则获取不到锁;
- 然后来要利用lua脚本来保证多个redis的原子操作
- 同时还要考虑到锁过期,所以需要额外的一个看门狗定时任务来监听锁是否需要续约
- 同时还要考虑到redis节点挂掉后的情况,所以需要采用红锁的方式来同时向N/2 + 1 个节点申请锁,都申请到了才证明获取锁成功,这样就算其中某个redis节点挂了,锁也不能被其他客户端获取到
58、Redis主从复制的核心原理
Redis的主从复制是提高Redis的可靠性的有效措施,主从复制的流程如下:
- 集群启动时,主从库会先建立链接,为全量复制做准备
- 主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载,这个过程依赖于内存快照RDB
- 在主库将数据同步给从库的过程中,主库不会阻塞,仍然正常接收数据。但是这些请求中的写操作并没有记录到刚刚生成的RDB文件中。为了保证主从库的数据一致性,主库会在内存中专门用 replication buffer,记录RDB文件生成收到的没有的写操作
- 最后,也就是第三个阶段,主库会把第二阶段执行过程中新收到的命令,在发送给从库。具体的操作是,当主库完成RDB文件发送后,就会把此时 replocation buffer 中修改操作发送给从库,从库再执行到这些命令。这样一来,就实现同步了
- 后续主库和从库都可以处理客户端读操作,写操作只能交给主库处理,主库接收到写操作后。还会将写操作发送给从库,实现增量同步
59、缓存问题
- 缓存雪崩:如果缓存中某一时刻大批量热点数据同时过期,那么就可能导致大量请求直接访问MySQL了,解决办法就是在过期时间上加随机值
- 缓存击穿:和缓存雪崩类似,缓存雪崩是大批量热点数据失效,而缓存击穿是指某一个热点key突然失效,也导致了大量请求直接访问MySQL数据库,这就是缓存击穿,解决方案是设置热点key不过期
- 缓存穿透:加入某一时刻访问redis的大量key都在redis中不存在,那么也会给数据库造成压力,这就是缓存穿透,解决方案是使用布隆过滤器或缓存空值。布隆过滤器需要提前对key进行缓存,在使用时,如果它认为一个key不存在,那么这个key肯定不存在
60、Redis和MySQL如何保证数据一致性
- 先更新MySQL,再更新Redis,如果更新Redis失败,可能不一致
- 先删除Redis缓存数据,再更新MySQL,再次查询的时候再将数据添加到缓存中,这种方案能解决1方案的问题,但是在高并发下性能较低,而且仍然会出现数据不一致的问题,比如线程1删除了Redis中的数据,正在更新MySQL,此时另外一个查询再查询,那么就会把MySQL中老数据又插到Redis中
- 延时
- 双删,步骤是:先删除redis中的缓存数据,再更新MySQL,延迟几百毫秒再次删除Redis中缓存数据
61、Explain语句中各个字段分别表示什么
列名 | 描述 |
id | 查询语句中每出现一个SELECT关键字,MySQL就会为它分配一个唯一id值,某些子查询会被优化为join查询,那么出现的id会一样 |
select_type | SELECT关键字对应的那个查询类型 |
table | 表名 |
partitions | 匹配的分区信息 |
type | 针对单表的查询方式(全表扫描、索引) |
possible_keys | 可能用到的索引 |
key | 实际用到的索引 |
key_len | 实际使用的索引长度 |
ref | 当使用索引查询时,与索引类进行等值匹配的对象信息 |
rows | 预估的需要读取的行数 |
filtered | 某个表经过搜索条件过滤后剩余记录条数的百分比 |
extra | 一些额外的信息,比如排序等 |
总结:在进行索引优化的时候,主要看type列和extra列,type和extra列反应了查询语句使用索引的情况。在进行SQL优化的时候,type最好能优化到rang范围,ref最好
62、索引优化原则
- 使用全值匹配,尽量使用=号加索引
- 最佳左前缀法则:如果索引了多列,要遵守最佳左前缀法则。指的是查询冲索引的最左列开始并且不跳过索引中的列。
- 不要再索引列上做操作
- 查询时尽量使用覆盖索引,只查询索引列,少使用 select * 语句
- 使用 != 、<>、not in 、 not exists、is null、is not null 、like 以通配符开头可能无法使用索引
- 字符串不加单引号索引失效
63、Innodb是如何实现事务的
- Innodb在收到一个update语句后,会先根据条件找到数据存在的页,并将该页缓存到buffer pool中
- 执行update语句,修改Buffer Pool中的数据,也就是内存中的数据
- 针对update语句生成一个RedoLog对象,并存入 LogBuffer中
- 针对update语句生成一个undolog日志,用于事务回滚
- 如果事务提交,那么则把RedoLog对象进行持久化,后续还有其他机制将Buffer Pool中的数据页持久化到磁盘中
- 如果事务回滚,则利用undolog日志进行回滚
64、B树和B+树的区别,为什么MySQL要使用B+树
B树的特点:
- 节点排序
- 一个节点可以存多个元素,多个元素也排序了
B+树的特点:
- 拥有B树的特点
- 叶子节点之间有指针
- 非叶子节点的元素在叶子节点上都冗余了,也就是叶子节点中存储了所有的元素,并且排好顺序
MySQL索引使用的是B+树,因为索引是用来加速查询的,而B+树通过对数据进行排序所以是可以提高查询速度的,然后通过一个节点中可以存储多个元素,从而可以使得B+树的高度不会太高,在MySQL中一个Innodb页就是一个B+树节点,一个Innodb页默认16kb,所以一般情况下一颗两层的B+树可以存2000万行左右的数据,然后通过利用B+树叶子节点存储了所有数据并且进行了排序,并且叶子节点之间有指针,可以很好的支持全表扫描,范围查找SQL等。
65、MySQL锁有哪些?如何理解
按锁粒度分类:
- 行锁:锁某行数据,锁粒度最小,并发度高
- 表锁:锁整张表,锁粒度最大,并发度低
- 间隙锁:锁的是一个区间
还可以分为:
- 共享锁:也就是读锁,一个事务给某行数据加了读锁,其他事务可以读,但是不能写
- 排它锁:也就是写锁,一个事务给某行数据加了写锁,其他事务不能读,也不能写
还可以分为:
- 乐观锁:并不会真正的去锁某行记录,而是通过一个版本号来实现的
- 悲观锁:上面的行锁、表锁等都是悲观锁
66、MySQL慢查询如何优化
- 检查是否走了索引,如果没有则进行优化SQL利用索引
- 检查所利用的索引是否是最优索引(比如最左前缀法则、模糊查询使用通配符、或者使用了is null 等会使索引失效的语句)
- 检查所查字段是不是必须的,是否查询了过多字段和多余的数据
- 检查表中数据是否过多,是否应该进行分库分表
- 检查数据库实例是否不能满足当前需求,是否配置太低,是否可以适当增加资源
67、如何保证消息可靠性传输
对于消息的可靠性传输需要考虑三个方面:
1、生成者丢失了消息
- 首先消息分为普通消息(同步、异步、单向发送),定时延时消息,顺序消息(不能保证全局有序,只能保证同一个queue中有序),事务消息
- producer端防止消息发送失败,可以采用同步阻塞式的发送(也就是同步消息),同步的检查Brocker返回的状态是否持久化成功,发送超时或者失败,则会默认重试2次,但有可能发送重复投递
- 如果是异步发送消息,则会有一个回调接口,当brocker存储成功或失败的时候,也可以在这里根据返回状态来决定是否需要重试
2、Brocker端消息丢失
- RockerMQ一般都是先把消息写到PageCache中,然后再持久化到磁盘上,数据从pageCache刷新到磁盘的方式有两种,同步和异步
- 同步刷盘方式:消息写入内存的PageCache后,立即通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程完成后唤醒等待的线程,返回消息写入成功的状态,这种方式可以保证数据绝对安全,但是吞吐量不大
- 异步刷盘方式(默认):消息写入到内存的PageCache中,立刻给客户端返回成功,当PageCache中的消息堆积到一定的量时,触发一次写操作,将PageCache中的消息写入到磁盘中。这种方式吞吐量大,性能高,但是PageCache中的数据可能丢失,不能保证数据绝对安全
3、Consumer消息丢失
- consumer端默认是消息消费之后自动返回消费成功确认ACK,但是这时我们程序执行失败了,数据不就丢失了吗?
- 所以我们可以自动提交消费响应,设置为在代码中手动提交,只有真正消费成功之后再通知Brocker消费成功,然后更新消费唯一offset或者删除Brocker中的消息
68、TCP的三次握手和四次挥手
三次握手:
- 客户端向服务端发送一个SYN
- 服务端收到SYN后,给客户端发送一个SYN_ACK
- 客户端收到SYN_ACK之后,再给服务端发送一个ACK
四次挥手:
- 客户端发送FIN
- 服务端收到FIN之后,向客户端发送ACK,表示我接收到了断开连接的请求,客户端可以不发数据了,不过服务端这边可能还有数据处理
- 服务端处理完所有数据之后,向客户端发送FIN,表示服务端现在可以断开连接了
- 客户端收到服务端的FIN,向服务端发送ACK,表示客户端会断开连接了
69、Spring IOC和AOP理解
总所周知,Spring拥有两大特性:IOC和AOP。IOC,英文全称Inversion of Control,意为控制反转。AOP,英文全称Aspect-Oriented Programming,意为面向切面编程。
Spring核心容器的主要组件是Bean工厂(BeanFactory),Bean工厂使用控制反转(IOC)模式来降低程序代码之间的耦合度,并提供了面向切面编程(AOP)的实现。
控制反转(IOC)
控制反转,简单点说,就是创建对象的控制权,被反转到了Spring框架上。
通常,我们实例化一个对象时,都是使用类的构造方法来new出来一个对象,这个过程是由我们自己来控制的,而控制反转就把new对象的工作交给了Spring容器。
IOC的主要实现方式有两种:依赖查找、依赖注入。
依赖注入是一种更可取的方式。
那么什么是依赖查找和依赖注入的区别是什么?
依赖查找,主要是容器为组件提供一个回调接口和上下文环境。这样一来,组件就必须自己使用容器提供的API来查找资源和协作对象,控制反转体现在那些方法回调上,容器调用这些回调方法,从而应用代码获取到资源。
依赖注入,组件不做定位查询,只提供标准的Java方法让容器去决定依赖关系。容器全权负责组件的装配,把符合依赖关系的对象通过Java Bean属性或构造方法传递给需要的对象。
IOC容器:具有依赖注入功能的容器,可以创建对象的容器。IOC容器负责实例化、定位、配置应用程序中的对象并建立这些对象之间的依赖。
70、mybatis一级缓存与二级缓存
区别:一级缓存的作用域是一个SQLSession内;二级缓存作用域是针对mapper进行缓存
一级缓存:
- 第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从数据库查询用户信息。得到用户信息,将用户信息缓存到一级缓存中。
- 如果中间SQLSession去执行commit操作(执行插入、更新、删除),则会清空SQLSession的一级缓存,这样做的目的为了让缓存中存储的是最新信息,避免脏读。
- 第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有值,直接从缓存中获取用户信息。
二级缓存:
- 不管是不是相同的session,只要mapper的namespace相同,可能共享缓存,要求:如果开启了二级缓存,那么在关闭SQLSession后,才会把该SQLSession一级缓存中的数据添加到namespace的二级缓存中。
- 开启了二级缓存后,还需要将缓存的pojo实现Serializable接口,为了将缓存数据取出执行反序列化操作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中。