(黑马点评)八、实现签到统计和uv统计
8.1 签到统计系列功能
8.1.1 认识BitMap结构
BitMap是Redis基于String实现的一种高效的二进制数位的数据结构。因此一个BItMap的最大上线为512M,转为bit位可表示 2^32位
常见命令
SETBIT:向指定位置(offset)存入一个0或1
GETBIT :获取指定位置(offset)的bit值
BITCOUNT :统计BitMap中值为1的bit位的数量
BITFIELD :操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值
BITFIELD_RO :获取BitMap中bit数组,并以十进制形式返回
BITOP :将多个BitMap的结果做位运算(与 、或、异或)
BITPOS :查找bit数组中指定范围内第一个0或1出现的位置
8.1.2 为什么要使用BitMap结构进行统计
传统的数据库表统计方式:十分耗费存储内存
采用二进制位的BitMap统计方式:高效、内存占用少、
key用来指定统计者的信息:姓名、编号、年月
vlaue采用二进制的0 | 1 串标识签到状况 ,0表示为未签到 1 表示为已签到
8.1.3 使用BitMap实现用户签到统计
按月来统计用户签到信息,签到记录为1,未签到则记录为0.
签到功能实现流程
1. 获取当前用户信息
2. 获取当前日期信息
3. 拼接业务key
4. 使用setBit(key,offset,value)实现签到打卡
/** * 签到统计 * @return */ @PostMapping("/sign") public Result sign(){ return userService.sign(); } /** * 签到功能 * @return */ @Override public Result sign() { //1. 获取当前的登陆用户 UserDTO user = UserHolder.getUser(); //2. 获取当前日期 LocalDateTime now = LocalDateTime.now(); //3. 拼接业务的key String keySuffix = now.format(DateTimeFormatter.ofPattern("yyyy/MM")); String key = USER_SIGN_KEY + user.getNickName() + '_' + user.getId() + '_' + keySuffix; //4. 获取签到日期 int day = now.getDayOfMonth(); //5. 写入Redis SETBIT key offset ture/false stringRedisTemplate.opsForValue().setBit(key, day-1, true); return Result.ok(); }
8.1.4 使用BitMap实现用户连续签到天数统计
实现统计连续签到天数的流程
1. 获取当前登录用户信息
2. 获取日期信息
3. 拼接业务key
4. 使用 stringRedisTemplate.opsForValue()
.bitField(key,
BitFieldSubCommands
.create()
.get(BitFieldSubCommands
.BitFieldType
.unsigned(dayOfMonth))
.valueAt(0)); 获取统计信息【10进制】5. 判空
6. 循环右移 + 与1 运算 得出连续签到天数
7. 返回结果
/**
* 连续签到天数统计
* @return
*/
@GetMapping("/sign/count")
public Result countSign(){
return userService.countSign();
}
/**
* 连续签到天数统计
* @return
*/
@Override
public Result countSign() {
//1. 获取当前登录用户
UserDTO user = UserHolder.getUser();
//2. 获取当前日期
LocalDateTime now = LocalDateTime.now();
int dayOfMonth = now.getDayOfMonth();
//3.拼接业务的key
String keySuffix = now.format(DateTimeFormatter.ofPattern("yyyy/MM"));
String key = USER_SIGN_KEY +
user.getNickName() +
'_' + user.getId() +
'_' + keySuffix;
//4. 获取当前用户签到数据(返回十进制数)
List<Long> signNum = stringRedisTemplate.opsForValue()
.bitField(key,
BitFieldSubCommands
.create()
.get(BitFieldSubCommands
.BitFieldType
.unsigned(dayOfMonth))
.valueAt(0));
if(signNum == null || signNum.isEmpty()){
return Result.ok(0);
}
Long num = signNum.get(0);
if(num == null || num == 0){
return Result.ok(0);
}
//5. 循环右移 + 1与运算 得出连续签到天数
int dayCount = 0;
while(true) {
if(((num & 1) == 0)){
break;
}else{
dayCount ++;
}
num = num >> 1;
}
return Result.ok(dayCount);
}
8.2 百万级UV统计功能实现
8.2.1 百万级UV统计的实现策略——HyperLogLog
什么是UV统计
UV:全称Unique Visitor,也叫独立访客量,是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站,只记录1次。
什么是PV统计
PV:全称Page View,也叫页面访问量或点击量,用户每访问网站的一个页面,记录1次PV,用户多次打开页面,则记录多次PV。往往用来衡量网站的流量
传统方式实现UV统计面对的难题
UV统计在服务端做会比较麻烦,因为要判断该用户是否已经统计过了,需要将统计过的用户信息保存。但是如果每个访问的用户都保存到Redis中,数据量会非常恐怖。
高效的解决措施——使用HyperLogLog
Hyperloglog(HLL)是从Loglog算法派生的概率算法,用于确定非常大的集合的基数,而不需要存储其所有值。
Redis中的HLL是基于string结构实现的,单个HLL的内存永远小于16kb,内存占用低的令人发指!作为代价,其测量结果是概率性的,有小于0.81%的误差。不过对于UV统计来说,这完全可以忽略。
HyperLogLog的原理
哈希处理
对每个要加入的元素进行哈希处理。这一步骤将元素映射为一个固定长度的二进制串。
划分与索引——分桶
在 Redis 的实现中,使用 64bit 的 Hash 函数,其中 14bit 用于桶索引(将原始集合分成多个子集),剩下的 50bit 用于计算前导零的个数。这意味着 Redis 中的 HyperLogLog 使用16384个桶,每个桶可以表达的最大数字是:2^5+2^4+...+1 = 63
估计基数——桶估计值公式
通过使用补偿和线性计数的技术,将最大前导零位转换为基数估计值。具体的计算方法可以使用查表或其他数学模型来实现。在 Redis 的 HyperLogLog 实现中,有一个基于经验值的参数 alpha_m 用于调整估计值,以确保在大多数情况下都能提供较为准确的估计。
误差修正——偏差修正因子
由于 HyperLogLog 是一种概率性算法,其估计结果会存在一定的误差。Redis 通过应用修正公式来纠正这些估计误差,从而提供更加准确的基数估计。
合并与查询
HyperLogLog 支持多个数据集的并集操作,可以将多个 Key 的 UV 进行去重合并,且合并的复杂度是 O(1)。同时,获取一个或多个 HyperLogLog 结构的基数估计值也非常高效,查询的复杂度同样是 O(1)。
更多请看:HyperLogLog 算法的原理讲解以及 Redis 是如何应用它的聪明的你可能会马上想到,用 HashMap 这种数 - 掘金 (juejin.cn)
8.2.2 使用HyperLogLog实现百万级UV统计测试
我们向Redis插入100万条数据,用来模拟超大UV访问量的统计,并计算使用HyperLogLog存储需要消耗的内存大小以及统计误差,从而向大家展示Hyper在统计方面的优秀性能
/**
* 向Hyperloglog插入100万条数据进行测试,查看内存占用情况
*/
@Test
void testHyperLogLog() {
// 开始循环
for(int i=0;i<1000;i++){
String[] values = new String[1000];
for(int j=0;j<1000;j++){
values[j] = "user_" + i + "_" + j;
}
stringRedisTemplate.opsForHyperLogLog().add("hl2",values);
}
// 统计数量
Long count = stringRedisTemplate.opsForHyperLogLog().size("hl2");
System.out.println("count = " + count);
}