开场三连问
极越汽⻋ Java ⾯试
有个 24 届毕业⽣的读者,校招的时候拿到这个公司 offer ,刚开始选择这个公司的时候,看到 2
个⼤股东是百度和吉 利,觉得虽然是新势⼒的新能源⻋⼚,但是有 2 个靠⼭,应该 2-3 年内不会
倒,所以就⼊职了极越。
结果没想到⼊职了半年,公司突然就要没了,现在领了⼤礼包⾛⼈了,开始重新找⼯作了。
这个事 情不出,我还真不知道有款⻋叫极越汽⻋,新能源汽⻋赛道现在准备进⼊到下半场了,后
⾯没能能 冲出决赛圈的公司,⼤概率也会⾯临“出局”,新能源汽⻋现状如下图,能进⼊决赛圈的有
⼏家?

新能源汽⻋赛道还是太卷了,太烧钱了,⼤家往新能源汽⻋⼚跳的时候,⼀定要多关注公司的财
报和销量情况,是否是连续⼏年都是亏损状态,持续再烧钱,如果⼀直都没有盈利,还是挺危险
的,很容易⾯临突然的组织 架构 调整,可能你所在的部⻔就没了。
虽然互联⽹公司也会有组织 架构 的变化 ,但是现在留下来的互联⽹⼤⼚,也基本在 10 年前互联⽹
⼤战厮杀中存活下来了,公司业务基本盘还是杀出⼀定的市场了,相对也⽐较成熟了。
那话说回来,极越汽⻋ Java 开发岗的⾯试难得如何?
这⾥不是⿎励⼤家去⾯极越汽⻋,⽽是⼀起看看 新能源⻋企公司对于 Java 开发岗位,考察的地⽅
有哪些重点知识。
这次就来分享⼀位同学极越(集度)的 Java 开发岗位的⼀⾯⾯经,主要是拷打了 Java (集合+ 并
发)+MySQL (事务+索引)+ 算法(2 道中等难度)这⼏个⽅⾯的知识,整场⾯试下来耗时 90
分钟!

集度⼀⾯ (90 分钟)
开场三连问
⾃我介绍(介绍个⼈基本情况+技术栈能⼒+做过的项⽬)
项⽬介绍(介绍你在项⽬中的⻆⾊+负责 了什么 模块的开发+有技术亮点的地⽅要说出来)
项⽬难点介绍和如何解决的(⾯试必问的问题,根据⾃⼰的项⽬要提前总结好⼀些话术)
谈谈 你对 Java 集合的了解?
Java 集合框架的基本结构:
Collection 接⼝:是所有单列集合的根接⼝,它继承⾃ Iterable 接⼝,表⽰⽀持迭代访问。
Collection 接⼝⼜包含了 List 、Set 、Queue 等⼦接⼝.
List 接⼝:继承⾃ Collection 接⼝,代表有序的可重复的数据列表。常⻅的实现类有
ArrayList 、LinkedList 、Vector 等.
Set 接⼝:同样继承⾃ Collection 接⼝,代表⽆序的不可重复的数据集合。其实现类主要包括
HashSet 、TreeSet 、LinkedHashSet 等.
Queue 接⼝:继承⾃ Collection 接⼝,表⽰⼀种先进先出的数据队列。LinkedList 等类实现了
该接⼝.
Map 接⼝:与 Collection 接⼝并列,代表的是 key-value 类型的映射表,常⻅的实现类有
HashMap 、TreeMap 、LinkedHashMap 等

