Spark-RDD持久化

news2024/9/20 15:45:50

一、Spark的三种持久化机制

1、cache

它是persist的一种简化方式,作用是将RDD缓存到内存中,以便后续快速访问,提高计算效率。cache操作是懒执行的,即执行action算子时才会触发。

2、persist

它提供了不同的存储级别(仅磁盘、仅内存、内存或磁盘、内存或磁盘+副本数、序列化后存入内存或磁盘、堆外)可以根据不同的应用场景进行选择。

3、checkpoint

它将数据永久保存,用于减少长血缘关系带来的容错成本。checkpoint不仅保存了数据,还保存了计算该数据的算子操作。当需要恢复数据时,可以通过这些操作重新计算,而不仅仅是依赖于原始数据。且在作业完成后仍然保留,可以用于后续的计算任务。

二、用法示例

1、cache

//制作数据
val data: RDD[Int] = sc.parallelize( 1 to 10000)
//简单加工
val tempRdd: RDD[(String, Int)] = data.map(num=>if(num%2==0)("even",num)else("odd",num))
//缓存
tempRdd.cache()
//调用action算子运行
tempRdd.foreach(println)

 我们看下tempRdd的存储情况:

2、persist

//制作数据
val data: RDD[Int] = sc.parallelize( 1 to 10000)
//简单加工
val tempRdd: RDD[(String, Int)] = data.map(num=>if(num%2==0)("even",num)else("odd",num))
//持久化
tempRdd.persist(StorageLevel.MEMORY_AND_DISK)
//调用action算子运行
tempRdd.foreach(println)

 

3、checkpoint

//使用checkpoint之前需要用sc先设置检查点目录
sc.setCheckpointDir("./local-spark/checkpoint-data")
//制作数据
val data:RDD[Int] = sc.parallelize( 1 to 10000)
//简单加工
val tempRdd:RDD[(String, Int)] = data.map(num=>if(num%2==0)("even",num)else("odd",num))
//持久化
tempRdd.persist(StorageLevel.MEMORY_AND_DISK)
//创建checkpoint 会触发job
tempRdd.checkpoint()
//调用action算子运行
tempRdd.foreach(println)

从历史服务界面可以观察到,该程序启动了两个job(在源码分析中我们就会知道原因)

 

我们再看下两个job的DAG

发现重复的计算跑了两次,因此我们在使用checkpoint前一般都会添加一个persist来进行加速

下面是添加完persist后再进行checkpoint的DAG,虽然也是两个Job,但是tempRdd上的那个点变了颜色,这意味着tempRdd之前的步骤就不用重复计算了

 

三、源码分析

1、cache

//使用默认存储级别(`MEMORY_ONLY`)持久化此RDD
def cache(): this.type = persist()
//其实背后就是使用的persist
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)

2、persist

RDD

