redis的Bitmap记录用户在线状态
具体实现:
# 用户上线时,设置对应bit为1
SETBIT online_users {user_id} 1
# 用户下线时,设置对应bit为0
SETBIT online_users {user_id} 0
# 判断用户是否在线
GETBIT online_users {user_id}
# 获取当前在线用户数量
BITCOUNT online_users
# 批量获取在线用户列表
# 每次获取一个字节(8位)的数据
for i in range(0, max_user_id, 8):
byte = GETRANGE online_users i i+7
# 解析byte中的每一位,位为1的即为在线用户
按业务分片
# 可以按照业务线划分不同的bitmap
SETBIT online_users:game {user_id} 1
SETBIT online_users:chat {user_id} 1
# 统计特定业务的在线用户
BITCOUNT online_users:game
时间分片
# 按天记录用户在线状态
SETBIT online_users:{date} {user_id} 1
# 统计最近7天的日活用户(使用BITOP OR合并)
BITOP OR online_users_7days
online_users:20240212
online_users:20240211
...
online_users:20240206
容量优化
# 用户ID按范围分片
SETBIT online_users:0 {user_id % 1000000} 1
SETBIT online_users:1 {user_id % 1000000} 1
// 1. 配置类
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
return template;
}
}
// 2. 用户在线状态服务
@Service
@Slf4j
public class UserOnlineStatusService {
@Autowired
private RedisTemplate redisTemplate;
private static final String ONLINE_KEY_PREFIX = "user:online:";
private static final int EXPIRE_DAYS = 7; // 数据保留7天
/**
* 设置用户在线
*/
public void setUserOnline(Long userId, String bizType) {
try {
String key = buildKey(bizType, LocalDate.now());
redisTemplate.opsForValue().setBit(key, userId, true);
// 设置过期时间
redisTemplate.expire(key, EXPIRE_DAYS, TimeUnit.DAYS);
} catch (Exception e) {
log.error("Failed to set user online status: userId={}, bizType={}", userId, bizType, e);
throw new RuntimeException("Failed to set user online status", e);
}
}
/**
* 设置用户离线
*/
public void setUserOffline(Long userId, String bizType) {
try {
String key = buildKey(bizType, LocalDate.now());
redisTemplate.opsForValue().setBit(key, userId, false);
} catch (Exception e) {
log.error("Failed to set user offline status: userId={}, bizType={}", userId, bizType, e);
throw new RuntimeException("Failed to set user offline status", e);
}
}
/**
* 批量设置用户在线状态
*/
public void batchSetUserOnline(List userIds, String bizType) {
String key = buildKey(bizType, LocalDate.now());
try {
for (Long userId : userIds) {
redisTemplate.opsForValue().setBit(key, userId, true);
}
redisTemplate.expire(key, EXPIRE_DAYS, TimeUnit.DAYS);
} catch (Exception e) {
log.error("Failed to batch set user online status: userCount={}, bizType={}",
userIds.size(), bizType, e);
throw new RuntimeException("Failed to batch set user online status", e);
}
}
/**
* 判断用户是否在线
*/
public boolean isUserOnline(Long userId, String bizType) {
try {
String key = buildKey(bizType, LocalDate.now());
return Boolean.TRUE.equals(redisTemplate.opsForValue().getBit(key, userId));
} catch (Exception e) {
log.error("Failed to check user online status: userId={}, bizType={}", userId, bizType, e);
throw new RuntimeException("Failed to check user online status", e);
}
}
/**
* 获取当前在线用户数量
*/
public long getOnlineUserCount(String bizType) {
try {
String key = buildKey(bizType, LocalDate.now());
return redisTemplate.execute((RedisCallback) con -> con.bitCount(key.getBytes()));
} catch (Exception e) {
log.error("Failed to get online user count: bizType={}", bizType, e);
throw new RuntimeException("Failed to get online user count", e);
}
}
/**
* 获取指定用户列表中的在线用户数量
*/
public long getOnlineUserCount(List userIds, String bizType) {
String key = buildKey(bizType, LocalDate.now());
long count = 0;
try {
for (Long userId : userIds) {
if (Boolean.TRUE.equals(redisTemplate.opsForValue().getBit(key, userId))) {
count++;
}
}
return count;
} catch (Exception e) {
log.error("Failed to get online user count for specific users: userCount={}, bizType={}",
userIds.size(), bizType, e);
throw new RuntimeException("Failed to get online user count", e);
}
}
/**
* 获取在线用户列表
* @param start 起始用户ID
* @param end 结束用户ID
*/
public List getOnlineUsers(String bizType, long start, long end) {
List onlineUsers = new ArrayList<>();
String key = buildKey(bizType, LocalDate.now());
try {
for (long userId = start; userId <= end; userId++) {
if (Boolean.TRUE.equals(redisTemplate.opsForValue().getBit(key, userId))) {
onlineUsers.add(userId);
}
}
return onlineUsers;
} catch (Exception e) {
log.error("Failed to get online users: bizType={}, start={}, end={}",
bizType, start, end, e);
throw new RuntimeException("Failed to get online users", e);
}
}
/**
* 统计今日在线过的用户数量(活跃用户)
*/
public long getDailyActiveUserCount(String bizType) {
try {
String key = buildKey(bizType, LocalDate.now());
return redisTemplate.execute((RedisCallback) con -> con.bitCount(key.getBytes()));
} catch (Exception e) {
log.error("Failed to get daily active user count: bizType={}", bizType, e);
throw new RuntimeException("Failed to get daily active user count", e);
}
}
/**
* 统计指定日期范围内的活跃用户数量(去重)
*/
public long getActiveUserCountByDateRange(String bizType, LocalDate startDate, LocalDate endDate) {
try {
List keys = new ArrayList<>();
LocalDate currentDate = startDate;
while (!currentDate.isAfter(endDate)) {
keys.add(buildKey(bizType, currentDate));
currentDate = currentDate.plusDays(1);
}
// 使用OR操作合并多个bitmap
String destKey = String.format("%s:temp:%s:%s",
ONLINE_KEY_PREFIX, startDate, endDate);
redisTemplate.execute((RedisCallback
# 记录用户访问
PFADD daily_active:{date} {user_id}
# 获取当日活跃用户数
PFCOUNT daily_active:{date}
# 合并多天数据得到周活
PFMERGE weekly_active
daily_active:20240212
daily_active:20240211
...
daily_active:20240206
多维度活跃度分析
# 按照不同维度记录
PFADD active:game:{date} {user_id}
PFADD active:shop:{date} {user_id}
PFADD active:social:{date} {user_id}
# 统计用户在各个维度的活跃度
PFCOUNT active:game:{date}
PFCOUNT active:shop:{date}
PFCOUNT active:social:{date}
活跃度分层:
# 记录不同活跃度的用户
PFADD active:level:high:{date} {user_id} # 高活跃用户
PFADD active:level:medium:{date} {user_id} # 中活跃用户
PFADD active:level:low:{date} {user_id} # 低活跃用户
# 统计各层级用户数
PFCOUNT active:level:high:{date}
漏斗分析:
# 记录用户在不同阶段的行为
PFADD funnel:visit:{date} {user_id} # 访问
PFADD funnel:browse:{date} {user_id} # 浏览
PFADD funnel:cart:{date} {user_id} # 加购
PFADD funnel:order:{date} {user_id} # 下单
PFADD funnel:pay:{date} {user_id} # 支付
# 分析转化率
visit_count = PFCOUNT funnel:visit:{date}
pay_count = PFCOUNT funnel:pay:{date}
conversion_rate = pay_count / visit_count
HyperLogLog的优点:
需要注意的限制:
最佳实践建议:
合理设置过期时间
# 设置数据过期时间
PFADD daily_active:{date} {user_id}
EXPIRE daily_active:{date} 30 * 86400 # 30天后过期
配合其他数据类型使用:
# 使用Set保存详细的用户列表(当需要少量精确数据时)
SADD active_users:{date} {user_id}
# 使用HyperLogLog统计大量数据
PFADD active_count:{date} {user_id}
批量统计优化:
# 使用pipeline批量写入
pipeline.pfadd(f"active:{date}", user_id1)
pipeline.pfadd(f"active:{date}", user_id2)
...
pipeline.execute()
// 1. 配置类
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new StringRedisSerializer());
return template;
}
}
// 2. 活跃度统计服务
@Service
@Slf4j
public class UserActivityService {
@Autowired
private RedisTemplate redisTemplate;
private static final String KEY_PREFIX = "hyperloglog:user:active:";
/**
* 记录用户活跃
* @param bizType 业务类型(如game, shop等)
* @param userId 用户ID
* @param date 日期
*/
public void recordUserActivity(String bizType, Long userId, LocalDate date) {
try {
String key = buildKey(bizType, date);
redisTemplate.opsForHyperLogLog().add(key, String.valueOf(userId));
} catch (Exception e) {
log.error("Failed to record user activity: bizType={}, userId={}, date={}",
bizType, userId, date, e);
throw new RuntimeException("Failed to record user activity", e);
}
}
/**
* 批量记录用户活跃
*/
public void recordUserActivities(String bizType, List userIds, LocalDate date) {
try {
String key = buildKey(bizType, date);
String[] users = userIds.stream()
.map(String::valueOf)
.toArray(String[]::new);
redisTemplate.opsForHyperLogLog().add(key, users);
} catch (Exception e) {
log.error("Failed to batch record user activities: bizType={}, userCount={}, date={}",
bizType, userIds.size(), date, e);
throw new RuntimeException("Failed to batch record user activities", e);
}
}
/**
* 获取日活跃用户数(DAU)
*/
public long getDailyActiveUsers(String bizType, LocalDate date) {
String key = buildKey(bizType, date);
return redisTemplate.opsForHyperLogLog().size(key);
}
/**
* 获取周活跃用户数(WAU)
*/
public long getWeeklyActiveUsers(String bizType, LocalDate endDate) {
// 获取前7天的key
List keys = new ArrayList<>();
for (int i = 0; i < 7; i++) {
LocalDate date = endDate.minusDays(i);
keys.add(buildKey(bizType, date));
}
// 合并统计
String mergeKey = buildKey(bizType, endDate) + ":week";
redisTemplate.opsForHyperLogLog().union(mergeKey,
keys.toArray(new String[0]));
return redisTemplate.opsForHyperLogLog().size(mergeKey);
}
/**
* 获取月活跃用户数(MAU)
*/
public long getMonthlyActiveUsers(String bizType, LocalDate endDate) {
// 获取前30天的key
List keys = new ArrayList<>();
for (int i = 0; i < 30; i++) {
LocalDate date = endDate.minusDays(i);
keys.add(buildKey(bizType, date));
}
// 合并统计
String mergeKey = buildKey(bizType, endDate) + ":month";
redisTemplate.opsForHyperLogLog().union(mergeKey,
keys.toArray(new String[0]));
return redisTemplate.opsForHyperLogLog().size(mergeKey);
}
/**
* 获取指定时间范围的活跃用户数
*/
public long getActiveUsersByDateRange(String bizType, LocalDate startDate, LocalDate endDate) {
// 获取日期范围内的所有key
List keys = new ArrayList<>();
LocalDate currentDate = startDate;
while (!currentDate.isAfter(endDate)) {
keys.add(buildKey(bizType, currentDate));
currentDate = currentDate.plusDays(1);
}
// 合并统计
String mergeKey = buildKey(bizType, endDate) + ":range";
redisTemplate.opsForHyperLogLog().union(mergeKey,
keys.toArray(new String[0]));
return redisTemplate.opsForHyperLogLog().size(mergeKey);
}
/**
* 获取多个业务维度的活跃用户数
*/
public Map getMultiDimensionActiveUsers(List bizTypes, LocalDate date) {
Map result = new HashMap<>();
for (String bizType : bizTypes) {
result.put(bizType, getDailyActiveUsers(bizType, date));
}
return result;
}
private String buildKey(String bizType, LocalDate date) {
return KEY_PREFIX + bizType + ":" + date.format(DateTimeFormatter.ISO_DATE);
}
}
// 3. Controller层示例
@RestController
@RequestMapping("/api/activity")
@Slf4j
public class UserActivityController {
@Autowired
private UserActivityService activityService;
@PostMapping("/record")
public ResponseEntity recordActivity(
@RequestParam String bizType,
@RequestParam Long userId) {
activityService.recordUserActivity(bizType, userId, LocalDate.now());
return ResponseEntity.ok("Success");
}
@GetMapping("/stats/daily")
public ResponseEntity