参考博文:2024最强秋招八股文(精简、纯手打)
参考博文:2024年 Java 面试八股文(20w字)
参考博文:一文讲完Java常用设计模式(全23种)
文章目录
- 一、java基础篇
- 二、多线程&并发篇
- A.线程状态:
- B.线程终止:
- 1.Java中实现多线程有几种方法?
- 2.线程池核心的参数?
- 3.线程池拒绝策略?
- 4.什么是线程安全?怎么实现线程安全?
- 5.线程的三大特性?
- 6.Threadlocal原理
- 7.sleep()和wait()的区别
- 8.为什么wait, notify 和 notifyAll这些方法不在thread类里面?
- 9.volatile原理
- 10.Synchronized锁原理?
- 11.能说下Synchronized的效率真的很低吗?
- 12.能说下Synchronized的升级过程吗?
- 13.synchronized和volatile比较
- 14.能说下Synchronized和Lock的区别吗?
- 15. Java中的锁优化的方法
- 16.如何根据 CPU 核心数设计线程池线程数量
- 17.yield()和join()区别
- 18.有三个线程T1,T2,T3,如何保证顺序执行?
- 19.什么是JMM内存模型?为什么需要JMM?
- 20. i++为什么不是线程安全的?
- 21.同步锁的几种方式(锁对象):
- 22.用户线程和守护线程:
- 23.什么是CAS锁,有什么缺点?
- 24.什么是AQS及工作原理?
- 25.J.U.C之并发工具类
- 三、JVM篇
- 四、Mysql数据库
- 五、spring框架
- 六、SpringCloud微服务
- 七、redis篇
- 八、分布式事务 分布式锁
- 九、MQ消息中间件
- 十、设计模式
- 数据结构与算法
- 网路编程(拓展)
- linux系列(拓展)
- 分库分表(拓展)
- 数据库设计规范(拓展)
一、java基础篇
1.重写重载区别?
重载:在类中创建多个方法,方法名相同,参数及定义不同,返回值也可以不同
重写:子类对父类允许访问的方法实现进行重新编写,返回值和参数不可改变
2.说一下你理解的多态?
同一个行为具有多个不同表现形式或形态的能力
3.== 和 equals 的区别 ?
==:基本类型比较的是值是否相同,引用类型比较堆内存地址是否相同
equals:引用类型默认情况下比较地址,也可重写equals方法用来比较值是否相同
4.ArrayList和LinkedList的区别
ArratList的底层使用动态数组,默认容量为10,当元素数量到达容量时,生成一个新的数组,大小为前一次的1.5倍,然后将原来的数组copy过来;因为数组在内存中是连续的地址,所以ArrayList查找数据更快,由于扩容机制添加数据效率更低
LinkedList的底层使用链表,没有扩容机制;LinkedList在查找数据时需要从头遍历,所以查找慢,但是添加数据效率更高
5.如何保证ArrayList的线程安全?
(1)使用collentions.synchronizedList()方法为ArrayList加锁
(2)使用Vector,Vector底层与Arraylist相同,但是每个方法都由synchronized修饰,速度很慢
(3)使用juc下的CopyOnWriterArrayList,该类实现了读操作不加锁,写操作时为list创建一个副本,期间其它线程读取的都是原本list,写操作都在副本中进行,写入完成后,再将指针指向副本。
6.String、StringBuffer、StringBuilder的区别
String 由 char[] 数组构成,使用了 final 修饰,对 String 进行改变时每次都会新生成一个 String 对象,然后把指针指向新的引用对象。
StringBuffer可变并且线程安全
StringBuiler可变但线程不安全。
操作少量字符数据用 String;单线程操作大量数据用 StringBuilder;多线程操作大量数据StringBuffer
7.深拷贝和浅拷贝
浅拷贝:浅拷贝只复制某个对象的引用,而不复制对象本身,新旧对象还是共享同一块内存
深拷贝:深拷贝会创造一个相同的对象,新对象和原对象不共享内存,修改新对象不会改变原对象。
8.cookie和session区别
- cookie数据存放在客户的浏览器上,session数据放在服务器上
- cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,如果主要考虑到安全应当使用session
- session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,如果主要考虑到减轻服务器性能方面,应当使用COOKIE
- 单个cookie在客户端的限制是3K,就是说一个站点在客户端存放的COOKIE不能3K。
- 将登陆信息等重要信息存放为SESSION;其他信息如果需要保留,可以放在COOKIE中
- 当客户端禁用 cookie 时将无法使用 cookie
- 在存储的数据量方面:session 能够存储任意的java 对象,cookie 只能存储 String 类型的对象
9.HashMap数据结构
在JDK1.7中,HashMap数据结构为数组+链表;JDK1.8之后增加了数组+链表+红黑树变换,如果链表的长度超过了 8且数组长度最小要达到64 ,那么链表将转化为红黑树;链表长度低于6,就把红黑树转回链表; 初始化 当创建一个 HashMap 实例时,它会初始化一个默认大小的数组(默认为16),每个数组元素是一个链表。
9.1 HashMap底层实现原理?
- put过程
计算键的哈希值→定位数组索引→处理哈希冲突→维护结构(链表 / 红黑树转换)。
插入数据:对key键hash对数组长度个数取余找到对应下标的位置
如果当前位置上有元素,判断key是否一致,如果一致覆盖
如果不一致,加入该元素的next下一个节点,形成链表结构
如果链表长度超过8个就把链表结构改变成红黑树结构(Node->TreeNode)
如果没有元素,就直接存储到该数组对应下标的位置
扩容:检查当前map的容量和元素个数,如果元素个数超过阈值(容量*扩容因子(默认0.75))
如果超过阈值就开始扩容,扩容为原来两倍,要对其中的元素key键重新hash取余找到节点
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
通过hash()计算出key的hash值,然后调用putVal()执行put操作
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
1、判断数组是否为空,是则进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
2、根据hash值求出数组下标(n-1&hash),并判断该下标是否有元素,没有则直接放入该下标
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
3、如果有值,则判断该下标的对象和要存储的对象是否相等,
先判断hash值是否相等,再判断key是否相等,如果是同一个对象,则直接覆盖并返回
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
4、如果不是同一个对象则判断是否为树节点对象,是就直接添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
5、既不是同一个对象,又不是树节点则找到链表的尾部插入,判断链表长度是否需要树化。如果key相同则直接覆盖
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
6、最后判断hashmap的size是否达到阈值,进行扩容处理
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
- get 原理
通过哈希值定位桶→遍历桶中的节点(链表 / 红黑树)→匹配键并返回值。
1.计算 key 的 hash 值,根据 hash 值找到对应数组下标。
2.如果该位置上没有元素,则返回 null
3.如果该位置上有元素,判断对应位置上的第一个node是否满足条件,如果满足条件,直接返回
4.如果不满足条件,判断当前node是否是最后一个,如果是,说明不存在key,则返回null
5.如果不是最后一个,判断该元素类型是否是红黑树,如果是红黑树,则使用红黑树的方式获取数据
6.如果不是红黑树,遍历链表是否有满足条件的,如果有,直接返回,否则返回null
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods.
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
// 设置一些局部变量
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 首先获取hashmap中的数组和长度,并判断是否为空,如果为空,返回null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 获取key对应的下标对应的链表对象, 并比较第一个是否满足条件
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
// 第一个如果满足条件,则直接返回
return first;
// 判断当前对象是否是最后一个,如果是,说明没有找到对应的key的值
if ((e = first.next) != null) {
// 如果不为空,判断是否是红黑树,如果是红黑树,使用红黑树获取对应key的值
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 如果不是红黑树, 遍历链表,找到对应hash和key的node对象
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
9.2 hashmap为什么采用两倍扩容?
以二次幂展开,容器的元素要么保持原来的索引,要么以二次幂的偏移量出现在新表中。也就是说hashmap采用2倍扩容,可以尽可能的减少元素位置的移动。
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
9.3 hashmap负载因子为什么是0.75?
时间和空间的权衡 值越高空间开销小但是增加了查找开销
As a general rule, the default load factor (.75) offers a good
* tradeoff between time and space costs. Higher values decrease the
* space overhead but increase the lookup cost
9.4 hashmap初始长度为什么是16?
服务于从Key映射到index的Hash算法,在性能和内存的使用上取平衡,实现一个尽量均匀分布的Hash函数,选取16,是通过位运算的方法进行求取的。
9.5 HashMap 和 Hashtable 的区别
- HashMap 允许 key 和 value 为 null,Hashtable 不允许。
- HashMap 的默认初始容量为 16,Hashtable 为 11。
- HashMap 的扩容为原来的 2 倍,Hashtable 的扩容为原来的 2 倍加 1。
- HashMap 是非线程安全的,Hashtable是线程安全的。
- HashMap 的 hash 值重新计算过,Hashtable 直接使用 hashCode。
- HashMap 去掉了 Hashtable 中的 contains 方法。
- HashMap 继承自 AbstractMap 类,Hashtable 继承自 Dictionary 类。
9.6 为什么HashMap会产生死循环?
为什么HashMap会产生死循环?
HashMap死循环只发生在JDK1.7版本中,主要原因是JDK1.7中的HashMap,在头插法 加 链表 加 多线程并发 加 扩容这几个情形累加到一起就会形成死循环。多线程环境下建议采用ConcurrentHashMap替代。在JDK1.8中,HashMap改成了尾插法,解决了链表死循环的问题。
10.ConcurrentHashMap如何保证的线程安全 为什么性能比HashTable高?
- ConcurrentHashMap是线程安全的Map容器
- 在JDK1.7使用分段锁,将数据分成16段存储,每个数据段配置一把锁,即segment类,这个类继承ReentrantLock来保证线程安全,
- JKD8采用CAS+Synchronized保证线程安全,底层也是使用Node数组+链表+红黑树
- hashtable类基本上所有的方法都是采用synchronized进行线程安全控制,高并发情况下效率就降低 ,
当多个线程访问同步方法时,会发生阻塞或轮询状态;当一个线程使用put()方法添加元素时,另一个线程不能使用put()方法添加元素,也不能使用get()方法,竞争会越来越激烈。
11.java1.8开始为什么ConcurrentHashMap弃用分段锁?
- 加入分段锁浪费内存空间
- 减少锁竞争
- 更高的并发性能和拓展性,避免了分段锁自旋等待开销
- 提高gc效率
12:为什么ConcurrentHashMap1.8使⽤synchronsized⽽不⽤lock?
1.7lock锁是加上了while循环实现⾃旋,效率不⾼,JDK1.6以后 对 synchronized锁做了优化
二、多线程&并发篇
A.线程状态:
- NEW:线程被创建但是没有start运行
- RUNABLE:线程可以运行,是否运行取决于cpu是否调度该线程,如果没有调度就是ready,如调度到就是running
- WAITING:当锁对象调用wait方法,会让持有该锁对象的线程进入无限等待状态,这个状态只有被同一个锁对象的notify才能解- 除,解除后进入RUNABLE状态。
- TIMED_WAITING:sleep(time),wait(time)的时候进入计时等待,当时间到了,继续运行
- BLOCKED:在线程获取不到锁对象的时候,就会进入阻塞状态,当其他线程释放锁,本线程获取到锁才能够继续运行。
- TERMINATED:run方法执行完毕之后,进入终止状态。
B.线程终止:
(1)设置退出标志,使线程正常退出。
(2)使用interrupt()方法中断线程。
1.Java中实现多线程有几种方法?
继承Thread类,实现runnable,callable接口 使用线程池创建线程
1.1为什么不建议使用 Executors静态工厂构建线程池
目的是为了更加明确线程池的运行规则,规避资源耗尽的风险
2.线程池核心的参数?
- corePoolSize 线程池核心线程大小
- maximumPoolSize 线程池最大线程数量
- keepAliveTime 空闲线程存活时间
- unit 空闲线程存活时间单位
- workQueue 工作队列
①ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQuene
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而基本不会去创建新线程直到maxPoolSize(很难达到Interger.MAX这个数),因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
③SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
④PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。 - threadFactory 线程工厂
- handler 拒绝策略
3.线程池拒绝策略?
- AbortPolicy:默认策略,在需要拒绝任务时抛出RejectedExecutionException;
- CallerRunsPolicy:直接在 execute 方法的调用线程中运行被拒绝的任务,如果线程池已经关闭,任务将被丢弃;
- DiscardPolicy:直接丢弃任务;
- DiscardOldestPolicy:丢弃队列中等待时间最长的任务,并执行当前提交的任务,如果线程池已经关闭,任务将被丢弃。
4.什么是线程安全?怎么实现线程安全?
- 线程安全:当多线程执行同一段程序的时候,如果发生了和预期结果不一致的情况,就是线程不安全的,如果和预期结果一致就是线程安全的,可以加锁解决(把并行运行的线程变成串行化执行)。
- 1.加锁 利用Synchronized或者ReenTrantLock来对不安全对象进行加锁
- 2.Threadlocal来为每一个线程创造一个共享变量的副本来避免几个线程同时操作一个对象时发生线程安全问题
5.线程的三大特性?
- 原子性:一次或多次操作在执行期间不被其他线程影响
- 可见性:当一个线程在工作内存修改了变量,其他线程能立刻知道
- 有序性:JVM对指令的优化会让指令执行顺序改变,有序性是禁止指令重排
6.Threadlocal原理
- 原理:为每个线程创建变量副本,不同线程之间不可见,保证线程安全。每个线程内部都维护了一个Map,key为threadLocal实例,value为要保存的副本。
- 问题:使用ThreadLocal会存在内存泄露问题,因为key为弱引用,而value为强引用,每次gc时key都会回收,而value不会被回收。所以为了解决内存泄漏问题,可以在每次使用完后删除value或者使用static修饰ThreadLocal,可以随时获取value
6.1 ThreadLocalMap它初始长度是多少呢?扩容因子又是多少呢?
。初始长度是16
。扩容因子是2/3
6.2 Threadlocal如何解决hash冲突的呢?
。Hashmap用的是链式地址法
。Threadlocal用的是线性探测法
所谓线性探测,就是根据初始key的hashcode值,确定元素在table数组中的位置,一旦发生哈希冲突,就继续往后找(环形),找到第一个空节点的位置,再把当前 Entry 放进去。查找的过程也是一样的,先根据哈希值计算下标,再从这个位置开始往后找,如果找到第一个空节点还没找到,就认为 key 不存在。
7.sleep()和wait()的区别
- wait() 是Object的方法,sleep() 是Thread类的方法
- wait() 会释放锁,sleep() 不会释放锁
- wait() 要在同步方法或者同步代码块中执行,sleep() 没有限制
- wait() 要调用 notify() 或 notifyall() 唤醒, sleep() 自动唤醒
8.为什么wait, notify 和 notifyAll这些方法不在thread类里面?
这些方法的作用是在多个线程之间进行协调和通信,而不是单个线程的控制,需要一个共享的锁对象来实现。因为所有的 Java 对象都可以作为锁,因此这些方法是定义在 Object 类中的,而不是 Thread 类中。它们被用于实现线程间的协作和同步,可以在多个线程之间共享同一个对象的锁来进行通信和控制。
9.volatile原理
- 缓存一致协议保证读到最新值
- 内存屏障防止指令重排(解决指令重排对volatile修饰的变量不会产生影响)
- 保证变量的可见性和有序性,不保证原子性。并通过引入内存屏障,防止volatile附近的变量执行重排。使用了 volatile 修饰变量后,在变量修改后会立即同步到主存中,每次用这个变量前会从主存刷新。
10.Synchronized锁原理?
synchronized同步块使用了monitorenter和monitorexit指令实现同步,这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。
线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁,
而执行monitorexit,就是释放monitor的所有权。
11.能说下Synchronized的效率真的很低吗?
Synchronized在jdk1.6以下效率很低的,因为直接使用了重量级锁,
而到了1.6及以上,经过编译器优化以及jvm的锁优化效率几乎接近到了lock的水平,
但是Synchronized用起来是比较简单,不需要关心锁释放问题,所以大多数场景下可以直接使用Synchronized
JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁
等技术来减少锁操作的开销。
12.能说下Synchronized的升级过程吗?
Synchronized经过优化后,升级步骤如下:偏向锁、无锁、轻量级锁、重量级锁
13.synchronized和volatile比较
- volatile不需要加锁,比synchronized更轻便,不会阻塞线程
- synchronized既能保证可见性、有序性,又能保证原子性,而volatile只能保证可见性、有序性,无法保证原子性
14.能说下Synchronized和Lock的区别吗?
- 底层工作机制不同
synchronized关键字是属于JVM层面实现的,它的底层是通过monitor对象来完成的,
Lock与synchronized不同,它是一个具体的类,它是java api层面的锁 - 使用方式的区别
Synchronized关键字运行后是不需要用户去手动释放锁的,
在synchronized代码执行成功后系统会自动让线程释放对锁的占据。
ReentrantLock锁运行后需要用户手动去释放锁,如若用户没有主动去释放锁,就有可能导致出现死锁现象。
ReentrantLock需要使用lock()和unlock()方法配合try finally语句块来完成。 - 是否可中断
synchronized不能中断,除非抛出异常或者正常运行完成。
ReetrantLock可中断,无影响。 - 是否公平
synchronized是一个非公平锁
ReetrantLock可以实现公平也可以实现非公平 - 是否支持条件唤醒
synchronized不支持多条件唤醒
ReentrantLock可以精确唤醒
15. Java中的锁优化的方法
- jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁开销。
- 减少锁的持有时间:只在必要的时候持有锁,尽量缩短锁的持有时间,从而减少线程阻塞的可能性。
- 减少锁的粒度:使用更细粒度的锁,例如,如果只有一个线程会访问一个对象的某个字段,那么就不需要对整个对象加锁。
- 锁分离:如果一个类有多个独立的操作,那么可以为每个操作使用不同的锁,这样就可以避免不必要的同步。
- 锁粗化:如果一个线程在一段时间内会多次获取和释放同一个锁,那么JVM可能会尝试将这些操作合并为一次。
- 锁消除:如果JVM检测到一段代码中的锁操作是不必要的,那么可能会消除这个锁操作。
- 无锁数据结构:例如,juc包中提供了许多无锁数据结构,如ConcurrentHashMap、CopyOnWriteArrayList等。
- 读写锁:如果一个数据结构的读操作比写操作更频繁,那么使用读写锁可以提高性能。
- 偏向锁和轻量级锁:JVM在1.6之后引入了偏向锁和轻量级锁,优化无竞争的同步代码,有效减少无必要的重量级锁操作。
16.如何根据 CPU 核心数设计线程池线程数量
- IO 密集型:线程中十分消耗Io的线程数*2
- CPU密集型: cpu线程数量
17.yield()和join()区别
yield()调用后线程进入就绪状态,A线程中调用B线程的join() ,则B执行完前A进入阻塞状态
18.有三个线程T1,T2,T3,如何保证顺序执行?
- 设置优先级priority: 在JAVA线程中,通过一个int priority来控制优先级,范围为1-10,其中10最高,默认值为5。
- join() 方法 :Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行变串行。
19.什么是JMM内存模型?为什么需要JMM?

JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。
- Java 内存模型将内存分为了主内存和工作内存(也称为栈空间)。主内存存放所有的共享变量,所有线程都可以访问。每个线程都有自己的工作内存,存储了该线程使用到的变量的副本,线程对变量的所有操作都必须在自己的工作内存中完成,不能直接操作主存中的变量。操作时,首先将变量从主内存拷贝到自己的工作内存中,然后在自己的工作内存中对变量进行操作,操作完成后再将变量写回主存。不同的线程间也无法直接访问对方的工作内存的变量,线程间的变量值的传递必须通过主内存来完成。
- 在Java中,可以使用synchronized和volatile来保证多线程之间操作的有序性。
- 实现方式有所区别:
volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。
20. i++为什么不是线程安全的?
- 把变量读到cpu缓存
- 操作缓存中的值++
- 写回主内存
- 由于上述三个步骤不是原子性的,所以会导致线程安全问题
21.同步锁的几种方式(锁对象):
- 同步代码块加锁:sync…(obj)
- 同步方法加锁:等价于sync…(this)
- 静态同步方法加锁:等价于sync…(this.getClass())
- 死锁:线程之间互相等待对方释放锁,就产生了死锁,尽量不要同步中嵌套同步。
22.用户线程和守护线程:
- 用户线程:一般是用户创建的,不会随着主线程的终止而终止
- 守护线程:一般是系统创建的,会随着主线的终止而终止,垃圾回收线程就是守护线程,可以使用
Thread::setDaemon方法将用户线程转化为守护线程
23.什么是CAS锁,有什么缺点?
- CAS锁可以保证原子性,思想是更新内存时会判断内存值是否被别人修改过,如果没有就直接更新。如果被修改,就重新获取值,直到更新完成为止。
- 缺点
(1)只能支持一个变量的原子操作,不能保证整个代码块的原子操作
(2)CAS频繁失败导致CPU开销大
(3)ABA问题:如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前V
值是否发生过变化。可以通过版本号或时间戳解决
24.什么是AQS及工作原理?
AQS即队列同步器,是juc的locks包下组件的基础框架,AQS维护了一个volatile int类型的变量state表示当前同步状态。当state>0时表示当前已有线程获取到了资源,当state = 0时表示释放了资源,如果获取不到资源就将当前线程加入队列,通过自旋的方式重复尝试获取资源。
- 工作原理
AQS内部维护着一个FIFO队列 先进先出,该队列就是CLH同步队列。
node对应的是被阻塞的线程,head,tail,这两个变量的操作包括入列操作都是cas原子操作。但是出列并不是cas,因为独享方式下只有一个线程获取到了state状态。
CLH队列入列非常简单,就是tail指向新节点、新节点的prev指向当前最后的节点,当前最后一个节点的next指向当前节点。
出列:CLH同步队列遵循FIFO,首节点的线程释放同步状态后,将会唤醒它的后继节点(next),而后继节点将会在获取同步状态成功时将自己设置为首节点。

- AQS定义两种资源共享方式:
Exclusive(独占,只有一个线程能执行,如ReentrantLock)
Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)
25.J.U.C之并发工具类
- CyclicBarrier的使用 eg:所有运动员准备好才可以起跑
同步屏障,当线程到达同步屏障会被阻塞,最后一个到达的线程会唤醒其他被阻塞的线程同时运行
CyclicBarrier(int num,Runable runbale):第一个参数是同步屏障拦截线程的数量,当拦截的线程数量到达同步数,就让最后一个线程执行runbale的run方法,同时唤醒其他线程执行。 - CountDownLatch的使用 eg:谁最后一个走谁关门
CountDownLatch(int num):通过CountDownLatch#await()阻塞线程,如果阻塞的num为0,就执行不阻塞,如果不为0就阻塞了,要等到其他线程通过countDown()减少num,减到0了会唤醒所有被阻塞的线程去执行,一般被框架用来做线程之间的调度。 - Semaphore的使用 eg:停车场等候空位
信号量Semaphore(int count,boolean fair):内部是通过实现aqs的sync内部类实现的,用来控制同时访问资源的线程数量,也就是Semaphore#acquire()尝试获取资源,获取资源的过程其实就是对state减1的过程,如果获取成功了就继续执行,如果获取失败了阻塞着等待其他线程释放了许可之后(把state+1),该线程可以继续尝试获取到许可执行。
三、JVM篇
1.JVM运行时数据区(内存结构)

