vault backup: 2026-05-28 00:08:17
This commit is contained in:
+137
-20
@@ -157,42 +157,159 @@ Bitmap 的内存效率比 Hash 方案低约 **380 倍**,这就是为什么在
|
||||
|
||||
### 5. BITFIELD:任意宽度整数操作
|
||||
|
||||
前面的 `SETBIT`/`GETBIT` 只能操作单个 bit(0 或 1)。如果我们需要存储的不是布尔值,而是一个小整数呢?比如"连续签到天数"(0~127)或"用户等级"(0~15)。
|
||||
前面的 `SETBIT`/`GETBIT` 只能操作单个 bit(0 或 1)。但现实场景经常需要存**小整数**——"连续签到天数"(0~127)可以用 8 位表示,"用户等级"(0~15)只需要 4 位。
|
||||
|
||||
这就是 `BITFIELD` 的用武之地——它将一个 String 视为一个**任意宽度整数的数组**,支持原子性的读写和自增。
|
||||
> [!question] 小整数用普通的 String key 不行吗?
|
||||
> 当然可以,一个 key 存一个值没问题。但如果你要存 **1 万个物品的库存量**,就需要 1 万个 key + 1 万次网络往返。能不能把它们"打包"到一个 key 里?
|
||||
|
||||
`BITFIELD` 就是为了解决这个问题——它把一个 String 当作一个**连续的 bit 流**,在任意 bit 偏移处读写指定宽度的整数。
|
||||
|
||||
#### 5.1 内存布局:一个 key 怎么存多个整数?
|
||||
|
||||
假设我们要在一个 key 里存储 3 个字段:等级(u4)、经验值(u16)、签到天数(u8)。
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
columns 28
|
||||
block:LVL:4
|
||||
A["u4 等级"]
|
||||
end
|
||||
block:EXP:16
|
||||
B["u16 经验值"]
|
||||
end
|
||||
block:SIGN:8
|
||||
C["u8 签到天数"]
|
||||
end
|
||||
|
||||
style LVL fill:#f9a825,color:#000
|
||||
style EXP fill:#42a5f5,color:#fff
|
||||
style SIGN fill:#66bb6a,color:#fff
|
||||
```
|
||||
|
||||
它们在内存中是**紧挨着排列**的:
|
||||
|
||||
```
|
||||
bit 偏移: 0 4 20 28
|
||||
┌───┬───────────────────┬───────────┐
|
||||
│u4 │ u16 │ u8 │
|
||||
│等级│ 经验值 │ 签到天数 │
|
||||
└───┴───────────────────┴───────────┘
|
||||
4 bit 16 bit 8 bit
|
||||
```
|
||||
|
||||
对应 Redis 命令:
|
||||
|
||||
```bash
|
||||
# 一次性读写三个字段,只需一次网络往返
|
||||
BITFIELD user:1001 SET u4 0 10 SET u16 4 5200 SET u8 20 15
|
||||
BITFIELD user:1001 GET u4 0 GET u16 4 GET u8 20
|
||||
```
|
||||
|
||||
> [!important] offset 是 **bit 偏移**,不是"第几个字段"
|
||||
> `SET u16 4` 的意思是"从第 4 个 **bit** 开始写入一个 u16 整数"。字段 1(u4)占了 bit 0~3,所以字段 2 从 bit 4 开始。计算偏移是使用者的职责。
|
||||
|
||||
#### 5.2 子指令速查
|
||||
|
||||
| 子指令 | 作用 | 示例 |
|
||||
|--------|------|------|
|
||||
| `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` |
|
||||
| `GET type offset` | 从 offset 位读取一个 type 宽度的整数 | `BITFIELD k GET u8 0` → 读取 bit 0~7 |
|
||||
| `SET type offset value` | 从 offset 位写入一个整数 | `BITFIELD k SET u8 0 255` |
|
||||
| `INCRBY type offset increment` | 从 offset 位读取整数 + increment 后写回 | `BITFIELD k INCRBY u8 0 1` |
|
||||
|
||||
其中 `type` 的格式为 `u`(无符号)或 `i`(有符号)+ 位宽(1~64),例如 `u8` 表示 8 位无符号整数(0~255),`i16` 表示 16 位有符号整数。
|
||||
**type 格式**:`u`(无符号)或 `i`(有符号)+ 位宽(1~64)
|
||||
|
||||
> [!tip] 为什么 INCRBY 很重要?
|
||||
> 它是原子操作。多个客户端可以同时对同一个 offset 做 INCRBY 而不会出现竞态条件,无需额外加锁。
|
||||
| type | 范围 | 说明 |
|
||||
|------|------|------|
|
||||
| `u8` | 0 ~ 255 | 无符号,适合存储天数、等级 |
|
||||
| `u16` | 0 ~ 65,535 | 无符号,适合存储经验值、库存量 |
|
||||
| `i8` | -128 ~ 127 | 有符号,适合存储温度、偏差值 |
|
||||
| `i32` | -2^31 ~ 2^31-1 | 有符号,适合存储金额(分为单位) |
|
||||
|
||||
#### 5.3 溢出控制:OVERFLOW
|
||||
|
||||
`INCRBY` 自增时如果超出 type 范围怎么办?Redis 提供了三种策略:
|
||||
|
||||
| 策略 | 行为 | 适用场景 |
|
||||
|------|------|---------|
|
||||
| `WRAP` | 回绕溢出(默认) | 循环计数器 |
|
||||
| `SAT` | 饱和到最大/最小值后停止 | 库存量、经验值上限 |
|
||||
| `FAIL` | 拒绝自增,返回 nil | 需要精确控制的业务 |
|
||||
|
||||
```bash
|
||||
# 经验值上限 65535,满了就饱和不动
|
||||
BITFIELD user:1001 OVERFLOW SAT INCRBY u16 4 100
|
||||
|
||||
# 库存扣减,不足时不扣(返回 nil 表示扣减失败)
|
||||
BITFIELD stock:item:5001 OVERFLOW FAIL INCRBY i16 0 -1
|
||||
```
|
||||
|
||||
> [!question] 想想看
|
||||
> 为什么默认是 WRAP 而不是 SAT?因为 `BITFIELD` 的设计初衷是模拟硬件整数行为(CPU 的整数溢出就是回绕的),SAT/FAIL 是后来为业务场景添加的"安全模式"。
|
||||
|
||||
#### 5.4 实战:物品背包系统
|
||||
|
||||
一个 RPG 游戏的背包,每个物品存数量(u8,最多 255 个)。背包有 50 个槽位,全部打包进一个 key。
|
||||
|
||||
```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()
|
||||
// 背包槽位宽度
|
||||
const slotBits = 8 // u8,每个槽位 8 bit
|
||||
|
||||
// 设置某个槽位的物品数量
|
||||
func SetSlot(ctx context.Context, rdb *redis.Client, bagKey string, slot int, count int) error {
|
||||
offset := slot * slotBits
|
||||
// BITFIELD bag:1001 SET u8 0 10
|
||||
_, err := rdb.BitField(ctx, bagKey, "SET", "u8", strconv.Itoa(offset), strconv.Itoa(count)).Result()
|
||||
return err
|
||||
}
|
||||
|
||||
// 读取某个槽位的物品数量
|
||||
func GetSlot(ctx context.Context, rdb *redis.Client, bagKey string, slot int) (int64, error) {
|
||||
offset := slot * slotBits
|
||||
vals, err := rdb.BitField(ctx, bagKey, "GET", "u8", strconv.Itoa(offset)).Result()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return vals[0], nil // 返回自增后的连续签到天数
|
||||
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()
|
||||
// 增加物品(带溢出保护)
|
||||
func IncrSlot(ctx context.Context, rdb *redis.Client, bagKey string, slot int, delta int) (int64, error) {
|
||||
offset := slot * slotBits
|
||||
// 一次性发送:溢出策略 + 自增,单次网络往返
|
||||
vals, err := rdb.BitField(ctx, bagKey,
|
||||
"OVERFLOW", "SAT",
|
||||
"INCRBY", "u8", strconv.Itoa(offset), strconv.Itoa(delta),
|
||||
).Result()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return vals[0], nil
|
||||
}
|
||||
```
|
||||
|
||||
> [!question] BITFIELD vs 多个 key 怎么选?
|
||||
> 如果每个用户只需要存 1~2 个小整数,用普通的 String/Hash key 就够了。`BITFIELD` 的优势在于:当需要**批量**存储大量同类型小整数时(比如 1 万个物品的库存量),可以把它们紧凑地打包到一个 key 中,大幅减少 key 数量和网络往返。
|
||||
> [!tip] 一次 BITFIELD 调用可以包含多条子指令
|
||||
> Redis 会把它们当作一个**原子事务**依次执行。背包里的 50 个槽位可以在一次 `BITFIELD` 调用中全部读取,只需 1 次网络往返(而非 50 次)。
|
||||
|
||||
#### 5.5 BITFIELD vs 多个 key 怎么选?
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A{"需要存小整数?"} -->|是| B{"数量多且结构相同?"}
|
||||
B -->|是, 如背包/库存表| C["BITFIELD 打包到一个 key"]
|
||||
B -->|否, 如用户余额/积分| D["普通 String/Hash key"]
|
||||
A -->|否, 布尔值| E["SETBIT/GETBIT"]
|
||||
|
||||
style C fill:#66bb6a,color:#fff
|
||||
style D fill:#42a5f5,color:#fff
|
||||
style E fill:#f9a825,color:#000
|
||||
```
|
||||
|
||||
| 维度 | BITFIELD 单 key | 多个 String key |
|
||||
|------|----------------|----------------|
|
||||
| **网络往返** | 1 次读写 N 个字段 | N 次 |
|
||||
| **过期管理** | 整个 key 统一过期 | 每个 key 独立过期 |
|
||||
| **单字段更新** | 只影响对应 bit 区间 | 只影响对应 key |
|
||||
| **适用规模** | 几十个到几万个同类字段 | 字段少或结构各异 |
|
||||
|
||||
### 6. Bitmap vs Set vs HyperLogLog 对比
|
||||
|
||||
|
||||
Reference in New Issue
Block a user