0%

Redis 原理初探(一)—— 字符串

简单动态字符串(simple dynamic string,SDS)

定义

  • redis 的默认字符表示,作为一个可被修改的字符串值

  • 作为字符串的底层实现

1
2
3
4
5
6
7
struct sdshdr
{
/* data */
int len; -- 数组已使用字节数量
int free; -- 数组未使用字节数量
char buf[]; -- 字节数组
};

注:buf [] 的最后一个字节会用于保存空字符’\0’(同 C style)

使用 SDS 而非 C style 字符串的好处

获取字符串长度不为性能瓶颈

  • 由定义可知,SDS 的结构体中已经保存了数组的长度(len),从而只需使用 STRLEN () 获取值,其对应算法复杂度始终仅为 O (1);C 字符串由于没有保存该信息,则需要对数组进行遍历得到长度,其算法复杂度为 O (n)

API 安全,排除了缓冲区溢出的风险

  • 对于 C 字符串而言,缓冲区容易溢出其实是不记录数组长度的衍生问题。如考虑下述场景:

1
char *strcat(char *c1, const char *c2);

​ strcat () 方法会假定系统尚且分配了足够的内存给 c1,以容纳 c2 中的所有内容;而 c2 数组长度过长且超出内存限制时,就造成了缓冲区的溢出。这可能会给相邻内存的内容带来意外的后果(如:未指定的意外修改,etc.)

​ 而 SDS 的 API 会自动对 len 进行修改和更新;当当前空间不能满足要求时,则会自动扩展当前 SDS 的空间。对应 redis 中字符串拼接方法为:

1
sdscat(c1, c2);

是二进制安全的

​ 既可存储文本数据、也可存储二进制数据

兼容部分 C style 字符串

1
2
// string compare
strcasecmp(c1->c2, "hello world");

用途

  • 作为数据库中字符串值、整数值和浮点数值的存储

1
2
RPUSH fruits "apple" "banana" "cherry"
-- output:(integer) 3
  • 作为缓冲区(buffer)

空间分配策略

目的

​ 减少连续执行字符串增长操作所需的内存重分配次数

  • 策略 1:空间预分配

    • 当未使用空间足够时,无需进行内存重分配,即没有对字符串进行修改
    • 若修改后的字符串长度小于 1MB,将分配与当前数组已使用长度等长的未使用空间
    • 若修改后的字符串长度大于 1MB,将分配 1MB 的未使用空间
  • 策略 2:惰性空间释放

    • 当空间被释放成为空余空间后,并不会立即对其进行回收,而是先由 free 属性记录下来
    • 避免了缩短字符串时所需的内存重分配操作,并为将来可能有的增长操作提供了优化
    • 当有实际空间需要时,会通过对应 API 真正完成空余空间的释放

字符串操作

自增 / 自减

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import redis

# 获取连接
conn = redis.Redis()
# 获取对应key:只要该字符串值可以被解释为整数值,则可进行下述处理

conn.get('sample_key')
# 对该key进行自增操作 incr = short for increase
conn.incr('sample_key')
# 也可以以数字形式指定自增参数,下两者效果相同
# INCRBY sample_key 10
conn.incr('sample_key', 10)

# 对该key进行自减操作 decr = short for decrease
# DECRBY sample_key 8
conn.decr('sample_key', 8)

# 二次获取当前的数值
conn.get('sample_key')

打包 (package) 处理结构化数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import redis
# 建立连接
conn = redis.Redis(host='127.0.0.1', port=6379, password='123456')
# 执行APPEND命令:返回的是当前字符串的长度
# 此时前者可不存在或为空串,则得到的新字符串长度显然为后者的长
conn.append('sample_string', 'hello ')
conn.append('sample_string', 'world!')
# 操作后sample_string的长度:12

# SUBSTR:获取字符串子串(部分区间值)
# redis数组索引也是从0开始,则取得的值为llo
conn.substr('sample_string', 2, 4)

# SETRANGE:为字符串设置范围
# 设置后仅改变输出的字符串内容
# 字符串长度仍为总长度,而非处理后的范围长度
conn.setrange('sample_string', 0, 'H')

# SETBIT:单独改变某二进制位

【参考】
[1] 《Redis 的设计与实现》

[2] 《Redis 实战》