news 2026/9/2 23:50:58

Spark Streaming最佳实践:处理TB级实时数据的技巧

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spark Streaming最佳实践:处理TB级实时数据的技巧

Spark Streaming最佳实践:处理TB级实时数据的技巧

1. 引入与连接:当“双11”的洪流涌来时,你需要的不只是勇气

想象这样一个场景:2024年双11零点,某电商平台的用户行为日志以每秒100万条的速度涌入数据管道——用户点击商品、加入购物车、提交订单的操作,每一条都需要实时计算:

  • 实时推荐系统需要根据用户最近5分钟的点击记录调整推荐列表;
  • 风控系统需要识别每秒10万条订单中的欺诈行为;
  • 实时Dashboard需要秒级更新全平台的成交金额、用户数。

此时,如果你负责实时计算引擎的搭建,如何用Spark Streaming处理TB级数据,同时保证低延迟、高可靠、Exactly-Once语义

这不是虚构的场景——阿里、京东、拼多多的实时计算系统,每天都在应对这样的挑战。而Spark Streaming作为实时计算生态中的“老将”,凭借易上手、兼容Spark生态、支持复杂状态计算的优势,依然是处理TB级实时数据的主流选择。

本文将从数据摄入、计算优化、状态管理、资源调度、故障恢复五大维度,拆解Spark Streaming处理TB级数据的“实战技巧”,帮你从“能用”到“用好”。

2. 概念地图:先搞懂Spark Streaming的“骨架”

在深入技巧前,我们需要先建立Spark Streaming的整体认知框架——它的核心逻辑、关键组件,以及与其他实时引擎的区别。

2.1 核心逻辑:微批处理(Micro-Batch)

Spark Streaming的本质是**“将流数据切割成小批次,用Spark Core的RDD计算模型处理”。比如,你设置“每1秒处理一批数据”,那么Spark会把连续的流数据分成1秒的“微批”,每个微批对应一个RDD,整个流计算就是RDD序列的连续处理**(即DStream,Discretized Stream)。

这种设计的优势是复用Spark Core的优化(如RDD缓存、Shuffle优化),但代价是延迟无法突破“微批间隔”(比如1秒微批的延迟至少1秒)。

2.2 关键组件与关系

组件作用
StreamingContextSpark Streaming的入口,负责启动计算、管理生命周期
DStream流数据的抽象,本质是“RDD序列”,每个RDD对应一个微批
Receiver早期的数据摄入组件(如Kafka Receiver),单线程拉取数据,易成为瓶颈
Direct API后期的Kafka数据摄入方式,直接拉取Kafka分区数据,并行度更高
Checkpoint保存流计算的状态(如RDD依赖、offset、状态数据),用于故障恢复
Backpressure反压机制,自动调整数据摄入速率,防止Executor被压垮

2.3 Spark Streaming在实时生态中的位置

与Storm(超低延迟、无状态)、Flink(流批一体、低延迟)相比,Spark Streaming的优势是**“平衡了延迟、复杂度和生态兼容性”**:

  • 比Storm支持更复杂的状态计算(如滚动窗口、会话窗口);
  • 比Flink更易上手(复用Spark SQL、DataFrame的知识);
  • 适合1秒-10秒延迟的场景(如实时报表、推荐系统、日志分析)。

3. 基础理解:避开Spark Streaming的“新手坑”

3.1 最常见的误解:“微批=实时?”

很多新手认为“微批就是实时”,但实际上:

  • 微批的延迟下限是“微批间隔”(比如1秒微批的延迟至少1秒);
  • 真实场景中,延迟会略高于微批间隔(比如数据摄入、计算的耗时)。

结论:如果你的场景需要亚秒级延迟(如金融高频交易),请选Flink;如果是1秒+延迟的场景,Spark Streaming足够用。

3.2 Receiver模式的“致命缺陷”

早期的Spark Streaming用Receiver模式摄入Kafka数据:

  1. Receiver启动一个线程,从Kafka拉取数据;
  2. 将数据缓存到Executor的内存中;
  3. Spark计算时从内存中读取数据。

这种模式的问题是**“单线程瓶颈”**——当数据量达到TB级时,Receiver的单线程根本拉不完数据,导致数据积压、延迟飙升。

