免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

基于Hadoop的电影推荐系统设计与实现(SpringBoot+Spider)

基于Hadoop的电影推荐系统设计与实现(SpringBoot+Spider) 简介这是一套基于Hadoop大数据生态的电影推荐系统毕设级完整源码面向Java/Python/大数据方向的学习者与毕业生解决个性化推荐系统从数据采集、分布式处理到前后端协同落地的全流程实践问题。资源共642个文件涵盖127个Java后端模块SpringBoot、99个Vue前端页面、8个Python爬虫脚本含scrapy.cfg及bat运行脚本、63个JS交互逻辑、43个JPG/PNG界面素材以及SQL建表文件、Hadoop配置片段和后台管理看板可视化组件压缩包仅26MB结构清晰、开箱可运行。已有94人学习下载适合课程设计、工程实训或毕设选题参考。读者可直接部署运行获得包含用户中心、管理员看板含评分/导演/类型等Hadoop分析图表、电影信息管理、论坛交流及Scrapy动态爬取能力的全链路系统同时掌握MySQL5.7HDFSMapReduceVueSpringBoot多技术栈集成经验。1. 为什么用 Hadoop 做电影推荐系统不是“为了大数据而大数据”而是解决真实吞吐瓶颈你手上有 500 万用户、2000 万条观影记录、30 万部电影元数据——用 MySQL 单表 JOIN 用户行为 电影标签查询一个协同过滤相似度矩阵要 47 秒SpringBoot 接口超时熔断前端白屏运营同学催着上线“猜你喜欢”模块。这不是理论题是上周我接手的生产事故现场。这个标题里的5b002基于Hadoop大数据技术的电影推荐系统的设计与实现_springbootspider.zip本质是一套可落地的分层架构方案用 Spider 爬取豆瓣/猫眼等公开电影数据含评分、标签、演职员存入 HDFS用 MapReduce 或 Spark本项目用原生 MapReduce计算用户-物品共现矩阵、ItemCF 相似度、Top-N 推荐列表Hive 建模做维度聚合如“科幻类用户偏好导演TOP10”最终 SpringBoot 作为服务网关从 HBase 或 MySQL缓存结果读取预计算好的推荐结果毫秒级返回。它不追求“全栈大数据”而是把 Hadoop 当作批处理加速器把 SpringBoot 当作稳态服务壳把 Spider 当作可控数据源管道——三者边界清晰各司其职。适合高校课程设计、中小厂推荐冷启动、或已有 Java 团队想低成本接入大数据能力的场景。别被“Hadoop”吓住本项目实际只用到 HDFS 存储 MapReduce 计算 Hive 元数据管理没碰 YARN 调度细节也没上 Kafka/Flink 实时链路。2. 从零搭起 Hadoop 伪分布式环境避坑版安装流程Ubuntu 20.04 Hadoop 3.3.6提示本节所有命令均在 Ubuntu 20.04 LTS 下实测通过JDK 版本必须为 11Hadoop 3.3.x 不兼容 JDK 17且需关闭 swap 分区否则 NameNode 启动失败。2.1 JDK 11 安装与环境变量固化# 下载官方 OpenJDK 11非 Oracle JDK避免 license 问题 wget https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.22%2B7/OpenJDK11U-jdk_x64_linux_hotspot_11.0.22_7.tar.gz tar -xzf OpenJDK11U-jdk_x64_linux_hotspot_11.0.22_7.tar.gz -C /opt/ sudo ln -sf /opt/jdk-11.0.227 /opt/java11 # 写入 /etc/profile.d/java11.sh比直接改 profile 更安全重启生效 echo export JAVA_HOME/opt/java11 | sudo tee /etc/profile.d/java11.sh echo export PATH$JAVA_HOME/bin:$PATH | sudo tee -a /etc/profile.d/java11.sh source /etc/profile.d/java11.sh # 验证 java -version # 输出应为 openjdk version 11.0.22 2024-01-16逻辑说明Hadoop 3.3.x 编译时绑定 JDK 11 字节码规范若用 JDK 17 运行会报UnsupportedClassVersionError/etc/profile.d/下的脚本在每次登录时自动 source比修改/etc/profile更易维护且不影响系统级服务。2.2 Hadoop 3.3.6 伪分布式核心配置5 个文件删减无用项本项目不启用 HA、不配 YARN ResourceManager 高可用仅需core-site.xml、hdfs-site.xml、mapred-site.xml、yarn-site.xml、workers五个文件。关键参数如下表全部位于$HADOOP_HOME/etc/hadoop/文件名关键配置项值作用说明core-site.xmlfs.defaultFShdfs://localhost:9000指定默认文件系统为本地 HDFS非file:///hdfs-site.xmldfs.replication1伪分布式只需 1 副本避免磁盘空间浪费dfs.namenode.name.dir/usr/local/hadoop/hdfs/namenodeNameNode 元数据存储路径必须提前创建并赋权dfs.datanode.data.dir/usr/local/hadoop/hdfs/datanodeDataNode 数据块存储路径同上mapred-site.xmlmapreduce.framework.nameyarn启用 YARN 作为 MapReduce 运行时即使伪分布式也需设yarn-site.xmlyarn.nodemanager.aux-servicesmapreduce_shuffle启用 ShuffleService否则 Reduce 阶段卡死yarn.nodemanager.env-whitelistJAVA_HOME,HADOOP_COMMON_HOME,HADOOP_HDFS_HOME,HADOOP_CONF_DIR,CLASSPATH_PREPEND_DISTCACHE,HADOOP_YARN_HOME,HADOOP_MAPRED_HOME必配否则 MapReduce 任务找不到 JAVA_HOME注意/usr/local/hadoop/hdfs/namenode和/usr/local/hadoop/hdfs/datanode目录需手动创建并执行sudo chown -R $USER:$USER /usr/local/hadoop/hdfs否则格式化失败报Permission denied。2.3 格式化 NameNode 并启动服务链# 创建 Hadoop 安装目录并解压假设已下载 hadoop-3.3.6.tar.gz sudo mkdir -p /usr/local/hadoop sudo tar -xzf hadoop-3.3.6.tar.gz -C /usr/local/hadoop --strip-components1 # 格式化 NameNode首次运行必须 hdfs namenode -format # 启动 HDFSNameNode DataNode start-dfs.sh # 启动 YARNResourceManager NodeManager start-yarn.sh # 验证进程应看到 NameNode、DataNode、ResourceManager、NodeManager jps | grep -E (NameNode|DataNode|ResourceManager|NodeManager) # 正常输出示例 # 12345 NameNode # 12389 DataNode # 12456 ResourceManager # 12511 NodeManager逻辑说明start-dfs.sh和start-yarn.sh是 Hadoop 封装的启动脚本内部调用hadoop-daemon.sh和yarn-daemon.shjps是 JDK 自带进程查看工具比ps aux \| grep更精准若只看到 NameNode 没有 DataNode大概率是hdfs-site.xml中dfs.datanode.data.dir路径权限不对或磁盘满。3. Spider 爬虫模块稳定抓取电影数据的 3 层防护策略本项目 Spider 模块Python 3.9 Scrapy 2.11不追求高并发而重抗反爬鲁棒性和数据结构一致性。目标站点为豆瓣电影 Top250公开页面、猫眼电影分类页需处理动态渲染数据字段包括movie_id,title,year,director,actors,genres,rating,vote_count,summary。3.1 请求层User-Agent 轮换 请求间隔 Referer 模拟# scrapy/spiders/movie_spider.py import random from scrapy import Spider from scrapy.http import Request class MovieSpider(Spider): name douban_movie start_urls [https://movie.douban.com/top250] # 预置 UA 池来自真实浏览器采集 USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ] def start_requests(self): for url in self.start_urls: yield Request( urlurl, headers{User-Agent: random.choice(self.USER_AGENTS)}, meta{download_delay: 2}, # 每请求间隔 2 秒 callbackself.parse_list ) def parse_list(self, response): # 提取每部电影链接 movie_links response.css(div.item a::attr(href)).getall() for link in movie_links: yield Request( urllink, headers{ User-Agent: random.choice(self.USER_AGENTS), Referer: response.url # 模拟从列表页跳转 }, callbackself.parse_detail )参数说明download_delay2是 Scrapy 内置限速参数比time.sleep()更可靠避免阻塞 reactorReferer必须设置豆瓣校验 Referer 是否来自其域名UA 池需定期更新建议每月从 useragentstring.com 抓取最新 20 条。3.2 解析层XPath 正则双保险提取结构化字段# 在 parse_detail 方法中 def parse_detail(self, response): item {} item[movie_id] response.url.split(/)[-2] # 从 URL 提取 douban id # 标题可能含年份括号需清洗 title_raw response.xpath(//span[propertyv:itemreviewed]/text()).get() item[title] re.sub(r\s*\(\d{4}\)\s*$, , title_raw).strip() if title_raw else # 年份优先从标题括号提取 fallback 到 info 区域 year_match re.search(r\((\d{4})\), title_raw) item[year] year_match.group(1) if year_match else \ response.xpath(//span[classyear]/text()).re_first(r\((\d{4})\)) or 0000 # 导演多导演用 / 分隔 directors response.xpath(//a[relv:directedBy]/text()).getall() item[director] / .join([d.strip() for d in directors]) if directors else # 评分保留一位小数 rating response.xpath(//strong[propertyv:average]/text()).get() item[rating] float(rating.strip()) if rating and rating.strip().replace(., ).isdigit() else 0.0 # 投票人数去除逗号转整型 vote_text response.xpath(//span[propertyv:votes]/text()).get() item[vote_count] int(vote_text.replace(,, )) if vote_text else 0 yield item逻辑说明response.xpath()比 CSS 选择器更稳定豆瓣 HTML 结构常变但 XPath 路径相对固定正则re.sub清洗标题是血泪经验——曾因未剔除(2023)导致后续 HBase RowKey 冲突v:votes字段含千分位逗号必须replace(,, )否则int()报错。3.3 存储层JSONL 格式写入 HDFS规避序列化陷阱# pipelines.py import json from hdfs import InsecureClient class HdfsPipeline: def __init__(self): self.client InsecureClient(http://localhost:9870, userhadoop) # HDFS Web UI 地址 self.hdfs_path /raw/movies/douban_top250.jsonl def process_item(self, item, spider): # 转 JSONL每行一个 JSON 对象Hadoop 生态友好 line json.dumps(item, ensure_asciiFalse) \n try: # 追加写入避免覆盖 with self.client.write(self.hdfs_path, encodingutf-8, appendTrue) as writer: writer.write(line) except Exception as e: spider.logger.error(fHDFS write failed: {e}) return item参数说明json.dumps(..., ensure_asciiFalse)保证中文不转义appendTrue是关键Scrapy 默认 pipeline 是流式处理每条 item 独立写入/raw/movies/是 HDFS 标准分层路径便于后续 Hive 外部表映射InsecureClient适用于伪分布式无 Kerberos生产环境需换KerberosClient。4. 推荐算法实现MapReduce 版 ItemCF 的 3 阶段拆解与参数调优本项目采用Item-Based Collaborative FilteringItemCF因电影 ID 稳定、物品数30 万远小于用户数500 万计算复杂度更低。Hadoop 实现分三阶段用户行为清洗MapReduce Job 1过滤低频用户/电影生成user_id, [(movie_id, rating)]共现矩阵构建MapReduce Job 2对每个用户行为列表两两组合输出movie_i,movie_j, count相似度计算 Top-N 生成MapReduce Job 3按movie_i分组计算余弦相似度取 Top10 相似电影4.1 共现矩阵 Mapper避免笛卡尔积爆炸的剪枝策略// CooccurrenceMapper.java public class CooccurrenceMapper extends MapperLongWritable, Text, Text, IntWritable { private final static IntWritable one new IntWritable(1); private Text outputKey new Text(); Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] parts value.toString().split(\t); // 输入格式user_id\tmovie_id1,rating1;movie_id2,rating2 if (parts.length 2) return; String userId parts[0]; String[] movies parts[1].split(;); // 剪枝单用户行为少于 2 条跳过无法构成共现 if (movies.length 2) return; // 两两组合但限制最大组合数防长尾用户拖慢 int maxPairs Math.min(20, movies.length * (movies.length - 1) / 2); int count 0; for (int i 0; i movies.length count maxPairs; i) { String[] mv1 movies[i].split(,); if (mv1.length 2) continue; String movie1 mv1[0].trim(); for (int j i 1; j movies.length count maxPairs; j) { String[] mv2 movies[j].split(,); if (mv2.length 2) continue; String movie2 mv2[0].trim(); // 确保 movie1 movie2避免重复键如 A,B 和 B,A if (movie1.compareTo(movie2) 0) { outputKey.set(movie1 , movie2); } else { outputKey.set(movie2 , movie1); } context.write(outputKey, one); count; } } } }逻辑说明maxPairs20是关键调优点——某用户观看了 1200 部电影全量组合产生 71.9 万对导致单个 Mapper 内存溢出限制后牺牲少量精度但保障稳定性movie1.compareTo(movie2)0保证键标准化Reducer 可正确聚合。4.2 相似度 Reducer内存敏感型计算避免 OOM// SimilarityReducer.java public class SimilarityReducer extends ReducerText, IntWritable, Text, Text { Override protected void reduce(Text key, IterableIntWritable values, Context context) throws IOException, InterruptedException { // key 格式movie_i,movie_j String[] movies key.toString().split(,); String movieI movies[0]; String movieJ movies[1]; // 统计共现次数 int cooccur 0; for (IntWritable val : values) cooccur val.get(); // 从 HDFS 读取 movie_i 和 movie_j 的总出现频次预计算好存入 /meta/movie_freq/ // 此处简化假设已缓存到 Configuration实际应走 DistributedCache int freqI context.getConfiguration().getInt(freq. movieI, 1); int freqJ context.getConfiguration().getInt(freq. movieJ, 1); // 余弦相似度sim(i,j) cooccur / sqrt(freq_i * freq_j) double similarity (double) cooccur / Math.sqrt(freqI * freqJ); // 只输出相似度 0.1 的对降噪 if (similarity 0.1) { context.write(new Text(movieI), new Text(movieJ : String.format(%.4f, similarity))); } } }参数说明similarity 0.1是经验值低于此值的相似关系噪声大String.format(%.4f)控制精度避免浮点误差影响排序freq.*从 Configuration 读取是伪代码实际需用DistributedCache.addCacheFile()加载频次文件并在setup()中解析为 HashMap。4.3 Top-N 合并用 Secondary Sort 实现单次 Reduce 聚合// TopNMapper.java —— 输出 movie_i, (similarity, movie_j)让 movie_i 为 partition key public class TopNMapper extends MapperLongWritable, Text, Text, Text { Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] parts value.toString().split(\t); if (parts.length ! 2) return; String movieI parts[0]; String[] simPair parts[1].split(:); if (simPair.length ! 2) return; String movieJ simPair[0]; double sim Double.parseDouble(simPair[1]); // 输出keymovie_i, valuesim|movie_j用 | 分隔便于 Reduce 解析 context.write(new Text(movieI), new Text(sim | movieJ)); } } // TopNReducer.java —— 利用 Text 的字典序使高相似度排前面 public class TopNReducer extends ReducerText, Text, Text, Text { Override protected void reduce(Text key, IterableText values, Context context) throws IOException, InterruptedException { ListString candidates new ArrayList(); for (Text val : values) { candidates.add(val.toString()); } // 按相似度降序字符串排序需补零如 0.9999 0.999 candidates.sort((a, b) - { double sa Double.parseDouble(a.split(\\|)[0]); double sb Double.parseDouble(b.split(\\|)[0]); return Double.compare(sb, sa); // 降序 }); // 取 Top10 StringBuilder topN new StringBuilder(); for (int i 0; i Math.min(10, candidates.size()); i) { if (i 0) topN.append(,); topN.append(candidates.get(i).split(\\|)[1]); // 只取 movie_j } context.write(key, new Text(topN.toString())); } }逻辑说明Secondary Sort 本质是让 Mapper 输出的 key 包含排序字段此处用sim|movie_j但 Hadoop 默认按整个 Text 排序所以需确保相似度字符串长度一致String.format(%06.4f, sim)更稳妥Double.compare(sb, sa)是安全的降序比较避免sb - sa浮点误差。5. SpringBoot 服务集成HBase 结果缓存 REST API 设计避坑重点SpringBoot 版本与 Hadoop 生态兼容性。本项目用 SpringBoot 2.7.18停更但最稳绝不用 3.x—— SpringBoot 3.x 默认 Jakarta EE 9而hbase-client 2.4.15仍依赖javax.*包直接 ClassNotFound。5.1 HBase 表设计RowKey 优化与列族精简# HBase Shell 创建表执行前确保 HBase 已启动 create movie_recommend, {NAME cf, TTL 2592000} # TTL30天自动清理过期推荐RowKey 设计说明示例user_id直接用用户 ID 作 RowKeyu_123456789列限定符cf:top10存储 JSON 字符串[1001,1005,1023,...]列限定符cf:timestamp写入时间戳毫秒用于判断是否需刷新逻辑说明HBase 行键设计原则是查询导向本场景 99% 请求是GET /recommend/{user_id}故 RowKey user_id最优TTL259200030天避免冷数据堆积cf:top10不拆分为多个列如cf:rec1,cf:rec2因 Top-N 数量固定且需原子更新。5.2 SpringBoot 配置HBase 连接池与超时控制# application.yml spring: hbase: zookeeper: quorum: localhost client-port: 2181 # HBase 2.4 默认使用 async client但 SpringBoot 2.7.x 需显式指定 connection: implementation: sync max-retries: 3 retry-sleep: 1000 timeout: 10000 # 10秒超时防 HBase hang 住整个服务 pool: max-total: 20 max-idle: 10 min-idle: 5参数说明connection.implementation: sync是关键HBase 2.4 的异步客户端AsyncConnection与 SpringBoot 2.7 的HBaseTemplate不兼容timeout: 10000必须设否则网络抖动时接口线程卡死连接池max-total20按 QPS 估算假设 100 QPS平均响应 200ms则需约 20 并发连接。5.3 REST Controller兜底策略与缓存穿透防护RestController RequestMapping(/api/recommend) public class RecommendController { Autowired private RecommendService recommendService; GetMapping(/{userId}) public ResponseEntityRecommendResponse getRecommend(PathVariable String userId) { try { // 1. 先查 HBase 缓存 ListString recList recommendService.getFromHBase(userId); if (recList ! null !recList.isEmpty()) { return ResponseEntity.ok(new RecommendResponse(recList)); } // 2. 缓存未命中触发降级返回热门电影MySQL 查询 ListString hotList recommendService.getHotMovies(10); // 3. 异步刷新 HBase避免阻塞主链路 CompletableFuture.runAsync(() - { recommendService.refreshHBase(userId, hotList); }); return ResponseEntity.ok(new RecommendResponse(hotList)); } catch (Exception e) { // 4. 兜底返回空列表不抛异常避免雪崩 log.error(Recommend failed for user {}, userId, e); return ResponseEntity.ok(new RecommendResponse(Collections.emptyList())); } } }逻辑说明CompletableFuture.runAsync()是核心——HBase 写入慢平均 50ms若同步等待会导致接口 P99 延迟飙升getHotMovies()从 MySQL 查ORDER BY vote_count DESC LIMIT 10热榜表加了vote_count索引兜底返回空列表而非 500是线上服务铁律。6. 避坑指南5 个让项目卡在验收前的真实翻车点6.1 现象HDFS 启动后jps看不到 DataNodelogs/hadoop-hadoop-datanode-*.log报java.io.IOException: All directories in dfs.datanode.data.dir are invalid原因dfs.datanode.data.dir目录权限不是当前用户或磁盘空间不足Hadoop 默认要求剩余空间 10%。解决sudo chown -R $USER:$USER /usr/local/hadoop/hdfs/datanodedf -h查磁盘清理/tmp或~/.cache检查hdfs-site.xml中路径末尾不能有斜杠如/path/to/data/错/path/to/data对。6.2 现象Spider 爬取豆瓣时大量403 Forbidden但浏览器能正常访问原因豆瓣校验Cookie中的__utmz和__utma且有效期短2小时Scrapy 默认不维护 Cookie。解决改用scrapy-splash渲染 JS或在start_requests中手动注入 Cookie从浏览器复制yield Request(url, cookies{__utmz: xxx, __utma: yyy}, callbackself.parse)血泪经验曾因忽略 Cookie爬虫跑 3 天只拿到 200 条数据重放抓包才定位。6.3 现象MapReduce Job 2共现运行到 90% 时失败日志报java.lang.OutOfMemoryError: Java heap space原因Mapper 输出键值对过多如某用户行为 500 条组合数达 12.4 万JVM 堆内存不足。解决调大 Mapper 堆内存mapred.map.child.java.opts-Xmx2g在mapred-site.xml中更优在 Mapper 中增加maxPairs50剪枝见 4.1 节比调内存更治本。6.4 现象SpringBoot 启动报java.lang.NoClassDefFoundError: javax/annotation/PostConstruct原因SpringBoot 2.7.x 依赖javax.annotation-api但 JDK 11 移除了该包。解决在pom.xml显式添加dependency groupIdjavax.annotation/groupId artifactIdjavax.annotation-api/artifactId version1.3.2/version /dependency6.5 现象HBase 查询get movie_recommend, u_123返回COLUMNcf:top10, timestamp..., valuevalue 为空原因HBase 写入时用了Put.addColumn()但未调用table.put(put)或table连接未close()导致缓冲未刷盘。解决检查 HBase 写入代码确保try-with-resourcestry (Table table connection.getTable(TableName.valueOf(movie_recommend))) { Put put new Put(Bytes.toBytes(u_123)); put.addColumn(Bytes.toBytes(cf), Bytes.toBytes(top10), Bytes.toBytes([\1001\,\1005\])); table.put(put); // 必须调用 }7. 验证与调优用真实数据跑通全流程的 3 个硬指标做完以上步骤别急着交差。我习惯用这 3 个硬指标验证是否真跑通7.1 数据链路完整性验证5 分钟步骤命令预期输出说明1. 爬虫数据落 HDFShdfs dfs -ls /raw/movies/Found 1 items大小 1MB确认 Spider 成功写入2. 共现矩阵生成hdfs dfs -cat /output/cooccur/part-r-00000 | head -51001,1005 12类格式MapReduce Job 2 输出正常3. HBase 结果可查echo get movie_recommend, u_123 | hbase shell -nCOLUMNcf:top10, value[1001,1005]端到端数据贯通提示hbase shell -n的-n参数静默模式避免 ANSI 转义符污染输出。7.2 推荐质量基线测试20 分钟用 Python 脚本抽样 100 个用户对比 HBase 推荐结果与人工规则# test_quality.py import happybase connection happybase.Connection(localhost) table connection.table(movie_recommend) # 抽样 100 个用户 users [fu_{i} for i in range(100, 200)] valid_rec 0 for user in users: row table.row(user.encode()) if bcf:top10 in row: recs json.loads(row[bcf:top10].decode()) if len(recs) 10 and all(isinstance(x, str) for x in recs): valid_rec 1 print(fValid recommendation rate: {valid_rec/100:.0%}) # 应 ≥ 95%达标线有效推荐率 ≥ 95%即 100 个用户中至少 95 个有完整 10 条推荐。低于此值检查 MapReduce Job 3 的 Top-N 逻辑或 HBase 写入异常。7.3 接口性能压测JMeter 脚本要点线程组100 线程Ramp-up 10 秒循环 10 次 → 总 1000 请求HTTP 请求GET http://localhost:8080/api/recommend/u_${__Random(1000,9999)}断言响应码 200 JSON 中data字段存在查看结果树重点关注90% Line ≤ 200ms错误率 0%翻车预警若 P90 500ms优先查 HBase 连接池max-total是否过小或 HDFS NameNode GC 日志logs/hadoop-*-namenode-*.log中频繁 Full GC。最后说句实在话这个项目的价值不在“用了 Hadoop”而在把大数据组件当工具用而不是供起来。我见过太多团队花 3 个月搭集群结果发现业务根本不需要 PB 级吞吐——反而用 HDFS 存原始日志、MapReduce 做 ETL、SpringBoot 托底服务两周就上线了可用推荐。技术选型不是炫技是让业务跑得更快、更稳。希望帮到你。本文还有配套的精品资源点击获取
返回列表