上周,极越汽车(原名集度)"闪崩"的事件大家肯定都知道了。简单来说,就是公司经营困难->资金链断了->大规模裁员,一系列连锁反应。甚至还有极越汽车员工在直播间卖车的时候,卖着卖着就被告知明天不用来了,因为公司要没了。。。。
有个 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)) {
iterator.remove();
}
}
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++;
}
}
}
}
private static int nextIndex(int i, int len) {
// 步长为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):是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。 隔离性(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令牌并返回给客户端。客户端在后续的请求中携带该令牌,服务器可以通过对令牌进行验证和解析来获取用户身份和权限信息,而无需访问共享的会话存储。
算法
旋转链表(中等)、最长不重复字符串子串(中等)
好文推荐
刚入职,看不懂组内代码...
你好,我是阿秀,普通学校毕业,校招时拿到字节跳动SP、百度、华为、农业银行等6个互联网中大厂offer,这是我在校期间的编程学习之路,详细记录了我是如何自学技术以应对第二年的校招的过程。
毕业后我先于抖音部门担任全栈开发工程师,目前在上海某外企带领团队继续从事全栈开发,负责的项目已经顺利盈利300w+。在研三那年就组建了一个阿秀的学习圈,持续分享校招/社招跳槽找工作的经验,目前已经累计服务22、23、24、25届同学,共计超过 4200 +人,欢迎点此🔗加入我们,一群人才能走的更远、更稳、更顺。