List 是有序的Collection ,使⽤此接⼝能够精确的控制每个元素的插⼊位置,⽤⼾能根据索引访问
List 中元素。常⽤的实现List 的类有LinkedList ,ArrayList ,Vector ,Stack 。
ArrayList 是容量可变的⾮线程安全列表,其底层使⽤数组实现。当发⽣扩容时,会创建更⼤的
数组,并把原数组复制到 新数 组。ArrayList ⽀持对元素的快速随机访问,但插⼊与删除速度很
慢。
LinkedList 本质是⼀个双向链表,与ArrayList 相⽐,,其插⼊和删除速度更快,但随机访问速度更
慢。
Vector 与 ArrayList 类似,底层也是基于数组实现,特点是线程安全,但效率相对较低,因为其
⽅法⼤多被 synchronized 修饰
Map 是⼀个键值对集合,存储键、值和之间的映射。Key ⽆序,唯⼀;value 不要求有序,允许重
复。Map 没有继承于 Collection 接⼝,从 Map 集合中检索元素时,只要给出键对象,就会返回对
应的值对象。主要实现有TreeMap 、HashMap 、HashTable 、LinkedHashMap 、
ConcurrentHashMap
HashMap :JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主
要为了 解决哈希冲突⽽存在的(“拉链法”解决冲 突),JDK1.8 以后在解决哈希冲突时有了较⼤的
变化 ,当链表⻓度⼤于阈值(默认为 8)时,将链表转化为红⿊树,以减少搜索时间
LinkedHashMap :LinkedHashMap 继承⾃ HashMap ,所以它的底层仍然是基于拉链式散列结
构即由数组和链表或红⿊树组成。另外,LinkedHashMap 在上⾯结构的基础上,增加了⼀条双
向链表,使得上⾯的结构可以保 持键值对的插⼊顺序。同时通过对链表进⾏相应的操作,实现
了访问顺序相关逻辑。
HashTable :数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了 解决哈希冲突⽽
存在的
TreeMap :红⿊树(⾃平衡的排序⼆叉树)
ConcurrentHashMap :Node 数组+链表+红⿊树实现,线程安全的(jdk1.8 以前Segment 锁,
1.8 以后volatile + CAS 或者 synchronized )
Set 不允许存在重复的元素,与List 不同,set 中的元素是⽆序的。常⽤的实现有HashSet ,
LinkedHashSet 和TreeSet 。
HashSet 通过HashMap 实现,HashMap 的Key 即HashSet 存储的元素,所有Key 都是⽤相同的
Value ,⼀个名为PRESENT 的Object 类型常量。使⽤Key 保证元素唯⼀性,但不保证有序性。由
于HashSet 是HashMap 实现的,因此线程不安全。
LinkedHashSet 继承⾃HashSet ,通过LinkedHashMap 实现,使⽤双向链表维护元素插⼊顺序。
TreeSet 通过TreeMap 实现的,添加元素到集合时按照⽐较规则将其插⼊合适的位置,保证插⼊
后的集合仍然有序。
使⽤for 循环对ArrayList 在遍历的时候进⾏删除会有什么 问题?
可能会存在遗漏元素的问题,当使⽤普通的 for 循环遍历 ArrayList 并删除元素时,由于
ArrayList 的底层实现是数组,删除元素会导致后续元素向前移动,使得索引发⽣变化 。
例如:
for(int i=0;i<arrayList.size();i++){
if(arrayList.get(i).equals("del"))
arrayList.remove(i);
}删除某个元素后,arrayList 的⼤⼩发⽣了变化 ,⽽你的索引也在变化 ,所以会 导致你在遍历的时候漏掉某些元素。⽐如当你删除第1个元素后,继续根据索引访问第2个元素时,因为删除的关系后⾯的元素都往前移动了⼀位,所以实际访问的是第3个元素。因此,这种⽅式可以⽤在删除特定的⼀个元素时使⽤,但不适合循环删除多个元素时使⽤。
使⽤Iterator 对List 集合进⾏删除操作时会报什么 异常?
当使⽤ Iterator 遍历 List 集合时,如果直接使⽤ List 的 remove ⽅法来删除元素,在⼤多数
情况下会抛出 ConcurrentModificationException 异常。
这是因为 List 集合在遍历过程中会维护⼀个内部的修改计数( modCount )。当通过 List ⾃⾝
的 remove ⽅法删除元素时, modCount 会被修改,⽽ Iterator 在创建时会记录当 时的
modCount ,在后续遍历过程中会检查 modCount 是否发⽣变化 。如果发⽣变化 ,就会认为集合的结构被⾮法修改,从⽽抛出异常。
以下是⼀个使⽤ ArrayList 和 Iterator 可能会抛出异常的⽰例:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ListIteratorRemoveExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if ("B".equals(element)) {
list.remove(element);
// 这里会抛出ConcurrentModificationException
}
}
}
}
在这个例⼦中,当遍历到元素 B 时,使⽤ list.remove(element) 来删除元素。这会导致 list 的modCount 发⽣改变,⽽ iterator 在遍历过程中检测到 modCount 与它期望 的值不⼀致,就会抛出 ConcurrentModificationException 异常。
正确的做法是使⽤ Iterator 本⾝的 remove ⽅法来删除元素。 Iterator 的 remove ⽅法会在内
部正确地处理 modCount ,从⽽避免抛出异常。
⽰例代 码如下:
import java .util .ArrayList ;
import java .util .Iterator ;
import java .util .List ;
public class ListIteratorRemoveCorrectExample {
public static void main (String [] args ) {
List <String > list = new ArrayList <>();
list .add ("A" );
list .add ("B" );
list .add ("C" );
Iterator <String > iterator = list .iterator ();
while (iterator .hasNext ()) {
String element = iterator .next ();
if ("B" .equals (element )) {
}
System.out.println(list);
}
}
在这个例⼦中,当遍历到元素 B 时,使⽤ iterator.remove() 来删除元素。这样就可以正确地从List 集合中删除元素,并且不 会抛出 ConcurrentModificationException 异常,最后输出的
list 集合中将不包含元素 B 。
Iterator 底层原理实现?
在 Java 中, Iterator 是⼀个接⼝,它定 义了 遍历集合元素的基本⽅法。其主要⽅法包括
hasNext() 和 next() 。 hasNext() ⽤于判断集合中是否还有下⼀个元素, next() ⽤于返回下⼀个元素。
不同的集合类(如 ArrayList 、 LinkedList 、 HashSet 等)会根据⾃⾝的存储结构实现
Iterator 接⼝,以提供适合⾃⾝的数据遍历⽅式。 Iterator 在 Java 集合框架中起到了将遍历操
作与集合的具体存储结构相分离的作⽤,使得遍历代码可以以 ⼀种统⼀的⽅式编写,⽽不依赖于
集合的具体实现细节。
以ArrayList 的terator 实现为例, ArrayList 内部有⼀个内部类 Itr 实现了 Iterator 接⼝。这个
Itr 类包含了⽤于遍历 ArrayList 的相关变量和⽅法。
关键变量:
cursor :这是 Itr 类中的⼀个重要变量,⽤于记录当 前遍历的位置。初始值为 0,表⽰还没
有开始遍历。随着 next() ⽅法的调⽤, cursor 会不断递增,始终指向即将返回的下⼀个元素的位置。
lastRet :⽤于记录上⼀次返回元素的位置。在调⽤ remove() ⽅法时会⽤到这个位置信息来删除正确的元素。初始值为 - 1 ,表⽰还没有返回过元素。
expectedModCount :这个变量记录了在创建 Iterator 时 ArrayList 的 modCount 的值。
modCount 是 ArrayList ⽤于记录结构修改次数的变量。当 ArrayList 的结构发⽣改变(如添
加、删除元素)时, modCount 会增加。
⽅法实现细节:
hasNext() ⽅法:在 ArrayList 的 Itr 类中, hasNext() ⽅法的实现⾮常简单。它判断cursor 是否⼩于 ArrayList 的⼤⼩( size )。如果 cursor ⼩于 size ,则表⽰还有下⼀个
元素,返回 true ;否则返回 false 。例如:
public boolean hasNext () {
return cursor < size ;
}
next() ⽅法:⾸先会检查 modCount 是否与 expectedModCount 相等,如果不相等就抛出ConcurrentModificationException 异常,以防⽌在遍历过程中集合结构被⾮法修改。然后,通
过 elementData[cursor] 获取当前位置( cursor 所指位置)的元素,并将 lastRet 赋值为
cursor ,再将 cursor 加 1,最后返回获取到的元素。例如:
public E next () {
checkForComodification ();
int i = cursor ;
if (i >= size )
throw new NoSuchElementException ();
Object [] elementData = ArrayList .this .elementData ;
if (i >= elementData .length )
throw new ConcurrentModificationException ();
cursor = i + 1;
return (E) elementData [lastRet = i];
}
remove() ⽅法:这个⽅法⽤于在遍历过程中删除元素。它⾸先会检查 lastRet 是否⼩于 0,
如果是则抛出 IllegalStateException ,因为在还没有调⽤ next() ⽅法获取元素之前是不能删除元素的。然后,调⽤ ArrayList 的 remove ⽅法删除 lastRet 位置的元素。接着,更新
lastRet 为 - 1 ,并更新 expectedModCount 为当前的 modCount ,以保 持 Iterator 与
ArrayList 的同步。例如:
public void remove () {
if (lastRet < 0)
throw new IllegalStateException ();
checkForComodification ();
try {
ArrayList .this .remove (lastRet );
cursor = lastRet ;
lastRet = -1;
expectedModCount = modCount ;
} catch (IndexOutOfBoundsException ex ) {
throw new ConcurrentModificationException ();
}
}总之, Iterator 接⼝的实现根据集合的存储结构(如数组、链表等)⽽有所不同,但它们的核⼼
⽬的都是提供⼀种安全、⾼效的遍历集合元素的⽅式,过隐藏集合内部的复杂存储结构细节,
Iterator 使得开 发⼈员可 以使 ⽤统⼀的⽅式来遍历不同类型的集合,同时也保证了在遍历过程中
对集合结构的修改能够被正确地处理,避免出现数据不⼀致等问题。
谈谈 你对ThreadLocal 的理解
ThreadLocal 是Java 中⽤于解决线程安全问题的⼀种机制,它允许创建线程局部变量,即每个线
程都有⾃⼰独⽴的变量副本,从⽽避免了线程间的资源共享和同 步问题。

从内存结构图,我们可以看到:
Thread 类中,有个ThreadLocal.ThreadLocalMap 的成员变量。
ThreadLocalMap 内部维护了Entry 数组,每个Entry 代表⼀个完整的对象,key 是ThreadLocal 本
⾝,value 是ThreadLocal 的泛型对象值。
ThreadLocal 的作⽤
线程隔离: ThreadLocal 为每个线程提供了独⽴的变量副本,这意味着线程之间不会相互影
响,可以安全地在 多线程环境中使⽤这些变量⽽不必担⼼数据竞争或同步问题。
降低耦合度:在同⼀个线程内的多个函数或组件之间,使⽤ ThreadLocal 可以减少参数的传
递,降低代 码之间的耦合度,使代 码更加清晰和模块化。
性能优势:由于 ThreadLocal 避免了线程间的同步开销,所以在⼤量线程并发执⾏时,相⽐传
统的锁机制,它可以提供更好的性能。
ThreadLocal 的原理
ThreadLocal 的实现依赖于 Thread 类中的⼀个 ThreadLocalMap 字段,这是⼀个存储
ThreadLocal 变量本⾝和对应值的映射。每个线程都有⾃⼰的 ThreadLocalMap 实例,⽤于存储该线程所持有的所有 ThreadLocal 变量的值。
当你创建⼀个 ThreadLocal 变量时,它实 际上就是⼀个 ThreadLocal 对象的实例。每个
ThreadLocal 对象都可以存储任意类型的值,这个值对每个线程来说是独⽴的。
当调⽤ ThreadLocal 的 get() ⽅法时, ThreadLocal 会检查当前线程的 ThreadLocalMap 中是否有与之 关联的值。
如果有 ,返回该值;
如果没有,会调⽤ initialValue() ⽅法(如果重写了的话)来初始化该值,然后将其放⼊ThreadLocalMap 中并返回。
当调⽤ set() ⽅法时, ThreadLocal 会将给定的值与当前线程关联起来,即在当前线程的ThreadLocalMap 中存储⼀个键值对,键是 ThreadLocal 对象⾃⾝,值是传⼊的值。
当调⽤ remove() ⽅法时,会从当前线程的 ThreadLocalMap 中移除与该 ThreadLocal 对象关联的条⽬。
可能存在的问题
当⼀个线程结束时,其 ThreadLocalMap 也会随之销毁,但是 ThreadLocal 对象本⾝不会⽴即被垃圾回收,直到没有其他引⽤指向它为⽌。
因此,在使⽤ ThreadLocal 时需要注意,如果不显式调⽤ remove() ⽅法,或者线程结束时未正确清理 ThreadLocal 变量,可能会导致内存泄漏,因为 ThreadLocalMap 会持续持有 ThreadLocal 变量的引⽤,即使这些变量不再被其他地⽅引⽤。
因此,实际应⽤中需要在使⽤完 ThreadLocal 变量后调⽤ remove() ⽅法释放资源。ThreadLocalMap 的哈希冲突如何解决?
在 ThreadLocal 中采取的是 开放地址 法 的⽅法来解决哈希冲突。当遇到哈希冲突时,会再次进
⾏ 探测,探测的意思其实就是去寻找下⼀个空位。
关于这个探测有⾮常多的实现⽅式,常⻅的有:
线性探测:如果当前位置被占⽤,则依次往后查找,直到找到⼀个空槽位。
⼆次探测:如果当前位置被占⽤,双向地去找。di = i + 1 ,i - 1 , i + 3, i - 3双重哈希:使⽤两个 哈希函数,根据第⼀个哈希函数的结果找到⼀个位置,如果该位置已被占
⽤,则使⽤第⼆个哈希函数计算下⼀个位置,以此类推,直到找到⼀个空槽位。
在 ThreadLocal ⾥⾯采⽤的时是 最简单的 线性探测。
private ThreadLocalMap (ThreadLocalMap parentMap ) {
Entry [] parentTable = parentMap .table ;
int len = parentTable .length ;
setThreshold (len );
table = new Entry [len ];
for (int j = 0; j < len ; j++ ) {
Entry e = parentTable [j];
if (e != null ) {
@SuppressWarnings ("unchecked" )
ThreadLocal <Object > key = (ThreadLocal <Object >) e.get ();
if (key != null ) {
Object value = key .childValue (e.value );
Entry c = new Entry (key , value );
int h = key .threadLocalHashCode & (len - 1);
// 这里采用的是线性探测,一直找到空的位置
while (table [h] != null )
h = nextIndex (h, len );
table [h] = c;
size ++ ;
}
}
}
}
// 步长为1, 一直往右边去找
return ((i + 1 < len) ? i + 1 : 0);
}ThreadLocalMap 的key 是如何计算出来的?
在 ThreadLocal 类中,每个 ThreadLocal 对象都有⼀个 threadLocalHashCode 字段。这个字段⽤于计算该 ThreadLocal 对象在 ThreadLocalMap 中的存储位置(也就是 key 相关的哈希值)。
哈希码的初始值是⼀个随机的魔数( 0x61c88647 ),这个魔数的选择是有特殊意义的。它可以使
得哈希码在不同的 ThreadLocal 对象之间能够⽐较均匀地分布。
创建⼀个 ThreadLocal 对象时,它的 threadLocalHashCode 会通过以下⽅式计算:
private final int threadLocalHashCode = nextHashCode();nextHashCode ⽅法的实现如下:
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}这⾥使⽤了 AtomicInteger 来保证在多线程环境下 threadLocalHashCode 的正确⽣成。每次 调⽤
nextHashCode ⽅法,都会在当前值的基础上加上 HASH_INCREMENT ( 0x61c88647 ),这样不同的ThreadLocal 对象就会有不同的哈希码。
知道了对象的哈希码之后, 就可以计算对象的存储位置了。
在 ThreadLocalMap 的内部⽅法(如 set 和 get ⽅法)中,会使 ⽤ ThreadLocal 对象的
threadLocalHashCode 来计算它在 ThreadLocalMap 的存储位置。
计算⽅式是通过与 table.length - 1 进⾏按位与操作( & )。例如,在 set ⽅法中:
int i = key.threadLocalHashCode & (table.length - 1);
这种计算⽅式类似于取模运算,但在性能上⽐取模运算( % )更⾼效。当 table.length 是 2 的
幂次⽅时, key.threadLocalHashCode & (table.length - 1) 等价于 key.threadLocalHashCode %table.length 。
这样就得到了 ThreadLocal 对象在 ThreadLocalMap 中的存储位置索引,也就是 key 相关的哈希
计算结果。这个索引⽤于在 ThreadLocalMap 的内部数组( table )中定位存储 ThreadLocal 对
象和对应的值的位置。
数据库的索引的结构是怎样的?
从数据结构的⻆度来看,MySQL 常⻅索引有 B+Tree 索引、HASH 索引、Full-Text 索引。
每⼀种存储引擎⽀持的索引类型不⼀定相同,我在表中总结了 MySQL 常⻅的存储引擎 InnoDB 、
MyISAM 和 Memory 分别 ⽀持的索引类型。

