Spark源码阅读03-Spark存储原理之存储分析

def registerOrLookupEndpoint(name: String, endpointCreator: => RpcEndpoint): RpcEndpointRef = {

//创建远程数据传输服务,使用Netty方式

val blockTransferService =

new NettyBlockTransferService(conf, securityManager, bindAddress, advertiseAddress,

blockManagerPort, numUsableCores)

//创建blockManagerMaster,如果是Driver端在blockManagerMaster内部则创建终端点BlockManagerMasterEndpoint

//如果是Executor,则创建BlockManagerMasterEndpoint的引用

val blockManagerMaster = new BlockManagerMaster(registerOrLookupEndpoint(

BlockManagerMaster.DRIVER_ENDPOINT_NAME,

new BlockManagerMasterEndpoint(rpcEnv, isLocal, conf, listenerBus)),

conf, isDriver)

//创建blockManager,如果是Driver端包含blockManagerMaster,如果是executor包含的是blockManagerMaster

//的引用,另外blockManager包含了远程数据传输服务,当BlockManager调用initialize()方法才生效

val blockManager = new BlockManager(executorId, rpcEnv, blockManagerMaster,

serializerManager, conf, memoryManager, mapOutputTracker, shuffleManager,

blockTransferService, securityManager, numUsableCores)

}

其中BlockManager调用initialize()方法的初始化如下:

def initialize(appId: String): Unit = {

//在Executor中启动远程数据传输服务,根据配置启动传输服务器blockTransferService,

//该服务器启动后等待其他节点发送请求消息

blockTransferService.init(this)

shuffleClient.init(appId)

blockReplicationPolicy = {

val priorityClass = conf.get(

“spark.storage.replication.policy”, classOf[RandomBlockReplicationPolicy].getName)

val clazz = Utils.classForName(priorityClass)

val ret = clazz.newInstance.asInstanceOf[BlockReplicationPolicy]

logInfo(s"Using $priorityClass for block replication policy")

ret

}

//获取blockManager编号

val id =

BlockManagerId(executorId, blockTransferService.hostName, blockTransferService.port, None)

val idFromMaster = master.registerBlockManager(

id,

maxOnHeapMemory,

maxOffHeapMemory,

slaveEndpoint)

blockManagerId = if (idFromMaster != null) idFromMaster else id

//获取shuffle服务编号,如果启动外部shuffle服务,则加入外部Shuffle服务端口信息,

//否则使用使用blockManager编号

shuffleServerId = if (externalShuffleServiceEnabled) {

logInfo(s"external shuffle service port = $externalShuffleServicePort")

BlockManagerId(executorId, blockTransferService.hostName, externalShuffleServicePort)

} else {

blockManagerId

}

//如果外部shuffle服务启动并且为executor节点,则注册为外部shuffle服务

if (externalShuffleServiceEnabled && !blockManagerId.isDriver) {

registerWithExternalShuffleServer()

}

logInfo(s"Initialized BlockManager: $blockManagerId")

}

(2)写入、 更新或删除数据完毕后, 发送数据块的最新状态消息UpdateBlockinfo给BlockManagerMasterEndpoint终端点, 由其更新数据块的元数据。 该终端点的元数据存放在BlockManagerMasterEndpoint的3个HashMap中, 分别如下:

class BlockManagerMasterEndpoint(override val rpcEnv: RpcEnv,val isLocal: Boolean,conf: SparkConf,listenerBus: LiveListenerBus)

extends ThreadSafeRpcEndpoint with Logging {

//该HashMap中存放了BlockManagerId与BlockManagerInfo的对应,其中BlockManagerInfo

//包含了executor的内存使用情况、数据块的使用情况、已被缓存的数据块和executor终端点的引用

private val blockManagerInfo = new mutable.HashMap[BlockManagerId, BlockManagerInfo]

//该HashMap存放了BlockManagerId和executorId对应列表

private val blockManagerIdByExecutor = new mutable.HashMap[String, BlockManagerId]

//该HashMap存放了BlockManagerId和BlockId序列所对应的列表,原因在于一个数据块可能存储

//多个副本,保存在多个executor中

private val blockLocations = new JHashMap[BlockId, mutable.HashSet[BlockManagerId]]

}

