当前位置: 首页 > article >正文

lua脚本实战—— Redis并发原子性陷阱

需求分析

对于内容类网站,比如用户浏览题目的答案,需要先登录才能追溯,那么可以统计用户访问频率来限制数据的爬取。

可采用分级反爬虫策略,先告警、再采取强制措施:

  1. 如果每分钟超过 10 道题,给管理员发送警告
  2. 如果每分钟超过 20 道题,直接踢下线,进行封号操作

解决方案

统计访问频率 - 基于 Redis 统计(分布式)

分布式存储 Redis 的 string 结构支持 incr 累加操作,可以对每个用户分钟(或其他时间精度)级别的访问次数进行累加统计。

1)设计 Redis 键值对

要能区分出用户和时间窗,示例 key 为:user:access:{userId}:{timestamp_in_minutes}

  1. {userId} 是用户 ID。
  2. {timestamp_in_minutes}是当前的分钟级时间戳,即将当前时间戳转化为分钟,这样每分钟的访问都会被统计到一个 key 中。

每个 key 的value,就是该用户在这分钟内的访问次数。

2)Redis 操作逻辑

对 Redis 的操作包括设置 key、给 key 增加计数、给 key 设置过期时间。

如下代码:

//假设使用 jedis 客户端
// 使用 Redis 的 INCR 操作增加当前秒的访问次数
jedis.incr(redisKey);// 设置过期时间(TTL),例如只保存60秒的数据
jedis.expire(redisKey, 60);  // 60秒后自动过期

然而,这种方法存在潜在的设计陷阱:incr 和 expire 是两个独立的操作。如果你在高并发情况下调用 incr() 之后发生上下文切换(比如另一个线程执行操作),可能会导致两个问题:

  1. 过期时间重置:如果在高并发场景下,多次调用 incr() 后又多次调用 expire(),可能会不断重置该 key 的过期时间,导致这个key 永远不会过期。(当前场景不会,但是连续性访问的场景就有可能发生)
  2. 非原子操作:由于 incr() 和 expire()是独立操作,在并发情况下,两个线程都可能先判断 key 不存在,然后各自执行 set 操作,导致计数逻辑出错。

所以我们要确保计数和过期时间的操作是原子性的,可以使用 Redis 的 Lua 脚本来完成。如果 key 不存在,则初始化并设置过期时间,否则只进行计数。

高并发场景的例子

假设你限制 1 分钟访问次数最多为 100 次,有很多用户在第 59 秒和第 60 秒猛戳接口。

这时你的 key user:count 已经存在,比如它只剩 1 秒 TTL:

TTL(user:count) = 1

这时候来了个请求,它调用:

jedis.incr("user:count");   // 现在 value = 88
jedis.expire("user:count", 60);  //  TTL 又被设置回了 60 秒!!!

现在这个 key 原本应该在 1 秒后过期,但被重新设置为 60 秒了。

正确做法如下:

String luaScript = "if redis.call('exists', KEYS[1]) == 1 then " +"  return redis.call('incr', KEYS[1]); " +"else " +"  redis.call('set', KEYS[1], 1); " +"  redis.call('expire', KEYS[1], 180); " +  // 设置 180 秒过期时间"  return 1; " +"end";

这里一定要给 Redis key 设置过期时间!因为统计超过一分钟,之前的数据就没什么用了。

为什么不用Sentinel或HotKey?

对于该项目,主要的目的是反爬虫,而不是应对高并发大流量的请求,所以不需要结合 Sentinel 或 Hotkey 去精确统计流量。自主实现固定时间窗口(1 分钟)的访问频率统计就足够了。为了便于项目扩展为分布式,使用 Redis 方案来实现。

http://www.lryc.cn/news/2386458.html

相关文章:

  • 【MySQL】第10节|MySQL全局优化与Mysql 8.0新增特性详解
  • CSS相关知识
  • AI扫描王APP:高效便捷的手机扫描工具,让生活更智能
  • 《仿盒马》app开发技术分享-- 原生地图展示(端云一体)
  • Linux 操作文本文件列数据的常用命令
  • IP、子网掩码、默认网关、DNS
  • 华为OD机试真题——字符串加密 (2025B卷:100分)Java/python/JavaScript/C/C++/GO最佳实现
  • 角度回归——八参数检测四边形Gliding Vertex
  • JVM 高质量面试题
  • AI助力,制作视频裁剪软件
  • SQL注入基础
  • 使用 A2A Python SDK 实现 CurrencyAgent
  • qt浏览文件支持惯性
  • Python类的力量:第六篇:设计模式——Python面向对象编程的“架构蓝图”
  • [实战]用户系统-2-完善登录和校验以及VIP
  • 负载均衡笔记
  • 印度语言指令驱动的无人机导航!UAV-VLN:端到端视觉语言导航助力无人机自主飞行
  • mysql都有哪些锁?
  • 解锁未来AI:使用DACA模式和Agentic技术提高开发效率
  • HarmonyOS NEXT 使用 relationalStore 实现数据库操作
  • R语言学习--Day04--数据分析技巧
  • SRS流媒体服务器之RTC播放环境搭建
  • Android 性能优化入门(三)—— ANR 问题分析
  • 用HTML5实现实时ASCII艺术摄像头
  • 鸿蒙Flutter实战:22-混合开发详解-2-Har包模式引入
  • 游戏引擎学习第302天:使用精灵边界进行排序
  • SpringBoot+MyBatis
  • wireshark: Display Filter Reference
  • Java基础 Day19
  • VMware+Windows 11 跳过安装阶段微软账号登录