InnoDB 是在 MySQL 5.5 之后成为默认的 MySQL 存储引擎,B+Tree 索引类型也是 MySQL 存储引
擎采⽤最多的索引类型,InnoDB 将数据存储在B+ 树的结构中,其中主 键索引的B+ 树就是所谓的聚簇索引。这意味着表中的数据⾏在物理上是按照主键的顺序排列的,聚簇索引的叶节点包含了实
际的数据⾏。

在创建表时,InnoDB 存储引擎会根据不同的场景选择不同的列作为索引:
如果有 主键,默认会使 ⽤主键作为聚簇索引的索引键(key );
如果没有主键,就选择第⼀个不 包含 NULL 值的唯⼀列作为聚簇索引的索引键(key );
在上⾯两个 都没有的情况下,InnoDB 将⾃动⽣成⼀个隐式⾃增 id 列作为聚簇索引的索引键
(key );其它索引都属于辅助索引(Secondary Index ),也被称为⼆级索引或⾮聚簇索引。创建的主键索引
和⼆级索引默认使⽤的是 B+Tree 索引。
数据库的索引失效的⼏个例⼦?
6 种会发⽣索引失效的情况:
当我们使 ⽤左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx% 这两种⽅式都会造成索
引失效;
当我们在查询条件中对索引列使⽤函数,就会导致索引失效。
当我们在查询条件中对索引列进⾏表达式计算,也是⽆法⾛索引的。
MySQL 在遇到字符串和数字⽐较的时候,会⾃动把字符串转为数字,然后再进⾏⽐较。如果字
符串是索引列,⽽条件语句中的输⼊参数是数字的话,那么索引列会发⽣隐式类型转换,由于
隐式类型转换是通过 CAST 函数实现的,等同于对索引列使⽤了函数,所以就会导致索引失
效。
联合索引要能正确使⽤需要遵循最左匹配原则,也就是按照最左优先的⽅式进⾏索引的匹配,
否则就会导致索引失效。
在 WHERE ⼦句中,如果在 OR 前的条件列是索引列,⽽在 OR 后的条件列不是索引列,那么索
引会失效。
谈谈 你对数据库中的事务的理解
mysql 事务具备 ACID 特性:
原⼦性(Atomicity ):⼀个事 务中的所有操作,要么全部完成,要么全部不完成,不会结束在
中间某个环节,⽽且事 务在执⾏过程中发⽣错误,会被回滚到事务开始前的状态,就像这个事
务从来没有执⾏过⼀样,就好⽐买⼀件商品,购买成功时,则给商家付了 钱,商品到⼿;购买
失败时,则商品在商家⼿中,消费者的钱也没花出去。
⼀致性(Consistency ):是指事务操作前和操作后,数据满⾜完整性约束,数据库保持⼀致性
状态。⽐如,⽤⼾ A 和⽤⼾ B 在银⾏分别 有 800 元和 600 元,总共 1400 元,⽤⼾ A 给⽤⼾ B
转账 200 元,分为两个 步骤,从 A 的账⼾扣除 200 元和对 B 的账⼾增加 200 元。⼀致性就是
要求上述步骤操作后,最后的结果是⽤⼾ A 还有 600 元,⽤⼾ B 有 800 元,总共 1400 元,⽽
不会出现⽤⼾ A 扣除了 200 元,但⽤⼾ B 未增加的情况(该情况,⽤⼾ A 和 B 均为 600 元,
总共 1200 元)。
隔离性(Isolation ):数据库允许多个并发事务同时对其数据进⾏读写和修改的能⼒,隔离性可
以防⽌多个事 务并发执⾏时由于交 叉执⾏⽽导致数据的不⼀致,因为多个事 务同时使⽤相同的
数据时,不会相互⼲扰,每个事 务都有⼀个完整的数据空间,对其他并发事务是隔离的。也就
是说,消费者购买商品这个事 务,是不影响其他消费者购买的。
持久性(Durability ):事务处理结束后,对数据的修改就是永久的,即便系统故障也不 会丢
失。
MySQL InnoDB 引擎通过什么 技术来 保证事务的这四个特性的呢?
持久性是通过 redo log (重做⽇志)来保证的;
原⼦性是通过 undo log (回滚⽇志) 来保证的;
隔离性是通过 MVCC (多版本并发控制) 或锁机制来保证的;
⼀致性则是通过持久性+原⼦性+隔离性来保证;
介绍MVCC 的原理
MVCC 允许多个事 务同时读取同⼀⾏数据,⽽不会彼此阻塞,每个事 务看到的数据版本是该事务开
始时的数据版本。这意味着,如果其他事 务在此期间修改了数据,正在运⾏的事务仍然看到的是
它开始时的数据状态,从⽽实现了⾮阻塞读操作。
对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的
区别在于创建 Read View 的时机不同,⼤家可以把 Read View 理解成⼀个数据快照,就像相机拍
照那样,定格某⼀时刻的⻛景。
「读提交」隔离级别是在「每个select 语句执⾏前」都会重新⽣成⼀个 Read View ;
「可重复读」隔离级别是执⾏第⼀条select 时,⽣成⼀个 Read View ,然后整个事 务期间都在⽤
这个 Read View 。
Read View 有四个重要的字段:

