### 为什么不用 Redis Set 存储未读 ID?
乍看起来,用 Set 存储未读 ID 非常直观,但在群聊场景中,内存压力会迅速暴露:一个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 实例。建议在模型层或服务层统一封装:
- **key 格式**统一为 `msg:read:{message_id}`,注意:不是每个用户一个 key,而是每条消息一个 key,value 是按用户 ID 映射的 bitmap。
- 需要预先维护 `user_id → offset` 的映射表(例如 MySQL 中的 `group_user_map` 表,包含 `group_id`、`user_id`、`offset` 字段),否则无法确定某个用户在 bitmap 中的位置。
- **标记已读**:`$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 亿),但业务中的 message_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`,排查起来相当隐蔽。TP6.0消息已读功能基于Redis Bitmap未读计数方案
TP6 0中使用Redis位图实现消息已读标记,每条消息仅占一个比特,内存效率高。需将消息ID映射为连续偏移量,通过SETBIT和GETBIT操作。需提前维护用户ID到偏移量的映射表,注意键过期与驱动类型(phpredis与predis)问题。此方法内存极省,适合海量消息场景,且需确保偏移量唯一。
#### 谈到消息已读功能,最轻量且节省内存的方案就是直接使用 `SETBIT` 与 `GETBIT` 指令。不过前提条件是:你的 `message_id` 必须是连续整数,或者能够稳定地映射为连续整数。TP6.0 框架本身并未内置 Bitmap 操作封装,因此需要借助原生 Redis 命令,再配合合理的 key 设计才能落地实现。
### 为什么不用 Redis Set 存储未读 ID?
乍看起来,用 Set 存储未读 ID 非常直观,但在群聊场景中,内存压力会迅速暴露:一个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 实例。建议在模型层或服务层统一封装:
- **key 格式**统一为 `msg:read:{message_id}`,注意:不是每个用户一个 key,而是每条消息一个 key,value 是按用户 ID 映射的 bitmap。
- 需要预先维护 `user_id → offset` 的映射表(例如 MySQL 中的 `group_user_map` 表,包含 `group_id`、`user_id`、`offset` 字段),否则无法确定某个用户在 bitmap 中的位置。
- **标记已读**:`$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 亿),但业务中的 message_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`,排查起来相当隐蔽。
### 为什么不用 Redis Set 存储未读 ID?
乍看起来,用 Set 存储未读 ID 非常直观,但在群聊场景中,内存压力会迅速暴露:一个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 实例。建议在模型层或服务层统一封装:
- **key 格式**统一为 `msg:read:{message_id}`,注意:不是每个用户一个 key,而是每条消息一个 key,value 是按用户 ID 映射的 bitmap。
- 需要预先维护 `user_id → offset` 的映射表(例如 MySQL 中的 `group_user_map` 表,包含 `group_id`、`user_id`、`offset` 字段),否则无法确定某个用户在 bitmap 中的位置。
- **标记已读**:`$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 亿),但业务中的 message_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`,排查起来相当隐蔽。来源:https://www.php.cn/faq/2787379.html
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。
相关推荐
补充同频道和同主题内容,方便继续浏览更多相关内容。
同类最新
继续查看同栏目最近更新的文章。
列表遍历时动态判断阈值并返回相应文本
遍历数值列表时,先筛选满足阈值的元素,再根据结果输出列表或友好提示。推荐使用列表推导式结合条件判断,注意边界用`>=`,空列表自动为假。也可用`any()`提前终止遍历,提升效率。此法简洁,避免显式循环,特别适合阈值筛选。
Maven项目中如何强制使用本地构建的依赖版本
多模块开发中强制使用本地依赖的正确做法是使用-SNAPSHOT版本并配合`-U`参数强制更新,或重构为多模块项目统一管理生命周期。避免使用版本范围语法或手动复制JAR,确保构建行为可靠可重复。
Go语言net.Conn并发写安全与原子性保障解析
Go标准库中net Conn支持并发方法调用,但Write()不保证原子性。多goroutine同时写入时,系统可能拆分数据包,导致内容交错,破坏消息边界。必须使用互斥锁或bufio Writer等显式同步机制确保写操作完整性,不可依赖系统调用本身的原子性。
Python在Windows系统中获取指定卷标U盘驱动器字母的方法
使用Pythonwmi库通过Win32_Volume接口查询Windows系统中卷标为“TOSHIBA”的U盘盘符。需安装wmi和pywin32,注意大小写区分及多设备过滤。仅适用于Windows。
