一、背景说明:
在目前实时数仓中,由于维表具有主键唯一性的特点,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>
学习交流,有任何问题还请随时评论指出交流。