在统计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

Flink 使用布隆过滤器统计UV_redis

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次方,足够存储
  •  
  jedis.setbit(Key,offset,true)  jedis.setbit(Key,offset,false)    val isExist = jedis.getbit(Key,offset)
布隆过滤器实现UVBloom
  •  
// 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    }}
result:是上面说的hash的结果,不让重复(cap-1):是保证 -1 之后&后面的内容都是1,每个位都能得到随机数种子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)); } }}

Flink 使用布隆过滤器统计UV_redis_02

因为种子(seed)一样,所以结果一样,我们这里也需要这种效果,同一个字符串,得到的结果是一样的
  •  
package com.duoduo.utils.networkflow
import com.duoduo.hotitems.UserBehaviorimport org.apache.flink.streaming.api.TimeCharacteristicimport org.apache.flink.streaming.api.scala._import org.apache.flink.streaming.api.scala.function.ProcessWindowFunctionimport org.apache.flink.streaming.api.windowing.time.Timeimport org.apache.flink.streaming.api.windowing.triggers.{Trigger, TriggerResult}import org.apache.flink.streaming.api.windowing.windows.TimeWindowimport org.apache.flink.util.Collectorimport 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()) .print 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 = ???}
使用触发器Trigger,当每条数据进入窗口时进行计算:
  • aggregate是增量聚合,不太合适

  • process也不太合适,因为是全量数据操作

onElement方法中TriggerResult.FIRE_AND_PURGE用完之后就丢弃
  •  
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)) } }}

 

Flink 使用布隆过滤器统计UV_ide_03