文章目录
- 前言
- [一、Bitmap 位图:极致节省空间的二元状态存储](#一、Bitmap 位图:极致节省空间的二元状态存储)
-
- [1. 底层原理](#1. 底层原理)
- [2. 核心实操命令](#2. 核心实操命令)
- [3. 适用场景](#3. 适用场景)
- [4. 踩坑总结](#4. 踩坑总结)
- 二、HyperLogLog:亿级去重统计神器
-
- [1. 底层原理](#1. 底层原理)
- [2. 核心实操命令](#2. 核心实操命令)
- [3. 适用场景](#3. 适用场景)
- [4. 踩坑总结](#4. 踩坑总结)
- [三、Geospatial(GEO) 地理坐标类型](#三、Geospatial(GEO) 地理坐标类型)
-
- [1. 底层原理](#1. 底层原理)
- [2. 核心实操命令](#2. 核心实操命令)
- [3. 适用场景](#3. 适用场景)
- [4. 踩坑总结](#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:位图位运算,统计多天连续签到用户
实例运行:
redis
# ========== 第一组: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) 2
3. 适用场景
用户签到打卡、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,统计周期总访客
实例运行:
redis
# 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) 5
3. 适用场景
网站独立访客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:查询指定范围内所有地点
实例运行:
redis
# ===================== 一、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多模拟线上大数据量场景,才能直观感受到不同结构的内存差距。