abstract class RDD[T: ClassTag](
    @transient private var _sc: SparkContext,
    @transient private var deps: Seq[Dependency[_]]
  ) extends Serializable with Logging {


  //设置此RDD的存储级别,以便在第一次计算后跨操作持久化其值。
  /只有当RDD尚未设置存储级别时,这才能用于分配新的存储级别。本地检查点是一个例外。
  def persist(newLevel: StorageLevel): this.type = {
    if (isLocallyCheckpointed) {
      //这意味着用户之前调用了localCheckpoint(),它应该已经将此RDD标记为持久化。
      //在这里,我们应该用用户明确请求的存储级别(在将其调整为使用磁盘后)覆盖旧的存储级别。
      persist(LocalRDDCheckpointData.transformStorageLevel(newLevel), allowOverride = true)
    } else {
      persist(newLevel, allowOverride = false)
    }
  }


  //标记此RDD以使用指定级别进行持久化
  //newLevel 目标存储级别
  //allowOverride 是否用新级别覆盖任何现有级别
  private def persist(newLevel: StorageLevel, allowOverride: Boolean): this.type = {
    // 如果想要重新调整一个RDD的存储级别,就必须将allowOverride 置为 true
    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标记为持久化,请在SparkContext中注册它以进行清理和核算。只做一次。
    if (storageLevel == StorageLevel.NONE) {
      sc.cleaner.foreach(_.registerRDDForCleanup(this))
      //注册此RDD以持久化在内存和/或磁盘存储中
      sc.persistRDD(this)
    }
    //设置该RDD的storageLevel 以便在Task计算时直接获取数据,来加速计算
    storageLevel = newLevel
    this
  }

  //迭代器嵌套计算,如果该RDD是持久化的,就直接获取数据封装成iterator给后续RDD使用
  final def iterator(split: Partition, context: TaskContext): Iterator[T] = {
    if (storageLevel != StorageLevel.NONE) {
      getOrCompute(split, context)
    } else {
      computeOrReadCheckpoint(split, context)
    }
  }

  //获取或计算RDD分区
  private[spark] def getOrCompute(partition: Partition, context: TaskContext): Iterator[T] = {
    val blockId = RDDBlockId(id, partition.index)
    var readCachedBlock = true
    // 此方法在executors上调用,因此需要调用SparkEnv.get而不是sc.env 获取blockManager
    //接下来我们看下BlockManager的getOrElseUpdate方法
    //最后一个参数是一个匿名函数,如果缓存中没有块,需要调用它来获取块
    SparkEnv.get.blockManager.getOrElseUpdate(blockId, storageLevel, elementClassTag, () => {
      readCachedBlock = false
      computeOrReadCheckpoint(partition, context)
    }) match {
      // Block hit.
      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]])
        }
      // Need to compute the block.
      case Right(iter) =>
        new InterruptibleIterator(context, iter.asInstanceOf[Iterator[T]])
    }
  }

  //当缓存中没有块时调用它来制作块
  private[spark] def computeOrReadCheckpoint(split: Partition, context: TaskContext): Iterator[T] =
  {
    if (isCheckpointedAndMaterialized) {
      //如果checkpointed和materialized 那么直接返回
      firstParent[T].iterator(split, context)
    } else {
      //继续计算,通过迭代器嵌套计算,知道读取到有持久化的块或者进行Shuffle或者最初的数据源
      compute(split, context)
    }
  }



}

SparkContext

class SparkContext(config: SparkConf) extends Logging {

  //跟踪所有持久的RDD
  private[spark] val persistentRdds = {
    val map: ConcurrentMap[Int, RDD[_]] = new MapMaker().weakValues().makeMap[Int, RDD[_]]()
    map.asScala
  }

  private[spark] def persistRDD(rdd: RDD[_]) {
    persistentRdds(rdd.id) = rdd
  }


}

BlockManager

