使用Redis统计网站的UV/DAU

HyperLogLog/BitMap

统计UV、DAU需要用到Redis的高级数据类型

M

java 复制代码
public class RedisKeyUtil {

    private static final String PREFIX_UV = "uv";
    private static final String PREFIX_DAU = "dau";

    // a single day's UV
    public static String getUVKey(String date){
        return PREFIX_UV + SPLIT + date;
    }

    // a series of days' UV
    public static String getUVKey(String startDate, String endDate){
        return PREFIX_UV + SPLIT + startDate + SPLIT + endDate;
    }

    // get the DAU of a single day
    public static String getDAUKey(String date){
        return PREFIX_DAU + SPLIT + date;
    }

    // get the DAU of a series of days
    public static String getDAUKey(String startDate, String endDate){
        return PREFIX_DAU + SPLIT + startDate + SPLIT + endDate;
    }
}

C

java 复制代码
@Service
public class DataService {

    @Autowired
    private RedisTemplate redisTemplate;

    private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");

    // add the specified IP address to the UV
    public void recordUV(String ip){
        String redisKey = RedisKeyUtil.getUVKey(dateFormat.format(new Date()));
        redisTemplate.opsForHyperLogLog().add(redisKey, ip);
    }

    // query the specified range of days' UV
    public long calculateUV(Date start, Date end) {
        if(start == null || end == null){
            throw new IllegalArgumentException("start and end must not be null");
        }

        if(start.after(end)){
            throw new IllegalArgumentException("start date should be before end date");
        }

        // get all the keys of those days
        List<String> keyList = new ArrayList<>();
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(start);
        while(!calendar.getTime().after(end)){
            String redisKey = RedisKeyUtil.getUVKey(dateFormat.format(calendar.getTime()));
            keyList.add(redisKey);

            // add calendar to 1
            calendar.add(Calendar.DATE, 1);
        }

        // merge all the UV of those days
        String redisKey = RedisKeyUtil.getUVKey(dateFormat.format(start), dateFormat.format(end));
        redisTemplate.opsForHyperLogLog().union(redisKey, keyList.toArray());

        // return the statistics result
        return redisTemplate.opsForHyperLogLog().size(redisKey);
    }

    // add the specified user to the DAU
    public void recordDAU(int userId){
        String redisKey = RedisKeyUtil.getDAUKey(dateFormat.format(new Date()));
        redisTemplate.opsForValue().setBit(redisKey, userId, true);
    }

    // query the specified range of days' DAV
    public long calculateDAU(Date start, Date end){
        if(start == null || end == null){
            throw new IllegalArgumentException("start and end must not be null");
        }

        if(start.after(end)){
            throw new IllegalArgumentException("start date should be before end date");
        }

        // get all the keys of those days
        List<byte[]> keyList = new ArrayList<>();
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(start);
        while(!calendar.getTime().after(end)){
            String key = RedisKeyUtil.getDAUKey(dateFormat.format(calendar.getTime()));
            keyList.add(key.getBytes());

            // add calendar to 1
            calendar.add(Calendar.DATE, 1);
        }

        // or operation
        return (long) redisTemplate.execute(new RedisCallback() {
            @Override
            public Object doInRedis(RedisConnection connection) throws DataAccessException {
                String redisKey = RedisKeyUtil.getDAUKey(dateFormat.format(start), dateFormat.format(end));
                connection.stringCommands().bitOp(RedisStringCommands.BitOperation.OR,
                        redisKey.getBytes(), keyList.toArray(new byte[0][0]));
                return connection.stringCommands().bitCount(redisKey.getBytes());
            }
        });
    }
}

使用拦截器记录访问

java 复制代码
@Component
public class DataInterceptor implements HandlerInterceptor{

    @Autowired
    private DataService dataService;

    @Autowired
    private HostHolder hostHolder;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // record into the UV
        String ip = request.getRemoteHost();
        dataService.recordUV(ip);

        // record into the DAU
        User user = hostHolder.getUser();
        if(user != null){
            dataService.recordDAU(user.getId());
        }

        return true;
    }
}

配置拦截器

java 复制代码
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private DataInterceptor dataInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
               registry.addInterceptor(dataInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
    }
}

V

java 复制代码
@Controller
public class DataController {

    @Autowired
    private DataService dataService;

    // the page of the statistics
    @RequestMapping(path = "/data", method = {RequestMethod.GET, RequestMethod.POST})
    public String getDataPage() {
        return "/site/admin/data";
    }

    // calculates the UV of the site
//    @PostMapping(path = "/data/uv")
    @RequestMapping(path = "/data/uv", method = {RequestMethod.GET/*, RequestMethod.POST*/})
    public String getUV(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start,
                        @DateTimeFormat(pattern = "yyyy-MM-dd") Date end, Model model) {

        long uv = dataService.calculateUV(start, end);

        model.addAttribute("uvResult", uv);
        model.addAttribute("uvStartDate", start);
        model.addAttribute("uvEndDate", end);

        // `forward` means this function just disposal a half of the request,
        // and the rest of request need to be executed by other functions
        // post request still be post after forwarding
        return "forward:/data";
    }

    // calculates the DAU of the site
//    @PostMapping(path = "/data/dau")
    @RequestMapping(path = "/data/dau", method = {RequestMethod.GET/*, RequestMethod.POST*/})
    public String getDAU(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start,
                        @DateTimeFormat(pattern = "yyyy-MM-dd") Date end, Model model) {
        long dau = dataService.calculateDAU(start, end);
        model.addAttribute("dauResult", dau);
        model.addAttribute("dauStartDate", start);
        model.addAttribute("dauEndDate", end);

        // `forward` means this function just disposal a half of the request,
        // and the rest of request need to be executed by other functions
        // post request still be post after forwarding
        return "forward:/data";
    }
}
相关推荐
老胖闲聊1 小时前
Python Copilot【代码辅助工具】 简介
开发语言·python·copilot
Blossom.1181 小时前
使用Python和Scikit-Learn实现机器学习模型调优
开发语言·人工智能·python·深度学习·目标检测·机器学习·scikit-learn
曹勖之2 小时前
基于ROS2,撰写python脚本,根据给定的舵-桨动力学模型实现动力学更新
开发语言·python·机器人·ros2
lyaihao2 小时前
使用python实现奔跑的线条效果
python·绘图
咖啡啡不加糖2 小时前
Redis大key产生、排查与优化实践
java·数据库·redis·后端·缓存
MickeyCV3 小时前
使用Docker部署MySQL&Redis容器与常见命令
redis·mysql·docker·容器·wsl·镜像
ai大师3 小时前
(附代码及图示)Multi-Query 多查询策略详解
python·langchain·中转api·apikey·中转apikey·免费apikey·claude4
小小爬虾3 小时前
关于datetime获取时间的问题
python
肥仔哥哥19303 小时前
springCloud2025+springBoot3.5.0+Nacos集成redis从nacos拉配置起服务
redis·缓存·最新boot3集成
蓝婷儿4 小时前
6个月Python学习计划 Day 16 - 面向对象编程(OOP)基础
开发语言·python·学习