【面试真题拆解】高并发场景下“恶意登录设备识别”系统设计

发布时间:2026/7/4 2:57:57

【面试真题拆解】高并发场景下“恶意登录设备识别”系统设计 这是面试遇到的一道场景题。面试官问我有这么一个场景现在有一个网站这个网站访问的人非常多在登录的时候有些坏人来这里捣乱怎么去设计一个系统实现把这些坏人找到的逻辑。条件说一下登录的时候你要的用户userid时间 设备deviceid这些信息都有坏人的定义是最近十分钟之内一个设备上登录了大于等于5个user我就认为这个是坏人。场景梳理输入信息用户登录时的userId用户ID、timestamp登录时间戳、deviceId设备ID坏人定义最近10分钟600秒内同一个deviceId上登录了≥5个不同的userId输出结果标记该deviceId为“恶意设备”后续可以做拦截、验证码、风控等处理。实现方案考虑用Redis ZSet实现滑动窗口。为什么选Redis主要是因为单线程高性能适合高并发场景内存数据库读写速度极快支持丰富的数据结构比如这次场景需要的ZSet支持过期时间自动清理数据为什么选ZSetZSet有序集合是 Redis 中最适合做 “时间窗口、排行榜、去重 排序” 的数据结构。它有以下几个特点Member成员唯一Member 不能重复天然去重每个 Member 对应一个 Score分数Score 是一个浮点数用来排序ZSet 会自动按 Score 从小到大进行排序如果Score 相同则按 Member 字典序排序。ZSet 的底层是哈希表 跳表哈希表保证 Member 唯一、O (1) 查找跳表保证有序、O (log n) 范围查询。数据模型维度说明Keylogin:device:{deviceId}比如login:device:abc123ValueZSet MemberuserId用户ID保证唯一性去重ScoreZSet Score登录时间戳用于滑动窗口理一下整个流程用户发起登录请求携带userId、deviceId、timestamp先查黑名单如果已经是恶意设备直接拦截写入登录记录到Redis ZSet以deviceId为KeyuserId为Membertimestamp为Score写入ZSet清理过期数据删除ZSet中Score 当前时间 - 10分钟的元素统计最近10分钟内的不同userId数量用ZCard命令统计ZSet的元素个数判断是否为坏人如果数量≥5标记该deviceId为恶意设备存入Redis黑名单返回登录结果如果是恶意设备返回“请完成验证码”或“登录失败”否则正常登录。简化版的代码实现用Spring Boot RedisTemplate写一个简化版的实现加深理解一下1先定义Redis Key的前缀和常量publicclassRedisKeyConstants{// 设备登录记录Key前缀publicstaticfinalStringLOGIN_DEVICE_KEY_PREFIXlogin:device:;// 恶意设备黑名单Key前缀publicstaticfinalStringMALICIOUS_DEVICE_KEY_PREFIXmalicious:device:;// 滑动窗口大小10分钟600秒publicstaticfinalintWINDOW_SIZE_SECONDS600;// 坏人阈值≥5个不同userIdpublicstaticfinalintMALICIOUS_THRESHOLD5;// 过期时间10分钟publicstaticfinalintEXPIRE_TIME_SECONDS600;}2服务实现importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.stereotype.Service;importjava.util.concurrent.TimeUnit;ServicepublicclassLoginRiskControlService{AutowiredprivateRedisTemplateString,StringredisTemplate;/** * 检查是否为恶意设备 * param userId 用户ID * param deviceId 设备ID * return true-是恶意设备false-正常设备 */publicbooleancheckMaliciousDevice(StringuserId,StringdeviceId){// 1. 构造Redis KeyStringloginKeyRedisKeyConstants.LOGIN_DEVICE_KEY_PREFIXdeviceId;StringmaliciousKeyRedisKeyConstants.MALICIOUS_DEVICE_KEY_PREFIXdeviceId;// 2. 先查黑名单如果已经是恶意设备直接返回trueif(Boolean.TRUE.equals(redisTemplate.hasKey(maliciousKey))){returntrue;}// 3. 获取当前时间戳秒longcurrentTimeSystem.currentTimeMillis()/1000;// 计算窗口起始时间当前时间 - 10分钟longwindowStartTimecurrentTime-RedisKeyConstants.WINDOW_SIZE_SECONDS;// 4. 写入当前登录记录到ZSetMemberuserIdScore当前时间戳redisTemplate.opsForZSet().add(loginKey,userId,currentTime);// 5. 清理过期数据删除Score 窗口起始时间的元素redisTemplate.opsForZSet().removeRangeByScore(loginKey,0,windowStartTime);// 6. 统计最近10分钟内的不同userId数量ZSet的元素个数LongcountredisTemplate.opsForZSet().zCard(loginKey);// 7. 设置Key的过期时间10分钟防止内存泄漏redisTemplate.expire(loginKey,RedisKeyConstants.EXPIRE_TIME_SECONDS,TimeUnit.SECONDS);// 8. 判断是否为坏人数量≥5if(count!nullcountRedisKeyConstants.MALICIOUS_THRESHOLD){// 标记为恶意设备存入黑名单过期时间可以设长一点比如24小时redisTemplate.opsForValue().set(maliciousKey,1,24,TimeUnit.HOURS);returntrue;}// 9. 正常设备返回falsereturnfalse;}}3在登录接口中调用importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;RestControllerRequestMapping(/login)publicclassLoginController{AutowiredprivateLoginRiskControlServiceriskControlService;PostMappingpublicStringlogin(StringuserId,StringdeviceId){// 1. 先检查是否为恶意设备booleanisMaliciousriskControlService.checkMaliciousDevice(userId,deviceId);if(isMalicious){return登录失败检测到异常设备请完成验证码验证;}// 2. 正常登录逻辑校验账号密码、生成Token等// ...return登录成功;}}高并发场景的优化在访问量非常大的场景下上面的代码肯定扛不住所以还需要做一些优化。1. 布隆过滤器快速过滤正常设备如果每个登录请求都查RedisRedis的压力会很大。可以考虑用布隆过滤器先快速过滤在布隆过滤器里只存“潜在恶意设备”比如最近10分钟内登录过≥3个userId的设备登录时先查布隆过滤器如果不在里面直接放行不查Redis如果在里面再查Redis确认。2. 异步处理如果Redis查询慢会阻塞主登录流程影响用户体验。考虑把“写入ZSet、清理过期数据、统计数量、判断坏人”这些步骤用线程池或消息队列异步执行这样的话主登录流程只做“账号密码校验、生成Token”用户体验会更好一点。小贴士异步会有极短的延迟但是风控允许这种小误差。3. 本地缓存Redis双层缓存如果恶意设备黑名单查询很频繁每次都查Redis还是会有压力。考虑用本地缓存比如Caffeine Redis的双层缓存。这样一来本地缓存存“最近1分钟内的恶意设备黑名单”登录时先查本地缓存命中直接返回没命中再查Redis查到后写入本地缓存。小贴士本地缓存不能设 10 分钟因为多实例部署时本地缓存互不同步会导致风控失效。设 1 分钟既能减轻 Redis 压力又能保证安全可控。

相关新闻