- 线程私有区:
1.虚拟机栈:是一个栈结构,每次调用方法都会在虚拟机栈中产生一个栈帧(栈帧入栈),每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后栈帧出栈
2.本地方法栈:本地方法栈服务的对象是JVM执行的native方法,hotspot把它和虚拟机栈合并成了1个
3.程序计数器:存储当前线程执行的字节码的偏移量;各线程之间独立存储,互不影响。 - 线程共享区:
4.堆内存:Jvm进行垃圾回收的主要区域,存放对象信息,分为新生代和老年代,内存比例为1:2,新生代的Eden区内存不够时时发生MinorGC,老年代内存不够时发生FullGC
5.方法区:存放类信息、静态变量、常量、运行时常量池等信息。JDK1.8之前用持久代实现,JDK1.8后用元数据空间实现,元空间使用的是本地内存,而非在JVM内存结构中。
2.JVM的内存模型
- JDK1.7的堆内存模型

- Young 年轻代
Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Eden区间变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间 - Tenured 年老区
Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。 - Perm 永久区
Perm代主要保存class,method,filed对象,这部份的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError :PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般
重新启动应用服务器可以解决问题。
Virtual区:
最大内存和初始内存的差值,就是Virtual区。 - JDK1.8的堆内存模型

jdk1.8的内存模型是由2部分组成,年轻代 + 年老代。
年轻代:Eden + 2*Survivor
年老代:OldGen
在jdk1.8中变化最大的Perm区,用Metaspace(元数据空间)进行了替换。
3.为什么要废弃1.7中的永久区?
移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。
由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError:PermGen。
4.JVM有哪些垃圾回收算法?
- 引用计数法:运行时根据对象的计数器是否为0,就可以直接回收
缺点:每次对象被引用时,都需要去更新计数器,有一点时间开销;浪费CPU资源;无法解决循环引用问题(最大的缺点) - 标记清除算法: 标记不需要回收的对象,然后清除没有标记的对象,会造成许多内存碎片。效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要停止应用程序STW
- 标记压缩算法:和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的清理未标记的对象,而是将存活的对象压缩到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题。其效率也有有一定的影响
- 复制算法: 将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。用在新生代
- 分代算法: 在jvm中,年轻代适合使用复制算法,老年代适合使用标记清除或标记压缩算法。
5.垃圾回收器
- 串行垃圾收集器:是指使用单线程进行垃圾回收,垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停,
等待垃圾回收的完成。这种现象称之为STW(Stop-The-World)。 - 并行垃圾收集器:将单线程改为了多线程进行垃圾回收,这样可以缩短垃圾回收的时间
- ParallelGC垃圾收集器:(jdk8默认垃圾收集器)ParallelGC收集器工作机制和ParNewGC收集器一样,只是在此基础之上,新增了两个和系统吞吐量相关的参数,使得其使用起来更加的灵活和高效。
- CMS:是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的
- G1 :JDK1.9以后的默认垃圾回收器,注重响应速度,支持并发,采用标记整理+复制算法回收内存,使用可达性分析法来判断对象是否可以被回收。
G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:
第一步,开启G1垃圾收集器
第二步,设置堆的最大内存
第三步,设置最大的停顿时间
G1中提供了三种模式垃圾回收模式,Young GC、Mixed GC 和 Full GC,在不同的条件下被触发。
Young GC:主要是对Eden区进行GC,它在Eden空间耗尽时会被触发
Mixed GC:当越来越多的对象晋升到老年代old region时,会触发Mixed GC,该算法并不是一个Old GC,除了回收整个Young Region,还会回收一部分的Old Region
Full GC:显式调用System.gc方法(建议JVM触发)。老年代空间不足,引起Full GC - ZGC收集器:它是JDK11中新引入的一种基于标记-整理算法实现的,以低延迟为首要目标的一款并发的垃圾收集器,可以在不超过10ms的停顿时间内进行全堆垃圾回收,适用于超大型的内存应用场景。
6.什么情况下会内存溢出?
- 堆内存溢出:(1)当对象一直创建而不被回收时(2)加载的类越来越多时(3)虚拟机栈的线程越来越多时
- 栈溢出:方法调用次数过多,一般是递归不当造成
7.什么是双亲委派机制?为什么会有这种机制?
what:
(1)当加载一个类时,先判断此类是否已经被加载,如果类已经被加载则返回;
(2)如果类没有被加载,则先委托父类加载(父类加载时会判断该类有没有被自己加载过),如果父类加载过则返回;如果没被加载过则继续向上委托;
(3)如果一直委托都无法加载,子类加载器才会尝试自己加载
why:
1.为了避免类重复加载(确保Class对象的唯一性)以及JVM的安全性。
2.保证了java核心api不被篡改
打破双亲委派机制
首先,我们需要自定义一个类加载器,继承自ClassLoader类,并重写loadClass()方法。在loadClass()方法中,我们可以实现自己的类加载逻辑。
双亲委派模型的实现在: ClassLoader.loadClass() 方法中:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
首先,检测是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
//如果没有加载,开始按如下规则执行:
long t0 = System.nanoTime();
try {
if (parent != null) {
//重点!父加载器不为空则调用父加载器的loadClass
c = parent.loadClass(name, false);
} else {
//没有父加载器也会先让Bootstrap加载器去加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//父加载器没有找到,则调用findclass,自己查找并加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
8.JVM中有哪些引用?
- 强引用:new的对象。哪怕内存溢出也不会回收
- 软引用:只有内存不足时才会回收
- 弱引用:每次垃圾回收都会回收
- 虚引用:必须配合引用队列使用,一般用于追踪垃圾回收动作
9.类加载过程

Java 类加载过程是 JVM 将 .class 字节码文件加载到内存,并转化为可执行的 “类对象” 的全过程。 这个过程严格遵循 “加载 → 连接 → 初始化” 三大阶段,其中连接阶段又细分为验证、准备、解析三步,总共 7 个关键步骤。
- 加载 :把字节码通过二进制的方式转化到方法区中的运行数据区
- 链接
- 验证:验证字节码文件的正确性。
1、文件格式验证(版本号,是不是CAFEBABYE开头,…)
2、元数据验证(验证属性、字段、类关系、方法等是否合规)
3、字节码验证
4、符号引用验证 - 准备:为 static 变量分配空间,设置默认值。(注意是对应类型的初始值,赋具体值在后面的初始化阶段)
final类型的变量在编译时已经赋值了 - 解析:将常量池中的符号引用(如类的全限定名)解析为直接引用(类在实际内存中的地址)
- 初始化 :执行类构造器(不是常规的构造方法),为静态变量赋初值并初始化静态代码块。
10.JVM类初始化顺序
父类静态代码块和静态成员变量->子类静态代码块和静态成员变量->父类代码块和普通成员变量->父类构造方法->子类代码块和普成员变量->子类构造方法
11.对象的创建过程
(1)检查类是否已被加载,没有加载就先加载类
(2)为对象在堆中分配内存
(3)初始化,将对象中的属性都分配0值或null
(4)设置对象头
(5)为属性赋值和执行构造方法
12.对象头中有哪些信息
对象头中有两部分,一部分是MarkWork,存储对象运行时的数据,如对象的hashcode、GC分代年龄、GC标记、锁的状态、获取到锁的线程ID等;另外一部分是类型指针,指向类元数据局 InstanceKlass,确定该对象所属的类型,如果是数组,还有一个部分存放数组长度
13.JVM内存参数
-Xmx[]:堆空间最大内存
-Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的
-Xmn[]:新生代的最大内存
-xx:[survivorRatio=3]:eden区与from+to区的比例为3:1,默认为4:1
-xx[use 垃圾回收器名称]:指定垃圾回收器
-xss:设置单个线程栈大小
一般设堆空间为最大可用物理地址的百分之80
JVM调优
- 简约版:
1.调整JVM内存参数,如堆内存大小、栈大小等。
2.分析并减少GC(垃圾回收)的频率和时间,如选择合适的GC算法、调整GC相关参数等。
3.优化类加载,尽可能减少类加载次数和时间,如使用预编译类、避免动态生成类等。
4.优化线程池的使用,如合理设置线程数量、避免死锁等。
5.诊断和解决内存泄漏问题,如通过分析dump文件等找出占用过多内存的对象和代码。
6.使用性能分析工具进行监测和分析,如VisualVM、JProfiler等。
以上是JVM调优中的一些常见步骤和方法,需要结合具体情况进行实际操作。
Java虚拟机(JVM)调优是指通过对JVM参数、程序代码、数据结构、算法等进行调整,以提升应用程序的性能、降低资源消耗、提高系统稳定性的一种技术手段。以下是进行JVM调优时需要关注的主要方面和一些常用方法: - 实操版:
-
理解应用需求与性能指标:
- 确定应用的业务场景、并发用户数、响应时间要求、吞吐量等关键指标。
- 诊断和解决内存泄漏问题:通过分析dump文件等找出占用过多内存的对象和代码。
- 使用监控工具(如JDK自带的
jconsole、jvisualvm,或第三方工具如VisualVM、Grafana搭配Prometheus等)收集系统运行时的各项性能数据,包括CPU使用率、内存占用、GC频率与耗时、线程状态、堆栈信息等。
-
JVM参数配置:
- 堆内存设置:通过
-Xms(初始堆大小)和-Xmx(最大堆大小)调整堆内存大小。一般保持两者相等以避免堆扩展带来的额外开销。如果堆太小,程序可能会频繁地进行GC,导致程序变慢;如果堆太大,会占用过多的内存资源。 - 垃圾回收器选择:根据应用特性和对响应时间、吞吐量的需求选择合适的垃圾回收器(如
Parallel GC、CMS、G1、ZGC或Shenandoah)。并可能需要调整相关参数(如-XX:MaxTenuringThreshold、-XX:InitiatingHeapOccupancyPercent等)。 - 元空间与栈设置:通过
-XX:MetaspaceSize和-XX:MaxMetaspaceSize控制元空间大小,用-Xss设置每个线程的栈大小。 - 其他参数:如
-XX:SurvivorRatio(新生代中Eden区与Survivor区比例)、-XX:NewRatio(老年代与新生代比例)、-XX:+UseCompressedOops(开启对象指针压缩)等。
- 堆内存设置:通过
-
代码优化:
- 减少对象创建:避免不必要的临时对象创建,利用对象池复用对象,合理设计数据结构减少冗余对象。
- 减少内存泄漏:及时释放不再使用的对象引用,避免长生命周期对象持有短生命周期对象引用导致内存无法释放。
- 合理使用集合类:根据数据特性和访问模式选择合适的数据结构(如ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等),避免过度扩容。
- 使用并发容器与工具类:在多线程环境下,使用
ConcurrentHashMap、CopyOnWriteArrayList等线程安全集合类,以及Semaphore、CyclicBarrier、CountDownLatch等并发控制工具。 - 避免锁竞争:合理设计数据结构和算法减少锁粒度,使用
Lock接口提供的更细粒度的锁(如ReentrantLock、StampedLock),或考虑无锁数据结构(如Atomic系列类)。 - 优化代码:通过优化算法、减少循环次数等方式,减少CPU的使用率。
- 线程数调优:JVM中的线程数也会影响程序的性能和稳定性。如果线程数过多,会导致竞争和上下文切换的开销增加,从而影响程序的性能;如果线程数过少,会导致程序并发性能的下降。因此,需要根据程序的特点和需求进行线程数的合理调整。
-
数据库与I/O优化:
- SQL优化:编写高效SQL语句,避免全表扫描,合理使用索引,减少数据库连接的创建与关闭。
- 缓存策略:使用缓存(如
Redis、Memcached)存储热点数据,减轻数据库压力。 - 批量操作与异步处理:批量处理数据库操作,利用消息队列实现异步处理以提高系统响应速度。
-
监控与分析:
- 使用 profiling 工具:如
JProfiler、YourKit等进行CPU、内存、线程等深度剖析,找出性能瓶颈。 - 分析GC日志:通过
-XX:+PrintGCDetails、-XX:+PrintGCDateStamps等参数输出GC日志,分析GC频率、暂停时间、晋升老年代对象数量等,判断是否存在内存问题。 - 监控JVM详细信息:使用
jstat、jmap、jstack等命令行工具获取JVM运行时状态。
- 使用 profiling 工具:如
-
JIT编译器调优:JIT编译器是JVM的另一个核心组件。通过调整JIT编译器的优化级别和参数,可以提高程序的执行效率和响应速度。
实战:
1. top 命令:找到占用 CPU 过高的 Java 进程 PID
执行 top 查看整机进程资源,按P按 CPU 使用率排序,记录 CPU 占用异常的进程 PID(笔记示例 PID:1937),此时能直观看到进程 CPU 占比达到 99%。
适用场景:服务器未预装任何诊断工具,紧急定位故障进程。
2. top -Hp PID:定位进程内耗 CPU 的线程 ID
命令:top -Hp 1937,查看该进程下所有线程资源占用,同样按 CPU 排序,拿到高负载线程十进制 TID(示例:2284,CPU99.9%、关联 Java 业务线程)。
关键:JVM 栈 dump 需要十六进制线程号,拿到十进制 TID 后需要进制转换。
3. printf “% x\n 线程号”:十进制 TID 转十六进制
printf "%x\n 2284,把线程 ID 转为 16 进制编码,用于后续 jstack 过滤线程栈信息。
4. jstack PID | grep -A30 十六进制线程号:抓取故障线程堆栈
shell
jstack 1937 | grep -A30 转换后的16进制tid
作用:筛选出目标耗 CPU 线程的完整调用栈,直接定位到具体 Java 类 + 行号,找到死循环、密集计算等耗 CPU 代码。
小结:纯 Linux+JDK 自带命令,零依赖,所有 Java 生产环境通用,是运维 / 后端必备基础排查手段。
二、Arthas 在线诊断工具:可视化排查 Java 故障(推荐日常环境使用)
Arthas 是阿里开源 Java 诊断利器,无需重启应用、不用改代码,一键在线查看线程、类、方法、堆栈。
- 快速启动 Arthas
1)下载启动包
shell
curl -O https://arthas.aliyun.com/arthas-boot.jar
2)依附 Java 进程启动
shell
java -jar arthas-boot.jar
启动后会列出本机所有 Java 进程,输入故障进程序号回车,attach 绑定目标 JVM。 - 核心排查命令(对应笔记要点)
thread:查看全量线程列表,自动标注占用 CPU 最高的线程,直接打印线程栈,省去 top、进制转换繁琐步骤;
thread 线程号:单独查看指定线程详细堆栈;
dashboard:仪表盘面板,实时刷新:线程状态、堆内存、CPU、GC 情况,全局观测应用运行指标;
jad 全类名:反编译线上运行的 class 源码,结合堆栈行号,直接查看故障代码原始逻辑,不用翻项目本地代码。
四、Mysql数据库
1.MyIsAm和InnoDB的区别
- 事务:InnoDB支持事务,MyISAM不支持
- 锁: InnoDB支持行级锁,MyISAM支持表锁
- 索引:
InnoDB不支持全文索引,MyISAM支持;
InnoDB是聚集索引,使用B+Tree作为索引结构,叶子节点就是数据文件。
MyISAM是非聚集索引,也是使用B+Tree作为索引结构,索引和数据文件是分离的,索引保存的是数据文件的指针
InnoDB表必须有唯一索引(如主键)(没有指定会自己找/生产一个隐藏列Row_id来充当默认主键),而MyISAM可以没有 - 外键: InnoDB支持外键,而MyISAM不支持
- 适用场景:
- 是否要支持事务,如果要请选择innodb,如果不需要可以考虑MyISAM;
- 如果表中绝大多数都只是读查询,可以考虑MyISAM,如果既有读也有写,请使用InnoDB。
- MyISAM恢复起来更困难,能否接受;
2.Mysql事务特性
- 分别是原子性、一致性、隔离性、持久性。
- 1.原子性(Atomicity) 由undolog日志保证,他记录了需要回滚的日志信息,回滚时撤销已执行的sql
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。 - 2.一致性(Consistency)由其他三大特性共同保证,是事务的目的
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。举例来说,假设用户A和用户B两者的钱加起来一共是1000,那么不管A和B之间如何转账、转几次账,事务结束后两个用户的钱相加起来应该还得是1000,这就是事务的一致性。 - 3.隔离性(Isolation) 由MVCC保证
隔离性是当多个用户并发访问数据库时,比如同时操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。 - 4.持久性(Durability) 由redolog日志和内存保证,mysql修改数据时内存和redolog会记录操作,宕机时可恢复
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的
3.三大范式
- 第一范式:每一列都是不可分割的原子数据项。
- 第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。
- 第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。
4.事务的隔离级别
- 读未提交(Read Uncommitted)
定义:一个事务可以读取另一个未提交事务的数据。
可能出现的问题: - 脏读:读取到其他事务未提交的 “脏数据”(若该事务后续回滚,读取的数据无效)。
- 不可重复读、幻读也可能出现。
- 适用场景:极少使用,仅在对数据一致性要求极低、追求极致性能时考虑(如某些实时监控场景)。
- 读已提交(Read Committed)
定义:一个事务只能读取另一个已提交事务的数据。 - 解决的问题:避免脏读(因为只能读已提交的数据)。
- 可能出现的问题: 不可重复读:同一事务内多次读取同一数据,因其他事务修改并提交,导致结果不一致。 幻读可能出现。
- 适用场景:多数关系型数据库的默认隔离级别(如 Oracle、SQL
Server),适用于对脏读敏感,但可接受偶尔不可重复读的场景(如普通业务系统)。 - 可重复读(Repeatable Read)
定义:同一事务内多次读取同一数据,结果始终一致(不受其他事务修改影响)。 - 解决的问题:避免脏读和不可重复读。
- 可能出现的问题: 幻读:同一事务内,多次执行相同查询,因其他事务插入 /删除数据,导致结果集行数不一致(“幻象” 行)。
- 适用场景:MySQL InnoDB的默认隔离级别,适用于对数据一致性要求较高的场景(如金融交易)。
- 串行化(Serializable)
定义:事务完全串行执行(而非并发),相当于单线程处理事务。 - 解决的问题:避免脏读、不可重复读、幻读(所有并发问题)。
- 缺点:并发性能极差,可能导致大量事务等待。
- 适用场景:对数据一致性要求极高,且并发量极低的场景(如某些特殊财务报表生成)。
**Inno DB 默认隔离级别为可重复读级别,分为快照度和当前读,并且通过间隙锁解决了幻读问题。**
5.MySQL有哪些索引
- 主键索引:一张表只能有一个主键索引,主键索引列不能有空值和重复值
- 唯一索引:唯一索引不能有相同值,但允许为空
- 普通索引:允许出现重复值
- 组合索引:对多个字段建立一个联合索引,减少索引开销,遵循最左匹配原则
- 全文索引:myisam引擎支持,通过建立倒排索引提升检索效率,广泛用于搜索引擎
6.聚簇索引和非聚簇索引的区别
聚簇索引(Clustered Index)
定义:索引的逻辑顺序与数据行在磁盘上的物理存储顺序完全一致的索引。其叶子节点直接存储完整的数据行,整个表的数据按照聚簇索引的键值顺序进行物理排列。由于数据的物理存储顺序具有唯一性,一张表只能存在一个聚簇索引。
例如:MySQL InnoDB 中,主键默认被设为聚簇索引,表中数据会按照主键的顺序在磁盘上连续存储。
非聚簇索引(Non-Clustered Index)
定义:索引的逻辑顺序与数据行的物理存储顺序无关的索引。其叶子节点不存储完整数据行,而是存储指向数据行物理位置的指针(或聚簇索引键,取决于数据库实现),通过该指针可定位到对应的数据行。一张表可以创建多个非聚簇索引,数量通常受限于性能(过多会影响写入效率)。
MySQL InnoDB 中,主键默认是聚簇索引,若表无主键,会自动选择唯一非空索引作为聚簇索引;
非聚簇索引(如普通索引)的叶子节点存储主键值,查询时需通过主键回表获取数据(除非覆盖索引)
7.MySQL整个查询的过程
(1)客户端向 MySQL 服务器发送一条查询请求
(2)服务器首先检查查询缓存,如果命中缓存,就从缓存中返回结果。否则进入下一阶段
(3)服务器进行 SQL 解析、预处理、
(4)查询优化器优化查询,生成对应的执行计划
(5)执行引擎调用存储引擎API执行查询
(6)将结果返回给客户端,同时缓存查询结果
注意:只有在8.0之前才有查询缓存,8.0之后查询缓存被去掉了

