Flink实时维表查询优化-旁路缓存

一、背景说明:

在目前实时数仓中,由于维表具有主键唯一性的特点,Hbase/Redis通常作为维表存放选择

  • Hbase:数据存于磁盘具有持久性但是查询效率慢。
  • Redis:数据存于内存查询效率高,但维表多数据量大时候占用资源多。

基于旁路缓存思想,对维表存储的优化的思路为:维表数据存储在Hbase,使用Redis作为缓存,但查询维表时有限查询Redis,如果没有该维表则去Hbase查询后并将维表数据放入Redis,并按一定时间保存,超过时间Redis自动清理(可使不常用维表无需常驻内存,缺点是首次查询较慢):
在这里插入图片描述

Phoenix支持SQL语法,使用Hbase可以通过Phoenix操作更简便。

二、代码实现

示例维表如下,使用直接查询及旁路缓存两种方式,对比用时。

0: jdbc:phoenix:hadoop102> select * from GMALL0820_REALTIME.DIM_BASE_PROVINCE;
+-----+-------+------------+------------+-----------+-------------+
| ID  | NAME  | REGION_ID  | AREA_CODE  | ISO_CODE  | ISO_3166_2  |
+-----+-------+------------+------------+-----------+-------------+
| 1   | 北京    | 1          | 110000     | CN-11     | CN-BJ       |

1.不使用旁路缓存,直接通过Phoenix查询hbase维表数据示例:

package com.test.cacheside;

import com.alibaba.fastjson.JSONObject;
import org.apache.flink.api.java.tuple.Tuple2;
import java.util.List;

public class DimUtilWithNoCache {
    //从phoenix查询数据  入参使用动态参数,可以实现多条件查询
    public static JSONObject getDimInfoNoCache(String tableName, Tuple2<String,String>... cloNameAndValue){

        //拼接SQL语句
        String wheresql = " where ";
        for (int i = 0; i < cloNameAndValue.length; i++) {
            Tuple2<String,String> tuple = cloNameAndValue[i];
            if (i>0){
                wheresql+="and";    //多条件查询拼接and条件
            }
            wheresql += tuple.f0 + "='" + tuple.f1 + "'";
        }
        String sql = "select * from " + tableName + wheresql;
        System.out.println("查询SQL为:" + sql);
        JSONObject dimJsonObj = null;
        List<JSONObject> dimList = PhoenixUtil.queryList(sql, JSONObject.class);

        if (dimList!=null || dimList.size()>0){
            dimJsonObj = dimList.get(0);
        }else{
            System.out.println("维度数据没有找到:" + sql);
        }
        return dimJsonObj;
    }
    public static void main(String[] args) {
        long startTime=System.currentTimeMillis();   //获取开始时间
        System.out.println(getDimInfoNoCache("DIM_BASE_PROVINCE", Tuple2.of("id", "1")));
        long endTime=System.currentTimeMillis();   //获取结束时间
        System.out.println("程序运行时间: "+(endTime-startTime)+"ms");
    }
}

2.使用旁路缓存实现:

package com.test.cacheside;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.flink.api.java.tuple.Tuple2;
import redis.clients.jedis.Jedis;
import java.util.List;

public class DimUtilWithCache {
    //方法重载的思路,减少调用时候的输入
    public static JSONObject getDimInfo(String tableName, String id){
        return getDimInfo(tableName, Tuple2.of("id",id));
    }
    /*
    优化,从phoenix查询数据,加入旁路缓存(Redis),先从缓存查询,如果缓存没有查到数据,再到Phoenix查询,并将查询结果放到缓存
    redis
        类型      string
        Key      dim:表名:值      例如:dim:DIM_BASE_PROVINCE:10_xxx
        value    通过PhoenixUtil到维度表中查询数据,取出第一条并将其转换为json字符串
        失效时间: 24*3600
    */
    //入参使用Tuple及动态参数,可以实现多条件查询
    public static JSONObject getDimInfo(String tableName, Tuple2<String,String>... cloNameAndValue){

        String wheresql = " where ";
        String redisKey = "dim:" + tableName.toLowerCase() + ":";
        for (int i = 0; i < cloNameAndValue.length; i++) {
            Tuple2<String,String> tuple = cloNameAndValue[i];
            if (i > 0){
                wheresql += "and";
                redisKey += "_";
            }
            wheresql += tuple.f0 + "='" + tuple.f1 + "'";
            redisKey += tuple.f1;
        }
        //从Redis中获取数据
        Jedis jedis = null;
        String dimJsonStr = null;
        JSONObject dimJsonObj = null;

        try {
            //获取jedis客户端
            jedis = RedisUtil.getJedis();
            //根据key到redis中查询
            dimJsonStr = jedis.get(redisKey);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("从Redis中查询维度失败!");
        }
        //判断是否从Redis中查询到数据
        if (dimJsonStr != null && dimJsonStr.length()>0){
            dimJsonObj = JSON.parseObject(dimJsonStr);
        }else{
            //Redis没有查到数据,到phoenix中查询
            String sql = "select * from " + tableName + wheresql;
            System.out.println("查询SQL为:" + sql);

            List<JSONObject> dimList = PhoenixUtil.queryList(sql, JSONObject.class);

            if (dimList!=null || dimList.size()>0){
                dimJsonObj = dimList.get(0);
                //将查询出来的数据放到Redis中缓存起来
                if (jedis!=null){
                    jedis.setex(redisKey,3600*24,dimJsonObj.toJSONString());
                }
            }else{
                System.out.println("维度数据没有找到:" + sql);
            }
        }
        //关闭jedis
        if (jedis!=null){
            jedis.close();
        }
        return dimJsonObj;
    }
    public static void main(String[] args) {
        long startTime=System.currentTimeMillis();   //获取开始
        System.out.println(DimUtilWithCache.getDimInfo("DIM_BASE_PROVINCE","1"));
        long endTime=System.currentTimeMillis();   //获取结束时间
        System.out.println("程序运行时间: "+(endTime-startTime)+"ms");
    }
}