解决方案永远用Direct API代替Receiver模式

3.3 Direct API的“正确打开方式”

Direct API是Spark 1.3引入的Kafka数据摄入方式,核心优势是**“并行度与Kafka分区数对齐”**:

  1. Spark直接连接Kafka的每个分区(比如Kafka有100个分区,Spark会启动100个线程拉取数据);
  2. 每个Kafka分区对应一个RDD的Partition,并行处理;
  3. 自动管理Kafka offset(不需要ZooKeeper或Kafka的auto-commit)。

代码示例(Scala):

valkafkaParams=Map[String,Object]("bootstrap.servers"->"kafka1:9092,kafka2:9092",// Kafka集群地址"key.deserializer"->classOf[StringDeserializer],"value.deserializer"->classOf[StringDeserializer],"group.id"->"spark-streaming-group",// 消费者组"auto.offset.reset"->"latest",// 从最新offset开始消费"enable.auto.commit"->(false:java.lang.Boolean)// 禁止自动提交offset)valtopics=Array("user-behavior-topic")// 要消费的Kafka主题valstream=KafkaUtils.createDirectStream[String,String](streamingContext,PreferConsistent,// 分区分配策略:尽量均匀分布到ExecutorSubscribe[String,String](topics,kafkaParams)// 订阅主题)

4. 层层深入:处理TB级数据的“五大核心技巧”

技巧1:数据摄入优化——从“瓶颈”到“流畅”

数据摄入是TB级处理的第一关,如果摄入慢,后面的计算再快也没用。重点优化以下三点:

4.1.1 用Direct API代替Receiver

如前所述,Direct API的并行度等于Kafka分区数,而Receiver是单线程。对于TB级数据,Direct API是必选

验证效果:某公司用Receiver模式处理Kafka日志,延迟高达10秒;换成Direct API后,延迟降到2秒(Kafka分区数100,并行度100)。

4.1.2 调整Kafka分区数与Spark并行度

Spark Streaming的并行度由Kafka分区数DStream的partition数共同决定。最佳实践是:

  • Kafka分区数 ≥ 100(TB级数据需要足够的并行度);
  • Spark并行度 = Kafka分区数 × 2(比如Kafka有100个分区,Spark并行度设为200)。

原因:Kafka分区是“数据分片的最小单位”,Spark并行度超过Kafka分区数,可以分散Shuffle压力(比如某个Kafka分区的数据量太大,多线程处理能拆分压力)。

代码示例:调整DStream的并行度

valstream=KafkaUtils.createDirectStream[...]// 省略valparallelStream=stream.repartition(200)// 将并行度设为200
4.1.3 禁用Kafka的auto-commit

Direct API会自己管理offset(保存在Checkpoint或Driver内存中),如果开启Kafka的auto-commit(enable.auto.commit=true),会导致offset不一致(比如Spark还没处理完数据,Kafka就提交了offset,故障恢复时会丢数据)。

正确配置enable.auto.commit=false,由Spark自己管理offset。

技巧2:计算层优化——让每个Executor“跑满”

TB级数据的计算压力主要来自Shuffle、序列化、内存管理,优化的核心是“让计算资源充分利用”。

4.2.1 用Kryo序列化代替Java序列化

Java序列化的问题是慢、占内存(比如一个对象用Java序列化占100KB,Kryo可能只占20KB)。Spark默认用Java序列化,但处理TB级数据时,必须换成Kryo。

配置步骤

  1. 在StreamingContext中注册Kryo序列化;
  2. 注册自定义类(如果有)。

代码示例

valconf=newSparkConf().setAppName("TBLevelStreaming").setMaster("yarn").set("spark.serializer","org.apache.spark.serializer.KryoSerializer")// 启用Kryo.set("spark.kryo.registrationRequired","false")// 不强制注册所有类(方便开发)valssc=newStreamingContext(conf,Seconds(1))// 1秒微批
4.2.2 调整Executor资源配置

Executor的配置直接决定计算能力,TB级数据的推荐配置(YARN集群):

  • Executor内存:16GB-32GB(太大容易导致GC时间过长,太小容易OOM);
  • Executor核数:8核-16核(核数太多会导致上下文切换频繁,太少无法利用多核);
  • 堆外内存:2GB-4GB(用于Netty通信、序列化,避免堆内存不足)。

YARN配置示例(spark-submit参数):

spark-submit\--class com.xxx.StreamingJob\--masteryarn\--deploy-mode cluster\--executor-memory 16g\--executor-cores8\--num-executors20\--conf spark.yarn.executor.memoryOverhead=4096\# 堆外内存4GB--conf spark.serializer=org.apache.spark.serializer.KryoSerializer\your-job.jar
4.2.3 开启动态资源分配

Spark 1.2引入了动态资源分配(Dynamic Resource Allocation),可以根据计算压力自动增加/减少Executor(比如数据量突增时,自动申请更多Executor;数据量下降时,释放空闲Executor)。

开启条件

  1. 集群支持动态资源分配(如YARN、K8s);
  2. 设置以下配置:
--conf spark.dynamicAllocation.enabled=true\--conf spark.dynamicAllocation.minExecutors=5\# 最小Executor数--conf spark.dynamicAllocation.maxExecutors=50\# 最大Executor数--conf spark.dynamicAllocation.executorIdleTimeout=60s\# 空闲60秒释放Executor

技巧3:状态管理——避免“状态爆炸”

TB级数据处理中,状态计算(如滚动窗口、会话窗口、累加计数)是常见需求,但状态数据如果不优化,会导致内存溢出、Checkpoint过大

4.3.1 区分“无状态”与“有状态”计算
  • 无状态计算:每个微批的处理不依赖之前的结果(如过滤日志、解析JSON),不需要保存状态,性能最好;
  • 有状态计算:每个微批的处理依赖之前的结果(如“最近5分钟的用户点击数”),需要保存状态(如窗口内的计数)。

建议:尽量用无状态计算,只有必须时才用有状态计算。

4.3.2 优化Checkpoint

Checkpoint是Spark Streaming保存状态数据的核心机制(比如窗口计算的中间结果、Kafka的offset),但Checkpoint太频繁会占用大量IO资源,太稀疏会导致故障恢复时丢失更多数据

最佳实践

  1. Checkpoint目录用可靠存储(如HDFS、S3),不要用本地文件系统(故障时会丢失);
  2. Checkpoint间隔设为“微批间隔的5-10倍”(比如1秒微批,Checkpoint间隔设为10秒);
  3. 禁用RDD的Checkpoint(DStream的Checkpoint已经包含RDD的依赖,不需要额外设置)。

代码示例:设置Checkpoint

valssc=newStreamingContext(conf,Seconds(1))ssc.checkpoint("hdfs://nn1:8020/spark/checkpoint")// 用HDFS作为Checkpoint存储
4.3.3 用“增量状态更新”代替“全量计算”

比如,计算“最近10分钟的用户点击数”,如果用“全量重新计算每个窗口”(比如每1秒重新计算过去10分钟的所有数据),会导致计算量随窗口大小线性增长(10分钟窗口的计算量是1分钟窗口的10倍)。

解决方案:用增量状态更新(如updateStateByKeymapWithState):

  • updateStateByKey:保存所有历史状态(比如用户从开始到现在的点击数),适合“全局累加”;
  • mapWithState:只保存“活跃状态”(比如用户最近5分钟的点击数),适合“窗口内的状态”。

代码示例:用mapWithState计算“最近5分钟的用户点击数”

// 定义状态更新函数:输入(用户ID, 当前点击数),输出(用户ID, 累计点击数)valstateSpec=StateSpec.function((key:String,value:Option[Int],state:State[Int])=>{valcurrentCount=value.getOrElse(0)valnewCount=state.getOption.getOrElse(0)+currentCount state.update(newCount)Some((key,newCount))})// 设置状态超时:5分钟内没有数据,自动清除状态valtimeoutSpec=stateSpec.timeout(Seconds(300))// 应用状态更新valstateStream=stream.map((_,1)).mapWithState(timeoutSpec)

技巧4:资源调度——避免“资源饥饿”

TB级数据的资源调度核心是**“让Driver、Executor、集群资源匹配”**,常见问题包括:

  • Driver内存不足(比如保存大量offset或状态数据);
  • Executor的CPU/内存比例失衡(比如CPU用满但内存空闲,或反之);
  • 集群资源不够(比如YARN的队列资源不足)。
4.4.1 配置Driver内存

Driver的主要职责是管理StreamingContext、调度任务、保存Checkpoint元数据,TB级数据下,Driver内存建议设为8GB-16GB(比如处理100万/s的Kafka数据,Driver需要保存100万条offset,内存约几GB)。

配置示例

spark-submit\--driver-memory 8g\# Driver内存设为8GB...
4.4.2 调整Executor的CPU/内存比例

Executor的CPU核数与内存的比例建议为1:2(比如8核Executor对应16GB内存)。原因:Spark的计算是“CPU密集型+内存密集型”,1:2的比例能平衡两者的压力(比如8核需要16GB内存来缓存数据,避免频繁GC)。

4.4.3 用YARN的“队列资源隔离”

如果你的集群有多个Spark任务,建议用YARN队列隔离资源(比如给实时任务分配“实时队列”,资源占集群的50%;给离线任务分配“离线队列”,资源占50%)。

配置示例:提交任务到“实时队列”

spark-submit\--queue realtime\# 提交到YARN的realtime队列...

技巧5:故障恢复——实现“Exactly-Once”语义

TB级数据处理中,数据不丢不重(Exactly-Once)是核心需求,Spark Streaming实现Exactly-Once需要满足三个条件:

4.5.1 条件1:幂等输出

幂等输出是指“重复写入同一数据,结果不变”(比如写入Redis的SET key value,重复执行不会改变结果)。

常见幂等输出场景

  • 写入Redis(SETHSET);
  • 写入HBase(put操作,主键唯一);
  • 写入Kafka(用事务性生产者,transactional.id唯一)。
4.5.2 条件2:事务性写入

如果输出不是幂等的(比如写入MySQL的INSERT操作,重复执行会插入重复数据),需要用事务性写入

  1. 启动事务;
  2. 执行计算;
  3. 写入数据;
  4. 提交事务(只有写入成功,才提交事务)。

示例:用事务性Kafka生产者实现Exactly-Once

valprops=newProperties()props.put("bootstrap.servers","kafka1:9092")props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer")props.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer")props.put("transactional.id","spark-transaction-1")// 唯一事务IDvalproducer=newKafkaProducer[String,String](props)producer.initTransactions()// 初始化事务// 处理数据并写入Kafkastream.foreachRDD{rdd=>producer.beginTransaction()// 开始事务try{rdd.foreach{record=>producer.send(newProducerRecord[String,String]("output-topic",record.key,record.value))}producer.commitTransaction()// 提交事务}catch{casee:Exception=>producer.abortTransaction()// 回滚事务throwe}}
4.5.3 条件3:精确的offset管理

