1、广告点击流量实时统计模块
网站 / app,通常会给一些第三方的客户,打一些广告;也是一些互联网公司的核心收入来源;广告在网站 / app某个广告位打出去,在用户来使用网站 / app的时候,广告会显示出来;此时,有些用户可能就会去点击那个广告。
广告被点击以后,实际上,我们就是要针对这种用户行为(广告点击行为),实时数据,进行实时的大数据计算和统计。
每次点击一个广告以后,通常来说,网站 / app中都会有埋点(前端的应用中,比如JavaScript Ajax;app中的socket网络请求,往后台发送一条日志数据);日志数据而言,通常,如果要做实时统计的话,那么就会通过某些方式将数据写入到分布式消息队列中(Kafka);
日志写入到后台web服务器(nginx),nginx产生的实时的不断增加 / 更新的本地日志文件,就会被日志监控客户端(比如flume agent),写入到消息队列中(kafka),我们要负责编写实时计算程序,去从消息队列中(kafka)去实时地拉取数据,然后对数据进行实时的计算和统计。
这个模块的意义在于,让产品经理、高管可以实时地掌握到公司打的各种广告的投放效果。以便于后期持续地对公司的广告投放相关的战略和策略,进行调整和优化;以期望获得最好的广告收益。
2、需求分析
1、实现实时的动态黑名单机制:将每天对某个广告点击超过100次的用户拉黑
2、基于黑名单的非法广告点击流量过滤机制:
3、每天各省各城市各广告的点击流量实时统计:
4、统计每天各省top3热门广告
5、统计各广告最近1小时内的点击量趋势:各广告最近1小时内各分钟的点击量
6、使用高性能方式将实时统计结果写入MySQL
7、实现实时计算程序的HA高可用性(Spark Streaming HA方案)
8、实现实时计算程序的性能调优(Spark Streaming Performence Tuning方案)
3、技术方案设计
1、实时计算各batch中的每天各用户对各广告的点击次数
2、使用高性能方式将每天各用户对各广告的点击次数写入MySQL中(更新)
3、使用filter过滤出每天对某个广告点击超过100次的黑名单用户,并写入MySQL中
4、使用transform操作,对每个batch RDD进行处理,都动态加载MySQL中的黑名单生成RDD,然后进行join后,过滤掉batch RDD中的黑名单用户的广告点击行为
5、使用updateStateByKey操作,实时计算每天各省各城市各广告的点击量,并时候更新到MySQL
6、使用transform结合Spark SQL,统计每天各省份top3热门广告:首先以每天各省各城市各广告的点击量数据作为基础,首先统计出每天各省份各广告的点击量;然后启动一个异步子线程,使用Spark SQL动态将数据RDD转换为DataFrame后,注册为临时表;最后使用Spark SQL开窗函数,统计出各省份top3热门的广告,并更新到MySQL中
7、使用window操作,对最近1小时滑动窗口内的数据,计算出各广告各分钟的点击量,并更新到MySQL中
8、实现实时计算程序的HA高可用性
9、对实时计算程序进行性能调优
4、数据设计
数据格式介绍
ad_user_click_count
ad_blacklist
ad_stat
ad_province_top3
ad_click_trend
5、代码实现
5.1 为动态黑名单实时计算每天各用户对广告的点击次数
package cn.ctgu.sparkproject.spark.ad;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import kafka.serializer.StringDecoder;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.function.Function2;
import org.apache.spark.api.java.function.PairFunction;
import org.apache.spark.streaming.Durations;
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 scala.Tuple2;
import com.ibeifeng.sparkproject.conf.ConfigurationManager;
import com.ibeifeng.sparkproject.constant.Constants;
import com.ibeifeng.sparkproject.util.DateUtils;
/**
* 广告点击流量实时统计spark作业
* @author Administrator
*
*/
public class AdClickRealTimeStatSpark {
public static void main(String[] args) {
// 构建Spark Streaming上下文
SparkConf conf = new SparkConf()
.setMaster("local[2]")
.setAppName("AdClickRealTimeStatSpark");
// spark streaming的上下文是构建JavaStreamingContext对象
// 而不是像之前的JavaSparkContext、SQLContext/HiveContext
// 传入的第一个参数,和之前的spark上下文一样,也是SparkConf对象;第二个参数则不太一样
// 第二个参数是spark streaming类型作业比较有特色的一个参数
// 实时处理batch的interval
// spark streaming,每隔一小段时间,会去收集一次数据源(kafka)中的数据,做成一个batch
// 每次都是处理一个batch中的数据
// 通常来说,batch interval,就是指每隔多少时间收集一次数据源中的数据,然后进行处理
// 一遍spark streaming的应用,都是设置数秒到数十秒(很少会超过1分钟)
// 咱们这里项目中,就设置5秒钟的batch interval
// 每隔5秒钟,咱们的spark streaming作业就会收集最近5秒内的数据源接收过来的数据
JavaStreamingContext jssc = new JavaStreamingContext(
conf, Durations.seconds(5));
// 正式开始进行代码的编写
// 实现咱们需要的实时计算的业务逻辑和功能
// 创建针对Kafka数据来源的输入DStream(离线流,代表了一个源源不断的数据来源,抽象)
// 选用kafka direct api(很多好处,包括自己内部自适应调整每次接收数据量的特性,等等)
// 构建kafka参数map
// 主要要放置的就是,你要连接的kafka集群的地址(broker集群的地址列表)
Map<String, String> kafkaParams = new HashMap<String, String>();
kafkaParams.put(Constants.KAFKA_METADATA_BROKER_LIST,
ConfigurationManager.getProperty(Constants.KAFKA_METADATA_BROKER_LIST));
// 构建topic set
String kafkaTopics = ConfigurationManager.getProperty(Constants.KAFKA_TOPICS);
String[] kafkaTopicsSplited = kafkaTopics.split(",");
Set<String> topics = new HashSet<String>();
for(String kafkaTopic : kafkaTopicsSplited) {
topics.add(kafkaTopic);
}
// 基于kafka direct api模式,构建出了针对kafka集群中指定topic的输入DStream
// 两个值,val1,val2;val1没有什么特殊的意义;val2中包含了kafka topic中的一条一条的实时日志数据
JavaPairInputDStream<String, String> adRealTimeLogDStream = KafkaUtils.createDirectStream(
jssc,
String.class,
String.class,
StringDecoder.class,
StringDecoder.class,
kafkaParams,
topics);
// 一条一条的实时日志
// timestamp province city userid adid
// 某个时间点 某个省份 某个城市 某个用户 某个广告
// 计算出每5个秒内的数据中,每天每个用户每个广告的点击量
// 通过对原始实时日志的处理
// 将日志的格式处理成<yyyyMMdd_userid_adid, 1L>格式
JavaPairDStream<String, Long> dailyUserAdClickDStream = adRealTimeLogDStream.mapToPair(
new PairFunction<Tuple2<String,String>, String, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Long> call(Tuple2<String, String> tuple)
throws Exception {
// 从tuple中获取到每一条原始的实时日志
String log = tuple._2;
String[] logSplited = log.split(" ");
// 提取出日期(yyyyMMdd)、userid、adid
String timestamp = logSplited[0];
Date date = new Date(Long.valueOf(timestamp));
String datekey = DateUtils.formatDateKey(date);
long userid = Long.valueOf(logSplited[3]);
long adid = Long.valueOf(logSplited[4]);
// 拼接key
String key = datekey + "_" + userid + "_" + adid;
return new Tuple2<String, Long>(key, 1L);
}
});
// 针对处理后的日志格式,执行reduceByKey算子即可
// (每个batch中)每天每个用户对每个广告的点击量
JavaPairDStream<String, Long> dailyUserAdClickCountDStream = dailyUserAdClickDStream.reduceByKey(
new Function2<Long, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2) throws Exception {
return v1 + v2;
}
});
// 到这里为止,获取到了什么数据呢?
// dailyUserAdClickCountDStream DStream
// 源源不断的,每个5s的batch中,当天每个用户对每支广告的点击次数
// <yyyyMMdd_userid_adid, clickCount>
// 构建完spark streaming上下文之后,记得要进行上下文的启动、等待执行结束、关闭
jssc.start();
jssc.awaitTermination();
jssc.close();
}
}
5.2 使用高性能方式将实时计算结果写入MySQL中
Spark Streaming foreachRDD的正确使用方式
误区一:在driver上创建连接对象(比如网络连接或数据库连接)
如果在driver上创建连接对象,然后在RDD的算子函数内使用连接对象,那么就意味着需要将连接对象序列化后从driver传递到worker上。而连接对象(比如Connection对象)通常来说是不支持序列化的,此时通常会报序列化的异常(serialization errors)。因此连接对象必须在worker上创建,不要在driver上创建。
dstream.foreachRDD { rdd =>
val connection = createNewConnection() // 在driver上执行
rdd.foreach { record =>
connection.send(record) // 在worker上执行
}
}
误区二:为每一条记录都创建一个连接对象
dstream.foreachRDD { rdd =>
rdd.foreach { record =>
val connection = createNewConnection()
connection.send(record)
connection.close()
}
}
通常来说,连接对象的创建和销毁都是很消耗时间的。因此频繁地创建和销毁连接对象,可能会导致降低spark作业的整体性能和吞吐量。
正确做法一:为每个RDD分区创建一个连接对象
dstream.foreachRDD { rdd =>
rdd.foreachPartition { partitionOfRecords =>
val connection = createNewConnection()
partitionOfRecords.foreach(record => connection.send(record))
connection.close()
}
}
比较正确的做法是:对DStream中的RDD,调用foreachPartition,对RDD中每个分区创建一个连接对象,使用一个连接对象将一个分区内的数据都写入底层MySQL中。这样可以大大减少创建的连接对象的数量。
正确做法二:为每个RDD分区使用一个连接池中的连接对象
dstream.foreachRDD { rdd =>
rdd.foreachPartition { partitionOfRecords =>
// 静态连接池,同时连接是懒创建的
val connection = ConnectionPool.getConnection()
partitionOfRecords.foreach(record => connection.send(record))
ConnectionPool.returnConnection(connection) // 用完以后将连接返回给连接池,进行复用
}
}
对于咱们这种实时计算程序的mysql插入,有两种pattern(模式)
1、比较挫:每次插入前,先查询,看看有没有数据,如果有,则执行insert语句;如果没有,则执行update语句;好处在于,每个key就对应一条记录;坏处在于,本来对一个分区的数据就是一条insert batch,现在很麻烦,还得先执行select语句,再决定是insert还是update。
j2ee系统,查询某个key的时候,就直接查询指定的key就好。
2、稍微好一点:每次插入记录,你就插入就好,但是呢,需要在mysql库中,给每一个表,都加一个时间戳(timestamp),对于同一个key,5秒一个batch,每隔5秒中就有一个记录插入进去。相当于在mysql中维护了一个key的多个版本。
j2ee系统,查询某个key的时候,还得限定是要order by timestamp desc limit 1,查询最新时间版本的数据
通过mysql来用这种方式,不是很好,很不方便后面j2ee系统的使用
不用mysql;用hbase(timestamp的多个版本,而且它不却分insert和update,统一就是去对某个行键rowkey去做更新)
AdClickRealTimeStatSpark.java
package cn.ctgu.sparkproject.spark.ad;
import java.util.ArrayList;
import java.util.Date;
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 kafka.serializer.StringDecoder;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaPairRDD;
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.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 scala.Tuple2;
import cn.ctgu.sparkproject.conf.ConfigurationManager;
import cn.ctgu.sparkproject.constant.Constants;
import cn.ctgu.sparkproject.dao.IAdUserClickCountDAO;
import cn.ctgu.sparkproject.dao.factory.DAOFactory;
import cn.ctgu.sparkproject.domain.AdUserClickCount;
import cn.ctgu.sparkproject.util.DateUtils;
/**
* 广告点击流量实时统计spark作业
* @author Administrator
*
*/
public class AdClickRealTimeStatSpark {
public static void main(String[] args) {
// 构建Spark Streaming上下文
SparkConf conf = new SparkConf()
.setMaster("local[2]")
.setAppName("AdClickRealTimeStatSpark");
// spark streaming的上下文是构建JavaStreamingContext对象
// 而不是像之前的JavaSparkContext、SQLContext/HiveContext
// 传入的第一个参数,和之前的spark上下文一样,也是SparkConf对象;第二个参数则不太一样
// 第二个参数是spark streaming类型作业比较有特色的一个参数
// 实时处理batch的interval
// spark streaming,每隔一小段时间,会去收集一次数据源(kafka)中的数据,做成一个batch
// 每次都是处理一个batch中的数据
// 通常来说,batch interval,就是指每隔多少时间收集一次数据源中的数据,然后进行处理
// 一遍spark streaming的应用,都是设置数秒到数十秒(很少会超过1分钟)
// 咱们这里项目中,就设置5秒钟的batch interval
// 每隔5秒钟,咱们的spark streaming作业就会收集最近5秒内的数据源接收过来的数据
JavaStreamingContext jssc = new JavaStreamingContext(
conf, Durations.seconds(5));
// 正式开始进行代码的编写
// 实现咱们需要的实时计算的业务逻辑和功能
// 创建针对Kafka数据来源的输入DStream(离线流,代表了一个源源不断的数据来源,抽象)
// 选用kafka direct api(很多好处,包括自己内部自适应调整每次接收数据量的特性,等等)
// 构建kafka参数map
// 主要要放置的就是,你要连接的kafka集群的地址(broker集群的地址列表)
Map<String, String> kafkaParams = new HashMap<String, String>();
kafkaParams.put(Constants.KAFKA_METADATA_BROKER_LIST,
ConfigurationManager.getProperty(Constants.KAFKA_METADATA_BROKER_LIST));
// 构建topic set
String kafkaTopics = ConfigurationManager.getProperty(Constants.KAFKA_TOPICS);
String[] kafkaTopicsSplited = kafkaTopics.split(",");
Set<String> topics = new HashSet<String>();
for(String kafkaTopic : kafkaTopicsSplited) {
topics.add(kafkaTopic);
}
// 基于kafka direct api模式,构建出了针对kafka集群中指定topic的输入DStream
// 两个值,val1,val2;val1没有什么特殊的意义;val2中包含了kafka topic中的一条一条的实时日志数据
JavaPairInputDStream<String, String> adRealTimeLogDStream = KafkaUtils.createDirectStream(
jssc,
String.class,
String.class,
StringDecoder.class,
StringDecoder.class,
kafkaParams,
topics);
// 一条一条的实时日志
// timestamp province city userid adid
// 某个时间点 某个省份 某个城市 某个用户 某个广告
// 计算出每5个秒内的数据中,每天每个用户每个广告的点击量
// 通过对原始实时日志的处理
// 将日志的格式处理成<yyyyMMdd_userid_adid, 1L>格式
JavaPairDStream<String, Long> dailyUserAdClickDStream = adRealTimeLogDStream.mapToPair(
new PairFunction<Tuple2<String,String>, String, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Long> call(Tuple2<String, String> tuple)
throws Exception {
// 从tuple中获取到每一条原始的实时日志
String log = tuple._2;
String[] logSplited = log.split(" ");
// 提取出日期(yyyyMMdd)、userid、adid
String timestamp = logSplited[0];
Date date = new Date(Long.valueOf(timestamp));
String datekey = DateUtils.formatDateKey(date);
long userid = Long.valueOf(logSplited[3]);
long adid = Long.valueOf(logSplited[4]);
// 拼接key
String key = datekey + "_" + userid + "_" + adid;
return new Tuple2<String, Long>(key, 1L);
}
});
// 针对处理后的日志格式,执行reduceByKey算子即可
// (每个batch中)每天每个用户对每个广告的点击量
JavaPairDStream<String, Long> dailyUserAdClickCountDStream = dailyUserAdClickDStream.reduceByKey(
new Function2<Long, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2) throws Exception {
return v1 + v2;
}
});
// 到这里为止,获取到了什么数据呢?
// dailyUserAdClickCountDStream DStream
// 源源不断的,每个5s的batch中,当天每个用户对每支广告的点击次数
// <yyyyMMdd_userid_adid, clickCount>
dailyUserAdClickCountDStream.foreachRDD(new Function<JavaPairRDD<String,Long>, Void>() {
private static final long serialVersionUID = 1L;
@Override
public Void call(JavaPairRDD<String, Long> rdd) throws Exception {
rdd.foreachPartition(new VoidFunction<Iterator<Tuple2<String,Long>>>() {
private static final long serialVersionUID = 1L;
@Override
public void call(Iterator<Tuple2<String, Long>> iterator) throws Exception {
// 对每个分区的数据就去获取一次连接对象
// 每次都是从连接池中获取,而不是每次都创建
// 写数据库操作,性能已经提到最高了
List<AdUserClickCount> adUserClickCounts = new ArrayList<AdUserClickCount>();
while(iterator.hasNext()) {
Tuple2<String, Long> tuple = iterator.next();
String[] keySplited = tuple._1.split("_");
String date = DateUtils.formatDate(DateUtils.parseDateKey(keySplited[0]));
// yyyy-MM-dd
long userid = Long.valueOf(keySplited[1]);
long adid = Long.valueOf(keySplited[2]);
long clickCount = tuple._2;
AdUserClickCount adUserClickCount = new AdUserClickCount();
adUserClickCount.setDate(date);
adUserClickCount.setUserid(userid);
adUserClickCount.setAdid(adid);
adUserClickCount.setClickCount(clickCount);
adUserClickCounts.add(adUserClickCount);
}
IAdUserClickCountDAO adUserClickCountDAO = DAOFactory.getAdUserClickCountDAO();
adUserClickCountDAO.updateBatch(adUserClickCounts);
}
});
return null;
}
});
// 构建完spark streaming上下文之后,记得要进行上下文的启动、等待执行结束、关闭
jssc.start();
jssc.awaitTermination();
jssc.close();
}
}
IAdUserClickCountDAO.java
package cn.ctgu.sparkproject.dao;
import java.util.List;
import cn.ctgu.sparkproject.domain.AdUserClickCount;
/**
* 用户广告点击量DAO接口
* @author Administrator
*
*/
public interface IAdUserClickCountDAO {
void updateBatch(List<AdUserClickCount> adUserClickCounts);
}
AdUserClickCountDAOImpl.java
package cn.ctgu.sparkproject.dao.impl;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import cn.ctgu.sparkproject.dao.IAdUserClickCountDAO;
import cn.ctgu.sparkproject.domain.AdUserClickCount;
import cn.ctgu.sparkproject.jdbc.JDBCHelper;
import cn.ctgu.sparkproject.model.AdUserClickCountQueryResult;
/**
* 用户广告点击量DAO实现类
* @author Administrator
*
*/
public class AdUserClickCountDAOImpl implements IAdUserClickCountDAO {
@Override
public void updateBatch(List<AdUserClickCount> adUserClickCounts) {
JDBCHelper jdbcHelper = JDBCHelper.getInstance();
// 首先对用户广告点击量进行分类,分成待插入的和待更新的
List<AdUserClickCount> insertAdUserClickCounts = new ArrayList<AdUserClickCount>();
List<AdUserClickCount> updateAdUserClickCounts = new ArrayList<AdUserClickCount>();
String selectSQL = "SELECT count(*) FROM ad_user_click_count "
+ "WHERE date=? AND user_id=? AND ad_id=? ";
Object[] selectParams = null;
for(AdUserClickCount adUserClickCount : adUserClickCounts) {
final AdUserClickCountQueryResult queryResult = new AdUserClickCountQueryResult();
selectParams = new Object[]{adUserClickCount.getDate(),
adUserClickCount.getUserid(), adUserClickCount.getAdid()};
jdbcHelper.executeQuery(selectSQL, selectParams, new JDBCHelper.QueryCallback() {
@Override
public void process(ResultSet rs) throws Exception {
if(rs.next()) {
int count = rs.getInt(1);
queryResult.setCount(count);
}
}
});
int count = queryResult.getCount();
if(count > 0) {
updateAdUserClickCounts.add(adUserClickCount);
} else {
insertAdUserClickCounts.add(adUserClickCount);
}
}
// 执行批量插入
String insertSQL = "INSERT INTO ad_user_click_count VALUES(?,?,?,?)";
List<Object[]> insertParamsList = new ArrayList<Object[]>();
for(AdUserClickCount adUserClickCount : insertAdUserClickCounts) {
Object[] insertParams = new Object[]{adUserClickCount.getDate(),
adUserClickCount.getUserid(),
adUserClickCount.getAdid(),
adUserClickCount.getClickCount()};
insertParamsList.add(insertParams);
}
jdbcHelper.executeBatch(insertSQL, insertParamsList);
// 执行批量更新
String updateSQL = "UPDATE ad_user_click_count SET click_count=? "
+ "WHERE date=? AND user_id=? AND ad_id=? ";
List<Object[]> updateParamsList = new ArrayList<Object[]>();
for(AdUserClickCount adUserClickCount : updateAdUserClickCounts) {
Object[] updateParams = new Object[]{adUserClickCount.getClickCount(),
adUserClickCount.getDate(),
adUserClickCount.getUserid(),
adUserClickCount.getAdid()};
updateParamsList.add(updateParams);
}
jdbcHelper.executeBatch(updateSQL, updateParamsList);
}
}
5.3 过滤出每个batch中的黑名单用户以生成动态黑名单
/**
* 生成动态黑名单
* @param filteredAdRealTimeLogDStream
*/
private static void generateDynamicBlacklist(
JavaPairDStream<String, String> filteredAdRealTimeLogDStream) {
//一条一条的实时日志
//timestamp province city userid adid
//某个时间点 某个省份 某个城市 某个用户 某个广告
//计算出每个5秒内的数据中,每天每个用户每个广告的点击量
//通过对原始实时日志的处理
//将日志的格式处理成<yyyyMMdd_userid_adid,1L>格式
JavaPairDStream<String,Long>dailyUserAdClickDStream=filteredAdRealTimeLogDStream.mapToPair(
new PairFunction<Tuple2<String,String>,String,Long>(){
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Long>
call(Tuple2<String, String> tuple)
throws Exception {
//从tuple中获取到每一条原始的实时日志
String log=tuple._2;
String[] logSplited=log.split(" ");
//提取出日期(yyyyMMdd)、userid、adid
String timestamp=logSplited[0];
Date date=new Date(Long.valueOf(timestamp));
String datekey=DateUtils.formatDate(date);
long userid=Long.valueOf(logSplited[3]);
long adid=Long.valueOf(logSplited[4]);
//拼接key
String key=datekey+"_"+userid+"_"+adid;
return new Tuple2<String,Long>(key,1L);
}
});
//针对处理后的日志格式,执行reduceByKey算子即可
//(每个batch中)每天每个用户对每个广告的点击量
JavaPairDStream<String,Long>dailyUserAdClickCountDStream=dailyUserAdClickDStream.reduceByKey(
new Function2<Long,Long,Long>(){
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2) throws Exception {
return v1+v2;
}
});
//到这里为止,获取到了什么数据呢?
//dailyUserAdClickCountDStream
//表示源源不断的每个5s的batch中当天每个用户对每支广告的点击次数
//数据格式为:<yyyyMMdd_userid_adid,clickCount>
dailyUserAdClickCountDStream.foreachRDD(new Function<JavaPairRDD<String,Long>,Void>(){
private static final long serialVersionUID = 1L;
@Override
public Void call(JavaPairRDD<String, Long> rdd) throws Exception {
rdd.foreachPartition(new VoidFunction<Iterator<Tuple2<String,Long>>>(){
/**
*
*/
private static final long serialVersionUID = 1L;
@Override
public void call(Iterator<Tuple2<String, Long>> iterator)
throws Exception {
//对每个分区的数据就尽量去获取一次连接对象
//每次都是从连接池中获取,而不是每次都创建
//写数据库性能已经提到最高了
List<AdUserClickCount>adUserClickCounts=new ArrayList<AdUserClickCount>();
while(iterator.hasNext()) {
Tuple2<String,Long>tuple=iterator.next();
String[] keySplited=tuple._1.split("_");
String date=DateUtils.formatDate(DateUtils.parseDateKey(keySplited[0]));
//yyyy-MM-dd
long userid=Long.valueOf(keySplited[1]);
long adid=Long.valueOf(keySplited[2]);
long clickCount=tuple._2;
AdUserClickCount adUserClickCount=new AdUserClickCount();
adUserClickCount.setDate(date);
adUserClickCount.setUserid(userid);
adUserClickCount.setAdid(adid);
adUserClickCount.setClickCount(clickCount);
adUserClickCounts.add(adUserClickCount);
}
IAdUserClickCountDAO adUserClickCountDAO=DAOFactory.getAdUserClickCountDAO();
adUserClickCountDAO.updateBatch(adUserClickCounts);
}
});
return null;
}
});
//现在我们在mysql里面,已经有了累计的每天各用户对各广告的点击量
//遍历每个batch中的所有记录,对每条记录都要去查询一下,这一天这个用户对这个广告的累计点击量是多少
//从mysql中查询,查询出来的结果,如果是100,如果你发现某个用户某天对某个广告的点击量已经大于等于100了
//那么就判定这个用户就是黑名单用户就写入mysql中持久化
//使用dailyUserAdClickCountDStream
//为什么用这个batch?因为这个batch是聚合过的数据,已经按照yyyyMMdd_userid_adid进行过聚合了
//比如原始数据可能是一个batch有一万条,聚合过后可能就只有五千条
//所以选用这个聚合后的dstream,既可以满足咱们的需求而且还可以尽量减少要处理的数据量
JavaPairDStream<String,Long>blacklistDStream=dailyUserAdClickCountDStream.filter(
new Function<Tuple2<String,Long>,Boolean>(){
private static final long serialVersionUID = 1L;
@Override
public Boolean call(Tuple2<String, Long> tuple)
throws Exception {
String key=tuple._1;
String[]keySplited=key.split("_");
//yyyyMMdd->yyyy-MM-dd
String date=DateUtils.formatDate(DateUtils.parseDateKey(keySplited[0]));
long userid=Long.valueOf(keySplited[1]);
long adid=Long.valueOf(keySplited[2]);
//从mysql中查询指定日期指定用户对指定广告的点击量
IAdUserClickCountDAO adUserClickCountDAO=DAOFactory.getAdUserClickCountDAO();
int clickCount=adUserClickCountDAO.findClickCountByMultiKey(
date, userid, adid);
//判断,如果点击量大于等于100,就为黑名单用户返回true
if(clickCount>=100) {
return true;
}
//反之,如果点击量小于100的,就暂时不用管
return false;
}
});
//blacklistDStream里面的每个batch其实就都是过滤出来的已经在某天对某个广告点击量超过100的用户
//遍历这个dstream中的每个rdd,然后将黑名单用户增加到mysql中
//这里一旦增加以后,在整个这段程序的前面会加上根据黑名单动态过滤用户的逻辑
//可以认为,一旦用户被拉入黑名单之后,以后就不会再出现在这里了
//所以直接插入mysql即可
//blacklistDStream中可能有userid是重复的,如果直接这样插入的话
//那么可能会发生插入重复的黑名单用户
//需要进行去重
//id为yyyyMMdd_userid_adid
//比如:20181220_10001_1002 100 20181220_10001_1003 100 其中这个userid 10001就是重复的
//实际上,要通过对dstream执行操作,对其中的rdd中的userid进行全局的去重
JavaDStream<Long>blacklistUseridDStream=blacklistDStream.map(new Function<Tuple2<String,Long>,Long>(){
private static final long serialVersionUID = 1L;
@Override
public Long call(Tuple2<String, Long> tuple) throws Exception {
String key=tuple._1;
String[]keySplited=key.split("_");
Long userid=Long.valueOf(keySplited[1]);
return null;
}
});
JavaDStream<Long>distinctBlacklistUseridDStream=blacklistUseridDStream.transform(
new Function<JavaRDD<Long>,JavaRDD<Long>>() {
private static final long serialVersionUID = 1L;
@Override
public JavaRDD<Long> call(JavaRDD<Long> rdd) throws Exception {
return rdd.distinct();
}
});
//到这一步位置,distinctBlacklistUseridDStream
//每一个rdd只包含了userid,而且还进行了全局的去重,保证每一次过滤出来的黑名单用户都没有重复的
distinctBlacklistUseridDStream.foreachRDD(
new Function<JavaRDD<Long>,Void>(){
private static final long serialVersionUID = 1L;
@Override
public Void call(JavaRDD<Long> rdd) throws Exception {
rdd.foreachPartition(new VoidFunction<Iterator<Long>>() {
/**
*
*/
private static final long serialVersionUID = 1L;
@Override
public void call(Iterator<Long> iterator) throws Exception {
List<AdBlacklist>adBlacklists=new ArrayList<AdBlacklist>();
while(iterator.hasNext()) {
long userid=iterator.next();
AdBlacklist adBlacklist=new AdBlacklist();
adBlacklist.setUserid(userid);
adBlacklists.add(adBlacklist);
}
IAdBlacklistDAO adBlacklistDAO=DAOFactory.getAdBlacklistDAO();
adBlacklistDAO.insertBatch(adBlacklists);
//到此位置,其实已经实现了动态黑名单了
//1、计算出每个batch中的每天每个用户对每个广告的点击量,并持久化到mysql中
//2、依据上述计算出来的数据对每个batch中的按date、userid、adid聚合的数据
//都要遍历一遍,查询一下,对应的累计的点击次数,如果超过了100那么就认定为黑名单
//所以说对黑名单用户进行去重,去重后,将黑名单用户持久化到mysql中
//所以说,mysql中的ad_blacklist表,就可以认为是一张动态黑名单
//3、基于上诉计算出来的动态黑名单在最一开始,就对每个batch中的点击行为
//根据动态黑名单进行过滤,把黑名单中的用户的点击行为直接过滤掉
}
});
return null;
}
});
}
5.4 基于动态黑名单进行点击行为过滤
/*
* 根据动态黑名单进行数据过滤
*
* */
private static JavaPairDStream<String, String> filterByBlacklist(
JavaPairInputDStream<String, String> adRealTimeLogDStream) {
// 刚刚接受到原始的用户点击行为日志之后
// 根据mysql中的动态黑名单,进行实时的黑名单过滤(黑名单用户的点击行为,直接过滤掉,不要了)
// 使用transform算子(将dstream中的每个batch RDD进行处理,转换为任意的其他RDD,功能很强大)
JavaPairDStream<String, String> filteredAdRealTimeLogDStream = adRealTimeLogDStream.transformToPair(
new Function<JavaPairRDD<String,String>, JavaPairRDD<String,String>>() {
private static final long serialVersionUID = 1L;
@SuppressWarnings("resource")
@Override
public JavaPairRDD<String, String> call(
JavaPairRDD<String, String> rdd) throws Exception {
// 首先,从mysql中查询所有黑名单用户,将其转换为一个rdd
IAdBlacklistDAO adBlacklistDAO = DAOFactory.getAdBlacklistDAO();
List<AdBlacklist> adBlacklists = adBlacklistDAO.findAll();
List<Tuple2<Long, Boolean>> tuples = new ArrayList<Tuple2<Long, Boolean>>();
for(AdBlacklist adBlacklist : adBlacklists) {
tuples.add(new Tuple2<Long, Boolean>(adBlacklist.getUserid(), true));
}
JavaSparkContext sc = new JavaSparkContext(rdd.context());
JavaPairRDD<Long, Boolean> blacklistRDD = sc.parallelizePairs(tuples);
// 将原始数据rdd映射成<userid, tuple2<string, string>>
JavaPairRDD<Long, Tuple2<String, String>> mappedRDD = rdd.mapToPair(new PairFunction<Tuple2<String,String>, Long, Tuple2<String, String>>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, Tuple2<String, String>> call(
Tuple2<String, String> tuple)
throws Exception {
String log = tuple._2;
String[] logSplited = log.split(" ");
long userid = Long.valueOf(logSplited[3]);
return new Tuple2<Long, Tuple2<String, String>>(userid, tuple);
}
});
// 将原始日志数据rdd,与黑名单rdd,进行左外连接
// 如果说原始日志的userid,没有在对应的黑名单中,join不到,左外连接
// 用inner join,内连接,会导致数据丢失
JavaPairRDD<Long, Tuple2<Tuple2<String, String>, Optional<Boolean>>> joinedRDD =
mappedRDD.leftOuterJoin(blacklistRDD);
JavaPairRDD<Long, Tuple2<Tuple2<String, String>, Optional<Boolean>>> filteredRDD = joinedRDD.filter(
new Function<Tuple2<Long,Tuple2<Tuple2<String,String>,Optional<Boolean>>>, Boolean>() {
private static final long serialVersionUID = 1L;
@Override
public Boolean call(
Tuple2<Long, Tuple2<Tuple2<String, String>, Optional<Boolean>>> tuple)
throws Exception {
Optional<Boolean> optional = tuple._2._2;
// 如果这个值存在,那么说明原始日志中的userid,join到了某个黑名单用户
if(optional.isPresent() && optional.get()) {
return false;
}
return true;
}
});
JavaPairRDD<String, String> resultRDD = filteredRDD.mapToPair(
new PairFunction<Tuple2<Long,Tuple2<Tuple2<String,String>,
Optional<Boolean>>>, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, String> call(
Tuple2<Long, Tuple2<Tuple2<String, String>,
Optional<Boolean>>> tuple)
throws Exception {
return tuple._2._1;
}
});
return resultRDD;
}
});
return filteredAdRealTimeLogDStream;
}
5.5 计算每天各省各城市各广告的点击量
private static JavaPairDStream<String, Long> calculateRealTimeStat(
JavaPairDStream<String, String> filteredAdRealTimeLogDStream) {
//业务逻辑一
/*
* 广告点击流量实时统计
* 上面的黑名单实际上是广告类的实时系统中比较常见的一种基础的应用
* 实际上要实现的业务功能,不是黑名单
*
* 计算每天各省各城市广告的点击量
* 这份数据实时不断地更新到mysql中的,j2EE系统,实时报表给用户查看的
* j2EE系统每隔几秒钟就从mysql中取一次最新数据,每次都可能不一样
* 设计出来几个维度:日期、省份、城市、广告
* j2EE系统可以非常的灵活
* 用户可以看到,实时的数据,比如2015-11-01,历史数据
* 2015-12-01,当天,可以看到当天所有的实时数据(动态改变),比如江苏省南京市
* 广告可以进行选择(广告主、广告名称、广告类型来筛选一个出来)
* 拿着date、province、city、adid去mysql中查询最新的数据
* 等等,基于这几个维度以及这份动态改变的数据是可以实现比较灵活的广告点击流量查看功能的
*
* date province city userid adid
* date_province city_adid做为key;1做为value
* 通过spark,直接统计出来全局的点击次数,在spark集群中保留一份,在mysql中也保留一份
* 我们要对原始数据进行map,映射成<date_province_city_adid,1>格式
* 然后呢 对上述格式的数据执行updateStateByKey算子
* updateStateByKey算子是spark streaming特有的一种算子,在spark集群内存中
* 维护一份key的全局状态
* */
JavaPairDStream<String,Long>mappedDStream=filteredAdRealTimeLogDStream.mapToPair(
new PairFunction<Tuple2<String,String>,String,Long>(){
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Long> call(Tuple2<String, String> tuple)
throws Exception {
String log=tuple._2;
String[] logSplited=log.split(" ");
String date=logSplited[0];
String province=logSplited[1];
String city=logSplited[2];
long adid=Long.valueOf(logSplited[4]);
String key=date+"_"+province+"_"+city+"_"+adid;
return new Tuple2<String,Long>(key,1L);
}
});
//在这个dstream中就相当于有每个batch rdd累加的各个key(各天各省份个城市各广告的点击次数)
//每次计算出最新的值就在aggregateDStream反映出来
JavaPairDStream<String,Long>aggregateDStream=mappedDStream.updateStateByKey(
new Function2<List<Long>,Optional<Long>,Optional<Long>>(){
private static final long serialVersionUID = 1L;
@Override
public Optional<Long> call(List<Long> values, Optional<Long> optional)
throws Exception {
//举例来说
//对于每个key都会调用一次这个方法
//比如key是<20181202_Jiangsu_Nanjing_10001,1>,就会调用一次这个方法
//10个
//values,(1,1,1,1,1,1,1,1,1,1,1)
//首先根据optional判断,之前这个key是否有对应的状态
long clickCount=0L;
//如果说,之前是存在这个状态的,那么就以之前的状态作为起点进行值得累加
if(optional.isPresent()) {
clickCount=optional.get();
}
//values代表了batch rdd中每个key对应的所有的值
for(Long value:values) {
clickCount+=value;
}
return Optional.of(clickCount);
}
});
//将计算出来的最新结果同步一份到mysql中以便于j2ee系统使用
aggregateDStream.foreachRDD(new Function<JavaPairRDD<String,Long>,Void>() {
private static final long serialVersionUID = 1L;
@Override
public Void call(JavaPairRDD<String, Long> rdd) throws Exception {
rdd.foreachPartition(new VoidFunction<Iterator<Tuple2<String,Long>>>(){
private static final long serialVersionUID = 1L;
@Override
public void call(Iterator<Tuple2<String, Long>> iterator)
throws Exception {
List<AdStat>adStats=new ArrayList<AdStat>();
while(iterator.hasNext()) {
Tuple2<String,Long>tuple=iterator.next();
String[]keySplited=tuple._1.split("_");
String date=keySplited[0];
String province=keySplited[1];
String city=keySplited[2];
long adid=Long.valueOf(keySplited[3]);
long clickCount=tuple._2;
AdStat adStat=new AdStat();
adStat.setAdid(adid);
adStat.setCity(city);
adStat.setDate(date);
adStat.setProvince(province);
adStat.setClickCount(clickCount);
adStats.add(adStat);
}
IAdStatDAO adStatDAO=DAOFactory.getAdStatDAO();
adStatDAO.updateBatch(adStats);
}
});
return null;
}
});
return aggregateDStream;
}
5.6 计算每天各省的top3热门广告
/*
* 计算每天各省份的top3热门广告
*
* */
private static void calculateProvinceTop3Ad(
JavaPairDStream<String, Long> adRealTimeStatDStream) {
// adRealTimeStatDStream
// 每一个batch rdd,都代表了最新的全量的每天各省份各城市各广告的点击量
JavaDStream<Row> rowsDStream = adRealTimeStatDStream.transform(
new Function<JavaPairRDD<String,Long>, JavaRDD<Row>>() {
private static final long serialVersionUID = 1L;
@Override
public JavaRDD<Row> call(JavaPairRDD<String, Long> rdd)
throws Exception {
// <yyyyMMdd_province_city_adid, clickCount>
// <yyyyMMdd_province_adid, clickCount>
// 计算出每天各省份各广告的点击量
JavaPairRDD<String, Long> mappedRDD = rdd.mapToPair(
new PairFunction<Tuple2<String,Long>, String, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Long> call(
Tuple2<String, Long> tuple) throws Exception {
String[] keySplited = tuple._1.split("_");
String date = keySplited[0];
String province = keySplited[1];
long adid = Long.valueOf(keySplited[3]);
long clickCount = tuple._2;
String key = date + "_" + province + "_" + adid;
return new Tuple2<String, Long>(key, clickCount);
}
});
JavaPairRDD<String, Long> dailyAdClickCountByProvinceRDD = mappedRDD.reduceByKey(
new Function2<Long, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2)
throws Exception {
return v1 + v2;
}
});
// 将dailyAdClickCountByProvinceRDD转换为DataFrame
// 注册为一张临时表
// 使用Spark SQL,通过开窗函数,获取到各省份的top3热门广告
JavaRDD<Row> rowsRDD = dailyAdClickCountByProvinceRDD.map(
new Function<Tuple2<String,Long>, Row>() {
private static final long serialVersionUID = 1L;
@Override
public Row call(Tuple2<String, Long> tuple)
throws Exception {
String[] keySplited = tuple._1.split("_");
String datekey = keySplited[0];
String province = keySplited[1];
long adid = Long.valueOf(keySplited[2]);
long clickCount = tuple._2;
String date = DateUtils.formatDate(DateUtils.parseDateKey(datekey));
return RowFactory.create(date, province, adid, clickCount);
}
});
StructType schema = DataTypes.createStructType(Arrays.asList(
DataTypes.createStructField("date", DataTypes.StringType, true),
DataTypes.createStructField("province", DataTypes.StringType, true),
DataTypes.createStructField("ad_id", DataTypes.LongType, true),
DataTypes.createStructField("click_count", DataTypes.LongType, true)));
HiveContext sqlContext = new HiveContext(rdd.context());
DataFrame dailyAdClickCountByProvinceDF = sqlContext.createDataFrame(rowsRDD, schema);
// 将dailyAdClickCountByProvinceDF,注册成一张临时表
dailyAdClickCountByProvinceDF.registerTempTable("tmp_daily_ad_click_count_by_prov");
// 使用Spark SQL执行SQL语句,配合开窗函数,统计出各身份top3热门的广告
DataFrame provinceTop3AdDF = sqlContext.sql(
"SELECT "
+ "date,"
+ "province,"
+ "ad_id,"
+ "click_count "
+ "FROM ( "
+ "SELECT "
+ "date,"
+ "province,"
+ "ad_id,"
+ "click_count,"
+ "ROW_NUMBER() OVER(PARTITION BY province ORDER BY click_count DESC) rank "
+ "FROM tmp_daily_ad_click_count_by_prov "
+ ") t "
+ "WHERE rank>=3"
);
return provinceTop3AdDF.javaRDD();
}
});
// rowsDStream
// 每次都是刷新出来各个省份最热门的top3广告
// 将其中的数据批量更新到MySQL中
rowsDStream.foreachRDD(new Function<JavaRDD<Row>, Void>() {
private static final long serialVersionUID = 1L;
@Override
public Void call(JavaRDD<Row> rdd) throws Exception {
rdd.foreachPartition(new VoidFunction<Iterator<Row>>() {
private static final long serialVersionUID = 1L;
@Override
public void call(Iterator<Row> iterator) throws Exception {
List<AdProvinceTop3> adProvinceTop3s = new ArrayList<AdProvinceTop3>();
while(iterator.hasNext()) {
Row row = iterator.next();
String date = row.getString(0);
String province = row.getString(1);
long adid = row.getLong(2);
long clickCount = row.getLong(3);
AdProvinceTop3 adProvinceTop3 = new AdProvinceTop3();
adProvinceTop3.setDate(date);
adProvinceTop3.setProvince(province);
adProvinceTop3.setAdid(adid);
adProvinceTop3.setClickCount(clickCount);
adProvinceTop3s.add(adProvinceTop3);
}
IAdProvinceTop3DAO adProvinceTop3DAO = DAOFactory.getAdProvinceTop3DAO();
adProvinceTop3DAO.updateBatch(adProvinceTop3s);
}
});
return null;
}
});
}
5.7 计算每天各广告最近1小时滑动窗口内的点击趋势
/*
* 计算最近一小时滑动窗口的广告点击趋势
* */
private static void calculateAdClickCountByWindow(
JavaPairInputDStream<String, String> adRealTimeLogDStream) {
// 映射成<yyyyMMddHHMM_adid,1L>格式
JavaPairDStream<String, Long> pairDStream = adRealTimeLogDStream.mapToPair(
new PairFunction<Tuple2<String,String>, String, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Long> call(Tuple2<String, String> tuple)
throws Exception {
// timestamp province city userid adid
String[] logSplited = tuple._2.split(" ");
String timeMinute = DateUtils.formatTimeMinute(
new Date(Long.valueOf(logSplited[0])));
long adid = Long.valueOf(logSplited[4]);
return new Tuple2<String, Long>(timeMinute + "_" + adid, 1L);
}
});
// 过来的每个batch rdd,都会被映射成<yyyyMMddHHMM_adid,1L>的格式
// 每次出来一个新的batch,都要获取最近1小时内的所有的batch
// 然后根据key进行reduceByKey操作,统计出来最近一小时内的各分钟各广告的点击次数
// 1小时滑动窗口内的广告点击趋势
// 点图 / 折线图
JavaPairDStream<String, Long> aggrRDD = pairDStream.reduceByKeyAndWindow(
new Function2<Long, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2) throws Exception {
return v1 + v2;
}
}, Durations.minutes(60), Durations.seconds(10));
// aggrRDD
// 每次都可以拿到,最近1小时内,各分钟(yyyyMMddHHMM)各广告的点击量
// 各广告,在最近1小时内,各分钟的点击量
aggrRDD.foreachRDD(new Function<JavaPairRDD<String,Long>, Void>() {
private static final long serialVersionUID = 1L;
@Override
public Void call(JavaPairRDD<String, Long> rdd) throws Exception {
rdd.foreachPartition(new VoidFunction<Iterator<Tuple2<String,Long>>>() {
private static final long serialVersionUID = 1L;
@Override
public void call(Iterator<Tuple2<String, Long>> iterator)
throws Exception {
List<AdClickTrend> adClickTrends = new ArrayList<AdClickTrend>();
while(iterator.hasNext()) {
Tuple2<String, Long> tuple = iterator.next();
String[] keySplited = tuple._1.split("_");
// yyyyMMddHHmm
String dateMinute = keySplited[0];
long adid = Long.valueOf(keySplited[1]);
long clickCount = tuple._2;
String date = DateUtils.formatDate(DateUtils.parseDateKey(
dateMinute.substring(0, 8)));
String hour = dateMinute.substring(8, 10);
String minute = dateMinute.substring(10);
AdClickTrend adClickTrend = new AdClickTrend();
adClickTrend.setDate(date);
adClickTrend.setHour(hour);
adClickTrend.setMinute(minute);
adClickTrend.setAdid(adid);
adClickTrend.setClickCount(clickCount);
adClickTrends.add(adClickTrend);
}
IAdClickTrendDAO adClickTrendDAO = DAOFactory.getAdClickTrendDAO();
adClickTrendDAO.updateBatch(adClickTrends);
}
});
return null;
}
});
}
5.7 实现实时计算程序的HA高可用性
HA高可用性:High Availability,如果有些数据丢失,或者节点挂掉;那么不能让你的实时计算程序挂了;必须做一些数据上的冗余副本,保证你的实时计算程序可以7 * 24小时的运转。
通过一整套方案(3个步骤),开启和实现实时计算程序的HA高可用性,保证一些关键数据都有其冗余副本,不至于因为节点挂掉或者其他原因导致数据丢失。
1、updateStateByKey、window等有状态的操作,自动进行checkpoint,必须设置checkpoint目录
checkpoint目录:容错的文件系统的目录,比如说,常用的是HDFS
SparkStreaming.checkpoint(“hdfs://192.168.1.105:9090/checkpoint”)
设置完这个基本的checkpoint目录之后,有些会自动进行checkpoint操作的DStream,就实现了HA高可用性;checkpoint,相当于是会把数据保留一份在容错的文件系统中,一旦内存中的数据丢失掉;那么就可以直接从文件系统中读取数据;不需要重新进行计算
2、Driver高可用性
第一次在创建和启动StreamingContext的时候,那么将持续不断地将实时计算程序的元数据(比如说,有些dstream或者job执行到了哪个步骤),如果后面,不幸,因为某些原因导致driver节点挂掉了;那么可以让spark集群帮助我们自动重启driver,然后继续运行时候计算程序,并且是接着之前的作业继续执行;没有中断,没有数据丢失
第一次在创建和启动StreamingContext的时候,将元数据写入容错的文件系统(比如hdfs);spark-submit脚本中加一些参数;保证在driver挂掉之后,spark集群可以自己将driver重新启动起来;而且driver在启动的时候,不会重新创建一个streaming context,而是从容错文件系统(比如hdfs)中读取之前的元数据信息,包括job的执行进度,继续接着之前的进度,继续执行。
使用这种机制,就必须使用cluster模式提交,确保driver运行在某个worker上面;但是这种模式不方便我们调试程序,一会儿还要最终测试整个程序的运行,打印不出log;我们这里仅仅是用我们的代码给大家示范一下:
JavaStreamingContextFactory contextFactory = new JavaStreamingContextFactory() {
@Override
public JavaStreamingContext create() {
JavaStreamingContext jssc = new JavaStreamingContext(...);
JavaDStream<String> lines = jssc.socketTextStream(...);
jssc.checkpoint(checkpointDirectory);
return jssc;
}
};
JavaStreamingContext context = JavaStreamingContext.getOrCreate(checkpointDirectory, contextFactory);
context.start();
context.awaitTermination();
spark-submit
--deploy-mode cluster
--supervise
3、实现RDD高可用性:启动WAL预写日志机制
spark streaming,从原理上来说,是通过receiver来进行数据接收的;接收到的数据,会被划分成一个一个的block;block会被组合成一个batch;针对一个batch,会创建一个rdd;启动一个job来执行我们定义的算子操作。
receiver主要接收到数据,那么就会立即将数据写入一份到容错文件系统(比如hdfs)上的checkpoint目录中的,一份磁盘文件中去;作为数据的冗余副本。
无论你的程序怎么挂掉,或者是数据丢失,那么数据都不肯能会永久性的丢失;因为肯定有副本。
WAL(Write-Ahead Log)预写日志机制
spark.streaming.receiver.writeAheadLog.enable true
5.8 对实时计算程序进行性能调优
1、并行化数据接收:处理多个topic的数据时比较有效
int numStreams = 5;
List<JavaPairDStream<String, String>> kafkaStreams = new ArrayList<JavaPairDStream<String, String>>(numStreams);
for (int i = 0; i < numStreams; i++) {
kafkaStreams.add(KafkaUtils.createStream(...));
}
JavaPairDStream<String, String> unifiedStream = streamingContext.union(kafkaStreams.get(0), kafkaStreams.subList(1, kafkaStreams.size()));
unifiedStream.print();
2、spark.streaming.blockInterval:增加block数量,增加每个batch rdd的partition数量,增加处理并行度
receiver从数据源源源不断地获取到数据;首先是会按照block interval,将指定时间间隔的数据,收集为一个block;默认时间是200ms,官方推荐不要小于50ms;接着呢,会将指定batch interval时间间隔内的block,合并为一个batch;创建为一个rdd,然后启动一个job,去处理这个batch rdd中的数据
batch rdd,它的partition数量是多少呢?一个batch有多少个block,就有多少个partition;就意味着并行度是多少;就意味着每个batch rdd有多少个task会并行计算和处理。
当然是希望可以比默认的task数量和并行度再多一些了;可以手动调节block interval;减少block interval;每个batch可以包含更多的block;有更多的partition;也就有更多的task并行处理每个batch rdd。
定死了,初始的rdd过来,直接就是固定的partition数量了
3、inputStream.repartition(<number of partitions>)
:重分区,增加每个batch rdd的partition数量
有些时候,希望对某些dstream中的rdd进行定制化的分区
对dstream中的rdd进行重分区,去重分区成指定数量的分区,这样也可以提高指定dstream的rdd的计算并行度
4、调节并行度
spark.default.parallelism
reduceByKey(numPartitions)
5、使用Kryo序列化机制:
spark streaming,也是有不少序列化的场景的
提高序列化task发送到executor上执行的性能,如果task很多的时候,task序列化和反序列化的性能开销也比较可观
默认输入数据的存储级别是StorageLevel.MEMORY_AND_DISK_SER_2,receiver接收到数据,默认就会进行持久化操作;首先序列化数据,存储到内存中;如果内存资源不够大,那么就写入磁盘;而且,还会写一份冗余副本到其他executor的block manager中,进行数据冗余。
6、batch interval:每个的处理时间必须小于batch interval
实际上你的spark streaming跑起来以后,其实都是可以在spark ui上观察它的运行情况的;可以看到batch的处理时间;
如果发现batch的处理时间大于batch interval,就必须调节batch interval
尽量不要让batch处理时间大于batch interval
比如你的batch每隔5秒生成一次;你的batch处理时间要达到6秒;就会出现,batch在你的内存中日积月累,一直囤积着,没法及时计算掉,释放内存空间;而且对内存空间的占用越来越大,那么此时会导致内存空间快速消耗
如果发现batch处理时间比batch interval要大,就尽量将batch interval调节大一些
5.7 完整代码
cn.ctgu.sparkproject.spark.ad.AdClickRealTimeStatSpark
package cn.ctgu.sparkproject.spark.ad;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
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 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.kafka.KafkaUtils;
import com.google.common.base.Optional;
import cn.ctgu.sparkproject.conf.ConfigurationManager;
import cn.ctgu.sparkproject.constant.Constants;
import cn.ctgu.sparkproject.dao.IAdBlacklistDAO;
import cn.ctgu.sparkproject.dao.IAdClickTrendDAO;
import cn.ctgu.sparkproject.dao.IAdProvinceTop3DAO;
import cn.ctgu.sparkproject.dao.IAdStatDAO;
import cn.ctgu.sparkproject.dao.IAdUserClickCountDAO;
import cn.ctgu.sparkproject.dao.factory.DAOFactory;
import cn.ctgu.sparkproject.domain.AdBlacklist;
import cn.ctgu.sparkproject.domain.AdClickTrend;
import cn.ctgu.sparkproject.domain.AdProvinceTop3;
import cn.ctgu.sparkproject.domain.AdStat;
import cn.ctgu.sparkproject.domain.AdUserClickCount;
import cn.ctgu.sparkproject.util.DateUtils;
import kafka.serializer.StringDecoder;
import scala.Tuple2;
/*
* 广告点击流量实时统计spark作业
*
* */
public class AdClickRealTimeStatSpark {
public static void main(String[] args) {
//构建spark streaming上下文
SparkConf conf=new SparkConf()
.setMaster("local[2]")
.setAppName("AdClickRealTimeStatSpark");
//spark streaming的上下文是构建JavaStreamingContext对象
//而不是像之前的JavaSparkContext、SQLContext/HiveContext
//传入的第一个参数和之前的spark上下文一样,也是sparkConf对象,第二个参数则不太一样
//第二个参数是spark streaming类型作业比较有特色的一个参数
//实时处理batch的interval
//spark streaming,每隔一小段时间,会去收集一次数据源(kafka)中的数据,做成一个batcha
//每次都是处理一个batch中的数据
//通常来说,batch interval就是指每隔多少时间收集一次数据源中的数据,然后进行处理
//以便spark streaming的应用,都是设置数秒到数十秒(很少会超过1分钟)
//咱们这里项目中,就设置5秒钟的batch interval
//每隔5秒钟,spark streaming作业就会收集最近5秒内的数据源接收过来的数据
JavaStreamingContext jssc=new JavaStreamingContext(
conf,Durations.seconds(5));
//设置持久化目录
jssc.checkpoint("hdfs://172.25.11.100:9090/streaming_checkpoint");
//正式开始进行代码的编写
//编写实时计算的业务逻辑和功能
//创建针对kafka数据来源的输入DStream(离线流,代表了一个源源不断的数据来源,抽象)
//选用kafka direct api(具有很多好处,包括自己内部自适应调整每次接收数据量的特性)
//构建kafka参数map
//主要放置的就是要连接的kafka集群的地址(broker集群的地址列表)
Map<String,String>kafkaParams=new HashMap<String,String>();
kafkaParams.put(Constants.KAFKA_METADATA_BROKER_LIST,
ConfigurationManager.getProperty(Constants.KAFKA_METADATA_BROKER_LIST));
//构建topic set
String kafkaTopics=ConfigurationManager.getProperty(Constants.KAFKA_TOPICS);
String[] kafkaTopicsSplited=kafkaTopics.split(",");
Set<String>topics=new HashSet<String>();
for(String kafkaTopic:kafkaTopicsSplited) {
topics.add(kafkaTopic);
}
//基于kafka direct api模式构建出了针对kafka集群中指定topic的输入DStream
//两个值,val1,val2;val1没有什么特殊的意义,val2中包含了kafka topic中的一条一条的实时日志数据
JavaPairInputDStream<String,String>adRealTimeLogDStream=KafkaUtils.createDirectStream(
jssc,
String.class,
String.class,
StringDecoder.class,
StringDecoder.class,
kafkaParams, topics);
// 根据动态黑名单进行数据过滤
JavaPairDStream<String, String> filteredAdRealTimeLogDStream =
filterByBlacklist(adRealTimeLogDStream);
// 生成动态黑名单
generateDynamicBlacklist(filteredAdRealTimeLogDStream);
// 业务功能一:计算广告点击流量实时统计结果(yyyyMMdd_province_city_adid,clickCount)
JavaPairDStream<String,Long>adRealTimeStatDStream=calculateRealTimeStat(
filteredAdRealTimeLogDStream);
//业务功能二:实时统计每天每个省份top3热门广告
calculateProvinceTop3Ad(adRealTimeStatDStream);
//业务功能三:实时统计每天每个广告在最近1小时滑动窗口内的点击趋势(每分钟点击量)
/*
* 每次都可以看到每个广告,最近一小时内,每分钟的点击量
* 每支广告的点击趋势
* */
calculateAdClickCountByWindow(adRealTimeLogDStream);
// 构建完spark streaming上下文之后,记得要进行上下文的启动、等待执行结束、关闭
jssc.start();
jssc.awaitTermination();
jssc.close();
}
/*
* 根据动态黑名单进行数据过滤
*
* */
private static JavaPairDStream<String, String> filterByBlacklist(
JavaPairInputDStream<String, String> adRealTimeLogDStream) {
// 刚刚接受到原始的用户点击行为日志之后
// 根据mysql中的动态黑名单,进行实时的黑名单过滤(黑名单用户的点击行为,直接过滤掉,不要了)
// 使用transform算子(将dstream中的每个batch RDD进行处理,转换为任意的其他RDD,功能很强大)
JavaPairDStream<String, String> filteredAdRealTimeLogDStream = adRealTimeLogDStream.transformToPair(
new Function<JavaPairRDD<String,String>, JavaPairRDD<String,String>>() {
private static final long serialVersionUID = 1L;
@SuppressWarnings("resource")
@Override
public JavaPairRDD<String, String> call(
JavaPairRDD<String, String> rdd) throws Exception {
// 首先,从mysql中查询所有黑名单用户,将其转换为一个rdd
IAdBlacklistDAO adBlacklistDAO = DAOFactory.getAdBlacklistDAO();
List<AdBlacklist> adBlacklists = adBlacklistDAO.findAll();
List<Tuple2<Long, Boolean>> tuples = new ArrayList<Tuple2<Long, Boolean>>();
for(AdBlacklist adBlacklist : adBlacklists) {
tuples.add(new Tuple2<Long, Boolean>(adBlacklist.getUserid(), true));
}
JavaSparkContext sc = new JavaSparkContext(rdd.context());
JavaPairRDD<Long, Boolean> blacklistRDD = sc.parallelizePairs(tuples);
// 将原始数据rdd映射成<userid, tuple2<string, string>>
JavaPairRDD<Long, Tuple2<String, String>> mappedRDD = rdd.mapToPair(new PairFunction<Tuple2<String,String>, Long, Tuple2<String, String>>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<Long, Tuple2<String, String>> call(
Tuple2<String, String> tuple)
throws Exception {
String log = tuple._2;
String[] logSplited = log.split(" ");
long userid = Long.valueOf(logSplited[3]);
return new Tuple2<Long, Tuple2<String, String>>(userid, tuple);
}
});
// 将原始日志数据rdd,与黑名单rdd,进行左外连接
// 如果说原始日志的userid,没有在对应的黑名单中,join不到,左外连接
// 用inner join,内连接,会导致数据丢失
JavaPairRDD<Long, Tuple2<Tuple2<String, String>, Optional<Boolean>>> joinedRDD =
mappedRDD.leftOuterJoin(blacklistRDD);
JavaPairRDD<Long, Tuple2<Tuple2<String, String>, Optional<Boolean>>> filteredRDD = joinedRDD.filter(
new Function<Tuple2<Long,Tuple2<Tuple2<String,String>,Optional<Boolean>>>, Boolean>() {
private static final long serialVersionUID = 1L;
@Override
public Boolean call(
Tuple2<Long, Tuple2<Tuple2<String, String>, Optional<Boolean>>> tuple)
throws Exception {
Optional<Boolean> optional = tuple._2._2;
// 如果这个值存在,那么说明原始日志中的userid,join到了某个黑名单用户
if(optional.isPresent() && optional.get()) {
return false;
}
return true;
}
});
JavaPairRDD<String, String> resultRDD = filteredRDD.mapToPair(
new PairFunction<Tuple2<Long,Tuple2<Tuple2<String,String>,
Optional<Boolean>>>, String, String>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, String> call(
Tuple2<Long, Tuple2<Tuple2<String, String>,
Optional<Boolean>>> tuple)
throws Exception {
return tuple._2._1;
}
});
return resultRDD;
}
});
return filteredAdRealTimeLogDStream;
}
/**
* 生成动态黑名单
* @param filteredAdRealTimeLogDStream
*/
private static void generateDynamicBlacklist(
JavaPairDStream<String, String> filteredAdRealTimeLogDStream) {
//一条一条的实时日志
//timestamp province city userid adid
//某个时间点 某个省份 某个城市 某个用户 某个广告
//计算出每个5秒内的数据中,每天每个用户每个广告的点击量
//通过对原始实时日志的处理
//将日志的格式处理成<yyyyMMdd_userid_adid,1L>格式
JavaPairDStream<String,Long>dailyUserAdClickDStream=filteredAdRealTimeLogDStream.mapToPair(
new PairFunction<Tuple2<String,String>,String,Long>(){
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Long>
call(Tuple2<String, String> tuple)
throws Exception {
//从tuple中获取到每一条原始的实时日志
String log=tuple._2;
String[] logSplited=log.split(" ");
//提取出日期(yyyyMMdd)、userid、adid
String timestamp=logSplited[0];
Date date=new Date(Long.valueOf(timestamp));
String datekey=DateUtils.formatDate(date);
long userid=Long.valueOf(logSplited[3]);
long adid=Long.valueOf(logSplited[4]);
//拼接key
String key=datekey+"_"+userid+"_"+adid;
return new Tuple2<String,Long>(key,1L);
}
});
//针对处理后的日志格式,执行reduceByKey算子即可
//(每个batch中)每天每个用户对每个广告的点击量
JavaPairDStream<String,Long>dailyUserAdClickCountDStream=dailyUserAdClickDStream.reduceByKey(
new Function2<Long,Long,Long>(){
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2) throws Exception {
return v1+v2;
}
});
//到这里为止,获取到了什么数据呢?
//dailyUserAdClickCountDStream
//表示源源不断的每个5s的batch中当天每个用户对每支广告的点击次数
//数据格式为:<yyyyMMdd_userid_adid,clickCount>
dailyUserAdClickCountDStream.foreachRDD(new Function<JavaPairRDD<String,Long>,Void>(){
private static final long serialVersionUID = 1L;
@Override
public Void call(JavaPairRDD<String, Long> rdd) throws Exception {
rdd.foreachPartition(new VoidFunction<Iterator<Tuple2<String,Long>>>(){
/**
*
*/
private static final long serialVersionUID = 1L;
@Override
public void call(Iterator<Tuple2<String, Long>> iterator)
throws Exception {
//对每个分区的数据就尽量去获取一次连接对象
//每次都是从连接池中获取,而不是每次都创建
//写数据库性能已经提到最高了
List<AdUserClickCount>adUserClickCounts=new ArrayList<AdUserClickCount>();
while(iterator.hasNext()) {
Tuple2<String,Long>tuple=iterator.next();
String[] keySplited=tuple._1.split("_");
String date=DateUtils.formatDate(DateUtils.parseDateKey(keySplited[0]));
//yyyy-MM-dd
long userid=Long.valueOf(keySplited[1]);
long adid=Long.valueOf(keySplited[2]);
long clickCount=tuple._2;
AdUserClickCount adUserClickCount=new AdUserClickCount();
adUserClickCount.setDate(date);
adUserClickCount.setUserid(userid);
adUserClickCount.setAdid(adid);
adUserClickCount.setClickCount(clickCount);
adUserClickCounts.add(adUserClickCount);
}
IAdUserClickCountDAO adUserClickCountDAO=DAOFactory.getAdUserClickCountDAO();
adUserClickCountDAO.updateBatch(adUserClickCounts);
}
});
return null;
}
});
//现在我们在mysql里面,已经有了累计的每天各用户对各广告的点击量
//遍历每个batch中的所有记录,对每条记录都要去查询一下,这一天这个用户对这个广告的累计点击量是多少
//从mysql中查询,查询出来的结果,如果是100,如果你发现某个用户某天对某个广告的点击量已经大于等于100了
//那么就判定这个用户就是黑名单用户就写入mysql中持久化
//使用dailyUserAdClickCountDStream
//为什么用这个batch?因为这个batch是聚合过的数据,已经按照yyyyMMdd_userid_adid进行过聚合了
//比如原始数据可能是一个batch有一万条,聚合过后可能就只有五千条
//所以选用这个聚合后的dstream,既可以满足咱们的需求而且还可以尽量减少要处理的数据量
JavaPairDStream<String,Long>blacklistDStream=dailyUserAdClickCountDStream.filter(
new Function<Tuple2<String,Long>,Boolean>(){
private static final long serialVersionUID = 1L;
@Override
public Boolean call(Tuple2<String, Long> tuple)
throws Exception {
String key=tuple._1;
String[]keySplited=key.split("_");
//yyyyMMdd->yyyy-MM-dd
String date=DateUtils.formatDate(DateUtils.parseDateKey(keySplited[0]));
long userid=Long.valueOf(keySplited[1]);
long adid=Long.valueOf(keySplited[2]);
//从mysql中查询指定日期指定用户对指定广告的点击量
IAdUserClickCountDAO adUserClickCountDAO=DAOFactory.getAdUserClickCountDAO();
int clickCount=adUserClickCountDAO.findClickCountByMultiKey(
date, userid, adid);
//判断,如果点击量大于等于100,就为黑名单用户返回true
if(clickCount>=100) {
return true;
}
//反之,如果点击量小于100的,就暂时不用管
return false;
}
});
//blacklistDStream里面的每个batch其实就都是过滤出来的已经在某天对某个广告点击量超过100的用户
//遍历这个dstream中的每个rdd,然后将黑名单用户增加到mysql中
//这里一旦增加以后,在整个这段程序的前面会加上根据黑名单动态过滤用户的逻辑
//可以认为,一旦用户被拉入黑名单之后,以后就不会再出现在这里了
//所以直接插入mysql即可
//blacklistDStream中可能有userid是重复的,如果直接这样插入的话
//那么可能会发生插入重复的黑名单用户
//需要进行去重
//id为yyyyMMdd_userid_adid
//比如:20181220_10001_1002 100 20181220_10001_1003 100 其中这个userid 10001就是重复的
//实际上,要通过对dstream执行操作,对其中的rdd中的userid进行全局的去重
JavaDStream<Long>blacklistUseridDStream=blacklistDStream.map(new Function<Tuple2<String,Long>,Long>(){
private static final long serialVersionUID = 1L;
@Override
public Long call(Tuple2<String, Long> tuple) throws Exception {
String key=tuple._1;
String[]keySplited=key.split("_");
Long userid=Long.valueOf(keySplited[1]);
return null;
}
});
JavaDStream<Long>distinctBlacklistUseridDStream=blacklistUseridDStream.transform(
new Function<JavaRDD<Long>,JavaRDD<Long>>() {
private static final long serialVersionUID = 1L;
@Override
public JavaRDD<Long> call(JavaRDD<Long> rdd) throws Exception {
return rdd.distinct();
}
});
//到这一步位置,distinctBlacklistUseridDStream
//每一个rdd只包含了userid,而且还进行了全局的去重,保证每一次过滤出来的黑名单用户都没有重复的
distinctBlacklistUseridDStream.foreachRDD(
new Function<JavaRDD<Long>,Void>(){
private static final long serialVersionUID = 1L;
@Override
public Void call(JavaRDD<Long> rdd) throws Exception {
rdd.foreachPartition(new VoidFunction<Iterator<Long>>() {
/**
*
*/
private static final long serialVersionUID = 1L;
@Override
public void call(Iterator<Long> iterator) throws Exception {
List<AdBlacklist>adBlacklists=new ArrayList<AdBlacklist>();
while(iterator.hasNext()) {
long userid=iterator.next();
AdBlacklist adBlacklist=new AdBlacklist();
adBlacklist.setUserid(userid);
adBlacklists.add(adBlacklist);
}
IAdBlacklistDAO adBlacklistDAO=DAOFactory.getAdBlacklistDAO();
adBlacklistDAO.insertBatch(adBlacklists);
//到此位置,其实已经实现了动态黑名单了
//1、计算出每个batch中的每天每个用户对每个广告的点击量,并持久化到mysql中
//2、依据上述计算出来的数据对每个batch中的按date、userid、adid聚合的数据
//都要遍历一遍,查询一下,对应的累计的点击次数,如果超过了100那么就认定为黑名单
//所以说对黑名单用户进行去重,去重后,将黑名单用户持久化到mysql中
//所以说,mysql中的ad_blacklist表,就可以认为是一张动态黑名单
//3、基于上诉计算出来的动态黑名单在最一开始,就对每个batch中的点击行为
//根据动态黑名单进行过滤,把黑名单中的用户的点击行为直接过滤掉
}
});
return null;
}
});
}
private static JavaPairDStream<String, Long> calculateRealTimeStat(
JavaPairDStream<String, String> filteredAdRealTimeLogDStream) {
//业务逻辑一
/*
* 广告点击流量实时统计
* 上面的黑名单实际上是广告类的实时系统中比较常见的一种基础的应用
* 实际上要实现的业务功能,不是黑名单
*
* 计算每天各省各城市广告的点击量
* 这份数据实时不断地更新到mysql中的,j2EE系统,实时报表给用户查看的
* j2EE系统每隔几秒钟就从mysql中取一次最新数据,每次都可能不一样
* 设计出来几个维度:日期、省份、城市、广告
* j2EE系统可以非常的灵活
* 用户可以看到,实时的数据,比如2015-11-01,历史数据
* 2015-12-01,当天,可以看到当天所有的实时数据(动态改变),比如江苏省南京市
* 广告可以进行选择(广告主、广告名称、广告类型来筛选一个出来)
* 拿着date、province、city、adid去mysql中查询最新的数据
* 等等,基于这几个维度以及这份动态改变的数据是可以实现比较灵活的广告点击流量查看功能的
*
* date province city userid adid
* date_province city_adid做为key;1做为value
* 通过spark,直接统计出来全局的点击次数,在spark集群中保留一份,在mysql中也保留一份
* 我们要对原始数据进行map,映射成<date_province_city_adid,1>格式
* 然后呢 对上述格式的数据执行updateStateByKey算子
* updateStateByKey算子是spark streaming特有的一种算子,在spark集群内存中
* 维护一份key的全局状态
* */
JavaPairDStream<String,Long>mappedDStream=filteredAdRealTimeLogDStream.mapToPair(
new PairFunction<Tuple2<String,String>,String,Long>(){
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Long> call(Tuple2<String, String> tuple)
throws Exception {
String log=tuple._2;
String[] logSplited=log.split(" ");
String date=logSplited[0];
String province=logSplited[1];
String city=logSplited[2];
long adid=Long.valueOf(logSplited[4]);
String key=date+"_"+province+"_"+city+"_"+adid;
return new Tuple2<String,Long>(key,1L);
}
});
//在这个dstream中就相当于有每个batch rdd累加的各个key(各天各省份个城市各广告的点击次数)
//每次计算出最新的值就在aggregateDStream反映出来
JavaPairDStream<String,Long>aggregateDStream=mappedDStream.updateStateByKey(
new Function2<List<Long>,Optional<Long>,Optional<Long>>(){
private static final long serialVersionUID = 1L;
@Override
public Optional<Long> call(List<Long> values, Optional<Long> optional)
throws Exception {
//举例来说
//对于每个key都会调用一次这个方法
//比如key是<20181202_Jiangsu_Nanjing_10001,1>,就会调用一次这个方法
//10个
//values,(1,1,1,1,1,1,1,1,1,1,1)
//首先根据optional判断,之前这个key是否有对应的状态
long clickCount=0L;
//如果说,之前是存在这个状态的,那么就以之前的状态作为起点进行值得累加
if(optional.isPresent()) {
clickCount=optional.get();
}
//values代表了batch rdd中每个key对应的所有的值
for(Long value:values) {
clickCount+=value;
}
return Optional.of(clickCount);
}
});
//将计算出来的最新结果同步一份到mysql中以便于j2ee系统使用
aggregateDStream.foreachRDD(new Function<JavaPairRDD<String,Long>,Void>() {
private static final long serialVersionUID = 1L;
@Override
public Void call(JavaPairRDD<String, Long> rdd) throws Exception {
rdd.foreachPartition(new VoidFunction<Iterator<Tuple2<String,Long>>>(){
private static final long serialVersionUID = 1L;
@Override
public void call(Iterator<Tuple2<String, Long>> iterator)
throws Exception {
List<AdStat>adStats=new ArrayList<AdStat>();
while(iterator.hasNext()) {
Tuple2<String,Long>tuple=iterator.next();
String[]keySplited=tuple._1.split("_");
String date=keySplited[0];
String province=keySplited[1];
String city=keySplited[2];
long adid=Long.valueOf(keySplited[3]);
long clickCount=tuple._2;
AdStat adStat=new AdStat();
adStat.setAdid(adid);
adStat.setCity(city);
adStat.setDate(date);
adStat.setProvince(province);
adStat.setClickCount(clickCount);
adStats.add(adStat);
}
IAdStatDAO adStatDAO=DAOFactory.getAdStatDAO();
adStatDAO.updateBatch(adStats);
}
});
return null;
}
});
return aggregateDStream;
}
/*
* 计算每天各省份的top3热门广告
*
* */
private static void calculateProvinceTop3Ad(
JavaPairDStream<String, Long> adRealTimeStatDStream) {
// adRealTimeStatDStream
// 每一个batch rdd,都代表了最新的全量的每天各省份各城市各广告的点击量
JavaDStream<Row> rowsDStream = adRealTimeStatDStream.transform(
new Function<JavaPairRDD<String,Long>, JavaRDD<Row>>() {
private static final long serialVersionUID = 1L;
@Override
public JavaRDD<Row> call(JavaPairRDD<String, Long> rdd)
throws Exception {
// <yyyyMMdd_province_city_adid, clickCount>
// <yyyyMMdd_province_adid, clickCount>
// 计算出每天各省份各广告的点击量
JavaPairRDD<String, Long> mappedRDD = rdd.mapToPair(
new PairFunction<Tuple2<String,Long>, String, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Long> call(
Tuple2<String, Long> tuple) throws Exception {
String[] keySplited = tuple._1.split("_");
String date = keySplited[0];
String province = keySplited[1];
long adid = Long.valueOf(keySplited[3]);
long clickCount = tuple._2;
String key = date + "_" + province + "_" + adid;
return new Tuple2<String, Long>(key, clickCount);
}
});
JavaPairRDD<String, Long> dailyAdClickCountByProvinceRDD = mappedRDD.reduceByKey(
new Function2<Long, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2)
throws Exception {
return v1 + v2;
}
});
// 将dailyAdClickCountByProvinceRDD转换为DataFrame
// 注册为一张临时表
// 使用Spark SQL,通过开窗函数,获取到各省份的top3热门广告
JavaRDD<Row> rowsRDD = dailyAdClickCountByProvinceRDD.map(
new Function<Tuple2<String,Long>, Row>() {
private static final long serialVersionUID = 1L;
@Override
public Row call(Tuple2<String, Long> tuple)
throws Exception {
String[] keySplited = tuple._1.split("_");
String datekey = keySplited[0];
String province = keySplited[1];
long adid = Long.valueOf(keySplited[2]);
long clickCount = tuple._2;
String date = DateUtils.formatDate(DateUtils.parseDateKey(datekey));
return RowFactory.create(date, province, adid, clickCount);
}
});
StructType schema = DataTypes.createStructType(Arrays.asList(
DataTypes.createStructField("date", DataTypes.StringType, true),
DataTypes.createStructField("province", DataTypes.StringType, true),
DataTypes.createStructField("ad_id", DataTypes.LongType, true),
DataTypes.createStructField("click_count", DataTypes.LongType, true)));
HiveContext sqlContext = new HiveContext(rdd.context());
DataFrame dailyAdClickCountByProvinceDF = sqlContext.createDataFrame(rowsRDD, schema);
// 将dailyAdClickCountByProvinceDF,注册成一张临时表
dailyAdClickCountByProvinceDF.registerTempTable("tmp_daily_ad_click_count_by_prov");
// 使用Spark SQL执行SQL语句,配合开窗函数,统计出各身份top3热门的广告
DataFrame provinceTop3AdDF = sqlContext.sql(
"SELECT "
+ "date,"
+ "province,"
+ "ad_id,"
+ "click_count "
+ "FROM ( "
+ "SELECT "
+ "date,"
+ "province,"
+ "ad_id,"
+ "click_count,"
+ "ROW_NUMBER() OVER(PARTITION BY province ORDER BY click_count DESC) rank "
+ "FROM tmp_daily_ad_click_count_by_prov "
+ ") t "
+ "WHERE rank>=3"
);
return provinceTop3AdDF.javaRDD();
}
});
// rowsDStream
// 每次都是刷新出来各个省份最热门的top3广告
// 将其中的数据批量更新到MySQL中
rowsDStream.foreachRDD(new Function<JavaRDD<Row>, Void>() {
private static final long serialVersionUID = 1L;
@Override
public Void call(JavaRDD<Row> rdd) throws Exception {
rdd.foreachPartition(new VoidFunction<Iterator<Row>>() {
private static final long serialVersionUID = 1L;
@Override
public void call(Iterator<Row> iterator) throws Exception {
List<AdProvinceTop3> adProvinceTop3s = new ArrayList<AdProvinceTop3>();
while(iterator.hasNext()) {
Row row = iterator.next();
String date = row.getString(0);
String province = row.getString(1);
long adid = row.getLong(2);
long clickCount = row.getLong(3);
AdProvinceTop3 adProvinceTop3 = new AdProvinceTop3();
adProvinceTop3.setDate(date);
adProvinceTop3.setProvince(province);
adProvinceTop3.setAdid(adid);
adProvinceTop3.setClickCount(clickCount);
adProvinceTop3s.add(adProvinceTop3);
}
IAdProvinceTop3DAO adProvinceTop3DAO = DAOFactory.getAdProvinceTop3DAO();
adProvinceTop3DAO.updateBatch(adProvinceTop3s);
}
});
return null;
}
});
}
/*
* 计算最近一小时滑动窗口的广告点击趋势
* */
private static void calculateAdClickCountByWindow(
JavaPairInputDStream<String, String> adRealTimeLogDStream) {
// 映射成<yyyyMMddHHMM_adid,1L>格式
JavaPairDStream<String, Long> pairDStream = adRealTimeLogDStream.mapToPair(
new PairFunction<Tuple2<String,String>, String, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Long> call(Tuple2<String, String> tuple)
throws Exception {
// timestamp province city userid adid
String[] logSplited = tuple._2.split(" ");
String timeMinute = DateUtils.formatTimeMinute(
new Date(Long.valueOf(logSplited[0])));
long adid = Long.valueOf(logSplited[4]);
return new Tuple2<String, Long>(timeMinute + "_" + adid, 1L);
}
});
// 过来的每个batch rdd,都会被映射成<yyyyMMddHHMM_adid,1L>的格式
// 每次出来一个新的batch,都要获取最近1小时内的所有的batch
// 然后根据key进行reduceByKey操作,统计出来最近一小时内的各分钟各广告的点击次数
// 1小时滑动窗口内的广告点击趋势
// 点图 / 折线图
JavaPairDStream<String, Long> aggrRDD = pairDStream.reduceByKeyAndWindow(
new Function2<Long, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2) throws Exception {
return v1 + v2;
}
}, Durations.minutes(60), Durations.seconds(10));
// aggrRDD
// 每次都可以拿到,最近1小时内,各分钟(yyyyMMddHHMM)各广告的点击量
// 各广告,在最近1小时内,各分钟的点击量
aggrRDD.foreachRDD(new Function<JavaPairRDD<String,Long>, Void>() {
private static final long serialVersionUID = 1L;
@Override
public Void call(JavaPairRDD<String, Long> rdd) throws Exception {
rdd.foreachPartition(new VoidFunction<Iterator<Tuple2<String,Long>>>() {
private static final long serialVersionUID = 1L;
@Override
public void call(Iterator<Tuple2<String, Long>> iterator)
throws Exception {
List<AdClickTrend> adClickTrends = new ArrayList<AdClickTrend>();
while(iterator.hasNext()) {
Tuple2<String, Long> tuple = iterator.next();
String[] keySplited = tuple._1.split("_");
// yyyyMMddHHmm
String dateMinute = keySplited[0];
long adid = Long.valueOf(keySplited[1]);
long clickCount = tuple._2;
String date = DateUtils.formatDate(DateUtils.parseDateKey(
dateMinute.substring(0, 8)));
String hour = dateMinute.substring(8, 10);
String minute = dateMinute.substring(10);
AdClickTrend adClickTrend = new AdClickTrend();
adClickTrend.setDate(date);
adClickTrend.setHour(hour);
adClickTrend.setMinute(minute);
adClickTrend.setAdid(adid);
adClickTrend.setClickCount(clickCount);
adClickTrends.add(adClickTrend);
}
IAdClickTrendDAO adClickTrendDAO = DAOFactory.getAdClickTrendDAO();
adClickTrendDAO.updateBatch(adClickTrends);
}
});
return null;
}
});
}
}
cn.ctgu.sparkproject.dao.IAdUserClickCountDAO
package cn.ctgu.sparkproject.dao;
import java.util.List;
import cn.ctgu.sparkproject.domain.AdUserClickCount;
/*
* 用户广告点击量DAO接口
*
* */
public interface IAdUserClickCountDAO {
//批量更新用户广告点击量
void updateBatch(List<AdUserClickCount>adUserClickCounts);
//根据多个key查询用户广告点击量
int findClickCountByMultiKey(String date,long userid,long adid);
}
cn.ctgu.sparkproject.dao.IAdStatDAO
package cn.ctgu.sparkproject.dao;
import java.util.List;
import cn.ctgu.sparkproject.domain.AdStat;
/*
* 广告实时统计DAO接口
*
* */
public interface IAdStatDAO {
void updateBatch(List<AdStat>adStats);
}
cn.ctgu.sparkproject.dao.IAdProvinceTop3DAO
package cn.ctgu.sparkproject.dao;
import java.util.List;
import cn.ctgu.sparkproject.domain.AdProvinceTop3;
public interface IAdProvinceTop3DAO {
void updateBatch(List<AdProvinceTop3>adProvinceTop3s);
}
cn.ctgu.sparkproject.dao.IAdClickTrendDAO
package cn.ctgu.sparkproject.dao;
import java.util.List;
import cn.ctgu.sparkproject.domain.AdClickTrend;
public interface IAdClickTrendDAO {
void updateBatch(List<AdClickTrend>adClickTrends);
}
cn.ctgu.sparkproject.dao.IAdBlacklistDAO
package cn.ctgu.sparkproject.dao;
import java.util.List;
import cn.ctgu.sparkproject.domain.AdBlacklist;
/*
* 广告黑名单DAO接口
*
* */
public interface IAdBlacklistDAO {
void insertBatch(List<AdBlacklist>adBlacklists);
//查询所有广告黑名单用户
List<AdBlacklist>findAll();
}
cn.ctgu.sparkproject.dao.factory.DAOFactory
package cn.ctgu.sparkproject.dao.factory;
import cn.ctgu.sparkproject.dao.IAdBlacklistDAO;
import cn.ctgu.sparkproject.dao.IAdClickTrendDAO;
import cn.ctgu.sparkproject.dao.IAdProvinceTop3DAO;
import cn.ctgu.sparkproject.dao.IAdStatDAO;
import cn.ctgu.sparkproject.dao.IAdUserClickCountDAO;
import cn.ctgu.sparkproject.dao.IAreaTop3ProductDAO;
import cn.ctgu.sparkproject.dao.IPageSplitConvertRateDAO;
import cn.ctgu.sparkproject.dao.ISessionAggrStatDAO;
import cn.ctgu.sparkproject.dao.ISessionDetailDAO;
import cn.ctgu.sparkproject.dao.ISessionRandomExtractDAO;
import cn.ctgu.sparkproject.dao.ITaskDAO;
import cn.ctgu.sparkproject.dao.ITop10CategoryDAO;
import cn.ctgu.sparkproject.dao.ITop10SessionDAO;
import cn.ctgu.sparkproject.dao.impl.AdBlacklistDAOImpl;
import cn.ctgu.sparkproject.dao.impl.AdClickTrendDAOImpl;
import cn.ctgu.sparkproject.dao.impl.AdProvinceTop3DAOImpl;
import cn.ctgu.sparkproject.dao.impl.AdStatDAOImpl;
import cn.ctgu.sparkproject.dao.impl.AdUserClickCountDAOImpl;
import cn.ctgu.sparkproject.dao.impl.AreaTop3ProductDAOImpl;
import cn.ctgu.sparkproject.dao.impl.PageSplitConvertRateDAOImpl;
import cn.ctgu.sparkproject.dao.impl.SessionAggrStatDAOImpl;
import cn.ctgu.sparkproject.dao.impl.SessionDetailDAOImpl;
import cn.ctgu.sparkproject.dao.impl.SessionRandomExtractDAOImpl;
import cn.ctgu.sparkproject.dao.impl.TaskDAOImpl;
import cn.ctgu.sparkproject.dao.impl.Top10CategoryDAOImpl;
import cn.ctgu.sparkproject.dao.impl.Top10SessionDAOImpl;
/*
* DAO工厂类
*
* */
public class DAOFactory {
/*
* 获取任务管理器DAO
*
* */
public static ITaskDAO getTaskDAO() {
return new TaskDAOImpl();
}
/*
* 获取统计sessionDAO
*
* */
public static ISessionAggrStatDAO getSessionAggrStatDAO() {
return new SessionAggrStatDAOImpl();
}
public static ISessionRandomExtractDAO getSessionRandomExtractDAO() {
return new SessionRandomExtractDAOImpl();
}
public static ISessionDetailDAO getSessionDetailDAO() {
return new SessionDetailDAOImpl();
}
public static ITop10CategoryDAO getTop10CategoryDAO() {
return new Top10CategoryDAOImpl();
}
public static ITop10SessionDAO getTop10SessionDAO() {
return new Top10SessionDAOImpl();
}
public static IPageSplitConvertRateDAO getPageSplitConvertRateDAO() {
return new PageSplitConvertRateDAOImpl();
}
public static IAreaTop3ProductDAO getAreaTop3ProductDAO() {
return new AreaTop3ProductDAOImpl();
}
public static IAdUserClickCountDAO getAdUserClickCountDAO() {
return new AdUserClickCountDAOImpl();
}
public static IAdBlacklistDAO getAdBlacklistDAO() {
return new AdBlacklistDAOImpl();
}
public static IAdStatDAO getAdStatDAO() {
return new AdStatDAOImpl();
}
public static IAdProvinceTop3DAO getAdProvinceTop3DAO() {
return new AdProvinceTop3DAOImpl();
}
public static IAdClickTrendDAO getAdClickTrendDAO() {
return new AdClickTrendDAOImpl();
}
}
cn.ctgu.sparkproject.dao.impl.AreaTop3ProductDAOImpl
package cn.ctgu.sparkproject.dao.impl;
import java.util.ArrayList;
import java.util.List;
import cn.ctgu.sparkproject.dao.IAreaTop3ProductDAO;
import cn.ctgu.sparkproject.domain.AreaTop3Product;
import cn.ctgu.sparkproject.jdbc.JDBCHelper;
/*
* 各区域热门商品实现类
*
* */
public class AreaTop3ProductDAOImpl implements IAreaTop3ProductDAO{
@Override
public void insertBatch(List<AreaTop3Product> areaTopsProducts) {
String sql="insert into area_top3_product values(?,?,?,?,?,?,?,?)";
List<Object[]>paramList=new ArrayList<Object[]>();
for(AreaTop3Product areaTop3Product:areaTopsProducts) {
Object[]params=new Object[8];
params[0]=areaTop3Product.getTaskid();
params[1]=areaTop3Product.getArea();
params[2]=areaTop3Product.getAreaLevel();
params[3]=areaTop3Product.getProductid();
params[4]=areaTop3Product.getCityInfo();
params[5]=areaTop3Product.getClickCount();
params[6]=areaTop3Product.getProductName();
params[7]=areaTop3Product.getProductStatus();
paramList.add(params);
}
JDBCHelper jdbcHelper=JDBCHelper.getInstance();
jdbcHelper.executeBatch(sql, paramList);
}
}
cn.ctgu.sparkproject.dao.impl.AdUserClickCountDAOImpl
package cn.ctgu.sparkproject.dao.impl;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import cn.ctgu.sparkproject.dao.IAdUserClickCountDAO;
import cn.ctgu.sparkproject.domain.AdUserClickCount;
import cn.ctgu.sparkproject.jdbc.JDBCHelper;
import cn.ctgu.sparkproject.model.AdUserClickCountQueryResult;
public class AdUserClickCountDAOImpl implements IAdUserClickCountDAO{
@Override
public void updateBatch(List<AdUserClickCount> adUserClickCounts) {
JDBCHelper jdbcHelper=JDBCHelper.getInstance();
//首先对用户广告进行分类,分为待分类的和待更新的
List<AdUserClickCount>insertAdUserClickCounts=new ArrayList<AdUserClickCount>();
List<AdUserClickCount>updateAdUserClickCounts=new ArrayList<AdUserClickCount>();
String selectSQL="SELECT count(*) FROM ad_user_click_count+?"
+ "WHERE date=? AND user_id=? AND ad_id=?";
Object[]selectParams=null;
for(AdUserClickCount adUserClickCount:adUserClickCounts) {
final AdUserClickCountQueryResult queryResult=new AdUserClickCountQueryResult();
selectParams=new Object[] {adUserClickCount.getDate(),
adUserClickCount.getAdid(),
adUserClickCount.getUserid()};
jdbcHelper.executeQuery(selectSQL, selectParams, new JDBCHelper.QueryCallback() {
@Override
public void process(ResultSet rs) throws Exception {
if(rs.next()) {
int count=rs.getInt(1);
queryResult.setCount(count);
}
}
});
int count=queryResult.getCount();
if(count>0) {
updateAdUserClickCounts.add(adUserClickCount);
}else {
insertAdUserClickCounts.add(adUserClickCount);
}
}
//执行批量插入
String insertSQL="INSERT INTO ad_user_click_count values(?,?,?,?)";
List<Object[]>insertParamsList=new ArrayList<Object[]>();
for(AdUserClickCount adUserClickCount:insertAdUserClickCounts) {
Object[]insertParams=new Object[] {adUserClickCount.getDate(),
adUserClickCount.getUserid(),
adUserClickCount.getAdid(),
adUserClickCount.getClickCount()};
insertParamsList.add(insertParams);
}
jdbcHelper.executeBatch(insertSQL, insertParamsList);
//执行批量更新
String updateSQL="UPDATE ad_user_click_count SET click_count=?"
+ "WHERE date=? AND user_id=? AND ad_id=?";
List<Object[]>updateParamsList=new ArrayList<Object[]>();
for(AdUserClickCount adUserClickCount:updateAdUserClickCounts) {
Object[]updateParams=new Object[] {adUserClickCount.getClickCount(),
adUserClickCount.getDate(),
adUserClickCount.getUserid(),
adUserClickCount.getAdid()
};
updateParamsList.add(updateParams);
}
jdbcHelper.executeBatch(updateSQL, updateParamsList);
}
@Override
public int findClickCountByMultiKey(String date, long userid, long adid) {
String sql="SELECT click_count"
+ "FROM ad_user_click_count"
+ "WHERE date=?"
+ "AND user_id=?"
+ "AND ad_id=?";
Object[]params=new Object[] {date,userid,adid};
final AdUserClickCountQueryResult queryResult=new AdUserClickCountQueryResult();
JDBCHelper jdbcHelper=JDBCHelper.getInstance();
jdbcHelper.executeQuery(sql, params, new JDBCHelper.QueryCallback() {
@Override
public void process(ResultSet rs) throws Exception {
if(rs.next()) {
int clickCount=rs.getInt(1);
queryResult.setClickCount(clickCount);
}
}
});
int clickCount=queryResult.getClickCount();
return clickCount;
}
}
cn.ctgu.sparkproject.dao.impl.AdStatDAOImpl
package cn.ctgu.sparkproject.dao.impl;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import cn.ctgu.sparkproject.dao.IAdStatDAO;
import cn.ctgu.sparkproject.domain.AdStat;
import cn.ctgu.sparkproject.jdbc.JDBCHelper;
import cn.ctgu.sparkproject.model.AdStatQueryResult;
/*
* 广告实时统计DAO实现类
*
* */
public class AdStatDAOImpl implements IAdStatDAO{
@Override
public void updateBatch(List<AdStat> adStats) {
//区分开来哪个是插入的,哪个是更新的
List<AdStat>insertAdStats=new ArrayList<AdStat>();
List<AdStat>updateAdStats=new ArrayList<AdStat>();
JDBCHelper jdbcHelper=JDBCHelper.getInstance();
String selectSQL="select count(*)"
+ "from ad_stat"
+ "where date=?"
+ "and province=?"
+ "and city=?"
+ "and ad_id=?";
for(AdStat adStat:adStats) {
final AdStatQueryResult queryResult=new AdStatQueryResult();
Object[]params=new Object[] {adStat.getAdid(),
adStat.getCity(),
adStat.getClickCount(),
adStat.getDate(),
adStat.getProvince()};
jdbcHelper.executeQuery(selectSQL, params, new JDBCHelper.QueryCallback() {
@Override
public void process(ResultSet rs) throws Exception {
if(rs.next()) {
int count=rs.getInt(1);
queryResult.setCount(count);
}
}
});
int count=queryResult.getCount();
if(count>0) {
updateAdStats.add(adStat);
}else {
insertAdStats.add(adStat);
}
}
String insertSQL="insert into ad_stat values(?,?,?,?,?)";
List<Object[]>insertParamsList=new ArrayList<Object[]>();
for(AdStat adStat:insertAdStats) {
Object[]params=new Object[] {adStat.getDate(),
adStat.getProvince(),
adStat.getCity(),
adStat.getClickCount(),
adStat.getAdid()};
insertParamsList.add(params);
}
jdbcHelper.executeBatch(insertSQL, insertParamsList);
//对于需要更新的数据,执行批量更新操作
String updateSQL="update ad_stat set click_count=?"
+ "from ad_stat"
+ "where date=?"
+ "and province=?"
+ "and city=?"
+ "and ad_id=?";
List<Object[]>updateParamsList=new ArrayList<Object[]>();
for(AdStat adStat:updateAdStats) {
Object[]params=new Object[] {adStat.getClickCount(),
adStat.getDate(),
adStat.getProvince(),
adStat.getCity(),
adStat.getAdid()};
updateParamsList.add(params);
}
jdbcHelper.executeBatch(updateSQL, updateParamsList);
}
}
cn.ctgu.sparkproject.dao.impl.AdProvinceTop3DAOImpl
package cn.ctgu.sparkproject.dao.impl;
import java.util.ArrayList;
import java.util.List;
import cn.ctgu.sparkproject.dao.IAdProvinceTop3DAO;
import cn.ctgu.sparkproject.domain.AdProvinceTop3;
import cn.ctgu.sparkproject.jdbc.JDBCHelper;
public class AdProvinceTop3DAOImpl implements IAdProvinceTop3DAO{
@Override
public void updateBatch(List<AdProvinceTop3> adProvinceTop3s) {
//先做一次去重(date_province)
List<String>dateProvinces=new ArrayList<String>();
JDBCHelper jdbcHelper=JDBCHelper.getInstance();
for(AdProvinceTop3 adProvinceTop3:adProvinceTop3s) {
String date=adProvinceTop3.getDate();
String province=adProvinceTop3.getProvince();
String key=date+"_"+province;
if(!dateProvinces.contains(key)) {
dateProvinces.add(key);
}
}
//根据去重后的date和province进行批量删除操作
String deleteSQL="delete from ad_province_top3 where date=? and province=?";
List<Object[]>daleteParamsList=new ArrayList<Object[]>();
for(String dateProvince:dateProvinces) {
String[] dateProvinceSplited=dateProvince.split("_");
String date=dateProvinceSplited[0];
String province=dateProvinceSplited[1];
Object[]params=new Object[] {date,province};
}
jdbcHelper.executeBatch(deleteSQL, daleteParamsList);
//批量插入传入进来的所有数据
String insertSQL="insert into ad_province_top3 values(?,?,?,?)";
List<Object[]>insertParamsList=new ArrayList<Object[]>();
for(AdProvinceTop3 adProvinceTop3:adProvinceTop3s) {
Object[]params=new Object[] {
adProvinceTop3.getDate(),
adProvinceTop3.getProvince(),
adProvinceTop3.getAdid(),
adProvinceTop3.getClickCount()};
insertParamsList.add(params);
}
jdbcHelper.executeBatch(insertSQL, insertParamsList);
}
}
cn.ctgu.sparkproject.dao.impl.AdClickTrendDAOImpl
package cn.ctgu.sparkproject.dao.impl;
import java.sql.ResultSet;
import java.util.ArrayList;
/*
* 广告点击趋势DAO实现类
*
* */
import java.util.List;
import cn.ctgu.sparkproject.dao.IAdClickTrendDAO;
import cn.ctgu.sparkproject.domain.AdClickTrend;
import cn.ctgu.sparkproject.jdbc.JDBCHelper;
import cn.ctgu.sparkproject.model.AdClickTrendQueryResult;
public class AdClickTrendDAOImpl implements IAdClickTrendDAO{
@Override
public void updateBatch(List<AdClickTrend> adClickTrends) {
JDBCHelper jdbcHelper=JDBCHelper.getInstance();
//区分出来哪些数据是要插入的,哪些数据是要更新的
/*
* 通常来说,同一个key的数据(比如rdd,包含了多条相同的key)
* 通常是在一个分区内的
* 一般不会出现重复插入的
*
* 但是根据业务需求来
* 如果说肯能出现key重复插入的情况
* 给一个create_time字段
*
* j2ee系统在查询的时候,直接查询最新的数据即可(可规避掉重复插入的情况)
*
* */
List<AdClickTrend>updateAdClickTrends=new ArrayList<AdClickTrend>();
List<AdClickTrend>insertAdClickTrends=new ArrayList<AdClickTrend>();
String selectSQL="select count(*)"
+ "from ad_click_trend"
+ "where date=?"
+ "and hour=?"
+ "and minute=?"
+ "and ad_id=?";
for(AdClickTrend adClickTrend:adClickTrends) {
final AdClickTrendQueryResult queryResult=new AdClickTrendQueryResult();
Object[]params=new Object[] {
adClickTrend.getDate(),
adClickTrend.getHour(),
adClickTrend.getMinute(),
adClickTrend.getAdid()};
jdbcHelper.executeQuery(selectSQL, params, new JDBCHelper.QueryCallback() {
@Override
public void process(ResultSet rs) throws Exception {
if(rs.next()) {
int count=rs.getInt(1);
queryResult.setCount(count);
}
}
});
int count=queryResult.getCount();
if(count>0) {
updateAdClickTrends.add(adClickTrend);
}else {
insertAdClickTrends.add(adClickTrend);
}
}
//批量更新操作
String updateSQL="update ad_click_trend set click_count=?"
+ "where date=?"
+ "and hour=?"
+ "and minute=?"
+ "and ad_id=?";
List<Object[]>updateParamsList=new ArrayList<Object[]>();
for(AdClickTrend adClickTrend:updateAdClickTrends) {
Object[]params=new Object[] {adClickTrend.getClickCount(),
adClickTrend.getDate(),
adClickTrend.getHour(),
adClickTrend.getMinute(),
adClickTrend.getAdid()};
updateParamsList.add(params);
}
jdbcHelper.executeBatch(updateSQL, updateParamsList);
//批量更新操作
String insertSQL="insert into ad_click_trend values(?,?,?,?,?)";
List<Object[]>insertParamsList=new ArrayList<Object[]>();
for(AdClickTrend adClickTrend:insertAdClickTrends) {
Object[]params=new Object[] {
adClickTrend.getDate(),
adClickTrend.getHour(),
adClickTrend.getMinute(),
adClickTrend.getAdid(),
adClickTrend.getClickCount()};
insertParamsList.add(params);
}
jdbcHelper.executeBatch(insertSQL, insertParamsList);
}
}
cn.ctgu.sparkproject.dao.impl.AdBlacklistDAOImpl
package cn.ctgu.sparkproject.dao.impl;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import cn.ctgu.sparkproject.dao.IAdBlacklistDAO;
import cn.ctgu.sparkproject.domain.AdBlacklist;
import cn.ctgu.sparkproject.jdbc.JDBCHelper;
/*
* 广告黑名单DAO实现类
*
* */
public class AdBlacklistDAOImpl implements IAdBlacklistDAO{
/*
* 批量插入广告黑名单用户
*
* */
@Override
public void insertBatch(List<AdBlacklist> adBlacklists) {
String sql="insert into ad_blacklist values(?)";
List<Object[]>paramList=new ArrayList<Object[]>();
for(AdBlacklist adBlacklist:adBlacklists) {
Object[]params=new Object[] {adBlacklist.getUserid()};
paramList.add(params);
}
JDBCHelper jdbcHelper=JDBCHelper.getInstance();
jdbcHelper.executeBatch(sql, paramList);
}
/*
* 查询所有黑名单用户
*
* */
@Override
public List<AdBlacklist> findAll() {
String sql="select * from ad_blacklist";
final List<AdBlacklist>adBlacklists=new ArrayList<AdBlacklist>();
JDBCHelper jdbcHelper=JDBCHelper.getInstance();
jdbcHelper.executeQuery(sql, null, new JDBCHelper.QueryCallback() {
@Override
public void process(ResultSet rs) throws Exception {
while(rs.next()) {
long userid=Long.valueOf(String.valueOf(rs.getInt(1)));
AdBlacklist adBlacklist=new AdBlacklist();
adBlacklist.setUserid(userid);
adBlacklists.add(adBlacklist);
}
}
});
return adBlacklists;
}
}
cn.ctgu.sparkproject.util.DateUtils
package cn.ctgu.sparkproject.util;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
/**
* 日期时间工具类
* @author Administrator
*
*/
public class DateUtils {
public static final SimpleDateFormat TIME_FORMAT =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static final SimpleDateFormat DATE_FORMAT =
new SimpleDateFormat("yyyy-MM-dd");
public static final SimpleDateFormat DATEKEY_FORMAT=
new SimpleDateFormat("yyyyMMdd");
/**
* 判断一个时间是否在另一个时间之前
* @param time1 第一个时间
* @param time2 第二个时间
* @return 判断结果
*/
public static boolean before(String time1, String time2) {
try {
Date dateTime1 = TIME_FORMAT.parse(time1);
Date dateTime2 = TIME_FORMAT.parse(time2);
if(dateTime1.before(dateTime2)) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 判断一个时间是否在另一个时间之后
* @param time1 第一个时间
* @param time2 第二个时间
* @return 判断结果
*/
public static boolean after(String time1, String time2) {
try {
Date dateTime1 = TIME_FORMAT.parse(time1);
Date dateTime2 = TIME_FORMAT.parse(time2);
if(dateTime1.after(dateTime2)) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 计算时间差值(单位为秒)
* @param time1 时间1
* @param time2 时间2
* @return 差值
*/
public static int minus(String time1, String time2) {
try {
Date datetime1 = TIME_FORMAT.parse(time1);
Date datetime2 = TIME_FORMAT.parse(time2);
long millisecond = datetime1.getTime() - datetime2.getTime();
return Integer.valueOf(String.valueOf(millisecond / 1000));
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
/**
* 获取年月日和小时
* @param datetime 时间(yyyy-MM-dd HH:mm:ss)
* @return 结果
*/
public static String getDateHour(String datetime) {
String date = datetime.split(" ")[0];
String hourMinuteSecond = datetime.split(" ")[1];
String hour = hourMinuteSecond.split(":")[0];
return date + "_" + hour;
}
/**
* 获取当天日期(yyyy-MM-dd)
* @return 当天日期
*/
public static String getTodayDate() {
return DATE_FORMAT.format(new Date());
}
/**
* 获取昨天的日期(yyyy-MM-dd)
* @return 昨天的日期
*/
public static String getYesterdayDate() {
Calendar cal = Calendar.getInstance();
cal.setTime(new Date());
cal.add(Calendar.DAY_OF_YEAR, -1);
Date date = cal.getTime();
return DATE_FORMAT.format(date);
}
/**
* 格式化日期(yyyy-MM-dd)
* @param date Date对象
* @return 格式化后的日期
*/
public static String formatDate(Date date) {
return DATE_FORMAT.format(date);
}
/**
* 格式化时间(yyyy-MM-dd HH:mm:ss)
* @param date Date对象
* @return 格式化后的时间
*/
public static String formatTime(Date date) {
return TIME_FORMAT.format(date);
}
/*
* 解析时间字符串
*
* */
public static Date parseTime(String time) {
try {
return TIME_FORMAT.parse(time);
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
/*
* 格式化日期key
*
* */
public static String formatDateKey(Date date) {
return DATEKEY_FORMAT.format(date);
}
/*
* 格式化日期key
*
* */
public static Date parseDateKey(String datekey) {
try {
return DATEKEY_FORMAT.parse(datekey);
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
/*
* 格式化时间,保留到分钟级别
*
* */
public static String formatTimeMinute(Date date) {
SimpleDateFormat sdf=new SimpleDateFormat("yyyyMMddHHmm");
return sdf.format(date);
}
}
cn.ctgu.sparkproject.constant.Constants
package cn.ctgu.sparkproject.constant;
//常量接口
public interface Constants {
/*
* 项目配置相关的常量
* */
String JDBC_DRIVER="jdbc.driver";
String JDBC_DATASOURCE_SIZE="jdbc.datasource.size";
String JDBC_URL="jdbc.url";
String JDBC_USER="jdbc.user";
String JDBC_PASSWORD="jdbc.password";
String JDBC_URL_PROD="jdbc.url.prod";
String JDBC_USER_PROD="jdbc.user.prod";
String JDBC_PASSWORD_PROD="jdbc.password.prod";
String SPARK_LOCAL="spark.local";
String SPARK_LOCAL_TASKID_SESSION="spark.local.taskid.session";
String SPARK_LOCAL_TASKID_PAGE="spark.local.taskid.page";
String SPARK_LOCAL_TASKID_PRODUCT="spark.local.taskid.product";
String KAFKA_METADATA_BROKER_LIST="kafka.metadata.broker.list";
String KAFKA_TOPICS="kafka.topics";
/*
* spark作业相关的常量
* */
String SPARK_APP_NAME_SESSION="UserVisitSessionAnalyzeSpark";
String SPARK_APP_NAME_PAGE="PageOneStepConvertRateSpark";
String FIELD_SESSION_ID="sessionid";
String FIELD_SEARCH_KEYWORDS="searchKeywords";
String FIELD_CLICK_CATEGORY_IDS="clickCategoryIds";
String FIELD_AGE="age";
String FIELD_PROFESSIONAL="professional";
String FIELD_CITY="city";
String FIELD_SEX="sex";
String FIELD_VISIT_LENGTH="visitLength";
String FIELD_STEP_LENGTH="stepLength";
String FIELD_START_TIME="startTime";
String FIELD_CLICK_COUNT="clickCount";
String FIELD_ORDER_COUNT="orderCount";
String FIELD_PAY_COUNT="payCount";
String FIELD_CATEGORY_ID="categoryid";
String SESSION_COUNT = "session_count";
String TIME_PERIOD_1s_3s = "1s_3s";
String TIME_PERIOD_4s_6s = "4s_6s";
String TIME_PERIOD_7s_9s = "7s_9s";
String TIME_PERIOD_10s_30s = "10s_30s";
String TIME_PERIOD_30s_60s = "30s_60s";
String TIME_PERIOD_1m_3m = "1m_3m";
String TIME_PERIOD_3m_10m = "3m_10m";
String TIME_PERIOD_10m_30m = "10m_30m";
String TIME_PERIOD_30m = "30m";
String STEP_PERIOD_1_3 = "1_3";
String STEP_PERIOD_4_6 = "4_6";
String STEP_PERIOD_7_9 = "7_9";
String STEP_PERIOD_10_30 = "10_30";
String STEP_PERIOD_30_60 = "30_60";
String STEP_PERIOD_60 = "60";
/*
* 任务相关的常量
* */
String PARAM_START_DATE="startDate";
String PARAM_END_DATE="endDate";
String PARAM_START_AGE="startAge";
String PARAM_END_AGE="endAge";
String PARAM_PROFESSIONALS="professionals";
String PARAM_CITIES="cities";
String PARAM_SEX="sex";
String PARAM_KEYWORDS="keywords";
String PARAM_CATEGORY_IDS="categoryIds";
String PARAM_TARGET_PAGE_FLOW="targetPageFlow";
}
my.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.datasource.size=10
jdbc.url=jdbc:mysql://localhost:3306/bigdata
jdbc.user=root
jdbc.password=123456
jdbc.url.prod=jdbc:mysql://172.25.11.100:3306/spark_project
jdbc.user.prod=hive
jdbc.password.prod=hive
spark.local=true
spark.local.taskid.session=2
spark.local.taskid.page=3
spark.local.taskid.product=4
kafka.metadata.broker.list=172.25.11.100:9092,172.25.11.132:9092,172.25.11.133:9092
kafka.topics=AdRealTimeLog