(3)应用程序数据存储后, 在获取远程节点数据、 获取RDD执行的首选位置等操作时需要根据数据块的编号查询数据块所处的位置, 此时发送 GetLocations 或GetLocationsMultipleBlocklds等消息给BlockManagerMasterEndpoint终端点,通过对元数据的查询获取数据块的位置信息。

代码实现如下:

private def getLocations(blockId: BlockId): Seq[BlockManagerId] = {

//根据blockId判断是否包含数据块,如果包含,则返回其对应的BlockManagerId序列

if (blockLocations.containsKey(blockId)) blockLocations.get(blockId).toSeq

else Seq.empty

}

(4)Spark提供删除RDD、 数据块和广播变量等方式。 当数据需要删除时, 提交删除消息给BlockManagerSlaveEndpoint 终端点, 在该终端点发起删除操作, 删除操作一方面需要删除Driver端元数据信息,另一方面需要发送消息通知Executor,删除对应的物理数据。下面以RDD的unpersistRDD方法描述其删除过程。类调用关系图如下:

在这里插入图片描述

首先, 在SparkConext中调用unpersistRDD方法, 在该方法中发送removeRdd 消息给 BlockManagerMasterEndpoint终端点;然后, 该终端点接收到消息时, 从blockLocations列表中找出该ROD对应的数据存在BlockManagerld 列表, 查询完毕后, 更新blockLocations和 blockManagerlnfo两个数据块元数据列表; 然后, 把获取的BlockManagerld列表, 发送消息给所在BlockManagerSlaveEndpoint 终端点, 通知其删除该 Executor上的RDD, 删除时调用 BlockManager的removeRdd方法, 删除在Executor上RDD所对应的数据块。 其中在 BlockManagerMasterEndpoint终端点的removeRdd代码如下:

private def removeRdd(rddId: Int): Future[Seq[Int]] = {

//在blockLocations和blockManagerInfo中删除该RDD的数据元消息

//首先,根据RDD编号获取该RDD存储的数据块信息

val blocks = blockLocations.asScala.keys.flatMap(.asRDDId).filter(.rddId == rddId)

blocks.foreach { blockId =>

//根据数据块信息找出这些数据块所在BlockManagerId列表,遍历这些列表并删除

//BlockManager包含该数据块的元数据,同时删除blockLocations对应数据块的元数据

val bms: mutable.HashSet[BlockManagerId] = blockLocations.get(blockId)

bms.foreach(bm => blockManagerInfo.get(bm).foreach(_.removeBlock(blockId)))

blockLocations.remove(blockId)

}

//最后发送RemoveRdd消息给executor,通知其删除RDD

val removeMsg = RemoveRdd(rddId)

val futures = blockManagerInfo.values.map { bm =>

bm.slaveEndpoint.askInt.recover {

case e: IOException =>

logWarning(s"Error trying to remove RDD $rddId from block manager ${bm.blockManagerId}",

e)

0 // zero blocks were removed

}

}.toSeq

Future.sequence(futures)

}

存储级别

Spark虽是基于内存的计算,但RDD的数据集不仅可以存储在内存中,还可以使用persist方法或cache方法显示地将RDD的数据集缓存到内存或者磁盘中。persist的代码实现如下:

private def persist(newLevel: StorageLevel, allowOverride: Boolean): this.type = {

// TODO: Handle changes of StorageLevel

//如果RDD指定了非NONE的存储级别,该存储级别则不能进行修改

if (storageLevel != StorageLevel.NONE && newLevel != storageLevel && !allowOverride) {

throw new UnsupportedOperationException(

“Cannot change storage level of an RDD after it was already assigned a level”)

}

//当RDD原来的存储级别为NONE时,可以对RDD进行持久化处理,在处理前需要先清除SparkContext

//中原来RDD相关的存储元数据,然后加入RDD的持久化信息

if (storageLevel == StorageLevel.NONE) {

sc.cleaner.foreach(_.registerRDDForCleanup(this))

sc.persistRDD(this)

}

//当RDD原来的存储级别为NONE时,把RDD存储级别修改为传入新值

storageLevel = newLevel

this

}

