Redis
得物 Java ⾯试
去年给我印象⽐较深刻的是得物(得物是⼀家做年轻⼈潮牌的电商公司,base 上海)的开奖。
因为开的确实很⾼,校招毕业年薪 40w 起步,⽐当时挺多⼀线⼤⼚的⽩菜档都开的⾼,最⾼档
offer 都到 50w 级别了。24 届得物校招后端开发岗位薪资:

不过,得物⼯作强度会⽐较⾼,所以才开了⽐较有竞争⼒的薪资。
今年很多⼤⼚校招薪资相⽐于去年都涨了不 少,⽐如京东 、美团、字节,不少⼈拿到都是年薪 40-50w 的年薪。
那这次来看看 25 届得物的校招薪资如何?是否相⽐于去年有⼀些变化 ?
我整理了 25 届得物「后端开发岗位」的校招薪资情况如下(根据读者反馈+offershow 的数据):

33k x 16 + 1.2k x 12 (房补)+ 5w 签字费,同学 bg 硕⼠ 985 ,base 上海
32k x 16 + 1.2k x 12 (房补)+ 6w 签字费,同学 bg 硕⼠ 985 ,base 上海
31k x 16 + 1.2k x 12 (房补),同学 bg 硕⼠ 985 ,base 上海
30k x 16 + 1.2k x 12 (房补),同学 bg 硕⼠ 985 ,base 上海 sp offfer :
29k x 16 + 1.2k x 12 (房补),同学 bg 硕⼠ 985 ,base 上海
28k x 16 + 1.2k x 12 (房补),同学 bg 硕⼠ 211 ,base 上海
27k x 16 + 1.2k x 12 (房补),同学 bg 硕⼠ 211 ,base 上海 普通 offer :
25k x 16 + 1.2k x 12 (房补),同学 bg 硕⼠ 211 ,base 上海
整体上是相⽐去年多了,普遍⽉薪多了 1-2k ,得物今年依然很猛啊。
最⾼的 ssp offer 都有 60w 年包了(算上房补+签字费),即使是⽩菜 offer 年薪都有 40w+ 。
很多同学就好奇 得物的⾯试难度如何?
其实都都 ⼤⼚差不多,围绕⼋股+项⽬+算法这三个 ⽅⾯来考察。
这次分享之 前同学得物 Java 后端岗位⼀⾯的⾯试,⾯试感受不错,有的问题还跟⾯试官讨论 起
来。

