文章目录
一. 相关面试题
1. 面试题一
- 抖音电商直播,主播介绍商品有评论,1个商品对应一系列评论,排序+展示+取前10条记录?
- 用户在手机APP上的签到打卡信息:1天对应一系列用户的签到记录,新浪微博、钉钉打卡签到,没来如何统计?
- 应用网站傻姑娘的网页浏览信息:1个网页对应一系列的点击访问,淘宝网首页,每天有多少人浏览首页
- 你们公司系统上线后,说一下UV、PV和DAU分别是多少
2. 面试题二
- 记录对集合中的数据进行统计
在移动应用中,需要统计每天的新增用户数和第二天的留存用户数
在电商网站的商品评论中,需要统计评论列表中的最新评论
在签到打卡中,需要统计一个月内连续打卡的用户数
在网页访问中,需要统计独立访客(UV)的量
痛点:
类似于今日头条、抖音、淘宝这样的用户访问级别是亿级的,请问如何处理?
上面问题的关键点就是,对于亿级数据的收集、清洗、统计和展现,如何存、如何取得快,真正有价值的是如何统计
二. 统计的类型
亿级系统中常见的统计有四种:
1. 聚合统计
统计多个集合元素的聚合结果,就是前面讲过的交差并等集合统计
- 集合的差集运算: A − B A-B A−B
属于A但不属于B的元素构成的集合
SDIFF key [key...]
- 集合的并集运算: A ∪ B A \cup B A∪B
属于A或者属于B的元素合并后的集合
SUNION key[key...]
- 集合的交集运算: A ∩ B A \cap B A∩B
SINTER key [key...]
SINTERCARD numkeys key[key ...] [LIMIT limit]
属于A同时也属于B的集
2. 排序统计
例如:抖音段视频最新评论留言的场景,请你设计一个列表。考察你的数据结构和设计思路
对于上面的需求我们可以使用zset
数据结构
3. 二值统计
集合元素的取值就只有0和1两种,在钉钉上班签到打卡的场景中,我们只需要用记录有签到或没签到,此时可以用redis的bitmap数据结构。
4. 基数统计
统计一个集合中不重复的元素个数,这里使用redis的hyperloglog数据结构。
三. Hyperloglog
1. 专业名词
- UV
Unique Visitor,独立访客,一般理解为客户端IP(需要去重考虑)
- PV
Page View,页面访问量(不用去重)
- DAU
Daily Active User,日活跃用户量,登陆或者使用了某个产品的用户数(去除重复登陆的用户),常用于反映网站、互联网应用或者网络游戏运营情况
- MAU
Mouthly Active User,月活跃用户量
很多计数类场景,比如每日注册IP数,每日访问IP数,页面实时访问数,访问用户数,因为主要的目标是高效、巨量的进行计数,所以对存储的数据内容并步关心。
2. Hyperloglog使用
基数:是一种数据集,去重复后的真实个数。
去重复统计功能的基数估计算法就是HyperLogLog,它的优点是,在输入元素数量或者体积非常非常大的时候,计算基数的空间总是固定的,并且很小。在Redis里面,每个HyperLogLog只需要12kb的内存,就可以计算洁净2^64个的不同元素的基数,但是HyperLogLog只会根据输入元素来计算基数,而不会存储输入元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素。
添加元素到 HyperLogLog:
PFADD key element [element ...]
PFADD my_hyperloglog a b c d e f g
获取 HyperLogLog 的基数估计值:
PFCOUNT key [key ...]
PFCOUNT my_hyperloglog
合并多个 HyperLogLog:
PFMERGE destkey sourcekey [sourcekey ...]
> PFADD hll1 a b c d
(integer) 1
> PFADD hll2 c d e f
(integer) 1
> PFMERGE hll_union hll1 hll2
OK
> PFCOUNT hll_union
(integer) 6
3. Hyperloglog原理
去重的思路:
- hashset:在java中hashset就是一个无重复元素的集合(但是数据量很大不适合)
- bitmap:
bitmap是通过用位bit数组来表示各个元素是否出现,每个元素队员一位,所需的总内存是N个bit。基数技术则嫁给你每一个元素对应到bit数组中的其中一位,新进入的元素只需要讲已经有的bit数组和新加入的元素进行按位或计算就行,这个方式能大大减少内存占用,且操作迅速。例如,假设一个样本案例就是一亿个基数位值数据,一个样本就是一亿,如果要统计1亿个数据的基数位值,大约需要内存1000000000/8/1024约为12M,内存减少占用的效果显著,这样得到的统计一个对象样本的基数值就是12M。但是,统计10000个对象样本(1w个亿级),就需要117.1875G,可见使用bitmaps还是不适合大数据量下(亿级)的基数计数场景。
但是bitmap的统计是精确的不会有误差
- 概率算法:
通过牺牲准确率来换取空间,对于不要求绝对准确的场景下可以使用,因为概率算法不直接存储数据本身,通过一定的概率统计方法预估基数值,同时误差在一定范围内,由于又不存储故此可以大大节约内存。
HyperLogLog就是一种概率算法的体现
HyperLogLog只是进行不重复的基数统计,不是集合也不保存数据,只记录数量而不体现具体内容。它提供一种不精确的去重计数方案,误差大概在0.81%左右。
4. Hyperloglog案例
- 需求
UV的统计需要去重,一个用户一天内的多次访问只能算做一次,淘宝、天猫首页的UV,平均每天是1~1.5亿左右。
- 案例实现
Service
@Service
public class HyperLogLogService {
@Autowired
private RedisTemplate redisTemplate;
@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);
redisTemplate.opsForHyperLogLog().add("hll", ip);
System.out.println("ip={" + ip + "}");
try{
TimeUnit.SECONDS.sleep(3);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}, "t1").start();
}
public long UV(){
return redisTemplate.opsForHyperLogLog().size("hll");
}
}
Controller
@RestController
public class HyperLogLogController {
@Autowired
HyperLogLogService hyperLogLogService;
@RequestMapping(value = "/uv",method = RequestMethod.GET)
public long UV() {
return hyperLogLogService.UV();
}
}
统计结果
四. GEO
1. 面试题
移动互联网时代LBS应用越来越多,交友软件中附近的小姐姐,外卖软件中附近的美食店,打车软件附近的车辆等等,这种位置选择是如何实现的?
为什么不使用Mysql?
- 查询性能问题,如果并发高,数据量大这种查询是会搞垮Mysql数据库的
- 一般mysql查询是一个平面矩阵访问,而叫车服务是要以我为中心N公里为半径的圆形覆盖
- 精确的问题,我们知道地球不是平面,而是一个球,这种矩形计算在长距离计算时会有很大误差
2. GEO使用
GEOADD 添加经纬度坐标
GEOADD key longitude latitude member [longitude latitude member ...]
> GEOADD cities 13.361389 38.115556 "Athens" 15.087269 37.502669 "Thessaloniki"
(integer) 2
GEO返回经纬度
GEORADIUS key longitude latitude radius m|km|mi|ft [WITHCOORD]
> GEORADIUS cities 15 37 200 km WITHCOORD
1) 1) "Thessaloniki"
2) 1) "22.942600429058075"
2) "40.640062264928304"
GEOHASH返回坐标的geohash表示
Geohash算法生成base32编码值,3维变为2维,2维变一维
GEOHASH key member [member ...]
> GEOADD cities 13.361389 38.115556 "Athens" 15.087269 37.502669 "Thessaloniki"
(integer) 2
> GEOHASH cities "Athens"
1) "sqdtr74hyu0"
GEODIST两个位置之间的距离
GEODIST key member1 member2 [unit]
> GEODIST cities "Athens" "Thessaloniki" km
"303.6469"
获取指定位置范围内的地理位置信息
GEORADIUS key longitude latitude radius m|km|mi|ft [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
> GEORADIUS cities 15 37 200 km
1) "Thessaloniki"
获取指定位置范围内的地理位置信息并按距离排序:
GEORADIUSBYMEMBER key member radius m|km|mi|ft [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
> GEORADIUSBYMEMBER cities "Athens" 200 km
1) "Athens"
3. GEO案例
- 需求
以给定的经纬度为中心,找出某一半径内的元素
- 案例实现
Controller
@RestController
public class GeoController {
@Autowired
private GeoService geoService;
@RequestMapping(value = "/geoadd", method = RequestMethod.GET)
public String geoAdd() {
return geoService.geoAdd();
}
@RequestMapping(value = "/geopos", method = RequestMethod.GET)
public Point position(String member) {
return geoService.postion(member);
}
@RequestMapping(value = "/geohash", method = RequestMethod.GET)
public String hash(String member) {
return geoService.hash(member);
}
@RequestMapping(value = "/geodist", method = RequestMethod.GET)
public Distance distance(String member1, String member2) {
return geoService.distance(member1, member2);
}
@RequestMapping(value = "/georadius", method = RequestMethod.GET)
public GeoResults radiusByxy() {
return geoService.radiusByxy();
}
@RequestMapping(value = "/georadiusByMerber", method = RequestMethod.GET)
public GeoResults radiusByMember() {
return geoService.radiusByMember();
}
}
Service
@Service
public class GeoService {
public static final String CITY = "city";
@Autowired
private RedisTemplate redisTemplate;
public String geoAdd() {
Map<String, Point> map = new HashMap<>();
map.put("天安门", new Point(116.403963, 39.915119));
map.put("故宫", new Point(116.403414, 39.924091));
map.put("长城", new Point(116.024067, 40.362639));
map.put("北京大学", new Point(116.316833,39.998877));
map.put("清华大学", new Point(116.333374,40.009645));
redisTemplate.opsForGeo().add(CITY, map);
return map.toString();
}
public Point postion(String member) {
List<Point> position = redisTemplate.opsForGeo().position(CITY, member);
return position.get(0);
}
public String hash(String member) {
List<String> hash = redisTemplate.opsForGeo().hash(CITY, member);
return hash.get(0);
}
public Distance distance(String member1, String member2) {
Distance distance = redisTemplate.opsForGeo().distance(CITY, member1, member2, RedisGeoCommands.DistanceUnit.KILOMETERS);
return distance;
}
public GeoResults radiusByxy() {
//116.418038,39.919790
Circle circle = new Circle(116.418038, 39.919790, Metrics.KILOMETERS.getMultiplier());
RedisGeoCommands.GeoRadiusCommandArgs geoRadiusCommandArgs = RedisGeoCommands
.GeoRadiusCommandArgs
.newGeoRadiusArgs()
.includeDistance()
.includeCoordinates()
.sortDescending();
GeoResults<RedisGeoCommands.GeoLocation<String>> radius = redisTemplate.opsForGeo().radius(CITY, circle, geoRadiusCommandArgs);
return radius;
}
public GeoResults radiusByMember() {
Circle circle = new Circle(116.418038,39.919790,Metrics.KILOMETERS.getMultiplier());
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
.newGeoRadiusArgs()
.includeDistance()
.includeCoordinates()
.sortDescending();
GeoResults<RedisGeoCommands.GeoLocation<String>> tian = redisTemplate.opsForGeo().radius(CITY, "天安门", new Distance(50), args);
return tian;
}
}
测试结果
五. BitMap
1. 面试题
网站日活统计,连续签到打卡,最近一周日活统计,统计指定用户一年之内的登陆天数,某用户按照一年365天,哪天登录过,哪几天没登陆,全年中的登陆天数。
2. BitMap使用
- 设置位
SETBIT key offset value
key: 位图的键名。
offset: 位图中的偏移量(从0开始)。
value: 要设置的值,可以是 0 或 1。
- 获取位
GETBIT key offset
- 统计位
BITCOUNT key [start end]
# 使用 BITCOUNT 命令统计位图中值为 1 的位的数量。
- 位运算
BITOP operation destkey key [key ...]
operation: 要执行的位运算,可以是 AND、OR、XOR、NOT 中的一种。
destkey: 结果存放的键名。
key [key …]: 参与运算的位图键名。
bitmap的案例后面结合布隆过滤器来讲解