m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是⼀个
列表,“活跃事务”指的就是,启动了但还没提交的事务。
min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事 务 id 最⼩的事务,也就是 m_ids 的最⼩值。
max_trx_id :这个并不是 m_ids 的最⼤值,⽽是创建 Read View 时当前数据库中应该给下⼀个事务的 id 值,也就是全局事务中最⼤的事务 id 值 + 1 ;creator_trx_id :指的是创建该 Read View 的事务的事务 id 。
对于使⽤ InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含下⾯两个 隐藏列:

trx_id ,当⼀个事 务对某条 聚簇索引记录进⾏改动时,就会把该事务的事务 id 记录在 trx_id 隐
藏列⾥;
roll_pointer ,每次 对某条 聚簇索引记录进⾏改动时,都会把旧版本的记录写⼊到 undo ⽇志
中,然后这个隐藏列是个指针,指向每⼀个旧版本记录,于是就可以通过它找到修改前的记
录。
在创建 Read View 后,我们可以将记录中的 trx_id 划分 这三种情况:

⼀个事 务去访问记录的时候,除了⾃⼰的更新记录总是可⻅之外,还有这⼏种情况:
如果记录的 trx_id 值⼩于 Read View 中的 min_trx_id 值,表⽰这个版本的记录是在创建 Read View 前已经提交的事务⽣成的,所以该版本的记录对当前事务可⻅。
如果记录的 trx_id 值⼤于等于 Read View 中的 max_trx_id 值,表⽰这个版本的记录是在创建Read View 后才启动的事务⽣成的,所以该版本的记录对当前事务不可⻅。
如果记录的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在
m_ids 列表中:
如果记录的 trx_id 在 m_ids 列表中,表⽰⽣成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可⻅。
如果记录的 trx_id 不在 m_ids 列表中,表⽰⽣成该版本记录的活跃事务已经被提交,所以该
版本的记录对当前事务可⻅。
这种通过「版本链」来控制并发事务访问同⼀个记录时的⾏为就叫 MVCC (多版本并发控制)。
看到你项⽬⽤了JWT 说⼀下原理。
JWT 令牌由三个 部分组成:头部(Header )、载荷(Payload )和签名(Signature )。其中,头部和载荷均为JSON 格式,使⽤Base64 编码进⾏序列化,⽽签名部分是对头部、载荷和密钥进⾏签名后 的结果。