8.哪些情况索引会失效/Mysql调优手段?
(1)or连接的条件中存在非索引列,or改union效率更高
(2)模糊查询以%开头
(3)索引列参与运算
(4)联合索引不满足最左前缀原则
(5)隐式类型转换
(6)使用NOT IN、!=、<>、IS NOT NULL这类操作可能导致数据库放弃索引,转而全表扫描,is null是可以使用索引的
MySQL 调优手段
1. 索引优化
优先为查询频繁的列(WHERE、JOIN、ORDER BY)建立索引。
避免过度建索引(索引会降低插入 / 更新性能),单表索引建议不超过 5 个。
使用联合索引时,遵循 “最左前缀原则”,将区分度高的列放在前面。
利用覆盖索引(查询字段均在索引中)避免 “回表”,例:SELECT a,b FROM t WHERE a=1(联合索引(a,b)可覆盖)。
2. SQL 语句优化
避免SELECT*,只查询必要字段(减少数据传输和 IO)。
拆分复杂JOIN,避免多表联查(尤其是大表),可通过分批次查询或冗余字段优化。
用UNION ALL替代OR(OR可能导致索引失效,UNION ALL效率更高)。
分页查询优化:大偏移量分页(如LIMIT 100000, 10) 可通过索引定位起点,例:WHERE id > 100000 LIMIT 10。
3. 表结构优化
选择合适的数据类型:如用INT代替VARCHAR存身份证(避免冗余),用TINYINT代替INT存状态(节省空间)。
避免使用NULL(NULL会增加索引和查询复杂度,可用默认值替代)。
分库分表:水平拆分(按数据量分表,如按时间分表)、垂直拆分(按字段拆分,如将大文本字段单独分表)。
4. 数据库配置优化
调整innodb_buffer_pool_size(建议设为物理内存的 50%-70%,提升缓存命中率)。
优化join_buffer_size、sort_buffer_size等连接 / 排序缓存(避免过小导致磁盘临时表)。
开启慢查询日志(slow_query_log = 1),通过EXPLAIN分析慢 SQL,定位优化点。
5. 其他手段
读写分离:主库写入,从库查询,分担主库压力。
使用缓存(如 Redis):缓存高频查询结果,减少数据库访问。
定期优化表:OPTIMIZE TABLE(清理碎片,优化索引结构,InnoDB 可通过ALTER TABLE重建表替代)。
调优核心原则:基于实际业务场景,通过监控(慢日志、执行计划)定位瓶颈,优先优化影响最大的环节(如高频慢 SQL、索引设计)。
9.B树和B+树的区别
- B树key和value都在节点上。并且叶子节点之间没有关系。
- B + 树仅叶子节点存键值,非叶子节点只存索引。叶子节点之间有双向指针,有引用链路。
- 由于B树每个节点都存储了一条记录的所有数据,因此IO开销大。
- B+树的叶子节点有指针,可以很好的支持全表扫描、范围查找。
在Mysql中一个innodb页就是一个B+树节点,一个innodb页默认16kb,一般情况下,一颗两层的B+树,可以存2000万行左右数据
10.MySQL中有哪几种锁?
- 1、共享锁:共享锁锁定的资源可以被其他用户读取,但不能修改
- 2、独占锁:独占锁锁定的资源只允许锁定操作的程序使用,其他任何对他的操作都不会被接受,执行增删改命令时,自动会使用独占锁,直到事务结束后才会被释放
- 3、乐观锁:假设不会出现并发问题,每次取数据都认为不会有其他线程对数据进行修改,因此不会上锁,但是更新的时候会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制实现,在数据表中增加一个版本号,表示被修改的次数,当数据被修改时,版本号+1,当线程要更新数据时,读取版本号,提交更新的时候判断刚才读取到的版本号和当前版本号相等时才更新,否则重试,直到更新成功为止
- 4、悲观锁:假设最坏的情况,每次取数据都认为其他线程会修改,所以都会枷锁,synchronized的思想就是悲观锁
11.MySQL插入百万级的数据如何优化?
(1)一次sql插入多条数据,可以减少写redolog日志和binlog日志的io次数(sql是有长度限制的,但可以调整)
(2)保证数据按照索引进行有序插入
(3)可以分表后多线程插入
12.MySQL B+树索引和哈希索引的区别
-
B+树索引
是一种多路径的平衡搜索树,具有如下特点:
1.非叶子节点不保存数据,只保存索引值
2.叶子节点保存所有的索引值和数据
3.同级节点通过指针自小而大顺序链接
4.节点内的数据也是自小而大顺序存放
5.叶子节点拥有父节点的所有信息
优点
由于数据顺序存放,所以无论是区间还是顺序扫描都更快。
非叶子节点不存储数据,因此几乎都能放在内存中,搜索效率更高
单节点中可存储的数据更多,平均扫描I/O请求树更少
平均查询效率稳定(每次查询都从根结点到叶子结点,查询路径长度相同)
缺点
新增数据不是按顺序递增时,索引树需要重新排列,容易造成碎片和页分裂情况。 -
哈希索引
哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,速度非常快,具有如下特点:
1.哈希索引建立在哈希表的基础上
2.对于每个值,需要先计算出对应的哈希码(Hash Code),不同值的哈希码唯一
3.把哈希码保存在哈希表中,同时哈希表也保存指向对应每行记录的指针
优点
大量唯一等值查询时,哈希索引效率通常更高。
缺点
哈希索引对于范围查询和模糊匹配查询显得无能为力。
哈希索引不支持排序操作,对于多列联合索引的最左匹配规则也不支持。
哈希索引不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。
13.什么是组合索引、覆盖索引 回表?
- 组合索引:多个字段建一个索引
- 覆盖索引:查询的字段都在索引里,不用回表
- 回表:查完索引,还得再跑回原表查一遍数据
14.分库分表
-
先垂直拆分:按业务或字段拆分,分离不同业务域(如用户、订单)或冷热数据,降低单库 / 单表复杂度,为后续拆分铺路。
-
后水平拆分:在垂直拆分的基础上,对同一业务域的大量数据按规则分片(如按 ID 哈希),解决数据量过大的问题。
-
shardingjdbc 核心是通过 “字段解析→标识计算→路由筛选” 的流程,将数据均匀分布到不同的库表中,从而实现水平扩展。
-
复合分片算法:处理多字段组合的分片,支持多个字段共同决定路由。
-
如何进行数据迁移?
-
- 全量数据迁移
使用 ShardingSphere 提供的 ShardingSphere Scaling(数据迁移工具),支持分库分表场景的全量 + 增量迁移,自动适配分片规则。
按分片键范围批量读取源数据(避免全表扫描),再按目标分片规则写入对应库表。
例:按 user_id 分批次(如每次 10 万条)读取,计算目标库表索引后写入。
- 全量数据迁移
-
- 增量数据同步
解决全量迁移期间的 “数据增量”(新写入 / 修改的数据),确保最终一致性:
基于 binlog 同步:
用 Canal、Debezium 等工具监听源库 binlog,解析增量操作(insert/update/delete),按目标分片规则同步到对应库表。
- 增量数据同步
分库分表数据迁移的核心是:明确分片规则→全量 + 增量同步→严格校验→平滑切换。优先使用成熟工具(如 ShardingSphere Scaling)减少自定义开发成本,同时必须设计回滚方案,确保业务稳定性。
15.MyBatis一级缓存与二级缓存
- 一级缓存是SqlSession级别的缓存,Mybatis默认开启一级缓存
一级缓存的作用域是同一个SqlSession,在同一个sqlSession中两次执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。
当一个sqlSession结束后该sqlSession中的一级缓存也就不存在了。 - 一级缓存失效的四种情况
不同的SqlSession对应不同的一级缓存
同一个SqlSession但是查询条件不同
同一个SqlSession两次查询期间执行了增删改操作
同一个SqlSession两次查询期间手动清空了缓存 - 二级缓存是mapper(namespace)级别的缓存
工作机制:一个会话查询一条数据会放到会话的一级缓存中。如果会话关闭了,一级缓存中的数据会保存带到二级缓存中。
Mybatis默认没有开启二级缓存,需要在全局配置文件中开启二级缓存
<setting name="cacheEnabled" value="true"/>
二级缓存(second level cache),全局作用域缓存。二级缓存默认不开启,需要手动配置。MyBatis提供二级缓存的接口以及实现,缓存实现要求POJO实现Serializable接口。二级缓存在SqlSession 关闭或提交之后才会生效
多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession去操作数据库得到数据会存在二级缓存区域,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。
- 清空sqlSession缓存的六种方式 :
① session.clearCache( ) ;
② execute update(增删改) ;
③ session.close( );
④ xml配置 flushCache=“true”;
⑤ rollback;
⑥ commit。
五、spring框架
1.什么是Spring?
Spring框架是一个轻量级的框架,其核心原则是面向接口编程和控制反转(IoC),简化java开发。
1.1:Spring的优点?
- 方便解耦:Spring框架实现了控制反转(IoC)和依赖注入(DI)等特性,可以将对象的创建和依赖关系交给Spring容器管理,从而简化了开发过程。
- AOP编程的支持:Spring提供了面向切面编程(AOP)的支持,可以方便地实现对程序进行权限拦截、运行监控等功能。
- 声明式事务的支持:Spring的事务管理机制可以通过配置来实现,从而实现声明式事务的效果,降低了手动编码的复杂性。
- 方便程序的测试:Spring对Junit4支持,可以通过注解方便地测试Spring程序。
- 方便集成各种优秀框架:Spring可以与各种优秀的开源框架无缝集成,如Struts、Hibernate、MyBatis等。
降低Java EE API的使用难度:Spring提供了许多方便的API和工具类来简化Java EE API的使用,从而降低了使用难度。
1.2.Spring用了哪些设计模式?
- BeanFactory用了工厂模式
- AOP用了动态代理模式
- RestTemplate用来模板方法模式
- SpringMVC中handlerAdaper用来适配器模式
- Spring里的监听器用了观察者模式
2.IOC(控制反转)与DI(依赖注入)理解与区别?
- IOC的意思是控制反转,DI的意思是依赖注入
- 前者以前创建对象的主动权和创建时机是由自己把控的,IOC则是把创建对象并给对象中的属性赋值交由spring工厂管理,从而达到控制反转的目的;
- 而后者则是通过依赖注入的手段让spring工厂来管理对象的创建和属性的赋值 。DI依赖注入:就是在构造某对象时,能将对象依赖的东西自动初始化进去。
3.AOP是什么?
- AOP是面向切面编程,可以将那些与业务不相关但是很多业务都要调用的代码抽取出来,思想就是不侵入原有代码的情况下对功能进行增强。
Spring 的 AOP 实现原理其实很简单,就是通过动态代理实现的。如果我们为 Spring 的某个 bean 配置了切⾯,
那么 Spring 在创建这个 bean 的时候,实际上创建的是这个 bean 的⼀个代理对象,我们后续对 bean 中⽅法的调
⽤,实际上调⽤的是代理类重写的代理⽅法。 - SpringAOP是基于动态代理实现的,动态代理是有两种,一种是jdk动态代理,一种是cglib动态代理;
- Spring默认使用jdk动态代理,当被代理的类没有实现接口时就使用cglib动态代理
3.1如何使用aop自定义日志?
- 第一步:创建一个切面类,把它添加到ioc容器中并添加@Aspect注解
- 第二步:在切面类中写一个通知方法,在方法上添加通知注解并通过切入点表达式来表示要对哪些方法进行日志打印
- 第三步:通过JoinPoint这个参数可以获取当前执行的方法名、方法参数等信息,根据需求在方法进入或结束时打印日志
3.2Spring AOP 中有哪些 Advice 类型?
- 前置通知(Before):在⽬标⽅法被调⽤之前调⽤通知功能;
- 最终通知(After):在⽬标⽅法完成之后调⽤通知,此时不会关⼼⽅法的输出是什么;
- 后置通知(After-returning ):在⽬标⽅法成功执⾏之后调⽤通知;
- 异常通知(After-throwing):在⽬标⽅法抛出异常后调⽤通知;
- 环绕通知(Around):通知包裹了被通知的⽅法,在被通知的⽅法调⽤之前和调⽤之后执⾏⾃定义的⾏为。
- 执⾏顺序(Spring 5.2.7 之前的版本):Around “前处理” > Before > ⽅法执⾏ > Around “后处理” > After >
AfterReturning|AfterThrowing - 执⾏顺序(Spring 5.2.7 开始):Around “前处理” > Before > ⽅法执⾏ > AfterReturning|AfterThrowing >
After > Around “后处理”
4.如何定义一个全局异常处理类?
添加@ControllerAdvice注解,然后定义一些用于捕捉不同异常类型的方法,在这些方法上添加@ExceptionHandler(value = 异常类型.class)和@ResponseBody注解,方法参数是HttpServletRequest和异常类型,然后将异常消息进行处理。
5.BeanFactory 和 ApplicationContext的关系是什么?
Spring 中设计了BeanFactory ApplicationContext 两个接⼝用以表示容器。
- 1.功能上的区别:
BeanFactory是Spring中最底层的接⼝,是IOC的核心,其功能包含了各种Bean的定义、加载、实例化、依赖注入
和⽣命周期的管理。是IOC最基本的功能。我们可以称之为 “低级容器”。
BeanFactorty接⼝提供了配置框架及基本功能,但是⽆法⽀持spring的aop功能和web应⽤。而
ApplicationContext接⼝作为BeanFactory的派生,因⽽提供BeanFactory所有的功能,还在功能上做了扩展。 - 2、加载方式的区别:
BeanFactory是延时加载,也就是说在容器启动时不会注入bean,而是在需要使⽤bean的时候,才会对该bean进⾏加载实例化。如果bean的某个属性没有注⼊,BeanFactory加载不会抛出异常,第⼀次调⽤getBean()⽅法时才会抛出异常。
ApplicationContext 是在容器启动的时候,⼀次性创建所有的bean(单例⾮懒加载),所以运⾏的时候速度相对BeanFactory比较快。(也因为其⼀次性加载的原因,导致占用内存空间,当Bean较多时,影响程序启动的速度) - 3、注册方式的区别
BeanFactory和ApplicationContext都⽀持BeanPostProcessor、BeanFactoryPostProcessor的使⽤
BeanFactory是需要手动注册的。
ApplicationContext 是⾃动注册的
6.说下 FactoryBean 和 BeanFactory有什么区别?
- BeanFactory:是 Bean 的工厂, ApplicationContext 的⽗类,IOC 容器的核⼼,负责⽣产和管理 Bean 对象。
- FactoryBean:是 Bean,可以通过实现 FactoryBean 接⼝定制实例化 Bean 的逻辑,通过代理⼀个Bean对象,对⽅法前后做⼀些操作(通常是⽤来创建⽐较复杂的bean)。
7.Spring循环依赖

