如何统计在线用户
字数: 0 字 时长: 0 分钟
你想要实现的在线用户统计功能通过Redis来管理用户的状态,是一个非常常见的解决方案。Redis天生适合处理类似会话管理、缓存等与用户状态相关的任务。下面给出一个完整的解决方案,涵盖了如何添加在线用户、查询在线人数、定时清除过期用户以及用户退出时删除用户的实现。
1. 项目依赖配置
首先,你需要确保你的Spring Boot项目中引入了相关的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2. Redis 配置
在application.yml
中添加Redis的配置:
spring:
redis:
host: localhost
port: 6379
# 选择一个数据库,默认是0
database: 0
3. 添加在线用户功能
每当用户登录时,我们会将用户的ID和登录时间添加到Redis中,使用zset
(有序集合)来存储在线用户,并为每个用户设置一个过期时间。这里可以以用户ID为key
,当前时间为score
。
@Service
public class OnlineUserService {
private static final String ONLINE_USERS_KEY = "online_users";
@Autowired
private StringRedisTemplate redisTemplate;
// 用户上线时添加用户,登录时间作为score
public void addUserOnline(Long userId) {
// 使用当前时间戳作为 score,ZSet 存储用户 ID
redisTemplate.opsForZSet().add(ONLINE_USERS_KEY, userId.toString(), System.currentTimeMillis());
}
// 获取在线用户数
public Long getOnlineUserCount() {
return redisTemplate.opsForZSet().zCard(ONLINE_USERS_KEY);
}
// 用户退出时删除用户
public void removeUserOnline(Long userId) {
redisTemplate.opsForZSet().remove(ONLINE_USERS_KEY, userId.toString());
}
}
4. 查询在线人数功能
查询在线人数其实就是查询Redis中zset
集合中的元素个数:
// 获取当前在线用户数
public Long getOnlineUserCount() {
return redisTemplate.opsForZSet().zCard(ONLINE_USERS_KEY);
}
5. 定时清除过期用户
通过定时任务清除已经超时的用户,比如用户超过30分钟未操作,就将其视为离线用户。可以通过Spring的@Scheduled
注解来实现定时清理任务。
@Component
public class OnlineUserCleanupTask {
private static final long USER_EXPIRE_TIME = 30 * 60 * 1000; // 30分钟
@Autowired
private StringRedisTemplate redisTemplate;
@Scheduled(fixedRate = 60000) // 每分钟执行一次
public void clearExpiredUsers() {
long currentTime = System.currentTimeMillis();
// 删除那些超过30分钟的用户
redisTemplate.opsForZSet().removeRangeByScore("online_users", 0, currentTime - USER_EXPIRE_TIME);
}
}
6. 用户退出登录时删除在线用户
当用户退出时,我们也需要将该用户从在线用户列表中删除:
// 用户退出时删除在线用户
public void removeUserOnline(Long userId) {
redisTemplate.opsForZSet().remove(ONLINE_USERS_KEY, userId.toString());
}
7. Vue3 前端交互
在前端使用Vue3进行在线人数展示与用户登录、退出等交互,可以通过Vue的生命周期钩子与Axios进行交互:
<template>
<div>
<h3>当前在线人数: {{ onlineCount }}</h3>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
onlineCount: 0
};
},
mounted() {
this.fetchOnlineCount();
},
methods: {
fetchOnlineCount() {
axios.get('/api/onlineUser/count')
.then(response => {
this.onlineCount = response.data;
})
.catch(error => {
console.error('获取在线人数失败', error);
});
}
}
};
</script>
8. Spring Boot 控制器
最后,你需要一个控制器来与前端进行交互,例如查询在线人数:
@RestController
@RequestMapping("/api/onlineUser")
public class OnlineUserController {
@Autowired
private OnlineUserService onlineUserService;
@GetMapping("/count")
public Long getOnlineUserCount() {
return onlineUserService.getOnlineUserCount();
}
@PostMapping("/add")
public void addUserOnline(@RequestParam Long userId) {
onlineUserService.addUserOnline(userId);
}
@PostMapping("/remove")
public void removeUserOnline(@RequestParam Long userId) {
onlineUserService.removeUserOnline(userId);
}
}
9. 数据库设计(MySQL)
你可能需要在数据库中存储一些用户基本信息(例如用户表),这里通过MyBatis-Plus和MySQL管理用户的持久化数据。
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50),
password VARCHAR(100),
last_login TIMESTAMP
);
MyBatis-Plus中,你可以创建UserMapper
接口来与用户表交互。
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
总结
Redis用于存储在线用户的状态。
定时任务负责定期清除过期的在线用户。
Spring Boot作为服务端,提供API接口供前端查询和操作在线用户。
Vue3前端展示在线人数,用户登录时调用后端接口。
其他
在构建应用的时候, 我们经常需要对用户的一举一动进行记录, 而其中一个比较重要的操作, 就是对在线的用户进行记录。
本文将介绍四种使用 Redis 对在线用户进行记录的方案, 这些方案虽然都可以对在线用户的数量进行统计, 但每个方案都有一些自己特有的操作, 并且各个方案的性能特征以及资源消耗也各有不同
方案 1 :使用有序集合
每当一个用户上线时, 我们就执行 ZADD 命令, 将这个用户以及它的在线时间添加到指定的有序集合中:
ZADD “online_users” <user_id> <current_timestamp>
通过使用 ZSCORE 命令检查指定的用户 ID 在有序集合中是否有相关联的分值, 我们可以知道该用户是否在线:
ZSCORE “online_users” <user_id>
而通过执行 ZCARD 命令, 我们可以知道总共有多用户在线:
ZCARD “online_users”
使用有序集合储存在线用户的强大之处在于, 它是本文介绍的所有方案当中, 能够执行最多聚合操作的一个方案, 原因在于, 这一方案既可以通过有序集合的成员(也即是用户的 ID)进行聚合操作, 也可以根据有序集合的分值(也即是用户的登录时间)进行聚合操作。
首先, 通过 ZINTERSTORE 和 ZUNIONSTORE 命令, 我们可以对多个记录了在线用户的有序集合进行聚合计算:
// 计算出 7 天之内都有上线的用户,并将它储存到 7_days_both_online_users 有序集合当中
ZINTERSTORE 7_days_both_online_users 7 “day_1_online_users” “day_2_online_users” … “day_7_online_users”
// 计算出 7 天之内总共有多少人上线了
ZUNIONSTORE 7_days_total_online_users 7 “day_1_online_users” … “day_7_online_users”
此外, 通过 ZCOUNT 命令, 我们可以统计出在指定的时间段之内有多少用户在线, 而 ZRANGEBYSCORE 命令则可以让我们获取到这些用户的名单:
// 统计指定时间段内上线的用户数量
ZCOUNT “online_users” <start_timestamp> <end_timestamp>
// 获取指定时间段内上线的用户名单
ZRANGEBYSCORE “online_users” <start_timestamp> <end_timestamp> WITHSCORES
通过这一方法, 我们可以知道网站在不同时间段的上线人数以及上线用户名单, 比如说, 我们可以用这个方法来分别获知网站在早晨、上午、中午、下午和夜晚的上线人数。
方案 2 :使用集合
正如上一节所说, 使用有序集合能够同时储存在线用户的名单以及各个用户的上线时间, 但如果我们只想要记录在线用户的名单, 而不想要储存用户的上线时间, 那么也可以使用集合来代替有序集合, 对在线的用户进行记录。
在这种情况下, 每当一个用户上线时, 我们就执行以下 SADD 命令, 将它添加到在线用户名单当中:
SADD “online_users” <user_id>
通过使用 SISMEMBER 命令, 我们可以检查一个指定的用户当前是否在线:
SISMEMBER “online_users” <user_id>
而统计在线人数的工作则可以通过执行 SCARD 命令来完成:
SCARD “online_users”
通过集合运算操作, 我们可以像有序集合方案一样, 对不同时间段或者日期的在线用户名单进行聚合计算。 比如说, 通过 SINTER 或者 SINTERSTORE 命令, 我们可以计算出一周都有在线的用户:
SINTER “day_1_online_users” “day_2_online_users” … “day_7_online_users”
此外, 通过 SUNION 命令或者 SUNIONSTORE 命令, 我们可以计算出一周内在线用户的总数量:
SUNION “day_1_online_users” “day_2_online_users” … “day_7_online_users”
而通过执行 SDIFF 命令或者 SDIFFSTORE 命令, 我们可以知道哪些用户今天上线了, 但是昨天没有上线:
SDIFF “today_online_users” “yesterday_online_users”
又或者工作日上线了, 但是假日没有上线:
// 计算工作日上线名单
SINTERSTORE “weekday_online_users” “monday_online_users” “tuesday_online_users” … “friday_online_users”
// 计算假日上线名单
SINTERSTORE “holiday_online_users” “saturday_online_users” “sunday_online_users”
// 计算工作日上线但是假日未上线的名单
SDIFF “weekday_online_users” “holiday_online_users”
诸如此类。
方案 3 :使用 HyperLogLog
虽然使用有序集合和集合能够很好地完成记录在线人数的工作, 但以上这两个方案都有一个明显的缺点, 那就是, 这两个方案耗费的内存会随着被统计用户数量的增多而增多: 如果你的网站用户数量比较多, 又或者你需要记录多天/多个时段的在线用户名单并进行聚合计算, 那么这两个方案可能会消耗你大量内存。
另一方面, 在有些情况下, 我们只想要知道在线用户的人数, 而不需要知道具体的在线用户名单, 这时有序集合和集合储存的信息就会显得多余了。
在需要尽可能地节约内存并且只需要知道在线用户数量的情况下, 我们可以使用 HyperLogLog 来对在线用户进行统计: HyperLogLog 是一个概率算法, 它可以对元素的基数进行估算, 并且每个 HyperLogLog 只需要耗费 12 KB 内存, 对于用户数量非常多但是内存却非常紧张的系统, 这一方案无疑是最佳之选。
在这一方案下, 我们使用 PFADD 命令去记录在线的用户:
PFADD “online_users” <user_id>
使用 PFCOUNT 命令获取在线人数:
PFCOUNT “online_users”
因为 HyperLogLog 也提供了计算交集的 PFMERGE 命令, 所以我们也可以用这个命令计算出多个给定时间段或日期之内, 上线的总人数:
// 统计 7 天之内总共有多少人上线了
PFMERGE “7_days_both_online_users” “day_1_online_users” “day_2_online_users” … “day_7_online_users”
PFCOUNT “7_days_both_online_users”
方案 4 :使用位图(bitmap)
回顾上面介绍的三个方案, 我们可以得出以上结论:
使用有序集合或者集合能够储存具体的在线用户名单, 但是却需要消耗大量的内存;
而使用 HyperLogLog 虽然能够有效地减少统计在线用户所需的内存, 但是它却没办法准确地记录具体的在线用户名单。
那么是否存在一种既能够获得在线用户名单, 又可以尽量减少内存消耗的方法存在呢? 这种方法的确存在 —— 使用 Redis 的位图就可以办到。
Redis 的位图就是一个由二进制位组成的数组, 通过将数组中的每个二进制位与用户 ID 进行一一对应, 我们可以使用位图去记录每个用户是否在线。
当一个用户上线时, 我们就使用 SETBIT 命令, 将这个用户对应的二进制位设置为 1 :
// 此处的 user_id 必须为数字,因为它会被用作索引
SETBIT “online_users” <user_id> 1
通过使用 GETBIT 命令去检查一个二进制位的值是否为 1 , 我们可以知道指定的用户是否在线:
GETBIT “online_users” <user_id>
而通过 BITCOUNT 命令, 我们可以统计出位图中有多少个二进制位被设置成了 1 , 也即是有多少个用户在线:
BITCOUNT “online_users”
跟集合一样, 用户也能够对多个位图进行聚合计算 —— 通过 BITOP 命令, 用户可以对一个或多个位图执行逻辑并、逻辑或、逻辑异或或者逻辑非操作:
// 计算出 7 天都在线的用户
BITOP “AND” “7_days_both_online_users” “day_1_online_users” “day_2_online_users” … “day_7_online_users”
// 计算出 7 在的在线用户总人数
BITOP “OR” “7_days_total_online_users” “day_1_online_users” “day_2_online_users” … “day_7_online_users”
// 计算出两天当中只有其中一天在线的用户
BITOP “XOR” “only_one_day_online” “day_1_online_users” “day_2_online_users”
HyperLogLog 方案记录一个用户是否在线需要花费 1 个二进制位, 对于用户数为 100 万的网站来说, 使用这一方案只需要耗费 125 KB 内存, 而对于用户数为 1000 万的网站来说, 使用这一方案也只需要花费 1.25 MB 内存。
虽然位图节约内存的效果不及 HyperLogLog 那么显著, 但是使用位图可以准确地判断一个用户是否上线, 并且能够像集合和有序集合一样, 对在线用户名单进行聚合计算。 因此对于想要尽量节约内存, 但又需要准确地知道用户是否在线, 又或者需要对用户的在线名单进行聚合计算的应用来说, 使用位图可以说是最佳之选。
总结
以下表格总结了以上四个方案的特点:
方案 | 特点 |
有序集合 | 能够同时储存在线用户的名单以及用户的上线时间,能够执行非常多的聚合计算操作,但是耗费的内存也非常多。 |
集合 | 能够储存在线用户的名单,也能够执行聚合计算,消耗的内存比有序集合少,但是跟有序集合一样,这个方案消耗的内存也会随着用户数量的增多而增多。 |
HyperLogLog | 无论需要统计的用户有多少,只需要耗费 12 KB 内存,但由于概率算法的特性,只能给出在线人数的估算值,并且也无法获取准确的在线用户名单。 |
位图 | 在尽可能节约内存的情况下,记录在线用户的名单,并且能够对这些名单执行聚合操作。 |
因为 Redis 同时支持多种数据结构, 所以一个问题常常可以在 Redis 里面找多种不同的解法, 并且每种解法都有各自的优点和缺点, 本文介绍的问题就是一个很好的例子。