网站的独立访客数(
Unique Visitor
,
UV
)。
UV 指的是
一段时间(比如一小时)内访问网站的总人数,1 天内同一访客的多次访问 只记录为一个访客
。(对同样用户IP去重)
输入数据:
![](https://img-blog.csdnimg.cn/20210904142006511.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA6L-c5pa55pe25YWJ,size_20,color_FFFFFF,t_70,g_se,x_16)
方法一:全窗口函数 + set集合去重(风险,数据量大可能内存不足)
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.AllWindowFunction
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector
// 定义输出Uv统计样例类
case class UvCount(windowEnd: Long, count: Long)
object UniqueVisitor {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
val inputStream: DataStream[String] = env.readTextFile("D:\\Mywork\\workspace\\Project_idea\\UserBehaviorAnalysis0903\\HotItemsAnalysis\\src\\main\\resources\\UserBehavior.csv")
//转换成样例类类型,并提取时间戳
val dataStream = inputStream.map(data => {
val arr: Array[String] = data.split(",")
UserBehavior(arr(0).toLong, arr(1).toLong, arr(2).toInt, arr(3), arr(4).toLong)
}).assignAscendingTimestamps(_.timestamp * 1000L)
val uvStream = dataStream
.filter(_.behavior == "pv")
.timeWindowAll(Time.hours(1)) // 直接不分组,基于DataStream开1小时滚动窗口
.apply(new UvCountResult())
uvStream.print()
env.execute("uv job")
}
}
// 自定义实现全窗口函数,用一个Set结构来保存所有的userId,进行自动去重
class UvCountResult() extends AllWindowFunction[UserBehavior, UvCount, TimeWindow] {
override def apply(window: TimeWindow, input: Iterable[UserBehavior], out: Collector[UvCount]): Unit = {
var userIdSet: Set[Long] = Set[Long]()
for (elem <- input) {
userIdSet += elem.userId
}
//将set的size作为去重后的uv值输出
out.collect(UvCount(window.getEnd, userIdSet.size))
}
}
// 输出结果
UvCount(1511661600000,28196)
UvCount(1511665200000,32160)
UvCount(1511668800000,32233)
UvCount(1511672400000,30615)
UvCount(1511676000000,32747)
UvCount(1511679600000,33898)
UvCount(1511683200000,34631)
UvCount(1511686800000,34746)
UvCount(1511690400000,32356)
UvCount(1511694000000,13)
但是这样还是有bug!
如果有1亿个用户 一个用户100B,那么10^10B,相当于10GB
如果使用布隆过滤器的话,一个用户的100B会转化位1bit,那么就是10^8bit
10^8bit/1024B/1024KB=100bitM/8bit约等于10MB
为了防止hash碰撞,我们要扩充10倍,大于要100MB
如果有1亿个用户 一个用户100B,那么10^10B,相当于10GB
如果使用布隆过滤器的话,一个用户的100B会转化位1bit,那么就是10^8bit
10^8bit/1024B/1024KB=100bitM/8bit约等于10MB
为了防止hash碰撞,我们要扩充10倍,大于要100MB
方法二:布隆过滤器 + redis
上面方法中,我们把所有数据的 userId
都存在了窗口计算的状态里
,在窗 口收集数据的过程中,状态会不断增大。一般情况下,
只要不超出内存的承受范围
,但如果我们遇到的数据量很大呢?
把所有数据暂存放到内存里,显然不是一个好注意。我们会想到,
可以利用 redis 这种内存级 k-v 数据库,为我们做一个缓存
。但如果我们遇到的情况非常极端,数 据大到惊人呢?比如上亿级的用户,要去重计算 UV
。
如果放到 redis
中,亿级的用户
id(每个 20
字节左右的话)可能需要几
G
甚至几十 G
的空间来存储。当然放到
redis 中,用集群进行扩展也不是不可以,但明显代价太大了。一个更好的想法是,其实我们
不需要完整地存储用户 ID 的信息,只要知道他在不在就行了
。所以其实我们可以
进行压缩处理,用一位(bit)就可以表示一个用户的状态
。这个思想的具体实现就是
布隆过滤器(Bloom Filter)。
本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构
(probabilistic
data structure
),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。
它本身是一个很长的二进制向量,既然是二进制的向量,那么显而易见的,存放的不是 0,就是
1
。
相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。
我们的目标就是,利用某种方法(一般是 Hash 函数)把每个数据,对应到一个位图的某一位上去
(布隆过滤器的位图:每一位与之对应一个UserId,
如果数据存在那一位就存 1
,不存在则存
0)
。
package UserBehaviorAnalysis.NetworkFlowAnalysis
import UserBehaviorAnalysis.HotItemsAnalysis.UserBehavior
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.ProcessWindowFunction
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.triggers.{Trigger, TriggerResult}
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector
import redis.clients.jedis.Jedis
object UvWithBloom {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
env.setParallelism(1)
// 从文件中读取数据
//val resource = getClass.getResource("/UserBehavior.csv")
val inputStream: DataStream[String] = env.readTextFile("D:\\Mywork\\workspace\\Project_idea\\flink-2021\\src\\main\\resources\\UserBehavior.csv")
// 转换成样例类类型并提取时间戳和watermark
val dataStream: DataStream[UserBehavior] = inputStream
.map(data => {
val arr = data.split(",")
UserBehavior(arr(0).toLong, arr(1).toLong, arr(2).toInt, arr(3), arr(4).toLong)
})
.assignAscendingTimestamps(_.timestamp * 1000L)
val uvStream = dataStream
.filter(_.behavior == "pv")
.map( data => ("uv", data.userId) )
.keyBy(_._1)
.timeWindow(Time.hours(1))
.trigger(new MyTrigger()) // 自定义触发器
.process( new UvCountWithBloom() )
uvStream.print()
env.execute("uv with bloom job")
}
}
// 触发器,每来一条数据,直接触发窗口计算并清空窗口状态
//CONTINUE:不做任何操作;
//FIRE_AND_PURGE:触发窗口事件并且清除窗口中积累的数据;
//FIRE:触发窗口事件不清除窗口中的累积的数据;
//PURGE:只清除窗口中的数据,且抛弃窗口;
class MyTrigger() extends Trigger[(String, Long), TimeWindow]{
override def onEventTime(time: Long, window: TimeWindow, ctx: Trigger.TriggerContext): TriggerResult = TriggerResult.CONTINUE
override def onProcessingTime(time: Long, window: TimeWindow, ctx: Trigger.TriggerContext): TriggerResult = TriggerResult.CONTINUE
override def clear(window: TimeWindow, ctx: Trigger.TriggerContext): Unit = {}
override def onElement(element: (String, Long), timestamp: Long, window: TimeWindow, ctx: Trigger.TriggerContext): TriggerResult =
TriggerResult.FIRE_AND_PURGE
}
// 自定义一个布隆过滤器,主要就是一个位图和hash函数
class Bloom(size: Long) extends Serializable{
private val cap = size // 默认cap应该是2的整次幂
// hash函数
def hash(value: String, seed: Int): Long = {
var result = 0
for( i <- 0 until value.length ){
result = result * seed + value.charAt(i)
}
// 返回hash值,要映射到cap范围内
(cap - 1) & result
}
}
// 实现自定义的窗口处理函数
class UvCountWithBloom() extends ProcessWindowFunction[(String, Long), UvCount, String, TimeWindow]{
// 定义redis连接以及布隆过滤器
lazy val jedis = new Jedis("hadoop102", 6379)
lazy val bloomFilter = new Bloom(1<<29) // 位的个数:2^6(64) * 2^20(1M) * 23(8bit)= 2^29 ,(64MB)可对应上亿位(上亿用户Id)
// 本来是收集齐所有数据、窗口触发计算的时候才会调用;现在每来一条数据都调用一次
override def process(key: String, context: Context, elements: Iterable[(String, Long)], out: Collector[UvCount]): Unit = {
// 先定义redis中存储位图的key
val storedBitMapKey = context.window.getEnd.toString
// 另外将当前窗口的uv count值,作为状态保存到redis里,用一个叫做uvcount的hash表来保存(windowEnd,count)
val uvCountMap = "uvcount"
val currentKey = context.window.getEnd.toString
var count = 0L
// 从redis中取出当前窗口的uv count值
if(jedis.hget(uvCountMap, currentKey) != null)
count = jedis.hget(uvCountMap, currentKey).toLong
// 去重:判断当前userId的hash值对应的位图位置,是否为0
val userId = elements.last._2.toString
// 计算hash值,就对应着位图中的偏移量
val offset = bloomFilter.hash(userId, 61)
// 用redis的位操作命令,取bitmap中对应位的值
val isExist = jedis.getbit(storedBitMapKey, offset)
if(!isExist){
// 如果不存在,那么位图对应位置置1,并且将count值加1
jedis.setbit(storedBitMapKey, offset, true)
jedis.hset(uvCountMap, currentKey, (count + 1).toString)
}
}
}