Direct API会将Kafka的offset保存在Checkpoint中,故障恢复时,Spark会从Checkpoint中读取最后一次处理的offset,重新处理从该offset开始的数据。

注意:如果Checkpoint存储不可靠(比如用本地文件系统),故障时会丢失offset,导致数据丢失。因此,Checkpoint必须用可靠存储(如HDFS、S3)

技巧6:反压机制——防止“数据洪流压垮系统”

当数据量突然飙升(比如双11零点的流量峰值),Spark Streaming的处理能力可能跟不上数据摄入速度,导致Executor内存溢出、任务排队。此时,**反压机制(Backpressure)**能自动调整数据摄入速率,让系统“喘口气”。

4.6.1 开启反压

Spark 1.5及以上版本支持反压,开启方式很简单:

spark-submit\--conf spark.streaming.backpressure.enabled=true\# 开启反压--conf spark.streaming.backpressure.pid.proportional=1.0\# 比例系数,控制调整幅度--conf spark.streaming.backpressure.pid.integral=0.5\# 积分系数,处理长期偏差--conf spark.streaming.backpressure.pid.derivative=0.1\# 微分系数,处理短期波动...
4.6.2 反压的工作原理

反压机制通过监控Executor的任务队列长度来调整数据摄入速率:

  1. 如果任务队列长度超过阈值(比如1000个任务),反压机制会降低数据摄入速率(比如从10万条/秒降到5万条/秒);
  2. 如果任务队列长度低于阈值,反压机制会提高数据摄入速率(比如从5万条/秒升到8万条/秒)。