经典面试问答
1. 构造器注入产生的循环依赖能解决吗?
答:不能。构造器注入是在实例化阶段(new A(B))就产生依赖,而 Spring 的三级缓存是在实例化后、属性注入阶段解决循环依赖,因此构造器循环依赖会直接抛出 BeanCurrentlyInCreationException。
2. 多例(Prototype)Bean 通过 setter 注入产生的循环依赖能解决吗?
答:不能。多例 Bean 每次getBean()都会新建对象,Spring 不会对多例 Bean 使用三级缓存缓存,因此循环依赖时会无限递归创建对象,最终抛出异常。
3. 若只有一级缓存(map1),能解决循环依赖吗?
答:从解决循环依赖的角度能执行,但使用过程会有问题。若成品和半成品都存在 map1 中,当 A 依赖 B、B 又依赖 A 时,A 在半成品阶段被 B 引用,后续 A 初始化完成后属性可能不完整,导致空指针或逻辑错误。
4. 若只有一级、二级缓存(map1+map2),能解决循环依赖吗?
答:在无 AOP 代理的情况下可以解决;有 AOP 时无法解决。
无 AOP:实例化 A→放入 map2(半成品)→A 依赖 B→实例化 B→B 依赖 A 时从 map2 取 A 的半成品→B 初始化完成放入 map1→A 注入 B 后完成初始化→A 放入 map1,流程通顺。
有 AOP:A 的代理对象需要在初始化后生成,若只有 map1(成品)和 map2(半成品),B 在属性注入时拿到的是 A 的半成品(无代理),后续 A 生成代理后,B 引用的还是旧对象,导致代理不一致。
5. 三级缓存的核心作用是什么?
答:解决 “循环依赖” 与 “AOP 代理” 的兼容性问题。
无循环依赖时:保证 Bean 的代理对象在初始化最后阶段生成,遵循 Spring “初始化完成后生成代理” 的设计原则。
有循环依赖时:通过 ObjectFactory 提前暴露 Bean 的早期引用(若有 AOP 则直接生成代理对象),确保循环依赖的 Bean 能拿到正确的代理引用。
6. 循环依赖中,AOP 代理对象何时生成?
答:在 BeanPostProcessor#after 阶段生成。若存在循环依赖,会提前通过三级缓存的 ObjectFactory 生成代理对象;若不存在循环依赖,则在初始化完成后生成,且 AOP 源码会判断是否已生成过代理,避免重复创建。
7.只有一级缓存和三级缓存能解决循环依赖吗?
当 Bean A 依赖 Bean B,Bean B 又依赖 Bean A 时,B 在获取 A 的早期引用时,只能从三级缓存的工厂反复生成新的引用(而非复用同一引用),会导致循环引用的 Bean 不是同一实例,最终引发逻辑错误或空指针异常。

