目前风险防控、营销等领域很多场景涉及较多规则的应用,底层技术实现大多依赖决策引擎。决策引擎提供专家规则的可视化维护功能和决策服务,很多专家规则依赖实时累计特征,例如(客户维度当天交易笔数,一小时内汇总交易金额等),实时累计特征一般通过流式计算、内存计算等方式实现。本文介绍基于redis 通过lua脚本实现实时统计,包含简单统计和基于滑动窗口实现的统计功能的实现。
举例:客户维度1小时转账的次数,
举例:客户维度最近30分钟转账的次数,
举例:设备维度最近1小时登录的账户数量,1个账户登录多次,重复计算账户数量。
数据结构:key-value
实现逻辑:1)查询:当key存在时,返回key对应的value值,key不存在时返回0.
2)更新:当key存在时,调用inrc命令 值自增1.当key不存在时设置值为1.
数据结构:每个时间片值存放在hash,key为时间片名称,value为次数。 时间片冗余存储在sorted set。
实现逻辑:1)查询:开始时间片为传入参数,结束时间默认为当前时间,根据 开始时间片和当前时间片过滤sorted set,获取列表后,遍历hash获取各时间片数量,做累加获取结果。
2)更新:当Hash key和当前时间片key存在时,获取当前时间片对应次数,增加一后再写入hash,同时更新sorted set key。当Hash key和当前时间片key存在时,hash写入key为当前时间片,value值1,同时更新sorted set key。
3)数据清理:时间片保留最大数量有特征配置时确定,当时间片超过最大值时,将最旧的数据删除。
数据结构:sorted set。 key为关联字段名称,score为当前时间。
实现逻辑:1)查询:开始时间片为传入参数,结束时间默认为当前时间,根据 开始时间片和当前时间片过滤sorted set,返回过滤结果的统计数量。
2)更新:当Item存在时,更新Item的score为当前时间。Item不存在时,增加Item,score为当前时间。
举例: 统计设备维度1小时登录的账户数量。 key为设备ID,sorted set key:账户ID,score为当前时间。
3)数据清理:特征配置时确定Item最大数量,当Item数量超过最大值时,删除score最小的项【最旧的项】
pom文件引入redis依赖
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>2.8.0version>
dependency>
REDIS 命令参考:https://www.redis.net.cn/order/3528.html
LUA语法参考:https://www.runoob.com/lua/lua-tables.html
GUI for redis :搜索RESP.app 下载。
---- 查询 非滑动窗口统计次数
local function querySimpleCount(cacheKey)
if (redis.call('exists', cacheKey) == 1) then
return redis.call('get', cacheKey);
else
return 0;
end;
end;
---- 更新 非滑动窗口统计次数
local function updateSimpleCount(cacheKey, expireTTL)
if (redis.call('exists', cacheKey) == 1) then
redis.call('incr', cacheKey);
else
redis.call('set',cacheKey,1);
redis.call('EXPIRE',cacheKey,tonumber(expireTTL));
end
return querySimpleCount(cacheKey);
end;
---- 查询 滑动窗口统计次数
local function querySlideWindowCount(cacheKey,beginTimeSlice)
local cacheSliceZSettKey=cacheKey..'ZSet'
local filterItmeTable=redis.call('ZRANGEBYLEX',cacheSliceZSettKey,'['..beginTimeSlice,'[ZZZ');
local totalResult=0;
for k,v in ipairs(filterItmeTable) do
totalResult=totalResult+tonumber(redis.call('HGET',cacheKey,v))
end;
return totalResult;
end;
---- 更新 滑动窗口统计次数
local function updateSlideWindowCount(cacheKey, beginTimeSlice,timeSliceName,timeSliceCnt,expireTTL)
local cacheSliceZSettKey=cacheKey..'ZSet'
if (redis.call('HEXISTS', cacheKey,timeSliceName) == 1) then
redis.call('HINCRBY', cacheKey,timeSliceName,1);
redis.call('ZADD', cacheSliceZSettKey,100,timeSliceName);
else
redis.call('hset',cacheKey,timeSliceName,1);
redis.call('EXPIRE',cacheKey,tonumber(expireTTL));
redis.call('ZADD', cacheSliceZSettKey,100,timeSliceName);
redis.call('EXPIRE',cacheSliceZSettKey,tonumber(expireTTL));
end
if redis.call('ZCARD',cacheSliceZSettKey)>tonumber(timeSliceCnt) then
local tempTimeSliceNameTable=redis.call('ZRANGE',cacheSliceZSettKey,0,0);
if #tempTimeSliceNameTable>0 then
redis.call('HDEL',cacheKey,tempTimeSliceNameTable[1]);
redis.call('ZREM',cacheSliceZSettKey,tempTimeSliceNameTable[1]);
end
end
return querySlideWindowCount(cacheKey,beginTimeSlice);
end;
----KEY1 键值,arg1[1:查询,2:更新 ]; arg2[1:简单统计次数,2:滑动窗口统计];arg3[beginTimeSlice 开始时间片]
----arg4[timeSliceName 当前时间片],arg5[timeSliceCnt 时间片保留数量],