关于线程:
多线程是什么?
同时有多条线程同时执行任务,并行能够充分利用系统资源,提高执行效率。
并发能够提高执行效率吗
总体上不能,切换线程时会记录状态、信息等,有资源消耗,对线程本身可以,每个线程都有机会争夺时间片去执行。
讲讲并发、并行、串行、高并发
- 并发:在时间段内,线程数量远大于核心数量就会发生线程切换(原子性问题)。
- 并行:在同一时间点,多条线程同时执行任务,要求CPU是多核(可见性问题)。
- 串行:线程间一个一个执行(线程安全)。
- 高并发:发并发是一种概念,是针对系统而言的,指的是请求达到甚至超过了系统的承受范围上限。
什么是Java多线程同步
在多线程环境下,通过合适的机制、技术手段协调线程的执行,保证线程对资源的访问是原子、有序、其他线程可见的,例如可以通过synchronized加锁、atomic原子类、volatile关键字、等待唤醒机制wait()、notify()等。
Java中线程间的通信方式
- synchronized、wait、notify、notifyAll通过对象锁保证线程操作的有序性
- volatile关键字,变量的及时可见性、指令的有序性
- blockingQueue阻塞队列
- reetrantLock灵活的锁机制
- condition更灵活的控制线程的等待与唤醒
- callable和future
为什么wait, notify和notifyAll这些方法不在thread类里面?
因为wait、notify、notifyAll这些方法是基于对象锁实现的,由持有对象锁的线程来调用,如果通过Thread类对象加锁力度太大,基于Thread实例粒度太小。
如何调用wait方法的,是通过 if 还是 while?
看需求,在while中唤醒后会进行判断是否满足条件继续等待,在if中唤醒会直接执行下面的代码。
如何停止一个正在执行的线程
- stop,过期方法,强制关闭。
- interrupt,通知线程,线程自行决定。
有三个线程T1,T2,T3,如何保证顺序执行?
- join,让线程进入等待状态按顺序进行执行任务。
- Semaphore信号量
- CountDownLatch
- 顺序依次调用。
- 公平锁执行任务。
多线程中的伪共享
指的是并发对缓存行中数据进行读写操作,缓存一致性协议会导致缓存行失效,导致要走内存或磁盘引发的查询性能问题。解决方案:对齐填充,一个缓存行有64字节,一个变量独享一个缓存行,在1.8后可以在类中添加注解@contended,表示对数据填充。
线程安全是什么、如何解决
在多线程场景下,线程对公共资源的操作,没有保证原子性、有序性和变量的及时可见性,导致产生了意想不到的结果;解决:1. 保证原子性 2.变量及时可见性 3.指令的有序性
原子性问题的本质
线程数量远大于cpu核心数量,就会发生线程切换,如果线程的操作不是连贯的、可被其他线程干扰的,就会发生原子性问题。加锁
及时可见性问题的本质
操作系统cpu层面的三级缓存,其中L1级缓存独属于每个cpu,所以肯定会出现缓存不一致问题。缓存一致性协议
指令有序性问题本质
编译器为了提高执行效率通过Happen-Before做指令重排,单线程环境不会影响结果,但多线程环境下会出问题。内存屏障
++
操作符是不是线程安全的
不是,++操作非原子的,原子类、加锁的方式保证线程安全。
如何去支撑高并发场景
- 单次请求的响应速度快,SQL调优、缓存、接口优化
- 异步削峰
- 通过硬件方面
- 多节点,分布式、微服务架构层面
- 限流
Java语言创建线程的方式有几种?
- 4种:继承thread、实现runnable、callable、线程池
- 0种:Java没能力创建线程,start方法通过本地方法由JVM层面向系统申请资源开启线程。
线程的生命周期(Thread类)
- 新建状态 (new Thread)
- 运行状态 (t.start)
- 等待状态 (wait、join)
- 超时等待 (sleep(100)、join(100))
- 阻塞状态 (没有持有监视器锁,造成阻塞)
- 终止状态 (run方法执行结束)
为什么Java中线程的运行状态没有分为就绪、运行中?
避免无用的资源消耗,线程抢到时间片状态变为“运行中”,发生线程切换又要变为“就绪”,在高并发环境中消耗会很大。
Thread类中的start()和run()方法有什么区别?
start方法调用本地方法由JVM层请求资源,开启线程最后回调run方法。
如何唤醒一个阻塞的线程
- wait 可以通过notify、notifyAll
- Lock的lock方法可以通过unlock
- BlockQueue的take方法,取到值唤醒
- Thread.sleep、Object.wait,可以通过interrupt唤醒
- Semaphore
- CountDownLatch
线程池中的七大参数
- corePoolSize 核心线程池数量
- maximumPoolSize 最大线程池数量
- keepAliveTime 线程存活时间
- timeUnit 线程存活单位
- worKQueue 工作队列
- threadFactory 线程工厂
- handler 拒绝策略
线程池的工作流程
- 如果线程池数量小于核心线程池数量,创建线程执行任务,通过阻塞队列的take方法阻塞、并尝试获取任务。
- 如果线程池数量不小于核心线程池数量,那么新的任务会存储到阻塞队列中。
- 如果阻塞队列满了装不下了,会继续创建线程,知道最大线程池数量。
- 如果线程池到了最大数量,仍然有任务,拒绝策略生效,对任务进行拒绝。
线程池的好处?
连接池是享元设计模式的一种具体落地,核心思想在于连接复用,因为线程是一种资源密集型操作,频繁的创建和销毁非常影响系统性能,连接池有以下几点好处。
- 线程复用,避免频繁的创建、销毁线程
- 控制线程数量(核心数量、最大数量)
- 提供阻塞队列存储任务
- 提供拒绝策略
- 自定义线程工厂,区分业务
线程池的执行原理
- 线程新建:通过for自旋+cas的方式不断地去尝试创建Worker对象。
- 线程复用:通过while自旋的方式,不断地去处理任务,并且尝试从阻塞队列中获得任务,会根据当前线程数量,来决定当前的worker对象是否回收。
ThreadLocal是什么?
ThreadLocal是Java中线程的一个局部变量,为每个线程都提供一块空间,每个线程都可以独立操作自己的变量副本,互不干扰,1. 每个线程都可以存储自己的变量副本互不影响,2. 避免多线程并发操作同一共享资源造成数据安全问题,2. 实现上下文的资源传递,比如Mybatis的连接。
为什么Mybatis要用到ThreadLocal去存储SqlSession
实现连接复用,并且线程间无法共用同一个SqlSession,整个操作非原子的会出现线程安全问题,
另外一个SqlSession对应着一个连接,开启一个事务,所以也会对事务的隔离性造成影响。
ThreadLocal的实现原理了解吗?
每个线程中都存在一个ThreadLocal变量,ThreadLocal变量中都维护着自己的ThreadLocalMap存储着自己的变量副本,所以副本之间互不干扰,值得一提的是,ThreadLocalMap中是通过entry数组来实现的,每个entry对象的key为当前的ThreadLocal的引用地址,Value为变量副本。
ThreadLocal之间可以实现跨线程的访问、修改吗?
不可以,原因是每个Thread中都有自己的ThreadLocalMap,相互隔离,并且使用set、get方法的时候,会默认的去获取当前的Thread,然后获取Thread中ThreadLocal所以无法访问、修改;Thread中的属性InheribleThreadLocal,它的子线程会继承父线程的ThreadLocalMap,但是如果涉及到修改的话,子线程会先将副本赋值到自己的ThreadLocalMap中,再做更改。
ThreadLocal是如何解决Hash冲突的?
ThreadLocal中如果发生了Hash冲突,它是通过开放寻址法中的线性探测法来解决的,通过向后寻找entry为null的位置插入,除了这种方式应对Hash冲突,链地址法(HashMap)、再Hash法、建立公共溢出区。
锁升级机制
在Java1.6后,Java引入了锁升级机制,synchronized根据线程间的竞争激烈情况,选择不同的锁进行锁升级。
- 偏向锁:没有发生线程竞争时,对象会对线程记录标记为偏向锁,下次执行可以直接获取锁。
- 轻量级锁:多个线程竞争一个锁时,会升级为轻量级锁,会通过不断自旋+cas的方式去尝试获取锁,通过短时间的忙等待避免线程切换带来的消耗。
- 重量级锁:轻量级锁的长时间的自旋是比较耗费CPU资源的,所以当轻量级锁自旋到了一定次数,会升级为重量级锁,此时线程阻塞更节省资源。
自旋锁是什么?
通过不断循环的去做某件事,等待循环结束的时机,通过短时间的忙等待,避免线程阻塞频繁的切换线程上下文带来的消耗。
可重入锁是什么?
首先第一点,同一个线程可以多次的获取同一个锁,第二点:支持锁的嵌套,1. 单体架构的可重入锁:Java中的ReenTrantLock,2. 分布式架构中的可重入锁:Redis中Redission用Hash结构实现的分布锁。
死锁是什么?
死锁指的是多个线程同时访问多个加了互斥锁的公共资源,因为彼此都持有对方所需要的资源,并且都在等待对象执行结束释放资源,造成的相互阻塞状态,此时没有外力介入,那么他们会一直僵持下去。
如何避免N个线程同时访问N个互斥资源所造成的死锁
- 资源排序法:要求使用资源必须按照指定的顺序依次执行,可以避免死锁。
- 尝试锁定法:通过tryLock( )方法实现,当线程不能获取锁时,他会释放已经持有的锁。
关于J.U.C
J.U.C是什么?
并发包,并发编程。
什么是 Java 的并发编程?为什么需要并发编程?
并发编程指的是,用多线程来同时执行任务的技术,目的是可以充分利用计算机的多核资源,提高执行效率。
J.U.C中都包括什么?
- 线程安全集合,CurrentHashMap、CopyOnWriteArrayList
- Locks锁,ReenTrainLock、ReentrantReadWriteLock
- Atomic原子类,AtomicInteger、AtomicLong
- BlockingQueue阻塞队列、ArrayBlockingQueue、LinkedBlockingQueue
-
同步工具类,CountDownLatch、Semaphore
什么是 Java 的内存模型?它如何影响多线程程序的执行?
什么是JMM,定义了程序中变量的访问规则,核心概念有主内存和工作内存,主内存中存储了所有的变量数据,而工作内存,每个线程都有一个工作内存,用于存储变量副本,线程对变量的操作都要在工作内存中进行。
对多线程程序的影响有: 1.原子性问题,有一些复合操作比如i++,不是原子的在多线程环境中可能导致线程安全问题,可以通过同步加锁的方式保证原子性 2.可见性问题,线程对变量进行了修改,其他的线程没有及时收到通知,可能导致数据不一致的问题,可以使用volatile解决可见性问题。
请介绍 Java 中的 volatile 关键字的作用和实现原理。
作用有1.禁止指令重排序 2. 保证其他线程可见性
原理:添加了volatile关键字的变量,涉及读取和修改时,都要从内存中获取和同步(内存屏障)。
什么是 Java 中的锁?请比较 synchronized 和 ReentrantLock 的区别和适用场景。
锁是用于控制多线程并发访问共享资源的机制。
-
隐式锁底层jvm实现的;显示锁。
-
synchronized发生异常自动释放锁,ReenTrantLock不会。
-
ReenTrantLock可以响应中断抢锁,synchronized而不会。
-
非公平锁;通过参数可实现公平、非公平锁。
-
性能上:1.6之前有区别,1.6后synchronized引入了锁升级机制。
使用场景:隐式锁适用于简单的同步场景,而显示锁功能多更灵活,适合复杂的场景。
ReentrantLock多线程lock方法流程
只有一条线程通过乐观锁执行任务,线程会更改锁的状态记录当前占有锁的线程,其他没有抢到锁的线程,会通过addWaiter方法包装成一个Node节点,并被组织成一个双向链表,按照循序等待唤醒,执行的条件:1.当前链表节点的前节点是头节点,2.通过cas的方式将状态从0改成1。
AQS是什么?
抽象队列同步器,是一个抽象类,其中有很多的模板方法,帮助实现锁、同步工具。
将ReentrantLock中阻塞的线程唤醒,线程可以获取到锁吗?
具体要看线程所在的Node节点在双向链表的位置,如果当前节点的前节点是head头节点,那么它的状态是可以抢夺锁的状态,否则仍然自旋进入阻塞。
JUC 中的锁有哪些?它们的区别是什么?
- ReenTrantLock: 可重入锁,支持公平、非公平锁。
- ReenTrantReadWriterLock: 读写分离,适合读多些少。
- LockSupport: 底层的工具类,提供park、unpark方法阻塞、唤醒线程。
- Condition: 与Lock配合使用,实现复杂的等待、唤醒机制。
请介绍 Java 中的 Condition 接口的作用和使用方式。
作用于线程间的相互协作、通信,await()、signal(),相比于Object中的wait、notify,Condition中提供了更加强大的功能,比如说Condition
可以精确的唤醒哪些条件的线程。
什么是 Java 中的并发容器?请举例说明其使用方式和实现原理。
Java中的并发容器指的是应用在多线程并发环境中的容器,比如ConcurrentHashMap,相比于HashMap做了解决了并发安全的问题,比如说数据丢失问题、扩容的问题,ConcurrentHashMap在扩容上的性能也大大提高了,采用的是多线程辅助扩容。
解决数据丢失:当没有发生Hash冲突的时候,通过无锁cas的方式尝试添加数据,当发生哈希冲突的话,会使用synchronized代码块锁住当前哈希同的方式提高并发效率。
解决多线程扩容问题:首先当线程正在扩容时,会锁住原数组的哈希桶,扩容完毕后会更改节点到原数组上标记扩容完成。
什么是CAS?
比较和交换算法,属于一种乐观锁,核心在于,在执行某个操作之前,会比较当前值是否为期望值,如果是,则更新数据,否则不更新,在Java底层代码中通常与自旋配合,通过短时间的忙等待,避免因为频繁阻塞切换线程上下文带来的开销。
JUC 中的线程池有哪些类型?
FixedThreadPool:固定线程池、CachedThreadPool:缓存线程池、SingleThreadPool:单例线程池、ScheduledThreadPool:定时线程池等,以上是Executors线程池工厂提供的线程池,其实底层使用的还是ThreadPoolExecutor,ThreadPoolExceutor有七大参数,核心、最大线程数量,存活时间、单位,工作队列,线程工厂,拒绝策略,Executors不过是帮我们填好了参数、增加其他的功能。
JUC 中的 CountDownLatch 是什么?怎么使用?
是Java中J.U.C下的一个同步工具,作用是可以等待多个线程执行完毕后再开始执行某个操作,本质上就是一个计数器。
- 新建CountDownLatch对象,初始化默认计数值
- 每个线程执行完毕,调用方法递减计数器
- 主方法调用await方法进入阻塞,等待计数器归零被唤醒
请谈谈你对 Java 中的并发控制工具类(如 Semaphore、CyclicBarrier、CountDownLatch 等)的理解和实际应用经验。
JUC 中的 Semaphore 是什么?怎么使用?
Semaphore是一种信号量,可以限制同时访问特定资源的线程数量。
- 新增Semaphore对象,初始许可证数量。
- 当需要访问共享资源时需要获取许可证数量。
- 如果获取不到则阻塞知道获取到。
- 执行操作完共享逻辑,释放许可证。
JUC 中的 BlockingQueue 是什么?有哪些实现方式?
BlockingQueue支持在多线程环境中实现的生产者-消费者模式的阻塞队列。
- ArrayBlockingQueue,基于数组实现
- LinkedBlockingQueue,基于链表实现的有界、无界阻塞队列。
- PriorityBlockingQueue,基于堆实现的,可以根据元素的优先级进行排序。
JUC 中的 FutureTask 是什么?怎么使用?
用于接受线程异步执行结果的类,参数可以传递继承Runnable或者Callable接口的任务,通过get方法获取执行结果,如果获取不到则一直阻塞。
- 新增FutureTask,参数为实现了Callable的任务。
- 创建线程池,通过线程池执行futureTask对象。
- 调用futureTask的get方法,方法会阻塞获取执行结果。
JUC 中的 CyclicBarrier 是什么?怎么使用?
同步辅助工具,它允许一组线程相互等待,直到到达规定的数量,可以触发一段操作。
- 新增CyclicBarrier,初始化规定数量、执行操作。
- 没段线程完成操作后,进行await。
- 当await数量达到规定的数量,执行定义的操作。
Java 中的 CountDownLatch 和 CyclicBarrier 类的使用方式和实现原理。
- CountDownLatch是让一个或者多个线程等待其他线程,其他线程执行完毕才开始执行。原理:内部维护了一个计数器,调用await方法会阻塞,直到计数器为0。
- CyclicBarrier可以使所有的线程都达到一个公共屏障点,然后这些线程可以继续执行,与CountDownLatch不同的是,可以重置屏障点。原理:内部使用了ReenTraintLock与Condition实现的等待和唤醒。
JUC 中的 Phaser 是什么?怎么使用?
同步辅助工具,与CyclicBarrier的升级之处在于,Phaser可以动态的调整线程同步的参与者数量,更加灵活。
JUC 中的 ThreadLocal 是什么?有什么作用?
ThreadLocal他是线程类中的一个局部变量,他为每个线程都提供属于自己的空间,内部管理着ThreadLocalMap,各线程之间的变量副本互不干扰;作用:避免了多线程同时访问同一共享资源带来的数据安全问题。另外ThreadLocalMap内部使用过Entry数组来实现的,数组长度默认为16,得到key的hasCode与长度-1得到下标,找到位置生成Entry对象插入,Entry对象它的key值是当前的ThreadLocal,value值是副本变量,key值是弱引用的,虽然ThreadLocal也做了防止内存泄漏的操作,但是为了避免内存泄漏,可以说手动的用完副本删除掉;
JUC 中的 AtomicInteger 是什么?有什么作用?
AtomicInteger是java.util.concurrent包下提供的原子类,底层通过自旋 + cas无锁的方式保证数据安全,通过短时间的忙等待,避免频繁阻塞、频繁切换线程上下文造成的资源消耗。
JUC 中的 CopyOnWriteArrayList 是什么?有什么作用?
相当于线程安全的ArrayList,每次更改数据前都会复制一份副本,在副本上进行修改,修改完毕将引用指向新的数组,由于写操作不会改变原有数组,所以读操作可以在没有加锁的情况下执行,数和读多写少的场景。
JUC 中的 LockSupport 是什么?有什么作用?
提供了基本的线程阻塞、唤醒等功能,相较于wait、notify,LockSupport更加灵活,不需要应用在同步方法、同步代码块中,并且可以精确的唤醒某个线程,ReenTrantLock底层就运用了LockSupport中的park、unPark实现了线程的阻塞和唤醒。
JUC 中的 Exchanger 是什么?有什么作用?
JUC 中的 ForkJoinPool 是什么?有什么作用?
ConcurrentHashMap的初始化容量是多少,什么时候会扩容,如何扩容?
关于初始化容量:
默认为16,设置了初始化容量会向上取值最近的2的n次幂。
关于扩容的时机:
- 当某一个哈希桶中链表长度大于等于8,数组长度小于等于64会优先扩容(长度大于64会树化)。
- 当ConcurrentHashMap当前容量大于总容量*负载因子也会触发扩容,负载因子默认0.75。
- 使用putAll时容量不足会发生扩容。
如何扩容:
创建一个新数组容量为原先的2倍,采用多线程辅助扩容,通过分段迁移将旧数组中的元素迁移到新数组。
具体的扩容步骤
- 生成扩容标识戳,二进制16位一定是1,左移16位+2,赋值给SIZECTL(负数表示的是正在扩容,并记录参与扩容的线程数量;正数表示下次扩容的阈值)
- 计算出每个线程处理多少个哈希桶(最少处理16),新增数组,大小为原数组的2倍。
- 线程从后往前领取任务并执行迁移,迁移是锁住哈希桶,迁移完毕将头节点更改,包含新数组的引用、hash值为MOVED,表示该数据桶已经迁移完毕。
- 其他线程进行put操作时,如果发生Hash冲突,hash值为MOVED,通过cas的方式抢夺成功协助扩容,抢夺失败返回新的数组引用进行下一步插入操作。
- 最后一个完成迁移的线程,将原table的引用指向新table,并且更改SIZECTL扩容状态信息。
为什么扩容时要自旋的去插入数据呢?
因为数组此时可能正在扩容,并且没有完成扩容,所以需要阻塞,当哈希桶扩容完毕,需要重新获取新数组的位置,进行数据插入。
ConcurrentHashmap 的key,value是否可以为null。
不可以
- 从代码上:put方法接收到参数需要进行校验,如果key或者value为null,则会抛出nullPointException;get方法提层在拿到key值时先会从key获取hashCode进行扰动函数运算,由于key为null,会引发nullPointException。
- 从逻辑上:ConCurrentHashMap在多线程环境中,没有办法向HashMap那样通过containsKey判断是key值为null、还是key对应的值为null
ConcurrentHashMap的数据结构是怎么样的
ConcurrentHashMap在1.8中是通过数组+链表+红黑树实现的,具体是一个Node数组,如果发生Hash冲突,会使用链地址法在Node节点的next上新增Node节点形成链表,如果链表长度达到8且数组长度大于64,那么会从链表转化成红黑树来提高查询效率,当链表小于等于6时会从红黑树退化为链表,节省维护树结构的开销。
ConcurrentHashMap的put过程是怎样的?
- 首先ConcurrentHashMap对key的hashCode进行扰动函数运算,降低Hash冲突。
- 初始化Node数组,默认大小16。
- 判断数组索引位置是否为空,如果为空:使用cas的方式插入位置。
- 判断头节点的hash是否等于MOVED。
- 如果不为空:通过synchronized锁住头节点,使用尾插法形成单向链表。
- 根据链表长度决定是否需要扩容、转换成红黑树。
- 添加元素数量、检查是否需要扩容。
ConcurrentHashMap的get过程是怎样的?
- 首先对key值的hashCode进行扰动函数运算,降低Hash冲突概率。
- 根据计算出的 hash 值和当前数组的长度,确定元素在数组中的位置。
- 根据元素的hash值,如果与当前位置的hash相同,则直接返回val;如果hash值为负数,则通过红黑树寻找;否则遍历链表查询
ConcurrentHashMap是如何计算它的size大小的?
- 线程竞争不激烈的情况,直接用cas (baseCount+1)。
- 线程竞争激烈的情况,采用数组的方式来分段计数,来降低线程之间的竞争。
JUC 中1.8 的 ConcurrentHashMap为什么可以实现高并发?
- 数组+链表/红黑树
- 无锁更新,如果没有发生哈希冲突,则使用cas的方式插入数据。
- 多线程辅助扩容,ConcurrentHashMap的扩容数据转移操作由多个线程共同完成,利用扩容标识戳记录线程扩容状态,将数组分为不同的段,每个线程负责一段,提高数据扩容的效率。
ConcurrentHashMap使用什么技术来保证线程安全?
cas乐观锁+synchronized同步锁的方式保证了线程安全
- 数据的覆盖问题:如果没有发生哈希冲突,使用cas的方式尝试插入数据;如果发生哈希冲突,那么使用synchronized的方式覆盖、插入数据。
- 扩容问题:利用扩容标识戳,记录线程扩容状态,将数组分成不同段的任务,第一个线程创建新数组,每个线程领取任务,最后的线程全面检查一遍有没有遗漏在老数组中的数据。
ConcurrentHashMap迭代器是强一致性还是弱一致性?HashMap呢?
关于集合容器
HashMap的初始容量是多少
- 如果不传递初始参数的话,初始容量为16。
- 如果传递参数的话,初始容量为向上取2的n次幂,第一,方便扩容,第二,HashMap为了减少哈希冲突的概率,会对key的hashcode使用扰动函数运算,计算下标就要长度与hashCode进行位运算,容量而如果不取2的n次幂,极有可能使扰动函数失效。
HashMap容量为什么总是2的n次幂
- 方便扩容
- 为了减少Hash冲突,HashMap对key的hashcode进行了扰动函数,计算下标就要长度与hashCode进行位运算,如果不使用2的n次幂,会导致扰动函数失效。
HashMap是如何扩容的
当创建HashMap是可以设置扩容因子,如果不设置则默认为0.75,如果当前容器数量到达最大容量的0.75,那么会发生扩容,扩容为原先大小的2倍,由于容器大小为2的n次幂,所以扩容非常方便,原数据扩容后不是在原下标就是在原下标+扩容大小的位置上。
HashMap怎么解决Hash冲突的
- HashMap会对key的hashCode使用扰动函数,通过高低16的异或运算,降低发生hash冲突的几率。
- 如果发生hash冲突的话,HashMap会使用链地址法的方式解决,会在对应的哈希桶位置使用尾插法形成链表,如果链表长度大于8,并且数组长度大于64,那么链表会转换成为红黑树,来提高查询效率。
HashMap的达到什么条件会从链表、红黑树之间相互转换
当链表长度大于8且数组长度大于64时,会从链表转换为红黑树,来解决链表过长查效率低下的问题;当链表小于6的时侯,从红黑树转化成链表,解决写操作效率低的问题。
HashMap会造成什么并发问题?
- 因为覆盖导致的数据丢失
- 因为结构变化导致报错
ConcurrentModificationException
(并发修改异常)
关于JVM
JVM是什么?
JVM是java虚拟机,实现跨平台的重要基石,将class字节码解释成机器码给各个操作系统,主要负责将类加载到内存,为程序运行提供内存空间、垃圾回收等。
JVM中都包括什么?
- 类加载子系统(负责将类读取到内存中、对类校验、类加载)
- 运行时数据区(负责为程序运行提供空间、记录代码执行顺序)
- 程序执行引擎(负责将字节码解释成机器码、垃圾回收)
- 本地库接口(负责调用其他语言)
了解JVM内存模型吗?
- 方法区(负责存储类信息、常量池)
- 堆(负责存储对象信息)
- 方法栈(存储执行方法所需要的参数,局部变量、方法调用和返回等)
- 本地栈(存储native本地方法的调用)
- 计数器(负责记录代码执行行数,唯一不会内存溢出)
对JVM调优有了解吗?
JVM调优,因为内存溢出导致垃圾回收器激活,引发STW导致程序停顿,主要部分是堆内存,堆内存中采取分区分代思想,目的是减少每次GC的扫描范围,当年轻代eden、幸存者区空间不足时,引发小GC,当老年代空间不足时,引发FullGC ,调优是为了降低程序卡顿。
CMS如果fullGC频繁的话怎么办?
对象被存储到老年代有两种情况,可以从这两点入手:
1.对象过大,年轻代经过小GC依然存储不下,会存储到老年代。
· 尽量避免大对象的产生拆分成小对象
2.对象年代数大于15,存储老年代
· 根据情况调整年轻代比例,减缓年代数的增加
3.编写代码规范
· 不是所有对象都会被分配到堆中,逃逸算法证实逃逸的对象,可以经过标量替换分配到栈中。
· 四大引用概念区分不同对象的生命周期
· 避免内存泄漏,列如ThreadLocal
垃圾回收算法有了解吗?
- 标记清除法(标记存活的对象,将其余对象全部删除,致命缺点:产生内存碎片)
- 标记复制法(标记存活的对象,复制到一片空间中,清除除此空间外的对象,不会产生内存碎片,需要额外一片空间,适合年轻代)
- 标记整理法(标记存活的对象,向一端移动、整理,整理完毕清除其余空间,不会产生内存碎片,适合年老代,对象生命周期较长,需要整理的对象少,效率高)
怎么判断对象是否为垃圾呢?
- 引用计数法:通过对象被引用数量判断,如果引用数量为0则为垃圾,缺点:存在垃圾对象相互引用的情况,其他引用无法指向,这个两个对象就会一直存在于内存中。
- 可达性分析算法:如果对象无法通过Root对象直接或间接的访问到,则该对象为垃圾对象。
常用的GC收集器
- CMS: Concurrent Mark Sweep, 用于老年代的并发类的垃圾回收器【用户线程与垃圾回收线程可以同时执行】比较关注的是停顿时间。 --XX:+UseConcMarkSweepGC.
- G1:可以设置期望停顿时间,但不能太严格,MixedGC:(Young区和部分Old区的GC)。
- Parallel、Parallel Old: Java1.8中的默认GC,优先处理吞吐量。
GC收集器分类
- 并行类的收集器:parallel Scanvent、Parallel Old,吞吐量优先
- 并发类的收集器:CMS、G1,停顿时间小、适用于Web服务器
- 串行类的收集器:Serial和Seria Old,内存比较小、嵌入式的设备
G1与CMS的区别有什么
- CMS用于老年代的回收,G1GC可以用在老年代和年轻代。
- CMS上使用的是分代分区的思想,而G1GC保留了逻辑上的分代分区,物理上使用了
Region的方式对堆内存进行了划分。 - CMS基于标记清除算法,会产生内存碎片,G1基于标记整理算法,整体减少垃圾碎片的产生
- G1可以去设置期望的停顿时间,JVM会自己通过调整达到这个期望时间。
什么情况下使用G1呢?
- 堆内存中的数据超过了了50%
- 对象分配率或晋升率差异很大
- 期望有比较小的停顿时间(长于0.5 ~ 1秒)
- 内存比较大,大于6GB
关于G1GC的官方调优建议
- 不要设置年轻代的大小或者调整年轻代与老年代的比例,会影响到停顿时间(G1GC是可以自定义期望的停顿时间,所以JVM会自动调整年轻代的大小)。
- 可以尝试调整停顿时间,但是不能太过严格,因为会增加GC的次数。
- 对其他参数做出调整:堆内存使用的比例,触发并发的周期;
发生OOM如何进行调优
- 找到具体的堆溢出文件,根据堆内存相关信息,判断如何调优JVM。
- 分析GC的日志
如何观测内存使用情况
- jconsole:JDK中自带的可视化检测工具。可以查看java应用程序的运行情况、监控堆信息、永久区使用情况、类加载情况等,在命令行输入:jconsole。
- Arthas:采用命令交互模式,排查JVM关键问题的利器。
如何导出堆内存溢出文件
- 手动导出:jmap -dump:format=b,file=heap.hprof PID
- 发生堆OOM自动导出,JVM参数添加:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
所有的对象都会保存在堆内存中吗?
不是所有对象都会被分配到堆中,逃逸算法证实未逃逸的对象,可以经过标量替换分配到栈中。
如何查看当前JVM使用的是什么垃圾收集器
- jps -l jinfo -flag UseG1GC 17764
官方建议收集器
尽量使用JVM默认选择的垃圾回收器,如果无法满足使用需求,可以适当的调整堆内存的空间
- 如果程序的内存空间小于100MB,可以选择:-XX:+UseSerialGC.
- 如果程序是单线程的,并且没有停顿时间的要求,那么可以让JVM去自己选择或者自己去选择:-XX:+UseSerialGC.
- 如果应用程序没有停顿时间的要求,但是更加关注吞吐量的话可以让JVM自己选择或者自己去选择:-XX:+UseParallelGC.
- 如果有更多停顿时间的要求,那么可以选择:-XX:UseConcMarkSweepGC. 或者 -XX:+UseG1GC.
关于SpringBoot
SpringBoot是什么?
首先SpringBoot是基于Spring实现的一个脚手架,SpringBoot以约定大于配置的理念,帮助快速搭建、资源整合。
SpringBoot产生的原因
Spring配置重量级、大量默认配置、微服务
为什么要用SpringBoot?
- 快速搭建:约定大于配置,内嵌式服务
- 资源整合:自动装配,开箱即用
- 其他功能:Actuator 监控
Spring Boot中的特性
- @EnableAutoCofiguration 自动装配
- Starter 依赖 依赖于自动装配的计数
- Actuator 监控,提供了一些endponit,http、jmx形式去访问,健康信息,metrics信息
- Spring Boot CLI (命令行操作的功能,groovy脚本)
Spring中自动装配(完整的)思路是什么?
- 自动装配
- 条件装配
- 属性自动绑定
@EnableConfiguration是如何完成自动装配的
- 启动类@SpringBootApplication中包含注解@EnableAutoConfiguration
- @EnableAutoConfiguration中包含着@Import注解
- 类中实现了ImportSelector接口
- 首先根据META_INF中加载META_INF中的spring.factories,文件中指向了要加载的配置类
- 筛选后进行条件装配,最终返回String数组。
第三方扩展框架也同样会根据SPI机制做出相应的文件。
SpringBoot的启动流程
- 通过启动类上的main方法启动,首先对SpringApplication初始化,调用run方法
- 初始化了初始化器和监听器,随后调用run方法
- 先后经过监听器、初始化器,进入到refresh方法,此方法是Spring实现IOC的核心方法,方法中有个重要的组件 后置处理器,它的职责主要是解析注解,根据注解去生成BeanDefinition对象,比如Bean、AOP代理类、starter依赖的BeanDefinition都是在这里生成的,随后BeanFactory会依据BeanDefinition去创建对象,。
- 之后监听器、runner回调,启动成功。
总结的来说,SpringBoot的核心是在Spring的基础之上去初始化的,同时也提供了很多周期方法。
Spring类加载的过程讲一讲
- 首先SpringBoot由唯一的启动类启动,Spring会默认扫描启动类所在包。
- 通过XML或者注解的形式,加载组件类,如果是XML则通过IO读取,注解通过Java反射技术判断是否为组件类。
- 通过BeanDefinitionBuilder构建出BeanDefinition对象,对象中包含了类的属性,最终存储到BeanDefinitionMap中。
- 由BeanFactory根据BeanDefinition创建Bean对象。
- 将Bean对象保存到ApplicationContext中,可以通过getBean()、依赖注入的方式使用。
说说SpringBoot中都有什么注解
- @SpringBootApplication(唯一的启动类,组合注解)
- @SpringConfiguration(表示启动类的同时也是配置类)
- @EnableConfiguration(包含注解@import,引入自动装配类)
- @ConponentScan(组件扫描的范围是启动类所在包)
SpringBoot核心配置文件有哪几个?他们的区别是什么?
有两个,application、bootstrap .properties/yml,两者都可以重写三方jar包的配置,最大的区别在读取的时机,bootstrap的读取时机较早,一般用在注册中心中,读取远程配置中心中的配置。
核心配置文件的启动时机
Actuator 监控 health 健康检测
Metrics: /actuator/metrics
- JVM(垃圾收集器、内存、堆)
- 系统(运行时间、平均负载、处理器的信息)
- 线程池信息
- tomcat对话信息
注意事项:
<!-- 添加依赖 -->
<dependency><groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# 开启端点
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
metrics:
enabled: true
SpringBoot项目中的application.yml用法
- @Value: 映射配置文件中的属性值。
- @ConfigurationProperty: 将配置文件中的属性映射到对象上。
在Spring中我们将类型交给SpringIOC管理的方式有哪些?
- 基于xml配置文件<bean>
- 基于注解@Controller、Service、Mapper、Configuration、Repository等
- @Bean
- @Import注解
- FactoryBean接口【getObject()】
关于import的三种注入方式
- @Import注解
- 实现ImportSelect重写方法,返回值数组包含多个类的全路径名称,import此类
- 实现ImportBeanDefinitionRegistrar,为我们提供了IOC注册器,可以直接将对象添加其中
SpringBoot自动装配原理
通过条件化配置和组件扫描完成
条件化配置:通过@Confitional系列注解,根据条件读取配置类,配置类中配置组件扫描的路径。
组件扫描:通过注解@ComponentScan指定的包下,Spring都会去扫描,并根据注解生成Bean添加到IOC容器中。
SpringBoot热部署
- 放开配置 setting->build->compiler √ Build project automatically
- ctrl+shift+alt+/ -> registry -> √ compiler.automake.allow.when.app.running
- 添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> </configuration> </plugin> </plugins> </build>
SpringBoot中的异常处理
- 在resource/Templates中添加一个error.html即可,给个静态页面。
- 使用ExceptionHandler,通常配合SpringMVC的全局异常处理类一同使用。
SpringBoot中的单元测试
- @Runwith @SpringBootTest 直接帮我们把IOC容器加载过来
- @Autowired 属性
Spring发展的各个阶段
- 1.x:采用XML的方式定义 Bean。
- 2.5:Component、Controller、RequestMapping。
- 3.x:去配置化,更加便捷的加载Bean到容器中。
- 4.x:@Conditional
没有Spring Boot之前的开发步骤
- 创建一个项目结构(maven/gradle)
- spring的依赖、springMVC、servlet api 的依赖
- web.xml,DispatchServlet
- 启动一个SpringMVC的配置,,Dispatch-Servlet.xml
- 创建一个Controller 发布一个http请求
- 发布到 jsp/servlet容器
总结: 配置重量级、重复的模板配置、微服务的萌芽阶段需要轻量级的工具
自定义Banner
网站:http://patorjk.com/software/taag
自定义banner后,在resources目录下创建 banner.txt文件
SpringBoot整合Filter
SpringBoot整合Listener
SpringBoot如何实现文件上传
Spring整合前端文件FreeMarker
返回的是一个ftl文件,最终
关于数据库MySQL
SQL语句的类型
- DQL:数据查询语句
- DML:数据控制语句,添加、修改、写
- DDL:数据结构化操作,create、alter等
- DCL:数据权限控制语句
- TCL:事务相关,比如commit、rollback
MySQL的基本架构
- 连接层:负责连接以及连接相关的配置
- 服务层 (连接层、解析器、预处理、优化器、执行器)
· 解析器:负责解析语句,查看是否符合SQL语法,预处理,防止SQL注入,通过减少解析次数来提升性能
· 优化器:对SQL语句进行优化
· 执行器:与存储引擎交互,调用存储引擎的API,返回结果
- 存储引擎层
SQL的执行流程
InnoDB的表空间
- 独立表空间:每个表都有一个单独文件
- 系统表空间:系统文件都存储在一起,耦合度很高
- 通用表空间:指定哪些表达到哪个通用表空间
- undo log表空间:回滚日志
- 临时表表空间:临时表只有当前会话可以访问
InnoDB内存磁盘结构
- 行 格式
- page 页
- extent 区:每个区有最少64个页
- segement 段
内存缓存结构 Buffer Pool
选择 “page页” 进行内存与磁盘交互的,page页是内存磁盘交互的最小单位,原因是如果选择 “行”,200条数据要交互200次,选择“extent区”会导致内存浪费。
预读机制
线性预读:默认56页,最大64页(一个extent区)
随机预读:
内存数据淘汰机制
LRU变种:
- 当访问页面数据,缓存到BufferPool中,都会放到OldList头部 包括预读页。
- 当old页中数据被访问,才会放到newList头部。
- newList中数据会随着新加入的数据而后移,慢慢会被淘汰。
Redo Log 重做日志
因为缓存页一个页是16K,修改一条数据,整个页都需要进行修改,非常消耗性能,所以InnoDB是通过异步的方式去缓存数据的,异步的方式可能就会导致数据丢失,数据的丢失、回复,就是通过Redo Log日志来完成的,而为了做到追求性能的极致,提供了log buffer的概念,通过配置选择性能或者数据一致性。
存储引擎是什么?都有什么存储引擎
由服务端的执行器来调用
InnoDB(数据一致性高,5.5后默认存储引擎)、MyISAM(适合读多写少,无法保证数据一致性但性能高)、Memory(性能最高,但无法保证数据一致性)
联合索引的原理
首先会根据最左边的字段进行排序,如果想的话才会比较第二个字段,因底层的数据结构B+树肯定是要对数据进行一个排序,这也是为什么设计、使用索引要符合左前缀原则的原因。
什么是回表,什么是索引覆盖
什么是索引下推
索引下推解决的问题是多个条件中的后部分条件没有索引,按照传统的方式会直接返回多条数据给服务端,服务端再根据条件进行一个过滤,过滤后根据索引信息去主键索引中获取数据;那么如果使用了索引下推,可以在存储引擎中直接筛选,筛选后去主键索引中查询,避免了多次回表查询。
索引是不是越多越好,我们应该如何使用索引?
- 索引这个数据结构也是需要占用磁盘空间的
- 如果索引上的数据涉及到更改,索引结构也同样要变化,会影响修改效率
1.不能使用离散度太高的字段作为索引,2.联合索引符合左前缀原则
组合索引a,b,c 查询条件 c=10 and a=1 and b>=30,是否能用到索引?
不会用到索引,因为创建索引的时候,会先通过a进行排序,如果a字段相等才会使用b进行排序,所以查询条件 c=10 在前面,那么没有基于c的索引,故索引不生效。
另外索引失效的场景:
- 没有符合最左原则
- 第一个条件是范围查询,也会导致第二个索引失效
- 类型存在转换、隐式转换,排序的方式不一样
- 运算
总来的说,就是看当前索引的查询顺序,与前的建立索引的拍寻顺序是不是一致的,无序就失效
同时另外走不走索引,优化器说的算
我们主键一般建议趋势递增的原因在哪里?
原因还是在底层索引的数据结构B+Tree,原因是底层的树结构,是一定要将数据排序的,那如果随便在中间添加一条数据,那么极有可能会导致树结构重构,而自增的方式,只会导致数据页的增加,不会重构。这也是不建议用UUID的原因,1.字段太大 2.随机性导致树结构重构
在做InnoDB表结构的时候,一些大字段,比如varchar(5000),会独立生成一个附表为什么?
虽然可能不经常大字段进行一个查询,但是主键索引上默认会存储这个字段,导致每条数据的数据量都非常的大,可能会导致底层树结构高度增加,影响查询效率,所以通常会单独进行存储。
我们使用InnoDB的索引为什么要用B+Tree的数据结构
- 相比于其他树结构,B+Tree能够以相同的高度存储更多的数据,每个非叶子节点上只存储了索引,而数据统一存储在叶子节点上,所以非叶子节点就能够存储更多的索引了,相同的数据量下,树的高度低,查询效率快
- 稳定性,总共就查询3个数据页
- 灵活有序性,数据页之间通过双向链表维护,数据页中的每行数据通过单向链表连接,支持范围查询
三层的B+Tree,能够支撑多大的数据量
按照默认页大小,一个页存储16k,每个索引16byte,可以支撑千万级别的数据量,所以保障查询性能单表数据量一般建议不超过2000万。
了解三大范式吗?
三大范式是数据表设计的常用规范,可以有效地保持数据表整洁、避免数据冗余
- 第一范式:所有的字段都应当是原子的,即不可分割的。
- 第二范式:在第一范式的基础上,非主键字段必须完全依赖于主键字段,不能依赖于部分主键字段(组合主键的情况下)。
- 第三范式:在第二范式的基础上,非主键字段不能存在传递依赖关系
因为三大范式会将表拆分的特别零散,查询时经常要使用复杂的表关联查询,所以时常也会牺牲一些磁盘空间,做出一些反范式设计。
索引是什么?
索引是一种数据结构,相当于字典中的目录,可以帮助我们减少底层IO查询次数,提高查询效率,同时索引也会占用额外的内存空间,并且对索引列进行写操作时,速度会变慢,因为涉及到修改索引数据结构。
执行计划的分析
- select_type索引的查询类型:all 表示全表查询,最少要达到range级别,否则就需要调优。
- possible_keys:可能用到的索引。
- key:实际用到的索引。
- ref:索引的引用关系
- rows:预估会读取的数据量。
- Extra:User index (索引覆盖)、Using index condition(索引下推)、Using filefort(需要将数据拿到内存中进行排序、Using index for group by(group用到了索引优化)
优化器的相关的优化
- count:count(*) ~= count(1) > count(字段) 因为count(字段)会判断字段是否为空
- limit:深分页的问题,首先确保查询的条件有添加索引,1. 可以先确认id的位置,在基于id的位置按照顺序来查找(基于索引查询);2. 通过子查询查询到范围的id,再通过内关联操作进行关联(减少回表查询)。可以考虑缓存、ES等方式,也可以功能上做出一些限制。
- order by:尽量去走索引,如果不走索引,会将数据读取到内存中进行排序
- group by:基于索引,单表匹配符合最左原则
慢日志查询优化
- 数据库层面优化:1. 表结构是否合理,包括字段类型、存储引擎;2. 是否建立合适的索引,是否充分进行索引覆盖,是否用到优化器的相关优化,比如count、limit、order by、group by
- 硬件上:1.更高的磁盘、cpu、内存 2.集群,负载均衡的把压力分摊到各个节点
事务间的隔离并发安全问题
- 脏读:指的是当前事务可以读取到其他事务还有提交的数据。
- 不可重复读:指的是当前事务先后读取一条数据的结果不一样。
- 幻读:指的是当前事务按照某一条件先后读取的数量不一致。
非锁定一致性读取:MVCC
锁定一致性读取:LBCC
- 意向锁:为了避免锁冲突、提高锁的检查效率,加锁是都会对表加上意向锁。
- 记录所:在索引树的节点添加记录锁,表示对改记录加锁
- 间隙锁:在索引树的节点添加间隙锁,表示对当前节点到上一节点这一区间区间内的数据无法添加、删除,但是可以修改。解决了幻读问题
- 临键锁:在索引树的节点添加临键锁,表示当前节点以及当前节点到上一节点区间都会加锁,其实就是记录所+间隙锁。
BinLog是什么?
BinLog是一个二进制的文件,作用有两个,主从复制和数据恢复,文件中采用存储命令+数据的方式对数据做记录,默认也是开启的。
Binlog与Redo Log的区别
- Binlog是Mysql服务层的,不管换成哪个存储引擎,Binlog日志都会记录。redolog是InnoDB中的,是为了保证持久性和一致性的。
- Binlog是一个二进制的文件,采用追加写的方式,做主从复制,redolog是一个物理日志,记录着准确的数据更改时间,采用覆盖写的方式,短时间的数据恢复。
- 更新数据时记录到redo log,提交事务时记录到Binlog
两者都有存在的必要,并且要保证事务一致性,要么全部写成功,要么全部写失败,否则会造成主从数据不一致问题。
三大日志
- InnoDB中的二个事务日志
Redolog:因为缓存机制Buffer pool的存在,交互的最小单位是page页,每页默认有16k文件,如果修改一条数据那就要更新1页的数据,所以非常消耗资源,InnoDB采取了异步持久化的方式,为了防止数据丢失,会先将数据记录在redolog中,两个好处:1.保证数据一致性,防止数据丢失 2.一条数据一条数据更新,速度更快。3.不会占用太多空间,采用覆盖写的方式,写前会刷脏。
Undolog:记录了每次提交后的数据,会自动生成事务id,以及上一数据版本的指向,主要用作数据回滚,同时MVCC也用到了undolog,配合readview视图读,完成了非锁定一致性。
- 服务器曾的日志
binlog:二进制的日志,通过记录命令+行数据的方式,可以去主从同步、大数据恢复
如何实现主从复制,读写分离
- 创建数据同步的用户
- 设置从库为只读 set GLOBAL read_only = 1;
- 创建主从关系 CHANGE MASTER TO...
- 开启主从同步 start replica; -- 开启主从同步、查看从库信息show replica status \G
主从集群的必要两个条件:
1. 实例id,server_id不能相同
2. 主库必须开启binlog,从库可以不开启,但如果这个从库想成为另外一个实例的数据源,也就是将数据同步给另一个库,那就要开启
主从同步方式
异步同步:主库开启异步线程去dump binlog日志文件传送给从库,不会等待数据同步的结果,性能高,但可能发生数据不一致的问题
半同步:就是主库必须等待从库接收到binlog、并记录后返回一定数量的响应,主库才允许提交事务,数量可以设定,可以保证数据一致性,但性能较差。
主从一致性不同步、同步慢问题问题
- 网络延迟:
- 主库负载很高:
- 大事务:
- 从库机器配置跟不上:
都有什么索引?
按数据结构来讲
- B+Tree索引
- Hash索引
按照存储形式来讲
- 聚簇索引
- 非聚簇索引
按照类型来讲
- 主键索引
- 非空索引
- 唯一索引
- 普通索引
- 组合索引
- 全文索引
怎么创建索引
- 创建表时
- create index index_name on table(column)
- alter table table_name add index index_name on column
索引失效的场景
- 条件数据类型存在隐式转换
- in后判断条件过多
- like左模糊查询
- 使用or连接条件要保证条件字段全部添加索引
- 条件中使用聚合函数
什么是索引覆盖、回表查询
索引覆盖指的是:所有要查询的字段都可以通过索引来获得,回表扫描指的是:索要查询的字段无法直接通过非聚簇索引获取,要经过二次查询。
事务四大特性
原子、一致、持久、隔离
事务的隔离性有了解吗
事物的隔离级别指的是,事务之间的对彼此的影响程度,一般:读未提交、读已提交、可重复读、串行化。
事务之间是怎么做到隔离的
通过读已提交、可重复读通过MVCC技术实现,串行化通过间隙锁实现
什么是MVCC
多版本并发控制,通过undo log日志实现,每次事务提交后都会存储到undo log日志中,包含事务ID、上一版本ID,多条记录构成一个版本链。
如何实现读已提交、可重复读的?
通过MVCC技术、Read View 视图读,视图读主要是为了判断当前事务可以读取版本链中的哪个版本的数据, 在事务开启时生成read view数据,主要包含 当前最小活跃ID、最大ID、所有活跃中的ID,与版本链进行比对,选取可以读取的数据。
关于Redis
Redis是什么?
Redis是一个基于内存的非关系型数据库,采用K、V结构存储数据,通常被用作分布式缓存提高查询效率,保护关系型数据库,也可以用来计数、分布式锁,布隆过滤器,还可以充当消息队列,但Redis他是AP分布式事务模型,优先保证系统可用性,能用但不推荐。
Redis为什么快?
- 基于内存,无需磁盘交互
- 命令执行是单线程的 因为基于内存的,线程的切换反而会影响查询速度
- Redis本身是一个k-v结构,类似HashMap,查询性能接近O(1)
- 底层的数据结构,比如说跳表,sds(以空间换取时间)
什么是跳表
跳表是一种实现有序集合的数据结构,以空间换取时间,将一个链表分成了很多段,每段都有一个节点标记,查询的时间先扫描节点,如果节点中没有想要的数据,则跳到下一段的节点继续查看。
Redis如何做到高可用的?
- 快,能够承受更多的访问量。
- 完善的内存管理,缓存过期、淘汰、持久化机制。
- 主从、sentinel、cluster集群
Redis中的数据结构有了解吗?
- String:值存储一个字符串,可以实现缓存、计数、分布式锁。
- List:值存储一个有序可重复集合,适合存储一对多关系的数据,可以实现消息队列,能用但是不建议,AP选型容易消息丢失。
- Hash:值存储的是Map,适合存储属性经常变动的对象,可以去实现可重入的分布式锁。
- Set:值存储一个无序不可重复的集合,全部数字时有序,适合并集、交集、差集的场景。
- ZSet:Value部分存储的是可排序的Set集合,通过分值决定排名顺序,适合排行榜场景。
- BitMap:值存储一个二进制位的集合,可以实现签到、布隆过滤器。
什么是分布式锁?
用于在分布式的系统中防止业务重复执行的锁,像是synchronized、lock只能保证本服务的同步执行,因为状态的标识符作用域只是本服务中,所以实现分布式锁肯定要通过一个第三方的公共标识来实现,比如Redis,Redis有两种方式实现分布式锁,RedisTemplate通过String实现分布式锁,Redission通过Hash类型实现可重入的分布式锁。
自己设计一个锁如何设计
- 有一个公共可见的占用标识(状态)
- 要保证其他线程的可见性(volatile)
- 线程间不能同时更改标识(CAS)
Redisstion实现的锁与RedisTemplate实现的锁的区别
- 底层实现的数据类型不同:Redission使用Hash,RedisTemplate使用的是String。
- 锁类型不同:Redission实现的是可重入锁而RedisTemplate实现的是普通锁同一线程会阻塞。
- 超时机制:Redission内部通过看门狗机制,确保业务执行完毕在释放锁,RedisTemplate不能保证释放锁时业务一定执行完毕。
Redission是如何实现可重入锁的
Redission底层是实现分布式锁的数据类型是Hash,通过field存储当前的线程id,由于是分布式系统所以线程id是由提前生成的UUID加上线程id组成的,value存储的是锁的数量,如果key存在、field存在,则抢锁成功锁数量+1,这段操作也同样是通过lua脚本来执行的,不会出现线程安全问题。
netty中时间轮的实现原理
时间轮的由一个循环的数组构成,每个位置都代表一个时间段,时间轮上维护一个指针,指针不断旋转表示时间的流失,任务根据时间被分配到时间轮的上,当指针指向任务的槽位且则执行对应的轮数为0任务。
watchDog看门狗机制
通过时间轮+递归的方式实现了任务未结束时锁的自动续期,在时间轮提交任务,10秒后执行,通过lua脚本判断锁是否存在,如果存在对锁延期30秒并递归,不存在则取消续期返回失败。
Redis如何处理Hash冲突的?
链地址法,头插法,原因1:头插快不需要遍历链表,2:单线程没有并发问题
Redis中的持久化机制
- RDB 快照的方式持久化数据
优势:1.rdb是一个非常紧凑的二进制文件,便于备份、数据恢复 2.避免与磁盘频繁交互,通过子进程负责持久化,不会影响性能; 缺点:1.数据安全性问题,可能丢失最后一段时间数据 2.经常子进程去持久化数据,可能对CPU造成一定的压力
- AOF Append Only File 文件指令追加的方式,可以调整指令追加的策略,在性能与数据安全之间做一个抉择
优势:1.数据安全性高, 2.文件可读性高 3.不小心执行flushall可以做数据恢复; 缺点:1.数据恢复慢,但4.0后不成问题,因为有了重写机制。2.需要频繁与磁盘交互,并发量比较高,占用内存
- RDB+AOF,为了提升AOF的重写性能以及AOF文件大小
重写机制
由于AOF文件会不断的追加命令导致文件特别的大,所以会对文件进行压缩,压缩成RDB文件,重写的时机可以有两个配置决定:1. 文件到达指定大小,进行重写。 2.当前文件相比上次重写后增长100%,会触发重写。
重写的过程
当触发重写的条件后,会生成新的AOF文件来代替旧的AOF文件,具体的过程是遍历Redis中的所有的键,获取到值,然后用一条命令来代替之前AOF中的多条命令。
命令有两种方式: 主线 、 子进程
RDB触发快照的时机
- 配置触发——比如60秒修改1000个key则触发RDB持久化
- shutdown服务正常关闭——防止数据丢失
- flushall清空数据的指令——备份一个空的RDB文件
- save 主动保存,会阻塞其他线程——目的是保证数据一致性
- bgsave 子进程去做,可能同步的不是最新数据,但不会阻塞其他线程
Redis主从:
写入主库中的数据会同步到从库,但从库写入的数据不会同步到主库,从库也可以选择只读。
- 从库作为数据备份,发生故障导致数据丢失,可以做数据恢复
- 负载均衡,分担节点之间的压力
- 高可用
sentinel哨兵
监控和通知节点个节点,实现故障自动转移,支持集群高可用
- 发现故障:使用心跳机制检测master是否存活,在一定时间内没有收到有效的恢复,那么会通知其他的sentinel,如果半数以上的哨兵都认为服务不可用,进行故障转移。
- 故障转移:需要半数以上的sentinel去进行选举,将一个slave升级为master。
脑裂问题
由于网络波动master与其他的sentinel暂时断开了连接,这时其余的sentinel超过半数,他们会选举出一个新的master,进行写操作,那么当网络恢复时,其中一个master要转变成slave,去master同步数据,就造成了数据丢失的问题,这个问题无法解决。
为什么哨兵要单数不要偶数
原因是故障转移的选举需要超过半数,也就输如果有三台机器分别部署了一主一从一哨兵,那么最多允许一台服务宕机,如果有四台机器分别部署了一主一从一哨兵,仍然最多允许一台机器宕机,所以没有作用。
cluster集群
提供了分片的功能,将数据分散到不同的节点上,就算某个节点出现了故障,依然可以保证部分功能的使用。Redis集群是通过虚拟槽进行数据分区的,共有16384虚拟槽,虚拟槽与真实的master节点关联,通过对key进行位运算,决定分配在哪个master上,这样做的好处是key值与虚拟槽的关系始终是不变的,所以就不需要再次计算,但我们可以调整master负责虚拟槽的范围。
为什么虚拟槽的大小是16384
- 现实中如果想增加虚拟槽数量,那我只能认为当前的空间存储不下这么多数据了,所以才要对数据做一个更详细的划分,那势必要增加Master数量,所以每次心跳包的传输数量也会大大提高,会造成更多的资源消耗。
- Redis的作者就不建议Master超过1000个节点,所以在这个前提下16384个虚拟槽是完全够用的。
主从之间如何保证数据一致性
如果从库允许写数据那没有办法保证数据一致性
主库同步到从库:
- 主从之间会建立连接,包括地址、端口等信息
- 节点与主建立连接会发送同步的指令,主节点根据传递来的master_repid/offest判断是否是第一次建立连接,第一次连接到主节点,会采用全量同步,
全量同步
master与slave连接成功,slave会发送数据同步请求,将主节点的id、offset给大master,master会拿到传递来的id与本身的id进行比对,不一致则说明是第一次连接,采用全量同步,master执行bgsave命令生成RDB文件,并且将RDB文件给到slave,同时把master_repid、offest给到slave,生成期间也会有新的指令,会临时存储在内存中,
增量同步
slave携带master_repid、offset发送同步请求,master通过对别master_repid发现已经全量同步过了,然后会通过offset偏移量来确定还有那些数据没有同步。
Redis中的过期删除策略
定时删除:通过Redis的定时任务,默认100ms执行一次,只是扫描专门存储会过期的键值对,每次会以哈希桶为单位进行查找数据,默认查找20个(当前的哈希桶要全部查找),将过期数据进行删除,如果当前删除的占比超过默认的10%,那么怀疑当前有大量的数据,那么重复之前的查找删除操作,循环16次后,会有个时间的检测,如果超过时则直接返回。——为了实现时间和空间的平衡
惰性删除:每次取数据时会查看数据是否过期,如果过期则删除数据,返回null。
Redis中的定时任务-serverCron
- 清理过期的键值对
- 更新服务器中的各类统计信息,比如时间、内存占用、数据库占用情况
- 关闭、清理连接失败的客户端
- 尝试进行持久化操作
Redis数据淘汰策略
淘汰策略是迫不得已的去删除数据,
对数据取样,不会扫描所有的值,将样本中最不常用的值存入淘汰池(16个)数组,淘汰池满了的话,实行末尾淘汰。
LRU:最久没有使用 时间
LFU:最不常使用 次数,时效性的问题
Redis的数据丢失场景 AP
- 持久化 默认RDB会有时间上一个区间的丢失,AOF默认也会有1S的丢失
- 淘汰策略
- 主从切换(主从同步数据采用异步)
- sentinel脑裂,会有两个master,当网络恢复,其中一个会转换成slave并清空自己的数据
Redis与DB的数据一致性问题
不适合存储敏感数据 因为操作DB、Redis不是原子的 另外本身就是AP
强一致性:加锁保证操作的原子性,延时双删——但是会影响查询的性能,使用Redis的初衷就是提高查询速度。
最终一致性:通过Redis的缓存过期策略,保障数据的最终一致性。
导致出现缓存不一致问题的场景
- 刚更新数据库未更新/删除缓存时,服务宕机。
- 更新数据库、更新缓存本身不是一个原子性操作,数据安全问题。
- 延时双删策略,有一段时间的数据不一致。
缓存雪崩、穿透、击穿问题
缓存雪崩:由于Redis宕机或者大量缓存在同一时间失效导致的
1.集群高可用
2.设置随机的过期时间
3.加锁查询数据并缓存——影响效率,不符合初衷
缓存穿透:
1.缓存null
2.布隆过滤器过滤
3.运维人员封ip
缓存击穿问题:
1.热点数据不过期
2.分布式锁查询数据并缓存
慢查询
slowlog-log-slower-than 10000
1. 由于指令导致查询缓慢的,比如keys *、hgetall keys *
2.对象太大,可以拆分成小对象
Redis阻塞
1.要记录好业务日志,做好降级,通过报警系统通知有阻塞
2.导致的原因:
(1)网络阻塞
(2)执行消耗时间长
(3)数据结构不合理
(4)AOF持久化
Redis中的全局命令
- exists(判断key是否存在)
- type
- expire(设置过期时间,保障最终一致性)
- flashall
- del
- Keys(生产环境不可用,单线程会卡死)
阐述Redis的主要特性和优势
- 快
- 业务场景多
- 高科用
关于消息队列
消息队列是什么?
消息队列是一种异步通信的组件,实现业务之间解耦,通过异步执行帮助提高接口的响应时间,提升系统的吞吐量,此外还可以在流量的高峰期对流量进行削峰填谷——包含以下功能:
有哪些使用场景?
- 业务解耦(A调用B,B执行失败不影响A成功)
- 异步通信(提高响应速度、系统吞吐量)
- 流量削峰(将消息存储,根据消费者能力消费信息)
- 分布式事务
RocketMQ的概述
RocketMQ是一款分布式消息队列中间件,最初由阿里巴巴开发,现在是apache基金会的顶级项目,承载了淘宝的双十一流量,有以下关键特性
- 单一队列百万消息的堆积能力
- 分布式高可用的架构
- 支持推push、拉pull两种消费方式(kafka的区别)
- 支持消息有序
- 支持分布式事务
RocketMQ的优势
- 有序性,可以实现全局有序、局部有序
- 支持18个级别的延迟消息
- 支持Tag标签过滤消息
- 支持重复消费
RocketMQ由哪些角色组成,每个角色作用和特点是什么
- NameServer(相当于注册中心,主要用于管理broker、producer、consumer的路由信息,对客户端进行心跳检测,NameServer本身无节点的,可以集群部署)
- broker(担任消息中转的角色,负责存储消息、转发消息,管理着topic,master读写,slave读)
- producer(消息生产者,定时从NameServer中拉取消息,与Broker建立长连接,通过负载均衡自动选择Broker集群队列进行发送消息)
- consumer(消息消费者,去broker中拉取消息、执行逻辑、移动偏移量,以组的方式出现)
NameServer 注册中心
namespace是什么?为什么不用nacos
namespace相当于注册中心,主要维护brocker的原数据和路由信息,producer与consumer可以通过namespace找到broker,实现消息的发送与消费,提供了统一入口,至于为什么不适用nacos,nacos的是CP模型,namespace的本身是AP模型,无状态节点,性能更高更符合RocketMQ的需求。
Namespace的主要流程
1.启动Netty Server 监听9876端口
2.定时任务更新路由新信息,剔除不健康的Broker,10一次 120秒剔除
Producer消息生产者
Producer发送消息的多种方式
- 同步发送:发送消息后会等待发送结果。
- 异步发送:发送消息后不会等待发送结果,但会通过回调函数获取方法。
- 单向发送:发送消息后就不管结果了。
Producer的高可用、高可靠设计
- 多种消息发送方式
- 消息重发机制
Producer发送消息的流程
1.通过 Topic主题 ,获取到Broker路由信息
2.通过路由信息选择一个Queue
3.构建消息、发送
Broker 存储端
Broker的启动流程
1.对远程服务器、消息存储服务启动,并监听
2.注册将自己的信息注册到namespace
3.定时向namespace发送心跳包
Broker高可用、高可靠设计
- 高可用:集群设计。
- 高可靠:1.brokerRole主从复制方式:同步复制(SYNC_MASTER)、异步复制(ASYNC_MASTER) 2.flushDiskType刷盘策略:SYNC_FLUSH(同步刷盘)、ASYNC_FLUSH(异步刷盘)
如何保存数据?刷盘方式、主从数据复制
- 同步刷盘:只有当消息落盘之后,才返回成功的响应,不过也要看生产者的发送方式。
- 异步刷盘:接收到消息,直接返回成功的响应,然后异步去落盘。
- 同步复制:当master消息落盘后,会发送给所属的slave,其中一个slave落盘成功,结束。
- 异步复制:采用异步的方式做slave数据同步。
broker如何处理拉取请求的?
通过PullMessageProcessor监听拉取的请求,根据请求获取消费者组、topic并选择对应的queue,对偏移量的数据读取,如果有则返回给consumer,没有的话会对请求hold,进行长轮询处理。另外,broker启动时会开启一个线程,默认每隔5秒都会检验一次是否有新消息到达,如果满足条件唤醒则请求获取消息。
存储消息的流程
接受消息:由Netty Server接受请求,默认异步去由SendMessageProcessor处理,采用顺序写在CommitLog中添加数据,期间会通过加锁来保证操作的原子性。
分发消息:在启动时会启动一个叫ReputMessageService的线程,主要就是通过一个自旋完成消息的分发,将CommitLog中数据的索引添加到Queue中。
RocketMQ Broker中的消息被消费后会立即删除吗?
不会,RocketMQ中消费端consumer的消费流程是这样的:1.会到根据topic到queque拉取消息,得到消息后执行消费逻辑,消费完毕后,发送消费成功的响应,移动消息偏移量。
Broker中过期文件的删除
通过定时任务,延时60s启动,每10秒种检验一次过期文件并删除,包括CommitLog以及Queue,在凌晨四点钟或者磁盘不足时进行判断,以文件最后一次写入的信息为准超过72H则删除,并且连续删除文件有间隔时间,以免对处理业务性能造成影响。
为什么在Broker的分片中,Queue的数量不同
- 集群模式下创建topic,在所有集群的broker里面,所有的Queue的数量是相同的。
- broker模式下创建的topic,在集群不同的broker里面,Queue的数量可以不同。
Broker把自己的信息注册到哪个NameServer上?
会向所有的NameServer注册信息,因为RocketMQ的NameServer是无状态的节点,也就是NameServer之间毫无关联,这样做的原因主要是提高性能。
消息会堆积吗?什么时候清理过期消息?
如果消息消费的速度跟不上消息生产的速度,那肯定会发生消息堆积,每日的凌晨4点会进行判断或者内存不足时会立即判断,如果Queue中消息过了72小时,就会清除。
Consumer 消费端
consumer的高可用、高可靠设计
- 高可用设计:Master可读可写,Slave可读
- 高可靠机制:消费消息失败,触发消费重试机制(广播消费机制没有),重试默认达到16次,消息会被丢尽死信队列。
消费消息的模式
集群:多个消费者负载均衡的负责topic中的queue,queue与消费者之间是一对一或者多对一的关系,每个queque只能被一个消费者消费,不会重复消费。
广播:topic中的每个queue,消费者都会消费一遍,会重复消费消息。
顺序消息
- 全局有序:设置一个生产者、一个queue、一个消费者来保证全局消息的有序性,但性能差。
- 局部有序:多个生产者、多个queue、多个消费者,但每一个生产者的消息只会发送到一个queue中,并且只由同一个消费者来消费,可以保证局部的消息有序性。
消息重复消费
消息重复消费这个情况,要解决的是在集群消费模式下的场景:
有可能是消息生产者为了保证消息成功发送,重复发送了消息,可以在消息发送成功后通过数据表或者redis做一个标识避免重复发送,第二是在borker中,虽然通过偏移量来管理消息,但在某些情况下依然会重复消费,比如消费者拉取消息、执行业务后,还没来得及更改偏移量就挂掉了,所以也要在真正执行业务前去查看业务表唯一的字段比如MsgId、OrderId是否已经存在。
关于消费者要注意的问题
- 消费者组中的消费者数量,尽量要小于、等于订阅的topic中queue的数量。
- 一个组中的消费者应该订阅同一个topic,业务清晰。
追问:当消费负载均衡consumer和queue不对等的时候会发生什么
同一个topic下的consumer与queue,集群模式下consumer与queue是一对N的关系,也就是说,如果4个queue,consumer有5个的话,则有一个consumer会消费不到消息。
rocketMQ的消息堆积如何处理
为什么消息堆积?
- Bug,改代码 / consumer消费的速度跟不上producer生产速度。
- 优化消费逻辑,减少consumer的rt响应时间。
- 增加consumer
如果Consumer和Queue不对等,上线了多台也在短时间内无法消费完堆积的消息怎么办?
调整增加queue的数量,可以提高并发能力,但同时会对master节点造成压力。
增加Master,做数据分片,分担单节点压力,进一步提高并发能力
为什么要主动拉取消息而不使用事件监听方式?
主从、集群高可用
RocketMQ的核心概念-分片
通过topic存储实现水平扩展
- Topic时Broker中的逻辑组织形式,而主题下还有队列的概念。
- 一个topic是收发一种类型的消息,可以实现业务消息的隔离。
RocketMQ如何做负载均衡?
NameServer、Broker主从,如果想进一步提升处理的效率,可以多master做数据分片。
任何一台Broker突然宕机了怎么办?
如果是slave,那么暂时服务功能上没有影响,会增加消息消费读的压力,如果宕机的是master,那在功能上可读不可写,需要手动将slave升级成master
其他问题
RocketMQ的启动流程
- NameServer启动,监听端口9876等待服务连接获取信息,开启延时定时任务5秒后执行,每10秒检查一次brokerLiveTable中,如果断开连接120秒剔除。
- broker启动,创建client、server,server监听端口10911,向NameServer注册所有的broker信息,并通过心跳机制,开启定时任务30秒更新一次信息。
- 创建topic分片
RockeMQ的工作流程
-
生产者发送消息,选择master中topic的一个queue写入。
-
broker接收到消息文件实际存储在CommitLog文件中,拿到最大偏移量,继续往后写,queue中存储的是位置信息,主从数据同步。
-
消费者消费消息,消费者会通过负载均衡算法,负责一个或者多个队列的消费
-
消费者提交偏移量,存储在consumerOffset.json文件
使用过程中存在什么问题?如何解决的
消息丢失、重复消费问题
首先消息队列有三部分,分别是生产者、存储端、消费端,保证在每一部分都不丢失就可以了。
- 生产者:同步发送消息的方式,要确认消息成功发送并确认返回结果。
- 存储段:同步刷盘,当消息持久化成功后,才会返回成功响应。
- 消费者:在消息消费成功后,做出标记,防止未消费或者重复消费(记录表中)。
RocketMQ的设计理念
- NameServer的极简设计。
- Broker的高可用、多种刷盘、主从复制方式、高效的IO存储机制。
- Producer的多种发送方式以及消息重发机制。
- Consumer的消费失败重试机制。
- 暴露设计缺陷,开发人员消费端去重。
消息的类型
- 普通消息
- 延迟消息
- 顺序消息
- 事务半消息
高吞吐量下如何优化生产者和消费者的性能?
- 关于生产者:1.同步异步单向 2.批量发送
- 关于消费者:1.保证正常消费、Bug 2.优化消费逻辑,提高响应时间 3.增加consumer 4. 涉及存储端的调整 增加 queue的数量 会增加master压力 增加master
再说说RocketMQ 是如何保证数据的高容错性的?
通过producer、broker、consumer高可靠性保障的
Producer: 1. 多种消息发送方式 2. 发送失败重试机制
Broker:1.刷盘策略 2.主从复制策略 3.集群部署
Consumer: 1.消息失败的重试机制
RocketMQ四大组件的连接流程
- NameServer启动后,监听端口,等待broker、producer、consumer连接,做路由控制,彼此会建立长连接,Broker每隔30s发送心跳包检测,确保路由服务信息有效。
- NameServer接收到心跳包的信息,发送给producer消费组,消费者 负载均衡的向topic中队列发送消息。
RocketMQ在分布式事务支持这块机制的底层原理?
RocketMQ提供了半事务消息这个机制:
- 半消息发送到Broker。
- Brocker对本消息进行存储,但不会直接给consumer消费。
- 会执行本地的事务,如果本地事务执行成功,则返回commit,将消息push给consumer。
- 执行失败返回rollback,则将消息删除。
- 如果超时没有结果,会触发回查机制,决定commit或rollback
看过RocketMQ 的源码没有。如果看过,说说你对RocketMQ 源码的理解?
大量的异步操作。
- brocker对请求统一做异步处理
- 刷盘机制的异步处理
- 主从节点数据复制的异步处理
- producer发送消息的异步处理
如果让你来动手实现一个分布式消息中间件,整体架构你会如何设计实现?
- 存储端如何存储消息
- 生产端如何发送消息
- 消费端如何获取消息
堆积时间过长消息超时了?
堆积的消息会不会进死信队列?
如果消息消费失败的话,会有重复消费的机制,默认16次,如果16次都没有消费成功,消息会被扔到死信队列中,防止影响其他消息消费。
关于Spring
能说一说Spring吗?
Spring是一个资源整合的框架,是Spring家族的基石,Spring通过底层的两大核心IOC、AOP完成资源整合的,IOC指的是控制反转,将对象的控制管理权从我们手中移交给Spring容器,它可以更好的对外提供服务,提高对象的利用率,降低系统资源消耗,AOP指的是面向切面编程思想,在程序运行时对核心业务功能进行增强扩展的同时,实现核心业务代码与非核心业务代码的解耦,提高程序扩展性可内聚性。
Spring如何完成组件类加载的
Spring可以通过XML/注解的方式获取组件类,XML:IO流读取xml文件,经过处理得到包含类的属性BeanDefinition对象,存储到Map中,注解:通过Java反射计数通过字节码对象得到类上的注解,符合条件生成BeanDefinition对象,存储到Map中,由BeanFactory的实现工厂根据BeanDefinition创建出对象,最终存储到IOC容器ApplicationContext中,可以通过getBean()取出,或者依赖注入使用。
Spring中的注解知道多少
@Controller、@RestController、@Service、@repostory、@Configuration、@Component、@Bean、@Autowired、@Scope、@Lazy、@PostConstruct、@PreDestroy
依赖注入的方式
属性注入(在属性上添加注解@Autowried,代码整洁)
构造方法注入(通过构造方法自动注入属性,在对象创建时就已经完成初始化)
Setter方法注入(通过Setter方法自动注入属性,可以更改依赖属性)
Spring框架中都用到了什么设计模式?
- 单例模式(Bean默认是单例的)
- 简单工厂模式(BeanFactory)
- 代理模式(AOP的动态代理)
- 建造模式(通过BeanDefinitionBuilder构建BeanDefinition对象)
- 适配器模式(HandlerAdapter)
- 方法模板模式(JdbcTemplate,其中包含访问数据库的模板方法)
说说Spring AOP,以及具体的应用
Spring AOP是Spring中的一个核心模块,是一种面向切面的编程思想,不需要修改代码就可以对核心业务功能进行扩展的,底层使用了动态代理,AOP中有几个重要概念,切面、切入点、连接点、通知类型,可以通过XML或者注解的形式,对核心功能进行增强的同时实现核心业务代码与非核心业务代码的分离解耦。
关于SpringMVC
说说你对SpringMVC的理解
SpringMVC是一个Web层的框架,是MVC架构设计的一个落地实现,将复杂的问题简单化,分而治之,通过内部组件相互配合,处理请求的接受和响应。
SpringMVC中的常用注解
@RequestMapping、@PostMapping、@GetMapping、@PutMapping、@PatchMapping、@DeleteMapping、@RequestBody、@RequestParam、@RequetBody、@PathVarible、@RequestPath、@RestControllerAdvice
SpringMVC的处理请求流程你了解吗?
WebRequest->Filter->DispatcherServlet->HandlerMapping->HandlerAdapter->HandlerIntercepter->Controller->
关于Mybatis
Mybatis是什么?
是一个优秀持久层框架,Mybatis可以通过注解或者XML的方式对数据库增删改查操作,封装了繁杂的JDBC操作。
有什么优势?
- 实现了Java代码与Sql语句分离。
- 便于DBA进行SQL调优
- 支持动态标签,语句更灵活
- 将查询结果映射到实体类
- 提供缓存机制
- 可扩展性,比如引入PageHelper实现物理分页
简单描述一下Mybatis的工作原理
动态标签都有什么,能说说执行原理吗?
- if
- choose、when、otherwise
- trim
- foreach
原理:通过ognl表达式解析动态标签中的条件是否成立,参数通过mapper接口中的方法参数获取,根据判断动态的生成语句。
如果查询语句涉及到一对多、多对一关系怎么解决?
一对多:collection标签
多对一:association标签
详细说说缓存机制
MyBatis存在一级、二级缓存,一级缓存是基于SqlSession的,一个事务共享一个SqlSession,如果想要做到跨SqlSession缓存,就要用到基于namespace级别的二级缓存,当SqlSession缓存完毕后、会缓存到二级缓存中,查询顺序:二级缓存->一级缓存->数据库,可以通过@CacheNameSpace开启二级缓存。
说一说Mybatis都用到了什么设计模式
- 建造模式:SqlSessionFactoryBuilder,构建SqlSessionFactory对象
- 简单工厂模式:SqlSessionFactory,创建SqlSession对象
- 代理模式:为@Mapper注解描述的接口创建动态代理对象
- 享元模式:连接池druid,HikaryCP
- 模板方法模式:SqlSessionTemplate
- 组合模式:动态Sql进行拼接
关于nacos
什么是注册中心?
注册中心提供了哪些功能?
服务的注册中心是如何选型的
说说你对nacos的理解
nacos如果来检测服务状态
nacos注册中心有哪些常用配置
关于dubbo
关于sentinel
什么是限流
为什么要限流