Redis字符串

Redis字符串

字符串类型的全称是 Simple Dynamic Strings 简称 SDS,中文意思是:简单动态字符串。它是以键值对 key-value 的形式进行存储的,根据 key 来存储和获取 value 值,它的使用相对来说比较简单,但在实际项目中应用非常广泛。

字符串类型的功能

  1. 字符串存储和操作
  2. 整数类型和浮点类型的存储和计算

Demo

1. 设置和获取值

SET name "Alice"
GET name

执行结果:

127.0.0.1:6379> SET name "Alice"
OK
127.0.0.1:6379> GET name
"Alice"

2. 设置带过期时间的键

SET session_token "abc123" EX 60
  • EX 60 表示设置 60 秒后过期
    执行结果:
127.0.0.1:6379> SET session_token "abc123" EX 60
OK

3. 批量设置/获取键值

MSET key1 "value1" key2 "value2"
MGET key1 key2

执行结果:

127.0.0.1:6379> MSET key1 "value1" key2 "value2"
OK
127.0.0.1:6379> MGET key1 key2
1) "value1"
2) "value2"

4. 判断 key 是否存在

EXISTS name

执行结果:

127.0.0.1:6379> EXISTS name
(integer) 1

5. 字符串追加(Append)

APPEND name " Smith"
GET name

执行结果:

127.0.0.1:6379> APPEND name " Smith"
(integer) 11
127.0.0.1:6379> GET name
"Alice Smith"

6. 获取字符串长度

STRLEN name

执行结果:

127.0.0.1:6379> STRLEN name
(integer) 11

7. 原子递增 / 递减(通常用于计数)

INCR page_view
INCRBY score 10
DECR stock
DECRBY balance 20
  • 如果 key 不存在,则会先初始化此 key 为 0 ,然后再执行加整数值的操作

执行结果:

127.0.0.1:6379> INCR page_view
(integer) 1
127.0.0.1:6379> INCRBY score 10
(integer) 10
127.0.0.1:6379> DECR stock
(integer) -1
127.0.0.1:6379> DECRBY balance 20
(integer) -20

