目录
1.对于异步IO的需求
在与外部系统交互(用数据库中的数据扩充流数据)的时候,需要考虑与外部系统的通信延迟对整个流处理应用的影响。
简单地访问外部数据库的数据,比如使用MapFunction,通常意味着同步交互:MapFunction向数据库发送一个请求然后一直等待,直到收到响应。在许多情况下,等待占据了函数运行的大部分时间。
与数据库异步交互是指一个并行函数实例可以并发地处理多个请求和接受多个响应。这样,函数在等待的时间可以发送其他请求和接收其他响应。至少等待的时间可以被多个请求摊分。大多数情况下,异步交互可以大幅度提高流处理的吞吐量。
注意:仅仅提高MapFunction的并行度(parallelism)在有些情况下也可以提升吞吐量,但是这样做通常会导致非常高的资源消耗:更多的并行MapFunction实例意味着更多的Task、更多的线程、更多的Flink内部网络连接、更多的与数据库的网络连接、更多的缓冲和更多程序内部协调的开销。
2.异步IO API
Flink的异步IO API允许用户在流处理中使用异步请求客户端。API处理与数据流的集成,同时还能处理好顺序、事件时间和容错等。
在具备异步数据库客户端的基础上,实现数据流转换操作与数据库的异步IO交互需要以下三部分:
- 实现分发请求的AsyncFunction
- 获取数据库交互的结果并发送给ResultFuture的回调函数
- 将异步IO操作应用于DataStream作为DataStream的一次转换操作
重要提示:第一次调用ResultFunction.complete后ResultFuture就完成了。后续的complete调用都将被忽略。
下面两个参数控制异步操作:
- Timeout:超时参数定义了异步请求发出多久后未得到响应即被认定为失败。它可以防止一直等待得不到响应的请求。
- Capacity:容量参数定义了可以同时进行的异步请求数。即使异步IO通常带来更高的吞吐量,执行异步IO操作的算子仍然可能成为流处理的瓶颈。限制并发请求的数量可以确保算子不会持续累积待处理的请求进而造成积压,而是在容量耗尽时触发反压。
结果的顺序
AsyncFunction发出的并发请求经常以不确定的顺序完成,这取决于请求得到响应的顺序,Flink提供两种模式控制结果记录以何种顺序发出。
- 无序模式:异步请求一结束就立刻发出结果记录。流中记录的顺序在经过异步IO算子之后发生了改变。当使用处理时间作为基本时间特征时,这个模式具有最低的延迟和最少的开销。此模式使用AsyncDataStream.unorderedWait(...)方法。
- 有序模式:这种模式保持了流的顺序。发出结果记录的顺序与触发异步请求的顺序(记录输入算子的顺序)相同。为了实现这一点,算子将缓冲一个结果记录直到这条记录前面的所有记录都发出(或超时)。由于记录或者结果要在checkpoint的状态中保存更长的时间,所以与无序模式相比,有序模式通常会带来一些额外的延迟和checkpoint开销。此模式使用AsyncDataStream.orderedWait(...)方法。
3.封装线程池工具类
public class ThreadPoolUtil {
private static ThreadPoolExecutor threadPoolExecutor = null;
private ThreadPoolUtil(){}
public static ThreadPoolExecutor getThreadPool(){
if(threadPoolExecutor == null){
synchronized(ThreadPoolUtil.class){
if(threadPoolExecutor == null){
threadPoolExecutor = new ThreadPoolExecutor(8,
16,
1L,
TimeUnit.MINUTES,
new LinkedBlockingDeque<>());
}
}
}
return threadPoolExecutor;
}
}
参数说明:
- corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去。
- maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量。
- keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁。
- unit:keepAliveTime的单位。
- workQueue:任务队列,被添加到线程池中,但尚未被执行的任务。
4.封装维度查询工具类(使用redis进行热点key缓存)
public class DimUtil {
public static JSONObject getDimInfo(Connection connection, String tableName, String id) throws Exception {
//查询Phoenix之前 先查询Redis
Jedis jedis = RedisUtil.getJedis();
String redisKey = "DIM:" + tableName + ":" +id;
String dimInfoJsonStr = jedis.get(redisKey);
//判断是否命中
if(dimInfoJsonStr != null){
//重置过期时间
jedis.expire(redisKey, 24*60*60);
//归还连接
jedis.close();
//返回结果
return JSONObject.parseObject(dimInfoJsonStr);
}
//拼接查询语句
String querySql = "select * from "+ GmallConfig.HBASE_SCHEMA + "." + tableName + " where id='" + id + "'";
//查询Phoenix
List<JSONObject> queryList = JdbcUtil.queryList(connection, querySql, JSONObject.class, false);
JSONObject dimInfoJson = queryList.get(0);
//返回结果之前,将数据写入Redis
jedis.set(redisKey, dimInfoJson.toJSONString());
jedis.expire(redisKey, 24*60*60);
jedis.close();
//返回结果
return dimInfoJson;
}
//如果维表有变化 删除redis中缓存 保持一致
public static void delRedisDimInfo(String tableName, String id){
Jedis jedis = RedisUtil.getJedis();
String redisKey = "DIM:" + tableName + ":" +id;
jedis.del(redisKey);
jedis.close();
}
}
5.封装JDBC工具类(使用Phoenix连接)
public class JdbcUtil {
/**
* @param connection 连接器类型(mysql、phoenix、...)
* @param querySql 查询sql语句
* @param clz 返回list的类型
* @param underScoreToCamel 是否将数据字段的下划线转换成Bean中的驼峰标识,例如 user_code -> userCode
* @return 返回一行或者多行记录行成的list
*/
public static <T> List<T> queryList(Connection connection, String querySql, Class<T> clz, boolean underScoreToCamel) throws Exception {
//创建集合用于存放查询结果数据
ArrayList<T> resultList = new ArrayList<>();
//预编译sql
PreparedStatement preparedStatement = connection.prepareStatement(querySql);
//执行查询
ResultSet resultSet = preparedStatement.executeQuery();
//获取元数据信息
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
//解析resultSet
while(resultSet.next()){
//创建泛型对象
T t = clz.newInstance();
//给泛型对象赋值
for (int i = 1; i < columnCount + 1; i++) {
//获取列名
String columnName = metaData.getColumnName(i);
//判断是否需要转换为驼峰命名
if(underScoreToCamel){
//转换成驼峰命名
columnName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, columnName.toLowerCase());
}
//获取列值
Object value = resultSet.getObject(i);
//给泛型对象赋值
BeanUtils.setProperty(t, columnName, value);
}
//将对象添加到集合
resultList.add(t);
}
preparedStatement.close();
resultSet.close();
return resultList;
}
}
6.封装DimAsyncFunction抽象类
为了通用,使用抽象类,获取维表的key和join维表延伸在子类实现,更加灵活通用
public abstract class DimAsyncFunction<T> extends RichAsyncFunction<T, T> {
private Connection connection;
private ThreadPoolExecutor threadPoolExecutor;
private String tableName;
public DimAsyncFunction(String tableName) {
this.tableName = tableName;
}
@Override
public void open(Configuration parameters) throws Exception {
connection = DriverManager.getConnection(GmallConfig.PHOENIX_SERVER);
//线程池
threadPoolExecutor = ThreadPoolUtil.getThreadPool();
}
public abstract String getKey(T t);
public abstract void joinInfo(T t, JSONObject dimInfo) throws ParseException;
@Override
public void asyncInvoke(T t, ResultFuture<T> resultFuture) throws Exception {
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
try{
//获取查询的主键
String id = getKey(t);
//查询维度信息
JSONObject dimInfo = DimUtil.getDimInfo(connection, tableName, id);
//补充维度信息
if(dimInfo != null){
joinInfo(t, dimInfo);
}
//将数据输出
resultFuture.complete(Collections.singletonList(t));
}catch (Exception e){
e.printStackTrace();
}
}
});
}
@Override
public void timeout(T input, ResultFuture<T> resultFuture) {
//超时处理
System.out.println("TimeOut:" + input);
}
}
7.主程序调用异步IO代码
SingleOutputStreamOperator<OrderWide> orderWideWithProvinceDS = AsyncDataStream.unorderedWait(
orderWideWithUserDS,
new DimAsyncFunction<OrderWide>("DIM_BASE_PROVINCE") {
@Override
public String getKey(OrderWide orderWide) {
return orderWide.getProvince_id().toString();
}
@Override
public void joinInfo(OrderWide orderWide, JSONObject dimInfo) throws ParseException {
orderWide.setProvince_name(dimInfo.getString("NAME"));
orderWide.setProvince_area_code(dimInfo.getString("AREA_CODE"));
orderWide.setProvince_iso_code(dimInfo.getString("ISO_CODE"));
orderWide.setProvince_3166_2_code(dimInfo.getString("ISO_3166_2"));
}
},60,TimeUnit.SECONDS);