在现代互联网应用中,用户活跃度统计是一个常见的需求。特别是对于拥有上亿用户的大型系统,如何高效地统计在特定日期范围内持续活跃的用户是一个技术挑战。本文将介绍如何使用Spring Boot结合Redis的Bitmap数据结构来实现这一功能。
Bitmap(位图)是Redis提供的一种特殊数据结构,它实际上是一个字符串类型,但可以像操作位数组一样操作它。每个bit位可以表示两种状态(0或1),这使得Bitmap非常适合用于大规模的二值状态统计。
Bitmap的主要优势:
我们为每个日期创建一个独立的Bitmap,其中每个bit位代表一个用户ID:
首先在Spring Boot项目中添加Redis依赖:
xml<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
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;
}
}
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;
}
}
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亿用户:
通过Redis的Bitmap数据结构,我们实现了高效的海量用户活跃统计功能。这种方案具有以下优势:
对于需要统计用户连续活跃状态的场景,这种基于Bitmap的方案是一个非常优秀的选择。