5. 多维透视:Spark Streaming的“过去、现在与未来”

5.1 历史视角:从“Receiver”到“Structured Streaming”

Spark Streaming的发展历程是**“不断解决瓶颈,向流批一体演进”**:

  • 2013年:Spark 0.7.0发布,引入Spark Streaming(Receiver模式);
  • 2014年:Spark 1.3.0发布,引入Kafka Direct API,解决Receiver的瓶颈;
  • 2016年:Spark 2.0.0发布,引入Structured Streaming(流批一体的新API);
  • 2020年:Spark 3.0.0发布,Structured Streaming成为“推荐的流处理API”,Spark Streaming进入“维护模式”。

5.2 实践视角:某电商TB级日志处理案例

某电商的实时日志分析系统(处理TB级Nginx日志),用Spark Streaming的配置如下:

  • 数据摄入:Kafka Direct API,Kafka分区数100,Spark并行度200;
  • 计算配置:Executor内存16GB,8核,动态资源分配(5-50 Executors);
  • 状态管理:用mapWithState计算“最近10分钟的接口调用量”,Checkpoint间隔10秒;
  • 故障恢复:Checkpoint存HDFS,Exactly-Once语义(幂等写入Redis);
  • 效果:处理延迟2秒以内,故障恢复时间<1分钟,日处理数据量10TB+。