private[spark] class BlockManager(
    val executorId: String,
    rpcEnv: RpcEnv,
    val master: BlockManagerMaster,
    val serializerManager: SerializerManager,
    val conf: SparkConf,
    memoryManager: MemoryManager,
    mapOutputTracker: MapOutputTracker,
    shuffleManager: ShuffleManager,
    val blockTransferService: BlockTransferService,
    securityManager: SecurityManager,
    externalBlockStoreClient: Option[ExternalBlockStoreClient])
  extends BlockDataManager with BlockEvictionHandler with Logging {


  //如果给定的块存在,则检索它,
  //否则调用提供的`makeIterator `方法来计算该块,持久化它,并返回其值。
  def getOrElseUpdate[T](
      blockId: BlockId,
      level: StorageLevel,
      classTag: ClassTag[T],
      makeIterator: () => Iterator[T]): Either[BlockResult, Iterator[T]] = {
    // 尝试从本地或远程存储读取块。如果它存在,那么我们就不需要通过local-get-or-put路径。
    get[T](blockId)(classTag) match {
      case Some(block) =>
        return Left(block)
      case _ =>
        // 没有获取到块,需要计算,如果该RDD设置了持久化就对其持久化
    }
    // 最初,我们在这个块上没有锁.
    doPutIterator(blockId, makeIterator, level, classTag, keepReadLock = true) match {
      case None =>
        // doPut() 没有将工作交还给我们,因此该块已经存在或已成功存储。
        //因此,我们现在在块上持有读取锁。
        val blockResult = getLocalValues(blockId).getOrElse {
          // 由于我们在doPut()和get()调用之间保持了读取锁,因此该块不应该被驱逐,因此get()不返回该块表示存在一些内部错误
          releaseLock(blockId)
          throw new SparkException(s"get() failed for block $blockId even though we held a lock")
        }
        // 我们已经通过doPut()调用在块上持有读取锁,getLocalValue()再次获取锁,因此我们需要在这里调用releaseLock(),这样锁获取的净次数为1(因为调用者只会调用release())一次)。
        releaseLock(blockId)
        Left(blockResult)
      case Some(iter) =>
        // put失败,可能是因为数据太大,无法放入内存,无法放入磁盘。因此,我们需要将输入迭代器传递回调用者,以便他们可以决定如何处理这些值(例如,在不缓存的情况下处理它们)。
       Right(iter)
    }
  }


  //根据给定级别将给定块放入其中一个块存储中,必要时复制值
  //如果该块已存在,则此方法不会覆盖它。
  private def doPutIterator[T](
      blockId: BlockId,
      iterator: () => Iterator[T],
      level: StorageLevel,
      classTag: ClassTag[T],
      tellMaster: Boolean = true,
      keepReadLock: Boolean = false): Option[PartiallyUnrolledIterator[T]] = {
    doPut(blockId, level, classTag, tellMaster = tellMaster, keepReadLock = keepReadLock) { info =>
      val startTimeNs = System.nanoTime()
      var iteratorFromFailedMemoryStorePut: Option[PartiallyUnrolledIterator[T]] = None
      // 块的大小(字节)
      var size = 0L
      //如果RDD持久化选择有内存
      if (level.useMemory) {
        // 先把它放在内存中,即使它也将useDisk设置为true;如果内存存储无法容纳它,我们稍后会将其放入磁盘。
        //如果RDD持久化选择需要反序列化 
        if (level.deserialized) {
          //尝试将给定块作为值放入内存存储中
          memoryStore.putIteratorAsValues(blockId, iterator(), level.memoryMode, classTag) match {
            case Right(s) =>
              size = s
            case Left(iter) =>
              // 没有足够的空间展开此块;如果持久化也选择了磁盘,请下载到磁盘
              if (level.useDisk) {
                logWarning(s"Persisting block $blockId to disk instead.")
                diskStore.put(blockId) { channel =>
                  val out = Channels.newOutputStream(channel)
                  serializerManager.dataSerializeStream(blockId, out, iter)(classTag)
                }
                size = diskStore.getSize(blockId)
              } else {
                iteratorFromFailedMemoryStorePut = Some(iter)
              }
          }
        } else { // RDD持久化没有选择反序列化
          //尝试将给定块作为字节放入内存存储中
          memoryStore.putIteratorAsBytes(blockId, iterator(), classTag, level.memoryMode) match {
            case Right(s) =>
              size = s
            case Left(partiallySerializedValues) =>
              // 没有足够的空间展开此块;如果持久化也选择了磁盘,请下载到磁盘
              if (level.useDisk) {
                logWarning(s"Persisting block $blockId to disk instead.")
                diskStore.put(blockId) { channel =>
                  val out = Channels.newOutputStream(channel)
                  partiallySerializedValues.finishWritingToStream(out)
                }
                size = diskStore.getSize(blockId)
              } else {
                iteratorFromFailedMemoryStorePut = Some(partiallySerializedValues.valuesIterator)
              }
          }
        }
      //RDD持久化时也选择了磁盘
      } else if (level.useDisk) {
        diskStore.put(blockId) { channel =>
          val out = Channels.newOutputStream(channel)
          serializerManager.dataSerializeStream(blockId, out, iterator())(classTag)
        }
        size = diskStore.getSize(blockId)
      }

      val putBlockStatus = getCurrentBlockStatus(blockId, info)
      val blockWasSuccessfullyStored = putBlockStatus.storageLevel.isValid
      if (blockWasSuccessfullyStored) {
        // 现在该块位于内存或磁盘存储中,请将其告知主机
        info.size = size
        if (tellMaster && info.tellMaster) {
          reportBlockStatus(blockId, putBlockStatus)
        }
        addUpdatedBlockStatusToTaskMetrics(blockId, putBlockStatus)
        logDebug(s"Put block $blockId locally took ${Utils.getUsedTimeNs(startTimeNs)}")
        //如果RDD持久化选择的副本数大于1
        if (level.replication > 1) {
          val remoteStartTimeNs = System.nanoTime()
          val bytesToReplicate = doGetLocalBytes(blockId, info)
          val remoteClassTag = if (!serializerManager.canUseKryo(classTag)) {
            scala.reflect.classTag[Any]
          } else {
            classTag
          }
          try {
            replicate(blockId, bytesToReplicate, level, remoteClassTag)
          } finally {
            bytesToReplicate.dispose()
          }
          logDebug(s"Put block $blockId remotely took ${Utils.getUsedTimeNs(remoteStartTimeNs)}")
        }
      }
      assert(blockWasSuccessfullyStored == iteratorFromFailedMemoryStorePut.isEmpty)
      iteratorFromFailedMemoryStorePut
    }
  }



}

3、checkpoint

RDD

//将此RDD标记为检查点。它将被保存到使用`SparkContext#setCheckpointDir`设置的检查点目录中的一个文件中,并且对其父RDD的所有引用都将被删除。必须在此RDD上执行任何作业之前调用此函数。强烈建议将此RDD持久化在内存中,否则将其保存在文件上将需要重新计算。
def checkpoint(): Unit = RDDCheckpointData.synchronized {
  // 注意:由于下游的复杂性,我们在这里使用全局锁来确保子RDD分区指向正确的父分区。今后我们应该重新考虑这个问题。
  if (context.checkpointDir.isEmpty) {
    //SparkContext中尚未设置检查点目录 , 因此使用之前需要用sc先设置检查点目录
    throw new SparkException("Checkpoint directory has not been set in the SparkContext")
  } else if (checkpointData.isEmpty) {
    checkpointData = Some(new ReliableRDDCheckpointData(this))
  }
}

ReliableRDDCheckpointData

private[spark] class ReliableRDDCheckpointData[T: ClassTag](@transient private val rdd: RDD[T])
  extends RDDCheckpointData[T](rdd) with Logging {

  //........省略..........

  //将此RDD具体化,并将其内容写入可靠的DFS。在该RDD上调用的第一个action 完成后立即调用。
  protected override def doCheckpoint(): CheckpointRDD[T] = {
    //将RDD写入检查点文件,并返回表示RDD的ReliableCheckpointRDD
    val newRDD = ReliableCheckpointRDD.writeRDDToCheckpointDirectory(rdd, cpDir)

    // 如果引用超出范围,可以选择清理检查点文件
    if (rdd.conf.getBoolean("spark.cleaner.referenceTracking.cleanCheckpoints", false)) {
      rdd.context.cleaner.foreach { cleaner =>
        cleaner.registerRDDCheckpointDataForCleanup(newRDD, rdd.id)
      }
    }

    logInfo(s"Done checkpointing RDD ${rdd.id} to $cpDir, new parent is RDD ${newRDD.id}")
    newRDD
  }


}

ReliableCheckpointRDD

private[spark] object ReliableCheckpointRDD extends Logging {

  def writeRDDToCheckpointDirectory[T: ClassTag](
      originalRDD: RDD[T],
      checkpointDir: String,
      blockSize: Int = -1): ReliableCheckpointRDD[T] = {
    val checkpointStartTimeNs = System.nanoTime()

    val sc = originalRDD.sparkContext

    // 为检查点创建输出路径
    val checkpointDirPath = new Path(checkpointDir)
    val fs = checkpointDirPath.getFileSystem(sc.hadoopConfiguration)
    if (!fs.mkdirs(checkpointDirPath)) {
      throw new SparkException(s"Failed to create checkpoint path $checkpointDirPath")
    }

    // 保存到文件,并将其重新加载为RDD
    val broadcastedConf = sc.broadcast(
      new SerializableConfiguration(sc.hadoopConfiguration))
    // 这很昂贵,因为它不必要地再次计算RDD ,因此一般都会在检查点前调用持久化
    sc.runJob(originalRDD,
      writePartitionToCheckpointFile[T](checkpointDirPath.toString, broadcastedConf) _)

    if (originalRDD.partitioner.nonEmpty) {
      //将分区器写入给定的RDD检查点目录。这是在尽最大努力的基础上完成的;写入分区器时的任何异常都会被捕获、记录并忽略。
      writePartitionerToCheckpointDir(sc, originalRDD.partitioner.get, checkpointDirPath)
    }

    val checkpointDurationMs =
      TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - checkpointStartTimeNs)
    logInfo(s"Checkpointing took $checkpointDurationMs ms.")

    //从以前写入可靠存储的检查点文件中读取的RDD
    val newRDD = new ReliableCheckpointRDD[T](
      sc, checkpointDirPath.toString, originalRDD.partitioner)
    if (newRDD.partitions.length != originalRDD.partitions.length) {
      throw new SparkException(
        s"Checkpoint RDD $newRDD(${newRDD.partitions.length}) has different " +
          s"number of partitions from original RDD $originalRDD(${originalRDD.partitions.length})")
    }
    newRDD
  }

}