ArrayList 和 LinkedList 的区别是什么 ?(答上来了)
数据结构⽅⾯:
ArrayList :内部使⽤动态数组存储数据。因此,它⽀持随机访问,通过索引访问元素⾮常快,
时间复杂度为O(1) 。LinkedList :内部使⽤双向链表存储数据。这使得在列表的开头或结尾插⼊、删除元素⾮常快,
时间复杂度为O(1) 。性能⽅⾯:
ArrayList :添 加元素时如果需要扩容(即当前数组已满),则需要复制原数组到新的更⼤的数
组,这样的操作时间复杂度为O(n) 。⽽对于⾮尾部的插⼊和删除操作,需要移动后⾯的所有元
素,时间复杂度也是O(n) 。LinkedList :对于⾮⾸尾的插⼊和删除操作,需要从头部或尾部遍 历到相应的位置,时间复杂度
为O(n) 。⽽访问元素(get 和set 操作)也需要从头部或尾部遍 历到相应的位置,时间复杂度为
O(n) 。ArrayList 的底层原理是什么 ?(答上来了)
ArrayList 是Java 中的动态数组,其底层原理是基于数组实现的。
具体来说,ArrayList 内部使⽤⼀个Object 类型的数组来存储元素。当我们向ArrayList 中添加元素
时,它会⾃动调整数 组的⼤⼩以适应新的元素。当数组的容量不⾜以容纳新元素时,ArrayList 会创
建⼀个更⼤的数组,并将原数组中的元素复制到 新数 组中。
这种动态调整数 组⼤⼩的策略可以确保ArrayList 具有可变⻓度的特性,可以根据需要动态添加或删
除元素,⽽⽆需⼿动管理数组的⼤⼩。
需要注意的是,由于ArrayList 底层使⽤数组实现,所以在插⼊或删除元素时,需要将后续的元素进
⾏移动,这可能会影响性能,特别是当ArrayList 中的元素数量很⼤时。因此,在需要频繁进⾏插⼊
和删除操作的场景下,可能需要考虑使⽤LinkedList 等其他数据结构来替 代ArrayList 。
ArrayList 是线程安全的吗?( 答上来了)
ArrayList 不是线程安全的,在多线程环境下,如果多个线程同时对同⼀个ArrayList 实例进⾏操作
(如添加、删除、修改等),会导致数据不⼀致的问题。
为什么不 是线程安全的,具体来说是哪⾥不安全?(源码层⾯,没答上来)
在⾼并发添加数据下,ArrayList 会暴露三个 问题;部分值为null (我们并没有add null 进去)
索引越界异常
size 与我们add 的数量不符
为了 知道这三种情况是怎么发⽣的,ArrayList ,add 增加元素的代码如下:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
ensureCapacityInternal() 这个⽅法的详细代码我们可以暂时 不看,它的作⽤就是判断如果将当前的新元素加到列 表后⾯,列表的elementData 数组的⼤⼩是否满⾜,如果size + 1 的这个需求⻓度⼤
于了 elementData 这个数组的⻓度,那么就要对这个数组进⾏扩容。
⼤体可以分为三 步:
判断数 组需不需要扩容,如果需要的话,调⽤grow ⽅法进⾏扩容;
将数组的size 位置设置值(因为数组的下标是从0开始的);
将当前集合的⼤⼩加1
下⾯我们来分析三种情况都是如何产⽣的:
部分值为null :当线程1⾛到了扩容那⾥发现当前size 是9,⽽数组容量是10 ,所以不⽤扩容,这
时候cpu 让出执⾏权,线程2也进来了,发现size 是9,⽽数组容量是10 ,所以不⽤扩容,这时候
线程1继续执⾏,将数组下标索引为9的位置set 值了,还没有来 得及执⾏size++ ,这时候线程2
也来执⾏了,⼜把数组下标索引为9的位置set 了⼀遍,这时候两个 先后进⾏size++ ,导致下标
索引10 的地⽅就为null 了。
索引越界异常:线程1⾛到扩容那⾥发现当前size 是9,数组容量是10 不⽤扩容,cpu 让出执⾏
权,线程2也发现不⽤扩容,这时候数组的容量就是10 ,⽽线程1 set 完之后size++ ,这时候线程2再进来size 就是10 ,数组的⼤⼩只有10 ,⽽你要设置下标索引为10 的就会越界(数组的下标索
引从0开始);
size 与我们add 的数量不符:这个基本上每次 都会发⽣,这个理解起来也很简单,因为size++ 本
⾝就不是原⼦操作,可以分为三 步:获取size 的值,将size 的值加1,将新的size 值覆盖掉原来
的,线程1和线程2拿到⼀样的size 值加完了同时覆盖,就会导致⼀次没有加上,所以肯定不会与
我们add 的数量保持⼀致的;
ArrayList 和 LinkedList 的应⽤场景,什么 时候该⽤哪个?(没答上来)
ArrayList 适⽤于需要频繁访问集 合元素的场景。它基于数组实现,可以通过索引快 速访问元
素,因此在按索引查找、遍历和随机访问元素的操作上具有较⾼的性能。当需要频繁访问和遍
历集合元素,并且集合⼤⼩不经常改变时,推荐使⽤ArrayList
LinkedList 适⽤于频繁进⾏插⼊和删除操作的场景。它基于链表实现,插⼊和删除元素的操作只
需要调整节点的指针,因此在插⼊和删除操作上具有较⾼的性能。当需要频繁进⾏插⼊和删除
操作,或者集合⼤⼩经常改变时,可以考虑使⽤LinkedList 。
Redis
缓存雪崩、缓存击穿、缓存穿透是什么 ?(答上来了)
缓存雪崩:当⼤量缓存数据在同⼀时间过期(失效)或者 Redis 故障宕机时,如果此时有⼤量
的⽤⼾请求,都⽆法在 Redis 中处理,于是全部请求都直接访问数据库,从⽽导致数据库的压
⼒骤增,严重的会造成数据库宕机,从⽽形成⼀系列连锁反应,造成整个系统崩溃,这就是缓
存雪崩的问题。

