Redis系列-8.Redis案例实战之Bitmap、Hyperloglog、GEO(上):https://developer.aliyun.com/article/1414702
GEO
经典面试题
面试题说明:
移动互联网时代LBS应用越来越多,交友软件中附近的小姐姐、外卖软件中附近的美食店铺、打车软件附近的车辆等等。
那这种附近各种形形色色的XXX地址位置选择是如何实现的?
会有什么问题呢?
1.查询性能问题,如果并发高,数据量大这种查询是要搞垮mysql数据库的
2.一般mysql查询的是一个平面矩形访问,而叫车服务要以我为中心N公里为半径的圆形覆盖。
3.精准度的问题,我们知道地球不是平面坐标系,而是一个圆球,这种矩形计算在长距离计算时会有很大误差,mysql不合适
地理知识说明
经纬度
经度与纬度的合称组成一个坐标系统。又称为地理坐标系统,它是一种利用三度空间的球面来定义地球上的空间的球面坐标系统,能够标示地球上的任何一个位置。
经线和纬线
是人们为了在地球上确定位置和方向的,在地球仪和地图上画出来的,地面上并线。
和经线相垂直的线叫做纬线(纬线指示东西方向)。纬线是一条条长度不等的圆圈。最长的纬线就是赤道。
因为经线指示南北方向,所以经线又叫子午线。 国际上规定,把通过英国格林尼治天文台原址的经线叫做0°所以经线也叫本初子午线。在地球上经线指示南北方向,纬线指示东西方向。
东西半球分界线:东经160° 西经20°
经度和维度
经度(longitude):东经为正数,西经为负数。东西经
纬度(latitude):北纬为正数,南纬为负数。南北纬
命令
GEOPOS添加经纬度坐标
命令如下:
GEOADD city 116.403963 39.915119 “天安门” 116.403414 39.924091 “故宫” 116.024067 40.362639 “长城”
解决中文乱码
GEOPOS返回经纬度
GEOHASH返回坐标的geohash表示
geohash算法生成的base32编码值
GEODIST两个位置之间距离
GEORADIUS
以半径为中心,查找附近的
georadius 以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
GEORADIUS city 116.418017 39.914402 10 km withdist withcoord count 10 withhash desc
GEORADIUS city 116.418017 39.914402 10 km withdist withcoord count 10 desc
WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。 WITHCOORD: 将位置元素的经度和维度也一并返回。 WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大 COUNT 限定返回的记录数。
GEORADIUSBYMEMBER
美团地图位置附近的酒店推送
需求分析
高德地图附近的人或者一公里以内的各种营业厅、加油站、理发店、超市…
找个单车
架构设计
编码实现
GeoService
@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
大厂真实面试题案例
日活统计
连续签到打卡
最近一周的活跃用户
统计指定用户一年之中的登陆天数
某用户按照一年365天,那几天登陆过?那几天没登录?全年中登录的天数共计多少?
是什么?
说明:用String类型作为底层数据结构实现的一种统计二值状态的数据类型
位图本质是数组,它是基于String数据类型的按位的操作。该数组由多个二进制位组成,每个二进制位都对应一个偏移量(我们可以称之为一个索引或者位格)。Bitmap支持的最大位数是232位,它可以极大的节约存储空间,使用512M内存就可以存储多大42.9亿的字节信息(232 = 4294967296)
能干嘛
用于状态统计
判断用户是否登录过,比如京东每日签到送京豆
电影、广告是否被点击播放过
钉钉打开上下班,签到统计
京东签到领京东豆
签到日历仅展示当月签到数据
签到日历需展示最近连续签到天数
假设当前日期是20210618,且20210616未签到
若20210617已签到且0618未签到,则连续签到天数为1
若20210617已签到且0618已签到,则连续签到天数为2
连续签到天数越多,奖励越大
所有用户均可签到
截至2020年3月31日的12个月,京东年度活跃用户数3.87亿,同比增长24.8%,环比增长超2500万,此外,2020年3月移动端日均活跃用户数同比增长46%假设10%左右的用户参与签到,签到用户也高达3千万。。。。。。o(╥﹏╥)o
小厂方法,传统mysql方式
建表
CREATE TABLE user_sign ( keyid BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT, user_key VARCHAR(200),#京东用户ID sign_date DATETIME,#签到日期(20210618) sign_count INT #连续签到天数 ) INSERT INTO user_sign(user_key,sign_date,sign_count) VALUES ('20210618-xxxx-xxxx-xxxx-xxxxxxxxxxxx','2020-06-18 15:11:12',1); SELECT sign_count FROM user_sign WHERE user_key = '20210618-xxxx-xxxx-xxxx-xxxxxxxxxxxx' AND sign_date BETWEEN '2020-06-17 00:00:00' AND '2020-06-18 23:59:59' ORDER BY sign_date DESC LIMIT 1;
方法正确但是难以落地实现,o(╥﹏╥)o。
签到用户量较小时这么设计能行,但京东这个体量的用户(估算3000W签到用户,一天一条数据,一个月就是9亿数据)
对于京东这样的体量,如果一条签到记录对应着当日用记录,那会很恐怖…
如何解决这个痛点?
1 一条签到记录对应一条记录,会占据越来越大的空间。
2 一个月最多31天,刚好我们的int类型是32位,那这样一个int类型就可以搞定一个月,32位大于31天,当天来了位是1没来就是0。
3 一条数据直接存储一个月的签到记录,不再是存储一天的签到记录。
大厂方法,基于Redis的Bitmap实现签到日历
在签到统计时,每个用户一天的签到用1个bit位就能表示,
一个月(假设是31天)的签到情况用31个bit位就可以,一年的签到也只需要用365个bit位,根本不用太复杂的集合类型
命令
setbit
setbit key offset value
setbit键 偏移位 0或1
Bitmap的偏移量是从零开始算的
getbit
getbit key offset
setbit和getbit案例说明
按年去存储一个用户的签到情况,365 天只需要 365 / 8 ≈ 46 Byte,1000W 用户量一年也只需要 44 MB 就足够了。
假如是亿级的系统,
每天使用1个1亿位的Bitmap约占12MB的内存(10^8/8/1024/1024),10天的Bitmap的内存开销约为120MB,内存压力不算太高。在实际使用时,最好对Bitmap设置过期时间,让Redis自动删除不再需要的签到记录以节省内存开销。
strlen
统计字节数占用多少
bitcount
全部键里面含有1的有多少个?
一年365天,全年天天登录占用多少字节
bitop
连续2天都签到的用户
加入某个网站或者系统,它的用户有1000W,做个用户id和位置的映射
比如0号位对应用户id:uid-092iok-lkj
比如1号位对应用户id:uid-7388c-xxx