戰(zhàn):廚具銷售數(shù)據(jù)分析與可視化大屏全鏈路)
做大數(shù)據(jù)方向的項目很多人第一反應(yīng)是先把 Hadoop、Spark 裝好然后發(fā)現(xiàn)真正難的從來不是安裝而是把從數(shù)據(jù)到展示的整條鏈路跑通。最近我在整理一套電商平臺廚具類目的銷售數(shù)據(jù)分析系統(tǒng)技術(shù)棧是 Hadoop Spark Hive Django最后用可視化大屏把結(jié)果呈現(xiàn)出來。這個組合在很多教學(xué)項目和實(shí)際業(yè)務(wù)里都很典型但網(wǎng)上能搜到的基本都是單點(diǎn)教程——單獨(dú)講 Spark 優(yōu)化、單獨(dú)講 Django 接口、單獨(dú)講大屏模板很少有人把 Hive 表怎么建、Spark 任務(wù)怎么跑、Django 接口怎么給大屏供數(shù)這條鏈路完整串起來也沒有人講調(diào)試時最容易在哪個環(huán)節(jié)浪費(fèi)半天。這篇文章會把整個項目的實(shí)現(xiàn)邏輯和我在調(diào)試中踩過的坑完整整理出來適合準(zhǔn)備做大數(shù)據(jù)課程設(shè)計、以及剛接觸數(shù)據(jù)倉庫和可視化開發(fā)的人參考。我會從選型理由講起再按數(shù)據(jù)準(zhǔn)備、分析計算、接口對接、調(diào)試排障、項目交付的順序往下走最后給一些擴(kuò)展建議。內(nèi)容不求面面俱到但每個環(huán)節(jié)都會給出能直接落地的細(xì)節(jié)。1. 為什么這套組合值得做HadoopSparkHiveDjango 的分工與適用邊界1.1 廚具類目數(shù)據(jù)量不大為什么還要上 Hadoop我自己也糾結(jié)過這個問題一個廚具品類的訂單明細(xì)撐死一年也就幾百萬到兩千萬行單獨(dú)拿 Pandas 也能跑分析為什么還要引入 Hadoop 這一套原因有兩個。第一個是數(shù)據(jù)形態(tài)不只是訂單明細(xì)還有商品維表、品牌維表、區(qū)域維表、評價流水join 起來以后中間結(jié)果會膨脹第二個是當(dāng)你要做的不是一次性報表而是可重復(fù)運(yùn)行的“分析系統(tǒng)”就必須有統(tǒng)一的存儲、統(tǒng)一的調(diào)度入口和穩(wěn)定的數(shù)據(jù)產(chǎn)出。Hive 先把數(shù)據(jù)倉庫的分層和元數(shù)據(jù)管理做好Spark 接著跑復(fù)雜的清洗和聚合最后 Django 面向展示層提供接口整條鏈路各管一段后面接手的人也能按層維護(hù)。這個設(shè)計不是為了顯得高大上而是為了“好維護(hù)、好替換”。如果你只是臨時分析一張小表直接讀 CSV 當(dāng)然更快。但項目一旦掛上“系統(tǒng)”兩個字就意味著每天要跑、每周要更新、不同角色要依賴它做決策。這個時候數(shù)據(jù)分層、任務(wù)調(diào)度、結(jié)果落庫、接口輸出就不是可有可無的優(yōu)化項而是基本要求。Hadoop 在這套鏈路里承擔(dān)的其實(shí)是兩件事一是把原始文件安全地放到 HDFS 上二是給 Hive 提供分布式存儲底座Hive 負(fù)責(zé)把文件變成“能寫 SQL 的表”Spark 負(fù)責(zé)把復(fù)雜計算跑得足夠快Django 只負(fù)責(zé)把最終結(jié)果以接口形式送出去。1.2 各組件邊界別讓 Django 去算數(shù)也別讓 Hive 去實(shí)時響應(yīng)我在很多類似項目里看到的最常見問題是組件職責(zé)混亂。比如有人把大屏需要的數(shù)據(jù)直接用 Django 實(shí)時查 Hive前端點(diǎn)一次刷新后端就去執(zhí)行一個分布式 SQL運(yùn)氣好幾秒返回運(yùn)氣不好幾十秒頁面一直轉(zhuǎn)圈。這種寫法在演示時還能忍真正連續(xù)點(diǎn)起來就會把 HiveServer2 的連接池打滿。組件職責(zé)邊界常見誤區(qū)HDFS原始文件與中間結(jié)果存儲把每張臨時表都存一份垃圾數(shù)據(jù)不清Hive元數(shù)據(jù)管理、分層寬表、離線 SQL 分析讓 Hive 支撐秒級在線查詢Spark復(fù)雜 ETL、大規(guī)模 Join、聚合、重分區(qū)全鏈路都寫成 Spark 程序連小表也開集群Django讀結(jié)果庫、輸出 JSON、控制權(quán)限在 Django 里直接跑 Hive SQL大屏展示趨勢、占比、地圖大屏直接連 HiveServer2正確的分工是Spark 算完結(jié)果寫成 MySQL 里的聚合結(jié)果表Django 只讀 MySQL把結(jié)果轉(zhuǎn)成 JSON大屏只負(fù)責(zé) fetch 這些 JSON 并繪圖。MySQL 里存的不是原始流水而是按天、按類目、按品牌匯總好的結(jié)果所以查詢量很小響應(yīng)速度才能穩(wěn)定在幾十毫秒級別。2. 廚具銷售數(shù)據(jù)如何變成 Hive 里可用的事實(shí)表2.1 數(shù)據(jù)來源與字段清單這套項目里沒有真實(shí)生產(chǎn)環(huán)境的數(shù)據(jù)授權(quán)我使用的是按電商后臺導(dǎo)出格式模擬的離線數(shù)據(jù)集字段結(jié)構(gòu)盡量貼近常見場景。如果你有平臺開放接口或脫敏離線包處理流程完全一樣只是字段名需要對齊。我建議不要為了“真實(shí)”去抓取未經(jīng)授權(quán)的大規(guī)模頁面數(shù)據(jù)重點(diǎn)放在數(shù)據(jù)建模和分析鏈路上更有價值。廚具銷售明細(xì)的核心字段大致如下字段名類型說明order_idSTRING訂單編號業(yè)務(wù)上的唯一鍵product_idSTRING商品編號product_nameSTRING商品標(biāo)題category_l1STRING一級類目比如鍋具、儲物、餐具category_l2STRING二級類目比如不粘鍋、保鮮盒brand_nameSTRING品牌名priceDECIMAL(10,2)銷售標(biāo)價pay_amountDECIMAL(10,2)實(shí)際支付金額quantityINT購買數(shù)量provinceSTRING收貨省份citySTRING收貨城市order_timeSTRING下單時間star_levelINT買家評價星級廚具類目有個特點(diǎn)客單價波動很大價格帶從幾十塊的調(diào)味盒到幾千塊的炒鍋都有而且消費(fèi)者購買頻次低很多人是搬新家或者裝修廚房才會集中采購。所以分析時不能只盯著銷量還要關(guān)注客單價、價格帶結(jié)構(gòu)、地域分布和跨品類的連帶購買。2.2 分層建表 DDL 與分區(qū)設(shè)計我的習(xí)慣是第一層 ODS 只做拷貝原字段長什么樣就什么樣第二層 DWD 做類型轉(zhuǎn)換、去重、空值補(bǔ)齊第三層 DWS 按維度輕度匯總最后一層 ADS 直接把給大屏展示的聚合結(jié)果落庫。這里貼一張 DWD 層的 Hive 建表腳本重點(diǎn)是分區(qū)和存儲格式CREATE TABLE IF NOT EXISTS dwd_kitchen_sale_detail ( order_id STRING, product_id STRING, product_name STRING, category_l1 STRING, category_l2 STRING, brand_name STRING, price DECIMAL(10,2), pay_amount DECIMAL(10,2), quantity INT, province STRING, city STRING, order_time STRING ) PARTITIONED BY (dt STRING) STORED AS ORC;為什么按天分區(qū)因為離線任務(wù)基本都是每天增量灌入按天分區(qū)可以讓每次寫入只影響當(dāng)天數(shù)據(jù)歷史分區(qū)不用動。為什么用 ORCORC 是列式存儲做聚合時只讀取需要的列I/O 會小很多。對于廚具這個量級的數(shù)據(jù)按天分區(qū)已經(jīng)足夠不需要再加二級分區(qū)。如果單日數(shù)據(jù)量真的到了千萬行以上再考慮在分區(qū)字段上加 hash 分桶不要一開始就過度設(shè)計。2.3 Spark 清洗不能跳過的三個環(huán)節(jié)數(shù)據(jù)從 ODS 到 DWD至少要過三關(guān)。第一關(guān)是去重同一個訂單可能因為數(shù)據(jù)回溯或者重復(fù)同步出現(xiàn)多次用訂單號做去重保留最后一條第二關(guān)是空值處理品牌為空補(bǔ)“未知”價格為零或者是負(fù)數(shù)直接剔除坪效和客單價指標(biāo)最容易被臟數(shù)據(jù)帶偏第三關(guān)是日期規(guī)范化把原始時間字符串轉(zhuǎn)成標(biāo)準(zhǔn)格式保證后面按天聚合不會出現(xiàn)“2024/1/1”和“2024-01-01”同時存在的情況。df spark.sql(SELECT * FROM ods_kitchen_sale WHERE dt 2024-01-01) df df.dropDuplicates([order_id]) df df.filter(df.pay_amount 0) df df.withColumn(dt, F.to_date(F.col(order_time), yyyy-MM-dd HH:mm:ss)) df.write.mode(overwrite).format(hive).saveAsTable( dwd_kitchen_sale_detail, partitionBydt )為什么清洗邏輯放在 Spark 而不是完全依賴 Hive因為 Hive 執(zhí)行 SQL 也是一樣能做的但 Spark 可以先把多張源表并行讀進(jìn)來通過 DataFrame 緩存復(fù)用中間結(jié)果寫復(fù)雜 ETL 時代碼更直觀。更重要的是清洗和后續(xù)聚合可以放在同一個 Spark 工程里避免多個引擎來回切換。3. 分析層實(shí)戰(zhàn)廚具銷售的核心指標(biāo)與 Spark 計算細(xì)節(jié)3.1 指標(biāo)清單要跟大屏一一對應(yīng)大屏不是把一堆圖表堆上去就完事指標(biāo)設(shè)計會直接決定數(shù)據(jù)模型的粗粒度。我在這個項目里的指標(biāo)清單是這樣拆的KPI 卡片總銷售額、總訂單量、客單價、銷量同比趨勢線按天/按周的銷售額和訂單量變化類目結(jié)構(gòu)一級類目銷售額占比鍋具、餐具、收納各自占多少品牌榜Top 10 品牌的銷量和銷售額地域分布分省銷售額熱力地圖服務(wù)體驗退款率和好評率廚具是低頻高客單品類單純看銷量容易失真。兩個品牌可能在銷量上差不多但一個靠幾十塊的保鮮盒沖量另一個靠上千塊的琺瑯鍋賺錢后者的商業(yè)價值明顯更高。所以每個指標(biāo)的背后最好都掛一個“業(yè)務(wù)問題”而不是為了填滿大屏格子硬算。3.2 一個典型的 Spark SQL品牌價格帶分析品牌榜是最基礎(chǔ)的分析真正能說明問題的是疊加價格帶之后的品牌結(jié)構(gòu)。下面這段 Spark SQL 就是按品牌和價格檔位計算訂單數(shù)與銷售額SELECT brand_name, CASE WHEN pay_amount 50 THEN 低客單 WHEN pay_amount 200 THEN 中客單 ELSE 高客單 END AS price_level, COUNT(DISTINCT order_id) AS order_cnt, SUM(quantity) AS sales_cnt, ROUND(AVG(pay_amount), 2) AS avg_order_value FROM dwd_kitchen_sale_detail WHERE dt 2024-01-01 GROUP BY brand_name, CASE WHEN pay_amount 50 THEN 低客單 WHEN pay_amount 200 THEN 中客單 ELSE 高客單 END;大屏上的品牌榜只是這個 SQL 去掉價格帶后的簡化版。加價格帶之后可以很清楚地看到某個品牌是靠低價引流還是靠高客單品帶動銷售這對運(yùn)營選品和投放策略都有參考價值。如果數(shù)據(jù)量很大先把價格帶和銷量做一次預(yù)聚合再去 join 品牌維表可以明顯減少 shuffle 數(shù)據(jù)量。3.3 任務(wù)調(diào)度與結(jié)果回寫分析任務(wù)不能每次手動跑。我會把整段分析邏輯寫成一個analysis_job.py每天凌晨用調(diào)度平臺或 cron 觸發(fā)一次。關(guān)鍵是把結(jié)果按天分區(qū)覆蓋寫避免大屏看到過期或者重復(fù)的數(shù)據(jù)。Spark 任務(wù)的提交命令長這樣spark-submit \ --master yarn \ --deploy-mode cluster \ --num-executors 4 \ --executor-cores 4 \ --executor-memory 6g \ --driver-memory 2g \ --conf spark.sql.shuffle.partitions200 \ analysis_job.py參數(shù)不要直接抄網(wǎng)上的。executor 數(shù)量和內(nèi)存取決于你實(shí)際集群的資源。我見過太多人把 executor memory 寫到 16g但 Yarn 隊列里根本沒那么多資源結(jié)果任務(wù)一直排隊或者被殺死。先用默認(rèn)參數(shù)跑通再根據(jù) Spark UI 里的運(yùn)行時間和數(shù)據(jù)量逐步調(diào)整。3.4 處理數(shù)據(jù)傾斜的小經(jīng)驗按品牌或區(qū)域聚合時最容易遇到數(shù)據(jù)傾斜幾個爆款品牌占了大部分?jǐn)?shù)據(jù)導(dǎo)致某個 executor 處理的數(shù)據(jù)量是其他人的好幾倍整體任務(wù)被拖慢。最簡單的處理辦法是先把熱點(diǎn)數(shù)據(jù)識別出來單獨(dú)計算再和非熱點(diǎn)結(jié)果合并。更通用的做法是給 join 鍵加上一個隨機(jī)后綴做“打散”把一個大 key 拆成多個小 key最后再按真實(shí) key 聚合。對于廚具這個數(shù)據(jù)量先看指標(biāo)結(jié)果是否符合預(yù)期再決定要不要做這些復(fù)雜度較高的優(yōu)化。4. Django 接口與大屏數(shù)據(jù)對接讓分析結(jié)果真正被“看見”4.1 接口設(shè)計直接按大屏組件拆Django 端的接口不要設(shè)計成一個“萬能查詢接口”。最省事的方式是前端大屏有幾個組件后端就提供幾個對應(yīng)接口每個接口只返回一個圖表所需的數(shù)據(jù)。這個項目的接口大致是這樣接口路徑返回內(nèi)容/api/v1/overview/kpi總銷售額、總訂單量、客單價/api/v1/overview/trend每日銷售額和訂單量列表/api/v1/category/ratio各一級類目銷售額占比/api/v1/brand/topTop10 品牌銷量與銷售額/api/v1/region/heat各省銷售額熱力數(shù)據(jù)/api/v1/review/cloud評價關(guān)鍵詞統(tǒng)計統(tǒng)一返回格式{code: 0, data: ...}數(shù)據(jù)異常時 code 非 0前端好做統(tǒng)一判斷。Django 視圖層寫起來非常輕下面是其中一個接口的示例from django.http import JsonResponse from django.core.cache import cache def sale_trend(request): data cache.get(sale_trend) if data is None: data list( ResultTrend.objects.order_by(dt).values(dt, sales) ) cache.set(sale_trend, data, 600) return JsonResponse({code: 0, data: data})大屏頁面通常幾秒輪詢一次如果每次請求都打 MySQL即使結(jié)果表只有幾百行壓力也不大但加上緩存更穩(wěn)妥而且能避免大屏多個圖表同時刷新時造成短時尖峰。4.2 大屏端怎么接數(shù)據(jù)大屏我用的是 ECharts工程結(jié)構(gòu)很簡單一個 index.html一個 api.js一個 charts.js。前端從接口拿到數(shù)據(jù)之后只需要把數(shù)據(jù)映射到圖表 option 里。比如趨勢圖的靈魂代碼就這幾行async function loadTrend() { const res await fetch(/api/v1/overview/trend); const json await res.json(); trendChart.setOption({ xAxis: { data: json.data.map(i i.dt) }, series: [{ data: json.data.map(i i.sales) }] }); }有一點(diǎn)要注意大屏上顯示的標(biāo)題、時間范圍、單位最好都從接口或者統(tǒng)一配置里讀取不要寫死在圖表代碼里。廚具銷售數(shù)據(jù)經(jīng)常要按周、按月切換寫死之后每次改都要翻代碼維護(hù)成本很高。4.3 為什么要在 Django 和 Hive 之間加一個結(jié)果庫這是整套架構(gòu)里最關(guān)鍵的一步。Spark 分析完幾千萬行明細(xì)后真正給人看的聚合結(jié)果往往只有幾百行。把這些結(jié)果寫進(jìn) MySQL 的一張結(jié)果表Django 每次訪問只查詢幾百行響應(yīng)速度可以穩(wěn)定在幾十毫秒。如果直接把 HiveServer2 暴露給 Django隨便一次刷新都會觸發(fā)一個分布式查詢而且 HiveServer2 還有并發(fā)連接數(shù)限制大屏輪詢一多就到了瓶頸。結(jié)果庫的清洗也很重要每天 Spark 任務(wù)跑完后先用TRUNCATE或者按日期刪除舊數(shù)據(jù)再寫入新結(jié)果避免昨天和今天的數(shù)據(jù)混在一起。大屏上的數(shù)值出現(xiàn)莫名其妙的跳變十有八九是因為沒有做覆蓋寫。5. 踩坑記錄環(huán)境、資源、接口三層最容易翻車的地方5.1 SparkSQL 連 Hive 報“元數(shù)據(jù)初始化失敗”這個錯幾乎每個第一次跑 Spark on Hive 的人都會遇到?,F(xiàn)象是程序一提交就報MetaException或者Could not connect to MetaStore。排查鏈路我一般是這樣走的先確認(rèn)hive-site.xml、core-site.xml、hdfs-site.xml是否在 Spark 任務(wù)的 classpath 里再確認(rèn) Hive Metastore 進(jìn)程是否真的啟動了hive --service metastore不能是只在某個終端里跑了一下就掛了然后看 Metastore 連接 MySQL 的賬號密碼和權(quán)限有沒有問題最后檢查spark.sql.warehouse.dir對應(yīng)的 HDFS 目錄是否存在、當(dāng)前用戶有沒有讀寫權(quán)限。本地模式跑 SparkSQL 讀集群 Hive 時最容易踩的是把一套單機(jī)版的配置文件放到 classpath 里導(dǎo)致 Spark 拿著 localhost 去找集群的 NameNode 和 Metastore結(jié)果當(dāng)然是一堆 Connection refused。先用bin/spark-sql --conf spark.sql.hive.metastore.jarsbuiltin做一個最小化驗證再回到完整工程里排錯效率會高很多。5.2 Yarn 作業(yè)被容器殺掉物理內(nèi)存超限另一個高頻問題是在集群模式提交 Spark 任務(wù)后日志里出現(xiàn)Container is running beyond physical memory limits。很多人的第一反應(yīng)是加內(nèi)存但這樣往往治標(biāo)不治本。先看 Spark UI 中各個 stage 的 Shuffle Read 和 Input 大小如果某個 stage 的輸入特別大說明分區(qū)數(shù)太少或者數(shù)據(jù)傾斜加內(nèi)存只是讓問題晚一點(diǎn)爆發(fā)。如果確認(rèn)只是 Yarn 容器預(yù)留的內(nèi)存不夠應(yīng)該調(diào)整spark.executor.memoryOverhead給容器預(yù)留出堆外內(nèi)存和基礎(chǔ)進(jìn)程的開銷。比如 executor memory 設(shè) 6g 的時候memoryOverhead 至少也要 1g 到 2g。另外不要把spark.sql.shuffle.partitions隨便設(shè)成 1000分區(qū)數(shù)量過多會讓每個 executor 承擔(dān)大量的小 task任務(wù)調(diào)度開銷比計算本身還大。5.3 Django 接口返回空了但 MySQL 里有數(shù)這類問題最讓人崩潰因為數(shù)據(jù)明明在接口卻查不到。我排查一圈之后發(fā)現(xiàn)基本都是三個原因第一Django ORM 的模型字段名和 MySQL 列名對不上比如日期字段叫dt模型里寫了date第二從 MySQL 讀出的Decimal和datetime類型沒有轉(zhuǎn)成 JSON 可序列化的基本類型第三緩存里存了上一次的空結(jié)果沒有及時清掉。解決思路也很直接給 ORM 模型字段名和 MySQL 列名做一個統(tǒng)一約定所有日期字段統(tǒng)一用字符串返回接口層加一個統(tǒng)一的 JSON 序列化函數(shù)把Decimal、datetime、date都轉(zhuǎn)成字符串或浮點(diǎn)數(shù)開發(fā)環(huán)境在重啟 Django 服務(wù)后執(zhí)行一次cache.clear()避免被舊緩存干擾判斷。5.4 時間都花在版本不匹配上如果項目從零搭版本兼容性是決定調(diào)試效率的關(guān)鍵。我的經(jīng)驗是盡量選一整套主版本配好的組合避免自己亂配。組件推薦版本說明Hadoop3.3.x穩(wěn)定生態(tài)兼容好Hive3.1.xSpark 集成最常用版本Spark3.3.x對 Hive 3.1 支持成熟Django4.2.x使用 LTS 版本更省心Python3.8 或 3.10不要用最新大版本MySQL8.0Hive 元數(shù)據(jù)和結(jié)果庫通用JDK 我建議用 JDK 8 或 11。JDK 17 和舊版 Hive 一起跑容易遇到非法反射訪問的告警雖然不一定直接報錯但會把日志刷得很亂影響排障判斷。6. 把整套系統(tǒng)變成能交付的“源碼文檔調(diào)試記錄”6.1 工程目錄怎么組織別人拿到能少問十句話很多人交付項目只給一團(tuán)代碼別人拿到之后根本不知道從哪里下手。我習(xí)慣把工程按層拆開讓數(shù)據(jù)流一眼能看出來sale-analysis/ ├── sql/ │ ├── 01_ods_ddl.sql │ ├── 02_dwd_ddl.sql │ └── 03_ads_ddl.sql ├── spark/ │ ├── analysis_job.py │ ├── config.py │ └── requirements.txt ├── django/ │ ├── manage.py │ ├── sale_api/ │ └── result_models.py ├── dashboard/ │ ├── index.html │ ├── api.js │ └── charts.js └── docs/ ├── 環(huán)境部署.md ├── 數(shù)據(jù)導(dǎo)入.md └── 調(diào)試FAQ.mdSQL 腳本放在一起是因為數(shù)據(jù)庫表結(jié)構(gòu)是整個系統(tǒng)的地基。Spark 工程負(fù)責(zé)清洗和計算。Django 工程負(fù)責(zé)接口。dashboard 是大屏前端。docs 里放部署文檔和常見問題。別人拿到這套目錄之后最多問“啟動命令是什么”不會再去問“這張表在哪建的”“這個圖表的數(shù)據(jù)從哪來”。6.2 文檔要覆蓋的三件事第一件事是環(huán)境版本矩陣。把操作系統(tǒng)、JDK、Python、Hadoop、Hive、Spark、Django、MySQL 的版本全部列出來避免接手的人裝了一套完全不同的版本跑出各種詭異問題。第二件事是啟動順序。HDFS、Yarn、Hive Metastore、Spark 任務(wù)、Django 服務(wù)每一步的啟動命令和驗證方法都要寫清楚。第三件事是數(shù)據(jù)初始化和自查方法。比如用SHOW PARTITIONS dwd_kitchen_sale_detail看分區(qū)有沒有生成用SELECT COUNT(*) FROM dwd_kitchen_sale_detail WHERE dt2024-01-01看當(dāng)天數(shù)據(jù)量是否正常。調(diào)試記錄不要只寫最終答案。最有價值的寫法是“報錯關(guān)鍵字 復(fù)現(xiàn)命令 排查路徑 最終解決方式”。我自己調(diào)試的時候會把一段卡了兩個小時的報錯記錄成模板下次再遇到同類問題直接在文檔里搜索關(guān)鍵字比重新從零查一遍快得多。6.3 這個系統(tǒng)還能往哪里擴(kuò)展如果有人打算繼續(xù)做二期我建議從幾個方向入手。任務(wù)調(diào)度可以替換成正式的調(diào)度平臺加失敗重試、告警和補(bǔ)數(shù)邏輯結(jié)果存儲可以換成 ClickHouse 或 Doris讓大屏支持任意時間段拖選不用每次重新觸發(fā)離線任務(wù)實(shí)時性要求上來之后可以在訂單鏈路前面加消息隊列把分鐘級實(shí)時趨勢和離線分析互補(bǔ)如果想做更完整的用戶分析就把加購、收藏、瀏覽日志也接進(jìn)來做轉(zhuǎn)化漏斗和連帶購買分析。最后說一個我自己的小習(xí)慣每次改完 Spark 邏輯我會先清掉 Django 緩存再刷新大屏頁面確認(rèn)數(shù)據(jù)變化避免被舊數(shù)據(jù)誤導(dǎo)。整套系統(tǒng)做完之后比起某個算法寫得多漂亮更讓我省心的是分層清晰、接口穩(wěn)定、調(diào)試記錄完整——這三樣才是這種數(shù)據(jù)系統(tǒng)能長期跑下去的關(guān)鍵。