1.需求:
在给定的订单数据,根据订单的分类ID进行聚合,然后按照订单分类名称,统计出某一天商品各个分类的成交金额,然后在结合商品分类表匹配上对应的商品分类字段,然后将计算结果保存到mysql中,要求结果如图所示:
2.数据样例
{"cid": 1, "money": 600.0, "longitude":116.397128,"latitude":39.916527,"oid":"o123", }
"oid":"o112", "cid": 3, "money": 200.0, "longitude":118.396128,"latitude":35.916527}
{"oid":"o124", "cid": 2, "money": 200.0, "longitude":117.397128,"latitude":38.916527}
{"oid":"o125", "cid": 3, "money": 100.0, "longitude":118.397128,"latitude":35.916527}
{"oid":"o127", "cid": 1, "money": 100.0, "longitude":116.395128,"latitude":39.916527}
{"oid":"o128", "cid": 2, "money": 200.0, "longitude":117.396128,"latitude":38.916527}
{"oid":"o129", "cid": 3, "money": 300.0, "longitude":115.398128,"latitude":35.916527}
{"oid":"o130", "cid": 2, "money": 100.0, "longitude":116.397128,"latitude":39.916527}
{"oid":"o131", "cid": 1, "money": 100.0, "longitude":117.394128,"latitude":38.916527}
{"oid":"o132", "cid": 3, "money": 200.0, "longitude":118.396128,"latitude":35.916527}
字段说明:
oid:订单id,String类型
cid: 商品分类id,Int类型
money: 订单金额,Double类型
longitude: 经度,Double类型
latitude: 纬度,Double类型
2.商品分类表
分类信息
1,家具
2,手机
3,服装
3.idea中导入依赖
<properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <scala.version>2.12.10</scala.version> <spark.version>3.0.1</spark.version> <encoding>UTF-8</encoding> </properties> <dependencies> <!-- 导入scala的依赖 --> <dependency> <groupId>org.scala-lang</groupId> <artifactId>scala-library</artifactId> <version>${scala.version}</version> <!-- <scope>provided</scope>--> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency> <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-core_2.12</artifactId> <version>${spark.version}</version> <!-- <scope>provided</scope>--> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency> </dependencies> <build> <pluginManagement> <plugins> <!-- 编译scala的插件 --> <plugin> <groupId>net.alchim31.maven</groupId> <artifactId>scala-maven-plugin</artifactId> <version>3.2.2</version> </plugin> <!-- 编译java的插件 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> </plugin> </plugins> </pluginManagement> <plugins> <plugin> <groupId>net.alchim31.maven</groupId> <artifactId>scala-maven-plugin</artifactId> <executions> <execution> <id>scala-compile-first</id> <phase>process-resources</phase> <goals> <goal>add-source</goal> <goal>compile</goal> </goals> </execution> <execution> <id>scala-test-compile</id> <phase>process-test-resources</phase> <goals> <goal>testCompile</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <executions> <execution> <phase>compile</phase> <goals> <goal>compile</goal> </goals> </execution> </executions> </plugin> <!-- 打jar插件 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.4.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> </execution> </executions> </plugin> </plugins> </build>
4. 代码实现功能
package com.zxx.spark.day06 import java.sql.{Connection, Date, DriverManager, PreparedStatement, Statement} import com.alibaba.fastjson.{JSON, JSONObject} import org.apache.spark.rdd.RDD import org.apache.spark.{SparkConf, SparkContext} import org.slf4j.{Logger, LoggerFactory} object OrderCount1 { def main(args: Array[String]): Unit = { //记录日志的工具类 val logger: Logger = LoggerFactory.getLogger(this.getClass) //案例要点,json格式的解析,对脏数据的处理,jdbc链接数据库将数据写入数据库中,并且理解算子底层的实现 //先创建SparkContext和集群建立链接 val conf: SparkConf = new SparkConf().setAppName(this.getClass.getName) //添加一个判断,判断是否设置为集群或是,还是本地模式 val flag: Boolean = args(0).toBoolean if (flag) { conf.setMaster("local[*]") } val sc: SparkContext = new SparkContext(conf) //创建RDD val rdd: RDD[String] = sc.textFile(args(1)) //读取数据 //对读取到的数据进行处理,将json数据解析为jsonObject val cidAndMoney: RDD[(String, Double)] = rdd.map(e => { //new tuple2是为了接收try catch中的元组,返回 var tuple = new Tuple2[String, Double](null, 0.0) //将容易出现脏数据的代码用try catch包起来 try { val json: JSONObject = JSON.parseObject(e) val cid: String = json.getString("cid") val money: Double = json.getDouble("money") //将解析出来的数据放入到对偶元组中 tuple = (cid, money) } catch { case exception: Exception => logger.error(s"数据有误:$e") } tuple }) //调用reduceByKey算子,先对每个分区进行局部聚合,然后在全局进行全局聚合,reduceBykey会产生shuffle,并且reduceByKey底层调用的combineBykeywithClassTag //在底层new shuffleRDD,默认使用的是HashPartitioner, val reduced: RDD[(String, Double)] = cidAndMoney.reduceByKey(_ + _) //这里是过滤掉元组中第一个元素为空的值,filter底层是调用了MapPartitionRDD val filtered: RDD[(String, Double)] = reduced.filter(_._1 != null) //ArrayBuffer((2,500.0), (3,600.0), (1,800.0)) //读取分类数据,将所得的结果和分类结果表进行leftOuterjoin val rdd2: RDD[String] = sc.textFile(args(2)) val categoryTup: RDD[(String, String)] = rdd2.map(e => { val sp = e.split(",") (sp(0), sp(1)) }) //将以左表为主表进行leftOuterjoin, val res: RDD[(String, (Double, Option[String]))] = filtered.leftOuterJoin(categoryTup) //leftOuterJoin底层调用了cogroup,然后是通过判断for (v <- pair._1.iterator; w <- pair._2.iterator) yield (v, Some(w)) // println(res.collect().toBuffer)//ArrayBuffer((2,(500.0,Some(手机))), (3,(600.0,Some(服装))), (1,(800.0,Some(家具)))) res.foreachPartition(e => { //调用foreachPartition是遍历每一个分区,为每一个分区常见数据库链接,这样可以节省资源 //创建jdbc链接数据库 val conn: Connection = DriverManager.getConnection("jdbc:mysql://linux01:3306/db_demo?characterEncoding=utf8", "root", "root") val st: PreparedStatement = conn.prepareStatement("INSERT INTO tb_order_demo1(uid,money,category,dt) VALUES (?,?,?,?)") //将迭代器中的数据写去到数据库中,一个分区一个迭代器,这样可以节省资源,如果调用foreach的话,每来一条数据需要创建一次链接,浪费资源, //foreachPartition则是一个分区用一个数据库链接,并且数据库链接不能放在 foreachPartition外面,会报序列化错误,调用foreachPartition是将数据写入数据库 //在executor端执行,这样可以有多个task并行的写入数据库 try { e.foreach(j => { st.setString(1, j._1) st.setDouble(2, j._2._1) st.setString(3, j._2._2.get) val date: Date = new Date(System.currentTimeMillis()) st.setDate(4, date) st.executeUpdate() }) } catch { case exception: Exception => logger.error(s"数据库写入错误:$exception") } finally { //关闭链接 if (conn != null) { conn.close() } if (st != null) { st.close() } } }) //释放资源 sc.stop() } }
5.案例总结:
1.在scala中解析json格式的数据,一般使用alibaba的fastjson解析,将json数据解析为JsonObject对象,调用了fastjson中的parseObject方法,然后通过getString的方法根据key取出 json中对应的value
2.在解析json数据时,对脏数据的处理,一般使用try catch的方法,将读取数据和解析数据的代码包起来,这样程序在遇到脏数据也不会停止,如果对于脏数据有另外的需求,可以将脏数据保留,另做处理
3.聚合后再关联维度数据,减少关联查询的请求数据,提升效率
4.对于建立数据库链接,应该是对每一个分区建立一次链接,同一个分区中的每条数据可以使用同一个链接对象往数据库写数据所以对RDD调用foreachPartition,如果使用foreach就是rdd中的每一条数据就会创建一个jdbc链接对象,还有要注意的一点是,千万不能再driver端和数据库建立链接,因为RDD在触发action时会生成job,将task提交到executor端进行执行,需要将数据序列化,通过网络传到executor进行执行,如果在dirver端建立了链接,就会导致jdbc链接不能序列化而报错