在上文中,我们使用sparkStreaming对kafka中某topic数据进行数据处理后再重新写入kafka,其实整套逻辑思路并不复杂,但全都写在一个类里面,只能一次性使用,修改时要修改类主体的内容,扩展性很差,安全性也得不到保障。
因此本文把该类主体内容拆解,抽出各个部分,便于管理和扩展
文章目录
1、思路分析:
1.1、从逻辑上抽出第一层概念:
- 用户要读Kafka的数据,用户要对数据进行处理,用户要往kafka里写数据
1.2、通过核心动作抽出第二层概念
- XXX用户->读XXX库的数据,XXX用户->对数据进行XXX处理,XXX用户把数据->写入XXX库
1.3、把可以替换的属性方法抽出来,得到:
- 顶层接口:读、写、操作
- 实现类1:读kafka、写kafka、操作数据
- 实现类2:执行类,串联上述三者的实现操作,把词组成句子
2、代码实现:
2.1、顶级接口功能设计
2.1.1、读
/**
* @tparam T:读MySQL时,返回值就会变,所以用泛型T
*/
trait ReadTrait[T] {
import scala.collection.JavaConversions._
//kafka配置参数底层默认是Object
def reader(prop:Map[String,Object],tableName:String):InputDStream[T]
def reader(prop:Properties,tableName:String):InputDStream[T] = reader(prop.toMap[String,Object],tableName)
}
2.1.2、写
/**
* @tparam T
*/
trait WriteTrait[T] {
import scala.collection.JavaConversions._
def write(prop:Map[String,Object],tableName:String,ds:DStream[T]):Unit
def write(prop:Properties,tableName:String,ds:DStream[T]):Unit=write(prop.toMap[String,Object],tableName,ds)
}
2.1.3、数据处理
进来一个出去一个,进来是T类型,出去是V类型
/**
* 对于不同的topic数据,转换变形的方法是不一样的,继承特质重写变化方法
* @tparam T:进来时的类型是T
* @tparam V:出去的时类型有可能会变,就用V
*/
trait TransformTrait[T,V] {
def transform(in:InputDStream[T]):DStream[V]
}
2.2、工具类:
把kafka不可序列化的record,包装为可序列的消息
/**
* 工具类
* @param producer
* @tparam K
* @tparam V
*/
class KafkaSink[K,V](producer:()=>KafkaProducer[K,V]) extends Serializable{
//懒汉模式,定义一个producer
lazy val prod:KafkaProducer[K, V] = producer()
def send(topic:String,key:K,value:V)={
prod.send(new ProducerRecord[K,V](topic,key,value))
}
def send(topic:String,value:V)={
prod.send(new ProducerRecord[K,V](topic,value))
}
}
object KafkaSink{
import scala.collection.JavaConversions._
def apply[K,V](config:Map[String,Object]) ={
val createKafkaProducer=()=>{
val produ = new KafkaProducer[K, V](config)
//销毁数据时做检查,效率极低,删了的话,出现垃圾时无法回收的问题
sys.addShutdownHook{
produ.close()
}
produ
}
new KafkaSink[K,V](createKafkaProducer)
}
def apply[K,V](config:Properties) :KafkaSink[K,V]=apply(config.toMap)
}
2.3、实现类1:开发人员
2.3.1、KafkaReader
数据库是kafka时,读的操作就是kafka消费者
/**
* ConsumerRecord[String,String]:读出来的是String
* InputDStream:
* @param ssc:在一个executor上只能有一个StreamingContext
*/
class KafkaReader(ssc:StreamingContext) extends ReadTrait[ConsumerRecord[String,String]]{
override def reader(prop: Map[String, Object],tableName:String): InputDStream[ConsumerRecord[String,String]] = {
KafkaUtils.createDirectStream(
ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[String,String](Set(tableName),prop)
)
}
}
object KafkaReader{
def apply(ssc: StreamingContext): KafkaReader = new KafkaReader(ssc)
}
2.3.2、KafkaWriter
/**
*
* @param ssc
* @tparam T
*/
class KafkaWriter[T](ssc:StreamingContext) extends WriteTrait[String]{
override def write(prop: Map[String, Object], tableName: String,ds:DStream[String]): Unit = {
//用KafkaSink作为广播对象
val bc = ssc.sparkContext.broadcast(KafkaSink[String,String](prop))
ds.foreachRDD(rdd=>rdd.foreachPartition(iter=>{
iter.map(record=>{
bc.value.send(tableName,record)
}).foreach(x=>x)//动作算子触发懒加载
}))
}
}
object KafkaWriter{
def apply[T](ssc: StreamingContext): KafkaWriter[T] = new KafkaWriter(ssc)
}
2.3.3、实现类2:用户
自定义对数据的处理
/**
* 对于不同的topic数据,转换变形的方法是不一样的,继承特质重写变化方法
* 重写TransformTrait方法
*/
trait User_friends_Trait extends TransformTrait[ConsumerRecord[String, String], String] {
override def transform(in: InputDStream[ConsumerRecord[String, String]]): DStream[String] = {
in.flatMap(line => {
val info = line.value().split(",", -1)
info(1).filter(_ != "").split(" ").map(friendid => {
(info(0), friendid)
})
}).filter(_._2!="").map(tp => tp.productIterator.mkString(","))
}
}
2.4、执行类:通过上下文环境串联各个抽象概念
这里采用了混用特质的方法
/**
* 设计模式:模板模式
*/
class KTKExecutor(readConf:Map[String,Object],writeConf:Map[String,Object]){
tran:TransformTrait[ConsumerRecord[String,String],String]=>
def worker(intopic:String,outtopic:String)={
//创建一个streamingContext
val conf = new SparkConf().setMaster("local[*]").setAppName("readtest")
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.set("spark.streaming.kafka.consumer.poll.ms", "10000")
val ssc = new StreamingContext(conf, Seconds(1))
ssc.checkpoint("checkpoint")
//调用kafka数据读取
val kr = new KafkaReader(ssc).reader(readConf,intopic)
//调用混入特质进行数据处理
val ds = tran.transform(kr)
//调用kafka写入topic
KafkaWriter(ssc).write(writeConf,outtopic,ds)
ssc.start()
ssc.awaitTermination()
}
}
2.5、用户调用:
输入参数,动态混入,调用方法
object UFtest {
def main(args: Array[String]): Unit = {
val inParams = Map(
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG->"single:9092",
ConsumerConfig.GROUP_ID_CONFIG->"uf1",
ConsumerConfig.MAX_POLL_RECORDS_CONFIG->"1000",
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG->classOf[StringDeserializer],
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG->classOf[StringDeserializer],
ConsumerConfig.AUTO_OFFSET_RESET_CONFIG->"earliest"
)
val outParams = Map(
ProducerConfig.BOOTSTRAP_SERVERS_CONFIG->"single:9092",
ProducerConfig.ACKS_CONFIG->"all",
ProducerConfig.RETRIES_CONFIG->"0",
ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG->classOf[StringSerializer],
ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG->classOf[StringSerializer]
)
(new KTKExecutor(inParams,outParams) with User_friends_Trait)
.worker("user_friends_raw","user_friends_ss")
}
}
设计结构: