> 文章列表 > redis 七. bitmap, hyperloglog, GEO 简单命令及应用场景

redis 七. bitmap, hyperloglog, GEO 简单命令及应用场景

redis 七. bitmap, hyperloglog, GEO 简单命令及应用场景

目录

  • 一 概述为什么出现这几个数据类型
  • 二. bitmap
      • bitmap 底层编码问题
      • 根据 "STRLEN"命令了解 bitmap 的扩容
      • 使用场景
        • bitop 与 bitcount使用案例
  • 二. hyperloglog
      • 使用场景
  • 三. GEO
      • 命令
        • GEOADD命令使用示例,与GEO数据类型问题
        • GEOPOS命令使用示例
        • GEOHASH 命令使用示例
        • GEODIST 命令使用示例
        • GEORADIUS 命令使用示例
        • GEORADIUSBYMEMBER 命令使用示例
      • 案例

一 概述为什么出现这几个数据类型

  1. 亿级数据收集统计流程

数据收集
数据清洗
数据统计

  1. 亿级数据收集统计面临的问题

高并发情况,能不能存的下
亿级数据存储完成后,怎么能方便的随时,瞬间取出
多条件多维度统计速度

  1. 常见统计的类型

聚合统计: 统计多个集合元素的聚合结果,也就是交,差,并等集合统计,例如set类型的交集差集,并集等集合运算
排序统计: list类型,zset(推荐),热点评论等
二值统计: 什么是二值统计,既至于两个值 true\\false,签到,上下班打卡,使用bitmap(zset也可以但是要考虑百万千万级别)
基数统计: 去重,例如统计一个集合中不重复的元素,使用hyperloglog

  1. 使用场景(亿级数据收集统计)

统计每天的新增用户,跟第二天的留存用户
商品评论中,统计评论列表中的最新评论(统计+分页)
签到打卡,统计一个月内连续打卡用户
统计网页独立访客的UniqueVistor UV量

  1. 几个专业名词解释

UV: 独立访客Unique Visitor 一般理解为客户端ip,假设两个不同ip一天内都访问了某个网站,则说有两个uv,多次访问去重
PV: 页面浏览量Pae View 假设两个用户分别访问了某个网站3次,则说6个PV
DAU: 日活跃用户量Daily Active User,常用于网站登录(日活),产品的点击量,或者运营情况,去重的
MAU: 月活跃用户量Monthly Active User

二. bitmap

  1. bitmap 是什么: 我们可以称为位图,用string类型作为底层数据结构实现的一种统计二值状态的数据类型(位图本质是数组,基于String数据类型的按位操作,该数组由多个二进制位组成,每个二进制位对应一个偏移量,我们可以称为一个索引或位格),bitmap 最大位数是2^32位,可以极大的节约存储空间,使用512m内存就可以存储42.9亿字节信息(2 ^32=4294967296)
    在这里插入图片描述
  2. 总结什么是bitmap: 由0和1状态表现的二进制位的bit数组,节省存储空间,存的快,取的快, 不适合亿级基数统计如果需要统计的有一亿个基数,大约需要100000000/8/1024/1024=12m,如果有1w个亿级比如热搜排行榜,热点视频等,就需要将近120G,所以不适合,注意点bitmap是精确的
  3. 简单命令示例
//1.setbit key offset value
//setbit 键 偏移位(偏移量是从0开始算) value只能是0或1//3.获取指定key的指定偏移量对应的value
getbit key offect//4.获取指定key的 value为1的个数
getcount key//5.统计该key中为1的个数
bitcount key//6.获取key1与key2两个key中相同偏移量都是1的个数(destkey 自定义表示"目的key"下方有示例)
bitop and destkey key1 key2
//7.与上面的命令配合使用,获取都是1的偏移量(destkey 自定义与上方的对应表示"目的key"下方有示例)
bitcount destkey

bitmap 底层编码问题

  1. 通过执行"type key"命令查看到bitmap底层实际使用的是String类型
    redis 七. bitmap, hyperloglog, GEO 简单命令及应用场景
  2. 通过redis的"get key"命令获取bitmap的类型数据时返回却是乱码
    在这里插入图片描述
  3. 原因是bitmap 底层是String类型,但是使用的是ascii进行编码的,可以使用"get key"命令,但是返回的是ascii表对应的值

根据 "STRLEN"命令了解 bitmap 的扩容

  1. bitmap 底层使用string字符串,可以使用"strlen key"命令来统计长度,但是要注意该命令按照字节来统计,在bitmap中一个偏移量占用一位,每八位一个byte,
  2. 查看下图发现首先执行"setbit k1 01"添加数据占用1位,然后执行"strlen"获取返回1,再添加>8位获取长度都是1,当执行了"setbit k1 9 1",添加k1的位数>8位(前面未添加的补0),再去获取长度返回2
  3. 一年365天全部签到占多少个字节365/8=46
  4. 总结bitmap每超过8位扩容一次,一次扩容增加8位也就是1个byte
    在这里插入图片描述

使用场景

  1. bitmap 使用场景

日活统计
连续打卡签到
最近一周活跃用户
统计指定用户一年之内的登入天数
某用户365天哪天登录过

  1. 分析签到统计,如果少用户量可以通过mysql实现,如果大数据量,千万级,使用redis的bitmap, 没给用户一天签到用1个bit位,一年也就365个, key为"签到模块+用户id+日期", 签到就存储为1(假设按照年存储用户签到情况,365天需要365/8约等于46bit大小,1000w用户一年需要44mb就可以)
    在这里插入图片描述
    在这里插入图片描述
  2. 假设亿级用户每天使用1个亿bitmap,占用128m的内存(10^8/8/1024/1024),10天需要120MB,实际使用中Bitmap通常会设置过期时间,让redis自动删除不需要的内存开销

bitop 与 bitcount使用案例

  1. 场景案例: 获取连续两天都签到的用户
  1. 注册用户与偏移量进行关系映射:例如0对应用户id0001,1对应用户id0002
  2. 使用bitmap 存储,key为年月日例如"20210515",签到就进行存储
  3. 使用bitop 命令获取连续两个key都为1的偏移量个数
  4. 使用 bitcount+"目的key"命令获取对应的偏移量
  5. 通过映射表拿到偏移量对应的用户id
    在这里插入图片描述

二. hyperloglog

  1. 什么是hyperloglog: 去重统计功能的基数估算发就是hyperloglog,
  2. 特点:在输入元素的数量或体积非常大时,计算基数所需要的空间总是固定的,并且是很小的,每个hyperloglog键需要12kb内存可以计算2^64个不同元素的基数,这和基数基数元素越多耗费内存越多的集合形成鲜明对比,但是hyperloglog只会根据元素来计算基数,不会存储元素本身,所以不能像其它类型一样返回各个元素
  3. hyperloglog是概率算法,是牺牲准确率换区空间的,对于对精度要求不高的情况下可以使用,因为概率算法本身不直接存储数据本身,能保证误差在一定范围内,又不占用空间,误差在0.81%左右(1.04/sqrt(m)) m表示redis使用寄存器的个数16384个等于0.81%
  4. 思考: 上面学过bitmap,学过set为什么不用这两个数据类型来统计基数,因为这里考虑的是大数据量千万级或亿级情况下,

bitmap用bit数组来表示元素是否出现,每个元素对应一位,需要n个bit,基数统计只需要获取存在的bit数组和新加入的元素按位计算即可,能够减少内存占用迅速响应,但是:如果需要统计的有一亿个基数,大约需要100000000/8/1024/1024=12m,如果有1w个亿级比如热搜排行榜,热点视频等,就需要将近120G,所以不适合,注意点bitmap是精确的
为什么不用redis的hash类型: 按照ipv4结构来说,ipv4一个地址最多占15个字节,像京东天猫这种每天访问量在亿级以上1.5亿字节=2g会打爆redis

  1. 小总结: hyperloglog: 只进行不重复基数统计,不是集合,也不保存数据本身,值记录数量,牺牲准确率换取空间,误差在0.81%左右
  2. hyperloglog 命令
//1.添加,var可以看为是key,element为对应该key的数据可以为空,可以是多个
PFADD var element element....emelent//2.获取该var对应数据的个数(去重的)
PFCOUNT var//3.合并多个var到新key dst自定义
PFMERGE dst var1 var2....varN
//4.获取上面dst中保存的个数
PFCOUNT dst

使用场景

  1. 统计某个网站的UV,统计某个文章的UV,例如统计京东的UV量, key为"jd:uv日期",后面的1,2,3,假设是访问用户的ip,黄色部分计算jd日期为uv1的访问量返回3,蓝色部分计算uv1,uv2两天的uv量量返回6(因为uv1与与uv2中有重复的)
    在这里插入图片描述
  2. 统计一个网站uv 代码
package com.redis.test.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
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 java.time.LocalDate;
import java.time.format.DateTimeFormatter;@Api(description = "获取网站UV的Redis统计方案")
@RestController
public class HyperLogLogController {@Autowiredprivate RedisTemplate redisTemplate;private static final String uvKey = "uv:";@ApiOperation("获得ip去重复后的首页访问量,总数统计")@RequestMapping(value = "/getUV", method = RequestMethod.GET)public long getUV(String date) {return redisTemplate.opsForHyperLogLog().size(uvKey+date);}@ApiOperation("记录用户访问uv")@RequestMapping(value = "/addUV", method = RequestMethod.GET)public Integer addUV(String ip) {DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");String timeStr = LocalDate.now().format(dateTimeFormatter);redisTemplate.opsForHyperLogLog().add(uvKey + timeStr, ip);return 0;}
}
  1. 用户搜索关键词的数量
  2. 统计用户每天搜索的不同词条数

三. GEO

