我有一个包含多种不同类型json的流,每种类型都与用户事件相关,拆分和聚合的最有效方式是什么

我有一个包含多种不同类型json的流,每种类型都与用户事件相关,拆分和聚合的最有效方式是什么,json,apache-spark,events,stream,Json,Apache Spark,Events,Stream,我有一个包含多种不同类型json消息的流。 总共有65种json事件类型,都具有不同的模式。 它们都共用一个用户id {'id': 123, 'event': 'clicked', 'target': 'my_button'} {'id': 123, 'event': 'viewed', 'website': 'http://xyz1...'} {'id': 123, 'event': 'viewed', 'website': 'http://xyz2...'} {'id': 123, 'eve

我有一个包含多种不同类型json消息的流。 总共有65种json事件类型,都具有不同的模式。 它们都共用一个用户id

{'id': 123, 'event': 'clicked', 'target': 'my_button'}
{'id': 123, 'event': 'viewed', 'website': 'http://xyz1...'}
{'id': 123, 'event': 'viewed', 'website': 'http://xyz2...'}
{'id': 123, 'event': 'login', 'username': 'Bob'}
{'id': 456, 'event': 'viewed', 'website': 'http://xyz3...'}
{'id': 456, 'event': 'login', 'username': 'Susy'}
我想处理所有事件类型,每个事件类型都有自定义字段,然后按用户在所有筛选器类型中聚合所有事件

{'id': 123, 'page_view_cnt': 100, 'user': 'Bob', 'click_cnt': 20}
{'id': 456, 'page_view_cnt': 14, 'user': 'Susy'}
有人知道一种有效的方法吗。以下是当前的思维过程

  • 从一连串的线开始
  • 使用GSON解析json,而不是使用内置的json解析器,后者可能会尝试推断类型
  • 根据每种类型创建65个筛选器语句。json将具有event=xyz,我可以对其进行区分
  • 将每个筛选器上的自定义属性聚合到用户id->properties的映射中
  • 合并所有过滤器中的所有贴图

这听起来合理吗?或者有更好的方法吗?

下面是我使用RDDAPI和Jackson提出的方法。我选择了低级Spark API,因为它是无模式的,并且不确定结构化API如何适合变量输入事件类型。如果提到的Gson支持多态反序列化,那么它可以用来代替Jackson,我只是选择Jackson,因为我更熟悉它

问题可分为以下几个步骤:

  • 按事件类型将输入反序列化到对象中
  • 按id和类型减少。对于不同的类型,reduce需要有不同的行为,例如,视图被简化为一个总和,而用户名需要以不同的方式处理。在本例中,我们假设用户名在
    id
    中是唯一的,然后选择第一个
  • id
    收集减少的项目
  • 步骤2最需要注意,因为Spark API中没有此类功能,并且需要进行某种运行时检查,以确定反序列化事件是否属于不同的类。为了克服这个问题,让我们介绍一个通用特性
    可还原的
    ,它可以封装不同的类型:

    trait Reducible[T] {
        def reduce(that: Reducible[_]): this.type
    
        def value: T
    }
    
    // simply reduces to sum
    case class Sum(var value: Int) extends Reducible[Int] {
        override def reduce(that: Reducible[_]): Sum.this.type = that match {
            case Sum(thatValue) =>
                value += thatValue
                this
        }
    }
    
    // for picking the first element, i.e. username
    case class First(value: String) extends Reducible[String] {
        override def reduce(that: Reducible[_]): First.this.type = this
    }
    
    运行时检查是在这些类中处理的,例如,如果右侧对象不是同一类型的对象,
    Sum
    将失败

    接下来,让我们定义事件的模型,并告诉Jackson如何处理多态性:

    @JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="event", visible=true)
    sealed trait Event[T] {
        val id: Int
        val event: String
    
        def value: Reducible[T]
    }
    
    abstract class CountingEvent extends Event[Int] {
        override def value: Reducible[Int] = Sum(1)
    }
    
    @JsonTypeName("clicked") case class Click(id: Int, event: String, target: String) extends CountingEvent
    @JsonTypeName("viewed") case class View(id: Int, event: String, website: String) extends CountingEvent
    @JsonTypeName("login") case class Login(id: Int, event: String, username: String) extends Event[String] {
        override def value: Reducible[String] = First(username)
    }
    
    object EventMapper {
        private val mapper = new ObjectMapper().registerModule(DefaultScalaModule)
        // the list of classes could be auto-generated, see
        // https://stackoverflow.com/questions/34534002/getting-subclasses-of-a-sealed-trait
        mapper.registerSubtypes(classOf[Click], classOf[View], classOf[Login])
    
        def apply(v1: String): Event[_] = mapper.readValue(v1, classOf[Event[_]])
    }
    
    所有事件都应有
    id
    event
    字段。后者用于确定反序列化到哪个类,Jackson需要事先知道所有类。Trait
    Event
    被声明为密封的Trait,因此所有实现类都可以在编译时确定。我省略了这个反射步骤,只是对类列表进行了硬编码,这里有一个很好的答案,可以自动完成

    现在我们准备编写应用程序逻辑。为了简单起见,
    sc.parallelize
    用于加载示例数据。也可以使用火花流

    val in = List(
        "{\"id\": 123, \"event\": \"clicked\", \"target\": \"my_button\"}",
        "{\"id\": 123, \"event\": \"viewed\", \"website\": \"http://xyz1...\"}",
        "{\"id\": 123, \"event\": \"viewed\", \"website\": \"http://xyz1...\"}",
        "{\"id\": 123, \"event\": \"login\", \"username\": \"Bob\"}",
        "{\"id\": 456, \"event\": \"login\", \"username\": \"Sue\"}",
        "{\"id\": 456, \"event\": \"viewed\", \"website\": \"http://xyz1...\"}"
    )
    
    // partition (id, event) pairs only by id to minimize shuffle
    // when we later group by id
    val partitioner = new HashPartitioner(10) {
    
        override def getPartition(key: Any): Int = key match {
            case (id: Int, _) => super.getPartition(id)
            case id: Int => super.getPartition(id)
        }
    }
    
    sc.parallelize(in)
        .map(EventMapper.apply)
        .keyBy(e => (e.id, e.event))
        .mapValues(_.value)
        .reduceByKey(partitioner, (left, right) => left.reduce(right))
        .map {
            case ((id, key), wrapper) => (id, (key, wrapper.value))
        }
        .groupByKey(partitioner)
        .mapValues(_.toMap)
        .foreach(println)
    
    输出:

    (123,Map(clicked -> 1, viewed -> 2, login -> Bob))
    (456,Map(login -> Sue, viewed -> 1))
    

    你能澄清一下“我有一条流”和“处理这些流并合并回来”之间明显的矛盾吗?谢谢,我会更新这个问题。65种json类型在一个流中,但有许多不同类型的事件。我的意思是从同一个流中处理所有这些事件,然后将它们合并回单个用户id。是否可能有多个用户具有相同的事件id?在输出示例中,两个聚合都具有
    id=123
    和用户
    Bob
    Sue
    。如果是这种情况,如何知道这是鲍勃还是苏
    {'id':123,'事件':'已查看','网站':'http://xyz2...“}