2025-06-18
Redis
0

目录

基于Spring Boot和Redis Bitmap的海量用户活跃统计方案
引言
Redis Bitmap简介
系统设计
数据结构设计
关键技术点
实现代码
1. 添加依赖
2. 配置RedisTemplate
3. 服务层实现
4. 控制器层
性能分析
空间复杂度
时间复杂度
优化建议
总结

基于Spring Boot和Redis Bitmap的海量用户活跃统计方案

引言

在现代互联网应用中,用户活跃度统计是一个常见的需求。特别是对于拥有上亿用户的大型系统,如何高效地统计在特定日期范围内持续活跃的用户是一个技术挑战。本文将介绍如何使用Spring Boot结合Redis的Bitmap数据结构来实现这一功能。

Redis Bitmap简介

Bitmap(位图)是Redis提供的一种特殊数据结构,它实际上是一个字符串类型,但可以像操作位数组一样操作它。每个bit位可以表示两种状态(0或1),这使得Bitmap非常适合用于大规模的二值状态统计。

Bitmap的主要优势:

  • 极高的空间效率:每个用户每天的状态只需要1bit
  • 快速的位运算:支持AND、OR、NOT等位操作
  • 时间复杂度低:O(1)的查询和设置复杂度

系统设计

数据结构设计

我们为每个日期创建一个独立的Bitmap,其中每个bit位代表一个用户ID:

  • 如果bit值为1,表示该用户当天活跃
  • 如果bit值为0,表示该用户当天不活跃

关键技术点

  1. 用户ID映射:由于用户ID是数字类型,可以直接作为bit位偏移量
  2. 日期键设计:使用"active
    "格式作为键名
  3. 位运算:使用BITOP命令进行多Bitmap的AND运算

实现代码

1. 添加依赖

首先在Spring Boot项目中添加Redis依赖:

xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>

2. 配置RedisTemplate

java
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericToStringSerializer<>(Object.class)); return template; } }

3. 服务层实现

java
@Service public class UserActiveService { private final RedisTemplate<String, Object> redisTemplate; public UserActiveService(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } /** * 记录用户活跃状态 * @param userId 用户ID * @param date 日期 */ public void recordUserActive(long userId, LocalDate date) { String key = "active:" + date.format(DateTimeFormatter.BASIC_ISO_DATE); redisTemplate.opsForValue().setBit(key, userId, true); } /** * 统计在指定日期范围内持续活跃的用户数 * @param startDate 开始日期 * @param endDate 结束日期 * @return 持续活跃的用户数 */ public long countContinuousActiveUsers(LocalDate startDate, LocalDate endDate) { // 生成日期范围内的所有键 List<String> keys = new ArrayList<>(); LocalDate current = startDate; while (!current.isAfter(endDate)) { keys.add("active:" + current.format(DateTimeFormatter.BASIC_ISO_DATE)); current = current.plusDays(1); } if (keys.isEmpty()) { return 0; } // 创建临时键名 String tempKey = "temp:active:intersection:" + System.currentTimeMillis(); try { // 执行AND运算 redisTemplate.execute((RedisCallback<Long>) connection -> { connection.bitOp(RedisStringCommands.BitOperation.AND, tempKey.getBytes(), keys.stream().map(String::getBytes).toArray(byte[][]::new)); return null; }); // 统计结果中的1的个数 return redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount(tempKey.getBytes())); } finally { // 删除临时键 redisTemplate.delete(tempKey); } } /** * 检查指定用户是否在日期范围内持续活跃 * @param userId 用户ID * @param startDate 开始日期 * @param endDate 结束日期 * @return 是否持续活跃 */ public boolean isUserContinuousActive(long userId, LocalDate startDate, LocalDate endDate) { LocalDate current = startDate; while (!current.isAfter(endDate)) { String key = "active:" + current.format(DateTimeFormatter.BASIC_ISO_DATE); Boolean isActive = redisTemplate.opsForValue().getBit(key, userId); if (isActive == null || !isActive) { return false; } current = current.plusDays(1); } return true; } }

4. 控制器层

java
@RestController @RequestMapping("/api/active") public class ActiveUserController { private final UserActiveService userActiveService; public ActiveUserController(UserActiveService userActiveService) { this.userActiveService = userActiveService; } @PostMapping("/record") public ResponseEntity<?> recordActive(@RequestParam long userId, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) { userActiveService.recordUserActive(userId, date); return ResponseEntity.ok().build(); } @GetMapping("/count") public ResponseEntity<Long> countContinuousActiveUsers( @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { long count = userActiveService.countContinuousActiveUsers(startDate, endDate); return ResponseEntity.ok(count); } @GetMapping("/check") public ResponseEntity<Boolean> checkUserContinuousActive( @RequestParam long userId, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { boolean result = userActiveService.isUserContinuousActive(userId, startDate, endDate); return ResponseEntity.ok(result); } }

性能分析

空间复杂度

假设系统有1亿用户:

  • 每个日期的Bitmap大小约为12MB(100,000,000 bits ≈ 12MB)
  • 存储一年的数据约需要4.3GB(12MB × 365)

时间复杂度

  • 设置用户活跃状态:O(1)
  • 统计持续活跃用户数:O(N)(N为日期范围内的天数)
  • 检查单个用户是否持续活跃:O(N)

优化建议

  1. 分片处理:对于特别大的用户ID范围,可以考虑按用户ID范围分片
  2. 定期归档:将历史数据归档到持久化存储,减少Redis内存占用
  3. 压缩存储:使用Redis的RDB或AOF持久化策略
  4. 异步处理:将统计计算任务放入后台队列处理

总结

通过Redis的Bitmap数据结构,我们实现了高效的海量用户活跃统计功能。这种方案具有以下优势:

  1. 极高的空间效率,1亿用户每天仅需12MB存储
  2. 快速的位运算能力,能够高效处理大规模数据
  3. 简单的实现方式,代码量少且易于维护

对于需要统计用户连续活跃状态的场景,这种基于Bitmap的方案是一个非常优秀的选择。