Redis有丰富的数据类型,常见的有五种: String(字符串)、hash(哈希)、List(列表)、Set(集合)、ZSet(有序集合) 随着Redis的版本不断更新,也出现了几种新的数据类型:BitMap(位图)、HyperLogLog(基数统计)、GEO(地理位置)
String
String是基本的key-value结构,key是唯一的标识,value可以是字符串(String)也可以是数字(int、double等)。value最多可容纳的数据长度为512M 。
Redis的字符串类型使用十分的广泛,例如:
arduino
1.作缓存 在用户登录后,存储用户登录的信息;大量数据插入数据库时因为要进行IO操作,需要较长的等待时间,可以先将这些数据写入缓存,后面再写入到DB中。
2.计数器 像我们可以限制1min内,用户只能调用api 10次,用户调用一次+1,到了10次后限制用户调用接口,等限制的周期过了,在重新刷新用户调用次数。(这里我们可以使用**incr key**来完成自增)
3.分布式锁 在多线程中,多个线程同时对一个资源进行操作的时候,我们一般会用**synchronized**来保证同一时刻只有一个线程在操作资源。但是在分布式中,各个服务都是在不同的服务器上部署的,显而易见,我们原来的办法已经是行不通了,因此我们要使用分布式锁。
String类型常用命令
1.set set的基本语法为 set key value
2.setnx(key如果不存在才会set,如果存在不会进行任何操作) setnx的基本语法为:setnx key value
这张图中我们先set了key为age,value为18的数据,后面我们又set了key为age,value为20,但通过get age我们可以看出age的value并没有被覆盖,因此setnx不会对已存在的key进行覆盖。
3.setex setex的基本语法为:setex key seconds value
这个命令与下面的两个命令有些类似:
bash
set key value #设置值
expire key seconds # 设置过期时间
不同的地方是,setex命令具有原子性,即设置值和设置过期时间的命令会同时完成。 注意,setex命令会对key的value进行覆盖
4.mset mset的基本语法为:meset key1 value1 key2 value2 可以一次性设置多个key的值,mset命令执行成功后会返回OK,表示所有值都被设置;执行失败就会返回0,表示没有值被设置。mset命令是一个原子操作,即所有key要么都成功的设置了值,要么都没有设置成功。
5.msetnx mset的基本语法为:mesetnx key1 value1 key2 value2 可以一次性设置多个key的值,但是如果key存在就会导致所有key都设置失败。因为msetnx命令是一个原子操作,即所有key要么都成功的设置了值,要么都没有设置成功。
我们执行了msetnx命令后,age2的值并未变化,age3的值为空。因为msetnx age2失败了,因此age3也没设置值成功。
6.append append的基本语法为:append key value 如果key不存在,就类似set key value,新增key。
如果key存在,就拼在value的末尾
7.get get的基本语法为:get key 如果key不存在则返回nil,如果key存在,返回的是value值,如果value存储的不是字符串的值返回错误。
为name的key不存在,返回nil
为name的key存在,返回对应的value。
8.mget mget的基本语法为:mget key1 key2
9.getset 基本语法为:getset key value 将key的value设置为新的value,并返回旧的value。
如果key不存在,则会生成一个key。返回值为nil。
、 如果key存在,则返回旧的value,并设置新的value
10.strlen strlen的基本语法:strlen key strlen返回的是key所储存字符串的长度
如果key不存在会返回0
11.incr incr的基本语法为:incr key 执行incr后,会将key所对应的value值+1
如果key所存储的不是数字,则会报错
如果key不存在,则会创建新的key,并且这个key的value为0,然后执行incr后,数值+1
12.decr decr的基本语法为:decr key 其用法与incr差不多,incr是自增,而decr是自减 执行decr后,会将key所对应的value值-1
String的内部结构
String类型底层的数据结构主要使用的是int 、SDS(简单动态字符串) 这里的SDS 与C语言的字符串并不相同,那他们有何不同呢,我们接着往下看。
我们先来看C语言的字符串 在C语言中,我们可以这样去定义字符串char * s = "hello"。 字符串"hello"的数组结构如下,数组的最后一个字符为'\0',代表字符串的结束。
字符串常量是一个字符数组,但是内容和长度在初始化时就已经固定了,不可更改;可以通过一个指向字符串常量第一个元素的指针来访问该字符串常量。字符数组的最后一位是'\0'他表示字符串的结束。 C 语言标准库 string.h
中的字符串有以下几点不足:
perl
1.C 语言使用 `char*` 字符串数组来实现字符串,在创建字符串的时候就要需要手动检查和分配字符串空间。没有length去记录字符串的长度,想获取length的长度只能从头到尾遍历获取长度。
2.'\0'在C语言代表字符串的结束,'\0'不能存储,像图片等二进制数据是没有办法进行存储的。
3.char数组的长度在创建字符串的时候已经确定下来,无法改变,如果想要追加数据要重新申请一块空间,并将追加后的字符串内容拷贝进去,在释放旧的空间,十分消耗资源。
那么SDS 是如何解决C语言字符串这些问题的呢? 我们先来看一下SDS的结构
c
struct sdshdr {
// buf中已使用的长度
int len;
// buf中未使用的长度
int free;
// 数据空间
char buf[];
};
简单介绍下这几个参数
c
len:SDS保存的字符串的长度,buf里已经使用的长度。
free:buf数组中未使用的字节数
buf[]:保存字符串中每个字符(为了保证字节数组的结束,redis会自动在数组最后加个'\0')
以"hello"为例子 相比于C语言的字符串,SDS新增了len,free属性,SDS获取字符串的长度时不用像C语言那样需要从头到尾去遍历,所以C语言获取字符串长度的时间复杂度是O(N),SDS可以通过读取len的长度获取字符串的长度,所以SDS获取字符串长度的时间复杂度是O(1)。
在C语言中,拼接字符串要使用strcat函数,如果分配的内存空间不够,会造成缓冲区溢出。SDS在进行拼接的时候会先通过free来判断内存空间是否足够,如果不够会进行扩容,因此不会出现缓冲区溢出。
C语言在修改字符串的时候因为没有记录len的长度,在修改的时候要重新分配内存。SDS在修改字符串的时候,因为有len和free属性,所以会使用空间预分配 和惰性空间释放两种策略。
c
空间预分配:字符串在进行空间扩展时,扩展的内存比实际需要的多,可以减少连续执行字符串增长所需的内存分配次数
惰性空间释放:对字符串进行缩短操作时,程序不会立即回收缩短后多余的字节,而是使用free属性记录多余的字节数,等待后续使用(SDS提供了相应的API,用于手动释放未使用的空间)。
C语言中字符串以空字符串作为字符串结束的标识,对于可能包含空字符串的二进制文件(如:图片,压缩文件等)无法正确存取。SDS是以二进制方式处理buf数组中元素,不是以空字符串作为结束标识,而是以len属性表示的长度来判断字符串是否结束。
总结
- SDS 不仅可以保存文本数据,还可以保存二进制数据 。因为
SDS
使用len
属性的值而不是空字符来判断字符串是否结束,并且 SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在buf[]
数组里的数据。所以 SDS 不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。 - SDS 获取字符串长度的时间复杂度是 O(1) 。因为 C 语言的字符串并不记录自身长度,所以获取长度的复杂度为 O(n);而 SDS 结构里用
len
属性记录了字符串长度,所以复杂度为O(1)
。 - Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题
字符串对象的内部编码有三种:int 、raw 、embstr 。 如果一个字符串对象里面保存的数字,且这个整数值可以使用long 来表示,那么字符串对象会将整数保存在字符串对象结构的ptr属性中并将字符串对象的编码改成int
如果字符串对象保存的是一个字符串,并且这个字符申的长度<= 32 字节,那么字符串对象将使用SDS来保存这个字符串,并将对象的编码设置为embstr 。embstr 编码是专门用于保存短字符串的一种优化编码方式 如果字符串对象保存的是一个字符串,并且这个字符串的长度> 32 字节,那么字符串对象将使用SDS 来保存这个字符串,并将对象的编码设置为raw embstr 和raw 都是用SDS来保存字符串对象,但不同之处在于embstr 会通过一次内存分配函数来分配一块连续的内存空间来保存redisObject 和SDS ,而raw 编码会通过调用两次内存分配函数来分别分配两块空间来保存redisObject 和SDS 那redis这样做的好处是什么呢
- embstr相比raw,创建的时候会少分配一次空间,删除时会少释放一次空间
- 因为embstr的所有数据都是连续空间,所以会方便查询 但是embstr 也有不好的地方: 如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,所以embstr编码的字符串对象实际上是只读的。