5.1 系统中常见的四种统计
1.聚合统计
统计多个集合元素的聚合结果(交并差集合统计) set集合
2.排序统计
抖音短视频最新评论留言的场景,设计一个展现列表
在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,建议使用zset
3.二值统计
集合元素的取值就只有 0 和 1 ,来记录签到还是没签到 bitmap
4.基数统计
只统计一个集合中不重复的元素个数 hyperloglog
5.2hyperloglog
1.名词解释
- UV (Unique Visitor) 独立访客,一般理解为客户端IP (需要考虑去重)
- PV (Page View) 页面浏览量,不用去重
- DAU(Daily Active User)日活跃用户量,登录或者使用了某个产品的用户数(去重复登录的用户),常用于反映网站、互联网应用或者网络游戏的运营情况
- MAU(Monthly Active User)月活跃用户量
2.需求
很多计数类场景,比如 每日注册 IP 数、每日访问 IP 数、页面实时访问数 PV、访问用户数 UV等。
因为主要的目标高效、巨量地进行计数,所以对存储的数据的内容并不太关心。
也就是说它只能用于统计巨量数量,不太涉及具体的统计对象的内容和精准性。
统计单日一个页面的访问量(PV),单次访问就算一次。
统计单日一个页面的用户访问量(UV),即按照用户为维度计算,单个用户一天内多次访问也只算一次。
多个key的合并统计,某个门户网站的所有模块的PV聚合统计就是整个网站的总PV。
3.hyperloglog复习
- 基数
- 是一种数据集,去重复后的真实个数
- 在Redis里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2的64次方个不同元素的基数
-
基数统计就是 HyperLogLog
-
去重复统计功能
- hashSet
- bitmap
- bitmap是通过用位bit数组来表示各元素是否出现,每个元素对应一位,所需的总内存为N个bit。
- 新进入的元素只需要将已经有的bit数组和新加入的元素进行按位或计算就行。这个方式能大大减少内存占用且位操作迅速。
- 如果数据较大,比如一个样本案例就是一亿个基数拉值数据,一个样本就是一亿,如果要统计一亿个数据的基数拉值,大约需要内存 1100000000/8/1024/1024 约等于 12M,内存减少占用的效果显著。这样得到统计一个对象样本的基数值需要12M。
- 如果统计10000个对象样本(1w个亿级),就需要117.1875G将近120G,可见使用bitmaps还是不适用大数据量下(亿级)的基数计数场景,
- 但是bitmap是精确计算的
- 结论
- 量变会引起质变
- 办法
- 概率算法
- 通过牺牲准确率来换取空间,对于不要求绝对准确率的场景下可以使用,因为概率算法不直接存储数据本身,通过牺牲准确率来换取空间,对于不要求绝对准确率的场景下可以使用,因为概率算法不直接存储数据本身
- HyperLogLog就是一种概率算法的实现。
- 概率算法
-
原理说明
-
只是进行不重复的基数统计,不是集合也不保存数据,只记录数量而不是具体内容
-
有误差
- hyperloglog提供不精确的去重技术方案
- 牺牲准确率来换取空间,误差仅仅只是 0.81% 左右
-
-
5.3统计亿级UV的Redis方案
1.对于技术的选型
- 用mysql
- mysql扛不住稍微大一点的并发,而且都需要存入mysql中,导致mysql的检索也会变慢
- 用redis的hash结构存储
- 按照ipv4的结构来说明,一个ip最多15个字节(ip=“192.168.238.1xx”),某一天 1.5亿*15个字节 = 2G,一个月60G,内存直接没了,加内存条都没用
- hyperloglog
- 在Redis里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2的64次方个不同元素的基数
2.编码(yml和pom与上次一致)
-
HypeLogLogService
/** * @author 晓风残月Lx * @date 2023/3/27 20:18 */ public interface HypeLogLogService { public long uv(); }
-
HyperLogLogServiceImpl
import com.xfcy.service.HypeLogLogService; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.util.Random; import java.util.concurrent.TimeUnit; /** * @author 晓风残月Lx * @date 2023/3/27 20:18 */ @Slf4j @Service public class HypeLogLogServiceImpl implements HypeLogLogService { @Resource private RedisTemplate redisTemplate; /** * 模拟后台有用户点击网站首页,每个用户来自不同的IP地址 * * @PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。 */ @PostConstruct public void initIp() { new Thread(() -> { String ip = null; for (int i = 0; i < 200; i++) { Random random = new Random(); ip = random.nextInt(256) + "." + random.nextInt(256) + "." + random.nextInt(256) + "." + random.nextInt(256); Long hll = redisTemplate.opsForHyperLogLog().add("hll", ip); log.info("ip = {}, 该IP地址访问首页的次数={}",ip,hll); try { // 暂停3秒钟 TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { throw new RuntimeException(e); } } }, "t1").start(); } public long uv() { // PFCOUNT return redisTemplate.opsForHyperLogLog().size("hll"); } }
-
HyperLogLogController
import com.xfcy.service.HypeLogLogService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; /** * @author 晓风残月Lx * @date 2023/3/27 20:17 */ @Api(tags = "淘宝亿级UV的Redis统计方案") @Slf4j @RestController public class HyperLogLogController { @Resource private HypeLogLogService hypeLogLogService; @ApiOperation("获得IP去重复后的UV统计访问量") @RequestMapping(value = "/uv",method = RequestMethod.GET) public long uv(){ return hypeLogLogService.uv(); } }
-
启动项目后测试