5.3 批判视角:Spark Streaming的局限性

  • 延迟无法突破微批间隔:不适合亚秒级延迟场景(如高频交易);
  • 状态管理复杂updateStateByKey会保存所有历史状态,容易导致内存溢出;
  • 流批分离:Spark Streaming的API与Spark SQL的API不兼容,需要写两套代码(流处理用DStream,批处理用DataFrame)。

5.4 未来视角:Structured Streaming的崛起

Structured Streaming是Spark 2.0引入的流批一体API,解决了Spark Streaming的痛点:

  • 流批一体:用DataFrame/DataSet API,流处理和批处理的代码几乎一样;
  • 低延迟:支持“连续处理模式”(Continuous Processing),延迟低至毫秒级;
  • 更简单的状态管理:用groupByWindow代替updateStateByKey,自动管理窗口状态;
  • 更完善的Exactly-Once:支持事务性写入(如Delta Lake)。

结论:如果你的项目是新启动的,优先选Structured Streaming;如果已经在用Spark Streaming,可以逐步迁移到Structured Streaming。

6. 实践转化:从“技巧”到“落地”的三步法

6.1 第一步:小数据量测试

在处理TB级数据前,先用**小数据量(比如1GB)**测试配置:

  1. 测试Direct API的并行度(比如Kafka分区数10,Spark并行度20);
  2. 测试Checkpoint的性能(比如Checkpoint间隔10秒,查看IO耗时);
  3. 测试反压机制(模拟数据量突增,查看延迟变化)。

6.2 第二步:逐步放大数据量

小数据量测试通过后,逐步放大到10GB→100GB→1TB→TB级,每一步都要监控:

  • 延迟:用Spark UI的“Streaming”页面查看“Total Delay”(总延迟);
  • 资源利用率:用YARN的ResourceManager查看Executor的CPU/内存利用率;
  • 故障恢复:手动kill Executor,查看是否能恢复,是否丢数据。

6.3 第三步:持续优化

TB级数据处理是“持续优化”的过程,需要定期:

  1. 分析Spark UI:查看Shuffle次数、GC时间、任务排队情况;
  2. 调整配置:比如GC时间太长,增加Executor内存;
  3. 监控报警:设置延迟超过5秒报警,资源利用率超过90%报警。

7. 整合提升:处理TB级数据的“终极清单”

7.1 核心技巧回顾

维度核心技巧
数据摄入用Direct API,并行度=Kafka分区数×2,禁用Kafka auto-commit
计算层用Kryo序列化,调整Executor的CPU/内存比例,开启动态资源分配
状态管理用增量状态更新(mapWithState),Checkpoint存可靠存储,间隔10秒以上
资源调度配置Driver内存8GB+,用YARN队列隔离资源,开启动态资源分配
故障恢复Exactly-Once语义(幂等输出、事务性写入、精确offset管理)
反压机制开启Backpressure,调整PID参数

