广告点击系统实时分析
广告来自于广告或者移动App等,广告需要设定在具体的广告位,当用户点击广告的时候,一般都会通过ajax或Socket往后台发送日志数据,在这里我们是要做基于SparkStreaming做实时在线统计。那么数据就需要放进消息系统(Kafka)中,我们的Spark Streaming应用程序就会去Kafka中Pull数据过来进行计算和消费,并把计算后的数据放入到持久化系统中(MySQL)
广告点击系统实时分析的意义:因为可以在线实时的看见广告的投放效果,就为广告的更大规模的投入和调整打下了坚实的基础,从而为公司带来最大化的经济回报。
核心需求:
1、实时黑名单动态过滤出有效的用户广告点击行为:因为黑名单用户可能随时出现,所以需要动态更新;
2、在线计算广告点击流量;
3、Top3热门广告;
4、每个广告流量趋势;
5、广告点击用户的区域分布分析
6、最近一分钟的广告点击量;
7、整个广告点击Spark Streaming处理程序7*24小时运行;
数据格式:
时间、用户、广告、城市等
技术细节:
在线计算用户点击的次数分析,屏蔽IP等;
使用updateStateByKey或者mapWithState进行不同地区广告点击排名的计算;
Spark Streaming+Spark SQL+Spark Core等综合分析数据;
使用Window类型的操作;
高可用和性能调优等等;
流量趋势,一般会结合DB等;
Spark Core
/**
*
*/
package com.tom.spark.SparkApps.sparkstreaming;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import kafka.javaapi.producer.Producer;
import kafka.producer.KeyedMessage;
import kafka.producer.ProducerConfig;
/**
* 数据生成代码,Kafka Producer产生数据
*/
public class MockAdClickedStat {
/**
* @param args
*/
public static void main(String[] args) {
final Random random = new Random();
final String[] provinces = new String[]{"Guangdong", "Zhejiang", "Jiangsu", "Fujian"};
final Map cities = new HashMap();
cities.put("Guangdong", new String[]{"Guangzhou", "Shenzhen", "Dongguan"});
cities.put("Zhejiang", new String[]{"Hangzhou", "Wenzhou", "Ningbo"});
cities.put("Jiangsu", new String[]{"Nanjing", "Suzhou", "Wuxi"});
cities.put("Fujian", new String[]{"Fuzhou", "Xiamen", "Sanming"});
final String[] ips = new String[] {
"192.168.112.240",
"192.168.112.239",
"192.168.112.245",
"192.168.112.246",
"192.168.112.247",
"192.168.112.248",
"192.168.112.249",
"192.168.112.250",
"192.168.112.251",
"192.168.112.252",
"192.168.112.253",
"192.168.112.254",
};
/**
* Kafka相关的基本配置信息
*/
Properties kafkaConf = new Properties();
kafkaConf.put("serializer.class", "kafka.serializer.StringEncoder");
kafkaConf.put("metadeta.broker.list", "Master:9092,Worker1:9092,Worker2:9092");
ProducerConfig producerConfig = new ProducerConfig(kafkaConf);
final Producer producer = new Producer(producerConfig);
new Thread(new Runnable() {
public void run() {
while(true) {
//在线处理广告点击流的基本数据格式:timestamp、ip、userID、adID、province、city
Long timestamp = new Date().getTime();
String ip = ips[random.nextInt(12)]; //可以采用网络上免费提供的ip库
int userID = random.nextInt(10000);
int adID = random.nextInt(100);
String province = provinces[random.nextInt(4)];
String city = cities.get(province)[random.nextInt(3)];
String clickedAd = timestamp + "\t" + ip + "\t" + userID + "\t" + adID + "\t" + province + "\t" + city;
producer.send(new KeyedMessage("AdClicked", clickedAd));
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}).start();
}
}
package com.tom.spark.SparkApps.sparkstreaming;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
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 kafka.serializer.StringDecoder;
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.JavaSparkContext;
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.sql.DataFrame;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.RowFactory;
import org.apache.spark.sql.hive.HiveContext;
import org.apache.spark.sql.types.DataTypes;
import org.apache.spark.sql.types.StructType;
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.api.java.JavaStreamingContextFactory;
import org.apache.spark.streaming.kafka.KafkaUtils;
import com.google.common.base.Optional;
import scala.Tuple2;
/**
* 数据处理,Kafka消费者
*/
public class AdClickedStreamingStats {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
//好处:1、checkpoint 2、工厂
final SparkConf conf = new SparkConf().setAppName("SparkStreamingOnKafkaDirect").setMaster("hdfs://Master:7077/");
final String checkpointDirectory = "hdfs://Master:9000/library/SparkStreaming/CheckPoint_Data";
JavaStreamingContextFactory factory = new JavaStreamingContextFactory() {
public JavaStreamingContext create() {
// TODO Auto-generated method stub
return createContext(checkpointDirectory, conf);
}
};
/**
* 可以从失败中恢复Driver,不过还需要指定Driver这个进程运行在Cluster,并且在提交应用程序的时候制定--supervise;
*/
JavaStreamingContext javassc = JavaStreamingContext.getOrCreate(checkpointDirectory, factory);
/**
* 第三步:创建Spark Streaming输入数据来源input Stream:
* 1、数据输入来源可以基于File、HDFS、Flume、Kafka、Socket等
* 2、在这里我们指定数据来源于网络Socket端口,Spark Streaming连接上该端口并在运行的时候一直监听该端口的数据
* (当然该端口服务首先必须存在),并且在后续会根据业务需要不断有数据产生(当然对于Spark Streaming
* 应用程序的运行而言,有无数据其处理流程都是一样的)
* 3、如果经常在每间隔5秒钟没有数据的话不断启动空的Job其实会造成调度资源的浪费,因为并没有数据需要发生计算;所以
* 实际的企业级生成环境的代码在具体提交Job前会判断是否有数据,如果没有的话就不再提交Job;
*/
//创建Kafka元数据来让Spark Streaming这个Kafka Consumer利用
Map kafkaParameters = new HashMap();
kafkaParameters.put("metadata.broker.list", "Master:9092,Worker1:9092,Worker2:9092");
Set topics = new HashSet();
topics.add("SparkStreamingDirected");
JavaPairInputDStream adClickedStreaming = KafkaUtils.createDirectStream(javassc,
String.class, String.class,
StringDecoder.class, StringDecoder.class,
kafkaParameters,
topics);
/**因为要对黑名单进行过滤,而数据是在RDD中的,所以必然使用transform这个函数;
* 但是在这里我们必须使用transformToPair,原因是读取进来的Kafka的数据是Pair类型,
* 另一个原因是过滤后的数据要进行进一步处理,所以必须是读进的Kafka数据的原始类型
*
* 在此再次说明,每个Batch Duration中实际上讲输入的数据就是被一个且仅被一个RDD封装的,你可以有多个
* InputDStream,但其实在产生job的时候,这些不同的InputDStream在Batch Duration中就相当于Spark基于HDFS
* 数据操作的不同文件来源而已罢了。
*/
JavaPairDStream filteredadClickedStreaming = adClickedStreaming.transformToPair(new Function, JavaPairRDD>() {
public JavaPairRDD call(
JavaPairRDD rdd) throws Exception {
/**
* 在线黑名单过滤思路步骤:
* 1、从数据库中获取黑名单转换成RDD,即新的RDD实例封装黑名单数据;
* 2、然后把代表黑名单的RDD的实例和Batch Duration产生的RDD进行Join操作,
* 准确的说是进行leftOuterJoin操作,也就是说使用Batch Duration产生的RDD和代表黑名单的RDD实例进行
* leftOuterJoin操作,如果两者都有内容的话,就会是true,否则的话就是false
*
* 我们要留下的是leftOuterJoin结果为false;
*
*/
final List blackListNames = new ArrayList();
JDBCWrapper jdbcWrapper = JDBCWrapper.getJDBCInstance();
jdbcWrapper.doQuery("SELECT * FROM blacklisttable", null, new ExecuteCallBack() {
public void resultCallBack(ResultSet result) throws Exception {
while(result.next()){
blackListNames.add(result.getString(1));
}
}
});
List> blackListTuple = new ArrayList>();
for(String name : blackListNames) {
blackListTuple.add(new Tuple2(name, true));
}
List> blacklistFromListDB = blackListTuple; //数据来自于查询的黑名单表并且映射成为
JavaSparkContext jsc = new JavaSparkContext(rdd.context());
/**
* 黑名单的表中只有userID,但是如果要进行join操作的话就必须是Key-Value,所以在这里我们需要
* 基于数据表中的数据产生Key-Value类型的数据集合
*/
JavaPairRDD blackListRDD = jsc.parallelizePairs(blacklistFromListDB);
/**
* 进行操作的时候肯定是基于userID进行join,所以必须把传入的rdd进行mapToPair操作转化成为符合格式的RDD
*
*/
JavaPairRDD> rdd2Pair = rdd.mapToPair(new PairFunction, String, Tuple2>() {
public Tuple2> call(
Tuple2 t) throws Exception {
// TODO Auto-generated method stub
String userID = t._2.split("\t")[2];
return new Tuple2>(userID, t);
}
});
JavaPairRDD, Optional>> joined = rdd2Pair.leftOuterJoin(blackListRDD);
JavaPairRDD result = joined.filter(new Function,Optional>>, Boolean>() {
public Boolean call(
Tuple2, Optional>> tuple)
throws Exception {
// TODO Auto-generated method stub
Optional optional = tuple._2._2;
if(optional.isPresent() && optional.get()){
return false;
} else {
return true;
}
}
}).mapToPair(new PairFunction,Optional>>, String, String>() {
public Tuple2 call(
Tuple2, Optional>> t)
throws Exception {
// TODO Auto-generated method stub
return t._2._1;
}
});
return result;
}
});
//广告点击的基本数据格式:timestamp、ip、userID、adID、province、city
JavaPairDStream pairs = filteredadClickedStreaming.mapToPair(new PairFunction, String, Long>() {
public Tuple2 call(Tuple2 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(clickedRecord, 1L);
}
});
/**
* 第4.3步:在单词实例计数为1基础上,统计每个单词在文件中出现的总次数
*/
JavaPairDStream adClickedUsers= pairs.reduceByKey(new Function2() {
public Long call(Long i1, Long i2) throws Exception{
return i1 + i2;
}
});
/*判断有效的点击,复杂化的采用机器学习训练模型进行在线过滤 简单的根据ip判断1天不超过100次;也可以通过一个batch duration的点击次数
判断是否非法广告点击,通过一个batch来判断是不完整的,还需要一天的数据也可以每一个小时来判断。*/
JavaPairDStream filterClickedBatch = adClickedUsers.filter(new Function, Boolean>() {
public Boolean call(Tuple2 v1) throws Exception {
if (1 < v1._2){
//更新一些黑名单的数据库表
return false;
} else {
return true;
}
}
});
//filterClickedBatch.print();
//写入数据库
filterClickedBatch.foreachRDD(new Function, Void>() {
public Void call(JavaPairRDD rdd) throws Exception {
rdd.foreachPartition(new VoidFunction>>() {
public void call(Iterator> partition) throws Exception {
//使用数据库连接池的高效读写数据库的方式将数据写入数据库mysql
//例如一次插入 1000条 records,使用insertBatch 或 updateBatch
//插入的用户数据信息:userID,adID,clickedCount,time
//这里面有一个问题,可能出现两条记录的key是一样的,此时需要更新累加操作
List userAdClickedList = new ArrayList();
while(partition.hasNext()) {
Tuple2 record = partition.next();
String[] splited = record._1.split("\t");
UserAdClicked userClicked = new UserAdClicked();
userClicked.setTimestamp(splited[0]);
userClicked.setIp(splited[1]);
userClicked.setUserID(splited[2]);
userClicked.setAdID(splited[3]);
userClicked.setProvince(splited[4]);
userClicked.setCity(splited[5]);
userAdClickedList.add(userClicked);
}
final List inserting = new ArrayList();
final List updating = new ArrayList();
JDBCWrapper jdbcWrapper = JDBCWrapper.getJDBCInstance();
//表的字段timestamp、ip、userID、adID、province、city、clickedCount
for(final UserAdClicked clicked : userAdClickedList) {
jdbcWrapper.doQuery("SELECT clickedCount FROM adclicked WHERE"
+ " timestamp =? AND userID = ? AND adID = ?",
new Object[]{clicked.getTimestamp(), clicked.getUserID(),
clicked.getAdID()}, new ExecuteCallBack() {
public void resultCallBack(ResultSet result) throws Exception {
// TODO Auto-generated method stub
if(result.next()) {
long count = result.getLong(1);
clicked.setClickedCount(count);
updating.add(clicked);
} else {
inserting.add(clicked);
clicked.setClickedCount(1L);
}
}
});
}
//表的字段timestamp、ip、userID、adID、province、city、clickedCount
List