Redis(高阶篇)05章——案例落地实战bitmap/hyperloglog/GEO
一、先看看大厂真实需求+面试题反馈
(1)面试题1
- 抖音电商直播,主播介绍的商品有评论,1个商品对应了一系列的评论,排序+展现+取前10条记录
- 用户在手机APP上的签到打卡信息:1天对应一系列用户的签到记录,新浪微博、钉钉打卡签到,来没来如何统计
- 应用网站上的网页访问信息:一个网页对应一系列的访问点击,淘宝网首页,每天有多少人浏览首页
-
公司系统上线后,说一下UV、PV、DAU分别是多少?
(2)面试题2
(3)需求痛点
- 亿级数据的收集+清洗+统计+展现
- 一句话:存的进+去得快+多维度展现
- 真正有价值的是统计
二、统计的类型有哪些
(1)聚合统计
- 统计多个集合元素的聚合结果,就是前面讲解过的交差并等集合统计
- 复习命令
- 交差并集和聚合函数的应用
(2)排序统计
- 抖音短视频最新评论留言的场景,请你设计一个展现列表。(考察数据结构和设计思路)
- 设计案例和回答思路:
- answer:
- zset
- 在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,建议使用ZSet
- zset
(3)二值统计
- 集合元素的取值就只有0和1两种。在钉钉上签到打卡的场景中,我们只用记录有签到(1)或没有签单(0)
- 见bitmap
(4)基数统计
- 指统计一个集合中不重复的元素个数
- 见hyperloglog
三、hyperloglog
(1)说名词,行话谈资
3.1.1什么是UV
- Unique Visitor,独立访客,一般理解为客户端IP
- 需要去重考虑
3.1.2什么是PV
- Page View,页面浏览量
- 不用去重
3.1.3什么是DAU
- Daily Active User,日活跃量用户,登录或者使用了某个产品的用户数(去重复登录的用户)
- 常用于反映网站、互联网应用或者网络游戏的运营情况
3.1.4什么是MAU
Monthly Active User,月活跃用户量
(2)看需求
(3)是什么(小白篇讲解过,快速复习一下)
3.3.1基数
- 是一种数据集,去重复后的真实个数
- 案例Case
3.3.2取重复统计功能的基数估算算法-就是HyperLogLog
3.3.3基数统计
用于统计一个集合中不重复的元素个数,就是对集合去重复后剩余元素的计算
3.3.4一句话
一句话:脱水后的真实数据
3.3.5基本命令


