Redis Bitmap:BitCount、bitTop的使用业务场景

前言

日常后端开发中,Redis Bitmap 是海量数据签到、日活统计、用户状态标记的神器,极致节省内存:1亿用户仅需要12.5MB内存,没有任何中间件能打。

但是绝大多数开发者都会踩一个致命大坑:误以为 BITCOUNT key start end 中的 start、end 是位偏移量,实际上官方定义是【字节偏移量】

很多人觉得这个设计反人类、没用,实则恰恰相反------这个特性是为按天存储、按周/按月区间统计量身定做的,适配签到、日活这类高频业务。今天一文讲透原理、踩坑案例、两种业务存储方案、线上最佳实践。


一、核心结论(先记死,避免线上bug)

  • BITCOUNT 不带参数:统计整个bitmap所有二进制位中1的总数

  • BITCOUNT key start end :start和end单位是字节(Byte),不是位(bit)

  • 换算关系:1字节 = 8个二进制位

  • 索引规则:支持负数索引,-1代表最后一个字节,-2代表倒数第二个字节,闭区间统计(包含首尾字节)

高频线上bug:业务想统计第10位~第20位的签到人数,直接填10 20,最终统计范围完全错误,导致报表数据彻底失真。


二、极简实操案例,直观看懂差异

1、初始化bitmap数据

redis 复制代码
# 第0位设置为1
SETBIT sign:202606 0 1
# 第8位设置为1(刚好跨一个字节)
SETBIT sign:202606 8 1

2、底层存储结构拆解

  • 第0个字节:00000001(第0位为1)

  • 第1个字节:00000001(第8位为1)

3、对比执行命令

redis 复制代码
# 统计全部字节:结果=2
BITCOUNT sign:202606

# 只统计第0个字节:结果=1
BITCOUNT sign:202606 0 0

# 只统计第1个字节:结果=1
BITCOUNT sign:202606 1 1

# 错误用法:想统计前10个位,直接填位偏移 0 10,实际统计0~10字节,数据完全错误
BITCOUNT sign:202606 0 10

看完案例就能明白:不能直接填位偏移,必须手动换算字节偏移

通用换算公式:字节偏移 = 位偏移 / 8(向下取整)


三、答疑:既然是字节偏移,为什么还要保留这个参数?

很多开发者疑惑:按字节统计很别扭,平时都是按位存用户ID,这个参数是不是鸡肋?

答案:绝对不是鸡肋,它是为区间聚合统计而生,适配90%的bitmap签到业务

我们结合最经典的用户签到系统,对比业界两种存储方案,高下立判。

方案一:每日单独一个key(新手常用,不推荐)

存储设计
  • key:sign:day:20260601、sign:day:20260602

  • 规则:一个key对应一天,bit位下标=用户ID,1=已签到,0=未签到

优缺点
  • ✅ 单日统计简单:直接 BITCOUNT sign:day:20260601

  • 周/月统计极度麻烦:统计一周签到,需要循环调用7次BITCOUNT,客户端累加结果,网络IO多、性能差

  • ❌ key数量爆炸,一个月产生30个key,运维麻烦

方案二:单key存储整月/整年数据(线上最优方案,利用字节偏移特性)

核心设计思想(吃透BITCOUNT字节偏移)

1个字节 = 1天,完美贴合BITCOUNT的字节区间统计能力

  • 第0字节 → 当月1号

  • 第1字节 → 当月2号

  • 第6字节 → 当月7号(一周)

  • 第29字节 → 当月30号

写到这里我自己当时也立马产生了巨大疑惑:单个字节只有8个bit,一天只能存8个用户?那线上用户量远超8人该怎么办?

如果单日数据需要占用2个、3个甚至更多字节,那之前「一天对应固定字节位置」的映射关系不就彻底乱了?BITCOUNT按字节区间统计的逻辑,不就完全失效了吗?

这也是我当初学这个知识点,卡了最久的盲区,下面我结合真实线上业务,把这个问题彻底讲通透:

✅ 纠正误区:从来不是一天只能用1个字节

首先澄清:1字节=1天只是极简入门案例,只为方便新手看懂字节偏移逻辑,绝对不能直接上生产

真实海量用户场景下,核心设计逻辑不变,只需要升级规则:每一天占用一块固定长度的连续字节,每一天占用的字节数量完全相等,绝不出现一天多、一天少的情况。

