Bitmap是最省内存的已读标记方式,需message_id映射为连续整数,用原生Redis setBit/getBit操作,key为msg:read:{message_id},依赖user_id→offset映射表,注意offset越界、key过期及phpredis驱动兼容性。

直接用 SETBIT + GETBIT 是最轻量、最省内存的已读标记方式,但前提是 message_id 必须是连续或可映射为连续整数的。TP6.0 本身不封装 Bitmap 操作,得靠原生 Redis 命令 + 合理 key 设计来落地。
为什么不用 Redis Set 存未读 ID?
Set 看似直观,但群聊场景下极易爆内存:200 人 × 每天 100 条消息 = 每天 2 万次 SADD,且 SCARD 统计未读数虽快,但无法快速回答「第 N 条消息我读没读」——必须查完整个 Set 或做交集运算。更麻烦的是,SREM 标记已读时无法原子性更新计数,容易出现计数不准。
而 Bitmap 单条消息仅占 1 bit,200 人只需 25 字节;10 万用户也才 12.5 KB —— 这才是 IM 场景该有的存储密度。
TP6.0 中怎么写 Bitmap 已读逻辑?
TP6.0 的 think\facade\Cache 不支持位操作,必须降级到原生 Redis 实例。推荐在模型或服务层封装:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- key 格式统一为
msg:read:{message_id},value 是按用户 ID 映射的 bitmap(注意:不是每个用户一个 key,而是每条消息一个 key) - 需提前维护
user_id → offset映射表(例如 MySQL 表group_user_map,含group_id、user_id、offset),否则无法定位 bit 位置 - 标记已读:调用
$redis->setBit('msg:read:12345', $offset, 1) - 查是否已读:
$redis->getBit('msg:read:12345', $offset)返回1或0 - 统计已读人数:
$redis->bitCount('msg:read:12345')
别用 Cache::store('redis')->handler() 套壳,它返回的是 ThinkPHP 封装对象,不保证暴露 setBit 方法;应直接从配置获取 Redis 实例:app('cache')->store('redis')->getRedis()。
Bitmap 方案最容易被忽略的三个坑
第一,message_id 不能直接当 offset —— Redis bitmap 的 offset 是从 0 开始的无符号整数,最大支持 2³²−1(约 42 亿),但业务 ID 往往是时间戳+雪花 ID,动辄超限。必须做映射,比如用数据库自增 seq_id 或 Redis INCR 生成紧凑序列号。
第二,bitmap key 过期问题。Redis 不支持对 bitmap 的某一位设过期,整个 key 要么永不过期,要么统一过期。若消息有效期为 30 天,需定时任务清理旧 key,或改用带时间戳的 key:msg:read:202606:{message_seq}。
第三,TP6.0 默认 Redis 驱动是 phpredis,不是 predis。predis 不支持 setBit 的整数 offset 参数(会转成字符串),必须确认驱动类型,否则 setBit 写入失败却静默返回 false。

