(4)HyperLogLog如何做的?如何演化出来的?
3.4.1基数统计就算HyperLogLog
3.4.2去重复统计你先会想到哪些方式?
(1)HashSet
(2)bitmap
(3)结论
- 样本元素越多内存消耗急剧增大,难以管控+各种慢,对于亿级统计不太合适
- 量变引起质变
(4)办法
3.4.3原理说明
(1)只是进行不重复的基数统计,不是集合也不保存数据,只记录数量而不是具体内容
(2)有误差
- HyperLogLog提供不精确的去重计数方案
- 只牺牲准确率来换取空间,误差仅仅只是0.81%左右
(3)这个误差率如何来的?
- Redis new data structure: the HyperLogLog - <antirez>
- Redis之父安特雷兹回答:
(5)淘宝网站首页亿级UV的Redis统计方案
3.5.1需求
- UV的统计需要去重,一个用户一天内的多次访问只能算作一次
- 淘宝、天猫首页的UV,平均每天是1~1.5个亿
- 每天存1.5个亿的IP,访问者来了后先去查是否存在,不存在就加入
3.5.2方案讨论
- 用MySQL,×
- 用redis的hash结构存储
- HyperLogLog
3.5.3HyperLogLogService
package com.atguigu.redis.service;
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;
@Service
@Slf4j
public class HyperLogLogService
{
@Resource
private RedisTemplate redisTemplate;
/**
* 模拟后台有用户点击首页,每个用户来自不同ip地址
*/
@PostConstruct
public void init()
{
log.info("------模拟后台有用户点击首页,每个用户来自不同ip地址");
new Thread(() -> {
String ip = null;
for (int i = 1; i <=200; i++) {
Random r = new Random();
ip = r.nextInt(256) + "." + r.nextInt(256) + "." + r.nextInt(256) + "." + r.nextInt(256);
Long hll = redisTemplate.opsForHyperLogLog().add("hll", ip);
log.info("ip={},该ip地址访问首页的次数={}",ip,hll);
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
}
},"t1").start();
}
}
3.5.4HyperLogLogController
package com.atguigu.redis.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
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;
@Api(description = "淘宝亿级UV的Redis统计方案")
@RestController
@Slf4j
public class HyperLogLogController
{
@Resource
private RedisTemplate redisTemplate;
@ApiOperation("获得IP去重后的首页访问量")
@RequestMapping(value = "/uv",method = RequestMethod.GET)
public long uv()
{
//pfcount
return redisTemplate.opsForHyperLogLog().size("hll");
}
}
四、GEO
(1)Redis之GEO
4.1.1大厂面试题简介
4.1.2地理知识说明
4.1.3如何获得某个地址的经纬度
4.1.4命令复习第二次
- GEOADD 添加经纬度坐标
- GEOPOS返回经纬度
- GEOHASH返回坐标的geohash表示
- GEODIST两个位置之间的距离
- GEORADIUS,以半径为中心,查找附近的XXX
- GEORADIUSBYMEMBER
(2)美团地图位置附近的酒店推送
4.2.1需求分析
4.2.2架构设计
- Redis的新类型GEO
- 命令:http://www.redis.cn/commands/geoadd.html
4.2.3编码实现
- 关键点
- GeoController
package com.atguigu.redis7.controller; import com.atguigu.redis7.service.GeoService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.data.geo.*; import org.springframework.data.redis.connection.RedisGeoCommands; import org.springframework.web.bind.annotation.GetMapping; 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; import java.util.HashMap; import java.util.List; import java.util.Map; @Api(tags = "美团地图位置附近的酒店推送GEO") @RestController @Slf4j public class GeoController { @Resource private GeoService geoService; @ApiOperation("添加坐标geoadd") @RequestMapping(value = "/geoadd",method = RequestMethod.GET) public String geoAdd() { return geoService.geoAdd(); } @ApiOperation("获取经纬度坐标geopos") @RequestMapping(value = "/geopos",method = RequestMethod.GET) public Point position(String member) { return geoService.position(member); } @ApiOperation("获取经纬度生成的base32编码值geohash") @RequestMapping(value = "/geohash",method = RequestMethod.GET) public String hash(String member) { return geoService.hash(member); } @ApiOperation("获取两个给定位置之间的距离") @RequestMapping(value = "/geodist",method = RequestMethod.GET) public Distance distance(String member1, String member2) { return geoService.distance(member1,member2); } @ApiOperation("通过经度纬度查找北京王府井附近的") @RequestMapping(value = "/georadius",method = RequestMethod.GET) public GeoResults radiusByxy() { return geoService.radiusByxy(); } @ApiOperation("通过地方查找附近,本例写死天安门作为地址") @RequestMapping(value = "/georadiusByMember",method = RequestMethod.GET) public GeoResults radiusByMember() { return geoService.radiusByMember(); } }
- GeoService
package com.atguigu.redis7.service; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; import org.springframework.data.geo.Circle; import org.springframework.data.redis.connection.RedisGeoCommands; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import java.util.HashMap; import java.util.List; import java.util.Map; @Service @Slf4j 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)); redisTemplate.opsForGeo().add(CITY,map); return map.toString(); } public Point position(String member) { //获取经纬度坐标 List<Point> list= this.redisTemplate.opsForGeo().position(CITY,member); return list.get(0); } public String hash(String member) { //geohash算法生成的base32编码值 List<String> list= this.redisTemplate.opsForGeo().hash(CITY,member); return list.get(0); } public Distance distance(String member1, String member2) { //获取两个给定位置之间的距离 Distance distance= this.redisTemplate.opsForGeo().distance(CITY,member1,member2, RedisGeoCommands.DistanceUnit.KILOMETERS); return distance; } public GeoResults radiusByxy() { //通过经度,纬度查找附近的,北京王府井位置116.418017,39.914402 Circle circle = new Circle(116.418017, 39.914402, Metrics.KILOMETERS.getMultiplier()); //返回50条 RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(50); GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(CITY,circle, args); return geoResults; } public GeoResults radiusByMember() { //通过地方查找附近 String member="天安门"; //返回50条 RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(50); //半径10公里内 Distance distance=new Distance(10, Metrics.KILOMETERS); GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(CITY,member, distance,args); return geoResults; } }
五、bitmap
(1)大厂真实面试题案例
- 日活统计
- 连续签到打卡
- 最近一周的活跃用户
- 统计指定用户一年之中的登录天数
- 某用户按照一年365天,哪几天登陆过,哪几天没有登录?
(2)是什么
- 图示:
- 一句话:由0和1状态表现的二进制位的bit数组
(3)能干嘛
5.3.1用于状态统计
用于状态统计,Y、N类似AtomicBoolean
5.3.2看需求
- 用户是否登录过Y、N,比如京东每日签到送京豆
- 电影、广告是否被点击播放过
- 钉钉打卡上下班,签到统计
(4)京东签到领取京豆
5.4.1需求说明
5.4.2小厂方法,传统mysql方式
- 建表SQL
- 困难和解决思路
5.4.3大厂方法,基于Redis的Bitmap实现签到日历
(5)命令复习,第二次
- setbit:
- setbit key offset value
- set bit 键 偏移位 只能0或1
- Bitmap的偏移量是从0开始算的
- setbit key offset value
- getbit:get bit key offset
- setbit和getbit案例说明:
- 按照天:
- 按照年:
- 按照天:
- bitmap的底层编码说明,get命令操作如何:
- 实质是二进制的ascii编码对应
- redis里用type命令看看bitmap实质是什么类型
- man ascii
- 设置命令
- strlen:统计字节数占用多少
- bitcount:
- 全部键里面含1的有多少个?
- 一年365天,全年天天登录占用多少字节?
- 全部键里面含1的有多少个?
- bitop: