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 设置过期时间。

如下代码:

java 复制代码
//假设使用 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:

java 复制代码
TTL(user:count) = 1

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

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

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

正确做法如下:

java 复制代码
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 方案来实现。

相关推荐
白露与泡影6 小时前
2026秋招冲刺:1000道Java高频面试题(各大厂考点汇总)
java·开发语言·面试
IT龟苓膏6 小时前
Java 并发基础:进程、线程、线程状态、synchronized、volatile 一篇讲清
java·开发语言·jvm
weixin_446729166 小时前
java中class类没有打进war包中
java
哭哭啼6 小时前
pgSql 事务篇
java·数据库·postgresql
架构源启7 小时前
Spring AI进阶系列(17)- 未来展望与职业发展:Java 工程师迈向 AI 工程化与智能体架构的路线图
java·人工智能·spring
我登哥MVP7 小时前
Spring Boot 从“会用”到“精通”:SpringBoot MVC 请求处理全流程
java·spring boot·后端·spring·mvc·maven·intellij-idea
我登哥MVP7 小时前
Spring Boot 从“会用”到“精通”:ReturnValueHandler原理
java·spring boot·后端·spring·java-ee·maven·intellij-idea
snow@li7 小时前
数据库:MySQL vs PostgreSQL 详尽对比(2026版)
java·mysql·postgresql
丑过三八线7 小时前
Runc 深度解析:从原理到实操
java·linux·开发语言·docker·容器·rpc
STDD7 小时前
ntfy 自托管推送通知服务搭建:一条 curl 命令向手机发送通知
java·开发语言·智能手机