Redis String 数据类型详解
String 是 Redis 最基本、最常用的数据类型。它不仅是简单的键值对,更支持原子操作、位运算等高级功能。本文将深入 String 的底层结构 SDS,掌握其核心原理和应用场景。
一、String 基础概念
1.1 什么是 String
Redis 的 String 是二进制安全的字符串,可以存储:
- 普通字符串
- 数字(整数、浮点数)
- 二进制数据(图片、序列化对象)
- 最大 512MB
# 基本操作
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
特点:
- 固定 12KB 内存
- 误差率约 0.81%
- 适合海量数据统计
应用场景: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 |
最佳实践:
- 小对象用 String,大对象用 Hash
- 批量操作使用 MGET/MSET
- 分布式锁使用 SET NX EX
- 限流、计数器使用原子操作
- 海量去重使用 HyperLogLog
掌握 String 类型,就掌握了 Redis 的基础!