第九篇

首页新闻博问专区闪存云上钜惠                          我的博客我的园子账号设置退出登录注册登录skaarl 博客园首页新随笔联系订阅管理随笔 - 77  文章 - 0  评论 - 29 Spark架构与原理这一篇就够了 一、基本介绍是什么?快速,通用,可扩展的分布式计算引擎。弹性分布式数据集RDDRDD(Resilient Distributed Dataset)弹性分布式数据集,是Spark中最基本的数据(逻辑)抽象,它代表一个不可变、可分区、里面的元素可并行计算的集合。 RDD具有数据流模型的特点:自动容错、位置感知性调度和可伸缩性。RDD允许用户在执行多个查询时显式地将工作集缓存在内存中,后续的查询能够重用工作集,这极大地提升了查询速度。基本概念基本流程二、Hadoop和Spark的区别Spark 是类Hadoop MapReduce的通用并行框架, 专门用于大数据量下的迭代式计算.是为了跟 Hadoop 配合而开发出来的,不是为了取代 Hadoop, Spark 运算比 Hadoop 的 MapReduce 框架快的原因是因为 Hadoop 在一次 MapReduce 运算之后,会将数据的运算结果从内存写入到磁盘中,第二次 Mapredue 运算时在从磁盘中读取数据,所以其瓶颈在2次运算间的多余 IO 消耗. Spark 则是将数据一直缓存在内存中,直到计算得到最后的结果,再将结果写入到磁盘,所以多次运算的情况下, Spark 是比较快的. 其优化了迭代式工作负载。Hadoop的局限Spark的改进抽象层次低,编码难以上手。通过使用RDD的统一抽象,实现数据处理逻辑的代码非常简洁。只提供Map和Reduce两个操作,欠缺表达力。通过RDD提供了许多转换和动作,实现了很多基本操作,如sort、join等。一个job只有map和reduce两个阶段,复杂的程序需要大量的job来完成。且job之间的依赖关系需要应用开发者自行管理。一个job可以包含多个RDD的转换操作,只需要在调度时生成多个stage。一个stage中也可以包含多个map操作,只需要map操作所使用的RDD分区保持不变。处理逻辑隐藏在代码细节中,缺少整体逻辑视图。RDD的转换支持流式API,提供处理逻辑的整体视图。对迭代式数据的处理性能比较差,reduce与下一步map的中间结果只能存放在HDFS的文件系统中。通过内存缓存数据,可大大提高迭代式计算的性能,内存不足时可溢写到磁盘上。reduce task需要等所有的map task全部执行完毕才能开始执行。分区相同的转换可以在一个task中以流水线的形式执行。只有分区不同的转换需要shuffle操作。时延高,只适合批数据处理,对交互式数据处理和实时数据处理支持不够。将流拆成小的batch,提供discretized stream处理流数据三、RDD操作两种类型: transformation和actionTransformation主要做的是就是将一个已有的RDD生成另外一个RDD。Transformation具有lazy特性(延迟加载)。
Transformation算子的代码不会真正被执行。只有当我们的程序里面遇到一个action算子的时候,代码才会真正的被执行。这种设计让Spark更加有效率地运行。
常用的Transformation:动作说明示例map(func)返回一个新的RDD,该RDD由每一个输入元素经过func函数转换后组成 (每一个输入元素只能被映射为一个)var rdd = sc.parallelize(List(“hello world”, “hello spark”, “hello hdfs”))
var rdd2 = rdd.map(x => x + “_1”)
rdd2.foreach(println)filter(func)返回一个新的RDD,该RDD由经过func函数计算后返回值为true的输入元素组成var rdd3 = rdd2.filter(x => x.contains(“world”))
rdd3.foreach(println)flatMap(func)类似于map,但是每一个输入元素可以被映射为0或多个输出元素(所以func应该返回一个序列,而不是单一元素)var rdd4 = rdd2.flatMap(x => x.split(" “))
rdd4.foreach(println)sample(withReplacement, fraction, seed)根据fraction指定的比例对数据进行采样,可以选择是否使用随机数进行替换,seed用于指定随机数生成器种子 groupByKey([numTasks])在一个(K,V)的RDD上调用,返回一个(K, Iterator[V])的RDDvar rdd5 = rdd4.map(x => (x, 1))
var rdd6 = rdd5.groupByKey()
rdd6.foreach(println)sample(withReplacement, fraction, seed)根据fraction指定的比例对数据进行采样,可以选择是否使用随机数进行替换,seed用于指定随机数生成器种子var rdd = sc.parallelize(1 to 10)rdd.sample(false,0.4).collect()
rdd.sample(false,0.4, 9).collect()combineByKey合并相同的key的值 rdd1.combineByKey(x => x, (a: Int, b: Int) => a + b, (m: Int, n: Int) => m + n)jake 80.0
jake 90.0
jake 85.0
mike 86.0
mike 90
求分数的平均值单Value类型算子补充:1. mapPartitions: 将待处理的数据以分区为单位发送到计算节点进行处理;
2. mapPartintions: 将待处理的数据以分区为单位发送到计算节点进行处理 ;
3. glom: 将同一个分区的数据直接转换为相同类型的内存数组进行处理,分区不变 ;
4. groupBy: 将数据根据指定的规则进行分组, 分区默认不变,但是数据会被打乱重新组合 ;
5. distinct: 将数据集中重复的数据去重 ;
6. coalesce: 根据数据量缩减分区,用于大数据集过滤后,提高小数据集的执行效率
当 spark 程序中,存在过多的小任务的时候,可以通过 coalesce 方法,收缩合并分区,减少
分区的个数,减小任务调度成本 ;
7. repartition: 该操作内部其实执行的是 coalesce 操作,参数 shuffle 的默认值为 true。
8. sortBy: 该操作用于排序数据。在排序之前,可以将数据通过 f 函数进行处理,之后按照 f 函数处理
的结果进行排序,默认为升序排列 双Value类型算子补充:1. intersection: 对源 RDD 和参数 RDD 求交集后返回一个新的 RDD
2. union: 对源 RDD 和参数 RDD 求并集后返回一个新的 RDD
3. subtract: 以一个 RDD 元素为主, 去除两个 RDD 中重复元素,将其他元素保留下来
Action触发代码的运行,我们一段spark代码里面至少需要有一个action操作。
常用的Action:动作含义示例reduce(func)通过func函数聚集RDD中的所有元素,可以实现,RDD中元素的累加,计数和其他类型的聚集操作var rdd = sc.parallelize(1 to 10)
rdd.reduce((x, y) => x+y)reduceByKey(func)按key进行reduce,让key合并wordcount示例:
var rdd = sc.parallelize(List(“hello world”, “hello spark”, “hello hdfs”))
rdd.flatMap(x => x.split(” “)).map(x => (x,1)).reduceByKey((x,y) => x+y).collect()collect()在驱动程序中,以数组的形式返回数据集的所有元素 count()返回RDD的元素个数 first()返回RDD的第一个元素(类似于take(1)) take(n)返回一个由数据集的前n个元素组成的数组 saveAsTextFile(path)将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本rdd.saveAsTextFile(”/user/jd_ad/ads_platform/outergd/0124/demo2.csv")foreach(func)在数据集的每一个元素上,运行函数func进行更新。 takeSample抽样返回一个dateset中的num个元素var rdd = sc.parallelize(1 to 10)
rdd.takeSample(false,10)四、Block与RDD生成过程
输入可能以多个文件的形式存储在HDFS上,每个File都包含了很多块,称为Block。
当Spark读取这些文件作为输入时,会根据具体数据格式对应的InputFormat进行解析,一般是将若干个Block合并成一个输入分片,称为InputSplit,注意InputSplit不能跨越文件。
随后将为这些输入分片生成具体的Task。InputSplit与Task是一一对应的关系。
随后这些具体的Task每个都会被分配到集群上的某个节点的某个Executor去执行。每个节点可以起一个或多个Executor。每个Executor由若干core组成,每个Executor的每个core一次只能执行一个Task。每个Task执行的结果就是生成了目标RDD的一个partiton。注意: 这里的core是虚拟的core而不是机器的物理CPU核,可以理解为就是Executor的一个工作线程。
而 Task被执行的并发度 = Executor数目 * 每个Executor核数。
至于partition的数目:对于数据读入阶段,例如sc.textFile,输入文件被划分为多少InputSplit就会需要多少初始Task。在Map阶段partition数目保持不变。在Reduce阶段,RDD的聚合会触发shuffle操作,聚合后的RDD的partition数目跟具体操作有关,例如repartition操作会聚合成指定分区数,还有一些算子是可配置的。五、依赖关系与Stage划分RDD之间有一系列的依赖关系,依赖关系又分为窄依赖和宽依赖。简单的区分发,可以看一下父RDD中的数据是否进入不同的子RDD,如果只进入到一个子RDD则是窄依赖,否则就是宽依赖。如下图

窄依赖( narrow dependencies )子RDD 的每个分区依赖于常数个父分区(即与数据规模无关)输入输出一对一的算子,且结果RDD 的分区结构不变,主要是map 、flatMap输入输出一对一,但结果RDD 的分区结构发生了变化,如union 、coalesce从输入中选择部分元素的算子,如filter 、distinct 、subtract 、sample宽依赖( wide dependencies )子RDD 的每个分区依赖于所有父RDD 分区对单个RDD 基于key 进行重组和reduce ,如groupByKey 、reduceByKey ;对两个RDD 基于key 进行join 和重组,如joinSpark任务会根据RDD之间的依赖关系,形成一个DAG有向无环图,DAG会提交给DAGScheduler,DAGScheduler会把DAG划分相互依赖的多个stage,划分stage的依据就是RDD之间的宽窄依赖。遇到宽依赖就划分stage,每个stage包含一个或多个task任务。然后将这些task以taskSet的形式提交给TaskScheduler运行。 stage是由一组并行的task组成。切割规则:从后往前,遇到宽依赖就切割stage,遇到窄依赖就将这个RDD加入该stage中。 如下图
六、Spark流程调度流程(粗粒度图解)1、DriverProgram即用户提交的程序定义并创建了SparkContext的实例,SparkContext会根据RDD对象构建DAG图,然后将作业(job)提交(runJob)给DAGScheduler。2、DAGScheduler对作业的DAG图进行切分成不同的stage[stage是根据shuffle为单位进行划分],每个stage都是任务的集合(taskset)并以taskset为单位提交(submitTasks)给TaskScheduler。3、TaskScheduler通过TaskSetManager管理任务(task)并通过集群中的资源管理器(Cluster Manager)[standalone模式下是Master,yarn模式下是ResourceManager]把任务(task)发给集群中的Worker的Executor, 期间如果某个任务(task)失败, TaskScheduler会重试,TaskScheduler发现某个任务(task)一直未运行完成,有可能在不同机器启动一个推测执行任务(与原任务一样),哪个任务(task)先运行完就用哪个任务(task)的结果。无论任务(task)运行成功或者失败,TaskScheduler都会向DAGScheduler汇报当前状态,如果某个stage运行失败,TaskScheduler会通知DAGScheduler可能会重新提交任务。4、Worker接收到的是任务(task),执行任务(task)的是进程中的线程,一个进程中可以有多个线程工作进而可以处理多个数据分片,执行任务(task)、读取或存储数据。执行流程(细粒度图解)1、通过SparkSubmit提交job后,Client就开始构建 spark context,即 application 的运行环境(使用本地的Client类的main函数来创建spark context并初始化它)2、yarn client提交任务,Driver在客户端本地运行;yarn cluster提交任务的时候,Driver是运行在集群上3、SparkContext连接到ClusterManager(Master),向资源管理器注册并申请运行Executor的资源(内核和内存)4、Master根据SparkContext提出的申请,根据worker的心跳报告,来决定到底在那个worker上启动executor5、Worker节点收到请求后会启动executor6、executor向SparkContext注册,这样driver就知道哪些executor运行该应用7、SparkContext将Application代码发送给executor(如果是standalone模式就是StandaloneExecutorBackend)8、同时SparkContext解析Application代码,构建DAG图,提交给DAGScheduler进行分解成stage,stage被发送到TaskScheduler。9、TaskScheduler负责将Task分配到相应的worker上,最后提交给executor执行10、executor会建立Executor线程池,开始执行Task,并向SparkContext汇报,直到所有的task执行完成11、所有Task完成后,SparkContext向Master注销七、spark在yarn上的两种运行模式(yarn-client和yarn-cluster)1、Yarn-Client1.Spark Yarn Client向YARN的ResourceManager申请启动Application Master。同时在SparkContent初始化中将创建DAGScheduler和TASKScheduler等,由于我们选择的是Yarn-Client模式,程序会选择YarnClientClusterScheduler和YarnClientSchedulerBackend;2.ResourceManager收到请求后,在集群中选择一个NodeManager,为该应用程序分配第一个Container,要求它在这个Container中启动应用程序的ApplicationMaster,与YARN-Cluster区别的是在该ApplicationMaster不运行SparkContext,只与SparkContext进行联系进行资源的分派;3.Client中的SparkContext初始化完毕后,与ApplicationMaster建立通讯,向ResourceManager注册,根据任务信息向ResourceManager申请资源(Container);4.一旦ApplicationMaster申请到资源(也就是Container)后,便与对应的NodeManager通信,要求它在获得的Container中启动启动CoarseGrainedExecutorBackend,CoarseGrainedExecutorBackend启动后会向Client中的SparkContext注册并申请Task;5.Client中的SparkContext分配Task给CoarseGrainedExecutorBackend执行,CoarseGrainedExecutorBackend运行Task并向Driver汇报运行的状态和进度,以让Client随时掌握各个任务的运行状态,从而可以在任务失败时重新启动任务;6.应用程序运行完成后,Client的SparkContext向ResourceManager申请注销并关闭自己。2、Yarn-Cluster(企业中主要使用)1.Spark Yarn Client向YARN中提交应用程序,包括ApplicationMaster程序、启动ApplicationMaster的命令、需要在Executor中运行的程序等;2.ResourceManager收到请求后,在集群中选择一个NodeManager,为该应用程序分配第一个Container,要求它在这个Container中启动应用程序的ApplicationMaster,其中ApplicationMaster进行SparkContext等的初始化;3.ApplicationMaster向ResourceManager注册,这样用户可以直接通过ResourceManage查看应用程序的运行状态,然后它将采用轮询的方式通过RPC协议为各个任务申请资源,并监控它们的运行状态直到运行结束;4.一旦ApplicationMaster申请到资源(也就是Container)后,便与对应的NodeManager通信,要求它在获得的Container中启动启动CoarseGrainedExecutorBackend,CoarseGrainedExecutorBackend启动后会向ApplicationMaster中的SparkContext注册并申请Task。这一点和Standalone模式一样,只不过SparkContext在Spark Application中初始化时,使用CoarseGrainedSchedulerBackend配合YarnClusterScheduler进行任务的调度,其中YarnClusterScheduler只是对TaskSchedulerImpl的一个简单包装,增加了对Executor的等待逻辑等;5.ApplicationMaster中的SparkContext分配Task给CoarseGrainedExecutorBackend执行,CoarseGrainedExecutorBackend运行Task并向ApplicationMaster汇报运行的状态和进度,以让ApplicationMaster随时掌握各个任务的运行状态,从而可以在任务失败时重新启动任务;6.应用程序运行完成后,ApplicationMaster向ResourceManager申请注销并关闭自己。3、两种模式区别理解YARN-Client和YARN-Cluster深层次的区别之前先清楚一个概念:Application Master。在YARN中,每个Application实例都有一个ApplicationMaster进程,它是Application启动的第一个容器。它负责和ResourceManager打交道并请求资源,获取资源之后告诉NodeManager为其启动Container。从深层次的含义讲YARN-Cluster和YARN-Client模式的区别其实就是ApplicationMaster进程的区别
YARN-Cluster模式下,Driver运行在AM(Application Master)中,它负责向YARN申请资源,并监督作业的运行状况。当用户提交了作业之后,就可以关掉Client,作业会继续在YARN上运行,因而YARN-Cluster模式不适合运行交互类型的作业
YARN-Client模式下,Application Master仅仅向YARN请求Executor,Client会和请求的Container通信来调度他们工作,也就是说Client不能离开
下图是几种模式下的比较:
八、MapReduce的Shuffle和Spark中的Shuffle区别和联系Spark在DAG调度阶段会将一个Job划分为多个Stage,上游Stage做map工作,下游Stage做reduce工作,其本质上还是MapReduce计算框架。Shuffle是连接map和reduce之间的桥梁,它将map的输出对应到reduce输入中,这期间涉及到序列化反序列化、跨节点网络IO以及磁盘读写IO等,所以说Shuffle是整个应用程序运行过程中非常昂贵的一个阶段,理解Spark Shuffle原理有助于优化Spark应用程序。注:
1.什么是大数据处理的Shuffle?
无论是Hadoop还是Spark,都要实现Shuffle。Shuffle描述数据从map tasks的输出到reduce tasks输入的这段过程。
2.为什么需要进行Shuffle呢?
map tasks的output向着reduce tasks的输入input映射的时候,并非节点一一对应的,在节点A上做map任务的输出结果,可能要分散跑到reduce节点A、B、C、D ,就好像shuffle的字面意思“洗牌”一样,这些map的输出数据要打散然后根据新的路由算法(比如对key进行某种hash算法),发送到不同的reduce节点上去。MapReduce的Shuffle
MapReduce 是 sort-based,进入 combine() 和 reduce() 的 records 必须先partition、key对中间结果进行排序合并。这样的好处在于 combine/reduce() 可以处理大规模的数据,因为其输入数据可以通过外排得到(mapper 对每段数据先做排序,reducer 的 shuffle 对排好序的每段数据做归并)。Spark中的Shuffle前面已经提到,在DAG调度的过程中,Stage阶段的划分是根据是否有shuffle过程,也就是存在ShuffleDependency宽依赖的时候,需要进行shuffle,这时候会将作业job划分成多个Stage;Spark的Shuffle实现大致如下图所示,在DAG阶段以shuffle为界,划分stage,上游stage做map task,每个map task将计算结果数据分成多份,每一份对应到下游stage的每个partition中,并将其临时写到磁盘,该过程叫做shuffle write;下游stage做reduce task,每个reduce task通过网络拉取上游stage中所有map task的指定分区结果数据,该过程叫做shuffle read,最后完成reduce的业务逻辑。下图是spark shuffle实现的一个版本演进。
基于Hash的Shuffle实现基于Sort的Shuffle实现(现在采用的机制)九、spark中的持久化(cache()、persist()、checkpoint())RDD持久化级别持久化级别含义解释MEMORY_ONLY使用未序列化的Java对象格式,将数据保存在内存中。如果内存不够存放所有的数据,则数据可能就不会进行持久化。那么下次对这个RDD执行算子操作时,那些没有被持久化的数据,需要从源头处重新计算一遍。这是默认的持久化策略,使用cache()方法时,实际就是使用的这种持久化策略。DISK_ONLY使用未序列化的Java对象格式,将数据全部写入磁盘文件中。MEMORY_ONLY_SER基本含义同MEMORY_ONLY。唯一的区别是,会将RDD中的数据进行序列化,RDD的每个partition会被序列化成一个字节数组。这种方式更加节省内存,从而可以避免持久化的数据占用过多内存导致频繁GC。MEMORY_AND_DISK使用未序列化的Java对象格式,优先尝试将数据保存在内存中。如果内存不够存放所有的数据,会将数据写入磁盘文件中,下次对这个RDD执行算子时,持久化在磁盘文件中的数据会被读取出来使用。MEMORY_AND_DISK_SER基本含义同MEMORY_AND_DISK。唯一的区别是,会将RDD中的数据进行序列化,RDD的每个partition会被序列化成一个字节数组。这种方式更加节省内存,从而可以避免持久化的数据占用过多内存导致频繁GC。  cache和persist都是用于将一个RDD进行缓存,这样在之后使用的过程中就不需要重新计算,可以大大节省程序运行时间。cache和persist的区别:cache只有一个默认的缓存级别MEMORY_ONLY,而persist可以根据情况设置其它的缓存级别。checkpoint接口是将RDD持久化到HDFS中,与persist的区别是checkpoint会切断此RDD之前的依赖关系,而persist会保留依赖关系。checkpoint的两大作用:
一是spark程序长期驻留,过长的依赖会占用很多的系统资源,定期checkpoint可以有效的节省资源;
二是维护过长的依赖关系可能会出现问题,一旦spark程序运行失败,RDD的容错成本会很高。
(注:checkpoint执行前要先进行cache,避免两次计算。)十、监控界面
标签: spark架构原理好文要顶 关注我 收藏该文 skaarl
关注 - 34
粉丝 - 22 +加关注 0 0

« 上一篇: MySQL查询这一篇就够了
» 下一篇: 朝花夕拾,小知识点积累,不定期更新 posted @ 2020-11-11 20:01  skaarl  阅读(78)  评论(0)  编辑  收藏

刷新评论刷新页面返回顶部

发表评论 【福利】注册AWS账号,立享12个月免费套餐 编辑预览 7693b08a-a8f6-49f3-f45a-08d88556cc23 Markdown 帮助自动补全 不改了退出 订阅评论 [Ctrl+Enter快捷键提交]

首页 新闻 博问 专区 闪存 班级 【推荐】News: 大型组态、工控、仿真、CADGIS 50万行VC++源码免费下载
【推荐】博客园 & 陌上花开HIMMR 给单身的程序员小哥哥助力脱单啦~
【推荐】博客园x示说网联合策划,AI实战系列公开课第二期
【推荐】了不起的开发者,挡不住的华为,园子里的品牌专区
【推荐】未知数的距离,毫秒间的传递,声网与你实时互动
【推荐】 阿里云双十一返场继续,云服务器0.73折起
【推荐】年薪100w+的技术人,都做对了什么?
相关博文:
· sparkthriftserver
· sparksql
· sparkrdd
· spark调优篇-sparkonyarnwebUI
· Tunningspark
» 更多推荐…最新 IT 新闻:
· 网络“奔现师”为啥火了?
· 中芯国际第三季度收入10.8亿美元,净利润2.56亿美元
· 为什么平台采购成受贿案高发地?
· 换“芯”的苹果,换了新的打法
· 虎牙第三季度净利润3730万美元 同比增长105.3%
» 更多新闻… 历史上的今天:
2020-11-11 Spark架构与原理这一篇就够了

公告 昵称: skaarl
园龄: 2年6个月
粉丝: 22
关注: 34 +加关注

< 2020年11月> 日一二三四五六 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 1 2 3 4 5 6 7 8 9 10 11 12

搜索    常用链接 我的随笔我的评论我的参与最新评论我的标签我的标签Django(4) python(2) flask(2) 类视图(2) 密码修改后git push失败(1) 模板复用(1) 前后端不分离(1) 前后端分离(1) 上下文管理器(1) 事务(1) 更多 随笔档案 2020年11月(4) 2020年10月(1) 2020年9月(1) 2020年8月(1) 2020年6月(1) 2020年5月(1) 2019年11月(2) 2019年10月(3) 2019年9月(2) 2019年8月(1) 2019年2月(1) 2019年1月(5) 2018年12月(4) 2018年9月(11) 2018年8月(23) 2018年7月(16) 最新评论1. Re:MySQL查询这一篇就够了大佬,您好~我是51testing软件测试网编辑,您这篇文章写的很好,想要转载到我们平台上让更多的人看到,我们会标书出处和作者,希望您能同意,非常感谢!–玛丽奥❀2. Re:MySQL查询这一篇就够了通俗易懂~~–灵雨飘零3. Re:撤销git add添加的文件不错.学到了.–gwdsss4. Re:前后端分离与前后端不分离的区别@ karrylemonvue只是javascript的渐进式框架,你java还是写你的后端,只不过往前端返回页面所要渲染的数据就ok了,你学完vue就懂了,javascript本身就是一门脚本语言,…–百事可爱5. Re:前后端分离与前后端不分离的区别最关键的是提升了开发效率和整体性能。向前端偏移,适当转移了部分开发任务到前端;前后端工程解耦,各自开发,开发过程不再互相依赖。前后端服务器负载更加均衡高效,后端服务器不再承担很多静态资源的负载,转由更…–千寻南国阅读排行榜 1. 前后端分离与前后端不分离的区别(48860) 2. Python获取路径下所有文件名(21990) 3. 电脑同时安装python2和python3, 如何实现切换使用(19519) 4. python查看文件的编码格式(14092) 5. Linux设置环境变量的方法(12914) 评论排行榜 1. 前后端分离与前后端不分离的区别(10) 2. Django中通过定时任务触发页面静态化的方式(3) 3. MySQL查询这一篇就够了(2) 4. 实现斐波拉契数列的四种方式python代码(2) 5. DRF中五大扩展类及视图集的介绍(2) 推荐排行榜 1. 前后端分离与前后端不分离的区别(19) 2. Django中类视图使用装饰器的方式(3) 3. MySQL查询这一篇就够了(2) 4. python装饰器中functools.wraps的作用详解(2) 5. 撤销git add添加的文件(1)

Copyright © 2020 skaarl
Powered by .NET 5.0.0 on Kubernetes

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值