  1. 需求: 附近的人,打车软件附近的车辆,附近的店铺等等,经度, 纬度(-90, 90)跟地理位置相关的
  2. 获取举例我500米内的车辆需求,传统模式下,例如使用MySql

高并发下性能问题
查询的是一个范围矩形访问,而不是以我为中心r公里为半径的原型访问
精准度问题,我们要的是一个地球坐标,这种矩形计算在长距离上会有很大误差

  1. 核心点: 将三维的地球转换为二维的坐标,将二维的坐标转换为一维的点块,最终将一维的点块转换为二进制base32编码存储
  2. 简单解释经纬度:

经度:(-180, 180),东经为正数,西经为负数
纬度:(-90, 90),南纬为正数,北纬为负数

命令

//1.添加经纬度坐标
GEOADD
//2.返回经纬度
GEOPOS
//3.返回坐标的geohash表示
GEOHASH
//4.两个位置之间的举例
GEODIST
//5.以给定的经纬度为中心,返回与该经纬度距离不超过给定距离的所有位置元素
GEORADIUS
//6.以给定的元素为中心(元素可能重复,假设存储时 经纬度 天安门,这个天安门就是元素值)
//获取距离该元素不超过给定范围的元素
GEORADIUSBYMEMBER

GEOADD命令使用示例,与GEO数据类型问题

  1. 插入天安门,故宫,长城经纬度,并通过示例与"type key"命令查看存储的GEO数据类型为ZSET,然后使用ZSET的命令获取该key值"ZRANGE key start stop",返回的可能是乱码,需要执行" – raw"命令后,再次执查询会返回(“天安门 故宫 长城”)
    在这里插入图片描述
  2. 可以这样理解zset类型是"key 分数 值"而 geo类型是 “key 经纬度 值”

GEOPOS命令使用示例

  1. 获取天安门,故宫,长城经纬度
    在这里插入图片描述

GEOHASH 命令使用示例

  1. 获取天安门的hash编码
    redis 七. bitmap, hyperloglog, GEO 简单命令及应用场景

GEODIST 命令使用示例

  1. 计算两个位置的举例
    在这里插入图片描述

GEORADIUS 命令使用示例

  1. 以给定的经纬度为中心,返回键包含的位置元素当中,与该键距离不超过给定距离的所有位置元素
  2. 其中有几个附加命令
    redis 七. bitmap, hyperloglog, GEO 简单命令及应用场景
  3. 示例:查看距离我不超过10mk的10个地标建筑
    在这里插入图片描述

GEORADIUSBYMEMBER 命令使用示例

在这里插入图片描述

案例

import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
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 java.util.HashMap;
import java.util.List;
import java.util.Map;@RestController
public class GeoController {public  static final String CITY ="city";@Autowiredprivate RedisTemplate redisTemplate;@ApiOperation("新增城市经纬度坐标")@RequestMapping(value = "/geoadd",method = RequestMethod.POST)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();}/*** * @param member 地理位置名称* @return*/@ApiOperation("获取地理位置的坐标")@RequestMapping(value = "/geopos",method = RequestMethod.GET)public Point position(String member) {//获取经纬度坐标List<Point> list= this.redisTemplate.opsForGeo().position(CITY,member);return list.get(0);}@ApiOperation("geohash算法生成的base32编码值")@RequestMapping(value = "/geohash",method = RequestMethod.GET)public String hash(String member) {//geohash算法生成的base32编码值List<String> list= this.redisTemplate.opsForGeo().hash(CITY,member);return list.get(0);}@ApiOperation("计算两个位置之间的距离")@RequestMapping(value = "/geodist",method = RequestMethod.GET)public Distance distance(String member1, String member2) {Distance distance= this.redisTemplate.opsForGeo().distance(CITY,member1,member2, RedisGeoCommands.DistanceUnit.KILOMETERS);return distance;}/*** 通过经度,纬度查找附近的* 北京王府井位置116.418017,39.914402,这里为了方便讲课,故意写死*/@ApiOperation("通过经度,纬度查找附近的")@RequestMapping(value = "/georadius",method = RequestMethod.GET)public GeoResults radiusByxy() {//这个坐标是北京王府井位置Circle circle = new Circle(116.418017, 39.914402, Metrics.MILES.getMultiplier());//返回50条RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(10);GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(CITY,circle, args);return geoResults;}/*** 通过地方查找附近*/@ApiOperation("通过地方查找附近")@RequestMapping(value = "/georadiusByMember",method = RequestMethod.GET)public GeoResults radiusByMember() {String member="天安门";//返回50条RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(10);//半径10公里内Distance distance=new Distance(10, Metrics.KILOMETERS);GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(CITY,member, distance,args);return geoResults;}
}