8. 获取/设置某一部分字符串(偏移量

SET greeting "Hello Redis"
GETRANGE greeting 6 10   

SETRANGE greeting 6 "World"
GET greeting             

执行结果:

127.0.0.1:6379> SET greeting "Hello Redis"
OK
127.0.0.1:6379> GETRANGE greeting 6 10   
"Redis"
127.0.0.1:6379> 
127.0.0.1:6379> SETRANGE greeting 6 "World"
(integer) 11
127.0.0.1:6379> GET greeting 
"Hello World"

9. 如果 key 不存在时才设置(防止覆盖)

SETNX lock:user:123 1

执行结果:

127.0.0.1:6379> SETNX lock:user:123 1
(integer) 1

10. 设置并返回旧值(常用于更新)

GETSET name "Bob"

执行结果:

127.0.0.1:6379> get name
"Alice Smith"
127.0.0.1:6379> GETSET name "Bob"
"Alice Smith"
127.0.0.1:6379> get name
"Bob"

为什么不复用C语言的Char *?

这是一个非常经典的问题!Redis 没有直接复用 C 语言的 char*,而是自己实现了一套叫做 SDS(Simple Dynamic String) 的字符串结构,主要是因为:

  • 原生 char* 太“原始”,无法满足 Redis 对性能、安全性、功能性的需求。

C 语言 char* 的问题

  1. 无法保存长度
  • char* 本质上就是一个指向字符数组的指针,长度必须靠 strlen 逐字节遍历 \0 才能知道,时间复杂度 O(n)。

  • Redis 读写字符串频繁,不能每次都扫描一遍,太慢了。

  1. 容易越界和缓冲区溢出
  • char* 没有内存管理能力,你不知道分配了多少空间,append 时极容易写过界,造成严重的安全漏洞(buffer overflow)。

  • Redis 是对外暴露接口的服务器,必须保证健壮和安全。

  1. 不支持二进制安全
  • 原生 C 字符串要求以 \0 结尾,不能含有 \0 字节(否则 strlen 失效),所以不能存储 任意二进制数据。

  • Redis 的 string 类型是支持二进制安全的,比如可以存储图片、压缩包、哈希值等。

Redis 的 SDS 解决方案

Redis 设计了自己的 SDS

特性 说明
长度 O(1) 获取 结构体中带 len 字段
动态扩容 alloc 字段,支持 append 自动扩容
空间预留 扩容时预留一定空间,避免频繁 realloc
二进制安全 不依赖 \0 结尾,字符串中可以有任何字节
多种 header 类型 sdshdr5/8/16/32/64 根据实际长度自动选择结构,节省内存
兼容 char* buf[] 字段就是普通 char 数组,使用上兼容 char*

源码

1. 数据结构

Redis 并不是直接使用 C 语言的 char* 字符串,而是封装了一种更安全高效的结构 —— SDS,具备以下优点:

  • 支持二进制安全(可存储任意字节)
  • 常数时间获取长度 O(1)
  • 减少 realloc 的频率(空间预分配)
  • 避免缓冲区溢出问题

src/sds.h L22-L51

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

结构体sdshdr8详解

字段 类型 大小 含义
len uint8_t 1 字节 当前已使用的字符串长度(不包含结尾 '\0')
alloc uint8_t 1 字节 分配的 buf 长度(不包含 header 和 '\0')
flags unsigned char 1 字节 低 3 位存储 SDS 类型,比如 SDS_TYPE_8
buf[] char 数组 n 字节 真正的字符串内容 + \0
  • 总共固定部分是 3 字节,字符串数据和 \0 跟在后面。因为使用了 __attribute__((__packed__)),结构体不会有对齐填充。

举个例子:
假设我们保存字符串 "hello",长度为 5。

struct sdshdr8 {
    uint8_t len = 5;
    uint8_t alloc = 10;       // 提前预分配了 10 字节空间
    unsigned char flags = 0x01; // SDS_TYPE_8
    char buf[11] = "hello\0...";
};

内存布局如下(总共 3 + alloc + 1 字节): 结构体大小是 3 字节 header + alloc 字节的 buf+1 字节 '\0'

| len=5 | alloc=10 | flags=0x01 | 'h' | 'e' | 'l' | 'l' | 'o' | \0 | ...剩余空间 |
   1B       1B          1B        ← buf[0] 开始            

flags 字段的编码

flags = (type & 0x7) | (unused << 3);
  • 低 3 位:表示 SDS 类型(例如:SDS_TYPE_8 = 1)
  • 高 5 位:暂时未用(为以后扩展保留)

Redis 会根据字符串长度选择不同的结构(sdshdr8, sdshdr16, sdshdr32, sdshdr64)

src/sds.c L22-L66

static inline int sdsHdrSize(char type) {
    switch(type&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return sizeof(struct sdshdr5);
        case SDS_TYPE_8:
            return sizeof(struct sdshdr8);
        case SDS_TYPE_16:
            return sizeof(struct sdshdr16);
        case SDS_TYPE_32:
            return sizeof(struct sdshdr32);
        case SDS_TYPE_64:
            return sizeof(struct sdshdr64);
    }
    return 0;
}

static inline char sdsReqType(size_t string_size) {
    if (string_size < 1<<5)
        return SDS_TYPE_5;
    if (string_size < 1<<8)
        return SDS_TYPE_8;
    if (string_size < 1<<16)
        return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX)
    if (string_size < 1ll<<32)
        return SDS_TYPE_32;
    return SDS_TYPE_64;
#else
    return SDS_TYPE_32;
#endif
}

static inline size_t sdsTypeMaxSize(char type) {
    if (type == SDS_TYPE_5)
        return (1<<5) - 1;
    if (type == SDS_TYPE_8)
        return (1<<8) - 1;
    if (type == SDS_TYPE_16)
        return (1<<16) - 1;
#if (LONG_MAX == LLONG_MAX)
    if (type == SDS_TYPE_32)
        return (1ll<<32) - 1;
#endif
    return -1; /* this is equivalent to the max SDS_TYPE_64 or SDS_TYPE_32 */
}
Header Type Max String Size Header Size When Used
sdshdr5 31 bytes 1 byte Tiny strings (special case)
sdshdr8 255 bytes 3 bytes Small strings
sdshdr16 64KB - 1 byte 5 bytes Medium strings
sdshdr32 4GB - 1 byte 9 bytes Large strings
sdshdr64 Unlimited 17 bytes Enormous strings

为什么长度为 0 时不使用 sdshdr5

src/sds.c L81-L144

sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
    void *sh;
    sds s;
    char type = sdsReqType(initlen);
    /* Empty strings are usually created in order to append. Use type 8
     * since type 5 is not good at this. */
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp; /* flags pointer. */
    size_t usable;
    ....

因为sdshdr5 的设计对“字符串追加”非常不友好。
由于它没有 alloc 字段(也没有 len 字段!),每次修改都必须:

  • 解包长度(从 flags 高位提取)

  • 重新分配新的 SDS(新建一个更长的)

  • 拷贝旧数据,写入新数据
    而如果你用 sdshdr8,就有:

  • len 和 alloc 字段

  • 可以直接就地 append,减少拷贝,性能更高

所以当 initlen == 0 时,Redis 推测你之后大概率是要 append 的,干脆直接创建 sdshdr8,提升未来写性能。

那为什么还保留 sdshdr5

因为它 在存储短字符串(1~31 字节)时是最省空间的!

  • sdshdr5 结构只占 1 字节 header + n 字节数据 + 1 字节 \0

  • 比 sdshdr8 少了 2 个字节的内存浪费

例如,存 "OK"(2 字节字符串)时:

  • sdshdr5:只占 4 字节(flags + "OK" + \0)

  • sdshdr8:占 6 字节(3 header + 2 数据 + \0)

2. 字符串对象编码

可以使用 object encoding key 命令来查看对象(键值对)存储的对象编码

1. int

127.0.0.1:6379> set key 666
OK
127.0.0.1:6379> object encoding key
"int"

2. embstr 类型

127.0.0.1:6379> set key abc
OK
127.0.0.1:6379> object encoding key
"embstr"

3.raw 类型

127.0.0.1:6379> set key abcdefghigklmnopqrstyvwxyzabcdefghigklmnopqrs
OK
127.0.0.1:6379> object encoding key
"raw"
  • 整数类型对应的就是 int 类型
  • 字符串则对应是 embstr 类型
  • 字符串长度大于 44 字节时,会变为 raw 类型存储

raw 和 embstr的区别

特性 embstr 编码 raw 编码
结构 redisObject + SDS 一次性分配 redisObject 和 SDS 分开分配
内存分配 一次 malloc,更快,缓存友好 两次 malloc
最大长度 最多 44 字节的字符串(含 SDS header) 任意长度
优化目的 高效处理小字符串 支持长字符串

为什么是44个字节?

  • 因为 Redis 的数据类型有很多,而且,不同数据类型都有些相同的元数据要记录(比如最后一次访问的时间、被引用的次数等),所以,Redis 会用一个 RedisObject 结构体来统一记录这些元数据,同时指向实际数据。

我们来估算一下大小限制:

  1. redisObject 结构体(通常约 16 字节)
typedef struct redisObject {
    unsigned type:4; // 4 bit
    unsigned encoding:4; // 4 bit
    unsigned lru:24; // 3 个字节
    int refcount; // 4 个字节
    void *ptr;// 8 个字节
} robj;

它的参数说明如下:

  • type:对象的数据类型,例如:string、list、hash 等,占用 4 bits 也就是半个字符的大小;
  • encoding:对象数据编码,占用 4 bits;
  • lru:记录对象的 LRU(Least Recently Used 的缩写,即最近最少使用)信息,内存回收时会用到此属性,占用 24 bits(3 字节);
  • refcount:引用计数器,占用 32 bits(4 字节);
  • *ptr:对象指针用于指向具体的内容,占用 64 bits(8 字节)。

redisObject 总共占用 0.5 bytes + 0.5 bytes + 3 bytes + 4 bytes + 8 bytes = 16 bytes(字节)。

  1. sdshdr8(embstr 使用的是 sdshdr8)
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; // 1 byte
    uint8_t alloc; // 1 byte
    unsigned char flags; // 1 byte
    char buf[];
};

可以看出除了内容数组(buf)之外,其他三个属性分别占用了 1 个字节,最终分隔字符等于 64 字节,减去 redisObject 的 16 个字节,再减去 SDS 自身的 3 个字节,再减去结束符 \0 结束符占用 1 个字节,最终的结果是 44 字节(64-16-3-1=44)

附录

  1. 官方文档
  2. 菜鸟教程-字符串
  3. Redis 核心技术与实战
posted @ 2025-05-01 01:21  Eiffelzero  阅读(157)  评论(0)    收藏  举报