缓存击穿:如果缓存中的某个热点 数据过期了,此时⼤量的请求访问了该热点 数据,就⽆法从
缓存中读取,直接访问数据库,数据库很容易就被⾼并发的请求冲垮,这就是缓存击穿的问
题。

缓存穿透:当⽤⼾访问的数据,既不在缓存中,也不 在数据库中,导致请求在访问缓存时,发
现缓存缺失,再去访问数据库时,发现数据库中也 没有要访问的数据,没办法构建缓存数据,
来服 务后续的请求。那么当有⼤量这样的请求到来时,数据库的压⼒骤增,这就是缓存穿透的
问题。

这些缓存问题,有什么 解决⽅案吗?(答上来了)
缓存雪崩解决⽅案:
均匀设置过期时间:如果要给缓存数据设置过期时间,应该避免将⼤量的数据设置成同⼀个过
期时间。我们可以在对缓存数据设置过期时间时,给这些数据的过期时间加上⼀个随机数,这
样就保证数据不会在同⼀时间过期。
互斥锁:当业务线程在处理⽤⼾请求时,如果发现访问的数据不在 Redis ⾥,就加个互 斥锁,
保证同⼀时间内只有⼀个请求来构 建缓存(从数据库读取数据,再将数据更新到 Redis ⾥),当
缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么
就返回空值或者默认值。实现互斥锁的时候,最好设置超时时 间,不然第⼀个请求拿到了锁,
然后这个请求发⽣了某种意外⽽⼀直阻塞,⼀直不释放锁,这时其他请求也⼀直拿不到锁,整
个系统就会出现⽆响应的现象。
后台 更新缓存:业务线程不再负责 更新缓存,缓存也不 设置有效期,⽽是让缓存“永久有效”,
并将更新缓存的⼯作交由后台 线程定时更新。
缓存击穿解决⽅案:
互斥锁⽅案,保证同⼀时间只有⼀个业 务线程更新缓存,未能获取互斥锁的请求,要么等待锁
释放后重新读取缓存,要么就返回空值或者默认值。
不给热点 数据设置过期时间,由后台 异步更新缓存,或者在热点 数据准备要过期前,提前通知
后台 线程更新缓存以及重新设置过期时间;
缓存穿透解决⽅案:
⾮法请求的限制:当有⼤量恶意请求访问不存在的数据的时候,也会发⽣缓存穿透,因此在
API ⼊⼝处我们要判断求请求参数是否合 理,请求参数是否含 有⾮法值、请求字段是否存在,
如果判断出是恶意请求就直接返回错误,避免进⼀步访问缓存和数据库。
缓存空值或者默认值:当我们线上业 务发现缓存穿透的现象时,可以针对查询的数据,在缓存
中设置⼀个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应
⽤,⽽不会继续查询数据库。
布隆过滤器:我们可以在写⼊数据库数据时,使⽤布隆过滤器做个标记,然后在⽤⼾请求到来
时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数 据是否存在,如果不存
在,就不⽤通过查询数据库来判断数 据是否存在。即使发⽣了缓存穿透,⼤量请求只会查询
Redis 和布隆过滤器, ⽽不会查询数据库,保证了数据库能正常运⾏,Redis ⾃⾝也是⽀持布隆
过滤器的。
⽹络协议
HTTP1.1 怎么对请求做拆包,具体来说怎么拆的?( ⼋股没背过这 ,不知
道)
在HTTP/1.1 中,请求的拆包是通过"Content-Length" 头字段来进⾏的。该字段指⽰了请求正⽂的⻓
度,服务器可以根据该⻓度来正确接收和解析请求。

