在统计UV的时候,是把所有的UserID都存储在了窗口计算的状态里,在窗口收集数据的过程中,状态会不断增大。一般情况,只要不超过内存的承受范围,就没什么问题,但是如果我们的数据量很大呢?把所欲数据都暂存在内存里,显然不是有效的好主意。我们首先想到,可以利用Redis这种内存级的k-v数据库,做缓存。但如果我们遇到的情况非常极端,数据大到惊人呢?比如上亿级的用户,要去重计算UV。如果放到redis中,亿级的用户id(每个20字节左右的话)可能需要几G甚至几十G的空间来存储。当然放到redis中,用集群进行扩展也不是不可以,但明显代价太大了。一个更好的想法是,我们不需要完整的存储用户ID的信息,只要知道他不在就行了。用一位(bit)就可以表示一个用户的状态了。这种思想的具体实现就是布隆过滤器(Bloom Filter)布隆过滤器本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “ 某样东西一定不存在或者可能存在 ”它本身是一个很长的二进制向量,既然是二进制的向量,那么,存放的不是0,就是1.相比传统的List、Set、Map等数据结构,它更高效、占用空间更少。缺点是:返回的结果是概率性的,而不是确切的目标是,利用某种方法(一般是Hash函数)把每个数据,对应到一个位图的某一位上去;如果数据存在,那么以为就是1,不存在则为0
hash(hashcode) & (length -1)
length为5,索引范围是(0,1,2,3,4),0能取到,4能取到,123永远取不到
-
索引4能取到,length是5,hash任意:
000101110
&
000000100
------------
000000100 => 4
-
索引0能取到,length是5,hash任意:
000101010
&
000000100
------------
000000100 => 0
-
索引123如果想取到,length是5,hash任意:
因为是与运算,所以要取到1,只能下面是000000001
000101011
&
000000001
------------
000000001 => 1
因为是与运算,所以要取到2,只能下面是000000010
000101011
&
000000010
------------
000000010 => 2
因为是与运算,所以要取到3,只能下面是000000011
000101011
&
000000011
------------
000000011 => 3
所以&后面的length只能是2的n次方,2的n次方减1之后后面的才能每一位都是1,才能取到所有位如:2的3次方-1 = 00000111此种结构自己实现复杂,而Redis有这种数据结构BITMAP。redis字符串存储:key最大为512M,value最大为512M=2的32次方,足够存储
布隆过滤器实现UVBloomjedis.setbit(Key,offset,true)
jedis.setbit(Key,offset,false)
val isExist = jedis.getbit(Key,offset)
result:是上面说的hash的结果,不让重复(cap-1):是保证 -1 之后&后面的内容都是1,每个位都能得到随机数种子seed,是为了不让不随机,例如:// TODO 布隆过滤器
class Bloom(size:Long) extends Serializable{
private val cap = size
def hash(value:String,seed:Int):Long={
//最简单的hash算法,每一位字符的ascii码值,乘以seed之后,做重叠
var result = 0
for (i <- 0 until value.length){
result = result * seed + value.charAt(i)
}
(cap -1) & result
}
}
因为种子(seed)一样,所以结果一样,我们这里也需要这种效果,同一个字符串,得到的结果是一样的import java.util.Random;
public class TestRandom {
public static void main(String[] args) {
Random r = new Random(100);
for ( int i = 1; i<=5;i++ ) {
System.out.println(r.nextInt(10));
}
System.out.println("------------------");
Random r1 = new Random(100);
for ( int i = 1; i<=5;i++ ) {
System.out.println(r1.nextInt(10));
}
}
}
使用触发器Trigger,当每条数据进入窗口时进行计算:package com.duoduo.utils.networkflow
import com.duoduo.hotitems.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
/**
* Author z
* Date 2020-11-08 14:33:43
*/
object UvBloomFilter {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
env.setParallelism(1)
val stream = env.readTextFile(this.getClass.getClassLoader
.getResource("User.csv").getPath)
.map(line => {
val linearray = line.split(",")
UserBehavior(linearray(0).toLong, linearray(1).toLong,
linearray(2).toInt, linearray(3), linearray(4).toLong)
})
.assignAscendingTimestamps(_.timestamp * 1000)
.filter(_.behavior == "pv")
.map(data=>("dummykey",data.userId))
.keyBy(_._1)
.timeWindow(Time.hours(1))
// 使用触发器,当每一条数据进入到窗口进行计算
.trigger(new MyTrigger())
.process(new UvCountBloomProcess())
env.execute("Unique Visitor BloomFilter Job")
}
}
class MyTrigger() extends Trigger[(String,Long),TimeWindow]{
override def onElement(t: (String, Long), timestamp: Long, w: TimeWindow,
triggerContext: Trigger.TriggerContext): TriggerResult = {
//每来一条数据,就触发窗口操作并清空,
TriggerResult.FIRE_AND_PURGE
}
override def onProcessingTime(l: Long, w: TimeWindow,
triggerContext: Trigger.TriggerContext): TriggerResult = {
TriggerResult.CONTINUE
}
override def onEventTime(l: Long, w: TimeWindow,
triggerContext: Trigger.TriggerContext): TriggerResult = {
TriggerResult.CONTINUE
}
override def clear(w: TimeWindow, triggerContext: Trigger.TriggerContext): Unit = ???
}
-
aggregate是增量聚合,不太合适
-
process也不太合适,因为是全量数据操作
class UvCountBloomProcess
extends ProcessWindowFunction[(String,Long),UvCount,String,TimeWindow]{
// 初始化 Jedis
lazy val jedis = new Jedis("localhost",6379)
// 初始化Bloom
lazy val bloom = new Bloom(1<<29)
override def process(key: String, context: Context,
elements: Iterable[(String, Long)], out: Collector[UvCount]): Unit = {
//窗口的大小是1小时,每个窗口的key是不能重复的,所以可以用窗口的时间
val storeKey = context.window.getEnd.toString
var count =0L
if (jedis.hget("count",storeKey)!=null) {
count=jedis.hget("count",storeKey).toLong
}
// 每个用户的id
val userId = elements.last._2.toString
val offset = bloom.hash(userId,61)
// 获取当前窗口中,此用户的位置是否存在
val isExist = jedis.getbit(storeKey,offset)
//redis中肯定不存在
if (!isExist){
// 设置位图
jedis.setbit(storeKey,offset,true)
//uv + 1
jedis.hset("count",storeKey,(count +1).toString)
//输出结果
out.collect(UvCount(storeKey.toLong,count +1))
}else{//redis 中可能存在,直接输出
out.collect(UvCount(storeKey.toLong,count))
}
}
}