目录
1.实时查询维表
2.预加载全量数据
3.LRU 缓存
4.广播变量
优点:维表数据实时更新,可以做到实时同步到。
缺点:访问压力大,如果失败会造成线程阻塞。
实时查询维表是指用户在Flink算子中直接访问外部数据库。这种方式可以保证数据是最新的,但是当我们流计算数据过大,会对外部系统带来巨大的访问压力,比如:连接失败,连接池满等情况,就会导致线程阻塞。task等待数据返回.
核心就是通过Map算子中建立访问外部系统的连接。核心代码如下:
public static void main(String[] args) throws Exception {
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
final DataStreamSource dataStreamSource = env.socketTextStream("192.168.8.174", 9999);
dataStreamSource.map(new Sync()).printToErr();
env.execute("start...");
}
static class Sync extends RichMapFunction {
private Connection connection = null;
//初始化建立连接
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
Class.forName("com.mysql.jdbc.Driver"); //注册数据库驱动
connection = DriverManager.getConnection("jdbc:mysql://192.168.8.181:3306/niu?characterEncoding=UTF-8", "root", "Fy123.com");
}
@Override
public String map(String isa) throws Exception {
//根据isa 查询 vin
PreparedStatement pst = connection.prepareStatement("select vin from fy_records where isa = ?");
pst.setString(1, isa);
ResultSet resultSet = pst.executeQuery();
String vin = null;
while (resultSet.next()) {
vin = resultSet.getString(1);
}
pst.close();
return isa + "_" + vin;
}
//注意要关闭连接
@Override
public void close() throws Exception {
super.close();
connection.close();
}
}
优点:避免频繁访问而导致连接和性能的问题
缺点:一旦维表发生更新,flink任务无法感知。不过可以通过定时来拉取维表数据.
当任务启动时,就将维表数据全部加载到内存中,然后数据在内存中进行关联,不需要直接访问外部数据库。核心代码如下:
public static void main(String[] args) throws Exception {
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
final DataStreamSource dataStreamSource = env.socketTextStream("192.168.8.174", 9999);
dataStreamSource.map(new WhoLoad()).printToErr();
env.execute("start...");
}
static class WhoLoad extends RichMapFunction {
private Logger logger = LoggerFactory.getLogger(WhoLoad.class);
//缓存
private Map cache = new HashMap();
//每隔1分钟重新加载一次mysql数据到内存
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
//初始化0,等上次任务完成后,等待10秒执行本次任务 定义了一个10秒的定时器,定时执行查询数据库的方法
Executors.newScheduledThreadPool(3).scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
load();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
},
0, //初始化时间
10, //任务间隔时间
TimeUnit.MILLISECONDS); //时间单位
}
@Override
public String map(String s) throws Exception {
final String vin = cache.get(s);
return "根据isa 获取到的vin : " + vin;
}
//加载mysql数据到内存
private void load() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver"); //注册数据库驱动
Connection connection = DriverManager.getConnection("jdbc:mysql://192.168.8.181:3306/niu?characterEncoding=UTF-8", "root", "Fy123.com");
//根据isa 查询 vin
PreparedStatement pst = connection.prepareStatement("select isa,vin from fy_records");
ResultSet resultSet = pst.executeQuery();
while (resultSet.next()) {
String isa = resultSet.getString("isa");
String vin = resultSet.getString("vin");
cache.put(isa, vin);
}
pst.close();
connection.close();
}
}
相同key在mysql新增数据前后测试结果:
如果维表的数据比较大,无法一次性全部加载到内存中,可以使用LRU策略加载维表数据。因为热点数据会经常被使用,会常驻到我们的缓存中,这种方式会有一定的数据延迟,并且需要额外设置每条数据的失效时间
整体思路就是利用flink的RichAsyncFunction读取hbase的数据到缓存中,在关联查询时先去查缓存,如果缓存不存在,利用客户端查找hbase,然后插入到缓存中。
核心代码如下:
org.hbase
asynchbase
1.8.2
private Logger logger = LoggerFactory.getLogger(LRU.class);
String table = "";
Cache cache = null;
private HBaseClient client = null;
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
//创建hbase客户端
final HBaseClient hBaseClient = new HBaseClient("192.168.8.174", "7071");
cache = CacheBuilder.newBuilder()
//最多存储10000条
.maximumSize(10000)
//过期时间1分钟
.expireAfterWrite(60, TimeUnit.SECONDS)
.build();
}
@Override
public void asyncInvoke(String input, ResultFuture resultFuture) {
JSONObject jsonObject = JSONObject.parseObject(input);
Integer cityId = jsonObject.getInteger("city_id");
String userName = jsonObject.getString("user_name");
String items = jsonObject.getString("items");
//读缓存
String cacheCityName = cache.getIfPresent(cityId);
//如果缓存获取失败再从hbase获取维度数据
if(cacheCityName != null){
Order order = new Order();
order.setCityId(cityId);
order.setItems(items);
order.setUserName(userName);
order.setCityName(cacheCityName);
final ArrayList orders = new ArrayList<>();
orders.add(order);
resultFuture.complete((scala.collection.Iterable) orders.iterator());
}else {
client.get(new GetRequest(table,String.valueOf(cityId))).addCallback((Callback>) ar -> {
for (KeyValue kv : ar) {
String value = new String(kv.value());
Order order = new Order();
order.setCityId(cityId);
order.setItems(items);
order.setUserName(userName);
order.setCityName(value);
final ArrayList orders = new ArrayList<>();
orders.add(order);
resultFuture.complete((scala.collection.Iterable) orders.iterator());
cache.put(String.valueOf(cityId), value);
}
return null;
});
}
}
广播变量会讲数据在每个节点都保存一份。
public static void main(String[] args) throws Exception {
final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
final DataSource source = env.fromElements(1, 2, 3);
//准备广播变量 ,广播变量数据会在每个节点保存一份
final ArrayList list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
final DataSource collection = env.fromCollection(list);
source.map(new RichMapFunction() {
List collectionNasme = null;
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
//获取广播变量
collectionNasme = getRuntimeContext().getBroadcastVariable("collectionNasme");
}
@Override
public String map(Integer integer) throws Exception {
final String s = collectionNasme.get(integer);
return s;
}
//注册广播变量
}).withBroadcastSet(collection,"collectionNasme").printToErr();
}