简介:聚焦Hadoop与Spark大数据处理及案例分析的Word文档,面向互联网行业的数据工程师、运维人员、架构师,以及希望快速建立大数据技术认知的初学者。文档以docx单文件封装,整个资源包仅1个文件,大小约15KB,下载和使用都很轻量。内容围绕Hadoop生态展开,涉及HDFS、MapReduce、HBase、Hive、Mahout、Storm、Spark、OpenTSDB等核心组件,并结合电信网络优化、移动话单查询、金融票据平台、银行记录系统、交通违章、医疗大数据、互联网公共数据等多行业典型项目,系统展示从分布式存储、批处理、机器学习到实时分析的常见实现思路,以及关键落地要点与典型项目的分析路径。已有502人学习下载,阅读后可快速理清Hadoop与Spark的技术脉络,积累真实场景中的项目式认知,也能为后续深入学习、培训准备或工程选型提供有价值的背景参考。
1. hadoop、spark大数据处理与案例分析.docx:一份培训班通知里挖出的完整学习路线
看到“hadoop、spark大数据处理与案例分析.docx”这个标题,多数人以为是某份课程PPT打包,其实这是2016年一期Hadoop/Spark高级工程师实战培训班的课程通知。里面没有一行代码,却把案例清单列得很实在:某省移动详单实时查询、银联大数据票据详单平台、某大型银行记录系统、某省交通违章分析、互联网公共数据大云DAAS。把这些名字串起来看,恰好是Hadoop生态最典型的落地路径:HDFS做存储、MapReduce/Spark做计算、HBase扛实时查询、openTSDB接时序数据。这份文档最大的价值不是给你现成代码,而是用真实业务场景圈定了该搭什么环境、练什么技能、准备哪些hadoop面试题。它适合三类人:想从零搭一套Hadoop+Spark环境的新手、做课程设计需要业务切入点的学生、以及准备跳槽大数据岗位的工程师。
2. 把 Hadoop 先跑起来:伪分布式搭建与 HDFS/MapReduce 实操
2.1 伪分布式还是集群:第一台机器怎么选
先说结论:第一次学 Hadoop,直接单机装伪分布式,不要一上来就搞三台虚拟机集群。伪分布式会把 NameNode、DataNode、ResourceManager、NodeManager 全部跑在同一台机器上,进程都在,行为接近真实集群,但省掉了 SSH 免密、时间同步、节点间通信这些和业务无关的坑。等你用 WordCount 把 MapReduce 的作业流程跑通,再拆成多节点集群,半天就能完成。
机器配置上,我一般建议至少 4G 内存、2 核 CPU、20G 可用磁盘。内存是关键,伪分布式里每个 Java 进程默认吃掉 1G 堆内存,机器只有 2G 的话,DataNode 和 NodeManager 很容易被系统 OOM Killer 杀掉。jps看进程的时候进程还在,跑作业的时候突然报连接拒绝,十有八九就是这个原因。
提示:云主机选 2C4G 入门规格就行;本地虚拟机记得把“处理器核心数”调成 2 以上,单核跑 Hadoop 会慢到让你怀疑人生。
搭建步骤我习惯写成命令逐条执行,避免配置文件改错又回头找。先确认 Java 环境,Hadoop 2.x 需要 JDK 1.8:
# 检查 JDK,1.8 是 Hadoop 2.x 和 Spark 2.x 的基准版本 java -version # 解压 hadoop 到 /opt,并创建软链方便切换版本 tar -zxf hadoop-2.10.2.tar.gz -C /opt ln -s /opt/hadoop-2.10.2 /opt/hadoop这里用 2.10.2 而不是 3.x,是因为很多培训班案例和网上开源项目基于 2.x 写的,配置文件路径和参数名兼容性最好。接着把 Hadoop 的 bin、sbin 加进环境变量,否则每次执行hdfs、yarn命令都要写全路径:
# 写入 /etc/profile 或 ~/.bashrc,source 之后全局生效 export HADOOP_HOME=/opt/hadoop export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin接下来改四个核心配置:core-site.xml指定 NameNode 地址,hdfs-site.xml改副本数,mapred-site.xml指定 YARN 调度,yarn-site.xml控制资源分配。伪分布式和单机模式(Local模式)的本质区别就在这里,Local 模式直接用本地文件系统,不启动 HDFS 相关进程,你学不到分布式文件系统的任何机制。
<!-- core-site.xml:告诉客户端 NameNode 在哪 --> <property> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property> <!-- hdfs-site.xml:伪分布式只有一台机器,副本必须设为 1 --> <property> <name>dfs.replication</name> <value>1</value> </property> <!-- mapred-site.xml:让 MapReduce 跑在 YARN 上 --> <property> <name>mapreduce.framework.name</name> <value>yarn</value> </property>fs.defaultFS是全局入口,填错会导致Unable to open file这类连接错误;dfs.replication在伪分布式里必须写 1,否则 DataNode 只有一份副本却要等三份副本都写完才报告成功,作业会一直卡在等待副本确认的状态,日志里没有报错,就是进度不动。改完配置后做两件事:免密登录和格式化 NameNode。
# 生成 SSH 密钥,Hadoop 进程间通信要用 ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys # 格式化 NameNode,注意这会清空 HDFS 上的所有数据 hdfs namenode -format # 一键启动,之后用 jps 看进程 start-dfs.sh && start-yarn.sh jps格式化这个动作,新手最容易误解:它不是格式化磁盘,而是初始化 NameNode 的元数据目录。第一次跑会打印successfully formatted,如果报错,先检查JAVA_HOME有没有写进hadoop-env.sh。jps之后应该看到NameNode、DataNode、ResourceManager、NodeManager四个进程,少任何一个都先别往后走,把对应日志打开看。
2.2 HDFS 基础操作与 MapReduce 用例验证
环境起来后,先用一个真实文件跑通“上传-计算-下载”全链路。我拿某次课程里电信网络优化的脱敏数据(CSV 格式)做演示,上传到 HDFS,然后跑官方 WordCount 验证:
# 在 HDFS 上建目录,-p 表示递归创建 hdfs dfs -mkdir -p /user/input # 把本地文件放进 HDFS,block 默认 128M hdfs dfs -put ./call_log.csv /user/input/ # 提交 WordCount,输出目录不能提前存在 hadoop jar /opt/hadoop/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.10.2.jar wordcount /user/input /user/output # 查看结果 hdfs dfs -cat /user/output/part-r-00000 | head -20这里有个容易踩的规则:MapReduce 的输出目录必须不存在,否则直接报FileAlreadyExistsException。跑完正常会看到 YARN 打印作业完成的时间和 Map/Reduce 进度,看到completed successfully就是全链路通了。WordCount 虽然简单,但它的执行过程包含了 MapReduce 最核心的机制:Map 阶段把每一行拆成单词,Shuffle 阶段把相同单词归到同一个 Reduce 任务,Reduce 阶段做累加。你在 Spark 里遇到的绝大多数性能问题,根源都能回溯到这个机制。
2.3 配置参数说明与内存控制
伪分布式调参,重点在yarn-site.xml。默认的 NodeManager 会认为整台机器都是它的,虚拟机里容易把内存打满,我一般会显式限制:
<property> <name>yarn.nodemanager.resource.memory-mb</name> <value>3072</value> </property> <property> <name>yarn.scheduler.minimum-allocation-mb</name> <value>512</value> </property> <property> <name>yarn.scheduler.maximum-allocation-mb</name> <value>2048</value> </property>memory-mb是 NodeManager 能支配的总内存,minimum-allocation-mb是每个 Container 申请的最小单位,maximum-allocation-mb是上限。把最大分配压到 2G,是为了防止某个 Map 任务把内存吃光导致其他进程被杀。还有一个隐藏坑:YARN 默认会检查虚拟内存,yarn.nodemanager.vmem-pmem-ratio默认是 2.1,意思是一个 Container 申请 1G 物理内存,虚拟内存要小于 2.1G 才不会被杀。虚拟机里虚拟内存很容易超限,作业跑着跑着被Container killed,就是这个检查在背后捣鬼。
注意:改完
yarn-site.xml必须重启 yarn,只重启 ResourceManager 不够,NodeManager 也要跟着重启,否则配置不会生效。重启后去localhost:8088看集群的可用内存和核数,确认和预期一致再提交作业。
内存参数设完,Hadoop 这条线就通了。但课程案例里那些“实时查询”“详单系统”,单靠 MapReduce 做交互式分析太笨重,Spark 才是处理这些场景的主力,下一章把 Spark 集群搭起来对接 HDFS。
3. Spark 集群搭建与核心 API 实战:从 RDD 到 DataFrame
3.1 Spark 安装与 Standalone 集群配置
Spark 的部署模式常见有三种:Local(本地调试)、Standalone(自带集群)、YARN(跑在 Hadoop 上)。我推荐的学习路径是先在 Local 模式下把 API 写熟,再切到 Standalone 体会真正的分布式调度,最后接 YARN 和 HDFS 打通。直接上 YARN 的话,排错时既要看 Spark 日志又要看 YARN 日志,两个调度系统叠在一起,新手很容易被绕晕。
安装这一步,最省心的组合是 Spark 2.4.x + Hadoop 2.10.x。都是 2.x 家族,依赖冲突最少,网上找对应版本的已编译 jar 包也方便。需要说明的是,Spark 2.4 之后 RDD 和 DataFrame 的 API 已经稳定成型,你用这份资源里的案例练习,语法到现在依然通用。解压后改spark-env.sh:
# 从模板复制,模板在 conf 目录下 cp /opt/spark/conf/spark-env.sh.template /opt/spark/conf/spark-env.sh # 指定 JDK 和 Master 节点地址 echo 'export JAVA_HOME=/opt/jdk1.8.0_202' >> /opt/spark/conf/spark-env.sh echo 'export SPARK_MASTER_HOST=localhost' >> /opt/spark/conf/spark-env.sh echo 'export SPARK_WORKER_CORES=2' >> /opt/spark/conf/spark-env.sh echo 'export SPARK_WORKER_MEMORY=2g' >> /opt/spark/conf/spark-env.shSPARK_WORKER_MEMORY是每个 worker 总共能给 Executor 用的内存池,不是单个 Executor 的内存。很多人以为写 2g 就是每个任务 2G,其实任务多了每个 Executor 只会拿到一部分,这个误解在提交作业时最容易暴露。启动后访问localhost:8080,能看到 worker 注册信息和可用内存,这是判断集群是否正常的第一个入口。
# 启动 master 和 worker /opt/spark/sbin/start-master.sh /opt/spark/sbin/start-worker.sh spark://localhost:7077 # 用 jps 确认进程,Master 和 Worker 都在才算成功 jps | grep -E 'Master|Worker'3.2 读取 HDFS 数据:Spark 处理 JSON 的两种方式
文档案例里反复出现“详单”“票据”这类半结构化数据,线上最常见的就是 JSON。Spark 读 JSON 有两种常见姿势,差别很大。第一种是spark.read.json,Spark 自动推断 schema,适合字段规整的日志;第二种是textFile逐行读再手动解析,适合嵌套深、字段多的票据数据。
# 方式一:自动推断 schema,字段规整时首选 df = spark.read.json("hdfs://localhost:9000/user/input/bill.json") df.printSchema() # 方式二:逐行读,手动指定 schema,性能更可控 from pyspark.sql.types import StructType, StructField, StringType, LongType from pyspark.sql import Row import json schema = StructType([ StructField("phone", StringType(), True), StructField("duration", LongType(), True), StructField("call_time", StringType(), True) ]) raw = spark.sparkContext.textFile("hdfs://localhost:9000/user/input/bill.json") parsed = raw.map(lambda line: json.loads(line)) \ .map(lambda d: Row(phone=d["phone"], duration=int(d["duration"]), call_time=d["call_time"])) df2 = spark.createDataFrame(parsed, schema)这两种方式的取舍很实际:read.json会自动扫描一遍数据来推断类型,数据量大时这个扫描是有成本的;手动指定 schema 省掉了推断过程,但要求你对字段结构完全清楚。我做银联票据类项目时习惯用第二种,因为票据字段多且乱,自动推断经常把空字符串推断成string而把数字字段推断成long,后面 join 时类型不匹配才回来排错,不如一开始就锁死。
3.3 内存参数与 Executor 配置
Spark 作业跑得慢,先查内存参数和并行度。提交作业时最常用的一套:
spark-submit \ --master spark://localhost:7077 \ --executor-memory 1g \ --executor-cores 1 \ --driver-memory 1g \ --conf spark.sql.shuffle.partitions=4 \ --conf spark.serializer=org.apache.spark.serializer.KryoSerializer \ /opt/jobs/bill_analysis.pyexecutor-memory定的是堆内内存,spark.sql.shuffle.partitions控制 shuffle 后分成几个分区。伪分布式上总共才 2G worker 内存,这个值设成 4 是合理的;真实集群里,这个值一般设为executor 核数 × 2~3,分区太多会导致小文件泛滥,分区太少会让单 task 处理数据量过大。Kryo 序列化器对内存的节省非常明显,尤其是处理字符串多的业务数据,默认的 Java 序列化能把内存占用放大两三倍。
提示:
driver-memory容易被忽略。跑collect()或take()这类把数据拉到 driver 的操作时,driver 才是瓶颈,堆内存不够直接 OOM,而且日志里只会看到一句不痛不痒的ExecutorLostFailure,真实原因在 driver 日志里。
从 2.1 到 3.3,Hadoop 和 Spark 单机链路已经全通。下一章把文档里那几个真实案例拆开,看业务模型怎么落到技术选型上。
4. 从文档案例拆业务模型:详单查询与票据平台的落地思路
4.1 某省移动详单实时查询:HBase 行键设计 + Spark 预聚合
文档里写的“某省移动详单实时查询系统”,是典型的“海量写入、按键查询”场景。每天几亿条通话详单,用户查自己的记录,条件是手机号加时间范围。这类需求就算用 Spark 也扛不住全表扫描,正确做法是 HBase 存明细,Spark 做预聚合。培训班文档里把 HBase 列在 Hadoop 生态里,但真正做项目时,HBase 和 Spark 通常是配合关系而不是替代关系。
HBase 的行键设计直接决定查询性能,我一般用三段拼接:手机号反转、时间戳、随机散列。手机号反转让相邻号码落在同一 Region,时间戳放在中间保证同一号码的数据按时间连续,随机散列用于分散写入热点。伪分布式上建表:
# 建表:cf 列族存详单原始字段 hbase shell <<'EOF' create 'call_detail', 'cf' # 预分区 16 个,避免所有写请求压在一个 Region 上 create 'call_detail', 'cf', {NUMREGIONS => 16, SPLITALGO => 'HexStringSplit'} EOFSpark 这边先把前一天的数据按用户维度聚合,写入 HBase 的cf:summary,实时查询走 HBase 点查,从而把“秒级响应”和“海量数据”两个矛盾需求拆开。预分区数量要按 RegionServer 个数和单 Region 数据量来定,16 个只是伪分布式实验值,真实环境通常按“预估数据总量 ÷ 单 Region 建议量(10G 左右)”来算。
4.2 某大型银行记录系统:HDFS 冷热分层 + Spark 批处理
银行日志记录系统的特点是:数据量大、不能丢、查询频率低。这类场景最适合 HDFS 存储加 Spark 定时批处理。原始日志落到 HDFS 的/raw目录,Spark 作业每天凌晨跑一次,清洗后写入/etl目录:
# 从 HDFS 读原始日志,过滤掉格式错误行,按日期分区写回 raw = spark.read.text("hdfs://localhost:9000/raw/2024-11-01/*.log") cleaned = raw.filter(raw.value.isNotNull()) \ .withColumn("row_ts", ...) # 解析时间字段 cleaned.write.partitionBy("dt").parquet("hdfs://localhost:9000/etl/bank_logs")这里最关键的设计是数据分层:/raw保留原始格式当后悔药,一旦清洗逻辑出错,可以直接从 raw 重跑;/etl是清洗后的 Parquet 列式存储,查询只碰/etl。实际项目中我还会加一层汇总层/agg,把频繁查询的维度预先算好,比如按天、按网点聚合的交易总额,避免每次报表都全量扫描几 TB 数据。冷热分层还涉及生命周期管理,HDFS 上可以通过dfs.namenode.handler.count和存储策略做到,但初学阶段先把握住“分层”这个思想就行。
| 数据层 | 存储格式 | 用途 | 保留周期 |
|---|---|---|---|
| /raw | 原始文本 | 数据回溯、重算 | 30 天 |
| /etl | Parquet | 明细查询 | 180 天 |
| /agg | Parquet | 报表、聚合分析 | 1 年 |
4.3 案例文档怎么落地:先把需求翻译成数据流
这份 docx 里所有的项目名,翻译过来都是同一个套路:数据进 HDFS → Spark 清洗/聚合 → 结果写 HBase/MySQL/Parquet。难点不在技术,在于把“查询详单”这种业务描述拆成“行键是什么、聚合粒度是什么、冷热数据怎么分”。以“某省交通违章系统”为例,原始数据是卡口日志,需求是统计路段拥堵指数,翻译成数据流就是:日志进 HDFS → Spark 按车牌和时间窗聚合 → 结果写 MySQL 供前端报表查询。
我建议拿到这类案例先画三行:数据从哪来、存哪、谁消费。画完再动手写代码,比直接写高效得多。文档里提到的“互联网公共数据大云(DAAS)”,本质上就是把公共数据打包成 API 服务,底层依然是“批处理 + 列式存储 + 服务层”三件套。案例的价值从来不是名字本身,而是你能不能从中抽出可复用的架构模式。
5. Hadoop 与 Spark 实战避坑:五个翻车现场与排查手册
5.1 NameNode 启动失败:格式化与元数据冲突
现象:执行start-dfs.sh后,jps看不到 NameNode,日志报Incompatible namespaceIDs。
原因:NameNode 格式化会生成新的 namespaceID,DataNode 记录的是旧的。常见于你格式化之后没有清空dfs.datanode.data.dir指向的目录,DataNode 带着旧 ID 启动,和新的 NameNode 对不上。
解决:停掉集群,清空 HDFS 数据目录,再重新格式化。
stop-dfs.sh rm -rf /tmp/hadoop-hdfs/dfs/data # 注意这是默认目录,实际以你的配置为准 hdfs namenode -format start-dfs.sh这个坑提醒你:不要在数据节点上随便格式化。伪分布式无所谓,真实集群格式化等于删库,操作前必须确认dfs.name.dir和dfs.data.dir指向的是临时目录还是正式数据盘。
5.2 Spark 读 HDFS 数据倾斜:分区数与 key 分布
现象:Spark 作业某个 stage 大部分 task 秒完,就一个 task 跑十几分钟,整个 job 卡在 99%。
原因:数据按 key 分区时,某个 key 的数据量特别大,那个分区的 task 就成了短板。做详单分析时,热门号码一天几万条,普通号码几十条,不处理就倾斜。
解决:先通过 Spark UI 确认是哪个 stage 卡住,再看 key 分布,然后做两阶段聚合或加盐。
# 加盐解决单 key 倾斜:先按随机前缀分组聚合,再去掉前缀聚合 from pyspark.sql.functions import col, concat, lit, rand df = df.withColumn("salt", (rand() * 10).cast("int")) \ .withColumn("salted_key", concat(col("key"), lit("_"), col("salt"))) df_grouped = df.groupBy("salted_key").count() # 清掉盐再做最终聚合 result = df_grouped.withColumn("key", ...).groupBy("key").sum()加盐的本质是把一个大 key 拆成多个小 key,代价是增加了一次 shuffle。数据倾斜这个问题,面试题里考的是原理,实战里考的是你能不能从 Spark UI 的 stage 详情里一眼定位到慢 task,而不是盲目调大并行度。
5.3 Executor 内存溢出:off-heap 与 Kryo 序列化
现象:作业频繁失败,日志出现java.lang.OutOfMemoryError: Java heap space或GC overhead limit exceeded。
原因:默认 Java 序列化会把对象膨胀 2~3 倍,字符串多的数据尤其明显;另外spark.memory.offHeap.enabled没开,所有内存都挤在堆内,GC 压力大。
解决:换 Kryo 序列化、适当开 off-heap、调大spark.executor.memoryOverhead。
spark-submit \ --conf spark.serializer=org.apache.spark.serializer.KryoSerializer \ --conf spark.memory.offHeap.enabled=true \ --conf spark.memory.offHeap.size=512m \ --conf spark.executor.memoryOverhead=512m \ job.pymemoryOverhead是给堆外用的,默认只有 executor 内存的 10%,处理大批量 Parquet 时很容易不够。要注意 Kryo 需要注册类,不注册会导致ClassNotFoundException,这也是为什么项目中通常用一个专门的类把所有 case class 集中注册。
5.4 集群时间不同步:HBase 与 Spark 都吃这个亏
现象:HBase RegionServer 启动正常,但 Spark 写 HBase 时偶发ServerNotRunningException,日志还会报org.apache.hadoop.hbase.DoNotRetryIOException。
原因:多节点集群时间差超过一定阈值,HBase 认为节点故障。单机伪分布式不会遇到,一上真集群就翻车,而且这个问题会在凌晨和整点最容易触发,因为那时各节点自己的时钟偏移最大。
解决:所有节点配置 NTP 同步。
# 校准时间,date 命令看到的时间差超过 30 秒就要处理 date # Debian/Ubuntu 用 chrony,CentOS 7 用 ntpd,任选其一 systemctl restart chronyd && systemctl enable chronyd这个坑不显眼,但一旦触发,日志会指向 HBase 的 RPC 层,查半天查不到根因。建议在搭建集群的第一天就把时间同步加上,别等作业失败再补。
5.5 jar 包冲突:hadoop-common 与 Spark 自带依赖
现象:NoSuchMethodError或ClassNotFoundException,且只在提交作业时出现,本地 IDE 跑得好好的。
原因:Spark 自带的 Hadoop 客户端版本和你集群里的 Hadoop 版本不一致,classpath 里出现了两个版本的hadoop-common。这个问题在 Spark 2.x 时代最常见,尤其当你用 CDH 发行版时。
解决:用--jars显式指定集群版本的 jar,并把 Spark 里冲突的依赖排除掉。
spark-submit \ --jars /opt/hadoop/share/hadoop/common/hadoop-common-2.10.2.jar \ --conf spark.executor.extraClassPath=/opt/hadoop/share/hadoop/common/* \ job.py这类问题最有效的排查方式是看提交时打印的 classpath,把两个版本号的 jar 对比一下就清楚了。不要盲目往--jars里塞依赖,每加一个 jar 都可能是新的冲突源。
6. 用文档案例反向验证:把 Spark 作业跑成一个可监控的流程
环境搭完、代码写完,怎么证明这套东西真能用于案例项目?我的习惯是用文档里最不起眼的“违章系统”做全链路验证,因为违章数据天然带有时间、地点、车牌三个维度,最适合验证过滤和聚合逻辑的完整性。
先写一个带数据质量校验的 Spark 作业,把结果写入 HDFS 的指定分区:
# 数据质量校验:输入行数、有效行数、输出行数都打印出来,方便后续检查 total = df.count() valid = df.filter(df.case_no.isNotNull()).count() result.write.mode("overwrite").parquet("hdfs://localhost:9000/etl/violation/dt=2024-11-01") print(f"total={total}, valid={valid}, written={result.count()}")这还不够,验证必须有人盯着,跑完没人看等于没跑。我用一个 shell 脚本把“提交作业-检查退出码-校验输出”串起来,配合 crontab 每天凌晨执行:
#!/bin/bash # 跑数并检查退出码,失败会写一个 marker 文件方便告警 spark-submit --master spark://localhost:7077 /opt/jobs/violation_daily.py if [ $? -ne 0 ]; then echo "job failed at $(date)" >> /opt/jobs/logs/fail_marker.log exit 1 fi # 校验输出目录是否存在且非空 hdfs dfs -test -e /user/etl/violation/dt=2024-11-01 && hdfs dfs -du /user/etl/violation/dt=2024-11-01加了这层验证之后,作业跑没跑、结果对不对就不再靠感觉了。从那以后,我每次搭完 Hadoop 和 Spark 环境,都强制走一遍“上传-计算-校验-看 Spark UI”的全链路,哪个环节是玄学一眼就能看出来。这个习惯帮我挡掉过不少问题,比如某次作业显示成功但输出目录是空的,原因就是count()触发的 job 和真正写数据的 job 用了不同的 DataFrame 缓存策略,如果没有输出非空的校验,这条数据就会默默缺失。
希望这份由培训班通知拆出来的实操路线,能帮你把环境搭起来、把文档里的案例落到可以跑的数据流上。
本文还有配套的精品资源,点击获取