Redis数据结构—String
一、简单介绍
Redis 的 String 数据结构底层实现是 SDS(Simple Dynamic String),它具有以下特点:
-
获取字符串长度的复杂度为 O(1):SDS 中使用 len 属性记录了字符串的长度,因此获取字符串长度的时间复杂度为 O(1),而 C 语言中需要遍历字符串来获取长度,时间复杂度为 O(n)。
-
杜绝缓冲区溢出:SDS 在进行字符串操作时,会自动检查空间是否足够,如果不够会自动扩展空间,从而避免缓冲区溢出的问题。
-
减少内存重分配次数:SDS 通过预分配和惰性空间释放的策略,减少了字符串修改时的内存重分配次数。
-
二进制安全:SDS 可以存储任意二进制数据,而不仅仅是文本数据,因为它不依赖于空字符来判断字符串的结束。
-
兼容部分 C 字符串函数:SDS 的 buf 数组以空字符结尾,因此可以兼容部分 C 字符串函数。 SDS 的定义如下:
struct sdshdr { int len; // 记录 buf 数组中已使用字节的数量,等于 SDS 所保存字符串的长度 int free; // 记录 buf 数组中未使用字节的数量 char buf[]; // 字节数组,用于保存字符串 };
二、应用场景
Redis 的字符串结构非常灵活,可以用于多种应用场景:
- 缓存:
- 最常见的用途之一是作为缓存系统,存储例如网页、图片或是其他计算密集型操作的结果,以减少数据库的访问次数和降低延迟。
- 计数器:
- 利用 Redis 的原子操作,字符串可以用作计数器,例如网站的访问次数、商品的浏览量等。
- 会话存储:
- 在 Web 应用中,可以使用 Redis 存储用户会话信息。由于 Redis 是内存数据库,读写速度快,非常适合此类用途。
- 分布式锁:
- 通过设置带有过期时间的字符串键,可以实现分布式锁的功能,用于多个进程或服务器之间的资源同步。
- 配置管理:
- 使用字符串存储应用配置信息,可以快速读取和更新配置,而不需要重启应用。
- 全局唯一ID生成器:
- 利用 Redis 的原子性操作,可以生成全局唯一的ID,如订单号、用户ID等
三、实现原理
Redis 字符串对象可以使用三种不同的内部编码方式:int、embstr 和 raw。这些编码方式是优化内存使用和提高性能的手段。
-
int编码:
- 当字符串对象可以被解析为一个整数值时,Redis 会选择使用
int编码。 - 这种编码方式将字符串直接存储为整数值,而不是字符数组,从而节省内存。
- 适用于表示数字的小字符串,因为它们可以直接以整数形式存储在内存中。
如果一个字符串对象保存的是整数值,并且这个整数值可以用
long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成 long),并将字符串对象的编码设置为int。 - 当字符串对象可以被解析为一个整数值时,Redis 会选择使用
-
embstr编码:
- 用于存储较小的字符串(长度小于等于 44 个字符,这个值可能根据 Redis 版本和平台有所不同)。
embstr编码将 Redis 对象和实际的字符串数据存储在单个连续的内存块中。- 这种编码方式减少了内存分配的次数(只需要一次内存分配),并且因为数据局部性提高了缓存效率。
embstr编码的字符串是只读的,如果需要修改,必须先转换为raw编码。
-
raw编码:
-
用于存储较大的字符串或需要修改的字符串。
-
raw编码使用两次内存分配:一次用于 Redis 对象本身,另一次用于实际的字符串数据。 -
这种编码方式允许字符串被修改,因为字符串数据是独立于对象结构的。
-
Redis 在运行时根据字符串的内容和大小动态选择最合适的编码方式。例如,当一个字符串对象被创建时,如果它可以被解析为一个整数,Redis 将使用 int 编码。如果字符串较小,它将使用 embstr 编码以节省内存和提高效率。对于较大或需要修改的字符串,将使用 raw 编码。这种灵活的编码策略是 Redis 高效内存使用的关键之一。
flowchart TD
A[开始] --> B{它是字符串对象吗?}
B -->|否| B1[返回原对象]
B -->|是| C{它是RAW或EMBSTR编码吗?}
C -->|否| C1[返回原对象]
C -->|是| D{引用计数 > 1吗?}
D -->|是| D1[返回原对象]
D -->|否| E{能表示为长整型吗?}
E -->|否| F{长度 <= EMBSTR限制吗?}
E -->|是| G{最大内存为0或允许共享整数吗?}
G -->|否| H{编码是RAW吗?}
H -->|是| I[转换为INT编码,释放RAW,赋值]
H -->|否| J{编码是EMBSTR吗?}
J -->|是| K[从值创建新的INT编码对象]
J -->|否| J1[返回原对象]
G -->|是| L{值在共享整数范围内吗?}
L -->|否| H
L -->|是| M[使用共享整数对象]
F -->|否| N{尝试修剪吗?}
F -->|是| O{编码是EMBSTR吗?}
O -->|是| O1[返回原对象]
O -->|否| P[创建EMBSTR编码对象]
N -->|是| Q[如果需要,修剪字符串对象]
N -->|否| R[返回原对象]
Q --> R
I --> R
K --> R
M --> R
P --> R
/* 尝试对字符串对象进行编码以节省空间 */
robj *tryObjectEncodingEx(robj *o, int try_trim) {
long value;
sds s = o->ptr;
size_t len;
/* 确保这是一个字符串对象,这是在此函数中编码的唯一类型。
* 其他类型使用编码的内存高效表示,但由实现类型的命令处理。 */
serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);
/* 我们只尝试对仍然由实际字符数组表示的对象进行一些特殊编码,
* 也就是说,仍然是RAW或EMBSTR编码的对象。 */
if (!sdsEncodedObject(o)) return o;
/* 对共享对象进行编码是不安全的:共享对象可以在Redis的“对象空间”中被共享,
* 并可能最终处于它们不被处理的地方。只将它们作为键空间中的值来处理。 */
if (o->refcount > 1) return o;
/* 检查是否可以将这个字符串表示为一个长整数。
* 一个大于20个字符的字符串不可能表示为32位或64位整数。 */
len = sdslen(s);
if (len <= 20 && string2l(s,len,&value)) {
/* 这个对象可以编码为长整数。尝试使用共享对象。
* 注意,当使用maxmemory时,避免使用共享整数,
* 因为每个对象都需要有一个私有的LRU字段,以便LRU算法能够很好地工作。 */
if ((server.maxmemory == 0 ||
!(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
value >= 0 &&
value < OBJ_SHARED_INTEGERS)
{
decrRefCount(o);
return shared.integers[value];
} else {
if (o->encoding == OBJ_ENCODING_RAW) {
sdsfree(o->ptr);
o->encoding = OBJ_ENCODING_INT;
o->ptr = (void*) value;
return o;
} else if (o->encoding == OBJ_ENCODING_EMBSTR) {
decrRefCount(o);
return createStringObjectFromLongLongForValue(value);
}
}
}
/* 如果字符串很小且仍然是RAW编码,
* 尝试EMBSTR编码,这更高效。
* 在这种表示中,对象和SDS字符串被分配在同一块内存中,以节省空间和缓存未命中。 */
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
robj *emb;
if (o->encoding == OBJ_ENCODING_EMBSTR) return o;
emb = createEmbeddedStringObject(s,sdslen(s));
decrRefCount(o);
return emb;
}
/*不能编码对象...
* 做最后的尝试,至少优化内部的SDS字符串 */
if (try_trim)
trimStringObjectIfNeeded(o, 0);
/* 返回原始对象。 */
return o;
}