有兴趣想学习国内整套Spark+Spark Streaming+Machine learning顶级课程的,可加我qq 471186150。共享视频,性价比超高!
1:传统的关系型数据库都是伪分布式,在100个使用了底层数据持久化层的案例中,至少有90个底层都是基于数据库的。当然可以选择基于Hbase或者Redis,但是实际上大多数人底层都会基于数据库。因为1,数据库可以非常高效的读写,尤其频繁的批量的插入,数据库特别适合。2,传统的it系统大多数都是基于数据库,所以spark处理的结果,如果仍给了数据库,传统的javaee系统例如spring,这些系统可以直接基于数据库的驱动,或者三方框架去操作数据库中的数据。而数据库中的i数据是spark计算后的结果,就可以操作这些数据绘制趋势图等。如股票一直变化的趋势图,用spark+数据库+javaee绘制就特别适合。如每隔十秒更新一次,图是一直在动的。这就是为什么很多人都使用数据库来做大数据系统计算后的结果。
package com.dt.streaming; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.LinkedBlockingQueue; import org.apache.spark.SparkConf; import org.apache.spark.api.java.JavaPairRDD; import org.apache.spark.api.java.JavaRDD; import org.apache.spark.api.java.function.Function; import org.apache.spark.api.java.function.Function2; import org.apache.spark.api.java.function.PairFunction; import org.apache.spark.api.java.function.VoidFunction; import org.apache.spark.streaming.Durations; import org.apache.spark.streaming.api.java.JavaDStream; import org.apache.spark.streaming.api.java.JavaPairDStream; import org.apache.spark.streaming.api.java.JavaPairInputDStream; import org.apache.spark.streaming.api.java.JavaStreamingContext; import org.apache.spark.streaming.kafka.KafkaUtils; import kafka.serializer.StringDecoder; import scala.Tuple2; /** * * 第107讲:在线处理广告点击流 * 广告点击的基本数据格式:timestamp、ip、userID、adID、province、city * * @author hp * */ public class AdClickedStreamingStats { public static void main(String[] args) { /* * 第一步:配置SparkConf: * 1,至少2条线程:因为Spark Streaming应用程序在运行的时候,至少有一条 * 线程用于不断的循环接收数据,并且至少有一条线程用于处理接受的数据(否则的话无法 * 有线程用于处理数据,随着时间的推移,内存和磁盘都会不堪重负); * 2,对于集群而言,每个Executor一般肯定不止一个Thread,那对于处理Spark Streaming的 * 应用程序而言,每个Executor一般分配多少Core比较合适?根据我们过去的经验,5个左右的 * Core是最佳的(一个段子分配为奇数个Core表现最佳,例如3个、5个、7个Core等); */ SparkConf conf = new SparkConf().setMaster("local[5]"). setAppName("AdClickedStreamingStats"); /*SparkConf conf = new SparkConf().setMaster("spark://Master:7077"). setAppName("SparkStreamingOnKafkaReceiver");*/ /* * 第二步:创建SparkStreamingContext: * 1,这个是SparkStreaming应用程序所有功能的起始点和程序调度的核心 * SparkStreamingContext的构建可以基于SparkConf参数,也可基于持久化的SparkStreamingContext的内容 * 来恢复过来(典型的场景是Driver崩溃后重新启动,由于Spark Streaming具有连续7*24小时不间断运行的特征, * 所有需要在Driver重新启动后继续上衣系的状态,此时的状态恢复需要基于曾经的Checkpoint); * 2,在一个Spark Streaming应用程序中可以创建若干个SparkStreamingContext对象,使用下一个SparkStreamingContext * 之前需要把前面正在运行的SparkStreamingContext对象关闭掉,由此,我们获得一个重大的启发SparkStreaming框架也只是 * Spark Core上的一个应用程序而已,只不过Spark Streaming框架箱运行的话需要Spark工程师写业务逻辑处理代码; */ JavaStreamingContext jsc = new JavaStreamingContext(conf, Durations.seconds(10)); /* * 第三步:创建Spark Streaming输入数据来源input Stream: * 1,数据输入来源可以基于File、HDFS、Flume、Kafka、Socket等 * 2, 在这里我们指定数据来源于网络Socket端口,Spark Streaming连接上该端口并在运行的时候一直监听该端口 * 的数据(当然该端口服务首先必须存在),并且在后续会根据业务需要不断的有数据产生(当然对于Spark Streaming * 应用程序的运行而言,有无数据其处理流程都是一样的); * 3,如果经常在每间隔5秒钟没有数据的话不断的启动空的Job其实是会造成调度资源的浪费,因为并没有数据需要发生计算,所以 * 实例的企业级生成环境的代码在具体提交Job前会判断是否有数据,如果没有的话就不再提交Job; * 4,在本案例中具体参数含义: * 第一个参数是StreamingContext实例; * 第二个参数是ZooKeeper集群信息(接受Kafka数据的时候会从ZooKeeper中获得Offset等元数据信息) * 第三个参数是Consumer Group * 第四个参数是消费的Topic以及并发读取Topic中Partition的线程数 */ /** * 创建Kafka元数据,来让Spark Streaming这个Kafka Consumer利用 */ Map<String, String> kafkaParameters = new HashMap<String, String>(); kafkaParameters.put("metadata.broker.list", "Master:9092,Worker1:9092,Worker2:9092"); Set<String> topics = new HashSet<String>(); topics.add("AdClicked"); JavaPairInputDStream<String, String> adClickedStreaming = KafkaUtils.createDirectStream(jsc, String.class, String.class, StringDecoder.class, StringDecoder.class, kafkaParameters, topics); /* * 第四步:接下来就像对于RDD编程一样基于DStream进行编程!!!原因是DStream是RDD产生的模板(或者说类),在Spark Streaming具体 * 发生计算前,其实质是把每个Batch的DStream的操作翻译成为对RDD的操作!!! *对初始的DStream进行Transformation级别的处理,例如map、filter等高阶函数等的编程,来进行具体的数据计算 * 广告点击的基本数据格式:timestamp、ip、userID、adID、province、city */ JavaPairDStream<String, Long> pairs = adClickedStreaming.mapToPair(new PairFunction<Tuple2<String,String>, String, Long>() { @Override public Tuple2<String, Long> call(Tuple2<String, String> t) throws Exception { String[] splited = t._2.split("\t"); String timestamp = splited[0]; //yyyy-MM-dd String ip = splited[1]; String userID = splited[2]; String adID = splited[3]; String province = splited[4]; String city = splited[5]; String clickedRecord = timestamp + "_" + ip + "_" + userID + "_" + adID + "_" + province + "_" + city; return new Tuple2<String, Long>(clickedRecord, 1L); } }); /* * 第四步:对初始的DStream进行Transformation级别的处理,例如map、filter等高阶函数等的编程,来进行具体的数据计算 * 计算每个Batch Duration中每个User的广告点击量 */ JavaPairDStream<String, Long> adClickedUsers = pairs.reduceByKey(new Function2<Long, Long, Long>(){ @Override public Long call(Long v1, Long v2) throws Exception { // TODO Auto-generated method stub return v1 + v2; } }); /** * * 计算出什么叫有效的点击? * 1,复杂化的一般都是采用机器学习训练好模型直接在线进行过滤; * 2,简单的?可以通过一个Batch Duration中的点击次数来判断是不是非法广告点击,但是实际上讲非法广告 * 点击程序会尽可能模拟真实的广告点击行为,所以通过一个Batch来判断是 不完整的,我们需要对例如一天(也可以是每一个小时) * 的数据进行判断! * 3,比在线机器学习退而求次的做法如下: * 例如:一段时间内,同一个IP(MAC地址)有多个用户的帐号访问; * 例如:可以统一一天内一个用户点击广告的次数,如果一天点击同样的广告操作50次的话,就列入黑名单; * * 黑名单有一个重点的特征:动态生成!!!所以每一个Batch Duration都要考虑是否有新的黑名单加入,此时黑名单需要存储起来 * 具体存储在什么地方呢,存储在DB/Redis中即可; * * 例如邮件系统中的“黑名单”,可以采用Spark Streaming不断的监控每个用户的操作,如果用户发送邮件的频率超过了设定的值,可以 * 暂时把用户列入“黑名单”,从而阻止用户过度频繁的发送邮件。 */ JavaPairDStream<String, Long> filteredClickInBatch = adClickedUsers.filter(new Function<Tuple2<String,Long>, Boolean>() { @Override public Boolean call(Tuple2<String, Long> v1) throws Exception { if ( 1 < v1._2){ //更新一下黑名单的数据表 return false; } else { return true; } } }); // Todo。。。。 /* * 此处的print并不会直接出发Job的执行,因为现在的一切都是在Spark Streaming框架的控制之下的,对于Spark Streaming * 而言具体是否触发真正的Job运行是基于设置的Duration时间间隔的 * * 诸位一定要注意的是Spark Streaming应用程序要想执行具体的Job,对Dtream就必须有output Stream操作, * output Stream有很多类型的函数触发,类print、saveAsTextFile、saveAsHadoopFiles等,最为重要的一个 * 方法是foraeachRDD,因为Spark Streaming处理的结果一般都会放在Redis、DB、DashBoard等上面,foreachRDD * 主要就是用用来完成这些功能的,而且可以随意的自定义具体数据到底放在哪里!!! * */ // filteredClickInBatch.print(); filteredClickInBatch.foreachRDD(new Function<JavaPairRDD<String,Long>, Void>() { @Override public Void call(JavaPairRDD<String, Long> rdd) throws Exception { rdd.foreachPartition(new VoidFunction<Iterator<Tuple2<String,Long>>>() { @Override public void call(Iterator<Tuple2<String, Long>> partition) throws Exception { /** * 在这里我们使用数据库连接池的高效读写数据库的方式把数据写入数据库MySQL; * 由于传入的参数是一个Iterator类型的集合,所以为了更加高效的操作我们需要批量处理 * 例如说一次性插入1000条Record,使用insertBatch或者updateBatch类型的操作; * 插入的用户信息可以只包含:useID、adID、clickedCount、time * 这里面有一个问题:可能出现两条记录的Key是一样的,此时就需要更新累加操作 */ } }); return null; } }); JavaPairDStream<String, Long> blackListBasedOnHistory = filteredClickInBatch.filter(new Function<Tuple2<String,Long>, Boolean>() { @Override public Boolean call(Tuple2<String, Long> v1) throws Exception { //广告点击的基本数据格式:timestamp、ip、userID、adID、province、city String[] splited = v1._1.split("\t"); String date = splited[0]; String userID = splited[2]; String adID = splited[3]; /** * 接下来根据date、userID、adID等条件去查询用户点击广告的数据表,获得总的点击次数 * 这个时候基于点击次数判断是否属于黑名单点击 * */ int clickedCountTotalToday = 81; if (clickedCountTotalToday > 50) { return true; } else { return false; } } }); /** * 必须对黑名单的整个RDD进行去重操作!!! */ JavaDStream<String> blackListuserIDtBasedOnHistory = blackListBasedOnHistory.map(new Function<Tuple2<String,Long>, String>() { @Override public String call(Tuple2<String, Long> v1) throws Exception { // TODO Auto-generated method stub return v1._1.split("\t")[2]; } }); JavaDStream<String> blackListUniqueuserIDtBasedOnHistory = blackListuserIDtBasedOnHistory.transform(new Function<JavaRDD<String>, JavaRDD<String>>() { @Override public JavaRDD<String> call(JavaRDD<String> rdd) throws Exception { // TODO Auto-generated method stub return rdd.distinct(); } }); //下一步写入黑名单数据表中 blackListUniqueuserIDtBasedOnHistory.foreachRDD(new Function<JavaRDD<String>, Void>() { @Override public Void call(JavaRDD<String> rdd) throws Exception { rdd.foreachPartition(new VoidFunction<Iterator<String>>() { @Override public void call(Iterator<String> t) throws Exception { /** * 在这里我们使用数据库连接池的高效读写数据库的方式把数据写入数据库MySQL; * 由于传入的参数是一个Iterator类型的集合,所以为了更加高效的操作我们需要批量处理 * 例如说一次性插入1000条Record,使用insertBatch或者updateBatch类型的操作; * 插入的用户信息可以只包含:useID * 此时直接插入黑名单数据表即可。 */ } }); return null; } }); /* * Spark Streaming执行引擎也就是Driver开始运行,Driver启动的时候是位于一条新的线程中的,当然其内部有消息循环体,用于 * 接受应用程序本身或者Executor中的消息; */ jsc.start(); jsc.awaitTermination(); jsc.close(); } } class JDBCWrapper { private static JDBCWrapper jdbcInstance = null; private static LinkedBlockingQueue<Connection> dbConnectionPool = new LinkedBlockingQueue<Connection> (); //底层访问mysql,首先通过静态代码块来加载驱动,这里先硬编码,实际肯定结合架构设计做配置字符串 static { try { Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } //有了驱动,获取数据库的句柄,基于句柄做事情,下面是个单例 public static JDBCWrapper getJDBCInstance(){ if (jdbcInstance == null){ //同时三个线程来请求,如果不用同步synchronized,就会3个位null,弄出3个实例。 synchronized(JDBCWrapper.class){ if (jdbcInstance == null){ jdbcInstance = new JDBCWrapper(); } } } return jdbcInstance; } private JDBCWrapper(){ for (int i = 0; i < 10; i++){ //这里创建10个connection try { Connection conn = DriverManager.getConnection("jdbc:mysql://Master:3306/sparkstreaming","root","root"); dbConnectionPool.put(conn);//数据库更高效,使用连接池 } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } } //从池子中拿conn,考虑线程问题,加synchronized,让一个线程获得一个实例 public synchronized Connection getConnection(){ while (0 == dbConnectionPool.size()){//池子里面可能没东西,就死循环,确保下面能从池子里拿到实例 try { Thread.sleep(20); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return dbConnectionPool.poll();//拿到实例后,拿实例进行操作 } //拿到实例,做查询,插入,更新等操作,而且是批量处理,不是一条条处理。第1个参数是你的sql语句,第2个参数是一批参数,因为批量,返回数组 public int[] doBatch(String sqlText, List<Object[]> paramsList, ExecuteCallBack callBack) { Connection conn = getConnection(); PreparedStatement preparedStatement = null; int[] result = null; try { conn.setAutoCommit(false);//设置批量操作不能自动提交 preparedStatement = conn.prepareStatement(sqlText);//写入statement,传入sql for (Object[] parameters : paramsList){ for(int i = 0; i < parameters.length; i++){ preparedStatement.setObject(i+1, parameters[i]);//这里索引必须加1 } preparedStatement.addBatch(); } result = preparedStatement.executeBatch();//针对处理的结果,一般会有回调函数,要写个接口 conn.commit();//这里真正执行 } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { if (preparedStatement != null){ try { preparedStatement.close();//要关闭prepareStatement } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (conn != null){ try { dbConnectionPool.put(conn);//弄过之后,把conn放回池子中 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } return result; } public void doQuery(String sqlText, Object[] paramsList, ExecuteCallBack callBack) { Connection conn = getConnection(); PreparedStatement preparedStatement = null; ResultSet result = null; try { preparedStatement = conn.prepareStatement(sqlText); for(int i = 0; i < paramsList.length; i++){ preparedStatement.setObject(i+1, paramsList[i]); } result = preparedStatement.executeQuery(); callBack.resultCallBack(result); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { if (preparedStatement != null){ try { preparedStatement.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (conn != null){ try { dbConnectionPool.put(conn); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } } //针对处理的结果,一般会有回调函数,要写个接口,调用回调函数,把result传过去 interface ExecuteCallBack { void resultCallBack(ResultSet result) throws Exception; }