redis设计统计用户访问量
需求:实现某个接口每天调用了多少次,每个用户只记录一次。
(例如,统计刷题模块,练题模块,模拟面试模块每天访问量,利于后续针对功能访问量做出其他优化设计。贴子的浏览量)先分析几种不同的方案:方案一:使用Hash哈希结构
实现方法:当用户访问网站时,我们可以使用用户的ID作为标识(若用户未登录,则生成一个随机标识)。通过Redis的HSET命令,以URI和日期拼接作为key,用户ID或随机标识作为field,将value设置为1。统计访问量时,使用HLEN命令获取结果。
优点:
-
实现简单,易于理解。
-
查询方便,数据准确性高。
缺点:
-
随着key的增多,内存占用过大,性能可能下降。
-
对于访问量巨大的网站(如拼多多),此方案可能无法承受。
方案二:使用Bitset
实现方法:利用Bitset对用户ID进行压缩存储。通过SETBIT命令标记用户访问,使用GETBIT查询用户是否访问,最后通过BITCOUNT统计访问量。
优点:
-
占用内存更小,适用于大规模用户数据。
-
查询方便,可指定查询某个用户。
缺点:
-
用户稀疏时,内存占用可能比方案一更大。
-
对于未登录用户,可能需要额外的映射开销。
方案三:使用概率算法
实现方法:采用Redis中的HyperLogLog算法,这是一种基数评估算法。使用PFADD命令记录用户访问,通过PFCOUNT命令计算访问量。
优点:
-
占用内存极小,每个key仅需要12KB。
-
非常适合超大规模用户访问量的网站。
缺点:
-
查询指定用户时可能存在误差。
-
总数统计存在一定的误差(约0.81%)。
以上三种方案各有优劣,具体选择应根据实际业务需求和场景来决定:
-
若对数据准确性要求较高,且访问量适中,可以选择方案一。
-
若需要处理大规模用户数据,且对内存占用有要求,可以选择方案二。
-
若对数据精度要求不高,但需要处理超大规模用户访问量,可以选择方案三。
以上是针对Redis提供的解决方案,在项目中对前后端日志埋点数据,通过流式计算以及大数据分析,也是常用的解决方案。
首先最简单的做法就是当用户访问某个功能时,在进行查询时自动传递一个携带用户id,日期与功能字段的数据传入mysql中,当第二次进入先查询,当日已存在便不再存储该数据,但是该方案存在问题便是,每个用户都在频繁的与数据库做交互,是否针对具体业务情况将其存入单独的库存储也需要权衡,而且我们系统已经引入的redis,不妨直接用redis来做。首次先查询redis,redis存在直接切断,不存在存入则存入redis,后续通过定时任务调度在流量稳定时迁入数据库。
redis的HyperLogLog
Redis 在 2.8.9 版本添加了 HyperLogLog 结构。Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以HyperLogLog 不能像集合那样,返回输入的各个元素。比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。
简单的说HyperLogLog通过概率学统计访问数量,统计的时候存在误差,但是误差很小。所以适合特殊场景,例如日访问量统计。
操作指令如下:
用户001访问。如果 HyperLogLog 的内部被修改了,那么返回 1,否则返回 0 .
PFADD visit_{data} “001”
用户002访问。
PFADD visit_{data} “002”
查看访问数量。
PFCOUNT visit_{data}
集成spring
public boolean add(String key, String obj) { return redisTemplate.opsForHyperLogLog().add(key, obj) > 0; } public long count(String key) { // pfcount 非精准统计 key的计数 return redisTemplate.opsForHyperLogLog().size(key); }
我们可以抽象出单独接口方法,需要统计日活量的直接调用接口即可
功能接口
public interface UniqueVisitor { Boolean add(String id, String loginid); Boolean addMonth(String id, String loginid); Long getCount(String id); }
@Service public class UniqueVisitorImpl implements UniqueVisitor { @Resource private RedisUtil redisUtil; private static final String Unique_Visitor = "user_Unique_Visitor:"; @Override public Boolean add(String id, String loginid) { LocalDate today = LocalDate.now(); long dayOfYear = today.getDayOfYear(); return redisUtil.add(Unique_Visitor + id+dayOfYear, loginid); } @Override public Boolean addMonth(String id, String loginid) { LocalDate today = LocalDate.now(); long Month = today.getMonthValue(); return redisUtil.add(Unique_Visitor + id+Month, loginid); } @Override public Long getCount(String id) { LocalDate today = LocalDate.now(); long dayOfYear = today.getDayOfYear(); return redisUtil.count(Unique_Visitor + id+dayOfYear); } }
调用计入日活量
@GetMapping("/uniquevisitor") public Result<Boolean> uniqueVisitor(@RequestParam("id") String id) { try { return Result.ok(uniqueVisitor.getCount(id)); } catch (Exception e) { log.error("SubjectCategoryController.add.error:{}", e.getMessage(), e); return Result.fail("查询浏览量失败"); } }
/** * 统计用户今日访问新增题目 */ uniqueVisitor.add("addSubject",LoginUtil.getLoginId());
实现效果
我们测试了两个账号,可以看到今日新增题目的日活量为2次。