循环依赖:一个或多个对象实例之间存在直接或间接的依赖关系,构成环形调用。
三级缓存机制:
- 一级缓存(singletonObjects):用于存放完全初始化的单例Bean,即存储成品 Bean。
- 二级缓存(earlySingletonObjects):用于存放提前曝光的Bean实例,这些实例已经完成了实例化,但尚未完成依赖注入等后续步骤。存储半成品 Bean 的早期引用(含 AOP 代理,若有)。
- 三级缓存(singletonFactories):存储生成早期引用的工厂,它存储的是生产Bean实例的
ObjectFactory对象。 - 构造器注入不支持循环依赖:因为构造器注入是在实例化阶段完成的,此时Bean还未被放入任何缓存中,因此构造器循环依赖会导致
BeanCurrentlyInCreationException异常。 - 非单例Bean不支持循环依赖:Spring只管理单例作用域的Bean的循环依赖,对于原型(prototype)作用域的Bean,每次请求都会创建新的实例,因此循环依赖不会被解决。
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// Quick check for existing instance without full singleton lock
// 1.从单例对象缓存(1级缓存)中获取beanName对应的单例对象
Object singletonObject = this.singletonObjects.get(beanName);
// 2.如果单例对象缓存(1级缓存)中没有,并且该beanName对应的单例bean正在创建中
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 3.从早期单例对象缓存中(二级缓存)获取单例对象(之所称成为早期单例对象,
// 是因为earlySingletonObjects里的对象的都是通过提前曝光的ObjectFactory创建出来的,还未进行属性填充等操作)
singletonObject = this.earlySingletonObjects.get(beanName);
// 4.如果在早期单例对象缓存中(二级缓存)也没有,并且允许创建早期单例对象引用
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// Consistent creation of early reference within full singleton lock
// 5.从单例工厂缓存中(三级缓存)获取beanName的单例工厂
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
// 再次从二级缓存中获取,重复校验
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
// 6.再从单例工厂缓存中(三级缓存)获取beanName的单例工厂
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 7.如果存在单例对象工厂,则通过工厂创建一个单例对象
singletonObject = singletonFactory.getObject();
// 8.将通过单例对象工厂创建的单例对象,放到早期单例对象缓存中
this.earlySingletonObjects.put(beanName, singletonObject);
// 9.移除该beanName对应的单例对象工厂,因为该单例工厂已经创建了一个实例对象,并且放到earlySingletonObjects缓存了
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
- spring2.6之前默认会解决循环依赖。在spring2.6之后需要通过配置开启解决循环依赖
8.Bean 的作用域
(1)Singleton:一个IOC容器只有一个
(2)Prototype:每次调用getBean()都会生成一个新的对象
(3)request:每个http请求都会创建一个自己的bean
(4)session:同一个session共享一个实例
(5)application:整个serverContext只有一个bean
(6)webSocket:一个websocket只有一个bean
9.Bean 生命周期
-
实例化Bean:当使用构造函数或者工厂方法创建Bean对象时,就进入了创建阶段。
-
Bean属性赋值:在Bean对象创建后,通过setter方法设置Bean的各个属性。
-
初始化Bean
- 当Bean的属性设置完成后,会触发初始化回调方法,进行一些额外的初始化工作。
实现了各种 Aware 通知的⽅法,如 BeanNameAware、BeanFactoryAware、 - ApplicationContextAware 的接⼝⽅法
- 执⾏ BeanPostProcessor 初始化前置⽅法
- 执⾏ @PostConstruct 初始化⽅法,依赖注⼊操作之后被执⾏
- 执⾏⾃⼰指定的 init-method ⽅法
- 执⾏ BeanPostProcessor 初始化后置⽅法
- 当Bean的属性设置完成后,会触发初始化回调方法,进行一些额外的初始化工作。
-
使用Bean:在初始化完成后,Bean对象处于可用状态,可以供应用程序使用。
-
销毁Bean:当Bean对象不再需要时,会触发销毁回调方法,进行资源释放等清理工作,销毁容器的各种⽅法,如 @PreDestroy、DisposableBean 接⼝⽅法、destroy-method
10.Spring 事务原理?
- spring事务有编程式和声明式,我们一般使用声明式,在某个方法上增加@Transactional注解
- 原理是:当一个方法加上@Transactional注解,spring会基于这个类生成一个代理对象并将这个代理对象作为bean,当使用这个bean中的方法时,如果存在@Transactional注解,就会将事务自动提交设为false,然后执行方法,没有异常则提交,有异常则回滚。
11.spring事务失效场景
(1)事务方法所在的类没有加载到容器中
(2)事务方法不是public类型
(3)在同⼀个类中的⽅法直接内部调⽤
(4)spring事务默认只回滚运行时异常,可以用rollbackfor属性设置
(5)业务自己捕获了异常,事务会认为程序正常秩序
12.spring事务的隔离级别
- DEFAULT:默认的事务隔离级别
- READ_UNCOMMITTED:读未提交
- READ_COMMITTED:读已提交
- REPEATABLE_READ:可重复读
- SERIALIZABLE:串行化
13.说一下Spring的事务传播行为
① PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务
② PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
③ PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前 不存在事务,就抛出异常。
④ PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
⑤ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前 事务挂起。
⑥ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
⑦ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则 按REQUIRED属性执行。
14.SpringMVC工作原理
- 前端控制器接收用户请求将请求发送给处理器映射器
- 处理器映射器根据请求url找到具体的handle和拦截器,返回给前端控制器
- 前端控制器调用处理器适配器,执行具体的controller,并将controller返回的ModelAndView返回给前端控制器
- 前端控制器将ModelAndView传给视图解析器,视图解析器解析后返回具体view
- 前端控制器根据view进行视图渲染,返回给用户

15.spring的bean是线程安全的吗?
实际上⼤部分时候 spring bean ⽆状态的(⽐如 dao 类),所有某种程度上来说 bean 也是安全的,但如果 bean
有状态的话(⽐如 view model 对象),那就要开发者⾃⼰去保证线程安全了,最简单的就是改变 bean 的作⽤
域,把“singleton”变更为“prototype”,这样请求 bean 相当于 new Bean()了,所以就可以保证线程安全了。
如果要保证线程安全
1.可以将bean的作用域改为prototype,比如像Model View。
2.采用ThreadLocal来解决线程安全问题。ThreadLocal为每个线程保存一个副本变量,每个线程只操作自己的副本变量。
拓展:mybatis如何实现分页?
Limit,pagehelper,rowbounds
六、SpringCloud微服务
1. 什么是 SpringBoot?
Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用Spring 的难度,简省了繁重的配置,提供了各种启动器,使开发者能快速上手。
2. SpringBoot与SpringCloud 区别
- SpringBoot可以单独使用,它不依赖于SpringCloud 。而SpringCloud 必然依赖于SpringBoot,属于依赖关系。
- SpringBoot专注于快速方便的开发单个个体微服务。
- SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来,为各个微服务之间提供,配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务
3.Springboot启动流程?
1.SpringApplication类初始化
2.执行SpringApplication类的run方法
2.1获取并启动监听器
2.2构造应用上下文环境
2.3初始化应用上下文
2.4刷新应用上下文前的准备阶段,prepareContext()方法。
2.5刷新应用上下文
2.6发布容器启动完成事件
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.sources = new LinkedHashSet();
this.bannerMode = Mode.CONSOLE;
this.logStartupInfo = true;
this.addCommandLineProperties = true;
this.addConversionService = true;
this.headless = true;
this.registerShutdownHook = true;
this.additionalProfiles = new HashSet();
this.isCustomEnvironment = false;
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = this.deduceMainApplicationClass();
}
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
this.configureHeadlessProperty();
SpringApplicationRunListeners listeners = this.getRunListeners(args);
listeners.starting();
Collection exceptionReporters;
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
this.configureIgnoreBeanInfo(environment);
Banner printedBanner = this.printBanner(environment);
context = this.createApplicationContext();
exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
this.refreshContext(context);
this.afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
}
listeners.started(context);
this.callRunners(context, applicationArguments);
} catch (Throwable var10) {
this.handleRunFailure(context, var10, exceptionReporters, listeners);
throw new IllegalStateException(var10);
}
try {
listeners.running(context);
return context;
} catch (Throwable var9) {
this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
throw new IllegalStateException(var9);
}
}
4.springboot自动配置原理
- 1.通过注解@SpringBootApplication=>@EnableAutoConfiguration=>@Import({AutoConfigurationImportSelector.class})实现自动装配
- 2.AutoConfigurationImportSelector类中重写了ImportSelector中selectImports方法,批量返回需要装配的配置类
- 3.通过Spring提供的SpringFactoriesLoader机制,扫描classpath下META-INF/spring.factories文件,读取需要自动装配的配置类
- 4.依据条件筛选的方式,把不符合的配置类移除掉,最终完成自动装配
5.Spring Cloud和dubbo区别?
- 通信方式
Dubbo 使用的是 RPC 通信;Spring Cloud 使用的是 HTTP RestFul 方式。 - 注册中心
Dubbo 使用 ZooKeeper(官方推荐),还有 Redis、Multicast、Simple 注册中心,但不推荐。;
Spring Cloud 使用的是 Spring Cloud Netflflix Eureka。 - 监控
Dubbo 使用的是 Dubbo-monitor;Spring Cloud 使用的是 Spring Boot admin。 - 断路器
Dubbo 在断路器这方面还不完善,Spring Cloud 使用的是 Spring Cloud Netflflix Hystrix。
分布式配置、网关服务、服务跟踪、消息总线、批量任务等。 Dubbo 不完善,而 Spring Cloud 都有相应的组件来支撑。
6.dubbo的工作原理
- Container服务容器负责启动,加载以及运行Provider服务提供者
- Provider服务提供者启动时,需要将自身暴露出去让远程服务器可以发现,同时向Registry注册中心注册自己提供的服务
- Consumer服务消费者启动时,向Registry注册中心订阅所需要的服务
- Registry注册中心返回服务提供者列表给消费者,同时如果发生变更,注册中心将基于长连接推送实时数据给消费者
- 服务消费者需要调用远程服务时,会从提供者的地址列表中,基于负载均衡算法选出一台提供者服务器进行调用,如果调用失败,会基于集群容错策略进行调用重试
- 服务消费者与提供者会在内存中统计调用次数和调用时间,然后通过定时任务将数据发送给Monitor监控中心
7.springcloud常用组件?
常用组件有服务治理Eureka、配置中心config、远程调用feign、负载均衡Ribbon、服务熔断Hystrix、网关gateway
8.RestTemplate怎么实现负载均衡的?
在使用了@LoadBalanced后,Spring容器在启动的时候会为被修饰过的RestTemplate添加拦截器,拦截器里会使用LoadBalanced相关的负载均衡接口来处理请求,通过这样一个间接的处理,会使原来的RestTemplate变得不是原来的RestTemplate了,具备了负载均衡的功能
9.Feign跟RestTemplate的区别
FeignClient简化了请求的编写,且通过动态负载进行选择要使用哪个服务进行消费,而这一切都由Spring动态配置实现RestTemplate还需要写上服务器IP这些信息等
10.熔断限流的理解?
- SprngCloud中用Hystrix组件来进行降级、熔断、限流
- 熔断是对于消费者来讲,当对提供者请求时间过久时为了不影响性能就对链接进行熔断,
- 限流是对于提供者来讲,为了防止某个消费者流量太大,导致其它更重要的消费者请求无法及时处理。
- 限流可用通过拒绝服务、服务降级、消息队列延时处理、限流算法来实现
11.常用限流算法
- 计数器算法:Redis 的 INCR 命令 + 过期时间实现固定窗口
- 漏桶算法:一般使用消息队列来实现,系统以恒定速度处理队列中的请求,当队列满的时候开始拒绝请求。
- 令牌桶算法:计数器算法和漏桶算法都无法解决突然的大并发,令牌桶算法是预先往桶中放入一定数量token,然后用恒定速度放入token直到桶满为止,所有请求都必须拿到token才能访问系统
- 滑动窗口:将固定窗口拆分为多个更小的子窗口(如 1 秒拆分为 10 个 100ms 子窗口),实时滑动计算最近一个完整窗口内的总请求数,超过则限流。
限流组件:sentinel



