
在 QQ 安全风控、用户行为分析、登录异常告警等场景中,“快速识别五分钟内重复登录两次的 QQ 号” 是核心需求 —— 既要处理亿级 QQ 号的高频登录请求(如高峰时段登录 QPS 达 10 万 +),又要保证判断延迟低于 10ms,还要避免无效数据占用过多内存。
本文将从 “业务需求拆解→数据结构对比→方案落地→优化扩展” 四个维度,讲清如何选对数据结构、设计高效方案,解决这一高频问题。
在选数据结构前,必须先明确 “判断逻辑” 和 “性能约束”,避免方案偏离实际场景:
对单个 QQ 号,需满足:
当前登录时间 - 最近一次登录时间 ≤ 300秒(5分钟)
→ 满足则判定为 “五分钟内重复登录两次”,需触发后续动作(如安全校验、日志记录)。
要满足 “快速查询(找 QQ 号的历史登录时间)+ 快速清理(删过期记录)+ 低内存”,需对比常见数据结构的适配性:
数据结构 | 查找效率 | 插入效率 | 过期清理效率 | 内存占用 | 适配性结论 |
|---|---|---|---|---|---|
数组 | O(n) | O(1) | O(n) | 低 | 差(查找需遍历) |
单向链表 | O(n) | O(1) | O(n) | 低 | 差(查找需遍历) |
普通哈希表 | O(1) | O(1) | O(n) | 中 | 一般(清理需遍历所有记录) |
哈希表 + 双端队列 | O(1) | O(1) | O (k)(k 为过期条数) | 低 | 优(兼顾查询、插入、清理) |
Redis Sorted Set | O(log n) | O(log n) | O(log n) | 中 | 优(适合分布式场景) |
适用于 “登录服务部署在单机 / 单集群,无需跨节点共享数据” 的场景(如小型应用、非分布式登录系统),用 Java 代码示例演示核心逻辑:
[QQ登录请求] → [拦截器/过滤器] → [重复登录判断模块] → [登录业务逻辑] ↓ [哈希表+双端队列](内存存储) ↓ [过期数据清理](登录时触发)import java.util.Deque;import java.util.HashMap;import java.util.LinkedList;import java.util.Map;/** * 五分钟内重复登录QQ号检测器(本地内存版) */public class QQDuplicateLoginDetector { // 核心存储:key=QQ号(字符串,避免长数字溢出),value=登录时间戳队列(毫秒级) private final Map<String, Deque<Long>> loginRecordMap = new HashMap<>(); // 时间窗口:5分钟=300000毫秒 private static final long TIME_WINDOW = 5 * 60 * 1000; // 并发安全锁:避免多线程操作同一QQ号的队列导致异常 private final Object lock = new Object(); /** * 判断当前登录是否为“五分钟内重复登录” * @param qqNumber QQ号 * @return true=重复登录,false=非重复 */ public boolean isDuplicateLogin(String qqNumber) { // 1. 获取当前时间戳(毫秒) long currentTime = System.currentTimeMillis(); // 2. 并发安全:同一QQ号的操作加锁(避免多线程同时修改队列) synchronized (lock) { // 3. 获取该QQ号的登录记录队列,无则创建新队列 Deque<Long> loginTimes = loginRecordMap.computeIfAbsent(qqNumber, k -> new LinkedList<>()); // 4. 清理队列中“超过5分钟的过期记录”(关键:只保留有效数据) while (!loginTimes.isEmpty()) { long earliestTime = loginTimes.peekFirst(); // 队头是最早的登录时间 if (currentTime - earliestTime > TIME_WINDOW) { loginTimes.pollFirst(); // 过期则删除 } else { break; // 队头未过期,后续记录更不会过期(队列按时间排序) } } // 5. 判断是否重复登录:清理后队列非空(说明有5分钟内的登录记录) boolean isDuplicate = !loginTimes.isEmpty(); // 6. 将当前登录时间加入队列(队尾追加) loginTimes.offerLast(currentTime); // 7. 优化:若队列长度超过2,删除最早的记录(仅需保留最近2条即可判断重复) if (loginTimes.size() > 2) { loginTimes.pollFirst(); } // 8. 优化:若队列空,从哈希表中删除(释放内存,避免无效key占用空间) if (loginTimes.isEmpty()) { loginRecordMap.remove(qqNumber); } return isDuplicate; } } // 测试示例 public static void main(String[] args) throws InterruptedException { QQDuplicateLoginDetector detector = new QQDuplicateLoginDetector(); String qq = "123456789"; // 第一次登录:非重复 System.out.println(detector.isDuplicateLogin(qq)); // false // 2分钟后第二次登录:重复 Thread.sleep(2 * 60 * 1000); System.out.println(detector.isDuplicateLogin(qq)); // true // 6分钟后第三次登录:非重复(前两次记录已过期) Thread.sleep(6 * 60 * 1000); System.out.println(detector.isDuplicateLogin(qq)); // false }}当登录服务部署在多节点(如分布式微服务),本地内存方案无法共享登录记录(节点 A 的登录记录,节点 B 无法获取),此时需用Redis实现分布式存储,推荐用Sorted Set(有序集合) 作为核心数据结构。
[QQ登录请求] → [API网关] → [分布式登录服务集群] → [Redis集群] ↓ ↓ [重复登录判断] [Sorted Set存储登录记录] ↓ [安全告警/业务处理]import redis.clients.jedis.Jedis;import redis.clients.jedis.JedisPool;import redis.clients.jedis.params.ZAddParams;import java.util.Set;/** * 五分钟内重复登录QQ号检测器(分布式Redis版) */public class RedisQQDuplicateLoginDetector { private final JedisPool jedisPool; // 时间窗口:5分钟=300000毫秒 private static final long TIME_WINDOW = 5 * 60 * 1000; // Redis key前缀:避免与其他业务key冲突 private static final String KEY_PREFIX = "qq:login:record:"; // Redis key过期时间:6分钟(比时间窗口多1分钟,确保过期记录被清理) private static final int KEY_EXPIRE_SECONDS = 6 * 60; public RedisQQDuplicateLoginDetector(JedisPool jedisPool) { this.jedisPool = jedisPool; } public boolean isDuplicateLogin(String qqNumber) { long currentTime = System.currentTimeMillis(); String redisKey = KEY_PREFIX + qqNumber; try (Jedis jedis = jedisPool.getResource()) { // 1. 清理过期记录:删除score(时间戳)< 当前时间-TIME_WINDOW的记录 jedis.zremrangeByScore(redisKey, 0, currentTime - TIME_WINDOW); // 2. 判断是否重复登录:记录数≥1说明有5分钟内的登录 long recordCount = jedis.zcard(redisKey); boolean isDuplicate = recordCount > 0; // 3. 插入当前登录记录:score=currentTime,member=currentTime(避免重复member) // ZAddParams.xx():仅当key存在时才插入(可选,避免无效插入) jedis.zadd(redisKey, currentTime, String.valueOf(currentTime), ZAddParams.zAddParams().nx()); // 4. 优化:保留最近2条记录(删除最早的记录) if (recordCount >= 2) { // ZRANGE获取最早的记录(0-0是第一条),再删除 Set<String> earliestMembers = jedis.zrange(redisKey, 0, 0); if (!earliestMembers.isEmpty()) { jedis.zrem(redisKey, earliestMembers.iterator().next()); } } // 5. 设置key过期时间(确保6分钟后自动删除,释放内存) jedis.expire(redisKey, KEY_EXPIRE_SECONDS); return isDuplicate; } } // 测试示例(需提前启动Redis服务) public static void main(String[] args) throws InterruptedException { JedisPool jedisPool = new JedisPool("localhost", 6379); RedisQQDuplicateLoginDetector detector = new RedisQQDuplicateLoginDetector(jedisPool); String qq = "987654321"; // 第一次登录:非重复 System.out.println(detector.isDuplicateLogin(qq)); // false // 3分钟后第二次登录:重复 Thread.sleep(3 * 60 * 1000); System.out.println(detector.isDuplicateLogin(qq)); // true // 7分钟后第三次登录:非重复(key已过期) Thread.sleep(7 * 60 * 1000); System.out.println(detector.isDuplicateLogin(qq)); // false jedisPool.close(); }}场景 | 推荐方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
单机 / 单集群 | 哈希表 + 双端队列 | 延迟极低(内存操作,<1ms) | 无法跨节点共享 | 小型应用、非分布式登录系统 |
分布式微服务 | Redis Sorted Set | 跨节点共享、高可用 | 依赖 Redis,延迟略高(~5ms) | 大型应用、多节点登录服务 |
超大规模(亿级 QPS) | Redis Cluster + 本地缓存 | 兼顾分布式共享与低延迟 | 实现复杂,需同步本地与 Redis | 腾讯 QQ、微信等超大规模登录场景 |
基于上述方案,可轻松扩展更多风控需求:
“定位五分钟内重复登录的 QQ 号” 的核心是 “快速查找 + 高效清理”:
最终,数据结构的选择不是 “选最复杂的”,而是 “选最适配业务场景的”—— 简单的组合结构,往往能解决复杂的高并发问题。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。