具体来说,当客⼾端发送⼀个HTTP 请求时,会在请求头中添加"Content-Length" 字段,该字段的
值表⽰请求正⽂的字节数。
服务器在接收到请求后,会根据"Content-Length" 字段的值来确定请求的⻓度,并从请求中读取相
应数量的字节,直到读取完整个请求内容。
这种基于"Content-Length" 字段的拆包机制可以确保服务器正确接收到完整的请求,避免了请求的
丢失或截 断问题。
讲讲 TCP 三次握⼿ (答上来了)

⼀开始,客⼾端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端⼝,处于 LISTEN 状
态
客⼾端会随机初始化序号(client_isn ),将此序号置于 TCP ⾸部的「序号」字段中,同时把
SYN 标志位置为 1,表⽰ SYN 报⽂。接着把第⼀个 SYN 报⽂发送给服务端,表⽰向服务端发起
连接,该报⽂不包含应⽤层数据,之后客⼾端处于 SYN-SENT 状态。
服务端收到客⼾端的 SYN 报⽂后,⾸先服务端也随机初始化⾃⼰的序号(server_isn ),将此序
号填⼊ TCP ⾸部的「序号」字段中,其次把 TCP ⾸部的「确认应答号」字段填⼊ client_isn + 1,接着把 SYN 和 ACK 标志位置为 1。最后把该报⽂发给客⼾端,该报⽂也不 包含应⽤层数据,之
后服务端处于 SYN-RCVD 状态。
客⼾端收到服务端报⽂后,还要向服务端回应最后⼀个应答报⽂,⾸先该应答报⽂ TCP ⾸部
ACK 标志位置为 1 ,其次「确认应答号」字段填⼊ server_isn + 1 ,最后把报 ⽂发送给服务
端,这次报⽂可以携带客⼾到服务端的数据,之后客⼾端处于 ESTABLISHED 状态。
服务端收到客⼾端的应答报⽂后,也进⼊ ESTABLISHED 状态。
三次握⼿要实现什么 ⽬的?
⾯试官:同步序列号,保证数据不丢 失。我说是建⽴连接,收发数据,⾯试官说你这个说的也没
问题,另⼀个⻆度解释⼀下。** 补充:** 三个 ⽅⾯分析三次握⼿的原因:
三次握⼿才可以阻⽌重复历史连接的初始化(主要原因)
三次握⼿才可以同步双⽅的初始序列号
三次握⼿才可以避免资源浪费
第⼀个原因:避免历史连接
简单来说,三次握⼿的** ⾸要原因是为了 防⽌旧的重复连接初始化造成混乱。** 我们考虑⼀个场
景,客⼾端先发送了 SYN (seq = 90 )报⽂,然后客⼾端宕机了,⽽且这个 SYN 报⽂还被⽹络阻
塞了,服务端并没有收到,接着客⼾端重启后 ,⼜重新向服务端建⽴连接,发送了 SYN (seq = 100 )报⽂(注意!不是重传 SYN ,重传的 SYN 的序列号是⼀样的)。看看 三次握⼿是如何阻⽌历史连接的:

客⼾端连续发送多次 SYN (都是同⼀个四元组)建⽴连接的报⽂,在⽹络拥堵情况下:
⼀个「旧 SYN 报⽂」⽐「最新的 SYN 」 报⽂早到达了服务端,那么此时服务端就会回⼀个
SYN + ACK 报⽂给客⼾端,此报⽂中的确认号是 91 (90+1 )。
客⼾端收到后,发现⾃⼰期望 收到的确认号应该是 100 + 1 ,⽽不是 90 + 1 ,于是就会回 RST
报⽂。
服务端收到 RST 报⽂后,就会释放连接。
后续最新的 SYN 抵达了服务端后,客⼾端与服务端就可以正常的完成三次握⼿了。
上述中的「旧 SYN 报⽂」称为历史连接,TCP 使⽤三次握⼿建⽴连接的最主要原因就是防⽌「历
史连接」初始化了连接。
第⼆个原因:同步双⽅初始序列号
TCP 协议的通信双⽅, 都必须维护⼀个「序列号」, 序列号是可靠传输的⼀个关键因素,它的作
⽤:
接收⽅可以去除重复的数据;
接收⽅可以根据数据包的序列号按序接收;
可以标识发送出去的数据包中, 哪些是已经被对⽅收到的(通过 ACK 报⽂中的序列号知道);
可⻅,序列号在 TCP 连接中占据着⾮常重要的作⽤,所以当客⼾端发送携带「初始序列号」的
SYN 报⽂的时候,需要服务端回⼀个 ACK 应答报⽂,表⽰客⼾端的 SYN 报⽂已被服务端成功接
收,那当服务端发送「初始序列号」给客⼾端的时候,依然也要得到客⼾端的应答回应,这样⼀
来⼀回,才能确保双⽅的初始序列号能被可靠的同步。

四次握⼿其实也能够可靠的同步双⽅的初始化序号,但由于第⼆步和第三步可以优 化成⼀步,所
以就成了「三次握⼿」。⽽两次握⼿只保证了⼀⽅的初始序列号能被对⽅成功接收,没办法保证双
⽅的初始序列号都能被确认接收。
原因三:避免资源浪费
如果只有「两次握⼿」,当客⼾端发⽣的 SYN 报⽂在⽹络中阻塞,客⼾端没有接收到 ACK 报⽂,
就会重新发送 SYN ,由于没有第三次握⼿,服务端不清楚客⼾端是否收到了⾃⼰回复的 ACK 报
⽂,所以服务端每收到⼀个 SYN 就只能先主动建⽴⼀个连接,这会造成什么 情况呢?如果客⼾端
发送的 SYN 报⽂在⽹络中阻塞了,重复发送多次 SYN 报⽂,那么服务端在收到请求后就会建⽴多
个冗余的⽆效链接,造成不必要的资源浪费。

即两次握⼿会造成消息滞留情况下,服务端重复接受⽆⽤的连接请求 SYN 报⽂,⽽造成重复分配
资源。
项⽬
JWT 令牌和传统⽅式有什么 区别?(答上来了)
⽆状态性 :JWT 是⽆状态的令牌,不需要在服务器端存储会话信息。相反,JWT 令牌中包含了所
有必要的信息,如⽤⼾⾝份、权限等。这使得JWT 在分布式系统中更加适⽤,可以⽅便地进⾏
扩展和跨域访问。
安全性:JWT 使⽤密钥对令牌进⾏签名,确保令 牌的完整性和真实性。只有持有正确密钥的服
务器才能对令牌进⾏验证和解析。这种⽅式⽐传统的基于会话和Cookie 的验证更加安全,有效
防⽌了CSRF (跨站请求伪造)等攻击。
跨域⽀持:JWT 令牌可以在不同域之间传递,适⽤于跨域访问的场景。通过在请求的头部或参
数中携带JWT 令牌,可以实现⽆需Cookie 的跨域⾝份验证。
JWT 令牌为什么 能解决集群部署,什么 是集群部署?( 答上来了)
在传统的基于会话和Cookie 的⾝份验证⽅式中,会话信息通常存储在服务器的内存或数据库中。
但在集群部署中,不同服务器之间没有共享的会话信息,这会导致⽤⼾在不同服务器之间切换时
需要重新登录,或者需要引⼊额外的共享机制(如Redis ),增加了复杂性和性能开销。