12.如何自定义Starter
参考链接:Springboot自定义Starter启动器
1.引入POM依赖;
2.配置和配置文件对应的xxxProperties类;
3.配置业务类;
4.配置自动配置xxxAutoConfiguration类;
5.配置spring.factories文件;
13.各自注册中心比较

七、redis篇
1.redis数据类型及应用场景
- String(字符串)类型 key/ value
应用场景:1.缓存结构体信息 2.incr命令实现自增或自减的计数器 3.分布式锁 - Hash 对象的键是一个字符串类型,值是一个键值对集合。
应用场景:该类型非常适合于存储对象的信息(结构体信息)。如一个用户有姓名,密码,年龄等信息,购物车。 - List 可以向Redis列表的头部或尾部添加元素。
应用场景:1、消息队列 2、list可用于秒杀抢购场景 - Set 无序集合,集合成员是唯一的。
应用场景:共同关注的人、共同喜好、朋友圈点赞,可能认识的人 - Zset (有序集合类型)
Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。每个元素都会关联一个double类型的分数。
应用场景:1、热搜 2.排行榜 - HyperLogLog 是一种算法,它提供了不精确的去重计数方案。可以用来统计网站的登陆人数以及其他指标
- Geo主要用来存储地理位置信息
- Bitmap 通常用于处理一些状态、计数等需求。统计网站活跃用户 统计用户是否在线、用户每个月的签到情况
2.redis为什么快?
- 纯内存操作
- 数据结构简单,对数据操作简单
- redis执行命令是单线程的,避免了上下文切换带来的性能问题,也不用考虑锁的问题
- 采用了非阻塞的io多路复用机制,使用了单线程来处理并发的连接;内部采用的epoll+自己实现的事件分离器
redis执行命令是单线程的,Redis6.0 以后的版本里,网络 IO 的部分变为了多线程处理,以提高性能。
3.redis持久化机制
- RDB(Redis DataBase)持久化是一种基于快照的持久化方式。在指定的时间间隔内,如果满足一定条件(如某段时间内发生的写操作次数),Redis会生成一个包含当前内存数据的RDB文件。这个RDB文件可以用于数据恢复或备份。
- AOF(Append Only File)持久化是一种基于日志的持久化方式。Redis将所有的写操作命令记录到一个AOF文件中。当Redis重新启动时,可以通过重放AOF文件中的命令来恢复数据。AOF持久化提供了更高的数据安全性,可以保证数据的完整性。然而,与RDB持久化相比,AOF文件通常较大,数据加载速度较慢。
- 混合持久化(RDB + AOF)
混合持久化结合了RDB持久化和AOF持久化的优点,可以在保证数据安全性的同时,提供较快的数据加载速度。
在这种持久化方式下,Redis会同时生成RDB文件和AOF文件。当Redis重新启动时,优先使用AOF文件恢复数据,以确保数据的完整性。混合持久化适用于对数据安全性和性能要求较高的场景。
4.Redis如何实现key的过期删除?
采用的定期过期+惰性过期
定期删除 :Redis 每隔一段时间从设置过期时间的 key 集合中,随机抽取一些 key ,检查是否过期,如果已经过期做删除处理。
惰性删除 :Redis 在 key 被访问的时候检查 key 是否过期,如果过期则删除。
5.Redis缓存穿透如何解决?
缓存穿透是指频繁请求客户端和缓存中都不存在的数据,缓存永远不生效,请求都到达了数据库。
解决方案:
(1)接口校验
(2)对空值进行缓存:找不到的数据也缓存起来,并设置过期时间,可能会造成短期不一致
(3)布隆过滤器:在客户端和缓存之间添加一个过滤器,拦截掉一定不存在的数据请求
6.Redis如何解决缓存击穿?
缓存击穿是值一个key非常热点,key在某一瞬间失效,导致大量请求到达数据库
解决方案:
(1)设置热点数据永不过期
(2)使用互斥锁,缺点是性能低
7.Redis如何解决缓存雪崩?
缓存雪崩是值某一时间Key同时失效或缓存服务故障宕机,导致大量请求到达数据库
解决方案:
(1)搭建集群保证高可用
(2)进行数据预热,给不同的key设置随机的过期时间
(3)给缓存业务添加限流降级,通过加锁或队列控制操作redis的线程数量
(4)给业务添加多级缓存
8.Redis分布式锁的实现原理
原理是使用setnx+setex命令来实现,但是会有一系列问题:
(1)任务时常超过缓存时间,锁自动释放。可以使用Redision看门狗解决
(2)加锁和释放锁的不是同一线程。可以在Value中存入uuid,删除时进行验证。但是要注意验证锁和删除锁也不是一个原子性操作,可以用lua脚本使之成为原子性操作
(3)不可重入。可以使用Redision解决(实现机制类似AQS,计数)
(4)redis集群下主节点宕机导致锁丢失。使用红锁解决
9.Redis集群方案
(1)主从模式:一个master节点,多个slave节点,master节点宕机slave自动变成主节点
(2)哨兵模式:在主从集群基础上添加哨兵节点或哨兵集群,用于监控master节点健康状态,通过投票机制选择slave成为主节点
(3)分片集群:主从模式和哨兵模式解决了并发读的问题,但没有解决并发写的问题,因此有了分片集群。分片集群有多个master节点并且不同master保存不同的数据,master之间通过ping相互监测健康状态。客户端请求任意一个节点都会转发到正确节点,因为每个master都被映射到0-16384个插槽上,集群的key是根据key的hash值与插槽绑定
10.Redis集群主从同步原理
主从同步第一次是全量同步:slave第一次请求master节点会根据replid判断是否是第一次同步,是的话master会生成RDB发送给slave
后续为增量同步:在发送RDB期间,会产生一个缓存区间记录发送RDB期间产生的新的命令,slave节点在加载完后,会持续读取缓存区间中的数据
11.Redis缓存一致性解决方案
Redis缓存一致性解决方案主要思考的是删除缓存和更新数据库的先后顺序
分布式读写锁 RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(rwLock);
解决方案是用中间件canal订阅binlog日志提取需要删除的key,然后另写一段非业务代码去获取key并尝试删除,若删除失败就把删除失败的key发送到消息队列,然后进行删除重试。
延迟双删
异步更新 + 消息队列(最终一致)
流程:
更新数据库
发送消息到 MQ(Kafka/RabbitMQ)
消费 MQ → 删除缓存
消费失败自动重试
12.Redis缓存淘汰策略
- volatile-lru:针对设置了过期时间的key,使用LRU算法进行淘汰
- allkeys-lru:针对所有key使用LRU算法进行淘汰 (推荐) Least Recently Used–最近最少使用
- volatile-lfu:针对设置了过期时间的key,使用LFU算法进行淘汰 Least Frequently Used --最不经常使用
- allkeys-lfu:针对所有key使用LFU算法进行淘汰
- volatile-random: 从设置了过期时间的key中随机删除 --随机
- allkeys-random: 从所有key中随机删除
- volatile-ttl:删除生存时间最近的一个键
- noeviction(默认策略):不删除键,返回错误OOM,只能读取不能写入
13.如何解决超卖问题?
mysql排它锁
版本号
Redis放队列单线程扣减库存
redisson分布式锁
14.如何解决高并发问题?
集群,缓存,sql优化,读写分离,分库分表,限流,消息中间键异步处理
15.Redis哨兵模式(了解)
哨兵模式是Redis可用性的解决方案;它由一个或多个 sentinel 实例构成 sentinel 系统;该系统可以监视任意多个主库以及这些主库所属的从库;当主库处于下线状态,自动将该主库所属的某个从库升级为新的主库;客户端来连接集群时,会首先连接 sentinel,通过 sentinel 来查询主节点的地址,然后再连接主节点进行数据交互。当主节点发生故障时,客户端会重新向 sentinel 索要主库地址,sentinel 会将最新的主库地址告诉客户端。通过这样客户端无须重启即可自动完成节点切换。
哨兵模式当中涉及多个选举流程采用的是 Raft 算法的领头选举方法的实现:sentinel 会以每秒一次的频率向所有节点(其他sentinel、主节点、以及从节点)发送 ping 消息,然后通过接收返回判断该节点是否下线;如果在配置指定 down-after-milliseconds 时间内,sentinel收到的都是无效回复, 则被判断为主观下线;当一个 sentinel 节点将一个主节点判断为主观下线之后,为了确认这个主节点是否真的下线,它会向其他sentinel 节点进行询问,如果收到一定数量的已下线回复,sentinel 会将主节点判定为客观下线,并通过领头 sentinel 节点对主节点执行故障转移;主节点被判定为客观下线后,开始领头 sentinel 选举,需要一半以上的 sentinel 支持,选举领头sentinel后,开始执行对主节点故障转移;从从节点中选举一个从节点作为新的主节点通知其他从节点复制连接新的主节点。若故障主节点重新连接,将作为新的主节点的从节点。
缺点:redis 采用异步复制的方式,意味着当主节点挂掉时,从节点可能没有收到全部的同步消息,这部分未同步的消息将丢失。
如果主从延迟特别大,那么丢失可能会特别多。sentinel 无法保证消息完全不丢失,但是可以通过配置来尽量保证少丢失。
八、分布式事务 分布式锁
1.什么是分布式事务?
分布式事务就是为了保证不同数据库的数据一致性。要么全部成功,要么全部失败。
2.分布式锁
2.1 数据库悲观锁 利用 select … where … for update 排他锁
2.2 数据库乐观锁 基于CAS思想,是不具有互斥性,不会产生锁等待而消耗资源,操作过程中认为不存在并发冲突,只有 update version 失败后才能觉察到。
2.3 Redis分布式锁 基于REDIS的SETNX()、EXPIRE() 、GETSET()方法做分布式锁。 redisson
2.4 zookeeper分布式锁
- zookeeper获取和释放锁原理利用临时节点与 watch 机制。每个锁占用一个普通节点 /lock,当需要获取锁时在 /lock 目录下创建一个临时节点,创建成功则表示获取锁成功,失败则 watch/lock 节点,有删除操作后再去争锁。临时节点好处在于当进程挂掉后能自动上锁的节点自动删除即取消锁。
- zookeeper获取锁的顺序原理:上锁为创建临时有序节点,每个上锁的节点均能创建节点成功,只是其序号不同。只有序号最小的可以拥有锁,如果这个节点序号不是最小的则 watch 序号比本身小的前一个节点 (公平锁)。
3.如何提高系统的并发能力?
系统拆分,缓存,mq,分库分表,读写分离,es
4.分布式有哪些理论?
CAP理论:
C:一致性,这里指的强一致性,也就是数据更新完,访问任何节点看到的数据完全一致
A:可用性,就是任何没有发生故障的服务必须在规定时间内返回合正确结果
P:容灾性,当网络不稳定时节点之间无法通信,造成分区,这时要保证系统可以继续正常服务。提高容灾性的办法就是把数据分配到每一个节点当中,所以P是分布式系统必须实现的,然后需要在C和A中取舍。因此一般是 CP ,或者 AP。
Base 理论:采用适当的方式来使系统达到最终一致性。
5.你知道哪些分布式事务解决方案?
基于XA协议的两阶段提交2pc,三阶段提交3pc,tcc,本地消息表,消息事务,seata
基于XA协议的两阶段提交(2PC)
1 基于XA协议的两阶段提交(2PC)
两阶段提交协议(Two Phase Commitment Protocol)中,涉及到两种角色
一个事务协调者(coordinator):负责协调多个参与者进行事务投票及提交(回滚)
多个事务参与者(participants):即本地事务执行者
总共处理步骤有两个
(1)投票阶段(voting phase):协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。参与者将告知协调者自己的决策:同意(事务参与者本地事务执行成功,但未提交)或取消(本地事务执行故障);
(2)提交阶段(commit phase):收到参与者的通知后,协调者再向参与者发出通知,根据反馈情况决定各参与者是否要提交还是回滚;
如果任一资源管理器在第一阶段返回准备失败,那么事务管理器会要求所有资源管理器在第二阶段执行回滚操作。通过事务管理器的两阶段协调,最终所有资源管理器要么全部提交,要么全部回滚,最终状态都是一致的
优点: 尽量保证了数据的强一致(无法完全保证),适合对数据强一致要求很高的关键领域。
缺点:
同步阻塞问题:执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
单点故障:由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
数据不一致:在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。
二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交
2 三段提交(3PC)
三段提交是两段提交的升级版
CanCommit阶段:询问阶段

