Flink_网站独立访客数(UV)(Redis+布隆过滤器)

网站的独立访客数( Unique Visitor UV )。 UV 指的是 一段时间(比如一小时)内访问网站的总人数,1 天内同一访客的多次访问 只记录为一个访客 。(对同样用户IP去重)
输入数据:

 方法一:全窗口函数 + 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
方法二:布隆过滤器 + 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)
    }
  }
}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值