资讯详情

从Nginx日志到在线用户列表:后端可观测性两招实战

📅 2026/10/5 2:39:53 | 华诺云谱 👁 阅读
从Nginx日志到在线用户列表:后端可观测性两招实战
“你都看到了谁”——这句话放在后端场景里其实是一道相当现实的题目线上服务每一天都在接收大量请求但如果你被问到“当前都有哪些人在访问系统”能不能在几分钟内给出明确答案这些人用的是浏览器还是脚本来自哪些 IP登录了哪些账号访问了哪些接口是否存在异常行为很多业务系统在开发阶段只关心功能能不能跑通上线之后才发现“可观测性”非常欠缺用户在线状态靠猜访问来源靠翻日志安全排查靠运气。本文就围绕“你都看到了谁”这个问题拆解两条完整的落地方案一条是用 Python 分析 Nginx 访问日志从静态数据里还原访问者的痕迹另一条是用 Spring Boot 实现一个在线用户列表从动态维度实时掌握当前在线的人。同时会补充核心概念、环境准备、常见坑点和工程建议适合后端开发者、运维工程师以及刚开始接触 Web 可观测性的新手。1. 背景这个问题为什么值得花时间研究1.1 “在线用户”和“活跃用户”不是一回事在动手之前先把两个容易混淆的概念分开。“在线用户”通常指当前仍然保持着会话状态、并且在最近一段时间内有请求活动的用户。比如一个用户登录了后台系统五分钟内又刷新了一次页面那他可以算作在线用户如果他关掉浏览器已经两个小时虽然会话记录还在但已经不应该出现在“在线列表”里。“活跃用户”则是一个更大的口径一般指某个统计周期内有访问行为的用户。一天内有登录记录就算当日活跃一周内有访问就算周活跃。在线状态强调的是“此刻”活跃状态强调的是“某段时间”。很多团队在统计在线人数时数字虚高往往就是把活跃口径混进了在线口径。1.2 常见的三类访问来源当我们试着回答“你都看到了谁”时能观察到的信息其实来自三个层面。第一层是网络入口层。只要请求到达服务器就可以拿到来源 IP、端口以及经过代理时携带的转发头信息。这一层能看到“哪个网络位置发起了请求”但看不到“具体是哪个人”。第二层是应用请求层。HTTP 请求里带有 User-Agent、Referer、Cookie、Authorization 等字段可以判断客户端类型、来源页面、登录凭证。这一层能看到“请求用什么工具发出”“是否携带登录态”但 User-Agent 和 Referer 都可以伪造。第三层是业务身份层。用户登录之后服务端会记录 userId、角色、权限、操作日志等。这一层才能把请求关联到具体账号也是做在线用户列表、操作审计、安全风控的基础。1.3 从“看到谁”到“做了什么”只回答“谁在访问”其实还不够更完整的可观测性还要回答“做了什么”。同样是看到一批 IP如果只是常规浏览页面那是正常流量如果短时间内高频请求登录接口、遍历订单接口、批量下载文件那就是需要重点关注的异常行为。所以本文的两个实战并不冲突日志分析适合回答“历史上有谁来过、谁频率异常”在线用户列表适合回答“此刻都有谁在线、谁最近活跃”。两者结合才能形成完整的访问者画像。2. 核心概念HTTP 请求中能“看到”谁2.1 一次请求中可以被观察的字段下面是一张常用的“可见信息”表后端开发者最好能熟练判断哪些字段可用、哪些字段不可信。信息来源说明注意点客户端 IPremote_addr发起 TCP 连接的地址经过代理后不一定准确X-Forwarded-For请求头记录经过的代理链可伪造需在可信代理后方可信任User-Agent请求头标识客户端类型可伪造只能作辅助判断Referer请求头记录来源页面 URL可空可伪造Cookie / Session请求头登录态身份凭证日志中不应输出完整 CookieAuthorization请求头接口认证凭证日志中必须脱敏请求方法 路径请求行用户行为痕迹分析热点接口和异常路径状态码响应行请求结果大量 4xx/5xx 需要关注在实际项目中日志里最容易犯的错误就是把 Token、Cookie、密码明文打印出来。日志是给人排查用的不是给攻击者送信息的脱敏是一条底线。2.2 在线用户状态的判定原理在线状态最核心的模型是“心跳 过期”。用户登录成功后服务端记录一条在线记录包含 userId、登录时间、最后活跃时间、IP 等信息。之后每次请求都看作一次心跳刷新最后活跃时间。当某个用户超过设定时间没有任何请求就判定为离线。整体流程可以概括为三步登录成功后写入在线记录设置最后活跃时间为当前时间。请求进入拦截器如果携带登录用户标识则刷新该用户的最后活跃时间。查询在线列表时过滤掉最后活跃时间超过阈值的用户即可得到实时在线列表。这里最关键的是“阈值”。阈值太短用户只是中途离开一会儿就被踢出在线列表阈值太长离线用户又长时间占着在线列表。通常 Web 管理后台会把阈值设置为 5 到 15 分钟具体要根据业务场景调整。2.3 日志分析的核心思路访问日志分析本质上做三件事读取、解析、统计。Nginx 的 access.log 默认是combined格式每行代表一次请求包含 IP、时间、请求行、状态码、响应大小、Referer、User-Agent。只要写一个正则表达式把关键字段提取出来再用Counter做频率统计就能得到“哪些 IP 来得多、哪些 User-Agent 是爬虫、哪些接口被频繁访问”等信息。日志分析是离线视角适合做回顾性排查在线用户列表是实时视角适合做现网监控。两者一起用才能更好回答“你都看到了谁”。3. 环境准备与示例素材3.1 版本说明本文的示例环境如下实际项目请根据自己的版本调整日志分析脚本Python 3.8 及以上仅使用标准库不需要额外安装第三方包。在线用户列表Spring Boot 2.7.xMaven 3.6JDK 1.8 或更高版本。演示环境Linux 或 Windows 均可本文命令以 Bash 风格为主。如果你使用的是 Spring Boot 3.x需要注意javax.servlet包名变成了jakarta.servlet核心思路不变替换包名即可。3.2 准备一份 access.log 示例分析日志之前我们需要有一个格式统一的日志文件。Nginx 默认的combined格式类似下面这样127.0.0.1 - - [10/Oct/2023:13:55:36 0800] GET /index.html HTTP/1.1 200 2326 https://example.com/ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0 Safari/537.36 192.168.1.100 - - [10/Oct/2023:13:56:02 0800] GET /api/user/list HTTP/1.1 200 4512 - python-requests/2.31.0 192.168.1.100 - - [10/Oct/2023:13:56:03 0800] POST /api/login HTTP/1.1 401 182 - python-requests/2.31.0从这份日志里我们已经能看到三个层次的蛛丝马迹IP 地址、请求路径、状态码、客户端工具。后文的正则就是按照这种格式编写的。3.3 Spring Boot 演示项目结构在线用户列表部分我会搭建一个最小可运行项目目录结构如下who-is-online ├── pom.xml └── src/main/java/com/example/whoisonline ├── WhoIsOnlineApplication.java ├── OnlineUser.java ├── OnlineUserManager.java ├── OnlineUserInterceptor.java ├── OnlineController.java ├── IpUtils.java └── WebConfig.java这个结构没有数据库、没有 Redis核心是用内存 Map 保存在线用户思路非常清晰适合先跑通再扩展。4. 实战一Python 脚本分析 Nginx 日志4.1 先确认日志格式不要拿到日志就直接套正则先打开日志文件看一两条真实记录确认字段顺序和数量。本文的正则针对的是标准的combined格式如果你在 Nginx 配置里添加了$request_time、$upstream_addr等自定义字段就需要自行调整正则。确认格式之后可以把日志文件放在任意目录脚本只负责读取和统计不修改原文件。4.2 编写日志解析脚本下面是完整的analyze_access_log.py脚本。它的功能包括逐行读取日志、提取 IP / 时间 / 请求 / 状态码 / UA、统计状态码分布、TOP IP、TOP UA、小时维度的请求趋势以及标记高频访问者。import re import sys import argparse from collections import Counter, defaultdict from datetime import datetime LOG_PATTERN re.compile( r^(?Pip\S) \S \S \[(?Ptime[^\]])\] r(?Prequest[^]*) (?Pstatus\d{3}) (?Pbody_size\S) r(?Preferer[^]*) (?Pua[^]*) ) def parse_line(line): match LOG_PATTERN.match(line) if not match: return None return match.groupdict() def parse_time(time_str): return datetime.strptime(time_str, %d/%b/%Y:%H:%M:%S %z) def extract_path(request): parts request.split( ) if len(parts) 2: return parts[1] return request def analyze_log(file_path, high_freq_threshold60): ip_counter Counter() ua_counter Counter() status_counter Counter() hour_counter Counter() path_counter Counter() invalid 0 total 0 with open(file_path, r, encodingutf-8, errorsignore) as f: for line in f: record parse_line(line) if not record: invalid 1 continue total 1 ip record[ip] ua record[ua] status record[status] path extract_path(record[request]) try: hour parse_time(record[time]).strftime(%Y-%m-%d %H) except ValueError: hour unknown ip_counter[ip] 1 ua_counter[ua] 1 status_counter[status] 1 hour_counter[hour] 1 path_counter[path] 1 print(f总请求数: {total}) print(f无法解析行数: {invalid}) print(\n 状态码分布 ) for status, count in status_counter.most_common(): print(f{status}: {count}) print(\n TOP 10 客户端 IP ) for ip, count in ip_counter.most_common(10): print(f{ip}: {count} 次) print(\n TOP 10 User-Agent ) for ua, count in ua_counter.most_common(10): print(f{ua}: {count} 次) print(\n TOP 10 请求路径 ) for path, count in path_counter.most_common(10): print(f{path}: {count} 次) print(\n 请求量 TOP 小时 ) for hour, count in hour_counter.most_common(10): print(f{hour}: {count} 次) print(f\n 高频访问者(超过 {high_freq_threshold} 次/小时) ) for ip, count in ip_counter.most_common(): if count high_freq_threshold: print(f{ip}: {count} 次) def main(): parser argparse.ArgumentParser(description分析 Nginx access.log) parser.add_argument(logfile, help日志文件路径) parser.add_argument(--threshold, typeint, default60, help高频请求阈值(次/小时)默认 60) args parser.parse_args() try: analyze_log(args.logfile, args.threshold) except FileNotFoundError: print(f文件不存在: {args.logfile}) sys.exit(1) if __name__ __main__: main()脚本里体现了几个工程细节正则匹配失败的行不会中断程序而是计入invalid日志编码使用errorsignore避免个别乱码行导致崩溃时间解析失败时归为unknown。这些处理在真实日志分析中非常实用。4.3 运行与分析结果在命令行执行python analyze_access_log.py /var/log/nginx/access.log --threshold 60如果日志文件在当前目录可以简化为python analyze_access_log.py access.log运行后输出大致如下总请求数: 10234 无法解析行数: 0 状态码分布 200: 8231 301: 1023 404: 890 500: 90 TOP 10 客户端 IP 210.12.34.56: 1520 次 103.88.46.132: 980 次 ...看到这样的结果第一反应不应该是直接封 IP而是结合请求路径和 User-Agent 进一步判断这些高频 IP 访问的是静态资源还是敏感接口UA 是浏览器还是python-requests如果两者都指向异常再考虑安全策略。4.4 扩展识别高频访问者和爬虫上面的脚本已经包含高频访问者识别功能。标准是“每小时超过 N 次”N 由--threshold控制。对于 User-Agent 中的爬虫特征可以在脚本中增加一个简单的关键词集合SPIDER_KEYWORDS [bot, spider, curl, python-requests, scrapy] def is_spider_ua(ua): ua_lower ua.lower() return any(keyword in ua_lower for keyword in SPIDER_KEYWORDS)然后在解析循环里对每条 UA 做一次判断单独统计爬虫请求量。需要说明的是正常搜索引擎爬虫也会访问站点不一定要拒绝但异常高频的脚本访问往往意味着接口被盗刷或数据被爬取需要另做治理。5. 实战二Spring Boot 实现在线用户列表5.1 需求与设计在线用户列表的需求很直接用户登录成功后出现在列表中用户有请求时刷新最后活跃时间超过 5 分钟没有请求则从列表消失管理员可以查看当前在线用户。为了不引入数据库和认证框架本文在演示时用请求头X-User-Id模拟登录用户的身份。实际项目中这个值应该来自登录后的 Token 解析结果而不是让客户端随意传一个 userId。5.2 项目结构与依赖在 Spring Boot 项目中只需要引入spring-boot-starter-web。pom.xml如下?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent groupIdcom.example/groupId artifactIdwho-is-online/artifactId version1.0.0/version properties java.version1.8/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /project5.3 核心代码实现先创建启动类WhoIsOnlineApplication.javapackage com.example.whoisonline; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class WhoIsOnlineApplication { public static void main(String[] args) { SpringApplication.run(WhoIsOnlineApplication.class, args); } }再创建在线用户实体类OnlineUser.java。这里使用普通 JavaBean 而不是 Lombok是为了避免读者因为 Lombok 版本问题导致编译失败package com.example.whoisonline; public class OnlineUser { private String userId; private String nickname; private String ip; private long loginTime; private long lastActiveTime; public String getUserId() { return userId; } public void setUserId(String userId) { this.userId userId; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname nickname; } public String getIp() { return ip; } public void setIp(String ip) { this.ip ip; } public long getLoginTime() { return loginTime; } public void setLoginTime(long loginTime) { this.loginTime loginTime; } public long getLastActiveTime() { return lastActiveTime; } public void setLastActiveTime(long lastActiveTime) { this.lastActiveTime lastActiveTime; } }核心管理类OnlineUserManager.java负责在线状态的写入、心跳续期、离线删除和在线列表查询package com.example.whoisonline; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; Component public class OnlineUserManager { private static final long EXPIRE_MILLIS 5 * 60 * 1000L; private final MapString, OnlineUser onlineUsers new ConcurrentHashMap(); public void online(String userId, String nickname, String ip) { OnlineUser user new OnlineUser(); user.setUserId(userId); user.setNickname(nickname); user.setIp(ip); long now System.currentTimeMillis(); user.setLoginTime(now); user.setLastActiveTime(now); onlineUsers.put(userId, user); } public void heartbeat(String userId, String ip) { OnlineUser user onlineUsers.get(userId); if (user ! null) { user.setLastActiveTime(System.currentTimeMillis()); if (ip ! null !ip.isEmpty()) { user.setIp(ip); } } } public void offline(String userId) { onlineUsers.remove(userId); } public ListOnlineUser listOnlineUsers() { long now System.currentTimeMillis(); ListOnlineUser result new ArrayList(); IteratorMap.EntryString, OnlineUser iterator onlineUsers.entrySet().iterator(); while (iterator.hasNext()) { OnlineUser user iterator.next().getValue(); if (now - user.getLastActiveTime() EXPIRE_MILLIS) { iterator.remove(); } else { result.add(user); } } return result; } public boolean isOnline(String userId) { OnlineUser user onlineUsers.get(userId); return user ! null System.currentTimeMillis() - user.getLastActiveTime() EXPIRE_MILLIS; } }这里有一个容易被忽略的细节heartbeat方法不会为不存在的 userId 创建在线记录。也就是说只有登录过的用户才能保持在线状态单纯伪造一个X-User-Id请求头是没法让自己出现在在线列表里的。IP 工具类IpUtils.java用于从请求中获取客户端地址package com.example.whoisonline; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; public class IpUtils { private IpUtils() { } public static String getClientIp(HttpServletRequest request) { String forwarded request.getHeader(X-Forwarded-For); if (StringUtils.hasText(forwarded)) { String first forwarded.split(,)[0].trim(); if (!first.isEmpty()) { return first; } } String realIp request.getHeader(X-Real-IP); if (StringUtils.hasText(realIp)) { return realIp; } return request.getRemoteAddr(); } }拦截器OnlineUserInterceptor.java会在每个请求进入时读取用户标识并刷新活跃时间package com.example.whoisonline; import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class OnlineUserInterceptor implements HandlerInterceptor { private final OnlineUserManager onlineUserManager; public OnlineUserInterceptor(OnlineUserManager onlineUserManager) { this.onlineUserManager onlineUserManager; } Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String userId request.getHeader(X-User-Id); if (StringUtils.hasText(userId)) { onlineUserManager.heartbeat(userId, IpUtils.getClientIp(request)); } return true; } }拦截器配置类WebConfig.javapackage com.example.whoisonline; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class WebConfig implements WebMvcConfigurer { private final OnlineUserManager onlineUserManager; public WebConfig(OnlineUserManager onlineUserManager) { this.onlineUserManager onlineUserManager; } Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new OnlineUserInterceptor(onlineUserManager)) .addPathPatterns(/**) .excludePathPatterns(/api/login, /error); } }控制器OnlineController.java提供登录、登出、在线列表三个接口package com.example.whoisonline; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.List; import java.util.Map; RestController public class OnlineController { private final OnlineUserManager onlineUserManager; public OnlineController(OnlineUserManager onlineUserManager) { this.onlineUserManager onlineUserManager; } PostMapping(/api/login) public MapString, Object login(RequestBody MapString, String body, HttpServletRequest request) { String userId body.get(userId); String nickname body.get(nickname); MapString, Object result new HashMap(); if (userId null || userId.trim().isEmpty()) { result.put(success, false); result.put(message, userId 不能为空); return result; } // 实际项目这里应该先校验账号密码再签发 Token最后记录在线状态。 onlineUserManager.online(userId, nickname, IpUtils.getClientIp(request)); result.put(success, true); result.put(message, 登录成功已记录在线状态); return result; } GetMapping(/api/online/list) public ListOnlineUser onlineList() { // 生产环境必须给该接口增加管理员权限校验不能匿名访问。 return onlineUserManager.listOnlineUsers(); } PostMapping(/api/logout) public MapString, Object logout(RequestBody MapString, String body) { String userId body.get(userId); MapString, Object result new HashMap(); if (userId null || userId.trim().isEmpty()) { result.put(success, false); result.put(message, userId 不能为空); return result; } onlineUserManager.offline(userId); result.put(success, true); result.put(message, 已下线); return result; } }这段代码把登录模拟、在线查询、登出操作放在一起完整可运行。生产环境需要把“从请求头取 userId”替换成“从 Token 解析 userId”并给/api/online/list增加权限控制。5.4 启动并验证启动项目mvn spring-boot:run使用 curl 模拟用户登录curl -X POST http://localhost:8080/api/login \ -H Content-Type: application/json \ -d {userId:10001,nickname:张三}查询在线列表curl http://localhost:8080/api/online/list预期输出类似[ { userId: 10001, nickname: 张三, ip: 127.0.0.1, loginTime: 1696948536000, lastActiveTime: 1696948536000 } ]再模拟一次心跳访问curl -H X-User-Id: 10001 http://localhost:8080/api/online/list这次请求本身会触发拦截器刷新lastActiveTime。如果你在 5 分钟内不再次请求用户就会从在线列表中消失。登出接口演示curl -X POST http://localhost:8080/api/logout \ -H Content-Type: application/json \ -d {userId:10001}5.5 生产环境扩展Redis 版思路上面的内存版适合单机演示和小型项目。一旦系统部署了多个实例每个实例的内存 Map 是各自独立的用户在实例 A 登录请求被负载均衡到实例 B 时在线状态就丢失了。生产环境建议使用 Redis 作为在线状态存储。核心思路是用 Redis 的 TTL 天然实现过期key 为online:user:{userId}登录时写入该 key值为用户信息的 JSON 字符串设置 5 分钟过期每次请求心跳时刷新过期时间查询在线用户时通过扫描online:user:*或额外维护一个用户索引集合来获取在线列表。使用StringRedisTemplate的核心片段思路如下stringRedisTemplate.opsForValue().set( online:user: userId, userJson, Duration.ofMinutes(5) ); // 心跳续期 stringRedisTemplate.expire(online:user: userId, Duration.ofMinutes(5));这段代码展示了基本思路实际使用时需要按项目的 Redis 客户端版本稍作调整。引入 Redis 后内存 Map 的清理逻辑就不再需要了TTL 会由 Redis 自动处理。6. 常见问题与排查思路6.1 在线人数统计为什么不准问题现象常见原因解决思路在线人数长期虚高过期时间太长或者根本没有过期清理逻辑设置合理的过期阈值并在查询时过滤超时用户在线人数波动剧烈阈值太短用户稍久没操作就被清除根据业务场景调整 TTL比如管理后台设为 15 分钟用户退出后仍在在线列表退出接口没有调用 offline 方法在登出逻辑中主动删除在线记录多个实例在线列表不一致内存 Map 方案无法跨节点共享改用 Redis 存储在线状态6.2 为什么拿到的 IP 不是真实 IP如果你的服务前面有 Nginx、SLB、CDN 等代理request.getRemoteAddr()拿到的是代理服务器地址而不是真实客户端 IP。排查时先看X-Forwarded-For和X-Real-IP请求头有没有值。如果为空说明代理层没有配置转发头如果包含多个逗号分隔的地址说明请求经过了多级代理通常第一个是原始客户端地址但没有绝对保证。需要特别注意的是X-Forwarded-For是客户端可伪造的。只有在 Nginx 层正确配置并覆盖该请求头应用层拿到它才有意义。6.3 日志脚本匹配不到记录运行日志分析脚本时如果发现无法解析行数很大大概率是日志格式和正则不匹配。解决办法是先执行head -n 1 access.log看真实日志内容然后对照正则里的字段顺序逐项确认。Nginx 默认的 combined 格式包含 IP、两个-占位符、时间、请求行、状态码、响应大小、Referer、UA。如果加了自定义字段需要同步修改正则。6.4 在线列表接口被当作“公开接口”滥用在线用户列表接口如果没做权限控制任何人都能查看系统当前有哪些用户在线这是一个安全隐患。最佳做法是将在线列表接口限制在管理后台或内网环境接口增加登录认证和管理员角色校验返回数据使用 VO 对象只暴露必要字段隐藏 IP 等敏感信息。7. 最佳实践与工程建议7.1 合规与隐私在线用户信息、IP 地址、User-Agent 在特定场景下都可能属于个人信息。系统在采集这些数据前需要遵循“合法、正当、必要”的收集原则并在隐私政策中告知用户。实际操作中有几条具体建议日志中不要打印完整 Token、Cookie、密码、手机号等敏感字段在线用户列表不向普通用户开放只面向有权限的管理人员日志保留时间要有策略比如保留 90 天或 180 天过期清理分析日志时只处理自己有权限访问的服务器日志不探测外部站点。7.2 安全与性能安全方面在线用户列表、日志查询这类功能属于高敏感功能要做单独的权限控制不能混在普通业务接口里。性能方面需要注意以下几点不要在每个请求里都更新数据库心跳更新应该使用 Redis TTL 或内存结构日志分析脚本要逐行读取不要一次性把整个日志文件 load 进内存在线列表查询如果数据量大要做分页在线状态阈值不要设成代码里的魔法数字建议放入配置中心或application.yml。7.3 可维护性与拓展在线用户功能看起来简单但维护起来有不少细节。把过期时间做成配置项可以方便在不同环境调整。登录时间、最后活跃时间、登录 IP 这些字段都应该记录因为它们是安全排查的基础数据。再往后拓展可以把“在线用户”升级成“用户行为轨迹”记录每个用户访问了哪些接口、停留了多久、操作顺序是什么。这时可以在请求入口埋点再配合日志采集链路逐步建立自己的可观测体系。8. 总结与下一步学习方向这篇文章用一个问句“你都看到了谁”串联了两条技术主线一条是通过 Python 解析 Nginx 日志从历史数据中还原访问者的来源和行为另一条是通过 Spring Boot 实现在线用户列表从实时数据中掌握当前在线用户。前者的关键词是“离线分析”后者的关键词是“心跳过期”两者并不冲突而是互补关系。如果要把这套能力做得更深入下一步可以学习三类内容第一是分布式会话与单点登录解决在线状态在多实例下的一致性第二是设备指纹与用户行为分析从访问者识别过渡到用户风险识别第三是可观测性体系搭建比如通过 Prometheus 和 Grafana 对你的活跃用户指标做可视化监控。最后建议你手头有服务器日志的话先把 Python 脚本跑一遍看看自己系统里到底“都有谁”。动态在线列表也可以从一个最小的 Spring Boot 项目开始理解了心跳和过期机制之后再考虑引入 Redis。动手验证永远比只看文章有效。如果这篇教程对你有帮助可以先收藏备用后面排查问题时再翻出来对照。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