SpringBoot + Redis的Bitmap实现活跃用户统计

前言
Redis的Bitmap数据结构是一种紧凑的位图,它可以用于实现各种场景,其中统计活跃用户是一种经典的业务场景。

实现原理是,通过将每个用户表示为一个位,从而跟踪用户的活跃状态,使用位图记录用户每天是否登录,并计算月度或年度活跃用户数。

案例代码
以下是一个小例子,可以看到,使用SpringDataRedis,可以很轻松的实现BitMap的操作。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.BitSet;

@Service
public class UserLoginService {

    // 用户登录记录的键前缀
    private static final String LOGIN_KEY_PREFIX = "login:"; 
    // 月份格式化器
    private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyyMM"); 
    // 年份格式化器
    private static final DateTimeFormatter YEAR_FORMATTER = DateTimeFormatter.ofPattern("yyyy"); 

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 记录用户登录
     *
     * @param userId 用户ID
     */
    public void recordLogin(String userId) {

        // 获取存储当天用户登录信息的键
        String loginKey = getLoginKey(); 
        // 计算位图偏移量,对应用户ID的哈希码
        int bitOffset = getUserIdHashCode(userId); 
        // 将用户ID对应的位设置为1,表示用户登录
        redisTemplate.opsForValue().setBit(loginKey, bitOffset, true); 
    }

    /**
     * 获取月度活跃用户统计数据
     *
     * @return 月度活跃用户统计数据
     */
    public BitSet getMonthlyActiveUsers() {

        // 获取存储月度活跃用户信息的键
        String monthlyKey = getMonthlyKey(); 
        // 从Redis中获取位图数据
        return retrieveBitSet(monthlyKey); 
    }

    /**
     * 获取年度活跃用户统计数据
     *
     * @return 年度活跃用户统计数据
     */
    public BitSet getYearlyActiveUsers() {

        // 获取存储年度活跃用户信息的键
        String yearlyKey = getYearlyKey(); 
        // 从Redis中获取位图数据
        return retrieveBitSet(yearlyKey); 
    }

    /**
     * 获取存储当天用户登录信息的键
     */
    private String getLoginKey() {

        LocalDate today = LocalDate.now();
        String dateKey = today.format(DateTimeFormatter.ISO_DATE);
        return LOGIN_KEY_PREFIX + dateKey;
    }

    /**
     * 计算用户ID的哈希码,保证非负数并适应位图长度
     */
    private int getUserIdHashCode(String userId) {

        int hashCode = userId.hashCode();
        // 1073741823是Redis位图最大支持长度(2^30-1),可根据实际需求调整
        return Math.abs(hashCode % 1073741823); 
    }

    /**
     * 获取存储月度活跃用户信息的键
     */
    private String getMonthlyKey() {

        LocalDate today = LocalDate.now();
        String monthKey = today.format(MONTH_FORMATTER);
        return LOGIN_KEY_PREFIX + "monthly:" + monthKey;
    }

    /**
     * 获取存储年度活跃用户信息的键
     */
    private String getYearlyKey() {

        LocalDate today = LocalDate.now();
        String yearKey = today.format(YEAR_FORMATTER);
        return LOGIN_KEY_PREFIX + "yearly:" + yearKey;
    }

    /**
     * 从Redis中获取位图数据
     */
    private BitSet retrieveBitSet(String key) {

        // 获取存储在Redis中的位图数据
        byte[] bytes = (byte[]) redisTemplate.opsForValue().get(key); 
        if (bytes != null) {
            // 将字节数组转换为BitSet
            return BitSet.valueOf(bytes); 
        } else {
            // 若不存在,则返回空的BitSet
            return new BitSet(); 
        }
    }
}

其中有几个地方解释一下:

1、recordLogin方法用于记录用户登录情况。每天的登录情况被保存在以"login:日期"为键的位图中,用户的登录状态由位图中对应的位表示;

2、countMonthlyActiveUsers方法用于计算月度活跃用户数量。每个月的活跃用户数保存在以"login:monthly:年月"为键的位图中,通过Redis的bitCount方法统计位图中置为1的位数,即月度活跃用户数;

3、ountYearlyActiveUsers方法用于计算年度活跃用户数量,原理同上,只是统计的键变为"login:yearly:年份";

4、getLoginKey、getUserIdHashCode、getMonthlyKey和getYearlyKey是辅助方法,负责生成对应的Redis键或计算用户ID的哈希码。

转换
上面的例子最终返回的是BitSet对象,通过这个对象我们经过转换后可以获取到许多经典的统计数据,这里列举一些经典常用的统计结果示例。

大家可以根据这里面列举的统计数据,针对上面的代码进行替换,得到自己想要的结果。

import java.util.BitSet;

// 获取月度活跃用户统计数据
BitSet monthlyActiveUsers = userLoginService.getMonthlyActiveUsers();

// 获取年度活跃用户统计数据
BitSet yearlyActiveUsers = userLoginService.getYearlyActiveUsers();

// 统计月度活跃用户数量
int monthlyActiveUserCount = monthlyActiveUsers.cardinality();

// 统计年度活跃用户数量
int yearlyActiveUserCount = yearlyActiveUsers.cardinality();

// 判断某个用户是否为月度活跃用户
String userId = "user123";
boolean isMonthlyActiveUser = monthlyActiveUsers.get(userLoginService.getUserIdHashCode(userId));

// 判断某个用户是否为年度活跃用户
boolean isYearlyActiveUser = yearlyActiveUsers.get(userLoginService.getUserIdHashCode(userId));

// 输出统计结果
System.out.println("月度活跃用户数量: " + monthlyActiveUserCount);
System.out.println("年度活跃用户数量: " + yearlyActiveUserCount);
System.out.println("用户user123是否为月度活跃用户: " + isMonthlyActiveUser);
System.out.println("用户user123是否为年度活跃用户: " + isYearlyActiveUser);

总结
Redis的Bitmap数据结构非常灵活,可以根据具体需求实现各种位操作,但平时在项目中很多人没有机会使用到,这个案例非常简单,希望能让大家对此有个认识,未来用到了不会感到陌生。

你可能感兴趣的:(springboot,spring,boot,redis,后端)