欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 时评 > redis记录用户在线状态+活跃度

redis记录用户在线状态+活跃度

2025/2/13 10:40:06 来源:https://blog.csdn.net/weixin_39075154/article/details/145586242  浏览:    关键词:redis记录用户在线状态+活跃度

1.记录用户在线状态

redis的Bitmap记录用户在线状态

  • 使用一个大的Bitmap,每个bit位对应一个用户ID
  • bit值1表示在线,0表示离线
  • 用户ID与bit位的映射关系: bit位置 = 用户ID % 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的即为在线用户

1.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:20240212online_users:20240211...online_users:20240206

容量优化

  • 每个bitmap最大512MB,可存储40亿用户状态
  • 当用户量超大时,可以分片存储:
# 用户ID按范围分片
SETBIT online_users:0 {user_id % 1000000} 1
SETBIT online_users:1 {user_id % 1000000} 1

1.2java代码示例

// 1. 配置类
@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<String, String> template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(new StringRedisSerializer());return template;}
}// 2. 用户在线状态服务
@Service
@Slf4j
public class UserOnlineStatusService {@Autowiredprivate RedisTemplate<String, String> 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<Long> 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<Long>) 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<Long> 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<Long> getOnlineUsers(String bizType, long start, long end) {List<Long> 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<Long>) 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<String> keys = new ArrayList<>();LocalDate currentDate = startDate;while (!currentDate.isAfter(endDate)) {keys.add(buildKey(bizType, currentDate));currentDate = currentDate.plusDays(1);}// 使用OR操作合并多个bitmapString destKey = String.format("%s:temp:%s:%s", ONLINE_KEY_PREFIX, startDate, endDate);redisTemplate.execute((RedisCallback<Object>) con -> {byte[][] byteKeys = keys.stream().map(String::getBytes).toArray(byte[][]::new);con.bitOp(RedisStringCommands.BitOperation.OR, destKey.getBytes(), byteKeys);return null;});// 统计合并后的bitmap中1的数量Long count = redisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(destKey.getBytes()));// 删除临时keyredisTemplate.delete(destKey);return count != null ? count : 0;} catch (Exception e) {log.error("Failed to get active user count by date range: bizType={}, startDate={}, endDate={}", bizType, startDate, endDate, e);throw new RuntimeException("Failed to get active user count by date range", e);}}private String buildKey(String bizType, LocalDate date) {return ONLINE_KEY_PREFIX + bizType + ":" + date.format(DateTimeFormatter.ISO_DATE);}
}// 3. Controller层示例
@RestController
@RequestMapping("/api/user/status")
@Slf4j
public class UserOnlineStatusController {@Autowiredprivate UserOnlineStatusService onlineStatusService;@PostMapping("/online")public ResponseEntity<String> setUserOnline(@RequestParam Long userId,@RequestParam String bizType) {onlineStatusService.setUserOnline(userId, bizType);return ResponseEntity.ok("Success");}@PostMapping("/offline")public ResponseEntity<String> setUserOffline(@RequestParam Long userId,@RequestParam String bizType) {onlineStatusService.setUserOffline(userId, bizType);return ResponseEntity.ok("Success");}@GetMapping("/check")public ResponseEntity<Boolean> checkUserOnline(@RequestParam Long userId,@RequestParam String bizType) {boolean isOnline = onlineStatusService.isUserOnline(userId, bizType);return ResponseEntity.ok(isOnline);}@GetMapping("/count")public ResponseEntity<Map<String, Object>> getOnlineCount(@RequestParam String bizType) {Map<String, Object> result = new HashMap<>();result.put("bizType", bizType);result.put("onlineCount", onlineStatusService.getOnlineUserCount(bizType));result.put("timestamp", LocalDateTime.now());return ResponseEntity.ok(result);}@GetMapping("/active/range")public ResponseEntity<Map<String, Object>> getActiveUsersByDateRange(@RequestParam String bizType,@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {Map<String, Object> result = new HashMap<>();result.put("bizType", bizType);result.put("startDate", startDate);result.put("endDate", endDate);result.put("activeUsers", onlineStatusService.getActiveUserCountByDateRange(bizType, startDate, endDate));return ResponseEntity.ok(result);}
}// 4. 测试类
@SpringBootTest
class UserOnlineStatusServiceTest {@Autowiredprivate UserOnlineStatusService onlineStatusService;@Testvoid testUserOnlineStatus() {String bizType = "test";Long userId = 1L;// 设置用户在线onlineStatusService.setUserOnline(userId, bizType);assertTrue(onlineStatusService.isUserOnline(userId, bizType));// 设置用户离线onlineStatusService.setUserOffline(userId, bizType);assertFalse(onlineStatusService.isUserOnline(userId, bizType));}@Testvoid testBatchOperation() {String bizType = "test";List<Long> userIds = Arrays.asList(1L, 2L, 3L, 4L, 5L);// 批量设置在线onlineStatusService.batchSetUserOnline(userIds, bizType);// 验证在线数量assertEquals(5, onlineStatusService.getOnlineUserCount(bizType));// 验证具体用户在线状态for (Long userId : userIds) {assertTrue(onlineStatusService.isUserOnline(userId, bizType));}}
}

1.3注意事项

