使用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";
    }
}
相关推荐
用户83562907805121 分钟前
如何使用 Python 将 PowerPoint 转换为 PDF 文档
后端·python
菜冻鱼21 分钟前
Python-pytorch-高级技巧
开发语言·人工智能·pytorch·python·深度学习·神经网络·聚类
lpfasd12322 分钟前
python webview打包版卡死、开发版正常
开发语言·python
用户83562907805132 分钟前
如何使用 Python 为 PowerPoint 幻灯片添加切换效果
后端·python
wear工程师39 分钟前
Redis 和 MySQL 缓存一致性,延迟双删为什么不是强一致
redis·mysql
菜冻鱼40 分钟前
Python-pytorch-模型保存与加载
开发语言·人工智能·pytorch·python·深度学习·机器学习
亿牛云爬虫专家1 小时前
爬虫调度系统设计:用 Celery 实现按媒体权重动态调整的抓取频率控制
爬虫·python·隧道代理·舆情采集·媒体权重·频率控制·ip自动轮换
添砖java‘’2 小时前
Redis中常用数据结构
数据库·redis·缓存
Code额2 小时前
Python 连接 DeepSeek API,OpenAI
开发语言·python·ai·ai编程
Suhan423 小时前
SQLAlchemy的两种查询query()查询、stmt=select()查询
数据库·python·sql·oracle