RDD第一次被计算时, persist方法会根据参数StorageLevel的设置采取特定的缓存策略,当RDD原本存储级别为NONE或者新传递进来的存储级别值与原来 的存储级别相等时才进行 操作,由于persist操作是控制操作的一种, 它只是改变了原RDD的元数据信息, 并没有进行数据的存储操作, 真正进行是RDD的iterator方法中。 对于cache方法而言, 它只是persist 方法的一个特例, 即persist方法的参数为MEMORY_ONLY的情况。

在StorageLevel类中, 根据useDisk、 useMemory、 useOffHeap、 deserialized 、replication5 个参数的组合, Spark提供了12种存储级别的缓存策略, 这可以将RDD持久化到内存、磁盘 和外部存储统, 或者是以序列化的方式持久化到内存中, 从至可以在集群的不同节点之间存储多份副本。代码实现如下:

class StorageLevel private(

private var _useDisk: Boolean,

private var _useMemory: Boolean,

private var _useOffHeap: Boolean,

private var _deserialized: Boolean,

private var _replication: Int = 1)

extends Externalizable {

object StorageLevel {

val NONE = new StorageLevel(false, false, false, false)

val DISK_ONLY = new StorageLevel(true, false, false, false)

val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)

val MEMORY_ONLY = new StorageLevel(false, true, false, true)

val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)

val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)

val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)

val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)

val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)

val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)

val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)

val OFF_HEAP = new StorageLevel(true, true, true, false, 1)

}

RDD存储调用

RDD与数据块Block之间的关系

RDD 包含多个 Partition, 每个 Partition 对应一个数据块 Block, 那么每个RDD 中包含一个或多个数据块 Block, 每个 Block 拥有唯一的编号BlockId,对应数据块编号规则:“rdd_” + rddId + ‘’_" + splitIndex。其中splitIndex为该数据块对应Partition的序列号。

在 persist 方法中并没有发生数据存储操作动作, 实际发生数据操作是任务运行过程中, RDD 调用 iterator 方法时发生的。 在调用过程中, 先根据数据块 Block 编号在 判断是否已经按照指定的存储级别进行存储, 如果存在该数据块 Block, 则从本地或远程节点读取数据; 如果不存在该数据块 Block, 则调用 RDD 的计算方法得出结果, 并把结果按照指定 的存储级别进行存储。 RDD 的 iterator 方法代码如下:

final def iterator(split: Partition, context: TaskContext): Iterator[T] = {

if (storageLevel != StorageLevel.NONE) {

//如果存在存储级别,尝试读取内存的数据进行迭代计算

getOrCompute(split, context)

} else {

//如果不存在存储级别,则直接读取数据进行迭代计算或者读取检查点结果进行迭代计算

computeOrReadCheckpoint(split, context)

}

}

其中调用的getOrCompute 方法是存储逻辑的核心, 代码如下:

private[spark] def getOrCompute(partition: Partition, context: TaskContext): Iterator[T] = {

//通过RDD的编号和partition序号获取数据块block的编号

val blockId = RDDBlockId(id, partition.index)

var readCachedBlock = true

// This method is called on executors, so we need call SparkEnv.get instead of sc.env.

//由于该方法由executor调用,可使用sparkEnv代替sc.env

//根据数据块block编号先读取数据,然后再更新数据,这里是读写数据的入口点(getOrElseUpdate)

SparkEnv.get.blockManager.getOrElseUpdate(blockId, storageLevel, elementClassTag, () => {

//如果数据块不在内存,则尝试读取检查点结果进行迭代计算

readCachedBlock = false

computeOrReadCheckpoint(partition, context)

}) match {

//对getOrElseUpdate返回结果进行处理,该结果表示处理成功,记录结果度量信息

case Left(blockResult) =>

if (readCachedBlock) {

val existingMetrics = context.taskMetrics().inputMetrics

existingMetrics.incBytesRead(blockResult.bytes)

new InterruptibleIterator[T](context, blockResult.data.asInstanceOf[Iterator[T]]) {

override def next(): T = {

existingMetrics.incRecordsRead(1)

delegate.next()

}

}

} else {

new InterruptibleIterator(context, blockResult.data.asInstanceOf[Iterator[T]])

}

//对getOrElseUpdate返回结果进行处理,该结果表示处理失败,把该结果返回给调用者,由其决定如何处理

case Right(iter) =>

new InterruptibleIterator(context, iter.asInstanceOf[Iterator[T]])

}

}

