字符串 string 是Redis 最简单的数据结构。Redis 所有的数据结构都是以唯一的 key 字符串作为名称,然后通过这个唯一 key 值来获取相应的 value 数据。
1.常用命令
1.增(set)
set key value // 存入字符串键值对
mset key value [key value...] // 批量存储字符串键值对
setnx key value // 存入一个不存在的键值对(只有当前key不存在时才能增加成功)
2.删除与过期
del key [key...] // 删除一个键
expire key seconds // 设置过期时间(秒)
3.原子操作
incr key // 将key中存储的数字加1
incrby key increment // 将key锁存储的值加上increment
decr key // 将key中存储的数字减1
decr key decrement // 将key所存储的值减去decrement
4.查(get)
get key // 获取一个字符串的键值
mget key [key...] // 批量获取字符串键值
2.应用示例
1.对象缓存
- 方式一:利用set命令,一次将对象的json字符串存入
set user:1 value(对象的json字符串)
- 方式二:利用mset将对象的各个字段分开存储。这种方案更加灵活,可以对指定字段进行操作
mset user:1:name zhangsan user:1:age 18
mget user:1:name ~~ user:1:age ~~
2.简单分布式锁
通过setnx只能新增不存在的可以进行简单的分布式锁
PS:分布式锁问题就是放大版的单机线程同步问题,这不过这里不再是一个个线程,而是一台台独立的服务器(进程)。
setnx product true // 进程一设置成功 == 获取到锁
setnx product true // 进程二再来时设置失败 == 阻塞
...... // 操作临界资源
del product // 进程一释放锁
注意:一般最好设置过期时间来避免死锁
set product true ex 10 nx // 过期时间=10s
3.计数器
incr article:readcount:{文章id}
get article:readcount:{文章id}
4.Web集群Session共享
spring session + redis 实现session共享
3.存储原理
字符串类型的内部编码有三种:
- int:存储 8 个字节的长整型(long,2^63-1)
- embstr:代表 embstr 格式的 SDS, 存储小于 44 个字节的字符串
- raw:代表 raw 格式的 SDS,存储大于 44 个字节的字符串(3.2 版本之前是 39 字节)
Redis 使用 C 语言编写,但是并没有直接使用 C 语言自带的字符串,而是使用了 SDS 来管理字符串。SDS 全称 Simple Dynamic String,即简单动态字符串。SDS 组成部分如下:
/* sds.h */
struct __attribute__ ((__packed__)) sdshdr8 {
/* 当前字符数组的长度 */
uint8_t len;
/* 表示 buf 中的空闲的空间大小 */
uint8_t free;
/* 当前字符数组的属性、用来标识到底是 sdshdr8 还是 sdshdr16 等 */
unsigned char flags;
/* char 类型的数组,用于存储实际字符串的内容 */
char buf[];
};
注:在 3.2 以后的版本中,SDS 又有多种结构(sds.h):sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,用于存储不同的长度的字符串,分别代表
2^5=32byte, 2^8=256byte,2^16=65536byte=64KB,2^32byte=4GB
。
来看 SDS 的简单示意图:
问题一:为什么 Redis 要用 SDS 实现字符串?
我们知道,C 语言本身没有字符串类型(只能用字符数组 char[]实现)
- 使用字符数组必须先给目标变量分配足够的空间,否则可能会溢出。
- 如果要获取字符长度,必须遍历字符数组,时间复杂度是 O(n)。
- C 字符串长度的变更会对字符数组做内存重分配。
- 通过从字符串开始到结尾碰到的第一个’\0’来标记字符串的结束,因此不能保存图片、音频、视频、压缩文件等二进制(bytes)保存的内容,二进制不安全。
SDS 的特点:
- 不用担心内存溢出问题,如果需要会对 SDS 进行扩容。
- 获取字符串长度时间复杂度为 O(1),因为定义了 len 属性。
- 通过“空间预分配”( sdsMakeRoomFor)和“惰性空间释放”,防止多次重分配内存。
- 判断是否结束的标志是 len 属性(它同样以’\0’结尾是因为这样就可以使用 C 语言中函数库操作字符串的函数了),可以包含’\0’。
空间预分配:;SDS 会预先分配一部分空闲空间,当字符串内容添加时不需要做空间申请的工作
空间惰性释放:当字符串从 buf 数组中移除时,空闲出来的空间不会立马被内存回收,防止新增字符串的内容写入时空间不够而临时申请空间
C 字符串 | SDS |
获取字符串长度的复杂度为 O(N) | 获取字符串长度的复杂度为 O(1) |
API 是不安全的,可能会造成缓冲区溢出 | API 是安全的,不会早晨个缓冲区溢出 |
修改字符串长度 N 次必然需要执行 N 次内存重分配 | 修改字符串长度 N 次最多需要执行 N 次内存重分配 |
只能保存文本数据 | 可以保存文本或者二进制数据 |
可以使用所有库中的函数 | 可以使用一部分库中的函数 |
问题二: embstr 和 raw 的区别?
embstr 的使用只分配一次内存空间(因为 RedisObject 和 SDS 是连续的),而 raw 需要分配两次内存空间(分别为 RedisObject 和 SDS 分配空间)。
因此与 raw 相比,embstr 的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。 而 embstr 的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个 RedisObject 和 SDS 都需要重新分配空间,因此 Redis 中的 embstr 实现为只读。
问题三:int 和 embstr 什么时候转化为 raw?
当 int 数 据 不 再 是 整 数 , 或大小超过了 long 的范围 (2^63-1=9223372036854775807)时,自动转化为 embstr。
127.0.0.1:6379> set k1 1
OK
127.0.0.1:6379> append k1 a
(integer) 2
127.0.0.1:6379> object encoding k1
"raw"
问题四:明明没有超过阈值,为什么变成 raw?
127.0.0.1:6379> set k2 a
OK
127.0.0.1:6379> object encoding k2
"embstr"
127.0.0.1:6379> append k2 b
(integer) 2
127.0.0.1:6379> object encoding k2
"raw"
对于 embstr,由于其实现是只读的,因此在对 embstr 对象进行修改时,都会先转化为 raw 再进行修改。 因此,只要是修改 embstr 对象,修改后的对象一定是 raw 的,无论是否达到了 44 个字节。
问题五:当长度小于阈值时,会还原吗?
关于 Redis 内部编码的转换,都符合以下规律:编码转换在 Redis 写入数据时完成,且转换过程不可逆,只能从小内存编码向大内存编码转换(但是不包括重新 set)