Skip to content
清晨的一缕阳光
返回

Redis String 数据类型详解

Redis String 数据类型详解

String 是 Redis 最基本、最常用的数据类型。它不仅是简单的键值对,更支持原子操作、位运算等高级功能。本文将深入 String 的底层结构 SDS,掌握其核心原理和应用场景。

一、String 基础概念

1.1 什么是 String

Redis 的 String 是二进制安全的字符串,可以存储:

# 基本操作
SET name "John"
GET name                    # "John"

# 存储数字
SET counter 100
GET counter                 # "100"

# 存储二进制数据
SET image (binary data)

1.2 与其他语言字符串的区别

特性C 语言字符串Redis String
二进制安全❌(遇\0 结束)
获取长度O(n)O(1)
内存分配固定动态预分配
安全性缓冲区溢出风险安全

二、SDS 底层结构

2.1 什么是 SDS

SDS(Simple Dynamic String) 是 Redis 自研的字符串结构,解决了 C 字符串的缺陷。

// SDS 结构定义(Redis 3.2+)
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len;           // 已使用长度
    uint8_t alloc;         // 总分配长度
    unsigned char flags;   // 类型标识
    char buf[];            // 字节数组
};

struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len;
    uint16_t alloc;
    unsigned char flags;
    char buf[];
};

struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len;
    uint32_t alloc;
    unsigned char flags;
    char buf[];
};

struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len;
    uint64_t alloc;
    unsigned char flags;
    char buf[];
};

2.2 SDS 内存布局

sdshdr8 结构(len < 256):
┌─────┬───────┬───────┬─────────┐
│ len │ alloc │ flags │  buf[]  │
│ 1B  │  1B   │  1B   │  可变   │
└─────┴───────┴───────┴─────────┘
  ↑                       ↑
  |                       |
 已用长度                实际数据

示例:SET name "John"
┌─────┬───────┬───────┬─────────────┐
│  4  │   8   │  0    │  J o h n \0 │
└─────┴───────┴───────┴─────────────┘
  len=4  alloc=8       实际内容 + 结束符
  空闲空间 = 8 - 4 = 4

2.3 SDS 的优势

优势 1:O(1) 获取长度

// C 字符串:O(n)
int strlen(char *s) {
    int len = 0;
    while(s[len] != '\0') len++;  // 需要遍历
    return len;
}

// SDS: O(1)
int sdslen(sds s) {
    return s->len;  // 直接返回
}

优势 2:防止缓冲区溢出

// C 字符串:可能溢出
char buf[8] = "hello";
strcat(buf, " world");  // 可能溢出!

// SDS: 安全
sds cat(sds s, const char *t) {
    int needed = s->len + strlen(t);
    if (needed > s->alloc) {
        // 自动扩容
        s = sdsMakeRoomFor(s, needed);
    }
    // 安全拼接
    memcpy(s->buf + s->len, t, strlen(t));
    s->len = needed;
    return s;
}

优势 3:减少内存分配

预分配策略:
- len < 1MB: 扩容 2 倍
- len >= 1MB: 扩容 1MB

惰性释放:
- 缩短时不立即释放
- 保留空闲空间供下次使用

2.4 SDS 类型选择

// 根据长度选择合适类型
flags = len & 0xFF;

if (len < 256) {
    // sdshdr8: len 1B, 最大 255 字节
    flags = 0;
} else if (len < 65536) {
    // sdshdr16: len 2B, 最大 64KB
    flags = 1;
} else if (len < 4294967296) {
    // sdshdr32: len 4B, 最大 4GB
    flags = 2;
} else {
    // sdshdr64: len 8B, 最大 512MB
    flags = 3;
}

三、String 基本命令

3.1 设置与获取

# 基本设置
SET key value
SET key value EX 10        # 10 秒后过期
SET key value PX 10000     # 10000 毫秒后过期
SET key value NX           # 仅当不存在
SET key value XX           # 仅当存在

# 批量设置
MSET key1 value1 key2 value2
MSETNX key1 value1 key2 value2  # 全部成功才设置

# 获取
GET key
MGET key1 key2 key3

# 获取并设置
GETSET key new_value       # 返回旧值,设置新值

3.2 原子计数

# 自增自减
INCR counter               # +1
INCRBY counter 5           # +5
INCRBYFLOAT counter 1.5    # +1.5

DECR counter               # -1
DECRBY counter 5           # -5

# 示例
SET counter 10
INCR counter               # 11
INCRBY counter 5           # 16
DECRBY counter 3           # 13

原子性保证

单线程执行,无需额外锁
INCR counter 操作:
1. 读取当前值
2. +1
3. 写回
整个过程不会被中断

3.3 追加与截取

# 追加
APPEND key " world"        # 返回追加后长度

# 截取
SETRANGE key 6 "Redis"     # 从偏移量 6 开始替换
GETRANGE key 0 4           # 获取子串 [0, 4]

# 示例
SET greeting "Hello"
APPEND greeting " World"   # 返回 11
GETRANGE greeting 0 4      # "Hello"
SETRANGE greeting 6 "Redis" # "Hello Redis"

四、高级操作

4.1 位运算

# 设置位
SETBIT bitmap 0 1          # 第 0 位设为 1
SETBIT bitmap 1 0          # 第 1 位设为 0
SETBIT bitmap 2 1          # 第 2 位设为 1