7.2 思考问题(深化理解)

  1. 如果遇到数据倾斜(某个Kafka分区的数据量是其他分区的10倍),如何处理?
  2. 如果需要跨窗口的状态计算(比如“用户上周的点击数+本周的点击数”),如何实现?
  3. 如何结合Structured Streaming,将现有的Spark Streaming任务迁移过去?

7.3 进阶资源

  • 官方文档:Spark Streaming Guide(https://spark.apache.org/docs/latest/streaming-programming-guide.html);
  • 书籍:《Spark快速大数据分析》(第2版,涵盖Spark Streaming);
  • 博客:Databricks的Spark Streaming系列(https://databricks.com/blog/categories/spark-streaming);
  • 工具:Spark UI(监控延迟、资源利用率)、Ganglia(监控集群资源)。

结语:实时计算的“平衡术”

处理TB级实时数据,本质是**“在延迟、复杂度、可靠性之间找平衡”。Spark Streaming不是“最完美的引擎”,但它是“最适合大多数场景的引擎”**——它复用了Spark生态的优势,易上手,能处理复杂的状态计算,支持TB级数据的低延迟处理。

最后,送你一句实时计算的“名言”:“没有银弹,只有适合场景的选择”。希望本文的技巧能帮你在TB级数据的洪流中,找到属于自己的“平衡”。

下一步行动:打开你的Spark Streaming项目,先把Receiver模式换成Direct API,再调整并行度到Kafka分区数的2倍,看看延迟有没有下降!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 8:22:54

G-Helper终极指南:华硕设备硬件控制与性能优化全解析

G-Helper终极指南&#xff1a;华硕设备硬件控制与性能优化全解析 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops. Control tool for ROG Zephyrus G14, G15, G16, M16, Flow X13, Flow X16, TUF, Strix, Scar and other models 项目地址…

作者头像 李华
网站建设 2026/9/2 0:23:39

哔哩下载姬DownKyi终极教程:7步精通视频下载与管理的完整指南

哔哩下载姬DownKyi终极教程&#xff1a;7步精通视频下载与管理的完整指南 【免费下载链接】downkyi 哔哩下载姬downkyi&#xff0c;哔哩哔哩网站视频下载工具&#xff0c;支持批量下载&#xff0c;支持8K、HDR、杜比视界&#xff0c;提供工具箱&#xff08;音视频提取、去水印等…

作者头像 李华
网站建设 2026/9/2 19:49:17

新手也能看懂的【前端自动化测试入门】

最近在网上搜索前端自动化测试相关的文档&#xff0c;但是发现网上的文章都是偏使用&#xff0c;没有把一些基础概念说清楚&#xff0c;导致后续一口气遇到一些karma、Jasmine、jest、Mocha、Chai、BDD等词汇的时候很容易一头雾水&#xff0c;这次一方面整理一下收获的知识一方…

作者头像 李华
网站建设 2026/9/2 3:04:10

XUnity.AutoTranslator 游戏翻译神器:打破语言障碍的终极解决方案

XUnity.AutoTranslator 游戏翻译神器&#xff1a;打破语言障碍的终极解决方案 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator 还在为看不懂外语游戏而苦恼吗&#xff1f;XUnity.AutoTranslator这款革命性…

作者头像 李华
网站建设 2026/9/2 16:43:37

Unity游戏翻译神器:XUnity Auto Translator完整使用手册

Unity游戏翻译神器&#xff1a;XUnity Auto Translator完整使用手册 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator XUnity Auto Translator是一款专业的Unity游戏自动翻译插件&#xff0c;能够为游戏开发…

作者头像 李华
网站建设 2026/9/2 5:12:06

springboot基于Hadoop的豆瓣电子图书推荐系统爬虫_28r41260

目录已开发项目效果实现截图开发技术介绍核心代码参考示例1.建立用户稀疏矩阵&#xff0c;用于用户相似度计算【相似度矩阵】2.计算目标用户与其他用户的相似度系统测试总结源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;已开发项目效果…

作者头像 李华