文章目录
- 前言
- 一、Bitmap 位图:极致节省空间的二元状态存储
- 1. 底层原理
- 2. 核心实操命令
- 3. 适用场景
- 4. 踩坑总结
- 二、HyperLogLog:亿级去重统计神器
- 1. 底层原理
- 2. 核心实操命令
- 3. 适用场景
- 4. 踩坑总结
- 三、Geospatial(GEO) 地理坐标类型
- 1. 底层原理
- 2. 核心实操命令
- 3. 适用场景
- 4. 踩坑总结
- 四、三大特殊类型完整总结
- 整体学习感悟
前言
Redis藏了三个专门适配特殊业务的新数据类型:Bitmap、HyperLogLog、Geospatial,让我们继续学习
提示:以下是本篇文章正文内容,下面案例可供参考
一、Bitmap 位图:极致节省空间的二元状态存储
1. 底层原理
Bitmap并不是独立的数据结构,底层依托String实现,操作的是字符串里每一个二进制bit位,一个bit只有0、1两种值,完美适配“是/否”类业务状态。
举个对比案例:千万用户签到场景
- MySQL方案:每条签到记录一条数据,海量数据占用大量磁盘,查询缓慢;
- Bitmap方案:一个用户单月签到仅占用31bit,百万用户存储只需要几十MB,性能碾压传统方案。
上限:单个Bitmap最大512M,对应2^32个bit,日常业务完全够用。
2. 核心实操命令
SETBIT key offset 0/1:指定下标存入状态(offset从0开始)
示例:setbit sign:user1 0 1用户1当月1号签到GETBIT key offset:查询指定位置bit值BITCOUNT key:统计值为1的bit总数(统计签到天数)BITOP and/or/xor dest key1 key2:位图位运算,统计多天连续签到用户
实例运行:
# ========== 第一组:Bitmap基础命令实操 ========== # setbit key 偏移量 0/1:设置指定bit位的值,返回修改前该位置的旧值 # bit1的第0位设为1,之前是空默认0,返回0 127.0.0.1:6379> setbit bit1 0 1 (integer) 0 # bit1的第1位设为1,旧值0 127.0.0.1:6379> setbit bit1 1 1 (integer) 0 # bit1的第3位设为1,跳过2号位,旧值0 127.0.0.1:6379> setbit bit1 3 1 (integer) 0 # bit1的第5位设为1,旧值0 127.0.0.1:6379> setbit bit1 5 1 (integer) 0 # bit1的第7位设为1,旧值0 127.0.0.1:6379> setbit bit1 7 1 (integer) 0 # getbit key 偏移量:查询指定bit位的值 # 查询第3位,值为1 127.0.0.1:6379> getbit bit1 3 (integer) 1 # 查询第2位,从未赋值,默认0 127.0.0.1:6379> getbit bit1 2 (integer) 0 # bitfield key get uN offset:从offset开始读取N个无符号bit,转十进制 # 从0号位读取2个bit:bit0=1、bit1=1 → 二进制11=十进制3 127.0.0.1:6379> bitfield bit1 get u2 0 1) (integer) 3 # bitpos key bit值:查找第一个等于目标bit的偏移量 # 查找bit1中第一个0,位置是2 127.0.0.1:6379> bitpos bit1 0 (integer) 23. 适用场景
用户签到打卡、APP登录状态标记、海量用户黑白名单、日活用户筛选
4. 踩坑总结
offset不要直接使用超大用户ID,会造成大量空bit浪费内存;仅适合二元状态,多状态场景不适用。
二、HyperLogLog:亿级去重统计神器
1. 底层原理
专门做基数统计(统计不重复元素数量),核心优势是极致压缩内存:无论存入多少数据,单个HyperLogLog固定仅占用12KB内存,最大可统计2^64个不同元素,仅存在0.81%左右微小误差,绝大多数互联网业务可以忽略。
对比Set:存储百万独立访客,Set需要几十MB,HLL仅12KB,差距巨大。
短板:只能统计数量,无法取出存入的原始数据。
2. 核心实操命令
PFADD key 元素:添加数据,自动去重
示例:pfadd uv:20260807 u001 u002 u003记录今日访客PFCOUNT key:查询不重复元素总数(网站UV)PFMERGE newkey key1 key2:合并多个HLL,统计周期总访客
实例运行:
# pfadd key 元素:往HyperLogLog添加数据;元素首次加入返回1,重复添加无变化返回0 127.0.0.1:6379> pfadd k1 java (integer) 1 # java第一次存入k1,新增成功 127.0.0.1:6379> pfadd k1 html (integer) 1 # html第一次存入k1,新增成功 127.0.0.1:6379> pfadd k1 javascript (integer) 1 # javascript第一次存入k1,新增成功 127.0.0.1:6379> pfadd k1 java (integer) 0 # java已存在,无新增,返回0 # pfcount key:统计HLL中不重复元素的近似基数(去重总数) 127.0.0.1:6379> pfcount k1 (integer) 3 # k1去重后共3个元素:java、html、javascript # 新建HLL集合k2,存入spring 127.0.0.1:6379> pfadd k2 spring (integer) 1 # 存入mysql 127.0.0.1:6379> pfadd k2 mysql (integer) 1 # 统计k2去重数量 127.0.0.1:6379> pfcount k2 (integer) 2 # k2去重后2个元素 # pfmerge 目标key 源key1 源key2:合并多个HLL,所有去重数据存入目标key 127.0.0.1:6379> pfmerge k3 k1 k2 OK # 合并操作执行成功 # 统计合并后k3的总去重数量(3+2无重复,结果为5) 127.0.0.1:6379> pfcount k3 (integer) 53. 适用场景
网站独立访客UV统计、搜索关键词去重计数、活动参与人数统计
4. 踩坑总结
如果业务必须获取全部唯一数据,不能用HLL,优先选择Set;对数据精准度要求极高的金融场景慎用。
三、Geospatial(GEO) 地理坐标类型
1. 底层原理
Redis3.2新增地理数据类型,底层依托Zset有序集合实现,将经纬度坐标编码成score存储,原生封装地图相关计算指令,不用自己写复杂地理算法。
2. 核心实操命令
GEOADD key 经度 纬度 名称:存入地点坐标
示例:geoadd city 121.47 31.23 shanghaiGEOPOS key 地点:获取目标经纬度GEODIST key A B km/m:计算两点直线距离GEORANGE key 经度 纬度 半径 km:查询指定范围内所有地点
实例运行:
# ===================== 一、GEO地理坐标类型实操 ===================== # geoadd key 经度 纬度 地点名:添加地理坐标,返回本次新增地点数量 127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai (integer) 1 # 成功新增上海1个坐标 # 批量添加重庆、深圳、北京三个城市,返回新增总数3 127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing 114.05 22.52 shenzhen 116.38 39.90 beijing (integer) 3 # geopos key 地点:查询指定城市的经纬度 127.0.0.1:6379> geopos china:city shanghai 1) 1) "121.47000163793563843" # 经度 2) "31.22999903975783553" # 纬度 # geodist 计算两点直线距离,默认单位米(m) 127.0.0.1:6379> geodist china:city beijing shanghai "1068153.5181" # 北京到上海距离,单位米 # 追加km参数,单位切换为千米 127.0.0.1:6379> geodist china:city beijing shanghai km "1068.1535" # 北京到深圳直线距离,单位千米 127.0.0.1:6379> geodist china:city beijing shenzhen km "1945.5740" # georadius 给定经纬度+半径,查询范围内所有存储的地点 127.0.0.1:6379> georadius china:city 110 30 1000 km 1) "chongqing" 2) "shenzhen" # flushdb:清空当前数据库所有key 127.0.0.1:6379> flushdb OK # ===================== 二、Redis事务场景1:正常执行提交 ===================== # multi:开启事务,后续命令全部进入队列缓存,不会立刻执行 127.0.0.1:6379> multi OK # 命令入队,返回QUEUED标识 127.0.0.1:6379(TX)> set k1 v1 QUEUED 127.0.0.1:6379(TX)> set k2 v2 QUEUED 127.0.0.1:6379(TX)> get k1 QUEUED 127.0.0.1:6379(TX)> get k2 QUEUED # exec:提交事务,一次性串行执行队列所有命令,逐条返回执行结果 127.0.0.1:6379(TX)> exec 1) OK # set k1执行结果 2) OK # set k2执行结果 3) "v1" # get k1查询结果 4) "v2" # get k2查询结果 # 清空数据库,准备下一组测试 127.0.0.1:6379> flushdb OK # ===================== 三、Redis事务场景2:discard放弃事务 ===================== 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> set k1 v1 QUEUED 127.0.0.1:6379(TX)> set k2 v2 QUEUED 127.0.0.1:6379(TX)> get k1 QUEUED 127.0.0.1:6379(TX)> get k2 QUEUED # discard:丢弃事务队列中全部命令,不执行任何操作 127.0.0.1:6379(TX)> discard OK # 事务作废,key不存在返回nil 127.0.0.1:6379> get k1 (nil) 127.0.0.1:6379> get k2 (nil) 127.0.0.1:6379> flushdb OK # ===================== 四、Redis事务场景3:入队阶段语法错误,整体作废 ===================== 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> set k1 v1 QUEUED 127.0.0.1:6379(TX)> set k2 v2 QUEUED # set语法错误(缺少value参数),入队直接报错 127.0.0.1:6379(TX)> set k3 (error) ERR wrong number of arguments for 'set' command 127.0.0.1:6379(TX)> get k1 QUEUED 127.0.0.1:6379(TX)> mget k2 k3 QUEUED # 只要入队时有语法错误,exec直接拒绝执行整个事务,全部失效 127.0.0.1:6379(TX)> exec (error) EXECABORT Transaction discarded because of previous errors 127.0.0.1:6379> flushdb OK # ===================== 五、Redis事务场景4:入队无错、执行时报错(无回滚) ===================== 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> set k1 v1 QUEUED # incr仅支持数字,存入字符串,执行阶段才会报错,入队不会报错 127.0.0.1:6379(TX)> incr k1 QUEUED 127.0.0.1:6379(TX)> set k2 v2 QUEUED 127.0.0.1:6379(TX)> get k2 QUEUED # 执行时:正确命令正常生效,出错命令单独报错,不会整体回滚(Redis事务不支持原子回滚) 127.0.0.1:6379(TX)> exec 1) OK # set k1执行成功 2) (error) ERR value is not an integer or out of range # incr执行失败 3) OK # set k2执行成功 4) "v2" # get k2查询成功3. 适用场景
外卖附近商家、同城好友、门店范围检索、打车附近车辆匹配
4. 踩坑总结
经纬度有合法区间(经度-180180,纬度-85.0585.05),超出会报错;只能计算直线距离,不包含道路路程。
四、三大特殊类型完整总结
| 数据类型 | 底层依托 | 核心优势 | 局限性 | 核心业务场景 |
|---|---|---|---|---|
| Bitmap | String | 占用空间极小,读写速度快 | 仅支持0/1二元状态 | 用户签到、用户状态标记 |
| HyperLogLog | String | 12KB固定内存,海量去重统计 | 无法取出原始数据,存在微小误差 | UV独立访客、去重计数 |
| Geospatial | ZSet | 内置地理计算,无需自研算法 | 仅直线距离,坐标区间有限 | 附近门店、同城匹配 |
整体学习感悟
- Redis每种数据结构都是为特定业务量身设计,没有万能类型,选型优先贴合业务需求;
- 三大新类型都是底层复用已有基础结构,Redis设计极度精简,没有冗余代码;
- 开发选型思路:
- 二元状态统计 → Bitmap
- 海量数据去重计数,不查明细 → HyperLogLog
- 经纬度、范围、距离计算 → G
- 实操小建议:平时写Demo多模拟线上大数据量场景,才能直观感受到不同结构的内存差距。