# 获取位
GETBIT bitmap 0            # 返回 1

# 统计 1 的数量
BITCOUNT bitmap            # 返回 2

# 位运算
SET key1 "foobar"
SET key2 "abcdef"
BITOP AND dest key1 key2   # 按位与
BITOP OR dest key1 key2    # 按位或
BITOP XOR dest key1 key2   # 按位异或
BITOP NOT dest key1        # 按位非

# 查找第一个 1 的位置
BITPOS bitmap 1            # 返回第一个 1 的位置

应用场景:签到系统

# 用户签到
SETBIT user:1001:signin 0 1    # 第 0 天签到
SETBIT user:1001:signin 1 1    # 第 1 天签到

# 检查是否签到
GETBIT user:1001:signin 1      # 1=已签到,0=未签到

# 统计签到天数
BITCOUNT user:1001:signin

# 连续签到统计(查找第一个 0)
BITPOS user:1001:signin 0

4.2 位图统计

# 多用户签到统计
SETBIT day:1:signin 1001 1     # 用户 1001 第 1 天签到
SETBIT day:1:signin 1002 1     # 用户 1002 第 1 天签到

# 统计当天签到人数
BITCOUNT day:1:signin

# 两天都签到的用户
BITOP AND active day:1:signin day:2:signin
BITCOUNT active

4.3 HyperLogLog

# 添加元素
PFADD hll user1 user2 user3

# 统计基数(去重数量)
PFCOUNT hll                    # 返回 3

# 合并多个 HLL
PFMERGE dest source1 source2
PFCOUNT dest

特点

应用场景:UV 统计

# 统计页面 UV
PFADD page:1001:uv user_id_1
PFADD page:1001:uv user_id_2

# 获取 UV 数量
PFCOUNT page:1001:uv

# 多页面总 UV
PFMERGE page:all:uv page:1001:uv page:1002:uv
PFCOUNT page:all:uv

五、应用场景

5.1 缓存

# 缓存用户信息
SET user:1001 '{"name":"John","age":25}' EX 3600
GET user:1001

# 批量缓存
MSET user:1001:data ... user:1002:data ...

# 缓存穿透保护(空值也缓存)
SET user:9999:cache "__NULL__" EX 60

5.2 分布式锁

# 获取锁(NX + EX)
SET lock:resource unique_value NX EX 30

# 释放锁(Lua 脚本保证原子性)
EVAL "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 lock:resource unique_value

Redlock 算法

def acquire_lock(resource, ttl=30000):
    locks = []
    for redis in redis_instances:
        if redis.set(lock_key, unique_value, nx=True, px=ttl):
            locks.append(redis)
    
    if len(locks) >= (n/2 + 1):
        return True
    else:
        # 释放所有锁
        for redis in locks:
            redis.delete(lock_key)
        return False

5.3 限流器

# 固定窗口限流
INCR rate_limit:user:1001
EXPIRE rate_limit:user:1001 60

# 滑动窗口限流(使用 ZSet 更精确)

5.4 计数器

# 文章阅读量
INCR article:1001:views

# 点赞数
INCR article:1001:likes

# 实时在线人数
SET user:1001:online 1 EX 300
BITCOUNT online:users

六、性能优化

6.1 批量操作

# 不推荐:多次网络往返
GET key1
GET key2
GET key3

# 推荐:批量操作
MGET key1 key2 key3

# 或使用 Pipeline
PIPELINE
  GET key1
  GET key2
  GET key3
END

6.2 大键处理

# 检查大键
redis-cli --bigkeys

# 避免大键
# ❌ 不推荐:单个值超过 10KB
SET large_key (100KB data)

# ✅ 推荐:拆分存储
HSET user:1001 name John age 25 email john@example.com

6.3 内存优化

# 使用整数编码
SET counter 100              # 内部使用 INT 编码

# 使用embstr编码(小字符串)
SET small "hello"            # < 44 字节使用 embstr

# 检查编码
OBJECT ENCODING key

七、常见问题

Q1: String 最大长度?

最大 512MB
实际建议:< 10KB

Q2: 如何存储对象?

# 方式 1: JSON 字符串(简单)
SET user:1001 '{"name":"John","age":25}'

# 方式 2: Hash(推荐,支持字段操作)
HSET user:1001 name John age 25

# 方式 3: 序列化(复杂对象)
SET user:1001 (protobuf/binary)

Q3: INCR 溢出怎么办?

# 整数范围:-2^63 到 2^63-1
INCR counter                 # 到达最大值报错

# 解决方案
# 1. 使用浮点数
INCRBYFLOAT counter 1.0

# 2. 使用字符串 + 应用层计算
GET counter
# 应用层计算后 SET

总结

Redis String 核心要点:

特性说明
底层结构SDS(简单动态字符串)
最大长度512MB
编码方式int、embstr、raw
原子操作INCR、DECR、APPEND
位运算SETBIT、GETBIT、BITOP
统计BITCOUNT、PFADD、PFCOUNT

最佳实践

  1. 小对象用 String,大对象用 Hash
  2. 批量操作使用 MGET/MSET
  3. 分布式锁使用 SET NX EX
  4. 限流、计数器使用原子操作
  5. 海量去重使用 HyperLogLog

掌握 String 类型,就掌握了 Redis 的基础!

参考资料


分享这篇文章到:

上一篇文章
Prompt 安全与防护工程
下一篇文章
MySQL IO 性能优化