类似2PC的准备阶段,协调者向参与者发送CanCommit请求,询问是否可以执行事务提交操作,然后开始等待参与者的响应。
PreCommit阶段:事务执行但不提交阶段

协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作:
协调者从所有的参与者获得的反馈都是Yes响应
1.发送预提交请求协调者向参与者发送PreCommit请求;
2.参与者接收到PreCommit请求后,执行事务操作,并将undo(执行前数据)和redo(执行后数据)信息记录到事务日志中;
3.参与者成功的执行了事务操作,则返回ACK(确认机制:已确认执行)响应,同时开始等待最终指令。
有任何一个参与者向协调者发送了No响应,或者等待超时
1.协调者向所有参与者发送中断请求请求。
2.参与者收到来自协调者的中断请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。
doCommit阶段:事务提交阶段

执行提交
1.协调接收到所有参与者返回的ACK响应后,协调者向所有参与者发送doCommit请求。
2.参与者接收到doCommit请求之后,执行最终事务提交,事务提交完之后,向协调者发送Ack响应并释放所有事务资源。
3.协调者接收到所有参与者的ACK响应之后,完成事务。
中断事务
1.协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),协调者向所有参与者发送中断请求;
2.参与者接收到中断请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后,向协调者发送ACK消息,释放所有的事务资源。
3.协调者接收到参与者反馈的ACK消息之后,执行事务的中断。
优点:相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。
缺点:会导致数据一致性问题。由于网络原因,协调者发送的中断响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到中断命令并执行回滚的参与者之间存在数据不一致的情况。
3 TCC补偿机制
TTCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。三个阶段如下:
| 操作方法 | 含义 |
|---|---|
| Try | 预留业务资源/数据效验-尝试检查当前操作是否可执行 |
| Confirm | 确认执行业务操作,实际提交数据,不做任何业务检查。try成功,confirm必定成功 |
| Cancel | 执行业务出错时,需要回滚数据的状态下执行的业务逻辑 |
其核心在于将业务分为两个步骤完成。不依赖事务协调器对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。
TCC属于应用层的一种补偿方式,需要程序员在实现的时候多写很多补偿的代码,复杂业务场景下代码逻辑非常复杂。
幂等性无法确保。
4 本地消息表(异步确保)
本地消息表这种实现方式应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,这种思路是来源于ebay。我们可以从下面的流程图中看出其中的一些细节:

工作流程:
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。
优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
5 MQ 事务消息
完整流程图:
优点: 实现了最终一致性,不需要依赖本地数据库事务。
缺点: 目前主流MQ中只有RocketMQ支持事务消息。
- 生产者先发送一条半事务消息到MQ
- MQ收到消息后返回ack确认
- 生产者开始执行本地事务
- 如果事务执行成功发送commit到MQ,失败发送rollback
- 如果MQ长时间未收到生产者的二次确认commit或者rollback,MQ对生产者发起消息回查
- 生产者查询事务执行最终状态
- 根据查询事务状态再次提交二次确认
6.Seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata的优点
对业务无侵入:即减少技术架构上的微服务化所带来的分布式事务问题对业务的侵入
高性能:减少分布式事务解决方案所带来的性能消耗(2PC)
AT模式
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:
提交异步化,非常快速地完成。
回滚通过一阶段的回滚日志进行反向补偿。
第一阶段:本地数据备份阶段
1.Seata 的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在变化前后的数据镜像组织成回滚日志(XID/分支事务ID(Branch ID/变化前的数据/变化后的数据)。
2.将回滚日志存入一张日志表UNDO_LOG(需要手动创建),并对UNDO_LOG表中的这条数据形成行锁(for update)。
3.若锁定失败,说明有其他事务在操作这条数据,它会在一段时间内重试,重试失败则回滚本地事务,并向TC汇报本地事务执行失败。这样,可以保证:任何提交的业务数据的更新一定有相应的回滚日志存在

第二阶段:全局事务提交/回滚
全局提交:
所有分支事务此时已经完成提交,所有分支事务提交都正常。
TM从TC获知后会决议执行全局提交,TC异步通知所有的RM释放UNDO_LOG表中的行锁,同时清理掉UNDO_LOG表中刚才释放锁的那条数据。
全局回滚:
若任何一个RM一阶段事务提交失败,通知TC提交失败。
TM从TC获知后会决议执行全局回滚,TC向所有的RM发送回滚请求。
RM通过XID和Branch ID找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚,同时释放锁,清除UNDO_LOG表中释放锁的那条数据。
TCC模式
seata也针对TCC做了适配兼容,支持TCC事务方案,原理前面已经介绍过,基本思路就是使用侵入业务上的补偿及事务管理器的
协调来达到全局事务的一起提交及回滚。

AT 模式基于 支持本地 ACID 事务 的 关系型数据库:
一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。
相应的,TCC 模式,不依赖于底层数据资源的事务支持:
一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
二阶段 commit 行为:调用 自定义 的 commit 逻辑。
二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
Saga 模式
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
Saga的实现:
基于状态机引擎的 Saga 实现


状态机引擎是无状态的,它是内嵌在应用中。
当应用正常运行时(图中上半部分):
状态机引擎会上报状态到Seata Server;
状态机执行日志存储在业务的数据库中;
当一台应用实例宕机时(图中下半部分):
Seata Server 会感知到,并发送事务恢复请求到还存活的应用实例;
状态机引擎收到事务恢复请求后,从数据库里装载日志,并恢复状态机上下文继续执行;
九、MQ消息中间件
1. 为什么要使用MQ,有什么优缺点?
优点
解耦、异步、削峰。
缺点
增加了系统的复杂度:因为一个系统引入了MQ之后会造成系统的复杂性的提升,复杂性提升后,增加MQ的维护成本。
降低的系统的可用性:复杂性的提升意味这系统可用性的降低,因为MQ一旦出现问题就会造成系统出现问题。
一致性问题:因为MQ是异步处理消息,需要处理类似于消息丢失以及重复消费的问题,一旦处理不好就会造成重复消费问题。
2.消息可靠性
🚀 RocketMQ 保证不丢:
同步发送 + 同步刷盘 + 同步复制 + 手动 ACK
🐰 RabbitMQ 保证不丢:
生产者 Confirm + 消息 / 队列持久化 + 手动 ACK
同步、异步刷盘
同步刷盘:当消息持久化到 broker 的磁盘后才算是消息写入成功。
异步刷盘:当消息写入到 broker 的内存后即表示消息写入成功,无需等待消息持久化到磁盘。
2.2RabbitMQ导致的死信的几种原因?
消息被拒( Basic.Reject /Basic.Nack ) 且 requeue = false 。
消息TTL过期。
队列满了,无法再添加。
3.如何保证消息的顺序性?
生产者生产的消息会放置在队列中,基于队列先进先出的特性天然的可以保证存入队列的消息顺序和拉取的消息顺序是一致的.
RabbitMQ:一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理
RocketMQ:生产者投递到同一个队列 ,消费者有序消费。
RockerMQ的MessageListener回调函数提供了两种消费模式,有序消费模式MessageListenerOrderly和并发消费模式MessageListenerConcurrently。在消费的时候,还需要保证消费者注册MessageListenerOrderly类型的回调接口实现顺序消费,如果消费者采用Concurrently并行消费,则仍然不能保证消息消费顺序。
4.为什么会出现重复消费?如何防止消息重复消费?
1.发送方消息重试 RocketMQ:producer.setRetryTimesWhenSendFailed(3);
2 消费方消息重试 RocketMQ:msg.getReconsumeTimes()获取重试次数进行控制
消费端处理消息的业务逻辑保持幂等性
保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现
5.消息积压处理
消息积压核心:生产太快、消费太慢、消费异常重试
急救最快手段:扩容消费者 + 优化慢消费
关键前提:队列数 ≥ 消费者并发数
根本解决:批量消费、异步化、拆分 Topic、监控告警
对于 RocketMQ,官方针对消息积压问题,提供了解决方案。
- 提高消费并行度2. 批量方式消费3. 跳过非重要消息4. 优化每条消息消费过程
增加消费者数量:通过增加消费者的数量来提高消息的处理速度。可以根据系统的负载情况动态地增加或减少消费者的数量。
提高消费者的处理能力:可以通过优化消费者的代码逻辑、提升消费者的性能等方式来提高消费者的处理能力,从而加快消息的消费速度。
增加消息队列的吞吐量:可以通过增加消息队列的并发处理能力来提高消息的处理速度。可以考虑增加队列的分区、增加消息的分发策略等方式来提高消息队列的吞吐量。
设置消息的过期时间:可以设置消息的过期时间,当消息在队列中超过一定时间还未被消费时,可以将其丢弃或进行其他处理
监控和报警:可以通过监控消息队列的积压情况,并及时报警,以便及时发现和解决消息积压的问题。
使用延迟队列:可以使用延迟队列来实现消息的延时处理,将消息发送到延迟队列中,然后在指定的时间后再进行消费
6.如何解决消息队列的延时及过期失效问题?
- 延时队列:可以使用延时队列来处理延时消息。延时队列是一种特殊的消息队列,它可以在指定的时间后将消息重新投递到主队列中。当消息到达延时时间后,将其重新发送到主队列,以便消费者可以处理。
- 过期失效策略:可以为消息设置过期时间,在消息发送时为每个消息设置一个过期时间戳。当消息过期时,可以选择将其丢弃或者进行特殊处理。消费者处理消息时,可以检查消息的过期时间,如果消息已经过期,则可以选择不处理或者进行相应的理。
- 定时任务:可以使用定时任务来处理延时消息。在发送消息时,可以将消息的执行时间记录下来,并使用定时任务来定期检查是否有需要执行的消息。当定时任务触发时,可以将需要执行的消息发送到主队列中,供消费者处理。
- 重试机制:对于延时或过期失效的消息,可以采取重试机制。当消息未能及时处理或者过期失效时,可以将其重新发送到队列中,供消费者再次尝试处理。可以设置最大重试次数,超过次数后可以选择丢弃或者进行其他处理。
7.MQ如何保证分布式事务的最终一致性?
rocketmq支持事务
RabbitMQ:
1、确认生产者一定要将数据投递到MQ服务器中(采用MQ消息确认机制)
2、MQ消费者消息能够正确消费消息,采用手动ACK模式(注意重试幂等性问题)
3、如何保证第一个事务先执行,采用补偿机制,在创建一个补单消费者进行监听,如果订单没有创建成功,进行补单。
8.描述一下消息中间件Ack确认机制?
ACK机制是消费者从RabbitMQ收到消息并处理完成后,反馈给RabbitMQ,RabbitMQ收到反馈后才将此消息从队列中删除。如果一个消费者在处理消息出现了网络不稳定、服务器异常等现象,那么就不会有ACK反馈,RabbitMQ会认为这个消息没有正常消费,会将消息重新放入队列中。
9.MQ延迟消息如何实现?
1.可以采用RabbitMQ的死信队列来实现延时队列,RabbitMQ可以针对Queue和Message设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为dead letter,dead letter会被投递到死信交换器,然后通过死信队列将消息发送出去

2.RabbitMQ 将消息发送到延迟交换机,消息达到延迟时间会投递到对应的队列
3.RocketMQ 可以实现18个等级的消息延时,但是不可以实现任意时间的消息延时,使用RocketMQ的延时消息只需要按照正常消息发送,并指定延时等级即可,简单高效,并且这个延时时间可以在RocketMQ的配置参数中进行配置。具体的时间可以为1s、 5s、 10s、 30s、 1m、 2m、 3m、 4m、 5m、 6m、 7m、 8m、 9m、 10m、 20m、 30m、 1h、 2h。
10.怎样选型MQ?

11.RocketMQ的事务消息实现原理?
1.生产者先发送一条半事务消息到MQ
2.MQ收到消息后返回ack确认
3.生产者开始执行本地事务
4.如果事务执行成功发送commit到MQ,失败发送rollback
5.如果MQ长时间未收到生产者的二次确认commit或者rollback,MQ对生产者发起消息回查
6.生产者查询事务执行最终状态
7.根据查询事务状态再次提交二次确认
12.RocketMQ Consumer消息分配模式:
集群模式:consumer.setMessageModel(MessageModel.CLUSTERING);
广播模式:consumer.setMessageModel(MessageModel.BROADCASTING);
13.RocketMQ存储
消息主体以及元数据都存储在CommitLog当中;
Consume Queue相当于kafka中的partition,是一个逻辑队列,存储了这个Queue在
CommiLog中的起始offset,log大小和MessageTag的hashCode;
每次读取消息队列先读取consumerQueue,然后再通过consumerQueue去commitLog中拿到
消息主体。

14.RabbitMQ的工作模式
一.simple简单模式
二.WorkQueues工作队列模式(资源的竞争)
三.Publish/Subscribe发布订阅(共享资源)
四.Routing路由模式
五.Topics 主题模式(路由模式的一种)
六.RPC远程调用模式
七.Publisher Confirms发布者确认模式



十、设计模式
设计模式分类
创建型模式:单例模式、工厂方法模式、抽象工厂模式、创建者模式、原型模式。
结构型模式:适配器模式、代理模式、装饰器模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、
访问者模式、中介者模式、解释器模式。
Java设计模式–原则
单一职责原则:一个类只负责一个功能领域中的相应职责
开闭原则:对扩展开放,对修改关闭
里氏代换原则:所有引用基类(父类的)地方都可以用子类来替换,且程序不会有任何的异常。
依赖倒转原则:要针对接口编程,而不是针对实现编程。
接口隔离原则:
1.使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口
2:类间的依赖关系应该建立在最小的接口上
合成复用原则:尽量使用对象组合,而不是继承来达到复用的目的。
以下是常用的设计模式的实战:
参考博文:一文讲完Java常用设计模式(全23种)
单例模式
单例模式(Singleton Pattern)用于确保某个类在整个应用程序中只有一个实例,并提供一个全局访问点来获取该实例。
写法:懒汉式,饿汉式,静态内部类,双重校验锁,枚举
观察者模式
观察者模式又称发布-订阅模式。观察者模式是一种通知机制,多个观察者对某个事物(被观察方)感兴趣时,事物(被观察方)一有动作就通知观察者。观察者模式能让发送通知的一方(被观察方)和接收通知的一方(观察者)彼此分离,减少耦合。
以生活中的微信公众号为例:我们对某个微信公众号感兴趣,关注了这个微信公众号后能收到新的文章的通知,了解最新的资讯。这是一种典型的观察者模式,用户(观察者)关注公众号(被观察者),公众号(被观察者)更新文章通知所有用户(观察者)。
以项目中的实际场景为例:在取消订单的时候,后边要跟一系列操作,比如:增加对应商品的库存、增加对应用户的账户余额。这里用观察者模式就很合适,取消订单这个动作是个被观察者,库存业务和余额业务都是观察者。
策略模式
策略模式的含义:定义一系列可互相替换的算法,并且每个算法独立封装,然后在运行的时候动态替换。
策略模式的核心:一个抽象+多个具体的实现,策略持有类要持有实现类的集合(要用抽象类替代),程序调用时根据类型去策略持有类中找到对应的实现类,然后调用实现类的具体方法。
策略模式的好处有哪些?
添加策略时很方便,只添加一个策略的实现类即可,符合开闭原则(对新增开放,对修改关闭)
逻辑解耦。每个策略负责自己的事情,调用方去选择使用哪个策略。符合单一职责原则。
提高了代码优雅度。调用方减少了对业务的if else的判断,直接给策略传入相应的类型即可。
责任链模式
使多将请求的发送者和接收者进行解耦,使多个接受者都有机会处理请求,这些接收者连成一条链,请求沿着这条链传递,直到有一个对象处理它为止。
还有一种责任链模式是:链上的所有接收者都要处理。比如:员工的转正申请,从小领导到大领导,每个领导都进行审批。
实际项目场景:
用户取消订单时,要按顺序处理:退回商品的库存、退回用户的余额、退回用户的优惠券。
在校验权限时,要按顺序判断:校验token是否过期、校验是否有这个url权限、是否被拉黑。有一个校验不通过则不允许请求。
责任链模式的好处有哪些?
调用者与链上的各个被调用者进行解耦。
各个被调用者只需负责自己的逻辑。(符合单一职责原则)
添加一个被调用者很方便,在链上加一个即可。(符合开闭原则)
提高了代码的可维护性、扩展性。
代理模式
代理模式的含义:使用代理对象来代替对真实对象的访问。
作用是:可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
适配器模式
适配器模式:将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器。
生活场景:如果去美国,我们随身带的电器是无法直接使用的,因为美国的插座标准和中国不同,所以,我们需要一个适配器。
实际项目场景
SpringMVC支持多种数据,比如:HTTP请求、WebSocket请求。当请求进来时,SprinvMVC就会根据请求去获取适配器,然后去调用HTTP的处理器或者WebSocket的处理器。
建造者模式
含义:将一个对象的构建过程与这个对象分离,使得可以很方便的用构建过程创建不同属性的对象。
建造者模式可以做到:
可以只指定某几个属性
可以肉眼可见地给某个属性赋值,不需要再点进去看构造方法的定义
lombok可以自动生成Builder,方法是:在实体类上加个@Builder即可。
工厂模式
含义:将对象的实例化封装在工厂类中,将对象的创建与使用分离。也就是说:是用工厂类的方法来代替 new 操作的。
工厂模式的优点
降低耦合度,对象的创建与使用分离,使用者无需关心创建对象的细节,符合单一职责原则。
使代码更简洁,易维护
享元模式:
利用共享技术有效地支持大量细粒度的对象,享元模式能够解决重复对象的内存浪费问题。
应用场景是需要缓冲池的场景,比如String常量池、数据库连接池。
模版模式:
是一种基于继承实现的设计模式,它是行为型的模式。就是将某一行为制定一个框架,然后子类填充细节。
迭代器模式:
Java迭代器模式是一种行为设计模式,它提供了一种访问集合对象元素的方法,而不需要暴露该对象的内部表示。该模式适用于需要遍历集合对象的场景,例如数组、列表、树等。 while (iterator.hasNext())
原型模式:
原型设计模式允许通过复制现有对象来创建新对象,而不是通过实例化类来创建新对象。
在需要创建大量相似对象时非常有用,它可以避免重复创建对象,从而提高性能,并且可以根据需要实现浅拷贝或深拷贝。
在Java中,原型模式的实现通常涉及到实现Cloneable接口和重写clone()方法。
外观模式:
外观模式(Facade Pattern)是一种结构型设计模式,它提供了一个简单的接口来访问复杂系统中的子系统,从而隐藏了子系统的复杂性。外观模式属于对象型模式,它通过创建一个外观类,将客户端与子系统解耦,使得客户端只需要与外观类交互即可完成操作。
数据结构与算法
参考:努力的老周
数据算法篇
冒泡排序
冒泡排序(Bubble Sort)是一种简单的排序算法,其基本思想是对待排序的元素从前向后依次比较相邻的两个元素,如果顺序不对则交换它们的位置,一轮比较下来,最大的元素就会“冒泡”到数组的末尾。
重复这个过程,直到没有需要交换的元素为止,排序完成。

public static void bubbleSort(int []arr) {
for(int i =1;i<arr.length;i++) {
for(int j=0;j<arr.length-i;j++) {
if(arr[j]>arr[j+1]) {
int temp = arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
}
选择排序
工作原理是每一次从待排序的数据元素中选出最小(最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。

public class SelectionSortExample {
public static void main(String[] args) {
int[] arr = {5, 1, 9, 3, 7};
selectionSort(arr);
}
public static void selectionSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int minIdx = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIdx]) {
minIdx = j;
}
}
int temp = arr[i];
arr[i] = arr[minIdx];
arr[minIdx] = temp;
System.out.println(Arrays.toString(arr));
}
}
}
插入排序
维护一个有序区,将数据一个一个插入到有序区的适当位置,直到整个数组都有序。即每步将一个待排序的记录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。