✅ 线上标准设计规则(可直接照搬)
  • 提前评估系统最大用户量,提前算出单日需要的总bit数

  • 向上补齐为完整字节,单日字节长度全局固定,全月统一

  • 每一天独占一段连续且等长的字节区间,前后日期互不干扰

举个真实业务例子:

  • 系统最大用户:100万

  • 单日所需bit:1000000 bit

  • 换算字节:单日固定占用 12207 字节

  • 1号:字节区间 0 ~ 12206

  • 2号:字节区间 12207 ~ 24413

  • 7号:字节区间 73242 ~ 85448

✅ 升级后依旧可以用BITCOUNT做区间统计
redis 复制代码
# 直接传起止字节,一键统计整周所有签到数据
BITCOUNT sign:month:202606 0 85448
✅ 我自己的核心解惑总结(直击痛点)
  1. 入门案例只是演示:1字节=1天,方便理解偏移逻辑,生产切勿直接用

  2. 不会出现单日字节数混乱:提前固定每日字节长度,每一天占用空间完全一致

  3. BITCOUNT设计依旧很香:只要日期和字节区间一一绑定,不管单日占多少字节,区间统计逻辑完全不变

回归原始极简版命令(小用户量场景)
redis 复制代码
# key:一个key存整个月签到数据
key: sign:month:202606

# 1、统计6月1日-6月7日 整周签到总人次(一条命令搞定)
BITCOUNT sign:month:202606 0 6

# 2、统计6月1日-6月30日 整月签到总人次
BITCOUNT sign:month:202606 0 29

# 3、统计月末最后3天签到人数(负数索引)
BITCOUNT sign:month:202606 -3 -1
方案二完整优缺点复盘
  • ✅ 全局只有一个月维度key,key数量极少,运维简单

  • ✅ 周、月区间统计单命令O(1)执行,无多次网络请求,性能拉满

  • ✅ 极致节约内存,bitmap本身内存优势完全发挥

  • ❌ 需要业务层提前做日期→字节偏移的换算,开发侧简单封装即可

这也是绝大多数人看懂基础案例后,卡住的核心盲区,我专门补充超大用户量适配方案,彻底解决你的疑问:

✅ 正确扩容规则:单日占用 N 个连续字节,而非1个字节

我们重新规范全局规则(适配海量用户,线上通用完整版):

  • 设定:单日需要容纳 100万用户

  • 换算:1用户=1bit,单日需要 1000000 bit ≈ 12207 字节

  • 规则:每一天固定占用【固定长度的连续字节块】,而非单个字节

核心不变原则:不管一天需要多少字节,每天占用的字节总数是固定值

  • 示例:提前规定 每天固定占用 12207 个字节

  • 第1天(1号):字节区间 0 ~ 12206

  • 第2天(2号):字节区间 12207 ~ 24413

  • 第7天(7号):字节区间 73242 ~ 85448


四、业务开发通用映射工具方法(Java版,直接复制上线)

日常开发只需要把【日期范围】自动转为【字节偏移范围】,封装工具类一劳永逸,无需手动计算:

java 复制代码
/**
 * 日期转Bitmap字节偏移量
 * @param day 当月第几天
 * @return 对应字节下标
 */
public static int dayToByteOffset(int day) {
    // 1号对应第0个字节
    return day - 1;
}

// 使用示例:查询当月3号~7号签到数据
int startByte = dayToByteOffset(3);
int endByte = dayToByteOffset(7);
// 直接调用redis命令:BITCOUNT sign:month:202606 startByte endByte

五、开发避坑总结(线上必看)

  1. 永远牢记:BITCOUNT start/end 是字节,不是位,不要直接传入bit下标做区间统计

  2. 业务选型建议:单日独立key适合只看单日报表的简单场景;中大型项目、需要周/月聚合统计,统一用单key存整月数据

  3. 负数索引妙用:做近7天、近30天滚动统计,直接用-7 -1,无需计算当月天数

  4. 位偏移换算:如果必须按bit做区间统计,业务层自行做位偏移/8整除换算,不要依赖原生参数


六、BITPOS 命令同样踩坑(start/end 依旧是字节偏移)

讲完了BITCOUNT,必须顺带补齐Bitmap另一个高频且极易踩坑的命令:BITPOS

BITPOS key targetBit start end 的 start、end 参数,和BITCOUNT完全一致,单位依旧是字节,不是位,绝大多数人都会连着踩两个一模一样的坑。