什么时候对RDD进行checkpoint

当该RDD所属的Job执行后再对该RDD进行checkpoint

class SparkContext(config: SparkConf) extends Logging {

  def runJob[T, U: ClassTag](
      rdd: RDD[T],
      func: (TaskContext, Iterator[T]) => U,
      partitions: Seq[Int],
      resultHandler: (Int, U) => Unit): Unit = {

    //执行任务
    dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)

    //递归调用父RDD查看是否要进行checkpoint
    rdd.doCheckpoint()
  }

}


abstract class RDD[T: ClassTag](... ) extends Serializable with Logging {
 
 //递归函数
 private[spark] def doCheckpoint(): Unit = {
    RDDOperationScope.withScope(sc, "checkpoint", allowNesting = false, ignoreParent = true) {
      if (!doCheckpointCalled) {
        doCheckpointCalled = true
        if (checkpointData.isDefined) {
          if (checkpointAllMarkedAncestors) {
            // 我们可以收集所有需要检查点的RDD,然后并行检查它们。首先检查父母,因为我们的血统在检查自己后会被截断
            dependencies.foreach(_.rdd.doCheckpoint())
          }
          checkpointData.get.checkpoint()
        } else {
          dependencies.foreach(_.rdd.doCheckpoint())
        }
      }
    }
  }
}

总结

1、RDD执行checkpoint方法,对该RDD进行标记

2、RDD所在的Job执行

3、执行完会用这个Job最后的RDD递归向父寻找,找到所有的被标记需要checkpoint的RDD,再次调用runJob启动任务,将这个RDD进行checkpoint

所以我们在对RDD进行checkpoint前一般会对其persist

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2149310.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解锁数字转型新纪元:Vatee万腾平台,您的智能加速与策略智库

在数字经济时代的大潮中,企业的数字化转型已不再是选择题,而是必答题。面对这一挑战,Vatee万腾平台以其卓越的技术实力和前瞻性的战略视野,成为了众多企业加速数字化转型、实现智能化升级的得力助手和智囊团。 加速转型&#xff…

人工智能时代:程序员如何在变革中保持核心竞争力?

随着人工智能生成内容(AIGC)领域的快速发展,大语言模型如ChatGPT、Midjourney、Claude等层出不穷,AI辅助编程工具迅速普及,程序员的工作方式正在经历翻天覆地的变革。面对这一趋势,有人担心AI可能取代部分编…

嵌入式处理器详解

文章目录 一、CPU、MPU、MCU、SoC、Application Processors的概念1.CPU (Central Processing Unit)2.MPU (Micro Processor Unit)3.MCU (Micro Controller Unit)4.SoC(System on Chip)5.Application Processors 二、哈弗架构与冯诺伊曼架构三、XIP概念四、嵌入式系统硬件组成五…

【架构设计】多级缓存:应用案例与问题解决策略

【架构设计】多级缓存:应用案例与问题解决策略 多级缓存系统的工作原理及其在提升应用性能方面的关键作用。通过对比本地缓存与分布式缓存的特点 | 原创作者/编辑:凯哥Java | 分类:架构设计系列教程 多级缓存…

模拟电路分析基础知识总结笔记(电子电路分析与设计前置知识)

必备条件 电子电路的直流分析电子电路的正弦稳态分析RC电路的瞬态分析戴维南定理和诺顿定理拉普拉斯变换(看不懂,根本看不懂) 电子电路的直流分析 欧姆定律 ​ 在恒定温度下,电压与电流成正比,电压与电阻成正比&am…

Java-数据结构-优先级队列(堆)-(二) (゚▽゚*)

文本目录: ❄️一、PriorityQueue的常用接口: ➷ 1、PriorityQueue的特性: ➷ 2、使用PriorityQueue的注意: ➷ 3、PriorityQueue的构造: ☞ 1、无参数的构造方法: ☞ 2、有参数的构造方法: …

DCMM介绍

目录 一、介绍 二、核心摘要 三、体系大纲 四、能力评估 1、过程与活动 2、等级判定依据 3、访谈对象 一、介绍 通过阅读本书,您将洞悉国际数据框架体系,并掌握国家对于数据管理能力的权威评估标准与等级划分。本书详尽阐述了数据管理领域的八大核心能力域,以及这八大…