在getOrCompute调用getOrElseUpdate方法, 该方法是存储读写数据的入口点:

//该方法是存储读写数据的入口点

def getOrElseUpdate[T](

blockId: BlockId,

level: StorageLevel,

classTag: ClassTag[T],

makeIterator: () => Iterator[T]): Either[BlockResult, Iterator[T]] = {

// Attempt to read the block from local or remote storage. If it’s present, then we don’t need

// to go through the local-get-or-put path.

//读取数据块入口,尝试从本地数据或者远程读取数据

getT(classTag) match {

case Some(block) =>

return Left(block)

case _ =>

// Need to compute the block.

}

// Initially we hold no locks on this block.

//写输入入口

doPutIterator(blockId, makeIterator, level, classTag, keepReadLock = true) match {

case None =>

val blockResult = getLocalValues(blockId).getOrElse {

releaseLock(blockId)

throw new SparkException(s"get() failed for block $blockId even though we held a lock")

}

releaseLock(blockId)

Left(blockResult)

case Some(iter) =>

Right(iter)

}

}

读数据过程

BlockManager的get方法是读数据的入口点, 在读取时分为本地读取和远程节点读取两个步骤。本地读取使用getLocalValues方法,在该方法中根据不同的存储级别直接调用不同存储实现的方法:而远程节点读取使用getRemote Values方法,在getRemoteValues 方法中调用了 getRemoteBytes方法,在方法中调用远程数据传输服务类BlockTransferService的fetchBlockSync 进行处理,使用Netty的fetchBlocks方法获取数据。整个数据读取类调用如下:

在这里插入图片描述

本地读取

本地读取根据不同的存储级别分为内存和磁盘两种读取方式。其介绍分别如下:

1. 内存读取

在getLocalValues方法中,读取内存中的数据根据返回的是封装成BlockResult类型还是数据流,分别调用MemoryStore的getValues和getBytes两种方法,代码如下:

def getLocalValues(blockId: BlockId): Option[BlockResult] = {

//使用内存存储级别,并且数据存储在内存情况

if (level.useMemory && memoryStore.contains(blockId)) {

val iter: Iterator[Any] = if (level.deserialized) {

//如果存储时使用反序列化,则直接读取内存中的数据

memoryStore.getValues(blockId).get

} else {

//如果存储时未使用反序列化,则内存中的数据后做反序列化处理

serializerManager.dataDeserializeStream(

blockId, memoryStore.getBytes(blockId).get.toInputStream())(info.classTag)

}

// We need to capture the current taskId in case the iterator completion is triggered

// from a different thread which does not have TaskContext set; see SPARK-18406 for

// discussion.

//数据读取完毕后,返回数据及数据块大小、读取方法等信息

val ci = CompletionIterator[Any, Iterator[Any]](iter, {

releaseLock(blockId, taskAttemptId)

})

Some(new BlockResult(ci, DataReadMethod.Memory, info.size))

}

}

在MemoryStore的getValues和getBytes方法中,最终都是通过数据块编号获取内存中的数据, 其代码为:

val entry = en七ries.synchronized { entries.get(blockld) }

2.磁盘读取

磁盘读取在getLocalValues方法中, 调用的是DiskStore的getBytes方法, 在读取磁盘中的数据后需要把这些数据缓存到内存中, 代码实现如下:

def getLocalValues(blockId: BlockId): Option[BlockResult] = {

else if (level.useDisk && diskStore.contains(blockId)) {

//从磁盘获取数据,由于保存到磁盘中的数据是序列化的,读取到的数据也是序列化的

val diskData = diskStore.getBytes(blockId)

val iterToReturn: Iterator[Any] = {

if (level.deserialized) {

//如果存储级别需要反序列化,则把读取数据反序列化,然后存储到内存中去

val diskValues = serializerManager.dataDeserializeStream(

blockId,

diskData.toInputStream())(info.classTag)

maybeCacheDiskValuesInMemory(info, blockId, level, diskValues)

} else {

//如果存储级别不需要反序列化,则直接把这些序列化数据存储到内存中

val stream = maybeCacheDiskBytesInMemory(info, blockId, level, diskData)

.map { _.toInputStream(dispose = false) }

.getOrElse { diskData.toInputStream() }

//返回的数据需进行反序列化处理

serializerManager.dataDeserializeStream(blockId, stream)(info.classTag)

}

}

//数据读取完毕后,返回数据及数据块大小,读取方法等信息

val ci = CompletionIterator[Any, Iterator[Any]](iterToReturn, {

releaseLockAndDispose(blockId, diskData, taskAttemptId)

})

Some(new BlockResult(ci, DataReadMethod.Disk, info.size))

}

在DiskStore中的getBytes方法中, 调用DiskBlockManager的getfile方法获取数据块所在文件的句柄。该文件名为数据块的文件名,文件所在一级目录和二级子目录索引值通过文件名的哈希值取模获取,其代码实现如下:

def getFile(filename: String): File = {

//根据文件名的哈希值获取一级目录和二级目录索引值,其中一级目录索引值为哈希值与一级目录个数的模,

// 而二级目录索引值为哈希值与二级子目录个数的模

val hash = Utils.nonNegativeHash(filename)

val dirId = hash % localDirs.length

val subDirId = (hash / localDirs.length) % subDirsPerLocalDir

//先通过一级目录和二级目录索引值获取该目录,然后判断该目录是否存在

val subDir = subDirs(dirId).synchronized {

val old = subDirs(dirId)(subDirId)

if (old != null) {

old

} else {

//如果该目录不存在则创建该目录,范围为00-63

val newDir = new File(localDirs(dirId), “%02x”.format(subDirId))

if (!newDir.exists() && !newDir.mkdir()) {

throw new IOException(s"Failed to create local dir in $newDir.")

}

//判断该文件是否存在,如果不存在,则创建

subDirs(dirId)(subDirId) = newDir

newDir

}

}

//通过文件的路径获取文件的句柄并返回

new File(subDir, filename)

}

获取文件句柄后, 读取整个文件内容。其代码如下:

def getBytes(blockId: BlockId): BlockData = {

//获取数据块所在文件的句柄

val file = diskManager.getFile(blockId.name)

val blockSize = getSize(blockId)

securityManager.getIOEncryptionKey() match {

case Some(key) =>

new EncryptedBlockData(file, blockSize, conf, key)

case _ =>

new DiskBlockData(minMemoryMapBytes, maxMemoryMapBytes, file, blockSize)

}

}

远程读取

在远程节点读取数据的时候, Spark只提供了Netty远程读取方式,下面分析Netty远程数据读取过程。 在Spark中主要由下而两个类处理Netty远程数据读取:

  • NettyBlockTransferService: 该类向Shuffle、 存储模块提供了数据存取的接口, 接收到数据存取的命令时, 通过Netty的RPC架构发送消息给指定节点, 请求进行数据存取操作。

  • NettyBlockRpcServer: 当Executor启动时, 同时会启动RCP监听器, 当监听到消息 把消息传递到该类进行处理, 消息内容包括读取数据OpenBlocks 和写入数据Upload Block两种。

使用Netty处理远程数据读取流程如下:

(1)Spark远程读取数据入口为getRemoteValues, 然后调用getRemoteBytes方法, 在该方法中调用sortLocations方法先向BlockManagerMasterEndpoint 终端点发送SortLocations消息,请求据数据块所在的位置信息。 当Driver的终端点接收到请求消息时, 根据数据块的编号获取该数据块所在的位置列表, 根据是否是本地节点数据对位置列表进行排序。 其中BlockManager 类中的sortLocations方法代码片段如下:

private def sortLocations(locations: Seq[BlockManagerId]): Seq[BlockManagerId] = {

//获取数据块节点所在节点的信息

val locs = Random.shuffle(locations)

//从获取的节点信息中,优先读取本地节点数据

val (preferredLocs, otherLocs) = locs.partition { loc => blockManagerId.host == loc.host }

blockManagerId.topologyInfo match {

case None => preferredLocs ++ otherLocs

case Some(_) =>

val (sameRackLocs, differentRackLocs) = otherLocs.partition {

loc => blockManagerId.topologyInfo == loc.topologyInfo

}

preferredLocs ++ sameRackLocs ++ differentRackLocs

}

}

获取数据块的位置列表后,在BlockManager.getRemoteBytes方法中调用BlockTransferService提供的fetchBlockSync方法进行读取远程数据。代码实现如下:

def getRemoteBytes(blockId: BlockId): Option[ChunkedByteBuffer] = {

var runningFailureCount = 0

var totalFailureCount = 0

//获取数据块的位置

val locations = sortLocations(blockLocations)

val maxFetchFailures = locations.size

var locationIterator = locations.iterator

while (locationIterator.hasNext) {

val loc = locationIterator.next()

logDebug(s"Getting remote block $blockId from $loc")

//通过blockTransferService提供的fetchBlockSync方法远程获取数据

val data = try {

blockTransferService.fetchBlockSync(

loc.host, loc.port, loc.executorId, blockId.toString, tempFileManager)

} catch {

}

//获取到数据后,返回该数据块

if (data != null) {

if (remoteReadNioBufferConversion) {

return Some(new ChunkedByteBuffer(data.nioByteBuffer()))

} else {

return Some(ChunkedByteBuffer.fromManagedBuffer(data))

}

}

logDebug(s"The value of block $blockId is null")

}

logDebug(s"Block $blockId not found")

None

}

(2)调用远程数据传输服务BlockTransferService的fetchBlockSync方法后,在该方法继续调用fetchBlocks方法。

override def fetchBlocks(

host: String,

port: Int,

execId: String,

blockIds: Array[String],

listener: BlockFetchingListener,

tempFileManager: DownloadFileManager): Unit = {

logTrace(s"Fetch blocks from h o s t : host: host:port (executor id $execId)")

try {

val blockFetchStarter = new RetryingBlockFetcher.BlockFetchStarter {

override def createAndStart(blockIds: Array[String], listener: BlockFetchingListener) {

//根据远程节点的节点和端口创建通信客户端

val client = clientFactory.createClient(host, port)

//通过该客户端向指定节点发送获取数据消息

new OneForOneBlockFetcher(client, appId, execId, blockIds, listener,

transportConf, tempFileManager).start()

}

}

}

其中发送读取消息是在OneForOneBlockFetcher类中实现,在该类中的构造函数定义了该消息this.openMessage = new OpenBlocks(appld, execld, blocklds),然后在该类的start方法中向RPC客户端发送消息:

public void start() {

if (blockIds.length == 0) {

throw new IllegalArgumentException(“Zero-sized blockIds array”);

}

//通过客户端发送读取数据块的消息

client.sendRpc(openMessage.toByteBuffer(), new RpcResponseCallback() {

@Override

public void onSuccess(ByteBuffer response) {

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

我还通过一些渠道整理了一些大厂真实面试主要有:蚂蚁金服、拼多多、阿里云、百度、唯品会、携程、丰巢科技、乐信、软通动力、OPPO、银盛支付、中国平安等初,中级,高级Java面试题集合,附带超详细答案,希望能帮助到大家。

新鲜出炉的蚂蚁金服面经,熬夜整理出来的答案,已有千人收藏

还有专门针对JVM、SPringBoot、SpringCloud、数据库、Linux、缓存、消息中间件、源码等相关面试题。

新鲜出炉的蚂蚁金服面经,熬夜整理出来的答案,已有千人收藏

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
s(ByteBuffer response) {

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-V9tlwQL8-1712725484525)]

[外链图片转存中…(img-IMnt258X-1712725484526)]

[外链图片转存中…(img-yIdlMl2F-1712725484526)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

我还通过一些渠道整理了一些大厂真实面试主要有:蚂蚁金服、拼多多、阿里云、百度、唯品会、携程、丰巢科技、乐信、软通动力、OPPO、银盛支付、中国平安等初,中级,高级Java面试题集合,附带超详细答案,希望能帮助到大家。

[外链图片转存中…(img-oMMuunsh-1712725484526)]

还有专门针对JVM、SPringBoot、SpringCloud、数据库、Linux、缓存、消息中间件、源码等相关面试题。

[外链图片转存中…(img-Fx4QSugu-1712725484526)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 27
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值