在传统的基于会话和Cookie 的⾝份验证⽅式中,会话信息通常存储在服务器的内存或数据库中。
但在集群部署中,不同服务器之间没有共享的会话信息,这会导致⽤⼾在不同服务器之间切换时
需要重新登录,或者需要引⼊额外的共享机制(如Redis ),增加了复杂性和性能开销。

⽽JWT 令牌通过在令牌中包含所有必要的⾝份验证和会话信息,使得服务器⽆需存储会话信息,从
⽽解决了集群部署中的⾝份验证和会话管理问题。当⽤⼾进⾏登录认证 后,服务器将⽣成⼀个JWT
令牌并返回给客⼾端。客⼾端在后续的请求中携带该令牌,服务器可以通过对令牌进⾏验证和解
析来 获取⽤⼾⾝份和权限信息,⽽⽆需访问共享的会话存储。
由于JWT 令牌是⾃包含的,服务器可以独⽴地对令牌进⾏验证,⽽不需要依赖其他服务器或共享存
储。这使得集群中的每个服务器都可以独⽴处理请求,提⾼了系统的可伸缩性和容错性。
JWT 的缺点是⼀旦派发出去,在失效之前都是有效的,没办法即使撤销JWT 。要解 决这个问题的
话,得在业务层增加判 断逻辑,⽐如增加⿊名单机制。使⽤内存数据库⽐如 Redis 维护⼀个⿊名
单,如果想让某个 JWT 失效的话就直接将这个 JWT 加⼊到 ⿊名单 即可。然后,每次 使⽤ JWT 进
⾏请求的话都会先判断这个 JWT 是否存在于⿊名单中。
算法
旋转链表(中等)
题⽬描述:给定⼀个链表的头节点 head ,将链表每个节点向右 移动 k 个位置。例如,对于链
表 1 -> 2 -> 3 -> 4 -> 5 , k = 2 时,旋转后的链表为 4 -> 5 -> 1 -> 2 -> 3 。解题思路
1、计算链表⻓度并 处理 k 值
⾸先需要遍历⼀遍链表来计算链表的⻓度 len ,通过⼀个指针从链表头开始,不断向后 移动,
直到到 达链表尾(指针的下⼀个节点为 null ),同时记录经过的节点个数,即链表⻓度。
然后对 k 取模( k %= len ),因为如果 k ⼤于链表⻓度,相当于多次重复旋转,取模后得到的就是实际有效的旋转次数,避免了不 必要的重复操作。
2、找到新的链表头和尾节点
再次遍历链表,当遍历到第 len - k 个节点时(从 1 开始计数),这个节点就是旋转后链表的
尾节点,它的下⼀个节点就是新的链表头节点。
记录下新的链表头节点和尾节点,⽅便后续进⾏链表的重新连接操作。
3、重新连接链表
将原链表的尾节点(当前的最后⼀个节点)连接到原链表的头节点,形成⼀个环。
然后将新的尾节点的下⼀个节点置为 null ,断开环,此时链表就完成了旋转,得到了最终旋
转后的链表。
代码实现如下:
class ListNode {
int val ;
ListNode next ;
ListNode (int val ) {
this .val = val ;
}
}
if (head == null) {
return null;
}
// 步骤1:计算链表长度并处理k值
int len = 1;
ListNode tail = head;
while (tail.next!= null) {
len++;
tail = tail.next;
}
k %= len;
if (k == 0) {
return head;
}
// 步骤2:找到新的链表头和尾节点
ListNode cur = head;
for (int i = 0; i < len - k - 1; i++) {
cur = cur.next;
}
ListNode newHead = cur.next;
ListNode newTail = cur;
// 步骤3:重新连接链表
tail.next = head;
newTail.next = null;
return newHead;
}
}复杂度分析:
时间复杂度:O(n) ,最坏情况下,我们需要遍历该链表两次。
空间复杂度:O(1) ,我们只需要常数的空间存储若⼲变量。最⻓不重复字符串⼦串(中等)
** 题⽬描述:** 给定⼀个字符串 s ,请你找出其中不 重复字符的最⻓⼦串的⻓度。
例如,对于字符串 "abcabcbb" ,其不重复字符的最⻓⼦串是 "abc" ,⻓度为 3 ;对于字符串
"bbbbb" ,最⻓不重复⼦串是 "b" ,⻓度为 1 ;对于字符串 "pwwkew" ,最⻓不重复⼦串是
"wke" ,⻓度为 3 。
解题思路
可以使 ⽤滑动窗⼝的思想来解决这个问题,具体步骤如下:
1、定义数据结构和变量
使⽤⼀个哈希表(在 Java 中可以使 ⽤ HashMap )来记录字符出现的最新位置。键为字符,值
为该字符在字符串中 的索引位置。
定义两个 指针,⼀个左指针 left 和⼀个右指针 right ,分别 表⽰滑动窗⼝的左右边界,初
始时都指向字符串的开头(索引为 0 )。
定义⼀个变量 maxLength ⽤于记录最⻓不重复⼦串的⻓度,初始值设为 0 。
2、滑动窗⼝移动过程
通过移动右指针 right 不断扩⼤窗⼝,每次 将右指针指向的字符加⼊窗⼝(即加⼊哈希表,
并记录其位置)。
在加⼊字符时,检查该字符是否已经在哈希表中存在,且其上次出现的位置在当前窗⼝内(即
上次出现位置⼤于等于左指针 left 所指位置)。如果是这样,说明出现了重复字符,需要收
缩窗⼝,将左指针 left 移动到 该重复字符上次出现位置的下⼀个位置,以保 证窗⼝内的字符
都是不重复的。
在每次 移动右指针 right 后(⽆论是否进⾏了窗⼝收缩操作),计算当前窗⼝的⻓度(即
right - left + 1 ),并更新 maxLength 的值,使其始终记录当 前找到的最⻓不重复⼦串的⻓度。
3、遍历结束后返回结果
当右指针 right 遍历完整个字符串后, maxLength 中记录的值就是整个字符串中不 重复字符
的最⻓⼦串的⻓度,直接返回该值即可。
代码实现如下:
import java .util .HashMap ;
import java .util .Map ;
public class LongestSubstringWithoutRepeatingCharacters {
public static int lengthOfLongestSubstring (String s) {
if (s == null || s.length () == 0) {
return 0;
}
// 用于记录字符出现的最新位置,键为字符,值为索引位置
int left = 0;
int right = 0;
// 移动右指针来扩大窗口
while (right < s.length()) {
char currentChar = s.charAt(right);
// 检查当前字符是否已在窗口内出现过(且上次出现位置在当前窗口内)
if (charMap.containsKey(currentChar) && charMap.get(currentChar) >= left) {
// 如果出现重复字符,收缩窗口,更新左指针位置
left = charMap.get(currentChar) + 1;
}
// 将当前字符加入哈希表,记录其最新位置
charMap.put(currentChar, right);
// 更新最长不重复子串的长度
maxLength = Math.max(maxLength, right - left + 1);
// 移动右指针,继续扩大窗口
right++;
}
return maxLength;
}
public static void main(String[] args) {
String s = "abcabcbb";
int result = lengthOfLongestSubstring(s);
System.out.println("最长不重复子串的长度为: " + result);
}
}复杂度分析:
时间复杂度:O(n) ,代码中最主要的操作是通过 while 循环遍历字符串,右指针 right 从字符串开头逐步移动到 字符串末尾,最多移动 n 次( n 为字符串⻓度),所以这部分的时间复
杂度主要取决于循环的次数,也就是字符串的⻓度 n ,因此时间复杂度为 O(n) 。
空间复杂度:O(1) ,我们只需要常数的空间存储若⼲变量。