今天来聊聊 Redis
的 string
,这一数据结构。
# string
简介
string
是 Redis
中最基本,也是最简单的数据结构。一个键 ( key
) 对应着一个 string
类型的值 ( value
). 我们都知道 redis
是使用 C
语言来编写的,但是 string
这一个数据结构并非是使用 C
语言的 string(char[])
来实现的,要想先了解,那就做电梯吧 ->( 电梯直达 ).
现在,先暂且抛开内部实现,我们先看看有怎么使用这一数据结构。
# string
相关常用命令
# set
命令
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
使用示例:
1 | # 1.设置一个键值对 f1=>f1 |
# setnx
命令
setnx key value
set if not exists
的缩写。如果已存在 key, 返回 0, 不存在返回 1. 常用于分布式锁。
使用实例
1 | # 设置一个不存在的键值对 k6=>v6 |
# setEx
命令
setex key seconds value
给键值对设置生存时间 (秒级别)。
1 | 设置k7=>v7这个键值对的生存时间为5s |
# psetEx
命令
psetex key milliseconds value
tip
: 命令助记:psetex
,p
直接的是毫秒。可以参考set
命令的PX
选项。
给键值对设置生存时间 (毫秒级别)。
1 | # 设置键值对 |
# get
命令
这个命令不多说了,获取 key
相关联的 value
. get key
# getset
命令
getset key value
设置键值对, key=>value
, 如果 key
已经存在,返回旧值。不存在返回 nil
1 | # 设置键值对 |
ps: 如果原来的存在 key,但是 value 的类型与新设置的类型不一致,会抛出命令错误。
1 | # 设置一个list类型,key为k9_1, Value中只有一个元素v9_1 |
# strlen
命令
strlen key
返回字符串的长度。如果 key 不存在的时候,返回 0, 如果 key 对应的不是一个字符串时,返回错误.
1 | 127.0.0.1:6379> set k10 v10 |
# APPEND
命令
APPEND key value
命令
根据 key,给 key 对应的值追加字符串。如果 key 不存在,就设置一对键值对。
1 | # 如果key不存在则设置键值对 |
# setrange
命令
setrange key offset value
从偏移量 offset
开始覆写原来 key
的值。如果 key
不存的时候当作空字符串处理。返回被设置后 Value
的长度。
1 | # 设置不存在的key |
# getrange
命令
getrange key start end
获取指定区间的值。报错 start 和 end 位置。索引位置是从 0 开始的。
负数偏移量表示从字符创的末位开始计数。
1 | 127.0.0.1:6379> set k13 v13v13v13 |
# incr
命令
incr key
在 key 对应的 Value 上进行自增 1. 如果 Value 可以解释为数据,则自增,反之,返回错误。
返回值为自增后的值。
如果 ke 不存在,则先初始化 key 对应的 Value=0, 然后再自增。
相对的是: DECR
命令
1 | 127.0.0.1:6379> incr k14 |
# incrby
命令
incrby key increment
带有步长的自增命令。
相对的命令是: DECRBY
命令
1 | 127.0.0.1:6379> incrby k15 5 |
# INCRBYFLOAT
命令
INCRBYFLOAT key increment
带有步长的浮点数自增
1 | 127.0.0.1:6379> INCRBYFLOAT k16 5.0 |
# DECR
命令
DECR key
自减 1
.
1 | # 如果key,不存在,同样会初始化为0,然后自减1 |
# DECRBY
命令
带有步长的自减命令,与 INCRBY
命令相对。
1 | # 如果key不存在,会初始化为0,在进行自减。 |
# mget
命令
mget key [key ...]
一次性返回多个 key
的值。 如果 key
不存在,返回 (nil)
1 | 127.0.0.1:6379> set k19_0 v19_0 |
# mset
命令
同时为设置多个键值对。 如果 key 已经存在,直接覆盖掉。
注意: 这个原子性操作。所有给定的 key 都会在同一时间内被设置。
tips: 如果希望,已经存在的 key 不被覆盖,可以参考
msetnx
命令
1 | # 一下设置三对 |
# msetnx
命令
MSETNX key value [key value ...]
当且仅当所有给定的 key 不存在的时候,才会设置键值对。即使有一个 key 存在,该命令也不会设置其他的 key 对应的键值对.
1 | # 演示设置成功 |
# Redis
如何实现 String
这一数据结构
在 string
的相关命令介绍的时候,我其实使用一个错误的描述。就是将 Redis
的 String
类型称为字符串。这种说法其实不正确的。
在 redis
中, string
这一数据结构使用 sds
来表示的。
# sds
sds
是 simple dynamic string
的简称。 意思是 简单的动态字符串
。 这里面的 string
就是实打实的 C
语言中的字符串 ( char[]
). Redis
也并非一点也没有使用 C
语言的字符串,像一些字面量常亮,日志都是使用 C
语言的字符串。
那 sds
到底是一个什么样的结构呢?
在源码的 src
目录下,我找到了 sds.h
这样一个文件。这里规定了 sds
结构。
1 | struct __attribute__ ((__packed__)) sdshdr64 { |
tips: 如果你注意到了这个结构体的命名。那么来看下这篇文章吧。
sds
保留了 C
字符串以空字符结尾的惯例。保留的这个空字符的长度不会保存在 len
字段中。保留这一惯例的好处就是可以使用 C
字符串函数库的一些方法。
假设我们分配了 10
个字节空间,只保存了 redis
这个 C
字符串,那么 在 sds
中,是这么表示的:
# 使用 sds
比使用 C
字符串有什么好处呢?
# 获取字符长度的时间复杂度为 O(1)
C
语言获取一个字符串的长度为 O(N)
. 需要遍历字符串并累加,判断字符是否为 '\0'
来获得字符串的长度。
sds
只需要根据 len
字段获取即可。怎么获取的呢?
我们来看下源码。
1 | // 定义char类型的指针类型。 |
# 可以杜绝缓冲区溢出
C
语言是不会判断数组是否越界的。比如 strcat
方法,如果当前的数据不能容纳拼接之后字符时,必然会发生缓存区溢出。
但是 sds
则不会。我们来看下 sds
的字符串拼接的方法 sdscat
。
1 | // s 原来的字符串,t是要拼接的字符串 |
# sds 优化了 C 语言的内存分配策略
# 空间预分配
空间预分配策略遵循下面的公式:
- 如果
SDS
的长度小于最大的预分配空间 (1MB
), 那么会分配两倍的新空间,再加上结尾的空字符'\0'
举个例子:原有的sds
的len
为5
,alloc
为5
, 要拼接的字符串长度为15
, 那么新分配的空间大小是:(5byte+15byte)*2 + 1byte = 41byte
. - 如果
sds
的长度大于等于默认的预分配空间,那么就在新分配的空间大小基础上,在分配1MB
的空间。如果修改后的,SDS
的len
是20M
,那么alloc
就是20M + 1M + 1byte
具体分配过程见下面的源码
1 | // SDS 默认最大的预分配空间为1M |
# 惰性空间释放
当对 sds 进行缩短操作时,程序并不会立马对内存重分配来回收收缩的空间,而是仅仅改变 len
属性,并且在队对应的位置上将字符设置为: '\0'
以 函数 sdstrim
为例。
1 | sds sdstrim(sds s, const char *cset) { |
上述实现中,并没有进行内存回收。 sds
也提供了内存回收的函数 sds_free
. 具体可以看 Redis 5.0.7
版源码. sds.c
第 1120
行。这里不再深入学习了。
# 二进制安全
sds
的 API
都是二进制安全的。因为 Redis
对 sds
结构中的 buf
数组中的数据都是以二进制的方式处理的。
# 兼容部分的 C
字符串函数
Redis
还是遵循了 C
字符串以 '\0'
结尾的习惯,所以保存了文本数据的 sds
是可以复用 <string.h>
库中的函数。
# 总结
-
string
是redis
中最简单的数据结构.string
不是C
字符串,而是对C
字符串进行了封装. -
学习了
string
类型相关的api
。set
,setnx
,setex
,get
,getset
,incr
,decr
,… -
sds
这种设计的好处,提高了性能,优化内存分配,二进制安全,兼容C
字符串。
# 最后
期望与你一起遇见更好的自己