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 方案来实现。

相关推荐
金銀銅鐵1 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
福建佰胜张工1 小时前
SMC 薄型气爪 MHF2-16D1 深度解析:平行开闭、低剖面结构在自动化装配中的选型与落地
java·服务器·安全·自动化
catoop1 小时前
API Key 设计与安全生成方案:结构拆解、数据库设计与 Java 实现
java·数据库·安全
system_error_x2 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
驭渊的小故事2 小时前
Spring AI Alibaba 实战:从maven依赖治理到声明式工具调用
java·人工智能·spring
Elastic 中国社区官方博客3 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn1883 小时前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn4 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
y1su4 小时前
Leetcode 字符串模板
java·数据结构·算法·leetcode·职场和发展·动态规划
我命由我123455 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法