### Spark Streaming 广告实时统计实验教程
#### 实验目标
使用 Spark Streaming 实现广告点击流量的实时统计,包括以下功能:
1. 实现实时动态黑名单机制:将每天广告点击超过 100 次的用户拉黑。
2. 统计每天各省各城市的广告点击流量。
3. 实时统计每天每个省份 top3 热门广告。
4. 实时统计最近 1 小时内各广告的点击趋势(每分钟的点击量)。
5. 将统计结果写入 Redis 或 MySQL 数据库。
---
#### 技术栈与环境准备
- **编程语言**:Scala 或 Python
- **框架**:Spark Streaming, Kafka
- **数据库**:Redis, MySQL
- **日志收集工具**:Flume 或 Logstash(可选)
- **消息队列**:Kafka
确保已安装并配置好 Spark 和 Kafka 集群。如果使用 MySQL 或 Redis,请提前创建对应的表或键值存储结构。
---
#### 实验步骤
#### 1. 数据生成
为了模拟广告点击数据,可以使用 Scala 或 Python 编写脚本生成模拟数据,并通过 Kafka 发送。每条数据格式如下[^4]:
```text
时间戳 省份 城市 用户ID 广告ID
```
以下是 Scala 数据生成代码示例:
```scala
import scala.collection.mutable.ListBuffer
import java.util.Date
import java.text.SimpleDateFormat
def generateData(): ListBuffer[String] = {
val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val data = ListBuffer[String]()
for (_ <- 1 to 10) {
val timestamp = sdf.format(new Date())
val province = Seq("Beijing", "Shanghai", "Guangdong").scala.util.Random.shuffle.head
val city = Seq("Chaoyang", "Huangpu", "Tianhe").scala.util.Random.shuffle.head
val userId = scala.util.Random.nextInt(1000)
val adId = scala.util.Random.nextInt(100)
data += s"$timestamp $province $city $userId $adId"
}
data
}
```
将生成的数据发送到 Kafka 主题中。
---
#### 2. 实现实时动态黑名单机制
通过 Spark Streaming 监听 Kafka 主题,统计每个用户对每个广告的点击次数。当某个用户点击次数超过 100 次时,将其加入黑名单并存储到 MySQL 中[^4]。
以下是实现代码示例:
```scala
val ssc = new StreamingContext(sc, Seconds(10))
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> "localhost:9092",
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> "test",
"auto.offset.reset" -> "earliest",
"enable.auto.commit" -> (false: java.lang.Boolean)
)
val topics = Array("ad-clicks")
val stream = KafkaUtils.createDirectStream[String, String](
ssc,
PreferConsistent,
Subscribe[String, String](topics, kafkaParams)
)
// 解析数据
val parsedStream = stream.map(record => {
val fields = record.value().split(" ")
(fields(3), fields(4)) // (userId, adId)
})
// 更新用户点击次数状态
val userAdClickCounts = parsedStream.map { case (userId, adId) => ((userId, adId), 1) }
.updateStateByKey((values: Seq[Int], state: Option[Int]) => {
val currentCount = values.sum + state.getOrElse(0)
Some(currentCount)
})
// 过滤黑名单用户
userAdClickCounts.foreachRDD(rdd => {
rdd.filter { case ((userId, adId), count) => count > 100 }
.foreachPartition(partition => {
partition.foreach { case ((userId, adId), count) =>
// 将黑名单用户写入 MySQL
writeToMySQL(userId, adId)
}
})
})
```
---
#### 3. 统计每天各省各城市的广告点击流量
通过 `reduceByKeyAndWindow` 方法,计算窗口内的广告点击流量[^3]。
代码示例:
```scala
val provinceCityAdClicks = parsedStream.map { case (userId, adId) => ((fields(1), fields(2), adId), 1) }
val windowedClicks = provinceCityAdClicks.reduceByKeyAndWindow(
(v1: Int, v2: Int) => v1 + v2,
Seconds(60 * 60), // 窗口大小为 1 小时
Seconds(10) // 滑动间隔为 10 秒
)
windowedClicks.foreachRDD(rdd => {
rdd.saveAsTextFile("hdfs://path/to/save/clicks")
})
```
---
#### 4. 实时统计每天每个省份 top3 热门广告
使用 `groupByKey` 和 `sortBy` 方法,计算每个省份的 top3 广告[^3]。
代码示例:
```scala
val provinceAdClicks = parsedStream.map { case (userId, adId) => ((fields(1), adId), 1) }
val topAdsPerProvince = provinceAdClicks.reduceByKeyAndWindow(
(v1: Int, v2: Int) => v1 + v2,
Seconds(60 * 60),
Seconds(10)
).map { case ((province, adId), count) => (province, (adId, count)) }
.groupByKey()
.mapValues(_.toList.sortBy(-_._2).take(3))
topAdsPerProvince.foreachRDD(rdd => {
rdd.saveAsTextFile("hdfs://path/to/save/top_ads")
})
```
---
#### 5. 实时统计最近 1 小时内各广告的点击趋势
使用滑动窗口统计每分钟的广告点击量。
代码示例:
```scala
val adClicksPerMinute = parsedStream.map { case (userId, adId) => (adId, 1) }
val trendClicks = adClicksPerMinute.reduceByKeyAndWindow(
(v1: Int, v2: Int) => v1 + v2,
Seconds(60), // 窗口大小为 1 分钟
Seconds(10) // 滑动间隔为 10 秒
)
trendClicks.foreachRDD(rdd => {
rdd.saveAsTextFile("hdfs://path/to/save/trends")
})
```
---
#### 6. 写入结果到 Redis 或 MySQL
将统计结果写入 Redis 或 MySQL,便于后续查询和展示[^1]。
Redis 示例:
```scala
import redis.clients.jedis.Jedis
def writeToRedis(adId: String, count: Int): Unit = {
val jedis = new Jedis("localhost", 6379)
jedis.set(s"ad_click:$adId", count.toString)
jedis.close()
}
```
MySQL 示例:
```scala
import java.sql.DriverManager
def writeToMySQL(userId: String, adId: String): Unit = {
Class.forName("com.mysql.cj.jdbc.Driver")
val connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db", "user", "password")
val statement = connection.prepareStatement("INSERT INTO blacklist (user_id, ad_id) VALUES (?, ?)")
statement.setString(1, userId)
statement.setString(2, adId)
statement.executeUpdate()
connection.close()
}
```
---
### 性能优化
- 调整 Spark Streaming 的批量处理时间(`batch interval`),以平衡延迟和吞吐量。
- 使用 `updateStateByKey` 时,启用检查点机制以防止状态丢失。
- 对于高并发场景,可以增加 Kafka 分区数量和 Spark 执行器数量。
---
###