⽽JWT 令牌通过在令牌中包含所有必要的⾝份验证和会话信息,使得服务器⽆需存储会话信息,从
⽽解决了集群部署中的⾝份验证和会话管理问题。当⽤⼾进⾏登录认证 后,服务器将⽣成⼀个JWT
令牌并返回给客⼾端。客⼾端在后续的请求中携带该令牌,服务器可以通过对令牌进⾏验证和解
析来 获取⽤⼾⾝份和权限信息,⽽⽆需访问共享的会话存储。
由于JWT 令牌是⾃包含的,服务器可以独⽴地对令牌进⾏验证,⽽不需要依赖其他服务器或共享存
储。这使得集群中的每个服务器都可以独⽴处理请求,提⾼了系统的可伸缩性和容错性。
JWT 令牌都有哪些字段?( 没答上来,忘了有哪些,没想到会问)
JWT 令牌由三个 部分组成:头部(Header )、载荷(Payload )和签名(Signature )。其中,头部和载荷均为JSON 格式,使⽤Base64 编码进⾏序列化,⽽签名部分是对头部、载荷和密钥进⾏签名后 的结果。

JWT 令牌如果泄露了,怎么解决,JWT 是怎么做的?
及时失效令牌:当检测到JWT 令牌泄露或存在⻛险时,可以⽴即将令牌标记为失效状态。服务
器在接收到带有失效标记的令牌时,会拒绝对其进⾏任何 操作,从⽽保护⽤⼾的⾝份和数据安
全。
刷新令牌:JWT 令牌通常具有⼀定的有效期,过期后需要重新获取新的令牌。当检测到令牌泄
露时,可以主动刷 新令牌,即重新⽣成⼀个新的令牌,并将旧令牌标记为失效状态。这样,即
使泄露的令牌被恶意使⽤,也会很快 失效,减少了被攻击者滥⽤的⻛险。
使⽤⿊名单:服务器可以维护⼀个令牌的⿊名单,将泄露的令牌添加到 ⿊名单中。在接收到令
牌时,先检查令牌是否在⿊名单中,如果在则拒绝操作。这种⽅法需要服务器维护⿊名单的状
态,对性能有⼀定的影响,但可以有效地保护泄露的令牌不被滥⽤。
⽹关统⼀鉴权怎么做的?
答上来了,把流程说⼀遍就⾏了(2次算法,时间戳,随机数)
你这个SHA256 算法不算加密吧?算签 名吧 ?
完了晕菜了,⼀直背的⽤SHA256 算法对secretKey 加密,没思考过
补充:签名是需要⽤秘钥和原始数据(或者SHA256 后的数据)⼀起⽤加密算法⽣成的
你说说 加密和签名有什么 区别?
追问⽴⻢就来了,跳进⼤坑,回答不知道
补充:
加密是为了 保护数据,通常对应解密;⽽签名是将数据和私钥⼀起⽤加密算法得到,⽤于验证
数据的来源和完整性,确保数据传输过 程中未被修改。
加密要⽤公钥私钥(对称秘钥),但签名验证可以只⽤公钥
那如果别⼈伪造你的签名,你怎么解决?
我说每个⽤⼾都有唯⼀的32 位secretKey ,没办法伪造
补充:
伪造签名的解决⽅法:伪造签名是⼀种安全漏洞,可能导致恶意⾏为。为了 防⽌伪造签名,可
以采取以下措施:
使⽤安全的密钥管理:确保私钥⽤于签名的安全存储和管理,以防⽌未经授权的访问。使⽤硬
件安全模块(HSM )等⼯具来保护私钥。
使⽤公开密钥基础设施(PKI ):PKI 提供了⼀种建⽴信任 关系的⽅式,可以确保公钥的有效性。
公钥的合法性可以通过证书颁发机构 (CA )进⾏验证。
使⽤数字证书:数字证书是包含公钥和⾝份信 息的数字⽂件,⽤于验证签名的合法性。使⽤数
字证书来验证签名可 以提⾼安全性。
保护通信渠道:确保数据在传输过 程中受到适当的加密和安全措施的保护,以防⽌中间⼈攻
击。
定期更 换密钥:定期更 换⽤于签名的密钥,以降低⻓期存在的密钥被滥⽤的⻛险。
