Async I/O 是阿里巴巴贡献给社区的一个呼声非常高的特性,于1.2版本引入。主要目的是为了解决与外部系统交互时网络延迟成为了系统瓶颈的问题。
**场景:**流计算中因原来的表字段不够,需要扩宽,这样我们就需要查询外部数据库关联以获取额外的字段,通常,我们的实现方式是向数据库发送用户a的查询请求,然后等待返回结果,在这之间我们无法发送用户b的查询请求。这是一种同步访问的模式,如下图左边所示。
很明显,同步IO会浪费大量的时间,所以我们采用异步IO解决表拉宽问题;
使用Aysnc I/O的前提条件
-
对外部系统进行异步IO访问的客户端API。(比如使用vertx,但是目前只支持scala 2.12的版本,可以使用java类库来做)
-
或者在没有这样的客户端的情况下,可以通过创建多个客户端并使用线程池处理同步调用来尝试将同步客户端转变为有限的并发客户端。但是,这种方法通常比适当的异步客户端效率低。(比如可以写ExecutorService来实现)
Asycn IO应用于DataStream
AsyncDataStream是一个工具类,用于将AsyncFunction应用于DataStream,AsyncFunction发出的并发请求都是无序的,该顺序基于哪个请求先完成,为了控制结果记录的发出顺序,flink提供了两种模式,分别对应AsyncDataStream的两个静态方法,OrderedWait和unorderedWait
AsyncDataStream.orderedWait();
AsyncDataStream.unorderWait();
orderedWait(有序):消息的发送顺序与接收到的顺序相同(包括 watermark ),也就是先进先出。
unorderWait(无序):
- 在ProcessingTime中,完全无序,即哪个请求先返回结果就先发送(最低延迟和最低消耗)。
- 在EventTime中,以watermark为边界,介于两个watermark之间的消息可以乱序,但是watermark和消息之间不能乱序,这样既认为在无序中又引入了有序,这样就有了与有序一样的开销。
实现思路:
1) 找到kafka中保存的mysql数据,过滤出来商品表数据
2) 将过滤出来的rowdata数据转换成GoodsWideBean实体对象,同时进行拉宽操作(关联维度表)
3) 将拉宽后的商品表转换成json字符转
4) 将商品数据写入掉kafka集群,供Druid实时摄取
因为是表拉宽,不需要考虑顺序问题,我们这里就以ProcessingTime作为演示,废话不多说,上代码:
- 实时获取商品数据ETL
package cn.itcast.shop.realtime.etl.process
import java.util.concurrent.TimeUnit
import cn.itcast.shop.realtime.etl.async.AsyncGoodsDetailRedisRequest
import cn.itcast.shop.realtime.etl.bean.GoodsWideBean
import cn.itcast.shop.realtime.etl.process.bean.MysqlBaseETL
import cn.itcast.shop.realtime.etl.utils.GlobalConfigUtil
import com.alibaba.fastjson.JSON
import com.alibaba.fastjson.serializer.SerializerFeature
import com.itcast.canal.bean.RowData
import org.apache.flink.streaming.api.scala.{AsyncDataStream, DataStream, StreamExecutionEnvironment, _}
case class GoodsDataETL(env: StreamExecutionEnvironment) extends MysqlBaseETL(env){
/**
* 处理数据的接口
*/
override def process(): Unit = {
/**
* 实现思路:
* 1: 找到kafka中保存的mysql数据,过滤出来商品表数据
* 2: 将过滤出来的rowdata数据转换成GoodsWideBean实体对象,同时进行拉宽操作(关联维度表)
* 3: 将拉宽后的商品表转换成json字符转
* 4: 将商品数据写入掉kafka集群,供Druid实时摄取
*/
//1: 找到kafka中保存的mysql数据,过滤出来商品表数据
val goodsCanalDataStream: DataStream[RowData] = getKafkaDataStream().filter(_.getTableName == "itcast_goods")
//2: 将过滤出来的rowdata数据转换成GoodsWideBean实体对象,同时进行拉宽操作(关联维度表)
val goodsWideDataStream: DataStream[GoodsWideBean] = AsyncDataStream.unorderedWait(
goodsCanalDataStream,
new AsyncGoodsDetailRedisRequest(),
5,
TimeUnit.SECONDS,
100)
//转换为Json
val goodsWideJson: DataStream[String] = goodsWideDataStream.map(goodsWide=>JSON.toJSONString(goodsWide,SerializerFeature.DisableCircularReferenceDetect))
//发送给kafka供Druid进行摄取
goodsWideJson.addSink(kafkaProducer(GlobalConfigUtil.`output.topic.goods`))
}
}
- 异步IO核心代码,AsyncGoodsDetailRedisRequest类编写:
package cn.itcast.shop.realtime.etl.async
import cn.itcast.shop.realtime.etl.bean._
import cn.itcast.shop.realtime.etl.utils.RedisUtil
import com.itcast.canal.bean.RowData
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.scala.async.{ResultFuture, RichAsyncFunction, _}
import redis.clients.jedis.Jedis
import scala.concurrent.Future
case class AsyncGoodsDetailRedisRequest() extends RichAsyncFunction[RowData, GoodsWideBean] {
var jedis: Jedis = _
//定义Future回调的执行上下文对象
implicit lazy val executor = ExecutionContext.fromExecutor(Executors.directExecutor())
override def asyncInvoke(rowData: RowData, resultFuture: ResultFuture[GoodsWideBean]): Unit = {
Future {
if (!jedis.isConnected) {
jedis = RedisUtil.getJedis()
//维度数据在第二个数据库中
jedis.select(1)
}
//商铺
val shopJson: String = jedis.hget("itcast_shop:dim_shops", rowData.getColumns.get("shopId"))
val dimShop = DimShopsDBEntity(shopJson)
//商品分类
//三级分类
val thirdCatJson: String = jedis.hget("itcast_shop:dim_goods_cats", rowData.getColumns.get("goodsCatId"))
val dimThirdCat = DimGoodsCatDBEntity(thirdCatJson)
//二级级分类
val secondCatJson: String = jedis.hget("itcast_shop:dim_goods_cats", dimThirdCat.parentId)
val dimSecondCat = DimGoodsCatDBEntity(secondCatJson)
//一级分类
val firstCatJson: String = jedis.hget("itcast_shop:dim_goods_cats", dimSecondCat.parentId)
val dimFirstCat = DimGoodsCatDBEntity(firstCatJson)
//门店商品分类
//一级分类
val secondShopCatJson: String = jedis.hget("itcast_shop:dim_shop_cats", rowData.getColumns.get("shopCatId2"))
val dimSecondShopCat = DimShopCatDBEntity(secondShopCatJson)
//二级分类
val firstShopCatJson: String = jedis.hget("itcast_shop:dim_shop_cats", rowData.getColumns.get("shopCatId1"))
val dimFirstShopCat = DimShopCatDBEntity(firstShopCatJson)
val cityJSON = jedis.hget("itcast_shop:dim_org", dimShop.areaId + "")
val dimOrgCity = DimOrgDBEntity(cityJSON)
val regionJSON = jedis.hget("itcast_shop:dim_org", dimOrgCity.parentId + "")
val dimOrgRegion = DimOrgDBEntity(regionJSON)
val goodsWide = GoodsWideBean(rowData.getColumns.get("goodsId").toLong,
rowData.getColumns.get("goodsSn"),
rowData.getColumns.get("productNo"),
rowData.getColumns.get("goodsName"),
rowData.getColumns.get("goodsImg"),
rowData.getColumns.get("shopId"),
dimShop.shopName,
rowData.getColumns.get("goodsType"),
rowData.getColumns.get("marketPrice"),
rowData.getColumns.get("shopPrice"),
rowData.getColumns.get("warnStock"),
rowData.getColumns.get("goodsStock"),
rowData.getColumns.get("goodsUnit"),
rowData.getColumns.get("goodsTips"),
rowData.getColumns.get("isSale"),
rowData.getColumns.get("isBest"),
rowData.getColumns.get("isHot"),
rowData.getColumns.get("isNew"),
rowData.getColumns.get("isRecom"),
rowData.getColumns.get("goodsCatIdPath"),
dimThirdCat.catId.toInt,
dimThirdCat.catName,
dimSecondCat.catId.toInt,
dimSecondCat.catName,
dimFirstCat.catId.toInt,
dimFirstCat.catName,
dimFirstShopCat.getCatId,
dimFirstShopCat.catName,
dimSecondShopCat.getCatId,
dimSecondShopCat.catName,
rowData.getColumns.get("brandId"),
rowData.getColumns.get("goodsDesc"),
rowData.getColumns.get("goodsStatus"),
rowData.getColumns.get("saleNum"),
rowData.getColumns.get("saleTime"),
rowData.getColumns.get("visitNum"),
rowData.getColumns.get("appraiseNum"),
rowData.getColumns.get("isSpec"),
rowData.getColumns.get("gallery"),
rowData.getColumns.get("goodsSeoKeywords"),
rowData.getColumns.get("illegalRemarks"),
rowData.getColumns.get("dataFlag"),
rowData.getColumns.get("createTime"),
rowData.getColumns.get("isFreeShipping"),
rowData.getColumns.get("goodsSerachKeywords"),
rowData.getColumns.get("modifyTime"),
dimOrgCity.orgId,
dimOrgCity.orgName,
dimOrgRegion.orgId,
dimOrgRegion.orgName)
goodsWide
println(s"商品明细异步拉去${goodsWide}")
resultFuture.complete(Array(goodsWide))
}
}
override def timeout(input: RowData, resultFuture: ResultFuture[GoodsWideBean]): Unit = {
println("超时了,赶快回来处理")
}
override def close(): Unit = {
if (jedis != null && jedis.isConnected) {
jedis.close()
}
}
override def open(parameters: Configuration): Unit = {
jedis = RedisUtil.getJedis()
jedis.select(1)
}
}
连接redis,连接kafka,创建goods表的bean对象这些我就不一一赘述了(收工 😗 )