相关链接

Redis 常用命令,思维导图:https://www.kdocs.cn/view/l/ss49IFenIOSm

SDS数据格式:https://redisbook.readthedocs.io/en/latest/internal-datastruct/sds.html

String

string类型在redis中是最常见的类型,value存储最大数据量为512M,可以存放json数据,图像数据等等

String 底层实现

Redis中,默认以SDS作为自己的字符串表示。只有在一些字符串不可能出现变化的地方使用C字符串。

SDS源码定义如下:

1
2
3
4
5
6
7
8
9
10
struct sdshdr{    
// 用于记录buf数组中使用的字节的数目
// 和SDS存储的字符串的长度相等
int len;
// 用于记录buf数组中没有使用的字节的数目
int free;
// 字节数组,用于储存字符串
char buf[];
//buf的大小等于len+free+1,其中多余的1个字节是用来存储’\0’的(‘\0’ 在c语言中占用一个字节的内存空间,也代表结束)。
};

SDS除了用来保存数据库中的字符串之外,SDS还被用作缓冲区(buffer),如AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区

SDS 的存储示例:

img

使用SDS而不使用c语言的string的好处:

1、常数复杂度获取字符串长度:

SDS:只需要访问SDS的len属性就能得到字符串的长度,复杂度为O(1)。

2、杜绝缓冲区溢出:

Redis是C语言编写的,并没有方便的数据类型来进行内存的分配和释放(C++ STL String),必须手动进行内存分配和释放。

对于字符串的拼接、复制等操作,C语言开发者必须确保目标字符串的空间足够大,不然就会出现溢出的情况。

当使用SDS的API对字符串进行修改的时候,

  • API内部第一步会检测字符串的大小是否满足。
  • 如果空间已经满足要求,那么就像C语言一样操作即可。如果不满足,则拓展buf的空间
  • 之后再进行操作。每次操作之后,len和free的值会做相应的修改。
1
2
3
4
5
扩展buf空间策略: 

修改之后总长度len<1MB: 总空间为2*len+1;
修改之后总长度len>=1MB: 总空间为len+1MB+1。
换句话说,预分配的空间上限是1MB,尽量为len。

3、减少修改字符串时带来的内存重分配次数

当执行字符串长度缩短的操作的时候,SDS并不直接重新分配多出来的字节,而是修改len和free的值(len相应减小,free相应增大,buf的空间大小不变化),避免内存重分配。

SDS也提供直接释放未使用空间的API,在需要的时候,也能真正的释放掉多余的空间。

4、二进制安全

C字符串除了末尾之外不能出现空字符,否则会被程序认为是字符串的结尾。这就使得C字符串只能存储文本数据,而不能保存图像,音频等二进制数据

使用SDS就不需要依赖控制符,而是用len来指定存储数据的大小,所有的SDS API都会以处理二进制的方式来处理SDS的buf的数据。程序不会对buf的数据做任何限制、过滤或假设,数据写入的时候是什么,读取的时候依然不变。

总结

C字符串 SDS
获取字符串长度的复杂度为O(N) 获取字符串长度的复杂度为O(1)
API是不安全的,可能会造成缓冲区溢出 API是安全的,不会造成缓冲区溢出
修改字符串长度N次必然需要执行N次内存重分配 修改字符串长度N次最多需要执行N次内存重分配
只能保存文本数据 可以保存文本或者二进制数据
可以使用所有库中的函数 可以使用一部分库的函数

String应用场景

  1. Session + Redis 实现Session共享
  2. 做计数器,计算文档浏览量。INCR article:readcount:{文章id} GET article:readcount:{文章id}
  3. 分布式锁
1
2
3
线程1:SETNX product:10001 true//返回1代表锁获取成功

线程2:SETNX product:10001 true//返回0代表所获取失败

为什么不用hash存储呢?

如果想获取一个对象数据(user1)的name,那么需要把user1的所有数据拿出,在单独获取name;如果用STRING格式,那么只需要取出user:1:name数据即可。

常用命令的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# redis数据写命令Set,相当于数据插入
127.0.0.1:6379> set name zlh
# 返回值: ok,说明插入成功。如果当前name存在值则覆盖替换原有的value值。

# redis数据读命令Get,获取数据
127.0.0.1:6379> get name
# 返回值: "zlh" ,如果当前key没有value值,则返回null

# redis数据追加命令Append,追加数据
127.0.0.1:6379> append name ' is my friend'
# 返回值:"zlh is my friend" ,如果当前key的value有值则附加到原有string后面,如果没有则写入。

# redis数据删除
127.0.0.1:6379>del name


# redis数据读写操作命令GetSet,获取原有value值的同时写入新的value值

127.0.0.1:6379> getset name zlh
# 返回值:"zlh is my friend",这里返回的是原有name的value值,同时又给name的value设置新值zlh。
# 此时name的值真实值为zlh

127.0.0.1:6379> get name
# 返回值:zlh ,因为上面getset给name的value设置值为zlh。



# redis数据加法计算命令incr,incrby,
# 数据加法运算,incr为+1内置运算,incrby为+n自设n运算

127.0.0.1:6379> incr name
# -返回值:“数据不是整型或数据超出64位有符号整形数据范围” ,由于原有name的value为"zlh",所以不能转换为整型,故报异常。

127.0.0.1:6379> incr age
# # 返回值:1,由于不存在age的key与value值,但是默认age为key值为0进行+1运算。

127.0.0.1:6379> incr age
# -返回值:2,由于上一行代码给age赋值为1,这里incr命令进行+1运算,故返回值为2。

127.0.0.1:6379> incrby age 10
# -返回值:12 ,因为原有age是2,这里+10故为12。


# redis数据减法计算命令decr,decrby,
# 数据减法运算,decr为-1内置运算,decrby为减n自设n运算

127.0.0.1:6379> decr name
# 返回值:“数据不是整型或数据超出64位有符号整形数据范围” ,由于原有name的value为"zlh",所以不能转换为整型,故报异常。

127.0.0.1:6379> decr age
# 返回自:11,因为原有age的value值为12,这里decr是自减1的意思,故为11。

127.0.0.1:6379> decrby age 10
# 返回值:1,因为原有age为11,这里-10,故为1。



# redis获取string长度的命令strlen

127.0.0.1:6379> strlen name
# 返回值:3,因为name的value值为zlh,故长度为3,如果该key或者value不存在,则返回0。

# redis设置value值并设置过期时间命令setex(单位秒)

127.0.0.1:6379> setex sex 20 male
# -返回值:ok,设置key为sex的value值为male,缓存的过期时间为20s。

127.0.0.1:6379> ddl sex
# -返回值: 剩余过期时间,0为已过期,-1为永不过期。

127.0.0.1:6379> get sex
# -返回值:male,说明此时为没有过期,当已经过期此处返回数据为null。



# redis赋值判断原值是否存在,存在不赋值,返回0;不存在才赋值,返回1;命令setnx

127.0.0.1:6379> setnx name Tom
# -返回值:0,因为name的原有value为zlh,存在值则不赋值。

127.0.0.1:6379> gete name
# -返回值:zlh,因为有值,故上面赋值为tom失败,返回0。

127.0.0.1:6379> setnx phone 18501733702
# -返回值:1,赋值成功,因为原来不存在phone的key与value。

127.0.0.1:6379> get phone
# -返回值:18501733702,说明上面的setnx赋值成功。



# redis字符串替换赋值,从指定位置开始替换,命令setrange

127.0.0.1:6379> setrange phone 9 123
# 返回值:12,12为字符串长度,11位号码变成了12位。因为从第9位开始替换,替换到原有字符串的最后一位还没有替换完毕,所以在最后面添加啦一位设置为3。

127.0.0.1:6379> get phone
# -返回值为:185017337123

127.0.0.1:6379> set phone 1
# -这里为了下面说下setrange的其他特性,把电话设置为1.

127.0.0.1:6379> setrange phone 3 aaa
# -返回值为:6,因为原有phone的value值为1,不到三位,用0*00替换2位,所以要添加2为才能替换第三位后面的值为aaa。

127.0.0.1:6379> get phone
# -返回值:1\*00\*00aaa。理解下上面的操作就知道这里为什么是这样的返回值了



# redis截取字符串,从下标为n开始截取到n或n+1,类似c#中的substring,命令getrange

127.0.0.1:6379> set phone 18511112222
# 方便下面操作

127.0.0.1:6379> getrange phone 1 5
# -返回值:85017,因为getrange是从下标为1开始截取截取到下标为5,这里包含下标为5的值。

127.0.0.1:6379> getrange phone 0 0
# -返回值:1,从下标为0开始截取,截取到下标为0,包含下标为0。故返回值为1。

127.0.0.1:6379> getrange phone 10 13
# 返回值为:1,此号码下标直到10的11位号码,从10开始截取,截到第13位,后2位不存在忽略,只返回第10位。故返回干净的1。



# redis批量操作修改及读取string数据,
# 命令mget,批量读取,命令mset,批量赋值,
# 命令msetnx,带事务性的赋值,发现有一个key已经存在,所有事务回归,不做赋值处理操作

127.0.0.1:6379> mset name zlh age 30
# -返回值:ok,这里设置了 key为name和age的value分别为zlh,30

127.0.0.1:6379> mget name zlh
# 返回值:1>zlh 2>30

127.0.0.1:6379> msetnx name Jim address China
# -返回值为:0,没有做任何修改,因为key(name)已存在。

127.0.0.1:6379> mget name address
# -返回值:1>zlh 2>null
# -这里看到adress空null,说明 mestnx 事物回归

127.0.0.1:6379> msetnx address China,hobbies sports
# 返回值为:1,插入成功

127.0.0.1:6379> mget address hobbies
# -返回值:1>China 2>sports