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

前言

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

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

案例代码

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

java 复制代码
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对象,通过这个对象我们经过转换后可以获取到许多经典的统计数据,这里列举一些经典常用的统计结果示例。

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

java 复制代码
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数据结构非常灵活,可以根据具体需求实现各种位操作,但平时在项目中很多人没有机会使用到,这个案例非常简单,希望能让大家对此有个认识,未来用到了不会感到陌生。

相关推荐
明月与玄武8 分钟前
快速掌握Django框架设计思想(图解版)
后端·python·django
陪我一起学编程9 分钟前
关于ORM增删改查的总结——跨表
数据库·后端·python·django·restful
Q_Q51100828512 分钟前
python+django/flask成都奥科厨具厂产品在线销售系统
vue.js·spring boot·python·django·flask·node.js·php
南囝coding15 分钟前
这个 361K Star 的项目,一定要收藏!
前端·后端·github
虎鲸不是鱼36 分钟前
Spring Boot3流式访问Dify聊天助手接口
java·spring boot·后端·大模型·llm
onlooker666636 分钟前
Go语言底层(五): 深入浅出Go语言的ants协程池
开发语言·后端·golang
武子康1 小时前
Java-46 深入浅出 Tomcat 核心架构 Catalina 容器全解析 启动流程 线程机制
java·开发语言·spring boot·后端·spring·架构·tomcat
寻月隐君1 小时前
Solana 开发实战:Rust 客户端调用链上程序全流程
后端·rust·web3
丘山子2 小时前
别再滥用 None 了!这才是 Python 处理缺失值的好方法
后端·python·面试
error_cn2 小时前
postgresql视图与触发器
后端