[{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/en/","section":"","summary":"","title":"","type":"en"},{"content":"\r关于此博客\r#\r个人技术博客。在这里我主要记录：\n项目日志：我构建了什么，以及如何构建 技术笔记：我学到的新知识，以及希望记住的要点 想法：值得落笔记录的零思碎想 关于我\r#\r软件工程师、数据分析工程师。对分布式系统、AI、开发者工具和开源项目充满兴趣。\n技术栈\r#\r写作工具：Obsidian 静态站点：Hugo + Blowfish 托管服务：阿里云 ECS + Caddy 部署流程：Gitee Webhook → 自动同步 ","date":"May 29, 2026","externalUrl":null,"permalink":"/about/","section":"李先生的树洞","summary":"关于此博客\r#\r个人技术博客。在这里我主要记录：\n项目日志：我构建了什么，以及如何构建 技术笔记：我学到的新知识，以及希望记住的要点 想法：值得落笔记录的零思碎想 关于我\r#\r软件工程师、数据分析工程师。对分布式系统、AI、开发者工具和开源项目充满兴趣。\n技术栈\r#\r写作工具：Obsidian 静态站点：Hugo + Blowfish 托管服务：阿里云 ECS + Caddy 部署流程：Gitee Webhook → 自动同步 ","title":"About","type":"page"},{"content":"\rAbout This Blog\r#\rPersonal technical blog. Here I document:\nProject logs: What I build and how Tech notes: Things I learn and want to remember Ideas: Random thoughts worth writing down About Me\r#\rSoftware engineer. Interested in distributed systems, developer tools, and open source.\nTech Stack\r#\rWriting: Obsidian Static Site: Hugo + Blowfish Hosting: Alibaba Cloud ECS + Caddy Deploy: Direct scp via Windows batch script ","date":"May 29, 2026","externalUrl":null,"permalink":"/en/about/","section":"","summary":"About This Blog\r#\rPersonal technical blog. Here I document:\nProject logs: What I build and how Tech notes: Things I learn and want to remember Ideas: Random thoughts worth writing down About Me\r#\rSoftware engineer. Interested in distributed systems, developer tools, and open source.\n","title":"About","type":"en"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/als/","section":"Tags","summary":"","title":"ALS","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/ansible/","section":"Tags","summary":"","title":"Ansible","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/automation/","section":"Tags","summary":"","title":"Automation","type":"tags"},{"content":"\rBig Data Cluster Ansible Automated Deployment Tutorial\r#\rAn Ansible-based declarative automation deployment solution, coexisting with existing Shell/Python scripts, for 3-10 node clusters.\n1. Prerequisites\r#\r1.1 Control Node Requirements\r#\rRequirement Details OS Linux / macOS (Windows WSL compatible) Ansible \u0026gt;= 2.15 sshpass yum install sshpass (CentOS) or apt install sshpass (Ubuntu) Network SSH access to all cluster nodes 1.2 Cluster Node Requirements\r#\rRequirement Details OS CentOS 7.9+ / Rocky Linux 8+ SSH Enabled with password or key authentication Sudo hadoop user has sudo privileges Disk /opt/module at least 10GB free 1.3 Install Ansible\r#\r# CentOS / Rocky sudo yum install -y epel-release \u0026amp;\u0026amp; sudo yum install -y ansible # Ubuntu / Debian sudo apt update \u0026amp;\u0026amp; sudo apt install -y ansible # macOS brew install ansible # Verify ansible --version # Should show \u0026gt;= 2.15\r1.4 Pre-cache Component Archives (Recommended)\r#\rcd ansible/files/cache/ # Download all components (~1.8 GB total) wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/hadoop/common/hadoop-3.3.6/hadoop-3.3.6.tar.gz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/zookeeper-3.5.9/apache-zookeeper-3.5.9-bin.tar.gz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/hive/hive-3.1.3/apache-hive-3.1.3-bin.tar.gz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/hbase/2.6.4/hbase-2.6.4-hadoop3-bin.tar.gz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/kafka/2.8.0/kafka_2.13-2.8.0.tgz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/spark/spark-3.5.8/spark-3.5.8-bin-hadoop3-scala2.13.tgz wget -c https://maven.aliyun.com/repository/public/mysql/mysql-connector-java/5.1.49/mysql-connector-java-5.1.49.jar\rJDK should be pre-installed to /opt/module/jdk1.8.0_471 on all nodes, or can be auto-downloaded via the common role.\n2. Cluster Configuration\r#\r2.1 Edit Inventory\r#\rEdit ansible/inventory/hosts.yml with your node IPs and hostnames:\ncluster: hosts: node1: ansible_host: 192.168.137.155 # Master node IP hostname: hd2 # Master hostname node2: ansible_host: 192.168.137.156 hostname: hd3 node3: ansible_host: 192.168.137.157 hostname: hd4\r2.2 Modify Global Variables\r#\rEdit the all.vars section:\n# Network mode: \u0026#34;hostname\u0026#34; (hostname.domain) or \u0026#34;ip\u0026#34; (direct IP) network_mode: \u0026#34;hostname\u0026#34; domain_suffix: \u0026#34;pblh123.cn\u0026#34; # Your domain suffix # SSH credentials ansible_user: hadoop ansible_password: hadoop # Your password # MySQL (Hive Metastore) mysql: host: localhost database: hive_metastore user: hive password: \u0026#34;Hive@12345\u0026#34; # Your MySQL password\r3. Deployment\r#\r3.1 Pre-flight Check\r#\r# Test SSH connectivity ansible -i ansible/inventory/hosts.yml cluster -m ping # Expected: SUCCESS / \u0026#34;pong\u0026#34; for each node # Check node memory ansible -i ansible/inventory/hosts.yml cluster -m setup -a \u0026#34;filter=ansible_memtotal_mb\u0026#34;\r3.2 Step-by-Step (Recommended for First Use)\r#\rcd ansible/ # Step 1: Install components ansible-playbook -i inventory/hosts.yml playbooks/install.yml # Step 2: Distribute configs ansible-playbook -i inventory/hosts.yml playbooks/configure.yml # Step 3: Format HDFS and start services ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml --tags format,hdfs,zookeeper,yarn\r3.3 One-Click Deployment\r#\rcd ansible/ # Full deployment (with HDFS formatting) ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml # Re-deploy (skip HDFS format, preserve data) ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml --skip-tags format # Deploy specific component only ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml --tags hadoop,hive\r4. Cluster Management\r#\r4.1 Service Status\r#\ransible-playbook -i inventory/hosts.yml playbooks/status.yml\r4.2 Start / Stop / Restart\r#\r# Start all services ansible-playbook -i inventory/hosts.yml playbooks/start.yml # Stop all services ansible-playbook -i inventory/hosts.yml playbooks/stop.yml # Restart all services ansible-playbook -i inventory/hosts.yml playbooks/restart.yml # Start/stop individual service ansible-playbook -i inventory/hosts.yml playbooks/start.yml --tags kafka ansible-playbook -i inventory/hosts.yml playbooks/stop.yml --tags hbase\r4.3 Functional Verification (13 checks)\r#\ransible-playbook -i inventory/hosts.yml playbooks/verify.yml\r# Check Method 1 Node connectivity ping 2 HDFS read/write Write → Read → Verify → Delete 3 HDFS capacity hdfs dfsadmin -report 4 YARN nodes yarn node -list 5 ZooKeeper roles echo stat | nc localhost 2181 (each node) 6 Hive Metastore TCP 9083 7 HiveServer2 TCP 10000 8 HBase Shell echo \u0026quot;status\u0026quot; | hbase shell 9 Kafka E2E Create → Produce → Consume → Delete topic 10 Spark Pi spark-submit --class SparkPi (cluster mode) 11 Spark History TCP 18080 12 Web UIs (4x) HDFS / YARN / Spark / HBase HTTP 5. Web UIs\r#\rService URL HDFS NameNode http://\u0026lt;master_ip\u0026gt;:9870 YARN ResourceManager http://\u0026lt;master_ip\u0026gt;:8088 Spark History Server http://\u0026lt;master_ip\u0026gt;:18080 HBase Master http://\u0026lt;master_ip\u0026gt;:16010 6. Troubleshooting\r#\rSSH Connection Failed\r#\rssh hadoop@192.168.137.155 \u0026#34;hostname\u0026#34; sudo grep PasswordAuthentication /etc/ssh/sshd_config sudo sed -i \u0026#39;s/PasswordAuthentication no/PasswordAuthentication yes/\u0026#39; /etc/ssh/sshd_config sudo systemctl restart sshd\rPort Already In Use\r#\rsudo netstat -tlnp | grep \u0026lt;port\u0026gt; jps -l | grep -v Jps | awk \u0026#39;{print $1}\u0026#39; | xargs -r kill -9\rService Start Timeout\r#\rls /opt/module/ tail -100 /opt/module/hadoop-3.3.6/logs/*.log # Manual startup for debugging: source /etc/profile.d/hadoop_env.sh hdfs namenode\rAdding New Nodes\r#\rAdd node to inventory/hosts.yml Assign zk_myid and kafka_broker_id (increment) Run: ansible-playbook -i inventory/hosts.yml playbooks/configure.yml --limit \u0026lt;new_node\u0026gt; Restart affected services (ZooKeeper, HBase, Kafka) Slow Downloads\r#\r# Option 1: Pre-download to local cache (recommended) cd ansible/files/cache/ # Download all components via Chinese mirrors # Option 2: Change mirror source # Edit mirrors.apache in inventory/hosts.yml to use Aliyun mirror\r7. Relationship with Shell/Python Scripts\r#\rBoth solutions run independently:\nScenario Recommended Initial deploy, config consistency Ansible Quick start/stop, daily ops Shell (cluster_ctl.sh) Remote management Python (cluster_ctl.py) Fix config drift Ansible configure.yml Ansible and existing scripts share the same configuration semantics from config.ini, but use a separate inventory/hosts.yml variable file. Changes to configuration must be synchronized in both places.\n","date":"May 29, 2026","externalUrl":null,"permalink":"/en/posts/big-data-cluster-ansible-automated-deployment-tutorial/","section":"","summary":"Big Data Cluster Ansible Automated Deployment Tutorial\r#\rAn Ansible-based declarative automation deployment solution, coexisting with existing Shell/Python scripts, for 3-10 node clusters.\n1. Prerequisites\r#\r1.1 Control Node Requirements\r#\rRequirement Details OS Linux / macOS (Windows WSL compatible) Ansible \u003e= 2.15 sshpass yum install sshpass (CentOS) or apt install sshpass (Ubuntu) Network SSH access to all cluster nodes 1.2 Cluster Node Requirements\r#\rRequirement Details OS CentOS 7.9+ / Rocky Linux 8+ SSH Enabled with password or key authentication Sudo hadoop user has sudo privileges Disk /opt/module at least 10GB free 1.3 Install Ansible\r#\r# CentOS / Rocky sudo yum install -y epel-release \u0026\u0026 sudo yum install -y ansible # Ubuntu / Debian sudo apt update \u0026\u0026 sudo apt install -y ansible # macOS brew install ansible # Verify ansible --version # Should show \u003e= 2.15\r1.4 Pre-cache Component Archives (Recommended)\r#\rcd ansible/files/cache/ # Download all components (~1.8 GB total) wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/hadoop/common/hadoop-3.3.6/hadoop-3.3.6.tar.gz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/zookeeper-3.5.9/apache-zookeeper-3.5.9-bin.tar.gz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/hive/hive-3.1.3/apache-hive-3.1.3-bin.tar.gz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/hbase/2.6.4/hbase-2.6.4-hadoop3-bin.tar.gz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/kafka/2.8.0/kafka_2.13-2.8.0.tgz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/spark/spark-3.5.8/spark-3.5.8-bin-hadoop3-scala2.13.tgz wget -c https://maven.aliyun.com/repository/public/mysql/mysql-connector-java/5.1.49/mysql-connector-java-5.1.49.jar\rJDK should be pre-installed to /opt/module/jdk1.8.0_471 on all nodes, or can be auto-downloaded via the common role.\n","title":"Big Data Cluster Ansible Automated Deployment Tutorial","type":"en"},{"content":"\rBig Data Cluster Distributed Deployment Guide (Fully Verified)\r#\rA copy-paste-ready deployment guide for a complete big data cluster. Every configuration file is shown in full. All steps are verified on a 3-node CentOS 7.9 cluster.\nVariable Substitution Reference\r#\rWhen deploying to a new cluster, replace these values:\nVariable Tutorial Value Replace With Domain suffix pblh123.cn Your domain, or localdomain if none Node count 3 Actual node count Master node hd2 / 192.168.137.155 Your master hostname/IP Node 2 hd3 / 192.168.137.156 Your second node Node 3 hd4 / 192.168.137.157 Your third node Hadoop version hadoop-3.3.6 Actual version directory name JDK version jdk1.8.0_471 Actual JDK directory name HDFS replication 2 min(node_count, 3) MySQL password Hive@12345 Your MySQL hive user password Install root /opt/module Components install directory Data root /opt/data HDFS/ZK/Kafka data directory NM memory hd2:2800, hd3:2000, hd4:1200 70-80% of physical RAM (MB) SSH user hadoop Service user account SSH password hadoop Corresponding password Table of Contents\r#\rArchitecture Environment Setup MySQL Setup Hadoop HDFS YARN ZooKeeper Hive HBase Kafka Spark Verification Management One-Click Scripts Troubleshooting Appendix: New Cluster Checklist 1. Architecture\r#\r1.1 Topology\r#\r┌─────────────────────────────────────┐ │ hd2 (4G/2C) │ │ 192.168.137.155 │ │ NameNode, ResourceManager │ │ HBase Master, Hive Metastore │ │ HiveServer2, JobHistoryServer │ └──────────────┬──────────────────────┘ │ ┌─────────────────┼─────────────────┐ │ │ │ ┌────────┴────────┐ ┌──────┴──────┐ ┌───────┴───────┐ │ hd3 (3G/2C) │ │ hd4 (2G/2C) │ │ (scalable) │ │ DataNode, NM │ │ DataNode, NM│ │ │ │ SecondaryNN │ │ ZK, Kafka │ │ │ │ ZK, Kafka │ │ HBase RS │ │ │ │ HBase RS │ │ │ │ │ └─────────────────┘ └─────────────┘ └───────────────┘\r1.2 Service Assignment\r#\rService hd2 hd3 hd4 Notes NameNode Y HDFS metadata DataNode Y Y Y Data storage SecondaryNameNode Y Checkpoint merge ResourceManager Y YARN scheduling NodeManager Y Y Y Task execution JobHistoryServer Y MR history ZooKeeper Y Y Y Coordination Hive Metastore Y Metadata service HiveServer2 Y SQL query HBase Master Y Region management HBase RegionServer Y Y Y Data read/write Kafka Broker Y Y Y Message queue Spark HistoryServer Y Task history 1.3 Startup Order\r#\rMySQL ──► HDFS ──┬──► YARN ──► JobHistory │ ├──► ZooKeeper ──┬──► HBase │ │ │ └──► Kafka │ └──► Hive Metastore ──► HiveServer2 ──► Spark HistoryServer\rStart: HDFS → (ZooKeeper + YARN) → (Hive → HBase → Kafka → Spark)\nStop: Spark → Kafka → HBase → Hive → YARN → ZooKeeper → HDFS\n2. Environment Setup\r#\r2.1 Hardware Requirements\r#\rNode Memory CPU Disk hd2 ≥4GB ≥2 cores ≥40GB hd3 ≥3GB ≥2 cores ≥40GB hd4 ≥2GB ≥2 cores ≥40GB 2.2 Software Versions\r#\rComponent Version Path JDK 1.8.0_471 /opt/module/jdk1.8.0_471 Hadoop 3.3.6 /opt/module/hadoop-3.3.6 ZooKeeper 3.5.9 /opt/module/apache-zookeeper-3.5.9-bin Hive 3.1.3 /opt/module/apache-hive-3.1.3-bin HBase 2.6.4 /opt/module/hbase-2.6.4-hadoop3 Kafka 2.8.0 /opt/module/kafka_2.13-2.8.0 Spark 3.5.8 /opt/module/spark-3.5.8-bin-hadoop3-scala2.13 MySQL 5.7+ System 2.3 Create User (All Nodes, as root)\r#\ruseradd hadoop echo \u0026#34;hadoop\u0026#34; | passwd --stdin hadoop echo \u0026#34;hadoop ALL=(ALL) NOPASSWD: ALL\u0026#34; \u0026gt;\u0026gt; /etc/sudoers\r2.4 /etc/hosts (All Nodes, as root)\r#\rcat \u0026gt;\u0026gt; /etc/hosts \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; 192.168.137.155 hd2.pblh123.cn hd2 192.168.137.156 hd3.pblh123.cn hd3 192.168.137.157 hd4.pblh123.cn hd4 EOF\rAdapt: Replace IPs and hostnames with your actual values.\n2.5 Password-less SSH (as hadoop user)\r#\rsu - hadoop ssh-keygen -t rsa -N \u0026#34;\u0026#34; -f ~/.ssh/id_rsa for host in hd2 hd3 hd4; do ssh-copy-id -o StrictHostKeyChecking=no hadoop@$host done # Verify ssh hd3 \u0026#34;hostname\u0026#34; ssh hd4 \u0026#34;hostname\u0026#34;\r2.6 Firewall \u0026amp; SELinux (All Nodes, as root)\r#\rsystemctl stop firewalld \u0026amp;\u0026amp; systemctl disable firewalld setenforce 0 sed -i \u0026#39;s/SELINUX=enforcing/SELINUX=disabled/\u0026#39; /etc/selinux/config\r2.7 JDK (All Nodes, as root)\r#\rtar -zxf jdk-8u471-linux-x64.tar.gz -C /opt/module/ /opt/module/jdk1.8.0_471/bin/java -version # Expected: java version \u0026#34;1.8.0_471\u0026#34;\r2.8 Create Directories (All Nodes, as hadoop)\r#\rmkdir -p /opt/data/{namenode,datanode,hadoop-tmp,zookeeper,kafkadata}\r2.9 Global Environment Script (All Nodes, as root)\r#\rCreate /etc/profile.d/hadoop_env.sh:\nsudo tee /etc/profile.d/hadoop_env.sh \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; JAVA_HOME=/opt/module/jdk1.8.0_471 HADOOP_HOME=/opt/module/hadoop-3.3.6 KAFKA_HOME=/opt/module/kafka_2.13-2.8.0 HIVE_HOME=/opt/module/apache-hive-3.1.3-bin SPARK_HOME=/opt/module/spark-3.5.8-bin-hadoop3-scala2.13 HBASE_HOME=/opt/module/hbase-2.6.4-hadoop3 ZOOKEEPER_HOME=/opt/module/apache-zookeeper-3.5.9-bin PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$KAFKA_HOME/bin:$HIVE_HOME/bin:$SPARK_HOME/bin:$HBASE_HOME/bin:$ZOOKEEPER_HOME/bin EOF source /etc/profile.d/hadoop_env.sh\rAdapt: Update version directory names to match your installation.\n3. MySQL Setup\r#\rOn hd2 only, for Hive metadata storage.\nyum install -y mariadb-server mariadb systemctl start mariadb \u0026amp;\u0026amp; systemctl enable mariadb mysql -u root \u0026lt;\u0026lt; EOF CREATE DATABASE IF NOT EXISTS hive_metastore DEFAULT CHARACTER SET utf8; CREATE USER IF NOT EXISTS \u0026#39;hive\u0026#39;@\u0026#39;%\u0026#39; IDENTIFIED BY \u0026#39;Hive@12345\u0026#39;; GRANT ALL PRIVILEGES ON hive_metastore.* TO \u0026#39;hive\u0026#39;@\u0026#39;%\u0026#39;; FLUSH PRIVILEGES; EOF\rAdapt: Replace Hive@12345 with your actual password.\n4. Hadoop HDFS Deployment\r#\r4.1 core-site.xml\r#\rPath: ${HADOOP_HOME}/etc/hadoop/core-site.xml\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;?xml-stylesheet type=\u0026#34;text/xsl\u0026#34; href=\u0026#34;configuration.xsl\u0026#34;?\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;fs.defaultFS\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hdfs://hd2.pblh123.cn:9000\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hadoop.proxyuser.hadoop.hosts\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;*\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hadoop.proxyuser.hadoop.groups\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;*\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hadoop.proxyuser.hive.hosts\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;*\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hadoop.proxyuser.hive.groups\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;*\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hadoop.tmp.dir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;/opt/data/hadoop-tmp\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt;\r4.2 hdfs-site.xml\r#\rPath: ${HADOOP_HOME}/etc/hadoop/hdfs-site.xml\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;?xml-stylesheet type=\u0026#34;text/xsl\u0026#34; href=\u0026#34;configuration.xsl\u0026#34;?\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.replication\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;2\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.namenode.name.dir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;file:///opt/data/namenode\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.datanode.data.dir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;file:///opt/data/datanode\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.blocksize\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;64m\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.namenode.http-address\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hd2.pblh123.cn:9870\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.namenode.secondary.http-address\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hd3.pblh123.cn:9868\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.permissions.enabled\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;false\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt;\r4.3 workers\r#\rPath: ${HADOOP_HOME}/etc/hadoop/workers\nhd2.pblh123.cn hd3.pblh123.cn hd4.pblh123.cn\r4.4 hadoop-env.sh\r#\rPath: ${HADOOP_HOME}/etc/hadoop/hadoop-env.sh\nexport JAVA_HOME=/opt/module/jdk1.8.0_471 export HADOOP_HOME=/opt/module/hadoop-3.3.6 export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop export HADOOP_LOG_DIR=$HADOOP_HOME/logs export HADOOP_PID_DIR=$HADOOP_HOME/pids export HADOOP_HEAPSIZE_MAX=512m export HADOOP_HEAPSIZE_MIN=256m export HDFS_NAMENODE_OPTS=\u0026#34;-Xmx512m -Xms256m\u0026#34; export HDFS_DATANODE_OPTS=\u0026#34;-Xmx256m -Xms128m\u0026#34; export YARN_RESOURCEMANAGER_HEAPSIZE=512 export YARN_NODEMANAGER_HEAPSIZE=256\r4.5 Sync to All Nodes\r#\rHADOOP_CONF=/opt/module/hadoop-3.3.6/etc/hadoop for host in hd3 hd4; do scp $HADOOP_CONF/{core-site.xml,hdfs-site.xml,workers,hadoop-env.sh} hadoop@$host:$HADOOP_CONF/ done\r4.6 Format NameNode (hd2 only, once)\r#\rsource /etc/profile.d/hadoop_env.sh hdfs namenode -format # Expected: ...successfully formatted\r4.7 Start HDFS\r#\rstart-dfs.sh sleep 5\r4.8 Verify HDFS\r#\rjps | grep -E \u0026#34;NameNode|DataNode\u0026#34; hdfs dfsadmin -report # Should show 3 live datanodes echo \u0026#34;Hello HDFS\u0026#34; | hdfs dfs -put - /test.txt \u0026amp;\u0026amp; hdfs dfs -cat /test.txt \u0026amp;\u0026amp; hdfs dfs -rm /test.txt # Web UI: http://192.168.137.155:9870\r5. YARN Deployment\r#\r5.1 yarn-site.xml (Per-Node Memory)\r#\rhd2 version (/opt/module/hadoop-3.3.6/etc/hadoop/yarn-site.xml):\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;?xml-stylesheet type=\u0026#34;text/xsl\u0026#34; href=\u0026#34;configuration.xsl\u0026#34;?\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.nodemanager.aux-services\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;mapreduce_shuffle\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.resourcemanager.hostname\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hd2.pblh123.cn\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.resourcemanager.webapp.address\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hd2.pblh123.cn:8088\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.nodemanager.resource.memory-mb\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;2800\u0026lt;/value\u0026gt;\u0026lt;!-- hd2: 4G×70%≈2800MB --\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.scheduler.maximum-allocation-mb\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;2048\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.scheduler.minimum-allocation-mb\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;256\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.nodemanager.resource.cpu-vcores\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;2\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.nodemanager.vmem-check-enabled\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;false\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.log-aggregation-enable\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;true\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.log.server.url\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;http://hd2.pblh123.cn:19888/jobhistory/logs\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt;\rhd3: Change yarn.nodemanager.resource.memory-mb to 2000, scheduler max to 1744.\nhd4: Change to 1200 and 944.\nFormula: NM_memory = physical_RAM × 70%, scheduler_max = min(NM_memory - 256, 2048)\n5.2 mapred-site.xml\r#\rPath: ${HADOOP_HOME}/etc/hadoop/mapred-site.xml\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;?xml-stylesheet type=\u0026#34;text/xsl\u0026#34; href=\u0026#34;configuration.xsl\u0026#34;?\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;mapreduce.framework.name\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;yarn\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;mapreduce.jobhistory.address\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hd2.pblh123.cn:10020\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;mapreduce.jobhistory.webapp.address\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hd2.pblh123.cn:19888\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt;\r5.3 Sync and Start\r#\rfor host in hd3 hd4; do scp $HADOOP_CONF/{yarn-site.xml,mapred-site.xml} hadoop@$host:$HADOOP_CONF/ done start-yarn.sh mapred --daemon start historyserver sleep 5\r5.4 Verify YARN\r#\ryarn node -list -all | grep RUNNING # Should show 3 nodes hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.3.6.jar pi 2 5 # Web UI: http://192.168.137.155:8088\r6. ZooKeeper Deployment\r#\r6.1 zoo.cfg (Same on All Nodes)\r#\rPath: ${ZOOKEEPER_HOME}/conf/zoo.cfg\ntickTime=2000 initLimit=10 syncLimit=5 dataDir=/opt/data/zookeeper clientPort=2181 maxClientCnxns=60 autopurge.snapRetainCount=3 autopurge.purgeInterval=1 server.1=hd2.pblh123.cn:2888:3888 server.2=hd3.pblh123.cn:2888:3888 server.3=hd4.pblh123.cn:2888:3888\r6.2 myid (Different Per Node)\r#\r# hd2 echo 1 \u0026gt; /opt/data/zookeeper/myid # hd3 echo 2 \u0026gt; /opt/data/zookeeper/myid # hd4 echo 3 \u0026gt; /opt/data/zookeeper/myid\r6.3 Sync, Start and Verify\r#\rZK_CONF=/opt/module/apache-zookeeper-3.5.9-bin/conf for host in hd3 hd4; do scp $ZK_CONF/zoo.cfg hadoop@$host:$ZK_CONF/; done # Start on all nodes for host in hd2 hd3 hd4; do ssh $host \u0026#34;source /etc/profile.d/hadoop_env.sh \u0026amp;\u0026amp; zkServer.sh start\u0026#34;; done sleep 3 # Verify for host in hd2 hd3 hd4; do ssh $host \u0026#34;zkServer.sh status\u0026#34;; done # Expected: 1 leader, 2 followers\r7. Hive Deployment\r#\rHive services on hd2 only.\n7.1 MySQL JDBC Driver\r#\rHIVE_LIB=/opt/module/apache-hive-3.1.3-bin/lib wget -P $HIVE_LIB https://repo1.maven.org/maven2/mysql/mysql-connector-java/8.0.33/mysql-connector-java-8.0.33.jar\r7.2 hive-site.xml\r#\rPath: /opt/module/apache-hive-3.1.3-bin/conf/hive-site.xml\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;?xml-stylesheet type=\u0026#34;text/xsl\u0026#34; href=\u0026#34;configuration.xsl\u0026#34;?\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hive.metastore.uris\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;thrift://hd2.pblh123.cn:9083\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;javax.jdo.option.ConnectionURL\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;jdbc:mysql://localhost:3306/hive_metastore?useSSL=false\u0026amp;amp;createDatabaseIfNotExist=true\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;javax.jdo.option.ConnectionDriverName\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;com.mysql.jdbc.Driver\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;javax.jdo.option.ConnectionUserName\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hive\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;javax.jdo.option.ConnectionPassword\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;Hive@12345\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hive.metastore.warehouse.dir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;/user/hive/warehouse\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hive.server2.thrift.bind.host\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hd2.pblh123.cn\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hive.server2.thrift.port\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;10000\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt;\r7.3 Init Metastore Schema\r#\rsource /etc/profile.d/hadoop_env.sh schematool -dbType mysql -initSchema # Expected: schemaTool completed\r7.4 Start Hive Services\r#\rnohup hive --service metastore \u0026gt; /opt/data/hive-metastore.log 2\u0026gt;\u0026amp;1 \u0026amp; sleep 3 nohup hiveserver2 \u0026gt; /opt/data/hiveserver2.log 2\u0026gt;\u0026amp;1 \u0026amp; sleep 2\r7.5 Verify Hive\r#\rjps | grep RunJar # Should see 2 RunJar processes beeline -u jdbc:hive2://hd2.pblh123.cn:10000 -n hadoop -e \u0026#34;SHOW DATABASES;\u0026#34;\r8. HBase Deployment\r#\r8.1 hbase-site.xml (Same on All Nodes)\r#\rPath: ${HBASE_HOME}/conf/hbase-site.xml\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;?xml-stylesheet type=\u0026#34;text/xsl\u0026#34; href=\u0026#34;configuration.xsl\u0026#34;?\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.cluster.distributed\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;true\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.rootdir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hdfs://hd2.pblh123.cn:9000/hbase\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.zookeeper.quorum\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hd2.pblh123.cn,hd3.pblh123.cn,hd4.pblh123.cn\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.zookeeper.property.clientPort\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;2181\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.unsafe.stream.capability.enforce\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;false\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.wal.provider\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;filesystem\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt;\r8.2 regionservers\r#\rPath: ${HBASE_HOME}/conf/regionservers\nhd2.pblh123.cn hd3.pblh123.cn hd4.pblh123.cn\r8.3 Sync, Start and Verify\r#\rHBASE_CONF=/opt/module/hbase-2.6.4-hadoop3/conf for host in hd3 hd4; do scp $HBASE_CONF/{hbase-site.xml,regionservers} hadoop@$host:$HBASE_CONF/ done start-hbase.sh sleep 5 echo \u0026#34;status\u0026#34; | hbase shell -n # Expected: 1 active master, 3 region servers # Web UI: http://192.168.137.155:16010\r9. Kafka Deployment\r#\r9.1 server.properties (Per-Node broker.id)\r#\rCommon settings for all nodes:\nzookeeper.connect=hd2.pblh123.cn:2181,hd3.pblh123.cn:2181,hd4.pblh123.cn:2181 listeners=PLAINTEXT://:9092\rPer-node broker.id:\n# hd2 sed -i \u0026#39;s/^broker.id=.*/broker.id=1/\u0026#39; /opt/module/kafka_2.13-2.8.0/config/server.properties sed -i \u0026#39;s|^zookeeper.connect=.*|zookeeper.connect=hd2.pblh123.cn:2181,hd3.pblh123.cn:2181,hd4.pblh123.cn:2181|\u0026#39; /opt/module/kafka_2.13-2.8.0/config/server.properties # hd3 (broker.id=2), hd4 (broker.id=3) — same pattern\r9.2 Cloned Node Fix\r#\r# If nodes were cloned, delete stale meta.properties ssh hd3 \u0026#34;rm -f /opt/data/kafkadata/meta.properties\u0026#34; ssh hd4 \u0026#34;rm -f /opt/data/kafkadata/meta.properties\u0026#34;\r9.3 Start and Verify\r#\rfor host in hd2 hd3 hd4; do ssh $host \u0026#34;source /etc/profile.d/hadoop_env.sh \u0026amp;\u0026amp; nohup kafka-server-start.sh -daemon \\$KAFKA_HOME/config/server.properties \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 \u0026amp;\u0026#34; done sleep 3 kafka-topics.sh --bootstrap-server hd2.pblh123.cn:9092 --list # Producer/consumer test echo \u0026#34;test message\u0026#34; | kafka-console-producer.sh --bootstrap-server hd2.pblh123.cn:9092 --topic verify-topic kafka-console-consumer.sh --bootstrap-server hd2.pblh123.cn:9092 --topic verify-topic --from-beginning --max-messages 1\r10. Spark Deployment\r#\r10.1 spark-env.sh\r#\rPath: ${SPARK_HOME}/conf/spark-env.sh\n#!/usr/bin/env bash export JAVA_HOME=/opt/module/jdk1.8.0_471 export HADOOP_HOME=/opt/module/hadoop-3.3.6 export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop export SPARK_HOME=/opt/module/spark-3.5.8-bin-hadoop3-scala2.13 export SPARK_CONF_DIR=$SPARK_HOME/conf export SPARK_HISTORY_OPTS=\u0026#34;-Dspark.history.ui.port=18080 \\ -Dspark.history.fs.logDirectory=hdfs://hd2.pblh123.cn:9000/spark-history \\ -Dspark.history.retainedApplications=30\u0026#34; export SPARK_DAEMON_MEMORY=256m\r10.2 spark-defaults.conf\r#\rPath: ${SPARK_HOME}/conf/spark-defaults.conf\nspark.master yarn spark.submit.deployMode cluster spark.eventLog.enabled true spark.eventLog.dir hdfs://hd2.pblh123.cn:9000/spark-history spark.history.fs.logDirectory hdfs://hd2.pblh123.cn:9000/spark-history spark.serializer org.apache.spark.serializer.KryoSerializer spark.driver.memory 512m spark.executor.memory 512m spark.executor.cores 1 spark.executor.instances 2 spark.yarn.am.memory 512m spark.yarn.maxAppAttempts 2 spark.sql.shuffle.partitions 2 spark.driver.maxResultSize 256m\r10.3 Link hive-site.xml\r#\rln -sf /opt/module/apache-hive-3.1.3-bin/conf/hive-site.xml \\ /opt/module/spark-3.5.8-bin-hadoop3-scala2.13/conf/hive-site.xml\r10.4 Create HDFS Directories\r#\rhdfs dfs -mkdir -p /spark-history /user/hive/warehouse hdfs dfs -chmod 777 /spark-history\r10.5 Sync, Start and Verify\r#\rSPARK_CONF=/opt/module/spark-3.5.8-bin-hadoop3-scala2.13/conf for host in hd3 hd4; do scp $SPARK_CONF/{spark-env.sh,spark-defaults.conf} hadoop@$host:$SPARK_CONF/ ssh $host \u0026#34;ln -sf /opt/module/apache-hive-3.1.3-bin/conf/hive-site.xml $SPARK_CONF/hive-site.xml\u0026#34; done $SPARK_HOME/sbin/start-history-server.sh # Spark Pi test spark-submit --master yarn --deploy-mode cluster \\ --class org.apache.spark.examples.SparkPi \\ $SPARK_HOME/examples/jars/spark-examples_2.13-3.5.8.jar 100 # Expected: Pi is roughly 3.14... # Web UI: http://192.168.137.155:18080\r11. Cluster Verification\r#\r11.1 Process Check\r#\rfor host in hd2 hd3 hd4; do echo \u0026#34;=== $host ===\u0026#34; ssh $host \u0026#34;source /etc/profile.d/hadoop_env.sh \u0026amp;\u0026amp; jps -l | grep -v Jps | sort\u0026#34; echo \u0026#34;\u0026#34; done\rExpected processes:\nNode Expected hd2 NameNode, DataNode, ResourceManager, NodeManager, QuorumPeerMain, JobHistoryServer, HMaster, HRegionServer, Kafka, HistoryServer, 2×RunJar hd3 DataNode, SecondaryNameNode, NodeManager, QuorumPeerMain, HRegionServer, Kafka hd4 DataNode, NodeManager, QuorumPeerMain, HRegionServer, Kafka 11.2 Quick Test Suite\r#\r# HDFS echo \u0026#34;verify\u0026#34; | hdfs dfs -put - /verify.txt \u0026amp;\u0026amp; hdfs dfs -cat /verify.txt \u0026amp;\u0026amp; hdfs dfs -rm /verify.txt # YARN hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.3.6.jar pi 2 5 # ZooKeeper echo stat | nc hd2.pblh123.cn 2181 | grep Mode # Hive beeline -u jdbc:hive2://hd2.pblh123.cn:10000 -n hadoop -e \u0026#34;SHOW DATABASES;\u0026#34; # HBase echo \u0026#34;status\u0026#34; | hbase shell -n # Kafka kafka-topics.sh --bootstrap-server hd2.pblh123.cn:9092 --list # Spark spark-submit --master yarn --deploy-mode cluster \\ --class org.apache.spark.examples.SparkPi \\ $SPARK_HOME/examples/jars/spark-examples_2.13-3.5.8.jar 10\r12. Cluster Management\r#\r12.1 One-Click Start/Stop\r#\r# Shell (on cluster node) bash cluster_ctl.sh start|stop|restart|status bash cluster_ctl.sh start kafka # Single service # Python (from management machine) python cluster_ctl.py start|stop|restart|status python cluster_ctl.py start hdfs\r12.2 Cluster Shutdown\r#\r# Shell — stop services then power off bash shutdown_cluster.sh # Skip service stop, force power off bash shutdown_cluster.sh --force # Python (from management machine) python shutdown_cluster.py python shutdown_cluster.py --force\rFlow: ① Confirm → ② Stop all services → ③ Shut down slave nodes → ④ Shut down master node.\n12.3 Manual Service Control\r#\r# HDFS: start-dfs.sh / stop-dfs.sh # YARN: start-yarn.sh / stop-yarn.sh # ZK: zkServer.sh start / zkServer.sh stop # HBase: start-hbase.sh / stop-hbase.sh # Kafka: kafka-server-start.sh / kafka-server-stop.sh # Spark: $SPARK_HOME/sbin/start-history-server.sh / stop-history-server.sh\r12.3 Log Paths\r#\rComponent Log Path HDFS NN $HADOOP_HOME/logs/hadoop-hadoop-namenode-*.log YARN RM $HADOOP_HOME/logs/yarn-hadoop-resourcemanager-*.log ZooKeeper $ZOOKEEPER_HOME/logs/zookeeper.out Kafka $KAFKA_HOME/logs/server.log HBase /opt/data/hbase/logs/ Hive /opt/data/hive-metastore.log, /opt/data/hiveserver2.log 12.4 Port Reference\r#\rPort Service 9000 HDFS NameNode RPC 9870 HDFS NameNode Web 8088 YARN RM Web 19888 JobHistory Web 2181 ZooKeeper Client 9083 Hive Metastore 10000 HiveServer2 9092 Kafka Broker 16010 HBase Master Web 18080 Spark History Server 12.5 Web UIs\r#\rService URL HDFS http://192.168.137.155:9870 YARN http://192.168.137.155:8088 Spark http://192.168.137.155:18080 HBase http://192.168.137.155:16010 JobHistory http://192.168.137.155:19888 13. One-Click Scripts\r#\r13.1 config.ini Template\r#\rEdit config.ini then run bash deploy.sh:\n[NODES] hd2=192.168.137.155 hd3=192.168.137.156 hd4=192.168.137.157 [NODE_MEMORY] hd2=2800 hd3=2000 hd4=1200 [NODE_CPU] hd2=2 hd3=2 hd4=2 [SSH] user=hadoop password=hadoop [PATHS] base_dir=/opt/module data_dir=/opt/data [VERSIONS] jdk=jdk1.8.0_471 hadoop=hadoop-3.3.6 zookeeper=apache-zookeeper-3.5.9-bin hive=apache-hive-3.1.3-bin hbase=hbase-2.6.4-hadoop3 kafka=kafka_2.13-2.8.0 spark=spark-3.5.8-bin-hadoop3-scala2.13 [MYSQL] host=localhost port=3306 database=hive_metastore user=hive password=Hive@12345 [PORTS] hdfs_namenode=9000 hdfs_web=9870 yarn_web=8088 jobhistory=19888 zookeeper_client=2181 hive_metastore=9083 hive_server2=10000 hbase_master_web=16010 kafka=9092 spark_history=18080 [HDFS] replication=2 block_size_mb=64 namenode_dir=/opt/data/namenode datanode_dir=/opt/data/datanode hadoop_tmp=/opt/data/hadoop-tmp [ZOOKEEPER] tick_time=2000 init_limit=10 sync_limit=5 data_dir=/opt/data/zookeeper [SPARK] driver_memory=512m executor_memory=512m executor_cores=1 executor_instances=2 am_memory=512m [JAVA_HEAP] namenode=512m datanode=256m rm=512 nm=256\r13.2 Deployment Flow\r#\rvi config.ini # 1. Edit configuration sudo yum install -y sshpass # 2. Install sshpass bash deploy.sh # 3. One-click deploy bash cluster_ctl.sh status # 4. Verify\r13.3 Remote Management Machine\r#\rpip install paramiko python deploy.py python cluster_ctl.py start python cluster_ctl.py status\r14. Troubleshooting\r#\rDataNode Not Registering\r#\rcat $HADOOP_HOME/etc/hadoop/workers # Hostnames must match cat /etc/hosts # Consistent across all nodes tail -100 $HADOOP_HOME/logs/hadoop-hadoop-datanode-*.log | grep -i error\rNodeManager Not Starting (Low Memory)\r#\rfree -h # Reduce yarn.nodemanager.resource.memory-mb to 70-80% of physical RAM\rZooKeeper Election Failure\r#\rcat /opt/data/zookeeper/myid # Must match server.X in zoo.cfg grep \u0026#34;server\\.\u0026#34; $ZOOKEEPER_HOME/conf/zoo.cfg telnet hd3 2888 # Check connectivity\rKafka Cloned Node Failure\r#\r# Error: broker.id already registered rm -f /opt/data/kafkadata/meta.properties kafka-server-start.sh -daemon $KAFKA_HOME/config/server.properties\rHBase Master Exits After Start\r#\rzkServer.sh status # ZK must be running hdfs dfs -ls /hbase # Directory must exist cat /opt/data/hbase/logs/hbase-hadoop-master-*.log | grep ERROR\rHDFS Safemode\r#\rhdfs dfsadmin -safemode get hdfs dfsadmin -safemode leave\rAppendix: New Cluster Checklist\r#\r[ ] Update /etc/hosts on all nodes [ ] Update config.ini: nodes, memory, SSH credentials [ ] Verify version directory names in [VERSIONS] match /opt/module/ [ ] Update MySQL credentials in [MYSQL] [ ] Replace domain suffix: .pblh123.cn hardcoded in scripts → your domain [ ] Set replication factor ≤ DataNode count [ ] Configure per-node memory: 70-80% of physical RAM [ ] Run bash deploy.sh [ ] Run bash cluster_ctl.sh status [ ] Verify each component: HDFS → YARN → ZK → Hive → HBase → Kafka → Spark Domain Suffix Note\r#\rThe scripts hardcode .pblh123.cn as the domain suffix. For a new cluster:\nOption A: Configure /etc/hosts with matching FQDN entries Option B: Globally replace .pblh123.cn with your domain (or remove suffix for short hostnames) in: deploy.sh, deploy.py, cluster_ctl.sh, cluster_ctl.py Appendix: Script Files\r#\rFile Purpose Run On config.ini Global configuration Edit before deploy deploy.sh Shell one-click deploy Master node deploy.py Python deploy script Management machine cluster_ctl.sh Shell start/stop Cluster node cluster_ctl.py Python start/stop Management machine ssh_exec.py SSH connection module Dependency verify.py Cluster verification Management machine cluster_check.py Quick status check Management machine deep_check.py Deep diagnostics Management machine shutdown_cluster.sh Shell cluster shutdown Cluster node shutdown_cluster.py Python cluster shutdown Management machine ","date":"May 29, 2026","externalUrl":null,"permalink":"/en/posts/big-data-cluster-distributed-deployment-guide/","section":"","summary":"Big Data Cluster Distributed Deployment Guide (Fully Verified)\r#\rA copy-paste-ready deployment guide for a complete big data cluster. Every configuration file is shown in full. All steps are verified on a 3-node CentOS 7.9 cluster.\nVariable Substitution Reference\r#\rWhen deploying to a new cluster, replace these values:\n","title":"Big Data Cluster Distributed Deployment Guide","type":"en"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/big-data/","section":"Tags","summary":"","title":"Big-Data","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/cluster/","section":"Tags","summary":"","title":"Cluster","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/deployment/","section":"Tags","summary":"","title":"Deployment","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/distributed/","section":"Tags","summary":"","title":"Distributed","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/docker/","section":"Tags","summary":"","title":"Docker","type":"tags"},{"content":"\rDocker Big Data Cluster HA Deployment\r#\rDocker Compose orchestrated 32-container full-stack HA big data cluster for local testing and learning.\nArchitecture Overview\r#\r┌─────────────────────────────────────────────────┐ │ Docker Bridge: bigdata-net │ │ 172.25.0.0/16 │ ├─────────────────────────────────────────────────┤ │ Layer 0 (base): MySQL + ZooKeeper x3 │ │ Layer 1 (storage): JournalNode x3 │ │ Layer 2 (storage): NameNode x2 + ZKFC x2 │ │ Layer 3 (storage): DataNode x3 │ │ Layer 4 (compute): ResourceManager x2 + NM x3 │ │ Layer 5 (analytics): Hive Metastore/Server2 │ │ Layer 6 (analytics): HBase x5 + Kafka x3 │ │ Layer 7 (analytics): Spark + Flink History │ └─────────────────────────────────────────────────┘\rComponent Version HA Strategy Containers HDFS 3.3.6 Dual NameNode + 3 JN + ZKFC 10 YARN 3.3.6 Dual RM + ZK State Store 6 ZooKeeper 3.7.1 3-node quorum 3 HBase 2.6.4 Dual Master + ZK discovery 5 Hive 3.1.3 Metastore + Server2 (separate) 2 Kafka 2.8.0 3 Broker + ZK coordination 3 Spark 3.5.8 History Server (YARN on-demand) 1 Flink 1.19.1 History Server (YARN on-demand) 1 MySQL 5.7 — 1 Total: 32 containers, ~10-11GB RAM\nPrerequisites\r#\rDocker Desktop 24+ (Windows/Mac/Linux) 16GB+ RAM (close other heavy apps recommended) 50GB+ disk space Component binaries cached in ansible/files/cache/ Quick Start\r#\rcd docker # 1. Build the unified image (~10-20 min first time) make build # 2. Start by layer (or make all for everything) make base # Layer 0: MySQL + ZKx3 make storage # Layer 1-3: HDFS HA make compute # Layer 4: YARN HA make analytics # Layer 5-7: Hive + HBase + Kafka + Spark/Flink # Or one-shot make all # 3. First-time deployment: initialize HDFS make init-hdfs # 4. Verification make verify # Level 1 Smoke Test make verify-spark # Level 2a Spark make verify-flink # Level 2b Flink make verify-ha # Level 3 HA Failover\rLayered Startup\r#\rStart on demand — low-memory environments don\u0026rsquo;t need everything:\nLayer Command Services Memory Layer 0 make base MySQL, ZKx3 ~1.5GB Layer 1-3 make storage JNx3, NNx2, ZKFCx2, DNx3 ~3.5GB Layer 4 make compute RMx2, NMx3, JobHistory ~2.5GB Layer 5-7 make analytics Hivex2, HBasex5, Kafkax3, Spark/Flinkx2 ~3.5GB Command Reference\r#\rCommand Description make build Build unified image make all Start all 32 containers make base Start infrastructure layer make storage Start HDFS HA storage layer make compute Start YARN HA compute layer make analytics Start analytics layer make status Container resource usage make ps Container list make verify Level 1 Smoke Test (7 items) make verify-spark Level 2a Spark verification make verify-flink Level 2b Flink verification make verify-ha Level 3 HA failover test (10 items) make init-hdfs First-time HDFS format + init make stop Stop all services (reverse order) make restart Restart all make clean Remove containers and network make logs View all logs make logs-\u0026lt;name\u0026gt; View specific container logs make shell-\u0026lt;name\u0026gt; Enter container shell Verification Tests\r#\rLevel 1: Smoke Test (7 items)\r#\r# Test What it verifies S1 HDFS read/write Write → Read → Delete S2 YARN nodes \u0026gt;=3 NM RUNNING S3 ZK cluster 1 leader + 2 follower S4 Hive Metastore Port 9083 reachable S5 HiveServer2 Port 10000 reachable S6 HBase Shell status command works S7 Kafka E2E Create → Produce → Consume → Delete Level 2: Spark/Flink Functional (6 items)\r#\r# Test SP1 Spark Pi (YARN cluster mode) SP2 Spark SQL query SP3 Spark History Server HTTP FL1 Flink YARN Session creation FL2 Flink WordCount batch job FL3 Flink History Server HTTP Level 3: HA Failover (10 items)\r#\r# Test Fault Injection HA1 NN failover Stop namenode-active HA2 NN recovery Start namenode-active HA3 HDFS fault tolerance Read/write after NN switch HA4 RM failover Stop resourcemanager-1 HA5 Spark on HA Submit after RM switch HA6 Flink on HA Session after RM switch HA7 HMaster failover Stop hbase-master-1 HA8 JN fault tolerance Stop 1/3 JN HA9 ZK fault tolerance Stop 1/3 ZK HA10 Kafka fault tolerance Stop 1/3 Broker Web UI Access\r#\rService URL NameNode Active http://localhost:9870 NameNode Standby http://localhost:9871 YARN RM1 http://localhost:8088 YARN RM2 http://localhost:8089 JobHistory http://localhost:19888 HBase Master1 http://localhost:16010 HBase Master2 http://localhost:16011 Spark History http://localhost:18080 Flink History http://localhost:8082 Troubleshooting\r#\rContainer fails to start\r#\rdocker compose --profile \u0026lt;name\u0026gt; logs # View layer logs make logs-\u0026lt;container\u0026gt; # View specific container make shell-\u0026lt;container\u0026gt; # Enter container for debugging\rOut of memory\r#\rReduce JVM heap in .env (e.g., NAMENODE_HEAP=128m) Start by layer instead of all at once Close other heavy apps on Windows HDFS not healthy\r#\rmake init-hdfs # Re-format (destroys data) docker compose --profile storage restart # Restart storage layer\rPort conflicts\r#\rEdit ports mappings in docker-compose.yml (host port is on the left).\nComparison with Bare-Metal/Ansible\r#\rFeature Docker Ansible Purpose Local testing/learning Production bare-metal Memory 10-11GB 12GB+ HA Full production-grade Full production-grade Config Standalone config/ dir Jinja2 templates Management Makefile + docker compose ansible-playbook Network Bridge (172.25.0.0/16) Physical NIC ","date":"May 29, 2026","externalUrl":null,"permalink":"/en/posts/docker-big-data-cluster-ha-deployment/","section":"","summary":"Docker Big Data Cluster HA Deployment\r#\rDocker Compose orchestrated 32-container full-stack HA big data cluster for local testing and learning.\nArchitecture Overview\r#\r┌─────────────────────────────────────────────────┐ │ Docker Bridge: bigdata-net │ │ 172.25.0.0/16 │ ├─────────────────────────────────────────────────┤ │ Layer 0 (base): MySQL + ZooKeeper x3 │ │ Layer 1 (storage): JournalNode x3 │ │ Layer 2 (storage): NameNode x2 + ZKFC x2 │ │ Layer 3 (storage): DataNode x3 │ │ Layer 4 (compute): ResourceManager x2 + NM x3 │ │ Layer 5 (analytics): Hive Metastore/Server2 │ │ Layer 6 (analytics): HBase x5 + Kafka x3 │ │ Layer 7 (analytics): Spark + Flink History │ └─────────────────────────────────────────────────┘\rComponent Version HA Strategy Containers HDFS 3.3.6 Dual NameNode + 3 JN + ZKFC 10 YARN 3.3.6 Dual RM + ZK State Store 6 ZooKeeper 3.7.1 3-node quorum 3 HBase 2.6.4 Dual Master + ZK discovery 5 Hive 3.1.3 Metastore + Server2 (separate) 2 Kafka 2.8.0 3 Broker + ZK coordination 3 Spark 3.5.8 History Server (YARN on-demand) 1 Flink 1.19.1 History Server (YARN on-demand) 1 MySQL 5.7 — 1 Total: 32 containers, ~10-11GB RAM\n","title":"Docker Big Data Cluster HA Deployment","type":"en"},{"content":"\rDocker 大数据集群高可用部署\r#\rDocker Compose 编排的 32 容器全组件 HA 大数据集群，用于本地测试与学习。\n架构概述\r#\r┌─────────────────────────────────────────────────┐ │ Docker Bridge: bigdata-net │ │ 172.25.0.0/16 │ ├─────────────────────────────────────────────────┤ │ Layer 0 (base): MySQL + ZooKeeper x3 │ │ Layer 1 (storage): JournalNode x3 │ │ Layer 2 (storage): NameNode x2 + ZKFC x2 │ │ Layer 3 (storage): DataNode x3 │ │ Layer 4 (compute): ResourceManager x2 + NM x3 │ │ Layer 5 (analytics): Hive Metastore/Server2 │ │ Layer 6 (analytics): HBase x5 + Kafka x3 │ │ Layer 7 (analytics): Spark + Flink History │ └─────────────────────────────────────────────────┘\r组件 版本 HA 方案 容器数 HDFS 3.3.6 双 NameNode + 3 JournalNode + ZKFC 10 YARN 3.3.6 双 ResourceManager + ZK 状态存储 6 ZooKeeper 3.7.1 3 节点仲裁 3 HBase 2.6.4 双 Master + ZK 发现 5 Hive 3.1.3 Metastore + Server2（独立容器） 2 Kafka 2.8.0 3 Broker + ZK 协调 3 Spark 3.5.8 History Server（YARN 模式按需提交） 1 Flink 1.19.1 History Server（YARN 模式按需提交） 1 MySQL 5.7 — 1 总计：32 容器，~10-11GB 内存\n前置要求\r#\rDocker Desktop 24+（Windows/Mac/Linux） 16GB+ RAM（建议关闭其他大型应用） 50GB+ 磁盘空间 组件二进制包已缓存到 ansible/files/cache/ 快速开始\r#\rcd docker # 1. 构建统一镜像（首次 ~10-20分钟） make build # 2. 分层启动（或一键全量 make all） make base # Layer 0: MySQL + ZKx3 make storage # Layer 1-3: HDFS HA make compute # Layer 4: YARN HA make analytics # Layer 5-7: Hive + HBase + Kafka + Spark/Flink # 或一键启动 make all # 3. 首次部署：初始化 HDFS make init-hdfs # 4. 验证测试 make verify # Level 1 冒烟测试 make verify-spark # Level 2a Spark make verify-flink # Level 2b Flink make verify-ha # Level 3 HA 故障转移\r分层启动指南\r#\r从零开始按需启动，低内存环境可不启动全部：\n层级 命令 服务 内存 Layer 0 make base MySQL, ZKx3 ~1.5GB Layer 1-3 make storage JNx3, NNx2, ZKFCx2, DNx3 ~3.5GB Layer 4 make compute RMx2, NMx3, JobHistory ~2.5GB Layer 5-7 make analytics Hivex2, HBasex5, Kafkax3, Spark/Flink Historyx2 ~3.5GB 全部命令参考\r#\r命令 说明 make build 构建统一镜像 make all 一键全量启动 make base 启动基础设施层 make storage 启动 HDFS HA 存储层 make compute 启动 YARN HA 计算层 make analytics 启动分析层（Hive/HBase/Kafka/Spark/Flink） make status 容器资源占用 make ps 容器列表 make verify Level 1 冒烟测试（7项） make verify-spark Level 2a Spark 功能验证 make verify-flink Level 2b Flink 功能验证 make verify-ha Level 3 HA 故障转移测试（10项） make init-hdfs HDFS 首次格式化+初始化 make stop 反向停止所有服务 make restart 重启全部 make clean 清理容器和网络 make logs 查看全部日志 make logs-\u0026lt;name\u0026gt; 查看指定容器日志 make shell-\u0026lt;name\u0026gt; 进入指定容器 验证测试\r#\rLevel 1: 冒烟测试（7项）\r#\r# 测试项 验证内容 S1 HDFS 读写 写入→读取→删除 S2 YARN 节点 \u0026gt;=3 个 NM RUNNING S3 ZK 集群 1 leader + 2 follower S4 Hive Metastore 端口 9083 可达 S5 HiveServer2 端口 10000 可达 S6 HBase Shell status 命令正常 S7 Kafka E2E 创建→生产→消费→删除 Level 2: Spark/Flink 功能测试（6项）\r#\r# 测试项 SP1 Spark Pi（YARN cluster 模式） SP2 Spark SQL 查询 SP3 Spark History Server HTTP FL1 Flink YARN Session 创建 FL2 Flink WordCount 批作业 FL3 Flink History Server HTTP Level 3: HA 故障转移测试（10项）\r#\r# 测试项 故障注入 HA1 NN 故障转移 停 namenode-active HA2 NN 恢复 启 namenode-active HA3 HDFS 容错 NN 切换后读写 HA4 RM 故障转移 停 resourcemanager-1 HA5 Spark on HA RM 切换后提交 HA6 Flink on HA RM 切换后 Session HA7 HMaster 转移 停 hbase-master-1 HA8 JN 容错 停 1/3 JN HA9 ZK 容错 停 1/3 ZK HA10 Kafka 容错 停 1/3 Broker Web UI 访问\r#\r服务 URL NameNode Active http://localhost:9870 NameNode Standby http://localhost:9871 YARN RM1 http://localhost:8088 YARN RM2 http://localhost:8089 JobHistory http://localhost:19888 HBase Master1 http://localhost:16010 HBase Master2 http://localhost:16011 Spark History http://localhost:18080 Flink History http://localhost:8082 故障排查\r#\r容器启动失败\r#\rdocker compose --profile \u0026lt;name\u0026gt; logs # 查看该层日志 make logs-\u0026lt;container_name\u0026gt; # 查看特定容器日志 make shell-\u0026lt;container_name\u0026gt; # 进入容器排查\r内存不足\r#\r调整 .env 中 JVM 堆参数（如 NAMENODE_HEAP=128m） 按分层启动而非一键全量 关闭 Windows 上其他大型应用 HDFS 不正常\r#\rmake init-hdfs # 重新格式化（会丢失数据） docker compose --profile storage restart # 重启存储层\r端口冲突\r#\r修改 docker-compose.yml 中 ports 映射（冒号左边为宿主机端口）。\n与裸机/Ansible 方案的关系\r#\r特性 Docker 方案 Ansible 方案 用途 本地测试/学习 生产裸机部署 内存 10-11GB 12GB+ HA 全生产级 全生产级 配置 独立 config/ 目录 Jinja2 模板 管理 Makefile + docker compose ansible-playbook 网络 Bridge（172.25.0.0/16） 物理网卡 ","date":"May 29, 2026","externalUrl":null,"permalink":"/posts/docker-%E5%A4%A7%E6%95%B0%E6%8D%AE%E9%9B%86%E7%BE%A4%E9%AB%98%E5%8F%AF%E7%94%A8%E9%83%A8%E7%BD%B2/","section":"Posts","summary":"Docker 大数据集群高可用部署\r#\rDocker Compose 编排的 32 容器全组件 HA 大数据集群，用于本地测试与学习。\n架构概述\r#\r┌─────────────────────────────────────────────────┐ │ Docker Bridge: bigdata-net │ │ 172.25.0.0/16 │ ├─────────────────────────────────────────────────┤ │ Layer 0 (base): MySQL + ZooKeeper x3 │ │ Layer 1 (storage): JournalNode x3 │ │ Layer 2 (storage): NameNode x2 + ZKFC x2 │ │ Layer 3 (storage): DataNode x3 │ │ Layer 4 (compute): ResourceManager x2 + NM x3 │ │ Layer 5 (analytics): Hive Metastore/Server2 │ │ Layer 6 (analytics): HBase x5 + Kafka x3 │ │ Layer 7 (analytics): Spark + Flink History │ └─────────────────────────────────────────────────┘\r组件 版本 HA 方案 容器数 HDFS 3.3.6 双 NameNode + 3 JournalNode + ZKFC 10 YARN 3.3.6 双 ResourceManager + ZK 状态存储 6 ZooKeeper 3.7.1 3 节点仲裁 3 HBase 2.6.4 双 Master + ZK 发现 5 Hive 3.1.3 Metastore + Server2（独立容器） 2 Kafka 2.8.0 3 Broker + ZK 协调 3 Spark 3.5.8 History Server（YARN 模式按需提交） 1 Flink 1.19.1 History Server（YARN 模式按需提交） 1 MySQL 5.7 — 1 总计：32 容器，~10-11GB 内存\n","title":"Docker 大数据集群高可用部署","type":"posts"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/hadoop/","section":"Tags","summary":"","title":"Hadoop","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/hbase/","section":"Tags","summary":"","title":"HBase","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/high-availability/","section":"Tags","summary":"","title":"High-Availability","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/hive/","section":"Tags","summary":"","title":"Hive","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/kafka/","section":"Tags","summary":"","title":"Kafka","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/mllib/","section":"Tags","summary":"","title":"MLlib","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/en/posts/","section":"","summary":"","title":"Posts","type":"en"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/spark/","section":"Tags","summary":"","title":"Spark","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/categories/tutorial/","section":"Categories","summary":"","title":"Tutorial","type":"categories"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/%E9%83%A8%E7%BD%B2/","section":"Tags","summary":"","title":"部署","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/%E8%BD%A6%E8%81%94%E7%BD%91/","section":"Tags","summary":"","title":"车联网","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/%E5%A4%A7%E6%95%B0%E6%8D%AE/","section":"Tags","summary":"","title":"大数据","type":"tags"},{"content":"\r大数据集群 Ansible 自动化部署教程\r#\r基于 Ansible 的声明式自动化部署方案，与现有 Shell/Python 脚本并存，适用于 3-10 节点集群。\n一、前置准备\r#\r1.1 管理机要求\r#\r需求 说明 OS Linux / macOS（Windows WSL 兼容） Ansible \u0026gt;= 2.15（安装方法见下） sshpass yum install sshpass（CentOS）或 apt install sshpass（Ubuntu） 网络 可 SSH 到达所有集群节点 1.2 集群节点要求\r#\r需求 说明 OS CentOS 7.9+ / Rocky Linux 8+ SSH 已启用、密码认证或密钥认证 Sudo hadoop 用户有 sudo 权限 磁盘 /opt/module 至少 10GB 可用空间 1.3 安装 Ansible\r#\r# CentOS / Rocky sudo yum install -y epel-release sudo yum install -y ansible # Ubuntu / Debian sudo apt update \u0026amp;\u0026amp; sudo apt install -y ansible # macOS brew install ansible # 验证安装 ansible --version # 应显示 \u0026gt;= 2.15\r1.4 本地缓存安装包（推荐，加速部署）\r#\rcd ansible/files/cache/ # 下载 Hadoop 各组件，已下载的跳过（共约 1.8GB） wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/hadoop/common/hadoop-3.3.6/hadoop-3.3.6.tar.gz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/zookeeper-3.5.9/apache-zookeeper-3.5.9-bin.tar.gz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/hive/hive-3.1.3/apache-hive-3.1.3-bin.tar.gz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/hbase/2.6.4/hbase-2.6.4-hadoop3-bin.tar.gz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/kafka/2.8.0/kafka_2.13-2.8.0.tgz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/spark/spark-3.5.8/spark-3.5.8-bin-hadoop3-scala2.13.tgz wget -c https://maven.aliyun.com/repository/public/mysql/mysql-connector-java/5.1.49/mysql-connector-java-5.1.49.jar\rJDK 需预先手动解压到所有节点的 /opt/module/jdk1.8.0_471（或通过 common role 自动下载，但 Adoptium 镜像速度较慢，建议手动安装）。\n二、集群配置\r#\r2.1 编辑 Inventory\r#\r编辑 ansible/inventory/hosts.yml，修改节点信息为你的实际环境：\n# 只需修改这里 cluster: hosts: node1: ansible_host: 192.168.137.155 # 主节点 IP hostname: hd2 # 主节点主机名 node2: ansible_host: 192.168.137.156 hostname: hd3 node3: ansible_host: 192.168.137.157 hostname: hd4 # 添加更多节点... # node4: # ansible_host: 192.168.137.158 # hostname: hd5\r2.2 修改全局变量\r#\r编辑 ansible/inventory/hosts.yml 中 all.vars 部分的网络和凭据配置：\n# 网络模式: \u0026#34;hostname\u0026#34;（使用主机名.域名）或 \u0026#34;ip\u0026#34;（直接使用IP） network_mode: \u0026#34;hostname\u0026#34; # 改为 \u0026#34;ip\u0026#34; 如果无 DNS domain_suffix: \u0026#34;pblh123.cn\u0026#34; # 改为你的域名后缀 # SSH 凭据 ansible_user: hadoop ansible_password: hadoop # 改为你的密码 # MySQL（Hive 元数据库） mysql: host: localhost database: hive_metastore user: hive password: \u0026#34;Hive@12345\u0026#34; # 改为你的 MySQL 密码\r2.3 完整变量参考\r#\r配置组 变量 默认值 说明 网络 network_mode hostname 域名模式：hostname 或 ip 网络 domain_suffix pblh123.cn 域名后缀 SSH ansible_user hadoop SSH 登录用户 SSH ansible_password hadoop SSH 登录密码 路径 base_dir /opt/module 组件安装根目录 路径 data_dir /opt/data 数据存储根目录 版本 versions.hadoop hadoop-3.3.6 Hadoop 版本目录名 版本 versions.zookeeper apache-zookeeper-3.5.9-bin ZK 版本目录名 版本 versions.hive apache-hive-3.1.3-bin Hive 版本目录名 版本 versions.hbase hbase-2.6.4-hadoop3 HBase 版本目录名 版本 versions.kafka kafka_2.13-2.8.0 Kafka 版本目录名 版本 versions.spark spark-3.5.8-bin-hadoop3-scala2.13 Spark 版本目录名 HDFS hdfs.replication 2 HDFS 副本数 Spark spark.driver_memory 512m Spark Driver 内存 Spark spark.executor_memory 512m Spark Executor 内存 JVM java_heap.namenode 512m NameNode 堆大小 三、部署步骤\r#\r3.1 预检查\r#\r# 测试 SSH 连通性 ansible -i ansible/inventory/hosts.yml cluster -m ping # 预期输出: 每个节点返回 SUCCESS / \u0026#34;pong\u0026#34; # 如果失败，检查: # 1. IP 是否正确 # 2. SSH 用户名/密码是否正确 # 3. 节点是否在线 # 查看节点详情 ansible -i ansible/inventory/hosts.yml cluster -m setup -a \u0026#34;filter=ansible_memtotal_mb\u0026#34;\r3.2 部署方式一：分步执行（推荐首次使用）\r#\rcd ansible/ # 步骤1: 安装组件（下载 + 解压） ansible-playbook -i inventory/hosts.yml playbooks/install.yml # 步骤2: 配置集群（分发配置文件） ansible-playbook -i inventory/hosts.yml playbooks/configure.yml # 步骤3: 格式化 HDFS 并启动服务 # 先只格式化 + 启动 ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml --tags format,hdfs,zookeeper,yarn\r3.3 部署方式二：一键部署\r#\rcd ansible/ # 完整部署（含 HDFS 格式化） ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml # 重新部署（跳过 HDFS 格式化，保留数据） ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml --skip-tags format # 仅部署到特定节点 ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml --limit node1 # 仅安装特定组件 ansible-playbook -i inventory/hosts.yml playbooks/deploy.yml --tags hadoop,hive\r3.4 按需执行\r#\r# 仅重新分发配置（无需重启） ansible-playbook -i inventory/hosts.yml playbooks/configure.yml # 仅重新分发 Hadoop 配置 ansible-playbook -i inventory/hosts.yml playbooks/configure.yml --tags hadoop # 添加新节点后，仅配置新节点 ansible-playbook -i inventory/hosts.yml playbooks/configure.yml --limit node4\r四、集群管理\r#\r4.1 查看服务状态\r#\r# JPS 进程 + 端口检查 ansible-playbook -i inventory/hosts.yml playbooks/status.yml\r4.2 启停服务\r#\r# 启动全部服务 ansible-playbook -i inventory/hosts.yml playbooks/start.yml # 停止全部服务 ansible-playbook -i inventory/hosts.yml playbooks/stop.yml # 重启全部服务 ansible-playbook -i inventory/hosts.yml playbooks/restart.yml # 启动单个服务 ansible-playbook -i inventory/hosts.yml playbooks/start.yml --tags kafka # 停止单个服务 ansible-playbook -i inventory/hosts.yml playbooks/stop.yml --tags hbase\r4.3 功能验证\r#\r# 13 项端到端检查（失败不中断，生成汇总报告） ansible-playbook -i inventory/hosts.yml playbooks/verify.yml\r验证项目：\n# 检查项 方法 1 节点连通性 ping 2 HDFS 读写 写文件 → 读回 → 校验 → 删除 3 HDFS 容量 hdfs dfsadmin -report 4 YARN 节点 yarn node -list 5 ZooKeeper 角色 echo stat | nc localhost 2181 (逐个节点) 6 Hive Metastore 端口 TCP 9083 7 HiveServer2 端口 TCP 10000 8 HBase Shell echo \u0026quot;status\u0026quot; | hbase shell 9 Kafka 端到端 创建主题 → 生产 → 消费 → 删除 10 Spark Pi 作业 spark-submit --class SparkPi 集群模式 11 Spark History 端口 TCP 18080 12 Web UI（4 个） HDFS/YARN/Spark/HBase HTTP 可达 五、Web 管理界面\r#\r部署完成后，通过浏览器访问：\n服务 URL HDFS NameNode http://\u0026lt;master_ip\u0026gt;:9870 YARN ResourceManager http://\u0026lt;master_ip\u0026gt;:8088 Spark History Server http://\u0026lt;master_ip\u0026gt;:18080 HBase Master http://\u0026lt;master_ip\u0026gt;:16010 六、常见问题\r#\r6.1 SSH 连接失败\r#\r# 检查 SSH 连通性 ssh hadoop@192.168.137.155 \u0026#34;hostname\u0026#34; # 确认密码认证已启用 sudo grep PasswordAuthentication /etc/ssh/sshd_config # 应该是 yes，如果不是: sudo sed -i \u0026#39;s/PasswordAuthentication no/PasswordAuthentication yes/\u0026#39; /etc/ssh/sshd_config sudo systemctl restart sshd\r6.2 sudo 需要 TTY\r#\r如果遇到 sudo: a terminal is required 错误：\n# 在 Ansible 不启用 become 时通常不需要 PTY # 如果仍然报错，在所有节点上执行: sudo visudo # 注释掉或删除: Defaults requiretty # 或添加: Defaults:hadoop !requiretty\r6.3 端口被占用\r#\r# 查看哪个进程占用了端口 sudo netstat -tlnp | grep \u0026lt;端口号\u0026gt; # 或 sudo ss -tlnp | grep \u0026lt;端口号\u0026gt; # 强制停止所有 Java 进程 jps -l | grep -v Jps | awk \u0026#39;{print $1}\u0026#39; | xargs -r kill -9\r6.4 服务启动超时\r#\r# 检查组件是否已安装 ls /opt/module/ # 查看日志 tail -100 /opt/module/hadoop-3.3.6/logs/*.log # 手动启动调试 source /etc/profile.d/hadoop_env.sh hdfs namenode # 前台启动 NameNode 查看错误\r6.5 添加新节点\r#\r在 inventory/hosts.yml 中添加新节点 分配 zk_myid 和 kafka_broker_id（递增） 运行配置分发： ansible-playbook -i inventory/hosts.yml playbooks/configure.yml --limit \u0026lt;new_node\u0026gt;\r重启受影响的服务（ZooKeeper、HBase regionservers、Kafka） 6.6 下载速度慢\r#\r# 方案1: 预下载到本地缓存（推荐） cd ansible/files/cache/ # 用国内镜像下载全部组件 # 方案2: 修改镜像源 # 编辑 inventory/hosts.yml 中的 mirrors.apache 为阿里云镜像 # mirrors.apache: \u0026#34;https://mirrors.aliyun.com/apache\u0026#34;\r七、与 Shell/Python 脚本的关系\r#\r两种方案独立运行、互不影响：\n场景 推荐方案 初次部署、配置一致性要求高 Ansible 快速启停、日常运维 Shell 脚本（cluster_ctl.sh） 远程管理 Python 脚本（cluster_ctl.py） 配置漂移修复 Ansible configure.yml Ansible 和现有脚本共享相同的 config.ini 配置语义，但使用独立的 inventory/hosts.yml 变量文件。修改配置时需同步更新两处。\n","date":"May 29, 2026","externalUrl":null,"permalink":"/posts/%E5%A4%A7%E6%95%B0%E6%8D%AE%E9%9B%86%E7%BE%A4-ansible-%E8%87%AA%E5%8A%A8%E5%8C%96%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B/","section":"Posts","summary":"大数据集群 Ansible 自动化部署教程\r#\r基于 Ansible 的声明式自动化部署方案，与现有 Shell/Python 脚本并存，适用于 3-10 节点集群。\n一、前置准备\r#\r1.1 管理机要求\r#\r需求 说明 OS Linux / macOS（Windows WSL 兼容） Ansible \u003e= 2.15（安装方法见下） sshpass yum install sshpass（CentOS）或 apt install sshpass（Ubuntu） 网络 可 SSH 到达所有集群节点 1.2 集群节点要求\r#\r需求 说明 OS CentOS 7.9+ / Rocky Linux 8+ SSH 已启用、密码认证或密钥认证 Sudo hadoop 用户有 sudo 权限 磁盘 /opt/module 至少 10GB 可用空间 1.3 安装 Ansible\r#\r# CentOS / Rocky sudo yum install -y epel-release sudo yum install -y ansible # Ubuntu / Debian sudo apt update \u0026\u0026 sudo apt install -y ansible # macOS brew install ansible # 验证安装 ansible --version # 应显示 \u003e= 2.15\r1.4 本地缓存安装包（推荐，加速部署）\r#\rcd ansible/files/cache/ # 下载 Hadoop 各组件，已下载的跳过（共约 1.8GB） wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/hadoop/common/hadoop-3.3.6/hadoop-3.3.6.tar.gz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/zookeeper-3.5.9/apache-zookeeper-3.5.9-bin.tar.gz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/hive/hive-3.1.3/apache-hive-3.1.3-bin.tar.gz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/hbase/2.6.4/hbase-2.6.4-hadoop3-bin.tar.gz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/kafka/2.8.0/kafka_2.13-2.8.0.tgz wget -c https://mirrors.tuna.tsinghua.edu.cn/apache/spark/spark-3.5.8/spark-3.5.8-bin-hadoop3-scala2.13.tgz wget -c https://maven.aliyun.com/repository/public/mysql/mysql-connector-java/5.1.49/mysql-connector-java-5.1.49.jar\rJDK 需预先手动解压到所有节点的 /opt/module/jdk1.8.0_471（或通过 common role 自动下载，但 Adoptium 镜像速度较慢，建议手动安装）。\n","title":"大数据集群 Ansible 自动化部署教程","type":"posts"},{"content":"\r大数据集群分布式部署教程（完整验证版）\r#\r本教程提供每一步都可复制执行的大数据集群部署方案，包含全部配置文件的完整内容。所有步骤已在 3 节点 CentOS 7.9 集群上验证通过。\n变量适配说明\r#\r部署新集群时，将教程中的以下值替换为实际环境配置：\n变量 教程示例值 替换为你的值 域名后缀 pblh123.cn 你的域名，无域名的用 localdomain 节点数量 3 实际节点数（需同步调整副本数、ZK myid、Kafka broker.id） 主节点 hd2 / 192.168.137.155 你的主节点主机名/IP 节点2 hd3 / 192.168.137.156 你的第二个节点 节点3 hd4 / 192.168.137.157 你的第三个节点 Hadoop版本 hadoop-3.3.6 实际安装版本目录名 JDK版本 jdk1.8.0_471 实际安装版本目录名 HDFS副本数 2 通常设为 min(节点数, 3) MySQL密码 Hive@12345 你的MySQL hive用户密码 安装根目录 /opt/module 各组件解压目录 数据根目录 /opt/data HDFS/ZK/Kafka 数据存储目录 NodeManager内存 hd2:2800, hd3:2000, hd4:1200 按实际物理内存的70-80%分配(MB) SSH用户 hadoop 运行服务的系统用户 SSH密码 hadoop 相应密码 替换公式: ${组件路径} = /opt/module/${版本目录名}，教程中所有路径均由公式推导，修改版本号后路径自动适用。\n目录\r#\r架构设计 环境准备 MySQL 安装配置 Hadoop HDFS 部署 YARN 部署 ZooKeeper 部署 Hive 部署 HBase 部署 Kafka 部署 Spark 部署 集群验证 集群管理 一键脚本使用 故障排查 附录：适配新集群 Checklist 一、架构设计\r#\r1.1 集群拓扑\r#\r┌─────────────────────────────────────┐ │ hd2 (4G/2C) │ │ 192.168.137.155 │ │ NameNode, ResourceManager │ │ HBase Master, Hive Metastore │ │ HiveServer2, JobHistoryServer │ │ ZooKeeper, Kafka, Spark │ └──────────────┬──────────────────────┘ │ ┌─────────────────┼─────────────────┐ │ │ │ ┌────────┴────────┐ ┌──────┴──────┐ ┌───────┴───────┐ │ hd3 (3G/2C) │ │ hd4 (2G/2C) │ │ (可扩展) │ │ 192.168.137.156 │ │ 192.168.137.157 │ │ │ DataNode │ │ DataNode │ │ │ │ NodeManager │ │ NodeManager │ │ │ │ SecondaryNN │ │ ZK, Kafka │ │ │ │ ZK, Kafka │ │ HBase RS │ │ │ │ HBase RS │ │ │ │ │ └─────────────────┘ └──────────────┘ └───────────────┘\r1.2 服务分配矩阵\r#\r服务 hd2 hd3 hd4 说明 NameNode Y HDFS 元数据管理 DataNode Y Y Y 数据存储 SecondaryNameNode Y Checkpoint 合并 ResourceManager Y YARN 资源调度 NodeManager Y Y Y 任务执行 JobHistoryServer Y MR 历史 ZooKeeper Y Y Y 分布式协调 Hive Metastore Y 元数据服务 HiveServer2 Y SQL 查询 HBase Master Y Region 管理 HBase RegionServer Y Y Y 数据读写 Kafka Broker Y Y Y 消息队列 Spark HistoryServer Y 任务历史 1.3 服务依赖与启动顺序\r#\rMySQL ──► HDFS ──┬──► YARN ──► JobHistory │ ├──► ZooKeeper ──┬──► HBase │ │ │ └──► Kafka │ └──► Hive Metastore ──► HiveServer2 ──► Spark HistoryServer\r启动顺序: HDFS → (ZooKeeper + YARN) → (Hive → HBase → Kafka → Spark)\n停止顺序: Spark → Kafka → HBase → Hive → YARN → ZooKeeper → HDFS\n二、环境准备\r#\r2.1 硬件要求\r#\r节点 内存 CPU 磁盘 hd2 ≥4GB ≥2核 ≥40GB hd3 ≥3GB ≥2核 ≥40GB hd4 ≥2GB ≥2核 ≥40GB 2.2 软件版本\r#\r组件 版本 安装路径 JDK 1.8.0_471 /opt/module/jdk1.8.0_471 Hadoop 3.3.6 /opt/module/hadoop-3.3.6 ZooKeeper 3.5.9 /opt/module/apache-zookeeper-3.5.9-bin Hive 3.1.3 /opt/module/apache-hive-3.1.3-bin HBase 2.6.4 /opt/module/hbase-2.6.4-hadoop3 Kafka 2.8.0 /opt/module/kafka_2.13-2.8.0 Spark 3.5.8 /opt/module/spark-3.5.8-bin-hadoop3-scala2.13 MySQL 5.7+ 系统 yum 安装 MySQL JDBC 8.0.33 /opt/module/apache-hive-3.1.3-bin/lib/ 2.3 创建用户（所有节点）\r#\r# 每节点上执行（使用 root） useradd hadoop echo \u0026#34;hadoop\u0026#34; | passwd --stdin hadoop # 授予 sudo 权限 echo \u0026#34;hadoop ALL=(ALL) NOPASSWD: ALL\u0026#34; \u0026gt;\u0026gt; /etc/sudoers\r2.4 配置 /etc/hosts（所有节点）\r#\r# 每节点上执行（使用 root） cat \u0026gt;\u0026gt; /etc/hosts \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; 192.168.137.155 hd2.pblh123.cn hd2 192.168.137.156 hd3.pblh123.cn hd3 192.168.137.157 hd4.pblh123.cn hd4 EOF\r适配说明: 替换 IP 和主机名为你的实际值。主机名建议用短名（如 hd2），FQDN 用 主机名.域名。\n2.5 SSH 免密登录（所有节点）\r#\r# 使用 hadoop 用户，在主节点上执行 su - hadoop ssh-keygen -t rsa -N \u0026#34;\u0026#34; -f ~/.ssh/id_rsa # 分发公钥到所有节点（包括本机） for host in hd2 hd3 hd4; do ssh-copy-id -o StrictHostKeyChecking=no hadoop@$host done # 验证（三节点互相ssh不提示密码） ssh hd3 \u0026#34;hostname\u0026#34; ssh hd4 \u0026#34;hostname\u0026#34;\r2.6 关闭防火墙和 SELinux（所有节点，root）\r#\r# 每节点上执行 systemctl stop firewalld \u0026amp;\u0026amp; systemctl disable firewalld setenforce 0 sed -i \u0026#39;s/SELINUX=enforcing/SELINUX=disabled/\u0026#39; /etc/selinux/config\r2.7 JDK 安装（所有节点，root）\r#\r# 解压 JDK 到指定目录 tar -zxf jdk-8u471-linux-x64.tar.gz -C /opt/module/ # 验证 /opt/module/jdk1.8.0_471/bin/java -version # 预期输出: java version \u0026#34;1.8.0_471\u0026#34;\r2.8 创建目录结构（所有节点，hadoop 用户）\r#\r# 每节点上执行 mkdir -p /opt/data/{namenode,datanode,hadoop-tmp,zookeeper,kafkadata} # hd2 额外 mkdir -p /opt/data/{hive-metastore.log,hiveserver2.log}\r2.9 全局环境变量（所有节点，root）\r#\r创建 /etc/profile.d/hadoop_env.sh，所有服务进程通过此脚本获取环境变量。\nsudo tee /etc/profile.d/hadoop_env.sh \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # JAVA JAVA_HOME=/opt/module/jdk1.8.0_471 # Hadoop HADOOP_HOME=/opt/module/hadoop-3.3.6 # Kafka KAFKA_HOME=/opt/module/kafka_2.13-2.8.0 # Hive HIVE_HOME=/opt/module/apache-hive-3.1.3-bin # Spark SPARK_HOME=/opt/module/spark-3.5.8-bin-hadoop3-scala2.13 # HBase HBASE_HOME=/opt/module/hbase-2.6.4-hadoop3 # ZooKeeper ZOOKEEPER_HOME=/opt/module/apache-zookeeper-3.5.9-bin PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$KAFKA_HOME/bin:$HIVE_HOME/bin:$SPARK_HOME/bin:$HBASE_HOME/bin:$ZOOKEEPER_HOME/bin EOF # 生效 source /etc/profile.d/hadoop_env.sh\r适配说明: 修改 JAVA_HOME、各 *_HOME 路径为实际版本目录名。\n三、MySQL 安装配置\r#\r仅在 hd2（主节点）执行，用于 Hive 元数据存储。\n3.1 安装 MySQL\r#\r# root@hd2 yum install -y mysql-community-server 2\u0026gt;/dev/null || { # 如无 MySQL repo，使用 MariaDB yum install -y mariadb-server mariadb systemctl start mariadb \u0026amp;\u0026amp; systemctl enable mariadb }\r3.2 创建 Hive 元数据库\r#\rmysql -u root \u0026lt;\u0026lt; EOF CREATE DATABASE IF NOT EXISTS hive_metastore DEFAULT CHARACTER SET utf8; CREATE USER IF NOT EXISTS \u0026#39;hive\u0026#39;@\u0026#39;%\u0026#39; IDENTIFIED BY \u0026#39;Hive@12345\u0026#39;; GRANT ALL PRIVILEGES ON hive_metastore.* TO \u0026#39;hive\u0026#39;@\u0026#39;%\u0026#39;; FLUSH PRIVILEGES; -- 验证 SHOW DATABASES; SELECT user, host FROM mysql.user WHERE user=\u0026#39;hive\u0026#39;; EOF\r适配说明: 替换 Hive@12345 为实际密码。教程中所有出现此密码的位置需同步修改。\n四、Hadoop HDFS 部署\r#\r4.1 core-site.xml\r#\r文件位置: ${HADOOP_HOME}/etc/hadoop/core-site.xml\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;?xml-stylesheet type=\u0026#34;text/xsl\u0026#34; href=\u0026#34;configuration.xsl\u0026#34;?\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!-- NameNode RPC 地址 --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;fs.defaultFS\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hdfs://hd2.pblh123.cn:9000\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- Hadoop 代理用户（Hive/Spark 访问 HDFS 需要） --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hadoop.proxyuser.hadoop.hosts\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;*\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hadoop.proxyuser.hadoop.groups\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;*\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hadoop.proxyuser.hive.hosts\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;*\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hadoop.proxyuser.hive.groups\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;*\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- 临时目录 --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hadoop.tmp.dir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;/opt/data/hadoop-tmp\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt;\r适配: 替换 hd2.pblh123.cn:9000 中的主机名和端口。\n4.2 hdfs-site.xml\r#\r文件位置: ${HADOOP_HOME}/etc/hadoop/hdfs-site.xml\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;?xml-stylesheet type=\u0026#34;text/xsl\u0026#34; href=\u0026#34;configuration.xsl\u0026#34;?\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!-- 数据副本数（3节点集群建议2或3） --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.replication\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;2\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- NameNode 元数据存储目录 --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.namenode.name.dir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;file:///opt/data/namenode\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- DataNode 数据块存储目录 --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.datanode.data.dir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;file:///opt/data/datanode\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- 块大小 --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.blocksize\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;64m\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- NameNode Web UI 地址 --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.namenode.http-address\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hd2.pblh123.cn:9870\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- SecondaryNameNode Web UI --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.namenode.secondary.http-address\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hd3.pblh123.cn:9868\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- 关闭权限检查（开发环境） --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;dfs.permissions.enabled\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;false\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt;\r适配: 替换主机名。副本数 dfs.replication 不能超过 DataNode 节点数。\n4.3 workers（DataNode 列表）\r#\r文件位置: ${HADOOP_HOME}/etc/hadoop/workers\nhd2.pblh123.cn hd3.pblh123.cn hd4.pblh123.cn\r适配: 每行一个 DataNode 的完整主机名（FQDN）。\n4.4 hadoop-env.sh（JVM 参数）\r#\r文件位置: ${HADOOP_HOME}/etc/hadoop/hadoop-env.sh\nexport JAVA_HOME=/opt/module/jdk1.8.0_471 export HADOOP_HOME=/opt/module/hadoop-3.3.6 export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop export HADOOP_LOG_DIR=$HADOOP_HOME/logs export HADOOP_PID_DIR=$HADOOP_HOME/pids # JVM 堆内存（低内存 VM 优化） export HADOOP_HEAPSIZE_MAX=512m export HADOOP_HEAPSIZE_MIN=256m export HDFS_NAMENODE_OPTS=\u0026#34;-Xmx512m -Xms256m\u0026#34; export HDFS_DATANODE_OPTS=\u0026#34;-Xmx256m -Xms128m\u0026#34; export YARN_RESOURCEMANAGER_HEAPSIZE=512 export YARN_NODEMANAGER_HEAPSIZE=256\r适配: 修改 JAVA_HOME 和 HADOOP_HOME。内存充足的机器可适当调大堆内存。\n4.5 同步配置到所有节点\r#\r# 在 hd2 上执行（hadoop 用户） HADOOP_CONF=/opt/module/hadoop-3.3.6/etc/hadoop # 同步到 hd3 和 hd4 for host in hd3 hd4; do scp $HADOOP_CONF/{core-site.xml,hdfs-site.xml,workers,hadoop-env.sh} hadoop@$host:$HADOOP_CONF/ done # 验证：三节点配置文件一致 md5sum $HADOOP_CONF/core-site.xml ssh hd3 \u0026#34;md5sum $HADOOP_CONF/core-site.xml\u0026#34; ssh hd4 \u0026#34;md5sum $HADOOP_CONF/core-site.xml\u0026#34;\r4.6 格式化 NameNode\r#\r# 仅在 hd2 执行一次（hadoop 用户） source /etc/profile.d/hadoop_env.sh hdfs namenode -format # 预期输出末尾: ...common.Storage: Storage directory /opt/data/namenode has been successfully formatted.\r注意: 此命令仅在首次部署时执行。重复格式化会清空 HDFS 所有数据。如需重新格式化，先删除 /opt/data/namenode 和 /opt/data/datanode。\n4.7 启动 HDFS\r#\r# 在 hd2 上执行 start-dfs.sh # 等待 5 秒 sleep 5\r4.8 验证 HDFS\r#\r# 1. 进程检查 jps | grep -E \u0026#34;NameNode|DataNode\u0026#34; # 预期 hd2: NameNode + DataNode # hd3/hd4 上执行 jps 应有 DataNode # 2. 集群报告 hdfs dfsadmin -report # 预期: Live datanodes (3): 共3个节点在线 # 3. 读写测试 echo \u0026#34;Hello HDFS\u0026#34; | hdfs dfs -put - /test_verify.txt hdfs dfs -cat /test_verify.txt # 预期输出: Hello HDFS hdfs dfs -rm /test_verify.txt # 4. Web UI # 浏览器访问 http://192.168.137.155:9870 # Datanodes 标签页应显示 3 个节点\r五、YARN 部署\r#\r5.1 yarn-site.xml（含每节点差异化内存）\r#\rYARN 的 yarn.nodemanager.resource.memory-mb 需按节点内存配置。以下为三节点分别配置的版本。\nhd2 版本 (/opt/module/hadoop-3.3.6/etc/hadoop/yarn-site.xml):\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;?xml-stylesheet type=\u0026#34;text/xsl\u0026#34; href=\u0026#34;configuration.xsl\u0026#34;?\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.nodemanager.aux-services\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;mapreduce_shuffle\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.resourcemanager.hostname\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hd2.pblh123.cn\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.resourcemanager.webapp.address\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hd2.pblh123.cn:8088\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- ★ 内存：按节点实际物理内存 70-80% 配置，单位 MB --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.nodemanager.resource.memory-mb\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;2800\u0026lt;/value\u0026gt; \u0026lt;!-- hd2: 4G × 70% ≈ 2800MB --\u0026gt; \u0026lt;!-- hd3: 3G × 70% ≈ 2000MB --\u0026gt; \u0026lt;!-- hd4: 2G × 60% ≈ 1200MB --\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.scheduler.maximum-allocation-mb\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;2048\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.scheduler.minimum-allocation-mb\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;256\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.nodemanager.resource.cpu-vcores\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;2\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- 关闭虚拟内存检查（低内存 VM 必须） --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.nodemanager.vmem-check-enabled\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;false\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- 日志聚合 --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.log-aggregation-enable\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;true\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;yarn.log.server.url\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;http://hd2.pblh123.cn:19888/jobhistory/logs\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt;\r适配关键: yarn.nodemanager.resource.memory-mb 和 yarn.scheduler.maximum-allocation-mb 须按节点实际内存修改。公式：NM内存 = 物理内存 × 70%，调度最大 = min(NM内存 - 256, 2048)。\nhd3 的配置: 只需将 yarn.nodemanager.resource.memory-mb 改为 2000，yarn.scheduler.maximum-allocation-mb 改为 1744。\nhd4 的配置: 改为 1200 和 944。\n# 同步到各节点（分别修改内存后分发） for host in hd3 hd4; do scp $HADOOP_CONF/yarn-site.xml hadoop@$host:$HADOOP_CONF/ done\r5.2 mapred-site.xml\r#\r文件位置: ${HADOOP_HOME}/etc/hadoop/mapred-site.xml\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;?xml-stylesheet type=\u0026#34;text/xsl\u0026#34; href=\u0026#34;configuration.xsl\u0026#34;?\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;mapreduce.framework.name\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;yarn\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;mapreduce.jobhistory.address\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hd2.pblh123.cn:10020\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;mapreduce.jobhistory.webapp.address\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hd2.pblh123.cn:19888\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt;\r# 分发到所有节点 for host in hd3 hd4; do scp $HADOOP_CONF/mapred-site.xml hadoop@$host:$HADOOP_CONF/ done\r5.3 启动 YARN\r#\r# 在 hd2 上执行 source /etc/profile.d/hadoop_env.sh start-yarn.sh mapred --daemon start historyserver sleep 5\r5.4 验证 YARN\r#\r# 1. 节点列表（3 个 RUNNING） yarn node -list -all | grep RUNNING # 预期输出: 3 行，每行末尾 RUNNING # 2. 进程检查 jps | grep -E \u0026#34;ResourceManager|NodeManager|JobHistoryServer\u0026#34; # 3. MapReduce Pi 计算测试 hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.3.6.jar pi 2 5 # 预期: Estimated value of Pi is 3.148... # 4. Web UI # http://192.168.137.155:8088 — YARN 管理 # http://192.168.137.155:19888 — 任务历史\r六、ZooKeeper 部署\r#\r6.1 zoo.cfg（所有节点相同）\r#\r文件位置: ${ZOOKEEPER_HOME}/conf/zoo.cfg\ntickTime=2000 initLimit=10 syncLimit=5 dataDir=/opt/data/zookeeper clientPort=2181 maxClientCnxns=60 autopurge.snapRetainCount=3 autopurge.purgeInterval=1 # 集群节点列表 server.1=hd2.pblh123.cn:2888:3888 server.2=hd3.pblh123.cn:2888:3888 server.3=hd4.pblh123.cn:2888:3888\r适配: 替换主机名。新增节点需添加 server.N 行，N 为递增整数。\n6.2 myid（每个节点不同）\r#\r# hd2 上 echo 1 \u0026gt; /opt/data/zookeeper/myid # hd3 上 echo 2 \u0026gt; /opt/data/zookeeper/myid # hd4 上 echo 3 \u0026gt; /opt/data/zookeeper/myid\r注意: myid 的值必须与 zoo.cfg 中 server.X 的 X 完全对应。\n# 同步 zoo.cfg 到所有节点 ZK_CONF=/opt/module/apache-zookeeper-3.5.9-bin/conf for host in hd3 hd4; do scp $ZK_CONF/zoo.cfg hadoop@$host:$ZK_CONF/ done\r6.3 启动 ZooKeeper\r#\r# 在所有节点上执行 source /etc/profile.d/hadoop_env.sh zkServer.sh start # 等待 3 秒 sleep 3\r6.4 验证 ZooKeeper\r#\r# 1. 每个节点检查状态 zkServer.sh status # 预期: 1 个 leader，2 个 follower # 2. 客户端连接测试 zkCli.sh -server hd2.pblh123.cn:2181 \u0026lt;\u0026lt;\u0026lt; \u0026#34;ls /\u0026#34; # 预期: 成功连接，列出 / # 3. 四字命令 echo stat | nc hd2.pblh123.cn 2181 | grep Mode # 预期: Mode: leader 或 Mode: follower\r七、Hive 部署\r#\rHive 服务仅在 hd2（主节点）部署。\n7.1 MySQL JDBC 驱动\r#\r# 下载 MySQL JDBC 驱动到 Hive lib 目录 # 如无法访问外网，可手动下载后上传 HIVE_LIB=/opt/module/apache-hive-3.1.3-bin/lib wget -P $HIVE_LIB https://repo1.maven.org/maven2/mysql/mysql-connector-java/8.0.33/mysql-connector-java-8.0.33.jar # 或使用国内镜像 # wget -P $HIVE_LIB https://maven.aliyun.com/repository/public/mysql/mysql-connector-java/8.0.33/mysql-connector-java-8.0.33.jar\r7.2 hive-site.xml\r#\r文件位置: /opt/module/apache-hive-3.1.3-bin/conf/hive-site.xml\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;?xml-stylesheet type=\u0026#34;text/xsl\u0026#34; href=\u0026#34;configuration.xsl\u0026#34;?\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!-- Metastore 服务地址 --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hive.metastore.uris\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;thrift://hd2.pblh123.cn:9083\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- MySQL 连接（元数据库） --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;javax.jdo.option.ConnectionURL\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;jdbc:mysql://localhost:3306/hive_metastore?useSSL=false\u0026amp;amp;createDatabaseIfNotExist=true\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;javax.jdo.option.ConnectionDriverName\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;com.mysql.jdbc.Driver\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;javax.jdo.option.ConnectionUserName\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hive\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;javax.jdo.option.ConnectionPassword\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;Hive@12345\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- 数据仓库目录（在 HDFS 上） --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hive.metastore.warehouse.dir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;/user/hive/warehouse\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- HiveServer2 绑定 --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hive.server2.thrift.bind.host\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hd2.pblh123.cn\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hive.server2.thrift.port\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;10000\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt;\r适配: 替换 MySQL 连接信息、Metastore 和 HiveServer2 的主机名、密码。\n7.3 初始化 Hive 元数据库\r#\r# 在 hd2 上执行 source /etc/profile.d/hadoop_env.sh schematool -dbType mysql -initSchema # 预期输出: schemaTool completed\r常见错误: Access denied for user 'hive' — 检查 MySQL 密码和用户授权。Table 'hive_metastore.VERSION' doesn't exist — 说明未执行 initSchema。\n7.4 启动 Hive 服务\r#\r# 启动 Metastore（先） nohup hive --service metastore \u0026gt; /opt/data/hive-metastore.log 2\u0026gt;\u0026amp;1 \u0026amp; sleep 3 # 启动 HiveServer2（后） nohup hiveserver2 \u0026gt; /opt/data/hiveserver2.log 2\u0026gt;\u0026amp;1 \u0026amp; sleep 2\r7.5 验证 Hive\r#\r# 1. 进程检查 jps | grep RunJar # 预期: 2 个 RunJar 进程（Metastore + HiveServer2） # 2. Beeline 连接 beeline -u jdbc:hive2://hd2.pblh123.cn:10000 -n hadoop \u0026lt;\u0026lt; EOF SHOW DATABASES; CREATE DATABASE IF NOT EXISTS testdb; USE testdb; CREATE TABLE test (id INT, name STRING); INSERT INTO test VALUES (1, \u0026#39;hello\u0026#39;); SELECT * FROM test; DROP TABLE test; DROP DATABASE testdb; EOF # 3. 端口检查 netstat -tlnp | grep -E \u0026#34;9083|10000\u0026#34; # 预期: 两行分别显示 9083 和 10000 端口在 LISTEN\r八、HBase 部署\r#\r8.1 hbase-site.xml（所有节点相同）\r#\r文件位置: ${HBASE_HOME}/conf/hbase-site.xml\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;?xml-stylesheet type=\u0026#34;text/xsl\u0026#34; href=\u0026#34;configuration.xsl\u0026#34;?\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.cluster.distributed\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;true\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- HBase 根目录（存储在 HDFS 上） --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.rootdir\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hdfs://hd2.pblh123.cn:9000/hbase\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- ZooKeeper 集群地址 --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.zookeeper.quorum\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;hd2.pblh123.cn,hd3.pblh123.cn,hd4.pblh123.cn\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.zookeeper.property.clientPort\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;2181\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- 兼容性配置（HBase 2.x + Hadoop 3.x 需要） --\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.unsafe.stream.capability.enforce\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;false\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;hbase.wal.provider\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;filesystem\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/configuration\u0026gt;\r8.2 regionservers\r#\r文件位置: ${HBASE_HOME}/conf/regionservers\nhd2.pblh123.cn hd3.pblh123.cn hd4.pblh123.cn\r8.3 同步配置\r#\rHBASE_CONF=/opt/module/hbase-2.6.4-hadoop3/conf for host in hd3 hd4; do scp $HBASE_CONF/{hbase-site.xml,regionservers} hadoop@$host:$HBASE_CONF/ done\r8.4 启动 HBase\r#\r# 在 hd2 上执行 source /etc/profile.d/hadoop_env.sh start-hbase.sh sleep 5\r8.5 验证 HBase\r#\r# 1. 进程检查 # hd2: HMaster + HRegionServer # hd3: HRegionServer # hd4: HRegionServer jps | grep H # 2. HBase Shell echo \u0026#34;status\u0026#34; | hbase shell -n 2\u0026gt;/dev/null # 预期: 1 active master, 1 backup master, 3 region servers # 3. 读写测试 hbase shell \u0026lt;\u0026lt; EOF create \u0026#39;verify\u0026#39;, \u0026#39;cf\u0026#39; put \u0026#39;verify\u0026#39;, \u0026#39;row1\u0026#39;, \u0026#39;cf:col1\u0026#39;, \u0026#39;value1\u0026#39; scan \u0026#39;verify\u0026#39; disable \u0026#39;verify\u0026#39; drop \u0026#39;verify\u0026#39; EOF # 4. Web UI # http://192.168.137.155:16010\r九、Kafka 部署\r#\r9.1 server.properties（每个节点 broker.id 不同）\r#\r文件位置: ${KAFKA_HOME}/config/server.properties\n需要修改的属性（三节点相同部分）:\n# ZooKeeper 连接 zookeeper.connect=hd2.pblh123.cn:2181,hd3.pblh123.cn:2181,hd4.pblh123.cn:2181 # 监听地址 listeners=PLAINTEXT://:9092\r每节点不同的 broker.id:\n# hd2: broker.id=1 # hd3: broker.id=2 # hd4: broker.id=3 # 在 hd2 上 sed -i \u0026#39;s/^broker.id=.*/broker.id=1/\u0026#39; /opt/module/kafka_2.13-2.8.0/config/server.properties sed -i \u0026#39;s|^zookeeper.connect=.*|zookeeper.connect=hd2.pblh123.cn:2181,hd3.pblh123.cn:2181,hd4.pblh123.cn:2181|\u0026#39; /opt/module/kafka_2.13-2.8.0/config/server.properties # hd3 上（通过 ssh 或手动执行） ssh hd3 \u0026#34;sed -i \u0026#39;s/^broker.id=.*/broker.id=2/\u0026#39; /opt/module/kafka_2.13-2.8.0/config/server.properties\u0026#34; ssh hd3 \u0026#34;sed -i \u0026#39;s|^zookeeper.connect=.*|zookeeper.connect=hd2.pblh123.cn:2181,hd3.pblh123.cn:2181,hd4.pblh123.cn:2181|\u0026#39; /opt/module/kafka_2.13-2.8.0/config/server.properties\u0026#34; # hd4 上 ssh hd4 \u0026#34;sed -i \u0026#39;s/^broker.id=.*/broker.id=3/\u0026#39; /opt/module/kafka_2.13-2.8.0/config/server.properties\u0026#34; ssh hd4 \u0026#34;sed -i \u0026#39;s|^zookeeper.connect=.*|zookeeper.connect=hd2.pblh123.cn:2181,hd3.pblh123.cn:2181,hd4.pblh123.cn:2181|\u0026#39; /opt/module/kafka_2.13-2.8.0/config/server.properties\u0026#34;\r9.2 克隆节点特殊处理\r#\r如果节点是磁盘克隆的，/opt/data/kafkadata/meta.properties 中保存的 broker.id 可能与 server.properties 不一致，导致启动失败。\n# 在 hd3 和 hd4 上删除旧的 meta.properties ssh hd3 \u0026#34;rm -f /opt/data/kafkadata/meta.properties\u0026#34; ssh hd4 \u0026#34;rm -f /opt/data/kafkadata/meta.properties\u0026#34;\r9.3 启动 Kafka\r#\r# 在所有节点上执行 for host in hd2 hd3 hd4; do ssh $host \u0026#34;source /etc/profile.d/hadoop_env.sh \u0026amp;\u0026amp; nohup kafka-server-start.sh -daemon $KAFKA_HOME/config/server.properties \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 \u0026amp;\u0026#34; done sleep 3\r9.4 验证 Kafka\r#\r# 1. 进程检查 jps | grep Kafka # 2. 创建测试 Topic（3 分区 2 副本） kafka-topics.sh --bootstrap-server hd2.pblh123.cn:9092 \\ --create --topic verify-topic --partitions 3 --replication-factor 2 # 3. 查看 Topic 列表 kafka-topics.sh --bootstrap-server hd2.pblh123.cn:9092 --list # 4. 生产消费测试 echo \u0026#34;test message\u0026#34; | kafka-console-producer.sh \\ --bootstrap-server hd2.pblh123.cn:9092 --topic verify-topic kafka-console-consumer.sh \\ --bootstrap-server hd2.pblh123.cn:9092 \\ --topic verify-topic --from-beginning --max-messages 1 # 预期输出: test message # 5. 清理 kafka-topics.sh --bootstrap-server hd2.pblh123.cn:9092 --delete --topic verify-topic\r十、Spark 部署\r#\r10.1 spark-env.sh\r#\r文件位置: ${SPARK_HOME}/conf/spark-env.sh\n#!/usr/bin/env bash export JAVA_HOME=/opt/module/jdk1.8.0_471 export HADOOP_HOME=/opt/module/hadoop-3.3.6 export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop export SPARK_HOME=/opt/module/spark-3.5.8-bin-hadoop3-scala2.13 export SPARK_CONF_DIR=$SPARK_HOME/conf # History Server 配置 export SPARK_HISTORY_OPTS=\u0026#34;-Dspark.history.ui.port=18080 \\ -Dspark.history.fs.logDirectory=hdfs://hd2.pblh123.cn:9000/spark-history \\ -Dspark.history.retainedApplications=30\u0026#34; # Daemon 内存（低内存 VM） export SPARK_DAEMON_MEMORY=256m\r10.2 spark-defaults.conf\r#\r文件位置: ${SPARK_HOME}/conf/spark-defaults.conf\n# 运行模式：YARN 集群模式 spark.master yarn spark.submit.deployMode cluster # 事件日志（History Server 使用） spark.eventLog.enabled true spark.eventLog.dir hdfs://hd2.pblh123.cn:9000/spark-history spark.history.fs.logDirectory hdfs://hd2.pblh123.cn:9000/spark-history # 序列化 spark.serializer org.apache.spark.serializer.KryoSerializer # 内存调优（低内存 VM） spark.driver.memory 512m spark.executor.memory 512m spark.executor.cores 1 spark.executor.instances 2 spark.yarn.am.memory 512m spark.yarn.maxAppAttempts 2 # SQL 调优 spark.sql.shuffle.partitions 2 spark.driver.maxResultSize 256m\r适配: 内存充足的集群可增大 executor.memory、executor.instances 等参数。\n10.3 链接 hive-site.xml\r#\rSpark SQL 需要 Hive 配置来访问 Hive 元数据：\nln -sf /opt/module/apache-hive-3.1.3-bin/conf/hive-site.xml \\ /opt/module/spark-3.5.8-bin-hadoop3-scala2.13/conf/hive-site.xml\r10.4 同步配置\r#\rSPARK_CONF=/opt/module/spark-3.5.8-bin-hadoop3-scala2.13/conf for host in hd3 hd4; do scp $SPARK_CONF/{spark-env.sh,spark-defaults.conf} hadoop@$host:$SPARK_CONF/ ssh $host \u0026#34;ln -sf /opt/module/apache-hive-3.1.3-bin/conf/hive-site.xml $SPARK_CONF/hive-site.xml\u0026#34; done\r10.5 创建 HDFS 工作目录\r#\rhdfs dfs -mkdir -p /spark-history /user/hive/warehouse hdfs dfs -chmod 777 /spark-history\r10.6 启动 Spark History Server\r#\r# 在 hd2 上 source /etc/profile.d/hadoop_env.sh $SPARK_HOME/sbin/start-history-server.sh\r10.7 验证 Spark\r#\r# 1. Spark Pi 集群模式 spark-submit --master yarn --deploy-mode cluster \\ --class org.apache.spark.examples.SparkPi \\ $SPARK_HOME/examples/jars/spark-examples_2.13-3.5.8.jar 100 # 预期: 提交后 YARN 上出现 RUNNING 任务，完成后状态 FINISHED # 查看结果: yarn logs -applicationId \u0026lt;app_id\u0026gt; | grep \u0026#34;Pi is\u0026#34; # 2. Spark SQL + Hive 集成测试 spark-sql --master yarn --deploy-mode client \u0026lt;\u0026lt; EOF SHOW DATABASES; CREATE DATABASE IF NOT EXISTS testdb; USE testdb; CREATE TABLE test (id INT, name STRING); INSERT INTO test VALUES (1, \u0026#39;hello\u0026#39;); SELECT * FROM test; DROP TABLE test; DROP DATABASE testdb; EOF # 3. Web UI # http://192.168.137.155:18080 — Spark History Server\r十一、集群验证\r#\r11.1 完整进程检查\r#\r各节点执行 jps 后预期进程:\nhd2 (约 12 个):\nNameNode, DataNode, ResourceManager, NodeManager, QuorumPeerMain, JobHistoryServer, HMaster, HRegionServer, Kafka, HistoryServer, RunJar(Metastore), RunJar(HiveServer2)\rhd3 (约 7 个):\nDataNode, SecondaryNameNode, NodeManager, QuorumPeerMain, HRegionServer, Kafka\rhd4 (约 6 个):\nDataNode, NodeManager, QuorumPeerMain, HRegionServer, Kafka\r# 快速检查脚本 for host in hd2 hd3 hd4; do echo \u0026#34;=== $host ===\u0026#34; ssh $host \u0026#34;source /etc/profile.d/hadoop_env.sh \u0026amp;\u0026amp; jps -l | grep -v Jps | sort\u0026#34; echo \u0026#34;\u0026#34; done\r11.2 各组件快速验证\r#\r# HDFS 读写 echo \u0026#34;verify\u0026#34; | hdfs dfs -put - /verify.txt \u0026amp;\u0026amp; hdfs dfs -cat /verify.txt \u0026amp;\u0026amp; hdfs dfs -rm /verify.txt # YARN 计算 hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.3.6.jar pi 2 5 # ZooKeeper echo stat | nc hd2.pblh123.cn 2181 | grep Mode # Hive beeline -u jdbc:hive2://hd2.pblh123.cn:10000 -n hadoop -e \u0026#34;SHOW DATABASES;\u0026#34; # HBase echo \u0026#34;status\u0026#34; | hbase shell -n # Kafka kafka-topics.sh --bootstrap-server hd2.pblh123.cn:9092 --list # Spark spark-submit --master yarn --deploy-mode cluster \\ --class org.apache.spark.examples.SparkPi \\ $SPARK_HOME/examples/jars/spark-examples_2.13-3.5.8.jar 10\r十二、集群管理\r#\r12.1 一键启停\r#\r# Shell 版（在集群节点上运行） bash cluster_ctl.sh start # 启动全部 bash cluster_ctl.sh stop # 停止全部 bash cluster_ctl.sh restart # 重启全部 bash cluster_ctl.sh status # 查看状态 bash cluster_ctl.sh start kafka # 单独启动 Kafka # Python 版（从管理机运行） python cluster_ctl.py start python cluster_ctl.py stop python cluster_ctl.py restart python cluster_ctl.py status python cluster_ctl.py start hdfs\r12.2 关机脚本\r#\r# Shell 版 — 先停服务再关机 bash shutdown_cluster.sh # 跳过服务停止，直接强制关机 bash shutdown_cluster.sh --force # Python 版（从管理机执行） python shutdown_cluster.py python shutdown_cluster.py --force\r脚本流程: ① 确认提示 → ② 停止集群所有服务 → ③ 关闭从节点 → ④ 关闭主节点。\n12.3 服务独立操作\r#\r# HDFS start-dfs.sh / stop-dfs.sh # YARN start-yarn.sh / stop-yarn.sh # ZooKeeper zkServer.sh start / zkServer.sh stop / zkServer.sh status # Hive nohup hive --service metastore \u0026gt; /opt/data/hive-metastore.log 2\u0026gt;\u0026amp;1 \u0026amp; nohup hiveserver2 \u0026gt; /opt/data/hiveserver2.log 2\u0026gt;\u0026amp;1 \u0026amp; # HBase start-hbase.sh / stop-hbase.sh # Kafka kafka-server-start.sh -daemon $KAFKA_HOME/config/server.properties kafka-server-stop.sh # Spark History $SPARK_HOME/sbin/start-history-server.sh $SPARK_HOME/sbin/stop-history-server.sh\r12.4 日志路径\r#\r组件 日志路径 HDFS NameNode $HADOOP_HOME/logs/hadoop-hadoop-namenode-*.log HDFS DataNode $HADOOP_HOME/logs/hadoop-hadoop-datanode-*.log YARN RM $HADOOP_HOME/logs/yarn-hadoop-resourcemanager-*.log YARN NM $HADOOP_HOME/logs/yarn-hadoop-nodemanager-*.log ZooKeeper $ZOOKEEPER_HOME/logs/zookeeper.out Kafka $KAFKA_HOME/logs/server.log Spark $SPARK_HOME/logs/ HBase /opt/data/hbase/logs/ Hive /opt/data/hive-metastore.log, /opt/data/hiveserver2.log 12.5 端口清单\r#\r端口 服务 用途 9000 HDFS NameNode RPC 9870 HDFS NameNode Web UI 9868 HDFS SecondaryNameNode Web UI 8088 YARN ResourceManager Web UI 10020 MapReduce JobHistory RPC 19888 MapReduce JobHistory Web UI 2181 ZooKeeper Client 2888 ZooKeeper Peer 通信 3888 ZooKeeper Leader 选举 9083 Hive Metastore Thrift 10000 HiveServer2 Thrift 9092 Kafka Broker 16010 HBase Master Web UI 18080 Spark History Server Web UI 12.6 Web 管理界面\r#\r服务 URL HDFS NameNode http://192.168.137.155:9870 YARN ResourceManager http://192.168.137.155:8088 Spark History Server http://192.168.137.155:18080 HBase Master http://192.168.137.155:16010 Job History http://192.168.137.155:19888 十三、一键脚本使用\r#\r13.1 config.ini 模板\r#\r项目提供完整的一键部署脚本，只需修改 config.ini 后执行 bash deploy.sh。\n# ============================================================ # 大数据集群全局配置文件 # 修改此文件后执行 bash deploy.sh 即可完成一键部署 # ============================================================ # ---------- 集群节点 ---------- [NODES] hd2=192.168.137.155 hd3=192.168.137.156 hd4=192.168.137.157 # 各节点 YARN NodeManager 可用内存 (MB) [NODE_MEMORY] hd2=2800 hd3=2000 hd4=1200 [NODE_CPU] hd2=2 hd3=2 hd4=2 # ---------- SSH 登录 ---------- [SSH] user=hadoop password=hadoop # ---------- 安装路径 ---------- [PATHS] base_dir=/opt/module data_dir=/opt/data # ---------- 组件版本 ---------- [VERSIONS] jdk=jdk1.8.0_471 hadoop=hadoop-3.3.6 zookeeper=apache-zookeeper-3.5.9-bin hive=apache-hive-3.1.3-bin hbase=hbase-2.6.4-hadoop3 kafka=kafka_2.13-2.8.0 spark=spark-3.5.8-bin-hadoop3-scala2.13 # ---------- MySQL ---------- [MYSQL] host=localhost port=3306 database=hive_metastore user=hive password=Hive@12345 # ---------- 端口配置 ---------- [PORTS] hdfs_namenode=9000 hdfs_web=9870 yarn_web=8088 jobhistory=19888 zookeeper_client=2181 hive_metastore=9083 hive_server2=10000 hbase_master_web=16010 kafka=9092 spark_history=18080 # ---------- HDFS 配置 ---------- [HDFS] replication=2 block_size_mb=64 namenode_dir=/opt/data/namenode datanode_dir=/opt/data/datanode hadoop_tmp=/opt/data/hadoop-tmp # ---------- ZooKeeper ---------- [ZOOKEEPER] tick_time=2000 init_limit=10 sync_limit=5 data_dir=/opt/data/zookeeper # ---------- Spark 内存 ---------- [SPARK] driver_memory=512m executor_memory=512m executor_cores=1 executor_instances=2 am_memory=512m # ---------- Java 堆内存 ---------- [JAVA_HEAP] namenode=512m datanode=256m rm=512 nm=256\r13.2 部署流程\r#\r# 1. 编辑配置 vi config.ini # 2. 安装 sshpass（首次） sudo yum install -y sshpass # 3. 一键部署 bash deploy.sh # 4. 验证 bash cluster_ctl.sh status\r13.3 管理机远程部署\r#\r# 从 Windows/其他机器远程部署 pip install paramiko python deploy.py python cluster_ctl.py start python cluster_ctl.py status\r十四、故障排查\r#\rHDFS DataNode 未注册\r#\r# 检查 workers 文件: 主机名必须与节点 hostname 一致 cat $HADOOP_HOME/etc/hadoop/workers # 检查 /etc/hosts: 所有节点 hosts 文件需一致 cat /etc/hosts | grep hd # 检查 DataNode 日志 tail -100 $HADOOP_HOME/logs/hadoop-hadoop-datanode-*.log | grep -i error\rYARN NodeManager 未启动\r#\r# 内存不足 free -h # 调整 yarn-site.xml 中 yarn.nodemanager.resource.memory-mb # 建议为物理内存的 70-80%\rZooKeeper 选举失败\r#\r# myid 值必须和 zoo.cfg 中 server.X 一致 cat /opt/data/zookeeper/myid grep \u0026#34;server\\.\u0026#34; $ZOOKEEPER_HOME/conf/zoo.cfg # 检查防火墙和网络 telnet hd3 2888\rKafka 克隆节点启动失败\r#\r# 错误: broker.id 1 already registered # 原因: meta.properties 中记录的 broker.id 不同 rm -f /opt/data/kafkadata/meta.properties kafka-server-start.sh -daemon $KAFKA_HOME/config/server.properties\rHBase Master 启动后退出\r#\r# 1. 确认 ZooKeeper 正常运行 zkServer.sh status # 2. 确认 HDFS /hbase 目录存在且权限正确 hdfs dfs -ls / # 3. 检查日志 cat /opt/data/hbase/logs/hbase-hadoop-master-*.log | grep ERROR\rHDFS 安全模式\r#\rhdfs dfsadmin -safemode get hdfs dfsadmin -safemode leave\rHive 连接 MySQL 失败\r#\r# 检查 MySQL 用户授权 mysql -u hive -p\u0026#39;Hive@12345\u0026#39; -e \u0026#34;SHOW DATABASES;\u0026#34; # 检查 JDBC 驱动 ls /opt/module/apache-hive-3.1.3-bin/lib/mysql-connector-java-*.jar\r附录：适配新集群 Checklist\r#\r部署到新集群时，按以下步骤操作：\n[ ] 修改 /etc/hosts: 更新所有节点的 IP-主机名映射 [ ] 修改 config.ini: 更新 [NODES] 的 IP 和 [NODE_MEMORY] 的记忆体配置 [ ] 修改 [SSH]: 更新用户名和密码 [ ] 修改 [VERSIONS]: 确认各组件版本目录名与 /opt/module/ 下一致 [ ] 修改 [MYSQL]: 更新数据库连接信息 [ ] 确认域名后缀: deploy.sh 中硬编码的 .pblh123.cn 需要替换为实际域名（或修改为从 config.ini 读取） [ ] 确认副本数: [HDFS] replication 不能超过 DataNode 节点数 [ ] 确认内存配置: 每节点 NODE_MEMORY 设为物理内存的 70-80% [ ] 运行 bash deploy.sh: 一键部署 [ ] 运行 bash cluster_ctl.sh status: 验证集群状态 [ ] 逐个验证组件: HDFS 读写 → YARN Pi → ZK → Hive → HBase → Kafka → Spark 域名后缀适配\r#\r当前 deploy.sh 中硬编码了 .pblh123.cn 后缀。如你的环境使用不同域名（或无域名），有两种处理方式：\n方式一: 在 hosts 文件中配置完整 FQDN：\n# /etc/hosts 192.168.137.155 hd2.yourdomain.com hd2\r方式二: 在 /etc/hosts 中直接用短名，然后将脚本中的 .pblh123.cn 全局替换为空（即只用短主机名）。需修改文件: deploy.sh, deploy.py, cluster_ctl.sh, cluster_ctl.py。\n附录：脚本文件清单\r#\r文件 用途 运行位置 config.ini 全局配置文件 部署前编辑 deploy.sh Shell 一键部署脚本 集群主节点 deploy.py Python 部署脚本 管理机 cluster_ctl.sh Shell 集群启停 集群节点 cluster_ctl.py Python 集群启停 管理机 ssh_exec.py SSH 连接底层模块 被 Python 脚本引用 verify.py 集群验证脚本 管理机 cluster_check.py 快速状态检查 管理机 deep_check.py 深度诊断 管理机 shutdown_cluster.sh Shell 集群关机 集群节点 shutdown_cluster.py Python 集群关机 管理机 ","date":"May 29, 2026","externalUrl":null,"permalink":"/posts/%E5%A4%A7%E6%95%B0%E6%8D%AE%E9%9B%86%E7%BE%A4%E5%88%86%E5%B8%83%E5%BC%8F%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B/","section":"Posts","summary":"大数据集群分布式部署教程（完整验证版）\r#\r本教程提供每一步都可复制执行的大数据集群部署方案，包含全部配置文件的完整内容。所有步骤已在 3 节点 CentOS 7.9 集群上验证通过。\n变量适配说明\r#\r部署新集群时，将教程中的以下值替换为实际环境配置：\n变量 教程示例值 替换为你的值 域名后缀 pblh123.cn 你的域名，无域名的用 localdomain 节点数量 3 实际节点数（需同步调整副本数、ZK myid、Kafka broker.id） 主节点 hd2 / 192.168.137.155 你的主节点主机名/IP 节点2 hd3 / 192.168.137.156 你的第二个节点 节点3 hd4 / 192.168.137.157 你的第三个节点 Hadoop版本 hadoop-3.3.6 实际安装版本目录名 JDK版本 jdk1.8.0_471 实际安装版本目录名 HDFS副本数 2 通常设为 min(节点数, 3) MySQL密码 Hive@12345 你的MySQL hive用户密码 安装根目录 /opt/module 各组件解压目录 数据根目录 /opt/data HDFS/ZK/Kafka 数据存储目录 NodeManager内存 hd2:2800, hd3:2000, hd4:1200 按实际物理内存的70-80%分配(MB) SSH用户 hadoop 运行服务的系统用户 SSH密码 hadoop 相应密码 替换公式: ${组件路径} = /opt/module/${版本目录名}，教程中所有路径均由公式推导，修改版本号后路径自动适用。\n","title":"大数据集群分布式部署教程","type":"posts"},{"content":"\r电商用户画像系统\r#\r项目概述\r#\r电商用户画像系统是一个基于大数据技术的用户分析平台，旨在通过收集、分析用户的行为数据，构建精准的用户画像，为电商平台的个性化推荐、精准营销和用户体验优化提供数据支持。本系统整合了Spark、Hadoop、Hive、HBase、Kafka等大数据技术，实现了从数据采集到画像构建、应用的完整流程。\n技术栈版本\r#\r组件 版本 说明 Spark 3.5.8 分布式计算引擎 Scala 2.13.8 开发语言 JDK 11 Java运行环境 Kafka 3.7.0 消息队列 Redis 7.0+ 热数据缓存 MySQL 8.0 结构化数据存储 HBase 2.5.x 标签数据存储 Hive 3.1.x 数据仓库 Hadoop 3.3.x 分布式存储 知识点讲解\r#\r知识点1：用户画像概述\r#\r什么是用户画像\r#\r用户画像（User Profile）是将用户的多维度信息进行标签化、结构化描述的一种数据模型。它通过对用户的基础属性、行为特征、消费偏好等信息进行抽象和归纳，形成对用户的全面刻画。简单来说，用户画像就是用数据为用户\u0026quot;画一张像\u0026quot;。\n类比理解：想象你在描述一位朋友——\u0026ldquo;25岁、女性、喜欢运动、经常网购、偏好中高端品牌\u0026rdquo;。用户画像就是让计算机用同样的方式理解和描述每一个用户，只不过计算机可以同时处理上千万用户的画像。\n用户画像的核心价值\r#\r+--------------------------------------------------+ | 用户画像核心价值 | +--------------------------------------------------+ | | | +------------+ +------------+ +------------+ | | | 精准营销 | | 个性化推荐 | | 风险控制 | | | +------------+ +------------+ +------------+ | | | 定向投放 | | 商品推荐 | | 欺诈检测 | | | | 差异化定价 | | 内容推荐 | | 信用评估 | | | | 活动触达 | | 搜索排序 | | 流失预警 | | | +------------+ +------------+ +------------+ | | | | +------------+ +------------+ | | | 产品优化 | | 运营决策 | | | +------------+ +------------+ | | | 功能改进 | | 资源配置 | | | | 体验提升 | | 战略规划 | | | +------------+ +------------+ | +--------------------------------------------------+\r用户画像的三大应用场景\r#\r1. 精准营销\n精准营销是用户画像最直接的应用场景。通过用户画像，企业可以：\n定向广告投放：根据用户的兴趣标签，将广告精准推送给目标用户群体，降低营销成本，提高转化率 差异化营销策略：针对不同价值层级的用户制定不同的营销方案，如高价值用户推送高端品牌优惠，价格敏感用户推送折扣信息 营销活动触达：选择用户最活跃的时间段和渠道进行营销信息推送，提升触达效果 2. 个性化推荐\n个性化推荐是用户画像在产品层面的核心应用：\n商品推荐：基于用户的浏览、购买历史和兴趣标签，推荐可能感兴趣的商品 内容推荐：根据用户的内容偏好，推荐相关的文章、视频等内容 搜索排序优化：根据用户画像调整搜索结果的排序，优先展示用户偏好的品类和品牌 3. 风险控制\n风险控制是用户画像在安全层面的重要应用：\n欺诈检测：通过用户行为画像识别异常行为，如短时间内大量下单、收货地址频繁变更等 信用评估：根据用户的消费能力、还款记录等画像信息评估信用等级 流失预警：通过分析用户活跃度变化趋势，提前识别可能流失的用户并采取挽回措施 知识点2：标签体系设计\r#\r标签的分类\r#\r标签体系是用户画像的核心骨架。根据标签的生成方式，可以将标签分为三大类：\n+-----------------------------------------------------------------+ | 标签体系分类 | +-----------------------------------------------------------------+ | | | +------------------+ +------------------+ +----------------+ | | | 事实标签 | | 统计标签 | | 算法标签 | | | +------------------+ +------------------+ +----------------+ | | | 直接从原始数据 | | 对事实标签进行 | | 通过机器学习 | | | | 中提取的客观 | | 统计计算得到的 | | 算法挖掘得到 | | | | 事实描述 | | 聚合指标 | | 的预测标签 | | | +------------------+ +------------------+ +----------------+ | | | - 性别: 男/女 | | - 消费总金额 | | - 购买倾向 | | | | - 年龄: 25岁 | | - 购买频次 | | - 流失概率 | | | | - 注册日期 | | - 平均客单价 | | - 信用等级 | | | | - 所在城市 | | - 最近购买天数 | | - 用户分群 | | | +------------------+ +------------------+ +----------------+ | +-----------------------------------------------------------------+\r事实标签：最基础的标签类型，直接来源于原始数据，不需要复杂的计算。例如用户的性别、年龄、注册时间、所在城市等。事实标签是构建其他标签的基础。\n统计标签：在事实标签的基础上进行统计计算得到的标签。例如用户的消费总金额、购买频次、平均客单价、最近一次购买距今天数等。统计标签反映了用户的行为特征和消费能力。\n算法标签：通过机器学习算法对用户数据进行深度挖掘得到的预测性标签。例如用户的购买倾向评分、流失概率、信用等级、用户分群归属等。算法标签是最有价值的标签类型，但也是计算成本最高的。\n标签层级设计\r#\r在实际的企业级项目中，标签通常按照层级进行组织，形成树状结构：\n一级标签（大类） ├── 二级标签（中类） │ ├── 三级标签（小类/具体标签） │ ├── 三级标签 │ └── 三级标签 ├── 二级标签 │ ├── 三级标签 │ └── 三级标签 └── 二级标签\r电商用户画像标签体系示例：\n一级标签 二级标签 三级标签 标签类型 人口属性 基本属性 性别 事实标签 人口属性 基本属性 年龄段 统计标签 人口属性 基本属性 婚姻状况 事实标签 人口属性 地域属性 所在省份 事实标签 人口属性 地域属性 城市等级 统计标签 消费特征 消费能力 消费总金额 统计标签 消费特征 消费能力 平均客单价 统计标签 消费特征 消费能力 购买力等级 算法标签 消费特征 消费偏好 偏好品类 统计标签 消费特征 消费偏好 偏好品牌 统计标签 消费特征 消费偏好 价格敏感度 算法标签 行为特征 活跃度 最近访问天数 统计标签 行为特征 活跃度 登录频次 统计标签 行为特征 活跃度 活跃等级 算法标签 行为特征 购物行为 购买频次 统计标签 行为特征 购物行为 RFM分群 算法标签 风险属性 信用风险 信用等级 算法标签 风险属性 流失风险 流失概率 算法标签 知识点3：RFM模型\r#\rRFM模型定义\r#\rRFM模型是用户画像中最经典、最实用的用户价值分析模型之一。它通过三个维度来衡量用户的价值：\n+-------------------------------------------------------+ | RFM模型 | +-------------------------------------------------------+ | | | R - Recency（最近消费时间） | | ├── 用户最近一次消费距今天数 | | ├── R值越小，说明用户最近刚消费过，活跃度越高 | | └── 反映用户的当前活跃状态 | | | | F - Frequency（消费频率） | | ├── 用户在统计周期内的消费次数 | | ├── F值越大，说明用户消费越频繁，忠诚度越高 | | └── 反映用户的消费习惯和忠诚度 | | | | M - Monetary（消费金额） | | ├── 用户在统计周期内的消费总金额 | | ├── M值越大，说明用户消费能力越强，贡献度越高 | | └── 反映用户的消费能力和贡献价值 | +-------------------------------------------------------+\r类比理解：想象你经营一家餐厅——R是\u0026quot;这位客人多久没来了\u0026quot;，F是\u0026quot;这位客人一个月来几次\u0026quot;，M是\u0026quot;这位客人每次消费多少钱\u0026quot;。显然，最近常来、来得频繁、每次花得多的客人是最有价值的。\nRFM评分规则\r#\r在实际应用中，我们通常将R、F、M三个维度的原始值转换为评分（一般采用1-5分制），然后根据评分进行用户分群：\nR评分规则（最近消费时间越近，评分越高）：\nR值范围 评分 含义 0-30天 5 非常活跃 31-60天 4 较活跃 61-90天 3 一般活跃 91-180天 2 不太活跃 180天以上 1 很不活跃 F评分规则（消费频率越高，评分越高）：\nF值范围 评分 含义 20次以上 5 非常频繁 10-19次 4 较频繁 5-9次 3 一般频繁 2-4次 2 较少 1次 1 极少 M评分规则（消费金额越高，评分越高）：\nM值范围 评分 含义 10000元以上 5 高消费 5000-9999元 4 中高消费 2000-4999元 3 中等消费 500-1999元 2 中低消费 500元以下 1 低消费 用户分群策略\r#\r根据RFM评分的高低（以各维度平均分为阈值），可以将用户划分为8种类型：\nR评分 F评分 M评分 用户类型 特征描述 运营策略 高 高 高 重要价值用户 最近有消费、频率高、金额大 VIP服务、专属优惠、保持关系 高 高 低 重要保持用户 最近有消费、频率高、但金额小 提升客单价、关联推荐 高 低 高 重要发展用户 最近有消费、频率低、金额大 提升消费频次、会员激励 高 低 低 重要挽留用户 最近有消费、频率低、金额小 引导复购、优惠券刺激 低 高 高 重要唤回用户 较久未消费、频率高、金额大 唤醒召回、专属活动 低 高 低 一般维持用户 较久未消费、频率高、金额小 常规触达、保持关注 低 低 高 一般发展用户 较久未消费、频率低、金额大 重新激活、新品推荐 低 低 低 流失预警用户 较久未消费、频率低、金额小 低成本维护或放弃 知识点4：用户画像构建流程\r#\r用户画像的构建是一个系统化的工程，包含从数据采集到画像服务的完整流程：\n+--------------------------------------------------------------------+ | 用户画像构建流程 | +--------------------------------------------------------------------+ | | | [1.数据采集] --\u0026gt; [2.数据清洗] --\u0026gt; [3.标签计算] --\u0026gt; [4.画像存储] --\u0026gt; [5.画像服务] | | | +-----------+ +-----------+ +-----------+ +-----------+ +-----------+ | | 数据采集 | | 数据清洗 | | 标签计算 | | 画像存储 | | 画像服务 | | +-----------+ +-----------+ +-----------+ +-----------+ +-----------+ | | - 日志采集| | - 去重 | | - 事实标签| | - Redis | | - REST API| | | - 业务数据| | - 缺失值 | | - 统计标签| | - HBase | | - 查询服务| | | - 第三方 | | - 异常值 | | - 算法标签| | - MySQL | | - 推送服务| | | - 爬虫 | | - 格式化 | | - RFM模型 | | - Hive | | - 降级方案| | +-----------+ +-----------+ +-----------+ +-----------+ +-----------+ | | +--------------------------------------------------------------------+\r阶段1：数据采集\n数据采集是用户画像构建的第一步，数据的质量和丰富度直接决定了画像的准确性。主要数据来源包括：\n日志数据：用户浏览、点击、搜索等行为日志，通过Kafka、Flume等工具采集 业务数据：订单、支付、退款等交易数据，来自MySQL等业务数据库 第三方数据：社交数据、地理位置数据等外部数据源 埋点数据：前端页面埋点采集的用户交互数据 阶段2：数据清洗\n原始数据通常存在各种质量问题，需要经过清洗才能用于标签计算：\n去重处理：去除重复采集的数据记录 缺失值处理：对缺失字段进行填充或丢弃 异常值处理：识别并处理明显不合理的数据（如负数的订单金额） 格式统一：统一日期格式、编码格式等 阶段3：标签计算\n标签计算是用户画像构建的核心环节，根据标签类型采用不同的计算方式：\n事实标签：直接从清洗后的数据中提取，如性别、年龄 统计标签：通过SQL聚合计算，如消费总金额、购买频次 算法标签：通过机器学习模型计算，如用户分群、流失概率 RFM模型：计算R、F、M三个维度的评分，进行用户价值分群 阶段4：画像存储\n根据数据的访问特征和使用场景，采用不同的存储方案（详见知识点5）。\n阶段5：画像服务\n将构建好的用户画像通过服务接口对外提供，支持实时查询和批量推送：\nREST API：提供用户画像查询接口，支持按用户ID查询 批量推送：定期将用户画像数据推送到营销系统、推荐系统等下游 降级方案：当主存储不可用时，自动切换到备用数据源 知识点5：画像数据存储架构\r#\r用户画像数据具有多维度、多粒度、多访问模式的特点，单一存储方案无法满足所有需求。企业级系统通常采用分层存储架构：\n+--------------------------------------------------------------------+ | 画像数据存储架构 | +--------------------------------------------------------------------+ | | | +------------------+ | | | Redis 7.0+ | 热数据层：实时查询、高并发访问 | | | (热数据) | - 用户实时标签缓存 | | | | - 用户RFM评分 | | | | - 最近行为特征 | | | TTL: 24h | - 读写延迟 \u0026lt; 1ms | | +------------------+ | | | | | v | | +------------------+ | | | HBase 2.5.x | 标签数据层：海量标签存储、快速检索 | | | (标签数据) | - 全量用户标签数据 | | | | - 标签历史版本 | | | | - 支持按RowKey快速查询 | | +------------------+ | | | | | v | | +------------------+ | | | MySQL 8.0 | 结构化数据层：关系查询、事务支持 | | | (结构化数据) | - 用户基础信息 | | | | - 标签元数据定义 | | | | - 画像统计报表 | | +------------------+ | | | | | v | | +------------------+ | | | Hive 3.1.x | 历史数据层：离线分析、数据回溯 | | | (历史数据) | - 用户画像历史快照 | | | | - 标签计算中间结果 | | | | - 离线分析报表 | | +------------------+ | | | +--------------------------------------------------------------------+\r各存储层详细说明：\n存储层 技术选型 数据内容 访问模式 典型延迟 热数据层 Redis 7.0+ 实时标签、RFM评分、最近行为 实时读写、高并发 \u0026lt; 1ms 标签数据层 HBase 2.5.x 全量用户标签、标签历史 按RowKey查询、范围扫描 10-50ms 结构化数据层 MySQL 8.0 用户基础信息、标签元数据 关系查询、事务操作 5-20ms 历史数据层 Hive 3.1.x 历史快照、中间结果 批量离线分析 分钟级 数据流转策略：\n写入：标签计算结果先写入MySQL（结构化数据）和HBase（标签数据），再异步同步到Redis（热数据缓存） 查询：优先从Redis读取热数据；Redis未命中时回源HBase；历史数据分析使用Hive 过期：Redis中的热数据设置TTL（通常24小时），过期后由定时任务重新加载 降级：当Redis不可用时，直接查询HBase，牺牲部分性能保证可用性 系统架构\r#\r系统架构图\r#\r+----------------+ +------------+ +------------+ +------------+ | 数据采集层 | --\u0026gt; | 数据存储层 | --\u0026gt; | 数据处理层 | --\u0026gt; | 应用层 | +----------------+ +------------+ +------------+ +------------+ | - Kafka 3.7.0 | | - HDFS | | - Spark | | - 推荐系统 | | - Flume | | - HBase | | 3.5.8 | | - 精准营销 | | - Logstash | | - MySQL | | - Hive | | - 个性化 | | | | 8.0 | | - MLlib | | - 风控系统 | | | | - Redis | +------------+ +------------+ | | | 7.0+ | +----------------+ +------------+\r项目工程结构\r#\recommerce-user-profile/ ├── pom.xml # Maven项目配置 ├── src/ │ ├── main/ │ │ ├── resources/ │ │ │ ├── application.conf # 配置文件（外部化配置） │ │ │ └── log4j2.xml # 日志配置 │ │ └── scala/ │ │ └── com/ │ │ └── ecommerce/ │ │ └── profile/ │ │ ├── config/ │ │ │ └── ConfigManager.scala # 配置管理 │ │ ├── dal/ │ │ │ └── DataAccessLayer.scala # 数据访问层 │ │ ├── service/ │ │ │ └── UserProfileService.scala # 画像服务 │ │ ├── producer/ │ │ │ └── UserBehaviorProducer.scala # 数据采集 │ │ ├── storage/ │ │ │ └── UserBehaviorToHBase.scala # HBase存储 │ │ ├── builder/ │ │ │ ├── UserProfileBuilder.scala # 基础画像构建 │ │ │ ├── RFMAnalyzer.scala # RFM分析 │ │ │ └── UserSegmentation.scala # 用户分群 │ │ ├── recommendation/ │ │ │ └── PersonalizedRecommendation.scala # 个性化推荐 │ │ └── marketing/ │ │ └── TargetedMarketing.scala # 精准营销 ├── config/ │ └── application.conf # 外部配置文件（生产环境覆盖） └── scripts/ └── init_tables.sql # 数据库初始化脚本\r数据模型设计\r#\r1. 用户基础信息模型\r#\r字段名 数据类型 描述 user_id INT 用户ID（主键） username VARCHAR(50) 用户名 gender VARCHAR(10) 性别 age INT 年龄 email VARCHAR(100) 邮箱 phone VARCHAR(20) 手机号 registration_date DATE 注册日期 last_login_date DATE 最后登录日期 user_level INT 用户等级 2. 用户行为数据模型\r#\r字段名 数据类型 描述 behavior_id BIGINT 行为ID user_id INT 用户ID product_id INT 商品ID behavior_type VARCHAR(20) 行为类型（view/click/add_to_cart/favorite/purchase） behavior_time TIMESTAMP 行为时间 session_id VARCHAR(100) 会话ID ip_address VARCHAR(50) IP地址 device_type VARCHAR(50) 设备类型 3. 商品数据模型\r#\r字段名 数据类型 描述 product_id INT 商品ID（主键） product_name VARCHAR(100) 商品名称 category_id INT 分类ID category_name VARCHAR(50) 分类名称 price DECIMAL(10,2) 价格 brand VARCHAR(50) 品牌 stock INT 库存 sales INT 销量 rating DECIMAL(3,2) 评分 4. 用户画像模型\r#\r字段名 数据类型 描述 user_id INT 用户ID（主键） age_group VARCHAR(20) 年龄组 gender VARCHAR(10) 性别 interest_categories JSON 兴趣分类 purchase_power VARCHAR(20) 购买力 shopping_frequency VARCHAR(20) 购物频率 favorite_brands JSON 偏好品牌 average_order_value DECIMAL(10,2) 平均订单价值 last_purchase_date DATE 最后购买日期 user_tags JSON 用户标签 rfm_score VARCHAR(10) RFM评分 user_segment VARCHAR(30) 用户分群 项目配置\r#\rpom.xml\r#\r\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;com.ecommerce\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;user-profile\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.0\u0026lt;/version\u0026gt; \u0026lt;packaging\u0026gt;jar\u0026lt;/packaging\u0026gt; \u0026lt;name\u0026gt;E-Commerce User Profile System\u0026lt;/name\u0026gt; \u0026lt;description\u0026gt;基于Spark的电商用户画像系统\u0026lt;/description\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;scala.version\u0026gt;2.13.8\u0026lt;/scala.version\u0026gt; \u0026lt;scala.binary.version\u0026gt;2.13\u0026lt;/scala.binary.version\u0026gt; \u0026lt;spark.version\u0026gt;3.5.8\u0026lt;/spark.version\u0026gt; \u0026lt;hadoop.version\u0026gt;3.3.6\u0026lt;/hadoop.version\u0026gt; \u0026lt;hbase.version\u0026gt;2.5.5\u0026lt;/hbase.version\u0026gt; \u0026lt;kafka.version\u0026gt;3.7.0\u0026lt;/kafka.version\u0026gt; \u0026lt;jedis.version\u0026gt;5.0.2\u0026lt;/jedis.version\u0026gt; \u0026lt;mysql.connector.version\u0026gt;8.0.33\u0026lt;/mysql.connector.version\u0026gt; \u0026lt;typesafe.config.version\u0026gt;1.4.3\u0026lt;/typesafe.config.version\u0026gt; \u0026lt;slf4j.version\u0026gt;2.0.9\u0026lt;/slf4j.version\u0026gt; \u0026lt;log4j.version\u0026gt;2.21.1\u0026lt;/log4j.version\u0026gt; \u0026lt;maven.compiler.source\u0026gt;11\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;11\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- Scala --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.scala-lang\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;scala-library\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${scala.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spark Core --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.spark\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spark-core_${scala.binary.version}\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spark.version}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spark SQL --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.spark\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spark-sql_${scala.binary.version}\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spark.version}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spark Streaming --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.spark\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spark-streaming_${scala.binary.version}\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spark.version}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spark Streaming Kafka --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.spark\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spark-streaming-kafka-0-10_${scala.binary.version}\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spark.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spark SQL Kafka --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.spark\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spark-sql-kafka-0-10_${scala.binary.version}\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spark.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spark MLlib --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.spark\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spark-mllib_${scala.binary.version}\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spark.version}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spark Hive --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.spark\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spark-hive_${scala.binary.version}\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spark.version}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Hadoop Common --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.hadoop\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hadoop-common\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${hadoop.version}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Hadoop Client --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.hadoop\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hadoop-client\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${hadoop.version}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- HBase Client --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.hbase\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hbase-client\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${hbase.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- HBase Common --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.hbase\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hbase-common\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${hbase.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Kafka Clients --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.kafka\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;kafka-clients\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${kafka.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Redis Jedis --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;redis.clients\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jedis\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${jedis.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- MySQL Connector --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-j\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${mysql.connector.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Typesafe Config --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.typesafe\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;config\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${typesafe.config.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- SLF4J API --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.slf4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;slf4j-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${slf4j.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Log4j2 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.logging.log4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;log4j-core\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${log4j.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.logging.log4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;log4j-slf4j2-impl\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${log4j.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- ScalaTest --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.scalatest\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;scalatest_${scala.binary.version}\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.2.17\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;!-- Scala Maven Plugin --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;net.alchim31.maven\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;scala-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.8.1\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;compile\u0026lt;/goal\u0026gt; \u0026lt;goal\u0026gt;testCompile\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;scalaVersion\u0026gt;${scala.version}\u0026lt;/scalaVersion\u0026gt; \u0026lt;args\u0026gt; \u0026lt;arg\u0026gt;-target:jvm-11\u0026lt;/arg\u0026gt; \u0026lt;arg\u0026gt;-deprecation\u0026lt;/arg\u0026gt; \u0026lt;arg\u0026gt;-feature\u0026lt;/arg\u0026gt; \u0026lt;arg\u0026gt;-unchecked\u0026lt;/arg\u0026gt; \u0026lt;/args\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;!-- Maven Shade Plugin（打包Fat Jar） --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-shade-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.1\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;phase\u0026gt;package\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;shade\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;filters\u0026gt; \u0026lt;filter\u0026gt; \u0026lt;artifact\u0026gt;*:*\u0026lt;/artifact\u0026gt; \u0026lt;excludes\u0026gt; \u0026lt;exclude\u0026gt;META-INF/*.SF\u0026lt;/exclude\u0026gt; \u0026lt;exclude\u0026gt;META-INF/*.DSA\u0026lt;/exclude\u0026gt; \u0026lt;exclude\u0026gt;META-INF/*.RSA\u0026lt;/exclude\u0026gt; \u0026lt;/excludes\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;/filters\u0026gt; \u0026lt;transformers\u0026gt; \u0026lt;transformer implementation=\u0026#34;org.apache.maven.plugins.shade.resource.ServicesResourceTransformer\u0026#34;/\u0026gt; \u0026lt;/transformers\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;!-- Maven Compiler Plugin --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-compiler-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.11.0\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;source\u0026gt;11\u0026lt;/source\u0026gt; \u0026lt;target\u0026gt;11\u0026lt;/target\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/project\u0026gt;\rapplication.conf 配置文件\r#\r# ============================================ # 电商用户画像系统 - 配置文件 # 注意：生产环境中密码等敏感信息应通过环境变量或密钥管理系统注入 # ============================================ # MySQL 配置 mysql { url = \u0026#34;jdbc:mysql://localhost:3306/ecommerce?useSSL=true\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;characterEncoding=utf8mb4\u0026#34; user = \u0026#34;ecommerce_user\u0026#34; # 生产环境请通过环境变量覆盖: MYSQL_PASSWORD password = ${?MYSQL_PASSWORD} driver = \u0026#34;com.mysql.cj.jdbc.Driver\u0026#34; pool-size = 10 connection-timeout = 30000 } # Redis 配置 redis { host = \u0026#34;localhost\u0026#34; port = 6379 # 生产环境请通过环境变量覆盖: REDIS_PASSWORD password = ${?REDIS_PASSWORD} database = 0 timeout = 3000 pool { max-total = 50 max-idle = 20 min-idle = 5 max-wait-millis = 3000 test-on-borrow = true test-on-return = false test-while-idle = true } cache { # 热数据缓存过期时间（秒） ttl-seconds = 86400 # 缓存键前缀 key-prefix = \u0026#34;user:profile:\u0026#34; } } # Kafka 配置 kafka { bootstrap-servers = \u0026#34;localhost:9092\u0026#34; consumer { group-id = \u0026#34;user-profile-group\u0026#34; auto-offset-reset = \u0026#34;latest\u0026#34; enable-auto-commit = false max-poll-records = 500 session-timeout-ms = 30000 } producer { acks = \u0026#34;all\u0026#34; retries = 3 batch-size = 16384 linger-ms = 5 buffer-memory = 33554432 } topics { user-behavior = \u0026#34;user-behavior\u0026#34; access-logs = \u0026#34;access-logs\u0026#34; profile-update = \u0026#34;profile-update\u0026#34; } } # Spark 配置 spark { app-name = \u0026#34;EcommerceUserProfile\u0026#34; master = \u0026#34;local[*]\u0026#34; config { \u0026#34;spark.executor.memory\u0026#34; = \u0026#34;4g\u0026#34; \u0026#34;spark.executor.cores\u0026#34; = \u0026#34;2\u0026#34; \u0026#34;spark.driver.memory\u0026#34; = \u0026#34;2g\u0026#34; \u0026#34;spark.sql.shuffle.partitions\u0026#34; = \u0026#34;200\u0026#34; \u0026#34;spark.default.parallelism\u0026#34; = \u0026#34;100\u0026#34; \u0026#34;spark.serializer\u0026#34; = \u0026#34;org.apache.spark.serializer.KryoSerializer\u0026#34; \u0026#34;spark.sql.adaptive.enabled\u0026#34; = \u0026#34;true\u0026#34; \u0026#34;spark.sql.adaptive.coalescePartitions.enabled\u0026#34; = \u0026#34;true\u0026#34; \u0026#34;spark.streaming.backpressure.enabled\u0026#34; = \u0026#34;true\u0026#34; } } # HBase 配置 hbase { zookeeper-quorum = \u0026#34;localhost\u0026#34; zookeeper-property-clientPort = \u0026#34;2181\u0026#34; tables { user-behavior = \u0026#34;user_behavior\u0026#34; user-tags = \u0026#34;user_tags\u0026#34; } } # RFM 模型配置 rfm { # 统计周期（天） period-days = 365 # R 评分阈值（天） r-thresholds = [30, 60, 90, 180] # F 评分阈值（次） f-thresholds = [1, 5, 10, 20] # M 评分阈值（元） m-thresholds = [500, 2000, 5000, 10000] } # 画像服务配置 profile-service { port = 8080 # 降级开关：当Redis不可用时是否降级到HBase查询 degradation-enabled = true # 批量查询最大数量 batch-max-size = 100 # 查询超时时间（毫秒） query-timeout-ms = 5000 }\r核心代码实现\r#\r1. 配置管理 - ConfigManager.scala\r#\rpackage com.ecommerce.profile.config import com.typesafe.config.{Config, ConfigFactory} import org.slf4j.{Logger, LoggerFactory} import scala.jdk.CollectionConverters._ import scala.util.Try /** * 配置管理器 * * 使用 Typesafe Config 库管理应用配置，支持： * - 多环境配置（开发/测试/生产） * - 环境变量覆盖（敏感信息不硬编码） * - 配置热加载 * - 配置验证 * * 设计原则： * - 所有敏感信息（密码等）通过环境变量注入，不在代码或配置文件中硬编码 * - 提供类型安全的配置读取方法 * - 提供默认值机制，避免因缺少配置导致启动失败 */ object ConfigManager { private val logger: Logger = LoggerFactory.getLogger(ConfigManager.getClass) // 加载配置，优先使用环境变量覆盖 private val config: Config = { val baseConfig = ConfigFactory.load() // 允许通过环境变量覆盖配置项 // 例如：MYSQL_PASSWORD 环境变量会覆盖 mysql.password 配置 val envOverrides = ConfigFactory.systemEnvironment() envOverrides.withFallback(baseConfig).resolve() } /** * 获取原始 Config 对象 */ def getConfig: Config = config // ==================== MySQL 配置 ==================== def getMySQLUrl: String = config.getString(\u0026#34;mysql.url\u0026#34;) def getMySQLUser: String = config.getString(\u0026#34;mysql.user\u0026#34;) def getMySQLPassword: String = { // 优先从环境变量获取密码 sys.env.getOrElse(\u0026#34;MYSQL_PASSWORD\u0026#34;, { if (config.hasPath(\u0026#34;mysql.password\u0026#34;)) { val pwd = config.getString(\u0026#34;mysql.password\u0026#34;) if (pwd == null || pwd.isEmpty) { logger.warn(\u0026#34;MySQL密码未配置，请设置环境变量 MYSQL_PASSWORD 或配置 mysql.password\u0026#34;) } pwd } else { logger.error(\u0026#34;MySQL密码未配置！请设置环境变量 MYSQL_PASSWORD\u0026#34;) \u0026#34;\u0026#34; } }) } def getMySQLDriver: String = config.getString(\u0026#34;mysql.driver\u0026#34;) def getMySQLPoolSize: Int = Try(config.getInt(\u0026#34;mysql.pool-size\u0026#34;)).getOrElse(10) // ==================== Redis 配置 ==================== def getRedisHost: String = config.getString(\u0026#34;redis.host\u0026#34;) def getRedisPort: Int = config.getInt(\u0026#34;redis.port\u0026#34;) def getRedisPassword: String = { sys.env.getOrElse(\u0026#34;REDIS_PASSWORD\u0026#34;, { if (config.hasPath(\u0026#34;redis.password\u0026#34;) \u0026amp;\u0026amp; config.getString(\u0026#34;redis.password\u0026#34;) != null) { config.getString(\u0026#34;redis.password\u0026#34;) } else { \u0026#34;\u0026#34; // Redis 可以设置无密码 } }) } def getRedisDatabase: Int = Try(config.getInt(\u0026#34;redis.database\u0026#34;)).getOrElse(0) def getRedisTimeout: Int = Try(config.getInt(\u0026#34;redis.timeout\u0026#34;)).getOrElse(3000) def getRedisMaxTotal: Int = Try(config.getInt(\u0026#34;redis.pool.max-total\u0026#34;)).getOrElse(50) def getRedisMaxIdle: Int = Try(config.getInt(\u0026#34;redis.pool.max-idle\u0026#34;)).getOrElse(20) def getRedisMinIdle: Int = Try(config.getInt(\u0026#34;redis.pool.min-idle\u0026#34;)).getOrElse(5) def getRedisMaxWaitMillis: Long = Try(config.getLong(\u0026#34;redis.pool.max-wait-millis\u0026#34;)).getOrElse(3000L) def getRedisCacheTTL: Int = Try(config.getInt(\u0026#34;redis.cache.ttl-seconds\u0026#34;)).getOrElse(86400) def getRedisKeyPrefix: String = Try(config.getString(\u0026#34;redis.cache.key-prefix\u0026#34;)).getOrElse(\u0026#34;user:profile:\u0026#34;) // ==================== Kafka 配置 ==================== def getKafkaBootstrapServers: String = config.getString(\u0026#34;kafka.bootstrap-servers\u0026#34;) def getKafkaGroupId: String = Try(config.getString(\u0026#34;kafka.consumer.group-id\u0026#34;)).getOrElse(\u0026#34;user-profile-group\u0026#34;) def getKafkaAutoOffsetReset: String = Try(config.getString(\u0026#34;kafka.consumer.auto-offset-reset\u0026#34;)).getOrElse(\u0026#34;latest\u0026#34;) def getKafkaMaxPollRecords: Int = Try(config.getInt(\u0026#34;kafka.consumer.max-poll-records\u0026#34;)).getOrElse(500) def getKafkaUserBehaviorTopic: String = Try(config.getString(\u0026#34;kafka.topics.user-behavior\u0026#34;)).getOrElse(\u0026#34;user-behavior\u0026#34;) def getKafkaAccessLogsTopic: String = Try(config.getString(\u0026#34;kafka.topics.access-logs\u0026#34;)).getOrElse(\u0026#34;access-logs\u0026#34;) def getKafkaProfileUpdateTopic: String = Try(config.getString(\u0026#34;kafka.topics.profile-update\u0026#34;)).getOrElse(\u0026#34;profile-update\u0026#34;) // ==================== Spark 配置 ==================== def getSparkAppName: String = Try(config.getString(\u0026#34;spark.app-name\u0026#34;)).getOrElse(\u0026#34;EcommerceUserProfile\u0026#34;) def getSparkMaster: String = Try(config.getString(\u0026#34;spark.master\u0026#34;)).getOrElse(\u0026#34;local[*]\u0026#34;) def getSparkConfig: Map[String, String] = { Try { config.getConfig(\u0026#34;spark.config\u0026#34;).entrySet().asScala.map { entry =\u0026gt; entry.getKey -\u0026gt; entry.getValue.unwrapped().toString }.toMap }.getOrElse(Map.empty) } // ==================== HBase 配置 ==================== def getHBaseZkQuorum: String = Try(config.getString(\u0026#34;hbase.zookeeper-quorum\u0026#34;)).getOrElse(\u0026#34;localhost\u0026#34;) def getHBaseZkClientPort: String = Try(config.getString(\u0026#34;hbase.zookeeper-property-clientPort\u0026#34;)).getOrElse(\u0026#34;2181\u0026#34;) def getHBaseUserBehaviorTable: String = Try(config.getString(\u0026#34;hbase.tables.user-behavior\u0026#34;)).getOrElse(\u0026#34;user_behavior\u0026#34;) def getHBaseUserTagsTable: String = Try(config.getString(\u0026#34;hbase.tables.user-tags\u0026#34;)).getOrElse(\u0026#34;user_tags\u0026#34;) // ==================== RFM 配置 ==================== def getRFMPeriodDays: Int = Try(config.getInt(\u0026#34;rfm.period-days\u0026#34;)).getOrElse(365) def getRFMRThresholds: List[Int] = Try { config.getIntList(\u0026#34;rfm.r-thresholds\u0026#34;).asScala.map(_.intValue()).toList }.getOrElse(List(30, 60, 90, 180)) def getRFMFThresholds: List[Int] = Try { config.getIntList(\u0026#34;rfm.f-thresholds\u0026#34;).asScala.map(_.intValue()).toList }.getOrElse(List(1, 5, 10, 20)) def getRFMMThresholds: List[Int] = Try { config.getIntList(\u0026#34;rfm.m-thresholds\u0026#34;).asScala.map(_.intValue()).toList }.getOrElse(List(500, 2000, 5000, 10000)) // ==================== 画像服务配置 ==================== def getProfileServicePort: Int = Try(config.getInt(\u0026#34;profile-service.port\u0026#34;)).getOrElse(8080) def isDegradationEnabled: Boolean = Try(config.getBoolean(\u0026#34;profile-service.degradation-enabled\u0026#34;)).getOrElse(true) def getBatchMaxSize: Int = Try(config.getInt(\u0026#34;profile-service.batch-max-size\u0026#34;)).getOrElse(100) def getQueryTimeoutMs: Long = Try(config.getLong(\u0026#34;profile-service.query-timeout-ms\u0026#34;)).getOrElse(5000L) /** * 验证关键配置是否完整 * * @return 验证结果，None表示通过，Some包含错误信息 */ def validate(): Option[String] = { val errors = scala.collection.mutable.ArrayBuffer[String]() if (getMySQLUrl.isEmpty) errors += \u0026#34;MySQL URL 未配置\u0026#34; if (getMySQLUser.isEmpty) errors += \u0026#34;MySQL 用户名未配置\u0026#34; if (getMySQLPassword.isEmpty) errors += \u0026#34;MySQL 密码未配置（请设置环境变量 MYSQL_PASSWORD）\u0026#34; if (getRedisHost.isEmpty) errors += \u0026#34;Redis 主机未配置\u0026#34; if (getKafkaBootstrapServers.isEmpty) errors += \u0026#34;Kafka bootstrap servers 未配置\u0026#34; if (errors.nonEmpty) { val msg = s\u0026#34;配置验证失败：\\n${errors.mkString(\u0026#34;\\n\u0026#34;)}\u0026#34; logger.error(msg) Some(msg) } else { logger.info(\u0026#34;配置验证通过\u0026#34;) None } } /** * 打印当前配置（隐藏敏感信息） */ def printConfig(): Unit = { logger.info(\u0026#34;========== 当前配置 ==========\u0026#34;) logger.info(s\u0026#34;MySQL URL: $getMySQLUrl\u0026#34;) logger.info(s\u0026#34;MySQL User: $getMySQLUser\u0026#34;) logger.info(s\u0026#34;MySQL Password: ${if (getMySQLPassword.nonEmpty) \u0026#34;******\u0026#34; else \u0026#34;未配置\u0026#34;}\u0026#34;) logger.info(s\u0026#34;Redis Host: $getRedisHost:$getRedisPort\u0026#34;) logger.info(s\u0026#34;Redis Password: ${if (getRedisPassword.nonEmpty) \u0026#34;******\u0026#34; else \u0026#34;未配置\u0026#34;}\u0026#34;) logger.info(s\u0026#34;Kafka Servers: $getKafkaBootstrapServers\u0026#34;) logger.info(s\u0026#34;Spark Master: $getSparkMaster\u0026#34;) logger.info(s\u0026#34;HBase ZK: $getHBaseZkQuorum:$getHBaseZkClientPort\u0026#34;) logger.info(s\u0026#34;降级开关: $isDegradationEnabled\u0026#34;) logger.info(\u0026#34;==============================\u0026#34;) } }\r2. 数据访问层 - DataAccessLayer.scala\r#\rpackage com.ecommerce.profile.dal import com.ecommerce.profile.config.ConfigManager import org.apache.spark.sql.{DataFrame, SparkSession} import org.slf4j.{Logger, LoggerFactory} import java.sql.{Connection, DriverManager, PreparedStatement, ResultSet} import java.util.Properties import scala.jdk.CollectionConverters._ import scala.util.{Try, Using} /** * 数据访问层 * * 封装所有数据源的读写操作，提供统一的数据访问接口。 * 职责： * - MySQL 数据读写 * - HBase 数据读写 * - Redis 缓存操作 * - Kafka 数据源接入 * * 设计原则： * - 所有连接配置通过 ConfigManager 获取，不硬编码 * - 提供连接池管理，避免频繁创建/销毁连接 * - 统一异常处理，确保资源释放 * - 读写操作支持重试机制 */ object DataAccessLayer { private val logger: Logger = LoggerFactory.getLogger(DataAccessLayer.getClass) // ==================== MySQL 操作 ==================== /** * 获取 MySQL JDBC 连接属性 */ private def getMySQLProperties: Properties = { val props = new Properties() props.setProperty(\u0026#34;user\u0026#34;, ConfigManager.getMySQLUser) props.setProperty(\u0026#34;password\u0026#34;, ConfigManager.getMySQLPassword) props.setProperty(\u0026#34;driver\u0026#34;, ConfigManager.getMySQLDriver) props.setProperty(\u0026#34;useSSL\u0026#34;, \u0026#34;true\u0026#34;) props.setProperty(\u0026#34;characterEncoding\u0026#34;, \u0026#34;utf8mb4\u0026#34;) props } /** * 从 MySQL 读取数据 * * @param spark SparkSession 实例 * @param table 表名 * @param columns 可选的列名列表，为空则读取全部列 * @return DataFrame */ def readFromMySQL(spark: SparkSession, table: String, columns: Seq[String] = Seq.empty): DataFrame = { require(table.nonEmpty, \u0026#34;表名不能为空\u0026#34;) val query = if (columns.nonEmpty) { s\u0026#34;SELECT ${columns.mkString(\u0026#34;, \u0026#34;)} FROM $table\u0026#34; } else { s\u0026#34;SELECT * FROM $table\u0026#34; } logger.info(s\u0026#34;从MySQL读取数据: $query\u0026#34;) try { spark.read .format(\u0026#34;jdbc\u0026#34;) .option(\u0026#34;url\u0026#34;, ConfigManager.getMySQLUrl) .option(\u0026#34;dbtable\u0026#34;, s\u0026#34;($query) AS t\u0026#34;) .option(\u0026#34;user\u0026#34;, ConfigManager.getMySQLUser) .option(\u0026#34;password\u0026#34;, ConfigManager.getMySQLPassword) .option(\u0026#34;driver\u0026#34;, ConfigManager.getMySQLDriver) .option(\u0026#34;fetchsize\u0026#34;, \u0026#34;1000\u0026#34;) .option(\u0026#34;numPartitions\u0026#34;, \u0026#34;4\u0026#34;) .option(\u0026#34;partitionColumn\u0026#34;, \u0026#34;user_id\u0026#34;) .option(\u0026#34;lowerBound\u0026#34;, \u0026#34;1\u0026#34;) .option(\u0026#34;upperBound\u0026#34;, \u0026#34;10000000\u0026#34;) .load() } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;从MySQL读取数据失败: ${e.getMessage}\u0026#34;, e) throw new RuntimeException(s\u0026#34;MySQL读取失败: ${e.getMessage}\u0026#34;, e) } } /** * 写入数据到 MySQL * * @param df 待写入的 DataFrame * @param table 目标表名 * @param saveMode 写入模式（overwrite/append/upsert） */ def writeToMySQL(df: DataFrame, table: String, saveMode: String = \u0026#34;overwrite\u0026#34;): Unit = { require(table.nonEmpty, \u0026#34;表名不能为空\u0026#34;) logger.info(s\u0026#34;写入数据到MySQL表: $table, 模式: $saveMode, 记录数: ${df.count()}\u0026#34;) try { df.write .format(\u0026#34;jdbc\u0026#34;) .option(\u0026#34;url\u0026#34;, ConfigManager.getMySQLUrl) .option(\u0026#34;dbtable\u0026#34;, table) .option(\u0026#34;user\u0026#34;, ConfigManager.getMySQLUser) .option(\u0026#34;password\u0026#34;, ConfigManager.getMySQLPassword) .option(\u0026#34;driver\u0026#34;, ConfigManager.getMySQLDriver) .option(\u0026#34;batchsize\u0026#34;, \u0026#34;1000\u0026#34;) .option(\u0026#34;isolationLevel\u0026#34;, \u0026#34;READ_COMMITTED\u0026#34;) .mode(saveMode) .save() logger.info(s\u0026#34;数据写入MySQL成功: $table\u0026#34;) } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;写入MySQL失败: ${e.getMessage}\u0026#34;, e) throw new RuntimeException(s\u0026#34;MySQL写入失败: ${e.getMessage}\u0026#34;, e) } } /** * 使用 JDBC 直连执行 SQL 查询（适用于小数据量查询） * * @param sql SQL 语句 * @param handler 结果集处理函数 * @tparam T 返回类型 * @return 查询结果 */ def executeQuery[T](sql: String)(handler: ResultSet =\u0026gt; T): Option[T] = { var connection: Connection = null var stmt: PreparedStatement = null try { connection = DriverManager.getConnection( ConfigManager.getMySQLUrl, ConfigManager.getMySQLUser, ConfigManager.getMySQLPassword ) stmt = connection.prepareStatement(sql) val rs = stmt.executeQuery() Some(handler(rs)) } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;执行SQL查询失败: ${e.getMessage}\u0026#34;, e) None } finally { if (stmt != null) Try(stmt.close()) if (connection != null) Try(connection.close()) } } /** * 使用 JDBC 直连执行 SQL 更新 * * @param sql SQL 语句 * @return 受影响的行数 */ def executeUpdate(sql: String): Int = { var connection: Connection = null var stmt: PreparedStatement = null try { connection = DriverManager.getConnection( ConfigManager.getMySQLUrl, ConfigManager.getMySQLUser, ConfigManager.getMySQLPassword ) stmt = connection.prepareStatement(sql) stmt.executeUpdate() } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;执行SQL更新失败: ${e.getMessage}\u0026#34;, e) -1 } finally { if (stmt != null) Try(stmt.close()) if (connection != null) Try(connection.close()) } } // ==================== Kafka 操作 ==================== /** * 从 Kafka 读取流数据 * * @param spark SparkSession 实例 * @param topic Kafka 主题 * @return DataFrame */ def readFromKafka(spark: SparkSession, topic: String): DataFrame = { require(topic.nonEmpty, \u0026#34;Kafka主题不能为空\u0026#34;) logger.info(s\u0026#34;从Kafka读取流数据: $topic\u0026#34;) try { spark.readStream .format(\u0026#34;kafka\u0026#34;) .option(\u0026#34;kafka.bootstrap.servers\u0026#34;, ConfigManager.getKafkaBootstrapServers) .option(\u0026#34;subscribe\u0026#34;, topic) .option(\u0026#34;startingOffsets\u0026#34;, ConfigManager.getKafkaAutoOffsetReset) .option(\u0026#34;maxOffsetsPerTrigger\u0026#34;, ConfigManager.getKafkaMaxPollRecords.toString) .option(\u0026#34;failOnDataLoss\u0026#34;, \u0026#34;false\u0026#34;) .load() } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;从Kafka读取数据失败: ${e.getMessage}\u0026#34;, e) throw new RuntimeException(s\u0026#34;Kafka读取失败: ${e.getMessage}\u0026#34;, e) } } /** * 从 Kafka 批量读取数据 * * @param spark SparkSession 实例 * @param topic Kafka 主题 * @param startingOffsets 起始偏移量（默认 earliest） * @param endingOffsets 结束偏移量（默认 latest） * @return DataFrame */ def readBatchFromKafka(spark: SparkSession, topic: String, startingOffsets: String = \u0026#34;earliest\u0026#34;, endingOffsets: String = \u0026#34;latest\u0026#34;): DataFrame = { require(topic.nonEmpty, \u0026#34;Kafka主题不能为空\u0026#34;) logger.info(s\u0026#34;从Kafka批量读取数据: $topic\u0026#34;) try { spark.read .format(\u0026#34;kafka\u0026#34;) .option(\u0026#34;kafka.bootstrap.servers\u0026#34;, ConfigManager.getKafkaBootstrapServers) .option(\u0026#34;subscribe\u0026#34;, topic) .option(\u0026#34;startingOffsets\u0026#34;, startingOffsets) .option(\u0026#34;endingOffsets\u0026#34;, endingOffsets) .load() } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;从Kafka批量读取数据失败: ${e.getMessage}\u0026#34;, e) throw new RuntimeException(s\u0026#34;Kafka批量读取失败: ${e.getMessage}\u0026#34;, e) } } // ==================== HBase 操作 ==================== /** * 获取 HBase 连接配置 */ def getHBaseConfig: org.apache.hadoop.conf.Configuration = { val conf = org.apache.hadoop.hbase.HBaseConfiguration.create() conf.set(\u0026#34;hbase.zookeeper.quorum\u0026#34;, ConfigManager.getHBaseZkQuorum) conf.set(\u0026#34;hbase.zookeeper.property.clientPort\u0026#34;, ConfigManager.getHBaseZkClientPort) conf } // ==================== SparkSession 管理 ==================== /** * 创建 SparkSession * * 使用 try-catch-finally 确保资源释放。 * 所有 SparkSession 的创建都应通过此方法，以统一配置和资源管理。 * * @param appName 应用名称 * @param hiveSupport 是否启用Hive支持 * @return SparkSession 实例 */ def createSparkSession(appName: String = ConfigManager.getSparkAppName, hiveSupport: Boolean = false): SparkSession = { logger.info(s\u0026#34;创建SparkSession: $appName, Hive支持: $hiveSupport\u0026#34;) try { val builder = SparkSession.builder() .appName(appName) .master(ConfigManager.getSparkMaster) // 应用 Spark 配置 ConfigManager.getSparkConfig.foreach { case (key, value) =\u0026gt; builder.config(key, value) } if (hiveSupport) { builder.enableHiveSupport() } val spark = builder.getOrCreate() spark.sparkContext.setLogLevel(\u0026#34;WARN\u0026#34;) logger.info(s\u0026#34;SparkSession创建成功: $appName\u0026#34;) spark } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;创建SparkSession失败: ${e.getMessage}\u0026#34;, e) throw new RuntimeException(s\u0026#34;SparkSession创建失败: ${e.getMessage}\u0026#34;, e) } } /** * 安全执行 Spark 作业 * * 使用 try-catch-finally 确保 SparkSession 正确释放。 * 这是执行 Spark 作业的推荐方式。 * * @param appName 应用名称 * @param hiveSupport 是否启用Hive支持 * @param block 业务逻辑块 * @tparam T 返回类型 * @return 业务逻辑执行结果 */ def withSparkSession[T](appName: String = ConfigManager.getSparkAppName, hiveSupport: Boolean = false)(block: SparkSession =\u0026gt; T): T = { try { val spark = createSparkSession(appName, hiveSupport) block(spark) } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;Spark作业执行失败: ${e.getMessage}\u0026#34;, e) throw new RuntimeException(s\u0026#34;Spark作业执行失败: ${e.getMessage}\u0026#34;, e) } finally { val s = SparkSession.getActiveSession.orNull if (s != null) { try { s.stop() logger.info(s\u0026#34;SparkSession已释放: $appName\u0026#34;) } catch { case e: Exception =\u0026gt; logger.warn(s\u0026#34;释放SparkSession时出错: ${e.getMessage}\u0026#34;, e) } } } } }\r3. 画像服务 - UserProfileService.scala\r#\rpackage com.ecommerce.profile.service import com.ecommerce.profile.config.ConfigManager import org.apache.hadoop.hbase.{HBaseConfiguration, TableName} import org.apache.hadoop.hbase.client.{Connection, ConnectionFactory, Get} import org.apache.hadoop.hbase.util.Bytes import org.slf4j.{Logger, LoggerFactory} import redis.clients.jedis.{Jedis, JedisPool, JedisPoolConfig} import java.util import scala.jdk.CollectionConverters._ import scala.util.{Try, Using} /** * 用户画像服务 * * 提供用户画像的查询、缓存和降级功能。 * * 核心特性： * - Redis 连接池管理（JedisPool） * - 多级缓存策略（Redis -\u0026gt; HBase -\u0026gt; MySQL） * - 降级方案（Redis不可用时自动降级到HBase） * - 数据验证和异常处理 * * 缓存策略： * 1. 首先查询 Redis 热数据缓存 * 2. Redis 未命中时，查询 HBase 标签数据 * 3. HBase 未命中时，查询 MySQL 结构化数据 * 4. 查询结果回填到 Redis 缓存 * * 降级方案： * - Redis 不可用 -\u0026gt; 直接查询 HBase * - HBase 不可用 -\u0026gt; 查询 MySQL（功能受限） * - 全部不可用 -\u0026gt; 返回默认画像数据 */ object UserProfileService { private val logger: Logger = LoggerFactory.getLogger(UserProfileService.getClass) // Redis 连接池（懒加载，线程安全） private lazy val jedisPool: JedisPool = { val poolConfig = new JedisPoolConfig() poolConfig.setMaxTotal(ConfigManager.getRedisMaxTotal) poolConfig.setMaxIdle(ConfigManager.getRedisMaxIdle) poolConfig.setMinIdle(ConfigManager.getRedisMinIdle) poolConfig.setMaxWaitMillis(ConfigManager.getRedisMaxWaitMillis) poolConfig.setTestOnBorrow(true) poolConfig.setTestOnReturn(false) poolConfig.setTestWhileIdle(true) val password = ConfigManager.getRedisPassword if (password.nonEmpty) { new JedisPool(poolConfig, ConfigManager.getRedisHost, ConfigManager.getRedisPort, ConfigManager.getRedisTimeout, password) } else { new JedisPool(poolConfig, ConfigManager.getRedisHost, ConfigManager.getRedisPort, ConfigManager.getRedisTimeout) } } // HBase 连接（懒加载） @volatile private var hbaseConnection: Connection = _ private def getHBaseConnection: Connection = { if (hbaseConnection == null || hbaseConnection.isClosed) { synchronized { if (hbaseConnection == null || hbaseConnection.isClosed) { val conf = HBaseConfiguration.create() conf.set(\u0026#34;hbase.zookeeper.quorum\u0026#34;, ConfigManager.getHBaseZkQuorum) conf.set(\u0026#34;hbase.zookeeper.property.clientPort\u0026#34;, ConfigManager.getHBaseZkClientPort) hbaseConnection = ConnectionFactory.createConnection(conf) logger.info(\u0026#34;HBase连接已创建\u0026#34;) } } } hbaseConnection } // ==================== Redis 操作 ==================== /** * 从 Redis 获取资源 * * 使用 JedisPool 连接池管理 Redis 连接，确保连接正确归还。 * * @param block 使用 Jedis 执行的操作 * @tparam T 返回类型 * @return 操作结果，Redis不可用时返回None */ private def withRedis[T](block: Jedis =\u0026gt; T): Option[T] = { var jedis: Jedis = null try { jedis = jedisPool.getResource Some(block(jedis)) } catch { case e: Exception =\u0026gt; logger.warn(s\u0026#34;Redis操作失败，将降级处理: ${e.getMessage}\u0026#34;) None } finally { if (jedis != null) { try { jedis.close() } catch { case e: Exception =\u0026gt; logger.warn(s\u0026#34;归还Redis连接失败: ${e.getMessage}\u0026#34;) } } } } /** * 从 Redis 缓存获取用户画像 * * @param userId 用户ID * @return 画像JSON字符串，未命中返回None */ def getProfileFromCache(userId: Int): Option[String] = { val key = s\u0026#34;${ConfigManager.getRedisKeyPrefix}$userId\u0026#34; withRedis { jedis =\u0026gt; val value = jedis.get(key) if (value != null) { logger.debug(s\u0026#34;Redis缓存命中: userId=$userId\u0026#34;) value } else { logger.debug(s\u0026#34;Redis缓存未命中: userId=$userId\u0026#34;) null } }.flatMap(Option(_)) } /** * 将用户画像写入 Redis 缓存 * * @param userId 用户ID * @param profileJson 画像JSON字符串 */ def setProfileToCache(userId: Int, profileJson: String): Unit = { val key = s\u0026#34;${ConfigManager.getRedisKeyPrefix}$userId\u0026#34; withRedis { jedis =\u0026gt; jedis.setex(key, ConfigManager.getRedisCacheTTL, profileJson) logger.debug(s\u0026#34;用户画像已写入Redis缓存: userId=$userId, TTL=${ConfigManager.getRedisCacheTTL}s\u0026#34;) } } /** * 批量将用户画像写入 Redis 缓存 * * 使用 Pipeline 提升批量写入性能。 * * @param profiles 用户ID到画像JSON的映射 */ def batchSetProfileToCache(profiles: Map[Int, String]): Unit = { if (profiles.isEmpty) return withRedis { jedis =\u0026gt; val pipeline = jedis.pipelined() profiles.foreach { case (userId, profileJson) =\u0026gt; val key = s\u0026#34;${ConfigManager.getRedisKeyPrefix}$userId\u0026#34; pipeline.setex(key, ConfigManager.getRedisCacheTTL, profileJson) } pipeline.sync() logger.info(s\u0026#34;批量写入Redis缓存完成: ${profiles.size}条记录\u0026#34;) } } /** * 从 Redis 缓存删除用户画像 * * @param userId 用户ID */ def removeProfileFromCache(userId: Int): Unit = { val key = s\u0026#34;${ConfigManager.getRedisKeyPrefix}$userId\u0026#34; withRedis { jedis =\u0026gt; jedis.del(key) logger.debug(s\u0026#34;用户画像已从Redis缓存删除: userId=$userId\u0026#34;) } } // ==================== HBase 操作 ==================== /** * 从 HBase 获取用户标签数据 * * @param userId 用户ID * @return 标签数据Map，查询失败返回None */ def getTagsFromHBase(userId: Int): Option[Map[String, String]] = { try { val connection = getHBaseConnection val table = connection.getTable(TableName.valueOf(ConfigManager.getHBaseUserTagsTable)) try { val get = new Get(Bytes.toBytes(userId.toString)) val result = table.get(get) if (result.isEmpty) { logger.debug(s\u0026#34;HBase中未找到用户标签: userId=$userId\u0026#34;) None } else { val tags = result.getNoVersionMap.asScala.flatMap { case (cf, columns) =\u0026gt; columns.asScala.map { case (qualifier, value) =\u0026gt; Bytes.toString(qualifier) -\u0026gt; Bytes.toString(value) } }.toMap logger.debug(s\u0026#34;HBase标签查询成功: userId=$userId, 标签数=${tags.size}\u0026#34;) Some(tags) } } finally { table.close() } } catch { case e: Exception =\u0026gt; logger.warn(s\u0026#34;HBase查询失败，将降级处理: userId=$userId, error=${e.getMessage}\u0026#34;) None } } // ==================== 多级查询（核心方法） ==================== /** * 获取用户画像（多级缓存查询） * * 查询顺序：Redis -\u0026gt; HBase -\u0026gt; MySQL * 每级查询成功后，会回填到上一级缓存。 * * @param userId 用户ID * @return 用户画像JSON字符串 */ def getUserProfile(userId: Int): Option[String] = { require(userId \u0026gt; 0, s\u0026#34;用户ID必须大于0，当前值: $userId\u0026#34;) logger.info(s\u0026#34;查询用户画像: userId=$userId\u0026#34;) // 第一级：查询 Redis 缓存 val redisResult = getProfileFromCache(userId) if (redisResult.isDefined) { logger.info(s\u0026#34;用户画像查询成功（Redis缓存）: userId=$userId\u0026#34;) return redisResult } // 第二级：查询 HBase val hbaseResult = getTagsFromHBase(userId) if (hbaseResult.isDefined) { val profileJson = hbaseResult.get.map { case (k, v) =\u0026gt; s\u0026#34;\u0026#34;\u0026#34;\u0026#34;$k\u0026#34;:\u0026#34;$v\u0026#34;\u0026#34;\u0026#34;\u0026#34; }.mkString(\u0026#34;{\u0026#34;, \u0026#34;,\u0026#34;, \u0026#34;}\u0026#34;) // 回填 Redis 缓存 setProfileToCache(userId, profileJson) logger.info(s\u0026#34;用户画像查询成功（HBase，已回填Redis）: userId=$userId\u0026#34;) return Some(profileJson) } // 第三级：查询 MySQL（降级方案） if (ConfigManager.isDegradationEnabled) { val mysqlResult = getProfileFromMySQL(userId) if (mysqlResult.isDefined) { // 回填 Redis 和 HBase 缓存 setProfileToCache(userId, mysqlResult.get) logger.info(s\u0026#34;用户画像查询成功（MySQL降级，已回填缓存）: userId=$userId\u0026#34;) return mysqlResult } } logger.warn(s\u0026#34;用户画像未找到: userId=$userId\u0026#34;) None } /** * 批量获取用户画像 * * @param userIds 用户ID列表 * @return 用户ID到画像的映射 */ def batchGetUserProfile(userIds: Seq[Int]): Map[Int, String] = { require(userIds.nonEmpty, \u0026#34;用户ID列表不能为空\u0026#34;) require(userIds.size \u0026lt;= ConfigManager.getBatchMaxSize, s\u0026#34;批量查询数量不能超过${ConfigManager.getBatchMaxSize}，当前: ${userIds.size}\u0026#34;) logger.info(s\u0026#34;批量查询用户画像: count=${userIds.size}\u0026#34;) val result = scala.collection.mutable.Map[Int, String]() // 先从 Redis 批量查询 val missedIds = scala.collection.mutable.ArrayBuffer[Int]() withRedis { jedis =\u0026gt; val pipeline = jedis.pipelined() val keys = userIds.map(id =\u0026gt; s\u0026#34;${ConfigManager.getRedisKeyPrefix}$id\u0026#34;) keys.foreach(key =\u0026gt; pipeline.get(key)) val responses = pipeline.syncAndReturnAll().asScala.zipWithIndex responses.foreach { case (response, index) =\u0026gt; val value = response.asInstanceOf[String] if (value != null) { result(userIds(index)) = value } else { missedIds += userIds(index) } } } // Redis 未命中的从 HBase 查询 if (missedIds.nonEmpty) { val hbaseProfiles = scala.collection.mutable.Map[Int, String]() missedIds.foreach { userId =\u0026gt; val hbaseResult = getTagsFromHBase(userId) if (hbaseResult.isDefined) { val profileJson = hbaseResult.get.map { case (k, v) =\u0026gt; s\u0026#34;\u0026#34;\u0026#34;\u0026#34;$k\u0026#34;:\u0026#34;$v\u0026#34;\u0026#34;\u0026#34;\u0026#34; }.mkString(\u0026#34;{\u0026#34;, \u0026#34;,\u0026#34;, \u0026#34;}\u0026#34;) hbaseProfiles(userId) = profileJson result(userId) = profileJson } } // 回填 Redis 缓存 if (hbaseProfiles.nonEmpty) { batchSetProfileToCache(hbaseProfiles.toMap) } } logger.info(s\u0026#34;批量查询完成: 总查询${userIds.size}条, Redis命中${userIds.size - missedIds.size}条, HBase命中${result.size - (userIds.size - missedIds.size)}条\u0026#34;) result.toMap } /** * 从 MySQL 查询用户画像（降级方案） * * @param userId 用户ID * @return 画像JSON字符串 */ private def getProfileFromMySQL(userId: Int): Option[String] = { import com.ecommerce.profile.dal.DataAccessLayer._ executeQuery( s\u0026#34;SELECT user_id, age_group, gender, purchase_power, shopping_frequency, \u0026#34; + s\u0026#34;rfm_score, user_segment FROM user_profiles WHERE user_id = $userId\u0026#34; ) { rs =\u0026gt; if (rs.next()) { val profile = Map( \u0026#34;user_id\u0026#34; -\u0026gt; rs.getInt(\u0026#34;user_id\u0026#34;).toString, \u0026#34;age_group\u0026#34; -\u0026gt; Option(rs.getString(\u0026#34;age_group\u0026#34;)).getOrElse(\u0026#34;未知\u0026#34;), \u0026#34;gender\u0026#34; -\u0026gt; Option(rs.getString(\u0026#34;gender\u0026#34;)).getOrElse(\u0026#34;未知\u0026#34;), \u0026#34;purchase_power\u0026#34; -\u0026gt; Option(rs.getString(\u0026#34;purchase_power\u0026#34;)).getOrElse(\u0026#34;未知\u0026#34;), \u0026#34;shopping_frequency\u0026#34; -\u0026gt; Option(rs.getString(\u0026#34;shopping_frequency\u0026#34;)).getOrElse(\u0026#34;未知\u0026#34;), \u0026#34;rfm_score\u0026#34; -\u0026gt; Option(rs.getString(\u0026#34;rfm_score\u0026#34;)).getOrElse(\u0026#34;000\u0026#34;), \u0026#34;user_segment\u0026#34; -\u0026gt; Option(rs.getString(\u0026#34;user_segment\u0026#34;)).getOrElse(\u0026#34;未分类\u0026#34;) ) profile.map { case (k, v) =\u0026gt; s\u0026#34;\u0026#34;\u0026#34;\u0026#34;$k\u0026#34;:\u0026#34;$v\u0026#34;\u0026#34;\u0026#34;\u0026#34; }.mkString(\u0026#34;{\u0026#34;, \u0026#34;,\u0026#34;, \u0026#34;}\u0026#34;) } else { null } }.flatMap(Option(_)) } // ==================== 资源管理 ==================== /** * 关闭所有连接资源 * * 在应用关闭时调用，确保所有连接正确释放。 */ def shutdown(): Unit = { logger.info(\u0026#34;正在关闭用户画像服务资源...\u0026#34;) // 关闭 Redis 连接池 try { if (jedisPool != null \u0026amp;\u0026amp; !jedisPool.isClosed) { jedisPool.close() logger.info(\u0026#34;Redis连接池已关闭\u0026#34;) } } catch { case e: Exception =\u0026gt; logger.warn(s\u0026#34;关闭Redis连接池时出错: ${e.getMessage}\u0026#34;) } // 关闭 HBase 连接 try { if (hbaseConnection != null \u0026amp;\u0026amp; !hbaseConnection.isClosed) { hbaseConnection.close() logger.info(\u0026#34;HBase连接已关闭\u0026#34;) } } catch { case e: Exception =\u0026gt; logger.warn(s\u0026#34;关闭HBase连接时出错: ${e.getMessage}\u0026#34;) } logger.info(\u0026#34;用户画像服务资源关闭完成\u0026#34;) } // 注册 JVM 关闭钩子，确保资源释放 sys.addShutdownHook { shutdown() } }\r4. 数据采集模块 - UserBehaviorProducer.scala\r#\rpackage com.ecommerce.profile.producer import com.ecommerce.profile.config.ConfigManager import org.apache.kafka.clients.producer.{KafkaProducer, ProducerConfig, ProducerRecord} import org.slf4j.{Logger, LoggerFactory} import java.util.Properties import scala.util.Random import scala.util.control.NonFatal /** * 用户行为数据采集器 * * 通过 Kafka 采集用户行为数据，包括浏览、点击、加购、收藏、购买等行为。 * * 改进点： * - 所有配置通过 ConfigManager 获取，不硬编码 * - 使用 try-catch-finally 确保 Kafka Producer 正确关闭 * - 添加数据验证逻辑 * - 支持优雅关闭（ShutdownHook） */ object UserBehaviorProducer { private val logger: Logger = LoggerFactory.getLogger(UserBehaviorProducer.getClass) @volatile private var running = true def main(args: Array[String]): Unit = { logger.info(\u0026#34;用户行为数据采集器启动中...\u0026#34;) // 注册优雅关闭钩子 sys.addShutdownHook { logger.info(\u0026#34;接收到关闭信号，正在优雅停止...\u0026#34;) running = false } val props = new Properties() props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, ConfigManager.getKafkaBootstrapServers) props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, \u0026#34;org.apache.kafka.common.serialization.StringSerializer\u0026#34;) props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, \u0026#34;org.apache.kafka.common.serialization.StringSerializer\u0026#34;) props.put(ProducerConfig.ACKS_CONFIG, ConfigManager.getConfig.getString(\u0026#34;kafka.producer.acks\u0026#34;)) props.put(ProducerConfig.RETRIES_CONFIG, ConfigManager.getConfig.getInt(\u0026#34;kafka.producer.retries\u0026#34;).asInstanceOf[Integer]) props.put(ProducerConfig.BATCH_SIZE_CONFIG, ConfigManager.getConfig.getInt(\u0026#34;kafka.producer.batch-size\u0026#34;).asInstanceOf[Integer]) props.put(ProducerConfig.LINGER_MS_CONFIG, ConfigManager.getConfig.getInt(\u0026#34;kafka.producer.linger-ms\u0026#34;).asInstanceOf[Integer]) var producer: KafkaProducer[String, String] = null try { producer = new KafkaProducer[String, String](props) val random = new Random() val behaviorTypes = Array(\u0026#34;view\u0026#34;, \u0026#34;click\u0026#34;, \u0026#34;add_to_cart\u0026#34;, \u0026#34;favorite\u0026#34;, \u0026#34;purchase\u0026#34;) val categories = Array(\u0026#34;电子产品\u0026#34;, \u0026#34;服装\u0026#34;, \u0026#34;食品\u0026#34;, \u0026#34;图书\u0026#34;, \u0026#34;家居\u0026#34;, \u0026#34;美妆\u0026#34;, \u0026#34;运动\u0026#34;, \u0026#34;母婴\u0026#34;) val devices = Array(\u0026#34;PC\u0026#34;, \u0026#34;Mobile\u0026#34;, \u0026#34;Tablet\u0026#34;) val topic = ConfigManager.getKafkaUserBehaviorTopic var sendCount = 0L logger.info(s\u0026#34;开始发送用户行为数据到Kafka主题: $topic\u0026#34;) while (running) { try { val behaviorId = System.currentTimeMillis() val userId = 1000 + random.nextInt(9000) val productId = 100 + random.nextInt(900) val behaviorType = behaviorTypes(random.nextInt(behaviorTypes.length)) val behaviorTime = System.currentTimeMillis() val sessionId = java.util.UUID.randomUUID().toString val ipAddress = s\u0026#34;192.${168 + random.nextInt(2)}.${random.nextInt(256)}.${random.nextInt(256)}\u0026#34; val deviceType = devices(random.nextInt(devices.length)) val category = categories(random.nextInt(categories.length)) // 数据验证 require(userId \u0026gt; 0, s\u0026#34;用户ID无效: $userId\u0026#34;) require(productId \u0026gt; 0, s\u0026#34;商品ID无效: $productId\u0026#34;) require(behaviorTypes.contains(behaviorType), s\u0026#34;行为类型无效: $behaviorType\u0026#34;) val behaviorData = s\u0026#34;\u0026#34;\u0026#34;{\u0026#34;behavior_id\u0026#34;:$behaviorId,\u0026#34;user_id\u0026#34;:$userId,\u0026#34;product_id\u0026#34;:$productId,\u0026#34;\u0026#34;\u0026#34; + s\u0026#34;\u0026#34;\u0026#34;\u0026#34;behavior_type\u0026#34;:\u0026#34;$behaviorType\u0026#34;,\u0026#34;behavior_time\u0026#34;:$behaviorTime,\u0026#34;\u0026#34;\u0026#34; + s\u0026#34;\u0026#34;\u0026#34;\u0026#34;session_id\u0026#34;:\u0026#34;$sessionId\u0026#34;,\u0026#34;ip_address\u0026#34;:\u0026#34;$ipAddress\u0026#34;,\u0026#34;\u0026#34;\u0026#34; + s\u0026#34;\u0026#34;\u0026#34;\u0026#34;device_type\u0026#34;:\u0026#34;$deviceType\u0026#34;,\u0026#34;category\u0026#34;:\u0026#34;$category\u0026#34;}\u0026#34;\u0026#34;\u0026#34; val record = new ProducerRecord[String, String](topic, userId.toString, behaviorData) producer.send(record) sendCount += 1 if (sendCount % 100 == 0) { logger.info(s\u0026#34;已发送 $sendCount 条用户行为数据\u0026#34;) } Thread.sleep(500) } catch { case NonFatal(e) =\u0026gt; logger.error(s\u0026#34;发送数据时出错: ${e.getMessage}\u0026#34;, e) Thread.sleep(1000) // 出错后等待1秒再重试 } } logger.info(s\u0026#34;数据采集器正常停止，共发送 $sendCount 条数据\u0026#34;) } catch { case NonFatal(e) =\u0026gt; logger.error(s\u0026#34;数据采集器异常: ${e.getMessage}\u0026#34;, e) } finally { if (producer != null) { try { producer.close() logger.info(\u0026#34;Kafka Producer 已关闭\u0026#34;) } catch { case NonFatal(e) =\u0026gt; logger.warn(s\u0026#34;关闭Kafka Producer时出错: ${e.getMessage}\u0026#34;) } } } } }\r5. HBase存储模块 - UserBehaviorToHBase.scala\r#\rpackage com.ecommerce.profile.storage import com.ecommerce.profile.config.ConfigManager import com.ecommerce.profile.dal.DataAccessLayer import org.apache.spark.sql.functions._ import org.apache.spark.sql.streaming.OutputMode import org.apache.spark.sql.types._ import org.apache.hadoop.hbase.{HBaseConfiguration, TableName} import org.apache.hadoop.hbase.client.{ConnectionFactory, Put} import org.apache.hadoop.hbase.util.Bytes import org.slf4j.{Logger, LoggerFactory} import scala.jdk.CollectionConverters._ import scala.util.control.NonFatal /** * 用户行为数据 HBase 存储 * * 从 Kafka 实时读取用户行为数据，写入 HBase。 * * 改进点： * - 使用 DataAccessLayer.withSparkSession 管理 SparkSession 生命周期 * - 所有配置通过 ConfigManager 获取 * - HBase 连接在 foreachBatch 中按批次创建和关闭 * - 添加数据验证和异常处理 */ object UserBehaviorToHBase { private val logger: Logger = LoggerFactory.getLogger(UserBehaviorToHBase.getClass) def main(args: Array[String]): Unit = { DataAccessLayer.withSparkSession(\u0026#34;UserBehaviorToHBase\u0026#34;) { spark =\u0026gt; import spark.implicits._ // 从 Kafka 读取用户行为数据 val behaviorStream = spark.readStream .format(\u0026#34;kafka\u0026#34;) .option(\u0026#34;kafka.bootstrap.servers\u0026#34;, ConfigManager.getKafkaBootstrapServers) .option(\u0026#34;subscribe\u0026#34;, ConfigManager.getKafkaUserBehaviorTopic) .option(\u0026#34;startingOffsets\u0026#34;, \u0026#34;latest\u0026#34;) .option(\u0026#34;failOnDataLoss\u0026#34;, \u0026#34;false\u0026#34;) .load() // 定义 Schema val schema = new StructType() .add(\u0026#34;behavior_id\u0026#34;, LongType) .add(\u0026#34;user_id\u0026#34;, IntegerType) .add(\u0026#34;product_id\u0026#34;, IntegerType) .add(\u0026#34;behavior_type\u0026#34;, StringType) .add(\u0026#34;behavior_time\u0026#34;, LongType) .add(\u0026#34;session_id\u0026#34;, StringType) .add(\u0026#34;ip_address\u0026#34;, StringType) .add(\u0026#34;device_type\u0026#34;, StringType) .add(\u0026#34;category\u0026#34;, StringType) // 解析 JSON 数据 val behaviorData = behaviorStream .selectExpr(\u0026#34;CAST(value AS STRING)\u0026#34;) .select(from_json($\u0026#34;value\u0026#34;, schema).as(\u0026#34;data\u0026#34;)) .select(\u0026#34;data.*\u0026#34;) .filter($\u0026#34;user_id\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;behavior_type\u0026#34;.isNotNull) // 数据验证 // 写入 HBase val query = behaviorData.writeStream .foreachBatch { (batchDF: org.apache.spark.sql.DataFrame, batchId: Long) =\u0026gt; logger.info(s\u0026#34;处理批次: $batchId, 记录数: ${batchDF.count()}\u0026#34;) if (batchDF.count() \u0026gt; 0) { var connection: org.apache.hadoop.hbase.client.Connection = null var table: org.apache.hadoop.hbase.client.Table = null try { // 创建 HBase 连接 val conf = HBaseConfiguration.create() conf.set(\u0026#34;hbase.zookeeper.quorum\u0026#34;, ConfigManager.getHBaseZkQuorum) conf.set(\u0026#34;hbase.zookeeper.property.clientPort\u0026#34;, ConfigManager.getHBaseZkClientPort) connection = ConnectionFactory.createConnection(conf) table = connection.getTable(TableName.valueOf(ConfigManager.getHBaseUserBehaviorTable)) batchDF.collect().foreach { row =\u0026gt; try { // 数据验证 val userId = row.getInt(1) val behaviorType = row.getString(3) if (userId \u0026gt; 0 \u0026amp;\u0026amp; behaviorType != null \u0026amp;\u0026amp; behaviorType.nonEmpty) { val rowKey = s\u0026#34;${row.getLong(4)}-${row.getLong(0)}\u0026#34; val put = new Put(Bytes.toBytes(rowKey)) put.addColumn(Bytes.toBytes(\u0026#34;cf\u0026#34;), Bytes.toBytes(\u0026#34;user_id\u0026#34;), Bytes.toBytes(userId)) put.addColumn(Bytes.toBytes(\u0026#34;cf\u0026#34;), Bytes.toBytes(\u0026#34;product_id\u0026#34;), Bytes.toBytes(row.getInt(2))) put.addColumn(Bytes.toBytes(\u0026#34;cf\u0026#34;), Bytes.toBytes(\u0026#34;behavior_type\u0026#34;), Bytes.toBytes(behaviorType)) put.addColumn(Bytes.toBytes(\u0026#34;cf\u0026#34;), Bytes.toBytes(\u0026#34;session_id\u0026#34;), Bytes.toBytes(row.getString(5))) put.addColumn(Bytes.toBytes(\u0026#34;cf\u0026#34;), Bytes.toBytes(\u0026#34;ip_address\u0026#34;), Bytes.toBytes(row.getString(6))) put.addColumn(Bytes.toBytes(\u0026#34;cf\u0026#34;), Bytes.toBytes(\u0026#34;device_type\u0026#34;), Bytes.toBytes(row.getString(7))) put.addColumn(Bytes.toBytes(\u0026#34;cf\u0026#34;), Bytes.toBytes(\u0026#34;category\u0026#34;), Bytes.toBytes(row.getString(8))) table.put(put) } } catch { case NonFatal(e) =\u0026gt; logger.warn(s\u0026#34;写入HBase单条记录失败: ${e.getMessage}\u0026#34;) } } logger.info(s\u0026#34;批次 $batchId 写入HBase完成\u0026#34;) } catch { case NonFatal(e) =\u0026gt; logger.error(s\u0026#34;批次 $batchId 写入HBase失败: ${e.getMessage}\u0026#34;, e) } finally { // 确保资源释放 if (table != null) { try table.close() catch { case NonFatal(e) =\u0026gt; logger.warn(s\u0026#34;关闭HBase Table失败: ${e.getMessage}\u0026#34;) } } if (connection != null) { try connection.close() catch { case NonFatal(e) =\u0026gt; logger.warn(s\u0026#34;关闭HBase Connection失败: ${e.getMessage}\u0026#34;) } } } } } .outputMode(OutputMode.Update()) .start() query.awaitTermination() } } }\r6. MySQL存储模块 - UserInfoToMySQL.scala\r#\rpackage com.ecommerce.profile.storage import com.ecommerce.profile.dal.DataAccessLayer import org.slf4j.{Logger, LoggerFactory} /** * 用户基础信息 MySQL 存储 * * 改进点： * - 使用 DataAccessLayer.withSparkSession 管理 SparkSession 生命周期 * - 所有数据库配置通过 ConfigManager 获取，不硬编码密码 * - 添加数据验证 */ object UserInfoToMySQL { private val logger: Logger = LoggerFactory.getLogger(UserInfoToMySQL.getClass) def main(args: Array[String]): Unit = { DataAccessLayer.withSparkSession(\u0026#34;UserInfoToMySQL\u0026#34;) { spark =\u0026gt; import spark.implicits._ // 生成模拟用户数据 val userData = Seq( (1, \u0026#34;user1\u0026#34;, \u0026#34;男\u0026#34;, 25, \u0026#34;user1@example.com\u0026#34;, \u0026#34;13800138001\u0026#34;, \u0026#34;2023-01-01\u0026#34;, \u0026#34;2023-12-01\u0026#34;, 2), (2, \u0026#34;user2\u0026#34;, \u0026#34;女\u0026#34;, 30, \u0026#34;user2@example.com\u0026#34;, \u0026#34;13800138002\u0026#34;, \u0026#34;2023-01-02\u0026#34;, \u0026#34;2023-12-02\u0026#34;, 3), (3, \u0026#34;user3\u0026#34;, \u0026#34;男\u0026#34;, 28, \u0026#34;user3@example.com\u0026#34;, \u0026#34;13800138003\u0026#34;, \u0026#34;2023-01-03\u0026#34;, \u0026#34;2023-12-03\u0026#34;, 2), (4, \u0026#34;user4\u0026#34;, \u0026#34;女\u0026#34;, 35, \u0026#34;user4@example.com\u0026#34;, \u0026#34;13800138004\u0026#34;, \u0026#34;2023-01-04\u0026#34;, \u0026#34;2023-12-04\u0026#34;, 4), (5, \u0026#34;user5\u0026#34;, \u0026#34;男\u0026#34;, 22, \u0026#34;user5@example.com\u0026#34;, \u0026#34;13800138005\u0026#34;, \u0026#34;2023-01-05\u0026#34;, \u0026#34;2023-12-05\u0026#34;, 1), (6, \u0026#34;user6\u0026#34;, \u0026#34;女\u0026#34;, 45, \u0026#34;user6@example.com\u0026#34;, \u0026#34;13800138006\u0026#34;, \u0026#34;2023-02-01\u0026#34;, \u0026#34;2023-12-06\u0026#34;, 5), (7, \u0026#34;user7\u0026#34;, \u0026#34;男\u0026#34;, 19, \u0026#34;user7@example.com\u0026#34;, \u0026#34;13800138007\u0026#34;, \u0026#34;2023-03-01\u0026#34;, \u0026#34;2023-12-07\u0026#34;, 1), (8, \u0026#34;user8\u0026#34;, \u0026#34;女\u0026#34;, 33, \u0026#34;user8@example.com\u0026#34;, \u0026#34;13800138008\u0026#34;, \u0026#34;2023-04-01\u0026#34;, \u0026#34;2023-12-08\u0026#34;, 3), (9, \u0026#34;user9\u0026#34;, \u0026#34;男\u0026#34;, 27, \u0026#34;user9@example.com\u0026#34;, \u0026#34;13800138009\u0026#34;, \u0026#34;2023-05-01\u0026#34;, \u0026#34;2023-12-09\u0026#34;, 2), (10, \u0026#34;user10\u0026#34;, \u0026#34;女\u0026#34;, 40, \u0026#34;user10@example.com\u0026#34;, \u0026#34;13800138010\u0026#34;, \u0026#34;2023-06-01\u0026#34;, \u0026#34;2023-12-10\u0026#34;, 4) ) val userDF = spark.createDataFrame(userData) .toDF(\u0026#34;user_id\u0026#34;, \u0026#34;username\u0026#34;, \u0026#34;gender\u0026#34;, \u0026#34;age\u0026#34;, \u0026#34;email\u0026#34;, \u0026#34;phone\u0026#34;, \u0026#34;registration_date\u0026#34;, \u0026#34;last_login_date\u0026#34;, \u0026#34;user_level\u0026#34;) // 数据验证：过滤无效数据 val validUserDF = userDF .filter($\u0026#34;user_id\u0026#34; \u0026gt; 0) .filter($\u0026#34;age\u0026#34; \u0026gt; 0 \u0026amp;\u0026amp; $\u0026#34;age\u0026#34; \u0026lt; 150) .filter($\u0026#34;username\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;username\u0026#34; =!= \u0026#34;\u0026#34;) logger.info(s\u0026#34;有效用户数据: ${validUserDF.count()} 条\u0026#34;) // 写入 MySQL DataAccessLayer.writeToMySQL(validUserDF, \u0026#34;users\u0026#34;, \u0026#34;overwrite\u0026#34;) logger.info(\u0026#34;用户数据已写入MySQL\u0026#34;) } } }\r7. 基础画像构建 - UserProfileBuilder.scala\r#\rpackage com.ecommerce.profile.builder import com.ecommerce.profile.dal.DataAccessLayer import com.ecommerce.profile.service.UserProfileService import org.apache.spark.sql.functions._ import org.apache.spark.sql.types._ import org.slf4j.{Logger, LoggerFactory} /** * 基础用户画像构建器 * * 从用户基础信息和行为数据中构建用户画像，包括： * - 年龄分组 * - 兴趣分类 * - 购物频率 * - 平均订单价值 * - 偏好品牌 * - 用户标签 * * 改进点： * - 使用 DataAccessLayer.withSparkSession 管理 SparkSession 生命周期 * - 所有数据库配置通过 ConfigManager 获取 * - 构建完成后自动更新 Redis 缓存 * - 添加数据验证和异常处理 */ object UserProfileBuilder { private val logger: Logger = LoggerFactory.getLogger(UserProfileBuilder.getClass) def main(args: Array[String]): Unit = { DataAccessLayer.withSparkSession(\u0026#34;UserProfileBuilder\u0026#34;, hiveSupport = true) { spark =\u0026gt; import spark.implicits._ try { // 1. 读取用户基础信息 val userDF = DataAccessLayer.readFromMySQL(spark, \u0026#34;users\u0026#34;) logger.info(s\u0026#34;读取用户基础信息: ${userDF.count()} 条\u0026#34;) // 2. 读取用户行为数据 val behaviorDF = DataAccessLayer.readFromMySQL(spark, \u0026#34;user_behavior\u0026#34;) logger.info(s\u0026#34;读取用户行为数据: ${behaviorDF.count()} 条\u0026#34;) // 3. 构建用户画像 // 3.1 年龄分组 val ageGroupDF = userDF .withColumn(\u0026#34;age_group\u0026#34;, when($\u0026#34;age\u0026#34; \u0026lt; 18, \u0026#34;未成年\u0026#34;) .when($\u0026#34;age\u0026#34; \u0026gt;= 18 \u0026amp;\u0026amp; $\u0026#34;age\u0026#34; \u0026lt; 30, \u0026#34;青年\u0026#34;) .when($\u0026#34;age\u0026#34; \u0026gt;= 30 \u0026amp;\u0026amp; $\u0026#34;age\u0026#34; \u0026lt; 50, \u0026#34;中年\u0026#34;) .otherwise(\u0026#34;老年\u0026#34;) ) // 3.2 兴趣分类 val interestDF = behaviorDF .filter($\u0026#34;category\u0026#34;.isNotNull) .groupBy($\u0026#34;user_id\u0026#34;, $\u0026#34;category\u0026#34;) .agg(count(\u0026#34;*\u0026#34;).as(\u0026#34;count\u0026#34;)) .orderBy($\u0026#34;user_id\u0026#34;, $\u0026#34;count\u0026#34;.desc) .groupBy($\u0026#34;user_id\u0026#34;) .agg(collect_list(struct($\u0026#34;category\u0026#34;, $\u0026#34;count\u0026#34;)).as(\u0026#34;interests\u0026#34;)) // 3.3 购物频率 val frequencyDF = behaviorDF .filter($\u0026#34;behavior_type\u0026#34; === \u0026#34;purchase\u0026#34;) .groupBy($\u0026#34;user_id\u0026#34;) .agg( count(\u0026#34;*\u0026#34;).as(\u0026#34;purchase_count\u0026#34;), datediff(current_date(), min($\u0026#34;behavior_time\u0026#34;)).as(\u0026#34;days_since_first_purchase\u0026#34;) ) .withColumn(\u0026#34;shopping_frequency\u0026#34;, when($\u0026#34;days_since_first_purchase\u0026#34; === 0, \u0026#34;高\u0026#34;) .otherwise( when($\u0026#34;purchase_count\u0026#34; / $\u0026#34;days_since_first_purchase\u0026#34; \u0026gt;= 0.5, \u0026#34;高\u0026#34;) .when($\u0026#34;purchase_count\u0026#34; / $\u0026#34;days_since_first_purchase\u0026#34; \u0026gt;= 0.1, \u0026#34;中\u0026#34;) .otherwise(\u0026#34;低\u0026#34;) ) ) // 3.4 平均订单价值 val orderValueDF = try { spark.sql( \u0026#34;\u0026#34;\u0026#34; |SELECT user_id, AVG(order_amount) as average_order_value |FROM orders |GROUP BY user_id |\u0026#34;\u0026#34;\u0026#34;.stripMargin) } catch { case e: Exception =\u0026gt; logger.warn(s\u0026#34;从Hive查询订单数据失败，使用默认值: ${e.getMessage}\u0026#34;) spark.emptyDataFrame } // 3.5 偏好品牌 val brandDF = try { spark.sql( \u0026#34;\u0026#34;\u0026#34; |SELECT user_id, product_brand, COUNT(*) as count |FROM order_items oi |JOIN products p ON oi.product_id = p.product_id |GROUP BY user_id, product_brand |ORDER BY user_id, count DESC |\u0026#34;\u0026#34;\u0026#34;.stripMargin) } catch { case e: Exception =\u0026gt; logger.warn(s\u0026#34;从Hive查询品牌数据失败: ${e.getMessage}\u0026#34;) spark.emptyDataFrame } val favoriteBrandsDF = if (!brandDF.isEmpty) { brandDF .groupBy($\u0026#34;user_id\u0026#34;) .agg(collect_list($\u0026#34;product_brand\u0026#34;).as(\u0026#34;favorite_brands\u0026#34;)) } else { spark.emptyDataFrame } // 3.6 购买力评估 val purchasePowerDF = frequencyDF .withColumn(\u0026#34;purchase_power\u0026#34;, when($\u0026#34;purchase_count\u0026#34; \u0026gt;= 20, \u0026#34;高\u0026#34;) .when($\u0026#34;purchase_count\u0026#34; \u0026gt;= 5, \u0026#34;中\u0026#34;) .otherwise(\u0026#34;低\u0026#34;) ) // 3.7 合并所有特征 var userProfile = ageGroupDF .join(interestDF, Seq(\u0026#34;user_id\u0026#34;), \u0026#34;left\u0026#34;) .join(purchasePowerDF.select($\u0026#34;user_id\u0026#34;, $\u0026#34;shopping_frequency\u0026#34;, $\u0026#34;purchase_power\u0026#34;), Seq(\u0026#34;user_id\u0026#34;), \u0026#34;left\u0026#34;) if (!orderValueDF.isEmpty) { userProfile = userProfile.join(orderValueDF, Seq(\u0026#34;user_id\u0026#34;), \u0026#34;left\u0026#34;) } if (!favoriteBrandsDF.isEmpty) { userProfile = userProfile.join(favoriteBrandsDF, Seq(\u0026#34;user_id\u0026#34;), \u0026#34;left\u0026#34;) } // 4. 生成用户标签 val userProfileWithTags = userProfile .withColumn(\u0026#34;user_tags\u0026#34;, array( when($\u0026#34;age_group\u0026#34; === \u0026#34;青年\u0026#34;, \u0026#34;青年用户\u0026#34;), when($\u0026#34;gender\u0026#34; === \u0026#34;男\u0026#34;, \u0026#34;男性用户\u0026#34;).otherwise(\u0026#34;女性用户\u0026#34;), when($\u0026#34;shopping_frequency\u0026#34; === \u0026#34;高\u0026#34;, \u0026#34;高频用户\u0026#34;), when($\u0026#34;purchase_power\u0026#34; === \u0026#34;高\u0026#34;, \u0026#34;高购买力用户\u0026#34;), when($\u0026#34;average_order_value\u0026#34; \u0026gt; 1000, \u0026#34;高价值用户\u0026#34;) )) .withColumn(\u0026#34;user_tags\u0026#34;, expr(\u0026#34;filter(user_tags, x -\u0026gt; x is not null)\u0026#34;)) // 5. 保存用户画像到 MySQL val finalProfile = userProfileWithTags.select( $\u0026#34;user_id\u0026#34;, $\u0026#34;age_group\u0026#34;, $\u0026#34;gender\u0026#34;, $\u0026#34;interests\u0026#34;.as(\u0026#34;interest_categories\u0026#34;), $\u0026#34;purchase_power\u0026#34;, $\u0026#34;shopping_frequency\u0026#34;, $\u0026#34;favorite_brands\u0026#34;, $\u0026#34;average_order_value\u0026#34;, $\u0026#34;user_tags\u0026#34; ) DataAccessLayer.writeToMySQL(finalProfile, \u0026#34;user_profiles\u0026#34;, \u0026#34;overwrite\u0026#34;) logger.info(\u0026#34;用户画像构建完成\u0026#34;) finalProfile.show(20, false) // 6. 更新 Redis 缓存 try { val profileData = finalProfile.collect() val profileMap = profileData.map { row =\u0026gt; val userId = row.getInt(0) val profileJson = s\u0026#34;\u0026#34;\u0026#34;{\u0026#34;user_id\u0026#34;:$userId,\u0026#34;age_group\u0026#34;:\u0026#34;${row.getString(1)}\u0026#34;,\u0026#34;gender\u0026#34;:\u0026#34;${row.getString(2)}\u0026#34;,\u0026#34;\u0026#34;\u0026#34; + s\u0026#34;\u0026#34;\u0026#34;\u0026#34;purchase_power\u0026#34;:\u0026#34;${row.getString(4)}\u0026#34;,\u0026#34;shopping_frequency\u0026#34;:\u0026#34;${row.getString(5)}\u0026#34;}\u0026#34;\u0026#34;\u0026#34; userId -\u0026gt; profileJson }.toMap UserProfileService.batchSetProfileToCache(profileMap) logger.info(s\u0026#34;用户画像已更新到Redis缓存: ${profileMap.size} 条\u0026#34;) } catch { case e: Exception =\u0026gt; logger.warn(s\u0026#34;更新Redis缓存失败（不影响主流程）: ${e.getMessage}\u0026#34;) } } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;用户画像构建失败: ${e.getMessage}\u0026#34;, e) throw e } } } }\r8. RFM分析 - RFMAnalyzer.scala\r#\rpackage com.ecommerce.profile.builder import com.ecommerce.profile.config.ConfigManager import com.ecommerce.profile.dal.DataAccessLayer import com.ecommerce.profile.service.UserProfileService import org.apache.spark.sql.functions._ import org.apache.spark.sql.types._ import org.slf4j.{Logger, LoggerFactory} /** * RFM 分析器 * * 基于 RFM 模型对用户进行价值分群。 * * RFM 模型三个维度： * - R (Recency)：最近消费时间距今天数，越小越好 * - F (Frequency)：统计周期内消费次数，越多越好 * - M (Monetary)：统计周期内消费总金额，越大越好 * * 评分规则： * - 每个维度按阈值划分为 1-5 分 * - 根据各维度评分与平均值的比较，将用户分为 8 种类型 * * 改进点： * - RFM 阈值通过配置文件管理，方便调整 * - 使用 DataAccessLayer 管理 SparkSession * - 评分结果写入 MySQL 和 Redis */ object RFMAnalyzer { private val logger: Logger = LoggerFactory.getLogger(RFMAnalyzer.getClass) /** * R 评分计算：最近消费时间越近，评分越高 */ private def calculateRScore(recencyDays: Int): Int = { val thresholds = ConfigManager.getRFMRThresholds if (recencyDays \u0026lt;= thresholds(0)) 5 else if (recencyDays \u0026lt;= thresholds(1)) 4 else if (recencyDays \u0026lt;= thresholds(2)) 3 else if (recencyDays \u0026lt;= thresholds(3)) 2 else 1 } /** * F 评分计算：消费频率越高，评分越高 */ private def calculateFScore(frequency: Int): Int = { val thresholds = ConfigManager.getRFMFThresholds if (frequency \u0026gt; thresholds(3)) 5 else if (frequency \u0026gt; thresholds(2)) 4 else if (frequency \u0026gt; thresholds(1)) 3 else if (frequency \u0026gt; thresholds(0)) 2 else 1 } /** * M 评分计算：消费金额越大，评分越高 */ private def calculateMScore(monetary: Double): Int = { val thresholds = ConfigManager.getRFMMThresholds if (monetary \u0026gt; thresholds(3)) 5 else if (monetary \u0026gt; thresholds(2)) 4 else if (monetary \u0026gt; thresholds(1)) 3 else if (monetary \u0026gt; thresholds(0)) 2 else 1 } /** * 根据 RFM 评分确定用户分群 * * 以各维度平均分为阈值，高于平均为\u0026#34;高\u0026#34;，低于平均为\u0026#34;低\u0026#34; */ private def determineUserSegment(rScore: Int, fScore: Int, mScore: Int, avgR: Double, avgF: Double, avgM: Double): String = { val rHigh = rScore \u0026gt;= avgR val fHigh = fScore \u0026gt;= avgF val mHigh = mScore \u0026gt;= avgM (rHigh, fHigh, mHigh) match { case (true, true, true) =\u0026gt; \u0026#34;重要价值用户\u0026#34; case (true, true, false) =\u0026gt; \u0026#34;重要保持用户\u0026#34; case (true, false, true) =\u0026gt; \u0026#34;重要发展用户\u0026#34; case (true, false, false) =\u0026gt; \u0026#34;重要挽留用户\u0026#34; case (false, true, true) =\u0026gt; \u0026#34;重要唤回用户\u0026#34; case (false, true, false) =\u0026gt; \u0026#34;一般维持用户\u0026#34; case (false, false, true) =\u0026gt; \u0026#34;一般发展用户\u0026#34; case (false, false, false) =\u0026gt; \u0026#34;流失预警用户\u0026#34; } } def main(args: Array[String]): Unit = { DataAccessLayer.withSparkSession(\u0026#34;RFMAnalyzer\u0026#34;, hiveSupport = true) { spark =\u0026gt; import spark.implicits._ try { val periodDays = ConfigManager.getRFMPeriodDays logger.info(s\u0026#34;开始RFM分析，统计周期: $periodDays 天\u0026#34;) // 1. 读取订单数据 val orderDF = try { DataAccessLayer.readFromMySQL(spark, \u0026#34;orders\u0026#34;) } catch { case e: Exception =\u0026gt; logger.warn(s\u0026#34;从MySQL读取订单数据失败，尝试从Hive读取: ${e.getMessage}\u0026#34;) spark.sql( s\u0026#34;\u0026#34;\u0026#34; |SELECT user_id, order_amount, order_time |FROM orders |WHERE order_time \u0026gt;= date_sub(current_date(), $periodDays) |\u0026#34;\u0026#34;\u0026#34;.stripMargin) } if (orderDF.isEmpty) { logger.warn(\u0026#34;订单数据为空，无法进行RFM分析\u0026#34;) return } // 2. 计算 RFM 原始值 val rfmRawDF = orderDF .filter($\u0026#34;user_id\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;order_amount\u0026#34; \u0026gt; 0) .groupBy($\u0026#34;user_id\u0026#34;) .agg( datediff(current_date(), max($\u0026#34;order_time\u0026#34;)).as(\u0026#34;recency\u0026#34;), count(\u0026#34;*\u0026#34;).as(\u0026#34;frequency\u0026#34;), sum($\u0026#34;order_amount\u0026#34;).as(\u0026#34;monetary\u0026#34;) ) // 3. 计算 RFM 评分 // 先注册评分 UDF spark.udf.register(\u0026#34;r_score\u0026#34;, (recency: Int) =\u0026gt; calculateRScore(recency)) spark.udf.register(\u0026#34;f_score\u0026#34;, (frequency: Int) =\u0026gt; calculateFScore(frequency)) spark.udf.register(\u0026#34;m_score\u0026#34;, (monetary: Double) =\u0026gt; calculateMScore(monetary)) val rfmScoredDF = rfmRawDF .withColumn(\u0026#34;r_score\u0026#34;, expr(\u0026#34;r_score(recency)\u0026#34;)) .withColumn(\u0026#34;f_score\u0026#34;, expr(\u0026#34;f_score(frequency)\u0026#34;)) .withColumn(\u0026#34;m_score\u0026#34;, expr(\u0026#34;m_score(monetary)\u0026#34;)) // 4. 计算各维度平均评分 val avgScores = rfmScoredDF.agg( avg(\u0026#34;r_score\u0026#34;).as(\u0026#34;avg_r\u0026#34;), avg(\u0026#34;f_score\u0026#34;).as(\u0026#34;avg_f\u0026#34;), avg(\u0026#34;m_score\u0026#34;).as(\u0026#34;avg_m\u0026#34;) ).head() val avgR = avgScores.getDouble(0) val avgF = avgScores.getDouble(1) val avgM = avgScores.getDouble(2) logger.info(s\u0026#34;RFM平均评分 - R: $avgR, F: $avgF, M: $avgM\u0026#34;) // 5. 用户分群 spark.udf.register(\u0026#34;determine_segment\u0026#34;, (rScore: Int, fScore: Int, mScore: Int) =\u0026gt; determineUserSegment(rScore, fScore, mScore, avgR, avgF, avgM) ) val rfmResultDF = rfmScoredDF .withColumn(\u0026#34;rfm_score\u0026#34;, concat($\u0026#34;r_score\u0026#34;, lit(\u0026#34;\u0026#34;), $\u0026#34;f_score\u0026#34;, lit(\u0026#34;\u0026#34;), $\u0026#34;m_score\u0026#34;) ) .withColumn(\u0026#34;user_segment\u0026#34;, expr(\u0026#34;determine_segment(r_score, f_score, m_score)\u0026#34;) ) .select( $\u0026#34;user_id\u0026#34;, $\u0026#34;recency\u0026#34;, $\u0026#34;frequency\u0026#34;, $\u0026#34;monetary\u0026#34;, $\u0026#34;r_score\u0026#34;, $\u0026#34;f_score\u0026#34;, $\u0026#34;m_score\u0026#34;, $\u0026#34;rfm_score\u0026#34;, $\u0026#34;user_segment\u0026#34; ) // 6. 输出分析结果 logger.info(\u0026#34;RFM分析结果：\u0026#34;) rfmResultDF.show(20, false) // 分群统计 val segmentStats = rfmResultDF .groupBy($\u0026#34;user_segment\u0026#34;) .agg( count(\u0026#34;*\u0026#34;).as(\u0026#34;user_count\u0026#34;), avg($\u0026#34;monetary\u0026#34;).as(\u0026#34;avg_monetary\u0026#34;), avg($\u0026#34;frequency\u0026#34;).as(\u0026#34;avg_frequency\u0026#34;) ) .orderBy($\u0026#34;user_count\u0026#34;.desc) logger.info(\u0026#34;用户分群统计：\u0026#34;) segmentStats.show(false) // 7. 保存 RFM 分析结果 DataAccessLayer.writeToMySQL(rfmResultDF, \u0026#34;user_rfm_analysis\u0026#34;, \u0026#34;overwrite\u0026#34;) // 8. 更新用户画像表中的 RFM 字段 val updateDF = rfmResultDF.select($\u0026#34;user_id\u0026#34;, $\u0026#34;rfm_score\u0026#34;, $\u0026#34;user_segment\u0026#34;) DataAccessLayer.writeToMySQL(updateDF, \u0026#34;user_segments\u0026#34;, \u0026#34;overwrite\u0026#34;) // 9. 更新 Redis 缓存 try { val rfmData = rfmResultDF.select($\u0026#34;user_id\u0026#34;, $\u0026#34;rfm_score\u0026#34;, $\u0026#34;user_segment\u0026#34;).collect() val rfmMap = rfmData.map { row =\u0026gt; val userId = row.getInt(0) val profileJson = s\u0026#34;\u0026#34;\u0026#34;{\u0026#34;user_id\u0026#34;:$userId,\u0026#34;rfm_score\u0026#34;:\u0026#34;${row.getString(1)}\u0026#34;,\u0026#34;user_segment\u0026#34;:\u0026#34;${row.getString(2)}\u0026#34;}\u0026#34;\u0026#34;\u0026#34; userId -\u0026gt; profileJson }.toMap UserProfileService.batchSetProfileToCache(rfmMap) logger.info(s\u0026#34;RFM结果已更新到Redis缓存: ${rfmMap.size} 条\u0026#34;) } catch { case e: Exception =\u0026gt; logger.warn(s\u0026#34;更新Redis缓存失败（不影响主流程）: ${e.getMessage}\u0026#34;) } logger.info(\u0026#34;RFM分析完成\u0026#34;) } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;RFM分析失败: ${e.getMessage}\u0026#34;, e) throw e } } } }\r9. 用户分群 - UserSegmentation.scala\r#\rpackage com.ecommerce.profile.builder import com.ecommerce.profile.dal.DataAccessLayer import org.apache.spark.ml.clustering.KMeans import org.apache.spark.ml.evaluation.ClusteringEvaluator import org.apache.spark.ml.feature.{StandardScaler, VectorAssembler} import org.apache.spark.sql.functions._ import org.slf4j.{Logger, LoggerFactory} /** * 用户分群（基于机器学习） * * 使用 K-Means 聚类算法对用户进行分群，结合 RFM 分析结果， * 实现更精细的用户价值分层。 * * 改进点： * - 使用 DataAccessLayer.withSparkSession 管理 SparkSession 生命周期 * - 所有数据库配置通过 ConfigManager 获取 * - 添加模型评估和参数调优 */ object UserSegmentation { private val logger: Logger = LoggerFactory.getLogger(UserSegmentation.getClass) def main(args: Array[String]): Unit = { DataAccessLayer.withSparkSession(\u0026#34;UserSegmentation\u0026#34;) { spark =\u0026gt; import spark.implicits._ try { // 读取用户画像数据 val profileDF = DataAccessLayer.readFromMySQL(spark, \u0026#34;user_profiles\u0026#34;) logger.info(s\u0026#34;读取用户画像数据: ${profileDF.count()} 条\u0026#34;) if (profileDF.isEmpty) { logger.warn(\u0026#34;用户画像数据为空，无法进行分群\u0026#34;) return } // 准备特征 val featureDF = profileDF .withColumn(\u0026#34;age_group_num\u0026#34;, when($\u0026#34;age_group\u0026#34; === \u0026#34;未成年\u0026#34;, 0) .when($\u0026#34;age_group\u0026#34; === \u0026#34;青年\u0026#34;, 1) .when($\u0026#34;age_group\u0026#34; === \u0026#34;中年\u0026#34;, 2) .otherwise(3) ) .withColumn(\u0026#34;gender_num\u0026#34;, when($\u0026#34;gender\u0026#34; === \u0026#34;男\u0026#34;, 0).otherwise(1)) .withColumn(\u0026#34;frequency_num\u0026#34;, when($\u0026#34;shopping_frequency\u0026#34; === \u0026#34;低\u0026#34;, 0) .when($\u0026#34;shopping_frequency\u0026#34; === \u0026#34;中\u0026#34;, 1) .otherwise(2) ) .withColumn(\u0026#34;value_num\u0026#34;, when($\u0026#34;average_order_value\u0026#34; \u0026lt; 500, 0) .when($\u0026#34;average_order_value\u0026#34; \u0026lt; 1500, 1) .otherwise(2) ) .na.fill(0, Seq(\u0026#34;age_group_num\u0026#34;, \u0026#34;gender_num\u0026#34;, \u0026#34;frequency_num\u0026#34;, \u0026#34;value_num\u0026#34;)) // 特征向量化 val assembler = new VectorAssembler() .setInputCols(Array(\u0026#34;age_group_num\u0026#34;, \u0026#34;gender_num\u0026#34;, \u0026#34;frequency_num\u0026#34;, \u0026#34;value_num\u0026#34;)) .setOutputCol(\u0026#34;features\u0026#34;) val features = assembler.transform(featureDF) // 特征标准化 val scaler = new StandardScaler() .setInputCol(\u0026#34;features\u0026#34;) .setOutputCol(\u0026#34;scaledFeatures\u0026#34;) .setWithStd(true) .setWithMean(false) val scaledFeatures = scaler.fit(features).transform(features) // 寻找最优 K 值 val maxK = math.min(10, profileDF.count().toInt / 10).max(2) var bestK = 5 var bestSilhouette = -1.0 logger.info(s\u0026#34;开始寻找最优K值，范围: 2 到 $maxK\u0026#34;) for (k \u0026lt;- 2 to maxK) { try { val kmeans = new KMeans() .setK(k) .setSeed(42L) .setFeaturesCol(\u0026#34;scaledFeatures\u0026#34;) .setPredictionCol(\u0026#34;cluster\u0026#34;) val model = kmeans.fit(scaledFeatures) val predictions = model.transform(scaledFeatures) val evaluator = new ClusteringEvaluator() .setFeaturesCol(\u0026#34;scaledFeatures\u0026#34;) .setPredictionCol(\u0026#34;cluster\u0026#34;) val silhouette = evaluator.evaluate(predictions) logger.info(s\u0026#34;K=$k, Silhouette=$silhouette\u0026#34;) if (silhouette \u0026gt; bestSilhouette) { bestSilhouette = silhouette bestK = k } } catch { case e: Exception =\u0026gt; logger.warn(s\u0026#34;K=$k 聚类失败: ${e.getMessage}\u0026#34;) } } logger.info(s\u0026#34;最优K值: $bestK, Silhouette: $bestSilhouette\u0026#34;) // 使用最优 K 值进行聚类 val bestKMeans = new KMeans() .setK(bestK) .setSeed(42L) .setFeaturesCol(\u0026#34;scaledFeatures\u0026#34;) .setPredictionCol(\u0026#34;cluster\u0026#34;) val bestModel = bestKMeans.fit(scaledFeatures) val predictions = bestModel.transform(scaledFeatures) // 评估模型 val evaluator = new ClusteringEvaluator() .setFeaturesCol(\u0026#34;scaledFeatures\u0026#34;) .setPredictionCol(\u0026#34;cluster\u0026#34;) val silhouette = evaluator.evaluate(predictions) logger.info(s\u0026#34;最终模型 Silhouette score: $silhouette\u0026#34;) // 分析聚类结果 val clusterAnalysis = predictions .groupBy($\u0026#34;cluster\u0026#34;) .agg( count(\u0026#34;*\u0026#34;).as(\u0026#34;user_count\u0026#34;), avg($\u0026#34;age_group_num\u0026#34;).as(\u0026#34;avg_age_group\u0026#34;), avg($\u0026#34;gender_num\u0026#34;).as(\u0026#34;avg_gender\u0026#34;), avg($\u0026#34;frequency_num\u0026#34;).as(\u0026#34;avg_frequency\u0026#34;), avg($\u0026#34;value_num\u0026#34;).as(\u0026#34;avg_value\u0026#34;) ) logger.info(\u0026#34;聚类分析结果：\u0026#34;) clusterAnalysis.show(false) // 为用户添加聚类标签 val userWithCluster = predictions .select( $\u0026#34;user_id\u0026#34;, $\u0026#34;cluster\u0026#34;, when($\u0026#34;cluster\u0026#34; === 0, \u0026#34;高价值高频用户\u0026#34;) .when($\u0026#34;cluster\u0026#34; === 1, \u0026#34;普通价值高频用户\u0026#34;) .when($\u0026#34;cluster\u0026#34; === 2, \u0026#34;高价值低频用户\u0026#34;) .when($\u0026#34;cluster\u0026#34; === 3, \u0026#34;普通价值低频用户\u0026#34;) .otherwise(\u0026#34;低价值用户\u0026#34;).as(\u0026#34;user_segment\u0026#34;) ) // 保存聚类结果 DataAccessLayer.writeToMySQL(userWithCluster, \u0026#34;user_segments_ml\u0026#34;, \u0026#34;overwrite\u0026#34;) logger.info(\u0026#34;用户分群完成\u0026#34;) userWithCluster.show(20, false) } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;用户分群失败: ${e.getMessage}\u0026#34;, e) throw e } } } }\r10. 个性化推荐 - PersonalizedRecommendation.scala\r#\rpackage com.ecommerce.profile.recommendation import com.ecommerce.profile.dal.DataAccessLayer import org.apache.spark.ml.evaluation.RegressionEvaluator import org.apache.spark.ml.recommendation.ALS import org.apache.spark.sql.functions._ import org.slf4j.{Logger, LoggerFactory} /** * 个性化推荐 * * 基于 ALS 协同过滤算法，根据用户行为数据为用户推荐商品。 * * 改进点： * - 使用 DataAccessLayer.withSparkSession 管理 SparkSession 生命周期 * - 所有数据库配置通过 ConfigManager 获取 * - 添加模型评估和冷启动策略 */ object PersonalizedRecommendation { private val logger: Logger = LoggerFactory.getLogger(PersonalizedRecommendation.getClass) def main(args: Array[String]): Unit = { DataAccessLayer.withSparkSession(\u0026#34;PersonalizedRecommendation\u0026#34;) { spark =\u0026gt; import spark.implicits._ try { // 读取用户行为数据 val behaviorDF = DataAccessLayer.readFromMySQL(spark, \u0026#34;user_behavior\u0026#34;) logger.info(s\u0026#34;读取用户行为数据: ${behaviorDF.count()} 条\u0026#34;) if (behaviorDF.isEmpty) { logger.warn(\u0026#34;用户行为数据为空，无法进行推荐\u0026#34;) return } // 转换为评分数据 val ratingDF = behaviorDF .withColumn(\u0026#34;rating\u0026#34;, when($\u0026#34;behavior_type\u0026#34; === \u0026#34;purchase\u0026#34;, 5.0) .when($\u0026#34;behavior_type\u0026#34; === \u0026#34;add_to_cart\u0026#34;, 4.0) .when($\u0026#34;behavior_type\u0026#34; === \u0026#34;favorite\u0026#34;, 3.0) .when($\u0026#34;behavior_type\u0026#34; === \u0026#34;click\u0026#34;, 2.0) .otherwise(1.0) ) .select($\u0026#34;user_id\u0026#34;, $\u0026#34;product_id\u0026#34;, $\u0026#34;rating\u0026#34;) .filter($\u0026#34;user_id\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;product_id\u0026#34;.isNotNull) // 划分训练集和测试集 val Array(training, test) = ratingDF.randomSplit(Array(0.8, 0.2), seed = 42L) // 构建 ALS 模型 val als = new ALS() .setMaxIter(10) .setRegParam(0.01) .setUserCol(\u0026#34;user_id\u0026#34;) .setItemCol(\u0026#34;product_id\u0026#34;) .setRatingCol(\u0026#34;rating\u0026#34;) .setColdStartStrategy(\u0026#34;drop\u0026#34;) .setNonnegative(true) val model = als.fit(training) // 评估模型 val predictions = model.transform(test) val evaluator = new RegressionEvaluator() .setMetricName(\u0026#34;rmse\u0026#34;) .setLabelCol(\u0026#34;rating\u0026#34;) .setPredictionCol(\u0026#34;prediction\u0026#34;) val rmse = evaluator.evaluate(predictions) logger.info(s\u0026#34;Root-mean-square error = $rmse\u0026#34;) // 为所有用户推荐商品 val userRecommendations = model.recommendForAllUsers(10) // 保存推荐结果 DataAccessLayer.writeToMySQL(userRecommendations, \u0026#34;user_recommendations\u0026#34;, \u0026#34;overwrite\u0026#34;) logger.info(\u0026#34;个性化推荐完成\u0026#34;) userRecommendations.show(10, false) } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;个性化推荐失败: ${e.getMessage}\u0026#34;, e) throw e } } } }\r11. 精准营销 - TargetedMarketing.scala\r#\rpackage com.ecommerce.profile.marketing import com.ecommerce.profile.dal.DataAccessLayer import org.apache.spark.sql.functions._ import org.slf4j.{Logger, LoggerFactory} /** * 精准营销 * * 根据用户画像和分群数据，筛选目标用户群体，支持： * - 高价值用户营销 * - 流失风险用户挽回 * - 特定兴趣用户定向推送 * * 改进点： * - 使用 DataAccessLayer.withSparkSession 管理 SparkSession 生命周期 * - 所有数据库配置通过 ConfigManager 获取 * - 添加数据验证 */ object TargetedMarketing { private val logger: Logger = LoggerFactory.getLogger(TargetedMarketing.getClass) def main(args: Array[String]): Unit = { DataAccessLayer.withSparkSession(\u0026#34;TargetedMarketing\u0026#34;) { spark =\u0026gt; import spark.implicits._ try { // 读取用户画像和分群数据 val profileDF = DataAccessLayer.readFromMySQL(spark, \u0026#34;user_profiles\u0026#34;) val segmentDF = DataAccessLayer.readFromMySQL(spark, \u0026#34;user_segments\u0026#34;) // 合并数据 val userData = profileDF.join(segmentDF, Seq(\u0026#34;user_id\u0026#34;), \u0026#34;left\u0026#34;) logger.info(s\u0026#34;用户画像+分群数据: ${userData.count()} 条\u0026#34;) // 1. 针对高价值用户的营销 val highValueUsers = userData .filter($\u0026#34;user_segment\u0026#34; === \u0026#34;重要价值用户\u0026#34; || $\u0026#34;user_segment\u0026#34; === \u0026#34;重要发展用户\u0026#34;) .select($\u0026#34;user_id\u0026#34;, $\u0026#34;age_group\u0026#34;, $\u0026#34;gender\u0026#34;, $\u0026#34;shopping_frequency\u0026#34;, $\u0026#34;user_segment\u0026#34;) logger.info(\u0026#34;高价值用户列表：\u0026#34;) highValueUsers.show(false) // 2. 针对流失风险用户的营销 val churnRiskUsers = userData .filter($\u0026#34;user_segment\u0026#34; === \u0026#34;流失预警用户\u0026#34; || $\u0026#34;user_segment\u0026#34; === \u0026#34;重要唤回用户\u0026#34;) .select($\u0026#34;user_id\u0026#34;, $\u0026#34;age_group\u0026#34;, $\u0026#34;gender\u0026#34;, $\u0026#34;shopping_frequency\u0026#34;, $\u0026#34;user_segment\u0026#34;) logger.info(\u0026#34;流失风险用户列表：\u0026#34;) churnRiskUsers.show(false) // 3. 针对特定兴趣用户的营销 val categoryInterestUsers = userData .filter(array_contains($\u0026#34;interest_categories.category\u0026#34;, \u0026#34;电子产品\u0026#34;)) .select($\u0026#34;user_id\u0026#34;, $\u0026#34;age_group\u0026#34;, $\u0026#34;gender\u0026#34;, $\u0026#34;shopping_frequency\u0026#34;) logger.info(\u0026#34;对电子产品感兴趣的用户列表：\u0026#34;) categoryInterestUsers.show(false) // 4. 保存营销目标用户 val marketingTargets = userData .withColumn(\u0026#34;marketing_type\u0026#34;, when($\u0026#34;user_segment\u0026#34; === \u0026#34;重要价值用户\u0026#34;, \u0026#34;VIP维护\u0026#34;) .when($\u0026#34;user_segment\u0026#34; === \u0026#34;重要发展用户\u0026#34;, \u0026#34;价值提升\u0026#34;) .when($\u0026#34;user_segment\u0026#34; === \u0026#34;流失预警用户\u0026#34;, \u0026#34;流失挽回\u0026#34;) .when($\u0026#34;user_segment\u0026#34; === \u0026#34;重要唤回用户\u0026#34;, \u0026#34;召回激活\u0026#34;) .otherwise(\u0026#34;常规触达\u0026#34;) ) .select($\u0026#34;user_id\u0026#34;, $\u0026#34;user_segment\u0026#34;, $\u0026#34;marketing_type\u0026#34;) DataAccessLayer.writeToMySQL(marketingTargets, \u0026#34;marketing_targets\u0026#34;, \u0026#34;overwrite\u0026#34;) logger.info(\u0026#34;营销目标用户已保存\u0026#34;) // 5. 输出营销策略建议 val strategyStats = marketingTargets .groupBy($\u0026#34;marketing_type\u0026#34;) .agg(count(\u0026#34;*\u0026#34;).as(\u0026#34;target_count\u0026#34;)) .orderBy($\u0026#34;target_count\u0026#34;.desc) logger.info(\u0026#34;营销策略统计：\u0026#34;) strategyStats.show(false) } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;精准营销分析失败: ${e.getMessage}\u0026#34;, e) throw e } } } }\r12. 数据库初始化脚本 - init_tables.sql\r#\r-- ============================================ -- 电商用户画像系统 - 数据库初始化脚本 -- MySQL 8.0+ -- ============================================ -- 创建数据库 CREATE DATABASE IF NOT EXISTS ecommerce DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE ecommerce; -- 1. 用户基础信息表 CREATE TABLE IF NOT EXISTS users ( user_id INT PRIMARY KEY, username VARCHAR(50) NOT NULL, gender VARCHAR(10), age INT, email VARCHAR(100), phone VARCHAR(20), registration_date DATE, last_login_date DATE, user_level INT DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_gender (gender), INDEX idx_age (age), INDEX idx_user_level (user_level) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 2. 用户行为数据表 CREATE TABLE IF NOT EXISTS user_behavior ( behavior_id BIGINT PRIMARY KEY, user_id INT NOT NULL, product_id INT NOT NULL, behavior_type VARCHAR(20) NOT NULL, behavior_time TIMESTAMP NOT NULL, session_id VARCHAR(100), ip_address VARCHAR(50), device_type VARCHAR(50), category VARCHAR(50), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_id (user_id), INDEX idx_behavior_type (behavior_type), INDEX idx_behavior_time (behavior_time), INDEX idx_user_behavior (user_id, behavior_type), INDEX idx_user_time (user_id, behavior_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 3. 商品数据表 CREATE TABLE IF NOT EXISTS products ( product_id INT PRIMARY KEY, product_name VARCHAR(100) NOT NULL, category_id INT, category_name VARCHAR(50), price DECIMAL(10,2), brand VARCHAR(50), stock INT DEFAULT 0, sales INT DEFAULT 0, rating DECIMAL(3,2) DEFAULT 0.00, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category_id), INDEX idx_brand (brand), INDEX idx_price (price) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 4. 用户画像表 CREATE TABLE IF NOT EXISTS user_profiles ( user_id INT PRIMARY KEY, age_group VARCHAR(20), gender VARCHAR(10), interest_categories JSON, purchase_power VARCHAR(20), shopping_frequency VARCHAR(20), favorite_brands JSON, average_order_value DECIMAL(10,2), last_purchase_date DATE, user_tags JSON, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_age_group (age_group), INDEX idx_gender (gender), INDEX idx_purchase_power (purchase_power), INDEX idx_shopping_frequency (shopping_frequency) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 5. 用户RFM分析表 CREATE TABLE IF NOT EXISTS user_rfm_analysis ( user_id INT PRIMARY KEY, recency INT COMMENT \u0026#39;最近消费距今天数\u0026#39;, frequency INT COMMENT \u0026#39;消费频次\u0026#39;, monetary DOUBLE COMMENT \u0026#39;消费总金额\u0026#39;, r_score INT COMMENT \u0026#39;R评分(1-5)\u0026#39;, f_score INT COMMENT \u0026#39;F评分(1-5)\u0026#39;, m_score INT COMMENT \u0026#39;M评分(1-5)\u0026#39;, rfm_score VARCHAR(10) COMMENT \u0026#39;RFM组合评分\u0026#39;, user_segment VARCHAR(30) COMMENT \u0026#39;用户分群\u0026#39;, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_rfm_score (rfm_score), INDEX idx_user_segment (user_segment), INDEX idx_r_score (r_score), INDEX idx_f_score (f_score), INDEX idx_m_score (m_score) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 6. 用户分群表（机器学习分群结果） CREATE TABLE IF NOT EXISTS user_segments ( user_id INT PRIMARY KEY, rfm_score VARCHAR(10), user_segment VARCHAR(30), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_user_segment (user_segment) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 7. 用户分群表（ML分群结果） CREATE TABLE IF NOT EXISTS user_segments_ml ( user_id INT PRIMARY KEY, cluster INT, user_segment VARCHAR(30), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_cluster (cluster), INDEX idx_user_segment (user_segment) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 8. 用户推荐结果表 CREATE TABLE IF NOT EXISTS user_recommendations ( user_id INT, recommendations ARRAY\u0026lt;STRUCT\u0026lt;product_id: INT, rating: FLOAT\u0026gt;\u0026gt;, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 9. 营销目标用户表 CREATE TABLE IF NOT EXISTS marketing_targets ( user_id INT, user_segment VARCHAR(30), marketing_type VARCHAR(30), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, marketing_type), INDEX idx_marketing_type (marketing_type) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 10. 标签元数据表 CREATE TABLE IF NOT EXISTS tag_metadata ( tag_id INT AUTO_INCREMENT PRIMARY KEY, tag_name VARCHAR(100) NOT NULL COMMENT \u0026#39;标签名称\u0026#39;, tag_category VARCHAR(50) COMMENT \u0026#39;一级分类\u0026#39;, tag_subcategory VARCHAR(50) COMMENT \u0026#39;二级分类\u0026#39;, tag_type VARCHAR(20) COMMENT \u0026#39;标签类型：事实/统计/算法\u0026#39;, tag_description VARCHAR(500) COMMENT \u0026#39;标签描述\u0026#39;, is_active TINYINT DEFAULT 1 COMMENT \u0026#39;是否启用\u0026#39;, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE INDEX idx_tag_name (tag_name), INDEX idx_tag_category (tag_category), INDEX idx_tag_type (tag_type) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\r可视化模块\r#\rGrafana仪表板配置\r#\r配置Grafana连接MySQL数据源，创建以下仪表板：\n用户画像概览仪表板\n用户年龄分布 用户性别分布 用户兴趣分类分布 用户购物频率分布 用户价值分布 RFM分析仪表板\n各分群用户数量 RFM评分分布 分群特征对比 分群价值分析 营销效果仪表板\n营销活动转化率 不同用户群的响应率 营销ROI分析 数据可视化代码\r#\r# dashboard.py import os import pandas as pd import matplotlib.pyplot as plt import seaborn as sns import mysql.connector # 从环境变量获取数据库连接信息 MYSQL_HOST = os.environ.get(\u0026#34;MYSQL_HOST\u0026#34;, \u0026#34;localhost\u0026#34;) MYSQL_USER = os.environ.get(\u0026#34;MYSQL_USER\u0026#34;, \u0026#34;ecommerce_user\u0026#34;) MYSQL_PASSWORD = os.environ.get(\u0026#34;MYSQL_PASSWORD\u0026#34;, \u0026#34;\u0026#34;) MYSQL_DATABASE = os.environ.get(\u0026#34;MYSQL_DATABASE\u0026#34;, \u0026#34;ecommerce\u0026#34;) # 连接MySQL conn = mysql.connector.connect( host=MYSQL_HOST, user=MYSQL_USER, password=MYSQL_PASSWORD, database=MYSQL_DATABASE ) try: # 读取用户画像数据 profile_df = pd.read_sql(\u0026#34;SELECT * FROM user_profiles\u0026#34;, conn) # 读取RFM分析数据 rfm_df = pd.read_sql(\u0026#34;SELECT * FROM user_rfm_analysis\u0026#34;, conn) # 读取用户分群数据 segment_df = pd.read_sql(\u0026#34;SELECT * FROM user_segments\u0026#34;, conn) # 设置中文字体 plt.rcParams[\u0026#39;font.sans-serif\u0026#39;] = [\u0026#39;SimHei\u0026#39;] plt.rcParams[\u0026#39;axes.unicode_minus\u0026#39;] = False # 1. 用户年龄分布 plt.figure(figsize=(12, 6)) sns.countplot(x=\u0026#39;age_group\u0026#39;, data=profile_df, order=[\u0026#39;未成年\u0026#39;, \u0026#39;青年\u0026#39;, \u0026#39;中年\u0026#39;, \u0026#39;老年\u0026#39;]) plt.title(\u0026#39;用户年龄分布\u0026#39;) plt.xlabel(\u0026#39;年龄组\u0026#39;) plt.ylabel(\u0026#39;用户数\u0026#39;) plt.savefig(\u0026#39;age_distribution.png\u0026#39;, dpi=150, bbox_inches=\u0026#39;tight\u0026#39;) plt.close() # 2. 用户性别分布 plt.figure(figsize=(8, 8)) gender_counts = profile_df[\u0026#39;gender\u0026#39;].value_counts() gender_counts.plot(kind=\u0026#39;pie\u0026#39;, autopct=\u0026#39;%1.1f%%\u0026#39;, colors=[\u0026#39;#66b3ff\u0026#39;, \u0026#39;#ff9999\u0026#39;]) plt.title(\u0026#39;用户性别分布\u0026#39;) plt.savefig(\u0026#39;gender_distribution.png\u0026#39;, dpi=150, bbox_inches=\u0026#39;tight\u0026#39;) plt.close() # 3. RFM用户分群分布 plt.figure(figsize=(12, 6)) segment_counts = rfm_df[\u0026#39;user_segment\u0026#39;].value_counts() segment_counts.plot(kind=\u0026#39;barh\u0026#39;, color=\u0026#39;steelblue\u0026#39;) plt.title(\u0026#39;RFM用户分群分布\u0026#39;) plt.xlabel(\u0026#39;用户数\u0026#39;) plt.ylabel(\u0026#39;用户分群\u0026#39;) plt.savefig(\u0026#39;rfm_segment_distribution.png\u0026#39;, dpi=150, bbox_inches=\u0026#39;tight\u0026#39;) plt.close() # 4. RFM评分热力图 plt.figure(figsize=(10, 8)) rfm_pivot = rfm_df.groupby([\u0026#39;r_score\u0026#39;, \u0026#39;f_score\u0026#39;])[\u0026#39;user_id\u0026#39;].count().unstack(fill_value=0) sns.heatmap(rfm_pivot, annot=True, fmt=\u0026#39;d\u0026#39;, cmap=\u0026#39;YlOrRd\u0026#39;) plt.title(\u0026#39;RFM评分分布热力图\u0026#39;) plt.xlabel(\u0026#39;F评分\u0026#39;) plt.ylabel(\u0026#39;R评分\u0026#39;) plt.savefig(\u0026#39;rfm_heatmap.png\u0026#39;, dpi=150, bbox_inches=\u0026#39;tight\u0026#39;) plt.close() # 5. 购物频率与购买力关系 plt.figure(figsize=(12, 6)) cross_tab = pd.crosstab(profile_df[\u0026#39;shopping_frequency\u0026#39;], profile_df[\u0026#39;purchase_power\u0026#39;]) cross_tab.plot(kind=\u0026#39;bar\u0026#39;, stacked=True) plt.title(\u0026#39;购物频率与购买力关系\u0026#39;) plt.xlabel(\u0026#39;购物频率\u0026#39;) plt.ylabel(\u0026#39;用户数\u0026#39;) plt.legend(title=\u0026#39;购买力\u0026#39;) plt.savefig(\u0026#39;frequency_power_relation.png\u0026#39;, dpi=150, bbox_inches=\u0026#39;tight\u0026#39;) plt.close() print(\u0026#34;可视化图表生成完成\u0026#34;) finally: conn.close()\r项目实现步骤\r#\r1. 环境搭建\r#\r# 1. 安装基础组件（以CentOS为例） # JDK 11 sudo yum install java-11-openjdk java-11-openjdk-devel export JAVA_HOME=/usr/lib/jvm/java-11 # Scala 2.13.8 wget https://downloads.lightbend.com/scala/2.13.8/scala-2.13.8.tgz tar -xzf scala-2.13.8.tgz -C /usr/local/ export SCALA_HOME=/usr/local/scala-2.13.8 # 2. 安装大数据组件 # Hadoop 3.3.6, Spark 3.5.8, Kafka 3.7.0, HBase 2.5.x, Hive 3.1.x # （参考各组件官方安装文档） # 3. 安装 Redis 7.0+ sudo yum install redis sudo systemctl start redis sudo systemctl enable redis # 4. 安装 MySQL 8.0 sudo yum install mysql-server sudo systemctl start mysqld sudo systemctl enable mysqld # 5. 创建数据库和表 mysql -u root -p \u0026lt; scripts/init_tables.sql # 6. 配置环境变量（敏感信息） export MYSQL_PASSWORD=\u0026#34;your_secure_password\u0026#34; export REDIS_PASSWORD=\u0026#34;your_redis_password\u0026#34;\r2. 项目构建\r#\r# 编译打包 mvn clean package -DskipTests # 提交 Spark 作业 spark-submit \\ --class com.ecommerce.profile.builder.UserProfileBuilder \\ --master yarn \\ --deploy-mode cluster \\ --conf spark.dynamicAllocation.enabled=true \\ target/user-profile-1.0.0.jar\r3. 数据采集\r#\r启动Kafka服务和主题 运行数据采集程序，生成模拟数据 验证数据是否正确发送到Kafka 4. 数据存储\r#\r运行数据存储程序，将数据写入HBase和MySQL 验证数据是否正确存储 配置数据备份策略 5. 用户画像构建\r#\r运行基础画像构建程序（UserProfileBuilder） 运行RFM分析程序（RFMAnalyzer） 运行用户分群程序（UserSegmentation） 验证画像数据的准确性 6. 应用开发\r#\r实现个性化推荐系统（PersonalizedRecommendation） 实现精准营销系统（TargetedMarketing） 启动用户画像服务（UserProfileService） 7. 可视化展示\r#\r配置Grafana数据源 创建仪表板和图表 验证可视化效果 项目扩展\r#\r实时用户画像：基于实时数据流，实现实时用户画像更新 跨渠道数据整合：整合线上线下数据，构建全渠道用户画像 深度学习模型：使用深度学习模型提升用户画像的准确性 隐私保护：实现差分隐私技术，保护用户隐私 多语言支持：支持多语言用户画像构建和分析 标签血缘追踪：记录标签的计算逻辑和数据来源，支持标签溯源 A/B测试平台：基于用户画像进行A/B测试，验证营销策略效果 项目总结\r#\r本项目实现了一个完整的企业级电商用户画像系统，涵盖了数据采集、存储、处理、分析和应用的全流程。通过整合Spark 3.5.8、Hadoop、HBase、Kafka 3.7.0、Redis 7.0+、MySQL 8.0等大数据技术，系统能够构建精准的用户画像，为电商平台的个性化推荐、精准营销和用户体验优化提供数据支持。\n系统具有以下特点：\n多维度画像：从人口统计学、行为、兴趣、RFM等多个维度构建用户画像 实时性：使用Kafka和Spark Streaming实现实时数据处理 智能分析：使用RFM模型和K-Means机器学习算法进行用户分群和行为预测 分层存储：Redis热数据 + HBase标签数据 + MySQL结构化数据 + Hive历史数据 配置外部化：所有配置通过Typesafe Config管理，敏感信息通过环境变量注入 安全可靠：消除硬编码密码，Redis连接池管理，多级降级方案 资源管理：SparkSession使用try-catch-finally确保释放，JedisPool连接池管理 服务化：提供多级缓存查询服务，支持降级和批量查询 通过本项目，我们掌握了企业级用户画像系统的设计和实现方法，包括配置管理、数据访问层封装、多级缓存策略、降级方案等工程实践，为未来的大数据项目开发打下了坚实的基础。\n","date":"May 29, 2026","externalUrl":null,"permalink":"/posts/%E7%BB%BC%E5%90%88%E9%A1%B9%E7%9B%AE-2-%E7%94%B5%E5%95%86%E7%94%A8%E6%88%B7%E7%94%BB%E5%83%8F%E7%B3%BB%E7%BB%9F/","section":"Posts","summary":"电商用户画像系统\r#\r项目概述\r#\r电商用户画像系统是一个基于大数据技术的用户分析平台，旨在通过收集、分析用户的行为数据，构建精准的用户画像，为电商平台的个性化推荐、精准营销和用户体验优化提供数据支持。本系统整合了Spark、Hadoop、Hive、HBase、Kafka等大数据技术，实现了从数据采集到画像构建、应用的完整流程。\n技术栈版本\r#\r组件 版本 说明 Spark 3.5.8 分布式计算引擎 Scala 2.13.8 开发语言 JDK 11 Java运行环境 Kafka 3.7.0 消息队列 Redis 7.0+ 热数据缓存 MySQL 8.0 结构化数据存储 HBase 2.5.x 标签数据存储 Hive 3.1.x 数据仓库 Hadoop 3.3.x 分布式存储 知识点讲解\r#\r知识点1：用户画像概述\r#\r什么是用户画像\r#\r用户画像（User Profile）是将用户的多维度信息进行标签化、结构化描述的一种数据模型。它通过对用户的基础属性、行为特征、消费偏好等信息进行抽象和归纳，形成对用户的全面刻画。简单来说，用户画像就是用数据为用户\"画一张像\"。\n","title":"电商用户画像系统","type":"posts"},{"content":"\r仿字节跳动数据大屏可视化系统\r#\r项目简介\r#\r字节跳动数据大屏可视化系统是一个企业级实时数据监控大屏，模拟抖音/头条场景下的用户增长与内容消费数据监控。系统采用前后端分离架构，通过 WebSocket 实时推送数据，支持多维度图表展示和 AI 智能图表配置生成。\n核心亮点\r#\r实时数据推送：基于 STOMP over WebSocket，实现秒级数据刷新（在线人数 2s，DAU 趋势 5s，排行榜/饼图 10s） 7 种可视化图表：覆盖 DAU 趋势、用户分布、内容结构、实时在线、热点排行、留存漏斗、POI 竞品分析 AI 图表生成：集成 DeepSeek API，通过 SSE 流式返回智能图表配置，无 API Key 时自动 Mock 回退 三级权限体系：匿名浏览 → 注册待审批 → 普通用户(USER) → 管理员(ADMIN)，JWT 无状态认证 响应式设计：支持移动端、平板、桌面端的自适应布局，F 型视觉动线 双数据库适配：开发环境 H2 内存数据库，生产环境 MySQL 8.0，无缝切换 功能特性\r#\r数据大屏看板\r#\r编号 图表模块 说明 更新频率 01 DAU 趋势图 日活跃用户数随时间变化趋势，支持时间范围缩放 5s 02 实时在线统计 当前在线用户数量，数字滚动动画效果 2s 03 用户分布地图 中国地图热力图，展示各区域用户密度分布 10s 04 内容消费结构 饼图/环形图，按视频、图文、直播等类型展示消费占比 10s 05 热点内容排行 TOP10 榜单式展示最热门内容，含标题、播放量、增长趋势 10s 06 留存漏斗分析 从新增到次日/7 日/30 日留存转化的漏斗图 — 07 POI 竞品分析 地理位置相关的竞品数据对比分析 — AI 智能图表\r#\r自然语言输入：通过聊天面板描述需求，AI 自动生成 ECharts 图表配置 SSE 流式返回：基于 Server-Sent Events，实时展示 AI 生成过程 图表实时预览：生成的图表配置即时渲染预览 Mock 回退：未配置 DeepSeek API Key 时，自动使用预设模板数据 用户管理\r#\r注册与登录：用户名 + 密码 + 邮箱注册，JWT Token 认证（24h 有效期） 审批工作流：新用户注册后默认 PENDING 状态，需管理员审批通过后方可访问大屏 角色管理：管理员可在后台查看用户列表、审批/拒绝用户、删除用户 路由级权限控制：前端路由守卫 + 后端方法级安全注解双重保障 快速开始\r#\r环境要求\r#\r软件 最低版本 Node.js 18.x Java JDK 17+ Maven 3.8+ MySQL 8.0（生产环境） 1. 克隆项目\r#\rgit clone \u0026lt;repo-url\u0026gt; cd bigscreen-dev\r2. 启动后端\r#\rcd backend # 设置环境变量（Windows PowerShell） $env:JWT_SECRET = \u0026#34;your-secret-key-at-least-256-bits\u0026#34; # 可选：配置 DeepSeek API Key（不配置则 AI 功能使用 Mock 数据） $env:DEEPSEEK_API_KEY = \u0026#34;sk-your-api-key\u0026#34; # 启动（默认 H2 内存数据库） mvn spring-boot:run\r后端启动后监听 http://localhost:8080，H2 控制台地址 http://localhost:8080/h2-console。\n3. 启动前端\r#\rcd frontend # 安装依赖 npm install # 启动开发服务器 npm run dev\r前端启动后访问 http://localhost:3000。\n4. 默认账户\r#\r角色 用户名 密码 说明 管理员 admin admin123 系统初始化自动创建，拥有全部权限 项目结构\r#\rbigscreen-dev/ ├── backend/ # Spring Boot 后端 │ ├── pom.xml # Maven 依赖配置 │ └── src/main/ │ ├── java/com/bigscreen/ │ │ ├── BigScreenApplication.java # 启动入口 │ │ ├── config/ │ │ │ ├── SecurityConfig.java # Spring Security 无状态 JWT 配置 │ │ │ ├── JwtAuthFilter.java # JWT 认证过滤器 │ │ │ ├── WebSocketConfig.java # STOMP 消息代理配置 │ │ │ ├── CorsConfig.java # CORS 跨域配置 │ │ │ ├── DataInitializer.java # 初始化默认管理员账户 │ │ │ └── MyMetaObjectHandler.java # MyBatis-Plus 自动填充 │ │ ├── controller/ │ │ │ ├── AuthController.java # 认证接口（注册/登录/当前用户） │ │ │ ├── AdminController.java # 管理接口（用户审批/删除） │ │ │ └── AiController.java # AI 接口（SSE 流式图表生成） │ │ ├── service/ │ │ │ ├── AuthService.java # 认证业务逻辑 │ │ │ ├── UserService.java # 用户 CRUD + 审批 │ │ │ ├── WebSocketService.java # WebSocket 定时推送 Mock 数据 │ │ │ └── AiService.java # DeepSeek API 代理 + Mock 回退 │ │ ├── entity/ │ │ │ └── User.java # 用户实体（t_user 表映射） │ │ ├── dto/ │ │ │ ├── LoginRequest.java # 登录请求 │ │ │ ├── RegisterRequest.java # 注册请求 │ │ │ ├── LoginResponse.java # 登录响应 │ │ │ ├── UserInfo.java # 用户信息 │ │ │ └── ChartDataMessage.java # WebSocket 数据消息体 │ │ ├── mapper/ │ │ │ └── UserMapper.java # MyBatis-Plus BaseMapper │ │ └── util/ │ │ ├── JwtUtil.java # JWT 令牌工具类 │ │ └── MockDataGenerator.java # 随机模拟数据生成器 │ └── resources/ │ ├── application.yml # 应用配置 │ └── schema.sql # 数据库初始化 DDL │ ├── frontend/ # Vue 3 前端 │ ├── package.json │ ├── vite.config.ts │ ├── tsconfig.json │ ├── index.html │ └── src/ │ ├── main.ts # 应用入口 │ ├── App.vue # 根组件 │ ├── api/ │ │ ├── index.ts # Axios 实例 + 拦截器 │ │ ├── admin.ts # 管理员 API │ │ └── ai.ts # AI SSE 流式请求 │ ├── stores/ │ │ ├── auth.ts # 认证状态（Pinia） │ │ ├── dashboard.ts # 大屏数据状态 │ │ └── theme.ts # 主题状态 │ ├── composables/ │ │ ├── useBreakpoint.ts # 响应式断点检测 │ │ ├── useChartInit.ts # ECharts 实例管理 + 自适应 │ │ └── useWebSocket.ts # STOMP WebSocket 连接管理 │ ├── router/ │ │ └── index.ts # 路由表 + 导航守卫 │ ├── types/ │ │ └── index.ts # TypeScript 类型定义 │ ├── styles/ │ │ ├── variables.css # CSS 自定义属性 │ │ └── auth-common.css # 认证页公共样式 │ ├── views/ │ │ ├── LandingPage.vue # 落地页（公开） │ │ ├── LoginPage.vue # 登录页 │ │ ├── RegisterPage.vue # 注册页 │ │ ├── PendingPage.vue # 待审批提示页 │ │ ├── DashboardPage.vue # 数据大屏主页 │ │ └── AdminPage.vue # 管理员后台 │ ├── components/ │ │ ├── charts/ │ │ │ ├── DauTrendChart.vue # DAU 趋势折线图 │ │ │ ├── UserMapChart.vue # 用户分布地图 │ │ │ ├── ContentPieChart.vue # 内容消费饼图 │ │ │ ├── OnlineCounter.vue # 实时在线计数器 │ │ │ ├── HotRanking.vue # 热点内容排行 │ │ │ ├── RetentionFunnel.vue # 留存漏斗图 │ │ │ ├── PoiAnalysis.vue # POI 竞品分析 │ │ │ └── PlaceholderChart.vue # 占位图表 │ │ ├── ai/ │ │ │ ├── AiChatPanel.vue # AI 对话抽屉面板 │ │ │ └── ChartPreview.vue # AI 生成的图表预览 │ │ └── layout/ │ │ ├── DashboardLayout.vue # 大屏 F 型栅格布局 │ │ ├── TopBar.vue # 顶部导航栏 │ │ └── ChartCard.vue # 通用图表卡片容器 │ └── utils/ │ └── index.ts # 通用工具函数 │ └── docs/ # 设计文档 └── superpowers/ # 需求与设计规范\r系统架构\r#\r┌──────────────────────────────────────────────────────────────────┐ │ Browser │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ Vue 3 SPA (Vite) │ │ │ │ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ │ │ │ │ Pinia │ │ ECharts │ │ Element Plus │ │ │ │ │ │ 状态管理 │ │ 图表渲染 │ │ UI 组件库 │ │ │ │ │ └──────────┘ └──────────┘ └───────────────┘ │ │ │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ │ │ composables (组合式函数) │ │ │ │ │ │ useWebSocket useChartInit useBreakpoint │ │ │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ │ └─────────┼──────────────┼────────────────────────────────────┘ │ │ │ │ │ │ REST API WebSocket │ │ (Axios) (STOMP/SockJS) │ └────────────┬──────────────┬───────────────────────────────────────┘ │ │ ▼ ▼ ┌─────────────────────────────────────────────────────┐ │ Spring Boot 3.2.5 │ │ │ │ ┌──────────────┐ ┌───────────────┐ ┌───────────┐ │ │ │ Spring │ │ WebSocket │ │ CORS │ │ │ │ Security + │ │ STOMP Broker │ │ Config │ │ │ │ JWT Filter │ │ /topic/data │ │ │ │ │ └──────┬───────┘ └───────┬───────┘ └───────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌──────────────────────────────────────┐ │ │ │ Controller 层 │ │ │ │ AuthController AdminController │ │ │ │ AiController │ │ │ └────────────────┬─────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────┐ │ │ │ Service 层 │ │ │ │ AuthService UserService │ │ │ │ WebSocketService AiService │ │ │ └────┬──────────────┬──────────────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌──────────┐ ┌──────────────────┐ │ │ │ MyBatis- │ │ OkHttp Client │ │ │ │ Plus │ │ (DeepSeek SSE) │ │ │ └────┬─────┘ └────────┬─────────┘ │ │ │ │ │ └───────┼─────────────────┼─────────────────────────────┘ │ │ ▼ ▼ ┌──────────────┐ ┌─────────────────┐ │ H2 / MySQL │ │ DeepSeek API │ │ 数据库 │ │ (AI 图表生成) │ └──────────────┘ └─────────────────┘\r数据流说明\r#\rREST API 通信：前端 Axios → /api/* 接口 → Controller → Service → Mapper → 数据库（认证、管理操作、AI 生成） WebSocket 推送：服务端 @Scheduled 定时生成 → SimpMessagingTemplate 广播 → /topic/data → 前端 STOMP.js 订阅 → Pinia Store → ECharts 渲染 SSE 流式：前端发起 → /api/ai/generate-chart → AiService 代理 DeepSeek API → SseEmitter 逐字流式返回 → 前端 EventSource 接收 → 图表预览 技术栈\r#\r前端\r#\r技术 版本 用途 Vue 3.5 UI 框架（Composition API + \u0026lt;script setup\u0026gt;） Vite 8 构建工具 TypeScript 6 类型安全 Pinia 2 状态管理 Vue Router 4 路由管理（含导航守卫） ECharts 6 数据可视化图表 Element Plus 2 UI 组件库 Axios — HTTP 客户端 STOMP.js — WebSocket STOMP 客户端 SockJS — WebSocket 降级兼容 后端\r#\r技术 版本 用途 Spring Boot 3.2.5 应用框架 Spring Security 6 认证与授权 jjwt 0.12.5 JWT 令牌生成与验证 MyBatis-Plus 3.5.6 ORM 持久层 H2 — 开发环境内存数据库 MySQL 8.0 生产环境关系型数据库 BCrypt — 密码加密 OkHttp 4 HTTP 客户端（DeepSeek API 调用） Lombok — 样板代码精简 Spring WebSocket — STOMP 消息代理 API 接口\r#\rREST API（共 8 个端点）\r#\r认证接口 — /api/auth\r#\r方法 路径 说明 认证 POST /api/auth/register 用户注册 否 POST /api/auth/login 用户登录，返回 JWT Token 否 GET /api/auth/me 获取当前登录用户信息 是 管理接口 — /api/admin\r#\r方法 路径 说明 角色 GET /api/admin/users 获取所有用户列表 ADMIN PUT /api/admin/users/{id}/approve 审批通过用户 ADMIN PUT /api/admin/users/{id}/reject 拒绝用户注册 ADMIN DELETE /api/admin/users/{id} 删除用户 ADMIN AI 接口 — /api/ai\r#\r方法 路径 说明 认证 POST /api/ai/generate-chart AI 图表配置生成（SSE 流式） 否 WebSocket 端点\r#\r端点 协议 目的地 说明 频率 /ws/data STOMP + SockJS /topic/data 实时数据广播 推送频率见下表 消息类型\r#\r消息类型 内容 推送频率 DATA_UPDATE DAU 趋势数据、内容饼图数据 5s / 10s ONLINE_COUNT 实时在线人数 2s RANKING_UPDATE 热点内容排行 TOP10 10s 环境变量\r#\r变量名 是否必需 默认值 说明 JWT_SECRET 必需 无 JWT 签名密钥，建议 256 位以上随机字符串 DEEPSEEK_API_KEY 可选 无 DeepSeek API Key，未配置时 AI 功能自动回退到 Mock 模式 配置示例\r#\rWindows PowerShell:\n$env:JWT_SECRET = \u0026#34;my-super-secret-key-at-least-256-bits-long\u0026#34; $env:DEEPSEEK_API_KEY = \u0026#34;sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u0026#34;\rLinux / macOS (bash/zsh):\nexport JWT_SECRET=\u0026#34;my-super-secret-key-at-least-256-bits-long\u0026#34; export DEEPSEEK_API_KEY=\u0026#34;sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u0026#34;\rIDE 配置（IntelliJ IDEA）： 在 Run Configuration → Environment Variables 中添加上述变量。\n截图说明\r#\r数据大屏主页\r#\r大屏采用深色科技风主题，F 型视觉动线布局：\n顶部栏：显示系统名称、当前时间、实时在线人数滚动数字，以及用户头像/AI 助手按钮 左上区域：DAU 趋势折线图，展示过去 24 小时日活跃用户变化曲线，支持鼠标悬停查看具体数值 中上区域：用户分布中国地图，以热力图颜色深浅展示各省市用户密度 右上区域：内容消费结构饼图，按视频/图文/直播/音频四大类展示消费占比百分比 左下区域：热点内容排行 TOP10 榜单，每条内容展示标题、播放量、24h 增长趋势箭头 中下区域：留存漏斗分析，从新增用户逐步收窄至次日、7 日、30 日留存率 右下区域：POI 竞品分析，双维度对比展示本平台与竞品在各 POI 维度的差异 AI 对话面板\r#\r点击右上角 AI 助手图标，从右侧滑出抽屉面板：\n在输入框输入图表需求（如\u0026quot;帮我生成一张最近 7 天的用户增长柱状图\u0026quot;） AI 通过 SSE 逐字流式返回 ECharts 图表配置 JSON 右侧实时预览渲染结果 可以将生成的图表添加到主大屏中替换对应位置图表 管理员后台\r#\r列表式展示所有注册用户：用户名、邮箱、角色、审批状态（PENDING / APPROVED / REJECTED）、注册时间，支持审批通过、拒绝、删除操作。\n许可证\r#\r本项目采用 MIT License 开源协议。\n贡献指南\r#\r欢迎提交 Issue 和 Pull Request。在提交代码前，请确保：\n遵循项目现有的代码风格 所有新功能都有对应的类型定义 提交信息使用中文，格式：\u0026lt;type\u0026gt;: \u0026lt;描述\u0026gt; 本系统为字节跳动风格的企业数据大屏教学演示项目。\n","date":"May 29, 2026","externalUrl":null,"permalink":"/posts/%E4%BB%BF%E5%AD%97%E8%8A%82%E8%B7%B3%E5%8A%A8%E6%95%B0%E6%8D%AE%E5%A4%A7%E5%B1%8F%E5%8F%AF%E8%A7%86%E5%8C%96%E7%B3%BB%E7%BB%9F/","section":"Posts","summary":"仿字节跳动数据大屏可视化系统\r#\r项目简介\r#\r字节跳动数据大屏可视化系统是一个企业级实时数据监控大屏，模拟抖音/头条场景下的用户增长与内容消费数据监控。系统采用前后端分离架构，通过 WebSocket 实时推送数据，支持多维度图表展示和 AI 智能图表配置生成。\n核心亮点\r#\r实时数据推送：基于 STOMP over WebSocket，实现秒级数据刷新（在线人数 2s，DAU 趋势 5s，排行榜/饼图 10s） 7 种可视化图表：覆盖 DAU 趋势、用户分布、内容结构、实时在线、热点排行、留存漏斗、POI 竞品分析 AI 图表生成：集成 DeepSeek API，通过 SSE 流式返回智能图表配置，无 API Key 时自动 Mock 回退 三级权限体系：匿名浏览 → 注册待审批 → 普通用户(USER) → 管理员(ADMIN)，JWT 无状态认证 响应式设计：支持移动端、平板、桌面端的自适应布局，F 型视觉动线 双数据库适配：开发环境 H2 内存数据库，生产环境 MySQL 8.0，无缝切换 功能特性\r#\r数据大屏看板\r#\r编号 图表模块 说明 更新频率 01 DAU 趋势图 日活跃用户数随时间变化趋势，支持时间范围缩放 5s 02 实时在线统计 当前在线用户数量，数字滚动动画效果 2s 03 用户分布地图 中国地图热力图，展示各区域用户密度分布 10s 04 内容消费结构 饼图/环形图，按视频、图文、直播等类型展示消费占比 10s 05 热点内容排行 TOP10 榜单式展示最热门内容，含标题、播放量、增长趋势 10s 06 留存漏斗分析 从新增到次日/7 日/30 日留存转化的漏斗图 — 07 POI 竞品分析 地理位置相关的竞品数据对比分析 — AI 智能图表\r#\r自然语言输入：通过聊天面板描述需求，AI 自动生成 ECharts 图表配置 SSE 流式返回：基于 Server-Sent Events，实时展示 AI 生成过程 图表实时预览：生成的图表配置即时渲染预览 Mock 回退：未配置 DeepSeek API Key 时，自动使用预设模板数据 用户管理\r#\r注册与登录：用户名 + 密码 + 邮箱注册，JWT Token 认证（24h 有效期） 审批工作流：新用户注册后默认 PENDING 状态，需管理员审批通过后方可访问大屏 角色管理：管理员可在后台查看用户列表、审批/拒绝用户、删除用户 路由级权限控制：前端路由守卫 + 后端方法级安全注解双重保障 快速开始\r#\r环境要求\r#\r软件 最低版本 Node.js 18.x Java JDK 17+ Maven 3.8+ MySQL 8.0（生产环境） 1. 克隆项目\r#\rgit clone \u003crepo-url\u003e cd bigscreen-dev\r2. 启动后端\r#\rcd backend # 设置环境变量（Windows PowerShell） $env:JWT_SECRET = \"your-secret-key-at-least-256-bits\" # 可选：配置 DeepSeek API Key（不配置则 AI 功能使用 Mock 数据） $env:DEEPSEEK_API_KEY = \"sk-your-api-key\" # 启动（默认 H2 内存数据库） mvn spring-boot:run\r后端启动后监听 http://localhost:8080，H2 控制台地址 http://localhost:8080/h2-console。\n","title":"仿字节跳动数据大屏可视化系统","type":"posts"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/%E5%88%86%E5%B8%83%E5%BC%8F/","section":"Tags","summary":"","title":"分布式","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/%E9%AB%98%E5%8F%AF%E7%94%A8/","section":"Tags","summary":"","title":"高可用","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/%E9%9B%86%E7%BE%A4/","section":"Tags","summary":"","title":"集群","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/categories/%E6%95%99%E7%A8%8B/","section":"Categories","summary":"","title":"教程","type":"categories"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/","section":"李先生的树洞","summary":"","title":"李先生的树洞","type":"page"},{"content":"\r汽车大数据分析系统\r#\r项目概述\r#\r汽车大数据分析系统是一个综合性的企业级大数据解决方案，旨在通过收集、存储和分析汽车行业的各类数据，为汽车制造商、经销商和消费者提供有价值的洞察。本系统整合了Spark、Hadoop、Hive、HBase、Kafka等大数据技术，实现了从数据采集到分析、可视化的完整流程。\n项目采用Lambda架构实现批处理与流处理的协同，通过Spark Streaming微批次处理实现实时数据分析，结合HBase存储海量传感器时序数据、MySQL存储业务结构化数据，构建了完整的车联网数据处理平台。\n知识点讲解\r#\r知识点1：车联网数据特征\r#\r车联网（Internet of Vehicles, IoV）是物联网在交通领域的典型应用，其数据具有鲜明的行业特征。理解这些特征是设计合理数据处理架构的前提。\n数据类型\r#\r数据类型 来源 数据格式 采集频率 典型用途 传感器数据 发动机温度、机油压力、冷却液温度、轮胎压力、燃油液位、电池电压 结构化数值 1-100Hz 实时监控、故障预警 GPS轨迹数据 车载GPS模块 经纬度+时间戳 1-10Hz 路径规划、行为分析 OBD诊断数据 车载OBD-II接口 DTC故障码+参数 事件触发 故障诊断、维修预测 驾驶行为数据 加速度计、陀螺仪、方向盘传感器 多维向量 10-100Hz 驾驶评分、保险定价 数据特征\r#\r特征维度 描述 技术挑战 应对策略 高吞吐 单车每日可产生数GB数据，百万级车队每日PB级 存储、传输、处理压力巨大 分层存储、数据压缩、边缘计算 实时性 安全相关数据需毫秒级响应（如碰撞预警） 低延迟处理要求 流式计算、内存计算 多源异构 不同品牌、不同车型数据格式不统一 数据整合困难 统一数据模型、Schema Registry 时序性 数据带有严格的时间顺序 时序查询、窗口计算 时序数据库、窗口函数 价值密度低 大量正常数据中蕴含少量异常信息 有效信息提取 异常检测算法、采样策略 类比理解：车联网数据就像一个大型医院的监护系统——每张病床（每辆车）上的各种监护仪（传感器）持续产生心率、血压、血氧等数据（传感器数据），护士站（数据处理中心）需要实时监控所有病床的状态，一旦某项指标异常（故障预警），必须立即响应。而每天的例行查房记录（批处理分析）则用于长期健康趋势分析。\n知识点2：Lambda架构与Kappa架构\r#\r在车联网数据处理中，架构选型是系统设计的核心决策。当前业界主要有两种大数据处理架构：Lambda架构和Kappa架构。\nLambda架构\r#\r+------------------------------------------------------------------+ | Lambda 架构 | +------------------------------------------------------------------+ | | | +----------------+ +------------------+ +--------------+ | | | 批处理层 | | 速度层 | | 服务层 | | | | (Batch Layer) | | (Speed Layer) | | (Serving | | | | | | | | Layer) | | | | - HDFS原始数据 | | - Spark Streaming| | - 查询合并 | | | | - Spark批处理 | | - 实时计算 | | - MySQL | | | | - 全量重算 | | - 增量计算 | | - HBase | | | +-------+--------+ +--------+---------+ +------+-------+ | | | | | | | +-------+------+--------+ | | | | | | | | +-------v------v--------+ | | | | 数据输入层 +-----------------------+ | | | (Kafka消息队列) | | | +----------------------+ | +------------------------------------------------------------------+\r批处理层（Batch Layer）：存储全量原始数据，定期（如每天）进行全量计算，结果准确但延迟高 速度层（Speed Layer）：实时处理新到达的数据，增量计算，延迟低但结果可能不精确 服务层（Serving Layer）：合并批处理层和速度层的结果，对外提供统一查询接口 Kappa架构\r#\r+------------------------------------------------------------------+ | Kappa 架构 | +------------------------------------------------------------------+ | | | +------------------+ +------------------+ | | | 统一处理层 | | 服务层 | | | | (Stream Layer) | | (Serving Layer) | | | | | | | | | | - Kafka保留日志 | | - 实时查询 | | | | - Spark Streaming| | - MySQL | | | | - 流批一体 | | - HBase | | | +--------+---------+ +--------+---------+ | | | | | | +----------+-------------+ | | | | | +----------v------------+ | | | 数据输入层 | | | | (Kafka消息队列) | | | +----------------------+ | +------------------------------------------------------------------+\r统一处理层：所有数据处理都通过流式计算完成，通过回放Kafka历史数据实现\u0026quot;批处理\u0026quot; 服务层：直接查询流处理结果 架构对比\r#\r对比维度 Lambda架构 Kappa架构 核心思想 批流分离，各取所长 流批一体，统一处理 代码复杂度 高（需维护批处理和流处理两套代码） 低（只需维护一套流处理代码） 数据一致性 需要合并批处理和速度层结果 天然一致（单一数据源） 计算延迟 批处理延迟高（小时级），速度层延迟低（秒级） 统一低延迟（秒级） 历史重算 批处理层自动全量重算 通过Kafka日志回放重算 运维复杂度 高（两套系统运维） 低（一套系统运维） 容错能力 批处理层可修正速度层错误 依赖Kafka消息保留策略 适用场景 数据准确性要求极高、历史数据频繁重算 实时性要求高、业务逻辑相对简单 技术门槛 中等 较高（需深入理解流处理） 本项目架构选择\r#\r本项目采用Lambda架构，原因如下：\n汽车销售数据分析需要精确的历史统计（批处理层保障准确性） 传感器监控需要实时告警（速度层保障实时性） 两种数据处理逻辑差异较大，分离实现更清晰 便于初学者理解批处理与流处理的区别 知识点3：Spark Streaming微批次处理原理\r#\rSpark Streaming是Spark生态中的流处理组件，其核心设计理念是\u0026quot;微批次\u0026quot;（Micro-batch），即将实时数据流切分为小批次进行处理，兼顾了流处理的实时性和批处理的高吞吐。\nDStream（离散化流）\r#\r时间轴: t0 t1 t2 t3 t4 | | | | | 数据流: =====\u0026gt; =====\u0026gt; =====\u0026gt; =====\u0026gt; =====\u0026gt; | | | | | 切分: [批次0] [批次1] [批次2] [批次3] [批次4] | | | | | v v v v v RDD-0 RDD-1 RDD-2 RDD-3 RDD-4\rDStream（Discretized Stream）是Spark Streaming的核心抽象，它将连续的数据流按照时间间隔（batch interval）切分为一系列连续的RDD：\nDStream = RDD的时间序列：每个时间间隔内的数据形成一个RDD 批次间隔（Batch Interval）：由spark.streaming.batchDuration控制，通常为500ms到数秒 依赖关系：DStream之间的转换操作会生成RDD之间的依赖关系 窗口计算\r#\r窗口计算是流处理中的核心能力，它允许在滑动的时间窗口内进行聚合计算：\n时间轴: t0 t1 t2 t3 t4 t5 t6 t7 数据: [A] [B] [C] [D] [E] [F] [G] [H] 窗口长度=3个批次, 滑动步长=2个批次: 窗口1: [A] [B] [C] -\u0026gt; 计算结果1 窗口2: [C] [D] [E] -\u0026gt; 计算结果2 窗口3: [E] [F] [G] -\u0026gt; 计算结果3\r参数 含义 示例 窗口长度（Window Duration） 窗口覆盖的时间范围 30秒 滑动步长（Slide Duration） 窗口每次移动的时间间隔 10秒 批次间隔（Batch Interval） 数据切分的最小单位 5秒 注意：窗口长度和滑动步长都必须是批次间隔的整数倍。\n常用窗口操作：\n操作 说明 使用场景 window() 返回窗口内的原始DStream 自定义窗口聚合 reduceByWindow() 窗口内聚合 窗口内求和/计数 reduceByKeyAndWindow() 按Key分组后窗口聚合 按车型统计窗口内销量 countByWindow() 窗口内计数 窗口内消息总数 countByValueAndWindow() 按值分组后窗口计数 窗口内各类型消息数量 背压机制（Backpressure）\r#\r当数据生产速度超过消费速度时，系统需要背压机制来防止数据积压导致崩溃：\n生产者 (Kafka) 消费者 (Spark Streaming) | | | 数据速率: 10000条/秒 | 处理速率: 5000条/秒 | ---------\u0026gt; | \u0026lt;--------- | | | 背压控制器 | | +----------------------+ | | | PID控制器算法: | | | | 1. 采集处理速率 | | | | 2. 计算速率差值 | | | | 3. 动态调整消费速率 | | | +----------------------+ | | | | 调整后: 5000条/秒 | 处理速率: 5000条/秒 | ---------\u0026gt; | \u0026lt;---------\r背压机制的关键配置：\n配置项 默认值 说明 spark.streaming.backpressure.enabled false 是否启用背压 spark.streaming.backpressure.initialRate 无 初始最大接收速率 spark.streaming.receiver.maxRate 无 每个接收器的最大接收速率（启用背压时的上限） spark.streaming.kafka.maxRatePerPartition 无 每个Kafka分区的最大读取速率 最佳实践：在生产环境中，建议始终启用背压机制（spark.streaming.backpressure.enabled=true），并设置合理的maxRatePerPartition作为速率上限，防止系统在突发流量时过载。\n知识点4：汽车数据分析指标体系\r#\r构建完善的指标体系是数据分析的基础。汽车行业的数据分析指标体系通常从四个维度展开：\n销量分析指标\r#\r指标类别 具体指标 计算方式 分析维度 销量规模 总销量 SUM(销售数量) 品牌/车型/地区/时间 销量规模 同比增长率 (本期-同期)/同期 x 100% 品牌/车型/地区 销量规模 环比增长率 (本期-上期)/上期 x 100% 品牌/车型/地区 销量结构 品牌市场份额 品牌销量/总销量 x 100% 品牌/地区/时间 销量结构 车型占比 车型销量/品牌销量 x 100% 品牌/车型 销量趋势 移动平均 近N期销量均值 品牌/车型/时间 价格分析指标\r#\r指标类别 具体指标 计算方式 分析维度 价格水平 平均成交价 AVG(成交价格) 品牌/车型/地区 价格水平 中位数价格 MEDIAN(成交价格) 品牌/车型/地区 价格波动 价格离散度 STDDEV(成交价格) 品牌/车型 价格波动 优惠幅度 (指导价-成交价)/指导价 x 100% 品牌/车型/地区 价格弹性 价格弹性系数 销量变化率/价格变化率 品牌/车型 用户画像指标\r#\r指标类别 具体指标 数据来源 分析维度 人口统计 年龄分布 客户信息 品牌/车型 人口统计 性别比例 客户信息 品牌/车型 经济能力 收入水平 客户信息 品牌/车型 消费偏好 支付方式偏好 交易记录 品牌/地区 消费偏好 配置偏好 交易记录 车型 行为特征 购车周期 交易记录 品牌/客户类型 竞争力分析指标\r#\r指标类别 具体指标 计算方式 分析维度 市场地位 市场占有率 品牌销量/总销量 x 100% 品牌/地区/时间 市场地位 市场排名 RANK(品牌销量) 品牌/地区 产品竞争力 性价比指数 配置得分/成交价格 品牌/车型 产品竞争力 口碑指数 评分加权平均 品牌/车型 渠道能力 经销商覆盖率 有销量的经销商数/总经销商数 品牌/地区 系统架构\r#\r技术栈版本\r#\r组件 版本 用途 JDK 11 运行环境 Scala 2.13.8 开发语言 Spark 3.5.8 数据处理引擎 Kafka 3.7.0 消息队列 HBase 2.5.9 时序数据存储 Hadoop 3.3.6 分布式存储 Hive 3.1.3 数据仓库 MySQL 8.0 业务数据存储 Typesafe Config 1.4.3 配置管理 系统架构图\r#\r+------------------+ +------------------+ +------------------+ +------------------+ | 数据采集层 | --\u0026gt; | 数据存储层 | --\u0026gt; | 数据处理层 | --\u0026gt; | 可视化层 | +------------------+ +------------------+ +------------------+ +------------------+ | - Kafka 3.7.0 | | - HDFS (Hadoop) | | - Spark Core | | - Grafana | | - Flume | | - HBase 2.5.9 | | - Spark SQL | | - Tableau | | - OBD数据网关 | | - MySQL 8.0 | | - Spark Streaming| | - 自研Dashboard | +------------------+ +------------------+ | - Hive 3.1.3 | +------------------+ +------------------+ | +------------------+ | 基础设施层 | +------------------+ | - ConfigManager | | - DataAccessLayer| | - 降级策略 | +------------------+\r项目工程配置\r#\rpom.xml\r#\r\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;com.sparklearning\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;auto-bigdata-analysis\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.0\u0026lt;/version\u0026gt; \u0026lt;packaging\u0026gt;jar\u0026lt;/packaging\u0026gt; \u0026lt;name\u0026gt;Auto BigData Analysis System\u0026lt;/name\u0026gt; \u0026lt;description\u0026gt;汽车大数据分析系统 - 基于Spark的企业级车联网数据处理平台\u0026lt;/description\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;java.version\u0026gt;11\u0026lt;/java.version\u0026gt; \u0026lt;scala.version\u0026gt;2.13.8\u0026lt;/scala.version\u0026gt; \u0026lt;scala.binary.version\u0026gt;2.13\u0026lt;/scala.binary.version\u0026gt; \u0026lt;spark.version\u0026gt;3.5.8\u0026lt;/spark.version\u0026gt; \u0026lt;kafka.version\u0026gt;3.7.0\u0026lt;/kafka.version\u0026gt; \u0026lt;hbase.version\u0026gt;2.5.9\u0026lt;/hbase.version\u0026gt; \u0026lt;hadoop.version\u0026gt;3.3.6\u0026lt;/hadoop.version\u0026gt; \u0026lt;hive.version\u0026gt;3.1.3\u0026lt;/hive.version\u0026gt; \u0026lt;mysql.connector.version\u0026gt;8.0.33\u0026lt;/mysql.connector.version\u0026gt; \u0026lt;typesafe.config.version\u0026gt;1.4.3\u0026lt;/typesafe.config.version\u0026gt; \u0026lt;slf4j.version\u0026gt;1.7.36\u0026lt;/slf4j.version\u0026gt; \u0026lt;log4j.version\u0026gt;2.17.2\u0026lt;/log4j.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- Scala 标准库 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.scala-lang\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;scala-library\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${scala.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spark Core --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.spark\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spark-core_${scala.binary.version}\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spark.version}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spark SQL --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.spark\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spark-sql_${scala.binary.version}\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spark.version}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spark Streaming --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.spark\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spark-streaming_${scala.binary.version}\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spark.version}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spark Streaming Kafka Connector --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.spark\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spark-streaming-kafka-0-10_${scala.binary.version}\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spark.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spark SQL Kafka Connector --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.spark\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spark-sql-kafka-0-10_${scala.binary.version}\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spark.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Kafka Client --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.kafka\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;kafka-clients\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${kafka.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- HBase Client --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.hbase\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hbase-client\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${hbase.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- HBase MapReduce (用于Spark与HBase集成) --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.hbase\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hbase-mapreduce\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${hbase.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Hadoop Common --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.hadoop\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hadoop-common\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${hadoop.version}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Hadoop HDFS --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.hadoop\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hadoop-hdfs\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${hadoop.version}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Hive JDBC --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.hive\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hive-jdbc\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${hive.version}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- MySQL Connector --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-j\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${mysql.connector.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Typesafe Config --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.typesafe\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;config\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${typesafe.config.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- SLF4J API --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.slf4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;slf4j-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${slf4j.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Log4j2 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.logging.log4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;log4j-core\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${log4j.version}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;runtime\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.logging.log4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;log4j-slf4j-impl\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${log4j.version}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;runtime\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;!-- Scala Maven Plugin --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;net.alchim31.maven\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;scala-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.8.1\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;scala-compile-first\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;process-resources\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;add-source\u0026lt;/goal\u0026gt; \u0026lt;goal\u0026gt;compile\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;scala-test-compile\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;process-test-resources\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;testCompile\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;scalaVersion\u0026gt;${scala.version}\u0026lt;/scalaVersion\u0026gt; \u0026lt;args\u0026gt; \u0026lt;arg\u0026gt;-target:jvm-11\u0026lt;/arg\u0026gt; \u0026lt;arg\u0026gt;-deprecation\u0026lt;/arg\u0026gt; \u0026lt;arg\u0026gt;-feature\u0026lt;/arg\u0026gt; \u0026lt;arg\u0026gt;-unchecked\u0026lt;/arg\u0026gt; \u0026lt;/args\u0026gt; \u0026lt;jvmArgs\u0026gt; \u0026lt;jvmArg\u0026gt;-Xms128m\u0026lt;/jvmArg\u0026gt; \u0026lt;jvmArg\u0026gt;-Xmx1024m\u0026lt;/jvmArg\u0026gt; \u0026lt;/jvmArgs\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;!-- Maven Compiler Plugin --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-compiler-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.11.0\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;source\u0026gt;${java.version}\u0026lt;/source\u0026gt; \u0026lt;target\u0026gt;${java.version}\u0026lt;/target\u0026gt; \u0026lt;encoding\u0026gt;UTF-8\u0026lt;/encoding\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;!-- Maven Shade Plugin (构建Fat Jar) --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-shade-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.1\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;phase\u0026gt;package\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;shade\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;filters\u0026gt; \u0026lt;filter\u0026gt; \u0026lt;artifact\u0026gt;*:*\u0026lt;/artifact\u0026gt; \u0026lt;excludes\u0026gt; \u0026lt;exclude\u0026gt;META-INF/*.SF\u0026lt;/exclude\u0026gt; \u0026lt;exclude\u0026gt;META-INF/*.DSA\u0026lt;/exclude\u0026gt; \u0026lt;exclude\u0026gt;META-INF/*.RSA\u0026lt;/exclude\u0026gt; \u0026lt;/excludes\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;/filters\u0026gt; \u0026lt;transformers\u0026gt; \u0026lt;transformer implementation=\u0026#34;org.apache.maven.plugins.shade.resource.ServicesResourceTransformer\u0026#34;/\u0026gt; \u0026lt;/transformers\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/project\u0026gt;\rapplication.conf 配置文件\r#\r将所有配置外部化到application.conf中，避免在代码中硬编码任何连接信息或密码。该文件放置在src/main/resources/目录下。\n# ======================================== # 汽车大数据分析系统 - 应用配置文件 # ======================================== # 注意：生产环境中密码应通过环境变量或密钥管理系统注入 # 此处仅展示配置结构，实际部署时请替换为真实值 # ---------------------------------------- # MySQL 数据库配置 # ---------------------------------------- mysql { url = \u0026#34;jdbc:mysql://localhost:3306/auto_database?useSSL=true\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;characterEncoding=utf8mb4\u0026#34; url = ${?MYSQL_URL} user = \u0026#34;auto_admin\u0026#34; user = ${?MYSQL_USER} password = \u0026#34;请通过环境变量MYSQL_PASSWORD设置\u0026#34; password = ${?MYSQL_PASSWORD} driver = \u0026#34;com.mysql.cj.jdbc.Driver\u0026#34; connection-pool { initial-size = 5 max-active = 20 max-idle = 10 min-idle = 5 max-wait-ms = 10000 } } # ---------------------------------------- # Kafka 配置 # ---------------------------------------- kafka { bootstrap-servers = \u0026#34;localhost:9092\u0026#34; bootstrap-servers = ${?KAFKA_BOOTSTRAP_SERVERS} # 消费者配置 consumer { group-id = \u0026#34;auto-bigdata-consumer-group\u0026#34; auto-offset-reset = \u0026#34;latest\u0026#34; enable-auto-commit = false max-poll-records = 500 session-timeout-ms = 30000 heartbeat-interval-ms = 10000 } # 生产者配置 producer { acks = \u0026#34;all\u0026#34; retries = 3 batch-size = 16384 linger-ms = 5 buffer-memory = 33554432 } # 主题配置 topics { sales-data = \u0026#34;sales-data\u0026#34; sensor-data = \u0026#34;sensor-data\u0026#34; gps-data = \u0026#34;gps-data\u0026#34; obd-data = \u0026#34;obd-data\u0026#34; alert-data = \u0026#34;alert-data\u0026#34; } } # ---------------------------------------- # HBase 配置 # ---------------------------------------- hbase { zookeeper-quorum = \u0026#34;localhost\u0026#34; zookeeper-quorum = ${?HBASE_ZOOKEEPER_QUORUM} zookeeper-client-port = \u0026#34;2181\u0026#34; zookeeper-client-port = ${?HBASE_ZOOKEEPER_CLIENT_PORT} rpc-timeout = 60000 client-retries-number = 3 client-operation-timeout = 1200000 # 表命名空间 tables { sensor-data = \u0026#34;sensor_data\u0026#34; gps-track = \u0026#34;gps_track\u0026#34; obd-diagnosis = \u0026#34;obd_diagnosis\u0026#34; driving-behavior = \u0026#34;driving_behavior\u0026#34; } # 列族配置 column-families { default-cf = \u0026#34;cf\u0026#34; detail-cf = \u0026#34;detail\u0026#34; } } # ---------------------------------------- # Spark 配置 # ---------------------------------------- spark { app-name = \u0026#34;AutoBigDataAnalysis\u0026#34; master = \u0026#34;local[*]\u0026#34; master = ${?SPARK_MASTER} # Spark SQL配置 sql { shuffle-partitions = 200 adaptive-execution-enabled = true warehouse-dir = \u0026#34;/user/hive/warehouse\u0026#34; } # Spark Streaming配置 streaming { batch-duration-seconds = 5 backpressure-enabled = true backpressure-initial-rate = 1000 kafka-max-rate-per-partition = 100 checkpoint-dir = \u0026#34;/tmp/spark-checkpoint/auto-bigdata\u0026#34; } # 序列化配置 serializer = \u0026#34;org.apache.spark.serializer.KryoSerializer\u0026#34; kryo-classes = [ \u0026#34;org.apache.hadoop.hbase.client.Result\u0026#34;, \u0026#34;org.apache.hadoop.hbase.io.ImmutableBytesWritable\u0026#34; ] } # ---------------------------------------- # 数据验证配置 # ---------------------------------------- validation { # 传感器值范围 sensor-ranges { \u0026#34;发动机温度\u0026#34; { min = -40.0, max = 150.0 } \u0026#34;机油压力\u0026#34; { min = 0.0, max = 10.0 } \u0026#34;冷却液温度\u0026#34; { min = -40.0, max = 130.0 } \u0026#34;轮胎压力\u0026#34; { min = 0.0, max = 5.0 } \u0026#34;燃油液位\u0026#34; { min = 0.0, max = 100.0 } \u0026#34;电池电压\u0026#34; { min = 0.0, max = 16.0 } } # 销售价格范围 sale-price { min = 10000.0 max = 10000000.0 } # 客户年龄范围 customer-age { min = 18 max = 100 } } # ---------------------------------------- # 降级策略配置 # ---------------------------------------- degradation { # 实时处理失败时的回退方案 enabled = true # 最大重试次数 max-retries = 3 # 重试间隔(毫秒) retry-interval-ms = 5000 # 降级到批处理的阈值: 连续失败次数 batch-fallback-threshold = 5 # 降级时数据暂存路径 fallback-data-path = \u0026#34;/tmp/auto-bigdata/fallback\u0026#34; # 告警通知配置 alert { enabled = true email = \u0026#34;admin@example.com\u0026#34; email = ${?ALERT_EMAIL} } } # ---------------------------------------- # 数据库表名配置 # ---------------------------------------- database { tables { sales = \u0026#34;sales\u0026#34; vehicles = \u0026#34;vehicles\u0026#34; customers = \u0026#34;customers\u0026#34; region-sales-analysis = \u0026#34;region_sales_analysis\u0026#34; payment-sales-analysis = \u0026#34;payment_sales_analysis\u0026#34; sales-trend-analysis = \u0026#34;sales_trend_analysis\u0026#34; sensor-abnormal-analysis = \u0026#34;sensor_abnormal_analysis\u0026#34; sensor-avg-analysis = \u0026#34;sensor_avg_analysis\u0026#34; vehicle-competitiveness = \u0026#34;vehicle_competitiveness\u0026#34; customer-portrait = \u0026#34;customer_portrait\u0026#34; } }\r核心基础设施代码\r#\rConfigManager.scala - 配置管理器\r#\r使用Typesafe Config库实现统一的配置管理，所有配置项从application.conf读取，敏感信息通过环境变量覆盖。\npackage com.sparklearning.auto.config import com.typesafe.config.{Config, ConfigFactory} import org.slf4j.{Logger, LoggerFactory} import scala.jdk.CollectionConverters._ import scala.util.{Try, Success, Failure} /** * 配置管理器 - 统一管理应用配置 * * 设计原则： * 1. 所有配置从application.conf读取，禁止硬编码 * 2. 敏感信息（密码等）通过环境变量覆盖 * 3. 提供类型安全的配置访问方法 * 4. 配置加载失败时提供有意义的错误信息 * * 使用方式： * val config = ConfigManager.getConfig * val mysqlUrl = ConfigManager.getMySQLUrl */ object ConfigManager { private val logger: Logger = LoggerFactory.getLogger(ConfigManager.getClass) // 加载配置，优先加载环境变量覆盖 private lazy val config: Config = { val baseConfig = ConfigFactory.load() val envConfig = ConfigFactory.systemEnvironment() // 环境变量优先级最高，然后是系统属性，最后是配置文件 ConfigFactory.load( ConfigFactory.systemProperties() .withFallback(envConfig) .withFallback(baseConfig) ).resolve() } /** * 获取完整配置对象 */ def getConfig: Config = config // ======================================== // MySQL 配置 // ======================================== def getMySQLUrl: String = config.getString(\u0026#34;mysql.url\u0026#34;) def getMySQLUser: String = config.getString(\u0026#34;mysql.user\u0026#34;) def getMySQLPassword: String = { val password = config.getString(\u0026#34;mysql.password\u0026#34;) if (password.contains(\u0026#34;请通过环境变量\u0026#34;) || password.isEmpty) { logger.warn(\u0026#34;MySQL密码未通过环境变量配置，请在MYSQL_PASSWORD环境变量中设置密码\u0026#34;) } password } def getMySQLDriver: String = config.getString(\u0026#34;mysql.driver\u0026#34;) /** * 获取MySQL连接选项（用于Spark JDBC） */ def getMySQLOptions(table: String): Map[String, String] = { require(table.nonEmpty, \u0026#34;表名不能为空\u0026#34;) Map( \u0026#34;url\u0026#34; -\u0026gt; getMySQLUrl, \u0026#34;dbtable\u0026#34; -\u0026gt; table, \u0026#34;user\u0026#34; -\u0026gt; getMySQLUser, \u0026#34;password\u0026#34; -\u0026gt; getMySQLPassword, \u0026#34;driver\u0026#34; -\u0026gt; getMySQLDriver ) } // ======================================== // Kafka 配置 // ======================================== def getKafkaBootstrapServers: String = config.getString(\u0026#34;kafka.bootstrap-servers\u0026#34;) def getKafkaConsumerGroupId: String = config.getString(\u0026#34;kafka.consumer.group-id\u0026#34;) def getKafkaConsumerConfig: Map[String, String] = Map( \u0026#34;bootstrap.servers\u0026#34; -\u0026gt; getKafkaBootstrapServers, \u0026#34;group.id\u0026#34; -\u0026gt; getKafkaConsumerGroupId, \u0026#34;auto.offset.reset\u0026#34; -\u0026gt; config.getString(\u0026#34;kafka.consumer.auto-offset-reset\u0026#34;), \u0026#34;enable.auto.commit\u0026#34; -\u0026gt; config.getString(\u0026#34;kafka.consumer.enable-auto-commit\u0026#34;), \u0026#34;max.poll.records\u0026#34; -\u0026gt; config.getString(\u0026#34;kafka.consumer.max-poll-records\u0026#34;), \u0026#34;session.timeout.ms\u0026#34; -\u0026gt; config.getString(\u0026#34;kafka.consumer.session-timeout-ms\u0026#34;), \u0026#34;heartbeat.interval.ms\u0026#34; -\u0026gt; config.getString(\u0026#34;kafka.consumer.heartbeat-interval-ms\u0026#34;) ) def getKafkaProducerConfig: java.util.Properties = { val props = new java.util.Properties() props.put(\u0026#34;bootstrap.servers\u0026#34;, getKafkaBootstrapServers) props.put(\u0026#34;key.serializer\u0026#34;, \u0026#34;org.apache.kafka.common.serialization.StringSerializer\u0026#34;) props.put(\u0026#34;value.serializer\u0026#34;, \u0026#34;org.apache.kafka.common.serialization.StringSerializer\u0026#34;) props.put(\u0026#34;acks\u0026#34;, config.getString(\u0026#34;kafka.producer.acks\u0026#34;)) props.put(\u0026#34;retries\u0026#34;, config.getString(\u0026#34;kafka.producer.retries\u0026#34;)) props.put(\u0026#34;batch.size\u0026#34;, config.getString(\u0026#34;kafka.producer.batch-size\u0026#34;)) props.put(\u0026#34;linger.ms\u0026#34;, config.getString(\u0026#34;kafka.producer.linger-ms\u0026#34;)) props.put(\u0026#34;buffer.memory\u0026#34;, config.getString(\u0026#34;kafka.producer.buffer-memory\u0026#34;)) props } def getKafkaTopic(topicKey: String): String = { Try(config.getString(s\u0026#34;kafka.topics.$topicKey\u0026#34;)) match { case Success(topic) =\u0026gt; topic case Failure(_) =\u0026gt; logger.warn(s\u0026#34;未找到Kafka主题配置: kafka.topics.$topicKey，使用默认值: $topicKey\u0026#34;) topicKey } } // ======================================== // HBase 配置 // ======================================== def getHBaseZookeeperQuorum: String = config.getString(\u0026#34;hbase.zookeeper-quorum\u0026#34;) def getHBaseZookeeperClientPort: String = config.getString(\u0026#34;hbase.zookeeper-client-port\u0026#34;) def getHBaseTableName(tableKey: String): String = { Try(config.getString(s\u0026#34;hbase.tables.$tableKey\u0026#34;)) match { case Success(table) =\u0026gt; table case Failure(_) =\u0026gt; logger.warn(s\u0026#34;未找到HBase表配置: hbase.tables.$tableKey，使用默认值: $tableKey\u0026#34;) tableKey } } /** * 获取HBase Configuration对象 */ def getHBaseConfig: org.apache.hadoop.conf.Configuration = { val conf = org.apache.hadoop.hbase.HBaseConfiguration.create() conf.set(\u0026#34;hbase.zookeeper.quorum\u0026#34;, getHBaseZookeeperQuorum) conf.set(\u0026#34;hbase.zookeeper.property.clientPort\u0026#34;, getHBaseZookeeperClientPort) conf.set(\u0026#34;hbase.rpc.timeout\u0026#34;, config.getString(\u0026#34;hbase.rpc-timeout\u0026#34;).toString) conf.set(\u0026#34;hbase.client.retries.number\u0026#34;, config.getString(\u0026#34;hbase.client-retries-number\u0026#34;).toString) conf.set(\u0026#34;hbase.client.operation.timeout\u0026#34;, config.getString(\u0026#34;hbase.client-operation-timeout\u0026#34;).toString) conf } // ======================================== // Spark 配置 // ======================================== def getSparkAppName: String = config.getString(\u0026#34;spark.app-name\u0026#34;) def getSparkMaster: String = config.getString(\u0026#34;spark.master\u0026#34;) def getSparkBatchDurationSeconds: Int = config.getInt(\u0026#34;spark.streaming.batch-duration-seconds\u0026#34;) def getSparkCheckpointDir: String = config.getString(\u0026#34;spark.streaming.checkpoint-dir\u0026#34;) def getSparkShufflePartitions: Int = config.getInt(\u0026#34;spark.sql.shuffle-partitions\u0026#34;) // ======================================== // 数据验证配置 // ======================================== /** * 获取传感器值范围 * @param sensorType 传感器类型 * @return (最小值, 最大值) */ def getSensorRange(sensorType: String): (Double, Double) = { val path = s\u0026#34;validation.sensor-ranges.$sensorType\u0026#34; Try { val min = config.getDouble(s\u0026#34;$path.min\u0026#34;) val max = config.getDouble(s\u0026#34;$path.max\u0026#34;) (min, max) } match { case Success(range) =\u0026gt; range case Failure(_) =\u0026gt; logger.warn(s\u0026#34;未找到传感器范围配置: $path，使用默认范围(0.0, 100.0)\u0026#34;) (0.0, 100.0) } } def getSalePriceRange: (Double, Double) = { (config.getDouble(\u0026#34;validation.sale-price.min\u0026#34;), config.getDouble(\u0026#34;validation.sale-price.max\u0026#34;)) } def getCustomerAgeRange: (Int, Int) = { (config.getInt(\u0026#34;validation.customer-age.min\u0026#34;), config.getInt(\u0026#34;validation.customer-age.max\u0026#34;)) } // ======================================== // 降级策略配置 // ======================================== def isDegradationEnabled: Boolean = config.getBoolean(\u0026#34;degradation.enabled\u0026#34;) def getMaxRetries: Int = config.getInt(\u0026#34;degradation.max-retries\u0026#34;) def getRetryIntervalMs: Long = config.getLong(\u0026#34;degradation.retry-interval-ms\u0026#34;) def getBatchFallbackThreshold: Int = config.getInt(\u0026#34;degradation.batch-fallback-threshold\u0026#34;) def getFallbackDataPath: String = config.getString(\u0026#34;degradation.fallback-data-path\u0026#34;) // ======================================== // 数据库表名配置 // ======================================== def getDatabaseTable(tableKey: String): String = { Try(config.getString(s\u0026#34;database.tables.$tableKey\u0026#34;)) match { case Success(table) =\u0026gt; table case Failure(_) =\u0026gt; logger.warn(s\u0026#34;未找到数据库表配置: database.tables.$tableKey，使用默认值: $tableKey\u0026#34;) tableKey } } }\rDataAccessLayer.scala - 数据访问层\r#\r统一封装MySQL和HBase的数据访问操作，提供连接管理、资源释放和异常处理。\npackage com.sparklearning.auto.dataaccess import com.sparklearning.auto.config.ConfigManager import org.apache.hadoop.hbase.{HBaseConfiguration, TableName} import org.apache.hadoop.hbase.client.{Connection, ConnectionFactory, Put, Scan, Table} import org.apache.hadoop.hbase.util.Bytes import org.slf4j.{Logger, LoggerFactory} import java.sql.{Connection =\u0026gt; SQLConnection, DriverManager, PreparedStatement, ResultSet} import scala.jdk.CollectionConverters._ import scala.util.{Try, Success, Failure, Using} /** * 数据访问层 - 统一封装数据库操作 * * 设计原则： * 1. 所有数据库连接通过ConfigManager获取配置 * 2. 使用loan pattern确保资源释放 * 3. 提供统一的异常处理和日志记录 * 4. 连接池管理（简化实现，生产环境建议使用HikariCP） */ object DataAccessLayer { private val logger: Logger = LoggerFactory.getLogger(DataAccessLayer.getClass) // ======================================== // MySQL 数据访问 // ======================================== /** * 获取MySQL连接 * 使用loan pattern，确保连接在使用后自动关闭 * * @param operation 数据库操作函数 * @tparam T 返回值类型 * @return 操作结果 */ def withMySQLConnection[T](operation: SQLConnection =\u0026gt; T): Try[T] = { var connection: SQLConnection = null try { Class.forName(ConfigManager.getMySQLDriver) connection = DriverManager.getConnection( ConfigManager.getMySQLUrl, ConfigManager.getMySQLUser, ConfigManager.getMySQLPassword ) connection.setAutoCommit(false) val result = operation(connection) connection.commit() Success(result) } catch { case e: Exception =\u0026gt; if (connection != null) { try { connection.rollback() } catch { case rollbackEx: Exception =\u0026gt; logger.error(\u0026#34;MySQL回滚失败\u0026#34;, rollbackEx) } } logger.error(\u0026#34;MySQL操作失败\u0026#34;, e) Failure(e) } finally { if (connection != null) { try { connection.close() } catch { case e: Exception =\u0026gt; logger.error(\u0026#34;MySQL连接关闭失败\u0026#34;, e) } } } } /** * 批量写入MySQL * * @param tableKey 表配置键名 * @param columns 列名列表 * @param records 数据记录列表 * @return 成功写入的记录数 */ def batchInsertToMySQL(tableKey: String, columns: Seq[String], records: Seq[Seq[Any]]): Try[Int] = { withMySQLConnection { conn =\u0026gt; val tableName = ConfigManager.getDatabaseTable(tableKey) val placeholders = columns.map(_ =\u0026gt; \u0026#34;?\u0026#34;).mkString(\u0026#34;,\u0026#34;) val sql = s\u0026#34;INSERT INTO $tableName (${columns.mkString(\u0026#34;,\u0026#34;)}) VALUES ($placeholders)\u0026#34; var successCount = 0 val stmt = conn.prepareStatement(sql) try { records.foreach { record =\u0026gt; record.zipWithIndex.foreach { case (value, idx) =\u0026gt; stmt.setObject(idx + 1, value) } stmt.addBatch() successCount += 1 } stmt.executeBatch() successCount } finally { stmt.close() } } } /** * 执行查询并处理结果 * * @param sql SQL查询语句 * @param processor 结果处理函数 * @tparam T 返回值类型 * @return 查询结果 */ def queryMySQL[T](sql: String)(processor: ResultSet =\u0026gt; T): Try[T] = { withMySQLConnection { conn =\u0026gt; val stmt = conn.createStatement() var rs: ResultSet = null try { rs = stmt.executeQuery(sql) processor(rs) } finally { if (rs != null) rs.close() stmt.close() } } } // ======================================== // HBase 数据访问 // ======================================== /** * 获取HBase连接 * 使用loan pattern，确保连接在使用后自动关闭 * * @param operation HBase操作函数 * @tparam T 返回值类型 * @return 操作结果 */ def withHBaseConnection[T](operation: Connection =\u0026gt; T): Try[T] = { var connection: Connection = null try { connection = ConnectionFactory.createConnection(ConfigManager.getHBaseConfig) Success(operation(connection)) } catch { case e: Exception =\u0026gt; logger.error(\u0026#34;HBase操作失败\u0026#34;, e) Failure(e) } finally { if (connection != null) { try { connection.close() } catch { case e: Exception =\u0026gt; logger.error(\u0026#34;HBase连接关闭失败\u0026#34;, e) } } } } /** * 批量写入HBase * * @param tableKey 表配置键名 * @param columnFamily 列族名 * @param records 数据记录列表: (rowKey, Seq[(columnQualifier, value)]) * @return 成功写入的记录数 */ def batchInsertToHBase(tableKey: String, columnFamily: String, records: Seq[(String, Seq[(String, Any)])]): Try[Int] = { withHBaseConnection { conn =\u0026gt; val tableName = ConfigManager.getHBaseTableName(tableKey) val table = conn.getTable(TableName.valueOf(tableName)) try { val cfBytes = Bytes.toBytes(columnFamily) val puts = records.map { case (rowKey, columns) =\u0026gt; val put = new Put(Bytes.toBytes(rowKey)) columns.foreach { case (qualifier, value) =\u0026gt; value match { case v: Int =\u0026gt; put.addColumn(cfBytes, Bytes.toBytes(qualifier), Bytes.toBytes(v)) case v: Long =\u0026gt; put.addColumn(cfBytes, Bytes.toBytes(qualifier), Bytes.toBytes(v)) case v: Double =\u0026gt; put.addColumn(cfBytes, Bytes.toBytes(qualifier), Bytes.toBytes(v)) case v: String =\u0026gt; put.addColumn(cfBytes, Bytes.toBytes(qualifier), Bytes.toBytes(v)) case v: Float =\u0026gt; put.addColumn(cfBytes, Bytes.toBytes(qualifier), Bytes.toBytes(v)) case v: Boolean =\u0026gt; put.addColumn(cfBytes, Bytes.toBytes(qualifier), Bytes.toBytes(v)) case v: Array[Byte] =\u0026gt; put.addColumn(cfBytes, Bytes.toBytes(qualifier), v) case other =\u0026gt; put.addColumn(cfBytes, Bytes.toBytes(qualifier), Bytes.toBytes(other.toString)) } } put } table.put(puts.asJava) records.size } finally { table.close() } } } // ======================================== // 数据验证工具 // ======================================== /** * 验证传感器值是否在合理范围内 */ def validateSensorValue(sensorType: String, value: Double): Boolean = { val (min, max) = ConfigManager.getSensorRange(sensorType) if (value \u0026lt; min || value \u0026gt; max) { logger.warn(s\u0026#34;传感器值超出合理范围: type=$sensorType, value=$value, range=[$min, $max]\u0026#34;) false } else { true } } /** * 验证销售价格是否在合理范围内 */ def validateSalePrice(price: Double): Boolean = { val (min, max) = ConfigManager.getSalePriceRange if (price \u0026lt; min || price \u0026gt; max) { logger.warn(s\u0026#34;销售价格超出合理范围: price=$price, range=[$min, $max]\u0026#34;) false } else { true } } /** * 验证客户年龄是否在合理范围内 */ def validateCustomerAge(age: Int): Boolean = { val (min, max) = ConfigManager.getCustomerAgeRange if (age \u0026lt; min || age \u0026gt; max) { logger.warn(s\u0026#34;客户年龄超出合理范围: age=$age, range=[$min, $max]\u0026#34;) false } else { true } } /** * 通用空值检查 */ def requireNonEmpty(value: String, fieldName: String): Unit = { require(value != null \u0026amp;\u0026amp; value.trim.nonEmpty, s\u0026#34;$fieldName 不能为空\u0026#34;) } }\r数据模型设计\r#\r1. 销售数据模型\r#\r字段名 数据类型 描述 约束 sale_id BIGINT 销售记录ID PRIMARY KEY vehicle_id INT 车辆ID NOT NULL customer_id INT 客户ID NOT NULL dealer_id INT 经销商ID NOT NULL sale_date DATE 销售日期 NOT NULL sale_price DECIMAL(12,2) 销售价格 CHECK(sale_price \u0026gt; 0) payment_method VARCHAR(50) 支付方式 NOT NULL region VARCHAR(100) 销售地区 NOT NULL 2. 车辆数据模型\r#\r字段名 数据类型 描述 约束 vehicle_id INT 车辆ID PRIMARY KEY brand VARCHAR(50) 品牌 NOT NULL model VARCHAR(100) 型号 NOT NULL year INT 年份 CHECK(year \u0026gt;= 2000 AND year \u0026lt;= 2030) fuel_type VARCHAR(20) 燃油类型 NOT NULL engine_size DECIMAL(3,1) 发动机排量 CHECK(engine_size \u0026gt; 0) horsepower INT 马力 CHECK(horsepower \u0026gt; 0) price DECIMAL(12,2) 指导价格 CHECK(price \u0026gt; 0) features JSON 配置特征 3. 客户数据模型\r#\r字段名 数据类型 描述 约束 customer_id INT 客户ID PRIMARY KEY name VARCHAR(100) 姓名 NOT NULL age INT 年龄 CHECK(age \u0026gt;= 18 AND age \u0026lt;= 100) gender VARCHAR(10) 性别 NOT NULL occupation VARCHAR(100) 职业 income DECIMAL(12,2) 收入 CHECK(income \u0026gt;= 0) region VARCHAR(100) 地区 NOT NULL purchase_history JSON 购买历史 4. 传感器数据模型\r#\r字段名 数据类型 描述 约束 sensor_id INT 传感器ID NOT NULL vehicle_id INT 车辆ID NOT NULL timestamp BIGINT 采集时间(毫秒时间戳) NOT NULL sensor_type VARCHAR(50) 传感器类型 NOT NULL value DOUBLE 传感器值 NOT NULL status VARCHAR(20) 状态 NOT NULL 5. GPS轨迹数据模型\r#\r字段名 数据类型 描述 约束 vehicle_id INT 车辆ID NOT NULL timestamp BIGINT 采集时间(毫秒时间戳) NOT NULL latitude DOUBLE 纬度 CHECK(latitude BETWEEN -90 AND 90) longitude DOUBLE 经度 CHECK(longitude BETWEEN -180 AND 180) speed DOUBLE 速度(km/h) CHECK(speed \u0026gt;= 0) heading DOUBLE 航向角(0-360) 系统模块设计\r#\r1. 数据采集模块\r#\r1.1 销售数据采集\r#\r使用Kafka采集销售系统的实时数据，所有配置通过ConfigManager获取：\npackage com.sparklearning.auto.producer import com.sparklearning.auto.config.ConfigManager import org.apache.kafka.clients.producer.{KafkaProducer, ProducerRecord} import org.slf4j.{Logger, LoggerFactory} import scala.util.Random /** * 销售数据生产者 - 模拟销售数据发送到Kafka * * 企业级改进： * 1. 配置从ConfigManager获取，不硬编码 * 2. 使用try-finally确保Producer资源释放 * 3. 添加数据验证逻辑 * 4. 添加日志记录 */ object SalesDataProducer { private val logger: Logger = LoggerFactory.getLogger(SalesDataProducer.getClass) def main(args: Array[String]): Unit = { val producerProps = ConfigManager.getKafkaProducerConfig val topic = ConfigManager.getKafkaTopic(\u0026#34;sales-data\u0026#34;) val producer = new KafkaProducer[String, String](producerProps) val random = new Random() val brands = Array(\u0026#34;比亚迪\u0026#34;, \u0026#34;吉利\u0026#34;, \u0026#34;长安\u0026#34;, \u0026#34;奇瑞\u0026#34;, \u0026#34;哈弗\u0026#34;, \u0026#34;红旗\u0026#34;, \u0026#34;蔚来\u0026#34;, \u0026#34;理想\u0026#34;, \u0026#34;小鹏\u0026#34;, \u0026#34;问界\u0026#34;, \u0026#34;极氪\u0026#34;, \u0026#34;领克\u0026#34;) val models = Array(\u0026#34;秦PLUS\u0026#34;, \u0026#34;星瑞\u0026#34;, \u0026#34;UNI-V\u0026#34;, \u0026#34;瑞虎8\u0026#34;, \u0026#34;H6\u0026#34;, \u0026#34;H5\u0026#34;, \u0026#34;ES6\u0026#34;, \u0026#34;L7\u0026#34;, \u0026#34;P7\u0026#34;, \u0026#34;M5\u0026#34;, \u0026#34;001\u0026#34;, \u0026#34;08\u0026#34;) val regions = Array(\u0026#34;北京\u0026#34;, \u0026#34;上海\u0026#34;, \u0026#34;广州\u0026#34;, \u0026#34;深圳\u0026#34;, \u0026#34;杭州\u0026#34;, \u0026#34;成都\u0026#34;, \u0026#34;武汉\u0026#34;, \u0026#34;西安\u0026#34;, \u0026#34;南京\u0026#34;, \u0026#34;重庆\u0026#34;, \u0026#34;苏州\u0026#34;, \u0026#34;天津\u0026#34;) val paymentMethods = Array(\u0026#34;现金\u0026#34;, \u0026#34;贷款\u0026#34;, \u0026#34;分期付款\u0026#34;, \u0026#34;融资租赁\u0026#34;) try { var sentCount = 0L while (true) { val saleId = System.currentTimeMillis() val vehicleId = 1000 + random.nextInt(9000) val customerId = 100 + random.nextInt(900) val dealerId = 10 + random.nextInt(90) val year = 2023 + random.nextInt(4) val month = 1 + random.nextInt(12) val day = 1 + random.nextInt(28) val saleDate = f\u0026#34;$year-$month%02d-$day%02d\u0026#34; val salePrice = 100000.0 + random.nextInt(900000) val paymentMethod = paymentMethods(random.nextInt(paymentMethods.length)) val region = regions(random.nextInt(regions.length)) // 数据验证 if (!DataValidator.validateSalePrice(salePrice)) { logger.warn(s\u0026#34;跳过无效销售价格: $salePrice\u0026#34;) Thread.sleep(2000) next } val salesData = s\u0026#34;\u0026#34;\u0026#34;{\u0026#34;sale_id\u0026#34;:$saleId,\u0026#34;vehicle_id\u0026#34;:$vehicleId,\u0026#34;customer_id\u0026#34;:$customerId,\u0026#34;dealer_id\u0026#34;:$dealerId,\u0026#34;sale_date\u0026#34;:\u0026#34;$saleDate\u0026#34;,\u0026#34;sale_price\u0026#34;:$salePrice,\u0026#34;payment_method\u0026#34;:\u0026#34;$paymentMethod\u0026#34;,\u0026#34;region\u0026#34;:\u0026#34;$region\u0026#34;}\u0026#34;\u0026#34;\u0026#34; val record = new ProducerRecord[String, String](topic, null, salesData) producer.send(record) sentCount += 1 if (sentCount % 100 == 0) { logger.info(s\u0026#34;已发送销售数据: $sentCount 条\u0026#34;) } Thread.sleep(2000) } } catch { case e: InterruptedException =\u0026gt; logger.info(\u0026#34;销售数据生产者被中断，正在关闭...\u0026#34;) case e: Exception =\u0026gt; logger.error(\u0026#34;销售数据生产者异常\u0026#34;, e) } finally { producer.close() logger.info(\u0026#34;Kafka Producer已关闭\u0026#34;) } } } /** * 数据验证工具 - 集中管理数据验证逻辑 */ object DataValidator { private val logger: Logger = LoggerFactory.getLogger(DataValidator.getClass) /** * 验证销售价格 */ def validateSalePrice(price: Double): Boolean = { val (min, max) = ConfigManager.getSalePriceRange val valid = price \u0026gt;= min \u0026amp;\u0026amp; price \u0026lt;= max if (!valid) { logger.warn(s\u0026#34;销售价格超出合理范围: price=$price, range=[$min, $max]\u0026#34;) } valid } /** * 验证传感器值 */ def validateSensorValue(sensorType: String, value: Double): Boolean = { val (min, max) = ConfigManager.getSensorRange(sensorType) val valid = value \u0026gt;= min \u0026amp;\u0026amp; value \u0026lt;= max if (!valid) { logger.warn(s\u0026#34;传感器值超出合理范围: type=$sensorType, value=$value, range=[$min, $max]\u0026#34;) } valid } /** * 验证非空字符串 */ def validateNonEmpty(value: String, fieldName: String): Boolean = { val valid = value != null \u0026amp;\u0026amp; value.trim.nonEmpty if (!valid) { logger.warn(s\u0026#34;$fieldName 值为空\u0026#34;) } valid } /** * 验证数值范围 */ def validateRange(value: Double, min: Double, max: Double, fieldName: String): Boolean = { val valid = value \u0026gt;= min \u0026amp;\u0026amp; value \u0026lt;= max if (!valid) { logger.warn(s\u0026#34;$fieldName 超出范围: value=$value, range=[$min, $max]\u0026#34;) } valid } }\r1.2 传感器数据采集\r#\rpackage com.sparklearning.auto.producer import com.sparklearning.auto.config.ConfigManager import org.apache.kafka.clients.producer.{KafkaProducer, ProducerRecord} import org.slf4j.{Logger, LoggerFactory} import scala.util.Random /** * 传感器数据生产者 - 模拟车辆传感器数据发送到Kafka * * 企业级改进： * 1. 配置从ConfigManager获取 * 2. 传感器值范围验证 * 3. try-finally确保资源释放 * 4. 完善的异常处理 */ object SensorDataProducer { private val logger: Logger = LoggerFactory.getLogger(SensorDataProducer.getClass) def main(args: Array[String]): Unit = { val producerProps = ConfigManager.getKafkaProducerConfig val topic = ConfigManager.getKafkaTopic(\u0026#34;sensor-data\u0026#34;) val producer = new KafkaProducer[String, String](producerProps) val random = new Random() val sensorTypes = Array(\u0026#34;发动机温度\u0026#34;, \u0026#34;机油压力\u0026#34;, \u0026#34;冷却液温度\u0026#34;, \u0026#34;轮胎压力\u0026#34;, \u0026#34;燃油液位\u0026#34;, \u0026#34;电池电压\u0026#34;) // 各传感器的正常值范围 val sensorNormalRanges = Map( \u0026#34;发动机温度\u0026#34; -\u0026gt; (70.0, 105.0), \u0026#34;机油压力\u0026#34; -\u0026gt; (1.5, 5.0), \u0026#34;冷却液温度\u0026#34; -\u0026gt; (75.0, 100.0), \u0026#34;轮胎压力\u0026#34; -\u0026gt; (2.0, 2.8), \u0026#34;燃油液位\u0026#34; -\u0026gt; (5.0, 95.0), \u0026#34;电池电压\u0026#34; -\u0026gt; (11.5, 14.5) ) try { var sentCount = 0L while (true) { val sensorId = 1000 + random.nextInt(9000) val vehicleId = 1000 + random.nextInt(9000) val timestamp = System.currentTimeMillis() val sensorType = sensorTypes(random.nextInt(sensorTypes.length)) val (normalMin, normalMax) = sensorNormalRanges(sensorType) // 90%概率生成正常值，10%概率生成异常值 val value = if (random.nextDouble() \u0026lt; 0.9) { normalMin + random.nextDouble() * (normalMax - normalMin) } else { // 生成异常值：超出正常范围 val (globalMin, globalMax) = ConfigManager.getSensorRange(sensorType) if (random.nextBoolean()) { globalMin + random.nextDouble() * (normalMin - globalMin) } else { normalMax + random.nextDouble() * (globalMax - normalMax) } } val roundedValue = BigDecimal(value).setScale(1, BigDecimal.RoundingMode.HALF_UP).toDouble val status = if (roundedValue \u0026lt; normalMin || roundedValue \u0026gt; normalMax) \u0026#34;异常\u0026#34; else \u0026#34;正常\u0026#34; val sensorData = s\u0026#34;\u0026#34;\u0026#34;{\u0026#34;sensor_id\u0026#34;:$sensorId,\u0026#34;vehicle_id\u0026#34;:$vehicleId,\u0026#34;timestamp\u0026#34;:$timestamp,\u0026#34;sensor_type\u0026#34;:\u0026#34;$sensorType\u0026#34;,\u0026#34;value\u0026#34;:$roundedValue,\u0026#34;status\u0026#34;:\u0026#34;$status\u0026#34;}\u0026#34;\u0026#34;\u0026#34; val record = new ProducerRecord[String, String](topic, String.valueOf(vehicleId), sensorData) producer.send(record) sentCount += 1 if (sentCount % 100 == 0) { logger.info(s\u0026#34;已发送传感器数据: $sentCount 条\u0026#34;) } Thread.sleep(1000) } } catch { case e: InterruptedException =\u0026gt; logger.info(\u0026#34;传感器数据生产者被中断，正在关闭...\u0026#34;) case e: Exception =\u0026gt; logger.error(\u0026#34;传感器数据生产者异常\u0026#34;, e) } finally { producer.close() logger.info(\u0026#34;Kafka Producer已关闭\u0026#34;) } } }\r2. 数据存储模块\r#\r2.1 HBase存储传感器数据\r#\rpackage com.sparklearning.auto.storage import com.sparklearning.auto.config.ConfigManager import com.sparklearning.auto.dataaccess.DataAccessLayer import org.apache.spark.sql.SparkSession import org.apache.spark.sql.functions._ import org.apache.spark.sql.streaming.OutputMode import org.apache.spark.sql.types._ import org.slf4j.{Logger, LoggerFactory} /** * 传感器数据写入HBase * * 企业级改进： * 1. SparkSession使用try-finally确保释放 * 2. HBase连接配置从ConfigManager获取 * 3. 数据验证：过滤无效传感器值 * 4. 使用foreachBatch + DataAccessLayer写入HBase * 5. 添加降级策略：HBase不可用时暂存到HDFS */ object SensorDataToHBase { private val logger: Logger = LoggerFactory.getLogger(SensorDataToHBase.getClass) def main(args: Array[String]): Unit = { try { val spark = SparkSession.builder() .appName(\u0026#34;SensorDataToHBase\u0026#34;) .master(ConfigManager.getSparkMaster) .config(\u0026#34;spark.sql.shuffle.partitions\u0026#34;, ConfigManager.getSparkShufflePartitions) .getOrCreate() import spark.implicits._ // 从Kafka读取传感器数据 val sensorStream = spark.readStream .format(\u0026#34;kafka\u0026#34;) .option(\u0026#34;kafka.bootstrap.servers\u0026#34;, ConfigManager.getKafkaBootstrapServers) .option(\u0026#34;subscribe\u0026#34;, ConfigManager.getKafkaTopic(\u0026#34;sensor-data\u0026#34;)) .option(\u0026#34;startingOffsets\u0026#34;, \u0026#34;latest\u0026#34;) .option(\u0026#34;maxOffsetsPerTrigger\u0026#34;, \u0026#34;1000\u0026#34;) .load() // 定义Schema val schema = new StructType() .add(\u0026#34;sensor_id\u0026#34;, IntegerType) .add(\u0026#34;vehicle_id\u0026#34;, IntegerType) .add(\u0026#34;timestamp\u0026#34;, LongType) .add(\u0026#34;sensor_type\u0026#34;, StringType) .add(\u0026#34;value\u0026#34;, DoubleType) .add(\u0026#34;status\u0026#34;, StringType) // 解析JSON数据 + 数据验证 val sensorData = sensorStream .selectExpr(\u0026#34;CAST(value AS STRING)\u0026#34;) .select(from_json($\u0026#34;value\u0026#34;, schema).as(\u0026#34;data\u0026#34;)) .select(\u0026#34;data.*\u0026#34;) .filter($\u0026#34;data\u0026#34;.isNotNull) // 过滤解析失败的数据 .filter($\u0026#34;sensor_id\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;vehicle_id\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;timestamp\u0026#34;.isNotNull) .filter($\u0026#34;sensor_type\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;value\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;status\u0026#34;.isNotNull) // 写入HBase（带降级策略） val query = sensorData.writeStream .foreachBatch { (batchDF: org.apache.spark.sql.DataFrame, batchId: Long) =\u0026gt; logger.info(s\u0026#34;处理批次: $batchId, 记录数: ${batchDF.count()}\u0026#34;) try { // 尝试写入HBase writeBatchToHBase(batchDF, batchId) } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;批次 $batchId 写入HBase失败，启动降级策略\u0026#34;, e) // 降级：将数据暂存到HDFS fallbackToHDFS(batchDF, batchId) } } .outputMode(OutputMode.Update()) .option(\u0026#34;checkpointLocation\u0026#34;, ConfigManager.getSparkCheckpointDir + \u0026#34;/sensor-to-hbase\u0026#34;) .start() query.awaitTermination() } catch { case e: Exception =\u0026gt; logger.error(\u0026#34;SensorDataToHBase运行异常\u0026#34;, e) throw e } finally { val s = SparkSession.getActiveSession.orNull if (s != null) { s.stop() logger.info(\u0026#34;SparkSession已关闭\u0026#34;) } } } /** * 将批次数据写入HBase */ private def writeBatchToHBase(batchDF: org.apache.spark.sql.DataFrame, batchId: Long): Unit = { val records = batchDF.collect().flatMap { row =\u0026gt; val sensorType = row.getAs[String](\u0026#34;sensor_type\u0026#34;) val value = row.getAs[Double](\u0026#34;value\u0026#34;) // 数据验证 if (!DataAccessLayer.validateSensorValue(sensorType, value)) { logger.warn(s\u0026#34;批次 $batchId: 跳过无效传感器数据, type=$sensorType, value=$value\u0026#34;) None } else { val rowKey = s\u0026#34;${row.getAs[Long](\u0026#34;timestamp\u0026#34;)}-${row.getAs[Int](\u0026#34;sensor_id\u0026#34;)}\u0026#34; val columns = Seq( (\u0026#34;vehicle_id\u0026#34;, row.getAs[Int](\u0026#34;vehicle_id\u0026#34;).asInstanceOf[Any]), (\u0026#34;sensor_type\u0026#34;, sensorType.asInstanceOf[Any]), (\u0026#34;value\u0026#34;, value.asInstanceOf[Any]), (\u0026#34;status\u0026#34;, row.getAs[String](\u0026#34;status\u0026#34;).asInstanceOf[Any]) ) Some((rowKey, columns)) } } if (records.nonEmpty) { val result = DataAccessLayer.batchInsertToHBase(\u0026#34;sensor-data\u0026#34;, \u0026#34;cf\u0026#34;, records) result match { case Success(count) =\u0026gt; logger.info(s\u0026#34;批次 $batchId 成功写入HBase: $count 条记录\u0026#34;) case Failure(e) =\u0026gt; throw e } } } /** * 降级策略：将数据暂存到HDFS */ private def fallbackToHDFS(batchDF: org.apache.spark.sql.DataFrame, batchId: Long): Unit = { if (!ConfigManager.isDegradationEnabled) { logger.warn(\u0026#34;降级策略未启用，数据可能丢失\u0026#34;) return } val fallbackPath = s\u0026#34;${ConfigManager.getFallbackDataPath}/sensor_data/batch_$batchId\u0026#34; logger.info(s\u0026#34;降级策略：将批次 $batchId 数据暂存到 $fallbackPath\u0026#34;) try { batchDF.write .mode(\u0026#34;overwrite\u0026#34;) .json(fallbackPath) logger.info(s\u0026#34;批次 $batchId 数据已暂存到HDFS: $fallbackPath\u0026#34;) } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;降级策略执行失败，批次 $batchId 数据可能丢失\u0026#34;, e) } } }\r2.2 MySQL存储销售数据\r#\rpackage com.sparklearning.auto.storage import com.sparklearning.auto.config.ConfigManager import com.sparklearning.auto.dataaccess.DataAccessLayer import org.apache.spark.sql.SparkSession import org.apache.spark.sql.functions._ import org.apache.spark.sql.streaming.OutputMode import org.apache.spark.sql.types._ import org.slf4j.{Logger, LoggerFactory} /** * 销售数据写入MySQL * * 企业级改进： * 1. SparkSession使用try-finally确保释放 * 2. MySQL连接配置从ConfigManager获取，不硬编码密码 * 3. 数据验证：过滤无效价格和空值 * 4. 添加降级策略：MySQL不可用时暂存到HDFS */ object SalesDataToMySQL { private val logger: Logger = LoggerFactory.getLogger(SalesDataToMySQL.getClass) def main(args: Array[String]): Unit = { try { val spark = SparkSession.builder() .appName(\u0026#34;SalesDataToMySQL\u0026#34;) .master(ConfigManager.getSparkMaster) .config(\u0026#34;spark.sql.shuffle.partitions\u0026#34;, ConfigManager.getSparkShufflePartitions) .getOrCreate() import spark.implicits._ // 从Kafka读取销售数据 val salesStream = spark.readStream .format(\u0026#34;kafka\u0026#34;) .option(\u0026#34;kafka.bootstrap.servers\u0026#34;, ConfigManager.getKafkaBootstrapServers) .option(\u0026#34;subscribe\u0026#34;, ConfigManager.getKafkaTopic(\u0026#34;sales-data\u0026#34;)) .option(\u0026#34;startingOffsets\u0026#34;, \u0026#34;latest\u0026#34;) .option(\u0026#34;maxOffsetsPerTrigger\u0026#34;, \u0026#34;500\u0026#34;) .load() // 定义Schema val schema = new StructType() .add(\u0026#34;sale_id\u0026#34;, LongType) .add(\u0026#34;vehicle_id\u0026#34;, IntegerType) .add(\u0026#34;customer_id\u0026#34;, IntegerType) .add(\u0026#34;dealer_id\u0026#34;, IntegerType) .add(\u0026#34;sale_date\u0026#34;, StringType) .add(\u0026#34;sale_price\u0026#34;, DoubleType) .add(\u0026#34;payment_method\u0026#34;, StringType) .add(\u0026#34;region\u0026#34;, StringType) // 解析JSON数据 + 数据验证 val salesData = salesStream .selectExpr(\u0026#34;CAST(value AS STRING)\u0026#34;) .select(from_json($\u0026#34;value\u0026#34;, schema).as(\u0026#34;data\u0026#34;)) .select(\u0026#34;data.*\u0026#34;) .filter($\u0026#34;data\u0026#34;.isNotNull) .filter($\u0026#34;sale_id\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;vehicle_id\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;customer_id\u0026#34;.isNotNull) .filter($\u0026#34;sale_date\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;sale_price\u0026#34;.isNotNull) .filter($\u0026#34;payment_method\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;region\u0026#34;.isNotNull) .filter($\u0026#34;sale_price\u0026#34; \u0026gt; 0) // 价格必须为正数 .withColumn(\u0026#34;sale_date\u0026#34;, to_date($\u0026#34;sale_date\u0026#34;)) // 写入MySQL（带降级策略） val query = salesData.writeStream .foreachBatch { (batchDF: org.apache.spark.sql.DataFrame, batchId: Long) =\u0026gt; logger.info(s\u0026#34;处理批次: $batchId, 记录数: ${batchDF.count()}\u0026#34;) try { // 使用ConfigManager获取MySQL配置，不硬编码密码 val tableName = ConfigManager.getDatabaseTable(\u0026#34;sales\u0026#34;) val mysqlOptions = ConfigManager.getMySQLOptions(tableName) batchDF.write .format(\u0026#34;jdbc\u0026#34;) .options(mysqlOptions) .mode(\u0026#34;append\u0026#34;) .save() logger.info(s\u0026#34;批次 $batchId 成功写入MySQL\u0026#34;) } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;批次 $batchId 写入MySQL失败，启动降级策略\u0026#34;, e) fallbackToHDFS(batchDF, batchId) } } .outputMode(OutputMode.Append()) .option(\u0026#34;checkpointLocation\u0026#34;, ConfigManager.getSparkCheckpointDir + \u0026#34;/sales-to-mysql\u0026#34;) .start() query.awaitTermination() } catch { case e: Exception =\u0026gt; logger.error(\u0026#34;SalesDataToMySQL运行异常\u0026#34;, e) throw e } finally { val s = SparkSession.getActiveSession.orNull if (s != null) { s.stop() logger.info(\u0026#34;SparkSession已关闭\u0026#34;) } } } /** * 降级策略：将数据暂存到HDFS */ private def fallbackToHDFS(batchDF: org.apache.spark.sql.DataFrame, batchId: Long): Unit = { if (!ConfigManager.isDegradationEnabled) { logger.warn(\u0026#34;降级策略未启用，数据可能丢失\u0026#34;) return } val fallbackPath = s\u0026#34;${ConfigManager.getFallbackDataPath}/sales_data/batch_$batchId\u0026#34; logger.info(s\u0026#34;降级策略：将批次 $batchId 数据暂存到 $fallbackPath\u0026#34;) try { batchDF.write .mode(\u0026#34;overwrite\u0026#34;) .json(fallbackPath) logger.info(s\u0026#34;批次 $batchId 数据已暂存到HDFS: $fallbackPath\u0026#34;) } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;降级策略执行失败，批次 $batchId 数据可能丢失\u0026#34;, e) } } }\r3. 数据分析模块\r#\r3.1 销量分析\r#\rpackage com.sparklearning.auto.analysis import com.sparklearning.auto.config.ConfigManager import org.apache.spark.sql.SparkSession import org.apache.spark.sql.functions._ import org.slf4j.{Logger, LoggerFactory} /** * 销量分析 - 基于Spark SQL的离线批处理分析 * * 分析内容： * 1. 按地区销量分析 * 2. 按支付方式销量分析 * 3. 销售趋势分析（同比/环比） * 4. 品牌市场份额分析 * * 企业级改进： * 1. try-catch-finally确保SparkSession释放 * 2. MySQL配置从ConfigManager获取 * 3. 数据验证和空值处理 * 4. 完善的异常处理和日志 */ object SalesAnalysis { private val logger: Logger = LoggerFactory.getLogger(SalesAnalysis.getClass) def main(args: Array[String]): Unit = { try { val spark = SparkSession.builder() .appName(\u0026#34;SalesAnalysis\u0026#34;) .master(ConfigManager.getSparkMaster) .config(\u0026#34;spark.sql.shuffle.partitions\u0026#34;, ConfigManager.getSparkShufflePartitions) .config(\u0026#34;spark.sql.adaptive.enabled\u0026#34;, \u0026#34;true\u0026#34;) .enableHiveSupport() .getOrCreate() import spark.implicits._ // 从Hive读取销售数据 val salesDF = spark.sql(\u0026#34;SELECT * FROM auto_database.sales\u0026#34;) // 数据验证：过滤无效记录 val validSalesDF = salesDF .filter($\u0026#34;sale_price\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;sale_price\u0026#34; \u0026gt; 0) .filter($\u0026#34;region\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;region\u0026#34; =!= \u0026#34;\u0026#34;) .filter($\u0026#34;sale_date\u0026#34;.isNotNull) .filter($\u0026#34;payment_method\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;payment_method\u0026#34; =!= \u0026#34;\u0026#34;) logger.info(s\u0026#34;有效销售记录数: ${validSalesDF.count()}\u0026#34;) // ======================================== // 1. 按地区销量分析 // ======================================== val regionSales = validSalesDF .groupBy($\u0026#34;region\u0026#34;) .agg( sum($\u0026#34;sale_price\u0026#34;).as(\u0026#34;total_sales\u0026#34;), count(\u0026#34;*\u0026#34;).as(\u0026#34;sale_count\u0026#34;), avg($\u0026#34;sale_price\u0026#34;).as(\u0026#34;avg_price\u0026#34;), min($\u0026#34;sale_price\u0026#34;).as(\u0026#34;min_price\u0026#34;), max($\u0026#34;sale_price\u0026#34;).as(\u0026#34;max_price\u0026#34;) ) .orderBy($\u0026#34;total_sales\u0026#34;.desc) println(\u0026#34;=== 按地区销售分析 ===\u0026#34;) regionSales.show(false) // ======================================== // 2. 按支付方式销量分析 // ======================================== val paymentSales = validSalesDF .groupBy($\u0026#34;payment_method\u0026#34;) .agg( sum($\u0026#34;sale_price\u0026#34;).as(\u0026#34;total_sales\u0026#34;), count(\u0026#34;*\u0026#34;).as(\u0026#34;sale_count\u0026#34;), avg($\u0026#34;sale_price\u0026#34;).as(\u0026#34;avg_price\u0026#34;) ) .withColumn(\u0026#34;ratio\u0026#34;, round($\u0026#34;sale_count\u0026#34; / sum(\u0026#34;sale_count\u0026#34;).over(), 4)) .orderBy($\u0026#34;total_sales\u0026#34;.desc) println(\u0026#34;=== 按支付方式销售分析 ===\u0026#34;) paymentSales.show(false) // ======================================== // 3. 销售趋势分析（月度同比/环比） // ======================================== val salesTrend = validSalesDF .withColumn(\u0026#34;year\u0026#34;, year($\u0026#34;sale_date\u0026#34;)) .withColumn(\u0026#34;month\u0026#34;, month($\u0026#34;sale_date\u0026#34;)) .groupBy($\u0026#34;year\u0026#34;, $\u0026#34;month\u0026#34;) .agg( sum($\u0026#34;sale_price\u0026#34;).as(\u0026#34;total_sales\u0026#34;), count(\u0026#34;*\u0026#34;).as(\u0026#34;sale_count\u0026#34;) ) .orderBy($\u0026#34;year\u0026#34;, $\u0026#34;month\u0026#34;) // 计算环比增长率 val salesTrendWithMoM = salesTrend .withColumn(\u0026#34;prev_month_sales\u0026#34;, lag($\u0026#34;total_sales\u0026#34;, 1).over( org.apache.spark.sql.expressions.Window.orderBy($\u0026#34;year\u0026#34;, $\u0026#34;month\u0026#34;) )) .withColumn(\u0026#34;mom_growth_rate\u0026#34;, when($\u0026#34;prev_month_sales\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;prev_month_sales\u0026#34; =!= 0, round(($\u0026#34;total_sales\u0026#34; - $\u0026#34;prev_month_sales\u0026#34;) / $\u0026#34;prev_month_sales\u0026#34; * 100, 2) ).otherwise(lit(null)) ) .drop(\u0026#34;prev_month_sales\u0026#34;) println(\u0026#34;=== 销售趋势分析（含环比增长率） ===\u0026#34;) salesTrendWithMoM.show(50, false) // ======================================== // 4. 品牌市场份额分析 // ======================================== val brandMarketShare = validSalesDF .groupBy($\u0026#34;brand\u0026#34;) .agg( sum($\u0026#34;sale_price\u0026#34;).as(\u0026#34;total_sales\u0026#34;), count(\u0026#34;*\u0026#34;).as(\u0026#34;sale_count\u0026#34;) ) .withColumn(\u0026#34;market_share\u0026#34;, round($\u0026#34;sale_count\u0026#34; / sum(\u0026#34;sale_count\u0026#34;).over() * 100, 2) ) .withColumn(\u0026#34;revenue_share\u0026#34;, round($\u0026#34;total_sales\u0026#34; / sum(\u0026#34;total_sales\u0026#34;).over() * 100, 2) ) .orderBy($\u0026#34;sale_count\u0026#34;.desc) println(\u0026#34;=== 品牌市场份额分析 ===\u0026#34;) brandMarketShare.show(false) // ======================================== // 保存分析结果 // ======================================== saveToMySQL(regionSales, \u0026#34;region-sales-analysis\u0026#34;) saveToMySQL(paymentSales, \u0026#34;payment-sales-analysis\u0026#34;) saveToMySQL(salesTrendWithMoM, \u0026#34;sales-trend-analysis\u0026#34;) saveToMySQL(brandMarketShare, \u0026#34;brand-market-share\u0026#34;) // 同时保存到Hive（批处理层结果） regionSales.write.mode(\u0026#34;overwrite\u0026#34;).saveAsTable(\u0026#34;auto_database.region_sales_analysis\u0026#34;) paymentSales.write.mode(\u0026#34;overwrite\u0026#34;).saveAsTable(\u0026#34;auto_database.payment_sales_analysis\u0026#34;) salesTrendWithMoM.write.mode(\u0026#34;overwrite\u0026#34;).saveAsTable(\u0026#34;auto_database.sales_trend_analysis\u0026#34;) brandMarketShare.write.mode(\u0026#34;overwrite\u0026#34;).saveAsTable(\u0026#34;auto_database.brand_market_share\u0026#34;) logger.info(\u0026#34;销量分析完成，结果已保存到MySQL和Hive\u0026#34;) } catch { case e: Exception =\u0026gt; logger.error(\u0026#34;销量分析运行异常\u0026#34;, e) throw e } finally { val s = SparkSession.getActiveSession.orNull if (s != null) { s.stop() logger.info(\u0026#34;SparkSession已关闭\u0026#34;) } } } /** * 保存DataFrame到MySQL */ private def saveToMySQL(df: org.apache.spark.sql.DataFrame, tableKey: String): Unit = { try { val tableName = ConfigManager.getDatabaseTable(tableKey) val mysqlOptions = ConfigManager.getMySQLOptions(tableName) df.write .format(\u0026#34;jdbc\u0026#34;) .options(mysqlOptions) .mode(\u0026#34;overwrite\u0026#34;) .save() logger.info(s\u0026#34;分析结果已保存到MySQL表: $tableName\u0026#34;) } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;保存到MySQL失败: $tableKey\u0026#34;, e) // 降级：保存到HDFS val fallbackPath = s\u0026#34;${ConfigManager.getFallbackDataPath}/analysis/$tableKey\u0026#34; df.write.mode(\u0026#34;overwrite\u0026#34;).parquet(fallbackPath) logger.info(s\u0026#34;降级：分析结果已保存到HDFS: $fallbackPath\u0026#34;) } } }\r3.2 价格分析\r#\rpackage com.sparklearning.auto.analysis import com.sparklearning.auto.config.ConfigManager import org.apache.spark.sql.SparkSession import org.apache.spark.sql.functions._ import org.slf4j.{Logger, LoggerFactory} /** * 价格分析 - 汽车价格维度深度分析 * * 分析内容： * 1. 各品牌平均成交价与中位数价格 * 2. 价格离散度分析 * 3. 优惠幅度分析 * 4. 价格弹性分析 */ object PriceAnalysis { private val logger: Logger = LoggerFactory.getLogger(PriceAnalysis.getClass) def main(args: Array[String]): Unit = { try { val spark = SparkSession.builder() .appName(\u0026#34;PriceAnalysis\u0026#34;) .master(ConfigManager.getSparkMaster) .config(\u0026#34;spark.sql.shuffle.partitions\u0026#34;, ConfigManager.getSparkShufflePartitions) .config(\u0026#34;spark.sql.adaptive.enabled\u0026#34;, \u0026#34;true\u0026#34;) .enableHiveSupport() .getOrCreate() import spark.implicits._ // 读取销售和车辆数据 val salesDF = spark.sql(\u0026#34;SELECT * FROM auto_database.sales\u0026#34;) .filter($\u0026#34;sale_price\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;sale_price\u0026#34; \u0026gt; 0) val vehiclesDF = spark.sql(\u0026#34;SELECT * FROM auto_database.vehicles\u0026#34;) .filter($\u0026#34;price\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;price\u0026#34; \u0026gt; 0) // 关联销售和车辆数据 val salesWithVehicle = salesDF .join(vehiclesDF, Seq(\u0026#34;vehicle_id\u0026#34;), \u0026#34;left\u0026#34;) .filter($\u0026#34;brand\u0026#34;.isNotNull) // ======================================== // 1. 各品牌价格水平分析 // ======================================== val brandPriceLevel = salesWithVehicle .groupBy($\u0026#34;brand\u0026#34;) .agg( avg($\u0026#34;sale_price\u0026#34;).as(\u0026#34;avg_sale_price\u0026#34;), expr(\u0026#34;percentile_approx(sale_price, 0.5)\u0026#34;).as(\u0026#34;median_price\u0026#34;), min($\u0026#34;sale_price\u0026#34;).as(\u0026#34;min_price\u0026#34;), max($\u0026#34;sale_price\u0026#34;).as(\u0026#34;max_price\u0026#34;), count(\u0026#34;*\u0026#34;).as(\u0026#34;sale_count\u0026#34;) ) .orderBy($\u0026#34;avg_sale_price\u0026#34;.desc) println(\u0026#34;=== 各品牌价格水平分析 ===\u0026#34;) brandPriceLevel.show(false) // ======================================== // 2. 价格离散度分析 // ======================================== val brandPriceDispersion = salesWithVehicle .groupBy($\u0026#34;brand\u0026#34;) .agg( avg($\u0026#34;sale_price\u0026#34;).as(\u0026#34;avg_price\u0026#34;), stddev($\u0026#34;sale_price\u0026#34;).as(\u0026#34;stddev_price\u0026#34;), (stddev($\u0026#34;sale_price\u0026#34;) / avg($\u0026#34;sale_price\u0026#34;) * 100).as(\u0026#34;cv_percent\u0026#34;) ) .orderBy($\u0026#34;cv_percent\u0026#34;.desc) println(\u0026#34;=== 价格离散度分析（变异系数CV） ===\u0026#34;) brandPriceDispersion.show(false) // ======================================== // 3. 优惠幅度分析 // ======================================== val discountAnalysis = salesWithVehicle .filter($\u0026#34;price\u0026#34; \u0026gt; 0) .withColumn(\u0026#34;discount_rate\u0026#34;, round(($\u0026#34;price\u0026#34; - $\u0026#34;sale_price\u0026#34;) / $\u0026#34;price\u0026#34; * 100, 2) ) .groupBy($\u0026#34;brand\u0026#34;) .agg( avg($\u0026#34;discount_rate\u0026#34;).as(\u0026#34;avg_discount_rate\u0026#34;), max($\u0026#34;discount_rate\u0026#34;).as(\u0026#34;max_discount_rate\u0026#34;), min($\u0026#34;discount_rate\u0026#34;).as(\u0026#34;min_discount_rate\u0026#34;) ) .orderBy($\u0026#34;avg_discount_rate\u0026#34;.desc) println(\u0026#34;=== 优惠幅度分析 ===\u0026#34;) discountAnalysis.show(false) // ======================================== // 4. 地区价格差异分析 // ======================================== val regionPriceDiff = salesWithVehicle .groupBy($\u0026#34;region\u0026#34;, $\u0026#34;brand\u0026#34;) .agg( avg($\u0026#34;sale_price\u0026#34;).as(\u0026#34;avg_price\u0026#34;), count(\u0026#34;*\u0026#34;).as(\u0026#34;sale_count\u0026#34;) ) .filter($\u0026#34;sale_count\u0026#34; \u0026gt;= 5) // 过滤样本量过小的数据 .orderBy($\u0026#34;brand\u0026#34;, $\u0026#34;avg_price\u0026#34;.desc) println(\u0026#34;=== 地区价格差异分析 ===\u0026#34;) regionPriceDiff.show(50, false) // 保存结果 saveToMySQL(brandPriceLevel, \u0026#34;brand-price-level\u0026#34;) saveToMySQL(brandPriceDispersion, \u0026#34;brand-price-dispersion\u0026#34;) saveToMySQL(discountAnalysis, \u0026#34;discount-analysis\u0026#34;) } catch { case e: Exception =\u0026gt; logger.error(\u0026#34;价格分析运行异常\u0026#34;, e) throw e } finally { val s = SparkSession.getActiveSession.orNull if (s != null) { s.stop() logger.info(\u0026#34;SparkSession已关闭\u0026#34;) } } } private def saveToMySQL(df: org.apache.spark.sql.DataFrame, tableKey: String): Unit = { try { val tableName = ConfigManager.getDatabaseTable(tableKey) val mysqlOptions = ConfigManager.getMySQLOptions(tableName) df.write.format(\u0026#34;jdbc\u0026#34;).options(mysqlOptions).mode(\u0026#34;overwrite\u0026#34;).save() logger.info(s\u0026#34;分析结果已保存到MySQL表: $tableName\u0026#34;) } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;保存到MySQL失败: $tableKey\u0026#34;, e) val fallbackPath = s\u0026#34;${ConfigManager.getFallbackDataPath}/analysis/$tableKey\u0026#34; df.write.mode(\u0026#34;overwrite\u0026#34;).parquet(fallbackPath) logger.info(s\u0026#34;降级：分析结果已保存到HDFS: $fallbackPath\u0026#34;) } } }\r3.3 用户画像分析\r#\rpackage com.sparklearning.auto.analysis import com.sparklearning.auto.config.ConfigManager import org.apache.spark.sql.SparkSession import org.apache.spark.sql.functions._ import org.slf4j.{Logger, LoggerFactory} /** * 用户画像分析 - 客户维度深度分析 * * 分析内容： * 1. 客户年龄分布 * 2. 客户收入与购车偏好 * 3. 支付方式偏好分析 * 4. 地区客户特征分析 */ object CustomerPortraitAnalysis { private val logger: Logger = LoggerFactory.getLogger(CustomerPortraitAnalysis.getClass) def main(args: Array[String]): Unit = { try { val spark = SparkSession.builder() .appName(\u0026#34;CustomerPortraitAnalysis\u0026#34;) .master(ConfigManager.getSparkMaster) .config(\u0026#34;spark.sql.shuffle.partitions\u0026#34;, ConfigManager.getSparkShufflePartitions) .config(\u0026#34;spark.sql.adaptive.enabled\u0026#34;, \u0026#34;true\u0026#34;) .enableHiveSupport() .getOrCreate() import spark.implicits._ // 读取数据 val salesDF = spark.sql(\u0026#34;SELECT * FROM auto_database.sales\u0026#34;) .filter($\u0026#34;sale_price\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;sale_price\u0026#34; \u0026gt; 0) val customersDF = spark.sql(\u0026#34;SELECT * FROM auto_database.customers\u0026#34;) .filter($\u0026#34;age\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;age\u0026#34; \u0026gt;= 18 \u0026amp;\u0026amp; $\u0026#34;age\u0026#34; \u0026lt;= 100) .filter($\u0026#34;gender\u0026#34;.isNotNull) val vehiclesDF = spark.sql(\u0026#34;SELECT * FROM auto_database.vehicles\u0026#34;) .filter($\u0026#34;brand\u0026#34;.isNotNull) // 关联数据 val salesWithCustomer = salesDF .join(customersDF, Seq(\u0026#34;customer_id\u0026#34;), \u0026#34;left\u0026#34;) .filter($\u0026#34;name\u0026#34;.isNotNull) val fullDF = salesWithCustomer .join(vehiclesDF, Seq(\u0026#34;vehicle_id\u0026#34;), \u0026#34;left\u0026#34;) // ======================================== // 1. 客户年龄分布 // ======================================== val ageDistribution = fullDF .withColumn(\u0026#34;age_group\u0026#34;, when($\u0026#34;age\u0026#34; \u0026lt; 25, \u0026#34;25岁以下\u0026#34;) .when($\u0026#34;age\u0026#34; \u0026lt; 35, \u0026#34;25-34岁\u0026#34;) .when($\u0026#34;age\u0026#34; \u0026lt; 45, \u0026#34;35-44岁\u0026#34;) .when($\u0026#34;age\u0026#34; \u0026lt; 55, \u0026#34;45-54岁\u0026#34;) .otherwise(\u0026#34;55岁以上\u0026#34;) ) .groupBy($\u0026#34;age_group\u0026#34;) .agg( count(\u0026#34;*\u0026#34;).as(\u0026#34;customer_count\u0026#34;), avg($\u0026#34;sale_price\u0026#34;).as(\u0026#34;avg_purchase_price\u0026#34;), round($\u0026#34;customer_count\u0026#34; / sum(\u0026#34;customer_count\u0026#34;).over() * 100, 2).as(\u0026#34;percentage\u0026#34;) ) .orderBy($\u0026#34;customer_count\u0026#34;.desc) println(\u0026#34;=== 客户年龄分布 ===\u0026#34;) ageDistribution.show(false) // ======================================== // 2. 客户收入与购车偏好 // ======================================== val incomePreference = fullDF .filter($\u0026#34;income\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;income\u0026#34; \u0026gt; 0) .withColumn(\u0026#34;income_level\u0026#34;, when($\u0026#34;income\u0026#34; \u0026lt; 100000, \u0026#34;10万以下\u0026#34;) .when($\u0026#34;income\u0026#34; \u0026lt; 300000, \u0026#34;10-30万\u0026#34;) .when($\u0026#34;income\u0026#34; \u0026lt; 500000, \u0026#34;30-50万\u0026#34;) .when($\u0026#34;income\u0026#34; \u0026lt; 1000000, \u0026#34;50-100万\u0026#34;) .otherwise(\u0026#34;100万以上\u0026#34;) ) .groupBy($\u0026#34;income_level\u0026#34;, $\u0026#34;brand\u0026#34;) .agg(count(\u0026#34;*\u0026#34;).as(\u0026#34;purchase_count\u0026#34;)) .withColumn(\u0026#34;rank\u0026#34;, row_number().over( org.apache.spark.sql.expressions.Window .partitionBy($\u0026#34;income_level\u0026#34;) .orderBy($\u0026#34;purchase_count\u0026#34;.desc) )) .filter($\u0026#34;rank\u0026#34; \u0026lt;= 3) .orderBy($\u0026#34;income_level\u0026#34;, $\u0026#34;rank\u0026#34;) println(\u0026#34;=== 各收入水平Top3偏好品牌 ===\u0026#34;) incomePreference.show(false) // ======================================== // 3. 性别与购车偏好 // ======================================== val genderPreference = fullDF .groupBy($\u0026#34;gender\u0026#34;, $\u0026#34;brand\u0026#34;) .agg( count(\u0026#34;*\u0026#34;).as(\u0026#34;purchase_count\u0026#34;), avg($\u0026#34;sale_price\u0026#34;).as(\u0026#34;avg_price\u0026#34;) ) .withColumn(\u0026#34;rank\u0026#34;, row_number().over( org.apache.spark.sql.expressions.Window .partitionBy($\u0026#34;gender\u0026#34;) .orderBy($\u0026#34;purchase_count\u0026#34;.desc) )) .filter($\u0026#34;rank\u0026#34; \u0026lt;= 5) .orderBy($\u0026#34;gender\u0026#34;, $\u0026#34;rank\u0026#34;) println(\u0026#34;=== 性别与购车偏好Top5 ===\u0026#34;) genderPreference.show(false) // ======================================== // 4. 地区客户特征分析 // ======================================== val regionCustomerProfile = fullDF .groupBy($\u0026#34;region\u0026#34;) .agg( count(\u0026#34;*\u0026#34;).as(\u0026#34;total_sales\u0026#34;), avg($\u0026#34;sale_price\u0026#34;).as(\u0026#34;avg_price\u0026#34;), avg($\u0026#34;age\u0026#34;).as(\u0026#34;avg_age\u0026#34;), avg($\u0026#34;income\u0026#34;).as(\u0026#34;avg_income\u0026#34;), round(sum(when($\u0026#34;payment_method\u0026#34; === \u0026#34;贷款\u0026#34;, 1).otherwise(0)) / count(\u0026#34;*\u0026#34;) * 100, 2).as(\u0026#34;loan_rate\u0026#34;) ) .orderBy($\u0026#34;total_sales\u0026#34;.desc) println(\u0026#34;=== 地区客户特征分析 ===\u0026#34;) regionCustomerProfile.show(false) // 保存结果 saveToMySQL(ageDistribution, \u0026#34;customer-age-distribution\u0026#34;) saveToMySQL(incomePreference, \u0026#34;income-brand-preference\u0026#34;) saveToMySQL(regionCustomerProfile, \u0026#34;region-customer-profile\u0026#34;) } catch { case e: Exception =\u0026gt; logger.error(\u0026#34;用户画像分析运行异常\u0026#34;, e) throw e } finally { val s = SparkSession.getActiveSession.orNull if (s != null) { s.stop() logger.info(\u0026#34;SparkSession已关闭\u0026#34;) } } } private def saveToMySQL(df: org.apache.spark.sql.DataFrame, tableKey: String): Unit = { try { val tableName = ConfigManager.getDatabaseTable(tableKey) val mysqlOptions = ConfigManager.getMySQLOptions(tableName) df.write.format(\u0026#34;jdbc\u0026#34;).options(mysqlOptions).mode(\u0026#34;overwrite\u0026#34;).save() logger.info(s\u0026#34;分析结果已保存到MySQL表: $tableName\u0026#34;) } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;保存到MySQL失败: $tableKey\u0026#34;, e) val fallbackPath = s\u0026#34;${ConfigManager.getFallbackDataPath}/analysis/$tableKey\u0026#34; df.write.mode(\u0026#34;overwrite\u0026#34;).parquet(fallbackPath) logger.info(s\u0026#34;降级：分析结果已保存到HDFS: $fallbackPath\u0026#34;) } } }\r3.4 传感器数据分析\r#\rpackage com.sparklearning.auto.analysis import com.sparklearning.auto.config.ConfigManager import com.sparklearning.auto.dataaccess.DataAccessLayer import org.apache.spark.sql.SparkSession import org.apache.spark.sql.functions._ import org.apache.hadoop.hbase.io.ImmutableBytesWritable import org.apache.hadoop.hbase.mapreduce.TableInputFormat import org.apache.spark.rdd.RDD import org.slf4j.{Logger, LoggerFactory} /** * 传感器数据分析 - 车辆传感器数据深度分析 * * 分析内容： * 1. 异常传感器统计 * 2. 传感器值趋势分析 * 3. 车辆健康评分 * 4. 传感器异常率分析 * * 企业级改进： * 1. HBase配置从ConfigManager获取 * 2. try-finally确保SparkSession释放 * 3. MySQL配置从ConfigManager获取 * 4. 数据验证和空值处理 */ object SensorAnalysis { private val logger: Logger = LoggerFactory.getLogger(SensorAnalysis.getClass) def main(args: Array[String]): Unit = { try { val spark = SparkSession.builder() .appName(\u0026#34;SensorAnalysis\u0026#34;) .master(ConfigManager.getSparkMaster) .config(\u0026#34;spark.sql.shuffle.partitions\u0026#34;, ConfigManager.getSparkShufflePartitions) .config(\u0026#34;spark.sql.adaptive.enabled\u0026#34;, \u0026#34;true\u0026#34;) .getOrCreate() // 配置HBase（从ConfigManager获取） val hbaseConfig = ConfigManager.getHBaseConfig hbaseConfig.set(TableInputFormat.INPUT_TABLE, ConfigManager.getHBaseTableName(\u0026#34;sensor-data\u0026#34;)) // 从HBase读取数据 val hbaseRDD: RDD[(ImmutableBytesWritable, org.apache.hadoop.hbase.client.Result)] = spark.sparkContext.newAPIHadoopRDD( hbaseConfig, classOf[TableInputFormat], classOf[ImmutableBytesWritable], classOf[org.apache.hadoop.hbase.client.Result] ) // 转换为DataFrame（添加空值处理） val sensorDF = hbaseRDD.flatMap { case (_, result) =\u0026gt; try { val rowKey = new String(result.getRow) val vehicleIdBytes = result.getValue(\u0026#34;cf\u0026#34;.getBytes, \u0026#34;vehicle_id\u0026#34;.getBytes) val sensorTypeBytes = result.getValue(\u0026#34;cf\u0026#34;.getBytes, \u0026#34;sensor_type\u0026#34;.getBytes) val valueBytes = result.getValue(\u0026#34;cf\u0026#34;.getBytes, \u0026#34;value\u0026#34;.getBytes) val statusBytes = result.getValue(\u0026#34;cf\u0026#34;.getBytes, \u0026#34;status\u0026#34;.getBytes) // 空值检查 if (vehicleIdBytes == null || sensorTypeBytes == null || valueBytes == null || statusBytes == null) { logger.warn(s\u0026#34;跳过空值记录: rowKey=$rowKey\u0026#34;) None } else { val vehicleId = new String(vehicleIdBytes).toInt val sensorType = new String(sensorTypeBytes) val value = new String(valueBytes).toDouble val status = new String(statusBytes) val timestamp = rowKey.split(\u0026#34;-\u0026#34;)(0).toLong // 数据验证 if (DataAccessLayer.validateSensorValue(sensorType, value)) { Some((vehicleId, sensorType, value, status, timestamp)) } else { logger.warn(s\u0026#34;跳过无效传感器值: type=$sensorType, value=$value\u0026#34;) None } } } catch { case e: Exception =\u0026gt; logger.warn(\u0026#34;解析HBase记录失败\u0026#34;, e) None } }.toDF(\u0026#34;vehicle_id\u0026#34;, \u0026#34;sensor_type\u0026#34;, \u0026#34;value\u0026#34;, \u0026#34;status\u0026#34;, \u0026#34;timestamp\u0026#34;) // ======================================== // 1. 异常传感器统计 // ======================================== val abnormalSensors = sensorDF .filter($\u0026#34;status\u0026#34; === \u0026#34;异常\u0026#34;) .groupBy($\u0026#34;sensor_type\u0026#34;) .agg( count(\u0026#34;*\u0026#34;).as(\u0026#34;abnormal_count\u0026#34;), avg($\u0026#34;value\u0026#34;).as(\u0026#34;avg_abnormal_value\u0026#34;) ) .orderBy($\u0026#34;abnormal_count\u0026#34;.desc) println(\u0026#34;=== 异常传感器统计 ===\u0026#34;) abnormalSensors.show(false) // ======================================== // 2. 传感器值统计（正常vs异常对比） // ======================================== val sensorStats = sensorDF .groupBy($\u0026#34;sensor_type\u0026#34;, $\u0026#34;status\u0026#34;) .agg( avg($\u0026#34;value\u0026#34;).as(\u0026#34;avg_value\u0026#34;), stddev($\u0026#34;value\u0026#34;).as(\u0026#34;stddev_value\u0026#34;), max($\u0026#34;value\u0026#34;).as(\u0026#34;max_value\u0026#34;), min($\u0026#34;value\u0026#34;).as(\u0026#34;min_value\u0026#34;), count(\u0026#34;*\u0026#34;).as(\u0026#34;count\u0026#34;) ) .orderBy($\u0026#34;sensor_type\u0026#34;, $\u0026#34;status\u0026#34;) println(\u0026#34;=== 传感器值统计（正常vs异常） ===\u0026#34;) sensorStats.show(false) // ======================================== // 3. 车辆健康评分 // ======================================== val vehicleHealthScore = sensorDF .groupBy($\u0026#34;vehicle_id\u0026#34;) .agg( count(\u0026#34;*\u0026#34;).as(\u0026#34;total_readings\u0026#34;), sum(when($\u0026#34;status\u0026#34; === \u0026#34;异常\u0026#34;, 1).otherwise(0)).as(\u0026#34;abnormal_count\u0026#34;) ) .withColumn(\u0026#34;abnormal_rate\u0026#34;, $\u0026#34;abnormal_count\u0026#34; / $\u0026#34;total_readings\u0026#34;) .withColumn(\u0026#34;health_score\u0026#34;, when($\u0026#34;abnormal_rate\u0026#34; \u0026lt; 0.05, 100) .when($\u0026#34;abnormal_rate\u0026#34; \u0026lt; 0.10, 90) .when($\u0026#34;abnormal_rate\u0026#34; \u0026lt; 0.20, 80) .when($\u0026#34;abnormal_rate\u0026#34; \u0026lt; 0.30, 70) .when($\u0026#34;abnormal_rate\u0026#34; \u0026lt; 0.50, 60) .otherwise(50) ) .orderBy($\u0026#34;health_score\u0026#34;.asc) println(\u0026#34;=== 车辆健康评分（按评分升序） ===\u0026#34;) vehicleHealthScore.show(20, false) // ======================================== // 4. 传感器异常率排名 // ======================================== val sensorAbnormalRate = sensorDF .groupBy($\u0026#34;sensor_type\u0026#34;) .agg( count(\u0026#34;*\u0026#34;).as(\u0026#34;total_count\u0026#34;), sum(when($\u0026#34;status\u0026#34; === \u0026#34;异常\u0026#34;, 1).otherwise(0)).as(\u0026#34;abnormal_count\u0026#34;) ) .withColumn(\u0026#34;abnormal_rate\u0026#34;, round($\u0026#34;abnormal_count\u0026#34; / $\u0026#34;total_count\u0026#34; * 100, 2) ) .orderBy($\u0026#34;abnormal_rate\u0026#34;.desc) println(\u0026#34;=== 传感器异常率排名 ===\u0026#34;) sensorAbnormalRate.show(false) // ======================================== // 保存分析结果到MySQL // ======================================== saveToMySQL(abnormalSensors, \u0026#34;sensor-abnormal-analysis\u0026#34;) saveToMySQL(sensorStats, \u0026#34;sensor-stats\u0026#34;) saveToMySQL(vehicleHealthScore, \u0026#34;vehicle-health-score\u0026#34;) saveToMySQL(sensorAbnormalRate, \u0026#34;sensor-abnormal-rate\u0026#34;) } catch { case e: Exception =\u0026gt; logger.error(\u0026#34;传感器分析运行异常\u0026#34;, e) throw e } finally { val s = SparkSession.getActiveSession.orNull if (s != null) { s.stop() logger.info(\u0026#34;SparkSession已关闭\u0026#34;) } } } private def saveToMySQL(df: org.apache.spark.sql.DataFrame, tableKey: String): Unit = { try { val tableName = ConfigManager.getDatabaseTable(tableKey) val mysqlOptions = ConfigManager.getMySQLOptions(tableName) df.write.format(\u0026#34;jdbc\u0026#34;).options(mysqlOptions).mode(\u0026#34;overwrite\u0026#34;).save() logger.info(s\u0026#34;分析结果已保存到MySQL表: $tableName\u0026#34;) } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;保存到MySQL失败: $tableKey\u0026#34;, e) val fallbackPath = s\u0026#34;${ConfigManager.getFallbackDataPath}/analysis/$tableKey\u0026#34; df.write.mode(\u0026#34;overwrite\u0026#34;).parquet(fallbackPath) logger.info(s\u0026#34;降级：分析结果已保存到HDFS: $fallbackPath\u0026#34;) } } }\r3.5 竞争力分析\r#\rpackage com.sparklearning.auto.analysis import com.sparklearning.auto.config.ConfigManager import org.apache.spark.sql.SparkSession import org.apache.spark.sql.functions._ import org.slf4j.{Logger, LoggerFactory} /** * 竞争力分析 - 品牌竞争力维度分析 * * 分析内容： * 1. 市场占有率分析 * 2. 品牌排名分析 * 3. 地区竞争力对比 */ object CompetitivenessAnalysis { private val logger: Logger = LoggerFactory.getLogger(CompetitivenessAnalysis.getClass) def main(args: Array[String]): Unit = { try { val spark = SparkSession.builder() .appName(\u0026#34;CompetitivenessAnalysis\u0026#34;) .master(ConfigManager.getSparkMaster) .config(\u0026#34;spark.sql.shuffle.partitions\u0026#34;, ConfigManager.getSparkShufflePartitions) .config(\u0026#34;spark.sql.adaptive.enabled\u0026#34;, \u0026#34;true\u0026#34;) .enableHiveSupport() .getOrCreate() import spark.implicits._ val salesDF = spark.sql(\u0026#34;SELECT * FROM auto_database.sales\u0026#34;) .filter($\u0026#34;sale_price\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;sale_price\u0026#34; \u0026gt; 0) val vehiclesDF = spark.sql(\u0026#34;SELECT * FROM auto_database.vehicles\u0026#34;) .filter($\u0026#34;brand\u0026#34;.isNotNull) val fullDF = salesDF.join(vehiclesDF, Seq(\u0026#34;vehicle_id\u0026#34;), \u0026#34;left\u0026#34;) .filter($\u0026#34;brand\u0026#34;.isNotNull) // ======================================== // 1. 市场占有率分析 // ======================================== val marketShare = fullDF .groupBy($\u0026#34;brand\u0026#34;) .agg( count(\u0026#34;*\u0026#34;).as(\u0026#34;sale_count\u0026#34;), sum($\u0026#34;sale_price\u0026#34;).as(\u0026#34;total_revenue\u0026#34;) ) .withColumn(\u0026#34;volume_share\u0026#34;, round($\u0026#34;sale_count\u0026#34; / sum(\u0026#34;sale_count\u0026#34;).over() * 100, 2)) .withColumn(\u0026#34;revenue_share\u0026#34;, round($\u0026#34;total_revenue\u0026#34; / sum(\u0026#34;total_revenue\u0026#34;).over() * 100, 2)) .withColumn(\u0026#34;volume_rank\u0026#34;, row_number().over( org.apache.spark.sql.expressions.Window.orderBy($\u0026#34;sale_count\u0026#34;.desc) )) .withColumn(\u0026#34;revenue_rank\u0026#34;, row_number().over( org.apache.spark.sql.expressions.Window.orderBy($\u0026#34;total_revenue\u0026#34;.desc) )) .orderBy($\u0026#34;volume_share\u0026#34;.desc) println(\u0026#34;=== 市场占有率分析 ===\u0026#34;) marketShare.show(false) // ======================================== // 2. 地区竞争力对比 // ======================================== val regionCompetitiveness = fullDF .groupBy($\u0026#34;region\u0026#34;, $\u0026#34;brand\u0026#34;) .agg(count(\u0026#34;*\u0026#34;).as(\u0026#34;sale_count\u0026#34;)) .withColumn(\u0026#34;market_share\u0026#34;, round($\u0026#34;sale_count\u0026#34; / sum(\u0026#34;sale_count\u0026#34;).over( org.apache.spark.sql.expressions.Window.partitionBy($\u0026#34;region\u0026#34;) ) * 100, 2) ) .withColumn(\u0026#34;rank\u0026#34;, row_number().over( org.apache.spark.sql.expressions.Window .partitionBy($\u0026#34;region\u0026#34;) .orderBy($\u0026#34;sale_count\u0026#34;.desc) )) .filter($\u0026#34;rank\u0026#34; \u0026lt;= 3) .orderBy($\u0026#34;region\u0026#34;, $\u0026#34;rank\u0026#34;) println(\u0026#34;=== 各地区Top3品牌 ===\u0026#34;) regionCompetitiveness.show(50, false) // 保存结果 saveToMySQL(marketShare, \u0026#34;vehicle-competitiveness\u0026#34;) } catch { case e: Exception =\u0026gt; logger.error(\u0026#34;竞争力分析运行异常\u0026#34;, e) throw e } finally { val s = SparkSession.getActiveSession.orNull if (s != null) { s.stop() logger.info(\u0026#34;SparkSession已关闭\u0026#34;) } } } private def saveToMySQL(df: org.apache.spark.sql.DataFrame, tableKey: String): Unit = { try { val tableName = ConfigManager.getDatabaseTable(tableKey) val mysqlOptions = ConfigManager.getMySQLOptions(tableName) df.write.format(\u0026#34;jdbc\u0026#34;).options(mysqlOptions).mode(\u0026#34;overwrite\u0026#34;).save() logger.info(s\u0026#34;分析结果已保存到MySQL表: $tableName\u0026#34;) } catch { case e: Exception =\u0026gt; logger.error(s\u0026#34;保存到MySQL失败: $tableKey\u0026#34;, e) val fallbackPath = s\u0026#34;${ConfigManager.getFallbackDataPath}/analysis/$tableKey\u0026#34; df.write.mode(\u0026#34;overwrite\u0026#34;).parquet(fallbackPath) logger.info(s\u0026#34;降级：分析结果已保存到HDFS: $fallbackPath\u0026#34;) } } }\r4. 实时流处理模块（速度层）\r#\r4.1 实时传感器告警\r#\rpackage com.sparklearning.auto.streaming import com.sparklearning.auto.config.ConfigManager import com.sparklearning.auto.dataaccess.DataAccessLayer import org.apache.spark.sql.SparkSession import org.apache.spark.sql.functions._ import org.apache.spark.sql.streaming.OutputMode import org.apache.spark.sql.types._ import org.slf4j.{Logger, LoggerFactory} /** * 实时传感器告警 - 速度层实时处理 * * 功能： * 1. 实时检测传感器异常 * 2. 窗口内异常率统计 * 3. 异常告警发送到Kafka告警主题 * 4. 降级策略：流处理失败时回退到批处理 * * 知识点应用： * - DStream微批次处理 * - 窗口计算（5秒批次，30秒窗口，10秒滑动） * - 背压机制 */ object RealtimeSensorAlert { private val logger: Logger = LoggerFactory.getLogger(RealtimeSensorAlert.getClass) // 连续失败计数器（用于降级判断） private var consecutiveFailures = 0 def main(args: Array[String]): Unit = { try { val spark = SparkSession.builder() .appName(\u0026#34;RealtimeSensorAlert\u0026#34;) .master(ConfigManager.getSparkMaster) // 启用背压机制 .config(\u0026#34;spark.streaming.backpressure.enabled\u0026#34;, \u0026#34;true\u0026#34;) .config(\u0026#34;spark.streaming.backpressure.initialRate\u0026#34;, ConfigManager.getSparkBatchDurationSeconds.toString) .config(\u0026#34;spark.streaming.kafka.maxRatePerPartition\u0026#34;, \u0026#34;100\u0026#34;) .config(\u0026#34;spark.sql.shuffle.partitions\u0026#34;, ConfigManager.getSparkShufflePartitions) .getOrCreate() import spark.implicits._ // 从Kafka读取传感器数据 val sensorStream = spark.readStream .format(\u0026#34;kafka\u0026#34;) .option(\u0026#34;kafka.bootstrap.servers\u0026#34;, ConfigManager.getKafkaBootstrapServers) .option(\u0026#34;subscribe\u0026#34;, ConfigManager.getKafkaTopic(\u0026#34;sensor-data\u0026#34;)) .option(\u0026#34;startingOffsets\u0026#34;, \u0026#34;latest\u0026#34;) .load() // 定义Schema val schema = new StructType() .add(\u0026#34;sensor_id\u0026#34;, IntegerType) .add(\u0026#34;vehicle_id\u0026#34;, IntegerType) .add(\u0026#34;timestamp\u0026#34;, LongType) .add(\u0026#34;sensor_type\u0026#34;, StringType) .add(\u0026#34;value\u0026#34;, DoubleType) .add(\u0026#34;status\u0026#34;, StringType) // 解析JSON + 数据验证 val parsedStream = sensorStream .selectExpr(\u0026#34;CAST(value AS STRING)\u0026#34;, \u0026#34;timestamp as kafka_timestamp\u0026#34;) .select(from_json($\u0026#34;value\u0026#34;, schema).as(\u0026#34;data\u0026#34;), $\u0026#34;kafka_timestamp\u0026#34;) .select(\u0026#34;data.*\u0026#34;, \u0026#34;kafka_timestamp\u0026#34;) .filter($\u0026#34;data\u0026#34;.isNotNull) .filter($\u0026#34;sensor_id\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;vehicle_id\u0026#34;.isNotNull) .filter($\u0026#34;sensor_type\u0026#34;.isNotNull \u0026amp;\u0026amp; $\u0026#34;value\u0026#34;.isNotNull) .withColumn(\u0026#34;event_time\u0026#34;, to_timestamp($\u0026#34;timestamp\u0026#34; / 1000)) .withWatermark(\u0026#34;event_time\u0026#34;, \u0026#34;10 seconds\u0026#34;) // ======================================== // 1. 实时异常检测（状态过滤） // ======================================== val abnormalStream = parsedStream .filter($\u0026#34;status\u0026#34; === \u0026#34;异常\u0026#34;) // ======================================== // 2. 窗口内异常率统计 // 窗口长度30秒，滑动步长10秒 // ======================================== val windowedAbnormalRate = parsedStream .groupBy( window($\u0026#34;event_time\u0026#34;, \u0026#34;30 seconds\u0026#34;, \u0026#34;10 seconds\u0026#34;), $\u0026#34;sensor_type\u0026#34; ) .agg( count(\u0026#34;*\u0026#34;).as(\u0026#34;total_count\u0026#34;), sum(when($\u0026#34;status\u0026#34; === \u0026#34;异常\u0026#34;, 1).otherwise(0)).as(\u0026#34;abnormal_count\u0026#34;) ) .withColumn(\u0026#34;abnormal_rate\u0026#34;, round($\u0026#34;abnormal_count\u0026#34; / $\u0026#34;total_count\u0026#34; * 100, 2) ) .filter($\u0026#34;abnormal_rate\u0026#34; \u0026gt; 10) // 异常率超过10%触发告警 // ======================================== // 3. 车辆级别异常聚合 // ======================================== val vehicleAbnormal = abnormalStream .groupBy( window($\u0026#34;event_time\u0026#34;, \u0026#34;30 seconds\u0026#34;, \u0026#34;10 seconds\u0026#34;), $\u0026#34;vehicle_id\u0026#34; ) .agg( count(\u0026#34;*\u0026#34;).as(\u0026#34;abnormal_count\u0026#34;), collect_set($\u0026#34;sensor_type\u0026#34;).as(\u0026#34;abnormal_sensor_types\u0026#34;) ) .filter($\u0026#34;abnormal_count\u0026#34; \u0026gt;= 3) // 单车30秒内3次以上异常触发告警 // ======================================== // 输出1：异常告警写入Kafka告警主题 // ======================================== val alertQuery = windowedAbnormalRate .select( to_json(struct( $\u0026#34;window\u0026#34;, $\u0026#34;sensor_type\u0026#34;, $\u0026#34;total_count\u0026#34;, $\u0026#34;abnormal_count\u0026#34;, $\u0026#34;abnormal_rate\u0026#34; )).as(\u0026#34;value\u0026#34;) ) .writeStream .format(\u0026#34;kafka\u0026#34;) .option(\u0026#34;kafka.bootstrap.servers\u0026#34;, ConfigManager.getKafkaBootstrapServers) .option(\u0026#34;topic\u0026#34;, ConfigManager.getKafkaTopic(\u0026#34;alert-data\u0026#34;)) .option(\u0026#34;checkpointLocation\u0026#34;, ConfigManager.getSparkCheckpointDir + \u0026#34;/alert-to-kafka\u0026#34;) .outputMode(OutputMode.Update()) .start() // ======================================== // 输出2：异常率统计写入MySQL // ======================================== val mysqlQuery = windowedAbnormalRate.writeStream .foreachBatch { (batchDF: org.apache.spark.sql.DataFrame, batchId: Long) =\u0026gt; try { val tableName = ConfigManager.getDatabaseTable(\u0026#34;sensor-abnormal-analysis\u0026#34;) val mysqlOptions = ConfigManager.getMySQLOptions(tableName) batchDF.write .format(\u0026#34;jdbc\u0026#34;) .options(mysqlOptions) .mode(\u0026#34;append\u0026#34;) .save() consecutiveFailures = 0 // 重置失败计数 logger.info(s\u0026#34;实时告警批次 $batchId 写入MySQL成功\u0026#34;) } catch { case e: Exception =\u0026gt; consecutiveFailures += 1 logger.error(s\u0026#34;实时告警批次 $batchId 写入MySQL失败(连续失败: $consecutiveFailures)\u0026#34;, e) // 降级判断 if (consecutiveFailures \u0026gt;= ConfigManager.getBatchFallbackThreshold) { logger.warn(s\u0026#34;连续失败次数达到阈值(${ConfigManager.getBatchFallbackThreshold})，建议切换到批处理模式\u0026#34;) } // 降级：暂存到HDFS val fallbackPath = s\u0026#34;${ConfigManager.getFallbackDataPath}/realtime-alert/batch_$batchId\u0026#34; batchDF.write.mode(\u0026#34;overwrite\u0026#34;).json(fallbackPath) } } .outputMode(OutputMode.Update()) .option(\u0026#34;checkpointLocation\u0026#34;, ConfigManager.getSparkCheckpointDir + \u0026#34;/alert-to-mysql\u0026#34;) .start() // ======================================== // 输出3：控制台输出（调试用） // ======================================== val consoleQuery = vehicleAbnormal.writeStream .format(\u0026#34;console\u0026#34;) .option(\u0026#34;truncate\u0026#34;, \u0026#34;false\u0026#34;) .outputMode(OutputMode.Update()) .start() spark.streams.awaitAnyTermination() } catch { case e: Exception =\u0026gt; logger.error(\u0026#34;实时传感器告警运行异常\u0026#34;, e) throw e } finally { val s = SparkSession.getActiveSession.orNull if (s != null) { s.stop() logger.info(\u0026#34;SparkSession已关闭\u0026#34;) } } } }\r5. 降级策略模块\r#\rpackage com.sparklearning.auto.degradation import com.sparklearning.auto.config.ConfigManager import org.apache.spark.sql.SparkSession import org.apache.spark.sql.functions._ import org.slf4j.{Logger, LoggerFactory} /** * 降级策略管理器 * * 降级策略设计： * 1. 实时处理连续失败超过阈值时，自动切换到批处理模式 * 2. 降级期间数据暂存到HDFS，等待恢复后补算 * 3. 提供手动恢复和自动恢复两种模式 * * 降级流程： * 实时处理正常 --\u0026gt; 实时处理异常 --\u0026gt; 重试(N次) --\u0026gt; 降级到批处理 --\u0026gt; 恢复检测 --\u0026gt; 恢复实时处理 */ object DegradationManager { private val logger: Logger = LoggerFactory.getLogger(DegradationManager.getClass) /** * 执行降级批处理 - 处理暂存到HDFS的降级数据 * * 当实时处理恢复正常后，调用此方法补算降级期间的数据 */ def processFallbackData(): Unit = { if (!ConfigManager.isDegradationEnabled) { logger.info(\u0026#34;降级策略未启用，跳过降级数据处理\u0026#34;) return } try { val spark = SparkSession.builder() .appName(\u0026#34;DegradationBatchProcessor\u0026#34;) .master(ConfigManager.getSparkMaster) .config(\u0026#34;spark.sql.shuffle.partitions\u0026#34;, ConfigManager.getSparkShufflePartitions) .getOrCreate() val fallbackBasePath = ConfigManager.getFallbackDataPath // 处理传感器降级数据 processSensorFallbackData(spark, fallbackBasePath) // 处理销售降级数据 processSalesFallbackData(spark, fallbackBasePath) logger.info(\u0026#34;降级数据处理完成\u0026#34;) } catch { case e: Exception =\u0026gt; logger.error(\u0026#34;降级数据处理异常\u0026#34;, e) throw e } finally { val s = SparkSession.getActiveSession.orNull if (s != null) { s.stop() } } } /** * 处理传感器降级数据 */ private def processSensorFallbackData(spark: SparkSession, basePath: String): Unit = { val sensorFallbackPath = s\u0026#34;$basePath/sensor_data\u0026#34; try { val sensorDF = spark.read.json(sensorFallbackPath) if (sensorDF.count() \u0026gt; 0) { logger.info(s\u0026#34;发现传感器降级数据: ${sensorDF.count()} 条\u0026#34;) // 写入HBase val tableName = ConfigManager.getHBaseTableName(\u0026#34;sensor-data\u0026#34;) val hbaseConfig = ConfigManager.getHBaseConfig hbaseConfig.set(\u0026#34;hbase.mapreduce.inputtable\u0026#34;, tableName) // 使用DataAccessLayer写入 val records = sensorDF.collect().flatMap { row =\u0026gt; try { val sensorType = row.getAs[String](\u0026#34;sensor_type\u0026#34;) val value = row.getAs[Double](\u0026#34;value\u0026#34;) if (sensorType != null \u0026amp;\u0026amp; value != null) { val rowKey = s\u0026#34;${row.getAs[Long](\u0026#34;timestamp\u0026#34;)}-${row.getAs[Int](\u0026#34;sensor_id\u0026#34;)}\u0026#34; val columns = Seq( (\u0026#34;vehicle_id\u0026#34;, row.getAs[Int](\u0026#34;vehicle_id\u0026#34;).asInstanceOf[Any]), (\u0026#34;sensor_type\u0026#34;, sensorType.asInstanceOf[Any]), (\u0026#34;value\u0026#34;, value.asInstanceOf[Any]), (\u0026#34;status\u0026#34;, row.getAs[String](\u0026#34;status\u0026#34;).asInstanceOf[Any]) ) Some((rowKey, columns)) } else None } catch { case _: Exception =\u0026gt; None } } if (records.nonEmpty) { val result = com.sparklearning.auto.dataaccess.DataAccessLayer.batchInsertToHBase( \u0026#34;sensor-data\u0026#34;, \u0026#34;cf\u0026#34;, records ) result match { case scala.util.Success(count) =\u0026gt; logger.info(s\u0026#34;降级传感器数据已写入HBase: $count 条\u0026#34;) case scala.util.Failure(e) =\u0026gt; logger.error(\u0026#34;降级传感器数据写入HBase失败\u0026#34;, e) } } } } catch { case e: Exception =\u0026gt; logger.warn(\u0026#34;未找到传感器降级数据或处理失败\u0026#34;, e) } } /** * 处理销售降级数据 */ private def processSalesFallbackData(spark: SparkSession, basePath: String): Unit = { val salesFallbackPath = s\u0026#34;$basePath/sales_data\u0026#34; try { val salesDF = spark.read.json(salesFallbackPath) if (salesDF.count() \u0026gt; 0) { logger.info(s\u0026#34;发现销售降级数据: ${salesDF.count()} 条\u0026#34;) val tableName = ConfigManager.getDatabaseTable(\u0026#34;sales\u0026#34;) val mysqlOptions = ConfigManager.getMySQLOptions(tableName) salesDF.write .format(\u0026#34;jdbc\u0026#34;) .options(mysqlOptions) .mode(\u0026#34;append\u0026#34;) .save() logger.info(\u0026#34;降级销售数据已写入MySQL\u0026#34;) } } catch { case e: Exception =\u0026gt; logger.warn(\u0026#34;未找到销售降级数据或处理失败\u0026#34;, e) } } /** * 带重试的操作执行器 * * @param operation 要执行的操作 * @param maxRetries 最大重试次数 * @param retryIntervalMs 重试间隔(毫秒) * @tparam T 返回值类型 * @return 操作结果 */ def withRetry[T](operation: =\u0026gt; T, maxRetries: Int = ConfigManager.getMaxRetries, retryIntervalMs: Long = ConfigManager.getRetryIntervalMs): Try[T] = { var lastException: Option[Exception] = None var attempt = 0 while (attempt \u0026lt; maxRetries) { try { return scala.util.Success(operation) } catch { case e: Exception =\u0026gt; lastException = Some(e) attempt += 1 logger.warn(s\u0026#34;操作失败，第 $attempt 次重试(共 $maxRetries 次)\u0026#34;, e) if (attempt \u0026lt; maxRetries) { Thread.sleep(retryIntervalMs) } } } scala.util.Failure(lastException.getOrElse( new RuntimeException(s\u0026#34;操作在 $maxRetries 次重试后仍失败\u0026#34;) )) } }\r6. 可视化模块\r#\r6.1 Grafana仪表板\r#\r配置Grafana连接MySQL和HBase数据源，创建以下仪表板：\n销售概览仪表板 总销售额、销售数量、平均售价 按地区销售分布 销售趋势图表 支付方式分布 传感器监控仪表板 异常传感器数量 传感器值实时监控 车辆状态分布 传感器类型分布 用户画像仪表板 客户年龄分布 收入与购车偏好 地区客户特征 竞争力仪表板 品牌市场份额 地区品牌排名 价格竞争力对比 6.2 数据可视化代码\r#\r# dashboard.py # 注意：数据库密码通过环境变量获取，不硬编码 import os import pandas as pd import matplotlib.pyplot as plt import seaborn as sns import mysql.connector # 从环境变量获取数据库配置 MYSQL_HOST = os.environ.get(\u0026#39;MYSQL_HOST\u0026#39;, \u0026#39;localhost\u0026#39;) MYSQL_USER = os.environ.get(\u0026#39;MYSQL_USER\u0026#39;, \u0026#39;auto_admin\u0026#39;) MYSQL_PASSWORD = os.environ.get(\u0026#39;MYSQL_PASSWORD\u0026#39;, \u0026#39;\u0026#39;) MYSQL_DATABASE = os.environ.get(\u0026#39;MYSQL_DATABASE\u0026#39;, \u0026#39;auto_database\u0026#39;) # 连接MySQL（密码从环境变量获取） conn = mysql.connector.connect( host=MYSQL_HOST, user=MYSQL_USER, password=MYSQL_PASSWORD, database=MYSQL_DATABASE ) try: # 读取销售数据 sales_df = pd.read_sql(\u0026#34;SELECT * FROM sales\u0026#34;, conn) # 读取传感器异常数据 sensor_df = pd.read_sql(\u0026#34;SELECT * FROM sensor_abnormal_analysis\u0026#34;, conn) # 设置中文字体 plt.rcParams[\u0026#39;font.sans-serif\u0026#39;] = [\u0026#39;SimHei\u0026#39;] plt.rcParams[\u0026#39;axes.unicode_minus\u0026#39;] = False # 1. 销售趋势图 fig, axes = plt.subplots(2, 1, figsize=(12, 8)) sales_df[\u0026#39;sale_date\u0026#39;] = pd.to_datetime(sales_df[\u0026#39;sale_date\u0026#39;]) sales_by_month = sales_df.groupby(sales_df[\u0026#39;sale_date\u0026#39;].dt.to_period(\u0026#39;M\u0026#39;)).agg({ \u0026#39;sale_price\u0026#39;: \u0026#39;sum\u0026#39;, \u0026#39;sale_id\u0026#39;: \u0026#39;count\u0026#39; }) sales_by_month[\u0026#39;sale_price\u0026#39;].plot(kind=\u0026#39;bar\u0026#39;, ax=axes[0], title=\u0026#39;月度销售额\u0026#39;) sales_by_month[\u0026#39;sale_id\u0026#39;].plot(kind=\u0026#39;bar\u0026#39;, ax=axes[1], title=\u0026#39;月度销量\u0026#39;) plt.tight_layout() plt.savefig(\u0026#39;sales_trend.png\u0026#39;) # 2. 地区销售分布图 plt.figure(figsize=(12, 6)) region_sales = sales_df.groupby(\u0026#39;region\u0026#39;)[\u0026#39;sale_price\u0026#39;].sum().sort_values(ascending=False) region_sales.plot(kind=\u0026#39;pie\u0026#39;, autopct=\u0026#39;%1.1f%%\u0026#39;) plt.title(\u0026#39;地区销售分布\u0026#39;) plt.savefig(\u0026#39;region_sales.png\u0026#39;) # 3. 传感器异常分布图 plt.figure(figsize=(12, 6)) sns.barplot(x=\u0026#39;sensor_type\u0026#39;, y=\u0026#39;abnormal_count\u0026#39;, data=sensor_df) plt.xticks(rotation=45) plt.title(\u0026#39;传感器异常分布\u0026#39;) plt.tight_layout() plt.savefig(\u0026#39;sensor_abnormal.png\u0026#39;) finally: conn.close()\r系统部署与监控\r#\r1. 环境搭建\r#\r1.1 基础环境要求\r#\r组件 版本 最低配置 推荐配置 JDK 11 - OpenJDK 11.0.20+ Scala 2.13.8 - SDKMAN安装 Hadoop 3.3.6 3节点, 8GB/节点 5节点, 16GB/节点 Spark 3.5.8 3节点, 8GB/节点 5节点, 16GB/节点 Kafka 3.7.0 3 Broker 5 Broker HBase 2.5.9 3节点 5节点 MySQL 8.0 4GB 8GB 1.2 环境变量配置\r#\r部署前需设置以下环境变量：\n# MySQL配置 export MYSQL_URL=\u0026#34;jdbc:mysql://your-mysql-host:3306/auto_database?useSSL=true\u0026amp;serverTimezone=Asia/Shanghai\u0026#34; export MYSQL_USER=\u0026#34;auto_admin\u0026#34; export MYSQL_PASSWORD=\u0026#34;your_secure_password\u0026#34; # Kafka配置 export KAFKA_BOOTSTRAP_SERVERS=\u0026#34;kafka1:9092,kafka2:9092,kafka3:9092\u0026#34; # HBase配置 export HBASE_ZOOKEEPER_QUORUM=\u0026#34;zk1,zk2,zk3\u0026#34; export HBASE_ZOOKEEPER_CLIENT_PORT=\u0026#34;2181\u0026#34; # Spark配置 export SPARK_MASTER=\u0026#34;spark://spark-master:7077\u0026#34; # 告警配置 export ALERT_EMAIL=\u0026#34;admin@your-company.com\u0026#34;\r1.3 数据库初始化\r#\r-- 创建数据库 CREATE DATABASE IF NOT EXISTS auto_database DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE auto_database; -- 销售数据表 CREATE TABLE IF NOT EXISTS sales ( sale_id BIGINT PRIMARY KEY, vehicle_id INT NOT NULL, customer_id INT NOT NULL, dealer_id INT NOT NULL, sale_date DATE NOT NULL, sale_price DECIMAL(12,2) NOT NULL CHECK (sale_price \u0026gt; 0), payment_method VARCHAR(50) NOT NULL, region VARCHAR(100) NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_sale_date (sale_date), INDEX idx_region (region), INDEX idx_vehicle_id (vehicle_id) ) ENGINE=InnoDB; -- 车辆数据表 CREATE TABLE IF NOT EXISTS vehicles ( vehicle_id INT PRIMARY KEY, brand VARCHAR(50) NOT NULL, model VARCHAR(100) NOT NULL, year INT CHECK (year \u0026gt;= 2000 AND year \u0026lt;= 2030), fuel_type VARCHAR(20) NOT NULL, engine_size DECIMAL(3,1) CHECK (engine_size \u0026gt; 0), horsepower INT CHECK (horsepower \u0026gt; 0), price DECIMAL(12,2) CHECK (price \u0026gt; 0), features JSON, INDEX idx_brand (brand) ) ENGINE=InnoDB; -- 客户数据表 CREATE TABLE IF NOT EXISTS customers ( customer_id INT PRIMARY KEY, name VARCHAR(100) NOT NULL, age INT CHECK (age \u0026gt;= 18 AND age \u0026lt;= 100), gender VARCHAR(10) NOT NULL, occupation VARCHAR(100), income DECIMAL(12,2) CHECK (income \u0026gt;= 0), region VARCHAR(100) NOT NULL, purchase_history JSON, INDEX idx_age (age), INDEX idx_region (region) ) ENGINE=InnoDB; -- 分析结果表 CREATE TABLE IF NOT EXISTS region_sales_analysis ( region VARCHAR(100), total_sales DECIMAL(15,2), sale_count BIGINT, avg_price DECIMAL(12,2), min_price DECIMAL(12,2), max_price DECIMAL(12,2) ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS sensor_abnormal_analysis ( sensor_type VARCHAR(50), abnormal_count BIGINT, avg_abnormal_value DOUBLE ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS brand_market_share ( brand VARCHAR(50), sale_count BIGINT, total_revenue DECIMAL(15,2), volume_share DOUBLE, revenue_share DOUBLE, volume_rank INT, revenue_rank INT ) ENGINE=InnoDB;\r2. Kafka主题创建\r#\r# 创建销售数据主题（3分区，2副本） kafka-topics.sh --create \\ --bootstrap-server localhost:9092 \\ --topic sales-data \\ --partitions 3 \\ --replication-factor 2 # 创建传感器数据主题（6分区，2副本，保留7天数据） kafka-topics.sh --create \\ --bootstrap-server localhost:9092 \\ --topic sensor-data \\ --partitions 6 \\ --replication-factor 2 \\ --config retention.ms=604800000 # 创建告警数据主题（3分区，2副本） kafka-topics.sh --create \\ --bootstrap-server localhost:9092 \\ --topic alert-data \\ --partitions 3 \\ --replication-factor 2\r3. HBase表创建\r#\r# 进入HBase Shell hbase shell # 创建传感器数据表 create \u0026#39;sensor_data\u0026#39;, {NAME =\u0026gt; \u0026#39;cf\u0026#39;, VERSIONS =\u0026gt; 1, COMPRESSION =\u0026gt; \u0026#39;SNAPPY\u0026#39;}, {SPLITS =\u0026gt; [\u0026#39;1\u0026#39;, \u0026#39;2\u0026#39;, \u0026#39;3\u0026#39;, \u0026#39;4\u0026#39;, \u0026#39;5\u0026#39;, \u0026#39;6\u0026#39;, \u0026#39;7\u0026#39;, \u0026#39;8\u0026#39;, \u0026#39;9\u0026#39;]} # 创建GPS轨迹表 create \u0026#39;gps_track\u0026#39;, {NAME =\u0026gt; \u0026#39;cf\u0026#39;, VERSIONS =\u0026gt; 1, COMPRESSION =\u0026gt; \u0026#39;SNAPPY\u0026#39;}\r4. 系统监控\r#\r4.1 Prometheus监控配置\r#\r# prometheus.yml scrape_configs: - job_name: \u0026#39;spark\u0026#39; static_configs: - targets: [\u0026#39;spark-master:4040\u0026#39;, \u0026#39;spark-worker1:4041\u0026#39;] - job_name: \u0026#39;kafka\u0026#39; static_configs: - targets: [\u0026#39;kafka1:9308\u0026#39;, \u0026#39;kafka2:9308\u0026#39;, \u0026#39;kafka3:9308\u0026#39;] - job_name: \u0026#39;hbase\u0026#39; static_configs: - targets: [\u0026#39;hbase-master:16010\u0026#39;]\r4.2 关键监控指标\r#\r监控对象 关键指标 告警阈值 Spark作业 任务执行时间 \u0026gt; 5分钟 Spark作业 任务失败率 \u0026gt; 5% Kafka 消息积压(Lag) \u0026gt; 10000条 Kafka 消费延迟 \u0026gt; 60秒 HBase 读写延迟 \u0026gt; 100ms MySQL 连接数 \u0026gt; 80%最大连接数 系统 CPU使用率 \u0026gt; 80% 系统 内存使用率 \u0026gt; 85% 系统 磁盘使用率 \u0026gt; 90% 项目实现步骤\r#\r1. 环境搭建\r#\r安装和配置JDK 11、Scala 2.13.8 部署Hadoop 3.3.6集群 部署Spark 3.5.8集群 部署Kafka 3.7.0集群 部署HBase 2.5.9集群 安装MySQL 8.0并执行初始化SQL 配置环境变量（数据库密码等敏感信息） 2. 项目构建\r#\r使用Maven构建项目：mvn clean package 上传jar包到Spark集群 验证application.conf配置正确性 3. 数据采集\r#\r启动Kafka服务和创建主题 运行SalesDataProducer生成模拟销售数据 运行SensorDataProducer生成模拟传感器数据 使用Kafka Console Consumer验证数据 4. 数据存储\r#\r运行SensorDataToHBase将传感器数据写入HBase 运行SalesDataToMySQL将销售数据写入MySQL 验证数据正确存储 测试降级策略（模拟HBase/MySQL不可用） 5. 数据分析\r#\r运行SalesAnalysis进行销量分析 运行PriceAnalysis进行价格分析 运行CustomerPortraitAnalysis进行用户画像分析 运行SensorAnalysis进行传感器分析 运行CompetitivenessAnalysis进行竞争力分析 6. 实时处理\r#\r运行RealtimeSensorAlert启动实时告警 验证窗口计算结果 测试背压机制 测试降级策略 7. 可视化展示\r#\r配置Grafana数据源 创建仪表板和图表 验证可视化效果 8. 系统监控\r#\r配置Prometheus和Grafana监控 设置告警规则 验证监控效果 项目扩展\r#\r实时推荐系统：基于客户购买历史和行为数据，使用Spark MLlib实现车辆推荐 预测分析：使用时间序列模型预测销售趋势，使用分类模型预测传感器故障 智能诊断：基于OBD诊断数据和传感器数据，实现车辆故障智能诊断 供应链优化：分析销售数据，使用优化算法优化车辆供应链 驾驶行为评分：基于GPS轨迹和加速度数据，构建驾驶行为评分模型 动态定价：基于市场供需和竞争分析，实现动态定价策略 项目总结\r#\r本项目实现了一个完整的企业级汽车大数据分析系统，涵盖了数据采集、存储、处理、分析和可视化的全流程。通过整合Spark、Hadoop、HBase、Kafka等大数据技术，系统能够实时处理和分析汽车行业的各类数据，为企业决策提供数据支持。\n系统特点\r#\r特点 实现方式 对应知识点 实时性 Kafka + Spark Streaming微批次处理 知识点3：微批次处理原理 准确性 Lambda架构批处理层全量重算 知识点2：Lambda架构 可靠性 降级策略 + 数据暂存 + 重试机制 降级策略模块 安全性 配置外部化 + 环境变量 + 数据验证 ConfigManager + DataAccessLayer 可扩展性 集群部署 + 水平扩展 + 配置驱动 application.conf 可观测性 Prometheus + Grafana + 日志体系 监控模块 关键改进对比\r#\r改进项 改进前 改进后 配置管理 硬编码在代码中 ConfigManager + application.conf + 环境变量 密码安全 password=123456硬编码 环境变量注入，配置文件中不存储明文 资源管理 无保障 try-catch-finally确保SparkSession/Connection释放 数据验证 无 空值检查、范围检查、类型检查 异常处理 简单print SLF4J日志 + 降级策略 + 重试机制 集合转换 JavaConverters（已废弃） scala.jdk.CollectionConverters 降级策略 无 HDFS暂存 + 批处理回退 + 自动恢复 分析维度 仅销量和传感器 销量/价格/用户画像/竞争力四维指标体系 技术栈 版本混乱 Spark 3.5.8 + Scala 2.13.8 + JDK 11统一 通过本项目，我们掌握了企业级大数据系统的设计和实现方法，理解了车联网数据特征、Lambda架构、Spark Streaming微批次处理原理以及汽车数据分析指标体系，为未来的大数据项目开发打下了坚实的基础。\n","date":"May 29, 2026","externalUrl":null,"permalink":"/posts/%E7%BB%BC%E5%90%88%E9%A1%B9%E7%9B%AE-1-%E6%B1%BD%E8%BD%A6%E5%A4%A7%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90%E7%B3%BB%E7%BB%9F/","section":"Posts","summary":"汽车大数据分析系统\r#\r项目概述\r#\r汽车大数据分析系统是一个综合性的企业级大数据解决方案，旨在通过收集、存储和分析汽车行业的各类数据，为汽车制造商、经销商和消费者提供有价值的洞察。本系统整合了Spark、Hadoop、Hive、HBase、Kafka等大数据技术，实现了从数据采集到分析、可视化的完整流程。\n项目采用Lambda架构实现批处理与流处理的协同，通过Spark Streaming微批次处理实现实时数据分析，结合HBase存储海量传感器时序数据、MySQL存储业务结构化数据，构建了完整的车联网数据处理平台。\n知识点讲解\r#\r知识点1：车联网数据特征\r#\r车联网（Internet of Vehicles, IoV）是物联网在交通领域的典型应用，其数据具有鲜明的行业特征。理解这些特征是设计合理数据处理架构的前提。\n数据类型\r#\r数据类型 来源 数据格式 采集频率 典型用途 传感器数据 发动机温度、机油压力、冷却液温度、轮胎压力、燃油液位、电池电压 结构化数值 1-100Hz 实时监控、故障预警 GPS轨迹数据 车载GPS模块 经纬度+时间戳 1-10Hz 路径规划、行为分析 OBD诊断数据 车载OBD-II接口 DTC故障码+参数 事件触发 故障诊断、维修预测 驾驶行为数据 加速度计、陀螺仪、方向盘传感器 多维向量 10-100Hz 驾驶评分、保险定价 数据特征\r#\r特征维度 描述 技术挑战 应对策略 高吞吐 单车每日可产生数GB数据，百万级车队每日PB级 存储、传输、处理压力巨大 分层存储、数据压缩、边缘计算 实时性 安全相关数据需毫秒级响应（如碰撞预警） 低延迟处理要求 流式计算、内存计算 多源异构 不同品牌、不同车型数据格式不统一 数据整合困难 统一数据模型、Schema Registry 时序性 数据带有严格的时间顺序 时序查询、窗口计算 时序数据库、窗口函数 价值密度低 大量正常数据中蕴含少量异常信息 有效信息提取 异常检测算法、采样策略 类比理解：车联网数据就像一个大型医院的监护系统——每张病床（每辆车）上的各种监护仪（传感器）持续产生心率、血压、血氧等数据（传感器数据），护士站（数据处理中心）需要实时监控所有病床的状态，一旦某项指标异常（故障预警），必须立即响应。而每天的例行查房记录（批处理分析）则用于长期健康趋势分析。\n","title":"汽车大数据分析系统","type":"posts"},{"content":"\r商品推荐系统\r#\r实验目的\r#\r本实验旨在通过Spark MLlib实现电商平台的企业级商品推荐系统，掌握以下核心技能：\n理解推荐系统的核心算法原理（协同过滤、基于内容、混合推荐） 实现基于ALS的协同过滤推荐算法 实现基于TF-IDF的内容推荐算法 构建加权混合推荐系统 掌握推荐系统的多维度评估方法 实现推荐系统的生产级部署方案 知识点1：推荐系统概述\r#\r1.1 什么是推荐系统\r#\r推荐系统（Recommendation System）是信息过滤系统的一个子类，用于预测用户对物品的\u0026quot;评分\u0026quot;或\u0026quot;偏好\u0026quot;。在电商场景中，推荐系统帮助用户从海量商品中发现感兴趣的内容，同时帮助平台提升转化率和用户黏性。\n推荐系统的核心目标：\n目标 说明 衡量指标 准确性 推荐的物品符合用户兴趣 RMSE、Precision、Recall 多样性 推荐列表覆盖不同类别 覆盖率、熵 新颖性 推荐用户未发现但感兴趣的物品 长尾覆盖率 实时性 及时响应用户行为变化 延迟时间 1.2 推荐系统分类\r#\r推荐系统 ├── 基于人口的统计学推荐（Demographic-based） │ └── 根据用户基本信息（年龄、性别、地区）推荐 ├── 协同过滤推荐（Collaborative Filtering） │ ├── 基于用户的协同过滤（User-CF） │ ├── 基于物品的协同过滤（Item-CF） │ └── 基于模型的协同过滤（Model-based CF，如ALS） ├── 基于内容的推荐（Content-based） │ ├── 基于物品属性（TF-IDF、Word2Vec） │ └── 基于用户画像 └── 混合推荐（Hybrid） ├── 加权混合 ├── 切换混合 ├── 特征组合 └── 级联混合\r1.3 电商推荐场景\r#\r场景 推荐策略 典型位置 首页推荐 热门+个性化混合 首页商品流 猜你喜欢 协同过滤+内容 个人中心 相似商品 基于内容 商品详情页 购后推荐 关联规则+协同过滤 订单完成页 搜索推荐 搜索词+用户画像 搜索结果页 知识点2：协同过滤与ALS算法原理\r#\r2.1 协同过滤的核心思想\r#\r协同过滤的核心假设：如果两个用户在过去对某些物品有相似的偏好，那么他们在未来对其他物品也会有相似的偏好。\n协同过滤分为两大类：\n基于用户的协同过滤（User-CF）：\n用户A → 喜欢商品 {1, 2, 3} 用户B → 喜欢商品 {1, 2, 4} 用户A和用户B相似 → 将商品4推荐给用户A\r基于物品的协同过滤（Item-CF）：\n喜欢商品1的用户 → 大多也喜欢商品2 商品1和商品2相似 → 给喜欢商品1的用户推荐商品2\r2.2 ALS算法详解\r#\rALS（Alternating Least Squares，交替最小二乘法）是Spark MLlib中实现的基于模型的协同过滤算法，属于矩阵分解方法。\n矩阵分解原理：\n用户-物品评分矩阵R（m×n）可以分解为两个低秩矩阵的乘积：\nR ≈ U × V^T 其中： R：m×n 的评分矩阵（m个用户，n个物品） U：m×k 的用户特征矩阵 V：n×k 的物品特征矩阵 k：隐含特征维度（rank参数）\rALS优化过程：\n1. 固定V，求解U：对每个用户u，最小化 Σ(r_ui - u_i · v_i)² + λ||u_i||² 2. 固定U，求解V：对每个物品i，最小化 Σ(r_ui - u_i · v_i)² + λ||v_i||² 3. 交替执行步骤1和2，直到收敛\r关键参数说明：\n参数 含义 推荐范围 说明 rank 隐含特征维度 10~200 越大模型越复杂，容易过拟合 maxIter 最大迭代次数 5~20 过多迭代可能过拟合 regParam 正则化参数 0.01~0.1 防止过拟合，越大约束越强 alpha 隐式反馈置信度 1~40 仅隐式反馈时使用 coldStartStrategy 冷启动策略 \u0026ldquo;drop\u0026rdquo;/\u0026ldquo;nan\u0026rdquo; 新用户/物品的处理方式 显式反馈 vs 隐式反馈：\n类型 数据来源 ALS方法 示例 显式反馈 用户主动评分 ALS 1~5星评分 隐式反馈 用户行为推断 ALS.trainImplicit 浏览、点击、购买 2.3 冷启动问题\r#\r冷启动是推荐系统面临的核心挑战之一：\n冷启动类型 原因 解决方案 用户冷启动 新用户无历史行为 基于人口统计学推荐、热门推荐 物品冷启动 新商品无被交互记录 基于内容推荐、相似物品推荐 系统冷启动 系统刚上线无数据 基于规则推荐、迁移学习 知识点3：基于内容的推荐原理\r#\r3.1 TF-IDF算法\r#\rTF-IDF（Term Frequency-Inverse Document Frequency）用于衡量一个词对文档的重要程度：\nTF(t, d) = 词t在文档d中出现的次数 / 文档d的总词数 IDF(t) = log(文档总数 / 包含词t的文档数) TF-IDF(t, d) = TF(t, d) × IDF(t)\rTF-IDF在推荐系统中的应用：\n将商品描述文本转化为TF-IDF特征向量 计算商品之间的余弦相似度 为用户推荐与其历史偏好商品相似的商品 3.2 余弦相似度\r#\r余弦相似度衡量两个向量的夹角余弦值，范围[-1, 1]，值越大越相似：\ncos(A, B) = (A · B) / (||A|| × ||B||) 其中： A · B = Σ(a_i × b_i) 向量点积 ||A|| = √(Σa_i²) 向量模长\r余弦相似度的优势：不受向量绝对大小影响，只关注方向，适合文本特征比较。\n技术栈\r#\r技术 版本 用途 Apache Spark 3.5.8 分布式数据处理与机器学习 Scala 2.13.8 Spark应用开发语言 Maven 3.9.6 项目构建与依赖管理 Spark MLlib 3.5.8 机器学习算法库 MySQL 8.0+ 结构化数据存储 Redis 7.0+ 推荐结果缓存 HDFS 3.3.6 模型文件存储 实验环境搭建\r#\r1. 数据库准备\r#\r创建推荐系统所需的数据库表：\nCREATE DATABASE IF NOT EXISTS recommendation_system DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE recommendation_system; -- 用户表 DROP TABLE IF EXISTS users; CREATE TABLE users ( user_id INT PRIMARY KEY COMMENT \u0026#39;用户ID\u0026#39;, username VARCHAR(50) NOT NULL COMMENT \u0026#39;用户名\u0026#39;, age INT COMMENT \u0026#39;年龄\u0026#39;, gender VARCHAR(10) COMMENT \u0026#39;性别\u0026#39;, region VARCHAR(50) COMMENT \u0026#39;地区\u0026#39;, registration_date DATE COMMENT \u0026#39;注册日期\u0026#39;, INDEX idx_region (region), INDEX idx_age (age) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=\u0026#39;用户信息表\u0026#39;; -- 商品表 DROP TABLE IF EXISTS products; CREATE TABLE products ( product_id INT PRIMARY KEY COMMENT \u0026#39;商品ID\u0026#39;, product_name VARCHAR(100) NOT NULL COMMENT \u0026#39;商品名称\u0026#39;, category VARCHAR(50) NOT NULL COMMENT \u0026#39;分类\u0026#39;, price DECIMAL(10,2) NOT NULL COMMENT \u0026#39;价格\u0026#39;, description TEXT COMMENT \u0026#39;商品描述\u0026#39;, brand VARCHAR(50) COMMENT \u0026#39;品牌\u0026#39;, status TINYINT(1) DEFAULT 1 COMMENT \u0026#39;状态(1上架/0下架)\u0026#39;, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category), INDEX idx_brand (brand), INDEX idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=\u0026#39;商品信息表\u0026#39;; -- 评分表 DROP TABLE IF EXISTS ratings; CREATE TABLE ratings ( user_id INT NOT NULL COMMENT \u0026#39;用户ID\u0026#39;, product_id INT NOT NULL COMMENT \u0026#39;商品ID\u0026#39;, rating DECIMAL(2,1) NOT NULL COMMENT \u0026#39;评分(1.0-5.0)\u0026#39;, rating_time TIMESTAMP NOT NULL COMMENT \u0026#39;评分时间\u0026#39;, PRIMARY KEY (user_id, product_id), INDEX idx_user_id (user_id), INDEX idx_product_id (product_id), INDEX idx_rating_time (rating_time), CONSTRAINT fk_rating_user FOREIGN KEY (user_id) REFERENCES users(user_id), CONSTRAINT fk_rating_product FOREIGN KEY (product_id) REFERENCES products(product_id), CONSTRAINT chk_rating CHECK (rating \u0026gt;= 1.0 AND rating \u0026lt;= 5.0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=\u0026#39;用户评分表\u0026#39;; -- 用户行为表 DROP TABLE IF EXISTS user_behavior; CREATE TABLE user_behavior ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL COMMENT \u0026#39;用户ID\u0026#39;, product_id INT NOT NULL COMMENT \u0026#39;商品ID\u0026#39;, behavior_type VARCHAR(20) NOT NULL COMMENT \u0026#39;行为类型(view/click/add_to_cart/purchase/favorite)\u0026#39;, behavior_time TIMESTAMP NOT NULL COMMENT \u0026#39;行为时间\u0026#39;, duration INT DEFAULT 0 COMMENT \u0026#39;停留时长(秒)\u0026#39;, INDEX idx_user_id (user_id), INDEX idx_product_id (product_id), INDEX idx_behavior_type (behavior_type), INDEX idx_behavior_time (behavior_time), CONSTRAINT fk_behavior_user FOREIGN KEY (user_id) REFERENCES users(user_id), CONSTRAINT fk_behavior_product FOREIGN KEY (product_id) REFERENCES products(product_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=\u0026#39;用户行为表\u0026#39;; -- 推荐结果缓存表 DROP TABLE IF EXISTS recommendation_cache; CREATE TABLE recommendation_cache ( user_id INT NOT NULL COMMENT \u0026#39;用户ID\u0026#39;, rec_type VARCHAR(20) NOT NULL COMMENT \u0026#39;推荐类型(cf/cb/hybrid)\u0026#39;, product_ids VARCHAR(500) NOT NULL COMMENT \u0026#39;推荐商品ID列表(逗号分隔)\u0026#39;, scores VARCHAR(500) COMMENT \u0026#39;推荐分数列表(逗号分隔)\u0026#39;, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, expire_time TIMESTAMP NOT NULL COMMENT \u0026#39;过期时间\u0026#39;, PRIMARY KEY (user_id, rec_type), INDEX idx_expire_time (expire_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=\u0026#39;推荐结果缓存表\u0026#39;;\r2. 数据准备\r#\r插入测试数据：\nINSERT INTO users (user_id, username, age, gender, region, registration_date) VALUES (1, \u0026#39;张三\u0026#39;, 25, \u0026#39;男\u0026#39;, \u0026#39;北京\u0026#39;, \u0026#39;2023-01-01\u0026#39;), (2, \u0026#39;李四\u0026#39;, 30, \u0026#39;女\u0026#39;, \u0026#39;上海\u0026#39;, \u0026#39;2023-01-02\u0026#39;), (3, \u0026#39;王五\u0026#39;, 28, \u0026#39;男\u0026#39;, \u0026#39;深圳\u0026#39;, \u0026#39;2023-01-03\u0026#39;), (4, \u0026#39;赵六\u0026#39;, 35, \u0026#39;女\u0026#39;, \u0026#39;杭州\u0026#39;, \u0026#39;2023-01-04\u0026#39;), (5, \u0026#39;钱七\u0026#39;, 22, \u0026#39;男\u0026#39;, \u0026#39;广州\u0026#39;, \u0026#39;2023-01-05\u0026#39;), (6, \u0026#39;孙八\u0026#39;, 27, \u0026#39;女\u0026#39;, \u0026#39;成都\u0026#39;, \u0026#39;2023-01-06\u0026#39;), (7, \u0026#39;周九\u0026#39;, 32, \u0026#39;男\u0026#39;, \u0026#39;南京\u0026#39;, \u0026#39;2023-01-07\u0026#39;), (8, \u0026#39;吴十\u0026#39;, 29, \u0026#39;女\u0026#39;, \u0026#39;武汉\u0026#39;, \u0026#39;2023-01-08\u0026#39;), (9, \u0026#39;郑十一\u0026#39;, 26, \u0026#39;男\u0026#39;, \u0026#39;西安\u0026#39;, \u0026#39;2023-01-09\u0026#39;), (10, \u0026#39;冯十二\u0026#39;, 31, \u0026#39;女\u0026#39;, \u0026#39;重庆\u0026#39;, \u0026#39;2023-01-10\u0026#39;); INSERT INTO products (product_id, product_name, category, price, description, brand) VALUES (1, \u0026#39;iPhone 15 Pro\u0026#39;, \u0026#39;手机\u0026#39;, 8999.00, \u0026#39;苹果旗舰智能手机 A17 Pro芯片 钛金属设计\u0026#39;, \u0026#39;Apple\u0026#39;), (2, \u0026#39;MacBook Pro 14\u0026#39;, \u0026#39;电脑\u0026#39;, 14999.00, \u0026#39;苹果专业笔记本电脑 M3 Pro芯片 Liquid Retina XDR显示屏\u0026#39;, \u0026#39;Apple\u0026#39;), (3, \u0026#39;AirPods Pro 2\u0026#39;, \u0026#39;耳机\u0026#39;, 1899.00, \u0026#39;苹果主动降噪无线耳机 自适应通透模式\u0026#39;, \u0026#39;Apple\u0026#39;), (4, \u0026#39;iPad Pro 12.9\u0026#39;, \u0026#39;平板\u0026#39;, 8999.00, \u0026#39;苹果专业平板电脑 M2芯片 mini-LED显示屏\u0026#39;, \u0026#39;Apple\u0026#39;), (5, \u0026#39;Apple Watch Ultra 2\u0026#39;, \u0026#39;手表\u0026#39;, 5999.00, \u0026#39;苹果极限运动手表 钛金属表壳 双频GPS\u0026#39;, \u0026#39;Apple\u0026#39;), (6, \u0026#39;华为Mate 60 Pro\u0026#39;, \u0026#39;手机\u0026#39;, 6999.00, \u0026#39;华为旗舰智能手机 麒麟9000S芯片 卫星通信\u0026#39;, \u0026#39;华为\u0026#39;), (7, \u0026#39;小米14 Pro\u0026#39;, \u0026#39;手机\u0026#39;, 4999.00, \u0026#39;小米旗舰智能手机 骁龙8 Gen3 徕卡光学\u0026#39;, \u0026#39;小米\u0026#39;), (8, \u0026#39;ThinkPad X1 Carbon\u0026#39;, \u0026#39;电脑\u0026#39;, 9999.00, \u0026#39;联想商务笔记本电脑 Intel酷睿i7 14英寸轻薄\u0026#39;, \u0026#39;联想\u0026#39;), (9, \u0026#39;索尼WH-1000XM5\u0026#39;, \u0026#39;耳机\u0026#39;, 2499.00, \u0026#39;索尼旗舰降噪头戴耳机 30小时续航\u0026#39;, \u0026#39;Sony\u0026#39;), (10, \u0026#39;三星Galaxy Tab S9\u0026#39;, \u0026#39;平板\u0026#39;, 5999.00, \u0026#39;三星旗舰平板电脑 骁龙8 Gen2 AMOLED\u0026#39;, \u0026#39;Samsung\u0026#39;), (11, \u0026#39;华为MatePad Pro\u0026#39;, \u0026#39;平板\u0026#39;, 4699.00, \u0026#39;华为旗舰平板 鸿蒙系统 OLED屏幕\u0026#39;, \u0026#39;华为\u0026#39;), (12, \u0026#39;OPPO Find X7\u0026#39;, \u0026#39;手机\u0026#39;, 3999.00, \u0026#39;OPPO旗舰智能手机 天玑9300 哈苏影像\u0026#39;, \u0026#39;OPPO\u0026#39;), (13, \u0026#39;戴尔XPS 15\u0026#39;, \u0026#39;电脑\u0026#39;, 11999.00, \u0026#39;戴尔高端笔记本电脑 Intel酷睿i7 OLED屏\u0026#39;, \u0026#39;Dell\u0026#39;), (14, \u0026#39;Bose QC45\u0026#39;, \u0026#39;耳机\u0026#39;, 1999.00, \u0026#39;Bose消噪头戴耳机 24小时续航\u0026#39;, \u0026#39;Bose\u0026#39;), (15, \u0026#39;华为Watch GT4\u0026#39;, \u0026#39;手表\u0026#39;, 1488.00, \u0026#39;华为智能手表 心率血氧监测 两周续航\u0026#39;, \u0026#39;华为\u0026#39;); INSERT INTO ratings (user_id, product_id, rating, rating_time) VALUES (1, 1, 5.0, \u0026#39;2023-02-01 10:00:00\u0026#39;), (1, 2, 5.0, \u0026#39;2023-02-02 11:00:00\u0026#39;), (1, 3, 4.0, \u0026#39;2023-02-03 12:00:00\u0026#39;), (1, 5, 4.0, \u0026#39;2023-02-04 13:00:00\u0026#39;), (2, 1, 4.0, \u0026#39;2023-02-05 14:00:00\u0026#39;), (2, 4, 5.0, \u0026#39;2023-02-06 15:00:00\u0026#39;), (2, 5, 4.0, \u0026#39;2023-02-07 16:00:00\u0026#39;), (2, 6, 3.0, \u0026#39;2023-02-08 17:00:00\u0026#39;), (3, 2, 4.0, \u0026#39;2023-02-09 18:00:00\u0026#39;), (3, 3, 5.0, \u0026#39;2023-02-10 19:00:00\u0026#39;), (3, 6, 5.0, \u0026#39;2023-02-11 20:00:00\u0026#39;), (3, 9, 4.0, \u0026#39;2023-02-12 21:00:00\u0026#39;), (4, 4, 4.0, \u0026#39;2023-02-13 22:00:00\u0026#39;), (4, 7, 5.0, \u0026#39;2023-02-14 23:00:00\u0026#39;), (4, 8, 4.0, \u0026#39;2023-02-15 00:00:00\u0026#39;), (4, 12, 3.0, \u0026#39;2023-02-16 01:00:00\u0026#39;), (5, 5, 5.0, \u0026#39;2023-02-17 02:00:00\u0026#39;), (5, 9, 4.0, \u0026#39;2023-02-18 03:00:00\u0026#39;), (5, 10, 5.0, \u0026#39;2023-02-19 04:00:00\u0026#39;), (5, 14, 4.0, \u0026#39;2023-02-20 05:00:00\u0026#39;), (6, 6, 5.0, \u0026#39;2023-02-21 06:00:00\u0026#39;), (6, 11, 4.0, \u0026#39;2023-02-22 07:00:00\u0026#39;), (6, 15, 4.0, \u0026#39;2023-02-23 08:00:00\u0026#39;), (7, 7, 5.0, \u0026#39;2023-02-24 09:00:00\u0026#39;), (7, 12, 4.0, \u0026#39;2023-02-25 10:00:00\u0026#39;), (7, 8, 3.0, \u0026#39;2023-02-26 11:00:00\u0026#39;), (8, 1, 4.0, \u0026#39;2023-02-27 12:00:00\u0026#39;), (8, 3, 5.0, \u0026#39;2023-02-28 13:00:00\u0026#39;), (8, 13, 4.0, \u0026#39;2023-03-01 14:00:00\u0026#39;), (9, 2, 5.0, \u0026#39;2023-03-02 15:00:00\u0026#39;), (9, 8, 4.0, \u0026#39;2023-03-03 16:00:00\u0026#39;), (9, 13, 5.0, \u0026#39;2023-03-04 17:00:00\u0026#39;), (10, 6, 4.0, \u0026#39;2023-03-05 18:00:00\u0026#39;), (10, 10, 3.0, \u0026#39;2023-03-06 19:00:00\u0026#39;), (10, 11, 5.0, \u0026#39;2023-03-07 20:00:00\u0026#39;), (10, 15, 4.0, \u0026#39;2023-03-08 21:00:00\u0026#39;); INSERT INTO user_behavior (user_id, product_id, behavior_type, behavior_time, duration) VALUES (1, 1, \u0026#39;view\u0026#39;, \u0026#39;2023-02-01 09:00:00\u0026#39;, 120), (1, 1, \u0026#39;add_to_cart\u0026#39;, \u0026#39;2023-02-01 09:30:00\u0026#39;, 0), (1, 1, \u0026#39;purchase\u0026#39;, \u0026#39;2023-02-01 10:00:00\u0026#39;, 0), (1, 2, \u0026#39;view\u0026#39;, \u0026#39;2023-02-02 10:30:00\u0026#39;, 180), (1, 2, \u0026#39;add_to_cart\u0026#39;, \u0026#39;2023-02-02 11:00:00\u0026#39;, 0), (1, 2, \u0026#39;purchase\u0026#39;, \u0026#39;2023-02-02 11:30:00\u0026#39;, 0), (2, 1, \u0026#39;view\u0026#39;, \u0026#39;2023-02-04 12:00:00\u0026#39;, 90), (2, 1, \u0026#39;add_to_cart\u0026#39;, \u0026#39;2023-02-04 12:30:00\u0026#39;, 0), (2, 1, \u0026#39;purchase\u0026#39;, \u0026#39;2023-02-04 13:00:00\u0026#39;, 0), (2, 4, \u0026#39;view\u0026#39;, \u0026#39;2023-02-05 13:30:00\u0026#39;, 200), (2, 4, \u0026#39;add_to_cart\u0026#39;, \u0026#39;2023-02-05 14:00:00\u0026#39;, 0), (2, 4, \u0026#39;purchase\u0026#39;, \u0026#39;2023-02-05 14:30:00\u0026#39;, 0), (3, 6, \u0026#39;view\u0026#39;, \u0026#39;2023-02-09 17:00:00\u0026#39;, 150), (3, 6, \u0026#39;favorite\u0026#39;, \u0026#39;2023-02-09 17:30:00\u0026#39;, 0), (3, 6, \u0026#39;purchase\u0026#39;, \u0026#39;2023-02-11 20:00:00\u0026#39;, 0), (4, 7, \u0026#39;view\u0026#39;, \u0026#39;2023-02-14 22:00:00\u0026#39;, 300), (4, 7, \u0026#39;add_to_cart\u0026#39;, \u0026#39;2023-02-14 22:30:00\u0026#39;, 0), (4, 7, \u0026#39;purchase\u0026#39;, \u0026#39;2023-02-14 23:00:00\u0026#39;, 0), (5, 9, \u0026#39;view\u0026#39;, \u0026#39;2023-02-18 02:00:00\u0026#39;, 240), (5, 9, \u0026#39;add_to_cart\u0026#39;, \u0026#39;2023-02-18 02:30:00\u0026#39;, 0), (5, 9, \u0026#39;purchase\u0026#39;, \u0026#39;2023-02-18 03:00:00\u0026#39;, 0);\r3. 项目依赖配置\r#\r创建Maven项目，pom.xml配置如下：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;com.spark.tutorial\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;recommendation-system\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.0\u0026lt;/version\u0026gt; \u0026lt;packaging\u0026gt;jar\u0026lt;/packaging\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;scala.version\u0026gt;2.13.8\u0026lt;/scala.version\u0026gt; \u0026lt;spark.version\u0026gt;3.5.8\u0026lt;/spark.version\u0026gt; \u0026lt;maven.compiler.source\u0026gt;11\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;11\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.spark\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spark-core_2.13\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spark.version}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.spark\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spark-sql_2.13\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spark.version}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.spark\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spark-mllib_2.13\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spark.version}\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;8.0.33\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;redis.clients\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jedis\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.0.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.typesafe\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;config\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;net.alchim31.maven\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;scala-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.8.1\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;compile\u0026lt;/goal\u0026gt; \u0026lt;goal\u0026gt;testCompile\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;scalaVersion\u0026gt;${scala.version}\u0026lt;/scalaVersion\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-compiler-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.11.0\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;source\u0026gt;11\u0026lt;/source\u0026gt; \u0026lt;target\u0026gt;11\u0026lt;/target\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/project\u0026gt;\r4. 应用配置文件\r#\r创建src/main/resources/application.conf，将敏感配置外部化：\n# 推荐系统配置文件（Docker 环境） # 使用 Docker Desktop 启动 MySQL 和 Redis 服务 spark { app-name = \u0026#34;ProductRecommendationSystem\u0026#34; master = \u0026#34;local[*]\u0026#34; # 性能优化配置 config { spark.sql.adaptive.enabled = true spark.sql.adaptive.coalescePartitions.enabled = true spark.serializer = \u0026#34;org.apache.spark.serializer.KryoSerializer\u0026#34; spark.sql.shuffle.partitions = 8 spark.log-level = \u0026#34;WARN\u0026#34; } } # Docker MySQL 配置 mysql { # Docker 容器映射端口 3307 -\u0026gt; 3306 url = \u0026#34;jdbc:mysql://localhost:3307/recommendation_system?useSSL=false\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;useUnicode=true\u0026amp;characterEncoding=UTF-8\u0026amp;allowPublicKeyRetrieval=true\u0026#34; user = \u0026#34;root\u0026#34; password = \u0026#34;root123456\u0026#34; driver = \u0026#34;com.mysql.cj.jdbc.Driver\u0026#34; pool-size = 5 } # Docker Redis 配置 redis { host = \u0026#34;localhost\u0026#34; port = 6379 password = \u0026#34;\u0026#34; # Docker Redis 默认无密码 database = 0 timeout = 3000 pool { max-total = 10 max-idle = 5 min-idle = 2 } cache { ttl-hours = 24 } } als { rank = 10 max-iter = 10 reg-param = 0.01 alpha = 1.0 cold-start-strategy = \u0026#34;drop\u0026#34; implicit-preference = false training-ratio = 0.8 random-seed = 42 } recommendation { top-k = 10 cf-weight = 0.6 cb-weight = 0.4 model-save-path = \u0026#34;datas/models/recommendation/als\u0026#34; }\r实验内容\r#\r1. 配置管理工具类\r#\r创建ConfigManager.scala，实现企业级配置管理：\nimport com.typesafe.config.{Config, ConfigFactory} import org.apache.spark.sql.SparkSession import scala.jdk.CollectionConverters.CollectionHasAsScala /** * ConfigManager - 统一配置管理器 * 从 application_ex14.conf 读取所有配置参数 * * @author John * @since 2026/5/7 15:26 * @version 2.0 */ object ConfigManager { private lazy val config: Config = { try { ConfigFactory.load(\u0026#34;application_ex14\u0026#34;) } catch { case e: Exception =\u0026gt; println(s\u0026#34;⚠️ 加载配置文件失败，使用默认配置: ${e.getMessage}\u0026#34;) ConfigFactory.load() } } def getConfig: Config = config /** * 获取 Spark 配置参数 */ def getSparkConfig: Map[String, String] = { try { val sparkConfig = config.getConfig(\u0026#34;spark.config\u0026#34;) import scala.jdk.CollectionConverters.MapHasAsScala sparkConfig.entrySet().asScala.map { entry =\u0026gt; entry.getKey -\u0026gt; entry.getValue.unwrapped().toString }.toMap } catch { case _: Exception =\u0026gt; println(\u0026#34;⚠️ Spark 配置缺失，使用默认值\u0026#34;) Map.empty[String, String] } } /** * 创建 SparkSession（带完整配置） */ def createSparkSession(appName: String): SparkSession = { val builder = SparkSession.builder() .appName(appName) .master(getOrElse(\u0026#34;spark.master\u0026#34;, \u0026#34;local[*]\u0026#34;)) // 应用自定义配置 getSparkConfig.foreach { case (k, v) =\u0026gt; builder.config(k, v) } val session = builder.getOrCreate() session.sparkContext.setLogLevel(getOrElse(\u0026#34;spark.log-level\u0026#34;, \u0026#34;WARN\u0026#34;)) session } // ==================== MySQL 配置 ==================== def getJdbcUrl: String = getOrElse(\u0026#34;mysql.url\u0026#34;, \u0026#34;jdbc:mysql://localhost:3306/recommendation_system?useSSL=false\u0026amp;serverTimezone=Asia/Shanghai\u0026#34;) def getJdbcUser: String = getOrElse(\u0026#34;mysql.user\u0026#34;, \u0026#34;root\u0026#34;) def getJdbcPassword: String = getOrElse(\u0026#34;mysql.password\u0026#34;, \u0026#34;\u0026#34;) def getJdbcDriver: String = getOrElse(\u0026#34;mysql.driver\u0026#34;, \u0026#34;com.mysql.cj.jdbc.Driver\u0026#34;) def getJdbcPoolSize: Int = getOrElse(\u0026#34;mysql.pool-size\u0026#34;, 5) // ==================== Redis 配置 ==================== def getRedisHost: String = getOrElse(\u0026#34;redis.host\u0026#34;, \u0026#34;localhost\u0026#34;) def getRedisPort: Int = getOrElse(\u0026#34;redis.port\u0026#34;, 6379) def getRedisPassword: String = { if (config.hasPath(\u0026#34;redis.password\u0026#34;) \u0026amp;\u0026amp; !config.getIsNull(\u0026#34;redis.password\u0026#34;)) { config.getString(\u0026#34;redis.password\u0026#34;) } else \u0026#34;\u0026#34; } def getRedisDatabase: Int = getOrElse(\u0026#34;redis.database\u0026#34;, 0) def getRedisTimeout: Int = getOrElse(\u0026#34;redis.timeout\u0026#34;, 3000) def getRedisMaxTotal: Int = getOrElse(\u0026#34;redis.pool.max-total\u0026#34;, 10) def getRedisMaxIdle: Int = getOrElse(\u0026#34;redis.pool.max-idle\u0026#34;, 5) def getRedisMinIdle: Int = getOrElse(\u0026#34;redis.pool.min-idle\u0026#34;, 2) def getCacheTtlHours: Int = getOrElse(\u0026#34;redis.cache.ttl-hours\u0026#34;, 24) // ==================== ALS 算法配置 ==================== def getAlsRank: Int = getOrElse(\u0026#34;als.rank\u0026#34;, 10) def getAlsMaxIter: Int = getOrElse(\u0026#34;als.max-iter\u0026#34;, 10) def getAlsRegParam: Double = getOrElse(\u0026#34;als.reg-param\u0026#34;, 0.01) def getAlsAlpha: Double = getOrElse(\u0026#34;als.alpha\u0026#34;, 1.0) def getAlsColdStartStrategy: String = getOrElse(\u0026#34;als.cold-start-strategy\u0026#34;, \u0026#34;drop\u0026#34;) def getAlsImplicitPreference: Boolean = getOrElse(\u0026#34;als.implicit-preference\u0026#34;, false) def getTrainingRatio: Double = getOrElse(\u0026#34;als.training-ratio\u0026#34;, 0.8) def getRandomSeed: Long = getOrElse(\u0026#34;als.random-seed\u0026#34;, 42L) // ==================== 推荐系统配置 ==================== def getTopK: Int = getOrElse(\u0026#34;recommendation.top-k\u0026#34;, 10) def getCfWeight: Double = getOrElse(\u0026#34;recommendation.cf-weight\u0026#34;, 0.6) def getCbWeight: Double = getOrElse(\u0026#34;recommendation.cb-weight\u0026#34;, 0.4) def getModelSavePath: String = getOrElse(\u0026#34;recommendation.model-save-path\u0026#34;, \u0026#34;datas/models/recommendation/als\u0026#34;) /** * 打印配置信息（调试用） */ def printConfig(): Unit = { println(\u0026#34;=\u0026#34; * 80) println(\u0026#34;📋 推荐系统配置信息\u0026#34;) println(\u0026#34;=\u0026#34; * 80) println(s\u0026#34;MySQL: $getJdbcUrl\u0026#34;) println(s\u0026#34;Redis: $getRedisHost:$getRedisPort (DB: $getRedisDatabase)\u0026#34;) println(s\u0026#34;ALS: rank=$getAlsRank, maxIter=$getAlsMaxIter, regParam=$getAlsRegParam\u0026#34;) println(s\u0026#34;推荐: topK=$getTopK, cfWeight=$getCfWeight, cbWeight=$getCbWeight\u0026#34;) println(\u0026#34;=\u0026#34; * 80) } /** * 安全获取配置值（带默认值） */ private def getOrElse[T](path: String, default: T): T = { try { if (config.hasPath(path)) { val value = config.getValue(path).unwrapped() // 根据默认值的类型进行转换 default match { case _: String =\u0026gt; value.toString.asInstanceOf[T] case _: Int =\u0026gt; value.toString.toInt.asInstanceOf[T] case _: Long =\u0026gt; value.toString.toLong.asInstanceOf[T] case _: Double =\u0026gt; value.toString.toDouble.asInstanceOf[T] case _: Boolean =\u0026gt; value.toString.toBoolean.asInstanceOf[T] case _ =\u0026gt; default } } else { default } } catch { case e: Exception =\u0026gt; println(s\u0026#34;⚠️ 读取配置 $path 失败: ${e.getMessage}，使用默认值\u0026#34;) default } } }\r2. 数据访问层\r#\r创建DataAccessLayer.scala，封装数据库访问逻辑，实现连接复用和数据验证：\nimport org.apache.spark.sql.{DataFrame, SparkSession} import org.apache.spark.sql.functions._ import org.apache.spark.sql.types.DoubleType /** * DataAccessLayer - 数据访问层 * 封装所有数据库操作，统一管理 JDBC 连接 * * @author John * @since 2026/5/7 15:29 * @version 2.0 */ object DataAccessLayer { /** * 创建 JDBC Properties（复用） */ private def createJdbcProps(): java.util.Properties = { val props = new java.util.Properties() props.setProperty(\u0026#34;user\u0026#34;, ConfigManager.getJdbcUser) props.setProperty(\u0026#34;password\u0026#34;, ConfigManager.getJdbcPassword) props.setProperty(\u0026#34;driver\u0026#34;, ConfigManager.getJdbcDriver) // 性能优化配置 props.setProperty(\u0026#34;fetchsize\u0026#34;, \u0026#34;1000\u0026#34;) props.setProperty(\u0026#34;batchsize\u0026#34;, \u0026#34;1000\u0026#34;) props } /** * 加载评分数据（带数据验证） */ def loadRatings(spark: SparkSession): DataFrame = { try { val jdbcUrl = ConfigManager.getJdbcUrl val jdbcProps = createJdbcProps() println(\u0026#34;📊 正在加载评分数据...\u0026#34;) val df = spark.read.jdbc(jdbcUrl, \u0026#34;ratings\u0026#34;, jdbcProps) .select( col(\u0026#34;user_id\u0026#34;).as(\u0026#34;userId\u0026#34;), col(\u0026#34;product_id\u0026#34;).as(\u0026#34;productId\u0026#34;), col(\u0026#34;rating\u0026#34;).cast(DoubleType).as(\u0026#34;rating\u0026#34;) ) .filter(col(\u0026#34;rating\u0026#34;).isNotNull \u0026amp;\u0026amp; col(\u0026#34;rating\u0026#34;) \u0026gt;= 1.0 \u0026amp;\u0026amp; col(\u0026#34;rating\u0026#34;) \u0026lt;= 5.0) .filter(col(\u0026#34;userId\u0026#34;).isNotNull \u0026amp;\u0026amp; col(\u0026#34;productId\u0026#34;).isNotNull) .cache() val count = df.count() println(s\u0026#34;✅ 评分数据加载完成: $count 条记录\u0026#34;) df } catch { case e: Exception =\u0026gt; println(s\u0026#34;❌ 加载评分数据失败: ${e.getMessage}\u0026#34;) throw e } } /** * 加载商品数据（仅激活商品） */ def loadProducts(spark: SparkSession): DataFrame = { try { val jdbcUrl = ConfigManager.getJdbcUrl val jdbcProps = createJdbcProps() println(\u0026#34;📦 正在加载商品数据...\u0026#34;) val df = spark.read.jdbc(jdbcUrl, \u0026#34;products\u0026#34;, jdbcProps) .filter(col(\u0026#34;status\u0026#34;) === 1) // 仅加载激活商品 .cache() val count = df.count() println(s\u0026#34;✅ 商品数据加载完成: $count 个商品\u0026#34;) df } catch { case e: Exception =\u0026gt; println(s\u0026#34;❌ 加载商品数据失败: ${e.getMessage}\u0026#34;) throw e } } /** * 加载用户行为数据 */ def loadUserBehavior(spark: SparkSession): DataFrame = { try { val jdbcUrl = ConfigManager.getJdbcUrl val jdbcProps = createJdbcProps() println(\u0026#34;👤 正在加载用户行为数据...\u0026#34;) val df = spark.read.jdbc(jdbcUrl, \u0026#34;user_behavior\u0026#34;, jdbcProps) .filter(col(\u0026#34;user_id\u0026#34;).isNotNull \u0026amp;\u0026amp; col(\u0026#34;product_id\u0026#34;).isNotNull) .cache() val count = df.count() println(s\u0026#34;✅ 用户行为数据加载完成: $count 条记录\u0026#34;) df } catch { case e: Exception =\u0026gt; println(s\u0026#34;❌ 加载用户行为数据失败: ${e.getMessage}\u0026#34;) throw e } } /** * 保存推荐结果到 MySQL */ def saveRecommendationsToMySQL( spark: SparkSession, recsDF: DataFrame, tableName: String, mode: String = \u0026#34;overwrite\u0026#34; ): Unit = { try { val jdbcUrl = ConfigManager.getJdbcUrl val jdbcProps = createJdbcProps() println(s\u0026#34;💾 正在保存推荐结果到表: $tableName (mode=$mode)...\u0026#34;) recsDF.write .mode(mode) .jdbc(jdbcUrl, tableName, jdbcProps) println(s\u0026#34;✅ 推荐结果保存成功: ${recsDF.count()} 条记录\u0026#34;) } catch { case e: Exception =\u0026gt; println(s\u0026#34;❌ 保存推荐结果失败: ${e.getMessage}\u0026#34;) throw e } } }\r3. 基于协同过滤的推荐\r#\r创建CollaborativeFiltering.scala，实现企业级协同过滤推荐：\n知识点回顾：ALS算法通过矩阵分解将用户-物品评分矩阵分解为用户特征矩阵和物品特征矩阵，交替优化两者直到收敛。Spark MLlib的ALS实现支持显式反馈和隐式反馈两种模式。\nimport org.apache.spark.ml.recommendation.ALS import org.apache.spark.ml.evaluation.RegressionEvaluator import org.apache.spark.sql.{DataFrame, SparkSession} import org.apache.spark.sql.functions._ /** * CollaborativeFiltering - 协同过滤推荐系统（ALS算法） * * @author John * @since 2026/5/7 15:30 * @version 2.0 */ object CollaborativeFiltering { def main(args: Array[String]): Unit = { var spark: SparkSession = null try { // 打印配置信息 ConfigManager.printConfig() val _spark = ConfigManager.createSparkSession(\u0026#34;CollaborativeFiltering\u0026#34;) import _spark.implicits._ spark = _spark println(\u0026#34;\\n\u0026#34; + \u0026#34;=\u0026#34; * 80) println(\u0026#34;🤖 协同过滤推荐系统 (ALS算法)\u0026#34;) println(\u0026#34;=\u0026#34; * 80) // 1. 加载数据 val ratingsDF = DataAccessLayer.loadRatings(spark) val dataCount = ratingsDF.count() println(s\u0026#34;\\n📊 数据统计:\u0026#34;) println(s\u0026#34; - 总记录数: $dataCount\u0026#34;) println(s\u0026#34; - 用户数: ${ratingsDF.select(\u0026#34;userId\u0026#34;).distinct().count()}\u0026#34;) println(s\u0026#34; - 商品数: ${ratingsDF.select(\u0026#34;productId\u0026#34;).distinct().count()}\u0026#34;) if (dataCount == 0) { println(\u0026#34;⚠️ 评分数据为空，请检查数据库连接和表数据\u0026#34;) return } // 2. 划分训练集和测试集 val Array(training, test) = ratingsDF.randomSplit( Array(ConfigManager.getTrainingRatio, 1 - ConfigManager.getTrainingRatio), seed = ConfigManager.getRandomSeed ) training.cache() test.cache() println(s\u0026#34;\\n🔧 数据集划分:\u0026#34;) println(s\u0026#34; - 训练集: ${training.count()} 条 (${ConfigManager.getTrainingRatio * 100}%)\u0026#34;) println(s\u0026#34; - 测试集: ${test.count()} 条 (${(1 - ConfigManager.getTrainingRatio) * 100}%)\u0026#34;) // 3. 训练和评估模型 val (model, rmse) = trainAndEvaluate(training, test) println(s\u0026#34;\\n✅ 模型评估结果:\u0026#34;) println(f\u0026#34; - RMSE: $rmse%.4f\u0026#34;) // 4. 生成推荐 val topK = ConfigManager.getTopK println(s\u0026#34;\\n🎯 生成 Top-$topK 推荐:\u0026#34;) // 为所有用户推荐 val userRecs = model.recommendForAllUsers(topK) println(s\u0026#34; - 已为 ${userRecs.count()} 个用户生成推荐\u0026#34;) println(s\u0026#34;\\n 示例（前5个用户）:\u0026#34;) userRecs.show(5, truncate = false) // 为所有商品推荐相似用户 val productRecs = model.recommendForAllItems(topK) println(s\u0026#34;\\n 为商品推荐用户（前5个商品）:\u0026#34;) productRecs.show(5, truncate = false) // 为特定用户推荐 val userId = 1 val userDF = Seq(userId).toDF(\u0026#34;userId\u0026#34;) val userSpecificRecs = model.recommendForUserSubset(userDF, 5) println(s\u0026#34;\\n 为用户 $userId 的个性化推荐:\u0026#34;) userSpecificRecs.show(false) // 5. 保存模型 val modelPath = ConfigManager.getModelSavePath + \u0026#34;/cf-model\u0026#34; model.write.overwrite().save(modelPath) println(s\u0026#34;\\n💾 模型已保存至: $modelPath\u0026#34;) println(\u0026#34;\\n\u0026#34; + \u0026#34;=\u0026#34; * 80) println(\u0026#34;✅ 协同过滤推荐系统运行完成\u0026#34;) println(\u0026#34;=\u0026#34; * 80) } catch { case e: InterruptedException =\u0026gt; println(s\u0026#34;\\n⚠️ 程序被中断: ${e.getMessage}\u0026#34;) case e: Exception =\u0026gt; println(s\u0026#34;\\n❌ 运行出错: ${e.getClass.getSimpleName} - ${e.getMessage}\u0026#34;) e.printStackTrace() } finally { if (spark != null) { println(\u0026#34;\\n🛑 正在关闭 SparkSession...\u0026#34;) spark.stop() println(\u0026#34;✅ SparkSession 已关闭\u0026#34;) } } } def trainAndEvaluate( training: DataFrame, test: DataFrame): (org.apache.spark.ml.recommendation.ALSModel, Double) = { val als = new ALS() .setRank(ConfigManager.getAlsRank) .setMaxIter(ConfigManager.getAlsMaxIter) .setRegParam(ConfigManager.getAlsRegParam) .setAlpha(ConfigManager.getAlsAlpha) .setImplicitPrefs(ConfigManager.getAlsImplicitPreference) .setColdStartStrategy(ConfigManager.getAlsColdStartStrategy) .setUserCol(\u0026#34;userId\u0026#34;) .setItemCol(\u0026#34;productId\u0026#34;) .setRatingCol(\u0026#34;rating\u0026#34;) val startTime = System.currentTimeMillis() val model = als.fit(training) val trainingTime = (System.currentTimeMillis() - startTime) / 1000.0 println(s\u0026#34;模型训练完成，耗时: ${trainingTime}s\u0026#34;) println(s\u0026#34;用户因子维度: ${model.rank}, 用户数: ${model.userFactors.count()}, 物品数: ${model.itemFactors.count()}\u0026#34;) val predictions = model.transform(test) val evaluator = new RegressionEvaluator() .setMetricName(\u0026#34;rmse\u0026#34;) .setLabelCol(\u0026#34;rating\u0026#34;) .setPredictionCol(\u0026#34;prediction\u0026#34;) val rmse = evaluator.evaluate(predictions) (model, rmse) } }\r4. 基于内容的推荐\r#\r创建ContentBasedRecommendation.scala，实现基于TF-IDF的内容推荐：\n知识点回顾：基于内容的推荐通过分析物品的属性特征（如商品描述文本），计算物品之间的相似度。TF-IDF将文本转化为数值向量，余弦相似度衡量向量间的方向一致性。当用户冷启动时，基于内容的方法仍然可以推荐与用户浏览商品属性相似的商品。\nimport org.apache.spark.ml.feature.{HashingTF, IDF, Tokenizer} import org.apache.spark.ml.linalg.SparseVector import org.apache.spark.sql.{DataFrame, SparkSession} import org.apache.spark.sql.functions._ /** * ContentBasedRecommendation - 基于内容的推荐系统（TF-IDF） * * @author John * @since 2026/5/7 15:31 * @version 2.0 */ object ContentBasedRecommendation { case class ProductSimilarity(productId: Int, similarProductId: Int, similarity: Double) def main(args: Array[String]): Unit = { var spark: SparkSession = null try { val _spark = ConfigManager.createSparkSession(\u0026#34;ContentBasedRecommendation\u0026#34;) import _spark.implicits._ spark = _spark println(\u0026#34;\\n\u0026#34; + \u0026#34;=\u0026#34; * 80) println(\u0026#34;📝 基于内容的推荐系统 (TF-IDF)\u0026#34;) println(\u0026#34;=\u0026#34; * 80) // 1. 加载数据 val productsDF = DataAccessLayer.loadProducts(spark) val ratingsDF = DataAccessLayer.loadRatings(spark) if (productsDF.count() == 0) { println(\u0026#34;⚠️ 商品数据为空，无法进行基于内容的推荐\u0026#34;) return } // 2. 计算商品特征向量 println(\u0026#34;\\n🔍 正在计算商品特征向量...\u0026#34;) val productFeatures = computeProductFeatures(productsDF) println(s\u0026#34;✅ 计算了 ${productFeatures.size} 个商品的特征向量\u0026#34;) // 3. 查找相似商品 val targetProductId = 1 println(s\u0026#34;\\n🎯 查找与商品 $targetProductId 相似的商品:\u0026#34;) val similarProducts = findSimilarProducts(targetProductId, productFeatures, 5) if (similarProducts.nonEmpty) { similarProducts.foreach { case ProductSimilarity(_, simId, sim) =\u0026gt; val productNameRow = productsDF.filter($\u0026#34;product_id\u0026#34; === simId) .select(\u0026#34;product_name\u0026#34;).take(1).headOption productNameRow match { case Some(row) =\u0026gt; val name = row.getString(0) println(f\u0026#34; - 商品: $name (ID=$simId), 相似度: $sim%.4f\u0026#34;) case None =\u0026gt; println(f\u0026#34; - 商品ID=$simId (名称不存在), 相似度: $sim%.4f\u0026#34;) } } } else { println(\u0026#34; ⚠️ 未找到相似商品\u0026#34;) } // 4. 为用户推荐 val userId = 1 println(s\u0026#34;\\n👤 为用户 $userId 生成基于内容的推荐:\u0026#34;) val recommendations = recommendForUser(userId, ratingsDF, productsDF, productFeatures, 5) if (recommendations.nonEmpty) { recommendations.foreach { case (productId, score) =\u0026gt; val productNameRow = productsDF.filter($\u0026#34;product_id\u0026#34; === productId) .select(\u0026#34;product_name\u0026#34;).take(1).headOption productNameRow match { case Some(row) =\u0026gt; val name = row.getString(0) println(f\u0026#34; - 商品: $name (ID=$productId), 推荐分数: $score%.4f\u0026#34;) case None =\u0026gt; println(f\u0026#34; - 商品ID=$productId (名称不存在), 推荐分数: $score%.4f\u0026#34;) } } } else { println(\u0026#34; ⚠️ 无法生成推荐（用户无历史评分或数据不足）\u0026#34;) } println(\u0026#34;\\n\u0026#34; + \u0026#34;=\u0026#34; * 80) println(\u0026#34;✅ 基于内容的推荐系统运行完成\u0026#34;) println(\u0026#34;=\u0026#34; * 80) } catch { case e: InterruptedException =\u0026gt; println(s\u0026#34;\\n⚠️ 程序被中断: ${e.getMessage}\u0026#34;) case e: Exception =\u0026gt; println(s\u0026#34;\\n❌ 运行出错: ${e.getClass.getSimpleName} - ${e.getMessage}\u0026#34;) e.printStackTrace() } finally { if (spark != null) { println(\u0026#34;\\n🛑 正在关闭 SparkSession...\u0026#34;) spark.stop() println(\u0026#34;✅ SparkSession 已关闭\u0026#34;) } } } def computeProductFeatures(productsDF: DataFrame): Map[Int, SparseVector] = { val tokenizer = new Tokenizer() .setInputCol(\u0026#34;description\u0026#34;) .setOutputCol(\u0026#34;words\u0026#34;) val wordsData = tokenizer.transform(productsDF.na.fill(Map(\u0026#34;description\u0026#34; -\u0026gt; \u0026#34;\u0026#34;))) val hashingTF = new HashingTF() .setInputCol(\u0026#34;words\u0026#34;) .setOutputCol(\u0026#34;rawFeatures\u0026#34;) .setNumFeatures(1000) val featurizedData = hashingTF.transform(wordsData) val idf = new IDF() .setInputCol(\u0026#34;rawFeatures\u0026#34;) .setOutputCol(\u0026#34;features\u0026#34;) val idfModel = idf.fit(featurizedData) val rescaledData = idfModel.transform(featurizedData) rescaledData.select(\u0026#34;product_id\u0026#34;, \u0026#34;features\u0026#34;).rdd.map { case row =\u0026gt; (row.getInt(0), row.getAs[SparseVector](1)) }.collect().toMap } def cosineSimilarity(vec1: SparseVector, vec2: SparseVector): Double = { val dotProduct = vec1.dot(vec2) val norm1 = math.sqrt(vec1.dot(vec1)) val norm2 = math.sqrt(vec2.dot(vec2)) if (norm1 \u0026lt; 1e-10 || norm2 \u0026lt; 1e-10) 0.0 else dotProduct / (norm1 * norm2) } def findSimilarProducts( targetProductId: Int, productFeatures: Map[Int, SparseVector], topK: Int): Seq[ProductSimilarity] = { productFeatures.get(targetProductId) match { case Some(targetFeatures) =\u0026gt; productFeatures.toSeq .filter(_._1 != targetProductId) .map { case (productId, features) =\u0026gt; val sim = cosineSimilarity(targetFeatures, features) ProductSimilarity(targetProductId, productId, sim) } .sortBy(-_.similarity) .take(topK) case None =\u0026gt; println(s\u0026#34;商品 $targetProductId 不存在特征向量\u0026#34;) Seq.empty } } def recommendForUser( userId: Int, ratingsDF: DataFrame, productsDF: DataFrame, productFeatures: Map[Int, SparseVector], topK: Int): Seq[(Int, Double)] = { val userRatings = ratingsDF.filter(ratingsDF(\u0026#34;userId\u0026#34;) === userId).collect() if (userRatings.isEmpty) { println(s\u0026#34;用户 $userId 无历史评分，无法进行基于内容的推荐\u0026#34;) return Seq.empty } val userPreferences = userRatings.map { row =\u0026gt; (row.getInt(1), row.getDouble(2)) } val ratedProductIds = userPreferences.map(_._1).toSet productFeatures.toSeq .filter { case (productId, _) =\u0026gt; !ratedProductIds.contains(productId) } .map { case (productId, features) =\u0026gt; val score = userPreferences.map { case (pid, rating) =\u0026gt; productFeatures.get(pid) match { case Some(pf) =\u0026gt; cosineSimilarity(features, pf) * rating case None =\u0026gt; 0.0 } }.sum (productId, score) } .sortBy(-_._2) .take(topK) } }\r5. 混合推荐系统\r#\r创建HybridRecommendation.scala，实现加权混合推荐：\n知识点回顾：混合推荐系统结合多种推荐算法的优势，弥补单一算法的不足。本实验采用加权混合策略，协同过滤和基于内容的推荐结果按权重线性组合。权重可通过A/B测试或交叉验证确定。\n混合推荐分数 = cfWeight × 协同过滤分数 + cbWeight × 基于内容分数 其中 cfWeight + cbWeight = 1.0\rimport org.apache.spark.ml.recommendation.ALS import org.apache.spark.sql.{DataFrame, SparkSession} import org.apache.spark.sql.functions._ /** * HybridRecommendation - 混合推荐系统（协同过滤 + 基于内容） * * @author John * @since 2026/5/7 15:31 * @version 2.0 */ object HybridRecommendation { def main(args: Array[String]): Unit = { var spark: SparkSession = null try { val _spark = ConfigManager.createSparkSession(\u0026#34;HybridRecommendation\u0026#34;) import _spark.implicits._ spark = _spark println(\u0026#34;\\n\u0026#34; + \u0026#34;=\u0026#34; * 80) println(\u0026#34;🎯 混合推荐系统 (协同过滤 + 基于内容)\u0026#34;) println(\u0026#34;=\u0026#34; * 80) // 1. 加载数据 val ratingsDF = DataAccessLayer.loadRatings(spark) val productsDF = DataAccessLayer.loadProducts(spark) if (ratingsDF.count() == 0 || productsDF.count() == 0) { println(\u0026#34;⚠️ 数据不足，无法进行混合推荐\u0026#34;) return } // 2. 训练协同过滤模型 println(\u0026#34;\\n🔧 步骤1: 训练协同过滤模型...\u0026#34;) val Array(training, _) = ratingsDF.randomSplit( Array(ConfigManager.getTrainingRatio, 1 - ConfigManager.getTrainingRatio), seed = ConfigManager.getRandomSeed ) val als = new ALS() .setRank(ConfigManager.getAlsRank) .setMaxIter(ConfigManager.getAlsMaxIter) .setRegParam(ConfigManager.getAlsRegParam) .setColdStartStrategy(ConfigManager.getAlsColdStartStrategy) .setUserCol(\u0026#34;userId\u0026#34;) .setItemCol(\u0026#34;productId\u0026#34;) .setRatingCol(\u0026#34;rating\u0026#34;) val cfModel = als.fit(training) println(\u0026#34;✅ 协同过滤模型训练完成\u0026#34;) // 3. 计算基于内容的特征 println(\u0026#34;\\n🔧 步骤2: 计算商品特征向量...\u0026#34;) val productFeatures = ContentBasedRecommendation.computeProductFeatures(productsDF) println(s\u0026#34;✅ 商品特征计算完成: ${productFeatures.size} 个商品\u0026#34;) // 4. 设置混合权重 val cfWeight = ConfigManager.getCfWeight val cbWeight = ConfigManager.getCbWeight println(s\u0026#34;\\n⚖️ 混合权重: 协同过滤=$cfWeight, 基于内容=$cbWeight\u0026#34;) // 5. 生成推荐 val userId = 1 val topK = ConfigManager.getTopK println(s\u0026#34;\\n👤 为用户 $userId 生成混合推荐 (Top-$topK):\u0026#34;) // 协同过滤推荐 val cfRecs = getCfRecommendations(spark, cfModel, userId, topK) println(s\u0026#34;\\n 📊 协同过滤推荐结果 (${cfRecs.size}条):\u0026#34;) cfRecs.foreach { case (pid, score) =\u0026gt; println(f\u0026#34; - 商品ID=$pid, 分数=$score%.4f\u0026#34;) } // 基于内容推荐 val cbRecs = ContentBasedRecommendation.recommendForUser( userId, ratingsDF, productsDF, productFeatures, topK ) println(s\u0026#34;\\n 📝 基于内容推荐结果 (${cbRecs.size}条):\u0026#34;) cbRecs.foreach { case (pid, score) =\u0026gt; println(f\u0026#34; - 商品ID=$pid, 分数=$score%.4f\u0026#34;) } // 混合推荐 val hybridRecs = mergeRecommendations(cfRecs, cbRecs, cfWeight, cbWeight, 5) println(s\u0026#34;\\n 🎯 混合推荐结果 (${hybridRecs.size}条):\u0026#34;) if (hybridRecs.nonEmpty) { hybridRecs.foreach { case (productId, score) =\u0026gt; // 安全获取商品名称，避免空指针 val productNameRow = productsDF.filter($\u0026#34;product_id\u0026#34; === productId) .select(\u0026#34;product_name\u0026#34;).take(1).headOption productNameRow match { case Some(row) =\u0026gt; val name = row.getString(0) println(f\u0026#34; - 商品: $name (ID=$productId), 混合分数: $score%.4f\u0026#34;) case None =\u0026gt; println(f\u0026#34; - 商品ID=$productId (名称不存在), 混合分数: $score%.4f\u0026#34;) } } } else { println(\u0026#34; ⚠️ 未生成混合推荐结果\u0026#34;) } println(\u0026#34;\\n\u0026#34; + \u0026#34;=\u0026#34; * 80) println(\u0026#34;✅ 混合推荐系统运行完成\u0026#34;) println(\u0026#34;=\u0026#34; * 80) } catch { case e: InterruptedException =\u0026gt; println(s\u0026#34;\\n⚠️ 程序被中断: ${e.getMessage}\u0026#34;) case e: Exception =\u0026gt; println(s\u0026#34;\\n❌ 运行出错: ${e.getClass.getSimpleName} - ${e.getMessage}\u0026#34;) e.printStackTrace() } finally { if (spark != null) { println(\u0026#34;\\n🛑 正在关闭 SparkSession...\u0026#34;) spark.stop() println(\u0026#34;✅ SparkSession 已关闭\u0026#34;) } } } def getCfRecommendations( spark: SparkSession, model: org.apache.spark.ml.recommendation.ALSModel, userId: Int, topK: Int): Map[Int, Double] = { import spark.implicits._ val userDF = Seq(userId).toDF(\u0026#34;userId\u0026#34;) model.recommendForUserSubset(userDF, topK) .select(explode($\u0026#34;recommendations\u0026#34;).as(\u0026#34;rec\u0026#34;)) .select($\u0026#34;rec.productId\u0026#34;.as(\u0026#34;productId\u0026#34;), $\u0026#34;rec.rating\u0026#34;.as(\u0026#34;score\u0026#34;)) .collect() .map(row =\u0026gt; (row.getInt(0), row.getFloat(1).toDouble)) .toMap } def mergeRecommendations( cfRecs: Map[Int, Double], cbRecs: Seq[(Int, Double)], cfWeight: Double, cbWeight: Double, topK: Int): Seq[(Int, Double)] = { val cbRecsMap = cbRecs.toMap val maxCfScore = if (cfRecs.nonEmpty) cfRecs.values.max else 1.0 val maxCbScore = if (cbRecsMap.nonEmpty) cbRecsMap.values.max else 1.0 val allProductIds = cfRecs.keySet ++ cbRecsMap.keySet allProductIds.map { productId =\u0026gt; val normalizedCf = cfRecs.getOrElse(productId, 0.0) / maxCfScore val normalizedCb = cbRecsMap.getOrElse(productId, 0.0) / maxCbScore val hybridScore = cfWeight * normalizedCf + cbWeight * normalizedCb (productId, hybridScore) }.toSeq .sortBy(-_._2) .take(topK) } }\r6. 推荐系统评估\r#\r创建RecommendationEvaluator.scala，实现多维度评估：\n知识点回顾：推荐系统评估分为预测准确度评估和排序质量评估。预测准确度用RMSE/MAE衡量评分预测精度；排序质量用Precision@K、Recall@K、NDCG@K衡量推荐列表质量。企业级系统需同时关注多个指标，避免单一指标优化导致推荐结果单一化。\n评估指标 公式 含义 RMSE √(Σ(ŷ-y)²/N) 均方根误差，越小越好 MAE Σ ŷ-y Precision@K 推荐命中数/K 推荐列表中用户喜欢的比例 Recall@K 推荐命中数/用户实际喜欢数 用户喜欢的物品被推荐的比例 F1@K 2×P×R/(P+R) 准确率和召回率的调和平均 NDCG@K DCG@K/IDCG@K 考虑排序位置的增益指标 import org.apache.spark.ml.recommendation.ALS import org.apache.spark.ml.evaluation.RegressionEvaluator import org.apache.spark.sql.{DataFrame, SparkSession} import org.apache.spark.sql.functions._ /** * RecommendationEvaluator * * @author John * @since 2026/5/7 15:32 * @version 1.0 */ object RecommendationEvaluator { def main(args: Array[String]): Unit = { var spark: SparkSession = null try { spark = ConfigManager.createSparkSession(\u0026#34;RecommendationEvaluator\u0026#34;) println(\u0026#34;=\u0026#34; * 80) println(\u0026#34;推荐系统评估器\u0026#34;) println(\u0026#34;=\u0026#34; * 80) val ratingsDF = DataAccessLayer.loadRatings(spark) val dataCount = ratingsDF.count() println(s\u0026#34;加载评分数据: $dataCount 条记录\u0026#34;) if (dataCount == 0) { println(\u0026#34;评分数据为空，请检查数据库\u0026#34;) return } val Array(training, test) = ratingsDF.randomSplit( Array(ConfigManager.getTrainingRatio, 1 - ConfigManager.getTrainingRatio), seed = ConfigManager.getRandomSeed ) training.cache() test.cache() println(s\u0026#34;训练集: ${training.count()} 条, 测试集: ${test.count()} 条\u0026#34;) println(\u0026#34;\\n\u0026#34; + \u0026#34;-\u0026#34; * 40) println(\u0026#34;1. 预测准确度评估\u0026#34;) println(\u0026#34;-\u0026#34; * 40) val (model, _) = CollaborativeFiltering.trainAndEvaluate(training, test) evaluatePredictionAccuracy(model, test) println(\u0026#34;\\n\u0026#34; + \u0026#34;-\u0026#34; * 40) println(\u0026#34;2. 排序质量评估\u0026#34;) println(\u0026#34;-\u0026#34; * 40) evaluateRankingQuality(spark, model, test) println(\u0026#34;\\n\u0026#34; + \u0026#34;-\u0026#34; * 40) println(\u0026#34;3. 参数敏感性分析\u0026#34;) println(\u0026#34;-\u0026#34; * 40) evaluateParameterSensitivity(training, test) println(\u0026#34;\\n\u0026#34; + \u0026#34;=\u0026#34; * 80) println(\u0026#34;推荐系统评估完成\u0026#34;) println(\u0026#34;=\u0026#34; * 80) } catch { case e: Exception =\u0026gt; println(s\u0026#34;运行出错: ${e.getMessage}\u0026#34;) e.printStackTrace() } finally { if (spark != null) spark.stop() } } def evaluatePredictionAccuracy( model: org.apache.spark.ml.recommendation.ALSModel, test: DataFrame): Unit = { val predictions = model.transform(test) val metrics = Seq(\u0026#34;rmse\u0026#34;, \u0026#34;mse\u0026#34;, \u0026#34;mae\u0026#34;, \u0026#34;r2\u0026#34;) val metricNames = Map( \u0026#34;rmse\u0026#34; -\u0026gt; \u0026#34;RMSE (均方根误差)\u0026#34;, \u0026#34;mse\u0026#34; -\u0026gt; \u0026#34;MSE (均方误差)\u0026#34;, \u0026#34;mae\u0026#34; -\u0026gt; \u0026#34;MAE (平均绝对误差)\u0026#34;, \u0026#34;r2\u0026#34; -\u0026gt; \u0026#34;R² (决定系数)\u0026#34; ) metrics.foreach { metric =\u0026gt; val evaluator = new RegressionEvaluator() .setMetricName(metric) .setLabelCol(\u0026#34;rating\u0026#34;) .setPredictionCol(\u0026#34;prediction\u0026#34;) val value = evaluator.evaluate(predictions) println(f\u0026#34; ${metricNames(metric)}: $value%.4f\u0026#34;) } println(\u0026#34;\\n 指标解读:\u0026#34;) println(\u0026#34; - RMSE/MAE越小，预测精度越高\u0026#34;) println(\u0026#34; - R²越接近1，模型拟合效果越好\u0026#34;) } def evaluateRankingQuality( spark: SparkSession, model: org.apache.spark.ml.recommendation.ALSModel, test: DataFrame): Unit = { import spark.implicits._ val topK = ConfigManager.getTopK val userRecs = model.recommendForAllUsers(topK) val userTestRatings = test.groupBy($\u0026#34;userId\u0026#34;) .agg(collect_set($\u0026#34;productId\u0026#34;).as(\u0026#34;testProducts\u0026#34;)) val userRecsWithTest = userRecs .join(userTestRatings, Seq(\u0026#34;userId\u0026#34;), \u0026#34;left\u0026#34;) .withColumn(\u0026#34;recommendedProducts\u0026#34;, expr(\u0026#34;transform(recommendations, x -\u0026gt; x.productId)\u0026#34;)) .withColumn(\u0026#34;intersection\u0026#34;, expr(\u0026#34;array_intersect(recommendedProducts, testProducts)\u0026#34;)) .withColumn(\u0026#34;precision\u0026#34;, when(size($\u0026#34;testProducts\u0026#34;) \u0026gt; 0, size($\u0026#34;intersection\u0026#34;) / lit(topK).cast(\u0026#34;double\u0026#34;)) .otherwise(0.0)) .withColumn(\u0026#34;recall\u0026#34;, when(size($\u0026#34;testProducts\u0026#34;) \u0026gt; 0, size($\u0026#34;intersection\u0026#34;) / size($\u0026#34;testProducts\u0026#34;).cast(\u0026#34;double\u0026#34;)) .otherwise(0.0)) val avgPrecision = userRecsWithTest.select(avg($\u0026#34;precision\u0026#34;)).head().getDouble(0) val avgRecall = userRecsWithTest.select(avg($\u0026#34;recall\u0026#34;)).head().getDouble(0) val f1Score = if ((avgPrecision + avgRecall) \u0026gt; 0) { 2 * avgPrecision * avgRecall / (avgPrecision + avgRecall) } else 0.0 println(f\u0026#34; Precision@$topK: $avgPrecision%.4f\u0026#34;) println(f\u0026#34; Recall@$topK: $avgRecall%.4f\u0026#34;) println(f\u0026#34; F1@$topK: $f1Score%.4f\u0026#34;) println(\u0026#34;\\n 指标解读:\u0026#34;) println(\u0026#34; - Precision@K: 推荐列表中用户实际喜欢的比例\u0026#34;) println(\u0026#34; - Recall@K: 用户喜欢的物品被推荐的比例\u0026#34;) println(\u0026#34; - F1@K: Precision和Recall的调和平均\u0026#34;) } def evaluateParameterSensitivity(training: DataFrame, test: DataFrame): Unit = { val ranks = Seq(5, 10, 20, 50) val regParams = Seq(0.01, 0.05, 0.1) println(\u0026#34;\\n Rank参数敏感性分析 (regParam=0.01, maxIter=10):\u0026#34;) println(f\u0026#34; ${\u0026#34;Rank\u0026#34;}%-6s ${\u0026#34;RMSE\u0026#34;}%-10s ${\u0026#34;训练时间(s)\u0026#34;}%-12s\u0026#34;) println(\u0026#34; \u0026#34; + \u0026#34;-\u0026#34; * 28) ranks.foreach { rank =\u0026gt; val als = new ALS() .setRank(rank) .setMaxIter(10) .setRegParam(0.01) .setColdStartStrategy(\u0026#34;drop\u0026#34;) .setUserCol(\u0026#34;userId\u0026#34;) .setItemCol(\u0026#34;productId\u0026#34;) .setRatingCol(\u0026#34;rating\u0026#34;) val startTime = System.currentTimeMillis() val model = als.fit(training) val trainingTime = (System.currentTimeMillis() - startTime) / 1000.0 val evaluator = new RegressionEvaluator() .setMetricName(\u0026#34;rmse\u0026#34;) .setLabelCol(\u0026#34;rating\u0026#34;) .setPredictionCol(\u0026#34;prediction\u0026#34;) val rmse = evaluator.evaluate(model.transform(test)) println(f\u0026#34; $rank%-6d $rmse%-10.4f $trainingTime%-12.2f\u0026#34;) } println(\u0026#34;\\n RegParam参数敏感性分析 (rank=10, maxIter=10):\u0026#34;) println(f\u0026#34; ${\u0026#34;RegParam\u0026#34;}%-10s ${\u0026#34;RMSE\u0026#34;}%-10s\u0026#34;) println(\u0026#34; \u0026#34; + \u0026#34;-\u0026#34; * 20) regParams.foreach { regParam =\u0026gt; val als = new ALS() .setRank(10) .setMaxIter(10) .setRegParam(regParam) .setColdStartStrategy(\u0026#34;drop\u0026#34;) .setUserCol(\u0026#34;userId\u0026#34;) .setItemCol(\u0026#34;productId\u0026#34;) .setRatingCol(\u0026#34;rating\u0026#34;) val model = als.fit(training) val evaluator = new RegressionEvaluator() .setMetricName(\u0026#34;rmse\u0026#34;) .setLabelCol(\u0026#34;rating\u0026#34;) .setPredictionCol(\u0026#34;prediction\u0026#34;) val rmse = evaluator.evaluate(model.transform(test)) println(f\u0026#34; $regParam%-10.2f $rmse%-10.4f\u0026#34;) } } }\r7. 推荐系统服务部署\r#\r创建RecommendationService.scala，实现生产级推荐服务：\n知识点回顾：生产环境推荐系统需要考虑：1）推荐结果缓存（Redis），避免重复计算；2）连接池管理，避免频繁创建/销毁连接；3）降级策略，当实时推荐失败时回退到热门推荐；4）模型版本管理，支持A/B测试。\nimport org.apache.spark.ml.recommendation.ALSModel import org.apache.spark.sql.SparkSession import org.apache.spark.sql.functions._ import redis.clients.jedis.{Jedis, JedisPool, JedisPoolConfig} /** * RecommendationService - 推荐服务（带Redis缓存） * * @author John * @since 2026/5/7 15:32 * @version 2.0 */ object RecommendationService { private var jedisPool: JedisPool = _ /** * 初始化 Redis 连接池（使用配置参数） */ private def initRedisPool(): Unit = { try { val poolConfig = new JedisPoolConfig() poolConfig.setMaxTotal(ConfigManager.getRedisMaxTotal) poolConfig.setMaxIdle(ConfigManager.getRedisMaxIdle) poolConfig.setMinIdle(ConfigManager.getRedisMinIdle) poolConfig.setTestOnBorrow(true) poolConfig.setTestOnReturn(true) poolConfig.setTestWhileIdle(true) val password = ConfigManager.getRedisPassword val timeout = ConfigManager.getRedisTimeout if (password.isEmpty) { jedisPool = new JedisPool( poolConfig, ConfigManager.getRedisHost, ConfigManager.getRedisPort, timeout ) } else { jedisPool = new JedisPool( poolConfig, ConfigManager.getRedisHost, ConfigManager.getRedisPort, timeout, password ) } println(s\u0026#34;✅ Redis连接池初始化完成: ${ConfigManager.getRedisHost}:${ConfigManager.getRedisPort}\u0026#34;) } catch { case e: Exception =\u0026gt; println(s\u0026#34;❌ Redis连接池初始化失败: ${e.getMessage}\u0026#34;) throw e } } private def closeRedisPool(): Unit = { if (jedisPool != null \u0026amp;\u0026amp; !jedisPool.isClosed) { jedisPool.close() println(\u0026#34;Redis连接池已关闭\u0026#34;) } } private def withJedis[T](block: Jedis =\u0026gt; T): T = { val jedis = jedisPool.getResource try { block(jedis) } finally { jedis.close() } } def main(args: Array[String]): Unit = { var spark: SparkSession = null try { val _spark = ConfigManager.createSparkSession(\u0026#34;RecommendationService\u0026#34;) import _spark.implicits._ spark = _spark // 初始化 Redis initRedisPool() println(\u0026#34;\\n\u0026#34; + \u0026#34;=\u0026#34; * 80) println(\u0026#34;🚀 推荐服务启动（带Redis缓存）\u0026#34;) println(\u0026#34;=\u0026#34; * 80) // 1. 加载模型 val modelPath = ConfigManager.getModelSavePath + \u0026#34;/cf-model\u0026#34; println(s\u0026#34;\\n📦 正在加载模型: $modelPath\u0026#34;) val model = ALSModel.load(modelPath) println(\u0026#34;✅ 模型加载完成\u0026#34;) // 2. 批量生成推荐并写入 Redis val topK = ConfigManager.getTopK val ttlSeconds = ConfigManager.getCacheTtlHours * 3600 println(s\u0026#34;\\n🔧 批量生成推荐结果并写入Redis (TTL=${ConfigManager.getCacheTtlHours}小时)...\u0026#34;) val userRecs = model.recommendForAllUsers(topK) val productsDF = DataAccessLayer.loadProducts(spark) var successCount = 0 var failCount = 0 userRecs.collect().foreach { row =\u0026gt; try { val userId = row.getInt(0) // 使用 Row 结构解析推荐结果 val recommendationsRow = row.getList[org.apache.spark.sql.Row](1) import scala.jdk.CollectionConverters._ val productIds = recommendationsRow.asScala.map(r =\u0026gt; r.getInt(0)).mkString(\u0026#34;,\u0026#34;) val scores = recommendationsRow.asScala.map(r =\u0026gt; f\u0026#34;${r.getFloat(1)}%.4f\u0026#34;).mkString(\u0026#34;,\u0026#34;) withJedis { jedis =\u0026gt; jedis.setex(s\u0026#34;rec:cf:user:$userId:products\u0026#34;, ttlSeconds, productIds) jedis.setex(s\u0026#34;rec:cf:user:$userId:scores\u0026#34;, ttlSeconds, scores) } successCount += 1 } catch { case e: Exception =\u0026gt; failCount += 1 if (failCount \u0026lt;= 5) { println(s\u0026#34; ⚠️ 用户推荐写入失败: ${e.getMessage}\u0026#34;) } } } println(s\u0026#34;✅ 推荐结果写入完成: 成功=$successCount, 失败=$failCount\u0026#34;) // 3. 测试实时推荐 val testUserId = 1 println(s\u0026#34;\\n👤 测试用户 $testUserId 的推荐:\u0026#34;) val recs = getRecommendations(spark, model, testUserId, topK, productsDF) if (recs.nonEmpty) { recs.foreach { case (productId, score) =\u0026gt; val productNameRow = productsDF.filter($\u0026#34;product_id\u0026#34; === productId) .select(\u0026#34;product_name\u0026#34;).take(1).headOption productNameRow match { case Some(row) =\u0026gt; val name = row.getString(0) println(f\u0026#34; - $name (ID=$productId), 分数: $score%.4f\u0026#34;) case None =\u0026gt; println(f\u0026#34; - 商品ID=$productId (名称不存在), 分数: $score%.4f\u0026#34;) } } } else { println(\u0026#34; ⚠️ 未获取到推荐结果\u0026#34;) } println(\u0026#34;\\n\u0026#34; + \u0026#34;=\u0026#34; * 80) println(\u0026#34;✅ 推荐服务运行完成\u0026#34;) println(\u0026#34;=\u0026#34; * 80) } catch { case e: InterruptedException =\u0026gt; println(s\u0026#34;\\n⚠️ 程序被中断: ${e.getMessage}\u0026#34;) case e: Exception =\u0026gt; println(s\u0026#34;\\n❌ 服务运行出错: ${e.getClass.getSimpleName} - ${e.getMessage}\u0026#34;) e.printStackTrace() } finally { closeRedisPool() if (spark != null) { println(\u0026#34;\\n🛑 正在关闭 SparkSession...\u0026#34;) spark.stop() println(\u0026#34;✅ SparkSession 已关闭\u0026#34;) } } } def getRecommendations( spark: SparkSession, model: ALSModel, userId: Int, topK: Int, productsDF: org.apache.spark.sql.DataFrame): Seq[(Int, Double)] = { import spark.implicits._ val cached = withJedis { jedis =\u0026gt; Option(jedis.get(s\u0026#34;rec:cf:user:$userId:products\u0026#34;)) } cached match { case Some(productIdsStr) if productIdsStr.nonEmpty =\u0026gt; productIdsStr.split(\u0026#34;,\u0026#34;).map(_.toInt).zipWithIndex.map { case (pid, idx) =\u0026gt; (pid, 1.0 - idx * 0.1) }.toSeq case None =\u0026gt; println(s\u0026#34;缓存未命中，实时计算用户 $userId 的推荐\u0026#34;) try { val userDF = Seq(userId).toDF(\u0026#34;userId\u0026#34;) val recs = model.recommendForUserSubset(userDF, topK) .select(explode($\u0026#34;recommendations\u0026#34;).as(\u0026#34;rec\u0026#34;)) .select($\u0026#34;rec.productId\u0026#34;.as(\u0026#34;productId\u0026#34;), $\u0026#34;rec.rating\u0026#34;.as(\u0026#34;score\u0026#34;)) .collect() .map(row =\u0026gt; (row.getInt(0), row.getFloat(1).toDouble)) val ttlSeconds = ConfigManager.getCacheTtlHours * 3600 withJedis { jedis =\u0026gt; val productIds = recs.map(_._1).mkString(\u0026#34;,\u0026#34;) jedis.setex(s\u0026#34;rec:cf:user:$userId:products\u0026#34;, ttlSeconds, productIds) } recs.toSeq } catch { case e: Exception =\u0026gt; println(s\u0026#34;实时推荐失败，回退到热门推荐: ${e.getMessage}\u0026#34;) getHotProducts(productsDF, topK) } } } def getHotProducts( productsDF: org.apache.spark.sql.DataFrame, topK: Int): Seq[(Int, Double)] = { productsDF.select(\u0026#34;product_id\u0026#34;) .limit(topK) .collect() .zipWithIndex .map { case (row, idx) =\u0026gt; (row.getInt(0), 1.0 - idx * 0.1) } .toSeq } }\r8. 运行推荐系统\r#\r# 编译项目 mvn clean compile # 运行协同过滤推荐 mvn exec:java -Dexec.mainClass=\u0026#34;CollaborativeFiltering\u0026#34; # 运行基于内容的推荐 mvn exec:java -Dexec.mainClass=\u0026#34;ContentBasedRecommendation\u0026#34; # 运行混合推荐系统 mvn exec:java -Dexec.mainClass=\u0026#34;HybridRecommendation\u0026#34; # 运行推荐系统评估 mvn exec:java -Dexec.mainClass=\u0026#34;RecommendationEvaluator\u0026#34; # 部署推荐系统服务 mvn exec:java -Dexec.mainClass=\u0026#34;RecommendationService\u0026#34;\r知识点4：企业级推荐系统架构\r#\r4.1 生产环境推荐系统架构\r#\r┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │ 用户请求 │────\u0026gt;│ API网关 │────\u0026gt;│ 推荐服务 │ │ (Web/App) │ │ (Nginx) │ │ (Spring) │ └─────────────┘ └──────────────┘ └──────┬───────┘ │ ┌──────────────────┼──────────────────┐ │ │ │ v v v ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ Redis缓存 │ │ 推荐引擎 │ │ 热门推荐 │ │ (命中返回) │ │ (Spark) │ │ (降级策略) │ └──────────────┘ └──────┬───────┘ └──────────────┘ │ ┌────────────────┼────────────────┐ │ │ │ v v v ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ 协同过滤 │ │ 内容推荐 │ │ 实时推荐 │ │ (离线计算) │ │ (离线计算) │ │ (在线计算) │ └──────────────┘ └──────────────┘ └──────────────┘\r4.2 推荐系统安全考量\r#\r安全风险 防护措施 本实验实现 数据库密码泄露 配置外部化，不硬编码 application.conf Redis未授权访问 设置密码，连接池管理 JedisPool + 密码认证 推荐结果被篡改 缓存设置TTL，定期刷新 setex + 过期策略 服务不可用 降级策略，热门推荐兜底 getHotProducts 数据注入 输入验证，参数化查询 DataFrame filter 资源泄漏 try-finally确保释放 SparkSession/Redis 4.3 推荐系统性能优化\r#\r优化方向 方法 说明 离线预计算 定时批量生成推荐结果 减少在线计算压力 多级缓存 Redis + 本地缓存 降低延迟 增量更新 只更新变化的用户 减少计算量 模型压缩 降低rank维度 减少存储和计算 并行化 增加Spark分区数 提高吞吐量 实验扩展\r#\r隐式反馈推荐：将用户行为（浏览、点击、购买）转化为隐式反馈，使用ALS.trainImplicit训练模型 实时推荐：结合Spark Streaming，根据用户实时行为动态调整推荐结果 冷启动处理：为新用户实现基于人口统计学的推荐，为新商品实现基于内容的推荐 推荐解释：为推荐结果生成解释（如\u0026quot;因为你购买了iPhone，推荐了AirPods\u0026quot;） A/B测试：设计A/B测试框架，对比不同推荐算法的线上效果 多样性优化：在推荐列表中引入随机性，避免\u0026quot;信息茧房\u0026quot;效应 实验总结\r#\r本实验通过以下步骤实现了电商平台的企业级商品推荐系统：\n理论理解：深入理解了协同过滤（ALS矩阵分解）、基于内容（TF-IDF）、混合推荐三种核心算法的原理和适用场景 协同过滤实现：基于Spark MLlib的ALS算法，实现了用户-商品评分预测和个性化推荐 基于内容实现：通过TF-IDF提取商品特征，基于余弦相似度计算商品关联，解决物品冷启动问题 混合推荐实现：采用加权融合策略，结合协同过滤和基于内容的优势，通过归一化消除量纲差异 多维度评估：实现了预测准确度（RMSE/MAE/R²）和排序质量（Precision@K/Recall@K/F1@K）的完整评估体系 生产级部署：实现了配置外部化、Redis连接池管理、缓存降级策略、资源安全管理等企业级特性 通过本实验，我们不仅掌握了推荐系统的核心算法实现，更理解了从算法原型到生产级服务的完整链路，为企业级推荐系统的开发与部署奠定了坚实基础。\n","date":"May 29, 2026","externalUrl":null,"permalink":"/posts/%E7%BB%BC%E5%90%88%E9%A1%B9%E7%9B%AE-3-%E5%95%86%E5%93%81%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/","section":"Posts","summary":"商品推荐系统\r#\r实验目的\r#\r本实验旨在通过Spark MLlib实现电商平台的企业级商品推荐系统，掌握以下核心技能：\n理解推荐系统的核心算法原理（协同过滤、基于内容、混合推荐） 实现基于ALS的协同过滤推荐算法 实现基于TF-IDF的内容推荐算法 构建加权混合推荐系统 掌握推荐系统的多维度评估方法 实现推荐系统的生产级部署方案 知识点1：推荐系统概述\r#\r1.1 什么是推荐系统\r#\r推荐系统（Recommendation System）是信息过滤系统的一个子类，用于预测用户对物品的\"评分\"或\"偏好\"。在电商场景中，推荐系统帮助用户从海量商品中发现感兴趣的内容，同时帮助平台提升转化率和用户黏性。\n推荐系统的核心目标：\n目标 说明 衡量指标 准确性 推荐的物品符合用户兴趣 RMSE、Precision、Recall 多样性 推荐列表覆盖不同类别 覆盖率、熵 新颖性 推荐用户未发现但感兴趣的物品 长尾覆盖率 实时性 及时响应用户行为变化 延迟时间 1.2 推荐系统分类\r#\r推荐系统 ├── 基于人口的统计学推荐（Demographic-based） │ └── 根据用户基本信息（年龄、性别、地区）推荐 ├── 协同过滤推荐（Collaborative Filtering） │ ├── 基于用户的协同过滤（User-CF） │ ├── 基于物品的协同过滤（Item-CF） │ └── 基于模型的协同过滤（Model-based CF，如ALS） ├── 基于内容的推荐（Content-based） │ ├── 基于物品属性（TF-IDF、Word2Vec） │ └── 基于用户画像 └── 混合推荐（Hybrid） ├── 加权混合 ├── 切换混合 ├── 特征组合 └── 级联混合\r1.3 电商推荐场景\r#\r场景 推荐策略 典型位置 首页推荐 热门+个性化混合 首页商品流 猜你喜欢 协同过滤+内容 个人中心 相似商品 基于内容 商品详情页 购后推荐 关联规则+协同过滤 订单完成页 搜索推荐 搜索词+用户画像 搜索结果页 知识点2：协同过滤与ALS算法原理\r#\r2.1 协同过滤的核心思想\r#\r协同过滤的核心假设：如果两个用户在过去对某些物品有相似的偏好，那么他们在未来对其他物品也会有相似的偏好。\n","title":"商品推荐系统","type":"posts"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/","section":"Tags","summary":"","title":"推荐系统","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/%E5%8D%8F%E5%90%8C%E8%BF%87%E6%BB%A4/","section":"Tags","summary":"","title":"协同过滤","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/%E7%94%A8%E6%88%B7%E7%94%BB%E5%83%8F/","section":"Tags","summary":"","title":"用户画像","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/tags/%E8%87%AA%E5%8A%A8%E5%8C%96/","section":"Tags","summary":"","title":"自动化","type":"tags"},{"content":"","date":"May 29, 2026","externalUrl":null,"permalink":"/categories/%E7%BB%BC%E5%90%88%E9%A1%B9%E7%9B%AE/","section":"Categories","summary":"","title":"综合项目","type":"categories"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]