基于Lua实现滑动窗口统计实现

该博客介绍了如何基于Lua脚本实现滑动窗口统计功能,包括简单统计、滑动窗口统计和关联统计。详细阐述了设计思路、数据结构和逻辑,并提供了环境搭建、lua脚本编写及java测试类的说明。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基于Lua脚本实现滑动窗口统计功能

 目前风险防控、营销等领域很多场景涉及较多规则的应用,底层技术实现大多依赖决策引擎。决策引擎提供专家规则的可视化维护功能和决策服务,很多专家规则依赖实时累计特征,例如(客户维度当天交易笔数,一小时内汇总交易金额等),实时累计特征一般通过流式计算、内存计算等方式实现。本文介绍基于redis 通过lua脚本实现实时统计,包含简单统计和基于滑动窗口实现的统计功能的实现。

总体实现思路

  1. 明确需求:1)实时累计特征配置后即可生效,交易过程实时累计,决策时支持实时查询。2)实时特征类型较多,本文局限介绍简单次数统计,滑动窗口次数统计和关联次数统计。
  2. 合理设计:1)明确各类特征使用的数据结构,数据清理方式。2)设计入参(查询,更新,简单统计还是滑动窗口统计,过期时间,开始时间片等)
  3. 工程研发:1)环境搭建 :redis ,GUI for redis,IntelliJ IDEA 2)搭建java工程 和测试类 3)编写lua脚本;
  4. 总结: 实时统计在流式计算外,可以考虑采用缓存计算实现,目前各类缓存软件支持lua脚本的高效执行。

明确需求

简单统计

举例:客户维度1小时转账的次数,

滑动窗口统计

举例:客户维度最近30分钟转账的次数,

关联统计

举例:设备维度最近1小时登录的账户数量,1个账户登录多次,重复计算账户数量。

合理设计

1、总体流程

在这里插入图片描述

2、脚本设计逻辑

简单次数统计

数据结构: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最小的项【最旧的项】

工程研发

环境搭建 :redis ,GUI for redis,IntelliJ IDEA

pom文件引入redis依赖

   <dependency>
     <groupId>redis.clients</groupId>
     <artifactId>jedis</artifactId>
     <version>2.8.0</version>
   </dependency>

REDIS 命令参考:https://www.redis.net.cn/order/3528.html
LUA语法参考:https://www.runoob.com/lua/lua-tables.html
GUI for redis :搜索RESP.app 下载。

次数统计lua脚本,包含简单统计和滑动窗口统计

---- 查询  非滑动窗口统计次数
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 时间片保留数量],arg6[expireTTL key保留周期]
   if (ARGV[2] == '1' ) then
       if (ARGV[1] == '1' ) then
           return querySimpleCount(KEYS[1]);
       elseif (ARGV[1] == '2' ) then
           return 
### 使用 Redis 实现滑动窗口算法的最佳实践 滑动窗口限流算法是一种高效的流量控制技术,能够在高并发场景下有效保护系统的稳定性。以下是基于 Redis 的滑动窗口算法实现方式及其最佳实践。 #### 1. 滑动窗口算法的核心概念 滑动窗口算法通过将时间划分为多个子区间(即“桶”),并对每个子区间的请求计数进行统计,从而实现了更精细的流量控制。相比固定窗口算法,滑动窗口能够更好地平滑流量突刺问题[^3]。 #### 2. 基于 Redis 的实现逻辑 Redis 提供了丰富的数据结构支持,其中有序集合(Sorted Set)非常适合用来存储滑动窗口的时间戳和对应的请求次数。具体实现思路如下: - **时间分片**:将整个时间窗口划分成若干个小的时间片段(例如每秒一个片段)。每个片段对应一个键值对。 - **过期处理**:利用 Redis 的 TTL 功能或主动清理超出时间范围的数据,确保内存占用可控。 - **原子操作**:使用 Lua 脚本保证增删查改的一致性和高效性。 #### 3. 示例代码 以下是一个简单的滑动窗口限流器的实现示例,采用 Python 结合 `redis-py` 客户端库完成。 ```python import time import redis def sliding_window_limiter(redis_client, key_prefix, window_size_seconds, max_requests, current_time=None): """ 滑动窗口限流器函数 :param redis_client: Redis 连接实例 :param key_prefix: 键名前缀 :param window_size_seconds: 时间窗口大小 (单位: 秒) :param max_requests: 最大允许请求数量 :param current_time: 当前时间戳,默认为 None 表示实时获取 :return: 是否允许当前请求通行 """ if not current_time: current_time = int(time.time()) # 构造 Redis Sorted Set 的 Key 名称 key_name = f"{key_prefix}:sliding_window" # 删除超过时间窗口范围的旧记录 pipeline = redis_client.pipeline() pipeline.zremrangebyscore(key_name, '-inf', current_time - window_size_seconds) pipeline.execute() # 获取当前窗口内的总请求数 total_requests = redis_client.zcard(key_name) if total_requests >= max_requests: return False # 请求被拒绝 # 将当前请求加入到 Sorted Set 中 redis_client.zadd(key_name, {current_time: current_time}) redis_client.expire(key_name, window_size_seconds + 1) # 设置过期时间 return True # 请求被接受 # 初始化 Redis 客户端 r = redis.StrictRedis(host='localhost', port=6379, db=0, decode_responses=True) # 测试调用 if __name__ == "__main__": allowed = sliding_window_limiter(r, "test", 10, 5) # 10 秒内最多允许 5 次请求 print(f"Request Allowed: {allowed}") ``` 上述代码展示了如何通过 Redis 的 ZSET 来维护滑动窗口的状态,并结合管道化批量执行命令提升效率。 #### 4. 部署与运维注意事项 在实际生产环境中部署该方案时需要注意以下几个方面: - **环境隔离**:确保开发、测试和生产的 Redis 实例相互独立,避免资源竞争[^2]。 - **配置管理**:合理设置 Redis 的最大连接数、持久化模式等参数,推荐使用 Sentinel 或 Cluster 方案提高可用性[^1]。 - **监控报警**:集成 Prometheus 和 Grafana 对 Redis 性能指标进行持续监测,及时发现潜在瓶颈。 - **备份恢复**:制定周期性的全量/增量备份计划,并验证灾备流程的有效性[^4]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值