6.1 BITPOS 命令作用

核心功能:在bitmap位图中,查找第一个值为目标bit(0/1)的二进制位偏移下标

  • 不传start/end:全局从头检索,返回第一个符合条件的bit位置

  • 传入start/end:在指定字节区间内检索,检索范围是字节,返回结果是位偏移(最容易混淆的点)

核心双坑(一定要记死)

  1. 入参 start/end:字节偏移,和BITCOUNT规则完全相同

  2. 返回值:二进制位偏移,不是字节偏移

入参是字节,出参是位,一不留神就错乱

6.2 极简实操案例,一眼看懂差异

复用前文同一份bitmap数据,保证上下文一致:

redis 复制代码
# 现有位图:第0位=1,第8位=1
SETBIT sign:202606 0 1
SETBIT sign:202606 8 1

# 1. 全局查找第一个1,返回位偏移 0
BITPOS sign:202606 1

# 2. 指定从第1个字节开始查找(跳过第0字节),返回位偏移 8
BITPOS sign:202606 1 1

# 3. 错误用法:误以为start是位偏移,想从第5位开始找,传入5,实际从第5字节开始检索,查不到数据返回-1
BITPOS sign:202606 1 5

6.3 BITPOS 真实业务场景(和签到系统完美联动)

很多人觉得BITPOS没用,实际上搭配我们前文的按月bitmap签到设计,有3个非常实用的线上场景,全部贴合之前的业务模型:

场景1:查询当月第一个签到用户

按月存储整个月签到数据,快速找出本月最早完成签到的用户ID,无需遍历全部位图:

redis 复制代码
# 全局查找第一个签到(bit=1)的用户位下标,直接得到用户ID
BITPOS sign:month:202606 1
场景2:查询指定日期范围内,首个签到用户

依托我们固定字节块的日期映射规则,查询6月7号当天第一个签到用户:

redis 复制代码
# 7号对应固定起始字节,在当天字节区间内查找首个1
BITPOS sign:month:202606 1 对应7号起始字节 对应7号结束字节
场景3:检测某段时间内是否存在遗漏签到位(查找第一个0)

用于巡检位图填充完整性,排查脏数据,快速定位空位:

redis 复制代码
# 查找整个位图中第一个未签到的空位
BITPOS sign:month:202606 0
场景4:连续签到断点排查

结合BITCOUNT统计总签到数,再用BITPOS定位第一个空缺日期,快速定位用户连续签到中断的第一天,做签到补签逻辑兜底。

6.4 BITCOUNT 与 BITPOS 同源规则汇总(一张表记完)

命令 start/end入参单位 返回值单位 核心用途
BITCOUNT 字节 个数(十进制) 区间统计1的总数(日/周/月签到人数)
BITPOS 字节 位偏移 定位第一个0/1的位置(找首个签到用户、断点排查)

6.5 个人踩坑总结

两个命令底层区间检索逻辑完全一致,Redis官方统一设计:所有bitmap区间范围参数,全部以字节为最小单位

不要凭直觉认为位操作命令就按位偏移,这是Redis Bitmap最反直觉但统一的设计规范,只要记住:只要带start、end区间参数,一律是字节偏移

相关推荐
永远不会出bug1 小时前
PgSql数据库函数
数据库
Volunteer Technology1 小时前
Flink Sink
大数据·数据库·flink
程思扬1 小时前
Android Room 数据库跨版本升级闪退问题根治方案
android·数据库·oracle
IvorySQL1 小时前
PostgreSQL 技术日报 (5月31日)|内核功能研讨,PG 大会赛事动态
数据库·postgresql
Devin~Y1 小时前
智慧物流+AIGC客服Java大厂面试:Spring Boot、Kafka、Redis、JVM与RAG Agent实战
java·jvm·spring boot·redis·spring cloud·kafka·rag
todoitbo1 小时前
一台 2C2G 服务器上的 KingbaseES 安装记录
运维·服务器·数据库·国产数据库
mN9B2uk171 小时前
SQL Server 数据库设计
数据库·oracle
Elastic 中国社区官方博客1 小时前
使用 Jina CLIP v2 和 Elasticsearch 实现多语言图片搜索
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·jina
闪电悠米1 小时前
黑马点评-分布式锁-02_simple_redis_lock_setnx
java·数据库·spring boot·redis·分布式·缓存·wpf