项目介绍在线视频: https://www.bilibili.com/video/BV1zv41157yY
本案例是一个专注于flink动态规则计算的项目,核心技术组件涉及flink、hbase、clickhouse、drools等
项目可根据各类个性化需求进行二次开发后,直接用于实时运营,实时风控、交通监控等场景的线上生产
列位看官,为了能够更好地理解后续《动态规则版实时运营系统》的设计思想和代码实现,
我们先来开发一个简化版且没有动态规则功能的实时运营系统;
存储上线以来的所有用户明细(显然不合适,但是先从简单做起)
代码片段;详细完整代码请参见项目工程
/**
* @author 涛哥
* @nick_name "deep as the sea"
* @contact qq:657270652 wx:doit_edu
* @site www.doitedu.cn
* @date 2021-03-28
* @desc 用户行为次数类条件查询服务实现:在flink的state中统计行为次数
*/
public class UserActionCountQueryServiceStateImpl implements UserActionCountQueryService {
/**
* 查询规则参数对象中,要求的用户行为次数类条件是否满足
* 同时,将查询到的真实次数,set回 规则参数对象中
*
* @param eventState 用户事件明细存储state
* @param ruleParam 规则整体参数对象
* @return 条件是否满足
*/
public boolean queryActionCounts(ListState<LogBean> eventState, RuleParam ruleParam) throws Exception {
// 取出各个用户行为次数原子条件
List<RuleAtomicParam> userActionCountParams = ruleParam.getUserActionCountParams();
// 取出历史明细数据
Iterable<LogBean> logBeansIterable = eventState.get();
// 统计每一个原子条件所发生的真实次数,就在原子条件参数对象中:realCnts
queryActionCountsHelper(logBeansIterable, userActionCountParams);
// 经过上面的方法执行后,每一个原子条件中,都拥有了一个真实发生次数,我们在此判断是否每个原子条件都满足
for (RuleAtomicParam userActionCountParam : userActionCountParams) {
if (userActionCountParam.getRealCnts() < userActionCountParam.getCnts()) {
return false;
}
}
// 如果到达这一句话,说明上面的判断中,每个原子条件都满足,则返回整体结果true
return true;
}
/**
* 序列匹配,性能改进版
*
* @param events
* @param userActionSequenceParams
* @return
*/
public int queryActionSequenceHelper2(Iterable<LogBean> events, List<RuleAtomicParam> userActionSequenceParams) {
int maxStep = 0;
for (LogBean event : events) {
if (RuleCalcUtil.eventBeanMatchEventParam(event, userActionSequenceParams.get(maxStep))) {
maxStep++;
}
if (maxStep == userActionSequenceParams.size()) break;
}
System.out.println("步骤匹配计算完成: 查询到的最大步骤号为: " + maxStep + ",条件中的步骤数为:" + userActionSequenceParams.size());
return maxStep;
}
/**
* @author 涛哥
* @nick_name "deep as the sea"
* @contact qq:657270652 wx:doit_edu
* @site www.doitedu.cn
* @date 2021-03-28
* @desc 用户画像查询服务,hbase查询实现类
*/
public class UserProfileQueryServiceHbaseImpl implements UserProfileQueryService {
Connection conn;
Table table;
/**
* 构造函数
*/
public UserProfileQueryServiceHbaseImpl() throws IOException {
Configuration conf = new Configuration();
conf.set("hbase.zookeeper.quorum", "hdp01:2181,hdp02:2181,hdp03:2181");
System.out.println("准备创建hbase连接........");
conn = ConnectionFactory.createConnection(conf);
table = conn.getTable(TableName.valueOf("yinew_profile"));
System.out.println("创建hbase连接完毕.........");
}
/**
* 传入一个用户号,以及要查询的条件
* 返回这些条件是否满足
* TODO 本查询只返回了成立与否,而查询到的画像数据值并没有返回
* TODO 可能为将来的缓存模块带来不便,有待改造
*
* @param deviceId
* @param ruleParam
* @return
*/
@Override
public boolean judgeProfileCondition(String deviceId, RuleParam ruleParam){
// 从规则参数中取出画像标签属性条件
HashMap<String, String> userProfileParams = ruleParam.getUserProfileParams();
// 取出条件中所要求的所有待查询标签名
Set<String> tagNames = userProfileParams.keySet();
// 构造一个hbase的查询参数对象
Get get = new Get(deviceId.getBytes());
// 把要查询的标签(hbase表中的列)逐一添加到get参数中
for (String tagName : tagNames) {
get.addColumn("f".getBytes(),tagName.getBytes());
}
// 调用hbase的api执行查询
try {
Result result = table.get(get);
// 判断结果和条件中的要求是否一致
for (String tagName : tagNames) {
// 从查询结果中取出该标签的值
byte[] valueBytes = result.getValue("f".getBytes(), tagName.getBytes());
// 判断查询到的value和条件中要求的value是否一致,如果不一致,方法直接返回:false
if(!(valueBytes!=null && new String(valueBytes).equals(userProfileParams.get(tagName)))){
System.out.println("查询了hbase,只是不匹配,真实值:" + new String(valueBytes) + "条件值:" + userProfileParams.get(tagName));
return false;
}
}
// 如果上面的for循环走完了,那说明每个标签的查询值都等于条件中要求的值,则可以返回true
return true;
} catch (IOException e) {
e.printStackTrace();
}
// 如果到了这,说明前面的查询出异常了,返回false即可
return false;
}
}
/***
* @author 涛哥
* @nick_name "deep as the sea"
* @contack qq:657270652 wx:doit_edu
* @site www.51doit.cn
* @date 2021/3/29
* @desc hbase查询性能简单测试代码
**/
public class HbaseGetTest {
public static void main(String[] args) throws IOException {
Configuration conf = new Configuration();
conf.set("hbase.zookeeper.quorum", "hdp01:2181,hdp02:2181,hdp03:2181");
Connection conn = ConnectionFactory.createConnection(conf);
Table table = conn.getTable(TableName.valueOf("yinew_profile"));
long s = System.currentTimeMillis();
for(int i=0;i<1000;i++){
Get get = new Get(StringUtils.leftPad(RandomUtils.nextInt(1, 900000) + "", 6, "0").getBytes());
int i1 = RandomUtils.nextInt(1, 100);
int i2 = RandomUtils.nextInt(1, 100);
int i3 = RandomUtils.nextInt(1, 100);
get.addColumn("f".getBytes(), Bytes.toBytes("tag"+i1));
get.addColumn("f".getBytes(), Bytes.toBytes("tag"+i2));
get.addColumn("f".getBytes(), Bytes.toBytes("tag"+i3));
Result result = table.get(get);
byte[] v1 = result.getValue("f".getBytes(), Bytes.toBytes("tag" + i1));
byte[] v2 = result.getValue("f".getBytes(), Bytes.toBytes("tag" + i2));
byte[] v3 = result.getValue("f".getBytes(), Bytes.toBytes("tag" + i3));
}
long e = System.currentTimeMillis();
System.out.println(e-s);
conn.close();
}
}
代码片段;完整代码请参见项目工程;
package cn.doitedu.dynamic_rule.engine;
import cn.doitedu.dynamic_rule.functions.DeviceKeySelector;
import cn.doitedu.dynamic_rule.functions.Json2BeanMapFunction;
import cn.doitedu.dynamic_rule.functions.RuleProcessFunction;
import cn.doitedu.dynamic_rule.functions.SourceFunctions;
import cn.doitedu.dynamic_rule.pojo.LogBean;
import cn.doitedu.dynamic_rule.pojo.ResultBean;
import org.apache.avro.data.Json;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
/**
* @author 涛哥
* @nick_name "deep as the sea"
* @contact qq:657270652 wx:doit_edu
* @site www.doitedu.cn
* @date 2021-03-28
* @desc 静态规则引擎版本1主程序
*/
public class RuleEngineV1 {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(new Configuration());
// 添加一个消费kafka中用户实时行为事件数据的source
DataStreamSource<String> logStream = env.addSource(SourceFunctions.getKafkaEventSource());
// 将json格式的数据,转成 logbean格式的数据
SingleOutputStreamOperator<LogBean> beanStream = logStream.map(new Json2BeanMapFunction());
// 对数据按用户deviceid分key
KeyedStream<LogBean, String> keyed = beanStream.keyBy(new DeviceKeySelector());
// 开始核心计算处理
SingleOutputStreamOperator<ResultBean> resultStream = keyed.process(new RuleProcessFunction());
// 打印
resultStream.print();
env.execute();
}
}
如果规则中的条件需要查询的数据时间跨度很长,而state不适合存储太大量的明细数据,“V1.0版”的方案就变得不可行了
可以引入clickhouse,存储用户历史以来的行为明细数据,并利用clickhouse进行快速聚合计算
用clickhouse支撑“时间跨度长”的条件查询
用state支撑“时间跨度短”的条件查询