public class InsertSort {
public static void main(String[] args) {
int[] a = {12, 15, 9, 20, 6, 31, 24};
Sort(a);//调用方法
}
public static void Sort(int[] s) {
for (int i = 1; i < s.length; i++) {//注意i不能等于数组长度,因为数组下标从零开始而不是从1.
int temp = s[i];//存储待插入数的数值
int j;//j必须在此声明,如果在for循环内声明,j就不能拿出for循环使用,最后一步插入就无法执行
for (j = i - 1; j >= 0 && temp < s[j]; j--) {
s[j + 1] = s[j];//如果大于temp,往前移动一个位置
}
s[j + 1] = temp;//将temp插入到适当位置
}
System.out.println(Arrays.toString(s));//输出排好序的数组
}
}
快速排序
划分是快速排序的根本机制。划分数据就是把数组分为两组,使所有关键字大于特定值的数组项在一组,使所有关键字小于特定值的数据在另一组。
左右指针法:
定义一个begin指向第一个元素,定义一个end指向最后一个元素。令第一个元素为key,begin向后找大于key的值,end向前找小于key的值,此时把begin跟end位置的值交换,直到begin大于等于end时结束。



三数据项取中
N个数据项数组的最坏的划分情况是一个子数组只有一个数据项,另外一个子数组包含N-1个数据项

为了避免我们选取的枢纽是数据项中最大或最小的,我们需要一种能够避免且简单的选取办法,办法如下:
- 随机选出一个
- 遍历整个待划分数组,选出最适合当枢纽的数据项
- 取头,中,尾三个元素,已中值为枢纽

归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
归并排序算法有两个基本的操作,一个是分,也就是把原数组划分成两个子数组的过程。另一个是治,它将两个有序数组合并成一个更大的有序数组。
1.将待排序的线性表不断地切分成若干个子表,直到每个子表只包含一个元素,这时可以认为只包含一个元素的子表是有序表。
2.将子表两两合并,每合并一次,就会产生一个新的且更长的有序表,重复这一步骤,直到最后只剩下一个子表,这个子表就是排好序的线性表。
图解算法
假设我们有一个初始数列为{8, 4, 5, 7, 1, 3, 6, 2},整个归并排序的过程如下图所示。

可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
合并两个有序数组流程
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。


网路编程(拓展)
1.socket通信流程
(1)服务端创建socket并调用bind()方法绑定ip和端口号
(2)服务端调用listen()方法建立监听,此时服务的scoket还没有打开
(3)客户端创建socket并调用connect()方法像服务端请求连接
(4)服务端socket监听到客户端请求后,被动打开,调用accept()方法接收客户端连接请求,当accept()方法接收到客户端connect()方法返回的响应成功的信息后,连接成功
(5)客户端向socket写入请求信息,服务端读取信息
(6)客户端调用close()结束链接,服务端监听到释放连接请求后,也结束链接
linux系列(拓展)
1.linux常用命令
ifconfig:查看网络接口详情
ping:查看与某主机是否能联通
ps -ef|grep 进程名称:查看进程号
lost -i 端口 :查看端口占用情况
top:查看系统负载情况,包括系统时间、系统所有进程状态、cpu情况
free:查看内存占用情况
kill:正常杀死进程,发出的信号可能会被阻塞
kill -9:强制杀死进程,发送的是exit命令,不会被阻塞
分库分表(拓展)
分库分表是在海量数据下,由于单库、表数据量过大,导致数据库性能持续下降的问题,演变出的技术方案
分库分表共分为四种方式:水平分库、水平分表、垂直分库、垂直分表
执行流程
sql解析→查询优化→sql路由→sql改写→sql执行→结构归并
分库分表最佳实践
系统设计之初就应该对业务数据的耦合进行考量,从而进行垂直分库分表,使数据结构清晰明了,若非必要无需进行水平切分,应从缓存技术着手降低对数据库的访问压力。如果缓存使用过后,数据库访问还是非常大,可以考虑数据库读写分离原则,若数据库压力依然大,且业务数据增长无法估量,最后可以考虑水平分库分表,单表数据控制在1000万以内。
数据库设计规范(拓展)
(一)基础规范
1、表存储引擎必须使用InnoD,表字符集默认使用utf8,必要时候使用utf8mb4
解读:
(1)通用,无乱码风险,汉字3字节,英文1字节
(2)utf8mb4是utf8的超集,有存储4字节例如表情符号时,使用它
2、禁止使用存储过程,视图,触发器,Event
3、禁止在数据库中存储大文件,例如照片,可以将大文件存储在对象存储系统,数据库中存储路径
4、禁止在线上环境做数据库压力测试
5、测试,开发,线上数据库环境必须隔离
(二)命名规范
1、库名,表名,列名必须用小写,采用下划线分隔
2、库名,表名,列名必须见名知义,长度不要超过32字符
3、库备份必须以bak为前缀,以日期为后缀
4、从库必须以-s为后缀
5、备库必须以-ss为后缀
(三)表设计规范
1、单实例表个数必须控制在2000个以内
2、单表分表个数必须控制在1024个以内
3、表必须有主键,推荐使用UNSIGNED整数为主键
4、禁止使用外键,如果要保证完整性,应由应用程式实现
5、建议将大字段,访问频度低的字段拆分到单独的表中存储,分离冷热数据
(四)列设计规范
1、根据业务区分使用tinyint/int/bigint,分别会占用1/4/8字节
2、根据业务区分使用char/varchar
3、根据业务区分使用datetime/timestamp
4、必须把字段定义为NOT NULL并设默认值
5、使用INT UNSIGNED存储IPv4,不要用char(15)
6、使用varchar(20)存储手机号,不要使用整数
7、使用TINYINT来代替ENUM
(五)索引规范
1、唯一索引使用uniq_[字段名]来命名
2、非唯一索引使用idx_[字段名]来命名
3、单张表索引数量建议控制在5个以内
4、组合索引字段数不建议超过5个
5、不建议在频繁更新的字段上建立索引
6、非必要不要进行JOIN查询,如果要进行JOIN查询,被JOIN的字段必须类型相同,并建立索引
7、理解组合索引最左前缀原则,避免重复建设索引,如果建立了(a,b,c),相当于建立了(a), (a,b), (a,b,c)
(六)SQL规范
1、禁止使用select *,只获取必要字段
2、insert必须指定字段,禁止使用insert into T values()
3、隐式类型转换会使索引失效,导致全表扫描
4、禁止在where条件列使用函数或者表达式
5、禁止负向查询以及%开头的模糊查询
6、禁止大表JOIN和子查询
7、同一个字段上的OR必须改写问IN,IN的值必须少于50个
8、应用程序必须捕获SQL异常
场景题
1.雪花算法如何解决时间回拨?
答:
时间回拨是指服务器系统时间倒退,会导致雪花算法生成重复 ID。
最常用解决方案:
a.记录上一次时间戳,发现回拨直接抛出异常或线程等待时间追上;
b.企业级方案会启用备用机器 ID 位或备用序列号,在回拨期间不依赖时间戳生成 ID,保证不重复;
c.分布式时钟同步 NTP时间同步服务器
d.美团leaf
核心目标:避免时间戳变小,保证 ID 单调递增不重复。
2.订单到期如何关闭?
3.如何在不加锁的情况下解决线程安全问题?
threadlocal 版本号 用 Atomic 原子类(CAS 无锁)
873

被折叠的 条评论
为什么被折叠?