  • Bitmap适合用户ID比较连续的场景
  • 如果用户ID不连续,可能会浪费一些空间
  • 对于大规模系统,建议按业务类型分片
  • 重要操作需要添加监控和告警
  • 考虑添加缓存层减少Redis访问

 

 

2.HyperLogLog统计活跃度

2.1使用场景:

  • 日活跃用户(DAU)
  • 周活跃用户(WAU)
  • 月活跃用户(MAU)
  • 页面/功能的独立访客数(UV)

2.2基本实现:

# 记录用户访问
PFADD daily_active:{date} {user_id}# 获取当日活跃用户数
PFCOUNT daily_active:{date}# 合并多天数据得到周活
PFMERGE weekly_active daily_active:20240212daily_active:20240211...daily_active:20240206

2.3高级应用:

多维度活跃度分析

# 按照不同维度记录
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

 

2.4 性能与准确性:

HyperLogLog的优点:

  • 空间效率极高,每个HyperLogLog仅需12KB内存
  • 计数效率高,不随数据量增加而降低性能
  • 可以合并统计,支持分布式场景

需要注意的限制:

  • 有0.81%的标准误差
  • 不支持删除单个元素
  • 只能统计基数,不能获取实际的元素内容

最佳实践建议:

合理设置过期时间

# 设置数据过期时间
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()

2.5java代码实现

// 1. 配置类
@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<String, String> 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 {@Autowiredprivate RedisTemplate<String, String> 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<Long> 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天的keyList<String> 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天的keyList<String> 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) {// 获取日期范围内的所有keyList<String> 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<String, Long> getMultiDimensionActiveUsers(List<String> bizTypes, LocalDate date) {Map<String, Long> 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 {@Autowiredprivate UserActivityService activityService;@PostMapping("/record")public ResponseEntity<String> recordActivity(@RequestParam String bizType,@RequestParam Long userId) {activityService.recordUserActivity(bizType, userId, LocalDate.now());return ResponseEntity.ok("Success");}@GetMapping("/stats/daily")public ResponseEntity<Map<String, Object>> getDailyStats(@RequestParam String bizType,@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {date = date == null ? LocalDate.now() : date;Map<String, Object> stats = new HashMap<>();stats.put("bizType", bizType);stats.put("date", date);stats.put("activeUsers", activityService.getDailyActiveUsers(bizType, date));return ResponseEntity.ok(stats);}@GetMapping("/stats/weekly")public ResponseEntity<Map<String, Object>> getWeeklyStats(@RequestParam String bizType,@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {endDate = endDate == null ? LocalDate.now() : endDate;Map<String, Object> stats = new HashMap<>();stats.put("bizType", bizType);stats.put("endDate", endDate);stats.put("startDate", endDate.minusDays(6));stats.put("activeUsers", activityService.getWeeklyActiveUsers(bizType, endDate));return ResponseEntity.ok(stats);}
}// 4. 测试类
@SpringBootTest
class UserActivityServiceTest {@Autowiredprivate UserActivityService activityService;@Testvoid testDailyActiveUsers() {String bizType = "test";LocalDate date = LocalDate.now();// 记录100个用户活跃List<Long> userIds = IntStream.range(1, 101).mapToObj(Long::valueOf).collect(Collectors.toList());activityService.recordUserActivities(bizType, userIds, date);long count = activityService.getDailyActiveUsers(bizType, date);assertEquals(100, count);}@Testvoid testWeeklyActiveUsers() {String bizType = "test";LocalDate endDate = LocalDate.now();// 记录7天的用户活跃数据for (int i = 0; i < 7; i++) {LocalDate date = endDate.minusDays(i);List<Long> userIds = IntStream.range(1, 101).mapToObj(Long::valueOf).collect(Collectors.toList());activityService.recordUserActivities(bizType, userIds, date);}long count = activityService.getWeeklyActiveUsers(bizType, endDate);assertTrue(count > 0);}
}

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com