<p>ThinkPHP 6 调用 Redis Bitmap 签到需严格遵循三点:key 必须为 sign:{uid}:{Ym} 按月隔离,offset 必须为 (int)date('j') - 1(8月17日对应16),setBit 第三个参数必须是整数1或0;否则 BITCOUNT 统计偏高、连续签到逻辑失效。</p>

直接用 ThinkPHP 6 配 Redis 做签到,关键不是“能不能”,而是setBit参数错一位、key没按月切、offset没减 1 就全乱——这三处出错,BITCOUNT 统计会偏高,连续签到逻辑直接失效。
ThinkPHP 6 怎么调用 Redis Bitmap 命令
TP6 不封装 Bitmap 操作,得靠底层 Redis 客户端(如 phpredis 或 predis)直连。确认你的配置已启用对应扩展,并在 config/cache.php 或 config/redis.php 中配好连接。
基础写法示例:
$redis = \think\facade\Cache::store('redis')->handler();
$key = 'sign:' . $userId . ':' . date('Ym'); // 如 sign:123:202608
$offset = (int)date('j') - 1; // ⚠️必须减 1!8 月 17 日 → offset = 16
$redis->setBit($key, $offset, 1);
-
setBit第三个参数必须是整数1或0,传布尔值true在某些 phpredis 版本下会静默转成0 -
date('j')返回字符串,不强转(int)可能被当字符串索引,导致 offset 错乱 - 别用
date('d')——它补零(如'08'),转整后仍是8,但语义易混淆,date('j')更直观
key 设计为什么必须带年月且不能复用单 key
用 sign:{uid}:{yyyyMM} 是硬性要求,不是建议。跨月共用一个 key(比如只用 sign:123)会导致三类问题:
立即学习“PHP免费学习笔记(深入)”;
- BITCOUNT 统计的是整个 key 的所有 bit,若存了 202607 和 202608 两个月数据,
BITCOUNT sign:123返回的是两月总和,无法分离 - offset 超出 31 后,Redis 仍允许写入,但高位全是冗余 0,浪费内存且影响 BITFIELD 解析效率
- 无法设置 TTL 精准过期:7 月 key 可设 32 天过期,8 月 key 设 31 天,混在一起就只能统一设长周期,历史数据堆积
TP6 中推荐动态生成 key:
$key = 'sign:' . $userId . ':' . date('Ym'); // ✅ 自动切换
// 不要写死或拼接成 sign:123:2026 或 sign:123:20260801(日粒度 key 无必要,且 BITOP 成本飙升)
怎么算连续签到天数(避免每次遍历)
Redis 本身不提供“最长连续 1”的原生命令,TP6 场景下最稳的方案是:用 BITFIELD 取低位片段 + PHP 位运算,而不是用 BITPOS 往回扫。
例如今天是 8 月 17 日,只关心截至今天的连续情况:
$bits = $redis->bitField($key, ['GET', 'u' . (date('j')), 0]); // 取前 N 位(u17 表示无符号17位整数)
$n = $bits[0] ?? 0;
$count = 0;
while ($n & 1) {
$count++;
$n >>= 1;
}
// $count 就是当前连续签到天数(从今天往回数,遇 0 或到月初截止)
- 用
uN(N = 当前日期)保证只取有效位,高位无效 0 不干扰结果 - 不要用
BITPOS $key 0查第一个未签日再倒推——如果用户从月初一直签到今天,BITPOS返回 -1,PHP 处理容易漏判 - 高频场景建议额外维护
streak:{uid}:{yyyyMM}计数器,签到时INCR,断签时SET0,读取 O(1)
容易被忽略的线上坑点
本地跑通不等于线上可用,这几个点上线前必须核对:
- Redis 版本 ≥ 2.8.9(
BITFIELD是 3.2+,但 TP6 若用旧版 phpredis,可能 fallback 到多次GETBIT,性能骤降) - PHP 进程重启后,Redis 连接可能未重连,
setBit报Connection refused却被静默吞掉——务必在调用前加if (!$redis->isConnected()) { $redis->connect(); } - 高并发签到时,
setBit+BITCOUNT+BITFIELD是三个独立命令,非事务。如需原子性(比如签到成功才发积分),得用 Lua 脚本封装,TP6 可通过$redis->eval(...)执行 - 前端日历组件显示“已签”状态时,别直接查 Redis——应缓存当日
GETBIT结果到本地 session 或前端 localStorage,减少重复请求
真正的难点不在命令怎么写,而在于 offset 是否减 1、key 是否按月隔离、连续逻辑是否避开全量扫描——这三个点卡住,后面优化都是白忙。



















