Spark SQL CBO(基于成本的优化器)参数深度解析

Spark SQL CBO(基于成本的优化器)参数深度解析

目录

  1. [CBO 核心概念与原理](#CBO 核心概念与原理)
  2. [CBO 总开关详解](#CBO 总开关详解)
  3. [JOIN 重排序机制深度剖析](#JOIN 重排序机制深度剖析)
  4. 星型模式过滤优化
  5. 聚合下推优化
  6. [CBO 与 AQE 的协同工作](#CBO 与 AQE 的协同工作)
  7. 实际场景调优案例

CBO 核心概念与原理

什么是 CBO?

CBO(Cost-Based Optimizer) 是 Spark SQL 的基于成本的查询优化器,它在编译时(查询计划生成阶段)根据表统计信息计算不同执行计划的成本,选择最优计划。

CBO vs 规则优化器(Rule-Based Optimizer)

复制代码
规则优化器(RBO):
  → 基于启发式规则
  → 不考虑数据大小和分布
  → 可能选择次优计划

基于成本的优化器(CBO):
  → 基于统计信息计算成本
  → 考虑数据大小、分布、选择性
  → 选择成本最低的计划

CBO 工作流程

复制代码
阶段1: 收集统计信息
  ↓
  ANALYZE TABLE table_name COMPUTE STATISTICS;
  → 收集:行数、列统计、直方图等
  ↓
阶段2: 生成候选执行计划
  ↓
  → 不同的 JOIN 顺序
  → 不同的 JOIN 策略
  → 不同的过滤顺序
  ↓
阶段3: 计算每个计划的成本
  ↓
  成本 = CPU 成本 + I/O 成本 + 网络成本
  ↓
阶段4: 选择成本最低的计划
  ↓
  执行最优计划

统计信息的重要性

CBO 的优化效果完全依赖统计信息的准确性:

复制代码
缺少统计信息:
  → CBO 无法计算准确成本
  → 可能回退到规则优化
  → 优化效果大打折扣

准确的统计信息:
  → CBO 能准确评估成本
  → 选择最优执行计划
  → 性能提升 2-10 倍

CBO 总开关详解

1. spark.sql.cbo.enabled = true

作用范围

启用 CBO 后,以下优化会被激活:

复制代码
1. JOIN 顺序优化(Join Reordering)
2. JOIN 策略选择(Join Strategy Selection)
3. 过滤下推优化(Predicate Pushdown)
4. 列裁剪优化(Column Pruning)
5. 常量折叠(Constant Folding)
6. 投影下推(Projection Pushdown)
成本模型

CBO 使用多维度成本模型:

复制代码
总成本 = CPU 成本 + I/O 成本 + 网络成本 + 内存成本

CPU 成本:
  - 数据扫描开销
  - 过滤操作开销
  - JOIN 操作开销
  - 聚合操作开销

I/O 成本:
  - 磁盘读取开销
  - 磁盘写入开销
  - 缓存命中率

网络成本:
  - Shuffle 数据传输
  - Broadcast 数据传输

内存成本:
  - Hash 表构建
  - 排序缓冲区
  - 缓存占用
性能影响示例

场景:3 表 JOIN 查询

sql 复制代码
SELECT *
FROM large_table l
JOIN medium_table m ON l.id = m.id
JOIN small_table s ON m.key = s.key
WHERE l.date = '2024-01-01'

关闭 CBO:

复制代码
执行计划(固定顺序):
  large_table → JOIN medium_table → JOIN small_table
  中间结果:100GB
  执行时间:45 分钟

开启 CBO:

复制代码
执行计划(优化顺序):
  small_table → JOIN medium_table → JOIN large_table
  中间结果:5GB(先过滤,再 JOIN)
  执行时间:8 分钟

性能提升:5.6 倍!
统计信息收集

必须收集的统计信息:

sql 复制代码
-- 表级统计信息
ANALYZE TABLE table_name COMPUTE STATISTICS;

-- 列级统计信息(包含直方图)
ANALYZE TABLE table_name COMPUTE STATISTICS FOR COLUMNS col1, col2, ...;

-- 分区统计信息
ANALYZE TABLE table_name PARTITION(partition_col='value') COMPUTE STATISTICS;

统计信息内容:

复制代码
表级统计:
  - 总行数(rowCount)
  - 总大小(sizeInBytes)

列级统计:
  - 不同值数量(distinctCount)
  - 空值数量(nullCount)
  - 最小值/最大值(min/max)
  - 平均长度(avgLen)
  - 直方图(histogram)- 数据分布
常见问题

问题1: CBO 启用但效果不明显

复制代码
可能原因:
1. 统计信息未收集或过期
   → 解决:定期收集统计信息
   
2. 统计信息不准确
   → 解决:使用 FOR COLUMNS 收集详细统计
   
3. 数据变化频繁
   → 解决:增量更新统计信息

问题2: CBO 选择次优计划

复制代码
可能原因:
1. 成本模型不准确
   → 解决:调整成本权重参数
   
2. 统计信息缺失
   → 解决:补充缺失的统计信息
   
3. 数据倾斜未考虑
   → 解决:结合 AQE 使用

JOIN 重排序机制深度剖析

2. spark.sql.cbo.joinReorder.enabled = true

JOIN 顺序的重要性

为什么 JOIN 顺序很重要?

复制代码
场景:3 表 JOIN
  Table A: 100GB
  Table B: 10GB
  Table C: 1GB

顺序1: A JOIN B JOIN C
  中间结果1: A JOIN B = 50GB
  中间结果2: 50GB JOIN C = 5GB
  总 Shuffle: 100GB + 50GB = 150GB

顺序2: C JOIN B JOIN A
  中间结果1: C JOIN B = 0.5GB
  中间结果2: 0.5GB JOIN A = 0.3GB
  总 Shuffle: 1GB + 0.5GB = 1.5GB

性能差异:100 倍!
重排序算法

动态规划算法(DP - Dynamic Programming)

复制代码
步骤1: 构建 JOIN 图
  A ──┐
      ├── JOIN
  B ──┘
      │
  C ──┘

步骤2: 枚举所有可能的 JOIN 顺序
  顺序1: (A JOIN B) JOIN C
  顺序2: (A JOIN C) JOIN B
  顺序3: (B JOIN C) JOIN A
  ...

步骤3: 计算每个顺序的成本
  成本 = 扫描成本 + JOIN 成本 + Shuffle 成本

步骤4: 选择成本最低的顺序
算法复杂度
复制代码
表数量: n
可能的 JOIN 顺序数: (2n-2)! / (n-1)!

n=3: 2 种顺序
n=4: 12 种顺序
n=5: 120 种顺序
n=6: 1680 种顺序
n=7: 30240 种顺序
...

复杂度: O(n!)
→ 表数量多时,需要限制搜索空间

3. spark.sql.cbo.joinReorder.dp.threshold = 12

阈值的作用

为什么需要阈值?

复制代码
无阈值限制:
  10 个表 JOIN
  → 可能的顺序数:3628800 种
  → 计算时间:数小时
  → 优化时间 > 执行时间(得不偿失)

有阈值限制(threshold = 12):
  表数 ≤ 12:完整搜索
  表数 > 12:使用启发式算法
  → 平衡优化效果和优化时间
搜索策略

表数 ≤ threshold(完整搜索):

复制代码
使用动态规划算法:
  → 枚举所有可能的 JOIN 顺序
  → 计算每个顺序的成本
  → 选择最优顺序

优点:找到全局最优解
缺点:计算开销大

表数 > threshold(启发式搜索):

复制代码
使用贪心算法:
  1. 从最小的表开始
  2. 每次选择成本最低的 JOIN
  3. 逐步构建 JOIN 树

优点:计算速度快
缺点:可能不是全局最优
阈值选择建议
表数量 推荐阈值 原因
≤ 5 5-8 表少,可以完整搜索
6-10 10-12 平衡优化效果和时间
11-15 12-15 需要更多搜索空间
> 15 15-20 大量表,需要限制搜索

注意:

  • 阈值越大,优化时间越长
  • 阈值太小,可能错过最优计划
  • 需要根据实际场景调整
实际示例

场景:8 表 JOIN

复制代码
threshold = 12(当前配置):
  → 8 < 12,使用完整搜索
  → 枚举所有 5040 种顺序
  → 计算时间:~5 秒
  → 找到最优顺序
  → 执行时间:10 分钟

threshold = 5:
  → 8 > 5,使用启发式搜索
  → 计算时间:~0.1 秒
  → 可能不是最优顺序
  → 执行时间:12 分钟(慢 20%)

星型模式过滤优化

4. spark.sql.cbo.joinReorder.dp.star.filter = false

星型模式(Star Schema)

什么是星型模式?

复制代码
星型模式结构:
  - 1 个事实表(Fact Table):大表,包含度量数据
  - N 个维度表(Dimension Tables):小表,包含描述信息

示例:
  事实表:sales (100GB)
    - sale_id, product_id, customer_id, date_id, amount
  
  维度表:
    - products (10MB)
    - customers (50MB)
    - dates (1MB)
星型模式 JOIN 特点
复制代码
典型查询:
  SELECT s.amount, p.name, c.name, d.date
  FROM sales s
  JOIN products p ON s.product_id = p.id
  JOIN customers c ON s.customer_id = c.id
  JOIN dates d ON s.date_id = d.id
  WHERE p.category = 'Electronics'
    AND c.region = 'North'
    AND d.year = 2024

特点:
  - 多个维度表过滤条件
  - 过滤后维度表很小
  - 先过滤维度表,再 JOIN 事实表,效果最好
star.filter 参数的作用

star.filter = true(启用):

复制代码
优化策略:
  1. 识别星型模式结构
  2. 先对维度表应用过滤条件
  3. 过滤后的维度表 JOIN 事实表
  4. 大幅减少中间结果

执行顺序:
  products (filtered) → JOIN sales
  customers (filtered) → JOIN sales
  dates (filtered) → JOIN sales
  → 中间结果:从 100GB 降到 1GB

star.filter = false(当前配置):

复制代码
使用标准 JOIN 重排序:
  → 基于成本选择 JOIN 顺序
  → 不特殊处理星型模式
  → 可能选择次优顺序

可能的问题:
  - 先 JOIN 事实表,再过滤维度表
  - 中间结果很大
  - 性能较差
为什么当前设置为 false?

可能的原因:

复制代码
1. 不是星型模式场景
   → 如果查询不是星型模式,启用反而可能影响优化

2. 维度表过滤效果不明显
   → 如果维度表过滤后仍然很大,优化效果有限

3. 兼容性问题
   → 某些场景下可能有问题
何时启用?

建议启用的场景:

复制代码
✓ 明确的星型模式数据仓库
✓ 维度表有强过滤条件
✓ 维度表过滤后显著变小(< 10%)
✓ 事实表很大(> 10GB)

不建议启用的场景:

复制代码
✗ 不是星型模式
✗ 维度表过滤效果不明显
✗ 查询模式复杂多变
性能对比示例

场景:星型模式查询

sql 复制代码
SELECT s.amount, p.name, c.name
FROM sales s (100GB)
JOIN products p (10MB) ON s.product_id = p.id
JOIN customers c (50MB) ON s.customer_id = c.id
WHERE p.category = 'Electronics'  -- 过滤后 1MB
  AND c.region = 'North'          -- 过滤后 5MB

star.filter = false

复制代码
执行顺序:sales JOIN products JOIN customers
  中间结果1: 100GB JOIN 10MB = 50GB
  中间结果2: 50GB JOIN 50MB = 25GB
  然后应用过滤条件
  最终结果: 1GB
  执行时间: 30 分钟

star.filter = true

复制代码
执行顺序:
  1. 过滤 products: 10MB → 1MB
  2. 过滤 customers: 50MB → 5MB
  3. 1MB JOIN sales = 0.5GB
  4. 0.5GB JOIN 5MB = 0.3GB
  最终结果: 1GB
  执行时间: 5 分钟

性能提升:6 倍!

聚合下推优化

5. spark.sql.distinctAggPushdown.enabled = true

聚合下推(Aggregation Pushdown)

什么是聚合下推?

复制代码
将聚合操作尽可能下推到数据源或更早的执行阶段
→ 减少需要处理的数据量
→ 提高查询性能
distinctAggPushdown 的作用

启用后,优化 DISTINCT 聚合操作:

sql 复制代码
-- 原始查询
SELECT 
  customer_id,
  COUNT(DISTINCT product_id) as unique_products,
  SUM(amount) as total_amount
FROM sales
GROUP BY customer_id

不启用(标准执行):

复制代码
执行流程:
1. 扫描 sales 表(100GB)
2. Shuffle 按 customer_id 分组
3. 在每个分区计算 COUNT(DISTINCT product_id)
4. 再次 Shuffle 合并结果
5. 计算 SUM(amount)

问题:
  - COUNT(DISTINCT) 需要两阶段聚合
  - 中间数据量大
  - Shuffle 开销大

启用(优化执行):

复制代码
执行流程:
1. 扫描 sales 表时,在 Map 端预聚合
2. 对每个 customer_id,先计算部分 DISTINCT
3. Shuffle 数据量减少(去重后)
4. Reduce 端合并 DISTINCT 结果
5. 计算 SUM(amount)

优势:
  - 减少 Shuffle 数据量(30-70%)
  - 减少网络传输
  - 提高执行效率
优化原理

两阶段 DISTINCT 聚合:

复制代码
阶段1: Map 端预聚合(Partial Aggregation)
  输入:原始数据
  输出:部分去重结果
  例如:customer_id=1, product_id={1,2,3} (Map 端)
  
阶段2: Reduce 端最终聚合(Final Aggregation)
  输入:部分去重结果
  输出:完全去重结果
  例如:customer_id=1, product_id={1,2,3,4,5} (合并所有 Map 结果)
性能影响

场景:1 亿条销售记录,1000 万客户,100 万产品

复制代码
不启用:
  Shuffle 数据量:100GB(原始数据)
  执行时间:45 分钟

启用:
  Shuffle 数据量:30GB(预聚合后)
  执行时间:15 分钟

性能提升:3 倍!
适用场景

适合的场景:

复制代码
✓ COUNT(DISTINCT column)
✓ 多个 DISTINCT 聚合
✓ 数据重复度高(去重效果好)
✓ 大表聚合查询

不适合的场景:

复制代码
✗ 数据重复度低(去重效果不明显)
✗ 小表查询(优化开销 > 收益)
✗ 简单的 SUM/AVG(不需要 DISTINCT)
与其他优化的协同

结合 GROUP BY 下推:

sql 复制代码
SELECT 
  c.region,
  COUNT(DISTINCT s.product_id) as unique_products
FROM sales s
JOIN customers c ON s.customer_id = c.id
WHERE c.region IN ('North', 'South')
GROUP BY c.region
复制代码
优化流程:
1. 过滤 customers(WHERE 下推)
2. JOIN sales(过滤后的 customers)
3. DISTINCT 聚合下推(Map 端预聚合)
4. GROUP BY 聚合

多重优化叠加效果:
  - 过滤减少数据量:90%
  - DISTINCT 下推减少 Shuffle:60%
  - 总体性能提升:10-20 倍

CBO 与 AQE 的协同工作

编译时优化 vs 运行时优化

复制代码
CBO(编译时):
  → 基于统计信息
  → 生成初始执行计划
  → 选择 JOIN 顺序、策略等

AQE(运行时):
  → 基于实际数据
  → 动态调整执行计划
  → 处理数据倾斜、分区合并等

协同工作:
  CBO 生成好的初始计划
    ↓
  AQE 根据实际情况调整
    ↓
  最优执行效果

协同示例

场景:多表 JOIN + 数据倾斜

sql 复制代码
SELECT *
FROM large_table l
JOIN medium_table m ON l.id = m.id
JOIN small_table s ON m.key = s.key
WHERE l.date = '2024-01-01'

阶段1: CBO 优化(编译时)

复制代码
CBO 分析:
  - large_table: 100GB
  - medium_table: 10GB
  - small_table: 1GB
  - 过滤条件:l.date = '2024-01-01'(选择性 10%)

CBO 决策:
  1. 先过滤 large_table:100GB → 10GB
  2. JOIN 顺序:small_table → medium_table → large_table
  3. JOIN 策略:Broadcast Join(small_table < 10MB)
  
生成执行计划

阶段2: AQE 优化(运行时)

复制代码
AQE 监控:
  - 发现 medium_table JOIN large_table 时数据倾斜
  - 某些分区 200MB,某些 10MB

AQE 调整:
  1. 拆分倾斜分区
  2. 动态调整分区大小
  3. 优化 Shuffle 策略

最终执行

性能对比:

复制代码
仅 CBO:30 分钟(被倾斜拖累)
仅 AQE:35 分钟(初始计划不佳)
CBO + AQE:8 分钟(最优组合)

协同效果:3.75 倍提升!

参数配置建议

完整优化配置:

复制代码
# CBO 配置
spark.sql.cbo.enabled = true
spark.sql.cbo.joinReorder.enabled = true
spark.sql.cbo.joinReorder.dp.threshold = 12
spark.sql.cbo.joinReorder.dp.star.filter = false  # 根据场景调整
spark.sql.distinctAggPushdown.enabled = true

# AQE 配置
spark.sql.adaptive.enabled = true
spark.sql.adaptive.coalescePartitions.enabled = true
spark.sql.adaptive.skewJoin.enabled = true  # 建议启用

# 统计信息收集
定期执行:
  ANALYZE TABLE table_name COMPUTE STATISTICS;
  ANALYZE TABLE table_name COMPUTE STATISTICS FOR COLUMNS col1, col2;

实际场景调优案例

案例1: 多表 JOIN 性能优化

问题描述:

  • 5 表 JOIN 查询,执行时间 2 小时
  • 中间结果很大,Shuffle 开销大

当前配置分析:

复制代码
cbo.enabled = true ✓
joinReorder.enabled = true ✓
joinReorder.dp.threshold = 12 ✓
5 表 < 12,会完整搜索

诊断步骤:

复制代码
1. 检查统计信息:
   SHOW TABLE EXTENDED LIKE 'table_name';
   → 发现部分表缺少统计信息

2. 收集统计信息:
   ANALYZE TABLE table1 COMPUTE STATISTICS;
   ANALYZE TABLE table2 COMPUTE STATISTICS FOR COLUMNS id, date;
   ...

3. 查看执行计划:
   EXPLAIN COST SELECT ...;
   → 发现 JOIN 顺序不是最优

优化方案:

复制代码
1. 收集完整统计信息:
   - 表级统计
   - 列级统计(JOIN 列和过滤列)
   - 分区统计(如果分区表)

2. 验证 CBO 优化:
   - 查看执行计划中的成本信息
   - 确认 JOIN 顺序已优化

3. 预期效果:
   执行时间:2 小时 → 30 分钟
   性能提升:4 倍

案例2: 星型模式查询优化

问题描述:

  • 数据仓库星型模式查询
  • 事实表 500GB,5 个维度表
  • 查询耗时 1.5 小时

当前配置问题:

复制代码
star.filter = false ✗
→ 未启用星型模式优化

优化方案:

复制代码
1. 启用星型模式优化:
   spark.sql.cbo.joinReorder.dp.star.filter = true

2. 收集维度表统计信息:
   ANALYZE TABLE dim_table COMPUTE STATISTICS FOR COLUMNS id, category;

3. 验证优化效果:
   - 查看执行计划
   - 确认先过滤维度表,再 JOIN 事实表

4. 预期效果:
   执行时间:1.5 小时 → 20 分钟
   性能提升:4.5 倍

案例3: DISTINCT 聚合优化

问题描述:

  • 大表 COUNT(DISTINCT) 查询
  • Shuffle 数据量大,执行慢

当前配置分析:

复制代码
distinctAggPushdown.enabled = true ✓
→ 已启用,但效果不明显

诊断:

复制代码
1. 检查执行计划:
   EXPLAIN SELECT COUNT(DISTINCT col) FROM table;
   → 确认是否使用了预聚合

2. 检查数据分布:
   → 发现数据重复度低(去重效果不明显)

3. 检查统计信息:
   → 发现表统计信息过期

优化方案:

复制代码
1. 更新统计信息:
   ANALYZE TABLE table_name COMPUTE STATISTICS;

2. 如果数据重复度确实低:
   → distinctAggPushdown 优化效果有限
   → 考虑其他优化:
     - 增加 Executor 数量
     - 调整 Shuffle 分区数
     - 使用 AQE 分区合并

3. 如果数据重复度高但效果不明显:
   → 检查是否有其他瓶颈
   → 网络带宽、磁盘 I/O 等

案例4: 复杂查询综合优化

问题描述:

  • 包含多个 CTE、多表 JOIN、复杂聚合
  • 执行时间 3 小时

完整优化方案:

复制代码
1. CBO 优化:
   ✓ 收集所有表的统计信息
   ✓ 启用 JOIN 重排序
   ✓ 调整 threshold(如果表多)

2. AQE 优化:
   ✓ 启用分区合并
   ✓ 启用倾斜处理
   ✓ 启用动态分区裁剪

3. 其他优化:
   ✓ CTE 缓存
   ✓ 广播小表
   ✓ 过滤下推

4. 预期效果:
   执行时间:3 小时 → 25 分钟
   性能提升:7.2 倍

统计信息管理最佳实践

统计信息收集策略

初始收集:

sql 复制代码
-- 全表统计
ANALYZE TABLE table_name COMPUTE STATISTICS;

-- 关键列统计(JOIN 列、过滤列、分组列)
ANALYZE TABLE table_name COMPUTE STATISTICS FOR COLUMNS 
  join_col1, join_col2, 
  filter_col1, filter_col2,
  group_col1, group_col2;

增量更新:

sql 复制代码
-- 分区表增量更新
ANALYZE TABLE table_name PARTITION(date='2024-01-01') COMPUTE STATISTICS;

-- 定期全量更新(如每天)
ANALYZE TABLE table_name COMPUTE STATISTICS;

统计信息维护

监控统计信息:

sql 复制代码
-- 查看表统计信息
DESCRIBE EXTENDED table_name;

-- 查看列统计信息
SHOW COLUMN STATS table_name;

统计信息过期检测:

复制代码
指标:
  - 数据变化率 > 10%
  - 上次统计时间 > 7 天
  - 查询性能下降

处理:
  → 重新收集统计信息

自动化脚本示例

sql 复制代码
-- 收集所有表的统计信息
SET spark.sql.adaptive.enabled = true;

-- 表列表
SHOW TABLES;

-- 对每个表执行
ANALYZE TABLE ${table_name} COMPUTE STATISTICS;

-- 对关键列执行
ANALYZE TABLE ${table_name} COMPUTE STATISTICS FOR COLUMNS ${key_columns};

总结与最佳实践

CBO 参数配置检查清单

复制代码
□ CBO 总开关已启用
□ JOIN 重排序已启用
□ JOIN 重排序阈值设置合理(根据表数量)
□ 星型模式过滤已配置(如果是星型模式)
□ DISTINCT 聚合下推已启用
□ 统计信息已收集(表级 + 列级)
□ 统计信息定期更新
□ 结合 AQE 使用

性能监控指标

复制代码
关键指标:
1. 执行计划成本(EXPLAIN COST)
2. JOIN 顺序是否优化
3. Shuffle 数据量
4. 统计信息准确性
5. 查询执行时间

调优流程

复制代码
1. 基线测试:记录当前性能
2. 收集统计信息:表级 + 列级
3. 启用 CBO 相关参数
4. 查看执行计划:EXPLAIN COST
5. 验证优化效果:对比性能
6. 持续监控:定期更新统计信息

常见问题排查

复制代码
问题1: CBO 未生效
  → 检查统计信息是否收集
  → 检查参数是否启用
  → 查看执行计划

问题2: 优化效果不明显
  → 检查统计信息准确性
  → 检查数据分布
  → 考虑结合 AQE

问题3: 优化时间过长
  → 降低 joinReorder.dp.threshold
  → 使用启发式算法

相关推荐
江上月5131 天前
JMeter中级指南:从数据提取到断言校验全流程掌握
java·前端·数据库
代码猎人1 天前
forEach和map方法有哪些区别
前端
恋猫de小郭1 天前
Google DeepMind :RAG 已死,无限上下文是伪命题?RLM 如何用“代码思维”终结 AI 的记忆焦虑
前端·flutter·ai编程
m0_471199631 天前
【小程序】订单数据缓存 以及针对海量库存数据的 懒加载+数据分片 的具体实现方式
前端·vue.js·小程序
编程大师哥1 天前
Java web
java·开发语言·前端
A小码哥1 天前
Vibe Coding 提示词优化的四个实战策略
前端
Murrays1 天前
【React】01 初识 React
前端·javascript·react.js
大喜xi1 天前
ReactNative 使用百分比宽度时,aspectRatio 在某些情况下无法正确推断出高度,导致图片高度为 0,从而无法显示
前端
helloCat1 天前
你的前端代码应该怎么写
前端·javascript·架构
电商API_180079052471 天前
大麦网API实战指南:关键字搜索与详情数据获取全解析
java·大数据·前端·人工智能·spring·网络爬虫