Flask、Werkzeug 和 WSGI 间的关系

一.Flask、Werkzeug和 WSGI 关系 1.WSGI Web 架构 Flask 是一个基于 Werkzeug 和 Jinja2 模板引擎的轻量级 Web 框架。Werkzeug 是 Flask 的底层 WSGI 工具包,它提供了 WSGI 服务器、请求和响应对象、路由等基础功能,Flask 在此基础上构建了更高级的 W…

HelpLook VS GitBook,在线文档管理工具对比

在线文档管理工具在当今时代非常重要。随着数字化时代的到来,人们越来越依赖于电子文档来存储、共享和管理信息。无论是与团队合作还是与客户分享,人们都可以轻松地共享文档链接或通过设置权限来控制访问。在线文档管理工具的出现大大提高了工作效率和协…

性能调优

性能调优 应用程序在运行过程中经常会出现性能问题,比较常见的性能问题现象是: 通过top命令查看CPU占用率高,接近100甚至多核CPU下超过100都是有可能的。请求单个服务处理时间特别长,多服务使用skywalking等监控系统来判断是哪一…

电子束光刻过程中的场拼接精度

以下内容如有错误,请不吝指教,感谢! 1、EBL为什么会出现场拼接误差,如何解决? ChatGPT 说: 在电子束光刻(EBL)过程中,SOI(硅绝缘体)芯片上出现*…

计算机毕业论文题目:设计与实现一个校园通知信息系统

设计与实现一个校园通知信息系统是一个涉及多个方面的复杂项目,它旨在提高信息传递的效率和准确性,确保学生、教师以及学校管理人员能够及时获取到重要的通知信息。以下是关于如何设计并实现这样一个系统的详细说明: 1. 需求分析 用户…

【高中数学/不等式/数学归纳法/等比数列】证明伯努利不等式(1+h)^n>1+nh的三种方式

【伯努利不等式】 (1h)^n>1nh (h>0,n为大于1的自然数) 【数学归纳法证法】 证明: n2时,(1h)^212hh^2>12h 不等式成立 n3时,(1h)^313h3h^2h^3>13h 不等式成立 假设nk时,有(1h)^k>…

机房三大网络拓扑图,太实用了

号主:老杨丨11年资深网络工程师,更多网工提升干货,请关注公众号:网络工程师俱乐部 下午好,我的网工朋友。 通常来说,机房的三大网络拓扑图指的是星型拓扑、总线型拓扑和环形拓扑。 在实际的机房网络设计中…

vue项目加载cdn失败解决方法

注释index.html文件中 找到vue.config.js文件注释、

MySQL_图形管理工具简介、下载及安装(超详细)

课 程 推 荐我 的 个 人 主 页:👉👉 失心疯的个人主页 👈👈入 门 教 程 推 荐 :👉👉 Python零基础入门教程合集 👈👈虚 拟 环 境 搭 建 :&#x1…

【VUE3.0】动手做一套像素风的前端UI组件库---先导篇

系列文章目录 【VUE3.0】动手做一套像素风的前端UI组件库—Button 目录 系列文章目录引言准备素材字体鼠标手势图 创建vue3项目构建项目1. 根据命令行提示选择如下:2. 进入项目根目录下载依赖并启动。3. 设置项目src路径别名,方便后期应用路径。4. 将素…

Debian 12上安装google chrome

当前系统:Debian 12.7 昨天在Debian 12.7上安装Google Chrome时,可能由于网络原因,导入公钥始终失败。 导致无法正常使用命令#apt install google-chrome-stable来安装google chrome; 解决办法: Step1.下载当前google chrome稳…

C++性能优化-代码角度

减少跳转/分支语句和函数调用 原因 分支语句:当 CPU 执行到分支语句时,将会进行分支预测(对大部分PC)。如果分支预测错误,就会清空已经预取和执行的部分指令,重新从正确的分支开始取指和执行,…

某东-h5st参数逆向分析

目标:商品搜索翻页接口 直接搜索h5st就可以搜到,所有可疑位置都打上断点,然后翻页,最终断点位置: window.PSign.sign(colorParamSign)是异步代码,colorParamSign是传入的参数,执行后把包含h5st…