vault backup: 2026-05-25 23:50:33
This commit is contained in:
+56
-9
@@ -48,15 +48,23 @@ func IsSigned(ctx context.Context, rdb *redis.Client, userID int64, dayOfYear in
|
||||
return val == 1, err
|
||||
}
|
||||
|
||||
// 本月累计签到天数
|
||||
// 本月累计签到天数(精确版:逐 bit 检查,避免字节边界误差)
|
||||
func MonthSignCount(ctx context.Context, rdb *redis.Client, userID int64) (int64, error) {
|
||||
key := fmt.Sprintf("sign:%d:%d", userID, time.Now().Year())
|
||||
// BITCOUNT 支持按字节范围统计,start/end 是字节索引
|
||||
// 需要根据月份计算对应的字节范围
|
||||
return rdb.BitCount(ctx, key, &redis.BitCount{
|
||||
Start: int64((time.Now().Month() - 1) * 4), // 近似值,实际需精确计算
|
||||
End: int64(time.Now().Month()*4 - 1),
|
||||
}).Result()
|
||||
now := time.Now()
|
||||
// 计算本月第一天和最后一天在年中的 day-of-year(1-based)
|
||||
firstDay := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.Local).YearDay()
|
||||
lastDay := time.Date(now.Year(), now.Month()+1, 0, 0, 0, 0, 0, time.Local).YearDay()
|
||||
|
||||
var count int64
|
||||
for day := firstDay; day <= lastDay; day++ {
|
||||
val, err := rdb.GetBit(ctx, key, int64(day-1)).Result() // YearDay 1-based -> offset 0-based
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
count += val
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
```
|
||||
|
||||
@@ -147,7 +155,46 @@ Bitmap 的内存效率比 Hash 方案低约 **380 倍**,这就是为什么在
|
||||
> [!question] 为什么 Bitmap 这么省?
|
||||
> 因为它只用 1 个 bit 来表示一个布尔值,而 Hash/Set 需要存储完整的 key 和 value。Redis String 底层 SDS 本身也有元数据开销,但分摊到数十亿个 bit 上几乎可以忽略。
|
||||
|
||||
### 5. Bitmap vs Set vs HyperLogLog 对比
|
||||
### 5. BITFIELD:任意宽度整数操作
|
||||
|
||||
前面的 `SETBIT`/`GETBIT` 只能操作单个 bit(0 或 1)。如果我们需要存储的不是布尔值,而是一个小整数呢?比如"连续签到天数"(0~127)或"用户等级"(0~15)。
|
||||
|
||||
这就是 `BITFIELD` 的用武之地——它将一个 String 视为一个**任意宽度整数的数组**,支持原子性的读写和自增。
|
||||
|
||||
| 子指令 | 作用 | 示例 |
|
||||
|--------|------|------|
|
||||
| `GET type offset` | 读取指定偏移处的整数 | `BITFIELD k GET u8 0` |
|
||||
| `SET type offset value` | 写入整数 | `BITFIELD k SET u8 0 255` |
|
||||
| `INCRBY type offset increment` | 原子自增 | `BITFIELD k INCRBY u8 0 1` |
|
||||
|
||||
其中 `type` 的格式为 `u`(无符号)或 `i`(有符号)+ 位宽(1~64),例如 `u8` 表示 8 位无符号整数(0~255),`i16` 表示 16 位有符号整数。
|
||||
|
||||
> [!tip] 为什么 INCRBY 很重要?
|
||||
> 它是原子操作。多个客户端可以同时对同一个 offset 做 INCRBY 而不会出现竞态条件,无需额外加锁。
|
||||
|
||||
```go
|
||||
// 用 BITFIELD 存储连续签到天数(u8 宽度,最多 255 天)
|
||||
func IncrContinuousSign(ctx context.Context, rdb *redis.Client, userID int64) (int64, error) {
|
||||
key := fmt.Sprintf("streak:%d", userID)
|
||||
// BITFIELD key INCRBY u8 0 1
|
||||
vals, err := rdb.BitField(ctx, key, "INCRBY", "u8", "0", "1").Result()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return vals[0], nil // 返回自增后的连续签到天数
|
||||
}
|
||||
|
||||
// 重置连续签到天数(签到中断时调用)
|
||||
func ResetContinuousSign(ctx context.Context, rdb *redis.Client, userID int64) error {
|
||||
key := fmt.Sprintf("streak:%d", userID)
|
||||
return rdb.BitField(ctx, key, "SET", "u8", "0", "0").Err()
|
||||
}
|
||||
```
|
||||
|
||||
> [!question] BITFIELD vs 多个 key 怎么选?
|
||||
> 如果每个用户只需要存 1~2 个小整数,用普通的 String/Hash key 就够了。`BITFIELD` 的优势在于:当需要**批量**存储大量同类型小整数时(比如 1 万个物品的库存量),可以把它们紧凑地打包到一个 key 中,大幅减少 key 数量和网络往返。
|
||||
|
||||
### 6. Bitmap vs Set vs HyperLogLog 对比
|
||||
|
||||
| 特性 | Bitmap | Set | HyperLogLog |
|
||||
|------|--------|-----|-------------|
|
||||
@@ -163,7 +210,7 @@ Bitmap 的内存效率比 Hash 方案低约 **380 倍**,这就是为什么在
|
||||
> - 需要精确集合运算(交集、差集) -> **Set**
|
||||
> - 只需要统计基数(UV/PV),允许 0.81% 误差 -> **HyperLogLog**
|
||||
|
||||
### 6. 签到系统流程
|
||||
### 7. 签到系统流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
|
||||
Reference in New Issue
Block a user