查询Redis,可以看到结果如下:

127.0.0.1:6379> keys *
1) "dim:dim_base_province:1"
127.0.0.1:6379> get dim:dim_base_province:1
"{\"REGION_ID\":\"1\",\"ISO_CODE\":\"CN-11\",\"ISO_3166_2\":\"CN-BJ\",\"ID\":\"1\",\"AREA_CODE\":\"110000\",\"NAME\":\"\xe5\x8c\x97\xe4\xba\xac\"}"

ps:首次运行旁路缓存的查询时间较慢,后续查询这是基于Redis查询。

三:其他工具类代码及依赖补充说明:

1.PhoenixUtil(从Phoenix查询数据工具类)

package com.test.cacheside;

import org.apache.commons.beanutils.BeanUtils;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class PhoenixUtil {

    private static Connection conn = null;
    public static void init(){
        try {
            //注册驱动
            Class.forName("org.apache.phoenix.jdbc.PhoenixDriver");
            //获取Phoenix的连接
            conn = DriverManager.getConnection("jdbc:phoenix:hadoop102:2181");
            //指定操作的表空间
            conn.setSchema("GMALL0820_REALTIME");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //从Phoenix查询数据
    public static <T> List<T> queryList(String sql, Class<T> clazz){

        List<T> resultList = new ArrayList<T>();
        if (conn==null){
            init();
        }
        //获取数据库操作对象
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            ps = conn.prepareStatement(sql);
            //执行SQL语句
            rs = ps.executeQuery();
            //处理结果集
            //查询元数据信息
            ResultSetMetaData metaData = rs.getMetaData();

            //判断结果集中是否存在数据,如果有,那么进行一次循环
            while (rs.next()) {
                //创建一个对象,用于封装查询出来的一条结果集中的数据
                T obj = clazz.newInstance(); //不知道字段类型,使用反射获取
                //对查询的所有列进行遍历,获取每一列的名称
                for (int i = 1; i <= metaData.getColumnCount(); i++) {
                    String columnName = metaData.getColumnName(i);
                    BeanUtils.setProperty(obj,columnName,rs.getObject(i));
                }
                //将当前结果中的一行数据封装的obj对象放到list集合中
                resultList.add(obj);
            }
        }catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("从维度表取数据错误!");
        }
        finally {
            if (rs!=null){
                try {
                    rs.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }
            }
            if (ps!=null){
                try {
                    ps.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }
            }
        }
        return resultList;
    }
}

2.RedisUtil(通过jdispool连接Redis)

package com.test.cacheside;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class RedisUtil {
    private static JedisPool jedisPool;

    public static Jedis getJedis(){
        if (jedisPool == null){
            JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();

            jedisPoolConfig.setMaxTotal(100); //最大可用连接数
            jedisPoolConfig.setBlockWhenExhausted(true); //连接耗尽是否等待
            jedisPoolConfig.setMaxWaitMillis(2000); //等待时间
            jedisPoolConfig.setMaxIdle(5); //最大闲置连接数
            jedisPoolConfig.setMinIdle(5); //最小闲置连接数
            jedisPoolConfig.setTestOnBorrow(true); //取连接的时候进行一下测试 ping pong

            jedisPool = new JedisPool(jedisPoolConfig,"hadoop102",6379,10000);
            System.out.println("开辟连接池");
            return jedisPool.getResource();
        }else {
            System.out.println(" 连接池:" + jedisPool.getNumActive());
            return jedisPool.getResource();
        }
    }
}

3.依赖

       <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-java</artifactId>
            <version>1.12.0</version>
        </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.68</version>
    </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.3.0</version>
        </dependency>
        <dependency>
            <groupId>commons-beanutils</groupId>
            <artifactId>commons-beanutils</artifactId>
            <version>1.9.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.phoenix</groupId>
            <artifactId>phoenix-spark</artifactId>
            <version>4.14.3-HBase-1.4</version>
            <exclusions>
                <exclusion>
                    <groupId>org.glassfish</groupId>
                    <artifactId>javax.el</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

学习交流,有任何问题还请随时评论指出交流。


版权声明:本文为Rango_lhl原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。